keywords: Proxy懒代理编译时优化对比启动流程
此文是余初习vue3源码所记,可谓走马观花。虽不常用vue,但看下来也是颇有所得,遂感其称三大框架之一,岂曰仅靠国人力捧邪?初学至此,已是殚精竭虑,头皮发麻,不敢深究,故文中仅梳理枝干,于海量细节精妙之大千世界尚少有探究。然力之所限,深以为憾,乃应为后之续习也。
vue3整体的思路和vue2基本一致,都是基于发布订阅模式的MVVM的实现,不一样的点在于主要是:vue2做响应式数据是:Object.definedPorperty + Dep + Watcher串联整体流程,而vue3是Proxy + Weakmap + effect;另外在模版编译阶段也有很多不同,主要是vue3摒弃了with,对模版做了分析之后,引入ctx来代替原本with的功能。
_

代码结构

多包单仓库架构:
Monorepo-多包单仓库的开发模式知乎:monorepo
github: lerna
所以我们看到vue3有一个packages,就是用lerna做的monorepo:
比如一下都是packages下的包:

  • compiler-core:平台无关编译器;
  • compiler-dom:浏览器而言的编译器;
  • compiler-sfc:vue模拆分
  • compiler-ssr:SSR toString
  • runtime-core:运行时和平台无关;
  • runtime-dom:运行时基于浏览器平台;

响应式数据

从defineProperty到Proxy

(reactivity包)
响应式数据一直是vue中最基础且重要的知识,所以,讨论vue3还是从双向数据绑定开始。
vue3用了Proxy和Reflect代替了vue2中的Object.defineProperty。
先简单看下proxy:

  1. let a = { name: '123'};
  2. let proxy_a = new Proxy(a, {
  3. get(target, key) {
  4. console.log('in proxy:');
  5. console.log('-- target:',target);
  6. console.log('-- key:', key);
  7. return target[key];
  8. },
  9. set(target, key, value) {
  10. console.log('in proxy:');
  11. console.log('-- target:',target);
  12. console.log('-- key:', key);
  13. console.log('-- value:', value);
  14. return Reflect.set(target, key, value)
  15. }
  16. })

我们看到proxy_a是a对象的一个代理对象,相当于包裹的一层,所以,借由proxy对象,对被代理对象的操作可以先被拦截,执行固定的逻辑后,再通过反射,作用在被拦截对象上。
proxy比Object.defindProperty好在哪里呢?Object.defindProperty的机制是对每一个属性进行重写,是从属性的视角来出发做一些动作,但是proxy是从对象本身的角度出发。Object.defindProperty对于新增属性,那就要重新对这个属性进行监听,但是proxy就不用,因为对象就已经被监听(代理)过了。
可是proxy依旧存在问题:

  1. 属性是对象的引用问题,比如data.arr[3] = ‘ccc’;这个赋值,只写个proxy能监听到吗?监听不到…
  2. 和Object.defindProperty一样,对数组的可能操作index的操作,有可能会触发多次方法…

那么看下vue3中的实现,这里面最重要的就是reactive.ts 和 effect.ts:功能上来说,reactive可以把数据变成响应式数据,返回代理对象,而effect接受一个函数,这个函数会在响应式数据变化的时候被触发, 大致就是这个意思:

  1. const { reactive, effect } = VueReactivity;
  2. const data = { count: 0 };
  3. // 返回一个监听的新数据 proxy
  4. const state = reactive(data);
  5. const fn = () => {
  6. const count = state.count;// ==>get count:fn();
  7. console.log('当前的count:', count);
  8. };
  9. effect(fn);

reactive.ts:

  1. export function reactive(target: object) {
  2. //只读数据,直接返回 readonly ==>只读数据
  3. if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
  4. return target
  5. }
  6. //调用创建响应式数据方法
  7. return createReactiveObject(
  8. target,
  9. false,
  10. mutableHandlers,//不同逻辑的处理情况
  11. mutableCollectionHandlers
  12. )
  13. }

其核心逻辑是调用了createReactiveObject函数, mutableHandlers、mutableCollectionHandlers涉及更细致的具体的代理过程:
createReactiveObject代码如下:

  1. function createReactiveObject(
  2. target: Target,
  3. isReadonly: boolean,
  4. baseHandlers: ProxyHandler<any>,
  5. collectionHandlers: ProxyHandler<any>
  6. ) {
  7. if (!isObject(target)) {
  8. if (__DEV__) {
  9. console.warn(`value cannot be made reactive: ${String(target)}`)
  10. }
  11. return target
  12. }
  13. // 已经是响应式数据,并且不是只读的 proxy
  14. if (
  15. target[ReactiveFlags.RAW] &&
  16. !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  17. ) {
  18. return target
  19. }
  20. // 原始数据已经有代理数据,直接找到之前代理后的数据
  21. const proxyMap = isReadonly ? readonlyMap : reactiveMap
  22. const existingProxy = proxyMap.get(target)
  23. if (existingProxy) {
  24. return existingProxy
  25. }
  26. // 判断数据是否可以被代理,比如VNode对象,component实例等
  27. const targetType = getTargetType(target)
  28. if (targetType === TargetType.INVALID) {
  29. return target
  30. }
  31. // 处理代理逻辑,内部有对应的get、set
  32. const proxy = new Proxy(
  33. target,
  34. targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  35. );
  36. //代理之后,直接设置,避免下次重复代理一样的原始数据
  37. proxyMap.set(target, proxy);
  38. //返回数据
  39. return proxy
  40. }

看到在这个函数中,首先先进行了几个判断:target已经是响应式数据吗?已经有代理数据吗?数据是否可以被代理?都通过之后,构建了代理对象,这时候具体的方法:

  1. targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers

