引言
Vue.js 3.0 "One Piece"
正式發(fā)布已經(jīng)有一段時間了,真可謂是千呼萬喚始出來啊!
相比于 Vue2.x
,Vue3.0
在新的版本中提供了更好的性能、更小的捆綁包體積、更好的 TypeScript
集成、用于處理大規(guī)模用例的新 API
。
在發(fā)布之前,尤大大就已經(jīng)聲明了響應(yīng)式方面將采用 Proxy
對于之前的 Object.defineProperty
進(jìn)行改寫。其主要目的就是彌補(bǔ) Object.defineProperty
自身的一些缺陷,例如無法檢測到對象屬性的新增或者刪除,不能監(jiān)聽數(shù)組的變化等。
而 Vue3
采用了新的 Proxy
實現(xiàn)數(shù)據(jù)讀取和設(shè)置攔截,不僅彌補(bǔ)了之前 Vue2
中 Object.defineProperty
的缺陷,同時也帶來了性能上的提升。
今天,我們就來盤一盤它,看看 Vue3
中響應(yīng)式是如何實現(xiàn)的。
Proxy ?
The Proxy object enables you to create a proxy for another object, which can intercept and redefine fundamental operations for that object.
MDN
Proxy
- 代理,顧名思義,就是在要訪問的對象之前增加一個中間層,這樣就不直接訪問對象,而是通過中間層做一個中轉(zhuǎn),通過操作代理對象,來實現(xiàn)修改目標(biāo)對象。
關(guān)于 Proxy
的更多的知識,可以參考我之前的一篇文章 —— 初探 Vue3.0 中的一大亮點——Proxy !,這里我就不在贅述。
reactive 和 effect 方法
Vue3
中響應(yīng)式核心方法就是 reactive
和 effect
, 其中 reactive
方法是負(fù)責(zé)將數(shù)據(jù)變成響應(yīng)式,effect
方法的作用是根據(jù)數(shù)據(jù)變化去更新視圖或調(diào)用函數(shù),與 react
中的 useEffect
有點類似~
其大概用法如下:
let { reactive, effect } = Vue;
let data = reactive({ name: 'Hello' });
effect(() => {
console.log(data.name)
})
data.name = 'World';
默認(rèn)會執(zhí)行一次,打印 Hello
, 之后更改了 data.name
的值后,會在觸發(fā)執(zhí)行一次,打印World
。
我們先看看 reactive
方法的實現(xiàn)~
reactive.js
首先應(yīng)該明確,我們應(yīng)該導(dǎo)出一個 reactive
方法,該方法有一個參數(shù) target
,目的就是將 target
變成響應(yīng)式對象,因此返回值就是一個響應(yīng)式對象。
import {isObject} from "../shared/utils";
// Vue3 響應(yīng)式原理
// 響應(yīng)式方法,將 target 對象變成響應(yīng)式對象
export function reactive (target) {
// 創(chuàng)建響應(yīng)式對象
return createReactiveObject(target);
}
// 創(chuàng)建響應(yīng)式對象
function createReactiveObject (target) {
// 不是對象,直接返回
if ( !isObject(target) ) return target;
// 創(chuàng)建 Proxy 代理
const observed = new Proxy(target,{})
return observed;
}
reactive
方法基本結(jié)構(gòu)就是如此,給定一個對象,返回一個響應(yīng)式對象。
其中 isObject
方法用于判斷是否是對象,不是對象不需要代理,直接返回即可。
reactive
方法的重點是 Proxy
的第二個參數(shù)handler
,它承載監(jiān)控對象變化,依賴收集,視圖更新等各項重大責(zé)任,我們重點來研究這個對象。
handler.js
在 Vue3
中 Proxy
的 handler
主要設(shè)置了 get
,set
,deleteProperty
,has
,ownKeys
這些屬性,即攔截了對象的讀取,設(shè)置,刪除,in
以及 Object.getOwnPropertyNames
方法和 Object.getOwnPropertySymbols
方法。
這里我們偷個懶,暫時就考慮 set
和 get
操作。
handler.get()
get
獲取屬性比較簡單,我們先來看看這個,這里我們用一個方法創(chuàng)建 getHanlder
。
// 創(chuàng)建 get
function createGetter () {
return function get (target, key, receiver) {
// proxy + reflect
const res = Reflect.get(target, key, receiver); // target[key];
// 如果是對象,遞歸代理
if ( isObject(res) ) return reactive(res);
console.log('獲取屬性的值:', 'target:', target, 'key:', key)
return res;
}
}
這里推薦使用了 Reflect.get
而并非 target[key]
。
可以發(fā)現(xiàn),Vue3
是在取值的時候才去遞歸遍歷屬性的,而非 Vue2
中一開始就遞歸 data
給每個屬性添加 Watcher
,這也是 Vue3
性能提升之一。
handler.set()
同理 set
操作,我們也是用一個方法創(chuàng)建 setHandler
。
// 創(chuàng)建 set
function createSetter () {
return function set (target, key, value, receiver) {
// 設(shè)置屬性值
const res = Reflect.set(target, key, value, receiver);
return res;
}
}
Reflect.set
會返回一個 Boolean
值,用于判斷屬性是否設(shè)置成功。
完事后將 handler
導(dǎo)出,然后在 reactive
中引入即可。
const get = createGetter();
const set = createSetter();
// 攔截普通對象和數(shù)組
export const mutableHandler = {
get,
set
}
測試幾組對象貌似沒啥問題,其實是有一個坑,這個坑也跟數(shù)組有關(guān)。
let { reactive } = Vue;
// 代理數(shù)組
let arr = [1,2,3]
let proxy = reactive(arr)
// 添加元素
proxy.push(4)
如上例子,如果我們選擇代理數(shù)組,在 setHandler
中打印其 key
和 value
的話會得到 3 4
,length 4
這兩組值:
- 第一組表示給數(shù)組索引為
3
的位置新增一個4
的值 - 第二組表示將數(shù)組的
length
改為4
如果不作處理,那么會導(dǎo)致如果更新視圖的話,則會觸發(fā)兩次,這肯定是不允許的,因此,我們需要將區(qū)分新增和修改這兩種操作。
Vue3
中是通過判斷 target
是否存在該屬性來區(qū)分是新增還是修改操作,需要借助一個工具方法 —— hasOwnProperty
。
// 判斷自身是否包含某個屬性
function hasOwnProperty (target,key) {
return Object.prototype.hasOwnProperty.call(target,key);
}
這里我們將上述的 createSetter
方法修改如下:
function createSetter () {
return function set (target, key, value, receiver) {
// 需要判斷修改屬性還是新增屬性,如果原始值于新設(shè)置的值一樣,則不作處理
const hasKey = hasOwnProperty(target, key);
// 獲取原始值
const oldVal = target[key];
const res = Reflect.set(target, key, value, receiver); // target[key]=value;
if ( !hasKey ) {
// 新增屬性
console.log('新增了屬性:', 'key:', key, 'value:', value);
} else if ( hasChanged(value, oldVal) ) {
// 原始值于新設(shè)置的值不一樣,修改屬性值
console.log('修改了屬性:', 'key:', key, 'value:', value)
}
// 值未發(fā)生變化,不作處理
return res;
}
}
如此一來,我們調(diào) push
方法的時候,就只會觸發(fā)一次更新了,非常巧妙的避免了無意義的更新操作。
effect.js
光上述構(gòu)造響應(yīng)式對象并不能完成響應(yīng)式的操作,我們還需要一個非常重要的方法 effect
,它會在初始化執(zhí)行的時候存儲跟其有關(guān)的數(shù)據(jù)依賴,當(dāng)依賴數(shù)據(jù)發(fā)生變化的時候,則會再次觸發(fā) effect
傳遞的函數(shù)。
其基本雛形如下,入?yún)⑹且粋€函數(shù),還有個可選參數(shù) options
方便后面計算屬性等使用,暫時不考慮:
// 響應(yīng)式副作用方法
export function effect (fn,options = {}) {
// 創(chuàng)建響應(yīng)式 effect
const reactiveEffect = createReactiveEffect(fn, options);
// 默認(rèn)執(zhí)行一次
reactiveEffect()
}
createReactiveEffect
就是為了將 fn
變成響應(yīng)式函數(shù),監(jiān)控數(shù)據(jù)變化,執(zhí)行 fn
函數(shù),因此該函數(shù)是一個高階函數(shù)。
let activeEffect; // 當(dāng)前 effect
const effectStack = []; // effect 棧
// 創(chuàng)建響應(yīng)式 effect
function createReactiveEffect (fn, options) {
// 創(chuàng)建的響應(yīng)式函數(shù)
const reactiveEffect = function () {
// 防止不停更改屬性導(dǎo)致死循環(huán)
if ( !effectStack.includes(reactiveEffect) ) {
try {
effectStack.push(reactiveEffect);
// 將當(dāng)前 effect 存儲到 activeEffect
activeEffect = reactiveEffect;
// 運行 fn 函數(shù)
return fn();
} finally {
// 執(zhí)行完清空
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];
}
}
}
return reactiveEffect;
}
createReactiveEffect
將原來的 fn
轉(zhuǎn)變成一個 reactvieEffect
, 并將當(dāng)前的 effect
掛到全局的 activeEffect
上,目的是為了一會與當(dāng)前所依賴的屬性做好對應(yīng)關(guān)系。
我們必須要將依賴屬性構(gòu)造成 { prop : [effect,effect] }
這種結(jié)構(gòu),才能保證依賴屬性變化的時候,依次去觸發(fā)與之相關(guān)的 effect
,因此,需要在 get
屬性的時候,做屬性的依賴收集,將屬性與 effect
關(guān)聯(lián)起來。
依賴收集 —— track
在獲取對象的屬性時,會觸發(fā) getHandler
,再次做屬性的依賴收集,即 Vue2
中的發(fā)布訂閱。
在 setHandler
中獲取屬性的時候,做一次 track(target, key)
操作。
整個 track
的數(shù)據(jù)結(jié)構(gòu)大概是這樣
/**
* 最外層是 WeakMap,其 key 是 target 對象,值是一個 map
* map 中包含 target 的屬性,key 為每一個屬性 , 值為屬性對應(yīng)的 `effect`
*/
key val(map)
{name : 'chris} { name : Set(effect,effect) , age : Set() }
目的就是將 target
,key
,effect
之間做好對應(yīng)的關(guān)系映射。
const targetMap = new WeakMap();
// 依賴收集
export function tract(target,key){
// activeEffect 為空
if ( activeEffect === undefined ) {
return; // 說明取值的屬性,不依賴于 effect
}
// 判斷 target 對象是否收集過依賴
let depsMap = targetMap.get(target);
// 不存在構(gòu)建
if ( !depsMap ) {
targetMap.set(target, (depsMap = new Map()));
}
// 判斷要收集的 key 中是否收集過 effect
let dep = depsMap.get(key);
// 不存在則創(chuàng)建
if ( !dep ) {
depsMap.set(key, (dep = new Set()));
}
// 如果未收集過當(dāng)前依賴則添加
if ( !dep.has(activeEffect) ) {
dep.add(activeEffect);
}
}
打印 targetMap
的結(jié)構(gòu)如下:
**觸發(fā)更新 —— trigger
**
上述已經(jīng)完成了依賴收集,剩下就是監(jiān)控數(shù)據(jù)變化,觸發(fā)更新操作,即在 setHandler
中添加 trigger
觸發(fā)操作。
// 觸發(fā)更新
export function trigger (target, type, key) {
// 獲取 target 的依賴
const depsMap = targetMap.get(target);
// 沒有依賴收集,直接返回
if ( !depsMap ) return;
// 獲取 effects
const effects = new Set();
// 添加 key 對應(yīng)的 effect
const add = (effectsToAdd) => {
if ( effectsToAdd ) {
effectsToAdd.forEach(effect => {
effects.add(effect)
})
}
}
// 執(zhí)行單個 effect
const run = (effect) => {
effect && effect()
}
// 獲取 key 對應(yīng)的 effect
if ( key !== null ) {
add(depsMap.get(key));
}
if ( type === 'add' ) { // 對數(shù)組新增會觸發(fā) length 對應(yīng)的依賴
let effects = depsMap.get(Array.isArray(target) ? 'length' : '');
add(effects);
}
// 觸發(fā)更新
effects.forEach(run);
}
這樣一來,獲取數(shù)據(jù)的時候通過 track
進(jìn)行依賴收集,更新數(shù)據(jù)的時候再通過 trigger
進(jìn)行更新,就完成了整個數(shù)據(jù)的響應(yīng)式操作。
再回頭看看我們先前提到的例子:
let { effect, reactive } = Vue;
let data = reactive({ name: 'Hello' })
effect(() => {
console.log(data.name, ' ***** effect ***** ');
})
data.name = 'World'
控制臺會依次打印 Hello ***** effect *****
以及 World ***** effect *****
, 分別是首次渲染觸發(fā)跟更新數(shù)據(jù)重渲染觸發(fā),至此功能實現(xiàn)!
總結(jié)
整體來說,Vue3
相比于 Vue2
在很多方面都做了調(diào)整,數(shù)據(jù)的響應(yīng)式只是冰山一角,但是可以看出尤大團(tuán)隊非常巧妙的利用了 Proxy
的特點以及 es6
的數(shù)據(jù)結(jié)構(gòu)和方法。另外,Composition API
的模式跟 React
在某些程度上有異曲同工之妙,這種設(shè)計模式讓我們在實際開發(fā)使用中更加的方法快捷,值得我們?nèi)W(xué)習(xí),加油!
最后附上倉庫地址 github,歡迎各位大佬批評斧正~