vue3相比较于vue2数据响应式做了较大改动,采用proxy实现数据劫持,定义响应式api的方式主要有reactie, ref,其它api由这2种api衍生而出。本文将介绍vue3响应式系统的实现基本原理(reactive)
vue3响应式系统的入口为reactivity/index。reactive的实现在reactive文件下。
- reactive
reactive将传入的对象转换为响应式对象,它只能传入对象类型,因为proxy只能代理对象,不能代理基本数据了行,以下分析reactive的实现原理。
reactive传入一个原始对象,返回createReactive的返回值,raw为原始对象,reactiveMap是一个WeakMap,用于存储原始对象和代理对象的映射,可以做缓存处理。baseHandler为proxy的handler
const reactive = (raw) => createReactive(raw, false, reactiveMap, baseHandler);
createReactive用于创建响应式对象。原始值不为对象时直接return,最终返回由proxy创建的响应式对象,并将其存入reactiveMap中。
const createReactive = (raw, isReadonly, proxyMap, handler) => {
if (!isObject(raw)) {
console.warn('this is not a object ,cant not be reactive');
return raw;
}
// raw[ReactiveFlags.RAW]已经是响应式对象,普通对象不存在RAW属性,
if (raw[ReactiveFlags.RAW] && !(isReadonly && raw[ReactiveFlags.REACTIVE])) {
return raw;
}
let proxy = proxyMap.get(raw);
if (proxy) {
return proxy;//获取缓存值
}
if (!proxy) {
proxy = new Proxy(raw, handler);//不存在? 创建代理对象
proxyMap.set(raw, proxy);//存入reactiveMap
return proxy;//返回
}
};
baseHandler实现
// 对象数据handler ( object,array)
const baseHandler = {
get,
set,
deleteProperty,
ownKeys,
has,
};
get和set分别通过createGetter和createSetter创建
const get = createGetter(false, false);
const set = createSetter(false, false);
createGetter实现
function createGetter(isReadonly, isShallow) { //这里存在一个闭包
return (target, key, receiver) => {
if (key === ReactiveFlags.READONLY) {//判断是否为readonly, isReadonly会用到
return isReadonly;
} if (key === ReactiveFlags.REACTIVE) {//判断是否为reactive,isReactive会用到
return !isReadonly;
} if (key === ReactiveFlags.SHALLOW) {//判断是否为shallow,isShallow会用到
return isShallow;
//下面这一坨是用于获取响应式对象的原始对象(源码中是通过三元运算符),receiver为proxy代理出来的对象,通过不同的proxMap获取
} if (key === ReactiveFlags.RAW && !isShallow && !isReadonly && receiver === reactiveMap.get(target)) { // 这里先分开写
return target;
} if (key === ReactiveFlags.RAW && isReadonly && !isShallow && receiver === readonlyMap.get(target)) {
return target;
} if (key === ReactiveFlags.RAW && !isReadonly && isShallow && receiver === shallowReactiveMap.get(target)) {
return target;
} if (key === ReactiveFlags.RAW && isShallow && isReadonly && receiver === shallowReadonlyMap.get(target)) {
return target;
}
// 判断是否调用数组方法
const targetIsArray = Array.isArray(target);
if (!isReadonly) {//如果是数组需要处理indexOf等方法,应为经过代理的对象不会是原对象,但是用户不会关心代理对象的查找方式
if (targetIsArray && arrayInstrumentations.hasOwnProperty(key)) {
return Reflect.get(arrayInstrumentations, key, receiver);
}
}
const res = Reflect.get(target, key, receiver);
if (!isReadonly) { //不是readonly的情况下需要去做依赖收集
track(target, key, 'get');
}
if (isShallow) {//如果是浅层响应式则无需对其对象类型的属性值做响应式处理,直接return
return res;
}
if (isObject(res)) {//如果是对象,则需要再次做响应式处理
return isReadonly ? readonly(res) : reactive(res);
}
return res;
};
}
处理数组查找方法
function createArrayInstrumentations() {
const instrumentations = {};
['includes', 'indexOf', 'lastIndexOf'].forEach((key) => {
instrumentations[key] = function (...args) {
const arr = toRaw(this);
for (let i = 0; i < this.length; i++) {
track(arr, `${i}`, 'get'); // 执行查找方法时,需要收集依赖,当用户修改arr[index]时触发;
}
const res = arr[key](...args); // 使用原始参数查找arg中有的可能为proxy,
if (res === -1 || res === false) {
return arr[key](...(args.map((item) => toRaw(item)))); // 将参数值设置为原始值,再去查找
}
return res;
};
});
return instrumentations;
}
// 这里的操作是什么意思呢,举个例子
const obj = {
name: '我在查找',
};
const proxy = reactive([
obj,
]);
console.log(proxy.indexOf(obj));
/*
这种情况下,obj在reactive中被做了代理,已经不是原始对象obj,所以如果没有createArrayInstrumentations 这里就会返回-1,
通过createArrayInstrumentations处理,经过代理的对象查找不到就会通过toRaw获取原始对象,再去查找
*/
get和依赖收集相关联,接下来介绍如何依赖收集。介绍依赖之前,先介绍一下依赖的数据结构,首先proxy只能代理对象类型,所以每一个对象会对应一个响应式对象,而对象的每一个键会对应一个依赖,所以就形成以下结构。
先看几个全局变量
const proxyMap = new WeakMap();//存储对象和对象键值映射dep的map
let activeEffect = void 0;//当前活跃的effect,一般当前正在执行的effect有一个,effect嵌套则通过链表记录上一次活跃情况
let shouldTrack = false;//是否应该收集依赖,
let effectTrackDepth = 0;//effect嵌套层数
const maxMakerBits = 30; // effect最大嵌套层级, 每嵌套一级则左移一位
export let trackOpBit = 1;//记录effect嵌套时,位移标识
const trackStack = [];//存储上一次是否应该收集依赖
track实现
const track = (target, key) => {
if (!isTracking()) {//是否正在收集依赖
return;
}
let depMap = proxyMap.get(target);//获取对象键对应dep的map
if (!depMap) {
proxyMap.set(target, depMap = new Map());//第一次收集则不存在,不存在设置一个空map
}
let dep = depMap.get(key);//获取到依赖
if (!dep) {
depMap.set(key, dep = createDep());//第一次收集不存在,不存在则创建空dep
}
trackEffects(dep);//收集依赖
};
收集依赖
const trackEffects = (dep) => {
let shouldTrack = false;
if (effectTrackDepth <= maxMakerBits) {//当前effect嵌套层数小于最大层数,采用优化方案(什么是优化方案?后面介绍)
if (!newTracked(dep)) {//刚开始收集(dep.n = 0 ,trackOpBit为层数,比如到15层,但是dep.n为0,按位与为0)
dep.n |= trackOpBit;//n,左移一位,标识该层的这一个dep已经处理过了
shouldTrack = !wasTracked(dep);//第一次执行dep.w都为0,因为还没收集依赖,deps的长度为0
}
} else {
shouldTrack = !dep.has(activeEffect);
}
if (shouldTrack) {
dep.add(activeEffect);//收集当前effect,
activeEffect.deps.push(dep);//双向关联,当前effect同时添加dep(为什么要双向关联,后面介绍)
}
};
先介绍一下依赖的数据结构,依赖是一个set结构,里面存储着effect,
const createDep = (effects) => {
const dep = new Set(effects);
dep.w = 0;//收集过了
dep.n = 0;//最新收集
return dep;
};
const wasTracked = (dep) => (trackOpBit & dep.w) > 0;// 这里注意运算符优先级 (当前层级trackOpbit & 当前dep的w)
const newTracked = (dep) => (trackOpBit & dep.n) > 0;// 这里注意运算符优先级(当前层级trackOpbit & 当前dep的w)
const initDepMakers = (reactiveEffect) => {//初始化的时候deps长度为0,不执行
const { deps } = reactiveEffect;
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
const dep = deps[i];
dep.w |= trackOpBit;
}
}
};
const finalizeDepMakers = (reactiveEffect) => {//effect执行退出后,dep的标识w,n,还原
const { deps } = reactiveEffect;
if (deps.length) {
let ptr = 0;
for (let i = 0; i < deps.length; i++) {
const dep = deps[i];
if (wasTracked(dep) && !newTracked(dep)) {//之前收集过了但是更新时发现不是新收集的了,则直接删除
dep.delete(reactiveEffect);
} else {
// 当dep中删除effect时,effect同时也要删除dep(从effect的deps中);
deps[ptr++] = dep;
}
dep.w &= ~trackOpBit;
dep.n &= ~trackOpBit;
}
}
};
响应式系统中比较重要的一个类就是ReactiveEffect,看一下ReactiveEfffect结构
class ReactiveEffect {
constructor(fn, scheduler) {
//构造器,传入fn(要执行的副作用函数,比如render函数),scheduler调度器,依赖触发时可以选择执行
this.fn = fn;
this.active = true;
this.deps = [];//当副作用函数执行时,target->key->dep会存储effect,但是effect也会反过来存储dep,deps就是多个dep
this.parent = undefined;//上一层活跃的effect,初始化为undefined
this.scheduler = scheduler;//调度器
}
// run的时候会执行副作用函数
run() {
if (!this.active) {
return this.fn();
}
const lastShouldTrack = shouldTrack;//将当前存储为上一次
try {
this.parent = activeEffect;
//当前复制给parent,如果是effect嵌套,activeEffect就会被赋值给下一层,防止上一层activEffect丢失
shouldTrack = true;
activeEffect = this;//全局活跃effect被赋值为当前effect
trackOpBit = 1 << ++effectTrackDepth; // 每次嵌套一层则左移一位
if (effectTrackDepth <= maxMakerBits) {
// 初始化的时候这里是不会执行的,因为dep的length1为0,更新的时候每一个dep的w置为trackOpBit
initDepMakers(this);
} else {
cleanupEffect(this);//清除所有副作用(什么意思?,后面阐述)
}
const result = this.fn();//执行副作用函数
return result;
} finally {//effec执行完毕后,要恢复至执行前的状态,因为每一层的状态都不一样
if (effectTrackDepth <= maxMakerBits) {
finalizeDepMakers(this);
}
trackOpBit = 1 << --effectTrackDepth;
activeEffect = this.parent;
shouldTrack = lastShouldTrack;
this.parent = undefined;
}
}
}
什么是清除副作用,为什么要清除副作用,什么是分支切换?举个例子(例子1.1)
const user =reactive({
name:"foo",
alias:"bar",
flag:true
})
effect(()=>{//effect是一个全局函数,将传入的fn构造为一个ReactiveEffect对象(这里传入的函数命名为fn)
let text = user.flag ? user.name : user.alias;
})
/*
effect执行时,user.flag 为true,则user.flag的dep收集到fn,user.name的dep会收集到 fn,而user.alias不会收集fn,
同样,effect的实例对象的deps属性会存储user.flag的dep,user.name得flag;
当user.flag变为false时,effect会重新执行,再次收集依赖,user.flag的dep收集到fn,user.alias收集到fn,effect的实例对象
的deps存储user.flag的dep,user.alias的dep。
但是此时user.name仍然收集着fn,如果user.flag这辈子再也不会变为true,那么当user.name发生改变时,依赖重新触发,着明显是不对的。
所以在每次执行副作用函数是,应该清除依赖(user.flag的dep删除fn,user.name的dep删除fn,user.alias的dep删除fn,然后重新收集,
就不会有遗留的副作用了)
*/
cleanupEffect的实现
const cleanupEffect = (effectFn) => {//传入当前执行的effect实例。找到实例中的dep,再从dep中删除自己(effect)
for (let i = 0; i < effectFn.deps.length; i++) {
const dep = effectFn.deps[i];
dep.delete(effectFn);
}
};
但是这么做很显然是浪费性能的,因为我只改变了一个属性(alias),但是user.name, user,alias都要重新收集effect(自己)。如果当前effect中有百个属性,那这些属性都要重新收集当前effect,着是没必要的,希望做到只删除那个修改后不会触发依赖重新执行的effect就可以了(本例子中希望user.name的dep删除fn就可以了,user.flag和user.alias不动),那要怎么做呢?这里就涉及到优化方案了,就是前面说的位运算(dep的w和n,以及trackOpBit等);那么要怎么做呢?举个例子体会一下按层收集依赖(例子1.2)
const user = {
name: 'app',
age: 180,
};
/*
trackOpBit = 1 // 0000 0001
effectTrackDepth = 0;
*/
// 初始化执行的时候 每嵌套一层effect时会重新 new ReactiveEffect() dep对应的 w 和 n都是 0
effect(() => {
// 左移一位 trackOpBit = 2 // 0000 0010
console.log(`--------${user.name}-------`); // name-> w:0000 0000 , n : 0000 0010//这里n的位移在trackEffects里
console.log(`--------${user.age}--------`); // age-> w:0000 0000 , n : 0000 0010
effect(() => {
// 再次左移一位 trackOpBit = 4 // 0000 0100
console.log(`---------${user.name}--------`); // name-> w:0000 0000, n : 0000 0100
console.log(`---------${user.age}--------`); // age-> w:0000 0000, n : 0000 0100
effect(() => {
// 再次左移一位 trackOpBit = 8 // 0000 1000
console.log(`--------${user.age}--------`); // age-> w:0000 0000, n : 0000 1000
});
});
}); // 初始化完毕之后 所有dep对应的w和n都会被置为 0 (调用源码的finalizeDepMakers方法) 恢复到初始状态 dep.n = 0 , dep.w=0
// 响应式数据更新的时候
effect(() => {
// 左移一位 trackOpBit = 2 // 0000 0010
/*
在用户传入的fn执行之前 会初始化depMakers 调用源码中的initDepMakers方法 ,之前收集的dep的w将被标记如下
*/
console.log(`--------${user.name}-------`); // name-> w:0000 0010 , n : 0000 0010
console.log(`--------${user.age}--------`); // age-> w:0000 0010 , n : 0000 0010
effect(() => {
// 再次左移一位 trackOpBit = 4 // 0000 0100
console.log(`---------${user.name}--------`); // name-> w:0000 0100, n : 0000 0100
console.log(`---------${user.age}--------`); // age-> w:0000 0100, n : 0000 0100
effect(() => {
// 再次左移一位 trackOpBit = 8 // 0000 1000
console.log(`--------${user.age}--------`); // age-> w:0000 1000, n : 0000 1000
});
});
}); // 更新流程执行完毕之后 所有dep的w和n都会被置为0 (调用源码的finalizeDepMakers方法) 恢复到初始状态 dep.n=0, dep.w=0
这里以例子1.1为例子
初始化effect执行,当前activeEffect为当前effect(fn简称),执行effect的实例方法,trackOpBit左移一位(0000 0010),effectTrackDepth++变为1,1<30,执行优化方案,但是此时effect实例的deps长度为0,user.flag的dep的w和n都为0,user.flag触发get,开始track,执行至trackEffects时,effectTrackDepth<30,newTracked(dep)的值为false,因为trackOpBit为0000 0010,而user.flag的dep的n为0000 0000 ,按位与为0,不大于0,此时user.flag的dep和trackOpBit按位或,此时user.flag的dep的 n 变为
0000 0010,由于user.flag的w为0,所以shouldTrakc为true,(user.name收集过程亦是如此)。
当user.flag修改为false的时候,重新执行effect的run,initDepMakers执行时,由于deps不为0,user.flag的dep的w和trackOpBit按位或,user.flag的dep的w变为0000 0010,n为0000 0000 ,因为每次run执行后会恢复状态,执行至trackEffects时,newTracked仍然为false,user.flag的dep的n和trackOpBit按位或,变为0000 0010,但是wasTracked变为true(因为w为0000 0010),所以shouldTrack变为false,不会再收集当前effect,user.name不执行,执行至user.alias时,(注意initDepMakers执行在fn之前),会创建一个dep,n和w都为0,收集流程和初始化时user.name保持一致。此时执行finalizeDepMakers,user.flag的dep的w为0000 0010,n为0000 0010。
user.alias的dep的w为0000 0000 ,n为0000 0010。
更新时这里重点分析user.name,更新时,会执行initDepMakers,所以user.name的w变为 0000 0010,但是由于user.flag为false,user.name不会执行,则不会执行trackEffects,则它的n为0000 0000,满足删除条件wasTracked(dep) && !newTracked(dep),则当前effect的deps中的dep删除当前effect(自己删除自己)也就是user.name的dep删除当前effect,这样user.name发生改变时,effect就不会重新执行。
依赖触发则需要编写其set函数
function createSetter(isReadonly, isShallow) {
return (target, key, newValue, receiver) => {
const oldValue = target[key];// 获取旧值
const hadKey = Array.isArray(target) && isIntegerKey(key) ? Number(key) < target.length : target.hasOwnProperty(key);// 判断是添加值还是设置值
const res = Reflect.set(target, key, newValue, receiver);
if (target === toRaw(receiver)) {
if (!hadKey) {
console.log('新增属性');
trigger(target, key, newValue, oldValue, 'add');
} else if (hasChanged(newValue, oldValue)) { // 前后的值没有发生改变则不需要触发依赖
console.log('设置属性');
trigger(target, key, newValue, oldValue, 'set');
}
}
return res;
};
}
trigger实现
const trigger = (target, key, newValue, oldValue, type) => {
// 获取对象的键对应的dep(dep是一个set)
const depMap = proxyMap.get(target);
if (!depMap) {
return;
}
const deps = [];// 存储所有需要执行的effect,不一定全是 depMap.get(key),比如数组新增元素需要触发length的dep
if (type === 'clear') {
} else if (key === 'length' && Array.isArray(target)) {
const newLength = Number(newValue); // 如果是修改了数组长度则获取数组最新长度
// map遍历时 dep为map的value,key为map的key;
depMap.forEach((dep, key) => {
// 当数组的长度由原来的100变为10,则90个dep需要执行;
// 这里有一个key 为迭代器(之后处理);
if (key === 'length' || (typeof key !== 'symbol' && key >= newLength)) {
deps.push(dep);
}
});
} else {
// 判断key 不为undefined(void 0 === undefinde 防止undefined被修改)
if (key !== void 0) {
deps.push(depMap.get(key));
}
switch (type) {
case 'add':
if (!Array.isArray(target)) { // 为对象添加一个属性的时候需要触发他的遍历dep
deps.push(depMap.get(ITERATE_KEY));
} else if (isIntegerKey(key)) {
// 如果是数组添加一个值,则需要触发他length对应的dep;
deps.push(depMap.get('length'));
} break;
case 'delete':
if (!Array.isArray(target)) { // 为对象删除一个属性的时候需要触发他的遍历dep
deps.push(depMap.get(ITERATE_KEY));
}
break;
case 'set': // 在修改值的时候不需要触发遍历key的操纵,因为key不变
break;
default:
}
}
const effects = [];
for (const dep of deps) {
if (dep) {
effects.push(...dep);
}
}
// 做一个拷贝;
triggerEffects(createDep(effects));
};
const triggerEffects = (dep) => {
dep.forEach((effect) => {
if (effect.scheduler) {
effect.scheduler();//如果调度器存在则执行调度器
} else {
effect.run();//负责触发副作用函数
}
});
};
删除和遍历时,实现比较简单,主要是在trigger时判断数据操作类型即可。
function deleteProperty(target, key) {
const hadKey = target.hasOwnProperty(key);
const oldValue = target[key];
const result = Reflect.deleteProperty(target, key);
if (hadKey && result) {
trigger(target, key, undefined, oldValue, 'delete');
}
return result;
}
function ownKeys(target) {
track(target, Array.isArray(target) ? 'length' : ITERATE_KEY, 'iterate');
return Reflect.ownKeys(target);
}
至此vue3的数据响应式原理解析完毕,但是源码中还涉及到了集合的代理,这里先不介绍,这里的代码是在源码的基础上做了一个提取,方便理解核心原理,源码中还有许多处理细节的地方。第一次学习源码,希望多多指正
文档中代码地址 Bug-codergb