一、总述
前一章节,通过对prop,data所定义的属性建立观察类和发布器,但此时的发布类Dep中sub空空如也,如何实现watcher的注册,并在属性发生变化时,实现更新。本章节将继续介绍订阅Watcher。
在介绍前,我们先思考下,用户定义的prop,data在哪些方法或者表达式是需要实现响应式变化的?
首先用户自定义的watch,computed是需要的,另外我们的视图也会用到大量的属性表达式(如前面的实例{{item.id}}),也是需要的。method中虽然用到了这些数据,但是都是及时调用,所以不需要的。
事实上,源码中也是在这三个场景创建watcher,如果阅读过我前面的博客,在initWatcher,initComputer(第五篇),以及mount(第六篇)简单的介绍过,做过一些知识的铺垫。
这三种watcher分别为user watcher,compute watcher,以及render watcher,本篇就结合前面的知识,详细介绍这几种watcher实现原理。
二、user watcher
我们以下面的的watch为运行实例:
data:{
msg:'this is msg',
items:[
{id:1},
{id:2},
{id:3}
]
}
watch: {
msg: function (val, oldVal) {
console.log('new: %s, old: %s', val, oldVal)
},
....
}
回顾下第五篇initWatcher的创建watcher过程,如下:
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
...
//创建watcher对象,进行监听
const watcher = new Watcher(vm, expOrFn, cb, options)
...
}
其中主要入参,vm,即vue对象;expOrFn,待观察的表达式,即本例中属性msg对象;cb,回调函数,即本例中msg属性的对应的方法。watcher的定义位于src/core/observer/watcher.js。
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
computed: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
dep: Dep;
deps: Array<Dep>;
newDeps: Array<Dep>;
depIds: SimpleSet;
newDepIds: SimpleSet;
before: ?Function;
getter: Function;
value: any;
constructor (
vm: Component,//组件对象
expOrFn: string | Function,//待观察的表达式
cb: Function,//回调函数,更新的时候调用
options?: ?Object,
isRenderWatcher?: boolean
) {
//1、初始化变量
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.computed = !!options.computed
this.sync = !!options.sync
this.before = options.before
} else {
this.deep = this.user = this.computed = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.computed // for computed watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// parse expression for getter
//2、解析表达式,获取getter方法
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = function () {}
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
//3、依赖收集
if (this.computed) {
//对于计算属性watcher,此时没有立即进行依赖收集,在执行render函数时收集
this.value = undefined
this.dep = new Dep()
} else {
//对于普通watcher,调用get方法,进行依赖收集
this.value = this.get()
}
}
...
}
1、构造方法
我们先看下其构造方法。主要过程包括:
(1)、初始化变量。主要变量如下:
deep,表示是否要进行深度监听,即属性嵌套的变量进行监听。
computed,表示是否是计算属性。
sync,表示更新时,是否需要同步执行。
deps,newDeps,该watcher对应的维护的发布器数组。
(2)、解析表达式,获取getter方法,如果expOrFn是function类型,则直接设置为getter方法,否则调用parsePath方法返回属性对象作为getter方法,本例中则使用后一种,执行结果为vm[msg]。
export function parsePath (path: string): any {
...
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
}
(3)、依赖收集,这部分处理的很巧妙,对于非计算属性,直接调用了get方法(对于计算属性的watcher我们等会在表)。继续看下get方法。
2、get方法
get () {
//1、将当前的watcher压栈
pushTarget(this)
let value
const vm = this.vm
try {
//2、核心代码,依赖收集
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
//3、后置处理
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
(1)将自身的watcher对象压入栈,设置全局的变量Dep.target为当前的watcher对象。
export function pushTarget (_target: ?Watcher) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}
(2)执行getter方法,触发该属性的get劫持,我们回顾前一章节定义的劫持方法。
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
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表示的就是当前的watcher对象,非空,继续调用Dep类的depend方法
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
最终又回调了watcher中的addDep方法,
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
//加入到watcher的dep数组
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
//加入到dep的sub数组
dep.addSub(this)
}
}
}
该方法,首先将dep保存到watcher的newDeps数组中,然后调用Dep的addSub,将watcher对象加入到sub数组中。
addSub (sub: Watcher) {
this.subs.push(sub)
}
整个调用过程比较绕,这样做的目的,就是为了建立dep和watcher间的双向链表。
(3)后置处理,包括deep处理,清除当前的watcher对象等。
整个过程完成后,msg属性的Dep对象的sub中就添加了watcher对象。msg的依赖模型如下:
三、compute watcher
以下面的计算属性computeMsg为运行实例:
<div id="app">
{{computeMsg}}
...
</div>
var vm = new Vue({
data:{
msg:'this is msg',
items:[
{id:1},
{id:2},
{id:3}
]
}
watch: {
msg: function (val, oldVal) {
console.log('new: %s, old: %s', val, oldVal)
},
computed:{
computeMsg:function(){
return "this is computed msg:"+this.msg
}
}
....
}
该实例中,创建computeMsg计算属性,并在模板(template)中使用。computeMsg计算属性表达式中调用了msg属性。
我们来回顾下第五章节的initComputed方法。
function initComputed (vm: Component, computed: Object) {
// $flow-disable-line
const watchers = vm._computedWatchers = Object.create(null)
// computed properties are just getters during SSR
const isSSR = isServerRendering()
//1、循环计算属性
for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
...
if (!isSSR) {
// create internal watcher for the computed property.
//2、为每个属性创建watcher
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
...
//3、劫持数据变化,创建监听方法
defineComputed(vm, key, userDef)
}
}
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
const shouldCache = !isServerRendering()
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: userDef
sharedPropertyDefinition.set = noop
} else {
....
}
//4、并对计算属性的getter和setter进行劫持
Object.defineProperty(target, key, sharedPropertyDefinition)
}
}
//getter方法劫持
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
//依赖收集,将订阅类添加到
watcher.depend()
//返回值
return watcher.evaluate()
}
}
}
在该方法中,循环所定义的计算属性,为每个计算属性创建watcher,并设置getter方法,监听数据变化(计算属性setter方法用的较少)。watcher的主要入参:vm,即vue对象;getter,计算属性的表达式,即本例中computeMsg对应的表达式。继续看下watcher的构造方法。
1、构造函数
constructor (
vm: Component,//组件对象
expOrFn: string | Function,//待观察的表达式
cb: Function,//回调函数
options?: ?Object,
isRenderWatcher?: boolean
) {
...
// parse expression for getter
//1、对于计算属性,表达式即为getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = function () {}
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
if (this.computed) {
//2、对于计算属性,创建了dep,此时没有立即进行依赖收集,
this.value = undefined
this.dep = new Dep()
} else {
this.value = this.get()
}
}
watcher的入参,vm表示vue对象,getter为待观察的表达式,即计算属性函数。
与user watcher相比,有两个不同的地方。
(1)、由于expOrFn 是计算属性的表达式,类型是function,所以走一个分支,将getter设置为计算属性表达式。
(2)、并没有调用get方法进行依赖收集,只是创建了dep对象,该对象保存依赖该计算属性的watcher。
计算属性的依赖关系不同于普通的属性,它即依赖于其表达式包含的普通属性,比如说本例中的属性msg,同时又被其他调用者依赖,如本例中调用computeMsg的模板表达式({{computeMsg}})。本例的依赖模型如下:
那么问题来了,既然这些依赖关系没有在定义的时候进行收集,那是什么时候做的呢?答案就是在模板调用的时候。
计算属性的设计初衷是简化模板的表达式,避免太多的逻辑导致模板的复杂。所以,只有在模板调用的情况下才会触发依赖,如果只定义不调用,进行依赖就会造成浪费。
下面我们来介绍与模板render相关的watcher(render watcher),并看下如何完成红色标注的依赖收集。
四、render watcher
我们先回顾下第六章节介绍挂载时的mountComponent方法。
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
....
//2、定义updateComponent,vm._render将render表达式转化为vnode,vm._update将vnode渲染成实际的dom节点
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
//3、首次渲染,并监听数据变化,并实现dom的更新
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
....
}
当时我们说这个watcher有两个作用,1、实现首次dom渲染,并完成依赖收集。2、监听模板所包含表达式的变化,实现update。
该watcher是数据"render"模板的"桥梁",称之为render watcher,其核心部分体现在入参expression(第二个参数,即updateComponent)。我们看下该watcher的初始化的主要过程:
constructor (
vm: Component,//组件对象
expOrFn : string | Function,//待观察的表达式
cb: Function,//回调函数
options?: ?Object,
isRenderWatcher?: boolean
) {
....
if (typeof expOrFn === 'function') {//1、设置getter方法为表达式
this.getter = expOrFn
} else {
.....
}
if (this.computed) {
....
} else {
//2、执行get方法,进行依赖收集
this.value = this.get()
}
}
1、expOrFn为function,所以设置getter方法为表达式,即为updateComponent方法。
2、由于不是计算属性,与user watcher一样,执行get方法,实际就是执行updateComponent方法。
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
该方法包含vm._render(),vm._update两个执行阶段,我们在第六章重点分析过,我们不再做详细介绍。今天我们重点回答前面提出的问题,如何实现依赖关系的收集。
以上面的计算属性为例,在模板中,我们使用了"{{computeMsg}}"。
<div id="app">
{{computeMsg}}
...
</div>
该模板经过编译(参见前面的编译部分)后,该部分的render表达式"_s(computeMsg)",在执行vm._render时,就会触发computeMsg所设置的getter方法。
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
//1、依赖收集,将订阅类添加到dep中
watcher.depend()
//2、返回值
return watcher.evaluate()
}
}
}
1、调用watcher.depend,将当前的render watcher对象添加到compteMsg的dep中,完成依赖关系的收集。
depend () {
if (this.dep && Dep.target) {
this.dep.depend()
}
}
注意这里的Dep.target指的是render watcher。依赖模型如下:
2、调用watcher.evaluate()。
evaluate () {
//如果有更新,则重新计算,否则返回缓存值
if (this.dirty) {
this.value = this.get()
this.dirty = false
}
return this.value
}
这是this指的是compute watcher。其get方法就是属性表达式。
function(){
return "this is computed msg:"+this.msg
}
由于表达式中包含了msg属性,在执行过程中,又触发msg的get监听方法,将该compute watcher 添加到msg的dep中,完成被依赖关系的收集。最终的依赖模型如下:
至此,computeMsg的依赖和被依赖关系收集完成。大家也可以尝试分析下模板中调用普通属性的依赖关系收集过程。
五、派发更新
目前msg属性的dep发布器中收集了两个依赖的watcher对象,当我们重新设置msg值,会发生什呢?
当重新设置msg值,就会触发set方法。
Object.defineProperty(obj, key, {
...
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()
}
})
调用其发布器的notify方法。
//通知相关的watcher类更新
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
循环属性关联的watcher类,由前面可知,此时dep的sub集合中有user watcher和computed watcher两个对象。分别调用其update方法。
update () {
/* istanbul ignore else */
if (this.computed) {//计算属性watcher处理
// A computed property watcher has two modes: lazy and activated.
// It initializes as lazy by default, and only becomes activated when
// it is depended on by at least one subscriber, which is typically
// another computed property or a component's render function.
//当没有订阅计算属性时,不需要计算,仅仅设置dirty为true,表示下次重新计算
if (this.dep.subs.length === 0) {
// In lazy mode, we don't want to perform computations until necessary,
// so we simply mark the watcher as dirty. The actual computation is
// performed just-in-time in this.evaluate() when the computed property
// is accessed.
this.dirty = true
} else {
// In activated mode, we want to proactively perform the computation
// but only notify our subscribers when the value has indeed changed.
this.getAndInvoke(() => {
this.dep.notify()
})
}
} else if (this.sync) {//同步处理,立即执行
this.run()
} else {//异步处理,进入堆栈
queueWatcher(this)
}
}
update分为三个分支处理,我们分别对三种情况进行分析。
(1)、计算属性处理。
(2)、同步处理,立即执行。
(3)、异步处理,加入到堆栈中。
1、计算属性
对于计算属性的watcher对象,判断是否有订阅过该计算属性,如果没有(即该计算属性的dep的sub集合是否为空),则设置dirty为true,下次将重新计算。
如本例中,computeMsg是被render watcher依赖,所以会进入else分支,执行getAndInvoke方法。
this.getAndInvoke(() => {
this.dep.notify()
})
getAndInvoke (cb: Function) {
//获取最新的值
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) ||//object对象
this.deep//深度watcher
) {
// set new value
//更新值
const oldValue = this.value
this.value = value
this.dirty = false
//执行回调函数
if (this.user) {
try {
cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
cb.call(this.vm, value, oldValue)
}
}
}
该方法核心部分是执行传入的回调cb方法,即this.dep.notify(),此时递归调用render watcher的update,又回到了update方法。
2、同步处理
如果不是计算属性(本例中的user watcher,render watcher),并设置了同步处理,则调用run方法。如:
run () {
if (this.active) {
this.getAndInvoke(this.cb)
}
}
run方法调用getAndInvoke,该方法核心部分是:
(1)执行get方法获取value,对于render watcher ,执行updateComponent方法,重新生成Vnode,并patch,实现dom的更新(下一章节将详细说明)。
(2)回调cb方法,如本例中的msg的watch表达式(注意:render watcher的cb是noon)
3、异步处理
对于非同步,则调用queueWatcher将watcher压入堆栈中。在将这个方法之前,我们先看如果更新queue中的watcher,即flushSchedulerQueue方法,在src/core/observer/scheduler.js中
function flushSchedulerQueue () {
//1、设置标识位flush为true,标识正在刷新中
flushing = true
let watcher, id
// Sort queue before flush.
// This ensures that:
// 1. Components are updated from parent to child. (because parent is always
// created before the child)
// 2. A component's user watchers are run before its render watcher (because
// user watchers are created before the render watcher)
// 3. If a component is destroyed during a parent component's watcher run,
// its watchers can be skipped.
//2、将queue数组从小到大排序
queue.sort((a, b) => a.id - b.id)
// do not cache length because more watchers might be pushed
// as we run existing watchers
//3、循环队栈queue,执行watcher.run方法,实现更新。
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
// in dev build, check and stop circular updates.
if (process.env.NODE_ENV !== 'production' && has[id] != null) {
circular[id] = (circular[id] || 0) + 1
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' + (
watcher.user
? `in watcher with expression "${watcher.expression}"`
: `in a component render function.`
),
watcher.vm
)
break
}
}
}
// keep copies of post queues before resetting state
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()
//4、重置相关的状态
resetSchedulerState()
// call component updated and activated hooks
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)
// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit('flush')
}
}
我们看下核心的步骤:
(1)设置标识位flush,表示queue正在处理中。
(2)根据queue中的watcher的id,从小到大进行排序(也就是创建的先后)
(3)循环queque中的watcher,执行run方法,实现更新。
(4)queue中的执行完成后,则重置相关的状态,包括flush,wait等,等待下一次执行。
整个处理过程还是比较清晰的,这里注意一点,在第3步中,queue是可能动态变化的。
现在回过头来,继续看queueWatcher,是如何将watcher加入到queue中,又是如何触发flushSchedulerQueue执行的。
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {//对于同一个watcher,不会重复加入到queue,避免多次触发
has[id] = true
//1、将watcher加入队列中
if (!flushing) {//尚未刷新,则加入队栈,待执行
queue.push(watcher)
} else {//2、正在刷新中,则动态的插入到到对应位置。
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1
//从后往前,查找对应位置
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
//3、通过nexttick执行queue中的watcher
if (!waiting) {
waiting = true
nextTick(flushSchedulerQueue)
}
}
}
将watcher加入queue队栈中,分两种情况,
(1) flushSchedulerQueue未执行,则将watcher加入到queue即可,待下一次执行。
(2)flushSchedulerQueue执行中,则从后往前查找对应的位置,然后插入到queue。
(3)通过nextTick,调用flushSchedulerQueue,实现queue中watcher的更新。vue的DOM是异步更新的,nextTick确保了在DOM更新后再执行,在这里可以认为下一个事件循环的"tick"。nextTick机制实现了"批量"的更新,效率更高。
这里要注意,对于同一个watcher,不能重复的加入到queue中,避免多次触发。
六、总结
本章节的逻辑还是比较复杂。我们将各个方法间的调用关系总结下,便于大家理解。