Vue 中的 watch 及源码解析

在这里插入图片描述

Watch API 官方文档

Watch 用法

1. 常见用法
<template>
  <div class="home">
    <h3>Watch 用法1:常见用法</h3>
    <input v-model="message" />
    <p>{{ copyMessage }}</p>
  </div>
</template>

<script>
export default {
  name: "home",
  data() {
    return {
      message: "Hello Vue",
      copyMessage: "",
    };
  },
  watch: {
    message(value) {
      this.copyMessage = value;
    },
  },
};
</script>

一开始,页面上面显示空白,在 input 里面输入内容的时候,message 的值发生了改变,watch 监听到 message 值的变化,将 value 赋值给 copymessage ,此时 copymessage 有了值,在界面上面显示出来。

2. 绑定方法
<template>
  <div class="home">
    <h3>Watch 用法2:绑定方法</h3>
    <input v-model="message" />
    <p>{{ copyMessage }}</p>
  </div>
</template>

<script>
export default {
  name: "home",
  data() {
    return {
      message: "Hello Vue",
      copyMessage: "",
    };
  },
  watch: {
    message: "handleMessage",
  },
  methods: {
    handleMessage(value) {
      this.copyMessage = value;
    },
  },
};
</script>

一开始,页面上面显示空白,在 input 里面输入内容的时候,message 的值发生了改变,watch 监听到 message 值的变化,触发 handleMessage 方法, handleMessage 将 value 赋值给 copymessage ,此时 copymessage 有了值,在界面上面显示出来。

3. deep + handler
<template>
  <div class="home">
    <h3>Watch 用法3:deep + handler</h3>
    <input v-model="deepMessage.a.b" />
    <p>{{ copyMessage }}</p>
  </div>
</template>

<script>
export default {
  name: "home",
  data() {
    return {
      deepMessage: {
        a: {
          b: "Deep Message",
        },
      },
      copyMessage: "",
    };
  },
  watch: {
    deepMessage: {
      handler: "handleDeepMessage",
      // 开启深度监听
      deep: true,
    },
  },
  methods: {
    handleDeepMessage(value) {
      this.copyMessage = value.a.b;
    },
  },
};
</script>

一开始,页面上面显示空白,在 input 里面输入内容的时候,deepMessage.a.b 的值发生了改变,watch 监听到 deepMessage.a.b 值的变化,触发 handleDeepMessage方法, handleDeepMessage将 value 赋值给 copymessage ,此时 copymessage 有了值,在界面上面显示出来

4. immediate
<template>
  <div class="home">
    <h3>Watch 用法4:immediate</h3>
    <input v-model="message" />
    <p>{{ copyMessage }}</p>
  </div>
</template>

<script>
export default {
  name: "home",
  data() {
    return {
      message: "Hello Vue",
      copyMessage: "",
    };
  },
  watch: {
    message: {
      handler: "handleMessage",
      // 提前把 message 的值渲染到界面上
      immediate: true,
    },
  },
  methods: {
    handleMessage(value) {
      this.copyMessage = value;
    },
  },
};
</script>

页面第一次渲染的时,message 没有变化,copyMessage 也就没有被赋值。想要显示出来,可以设置 immediate,提前把 message 的值渲染到界面上。

5. 绑定多个 handler
<template>
  <div class="home">
    <h3>Watch 用法5:绑定多个 handler</h3>
    <input v-model="message" />
    <p>{{ copyMessage }}</p>
  </div>
</template>

<script>
export default {
  name: "home",
  data() {
    return {
      message: "Hello Vue",
      copyMessage: "",
    };
  },
  watch: {
    message: [
      {
        handler: "handleMessage",
      },
      "handleMessage2",
      function (value) {
        this.copyMessage = this.copyMessage + "...";
      },
    ],
  },
  methods: {
    handleMessage(value) {
      this.copyMessage = value;
    },
    handleMessage2(value) {
      this.copyMessage = this.copyMessage + "*";
    },
  },
};
</script>

message 发生变化时,依次调用handleMessage,handleMessage2,function 函数。对 copyMessage 去做出相对应的赋值。

6. 监听对象属性
<template>
  <div class="home">
    <h3>Watch 用法6:监听对象属性</h3>
    <input v-model="deepMessage.a.b" />
    <p>{{ copyMessage }}</p>
  </div>
</template>

<script>
export default {
  name: "home",
  data() {
    return {
      deepMessage: {
        a: {
          b: "Hello Vue",
        },
      },
      copyMessage: "",
    };
  },
  watch: {
    "deepMessage.a.b": "handleMessage",
  },
  methods: {
    handleMessage(value) {
      this.copyMessage = value;
    },
  },
};
</script>

