3-2-16-Vue.js 源码阅读-响应式原理-Watcher

墨雨华
2023-12-01

响应式原理-Watcher

Watcher 分为三种类型,Computed Watcher、用户 Watcher(侦听器)、渲染 Watcher

首次渲染的 Watcher

首先是首次渲染的时候创建 Watcher 的时候。

// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, {
  before () {
    if (vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'beforeUpdate')
    }
  }
}, true /* isRenderWatcher */)

在 mountComponent 这个方法中创建了 Watcher 示例,传入了几个参数。

  • 第一个参数就是当前的 Vue 实例
  • 第二个参数是之前定义好的将 template 或者 render 转换成真实 DOM 的方法
  • 第三个参数是一个空函数(当是渲染函数的时候会执行这个 noop 即不执行任何操作,当时用户 Watcher 的时候需要调用回调函数进行响应处理)
  • 第四个参数是一个对象,传入了更新视图之前要触发的生命周期钩子函数 beforeUpdate
  • 第五个参数为一个标识位,标识当前的 Watcher 是否是一个渲染 Watcher

下面分析 Watcher 类中执行的主要步骤

// parse expression for getter
if (typeof expOrFn === 'function') {
  this.getter = expOrFn
} else {
  // expOrFn 是字符串的时候,例如 { 'person.name': function... }
  // parsePath('person.name') 返回一个函数获取 person.name 的值
  this.getter = parsePath(expOrFn)
  if (!this.getter) {
    this.getter = noop
    process.env.NODE_ENV !== 'production' && warn(
      `Failed watching path: "${expOrFn}" ` +
      'Watcher only accepts simple dot-delimited paths. ' +
      'For full control, use a function instead.',
      vm
    )
  }
}
this.value = this.lazy
  ? undefined
  : this.get()

这里通过判断 expOrFn 是 function 还是 字符串来区分渲染 Watcher 还是 用户 Watcher ,然后进行分类讨论。

  • 如果是渲染 Watcher , expOrFn 就是更新视图的方法
  • 如果是用户 Watcher , 调用 parsePath 方法返回一个方法,这个方法就是获取当前属性的值。

然后调用 this.get() 方法,在 get 中会将当前的 Dep.target 进行入栈以及渲染等操作。入栈是为了处理父子组件嵌套的情况。

/**
  * Evaluate the getter, and re-collect dependencies.
  */
get () {
  // 设置 Dep.target 属性
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    // 将模板转换成虚拟 DOM ,然后将虚拟 DOM 转换成真实 DOM,过程中会触发属性的 get,在 get 中去收集依赖
    value = this.getter.call(vm, vm)
  } catch (e) {
    if (this.user) {
      handleError(e, vm, `getter for watcher "${this.expression}"`)
    } else {
      throw e
    }
  } finally {
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
      traverse(value)
    }
    popTarget()
    this.cleanupDeps()
  }
  return value
}

在 get 中会通过调用 this.getter.call(vm, vm) 来更新视图。如果是 Computed 或者 用户 Watcher 还会将最终得到的 value 返回。

数据更新的 Watcher

在数据更新的时候会调用 dep 的 notify 方法发送通知,遍历 dep 的 subs 数组,调用所有的 Watcher 的 update 方法更新视图。

// 发布通知
notify () {
  // stabilize the subscriber list first
  // 使用 slice 对数组进行克隆,为后面数组根据 id 排序做准备
  const subs = this.subs.slice()
  if (process.env.NODE_ENV !== 'production' && !config.async) {
    // subs aren't sorted in scheduler if not running async
    // we need to sort them now to make sure they fire in correct
    // order
    subs.sort((a, b) => a.id - b.id)
  }
  // 调用每个订阅者的 update 方法实现更新
  for (let i = 0, l = subs.length; i < l; i++) {
    subs[i].update()
  }
}

在 notify 中遍历 subs 调用所有的 Watcher 的 update 方法。下面是 update 方法的实现。

