首頁 >web前端 >Vue.js >深入了解Vue中的Watcher和Scheduler

深入了解Vue中的Watcher和Scheduler

青灯夜游
青灯夜游轉載
2021-12-01 19:58:293013瀏覽

這篇文章帶大家了解Vue中的Watcher和Scheduler,介紹一下Vue的Watcher實作原理,希望對大家有幫助。

深入了解Vue中的Watcher和Scheduler

Vue透過資料偵測機制感知狀態的變化,上一篇《Vue如何實現資料偵測》有提到Watcher對象,當資料更新有更新,例如當執行this.title = '監聽我變化了沒',在setter函數呼叫dep.notify通知watcher執行更新(具體執行watcher.update函數)。

那麼Vue在何時創建Watcher,如何透過Scheduler來調度Watcher佇列,watcher的更新最終如何體現到視圖的渲染,本篇內容主要圍繞這三個問題來介紹Vue的Watcher實作原理。 【相關推薦:《vue.js教學》】

深入了解Vue中的Watcher和Scheduler

#1.何時創建Watcher

##元件從創建到銷毀會經歷一系列生命週期,其中我們比較熟悉的有beforeMount、mounted、beforeUpdate、updated, 了解了生命週期,理解Watcher在何時被創建就會容易很多。 Vue共三個地方會創建Watcher對象,mount事件、$watch函數、computed和watch屬性, mount事件創建Watcher用於渲染通知,watch和computed創建的Watcher都用於監聽用戶自定義的屬性變化。

深入了解Vue中的Watcher和Scheduler

1.1 mount事件

檔案core/instance/lifecycle.js包含了Vue生命週期相關的函數,例如$forupdate、$destroy以及實例化Watcher的mountComponent函數,mountComponent函數在元件掛載完成執行$mount時觸發,函數首先觸發beforeMount鉤子事件,在實例化Watcher時有傳入before函數,before將觸發beforeUpdate hook。當元件有屬性更新時,watcher在更新(watcher.run)之前會觸發beforeUpdate事件。 isRenderWatcher表示創建的是渲染Watcher,直接掛在vm._watcher屬性上,當強制執行$forceUpdate刷新渲染,會執行vm._watcher.update觸發渲染過程以及對應的update hook。

/**
 * 生命周期mount事件触发函数
 * @param {*} vm 
 * @param {*} el 
 * @param {*} hydrating 
 * @returns 
 */
 export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el

  callHook(vm, 'beforeMount')

  let updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }

  // 实例化Watcher对象,在Watcher构造函数中建立Watcher和vm的关系
  new Watcher(vm, updateComponent, noop, {
    // 在执行wather.run函数之前触发before hook事件
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
    // isRenderWatcher表示用于渲染的Watcher,在执行$forceupdate时会手动触发watcher.update
  }, true /* isRenderWatcher */)
  
  return vm
}

export default class Watcher {
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    this.getter = expOrFn
    this.value = this.lazy
      ? undefined
      : this.get()
  }
}

Vue.prototype.$forceUpdate = function () {
  const vm: Component = this
  if (vm._watcher) {
    vm._watcher.update()
  }
}

1.2.$watch函數

在元件中,除了使用watch、computed方法監聽屬性變化,Vue定義了$watch函數用於監聽屬性變化,例如當a.b.c巢狀屬性變化,可以$watch來實現監聽做後續處理,$watch相當於在組件中直接寫watch屬性的函數式寫法,可支持在運行時動態的添加依賴監聽,例如Vue源碼中的keep-alive組件在mounted事件中使用$watch監聽include、exclude屬性變化。

vm.$watch( expOrFn, callback, [options] )
参数:
    {string | Function} expOrFn
    {Function | Object} callback
    {Object} [options]
    {boolean} deep
    {boolean} immediate
返回值:{Function} unwatch

// 键路径
vm.$watch('a.b.c', function (newVal, oldVal) {
  // 做点什么
})

// keep-alive.js文件
  mounted () {
    this.cacheVNode()
    this.$watch('include', val => {
      pruneCache(this, name => matches(val, name))
    })
    this.$watch('exclude', val => {
      pruneCache(this, name => !matches(val, name))
    })
  }

$watch函數和mountComponent函數的差異是,mountComponent用於渲染監聽,會觸發相關的hook事件,而$watch的職責比較專一,就處理expOrFn的監聽。另外,$watch的cb參數可以是函數、物件或字串,當為字串時表示定義在Vue物件的函數名,例如在Vue元件中定義了nameChange函數,那麼定義vm.$watch('name' , 'nameChange')後,如果name有更新會觸發Vue實體的nameChange函數。

