Vue源碼實(shí)現(xiàn)--依賴(lài)收集(1)

最近閑來(lái)在比較深入的學(xué)習(xí)vue的源碼,受益匪淺,在這邊記錄一些心得,順便給自己定個(gè)小目標(biāo)--自己實(shí)現(xiàn)一個(gè)簡(jiǎn)單的vue框架,不考慮錯(cuò)誤檢查,不考慮邊界情況,包含vue最主要最基礎(chǔ)的流程。
第一篇文章,就來(lái)簡(jiǎn)單說(shuō)說(shuō)vue的依賴(lài)收集。
首先,何為依賴(lài)收集?
用例1:我們使用vue的時(shí)候,經(jīng)常會(huì)用到$watch方法去監(jiān)聽(tīng)屬性的改變,比如:

app.$watch('a', function () {
  console.log('a is change')
})

用例2:再比如vue框架幫我們做的數(shù)據(jù)與視圖的綁定,每次改變數(shù)據(jù)的時(shí)候,頁(yè)面也會(huì)跟著刷新,這些都和vue的依賴(lài)收集機(jī)制有著密切的關(guān)系。
從$watch方法說(shuō)起,現(xiàn)在有一個(gè)obj={a:1},要怎樣監(jiān)測(cè)到obj.a的改變,就需要使用Object.defineProperty為a這個(gè)屬性設(shè)置get方法,在get方法里面就能知道a的改變了。
首先先把vm.data里的數(shù)據(jù)代理到vm下:

  function initData (vm) {
    var data = vm.$options.data
    // 通常每個(gè)data我們都是通過(guò)一個(gè)工廠方法返回一個(gè)新對(duì)象,避免重復(fù)實(shí)例化導(dǎo)致幾個(gè)實(shí)例用了同一個(gè)data,因此這里要執(zhí)行這個(gè)方法獲得data對(duì)象
    data = vm._data = typeof data === 'function' ? data.call(vm) : data || {}
    var keys = Object.keys(data)
    var i = keys.length
    while(i--) {
      proxy(vm, keys[i]) 
    }
    observer(data)
  }
  function proxy (vm, key) {
    Object.defineProperty(vm, key, {
      configurable: true,
      enumerable: true,
      get: function () {
        return vm._data[key]
      },
      set: function (val) {
        vm._data[key] = val
      }
    })
  }

然后需要一個(gè)Observer類(lèi),Observer可直譯為觀察者(這里的觀察者和觀察者模式并不一樣,不可混淆),Observer類(lèi)做的事情就是使每個(gè)屬性可被觀測(cè),并添加上ob這個(gè)屬性:

  /* Observe */
  var Observer = (function () {
    var Observer = function (data) {
      Object.defineProperty(data, '__ob__', {
        enumerable: false, 
        value: this
      })
      this.walk(data)
    }
    Observer.prototype.walk = function (data) {
      var keys = Object.keys(data)
      var i = keys.length
      while (i--) {
        defineReactive(data, keys[i], data[keys[i]])
      }
    }
    return Observer
  })()
  function defineReactive (data, key, val) {
    observer(val)
    var dep = new Dep()
    Object.defineProperty(data, key, {
      get: function () {
        if (Dep.target) {
          dep.addSub(Dep.target)
        }
        return val
      },
      set: function (newVal) {
        if (val === newVal) {
          return
        }
        val = newVal
        dep.notify()
      }
    })
  }
  function observer (data) {
    if (typeof data !== 'object') {
      return
    }
    new Observer(data)
  }

在initData方法中調(diào)用observer方法。上面的代碼會(huì)為每一個(gè)被Observer的屬性在閉包中定義一個(gè)dep實(shí)例,這個(gè)dep在getter被調(diào)用的時(shí)候如果存在Dep.target,就會(huì)去收集當(dāng)前的Dep.target到閉包中dep實(shí)例的subs數(shù)組中
上面的代碼中可以看到,還有一個(gè)Dep類(lèi),Dep負(fù)責(zé)收集與該屬性有依賴(lài)關(guān)系的Watcher,代碼很簡(jiǎn)單:

  /*Dep*/
  var Dep = (function () {
    var Dep = function () {
      this.subs = []
    }
    // Dep.target是一個(gè)Watcher
    Dep.target = null
    Dep.prototype.addSub = function (sub) { // sub為Watcher
      this.subs.push(sub)
    }
    Dep.prototype.notify = function () {
      for (var i = 0, l=this.subs.length; i < l; i++) {
        this.subs[i].update()
      }
    }
    return Dep
  })()

