汪图南
  • RAG

    • RAG
  • 快速入门
  • 高级技巧
前端面试之道
  • 打包工具

    • Webpack
    • Rollup
  • TypeScript

    • TypeScript基础
    • TypeScript类型挑战
  • CSS预编译器

    • SASS
  • 自动化测试

    • Vue应用测试
  • Vue2.0源码分析
  • Vue3.0源码分析
  • 数据结构和算法(基础)
  • LeetCode(刷题)
  • JavaScript书籍

    • 你不知道的JavaScript(上)
    • 你不知道的JavaScript(中下)
    • JavaScript数据结构和算法
    • JavaScript设计模式与开发实践
    • 深入理解ES6
  • Git书籍

    • 精通Git
Github
  • RAG

    • RAG
  • 快速入门
  • 高级技巧
前端面试之道
  • 打包工具

    • Webpack
    • Rollup
  • TypeScript

    • TypeScript基础
    • TypeScript类型挑战
  • CSS预编译器

    • SASS
  • 自动化测试

    • Vue应用测试
  • Vue2.0源码分析
  • Vue3.0源码分析
  • 数据结构和算法(基础)
  • LeetCode(刷题)
  • JavaScript书籍

    • 你不知道的JavaScript(上)
    • 你不知道的JavaScript(中下)
    • JavaScript数据结构和算法
    • JavaScript设计模式与开发实践
    • 深入理解ES6
  • Git书籍

    • 精通Git
Github
  • 介绍

    • 介绍和参考
  • 源码目录设计和架构设计

    • 设计
  • Rollup构建版本

    • Rollup基础知识
    • Vue中的Rollup构建
  • 从入口到构造函数整体流程

    • 整体流程
    • initGlobalAPI流程
    • initMixin流程
    • stateMixin流程
    • eventsMixin流程
    • lifecycleMixin流程
    • renderMixin流程
  • 响应式原理

    • 介绍
    • 前置核心概念
    • props处理
    • methods处理
    • data处理
    • computed处理
    • watch处理
    • 深入响应式原理
    • 依赖收集
    • 派发更新
    • nextTick实现原理
    • 变化侦测注意事项
    • 变化侦测API实现
  • 虚拟DOM和VNode

    • 虚拟DOM
    • VNode介绍
    • Diff算法
  • 组件化

    • 介绍
    • $mount方法
    • render和renderProxy
    • createElement
    • createComponent
    • 合并策略
    • update和patch
    • 组件生命周期
    • 组件注册
  • 编译原理

    • 介绍
    • compileToFunctions
    • parse模板解析
    • optimize优化
    • codegen代码生成
  • 扩展

    • 扩展
    • directive指令
    • filter过滤器
    • event事件处理
    • v-model
    • 插槽
    • Keep-Alive
    • Transition
    • Transition-Group
    • Vue.use插件机制
  • Vue-Router

    • 介绍
    • 路由安装
    • matcher介绍
    • 路由切换
    • 内置组件
    • 路由hooks钩子函数
  • Vuex

    • 介绍
    • Vuex安装
    • Vuex初始化
    • Vuex辅助API
    • Store实例API

watch处理

在介绍完处理computed相关的逻辑后,我们接下来看watch是如何处理的。

watch初始化

export function initState (vm: Component) {
  // 省略代码
  const opts = vm.$options
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

我们可以看到,处理watch的逻辑发生在initWatch()方法中,在这个方法调用之前,首先对watch做了判断,其中nativeWatch是定义在src/core/util/env.js中的一个常量:

// Firefox has a "watch" function on Object.prototype...
export const nativeWatch = ({}).watch

然后,让我们来看一下initWatch的实现,它定义在src/core/instance/state.js文件中:

function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key]
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}

我们可以看到,initWatch()方法的实现非常简单,首先对watch做了判断,如果是数组则遍历这个数组调用createWatcher()方法,如果不是则直接调用createWatcher()。按照watch的使用规则,我们有如下几种常见的写法:

export default {
  data () {
    return {
      age: 23,
      name: 'AAA',
      nested: {
        a: {
          b: 'b'
        }
      }
    }
  },
  watch: {
    name (newVal, oldVal) {
      console.log(newVal, oldVal)
    },
    nested: {
      handler (newVal, oldVal) {
        console.log(newVal, oldVal),
      },
      deep: true
    }
  }
}

接着,我们需要来看一下createWatcher()函数的具体实现:

