Vue3源码解读computed和watch

目录
  • computed
  • ComputedRefImpl
    • 小结:
  • watch

computed

computed和watch在面试中经常被问到他们的区别,那么我们就从源码的实现来看看他们的具体实现

// packages/reactivity/src/computed.ts
export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
  debugOptions?: DebuggerOptions,
  isSSR = false
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>
  const onlyGetter = isFunction(getterOrOptions)
  if (onlyGetter) {
    getter = getterOrOptions
    setter = __DEV__
      ? () => {
          console.warn('Write operation failed: computed value is readonly')
        }
      : NOOP
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }
  // new ComputedRefImpl
  const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter, isSSR)
  if (__DEV__ && debugOptions && !isSSR) {
    cRef.effect.onTrack = debugOptions.onTrack
    cRef.effect.onTrigger = debugOptions.onTrigger
  }
  // 返回ComputedRefImpl实例
  return cRef as any
}

可以看到computed内部只是先处理getter和setter,然后new一个ComputedRefImpl返回,如果你知道ref API的实现,可以发现他们的实现有很多相同之处

ComputedRefImpl

// packages/reactivity/src/computed.ts
export class ComputedRefImpl<T> {
  public dep?: Dep = undefined // 存储effect的集合
  private _value!: T
  public readonly effect: ReactiveEffect<T>
  public readonly __v_isRef = true
  public readonly [ReactiveFlags.IS_READONLY]: boolean = false
  public _dirty = true // 是否需要重新更新value
  public _cacheable: boolean
  constructor(
    getter: ComputedGetter<T>,
    private readonly _setter: ComputedSetter<T>,
    isReadonly: boolean,
    isSSR: boolean
  ) {
    // 创建effect
    this.effect = new ReactiveEffect(getter, () => {
      // 调度器执行 重新赋值_dirty为true
      if (!this._dirty) {
        this._dirty = true
        // 触发effect
        triggerRefValue(this)
      }
    })
    // 用于区分effect是否是computed
    this.effect.computed = this
    this.effect.active = this._cacheable = !isSSR
    this[ReactiveFlags.IS_READONLY] = isReadonly
  }
  get value() {
    // the computed ref may get wrapped by other proxies e.g. readonly() #3376
    // computed ref可能被其他代理包装,例如readonly() #3376
    // 通过toRaw()获取原始值
    const self = toRaw(this)
    // 收集effect
    trackRefValue(self)
    // 如果是脏的,重新执行effect.run(),并且将_dirty设置为false
    if (self._dirty || !self._cacheable) {
      self._dirty = false
      // run()方法会执行getter方法 值会被缓存到self._value
      self._value = self.effect.run()!
    }
    return self._value
  }
  set value(newValue: T) {
    this._setter(newValue)
  }
}

可以看到ComputedRefImplget的get实现基本和ref的get相同(不熟悉ref实现的请看上一章),唯一的区别就是_dirty值的判断,这也是我们常说的computed会缓存value,那么computed是如何知道value需要更新呢?

可以看到在computed构造函数中,会建立一个getter与其内部响应式数据的关系,这跟我们组件更新函数跟响应式数据建立关系是一样的,所以与getter相关的响应式数据发生修改的时候,就会触发getter effect 对应的scheduler,这里会将_dirty设置为true并去执行收集到的effect(这里通常是执行get里收集到的函数更新的effect),然后就会去执行函数更新函数,里面会再次触发computed的get,此时dirty已经被置为true,就会重新执行getter获取新的值返回,并将该值缓存到_vlaue。

小结:

所以computed是有两层的响应式处理的,一层是computed.value和函数的effect之间的关系(与ref的实现相似),一层是computed的getter和响应式数据的关系。

注意:如果你足够细心就会发现函数更新函数的effect触发和computed getter的effect的触发之间可能存在顺序的问题。假如有一个响应式数据a不仅存在于getter中,还在函数render中早于getter被访问,此时a对应的dep中更新函数的effect就会早于getter的effect被收集,如果此时a被改变,就会先执行更新函数的effect,那么此时render函数访问到computed.value的时候就会发现_dirty依然是false,因为getter的effect还没有被执行,那么此时依然会是旧值。vue3中对此的处理是执行effects的时候会优先执行computed对应的effect(此前章节也有提到):

// packages/reactivity/src/effect.ts
export function triggerEffects(
  dep: Dep | ReactiveEffect[],
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  // spread into array for stabilization
  const effects = isArray(dep) ? dep : [...dep]
  // computed的effect会先执行
  // 防止render获取computed值得时候_dirty还没有置为true
  for (const effect of effects) {
    if (effect.computed) {
      triggerEffect(effect, debuggerEventExtraInfo)
    }
  }
  for (const effect of effects) {
    if (!effect.computed) {
      triggerEffect(effect, debuggerEventExtraInfo)
    }
  }
}

