vue3源码分析reactivity实现原理
猪猪爱前端 人气:0引言
上一章中我们解析了createApp到底发生了什么? 本来我们应该继续向下解析mount方法的,但是后面很多地方涉及到了响应式的api也就是reactivity的api,所以我们必须要单独将这一章拎出来做单独的讲解。本章主要分析内容:
第一部分:简单版reactivity
- 响应式主要是为了实现如下效果
//设置响应式对象 const proxy = reactive({a:1}) //当proxy.a发生变化的时候,调用effect中的回调函数 effect(()=>{ console.log(proxy.a) }) proxy.a++
下面我们先来设计一个方案实现这样的效果
- 在effect中有一个回调函数,当回调函数第一次执行的时候我们需要监听到这个函数内部有哪些响应式对象
- 如何让内部响应式和当前执行的这个函数产生关联呢?我们可以在即将执行这个回调函数的时候设置一个全局变量activeEffect,让当前即将执行的副作用函数为activeEffect,然后在响应式内部的get中收集到这个函数,执行完这个函数立刻设置activeEffect为null,这样就不会影响其他收集依赖的执行。
- 当这个proxy发生变化的时候立刻找到这个收集到的依赖项触发就实现了这样的效果。
- 思考一下这是一个怎样的结构?首先对象和对象的建对应一个依赖,依赖可能有多个,考虑到如果对象失去引用,那么依赖将不可能被调用,我们 采用weakMap结构,一个对象对应一个depsMap,而对象含有多个key,一个对象加一个key对应dep依赖集合,一个对象和一个key可能被多次使用在不同effect中,可能有多个依赖,所以dep类型为Set,也就是如下结构
reactiveMap = { [object Object]:{ [key]:new Set() } }
了解了整个设计流程我们开始书写代码:
(1).实现reactive和effect
1.当effect函数即将开始执行的时候设置全局变量
let activeEffect = null; const reactiveMap = new WeakMap(); function effect(fn) { const reactEffect = new ReactiveEffect(fn); reactEffect.run();//第一次立刻执行副作用函数 } //存放副作用的类 class ReactiveEffect { constructor(fn, scheduler) { this.fn = fn; this.scheduler = scheduler;//调度器 } run() { try { //执行之前先修改activeEffect activeEffect = this; return this.fn(); } finally { //执行完设置activeEffect为null activeEffect = null; } } }
2.创建响应式函数reactive
function reactive(obj) { //获取值的时候收集依赖 const getter = function (object, key, receiver) { const res = Reflect.get(object, key, receiver); //获取真实的值 track(object, key, res); if (typeof res === "object") { return reactive(res); } return res; }; //当设置值的时候触发依赖 const setter = function (object, key, value, receiver) { const res = Reflect.set(object, key, value, receiver); trigger(object, key, value, res); return res; }; const mutations = { get: getter, set: setter, }; const proxy = new Proxy(obj, mutations); return proxy; }
3.实现track和trigger函数
function track(object, key, oldValue) { //首先看看之前是否有这个对象的depsMap //如果没有表示是第一次收集创建一个new Map let depsMap = reactiveMap.get(object); if (!depsMap) { reactiveMap.set(object, (depsMap = new Map())); } //如果是第一次收集这个key,则创建一个新的dep依赖 let dep = depsMap.get(key); if (!dep) { depsMap.set(key, (dep = new Set())); } 找到这个target和key对应的依赖之后进行副作用收集 trackEffects(dep); } function trackEffects(dep) { //因为设置的对象是响应式的所以只要 //响应式对象改变都会收集,但是只有 //在effect执行的时候activeEffect才有值 //才能收集到依赖并且dep采用了集合防止 //重复收集同一个依赖 if (activeEffect) { dep.add(activeEffect); } } //当修改值的时候触发依赖函数 function trigger(object, key, newVal, oldVal) { const depsMap = reactiveMap.get(object); const dep = depsMap.get(key);//找到target和key对应的dep //执行依赖函数 if (dep.size > 0) { for (const effect of dep) { if (effect.scheduler) { effect.scheduler(); } else effect.run(); } } }
这就是reactivity最核心的逻辑,是不是觉得非常简单呢?目前我们代理的是对象,那如果我们代理的时候是一个值呢?那就要使用到ref,下面我们来写写极简版的ref实现吧!
(2).实现ref
- 我们可以采用把值包装成一个对象的方法,利用类自带的拦截器当get的时候收集依赖,那么收集依赖需要target和key,显然target就是RefImpl实例,而key就是value,同样在set的时候触发依赖就实现了ref
function ref(value) { return createRef(value); } function createRef(value) { return new RefImpl(value); } class RefImpl { constructor(value) { this.__v_isRef = true; this._value = value; } get value() { track(this, "value"); return this._value; } set value(value) { this._value = value; trigger(this, "value", value, this._value); return true; } }
(3).实现computed
说到computed,他是如何实现的呢?我们先来说说他的要求,computed接受一个getter,必须有返回值,当内部收集到的响应式发生改变的时候我们去读取compute.value也会发生相应的变化,并且computed返回的对象也是响应式的例如:
//设置响应式 const proxy = reactive({ a: 1, b: { a: 1 } }); //设置计算属性 const comp = computed(() => { return proxy.a + 1; }); effect(() => { console.log(comp.value); }); //当proxy.a发生变化,读取comp的value也会发生变化,并且因为comp是响应式 //在effect中被收集了,所以当proxy.a发生变化也会导致effect中的函数执行 proxy.a++;
下面我们来看看他的实现
这里必须要说一个scheduler,ReactiveEffect接受两个参数如果有第二个参数,那么就不会调用run方法而是调用scheduler方法。
所以computed的实现原理就是,当执行computed这个函数的时候创建ComputedRefImpl,而构造器中会自动创建ReactiveEffet,这个时候会传递一个schduler,也就是说以后这个effect不会调用run方法而是调用schduler方法,我们只需要在在shcduler方法中设置dirty为true表示修改了值,然后在进行调度,通过comp.value收集到的依赖就可以了,这里的响应式其实有两个地方,第一个地方是computed内部有一个响应式,第二是comp本身也是响应式需要收集依赖,当computed内部响应式发生变化会导致this._effect.scheduler执行,那么dirty会设置为true,当comp.value在其他effect中的时候会触发track收集依赖,所以当computed内部响应式发生改变就会触发get时候收集到的effect。
class ComputedRefImpl { constructor(getter) { //调度器 this._effect = new ReactiveEffect(getter, () => { if (!this._dirty) { this._dirty = true; //修改了getter中监听到的值 //引起对dep内的更新 for (const effect of this.dep) { if (effect.scheduler) { effect.scheduler(); } else effect.run(); } } }); this.dep = new Set(); //依赖 this._dirty = true; //是否需要更新 this._value = null; } get value() { trackEffects(this.dep); //收集依赖 if (this._dirty) { this._dirty = false; this._value = this._effect.run(); } return this._value; } }
最后我们来试试效果吧
const proxy = reactive({ a: 1, b: { a: 1 } }); const comp = computed(() => { return proxy.a + 1; }); const proxyRef = ref(100); effect(() => { console.log(proxy.b.a); console.log(comp.value); }); effect(() => { console.log(proxyRef.value); }); proxy.a++; proxy.b.a++; proxyRef.value++; //log:1 2 100 1 3 2 3 101
好啦! 看了reactivity的建议版本实现,相信你已经基本了解了reactivety,我们开始分析源码吧!
第二部分:深入分析对于object、array的响应式代理
- 我们重reactivity包最常用的api,reactive开始进行分析,因为采用了工厂函数,所以对应的shallow,readonly,shallowReadonly也会分析到。
- 我们先来看看reactive函数
//深度代理 export function reactive(target) { //如果被代理的是readonly返回已经被readonly代理过的target if (isReadonly(target)) { return target; } return createReactiveObject( target, false, mutableHandlers, mutableCollectionHandlers, reactiveMap ); } //只代理第一层 export function shallowReactive(target) { return createReactiveObject( target, false, shallowReactiveHandlers, shallowCollectionHandlers, shallowReactiveMap ); } //代理只读属性 export function readonly(target) { return createReactiveObject( target, true, readonlyHandlers, readonlyCollectionHandlers, readonlyMap ); } //只代理只读的第一层 export function shallowReadonly(target) { return createReactiveObject( target, true, shallowReadonlyHandlers, shallowReadonlyCollectionHandlers, shallowReadonlyMap ); }
我们发现这四个api本质都是调用了createReactiveObject,但是他们传递的参数是不同的,对于不同的代理handlers处理是不同的,而其中还有对于map set等的代理就需要使用到collectionHandlers,对于代理过的对象我们再次对这个对象进行代理是不必要的,需要reactiveMap进行缓存。已经代理过的对象读取缓存就可以了。
接下来我们深入createReactiveObject,先来看看源代码
export function createReactiveObject( target, isReadonly, baseHandlers, collectionHandlers, proxyMap ) { //不能够代理非对象 if (!isObject(target)) { { console.warn(`value cannot be made reactive: ${String(target)}`); } return target; } //已经代理过的对象不需要在进行二次代理 if (target[RAW] && !(isReadonly && target[IS_REACTIVE])) { return target; } //防止重复代理 const existingProxy = proxyMap.get(target); if (existingProxy) { return existingProxy; } //获取当前被代理对象的类型 //为0表示被代理对象为不可拓展对象 //或者当前对象含有__v_skip属性 //为1表示Array,Object类型用baseHandlers处理 //为2表示map set weakMap weakSet 用collectionHandlers处理 const targetType = getTargetType(target); //不可代理 返回原对象 if (targetType === 0) { console.warn(`current target:${target} can not been proxy!`); return target; } //进行代理 const proxy = new Proxy( target, //判断当前代理对象的类型,如果是array object采用baseHandlers //如果是map set weakMap weakSet采用collectionHandlers targetType === 2 ? collectionHandlers : baseHandlers ); proxyMap.set(target, proxy); //返回代理成功的对象 return proxy; }
- 这个函数比较简单,首先是第一种情况,调用了 reactive(target) 然后再次调用 reactive(target) 会返回同一个proxy代理对象,因为内部建立了reactiveMap缓存
- 第二种情况是得到了 proxy = reactive(target) 然后再对proxy进行代理reactive(proxy) 这样的为了防止二次代理,最终会选择返回proxy。
- 当然还会判断对于不是对象的,是不能够进行代理的
- 之后还通过targetType判断了当前代理的类型,对于不同的类型使用不同的代理方式,我们顺便来看看getTargetType函数
//如果对象带有__v_skip或则对象不可拓展则不可代理 //然后根据类型判断需要哪种函数进行代理 export function getTargetType(value) { return value[SKIP] || !Object.isExtensible(value) ? 0 : targetTypeMap(toRawType(value)); } //对截取的类型判断 如果是object array返回1 //如果是set map weakMap weakSet返回2 export function targetTypeMap(rawType) { switch (rawType) { case "Object": case "Array": return 1; case "Map": case "Set": case "WeakMap": case "WeakSet": return 2; default: return 0; } } //截取类型 export const toRawType = (value) => { //截取[object Object]中的"Object" return Object.prototype.toString.call(value).slice(8, -1); };
本部分我们仅讨论对于object和array类型的代理,所以我们跳过collectionHandlers的实现,现在我们来看看baseHandlers,baseHandlers显然是根据shallow readonly不同传递的不同的handlers,其中包含:
- mutableHandlers
- shallowReadonlyHandlers
- readonlyHandlers
- shallowReactiveHandlers 我们看看他是如何创建这四个handlers的吧!
//reactive的proxy handlers //这个便是new Proxy()中的第二个参数,可以拦截get //set deleteProperty has ownKeys等进行处理 const mutableHandlers = { get, set, deleteProperty, has, ownKeys, }; //处理readonly的proxy handler const readonlyHandlers = { get: readonlyGet, //对于readonly的handlers不需要set值 //打印警告,但是不修改值 set(target, key) { { warn( `Set operation on key "${String(key)}" failed: target is readonly.`, target ); } return true; }, //对于只读属性,不能删除值 deleteProperty(target, key) { { warn( `Delete operation on key "${String(key)}" failed: target is readonly.`, target ); } return true; }, }; //处理只代理第一层的proxy handler const shallowReactiveHandlers = shared.extend({}, mutableHandlers, { get: shallowGet, set: shallowSet, }); //处理只对第一层做只读代理的proxy handler const shallowReadonlyHandlers = shared.extend({}, readonlyHandlers, { get: shallowReadonlyGet, }); //这里的shared.extend就是Object的assign方法 //shared.extend = Object.assgin
显然,在以上代码中出现了几个代理函数分别是getter setter deleteProperty ownKeys has,接下来我们便对每一个进行分析。
(1).handlers中的getter
- 我们发现对于getter,有shallowGet、readonlyGet、shallowReadonlyGet以及get,我们看看是如何得到这些方法的。
const get = createGetter(); const shallowGet = createGetter(false, true); const readonlyGet = createGetter(true, false); const shallowReadonlyGet = createGetter(true, true);
他们都调用了createGetter方法,这是一个工厂函数,通过传递isReadonly isShallow来判断是哪种类型的getter,然后创建不同的get。所以接下来我们自然而然需要分析createGetter函数。
//创造getter的工厂函数,通过是否是只读和 //是否只代理第一层创造不同的getter函数 export function createGetter(isReadonly = false, shallow = false) { //传递进入Proxy的get函数 //例如const obj = {a:2} // const proxy = new Proxy(obj,{ // get(target,key,receiver){ // 当通过proxy.a对obj进行访问的时候,会先进入这个函数 // 返回值将会作为proxy.a获得的值 // } // }) return function get(target, key, receiver) { //1.对isReadonly isShallow等方法的处理 //以下前面几个判断都是为了通过一些关键key判断 //当前的对象是否是被代理的,或者是否是只读的 //是否是只代理第一层的。 //假设当前我们的代理是reactive类型 //如果我们访问__v_isReactive那么返回值应该为true //同理访问readonly类型则返回false //故而这里取反 if (key === IS_REACTIVE) { return !isReadonly; } //访问__v_isReadonly返回isReadonly真实值即可 else if (key === IS_READONLY) { return isReadonly; } //访问__v_isShallow 返回shallow真实值即可 else if (key === IS_SHALLOW) { return shallow; } //当访问__v_raw的时候,根据当前的readonly和shallow属性 //访问不同的map表,通过map表获得代理前的对象 else if ( key === RAW && receiver === (isReadonly ? shallow ? shallowReadonlyMap : readonlyMap : shallow ? shallowReactiveMap : reactiveMap ).get(target) ) { return target; } //判断当前target是否是数组 const targetIsArray = isArray(target); //如果调用的push pop shift unshift splice includes indexOf lastIndexOf //拦截这个方法 if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) { return Reflect.get(arrayInstrumentations, key, receiver); } //获取访问的真实值 const res = Reflect.get(target, key, receiver); //判断当前访问的key是否是内置的Symbol属性或则是否是不需要track的key //例如__proto__ , __v_isRef , __isVue 如果是这些属性则直接返回 if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) { return res; } //如果不是只读属性 开始收集依赖 只读属性不需要收集依赖 if (!isReadonly) { track(target, trackOpTypes.get, key); } //只需要代理一层,不用再进行代理了返回即可 if (shallow) { return res; } //如果是访问到的value是ref类型,返回res.value //访问的是数组的数字属性则返回res if (isRef(res)) { return targetIsArray && isIntegerKey(key) ? res : res.value; } //如果得到的结果依然是对象继续进行深度代理 if (isObject(res)) { return isReadonly ? readonly(res) : reactive(res); } return res; }; }
- 首先对于已经进行了代理的对象,可以通过判断key=__v_isReactive,__v_isShallow,__v_isReadonly判断是否是 reactive,shallow,readonly, 当然这也是isReactive、isReadonly等api实现基础。
- 之后对于某些特殊属性的访问我们也不需要去收集依赖例如 [Symbol.iterator]。
- 如果不是只读的代理,就需要收集依赖方便后续effect调用。
- 如果访问到的value还是一个对象我们还需要进行深度代理。
isNonTrackableKeys函数、builtInSymbols、如果数组调用了push pop includes方法该怎么处理呢?
//这里贴上源码,感兴趣的仔细阅读,不在进行讲解 const isNonTrackableKeys = makeMap(`__proto__,__v_isRef,__isVue`); function makeMap(str, expectsLowerCase) { const map = Object.create(null); //创造一个空对象 const list = str.split(","); //["__proto__","__isVUE__"] for (let i = 0; i < list.length; i++) { map[list[i]] = true; //{"__proto__":true,"__isVUE__":true} } //返回一个函数,用于判断是否是传递的str分割出来的某一个值 //可以通过expectsLowerCase指定是否需要将分隔值转化为小写 return expectsLowerCase ? (val) => !!map[val.toLowerCase()] : (val) => !!map[val]; } //Symbol的所有属性值 export const builtInSymbols = new Set( //首先获取所有的Symbol的key Object.getOwnPropertyNames(Symbol) //过滤掉arguments和caller .filter((key) => key !== "arguments" && key !== "caller") //拿到所有的Symbol值 .map((key) => Symbol[key]) //过滤掉不是symbol的值 .filter(shared.isSymbol) );
- buildInSymbols就是Symbol的所有内置属性key例如Symbol.iterator等。
- 再来看看如何处理数组特殊方法的调用。
//当前代理的对象是数组,且访问了pop等8个方法中的一个 if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) { //进行代理 return Reflect.get(arrayInstrumentations, key, receiver); } const arrayInstrumentations = createArrayInstrumentations(); function createArrayInstrumentations() { const instrumentations = {}; //拦截数组的方法 arr.includes() ["includes", "indexOf", "lastIndexOf"].forEach((key) => { instrumentations[key] = function (...args) { //这里的this指向调用当前方法的数组 const arr = toRaw(this); //将当前数组中的所有元素收集依赖 for (let i = 0, l = this.length; i < l; i++) { track(arr, trackOpTypes.get, i + ""); } //执行函数 const res = arr[key](...args); if (res === -1 || res === false) { return arr[key](...args.map(toRaw)); } else { return res; } }; }); //如果使用这些方法取消收集依赖 ["push", "pop", "shift", "unshift", "splice"].forEach((key) => { instrumentations[key] = function (...args) { //停止收集依赖 将shouldTrack变为false pauseTracking(); //这里toRaw是为了防止二次执行getter,执行数组对应的方法 const res = toRaw(this)[key].apply(this, args); //重新收集依赖,将shouldTrack变为true resetTracking(); return res; }; }); return instrumentations; } //中断追踪 export function pauseTracking() { trackStack.push(shouldTrack); shouldTrack = false; } //重设追踪 export function resetTracking() { //获取之前的shouldTrack值 const last = trackStack.pop(); //如果trackStack中没有值shouldTrack设置为true shouldTrack = last === undefined ? true : last; }
- 首先,对于includes、indexOf、lastIndexOf会遍历数组中的所有元素并且会有获取的操作,也就是说数组所有元素都可能进行访问执行get,所以整个数组中的所有元素都必须要进行track操作。
- 对于pop等五个方法,依赖收集是混乱的,例如我执行shift操作,对于底层来说就需要对元素进行移动,这显然会导致getter和setter的多次触发,所以我们必须要停止收集依赖。
好啦,接下来我们进行track函数进行分析,看看是如何收集依赖的。
export function track(target, type, key) { //当调用了effect方法,会给activeEffect赋值 if (shouldTrack && activeEffect) { let depsMap = targetMap.get(target); if (!depsMap) { targetMap.set(target, (depsMap = new Map())); } let dep = depsMap.get(key); if (!dep) { depsMap.set(key, (dep = createDep())); } //传递入生命周期钩子的当前effect的信息 const eventInfo = { effect: activeEffect, target, type, key }; trackEffects(dep, eventInfo); } }
- 这个方法相信大家已经相当的熟悉了吧!跟我们写的简易版reactivity是一样的,就是通过target,key获取依赖,没有就创建。
- 那么activeEffect是什么时候赋值的呢?相信在简易版reactivity中大家已经知道啦,就是在调用effect之前赋值,调用完成后变为null,但是源码的实现更加复杂,考虑的问题更加全面。
export class ReactiveEffect { constructor(fn, scheduler = null, scope) { this.fn = fn; //副作用函数 //调度器(如果有调用器就不在执行run方法而是执行调度器) this.scheduler = scheduler; this.active = true; /** * 当前副作用被那些变量所依赖 * 例如: * effect(()=>{ * console.log(proxy.a) * }) * effect(()=>{ * console.log(proxy.a) * }) * * 每一个effect的回调函数都会产生一个ReactiveEffect实例 * 第一个effect中有proxy.a被读取,那么就会被收集依赖,则 * 对于第一个ReactiveEffect实例来说deps中就有有proxy.a * 也就是target key 指向的dep,这个dep是一个集合,代表的是 * target key对应的dep */ this.deps = []; this.parent = undefined; //TODO recordEffectScope recordEffectScope(this, scope); } //开始执行 run() { if (!this.active) { return this.fn(); } let parent = activeEffect; let lastShouldTrack = shouldTrack; while (parent) { if (parent === this) { return; } parent = parent.parent; } try { //可能有嵌套的effect,当执行到effect回调函数中有effect的时候 //现在的activeEffect相当于最新创建的effect的父级effect /* 例如:effect(()=>{ 现在指向外部的effect console.log(proxy.a) effect(()=>{ 在这里面的时候activeEffect指向内部effect console.log(proxy.b) }) 现在需要将activeEffect恢复为外部effect console.log(proxy.b) }) 当然对应的parent也应该改变,这就是try finally的作用 */ this.parent = activeEffect; //让当前的activeEffect为当前effect实例 activeEffect = this; shouldTrack = true; //设置嵌套深度 trackOpBit = 1 << ++effectTrackDepth; if (effectTrackDepth <= maxMarkerBits) { initDepMarkers(this); } else { cleanupEffect(this); } //执行effect副作用 return this.fn(); } finally { //退出当前effect回调函数的执行,要将全局变量退回到当前 //effect的父级effect(回溯) if (effectTrackDepth <= maxMarkerBits) { finalizeDepMarkers(this); } //全部进行回溯 trackOpBit = 1 << --effectTrackDepth; //恢复trackOpBit activeEffect = this.parent; shouldTrack = lastShouldTrack; this.parent = undefined; if (this.deferStop) { this.stop(); } } } stop() { if (activeEffect === this) { this.deferStop = true; } else if (this.active) { cleanupEffect(this); if (this.onStop) { this.onStop(); } this.active = false; } } }
- 通过try finally解决了嵌套的effect activeEffect指向不明确问题。
- 设置了effectOpBit表示当前深度,超过30层则不能再嵌套了。
- stop方法用于停止执行副作用执行。
- 接下来我们继续看trackEffects执行。
//收集副作用 export function trackEffects(dep, debuggerEventExtraInfo) { let shouldTrack = false; if (effectTrackDepth <= maxMarkerBits) { if (!newTracked(dep)) { dep.n |= trackOpBit; shouldTrack = !wasTracked(dep); } } else { //如果已经收集过就不收集了 shouldTrack = !dep.has(activeEffect); } //通过上面的判断是否需要收集 if (shouldTrack) { //往当前target key对应的dep中添加effect dep.add(activeEffect); //当前effect中有哪些被代理的变量的dep activeEffect.deps.push(dep); //生命周期,当真正执行track的时候调用函数 if (activeEffect.onTrack) { activeEffect.onTrack({ effect: activeEffect, ...debuggerEventExtraInfo, }); } } }
- 显然这个函数就是用于收集effect到dep,同时构建effect的deps(代表当前effect中有哪些被代理过的变量指向的dep,例如proxy.a能指向一个dep,同时proxy.a在当前effect回调函数中执行,那么对于当前effect来说deps中应该包含代表proxy.a的dep)
- 完成依赖收集我们就可以进入setter的学习了!触发依赖更新。
(2).handlers中的setter
//创造setter的工厂函数 export function createSetter(shallow) { return function set(target, key, value, receiver) { let oldValue = target[key]; //获取代理对象之前的value //旧值是ref,新值不是ref if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) { return false; } //深度代理的情况 if (!shallow) { if (!isShallow(value) && !isReadonly(value)) { //防止如果后面操作了value 引起二次setter oldValue = toRaw(oldValue); value = toRaw(value); } //target是对象且值为ref类型,当对这个值修改的时候应该修改ref.value if (!isArray(target) && isRef(oldValue) && !isRef(value)) { oldValue.value = value; return true; } } //判断当前访问的key是否存在,不存在则是设置新的值 const hadKey = //当前的target为数组且访问的是数字 isArray(target) && isIntegerKey(key) ? Number(key) < target.length : hasOwn(target, key); //设置value const result = Reflect.set(target, key, value, receiver); if (target === toRaw(receiver)) { //设置新的值 if (!hadKey) { trigger(target, triggerOpTypes.add, key, value); } //修改老的值 else if (hasChanged(value, oldValue)) { trigger(target, triggerOpTypes.set, key, value, oldValue); } } return result; }; }
- 根据hadKey判断当前是修改值还是新增值,传递不同的类型进行trigger(触发更新)所以我们接下来继续分析trigger
//根据不同的类型添加必要的副作用到deps中 export function trigger(target, type, key, newValue, oldValue, oldTarget) { const depsMap = targetMap.get(target); if (!depsMap) { return; } let deps = []; //当前要处理的所有依赖 if (type === triggerOpTypes.clear) { //清空,相当于所有的元素都发生改变 //故而全部都需要添加进依赖 deps = [...depsMap.values()]; } //拦截修改数组长度的情况 else if (key === "length" && isArray(target)) { //放入key为length或者数组下标大于设置值的所以依赖 //例如:const a = [1,2,3] a.length=1 //那么数组长度发生了变化,2,3的依赖都应该被放入 depsMap.forEach((dep, key) => { if (key === "length" || key >= newValue) { deps.push(dep); } }); } //其他的情况获取之前在getter收集的依赖到deps中 else { //将target key 指向的依赖放入deps中 if (key !== void 0) { deps.push(depsMap.get(key)); } //根据不同type添加不同的必要依赖到deps switch (type) { //处理添加新的值 case triggerOpTypes.add: if (!isArray(target)) { //set或map deps.push(depsMap.get(ITERATE_KEY)); if (isMap(target)) { deps.push(depsMap.get(MAP_KEY_ITERATE_KEY)); } } else if (isIntegerKey(key)) { //当前修改的是数组且是新增值 //例如 arr.length = 3 arr[4] = 8 //此时数组长度会发生改变所以当前数组的 //length属性依然需要被放入依赖 deps.push(depsMap.get("length")); } break; case triggerOpTypes.delete: //处理delete... break; case triggerOpTypes.set: //处理map类型... } } //当前effect的信息 const eventInfo = { target, type, key, newValue, oldValue, oldTarget }; if (deps.length === 1) { if (deps[0]) { { triggerEffects(deps[0], eventInfo); } } } else { const effects = []; //扁平化所有的effect for (const dep of deps) { if (dep) { effects.push(...dep); } } //执行所有的副作用 triggerEffects(createDep(effects), eventInfo); } } //创建dep export const createDep = (effects) => { const dep = new Set(effects); dep.w = 0; dep.n = 0; return dep; };
- 这个函数显然就是处理边际情况,收集所有的deps并调用triggerEffects进行触发。
- triggerOpTypes一共有 "clear"、"set"、"delete"、"add",其中只有 "add" 是处理object和array的代理的。
- "clear":当触发了clear表示清除当前代理对象所有的元素,所有元素都被修改了,所以所有的dep都需要被添加到deps中。
- "add":代表当前是新增的值,对于数组来说如果访问了比自身长度大的属性,那么length属性将被修改所以这种情况属性 "length" 对应的dep也应该被放入 deps。
- 对于数组假设数组长度是10,然后修改了数组length属性例如arr.length = 3,那么相当于删除了7个元素,那么这7个元素对应的dep应当放入deps中。
- 接下来继续调用triggerEffects触发收集到的所有dep。
//根据trigger最终组成的deps触发所有副作用执行 function triggerEffects(dep, debuggerEventExtraInfo) { //拿到所有的effects 包装成数组 const effects = isArray(dep) ? dep : [...dep]; //含有computed属性先执行 for (const effect of effects) { if (effect.computed) { triggerEffect(effect, debuggerEventExtraInfo); } } //不含有computed属性后执行 for (const effect of effects) { if (!effect.computed) { triggerEffect(effect, debuggerEventExtraInfo); } } } function triggerEffect(effect, debuggerEventExtraInfo) { if (effect !== activeEffect || effect.allowRecurse) { //生命周期,对于这个effect在进行trigger的时候调用 if (effect.onTrigger) { effect.onTrigger(shared.extend({ effect }, debuggerEventExtraInfo)); } //如果有调度器则执行调度器否则执行run if (effect.scheduler) { effect.scheduler(); } else { effect.run(); } } }
- 含有computed属性的先执行,没有的后执行,有scheduler调用scheduler否则调用run。这样就完成了触发。
(3).handlers的deleteProperty
//处理删除属性的逻辑(统一处理) //target:要删除属性的对象 key:要删除对象值的键 export function deleteProperty(target, key) { const hadKey = hasOwn(target, key); //判断删除的属性是否在 const oldValue = target[key]; //获得旧值 //删除属性返回值为是否删除成功 const result = Reflect.deleteProperty(target, key); if (result && hadKey) { //触发副作用 trigger(target, triggerOpTypes.delete, key, undefined, oldValue); } return result; }
当调用delete obj.xxx的时候deleteProperty就会监听到,这显然是修改值的情况所以我们执行trigger,类型自然就是 "delete" ,还记得trigger中对于 "delete" 类型我们并没有讲解,下面我们看看这部分如何处理。
//将target key 指向的依赖放入deps中 if (key !== void 0) { deps.push(depsMap.get(key)); } //省略部分代码... case triggerOpTypes.delete: if (!isArray(target)) { //添加key为iterate的依赖,后面讲这个依赖来自于哪里 deps.push(depsMap.get(ITERATE_KEY)); if (isMap(target)) { deps.push(depsMap.get(MAP_KEY_ITERATE_KEY)); } } break; //省略部分代码...
- 首先把删除的那个元素的依赖放入deps中。
- 如果删除的是对象那么会添加key为ITERATE_KEY的依赖。这个key来自于ownKeys的拦截,当在收集依赖的时候也就是在effect中写了Object.keys、Object.getOwnPropertyNames、Object.getOwnPropertySymbols、又或者调用了Reflect.ownKeys。这样的代码就会触发ownKeys的拦截这个时候其实就是track的类型就是ITERATE_KEY,也就是说如果你写了Object.keys那么就会收集依赖,某一天你删除了proxy上的属性,同样会触发依赖更新。
const {reactive,effect} = require('./reactivity.cjs') const proxy = reactive({a:1}) effect(()=>{ Object.keys(proxy) console.log(111) }) effect(()=>{ proxy.a console.log(111) }) delete proxy.a //log: 111 111 111 111
(4).handlers的ownKeys
//拦截Object.keys getOwnPropertyNames等 export function ownKeys(target) { track(target, "iterate", isArray(target) ? "length" : ITERATE_KEY); return Reflect.ownKeys(target); }
- track我们已经分析过了,如果target是非数组元素,那么追踪的key就是ITERATE_KEY这就是上面delete哪里的来源。
(5).handlers的has
//拦截foo in proxy foo in Object.create(proxy) //with(proxy){foo} Reflect.has export function has(target, key) { const result = Reflect.has(target, key); //判断是否有这个属性 //不是Symbol或内置Symbol属性 if (!isSymbol(key) || !builtInSymbols.has(key)) { track(target, "has", key); } return result; }
- has 同样是判断是否存在元素,不涉及修改,所以是track,传递类型为 "has",收集依赖即可,特殊的是has只能拦截注释中的情况,getOwnProperty是不能拦截的。
好啦! 第二部分们已经完成了所有的分析,但是本文还没有完!因为篇幅过长,第三部分和第四部分我放在下一章节。我们最后再来总结一下吧!
本文总结:
本文我们写了一个简单版本的reactivity,便于大家后续理解真正的源码,然后我们分析了如何拦截array和object类型的数据,总体来说就是在effect执行的时候修改当前activeEffect的指向,然后执行effect的时候收集依赖通过proxy原生api拦截get has ownKeys的操作,完成依赖的收集,然后在set和delete的时候进行触发,并且对边际情况也进行了处理、例如数组访问修改length、使用pop push includes方法的处理等。
下文我们将会继续分析对于map set weakMap weakSet的拦截以及ref computed等api的实现,更多关于vue3源码分析reactivity的资料请关注其它相关文章!
加载全部内容