重学Vue源码,根据黄轶大佬的vue技术揭秘,逐个过一遍,巩固一下vue源码知识点,毕竟嚼碎了才是自己的,所有文章都同步在 公众号(道道里的前端栈) 和 github 上。
正文
上篇依赖收集过程中说到,在 defineRective 过程中访问到定义在 obj 里的属性的时候,就会触发它的 getter,在 render 过程中把 渲染watcher 作为依赖收集起来,那在修改这个属性的时候,其实就是出发了 setter,也就是说:改变数据就会触发 setter 过程:
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
// ...
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()
}
})
来看下 setter 都做了什么事情:
首先新值和旧值作对比,如果二者一样就什么都不做,否则就做一些逻辑,有两个比较关键的就是 childOb = !shallow && observe(newVal),如果新值 neVal 也是一个对象,那就执行一次 observe 变成响应式的对象。另一个就是 dep.notify(),通知所有的订阅者,也就是派发更新的过程。
派发过程
来看一下这个 notify(),在 src/core/observer/dep.js 中:
class Dep {
// ...
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
逻辑比较简单,遍历了所有的 subs,也就是 Watcher 实例数组,然后通知它们,调用每一个 watcher 的 update 方法,update 定义在 src/core/observer/watcher.js 中:
class Watcher {
// ...
update () {
/* istanbul ignore else */
if (this.computed) {
// 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.
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)
}
}
}
computed 和 sync 它们是针对不同的状态分析,先忽略,接着它会走到 queueWatcher(this) 函数,它定义在 src/core/observer/scheduler.js 中:
const queue: Array<Watcher> = []
let has: { [key: number]: ?true } = {}
let waiting = false
let flushing = false
/**
* Push a watcher into the watcher queue.
* Jobs with duplicate IDs will be skipped unless it's
* pushed when the queue is being flushed.
*/
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
// 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
if (!waiting) {
waiting = true
nextTick(flushSchedulerQueue)
}
}
}
queueWatcher 实际上是一个队列的概念,它会把所有需要更新的 watcher 往 queue 队列里面推,
这里先获取 watcher.id,因为这个 id 在 new Watcher 的时候是自增的,所以不同的 watcher 的 id 是唯一的, 然后 has 对象的目的是保证同一个 watcher 只添加一次,因为每次 update 的时候,都会走一次 queueWatcher,如果是相同的watcher,那它们的 id 一定是一样的,为了避免重复watcher的存在(去除多余的wather操作),所以用了一个 null 判断,从而保证一个队列里不会重复的watcher。
接着判断了 flushing,最后通过 waiting 状态保证对 nextTick(flushSchedulerQueue) 的调用只进行一次。nextTick 是一个异步的实现,本篇先略过,可以先理解为逻辑会在下一个tick中进行(JS是单线程的)。到这里之后,队列已经完毕了,这个 flushSchedulerQueue 其实就是遍历这个队列,进行一些操作,来看一下这个 flushSchedulerQueue 都做了什么,它的定义在 src/core/observer/scheduler.js 中:
let flushing = false
let index = 0
/**
* Flush both queues and run the watchers.
*/
function flushSchedulerQueue () {
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.
queue.sort((a, b) => a.id - b.id)
// do not cache length because more watchers might be pushed
// as we run existing watchers
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()
resetSchedulerState()
// call component updated and activated hooks
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)
// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit('flush')
}
}
对 queue 进行了一个排序,注释上说了三种场景:
- 组件更新是父到子的,因为组件创建过程也是父到子,所以要保证父的
watcher在前面,子的watcher在后面,先父后子。 - 我们自定义一个组件对象,然后在代码里写的
watcher属性,或者我们调用$watch方法的时候,都会创建一个user watcher,它是在渲染watcher之前的,所以user watcher也要放在前面。 - 当子组件的销毁(destroy),是在父组件的
watcher回调中执行的时候,也就是一个组件在父组件的watcher执行期间被销毁,那么它对应的watcher执行都可以跳过,所以父组件的watcher应该先执行。
以上三点就是 queue,从大到小排列的目的,排好之后开始遍历。
先拿到 before,如果有 before,就执行 before 函数,然后拿到 id,然后把 has[id] 置为 null,然后执行 run() 函数,后面有一个针对无限更新的判断,如果有就报错。那为什么 has[id] 已经是 null 了,下面又判断 has[id] != null 呢? 就是因为 watcher.run() 这个函数,它是做了一层回调,回调里的逻辑有可能再次触发 queueWatcher,这样的话 queue 的长度就变了:
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
// 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)
}
// ...
}
}
可以看到,这时候 flushing 为 true,就会执行到 else 的逻辑,然后就会从后往前找,找到第一个待插入 watcher 的 id 比当前队列中 watcher 的 id 大的位置。把 watcher 按照 id 的插入到队列中,因此 queue 的长度发生了变化。
这也就是为什么做 for 循环的时候,不用一个变量来保存 queue.length,而是每次都计算这个长度的原因,从而报一个无限更新的错。
接下来看下这个 watcher.run() 是干什么的,它的定义在 src/core/observer/watcher.js 中:
class Watcher {
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run () {
if (this.active) {
this.getAndInvoke(this.cb)
}
}
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) ||
this.deep
) {
// 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)
}
}
}
}
run 函数只执行了 getAndInvoke 方法,并传入 watcher 的回调函数 cb。
getAndInvoke 函数也比较简单,先通过 get 方法得到一个新值 value,然后让新值和之前的值做对比,如果值不相等,或者新值 value 是一个对象,或者它是一个 deep watcher 的话,就执行回调(user watcher 比 渲染watcher 多了一个报错提示而已),注意回调函数 cb 执行的时候会把第一个和第二个参数传入新值 value 和旧值 oldValue,这就是当我们自定义 watcher 的时候可以拿到新值和旧值的原因。
对于渲染watcher而言,它的回调函数 cb 其实就是一个空函数,在执行 value = this.get() 的时候,就会调用 get():
get () {
pushTarget(this)
let value
const vm = this.vm
try {
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
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
就会执行 getter,那对于渲染watcher而言:
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
回调函数cb 就是一个 noop(空函数,定义在 src/shared/util.js),它的 getter 就是一个 updateComponent:
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
那在执行 getAndInvoke 的时候,就会再次执行 updateComponent,就会再次调用 vm._update 和 vm._render 进行重新渲染。
所以在修改组件相关的响应式数据的时候,会触发watcher的 update,在 nextTick 后,执行 flushSchedulerQueue,再执行里面的 watcher.run(),接着会执行 getAndInvoke,对于渲染watcher而言,就会重新做一次渲染。
最后,在 flushSchedulerQueue 进行完一系列操作之后,调用了一个 resetSchedulerState 函数:
const queue: Array<Watcher> = []
let has: { [key: number]: ?true } = {}
let circular: { [key: number]: number } = {}
let waiting = false
let flushing = false
let index = 0
/**
* Reset the scheduler's state.
*/
function resetSchedulerState () {
index = queue.length = activatedChildren.length = 0
has = {}
if (process.env.NODE_ENV !== 'production') {
circular = {}
}
waiting = flushing = false
}
就是把这些控制流程状态的变量恢复到初始值,然后把 watcher 队列清空。
总结
依赖收集是把订阅了数据变化的 watcher 装入一个队列,派发更新其实就是当响应式数据发生变化,通知所有订阅了这个数据变化的 watcher 执行 update,并在 nextTick 后执行 flush,把所有的变化都放入下一个tick,避免多次执行异步。
我的公众号:道道里的前端栈,每一天一篇前端文章,嚼碎的感觉真奇妙~