Vue原理解析(九):監(jiān)聽屬性watch和計(jì)算屬性computed實(shí)現(xiàn)原理

上一篇 Vue原理解析(八):一起搞明白令人頭疼的diff算法

之前的八個(gè)章節(jié),我們按照流程介紹了vue的初始化、虛擬Dom生成、虛擬Dom轉(zhuǎn)為真實(shí)Dom、深入理解響應(yīng)式以及diff算法等這些核心概念,對(duì)它內(nèi)部的實(shí)現(xiàn)原理做了分析,相信大家對(duì)vue已經(jīng)有了較深入的理解?,F(xiàn)在我們來進(jìn)一步豐富對(duì)vue的認(rèn)識(shí),開啟API系列之旅,介紹日常開發(fā)中經(jīng)常會(huì)使用到的API的實(shí)現(xiàn)原理,它們主要包括以下:

響應(yīng)式相關(guān)APIthis.$watchthis.$set、this.$delete

事件相關(guān)APIthis.$on、this.$offthis.$once、this.$emit

生命周期相關(guān)APIthis.$mountthis.$forceUpdate、this.$destroy

全局APIVue.extend、Vue.nextTick、Vue.set、Vue.delete、Vue.component、Vue.useVue.mixin、Vue.compileVue.version、Vue.directive、Vue.filter

  • 1. this.$watch

這個(gè)API是我們之前介紹響應(yīng)式時(shí)的Watcher類的一種封裝,也就是三種watcher中的user-watcher,監(jiān)聽屬性經(jīng)常會(huì)被這樣使用到:

export default {
  watch: {
    name(newName) {...}
  }
}

其實(shí)它只是this.$watch這個(gè)API的一種封裝:

export default {
  created() {
    this.$watch('name', newName => {...})
  }
}

監(jiān)聽屬性初始化

為什么這么說,我們首先來看下初始化時(shí)watch屬性都做了什么:

function initState(vm) {  // 初始化所有狀態(tài)時(shí)
  vm._watchers = []  // 當(dāng)前實(shí)例watcher集合
  const opts = vm.$options  // 合并后的屬性
  
  ... // 其他狀態(tài)初始化
  
  if(opts.watch) {  // 如果有定義watch屬性
    initWatch(vm, opts.watch)  // 執(zhí)行初始化方法
  }
}

---------------------------------------------------------

function initWatch (vm, watch) {  // 初始化方法
  for (const key in watch) {  // 遍歷watch內(nèi)多個(gè)監(jiān)聽屬性
    const handler = watch[key]  // 每一個(gè)監(jiān)聽屬性的值
    if (Array.isArray(handler)) {  // 如果該項(xiàng)的值為數(shù)組
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])  // 將每一項(xiàng)使用watcher包裝
      }
    } else {
      createWatcher(vm, key, handler) // 不是數(shù)組直接使用watcher
    }
  }
}

---------------------------------------------------------

function createWatcher (vm, expOrFn, handler, options) {
  if (isPlainObject(handler)) { // 如果是對(duì)象,參數(shù)移位
    options = handler  
    handler = handler.handler
  }
  if (typeof handler === 'string') {  // 如果是字符串,表示為方法名
    handler = vm[handler]  // 獲取methods內(nèi)的方法
  }
  return vm.$watch(expOrFn, handler, options)  // 封裝
}

以上對(duì)監(jiān)聽屬性的多種不同的使用方式,都做了處理。使用示例在官網(wǎng)上均可找到:watch示例,這里就不做過多的介紹了。可以看到最后是調(diào)用了vm.$watch方法。

監(jiān)聽屬性實(shí)現(xiàn)原理

所以我們來看下$watch的內(nèi)部實(shí)現(xiàn):

