話說vue3
已經發布,就引發了大量前端人員的關注,木得辦法,學不動也得硬着頭皮學呀,本篇文章就簡單介紹一下「vue3的數據響應原理」,以及簡單實現其reactive
、effect
、computed
函數,但願能對你們理解vue3
響應式有一點點的幫助。話很少說,看下面栗子的代碼和其運行的結果。html
<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
執行,以下圖:前端
因此,針對上面的栗子,制定一些小目標,而後一一實現,以下:vue
reactive
其實數據響應式函數,其內部經過es6
的proxy api
來實現,
下面面其實經過簡單幾行代碼,就能夠對一個對象進行代理攔截了。react
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)
可是這麼作的話有缺點,一、重複屢次寫ob = reactive(person)
就會一直執行new Proxy
,這不是咱們想要的。理想狀況應該是,代理過的對象緩存下來,下次訪問直接返回緩存對象就能夠了;二、同理屢次這麼寫ob = reactive(person); ob = reactive(ob)
那也要緩存下來。下面咱們改造一下上面的reactive
函數代碼。git
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
是作不到深層代理的。例如es6
let person = { name: '張三', age: 10, hobby: { paly: ['basketball', 'football'] } } let ob = reactive(person) console.log(ob)
從上面的打印結果能夠看出hobby
對象沒有咱們上面的handlers
代理,也就是說當咱們對hobby
作一些依賴收集的時候是沒有辦法的,因此咱們改寫一下handlers
對象。github
// 對象類型判斷 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函數
的完整代碼。api
到了這裏離咱們的目標又近了一步,這裏來實現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
函數。
其實上面尚未寫好的這個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() -> 渲染視圖
仍是先看用法,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
才真正的執行。
最後,全部的函數畫成一張流程圖。
若是文章有哪些不對,請各位大佬指出來,我有摸魚時間必定會修正過來的。
至此,全部的的小目標咱們都已經完成了,撒花✿✿ヽ(°▽°)ノ✿
ps: