诗号:六道同坠,魔劫万千,引渡如来。

/img/bdx/yiyeshu-001.jpg

stb-vue-next 完全拷贝于 vue-next ,主要目的用于学习。

本文为 runtime-core(2) 续集,上篇: Vue3 源码头脑风暴之 7 ☞ runtime-core(2) - render

流程图(脑图)

/img/vue3/runtime-core/vue-runtime-core-render-component.svg

这一节新增内容较多,主要新增以下几个函数

  1. processComponent() 在 patch() 中执行 switch default 分支,满足 ShapeFlags.COMPONENT 条件

  2. mountComponent(n2,...) 首次加载组件时调用的函数

  3. setupComponent(instance) 建立组件实例,做一些结构初始化操作(如:props和 slots)等

  4. setupStatefulComponent(instance,isSSR) 创建有状态组件,执行 setup() 函数

  5. setupRenderEffect() 通过 effect() 函数返回 instance.update 创建一个监听- 更新函数。

  6. finishComponentSetup(instance,isSSR) 这个函数在 setupStatefulComponent() 中调用,主要做的事情是处理 SSR,没有 render 函数有 template 时调用 compile 编 译出 render 函数,兼容 2.x 的 options api

processComponent(如何patch组件的?)

问题修复: TypeError: Cannot read property 'allowRecurse' of null

processComponent(n1,n2,...) 函数主要分三种情况

  1. mount, 没有 n1 old 时候,属于纯 mount 操作

    1. keep-alive 类型,只需要重新激活 activate

    2. 否则执行 mountComponent(n2, ….) 首次加载组件

  2. update, 非首次加载执行更新操作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const { log, f, shuffle, runtime_test, renderChildren } = require(process.env
  .BLOG_DIR_VUE + "/lib.js");
import(process.env.BLOG_DIR_VUE + "/runtime-test.global.js").then(
  ({ h, render, nodeOps, serializeInner: inner, ref }) => {
    const root = nodeOps.createElement("div");
    const logRoot = () => log("root: " + inner(root));

    logRoot();
    const value = ref(true);
    let parentVnode, childVnode1, childVnode2;

    const Parent = {
      render: () => {
        // return h("div", "测试...");
        return (parentVnode = h(Child));
      },
    };

    const Child = {
      render: () => {
        return value.value
          ? (childVnode1 = h("div", "child 1"))
          : (childVnode2 = h("span", "child 2"));
      },
    };

    const p = h(Parent);
    render(p, root);
    logRoot();
    value.value = false;
    logRoot();
  },
  (err) => {
    console.log(err.message);
  }
);
undefinedroot:
component stateful ? 4
call setup
no setup
[Function: render] render
mount component
normalize vnode
patch component
component stateful ? 4
call setup
no setup
[Function: render] render
mount component
normalize vnode
patch component
root: <div>child 1</div>
root: <div>child 1</div>
component update

流程简图:

/img/vue3/runtime-core/vue-runtime-core-render-component-brief.svg

这里执行就是 mountComponent(n2,...) 行为,首次加载组件,完成:

  1. setupComponent(instance) 执行 setup 函数,初始化 props&slots 等

  2. setupRenderEffect(instance,...) 注册 instance.update effect

    当实例状态发生改变时执行这个 effect fn,如果是首次(父级调用 processComponent) 执行!isMounted 分支进行组件首次加载,否则当组件自身状态改变是触发的 update 操 作

setupComponent 中,主要完成

  1. initProps

  2. initSlots

  3. setupStatefulComponent(instance,isSSR) 有状态组件(非函数组件)

紧接着 setupStatefulComponent(instance,isSSR) 中检测 setup 函数,并执行它,如 果没有 setup 函数就进入 finishComponentSetup(instance) 检测 render 或 template 最终目的是获得 render 函数,如果没有 render 会通过 compile(template) 编译出 render 函数,最后在 instance.update 中执行 render 函数(在这前后会触发 beforeMount 和 mounted 周期函数)。

所以,一套流程下来可以简单描述为

mount -> props&slots 初始化 -> setup() -> 有状态组件处理得到 render 函数 -> 最后 通过 instance.update effect 来监听实例状态变化,触发 mount 或者 update。

在 effect mount 阶段会触发生命周期函数:

  1. beforeMount + mounted

  2. onVnodeBeforeMount + onVnodeMounted(针对 vnode 结构变化而言)

  3. activated(如果是 keep-alive 的话)

组件的渲染就发生在 beforeMount 之后 mounted 之前的 renderComponentRoot() 得到 vnode 交给 patch 去进行渲染。

示例代码中,后面修改了 value.value=false 后面 dom 并没改变,但是输出了 component update 说明进入了 instance.update effect 的 else 分支,因为不是第 一次,所以这里需要实现更新组件部分。

effect update component

因为 instance.update 是通过 effect() 封装的函数,且这个函数中使用到了 instance 实例而这个实例又在 setupComponent 中有做过代理,因此对它的访问会触发 effect track,状态更新会触发 effect trigger(响应式原理)。

feat(add): component update · gcclll/stb-vue-next@1254465

涉及的修改:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
instance.update = effect(
  function componentEffect() {
    // 监听更新
    if (!instance.isMounted) {
      // ...
    } else {
      // updateComponent
      // 当组件自身的状态或父组件调用 processComponent 时触发
      console.log("component update");
      let { next, bu, u, parent, vnode } = instance;
      let originNext = next;
      let vnodeHook: VNodeHook | null | undefined;

      if (next) {
        next.el = vnode.el;
        updateComponentPreRender(instance, next, optimized);
      } else {
        next = vnode;
      }

      // beforeUpdate hook
      if (bu) {
        invokeArrayFns(bu);
      }
      // onVnodeBeforeUpdate
      if ((vnodeHook = next.props && next.props.onVnodeBeforeUpdate)) {
        invokeVNodeHook(vnodeHook, parent, next, vnode);
      }

      //render
      const nextTree = renderComponentRoot(instance);
      const prevTree = instance.subTree;
      instance.subTree = nextTree;

      patch(
        prevTree,
        nextTree,
        // 如果在 teleport 中,parent 可能会发生改变
        hostParentNode(prevTree.el!)!,
        // anchor may have changed if it's in a fragment
        getNextHostNode(prevTree),
        instance,
        parentSuspense,
        isSVG
      );

      next.el = nextTree.el;
      if (originNext === null) {
        // self-triggered update. In case of HOC, update parent component
        // vnode el. HOC is indicated by parent instance's subTree pointing
        // to child component's vnode
        // TODO
      }

      // updated hook
      if (u) {
        queuePostRenderEffect(u, parentSuspense);
      }
      // onVnodeUpdated
      if ((vnodeHook = next.props && next.props.onVnodeUpdated)) {
        queuePostRenderEffect(() => {
          invokeVNodeHook(vnodeHook!, parent, next!, vnode);
        });
      }
    }
  },
  __DEV__
    ? // 提供 onTrack/onTrigger 选项执行 rtc&rtg 两个周期函数
      createDevEffectOptions(instance)
    : prodEffectOptions
);

和 updateComponentPreRender 实现这个函数让 instance.update 在 nextTick() 之后执 行 pre 优先于 post 和 job 任务(详情查看任务调度->):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const updateComponentPreRender = (
    instance: ComponentInternalInstance,
    nextVNode: VNode,
    optimized: boolean
  ) => {
    nextVNode.component = instance
    // const prevProps = instance.vnode.props
    instance.vnode = nextVNode
    instance.next = null
    // TODO update props
    // TODO update slots

    // props update may have triggered pre-flush watchers.
    // flush them before the render update.
    flushPreFlushCbs(undefined, instance.update)
  }

之前的用例再测试一遍:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
const { log, f, shuffle, runtime_test, renderChildren } = require(process.env
  .BLOG_DIR_VUE + "/lib.js");
import(process.env.BLOG_DIR_VUE + "/runtime-test.global.js").then(
  async ({ h, render, nodeOps, serializeInner: inner, ref, nextTick }) => {
    const root = nodeOps.createElement("div");
    const logRoot = () => log("root: " + inner(root));

    logRoot();
    const value = ref(true);
    let parentVnode, childVnode1, childVnode2;
    const idValue = ref("parent");

    const Parent = {
      render: () => {
        console.log("parent render");
        return (parentVnode = h("div", { id: idValue.value }, h(Child)));
      },
    };

    const Child = {
      render: () => {
        console.log("child render");
        return value.value
          ? (childVnode1 = h("div", "child 1"))
          : (childVnode2 = h("span", "child 2"));
      },
    };

    const p = h(Parent);
    render(p, root);
    logRoot();
    console.log("before change value");
    value.value = false;
    await nextTick();
    console.log("after change value");
    logRoot();

    console.log('before id change');
    idValue.value = 'parent-id'
    await nextTick()
    console.log('after id change');
    logRoot()
  },
  (err) => {
    console.log(err.message);
  }
);
undefinedroot:
component stateful ? 4
call setup
no setup
[Function: render] render
mount component
normalize vnode
parent render
patch component
component stateful ? 4
call setup
no setup
[Function: render] render
mount component
normalize vnode
child render
patch component
root: <div id="parent"><div>child 1</div></div>
before change value
component update
normalize vnode
child render
after change value
root: <div id="parent"><span>child 2</span></div>
before id change
component update
normalize vnode
parent render
after id change
root: <div id="parent"><span>child 2</span></div>

这里要让输出达到效果,需要将 resolve 改成 async function 并且要在 nextTick() 后 输出更新后的结果,因为 instance.update 调用了 flushPreFlushCbs(null, instane.update) 也就是说这个函数是个异步更新,且会在 nextTick() 后触发,详情 分析查看“任务调度机制分析

问题: 如上面的结果,当我们改变 idValue.value="parent-id" 的时候,实际结果并没 有改变?

答: 因为在 setupComponent() 中的 initProps() 以及 updateComponentPreRender() 中的 updateProps() 还没实现,下一节揭晓。

normalize props options

feat(add): normalize props options · gcclll/stb-vue-next@7d6ac55

对应官方文档内容: Props | Vue.js

这里作用简单描述就是,将 props 的定义在组件加载初始化时解析成具体的值,如: props: ['foo'] 解析成 foo={} 因为字符串数组的 props 会给每个属性初始化一个空 对象。

比如:

  1. 数组: props: ['foo', 'bar', 'foo-bar']

    转成 {foo: {}, bar: {}, fooBar: {}}

  2. 对象: props: { foo: [Boolean, String], bar: Function }

    表示 foo 可以是布尔值或字符串,bar 是个函数

    转换过程(0: BooleanFlags.shouldCast, 1: BooleanFlags.shouldCastTrue)

    foo = { type: [Boolean, String] } -> 找 Boolean

    foo = { type: [Boolean, String], 0: true } ->

    找 String 需满足 stringIndex < 0 || booleanIndex < stringIndex

    foo = { type: [Boolean, String], 0: true, 1: true }

    最后决定 foo 是不是应该进行 cast ? 条件是布尔类型或者有 default 默认值。

源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
export function normalizePropsOptions(
  comp: ConcreteComponent,
  appContext: AppContext,
  asMixin: false
): NormalizedPropsOptions {
  if (!appContext.deopt && comp.__props) {
    return comp.__props
  }

  const raw = comp.props
  const normalized: NormalizedPropsOptions[0] = {}
  const needCastKeys: NormalizedPropsOptions[1] = []

  // mixin/extends props 应用
  let hasExtends = false
  // 必须开支 2.x options api 支持,且不是函数式组件
  // 继承来的属性,用法: ~CompA = { extends: CompB, ... }~
  // CompA 会继承 CompB 的 props
  if (__FEATURE_OPTIONS_API__ && !isFunction(comp)) {
    const extendProps = (raw: ComponentOptions) => {
      hasExtends = true
      const [props, keys] = normalizePropsOptions(raw, appContext, true)
      extend(normalized, props)
      if (keys) {
        needCastKeys.push(...keys)
      }
    }

    // Comp: { extends: CompA } 处理
    if (comp.extends) {
      extendProps(comp.extends)
    }

    // Comp: { mixins: [mixin] } 处理
    if (!asMixin && appContext.mixins.length) {
      appContext.mixins.forEach(extendProps)
    }
  }

  // 既没有自身的 props 也没有 extends 继承来的 props 初始化为 []
  if (!raw && !hasExtends) {
    return (comp.__props = EMPTY_ARR as any)
  }

  if (isArray(raw)) {
    // 当 props 是数组的时候,必须是字符类型,如: props: ['foo', 'bar', 'foo-bar']
    // 'foo-bar' 会转成 'fooBar',不允许 '$xxx' 形式的变量名
    for (let i = 0; i < raw.length; i++) {
      const normalizedKey = camelize(raw[i])
      // 组件的属性名不能是以 $xx 开头的名称,这个是作为内部属性的
      if (validatePropName(normalizedKey)) {
        normalized[normalizedKey] = EMPTY_OBJ
      }
    }
  } else if (raw) {
    // 对象类型 props: { foo: 1, bar: 2, ... }
    for (const key in raw) {
      // 'foo-bar' -> 'fooBar'
      const normalizedKey = camelize(key)
      // 检查 $xxx 非法属性
      if (validatePropName(normalizedKey)) {
        const opt = raw[key]
        // ? 值为数组或函数变成: { type: opt } ?
        // 这里含义其实是: ~props: { foo: [Boolean, Function] }~
        // 可以用数组定义该属性可以是多种类型的其中一种
        const prop: NormalizedProp = (normalized[normalizedKey] =
          isArray(opt) || isFunction(opt) ? { type: opt } : opt)
        if (prop) {
          // 找到 Boolean 在 foo: [Boolean, Function] 中的索引
          const booleanIndex = getTypeIndex(Boolean, prop.type)
          const stringIndex = getTypeIndex(String, prop.type)
          prop[BooleanFlags.shouldCast] = booleanIndex > -1
          // [String, Boolean] 类型,String 在 Boolean 前面
          prop[BooleanFlags.shouldCastTrue] =
            stringIndex < 0 || booleanIndex < stringIndex
          // 如果是布尔类型的值或者有默认值的属性需要转换
          // 转换是根据 type 和 default 值处理
          // type非函数,default是函数,执行 default() 得到默认值
          if (booleanIndex > -1 || hasOwn(prop, 'default')) {
            needCastKeys.push(normalizedKey)
          }
        }
      }
    }
  }

  return (comp.__props = [normalized, needCastKeys])
}

然后这个处理之后的 props,会被保存到组件的 comp.__props=[normalied, needCastKeys] 上,而这个会在 resolvePropValue() 中进一步处理,这里的 needCastKeys 非常重要,它会决定最后的值应该如何被处理(resolvePropValue 中处 理)。

比如: { type: String, default: () => 'xxx' } 那么满足 type!==Function && isFunction(dfault) 则会直接执行 default() 得到属性默认值。

如果属性的 opt[BooleanFlags.shouldCast]true最开始的说明,其实就是 prop["0"] 的值,只要 prop 的类型中有 Boolean 这个值就是 true

此时需要将属性的值转成

  1. true : 类型声明中有 Boolean 且有 String 的时候,它的值如果是 '' 或者 key === value 情况下转成 true, 因为指定了可以是 String 类型,所以空字符 串是允许的。

  2. false : (!hasOwn(props, key) && !hasDefault), raw props 中没有这个属性且 没有 default 默认值的时候转成 false, 等于是假值类型。

component props

feat(add): init component props · gcclll/stb-vue-next@9a6aa70

新增代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// component.ts > setupComponent()
export function setupComponent(
  instance: ComponentInternalInstance,
  isSSR = false
) {
  // ...
  // init props & slots
  initProps(instance, props, isStateful, isSSR);
  // ...
  return setupResult;
}

componentProps.ts > initProps()

  1. def -> attrs.__vInterval = 1

  2. setFullProps 处理 rawProps 将结果反馈到 props 和 attrs

  3. 有状态组件?将 props reactive 化,SSR下不支持属性响应式其实就是服务器返回的属 性都是带有最终值的而不是在客户端动态能改变的

  4. 函数组件的 props 可选属性和必须属性?可选用 attrs 否则用 props

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
export function initProps(
  instance: ComponentInternalInstance,
  rawProps: Data | null,
  isStateful: number,
  isSSR = false
) {
  const props: Data = {};
  const attrs: Data = {};
  def(attrs, InternalObjectKey, 1);
  setFullProps(instance, rawProps, props, attrs);
  // TODO validation

  if (isStateful) {
    instance.props = isSSR ? props : shallowReactive(props);
  } else {
    if (!instance.type.props) {
      // functional optional props, props === attrs
      instance.props = attrs;
    } else {
      // functional declared props
      instance.props = props;
    }
  }
  instance.attrs = attrs;
}

componentProps.ts > setFullProps() 这个函数目的是将 rawProps 组件的 props 解析出来根据各自特性 分派到 props 或 attrs

  1. key, ref 属性不保留,因为组件更新时 key 可能发生改变,ref引用也会变好指向更新后的 DOM 元素

  2. options 啥意思?

  3. 事件属性(onClick)会存放到 attrs !

  4. needCastKeys ? 这是做啥呢 resolvePropValue?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
function setFullProps(
  instance: ComponentInternalInstance,
  rawProps: Data | null,
  props: Data,
  attrs: Data
) {
  const [options, needCastKeys] = instance.propsOptions;
  if (rawProps) {
    for (const key in rawProps) {
      const value = rawProps[key];
      // key, ref 保留,不往下传
      // 即这两个属性不会继承给 child
      if (isReservedProp(key)) {
        continue;
      }

      let camelKey;
      if (options && hasOwn(options, (camelKey = camelize(key)))) {
        props[camelKey] = value;
      } else if (!isEmitListener(instance.emitsOptions, key)) {
        attrs[key] = value;
      }
    }
  }

  if (needCastKeys) {
    const rawCurrentProps = toRaw(props);
    for (let i = 0; i < needCastKeys.length; i++) {
      const key = needCastKeys[i];
      props[key] = resolvePropValue(
        options!,
        rawCurrentProps,
        key,
        rawCurrentProps[key],
        instance
      );
    }
  }
}

componentProps.ts -> resolvePropValue()

  1. props:{name: {default: v=> myname }, type: String}

    当 type 非函数时,说明 name 是个字符串类型,但是它的 default 又是个函数? 那么这种情况会在这里被处理,最后将 name 的值赋值为 default(props) 执行之后的结果

  2. props:{name: {default: v=> myname }, type: Function}

    这种情况,说明 name 本身就是函数,不需要执行 default。

  3. props:{name: value, type: String|Number} 普通类型情况

  4. boolean 类型的值处理,最后都会转成 truefalse

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
function resolvePropValue(
  options: NormalizedProps,
  props: Data,
  key: string,
  value: unknown,
  instance: ComponentInternalInstance
) {
  /*
   * 这里面的处理是针对 props: { name: { ... } } 类型而言
   * 1. 默认值的处理, default 可能是函数或普通类型值,如果是函数应该得到
   * 函数执行的结果作为它的值,注意下面的检测函数时前置条件是该类型不是函数,
   * 如果类型也是函数,默认值就是该函数本身,而非执行后的结果值
   * 2. 布尔值的处理,值转成 true or false
   */
  const opt = options[key]
  if (opt != null) {
    const hasDefault = hasOwn(opt, 'default')
    // 默认值
    if (hasDefault && value === undefined) {
      const defaultValue = opt.default
      // props: { name: { default: (props) => 'xxx' } }
      // 类型不是函数?但是默认值是函数,执行得到结果
      if (opt.type !== Function && isFunction(defaultValue)) {
        setCurrentInstance(instance)
        value = defaultValue(props)
        setCurrentInstance(null)
      } else {
        // props: { name: { default: 'xxx' } }
        value = defaultValue
      }
    }
    // boolean casting
    if (opt[BooleanFlags.shouldCast]) {
      if (!hasOwn(props, key) && !hasDefault) {
        value = false
      } else if (
        opt[BooleanFlags.shouldCastTrue] &&
        (value === '' || value === hyphenate(key))
      ) {
        value = true
      }
    }
  }
  return value
}

❓ 然后与 props 有关的 propsOptions 是来自哪里?

回顾下 component render 过程:

patch -> switch default -> PatchFlags.COMPONENT ->

processComponent -> mountComponent ->

createComponentInstance -> setupComponent -> setupRenderEffect

有了?

是的,就是它 -> createComponentInstance 创建组件实例中,进行了初始化,其中组织 的结构里面就有一个

propsOptions: normalizePropsOptions(type, appContext)

emitsOptions: normalizeEmitsOptions(type, appContext)

component setup

  1. setup 如果返回值是函数直接是 render 函数

  2. setup 返回值是对象,则当做和 data 一样的组件状态处理

/img/vue3/runtime-core/vue-runtime-core-setup-result.jpg

更多分析见注释,相关代码:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
// 如果组件是个对象,而非函数是组件是会经过这个函数
function setupStatefulComponent(
  instance: ComponentInternalInstance,
  isSSR: boolean
) {
  const Component = instance.type as ComponentOptions;

  // 0. create render proxy property access cache
  // 这个是针对 instance 上属性的 get 操作类型进行了 key 值缓存
  // 比如:当你对 setupState 或 data的属性 进行了 get 访问,
  // 那么该属性的key值会记录为该类型(accessCache[key]=AccessTypes.SETUP)
  // 当你下次再在 instance 上访问这个key 的时候,那么这个时候就会知道这个 key
  // 是在 setupState 上,那么就直接返回 setupState[key] 就行了
  // 而不用去重复进行 if...elseif...else 去 setupData, data, context
  // 或 props 判断然后决定去哪个上面取值,加快求值速度。
  // 如: setupState={foo:1}, data={bar:2}
  // 取值: this.foo 触发 get 操作,这个时候第一次取值的时候会进行
  // if setupState else if data 检测'foo'在哪个对象上,发现在
  // setupState 上,然后将 'foo' 缓存到 accessCache['foo'] ='setup'
  // 下次再次取值this.foo,那么本次就会直接返回 setupState['foo']
  instance.accessCache = Object.create(null);

  // 1. create public instance / render proxy
  // also mark it raw so it's never observed
  // 代理目的:让取值操作能在 setupState, data, ctx, props 及
  // appContext.config.globalProperties 上依次查找对应的属性值
  // 优先级:
  // 1. 非 $xxx 属性, setupState > data > ctx > props
  // 2. this.$xxx 取值, public 属性: $,$el,$data,$props,$attrs
  //  ,$slots,$refs,$parent,$root,$emit,$options,$forceUpdate,
  //  ,$nextTick,$watch
  // > cssModule 属性 vue-loader 注入的css 变量
  // > instance.ctx
  // > appContext.config.globalProperties, 如: this.$router
  instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers);

  console.log("call setup");
  // 2. call setup()
  const { setup } = Component;
  if (setup) {
    // 传递给 setup(props, setupContext) 的第二个参数
    // setupContext: { attrs, slots, emit, expose }
    const setupContext = (instance.setupContext =
      setup.length > 1 ? createSetupContext(instance) : null);

    currentInstance = instance;
    // 实例初始化期间,禁止 track 操作,get 收集依赖
    pauseTracking();
    // 执行 setup 函数
    const setupResult = callWithErrorHandling(
      setup,
      instance,
      ErrorCodes.SETUP_FUNCTION,
      [__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
    );
    resetTracking();
    currentInstance = null;

    // 对setup 结果处理,返回值只能是对象或函数
    if (isPromise(setupResult)) {
      if (isSSR) {
        // return the promise so server-renderer can wait on it
        return setupResult.then((resolvedResult: unknown) => {
          handleSetupResult(instance, resolvedResult, isSSR);
        });
      } else if (__FEATURE_SUSPENSE__) {
        // async setup returned Promise.
        // bail here and wait for re-entry.

        instance.asyncDep = setupResult;
      } else if (__DEV__) {
        // TODO warn
      }
    } else {
      // setup() 执行结果只能是函数或对象
      // 1. 如果是对象,返回对象的所有属性当做状态处理,和 data 性质相同
      // 2. 如果是函数,视为组件的 render 函数
      // 即,支持在 setup 中直接手写 render 函数
      handleSetupResult(instance, setupResult, isSSR);
    }
  } else {
    // ...
  }
}

// handleSetupResult
export function handleSetupResult(
  instance: ComponentInternalInstance,
  setupResult: unknown,
  isSSR: boolean
) {
  // 1. 如果是函数当做render函数处理
  // 2. 如果是对象
  if (isFunction(setupResult)) {
    // 返回内联 render 函数
    if (__NODE_JS__ && (instance.type as ComponentOptions).__ssrInlineRender) {
      // SSR 服务端渲染,替换 ssrRender 函数
      // when the function's name is `ssrRender` (compiled by SFC inline mode),
      // set it as ssrRender instead.
      instance.ssrRender = setupResult;
    } else {
      instance.render = setupResult as InternalRenderFunction;
    }
  } else if (isObject(setupResult)) {
    // 返回 bindings,这些变量可以直接在模板中使用
    // 注意这里的 state 是 shallow ref,即非递归 reactive 的
    instance.setupState = proxyRefs(setupResult);
  } else {
    // warn 必须返回对象
  }
  // 最后完成render函数检查
  // 可能是 SFC情况的 模板语法,没有直接的render函数,需要进行
  // compile 操作生成 instance.rendder = Component.render函数
  // render 执行不是这里,而是在 instance.update 的 effect 函数中的
  // renderComponentRoot 中
  finishComponentSetup(instance, isSSR);
}

测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
const { log, f, shuffle, runtime_test, renderChildren } = require(process.env
  .BLOG_DIR_VUE + "/lib.js");