Vue.prototype.$watch = function(expOrFn, cb, options = {}) {
  const vm = this
  if (isPlainObject(cb)) {  // 如果cb是對(duì)象,當(dāng)手動(dòng)創(chuàng)建監(jiān)聽屬性時(shí)
    return createWatcher(vm, expOrFn, cb, options)
  }
  
  options.user = true  // user-watcher的標(biāo)志位,傳入Watcher類中
  const watcher = new Watcher(vm, expOrFn, cb, options)  // 實(shí)例化user-watcher
  
  if (options.immediate) {  // 立即執(zhí)行
    cb.call(vm, watcher.value)  // 以當(dāng)前值立即執(zhí)行一次回調(diào)函數(shù)
  }  // watcher.value為實(shí)例化后返回的值
  
  return function unwatchFn () {  // 返回一個(gè)函數(shù),執(zhí)行取消監(jiān)聽
    watcher.teardown()
  }
}

---------------------------------------------------------------

export default {
  data() {
    return {
      name: 'cc'
    }  
  },
  created() {
    this.unwatch = this.$watch('name', newName => {...})
    this.unwatch()  // 取消監(jiān)聽
  }
}

雖然watch內(nèi)部是使用this.$watch,但是我們也是可以手動(dòng)調(diào)用this.$watch來創(chuàng)建監(jiān)聽屬性的,所以第二個(gè)參數(shù)cb會(huì)出現(xiàn)是對(duì)象的情況。接下來設(shè)置一個(gè)標(biāo)記位options.usertrue,表明這是一個(gè)user-watcher再給watch設(shè)置了immediate屬性后,會(huì)將實(shí)例化后得到的值傳入回調(diào),并立即執(zhí)行一次回調(diào)函數(shù),這也是immediate的實(shí)現(xiàn)原理。最后的返回值是一個(gè)方法,執(zhí)行后可以取消對(duì)該監(jiān)聽屬性的監(jiān)聽。接下來我們看看user-watcher是如何定義的:

class Watcher {
  constructor(vm, expOrFn, cb, options) {
    this.vm = vm
    vm._watchers.push(this)  // 添加到當(dāng)前實(shí)例的watchers內(nèi)
    
    if(options) {
      this.deep = !!options.deep  // 是否深度監(jiān)聽
      this.user = !!options.user  // 是否是user-wathcer
      this.sync = !!options.sync  // 是否同步更新
    }
    
    this.active = true  // // 派發(fā)更新的標(biāo)志位
    this.cb = cb  // 回調(diào)函數(shù)
    
    if (typeof expOrFn === 'function') {  // 如果expOrFn是函數(shù)
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)  // 如果是字符串對(duì)象路徑形式,返回閉包函數(shù)
    }
    
    ...
    
  }
}

當(dāng)是user-watcher時(shí),Watcher內(nèi)部是以上方式實(shí)例化的,通常情況下我們是使用字符串的形式創(chuàng)建監(jiān)聽屬性,所以首先來看下parsePath方法是干什么的:

const bailRE = /[^\w.$]/  // 得是對(duì)象路徑形式,如info.name

function parsePath (path) {
  if (bailRE.test(path)) return // 不匹配對(duì)象路徑形式,再見
  
  const segments = path.split('.')  // 按照點(diǎn)分割為數(shù)組
  
  return function (obj) {  // 閉包返回一個(gè)函數(shù)
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      obj = obj[segments[i]]  // 依次讀取到實(shí)例下對(duì)象末端的值
    }
    return obj
  }
}

parsePath方法最終返回一個(gè)閉包方法,此時(shí)Watcher類中的this.getter就是一個(gè)函數(shù)了,再執(zhí)行this.get()方法時(shí)會(huì)將this.vm傳入到閉包內(nèi),補(bǔ)全Watcher其他的邏輯:

class Watcher {
  constructor(vm, expOrFn, cb, options) {
    
    ...
    this.getter = parsePath(expOrFn)  // 返回的方法
    
    this.value = this.get()  // 執(zhí)行g(shù)et
  }
  
  get() {
    pushTarget(this)  // 將當(dāng)前user-watcher實(shí)例賦值給Dep.target,讀取時(shí)收集它
    
    let value = this.getter.call(this.vm, this.vm)  // 將vm實(shí)例傳給閉包,進(jìn)行讀取操作
    
    if (this.deep) {  // 如果有定義deep屬性
      traverse(value)  // 進(jìn)行深度監(jiān)聽
    }
    
    popTarget()
    return value  // 返回閉包讀取到的值,參數(shù)immediate使用的就是這里的值
  }
  
