Vue0.11版本源碼閱讀系列二:數(shù)據(jù)觀察

上篇介紹了創(chuàng)建vue實(shí)例時(shí)大概做了一些什么事情,其中有一項(xiàng)是初始化數(shù)據(jù),本篇來看一下數(shù)據(jù)觀察具體是怎么做的。

_initData就是數(shù)據(jù)觀察的起點(diǎn)了:

exports._initData = function () {
  // 代理data到實(shí)例
  var data = this._data
  var keys = Object.keys(data)
  var i = keys.length
  var key
  while (i--) {
    key = keys[i]
    if (!_.isReserved(key)) {
      this._proxy(key)
    }
  }
  // 觀察data數(shù)據(jù)
  Observer.create(data).addVm(this)
}

_proxy方法上一篇已經(jīng)說過了,就是把data數(shù)據(jù)代理到vue實(shí)例上,可以通過this.xx訪問到this.data.xx的數(shù)據(jù),關(guān)鍵是Observer。

createObserver類的靜態(tài)方法,用來給一個數(shù)組或?qū)ο髣?chuàng)建觀察對象:

Observer.create = function (value) {
  if (
    value &&
    value.hasOwnProperty('__ob__') &&
    value.__ob__ instanceof Observer
  ) {
    return value.__ob__
  } else if (_.isArray(value)) {
    return new Observer(value, ARRAY)
  } else if (
    _.isPlainObject(value) &&
    !value._isVue
  ) {
    return new Observer(value, OBJECT)
  }
}

從這里可以知道vue只會對數(shù)組和純粹的對象進(jìn)行觀察,其他比如函數(shù)什么的是不會觀察的,其主要邏輯是判斷該屬性是否已經(jīng)觀察過了,是的話就返回觀察者對象,否則分別對數(shù)組和對象使用不同的標(biāo)志來實(shí)例化觀察對象。

來看Observer類:

function Observer (value, type) {
  this.id = ++uid
  this.value = value
  this.deps = []
  // 將該觀察實(shí)例設(shè)置到該對象或數(shù)組的一個屬性,方便后面檢查和使用
  _.define(value, '__ob__', this)
  if (type === ARRAY) {// 數(shù)組分支
    var augment = _.hasProto
      ? protoAugment
      : copyAugment
    augment(value, arrayMethods, arrayKeys)
    this.observeArray(value)
  } else if (type === OBJECT) {// 對象分支
    this.walk(value)
  }
}

初始化了一些屬性,先看一下比較簡單的對象分支:

p.walk = function (obj) {
  var keys = Object.keys(obj)
  var i = keys.length
  var key, prefix
  while (i--) {
    key = keys[i]
    prefix = key.charCodeAt(0)
    if (prefix !== 0x24 && prefix !== 0x5F) { // 跳過 $ or _開頭的私有屬性
      this.convert(key, obj[key])
    }
  }
}

walk方法對對象的每個子屬性遍歷調(diào)用convert方法:

p.convert = function (key, val) {
  var ob = this
  // 如果該屬性的值也是個數(shù)組或?qū)ο?,那么也需要進(jìn)行觀察,observe方法最終調(diào)用的也是Object.create方法
  var childOb = ob.observe(val)
  // 每個屬性都會創(chuàng)建一個依賴收集實(shí)例,利用閉包來保存
  var dep = new Dep()
  // 該屬性的觀察實(shí)例添加到屬性值的觀察對象里
  if (childOb) {
    childOb.deps.push(dep)
  }
  Object.defineProperty(ob.value, key, {
    enumerable: true,
    configurable: true,
    get: function () {
      // 這里進(jìn)行收集依賴,Observer.target是一個全局屬性,是一個watcher實(shí)例,后續(xù)再細(xì)說,當(dāng)引用該屬性前把watcher實(shí)例賦值給這個全局屬性,此處就能引用到,然后收集到該屬性的dep實(shí)例列表里
      if (Observer.target) {
        Observer.target.addDep(dep)
      }
      return val
    },
    set: function (newVal) {
      if (newVal === val) return
      // 如果舊的值是對象或數(shù)組那么肯定也有對應(yīng)的觀察實(shí)例,所以需要從對應(yīng)的觀察實(shí)例里移除該屬性的dep
      var oldChildOb = val && val.__ob__
      if (oldChildOb) {
        var oldDeps = oldChildOb.deps
        oldDeps.splice(oldDeps.indexOf(dep), 1)
      }
      val = newVal
      // 檢查新值,新賦的值是對象或數(shù)組又需要進(jìn)行遞歸創(chuàng)建觀察實(shí)例
      var newChildOb = ob.observe(newVal)
      if (newChildOb) {
        newChildOb.deps.push(dep)
      }
      // 通知該屬性的依賴進(jìn)行更新
      dep.notify()
    }
  })
}

