vue2源码学习 (8).响应式原理-6.Watcher

98 阅读8分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第 12 天,点击查看活动详情

8.响应式原理-6.Watcher

start

  • 前面一直提到的 Watcher ,现在来学习学习。

Watcher 的分类

其实 Watcher 的分类有很多,例如:

  1. 渲染 Watcher;
  2. 用户定义的 Watcher;
  3. 计算属性 Watcher

这里就是列举一下 Watcher 的分类,其实本质都是 Watcher,不要看到很多种类,就觉得复杂。 具体种类的 Watcher 后续会去学习。 页面开始展示的时候,初始化的就是渲染 Watcher,我们就以渲染 Watcher,作为我们学习 Watcher 的起点。

  1. 利用 vue-cli 搭建的项目,main.js是这样开始初始化的。
new Vue({
  render: (h) => h(App),
}).$mount('#app')
  1. $mount 方法是这样的
//  \src\platforms\web\runtime\index.js
 
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  // 处理挂载元素
  el = el && inBrowser ? query(el) : undefined
  // 主要是执行了 mountComponent
  return mountComponent(this, el, hydrating)
}
  1. mountComponent的逻辑

具体到 mountComponent 函数的逻辑,暂时先看 new Watcher()

\src\core\instance\lifecycle.js

// 挂载组件
export function mountComponent(
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  let updateComponent

  // 更新组件
  updateComponent = () => {
    vm._update(vm._render(), hydrating)
  }

  new Watcher(
    vm,
    updateComponent,
    noop,
    {
      before() {
        if (vm._isMounted && !vm._isDestroyed) {
          callHook(vm, 'beforeUpdate')
        }
      },
    },
    true
  )
}