  ...
  
}

因?yàn)橹俺跏蓟呀?jīng)將狀態(tài)已經(jīng)全部都代理到了this下,所以讀取this下的屬性即可,比如:

export default {
  data() {  // data的初始化先與watch
    return {
      info: {
        name: 'cc'
      }
    }
  },
  created() {
    this.$watch('info.name', newName => {...})  // 何況手動(dòng)創(chuàng)建
  }
}

首先讀取this下的info屬性,然后讀取info下的name屬性。大家注意,這里我們使用了讀取這個(gè)動(dòng)詞,所以會(huì)執(zhí)行之前包裝data響應(yīng)式數(shù)據(jù)的get方法進(jìn)行依賴收集,將依賴收集到讀取到的屬性的dep里,不過收集的是user-watcher,get方法最后返回閉包讀取到的值。

之后就是當(dāng)info.name屬性被重新賦值時(shí),走派發(fā)更新的流程,我們這里把和render-watcher不同之處做單獨(dú)的說明,派發(fā)更新會(huì)執(zhí)行Watcher內(nèi)的update方法內(nèi):

class Watcher {
  constructor(vm, expOrFn, cb, options) {
    ...
  }
  
  update() {  // 執(zhí)行派發(fā)更新
    if(this.sync) {  // 如果有設(shè)置sync為true
      this.run()  // 不走nextTick隊(duì)列,直接執(zhí)行
    } else {
      queueWatcher(this)  // 否則加入隊(duì)列,異步執(zhí)行run()
    }
  }
  
  run() {
    if (this.active) {
      this.getAndInvoke(this.cb)  // 傳入回調(diào)函數(shù)
    }
  }
  
  getAndInvoke(cb) {
    const value = this.get()  // 重新求值
    
    if(value !== this.value || isObject(value) || this.deep) {
      const oldValue = this.value  // 緩存之前的值
      this.value = value  // 新值
      if(this.user) {  // 如果是user-watcher
        cb.call(this.vm, value, oldValue)  // 在回調(diào)內(nèi)傳入新值和舊值
      }
    }
  }
}

其實(shí)這里的sync屬性已經(jīng)沒在官網(wǎng)做說明了,不過我們看到源碼中還是保留了相關(guān)代碼。接下來我們看到為什么watch的回調(diào)內(nèi)可以得到新值和舊值的原理,因?yàn)?code>cb.call(this.vm, value, oldValue)這句代碼的原因,內(nèi)部將新值和舊值傳給了回調(diào)函數(shù)。

watch監(jiān)聽屬性示例:
<template>  
  <div>{{name}}</div>
</template>

export default {  // App組件
  data() {
    return {
      name: 'cc'
    }
  },
  watch: {
    name(newName, oldName) {...}  // 派發(fā)新值和舊值給回調(diào)
  },
  mounted() {
    setTimeout(() => {  
      this.name = 'ww'  // 觸發(fā)name的set
    }, 1000)
  }
}
image

監(jiān)聽屬性的deep深度監(jiān)聽原理

之前的get方法內(nèi)有說明,如果有deep屬性,則執(zhí)行traverse方法:

const seenObjects = new Set()  // 不重復(fù)添加

function traverse (val) {
  _traverse(val, seenObjects)
  seenObjects.clear()
}

