依赖收集
TODO: 待完善
get
前面,我们已经讲到了在组件渲染过程会安装渲染 Effect。然后,进入渲染组件的阶段,即 renderComponentRoot(),而此时会调用 proxyToUse,即会触发 runtimeCompiledRenderProxyHandlers 的 get,即:
get(target, key) {
...
else if (renderContext !== EMPTY_OBJ && hasOwn(renderContext, key)) {
accessCache[key] = 1 /* CONTEXT */;
return renderContext[key];
}
...
}
可以看出,此时会命中 accessCache[key] = 1 和 renderContext[key] 。对于前者是做一个缓存的作用,后者是从当前的渲染上下文中获取 key 对应的值((对于本文这个 case,key 对应的就是 count,它的值为 0)。
那么,我想这个时候大家会立即反应,此时会触发这个 count 对应 Proxy 的 get。但是,在我们这个 case 中,用了 toRefs() 将 reactive 包裹导出,所以这个触发 get 的过程会分为两个阶段:
两个阶段的不同点在于,第一阶段的
target为一个object(即上面所说的toRefs的对象结构),而第二阶段的target为Proxy对象{count: 0}。具体细节可以看我上篇文章
Proxy 对象toRefs() 后得到对象的结构:
{
value: 0
_isRef: true
get: function() {}
set: ƒunction(newVal) {}
}
我们先来看看 get() 的逻辑:
function createGetter(isReadonly = false, shallow = false) {
return function get(target, key, receiver) {
...
const res = Reflect.get(target, key, receiver);
if (isSymbol(key) && builtInSymbols.has(key)) {
return res;
}
...
// ref unwrapping, only for Objects, not for Arrays.
if (isRef(res) && !isArray(target)) {
return res.value;
}
track(target, "get" /* GET */, key);
return isObject(res)
? isReadonly
? // need to lazy access readonly and reactive here to avoid
// circular dependency
readonly(res)
: reactive(res)
: res;
};
}
第一阶段:触发普通对象的
get
由于此时是第一阶段,所以我们会命中 isRef() 的逻辑,并返回 res.value 。此时就会触发 reactive 定义的 Proxy 对象的 get。并且需要注意的是 toRefs() 只能用于对象,否则我们即时触发了 get 也不能获取对应的值(这其实也是看源码的一些好处,深度理解 API 的使用)。
track
第二阶段:触发
Proxy对象的get
此时属于第二阶段,所以我们会命中 get 的最后逻辑:
track(target, "get" /* GET */, key);
return isObject(res)
? isReadonly
? // need to lazy access readonly and reactive here to avoid
// circular dependency
readonly(res)
: reactive(res)
: res;
可以看到,首先会调用 track() 函数,进行依赖收集,而 track() 函数定义如下:
function track(target, type, key) {
if (!shouldTrack || activeEffect === undefined) {
return;
}
let depsMap = targetMap.get(target);
if (depsMap === void 0) {
targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(key);
if (dep === void 0) {
depsMap.set(key, (dep = new Set()));
}
if (!dep.has(activeEffect)) {
dep.add(activeEffect);
activeEffect.deps.push(dep);
if (process.env.NODE_ENV !== "production" && activeEffect.options.onTrack) {
activeEffect.options.onTrack({
effect: activeEffect,
target,
type,
key,
});
}
}
}
可以看到,第一个分支逻辑不会命中,因为我们在前面分析 run() 的时候,就已经定义 ishouldTrack = true 和 activeEffect = effect。然后,命中 depsMap === void 0 逻辑,往 targetMap 中添加一个键名为 {count: 0} 键值为一个空的 Map:
if (depsMap === void 0) {
targetMap.set(target, (depsMap = new Map()));
}
而此时,我们也可以对比
Vue 2.x,这个{count: 0}其实就相当于data选项(以下统称为data)。所以,这里也可以理解成先对data初始化一个Map,显然这个Map中存的就是不同属性对应的dep
然后,对 count 属性初始化一个 Map 插入到 data 选项中,即:
let dep = depsMap.get(key);
if (dep === void 0) {
depsMap.set(key, (dep = new Set()));
}
所以,此时的 dep 就是 count 属性对应的主题对象了。接下来,则判断是否当前 activeEffect 存在于 count 的主题中,如果不存在则往主题 dep 中添加 activeEffect,并且将当前主题 dep 添加到 activeEffect 的 deps 数组中。
if (!dep.has(activeEffect)) {
dep.add(activeEffect);
activeEffect.deps.push(dep);
// 最后的分支逻辑,我们这次并不会命中
}
最后,再回到 get(),会返回 res 的值,在我们这个 case 是 res 的值是 0。
return isObject(res)
? isReadonly
? // need to lazy access readonly and reactive here to avoid
// circular dependency
readonly(res)
: reactive(res)
: res;
总结
好了,整个 reactive 的依赖收集过程,已经分析完了。我们再来回忆其中几个关键点,首先在组件渲染过程,会给当前 vm 实例创建一个 effect,然后将当前的 activeEffect 赋值为 effect,并在 effect 上创建一些属性,例如非常重要的 deps 用于保存依赖。
接下来,当该组件使用了 data 中的变量时,会访问对应变量的 get()。第一次访问 get() 会创建 data 对应的 depsMap,即 targetMap。然后再往 targetMap 的 depMap 中添加对应属性的 Map,即 depsMap。
创建完属性的 depsMap 后,一方面会往该属性的 depsMap 中添加当前 activeEffect,即收集订阅者。另一方面,将该属性的 depsMap 添加到 activeEffect 的 deps 数组中,即订阅主题。从而,形成整个依赖收集过程。
整个
get过程的流程图