Vue響應式資料: Observer模組實現

請叫我王磊同學發表於2018-07-01

前言

  首先歡迎大家關注我的Github部落格,也算是對我的一點鼓勵,畢竟寫東西沒法獲得變現,能堅持下去也是靠的是自己的熱情和大家的鼓勵。接下來的日子我應該會著力寫一系列關於Vue與React內部原理的文章,感興趣的同學點個關注或者Star。

  之前的兩篇文章響應式資料與資料依賴基本原理從Vue陣列響應化所引發的思考我們介紹了響應式資料相關的內容,沒有看的同學可以點選上面的連結瞭解一下。如果大家都閱讀過上面兩篇文章的話,肯定對這方面內容有了足夠的知識儲備,想來是時候來看看Vue內部是如何實現資料響應化。目前Vue的程式碼非常龐大,但其中包含了例如:伺服器渲染等我們不關心的內容,為了能集中於我們想學習的部分,我們這次閱讀的是Vue的早期程式碼,大家可以checkout這裡檢視對應的程式碼。

  之前零零碎碎的看過React的部分原始碼,當我看到Vue的原始碼,覺得真的是非常優秀,各個模組之間解耦的非常好,可讀性也很高。Vue響應式資料是在Observer模組中實現的,我們可以看看Observer是如何實現的。   

釋出-訂閱模式  

  如果看過上兩篇文章的同學應該會發現一個問題:資料響應化的程式碼與其他的程式碼耦合太強了,比如說:   

//程式碼來源於文章:響應式資料與資料依賴基本原理
//定義物件的單個響應式屬性
function defineReactive(obj, key, value){
  observify(value);
  Object.defineProperty(obj, key, {
    configurable: true,
    enumerable: true,
    set: function(newValue){
      var oldValue = value;
      value = newValue;
      //可以在修改資料時觸發其他的操作
      console.log("newValue: ", newValue, " oldValue: ", oldValue);
    },
    get: function(){
      return value;
    }
  });
}
複製程式碼

  比如上面的程式碼,set內部的處理的程式碼就與整個資料響應化相耦合,如果下次我們想要在set中做其他的操作,就必須要修改set函式內部的內容,這是非常不友好的,不符合開閉原則(OCP: Open Close Principle)。當然Vue不會採用這種方式去設計,為了解決這個問題,Vue引入了釋出-訂閱模式。其實發布-訂閱模式是前端工程師非常熟悉的一種模式,又叫做觀察者模式,它是一種定義物件間一種一對多的依賴關係,當一個物件的狀態發生改變的時候,其他觀察它的物件都會得到通知。我們最常見的DOM事件就是一種釋出-訂閱模式。比如:   

document.body.addEventListener("click", function(){
    console.log("click event");
});
複製程式碼

  在上面的程式碼中我們監聽了bodyclick事件,雖然我們不知道click事件什麼時候會發生,但是我們一定能保證,如果發生了bodyclick事件,我們一定能得到通知,即回撥函式被呼叫。在JavaScript中因為函式是一等公民,我們很少使用傳統的釋出-訂閱模式,多采用的是事件模型的方式實現。在Vue中也實現了一個事件模型,我們可以看一下。因為Vue的模組之間解耦的非常好,因此在看程式碼之前,其實我們可以先來看看對應的單元測試檔案,你就知道這個模組要實現什麼功能,甚至如果你願意的話,也可以自己實現一個類似的模組放進Vue的原始碼中執行。

  Vue早期程式碼使用是jasmine進行單元測試,emitter_spec.js是事件模型的單元測試檔案。首先簡單介紹一下jasmine用到的函式,可以對照下面的程式碼瞭解具體的功能:

  • describe是一個測試單元集合
  • it是一個測試用例
  • beforeEach會在每一個測試用例it執行前執行
  • expect期望函式,用作對期望值和實際值之間執行邏輯比較
  • createSpy用來建立spy,而spy的作用是監測函式的呼叫相關資訊和函式執行引數

  

