詳解如何編寫一個Vue3響應(yīng)式系統(tǒng)
前言
都說今年是最慘工作年,大廠裁員,小廠跟風(fēng),簡歷投了幾百封回信的寥寥無幾,金三銀四怕是成了銅三鐵四,冷冷清清,凄凄慘慘。
但是今天的主角,小帥同學(xué)卻在逆風(fēng)環(huán)境中給了面試官當(dāng)頭一喝,秀了他一身,優(yōu)秀如他,到底經(jīng)歷了一場怎樣的面試?
文中的例子和代碼都可以點擊這里查看
1.題目亮相
面試官: 我看你簡歷寫的精通Vue3
,并研究過其源碼? 小伙子很狂?。∧窃劬同F(xiàn)場秀一段如何?
說罷,面試官現(xiàn)場給了一道題...
<div id="app"></div> <script> const $app = document.querySelector('#app') let state = { text: 'hello fatfish' } function effect() { $app.innerText = state.text } effect() setTimeout(() => { // 1秒后希望app的內(nèi)容變成hello Vue3 state.text = 'hello Vue3' }, 1000) </script>
小帥竊喜: 這個簡單,只要攔截state
對象,在對text
進行取值
時,收集effect
函數(shù)依賴,然后text
設(shè)置值時,把收集的effect
函數(shù)執(zhí)行一波就可以。
面試官: 口嗨我也會,別逼逼了,趕緊寫起來...
2 版本1:跑起來了,卻不通用,卒
2.1 源碼實現(xiàn)
小帥很快就寫出了第一版,核心只有兩步:
- 第一步:收集依賴(
effect
函數(shù)),在讀取key時,將effect函數(shù)存儲起來 - 第二步:設(shè)置值時,將依賴(
effect
函數(shù))執(zhí)行
const $app = document.querySelector('#app') const bucket = new Set() const state = new Proxy({ text: 'hello fatfish' }, { get (target, key) { const value = target[ key ] // 第一步:收集依賴,在讀取key時,將effect函數(shù)存儲起來 bucket.add(effect) console.log(`get ${key}: ${value}`) return value }, set (target, key, newValue) { console.log(`set ${key}: ${newValue}`) target[ key ] = newValue // 第二步:設(shè)置值時,將依賴執(zhí)行 bucket.forEach((fn) => fn()) } }) function effect() { console.log('執(zhí)行了effect') $app.innerText = state.text } effect() setTimeout(() => { state.text = 'hello Vue3' }, 1000)
效果預(yù)覽
點擊預(yù)覽,噠噠噠,看起來很簡單哦,瞬間就完成啦!
2.2 面試官點評
面試官: 功能是實現(xiàn)了,但是我不太滿意,你這里收集依賴是寫死的函數(shù)名字effect
,只要稍微變化一下題目,就不行了。
<div id="container"> <div id="app1"></div> <div id="app2"></div> </div>
const $app1 = document.querySelector('#app1') const $app2 = document.querySelector('#app2') const state = { text: 'hello fatfish', text2: 'hello fatfish2' } // 改變app1的值 function effect1() { console.log('執(zhí)行了effect') $app1.innerText = state.text } // 改變app2的值 function effect2() { console.log('執(zhí)行了effect2') $app2.innerText = state.text2 } // 1秒鐘之后兩個div的值要分別改變 setTimeout(() => { state.text = 'hello Vue3' state.text2 = 'hello Vue3-2' }, 1000)
3 版本2: 支持多屬性響應(yīng)式修改和主動注冊
3.1 源碼實現(xiàn)
小帥心想: "大意了,我應(yīng)該把effect
依賴函數(shù)通過某種機制,主動注冊到桶中,這樣無論你是匿名函數(shù)亦或者是具名函數(shù)都一視同仁"
機靈的他馬上就想到了答案。
const bucket = new Set() let activeEffect // 變化點: // 通過effect函數(shù)來主動收集依賴 const effect = function (fn) { // 每執(zhí)行一次,將當(dāng)前fn賦值給activeEffect,這樣在fn中觸發(fā)讀取操作時,就可以被收集進bucket中了 activeEffect = fn // 主動執(zhí)行一次很重要,必不可少 fn() } const state = new Proxy({ text: 'hello fatfish', text2: 'hello fatfish2' }, { get (target, key) { const value = target[ key ] // 變化點:由版本1的effect變成了activeEffect,從而不再依賴具體的函數(shù)名字 bucket.add(activeEffect) console.log(`get ${key}: ${value}`) return value }, set (target, key, newValue) { console.log(`set ${key}: ${newValue}`) target[ key ] = newValue bucket.forEach((fn) => fn()) } }) effect(function effect1 () { console.log('執(zhí)行了effect1') $app1.innerText = state.text }) effect(function effect2() { console.log('執(zhí)行了effect2') $app2.innerText = state.text2 }) setTimeout(() => { state.text = 'hello Vue3' state.text2 = 'hello Vue3-2' }, 1000)
效果預(yù)覽 可以看到,此時app1和app2在1秒后都變成了對應(yīng)值,目標(biāo)達成。
3.2 面試官點評
面試官:小伙子非常不錯,思路靈活,變通很快嘛!不過你有沒有想過一個問題?
給state
上增加一個之前不存在的屬性,你的bucket
卻會把收集的依賴執(zhí)行一次,是不是有點浪費?
能否做到effect
中依賴了state的什么值,其值改變了回調(diào)才會被執(zhí)行?
4 版本3:推倒重來,再次設(shè)計"桶"數(shù)據(jù)結(jié)構(gòu)
4.1 重新設(shè)計數(shù)據(jù)結(jié)構(gòu)
小帥: 心里有點沒底了,簡歷上寫精通Vue
,深入研究過Vue
源碼真TM巨坑??!
面試還得繼續(xù),苦思冥想之后終于明白了第二個版本的問題所在:
沒有在effect
函數(shù)與被操作的目標(biāo)字段之間建立明確的聯(lián)系:
const state = new Proxy({ text: 'hello fatfish' }, { get (target, key) { const value = target[ key ] // 無論`state`上啥屬性被讀取了,都會執(zhí)行`get`然后被收集進`bucket` bucket.add(effect) return value }, set (target, key, newValue) { target[ key ] = newValue // 無論`state`上啥值被修改了,都會觸發(fā)`set`,進而收集的依賴被執(zhí)行。 bucket.forEach((fn) => fn()) } })
1. 新的映射關(guān)系
該如何設(shè)計bucket
中存儲的值呢?咱們先來看看關(guān)鍵代碼
effect(function effectFn () { $app.innerText = state.text })
這段代碼中有幾個角色:
- 被操作(讀?。┑拇韺ο?code>state
- 被操作的(讀取)的字段名text
- 使用
effect
函數(shù)注冊的effectFn
函數(shù)
那么他們之間的關(guān)系可以用一顆樹來表述
state
|__key
|__effectFn
2. 場景1:有兩個effectFn讀取同一個對象的屬性值
effect(function effectFn1 () { // 讀取text state.text }) effect(function effectFn2 () { // 讀取text state.text })
那么按照上面樹形結(jié)構(gòu),現(xiàn)在表示如下: text
屬性應(yīng)該要和effectFn1
、effectFn2
建立聯(lián)系
state
|__text
|__effectFn1
|__effectFn2
3. 場景2:effectFn中讀取了同一個對象的多個不同屬性
effect(function effectFn1 () { // 讀取text1和text2 state.text state.text2 })
text
和text2
屬性應(yīng)該要和effectFn1
建立聯(lián)系
state
|__text
|__effectFn1
|__text2
|__effectFn1
4. 場景3:不同的effectFn中讀取了不同對象的不同屬性
effect(function effectFn1 () { // 讀取text1 state1.text1 }) effect(function effectFn2 () { // 讀取text2 state2.text2 })
對應(yīng)的關(guān)系表示如下:
state1
|__text1
|__effectFn1
state2
|__text2
|__effectFn2
看到這里,相信聰明的你一定明白了,當(dāng)我們改變了state2.text2
的值時,只有effectFn2
函數(shù)會被重新執(zhí)行,而effectFn1
卻不會。當(dāng)然了新增一個以往不存在的屬性時,effectFn1和effectFn2
都不會被執(zhí)行。
5. 畫一個數(shù)據(jù)結(jié)構(gòu)圖來理解一下存儲關(guān)系:
4.2 源碼實現(xiàn)
6: 新版源碼實現(xiàn)
const $app = document.querySelector('#app') // 重新定義bucket數(shù)據(jù)類型為WeakMap const bucket = new WeakMap() let activeEffect const effect = function (fn) { activeEffect = fn fn() } const state = new Proxy({ name: 'fatfish', age: 100 }, { get (target, key) { const value = target[ key ] // activeEffect無值意味著沒有執(zhí)行effect函數(shù),無法收集依賴,直接return掉 if (!activeEffect) { return } // 每個target在bucket中都是一個Map類型: key => effects let depsMap = bucket.get(target) // 第一次攔截,depsMap不存在,先創(chuàng)建聯(lián)系 if (!depsMap) { bucket.set(target, (depsMap = new Map())) } // 根據(jù)當(dāng)前讀取的key,嘗試讀取key的effects函數(shù) let deps = depsMap.get(key) if (!deps) { // deps本質(zhì)是個Set結(jié)構(gòu),即一個key可以存在多個effect函數(shù),被多個effect所依賴 depsMap.set(key, (deps = new Set())) } // 將激活的effectFn存進桶中 deps.add(activeEffect) console.log(`get ${key}: ${value}`) return value }, set (target, key, newValue) { console.log(`set ${key}: ${newValue}`) // 設(shè)置屬性值 target[ key ] = newValue // 讀取depsMap 其結(jié)構(gòu)是 key => effects const depsMap = bucket.get(target) if (!depsMap) { return } // 真正讀取依賴當(dāng)前屬性值key的effects const effects = depsMap.get(key) // 挨個執(zhí)行即可 effects && effects.forEach((fn) => fn()) } }) effect(() => { console.log('執(zhí)行了effect') $app.innerText = `hello ${ state.name }, are you ${state.age} years old?` }) setTimeout(() => { state.name = 'Vue3' state.age = 18 }, 1000)
效果預(yù)覽
可以看到我們給state
新增了一個屬性text
但是effect
并不會被執(zhí)行,修改了name
屬性為juejin
之后才被執(zhí)行了,而視圖層也更新了。
4.3 面試官點評
牛,差點給我整懵逼,小弟佩服!
不過能不能再進一步,你這只能對state
一個對象進行響應(yīng)式處理,能不能再封裝一下,像Vue3
里面使用reactive
一樣使用?
5 版本4:reactive抽象,有點Vue3的味道了
5.1 源碼實現(xiàn)
小帥心想:你一定是不想讓我面試通過,故意刁難我,不過你是面試官你最大。搞就搞。
前面我們已經(jīng)實現(xiàn)了基本的響應(yīng)式功能,不過為了通用化,我們可以進一步封裝。
const bucket = new WeakMap() // 重新定義bucket數(shù)據(jù)類型為WeakMap let activeEffect const effect = function (fn) { activeEffect = fn fn() } // track表示追蹤的意思 function track (target, key) { // activeEffect無值意味著沒有執(zhí)行effect函數(shù),無法收集依賴,直接return掉 if (!activeEffect) { return } // 每個target在bucket中都是一個Map類型: key => effects let depsMap = bucket.get(target) // 第一次攔截,depsMap不存在,先創(chuàng)建聯(lián)系 if (!depsMap) { bucket.set(target, (depsMap = new Map())) } // 根據(jù)當(dāng)前讀取的key,嘗試讀取key的effects函數(shù) let deps = depsMap.get(key) if (!deps) { // deps本質(zhì)是個Set結(jié)構(gòu),即一個key可以存在多個effect函數(shù),被多個effect所依賴 depsMap.set(key, (deps = new Set())) } // 將激活的effectFn存進桶中 deps.add(activeEffect) } // trigger執(zhí)行依賴 function trigger (target, key) { // 讀取depsMap 其結(jié)構(gòu)是 key => effects const depsMap = bucket.get(target) if (!depsMap) { return } // 真正讀取依賴當(dāng)前屬性值key的effects const effects = depsMap.get(key) // 挨個執(zhí)行即可 effects && effects.forEach((fn) => fn()) } // 統(tǒng)一對外暴露響應(yīng)式函數(shù) function reactive (state) { return new Proxy(state, { get (target, key) { const value = target[ key ] track(target, key) console.log(`get ${key}: ${value}`) return value }, set (target, key, newValue) { console.log(`set ${key}: ${newValue}`) // 設(shè)置屬性值 target[ key ] = newValue trigger(target, key) } }) }
有了上面的封裝咱們使用起來就真的有點Vue3
的感覺啦!
const $app = document.querySelector('#app') const nameObj = reactive({ name: 'fatfish' }) const ageObj = reactive({ age: 100 }) effect(() => { console.log('執(zhí)行了effect') $app.innerText = `hello ${ nameObj.name }, are you ${ageObj.age} years old?` }) setTimeout(() => { nameObj.name = 'Vue3' }, 1000) setTimeout(() => { ageObj.age = 18 }, 2000)
效果預(yù)覽
可以看到咱們通過reactive
定義了兩個響應(yīng)式數(shù)據(jù),在1秒后修改了nameObj
的值,視圖也馬上更新了,2秒后修改了ageObj
的值,視圖也馬上更新了。這下夠通用了吧!完美
到此這篇關(guān)于詳解如何編寫一個Vue3響應(yīng)式系統(tǒng)的文章就介紹到這了,更多相關(guān)Vue3響應(yīng)式系統(tǒng)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vuepress實現(xiàn)自定義首頁的樣式風(fēng)格
這篇文章主要介紹了vuepress實現(xiàn)自定義首頁的樣式風(fēng)格,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-08-08Vue中使用jsencrypt進行RSA非對稱加密的操作方法
這篇文章主要介紹了Vue中使用jsencrypt進行RSA非對稱加密,在這里需要注意要加密的數(shù)據(jù)必須是字符串,對Vue?RSA非對稱加密相關(guān)知識感興趣的朋友一起看看吧2022-04-04vue?iview?導(dǎo)航高亮動態(tài)設(shè)置方式
這篇文章主要介紹了vue?iview?導(dǎo)航高亮動態(tài)設(shè)置方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-05-05vue使用svg文件補充-svg放大縮小操作(使用d3.js)
這篇文章主要介紹了vue使用svg文件補充-svg放大縮小操作(使用d3.js),具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-09-09