import(process.env.BLOG_DIR_VUE + "/runtime-test.global.js").then(
  async ({
    h,
    render,
    nodeOps,
    serializeInner: inner,
    ref,
    nextTick,
    defineComponent,
  }) => {
    const root = nodeOps.createElement("div");
    const logRoot = () => log("root: " + inner(root));

    logRoot();
    log(">>>component setup return object");
    let props, attrs;
    try {
      const Comp = defineComponent({
        props: ["bar"],
        setup(_props, { attrs: _attrs }) {
          console.log("setup...");
          return () => {
            props = _props;
            attrs = _attrs;
          };
        },
      });
      render(h(Comp, { foo: 1, bar: 2 }), root);
      log([props, attrs]);
      render(h(Comp, { fooBar: 2, bar: 3, fooBaz: 4 }), root);
      log([props, attrs]);
      render(h(Comp, { qux: 5 }), root);
      log([props, attrs]);
    } catch (e) {
      log(e);
    }

    logRoot();
  },
  (err) => {
    console.log(err.message);
  }
);
undefinedroot:
>>>component setup return object
component stateful ? 4
call setup
setup...
[Function (anonymous)] render
mount component
normalize vnode
patch component
{ bar: 2 } { foo: 1 }
{ bar: 2 } { foo: 1 }
{ bar: 2 } { foo: 1 }
root:

component update

需要修改点:

  1. processComponent 中增加 updateComponent 更新组件

  2. 在 instance.update effect 函数中增加 updateProps() diff->update props

这里主要包含了 props 的更新规则,对于 children 的 diff 和 update 规则分析可以查 看 patchKeyedChildren diff 和 更新原理分析!

组件更新,代码执行流程:

状态变更 -> instance.update effect 执行 ->

如果有 next vnode 触发 updateComponentPreRender() 更新 props 和 slots

执行 beforeUpdate hook

执行 onVnodeBeforeUpdate hook

得到新树🌲 nextTree = renderComponentRoot(instance)

老树🌲 prevTree = instance.subTree

进行 patch(prevTree, nextTree) 操作

执行 updated hook 和 onVnodeUpdated hook

测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
const { log, f, shuffle, runtime_test, renderChildren } = require(process.env
  .BLOG_DIR_VUE + "/lib.js");
import(process.env.BLOG_DIR_VUE + "/runtime-test.global.js").then(
  async ({
    h,
    render,
    nodeOps,
    serializeInner: inner,
    ref,
    nextTick,
    defineComponent,
  }) => {
    const root = nodeOps.createElement("div");
    const logRoot = () => log("root: " + inner(root));
    let oa = { a: 1 },
      ob = { b: 1 },
      i = 0,
      j = 0;
    const defaultFn = () => (console.log(`fn called ${++i}`), oa);
    const defaultBaz = () => (console.log(`baz called ${++j}`), ob);

    let proxy;
    logRoot();
    try {
      const Comp = {
        props: {
          foo: { default: 1 },
          bar: { default: defaultFn },
          baz: { type: Function, default: defaultBaz },
        },
        render() {
          proxy = this;
        },
      };
      const print = (s) => {
        log(">>> " + s);
        const prevBar = proxy.bar;
        log("proxy.foo = " + proxy.foo);
        // 因为无 type,而 default 是个函数,会被执行得到结果
        log("prevBar === oa: " + (prevBar === oa));
        // 因为 type Function ,所以default 是 Function 的话不会被执行
        log("proxy.baz === defaultBaz, " + (proxy.baz === defaultBaz));
        log("proxy.bar === prevBar, " + (proxy.bar === prevBar));
      };
      render(h(Comp, { foo: 2 }), root);
      print("first");
      // update
      render(h(Comp, { foo: 3 }), root);
      print("update");
    } catch (e) {
      log(e.message);
    }
  },
  (err) => {
    console.log(err.message);
  }
);
undefinedroot:
{
  type: {
    props: { foo: [Object], bar: [Object], baz: [Object] },
    render: [Function: render]
  },
  shapeFlag: 4
}
fn called 1
component stateful ? 4
call setup
no setup
[Function: render] render
mount component
update effect
normalize vnode
patch component
{ type: Symbol(Comment), shapeFlag: 0 }
>>> first
proxy.foo = 2
prevBar === oa: true
proxy.baz === defaultBaz, true
proxy.bar === prevBar, true
{
  type: {
    props: { foo: [Object], bar: [Object], baz: [Object] },
    render: [Function: render],
    __props: [ [Object], [Array] ]
  },
  shapeFlag: 4
}
update component
should update component
has changed props
should update component....
normal update
update effect
component update
update comp pre render
normalize vnode
Cannot read property 'parentNode' of null

❓ 没有触发 instance.update ?

fix: props update invalid · gcclll/stb-vue-next@3771bfb

修复后,回去重新测试。

FIX 增加代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const updateComponent = (n1: VNode, n2: VNode, optimized: boolean) => {
  console.log("update component");
  const instance = (n2.component = n1.component)!;
  if (shouldUpdateComponent(n1, n2, optimized)) {
    console.log("should update component....");
    if (
      __FEATURE_SUSPENSE__ &&
      instance.asyncDep && // async setup
      instance.asyncResolved
    ) {
      // ...
      return;
    } else {
      // 新增代码》》》》》》》》
      // 正常更新
      instance.next = n2;
      // 考虑到 child 组件可能正在队列中排队,移除它避免
      // 在同一个 flush tick 重复更新同一个子组件
      // 当下一次更新来到时,之前的一次更新取消?
      invalidateJob(instance.update);
      // instance.update 是在 setupRenderEffect 中
      // 定义的一个 reactive effect runner
      // 主动触发更新
      instance.update();
    }
    return;
  } else {
    // ...
  }
};

❓ Cannot read property 'parentNode' of null

这个报错发生在 instance.update effect 的 else 更新组件中,

patch(… hostParentNode(prevTree.el!)!, …)

的时候,去取值 prevTree.el 得到的是空值,进入 hostParentNode 调用 node.parentNode 报错的。

这里为什么 prevTree.el 是 null ? 更新的话之前的 node 不应该已经加载好了吗?

component slots

feat(add): init&update slots · gcclll/stb-vue-next@a788430

修改点:

  1. 初始化, setupComponent() 中的 initSlots()

  2. updateComponent() -> updateComponentPreRender()updateSlots() 更新 slots

对应动作: init -> update

对应组件阶段: 初始化(initSlots()) -> 更新(updateSlots())

初始化(initSlots()):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
export const initSlots = (
  instance: ComponentInternalInstance,
  children: VNodeNormalizedChildren
) => {
  if (instance.vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN) {
    const type = (children as RawSlots)._
    if (type) {
      instance.slots = children as InternalSlots
      // make compiler marker non-enumerable
      def(children as InternalSlots, '_', type)
    } else {
      normalizeObjectSlots(children as RawSlots, (instance.slots = {}))
    }
  } else {
    instance.slots = {}
    if (children) {
      normalizeVNodeSlots(instance, children)
    }
  }
  def(instance.slots, InternalObjectKey, 1)
}

要分析整个,需要回顾下 normalizeChildren(vnode, children) 处理逻辑,要搞清楚什么 情况下会是 SLOTS_CHILDREN

根据 normalizeChildren() 的实现中,可知需要满足下面几个条件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
if (isObject(vnode.children)) {
  if (isElement(vnode.shapeFlag) || isTELEPORT(vnode.shapeFlag)) {
    // default slot
  } else {
    // 非 ELEMENT 或 TELEPORT 类型
    // 如: <Comp><template v-slot:named><div/></template></Comp>
    // children 只有一个 template 会被解析成一个 vnode 对象
    // 且 vnode type 是 template
    type = ShapeFlags.SLOTS_CHILDREN;
  }
} else if (isFunction(vnode.children)) {
  // children 是个函数
  // 函数式组件 Comp = { render() {
  //   return h('div', null, () => h('div') /* slot */)
  // }}
  type = ShapeFlags.SLOTS_CHILDREN;
}
  1. children = { _: ... } 内部插槽?

  2. normalizeObjectSlots: children 是对象类型:

    {named: slotFn1, default: slotFn2 }

    遍历所有 key-value =>

    (推荐)如果 value 是函数需要将 slotFn 用 withCtx 封装一层,让其在当前实例的上下文中正确✅执行。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
     const normalizeSlot = (
       key: string,
       rawSlot: Function,
       ctx: ComponentInternalInstance | null | undefined
     ): Slot =>
       withCtx((props: any) => {
         // warn: 在 Render 函数外执行了 slot function
         return normalizeSlotValue(rawSlot(props));
       }, ctx);
    

    (不推荐)如果 value 不是函数,经过

    1
    2
    3
    4
    
    const normalizeSlotValue = (value: unknown): VNode[] =>
    isArray(value)
        ? value.map(normalizeVNode)
        : [normalizeVNode(value as VNodeChild)]
    

    处理之后转成函数赋值 slots[key] = () => normalized

    最终都是将 slot value 转成一个函数保存到 instance.slots{}

  3. SLOTS_CHILDREN ,那只有一种情况

    children 中没有 <template v-slot:named ...> ,此时它所有的 child 都会被当做 默认插槽来处理。

    1
    2
    3
    4
    5
    6
    7
    
    const normalizeVNodeSlots = (
      instance: ComponentInternalInstance,
      children: VNodeNormalizedChildren
    ) => {
      const normalized = normalizeSlotValue(children);
      instance.slots.default = () => normalized;
    };
    

    如:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    const { log, shuffle, runtime_test, renderChildren } = require(process.env
      .BLOG_DIR_VUE + "/lib.js");
    import(process.env.BLOG_DIR_VUE + "/runtime-test.global.js").then(
      async ({ h, createVNode: c }) => {
        log.br();
        const Comp = { template: "<div/>" };
        const slot = () => {};
        const node = h(Comp, slot);
        log(">>> 函数作为 children 解析为默认插槽");
        log.f(node, ["children", "type"]);
        log(node.children);
      }
    );
    
    undefined
    
    >>> 函数作为 children 解析为默认插槽
    {
      type: { template: '<div/>' },
      children: { default: [Function: slot], _ctx: null }
    }
    { default: [Function: slot], _ctx: null }
    

更新(updateSlots())

更新插槽步骤:

  1. 合并 instance.slots 和 children

  2. 然后删除 children 中没有的插槽

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
export const updateSlots = (
  instance: ComponentInternalInstance,
  children: VNodeNormalizedChildren
) => {
  const { vnode, slots } = instance;
  let needDeletionCheck = true;
  let deletionComparisonTarget = EMPTY_OBJ;
  console.log("update slots");
  // children 是 函数或对象类型(非数组)
  if (vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN) {
    const type = (children as RawSlots)._;
    if (type) {
      console.log("update slots type");
      // compiled slots.
      if (__DEV__ && isHmrUpdating) {
        // TODO
      } else if (type === SlotFlags.STABLE) {
        // compiled AND stable
        // 不需要更新,跳过 slots 删除操作
        needDeletionCheck = false;
      } else {
        // compiled but dynamic (v-if/v-for on slots)
        // update slots, but skip normalization
        extend(slots, children as Slots);
      }
    } else {
      console.log("update slots no type");
      needDeletionCheck = !(children as RawSlots).$stable;
      normalizeObjectSlots(children as RawSlots, slots);
    }
    // 对象类型直接合并,这里记录需要进行删除操作的对象,children
    // 上面只是进行了简单的对象合并操作
    // 如: slots={a,b,d}, children = {a,b,c}
    // 合并之后: slots={a,b,c,d},后面需要删除的是 d 这个插槽
    deletionComparisonTarget = children as RawSlots;
  } else if (children) {
    // <Comp>...这里没有 <template #named ...> 情况</Comp>
    // <Comp> 里面的所有内容都会被当做默认插槽来解析
    console.log("update slots children");
    // non slot object children (direct value)
    // passed to a component
    // 当做默认插槽来处理,解析后: slots.default = () => normalized
    normalizeVNodeSlots(instance, children);
    // 这里目的是为了只保留 default 其他都需要删除
    deletionComparisonTarget = { default: 1 };
  }

  console.log({ needDeletionCheck });
  // delete stale slots
  // 删除旧的 slots
  if (needDeletionCheck) {
    for (const key in slots) {
      // 非 `_` 内部插槽,且不再新的 children 中的
      if (!isInternalKey(key) && !(key in deletionComparisonTarget)) {
        delete slots[key];
      }
    }
  }
};

props tests

传入的 rawProps 和组件自身的 props 经过处理之后(setFullProps()) 会将 rawProps 根 据一定规则分派到组件 props 或 attrs 中去。

这里的 rawProps 代表是 parent 在渲染子组件的时候传递给它的 props ,如:

render(h(Child, { foo:1, bar:2}),root)

中的 {foo:1,bar:2} 即 parent props,然后组件可以定义自身的 props 属性:

defineComponent({ props: ['foo'] }) 意味着,该子组件只接受 'foo' 作为 props 而其他的会被解析成 attrs 。

component props 测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
const { log, f, shuffle, runtime_test, renderChildren } = require(process.env
  .BLOG_DIR_VUE + "/lib.js");
