深入理解 vue2.x 双向数据绑定原理
前言
在面试中会经常问到 vue2.x 双向数据绑定是怎么实现的,大多数面试者都会回答Object.defineProperty()
方法对属性进行拦截,把data
中的每个数据的读写都转化成getter/setter
,当数据发生变化时候通知试图进行更新。本文将详细论述双向数据绑定的原理是怎么实现。
MVVM 模式
简单来说就是:
页面数据变化——>data 中同步变化
data 中数据变化——>文本节点内容同步变化
页面中数据改变,我们可以用过事件监听得到,比如input
框中数据改变,我们可以通过oninput
监听,但是 data 中数据的改变怎么更新到视图层呢?data 中数据改变可以通过Object.defineProperty()
对属性设置 set 属性,所以我们需要在 set 中添加一些更新数据的方法,就能够实现 data 变化更新视图。实现双向数据绑定,首先需要对数据劫持监听,因此设置一个监听器Observer
来监听数据。属性数据发生变化需要告诉订阅者Watcher
是否需要更新数据。订阅者可能是多个(多个属性发生变化),所以就需要一个消息订阅器Dep
收集这些订阅者,Observer 和 Watcher 通过 Dep 进行统一管理。我们能监听数据是怎么改变,但是如何更新呢。我们就需要一个指令解析器 Compile,对每个节点元素进行扫描和解析,将相关指令对应初始化成一个订阅者 Watcher,并替换模板数据或者绑定相应的函数,此时当订阅者 Watcher 接收到相应属性的变化,就会执行对应的更新函数,从而更新视图。因此我们需要四个操作:
实现一个监听器 Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者。
实现一个订阅器 Dep,用来收集订阅者,对监听器 Observer 和 订阅者 Watcher 进行统一管理
实现一个订阅者 Watcher,可以收到属性的变化通知并执行相应的函数,从而更新视图。
实现一个解析器 Compile,可以扫描和解析每个节点的相关指令,并根据初始化模板数据以及初始化相应的订阅器。
监听器 Observer
以下我们对数据可以实现了监听。如果多个属性就需要遍历属性监听。
我们需要一个容器能够放置这些订阅者,若有属性变化则对应的订阅者执行更新的函数。将上面代码更改,植入一个消息订阅者。
从代码上看,我们设计了一个订阅器 Dep 类,该类里面定义了一些属性和方法,这里需要特别注意的是它有一个静态属性 Dep.target,这是一个全局唯一 的 Watcher,因为在同一时间只能有一个全局的 Watcher 被计算
。我们还将订阅器 Dep 添加一个订阅者设计在 getter 里面,这是为了让 Watcher 初始化进行触发,因此需要判断是否要添加订阅者,至于具体设计方案,下文会详细说明的。在 setter 函数里面,如果数据变化,就会去通知所有订阅者,订阅者们就会去执行对应的更新的函数。到此为止,一个比较完整 Observer 已经实现了,接下来我们开始设计 Watcher。
订阅者 Watcher
订阅者 Watcher 在初始化的时候需要将自己添加进订阅器 Dep 中,那该如何添加呢?我们已经知道监听器 Observer 是在 get 函数执行了添加订阅者 Wather 的操作的,所以我们只要在订阅者 Watcher 初始化的时候触发对应的 get 函数去执行添加订阅者操作即可,那要如何触发 get 的函数,只要获取对应的属性值就可以触发了,核心原因就是因为我们使用了 Object.defineProperty( ) 进行数据监听。这里还有一个细节点需要处理,我们只要在订阅者 Watcher 初始化的时候才需要添加订阅者
,所以需要做一个判断操作,因此可以在订阅器上做一下手脚:在 Dep.target 上缓存下订阅者,添加成功后再将其去掉就可以了。订阅者 Watcher 的实现如下:
订阅者 Watcher 中的参数:
cb:更新函数
vm:vue 实例
exp:node 节点指令的属性值。比如
v-mode="data"
,exp 就是 data。
当我们去实例化一个渲染 watcher 的时候,首先进入 watcher 的构造函数逻辑,就会执行它的 this.get() 方法,进入 get 函数,首先会执行:
Dep.target = this;
将自己赋值为全局的订阅者 复制代码实际上就是把 Dep.target 赋值为当前的渲染 watcher ,接着又执行let value =this.vm.data[this.exp]
强制执行监听器里的 get 函数 复制代码在这个过程中会对 vm 上的数据访问,其实就是为了触发数据对象的 getter。 每个对象值的 getter 都持有一个 dep,在触发 getter 的时候会调用 dep.depend() 方法,也就会执行 this.addSub(Dep.target),即把当前的 watcher 订阅到这个数据持有的 dep 的 watchers 中,这个目的是为后续数据变化时候能通知到哪些 watchers 做准备。这样实际上已经完成了一个依赖收集的过程。那么到这里就结束了吗?其实并没有,完成依赖收集后,还需要把 Dep.target 恢复成上一个状态,即:Dep.target = null;
释放自己 复制代码而 update() 函数是用来当数据发生变化时调 Watcher 自身的更新函数进行更新的操作。先通过 let value = this.vm.data[this.exp],获取到最新的数据,然后将其与之前 get() 获得的旧数据进行比较,如果不一样,则调用更新函数 cb 进行更新。 至此,简单的订阅者 Watcher 设计完毕。
解析器 Compile
通过监听器 Observer 订阅器 Dep 和订阅者 Watcher 的实现,其实就已经实现了一个双向数据绑定的例子,但是整个过程都没有去解析 dom 节点,而是直接固定某个节点进行替换数据的,所以接下来需要实现一个解析器 Compile 来做解析和绑定工作。解析器 Compile 实现步骤:
解析模板指令,并替换模板数据,初始化视图;
将模板指令对应的节点绑定对应的更新函数,初始化相应的订阅器。
我们下面对 '{{变量}}' 这种形式的指令处理的关键代码进行分析,感受解析器 Compile 的处理逻辑,关键代码如下:
总结
从头撸到尾,还是很有收获的,参考了几位大佬的文章做了总结,收获满满。
版权声明: 本文为 InfoQ 作者【不叫猫先生】的原创文章。
原文链接:【http://xie.infoq.cn/article/3a35809a7ad953b40ece3c9c2】。文章转载请联系作者。
评论