Vue3源码解析watch函数实例

目录
  • 引言
  • 一、watch参数类型
    • 1. 选项options
    • 2. 回调cb
    • 3. 数据源source
  • 二、watch函数
  • 三、watch的核心:doWatch 函数

引言

想起上次面试,问了个古老的问题:watch和computed的区别。多少有点感慨,现在已经很少见这种耳熟能详的问题了,网络上八股文不少。今天,我更想分享一下从源码的层面来区别这八竿子打不着的两者。本篇针对watch做分析,下一篇分析computed

一、watch参数类型

我们知道,vue3里的watch接收三个参数:侦听的数据源source、回调cb、以及可选的optiions

1. 选项options

我们可以在options里根据需要设置**immediate来控制是否立即执行一次回调;设置deep来控制是否进行深度侦听;设置flush来控制回调的触发时机,默认为{ flush: 'pre' },即vue组件更新前;若设置为{ flush: 'post' }则回调将在vue组件更新之后触发;此外还可以设置为{ flush: 'sync' },表示同步触发;以及设置收集依赖时的onTrack和触发更新时的onTrigger两个listener,主要用于debuggerwatch函数会返回一个watchStopHandle用于停止侦听。options**的类型便是WatchOptions,在源码中的声明如下:

// reactivity/src/effect.ts
export interface DebuggerOptions {
  onTrack?: (event: DebuggerEvent) => void
  onTrigger?: (event: DebuggerEvent) => void
}
​
// runtime-core/apiWatch.ts
export interface WatchOptionsBase extends DebuggerOptions {
  flush?: 'pre' | 'post' | 'sync'
}
​
export interface WatchOptions<Immediate = boolean> extends WatchOptionsBase {
  immediate?: Immediate
  deep?: boolean
}

2. 回调cb

了解完options,接下来我们看看回调**cb**。通常我们的cb接收三个参数:valueoldValueonCleanUp,然后执行我们需要的操作,比如侦听表格的页码,发生变化时重新请求数据。第三个参数onCleanUp,用于注册副作用清理的回调函数, 在副作用下次执行之前,这个回调函数会被调用,通常用来清除不需要的或者无效的副作用。

// 副作用
export type WatchEffect = (onCleanup: OnCleanup) => void
​
export type WatchCallback<V = any, OV = any> = (
  value: V,
  oldValue: OV,
  onCleanup: OnCleanup
) => any
​
type OnCleanup = (cleanupFn: () => void) => void

3. 数据源source

watch函数可以侦听单个数据或者多个数据,共有四种重载,对应四种类型的source。其中,单个数据源的类型有WatchSource和响应式的object,多个数据源的类型为MultiWatchSourcesReadonly<MultiWatchSources>,而MultiWatchSources其实也就是由单个数据源组成的数组。

// 单数据源类型:可以是 Ref 或 ComputedRef 或 函数
export type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T)
​
// 多数据源类型
type MultiWatchSources = (WatchSource<unknown> | object)[]
​

二、watch函数

下面是源码中的类型声明,以及watch的重载签名和实现签名:

// watch的重载与实现
export function watch<
  T extends MultiWatchSources,
  Immediate extends Readonly<boolean> = false
>(
  sources: [...T],
  cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>,
  options?: WatchOptions<Immediate>
): WatchStopHandle
​
// overload: multiple sources w/ `as const`
// watch([foo, bar] as const, () => {})
// somehow [...T] breaks when the type is readonly
export function watch<
  T extends Readonly<MultiWatchSources>,
  Immediate extends Readonly<boolean> = false
>(
  source: T,
  cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>,
  options?: WatchOptions<Immediate>
): WatchStopHandle
​
// overload: single source + cb
export function watch<T, Immediate extends Readonly<boolean> = false>(
  source: WatchSource<T>,
  cb: WatchCallback<T, Immediate extends true ? T | undefined : T>,
  options?: WatchOptions<Immediate>
): WatchStopHandle
​
// overload: watching reactive object w/ cb
export function watch<
  T extends object,
  Immediate extends Readonly<boolean> = false