对于集合走collectionHandlers、对于数组和对象,走baseHandlers,我们重点关注下baseHandlers,这个东西其实是引入进来的mutableHandlers,位于包中baseHandlers.ts文件中,这里才是核心逻辑:

  1. export const mutableHandlers: ProxyHandler<object> = {
  2. get, // get逻辑,收集依赖
  3. set, // set逻辑,发布订阅
  4. deleteProperty,
  5. has,
  6. ownKeys
  7. }
  • 先看下get: ```typescript const get = /#PURE/ createGetter();

function createGetter(isReadonly = false, shallow = false) { return function get(target: Target, key: string | symbol, receiver: object) { //对特殊的key,做了一层处理,判断对应数据是否什么响应类型数据,只读与响应式。或者获取原始数据 if (key === ReactiveFlags.IS_REACTIVE) { return !isReadonly } else if (key === ReactiveFlags.IS_READONLY) { return isReadonly } else if ( key === ReactiveFlags.RAW && receiver === (isReadonly ? readonlyMap : reactiveMap).get(target) ) { return target }

  1. // 是数组吗?
  2. const targetIsArray = isArray(target)
  3. // 数组则借助此对象; arrayInstrumentations
  4. if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
  5. return Reflect.get(arrayInstrumentations, key, receiver)
  6. }
  7. //其他情况,直接获取
  8. const res = Reflect.get(target, key, receiver)
  9. //symbol不处理,直接返回
  10. if (
  11. isSymbol(key)
  12. ? builtInSymbols.has(key as symbol)
  13. : key === `__proto__` || key === `__v_isRef`
  14. ) {
  15. return res
  16. }
  17. // 不是只读数据,开始收集依赖:track
  18. if (!isReadonly) {
  19. track(target, TrackOpTypes.GET, key)
  20. }
  21. if (shallow) {
  22. return res
  23. }
  24. if (isRef(res)) { // 如果获取到的值是ref数据,直接返回对应的value情况
  25. // ref unwrapping - does not apply for Array + integer key.
  26. const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
  27. return shouldUnwrap ? res.value : res
  28. }

// 如果是对象,那就做一个懒代理,避免一开始就深度嵌套 // 这里很有点意思 if (isObject(res)) { // Convert returned value into a proxy as well. we do the isObject check // here to avoid invalid value warning. Also need to lazy access readonly // and reactive here to avoid circular dependency. return isReadonly ? readonly(res) : reactive(res) }

  1. return res

} }

  1. 这个get函数就很有点东西了~首先也是先做了一堆判断,然后判断了下是不是数组,数组暂且不表。如果不是数组,Reflect.get(target, key, receiver)通过反射拿到原始对象的值,res,之后,做了两件事情,1track,收集依赖;2)判断res是不是object;<br />为什么要判断res是不是object,这里其实就是vue3对于对象嵌套监听不到的问题的答案。
  2. 当获取的值res,经过isObject(res)判断之后又是对象,这种情况下对res也代理下,再调用reactive(res),这样解决了对象嵌套监听不到的问题,同时,这个是**懒代理**,懒在哪里?懒在没有像vue2那样对所有属性就深度遍历,而是对属性真正访问的时候,才去做的这样的代理,这样必然节约计算成本,性能甚好,岂不美哉?
  3. 回头看下数组:
  4. ```typescript
  5. // 重写数组,因为数组还是会存在修改一次数据,触发多次修改的情况,直接重写方法
  6. const arrayInstrumentations: Record<string, Function> = {}
  7. // values,可能会触发多次get的情况
  8. ;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {
  9. const method = Array.prototype[key] as any
  10. arrayInstrumentations[key] = function(this: unknown[], ...args: unknown[]) {
  11. //获取到数据的原始数据
  12. const arr = toRaw(this)
  13. for (let i = 0, l = this.length; i < l; i++) {
  14. track(arr, TrackOpTypes.GET, i + ''); // 收集一次依赖
  15. }
  16. // we run the method using the original args first (which may be reactive)
  17. const res = method.apply(arr, args);// 第一次执行,可能参数是响应式数据的情况
  18. if (res === -1 || res === false) {// 可能是数据被代理的情况,查找原始数据,重新处理
  19. // if that didn't work, run it again using raw values.
  20. return method.apply(arr, args.map(toRaw))
  21. } else {
  22. return res
  23. }
  24. }
  25. })
  26. // 改变数组的情况,可能导致多次get、set,特定处理
  27. ;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
  28. const method = Array.prototype[key] as any
  29. arrayInstrumentations[key] = function(this: unknown[], ...args: unknown[]) {
  30. pauseTracking()
  31. const res = method.apply(this, args) // 执行方法
  32. resetTracking()
  33. return res
  34. }
  35. });

可以看到,和vue2思路是相同的,重新数组方法。但是这个重写的逻辑都是借用原始的方法,const method = Array.prototype[key] as any,然后拿到原始方法的值 const res = method.apply(this, args) // 执行方法,在拿到值的前后做依赖收集的之类的行为。

  • 看下set:

    1. const set = /*#__PURE__*/ createSetter()
    2. function createSetter(shallow = false) {
    3. return function set(
    4. target: object,
    5. key: string | symbol,
    6. value: unknown,
    7. receiver: object
    8. ): boolean {
    9. // 获取到原来的值
    10. const oldValue = (target as any)[key]
    11. if (!shallow) {
    12. // 处理对应ref数据的情况
    13. value = toRaw(value)
    14. if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
    15. oldValue.value = value; // ref的时候,需要修改value来触发对应数据的内部逻辑
    16. return true
    17. }
    18. } else {
    19. // in shallow mode, objects are set as-is regardless of reactive or not
    20. }
    21. // 判断这个key是新添加的,还是修改的
    22. const hadKey =
    23. isArray(target) && isIntegerKey(key)
    24. ? Number(key) < target.length
    25. : hasOwn(target, key)
    26. // 触发修改逻辑
    27. const result = Reflect.set(target, key, value, receiver)
    28. // don't trigger if target is something up in the prototype chain of original
    29. // receiver 为Proxy或者继承Proxy的对象,这里需要处理原型链的情况,因为如果原型链继承的也是一个proxy,通过Reflect.set修改原型链上的属性会触发两次setter
    30. if (target === toRaw(receiver)) {
    31. if (!hadKey) {
    32. //trigger 派发通知
    33. trigger(target, TriggerOpTypes.ADD, key, value)
    34. } else if (hasChanged(value, oldValue)) {
    35. trigger(target, TriggerOpTypes.SET, key, value, oldValue)
    36. }
    37. }
    38. return result
    39. }
    40. }

