1. 程式人生 > 實用技巧 >解析Vue2.0和3.0的響應式原理和異同(帶原始碼)

解析Vue2.0和3.0的響應式原理和異同(帶原始碼)

前言

2019.10.5日釋出了Vue3.0,現在2020年了,估計Vue3.0正式版也快出來了。

2.0跟3.0的變化也挺大的,

  • 結構: 2.0用Flex,3.0用TypeScript。
  • 效能: 3.0優化了Virtual Dom的演算法。
  • 響應式原理:2.0用Object.defineProperty,3.0用Proxy
  • ...

Vue2.0和Vue3.0實現原理

  1. Vue 2.0

    Vue2.0實現MVVM(雙向資料繫結)的原理是通過Object.defineProperty來劫持各個屬性的setter、getter,在資料變動時釋出訊息給訂閱者,觸發相應的監聽回撥。

    Vue官網也給出瞭解釋:

  2. Vue 3.0實現響應式基於ES6:
    Proxy

Vue2.0和Vue3.0的差異如下:

Vue2.0

  • 基於Object.defineProperty,不具備監聽陣列的能力,需要重新定義陣列的原型來達到響應式。
  • Object.defineProperty無法檢測到物件屬性的新增和刪除 。
  • 由於Vue會在初始化例項時對屬性執行getter/setter轉化,所有屬性必須在data物件上存在才能讓Vue將它轉換為響應式。
  • 深度監聽需要一次性遞迴,對效能影響比較大。

Vue3.0

  • 基於Proxy和Reflect,<可以原生監聽陣列,可以監聽物件屬性的新增和刪除。
  • 不需要一次性遍歷data的屬性,可以顯著提高效能。
  • 因為Proxy是ES6新增的屬性,有些瀏覽器還不支援,只能相容到IE11 。

Vue2.x實現響應式

下面是基於Object.defineProperty,一步步實現簡單版Vue2.0。

  1. 由於Object.defineProperty無法監聽陣列,所以陣列型別實現響應式,需要處理。

判斷如果是陣列型別,就重寫陣列的原型方法('push','pop','shift',unshift)

    // 重新定義陣列原型,Object.defineProperty不具備監聽陣列的方法
    const oldArrayProperty = Array.prototype;
        const arrProto = Object.create(oldArrayProperty);
        ["push","pop","shift","unshift","splice"].forEach(
            methodName => 
            (arrProto[methodName] = function() {
                updateView();
                oldArrayProperty[methodName].call(this, ...arguments);
            })
        )
  1. 將傳入的data屬性進行深度監聽,判斷是物件還是陣列。
    function observer(target){
        if(typeof target !== 'object' || target === null){
            return target
        }
    
        // 如果是陣列型別,重寫陣列原型的方法("push","pop","shift","unshift","splice")
        if(Array.isArray(target)){
            target.__proto__ == arrProto;
        }
    
        // 如果是物件,遍歷物件所有的屬性,並使用Object.defineProperty把這些屬性全部轉為getter/setter
        for(let key in target){
            defineReactive(target,key,target[key])
        }
    }
  1. 核心API Object.defineProperty,將傳入屬性轉為getter/setter

      function defineReactive(target, key, value){
          // 如果物件有更多的層級,再次呼叫observer監聽方法,實現深層次的監聽。
          observer(value);
      
          Object.defineProperty(target, key, {
              get(){
                  return value;
              },
              set(newValue){
                  // 設定值的時候也需要深度監聽
                  observer(value);
      
                  if(newValue !== value){
                      value = newValue;
      
                      // 資料驅動檢視,如果資料改變,就呼叫檢視更新的方法。對應到Vue中是執行VDOM
                      updateView();
                  }
              }
          })
      }
      
  2. 資料更新會觸發檢視更新,這是MVVM的繫結原理,這就會涉及到Vue的 template 編譯為 render 函式,在執行 Virtual Dom, Diff演算法, Vnode等 這些東西了。

      function updateView(){
          console.log('檢視更新')
      }
    

5.使用