var Emitter = require('../../../src/emitter')
var u = undefined
// 程式碼有刪減
describe('Emitter', function () {

  var e, spy
  beforeEach(function () {
    e = new Emitter()
    spy = jasmine.createSpy('emitter')
  })
  
  it('on', function () {
    e.on('test', spy)
    e.emit('test', 1, 2 ,3)
    expect(spy.calls.count()).toBe(1)
    expect(spy).toHaveBeenCalledWith(1, 2, 3)
  })

  it('once', function () {
    e.once('test', spy)
    e.emit('test', 1, 2 ,3)
    e.emit('test', 2, 3, 4)
    expect(spy.calls.count()).toBe(1)
    expect(spy).toHaveBeenCalledWith(1, 2, 3)
  })

  it('off', function () {
    e.on('test1', spy)
    e.on('test2', spy)
    e.off()
    e.emit('test1')
    e.emit('test2')
    expect(spy.calls.count()).toBe(0)
  })
  
  it('apply emit', function () {
    e.on('test', spy)
    e.applyEmit('test', 1)
    e.applyEmit('test', 1, 2, 3, 4, 5)
    expect(spy).toHaveBeenCalledWith(1)
    expect(spy).toHaveBeenCalledWith(1, 2, 3, 4, 5)
  })

})
複製程式碼

  可以看出Emitter物件例項對外提供以下介面:

  • on: 註冊監聽介面,引數分別是事件名監聽函式
  • emit: 觸發事件函式,引數是事件名
  • off: 取消對應事件的註冊函式,引數分別是事件名監聽函式
  • once: 與on類似,僅會在第一次時通知監聽函式,隨後監聽函式會被移除。

  看完了上面的單元測試程式碼,我們現在已經基本瞭解了這個模組要幹什麼,現在讓我們看看對應的程式碼:

// 刪去了註釋並且對程式碼順序有調整
// ctx是監聽回撥函式的執行作用域(this)
function Emitter (ctx) {
  this._ctx = ctx || this
}

var p = Emitter.prototype

p.on = function (event, fn) {
  this._cbs = this._cbs || {}
  ;(this._cbs[event] || (this._cbs[event] = []))
    .push(fn)
  return this
}
// 三種模式 
// 不傳參情況清空所有監聽函式 
// 僅傳事件名則清除該事件的所有監聽函式
// 傳遞事件名和回撥函式,則對應僅刪除對應的監聽事件
p.off = function (event, fn) {
  this._cbs = this._cbs || {}

  // all
  if (!arguments.length) {
    this._cbs = {}
    return this
  }

  // specific event
  var callbacks = this._cbs[event]
  if (!callbacks) return this

  // remove all handlers
  if (arguments.length === 1) {
    delete this._cbs[event]
    return this
  }

  // remove specific handler
  var cb
  for (var i = 0; i < callbacks.length; i++) {
    cb = callbacks[i]
    // 這邊的程式碼之所以會有cb.fn === fn要結合once函式去看
    // 給once傳遞的監聽函式其實已經被wrapped過
    // 但是仍然可以通過原來的監聽函式去off掉
    if (cb === fn || cb.fn === fn) {
      callbacks.splice(i, 1)
      break
    }
  }
  return this
}
// 觸發對應事件的所有監聽函式,注意最多隻能用給監聽函式傳遞三個引數(採用call)
p.emit = function (event, a, b, c) {
  this._cbs = this._cbs || {}
  var callbacks = this._cbs[event]

  if (callbacks) {
    callbacks = callbacks.slice(0)
    for (var i = 0, len = callbacks.length; i < len; i++) {
      callbacks[i].call(this._ctx, a, b, c)
    }
  }

  return this
}
// 觸發對應事件的所有監聽函式,傳遞引數個數不受限制(採用apply)
p.applyEmit = function (event) {
  this._cbs = this._cbs || {}
  var callbacks = this._cbs[event], args

  if (callbacks) {
    callbacks = callbacks.slice(0)
    args = callbacks.slice.call(arguments, 1)
    for (var i = 0, len = callbacks.length; i < len; i++) {
      callbacks[i].apply(this._ctx, args)
    }
  }

  return this
}
// 通過呼叫on與off事件事件,在第一次觸發之後就`off`對應的監聽事件
p.once = function (event, fn) {
  var self = this
  this._cbs = this._cbs || {}

  function on () {
    self.off(event, on)
    fn.apply(this, arguments)
  }

  on.fn = fn
  this.on(event, on)
  return this
}

