回顾Vue
的生命周期详解,在调用_init
方法初始化Vue
实例时,其中一步是调用initState(vm)
初始化一些状态(props、data、computed、watch、methods),其中initData(vm)
用于初始化data
:
function initData(vm) { //初始化data
...
observe(data, true /* asRootData */);
}
observe(data)
会将用户定义的data
变成响应式的数据,它的创建过程:
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 {
ob = new Observer(value) //为`data`创建一个`Observer`实例
}
if (asRootData && ob) { //如果是根 data,ob.vmCount自增
ob.vmCount++
}
return ob
}
1. 三个类
Vue
实现响应式数据主要用到了三个类,首先就是Observer
观察者类:
1.1 Observer 类
以用户提供的选项data
(vm.$data)为例,假如当前正在调用new Observer(data)
来创建一个Observer
实例:
export class Observer {
value: any; //用户提供的`data`
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value //保存 data
this.dep = new Dep() //dep 实例
this.vmCount = 0
def(value, '__ob__', this) //用 vm.$data.__ob__ 存储当前`observer`实例
if (Array.isArray(value)) {
//如果 value 是数组,添加修改后的 mutators 方法到原型或者数组本身上面
if (hasProto) {
protoAugment(value, arrayMethods) //添加到原型上
} else {
copyAugment(value, arrayMethods, arrayKeys) //添加到数组本身
}
//遍历数组每一项,observe 每一项
this.observeArray(value)
} else {
//如果 value 是对象,遍历对象
this.walk(value)
}
}
walk (obj: Object) {
const keys = Object.keys(obj)
//遍历对象的每一个属性,将所有属性都变为响应式属性
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
observeArray (items: Array) {
//遍历数组的每一项,observe 每一项。
//并没有将任何一项变为响应式属性,因为数组是用 mutators 方法来通知更新的。
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
观察者在观察一个数据时,会分为对象类型和数组类型这两种情况,进行不同的操作,下面分情况讨论。
1.1.1 observe 对象类型
假如当前观察的是一个对象类型的数据obj
:
const obj = {
num: 1,
str: 'a'
}
如第1.1
节所示,在实例化obj
的Observer
时,会执行obj.__ob__.walk(data)
来遍历data
对象,将data
对象的每一个属性变成响应式属性。
假如当前正在执行defineReactive(obj, 'num')
将obj.num
变为响应式属性:
export function defineReactive (
obj: Object,
key: string,
val: any, //obj.num 属性对应的值
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep() //obj.num 属性对应的 Dep 实例
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// 保存初始 getter/setter
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
//继续观察 obj.num 的值,形成了 observe 方法的递归调用,实现了深度遍历 data 对象
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val //obj.num 的值
if (Dep.target) { //如果当前存在目标
dep.depend() //给 Dep.target 添加依赖 —— obj.num 属性对应的 dep(闭包中的 dep)
//如果 obj.num 的值 value 是对象或者数组类型,那么它就有观察者 value.__ob__
if (childOb) {
childOb.dep.depend() //给 Dep.target 添加依赖 —— value.__ob__.dep
//如果 value 是数组类型,就给 Dep.target 添加依赖 —— value 的每一项的 __ob__.dep
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
...
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
//观察 obj.num 的新值
childOb = !shallow && observe(newVal)
//通知更新
dep.notify()
}
})
}
上面提到的依赖主要分为两种:
- 属性对应的依赖
dep
(在闭包中); - 对象的观察者的
dep
属性。
需要注意的是,对象和数组最开始都是作为父对象的属性,执行过defineReactive
方法的,所以它们也有闭包中的dep
。对于对象类型和基本类型来说,只需用到闭包中的依赖。
而数组类型则需要用到两种依赖,让我们来看看为什么会这样。
1.1.2 observe 数组类型
回顾 Observer
构造函数:
if (Array.isArray(value)) {
//如果 value 是数组,添加修改后的突变方法到原型或者数组本身上面
if (hasProto) {
value.__proto__ = arrayMethods; //将突变方法添加到原型上
} else {
copyAugment(value, arrayMethods, arrayKeys) //将突变方法添加到数组本身
}
this.observeArray(value)
}
我们来看arrayMethods
是一个什么样的原型:
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto) //相当于 arrayMethods = {}; arrayMethods.__proto__ = arrayProto;
//突变方法,也就是会改变数组本身的方法
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(function (method) {
//缓存初始方法
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
//调用 array.__ob__.dep.notify() 方法来通知更新
ob.dep.notify()
return result
})
})
数组类型的响应式属性需要用到两种依赖:直接给数组赋一个新值,需要用闭包中的依赖来通知更新;
如果调用 mutators
方法,则需要用array.__ob__.dep
依赖来通知更新。
总的来说,Vue
是使用数据劫持来收集依赖和通知更新的,数据劫持的两种方法:
-
Vue3.0
之前使用ES5
就支持的Object.defineProperty
来实现数据劫持,优点是无须polyfill
,缺点是无法监听对象添加、删除属性的动作,对数组基于下标的修改、对于.length
修改等; - 在
Vue3.0
版本将使用es6
的proxy
来代替Object.defineProperty
,优点是proxy
的强大功能可以监听几乎所有的动作,还能支持监听Map
、Set
、WeakMap
和WeakSet
等类型的数据(简直完美),
缺点是浏览器的支持程度不及 Object.defineProperty
( ie
浏览器完全不支持 proxy
)。
1.2 Dep 类
上面提到的依赖都是 Dep
类的实例,Dep
类的定义如下:
let uid = 0
//Dep 实例是一个可被订阅的对象
export default class Dep {
static target: ?Watcher; //静态属性 Dep.target 表示当前目标,它是一个 Watcher 实例
id: number;
subs: Array; //订阅者数组,订阅者为 Watcher 实例
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
//为目标 Dep.target 收集依赖
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
//通知 subs 中的订阅者们更新
notify () {
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
当Vue
的config.async
为false
时,会先对subs
进行排序,再执行watcher.update
,这是为了保证watchers
订阅者们run
方法的执行顺序;而如果config.async
为true
的话,订阅者们的run
方法是在下一次事件循环执行的,而且在执行前会排一次序,所以就没必要在notify
方法中排序了。
需要注意的是,Vue2.0+
已经移除了Vue.config.async
配置项,也就是说虽然代码中任有余留,但用户已经无法对它进行配置了,所以我们在阅读Vue
源码时可以选择性忽略!config.async
。
在Vue
中,一个时间点只能存在一个目标Dep.target
,其相关操作也是全局的:
Dep.target = null
const targetStack = [] //target 保存在栈中
//target 入栈,Dep.target 始终为栈顶 target
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
//target 出栈,Dep.target 始终为栈顶 target
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
1.3 Watcher 类
Watcher
实例大致可分为三种类型:render Watcher
,user Watcher
和computed Watcher
。
不同类型的Watcher
实例,具有不同的作用,但是所有的Watcher
实例都可以收集依赖。Watcher
类的基本定义:
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
user: boolean; //user Watcher 的 flag
lazy: boolean; //computed Watcher 的 flag
active: boolean;
deps: Array; //Dep 数组,收集的依赖
getter: Function; //收集依赖、求值的方法
value: any;
...
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean //render Watcher 的 flag
) {
//一系列初始化操作
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this //render Watcher 保存于 vm._watcher 属性
}
//vm._watchers 数组用于存储组件实例 vm 的所有 watchers
vm._watchers.push(this)
// 用户和 vue 提供的选项
if (options) {
this.deep = !!options.deep
this.user = !!options.user
...
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.deps = []
...
if (typeof expOrFn === 'function') {
//render watcher 的 getter 通常为 updateComponent 方法,详见 Vue 的生命周期详解
this.getter = expOrFn
} else {
//初始化 user watcher 的 getter
...
}
//在构造函数的最后,执行 this.get(),首次收集依赖。所以说在 new Watcher 时,马上就会收集一次依赖。
this.value = this.lazy ? undefined : this.get()
}
//添加依赖 dep 到 newDeps 中(newDeps 用于过渡,之后会添加到 deps 中)
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)
}
}
}
}
Watcher
实例主要是用原型方法get
来收集依赖的:
Watcher.prototype.get = function () {
//第一步,确定目标
//pushTarget 的定义见第 1.2 节末尾,此时 Dep.target = this
pushTarget(this)
let value
const vm = this.vm
try {
//第二步,为目标收集依赖
//忽视参数 vm,执行了 this.getter() 收集依赖。对于不同类型的 watcher,它们的 getter 不尽相同
value = this.getter.call(vm, vm)
} catch (e) {
...
} finally {
...
//如第 1.2 节所示,this 出栈,此时 Dep.target = Previous Target
popTarget()
//先清空 deps,然后 deps = newDeps
this.cleanupDeps()
}
return value
}
当Dep
实例dep
执行dep.notify
方法通知订阅者更新时,每个订阅者都会执行update
方法进行更新:
Watcher.prototype.update = function () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
//将当前 Watcher 实例添加到队列中
queueWatcher(this)
}
}
如果不是lazy
或者sync
类型,update
方法会执行了queueWatcher(this)
:
var queue = []; //存储所有组件的所有需要更新的 watcher
function queueWatcher (watcher) {
var id = watcher.id;
if (has[id] == null) {
has[id] = true;
if (!flushing) {
//如果任未执行 flushSchedulerQueue,就直接添加该 watcher
queue.push(watcher);
} else {
//如果正在执行 flushSchedulerQueue,就根据 watcher.id 将其插入 queue 中;
//如果目前已经执行到它后面的 watcher,虽然任会被插入 queue 中,但是不会在此次事件循环执行了
var i = queue.length - 1;
while (i > index && queue[i].id > watcher.id) {
i--;
}
queue.splice(i + 1, 0, watcher);
}
// queue the flush
if (!waiting) {
waiting = true
//之前就说过了,Vue2.0+ 可以忽略 !config.async,所以 waiting 同样意义不大
if (!config.async) {
flushSchedulerQueue();
return
}
//在下一事件循环执行 flushSchedulerQueue
nextTick(flushSchedulerQueue);
}
}
}
flushSchedulerQueue
方法会根据id
排序queue
中的所有watcher
,然后执行它们的run
方法,详见Vue
源码或者Vue
的生命周期详解。watcher.run
方法的定义:
Watcher.prototype.run = function () {
if (this.active) {
//在 this.get 中执行 this.getter 收集依赖,并返回新的结果
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may have mutated.
isObject(value) ||
this.deep
) {
//缓存旧值,保存新值
const oldValue = this.value
this.value = value
if (this.user) {
//如果是 user watcher
try {
//调用 this.cb 回调
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
//如果不是 user watcher,this.cb 通常为 ()=>{}
this.cb.call(this.vm, value, oldValue)
}
}
}
}
2 流程
接下来我们分别分析三种Watcher
实例(render Watcher
、user Watcher
和computed Watcher
)实现响应式的原理。
2.1 render Watcher
每个组件实例有且仅有一个render Watcher
,保存于vm._watcher
属性中。它的主要作用是在依赖改变时重新生成组件的虚拟Dom
,并更新到HTML
中。
我们首先假设有如下两个文件:
main.js:
new Vue({ // 根组件
render: h => h(App)
})
---------------------------------------------------
app.vue:
{{obj.num}} // 只用了obj.num属性
export default { // app组件
name: 'app',
data() {
return {
obj: {
num: 1,
str: 'a' // 即使是响应式数据,没被使用就不会进行依赖收集
}
}
}
}
回顾之前的内容,在初始化app
实例时,忽视无关的步骤,会依次执行 app._init()
=> initState(app)
=> initData(app)
=> observe(data, true)
=> new Observer(data)
=> app.$data.__ob__.walk(data)
。
其中data
和app.$data
都是用户提供的data
选项。
回顾第1.1
节,在app.$data.__ob__.walk(data)
方法中会执行defineReactive
方法将data
对象的所有属性变成响应式属性,在这个例子中,响应式属性包括obj
、obj.num
和obj.str
。
这样,在访问这些响应式属性时就会触发其getter
方法从而收集依赖,在修改这些响应式属性的值时就会触发其setter
方法从而通知更新。接下来我们看render Watcher
是在什么时候收集依赖和更新的。
2.1.1 收集依赖
回顾Vue
的生命周期详解,在执行mountComponent
方法挂载组件时,在执行beforeMount
钩子和mounted
钩子之间,会实例化render Watcher
:
new Watcher(vm, updateComponent, noop, {
before: function before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate');
}
}
}, true /* isRenderWatcher */);
从上可以看出render Watcher
的getter
就是updateComponent
方法。回顾第1.3
节中Watcher
类的构造方法,render Watcher
会被存储于app._watcher
中,
在构造函数的最后会执行app._watcher.get
方法,此方法令 Dep.target
= app._watcher
并执行app._watcher.getter
方法。
在执行getter
方法,即updateComponent
方法的过程中,会执行app._render
方法:
const updateComponent = function () {
app._update(app._render(), hydrating);
};
在执行app._render()
时会执行app
组件的render
函数,在这个例子中,app
组件的 render
函数如下:
var render = function() {
var _vm = this
var _h = _vm.$createElement
var _c = _vm._self._c || _h
return _c("div", { attrs: { id: "app" } }, [
_c("div", [_vm._v(_vm._s(_vm.obj.num))]) //此处访问了 app.obj 以及 app.obj.num 属性
])
}
值得注意的是,在这个过程中我们访问了app.obj
以及app.obj.num
属性,而且一个重要的前提是当前的目标 Dep.target
= app._watcher
(你在其他什么乱七八糟的地方也有可能访问app.obj.num
属性,但是你没有目标的话你就无法给目标添加依赖)。
现在,我们访问了app.obj
以及app.obj.num
属性,那么就会调用它们的getter
方法。
- 回顾第
1.1.1
节,在响应式属性的getter
方法中会调用dep.depend()
方法。 - 回顾第
1.2
节中的depend
方法的定义,dep.depend()
方法会为目标Dep.target
添加app.obj
和app.obj.num
属性的依赖,也就是调用Dep.target.addDep(objDep)
和Dep.target.addDep(numDep)
。 - 回顾第
1.3
节中的addDep
方法的定义,Dep.target.addDep(dep)
方法不仅会将依赖objDep
和numDep
添加到依赖数组Dep.target.deps
中,为了保持一致,还会调用dep.addSub(Dep.target)
方法。 - 回顾第
1.2
节中的addSub
方法的定义,dep.addSub(Dep.target)
方法会将Dep.target
添加到依赖objDep
和numDep
的订阅者数组dep.subs
中。
至此,app._watcher
收集依赖objDep
和numDep
,以及objDep
和numDep
收集订阅者app._watcher
的过程结束。
接下来,只要app._watcher.deps
中的任意依赖对应的响应式属性发生改变,就会通知app._watcher
进行更新。
2.1.2 通知更新
我们假设有如下created
钩子和mounted
钩子:
app.vue:
export default { // app组件
data() {
...
},
created() {
this.obj.num = 2;
},
mounted() {
this.obj.num = 3;
}
}
created
钩子是用于混淆的,上文已经说过,render Watcher
是在beforeMount
钩子和mounted
钩子之间实例化的,实例化时会马上收集一次依赖。所以在执行created
钩子时,虽然也会执行app.obj.num
属性的setter
方法,但是此时它的依赖的订阅者数组subs
为空,所以没有订阅者更新。
我们在mounted
钩子中修改了app.obj.num
属性的值,所以会调用它的setter
方法。
- 回顾第
1.1.1
节中的setter
方法的定义,它会执行app.obj.num
属性对应的依赖numDep
的notify
方法来通知更新。 - 回顾第
1.2
节中的notify
方法的定义,numDep.notify
方法会依次执行numDep
的订阅者数组subs
中的所有watcher
的update
方法。在本例中,numDep.subs
中只有app._watcher
。 - 回顾第
1.3
节中的update
方法的定义,app._watcher.update
方法会将app._watcher
添加到队列queue
中,等到下一次事件循环再执行app._watcher.run
方法。 - 回顾第
1.3
节中的run
方法的定义,执行app._watcher.run
时,会执行app._watcher.get
方法。 - 回顾第
1.3
节中的get
方法的定义,执行app._watcher.get
时,首先会确定目标,然后会执行app._watcher.getter
方法(即updateComponent
方法)更新组件。 - 没错,接下来又进入了收集依赖的过程。
2.2 user Watcher
user Watcher
是指用户自己定义的watcher
。假如用户定义了如下所示的app
组件:
export default {
name: 'app', // app组件
data() {
return {
obj: {
num: 1,
str: 'a'
}
}
},
watch: {
'obj.num': {
handler(val) {
console.log(val)
}
}
},
created() {
this.$watch('obj.str', val => {...})
}
}
使用watch
选项或者$watch
方法监听一个属性时,最终都会为该属性生成一个对应的user Watcher
。
从源码来看,在调用initState(app)
初始化状态时,如果有watch
选项,就会调用initWatch(app, app.$options.watch)
初始化watch
状态,而在其中又会调用app.$watch(expOrFn, handler, options)
生成一个userWatcher
实例。
也就是说,watch
选项中的属性其实最终也是通过$watch
方法来实现监听的。而其中的过程我就不细说了,具体请看
参考链接: Vue原理解析(九):搞懂computed和watch原理,减少使用场景思考时间
最终生成user Watcher
的语句是:
const watcher = new Watcher(vm, expOrFn, cb, options)
我们以obj.num
属性的user Watcher
为例,上述语句中的参数vm
= app实例
,expOrFn
= 'obj.num'
,cb
= val => {console.log(val)}
,options
= {user: true}
。回顾Watcher
类的构造函数:
export default class Watcher {
user: boolean; //user Watcher 的 flag
value: any;
...
constructor (
vm: Component, // app
expOrFn: string | Function, // 'obj.num'
cb: Function, // val => {console.log(val)}
options?: ?Object, // {user: true}
isRenderWatcher?: boolean
) {
this.vm = vm
vm._watchers.push(this)
if (options) {
this.user = !!options.user //true,user Watcher 的 flag
...
}
this.cb = cb //val => {console.log(val)},即用户定义的回调
...
if (typeof expOrFn === 'function') {
...
} else {
//初始化 user watcher 的 getter
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
...
}
}
//在构造函数的最后,执行 this.get() 收集依赖。
this.value = this.lazy ? undefined : this.get()
}
}
执行parsePath('obj.num')
,来获取user Watcher
的getter
:
const bailRE = new RegExp(`[^${unicodeRegExp.source}.$_\\d]`)
export function parsePath (path: string): any {
//如果包含不正确的字符,直接返回
if (bailRE.test(path)) {
return
}
const segments = path.split('.')
//返回的方法就是 getter 方法
return function (obj) {
//循环赋值 obj,深度遍历路径,返回最终的结果
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
}
回顾第1.3
节中的get
方法的定义,在之后调用getter
方法时会传入app
实例作为实参,所以在getter
方法中,刚开始obj
= app
;
第一次循环,obj
= app.obj
;第二次循环,obj
= app.obj.num
,最终返回 app.obj.num
。
2.2.1 收集依赖
user Watcher
和render Watcher
收集依赖过程的区别在于watcher.getter
方法不同:
-
user Watcher
和render Watcher
一样,会在new Watcher()
的最后执行userWatcher.get
方法; - 执行
userWatcher.get
时,首先会确定目标Dep.target
=userWatcher
,然后会执行userWatcher.getter
方法,传入app
实例作为实参; - 观察上述
userWatcher.getter
方法,可知先访问了属性app.obj
,再访问了属性app.obj.num
,所以会依次调用这两个属性的getter
方法; - 接下来与
render Watcher
收集依赖的过程类似,就不细说了。
接下来,只要userWatcher.deps
中的任意依赖对应的响应式属性发生改变,就会通知userWatcher
进行更新。
2.2.2 通知更新
user Watcher
通知更新的过程和render Watcher
略有不同,区别在于user Watcher
最后会执行用户定义的回调函数。例子不变:
app.vue:
export default { // app组件
data() {
...
},
mounted() {
this.obj.num = 3;
}
}
我们在mounted
钩子中修改了app.obj.num
属性的值,所以会调用它的setter
方法。
- 在
setter
方法中会调用app.obj.num
属性对应的依赖numDep
的notify
方法来通知更新。 -
numDep.notify
方法会依次执行numDep
的订阅者数组subs
中的所有watcher
的update
方法。结合上一节,目前,numDep.subs
中包含userWatcher
和renderWatcher
这两个订阅者,其中userWatcher
先生成。 -
userWatcher.update
方法会将userWatcher
添加到队列queue
中,等到下一次事件循环再执行userWatcher.run
方法。 - 执行
userWatcher.run
时,首先会执行userWatcher.get
方法;执行userWatcher.get
时,首先会确定目标,然后会执行userWatcher.getter
方法收集依赖,并且获得app.obj.num
属性的新值,新值将从getter
方法一直返回到run
方法中。 - 回顾第
1.3
节中的run
方法的定义,回到run
方法中后,接下来会缓存旧值,保存新值,将新值和旧值作为实参传入userWatcher.cb
方法中,并执行userWatcher.cb
方法,即用户定义的回调函数。
回顾Vue
的生命周期详解,之所以说userWatcher
比renderWatcher
先生成,是因为initState(app)
是在beforeCreate
和created
钩子之间执行的,
所以userWatcher
也是在这之间实例化的;而前面已经说过,renderWatcher
是在beforeMount
钩子和mounted
钩子之间实例化的,所以显而易见userWatcher
比renderWatcher
先生成。
2.3 computed Watcher(lazy Watcher)
computed Watcher
是指用户定义的计算属性所对应的watcher
。假如用户定义了如下所示的app
组件:
export default {
name: 'app', // app组件
data() {
return {
obj: {
num: 1,
str: 'a'
}
}
},
...
computed: {
doubleNum() {
return this.obj.num * 2;
}
}
}
使用computed
选项定义一个计算属性时,会为该计算属性生成一个对应的computed Watcher
。
从源码来看,在调用initState(app)
初始化状态时,如果有computed
选项,就会调用initComputed(app, app.$options.computed)
初始化computed
状态:
function initState (vm) {
vm._watchers = []; //app 实例的所有 watcher 的集合
var opts = vm.$options; //app 实例的选项
if (opts.props) { initProps(vm, opts.props); }
if (opts.methods) { initMethods(vm, opts.methods); }
//初始化 data
if (opts.data) {
initData(vm);
} else {
observe(vm._data = {}, true /* asRootData */);
}
//初始化 computed(先于 initWatch)
if (opts.computed) { initComputed(vm, opts.computed); }
//初始化 watch
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch);
}
}
结合上一节和initState
方法的定义,相信聪明的你已经发现,在不考虑$watch
方法的情况下,同一组件中三种类型的watcher
实例的生成顺序为:
computed Watcher
=> user Watcher
=> render Watcher
。
watcher
实例的生成顺序也就是它们的watcher.id
的大小顺序(从小到大)。回顾Vue
的生命周期详解,在下一事件循环执行flushSchedulerQueue
方法时,
会根据watcher.id
对queue
中的watcher
进行排序,然后再执行这些watcher
的run
方法。
所以queue
中同一组件的watcher
的run
方法的执行顺序为(不考虑$watch
生成的user Watcher
和其它组件的watcher
):
user Watcher
=> render Watcher
。
因为computed Watcher
不会被加入queue
中,也不是通过watcher.run
方法来更新的,所以我们忽略它。而user Watcher
先于render Watcher
执行显然是符合我们期望的。
回归正题,在initComputed
方法中:
function initComputed(vm, computed) {
const watchers = vm._computedWatchers = Object.create(null) // 创建一个纯净对象存储 computedWatchers
for(const key in computed) {
const userDef = computed[key]; //用户的定义
const getter = typeof userDef === 'function' ? userDef : userDef.get; // 计算属性 key 对应的回调函数
watchers[key] = new Watcher(vm, getter, noop, {lazy: true}) // 实例化 computed-watcher
...
}
}
我们以计算属性doubleNum
为例,回顾Watcher
类的构造函数:
export default class Watcher {
constructor (
vm: Component, //app
expOrFn: string | Function, //用户定义的回调函数,function() { return this.obj.num * 2; }
cb: Function, //_ => {}
options?: ?Object, //{lazy: true}
isRenderWatcher?: boolean
) {
this.vm = vm
vm._watchers.push(this)
if (options) {
this.lazy = !!options.lazy // true,computed Watcher 的 flag
...
}
this.dirty = this.lazy // 表示是否需要对 computed 属性进行计算
...
if (typeof expOrFn === 'function') {
this.getter = expOrFn
}
// 不执行 this.get()
this.value = this.lazy ? undefined : this.get()
}
}
实例化computed Watcher
后,回到initComputed
方法中:
function initComputed(vm, computed) {
...
for(const key in computed) {
const userDef = computed[key]; //用户的定义
...
if (!(key in vm)) {
defineComputed(vm, key, userDef);
} else {
if (key in vm.$data) { // key 不能和 data 里面的属性重名
warn(("The computed property \"" + key + "\" is already defined in data."), vm);
} else if (vm.$options.props && key in vm.$options.props) { // key 不能和 props 里面的属性重名
warn(("The computed property \"" + key + "\" is already defined as a prop."), vm);
}
}
}
}
接下来调用defineComputed
方法定义计算属性:
function defineComputed (target, key, userDef) {
//在 SSR 中,计算属性不缓存
var shouldCache = !isServerRendering();
if (typeof userDef === 'function') {
//创建计算属性的 getter,存于属性描述符中
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: createGetterInvoker(userDef);
sharedPropertyDefinition.set = noop;
}
//创建响应式的计算属性 target.key
Object.defineProperty(target, key, sharedPropertyDefinition);
}
同样以计算属性app.doubleNum
为例,在defineComputed
方法中,首先设置了doubleNum
的属性描述符(Property Descriptor
)对象sharedPropertyDefinition
的get
和set
属性,然后用Object.defineProperty
方法在app
实例上定义了一个属性doubleNum
。
这样就为app.doubleNum
属性定义了getter
和setter
方法,实现了数据劫持,详见MDN
文档Object.defineProperty()。
2.3.1 收集依赖
在SSR
中,计算属性不缓存,每次访问属性都会直接调用用户定义的回调函数,但是其他情况下会进行缓存。计算属性的缓存是通过createComputedGetter
方法实现的:
function createComputedGetter (key) {
//返回计算属性的 getter
return function computedGetter () {
//首先获取刚才生成的 computedWatcher 实例
var watcher = this._computedWatchers && this._computedWatchers[key];
if (watcher) {
if (watcher.dirty) { //如果需要计算
watcher.evaluate(); //进行计算
}
if (Dep.target) { // 当前的 watcher,如果是页面渲染时触发的这个方法,Dep.target = render watcher
watcher.depend() // 将 computedWatcher 的依赖全部复制给 Dep.target
}
return watcher.value
}
}
}
---------------------------------------------------------------------------------
Watcher.prototype.evaluate = function () {
this.value = this.get() // 计算属性求值
this.dirty = false // 表示计算属性已经计算,不需要再计算
}
Watcher.prototype.depend () {
let i = this.deps.length // computedWatcher.deps 的长度
while (i--) {
this.deps[i].depend() // 收集依赖
}
}
在createComputedGetter
方法中对用户定义的回调函数(存于computedWatcher.getter
)进行了封装,最终返回了计算属性的getter
方法。
以计算属性app.doubleNum
为例,在定义了它的getter
方法后,当用户首次访问doubleNum
属性,调用它的getter
方法时,会进行第一次依赖收集:
- 在
doubleNum
属性的getter
方法中,首先获取相应的computedWatcher
实例。回顾第2.3
节中computedWatcher
的实例化过程,可知computedWatcher.getter
= function() { return this.obj.num * 2; } ,即用户定义的回调; - 如果属性需要进行计算,就执行
computedWatcher.evaluate
方法,在其中执行computedWatcher.get
方法收集依赖,并且设置计算标识dirty
为false
,表示已经计算过了; - 在执行
computedWatcher.get
方法时,首先会确定目标Dep.target
=computedWatcher
,然后会执行computedWatcher.getter
方法(即用户定义的回调函数)来获取计算属性的当前值; - 观察用户定义的回调函数,可知它执行时先访问了属性
app.obj
,再访问了属性app.obj.num
,所以会依次调用这两个响应式属性的getter
方法添加依赖,添加依赖的具体步骤见第2.1.1
节; - 然后回到
computedWatcher.get
方法中,执行popTarget()
方法,令Dep.target
=Previous Target
,Previous Target
就是之前的watcher
。
简单来说,如果用户用$watch
方法监听了一个计算属性,那么在访问这个计算属性时(即调用属性的getter
方法时),当前target
栈为[...,userWatcher
,computedWatcher
]。在收集完computedWatcher
的依赖之后,
就会执行popTarget()
方法将computedWatcher
出栈,所以当前的栈顶元素为userWatcher
,Dep.target
=userWatcher
; - 回到
doubleNum
属性的getter
方法中,如果Dep.target
(在computedWatcher
前面的watcher
)不为空,就执行computedWatcher.depend
方法,该方法依次执行computedWatcher
收集的依赖的depend
方法; - 调用
Dep.prototype.depend
方法为当前的Dep.target
添加依赖,具体步骤见第2.1.1
节; - 最终回到
doubleNum
属性的getter
方法中,返回计算属性的当前值给用户。
接下来,只要computedWatcher.deps
中的任意依赖对应的响应式属性发生改变,就会通知computedWatcher
进行更新。
2.3.2 通知更新
例子如下:
app.vue:
export default { // app组件
data() {
...
},
mounted() {
this.doubleNum; //先访问一次添加依赖
this.obj.num = 3;
}
}
我们在mounted
钩子中修改了app.obj.num
属性的值,所以会调用它的setter
方法。
- 在
setter
方法中会调用app.obj.num
属性对应的依赖numDep
的notify
方法来通知更新。 -
numDep.notify
方法会依次执行numDep
的订阅者数组subs
中的所有watcher
的update
方法。目前,numDep.subs
中包含userWatcher
、renderWatcher
和computedWatcher
。 - 回顾第
1.3
节中的update
方法的定义,computedWatcher.update
方法会将计算标识computedWatcher.dirty
设为true
,表示下一次访问该属性时需要重新进行计算。
总的来说,computed
属性是一种惰性求值(延迟求值)属性,这种属性不是在被赋予表达式时求值,而是在被访问时求值。另外计算属性的值会被缓存,极大地提升了性能。
简单来说,计算属性不访问就不会计算,访问了就会计算并缓存,等依赖更新后就会将计算标识赋值为true
,等下一次访问时会重新计算并缓存。
3 总结:
-
收集依赖总是从调用
watcher.get
方法开始,在watcher.get
方法中调用watcher.getter
方法时会访问响应式属性,调用响应式属性的getter
方法,从而给Dep.target
添加依赖(同时给依赖添加订阅者),而调用watcher.get
方法收集依赖的时机有:- for render Watcher & user Watcher:
watcher
实例化时会在构造函数的最后调用watcher.get
方法;watcher
更新后,下一次事件循环watcher
执行run
方法时会调用watcher.get
方法; - for computed Watcher: 首次访问计算属性,会调用计算属性的
getter
方法,从而调用watcher.get
方法;watcher
更新后,下一次访问计算属性时会调用watcher.get
方法;
- for render Watcher & user Watcher:
-
通知更新:
- 从给任意类型的响应式属性设置新值开始,调用响应式属性的
setter
方法,在setter
方法中调用属性所对应的依赖的notify
方法,通知该依赖的所有订阅者进行更新,然后在下一次事件循环,执行所有订阅者的watcher.run
方法; - 从调用数组类型的响应式属性的突变方法开始,在突变方法中调用
array.__ob__.dep
依赖的notify
方法,通知该依赖的所有订阅者进行更新,然后在下一次事件循环,执行所有订阅者的watcher.run
方法。
- 从给任意类型的响应式属性设置新值开始,调用响应式属性的
对于render Watcher
和user Watcher
,再次收集依赖都是调用watcher.run
方法来实现的,而watcher.run
方法除了收集依赖这个副作用外,它的主要作用还有:
- for render Watcher: 在
watcher.get
方法内部调用watcher.getter
方法实现组件的更新(首先生成组件的VNode
,然后直接进行首次渲染或者diff
后进行再次渲染); - for user Watcher: 首先在
watcher.get
方法内部调用watcher.getter
方法获取所监听属性的新值,然后回到watcher.run
方法中,将属性的新值与旧值作为参数一起传入watcher.cb
(即用户定义的回调)中并执行。