大家好,我是Mokou,最近一直在做 vue3 相關內容,比如源碼解析和mini-vue3的開發。
回顧下前幾章的內容,在前幾章中主要講述了以下內容。
- 新構建工具
vite
的原理和從零開始實現 vue3
使用新姿勢- 新api:
reactive
使用和源碼解析 - 追蹤收集
track
實現和源碼解析 - 追蹤觸發器
trigger
實現和源碼解析 - 響應式核心
effect
與track、trigger
工作原理和源碼解析
好的,這章的目標:從零開始完成一個 Vue3 !
必須要知道的前置知識 effect
與 track、trigger
工作原理,具體詳情請看公眾號 -> 前端進階課
,一個有溫度且沒有廣告的前端技術公眾號。
在這里還是簡單解析下這3個函數的作用吧
- track: 收集依賴,存入
targetMap
- trigger:觸發依賴,使用
targetMap
- effect:副作用處理
本章源碼請看 uuz 急需 star 維持生計。
手摸手實現 Vue3
首先。我們2個全局變量,用來存放和定位追蹤的依賴。優秀的代碼呼之欲出。
let targetMap = new WeakMap();
let activeEffect;
然后優秀的打工仔一號出來了,那就是 effect
,還記得該打工仔的api在vue3中是如何調用的嗎?
effect(() => {
console.log('run cb')
})
該打工仔接收一個回調函數,然后會被送給 track
干苦力。所以我們可以這么完成 effect
- 定義一個內部函數
_effect
,並執行。 - 返回一個閉包
而內部 _effect
也做了兩件事
- 將自身賦值給
activeEffect
- 執行
effect
回調函數
優秀的代碼呼之欲出。
function effect(fn) {
// 定義一個內部 _effect
const _effect = function(...args) {
// 在執行是將自身賦值給 activeEffect
activeEffect = _effect;
// 執行回調
return fn(...args);
};
_effect();
// 返回閉包
return _effect;
}
然后出現的是另一個優秀的打工仔 track
,還記得該track
在vue3是如何調用的嗎?
track(obj, 'get', 'x');
該打工仔會去找 obj.x
是否被追蹤,如果沒找到就將obj.x放入targetMap
(完成追蹤任務),然后讓當前最優秀的打工仔 activeEffect
制定工期(將obj.x作為map的key將activeEffect作為map的value)。
優秀的代碼呼之欲出。
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')
其實該打工仔只會去 targetMap
中尋找obj.x
的追蹤任務,如果找到了就去重,然后執行任務。
優秀的代碼呼之欲出。
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())
}
打工仔都就緒了。然后出現了優秀的包工頭 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 代理數據實現的,而優秀的包工頭只需要看看工地,然后讓優秀的打工仔干活就可以了。
優秀的代碼呼之欲出。
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()
}
用 mini-vue3 寫一個 demo
測試一下。參照 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
了)- 虛擬 dom 的遞歸
- 別再說了
- -!
,我不聽。
ref
使用 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
那么該怎么實現 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;
}
總結
- reactive 的核心是
track
+trigger
+Proxy
- ref 是通過對象自有的
getter
和setter
配合track
+trigger
實現的 - computed 其實是一個在
effect
基礎上的改進
下章內容:vue3
該怎么結合 jsx
?
最后
原創不易,給個三連安慰下弟弟吧。
- 源碼請看 uuz
- 本文內容出自 https://github.com/zhongmeizhi/FED-note
- 歡迎關注公眾號「前端進階課」認真學前端,一起進階。回復
全棧
或Vue
有好禮相送哦