千家信息网

Vue中Watcher和Scheduler的实现原理是什么

发表于:2026-11-02 作者:千家信息网编辑
千家信息网最后更新 2026年11月02日,这篇文章主要介绍"Vue中Watcher和Scheduler的实现原理是什么",在日常操作中,相信很多人在Vue中Watcher和Scheduler的实现原理是什么问题上存在疑惑,小编查阅了各式资料,
千家信息网最后更新 2026年11月02日Vue中Watcher和Scheduler的实现原理是什么

这篇文章主要介绍"Vue中Watcher和Scheduler的实现原理是什么",在日常操作中,相信很多人在Vue中Watcher和Scheduler的实现原理是什么问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答"Vue中Watcher和Scheduler的实现原理是什么"的疑惑有所帮助!接下来,请跟着小编一起来学习吧!

Vue通过数据侦测机制感知状态的变化,上一篇《Vue如何实现数据侦测》有提到Watcher对象,当数据更新有更新,例如当执行this.title = '监听我变化了没',在setter函数调用dep.notify通知watcher执行更新(具体执行watcher.update函数)。

那么Vue在何时创建Watcher,如何通过Scheduler来调度Watcher队列,watcher的更新最终如何体现到视图的渲染,本篇内容主要围绕这三个问题来介绍Vue的Watcher实现原理。

1.何时创建Watcher

组件从创建到销毁会经历一系列生命周期,其中我们比较熟悉的有beforeMount、mounted、beforeUpdate、updated, 了解了生命周期,理解Watcher在何时被创建就会容易很多。Vue共三处地方会创建Watcher对象,mount事件、$watch函数、computed和watch属性, mount事件创建Watcher用于渲染通知,watch和computed创建的Watcher都用于监听用户自定义的属性变化。


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.jsVue.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.jsexport 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('person', () => {  this.message = '年龄为:' + 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函数。

当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()}

到此,关于"Vue中Watcher和Scheduler的实现原理是什么"的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注网站,小编会继续努力为大家带来更多实用的文章!

函数 属性 对象 更新 队列 组件 监听 附加 变化 事件 调度 状态 任务 处理 原理 位置 周期 实体 文件 生命 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 天津市摩尔互联网科技 广州万易互联网科技有限公司 互联网中国科技 互联网金融公司科技 伴你行互联网科技有限公司 互联网科技有限公司 翻译 联恩商钥互联网科技有限公司 中正互联网科技招聘 太原新华互联网科技学校怎么样 互联网公司会议桌创意科技软装饰 互联网科技革命什么开始 开一家科技互联网公司需要多少钱 爱医互联网络科技 工业互联网展湾科技股份有限公司 互联网时代的科技公司 互联网公司高科技产品 万物互联网智能科技 互联网科技大会大佬 长沙易易互联网科技有限公司 瑞安市蟠桃会互联网科技有限公司 华服互联网科技靠谱吗 互联网金融监管科技数据 互联网巨头科技公司 鑫亿盛互联网科技 深圳天众互联网科技公司 江苏南京365互联网科技 苏州铭阳互联网科技有限公司 允成互联网科技有限公司融资 张召忠谈中国互联网科技 长沙易易互联网科技有限公司
0