watch

watch相对于computed要更简单一些,因为他只用建立getter与响应式数据之间的关系,在响应式数据变化时调用用户传过来的回调并将新旧值传入即可

// packages/runtime-core/src/apiWatch.ts
export function watch<T = any, Immediate extends Readonly<boolean> = false>(
  source: T | WatchSource<T>,
  cb: any,
  options?: WatchOptions<Immediate>
): WatchStopHandle {
  if (__DEV__ && !isFunction(cb)) {
    warn(...)
  }
  // watch 具体实现
  return doWatch(source as any, cb, options)
}
function doWatch(
  source: WatchSource | WatchSource[] | WatchEffect | object,
  cb: WatchCallback | null,
  { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
): WatchStopHandle {
  if (__DEV__ && !cb) {
    ...
  }
  const warnInvalidSource = (s: unknown) => {
    warn(...)
  }
  const instance =
    getCurrentScope() === currentInstance?.scope ? currentInstance : null
  // const instance = currentInstance
  let getter: () => any
  let forceTrigger = false
  let isMultiSource = false
  // 根据不同source 创建不同的getter函数
  // getter 函数与computed的getter函数作用类似
  if (isRef(source)) {
    getter = () => source.value
    forceTrigger = isShallow(source)
  } else if (isReactive(source)) {
    // source是reactive对象时 自动开启deep=true
    getter = () => source
    deep = true
  } else if (isArray(source)) {
    isMultiSource = true
    forceTrigger = source.some(s => isReactive(s) || isShallow(s))
    getter = () =>
      source.map(s => {
        if (isRef(s)) {
          return s.value
        } else if (isReactive(s)) {
          return traverse(s)
        } else if (isFunction(s)) {
          return callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER)
        } else {
          __DEV__ && warnInvalidSource(s)
        }
      })
  } else if (isFunction(source)) {
    if (cb) {
      // getter with cb
      getter = () =>
        callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
    } else {
      // no cb -> simple effect
      getter = () => {
        if (instance && instance.isUnmounted) {
          return
        }
        if (cleanup) {
          cleanup()
        }
        return callWithAsyncErrorHandling(
          source,
          instance,
          ErrorCodes.WATCH_CALLBACK,
          [onCleanup]
        )
      }
    }
  } else {
    getter = NOOP
    __DEV__ && warnInvalidSource(source)
  }
  // 2.x array mutation watch compat
  // 兼容vue2
  if (__COMPAT__ && cb && !deep) {
    const baseGetter = getter
    getter = () => {
      const val = baseGetter()
      if (
        isArray(val) &&
        checkCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance)
      ) {
        traverse(val)
      }
      return val
    }
  }
  // 深度监听
  if (cb && deep) {
    const baseGetter = getter
    // traverse会递归遍历对象的所有属性 以达到深度监听的目的
    getter = () => traverse(baseGetter())
  }
  let cleanup: () => void
  // watch回调的第三个参数 可以用此注册一个cleanup函数 会在下一次watch cb调用前执行
  // 常用于竞态问题的处理
  let onCleanup: OnCleanup = (fn: () => void) => {
    cleanup = effect.onStop = () => {
      callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
    }
  }
  // in SSR there is no need to setup an actual effect, and it should be noop
  // unless it's eager or sync flush
  let ssrCleanup: (() => void)[] | undefined
  if (__SSR__ && isInSSRComponentSetup) {
    // ssr处理 ...
  }
  // oldValue 声明 多个source监听则初始化为数组
  let oldValue: any = isMultiSource
    ? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE)
    : INITIAL_WATCHER_VALUE
  // 调度器调用时执行
  const job: SchedulerJob = () => {
    if (!effect.active) {
      return
    }
    if (cb) {
      // watch(source, cb)
      // 获取newValue
      const newValue = effect.run()
      if (
        deep ||
        forceTrigger ||
        (isMultiSource
          ? (newValue as any[]).some((v, i) =>
              hasChanged(v, (oldValue as any[])[i])
            )
          : hasChanged(newValue, oldValue)) ||
        (__COMPAT__ &&
          isArray(newValue) &&
          isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance))
      ) {
        // cleanup before running cb again
        if (cleanup) {
          // 执行onCleanup传过来的函数
          cleanup()
        }
        // 调用cb 参数为newValue、oldValue、onCleanup
        callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
          newValue,
          // pass undefined as the old value when it's changed for the first time
          oldValue === INITIAL_WATCHER_VALUE
            ? undefined
            : isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE
            ? []
            : oldValue,
          onCleanup
        ])
        // 更新oldValue
        oldValue = newValue
      }
    } else {
      // watchEffect
      effect.run()
    }
  }
  // important: mark the job as a watcher callback so that scheduler knows
  // it is allowed to self-trigger (#1727)
  job.allowRecurse = !!cb
  let scheduler: EffectScheduler
  if (flush === 'sync') {
    // 同步更新 即每次响应式数据改变都会回调一次cb 通常不使用
    scheduler = job as any // the scheduler function gets called directly
  } else if (flush === 'post') {
    // job放入pendingPostFlushCbs队列中
    // pendingPostFlushCbs队列会在queue队列执行完毕后执行 函数更新effect通常会放在queue队列中
    // 所以pendingPostFlushCbs队列执行时组件已经更新完毕
    scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
  } else {
    // default: 'pre'
    job.pre = true
    if (instance) job.id = instance.uid
    // 默认异步更新 关于异步更新会和nextTick放在一起详细讲解
    scheduler = () => queueJob(job)
  }
  // 创建effect effect.run的时候建立effect与getter内响应式数据的关系
  const effect = new ReactiveEffect(getter, scheduler)
  if (__DEV__) {
    effect.onTrack = onTrack
    effect.onTrigger = onTrigger
  }
  // initial run
  if (cb) {
    if (immediate) {
      // 立马执行一次job
      job()
    } else {
      // 否则执行effect.run() 会执行getter 获取oldValue
      oldValue = effect.run()
    }
  } else if (flush === 'post') {
    queuePostRenderEffect(
      effect.run.bind(effect),
      instance && instance.suspense
    )
  } else {
    effect.run()
  }
  // 返回一个取消监听的函数
  const unwatch = () => {
    effect.stop()
    if (instance && instance.scope) {
      remove(instance.scope.effects!, effect)
    }
  }
  if (__SSR__ && ssrCleanup) ssrCleanup.push(unwatch)
  return unwatch
}

