05 | 【阅读Vue2源码】watch实现原理

阅读过程中的相关demo代码:github.com/AlanLee97/r...

简单概括

开发者定义的options.watch,就是Vue内部的Watcher类的实例,。

通过Dep类连接data的属性与Watcher之间的关系,在初始化Vue/Vue组件时,使用Object.defineProperty的属性描述符的get/set完成响应式,其中就是使用到Dep类的实例进行依赖收集,在getter中进行依赖收集dep.depend(),就是把Watcher的实例收集起来,在setter中进行通知依赖更新dep.notify(),notify函数则会遍历dep中收集的watcher实例,执行watcherrun()方法,实际就是执行开发者定义的options.watch的回调函数。

那么具体过程是怎么实现的呢?下面来分析源码。

思维导图

watch的全链路图

watch的主要使用方式

先回顾下watch的几个主要的定义方式

  1. 函数方式
js 复制代码
data() {
  return {
    count: 0
  }
},
watch: {
  count(newVal, oldVal) {
    console.log('alan->watch count', newVal, oldVal)
  }
},
  1. 对象方式
js 复制代码
data() {
  return {
    countObj: {
      value: 0
    }
  }
},
watch: {
  countObj: {
    handler(newVal) {
      console.log('alan->watch countObj', newVal)
    },
    immediate: true,
    deep: true
  }
},
  1. 字符串方式
js 复制代码
data() {
  return {
    countObj: {
      value: 0
    }
  }
},
watch: {
  'countObj.value': function (newVal) {
    console.log('alan->watch countObj.value', newVal)
  }
},

源码分析

开发者定义的watch的每一个属性,Vue会给它new一个Watcher类的实例,所以我们主要分析它的源码。

先简单过一下Watcher类的源码

Watcher类

Watcher类思维导图

完整Watcher类源码:

js 复制代码
// src\core\observer\watcher.js
let uid = 0

// Watcher解析表达式,收集依赖项,并在表达式值发生改变时触发回调
// 用于$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) {
      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 // for lazy watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      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()
  }

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  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
  }

  /**
   * Add a dependency to this directive.
   */
  addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        dep.addSub(this)
      }
    }
  }

  /**
   * Clean up for dependency collection.
   */
  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.
   * Will be called when a dependency changes.
   */
  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()
      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) {
          const info = `callback for watcher "${this.expression}"`
          invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }

  /**
   * Evaluate the value of the watcher.
   * This only gets called for lazy watchers.
   */
  evaluate () {
    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()
    }
  }

  /**
   * Remove self from all dependencies' subscriber list.
   */
  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
    }
  }
}

其中比较重要属性和方法的有:

  • 属性

    • cb
    • expression
    • getter
    • value
  • 方法

    • get()
    • update()
    • run()

属性/方法浅析

  • cb就是开发者写的回调函数,如当前示例中的
js 复制代码
count(newVal, oldVal) {
  console.log('alan->watch count', newVal, oldVal)
},
  • expression是watch对象的键
  • getter是一个取值函数,如果expOrFn是函数,则直接使用expOrFn,否则通过parsePath得到一个取值函数
js 复制代码
if (typeof expOrFn === 'function') {
  this.getter = expOrFn
} else {
  this.getter = parsePath(expOrFn)
}
  • value是这个watch观察的这个属性的值,初始化Watcher时通过this.get()函数取到
  • get()函数获取当前观察的data的属性的值,通过getter函数取的值,这里getter取值函数,会触发初始化时Object.defineProperty观察data时的getter,进入到getter,收集依赖。到这里完成了一个watch的初始化流程,那么另外的状态就是等待用户改变data,触发setter,执行更新流程。
  • update()函数,执行更新,其实里面是调用run()方法,判断是同步调用还是异步调用
  • run(),这个方法是】里执行用户写的回调函数cb

我们可以把watch的执行过程分为两个流程:

  1. 初始化watch流程
  2. 改变数据时,watch的执行流程

先看初始化流程

初始化Watch流程

  1. 从图中可以看到,首先是执行Vue的初始化过程,进入Vue._init
  2. 接着调用initWatch,里面调用createWatcher,实际上就是调用$watch
  3. $watch函数是new Vue之前在stateMixin(Vue)Vue.prototype挂载实例方法中赋值的函数
js 复制代码
// src\core\instance\state.js
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.user = true
  // new Watcher对象
  const watcher = new Watcher(vm, expOrFn, cb, options)
  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()
  }
}

