如何手寫Vue-next響應式呢?本文詳解

前言

1.本文將從零開始手寫一份vue-next中的響應式原理,出於篇幅和理解的難易程度,咱們將只實現核心的api並忽略一些邊界的功能點javascript

本文將實現的api包括前端

  • track
  • trigger
  • effect
  • reactive
  • watch
  • computed

    2.最近不少人私信我問前端問題,博客登錄的少沒及時回覆,爲此我建了個前端扣扣裙 519293536  你們之後有問題直接羣裏找我。都會盡力幫你們,博客私信我不多看

項目搭建

咱們採用最近較火的vite建立項目vue

本文演示的版本java

  • node v12.16.1
  • npm v6.14.5
  • yarn v1.22.4

咱們首先下載模板node

yarn create vite-app vue-next-reactivity
複製代碼

模板下載好後進入目錄react

cd vue-next-reactivity 複製代碼

而後安裝依賴git

yarn install
複製代碼

而後咱們僅保留src目錄下的main.js文件,清空其他文件並建立咱們要用到的reactivity文件夾es6

 

 

整個文件目錄如圖所示,輸入npm run dev項目便啓動了github

 

 

手寫代碼

響應式原理的本質

在開始手寫前,咱們思考一下什麼是響應式原理呢?web

咱們從vue-next的使用中來解釋一下

vue-next中用到的響應式大概分爲三個

  • template或render

在頁面中使用到的變量改變後,頁面自動進行了刷新

  • computed

當計算屬性函數中用到的變量發生改變後,計算屬性自動進行了改變

  • watch

當監聽的值發生改變後,自動觸發了對應的回調函數

以上三點咱們就能夠總結出響應式原理的本質

當一個值改變後會自動觸發對應的回調函數

這裏的回調函數就是template中的頁面刷新函數,computed中的從新計算屬性值的函數以及原本就是一個回調函數的watch回調

因此咱們要去實現響應式原理如今就拆分爲了兩個問題

  • 監聽值的改變
  • 觸發對應的回調函數

咱們解決了這兩個問題,便寫出了響應式原理

監聽值的改變

javascript中提供了兩個api能夠作到監聽值的改變

一個是vue2.x中用到的Object.defineProperety

const obj = {}; let aValue = 1; Object.defineProperty(obj, 'a', { enumerable: true, configurable: true, get() { console.log('我被讀取了'); return aValue; }, set(value) { console.log('我被設置了'); aValue = value; }, }); obj.a; // 我被讀取了 obj.a = 2; // 我被設置了 複製代碼

還有一個方法就是vue-next中用到的proxy,這也是本次手寫中會用到的方法

這個方法解決了Object.defineProperety的四個痛點

  1. 沒法攔截在對象上屬性的新增和刪除
  2. 沒法攔截在數組上調用push pop shift unshift等對當前數組會產生影響的方法
  3. 攔截數組索引過大的性能開銷
  4. 沒法攔截Set Map等集合類型

固然主要仍是前兩個

關於第三點,vue2.x中數組索引的改變也得經過this.$set去設置,致使不少同窗誤認爲Object.defineProperety也無法攔截數組索引,其實它是能夠的,vue2.x沒作的緣由估計就是由於性價比不高

以上4點proxy就能夠完美解決,如今讓咱們動手開始寫一個proxy攔截吧!

proxy攔截

咱們在以前建立好的reactivity目錄建立兩個文件

utils.js存放一些公用的方法

reactive.js 存放proxy攔截的方法

咱們先在utils.js中先添加將要用到的判斷是否爲原生對象的方法

reactivity/utils.js

// 獲取原始類型 export function toPlain(value) { return Object.prototype.toString.call(value).slice(8, -1); } // 是不是原生對象 export function isPlainObject(value) { return toPlain(value) === 'Object'; } 複製代碼

reactivity/reactive.js

import { isPlainObject } from './utils'; // 本列只有數組和對象才能被觀測 function canObserve(value) { return Array.isArray(value) || isPlainObject(value); } // 攔截數據 export function reactive(value) { // 不能監聽的數值直接返回 if (!canObserve(value)) { return; } const observe = new Proxy(value, { // 攔截讀取 get(target, key, receiver) { console.log(`${key}被讀取了`); return Reflect.get(target, key, receiver); }, // 攔截設置 set(target, key, newValue, receiver) { const res = Reflect.set(target, key, newValue, receiver); console.log(`${key}被設置了`); return res; }, }); // 返回被觀察的proxy實例 return observe; } 複製代碼