接下來看一下數(shù)組的分支:

if (type === ARRAY) {
    var augment = _.hasProto
    ? protoAugment
    : copyAugment
    augment(value, arrayMethods, arrayKeys)
    this.observeArray(value)
}

vue修改了數(shù)組原型上的一些方法,比如:push、shift等等,原因是使用這些方法操作數(shù)組不會觸發(fā)該屬性的setter,所以vue就無法檢測到變化進(jìn)行更新,所以需要攔截這些方法進(jìn)行修改。

這里使用了兩種方法,如果瀏覽器支持__proto__,直接通過修改數(shù)組的__proto__來設(shè)置新的原型對象,如果不支持,則使用Object.defineProperty來覆蓋添加修改后的數(shù)組方法。

var arrayProto = Array.prototype
// 創(chuàng)建一個以數(shù)組原型對象為原型的新對象
var arrayMethods = Object.create(arrayProto)
;[
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
.forEach(function (method) {
  // 緩存數(shù)組的原始方法
  var original = arrayProto[method]
  _.define(arrayMethods, method, function mutator () {
    //  這里將arguments拷貝了一份,避免將該對象直接傳遞給其他函數(shù)使用,可能對性能不利
    var i = arguments.length
    var args = new Array(i)
    while (i--) {
      args[i] = arguments[i]
    }
    // 調(diào)用原始方法
    var result = original.apply(this, args)
    // 獲取該數(shù)組的觀察實(shí)例
    var ob = this.__ob__
    // 獲取新插入數(shù)組的值
    var inserted
    switch (method) {
      case 'push':
        inserted = args
        break
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    // 如果有新插入的值,那么對它遞歸進(jìn)行觀察
    if (inserted) ob.observeArray(inserted)
    // 通知依賴更新
    ob.notify()
    return result
  })
})

邏輯很簡單,就是當(dāng)調(diào)用了這些方法更新數(shù)組后觀察新插入的數(shù)據(jù),以及通知更新,這里是調(diào)用觀察對象ob的更新方法notify

p.notify = function () {
  var deps = this.deps
  for (var i = 0, l = deps.length; i < l; i++) {
    deps[i].notify()
  }
}

通過上面的convert方法我們知道這個deps數(shù)組里收集的是該屬性值對應(yīng)的屬性的依賴收集實(shí)例dep,有點(diǎn)繞:

{
    data: {
        a: [1, 2, 3],
        b: {
            c: [4, 5, 6]
        }
    }
}

比如這個例子,忽略b的話,一共存在兩個Observer實(shí)例,一個是屬性data的值的,另一個是 [1, 2, 3]的, [1, 2, 3]Observer實(shí)例的deps數(shù)組收集了adep,我們使用上述數(shù)組的方法更新了這個數(shù)組,會通知adep進(jìn)行更新通知,這很容易理解,如果我們給a設(shè)置了新值,比如:data.a = 2是會觸發(fā)asetter的,里面會調(diào)用adepnotify方法,只是現(xiàn)在這個a的值變成了數(shù)組,數(shù)組變化了就相當(dāng)于a變化了,但問題是數(shù)組變化并不會觸發(fā)asetter,所以就只能手動去調(diào)用adep的更新方法去通知a的依賴也去更新,但是,比如c的數(shù)組變化了,會通知c的依賴更新,但是不會向上再去通知b的依賴更新。

數(shù)組的原型方法修改完后就需要去遍歷該數(shù)組的元素進(jìn)行觀察:

p.observeArray = function (items) {
  var i = items.length
  while (i--) {
    this.observe(items[i])
  }
}

很簡單,遍歷數(shù)組調(diào)用observe方法。

到這里,就完成了對data上所有數(shù)據(jù)的觀察了,總結(jié)一下,從data對象開始,給該對象創(chuàng)建一個觀察實(shí)例,然后遍歷它的子屬性,值是數(shù)組或?qū)ο蟮脑捰謩?chuàng)建對應(yīng)的觀察實(shí)例,然后再繼續(xù)遍歷它們的子屬性,繼續(xù)遞歸,直到把每個屬性都轉(zhuǎn)換成gettersetter