Dep是一個(gè)典型的發(fā)布訂閱模式,有個(gè)subs數(shù)組,是一個(gè)Watcher數(shù)組,用來(lái)收集所有這個(gè)屬性上的訂閱者,當(dāng)這個(gè)屬性發(fā)生變化時(shí)候,那么在屬性的setter里就可以獲取到這個(gè)變化,然后調(diào)用閉包中維護(hù)的dep實(shí)例的dep.notify方法調(diào)用所有的訂閱者。
那么剩下的就是Watcher這個(gè)訂閱者了,很多地方可能都會(huì)稱(chēng)為觀察者,但是在這里為了避免和Observer類(lèi)混淆,我就稱(chēng)為訂閱者:

  /*Watcher*/
  var Watcher = (function () {
    var Watcher = function (vm, expOrFn, cb, options) {
      if (typeof expOrFn === 'string') {
        this.getter = function () {
          return vm[expOrFn]
        }
      } else if (typeof expOrFn === 'function') {
        // todo
      }
      this.vm = vm
      this.cb = cb
      this.value = this.get()
    }
    Watcher.prototype.get = function () {
      Dep.target = this
      let value = this.getter.call(this.vm)
      Dep.target = null
      return value
    }
    Watcher.prototype.update = function () {
      // todo
      this.cb()
    }
    return Watcher
  })()

Watcher的構(gòu)造函數(shù)接收這幾個(gè)參數(shù),vm(vue實(shí)例),expOrFn(要被訂閱的屬性),cb(回調(diào)方法),還有options(配置),
在這里expOrFn是被觀察的對(duì)象上的一個(gè)屬性名稱(chēng),是一個(gè)字符串;但是在我們之前說(shuō)的依賴(lài)收集用例2中,數(shù)據(jù)與視圖的綁定時(shí)候的依賴(lài)收集,這里的expOrFn就會(huì)直接是一個(gè)updateComponent方法,所以在這里統(tǒng)一把屬性也轉(zhuǎn)成一個(gè)方法this.getter。
然后調(diào)用Watcher的get方法,this.value = this.get(),
在get方法中先把Dep的靜態(tài)屬性target設(shè)置為當(dāng)前的實(shí)例,然后執(zhí)行this.getter方法,此時(shí),屬性的getter方法中就會(huì)執(zhí)行:

if (Dep.target) {
     dep.addSub(Dep.target)
}

這樣就把這個(gè)Watcher實(shí)例收集到了閉包的dep中了
此時(shí)執(zhí)行如下代碼,就會(huì)按我們預(yù)想的,輸出a is change和a is change2了

    var app = new Vue({
      data: function () {
        return {
          a: 1,
          b: {
            c: 2
          }
        }
      }
    })
    app.$watch('a', function () {
      console.log('a is change')
    })
    app.$watch('a', function () {
      console.log('a is change2')
    })
    app.a = 3

總結(jié)一下基本的原理:
1.首先observer一個(gè)對(duì)象,使其中的參數(shù)都有g(shù)et,set方法;
2.watch一個(gè)屬性的時(shí)候,new 一個(gè)Watcher實(shí)例,并把Dep.target設(shè)置為當(dāng)前的Watcher實(shí)例,然后獲取一次該屬性的值,觸發(fā)get方法中的依賴(lài)收集。
另外附上本篇完整的例子 附件
上面就是一個(gè)簡(jiǎn)單的依賴(lài)收集的原理,如果去看vue的源碼,就會(huì)發(fā)現(xiàn)復(fù)雜很多,有很多代碼很多屬性我這里并沒(méi)有展示出來(lái),這也是我看源碼時(shí)候的一些疑問(wèn):
比如:
1.數(shù)組的改變要怎樣被依賴(lài)收集
2.Computed屬性的Watch要怎樣實(shí)現(xiàn)
3.Observer的dep,和Watcher上的deps分別是做什么用的
4.Watcher中的user,lazy,deep,sync分別是什么用的
今天先寫(xiě)到這里,上面幾個(gè)問(wèn)題且看下面的幾篇博文

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

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

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