reactivity/index.js

導出方法

export * from './reactive'; 複製代碼

main.js

import { reactive } from './reactivity'; const test = reactive({ a: 1, }); const testArr = reactive([1, 2, 3]); // 1 test.a; // a被讀取了 test.a = 2; // a被設置了 // 2 test.b; // b被讀取了 // 3 testArr[0]; // 0被讀取了 // 4 testArr.pop(); // pop被讀取了 length被讀取了 2被讀取了 length被設置了 複製代碼

能夠看到咱們添加了一個reactive方法用於將對象和數組進行proxy攔截,並返回了對應的proxy實例

列子中的1 2 3都很好理解,咱們來解釋下第4個

咱們調用pop方法首先會觸發get攔截,打印pop被讀取了

而後調用pop方法後會讀取數組的長度觸發get攔截,打印length被讀取了

pop方法的返回值是當前刪除的值,會讀取數組索引爲2的值觸發get攔截,打印2被讀取了

pop後數組長度會被改變,會觸發set攔截,打印length被設置了

你們也能夠試試其餘改變數組的方法

能夠概括爲一句話

對數組的自己有長度影響的時候length會被讀取和從新設置,對應改變的值的索引也會被讀取或從新設置(push unshift)

添加回調函數

咱們經過了proxy實現了對值的攔截解決了咱們提出的第一個問題

但咱們並無在值的改變後觸發回調函數,如今讓咱們來補充回調函數

reactivity/reactive.js

import { isPlainObject } from './utils';

// 本列只有數組和對象才能被觀測
function canObserve(value) {
  return Array.isArray(value) || isPlainObject(value);
}

+ // 假設的回調函數 + function notice(key) { + console.log(`${key}被改變了並觸發了回調函數`); + } // 攔截數據 export function reactive(value) { // 不能監聽的數值直接返回 if (!canObserve(value)) { return; } const observe = new Proxy(value, { // 攔截讀取 get(target, key, receiver) { - console.log(`${key}被讀取了`); return Reflect.get(target, key, receiver); }, // 攔截設置 set(target, key, newValue, receiver) { const res = Reflect.set(target, key, newValue, receiver); - console.log(`${key}被設置了`); + // 觸發假設的回調函數 + notice(key); return res; }, }); // 返回被觀察的proxy實例 return observe; } 複製代碼

我麼以最直觀的方法在值被改變的set攔截中觸發了咱們假設的回調

main.js

import { reactive } from './reactivity'; const test = reactive({ a: 1, b: 2, }); test.a = 2; // a被改變了並觸發了回調函數 test.b = 3; // b被改變了並觸發了回調函數 複製代碼

能夠看到當值改變的時候,輸出了對應的日誌

但這個列子確定是有問題的,問題還不止一處,讓咱們一步一步來升級它

回調函數的收集

上面的列子中ab都對應了一個回調函數notice,可實際的場景中,ab可能對應分別不一樣的回調函數,若是咱們單單用一個簡單的全局變量存儲回調函數,很明顯這是不合適的,若是有後者則會覆蓋前者,那麼怎麼才能讓回調函數和各個值之間對應呢?

很容易想到的就是js中的key-value的對象,屬性ab分別做爲對象的key值則能夠區分各自的value

 

 

但用對象收集回調函數是有問題的

上列中咱們有一個test對象,它的屬性有ab,當咱們存在另一個對象test1它要是也有ab屬性,那不是重複了嗎,這又會觸發咱們以前說到的重複的問題

 

 

有同窗可能會說,那再包一層用testtest1做爲屬性名不就行了,這種方法也是不可行的,在同一個執行上下文中不會出現兩個相同的變量名,但不一樣執行上下文能夠,這又致使了上面說到的重複的問題

處理這個問題要用到js對象按引用傳遞的特色

// 1.js const obj = { a: 1, }; // 2.js const obj = { a: 1, }; 複製代碼

