話說vue3已經發布,就引起了大量前端人員的關注,木得辦法,學不動也得硬着頭皮學呀,本篇文章就簡單介紹一下「vue3的數據響應原理」,以及簡單實現其reactive、effect、computed函數,希望能對大家理解vue3響應式有一點點的幫助。話不多說,看下面栗子的代碼和其運行的結果。
<div id="root"></div> <button id="btn">年齡+1</button>
const root = document.querySelector('#root') const btn = document.querySelector('#btn') const ob = reactive({ name: '張三', age: 10 }) let cAge = computed(() => ob.age * 2) effect(() => { root.innerhtml = `<h1>${ob.name}---${ob.age}---${cAge.value}</h1>` }) btn.onclick = function () { ob.age += 1 }
上面帶代碼,是每點擊一次按鈕,就會給obj.age + 1 然后執行effect,計算屬性也會相應的 ob.age * 2 執行。
所以,針對上面的栗子,制定一些小目標,然后一一實現,如下:
- 1、實現reactive函數
- 2、實現effect函數
- 3、把reactive 和 effect 串聯起來
- 4、實現computed函數
實現reactive函數
reactive其實數據響應式函數,其內部通過es6的proxy api 來實現,
下面面其實通過簡單幾行代碼,就可以對一個對象進行代理攔截了。
const handlers = { get (target, key, receiver) { return Reflect.get(target, key, receiver) }, set (target, key, value, receiver) { return Reflect.set(target, key, value, receiver) } } function reactive (target) { observed = new Proxy(target, handlers) return observed } let person = { name: '張三', age: 10 } let ob = reactive(person)
但是這么做的話有缺點,1、重復多次寫ob = reactive(person)就會一直執行new Proxy,這不是我們想要的。理想情況應該是,代理過的對象緩存下來,下次訪問直接返回緩存對象就可以了;2、同理多次這么寫ob = reactive(person); ob = reactive(ob) 那也要緩存下來。下面我們改造一下上面的reactive函數代碼。
const toProxy = new WeakMap() // 緩存代理過的對象 const toRaw = new WeakMap() // 緩存被代理過的對象 // handlers 跟上面的一樣,為了篇幅這里省略 function reactive (target) { let observed = toProxy.get(target) // 如果是緩存代理過的 if (observed) { return observed } if (toRaw.has(target)) { return target } observed = new Proxy(target, handlers) toProxy.set(target, observed) // 緩存observed toRaw.set(observed, target) // 緩存target return observed } let person = { name: '張三', age: 10 } let ob = reactive(person) ob = reactive(person) // 返回都是緩存的 ob = reactive(ob) // 返回都是緩存的 console.log(ob.age) // 10 ob.age = 20 console.log(ob.age) // 20
這樣子調用reactive()返回都是我們第一次的代理對象啦(ps:WeakMap是弱引用)。緩存做好了,但是還有新的問題,如果代理target對象層級嵌套比較深的話,上面的proxy是做不到深層代理的。例如
let person = { name: '張三', age: 10, hobby: { paly: ['basketball', 'football'] } } let ob = reactive(person) console.log(ob)
從上面的打印結果可以看出hobby 對象沒有我們上面的handlers 代理,也就是說當我們對hobby做一些依賴收集的時候是沒有辦法的,所以我們改寫一下handlers對象。
// 對象類型判斷 const isObject = val => val !== null && typeof val === 'object' const toProxy = new WeakMap() // 緩存代理過的對象 const toRaw = new WeakMap() // 緩存被代理過的對象 const handlers = { get (target, key, receiver) { const res = Reflect.get(target, key, receiver) // TODO: effect 收集 return isObject(res) ? reactive(res) : res }, set (target, key, value, receiver) { const result = Reflect.set(target, key, value, receiver) // TODO: trigger effect return result } } function reactive (target) { let observed = toProxy.get(target) // 如果是緩存代理過的 if (observed) { return observed } if (toRaw.has(target)) { return target } observed = new Proxy(target, handlers) toProxy.set(target, observed) // 緩存observed toRaw.set(observed, target) // 緩存target return observed }
上面的代碼通過在get里面添加 return isObject(res) ? reactive(res) : res,意思是當訪問到某一個對象時候,如果判斷類型是「object」,那么就繼續調用reactive代理。上面也是我們的reactive函數的完整代碼。
實現effect函數
到了這里離我們的目標又近了一步,這里來實現effect函數,首先我們先看看effect的用法。
effect(() => { root.innerhtml = `<h1>${ob.name}---${ob.age}---${cAge.value}</h1>` })
第一感覺看起來很簡單嘛,就是函數當做參數傳進去,然后調用傳進來函數,完事。下面代碼最簡單實現
function effect(fn) { fn() }
但是到這里,所有人都看出來缺點了,這只是執行一次呀?怎么跟響應式聯系起來呀?還有后面computed怎么基於這個實現呀?等等。帶着一大堆問題,通過改寫effect和增加effect功能去解決這一系列問題。
function effect (fn, options = {}) { const effect = createReactiveEffect(fn, options) // 不是理解計算的,不需要調用此時調用effect if (!options.lazy) { effect() } return effect } function createReactiveEffect(fn, options) { const effect = function effect(...args) { return run(effect, fn, args) // 里面執行fn } // 給effect掛在一些屬性 effect.lazy = options.lazy effect.computed = options.computed effect.deps = [] return effect }
在createReactiveEffect函數中:創建一個新的effect函數,並且給這個effect函數掛在一些屬性,為后面做computed准備,這個effect函數里面調用run函數(此時還沒有實現), 最后在返回出新的effect。
在effect函數中:如果判斷options.lazy是false就調用上面創建一個新的effect函數,里面會調用run函數。
把reactive 和 effect 串聯起來
其實上面還沒有寫好的這個run函數的作用,就是把reactive 和 effect 的邏輯串聯起來,下面去實現它,目標又近了一步。
const activeEffectStack = [] // 聲明一個數組,來存儲當前的effect,訂閱時候需要 function run (effect, fn, args) { if (activeEffectStack.indexOf(effect) === -1) { try { // 把effect push到數組中 activeEffectStack.push(effect) return fn(...args) } finally { // 清除已經收集過得effect,為下個effect做准備 activeEffectStack.pop() } } }
上面的代碼,把傳進來的effect推送到一個activeEffectStack數組中,然后執行傳進來的fn(...args),這里的fn就是
fn = () => { root.innerHTML = `<h1>${ob.name}---${ob.age}---${cAge.value}</h1>` }
執行上面的fn訪問到ob.name、ob.age、cAge.value(這是computed得來的),這樣子就會觸發到proxy的getter,就是執行到下面的handlers.get函數
const handlers = { get (target, key, receiver) { const res = Reflect.get(target, key, receiver) // effect 收集 track(target, key) return isObject(res) ? reactive(res) : res }, set (target, key, value, receiver) { const result = Reflect.set(target, key, value, receiver) const extraInfo = { oldValue: target[key], newValue: value } // trigger effect trigger(target, key, extraInfo) return result } }
聰明的小伙伴看到這里已經看出來,上面handlers.get函數里面track的作用是依賴收集,而handlers.set里面trigger是做派發更新的。
下面補全track函數代碼
// 存儲effect const targetMap = new WeakMap() function track (target, key) { // 拿到上面push進來的effect const effect = activeEffectStack[activeEffectStack.length - 1] if (effect) { let depsMap = targetMap.get(target) if (depsMap === void 0) { depsMap = new Map() // targetMap如果不存在target 的 Map 就設置一個 targetMap.set(target, depsMap) } let dep = depsMap.get(key) if (dep === void 0) { dep = new Set() // 如果depsMap里面不存在key 的 Set 就設置一個 depsMap.set(key, dep) } if (!dep.has(effect)) { // 收集當前的effect dep.add(effect) // effect 收集當前的dep effect.deps.push(dep) } } }
看到這里呀,大家別方,上面的代碼意思就是,從run函數里面的activeEffectStack拿到當前的effect,如果有effect,就從targetMap里面拿depsMap,targetMap如果不存在target 的 Map 就設置一個targetMap.set(target, depsMap),再從depsMap 里面拿 key 的 Set ,如果depsMap里面不存在 key 的 Set 就設置一個depsMap.set(key, dep),下面就是收集前的effect和effect 收集當前的dep了。收集完畢后,targetMap的數據結構就類似下面的樣子的了。
// track的作用就是完成下面的數據結構 targetMap = { target: { name: [effect], age: [effect] } } // ps: targetMap 是WeakMap 數據結構,為了直觀和理解就用對象表示 // [effect] 是 Set數據結構,為了直觀和理解就用數組表示
track執行完畢之后,handlers.get就會返回 res,進行一系列收集之后,fn執行完畢,run函數最后就執行finally {activeEffectStack.pop()},因為effect已經收集結束了,清空為了下一個effect收集做處理。
依賴收集已經完畢了,但是當我們更新數據的時候,例如ob.age += 1,更改數據會觸發proxy的getter,也就是會調用handlers.set函數,里面就執行了trigger(target, key, extraInfo),trigger函數如下
// effect 的觸發 function trigger(target, key, extraInfo) { // 拿到所有target的訂閱 const depsMap = targetMap.get(target) // 沒有被訂閱到 if (depsMap === void 0) { return; } const effects = new Set() // 普通的effect const computedRunners = new Set() // computed 的 effect if (key !== void 0) { let deps = depsMap.get(key) // 拿到deps訂閱的每個effect,然后放到對應的Set里面 deps.forEach(effect => { if (effect.computed) { computedRunners.add(effect) } else { effects.add(effect) } }) } const run = effect => { effect() } // 循環調用effect computedRunners.forEach(run) effects.forEach(run) }
上面的代碼的意思是,拿到對應key的effect,然后執行effect,然后執行run,然后執行fn,然后就是get上面那一套流程了,最后拿到數據是更改后新的數據,然后更改視圖。
下面簡單弄一個幫助理解的流程圖,實在不能理解,大家把倉庫代碼拉下來,debuger執行一遍
targetMap = {
name: [effect],
age: [effect]
}
ob.age += 1 -> set() -> trigger() -> age: [effect] -> effect() -> run() -> fn() -> getget() -> 渲染視圖
PPT模板下載大全https://www.wode007.com
實現computed函數
還是先看用法,let cAge = computed(() => ob.age * 2),上面寫effect的時候,有很多次提到為computed做准備,其實computed就是基於effect來實現的,下面我們看代碼
function computed(fn) { const getter = fn // 手動生成一個effect,設置參數 const runner = effect(getter, { computed: true, lazy: true }) // 返回一個對象 return { effect: runner, get value() { value = runner() return value } } }
值得注意的是,我們上面 effet函數里面有個判斷
if (!options.lazy) { effect() }
如果options.lazy為true就不會立刻執行,就相當於let cAge = computed(() => ob.age * 2)不會立刻執行runner函數,當cAge.value才真正的執行。
最后,所有的函數畫成一張流程圖。
如果文章有哪些不對,請各位大佬指出來,我有摸魚時間一定會修正過來的。
至此,所有的的小目標我們都已經完成了