Vue之Computed依赖收集与更新原理分析

 更新时间:2023年11月16日 10:38:07   作者:兰亭古墨  
这篇文章主要介绍了Vue之Computed依赖收集与更新原理分析,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教

Computed依赖收集与更新原理

今天面了家小公司,上来直接问 computed 底层原理,面试官是这样问的,data 中定义了 a 和 b 变量。

computed 里面定义了 c 属性,c 的结果依赖与 a 和 b,模板中使用了变量 c。

假设改变了 a,请问底层是如何收集依赖,如何触发更新的?

<div>{{ c }}</div>
data(){
	return {
		a: 'foo',
		b: 'bar'
	}
},
computed: {
	c() { 
		return this.a + ' - ' + this.b;
	 }
},
mounted(){
	setTimeout(() => { this.a = 'FOO' }, 1000)
}

页面效果:先显示了 foo - bar,一秒之后显示 FOO - bar

这里查源码后,对computed原理简述

如下:

1、initState 函数中初始化了 initComputed

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

2、initComputed 函数中遍历 computed 中每一个属性,并且 new Watcher(computed watcher),可以看到传入 Watcher 构造函数的 cb 是 noop,它是一个空函数。

并且 defineComputed

function initComputed (vm: Component, computed: Object) {
  // $flow-disable-line
  const watchers = vm._computedWatchers = Object.create(null)
  // computed properties are just getters during SSR
  const isSSR = isServerRendering()

  for (const key in computed) {
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    if (process.env.NODE_ENV !== 'production' && getter == null) {
      warn(
        `Getter is missing for computed property "${key}".`,
        vm
      )
    }

    if (!isSSR) {
      // create internal watcher for the computed property.
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }

    // component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // at instantiation here.
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      if (key in vm.$data) {
        warn(`The computed property "${key}" is already defined in data.`, vm)
      } else if (vm.$options.props && key in vm.$options.props) {
        warn(`The computed property "${key}" is already defined as a prop.`, vm)
      } else if (vm.$options.methods && key in vm.$options.methods) {
        warn(`The computed property "${key}" is already defined as a method.`, vm)
      }
    }
  }
}

defineComputed 中使用了 Object.defineProperty 对属性进行劫持,获取是返回对应的 getter 即 createComputedGetter

export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  const shouldCache = !isServerRendering()
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : createGetterInvoker(userDef)
    sharedPropertyDefinition.set = noop
  } else {
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : createGetterInvoker(userDef.get)
      : noop
    sharedPropertyDefinition.set = userDef.set || noop
  }
  if (process.env.NODE_ENV !== 'production' &&
      sharedPropertyDefinition.set === noop) {
    sharedPropertyDefinition.set = function () {
      warn(
        `Computed property "${key}" was assigned to but it has no setter.`,
        this
      )
    }
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) {
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}

看到这里,我们大致可以理解,Vue 组件在初始化时,initState -> initComputed -> new Watcher() 计算watcher,传入的 callback 是 computer 属性的 getter,由于 computed 是 lazy: true watcher,所以 new Watcher 时并不会立即执行 watcher 实例上的 get(), 而是在 defineComputed 函数里面调用 createComputedGetter 函数,在 createComputedGetter 函数执行了 watcher.evaluate() 进而执行了 watcher.get().

执行 watcher.get() 首先 pushTarget(this),将 Dep.target 赋值为当前的 computed watcher,然后执行 this.getter

也就是执行 computer 属性配置的 getter,执行getter 就会访问所依赖的每一个值,就会被 Object.defineProperty 劫持到进入 get ,执行 dep.depend() , 会为每一个属性对应的 dep 实例添加一个 computed watcher,同时这个 computed watcher 也会保存对应的 dep。

说了这么多都在讲 computed watcher,那修改 this.a 页面为什么会发生更新呢?

答案:因为 this.a 的依赖中不仅有 computed watcher 还有一个 render watcher

原因:

$mount 是会执行 mountComponent,会创建一个 render watcher,它会立即执行 cb(目前 Dep.target 是 render watcher),调用 render 函数在底层编译模板,最后访问属性计算属性 c,访问计算属性 c 就必定会访问 a,当访问 a 时会触发 defineComputed 中的 Object.defineProperty 进而劫持调用 createComputedGetter,进而调用 watcher.depend(),这个 watcher 是 computed watcher

function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) {
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}
// Watcher.js
depend () {
  let i = this.deps.length
  while (i--) {
    this.deps[i].depend()
  }
}

调用 watcher.depend() , this 指的是 computed watcher,会将 computed watcher 里面的 deps 保存在所有依赖调用 deps[i].depend(),进而调用 Dep 类中的 Dep.target.addDep(this),使得 render watcher 中保存了当前的 dep,dep 中同时保存了 render watcher

dep 中同时保存了 render watcher。就可以看出,示例中的属性 a 的 dep 中也会保存 render watcher,所以 a 属性的 dep 中有两个 watcher: [computedWatcher, renderWatcher]

所以,修改 a 属性的值,最后 notify 会清空这个 保存 watcher 的队列,进行页面更新!Okay!

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • vue v-model表单控件绑定详解

    vue v-model表单控件绑定详解

    这篇文章主要为大家详细介绍了vue v-model表单控件绑定的相关资料,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-05-05
  • vue编写简单的购物车功能

    vue编写简单的购物车功能

    这篇文章主要为大家详细介绍了vue编写简单的购物车功能,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-01-01
  • Ant Design Vue Pro静态路由如何改为动态路由

    Ant Design Vue Pro静态路由如何改为动态路由

    这篇文章主要介绍了Ant Design Vue Pro静态路由如何改为动态路由问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-10-10
  • el-descriptions引入代码中label不生效问题及解决

    el-descriptions引入代码中label不生效问题及解决

    这篇文章主要介绍了el-descriptions引入代码中label不生效问题及解决方案,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-12-12
  • Vuejs使用addEventListener的事件如何触发执行函数的this

    Vuejs使用addEventListener的事件如何触发执行函数的this

    这篇文章主要介绍了Vuejs使用addEventListener的事件触发执行函数的this方法,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-06-06
  • vue element-ui实现动态面包屑导航

    vue element-ui实现动态面包屑导航

    这篇文章主要为大家详细介绍了vue element-ui实现动态面包屑导航,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-12-12
  • 使用vue-cli创建vue项目介绍

    使用vue-cli创建vue项目介绍

    这篇文章介绍了使用vue-cli创建vue项目的方法,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-01-01
  • 详解webpack编译多页面vue项目的配置问题

    详解webpack编译多页面vue项目的配置问题

    本篇文章主要介绍了详解webpack编译多页面vue项目的配置问题,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-12-12
  • Vue打包部署到Nginx时,css样式不生效的解决方式

    Vue打包部署到Nginx时,css样式不生效的解决方式

    这篇文章主要介绍了Vue打包部署到Nginx时,css样式不生效的解决方式,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-08-08
  • vite + electron-builder 打包配置详解

    vite + electron-builder 打包配置详解

    这篇文章主要为大家介绍了electron基于vite + electron-builder 打包配置详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-09-09

最新评论