vue3 beta
版本已經發布快兩個月了,相信你們或多或少都有去了解一些vue3
的新特性,也有一部分人調侃學不動了,在我看來,技術確定是不斷更迭的,新的技術出現可以提升生產力,落後的技術確定是要被淘汰的,五年前會JQ一把梭就能找到一份還行的工做,如今只會JQ應該不多公司會要了吧。恰好前兩天尤大也發了一篇文章講述了vue3
的製做歷程,有興趣的同窗能夠點擊連接前往查看,文章是全英文的,英文不是很好的同窗能夠藉助翻譯插件閱讀。好了,廢話很少說,本篇的主題是手寫vue3的響應式功能。javascript
在寫代碼前,不妨來看看如何使用vue3吧,咱們能夠先去 github.com/vuejs/vue-n… clone一份代碼,使用npm install && npm run dev後,會生成一個packages -> vue -> dist -> vue.global.js文件,這樣咱們就可使用vue3了,在vue文件夾新建一個index.html文件。html
<!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
方法。vue
咱們知道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
操做進行了代理。java
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
屬性修改的變化,以下圖:react
因此咱們應該只在新增屬性的時候去觸發更新,咱們添加hasOwnProperty
判斷與老值和新值比較判斷,只有修改自身對象的屬性或者修改了自身屬性而且值不一樣的時候纔去更新視圖。git
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
,也就不會更新視圖了。以下圖:es6
那麼咱們應該怎麼進行深層次的代理呢?github
咱們觀察一下person.hair.push(4)
這個操做,當咱們去取person.hair
的時候,會去調用person
的get
方法,拿到屬性hair
的值,那麼咱們就能夠再它拿到值以後判斷是不是對象,再去進行深層次的監聽。npm
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
方法,想及時查看的話點個收藏或者關注。(^o^)/~