以上就是Vue3源码解读computed和watch的详细内容,更多关于Vue3 computed watch的资料请关注我们其它相关文章!

(0)

相关推荐

  • Vue3中的setup语法糖、computed函数、watch函数详解

    目录 写在前面 setup 语法糖 computed函数 watch 函数 写在最后 写在前面 专栏介绍:凉哥作为 Vue 的忠实 粉丝输出过大量的 Vue 文章,应粉丝要求开始更新 Vue3 的相关技术文章,Vue 框架目前的地位大家应该都晓得,所谓三大框架使用人数最多,公司选型最多的框架,凉哥之前在文章中也提到过就是 Vue 框架之所以火起来的原因,和 Vue 框架相比其他框架的巨大优势,有兴趣的伙伴可以看一下Vue与React的区别和优势对比,随着 Vue 框架不断的被认可,现如今的 Vu

  • 浅析Vue3中通过v-model实现父子组件的双向数据绑定及利用computed简化父子组件双向绑定

    目录 一.vue2 中 sync 修饰符的功能在 vue3 中如何呈现? 1.sync 修饰符回顾 2.sync 的语法糖功能在vue3中如何编写使用? 二.如何通过v-model实现父子组件的双向数据绑定 1.单个数据双向绑定 2.多个数据双向绑定 - 与单数据绑定差别不大 3.利用 computed 简化父子组件双向数据绑定 一.vue2 中 sync 修饰符的功能在 vue3 中如何呈现? 1.sync 修饰符回顾 1.vue 规则:props 是单向向下绑定的,子组件不能修改 props

  • vue3.0中的computed写法

    目录 vue3.0 computed写法 vue3.0 computed的使用及其注意点 为什么使用 使用 注意事项 总结 vue3.0 computed写法 用computed计算属性之前别忘了引入 使用完毕别忘了导出 computed里面还有两个方法:get和set get方法: 结果: get方法是读取数据时候调用的,监测到数据变化以后就自动执行: 结果: 点击以后: 说明get在程序运行时自动调用,后面监测到数据变化就再次调用 我们改一下代码: 点击前结果: 点击后结果: 所以我们能得出

  • Vue3 computed初始化获取设置值实现示例

    目录 computed 用法 computed 实现 computed 初始化 computed 获取值的实现 值的展示 缓存功能 computed 设置值实现 computed 用法 本文给大家带来的是vue3 中 computed API的实现. 大家看过vue3的官网,应该都知道,在vue3 的组合式API中,computed这个功能与以往的有所不同了. 以往vue2 的 computed 用法: export default { components: { }, computed: {

  • Vue3 计算属性computed的实现原理

    目录 computed 的函数签名 computed 的实现 ComputedRefImpl 类 总结 版本:3.2.31 computed 的函数签名 // packages/reactivity/src/computed.ts // 只读的 export function computed<T>( getter: ComputedGetter<T>, debugOptions?: DebuggerOptions ): ComputedRef<T> // 可写的 ex

  • Vue3 源码解读之副作用函数与依赖收集

    目录 副作用函数 副作用函数的全局变量 targetMap targetMap 为什么使用 WeakMap activeEffect shouldTrack 副作用的实现 effect 函数 ReactiveEffect 类 track 收集依赖 track 函数 trackEffects 函数 trigger 派发更新 trigger 函数 triggerEffects 函数 总结 版本:3.2.31 副作用函数 副作用函数是指会产生副作用的函数,如下面的代码所示: function effe

  • Vue3 源码解读之 Teleport 组件使用示例

    目录 Teleport 组件解决的问题 Teleport 组件的基本结构 Teleport 组件 process 函数 Teleport 组件的挂载 Teleport 组件的更新 moveTeleport 移动Teleport 组件 hydrateTeleport 服务端渲染 Teleport 组件 总结 Teleport 组件解决的问题 版本:3.2.31 如果要实现一个 “蒙层” 的功能,并且该 “蒙层” 可以遮挡页面上的所有元素,通常情况下我们会选择直接在 标签下渲染 “蒙层” 内容.如果

  • Vue3 源码解读静态提升详解

    目录 什么是静态提升 transform 转换器 hoistStatic 静态提升 walk 函数 walk 函数流程图 总结 什么是静态提升 静态提升是Vue3编译优化中的其中一个优化点.所谓的静态提升,就是指在编译器编译的过程中,将一些静态的节点或属性提升到渲染函数之外.下面,我们通过一个例子来深入理解什么是静态提升. 假设我们有如下模板: <div> <p>static text</p> <p>{{ title }}</p> </di

  • vue3 源码解读之 time slicing的使用方法

    今天给大家带来一篇源码解析的文章,emm 是关于 vue3 的,vue3 源码放出后,已经有很多文章来分析它的源码,我觉得很快又要烂大街了,哈哈 不过今天我要解析的部分是已经被废除的 time slicing 部分,这部分源码曾经出现在 vue conf 2018 的视频中,但是源码已经被移除掉了,之后可能也不会有人关注,所以应该不会烂大街 打包 阅读源码之前,需要先进行打包,打包出一份干净可调试的文件很重要 vue3 使用的 rollup 进行打包,我们需要先对它进行改造 import cle

  • Vue3源码分析reactivity实现方法示例

    目录 深入分析对于map.set.weakMap.weakSet的响应式拦截 (1).mutableInstrumentations (2).shallowInstrumentations (3).readonlyInstrumentations (4).shallowReadonlyInstrumentations ref.computed等方法的实现 (1).ref与shallowRef源码解析 (2).toRefs (4).computed (5)其他api源码 最后总结: 深入分析对于m

  • Vue3源码解析watch函数实例

    目录 引言 一.watch参数类型 1. 选项options 2. 回调cb 3. 数据源source 二.watch函数 三.watch的核心:doWatch 函数 引言 想起上次面试,问了个古老的问题:watch和computed的区别.多少有点感慨,现在已经很少见这种耳熟能详的问题了,网络上八股文不少.今天,我更想分享一下从源码的层面来区别这八竿子打不着的两者.本篇针对watch做分析,下一篇分析computed. 一.watch参数类型 我们知道,vue3里的watch接收三个参数:侦听

  • Vue3源码分析组件挂载初始化props与slots

    目录 前情提要 初始化组件 (1).setupComponent (2).initProps (3).initSlots 额外内容 总结 前情提要 上文我们分析了挂载组件主要调用了三个函数: createComponentInstance(创建组件实例).setupComponent(初始化组件).setupRenderEffect(更新副作用).并且上一节中我们已经详细讲解了组件实例上的所有属性,还包括emit.provide等的实现.本文我们将继续介绍组件挂载流程中的初始化组件. 本文主要内

  • Vue3 源码分析reactive readonly实例

    目录 引言 一.reactive 和 readonly 1. reactive相关类型 2. 相关全局变量与方法 3. reactive函数 4. 造物主createReactiveObject 5. shallowReactive.readonly和shallowReadonly 二.对应的 Handlers 1. baseHandlers 1.1 reactive 1.2 readonly 1.3 shallowReactive 1.4 shallowReadonly 2. cellecti

  • [转]prototype 源码解读 超强推荐第1/3页

    复制代码 代码如下: Prototype is a JavaScript framework that aims to ease development of dynamic web applications. Featuring a unique, easy-to-use toolkit for class-driven development and the nicest Ajax library around, Prototype is quickly becoming the codeb

  • Bootstrap源码解读网格系统(3)

    源码解读Bootstrap网格系统 工作原理 数据行(.row)必须包含在容器(.container)中,以便为其赋予合适的对齐方式和内距(padding).如: <div class="container"> <div class="row"></div> </div> .container的实现源码: .container { padding-right: 15px; padding-left: 15px; mar

随机推荐