set的核心逻辑就是要得到被代理对象的值,并且通知值已经产生变化,关注核心逻辑,const result = Reflect.set(target, key, value, receiver),得到result值,通过trigger派发通知(新增key和已存在的key调用trigger不太一样)

至此,响应式数据的代理过程基本了解了。
总结下来:

  • 在get阶段,vue3通过懒代理的方式解决object嵌套问题、通过重写数组方法解决调用数组方法后多次触发的问题;这个阶段涉及track收集依赖;
  • 在set阶段,涉及trigger,派发通知的过程;

effect、track、trigger

  • effect
    可以这么理解,vue3中这一套响应式数据系统,最初的激励就是依靠effect完成的。同时,effect还是后续系统流转起来的重要角色。那么effect是什么呢?(reactivity/src/effect.ts)
  1. export function effect<T = any>(
  2. fn: () => T,
  3. options: ReactiveEffectOptions = EMPTY_OBJ
  4. ): ReactiveEffect<T> {
  5. if (isEffect(fn)) {
  6. // 如果fn已经是一个effect函数了,那么就直接指向原始函数
  7. fn = fn.raw
  8. }
  9. // 包装
  10. const effect = createReactiveEffect(fn, options)
  11. //判断是否需要lazy,如果不是lazy,直接执行一次
  12. if (!options.lazy) {
  13. // ----> 执行
  14. effect()
  15. }
  16. //返回函数
  17. return effect
  18. }

可以看到effect函数内,执行了一次effect,并返回了effect,而这个effect是createReactiveEffect的返回值:

  1. function createReactiveEffect<T = any>(
  2. fn: () => T,
  3. options: ReactiveEffectOptions
  4. ): ReactiveEffect<T> {
  5. const effect = function reactiveEffect(): unknown {
  6. if (!effect.active) {
  7. // 1. 在effect没有激活的状态之下,调用effect的时候,没有调度,直接执行fn
  8. return options.scheduler ? undefined : fn()
  9. }
  10. if (!effectStack.includes(effect)) {
  11. cleanup(effect)
  12. try {
  13. // 2. 开启允许依赖收集
  14. enableTracking()
  15. // 3. effect压栈
  16. effectStack.push(effect)
  17. // 4. 设置激活的effect
  18. activeEffect = effect
  19. // 5. 执行fn逻辑,触发内部数据依赖的收集,开始收集activeEffect
  20. return fn();
  21. // targetMap
  22. } finally {
  23. // 6. 出栈
  24. effectStack.pop()
  25. // 恢复之前的状态
  26. resetTracking()
  27. // 指向栈最后一个effect
  28. activeEffect = effectStack[effectStack.length - 1]
  29. }
  30. }
  31. } as ReactiveEffect
  32. // 这里
  33. effect.id = uid++
  34. effect.allowRecurse = !!options.allowRecurse
  35. effect._isEffect = true // 表示一个effect函数
  36. effect.active = true // 激活状态
  37. effect.raw = fn // 原始的函数
  38. effect.deps = [] // effect对应的依赖 ,一个effect里面,有多个响应式数据的使用,可能这些数据会触发其他依赖的修改
  39. //其他配置项
  40. effect.options = options
  41. return effect
  42. }

看上面的代码中,逻辑大致可以整理成:1. 在effect没有激活的状态之下,调用effect的时候,没有调度,直接执行fn;2. 检查当前的effect栈中,如果不存在effect就把effect压入栈中,然后标记为激活,执行下fn并返回;简而言之,effect中是要执行fn的。

  • track
    然后看下get阶段要利用track来收集依赖,是怎么收集的?track函数也是在此文件中。 ```typescript

export function track(target: object, type: TrackOpTypes, key: unknown) { if (!shouldTrack || activeEffect === undefined) { return }

//targetMap 维护了全局的依赖情况 let depsMap = targetMap.get(target) if (!depsMap) { //每一个target,对应一个depsMap targetMap.set(target, (depsMap = new Map())) } let dep = depsMap.get(key) if (!dep) { //depsMap中维护了对应的key到dep的集合 depsMap.set(key, (dep = new Set())) } if (!dep.has(activeEffect)) { //收集当前激活的effect作为依赖 dep.add(activeEffect) activeEffect.deps.push(dep) //当前激活的effect,收集dep作为依赖,当前effect内部还有触发其他effect的情况 if (DEV && activeEffect.options.onTrack) { activeEffect.options.onTrack({ effect: activeEffect, target, type, key }) } } }

  1. track是当data中的keyget的时候触发的,首先我们要知道,这里要做的事情是收集依赖关系,维护“电话本”。<br />然后看函数,target是原始数据,首先,用targetMap.get(target)来获得对target的依赖关系,targetMap是一个Weakmap,所以key可以不是基本类型。<br />这里targetMap是对target的依赖,target上的key的依赖是:let dep = depsMap.get(key),dep是一个set,当前激活状态下的activeEffect最终被add到这个set中了,dep.add(activeEffect),这个数据结构见定义:
  2. ```typescript
  3. type Dep = Set<ReactiveEffect>
  4. type KeyToDepMap = Map<any, Dep>
  5. // targetMap
  6. const targetMap = new WeakMap<any, KeyToDepMap>();

