Gahing's blog Gahing's blog
首页
知识体系
  • 前端基础
  • 应用框架
  • 工程能力
  • 应用基础
  • 专业领域
  • 业务场景
  • 前端晋升 (opens new window)
  • Git
  • 网络基础
  • 算法
  • 数据结构
  • 编程范式
  • 编解码
  • Linux
  • AIGC
  • 其他领域

    • 客户端
    • 服务端
    • 产品设计
软素质
  • 面试经验
  • 人生总结
  • 个人简历
  • 知识卡片
  • 灵感记录
  • 实用技巧
  • 知识科普
  • 友情链接
  • 美食推荐 (opens new window)
  • 收藏夹

    • 优质前端信息源 (opens new window)
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Gahing / francecil

To be best
首页
知识体系
  • 前端基础
  • 应用框架
  • 工程能力
  • 应用基础
  • 专业领域
  • 业务场景
  • 前端晋升 (opens new window)
  • Git
  • 网络基础
  • 算法
  • 数据结构
  • 编程范式
  • 编解码
  • Linux
  • AIGC
  • 其他领域

    • 客户端
    • 服务端
    • 产品设计
软素质
  • 面试经验
  • 人生总结
  • 个人简历
  • 知识卡片
  • 灵感记录
  • 实用技巧
  • 知识科普
  • 友情链接
  • 美食推荐 (opens new window)
  • 收藏夹

    • 优质前端信息源 (opens new window)
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 前端基础

  • 应用框架

    • UI 框架

      • Angular

      • React

      • Solid

      • Svelte

      • Vue

        • Vue 项目中的 data-v-xxx 是怎么生成的
        • Vue之从零编写一个ContextMenu(右键菜单)插件
        • Vue 第一个组件,浏览器后退无法触发beforeRouteLeave的问题与解决
        • Vue问题记录
        • vue 中 updated 的执行时机
        • 源码解析

          • Vue 源码解析-0.前置准备
          • Vue实例化过程
          • Vue响应式原理
          • Vue响应式之data
          • Vue响应式之props
          • Vue响应式之computed
          • Vue响应式之watch
            • watch 干么用的
            • 实现
            • 源码分析
              • 附加题1
              • 附加题2
            • 总结
          • Vue响应式之异步更新与nextTick
          • Vue响应式之组件更新
      • 框架本质

    • 开发框架

    • 组件库

  • 工程能力

  • 应用基础

  • 专业领域

  • 业务场景

  • 大前端
  • 应用框架
  • UI 框架
  • Vue
  • 源码解析
gahing
2019-11-15
目录

Vue响应式之watch

# watch 干么用的

先上官方文档 (opens new window) ,值支持 string | Function | Object | Array

对于对象类型,支持 immediate/deep/handler 参数

immediate: 开始监听时立即调用 handler 此时 oldValue 一定为 undefined
deep: 是否深入监听对象,即监听对象的子孙属性

# 实现

老样子,先上示例,简单起见,我们传入一个对象类型的 watch

<script type="text/x-template" id="demo-template">
  <div>
    {{user.name}}
  </div>
</script>

<script>
  var vm = new Vue({
    el: '#el',
    template: '#demo-template',
    data: {
      user: {
        name: "xxx"
      }
    },
    watch: {
      user:{
        deep:true,
        immediate:true,
        handler:function(val,oldVal){
          console.log(val,oldVal)
        }
      }
    },
  })
</script>
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

尝试自己实现 watch

我们先看下 Watcher 支持哪些参数

首先是 expOrFn ,用于取值的,支持 xx.xx 字符串,因此这个参数应该是用于传入 watch 的属性

其次是 cb ,在 Watcher 执行更新时会进行触发,参数为新旧两个值,那么这个就很适合我们的 handler 函数

最后是 options 的 deep 和 lazy 参数, deep 应该是一样的,lazy 含义貌似和 immediate 相反

因此,我们遍历了 watch 的每个属性,并各自内部创建一个 Watcher