咱們在兩個文件夾定義了名字屬性數據結構徹底同樣的對象obj,但咱們知道這兩個obj並非相等的,由於它們的內存指向不一樣地址

因此若是咱們能直接把對象做爲key值,那麼是否是就能夠區分看似"相同"的對象了呢?

答案確定是能夠的,不過咱們得換種數據結構,由於js中對象的key值是不能爲一個對象的

這裏咱們就要用到es6新增的一種數據結構MapWeakMap

咱們經過舉例來講明這種數據結構的存儲模式

假設如今咱們有兩個數據結構「相同」的對象obj,它們都有各自的屬性ab,各個屬性的改變會觸發不一樣的回調函數

// 1.js const obj = { a: 1, b: 2 }; // 2.js const obj = { a: 1, b: 2 }; 複製代碼

MapWeakMap來存儲就以下圖所示

咱們將存儲回調函數的全局變量targetMap定義爲一個WeakMap,它的key值是各個對象,在本列中就是兩個obj,targetMapvalue值是一個Map,本列中兩個obj分別擁有兩個屬性ab,Mapkey就是屬性ab,Mapvalue就是屬性ab分別對應的Set回調函數集合

 

 

可能你們會有疑問爲何targetMapWeakMap而各個對象的屬性存儲用的Map,這是由於WeakMap只能以對象做爲key,Map是對象或字符串均可以,像上面的列子屬性ab只能用Map

咱們再以實際api來加深對這種存儲結構的理解

  • computed
const c = computed(() => test.a) 複製代碼

這裏咱們須要將() => test.a回調函數放在test.a的集合中,如圖所示

 

 

  • watch
watch(() => test.a, val => { console.log(val) }) 複製代碼

這裏咱們須要將val => { console.log(val) }回調函數放在test.a的集合中,如圖所示

 

 

  • template
createApp({
  setup() {
    return () => h('div', test.a); }, }); 複製代碼

這裏咱們須要將dom刷新的函數放在test.a中,如圖所示

 

 

上面咱們已經知道了存儲回調函數的方式,如今咱們來思考如何將回調函數放到咱們定義好的存儲結構中

仍是拿上面的列子舉列

watch(() => test.a, val => { console.log(val) }) 複製代碼

這個列子中,咱們須要將回調函數val => { console.log(val) })放到test.aSet集合中,因此咱們須要拿到對象test和當前對象的屬性a,若是僅經過() => test.a,咱們只能拿到test.a的值,沒法得知具體的對象和屬性

但其實這裏讀取了test.a的值,就變相的拿到了具體的對象和屬性

你們還記得咱們在前面用proxy攔截了test.a的讀取嗎,get攔截的第一個參數就是當前讀取的對象,第二個參數就是當前讀取的屬性

因此回調函數的收集是在proxyget攔截中處理

如今讓咱們用代碼實現剛剛理好的思路

首先咱們建立effect.js文件,這個文件用於存放回調函數的收集方法和回調函數的觸發方法

reactivity/effect.js

// 回調函數集合 const targetMap = new WeakMap(); // 收集回調函數 export function track(target, key) { } // 觸發回調函數 export function trigger(target, key) { } 複製代碼

而後改寫proxy中的攔截內容

reactivity/reactive.js

import { isPlainObject } from './utils';
+ import { track, trigger } from './effect'; // 本列只有數組和對象才能被觀測 function canObserve(value) { return Array.isArray(value) || isPlainObject(value); } - // 假設的回調函數 - function notice(key) { - console.log(`${key}被改變了並觸發了回調函數`); - } // 攔截數據 export function reactive(value) { // 不能監聽的數值直接返回 if (!canObserve(value)) { return; } const observe = new Proxy(value, { // 攔截讀取 get(target, key, receiver) { + // 收集回調函數 + track(target, key); return Reflect.get(target, key, receiver); }, // 攔截設置 set(target, key, newValue, receiver) { const res = Reflect.set(target, key, newValue, receiver); + // 觸發回調函數 + trigger(target, key); - // 觸發假設的回調函數 - notice(key); return res; }, }); // 返回被觀察的proxy實例 return observe; } 複製代碼

這裏還沒補充effect中的內容是讓你們能夠清晰的看見收集和觸發的位置

