天天看點

Object 的變化偵測

是什麼

是指在資料狀态發生變化時,讓對應的重新DOM渲染。這裡我們首先了解對象(Object)變化時,怎麼通知到外界的。

(1)簡單來看,第一次渲染的時候資料傳遞就是這樣一個過程

Object 的變化偵測

這裡的外界,指的是要渲染的模闆,或者要暴露出去的watch監聽方法等。

(2)當資料發生變化時,要讓外界知道資料的變化。是以資料和外界的關系應當如下。

Object 的變化偵測

(3)然後就是怎麼通知的問題了,這就是我們這次要将的Object變化偵測,就是要監聽資料的變化,并且通知到外界。這個監聽器我們暫且定義為watcher。watcher監聽着資料的變化,并負責通知外界。

Object 的變化偵測

(4)接下來,就是研究怎麼去監聽資料的變化了。

Vue2.0中使用的監聽方法是,使用Object.defineProperty()給對象設定setter和getter方法,getter和setter方法監聽資料的變化。

Object 的變化偵測

簡單實作簡單監聽的Demo:

function defindReactive(data, key, val) {
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function () {
      console.log('get data')
      return val;
    },
    set: function (newVal) {
      console.log('set data')
      if (val === newVal) {
        return
      }
      val = newVal
    }
  });
}


let numData = {num: 1};
let key = 'num';

// 設定監聽
defindReactive(numData, key, 2);

numData.num = 3
console.log(numData.num)

// 輸出
set data
get data
3
           

Object.defineProperty(obj, prop, descriptor);

具體參考:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty

可以看出在設定好監聽後,我們再對對象設定或讀取時,都可以觸發自定義的操作。

(5)能監聽到資料變化後,就要在資料變化時通知到使用該資料的地方,如某個對象的name字段值被修改了,就需要通知頁面上展示字段name的地方修改值。這裡展示name的地方也可以稱為該資料的依賴。是以我們在監聽到資料變化後,要做的主要工作就有:1. 收集依賴。 2. 通知依賴變化。

為了減少耦合,我們單獨定義一個依賴收集器Dep。

Object 的變化偵測

我們首先定義一個依賴收集器:

// 依賴收集器
class Dep {
  // 建立對象時,初始化收集器的數組
  constructor() {
    this.subs = []
  }
  
  addSub(sub) {
    this.subs.push(sub)
  }
  
  removeSub(sub) {
    remove(this.subs, sub);
  }
  
  // 依賴
  depend() {
    if (window.target) {
      this.addSub(window.target)
    }
  }
  
  // 通知
  notify() {
    // 複制數組
    const subs = this.subs.slice()
    for (let i = 0; i < subs.length; i++) {
      subs[i].update()
    }
  }
}

function remove(arr, item) {
  if (arr.length) {
    const index = arr.indexOf(item)
    if(index > -1) {
      arr.splice(index, 1)
    }
  }
}
           

然後在資料getter時收集依賴,在setter時觸發依賴:

// 資料監聽和依賴收集器結合
function defindReactive(data, key, val) {
  let dep = new Dep();
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function () {
      // 收集依賴
      dep.depend()
      return val;
    },
    set: function (newVal) {
      console.log('set data')
      if (val === newVal) {
        return
      }
      val = newVal
      // 通知依賴發生變化
      dep.notify();
    }
  });
}
           

(6)對于Object,我們希望把對象所有的屬性以及後代屬性都監聽到,是以我們還需要定義一個類,把資料内所有的屬性的都轉換成getter和setter形式。我們把它定義為Observer。

Object 的變化偵測
class Observer {
  constructor(value) {
    this.value = value
    
    if (!Array.isArray(value)) {
      this.walk(value);
    }
  }
  
  walk(obj) {
    for (let key in obj) {
      defineReactive(obj, key, obj[key]);
      console.log('value', obj[key]);
      console.log('key', key);
    }
  }
}
           

簡單測試一下:

let user = {
  name: 'hello',
  age: 223
}

let ob = new Observer(user)

// 列印輸出
value hello
key name
value 223
key age
           

再改造一下defineReactive:

// 資料監聽和依賴收集器結合
function defindReactive(data, key, val) {
  // 新增點
  if (typeof val === 'object) {
      new Observer(val)
  }
  let dep = new Dep();
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function () {
      // 收集依賴
      dep.depend()
      return val;
    },
    set: function (newVal) {
      console.log('set data')
      if (val === newVal) {
        return
      }
      val = newVal
      // 通知依賴發生變化
      dep.notify();
    }
  });
}
           

(7)最後實作Watcher的工作:

const bailRE = /[^\w.$]/

// 比對path是否滿足 a.b.c這樣的格式。傳回一個函數
export default function parsePath(path) {
  if (bailRE.test(path)) {
    return
  }
  const segments = path.split('.')
  
  return function(obj) {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      obj = obj[segments[i]]
    }
  }
}
           
class watcher {
  constructor(vm, expOrFn, cb) {
    this.vm = vm
    this.getter = parsePath(expOrFn)
    this.cb = cb
    this.value = this.get()
  }
  
  get() {
    window.target = this
    let value = this.getter.call(this.vm, this.vm);
    window.target = undefined
    return value
  }
  
  set() {
    const oldValue = this.value
    this.value = this.get()
    this.cb.call(this.vm, this.value, oldValue)
  }
}
           

代碼中的window.target用來表示目前依賴。

Object偵測的簡單邏輯圖

Object 的變化偵測