Vue源码阅读(10):详细解析 vm.$watch 的实现原理

 我的开源库:

看原理解析前,先把 $watch 的官方文档看一下。

 vm.$watch API 的作用很简单,就是对一个目标进行监控,一旦该目标变化了的话,就会触发注册的回调函数,接下来开始看源码。

1,src/core/instance/state.js ==> stateMixin()

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
    // vm.$watch 方法的核心,借助 Watcher 实现功能
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {
      // 如果 immediate 为 true 的话,立即执行回调函数
      cb.call(vm, watcher.value)
    }
    return function unwatchFn () {
      watcher.teardown()
    }
  }
}

$watch 方法的内容很简单,因为实现 $watch 功能的代码主要在 Watcher 类里面,$watch 方法中的代码主要进行一些参数的处理和附加功能的实现。

1-1,首先看第一段代码的作用。

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

我们知道,$watch API 有两种写法:

this.$watch('numA',() => {
  console.log('numA 改变了')
},{
  immediate: true
})

this.$watch('numB',{
  handler(){
    console.log('numB 改变了')
  },
  immediate: true
})

第一种写法:第一个参数是监控的目标,第二个参数是回调函数,第三个参数是配置对象;

第二种写法:第一个参数是监控的目标,第二个参数是一个对象,对象中的 handler 函数是对应的回调函数,配置对象也写在这个对象中;

$watch 中的第一段代码就是用于处理第二种写法的,将第二种写法转换成第一种写法,确保执行到下面的代码时,参数的形式只能是第一种,我们看下 createWatcher() 的源码。

function createWatcher (
  vm: Component,
  keyOrFn: string | Function,
  handler: any,
  options?: Object
) {
  // 如果 handler 是对象类型的话,需要进行下数据整形,确保 handler 指向处理函数,options 指向配置对象
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  // 如果 handler 是字符串类型的话,从 vm 实例中获取到对应的处理函数
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  // 调用 vm.$watch 实现侦听属性的功能
  // 代码执行到这里,handler 只能是函数类型的
  return vm.$watch(keyOrFn, handler, options)
}

在这里,handler 是一个对象,里面包含 handler 回调函数和配置信息,需要把他们分离开并存储到对应的变量中。

接下来,如果 handler 是一个字符串的话,说明 handler 函数是注册在 Vue 实例中的一个方法,通过 vm[handler] 获取并赋值给 handler。

1-2,接下来看第二段代码

if (options.immediate) {
  // 如果 immediate 为 true 的话,立即执行回调函数
  cb.call(vm, watcher.value)
}

这段代码是用于处理 immediate 配置参数的,如果配置的 immediate 属性为 true 的话,则立即执行 cb 回调函数,回调函数中的 this 指向 vm(当前的组件实例),传递的参数是当前 $watch 监控目标的值。

1-3,最后的返回函数

return function unwatchFn () {
  watcher.teardown()
}

通过官方文档可知,$watch API 最后会返回一个函数,执行该函数,会解除对目标的监控。内部的实现是调用了 watcher.teardown() 方法,该方法的内部实现也很简单,只是将自己(Watcher 实例)从存储的 Dep 实例中移除掉,具体内容下面再说。

接下来,看最重要的一行代码:

// vm.$watch 方法的核心,借助 Watcher 实现功能
const watcher = new Watcher(vm, expOrFn, cb, options)

2,new Watcher(vm, expOrFn, cb, options) 做了什么

我们以下面的业务代码为例:

<template>
  <div id="app">
    <h1>{{person.age}}岁了</h1>
    <button @click="changeAge">change age</button>
  </div>
</template>
<script>
export default {
  name: 'App',
  data(){
    return {
      person: {
        age: 1
      }
    }
  },
  mounted() {
    this.$watch('person.age',function ageChange() {
      console.log('年龄改变了')
    })
  },
  methods: {
    changeAge(){
      this.person.age++
    },
  }
}
</script>