複製程式碼

  我們可以看到上面的程式碼採用了原型模式建立了一個Emitter類。配合Karma跑一下這個模組 ,測試用例全部通過,到現在我們已經閱讀完Emitter了,這算是一個小小的熱身吧,接下來讓我們正式看一下Observer模組。   

Observer

對外功能

  按照上面的思路我們先看看Observer對應的測試用例observer_spec.js,由於Observer的測試用例非常長,我會在程式碼註釋中做解釋,並儘量精簡測試用例,能讓我們瞭解模組對應功能即可,希望你能有耐心閱讀下來。  

//測試用例是精簡版,否則太冗長
var Observer = require('../../../src/observe/observer')
var _ = require('../../../src/util') //Vue內部使用工具方法
var u = undefined
Observer.pathDelimiter = '.' //配置Observer路徑分隔符

describe('Observer', function () {

  var spy
  beforeEach(function () {
    spy = jasmine.createSpy('observer')
  })
//我們可以看到我們通過Observer.create函式可以將資料變為可響應化,
//然後我們監聽get事件可以在屬性被讀取時觸發對應事件,注意物件巢狀的情況(例如b.c)
  it('get', function () {
    Observer.emitGet = true
    var obj = {
      a: 1,
      b: {
        c: 2
      }
    }
    var ob = Observer.create(obj)
    ob.on('get', spy)

    var t = obj.b.c
    expect(spy).toHaveBeenCalledWith('b', u, u)
    expect(spy).toHaveBeenCalledWith('b.c', u, u)
    
    Observer.emitGet = false
  })
//我們可以監聽響應式資料的set事件,當響應式資料修改的時候,會觸發對應的時間
  it('set', function () {
    var obj = {
      a: 1,
      b: {
        c: 2
      }
    }
    var ob = Observer.create(obj)
    ob.on('set', spy)

    obj.b.c = 4
    expect(spy).toHaveBeenCalledWith('b.c', 4, u)
  })
//帶有$與_開頭的屬性都不會被處理
  it('ignore prefix', function () {
    var obj = {
      _test: 123,
      $test: 234
    }
    var ob = Observer.create(obj)
    ob.on('set', spy)
    obj._test = 234
    obj.$test = 345
    expect(spy.calls.count()).toBe(0)
  })
//訪問器屬性也不會被處理
  it('ignore accessors', function () {
    var obj = {
      a: 123,
      get b () {
        return this.a
      }
    }
    var ob = Observer.create(obj)
    obj.a = 234
    expect(obj.b).toBe(234)
  })
// 對數屬性的get監聽,注意巢狀的情況
  it('array get', function () {

    Observer.emitGet = true

    var obj = {
      arr: [{a:1}, {a:2}]
    }
    var ob = Observer.create(obj)
    ob.on('get', spy)

    var t = obj.arr[0].a
    expect(spy).toHaveBeenCalledWith('arr', u, u)
    expect(spy).toHaveBeenCalledWith('arr.0.a', u, u)
    expect(spy.calls.count()).toBe(2)

    Observer.emitGet = false
  })
// 對數屬性的get監聽,注意巢狀的情況
  it('array set', function () {
    var obj = {
      arr: [{a:1}, {a:2}]
    }
    var ob = Observer.create(obj)
    ob.on('set', spy)

    obj.arr[0].a = 2
    expect(spy).toHaveBeenCalledWith('arr.0.a', 2, u)
  })
// 我們看到可以通過監聽mutate事件,在push呼叫的時候對應觸發事件
// 觸發事件第一個引數是"",代表的是路徑名,具體原始碼可以看出,對於陣列變異方法都是空字串
// 觸發事件第二個引數是陣列本身
// 觸發事件第三個引數比較複雜,其中:
// method屬性: 代表觸發的方法名稱
// args屬性: 代表觸發方法傳遞引數
// result屬性: 代表觸發變異方法之後陣列的結果
// index屬性: 代表變異方法對陣列發生變化的最開始元素
// inserted屬性: 代表陣列新增的元素
// remove屬性: 代表陣列刪除的元素
// 其他的變異方法: pop、shift、unshift、splice、sort、reverse內容都是非常相似的
// 具體我們就不一一列舉的了,如果有疑問可以自己看到全部的單元測試程式碼
  it('array push', function () {
    var arr = [{a:1}, {a:2}]
    var ob = Observer.create(arr)
    ob.on('mutate', spy)
    arr.push({a:3})
    expect(spy.calls.mostRecent().args[0]).toBe('')
    expect(spy.calls.mostRecent().args[1]).toBe(arr)
    var mutation = spy.calls.mostRecent().args[2]
    expect(mutation).toBeDefined()
    expect(mutation.method).toBe('push')
    expect(mutation.index).toBe(2)
    expect(mutation.removed.length).toBe(0)
    expect(mutation.inserted.length).toBe(1)
    expect(mutation.inserted[0]).toBe(arr[2])
  })
  
// 我們可以看到響應式資料中存在$add方法,類似於Vue.set,可以監聽add事件
// 可以向響應式物件中新增新一個屬性,如果之前存在該屬性則操作會被忽略
// 並且新賦值的物件也必須被響應化
// 我們省略了物件資料$delete方法的單元測試,功能類似於Vue.delete,與$add方法相反,可以用於刪除物件的屬性
// 我們省略了陣列的$set方法的單元測試,功能也類似與Vue.set,可以用於設定陣列對應數字下標的值
// 我們省略了陣列的$remove方法的單元測試,功能用於移除陣列給定下標的值或者給定的值,例如:
// var arr = [{a:1}, {a:2}]
// var ob = Observer.create(arr)
// arr.$remove(0) => 移除對應下標的值 或者
// arr.$remove(arr[0]) => 移除給定的值

  it('object.$add', function () {
    var obj = {a:{b:1}}
    var ob = Observer.create(obj)
    ob.on('add', spy)

    // ignore existing keys
    obj.$add('a', 123)
    expect(spy.calls.count()).toBe(0)

    // add event
    var add = {d:2}
    obj.a.$add('c', add)
    expect(spy).toHaveBeenCalledWith('a.c', add, u)

    // check if add object is properly observed
    ob.on('set', spy)
    obj.a.c.d = 3
    expect(spy).toHaveBeenCalledWith('a.c.d', 3, u)
  })

// 下面的測試用例用來表示如果兩個不同物件parentA、parentB的屬性指向同一個物件obj,那麼該物件obj改變時會分別parentA與parentB的監聽事件

  it('shared observe', function () {
    var obj = { a: 1 }
    var parentA = { child1: obj }
    var parentB = { child2: obj }
    var obA = Observer.create(parentA)
    var obB = Observer.create(parentB)
    obA.on('set', spy)
    obB.on('set', spy)
    obj.a = 2
    expect(spy.calls.count()).toBe(2)
    expect(spy).toHaveBeenCalledWith('child1.a', 2, u)
    expect(spy).toHaveBeenCalledWith('child2.a', 2, u)
    // test unobserve
    parentA.child1 = null
    obj.a = 3
    expect(spy.calls.count()).toBe(4)
    expect(spy).toHaveBeenCalledWith('child1', null, u)
    expect(spy).toHaveBeenCalledWith('child2.a', 3, u)
  })

})
複製程式碼