如今咱們來補充track收集回調函數和trigger觸發回調函數

reactivity/effect.js

// 回調函數集合 const targetMap = new WeakMap(); // 收集回調函數 export function track(target, key) { // 經過對象獲取每一個對象的map let depsMap = targetMap.get(target); if (!depsMap) { // 當對象被第一次收集時 咱們須要添加一個map集合 targetMap.set(target, (depsMap = new Map())); } // 獲取對象下各個屬性的回調函數集合 let dep = depsMap.get(key); if (!dep) { // 當對象屬性第一次收集時 咱們須要添加一個set集合 depsMap.set(key, (dep = new Set())); } // 這裏添加回調函數 dep.add(() => console.log('我是一個回調函數')); } // 觸發回調函數 export function trigger(target, key) { // 獲取對象的map const depsMap = targetMap.get(target); if (depsMap) { // 獲取對應各個屬性的回調函數集合 const deps = depsMap.get(key); if (deps) { // 觸發回調函數 deps.forEach((v) => v()); } } } 複製代碼

而後運行咱們的demo

main.js

import { reactive } from './reactivity'; const test = reactive({ a: 1, b: 2, }); test.b; // 讀取收集回調函數 setTimeout(() => { test.a = 2; // 沒有任何觸發 由於沒收集回調函數 test.b = 3; // 我是一個回調函數 }, 1000); 複製代碼

咱們來看看此時的targetMap結構

 

 

targetMap中存在key{ a: 1,b: 2 },它的value值也是一個Map,這個Map中存在keyb,這個Mapvalue即是回調函數的集合Set,如今就只有一個咱們寫死的() => console.log('我是一個回調函數')

用圖形結構就是這樣

 

 

你們可能以爲要收集回調函數要讀取一次test.b是反人類的操做,這是由於咱們尚未講到對應的api,日常讀取的操做不須要這麼手動式的調用,api會本身處理

watch

上面的列子存在一個很大的問題,就是咱們沒有自定義回調函數,回調函數在代碼中直接被寫死了

如今咱們將經過watch去實現自定義的回調函數

watchvue-next中的api還蠻多的,咱們將實現其中一部分類型,這足以讓咱們理解響應式原理

咱們將實現的demo以下

export function watch(fn, cb, options) {} const test = reactive({ a: 1, }); watch( () => test.a, (val) => { console.log(val); } ); 複製代碼

watch接受三個參數

第一個參數是一個函數,表達被監聽的值

第二個參數是一個函數,表達監聽值改變後要觸發的回調,第一個參數是改變後的值,第二個參數是改變前的值

第三個參數是一個對象,只有一個deep屬性,deep表深度觀察

如今咱們須要作的就是把回調函數(val) => { console.log(val); }放到test.aSet集合中

因此在() => test.a執行讀取test.a前,咱們須要將回調函數用一個變量存儲

當讀取test.a觸發track函數的時候,能夠在track函數中獲取到這個變量,並將它存儲到對應屬性的集合Set

reactivity/effect.js

// 回調函數集合
const targetMap = new WeakMap();

+ // 當前激活的回調函數 + export let activeEffect; + // 設置當前回調函數 + export function setActiveEffect(effect) { + activeEffect = effect; + } // 收集回調函數 export function track(target, key) { // 沒有激活的回調函數 直接退出不收集 if (!activeEffect) { return; } // 經過對象獲取每一個對象的map let depsMap = targetMap.get(target); if (!depsMap) { // 當對象被第一次收集時 咱們須要添加一個map集合 targetMap.set(target, (depsMap = new Map())); } // 獲取對象下各個屬性的回調函數集合 let dep = depsMap.get(key); if (!dep) { // 當對象屬性第一次收集時 咱們須要添加一個set集合 depsMap.set(key, (dep = new Set())); } // 這裏添加回調函數 - dep.add(() => console.log('我是一個回調函數')); + dep.add(activeEffect); } // 觸發回調函數 export function trigger(target, key) { // 省略 } 複製代碼

由於watch方法和tracktrigger方法不在同一個文件,因此咱們用export導出變量activeEffect,並提供了一個方法setActiveEffect修改它

這也是一個不一樣模塊下使用公共變量的方法

