likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 11

阅读过程中的相关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的全链路图

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

watch的主要使用方式

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

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

源码分析

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

先简单过一下Watcher类的源码

Watcher类

Watcher类思维导图

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

完整Watcher类源码:

// 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就是开发者写的回调函数,如当前示例中的
count(newVal, oldVal) {
  console.log('alan->watch count', newVal, oldVal)
},

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

  • expression是watch对象的键

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

  • getter是一个取值函数,如果expOrFn是函数,则直接使用expOrFn,否则通过parsePath得到一个取值函数
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,执行更新流程。

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

  • update()函数,执行更新,其实里面是调用run()方法,判断是同步调用还是异步调用
  • run(),这个方法是】里执行用户写的回调函数cb

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

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

先看初始化流程

初始化Watch流程

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

  1. 从图中可以看到,首先是执行Vue的初始化过程,进入Vue._init
  2. 接着调用initWatch,里面调用createWatcher,实际上就是调用$watch
  3. $watch函数是new Vue之前在stateMixin(Vue)Vue.prototype挂载实例方法中赋值的函数
// 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时的逻辑,构造时,给一系列属性赋值
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()的实现
/**
 * 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方法实现
/**
 * 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. 完成初始化

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

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

上面分析的过程中出现了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的工作过程就结束了

调用链路如图:

05 | 【阅读Vue2源码】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()
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

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

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

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;
}

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

可以看到效果正常。

完整代码

mini-watcher.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

<!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>