原始碼實現

陣列

  能堅持看到這裡,我們的長征路就走過了一半了,我們已經知道了Oberver對外提供的功能了,現在我們就來了解一下Oberver內部的實現原理。      Oberver模組實際上採用採用組合繼承(借用建構函式+原型繼承)方式繼承了Emitter,其目的就是繼承Emitteron, offemit等方法。我們在上面的測試用例發現,我們並沒有用new方法直接建立一個Oberver的物件例項,而是採用一個工廠方法Oberver.create方法來建立的,我們接下來看原始碼,由於程式碼比較多我會盡量去拆分成一個個小塊來講:   

// 程式碼出自於observe.js
// 為了方便講解我對程式碼順序做了改變,要了解詳細的情況可以檢視具體的原始碼

var _ = require('../util')
var Emitter = require('../emitter')
var arrayAugmentations = require('./array-augmentations')
var objectAugmentations = require('./object-augmentations')

var uid = 0
/**
 * Type enums
 */

var ARRAY  = 0
var OBJECT = 1

function Observer (value, type, options) {
  Emitter.call(this, options && options.callbackContext)
  this.id = ++uid
  this.value = value
  this.type = type
  this.parents = null
  if (value) {
    _.define(value, '$observer', this)
    if (type === ARRAY) {
      _.augment(value, arrayAugmentations)
      this.link(value)
    } else if (type === OBJECT) {
      if (options && options.doNotAlterProto) {
        _.deepMixin(value, objectAugmentations)
      } else {
        _.augment(value, objectAugmentations)
      }
      this.walk(value)
    }
  }
}