/**
  * Subscriber interface.
  * Will be called when a dependency changes.
  */
update () {
  /* istanbul ignore else */
  if (this.lazy) {
    // lazy 为 true 说明是 Computed Watcher
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
    // 是渲染 Watcher
    queueWatcher(this)
  }
}

在 update 中如果是渲染 Watcher 会调用 queueWatcher 将当前的 Watcher 添加到 Watcher 任务队列中。

/**
 * Push a watcher into the watcher queue.
 * Jobs with duplicate IDs will be skipped unless it's
 * pushed when the queue is being flushed.
 */
export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  // has 是一个对象,用于存储所有已经处理过的 Watcher,防止 Watcher 被重复处理
  if (has[id] == null) {
    // 如果为 null 说明没有被处理过
    has[id] = true
    // flushing 为 true 说明当前的队列正在被处理,即队列中存储的 Watcher 对象正在被处理
    if (!flushing) {
      // 如果没有在处理,就直接将当前 Watcher 放到队尾
      queue.push(watcher)
    } else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      // 如果正在被处理,需要找到一个合适的位置将当前的 Watcher 插入到合适的位置
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // queue the flush
    if (!waiting) {
      waiting = true

      if (process.env.NODE_ENV !== 'production' && !config.async) {
        // 遍历队列,调用 update 方法
        flushSchedulerQueue()
        return
      }
      // 如果是生产环境,将处理函数交给 nextTick 
      nextTick(flushSchedulerQueue)
    }
  }
}

执行 queueWatcher 的最后会去调用 flushSchedulerQueue 方法遍历队列执行更新。

/**
 * Flush both queues and run the watchers.
 */
function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  // 更改标记,标识正在处理队列
  flushing = true
  let watcher, id

  // Sort queue before flush.
  // This ensures that:
  // 1. Components are updated from parent to child. (because parent is always
  //    created before the child)
  // 2. A component's user watchers are run before its render watcher (because
  //    user watchers are created before the render watcher)
  // 3. If a component is destroyed during a parent component's watcher run,
  //    its watchers can be skipped.
  // 根据 id 进行排序,即依据 Watcher 的创建顺序进行排序。
  queue.sort((a, b) => a.id - b.id)

  // do not cache length because more watchers might be pushed
  // as we run existing watchers
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    watcher.run()
    // in dev build, check and stop circular updates.
    if (process.env.NODE_ENV !== 'production' && has[id] != null) {
      circular[id] = (circular[id] || 0) + 1
      if (circular[id] > MAX_UPDATE_COUNT) {
        warn(
          'You may have an infinite update loop ' + (
            watcher.user
              ? `in watcher with expression "${watcher.expression}"`
              : `in a component render function.`
          ),
          watcher.vm
        )
        break
      }
    }
  }

  // keep copies of post queues before resetting state
  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()

  resetSchedulerState()

  // call component updated and activated hooks
  callActivatedHooks(activatedQueue)
  callUpdatedHooks(updatedQueue)

  // devtool hook
  /* istanbul ignore if */
  if (devtools && config.devtools) {
    devtools.emit('flush')
  }
}

在 flushSchedulerQueue 中遍历队列执行了的 run 方法

/**
  * Scheduler job interface.
  * Will be called by the scheduler.
  */
run () {
  if (this.active) {
    const value = this.get()
    if (
      value !== this.value ||
      // Deep watchers and watchers on Object/Arrays should fire even
      // when the value is the same, because the value may
      // have mutated.
      isObject(value) ||
      this.deep
    ) {
      // set new value
      const oldValue = this.value
      this.value = value
      if (this.user) {
        try {
          this.cb.call(this.vm, value, oldValue)
        } catch (e) {
          handleError(e, this.vm, `callback for watcher "${this.expression}"`)
        }
      } else {
        this.cb.call(this.vm, value, oldValue)
      }
    }
  }
}
 类似资料: