Vue響應(yīng)式原理深入分析
1.響應(yīng)式數(shù)據(jù)和副作用函數(shù)
(1)副作用函數(shù)
副作用函數(shù)就是會(huì)產(chǎn)生副作用的函數(shù)。
function effect() { document.body.innerText = 'hello world.' }
? 當(dāng)effect函數(shù)執(zhí)行時(shí),它會(huì)設(shè)置body的內(nèi)容,而body是一個(gè)全局變量,除了effect函數(shù)外任何地方都可以訪問到,也就是說effect函數(shù)的執(zhí)行會(huì)對(duì)其他操作產(chǎn)生影響,即effect函數(shù)是一個(gè)副作用函數(shù)。
(2)響應(yīng)式數(shù)據(jù)
const obj = { text: 'hello world.'}; function effect() { document.body.innerText = obj.text; } obj.text = 'text';
? 當(dāng) obj.text = 'text'
這條語(yǔ)句執(zhí)行之后,我們期望 document.body.innerText
的值也能夠隨之修改,這就是通常意義上的響應(yīng)式數(shù)據(jù)。
2.響應(yīng)式數(shù)據(jù)的基本實(shí)現(xiàn)
? 上文中,對(duì)響應(yīng)式數(shù)據(jù)進(jìn)行描述的代碼段,并未實(shí)現(xiàn)真正的響應(yīng)式數(shù)據(jù)。而通過觀察我們可以發(fā)現(xiàn),要實(shí)現(xiàn)真正的響應(yīng)式數(shù)據(jù),我們需要對(duì)數(shù)據(jù)的讀取和設(shè)置進(jìn)行攔截。當(dāng)有操作對(duì)響應(yīng)式數(shù)據(jù)進(jìn)行讀取中,我們將其添加至一個(gè)依賴隊(duì)列,當(dāng)修改響應(yīng)式數(shù)據(jù)的值時(shí),將依賴隊(duì)列中的操作依次取出,并執(zhí)行。以下使用Proxy對(duì)該思路進(jìn)行實(shí)現(xiàn)。
const bucket = new Set(); const data = { text: "hello world." }; const obj = new Proxy(data, { get(target, key) { bucket.add(effect); return target[key]; }, set(target, key, newVal) { target[key] = newVal; bucket.forEach((fn) => fn()); return true; }, }); const body = { innerText: "", }; function effect() { body.innerText = obj.text; } effect(); console.log(body.innerText); // hello world obj.text = "text"; console.log(body.innerText); // text
? 但是,該實(shí)現(xiàn)仍然存在缺陷,比如說只能通過effect函數(shù)的名字實(shí)現(xiàn)依賴收集。
3.設(shè)計(jì)一個(gè)完善的響應(yīng)式系統(tǒng)
(1)消除依賴收集的硬綁定
? 這里我們使用一個(gè)active變量來保存當(dāng)前需要進(jìn)行依賴收集的函數(shù)。
const bucket = new Set(); const data = { text: "hello world." }; let activeEffect; // 新增一個(gè)active變量 const obj = new Proxy(data, { get(target, key) { if (activeEffect) { bucket.add(activeEffect); // 添加active變量保存的函數(shù) } return target[key]; }, set(target, key, newVal) { target[key] = newVal; bucket.forEach((fn) => fn()); return true; }, }); function effect(fn) { activeEffect = fn; // 將當(dāng)前函數(shù)賦值給active變量 fn(); } const body = { innerText: "", }; effect(() => { body.innerText = obj.text; }); console.log(body.innerText); // hello world obj.text = "text"; console.log(body.innerText); // text
? 但是該設(shè)計(jì)仍然存在很多問題,比如說,當(dāng)訪問一個(gè)obj對(duì)象上并不存在的屬性假設(shè)為val
時(shí),邏輯上并沒有存在對(duì)obj.val的訪問,因此該操作不會(huì)產(chǎn)生任何響應(yīng),但實(shí)際上,當(dāng)val
的值被修改后,傳入effect的匿名函數(shù)會(huì)再次執(zhí)行。
(2)基于屬性的依賴收集
? 上一個(gè)版本的響應(yīng)式系統(tǒng)只能對(duì)攔截對(duì)象所有的get和set操作進(jìn)行響應(yīng),并不能做到細(xì)粒度的控制??紤]針對(duì)屬性進(jìn)行依賴攔截,主要有三個(gè)角色,對(duì)象、屬性和依賴方法。因此考慮修改bucket的結(jié)構(gòu),由原來的Set修改為WeakMap(target,Map(key,activeEffect));這樣就可以針對(duì)屬性進(jìn)行細(xì)粒度的依賴收集了。
ps.使用WeakMap是因?yàn)閃eakMap是對(duì)key的弱引用,不會(huì)影響垃圾回收機(jī)制的工作,當(dāng)target對(duì)象不存在任何引用時(shí),說明target對(duì)象已不被需要,這時(shí)target對(duì)象將會(huì)被垃圾回收。如果換成Map,即時(shí)target不存在任何引用,Map已然會(huì)保持對(duì)target的引用,容易造成內(nèi)存泄露。
// bucket的數(shù)據(jù)結(jié)構(gòu)修改為WeakMap const bucket = new WeakMap(); const data = { text: "hello world." }; let activeEffect; const obj = new Proxy(data, { get(target, key) { track(target, key); return target[key]; }, set(target, key, newVal) { target[key] = newVal; trigger(target, key); }, }); function track(target, key) { if (!activeEffect) { return; } let depsMap = bucket.get(target); if (!depsMap) { bucket.set(target, (depsMap = new Map())); } let deps = depsMap.get(key); if (!deps) { depsMap.set(key, (deps = new Set())); } deps.add(activeEffect); } function trigger(target, key) { const depsMap = bucket.get(target); if (!depsMap) return; const effects = depsMap.get(key); effects && effects.forEach((fn) => fn()); } function effect(fn) { activeEffect = fn; fn(); } const body = { innerText: "", }; effect(() => { body.innerText = obj.text; }); console.log(body.innerText); // hello world obj.text = "text"; console.log(body.innerText); // text
(3)分支切換和cleanup
? 對(duì)于一段三元運(yùn)算符 obj.flag? obj.text : 'text'
,我們所期望的結(jié)果是,當(dāng)obj.flag的值為false時(shí),不會(huì)對(duì)obj.text屬性進(jìn)行響應(yīng)操作。 如果是上面那段程序,當(dāng)obj.flag的值為false時(shí),操作obj.text仍然會(huì)進(jìn)行相應(yīng)操作,因?yàn)閛bj.text對(duì)應(yīng)的依賴仍然存在。對(duì)此如果我們能夠在每次的函數(shù)執(zhí)行之前,將其從之前相關(guān)聯(lián)的依賴集合中移除,就可以達(dá)到目的了。這里通過修改副作用函數(shù)來實(shí)現(xiàn):
function effect(fn) { const effectFn = () => { // 在依賴函數(shù)執(zhí)行之前,清除依賴函數(shù)之前的依賴項(xiàng) cleanup(effectFn); activeEffect = effectFn; fn(); }; // 給副作用函數(shù)添加一個(gè)deps數(shù)組用來收集和該副作用函數(shù)相關(guān)聯(lián)的依賴 effectFn.deps = []; effectFn(); } // cleanup函數(shù)實(shí)現(xiàn) function cleanup(effectFn) { for (let i = 0; i < effectFn.deps.length; i++) { const deps = effectFn.deps[i]; deps.delete(effectFn); } effectFn.deps.length = 0; } function track(target, key) { if (!activeEffect) { return; } let depsMap = bucket.get(target); if (!depsMap) { bucket.set(target, (depsMap = new Map())); } let deps = depsMap.get(key); if (!deps) { depsMap.set(key, (deps = new Set())); } deps.add(activeEffect); activeEffect.deps.push(deps); // 在這里收集相關(guān)聯(lián)的依賴 } function trigger(target, key) { const depsMap = bucket.get(target); if (!depsMap) return; const effects = depsMap.get(key); const effectToRun = new Set(effects); // 這里需要?jiǎng)?chuàng)建一個(gè)新集合來遍歷,因?yàn)閒oreach循環(huán)會(huì)對(duì)新加入序列的元素也執(zhí)行遍歷,若遍歷直接原集合,會(huì)出現(xiàn)死循環(huán)。 effectToRun.forEach((fn) => fn()); }
(4)嵌套effect
? 雖然我們已經(jīng)解決了很多問題,但是作為響應(yīng)式系統(tǒng)中比較常見的場(chǎng)景之一的嵌套,我們還不能實(shí)現(xiàn)。因?yàn)槲覀兌x的activeEffect是一個(gè)變量,當(dāng)嵌套操作時(shí),無論怎樣,最后activeEffect變量中存放的都是操作的最后一個(gè)副作用函數(shù)。這里,我們通過加入一個(gè)effect棧的方式,來給這套響應(yīng)式系統(tǒng)添加嵌套功能。
// 定義一個(gè)effect棧 const effectStack = []; function effect(fn) { const effectFn = () => { cleanup(effectFn); activeEffect = effectFn; effectStack.push(effectFn); // 在effect執(zhí)行之前,放入棧中 fn(); effectStack.pop(); // 執(zhí)行完畢立即彈出 activeEffect = effectStack[effectStack.length - 1]; // activeEffect指向新的effect }; effectFn.deps = []; effectFn(); }
(5)避免產(chǎn)生死循環(huán)
? 試看obj.val++
這條語(yǔ)句,它實(shí)際上相當(dāng)于obj.val = obj.val+1
,也就是進(jìn)行了一次讀取操作和一次賦值操作,共兩次操作。而若將該操作運(yùn)行在我們前面的響應(yīng)式系統(tǒng)中,它將會(huì)產(chǎn)生死循環(huán),因?yàn)楫?dāng)我們進(jìn)行了讀取操作后,會(huì)立即進(jìn)行賦值操作,而在賦值操作中,讀取操作再次被觸發(fā),然后循環(huán)的執(zhí)行這一系列操作。這里我們?cè)趖rigger函數(shù)中判斷trigger觸發(fā)的副作用函數(shù),是否與當(dāng)前正在執(zhí)行的副作用函數(shù)相同,若相同,則不執(zhí)行當(dāng)前副作用函數(shù)。這樣就能避免無限遞歸調(diào)用,避免內(nèi)存溢出。
function trigger(target, key) { const depsMap = bucket.get(target); if (!depsMap) return; const effects = depsMap.get(key); const effectToRun = new Set(); effects && effects.forEach((fn) => { // 若正在執(zhí)行的副作用函數(shù)與當(dāng)前觸發(fā)的副作用函數(shù)相同,則不執(zhí)行 if (fn !== activeEffect) { effectToRun.add(fn); } }); effectToRun.forEach((fn) => fn()); }
(6)實(shí)現(xiàn)調(diào)度實(shí)行
現(xiàn)在要實(shí)現(xiàn)一個(gè)這樣的效果:
effect(()=> { console.log(obj.val); }); obj.val ++; console.log("結(jié)束");
// 這段代碼本來會(huì)輸出的結(jié)果是:
/**
1
2
結(jié)束
**/
// 現(xiàn)在我們想讓它變成
/**
1
結(jié)束
2
**/
這里我們可以通過給effect函數(shù)添加一個(gè)配置項(xiàng)來實(shí)現(xiàn):
effect( ()=> { console.log(obj.val); }, { scheduler(fn) { setTimeout(fn); } } function effect(fn,options = {}) { const effectFn = ()=> { ... } effectFn.deps = []; effectFn.options = options; // 為副作用函數(shù)添加配置項(xiàng) effectFn(); } function trigger(target, key) { const depsMap = bucket.get(target); if (!depsMap) return; const effects = depsMap.get(key); const effectToRun = new Set(); effects && effects.forEach((fn) => { if (fn !== activeEffect) { effectToRun.add(fn); } }); effectToRun.forEach((fn) => { // 若當(dāng)前依賴函數(shù)含有調(diào)度執(zhí)行,將當(dāng)前函數(shù)傳遞給調(diào)度函數(shù)執(zhí)行 if (fn.options.scheduler) { fn.options.scheduler(fn); //將當(dāng)前函數(shù)傳遞給調(diào)度函數(shù) } else { fn(); } }); }
如果還要實(shí)現(xiàn)一下效果:
effect(()=> { console.log(obj.val); }); obj.val ++; obj.val ++; // 這段代碼本來會(huì)輸出的結(jié)果是: /** 1 2 3 **/ // 現(xiàn)在我們想讓它變成 /** 1 3 **/
這里通過添加一個(gè)任務(wù)執(zhí)行隊(duì)列來實(shí)現(xiàn):
const jobQueue = new Set(); const p = Promise.resolve(); let isFlushing = false; effect( ()=> { console.log(obj.val); }, { scheduler(fn){ jobQueue.add(fn); flushJob(); } } ); function flushJob() { if(isFlushing) return; isFlushing = true; p.then(()=> { jobQueue.forEach(job=>job()); }).finally(()=> { isFlushing = false; }) }
? 像這樣,由于Set保證了任務(wù)的唯一性,也就是jobQueue中只會(huì)保存唯一的一個(gè)任務(wù),即當(dāng)前執(zhí)行的任務(wù)。而isFlushing標(biāo)記則保證任務(wù)只會(huì)執(zhí)行一次。而因?yàn)橥ㄟ^Promise將任務(wù)添加到了微任務(wù)隊(duì)列中,當(dāng)任務(wù)最后執(zhí)行的時(shí)候,obj.val的值已經(jīng)是3了。
(7)computed和lazy
? 計(jì)算屬性是vue中一個(gè)比較有特色的屬性,它會(huì)緩存表達(dá)式的計(jì)算結(jié)果,只有當(dāng)表達(dá)式依賴的變量發(fā)生變化時(shí),它才會(huì)進(jìn)行重新計(jì)算。實(shí)現(xiàn)計(jì)算屬性的前提是實(shí)現(xiàn)懶加載標(biāo)記,這里我們可以通過之前effect函數(shù)的配置項(xiàng)來實(shí)現(xiàn)。
effect( ()=> { return ()=>obj.val * 2; }, { lazy: true; // 設(shè)置 lazy 標(biāo)記 } ); effect(fn, options = {}) { const effectFn = () => { cleanup(effectFn); activeEffect = effectFn; effectStack.push(effectFn); const res = fn(); effectStack.pop(); activeEffect = effectStack[effectStack.length - 1]; return res; }; effectFn.deps = []; effectFn.options = options; if (!effectFn.options.lazy) { // 若副作用函數(shù)持有l(wèi)azy標(biāo)記,則直接將副作用函數(shù)返回 effectFn(); } return effectFn; }
通過上面對(duì)lazy標(biāo)記的設(shè)置,現(xiàn)在可以實(shí)現(xiàn)下面的效果:
const effectFn = effect( ()=> { return ()=>obj.val * 2; }, { lazy: true; // 設(shè)置 lazy 標(biāo)記 } )(); console.log(effectFn); // 2
在此基礎(chǔ)上,我們來實(shí)現(xiàn)computed
function computed(getter) { let value; let dirty = false; const effectFn = effect(getter, { lazy: true, scheduler(){ if(!dirty) { dirty = true; tirgger(obj, 'value'); } } }); const obj = { get value() { if(!dirty) { value = effectFn(); dirty = true; } track(target, 'value'); return value; } }; return obj; }
(8)watch
? 想要實(shí)現(xiàn)watch,其實(shí)只需要添加一個(gè)scheduler(),像是這樣:
effect( ()=> { consoloe.log(obj.val); }, { scheduler() { console.log("數(shù)值發(fā)生了變化"); } } )
就可以實(shí)現(xiàn)一個(gè)基本的watch效果,現(xiàn)在來編寫一個(gè)功能完整的watch函數(shù)
function watch(source, cb) { let getter; if(typeof source === "function") { //若傳入()=> obj.val,則直接使用該匿名函數(shù) getter = source; } else { getter = traverse(source); // 否則遞歸遍歷該對(duì)象的所有屬性,從而達(dá)到監(jiān)聽所有屬性的目的 } let oldValue, newValue; // 保存新舊值 const effectFn = effect(getter, { lazy: true, scheduler() { newValue = effectFn(); // 獲取新值 cb(oldValue, newValue); oldValue = newValue; // 函數(shù)執(zhí)行完后,更新舊值。 } }); oldValue = effectFn(); // 獲取初始舊值 } function traverse(value, seen = new Set()) { if(typeof value !== 'object' || value !== null || seen.has(value)) return ; seen.add(value); for(const k in seen) { traverse(seen[k],seen); } }
到此這篇關(guān)于Vue響應(yīng)式原理深入分析的文章就介紹到這了,更多相關(guān)Vue響應(yīng)式原理內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vue 實(shí)現(xiàn)購(gòu)物車總價(jià)計(jì)算
今天小編就為大家分享一篇vue 實(shí)現(xiàn)購(gòu)物車總價(jià)計(jì)算,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2019-11-11Vue項(xiàng)目中使用百度地圖api的詳細(xì)步驟
在之前的一個(gè)小項(xiàng)目中,用到的顯示當(dāng)?shù)氐牡貓D功能,下面這篇文章主要給大家介紹了關(guān)于Vue項(xiàng)目中使用百度地圖api的詳細(xì)步驟,文中通過圖文介紹的非常詳細(xì),需要的朋友可以參考下2022-10-10vue實(shí)現(xiàn)標(biāo)簽云效果的方法詳解
這篇文章主要介紹了vue實(shí)現(xiàn)標(biāo)簽云效果的方法,結(jié)合實(shí)例形式詳細(xì)分析了vue標(biāo)簽云的實(shí)現(xiàn)技巧與相關(guān)操作注意事項(xiàng),需要的朋友可以參考下2019-08-08vue3?使用setup語(yǔ)法糖實(shí)現(xiàn)分類管理功能
這篇文章主要介紹了vue3?使用setup語(yǔ)法糖實(shí)現(xiàn)分類管理,本次模塊使用 vue3+element-plus 實(shí)現(xiàn)一個(gè)新聞?wù)镜暮笈_(tái)分類管理模塊,其中新增、編輯采用對(duì)話框方式公用一個(gè)表單,需要的朋友可以參考下2022-08-08vue實(shí)現(xiàn)簡(jiǎn)單學(xué)生信息管理
這篇文章主要為大家詳細(xì)介紹了vue實(shí)現(xiàn)簡(jiǎn)單學(xué)生信息管理,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-05-05VUE3?Vite打包后動(dòng)態(tài)圖片資源不顯示問題解決方法
這篇文章主要給大家介紹了關(guān)于VUE3?Vite打包后動(dòng)態(tài)圖片資源不顯示問題的解決方法,可能是因?yàn)樵诓渴鸷蟮姆?wù)器環(huán)境中對(duì)中文文件名的支持不完善,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-09-09