Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
theydy opened this issue Feb 20, 2021 · 0 comments

Comments

@theydy
Copy link
Owner

theydy commented Feb 20, 2021

在初始化 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()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
前端学习笔记
Awaiting triage
Development

No branches or pull requests

1 participant