ok,这就是track的过程,简单说维护了一个Weakmap,存放依赖关系,所谓的关系,就是key和effect发生了关系,一对多的关系…(没有开车)

  • trigger
    当响应式数据被赋值,那自然是触发set方法,而在set方法中会触发trigger,函数也是在这个文件里面:

    1. export function trigger(
    2. target: object,
    3. type: TriggerOpTypes,
    4. key?: unknown,
    5. newValue?: unknown,
    6. oldValue?: unknown,
    7. oldTarget?: Map<unknown, unknown> | Set<unknown>
    8. ) {
    9. // 获取到对应数据原始数据的依赖集合
    10. const depsMap = targetMap.get(target)
    11. if (!depsMap) {
    12. // 没有被收集,直接返回
    13. return
    14. }
    15. // 创建需要运行的effect集合
    16. const effects = new Set<ReactiveEffect>()
    17. // 定义遍历添加effects的函数
    18. const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
    19. if (effectsToAdd) {
    20. effectsToAdd.forEach(effect => {
    21. if (effect !== activeEffect || effect.allowRecurse) {
    22. effects.add(effect)
    23. }
    24. })
    25. }
    26. }
    27. // 触发了对应的操作,修改、删除、添加,都添加effect依赖到effects中
    28. if (type === TriggerOpTypes.CLEAR) {
    29. // collection being cleared
    30. // trigger all effects for target
    31. depsMap.forEach(add)
    32. } else if (key === 'length' && isArray(target)) {
    33. depsMap.forEach((dep, key) => {
    34. if (key === 'length' || key >= (newValue as number)) {
    35. add(dep)
    36. }
    37. })
    38. } else {
    39. // schedule runs for SET | ADD | DELETE
    40. if (key !== void 0) {
    41. add(depsMap.get(key))
    42. }
    43. // also run for iteration key on ADD | DELETE | Map.SET
    44. switch (type) {
    45. case TriggerOpTypes.ADD:
    46. if (!isArray(target)) {
    47. add(depsMap.get(ITERATE_KEY))
    48. if (isMap(target)) {
    49. add(depsMap.get(MAP_KEY_ITERATE_KEY))
    50. }
    51. } else if (isIntegerKey(key)) {
    52. // new index added to array -> length changes
    53. add(depsMap.get('length'))
    54. }
    55. break
    56. case TriggerOpTypes.DELETE:
    57. if (!isArray(target)) {
    58. add(depsMap.get(ITERATE_KEY))
    59. if (isMap(target)) {
    60. add(depsMap.get(MAP_KEY_ITERATE_KEY))
    61. }
    62. }
    63. break
    64. case TriggerOpTypes.SET:
    65. if (isMap(target)) {
    66. add(depsMap.get(ITERATE_KEY))
    67. }
    68. break
    69. }
    70. }
    71. //定义执行函数
    72. const run = (effect: ReactiveEffect) => {
    73. if (__DEV__ && effect.options.onTrigger) {
    74. effect.options.onTrigger({
    75. effect,
    76. target,
    77. key,
    78. type,
    79. newValue,
    80. oldValue,
    81. oldTarget
    82. })
    83. }
    84. // 如果有调度函数,就先执行调度函数
    85. if (effect.options.scheduler) {
    86. effect.options.scheduler(effect)
    87. } else {
    88. //没有的话,直接执行
    89. effect()
    90. }
    91. }
    92. effects.forEach(run);// 遍历执行effect
    93. }

这段代码看似长,但是实际上逻辑不难,主要流程就是在找到target和key有关的依赖—-effect,把这些effect最终遍历执行。
所以,上述过程就接上了:当我们访问数据,会将数据和effect建立关系维护起来;当我们修改数据,修改的时候要找到维护起来的关系,依次执行这些相关的effect。
再对比下Vue2,vue3中的双向数据绑定,或者说响应式数据流程,从v2的Object.definedPorperty() + Dep + Watcher 变成了v3时代Proxy + Weakmap + effect~

vue3 编译和优化

vue2编译的问题主要有2点:1. 正则; 2. with;

  1. 正则匹配,回溯的算法问题。所以用正则匹配回溯次数完全不可控;
    vue3用了状态机(经典方案,很多编译器都是AST + 状态机) 。
    以前是用字符串正则匹配出指令,现在改成解析成AST。

在vue3中,编译最终生成的是这样,astexplorer,这是个在线编译的工具,可以看到编译后带render函数,以及在console中输出了AST树对象:

  1. <div>Hello World!
  2. {{ msg }}
  3. </div>

编译成:

  1. import { toDisplayString as _toDisplayString, createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from "vue"
  2. export function render(_ctx, _cache, $props, $setup, $data, $options) {
  3. return (_openBlock(), _createBlock("div", null, "Hello World! " + _toDisplayString(_ctx.msg), 1 /* TEXT */))
  4. }
  1. with的问题
    with,不会立即释放内存,导致性能问题,vue3中离线编译出来的render已经不需要with了,而是有一个ctx作为参数,代替以前的with的功能;
    问题来了:vue2为什么不把数据挂在ctx上,这样不是也可以不用with 只要把变量都变成ctx.变量名 ?
    我的理解,因为vue2中不能对模版中的动态语句,没有做,分析不了。所以无法变成 ctx.变量名形式??
    这里逻辑是不是这样,因为with性能不好,所以需要优化,但是没有with又拿不到数据,所以把数据放到ctx上了,所以需要分析语句,因为至少需要在变量前面加上 ctx.变量名??
    {{ name + familyName }} 比如在vue2时代,没有分析语句的话,要变成 ctx.name和ctx.familyName,因为编译程序无法识别这个字符串从哪里到那里是变量,所以就简单粗暴的在最外层加一个with,这语句原封不动拿过来就好。
  2. 提升动态VNode

    1. <div>
    2. <section v-if="foo">
    3. <p>{{ a }}</p>
    4. </section>
    5. <div v-else>
    6. <p>{{ a }}</p>
    7. </div>
    8. </div>

这个节点生成的Vnode可能会是这样:

  1. const block = {
  2. tag: 'div',
  3. dynamicChildren: [
  4. { tag: 'p', children: ctx.a, patchFlag: 1 }
  5. ]
  6. }

dynamicChildren会把当前的组件的动态节点向上提升,这样的好处就是,更新的时候不用遍历到更深的内部了。但是提升到什么高度呢,总不能提升到root吧。
这里这个机制对事件也适用。

  1. 连续的静态节点生成字符串

诸如这样的一些列静态节点:

  1. <div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div> ... n ... <div></div><div></div><div></div>

在编译时就直接会生成静态节点的字符串:

  1. _createStaticVnode('<div></div><div>......</div>')

启动流程和挂载

看下vue整体的启动流程, 用vue是从这里开始的:

  1. import { createApp } from 'vue'
  2. import App from './app'
  3. const app = createApp(App)
  4. app.mount('#app')

这部分代码很长,很多,所以目下只学了个流程,代码就贴个简单的架子。
从createApp开始,代码在runtime-core包的index.ts中:

  1. export const createApp = ((...args) => {
  2. // A >> 创造app 实例对象
  3. const app = ensureRenderer().createApp(...args)
  4. if (__DEV__) {
  5. injectNativeTagCheck(app)
  6. }
  7. // 重写挂载的mount
  8. const { mount } = app
  9. app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
  10. // ...重写挂载的mount,可以独立到不用的平台实现
  11. }
  12. return app
  13. }) as CreateAppFunction<Element>