var p = Observer.prototype = Object.create(Emitter.prototype)

Observer.pathDelimiter = '\b'

Observer.emitGet = false

Observer.create = function (value, options) {
  if (value &&
      value.hasOwnProperty('$observer') &&
      value.$observer instanceof Observer) {
    return value.$observer
  } if (_.isArray(value)) {
    return new Observer(value, ARRAY, options)
  } else if (
    _.isObject(value) &&
    !value.$scope // avoid Vue instance
  ) {
    return new Observer(value, OBJECT, options)
  }
}

複製程式碼

  我們首先從Observer.create看起,如果value值沒有響應化過(通過是否含有$observer屬性去判斷),則使用new操作符建立Obsever例項(區分物件OBJECT與陣列ARRAY)。接下來我們看Observer的建構函式是怎麼定義的,首先借用Emitter建構函式:   

Emitter.call(this, options && options.callbackContext)
複製程式碼

配合原型繼承

var p = Observer.prototype = Object.create(Emitter.prototype)
複製程式碼

從而實現了組合繼承Emitter,因此Observer繼承了Emitter的屬性(ctx)和方法(on,emit等)。我們可以看到Observer有以下屬性:

  • id: 響應式資料的唯一標識
  • value: 原始資料
  • type: 標識是陣列還是物件
  • parents: 標識響應式資料的父級,可能存在多個,比如var obj = { a : { b: 1}},在處理{b: 1}的響應化過程中parents中某個屬性指向的就是obj$observer

  我們接著看首先給該資料賦值$observer屬性,指向的是例項物件本身。_.define內部是通過defineProperty實現的:

define = function (obj, key, val, enumerable) {
  Object.defineProperty(obj, key, {
    value        : val,
    enumerable   : !!enumerable,
    writable     : true,
    configurable : true
  })
}
複製程式碼

  下面我們首先看看是怎麼處理陣列型別的資料的

if (type === ARRAY) {
    _.augment(value, arrayAugmentations)
    this.link(value)
}
複製程式碼

  如果看過我前兩篇文章的同學,其實還記得我們對陣列響應化當時還做了一個著重的原理講解,大概原理就是我們通過給陣列物件設定新的原型物件,從而遮蔽掉原生陣列的變異方法,大概的原理可以是:   

function observifyArray(array){
    var aryMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
    var arrayAugmentations = Object.create(Array.prototype);
    aryMethods.forEach((method)=> {
        let original = Array.prototype[method];
        arrayAugmentations[method] = function () {
            // 呼叫對應的原生方法並返回結果
            // do everything you what do !
            return original.apply(this, arguments);
        };
    });
    array.__proto__ = arrayAugmentations;
}
複製程式碼

  回到Vue的原始碼,雖然我們知道基本原理肯定是相同的,但是我們仍然需要看看arrayAugmentations是什麼?下面arrayAugmentations程式碼比較長。我們會在註釋裡面解釋基本原理:   

