七、vm.$watch

Vue

# 功能

监听$data中成员的变化,观察 Vue 实例变化的一个表达式或计算属性函数。回调函数得到的参数为新值和旧值。表达式只 接受监督的键路径。对于更复杂的表达式,用一个函数取代。

# 参数

  • expOrFn:要监视的 $data 中的属性,可以是表达式或函数
  • callback:数据变化后执行的函数,接收两个参数,新值和旧值
    • 函数:回调函数
    • 对象:具有 handler 属性(字符串或者函数),如果该属性为字符串则 methods 中相应 的定义
  • options:可选的选项,重点介绍
    • deep:布尔类型,深度监听
    • immediate:布尔类型,是否立即执行一次回调函数

# 示例

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Vue.js 01 component example</title>
  </head>
  <body>
    <div id="app">
      {{ user.fullName }}
    </div>

    <script src="../../dist/vue.js"></script>
    <script>
      const vm = new Vue({
        el: '#app',
        data: {
          user: {
            firstName: '张',
            lastName: '三',
            fullName: ''
          }
        }
      })

      vm.$watch('user',
        function (newValue, oldValue) {
          this.user.fullName = newValue.firstName + newValue.lastName
        }
      )
    </script>
  </body>
</html>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

这样一开始的时候不会立马执行,想要立马执行就在第三个参数中添加选项

vm.$watch('user',
    function (newValue, oldValue) {
      this.user.fullName = newValue.firstName + newValue.lastName
    },{
      immediate: true
    }
 )
1
2
3
4
5
6
7

此时就会立即执行,页面展示张三

image

下面改变user的值

  1. 将user置为{}

image

修改user的值可以看到页面进行了响应式变化

image

  1. 将user的firstName改为李

image

这个时候并没有进行改变,要想监听,可以这样写

vm.$watch('user.firstName',
    function (newValue, oldValue) {
      this.user.fullName = newValue + this.user.lastName
    },{
      immediate: true
    }
)