如果用户点击了 change age 按钮的话,age 属性便会变化,然后 ageChange 回调函数便会触发执行。

在这个例子中,new Watcher(vm, expOrFn, cb, options) 中的 expOrFn 是 'person.age',cb 对应 ageChange 回调函数,接下来我们开始看 Watcher 类的源码。

export default class Watcher {
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: Object
  ) {
    this.vm = vm
    this.cb = cb
    // getter 属性必须是一个函数,并且函数中有对使用到的值的读取操作(用于触发数据的 getter 函数,在 getter 函数中进行该数据依赖的收集)
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      // 而如果是一个字符串类型的话,例如:"a.b.c.d",是一个数据的路径
      // 就将 parsePath(expOrFn) 赋值给 this.getter,
      // parsePath 能够读取这个路径字符串对应的数据(一样能触发 getter,触发数据的 getter 是关键)
      this.getter = parsePath(expOrFn)
    }
    this.value = this.get()
  }

  get () {
    // 将自身实例赋值到 Dep.target 这个静态属性上(保证全局都能拿到这个 watcher 实例),
    // 使得 getter 函数使用数据的 Dep 实例能够拿到这个 Watcher 实例,进行依赖的收集。
    // pushTarget 操作很重要
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      // 执行 getter 函数,该函数执行时,会对响应式的数据进行读取操作,这个读取操作能够触发数据的 getter,
      // 在 getter 中会将 Dep.target 这个 Watcher 实例存储到该数据的 Dep 实例中,以此就完成了依赖的收集
      // 依赖收集需要执行 addDep() 方法完成
      value = this.getter.call(vm, vm)
    } catch (e) {
        ......
    } 
    // 将 expOrFn 对应的值返回出去
    return value
  }
}

2-1,this.getter = parsePath(expOrFn)

在我们的例子中,expOrFn 的值是 'person.age',所以会进入 this.getter = parsePath(expOrFn) 的逻辑。Watcher 中的 getter 属性必须是一个函数,并且调用这个函数要能够访问并返回目标值,这个访问的动作很关键,因为要触发数据的 getter。所以 parsePath 方法的作用是将 'person.age',转换成如下的函数:

const segments = ['person','age']
function (obj) {  
  for (let i = 0; i < segments.length; i++) {
    if (!obj) return
    obj = obj[segments[i]]
  }
  return obj
}

2-2,接下来执行 get 方法

get 方法中执行了 pushtarget(this) 和 value = this.getter.call(vm, vm),这两条语句前面的博客已经说了,具体原理就不解释了,作用是:进行依赖收集,使 vm.person.age 这个属性对应的 Dep 实例存储当前的 Watcher 实例。

3,数据发生变化到回调函数触发执行的整个流程

3-1,Object.defineProperty setter

数据发生变化,对应的 setter 方法会被触发执行。

// 在此进行派发更新
set: function reactiveSetter (newVal) {
  // 触发依赖的更新
  dep.notify()
}

setter 方法中会执行该数据对应 Dep 实例的 notify() 方法。

3-2,class Dep ==> notify()

// 触发 subs 数组中依赖的更新操作
notify () {
  // 数组的 slice 函数具有拷贝的作用
  const subs = this.subs.slice()
  // 遍历 subs 数组中的依赖项
  for (let i = 0, l = subs.length; i < l; i++) {
    // 执行依赖项的 update 函数,触发执行依赖
    subs[i].update()
  }
}

notify 方法会遍历执行 subs 数组中 Watcher 实例的 update 方法

3-3,class Watcher ==> update()

update () {
  this.run()
}

3-4,class Watcher ==> run()

run () {
  if (this.active) {
    const value = this.get()
    // 下面进行回调函数 cb 的处理
    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
      this.cb.call(this.vm, value, oldValue)
  }
}

首先调用 this.get() 获取最新的值,然后使用 oldValue 存储旧的值。这样,新的值和旧的值我们都获取到了,最后以这两个值为参数触发执行回调函数。

至此,$watche API 的整个流程就讲完了。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值