import(process.env.BLOG_DIR_VUE + "/runtime-test.global.js").then(
  async ({
    h,
    render,
    nodeOps,
    serializeInner: inner,
    ref,
    nextTick,
    defineComponent,
  }) => {
    const root = nodeOps.createElement("div");
    const logRoot = () => log("root: " + inner(root));

    logRoot();
    log(">>>stateful");
    let props, attrs, proxy;
    try {
      const Comp = defineComponent({
        props: ["fooBar", "barBaz", "foo-baz"],
        render() {
          console.log("comp render");
          props = this.$props;
          attrs = this.$attrs;
          proxy = this;
        },
      });

      render(h(Comp, { fooBar: 1, bar: 2, fooBaz: 3 }), root);
    } catch (e) {
      log(e);
    }

    console.log("proxy.fooBar=" + proxy.fooBar);
    log([props, attrs]);
    logRoot();
  },
  (err) => {
    console.log(err.message);
  }
);
undefinedroot:
>>>stateful
{
  type: {
    props: [ 'fooBar', 'barBaz', 'foo-baz' ],
    render: [Function: render]
  },
  shapeFlag: 4
}
component stateful ? 4
call setup
no setup
[Function: render] render
mount component
update effect
normalize vnode
comp render
patch component
{ type: Symbol(Comment), shapeFlag: 0 }
proxy.fooBar=1
{ fooBar: 1, fooBaz: 3 } { bar: 2 }
root:

component unmount

feat(add): add unmount component · gcclll/stb-vue-next@79c5061

主要工作:

  1. 执行 beforeUnmount 周期函数

  2. 停掉所有 effects 依赖

  3. 检查 update 函数,处理在异步 update 之前执行了 unmount

  4. 在 post queue 中执行 unmounted 周期函数

  5. 在 post queue 中标记 instance.isUnmounted=true 标记组件已经卸载了

三种队列任务, pre, post, job 执行顺序: pre > job > post,详情查看

scheduler 任务调度机制

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const unmountComponent = (
  instance: ComponentInternalInstance,
  parentSuspense: SuspenseBoundary | null,
  doRemove?: boolean
) => {
  const { bum, effects, update, subTree, um } = instance;
  // beforeUnmount hook
  if (bum) {
    invokeArrayFns(bum);
  }
  if (effects) {
    for (let i = 0; i < effects.length; i++) {
      stop(effects[i]);
    }
  }

  // update may be null if a component is unmounted before its async
  // setup has resolved.
  if (update) {
    stop(update);
    unmount(subTree, instance, parentSuspense, doRemove);
  }

  // unmounted hook
  if (um) {
    queuePostRenderEffect(um, parentSuspense);
  }
  queuePostRenderEffect(() => {
    instance.isUnmounted = true;
  }, parentSuspense);

  // TODO suspense
};

问题

TypeError: Cannot read property 'allowRecurse' of null

TypeError: Cannot read property 'allowRecurse' of null
    at createReactiveEffect (/Users/simon/blog/cheng92.com/static/js/vue/runtime-test.global.js:251:39)
    at effect (/Users/simon/blog/cheng92.com/static/js/vue/runtime-test.global.js:199:22)
    at setupRenderEffect (/Users/simon/blog/cheng92.com/static/js/vue/runtime-test.global.js:2738:29)
    at mountComponent (/Users/simon/blog/cheng92.com/static/js/vue/runtime-test.global.js:2733:11)
    at processComponent (/Users/simon/blog/cheng92.com/static/js/vue/runtime-test.global.js:2724:19)
    at patch (/Users/simon/blog/cheng92.com/static/js/vue/runtime-test.global.js:2616:23)
    at render (/Users/simon/blog/cheng92.com/static/js/vue/runtime-test.global.js:3099:15)
    at /private/var/folders/1n/xw58p9v90tn42m87q527fvgr0000gn/T/babel-orafVD/js-script-Vmw0ga:29:5

因为实现问题:

1
2
3
4
5
6
7
8
9
instance.update = effect(function componentEffect() {
      // 监听更新
      if (!instance.isMounted) {
        // 还没加载完成,可能是第一次 mount 操作
        // TODO
      } else {
        // TODO
      }
    }, __DEV__ ? /* TODO */ (null as any) : prodEffectOptions)

文字内的测试是基于 node development 环境测试的,这里 effect options 是 null 所以 报错。

fix: effect null options · gcclll/stb-vue-next@63675a4

processText|Comment|Static

feat(add): processText updte · gcclll/stb-vue-next@636e870 · GitHub

本节包含(主要源码,没啥好分析的):

  1. 文本节点

  2. 注释节点

  3. 静态节点

Text

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const processText: ProcessTextOrCommentFn = (n1, n2, container, anchor) => {
  if (n1 == null /* old */) {
    // 新节点,插入处理
    hostInsert(
      (n2.el = hostCreateText(n2.children as string)),
      container,
      anchor
    );
  } else {
    // has old vnode, need to diff
    const el = (n2.el = n1.el!);
    if (n2.children !== n1.children) {
      hostSetText(el, n2.children as string);
    }
  }
};

因为在 compiler-core parse 阶段的文本处理中,如果是响铃的文本节点会被合并,如:

<div>{{ text1 }} {{ text2 }}</div> 最终会合并:

<div>{{ text1 + ' ' + text2 }}</div> 最终替换的是 <div/> 整个内容。

Comment

feat(add): process comment node · gcclll/stb-vue-next@4489366 · GitHub

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
const processCommentNode: ProcessTextOrCommentFn = (
    n1,
    n2,
    container,
    anchor
  ) => {
    if (n1 == null) {
      hostInsert(
        (n2.el = hostCreateComment((n2.children as string) || '')),
        container,
        anchor
      )
    } else {
      // there's no support for dynamic comments
      n2.el = n1.el
    }
  }

Static

patch -> case Static:

1
2
3
4
5
6
7
// case Static:
if (n1 == null) {
  mountStaticNode(n2, container, anchor, isSVG);
} else if (__DEV__) {
  patchStaticNode(n1, n2, container, isSVG);
}
// break

没有 old vnode -> mount

有 old node -> patch

mount:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const mountStaticNode = (
  n2: VNode,
  container: RendererElement,
  anchor: RendererNode | null,
  isSVG: boolean
) => {
  // static nodes are only present when used with compiler-dom/runtime-dom
  // which guarantees presence of hostInsertStaticContent.
  [n2.el, n2.anchor] = hostInsertStaticContent!(
    n2.children as string,
    container,
    anchor,
    isSVG
  );
};

mount 时用到的 hostInsertStaticContent() 是在 runtime-dom 包中实现的,先预览下 代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
function insertStaticContent(content, parent, anchor, isSVG) {
  const temp = isSVG
    ? tempSVGContainer || (tempSVGContainer = doc.createElementNS(svgNS, "svg"))
    : tempContainer || (tempContainer = doc.createElement("div"));
  temp.innerHTML = content;
  const first = temp.firstChild as Element;
  let node: Element | null = first;
  let last: Element = node;
  while (node) {
    last = node;
    nodeOps.insert(node, parent, anchor);
    node = temp.firstChild as Element;
  }
  return [first, last];
}

可以看到 temp.innerHTML = content 一个简单的内容全替换操作。

patchStaticNode: 因为静态节点在生产环境中会被提升,重用,因此不存在 patch 阶段。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const patchStaticNode = (
  n1: VNode,
  n2: VNode,
  container: RendererElement,
  isSVG: boolean
) => {
  // static nodes are only patched during dev for HMR
  if (n2.children !== n1.children) {
    const anchor = hostNextSibling(n1.anchor!);
    // remove existing
    removeStaticNode(n1);
    // insert new
    [n2.el, n2.anchor] = hostInsertStaticContent!(
      n2.children as string,
      container,
      anchor,
      isSVG
    );
  } else {
    n2.el = n1.el;
    n2.anchor = n1.anchor;
  }
};

moveStaticNode: 在 diff -> update 阶段 move() 中触发

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const moveStaticNode = (
  { el, anchor }: VNode,
  container: RendererElement,
  nextSibling: RendererNode | null
) => {
  let next;
  while (el && el !== anchor) {
    next = hostNextSibling(el);
    hostInsert(el, container, nextSibling);
    el = next;
  }
  hostInsert(anchor!, container, nextSibling);
};

removeStaticNode: remove() 中触发

1
2
3
4
5
6
7
8
9
const removeStaticNode = ({ el, anchor }: VNode) => {
  let next;
  while (el && el !== anchor) {
    next = hostNextSibling(el);
    hostRemove(el);
    el = next;
  }
  hostRemove(anchor!);
};

processFragment

Fragment 的情况: children 有多个 child 的时候,会用一个 fragment 事先包起来。

/img/vue3/runtime-core/vue-runtime-core-render-fragment.svg

STABLE_FRAGMENT 情况:

  1. v-if

    首先要满足 children.length !== 1 即有一个以上的 children, 如:

    <div><p/><p/></div>

    或者非第一个 child ELEMENT 类型,如:

    <div><Comp/></div>

    其要满足 (children.length === 1 && firstChild.type === NodeTypes.FOR) 如:

    <div v-for="item in list"><p/></div>

    才会被当做 PatchFlags.STABLE_FRAGMENT

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
     // vIf.ts
     const needFragmentWrapper =
       children.length !== 1 || firstChild.type !== NodeTypes.ELEMENT;
     if (needFragmentWrapper) {
       if (children.length === 1 && firstChild.type === NodeTypes.FOR) {
         // ...
       } else {
         return createVNodeCall(
           // ...
           PatchFlags.STABLE_FRAGMENT +
             (__DEV__ ? ` /* ${PatchFlagNames[PatchFlags.STABLE_FRAGMENT]} */` : ``),
           // ...
         );
       }
     }
    
  2. v-for

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
     // vFor.ts
     const isStableFragment =
         forNode.source.type === NodeTypes.SIMPLE_EXPRESSION &&
         forNode.source.constType > 0
       const fragmentFlag = isStableFragment
         ? PatchFlags.STABLE_FRAGMENT
         : keyProp
           ? PatchFlags.KEYED_FRAGMENT
           : PatchFlags.UNKEYED_FRAGMENT
    

源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
const processFragment = (
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    optimized: boolean
  ) => {
    const fragmentStartAnchor = (n2.el = n1 ? n1.el : hostCreateText(''))!
    const fragmentEndAnchor = (n2.anchor = n1 ? n1.anchor : hostCreateText(''))!

    let { patchFlag, dynamicChildren } = n2
    if (patchFlag > 0) {
      optimized = true
    }

    if (__DEV__ && isHmrUpdating) {
      // HMR updated, force full diff
      patchFlag = 0
      optimized = false
      dynamicChildren = null
    }

    if (n1 == null) {
      hostInsert(fragmentStartAnchor, container, anchor)
      hostInsert(fragmentEndAnchor, container, anchor)
      // fragment 的 children 只会是 array children
      // 因为他们要么是通过 compiler 生成的,要么是由数组创建的
      mountChildren(
        n2.children as VNodeArrayChildren,
        container,
        fragmentEndAnchor,
        parentComponent,
        parentSuspense,
        isSVG,
        optimized
      )
    } else {
      if (
        patchFlag > 0 &&
        patchFlag & PatchFlags.STABLE_FRAGMENT &&
        dynamicChildren &&
        // #2715 the previous fragment could've been a BAILed one as a result
        // of renderSlot() with no valid children
        n1.dynamicChildren
      ) {
        // a stable fragment (template root or <template v-for>) doesn't need to
        // patch children order, but it may contain dynamicChildren.
        patchBlockChildren(
          n1.dynamicChildren,
          dynamicChildren,
          container,
          parentComponent,
          parentSuspense,
          isSVG
        )
        if (__DEV__ && parentComponent && parentComponent.type.__hmrId) {
          traverseStaticChildren(n1, n2)
        } else if (
          // #2080 if the stable fragment has a key, it's a <template v-for> that may
          //  get moved around. Make sure all root level vnodes inherit el.
          // #2134 or if it's a component root, it may also get moved around
          // as the component is being moved.
          n2.key != null ||
          (parentComponent && n2 === parentComponent.subTree)
        ) {
          traverseStaticChildren(n1, n2, true /* shallow */)
        }
      } else {
        // keyed / unkeyed, or manual fragments.
        // for keyed & unkeyed, since they are compiler generated from v-for,
        // each child is guaranteed to be a block so the fragment will never
        // have dynamicChildren.
        patchChildren(
          n1,
          n2,
          container,
          fragmentEndAnchor,
          parentComponent,
          parentSuspense,
          isSVG,
          optimized
        )
      }
    }
  }

TELEPORT

feat(init): Teleport · gcclll/stb-vue-next@0fcfa32 · GitHub

/img/vue3/runtime-core/vue-runtime-core-render-teleport.svg

新增代码:

TeleportImpl: 组件模板

1
2
3
4
5
6
7
export const TeleportImpl = {
  __isTeleport: true,
  process() {},
  remove() {},
  move: moveTeleport,
  hydrate: hydrateTeleport
}

resolveTarget: 根据选择器找到目标元素

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
const resolveTarget = <T = RendererElement>(
  props: TeleportProps | null,
  select: RendererOptions['querySelector']
): T | null => {
  const targetSelector = props && props.to
  if (isString(targetSelector)) {
    if (!select) {
      // 无效选择器
      return null
    } else {
      const target = select(targetSelector)
      // Teleport 设置失败
      return target as any
    }
  } else {
    // 无效的 Teleport 目标
    return targetSelector as any
  }
}

moveTeleport: 执行移动

1
2
3
4
5
6
7
8
9
function moveTeleport(
  vnode: VNode,
  container: RendererElement,
  parentAnchor: RendererNode | null,
  { o: { insert }, m: move }: RendererInternals,
  moveType: TeleportMoveTypes = TeleportMoveTypes.REORDER
) {
  // TODO
}

