博客 / 詳情

返回

vue響應式原理(底層)超詳細的解讀,手寫響應式原理

vue響應式原理

關於vue響應式原理(底層)原理,今天和大家一起探討研究,結尾附上手敲代碼以及git下載地址,如有不足或不準確請及時留言指正,期待共同進步~

響應式原理官網圖

本文將採用webpack環境進行編寫,項目目錄如下。
index.js: 入口文件
arrar.js: 數組文件
def.js: 定義一個對象屬性
defineReactive.js: 給對象data的屬性key定義監聽
Dep.js: Dep類專門幫助我們管理依賴,可以收集依賴,刪除依賴,向watcher發送通知等。
Observe.js: 監聽 value,試圖創建bserver實例,如果value已經是響應式數據(根據是否具有__ob__屬性判斷),就不需要再創建Observer實例,直接返回已經創建的Observer實例即可。
Observer.js: 將一個正常的object轉換為每個層級的屬性都是響應式(可以被偵測)的object。
Wather.js: Watcher是一箇中介的角色,數據發生變化時通知它,然後它再通知其他地方.

image.png

先從index.js代碼編寫,環境搭建完後我們編寫代碼,輸出就會在控制枱展示,定義obj,Object.defineProperty進行數據劫持。

// index.js 入口文件

let obj = {
  a: 1,
  b: {
    c: {
      d: 4,
    },
  },
  e: [22, 33, 44, 55],
};
//定義一個函數 defineReactive 
let val;
function defineReactive(data, key, value) {
  Object.defineProperty(obj, key, {
    // 收集依賴 getter
    get() {
      console.log('您試圖訪問' + key  + '屬性');
      return val
    },
    // 觸發依賴 setter
    set (newVal) {
      console.log('你試圖訪問 ' + key + '屬性', newVal);
      if (val === newVal) {
        return 
      }
      val = newVal
      
    }
  })
}


defineReactive(obj, 'a', 10)
console.log('obj.a', obj.a); // 1
obj.a = 8
console.log('obj.a改變後', obj.a); // 8 

在Vue2.X 響應式中使用到了 Object.defineProperty 進行數據劫持,所以我們對它必須有一定的瞭解,Object.defineProperty中有兩個非常重要的函數,getter和setter,getter負責收集依賴,簡單來説,getter就是收集當前的obj對象的依賴,setter函數則是當你的數據改變時進行觸發的函數。

接下來我們思考obj.a是一個簡單簡單的數據類型,那如果我想要複雜的對象obj.b.c.d 的數值呢,打印一下控制枱發現雖然Object.defineProperty訪問了getter,但是並未監測到obj.b.c.d,只是説監測到b屬性。如圖2

image.png

那我們該怎麼做才能實現對深層次的對象進行監聽呢? 我們是否可以講每一層對象都循環調用,添加監聽,這時是不是聽到循環調用就想到了遞歸,沒錯我們可以使用---遞歸偵聽

為了代碼整齊,首先我們將defineReactive函數提出來,形成一個獨立文件,代碼和index.js分離。

// defineReactive.js
/**
 * 給對象data的屬性key定義監聽
 * @param {*} data 傳入的數據
 * @param {*} key 監聽的屬性
 * @param {*} value 閉包環境提供的週轉變量
 */
export default function defineReactive (data, key , val) {
  if (arguments.length === 2) {
    val = data[key]
  }
    Object.defineProperty(data, key, {
      // 可枚舉 可以循環
      enumerable: true,
      // 可被配置,比如可以被刪除
      configurable: true,
      get() {
        console.log('您試圖訪問' + key  + '屬性');
        return val
      },
      set (newVal) {
        console.log('你試圖訪問 ' + key + '屬性', newVal);
        if (val === newVal) {
          return 
        }
        val = newVal
      }
    })
}

在index.js中引入該文件,代碼如下

// index.js
import defineReactive from "./defineReactive";
let obj = {
  a: 1,
  b: {
    c: {
      d: 4,
    },
  },
  e: [22, 33, 44, 55],
};
defineReactive(obj, 'a')
defineReactive(obj, 'b')