这里可以看到主要逻辑是

  • 如果是纯对象,则调用createWatcher,直接返回结果,否则执行下面的逻辑
  • 做了一个user的标记,用于区分渲染用的Watcher(即如果options.user=true,则表示这个watcher实例是对应开发者写在组件中的watch)
  • new了一个Watcher
  • 如果有immediate,就立即执行回调函数
  • 返回销毁函数unwatchFn
  1. 重点看一下new Watcher,实例化Watcher时的逻辑,构造时,给一系列属性赋值
js 复制代码
class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  lazy: boolean;
  getter: Function;
  value: any;
  // ...

  constructor (
    vm: Component,
    expOrFn: string | Function, // 定义watch时的键名
    cb: Function, // 定义watch时的键名对应的函数
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    // options
    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
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.lazy // for lazy watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      // 根据data的属性的键名,得到一个取值函数
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
      }
    }
    // 取值的这一步挺重要的,会触发data属性的getter
    // this.lazy为true是给computed用的
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  // more code ...
}
  1. 取值的这一步,this.get()挺重要的,会触发data属性的getter,dep.depend()收集依赖
  2. 看看get()的实现
js 复制代码
/**
 * Evaluate the getter, and re-collect dependencies.
 * 求值getter,并重新收集依赖
 */
get () {
  // 把当前实例的值放到Dep.target中
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    // 取options.data中的值,触发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)
    }
    // 把Dep.target清空
    popTarget()
    this.cleanupDeps()
  }
  return value
}

这里的逻辑:

  • 首先pushTarget(this),把当前实例的值放到Dep.target
  • 再调用getter取值,进行依赖收集dep.depend(),实际上就是Dep.target.addDep(this),其实就是调用watcher.addDep(dep)
  • 执行完取值过程,然后调用popTarget()Dep.target清空
  • 返回取到的值
  1. 上面getter取值过程中会触发依赖收集,其实就是调用watcher.addDep(dep),看看watcher.addDep方法实现
js 复制代码
/**
 * Add a dependency to this directive.
 */
addDep (dep: Dep) {
  const id = dep.id
  if (!this.newDepIds.has(id)) {
    this.newDepIds.add(id)
    this.newDeps.push(dep)
    if (!this.depIds.has(id)) {
      dep.addSub(this)
    }
  }
}

其实也很简单,就是把当前watcher的实例,添加到dep中,这样的Dep与Watcher就产生了关联,到这里初始化Watch的过程也完成了。

简单总结

  1. new Vue时初始化watch,创建Watcher的实例
  2. 创建Watcher的实例过程中会,取一下当前watch的data的属性值,触发依赖收集,把watcher的实例添加到Dep中,这样DepWatcher就产生了关联
  3. 完成初始化

如果对这个过程还是很迷糊,可以再看一看初始化过程的流程图:

上面分析的过程中出现了Watcher,那么我们可以对Watcher的源码进行一个预览

改变数据时,watch的执行流程

  1. 用户改变data的数据,触发Object.defineProperty的setter
  2. 触发setter时,会setter里调用dep.notify(),然后dep遍历收集的subs,其实每一个sub就是一个watcher,再调用watcher.update(),update实际是调用watcher.run(),然后会判断是否是同步调用,如果是,则立即执行,否则进行异步调用,这里的异步调用就是使用的nextTick(简单来讲,实际上就是Promise.resolve().then(cb)),在nextTick里调用watcher.run()run()方法里执行我们写的回调函数cb()
  3. 执行完cb()函数,一个watch的工作过程就结束了

调用链路如图:

简单总结

简单总结执行过程,就是:改变data数据,触发setter,通知watcher执行回调函数。

总结

总结watch的工作过程,主要分为2个流程:

  1. 初始化watch
  • new Vue时初始化watch,创建Watcher的实例
  • 创建Watcher的实例过程中,会取一下当前watch的data的属性值,触发依赖收集,把watcher的实例添加到Dep中,这样DepWatcher就产生了关联
  • 完成初始化,然后等待改变数据时触发watch的回调
  1. 改变data时,触发watch的回调
  • 改变data数据,触发setter,dep.notify()通知watcher执行cb()回调函数