// 程式碼來自於array-augmentations.js
var _ = require('../util')
var arrayAugmentations = Object.create(Array.prototype)
// 這邊操作和我們之前的實現方式非常相似
// 建立arrayAugmentations原型繼承`Array.prototype`從而可以呼叫陣列的原生方法
// 然後通過arrayAugmentations覆蓋陣列的變異方法,基本邏輯大致相同
['push','pop','shift','unshift','splice','sort','reverse'].forEach(function (method) {
  var original = Array.prototype[method]
  // 覆蓋arrayAugmentations中的變異方法
  _.define(arrayAugmentations, method, function () {
    
    var args = _.toArray(arguments)
    // 這裡呼叫了原生的陣列變異方法,並獲得結果
    var result = original.apply(this, args)
    var ob = this.$observer
    var inserted, removed, index
    // 下面switch這一部分程式碼看起來很長,其實目的就是針對於不同的變異方法生成:
    // insert removed inserted 具體的含義對照之前的解釋,瞭解即可
    switch (method) {
      case 'push':
        inserted = args
        index = this.length - args.length
        break
      case 'unshift':
        inserted = args
        index = 0
        break
      case 'pop':
        removed = [result]
        index = this.length
        break
      case 'shift':
        removed = [result]
        index = 0
        break
      case 'splice':
        inserted = args.slice(2)
        removed = result
        index = args[0]
        break
    }

    // 如果給陣列中插入新的資料,則需要呼叫ob.link
    // link函式其實在上面的_.augment(value, arrayAugmentations)之後也被呼叫了
    // 具體的實現我們可以先不管
    // 我們只要知道其目的就是分別對插入的資料執行響應化
    if (inserted) ob.link(inserted, index)
    // 其實從link我們就可以猜出unlink是幹什麼的
    // 主要就是對刪除的資料解除響應化,具體實現邏輯後面解釋
    if (removed) ob.unlink(removed)

    // updateIndices我們也先不講是怎麼實現的,
    // 目的就是更新子元素在parents的key
    // 因為push和pop是不會改變現有元素的位置,因此不需要呼叫
    // 而諸如splce shift unshift等變異方法會改變對應下標值,因此需要呼叫
    if (method !== 'push' && method !== 'pop') {
      ob.updateIndices()
    }

    // 同樣我們先不考慮propagate內部實現,我們只要propagate函式的目的就是
    // 觸發自身及其遞迴觸發父級的事件
    // 如果陣列中的資料有插入或者刪除,則需要對外觸發"length"被改變
    if (inserted || removed) {
      ob.propagate('set', 'length', this.length)
    }

    // 對外觸發mutate事件
    // 可以對照我們之前講的測試用例'array push',就是在這裡觸發的,回頭看看吧
    ob.propagate('mutate', '', this, {
      method   : method,
      args     : args,
      result   : result,
      index    : index,
      inserted : inserted || [],
      removed  : removed || []
    })

    return result
  })
})

// 可以回看一下測試用例 array set,目的就是設定對應下標的值
// 其實就是呼叫了splice變異方法, 其實我們在Vue中國想要改變某個下標的值的時候
// 官網給出的建議無非是Vue.set或者就是splice,都是相同的原理
// 注意這裡的程式碼忽略了超出下標範圍的值
_.define(arrayAugmentations, '$set', function (index, val) {
  if (index >= this.length) {
    this.length = index + 1
  }
  return this.splice(index, 1, val)[0]
})
// $remove與$add都是一個道理,都是呼叫的是`splice`函式
_.define(arrayAugmentations, '$remove', function (index) {
  if (typeof index !== 'number') {
    index = this.indexOf(index)
  }
  if (index > -1) {
    return this.splice(index, 1)[0]
  }
})

module.exports = arrayAugmentations
複製程式碼

  上面的程式碼相對比較長,具體的解釋我們在程式碼中已經註釋。到這裡我們已經瞭解完arrayAugmentations了,我們接著看看_.augment做了什麼。我們在文章從Vue陣列響應化所引發的思考中講過Vue是通過__proto__來實現陣列響應化的,但是由於__proto__是個非標準屬性,雖然廣泛的瀏覽器廠商基本都實現了這個屬性,但是還是存在部分的安卓版本並不支援該屬性,Vue必須對此做相關的處理,_.augment就負責這個部分:   

exports.augment = '__proto__' in {}
  ? function (target, proto) {
      target.__proto__ = proto
    }
  : exports.deepMixin
  
exports.deepMixin = function (to, from) {
  Object.getOwnPropertyNames(from).forEach(function (key) {
    var desc =Object.getOwnPropertyDescriptor(from, key)
    Object.defineProperty(to, key, desc)
  })
}  
複製程式碼

  我們看到如果瀏覽器不支援__proto__話呼叫deepMixin函式。而deepMixin的實現也是非常的簡單,就是使用Object.defineProperty將原物件的屬性描述符賦值給目標物件。接著呼叫了函式:   

