Vue 依賴收集原理分析

Vue 依賴收集原理分析

Vue實例在初始化時讼呢,可以接受以下幾類數(shù)據(jù):

  • 模板
  • 初始化數(shù)據(jù)
  • 傳遞給組件的屬性值
  • computed
  • watch
  • methods

Vue 根據(jù)實例化時接受的數(shù)據(jù)撩鹿,在將數(shù)據(jù)和模板轉化成DOM節(jié)點的同時,分析其依賴的數(shù)據(jù)吝岭。在特定數(shù)據(jù)改變時三痰,自動在下一個周期重新渲染DOM節(jié)點

本文主要分析Vue是如何進行依賴收集的吧寺。

Vue中窜管,與依賴收集相關的類有:

Dep : 一個訂閱者的列表類,可以增加或刪除訂閱者稚机,可以向訂閱者發(fā)送消息

Watcher : 訂閱者類幕帆。它在初始化時可以接受getter, callback兩個函數(shù)作為參數(shù)。getter用來計算Watcher對象的值赖条。當Watcher被觸發(fā)時失乾,會重新通過getter計算當前Watcher的值常熙,如果值改變,則會執(zhí)行callback.

對初始化數(shù)據(jù)的處理

對于一個Vue組件碱茁,需要一個初始化數(shù)據(jù)的生成函數(shù)裸卫。如下:

export default {
    data () {
        return {
            text: 'some texts',
            arr: [],
            obj: {}
        }
    }
}

Vue為數(shù)據(jù)中的每一個key維護一個訂閱者列表。對于生成的數(shù)據(jù)纽竣,通過Object.defineProperty對其中的每一個key進行處理墓贿,主要是為每一個key設置get, set方法,以此來為對應的key收集訂閱者蜓氨,并在值改變時通知對應的訂閱者聋袋。部分代碼如下:

  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set

  let childOb = observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
        }
        if (Array.isArray(value)) {
          dependArray(value)
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = observe(newVal)
      dep.notify()
    }
  })

每一key都有一個訂閱者列表
const dep = new Dep()

在為key進行賦值時,如果值發(fā)生了改變穴吹,則會通知所有的訂閱者
dep.notify()

在對key進行取值時幽勒,如果Dep.target有值,除正常的取值操作外會進行一些額外的操作來添加訂閱者港令。大多數(shù)時間里啥容,Dep.target的值都為null,只有訂閱者在進行訂閱操作時缠借,Dep.target才有值干毅,為正在進行訂閱的訂閱者。此時進行取值操作泼返,會將訂閱者加入到對應的訂閱者列表中硝逢。

訂閱者在進行訂閱操作時,主要包含以下3個步驟:

  • 將自己放在Dep.target
  • 對自己依賴的key進行取值
  • 將自己從Dep.target移除

在執(zhí)行訂閱操作后绅喉,訂閱者會被加入到相關key的訂閱者列表中渠鸽。

針對對象和數(shù)組的處理

如果為key賦的值為對象:

  • 會遞歸地對這個對象中的每一key進行處理

如果為key賦的值為數(shù)組:

  • 遞歸地對這個數(shù)組中的每一個對象進行處理
  • 重新定義數(shù)組的push,pop,shift,unshift,splice,sort,reverse方法,調用以上方法時key的訂閱者列表會通知訂閱者們“值已改變”柴罐。如果調用的是push,unshift,splice方法徽缚,遞歸處理新增加的項

對模板的處理

Vue將模板處理成一個render函數(shù)。需要重新渲染DOM時革屠,render函數(shù)結合Vue實例中的數(shù)據(jù)生成一個虛擬節(jié)點凿试。新的虛擬節(jié)點和原虛擬節(jié)點進行對比,對需要修改的DOM節(jié)點進行修改似芝。

訂閱者

訂閱者在初始化時主要接受2個參數(shù)getter, callback那婉。getter用來計算訂閱者的值,所以其在執(zhí)行時會對訂閱者所有需要訂閱的key進行取值党瓮。訂閱者的訂閱操作主要是通過getter來實現(xiàn)详炬。

部分代碼如下:

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    if (this.user) {
      try {
        value = this.getter.call(vm, vm)
      } catch (e) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      }
    } else {
      value = this.getter.call(vm, vm)
    }
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
      traverse(value)
    }
    popTarget()
    this.cleanupDeps()
    return value
  }