上述代码的A处,可以看到ensureRenderer().createApp(…args)这个方法返回了app实例,最终被return出去了。所以追进去看下ensureRenderer, 依旧在index.ts:

  1. function ensureRenderer() {
  2. return renderer || (renderer = createRenderer<Node, Element>(rendererOptions))
  3. }

发现这里其实返回的是createRenderer函数的返回值,createRenderer代码在runtime-core包的renderer.ts中,发现其最终调用同模块下的baseCreateRenderer函数,我们看下这个函数, 这是个超长的函数,我们需要看下框架:

  1. function baseCreateRenderer(
  2. options: RendererOptions,
  3. createHydrationFns?: typeof createHydrationFunctions
  4. ): any {
  5. // compile-time feature flags check
  6. if (__ESM_BUNDLER__ && !__TEST__) {
  7. initFeatureFlags()
  8. }
  9. const {
  10. // ... 结构配置选项
  11. } = options
  12. // ******** 开始大规模函数定义
  13. const patch: PatchFn = () => {}
  14. const processText: ProcessTextOrCommentFn = () => {}
  15. const processCommentNode: ProcessTextOrCommentFn = () => {}
  16. const mountStaticNode = () => {}
  17. const patchStaticNode = () => {}
  18. const moveStaticNode = () => {}
  19. const removeStaticNode = () => {}
  20. const processElement = () => {}
  21. const mountElement = () => {}
  22. const setScopeId = () => {}
  23. const mountChildren: MountChildrenFn = () => {}
  24. const patchElement = () => {}
  25. const patchBlockChildren: PatchBlockChildrenFn = () => {}
  26. const patchProps = () => {}
  27. const processFragment = () => {}
  28. const processComponent = () => {}
  29. const mountComponent: MountComponentFn = () => {}
  30. const updateComponent = () => {}
  31. const setupRenderEffect: SetupRenderEffectFn = () => {}
  32. const updateComponentPreRender = () => {}
  33. const patchChildren: PatchChildrenFn = () => {}
  34. const patchUnkeyedChildren = () => {}
  35. const patchKeyedChildren = () => {}
  36. const move: MoveFn = () => {}
  37. const unmount: UnmountFn = () => {}
  38. const remove: RemoveFn = () => {}
  39. const removeFragment = () => {}
  40. const unmountComponent = () => {}
  41. const unmountChildren: UnmountChildrenFn = () => {}
  42. const getNextHostNode: NextFn = vnode => () => {}
  43. const render: RootRenderFunction = () => {}
  44. // ******** end 大规模函数定义
  45. const internals: RendererInternals = {
  46. p: patch,
  47. um: unmount,
  48. m: move,
  49. r: remove,
  50. mt: mountComponent,
  51. mc: mountChildren,
  52. pc: patchChildren,
  53. pbc: patchBlockChildren,
  54. n: getNextHostNode,
  55. o: options
  56. }
  57. let hydrate: ReturnType<typeof createHydrationFunctions>[0] | undefined
  58. let hydrateNode: ReturnType<typeof createHydrationFunctions>[1] | undefined
  59. if (createHydrationFns) {
  60. ;[hydrate, hydrateNode] = createHydrationFns(internals as RendererInternals<
  61. Node,
  62. Element
  63. >)
  64. }
  65. // 返回对象
  66. return {
  67. render,
  68. hydrate,
  69. createApp: createAppAPI(render, hydrate)
  70. }
  71. }

上面的代码本来是一个几千行的代码,这里列出的只是结构,而且这些函数的签名都不对,为了简单,都删了参数列表。
我们只看关心的部分,之前的vue app实例其实就是createApp调用后的返回值,那么找下来就是baseCreateRenderer返回对象中的creatrApp,它是这么得到的reateAppAPI(render, hydrate),这个render就是上面这些大规模函数中的render:const render: RootRenderFunction = () => {}。
ok,先看下createAppAPI,此函数位于runtime-core包的apiCreateApp.ts:

  1. export function createAppAPI<HostElement>(
  2. render: RootRenderFunction,
  3. hydrate?: RootHydrateFunction
  4. ): CreateAppFunction<HostElement> {
  5. //接受两个参数,根组件的对象与props,默认为null
  6. return function createApp(rootComponent, rootProps = null) {
  7. if (rootProps != null && !isObject(rootProps)) {
  8. __DEV__ && warn(`root props passed to app.mount() must be an object.`)
  9. rootProps = null
  10. }
  11. const context = createAppContext();//创建应用content
  12. const installedPlugins = new Set();//所有插件
  13. let isMounted = false
  14. //app实例
  15. const app: App = (context.app = {
  16. _uid: uid++,
  17. _component: rootComponent as ConcreteComponent,
  18. _props: rootProps,//props
  19. _container: null,//挂载容器
  20. _context: context,
  21. version,
  22. get config() {
  23. },
  24. set config(v) {
  25. },
  26. // 挂载中间件,通过use
  27. use(plugin: Plugin, ...options: any[]) {
  28. },
  29. // 使用mixin的api
  30. mixin(mixin: ComponentOptions) {
  31. },
  32. // 在这个App实例上定义组件
  33. component(name: string, component?: Component): any {
  34. },
  35. // 定义指令
  36. directive(name: string, directive?: Directive) {
  37. },
  38. // 挂载内容,核心的组件渲染逻辑
  39. mount(rootContainer: HostElement, isHydrate?: boolean): any {
  40. },
  41. //卸载
  42. unmount() {
  43. },
  44. provide(key, value) {
  45. }
  46. })
  47. return app
  48. }
  49. }

