Vue2.0的响应式设计原理

前言

上周抽空看了看Vue的源码,设计的精巧让我这个鶸一时吸收不来。如果想写篇既大又全的文章,一劳永逸地介绍Vue2.0的源码,以我的水平显然是做不到的。于是就只取一瓢饮地,简单记录下Vue2.0在响应式原理上的设计。其他的部分等我功力深厚了(其实就是懒)再做总结吧。

基础

这一部分是Vue响应式原理的基础。包含Observer类,Watcher类,Dep类,事件订阅模式,还有最关键的Object.defineProperty方法。

Object.defineProperty

Vue实现数据绑定的方式和其他的MVVM同侪不同,React和backbone(这货不是MVVM)采用的是典型的发布订阅模式,Angular则采用的脏值检测。

Vue使用了更为隐蔽和magical的Object.defineProperty设置对象访问器属性(这也意味着Vue只支持到IE9+)。

把一个普通 Javascript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。Object.defineProperty 是仅 ES5 支持,且无法 shim 的特性,这也就是为什么 Vue 不支持 IE8 以及更低版本浏览器的原因。

Object.defineProperty()可以定义对象的访问器属性,关于访问器属性的更多介绍可以看这里。其中getset方法分别用来指定属性的getter和setter。

1
2
3
4
5
6
7
8
9
10
11
12
13
var person = {
_name: '', //`_`表示只能通过方法访问
nickname: '酱'
};
Object.defineProperty(person, 'name', function () {
get: function () {
return this._name;
},
set: function (newValue) {
this.nickname = newValue + '酱';
this._name = newValue;
}
});

通过getter和setter实现数据劫持是Vue数据绑定的基础。

发布订阅

发布订阅是JavaScript中事件机制的实现方式,也是JavaScript异步编程的实现方式之一。

发布订阅模式中的角色主要有发布者、事件对象、订阅者。发布者和事件对象是一对多的关系,事件对象和订阅者又是一对多的关系。当发布者的状态改变触发事件对象时,相关的订阅者就会收到通知。实现起来就像下面这样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var EventUtil = {
// 全局事件管理
var events = {},
// 注册事件
on = function (type, handler) {
if (events[type]) {
events[type].push(handler);
} else {
events[type] = [handler];
}
},
// 触发事件
emit = function (type) {
if (!events[type]) return;
for (var i = 0, len = events[type].length; i < len; i++) {
events[type][i];
}
};
};

Vue实现视图自动更新的原理也是如此,当然细节上就复杂多了。

Observer,Watcher,Dep

这三个是相辅相成实现Vue数据绑定的组件。

Observer

Vue在组件(Component)初始化过程中,会将数据对象封装为Observer对象,便于监听数据的改变,并绑定依赖在上面。我们来看下源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
constructor (value: any) {
this.value = value
this.dep = new Dep() //一个 Dep对象实例,Watcher 和 Observer 之间的纽带
this.vmCount = 0
def(value, '__ob__', this) //把自身 this 添加到 value 的 __ob__ 属性上
if (Array.isArray(value)) { //对 value 的类型进行判断
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys) // 数组增强方法
this.observeArray(value) //如果是数组则观察数组
} else {
this.walk(value) //否则观察单个元素。
}
}

Observer对象储存在 ob 这个属性,这个属性保存了 Observer 对象自己本身。对象在转化为 Observer 对象的过程中是一个递归的过程,对象的子元素如果是对象或数组的话,也会转化为 Observer 对象。

由于JavaScript本身的原因,Vue不能监测数组的变化,Vue采用的折中方法是增强数组的原生方法push, pop, shift, unshift, splice, sort, reverse,以及建议使用者通过Vue.set的方式显示调用。通过其他方式对数组进行的修改将无法被监听到。

Watcher