hydrateTeleport:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
function hydrateTeleport(
  node: Node,
  vnode: TeleportVNode,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  optimized: boolean,
  {
    o: { nextSibling, parentNode, querySelector }
  }: RendererInternals<Node, Element>,
  hydrateChildren: (
    node: Node | null,
    vnode: VNode,
    container: Element,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    optimized: boolean
  ) => Node | null
): Node | null {
  return vnode.anchor && nextSibling(vnode.anchor as Node)
}

导出组件 ~Teleport~:

1
2
3
4
// Force-casted public typing for h and TSX props inference
export const Teleport = (TeleportImpl as any) as {
  __isTeleport: true
  new (): { $props: VNodeProps & TeleportProps }

process()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
function process(/*省略参数*/) {
  // ...

  const disabled = isTeleportDisabled(n2.props);
  const { shapeFlag, children } = n2;

  if (n1 == null) {
    // insert anchors in the main view
    // <container><placeholder/><anchor/></container>
    insert(placeholder, container, anchor);
    // <container><main-anchor/><anchor/></container>
    insert(mainAnchor, container, anchor);
    // 根据选择器 <Teleport to="selector"/> selector
    // 找到目标 DOM 元素
    const target = (n2.target = resolveTarget(n2.props, querySelector));
    // <target><!-- '' --></target>,用来作为插入时的参考节点
    const targetAnchor = (n2.targetAnchor = createText(""));
    if (target) {
      insert(targetAnchor, target);
      // #2652 we could be teleporting from a non-SVG tree into an SVG tree
      isSVG = isSVG || isTargetSVG(target);
    } /* else if warn ... */

    const mount = (container: RendererElement, anchor: RendererNode) => {
      // 将 vnode children 渲染到 target 元素内
      // 会插入到 anchor 的前面,如: ~<target><children/><!--''--></target>~
    };

    if (disabled) {
      // 失效状态,不直接渲染到目标元素中,而是挂在了 #app 内对应的
      // 节点里面,等待状态 enable 再渲染回 target 元素
      mount(container, mainAnchor);
    } else if (target) {
      // 直接渲染进目标元素
      mount(target, targetAnchor);
    }
  } else {
    // update content
    // 非首次渲染
    n2.el = n1.el;
    // 已经渲染到 tar

    if (n2.dynamicChildren) {
      // 动态子节点 patch
    } else if (!optimized) {
      // patch n1|n2 children
    }

    if (disabled) {
      // n2 new teleport disabled -> n1 old target enabled
      // n2 直接移到 #app 结构中的 container 上,暂时不直接渲染到
      // 目标元素上
      if (!wasDisabled) {
        // moveTeleport
      }
    } else {
      // target changed
      // teleport 的 to 属性值发生了变化,找到新的目标
      // 进行移动
      if ((n2.props && n2.props.to) !== (n1.props && n1.props.to)) {
        // 1. 找新目标
        // 2. 将 n2 移动到新的目标中
        // ...
      } else if (wasDisabled) {
        // 状态变更
        // disabled -> enabled
        // move into teleport target
        // 从 container 中将 n2 移到目标元素中
      }
    }
  }
}

对于 teleport 的 mount 和 update 两个共同点(也是重点):

  1. 当 new teleport 是 disabled 时,不直接渲染到目标元素中,而是挂在当前 container 中待用

  2. 当 new teleport 状态 enabled 时,不论 old 什么状态,都会讲新的 teleport children 渲染到目标元素下面。

Teleport 的移动类型有:

  1. TARGET_CHANGE 目标发生了变化, teleport 的 to 属性变化

    1
    2
    3
    4
    
    // move target anchor if this is a target change.
    if (moveType === TeleportMoveTypes.TARGET_CHANGE) {
      insert(vnode.targetAnchor!, container, parentAnchor);
    }
    
  2. TOGGLE 状态发生了变化 enable -> disable 或 disable -> enable

  3. REORDER 目标元素内进行重新排序 ?

    1
    2
    3
    4
    
    // move main view anchor if this is a re-order.
    if (isReorder) {
      insert(anchor!, container, parentAnchor);
    }
    

TODO 测试

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const { log, f, shuffle, runtime_test, renderChildren } = require(process.env
  .BLOG_DIR_VUE + "/lib.js");
import(process.env.BLOG_DIR_VUE + "/runtime-test.global.js").then(
  ({ h, render, Teleport, nodeOps, serializeInner: inner, ref }) => {
    const target = nodeOps.createElement("div");
    const root = nodeOps.createElement("div");

    try {
      render(
        h(() => [
          h(Teleport, { to: target }, h("div", "teleported")),
          h("div", "root"),
        ]),
        root
      );
    } catch (e) {
      console.log(e.message);
    }

    log([">>> root", inner(root)]);
    log([">>> target", inner(target)]);
  },
  (err) => {
    console.log(err.message);
  }
);
undefinedcomponent stateful ? 0
mount component
update effect
patch component
>>> root
>>> target

❓ 没结果!!!!!!

SUSPENSE

feat(add): suspense · gcclll/stb-vue-next@fd651ab

Suspense 组件和 Teleport 一样的组织结构和使用方式

结构:

1
2
3
4
var Tmpl = {
  __isSuspense: true,
  process() {}
}

然后在 process 中处理 mount 或 patch 流程,这里面和普通标签或普通组件的处理是一 样的, mount or patch。

下面来看下这个组件是如何实现的,功能又是如何?

新增函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 1. 模板
// 根据注释说明,之所以采用这种结构是为了能让 Suspense 适用 tree-shaking
export const SuspenseImpl = {
  __isSuspense: true,
  process(n1: VNode | null, n2: VNode /*...*/) {
    if (n1 == null) {
      // mount
    } else {
      // patch
    }
  },
  hydrate: hydrateSuspense,
  create: createSuspenseBoundary,
};

// 2. mountSuspense
// 3. patchSuspense

列表:

名称描述
SuspenseImpl-
mountSuspense()-
patchSuspense()-
SuspenseBoundary-
createSuspenseBoundary()-
hydrateSuspense()-

脑图:

/img/vue3/runtime-core/vue-runtime-core-suspense.svg

重点逻辑:

  1. Suspense 的渲染转折点发生在 mountComponent 中,将 setupRenderEffect 做了 一次封装,让其在 setup() 返回的 Promise 状态完成之后去执行

  2. 在整个 Suspense mount 或 patch 过程中,使用了 suspense.deps 来记录异步事件, 只有当这个值为 0 的时候说明可以进行解析并挂在到真实DOM上了(比如. 服务器端数 据请求完成)

SuspenseBoundary 数据结构

只列出部分与 Suspense 关联性强的字段:

名称描述
vnodeVNode 结构
hiddenContainer-
activeBranch请求完成之后显示的组件分支 #default
pendingBranch请求中显示的分支 #fallback
deps组件依赖
timeout超时时间
isInFallback-
isHydrating-
effects[] 依赖列表
resolve(force)-
fallback()参数: fallbackVnode
move()-
next()-
registerDep()注册实例依赖
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
export interface SuspenseBoundary {
  vnode: VNode<RendererNode, RendererElement, SuspenseProps>;
  parent: SuspenseBoundary | null;
  parentComponent: ComponentInternalInstance | null;
  isSVG: boolean;
  container: RendererElement;
  hiddenContainer: RendererElement;
  anchor: RendererNode | null;
  activeBranch: VNode | null;
  pendingBranch: VNode | null;
  deps: number;
  pendingId: number;
  timeout: number;
  isInFallback: boolean;
  isHydrating: boolean;
  isUnmounted: boolean;
  effects: Function[];
  resolve(force?: boolean): void;
  fallback(fallbackVNode: VNode): void;
  move(
    container: RendererElement,
    anchor: RendererNode | null,
    type: MoveType
  ): void;
  next(): RendererNode | null;
  registerDep(
    instance: ComponentInternalInstance,
    setupRenderEffect: SetupRenderEffectFn
  ): void;
  unmount(parentSuspense: SuspenseBoundary | null, doRemove?: boolean): void;
}

mountSuspense()

feat(add): suspense mount · gcclll/stb-vue-next@802b9ad

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
function mountSuspense() {
  const {
    p: patch,
    o: { createElement },
  } = rendererInternals;
  const hiddenContainer = createElement("div");
  const suspense = (vnode.suspense = createSuspenseBoundary(
    vnode,
    parentSuspense,
    parentComponent,
    container,
    hiddenContainer,
    anchor,
    isSVG,
    optimized,
    rendererInternals
  ));

  // start mounting the content subtree in an off-dom container
  patch(
    null,
    (suspense.pendingBranch = vnode.ssContent!),
    hiddenContainer,
    null,
    parentComponent,
    suspense,
    isSVG
  );
  // now check if we have encountered any async deps
  if (suspense.deps > 0) {
    // has async
    // mount the fallback tree
    patch(
      null,
      vnode.ssFallback!,
      container,
      anchor,
      parentComponent,
      null, // fallback tree will not have suspense context
      isSVG
    );
    setActiveBranch(suspense, vnode.ssFallback!);
  } else {
    // Suspense has no async deps. Just resolve.
    suspense.resolve();
  }
}

// 设置激活的 branch
function setActiveBranch(suspense: SuspenseBoundary, branch: VNode) {
  suspense.activeBranch = branch;
  const { vnode, parentComponent } = suspense;
  const el = (vnode.el = branch.el);
  // in case suspense is the root node of a component,
  // recursively update the HOC el
  if (parentComponent && parentComponent.subTree === vnode) {
    parentComponent.vnode.el = el;
    updateHOCHostEl(parentComponent, el);
  }
}
  1. 创建一个 DOM 之后的 div,即还没渲染到 DOM 结构中的

  2. 构建 Suspense 组件结构,这个结构非 VNode ,而是挂在 vnode.suspense 上的一个 SuspenseBoundary 结构

  3. 开始 mount 内容里的子树

  4. 检测 Suspense 有没异步依赖,如果有,则需要先解析这些异步依赖,完成之后再激活 branch

  5. 没有异步依赖直接拿到结果解析出组件

也就是说这里面需要重点关注的其实是“有没异步依赖的问题”。

没有依赖的时候用到了 suspense.resolve() 这个应该是将创建的 off-dom div 挂到真 实 DOM 上去。

suspense.resolve()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
function resolve(resume = false) {
  const {
    vnode,
    activeBranch,
    pendingBranch,
    pendingId,
    effects,
    parentComponent,
    container,
  } = suspense;

  if (suspense.isHydrating) {
    suspense.isHydrating = false;
  } else if (!resume) {
    // 1. transition 支持,将 move() 操作注册到 afterLeave 回调
    // 2. 卸载当前的 subTree 可能是 fallback
    // 3. 不是 transition dely enter 进行 move()
    // 这里最后执行的操作就是 move() 如果是 transition delay enter
    // 则将 move() 注册到 afterLeave,否则直接执行 move() 将 suspense
    // 内容渲染到真实DOM上
    const delayEnter =
      activeBranch &&
      pendingBranch!.transition &&
      pendingBranch!.transition.mode === "out-in";
    if (delayEnter) {
      activeBranch!.transition!.afterLeave = () => {
        if (pendingId === suspense.pendingId) {
          move(pendingBranch!, container, anchor, MoveType.ENTER);
        }
      };
    }
    // this is initial anchor on mount
    let { anchor } = suspense;
    // unmount current active tree
    if (activeBranch) {
      // if the fallback tree was mounted, it may have been moved
      // as part of a parent suspense. get the latest anchor for insertion
      anchor = next(activeBranch);
      unmount(activeBranch, parentComponent, suspense, true);
    }
    if (!delayEnter) {
      // move content from off-dom container to actual container
      move(pendingBranch!, container, anchor, MoveType.ENTER);
    }
  }

  // 标记当前激活状态的分支,此时是 #default
  setActiveBranch(suspense, pendingBranch!);
  suspense.pendingBranch = null;
  suspense.isInFallback = false;

  // flush buffered effects
  // check if there is a pending parent suspense
  // 注册的 effect 处理,这里的处理说明了 suspense 的父子依赖执行
  // 的顺序问题, effects 是按照数组加入顺序执行的(详情可以查看 reactivity 文章)
  // 所以 effects 的优先级是自上而下的,即 parent-parent > parent > children
  let parent = suspense.parent;
  let hasUnresolvedAncestor = false;
  while (parent) {
    if (parent.pendingBranch) {
      // found a pending parent suspense, merge buffered post jobs
      // into that parent
      parent.effects.push(...effects);
      hasUnresolvedAncestor = true;
      break;
    }
    parent = parent.parent;
  }
  // no pending parent suspense, flush all jobs
  // 如果没有挂起的 parent suspense 直接 flush 掉所有任务
  // 结合上面的 while 举例:
  // CompA -> CompB -> CompC
  // 当解析到 CompC 时,一直往上检测 B 和 A 如果 B 有挂起的任务
  // C 这里的任务不会被 flush,而是加入到 B 的队列等待执行
  // 然后 C 解析完成,回溯到 B 的解析,此时又遵循同一套规则检测 A 的
  // 挂起任务,直到最后要么立即执行 B 的任务要么 B 的任务也加入到 A
  // 最后由 A 执行所有的任务(包含子 suspense 的)
  if (!hasUnresolvedAncestor) {
    queuePostFlushCb(effects);
  }
  suspense.effects = [];

  // invoke @resolve event
  const onResolve = vnode.props && vnode.props.onResolve;
  if (isFunction(onResolve)) {
    onResolve();
  }
}

分析如上面的注释, resolve() 主要目的就是将 off-dom div 上的 suspense 组件在异 步事件完成后根据结果解析出对应的分支,将这个分支挂载到真实的 DOM 上去,同时激活 它(显示出来)。

其他处理:

  1. transition 的延迟进入处理,通过将 move() 操作注册到 afterLeave() 回调实现

  2. effects 任务处理,这里的任务处理机制是:

    只有在 parent 没有任何挂起的任务时候才会立即得到执行,否则只会进行合并操作。

因为代码最后需要执行 move() 操作将 #default 替换 #fallback ,所以下面先实 现 suspense.move() 再来测试。

suspense.move()

实现这个 move 有几个地方需要修改

  1. SuspenseBoundary 中的 move()

    1
    2
    3
    4
    5
    6
    7
    
    var foo = {
      move(container, anchor, type) {
        suspense.activeBranch &&
          move(suspense.activeBranch, container, anchor, type);
        suspense.container = container;
      },
    };
    
  2. renderer.ts 中的 move() 函数,实现 SUSPENSE 组件的处理

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    const move = () => {
      // ...
      // SUSPENSE
      if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
        console.log("move suspense");
        vnode.suspense!.move(container, anchor, moveType);
        return;
      }
      // ...
    };
    
  3. 另外 mountComponent 中漏了对 SUSPENSE 的处理

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
     const mountComponent = () => {
       // ... create instance
       // ... setupComponent
    
       // setup() 是个异步函数,返回了 promise ,在 setupComponent
       // 中会将 setup 执行结果赋值给 instance.asyncDep,即 SUSPENSE 处理
       if (__FEATURE_SUSPENSE__ && instance.asyncDep) {
         // 将 setupRenderEffect 注册到 parent deps 这里的 deps
         // 执行由一定的规则, 如果 parent suspense 没有结束,child deps
         // 不会立即执行,而是将它们合并到 parent suspense deps 中等待 parent 状态完成了才会执行,对于
         // parent deps 也遵循这个规则,直到没有未完成的 parent suspense为止
         parentSuspense && parentSuspense.registerDep(instance, setupRenderEffect);
         // 这里等于是说先用一个注释节点占位,等异步完成之后替换
         if (!initialVNode.el) {
           const placeholder = (instance.subTree = createVNode(Comment));
           processCommentNode(null, placeholder, container!, anchor);
         }
         return;
       }
    
       // ... setupRenderEffect SUSPENSE 不会进入到这里
     };
    

