job 是什么?
job 是依赖响应式对象属性值变更触发执行的函数(执行时响应式对象属性值建立依赖)。
举几个 job 的栗子
这里看不懂可以先跳过,下面有详细介绍
- redner job
渲染组件实例,首次挂载立即执行,执行阶段获取响应式对象属性时和其建立依赖,
生成依赖后,其他响应式对象属性值变更时触发 render job
或可以通过 $forceUpdate 强制触发。
- computed job:
某 job 执行段需要计算值,会先触发生成 comoputed job
生成时,立即执行计算函数,computed job 会和计算函数使用到响应式对象的属性值建立依赖
生成后,某 job 会和当前计算属性值建立依赖
Vue 会在计算属性值被某 job 使用时在内部使用 ComputedRefImpl 创建一个只有 value 属性的对象 o。
value 的 get 函数生成 computed job,并执行计算函数,并使用 track 函数将 value 和触发 get 函数 的某 job 建立依赖。
computed job 的作用是,打开控制缓存的开关变量,并触发依赖 value 的 job
Vue 还会在实例上创建一个与计算函数同名的属性,其 get 函数为 () => o.value
当响应式对象变更,触发 computed job,computed job 又触发 value 的 job,value 的 job 使用计算值,又重新触发 value 的 get 函数
- watch job 组件实例子初始化,获取 oldValue 时立即生成 watch job
后续依赖的响应式对象属性变更时,触发 watch job
在首次挂载阶段同步执行 watch job
在挂载后,异步执行 watch jop(存于 pendingPreFlushCbs),先于 render job
存储待执行 job 的容器
以下容器皆为数组:
- pendingPreFlushCbs
- queue
- pendingPostFlushCbs
注:以上排序也是执行顺序
操作 job 的 api
flushJobs 依次执行 pendingPreFlushCbs queue pendingPostFlushCbs 三个数组中的 job
queueFlush 执行 Promise.resolve().then(flushJobs)
queueJob 将 job 推入 queue,并立马执行 queueFlush
queuePreFlushCb 将 job 推入 pendingPreFlushCbs,并立马执行 queueFlush
queuePostFlushCb 将 job 推入 pendingPostFlushCbs,并立马执行 queueFlush
nextTick queueFlush.then(fn) 或 Promise.resolve().then(fn)
可以看出 nextTick 永远在当前 job 执行完毕后执行。
创建 job 的 effect 方法
每一次创建时,都会清除和响应式对象属性已有的依赖,并重新建立依赖
// 接收要执行的fn,并返回一个 job。
function effect(fn, options = EMPTY_OBJ) {
if (isEffect(fn)) {
fn = fn.raw;
}
/*
若 options 配置了 scheduler 方法,
响应式对象属性值变更引起 job 执行时会执行 scheduler(job),
否则,直接执行 job 函数(也就是执行fn)
*/
const effect = createReactiveEffect(fn, options);
/*
生成 render job 时,lazy 为 false,立即执行 render job
*/
if (!options.lazy) {
effect();
}
return effect;
}
function createReactiveEffect(fn, options) {
// 为了方便称呼这个 effect 函数为 job
const effect = function reactiveEffect() {
if (!effect.active) {
return options.scheduler ? undefined : fn();
}
/*
假如
*/
if (!effectStack.includes(effect)) {
/*
清除 effect 对响应式对象属性值的依赖
因为 fn 每次执行时,都会和响应式对象重新建立依赖
所以在执行前,要将已存在的依赖关系清除
*/
cleanup(effect);
try {
enableTracking();
effectStack.push(effect);
// 使用全局变量存储当前 job,在执行 fn 时,关联响应式对象会需要
activeEffect = effect;
return fn();
}
finally {
effectStack.pop();
resetTracking();
activeEffect = effectStack[effectStack.length - 1];
}
}
};
effect.id = uid++;
effect.allowRecurse = !!options.allowRecurse;
effect._isEffect = true;
effect.active = true;
effect.raw = fn;
effect.deps = [];
/*
1、若 options 有 scheduler 方法,
则响应式对象引起 job 执行阶段,会执行 scheduler(job),若无,执行 job。
2、举个例子:render job 配置 scheduler 为 queueJob,将渲染任务放在微任务中异步执行。
*/
effect.options = options;
return effect;
}
render job
instance.update = effect(function componentEffect() {
render...
}, {
scheduler: queueJob
})
computed job
this.effect = effect(getter, {
lazy: true,
scheduler: () => {
if (!this._dirty) {
this._dirty = true;
// 触发依赖 value 值的 job
trigger$1(toRaw(this), "set" /* SET */, 'value');
}
}
});
watch job
const runner = effect(getter, {
lazy: true,
scheduler = () => {
// 组件渲染阶段触发同步执行,组件更新阶段触发异步执行
if (!instance || instance.isMounted) {
queuePreFlushCb(job);
}
else {
// with 'pre' option, the first call must happen before
// the component is mounted so it is called synchronously.
job();
}
}
});
什么是响应式对象
举个栗子
- data 方法创建的对象
- comouted 使用 ComputedRefImpl 类创建的一个对象 o,其只有一个属性值 value(注:vue 又手动在 this 上定义一个与 fn 同名的属性,其 get 函数 为 () => o.value, set 函数为 (v) => o.value = v )
- props 所有传入的 prop 值组成一个响应式对象
响应式对象属性值变更,会触发依赖该属性值的 job。
job 如何关联 响应式对象?
响应式对象属性值变更会触发 job 执行
总结如下两点:
- 在 job 执行阶段,主动获取响应式对象属性时,使用 track 建立响应式对象属性和当前 job 的依赖。
- 在响应式对象属性值变更时,使用 trigger$1 根据已有的映射,触发依赖改属性值得 job。
track
track(target, type, key) { ... }
track 设置依赖响应式对象 target 的 key 属性变更的 job,存于 targetMap。
- targetMap 是一个 weakMap,键值为响应式对象 target,值为 depsMap
- depsMap 是一个 weakMap, 键值为响应式对象的某键值 key,值为依赖该键值得 job 集合
怎么知道当前 job 呢?上文有介绍,在 job 执行阶段,会设置全局变量 activeEffect 为当前job。
track 什么时候执行呢?比如 render job 渲染模版阶段,获取 proxy(data) 的 test 属性,触发 get 劫持函数 fn,在 fn 中判断当前存在 activeEffect,即执行 track 函数绑定依赖。
trigger
trigger$1(target, type, key, newValue, oldValue, oldTarget){ ... }
trigger 会根据 targetMap 找出要执行的 job,若 job 无特殊处理,直接执行。若有配置 scheduler ,执行scheduler(effect)
举个栗子
<template>
<div>{{ test }} {{ cTest }} </div>
</tempalte>
export default {
data() {
return {
test: '姑且称 data 为响应式对象'
}
},
computed() {
cTest() {
return this.test
}
}
}
Vue 使用 proxy 劫持 data,生成 proxy data
const proxy = new Proxy(target, targetType === 2 /* COLLECTION */ ? collectionHandlers : baseHandlers);
target 为要监听的对象,普通对象使用 baseHandlers,集合使用 collectionHandlers
渲染模时,获取 data 属性,会触发 baseHandlers 的 get 函数,get 函数会触发 track(target, "get" , key);
redner job
render job 即组件初始化后的渲染流程,首次挂载立即执行,或者在响应式对象属性变更时执行。
render job 渲染模版阶段,获取 proxy data 属性值时,触发 proxy data 的 get 方法这中,使用 track 建立依赖。在 proxy data 的 set 方法中,使用 trigger 触发依赖。
computed job
class ComputedRefImpl {
constructor(getter, _setter, isReadonly) {
this._setter = _setter;
this._dirty = true;
this.__v_isRef = true;
this.effect = effect(getter, {
lazy: true,
scheduler: () => {
if (!this._dirty) {
this._dirty = true;
trigger$1(toRaw(this), "set" /* SET */, 'value');
}
}
});
this["__v_isReadonly" /* IS_READONLY */] = isReadonly;
}
get value() {
// the computed ref may get wrapped by other proxies e.g. readonly() #3376
const self = toRaw(this);
if (self._dirty) {
self._value = this.effect();
self._dirty = false;
}
track(self, "get" /* GET */, 'value');
return self._value;
}
set value(newValue) {
this._setter(newValue);
}
}
Object.defineProperty(ctx, key, {
enumerable: true,
configurable: true,
get: () => c.value,
set: v => (c.value = v)
});
computed 有些复杂,以上面的 cTest 为栗子
render job 阶段,获取 cTest,触发 cTest job,cTest job 执行阶段,使用了 data 中的 test,建立了 test 的 cTest job 的依赖关系,cTest job 执行完后,又主动建立了 cTest 计算值和 render job 的依赖关系。
当 test 变更时,触发 cTest job,执行 cTest job 的 scheduler,又主动触发 render job,render job 又重新计算 cTest 的值,也就是执行 ComputedRefImpl 中的 get 方法