你們好,我是Mokou,最近一直在作 vue3 相關內容,好比源碼解析和mini-vue3的開發。html
回顧下前幾章的內容,在前幾章中主要講述瞭如下內容。前端
vite
的原理和從零開始實現vue3
使用新姿式reactive
使用和源碼解析track
實現和源碼解析trigger
實現和源碼解析effect
與 track、trigger
工做原理和源碼解析好的,這章的目標:從零開始完成一個 Vue3 !vue
必需要知道的前置知識 effect
與 track、trigger
工做原理,具體詳情請看公衆號 -> 前端進階課
,一個有溫度且沒有廣告的前端技術公衆號。react
在這裏仍是簡單解析下這3個函數的做用吧git
targetMap
targetMap
本章源碼請看 uuz 急需 star 維持生計。
前兩章連載內容:github
首先。咱們2個全局變量,用來存放和定位追蹤的依賴,也就是給 track
和 trigger
使用的倉庫。api
let targetMap = new WeakMap(); let activeEffect;
因此第一個須要設計的方法就是 track
,還記得該track
在vue3是如何調用的嗎?瀏覽器
track(obj, 'get', 'x');
track
會去找 obj.x
是否被追蹤,若是沒找到就將obj.x放入targetMap
(完成追蹤任務),將 obj.x
做爲 map 的 key 將 activeEffect 做爲 map 的 value。閉包
拋開取值異常處理之類的,track
只作了一件事,將activeEffect
塞入targetMap
;app
function track(target, key) { // 首先找 obj 是否有被追蹤 let depsMap = targetMap.get(target); if (!depsMap) { // 若是沒有被追蹤,那麼添加一個 targetMap.set(target, (depsMap = new Map())); } // 而後尋找 obj.x 是否被追蹤 let dep = depsMap.get(key); if (!dep) { // 若是沒有被追蹤,那麼添加一個 depsMap.set(key, (dep = new Set())); } // 若是沒有添加 activeEffect 那麼添加一個 if (!dep.has(activeEffect)) { dep.add(activeEffect); } }
而後就是寫一個 trigger
,還記得trigger
在vue是如何調用的嗎?
trigger(obj, 'set', 'x')
trigger
只會去 targetMap
中尋找obj.x
的追蹤任務,若是找到了就去重,而後執行任務。
也就是說:拋開取值異常相關,trigger
也只作了一件事:從 targetMap
取值而後調用該函數值。
function trigger(target, key) { // 尋找追蹤項 const depsMap = targetMap.get(target); // 沒找到就什麼都不幹 if (!depsMap) return; // 去重 const effects = new Set() depsMap.get(key).forEach(e => effects.add(e)) // 執行 effects.forEach(e => e()) }
最後就是 effect
,還記得該打工仔的api在vue3中是如何調用的嗎?
effect(() => { console.log('run cb') })
effect
接收一個回調函數,而後會被送給 track
。因此咱們能夠這麼完成 effect
_effect
,並執行。而內部 _effect
也作了兩件事
activeEffect
effect
回調函數優秀的代碼呼之欲出。
function effect(fn) { // 定義一個內部 _effect const _effect = function(...args) { // 在執行是將自身賦值給 activeEffect activeEffect = _effect; // 執行回調 return fn(...args); }; _effect(); // 返回閉包 return _effect; }
全部的前置項都完成了,如今開始完成一個 reactive
,也就是對象式響應式的api。還記得vue3中如何使用 reactive
嗎?
<template> <button @click="appendName">{{author.name}}</button> </template> setup() { const author = reactive({ name: 'mokou', }) const appendName = () => author.name += '優秀'; return { author, appendName }; }
經過上面的的優秀代碼,很輕易的實現了vue3的響應式操做。經過回顧前幾章的內容,咱們知道 reactive
是經過 Proxy 代理數據實現的。
這樣咱們就能夠經過 Proxy
來調用 track
和 trigger
,劫持 getter
和 setter
完成響應式設計
export function reactive(target) { // 代理數據 return new Proxy(target, { get(target, prop) { // 執行追蹤 track(target, prop); return Reflect.get(target, prop); }, set(target, prop, newVal) { Reflect.set(target, prop, newVal); // 觸發effect trigger(target, prop); return true; } }) }
好了。一切就緒,那麼咱們掛載下咱們的 fake vue3
吧
export function mount(instance, el) { effect(function() { instance.$data && update(el, instance); }) instance.$data = instance.setup(); update(el, instance); } function update(el, instance) { el.innerHTML = instance.render() }
測試一下。參照 vue3 的寫法。定義個 setup
和 render
。
const App = { $data: null, setup () { let count = reactive({ num: 0 }) setInterval(() => { count.num += 1; }, 1000); return { count }; }, render() { return `<button>${this.$data.count.num}</button>` } } mount(App, document.body)
執行一下,果真是優秀的代碼。響應式正常執行,每次 setInterval
執行後,頁面都重寫刷新了 count.num
的數據。
源碼請看 uuz,ps:7月23日該源碼已經支持 jsx 了。
以上經過 50+
行代碼,輕輕鬆鬆的實現了 vue3
的響應式。但這就結束了嗎?
還有如下問題
Proxy
必定須要傳入對象render
函數 和 h
函數並正確(Vue3的h函數如今是2個不是之前的createElement
了)- -!
,我不聽。使用 reactive 會有一個缺點,那就是,Proxy 只能代理對象,但不能代理基礎類型。
若是你調用這段代碼 new Proxy(0, {})
,瀏覽器會反饋你 Uncaught TypeError: Cannot create proxy with a non-object as target or handler
因此,對於基礎類型的代理。咱們須要一個新的方式,而在 vue3
中,對於基礎類型的新 api 是 ref
<button >{{count}}</button> export default { setup() { const count = ref(0); return { count }; } }
實現 ref 其實很是簡單:利用 js 對象自帶的 getter 就能夠實現
舉個栗子:
let v = 0; let ref = { get value() { console.log('get') return v; }, set value(val) { console.log('set', val) v= val; } } ref.value; // 打印 get ref.value = 3; // 打印 set
那麼經過前面幾章實現的 track
和 trigger
能夠輕鬆實現 ref
直接上完成的代碼
function ref(target) { let value = target const obj = { get value() { track(obj, 'value'); return value; }, set value(newVal) { if (newVal !== value) { value = newVal; trigger(obj, 'value'); } } } return obj; }
那麼該怎麼實現 computed
?
首先:參考 vue3
的 computed
使用方式
let sum = computed(() => { return count.num + num.value + '!' })
盲猜能夠獲得一個想法,經過改造下 effect
能夠實現,即在 effect
調用的那一刻不執行 run
方法。因此咱們能夠加一個 lazy
參數。
function effect(fn, options = {}) { const _effect = function(...args) { activeEffect = _effect; return fn(...args); }; // 添加這段代碼 if (!options.lazy) { _effect(); } return _effect; }
那麼 computed
能夠這麼寫
effect(fn, {lazy: true})
保證 computed
執行的時候不觸發回調。getter
屬性,在 computed
被使用的時候執行回調。dirty
防止出現內存溢出。優秀的代碼呼之欲出:
function computed(fn) { let dirty = true; let value; let _computed; const runner = effect(fn, { lazy: true }); _computed = { get value() { if (dirty) { value = runner(); dirty = false; } return value; } } return _computed; }
那麼問題來了 dirty
在第一次執行後就被設置爲 false
如何重置?
此時 vue3
的解決方法是,給 effect
添加一個 scheduler
用來處理反作用。
function effect(fn, options = {}) { const _effect = function(...args) { activeEffect = _effect; return fn(...args); }; if (!options.lazy) { _effect(); } // 添加這行 _effect.options = options; return _effect; }
既然有了 scheduler
那就須要更改 trigger
來處理新的 scheduler
。
function trigger(target, key) { const depsMap = targetMap.get(target); if (!depsMap) return; const effects = new Set() depsMap.get(key).forEach(e => effects.add(e)) // 更改這一行 effects.forEach(e => scheduleRun(e)) } // 添加一個方法 function scheduleRun(effect) { if (effect.options.scheduler !== void 0) { effect.options.scheduler(effect); } else { effect(); } }
而後,把上面代碼合併一下,computed
就完成了
function computed(fn) { let dirty = true; let value; let _computed; const runner = effect(fn, { lazy: true, scheduler: (e) => { if (!dirty) { dirty = true; trigger(_computed, 'value'); } } }); _computed = { get value() { if (dirty) { value = runner(); dirty = false; } track(_computed, 'value'); return value; } } return _computed; }
track
+ trigger
+ Proxy
getter
和 setter
配合 track
+ trigger
實現的effect
基礎上的改進下章內容:vue3
該怎麼結合 jsx
?
原創不易,給個三連安慰下弟弟吧。
全棧
或 Vue
有好禮相送哦