function _traverse (val, seen) {
  let i, keys
  const isA = Array.isArray(val)  // val是否是數(shù)組
  
  if ((!isA && !isObject(val))  // 如果不是array和object
        || Object.isFrozen(val)  // 或者是已經(jīng)凍結(jié)對(duì)象
        || val instanceof VNode) {  // 或者是VNode實(shí)例
    return  // 再見
  }
  
  if (val.__ob__) {  // 只有object和array才有__ob__屬性
    const depId = val.__ob__.dep.id  // 手動(dòng)依賴收集器的id
    if (seen.has(depId)) {  // 已經(jīng)有收集過
      return  // 再見
    }
    seen.add(depId)  // 沒有被收集,添加
  }
  
  if (isA) {  // 是array
    i = val.length
    while (i--) {
      _traverse(val[i], seen)  // 遞歸觸發(fā)每一項(xiàng)的get進(jìn)行依賴收集
    }
  } 
  
  else {  // 是object
    keys = Object.keys(val)
    i = keys.length
    while (i--) {
      _traverse(val[keys[i]], seen)  // 遞歸觸發(fā)子屬性的get進(jìn)行依賴收集
    }
  }
}

看著還挺復(fù)雜,簡(jiǎn)單來說deep的實(shí)現(xiàn)原理就是遞歸的觸發(fā)數(shù)組或?qū)ο蟮?code>get進(jìn)行依賴收集,因?yàn)橹挥袛?shù)組和對(duì)象才有__ob__屬性,也就是我們第七章說明的手動(dòng)依賴管理器,將它們的依賴收集到Observer類里的dep內(nèi),完成deep深度監(jiān)聽。

watch總結(jié):這里說明了為什么watchthis.$watch的實(shí)現(xiàn)是一致的,以及簡(jiǎn)單解釋它的原理就是為需要觀察的數(shù)據(jù)創(chuàng)建并收集user-watcher,當(dāng)數(shù)據(jù)改變時(shí)通知到user-watcher將新值和舊值傳遞給用戶自己定義的回調(diào)函數(shù)。最后分析了定義watch時(shí)會(huì)被使用到的三個(gè)參數(shù):sync、immediatedeep它們的實(shí)現(xiàn)原理。簡(jiǎn)單說明它們的實(shí)現(xiàn)原理就是:sync是不將watcher加入到nextTick隊(duì)列而同步的更新、immediate是立即以得到的值執(zhí)行一次回調(diào)函數(shù)、deep是遞歸的對(duì)它的子值進(jìn)行依賴收集。

  • 2. this.$set

這個(gè)API已經(jīng)在第七章的最后做了具體分析,大家可以前往this.$set實(shí)現(xiàn)原理查閱。

  • 3. this.$delete

這個(gè)API也已經(jīng)在第七章的最后做了具體分析,大家可以前往this.$delete實(shí)現(xiàn)原理查閱。

  • 4. computed計(jì)算屬性

計(jì)算屬性不是API,但它是Watcher類的最后也是最復(fù)雜的一種實(shí)例化的使用,還是很有必要分析的。(vue版本2.6.10)其實(shí)主要就是分析計(jì)算屬性為何可以做到當(dāng)它的依賴項(xiàng)發(fā)生改變時(shí)才會(huì)進(jìn)行重新的計(jì)算,否則當(dāng)前數(shù)據(jù)是被緩存的。計(jì)算屬性的值可以是對(duì)象,這個(gè)對(duì)象需要傳入getset方法,這種并不常用,所以這里的分析還是介紹常用的函數(shù)形式,它們之間是大同小異的,不過可以減少認(rèn)知負(fù)擔(dān),聚焦核心原理實(shí)現(xiàn)。

export default {
  computed: {
    newName: {  // 不分析這種了~
      get() {...},  // 內(nèi)部會(huì)采用get屬性為計(jì)算屬性的值
      set() {...}
    }
  }
}

計(jì)算屬性初始化

function initState(vm) {  // 初始化所有狀態(tài)時(shí)
  vm._watchers = []  // 當(dāng)前實(shí)例watcher集合
  const opts = vm.$options  // 合并后的屬性
  
  ... // 其他狀態(tài)初始化
  
  if(opts.computed) {  // 如果有定義計(jì)算屬性
    initComputed(vm, opts.computed)  // 進(jìn)行初始化
  }
  ...
}

---------------------------------------------------------------------------

