Vue
实现双向绑定的原理就是通过数据劫持结合发布者-订阅者模式的方式来实现的。
但是数据劫持是什么?咱们先来看看Object.defineProperty()
开始之前先用原作者的代码和演示图给大家展示一下效果
下面我们由易到难一步步实现这个SelfVue
。
我们只实现简易版的vue
过程,主要包括双括号和v-model
和事件指令的功能。
Object.defineProperty()
首先我们在引入vue
文件之后在控制台打印一下vue
实例中的data
数据:
1 | var vm = new Vue({ |
可见这是一个拥有get
和set
方法的对象。因为Vue
是通过Object.defineProperty()
来实现数据劫持的。
三个参数
obj
:必须,目标对象prop
:必须,需定义或修改的属性的名字descriptor
:必须,目标属性所拥有的特性- 可以提供两种形式的设置:数据描述 & 存取器描述
该函数返回设置完毕后的参数对象
所以我们可以通过该属性设置它的存取器让其发扬我们自己的特性也就是常规操作啦
举个栗子
我们现在有一个对象Book
1 | var Book = { |
如果我们想要在执行console.log(book.name)
的同时,直接给书名加个书名号,或者说要通过什么监听对象Book
的属性值。这时候Object.defineProperty()
就派上用场了
1 | var Book = {} |
喏, 咱们现在的get
和set
就是自己定义的啦。
还记不记得刚才咱们打印的Vue
实例?现在咱们也打印一下这个对象(console.log(Book)
)
所以可以确定,Vue
确实是通过此种方式对数据进行劫持的
思路分析
双向绑定的思路就是两个方面:
- 数据变化更新视图
- 视图变化更新数据
前边咱们讲Object.defineProperty
的意思就是——我们可以通过改变数据来更新视图——只需要在相应的set
函数中添加对应的 DOM 操作就可以啦
我们只需要给要监听的对象(Watcher
)设置一个set
函数,当数据改变自然会触发。我们将更新所需的方法放在其中就可以实现啦
实现过程
我们所需要的流程如下
需要三个身份:
- 监听者(
Observer
):用来劫持并监听所有属性。若有变动则同之订阅者 - 订阅者(
Watcher
):可以收到属性的变化通知并执行相应的函数,从而更新视图 - 解析器(
Compile
):可以扫描和解析每个节点的相关指令,并根据初始化模板数据初始化相应的订阅器
Observer实现
之前我们提到了Observer
是一个数据监听器,其核心方法就是前文所述的Object.defineProperty()
。我们可以通过递归的方式遍历监听所有属性值,并对其用Object.defineProperty
处理相应操作
1 | function defineReactive(data, key, val) { |
订阅者(Watcher
)显然不止一个,所以我们需要有一个用来容纳订阅者的容器——消息订阅器(Dep
)。
Dep
主要负责收集订阅者,然后在属性变化之时执行相应Watcher
的更新函数
为此我们需要给Dep
一个容器——list
同时我们需要将Observer
改造一下,植入消息订阅器
1 | function defineReactive(data, key, val) { |
我们将订阅器Dep
添加订阅者的代码块放在getter
中,这是为了让Watcher
初始化进行触发,所以需要判断是否要添加订阅者。
而setter
则负责变化的数据通知给订阅者,令其执行相应的操作。当然,订阅者的update
函数咱们一会儿再实现。
Watcher实现
Watcher
在初始化的时候就需要将自己添加到订阅器Dep
中。
已知监听器(Observer
)是在get
函数添加了订阅者(Watcher
)之后工作的,所以我们只需要在订阅者(Watcher
)初始化的时候触发对应的get
函数区去执行添加订阅者操作即可
如何触发订阅者(Watcher
)的get
函数?
当然是获取其相应的属性值
另外还有一个细节点需要处理:我们只要在订阅者(Watcher
)初始化的时候才需要添加订阅者,所以可以在订阅器Dep
上做点手脚:在Dep.target
上缓存下订阅者,添加成功后再将其去掉
1 | function Watcher(vm, exp, cb) { |
此时为了兼容新加入的订阅者Watcher
我们需要给监听器Observer
做个微调:
设置缓存
1 | function defineReactive(data, key, val) { |
至此我们就可以进行一个简单的双向绑定数据啦。因为此处没有解析器Compile
所以对于模板数据我们进行写死处理。
1 | <body> |
将监听者Observer
和订阅者Watcher
关联起来:
1 | function SelfVue (data, el, exp) { |
然后在页面上new一下SelfVue
类,就可以实现数据的双向绑定了。我们在页面上试试看
1 | <body> |
此时打开页面,可以看到刚开始页面显示'hello world'
,2s之后变成了'canfoo'
。
但此处有一个问题:我们每次赋值时需要这样:seleVue.data.name = yourName
。为了让咱们体验好一点(更加趋近Vue
),我们可以在进行new SelfVue
时对其做一个代理,让访问selfVue
的属性代理访问selfVue.data
的属性
思路还是一样,用Object.defineProperty()
包装
1 | function SelfVue (data, el, exp) { |
Compile实现
上边虽然已经实现了双向绑定,但是本质上是个阉割版的,只能监听固定的节点。所以我们现在试着搞一个解析器Compile
来进行解析和绑定工作
实现步骤
- 解析模板指令,并替换模板数据,初始化视图
- 将模板指令对应的节点绑定对应的更新函数,初始化相应的订阅器
为了解析模板,首先需要获取到 DOM 元素,然后对 DOM 元素上含有指令的节点进行处理。为了避免对 DOM 的频繁操作,我们可以先建立一个fragment
片段,将需要解析的 DOM 节点存入fragment
片段再进行处理
1 | // 缓存DOM处理的fragment片段 |
接下来就是遍历各个节点,对含有相关指令的节点进行特殊处理。
双括号
1 | function compileElement (el) { |
获取到最外层节点后,调用compileElement
函数,对所有子节点进行判断。若节点为文本节点且匹配双括号,则这种形式的指令就开始进行编译处理。
- 编译处理首先需要初始化视图数据(解析模板指令,并替换模板数据,初始化视图)
- 接下来需要生成一个订阅者
Watcher
并绑定更新函数的订阅器(将模板指令对应的节点绑定对应的更新函数,初始化相应的订阅器)
为了将解析器Compile
与监听器Observer
和订阅者Watcher
关联起来,我们需要修改一下类SelfVue
函数
1 | function SelfVue (options) { |
更改后,我们就不要像之前通过传入固定的元素值进行双向绑定了,可以随便命名各种变量进行双向绑定
1 | <body> |
以上,我们可以观察到,刚开始title
和name
分别被初始化为hello world
和空,2s 后title
被替换为'你好'
,3s 后name
被替换为'canfoo'
。
此时我们已经完成了双向绑定的第一个功能:解析双括号
添加一个v-model
到此为止我们只是实现了解析器Compile
的其中一个基本的双向绑定功能,而现在我们准备向着更远的地方行进——完善更多指令的解析编译
至于方式嘛~很简单,我们继续在compileElement
函数上对其他指令节点进行判断,然后遍历其所有属性,看是否有匹配的指令的属性。若有则对其进行解析编译。
现在我们实现一个v-model
指令和事件指令的解析编译,对于这些节点我们使用compile
函数进行解析处理
1 | function compile (node) { |
compile
函数是挂在Compile
原型上的。它首先遍历所有的节点属性,再判断属性是否是指令属性。若是则再区分是哪种指令,然后再做相应处理。
最后稍微改造一下SelfVue
类,使其更趋近vue
的用法
1 | function SelfVue (options) { |
我们来试试看
1 | <body> |
结果如下
last
本文转载自博客园的canfoo
,原文地址
在此表示对作者深深的敬意。