Vue中,Watcher和模板渲染紧密相连,它将Observer发生的改变反映到模板内容上。它关键部分的源码是这样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: Object
) {
this.vm = vm
vm._watchers.push(this)// 将当前 Watcher 类推送到对应的 Vue 实例中
......
// parse expression for getter
if (typeof expOrFn === 'function') {
// 如果是函数,相当于指定了当前订阅者获取数据的方法,每次订阅者通过这个方法获取数据然后与之前的值进行对比
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)// 否则的话将表达式解析为可执行的函数
......
}
this.value = this.lazy
? undefined
: this.get() //如果 lazy 不为 true,则执行 get 函数进行依赖收集
}

其中输入参数vm是监听的组件,expOrFn最终将交给getter属性,cb是更新时的回调函数。最后一句中的this.get()完成了依赖的收集工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Evaluate the getter, and re-collect dependencies.
*/
get () {
pushTarget(this) // 设置全局变量 Dep.target,将 Watcher 保存在这个全局变量中
const value = this.getter.call(this.vm, this.vm) // 调用 getter 函数,进入 get 方法进行依赖收集操作
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget() // 将全局变量 Dep.target 置为 null
this.cleanupDeps()
return value
}

第一句的pushTarget(this)设置了Dep.target,getter函数正是通过Dep.target是否为null,判断当前处于依赖收集阶段还是普通数据读取。后面的两句去touch``expOrFn涉及到的每个数据项。从而将expOrFn的依赖收集起来。最后将dep中的内容清空,为下次收集依赖做准备。

Dep

Dep类用于连接Watcher类和Observer类,每个Observer对象中都有一个Dep实例,其中存储了订阅者Watcher。源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//...
constructor () {
this.id = uid++
this.subs = [] //存储 Watcher 实例的数组
}
//...
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
// stablize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) { //遍历 Watcher 列表,调用 update 方法进行更新操作
subs[i].update()
}
}

Dep类比较简单,主要是一个存储Watcher实例的数组this.subsdepend()方法用于向Watcher对象中添加这个Dep。notify()方法将遍历Watcher列表,通知订阅者更新视图。

实现

下面从源码角度上看看Vue实现数据绑定的设计。

目录结构

Vue核心部分的代码放在src目录。路径下还有下面这些子文件夹:

  • entries 入口文件,根据编译环境的不同,更改一些配置
  • compiler 编译模板,render函数的实现
  • core 关键部分代码
  • core/observer 响应式设计中的Observer对象实现
  • core/vdom 虚拟DOM,diff算法,patch函数实现
  • core/instance 组件实例生命周期实现,组件初始化入口
  • core/components 全局组件
  • core/global-api 全局API
  • server 服务端渲染
  • platform 平台特定代码,分为webweex
  • sfc 处理单文件组件 解析.vue文件
  • share 工具函数

生命周期

关于Vue的生命周期,这里假设你已经熟悉,就不做介绍了。了解它也将帮助你了解Vue的工作流程。

源码的入口从下面一行代码开始:

1
2
3
4
5
6
7
8
9
10
11
12
13
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options) //开始初始化步骤
}
initMixin(Vue) //插入初始化代码
stateMixin(Vue) //插入数据绑定代码
eventsMixin(Vue) //插入事件相关代码
lifecycleMixin(Vue) //插入生命周期代码
renderMixin(Vue) //插入模板渲染代码

文件为src/core/instance/index.js,关键在于最后一句,通过调用init.js中定义的_init(options)方法初始化Vue实例。这个方法是在下面的initMixin(Vue)中导入的。这种mixin的方式不同于Vue1.x版本,更具模块化适合拓展(同时也增加了寻找代码的难度)。

初始化相关的主要代码如下;

1
2
3
4
5
6
7
8
9
10
11
12
13
function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
//...
initLifecycle(vm) //vm 的生命周期相关变量初始化
initEvents(vm) // vm 的事件监控初始化
initRender(vm) // 模板解析
callHook(vm, 'beforeCreate')
initState(vm) //vm 的状态初始化,prop/data/computed/method/watch 都在这里完成初始化
callHook(vm, 'created')
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}

