数据驱动开发是 Vue 的一大特征。
那么什么是数据驱动呢?在 Vue 的概念下,我们可以通过 data 来初始化页面;后续可以通过操作data 的值,来改变页面。整个过程都是围绕 data 来变化,所以称之为数据驱动,其中操作数据更新页面又常被称为响应式。
在 深入浅出 Vue 数据驱动 (一) 中,我们已经介绍了初始化的部分,本节主要介绍响应式是如何实现的。
写在前面
由上图可知,我们改变了 message 的值,对应的 ui 就会发生变化。1
App.message = 'Some one say hello to Vue!';
而在正常情况下,给属性赋值就是赋值,没有任何特别之处:
1 | const a = { b: 1} |
在 Vue 里面却变成 ui 变更,跟我们赋值操作做的看起来不是一件事儿。这说明 Vue 在把自己挂载到dom之前,做了一些工作。我们知道在 es5 中,可以通过 Object.defineProperty 来实现赋值 set 添加其他功能。
在 Vue 的源码分析过程中,一个重要的点就是 找到 Object.defineProperty 的定义。
另外 message 可以形成 getter 、computed 等,相互之间的依赖关系会越来越复杂。Vue 通过一个 Pub / Sub 模型来管理这些依赖。
总结一下上面的流程:
在挂载到 Dom 前, Vue 需要完成两件事:
- 将属性转换为 get 、set
- 将所有依赖关系收集起来。
1 | 虽然这部分也属于 new Vue 到 dom, 但是为了减小复杂度,我们在 深入浅出 Vue 数据驱动 (一) 中,故意省略了这部分。 |
在挂载到 dom 后:
- 调用 set ,执行所有依赖,更新 dom。
源码分析
以一个最简单的例子开始:
1 | new Vue({ |
下面分两个部分来分析源码。
属性转换与依赖收集
我们直接从 Vue.prototype._init 开始(参考前一篇文章)
1 | Vue.prototype._init__ = function( options ) { |
找到 initState:
1 | function initState (vm) { |
忽略不相干的代码,直接看 initData:
1 | function initData (vm) { |
initData 做了许多事情,我们主要关注三点:1. vm._data 2. proxy 3. observe。
vm._data 是 data 的内部表示。所以 proxy(vm, "_data", key);
是对 data 的访问代理。
1 | function proxy (target, sourceKey, key) { |
针对我们上面的例子,vm.message 访问代理到 vm._data.message。
在开始分析 observe 之前,我们先梳理一下到此为止的整个流程,如图所示:
可以看出我们在逐步细化这个流程,比如在第二步,不仅有 initData, 还有initProps。我们故意忽略了这个细节,方便我们整体去把控流程。
1 | function observe (value, asRootData) { |
同样的忽略所有相关细节, observe 函数主要作用是建立一个 Observer 类。如果传给 observe 的不是一个对象的话,返回 undefined,否则返回一个 Observer 实例(后续会利用这个特性做深度响应式处理)。
1 | export class Observer { |
此时传给 observer 的 value 为:{ message: ‘Flyyang say hello to Vue ‘}。
将会走到 this.walk(value):
1 | /** |
walk 的作用是循环所有的对象属性,转换为 geter/setter。转换操作在 defineReactive 里:
1 | function defineReactive ( |
饶了这么一大圈,终于看到了 Object.defineProperty 的庐山真面目。我们将我们的参数代入进去:
- obj: { message: ‘ Flyyang say hello to Vue’}
- key: ‘message’
- value: ‘Flyyang say hello to Vue’。
首先新建了一个 dep,我们先理解为依赖管理器。然后定义一个 childOb, 也就是 子的 Obsever。
由上面 observe 函数可知,当传入的 value 不是对象时,返回 undefind。所以 childOb 应为 false。
1 | 如果我们定义的 data 包含对象时,会递归调用 observe ,重走上面的流程知道 value 非 object。对这一块的理解非常重要。 |
由于这里只是定义 getter setter,我们先将分析到此为止。回忆一下我们的 init 方法:
我们在 initState 阶段对数据做了响应式处理。然后走入 mount 的流程。由上一节可知,在 mount 的流程里
会新建一个 Watcher:
1 | new Watcher(vm, updateComponent, noop, { |
1 | /** |
我们来回忆一下上一节中的流程,新建一个 wathcer, 然后构造函数中将 updateComponent
付给 watcher 的 getter。最后 在赋值 this.value 中调用 get 方法,同时执行 pushTarget 和 updateComponent。
我们先来看 pushTarget
1 | // The current target watcher being evaluated. |
pushTarget(this)
将当前执行的 Watcher 实例 当做 Dep 对象的静态属性。这种黑科技相当于我在一个对象上面挂了一个全局变量。
然后我们看下 updateComponent
部分。根据上篇文章介绍,在生成 dom 的过程中,会先将模板变异成 render 函数,并执行render 函数:
在 /src/core/instance/render.js
中:
1 | Vue.prototype._render = function (): VNode { |
vm._renderProxy 其实就是 vm 本身(或者proxy 过得 vm)。那么我们上面示例模板会编出什么代码呢?
如上图所示,render 函数中访问了 message 属性。我们知道它是被代理过得,并且也转换了 getter /setter。
访问意味着会走到其get 访问器。
1 | function defineReactive ( |
我们看 Dep.target ,在新建 Watcher 的时候,我们把当前 Watcher 赋值给了 Dep 对象的静态属性 target,那么此时 Dep.target 是有值的。
我们只有一个 属性 message
并且其值不是对象也不是 Array。那么只会执行 dep.depend()方法:
1 | Dep.prototype.depend = function depend () { |
其作用是把 dep 实例 添加到 watcher 上。
1 | // watcher.js |
1 | // dep |
同时又将 watcher 添加到添加到 dep.subs 内。至此依赖收集已经做完了。当前几个对象的关系用图片来表示为:
1 | 我们这里只描述了一个属性对应的 dep 。当你初始化的属性越多,包含嵌套对象和数组越多,那么生成的 dep 实例也就越多。 |
派发更新
接下来分析当我们修改 App.message 时会发生什么:
1 | App.message = 'Some one say hello to Vue' |
由上面的分析可知 App.message 时代理过后的属性,最终会走到属性的 setter:
1 | function defineReactive ( |
我们关注两个细节:
1 | childOb = !shallow && observe(newVal); |
当你 set 一个新值时,同样也会判断是否为对象数组等,仍然会走一遍 observe 的流程。
最后调用 dep.notify()
。
1 | // dep.js |
由依赖分析小节可知, sub 内存放的是 watcher 实例。notify 的作用是按顺序触发所有 watcher。
1 | // watcher.js |
忽略特殊选项,将会执行到 queueWatcher。 在 scheduler.js
中
1 | /** |
queueWatcher 作用是,如果当前没有在 flushing 的状态,那么就进入队列排队。如果在的话,在 nextTick 阶段则 flush 队列。
1 | // core/util/next-tick.js |
我们不对 nextTick 做过多分析。以一个最简单的例子来说明,假设nextTick 是 new 了一个 Promise,那么他的回调会在下一个 event loop 过程中执行。也就是说要走一遍 js 的 event loop 流程。
1 | 依赖变化并不会直接更新 dom ,而是先入队做处理。在 nextTick 更新。 |
接下来看一下 nextTick 的 cb 函数: flushSchedulerQueue
。
1 | // scheduler.js |
flush 的过程中会调用 watcher 的 run 方法:
1 | // watcher.js |
run
方法会调用 this.get() 。其实就是我们的 updateComponent 函数。这样就回到了我们上一章中的流程。
唯一不同的是我们的 message 变了。此时生成的 vnode 也就变了:
剩下的就是做 dom diff 和 patch,最后更新页面。
以上。
ISSUE
有问题?来 GitHub 一起讨论。