也許你我素未謀面,但很可能相見恨晚,我是前端胖頭魚
前言
都說今年是最慘工作年,大廠裁員,小廠跟風,簡歷投了幾百封回信的寥寥無幾,金三銀四
怕是成了銅三鐵四
,冷冷清清,悽悽慘慘。
但是今天的主角,小帥同學卻在逆風環境中給了面試官當頭一喝,秀了他一身,優秀如他,到底經歷了一場怎樣的面試?
文中的例子和程式碼都可以點選這裡檢視
1.# 題目亮相
面試官: 我看你簡歷寫的精通Vue3
,並研究過其原始碼? 小夥子很狂啊!那咱就現場秀一段如何?
說罷,面試官現場給了一道題...
<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的內容變成hello Vue3
state.text = 'hello Vue3'
}, 1000)
</script>
小帥竊喜😋: 這個簡單,只要攔截state
物件,在對text
進行取值
時,收集effect
函式依賴,然後text
設定值時,把收集的effect
函式執行一波就可以。
面試官: 口嗨我也會,別逼逼了,趕緊寫起來...
2 版本1:跑起來了,卻不通用,卒
2.1# 原始碼實現
小帥很快就寫出了第一版,核心只有兩步:
- 第一步:收集依賴(
effect
函式),在讀取key時,將effect函式儲存起來 - 第二步:設定值時,將依賴(
effect
函式)執行
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函式儲存起來
bucket.add(effect)
console.log(`get ${key}: ${value}`)
return value
},
set (target, key, newValue) {
console.log(`set ${key}: ${newValue}`)
target[ key ] = newValue
// 第二步:設定值時,將依賴執行
bucket.forEach((fn) => fn())
}
})
function effect() {
console.log('執行了effect')
$app.innerText = state.text
}
effect()
setTimeout(() => {
state.text = 'hello Vue3'
}, 1000)
效果預覽
點選預覽,噠噠噠,看起來很簡單哦,瞬間就完成啦!
2.2# 面試官點評
面試官: 功能是實現了,但是我不太滿意,你這裡收集依賴是寫死的函式名字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('執行了effect')
$app1.innerText = state.text
}
// 改變app2的值
function effect2() {
console.log('執行了effect2')
$app2.innerText = state.text2
}
// 1秒鐘之後兩個div的值要分別改變
setTimeout(() => {
state.text = 'hello Vue3'
state.text2 = 'hello Vue3-2'
}, 1000)
3# 版本2: 支援多屬性響應式修改和主動註冊
3.1# 原始碼實現
小帥心想: "大意了,我應該把effect
依賴函式透過某種機制,主動註冊到桶中,這樣無論你是匿名函式亦或者是具名函式都一視同仁"
機靈的他馬上就想到了答案。
const bucket = new Set()
let activeEffect
// 變化點:
// 透過effect函式來主動收集依賴
const effect = function (fn) {
// 每執行一次,將當前fn賦值給activeEffect,這樣在fn中觸發讀取操作時,就可以被收集進bucket中了
activeEffect = fn
// 主動執行一次很重要,必不可少
fn()
}
const state = new Proxy({ text: 'hello fatfish', text2: 'hello fatfish2' }, {
get (target, key) {
const value = target[ key ]
// 變化點:由版本1的effect變成了activeEffect,從而不再依賴具體的函式名字
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('執行了effect1')
$app1.innerText = state.text
})
effect(function effect2() {
console.log('執行了effect2')
$app2.innerText = state.text2
})
setTimeout(() => {
state.text = 'hello Vue3'
state.text2 = 'hello Vue3-2'
}, 1000)
效果預覽
可以看到,此時app1和app2在1秒後都變成了對應值,目標達成。
點選檢視
3.2# 面試官點評
面試官:小夥子非常不錯,思路靈活,變通很快嘛!不過你有沒有想過一個問題?
給state
上增加一個之前不存在的屬性,你的bucket
卻會把收集的依賴執行一次,是不是有點浪費?
能否做到effect
中依賴了state的什麼值,其值改變了回撥才會被執行?
4# 版本3:推倒重來,再次設計"桶"資料結構
4.1# 重新設計資料結構
小帥: 心裡有點沒底了,簡歷上寫精通Vue
,深入研究過Vue
原始碼真TM巨坑啊!
面試還得繼續,苦思冥想之後終於明白了第二個版本的問題所在:
沒有在effect
函式與被操作的目標欄位之間建立明確的聯絡:
const state = new Proxy({ text: 'hello fatfish' }, {
get (target, key) {
const value = target[ key ]
// 無論`state`上啥屬性被讀取了,都會執行`get`然後被收集進`bucket`
bucket.add(effect)
return value
},
set (target, key, newValue) {
target[ key ] = newValue
// 無論`state`上啥值被修改了,都會觸發`set`,進而收集的依賴被執行。
bucket.forEach((fn) => fn())
}
})
1. 新的對映關係
該如何設計bucket
中儲存的值呢?咱們先來看看關鍵程式碼
effect(function effectFn () {
$app.innerText = state.text
})
這段程式碼中有幾個角色:
- 被操作(讀取)的代理物件
state
- 被操作的(讀取)的欄位名text
- 使用
effect
函式註冊的effectFn
函式
那麼他們之間的關係可以用一顆樹來表述
state
|__key
|__effectFn
2. 場景1:有兩個effectFn讀取同一個物件的屬性值
effect(function effectFn1 () {
// 讀取text
state.text
})
effect(function effectFn2 () {
// 讀取text
state.text
})
那麼按照上面樹形結構,現在表示如下:
text
屬性應該要和effectFn1
、effectFn2
建立聯絡
state
|__text
|__effectFn1
|__effectFn2
3. 場景2:effectFn中讀取了同一個物件的多個不同屬性
effect(function effectFn1 () {
// 讀取text1和text2
state.text
state.text2
})
text
和text2
屬性應該要和effectFn1
建立聯絡
state
|__text
|__effectFn1
|__text2
|__effectFn1
4. 場景3:不同的effectFn中讀取了不同物件的不同屬性
effect(function effectFn1 () {
// 讀取text1
state1.text1
})
effect(function effectFn2 () {
// 讀取text2
state2.text2
})
對應的關係表示如下:
state1
|__text1
|__effectFn1
state2
|__text2
|__effectFn2
看到這裡,相信聰明的你一定明白了,當我們改變了state2.text2
的值時,只有effectFn2
函式會被重新執行,而effectFn1
卻不會。當然了新增一個以往不存在的屬性時,effectFn1和effectFn2
都不會被執行。
5. 畫一個資料結構圖來理解一下儲存關係:
4.2# 原始碼實現
6: 新版原始碼實現
const $app = document.querySelector('#app')
// 重新定義bucket資料型別為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無值意味著沒有執行effect函式,無法收集依賴,直接return掉
if (!activeEffect) {
return
}
// 每個target在bucket中都是一個Map型別: key => effects
let depsMap = bucket.get(target)
// 第一次攔截,depsMap不存在,先建立聯絡
if (!depsMap) {
bucket.set(target, (depsMap = new Map()))
}
// 根據當前讀取的key,嘗試讀取key的effects函式
let deps = depsMap.get(key)
if (!deps) {
// deps本質是個Set結構,即一個key可以存在多個effect函式,被多個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}`)
// 設定屬性值
target[ key ] = newValue
// 讀取depsMap 其結構是 key => effects
const depsMap = bucket.get(target)
if (!depsMap) {
return
}
// 真正讀取依賴當前屬性值key的effects
const effects = depsMap.get(key)
// 挨個執行即可
effects && effects.forEach((fn) => fn())
}
})
effect(() => {
console.log('執行了effect')
$app.innerText = `hello ${ state.name }, are you ${state.age} years old?`
})
setTimeout(() => {
state.name = 'Vue3'
state.age = 18
}, 1000)
效果預覽
點選檢視
可以看到我們給state
新增了一個屬性text
但是effect
並不會被執行,修改了name
屬性為juejin
之後才被執行了,而檢視層也更新了。
4.3# 面試官點評
牛逼,牛逼,差點給我整懵逼,小弟佩服!
不過能不能再進一步,你這隻能對state
一個物件進行響應式處理,能不能再封裝一下,像Vue3
裡面使用reactive
一樣使用?
5# 版本4:reactive抽象,有點Vue3的味道了
5.1# 原始碼實現
小帥心想:你一定是不想讓我面試透過,故意刁難我,不過你是面試官你最大。搞就搞。
前面我們已經實現了基本的響應式功能,不過為了通用化,我們可以進一步封裝。
const bucket = new WeakMap()
// 重新定義bucket資料型別為WeakMap
let activeEffect
const effect = function (fn) {
activeEffect = fn
fn()
}
// track表示追蹤的意思
function track (target, key) {
// activeEffect無值意味著沒有執行effect函式,無法收集依賴,直接return掉
if (!activeEffect) {
return
}
// 每個target在bucket中都是一個Map型別: key => effects
let depsMap = bucket.get(target)
// 第一次攔截,depsMap不存在,先建立聯絡
if (!depsMap) {
bucket.set(target, (depsMap = new Map()))
}
// 根據當前讀取的key,嘗試讀取key的effects函式
let deps = depsMap.get(key)
if (!deps) {
// deps本質是個Set結構,即一個key可以存在多個effect函式,被多個effect所依賴
depsMap.set(key, (deps = new Set()))
}
// 將啟用的effectFn存進桶中
deps.add(activeEffect)
}
// trigger執行依賴
function trigger (target, key) {
// 讀取depsMap 其結構是 key => effects
const depsMap = bucket.get(target)
if (!depsMap) {
return
}
// 真正讀取依賴當前屬性值key的effects
const effects = depsMap.get(key)
// 挨個執行即可
effects && effects.forEach((fn) => fn())
}
// 統一對外暴露響應式函式
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}`)
// 設定屬性值
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('執行了effect')
$app.innerText = `hello ${ nameObj.name }, are you ${ageObj.age} years old?`
})
setTimeout(() => {
nameObj.name = 'Vue3'
}, 1000)
setTimeout(() => {
ageObj.age = 18
}, 2000)
效果預覽
點選預覽
可以看到咱們透過reactive
定義了兩個響應式資料,在1秒後修改了nameObj
的值,檢視也馬上更新了,2秒後修改了ageObj
的值,檢視也馬上更新了。這下夠通用了吧!完美
5.2# 面試官點評
面試官: 你特別優秀,不過...
小帥:不過你妹啊! 我就面個試,你要我造個Vue3不成?
面試官: 好好好,哈哈哈!這輪面試透過了,接下來二面面試官會繼續讓你實現更全面的響應式系統,好好準備把!
小帥: 心裡一萬個草泥馬飛過...
6.# 下集預告
雖然小帥已經得到了一面面試官的認可,但是就目前實現的響應式系統來說還是不夠完善,問題依舊不少比如:
- effect巢狀執行會有啥問題?
- 會不會出現迴圈依賴,死迴圈等問題?
- ...
這些問題請看小帥在二面如何解決,敬請期待。
最後
希望能一直給大家分享實用、基礎、進階的知識點,一起早早下班,快樂摸魚。
期待你在掘金關注我:前端胖頭魚,也可以在公眾號裡找到我:前端胖頭魚。
參考
Vue.js設計與實現