initLifecycle主要是初始化vm实例上的一些参数;initEvents是事件监控的初始化;initRender是模板解析,2.0的版本中这一块有很大的改动,1.0的版本中Vue使用的是DocumentFragment来进行模板解析,而 2.0 中作者采用的John Resig的HTML Parser将模板解析成可直接执行的render函数。initState是数据绑定的主战场,我们下一节会详细讲到。callHook执行生命周期的钩子函数。

initState

在初始化中,initState函数承担了数据绑定中的最主要的脏活累活。它的源码像下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
export function initState (vm: Component) {
vm._watchers = [] //新建一个订阅者列表
const opts = vm.$options
if (opts.props) initProps(vm, opts.props) // 初始化 Props,与 initData 差不多
if (opts.methods) initMethods(vm, opts.methods) // 初始化 Methods,Methods 的初始化比较简单,就是作用域的重新绑定。
if (opts.data) {
initData(vm) // 初始化 Data,响应式关键步
} else {
observe(vm._data = {}, true /* asRootData */) //如果没有 data,则观察一个空对象
}
if (opts.computed) initComputed(vm, opts.computed)// 初始化 computed,这部分会涉及 Watcher 类以及依赖收集,computed 其实本身也是一种特殊的 Watcher
if (opts.watch) initWatch(vm, opts.watch)// 初始化 watch,这部分会涉及 Watcher 类以及依赖收集
}

可以看到,initState将工作拆解成观察props, data, methods, computed, watch几个关键部分。

initData

initData方法为例,它是如何使用上面提到的Observer, Dep, Watcher类的呢,我们看看源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? data.call(vm)
: data || {}
if (!isPlainObject(data)) {// 保证data必须为纯对象
......
}
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
let i = keys.length
while (i--) {
if (props && hasOwn(props, keys[i])) {// 是props,则不代理
...... //如果和 props 里面的变量重了,则抛出 Warning
} else {// 否则将属性代理的 vm 上,这样就可以通过 vm.xx 访问到 vm._data.xx
proxy(vm, keys[i]) //proxy方法遍历 data 的 key,把 data 上的属性代理到 vm 实例上
}
}
// observe data
observe(data, true /* asRootData */) //关键一步,observe(data, this)方法来对 data 做监控
}

可以看到,这个函数做了下面的工作:

  • 保证data为纯对象
  • 检查是否与props中属性有重复
  • 进行数据代理,便于我们直接通过vm.xxx的形式访问原本位于vm._data.xxx的属性。
  • 调用observe方法对data进行包装,使之具有响应式的特点。

那我们看看observe方法是怎么写的吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* Attempt to create an observer instance for a value,
* returns the new observer if successfully observed,
* or the existing observer if the value already has one.
* 返回一个 Observer 对象
*/
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value)) { //如果不是对象和数组则不监控,直接返回
return
}
let ob: Observer | void
//判断 value 是否已经添加了 __ob__ 属性,并且属性值是 Observer 对象的实例。避免重复引用导致的死循环
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { //如果是就直接用
ob = value.__ob__
} else if (
observerState.shouldConvert && //只有 root instance props 需要创建 Observer 对象
!isServerRendering() && //不是服务端渲染
(Array.isArray(value) || isPlainObject(value)) && //数组或者普通对象
Object.isExtensible(value) && //可扩展对象
!value._isVue // 非 Vue 组件
) {
ob = new Observer(value) //关键步!在 value 满足上述条件的情况下创建一个 Observer 对象
}
if (asRootData && ob) {
ob.vmCount++
}
return ob // 返回一个 Observer 对象
}

observe方法主要就是判断value是否满足一些预设条件,并将这个对象转化为Observer对象。

关于Observer类我们上面已经提到,它的构造函数做了下面几个工作:

  • 首先创建了一个Dep对象实例;
  • 然后把自身this添加到value的__ob__属性上;
  • 最后对value的类型进行判断,如果是数组则观察数组,否则观察单个元素(要理解这一步是个递归过程,即value的元素如果符合条件也需要转化为Observer对象)。

