本文同步发表在 https://github.com/whxaxes/blog/issues/7javascript
在一篇文章中简单讲了 vue 是如何把模板解析成 render function 的,这一篇文章就来说讲 vue 是如何把数据包装成 reactive,从而实现 MDV(Model-Driven-View) 的效果。vue
先说明一下什么叫 reactive,简单来讲,就是将数据包装成一种可观测的类型,当数据产生变动的时候,咱们可以感知到。java
而 Vue 的相关实现代码所有都在 core/observer
目录下,而要自行阅读的话,建议从 core/instance/index.js
中开始。node
在开始讲 reactive 的具体实现以前,先说说几个对象:Watcher、Dep、Observer。react
Watcher 是 vue 实现的一个用于观测数据的对象,具体实如今 core/observer/watcher.js
中。git
这个类主要是用来观察方法/表达式
中引用到的数据(数据须要是 reative 的,即 data 或者 props)变动,当变动后作出相应处理。先看一下实例化 Watcher 这个类须要传的入参有哪些:github
constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: Object )
能够看到,有四个入参可供选择,其中 options 是非必传的,解释一下这几个入参是干吗的:oop
vm
:当前这个 watcher 所属的 VueComponent。expOrFn
:须要监听的 方法/表达式。举个例子:VueComponent 的 render function,或者是 computed property 的 getter 方法,再或者是abc.bbc.aac
这种类型的字符串(因为 vue 的 parsePath 方法是用 split('.') 来作的属性分割,因此不支持abc['bbc']
)。expOrFn 若是是方法,则直接赋值给 watcher 的 getter 属性,若是是表达式,则会转换成方法再给 getter。cb
:当 getter 中引用到的 data 发生改变的时候,就会触发该回调。options
:额外参数,能够传入的参数为包括deep
、user
,lazy
,sync
,这些值默认都是为 false。
deep
若是为 true,会对 getter 返回的对象再作一次深度遍历,进行进一步的依赖收集,好比 $watch 一个对象,若是 deep 为 true,那么当这个对象里的元素更改,也会触发 callback。user
是用于标记这个监听是否由用户经过 $watch 调用的。lazy
用于标记 watcher 是否为懒执行,该属性是给 computed property 用的,当 data 中的值更改的时候,不会当即计算 getter 获取新的数值,而是给该 watcher 标记为 dirty,当该 computed property 被引用的时候才会执行从而返回新的 computed property,从而减小计算量。sync
则是表示当 data 中的值更改的时候,watcher 是否同步更新数据,若是是 true,就会当即更新数值,不然在 nextTick 中更新。其实,只要了解了入参是用来干吗的以后,也就基本上知道 Watcher 这个对象干了啥或者是须要干啥了。this
Dep 则是 vue 实现的一个处理依赖关系的对象,具体实如今 core/observer/dep.js
中,代码量至关少,很容易理解。lua
Dep 主要起到一个纽带的做用,就是链接 reactive data 与 watcher,每个 reactive data 的建立,都会随着建立一个 dep 实例。参见 observer/index.js 中的 defineReactive
方法,精简的 defineReactive 方法以下。
function defineReactive(obj, key, value) { const dep = new Dep(); Object.defineProperty(obj, key, { get() { if (Dep.target) { dep.depend(); } return value } set(newValue) { value = newValue; dep.notify(); } }) }
建立完 dep 实例后,就会在该 data 的 getter 中注入收集依赖的逻辑,同时在 setter 中注入数据变动广播的逻辑。
所以当 data 被引用的时候,就会执行 getter 中的依赖收集,而何时 data 会被引用呢?就是在 watcher 执行 watcher.getter 方法的时候,在执行 getter 以前 watcher 会被塞入 Dep.target,而后经过调用 dep.depend() 方法,这个数据的 dep 就和 watcher 建立了链接,执行 getter 完成以后再把 Dep.target 恢复成此前的 watcher。
建立链接以后,当 data 被更改,触发了 setter 逻辑。而后就能够经过 dep.notify() 通知到全部与 dep 建立了关联的 watcher。从而让各个 watcher 作出响应。
好比我 watch 了一个 data ,而且在一个 computed property 中引用了同一个 data。再同时,我在 template 中也有显式引用了这个 data,那么此时,这个 data 的 dep 里就关联了三个 watcher,一个是 render function 的 watcher,一个是 computed property 的 watcher,一个是用户本身调用 $watch 方法建立的 watcher。当 data 发生更改后,这个 data 的 dep 就会通知到这三个 watcher 作出相应处理。
Observer 能够将一个 plainObject 或者 array 变成 reactive 的。代码不多,就是遍历 plainObject 或者 array,对每个键值调用 defineReactive
方法。
以上三个类介绍完了,基本上对 vue reactive 的实现应该有个模糊的认识,接下来,就结合实例讲一下整个流程。
在 vue 实例化的时候,会先调用 initData,再调用 initComputed,最后再调用 mountComponent 建立 render function 的 watcher。从而完成一个 VueComponent 的数据 reactive 化。
initData 方法在 core/instance/state.js 中,而这个方法里大部分都是作一些判断,好比防止 data 里有跟 methods 里重复的命名之类的。核心其实就一行代码:
observe(data, true)
而这个 observe 方法干的事就是建立一个 Observer 对象,而 Observer 对象就像我上面说的,对 data 进行遍历,而且调用 defineReactive 方法。
就会使用 data 节点建立一个 Observer 对象,而后对 data 下的全部数据,依次进行 reactive 的处理,也就是调用 defineReactive
方法。当执行完 defineReactive 方法以后,data 里的每个属性,都被注入了 getter 以及 setter 逻辑,而且建立了 dep 对象。至此 initData 执行完毕。
而后是 initComputed 方法。这个方法就是处理 vue 中 computed 节点下的属性,遍历 computed 节点,获取 key 和 value,建立 watcher 对象,若是 value 是方法,实例化 watcher 的入参 expOrFn 则为 value,不然是 value.get。
function initComputed (vm: Component, computed: Object) { ... const watchers = vm._computedWatchers = Object.create(null) for (const key in computed) { const userDef = computed[key] let getter = typeof userDef === 'function' ? userDef : userDef.get ... watchers[key] = new Watcher(vm, getter, noop, { lazy: true }) if (!(key in vm)) { defineComputed(vm, key, userDef) } else if (process.env.NODE_ENV !== 'production') { ... } } }
咱们知道 expOrFn 是能够为方法,也能够是字符串的。所以,经过上面的代码咱们发现了一种官方文档里没有说明的用法,好比个人 data 结构以下
{ obj: { list: [{value: '123'}] } }
若是咱们要在 template 中须要使用 list 中第一个节点的 value 属性 值,就写个 computed property:
computed: { value: { get: 'obj.list.0.value' } }
而后在 template 中使用的时候,直接用{{ value }}
,这样的话,就算 list 为空,也能保证不会报错,相似于 lodash.get 的用法,例子 https://jsfiddle.net/wanghx/n5r1vj1o/1/ 。
扯远了,回到正题上。
建立完 watcher,就经过 Object.defineProperty 把 computed property 的 key 挂载到 vm 上。而且在 getter 中添加如下逻辑
if (watcher.dirty) { watcher.evaluate() } if (Dep.target) { watcher.depend() } return watcher.value
前面我有说过,computed property 的 watcher 是 lazy 的,当 computed property 中引用的 data 发生改变后,是不会立马从新计算值的,而只是标记一下 dirty 为 true,而后当这个 computed property 被引用的时候,上面的 getter 逻辑就会判断 watcher 是否为 dirty,若是是,就从新计算值。
然后面那一段watcher.depend
。则是为了收集 computed property 中用到的 data 的依赖,从而可以实现当 computed property 中引用的 data 发生更改时,也能触发到 render function 的从新执行。
depend () { let i = this.deps.length while (i--) { this.deps[i].depend() } }
把 data 以及 computed property 都初始化好以后,则建立一个 render function 的 watcher。逻辑以下:
export function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean ): Component { vm.$el = el ... callHook(vm, 'beforeMount') let updateComponent ... updateComponent = () => { vm._update(vm._render(), hydrating) } ... vm._watcher = new Watcher(vm, updateComponent, noop) if (vm.$vnode == null) { vm._isMounted = true callHook(vm, 'mounted') } return vm }
能够看到,建立 watcher 时候的入参 expOrFn 为 updateComponent 方法,而 updateComponent 方法中则是执行了 render function。而这个 watcher 不是 lazy 的,所以建立该 watcher 的时候,就会立马执行 render function 了,当执行 render function 的时候。若是 template 中有使用 data,则会触发 data 的 getter 逻辑,而后执行 dep.depend() 进行依赖收集,若是 template 中有显式使用 computed property,也会触发 computed property 的 getter 逻辑,从而再收集 computed property 的方法中引用的 data 的依赖。最终完成所有依赖的收集。
最后举个例子:
<template> <div>{{ test }}</div> </template> <script> export default { data() { return { name: 'cool' } }, computed: { test() { return this.name + 'test'; } } } </script>
至此,vue 的 reactive 是怎么实现的,就讲完了。