测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
const { log, f, shuffle, runtime_test, renderChildren } = require(process.env
  .BLOG_DIR_VUE + "/lib.js");
import(process.env.BLOG_DIR_VUE + "/runtime-test.global.js").then(
  async ({
    nextTick,
    h,
    render,
    nodeOps,
    serializeInner: inner,
    ref,
    Suspense,
  }) => {
    log.br()
    const deps = [];
    function defineAsyncComponent(comp, delay = 0) {
      return {
        setup(props, { slots }) {
          const p = new Promise((resolve) => {
            setTimeout(() => {
              resolve(() => h(comp, props, slots));
            }, delay);
          });
          deps.push(p.then(() => Promise.resolve()));
          return p;
        },
      };
    }

    const Async = defineAsyncComponent({
      render() {
        return h("div", "async");
      },
    });

    const Comp = {
      setup() {
        return () =>
          h(Suspense, null, {
            default: h(Async),
            fallback: h("div", "fallback"),
          });
      },
    };

    const root = nodeOps.createElement("div");
    try {
      render(h(Comp), root);
    } catch (e) {
      console.log(e);
    }
    console.log("before");
    console.log(inner(root));

    await Promise.all(deps);
    await nextTick();
    console.log("after");
    console.log(inner(root));
  },
  (err) => {
    console.log(err);
  }
);
undefined

component stateful ? 4
call setup
[Function (anonymous)] render
mount component
update effect
normalize vnode
patch component
component stateful ? 4
call setup
mount component
process element
mount elment
{ shapeFlag: 9 }
before
<div>fallback</div>
[Function (anonymous)] render
update effect
normalize vnode
patch component
component stateful ? 4
call setup
no setup
[Function: render] render
mount component
update effect
normalize vnode
patch component
process element
mount elment
{ shapeFlag: 9 }
moving...
move component
moving...
move component
moving...
move host insert
after
<div>async</div>

❓. 结果发现并没变化???

before
<!---->
after
<!---->

既没有渲染 fallback 也没有渲染 default 的,为何?

上面的第二点有说到在 renderer.tsmountComponent() 中增加了对 SUSPENSE 的处理,这里面有个注册依赖的动作,这里注册的是 setupRenderEffect 函数,这个函 数正是用来 mount & update 组件的,而在 components/Suspense.ts 中并没有实现,所 以问题就出在这里了!!!

FIX: suspense.registerDep()

suspense.registerDep()

feat(add): suspense registerDeps · gcclll/stb-vue-next@e0fa81e

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
function registerDep(instance, setupRenderEffect) {
  const isInPendingSuspense = !!suspense.pendingBranch;
  if (isInPendingSuspense) {
    suspense.deps++;
  }
  const hydratedEl = instance.vnode.el;
  // 捕获 setup 执行的异常,或接受执行的结果
  instance
    .asyncDep!.catch((err) => {
      handleError(err, instance, ErrorCodes.SETUP_FUNCTION);
    })
    .then((asyncSetupResult) => {
      // 当 setup() 的 promise 状态变更之后重试
      // 因为在解析之前组件可能已经被卸载了
      if (
        instance.isUnmounted ||
        suspense.isUnmounted ||
        suspense.pendingId !== instance.suspenseId
      ) {
        return;
      }

      // 从该组件开始重试,状态标记为已经完成
      instance.asyncResolved = true;
      const { vnode } = instance;
      handleSetupResult(instance, asyncSetupResult, false);
      if (hydratedEl) {
        // 虚拟节点可能在 async dep 状态完成之前被某个更新替换掉了
        vnode.el = hydratedEl;
      }
      const placeHolder = !hydratedEl && instance.subTree.el;
      setupRenderEffect(
        instance,
        vnode,
        // 组件可能在 resolve 之前被移除了
        // 如果这个不是一个 hydration,instance.subTree 将会是个注释
        // 占位节点
        parentNode(hydratedEl || instance.subTree.el!),
        hydratedEl ? null : next(instance.subTree),
        suspense,
        isSVG,
        optimized
      );
      if (placeHolder) {
        remove(placeHolder);
      }
      updateHOCHostEl(instance, vnode.el);
      // only decrease deps count if suspense is not already resolved
      // 没有任何依赖了就开始解析 Suspense
      if (isInPendingSuspense && --suspense.deps === 0) {
        suspense.resolve();
      }
    });
}

这个函数主要实现点:

  1. 接受 setup() 执行的结果(Promise) asyncSetupResult 并捕获异常,对结果进行 分析处理

  2. 检测组件是不是已经卸载了,或者 suspense 被移除,就不需要继续处理了,退出即可

    1
    2
    3
    4
    5
    6
    7
    
     if (
       instance.isUnmounted ||
       suspense.isUnmounted ||
       suspense.pendingId !== instance.suspenseId
     ) {
       return;
     }
    
  3. 使用 handleSetupResult(instance, asyncSetupResult, false) 处理 setup 执行结 果,到底是 render 还是状态,需要解析

  4. 然后执行 setupRenderEffect 执行组件的 mount 或 update 操作

  5. 移除占位的注释节点

  6. suspense.deps 执行完成之后就可以开始解析 suspense 组件 进行 move 操作了。

patchSuspense()

suspense.unmount&fallback&其他

feat(add): suspense unmount & fallback… · gcclll/stb-vue-next@080898d

新增:

  1. patchSuspense() 和其他普通类型的处理差不多,无非就是检测 old 和 new branch 的 类型,进行 patch(),期间触发 onPending 事件

  2. suspense.fallback() 处理,当异步事件未完成时显示的 #fallback 分支处理,期间 触发 onFallback 事件

  3. suspense.unmount() 检测 activeBranch 和 pendingBranch 先卸载 active 随后卸载 pending 分支(前提是存在的情况下)

Suspense 组件测试

1
2
// `/js/vue/tests/Suspense.js'
require(process.env.BLOG_DIR_VUE + "/tests/Suspense.js");

小结

SUSPENSE 组件的大致执行流程

  1. patch 进入 switch default 检测到 shapeFlag 是 SUSPENSE

  2. 调用 type.process(n1,n2,…) 处理 Suspense 组件,根据 n1 决定是 mountSuspense 还是 patchSuspense 这里和其他类型组件处理逻辑一致

  3. 首次(mount), 进行 mountSuspense 创建 Suspense 组件,对 pendingBranch 进行 patch 操作 (挂在到一个非DOM树中的 'div' 元素(off-dom),待用),即异步操作还未完成时显示 的分支,如: #fallback

  4. 非首次(update),进行 patchSuspense 对比新旧的 branch 进行 patch

要点:在 mountComponent() 中不是直接调用 setupRenderEffect() 而是调 用 suspense.registerDep() 去处理 setup 执行的结果(instance.asyncDep),它是 个Promise 在其后的 then() 中接受 setup 执行结果,然后开始调用 setupRenderEffect mount 或 update 子树节点,待 suspense 上的所有依赖都完成之后开始 resolve() Suspense 组件将其挂在到真实的 DOM 中。

原理: setup() 返回 Promise,render 过程中注册渲染函数,待 promise 状态完成调用 then 接受异步结果来渲染 Suspense 组件(任务为 post 类型)。

KEEP_ALIVE

feat(add): keep-alive render · gcclll/stb-vue-next@a192cb4