可以看到,Watcher的整个工作流程需要Vue的响应式的配合才能完成,并且Watcher类对于Vue来说是一个核心的代码模块,它的用处很多,例如最重要的渲染更新的过程也是由Watcher配合实现的,Vue本身初始化时会new一个Watcher的实例,用更新函数_update()作为回调函数,当数据发生改变,触发watcher的回调,执行更新函数更新视图。另外computed也是由Watcher实现的(后期分析)。

动手实现一个MiniWatcher

在了解了Watcher的原理后,我们可以动手实现一个简单的Watcher

实现

首先实现几个重要的属性和方法

  • vm
  • cb
  • getter
  • expression
  • user
  • value
  • get()
  • update()
  • run()
js 复制代码
class MiniWatcher {
  vm = null; // 当前vue/vue组件实例
  cb = () => {}; // 回调函数
  getter = () => {}; // 取值函数
  expression = ''; // watch的键名
  user = false; // 是否是用户定义的watch
  value; // 当前观察的属性的值

  constructor(vm, expOrFn, cb, options = {}) {
    this.vm = vm;
    this.cb = cb;
    this.expression = expOrFn;
    this.getter = parseExpression(this.expression, vm, this);
    this.user = options.user;
    this.value = this.get();
  }

  get() {
    const value = this.getter();
    return value;
  }

  update() {
    nextTick(() => {
      this.run();
    })
  }

  run() {
    // 获取新值和旧值
    const newValue = this.get();
    const oldValue = this.value;
    this.value = newValue;
    this.cb.call(this.vm, newValue, oldValue);
  }
}

// 解析表达式,返回一个函数
function parseExpression(key, vm, watcher) {
  return () => {
    MiniDep.target = watcher;
    // 取值,触发getter,取值前先把watcher实例放到target中
    const value = vm.data[key];
    // 取完值后,清空Dep.target
    MiniDep.target = null;
    return value;
  }
}

function nextTick(cb) {
  return Promise.resolve().then(cb);
}

当然,Watcher的实现需要Dep的配合,我们可以实现一个简单的Dep

js 复制代码
class MiniDep {
  static target = null;
  subs = [];

  depend(sub) {
    if(sub && !this.subs.includes(sub)) {
      this.subs.push(sub);
    }
  }

  notify() {
    this.subs.forEach(sub => {
      sub && sub.update();
    })
  }
}

光有了Watcher和Dep还是不够,需要在Vue中才可以验证我们的实现,所以我们也可以实现一个简单的Vue

js 复制代码
function MiniVue(options = {}) {
  const vm = this;
  this.vm = this;
  this.data = options.data;
  this.watch = options.watch;
  this.deps = new Set();

  initData(this.data); // 初始化data
  initWatch(this.watch); // 初始化watch

  function observe(data) {
    for (const key in data) {
      defineReactive(data, key);
    }
  }

  function defineReactive(data, key) {
    const dep = new MiniDep();
    vm.deps.add(dep);
    const clonedData = JSON.parse(JSON.stringify(data));
    Object.defineProperty(data, key, {
      get: function reactiveGetter() {
        // console.log('alan->', 'get', clonedData[key]);
        dep.depend(MiniDep.target);
        return clonedData[key];
      },
      set: function reactiveSetter(value) {
        // console.log('alan->', 'set', key, value);
        dep.notify();
        clonedData[key] = value;
        return value;
      }
    });
  }
  
  function initData(data = {}) {
    for (const key in data) {
      vm[key] = vm.data[key];
      observe(vm.data);
    }
  }

  function initWatch(watch = {}) {
    for (const key in watch) {
      new MiniWatcher(vm, key, watch[key], {user: true}); // user = true,标记这是用户定义的watch
    }
  }
}

测试效果,new 一个MiniVue

js 复制代码
const vm = new MiniVue({
  data: {
    count: 0
  },
  watch: {
    count(newVal, oldVal) {
      console.log('alan->watch count', {newVal, oldVal})
    }
  }
})

const btn = document.getElementById('btnPlus');
const res = document.getElementById('res');
btn.onclick = () => {
  vm.data.count = vm.data.count + 1;
  const count = vm.data.count
  console.log('alan->count', count);
  console.log('alan->vm', vm);
  res.innerHTML = count;
}

可以看到效果正常。

完整代码

mini-watcher.js

js 复制代码
class MiniWatcher {
  vm = null; // 当前vue/vue组件实例
  cb = () => {}; // 回调函数
  getter = () => {}; // 取值函数
  expression = ''; // watch的键名
  user = false; // 是否是用户定义的watch
  value; // 当前观察的属性的值

