vue3源碼分析reactivity實現(xiàn)原理
引言
上一章中我們解析了createApp到底發(fā)生了什么? 本來我們應該繼續(xù)向下解析mount方法的,但是后面很多地方涉及到了響應式的api也就是reactivity的api,所以我們必須要單獨將這一章拎出來做單獨的講解。本章主要分析內(nèi)容:
第一部分:簡單版reactivity
- 響應式主要是為了實現(xiàn)如下效果
//設置響應式對象 const proxy = reactive({a:1}) //當proxy.a發(fā)生變化的時候,調(diào)用effect中的回調(diào)函數(shù) effect(()=>{ console.log(proxy.a) }) proxy.a++
下面我們先來設計一個方案實現(xiàn)這樣的效果
- 在effect中有一個回調(diào)函數(shù),當回調(diào)函數(shù)第一次執(zhí)行的時候我們需要監(jiān)聽到這個函數(shù)內(nèi)部有哪些響應式對象
- 如何讓內(nèi)部響應式和當前執(zhí)行的這個函數(shù)產(chǎn)生關聯(lián)呢?我們可以在即將執(zhí)行這個回調(diào)函數(shù)的時候設置一個全局變量activeEffect,讓當前即將執(zhí)行的副作用函數(shù)為activeEffect,然后在響應式內(nèi)部的get中收集到這個函數(shù),執(zhí)行完這個函數(shù)立刻設置activeEffect為null,這樣就不會影響其他收集依賴的執(zhí)行。
- 當這個proxy發(fā)生變化的時候立刻找到這個收集到的依賴項觸發(fā)就實現(xiàn)了這樣的效果。
- 思考一下這是一個怎樣的結構?首先對象和對象的建對應一個依賴,依賴可能有多個,考慮到如果對象失去引用,那么依賴將不可能被調(diào)用,我們 采用weakMap結構,一個對象對應一個depsMap,而對象含有多個key,一個對象加一個key對應dep依賴集合,一個對象和一個key可能被多次使用在不同effect中,可能有多個依賴,所以dep類型為Set,也就是如下結構
reactiveMap = { [object Object]:{ [key]:new Set() } }
了解了整個設計流程我們開始書寫代碼:
(1).實現(xiàn)reactive和effect
1.當effect函數(shù)即將開始執(zhí)行的時候設置全局變量
let activeEffect = null; const reactiveMap = new WeakMap(); function effect(fn) { const reactEffect = new ReactiveEffect(fn); reactEffect.run();//第一次立刻執(zhí)行副作用函數(shù) } //存放副作用的類 class ReactiveEffect { constructor(fn, scheduler) { this.fn = fn; this.scheduler = scheduler;//調(diào)度器 } run() { try { //執(zhí)行之前先修改activeEffect activeEffect = this; return this.fn(); } finally { //執(zhí)行完設置activeEffect為null activeEffect = null; } } }
2.創(chuàng)建響應式函數(shù)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; }; //當設置值的時候觸發(fā)依賴 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.實現(xiàn)track和trigger函數(shù)
function track(object, key, oldValue) { //首先看看之前是否有這個對象的depsMap //如果沒有表示是第一次收集創(chuàng)建一個new Map let depsMap = reactiveMap.get(object); if (!depsMap) { reactiveMap.set(object, (depsMap = new Map())); } //如果是第一次收集這個key,則創(chuàng)建一個新的dep依賴 let dep = depsMap.get(key); if (!dep) { depsMap.set(key, (dep = new Set())); } 找到這個target和key對應的依賴之后進行副作用收集 trackEffects(dep); } function trackEffects(dep) { //因為設置的對象是響應式的所以只要 //響應式對象改變都會收集,但是只有 //在effect執(zhí)行的時候activeEffect才有值 //才能收集到依賴并且dep采用了集合防止 //重復收集同一個依賴 if (activeEffect) { dep.add(activeEffect); } } //當修改值的時候觸發(fā)依賴函數(shù) function trigger(object, key, newVal, oldVal) { const depsMap = reactiveMap.get(object); const dep = depsMap.get(key);//找到target和key對應的dep //執(zhí)行依賴函數(shù) if (dep.size > 0) { for (const effect of dep) { if (effect.scheduler) { effect.scheduler(); } else effect.run(); } } }
這就是reactivity最核心的邏輯,是不是覺得非常簡單呢?目前我們代理的是對象,那如果我們代理的時候是一個值呢?那就要使用到ref,下面我們來寫寫極簡版的ref實現(xiàn)吧!
(2).實現(xiàn)ref
- 我們可以采用把值包裝成一個對象的方法,利用類自帶的攔截器當get的時候收集依賴,那么收集依賴需要target和key,顯然target就是RefImpl實例,而key就是value,同樣在set的時候觸發(fā)依賴就實現(xiàn)了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).實現(xiàn)computed
說到computed,他是如何實現(xiàn)的呢?我們先來說說他的要求,computed接受一個getter,必須有返回值,當內(nèi)部收集到的響應式發(fā)生改變的時候我們?nèi)プx取compute.value也會發(fā)生相應的變化,并且computed返回的對象也是響應式的例如:
//設置響應式 const proxy = reactive({ a: 1, b: { a: 1 } }); //設置計算屬性 const comp = computed(() => { return proxy.a + 1; }); effect(() => { console.log(comp.value); }); //當proxy.a發(fā)生變化,讀取comp的value也會發(fā)生變化,并且因為comp是響應式 //在effect中被收集了,所以當proxy.a發(fā)生變化也會導致effect中的函數(shù)執(zhí)行 proxy.a++;
下面我們來看看他的實現(xiàn)
這里必須要說一個scheduler,ReactiveEffect接受兩個參數(shù)如果有第二個參數(shù),那么就不會調(diào)用run方法而是調(diào)用scheduler方法。
所以computed的實現(xiàn)原理就是,當執(zhí)行computed這個函數(shù)的時候創(chuàng)建ComputedRefImpl,而構造器中會自動創(chuàng)建ReactiveEffet,這個時候會傳遞一個schduler,也就是說以后這個effect不會調(diào)用run方法而是調(diào)用schduler方法,我們只需要在在shcduler方法中設置dirty為true表示修改了值,然后在進行調(diào)度,通過comp.value收集到的依賴就可以了,這里的響應式其實有兩個地方,第一個地方是computed內(nèi)部有一個響應式,第二是comp本身也是響應式需要收集依賴,當computed內(nèi)部響應式發(fā)生變化會導致this._effect.scheduler執(zhí)行,那么dirty會設置為true,當comp.value在其他effect中的時候會觸發(fā)track收集依賴,所以當computed內(nèi)部響應式發(fā)生改變就會觸發(fā)get時候收集到的effect。
class ComputedRefImpl { constructor(getter) { //調(diào)度器 this._effect = new ReactiveEffect(getter, () => { if (!this._dirty) { this._dirty = true; //修改了getter中監(jiān)聽到的值 //引起對dep內(nèi)的更新 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的建議版本實現(xiàn),相信你已經(jīng)基本了解了reactivety,我們開始分析源碼吧!
第二部分:深入分析對于object、array的響應式代理
- 我們重reactivity包最常用的api,reactive開始進行分析,因為采用了工廠函數(shù),所以對應的shallow,readonly,shallowReadonly也會分析到。
- 我們先來看看reactive函數(shù)
//深度代理 export function reactive(target) { //如果被代理的是readonly返回已經(jīng)被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 ); }
我們發(fā)現(xiàn)這四個api本質(zhì)都是調(diào)用了createReactiveObject,但是他們傳遞的參數(shù)是不同的,對于不同的代理handlers處理是不同的,而其中還有對于map set等的代理就需要使用到collectionHandlers,對于代理過的對象我們再次對這個對象進行代理是不必要的,需要reactiveMap進行緩存。已經(jīng)代理過的對象讀取緩存就可以了。
接下來我們深入createReactiveObject,先來看看源代碼
export function createReactiveObject( target, isReadonly, baseHandlers, collectionHandlers, proxyMap ) { //不能夠代理非對象 if (!isObject(target)) { { console.warn(`value cannot be made reactive: ${String(target)}`); } return target; } //已經(jīng)代理過的對象不需要在進行二次代理 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; }
- 這個函數(shù)比較簡單,首先是第一種情況,調(diào)用了 reactive(target) 然后再次調(diào)用 reactive(target) 會返回同一個proxy代理對象,因為內(nèi)部建立了reactiveMap緩存
- 第二種情況是得到了 proxy = reactive(target) 然后再對proxy進行代理reactive(proxy) 這樣的為了防止二次代理,最終會選擇返回proxy。
- 當然還會判斷對于不是對象的,是不能夠進行代理的
- 之后還通過targetType判斷了當前代理的類型,對于不同的類型使用不同的代理方式,我們順便來看看getTargetType函數(shù)
//如果對象帶有__v_skip或則對象不可拓展則不可代理 //然后根據(jù)類型判斷需要哪種函數(shù)進行代理 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的實現(xiàn),現(xiàn)在我們來看看baseHandlers,baseHandlers顯然是根據(jù)shallow readonly不同傳遞的不同的handlers,其中包含:
- mutableHandlers
- shallowReadonlyHandlers
- readonlyHandlers
- shallowReactiveHandlers 我們看看他是如何創(chuàng)建這四個handlers的吧!
//reactive的proxy handlers //這個便是new Proxy()中的第二個參數(shù),可以攔截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
顯然,在以上代碼中出現(xiàn)了幾個代理函數(shù)分別是getter setter deleteProperty ownKeys has,接下來我們便對每一個進行分析。
(1).handlers中的getter
- 我們發(fā)現(xiàn)對于getter,有shallowGet、readonlyGet、shallowReadonlyGet以及get,我們看看是如何得到這些方法的。
const get = createGetter(); const shallowGet = createGetter(false, true); const readonlyGet = createGetter(true, false); const shallowReadonlyGet = createGetter(true, true);
他們都調(diào)用了createGetter方法,這是一個工廠函數(shù),通過傳遞isReadonly isShallow來判斷是哪種類型的getter,然后創(chuàng)建不同的get。所以接下來我們自然而然需要分析createGetter函數(shù)。
//創(chuàng)造getter的工廠函數(shù),通過是否是只讀和 //是否只代理第一層創(chuàng)造不同的getter函數(shù) export function createGetter(isReadonly = false, shallow = false) { //傳遞進入Proxy的get函數(shù) //例如const obj = {a:2} // const proxy = new Proxy(obj,{ // get(target,key,receiver){ // 當通過proxy.a對obj進行訪問的時候,會先進入這個函數(shù) // 返回值將會作為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的時候,根據(jù)當前的readonly和shallow屬性 //訪問不同的map表,通過map表獲得代理前的對象 else if ( key === RAW && receiver === (isReadonly ? shallow ? shallowReadonlyMap : readonlyMap : shallow ? shallowReactiveMap : reactiveMap ).get(target) ) { return target; } //判斷當前target是否是數(shù)組 const targetIsArray = isArray(target); //如果調(diào)用的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是否是內(nèi)置的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 //訪問的是數(shù)組的數(shù)字屬性則返回res if (isRef(res)) { return targetIsArray && isIntegerKey(key) ? res : res.value; } //如果得到的結果依然是對象繼續(xù)進行深度代理 if (isObject(res)) { return isReadonly ? readonly(res) : reactive(res); } return res; }; }
- 首先對于已經(jīng)進行了代理的對象,可以通過判斷key=__v_isReactive,__v_isShallow,__v_isReadonly判斷是否是 reactive,shallow,readonly, 當然這也是isReactive、isReadonly等api實現(xiàn)基礎。
- 之后對于某些特殊屬性的訪問我們也不需要去收集依賴例如 [Symbol.iterator]。
- 如果不是只讀的代理,就需要收集依賴方便后續(xù)effect調(diào)用。
- 如果訪問到的value還是一個對象我們還需要進行深度代理。
isNonTrackableKeys函數(shù)、builtInSymbols、如果數(shù)組調(diào)用了push pop includes方法該怎么處理呢?
//這里貼上源碼,感興趣的仔細閱讀,不在進行講解 const isNonTrackableKeys = makeMap(`__proto__,__v_isRef,__isVue`); function makeMap(str, expectsLowerCase) { const map = Object.create(null); //創(chuàng)造一個空對象 const list = str.split(","); //["__proto__","__isVUE__"] for (let i = 0; i < list.length; i++) { map[list[i]] = true; //{"__proto__":true,"__isVUE__":true} } //返回一個函數(shù),用于判斷是否是傳遞的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的所有內(nèi)置屬性key例如Symbol.iterator等。
- 再來看看如何處理數(shù)組特殊方法的調(diào)用。
//當前代理的對象是數(shù)組,且訪問了pop等8個方法中的一個 if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) { //進行代理 return Reflect.get(arrayInstrumentations, key, receiver); } const arrayInstrumentations = createArrayInstrumentations(); function createArrayInstrumentations() { const instrumentations = {}; //攔截數(shù)組的方法 arr.includes() ["includes", "indexOf", "lastIndexOf"].forEach((key) => { instrumentations[key] = function (...args) { //這里的this指向調(diào)用當前方法的數(shù)組 const arr = toRaw(this); //將當前數(shù)組中的所有元素收集依賴 for (let i = 0, l = this.length; i < l; i++) { track(arr, trackOpTypes.get, i + ""); } //執(zhí)行函數(shù) 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變?yōu)閒alse pauseTracking(); //這里toRaw是為了防止二次執(zhí)行getter,執(zhí)行數(shù)組對應的方法 const res = toRaw(this)[key].apply(this, args); //重新收集依賴,將shouldTrack變?yōu)閠rue 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會遍歷數(shù)組中的所有元素并且會有獲取的操作,也就是說數(shù)組所有元素都可能進行訪問執(zhí)行get,所以整個數(shù)組中的所有元素都必須要進行track操作。
- 對于pop等五個方法,依賴收集是混亂的,例如我執(zhí)行shift操作,對于底層來說就需要對元素進行移動,這顯然會導致getter和setter的多次觸發(fā),所以我們必須要停止收集依賴。
好啦,接下來我們進行track函數(shù)進行分析,看看是如何收集依賴的。
export function track(target, type, key) { //當調(diào)用了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); } }
- 這個方法相信大家已經(jīng)相當?shù)氖煜ち税?!跟我們寫的簡易版reactivity是一樣的,就是通過target,key獲取依賴,沒有就創(chuàng)建。
- 那么activeEffect是什么時候賦值的呢?相信在簡易版reactivity中大家已經(jīng)知道啦,就是在調(diào)用effect之前賦值,調(diào)用完成后變?yōu)閚ull,但是源碼的實現(xiàn)更加復雜,考慮的問題更加全面。
export class ReactiveEffect { constructor(fn, scheduler = null, scope) { this.fn = fn; //副作用函數(shù) //調(diào)度器(如果有調(diào)用器就不在執(zhí)行run方法而是執(zhí)行調(diào)度器) this.scheduler = scheduler; this.active = true; /** * 當前副作用被那些變量所依賴 * 例如: * effect(()=>{ * console.log(proxy.a) * }) * effect(()=>{ * console.log(proxy.a) * }) * * 每一個effect的回調(diào)函數(shù)都會產(chǎn)生一個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); } //開始執(zhí)行 run() { if (!this.active) { return this.fn(); } let parent = activeEffect; let lastShouldTrack = shouldTrack; while (parent) { if (parent === this) { return; } parent = parent.parent; } try { //可能有嵌套的effect,當執(zhí)行到effect回調(diào)函數(shù)中有effect的時候 //現(xiàn)在的activeEffect相當于最新創(chuàng)建的effect的父級effect /* 例如:effect(()=>{ 現(xiàn)在指向外部的effect console.log(proxy.a) effect(()=>{ 在這里面的時候activeEffect指向內(nèi)部effect console.log(proxy.b) }) 現(xiàn)在需要將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); } //執(zhí)行effect副作用 return this.fn(); } finally { //退出當前effect回調(diào)函數(shù)的執(zhí)行,要將全局變量退回到當前 //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方法用于停止執(zhí)行副作用執(zhí)行。
- 接下來我們繼續(xù)看trackEffects執(zhí)行。
//收集副作用 export function trackEffects(dep, debuggerEventExtraInfo) { let shouldTrack = false; if (effectTrackDepth <= maxMarkerBits) { if (!newTracked(dep)) { dep.n |= trackOpBit; shouldTrack = !wasTracked(dep); } } else { //如果已經(jīng)收集過就不收集了 shouldTrack = !dep.has(activeEffect); } //通過上面的判斷是否需要收集 if (shouldTrack) { //往當前target key對應的dep中添加effect dep.add(activeEffect); //當前effect中有哪些被代理的變量的dep activeEffect.deps.push(dep); //生命周期,當真正執(zhí)行track的時候調(diào)用函數(shù) if (activeEffect.onTrack) { activeEffect.onTrack({ effect: activeEffect, ...debuggerEventExtraInfo, }); } } }
- 顯然這個函數(shù)就是用于收集effect到dep,同時構建effect的deps(代表當前effect中有哪些被代理過的變量指向的dep,例如proxy.a能指向一個dep,同時proxy.a在當前effect回調(diào)函數(shù)中執(zhí)行,那么對于當前effect來說deps中應該包含代表proxy.a的dep)
- 完成依賴收集我們就可以進入setter的學習了!觸發(fā)依賴更新。
(2).handlers中的setter
//創(chuàng)造setter的工廠函數(shù) 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為數(shù)組且訪問的是數(shù)字 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; }; }
- 根據(jù)hadKey判斷當前是修改值還是新增值,傳遞不同的類型進行trigger(觸發(fā)更新)所以我們接下來繼續(xù)分析trigger
//根據(jù)不同的類型添加必要的副作用到deps中 export function trigger(target, type, key, newValue, oldValue, oldTarget) { const depsMap = targetMap.get(target); if (!depsMap) { return; } let deps = []; //當前要處理的所有依賴 if (type === triggerOpTypes.clear) { //清空,相當于所有的元素都發(fā)生改變 //故而全部都需要添加進依賴 deps = [...depsMap.values()]; } //攔截修改數(shù)組長度的情況 else if (key === "length" && isArray(target)) { //放入key為length或者數(shù)組下標大于設置值的所以依賴 //例如:const a = [1,2,3] a.length=1 //那么數(shù)組長度發(fā)生了變化,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)); } //根據(jù)不同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)) { //當前修改的是數(shù)組且是新增值 //例如 arr.length = 3 arr[4] = 8 //此時數(shù)組長度會發(fā)生改變所以當前數(shù)組的 //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); } } //執(zhí)行所有的副作用 triggerEffects(createDep(effects), eventInfo); } } //創(chuàng)建dep export const createDep = (effects) => { const dep = new Set(effects); dep.w = 0; dep.n = 0; return dep; };
- 這個函數(shù)顯然就是處理邊際情況,收集所有的deps并調(diào)用triggerEffects進行觸發(fā)。
- triggerOpTypes一共有 "clear"、"set"、"delete"、"add",其中只有 "add" 是處理object和array的代理的。
- "clear":當觸發(fā)了clear表示清除當前代理對象所有的元素,所有元素都被修改了,所以所有的dep都需要被添加到deps中。
- "add":代表當前是新增的值,對于數(shù)組來說如果訪問了比自身長度大的屬性,那么length屬性將被修改所以這種情況屬性 "length" 對應的dep也應該被放入 deps。
- 對于數(shù)組假設數(shù)組長度是10,然后修改了數(shù)組length屬性例如arr.length = 3,那么相當于刪除了7個元素,那么這7個元素對應的dep應當放入deps中。
- 接下來繼續(xù)調(diào)用triggerEffects觸發(fā)收集到的所有dep。
//根據(jù)trigger最終組成的deps觸發(fā)所有副作用執(zhí)行 function triggerEffects(dep, debuggerEventExtraInfo) { //拿到所有的effects 包裝成數(shù)組 const effects = isArray(dep) ? dep : [...dep]; //含有computed屬性先執(zhí)行 for (const effect of effects) { if (effect.computed) { triggerEffect(effect, debuggerEventExtraInfo); } } //不含有computed屬性后執(zhí)行 for (const effect of effects) { if (!effect.computed) { triggerEffect(effect, debuggerEventExtraInfo); } } } function triggerEffect(effect, debuggerEventExtraInfo) { if (effect !== activeEffect || effect.allowRecurse) { //生命周期,對于這個effect在進行trigger的時候調(diào)用 if (effect.onTrigger) { effect.onTrigger(shared.extend({ effect }, debuggerEventExtraInfo)); } //如果有調(diào)度器則執(zhí)行調(diào)度器否則執(zhí)行run if (effect.scheduler) { effect.scheduler(); } else { effect.run(); } } }
- 含有computed屬性的先執(zhí)行,沒有的后執(zhí)行,有scheduler調(diào)用scheduler否則調(diào)用run。這樣就完成了觸發(fā)。
(3).handlers的deleteProperty
//處理刪除屬性的邏輯(統(tǒng)一處理) //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) { //觸發(fā)副作用 trigger(target, triggerOpTypes.delete, key, undefined, oldValue); } return result; }
當調(diào)用delete obj.xxx的時候deleteProperty就會監(jiān)聽到,這顯然是修改值的情況所以我們執(zhí)行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、又或者調(diào)用了Reflect.ownKeys。這樣的代碼就會觸發(fā)ownKeys的攔截這個時候其實就是track的類型就是ITERATE_KEY,也就是說如果你寫了Object.keys那么就會收集依賴,某一天你刪除了proxy上的屬性,同樣會觸發(fā)依賴更新。
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我們已經(jīng)分析過了,如果target是非數(shù)組元素,那么追蹤的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或內(nèi)置Symbol屬性 if (!isSymbol(key) || !builtInSymbols.has(key)) { track(target, "has", key); } return result; }
- has 同樣是判斷是否存在元素,不涉及修改,所以是track,傳遞類型為 "has",收集依賴即可,特殊的是has只能攔截注釋中的情況,getOwnProperty是不能攔截的。
好啦! 第二部分們已經(jīng)完成了所有的分析,但是本文還沒有完!因為篇幅過長,第三部分和第四部分我放在下一章節(jié)。我們最后再來總結一下吧!
本文總結:
本文我們寫了一個簡單版本的reactivity,便于大家后續(xù)理解真正的源碼,然后我們分析了如何攔截array和object類型的數(shù)據(jù),總體來說就是在effect執(zhí)行的時候修改當前activeEffect的指向,然后執(zhí)行effect的時候收集依賴通過proxy原生api攔截get has ownKeys的操作,完成依賴的收集,然后在set和delete的時候進行觸發(fā),并且對邊際情況也進行了處理、例如數(shù)組訪問修改length、使用pop push includes方法的處理等。
下文我們將會繼續(xù)分析對于map set weakMap weakSet的攔截以及ref computed等api的實現(xiàn),更多關于vue3源碼分析reactivity的資料請關注腳本之家其它相關文章!
- Android registerForActivityResult動態(tài)申請權限案例詳解
- ActivityManagerService廣播并行發(fā)送與串行發(fā)送示例解析
- ActivityManagerService廣播注冊與發(fā)送示例解析
- ActivityManagerService之Service啟動過程解析
- Android面向單Activity開發(fā)示例解析
- Vue3源碼分析reactivity實現(xiàn)方法示例
- Android10 App啟動Activity源碼分析
- Android?Activity共享元素動畫示例解析
- Android?registerForActivityResult新用法實現(xiàn)兩個Activity間數(shù)據(jù)傳遞
相關文章
vue中beforeRouteLeave實現(xiàn)頁面回退不刷新的示例代碼
這篇文章主要介紹了vue中beforeRouteLeave實現(xiàn)頁面回退不刷新的示例代碼,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-11-11vue循環(huán)中點擊選中再點擊取消(單選)的實現(xiàn)
這篇文章主要介紹了vue循環(huán)中點擊選中再點擊取消(單選)的實現(xiàn),具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-09-09Vue.js實現(xiàn)在下拉列表區(qū)域外點擊即可關閉下拉列表的功能(自定義下拉列表)
這篇文章主要介紹了Vue.js實現(xiàn)在下拉列表區(qū)域外點擊即可關閉下拉列表的功能(自定義下拉列表) ,需要的朋友可以參考下2017-05-05