依赖收集
讲依赖收集之前我们需要先了解三个点:
Observe类:用于将响应式对象的属性转换成可以被检测的属性(为其属性添加getter和setter)
Dep类:用于收集当前响应式对象的依赖
Watcher类:作为一个中介(观察者),当数据发生变化时,通过watcher中转通知组件。watcher实例分为渲染watcher、计算watcher、侦听器watcher
vue2.x,中等粒度依赖,用到数据的组件是依赖
在getter中收集依赖,在setter中出发依赖(diff)
Dep
整个getter依赖收集的核心是Dep,将依赖收集的代码封装成一个Dep类,用它来专门管理依赖,每个Observer的实例成员中都有一个Dep的实例;
import type Watcher from './watcher'
import { remove } from '../util/index'
let uid = 0
//dep是一个可观察对象,可以有多个指令订阅它
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++ //Dep实例的id是为了方便去重
this.subs = [] //subs是为了存储需要依赖收集的watcher
}
addSub (sub: Watcher) {//添加当前的观察者对象
this.subs.push(sub)
}
removeSub (sub: Watcher) {//移除当前的观察者对象
remove(this.subs, sub)
}
depend () {//依赖收集
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {...}
}
Dep.target = null
const targetStack = []
export function pushTarget (_target: ?Watcher) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}
export function popTarget () {
Dep.target = targetStack.pop()
}
wather
watcher是一个中介,数据发生变化时通过watcher中转,通知组件
比较巧妙的点是:watcher把自己设置到全局的一个指定位置,然后读取了数据,所以会触发这个数据的getter.在getter中就能得到当前正在读取的watcher,并把这个watcher收集到Dep中。
export default class Watcher {
constructor(
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean // 是否是渲染watcher
) {
this.getter = expOrFn // 在get方法中执行
/*是否是计算属性,是的话并不会立刻求值,而是实例化一个dep*/
if (this.computed) {
this.value = undefined
this.dep = new Dep()
} else {
/* 不是计算属性会立刻求值*/
this.value = this.get()
}
}
/* 获取getter的值并且重新进行依赖收集 */
get() {
// 设置Dep.target = this
pushTarget(this)
let value
//this.getter对应的就是updateComponent函数
value = this.getter.call(vm, vm)
// 将观察者实例从target栈中取出并设置给Dep.target
popTarget()
this.cleanupDeps()
return value
}
addDep(dep: Dep) { ... } // 添加一个依赖关系到Deps集合中
cleanupDeps() { ... } // 清除newDeps中无用watcher依赖
update() { ... } //当依赖发生变化进行回调
run() { ... } //在update被调用时会回调
getAndInvoke(cb: Function) { ... }
evaluate() { ... } // 收集该watcher的所有deps依赖
depend() { ... } // 收集该watcher的所有deps依赖,只有计算属性使用
teardown() { ... } //把自身从所有依赖收集订阅列表删除
}
我们知道当访问响应式数据是会触发它们的getter方法,那这些对象什么时候被访问?
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
首先当我们实例化一个渲染Watcher时,进入Watcher构造函数,执行它的get()方法,进入get方法会执行pushTarget()将当前的渲染watcher赋值给Dep.target,并进行压栈操作。在这里又执行value = this.getter.call(vm, vm)(实际上就是在执行vm._update(vm._render(),hydrating))
这个函数首先执行vm.render生成渲染VNode,从而在这个过程中会完成对当前vm上数据的访问,触发数据对象的getter,而每个对象值的getter都有一个dep ,触发了getter就会触发dep.depend(),也就会执行Dep.target.addDep(this)
将当前的watcher订阅到这个数据的dep的subs中,目的是为后续数据变化时能知道通知那些订阅者
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)
}
}
}
到这里基本上依赖收集的过程就完了。
依赖清空
但是依赖收集完后,如何依赖清空呢?
cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
在Watcher构造函数中,我们发现它定义了这几个属性depIds和deps、newDepIds和newDeps。
deps和newDeps表示Watcher实例持有的Dep实例数组。deps表示上一次添加的Dep实例数组。newDeps表示新添加的Dep实例数组。
在执行cleanupDeps时,我们发现遍历了debs,移除了newDepIds中不存在的deps的watcher订阅,然后交换deps和newDeps、depIds和newDepIds。并且将newDepIds和newDeps清空。
那为什么做deps订阅的移除呢?
为了避免这种场景:v-if 已不需要的模板依赖的数据发生变化时就不会通知watcher去 update
当我们根据v-if渲染a和b模版,当条件满足我们渲染a,会访问a中的数据,对a的数据添加getter进行依赖收集,修改了a的数据会通知那些订阅者。当时改变了条件渲染b后,我们如果没有进行依赖移除,修改到a的模版数据时,又会触发a数据的订阅的回调,这显然有浪费的。
派发更新
收集的目的就是修改数据后对相关的依赖派发更新。
当数组中的响应数据被修改就会触发setter逻辑,然后调用dep.notify(),然遍历数组调用sub[i].update(即调用每一个watcher的update)
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()
}
}
}
class Watcher {
/* 当依赖发生变化进行回调 */
update () {
/*
*计算属性监视器有两种模式:惰性模式和激活模式。
*默认情况下,它初始化为lazy;
*只有当至少有一个订阅者依赖它时才会被激活
*/
if (this.computed) { //computed watcher
if (this.dep.subs.length === 0) {
this.dirty = true
} else {
this.getAndInvoke(() => {
this.dep.notify()
})
}
} else if (this.sync) { //sync watcher
// sync为true,就可以在当前Tick中同步执行watcher的回调函数
this.run()
} else {
/*这里引入了watcher队列,也是派发更新的优化点
*不是每次数据变化都触发watcher的回调,而是添加到队列,
*在nextTick中执行flushSchedulerQueue
*/
queueWatcher(this)
}
}
}
queueWatcher中通过nextTick将flushSchedulerQueue方法,放入全局的callback数组中。
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
//has确保同一个watcher仅添加一次
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
// 已经刷新,根据它的id连接观察器如果已经超过了它的id,它将立即运行
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// 刷新队列
if (!waiting) {
waiting = true
nextTick(flushSchedulerQueue)
}
}
}
flushSchedulerQueue中对队列进行排序后,遍历队列执行watcher.run
function flushSchedulerQueue () {
flushing = true
let watcher, id
// 队列排序从小到大
// 确保以下几点:
// 1.组件从父组件更新到子组件,watcher也是从到子
// 2.用户的自定义watcher要优先于渲染watcher执行
// 3.一个组件在父组件的watcher运行期间被破坏,则它对应的watcher都可以被跳过。
queue.sort((a, b) => a.id - b.id)
// 遍历队列,拿到对应的watcher,执行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()
......
}
}
}
watcher.run()实际上实在在行getAndInvoke
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 ||
isObject(value) ||
this.deep
) {
// 即使值相同,深层watcher和对象/数组上的watcher也应该触发,因为值可能已经发生了变化。
//设置新值
const oldValue = this.value
this.value = value
this.dirty = false
if (this.user) {
try {
/* 回调函数传入了value 和旧值 oldValue
*这就是为什么我们自定义watcher可以拿到新旧值
*/
cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
cb.call(this.vm, value, oldValue)
}
}
}
}
flushSchedulerQueue:负责刷新wather队列,即执行queue数组中的每个watcher的run方法,从而进入更新阶段。比如执行组件更新函数或者执行用户watcher的回调函数
vue做派发更新的一个优化点:不是每次数据改变都出发watcher回调,而是把watcher添加到一个队列里,然后再nextTick后执行flushSchedulerQueue
到这里大概就讲完了vue2.x依赖收集和派发更新的过程,博主也是在学习中,🈶️不正确的地方请各位友友指正