  constructor(vm, expOrFn, cb, options = {}) {
    this.vm = vm;
    this.cb = cb;
    this.expression = expOrFn;
    this.getter = parseExpression(this.expression, vm, this);
    this.user = options.user;
    this.value = this.get();
  }

  get() {
    const value = this.getter();
    return value;
  }

  update() {
    nextTick(() => {
      this.run();
    })
  }

  run() {
    // 获取新值和旧值
    const newValue = this.get();
    const oldValue = this.value;
    this.value = newValue;
    this.cb.call(this.vm, newValue, oldValue);
  }
}

class MiniDep {
  static target = null;
  subs = [];

  depend(sub) {
    if(sub && !this.subs.includes(sub)) {
      this.subs.push(sub);
    }
  }

  notify() {
    this.subs.forEach(sub => {
      sub && sub.update();
    })
  }
}

// 解析表达式,返回一个函数
function parseExpression(key, vm, watcher) {
  return () => {
    MiniDep.target = watcher;
    // 取值,触发getter,取值前先把watcher实例放到target中
    const value = vm.data[key];
    // 取完值后,清空Dep.target
    MiniDep.target = null;
    return value;
  }
}

function nextTick(cb) {
  return Promise.resolve().then(cb);
}

function MiniVue(options = {}) {
  const vm = this;
  this.vm = this;
  this.data = options.data;
  this.watch = options.watch;
  this.deps = new Set();

  initData(this.data); // 初始化data
  initWatch(this.watch); // 初始化watch

  function observe(data) {
    for (const key in data) {
      defineReactive(data, key);
    }
  }

  function defineReactive(data, key) {
    const dep = new MiniDep();
    vm.deps.add(dep);
    const clonedData = JSON.parse(JSON.stringify(data));
    Object.defineProperty(data, key, {
      get: function reactiveGetter() {
        // console.log('alan->', 'get', clonedData[key]);
        dep.depend(MiniDep.target);
        return clonedData[key];
      },
      set: function reactiveSetter(value) {
        // console.log('alan->', 'set', key, value);
        dep.notify();
        clonedData[key] = value;
        return value;
      }
    });
  }
  
  function initData(data = {}) {
    for (const key in data) {
      vm[key] = vm.data[key];
      observe(vm.data);
    }
  }

  function initWatch(watch = {}) {
    for (const key in watch) {
      new MiniWatcher(vm, key, watch[key], {user: true}); // user = true,标记这是用户定义的watch
    }
  }
}

const vm = new MiniVue({
  data: {
    count: 0
  },
  watch: {
    count(newVal, oldVal) {
      console.log('alan->watch count', {newVal, oldVal})
    }
  }
})

const btn = document.getElementById('btnPlus');
const res = document.getElementById('res');
btn.onclick = () => {
  vm.data.count = vm.data.count + 1;
  const count = vm.data.count
  console.log('alan->count', count);
  console.log('alan->vm', vm);
  res.innerHTML = count;
}

mini-watcher.html

html 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Mini Watcher</title>
</head>

<body>
  <section id="mini-vue-app">
    <button id="btnPlus">+1</button>
    <h1 id="res"></h1>
  </section>

  <script src="./mini-watcher.js"></script>
</body>

</html>
相关推荐
林太白5 分钟前
Zustand状态库(简洁、强大、易用的React状态管理工具)
前端·javascript·react.js
Juchecar9 分钟前
Vue3 模板引用 useTemplateRef 详解
前端·vue.js
鼓浪屿11 分钟前
vue3的组件通信方式
前端
念旧Zestia29 分钟前
Oxc 家族 vs Biome——定位、能力与底层差异综述
前端
YuJie30 分钟前
vue3 无缝滚动
前端·javascript·vue.js
Juchecar30 分钟前
Vue3 表单输入 v-model 指令详解
前端·vue.js
晴空雨38 分钟前
Emmet 完全指南:让 HTML/CSS 开发效率提升 10 倍
前端·html
小野鲜40 分钟前
前端打开新的独立标签页面,并且指定标签页的大小,管理新标签页面的打开和关闭(包含源码和使用文档)
前端·javascript
一枚前端小能手41 分钟前
🌐 Web应用也想有原生App的体验,PWA来实现
前端·pwa
十五_在努力1 小时前
参透 JavaScript —— 解析浅拷贝、深拷贝及手写实现
前端·javascript