主要步驟:

  • 將自己放在Dep.target上(pushTarget(this))
  • 執(zhí)行getter(this.getter.call(vm, vm))
  • 將自己從Dep.target移除(popTarget())
  • 清理之前的訂閱(this.cleanupDeps())

此后,訂閱者在依賴的key的值發(fā)生變化會得到通知寞奸。獲得通知的訂閱者并不會立即被觸發(fā)呛谜,而是會被加入到一個待觸發(fā)的數(shù)組中在跳,在下一個周期統(tǒng)一被觸發(fā)。

訂閱者在被觸發(fā)時隐岛,會執(zhí)行getter來計算訂閱者的值猫妙,如果值改變,則會執(zhí)行callback.

負責渲染DOM的訂閱者

Vue實例化后都會生成一個用于渲染DOM的訂閱者聚凹。此訂閱者在實例化時傳入的getter方法為渲染DOM的方法吐咳。

部分代碼如下:

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}
    
vm._watcher = new Watcher(vm, updateComponent, noop) 

vm._render()結合模板和數(shù)據(jù),計算出虛擬DOM
vm._update()根據(jù)虛擬DOM渲染真實的DOM節(jié)點

此訂閱者在初始化時就會進行訂閱操作元践。實例化時傳入的getterupdateComponent韭脊。其中的vm._render()在執(zhí)行時一定會對所有依賴的key進行取值,能完成對依賴的key的訂閱单旁。同時vm._update()完成了第一次DOM渲染沪羔。當前依賴的key的值發(fā)生變化,訂閱者被觸發(fā)時象浑,作為getterupdateComponent會重新執(zhí)行蔫饰,重新渲染DOM。因為getter返回的值一直為undefined,所以此訂閱者中的callback并沒有被用到愉豺,于是傳入了一個空函數(shù)noop作為callback

對computed的處理

通過computed可以定義一組計算屬性篓吁,通過計算屬性可以將一些復雜的計算過程抽離出來,保持模板的簡單和清晰蚪拦。

代碼示例:

export default {
    data () {
        return {
            text: 'some texts',
            arr: [],
            obj: {}
        }
    },
    computed: {
        key1: function () {
            return this.text + this.arr.length
        }
    }
}

在定義一個計算屬性時杖剪,需要定義一個key和一個計算方法。

Vue在對computed進行處理時驰贷,會為每一個計算屬性生成一個lazy狀態(tài)的訂閱者盛嘿。普通的訂閱者在實例化和觸發(fā)時會執(zhí)行getter來計算自身的值和進行訂閱操作。而lazy狀態(tài)的訂閱者在上述情況下只會將自身置為dirty狀態(tài)括袒,不進行其它操作次兆。在訂閱者執(zhí)行自身的evaluate方法時,會清除自身的dirty狀態(tài)并執(zhí)行getter來計算自身的值和進行訂閱锹锰。

Vue在為計算屬性生成訂閱者時的示例代碼如下:

const computedWatcherOptions = { lazy: true }

// create internal watcher for the computed property.
watchers[key] = new Watcher(vm, getter, noop, computedWatcherOptions)

傳入的getter為自定義的計算方法芥炭,callback為空函數(shù)。(lazy狀態(tài)的訂閱者永遠都沒有機會執(zhí)行callback)

Vue 在自身實例上為指定key定義get方法恃慧,使可以通過Vue實例獲取計算屬性的值园蝠。

部分代碼如下:

function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) {
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}

在對計算屬性定義的key進行取值時,會首先獲取之前生成好的訂閱者糕伐。只有訂閱者處于dirty狀態(tài)時砰琢,才會執(zhí)行evaluate計算訂閱者的值蘸嘶。所以為計算屬性定義的計算方法只有在對計算屬性的key進行取值并且計算屬性依賴的key曾經(jīng)改變時才會執(zhí)行良瞧。

假如對上文定義的計算屬性key1進行取值