console.log(obj.b.c.d);

接下來開始寫遞歸偵聽,新建一個observe類,這個類的作用就是判斷是否需要試創建新的Observer類,檢測value身上是否有__ob__屬性,如果有可以理解為value為響應式,響應式數據,就不需要再創建Observer實例,直接返回已經創建的Observer實例即可,避免重複偵測value變化的問題,否則,創建Observer實例。

// observe.js
import Observer from "./Observer";

/**
 * 監聽 value
 * 嘗試創建Observer實例,如果value已經是響應式數據,就不需要再創建Observer實例,直接返回已經創建的Observer實例即可,避免重複偵測value變化的問題
 * @param {*} value 
 * @returns 
 */
export default function observe(value) {
  // 如果value不是對象,就什麼都不做
  if (typeof value != "object") return;

  let ob; // Observer的實例
  if (typeof value.__ob__ !== "undefined") {
    ob = value.__ob__;
  } else {
    ob = new Observer(value);
  }
  
  return ob;
}

接下來寫observer類。

// observer.js

import observe from "./observe";
export default class Observer {
  constructor (value) {
    console.log('我是observer構造器', value);
    // 給實例添加__ob__屬性,值是當前Observer的實例,不可枚舉 
    // def被單獨拎出來了 主要作用就是為了添加__ob__屬性帶哦
    // this是當前new的實例
    def(value, "__ob__", this, false);
    this.walk(value);
  }
  // 遍歷
  walk(value) {
    for (let k in value) {
      defineReactive(value, k)
    }
  } 
}

// def.js
/**
 * 定義一個對象屬性
 * @param {*} obj 
 * @param {*} key 
 * @param {*} value 
 * @param {*} enumerable 
 */
export default function def(obj, key, value, enumerable) {
  Object.defineProperty(obj, key, {
    value,
    enumerable,
    writable: true,
    configurable: true,
  });
}

在observer類中,我們在構造器內執行def函數,def函數主要作用是為當前實例(obj)添加__ob__屬性,前面説過了這個屬性是代表實例是否是響應式的標誌。然後調用walk方法循環實例,在循環裏調用defineReactive函數。至此,外層的屬性(obj.a)已經成為響應式了
在index.js中 創建observe 函數 observe(obj),可以看見控制枱打印如下,並發現__ob__屬性已存在。

image.png

但是我們會發現在obj.b obj.c的身上沒有__ob__屬性。

image.png

我們寫Observer的目的是為了遞歸偵聽,現在我們對外層的元素已經完成了監測,思考下我們現在只剩下對內部的屬性進行偵聽了,那麼該怎麼做呢?

拆解一下,首先要監測內部元素,少不了循環,那如果在循環中對每一層進行監測不就ok了嗎?循環我們寫過了,剩下的就是需要在efineReactive函數內部調用observe類,observe子元素,而observe中又會調用observer類,然後循環,最後在setter中監測新的子元素的值即可。看下整體代碼。

// index.js
import observe from "./observe";
import defineReactive from "./defineReactive";
let obj = {
  a: 1,
  b: {
    c: {
      d: 4,
    },
  },
  e: [22, 33, 44, 55],
};
// 創建observe 函數 
observe(obj)



// observe.js
import Observer from "./Observer";
/**
 * 監聽 value
 * 嘗試創建Observer實例,如果value已經是響應式數據,就不需要再創建Observer實例,直接返回已經創建的Observer實例即可,避免重複偵測value變化的問題
 * @param {*} value 
 * @returns 
 */
export default function observe(value) {
  // 如果value不是對象,就什麼都不做
  if (typeof value != "object") return;

  let ob; // Observer的實例
  if (typeof value.__ob__ !== "undefined") {
    ob = value.__ob__;
  } else {
    ob = new Observer(value);
  }
  
  return ob;
}


// observer.js