看下结构,createAppAPI返回的是一个函数:createApp,createApp返回的就是vue实例了,看到vue实例的方法都在这里了,依旧只贴了个架子。
不过,在使用vue的时候,createApp得到app之后,会调用app.mount(‘#app’);这里的mount就在这里了,所以看下mount函数:

  1. // rootContainer : #app
  2. mount(rootContainer: HostElement, isHydrate?: boolean): any {
  3. if (!isMounted) {
  4. // 1. 没有挂载,创建根节点的VNode
  5. const vnode = createVNode(
  6. rootComponent as ConcreteComponent,
  7. rootProps
  8. );
  9. // store app context on the root VNode.
  10. // this will be set on the root instance on initial mount.
  11. vnode.appContext = context
  12. // HMR root reload
  13. if (__DEV__) {
  14. context.reload = () => {
  15. render(cloneVNode(vnode), rootContainer)
  16. }
  17. }
  18. if (isHydrate && hydrate) {
  19. hydrate(vnode as VNode<Node, Element>, rootContainer as any)
  20. } else {
  21. //NOTE:实例化触发render,使用渲染器渲染VNode,传入VNode与container容器
  22. render(vnode, rootContainer)
  23. }
  24. isMounted = true
  25. app._container = rootContainer
  26. // for devtools and telemetry
  27. ; (rootContainer as any).__vue_app__ = app
  28. if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
  29. devtoolsInitApp(app, version)
  30. }
  31. return vnode.component!.proxy
  32. } else if (__DEV__) {
  33. warn(
  34. `App has already been mounted.\n` +
  35. `If you want to remount the same app, move your app creation logic ` +
  36. `into a factory function and create fresh app instances for each ` +
  37. `mount - e.g. \`const createMyApp = () => createApp(App)\``
  38. )
  39. }
  40. },

上面方法中rootComponent就是const app = createApp(App)的时候传进去的App组件。可以看到它先创建了这个App(根组件的)Vnode,然后后面经过一些环境判断,最终的逻辑都是调用render

  1. // vnode => App
  2. // rootContainer => #app
  3. render(vnode, rootContainer)

这个render,就又回到了renderer.ts的baseCreateRenderer中了,这回单看render函数:

  1. const render: RootRenderFunction = (vnode, container) => {
  2. //卸载流程
  3. if (vnode == null) {
  4. if (container._vnode) {
  5. unmount(container._vnode, null, null, true)
  6. }
  7. // 1. 创建或者更新组件
  8. patch(container._vnode || null, vnode, container)
  9. }
  10. flushPostFlushCbs();
  11. //缓存VNode节点,表示已经渲染过
  12. container._vnode = vnode
  13. }

我们看到核心的逻辑来到了patch函数中,那就继续看patch:

  1. const patch: PatchFn = (
  2. n1,
  3. n2,
  4. container,
  5. anchor = null,
  6. parentComponent = null,
  7. parentSuspense = null,
  8. isSVG = false,
  9. optimized = false
  10. ) => {
  11. if (n1 && !isSameVNodeType(n1, n2)) {
  12. anchor = getNextHostNode(n1)
  13. unmount(n1, parentComponent, parentSuspense, true)
  14. // n1设置为null,保证后面走整个节点的mount逻辑
  15. n1 = null
  16. }
  17. //节点属于没有优化的类型
  18. if (n2.patchFlag === PatchFlags.BAIL) {
  19. optimized = false
  20. n2.dynamicChildren = null
  21. }
  22. const { type, ref, shapeFlag } = n2
  23. switch (type) {
  24. case Text: // 处理文本 略...
  25. case Comment: // 处理注释 略...
  26. case Static:// 处理静态节点 略...
  27. case Fragment:// 处理Fragment元素</> 略...
  28. default:
  29. //elemment 处理DOM元素
  30. if (shapeFlag & ShapeFlags.ELEMENT) {
  31. // 元素处理逻辑
  32. processElement( /* 参数略 */ )
  33. } else if (shapeFlag & ShapeFlags.COMPONENT) {
  34. // ==> 组件 组件处理逻辑
  35. processComponent( /* 参数略 */ )
  36. } else if (shapeFlag & ShapeFlags.TELEPORT) {
  37. // 处理TELEPORT 略
  38. } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
  39. // 处理SUSPENSE 略
  40. } else if (__DEV__) {
  41. warn('Invalid VNode type:', type, `(${typeof type})`)
  42. }
  43. }
  44. if (ref != null && parentComponent) {
  45. setRef(ref, n1 && n1.ref, parentSuspense, n2)
  46. }
  47. }

代码被删过还是挺少的,发现patch方法对不同类型的节点处理方法都进行了分发,不过现在只关系n2.shapeFlag === ShapeFlags.COMPONENT 的情况,就是处理组件,处理组件的逻辑很显然要去看processComponent函数的代码了:

  1. const processComponent = (
  2. n1: VNode | null,
  3. n2: VNode,
  4. container: RendererElement,
  5. anchor: RendererNode | null,
  6. parentComponent: ComponentInternalInstance | null,
  7. parentSuspense: SuspenseBoundary | null,
  8. isSVG: boolean,
  9. optimized: boolean
  10. ) => {
  11. if (n1 == null) {
  12. if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
  13. // keep-alive不管
  14. } else {
  15. // 组件初始化挂载
  16. mountComponent(
  17. n2,
  18. container,
  19. anchor,
  20. parentComponent,
  21. parentSuspense,
  22. isSVG,
  23. optimized
  24. )
  25. }
  26. } else {
  27. updateComponent(n1, n2, optimized)
  28. }
  29. }

