詳解如何編寫一個(gè)Vue3響應(yīng)式系統(tǒng)
前言
都說今年是最慘工作年,大廠裁員,小廠跟風(fēng),簡歷投了幾百封回信的寥寥無幾,金三銀四怕是成了銅三鐵四,冷冷清清,凄凄慘慘。
但是今天的主角,小帥同學(xué)卻在逆風(fēng)環(huán)境中給了面試官當(dāng)頭一喝,秀了他一身,優(yōu)秀如他,到底經(jīng)歷了一場怎樣的面試?
文中的例子和代碼都可以點(diǎn)擊這里查看
1.題目亮相
面試官: 我看你簡歷寫的精通Vue3,并研究過其源碼? 小伙子很狂??!那咱就現(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>小帥竊喜: 這個(gè)簡單,只要攔截state對象,在對text進(jìn)行取值時(shí),收集effect函數(shù)依賴,然后text設(shè)置值時(shí),把收集的effect函數(shù)執(zhí)行一波就可以。
面試官: 口嗨我也會,別逼逼了,趕緊寫起來...
2 版本1:跑起來了,卻不通用,卒
2.1 源碼實(shí)現(xiàn)
小帥很快就寫出了第一版,核心只有兩步:
- 第一步:收集依賴(
effect函數(shù)),在讀取key時(shí),將effect函數(shù)存儲起來 - 第二步:設(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ù)存儲起來
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ù)覽,噠噠噠,看起來很簡單哦,瞬間就完成啦!

2.2 面試官點(diǎn)評
面試官: 功能是實(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)式修改和主動注冊
3.1 源碼實(shí)現(xiàn)
小帥心想: "大意了,我應(yīng)該把effect依賴函數(shù)通過某種機(jī)制,主動注冊到桶中,這樣無論你是匿名函數(shù)亦或者是具名函數(shù)都一視同仁"
機(jī)靈的他馬上就想到了答案。
const bucket = new Set()
let activeEffect
// 變化點(diǎn):
// 通過effect函數(shù)來主動收集依賴
const effect = function (fn) {
// 每執(zhí)行一次,將當(dāng)前fn賦值給activeEffect,這樣在fn中觸發(fā)讀取操作時(shí),就可以被收集進(jìn)bucket中了
activeEffect = fn
// 主動執(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秒后都變成了對應(yīng)值,目標(biāo)達(dá)成。

3.2 面試官點(diǎn)評
面試官:小伙子非常不錯(cuò),思路靈活,變通很快嘛!不過你有沒有想過一個(gè)問題?
給state上增加一個(gè)之前不存在的屬性,你的bucket卻會把收集的依賴執(zhí)行一次,是不是有點(diǎn)浪費(fèi)?

能否做到effect中依賴了state的什么值,其值改變了回調(diào)才會被執(zhí)行?
4 版本3:推倒重來,再次設(shè)計(jì)"桶"數(shù)據(jù)結(jié)構(gòu)
4.1 重新設(shè)計(jì)數(shù)據(jù)結(jié)構(gòu)
小帥: 心里有點(diǎn)沒底了,簡歷上寫精通Vue,深入研究過Vue源碼真TM巨坑啊!
面試還得繼續(xù),苦思冥想之后終于明白了第二個(gè)版本的問題所在:
沒有在effect函數(shù)與被操作的目標(biāo)字段之間建立明確的聯(lián)系:
const state = new Proxy({ text: 'hello fatfish' }, {
get (target, key) {
const value = target[ key ]
// 無論`state`上啥屬性被讀取了,都會執(zhí)行`get`然后被收集進(jìn)`bucket`
bucket.add(effect)
return value
},
set (target, key, newValue) {
target[ key ] = newValue
// 無論`state`上啥值被修改了,都會觸發(fā)`set`,進(jìn)而收集的依賴被執(zhí)行。
bucket.forEach((fn) => fn())
}
})1. 新的映射關(guān)系
該如何設(shè)計(jì)bucket中存儲的值呢?咱們先來看看關(guān)鍵代碼
effect(function effectFn () {
$app.innerText = state.text
})這段代碼中有幾個(gè)角色:
- 被操作(讀?。┑拇韺ο?code>state
- 被操作的(讀取)的字段名text
- 使用
effect函數(shù)注冊的effectFn函數(shù)
那么他們之間的關(guān)系可以用一顆樹來表述
state
|__key
|__effectFn
2. 場景1:有兩個(gè)effectFn讀取同一個(gè)對象的屬性值
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中讀取了同一個(gè)對象的多個(gè)不同屬性
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的值時(shí),只有effectFn2函數(shù)會被重新執(zhí)行,而effectFn1卻不會。當(dāng)然了新增一個(gè)以往不存在的屬性時(shí),effectFn1和effectFn2都不會被執(zhí)行。
5. 畫一個(gè)數(shù)據(jù)結(jié)構(gòu)圖來理解一下存儲關(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無值意味著沒有執(zhí)行effect函數(shù),無法收集依賴,直接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并不會被執(zhí)行,修改了name屬性為juejin之后才被執(zhí)行了,而視圖層也更新了。

4.3 面試官點(diǎn)評
牛,差點(diǎn)給我整懵逼,小弟佩服!
不過能不能再進(jìn)一步,你這只能對state一個(gè)對象進(jìn)行響應(yīng)式處理,能不能再封裝一下,像Vue3里面使用reactive一樣使用?
5 版本4:reactive抽象,有點(diǎn)Vue3的味道了
5.1 源碼實(shí)現(xiàn)
小帥心想:你一定是不想讓我面試通過,故意刁難我,不過你是面試官你最大。搞就搞。
前面我們已經(jīng)實(shí)現(xiàn)了基本的響應(yīng)式功能,不過為了通用化,我們可以進(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無值意味著沒有執(zhí)行effect函數(shù),無法收集依賴,直接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)一對外暴露響應(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)
}
})
}有了上面的封裝咱們使用起來就真的有點(diǎn)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定義了兩個(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)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vuepress實(shí)現(xiàn)自定義首頁的樣式風(fēng)格
這篇文章主要介紹了vuepress實(shí)現(xiàn)自定義首頁的樣式風(fēng)格,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-08-08
Vue中使用jsencrypt進(jìn)行RSA非對稱加密的操作方法
這篇文章主要介紹了Vue中使用jsencrypt進(jìn)行RSA非對稱加密,在這里需要注意要加密的數(shù)據(jù)必須是字符串,對Vue?RSA非對稱加密相關(guān)知識感興趣的朋友一起看看吧2022-04-04
vue?iview?導(dǎo)航高亮動態(tài)設(shè)置方式
這篇文章主要介紹了vue?iview?導(dǎo)航高亮動態(tài)設(shè)置方式,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-05-05
vue項(xiàng)目中Eslint校驗(yàn)代碼報(bào)錯(cuò)的解決方案
這篇文章主要介紹了vue項(xiàng)目中Eslint校驗(yàn)代碼報(bào)錯(cuò)的解決方案,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-04-04
vue使用svg文件補(bǔ)充-svg放大縮小操作(使用d3.js)
這篇文章主要介紹了vue使用svg文件補(bǔ)充-svg放大縮小操作(使用d3.js),具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-09-09