vm.key1; //第一次取值陪汽,自定義計算方法執(zhí)行
vm.key1; //第二次取值,依賴的key的值沒有變化褥蚯,自定義計算方法不會執(zhí)行
vm.text = '' //改變計算屬性依賴的key的值挚冤,計算屬性對應的訂閱者會進入dirty狀態(tài),自定義計算方法不會執(zhí)行
vm.key1; //第三次取值赞庶,計算屬性依賴的key的值發(fā)生了變化并且對計算屬性進行取值训挡,自定義的計算方法執(zhí)行
訂閱計算屬性值的變化

計算屬性的key不會維護一個訂閱者列表,也不能通過計算屬性的set方法在觸發(fā)所有訂閱者歧强。(計算屬性不能被賦值)澜薄。一個訂閱者執(zhí)行訂閱操作來訂閱計算屬性值的變化其實是訂閱了計算屬性依賴的key的值的變化。
在計算屬性的get方法中

if (Dep.target) {
    watcher.depend()
}

如果有訂閱者來訂閱計算屬性的變化摊册,計算屬性會將自己的訂閱復制到正在進行訂閱的訂閱者上肤京。watcher.depend()的作用就是如此。

例如:

//初始化訂閱者watcher, 依賴計算屬性key1
var watcher = new Watcher(function () {
    return vm.key1
}, noop)

vm.text = '' //計算屬性key1依賴的text的值發(fā)生變化茅特,watcher會被觸發(fā)

對watch的處理

Vue實例化時可以傳入watch對象忘分,來監(jiān)聽某些值的變化。
例如:

export default {
    watch: {
        'a.b.c': function (val, oldVal) {
            console.log(val)
            console.log(oldVal)
        }
    }
}

Vue 會為watch中的每一項生成一個訂閱者白修。訂閱者的getter通過處理字符串得到妒峦。如'a.b.c'會被處理成

function (vm) {
    var a = vm.a
    var b = a.b
    var c = b.c
    return c
}

處理字符串的源碼如下:

/**
 * Parse simple path.
 */
const bailRE = /[^\w.$]/
export function parsePath (path: string): any {
  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]]
    }
    return obj
  }
}

訂閱者的callback為定義watch時傳入的監(jiān)聽函數(shù)。當訂閱者被觸發(fā)時兵睛,如果訂閱者的值發(fā)生變化肯骇,則會執(zhí)行callbackcallback執(zhí)行時會傳入變化后的值祖很,變化前的值作為參數(shù)累盗。


最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市突琳,隨后出現(xiàn)的幾起案子若债,更是在濱河造成了極大的恐慌,老刑警劉巖拆融,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蠢琳,死亡現(xiàn)場離奇詭異,居然都是意外死亡镜豹,警方通過查閱死者的電腦和手機傲须,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來趟脂,“玉大人泰讽,你說我怎么就攤上這事。” “怎么了已卸?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵佛玄,是天一觀的道長。 經(jīng)常有香客問我累澡,道長梦抢,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任愧哟,我火速辦了婚禮奥吩,結果婚禮上,老公的妹妹穿的比我還像新娘蕊梧。我一直安慰自己霞赫,他們只是感情好,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布肥矢。 她就那樣靜靜地躺著绩脆,像睡著了一般。 火紅的嫁衣襯著肌膚如雪橄抹。 梳的紋絲不亂的頭發(fā)上靴迫,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天,我揣著相機與錄音楼誓,去河邊找鬼玉锌。 笑死,一個胖子當著我的面吹牛疟羹,可吹牛的內容都是我干的主守。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼榄融,長吁一口氣:“原來是場噩夢啊……” “哼参淫!你這毒婦竟也來了?” 一聲冷哼從身側響起愧杯,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤涎才,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后力九,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體耍铜,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年跌前,在試婚紗的時候發(fā)現(xiàn)自己被綠了棕兼。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡抵乓,死狀恐怖伴挚,靈堂內的尸體忽然破棺而出靶衍,到底是詐尸還是另有隱情,我是刑警寧澤茎芋,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布颅眶,位于F島的核電站,受9級特大地震影響败徊,放射性物質發(fā)生泄漏。R本人自食惡果不足惜掏缎,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一皱蹦、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧眷蜈,春花似錦沪哺、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至忌怎,卻和暖如春籍滴,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背榴啸。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工孽惰, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人鸥印。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓勋功,卻偏偏與公主長得像,于是被迫代替她去往敵國和親库说。 傳聞我的和親對象是個殘疾皇子狂鞋,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內容