this.link(value)
複製程式碼

  關於link函式在上面的備註中我們已經見過了:

if (inserted) ob.link(inserted, index)
複製程式碼

  當時我們的解釋是將新插入的資料響應化,知道了功能我們看看程式碼的實現:   

// p === Observer.prototype
p.link = function (items, index) {
  index = index || 0
  for (var i = 0, l = items.length; i < l; i++) {
    this.observe(i + index, items[i])
  }
}

p.observe = function (key, val) {
  var ob = Observer.create(val)
  if (ob) {
    // register self as a parent of the child observer.
    var parents = ob.parents
    if (!parents) {
      ob.parents = parents = Object.create(null)
    }
    if (parents[this.id]) {
      _.warn('Observing duplicate key: ' + key)
      return
    }
    parents[this.id] = {
      ob: this,
      key: key
    }
  }
}
複製程式碼

  其實程式碼邏輯非常簡單,link函式會對給定陣列index(預設為0)之後的元素呼叫this.observe, 而observe其實也就是對給定的val值遞迴呼叫Observer.create,將資料響應化,並建立父級的Observer與當前例項的對應關係。前面其實我們發現Vue不僅僅會對插入的資料響應化,並且也會對刪除的元素呼叫unlink,具體的呼叫程式碼是:

if (removed) ob.unlink(removed)
複製程式碼

  之前我們大致講過其用作就是對刪除的資料解除響應化,我們來看看具體的實現:

p.unlink = function (items) {
  for (var i = 0, l = items.length; i < l; i++) {
    this.unobserve(items[i])
  }
}
p.unobserve = function (val) {
  if (val && val.$observer) {
    val.$observer.parents[this.id] = null
  }
}
複製程式碼

  程式碼非常簡單,就是對資料呼叫unobserve,而unobserve函式的主要目的就是解除父級observer與當前資料的關係並且不再保留引用,讓瀏覽器核心必要的時候能夠回收記憶體空間。

  在arrayAugmentations中其實還呼叫過Observer的兩個原型方法,一個是:

ob.updateIndices()
複製程式碼

  另一個是:

ob.propagate('set', 'length', this.length)
複製程式碼

  首先看看updateIndices函式,當時的函式的作用是更新子元素在parents的key,來看看具體實現:   

p.updateIndices = function () {
  var arr = this.value
  var i = arr.length
  var ob
  while (i--) {
    ob = arr[i] && arr[i].$observer
    if (ob) {
      ob.parents[this.id].key = i
    }
  }
}
複製程式碼

  接著看函式propagate:   

p.propagate = function (event, path, val, mutation) {
  this.emit(event, path, val, mutation)
  if (!this.parents) return
  for (var id in this.parents) {
    var parent = this.parents[id]
    if (!parent) continue
    var key = parent.key
    var parentPath = path
      ? key + Observer.pathDelimiter + path
      : key
    parent.ob.propagate(event, parentPath, val, mutation)
  }
}
複製程式碼

  我們之前說過propagate函式的作用的就是觸發自身及其遞迴觸發父級的事件,首先呼叫emit函式對外觸發時間,其引數分別是:事件名、路徑、值、mutatin物件。然後接著遞迴呼叫父級的事件,並且對應改變觸發的path引數。parentPath等於parents[id].key + Observer.pathDelimiter + path

  到此為止我們已經學習完了Vue是如何處理陣列的響應化的,現在需要來看看是如何處理物件的響應化的。   

物件  

     在Observer的建構函式中關於物件處理的程式碼是:

if (type === OBJECT) {
    if (options && options.doNotAlterProto) {
        _.deepMixin(value, objectAugmentations)
    } else {
        _.augment(value, objectAugmentations)
    }
    this.walk(value)
}
複製程式碼

  和陣列一樣,我們首先要了解一下objectAugmentations的內部實現:

var _ = require('../util')
var objectAgumentations = Object.create(Object.prototype)