let watch = vm.$watch[key]
watcher[key] = new Watcher(vm,key,watch.handler,{
  lazy: !watch.immediate,
  deep: watch.deep
})
1
2
3
4
5

实例化 Watcher 时, watch 的 key 会被 parsePath 解析成一个 getter 方法,在实例化的最后,调用该 getter 以至 watcher 添加上 watch 的 key 的依赖

当观察目标发生变更时,通知 watcher 更新最后调用了 handler 函数

但是注意一点,当 lazy 为 true ( immediate = false )的时候,依赖是还没有绑定的,怎样才能调用 Watcher.get 去绑定依赖呢?

computed 属性创建的 Watcher 也是 lazy 的,同时 computed 属性是可以被取值的,当在其他地方访问到该计算属性(被取值)时才会开始依赖收集.
但是 watch 不同, watch 是对某个值的监听,不会有被取值的说法,于是 get 也就调用不到,依赖也就绑定不了

我们换个思路,无论是否 immediate 都会调用 get 进行去添加依赖,即 lazy 都设置为 false

并且在实例化 Watcher 之后,手动触发一个 handler

这样就可以实现 immediate 的效果了(我真是个小精灵鬼

不过这里我们还有一个疑问, Watcher 是怎么实现 deep ,这个我们稍后分析源码的时候再看

# 源码分析

前言:和我上面的实现基本无异

对于 watch ,Vue 组件上没有特殊的处理,我们从 initState 开始分析

这里有个小知识点,对 watch 参数做了判断

// src\core\instance\state.js

// nativeWatch = ({}).watch
if (opts.watch && opts.watch !== nativeWatch) {
  initWatch(vm, opts.watch)
}
1
2
3
4
5
6

注释是说:Firefox has a "watch" function on Object.prototype

根据 MDN Object.prototype.watch (opens new window) 上的描述,在 Firefox 58 版本之前, Object 原型上有个 watch 方法用于监听某个属性值的变动的。

所以以后在 watch 方法的判断上注意避免踩坑.

前面合并参数的时候已经判断过 watch 参数是否为纯对象了,本以为这里如果还有值就只有有赋值的情况,没想到还有 Firefox 的深坑

继续分析,进入 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)
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

当 watch[key] 值为数组时遍历每一项并执行 createWatcher ,否则直接执行 createWatcher

之所以支持数组是考虑方法职责明确,方便代码复用

进入 createWatcher

// src\core\instance\state.js

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)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

纯对象的解析就不说了,来看下这个字符串类型的 handler 是怎么回事

这时候能从 vm 上取到的方法,也就 methods 中的了

所以支持字符串属性是为了代码复用

最后调用 vm.$watch ,这个在 官方文档 (opens new window) 上有说明

vm.$watch( expOrFn, callback, [ options:{deep,immediate}] ) 该方法最后会返回一个取消监听的方法 unwatch

我们在 1.Vue实例化过程 中有提过,Vue 初始化的时候会执行 stateMixin 方法,最后再原型上挂载 $watch 方法

// src/core/instance/state.js

export function stateMixin (Vue: Class<Component>) {
  // ... 省略部分代码
  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()
    }
  }
}
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

先说 unwatchFn ,执行 unwatchFn 时会调用 watcher.teardown 作用就是删除依赖,依赖清除观察者等等

里面一些细节: active 控制等可以自行研究

if (isPlainObject(cb)) {
  return createWatcher(vm, expOrFn, cb, options)
}
1
2
3

这个处理是外部使用 $watch 时进行的,我们初始化 watch 时已经处理过纯对象的情况了

接着根据传入值设置 options , Watcher 中有用到的只有 deep 和 user

user 表示是用户主动创建的 Watcher ,其作用就是出现异常时提示用户哪个监听器回调出错了

系统自己创建的一般是不会有问题