不论是基础类型还是数组或对象,最终都会走入到walk方法,方法定义在src/core/observer/index.js中。

1
2
3
4
5
6
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]]) //遍历对象,结合defineReactive方法地柜将属性转化为getter和setter
}
}

defineReactive

在经过一系列的准备工作和铺垫后,终于可以接触到数据绑定最核心部分的defineReactive函数。方法也定义在src/core/observer/index.js中,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* Define a reactive property on an Object.
*/
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: Function
) {
const dep = new Dep() //每个对象都会有一个 Dep 实例,用来保存依赖 (Watcher 对象)
......
let childOb = observe(val) //结合 observe 函数进行将对象的对象也变成监控对象
// 最重点、基石、核心的部分:通过调用 Object.defineProperty 给 data 的每个属性添加 getter 和 setter 方法。
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
// 依赖收集的重要步骤
if (Dep.target) {//如果存在Dep.target这个全局变量不为空,表示是在新建Watcher的时候调用的,代码已经保证
dep.depend() // 依赖收集
if (childOb) {
childOb.dep.depend() // 处理好子元素的依赖 watcher
}
if (Array.isArray(value)) { // 如果是数组,进一步处理
dependArray(value)
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
......
childOb = observe(newVal) // 对新数据重新 observe,更新数据的依赖关系
dep.notify() // 通知dep进行数据更新,这个方法在前面的 Dep 类中讲过
}
})
}

defineReactive是对Object.defineProperty方法的包装,结合observe方法对数据项进行深入遍历,最终将所有的属性就转化为getter和setter。其中对于Dep的处理用于收集依赖data的Watcher对象。

依赖收集

data的依赖收集是在getter函数中完成的。Observer和Dep是一对一的关系,Dep用来存储依赖Observer的Watcher。Dep和Watcher是多对多的关系,一个Dep中存储了若干Watcher,一个Watcher可能同时依赖于多个Observer。

可以看到Dep是连接Observer(生产者)和Watcher(消费者)的关键纽带。Watcher通过getter函数建立起和Observer中Dep的关联。在Observer的setter函数中会触发dep.notify()方法,根据上文对该方法的讲解,它实际上对数组中每个Watcher执行了update方法。在方法中根据是否同步去执行run方法,这个方法中通过源码可以看到实际上正是通过const value = this.get()获取最新的value。

1
2
3
4
5
6
7
8
9
10
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
  1. 模板编译过程中的指令和数据绑定都会生成 Watcher实例,watch函数中的对象也会生成 Watcher实例,在实例化的过程中,会调用watcher.js中的get函数touch这个Watcher的表达式或函数涉及的所有属性;
  2. touch开始之前Watcher会设置Dep的静态属性Dep.target指向其自身,然后开始依赖收集;
  3. touch属性的过程中,属性的getter函数会被访问;
  4. 属性gette 函数中会判断Dep.target(target中保存的是第2步中设置的Watcher实例)是否存在,若存在则将 getter函数所在的Observer 实例的Dep实例保存到Watcher的列表中,并在此Dep实例中添加Watcher为订阅者;
  5. 重复上述过程直至Watcher的表达式或函数涉及的所有属性均touch结束(即表达式或函数中所有的数据的getter函数都已被触发),Dep.target被置为null,依赖收集完成;

总结

上面尝试从源码角度对Vue2.0的响应式设计做了浅析。总结一下就是下面几点:

  • 在生命周期的initState方法中对data, prop, method, computed, watch方法中的数据进行劫持,通过defineReactiveobserve将之转换为Observer对象
  • initRender函数中解析模板,新建Watcher对象通过Dep对象和对应数据建立了依赖关系,通过Dep.target这个全局对象判断是否是依赖收集阶段。
  • 数据变化时,通过setter函数中的dep.notify方法执行Watcher的update方法更新视图

参考

Vue2.0 源码阅读:响应式原理