我们看挂在阶段的话,n1应该就是null,所以继续追踪下去就是mountComponent,顾名思义,updateComponent是更新节点用的,现在是挂载阶段,mountComponent is Here:

  1. // 挂载组件的处理逻辑
  2. const mountComponent: MountComponentFn = (
  3. initialVNode, // n2
  4. container,
  5. anchor,
  6. parentComponent,
  7. parentSuspense,
  8. isSVG,
  9. optimized
  10. ) => {
  11. // 1. 创建组件实例
  12. const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance(
  13. initialVNode,
  14. parentComponent,
  15. parentSuspense
  16. ))
  17. // 非核心流程 略...
  18. if (__DEV__ && instance.type.__hmrId) { }
  19. if (__DEV__) { }
  20. if (isKeepAlive(initialVNode)) { }
  21. if (__DEV__) { }
  22. // 2. 设置组件实例
  23. setupComponent(instance) //render 数据 组件实例上面 setup
  24. // 非核心流程 略...
  25. if (__DEV__) { }
  26. if (__FEATURE_SUSPENSE__ && instance.asyncDep) { }
  27. // 3. setupRenderEffect
  28. setupRenderEffect(
  29. instance,
  30. initialVNode,
  31. container,
  32. anchor,
  33. parentSuspense,
  34. isSVG,
  35. optimized
  36. )
  37. if (__DEV__) { }
  38. }

这个函数中我们观察主流程:1. 创建组件实例;2. 设置组件实例 setupComponent(instance); 3. 调用setupRenderEffect;
1) 先跟进下createComponentInstance,这个方法在同包下的component.ts:

  1. export function createComponentInstance(
  2. vnode: VNode,
  3. parent: ComponentInternalInstance | null,
  4. suspense: SuspenseBoundary | null
  5. ) {
  6. const type = vnode.type as ConcreteComponent
  7. // inherit parent app context - or - if root, adopt from root vnode
  8. const appContext =
  9. (parent ? parent.appContext : vnode.appContext) || emptyAppContext
  10. // 创建实例
  11. const instance: ComponentInternalInstance = {
  12. uid: uid++,// 组件唯一id
  13. vnode,// 组件的VNode
  14. type,// VNode 节点类型
  15. parent,// 父组件实例
  16. appContext,// app上下文
  17. root: null!, // to be immediately set 根组件实例
  18. next: null,// 新的组件VNode
  19. subTree: null!, // will be set synchronously right after creation 子组件的VNode
  20. update: null!, // will be set synchronously right after creation //带有副作用的更新函数
  21. render: null,// 渲染函数
  22. proxy: null,// 渲染上下文代理
  23. exposed: null,
  24. withProxy: null,// 带有with区块的渲染上下文代理
  25. effects: null,// 响应式的闲逛对象
  26. provides: parent ? parent.provides : Object.create(appContext.provides),//依赖注入
  27. accessCache: null!,// 渲染代理的属性访问缓存
  28. renderCache: [],// 渲染缓存
  29. // local resovled assets
  30. components: null, //注册的组件
  31. directives: null,//注册的指令
  32. // resolved props and emits options
  33. propsOptions: normalizePropsOptions(type, appContext), // 标准化的props
  34. emitsOptions: normalizeEmitsOptions(type, appContext),// 标准化的emits
  35. // emit
  36. emit: null as any, // to be set immediately //事件派发方法
  37. emitted: null,
  38. // state
  39. ctx: EMPTY_OBJ,// 渲染上下文
  40. data: EMPTY_OBJ,// data数据
  41. props: EMPTY_OBJ,// props数据
  42. attrs: EMPTY_OBJ,// 普通属性
  43. slots: EMPTY_OBJ,// 插槽
  44. refs: EMPTY_OBJ,// 组件ref
  45. setupState: EMPTY_OBJ,// steup返回的响应式结果
  46. setupContext: null,// setup函数上下文
  47. // suspense related
  48. suspense,
  49. suspenseId: suspense ? suspense.pendingId : 0,
  50. asyncDep: null,
  51. asyncResolved: false,
  52. // lifecycle hooks 生命周期
  53. // not using enums here because it results in computed properties
  54. isMounted: false,// 是否挂载
  55. isUnmounted: false,// 是否卸载
  56. isDeactivated: false,// 是否激活
  57. bc: null,// before create
  58. c: null,// created
  59. bm: null,// before mounted
  60. m: null,// mounted
  61. bu: null,// before update
  62. u: null,// updated
  63. um: null,// unmounted
  64. bum: null,// before unmount
  65. da: null,// deactived
  66. a: null,// actived
  67. rtg: null,// render triggered
  68. rtc: null,// render tracked
  69. ec: null// error
  70. }
  71. if (__DEV__) { }
  72. instance.root = parent ? parent.root : instance
  73. instance.emit = emit.bind(null, instance)
  74. if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { }
  75. return instance
  76. }

这个方法最终return组件的instance,这么多属性,详细含义见注释。
2)setupComponent也和上面的createComponentInstance位于同一个文件下面,这个函数是在设置初始属性另外用户在setup中写的逻辑也在这里执行:

  1. export function setupComponent(
  2. instance: ComponentInternalInstance,
  3. isSSR = false
  4. ) {
  5. isInSSRComponentSetup = isSSR
  6. const { props, children, shapeFlag } = instance.vnode
  7. const isStateful = shapeFlag & ShapeFlags.STATEFUL_COMPONENT
  8. //设置属性
  9. initProps(instance, props, isStateful, isSSR)
  10. initSlots(instance, children)
  11. // 执行setup(){} 设置有状态的实例相关数据
  12. const setupResult = isStateful
  13. ? setupStatefulComponent(instance, isSSR) // 设置setup和render
  14. : undefined
  15. isInSSRComponentSetup = false
  16. return setupResult
  17. }

这里的setupStatefulComponent 非常值得一看究竟:

  1. function setupStatefulComponent(
  2. instance: ComponentInternalInstance,
  3. isSSR: boolean
  4. ) {
  5. const Component = instance.type as ComponentOptions
  6. if (__DEV__) { }
  7. // 1. 创建渲染代理的属性访问缓存
  8. instance.accessCache = Object.create(null)
  9. // 2. create public instance / render proxy 创建上下文代理
  10. // 访问实例上的props、options、data的时候,都可以通过组件实例上的ctx上访问到对应的结果,这就是一个代理的过程
  11. instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)
  12. if (__DEV__) { }
  13. // 3. call setup() 调用setup
  14. const { setup } = Component
  15. if (setup) {
  16. // 省略 setup,其实setup中的逻辑后面也会
  17. // 走到finishComponentSetup
  18. } else {
  19. // 完成组件实例的设置
  20. finishComponentSetup(instance, isSSR)
  21. }
  22. }