整个挂载组件做了那些操作。

  • $mount 基础功能是 调用了 mountComponent;

  • mountComponent

    1. new Watcher;
    2. 传入了updateComponent一个箭头函数。
  1. Watcher`
// `\src\core\observer\watcher.js`


let uid = 0

/**
 * A watcher parses an expression, collects dependencies,
 * and fires callback when the expression value changes.
 * This is used for both the $watch() api and directives.
 * 观察者解析表达式,收集依赖项,
 * 并在表达式值更改时触发回调。
 * 这用于$watch() api和指令。
 */

// 1. 整体看下来,Watcher 其实就是一个类
export default class Watcher {
  // 2. 实例上有一堆属性 , vm 是我们的Vue实例
  vm: Component // 实例
  expression: string // 表达式
  cb: Function // 回调函数
  id: number // 唯一id
  deep: boolean // 是否深度监听
  user: boolean // 是否用户自定义的watcher
  lazy: boolean // 是否是 lazy Watcher
  sync: boolean // 是否同步
  dirty: boolean //
  active: boolean
  deps: Array<Dep> // 存储 旧dep 的数组
  newDeps: Array<Dep> // 存储 新dep 的数组
  depIds: SimpleSet // 存储 旧dep的id (Set类型,支持去重)
  newDepIds: SimpleSet // 存储 新dep 的id (Set类型,支持去重)
  before: ?Function
  getter: Function
  value: any

  constructor(
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    // 3. Watcher实例上存储一下我们的 vm
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }

    // 4. 一个组件的watch,除了渲染watcher,还有其他的watcher,定义一个变量`_watchers`, 存储所有的watcher。
    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 {
      // 全部为false
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching 存储唯一的id
    this.active = true
    this.dirty = this.lazy // for lazy watchers
    this.deps = [] //存放dep的容器
    this.newDeps = [] // 存储新dep的容器
    this.depIds = new Set() //去重dep
    this.newDepIds = new Set() // 去重 新dep容器
    this.expression =
      process.env.NODE_ENV !== 'production' ? expOrFn.toString() : ''
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      // expOrFn是我们传入的第二个参数,可以为字符串或函数。
      // 如果传入的是函数,函数中使用到的数据都会被观察,只要有一个数据改变,watcher就会收到通知。
      // computed就是这么一个原理。 其次仔细想想 正常的渲染watcher,getter是去触发 render,render获取数据。这种传入函数的watcher,getter会去获取函数中使用的数据。
      this.getter = expOrFn
    } else {
      // parsePath解析我们的表达式
      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
          )
      }
    }

    // 5. 整体看下来上方的逻辑,都是在 watcher 实例上,初始化一些属性;

    // 6. 注意这里: this.lazy ,渲染的 watcher的 this.lazy 为 false,会执行 `this.get()`
    this.value = this.lazy ? undefined : this.get()
  }

  /**
   * Evaluate the getter, and re-collect dependencies.
   * 求值getter,并重新收集依赖项。
   */
  get() {
    // 7. pushTarget会做这么两个操作。
    // 7.1 向一个数组中添加当前的 watcher实例;
    // 7.2 `Dep.target = this` => `Dep.target = 当前的 watcher`
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      // 8.

      /* 
      1.执行 this.getter
      2.渲染的时候,实际执行的是: () => {vm._update(vm._render(), hydrating);};
      3. vm._render()会获取 data 的值,触发了数据的 get ,从而实现依赖收集
      */
      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)
      }

      // 9. 收集完依赖之后,删除当前依赖
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

  /**
   * Add a dependency to this directive.
   * 添加新的依赖到当前的指令
   */
  addDep(dep: Dep) {
    // 10 获取到 dep实例的id
    const id = dep.id

    // 11. 如果新 depid数组没有这个id
    if (!this.newDepIds.has(id)) {
      // 添加这个id
      this.newDepIds.add(id) // 去重后的 dep id的数 Set   (新)
      this.newDeps.push(dep) // 存储dep的容器    (新)

      // 如果 去重的数组 (旧) 也没有这个 dep.id, dep中收集一下当前的 watcher
      if (!this.depIds.has(id)) {
        // 收集这个 watcher
        dep.addSub(this)
      }
    }
  }

  /**
   * Clean up for dependency collection.
   * 清理依赖项收集。
   */

  // 感觉是清除旧的依赖
  cleanupDeps() {
    let i = this.deps.length // 存储 deps
    while (i--) {
      const dep = this.deps[i]

      // 如果新的newDepIds没有这个 dep.id
      if (!this.newDepIds.has(dep.id)) {
        // dep中删除当前 watcher
        dep.removeSub(this)
      }
    }

    // 1. 设置this.depIds存储的最新id的Set ;2. 清除原本的存储id的Set
    let tmp = this.depIds
    this.depIds = this.newDepIds
    this.newDepIds = tmp
    this.newDepIds.clear()

    // 上面替换的是存储id的 Set(Set特性不会重复)
    // 这里的作用 `this.deps`更新为最新dep的数组的引用地址,原本旧的数组引用给释放掉。
    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
      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() {
    // 执行get,重新计算值
    this.value = this.get()

    // 设置 diety 为 false
    this.dirty = false
  }

  /**
   * Depend on all deps collected by this watcher.
   * 依赖于这个观察者收集的所有深度。
   */
  depend() {
    // this.deps属性保存了所有状态的dep实例,每个dep实例保存了它的所有依赖
    // 简单来说,就是遍历了 this.deps, 将当前的 watcher放到所有的依赖项中。
    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.
      // 从vm的监视列表中删除self
      // 这是一个有点昂贵的操作,所以我们跳过它
      // 如果虚拟机正在被销毁。
      if (!this.vm._isBeingDestroyed) {
        remove(this.vm._watchers, this)
      }

      // this.deps => watcher本身记录着它自己订阅了谁
      // 当需要
      let i = this.deps.length
      while (i--) {
        this.deps[i].removeSub(this)
      }
      this.active = false
    }
  }
}

梳理一下Watcher逻辑,说说我个人理解。

  • Watcher 本质是一个类。

  • 基于这个类生成的实例也有很多属性;

  • 注意 new Watcher 时传入的参数。

      vm: Component, // 实例
      expOrFn: string | Function, // 表达式
      cb: Function, // 回调函数
      options?: ?Object, // 配置
      isRenderWatcher?: boolean //
    
  • new Watcher的时候,如果 this.lazy不存在,则会调用 get 方法。

  • Watcher 中的 get 方法

    • 纵观全局, get 中主要做了这些操作

    1. Dep.target = 当前的 watcher;
    2. 触发配置项 data 中的属性 get() 方法;
    3. 置空 Dep.target;

详细解释

  • Dep.target 上存储的是我们当前的 watcher 实例。

  • 整体的执行逻辑:

this.getter.call(vm, vm) => vm._render() => defineReactive中的get => dep.depend() => Dep.target.addDep(this); => 其实就是调用 当前 watcher 中的 addDep()

 /**
   * Add a dependency to this directive.
   * 添加新的依赖到当前的指令
   */
  addDep(dep: Dep) {
    // 10 获取到 dep实例的id
    const id = dep.id;

    // 11. 如果新 depid 数组没有这个id
    if (!this.newDepIds.has(id)) {
      // 添加这个id
      this.newDepIds.add(id); // 去重后的 dep id的数 Set   (新)
      this.newDeps.push(dep); // 存储dep的容器    (新)

      // 如果 去重的数组 (旧) 也没有这个 dep.id, dep中收集一下当前的 watcher
      if (!this.depIds.has(id)) {
        // 收集这个 watcher
        dep.addSub(this);
      }
    }
  }
  • 可以看到,这里的逻辑: Watcher 中的 newDepIds 存储的 dep 的 id; dep 的 subs 存储了当前的 watcher; 两者本身是相互存储。

  • 其他就是 watcher 中的一些方法。

思考

  1. pushTargetpopTarget

在收集依赖的时候,经常会看到这两个函数,研究一下。

// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
// 当前被求值的目标监视器。
// 这是全局唯一的,因为只有一个监视器
// 可以一次求值。
Dep.target = null
const targetStack = []

// 这里会有一个数组 targetStack ,栈结构 (栈结构,特点:后进先出)

// 向 targetStack 中push数据
export function pushTarget(target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

// 从 targetStack 取出最后一项
export function popTarget() {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}
  • 整体逻辑呢,无非就是修改 Dep.target 中存储的数据。

  • 为什么会有 targetStack呢?

    设想一个逻辑吧,同样是渲染 watcher。我组件 a 中有组件 b 先渲染 a。此时 Dep.target = a 的 watcher; 渲染到 b 。此时 Dep.target = b 的 watcher; b 渲染完毕,继续渲染 a,此时 Dep.target 已经被清空了; 所以这里利用 targetStack 栈结构,对前置 watcher 进行了存储,后进先出。

看到网上有人这样解释,targetStack 是 vue2 才引入的。 vue1 视图更新采用的是细粒度绑定的方式,而 vue2 采取的是 virtual DOM 的方式。 两者渲染方式不同,后者会产生嵌套调用的相关逻辑,所以需要引入targetStack 回答的原文

  1. traverse

首先捋一下 traverse 的调用逻辑,每次执行 watcher 中的 get 的时候,且 depp 存在的时候,会执行 traverse(value)。

finally {
  // "touch" every property so they are all tracked as
  // dependencies for deep watching
  // “触摸”每个属性,因此它们都被跟踪为
  // 依赖深度观察
  if (this.deep) {
    traverse(value);
  }
}

这个 value 是什么?

// 例如我是这样使用的Vue.js
new Vue({
  el: '#app',
  template: `
      <div id='app'>
        <div>a的值: {{a.b}}</div>
      </div>
      `,
  data() {
    return {
      a: {
        b: 1,
      },
    }
  },
  watch: {
    a: function (val, oldVal) {
      console.log('new: %s, old: %s', val, oldVal)
    },
  },
})

此时有两个 Watcher,一个是渲染整个页面的 watcher。一个是通过 watch 配置定义的一个 Watcher。

  1. 渲染 watcher 传入的 expOrFn 是一个箭头函数 updateComponent
updateComponent = () => {
  vm._update(vm._render(), hydrating)
}
// 没有返回值,此时value是 undefined
  1. 自定义的 watcher 传入的 expOrFn 是 'a' ,通过 parsePath 进行转换,返回一个函数,函数的返回值就是 { b : 1 }

简单来说,value 就是我们 data 对应表达式的值,如果 deep 配置存在,需要深层监听表达式对应的值,通过 traverse 深层遍历,每个属性都访问一遍,就能够达到收集依赖的目的。

traverse实现的源码

function _traverse(val: any, seen: SimpleSet) {
  let i, keys
  const isA = Array.isArray(val)
  // 不是数组 不是对象 ; 冻结的对象; 是虚拟dom; 直接退出
  if (
    (!isA && !isObject(val)) ||
    Object.isFrozen(val) ||
    val instanceof VNode
  ) {
    return
  }

  // 存在响应式,存储依赖的唯一标识 id
  if (val.__ob__) {
    const depId = val.__ob__.dep.id
    if (seen.has(depId)) {
      return
    }
    // 添加我们的依赖id
    seen.add(depId)
  }

  /* 核心逻辑就是这里,遍历所有的数据项,实现深层数据的依赖收集。 */

  // 如果是数组,循环数组,每一项都调用 _traverse
  if (isA) {
    i = val.length
    while (i--) _traverse(val[i], seen)
  } else {
    // 如果对象 循环 Object中所有的key,执行一次读取,再递归子值。 其实就是这里的dep还没有被清除,这里读取了深层对象,触发了对应的get (递归调用,相当于,底层的对象的dep中都存储了我们这个 watcher)
    keys = Object.keys(val)
    i = keys.length
    while (i--) _traverse(val[keys[i]], seen)
  }
}

end

  • 本节主要阅读了 Watcher 的源码。