def from "./def";
import defineReactive from "./defineReactive";
export default class Observer {
  constructor (value) {
    console.log('我是observer構造器', value);
    // 給實例添加__ob__屬性,值是當前Observer的實例,不可枚舉 
    def(value, "__ob__", this, false);
    this.walk(value)
  }
  // 遍歷
  walk(value) {
    for (let k in value) {
      defineReactive(value, k)
    }
  }
  
}


// def.js
/**
 * 定義一個對象屬性
 * @param {*} obj 
 * @param {*} key 
 * @param {*} value 
 * @param {*} enumerable 
 */
export default function def(obj, key, value, enumerable) {
  Object.defineProperty(obj, key, {
    value,
    enumerable,
    writable: true,
    configurable: true,
  });
}


// defineReactive.js
import observe from "./observe";

/**
 * 給對象data的屬性key定義監聽
 * @param {*} data 傳入的數據
 * @param {*} key 監聽的屬性
 * @param {*} value 閉包環境提供的週轉變量
 */
export default function defineReactive (data, key , val) {
  console.log('遞歸偵聽屬性',key);
  if (arguments.length === 2) {
    val = data[key]
  }
  // 子元素要進行observe,形成遞歸
  let childOb = observe(val);
    Object.defineProperty(data, key, {
      // 可枚舉 可以循環
      enumerable: true,
      // 可被配置,比如可以被刪除
      configurable: true,
      get() {
        console.log('您試圖訪問' + key  + '屬性');
        return val
      },
      set (newVal) {
        console.log('你試圖訪問 ' + key + '屬性', newVal);
        if (val === newVal) {
          return 
        }
        // 當設置了新值,新值也要被observe
        childOb = observe(newVal);
        val = newVal
      }
    })
}

在詳細講解一下上面這寫代碼邏輯,將代碼串聯一下。
首先,在index.js中let一個對象obj,調用obeserve函數(注意不是observer)在observe函數內部,首先查看是否為響應式,如果是,則不需要創建observer實例,避免重複監聽,節約資源。顯然當前的obj是非響應式的,那麼就需要創建一個observer實例,進入observer.js文件,進入observer內部執行defineReactive方法,進defineReactive文件,最關鍵一步,子元素要進行observe,形成遞歸(這個遞歸不是自己調用自己,而是多個函數嵌套調用)---〉這行代碼 let childOb = observe(val);完成了遞歸的重要一步,接下來要在setter函數中,更新值,childOb = observe(newVal);可以看見控制枱會輸入obj的每個屬性,b、c、d

WeChatfd505da72b247e72c451d3ce7a9ad691.png

我們設置obj.b.c.d = 110, 打印控制枱看下是否會生效。

image.png

以上遞歸偵聽可以歸納為下圖

下面,我們在已有函數的基礎上將數組的響應式原理補上去。數組的響應式原理。尤雨溪老師講數組的七個方法進行了改寫("push", "pop", "shift", "unshift", "splice",
"sort", "reverse",),數組的偵聽可以簡單的理解為如下原理

image.png

我們將數組的隱式原型鏈__proto__指向arrayMethods,而arrayMethods是以Array.prototype為原型創建的,我們在arrayMethods上改寫方法即可,新建array.js

首先備份一份,然後調用def.js, 添加屬性__ob__。小夥伴們是否記得我們在observer內部使用walk方法循環了實例,然後調用defineReactive方法,就是這裏我們需要補充一下數組的方法,增加代碼如下,判斷實例是對象還是數組,數組的話,就將這個數組的原型指向arrayMethods,去改寫數組方法,然後執行observeArray(數組的遍歷方法),簡單來説就是去偵測數組中的每一項,逐個進行observe。

// arrar.js

import def from "./def";

const arrayPrototype = Array.prototype;

// 以Array.prototype為原型創建arrayMethod
export const arrayMethods = Object.create(arrayPrototype);

// 要被改寫的7個數組方法
const methodsNeedChange = [
  "push",
  "pop",
  "shift",
  "unshift",
  "splice",
  "sort",
  "reverse",
];