可以看到,这段代码的逻辑是首先,设置上下文代理,而后如果又setup的逻辑需要执行就执行setup,有没有setup其实最后都能走到了finishComponentSetup函数中,这个finishComponentSetup就很重要了,因为我们模版的编译逻辑就在里面:
截取一段代码:

  1. if (Component.render) {
  2. instance.render = Component.render as InternalRenderFunction
  3. }
  4. } else if (!instance.render) {
  5. // could be set from setup() 设置对应的render
  6. if (compile && Component.template && !Component.render) {
  7. if (__DEV__) { }
  8. // 缓存render到component中,在线编译模板的情况
  9. Component.render = compile(Component.template, {
  10. isCustomElement: instance.appContext.config.isCustomElement,
  11. delimiters: Component.delimiters
  12. })
  13. if (__DEV__) { }
  14. }
  15. //挂载render到实例上
  16. instance.render = (Component.render || NOOP) as InternalRenderFunction
  17. // ....
  18. }

可以看到instance.render 最终挂载了编译的结果(编译的结果是一个render函数这在上文中提及),更深入的逻辑就不进入了。
3)setupRenderEffect: instance已经构造好了,下面调用的就是这个函数, 这个函数回到了renderer.ts的baseCreateRenderer中来,这可能将是初始化中最复杂的一个函数:

  1. const setupRenderEffect: SetupRenderEffectFn = (
  2. instance,
  3. initialVNode,
  4. container,
  5. anchor,
  6. parentSuspense,
  7. isSVG,
  8. optimized
  9. ) => {
  10. // instance的update被赋值了effect
  11. instance.update = effect(function componentEffect() {
  12. if (!instance.isMounted) { // 没挂载
  13. let vnodeHook: VNodeHook | null | undefined
  14. const { el, props } = initialVNode
  15. const { bm, m, parent } = instance // beforemount
  16. // beforeMount hook 生命周期的调用 before mounted、mounted
  17. if (bm) { invokeArrayFns(bm) }
  18. // onVnodeBeforeMount
  19. if ((vnodeHook = props && props.onVnodeBeforeMount)) {
  20. invokeVNodeHook(vnodeHook, parent, initialVNode)
  21. }
  22. if (__DEV__) { }
  23. // ⭐️⭐️⭐️⭐️⭐️
  24. // 这个方法很重要
  25. const subTree = (instance.subTree = renderComponentRoot(instance))
  26. if (__DEV__) { }
  27. if (el && hydrateNode) {
  28. if (__DEV__) { }
  29. hydrateNode(/* 略参数 */)
  30. if (__DEV__) { }
  31. } else {
  32. if (__DEV__) { }
  33. // 把对应的子树渲染到container中,子树可能是element、text、component等
  34. patch(
  35. null,
  36. subTree,
  37. container,
  38. anchor,
  39. instance,
  40. parentSuspense,
  41. isSVG
  42. )
  43. if (__DEV__) { }
  44. // 保留渲染生成子树的根节点 DOM节点
  45. initialVNode.el = subTree.el
  46. }
  47. // mounted hook 挂载的生命周期调用
  48. if (m) { queuePostRenderEffect(m, parentSuspense) }
  49. // onVnodeMounted
  50. // ... 省略了部分处理 issues的逻辑...
  51. } else { // 组件已经被挂载,那就是更新了
  52. /**
  53. * 省略了更新流程 大致是:
  54. * 1 更新组件VNode的节点信息
  55. * 2 渲染成为新的子树VNode
  56. * 3 根据新旧子树VNode执行patch逻辑
  57. */
  58. }
  59. }, __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions)
  60. }

setupRenderEffect这个函数,以及其内部的调用函数renderComponentRoot将会将整个流程串联起来,非常重要~
这个函数的结构是这样,只有一句话instance.update = effect(componentEffect);
上文中我们学习了effect是vue3中响应式数据的重要触发方法,effect内部的函数fn会先执行,然后因为响应式数据的track和trigger机制,在track阶段收集的、trigger阶段查找到的依赖关系本质上都是一个个effect函数。
那么,现在讨论的是挂载阶段,就挂载阶段来看,再仔细看下effect中的componentEffect,这个函数最重要的一点就是生成对应的subTree,调用了renderComponentRoot,这个函数在componentRenderUtils.ts中。
这个函数就从instance中拿到了render方法,并执行:

  1. result = normalizeVNode(
  2. render.length > 1
  3. ? render(
  4. props,
  5. __DEV__
  6. ? {
  7. get attrs() {
  8. markAttrsAccessed()
  9. return attrs
  10. },
  11. slots,
  12. emit
  13. }
  14. : { attrs, slots, emit }
  15. )
  16. : render(props, null as any /* we know it doesn't need it */)
  17. )

在render中使用了响应式数据,所以依赖关系的收集这些步骤也会如期进行,我们的整体流程就串联起来了。

关于挂载和更新整体总结下,这里整体的核心流程是:

  1. 判断是挂载组件,还是更新组件,挂载调用mountComponent,更新调用updateComponent;
  2. 对于首次挂载的mountedComponent内部;
    a. 创建组件实例,根据组件VNode;
    b. 设置组件实例,调用setup方法,以及处理options等等;
    c. 触发并且运行带有副作用的渲染函数,内部会结合使用Effect与render的调用;
  3. 渲染逻辑
    a. 生成对应的subTree,因为组件会是其他子组件的情况,这个过程是调用render的过程,然后生成对应的subTree;
    b. 把对应的subTree渲染到container中,通过执行对应的patch,然后再次进入到这个函数,如果patch是element,就开始触发渲染任务;
    不过结合响应式数据整体串联起来更重要。