const data = {
  name: "zhangsan",
  age: 20,
  info: {
    address: "北京" // 需要深度監聽
  },
  nums: [10, 20, 30]
};

observer(data);

Vue3.0實現響應式

Vue3.0基於Proxy來做資料大劫持代理,可以原生支援到陣列的響應式,不需要重寫陣列的原型,還可以直接支援新增和刪除屬性, 比Vue2.x的Object.defineProperty更加的清晰明瞭。

  1. 核心程式碼(非常少)

    const proxyData = new Proxy(data, {
      get(target,key,receive){ 
        // 只處理本身(非原型)的屬性
        const ownKeys = Reflect.ownKeys(target)
        if(ownKeys.includes(key)){
          console.log('get',key) // 監聽
        }
        const result = Reflect.get(target,key,receive)
        return result
      },
      set(target, key, val, reveive){
        // 重複的資料,不處理
        const oldVal = target[key]
        if(val == oldVal){
          return true
        }
        const result = Reflect.set(target, key, val,reveive)
        return result
      },
      // 刪除屬性
      deleteProperty(target, key){
        const result = Reflect.deleteProperty(target,key)
        return result
      }
    })
    
  2. 使用

    const data = {
      name: "zhangsan",
      age: 20,
      info: {
        address: "北京" // 需要深度監聽
      },
      nums: [10, 20, 30]
    };
    

    直接這樣就可以了,也不需要宣告,Proxy直接會代理監聽data的內容,非常的簡單方便,唯一的不足就是部分瀏覽器無法相容Proxy,也不能hack,所以目前只能相容到IE11。

全部原始碼

可直接將程式碼複製到chrome瀏覽器的控制檯,直接除錯列印。
  1. Vue2.0

    function defineReactive(target, key, value) {
      //深度監聽
      observer(value);
    
      Object.defineProperty(target, key, {
        get() {
          return value;
        },
        set(newValue) {
          //深度監聽
          observer(value);
          if (newValue !== value) {
            value = newValue;
    
            updateView();
          }
        }
      });
    }
    
    function observer(target) {
      if (typeof target !== "object" || target === null) {
        return target;
      }
    
      if (Array.isArray(target)) {
        target.__proto__ = arrProto;
      }
    
      for (let key in target) {
        defineReactive(target, key, target[key]);
      }
    }
    
    // 重新定義陣列原型
    const oldAddrayProperty = Array.prototype;
    const arrProto = Object.create(oldAddrayProperty);
    ["push", "pop", "shift", "unshift", "spluce"].forEach(
      methodName =>
        (arrProto[methodName] = function() {
          updateView();
          oldAddrayProperty[methodName].call(this, ...arguments);
        })
    );
    
    // 檢視更新
     function updateView() {
      console.log("檢視更新");
    }
    
    // 宣告要響應式的物件
    const data = {
      name: "zhangsan",
      age: 20,
      info: {
        address: "北京" // 需要深度監聽
      },
      nums: [10, 20, 30]
    };
    
    // 執行響應式
    observer(data);
    
  2. Vue3.0

    const proxyData = new Proxy(data, {
      get(target,key,receive){ 
        // 只處理本身(非原型)的屬性
        const ownKeys = Reflect.ownKeys(target)
        if(ownKeys.includes(key)){
          console.log('get',key) // 監聽
        }
        const result = Reflect.get(target,key,receive)
        return result
      },
      set(target, key, val, reveive){
        // 重複的資料,不處理
        const oldVal = target[key]
        if(val == oldVal){
          return true
        }
        const result = Reflect.set(target, key, val,reveive)
        console.log('set', key, val)
        return result
      },
      deleteProperty(target, key){
        const result = Reflect.deleteProperty(target,key)
        console.log('delete property', key)
        console.log('result',result)
        return result
      }
    })
    
     // 宣告要響應式的物件,Proxy會自動代理
    const data = {
      name: "zhangsan",
      age: 20,
      info: {
        address: "北京" // 需要深度監聽
      },
      nums: [10, 20, 30]
    };