// 批量操作這些方法
methodsNeedChange.forEach((methodName) => {
  // 備份原來的方法
  const original = arrayPrototype[methodName];

  // 定義新的方法
  def(
    arrayMethods,
    methodName,
    function () {
      console.log("array數據已經被劫持");

      // 恢復原來的功能(數組方法)
      const result = original.apply(this, arguments);
      // 把類數組對象變成數組
      const args = [...arguments];

      // 把這個數組身上的__ob__取出來
      // 在攔截器中獲取Observer的實例
      const ob = this.__ob__;

      // 有三種方法 push、unshift、splice能插入新項,要劫持(偵測)這些數據(插入新項)
      let inserted = [];
      switch (methodName) {
        case "push":
        case "unshift":
          inserted = args;
          break;
        case "splice":
          inserted = args.slice(2);
          break;
      }

      // 查看有沒有新插入的項inserted,有的話就劫持
      // ob.observeArray實例內部方法
      if (inserted) {
        ob.observeArray(inserted);
      }

      return result;
    },
    false
  );
});


// observer.js
// 在observer 內部判斷是否為數組

import def from "./def";
import defineReactive from "./defineReactive";
import { arrayMethods } from "./array";
export default class Observer {
  constructor (value) {
    console.log('我是observer構造器', value);
    // 給實例添加__ob__屬性,值是當前Observer的實例,不可枚舉 
    def(value, "__ob__", this, false);
    // 判斷是數組還是對象
    if (Array.isArray(value)) {
      // 是數組,就將這個數組的原型指向arrayMethods
      Object.setPrototypeOf(value, arrayMethods);
      // 早期實現是這樣
      // value.__proto__ = arrayMethods;
      
      // observe數組
      this.observeArray(value);
    } else {
      this.walk(value);
    }
  }
  // 遍歷
  walk(value) {
    for (let k in value) {
      defineReactive(value, k)
    }
  }
  // 數組的遍歷方式,偵測數組中的每一項
  observeArray(arr) {
    for (let i = 0, l = arr.length; i < l; i++) {
      // 逐項進行observe
      observe(arr[i]);
    }
  }
  
}

自此數組的偵測已經完成。

下面進行收集依賴和watcher的講解。
新建一個Dep類,Dep類專門幫助我們管理依賴,可以收集依賴,刪除依賴,向依賴發送通知等,新建文件Dep.js,Dep類中包含addSub,depend,notify方法。watcher則是中轉站,依賴的變化需要通知watcher。

新建dep類

let uid = 0;
/**
 * Dep類專門幫助我們管理依賴,可以收集依賴,刪除依賴,向依賴發送通知等
 */
export default class Dep {
  constructor() {
    console.log("Dep構造器", this);
    this.id = uid++;
    // 用數組存儲自己的訂閲者,放的是Watcher的實例
    this.subs = [];
  }

  // 添加訂閲
  addSub(sub) {
    this.subs.push(sub);
  }

  // 刪除訂閲
  removeSub(sub) {
    remove(this.subs, sub);
  }

  // 添加依賴
  depend() {
    // Dep.target 是一個我們指定的全局的位置,用window.target也行,只要是全局唯一,沒有歧義就行
    if (Dep.target) {
      this.addSub(Dep.target);
    }
  }

  // 通知更新
  notify() {
    console.log("通知更新notify");
    // 淺拷貝一份
    const subs = this.subs.slice();
    // 遍歷
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update();
    }
  }
}

/**
 * 從arr數組中刪除元素item
 * @param {*} arr
 * @param {*} item
 * @returns
 */
function remove(arr, item) {
  if (arr.length) {
    const index = arr.indexOf(item);
    if (index > -1) {
      return arr.splice(index, 1);
    }
  }
}


那麼收集依賴應該在哪裏收集呢,答案很顯然是在Object.defineProperty中,因為Object.defineProperty天生可以進行數據劫持,故我們在Object.defineProperty中創建Dep數組。在getter中判斷當前是否處於依賴收集階段(Dep.target為true),還需要對子元素進行判斷。