vm.$watch('user.lastName',
    function (newValue, oldValue) {
      this.user.fullName = this.user.firstName + newValue
    },{
      immediate: true
    }
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

image

这样就比较麻烦,每个相关的属性都要写,推荐这样写,在第三个参数配置中写deep深度监听

vm.$watch('user',
    function (newValue, oldValue) {
      this.user.fullName = newValue.firstName + newValue.lastName
    },{
      immediate: true,
      deep: true
    }
)
1
2
3
4
5
6
7
8

image

# watcher的讲解

三种类型的 Watcher 对象

  • 没有静态方法,因为 $watch 方法中要使用 Vue 的实例
  • Watcher 分三种:计算属性 Watcher、用户 Watcher (侦听器)、渲染 Watcher
  • 创建顺序:计算属性 Watcher、用户 Watcher (侦听器)、渲染 Watcher

# 调试watcher渲染顺序

# 源码解析

我们对$watch的源码进行解析

从initState里面看到调用了initWatch方法,initWatch方法对用户传入的属性进行遍历,

function initWatch (vm: Component, watch: Object) {
  // 遍历用户传入的watch的属性,这里是user
  for (const key in watch) {
    // 获取watch的值,可以是数组,也可以是对象,函数
    const handler = watch[key]
    // 如果是数组,就对元素进行遍历,每一个元素都创建watcher,当这个属性变化的时候,会执行多个回调
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    // 如果不是数组,直接执行createWatcher
    } else {
      createWatcher(vm, key, handler)
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

那么createWatcher中是怎么定义的呢?

function createWatcher (
  // Vue实例,对应属性,和handler
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  // 先判断handler是否是一个原生对象
  if (isPlainObject(handler)) {
    options = handler
    // 真正的handler,回调函数
    handler = handler.handler
  }
  // 判断handler是否是字符串,如果是字符串就会去实例对象上找,也就是methods中定义的方法
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  // 将解析好的数据给$watch
  return vm.$watch(expOrFn, handler, options)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

下面我们进入$watch函数,也在当前文件中,搜索跳转

//原型上挂载了$watch,监视数据的变化
Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
): Function {
    // 获取当前实例,没有对应的静态方法,因为其用到了vue的实例
    const vm: Component = this
    // 判断回调函数是否是原生对象,如果是继续放到createWatcher中,这里要保证回调函数是函数
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    // 把当前watch的属性赋值,如果没有赋值空对象
    options = options || {}
    // 标记为用户watcher
    options.user = true
    // 创建用户watcher对象
    const watcher = new Watcher(vm, expOrFn, cb, options)
    // 判断选项中是否要立即执行
    if (options.immediate) {
      try {
        // 如果是就立刻调用回调函数,使用call改变其内部指向为vue实例,并将值返回
        // 使用try-catch是不确定我们传入的代码是否安全,不要阻塞之后的代码执行,
        cb.call(vm, watcher.value)
      } catch (error) {
        handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
      }
    }
    // 返回取消监听的方法
    return function unwatchFn () {
      watcher.teardown()
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

那user只是定义了没有看到有使用的地方,现在调转到Watcher中看做了什么,在watcher.js中搜索user

  • 将this.user设置为true
  • get方法中
get () {
    // 调用pushTarget,将当前的Watcher对象放入栈中
    // 每个组件对应一个Watcher,Watcher会去渲染视图,如果组件有嵌套的话会先渲染内部的组件,所以要将父组件的Watcher先保存起来,这是这个pushTarget的作用
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      // 最关键的一句话
      // 这句话调用了getter,getter存储的是传入的第二个参数,且是函数,首次渲染是updateComponent,所以在get方法的内部调用了updateComponent,并且改变了函数内部的this指向到Vue实例vm,并且传入了vm
      // 这里将虚拟DOM转化成了真实DOM并更新到页面中

      // 如果是用户watcher的话,这个getter是获取属性的,如果在获取属性的时候有异常,下面会处理异常,这里不看
      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)
      }
      // 将watcher从栈中弹出
      popTarget()
      // 会把当前watcher会从dep.subs数组中移除,把watcher里面的dep也移除
      this.cleanupDeps()
    }
    return value
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
  • run方法
run () {
    // 标记当前watcher是否是存活的状态,默认为true,可处理
    if (this.active) {
      // 调用其get方法,如果是渲染watcher会调用getter,执行updateComponent方法渲染DOM更新页面
      // 之后用value记录返回结果,如果是渲染watcher没有返回结果,value是undefined,渲染函数的cb是noop空函数.
      // 如果是用户watcher,继续执行,获取旧值记录新值,调用cb回调函数,侦听器的function就是回调函数,
      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) {
          // 如果是用户watcher,添加异常处理
          try {
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "${this.expression}"`)
          }
        } else {
          // 如果是其他watcher,直接调用
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

通过源码可以看出来,watcher中的属性可以传数组,也可以是对象,也可以是函数,里面的handler也可以嵌套

const vm = new Vue({
    el: '#app',
    data: {
      message:'Hello Vue',
      user: {
        firstName: '张',
        lastName: '三',
        fullName: ''
      },
      count: 0
    },
    computed: {
      reversedMessage: function () {
        return this.message.split('').reverse().join('')
      }
    },
    watch: {
      // 这里不给user.firstName和lastName单独设置watcher,是因为没有办法配置立即执行immediate和深度监听deep
      // 监听对象user,要提供handler,就是回调函数,还有配置立即执行immediate和深度监听deep
      // 在侦听器里面,当数据变化的时候,还可以做异步处理这些更复杂的内容
      'user': [{
        handler: function (newValue, oldValue) {
          this.user.fullName = this.user.firstName + this.user.lastName
        },
        deep: true,
        immediate: true
      }, {
        handler: function (newValue, oldValue) {
          this.count++
        },
        deep: true,
        immediate: true
      }]
    }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
更新时间: 2022-01-13 21:13