_.define(objectAgumentations, '$add', function (key, val) {
  if (this.hasOwnProperty(key)) return
  _.define(this, key, val, true)
  var ob = this.$observer
  ob.observe(key, val)
  ob.convert(key, val)
  ob.emit('add:self', key, val)
  ob.propagate('add', key, val)
})

_.define(objectAgumentations, '$delete', function (key) {
  if (!this.hasOwnProperty(key)) return
  delete this[key]
  var ob = this.$observer
  ob.emit('delete:self', key)
  ob.propagate('delete', key)
})
複製程式碼

  相比於arrayAugmentationsobjectAgumentations內部實現則簡單的多,objectAgumentations新增了兩個方法: $add$delete

  $add用於給物件新增新的屬性,如果該物件之前就存在鍵值為key的屬性則不做任何操作,否則首先使用_.define賦值該屬性,然後呼叫ob.observe目的是遞迴呼叫使得val值響應化。而convert函式的作用是將該屬性轉換成訪問器屬性getter/setter使得屬性被訪問或者被改變的時候我們能夠監聽到,具體我可以看一下convert函式的內部實現:   

p.convert = function (key, val) {
  var ob = this
  Object.defineProperty(ob.value, key, {
    enumerable: true,
    configurable: true,
    get: function () {
      if (Observer.emitGet) {
        ob.propagate('get', key)
      }
      return val
    },
    set: function (newVal) {
      if (newVal === val) return
      ob.unobserve(val)
      val = newVal
      ob.observe(key, newVal)
      ob.emit('set:self', key, newVal)
      ob.propagate('set', key, newVal)
    }
  })
}
複製程式碼

  convert函式的內部實現也不復雜,在get函式中,如果開啟了全域性的Observer.emitGet開關,在該屬性被訪問的時候,會對呼叫propagate觸發本身以及父級的對應get事件。在set函式中,首先呼叫unobserve對之間的值接觸響應化,接著呼叫ob.observe使得新賦值的資料響應化。最後首先觸發本身的set:self事件,接著呼叫propagate觸發本身以及父級的對應set事件。

  $delete用於給刪除物件的屬性,如果不存在該屬性則直接退出,否則先用delete操作符刪除物件的屬性,然後對外觸發本身的delete:self事件,接著呼叫delete觸發本身以及父級對應的delete事件。

  看完了objectAgumentations之後,我們在Observer建構函式中知道,如果傳入的引數中存在op.doNotAlterProto意味著不要改變物件的原型,則採用deepMixin函式將$add$delete函式新增到物件中,否則採用函式arguments函式將$add$delete新增到物件的原型中。最後呼叫了walk函式,讓我們看看walk是內部是怎麼實現的:   

p.walk = function (obj) {
  var key, val, descriptor, prefix
  for (key in obj) {
    prefix = key.charCodeAt(0)
    if (
      prefix === 0x24 || // $
      prefix === 0x5F    // _
    ) {
      continue
    }
    descriptor = Object.getOwnPropertyDescriptor(obj, key)
    // only process own non-accessor properties
    if (descriptor && !descriptor.get) {
      val = obj[key]
      this.observe(key, val)
      this.convert(key, val)
    }
  }
}
複製程式碼

  首先遍歷obj中的各個屬性,如果是以$或者_開頭的屬性名,則不做處理。接著獲取該屬性的描述符,如果不存在get函式,則對該屬性值呼叫observe函式,使得資料響應化,然後呼叫convert函式將該屬性轉換成訪問器屬性getter/setter使得屬性被訪問或者被改變的時候能被夠監聽。   

總結

  到此為止,我們已經看完了整個Observer模組的所有程式碼,其實基本原理和我們之前設想都是差不多的,只不過Vue程式碼中各個函式分解粒度非常小,使得程式碼邏輯非常清晰。看到這裡,我推薦你也clone一份Vue原始碼,checkout到對應的版本號,自己閱讀一遍,跑跑測試用例,打個斷點試著除錯一下,應該會對你理解這個模組有所幫助。

  最後如果對這個系列的文章感興趣歡迎大家關注我的Github部落格算是對我鼓勵,感謝大家的支援!      

Vue響應式資料: Observer模組實現

相關文章