而后实例化 Watcher: watcher = new Watcher(vm, expOrFn, cb, options) ,和我们前文分析的一样,会在实例化过程中解析 expOrFn 顺序访问对象(如'a.b'则顺序访问 a,a.b)并将 watcher 添加这些对象(a,a.b)的依赖, 之后无论是 a 还是 a.b 发生变更都会触发 watcher 更新执行 handler

实例化后,此时 watcher.value 的值为所观察的属性的最新值,若设置了 immediate ,将该值传入 handler 并调用

cb.call(vm, watcher.value)
// 此时 oldValue 必为 undefined
1
2

最后再来分析下 deep

对于这个场景:

data: {
  user: {
    name: {
      firstName: 'xxx'
    }
  }
},
watch: {
  'user.name': {
    deep: true,
    handler: function (val, oldVal) {
      console.log(val, oldVal)
    }
  }
},
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

user.age 变化会不会触发 handler?
user.name.firstName 变化会不会触发 handler ,怎么实现

前者肯定是不会的,如果 user 的值(对象引用)变化了会触发

后者肯定会,在 Watcher 中做了处理

get () {
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    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
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

一开始执行 getter 后,watcher 添加了 user 和 name 的依赖

而后执行 traverse(value)

// src\core\observer\traverse.js

/**
 * Recursively traverse an object to evoke all converted
 * getters, so that every nested property inside the object
 * is collected as a "deep" dependency.
 */
export function traverse (val: any) {
  // seenObjects = new Set()
  _traverse(val, seenObjects)
  seenObjects.clear()
}

function _traverse (val: any, seen: SimpleSet) {
  let i, keys
  const isA = Array.isArray(val)
  if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
    return
  }
  if (val.__ob__) {
    const depId = val.__ob__.dep.id
    if (seen.has(depId)) {
      return
    }
    seen.add(depId)
  }
  if (isA) {
    i = val.length
    while (i--) _traverse(val[i], seen)
  } else {
    keys = Object.keys(val)
    i = keys.length
    while (i--) _traverse(val[keys[i]], seen)
  }
}
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

递归遍历对象,触发嵌套属性已转换的 getter ,以便 watcher 添加这些嵌套属性的依赖

比如 name 的嵌套属性有 firstName ,此时 watcher 会添加 firstName 的依赖

这里有个细节,进行 traverse 时 seenObjects 用的是一开始就生成的 Set ,用完将其清空,这样每次执行 traverse 时用的都是 seenObjects ,而不是每次都新创建 Set ,因此新建 Set 的性能以及 GC 方法的处理肯定都不如复用 seenObjects

# 附加题1

firstName 变更,返回的 oldValue.firstName 是原来的值么?

答案

不是,还是新值,因为 value 和 oldValue 是同一个引用,此时访问 oldValue.firstName 其实和访问 value.firstName 是一样的

# 附加题2

如果这时执行 vm.$set(vm.user.name,'lastName','ttt') 会触发 handler 么,为什么?

答案

会,因为一开始依赖收集,触发了 user.name 的 getter , Watcher 观察者会添加了 user.name 的值的依赖

if (childOb) {
  childOb.dep.depend()
  // ...
}
1
2
3
4

通过 set 方法对 user.name 的属性值进行变更时,user.name 值的依赖会通知观察者更新,于是会触发 handler

最后,返回的 value/oldValue 都是同一个引用

# 总结

从 watch 和 computed 的源码分析上来看,两者的区别一个是主动监听,一个是被动监听

在分析源码的时候,也明白了一些平时忽略的用法,受益良多

编辑 (opens new window)
上次更新: 2024/09/01, 23:56:56
Vue响应式之computed
Vue响应式之异步更新与nextTick

← Vue响应式之computed Vue响应式之异步更新与nextTick→

最近更新
01
浅谈代码质量与量化指标
08-27
02
快速理解 JS 装饰器
08-26
03
Vue 项目中的 data-v-xxx 是怎么生成的
09-19
更多文章>
Theme by Vdoing | Copyright © 2016-2024 Gahing | 闽ICP备19024221号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式