如今讓咱們建立watch.js,並添加watch方法

reactivity/watch.js

import { setActiveEffect } from './effect'; export function watch(fn, cb, options = {}) { let oldValue; // 在執行fn獲取oldValue前先存儲回調函數 setActiveEffect(() => { // 確保回調函數觸發 獲取到的是新值 let newValue = fn(); // 觸發回調函數 cb(newValue, oldValue); // 新值賦值給舊值 oldValue = newValue; }); // 讀取值並收集回調函數 oldValue = fn(); // 置空回調函數 setActiveEffect(''); } 複製代碼

很簡單的幾行代碼,在執行fn讀取值前把回調函數經過setActiveEffect設置以便在讀取的時候track函數中能夠拿到當前的回調函數activeEffect,讀取完後再製空回調函數,就完成了

一樣咱們須要導出watch方法

reactivity/index.js

export * from './reactive';
+ export * from './watch'; 複製代碼

main.js

import { reactive, watch } from './reactivity'; const test1 = reactive({ a: 1, }); watch( () => test1.a, (val) => { console.log(val) // 2; } ); test1.a = 2; 複製代碼

能夠看到列子正常執行打印出了2,咱們來看看targetMap的結構

targetMap存在一個key{a:1},它的value值也是一個Map,這個Map中存在keya,這個Map的value即是回調函數(val) => { console.log(val); }

 

 

targetMap的圖形結構以下

 

 

computed

watch的其餘api補充咱們將放到後面,在感覺到響應式原理的思惟後,咱們趁熱打鐵再來實現computed的功能

一樣的computed這個apivue-next中也有多種寫法,咱們將只實現函數返回值的寫法

export function computed(fn) {} const test = reactive({ a: 1, }); const w = computed(() => test.a + 1); 複製代碼

但若是咱們僅實現computed傳入函數的寫法,其實在vue-next中和響應式原理沒多大關係

由於vue-next中提供的api讀取值不是直接讀取的w而是w.value

咱們建立computed.js,補充computed函數

reactivity/computed.js

export function computed(fn) { return { get value() { return fn(); }, }; } 複製代碼

能夠看到就幾行代碼,每次讀取value從新運行一次fn求值就好了

reactivity/index.js

咱們再導出它

export * from './reactive';
export * from './watch';
+ export * from './computed'; 複製代碼

main.js

import { reactive, computed } from './reactivity'; const test = reactive({ a: 1, }); const w = computed(() => test.a + 1); console.log(w.value); // 2 test.a = 2; console.log(w.value); // 3 複製代碼

能夠看到列子完美運行

這裏帶來了兩個問題

  • 爲何api的寫法不是直接讀取w而是w.value的形式

這個和爲啥有ref是一個道理,proxy沒法攔截基礎類型,因此要加一層value包裝成對象

  • vue-next中的computed真的和響應式原理不要緊了嗎

其實有關係,在僅實現computed傳入函數的寫法中,響應式原理啓優化做用

能夠看到若是按咱們以前的寫法,即使w.value的值沒有變化,咱們讀取的時候也會去執行一次fn,當數據量多起來的時候,對性能的影響就大了

那咱們怎麼優化呢?

容易想到的就是執行一次fn對比新老值,但這和以前其實就同樣了,由於咱們仍然執行了一次fn

這裏咱們就能夠運用響應式原理,只要內部的影響值test.a被修改了,咱們就從新執行fn獲取一次值,否則就讀取以前的存儲的值

reactivity/computed.js

import { setActiveEffect } from './effect'; export function computed(fn) { // 變量被改變後此值纔會爲true 第一次進來時候爲true let dirty = true; // 返回值 let value; // 設置爲true表達下次讀取須要從新獲取 function changeDirty() { dirty = true; } return { get value() { // 當標誌爲true表明變量須要更改 if (dirty) { dirty = false; // 將變量控制設置爲 setActiveEffect(changeDirty); // 獲取值 value = fn(); // 制空依賴 setActiveEffect(''); } return value; }, }; } 複製代碼

咱們定義了一個變量dirty用於表達這個值是否被修改過,修改過就爲true

一樣的,咱們再每次讀取值以前,將回調函數() => { dirty = true }賦值給中間變量activeEffect,而後再執行fn讀取,此時回調被收集,當對應的屬性更改的時候,dirty也就更改了

咱們再運行上面的列子,程序仍然正常運行了

咱們來看看targetMap的結構,targetMap存在一個key{a:1},它的value值也是一個Map,這個Map中存在keya,這個Map的value即是回調函數function changeDirty() { dirty = true; }

 

 

targetMap的圖形結構以下

 

 

提取effect

watchcomputed中咱們都經歷過 設置回調函數=>讀取值(存儲回調函數)=>清空回調函數 這三個步驟

vue-next的源碼中這個步驟被提取爲了一個公用函數,爲了符合vue-next的設計咱們將這個步驟提取出來,取名effect

函數的第一個參數是一個函數,函數執行後,會觸發函數中各個變量的讀取,並收集對應的回調函數

函數的第二個參數是一個對象

有一個schedular屬性,表達特殊指定的回調函數,若是沒有這個屬性,回調函數就是第一個參數

有一個lazy屬性,爲true時表明第一個參數傳入的函數不用當即執行,默認爲false,即當即指定第一個參數傳入的函數

reactivity/effect.js

// 回調函數集合
const targetMap = new WeakMap();

// 當前激活的回調函數
export let activeEffect;

- // 設置當前回調函數 - export function setActiveEffect(effect) { - activeEffect = effect; - } + // 設置當前回調函數 + export function effect(fn, options = {}) { + const effectFn = () => { + // 設置當前激活的回調函數 + activeEffect = effectFn; + // 執行fn收集回調函數 + let val = fn(); + // 制空回調函數 + activeEffect = ''; + return val; + }; + // options配置 + effectFn.options = options; + // 默認第一次執行函數 + if (!options.lazy) { + effectFn(); + } + return effectFn; + } // 收集回調函數 export function track(target, key) { // 省略 } // 觸發回調函數 export function trigger(target, key) { // 獲取對象的map const depsMap = targetMap.get(target); if (depsMap) { // 獲取對應各個屬性的回調函數集合 const deps = depsMap.get(key); if (deps) { // 觸發回調函數 - deps.forEach((v) => v()); + deps.forEach((v) => { + // 特殊指定回調函數存放在了schedular中 + if (v.options.schedular) { + v.options.schedular(); + } + // 當沒有特地指定回調函數則直接觸發 + else if (v) { + v(); + } + }); } } } 複製代碼

reactivity/index.js

導出effect

export * from './reactive';
export * from './watch';
export * from './computed';
+ export * from './effect'; 複製代碼

main.js

import { reactive, effect } from './reactivity'; const test = reactive({ a: 1, }); effect(() => { document.title = test.a; }); setTimeout(() => { test.a = 2; }, 1000); 複製代碼

effect第一次自執行,將() => { document.title = test.a; }這個回調函數放入了test.a中,當test.a改變,觸發對應回調函數

targetMap如圖所示

 

 

圖形結構如圖所示

 

 

一樣咱們更改computedwatch中的寫法,用effect替代

reactivity/computed.js

import { effect } from './effect'; export function computed(fn) { // 變量被改變後此值纔會爲true 第一次進來時候爲true let dirty = true; let value; const runner = effect(fn, { schedular: () => { dirty = true; }, // 第一次不用執行 lazy: true, }); // 返回值 return { get value() { // 當標誌爲true表明變量須要更改 if (dirty) { value = runner(); // 制空依賴 dirty = false; } return value; }, }; } 複製代碼

reactivity/watch.js

import { effect } from './effect'; export function watch(fn, cb, options = {}) { let oldValue; const runner = effect(fn, { schedular: () => { // 當這個依賴執行的時候 獲取到的是新值 let newValue = fn(); // 觸發回調函數 cb(newValue, oldValue); // 新值賦值給舊值 oldValue = newValue; }, // 第一次不用執行 lazy: true, }); // 讀取值並收集依賴 oldValue = runner(); } 複製代碼

main.js

import { reactive, watch, computed } from './reactivity'; const test = reactive({ a: 1, }); const w = computed(() => test.a + 1); watch( () => test.a, (val) => { console.log(val); // 2 } ); console.log(w.value); // 2 test.a = 2; console.log(w.value); // 3 複製代碼

能夠看到代碼正常執行,targetMap如圖所示,屬性a中存放了兩個回調函數

 

 

targetMap圖形結構如圖所示

 

 

補充watch的options

咱們來看看這個列子

import { watch, reactive } from './reactivity'; const test = reactive({ a: { b: 1, }, }); watch( () => test.a, (val) => { console.log(val); // 沒有觸發 } ); test.a.b = 2; 複製代碼

咱們用watch觀察了test.a,當咱們去改變test.a.b的時候,觀察的回調並無觸發,用過vue的同窗都會知道,這種狀況應該用deep屬性就能夠解決

那麼deep是如何實現的呢

咱們再來回憶一下回調函數收集的過程

test.a被讀取時,回調函數被收集進了test.a中,但這裏test.a.b並無被讀取,因此回調函數天然就沒有被收集進test.a.b

因此咱們只用在回調函數收集的時候,深度遍歷一下test,去讀取一下各個屬性便可

這裏還須要注意一點,咱們用reactive攔截對象的時候,是不會攔截對象的第二層的

const test = { a: { b: 1, }, }; const observe = new Proxy(test, { get(target, key, receiver) { return Reflect.set(target, key, receiver); }, }); test.a // 觸發攔截 test.a.b // 不會觸發攔截 複製代碼

因此咱們須要遞歸的將攔截值用proxy代理

reactivity/reactive.js

const observe = new Proxy(value, {
  // 攔截讀取
  get(target, key, receiver) {
    // 收集回調函數
    track(target, key);
+ const res = Reflect.get(target, key, receiver); + return canObserve(res) ? reactive(res) : res; - return Reflect.get(target, key, receiver); }, // 攔截設置 set(target, key, newValue, receiver) { const res = Reflect.set(target, key, newValue, receiver); // 觸發回調函數 trigger(target, key); return res; }, }); 複製代碼

reactivity/watch.js

import { effect } from './effect';
+ import { isPlainObject } from './utils'; + // 深度遍歷值 + function traverse(value) { + if (isPlainObject(value)) { + for (const key in value) { + traverse(value[key]); + } + } + return value + } export function watch(fn, cb, options = {}) { + let oldValue; + let getters = fn; + // 當存在deep屬性的時候 深度遍歷值 + if (options.deep) { + getters = () => traverse(fn()); + } + const runner = effect(getters, { - const runner = effect(fn, { schedular: () => { // 當這個依賴執行的時候 獲取到的是新值 let newValue = runner(); // 觸發回調函數 cb(newValue, oldValue); // 新值賦值給舊值 oldValue = newValue; }, // 第一次不用執行 lazy: true, }); // 讀取值並收集回調函數 oldValue = runner(); } 複製代碼

main.js

import { watch, reactive } from './reactivity'; const test = reactive({ a: { b: 1, }, }); watch( () => test.a, (val) => { console.log(val); // { b: 2 } }, { deep: true, } ); test.a.b = 2; 複製代碼

targetMap以下,咱們不只在對象{ a: { b: 1 } }上添加了回到函數,也在{ b: 1 }上添加了

 

 

targetMap圖形結構如圖所示

 

 

能夠看到加入deep屬性後即可深度觀察數據了,上面的列子中咱們都是用的對象,其實深度觀察對數組也是須要的,不過數組的處理有一點不一樣咱們來看看不一樣點

數組的處理

import { watch, reactive } from './reactivity'; const test = reactive([1, 2, 3]); watch( () => test, (val) => { console.log(val); // 沒有觸發 } ); test[0] = 2; 複製代碼

上面的列子是不會觸發的,由於咱們只讀取了test,targetMap裏面啥也沒有

 

 

因此在數組的狀況下,咱們也屬於deep深度觀察範疇,深度遍歷的時候,須要讀取數組的每一項

reactivity/watch.js

// 深度遍歷值
function traverse(value) {
  // 處理對象
  if (isPlainObject(value)) {
    for (const key in value) {
      traverse(value[key]);
    }
  }
+ // 處理數組 + else if (Array.isArray(value)) { + for (let i = 0; i < value.length; i++) { + traverse(value[i]); + } + } return value; } 複製代碼

main.js

import { watch, reactive } from './reactivity'; const test = reactive([1, 2, 3]); watch( () => test, (val) => { console.log(val); // [2, 2, 3] }, { deep: true } ); test[0] = 2; 複製代碼

在上面的列子中添加deeptrue能夠看見回調觸發了

targetMap如圖所示

 

 

第一項Set是一個Symbol(Symbol.toStringTag),咱們不用管它

咱們將數組的每一項都進行了回調函數的儲存,且也在數組的length屬性上也進行了存儲

咱們再來看一個列子

import { watch, reactive } from './reactivity'; const test = reactive([1, 2, 3]); watch( () => test, (val) => { console.log(val); // 沒有觸發 }, { deep: true, } ); test[3] = 4; 複製代碼

上面的列子不會觸發,細心的同窗可能記得,咱們targetMap裏面只收集了索引爲0 1 2的三個位置,新增的索引爲3的並無收集

 

 

咱們應該如何處理這種臨界的狀況呢?

你們還記得咱們最初講到的在proxy下數組pop方法的解析嗎,當時咱們概括爲了一句話

對數組的自己有長度影響的時候length會被讀取和從新設置

如今咱們經過索引新增值其實也是改變了數組自己的長度,因此length會被從新設置,如今就有方法了,咱們在新增索引上找不到回調函數的時候,咱們能夠去讀取數組length上存儲的回調函數

reactivity/reactive.js

const observe = new Proxy(value, {
  // 攔截讀取
  get(target, key, receiver) {
    // 收集回調函數
    track(target, key);
    const res = Reflect.get(target, key, receiver);
    return canObserve(res) ? reactive(res) : res;
  },
  // 攔截設置
  set(target, key, newValue, receiver) {
+ const hasOwn = target.hasOwnProperty(key); + const oldValue = Reflect.get(target, key, receiver); const res = Reflect.set(target, key, newValue, receiver); + if (hasOwn) { + // 設置以前的屬性 + trigger(target, key, 'set'); + } else if (oldValue !== newValue) { + // 添加新的屬性 + trigger(target, key, 'add'); + } - // 觸發回調函數 - trigger(target, key); return res; }, }); 複製代碼

咱們用hasOwnProperty判斷當前屬性是否在對象上,對於數組的新增索引很明顯是不在的,此時會走到trigger(target, key, 'add');這個函數

reactivity/effect.js

// 觸發回調函數
export function trigger(target, key, type) {
  // 獲取對象的map
  const depsMap = targetMap.get(target);
  if (depsMap) {
    // 獲取對應各個屬性的回調函數集合
- const deps = depsMap.get(key); + let deps = depsMap.get(key); + // 當數組新增屬性的時候 直接獲取length上存儲的回調函數 + if (type === 'add' && Array.isArray(target)) { + deps = depsMap.get('length'); + } if (deps) { // 觸發回調函數 deps.forEach((v) => { // 特殊指定回調函數存放在了schedular中 if (v.options.schedular) { v.options.schedular(); } // 當沒有特地指定回調函數則直接觸發 else if (v) { v(); } }); } } } 複製代碼

而後咱們處理typeadd的狀況,當typeadd且對象爲數組的時候,咱們便去讀取length上存儲的回調函數

能夠看到這麼一改寫,列子就能夠正常運行了

總結

1.其實讀完本文後,你會發現本文不是一篇vue源碼解剖,咱們全程沒有貼出vue-next中對應的源碼,由於我以爲從零開始的思路去思考如何實現會比從源碼解讀去思考爲何這麼實現會好點

2.固然本文也只實現了簡易的響應式原理,若是你想查看完整的代碼能夠點擊這裏,雖然不少功能點也沒實現,但大致思路都是一致的,若是你能讀懂本問講解的思路,你確定能看懂vue-next中對應的源碼
3.最近不少人私信我問前端問題,博客登錄的少沒及時回覆,爲此我建了個前端扣扣裙 519293536  你們之後有問題直接羣裏找我。都會盡力幫你們,博客私信我不多看

本文的文字及圖片來源於網絡加上本身的想法,僅供學習、交流使用,不具備任何商業用途,版權歸原做者全部,若有問題請及時聯繫咱們以做處理

相關文章
相關標籤/搜索