系列文章目录
Vue3源码 第一篇-总览
Vue3源码 第二篇-Reactive API
Vue3源码 第四篇-Vue3 setup
Vue3源码 第五篇-Vue3 模版compile AST生成篇
前言
这篇文章主要是阅读effect、trigger、track相关的源码,也就是对依赖的收集以及触发的一个过程,不涉及到render,queueJob等渲染和调度的源码,之后会分章节阅读render和queueJob部分的源码
一、响应性是什么?
大家可以去官网了解响应性,官网的讲解就很好,在阅读源码前可以先学习下官网的响应性原理,对学习底层源码会有更好的理解。
二、源码阅读
想要读懂响应性的源码,首先要明白Vue3响应性是如何去实现,响应性实现的功能是,当数据变更的时候,视图会自动的发起更新,我们简单画图理解下功能。Data Proxy 作为数据代理来拦截get,set 操作,让我们得知数据发生改变,但我们此时还无法更新到视图层,因为我们不知道谁受变化的Data影响,这时候就需要effect副作用函数登场了,effect会包裹在所有对数据变化有依赖的函数外层(组件的渲染和计算属性和watch等),通过effect可以知道当前到底是哪个函数在运行了,Data Proxy也可找到是谁在依赖他,也就是图中track跟踪所做的事。当值变化时,通过set调用trigger触发器来告诉effect需要执行data变化后带来的影响,不同的effect调用不同的schedule任务来完成值变化后需要做的事情。
那这里就有几个关键的问题,第一:我们如何监听数据的变化也就是Data Proxy,第二:如何得知当前正在运行的函数,第三:Data Proxy如何收集依赖于自己的函数,第四:依赖项变更时如何通知正在运行的函数。带着这几个问题我们一步步从源码中获取答案。话不多说,开冲~~~
1.数据变更
首先我们看第一个问题如何知道数据变更了:上一篇Vue3源码 第二篇-Reactive API更大家一起了解了Reactive API 响应性API,我们知道Vue3是通过ES6 Proxy新特性来实现对所有数据的代理,我们可以通过代理去拦截到数据的get和set操作。
2.如何得知当前正在运行的函数
通过官网的解释,我们知道在组件渲染(computed和watch会在之后的文章了解其源码)的外层包裹了effect副作用函数,所以我们可以找到创建组件时的源码,从其中找到创建effect的方法,所以在mountComponent() 挂载组件上我们找到了创建effect的方法 setupRenderEffect,我们来看下这个创建的源码:
const setupRenderEffect = (instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized) => {
// create reactive effect for rendering 创建组件渲染的副作用函数effect,赋值到instance.update
// effect 有2个参数,第一个是组件渲染的代码,effect包裹在他的外层,第二个参数是一个option,传入相关配置项,配置项在下面解释
instance.update = effect(function componentEffect() {
// ....组件渲染的代码,这里只去了解响应性,之后的篇章会去单独了解组件渲染的源码。
}, createDevEffectOptions(instance) );
};
function createDevEffectOptions(instance) {
return {
scheduler: queueJob, // 值更新时调用trigger时触发
allowRecurse: true, // 是否可以递归
onTrack: instance.rtc ? e => invokeArrayFns(instance.rtc, e) : void 0,
onTrigger: instance.rtg ? e => invokeArrayFns(instance.rtg, e) : void 0
};
}
function effect(fn, options = EMPTY_OBJ) {
// 如果已经创建了,就直接获取
if (isEffect(fn)) {
fn = fn.raw;
}
// 创建effect
const effect = createReactiveEffect(fn, options);
// 如果是立即执行的,就直接调用,computed是lazy的
if (!options.lazy) {
// 执行effect中包裹的函数这里包裹着的是组件渲染函数,并且在执行前设置当前全局正在运行的副作用为该effect!
effect();
}
return effect;
}
function createReactiveEffect(fn, options) {
// 定义副作用函数
const effect = function reactiveEffect() {
if (!effect.active) {
return options.scheduler ? undefined : fn();
}
// 如果当前effect栈中不存在该effect
if (!effectStack.includes(effect)) {
// 清空当前effect中记录的deps(依赖项)
cleanup(effect);
try {
enableTracking();
// 将effect加入到栈中
effectStack.push(effect);
// 设置全局当前激活的effect为该effect,这里我们就知道了当前正在运行的函数!!
activeEffect = effect;
// 运行副作用包裹内的函数,这里是组件渲染函数
return fn();
}
finally {
// 最后从栈顶推出
effectStack.pop();
resetTracking();
// 将当前全局激活的effect设置为栈中的下一个
activeEffect = effectStack[effectStack.length - 1];
}
}
};
effect.id = uid++;
effect.allowRecurse = !!options.allowRecurse; // 是否可以递归
effect._isEffect = true; // 是否是effect
effect.active = true; // effect是否激活
effect.raw = fn; // 源码
// 这个存放着当前effect依赖的dep,deps会在track时赋值,同时值也会放入这里,
// 主要是用cleanup时删除和该effect有关的依赖项,重新获取依赖项。
effect.deps = [];
effect.options = options; // 配置项
return effect;
}
3.Data Proxy如何收集依赖于自己的函数
Data Proxy 如何收集依赖自己的函数呢?简单的来说,哪个函数获取了需要我的值就证明依赖我,有几种情况会获取值呢?获取值的情况大体可以分为 has,ownKeys,get,下面就是对象Proxy的handle,主要是拦截了一下几个操作。
const mutableHandlers = {
get, // 获取值的拦截
set, // 更新值的拦截
deleteProperty, // 删除拦截
has, // 模版with绑定访问对象时会拦截
ownKeys // 获取属性key列表
};
我们看下这几个程序内部,都有调用track的地方
function createGetter(isReadonly = false, shallow = false) {
return function get(target, key, receiver) {
// .... 省略获取的代码
// 如果不是只读就进入track
if (!isReadonly) {
track(target, "get" /* GET */, key);
}
// ....
return res;
};
}
function has(target, key) {
const result = Reflect.has(target, key);
if (!isSymbol(key) || !builtInSymbols.has(key)) {
track(target, "has" /* HAS */, key);
}
return result;
}
function ownKeys(target) {
track(target, "iterate" /* ITERATE */, isArray(target) ? 'length' : ITERATE_KEY);
return Reflect.ownKeys(target);
}
我们再看一下track内部的代码
const targetMap = new WeakMap(); // key为对象,值存的是收集到的effect,WeekMap的key必须是对象,比Map好的是不会内存泄漏
function track(target, type, key) {
// 如果全局不存在正在运行的effect,就返回
if (!shouldTrack || activeEffect === undefined) {
return;
}
// 先查看targetMap是否存在该target(变量)的副作用Map,如果不存在就要创建一个他的effectMap,
// 如果存在就要查询effectMap中是否存在该key的effect
let depsMap = targetMap.get(target);
// 如果不存在就就创建该对象的一个依赖的Map
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
// 查看该key是否有依赖他的effect
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
// 如果当前正在运行的函数不在依赖中
if (!dep.has(activeEffect)) {
// 向dep中添加该effect(渲染组件的副作用函数)
dep.add(activeEffect);
// 并且在effect中也添加该依赖,这里就是之前提到的,dep保存了effect,effect也保存了dep,
// 主要是为了effect重新运行时,将收集到该effect的dep删除该effect,重新收集。这样就不需要在每次dep新增时考虑删除旧数据。
activeEffect.deps.push(dep);
if (activeEffect.options.onTrack) {
activeEffect.options.onTrack({
effect: activeEffect,
target,
type,
key
});
}
}
}
4.依赖项变更时如何通知正在运行的函数
通知的话很容易想到,只要拦截到更新相关的操作就可以了。所以就是set和delete相关的操作。所以在Proxy的set和deleteProperty拦截器中能找到trigger函数
function createSetter(shallow = false) {
return function set(target, key, value, receiver) {
// ... 省略部分更新代码
// 查看target是否有当前key,修改和添加属性逻辑不同所以需要判断
const hadKey = isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: hasOwn(target, key);
// don't trigger if target is something up in the prototype chain of original
// 如果是原型链上的值变化了,不去做任何操作
if (target === toRaw(receiver)) {
// 添加和修改逻辑不同
if (!hadKey) {
trigger(target, "add" /* ADD */, key, value);
}
else if (hasChanged(value, oldValue)) {
trigger(target, "set" /* SET */, key, value, oldValue);
}
}
return result;
};
}
function deleteProperty(target, key) {
const hadKey = hasOwn(target, key);
const oldValue = target[key];
const result = Reflect.deleteProperty(target, key);
if (result && hadKey) {
trigger(target, "delete" /* DELETE */, key, undefined, oldValue);
}
return result;
}
trigger函数会取出对应key依赖的effect去执行他所对应的scheduler。
function trigger(target, type, key, newValue, oldValue, oldTarget) {
// 获取该target的依赖Map,在track方法中向其中添加了依赖
const depsMap = targetMap.get(target);
// 如果没有effect依赖他就直接返回
if (!depsMap) {
// never been tracked
return;
}
// 创建一个一会需要执行的effect去重数组
const effects = new Set();
// 添加该key对应的effect到effects中
const add = (effectsToAdd) => {
if (effectsToAdd) {
effectsToAdd.forEach(effect => {
// 如果当前不是正在运行的副作用或者当前副作用允许递归就添加到一会要执行的effects中
// 组件渲染时的option中传入了 allowRecurse = true,组件渲染的effect是允许递归的
if (effect !== activeEffect || effect.allowRecurse) {
// 将需要的effect都添加到这个去重数组中,这些就是数据更新后待执行的副作用函数
effects.add(effect);
}
});
}
};
if (type === "clear" /* CLEAR */) {
// collection being cleared 如果是clear就执行所有依赖
// trigger all effects for target
depsMap.forEach(add);
}
// 如果是数组长度发生变化
else if (key === 'length' && isArray(target)) {
depsMap.forEach((dep, key) => {
if (key === 'length' || key >= newValue) {
add(dep);
}
});
}
else {
// schedule runs for SET | ADD | DELETE
// key !== undefined 就先添加当前key所收集的依赖effect
if (key !== void 0) {
add(depsMap.get(key));
}
// also run for iteration key on ADD | DELETE | Map.SET
switch (type) {
case "add" /* ADD */:
if (!isArray(target)) {
// 这里是为了 Proxy handler中的ownKeys,ownKeys是获取对象本身的属性列表,所以依赖属性的变化。
add(depsMap.get(ITERATE_KEY));
if (isMap(target)) {
add(depsMap.get(MAP_KEY_ITERATE_KEY));
}
}// 数字的话可能是数组索引获取值
else if (isIntegerKey(key)) {
// new index added to array -> length changes
add(depsMap.get('length'));
}
break;
case "delete" /* DELETE */:
if (!isArray(target)) {
add(depsMap.get(ITERATE_KEY));
if (isMap(target)) {
add(depsMap.get(MAP_KEY_ITERATE_KEY));
}
}
break;
case "set" /* SET */:
if (isMap(target)) {
add(depsMap.get(ITERATE_KEY));
}
break;
}
}
// 定义run方法,
const run = (effect) => {
if (effect.options.onTrigger) {
effect.options.onTrigger({
effect,
target,
key,
type,
newValue,
oldValue,
oldTarget
});
}
// effect副作用函数option是否传入了调度程序,如果传入了就需要等待调度,否则就可以直接执行
if (effect.options.scheduler) {
effect.options.scheduler(effect);
}
else {
effect();
}
};
effects.forEach(run);
}
总结
这篇源码阅读从4个问题开始一一阅读源码,对官网响应性的解释对应的源码进行了解,个人能力有限,如果有理解不对的地方,希望大家多多斧正。最后希望大家给个赞吧,谢谢啦~