// 监听属性变化
Vue.prototype.$watch = function (
  expOrFn: string | Function,
  cb: any,
  options?: Object
): Function {
  const vm: Component = this
  // cb可能是纯JS对象,那么回调为cb.handler
  if (isPlainObject(cb)) {
    return createWatcher(vm, expOrFn, cb, options)
  }
  const watcher = new Watcher(vm, expOrFn, cb, options)
  
  // 返回watch注销监听函数
  return function unwatchFn () {
    watcher.teardown()
  }
}

function createWatcher (
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  // 当执行函数是一个对象的时候, 将 handler 的 handler调用给执行函数
    // 这里的 options 是 watch 函数的配置信息
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  return vm.$watch(expOrFn, handler, options)
}

1.3.watch和computed屬性

使用Vue開發元件,這兩個屬性一定不陌生,例如使用watch定義firstName、secondName屬性的監聽,使用computed定義fullName屬性監聽,當firstName和secondName更新時fullName也隨之觸發更新。

new Vue({
  el: '#app',
  data() {
    return {
        firstName: 'Li',
        secondName: 'Lei'
    }
  },
  watch: {
      secondName: function (newVal, oldVal) {
          console.log('second name changed: ' + newVal)
      }
  },
  computed: {
      fullName: function() {
          return this.firstName + this.secondName
      }
  },
  mounted() {
    this.firstName = 'Han'
    this.secondName = 'MeiMei'
  }
})

當我們在watch和computed定義了對屬性的監聽,Vue在何時將其轉換為Watcher物件執行監聽? Vue的建構子會呼叫_init(options)執行初始化,原始碼core/components/instance/init.js檔案定義了_init函數,執行了一些欄位初始化操作,例如初始化生命週期、事件、狀態等,其中initState函數就包含了watch和computed的初始化。

// core/components/instance/init.js
// Vue构造函数
function Vue (options) {
  this._init(options)
}

// core/components/instance/init.js
Vue.prototype._init = function (options?: Object) {
  initLifecycle(vm)
  initEvents(vm)
  initRender(vm)
  callHook(vm, 'beforeCreate')
  initInjections(vm) // resolve injections before data/props
  initState(vm)
  initProvide(vm) // resolve provide after data/props
  callHook(vm, 'created')
}

// // core/components/state.js
export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  ...
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

1.3.1 computed屬性

initComputed初始化computed屬性,每個Vue實體都包含_computedWatchers物件用於儲存所有computed屬性的watcher物件。首先遍歷computed對象,為每個key建立一個新的Watcher對象,其lazy屬性為true,表示Watcher會快取計算值,如果依賴其依賴的屬性(如firstName、secondName)沒有更新,當前computed屬性(例如fullName )也不會觸發更新。 computed中定義的屬性可以透過this(例如this.fullName)訪問,defineComputed將所有computed屬性掛載到Vue實體上。

// lazy为true表示需要缓存,一般只有computed属性才会用到
const computedWatcherOptions = { lazy: true }

function initComputed (vm: Component, computed: Object) {
    const watchers = vm._computedWatchers = Object.create(null)

    for (const key in computed) {
      const userDef = computed[key]
      // 用户定义的执行函数可能是{ get: function() {} }形式
      const getter = typeof userDef === 'function' ? userDef : userDef.get
      // 为用户定义的每个computed属性创建watcher对象
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )

      // 组件自身的computed属性已经定义在组件原型链上,我们只需要定义实例化的computed属性。
      // 例如我们在computed定义了fullName,defineComputed会将其挂接到Vue对象的属性上
      if (!(key in vm)) {
        defineComputed(vm, key, userDef)
      }
}

defineComputed函數將計算屬性轉換為{ get, set }形式,但計算屬性不需要set,所以程式碼直接為其賦值了noop空函數。計算屬性的get函數透過createComputedGetter封裝,先找到對應屬性的watcher對象,如果watcher的dirty為true,表示依賴屬性有更新,需要呼叫evaluate函數重新計算新值。

// 将computed定义的属性转换为{ get, set }形式并挂接到Vue实体上,这样就可以通过this.fullName形式调用
export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = createComputedGetter(key)
    sharedPropertyDefinition.set = noop
  } else {
    sharedPropertyDefinition.get = userDef.get
      ? createComputedGetter
      : noop
    sharedPropertyDefinition.set = userDef.set || noop
  }

  Object.defineProperty(target, key, sharedPropertyDefinition)
}

// 定义computed的专属getter函数
function createComputedGetter (key) {
  return function computedGetter () {
    // _computedWatchers上为每个computed属性定义了Watcher对象
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      // dirty为true,表示依赖的属性有变化
      if (watcher.dirty) {
      // 重新计算值
        watcher.evaluate()
      }
      if (Dep.target) {
        // 将Dep.target(watcher)附加到当前watcher的依赖中
        watcher.depend()
      }
      return watcher.value
    }
  }
}