function initComputed(vm, computed) {
  const watchers = vm._computedWatchers = Object.create(null) // 創(chuàng)建一個(gè)純凈對(duì)象
  
  for(const key in computed) {
    const getter = computed[key]  // computed每項(xiàng)對(duì)應(yīng)的回調(diào)函數(shù)
    
    watchers[key] = new Watcher(vm, getter, noop, {lazy: true})  // 實(shí)例化computed-watcher
    
    ...
    
  }
}

計(jì)算屬性實(shí)現(xiàn)原理

這里還是按照慣例,將定義的computed屬性的每一項(xiàng)使用Watcher類進(jìn)行實(shí)例化,不過這里是按照computed-watcher的形式,來看下如何實(shí)例化的:

class Watcher{
  constructor(vm, expOrFn, cb, options) {
    this.vm = vm
    this._watchers.push(this)
    
    if(options) {
      this.lazy = !!options.lazy  // 表示是computed
    }
    
    this.dirty = this.lazy  // dirty為標(biāo)記位,表示是否對(duì)computed計(jì)算
    
    this.getter = expOrFn  // computed的回調(diào)函數(shù)
    
    this.value = undefined
  }
}

這里就點(diǎn)到為止,實(shí)例化已經(jīng)結(jié)束了。并沒有和之前render-watcher以及user-watcher那般,執(zhí)行get方法,這是為什么?我們接著分析為何如此,補(bǔ)全之前初始化computed的方法:

function initComputed(vm, computed) {
  ...
  
  for(const key in computed) {
    const getter = computed[key]  // // computed每項(xiàng)對(duì)應(yīng)的回調(diào)函數(shù)
    ...
    
    if (!(key in vm)) {
      defineComputed(vm, key, getter)
    }
    
    ... key不能和data里的屬性重名
    ... key不能和props里的屬性重名
  }
}

這里的App組件在執(zhí)行extend創(chuàng)建子組件的構(gòu)造函數(shù)時(shí),已經(jīng)將key掛載到vm的原型中了,不過之前也是執(zhí)行的defineComputed方法,所以不妨礙我們看它做了什么:

function defineComputed(target, key) {
  ...
  Object.defineProperty(target, key, {
    enumerable: true,
    configurable: true,
    get: createComputedGetter(key),
    set: noop
  })
}

這個(gè)方法的作用就是讓computed成為一個(gè)響應(yīng)式數(shù)據(jù),并定義它的get屬性,也就是說當(dāng)頁面執(zhí)行渲染訪問到computed時(shí),才會(huì)觸發(fā)get然后執(zhí)行createComputedGetter方法,所以之前的點(diǎn)到為止再這里會(huì)續(xù)上,看下get方法是怎么定義的:

function createComputedGetter (key) { // 高階函數(shù)
  return function () {  // 返回函數(shù)
    const watcher = this._computedWatchers && this._computedWatchers[key]
    // 原來this還可以這樣用,得到key對(duì)應(yīng)的computed-watcher
    
    if (watcher) {
      if (watcher.dirty) {  // 在實(shí)例化watcher時(shí)為true,表示需要計(jì)算
        watcher.evaluate()  // 進(jìn)行計(jì)算屬性的求值
      }
      if (Dep.target) {  // 當(dāng)前的watcher,這里是頁面渲染觸發(fā)的這個(gè)方法,所以為render-watcher
        watcher.depend()  // 收集當(dāng)前watcher
      }
      return watcher.value  // 返回求到的值或之前緩存的值
    }
  }
}

------------------------------------------------------------------------------------

class Watcher {
  ...
  
  evaluate () {
    this.value = this.get()  //  計(jì)算屬性求值
    this.dirty = false  // 表示計(jì)算屬性已經(jīng)計(jì)算,不需要再計(jì)算
  }
  
  depend () {
    let i = this.deps.length  // deps內(nèi)是計(jì)算屬性內(nèi)能訪問到的響應(yīng)式數(shù)據(jù)的dep的數(shù)組集合
    while (i--) {
      this.deps[i].depend()  // 讓每個(gè)dep收集當(dāng)前的render-watcher
    }
  }
}

