vue3 beta
版本已經發布快兩個月了,相信你們或多或少都有去了解一些vue3
的新特性,也有一部分人調侃學不動了,在我看來,技術確定是不斷更迭的,新的技術出現可以提升生產力,落後的技術確定是要被淘汰的,五年前會JQ一把梭就能找到一份還行的工做,如今只會JQ應該不多公司會要了吧。恰好前兩天尤大也發了一篇文章講述了vue3
的製做歷程,有興趣的同窗能夠點擊連接前往查看,文章是全英文的,英文不是很好的同窗能夠藉助翻譯插件閱讀。好了,廢話很少說,本篇的主題是手寫vue3的響應式功能。html
在寫代碼前,不妨來看看如何使用vue3吧,咱們能夠先去 github.com/vuejs/vue-n… clone一份代碼,使用npm install && npm run dev後,會生成一個packages -> vue -> dist -> vue.global.js文件,這樣咱們就可使用vue3了,在vue文件夾新建一個index.html文件。vue
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Vue3示例</title> </head> <body> <div id="app"></div> <button id="btn">按鈕</button> <script src="./dist/vue.global.js"></script> <script> const { reactive, computed, watchEffect } = Vue; const app = document.querySelector('#app'); const btn = document.querySelector('#btn'); const year = new Date().getFullYear(); let person = reactive({ name: '煙花渲染離別', age: 23 }); let birthYear = computed(() => year - person.age); watchEffect(() => { app.innerHTML = `<div>我叫${person.name},今年${person.age}歲,出生年是${birthYear.value}</div>`; }); btn.addEventListener('click', () => { person.age += 1; }); </script> </body> </html> 複製代碼
能夠看到,咱們每次點擊一次按鈕,觸發person.age += 1;
,而後watchEffect
自動執行,計算屬性也相應更新,如今咱們的目標就很明確了,就是實現reactive
、watchEffect
、computed
方法。react
咱們知道vue3
是基於proxy
來實現響應式的,對proxy
不熟悉的能夠去看看阮一峯老師的es6教程:es6.ruanyifeng.com/#docs/proxy reflect
也是es6
新提供的API,具體做用也能夠參考阮一峯老師的es6教程:es6.ruanyifeng.com/#docs/refle… ,簡單來講他提供了一個操做對象的新API
,將Object對象屬於語言內部的方法放到Reflect
對象上,將老Object方法報錯的狀況改爲返回false
值。 下面咱們來看看具體的代碼吧,它對對象的get
、set
、del
操做進行了代理。git
function isObject(target) { return typeof target === 'object' && target !== null; } function reactive() { // 判斷是否對象,proxy只對對象進行代理 if (!isObject(target)) { return target; } const baseHandler = { set(target, key, value, receiver) { // receiver:它老是指向原始的讀操做所在的那個對象,通常狀況下就是 Proxy 實例 trigger(); // 觸發視圖更新 return Reflect.set(target, key, value, receiver); }, get(target, key, receiver) { return Reflect.get(target, key, value, receiver); }, del(target, key) { return Reflect.deleteProperty(target, key); } }; let observed = new Proxy(target, baseHandler); return observed; } 複製代碼
上面的代碼看上去好像沒啥問題,可是在代理數組的時候,添加、刪除數組的元素,除了能監聽到數組自己要設置的元素變化,還會監聽到數組長度length
屬性修改的變化,以下圖:es6
因此咱們應該只在新增屬性的時候去觸發更新,咱們添加hasOwnProperty
判斷與老值和新值比較判斷,只有修改自身對象的屬性或者修改了自身屬性而且值不一樣的時候纔去更新視圖。github
set(target, key, value, receiver) { const oldValue = target[key]; if (!target.hasOwnProperty(key) || oldValue !== value) { // 新增屬性或者設置屬性老值不等於新值 trigger(target, key); // 觸發視圖更新函數 } return Reflect.set(target, key, value, receiver); } 複製代碼
上面咱們只對對象進行了一層代理,若是對象的屬性對應的值仍是對象的話,它並無被代理過,此時咱們去操做該對象的時候,就不會觸發set
,也就不會更新視圖了。以下圖:npm
那麼咱們應該怎麼進行深層次的代理呢?設計模式
咱們觀察一下person.hair.push(4)
這個操做,當咱們去取person.hair
的時候,會去調用person
的get
方法,拿到屬性hair
的值,那麼咱們就能夠再它拿到值以後判斷是不是對象,再去進行深層次的監聽。數組
get(target, key, receiver) { const res = Reflect.get(target, key, receiver); return isObject(res) ? reactive(res) : res; }, 複製代碼
代理過的對象再去執行reactive
方法的時候,會去從新設置代理,咱們應該避免這種狀況,經過hashmap
緩存代理過的對象,這樣在再次代理的時候,判斷對象存在hashmap
中,直接返回該結果便可。緩存
let obj = { name: '煙花渲染離別', age: 23, hair: [1,2,3] } let person = reactive(obj); person = reactive(obj); person = reactive(obj); 複製代碼
hashmap
緩存代理對象咱們使用WeakMap
緩存代理對象,它是一個弱引用對象,不會致使內存泄露。 es6.ruanyifeng.com/#docs/set-m…
const toProxy = new WeakMap(); // 代理後的對象 const toRaw = new WeakMap(); // 代理前的對象 function reactive(target) { // 判斷是否對象,proxy只對對象進行代理 if (!isObject(target)) { return target; } let proxy = toProxy.get(target); // 當前對象在代理表中,直接返回該對象 if (proxy) { return proxy; } if (toRaw.has(target)) { // 當前對象是代理過的對象 return target; } let observed = new Proxy(target, baseHandler); toProxy.set(target, observed); toRaw.set(observed, target); return observed; } let obj = { name: '煙花渲染離別', age: 23, hair: [1,2,3] } let person = reactive(obj); person = reactive(obj); // 再去代理的時候返回的就是從緩存中取到的數據了 複製代碼
這樣reactive
方法就基本已經實現完了。
咱們先來瞅瞅以前是怎麼渲染DOM的。
watchEffect(() => { app.innerHTML = `<div>我叫${person.name},今年${person.age}歲,出生年是${birthYear.value}</div>`; }); 複製代碼
在初始化默認執行一次watchEffect
函數後,渲染DOM數據,以後依賴的數據發生變化,會自動再次執行,也就會自動更新咱們的DOM內容了,這就是咱們常說的收集依賴,響應式更新。
那麼咱們在哪裏進行依賴收集,何時通知依賴更新呢?
proxy
對象的get
方法,這個時候咱們就能夠收集依賴了。set
方法,咱們在set
中通知依賴更新。 這實際上是一種設計模式叫作發佈訂閱。咱們在get
中收集依賴:
get(target, key, receiver) { const res = Reflect.get(target, key, receiver); track(target, key); // 收集依賴,若是目標上的key變化,執行棧中的effect return isObject(res) ? reactive(res) : res; } 複製代碼
在set
中通知依賴更新:
set(target, key, value, receiver) { if (target.hasOwnProperty(key)) { trigger(target, key); // 觸發更新 } return Reflect.set(target, key, value, receiver); } 複製代碼
能夠看到咱們在get
中執行了一個track
方法進行收集依賴,在set
中執行trigger
觸發更新,這樣咱們知道了它的過程後,再來看看怎麼實現watchEffect
方法。
咱們傳入到watchEffect
方法裏的函數就是咱們要收集的依賴,咱們將收集到的依賴用棧保存起來,棧是一種先進後出的數據結構,具體咱們看看下面代碼實現:
let effectStack = []; // 存儲依賴數據effect function watchEffect(fn, options = {}) { // 建立一個響應式的影響函數,往effectsStack push一個effect函數,執行fn const effect = createReactiveEffect(fn, options); return effect; } function createReactiveEffect(fn) { const effect = function() { if (!effectsStack.includes(effect)) { // 判斷棧中是否已經有過該effecy,防止重複添加 try { effectsStack.push(effect); // 將當前的effect推入棧中 return fn(); // 執行fn } finally { effectsStack.pop(effect); // 避免fn執行報錯,在finally裏執行,將當前effect出棧 } } } effect(); // 默認執行一次 } 複製代碼
上面咱們只是收集了fn
存到effectsStack
中,可是咱們還沒將fn
和對應的對象屬性關聯,下面步咱們要實現track
方法,將effect
和對應的屬性關聯。
let targetsMap = new WeakMap(); function track(target, key) { // 若是taeget中的key發生改變,執行棧中的effect方法 const effect = effectsStack[effectsStack.length - 1]; // 最新的effect,有才建立關聯 if (effect) { let depsMap = targetsMap.get(target); if (!depsMap) { // 第一次渲染沒有,設置對應的匹配值 targetsMap.set(target, depsMap = new Map()); } let deps = depsMap.get(key); if (!deps) { // 第一次渲染沒有,設置對應的匹配值 depsMap.set(key, deps = new Set()); } if (!deps.has(effect)) { deps.add(effect); // 將effect添加到當前的targetsMap對應的target的存放的depsMap裏key對應的deps } } } function trigger(target, key, type) { // 觸發更新,找到依賴effect let depsMap = targetsMap.get(target); if (depsMap) { let deps = depsMap.get(key); if (deps) { deps.forEach(effect => { effect(); }); } } } 複製代碼
targetsMap
的數據結構較爲複雜,它是一個WeakMap
對象,targetsMap
的key
就是咱們target
對象,在targetsMap
中該target
對應的值是一個Map
對象,該Map
對象的key
是target
對象的屬性,Map
對象對應的key
的值是一個Set
數據結構,存放了當前該target.key
對應的effect
依賴。看下面的代碼可能會比較清晰點:
let person = reactive({ name: '煙花渲染離別', }); targetsMap = { person: { 'name': [effect] } } // { // target: { // key: [dep1, dep2] // } // } 複製代碼
watchEffect
方法,將fn
也就是effect
push到effectStack
棧中,執行fn
,若是fn
中有用到reactive
代理過的對象,此時會觸發該代理對象的get
方法,而咱們在get
方法中使用了track
方法收集依賴,track
方法首先從effectStack
中取出最後一個effect
,也就是咱們剛剛push到棧中的effect
,而後判斷它是否存在,若是存在的話,咱們從targetMap
取出對應的target
的depsMap
,若是depsMap
不存在,咱們手動將當前的target
做爲key
,depsMap = new Map()
做爲值設置到targetMap
中,而後咱們再從depsMap
中取出當前代理對象key
對應的依賴deps
,若是不存在則存放一個新Set
進去,而後將對應的effect
添加到該deps
中。set
方法,執行trigger
方法,經過傳入的target
在targetsMap
中找到depsMap
,經過key
在depsMap
中找到對應的deps
,循環執行裏面保存的effect
。寫computed
以前咱們也來回顧下它的用法:
let person = reactive({ name: '煙花渲染離別', age: 23 }); let birthYear = computed(() => 2020 - person.age); person.age += 1; 複製代碼
能夠看到computed
接受一個函數,而後返回一個通過處理後的值,在依賴的數據發生了修改後,computed
也會從新計算一次。
實際computed
它也是一個watchEffect
函數,不過它比較特殊,這裏在調用watchEffect
時候傳入了兩個參數,一個是computed
的fn
,還有一個就是咱們要給watchEffect
的參數{ lazy: true, computed: true }
,咱們以前寫watchEffect
的時候並無對這些參數進行處理,因此如今咱們還得進行處理。
function computed(fn) { let computedValue; const computedEffect = watchEffect(fn, { lazy: true, computed: true }); return { effect: computedEffect, get value() { computedValue = computedEffect(); trackChildRun(computedEffect); return computedValue; } } } function trackChildRun(childEffect) { if (!effectsStack.length) return; const effect = effectsStack[effectsStack.length - 1]; for (let i = 0; i < childEffect.deps.length; i++) { const dep = childEffect.deps[i]; if (!dep.has(effect)) { dep.add(effect); effect.deps.push(dep); } } } 複製代碼
修改watchEffect
方法,接收一個opstion
參數而且添加lazy
屬性判斷,當lazy
爲true
時不當即執行傳入的函數,由於computed
方法是不會當即執行的。
function watchEffect(fn, options = {}) { // 建立一個響應式的影響函數,往effectsStack push一個effect函數,執行fn const effect = createReactiveEffect(fn, options); // start: 添加的代碼 if (!options.lazy) { effect() } // end: 添加的代碼 return effect; } 複製代碼
修改createReactiveEffect
方法,添加options
參數,而且給當前的effect
添加deps
用於收集被計算的屬性的依賴,在本文的實例中就是age
屬性的依賴集合,保存computed
、lazy
屬性。
function createReactiveEffect(fn, options) { const effect = function() { // 判斷棧中是否已經有過該effect,避免遞歸循環重複添加,好比在監聽函數中修改依賴數據 if (!effectsStack.includes(effect)) { try { effectsStack.push(effect); // 將當前的effect推入棧中 return fn(); // 執行fn } finally { effectsStack.pop(effect); // 避免fn執行報錯,在finally裏執行,將當前effect出棧 } } } // start: 添加的代碼 effect.deps = []; effect.computed = options.computed; effect.lazy = options.lazy; // end: 添加的代碼 return effect; } 複製代碼
在track
方法將收集到的屬性依賴集合添加到effect
的deps
。
function track(target, key) { // 若是taeget中的key發生改變,執行棧中的effect方法 const effect = effectsStack[effectsStack.length - 1]; // 最新的effect,有才建立關聯 if (effect) { let depsMap = targetsMap.get(target); if (!depsMap) { // 第一次渲染沒有,設置對應的匹配值 targetsMap.set(target, depsMap = new Map()); } let deps = depsMap.get(key); if (!deps) { // 第一次渲染沒有,設置對應的匹配值 depsMap.set(key, deps = new Set()); } if (!deps.has(effect)) { deps.add(effect); // start: 添加的代碼 effect.deps.push(deps); // 將屬性的依賴集合掛載到effect // end: 添加的代碼 } } } 複製代碼
在trigger
方法經過以前保存在effect
的computed
屬性區分是computed
函數仍是普通的函數,而後分別保存起來,而後先執行普通的effect
函數,在執行computed
函數。
function trigger(target, key, type) { // 觸發更新,找到依賴effect let depsMap = targetsMap.get(target); if (depsMap) { let effects = new Set(); let computedRunners = new Set(); let deps = depsMap.get(key); if (deps) { deps.forEach(effect => { if (effect.computed) { computedRunners.add(effect); } else { effects.add(effect); } }); } if ((type === 'ADD' || type === 'DELETE') && Array.isArray(target)) { const iterationKey = 'length'; const deps = depsMap.get(iterationKey); if (deps) { deps.forEach(effect => { effects.add(effect); }); } } computedRunners.forEach(computed => computed()); effects.forEach(effect => effect()); } } 複製代碼
computed
執行流程咱們來根據下面的代碼來分析執行流程。
const value = reactive({ count: 0 }); const cValue = computed(() => value.count + 1); let dummy; watchEffect(() => { dummy = cValue.value; console.log(dummy) }); value.count = 1; 複製代碼
第一步:先將count
對象轉換成響應式的對象。
第二步:執行computed
方法,computed
內部會執行watchEffect
,而且傳入lazy
、computed
屬性,因爲傳入了lazy
爲true
,因此並不會當即執行生成的effect
,爲了區分,下面統稱這個effect
爲計算effect
,將傳入的fn
稱爲計算fn
,也就是不會往棧中添加數據,此時cValue
保存的是一個包含計算effect
和get
方法的對象。
第三步:執行watchEffect
方法:這也是最關鍵的一步,執行watchEffect
方法,因爲沒有帶lazy
屬性,因此此時會馬上執行effect
方法,往effectsStack
中添加當前的effect
,而後執行fn
。
第四步:執行fn
,執行fn
中會去獲取cValue
的值,此時觸發了computed
的get
方法,而後執行第二步保存的計算effect
。
第五步:執行計算effect
,將計算effect
添加到effectsStack
中(此時的effectsStack
爲[普通effect, 計算effect]
),而後執行計算fn
。
第五步:執行計算fn
,計算fn
依賴了響應式對象value
,此時讀取value
的count
屬性,觸發value
對象的get
方法,get
方法中執行track
方法收集依賴。
第六步:執行track
方法,拿到棧中最後一個元素也就是計算effect
,初始化targetsMap
和depsMap
,而後將計算effect
保存到count
對應的deps
中,同時也將deps
保存到計算effect
的deps
中,下一步要用,這樣就造成了一個雙向收集的關係,計算effect
保存了count
的全部依賴,count
也存了計算effect
的依賴,track
方法執行完執行下一步,返回獲取到的value.count
的值,存到computedValue
中,而後咱們繼續往下執行。
第六步:執行trackChildRun
,計算fn
執行完則將計算effect
從棧中推出,此時effectsStack
的棧頂爲普通effect
,首先咱們在trackChildRun
中拿到棧尾元素也就是剩下的普通effect
,而後循環傳入的計算effect
的deps
數據,咱們在上一步執行track
的時候,在計算effect
的deps
中保存了count
屬性對應的依賴集合,此時的deps
中只有一個元素[計算effect]
,如今將普通effect
也添加到dep
中,因此此時depsMap
爲{ count: [計算effect, 普通effect] }
。
第七步:執行value.count = 1
,觸發set
方法,執行trigger
方法,獲取到count
對應的deps
也就是[計算effect, 普通effect]
,循環deps
分別存儲普通的effect
和計算effect
,而後前後執行計算effect
和普通effect
。
感謝小夥伴們看到了這裏,以爲本文寫的不錯的點個贊再走唄。(^o^)/~