15 分鐘掌握 vue 3.0 響應式原理

寫在前面

最新 vue-next 的源碼發佈了,雖然是 pre-alpha 版本,但這時候實際上是閱讀源碼的比較好的時機。在 vue 中,比較重要的東西固然要數它的響應式系統,在以前的版本中,已經有若干篇文章對它的響應式原理和實現進行了介紹,這裏就不贅述了。在 vue-next 中,其實現原理和以前仍是相同的,即經過觀察者模式和數據劫持,只不過對其實現方式進行了改變。vue

對於解析原理的文章,我我的是比較喜歡那種「小白」風格的文章,即不要摘錄特別多的代碼,也不要闡述一些很深奧的原理與概念。在我剛接觸 react 的時候,還記得有一篇利用 jquery 來介紹 react 的文章,從簡入繁,面面俱到,其背後闡述的知識點對我後來學習 react 起到不少的幫助。react

所以,這篇文章我也打算按這種風格來寫一下利用最近空閒時間閱讀 vue-next 響應式模塊的源碼的一些心得與體會,算是拋磚引玉,同時實現一個極簡的響應式系統。jquery

若有錯誤,還望指正。git

預備知識

不管是閱讀這篇文章,仍是閱讀 vue-next 響應式模塊的源碼,首先有兩個知識點是必備的:es6

  • Proxy:es6 中新的代理內建工具類
  • Reflect:es6 中新的反射工具類

因爲篇幅有限,這裏也不詳細贅述這兩個類的用途與使用方法了,推薦三篇我認爲不錯的文章,僅供參考:github

接口