這里的變量watcher就是之前computed對(duì)應(yīng)的computed-watcher實(shí)例,接下來會(huì)執(zhí)行Watcher類專門為計(jì)算屬性定義的兩個(gè)方法,在執(zhí)行evaluate方法進(jìn)行求值的過程中又會(huì)觸發(fā)computed內(nèi)可以訪問到的響應(yīng)式數(shù)據(jù)的get,它們會(huì)將當(dāng)前的computed-watcher作為依賴收集到自己的dep里,計(jì)算完畢之后將dirty置為false,表示已經(jīng)計(jì)算過了。

然后執(zhí)行depend讓計(jì)算屬性內(nèi)的響應(yīng)式數(shù)據(jù)訂閱當(dāng)前的render-watcher,所以computed內(nèi)的響應(yīng)式數(shù)據(jù)會(huì)收集computed-watcherrender-watcher兩個(gè)watcher,當(dāng)computed內(nèi)的狀態(tài)發(fā)生變更觸發(fā)set后,首先通知computed需要進(jìn)行重新計(jì)算,然后通知到視圖執(zhí)行渲染,再渲染中會(huì)訪問到computed計(jì)算后的值,最后渲染到頁面。

Ps: 計(jì)算屬性內(nèi)的值須是響應(yīng)式數(shù)據(jù)才能觸發(fā)重新計(jì)算。

當(dāng)computed內(nèi)的響應(yīng)式數(shù)據(jù)變更后觸發(fā)的通知:

class Watcher {
  ...
  update() {  // 當(dāng)computed內(nèi)的響應(yīng)式數(shù)據(jù)觸發(fā)set后
    if(this.lazy) {
      this.diray = true  // 通知computed需要重新計(jì)算了
    }
    ...
  }
}

最后還是以一個(gè)示例結(jié)合流程圖來幫大家理清楚這里的邏輯:

export default {
  data() {
    return {
      manName: "cc",
      womanName: "ww"
    };
  },
  computed: {
    newName() {
      return this.manName + ":" + this.womanName;
    }
  },
  methods: {
    changeName() {
      this.manName = "ss";
    }
  }
};
image

watch總結(jié):為什么計(jì)算屬性有緩存功能?因?yàn)楫?dāng)計(jì)算屬性經(jīng)過計(jì)算后,內(nèi)部的標(biāo)志位會(huì)表明已經(jīng)計(jì)算過了,再次訪問時(shí)會(huì)直接讀取計(jì)算后的值;為什么計(jì)算屬性內(nèi)的響應(yīng)式數(shù)據(jù)發(fā)生變更后,計(jì)算屬性會(huì)重新計(jì)算?因?yàn)閮?nèi)部的響應(yīng)式數(shù)據(jù)會(huì)收集computed-watcher,變更后通知計(jì)算屬性要進(jìn)行計(jì)算,也會(huì)通知頁面重新渲染,渲染時(shí)會(huì)讀取到重新計(jì)算后的值。

最后按照慣例我們還是以一道vue可能會(huì)被問到的面試題作為本章的結(jié)束~

面試官微笑而又不失禮貌的問道:

  • 請(qǐng)問computed屬性和watch屬性分別什么場(chǎng)景使用?

懟回去:

  • 當(dāng)模板中的某個(gè)值需要通過一個(gè)或多個(gè)數(shù)據(jù)計(jì)算得到時(shí),就可以使用計(jì)算屬性,還有計(jì)算屬性的函數(shù)不接受參數(shù);監(jiān)聽屬性主要是監(jiān)聽某個(gè)值發(fā)生變化后,對(duì)新值去進(jìn)行邏輯處理。

下一篇 Vue原理解析(十):搞懂事件API原理及在組件庫中的妙用
順手點(diǎn)個(gè)贊或關(guān)注唄,找起來也方便~

分享一個(gè)筆者自己寫的組件庫,哪天可能會(huì)用的上了 ~ ↓

你可能會(huì)用的上的一個(gè)vue功能組件庫,持續(xù)完善中...

最后編輯于
?著作權(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)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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