活着,最有意义的事情,就是不遗余力地提升自己的认知,拓展自己的认知边界。
引言
在Vue源码解析·初始化章节中,在初始化的过程中曾多次提到对state属性的响应式处理。
Vue是如何收集依赖的?
Vue是如何通知更新的?
对象和数组分别是如何进行深层响应式处理的?
嵌套对象是如何进行浅层响应式处理的?
上面这些问题,都将在本章通过源码一一解释。
响应式机制概述
上图来自vue.js官方文档
几个重要概念
Observer
在初始化的过程中,Observer
会对对象和数组分别执行不同的响应式处理。
对象通过ES5 API
为对象的每一个属性定义数据劫持。
数组通过原型链劫持或定义隐藏属性来实现数据劫持。
Dep
在观察者模式中,Dep
是发布者,负责将数据变化的消息通知给所有的Watcher
。
Watcher
在观察者模式中,Watcher
是订阅者,收到Dep
的通知后,执行update
,重新渲染。
简述响应式流程
-
在Vue初始化过程中,为每一个属性key定义setter和getter。
-
当页面初次渲染时,执行渲染函数,访问属性时,会触发setter,从而收集依赖,将Dep和Watcher关联起来。
-
当属性key对应的值变化时,会触发setter,从而通过Dep通知Watcher执行update。
-
之后,重新渲染页面,
源码解析
以data中的属性为例,来看看data中的属性是如何一步步实现响应式处理的,数据变化后又是如何更新的。
测试案例
编写一个reactive.html文件,内容如下:
<!DOCTYPE html>
<html>
<head>
<title>Vue源码剖析</title>
<script src="../../dist/vue.js"></script>
</head>
<body>
<div id="demo">
<h1>数据响应化</h1>
<p>{{face.description}}</p>
<button @click="care">关心一下</button>
</div>
<script>
const app = new Vue({
el: '#demo',
data: {
face: {
description: '面无表情'
}
},
methods: {
care(){
this.face.description = '你笑起来真好看'
}
}
});
</script>
</body>
</html>
在浏览器中打开reactive.html,打开调试器,让调试器获得焦点,使用快捷键Ctrl + P,打开搜索框,键入reactive.html即可看到我们编写的代码。
然后在new Vue行打上断点,刷新页面即可调试Vue源码。
Observe阶段:定义属性的数据劫持
首先通过快捷键Ctrl + P打开调试器搜索框,键入src/core/instance/init.js
打开文件,定位到initState(vm)
行,打上断点,让代码运行到此处。
然后,步入方法initState
内,代码运行到initData(vm)
处,开始初始化data选项,执行的操作如下:
- 获取用户编写的data选项,并挂载到vm的_data属性上
- 检查data选项是否能得到一个普通的对象,即其对象的toString结果是
'[object Object]'
- 依次检查data中的每个属性key是否在methods、props中使用过,如果使用过则报错
- 检查data的每一个key是否以_或$为前缀,如果不是,则在vm上设置对于_data中key的代理(这也是我们可以通过
this[key]
的原因,本质上访问的还是this[_data][key]
) - 此时,对data进行observe处理
在observe
方法中,最核心的代码就是ob = new Observer(value)
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__ //如果value中包含__ob__属性,表明已经observe过
} else if (
shouldObserve && //用于控制特定的数据是否对其进行Observe(将在props,provide,inject,computed等选项的初始化详细说明)
!isServerRendering() && //非服务端渲染
(Array.isArray(value) || isPlainObject(value)) && //Vue只对数组和普通对象进行observe
Object.isExtensible(value) && // ES5 的API,判断一个对象是否可扩展
!value._isVue //_isVue属性是在_init方法中挂载到vm上的,说明不对vue实例进行observe
) {
ob = new Observer(value)
}
下面我们来看,Observer
的实例化,源码如下:
/**
* 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 have this object as root $data
constructor (value: any) {
this.value = value //被observe的对象或数组
this.dep = new Dep() //用于收集依赖
this.vmCount = 0 //该对象或数组被多少vue实例挂载为$data
def(value, '__ob__', this) //当当前Observer实例作为value的__ob__属性
if (Array.isArray(value)) {
if (hasProto) {
// 通过__proto__属性拦截value的原型链,概念比较抽象,详细操作可看具体源码
protoAugment(value, arrayMethods)
} else {
// 给value定义隐藏属性
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value) //对value的每一个元素进行observe,代码很简单
} else {
this.walk(value) //遍历value的每一个属性,并执行响应式处理
}
}
在此方法中,做了如下事情:
- 给Observer实例定义了value属性(也就是被observe的对象或数组)
- 给Observer实例定义了dep属性,用于收集依赖
- 给Observer实例定义了vmCount,类似于windows变成中进程的引用计数
- 针对数组,拦截原型链或定义隐藏属性,对每一个元素进行observe
- 针对对象,执行了walk方法
walk
方法的源码,逻辑很简单,将对象的所有属性执行defineReactive
:
/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i]) //最核心的代码:实现响应式的基础
}
}
到此,进入了关键代码段,执行defineReactive
方法:
- 首先创建了一个Dep实例:
const dep = new Dep()
- 如果被处理的key的configurable是false,则不执行处理
- 对key对应的val进行预处理,如果不是浅层响应式处理,则对val进行递归式observe
- 定义getter和setter(数据拦截),源码如下:
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () { //定义对应key的取值器
//略
},
set: function reactiveSetter (newVal) { //定义对应key的存值器
//略
}
})
注意事项:此处只是进行了定义,代码并没有执行,此时还处于initState阶段,处于beforeCreate和created之间,详细代码不做介绍,代码执行时结合实例详细说明
初始化render watcher阶段:触发首次挂载
结合上图的调用堆栈说明:
- (匿名):html中的脚本调用
Vue
:Vue
构造函数执行Vue._init
:_init
方法执行,created钩子及其之前的初始化,包含provide,inject,data,props,computed,watch等用户代码的初始化。Vue.$mount
:$mount
扩展部分的执行,包含对render、template和el的处理。Vue.$mount
:$mount
初始定义函数的执行,核心就是调用mountComponent方法mountComponent
:在此方法内,创建了当前Vue组件实例的render watcher
下面我们看render watcher的实例化过程:
render watcher的构造参数
// src/core/instance/lifecycle.js
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
- 参数1:
vm
——Vue
组件实例 - 参数2:
expOrFn
表达式或函数, 此处传入的是updateComponent
—— 用于执行实例的渲染和挂载 - 参数3:
cb
回调函数,此处的noop
表示什么都不执行,no operation - 参数4:
options
包含deep
、user
、lazy
、sync
、before
,在Vue中共有三种watcher,分别为render watcher, computed watcher,watch watcher(将在专门的章节中详细说明) - 参数5:
isRenderWatcher
,此处传入true,表明是一个render watcher
watcher实例化细节
- 绑定组件实例:
this.vm = vm
,意味着通过watcher可以访问其所属的vue实例 - 如果是render watcher,则将当前watcher实例挂载到vm的_watcher属性上
if (isRenderWatcher) {
vm._watcher = this
}
- 将当前
watcher
实例保存在vm
的_watchers
属性中,_watchers
属性中保存了当前组件实例中所有的watcher
(包含三种watcher
) - 根据
options
初始化相应的属性
// options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
this.before = options.before
} else {
this.deep = this.user = this.lazy = this.sync = false
}
- 部分
watcher
实例属性的初始化,此处不做详解
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
- 处理
expOrFn
,render watcher的实参是updateComponent
,此处将updateComponent
赋值给watcher
的getter
属性(很重要)
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn //updateComponent是在创建render watcher之前定义的函数
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
}
}
- 给
watcher
的value
属性赋值,对于render watcher
,options
中只传了before
,也就是说lazy
属性是false
,所以会执行watcher
实例的get
方法(很重要)
this.value = this.lazy
? undefined
: this.get()
很难理解的一步:如何触发了组建的渲染和更新
在上面watcher
实例的最后一行代码,调用了get
方法,下面看get的源码:
/**
* Evaluate the getter, and re-collect dependencies.
*/
get () {
pushTarget(this)
let value
const vm = this.vm //当前watcher实例所属的vue实例
try {
value = this.getter.call(vm, vm) //关键代码
} catch (e) {
//略
} finally {
//略
}
return value
}
第一行代码:pushTarget(this):本质上是将当前watcher实例赋值给Dep.target,在收集依赖的时候,Dep.target必须有值
上面提到过,render watcher
实例的getter
属性本质上是updateComponent
,所以value = this.getter.call(vm, vm)
,实际上就是执行vue实例的updateComponent
相关源码:
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
vm._render()
:执行vue实例的渲染,返回vue实例的vnode(即虚拟DOM)vm._update()
:执行挂载
其调用堆栈如下:
执行顺序依次为:
Watcher
: watcher的实例化get
: 对于render watcher来说,给value属性赋值本质上就是调用了get实例方法updateComponent
:在上面的get方法中执行了getter函数,即updateComponent函数(对于render watcher来说)
依赖收集阶段
通过调用堆栈图,看从updateComponent
到开始收集依赖是怎么运行的:
updateComponent
: 在updateComponent
函数内部调用了vm._update
方法,该方法的第一个参数是vm._render
函数的返回结果(其实就是vm对应的虚拟DOM)Vue._render
:执行了渲染函数,并返回虚拟DOM(匿名)
:组件实例对应的渲染函数的执行,渲染函数代码举例:
(function anonymous(
) {
with(this){return _c('div',{attrs:{"id":"demo"}},[_c('h1',[_v("数据响应化")]),
_v(" "),_c('p',[_v(_s(face.description))]),_v(" "),
_c('button',{on:{"click":care}},[_v("关心一下")])])}
})
解释:
_c
:vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
(插曲)Vue内置的渲染函数声明(有兴趣的可以自行研究一下)
// flow/component.js
// _c is internal that accepts `normalizationType` optimization hint
_c: (
vnode?: VNode,
data?: VNodeData,
children?: VNodeChildren,
normalizationType?: number
) => VNode | void;
// renderStatic
_m: (index: number, isInFor?: boolean) => VNode | VNodeChildren;
// markOnce
_o: (vnode: VNode | Array<VNode>, index: number, key: string) => VNode | VNodeChildren;
// toString
_s: (value: mixed) => string;
// text to VNode
_v: (value: string | number) => VNode;
// toNumber
_n: (value: string) => number | string;
// empty vnode
_e: () => VNode;
// loose equal
_q: (a: mixed, b: mixed) => boolean;
// loose indexOf
_i: (arr: Array<mixed>, val: mixed) => number;
// resolveFilter
_f: (id: string) => Function;
// renderList
_l: (val: mixed, render: Function) => ?Array<VNode>;
// renderSlot
_t: (name: string, fallback: ?Array<VNode>, props: ?Object) => ?Array<VNode>;
// apply v-bind object
_b: (data: any, tag: string, value: any, asProp: boolean, isSync?: boolean) => VNodeData;
// apply v-on object
_g: (data: any, value: any) => VNodeData;
// check custom keyCode
_k: (eventKeyCode: number, key: string, builtInAlias?: number | Array<number>, eventKeyName?: string) => ?boolean;
// resolve scoped slots
_u: (scopedSlots: ScopedSlotsData, res?: Object) => { [key: string]: Function };
proxyGetter
: 我们在模板上使用的都是代理后的属性(即可以直接通过this访问的),然后再访问vm._data
中的属性(即用户编写的data选项)reactiveGetter
:也就是在defineReactive
方法中,给key
定义的get
函数,在渲染函数中通过访问代理属性,间接触发
reactiveGetter:对指定属性的访问进行劫持,将watcher、dep、vm、state属性(此处是data属性)关联起来
注意事项:暂且不考虑数组,嵌套对象的依赖收集,保持一个低开高走的姿态
Dep.target
:在render watcher
实例化过程中,调用get方法的第一步就是pushTarget方法,本质上就是将render watcher
的实例赋值给Dep.target
,紧接着依次执行vm._render
,匿名渲染函数
,proxyGetter
,reactiveGetter
。- dep:是在定义数据劫持阶段创建的,此处形成了闭包。此处的dep和key是一对一的关系,dep实例上有一个subs属性,可以保存多个watcher实例(即访问key的vm对应的render watcher),前提:仅考虑单层属性的对象,而不考虑嵌套属性。
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.depend:其实执行的是Dep.target.addDep(this)
Dep.target
是包含挂载当前key
属性的vm
对应的render watcher
Dep实例有一个id属性,该属性是在定义数据劫持阶段赋值的,先遍历到的属性,对应的id值越大,跟data选项中的顺序有关。但是收集依赖的顺序,跟渲染函数的访问顺序有关,也就是我们经常编写的template中的先后编写顺序有关。
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
下面看watcher
又是如何处理传入的Dep
实例的
Watcher实例的addDep方法
注意事项:此方法是通过watcher实例来调用,一定要明确this指的是什么
newDepIds
: 用来判断一个dep
是否处于一个render watcher
的newDeps
数组中。一个render watcher
对应一个vue
组件实例,一个dep
姑且代表一个data
属性key
,一个组件实例对应多个data
选项中的属性,因此一个watcher
对应多个dep
。depIds
:用来判断一个watcher是否存在于dep实例的subs数组中。一个vue组件可以被多个页面使用,即有多个实例,多个render watcher
,因此一个dep对应多个watcher。
综合以上两点,watcher
和dep
是多对多的关系。
/**
* Add a dependency to this directive.
*/
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
到此,依赖收集简单介绍完了,对于深层嵌套属性的依赖收集,会在响应式流程走完后再做补充。
通知更新
当vm实例的data选项属性发生变化,是如何通知渲染的,先来看一个堆栈调用图
当我们点击一个按钮,触发一个事件,修改一个属性的值,然后触发了该属性的修改器,也就是reactiveSetter
函数。
reactiveSetter
修改器中的两个关键点:
shalow
:在初始化阶段形成闭包,其值为undefined
,如果对应key的值修改后是一个对象,则对其进行深层响应式处理。dep
跟key
是一对一的关系,所以通知更新应该始于dep
。
set: function reactiveSetter (newVal) {
childOb = !shallow && observe(newVal)
dep.notify()
}
dep.notify:dep的通知方法做了什么
当设置key对应的值时,与key关联的dep发出通知,dep的subs数组中保存着使用当前属性的所有render watcher(render watcher和vm是一对一的关系)。通过代码可知,值变化时,会通知该属性对应的dep收集的所有render watcher执行update方法。(???此处有疑问)
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
watcher.update():执行更新
update
方法会涉及到lazy
,dirty
,sync
等属性,将会在watcher
专题章节做详细说明;queueWatcher将当前watcher实例压入队列,至于后续操作,将会在异步更新机制章节中做详细说明。
update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
数组的响应式
测试案例
<!DOCTYPE html>
<html>
<head>
<script src="../../dist/vue.js"></script>
</head>
<body>
<div id="demo">
<h1>面部特征</h1>
<button @click="showEyes()">抬头</button>
<li v-for="a in arr" :key="a">
{{ a }}
</li>
</div>
<script>
// 创建实例
const app = new Vue({
el: '#demo',
data: { arr: ['双眼皮', '偶尔皱眉'] },
methods:{
showEyes(){
this.arr.push('清澈的眼神')
}
}
});
</script>
</body>
</html>
调用堆栈
Vue
:执行构造函数Vue._init
:执行初始化方法initState
:初始化state
属性,包括data
、props
等initData
:初始化data
选项observe
:此处observe
的value
是data
对象Observer
:给data
对象创建Observer
实例,并遍历内部属性walk
:此方法一定会执行,因为data
返回的是一个普通对象defineReactive$$1
:定义响应式observe
:由于shallow
是undefined
,所以无论key
对应的值是否是对象或数组,都会对其执行observe
,由于此处的arr
属性的值是一个数组,所以会创建Observer
实例,对执行observeArray
方法
observeArray方法的代码如下:
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
在此示例中,由于数组的元素都是基本数据类型,所以会执行observe,但不会针对索引定义响应式,那么数组元素的响应式是如何实现的呢?
为数组创建Observer
实例时,会执行protoAugment(value, arrayMethods)
或copyAugment(value, arrayMethods, arrayKeys)
,对应的源码如下:
if (Array.isArray(value)) {
if (hasProto) {
// 通过__proto__属性拦截value的原型链,概念比较抽象,详细操作可看具体源码
protoAugment(value, arrayMethods)
} else {
// 给value定义隐藏属性
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value) //对value的每一个元素进行observe,代码很简单
}
arrayMethods
的值如下图:
arrayKeys
的值如下图:
也就是说Vue对数组的这7个方法进行了数据劫持,添加了自己的逻辑,具体逻辑,继续往下看
数组原型链劫持
保存Array的原型:
const arrayProto = Array.prototype
以数组的原型为原型创建一个新对象:
export const arrayMethods = Object.create(arrayProto)
arrayMethods的值如下图:
定义打补丁的几个方法:
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
为补丁方法定义劫持逻辑:
请看源码注释
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method] //以push方法为例,就是Array.prototype.push
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args) //执行Array的原型方法,this指向被observe的数组对象
const ob = this.__ob__ //被定义在数组上的observer实例
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args //向数组中添加的元素
break
case 'splice':
inserted = args.slice(2) //splice有第三个参数时,表示插入元素
break
}
if (inserted) ob.observeArray(inserted) //对添加的数组对象进行observe
// notify change
ob.dep.notify() //通知更新
return result
})
})
执行完上述代码后,结果如下:
arrayMethods
本身就是一个数组,原型自然指向Array.prototype
- 在为数组创建
Observer
实例的时候,会判断value
是否拥有_proto
属性,并进行不同的处理,这意味着Vue
并非仅仅处理严格意义上的数组,也可以处理类数组对象。
上图中的arrayMethods
就是被observe
的数组对象的新原型(定义了7个原型方法)。
arrayMethods
和methodsToPatch
是在执行Vue构造函数前被定义的- 在创建
Observer
实例的时候劫持原型链(数组)或者定义隐藏属性(类数组对象) - 执行特定的7个方法时,执行
mutator
方法
下面看一下被劫持的数组对象的原型链:
被劫持的执行逻辑:
首先建立一组类比:
- 数组对象(我)
- 插入的原型(父亲)
- Array.prototype(祖父)
正常数组对象的执行逻辑(除7个特定方法外):
- 我想吃糖,我没有,跟父亲要
- 父亲有就给你,但是也没有,我去问问你爷爷
- 爷爷有就给你了,没有就说你吃的啥啊?
被Vue劫持的数组对象的执行逻辑(7个特定的方法):
- 我想吃糖,我没有,跟父亲要
- 父亲有糖,但是你先去爷爷那儿拿些糖,父亲给你做个冰糖葫芦吧
数组响应式机制总结:
- 定义数据劫持:对象是通过ES5 API
Object.prototype.defineProperty
来完成的,数组是通过原型链劫持或者定义隐藏属性来完成的。 - 依赖收集:
Vue
会为对象的每个key
创建一个dep
,而只会为数组对象对应的key
创建一个dep
(对于数组元素是引用数据类型,则需额外的考虑)。 - 触发更新:对象是通过
setter
修改器来触发更新的,数组是通过7个原型方法mutator
的执行来触发更新。
嵌套对象响应式的探索
测试案例
<!DOCTYPE html>
<html>
<head>
<script src="../../dist/vue.js"></script>
</head>
<body>
<div id="demo">
<h1>picture</h1>
<button @click="changeTrousers">换裤子</button>
<div>皮肤:{{description.head.skin}}</div>
<div>眼神:{{description.head.eyes}}</div>
<div>辫子:{{description.head.braid}}</div>
<div>裤子:{{description.clothes.trousers.names}}</div>
<div>裤子颜色:{{description.clothes.trousers.color}}</div>
</div>
<script>
// 创建实例
const app = new Vue({
el: '#demo',
data: {
description: {
head: {
skin: 'smooth',
eyes: 'clean',
braid: 'ponytail'
},
clothes: {
trousers: {
name: 'jeans',
color: 'blue'
}
}
}
},
methods:{
changeTrousers(){
let trousers = this.description.clothes.trousers;
trousers.name = 'casual';
trousers.color = 'black';
}
}
});
</script>
</body>
</html>
在src/core/observer/index.js
中defineReactive
方法中打印key的值。
先来看data选项中定义数据劫持的顺序:
我们会发现,定义数据劫持的顺序与data选项中编写的顺序一致。
在defineReactive
方法中创建dep
的时候,将key
作为参数传进去,然后在Dep
的构造函数中接受这个key
值,并打印出来。请看下图:
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep(key)
console.log('定义数据劫持的key:', key)
}
constructor (key) {
this.id = uid++
this.subs = []
console.log('key的值:', key)
}
从图中可以看到,并非所有的dep都是在defineReactive
方法中创建的,在Dep的构造函数打印日志的地方打一个条件断点key==undefined
,然后在调用堆栈中找到无参的new Dep
是在哪儿创建的。
从图中可知,在创建Observer
实例的时候,在Observer
实例上挂载了一个Dep
实例,也就是说data
选项中,如果一个key
对应的值如果是对象或者数组,则会创建两个Dep
实例。在初始化data
的过程中,创建了两种Dep
实例,那么这两种Dep
实例的功能有什么不同呢?
首先,在Dep构造函数中为Dep实例添加一个属性,改造代码如下:
constructor (key) {
this.id = uid++
this.subs = []
this.key = key; //新添加的属性,如果是Observer构造函数中调用,不传key,因此值是undefined
this.isObserverCreated = !key //新添加的属性
}
在watcher的addDep方法中打印日志,改造代码如下:
addDep (dep: Dep) {
//略
if (!this.newDepIds.has(id)) {
//略
console.log('addDep——isObserverCreated:', dep.isObserverCreated)
console.log('addDep——key:', dep.key)
//略
}
}
之所以在addDep方法中打印日志,是因为Dep.addDepend方法中没有进行过滤,一个属性在一个vm实例中可能被访问多次,为了避免重复打印一个key的依赖收集过程,将打印日志代码编写在Watcher的addDep方法中(因为有过滤,避免对一个Dep实例重复收集)
保存代码后,会重新生成vue.js(前提是已经执行npm run dev),刷新页面后可以看到如下日志:
在addDep
方法中addDep——key: undefined
处打一个条件断点:dep.key==undefined
,然后刷新页面,看调用堆栈,如下图:
上图中,通过调用堆栈找到收集依赖的dep,恰好是创建的Observer实例中挂载的dep。
验证我们的猜想:在childOb.dep.depend()
代码传参'childOb'
,透传到addDep方法中,并在addDep方法中将其打印出来,打印日志如下:
由图可知,在收集依赖时,凡是在defineReactive
方法中创建的Dep
实例,调用的都是dep.addDepend
方法,凡是在Observer
实例化时创建的Dep
实例,调用的都是childOb.dep.addDepend
方法。
在初始化阶段,Vue只是为data选项中的属性定义了数据劫持,但并没有覆盖对象或数组动态添加或删除属性的场景。为此,Vue提供了全局方法$set
和$delete
,下面看一下$set的源码
function set (target, key, val) {
//错误传参的拦截代码(自行看源码)
//对于数组的处理
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key);
target.splice(key, 1, val);
return val
}
//target是data选项中的对象或数组,进行observe的时候,会为value添加`__ob__`属性,值就是Observer实例
//而Observer实例化过程中,挂载了一个Dep实例,用途就在于此
var ob = (target).__ob__;
defineReactive$$1(ob.value, key, val);//定义数据劫持
ob.dep.notify();//立即通知更新
return val
}
关于Vue的响应式机制到此告一段落,关于异步更新机制和Diff算法将在专门的章节中介绍,在解读异步更新机制前,先剖析一下watch
,watcher
,computed
的区别。
结束语
千山万水何惧怕,拨开云雾见红霞
—— 祝君好梦 ——