KeepAlive 组件的 render 入口在 processComponent() 中,当 n1 == null 情况下, 会去检测该组件是不是 keep-alive 类型,如果是直接调用 activate() 激活。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 17. processComponent
  const processComponent = (
  /*...*/) => {
    if (n1 == null) {
      // mount
-      if (false /* keep alive */) {
-        // TODO
+      if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
+        ;(parentComponent!.ctx as KeepAliveContext).activate(
+          n2,
+          container,
+          anchor,
+          isSVG,
+          optimized
+        )

unmount 操作时,如果是 keep-alive 直接调用 deactivate() 失效,而不是真正的从 DOM 移除。

feat(add): keep-alive render unmount · gcclll/stb-vue-next@024b24b

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const unmount: UnmountFn = (...) => {
// ...
-    // TODO keep-alive
-    // keep-alive
+    if (shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
+      ;(parentComponent!.ctx as KeepAliveContext).deactivate(vnode)
+      return
+    }
// ...
}

mountComponent() 中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
  // 18. mountComponent
  const mountComponent: MountComponentFn = (...) => {
    const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance(
      initialVNode,
      parentComponent,
      parentSuspense
    ))

+    if (isKeepAlive(initialVNode)) {
+      ;(instance.ctx as KeepAliveContext).renderer = internals
+    }

这里将 keep-alive 组件的 setup() 函数中用到的一些 renderer 函数保存引用到 ctx.renderer 上供后面 setup() 中使用。

1
2
3
4
5
6
instance.ctx.renderer: {
    p: patch,
    m: move,
    um: _unmount,
    o: { createElement }
}

keep-alive 作为内部组件,内置了 setup() 函数的实现,所以在

patch -> processComponent -> mountComponent -> setupComponent

时调用的就是这个内置的 setup() 函数。

setup() 函数体大致代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
function setup(props: KeepAliveProps, { slots }: SetupContext) {
  const cache: Cache = new Map();
  const keys: Keys = new Set();
  let current: VNode | null = null;

  const instance = getCurrentInstance()!;
  const parentSuspense = instance.suspense;

  const sharedContext = instance.ctx as KeepAliveContext;
  const {
    renderer: {
      p: patch,
      m: move,
      um: _unmount,
      o: { createElement },
    },
  } = sharedContext;
  const storageContainer = createElement("div");

  sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => {};

  sharedContext.deactivate = (vnode: VNode) => {};

  // 对 renderer unmount 的一次封装
  function unmount(vnode: VNode) {}

  // 过滤掉缓存
  function pruneCache(filter?: (name: string) => boolean) {}

  function pruneCacheEntry(key: CacheKey) {}

  // TODO 监听 include/exclude 属性变化
  // TODO 在 render 之后缓存子树(subTree)
  // TODO 注册生命周期

  return () => {
    // 该函数解析出原始 VNode 节点返回
  };
}

上面代码提供了一下信息:

  1. activate & deactivate() 函数是挂在 VNode 的 ctx 上的,并且是在 setup() 调 用期间产生

  2. 缓存机制

  3. 只注册了 mounted, unmounted, update 声明周期

  4. 最后返回的函数可以得到最原始的 VNode 节点

注意看 processComponent() 中的判断:

if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) { activate() }

COMPONENT_KEPT_ALIVE 标记的赋值又是发生在 setup() 函数中,也就是说对于 keep-alive 组件首次加载不会进入到 activate() 而是直接按照普通组件处理调用 mountComponent() 去调用 setup() 初始化该 keep-alive 组件的一些函数等(其中 就包含 activatedeactivate() 函数)

当状态发生变化时根据特定条件最后执行激活才会去调用 activate() 而不是进入 mountComponent()

/img/vue3/runtime-core/vue-runtime-core-keep-alive.svg

activate()

feat(add): keep-alive ctx.activate · gcclll/stb-vue-next@267fdbd

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => {
  const instance = vnode.component!;
  move(vnode, container, anchor, MoveType.ENTER, parentSuspense);
  // props 可能发生变化,这里执行一次 patch 操作
  patch(
    instance.vnode,
    vnode,
    container,
    anchor,
    instance,
    parentSuspense,
    isSVG,
    optimized
  );
  queuePostRenderEffect(() => {
    instance.isDeactivated = false;
    if (instance.a) {
      // activated 周期函数
      invokeArrayFns(instance.a);
    }
    const vnodeHook = vnode.props && vnode.props.onVnodeMounted;
    if (vnodeHook) {
      invokeVNodeHook(vnodeHook, instance.parent, vnode);
    }
  }, parentSuspense);
};

激活 keep-alive 组件的函数,只有当非首次的时候,状态发生变更时会被调用,注意上 面的任务类型 post ,周期函数的调用是异步发生的,会在下一个 tick 中赋值。

deactivate()

feat(add): keep-alive ctx.deactivate · gcclll/stb-vue-next@b340d57

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
sharedContext.deactivate = (vnode: VNode) => {
  const instance = vnode.component!;
  move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense);
  queuePostRenderEffect(() => {
    if (instance.da) {
      invokeArrayFns(instance.da);
    }
    const vnodeHook = vnode.props && vnode.props.onVnodeUnmounted;
    if (vnodeHook) {
      invokeVNodeHook(vnodeHook, instance.parent, vnode);
    }
    instance.isDeactivated = true;
  }, parentSuspense);
};

如果看这里失活状态下组件是如何进行更新的?

storageContainer 是在 setup 中创建的一个空的 off-dom div 元素,这里等于是当组 件失活时会将 keep-alive 先挂载到这个 off-dom div 上去.

unmount()

feat(add): keep-alive unmount · gcclll/stb-vue-next@4ce0b9b

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 对 renderer unmount 的一次封装
function unmount(vnode: VNode) {
  resetShapeFlag(vnode);
  _unmount(vnode, instance, parentSuspense);
}

function resetShapeFlag(vnode: VNode) {
  let shapeFlag = vnode.shapeFlag;
  if (shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
    shapeFlag -= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE;
  }
  if (shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
    shapeFlag -= ShapeFlags.COMPONENT_KEPT_ALIVE;
  }
  vnode.shapeFlag = shapeFlag;
}

include & exclude props

feat(add): keep-alive include & exclude props · gcclll/stb-vue-next@fdcc306

主要增加两个函数实现,一个监听动作(watch([include, exclude],...)):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 过滤掉缓存
function pruneCache(filter?: (name: string) => boolean) {
  cache.forEach((vnode, key) => {
    const name = getComponentName(vnode.type as ConcreteComponent);
    if (name && (!filter || !filter(name))) {
      pruneCacheEntry(key);
    }
  });
}

function pruneCacheEntry(key: CacheKey) {
  const cached = cache.get(key) as VNode;
  if (!current || cached.type !== current.type) {
    // 新增或节点类型发生变化,直接卸载掉老的
    unmount(cached);
  } else if (current) {
    // 重置标记就可以了?
    // 当前激活的实例不该再是 kept-alive
    // 我们不能立即卸载但是稍后会进行卸载,所以这里先重置其标记
    // 不能立即卸载?
    // 是因为在 activate 和 deactivate 中的周期函数调用
    // 是采用的 post 类型异步执行的缘故吗?
    resetShapeFlag(current);
  }
  cache.delete(key);
  keys.delete(key);
}

// 监听 include/exclude 属性变化
watch(
  () => [props.include, props.exclude],
  ([include, exclude]) => {
    // 支持三种类型
    // 1. 字符串, 'a,b,c'
    // 2. 正则表达式, /a|b|c/
    // 3. 数组, ['a', 'b', 'c', /d|e/]
    include && pruneCache((name) => matches(include, name));
    exclude && pruneCache((name) => !matches(exclude, name));
  },
  { flush: "post", deep: true }
);

include 指定需要缓存的组件名称

exclude 指定不需要进行缓存的组件名称

类型: String, RegExp, Array

cache subtree

feat(add): keep-alive cache subtree · gcclll/stb-vue-next@efb7577

<keep-alive/> 的孩子节点🌲进行缓存。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 在 render 之后缓存子树(subTree)
let pendingCacheKey: CacheKey | null = null;
const cacheSubtree = () => {
  if (pendingCacheKey != null) {
    cache.set(pendingCacheKey, getInnerChild(instance.subTree));
  }
};
// 注册生命周期
onMounted(cacheSubtree);
onUpdated(cacheSubtree);

onBeforeUnmount(() => {
  cache.forEach((cached) => {
    const { subTree, suspense } = instance;
    const vnode = getInnerChild(subTree);
    if (cached.type === vnode.type) {
      // 有缓存的节点
      // 当前实例会成为 keep-alive 的 unmount 一部分
      resetShapeFlag(vnode);
      // 但是在这里执行它的 deactivated 钩子函数
      const da = vnode.component!.da;
      da && queuePostRenderEffect(da, suspense);
      return;
    }
    // 没有缓存的直接 unmount
    unmount(cached);
  });
});

<keep-alive/> 卸载之前将已经缓存 deactivated 钩子函数推入队列等待执行,没 有缓存的直接调用 unmount() 卸载掉。

setup() -> render 函数

feat(add): keep-alive return render function · gcclll/stb-vue-next@9b75803

setupComponent() 中,最后调用 setup() 得到 setupResult ,最后会将这个 setupResult 传递给 handleSetupResult() 去处理,返回结果,这里面当检测到 setupResult 是个函数的时候,那么这个函数会被当做是 instance.render 函数处理。

即,这里的 setupResult 是 <keep-alive/> 组件 children 的 render 函数。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
function setup() {
  // ...
  return () => {
    // 该函数解析出原始 VNode 节点返回
    // 根据组件的执行流程,这个函数将会在 setupComponent() 中
    // 执行 setup() 得到 setupResult ,传递给 handleSetupResult()
    // 函数,这里面检测 setupResult 也就是这个匿名函数,如果它是函数
    // 会直接被当做 render 函数处理(instance.render 或 instance.ssrRender)
    // 结论就是,这个匿名函数是 render() 函数

    pendingCacheKey = null;
    if (!slots.default) {
      // 组件支持默认插槽使用方式
      return null;
    }

    const children = slots.default();
    const rawVNode = children[0];
    if (children.length > 1) {
      // KeepAlive 组件只能包含一个组件作为 child
      // 也就是说 ~<keep-alive><CompA/><CompB/></keep-alive/>~
      // 是不合法的使用
      current = null;
      // warn...
      return children;
    } else if (
      !isVNode(rawVNode) ||
      (!(rawVNode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) &&
        !(rawVNode.shapeFlag & ShapeFlags.SUSPENSE))
    ) {
      // 1. 非 VNode 类型节点
      // 2. 既不是有状态组件(对象类型组件)也不是 Suspense 的时候
      // 相反意味着,节点必须满足下面几种情况
      // 1. 是 VNode 类型且是有状态组件(非函数式组件)
      // 2. 或者是 VNode 类型且是Suspense 组件
      current = null;
      return rawVNode;
    }

    // 也就是说 keep-alive 只接受有状态组件或者 Suspense 作为唯一的 child
    let vnode = getInnerChild(rawVNode);
    const comp = vnode.type as ConcreteComponent;
    const name = getComponentName(comp);
    const { include, exclude, max } = props;

    if (
      // 无缓存的节点
      (include && (!name || !matches(include, name))) ||
      // 在不缓存的节点们之列
      (exclude && name && matches(exclude, name))
    ) {
      current = vnode;
      return rawVNode;
    }

    const key = vnode.key == null ? comp : vnode.key;
    const cachedVNode = cache.get(key);

    // 克隆一份如果它有被复用的话,因为我们即将修改它
    if (vnode.el) {
      vnode = cloneVNode(vnode);
      if (rawVNode.shapeFlag & ShapeFlags.SUSPENSE) {
        rawVNode.ssContent = vnode;
      }
    }

    pendingCacheKey = key;

    if (cachedVNode) {
      vnode.el = cachedVNode.el;
      vnode.component = cachedVNode.component;
      if (vnode.transition) {
        // 在 subTree 上递归更新 transition 钩子函数
        setTransitionHooks(vnode, vnode.transition!);
      }

      // 避免 vnode 正在首次 mount
      vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE;
      // 标记 key 为最新的
      keys.delete(key);
      keys.add(key);
    } else {
      // 没有缓存的情况
      keys.add(key);
      // 删除最老的 entry,缓冲池已经满了,删除掉最老的那个
      if (max && keys.size > parseInt(max as string, 10)) {
        // 因为 Set 没有直接取指定位置元素的值
        // 这里的目的是变相的取 Set 中第一个元素,即最早 add 的那个 key
        // 如: new Set([1,2,3,4]) => keys.values() => <1,2,3,4>
        // next() 得到迭代器下一个值 { value: 1, done: false }
        // .value 得到第一个集合元素的值
        pruneCacheEntry(keys.values().next().value);
      }
    }

    // 避免 vnode 正在被卸载,在renderer unmount 中会检测
    vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE;

    current = vnode;

    return rawVNode;
  };
}

最后返回的是 children[0] 节点。

里面有个置位标识值得注意: ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE

这个作用是啥?

  1. unmount() 中调用 deactivate() 的条件

    1
    2
    3
    4
    5
    
    // keep-alive
    if (shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
      (parentComponent!.ctx as KeepAliveContext).deactivate(vnode);
      return;
    }
    
  2. instance.update 的 effect 中触发 activated 周期函数的条件

    1
    2
    3
    4
    5
    6
    7
    
    // activated hook for keep-alive roots.
    // #1742 activated hook must be accessed after first render
    // since the hook may be injected by a child keep-alive
    const { a } = instance;
    if (a && initialVNode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
      queuePostRenderEffect(a, parentSuspense);
    }
    

测试

1
2
// `/js/vue/tests/Suspense.js'
require(process.env.BLOG_DIR_VUE + "/tests/KeepAlive.js");
undefined

Cannot read property '_vnode' of undefined

⚠ 有错误,待完成。。。vue-next 测试见最后一章节《测试》

set ref

官方使用文档: Special Attributes | Vue.js

官方示例:

1
2
3
4
5
6
7
8
<!-- vm.$refs.p will be the DOM node -->
<p ref="p">hello</p>

<!-- vm.$refs.child will be the child component instance -->
<child-component ref="child"></child-component>

<!-- When bound dynamically, we can define ref as a callback function, passing the element or component instance explicitly -->
<child-component :ref="(el) => child = el"></child-component>

patch() 函数中对 ref 属性的处理(set ref):

feat(add): set ref · gcclll/stb-vue-next@a0a1344

源码实现主要有几个步骤:

  1. ref 支持数据?

  2. 是不是异步组件 value = null

  3. 有状态组件(STATEFULL_COMPONENT, 分函数组件)

    value = vnode.component!.exposed || vnode.component!.proxy

  4. 其他情况下 value = vnode.el

    即 2,3,4 都是为了设置 value 指向哪个引用,比如 vnode.el 在渲染之后会被赋值为当 前 vnode 对应的那个 DOM 元素。

  5. 断开 oldRef 引用

  6. 设置 ref,分三种情况

    • ref 是字符串直接 refs[ref] = value 取 key 设值

    • ref 是 Ref 类型, ref.value = value

    • ref 是函数类型, ref(value, refs) 调用

在设值的时候会根据 value 是否为空值来控制是否进行异步设置,等 Render 执行完 成之后再设置

1
2
3
4
5
6
if (value) {
  (doSet as SchedulerCb).id = -1;
  queuePostRenderEffect(doSet, parentSuspense);
} else {
  doSet();
}

源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
const patch: PatchFn = (/*...*/) => {
  // ...
  // set ref
  setRef(ref, n1 && n1.ref, parentSuspense, n2);
};

export const setRef = (
  rawRef: VNodeNormalizedRef,
  oldRawRef: VNodeNormalizedRef | null,
  parentSuspense: SuspenseBoundary | null,
  vnode: VNode | null
) => {
  if (isArray(rawRef)) {
    rawRef.forEach((r, i) =>
      setRef(
        r,
        oldRawRef && (isArray(oldRawRef) ? oldRawRef[i] : oldRawRef),
        parentSuspense,
        vnode
      )
    );
    return;
  }

  let value:
    | ComponentPublicInstance
    | RendererNode
    | Record<string, any>
    | null;
  // async 组件,可以通过 defineAsyncComponent 声明的组件
  // loader 会赋给 __asyncLoader,如果是异步组件需要等组件渲染完成之后
  // 再去设置 ref
  if (!vnode || isAsyncWrapper(vnode)) {
    value = null;
  } else {
    if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
      value = vnode.component!.exposed || vnode.component!.proxy;
    } else {
      value = vnode.el;
    }
  }

  const { i: owner, r: ref } = rawRef;

  if (__DEV__ && !owner) {
    // warn 丢失 ref owner 上下文
    return;
  }

  const oldRef = oldRawRef && (oldRawRef as VNodeNormalizedRefAtom).r;
  const refs = owner.refs === EMPTY_OBJ ? (owner.refs = {}) : owner.refs;
  const setupState = owner.setupState;

  // unset old ref
  if (oldRef != null && oldRef !== ref) {
    if (isString(oldRef)) {
      refs[oldRef] = null;
      if (hasOwn(setupState, oldRef)) {
        setupState[oldRef] = null;
      }
    } else if (isRef(oldRef)) {
      oldRef.value = null;
    }
  }

  if (isString(ref)) {
    const doSet = () => {
      refs[ref] = value;
      if (hasOwn(setupState, ref)) {
        setupState[ref] = value;
      }
    };

    // #1789: 非空值,在 render 结束后设置
    // 控制意味着是 unmount,它不应该重写同key 的其他 ref
    if (value) {
      (doSet as SchedulerCb).id = -1;
      queuePostRenderEffect(doSet, parentSuspense);
    } else {
      doSet();
    }
  } else if (isRef(ref)) {
    const doSet = () => {
      ref.value = value;
    };

    if (value) {
      (doSet as SchedulerCb).id = -1;
      queuePostRenderEffect(doSet, parentSuspense);
    } else {
      doSet();
    }
  } else if (isFunction(ref)) {
    callWithErrorHandling(ref, owner, ErrorCodes.FUNCTION_REF, [value, refs]);
  } else if (__DEV__) {
    // warn ...
  }
};