function createWatcher (
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  return vm.$watch(expOrFn, handler, options)
}

代码分析:

  • createWatcher()方法的主要作用就是进行watch参数规范化,然后将规范化后的参数传递给vm.$watch()。
  • 在createWatcher()中首先判断了handler参数是否为普通对象,如果是普通对象则代表是如下形式定义的watch:
{
  watch: {
    nested: {
      handler (newVal, oldVal) {
        console.log(newVal, oldVal),
      },
      deep: true
    }
  }
}

此时,应该把handler赋值给可选的options参数,然后handler赋值为真正的回调函数。

  • 接着,对handler进行了类型判断,如果是string类型则把此时vm[handler]赋值给它。根据这段代码的逻辑,意味着我们可以选择把watch回调函数定义在methods中:
export default {
  data () {
    return {
      name: 'AAA'
    }
  },
  watch: {
    name: 'nameWatchCallback'
  },
  methods: {
    nameWatchCallback (newVal, oldVal) {
      console.log(newVal, oldVal)
    }
  }
}
  • 最后,把规范化后的参数传递给vm.$watch()。关于$watch()何时挂载到Vue.prototype上,我们已经在之前介绍过了,它发生在stateMixin中。

在分析完createWatcher()方法实现逻辑后,我们接着来看$watch()方法的具体实现逻辑:

Vue.prototype.$watch = function (
  expOrFn: string | Function,
  cb: any,
  options?: Object
): Function {
  const vm: Component = this
  if (isPlainObject(cb)) {
    return createWatcher(vm, expOrFn, cb, options)
  }
  options = options || {}
  options.user = true
  const watcher = new Watcher(vm, expOrFn, cb, options)
  if (options.immediate) {
    try {
      cb.call(vm, watcher.value)
    } catch (error) {
      handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
    }
  }
  return function unwatchFn () {
    watcher.teardown()
  }
}

我们可以发现,$watch方法主要做两件事情:创建Watcher实例和返回unwatchFn函数,接下来我们分别对这两部分的逻辑进行详细的解释。

创建Watcher实例

我们先来看一下Watcher构造函数的代码:

// 精简代码
class Watcher {
  constructor (vm, expOrFn, cb, options, isRenderWatcher) {
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
  }
}

我们从构造函数中可以看到,当实例化一个watch的时候,会根据传递的options来处理deep、user、lazy、sync以及before属性。watcher根据不同的用法,有几种不同的分类:

  • render watcher:渲染watcher,例如当在template模板中使用{{}}语法读取一个变量的时候,此时这个变量收集的依赖就是render watcher,当这个变量值更新的时候会触发render watcher进行组件的重新渲染。是否为渲染warcher,使用构造函数参数isRenderWatcher为true进行区分。
  • computed watcher:计算属性watcher,当我们在定义计算属性的时候,计算属性收集的依赖就是另外一个或者多个变量,当其中一个变量的值发生变量,就会触发计算属性重新进行求值。是否为计算属性watcher,使用options.lazy为true进行区分。
  • user watcher:用户自定义watcher,多发生在this.$watch或者组件watch选择配置中,此时收集的依赖就是变量自身,当变量的值发生变化的时候,就会调用watch提供的回调函数。是否为用户自定义watcher,使用options.user为true进行区分。

返回unwatchFn函数

我们在构造函数中可以发现,它定义了一个_watchers变量,然后在每次实例化的时候,把自身添加到这个数组中,这样做的目的是为了方便清除依赖。在之前的介绍中,我们知道$watch返回了一个unwatchFn函数,它用来取消监听。接下来,我们看一下teardown()方法的具体实现。

// Watcher类精简代码
class Watcher {
  constructor () {
    this.active = true
    this.deps = []
  }
  teardown () {
    if (this.active) {
      // remove self from vm's watcher list
      // this is a somewhat expensive operation so we skip it
      // if the vm is being destroyed.
      if (!this.vm._isBeingDestroyed) {
        remove(this.vm._watchers, this)
      }
      let i = this.deps.length
      while (i--) {
        this.deps[i].removeSub(this)
      }
      this.active = false
    }
  }
}

// Dep类精简代码
class Dep {
  constructor () {
    this.subs = []
  }
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }
}

teardown()方法的实现很简单,就是从deps数组中移除当前的watcher,其中deps存储的是Dep实例。

最后更新时间: 2025/5/6 15:36
贡献者: wangtunan
Prev
computed处理
Next
深入响应式原理