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