一开始,页面上面显示空白,在 input 里面输入内容的时候,deepMessage.a.b 的值发生了改变,watch 监听到 deepMessage.a.b 值的变化,触发 handleMessage方法, handleMessage 将 value 赋值给 copymessage ,此时 copymessage 有了值,在界面上面显示出来。


watch能监听computed的属性?

答案是:可以的

<template>
  <div class="home">
    <p>{{ total }}</p>
    <button @click="addGoodsNum">添加商品</button>
  </div>
</template>

<script>
export default {
  data: function () {
    return {
      goodsNum: 0,
      price: 100,
    };
  },
  mounted: function () {
    this.goodsNum = 1;
  },
  methods: {
    addGoodsNum: function () {
      this.goodsNum = this.goodsNum + 1;
    },
  },
  computed: {
    total: function () {
      var totalPrice = this.goodsNum * this.price;
      return totalPrice;
    },
  },
  watch: {
    total: function (newValue, oldValue) {
      console.log('总价在变化', newValue)
    },
  },
};
</script>

上面这段代码中,total 是由 this.goodsNum * this.price 计算而来的,同时也可以被 watch 监听,在 total 发生变化的时候,可以获取最新的值。

Watcher 原理

vm.$watch 其实是对 Watcher 的一种封装

或许你听说过收集依赖,那依赖是什么?也就是说收集谁?

当数据发生变化的时候,需要通知用到这些数据的地方,而使用这个数据的地方有很多,类型也很杂。这时需要抽象出一个能集中处理这些情况的类(Watcher)。收集的时候,只收集这个类的实例。那么数据变化的时候,也只通知它,在由它负责通知其他地方。

相当于一个中介的角色,数据变化时通知它,然后它再通知其他地方。

(这坨代码有点长,可以先大致浏览一下)

/* @flow */

import {
  warn,
  remove,
  isObject,
  parsePath,
  _Set as Set,
  handleError,
  invokeWithErrorHandling,
  noop
} from '../util/index'

import { traverse } from './traverse'
import { queueWatcher } from './scheduler'
import Dep, { pushTarget, popTarget } from './dep'

import type { SimpleSet } from '../util/index'

let uid = 0

/**
 *观察者解析表达式,收集依赖关系,
 *并在表达式值更改时触发回调。
 *这用于$ watch()api和指令。
 */ 
export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  lazy: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: SimpleSet;
  newDepIds: SimpleSet;
  before: ?Function;
  getter: Function;
  value: any;

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    // options
    if (options) {
      // 如果用户传入了 deep 参数
      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
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.lazy // 结合 Watcher 的 dirty 属性来分辨计算属性的返回值是否发生了改变
    this.deps = [] // 记录自己都订阅了哪些 Dep
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // expOrFn 参数是支持函数的,如果是函数直接将它赋值给 getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      // 使用 parsePath 读取 keypath 中的数据。keyPath => deepMessage.a.b
      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()
  }

  /**
   * 实例化一个渲染 watcher 的时候,首先进入 watcher 的构造函数逻辑,然后会执行它的 this.get() 方法,进入 get 函数
   */
  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

      // 递归去访问 value,触发它所有子项的 getter
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

  /**
   * 记录自己都订阅了哪些 Dep
   */
  addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      // 记录当前的 Watcher 已经订阅了这个 Dep,避免重复的添加
      this.newDepIds.add(id)
      // 记录自己都订阅了哪些 Dep
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        // 把当前的 watcher 订阅到这个数据持有的 dep 的 subs 中,这个目的是为后续数据变化时候能通知到哪些 subs 做准备。
        dep.addSub(this)
      }
    }
  }

  /**
   * 清理依赖项收集。
   * 每次添加完新的订阅,会移除掉旧的订阅
   */ 
  cleanupDeps () {
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)
      }
    }
    let tmp = this.depIds
    this.depIds = this.newDepIds
    this.newDepIds = tmp
    this.newDepIds.clear()
    tmp = this.deps
    this.deps = this.newDeps
    this.newDeps = tmp
    this.newDeps.length = 0
  }

  /**
   * Subscriber interface.
   * 依赖项更改时将被调用
   */
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      // 需要重新计算 "计算属性" 的返回值
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }

  /**
   * Scheduler job interface.
   * Will be called by the scheduler.
   */
  run () {
    if (this.active) {
      // 获取当前的值
      const value = this.get()
      // 满足新旧值不等、新值是对象类型、deep 模式任何一个条件
      if (
        value !== this.value ||
        isObject(value) ||
        this.deep
      ) {
        // 设置新值
        const oldValue = this.value
        this.value = value
        // 
        if (this.user) {
          const info = `callback for watcher "${this.expression}"`
          invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
        } else {
          // 执行 watcher 的回调,注意回调函数执行的时候会把第二个和第三个参数传入新值 value 和旧值 oldValue
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }

  /**
   * Evaluate the value of the watcher.
   * This only gets called for lazy watchers.
   */
  evaluate () {
    // 重新获取值,用于 computed
    this.value = this.get()
    // 计算属性的值并没有变,不需要重新计算
    this.dirty = false
  }

  /**
   * Depend on all deps collected by this watcher.
   */
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }

  /**
   * 从所有依赖项的 Dep 列表中将自己移除
   */
  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.

      // 取消观察数据,本质上是把 watcher 实例从当前正在观察的状态的依赖列表中移除
      if (!this.vm._isBeingDestroyed) {
        remove(this.vm._watchers, this)
      }
      // 从所有依赖项的 Dep 列表中将自己移除
      let i = this.deps.length
      while (i--) {
        this.deps[i].removeSub(this)
      }
      this.active = false
    }
  }
}
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
  // 先执行 new Watcher 来实现 vm.$watch 的基本功能
  const watcher = new Watcher(vm, expOrFn, cb, options)
  // 如果用户使用了 immediate 参数,则立即执行一次 cb
  if (options.immediate) {
    const info = `callback for immediate watcher "${watcher.expression}"`
    pushTarget()
    invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
    popTarget()
  }
  // 取消观察函数,用来停止触发回调
  return function unwatchFn () {
    watcher.teardown()
  }
}