如果Dep.target有值,将其他依赖当前计算属性的Watcher(例如使用到fullName的依赖Watcher)附加到当前计算属性所依赖的属性的dep集合中。如下面的代码创建了对fullName计算属性的监听, 我们将其命名为watcher3。那么firstName和secondName的dep对象都会附加上watcher3观察者,只要其属性有任何变化,都会触发watcher3的update函数,重新读取fullName属性值。

vm.$watch('fullName', function (newVal, oldVal) {
  // 做点什么
})

1.3.2 watch属性

initWatch函数逻辑相对简单些,遍历每个属性的依赖项,如果依赖项为数组,则遍历数组,为每个依赖项单独创建Watcher观察者,createWatcher函数在前文中有提到,它使用$watch创建新的watcher实体。

// 初始化Watch属性
function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key]
    // 如果对应属性key有多个依赖项,则遍历为每个依赖项创建watcher
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}

2.Scheduler调度处理

Vue在core/observer/scheduler.js文件定义了调度函数,一共有两处使用,Watcher对象以及core/vdom/create-component.js文件。watcher对象在执行更新时,会被附加到调度队列中等待执行。create-component.js主要处理渲染过程,使用scheduler的主要作用是触发activated hook事件。这里重点阐述Watcher对Scheduler的使用。
当执行watcher的update函数,除了lazy(计算属性watcher)、sync(同步watcher),所有watcher都将调用queueWatcher函数附加到调度队列中。

export default class Watcher {
  /**
 * 通知订阅,如果依赖项有更新,该函数会被触发
 */
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
}

queueWatcher函数定义如下,函数的目的是将watcher附加到调度队列中,对调度队列创建微任务(microTask),等待执行。关于microTask和macroTask的区别,看查看参考8“宏任务macroTask和微任务microTask的区别”。如果微任务flushSchedulerQueue还未执行(flushing为false),直接将watcher附加到queue即可。否则,还需判断当前微任务的执行进度,queue会按watcher的id做升序排序,保证先创建的watcher先执行。index为微任务中正在被执行的watcher索引,watcher将会插入到大于index且符合id升序排列的位置。最后队列执行函数flushSchedulerQueue将通过nextTick创建一个微任务等待执行。

/*
* 附加watcher到队列中,如果有重复的watcher直接跳过。
* 如果调度队列正在执行(flushing为true),将watcher放到合适的位置
*/
export function queueWatcher (watcher: Watcher) {
  // 所有watcher都有一个递增的唯一标识,
  const id = watcher.id
  // 如果watcher已经在队列中,不做处理
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      // 如果队列还未执行,则直接附加到队列尾部
      queue.push(watcher)
    } else {
      // 如果正在执行,基于id将其附加到合适的位置。
      // index为当前正在执行的watcher索引,并且index之前的watcher都被执行了。
      // 先创建的watcher应该被先执行,和队列中的watcher比较id大小,插入到合适的位置。
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      // i的位置,表明 watcher[i - 1].id < watcher[i].id < watcher[i + 1].id
      queue.splice(i + 1, 0, watcher)
    }
    // 如果未排队,开始排队,nextick将执行调度队列。
    if (!waiting) {
      waiting = true
      nextTick(flushSchedulerQueue)
    }
  }
 }

nextTick将会选择适合当前浏览器的微任务执行队列,例如MutationObserver、Promise、setImmediate。flushSchedulerQueue函数将遍历所有watcher并执行更新,首先需要将queue做升序排序,确保先创建的watcher先被执行,例如父组件的watcher优先于子组件执行。接着遍历queue队列,先触发watcher的before函数,例如前文中介绍mountComponent函数在创建watcher时会传入before事件,触发callHook(vm, 'beforeUpdate')。接下来就具体执行更新(watcher.run)操作。当队列执行完后,调用resetSchedulerState函数清空队列、重置执行状态。最后callActivatedHooks和callUpdatedHooks将触发对应的activated、updated hook事件。

/**
 * 遍历执行所有的watchers
 */
 function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id

  // 遍历之前先排序队列
  // 排序的队列能确保:
  //    1.父组件先于子组件更新,因为父组件肯定先于子组件创建。
  //    2.组件自定义的watcher将先于渲染watcher执行,因为自定义watcher先于渲染watcher创建。
  //    3.如果组件在父组件执行wtcher期间destroyed了,它的watcher集合可以直接被跳过。
  queue.sort((a, b) => a.id - b.id)

  // 不要缓存length,因为在遍历queue执行wacher的同时,queue队列一直在调整。
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      // 通过before可触发hook,例如执行beforeUpdated hook
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    // 执行watcher的更新
    watcher.run()
  }

  // 由于activatedChildren和queue两个队列一直在更新,因为需要拷贝处理
  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()
  // 重置掉队队列状态
  resetSchedulerState()

  // 触发activated和updated hooks
  callActivatedHooks(activatedQueue)
  callUpdatedHooks(updatedQueue)
}