對於 vue-next 響應式系統的 RFC,能夠參考[這裏](github.com/vuejs/rfcs/…。雖然距離如今有一段時間了,可是經過閱讀源碼,能夠發現一些影子。api

咱們大致要實現的效果以下面的代碼所示:數組

// 實現兩個方法 reactive 和 effect

const state = reactive({
    count: 0
})

effect(() => {
    console.log('count: ', state.count)
})

state.count++ // 輸入 count: 1
複製代碼

能夠發現咱們熟悉的依賴收集階段(同時也是觀察者模式的訂閱過程),是在 effect 中進行的,依賴收集的準備工做(即數據劫持邏輯),是在 reactive 中進行的,而數據變化的觸發響應的邏輯在後面的 state.count++ 代碼執行時進行(同時也是觀察者模式的發佈過程),以後便會執行以前傳入 effect 內部的回調函數並輸入 count: 1緩存

類型與公共變量

因爲 vue-nextts 進行了重寫,這裏我也使用 ts 來實現這個極簡版本的響應式系統。主要涉及到的類型和公共變量以下:bash

type Effect = Function;
type EffectMap = Map<string, Effect[]>;

let currentEffect: Effect;
const effectMap: EffectMap = new Map();
複製代碼
  • currentEffect:用來儲存當前正在收集依賴的 effect
  • effectMap:表明目標對象每一個 key 所對應的依賴於它的 effect 數組,也能夠把它理解爲觀察者模式中的訂閱者字典

利用 Proxy 實現數據劫持

在以前的版本中,vue 利用 Object.defineProperty 中的 settergetter 來對數據對象進行劫持,vue-next 則經過 Proxy。衆所周知,Object.defineProperty 所實現的數據劫持是有必定限制的,而 Proxy 就會強大不少。

首先,咱們在腦後中,設想一下如何使用 Proxy 來實現數據劫持呢?很簡單,大致結構以下所示:

export function reactive(obj) {
  const proxied = new Proxy(obj, handlers);

  return proxied;
}
複製代碼

這裏的 handlers 是聲明如何處理各個 trap 的邏輯,好比:

const handlers = {
    get: function(target, key, receiver) {
        ...
    },
    set: function(target, key, value, receiver) {
        ...
    },
    deleteProperty(target, key) {
        ...
    }
    // ...以及其餘 trap
  }
複製代碼

因爲這裏是極簡版本的實現,那麼咱們就僅僅實現 getset 兩個 trap 就能夠了,分別對應依賴收集和觸發響應的邏輯。

依賴收集

對於依賴收集的實現,因爲是極簡版本,實現的前提以下:

  • 不考慮對象的嵌套
  • 不考慮集合類型
  • 不考慮基礎類型
  • 不考慮對代理對象的處理

哈哈,基本這四點排除以後,這個依賴收集函數就會很輕很薄,以下:

function(target, key: string, receiver) {
        // 僅僅在某個 effect 內部進行依賴收集
        if (currentEffect) {
          if (effectMap.has(key)) {
            const effects = effectMap.get(key);
            if (effects.indexOf(currentEffect) === -1) {
              effects.push(currentEffect);
            }
          } else {
            effectMap.set(key, [currentEffect]);
          }
        }

      return Reflect.get(target, key, receiver);
}
複製代碼

實現的邏輯很簡單,其實就是觀察者模式中註冊訂閱者的實現邏輯,值得注意的是,這裏對於 target 的賦值邏輯,咱們委託給 Reflect 來完成,雖然 target[key] 也是能夠工做的,可是使用 Reflect 是更提倡的方式。

觸發響應

觸發響應的邏輯就比較簡單了,實際上是對應觀察者模式中,發佈事件的邏輯,以下:

function(target, key: string, value, receiver) {
        const result = Reflect.set(target, key, value, receiver);
        
        if (effectMap.has(key)) {
          effectMap.get(key).forEach(effect => effect());
        }

        return result;
}
複製代碼

一樣,這裏使用 Reflect 來對 target 進行賦值操做,由於它會返回一個 boolean 值表明是否成功,而 set 這個 trap 也須要表明相同含義的值。

經過 reactive 方法來初始化代理對象

實現了數據劫持的代理邏輯以後,咱們只須要在 reactive 這個方法中,返回一個代理對象的實例便可,還記的上文中咱們在實現以前腦海中浮現的大體代碼框架嗎?

以下:

export function reactive(obj: any) {
  const proxied = new Proxy(obj, {
    get: function(target, key: string, receiver) {
      if (currentEffect) {
        if (effectMap.has(key)) {
          const effects = effectMap.get(key);
          if (effects.indexOf(currentEffect) === -1) {
            effects.push(currentEffect);
          }
        } else {
          effectMap.set(key, [currentEffect]);
        }
      }

      return Reflect.get(target, key, receiver);
    },
    set: function(target, key: string, value, receiver) {
      const result = Reflect.set(target, key, value, receiver);

      if (effectMap.has(key)) {
        effectMap.get(key).forEach(effect => effect());
      }

      return result;
    }
  });

  return proxied;
}
複製代碼

依賴收集的準備工做

上文中提到了,對於依賴收集的工做,咱們是有條件地進行的,即在一個 effect 中,咱們纔會進行收集,其餘狀況下的取值邏輯,咱們則不會進行依賴收集,所以,effect 方法正式爲了實現這點而存在的,以下:

export function effect(fn: Function) {
  const effected = function() {
    fn();
  };

  currentEffect = effected;
  effected();
  currentEffect = undefined;

  return effected;
}
複製代碼

之因此實現如此簡單,是由於咱們這裏是極簡版本,不須要考慮諸如 readOnly 、異常以及收集時機等因素。能夠發現,就是將傳入的回調函數包裹在另外一個方法中,而後將這個方法用 currentEffect 這個變量暫存,以後嘗試運行一下便可。當 effect 運行完畢以後,再將 currentEffect 置空,這樣就能夠達到只在 effect 下進行依賴收集的目的。

運行效果

我在 codepen 上簡單寫了一個計數器 demo,連接以下: codepen.io/littlelyon1…

寫在最後

這個極簡的響應式系統雖然能用,可是有不少未考慮的因素,其實就是在上文中被咱們忽略的那些前提條件,這裏再列舉一下,並給出源代碼中的解法:

  • 基礎數據類型的處理:能夠將基礎數據類型封裝爲一個 ref 對象,其 value 指向基礎數據類型的值
  • 嵌套對象:遞歸進行執行代理過程便可
  • 集合對象:編寫專門的 trap 處理邏輯
  • 代理實例:緩存這些代理實例,下次遇到直接返回便可

但我仍然推薦你直接去閱讀一下源碼,由於你會發現,源碼會在這個極簡版本基礎上,利用了更加複雜數據結構以及流程,來控制依賴收集和觸發響應的流程,同時各類特殊狀況也有更加明細的考慮。

另外,這僅僅是 vue-next 響應式系統的簡易實現,諸如其餘功能模塊,好比指令、模板解析、vdom 等,我也準備利用最近的空閒時間再去看看,有時間的話,最近也整理出來,分享給你們。


關注公衆號全棧_101,只談技術,不談人生。


長期兼職接各類規模的外包項目,有意者私聊。

相關文章
相關標籤/搜索