Vue 采用数据劫持结合发布者-订阅者模式的方式来实现数据的响应式,经过Object.defineProperty来劫持数据的setter,getter,在数据变更时发布消息给订阅者,订阅者收到消息后进行相应的处理。html
要实现mvvm的双向绑定,就必需要实现如下几点:vue
Compile—指令解析系统,对每一个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数node
Observer—数据监听系统,可以对数据对象的全部属性进行监听,若有变更可拿到最新值并通知订阅者react
Dep+Watcher—发布订阅模型,做为链接Observer和Compile的桥梁,可以订阅并收到每一个属性变更的通知,执行指令绑定的相应回调函数,从而更新视图。git
Dep是发布订阅者模型中的发布者:get数据的时候,收集订阅者,触发Watcher的依赖收集;set数据时发布更新,通知Watcher 。一个Dep实例对应一个对象属性或一个被观察的对象,用来收集订阅者和在数据改变时,发布更新。github
Watcher是发布订阅者模型中的订阅者:订阅的数据改变时执行相应的回调函数(更新视图或表达式的值)。一个Watcher能够更新视图,如html模板中用到的{{test}},也能够执行一个$watch监督的表达式的回调函数(Vue实例中的watch项底层是调用的$watch实现的),还能够更新一个计算属性(即Vue实例中的computed项)。web
mvvm入口函数,整合以上三者,具体如图所示:算法
compire能够参看《双向绑定的实现原理》,这里不作过多解读。api
Observer,Dep和Watcher类的实现及原理,推荐阅读《Vue源码解读一:Vue数据响应式原理》,通常开发者须要关注:数组
Watcher,做用是分割表达式,收集依赖而且在值变化的时候调用回调函数。
咱们上面说过一个Dep对应着一个数据(这个数据多是:对象的属性、一个对象、一个数组);一个Watcher对应能够是一个模板也能够是一个$watch对应的表达式、函数等,不管那种状况,他们都依赖于data里面的数据,因此这里说的依赖其实就是模板或表达式所依赖的数据,对应着相关数据的Dep。
第一种:观察模板中的数据
第二种:观察建立Vue实例时watch选项里的数据
第三种:观察建立Vue实例时computed选项里的数据所依赖的数据
第四种:调用$watch api观察的数据或表达式
Watcher只有在这四种场景中,Watcher才会收集依赖,更新模板或表达式,不然,数据改变后,没法通知依赖这个数据的模板或表达式:
因此在解决数据改变,模板或表达式没有改变的问题时,能够这么作:
首先仔细看一看数据是否在上述四种应用场景中,以便确认数据已经收集依赖;其次查看改变数据的方式,肯定这种方式会使数据的改变被拦截(关于这一点,上面Obsever相关内容中说的比较多)。
当对象增删的时候,是监控不到的。好比:data={a:"a"},这个时候若是咱们设置data.test="test",这个时候是监控不到的。由于在observe data的时候,会遍历已有的每一个属性(好比a),添加getter/setter,然后面设置的test属性并无机会设置getter/setter,因此检测不到变化。一样的,删除对象属性的时候,getter/setter会跟着属性一块儿被删除掉,拦截不到变化。
vm.$set/Vue.set和vm.$delete/Vue.delete这样的api来解决这个问题
getter/setter是针对对象的对于数组的修改(push(),pop(),shift(),unshift(),splice(),sort(),reverse())等方法,arr发生了改变,此时是须要更新视图的,可是arr的getter/setter拦截不到变化(只有在赋值的时候才会调用setter,好比:arr=[6,7,8])。
对于这种状况,vue经过改写Array的默认方法,在调用这些方法的时候发布更新消息。通常无需关注,可是对于以下两种状况:
当你利用索引直接设置一个项时,例如:vm.items[indexOfItem] = newValue
当你修改数组的长度时,例如:vm.items.length = newLength
须要vm.$set/Vue.set和vm.items.splice(newLength)解决,具体参看官方说明
每次给数据设置值得时候,都会调用setter函数,这个时候就会发布属性更新消息,即便数据的值没有变。从性能方便考虑咱们确定但愿值没有变化的时候,不更新模板。(像Angular这样把批量操做延时到一次更新,一次作完全部数据变动,而后总体应用到界面上)
总体感知virtual DOM
virtual DOM分为三个步骤:
1.createElement(): 用 JavaScript对象(虚拟树) 描述 真实DOM对象(真实树)
2.diff(oldNode, newNode) : 对比新旧两个虚拟树的区别,收集差别
3.patch() : 将差别应用到真实DOM树
有的时候 第二步 可能与 第三步 合并成一步(Vue 中的patch就是这样)
Vue的实现原理总结
首先,在实例化的过程当中,把一个普通 JavaScript 对象传给 Vue 实例的 data选项,Vue 将遍历此对象全部的属性,并使用 Object.defineProperty 把这些属性所有转为 getter/setter。
Dep 是一个依赖收集器。data 下的每个属性都有一个惟一的 Dep 对象,在 get 中收集仅针对该属性的依赖,而后在 set 方法中触发全部收集的依赖。
在Watcher中对表达式求值,从而触发数据的get。在求值以前将当前Watch实例设置到全局,使用pushTarget(this)方法。
在get()中收集依赖,this.subs.push(sub),set的时候触发回调Dep.notify()。
Compile中首先将template或el编译成render函数,render函数返回一个虚拟DOM对象(将模板转为 render 函数的时候,实际是先生成的抽象语法树(AST),再将抽象语法树转成的 render 函数)
当 vm._render 执行的时候,所依赖的变量就会被求值,并被收集为依赖。按照Vue中 watcher.js 的逻辑,当依赖的变量有变化时不只仅回调函数被执行,实际上还要从新求值,即还要执行一遍
若是尚未 prevVnode 说明是首次渲染,直接建立真实DOM。若是已经有了 prevVnode 说明不是首次渲染,那么就采用 patch 算法进行必要的DOM操做。这就是Vue更新DOM的逻辑。
最后,安利下:《Vue.js 技术揭秘》
参考文章
文自《梳理vue双向绑定的实现原理 - vue入坑总结 - 周陆军的我的网站》,若有不妥以前,请源站留言告知。