这里可以简单理解为:将 ref 设值为 vnode.el 这是个引用,因此当它有值的时候也 等于是 ref 有值了,然后分为异步和同步,异步需要等 render 完成再去设置。

direcitve hooks

feat(add): directive hooks · gcclll/stb-vue-next@1343be2

执行指令声明周期钩子函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export function invokeDirectiveHook(
  vnode: VNode,
  prevVNode: VNode | null,
  instance: ComponentInternalInstance | null,
  name: keyof ObjectDirective
) {
  const bindings = vnode.dirs!
  const oldBindings = prevVNode && prevVNode.dirs!
  for (let i = 0; i < bindings.length; i++) {
    const binding = bindings[i]
    if (oldBindings) {
      binding.oldValue = oldBindings[i].value
    }
    const hook = binding.dir[name] as DirectiveHook | undefined
    if (hook) {
      callWithAsyncErrorHandling(hook, instance, ErrorCodes.DIRECTIVE_HOOK, [
        vnode.el,
        binding,
        vnode,
        prevVNode
      ])
    }
  }
}

给组件注册指令集:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

export function withDirectives<T extends VNode>(
  vnode: T,
  directives: DirectiveArguments
): T {
  const internalInstance = currentRenderingInstance
  if (internalInstance === null) {
    __DEV__ && warn(`withDirectives can only be used inside render functions.`)
    return vnode
  }
  const instance = internalInstance.proxy
  const bindings: DirectiveBinding[] = vnode.dirs || (vnode.dirs = [])
  for (let i = 0; i < directives.length; i++) {
    let [dir, value, arg, modifiers = EMPTY_OBJ] = directives[i]
    if (isFunction(dir)) {
      dir = {
        mounted: dir,
        updated: dir
      } as ObjectDirective
    }
    bindings.push({
      dir,
      instance,
      value,
      oldValue: void 0,
      arg,
      modifiers
    })
  }
  return vnode
}

created, befoureMounted, mounted 发生在 mountElement()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 发生在 mountChildren 之后
if (dirs) {
  invokeDirectiveHook(vnode, null, parentComponent, "created");
}

// ...

// 在插入DOM之前执行
if (dirs) {
  invokeDirectiveHook(vnode, null, parentComponent, "beforeMount");
}

//  hostInsert(el, container, anchor)

// 插入DOM之后执行,放入任务队列等待 render 结束
if (
  (vnodeHook = props && props.onVnodeMounted) ||
  needCallTransitionHooks ||
  dirs
) {
  queuePostRenderEffect(() => {
    vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode);
    needCallTransitionHooks && transition!.enter(el);
    dirs && invokeDirectiveHook(vnode, null, parentComponent, "mounted");
  }, parentSuspense);
}

beforeUpdate, updated 发生在 mountChildren()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
+    if ((vnodeHook = newProps.onVnodeBeforeUpdate)) {
+      invokeVNodeHook(vnodeHook, parentComponent, n2, n1)
+    }

+ if (dirs) {
+      invokeDirectiveHook(n2, n1, parentComponent, 'beforeUpdate')
+    }

+   if ((vnodeHook = newProps.onVnodeUpdated) || dirs) {
+      queuePostRenderEffect(() => {
+        vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, n2, n1)
+        dirs && invokeDirectiveHook(n2, n1, parentComponent, 'updated')
+      }, parentSuspense)
+    }

unmounted 发生在 unmount()

1
2
3
4
5
6
7
+    if ((vnodeHook = props && props.onVnodeUnmounted) || shouldInvokeDirs) {
+      queuePostRenderEffect(() => {
+        vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode)
+        shouldInvokeDirs &&
+          invokeDirectiveHook(vnode, null, parentComponent, 'unmounted')
+      }, parentSuspense)
+    }

测试:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
const { log, f, shuffle, runtime_test, renderChildren } = require(process.env
  .BLOG_DIR_VUE + "/lib.js");
import(process.env.BLOG_DIR_VUE + "/runtime-test.global.js").then(
  ({
    h,
    render,
    nodeOps,
    serializeInner: inner,
    ref,
    currentInstance,
    withDirectives,
  }) => {
    const count = ref(0);
    let _vnode = null,
      _prevVnode;

    const beforeMount = (el, binding, vnode, prevVnode) => {
      log(">>> before mounted");
      log("el.tag = " + el.tag);
      log("el.parentNode = " + el.parentNode);
      log("root.children.length = " + root.children.length);

      log(binding);
      log("vnode = _vnode, " + (vnode === _vnode));
      log("prev vnode = " + prevVnode);
    };

    const mounted = (el, binding, vnode, prevVnode) => {
      log(">>> mounted");
      log("el.tag = " + el.tag);
      log("el.parentNode = root" + (el.parentNode === root));
      log("root.children[0] = el" + (el.children[0] === el));

      log(binding);
      log("vnode = _vnode, " + (vnode === _vnode));
      log("prev vnode = " + prevVnode);
    };

    const beforeUpdate = (el, binding, vnode, prevVnode) => {
      log(">>> before update");
      log("el.tag = " + el.tag);
      log("el.parentNode = root" + (el.parentNode === root));
      log("root.children[0] = el" + (el.children[0] === el));

      log("节点应该还没更新 el.children[0].text = " + (count.value - 1));
      log(binding);
      log("vnode = _vnode, " + (vnode === _vnode));
      log("prev vnode = _prevVnode" + (prevVNode === _prevVnode));
    };

    const updated = (el, binding, vnode, prevVnode) => {
      log(">>> updated");
      log("el.tag = " + el.tag);
      log("el.parentNode = root" + (el.parentNode === root));
      log("root.children[0] = el" + (el.children[0] === el));

      log("节点应该已经更新 el.children[0].text = " + count.value);
      log(binding);
      log("vnode = _vnode, " + (vnode === _vnode));
      log("prev vnode = _prevVnode" + (prevVNode === _prevVnode));
    };

    const beforeUnmount = (el, binding, vnode, prevVnode) => {
      log(">>> before unmount");
      log("el.tag = " + el.tag);
      log("el.parentNode = root" + (el.parentNode === root));
      log("root.children[0] = el" + (el.children[0] === el));

      log("节点应该已经更新 el.children[0].text = " + count.value);
      log(binding);
      log("vnode = _vnode, " + (vnode === _vnode));
      log("prev vnode = " + prevVNode);
    };
    const unmounted = (el, binding, vnode, prevVnode) => {
      log(">>> unmounted");
      log("el.tag = " + el.tag);
      log("el.parentNode = " + el.parentNode);
      log("root.children.length = " + el.children.length);

      log("节点应该已经更新 el.children[0].text = " + count.value);
      log(binding);
      log("vnode = _vnode, " + (vnode === _vnode));
      log("prev vnode = " + prevVNode);
    };

    const dir = {
      beforeMount,
      mounted,
      beforeUpdate,
      updated,
      beforeUnmount,
      unmounted,
    };

    let _instance = null;
    const Comp = {
      setup() {
        _instance = currentInstance;
      },
      render() {
        (_prevVnode = _vnode),
          (_vnode = withDirectives(h("div", count.value), [
            [
              dir, // dir, v-dir
              count.value, // value, v-dir:foo.ok=value
              "foo", // argument
              { ok: true }, // modifiers
            ],
          ]));
        return _vnode;
      },
    };

    const root = nodeOps.createElement("div");
    render(h(Comp), root);
  },

  (err) => {
    console.log(err.message);
  }
);
undefinedcomponent stateful ? 4
call setup
[Function: render] render
normalize vnode
>>> before mounted
el.tag = div
el.parentNode = null
root.children.length = 0
{
  dir: {
    beforeMount: [Function: beforeMount],
    mounted: [Function: mounted],
    beforeUpdate: [Function: beforeUpdate],
    updated: [Function: updated],
    beforeUnmount: [Function: beforeUnmount],
    unmounted: [Function: unmounted]
  },
  instance: {},
  value: 0,
  oldValue: undefined,
  arg: 'foo',
  modifiers: { ok: true }
}
vnode = _vnode, true
prev vnode = null
>>> mounted
el.tag = div
el.parentNode = roottrue
root.children[0] = elfalse
{
  dir: {
    beforeMount: [Function: beforeMount],
    mounted: [Function: mounted],
    beforeUpdate: [Function: beforeUpdate],
    updated: [Function: updated],
    beforeUnmount: [Function: beforeUnmount],
    unmounted: [Function: unmounted]
  },
  instance: {},
  value: 0,
  oldValue: undefined,
  arg: 'foo',
  modifiers: { ok: true }
}
vnode = _vnode, true
prev vnode = null

TODO component props

feat(add): patch props · gcclll/stb-vue-next@6f6a0be

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
const { log, f, shuffle, runtime_test, renderChildren } = require(process.env
  .BLOG_DIR_VUE + "/lib.js");
import(process.env.BLOG_DIR_VUE + "/runtime-test.global.js").then(
  ({
    h,
    render,
    nodeOps,
    serializeInner: inner,
    ref,
    currentInstance,
    withDirectives,
    defineComponent,
  }) => {
    let props;
    let attrs;
    let proxy;

    const Comp = defineComponent({
      props: ["fooBar", "barBaz"],
      render() {
        props = this.$props;
        attrs = this.$attrs;
        proxy = this;
      },
    });

    const _log = (title) => {
      log([
        '>>> ' + title,
        '\nproxy.fooBar = ' + proxy.fooBar,
        '\n> props\n', props,
        '\n> attrs\n', attrs
      ])
    }

    const root = nodeOps.createElement('div')
    render(h(Comp, { fooBar: 1, bar: 2 }), root)
    _log('test')

    render(h(Comp, { 'foo-bar': 3, bar: 3, baz: 4, barBaz: 5 }), root)
    _log('foo-bar 会转成 fooBar')

    render(h(Comp, { qux: 5 }), root)
    _log('删除 camel case')

    log('\n>>> stateful with setup')
  },

  (err) => {
    console.log(err.message);
  }
);
undefinedcomponent stateful ? 4
call setup
no setup
[Function: render] render
normalize vnode
>>> test
proxy.fooBar = 1
> props
 { fooBar: 1 }
> attrs
 { bar: 2 }
should update component
has changed props
normalize vnode
>>> foo-bar 会转成 fooBar
proxy.fooBar = 3
> props
 { fooBar: 3, barBaz: 5 }
> attrs
 { bar: 3, baz: 4 }
should update component
has changed props
normalize vnode
>>> 删除 camel case
proxy.fooBar = undefined
> props
 { fooBar: undefined, barBaz: undefined }
> attrs
 { qux: 5 }

测试

Teleport Testing...

Suspense Testing...

KeepAlive Testing...

Directive Testing...

脑图 & 测试结果 GIF

  1. keep-alive 测试变化 GIF(13M):

    /img/vue3/runtime-core/gifs/test-keep-alive.gif

    结合源码

    1
    2
    3
    
    // deactivate()
    const storageContainer = createElement("div");
    move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense);
    

    即, deactivated 的 DOM 节点其实并非直接删除了,而是移到到了一个 off-dom 的元素上了,待重新激活的时候再移回来(在源码的 deactivate 和 activate 函数中增 加 storageContainer 打印).

  2. 测试脑图:

    /img/vue3/runtime-core/vue-sample-runtime-core-suspense.svg