>(
  source: T,
  cb: WatchCallback<T, Immediate extends true ? T | undefined : T>,
  options?: WatchOptions<Immediate>
): WatchStopHandle
​
// implementation
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(fn, options?)` signature has been moved to a separate API. ` +
        `Use `watchEffect(fn, options?)` instead. `watch` now only ` +
        `supports `watch(source, cb, options?) signature.`
    )
  }
  return doWatch(source as any, cb, options)
}

watch的实现签名中可以看到,和watchEffect不同,watch的第二个参数cb必须是函数,否则会警告。最后,尾调用了doWatch,那么具体的实现细节就都得看doWatch了。让我们来瞅瞅它到底是何方神圣。

三、watch的核心:doWatch 函数

先瞄一下doWatch的签名:接收的参数大体和watch一致,其中source里多了个WatchEffect类型,这是由于在watchApi.js文件里,还导出了三个函数:watchEffectwatchSyncEffectwatchPostEffect,它们接收的第一个参数的类型就是WatchEffect,然后传递给doWatch,会在后面讲到,也可能不会;而options默认值为空对象,函数返回一个WatchStopHandle,用于停止侦听。

function doWatch(
  source: WatchSource | WatchSource[] | WatchEffect | object,
  cb: WatchCallback | null,
  { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
): WatchStopHandle {
    // ...
  }

再来看看doWatch的函数体,了解一下它干了些啥:

首先是判断在没有cb的情况下,如果options里设置了immediatedeep,就会告警,这俩属性只对有cbdoWatch签名有效。其实也就是上面说到的watchEffect等三个函数,它们是没有cb这个参数的,因此它们设置的immediatedeep是无效的。声明一个当source参数不合法时的警告函数,代码如下:

if (__DEV__ && !cb) {
    if (immediate !== undefined) {
      warn(
        `watch() "immediate" option is only respected when using the ` +
          `watch(source, callback, options?) signature.`
      )
    }
    if (deep !== undefined) {
      warn(
        `watch() "deep" option is only respected when using the ` +
          `watch(source, callback, options?) signature.`
      )
    }
  }
​
// 声明一个source参数不合法的警告函数
const warnInvalidSource = (s: unknown) => {
    warn(
      `Invalid watch source: `,
      s,
      `A watch source can only be a getter/effect function, a ref, ` +
        `a reactive object, or an array of these types.`
    )
  }
// ...

接下来,就到了正文了。第一步的目标是设置getter,顺便配置一下强制触发和深层侦听等。拿到getter的目的是为了之后创建effectvue3的响应式离不开effect,日后再出一篇文章介绍。

先拿到当前实例,声明了空的getter,初始化关闭强制触发,且默认为单数据源的侦听,然后根据传入的source的类型,做不同的处理:

  • Ref: getter返回值为Ref的·value,强制触发由source是否为浅层的Ref决定;
  • Reactive响应式对象:getter的返回值为source本身,且设置深层侦听;
  • Arraysource为数组,则是多数据源侦听,将isMultiSource设置为true,强制触发由数组中是否存在Reactive响应式对象或者浅层的Ref来决定;并且设置getter的返回值为从source映射而来的新数组;
  • function:当source为函数时,会判断有无cb,有cb则是watch,否则是watchEffect等。当有cb时,使用callWithErrorHandling包裹一层来调用source得到的结果,作为getter的返回值;
  • otherTypes:其它类型,则告警source参数不合法,且getter设置为NOOP,一个空的函数。
// 拿到当前实例,声明了空的getter,初始化关闭强制触发,且默认为单数据源的侦听
const instance = currentInstance
let getter: () => any
let forceTrigger = false
let isMultiSource = false
​
// 根据侦听数据源的类型做相应的处理
if (isRef(source)) {
    getter = () => source.value
    forceTrigger = isShallow(source)
  } else if (isReactive(source)) {
    getter = () => source
    deep = true
  } else if (isArray(source)) {
    isMultiSource = true
    forceTrigger = source.some(s => isReactive(s) || isShallow(s))
    getter = () =>
      // 可见,数组成员只能是Ref、Reactive或者函数,其它类型无法通过校验,将引发告警
      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)
  }

然后还顺便兼容了下vue2.x版本的watch

// 2.x array mutation watch compat
  if (__COMPAT__ && cb && !deep) {
    const baseGetter = getter
    getter = () => {
      const val = baseGetter()
      if (
        isArray(val) &&
        checkCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance)
      ) {
        traverse(val)
      }
      return val
    }
  }

然后判断了下deepcb,在深度侦听且有cb的情况下(说白了就是watch而不是watchEffect等),对getter做个traverse,该函数的作用是对getter的返回值做一个递归遍历,将遍历到的值添加到一个叫做seen的集合中,seen的成员即为当前watch要侦听的那些数据。代码如下(影响主线可先跳过):

export function traverse(value: unknown, seen?: Set<unknown>) {
  if (!isObject(value) || (value as any)[ReactiveFlags.SKIP]) {
    return value
  }
  seen = seen || new Set()
  if (seen.has(value)) {
    return value
  }
  seen.add(value)
  // Ref
  if (isRef(value)) {
    traverse(value.value, seen)
  } else if (isArray(value)) {
    // 数组
    for (let i = 0; i < value.length; i++) {
      traverse(value[i], seen)
    }
  } else if (isSet(value) || isMap(value)) {
    // 集合与映射
    value.forEach((v: any) => {
      traverse(v, seen)
    })
  } else if (isPlainObject(value)) {
    // 普通对象
    for (const key in value) {
      traverse((value as any)[key], seen)
    }
  }
  return value
}

至此,getter就设置好了。之后声明了cleanuponCleanup,用于清除副作用。以及SSR检测。虽然不是本文的重点,但还是贴一下源码:

let cleanup: () => void
  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
if (__SSR__ && isInSSRComponentSetup) {
  // we will also not call the invalidate callback (+ runner is not set up)
  onCleanup = NOOP
  if (!cb) {
    getter()
  } else if (immediate) {
    callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
      getter(),
      isMultiSource ? [] : undefined,
      onCleanup
    ])
  }
  return NOOP
}

随后就是重头戏了,拿到oldValue,以及在job函数中取得newValue,这不就是我们在使用watch的时候的熟悉套路嘛。

let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE
// job为当前watch要做的工作,后续通过调度器来处理
const job: SchedulerJob = () => {
  // 当前effect不在active状态,说明没有触发该effect的响应式变化,直接返回
  if (!effect.active) {
    return
  }
  // cb存在,说明是watch,而不是watchEffect
  if (cb) {
    // watch(source, cb)
    // 调用 effect.run 得到新的值 newValue
    const newValue = effect.run()
    if (
      deep ||
      forceTrigger ||
      // 取到的新值和旧值是否相同,如果有变化则进入分支
      (isMultiSource
        ? (newValue as any[]).some((v, i) =>
            hasChanged(v, (oldValue as any[])[i])
          )
        : hasChanged(newValue, oldValue)) ||
      // 兼容2.x
      (__COMPAT__ &&
        isArray(newValue) &&
        isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance))
    ) {
      // cleanup before running cb again
      if (cleanup) {
        cleanup()
      }
      // 用异步异常处理程序包裹了一层来调用cb
      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 : oldValue,
        onCleanup
      ])
      // cb执行完成,当前的新值就变成了旧值
      oldValue = newValue
    }
  } else {
    // cb不存在,则是watchEffect
    // watchEffect
    effect.run()
  }
}
// 设置allowRecurse,让调度器知道它可以自己触发
job.allowRecurse = !!cb

一看job里,在watch的分支出现了effect,但是这个分支并没有effect呀,再往下看,噢,原来是由之前取得的getter来创建的effect。在这之前,还定义了调度器,调度器scheduler被糅合进了effect里,影响了newValue的获取,从而影响cb的调用时机:

  • sync:同步执行,也就是回调cb直接执行;
  • pre:默认值是pre,表示组件更新前执行;
  • post:组件更新后执行。
let scheduler: EffectScheduler
// 根据flush的值来创建不同的调度器
if (flush === 'sync') {
  scheduler = job as any // the scheduler function gets called directly
} else if (flush === 'post') {
  scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
} else {
  // default: 'pre'
  scheduler = () => queuePreFlushCb(job)
}
// 为 watch 创建 effect ,watchEffect就不必了,因为自带的有
const effect = new ReactiveEffect(getter, scheduler)
// 主要是调试用的onTrack和onTrigger,当收集依赖和触发更新时做一些操作
if (__DEV__) {
  effect.onTrack = onTrack
  effect.onTrigger = onTrigger
}

现在来到了doWatch最后的环节了:侦听器的初始化。

  • immediate:如果为真值。将直接调用一次job,上文我们知道,job是包裹了一层错误处理程序来调用cb,所以我们现在终于亲眼看到了为什么immediate能让cb立即触发一次。
// initial run
// 有cb,是 watch
if (cb) {
  if (immediate) {
    job()
  } else {
    // 获取一下当前的值作为旧值
    oldValue = effect.run()
  }
} else if (flush === 'post') {
  // 没有cb,是watchEffect,副作用的时机在组件更新之后,用queuePostRenderEffect包裹一层来调整时机
  queuePostRenderEffect(
    effect.run.bind(effect),
    instance && instance.suspense
  )
} else {
  // watchEffect,副作用的时机在组件更新之前,直接执行一次effect.run
  effect.run()
}
// 返回一个WatchStopHandle,内部执行 effect.stop来达到停止侦听的作用
return () => {
  effect.stop()
  // 移除当前实例作用域下的当前effect
  if (instance && instance.scope) {
    remove(instance.scope.effects!, effect)
  }
}

到这里,watch的源码算是差不多结束了。小结一下核心流程:

  • watch:判断若没有cb则告警;
  • watch:尾调用doWatch,之后的操作都在doWatch里进行;
  • doWatch:判断没有cb时若设置了deepimmediate则告警;
  • doWatch:根据source的类型得到getter
  • doWatch:如果cb存在且deep为真则对getter()进行递归遍历;
  • doWatch:获取oldValue,声明job函数,在job内部获取newValue并使用callWithAsyncErrorHandling来调用cb
  • doWatch:根据post的值定义的调度器scheduler
  • doWatch:根据getterscheduler创建effect
  • doWatch:初始化侦听器,如果有cbimmediate为真值,则立即调用job函数,相当于调用我们写的cb;如果immediate为假值,则只调用effect.run()来初始化oldValue
  • doWatch:返回一个WatchStopHandle,内部通过effect.stop()来实现停止侦听。
  • watch:接收到doWatch返回的WatchStopHandle,并返回给外部使用。

以上就是Vue3源码解析watch函数实例的详细内容,更多关于Vue3 watch函数的资料请关注我们其它相关文章!

(0)

相关推荐

  • 详解Vue3中的watch侦听器和watchEffect高级侦听器

    目录 1watch侦听器 2watchEffect高级侦听器 清除副作用:就是在触发监听之前会调用一个函数可以处理你的逻辑例如防抖 停止跟踪 watchEffect 返回一个函数 调用之后将停止更新 1watch侦听器 watch 需要侦听特定的数据源,并在单独的回调函数中执行副作用 watch第一个参数监听源 watch第二个参数回调函数cb(newVal,oldVal) watch第三个参数一个options配置项是一个对 { immediate:true //是否立即调用一次 deep:t

  • Vue3中watch监听使用详解

    目录 Vue2使用watch Vue3使用watch 情况1 情况2 情况3 情况4 情况5 特殊情况 总结 Vue2使用watch <template> <div>总合:{{ sum }}<button @click="sum++">点击累加</button></div> </template> <script> import { ref } from "vue"; export

  • Vue3源码分析侦听器watch的实现原理

    目录 watch 的本质 watch 的函数签名 侦听多个源 侦听单一源 watch 的实现 watch 函数 source 参数 cb 参数 options 参数 doWatch 函数 doWatch 函数签名 初始化变量 递归读取响应式数据 定义清除副作用函数 封装 scheduler 调度函数 设置 job 的 allowRecurse 属性 flush 选项指定回调函数的执行时机 创建副作用函数 执行副作用函数 返回匿名函数,停止侦听 总结 watch 的本质 所谓的watch,其本质就

  • 一文搞懂Vue3中watchEffect侦听器的使用

    目录 watchEffect 侦听器 watchEffect 侦听器使用 watchEffect 监听基本数据 watchEffect 监听复杂数据 watchEffect 啥时候执行 关闭 watchEffect 监听 上一节我们学习了 watch 侦听器的基础用法,用来监听页面数据的变化,那么今天呢,我们来学习一下 watch 侦听器的好兄弟 watchEffect 侦听器.这个相对来说比较简单,用的不是很多,当然了,根据自己的项目情况自行决定使用.这个就不详细说了,简单过一下子. watc

  • 详解Vue3中侦听器watch的使用教程

    目录 watch 侦听器使用. 侦听器监听 reactive 监听多个参数执行各自逻辑 监听多个参数执行相同逻辑 上一节我们简单的介绍了一下vue3 项目中的计算属性,这一节我们继续 vue3 的基础知识讲解. 这一节我们来说 vue3 的侦听器. 学过 vue2 的小伙伴们肯定学习过侦听器,主要是用来监听页面数据或者是路由的变化,来执行相应的操作,在 vue3里面呢,也有侦听器的用法,功能基本一样,换汤不换药的东西. 侦听器是常用的 Vue API 之一,它用于监听一个数据并在数据变动时做一些

  • vue3如何使用watch监听props中的数据

    目录 情况一:监听 props 中基本数据类型 情况二:监听 props 中引用数据类型且父组件不改变地址指向 情况三:监听 props 中引用数据类型且父组件改变地址指向 总结 写在最后 情况一:监听 props 中基本数据类型 父组件中对传入数据的处理 const handleClick = () => { testStr.value += 'P' } 子组件中监听传入的数据 watch( () => props.testStr, (newVal, oldVal) => { cons

  • Vue3源码解析watch函数实例

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

  • 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

  • Vue 2源码解析ParseHTML函数HTML模板

    ParseHTML函数 - HTML 模板解析 之前在解析 parse 函数时,我们知道整个 解析 template 模板并生成 ast 对象 的过程都发生在这个函数的执行过程中. 但是 parse 函数内部本身只定义了一些标签.指令的处理方法和警告函数,并且在传递给 parseHTML 函数的参数中定义了四个处理方法. 最终是通过调用 parseHTML 来解析 template 模板 整个解析过程,其实就是 通过一系列正则表达式来匹配 template 模板字符串,并截取该部分匹配内容并重新

  • Vue 2源码解析Parse函数定义

    目录 Parse 函数 parseHTML Parse 函数 在 baseCompile() 执行过程中,首先就是通过 parse方法 解析 template模板字符串,生成对应的 AST 抽象语法树. 整个 parse函数 定义太长,这里省略几个内部方法 /** * Convert HTML string to AST. */ export function parse(template: string, options: CompilerOptions): ASTElement { warn

  • vue3 keepalive源码解析解决线上问题

    目录 引言 1.keepalive功能 2.keepalive使用场景 3.在项目中的使用过程 4.vue3 keepalive源码调试 5.vue3 keealive源码粗浅分析 6.总结 引言 1.通过本文可以了解到vue3 keepalive功能 2.通过本文可以了解到vue3 keepalive使用场景 3.通过本文可以学习到vue3 keepalive真实的使用过程 4.通过本文可以学习vue3 keepalive源码调试 5.通过本文可以学习到vue3 keepalive源码的精简分

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

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

  • Vue3系列之effect和ReactiveEffect track trigger源码解析

    目录 引言 一.ReactiveEffect 1. 相关的全局变量 2. class 声明 3. cleanupEffect 二.effect 函数 1. 相关ts类型 2. 函数声明 3. stop函数 三.track 依赖收集 1. track 2. createDep 3. trackEffects 4. 小结 四.trigger 1. triggerEffect 2. triggerEffects 3. trigger 五.小结 1. 依赖收集 2. 触发更新 引言 介绍几个API的时候

  • jq源码解析之绑在$,jQuery上面的方法(实例讲解)

    1.当我们用$符号直接调用的方法.在jQuery内部是如何封装的呢?有没有好奇心? // jQuery.extend 的方法 是绑定在 $ 上面的. jQuery.extend( { //expando 用于决定当前页面的唯一性. /\D/ 非数字.其实就是去掉小数点. expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ), // Assume jQuery is ready wit

  • Vue3 AST解析器-源码解析

    目录 1.生成 AST 抽象语法树 2.创建 AST 的根节点 3.解析子节点 4.解析模板元素 Element 5.示例:模板元素解析 上一篇文章Vue3 编译流程-源码解析中,我们从 packges/vue/src/index.ts 的入口开始,了解了一个 Vue 对象的编译流程,在文中我们提到 baseCompile 函数在执行过程中会生成 AST 抽象语法树,毫无疑问这是很关键的一步,因为只有拿到生成的 AST 我们才能遍历 AST 的节点进行 transform 转换操作,比如解析 v

  • Vue3 编译流程-源码解析

    前言: Vue3 发布已经很长一段时间了,最近也有机会在公司项目中用上了 Vue3 + TypeScript + Vite 的技术栈,所以闲暇之余抽空也在抽空阅读 Vue3 的源码.本着好记性不如烂笔头的想法,在阅读源码时顺便记录了一些笔记,也希望能争取写一些源码阅读笔记,帮助每个想看源码但可能存在困难的同学减少理解成本. Vue2.x 的源码我也有过一些简单的阅读,自 Vue3 重构后,Vue 项目的目录结构也发生了很大的变化,各个功能模块被分别放入了 packages 目录下,职责更加清晰,

随机推荐