// defineReactive.js

// 可以理解為所有的依賴收集工作都有Dep完成,然後在通知watcher

import Dep from "./Dep";
import observe from "./observe";

/**
 * 給對象data的屬性key定義監聽
 * @param {*} data 傳入的數據
 * @param {*} key 監聽的屬性
 * @param {*} value 閉包環境提供的週轉變量
 */
export default function defineReactive(data, key, value) {
  console.log('執行defineReactive()', key)
  
  // 每個數據都要維護一個屬於自己的數組,用來存放依賴自己的watcher
  const dep = new Dep();

  if (arguments.length === 2) {
    value = data[key];
  }

  // 子元素要進行observe,形成遞歸
  let childOb = observe(value);

  Object.defineProperty(data, key, {

    // 可枚舉 可以for-in
    enumerable: true,
    // 可被配置,比如可以被delete
    configurable: true,

    // getter  收集依賴
    get() {
      console.log(`getter試圖訪問${key}屬性 偵測中 `);

      // 收集依賴 Dep.target就是當前的wather實例
      if (Dep.target) {
        dep.depend();

        // 判斷有沒有子元素
        if (childOb) {
          // 數組收集依賴
          childOb.dep.depend();
        }
      }

      return value;
    },

    // setter 觸發依賴
    set(newValue) {
      console.log(`setter試圖改變${key}屬性 偵測中`, newValue);

      if (value === newValue) return;
      value = newValue;

      // 當設置了新值,新值也要被observe
      childOb = observe(newValue);

      // 觸發依賴
      // 發佈訂閲模式,通知dep
      dep.notify();
    },
  });
}

最後我們還差一個watcher類,用來接受dep的消息,並更新。

// watcher.js

import Dep from "./Dep";

let uid = 0;
/**
 * Watcher是一箇中介的角色,數據發生變化時通知它,然後它再通知其他地方
 */
export default class Watcher {
  constructor(target, expression, callback) {
    console.log("Watcher構造器");
    this.id = uid++;
    this.target = target;
    // 按點拆分  執行this.getter()就可以讀取data.a.b.c的內容
    this.getter = parsePath(expression);
    this.callback = callback;
    this.value = this.get();
  }

  get() {
    // 進入依賴收集階段。
    // 讓全局的Dep.target設置為Watcher本身
    Dep.target = this;
    const obj = this.target;
    var value;
    // 只要能找就一直找
    try {
      value = this.getter(obj);
    } finally {
      Dep.target = null;
    }
    return value;
  }

  update() {
    this.run();
  } 

  run() {
    this.getAndInvoke(this.callback);
  }
  getAndInvoke(callback) {
    const value = this.get();
    if (value !== this.value || typeof value === "object") {
      const oldValue = this.value;
      this.value = value;
      callback.call(this.target, value, oldValue);
    }
  }
}

/**
 * 將str用.分割成數組segments,然後循環數組,一層一層去讀取數據,最後拿到的obj就是str中想要讀的數據
 * @param {*} str
 * @returns
 */
function parsePath(str) {
  let segments = str.split(".");
  return function (obj) {
    for (let key of segments) {
      if (!obj) return;
      obj = obj[key];
    }
    return obj;
  };
}

Watcher類中需要注意的是構造器中的getter,getter接收parsePath函數的返回值,parsePath函數的主要作用是用.分割成數組segments,然後循環數組,一層一層去讀取數據,最後拿到的obj就是str中想要讀的數據,對於當前例子來説就是當我們對obj在data中定義時,內層obj.b.c.d : 4(初始值),這裏的拿到的就是obj.b.c.d的初始值4。

到此響應式原理基本完成。如圖

image.png

希望可以幫助到大家,也希望大家積極留言點贊,歡迎交流,前端小白,請各位大佬多點包容~~ 一起進步

項目地址(可以直接跑起來github):https://github.com/yu15645630...

參考文件:https://www.bilibili.com/vide...

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.