VUE2.0 的響應(yīng)式原理
本篇文章篇幅較長,已經(jīng)對2.0響應(yīng)式原理熟悉的可直接跳過此部分,各取所需,共同交流
在vue中我們使用最多的就是響應(yīng)式數(shù)據(jù)了,給我們帶來了很多便利,說起響應(yīng)式數(shù)據(jù),也就是數(shù)據(jù)變了就要更新視圖,我們一步一步看一下在vue2.0中是怎么實現(xiàn)這一功能的~
首先我們定義一個數(shù)據(jù):
let data = {name: "yangbo"}
然后我們改變這個數(shù)據(jù):
data.name = 'yb'
我們都知道在vue中數(shù)據(jù)改變了我們就要更新視圖,所以這時我們就需要一個更新視圖的方法:
function updateView(){
console.log('觸發(fā)更新視圖方法了')
}
這時問題來了,當(dāng)我們設(shè)置這個對象的屬性值的時候,怎么去觸發(fā)我們的updateView()方法呢,這里我們就用到了Object.defineProperty() 方法, 我們通過這個方法來重新定義屬性并給對象的屬性增加getter和setter, 話不多說開始第一步吧:
我們定義一個觀察數(shù)據(jù)的方法,這個方法里我們調(diào)用defineProperty方法去為數(shù)據(jù)的屬性設(shè)置getter和setter;
在defineProperty方法中,我們使用了Object.defineProperty() ,當(dāng)取值的時候就會走get方法,這時我們直接將value返回即可,在重新設(shè)置值的時候(比如data.name = 'yb')就會走set方法,在set方法中我們調(diào)用更新視圖的方法,并將新的值賦給value
function watchData(target) {
if(typeof target !== 'object' || target === null) {
return target;
}
for (let key in target) {
defineProperty(target, key, target[key])
}
}
function defineProperty(target, key, value) {
// 重新給target 定義key 并增加getter和 setter
Object.defineProperty(target, key, {
get() {
return value
},
set(newVal) {
if (newVal !== value) {
updateView();
value = newVal;
}
}
})
};
以上的代碼就完成了第一步,這時問題又來了,我們的數(shù)據(jù)不可能全是一層的,有可能是多層的數(shù)據(jù),比如:
let data = {
name: 'yangbo',
info: {
age: '26'
}
}
所以,我們要做的是如果內(nèi)層還是一個對象,那我們就要再次去觀察這個對象, 這時我們在defineProperty函數(shù)中增加一行代碼,這其實就形成了遞歸:
function defineProperty(target, key, value) {
// 增加一行代碼
watchData(value);
// 重新給target 定義key 并增加getter和 setter
Object.defineProperty(target, key, {
get() {
return value
},
set(newVal) {
if (newVal !== value) {
updateView();
value = newVal;
}
}
})
};
至此可以說我們已經(jīng)為數(shù)據(jù)實現(xiàn)響應(yīng)式了,通過以上代碼可以看出,如果你的Object層級很深,那么遞歸是會影響性能的。接下來我們思考一個問題,假如我們使用如下方式給對象賦值會觸發(fā)幾次更新視圖的方法:
data.info = {sex: 'man'}
data.info.sex = 'woman'
答案是一次,只有data.info = {sex: 'man'}會觸發(fā)更新視圖的方法,因為我們上面的方法并沒有為sex定義getter與setter, 所以我們要再defineProperty中再加一行代碼,如下:
function defineProperty(target, key, value) {
watchData(value);
// 重新給target 定義key 并增加getter和 setter
Object.defineProperty(target, key, {
get() {
return value
},
set(newVal) {
if (newVal !== value) {
// 新增一行代碼,如果設(shè)置的值是Object,則也為新的值添加getter和setter
watchData(newVal);
updateView();
value = newVal;
}
}
})
};
這時就會執(zhí)行兩次更新視圖的方法了,到這里我們基本實現(xiàn)對象的響應(yīng)式了;但再往細(xì)想還是有問題:假如屬性不存在,新增的屬性會是響應(yīng)式的嗎?假如屬性的值是個數(shù)組該怎么做呢?
我們一起來看下,以目前的代碼,以下這種為數(shù)據(jù)中數(shù)組的改變是不會觸發(fā)
更新視圖的方法的:
let data = {name: 'yangbo', phone: [1,2,3]};
watchData(data);
data.phone.push(4);
那我們來讓這種方式改變數(shù)據(jù)也會觸發(fā)更新視圖,首先我們看下,我們調(diào)用了push方法,這個方法是Array的,所以我們需要對數(shù)組的方法進行重寫,這里我們不能直接對數(shù)組原型上的方法進行重寫,而且我們是拿到數(shù)組的所有方法并對其中的幾個方法進行改寫,本例僅對push和pop方法進行改寫,我們需要創(chuàng)建一個和Array.property相同的Object,并且修改我們的Object并不會影響到Array.property, 這里就用到了繼承,估計你在面試的時候會遇到這個問題。
let ArrayProperty = Array.prototype; // Array的原型
let proto = Object.create(ArrayProperty); // 繼承
接著我們重新定義proto上的方法,記住,定義propo上面的方法的時候我們還需要用到ArrayProperty上的方法, 所以這里我們還是調(diào)用數(shù)組的原始方法,在調(diào)用之前我們來調(diào)用更新視圖的方法,這里不要忘記使用call方法來改變this指向,這里我們做的就是函數(shù)劫持:
['push', 'pop'].forEach((method) => {
proto[method] = function() {
updateView();
// 調(diào)用ArrayProperty上的方法
ArrayProperty[method].call(this, ...arguments);
}
});
函數(shù)我們已經(jīng)劫持了,那在watchData這個方法中,我們怎么讓target找到proto上的方法呢? 我們判斷一下target是不是數(shù)組,如果是數(shù)組那我們讓它的proto指向proto:
function watchData(target) {
if(typeof target !== 'object' || target === null) {
return target;
}
if(Array.isArray(target)) {
// 如果是數(shù)組,讓target的鏈指向proto
target.__proto__ = proto;
}
for (let key in target) {
defineProperty(target, key, target[key])
}
}
以上就搞定了對對象和數(shù)組的響應(yīng)式,下面貼出完整代碼:
let ArrayProperty = Array.prototype; // Array的原型
let proto = Object.create(ArrayProperty); // 繼承
['push', 'pop'].forEach((method) => {
proto[method] = function() {
updateView();
// 調(diào)用ArrayProperty上的方法
ArrayProperty[method].call(this, ...arguments);
}
})
function updateView(val) {
console.log('觸發(fā)更新視圖方法了');
}
function watchData(target) {
if(typeof target !== 'object' || target === null) {
return target;
}
if(Array.isArray(target)) {
// 如果是數(shù)組,讓target的鏈指向proto
target.__proto__ = proto;
}
for (let key in target) {
defineProperty(target, key, target[key])
}
}
function defineProperty(target, key, value) {
watchData(value);
// 重新給target 定義key 并增加getter和 setter
Object.defineProperty(target, key, {
get() {
return value
},
set(newVal) {
if (newVal !== value) {
watchData(newVal);
updateView();
value = newVal;
}
}
})
};
let data = {name: 'yangbo', phone: [1,2,3]};
watchData(data);
data.phone.push(4);
console.log(data.phone);
// let data = {name: 'yangbo', info: {age: 25}}
// watchData(data);
// data.name = 'yb';
// data.info = {sex: 'man'}
// data.info.sex = 'woman'
VUE3.0 的響應(yīng)式原理
上面我們已經(jīng)手寫了簡易版的vue2.0響應(yīng)式,我們不難發(fā)現(xiàn),在2.0中一上來默認(rèn)就會對數(shù)據(jù)進行遞歸,還有就是對象不存在的屬性不能被攔截,帶著這些問題我們一起看下vue3.0的響應(yīng)式實現(xiàn)原理~
首先說下我們現(xiàn)階段怎么體驗vue3.0 :
- 將vue3.0源碼clone到本地
git clone git@github.com:vuejs/vue-next.git
- 安裝依賴,然后在根目錄執(zhí)行
npm run dev
- 編譯好的vue3.0是package/vue/dist/vue.global.js,將vue.global.js引入你的demo就可以使用了,在這之前務(wù)必要看下Vue Composition API
接下來我們使用一下vue3.0
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="container"></div>
<script src="./vue.global.js"></script>
<script>
let proxy = Vue.reactive({name: 'yangbo'});
proxy.name = 'yb';
</script>
</body>
</html>
vue3.0 的響應(yīng)式數(shù)據(jù)是通過es6 的Proxy來實現(xiàn)的,Vue.reactive()返回一個Proxy,從上面的代碼可以看出,當(dāng)let proxy = Vue.reactive({name: 'yangbo'});是應(yīng)該觸發(fā)一次更新視圖的,而當(dāng)變更值proxy.name = 'yb';的時候需要再觸發(fā)一次更新視圖,那在vue3.0中是怎么做的呢? vue3.0這里的主要邏輯是通過Vue.effect()這個方法去實現(xiàn)的,effect這個方法會先執(zhí)行一次,當(dāng)數(shù)據(jù)變化的時候會再執(zhí)行,接下來我們一步一步的區(qū)實現(xiàn),我們這次是用js來實現(xiàn)不是ts,先熟悉思路。
- 首先我們實現(xiàn)Vue.reactive這個方法
不知道你是否還記得上面Vue2.0實現(xiàn)響應(yīng)式的方法,就是一上來就會對數(shù)據(jù)進行遞歸,帶著這個問題看看vue3.0到底怎么優(yōu)化的;在這之前還是需要熟悉ES6的Proxy的~
Vue.reactive返回的是一個響應(yīng)式對象,所以我們在reactive方法中返回一個響應(yīng)式對象:
function reactive(target) {
return createReactive(target);
}
function createReactive(target) {
// 創(chuàng)建響應(yīng)式對象
if(!(typeof target === 'object' && target !== null)) {
return target;
}
// 創(chuàng)建觀察者
let baseHandle = {
get(target, key, receiver) {
// 獲取
let datas = Reflect.get(target, key, receiver);
return datas;
},
set(target, key, value, receiver) {
// 設(shè)置
let res = Reflect.set(target, key, value, receiver);
console.log(res);
return res;
}
}
let observed = new Proxy(target, baseHandle);
return observed;
}
let proxy = reactive({name: 'yangbo'})
proxy.name = 'yb';
console.log(proxy.name);
在vue3.0中,一般proxy會和一個api reflect來一起使用,在get方法中,Reflect.get(target, key, receiver)和target[key]這兩種寫法是等價的;
在我們代碼的set方法中,我們同樣使用Reflect.set()來設(shè)置值,并且它會返回一個布爾值來標(biāo)示是否設(shè)置成功, 通過以上的代碼我們已經(jīng)可以成功代理一層Object,那多層的Object呢?所以我們在獲取的時候,也就是get方法中判斷要獲取的值是不是Object, 如果Reflect的結(jié)果是Object的話,那我們直接把結(jié)果代理(遞歸)
get(target, key, receiver) {
// 獲取
let datas = Reflect.get(target, key, receiver);
// 變更了下面這行代碼
return typeof target === 'object' && target !== null ? reactive(datas) : datas;
}
和2.0相比,2.0是開始就遞歸,而3.0是在取值的時候判斷,有必要才去遞歸。以上我們實現(xiàn)了對Object的代理,但是我們還要防止同一個Object被多次代理,所以這里我們用到了new WeakMap()弱引用
我們來定義兩個WeakMap用于判斷當(dāng)前對象是否被代理過:
let sourceProxy = new WeakMap(); // 存放原對象和代理過的對象
/**
* sourceProxy
* {
* 原對象:代理過的對象
* }
* **/
let toRaw = new WeakMap(); // 存放被代理過的對象和原對象
/**
* toRaw
* {
* 代理過的對象:原對象
* }
* **/
然后我將createReactive方法改為如下:
function createReactive(target) {
// 創(chuàng)建響應(yīng)式對象
if(!(typeof target === 'object' && target !== null)) {
return target;
}
// 判斷target是否被代理過
if(sourceProxy.get(target)) {
return sourceProxy.get(target);
}
// 判斷代理過的里面是否有target, 防止一個對象被多次代理
if (toRaw.has(target)) {
return target;
}
// 創(chuàng)建觀察者
let baseHandle = {
get(target, key, receiver) {
// 獲取
let datas = Reflect.get(target, key, receiver);
// 變更了下面這行代碼
return typeof target === 'object' && target !== null ? reactive(datas) : datas;
},
set(target, key, value, receiver) {
// 設(shè)置
let res = Reflect.set(target, key, value, receiver);
console.log(res);
return res;
}
}
let observed = new Proxy(target, baseHandle);
// 設(shè)置weakmap
sourceProxy.set(target, observed);
toRaw.set(observed, target);
return observed;
}
接著我們要實現(xiàn)依賴收集(發(fā)布訂閱),還記得前面我們說的effect方法嗎?它默認(rèn)會執(zhí)行一次,當(dāng)依賴數(shù)據(jù)變化的時候會再次執(zhí)行,現(xiàn)在我們實現(xiàn)這個effect,這個也是響應(yīng)式的核心 ;首先我們這個effect方法接收一個函數(shù),并且我們要把這個函數(shù)變成響應(yīng)式函數(shù),并且讓這個函數(shù)默認(rèn)執(zhí)行一次:
function effect(fn) {
let effect = createReactiveFn(fn); // 創(chuàng)建響應(yīng)式的函數(shù)
effect(); // 默認(rèn)執(zhí)行一次
}
接著我們來編寫createReactiveFn方法 :
let stacks = [];
function createReactiveFn(fn) {
let eFn = function() {
// 執(zhí)行fn并且將fn存入棧中
return carryOut(eFn, fn)
}
return eFn;
}
function carryOut(effect, fn) {
// 執(zhí)行fn并且將fn存入棧中
stacks.push(effect);
fn();
}
在取值的時候,也就是get方法中,我們就要對值和effect做好映射,當(dāng)值發(fā)生變化的時候,我們直接執(zhí)行值對應(yīng)的effect,接下來我們先實現(xiàn)這部分創(chuàng)建關(guān)聯(lián)的方法, 最后我們要一種如下的數(shù)據(jù)結(jié)構(gòu):
{
target: {
key: [fn, fn]
}
}
我們編寫subscribeTo來創(chuàng)建關(guān)聯(lián):
let targetMap = new WeakMap();
function subscribeTo(target, key) {
let effect = stacks[stacks.length - 1];
if (effect) { //如果有,則創(chuàng)建關(guān)聯(lián)
let maps = targetMap.get(target);
if (!maps) {
targetMap.set(target, maps = new Map);
}
let deps = maps.get(key);
if (!deps) {
maps.set(key, deps = new Set());
}
if(!deps.has(effect)) {
deps.add(effect);
}
}
}
還記得之前寫的將effect入棧嗎stacks.push(effect); 當(dāng)我們做完關(guān)聯(lián)之后,其實這個effect在棧里就沒有意義了,所以我們將它清出棧:
function carryOut(effect, fn) {
// 執(zhí)行fn并且將fn存入棧中
stacks.push(effect);
fn();
stacks.pop(effect);
}
現(xiàn)在我們已經(jīng)創(chuàng)建關(guān)聯(lián)了,那在更新值的時候,也就是set方法中我們應(yīng)該去取targetMap中的effect執(zhí)行:
function trigger(target, type, key) {
let tMap = targetMap.get(target);
if (tMap) {
let keyMaps = tMap.get(key);
// 將key對應(yīng)的effect執(zhí)行
if (keyMaps) {
keyMaps.forEach((eff) => {
eff();
})
}
}
}
以下貼出本篇文章實現(xiàn)的完整代碼:
let sourceProxy = new WeakMap(); // 存放原對象和代理過的對象
let toRaw = new WeakMap(); // 存放被代理過的對象和原對象
function reactive(target) {
return createReactive(target);
}
function createReactive(target) {
// 創(chuàng)建響應(yīng)式對象
if(!(typeof target === 'object' && target !== null)) {
return target;
}
// 判斷target是否被代理過
if(sourceProxy.get(target)) {
return sourceProxy.get(target);
}
// 判斷代理過的里面是否有target, 防止一個對象被多次代理
if (toRaw.has(target)) {
return target;
}
// 創(chuàng)建觀察者
let baseHandle = {
get(target, key, receiver) {
// 獲取
let datas = Reflect.get(target, key, receiver);
// 訂閱
subscribeTo(target, key); // 當(dāng)key變化的時候重新執(zhí)行effect
// 變更了下面這行代碼
return typeof target === 'object' && target !== null ? reactive(datas) : datas;
},
set(target, key, value, receiver) {
// 設(shè)置
let oldValue = target[key];
let res = Reflect.set(target, key, value, receiver);
// 此處的判斷是屏蔽無意義的修改
if (!target.hasOwnProperty(key)) {
// 新增屬性
console.log('新增');
trigger(target, 'add', key);
} else if (oldValue !== value) {
// 修改屬性
console.log('修改');
trigger(target, 'set', key);
}
console.log(res);
return res;
}
}
let observed = new Proxy(target, baseHandle);
// 設(shè)置weakmap
sourceProxy.set(target, observed);
toRaw.set(observed, target);
return observed;
}
let stacks = [];
let targetMap = new WeakMap();
// 響應(yīng)式
function effect(fn) {
let effect = createReactiveFn(fn); // 創(chuàng)建響應(yīng)式的函數(shù)
effect(); // 默認(rèn)執(zhí)行一次
}
function createReactiveFn(fn) {
let eFn = function() {
// 執(zhí)行fn并且將fn存入棧中
return carryOut(eFn, fn)
}
return eFn;
}
function carryOut(effect, fn) {
// 執(zhí)行fn并且將fn存入棧中
try {
stacks.push(effect);
fn();
} finally {
stacks.pop(effect);
}
}
function subscribeTo(target, key) {
let effect = stacks[stacks.length - 1];
if (effect) { //如果有,則創(chuàng)建關(guān)聯(lián)
let maps = targetMap.get(target);
if (!maps) {
targetMap.set(target, maps = new Map);
}
let deps = maps.get(key);
if (!deps) {
maps.set(key, deps = new Set());
}
if(!deps.has(effect)) {
deps.add(effect);
}
}
}
function trigger(target, type, key) {
let tMap = targetMap.get(target);
if (tMap) {
let keyMaps = tMap.get(key);
// 將key對應(yīng)的effect執(zhí)行
if (keyMaps) {
keyMaps.forEach((eff) => {
eff();
})
}
}
}
// let proxy = reactive({name: 'yangbo'})
// proxy.name = 'yb';
// console.log(proxy.name);
let proxyObj = reactive({name: 'yangbo'});
effect(() => {
console.log(proxyObj.name);
})
proxyObj.name = 'yb'
以上簡單模仿了vue3.0中對響應(yīng)式數(shù)據(jù)的實現(xiàn),使用js實現(xiàn)的,感興趣可以看看Vue3.0源碼的TypeScript的寫法和實現(xiàn)方式,本篇文章僅供參考~