Vue的双向绑定是其一大特点,但其实双向数据绑定无非就是在单向绑定的基础上给可输入元素(input、textare等)添加了change(input)事件,来动态修改model和 view。因此单向绑定和双向绑定并没有太大区别,这里就来探究一下它数据绑定的奥秘。
响应式原理
Vue 最独特的特性之一,是其非侵入性的响应式系统。数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。这使得状态管理非常简单直接。
Vue.js的响应式原理依赖于 Object.defineProperty,Vue.js官方文档中就已经提到过,这也是Vue.js不支持IE8 以及更低版本浏览器的原因。Vue通过设定对象属性的 setter/getter 方法来监听数据的变化,通过getter进行依赖收集,而每个setter方法就是一个观察者,在数据变更的时候通知订阅者更新视图。
下面先用一些简单的例子来讲述实现的原理。
可观察的数据(observable data)
1 | function observe(value, cb) { |
不考虑数组等情况,代码如上所示。在initData
中会调用observe
这个函数将Vue的数据设置成observable的。当_data数据发生改变的时候就会触发set
,对订阅者进行回调(在这里是render)。数据成为可观察的之后,它的getter和setter都被重写了,如有变动则可以拿到最新的值并通知订阅者。
但是需要对app._data.text操作才会触发set。为了能通过app.text直接操作,就要用到代理了。
代理
我们可以在Vue的构造函数constructor中为data执行一个代理proxy。这样我们就把data上面的属性代理到了vm实例上。
1 | _proxy.call(this, options.data);/*构造函数中*/ |
我们就可以用app.text代替app._data.text了。
依赖收集
按照上面的代码,只要调用了set方法,就会让视图重新渲染。但是如果一个data在实际模板中并没有被用到,那么就不应该重新渲染视图。当对data上的对象进行修改值的时候会触发它的setter,那么取值的时候自然就会触发getter事件,所以我们只要在最开始进行一次render,那么所有被渲染所依赖的data中的数据就会被getter收集到Dep的subs中去。在对data中的数据进行修改的时候setter只会触发Dep的subs的函数。
定义一个依赖收集类Dep。
1 | class Dep { |
Watcher
订阅者,当依赖收集的时候会addSub到sub中,在修改data中数据的时候会触发dep对象的notify,通知所有Watcher对象去修改对应视图。Watcher通过dep能够订阅并收到每个监听属性变动的通知,执行指令绑定的相应回调函数,从而更新视图
1 | class Watcher { |
开始依赖收集
1 | class Vue { |
将观察者Watcher实例赋值给全局的Dep.target,然后触发render操作只有被Dep.target标记过的才会进行依赖收集。有Dep.target的对象会将Watcher的实例push到subs中,在对象被修改出发setter操作的时候dep会调用subs中的Watcher实例的update方法进行渲染。
从源码看数据绑定原理
前面已经讲过Vue数据绑定的原理了,现在从源码来看一下数据绑定在Vue中是如何实现的。
首先看一下Vue.js官网介绍响应式原理的这张图。
这张图比较清晰地展示了整个流程,首先通过一次渲染操作触发Data的getter(这里保证只有视图中需要被用到的data才会触发getter)进行依赖收集,这时候其实Watcher与data可以看成一种被绑定的状态(实际上是data的闭包中有一个Deps订阅者,在修改的时候会通知所有的Watcher观察者),在data发生变化的时候会触发它的setter,setter通知Watcher,Watcher进行回调通知组件重新渲染的函数,之后根据diff算法来决定是否发生视图的更新。因此,当一个data没有在实际模板中用到,那么它将没有Watcher,也就不会渲染视图了。
Vue在初始化组件数据时,在生命周期的beforeCreate与created钩子函数之间实现了对data、props、computed、methods、events以及watch的处理。
initData
initData主要是初始化data中的数据,将数据进行Observer,监听数据的变化,其他的监视原理一致,这里以data为例。
1 | function initData (vm: Component) { |
其实这段代码主要做了两件事,一是将_data上面的数据代理到vm上,另一件事通过observe将所有数据变成observable。
proxy
接下来看一下proxy代理。
1 | /*添加代理*/ |
这里比较好理解,通过proxy函数将data上面的数据代理到vm上,这样就可以用app.text代替app._data.text了。
observe
接下来是observe,这个函数定义在core文件下observer的index.js文件中。
1 | /** |
Vue的响应式数据都会有一个ob的属性作为标记,里面存放了该属性的观察器,也就是Observer的实例,防止重复绑定。
Observer
接下来看一下新建的Observer。Observer的作用就是遍历对象的所有属性将其进行双向绑定。
1 |
|
Observer为数据加上响应式属性进行双向绑定。如果是对象则进行深度遍历,为每一个子对象都绑定上方法,如果是数组则为每一个成员都绑定上方法。
如果是修改一个数组的成员,该成员是一个对象,那只需要递归对数组的成员进行双向绑定即可。但这时候出现了一个问题,?如果我们进行pop、push等操作的时候,push进去的对象根本没有进行过双向绑定,更别说pop了,那么我们如何监听数组的这些变化呢? Vue.js提供的方法是重写push、pop、shift、unshift、splice、sort、reverse这七个数组方法。修改数组原型方法的代码可以参考observer/array.js以及observer/index.js。
1 | export class Observer { |
1 | /* |
从数组的原型新建一个Object.create(arrayProto)对象,通过修改此原型可以保证原生数组方法不被污染。如果当前浏览器支持proto这个属性的话就可以直接覆盖该属性则使数组对象具有了重写后的数组方法。如果没有该属性的浏览器,则必须通过遍历def所有需要重写的数组方法,这种方法效率较低,所以优先使用第一种。
在保证不污染不覆盖数组原生方法添加监听,主要做了两个操作,第一是通知所有注册的观察者进行响应式处理,第二是如果是添加成员的操作,需要对新成员进行observe。
但是修改了数组的原生方法以后我们还是没法像原生数组一样直接通过数组的下标或者设置length来修改数组,可以通过Vue.set以及splice方法。
Watcher
Watcher是一个观察者对象。依赖收集以后Watcher对象会被保存在Deps中,数据变动的时候会由Deps通知Watcher实例,然后由Watcher实例回调cb进行视图的更新。
1 | export default class Watcher { |
Dep
来看看Dep类。其实Dep就是一个发布者,可以订阅多个观察者,依赖收集之后Deps中会存在一个或多个Watcher对象,在数据变更的时候通知所有的Watcher。
1 | /** |
defineReactive
接下来是defineReactive。defineReactive的作用是通过Object.defineProperty为数据定义上getter\setter方法,进行依赖收集后闭包中的Deps会存放Watcher对象。触发setter改变数据的时候会通知Deps订阅者通知所有的Watcher观察者对象进行试图的更新。
1 | /** |