theydy / notebook

记录读书笔记 + 知识整理,vuepress 迁移中 https://theydy.github.io/notebook/
0 stars 0 forks source link

data 初始化时 getData 中取值前为啥要执行 pushTarget() 置空 Dep.target ? #23

Open theydy opened 3 years ago

theydy commented 3 years ago

在初始化 data 时,当 data 写成函数的形式,会进入 getData 函数

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  //...
}

export function getData (data: Function, vm: Component): any {
  // #7573 disable dep collection when invoking data getters
  pushTarget()
  try {
    return data.call(vm, vm)
  } catch (e) {
    handleError(e, vm, `data()`)
    return {}
  } finally {
    popTarget()
  }
}

这里有个有意思的地方,就是在真正执行 data.call(vm, vm) 取值前有一个 pushTarget() 置空 Dep.target 的操作,取值后再恢复 popTarget()

Dep.target = null

const targetStack = []

export function pushTarget (_target: ?Watcher) {
  if (Dep.target) targetStack.push(Dep.target)
  Dep.target = _target
}

export function popTarget () {
  Dep.target = targetStack.pop()
}

其实这么做的原因已经在源码的注释中写明了,就是为了解决 #7573 这个 issues。

Pitfalls of Vue dependency detection may cause redundant dependencies · Issue #7573 · vuejs/vue

这个 bug 的现象是当子组件 data 写成函数形式并且函数中使用了父组件传给子组件的 props,当父组件中做为 props 传入子组件的那个响应式数据改变时,会触发两次父组件的更新。而且触发两次更新只在数据第一次改变时发生,后续就是正常的只触发一次更新。

之所以会这样,是因为执行 data.call(vm, vm) 获取子组件 data 值时,里面使用了 props,此时会触发 propsgetter,造成 props 收集依赖。由于数据初始化的时机是 beforeCreated -> created 之间,此时还没有进入子组件的渲染阶段(生成渲染 Watcher 是在 mountComponent 中),也就没有子组件的渲染 Watcher。所以这时候 Dep.target 指向的依然是父组件的渲染 Watcher。

最终表现就是父组件的字段更新时,正确触发了一次父组件的渲染 Watcher 的 update,更新子组件的 props 时,又触发了一次父组件的渲染 Watcher 的 update。

而第一次更新后,后续收集依赖时子组件的渲染 Watcher 已经存在,所以不会收集到父组件的渲染 Watcher。

其实不只是这里,子组件的 beforeCreatecreatedbeforeMount 这三个生命周期钩子函数如果用了 props 的话,也会出现同样的问题,所以在 callHook 函数中也做了同样 Dep.target 置空的操作。

其实不只是这里,子组件的 beforeCreatecreatedbeforeMount 这三个生命周期钩子函数如果用了 props 的话,也会出现同样的问题,所以在 callHook 函数中也做了同样 Dep.target 置空的操作。

export function callHook (vm: Component, hook: string) {
  // #7573 disable dep collection when invoking lifecycle hooks
  pushTarget()
  const handlers = vm.$options[hook]
  if (handlers) {
    for (let i = 0, j = handlers.length; i < j; i++) {
      try {
        handlers[i].call(vm)
      } catch (e) {
        handleError(e, vm, `${hook} hook`)
      }
    }
  }
  if (vm._hasHookEvent) {
    vm.$emit('hook:' + hook)
  }
  popTarget()
}