在第一次渲染的時(shí)候會引用用到的值,也就是會觸發(fā)對應(yīng)屬性的getter,引用前會把對應(yīng)的watcher賦值到Observer.target屬性,JavaScript代碼執(zhí)行是單線程的,所以同一時(shí)刻只會有一個Observer.target,所以只要某個屬性的getter里獲取到了此刻的Observer.target,那一定代表該watcher是依賴該屬性的,那么就添加到該屬性的依賴收集對象dep里,這里巧妙的使用閉包來保存每個屬性的dep實(shí)例,后續(xù)如果該屬性值變化了,那么會觸發(fā)setter,如果新賦值是對象或數(shù)組又會遞歸進(jìn)行觀察,最后再通知該屬性的所有依賴進(jìn)行更新。

上面一直都提到了這個dep,現(xiàn)在來看一下:

function Dep () {
  this.id = ++uid
  this.subs = []
}
var p = Dep.prototype
p.addSub = function (sub) {
  this.subs.push(sub)
}
p.removeSub = function (sub) {
  if (this.subs.length) {
    var i = this.subs.indexOf(sub)
    if (i > -1) this.subs.splice(i, 1)
  }
}
p.notify = function () {
  var subs = _.toArray(this.subs)
  for (var i = 0, l = subs.length; i < l; i++) {
    subs[i].update()
  }
}

這個類很簡單,這就是全部代碼,功能是收集訂閱者、刪除訂閱者以及遍歷調(diào)用訂閱者的update方法。

最后看一下修改數(shù)組和對象的輔助方法,如:$set、$remove等。

對于數(shù)組,直接使用索引設(shè)置數(shù)組項(xiàng)vue是不能檢測到的,所以提供了$set方法:

_.define(
  arrayProto,
  '$set',
  function $set (index, val) {
    if (index >= this.length) {
      this.length = index + 1
    }
    return this.splice(index, 1, val)[0]
  }
)

給數(shù)組的原型上添加了$set方法,調(diào)用splice方法來設(shè)置值,這個方法由于已經(jīng)被重寫過了,所以可以觸發(fā)更新,我們完全可以直接使用splice方法。

對于對象,在data初始化后在添加新屬性也是不能檢測到的,在0.11版本提供各了$add方法:

_.define(
  objProto,
  '$add',
  function $add (key, val) {
    if (this.hasOwnProperty(key)) return
    var ob = this.__ob__
    if (!ob || _.isReserved(key)) {
      this[key] = val
      return
    }
    ob.convert(key, val)
    if (ob.vms) {
      var i = ob.vms.length
      while (i--) {
        var vm = ob.vms[i]
        vm._proxy(key)
        vm._digest()
      }
    } else {
      ob.notify()
    }
  }
)

直接調(diào)用convert方法就可以了,設(shè)置完得通知更新,這里分了兩種情況,如果設(shè)置的是data的根屬性,那么需要把該屬性代理到vue實(shí)例上,另外需要通知該實(shí)例及其所有子實(shí)例的watcher進(jìn)行強(qiáng)制更新。如果不是根屬性,那么調(diào)用所在對象的觀察者實(shí)例的notify方法,通知對象對應(yīng)的屬性的訂閱者進(jìn)行更新。

數(shù)據(jù)觀察到這里就結(jié)束了,但是現(xiàn)在還不知道,依賴到底是什么時(shí)候才進(jìn)行收集的,Observer.target到底什么時(shí)候才會被賦值,如果數(shù)據(jù)更新了,watcher是什么,watcher又是怎么觸發(fā)DOM更新以及怎么更新,問題還有很多,咱們下回再見。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容