当用户执行这个函数时,实际上是执行了 watcher.teardown() 来取消观察数据,其本质是把 watcher 实例从当前正在观察的状态依赖列表中移除。

首先是在 Watcher 中记录自己都订阅过哪些 Dep(收集依赖存储的地方)

addDep

/**
   * 记录自己都订阅了哪些 Dep
   */
addDep(dep: Dep) {
  const id = dep.id
  if (!this.newDepIds.has(id)) {
    // 记录当前的 Watcher 已经订阅了这个 Dep,避免重复的添加
    this.newDepIds.add(id)
    // 记录自己都订阅了哪些 Dep
    this.newDeps.push(dep)
    if (!this.depIds.has(id)) {
      // 把当前的 watcher 订阅到这个数据持有的 dep 的 subs 中,这个目的是为后续数据变化时候能通知到哪些 subs 做准备。
      dep.addSub(this)
    }
  }
}

从所有依赖项的 Dep 列表中将自己移除

/**
   * 从所有依赖项的 Dep 列表中将自己移除
   */
teardown() {
  if (this.active) {
    // 取消观察数据,本质上是把 watcher 实例从当前正在观察的状态的依赖列表中移除
    if (!this.vm._isBeingDestroyed) {
      remove(this.vm._watchers, this)
    }
    // 从所有依赖项的 Dep 列表中将自己移除
    let i = this.deps.length
    while (i--) {
      this.deps[i].removeSub(this)
    }
    this.active = false
  }
}

Watcher 想监听某个数据,就会触发某个数据收集依赖的逻辑,将自己收集进去,然后当它发生变化时,就会通知 Watcher。

deep 参数的实现原理

除了触发当前这个被监听数据的收集依赖的逻辑之外,还要把当前监听的这个值在内的所有子值都触发一遍收集依赖逻辑。这就可以实现当前这个依赖的所有子数据发生变化时,通知当前 Watcher。

/**
 * 实例化一个渲染 watcher 的时候,首先进入 watcher 的构造函数逻辑,然后会执行它的 this.get() 方法,进入 get 函数
 */
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 {
    // 递归去访问 value,触发它所有子项的 getter
    if (this.deep) {
      traverse(value)
    }
    popTarget()
    this.cleanupDeps()
  }
  return value
}
const seenObjects = new Set()

/**
 * 递归 value 的所有子值来触发它们收集依赖的能力
 * @param {*} val 
 */
export function traverse (val: any) {
  _traverse(val, seenObjects)
  seenObjects.clear()
}

function _traverse (val: any, seen: SimpleSet) {
  let i, keys
  const isA = Array.isArray(val)
  // 如果它不是 Array 和 Object,或者已经被冻结了,那么直接返回
  if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
    return
  }
  if (val.__ob__) {
    // 拿到 val 的 dep.id,用这个 id来保证不会重复收集依赖
    const depId = val.__ob__.dep.id
    if (seen.has(depId)) {
      return
    }
    seen.add(depId)
  }
  // 如果是数组,则循环数组,将数组中的每一项递归调用 _traverse
  if (isA) {
    i = val.length
    while (i--) _traverse(val[i], seen)
  } else {
    // 如果 Object 类型的数据,则循环 Object 中的所有 key,然后执行一次读取操作,再递归子值。
    keys = Object.keys(val)
    i = keys.length
    // val[keys[i]] => 读取 key 的时候,会触发收集依赖的操作,把当前的 Watcher 收集进去。
    while (i--) _traverse(val[keys[i]], seen)
  }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值