1. 前言
原文发布在语雀上,地址在这里。语雀带有大纲,阅读起来可能更舒服。
<Vue 源码笔记系列1>Data 的初始化与依赖收集的准备工作 · 语雀www.yuque.com![0c0d7e21e87da0964d63bb386c05c1c1.png](https://img-blog.csdnimg.cn/img_convert/0c0d7e21e87da0964d63bb386c05c1c1.png)
React、Vue、Angular 流行框架让开发者从抠 DOM 变化中解脱出来。我们可以将主要的精力放在数据的变化即业务逻辑上,框架会依据数据变化灵活更新视图。
三个框架在实现上各有不同,那么 Vue 是如何做到数据变化自行更新视图呢?
要实现上述目标我们需要解决两个问题:
- 如何监听到数据变化
- 数据变化后应该更新哪些视图
2. 基础 Object.defineProperty
defineProperty 定义 get 与 set 回调,读取值的时候触发 get,改变其值时触发 set。这是一切的基础,这里不详细讲了,网上关于这部分的资源实在太多。
3. 大致流程图
先将函数执行顺序图放在这里,不需要立即了解里面的细节,放在这里只是方便接下来的阅读作为对照。
![3b7c5b1cd13042c42fd0dfa79f4233ed.png](https://img-blog.csdnimg.cn/img_convert/3b7c5b1cd13042c42fd0dfa79f4233ed.png)
data 初始化始于 initData
// src/core/instance/state.js
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object:n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// ...
// observe data
observe(data, true /* asRootData */)
}
上半部分,let data = vm.$options.data 获取到 data,$options 为我们实例化 Vue 时传入的参数,当然Vue 并不会原原本本地使用我们传入的参数,会有些合并的处理,我们以后再讲。
之后判断 data 是否是纯对象,如果不是生产环境就会在打印错误。<br />isPlainObject 其实很简单,源码如下:
/**
* Strict object type check. Only returns true
* for plain JavaScript objects.
*/
export function isPlainObject (obj: any): boolean {
return _toString.call(obj) === '[object Object]'
}
中间我们省略了一部分代码,大致作用是代理 data,与本文主题关系不大,以后会单独讲。
最后一行代码才是本文的核心,调用 observe 工厂函数并传入 data。observe(data, true / asRootData /)
4. observe 工厂函数
先来看下 observe 的代码:
// core/observer/index.js
/**
* 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.
*/
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
可以看到 observe 有返回值 ob,虽然在 initData 中并没有使用,但是在之后的其他地方会有使用,我们稍微注意下即可。
9 到 11 行代码:
if (!isObject(value) || value instanceof VNode) {
return
}
如果传入的值 value 不是对象,或者 value 是 VNode 的实例,直接 return。此时返回值为 undefined。
12 到 23 行
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
判断 value 是否存在 ob 属性,并且为 Observer 实例,是则返回该属性,这么做的目的在于防止重复依赖。<br />否则进入下一个判断<br />当 value 为数组或者纯对象时,new Observer,并且赋值给 ob,作为函数的返回值。
综上可以看出来 observe 的返回值有两种,一个是 undefined,一个是 Observer 的实例。
observe 函数只干了两件事:
- 调用 new Observer(value)
- 返回 Observer 实例 ob
注意:
只有数组和纯对象才会往下走 new Observer,进而有机会
下一步我们看看 class Observer 干了些什么。
5. Observer 观察者类
Observer 源码如下:
// src/core/observer/index.js
/**
* Observer class that is attached to each observed
* object. Once attached, the observer converts the target
* object's property keys into getter/setters that
* collect dependencies and dispatch updates.
*/
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that has this object as root $data
constructor (value: any) {
// ...
}
walk (obj: Object) {
// ...
}
observeArray (items: Array<any>) {
// ...
}
}
可以看到 Observer 类有三个实例属性 value、dep、vmCount。一个构造函数。两个实例方法 walk、observeArray。
5.1 构造函数
构造函数代码如下:
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value)
}
}
第3行:<br />this.dep = new Dep()<br />Dep 中会存储依赖,也包含触发依赖更新的方法,之后我们会详细讲,这里我们先认为他是一个盒子,提供一个 depend 方法收集相关依赖,提供 notify 用以遍历所有依赖依次触发。
第5行:<br />def(value, ‘ob‘, this)<br />给 value 添加 ob 属性,值为 Observer 实例。<br />如果对上一节 observe 工厂函数段落还有印象的话,我们提到过这么一段代码:
// 来自 observe 方法
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
}
当时我们提过这个判断是为了防止重复依赖,这里 value.ob 的值来源就是 def(value, ‘ob’, this) 。<br />def 方法源码如下:
/**
* Define a property.
*/
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
使用 defineProperty 为对象添加属性,并且可以设置该属性是否可遍历,默认为 false 不可遍历。这里使用 def 的主要目的就是防止遍历到 ob 属性。
6 到 14 行:
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value)
}
这段逻辑比较简单,value 为数组调用 this.observeArray 否则调用 this.walk 。
5.2 实例方法 this.walk
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
遍历对象,对每一个属性调用 defineReactive
<a name="UqlBS"></a>
5.3 实例方法 this.observeArray
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
遍历数组,对数组的每一项重新走 observe, 这里又回到了 第四节 observe 工厂函数。
<a name="nu2oI"></a>
5.4 小结
Observer 主要干了以下几件事:
- 给传进来的 value 添加 ob 属性,值为自身实例
- value 为对象时为每一个属性调用 defineReactive
- value 为数组时为每一项调用 observe
假如现在有 data 如下:
const data = {
data1: 1,
data2: {
a: 2
},
data3: [ 3, { b: 4 } ],
}
经过处理后
const data = {
data1: 1, // 基本数据类型,在 observe 时直接 return,没有 __ob__
data2: {
a: 2,
__ob__: { value, dep, vmCount }
},
data3: [
3,
{ b: 4, __ob__: { value, dep, vmCount } }
// __ob__: { value, dep, vmCount } 这里__ob__ 为 data3 的属性,而不是数组的(ˇˍˇ) 想~
],
// __ob__ 不可枚举
__ob__: {
value: data,
dep: Dep 实例 new Dep(),
vmCount: 0,
}
}
6. defineReactive 函数
Observer 类实例化过程中会调用 defineReactive , 先看一下该方法的源码:
// src/core/observer/index.js
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
// ...
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
// ...
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
大致能看到,这个方法主要目的将 data 的属性变为响应式属性(添加 get set 方法)。
第 10 行:<br />const dep = new Dep()<br />Observer 类的构造函数有这样一句 this.dep = new Dep() ,当时我们讲 Dep 提供 depend 方法收集依赖,提供 notify 遍历依赖并触发。但是这两个 dep 仍有些不同。稍后我们再讲。
12 到 15 行:
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
getOwnPropertyDescriptor 为 js 方法,用于获取属性描述对象。接着判断如果 configurable 为 false,直接终止。因为 configurable 为 false 时是无法设置 get set 的。
18 到 22 行:
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
我们先缓存 obj[key] 原本的 get set 方法。<br />接下来的判断条件比较奇怪,我们先看后边的 arguments.length = 2 ,这个比较好理解,只有两个参数时,我们才需要使用 val = obj[key] 获取值。<br />另一个判断条件 (!getter || setter) ,如果用户之前已经定义了 obj[key] 的 get 属性,那么我们就不再使用 val = obj[key] 取值,因为这里取值将会触发用户定义的 get 方法。当真正使用值的时候,我们调用缓存的 getter 来取值。<br />这样会产生一个问题,用户已经定义过 get 时,我们的 val 为 undefined,会导致下一步 childOb 为 undefined。也就是说我们不会对这个已定义 get 的属性深度观测。<br />还有另一个问题,当我们将 obj[key] 变为可观测后,会定义 get set 方法,仅仅按照有 get 就不进行深度观测,这里就会有问题,所以这里又补上了 如果有 set 的话,也要取值,进行深度观测。<br />最终判断条件就成了 (!getter || setter) && arguments.length = 2
。
第 24 行:<br />let childOb = !shallow && observe(val)<br />又又又调用 observe 工厂函数。我们多次提到过,observe 传入的 val 不是数组和纯对象时,返回 undefined,是数组或纯对象时,返回 val.ob 属性,该属性为 Observer 实例,具有 dep 属性,为 Dep 实例。<br />执行这一句就是要对数组和纯对象进行深度观测。
剩下的部分:<br />定义 get set 方法。<br />先来看看 get:
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
Dep.target 就是我们需要收集的依赖,之后会详细讲。<br />调用 dep.depend() 来收集依赖。注意这里的 dep 为第 10 行代码声明的 dep,当时我们说它和 Observer 类 this.dep = new Dep() 生成的 dep 不太一样。这里的 dep 在 get set 中被使用,作为闭包存在,它针对的是纯对象的每一个属性,每一个属性都有,不论它的值是数字、字符串这样的基本数据类型还是数组、对象。<br />Observer 实例上的 dep,因为只有数组、纯对象有 ob 属性,所以只有数组、纯对象有该 dep。<br />当 childOb 存在时,我们再次将该依赖收集到 childOb。<br />如此一来对于数组对象来说岂不是同一个依赖被收集了两次?没错,在闭包 dep 中一次,在 childOb 中一次。为什么要收集两次呢?<br />假设有如下 data:
const data = {
a: {
b: 1
}
}
首先是 observe(data)<br />接着 Vue 会遍历 data,对每一个属性调用 observe。即在 defineReactive 中 childOb = observe(data.a),获得其_ob 属性,我们记为 childOb1<br />进入 observe(data.a),会给 data.a 添加 _ob 属性。此时 data 如下:
const data = {
a: {
b: 1
__ob__: { value, dep, vmCount } // 注意此时,dep 只是空壳子,因为我们还没有调用 depend 收集依赖
}
// 这里也有 __ob__,为了简化描述,我们将重点放在 data.a 所以这里的 __ob__ 我们略过不谈
}
接着往下走,new Observer 的时候会遍历 data.a ,调用 defineReactive ,也就是对 data.a.b 调用 defineReactive, 在 defineReactive 中我们使用闭包保存了 data.a.b 的依赖。此时的 childOb = observe(data.a.b) 为 undefined。我们记为 childOb2<br />执行完毕后,会返回到刚刚进入的节点接着往下走,此节点已在上方标注为蓝色。返回到这里后,childOb1 的值也就出现了,为 data.a.ob。<br />再往下走,进入 get 设置
if (childOb) {
childOb.dep.depend() // 之前只是定义了 obj[key].__ob__.dep,这里才是真正收集依赖
// ...
}
调用 data.a.ob 的 dep 的 depend 方法收集依赖。<br />如此依赖,针对 data.a 我们既有闭包收集的依赖,又有 data.a.ob 收集的依赖。因为为 object 添加或者删除属性时,set 方法不会被触发,所以我们使用 ob 保存一份依赖,当 obj.a 添加或者删除属性的时候,手动触发依赖。这才有了 Vue.set Vue.delete 方法。<br />另外针对数组的 push shift 等会改变原有数组的变异方法,Vue已经做了代理,调用这些方法时,Vue 会帮我们手动触发 ob 保存的依赖。
我们还剩下一句没讲:
if (Array.isArray(value)) {
dependArray(value)
}
dependArray 其实很简单
function dependArray (value: Array<any>) {
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i]
e && e.__ob__ && e.__ob__.dep.depend()
if (Array.isArray(e)) {
dependArray(e)
}
}
}
如果value 是数组的话,我们需要遍历数组,对数组的每一项收集一次依赖。
再看看 set:
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
4 5 6 行:<br />如果 newVal = value,即值没有改变的时候,放弃执行。<br />newVal ! newVal && value !== value 如果值为 NaN,也放弃执行更新。
接下来就是执行 setter,设置新值。<br />针对新值再次调用 observe 进行观测。<br />最后调用闭包 dep.notify() 通知更新。
defineReactive 方法主要作用是将数据变为可观测的。它针对object每个属性生成闭包保存依赖,并且调用数组或纯对象数据的 ob.dep.depend 再另存一份依赖以供 Vue.set Vue.delete 已经数组的编译方法使用。
7. 小结
结合第三节的流程图,我们通篇主要涉及的只有三个点。
- observe 工厂函数。针对数组、纯对象,调用 new Observer,并返回 Observer 实例,其他数据类型返回 undefined。
- Observer 类。为数据添加ob属性,值为自己的实例。针对对象类型数据的每一个属性调用 definedReactive,针对数组类型数据,调用 observe。
- definedReactive 方法。为数据设置 get set,使其可观测,并且产生闭包,保存依赖。同时触发Observer 类为数据添加的ob属性上的 dep.depend,另存一份依赖,以供 Vue.set Vue.delete 代理数组变异方法时使用。
到此,算是大致过了一遍 data 的初始化,其实 data 的初始化就是为其依赖收集做准备工作。注意,我们只是准备好了去收集依赖,也即定义好了 get。下一步需要有人触发 get,比如 render?这些我们放在另外的章节来讲。
8. 参考文献
- Vue 技术内幕
- 【Vue原理】响应式原理 - 白话版
- 【Vue原理】依赖收集 - 源码版之基本数据类型
- 【Vue原理】依赖收集 - 源码版之引用数据类型