3.Watcher更新

调度队列会执行watcher的run函数触发更新,每个watcher有active状态,表明当前watcher是否处于激活状态,当组件执行$destroy函数,会调用watcher的teardown函数将active设置为false。在执行更新通知回调cb之前,有三个条件判断,首先判断值是否相等,对于简单值string或number类型的可直接判断;如果value为对象或需要深度遍历(deep为true),例如用户自定义了person属性,其值为对象{ age: number, sex: number },我们使用$watch('person', cb)监听了person属性,但当person.age发生变化时,cb不会被执行。如果改成$watch('person', cb, { deep: true }),任何嵌套的属性发生变化,cb都会被触发。满足三个条件其中之一,cb回调函数将被触发。

export default class Watcher {
  /**
 * 调度接口,将被调度器执行
 */
   run () {
    // 仅当watcher处于激活状态,才会执行更新通知
    // 当组件destroyed时,会调用watcher的teardown将其重置到非激活状态
    if (this.active) {
      // 调用get获取值
      const value = this.get()
      if (
        // 如果新计算的值更新了
        value !== this.value ||
        // 如果value为对象或数组,不管value和this.value相等否,则其深度watchers也应该被触发
        // 因为其嵌套属性可能发生变化了
        isObject(value) ||
        this.deep
      ) {
        const oldValue = this.value
        this.cb.call(this.vm, value, oldValue)
      }
    }
  }
}

this.$watch(&#39;person&#39;, () => {
  this.message = &#39;年龄为:&#39; + this.person.age
  }, 
  // 当deep为true,当age更新,回调会被触发;如果deep为false,age更新不会触发回调
  { deep: true }
)

run函数有调用get获取最新值,在get函数中,首先调用pushTarget函数将当前Watcher附加到全局Dep.target上,然后执行getter获取最新值。在finally模块中,如果deep为true,则调用traverse递归遍历最新的value,value可能为Object或者Array,所以需要遍历子属性并触发其getter函数,将其dep属性附加上Dep.target(当前Watcher),这样任何子属性的值发生变化都会通知到当前watcher,至于为什么,可以回顾下上篇《Vue如何实现数据状态的侦测》。

export default class Watcher {
  /**
* 执行getter,重新收集依赖项
*/
  get () {
    // 将当前Watcher附加到全局Dep.target上,并存储targetStack堆栈中
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      // 执行getter读取value
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // 如果deep为true,将遍历+递归value对象
      // 将所有嵌套属性的dep都附加上当前watcher,所有子属性对应的dep都会从push(Dep.target)
      if (this.deep) {
        // 递归遍历所有嵌套属性,并触发其getter,将其对应的dep附加当前watcher
        traverse(value)
      }
      // 退出堆栈
      popTarget()
      // 清理依赖
      this.cleanupDeps()
    }
    return value
  }
}

在get函数中为什么要执行traverse递归遍历子属性,我们可以通过实际的例子来说明,例如在data中定义了{ person: { age: 18, sex: 0, addr: { city: '北京', detail: '五道口' } }, Vue会调用observe将person转换为如下Observer对象,子属性(如果为对象)也会转换为Observer对象,简单属性都会定义get、set函数。

深入了解Vue中的Watcher和Scheduler

当watcher.get执行traverse函数时,会递归遍历子属性,当遍历到addr属性时,触发get函数,该函数将调用其dep.depend将当前Watcher附加到依赖项中,这样我们在执行执行this.person.age = 18,其set函数调用dep.notify触发watcher的update函数,实现person对象的监听。

get: function reactiveGetter () {
  const value = getter ? getter.call(obj) : val
  if (Dep.target) {
    dep.depend()
    ...
  }
  return value
}

set: function reactiveSetter (newVal) {
  ...
  dep.notify()
}

4.总结

本文首先介绍Watcher从创建到最终组件渲染经历的流程,然后就"何时创建Watcher"、"Scheduler调度处理"、"Watcher更新"三部分作了详细介绍。通过"何时创建Watcher"了解到Vue在哪些地方会创建Watcher对象,"Scheduler调度处理"介绍了Vue如何通过微任务来执行wacher队列的更新,"Watcher更新"简单讲述了每一个watcher如何执行更新。由于篇幅原因,watcher的更新最终如何体现到视图的渲染,将在下一篇做具体介绍。篇幅有点长,能看到这里,说明大家对Vue是真爱。

更多编程相关知识,请访问:编程入门!!

以上是深入了解Vue中的Watcher和Scheduler的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:juejin.cn。如有侵權,請聯絡admin@php.cn刪除