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

/img/bdx/yiyeshu-001.jpg

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

声明 :vue-next runtime-core 运行时核心代码,这部分内容较多,可能会分为几篇来 叙述, f 过滤掉对象空值属性。

更新日志&Todos

  1. [2021-01-08 10:12:50] 创建

  2. DONE [2021-01-15 10:24:00] scheduler

  3. TODO apiWatch -> post cb & ssr

  4. TODO STATEFUL_COMONENT

  5. TODO patchFlag 测试和用途

  6. TODO transformVNodeArgs

  7. TODO Suspense 组件

  8. TODO shouldTrack, currentBlock 和 block 相关函数的作用

  9. TODO setup() 返回值用来做了啥?

  10. TODO setup() 里面是如何收集生命周期函数,又是如何?在什么时候?执行他们的?

  11. TODO async component

模块初始化: feat(init): runtime-core · gcclll/stb-vue-next@b22b4db · GitHub

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

⚠ Tips

  1. class 支持数组(['foo', 'bar']),对象({foo:true,bar:false}),字符串('foo bar')

  2. style 支持数组(['color:red', {foo:'foo'}]),对象({color:'red',foo:'foo'}),字符串(color:red)

  3. class component 条件:

    1. function

    2. __vccOpts = { template: '<div />'}

  4. vnode ref 属性合并处理逻辑?

  5. vnode key 属性简单的值覆盖操作?

  6. h()createVNode 函数多种使用方式组合?

    h(type, propsOrChildren, ...children), 参数个数多变,对于这个函数的使用方法 记忆只要记住一点:

    props 总是对象,children 可以是对象(必须是 VNode 类型 __v_isVNode)也可以是 数组,所以:

    1. argc = 2, 如果是数组就一定是 children

    2. argc = 2, 如果是对象且有 __v_isVNode 标识,一定是 children 否则是 props

    3. argc = 3, 按照 h(type, props, children) 处理

    4. argc > 3, 按照 h(type, props, ...children) 处理,从第三个开始都是 children

    createVNode(type, props, children), 固定三个参数,第二个一定是 props, 第三 个一定是数组类型的 children,因为它后面还有更多的其他参数(patchFlag, dynamicProps, isBlockNode),所以前三个必须确定下来。

  7. scheduler, vue-next 中的任务调度器如何实现?

  8. api watch(source, cb, option) 中的 source 只能是 reactive/ref/function/array 类型, 如果是数组时其元素只能是 reactive/ref/function

  9. api watch(…, { deep: true }) 是如何做到深度监听的?

  10. api watch(shallowRef, cb (newVal) => {}) 是如何直接使用 newVal.a 的?

    1
    2
    3
    4
    
    var obj = shallowRef({ a: 0 });
    watch(shallowRef, (newVal) => {
      dummy = newVal.a; // 这里为什么可以直接访问 obj.a,obj 又是什么?
    });
    
  11. provide & inject 如何实现?

    provide(key,value) 向组件 provides[key] = value 设置

    inject(key) 从组件 provides[key] 取值

  12. TODO setup() 返回值用来做了啥?

  13. 组件声明周期函数(onBeforeXxx, onXxx)触发顺序是什么?

🐂 init

导出已完成模块(reactiviy)里的 Apis: feat(init): runtime-core> add exports from @vue/reactivity · gcclll/stb-vue-next@38e91a8 · GitHub

这部分代码有点多,所以这里事先将所有类型定义添加好:

feat(add): runtime-core>all types · gcclll/stb-vue-next@e3f7b94 · GitHub

有关类型定义请移步最后一节(纯贴代码的,所以放到最后)

😜 h function

feat(add): runtime-core>h function · gcclll/stb-vue-next@e48d5e2 · GitHub

h, render 函数初始化。

1
2
3
4
5
// Actual implementation
export function h(type: any, propsOrChildren?: any, children?: any): VNode {
  // TODO
  return {} as VNode;
}

实现:

 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
// Actual implementation
export function h(type: any, propsOrChildren?: any, children?: any): VNode {
  const l = arguments.length;
  if (l === 2) {
    if (isObject(propsOrChildren) && !isArray(propsOrChildren)) {
      // 没有 props 的 单节点(single vnode)
      if (isVNode(propsOrChildren)) {
        return createVNode(type, null, [propsOrChildren]);
      }
      // 有 props 没有 children
      return createVNode(type, propsOrChildren);
    } else {
      // omit props
      return createVNode(type, null, propsOrChildren);
    }
  } else {
    // 从第三个参数开始全当做孩子节点处理
    if (l > 3) {
      children = Array.prototype.slice.call(arguments, 2);
    } else if (l === 3 && isVNode(children)) {
      children = [children];
    }
    return createVNode(type, propsOrChildren, children);
  }
}

h, 接受不定参数

逻辑脑图:

/img/tmp/20210108152508.png

从脑图分支得出支持的情况代码示例:

  1. h('div') 无参数无孩子

  2. h('div', { id: 'foo' }) 有 props 无 children

  3. h('div', ['foo']) 数组当做 chilren

  4. h('div', vnode) 有 __v_isVNode 标识当做 children,并转成数组 [vnode]

  5. h('div', {}, ['foo']) 有 props 有 children

  6. h('div', {}, vnode) 有 props, 有 children 且 = [vnode]

接下来需要具体去实现 createVNode 函数。

🌿 createVNode function

feat(add): rc->createVNode · gcclll/stb-vue-next@194f72f · GitHub

这个函数最终是构造了 vnode: VNode 虚拟节点结构,返回。

这里面分为以下几个步骤实现:

  1. type 是 vnode 时候处理

  2. class 组件处理

  3. props 处理

  4. shapeFlag 检测,是什么类型 的 vnode

  5. 组件对象不应该 reactive(有状态的组件, STATEFUL_COMONENT)

  6. 构建 vnode: VNode 对象

  7. 检测 vnode.key 是不是 NaN

  8. normalize children

  9. normalize suspense children

  10. currentBlock 处理

  11. 返回 vnode 节点

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 源文件:/js/vue/lib.js
const {
  rc: { h, createVNode, reactive },
  f,
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");
const _h = (...args) => f(h(...args));

log([">>> type only\n", _h("div")]);
log([">>> type + props\n", _h("div", { id: "foo" })]);
log([">>> type + omit props\n", _h("div", ["foo"])]);
>>> type only
 { __v_isVNode: true, __v_skip: true, type: 'div', shapeFlag: 1 }
>>> type + props
 {
  __v_isVNode: true,
  __v_skip: true,
  type: 'div',
  props: { id: 'foo' },
  shapeFlag: 1
}
>>> type + omit props
 { __v_isVNode: true, __v_skip: true, type: 'div', shapeFlag: 1 }
>>> default slot
 {
  __v_isVNode: true,
  __v_skip: true,
  type: { template: '<br />' },
  shapeFlag: 4
}
undefined

d3c6563 props

feat(add): rc->createVNode, props · gcclll/stb-vue-next@d3c6563 · GitHub

处理 class 和 style 属性。

 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
 // 3. props 处理, class & style normalization
 if (props) {
   // for reactive or proxy objects, we need to clone it to enable mutation.
   if (isProxy(props) || InternalObjectKey in props) {
     props = extend({}, props);
   }
   let { class: klass, style } = props;
   if (klass && !isString(klass)) {
     // 1. string -> klass
     // 'foo' -> 'foo'
     // 2. array -> '' + arr.join(' ')
     // ['foo', 'bar'] -> 'foo bar'
     // 3. object -> '' + value ? ' value' : ''
     // { foo: true, bar: false, baz: true } -> 'foo baz'
     props.class = normalizeClass(klass);
   }

   if (isObject(style)) {
     // reactive state objects need to be cloned since they are likely to be
     // mutated
     if (isProxy(style) && !isArray(style)) {
       style = extend({}, style);
     }
     // 1. array -> object
     // [{ color: 'red' }, 'font-size:10px;height:100px;'] ->
     // { color: 'red', 'font-size': '10px', height: '100px' }
     // 2. object -> object 原样返回
     props.style = normalizeStyle(style);
   }
 }
  1. class 数组,对象,字符串?

    数组: 合并成字符串, ['foo', 'bar'] -> 'foo bar'

    对象: 合并成字符串, {foo: true, bar: false, baz: true} -> 'foo baz'

    字符串: 原样输出

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    export function normalizeClass(value: unknown): string {
      let res = "";
      if (isString(value)) {
        res = value;
      } else if (isArray(value)) {
        for (let i = 0; i < value.length; i++) {
          res += normalizeClass(value[i]) + " ";
        }
      } else if (isObject(value)) {
        for (const name in value) {
          if (value[name]) {
            res += name + " ";
          }
        }
      }
      return res.trim();
    }
    
  2. style 数组,对象,字符串?

    数组: 合并成对象, ['color:red', { 'font-size': '10px', height: '100px' }] -> {color: 'red', 'font-size': '10px', height: '100px'}

    对象: 原样返回

    字符串:解析成对象, 如数组内字符串部分

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
     export function normalizeStyle(value: unknown): NormalizedStyle | undefined {
       if (isArray(value)) {
         const res: Record<string, string | number> = {};
         for (let i = 0; i < value.length; i++) {
           const item = value[i];
           const normalized = normalizeStyle(
             isString(item) ? parseStringStyle(item) : item
           );
           if (normalized) {
             for (const key in normalized) {
               res[key] = normalized[key];
             }
           }
         }
         return res;
       } else if (isObject(value)) {
         return value;
       }
     }
    

测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19

// 源文件:/js/vue/lib.js
const { rc: { h, createVNode: c }, f, log } = require(process.env.BLOG_JS + '/vue/lib.js')
let _h = (...args) => f(c(...args), 'props')

// class 合并成字符串
log(['>>> class: string\n', _h('p', { class: 'foo baz' })])
log(['>>> class: array\n', _h('p', { class: ['foo', 'baz'] })])
log(['>>> class: array<object|string>\n', _h('p', { class: [{ foo:  'foo' }, 'baz', { baz: 'baz' }] })])
log(['>>> class: object\n', _h('p', { class: {'foo': true, 'baz': false, 'bar': true} })])

// style 合并成对象
log(['>>> style: array\n', _h('p', { style: [{ foo: 'foo' }, { baz: 'baz' }] })])
log(['>>> style: object\n', _h('p', {
  style: { foo: 'foo', baz: 'baz' }
})])
log(['>>> style: array<object|string>\n', _h('p', {
  style: [{ foo: 'foo' }, 'color:red', { baz: 'baz' }]
})])
>>> class: string
 { props: { class: 'foo baz' } }
>>> class: array
 { props: { class: 'foo baz' } }
>>> class: array<object|string>
 { props: { class: 'foo baz baz' } }
>>> class: object
 { props: { class: 'foo bar' } }
>>> style: array
 { props: { style: { foo: 'foo', baz: 'baz' } } }
>>> style: object
 { props: { style: { foo: 'foo', baz: 'baz' } } }
>>> style: array<object|string>
 { props: { style: { foo: 'foo', color: 'red', baz: 'baz' } } }
undefined

class component

是类组件前提是:

  1. 必须是函数

  2. 必须包含 __vccOpts 属性

1
2
3
4
5
6
7
8
  // 2. class component
  if (isClassComponent(type)) {
    type = type.__vccOpts;
  }

  export function isClassComponent(value: unknown): value is ClassComponent {
    return isFunction(value) && "__vccOpts" in value;
  }

测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11

// 源文件:/js/vue/lib.js
const { rc: { h, createVNode: c }, f, log } = require(process.env.BLOG_JS + '/vue/lib.js')
const _h = (...args) => f(c(...args))

class Component {
  $props

  static __vccOpts = { template: '<div />' }
}
log(_h(Component))
{
  __v_isVNode: true,
  __v_skip: true,
  type: { template: '<div />' },
  shapeFlag: 4 // STATEFUL_COMPONENT
}
undefined

TODO stateful component & key NaN

有状态的组件?

即 type 为对象时候视为有状态的组件。

如果是 STATEFUL_COMPONENT 且是个 proxy 的时候,开发模式下给出警告⚠️。

1
2
3
4
5
6

// 源文件:/js/vue/lib.js
const { rc: { h, createVNode: c, reactive:r }, f, log } = require(process.env.BLOG_JS + '/vue/lib.js')
const _h = (...args) => f(c(...args))

log(_h('div', { key: NaN }))
{
  __v_isVNode: true,
  __v_skip: true,
  type: 'div',
  props: { key: NaN },
  shapeFlag: 1
}
undefined

88eaf09 type is vnode

feat(add): rc->createVNode, type is vnode · gcclll/stb-vue-next@88eaf09 · GitHub

 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
  // > in createVNode
  // 1. type is vnode
  if (isVNode(type)) {
    // createVNode receiving an existing vnode. This happens in cases like
    // <component :is="vnode"/>
    // #2078 make sure to merge refs during the clone instead of overwriting it
    const cloned = cloneVNode(type, props, true /* mergeRef: true */);
    if (children) {
      normalizeChildren(cloned, children);
    }
    return cloned;
  }

  // cloneVNode
  // 省略直接取 vnode 值部分
  export function cloneVNode<T, U>(
    vnode: VNode<T, U>,
    extraProps?: (Data & VNodeProps) | null,
    mergeRef = false
  ): VNode<T, U> {
    // This is intentionally NOT using spread or extend to avoid the runtime
    // key enumeration cost.
    const { props, ref, patchFlag } = vnode;
    const mergedProps = extraProps ? mergeProps(props || {}, extraProps) : props;
    return {
      __v_isVNode: true,
      [ReactiveFlags.SKIP]: true,
      type: vnode.type,
      props: mergedProps,
      key: mergedProps && normalizeKey(mergedProps),
      ref:
        extraProps && extraProps.ref
          ? // #2078 in the case of <component :is="vnode" ref="extra"/>
            // if the vnode itself already has a ref, cloneVNode will need to merge
            // the refs so the single vnode can be set on multiple refs
            mergeRef && ref
            ? isArray(ref)
              ? ref.concat(normalizeRef(extraProps)!)
              : [ref, normalizeRef(extraProps)!]
            : normalizeRef(extraProps)
          : ref,
      // if the vnode is cloned with extra props, we can no longer assume its
      // existing patch flag to be reliable and need to add the FULL_PROPS flag.
      // note: perserve flag for fragments since they use the flag for children
      // fast paths only.
      patchFlag:
        extraProps && vnode.type !== Fragment
          ? patchFlag === -1 // hoisted node
            ? PatchFlags.FULL_PROPS
            : patchFlag | PatchFlags.FULL_PROPS
          : patchFlag,

      ssContent: vnode.ssContent && cloneVNode(vnode.ssContent),
      ssFallback: vnode.ssFallback && cloneVNode(vnode.ssFallback),
    };
  }

cloneVNode 绝大部分属性都是直接引用自 vnode,上面列出的都是需要处理的属性,比如:

  1. props 会将 vnode 和 cloneVNode 传入的 props 进行合并,并且是传入的 props 覆盖 vnode.props。

  2. key 属性,取合并之后的 key(测试->)

    1
    2
    3
    4
    5
    
     // normalize 合并后的 key
     const key = mergedProps && normalizeKey(mergedProps);
    
     const normalizeKey = ({ key }: VNodeProps): VNode["key"] =>
       key != null ? key : null;
    
  3. ref 属性,合并规则(测试->):

     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
    
     // 1. mergeRef: boolean 可以手动指定是否需要合并
     // 2. extraProps.ref 调用 cloneVNode 时候传入的 props ref
     // 3. ref 如果是数组,加上新的 ref 扩展原数组
     // 4. ref 不是数组,用 ref 和 extra ref 合并成新数组
     // 5. 如果 ref null, 则直接用 extra ref normalize 出新的 ref
     const ref =
       extraProps && extraProps.ref
         ? // #2078 in the case of <component :is="vnode" ref="extra"/>
           // if the vnode itself already has a ref, cloneVNode will need to merge
           // the refs so the single vnode can be set on multiple refs
           mergeRef && ref
           ? isArray(ref)
             ? ref.concat(normalizeRef(extraProps)!)
             : [ref, normalizeRef(extraProps)!]
           : normalizeRef(extraProps)
         : ref;
    
     // normalization
     const normalizeRef = ({ ref }: VNodeProps): VNodeNormalizedRefAtom | null => {
       return (ref != null
         ? isString(ref) || isRef(ref) || isFunction(ref)
           ? { i: currentRenderingInstance, r: ref }
           : ref
         : null) as any;
     };
    
  4. patchFlag 属性(测试->)

    1
    2
    3
    4
    5
    6
    
     const patchFlag =
       extraProps && vnode.type !== Fragment
         ? patchFlag === -1 // hoisted node
           ? PatchFlags.FULL_PROPS
           : patchFlag | PatchFlags.FULL_PROPS
         : patchFlag;
    
  5. ssContent 递归调用 cloneVNode(vnode.ssContent)

  6. ssFallback 递归调用 cloneVNode(vnode.ssFallback)

测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 源文件:/js/vue/lib.js
const {
  rc: { h, createVNode: c, cloneVNode: cv },
  f,
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");
const _h = (...args) => f(c(...args));

const node1 = _h("div", { foo: 1 }, null /* children */);
log([">>> vnode 1\n", node1]);

const node2 = _h({}, null, [node1]);
const cloned2 = cv(node2);
// cloneVNode 只是一次浅拷贝
log([">>> node2 == cloned2\n", f(cloned2), "\n > node2 \n", node2]);
>>> vnode 1
 {
  __v_isVNode: true,
  __v_skip: true,
  type: 'div',
  props: { foo: 1 },
  shapeFlag: 1
}
>>> node2 == cloned2
 {
  __v_isVNode: true,
  __v_skip: true,
  type: {},
  children: [
    {
      __v_isVNode: true,
      __v_skip: true,
      type: 'div',
      props: [Object],
      shapeFlag: 1
    }
  ],
  shapeFlag: 20
}
 > node2
 {
  __v_isVNode: true,
  __v_skip: true,
  type: {},
  children: [
    {
      __v_isVNode: true,
      __v_skip: true,
      type: 'div',
      props: [Object],
      shapeFlag: 1
    }
  ],
  shapeFlag: 20
}
undefined

feat(add): rc->createVNode, currentRenderingInstance · gcclll/stb-vue-next@4fbd98f · GitHub

key test

vnode.key 的 clone 操作,属于单纯的值覆盖操作。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 源文件:/js/vue/lib.js
const {
  rc: { h, createVNode: c, cloneVNode: cv },
  f,
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");
const _h = (...args) => f(c(...args));

log([">>> 保留 vnode.key 值\n", f(cv(c("div", { key: 1 })), "key")]);
log([
  ">>> 替换 vnode.key 值\n",
  f(cv(c("div", { key: 1 }), { key: 2 }), "key"),
]);
log([">>> 新 props.key 值\n", f(cv(c("div"), { key: 2 }), "key")]);

log(">>> 测试 vnode.key 各种情况值");
for (const key of ["", "a", 0, 1, NaN]) {
  log(f(c("div", { key }), "key"));
}
>>> 保留 vnode.key 值
 { key: 1 }
>>> 替换 vnode.key 值
 { key: 2 }
>>> 新 props.key 值
 { key: 2 }
>>> 测试 vnode.key 各种情况值
{}
{ key: 'a' }
{}
{ key: 1 }
[Vue warn]: VNode created with invalid key (NaN). VNode type:div
{}
undefined

ref test

流程脑图: /img/vue3/runtime-core/vue-runtime-core-vnode-ref.svg

测试

 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
// 源文件:/js/vue/lib.js
const {
  rc: {
    h,
    createVNode: c,
    cloneVNode: cv,
    ssrUtils: { setCurrentRenderingInstance: s },
  },
  f,
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");
const _h = (...args) => f(c(...args));

const mockIns1 = { ins: 1 },
  mockIns2 = { ins: 2 };
s(mockIns1);

let original = c("div", { ref: "foo" });
// 本身没有的时候会将 extraProps.ref 作为新的 vnode.ref 值
log([">>> 1. vnode 本身无 ref\n", f(original, "ref")]);
let cloned1 = cv(original);
log([">>> 2. 保留原有的 vnode.ref\n", f(cloned1, "ref")]);
// 这里没指定 mergeProp 所以会替换原来的
let cloned2 = cv(original, { ref: "bar" });
log(['>>> 3. ref: "bar" 替换原有的 vnode.ref\n', f(cloned2, "ref")]);
let original2 = c("div");
let cloned3 = cv(original2, { ref: "bar" });
log([">>> 4. 没有 vnode.ref 情况,新增 ref\n", f(cloned3, "ref")]);

s(mockIns2);
// 应该保留原有的 context instance
let cloned4 = cv(original);
log([">>> 5. 应该保留原有的 context instance\n", f(cloned4, "ref")]);
// ref 覆盖,使用新的 context instance: mockIns2
let cloned5 = cv(original, { ref: "bar" });
log([">>> 6. ref 改变,使用新的 context instance\n", f(cloned5, "ref")]);
s(null); // 置空 context instance

log('\n\n// mergeRef 情况测试\n')
s(mockIns1)
original = c('div', { ref: 'foo' })
s(mockIns2)
cloned1 = cv(original, { ref: 'bar' }, true)
log(['>>> mergeRef: true 合并 vnode.ref\n', f(cloned1, 'ref')])
log(cloned1.ref[0])
log(cloned1.ref[1])
>>> 1. vnode 本身无 ref
 { ref: { i: { ins: 1 }, r: 'foo' } }
>>> 2. 保留原有的 vnode.ref
 { ref: { i: { ins: 1 }, r: 'foo' } }
>>> 3. ref: "bar" 替换原有的 vnode.ref
 { ref: { i: { ins: 1 }, r: 'bar' } }
>>> 4. 没有 vnode.ref 情况,新增 ref
 { ref: { i: { ins: 1 }, r: 'bar' } }
>>> 5. 应该保留原有的 context instance
 { ref: { i: { ins: 1 }, r: 'foo' } }
>>> 6. ref 改变,使用新的 context instance
 { ref: { i: { ins: 2 }, r: 'bar' } }


// mergeRef 情况测试

>>> mergeRef: true 合并 vnode.ref
 { ref: [ { i: [Object], r: 'foo' }, { i: [Object], r: 'bar' } ] }
{ i: { ins: 1 }, r: 'foo' }
{ i: { ins: 2 }, r: 'bar' }
undefined

TODO patchFlag test

TODO need openBlock&createBlock support.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 源文件:/js/vue/lib.js
const {
  rc: {
    h,
    createVNode: c,
    cloneVNode: cv,
    ssrUtils: { setCurrentRenderingInstance: s },
  },
  f,
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");
const _h = (...args) => f(c(...args));

const hoist = c('div') // 静态节点
let vnode1
const vnode = (openBlock(), createBlock('div'))

shapeFlag test

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 源文件:/js/vue/lib.js
const {
  rc: { h, createVNode: c, cloneVNode: cv, Text },
  f,
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");
const _h = (...args) => f(c(...args));

log([">>> ELEMENT\n", f(c("div"), "shapeFlag")]);
log([">>> STATEFUL_COMONENT\n", f(c({}), "shapeFlag")]);
log([
  ">>> FUNCTION_COMONENT\n",
  f(
    c(() => {}),
    "shapeFlag"
  ),
]);
log([">>> Text\n", f(c(Text), "shapeFlag")]);
>>> ELEMENT
 { shapeFlag: 1 }
>>> STATEFUL_COMONENT
 { shapeFlag: 4 }
>>> FUNCTION_COMONENT
 { shapeFlag: 2 }
>>> Text
 { shapeFlag: 0 }
undefined

mergeProps test

 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
// 源文件:/js/vue/lib.js
const {
  rc: { h, createVNode: c, cloneVNode: cv, Text, mergeProps },
  f,
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");

let p1 = { class: "c" };
let p2 = { class: ["cc"] };
let p3 = { class: [{ ccc: true }] };
let p4 = { class: { cccc: true } };
log([">>> merge class\n", mergeProps(p1, p2, p3, p4)]);
let ps1 = {
  style: { color: "red", fontSize: 10 },
};
let ps2 = {
  style: [
    { color: "blue", width: "200px" },
    {
      width: "300px",
      height: "300px",
      fontSize: 30,
    },
  ],
};
let ps3 = { style: 'width:100px;right:10;top:10' }
log([">>> merge style\n", mergeProps(ps1, ps2, ps3)]);
let clickHandler1  = function(){}
let clickHandler2  = function(){}
let focusHandler3  = function(){}
let ph1 = { onClick: clickHandler1 }
let ph2 = { onClick: clickHandler2, onFocus: focusHandler3 }
log(['>>> merge handlers\n', mergeProps(ph1, ph2)])
>>> merge class
 { class: 'c cc ccc cccc' }
>>> merge style
 {
  style: {
    color: 'blue',
    fontSize: 30,
    width: '100px',
    height: '300px',
    right: '10',
    top: '10'
  }
}
>>> merge handlers
 {
  onClick: [ [Function: clickHandler1], [Function: clickHandler2] ],
  onFocus: [Function: focusHandler3]
}
undefined

TODO dynamic children test

> need openBlock&createBlock support

1
2
3
4
5
6
7
8
const {
  rc: { h, createVNode: c, cloneVNode: cv, Text, mergeProps },
  f,
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");

const hoist = createVNode('div')
let vnode1

TODO transformVNodeArgs test

TODO 7ec1d30 suspense component

feat(add): rc->createVNode, type is suspense component · gcclll/stb-vue-next@7ec1d30 · GitHub

Suspense 的 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
60
61
62
63
64
65
  // 7. normalize suspense children
  if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
    const { content, fallback } = normalizeSuspenseChildren(vnode);
    vnode.ssContent = content;
    vnode.ssFallback = fallback;
  }

  // normalizeSuspenseChildren
  export function normalizeSuspenseChildren(
    vnode: VNode
  ): {
    content: VNode;
    fallback: VNode;
  } {
    const { shapeFlag, children } = vnode;
    let content: VNode, fallback: VNode;

    if (shapeFlag & ShapeFlags.SLOTS_CHILDREN) {
      content = normalizeSuspenseSlot((children as Slots).default);
      fallback = normalizeSuspenseSlot((children as Slots).fallback);
    } else {
      content = normalizeSuspenseSlot(children as VNodeChild);
      fallback = normalizeVNode(null);
    }

    return {
      content,
      fallback,
    };
  }

// >>> normalizeSuspenseSlot
function normalizeSuspenseSlot(s: any) {
  if (isFunction(s)) {
    s = s()
  }
  if (isArray(s)) {
    // ROOT 必须是单节点 <div>...</div>
    const singleChild = filterSingleRoot(s)
    if (__DEV__ && !singleChild) {
      warn(`<Suspense> slots expect a single root node.`)
    }
    s = singleChild
  }
  return normalizeVNode(s)
}

// normalizeVNode
export function normalizeVNode(child: VNodeChild): VNode {
  if (child == null || typeof child === 'boolean') {
    // empty placeholder
    return createVNode(Comment)
  } else if (isArray(child)) {
    // fragment
    return createVNode(Fragment, null, child)
  } else if (typeof child === 'object') {
    // already vnode, this should be the most common since compiled templates
    // always produce all-vnode children arrays
    // 这是最常用的情况,因为使用模板的时候最后生成的 children 是数组
    return child.el === null ? child : cloneVNode(child)
  } else {
    // strings and numbers
    return createVNode(Text, null, String(child))
  }
}

检测是不是 single root 函数: filterSingleRoot

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
export function filterSingleRoot(
  children: VNodeArrayChildren
): VNode | undefined {
  let singleRoot;
  for (let i = 0; i < children.length; i++) {
    const child = children[i];
    if (isVNode(child)) {
      // ignore user comment
      if (child.type !== Comment || child.children === "v-if") {
        if (singleRoot) {
          // has more than 1 non-comment child, return now

          return;
        } else {
          singleRoot = child;
        }
      }
    } else {
      return;
    }
  }
  return singleRoot;
}

TODO 23fc943 currentBlock 优化

feat(add): rc->createVNode, optimize diff, currentBlock · gcclll/stb-vue-next@23fc943 · GitHub

这里的处理没怎么搞明白❓

注意这里增加的几个变量‼

blockStack, currentBlock:

1
2
3
4
5
6
7
8
9

// Since v-if and v-for are the two possible ways node structure can dynamically
// change, once we consider v-if branches and each v-for fragment a block, we
// can divide a template into nested blocks, and within each block the node
// structure would be stable. This allows us to skip most children diffing
// and only worry about the dynamic nodes (indicated by patch flags).
// 针对 v-if, v-for 动态性做的由于,减少对静态节点的 diff ,只需要关心动态节点即可
export const blockStack: (VNode[] | null)[] = []
let currentBlock: VNode[] | null = null

shouldTrack:

1
2
3
4
5
6
// Whether we should be tracking dynamic child nodes inside a block.
// Only tracks when this value is > 0
// We are not using a simple boolean because this value may need to be
// incremented/decremented by nested usage of v-once (see below)
// 是否应该 tracking block 内动态的孩子节点
let shouldTrack = 1;

新增处理逻辑:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
 // 8. currentBlock
 if (
   shouldTrack > 0 &&
   // 避免 block 节点 tracking 自己
   !isBlockNode &&
   // has current parent block
   currentBlock &&
   // presence of a patch flag indicates this node needs patching on updates.
   // component nodes also should always be patched, because even if the
   // component doesn't need to update, it needs to persist the instance on to
   // the next vnode so that it can be properly unmounted later.
   (patchFlag > 0 || shapeFlag & ShapeFlags.COMPONENT) &&
   // the EVENTS flag is only for hydration and if it is the only flag, the
   // vnode should not be considered dynamic due to handler caching.
   patchFlag !== PatchFlags.HYDRATE_EVENTS
 ) {
   currentBlock.push(vnode);
 }

跟这几个变量有关的函数:

normalizeChildren function

shapeFlag 初始值检测:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// encode the vnode type information into a bitmap
const shapeFlag = isString(type)
  ? ShapeFlags.ELEMENT // 1
  : __FEATURE_SUSPENSE__ && isSuspense(type)
  ? ShapeFlags.SUSPENSE // 1 << 7, 128
  : isTeleport(type)
  ? ShapeFlags.TELEPORT // 1 << 6, 64
  : isObject(type)
  ? ShapeFlags.STATEFUL_COMPONENT // 1 << 2, 4
  : isFunction(type)
  ? ShapeFlags.FUNCTIONAL_COMPONENT // 1 << 1, 2
  : 0;

测试:

1
2
3
4
5
6
7
// 源文件:/js/vue/lib.js
const { rc: { h, createVNode: c }, f, log } = require(process.env.BLOG_JS + '/vue/lib.js')
const _h = (...args) => f(c(...args))

log(['>>> only tag\n', _h('p')])
log(['>>> tag + props\n', _h('p', { foo: 'foo' })])
log(['>>> tag + props + children\n', _h('p', { foo: 'foo' }, ['foo'])])
>>> only tag
 { __v_isVNode: true, __v_skip: true, type: 'p', shapeFlag: 1 }
>>> tag + props
 {
  __v_isVNode: true,
  __v_skip: true,
  type: 'p',
  props: { foo: 'foo' },
  shapeFlag: 1
}
>>> tag + props + children
 {
  __v_isVNode: true,
  __v_skip: true,
  type: 'p',
  props: { foo: 'foo' },
  children: [ 'foo' ],
  shapeFlag: 17
}
undefined

children is function

feat(add): rc->propsOrChildren is function · gcclll/stb-vue-next@28d4a55 · GitHub

如果是函数,当做 slot 的 children 处理。

normalizeChildren:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
export function normalizeChildren(vnode: VNode, children: unknown) {
  let type = 0
  if (children == null) {
    children = null
  } else if (false /*array*/) {
    // TODO
  } else if (false /*object*/) {
    // TODO
  } else if (isFunction(children)) {
    // 如果是函数当做 slot children ?
    children = { default: children, _ctx: currentRenderingInstance }
    type = ShapeFlags.SLOTS_CHILDREN
  } else {
    // TODO 普通类型
  }

  vnode.children = children as VNodeNormalizedChildren
  vnode.shapeFlag |= type
}

测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10

// 源文件:/js/vue/lib.js
const { rc: { h, createVNode:c }, log, f } = require(process.env.BLOG_JS + '/vue/lib.js')
const _h = (...args) => f(h(...args));
const _c = (...args) => f(c(...args));

const Component = { template: '<br />' }
const slot = () => {}
log(['>>> default slot\n', _h(Component, slot)])
log(['>>> children is function\n', _c('div', {}, slot)])
>>> default slot
 {
  __v_isVNode: true,
  __v_skip: true,
  type: { template: '<br />' },
  children: { default: [Function: slot], _ctx: null },
  shapeFlag: 36
}
>>> children is function
 {
  __v_isVNode: true,
  __v_skip: true,
  type: 'div',
  props: {},
  children: { default: [Function: slot], _ctx: null },
  shapeFlag: 33
}
undefined

children is array or 普通类型

feat(add): rc->createVNode, children is array or primitive · gcclll/stb-vue-next@850c0bc · GitHub

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 数组类型
if (isArray(children)) {
  type = ShapeFlags.ARRAY_CHILDREN;
}

// 非对象,数组,函数的普通类型处理
{
  children = String(children);
  // force teleport children to array so it can be moved around
  if (shapeFlag & ShapeFlags.TELEPORT) {
    type = ShapeFlags.ARRAY_CHILDREN;
    children = [createTextVNode(children as string)];
  } else {
    type = ShapeFlags.TEXT_CHILDREN;
  }
}

// createTextVNode
export function createTextVNode(text: string = " ", flag: number = 0): VNode {
  return createVNode(Text, null, text, flag);
}

export const Text = Symbol(__DEV__ ? 'Text' : undefined)

普通类型处理中如果是 ShapeFlags.TELETPORT 当做 ARRAY_CHILDREN 处理,且 children 按照文本节点处理。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const {
  rc: { h, createVNode: c },
  f,
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");
const _h = (...args) => f(h(...args));
const _c = (...args) => f(c(...args));

log([`>>> array will be children(${1 | (1 << 4)})\n`, _h("div", ["foo"])]);
log([">>> string will be children()\n", _h("div", "foo")]);
>>> array will be children(17)
 {
  __v_isVNode: true,
  __v_skip: true,
  type: 'div',
  children: [ 'foo' ],
  shapeFlag: 17
}
>>> string will be children()
 {
  __v_isVNode: true,
  __v_skip: true,
  type: 'div',
  children: 'foo',
  shapeFlag: 9
}
undefined

children is object

feat(add): rc->createVNode, normalizeChildren is object · gcclll/stb-vue-next@959879e · GitHub

shapeFlag 可能是 ShapeFlags.ELEMENT 或者 ShapeFalgs.TELEPORT

这里先测试 ELEMENT 情况,因为 TELEPORT 还需要实现 components/Teleport 。

如果 type 是 对象, shapeFlag 初始类型会是 ShapeFlags.STATEFULL_COMPONENT, 1 << 2

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 源文件:/js/vue/lib.js
const {
  rc: { h, createVNode: c },
  f,
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");
const _h = (...args) => f(c(...args));

// 因为 type = {} , shapeFlag = 1 << 2, 4
// 所以在 normalizeChildren 里面 isObject 分支会进入 else
// 进行处理,经过处理之后成为 4 | SLOTS_CHILDREN,2<<5,32 = 36
log([">>> object\n", _h({}, null, { foo: "foo" })]);
>>> object
 {
  __v_isVNode: true,
  __v_skip: true,
  type: {},
  children: { foo: 'foo', _ctx: null },
  shapeFlag: 36
}
undefined

⏱ api watch(source, cb, options)

feat(add): api watch TODOs · gcclll/stb-vue-next@4f0301e · GitHub

脑图: /img/vue3/runtime-core/vue-runtime-core-api-watch.svg

为了更好的完成 apiWatch, 需要先完成了 scheduler 任务调度部分。

watch(source, cb, options) 函数以下种使用方式(下面的 cb 均可选参数):

  1. watch(fn) 等价于 watchEffect(fn), 无 cb

  2. watch(fn, cb) 监听函数

  3. watch(ref(0), cb)

  4. watch(reactive({ count: 0}), cb) , reactive 对象默认 deep = true

  5. watch([ref(0), reactive({count: 0})], cb)

  6. watch(fn, cb, { immediate: true }) 此时, cb 必须为函数, job->fn 被立即执 行一次, cb 接受新旧值

  7. watch(ref({ count: 0}), cb, { deep: true }) 手动指定 deep: true 深度监听

执行具体实现的函数: doWatch()

Argvaluedescription
sourceWatchSource, WatchSource[], WatchEffect, objectobject watched
cbWatchCallback or nullcallback
optionsWatchOptions = EMPTY_OBJ
immediate
deep
flush
onTrack
onTrigger
instancecurrentInstance-

watch(source, cb, options?) 函数中的 cb 是必选项,如果想直接 watch effect,可使 用 watchEffect(fn, options?) api 。

watch 函数基本流程:

  1. cb, immediate, deep 检测

  2. getter, 根据 source 不同类型设置 getter

  3. cb + deep: true

  4. SSR node env

  5. 将 cb 封装成 job

  6. runner = effect(getter, option)

  7. runner 如何执行?

  8. stop, remove,函数返回一个 stop+remove 该 runner 操作的函数

下面章节中测试的用例分析脑图: /img/vue3/runtime-core/vue-runtime-core-api-watch-tests.svg

source is ref

feat(add): apiWatch->no cb, getter is ref · gcclll/stb-vue-next@b9b7ac6 · GitHub

fix: watch->source is ref, cb -> job · gcclll/stb-vue-next@6752326 · GitHub 测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 源文件:/js/vue/lib.js
const {
  rc: { ref, nextTick, watch },
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");
const run = async () => {
  const count = ref(0);
  let dummy,
    i = 0;
  watch(count, (count, prevCount) => {
    log("\nvalue changed: " + i++);
    dummy = [count, prevCount];
    count + 1;
    if (prevCount) {
      prevCount + 1;
    }
  });
  count.value++;
  await nextTick();
  log(dummy);
};
run();
undefined
value changed: 0
1 0

有关代码(doWatch):

 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
// -> getter
let getter: () => any;
let forceTrigger = false;
// 2.1 source is ref
if (isRef(source)) {
  getter = () => (source as Ref).value;
  forceTrigger = !!(source as Ref)._shallow;
}

// cb -> job 封装
let oldValue = isArray(source) ? [] : INITIAL_WATCHER_VALUE;
const job: SchedulerJob = () => {
  if (cb) {
    // watch(source, cb)
    const newValue = runner();
    if (deep || forceTrigger || hasChanged(newValue, oldValue)) {
      // cleanup
      if (cleanup) cleanup();
      callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
        newValue,
        // pass undefined as the old value when it's changed for the first time
        // 第一次的时候 oldValue 为 undefined
        oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
        onInvalidate,
      ]);
      oldValue = newValue;
    }
  } else {
    // TODO
  }
};

// scheduler 封装
scheduler = () => {
  if (!instance || instance.isMounted) {
    queuePreFlushCb(job);
  } else {
  }
};

// 什么方式执行 runner?
// 8. TODO runner 如何执行?
if (cb) {
  if (immediate) {
    // TODO
  } else {
    oldValue = runner();
  }
} else if (false /*flush->post*/) {
} else {
  runner();
}

source is reactive

如果要 watch 的对象是个 reactive ,需要进行递归 watch ,得到 getter.

fix: watch->source is reactive · gcclll/stb-vue-next@697f7f2 · GitHub

新增相关代码:

 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
// 1. 如果是 reactive,需要深度监听
if (isReactive(source)) {
  getter = () => source;
  deep = true;
}

// 2. deep: true
if (cb && deep) {
  const baseGetter = getter;
  // a. deep: true
  // b. source is reactive
  getter = () => traverse(baseGetter());
}

// traverse 函数
function traverse(value: unknown, seen: Set<unknown> = new Set()) {
  if (!isObject(value) || seen.has(value)) {
    return value;
  }
  seen.add(value);
  if (isRef(value)) {
    traverse(value.value, seen);
  } else if (isArray(value)) {
    for (let i = 0; i < value.length; i++) {
      traverse(value[i], seen);
    }
  } else if (isSet(value) || isMap(value)) {
    value.forEach((v: any) => {
      traverse(v, seen);
    });
  } else {
    for (const key in value) {
      traverse(value[key], seen);
    }
  }
  return value;
}

递归监听 reactive 对象任意层级上的属性变化。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// 源文件:/js/vue/lib.js
const {
  rc: { nextTick, watchEffect, reactive, watch },
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");

const run = async () => {
  const state = reactive({ count: 0, r1: { count: 10 } });
  let dummy;
  watch(state, (newVal, preVal) => {
    dummy = [newVal, preVal];
  });
  state.count++;
  await nextTick();
  log.br(dummy);
  state.r1.count--
  await nextTick()
  log.br(dummy)
};
run();
undefined

{ count: 1, r1: { count: 10 } } { count: 1, r1: { count: 10 } }


{ count: 1, r1: { count: 9 } } { count: 1, r1: { count: 9 } }

注意: newVal 和 preVal 返回的是整个 state 而非当前所发生变更的属性 (count/r1.count),因为在 job 里面执行 runner() 得到新值是在 traverse(baseGetter()) 之前发生的,此时取到的值是 state 自身。

/img/tmp/20210115141244.png

soure is array

feat(add): apiWatch->source is array · gcclll/stb-vue-next@af1e590 · GitHub

如果要监听的对象是个数组的时候,需要检测数组元素的类型,针对不同类型进行处理。

要点:

  1. 数组元素不能是除 ref/reactive/function 之外的类型

  2. 对数组元素设值时必须通过元素原始设值方式进行(比如: ref 要 ref.value = xxx), 因为该数组本身不是 reactive 的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
if (isArray(source)) {
  getter = () =>
    source.map((s) => {
      if (isRef(s)) {
        return s.value;
      } else if (isReactive(s)) {
        return traverse(s);
      } else if (isFunction(s)) {
        return callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER);
      } else {
        // TODO warn invalid source
      }
    });
}
  1. isRef -> 监听 item.value

  2. isReactive -> traverse(item) 递归

  3. isFunction -> callWithErrorHandling(item, instance, …) 监听函数返回值

  4. 其他类型不支持 -> warn invalid source

测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 源文件:/js/vue/lib.js
const {
  rc: { ref, watch, nextTick, reactive },
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");

const run = async () => {
  const array = reactive([]);
  let dummy;
  watch(array, (newArr, preArr) => {
    dummy = [newArr, "\n"];
  });
  array.push(1);
  await nextTick();
  log.br(dummy);
};
run();
undefined

[ 1 ]

数组混合模式(元素只支持 ref, reactive, function):

 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
const {
  rc: { ref, watch, nextTick, reactive, effect },
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");

let dummy,
  val = reactive([10, 1]);
effect(() => {
  dummy = val[0];
});
val[0]++;
log(`dummy = ${dummy}\n`);

console.warn("---");
const run = async () => {
  const state = reactive({ count: 1 });
  const status = ref(false);
  let dummy;
  watch([() => state.count, status], (vals, oldVals) => {
    dummy = [vals, oldVals];
  });
  state.count++;
  status.value = true;
  await nextTick();
  log.br(dummy);
};
run();
dummy = 11

undefined
 [ [ 2, true ], [ 1, false ] ]

Tip. watch 数组的时候,需要通过数组元素原来的对象去操作值的变更,如果通过数组下 标设值是不会成功的,因为这个数组本身不是 reactive 的。

比如: array[0]++ 并不会改变 state.count

只有通过 state.count++ 自身赋值操作才会触发更新。

source is function

feat(add): rc->api watch->source is function · gcclll/stb-vue-next@694a389 · GitHub

当要 watch 的对象是个函数的时候,无论是否有 cb 最后的 getter 都是通过

callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)

或无 cb 时等价于普通的 effect 函数

callWithErrorHandling(source, instance,ErrorCodes.WATCH_CALLBACK,[onInvalidate])

直接执行这个函数去收集依赖。

/img/tmp/20210115180348.png

新增代码:

 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
if (isFunction(source)) {
  // 如果是函数,直接执行取得函数执行结果
  if (cb) {
    // getter with cb
    getter = () =>
      callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER);
  } else {
    // no cb -> simple effect
    getter = () => {
      if (instance && instance.isUnmounted) {
        // 组件已经卸载了
        return;
      }

      if (cleanup) cleanup();

      return callWithErrorHandling(
        source,
        instance,
        ErrorCodes.WATCH_CALLBACK,
        [onInvalidate]
      );
    };
  }
}

feat(add): rc->api watch->source is function without cb · gcclll/stb-vue-next@9565b4a · GitHub

测试:

 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
// 源文件:/js/vue/lib.js
const {
  rc: { nextTick, watchEffect, watch, ref },
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");

const run = async () => {
  let dummy,
    val = ref(0);
  watch(() => (dummy = val.value));
  val.value++;
  await nextTick();
  log.br({ dummy });

  log("with cb\n");
  // function with cb
  watch(
    () => val.value,
    (val, oldVal) => {
      dummy = [val, oldVal];
    }
  );
  val.value = 100;
  await nextTick();
  log([dummy, "\n"]);
};
run();
undefined

{ dummy: 1 }
with cb

[ 100, 1 ]

feat(add): rc->api watch->source invalid warning · gcclll/stb-vue-next@11ee8ef · GitHub

  1. 这里有个容易搞混淆的地方, watch(fn, cb) 的时候,虽然 fn 和 cb 都是函数,但 是要区分开这两者,并搞清楚他们是啥和关系是啥。

    1. fn 是被检测的对象,如果是 function 那在被监听之前需要先执行它,等于是监听 函数里面的内容,比如:函数内有访问某个 reactive 变量

    2. 而 cb 是属于回调性质,且是当数据有更新的时候的回调函数,它只会在一个地方被 执行,即封装 job 的时候,需要将数据更新前后的变化值通过它传递出来(如下面👇的 代码)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
const job: SchedulerJob = () => {
  if (cb) {
    // watch(source, cb)
    const newValue = runner();
    if (deep || forceTrigger || hasChanged(newValue, oldValue)) {
      // cleanup
      if (cleanup) cleanup();
      callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
        newValue,
        // pass undefined as the old value when it's changed for the first time
        // 第一次的时候 oldValue 为 undefined
        oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
        onInvalidate,
      ]);
      oldValue = newValue;
    }
  } else {
    // watchEffect, no cb
    runner();
  }
};

option deep

对于深度监听主要是因为 traverse() 函数对 reactive 对象进行了递归遍历,对每个属 性进行了访问,从而让它收集到当前的 effect 作为依赖,这样将来这些被遍历到的值发生 改变时就会触发这个收集到的 effect 执行,达到深度监听效果。

1
2
3
4
5
6
7
8
 // 3. cb + deep: true
 if (cb && deep) {
   const baseGetter = getter;
   // a. deep: true

   // b. source is reactive
   getter = () => traverse(baseGetter());
 }

traverse() 作用就是递归遍历所有属性通过 return value 来执行 get 操作收集依赖。

测试:

 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
// 源文件:/js/vue/lib.js
const {
  rc: { ref, reactive, watch, nextTick },
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");

const run = async () => {
  const count = ref(0);

  const state = reactive({
    nested: { count },
    array: [1, 2, 3],
    map: new Map([
      ["a", 1],
      ["b", 2],
    ]),
    set: new Set([1, 2, 3]),
  });

  let dummy;
  watch(
    () => state,
    (state) => {
      dummy = [
        state.nested.count,
        state.array[0],
        state.map.get("a"),
        state.set.has(1),
      ];
    },
    { deep: true }
  );

  state.nested.count++;
  await nextTick();
  log(["\n", dummy]);

  state.array[0] = 2;
  await nextTick();
  log(["\n", dummy]);

  state.map.set("a", 100);
  await nextTick();
  log(["\n", dummy]);

  state.set.delete(1);
  await nextTick();
  log(["\n", dummy]);
};
run();
undefined
 [ 1, 1, 1, true ]

 [ 1, 2, 1, true ]

 [ 1, 2, 100, true ]

 [ 1, 2, 100, false ]

TODO deep ref

option immediate

feat(add): rc->api watch->immediate option · gcclll/stb-vue-next@204ce68 · GitHub

immediate 选项,会让 cb/job 立即执行一次,而不是在队列中等待异步执行。

新增代码只需要加一行:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
if (cb) {
  if (immediate) {
    job(); // 这里直接调用 Job
  } else {
    oldValue = runner();
  }
} else if (false /*flush->post*/) {
} else {
  runner();
}

测试:

 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
// 源文件:/js/vue/lib.js
const {
  rc: { nextTick, watch, ref },
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");

const run = async () => {
  const _log = (desc, newline) =>
    log([newline ? "\n" : "", `${desc} > dummy = ${dummy}`]);
  const cb = (val) => (dummy = val);
  const option = { immediate: true };

  const count = ref(0);
  let dummy;
  watch(count, cb, option);

  _log("改变值之前");
  count.value++;
  await nextTick();
  _log("改变值之后", true);

  const nul = ref(null);
  watch(() => nul.value, cb, option);
  _log("当初始值为 null");

  const undef = ref();
  watch(() => undef.value, cb, option);
  _log("当初始值为 undefined");
  undef.value = 3;
  await nextTick();
  _log("当初始值为 undefined, set 3");
  undef.value = undefined;
  await nextTick();
  _log("当初始值为 undefined, set undefined");
  // undefined === undefined -> hasChanged() -> false
  undef.value = undefined;
  await nextTick();
  _log("当初始值为 undefined, set undefined");
};
run();
 改变值之前 > dummy = 0
undefined
 改变值之后 > dummy = 1
 当初始值为 null > dummy = null
 当初始值为 undefined > dummy = undefined
 当初始值为 undefined, set 3 > dummy = 3
 当初始值为 undefined, set undefined > dummy = undefined
 当初始值为 undefined, set undefined > dummy = undefined

如上结果, cb 会立即执行。

在使用 deep 和 immediate 选项的时候如果没有 cb 会给出警告,直接看源码吧:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 1. cb, immediate, deep 检测
if (__DEV__ && !cb) {
  if (immediate !== undefined) {
    warn(
      `watch() "immediate" option is only respected when using the ` +
        `watch(source, callback, options?) signature.`
    );
  }
  if (deep !== undefined) {
    warn(
      `watch() "deep" option is only respected when using the ` +
        `watch(source, callback, options?) signature.`
    );
  }
}

也就是说, deepimmediate 建议在 watch(s, cb, options) 形式下使用,即在 有 cb 参数的情况下使用。

那为什么呢?

option onTrack + onTrigger

这部分实现逻辑主要在 reactivity 模块。

onTrack 在 reactivity 中使用的,用来在触发 get 取值操作时调用 track() 函数收集依 赖时的一个自定义事件回调。

 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
// track() 函授最后 add 操作之后
if (!dep.has(activeEffect)) {
  dep.add(activeEffect);
  // 自身保存一份被依赖者名单
  activeEffect.deps.push(dep);
  if (__DEV__ && activeEffect.options.onTrack) {
    activeEffect.options.onTrack({
      effect: activeEffect,
      target,
      type,
      key,
    });
  }
}

// trigger() 函数中实现
if (effect.options.onTrigger) {
  effect.options.onTrigger({
    effect,
    target,
    key,
    type,
    newValue,
    oldValue,
    oldTarget,
  });
}

这里会将 当前 target 的 key 属性所收集的依赖 activeEffect 暴露出来。

测试:

 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
// 源文件:/js/vue/lib.js
const {
  rc: { nextTick, watchEffect, reactive },
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");

const run = async () => {
  const trackEvents = [];
  const triggerEvents = [];
  let dummy;
  const onTrack = (e /* activeEffect */) => trackEvents.push(e);
  const onTrigger = (e /* effect */) => triggerEvents.push(e);
  const obj = reactive({ foo: 1, bar: 2 });
  watchEffect(
    () => {
      dummy = [obj.foo, "bar" in obj, Object.keys(obj)];
    },
    { onTrack, onTrigger }
  );

  await nextTick();
  log(["\n", dummy]);
  // 有多少个就等于呗调用了多少次
  log("track events count = " + trackEvents.length);
  trackEvents.forEach((e) => log.props(e, ["target", "type", "key", "deps"]));

  obj.foo = 3;
  obj.bar = 4;
  log("trigger events count = " + triggerEvents.length);
  triggerEvents.forEach((e) =>
    log.props(e, ["type", "key", "oldValue", "newValue"])
  );
};
run();
undefined
 [ 1, true, [ 'foo', 'bar' ] ]
track events count = 3
{ target: { foo: 1, bar: 2 }, type: 'get', key: 'foo' }
{ target: { foo: 1, bar: 2 }, type: 'has', key: 'bar' }
{ target: { foo: 1, bar: 2 }, type: 'iterate', key: Symbol(iterate) }
trigger events count = 2
{ key: 'foo', type: 'set', newValue: 3, oldValue: 1 }
{ key: 'bar', type: 'set', newValue: 4, oldValue: 2 }

这里还需要开发环境才能测试 onTrack,只能改一改去掉 __DEV__ 试试。

stop & cleanup

stop: watch() 的返回值,用来停掉 effect 使其 effect.active = false,让 effect 失效。

1
2
3
4
5
6
7
// 9. return runner->stop, remove runner from instance.effects
return () => {
  stop(runner);
  if (instance) {
    remove(instance.effects!, runner);
  }
};

cleanup: 清理工作,这有两个被调用的地方(cleanup + onStop它们被注册了同一个函数), 一个是调动 cb/fn 之前,一个是 runner effect 调用 stop 的时候。

1
2
3
4
5
6
let cleanup: () => void;
const onInvalidate: InvalidateCbRegistrator = (fn: () => void) => {
  cleanup = runner.options.onStop = () => {
    callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP);
  };
};

stop

stop 是 watch 调用的返回值,里面会 stop runner 然后将 runner 从 instance.effects 里面删除。

 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
// 源文件:/js/vue/lib.js
const {
  rc: { reactive, nextTick, watch },
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");

const run = async () => {
  const state = reactive({ count: 0 });
  let dummy;
  const stop = watch(
    () => state.count,
    (count) => {
      dummy = count;
    }
  );

  state.count++;
  await nextTick();
  log.br({ dummy });

  stop();
  state.count = 100;
  await nextTick();
  log({ dummy });
};
run();
undefined
 { dummy: 1 }
{ dummy: 1 }

可以看到 stop 之后两次输出结果是一样,即 stop 后面的 state.count 失效了,因为 stop effect 会将 effect.active 置为 false ,有如下代码被执行:

1
2
3
4
// reactivity/src/effect.ts -> createReactiveEffect()
if (!effect.active) {
  return options.scheduler ? undefined : fn();
}

又, watch 函数里面无论如何 scheduler 都是有值的,所以当 effect 为非激活状态,什 么都不会干。

cleanup(无 cb)

cleanup 相关源码,可能有点绕:

 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
// cleanup 和注册 cleanup 的一个函数
// 如下,cleanup 和 effect onStop 是同一个函数,清理 effect 用
let cleanup: () => void;
const onInvalidate: InvalidateCbRegistrator = (fn: () => void) => {
  cleanup = runner.options.onStop = () => {
    callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP);
  };
};

// runtime-core/src/apiWatch.ts:watch(source, cb, option)
// Job 封装中和 cleanup 有关的
const job: SchedulerJob = () => {
  if (!runner.active) {
    return;
  }
  if (cb) {
    // watch(source, cb)
    const newValue = runner();
    if (deep || forceTrigger || hasChanged(newValue, oldValue)) {
      // cleanup,在执行 cb 之前先执行 cleanup
      if (cleanup) cleanup();
      // call cb with catch error
      // 这里等价于 cb(newValue, oldValue, onInvalidate)
      oldValue = newValue;
    }
  } /* else... */
};

// 然后还有个地方与 cleanup 有关,且这里要讲到的内容会在这部分执行
// 获取 getter函数时候,如果 source 是函数等价于
// watchEffect(source)
if (isFunction(source)) {
  // 如果是函数,直接执行取得函数执行结果
  if (cb) {
    // ...
  } else {
    // no cb -> simple effect
    getter = () => {
      if (instance && instance.isUnmounted) {
        // 组件已经卸载了
        return;
      }

      if (cleanup) cleanup();
      // 等价于 return source(onInvalidate)
    };
  }
}

测试:

 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
// 源文件:/js/vue/lib.js
const {
  rc: { reactive, nextTick, watchEffect },
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");

const run = async () => {
  const state = reactive({ count: 0 });
  let n = 0;
  const cleanup = () => log(`\ncalled ${++n} times.`);
  let dummy;
  const stop = watchEffect((onCleanup) => {
    // 这里执行的实际上是 onInvalidate 函数,将cleanup 封装后注册到
    // cleanup 和 onStop 上,在 cb 执行之前或 effect stop 时候调用
    onCleanup(cleanup);
    dummy = state.count;
  });

  state.count++;
  await nextTick();
  // 这里会输出一次 'called 1 times.'
  // 因为 cb 之前之前进行了清理工作(cleanup())
  log.br({ dummy });

  // 这里会输出一次 'called 2 times.'
  // 这里是 effect stop 的 onStop 触发的
  stop();
};
run();
undefined
called 1 times.

 { dummy: 1 }

called 2 times.

即. 如果想在 effect fn 之前或停止的时候进行清理工作,可以使用 watchEffect(effect) 的参数 effect 函数的第一个参数来注册 一个函数作为清理工作 或做其他事情。 如: watchEffect((onCleanup) => { onCleanup(cleanup) ... }

cleanup(有 cb)

当有 cb 的时候: watch(source, cb, ...) ,将 onCleanup 注册函数从 cb 的第三个参数暴露出来

 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
// 源文件:/js/vue/lib.js
const {
  rc: { nextTick, watch, ref },
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");

const run = async () => {
  const count = ref(0);
  let n = 0;
  const cleanup = () => log(`\ncalled ${++n} times, dummy = ${dummy}`);
  let dummy;
  const stop = watch(count, (newVal, oldVal, onCleanup) => {
    onCleanup(cleanup);
    dummy = newVal;
  });
// 这里 cleanup 尚不会执行
// 因为第一次执行是注册 cleanup 行为
  count.value++;
  await nextTick();

// 这里会执行一次 cleanup ,因为第一次赋值时注册过了
  count.value = 100;
  await nextTick();

// stop 时候执行一次,所以总共会执行两次 cleanup, n = 2
  stop();
  log({n})
};
run();
undefined
called 1 times, dummy = 1

called 2 times, dummy = 100
{ n: 2 }

脑图分析:

/img/vue3/runtime-core/vue-runtime-core-api-watch-cleanup.jpg

文字分析:

  1. cleanup 注册时机分为两种情况

    • 一是无 cb 的 watchEffect(fn) ,是在 getter 设置阶段封装到 getter 函数里面 注册的,此时作为 fn 的第一个参数暴露出来 fn(onCleanUp)

    • 二是有 cb 的 watch(ref(0), cb) , 在 job 封装期间在调用 cb 的时候注册,此 时作为 cb 的第三个参数暴露出来 cb(newVal, oldVal, onCleanup)

    两者区别 : watchEffect 由于无 cb 会立即执行一次 runner, 此时就收集到了 cleanup, 而 watch 有 cb 时则是会在第一次值更新触发 runner 执行才开始收集 cleanup。

  2. 执行时机,该阶段和注册时机相辅相成,且在 cb/fn 执行之前就会被执行,因此 cb/fn 的第一次执行都属于对 cleanup 的注册

flush sync

feat(add): rc->api watch->option flush=sync · gcclll/stb-vue-next@e1436f2 · GitHub

支持同步代码,即所有任务立即执行(在值发生改变之后),而不是进入队列异步执行。

只需要增加一行代码就行:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 6. TODO scheduler 设置
let scheduler: ReactiveEffectOptions["scheduler"];
// 6.1 flush is 'sync'
if (flush === "sync") {
  scheduler = job;  // 新增
}
// 6.2 TODO flush is 'post'
else if (false /* post */) {
}
// 6.3 TODO flush is 'pre'(default)
else {
  // default: 'pre'
  scheduler = () => {
    if (!instance || instance.isMounted) {
      queuePreFlushCb(job);
    } else {
      // 带 { pre: true } 选项,第一次调用必须发生在组件 mounted 之前
      // 从而使他被同步调用,立即执行一次
      job();
    }
  };
}

新增 scheduler = job 直接让任务函数赋值给调度器,这个时候如果有值发生变化,会 触发 effect> trigger() 在这里面会检测是不是有 option.scheduler 如果有会立即执行这 个函数。

1
2
3
4
5
6
// reactivity/effect.ts>trigger()
if (effect.options.scheduler) {
  effect.options.scheduler(effect);
} else {
  effect();
}

测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 源文件:/js/vue/lib.js
const {
  rc: { watch, ref },
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");

const run = async () => {
  const value = ref(0);
  let calls = 0;
  watch(value, () => ++calls, { flush: "sync" });

  log({ calls }); // -> 0
  value.value = 100;
  log({ calls }); // -> 1
};
run();
{ calls: 0 }
{ calls: 1 }
undefined

注意看上面的测试用例并没有用 await nextTick() ,而是同步代码执行。

shallow ref

 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
// 源文件:/js/vue/lib.js
const {
  rc: { watch, shallowRef, nextTick, triggerRef },
  f,
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");

const run = async () => {
  const v = shallowRef({ a: 1 }); // #1
  let sideEffect = 0;
  watch(v, (obj) => { // #2 cb -> cb(newVal, oldVal, onCleanUp)
    sideEffect = obj.a;
  });

  v.value = v.value; // #3
  await nextTick();
  log(["\nshould not trigger: ", sideEffect]); // #4

  v.value.a++; // #5
  await nextTick();
  log(["\nshould not trigger: ", sideEffect]); // #6

  triggerRef(v); // #7
  await nextTick();
  log(["\nshould trigger now: ", sideEffect]); // #8
};
run();
undefined
should not trigger:  0

should not trigger:  0

should trigger now:  2

ref 这一块还没深入去分析过,先暂停⏸去完成下这部分。

  1. DONE [2021-01-20 15:18:37] ref 完成,可以往下继续了

triggerRef 作用是手动触发 ref.value 上收集的所有依赖。

结果分析:

  1. #1 shallowRef 意味着 {a: 1} 中的属性 a 非 reactive

  2. #2 watch v 基于 1 所以只是对 ref value 进行了监听,后面是值变更回调

  3. #3 值没发生改变,所有 #4 输出还是 0

  4. #5 由于 a 属性非 reactive 所以它没有依赖收集所以不会执行 cb,所以 #6 出 依然是 0

  5. #7 这里手动调用 triggerRef(v) 等价于 trigger(v, SET, 'value') 触发 ref value 的依赖执行,此时 cb 会得到执行,sideEffect 被赋值新的 v.a

    这里有一点需要注意,在 cb 里面是用的 v.a 而不是 v.value.a 因为在 watch(s,cb,option) 里面检测到如果 s 是 ref 类型,会将 getter 设置为 getter = () => s.value

    而在执行 cb 之前取新值是通过 newVal = runner() 得到的,而这个 runner = effect(getter, {...}) 所以等于是 effect(() => s.value, {...})

    所以对于 ref 类型 effect 封装的其实是 () => s.value 这个函 数,那么对于 s.value 的依赖列表中就会有这个箭头函数。

    然后在 watch 里面会将 cb 的执行封装进 job ,然后根据情况将 job 封装或直接赋值 给 scheduler ,这个会作为 effect(, { scheduler }) 的选项传递进去。

    那么在 trigger 的时候检测到提供了 scheduler 就会调用它,所以最终调用 triggerRef(v) 会触发 cb 的调用将 obj.a~复制给 ~sideEffect ,这个 obj 就是 runner() 执行的返回值也就是 () => s.value 这个函数执行的返回值。

TODO flush pre(default) cb

TODO flush post cb

feat(add): rc->watch->flush = 'post' · gcclll/stb-vue-next@ec14879 · GitHub

feat(add): rc->watch->cb->flush = 'post' · gcclll/stb-vue-next@9f3ac7d · GitHub

新增代码:

 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
// apiWatch.ts -> scheduler when flush=post
if (flush === "post") {
  scheduler = () => queuePostRenderEffect(job, instance && instance.suspense);
}

// renderer.ts -> queuePostRenderEffect
// 将任务加入到 suspense.effects 或 调用 queuePostFlushCb
// 加入到 pendingPostFlushCbs
export const queuePostRenderEffect = __FEATURE_SUSPENSE__
  ? queueEffectWithSuspense
  : queuePostFlushCb;

// components/Suspense.ts
export function queueEffectWithSuspense(
  fn: Function | Function[],
  suspense: SuspenseBoundary | null
): void {
  if (suspense && suspense.pendingBranch) {
    if (isArray(fn)) {
      suspense.effects.push(...fn);
    } else {
      suspense.effects.push(fn);
    }
  } else {
    queuePostFlushCb(fn);
  }
}

TODO ssr support

TODO instance watch

feat(add): rc->watch->instance watch · gcclll/stb-vue-next@9ec5d51 · GitHub

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// this.$watch
export function instanceWatch(
  this: ComponentInternalInstance,
  source: string | Function,
  cb: WatchCallback,
  options?: WatchOptions
): WatchStopHandle {
  const publicThis = this.proxy as any
  const getter = isString(source)
    ? () => publicThis[source]
    : source.bind(publicThis)
  return doWatch(getter, cb.bind(publicThis), options, this)
}

将监听源,与当前实例绑定,如果是字符串转成函数。

🍺 小结

这一节所描述的主要是 watch/watchEffect 函数的实现和使用,用来监听数据变化做出相 应。

watch(source, cb, option) -> doWatch(source, cb, option)

watchEffect(effect, option) -> doWatch(source /*function*/, null, option)

重点回顾 watch ,因为 watchEffect 是在 source 为 function 且无 cb 时候的情况。

使用方法,我们可以按照 watch 函数的实现内容来逐个回顾,函数实现代码有三个重点

注🐖:watch 里面函数的调用(source 或 cb) 都是通过 callWithErrorHandling 去完成的, 这里其实就是个异常拦截的作用(try…catch),防止执行报错阻碍整体页面的执行。

  1. getter: 根据 source 类型封装 getter

    Ref : getter = () => source.value

    Reactive: deep = true, traverse(source) 对对象所有属性触发一次 getter 操作。

    Array: 根据元素类型不同做不同处理(比如: ref/reactive/function 且只支持这 三种)

    Function: getter => { …直接执行 source } 不同点在于如果有 cb 时,执行 source() ,没有时 source(onInvalidate) 这个 onInvalidate 是 watch 暴露出 去的一个清理函数它会被绑定到 cleanupoption.onStop 上,在 effect 被 stop 或在调用之前做的一些清理工作。

    这个最终目的是为了对每个属性进行一次 getter 调用,用来 effect -> track 收集每 个属性的依赖列表(reactivity: targetMap -> depsMap -> dep)。

  2. job: 根据 cb 参数封装 job(将来在值变更是触发执行的函数)

    job 的封装分为两种情况,分别是 cb 是函数或为空值时候,这里的区别也是体现在 cb 的调用上面(具体就是给 cb 的参数)。

    if(cb) 那么其调用参数会是: cb(newVal, oldVal, onCleanup/*onInvalidate*/)

    else 没有 cb 的时候会直接执行 runner() 触发更新。

    注意到和 1 中有点类似地方了没,这里的 cb(…) 和 getter 封装阶段对 source(onInvalidate) 的调用,即不管是有 cb 还是没有 cb, onInvalidate 这个 回调总是会被暴露出去,无非是作为 cb 参数还是 function source 的参数,这使得我 们可以在每个任务执行之前和执行结束的时间点做一些事情(比如: cleanup)。

  3. scheduler 的封装(根据选项类型,主要是 flush = sync|post|pre),值变更 effect trigger 中调用的函数

    scheduler 是将 job 进一步封装,将来当值发生变化的时候 effect -> trigger 里面 会被调用到。

    这个函数的值主要由 flush 选项决定,默认值是 pre ,其次有 post|sync

    post: 异步执行,会添加到异步任务队列中等待执行(pendingPostFlushCbs)

    sync: 表示为同步更新,即当值发生变化了会立即更新,详情

    1
    2
    3
    
    // 比如:ref.value = 0
    ref.value++;
    ref.value === 1; // true, 而不用 await nextTick() 就可以取到更新后的值
    

    pre: 默认是 pre 类型,这里区分组件是否加载完成, instance.isUnmounted 如果 加载完成,调用 queuePreFlushCb(job) 添加到 pendingPreFlushCbs[] 中等待执 行,如果组件没有加载完成需要立即执行 job()

在前面三个基本条件(getter/job/scheduler)封装完成之后,接下来是调用 effect(getter, { lazy: true, scheduler, ... }) 得到 runner() 这个 runner 是 对 getter 函数的 ReactiveEffect 封装。

runner: 随后决定什么时机执行这个 runner() 触发 track 收集依赖,判定条件有 cb, option.immediate, option.flush="post"

  1. cb && immediate 时候立即执行 job() 不走 runner

  2. cb && !immediate 立即执行 runner() 根据任务性质决定是同步还是异步

  3. !cb && flush==='post' 检测如果是 suspense 加入到 instance.suspense.effects 否则直接 queuePostFlushCb(runner)

    这个需要开启 Suspense 组件支持,这个 ast render 函数其实就是在一个 async 函数里面执行的代码。

  4. else 直接执行 runner()

🍎 api createApp

declaration

这里就两个函数

  1. createAppContext 上下文创建

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
     export function createAppContext(): AppContext {
       return {
         app: null as any,
         config: {
           isNativeTag: NO,
           performance: false,
           globalProperties: {},
           optionMergeStrategies: {},
           isCustomElement: NO,
           errorHandler: undefined,
           warnHandler: undefined,
         },
         mixins: [],
         components: {},
         directives: {},
         provides: Object.create(null),
       };
     }
    
  2. createAppAPI 创建 createApp 函数

    1
    2
    3
    4
    5
    6
    
    export function createAppAPI<HostElement>(
      render: RootRenderFunction,
      hydrate?: RootHydrateFunction
    ): CreateAppFunction<HostElement> {
      return function createApp(rootComponent, rootProps = null) {};
    }
    

所以一个 App 上下文包含内容:

name--
app-
config-
isNativeTagNO
performancefalse
globalProperties{}
optionMergeStrategies{}
isCustomElementNO
errorHandlerundefined
warnHandlerundefined
mixins[]
components{}
directives{}
providesObject.create(null)

implementation

feat(add): createApp app apis · gcclll/stb-vue-next@1facd1b · GitHub

  1. context = createAppContext() 创建上下文

  2. installedPlugins = new Set() 插件列表

  3. isMounted = false 加载完成标识

  4. app = context.app = {...}

  5. return app

所以重点就在 4 构造 app,其包含的 API 如下:

namefunction
get config()获取上下文配置信息,不可更改,可以通过创建实例时的 option 改变
use(pugin, …options)插件系统
mixin(mixin)混合器系统
component(name, component)组件系统
directive(name, directive)指令系统
mount(rootContainer, isHydrate)mount 函数
unmount()-
provide(key, value)注入系统

app.use(plugin, …options)

feat(add): createApp use plugin · gcclll/stb-vue-next@a3abcba · GitHub

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
function use(plugin: Plugin, ...options: any[]) {
  if (installedPlugins.has(plugin)) {
    __DEV__ && warn(`Plugin has already been applied to target app.`);
  } else if (plugin && isFunction(plugin.install)) {
    // 函数直接执行
    installedPlugins.add(plugin);
    plugin.install(app, ...options);
  } else if (isFunction(plugin)) {
    // 没有 install 函数时
    installedPlugins.add(plugin);
    plugin(app, ...options);
  } else if (__DEV__) {
    // plugin 必须要么自己是函数,要么是包含 install 函数的对象
    warn(
      `A plugin must either be a function or an object with an "install" ` +
        `function.`
    );
  }
  return app;
}

所以插件 plugin 必须要么自己是函数。

app.mixin(mixin)

feat(add): createApp mixin api · gcclll/stb-vue-next@b96afcf · GitHub

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
function mixin(mixin: ComponentOptions) {
  if (__FEATURE_OPTIONS_API__) {
    if (!context.mixins.includes(mixin)) {
      context.mixins.push(mixin);
      // 带有 props/emits 的全局 mixin 会是的 props/emits 缓存优化失效
      if (mixin.props || mixin.emits) {
        context.deopt = true;
      }
    } else if (__DEV__) {
      warn(
        "Mixin has already been applied to target app" +
          (mixin.name ? `: ${mixin.name}` : "")
      );
    }
  } else if (__DEV__) {
    // 必须开启了 options api 特性才能使用,即 vue3 里面可根据需要
    // 关闭这个功能
    warn("Mixins are only available in builds supporting Options API");
  }
  return app;
}

可根据需要在打包的时候由 __FEATURE_OPTIONS_API__ 决定是否继续支持 mixin 功能。

app.component(name, component)

feat(add): createApp component api · gcclll/stb-vue-next@9b2579d · GitHub

内置标签: slot,component

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
function component(name: string, component?: Component): any {
  if (__DEV__) {
    // 验证组件名称是否合法
    validateComponentName(name, context.config);
  }

  // 没有对应组件,视为根据名称获取组件操作
  if (!component) {
    return context.components[name];
  }

  if (__DEV__ && context.components[name]) {
    // 组件已经注册过了,再次注册等于覆盖原有的
    warn(`Component "${name}" has already been registered in target app.`);
  }

  context.components[name] = component;
  return app;
}
  1. 验证组件名,不能用内置(isBuiltInTag())或自定义(isCustomElement())的标签作为组件名

  2. 没有 component 参数的时候视为根据 name 去获取组件

  3. 当 name - component 存在时属于覆盖操作,给出警告

app.directive(name, directive)

feat(add): createApp directive api · gcclll/stb-vue-next@ec1a20e · GitHub

内置指令集: bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
function directive(name: string, directive?: Directive) {
  if (__DEV__) {
    validateDirectiveName(name);
  }

  if (!directive) {
    return context.directives[name] as any;
  }

  if (__DEV__ && context.directives[name]) {
    warn(`Directive "${name}" has already been registered in target app.`);
  }
  context.directives[name] = directive;

  return app;
}

app.mount(rootContainer, isHydrate)

 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 mount(rootContainer: HostElement, isHydrate?: boolean): any {
  // TODO
  if (!isMounted) {
    const vnode = createVNode(rootComponent as ConcreteComponent, rootProps);

    // 保存 app context 到 root VNode 节点上
    // 这个将会在初始化 mount 时候被设置到根实例上
    vnode.appContext = context;

    // HMR root reload
    if (__DEV__) {
      context.reload = () => {
        render(cloneVNode(vnode), rootContainer);
      };
    }

    if (isHydrate && hydrate) {
      hydrate(vnode as VNode<Node, Element>, rootContainer as any);
    } else {
      render(vnode, rootContainer);
    }

    isMounted = true;
    app._container = rootContainer;
    // for devtools and telemetry
    (rootContainer as any).__vue_app__ = app;

    if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
      devtoolsInitApp(app, version);
    }

    return vnode.component!.proxy;
  } else if (__DEV__) {
    warn(
      `App has already been mounted.\n` +
        `If you want to remount the same app, move your app creation logic ` +
        `into a factory function and create fresh app instances for each ` +
        `mount - e.g. \`const createMyApp = () => createApp(App)\``
    );
  }
}
  1. vnode = createVNode(rootComponent, rootProps) 创建虚拟节点

  2. vnode.appContext = context 保存上下文到虚拟节点上,组件初始化时使用

  3. context.reload = () => render(cloneVNode(vnode), rootContainer) HMR 开发时 有变更的热加载

  4. rootContainer.__vue_app__ = app

  5. devtool 开发工具相关

  6. 返回 vnode.component.proxy 这个是干啥的?

最后,如果 app 已经被加载了不能重复 mount。如果需要对一个app做重复 mount,可能的 需求是存在同时创建多个 app 情况?

那么这个时候应该使用函数方式使用,如:

myCreateApp = (...) => createApp(App)

app.unmount()

feat(add): createApp unmount api · gcclll/stb-vue-next@63354d3 · GitHub

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
function unmount() {
  if (isMounted) {
    render(null, app._container);
    if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
      devtoolsUnmountApp(app);
    }
  } else {
    warn(`Cannot unmount an app that is not mounted.`);
  }
}

render(null, ...) ?

app.provide(key, value)

feat(add): createApp provide api · gcclll/stb-vue-next@18fb22b · GitHub

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
function provide(key, value) {
  if (__DEV__ && (key as string | symbol) in context.provides) {
    warn(
      `App already provides property with key "${String(key)}". ` +
        `It will be overwritten with the new value.`
    );
  }

  // TypeScript doesn't allow symbols as index type
  // https://github.com/Microsoft/TypeScript/issues/24587
  context.provides[key as string] = value;

  return app;
}

就是简单的给 context.provides 设置操作。

  1. 做什么的呢?

test

这里目前只能测试 mount 和 unmount 其他的 api(provide/…) 需要等它们被实现了才能 继续测试。

 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
// 源文件:/js/vue/lib.js
const {
  rcTest: { defineComponent, nodeOps, createApp, serializeInner },
  f,
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");

const Comp = defineComponent({
  props: {
    count: {
      default: 0,
    },
  },
  setup(props) {
    return () => props.count;
  },
});

const root1 = nodeOps.createElement("div");
createApp(Comp).mount(root1);
let si = serializeInner(root1);
log("root1 serialize, " + si);

// mount with props
const root2 = nodeOps.createElement("div");
const app2 = createApp(Comp, { count: 1 });
app2.mount(root2);
si = serializeInner(root2);
log("root2 serialize, " + si);

// unmount
const root3 = nodeOps.createElement("div");
const app3 = createApp(Comp, { foo: 1 });

// 在 mount 之前卸载
try {
  app3.unmount(root3); // warnning
} catch (e) {
  log("unmount failed.");
}

app3.mount(root3);
app3.unmount(root3);
si = serializeInner(root3);
log("root3 serialize, " + si);
root1 serialize, 0
root2 serialize, 1
root3 serialize,
undefined

⛏ api provide & inject

feat(add): provide & inject api · gcclll/stb-vue-next@b4a1cbc · GitHub

作用: 在父组件中 provide(key,value) 向子组件传值, inject(key) 在子组件中可以取到该值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
export function provide<T>(key: InjectionKey<T> | string | number, value: T) {
  // TODO
}

export function inject(
  key: InjectionKey<any> | string,
  defaultValue?: unknown,
  treatDefaultAsFactory = false
) {
  // TODO
}

涉及内容

  1. provides 继承 parent provides,需要自己的就创建新对象继承自 parent provides, 所以查找 injections 可以在原型链查找。

  2. inject(key, defaultValue, treatDefaultAsFactory = false) 你只是个简单的取值 操作 (provides[key])?😭?😭?

Imp. provide 和 inject 只能在 setup() 或函数式组件中使用,它们的作用是根据原 型链特性实现从父组件向子组件传递一些值。

/img/vue3/runtime-core/vue-runtime-core-inject-provide.jpg

provide(key, value)

feat(add): provide & inject provide implementation · gcclll/stb-vue-next@42f3d05 · GitHub

 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
export function provide<T>(key: InjectionKey<T> | string | number, value: T) {
  if (!currentInstance) {
    if (__DEV__) {
      warn(`provide() can only be used inside setup().`);
    }
  } else {
    let provides = currentInstance.provides;

    // 默认情况实例会继承它父亲的 provides 对象
    // 但是当它需要 provide 自己的 values 时候,那么使用它
    // 父亲的 provides 作为原型创建一个新的对象出来变成自己的 provides
    // 这样在 `inject` 里面可以简便的从原型链中查找 injections
    // 简单说就是:
    // 1. 需要自己的就创建个新的对象继承自 Parent provides
    // 2. 这样在查找的时候就可以含方便的通过原型链查找 injections
    const parentProvides =
      currentInstance.parent && currentInstance.parent.provides;

    // 当父组件和当前实例相同的时候,从父组件的 provides 创建一个备份出来
    if (parentProvides === provides) {
      provides = currentInstance.provides = Object.create(parentProvides);
    }

    // TS doesn't allow symbol as index type
    provides[key as string] = value;
  }
}

Object.create(parentProvides) 创新新的对象来容纳新的 key-value

inject(key, defaultValue, treatDefaultAsFactory = false)

 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
export function inject(defaultValue?: unknown, treatDefaultAsFactory = false) {
  // TODO
  // currentRenderingInstance 兼容函数式组件
  const instance = currentInstance || currentRenderingInstance;
  if (instance) {
    // #2400
    // to support `app.use` plugins,
    // fallback to appContext's `provides` if the intance is at root
    const provides =
      instance.parent == null // root
        ? instance.vnode.appContext && instance.vnode.appContext.provides
        : instance.parent.provides;

    if (provides && (key as string | symbol) in provides) {
      // TS doesn't allow symbol as index type
      return provides[key as string];
    } else if (arguments.length > 1) {
      return treatDefaultAsFactory && isFunction(defaultValue)
        ? defaultValue()
        : defaultValue;
    } else if (__DEV__) {
      warn(`injection "${String(key)}" not found.`);
    }
  } else if (__DEV__) {
    warn(`inject() can only be used inside setup() or functional components.`);
  }
}

inject 就是个简单的取值操作而已。

取值来源需要检测是不是根节点,如果是根节点

provides = instance.vnode.appContext.provides

否则,使用 instance.parent.provides

然后根据 key 取出值 provides[key]

如果有默认值需要返回默认值?

defaultValuedefaultValue()

居然如此简单~~~,还是先看下怎么用吧!!!

test

 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
// 源文件:/js/vue/lib.js
const {
  rcTest: { provide, inject, h, nodeOps, render, serialize, ref, nextTick },
  f,
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");

const run = async () => {
  const skey = Symbol();
  const count = ref(1);
  const Provider = {
    setup() {
      provide("foo", "foo-p1");
      // 支持符号属性名
      provide(skey, 2);
      // 可以是其他类型 ref, reactive, ...
      provide("count", count);
      return () => h(Provider2);
    },
  };

  const Provider2 = {
    setup() {
      provide("foo", "foo-p2");
      provide("baz", "baz");
      return () => h(Middle);
    },
  };

  const Middle = {
    render: () => h(Consumer),
  };

  const Consumer = {
    setup() {
      const symb = inject(skey);
      // 这里 foo 拿到的会是 Provider2 的 foo 值
      // 因为 provides 原型链是基于组件父子关系来创建的
      const foo = inject("foo");
      // 默认值
      const bar = inject("bar", "bar");
      // 因为是简单的赋值操作,所以这里的 count 就是 ref(1) 返回的那个 count
      const count = inject("count");
      return () => [symb, foo, bar, count.value].join(",");
    },
  };

  const root = nodeOps.createElement("div");
  render(h(Provider), root);
  let s = serialize(root);
  log("1. root serialize, " + s);
  count.value++;
  await nextTick();
  s = serialize(root);
  log("2. root serialize, " + s);
};
run();
1. root serialize, <div>2,foo-p2,bar,1</div>
undefined2. root serialize, <div>2,foo-p2,bar,2</div>

组件层级关系(父->子): Provider -> Middle -> Consumer

provide('foo', 1) 则在 Provider 组件的 provides 中增加了 { foo: 1 } ,由于 子组件的 provides 是沿用或继承了父级的 provides。

所以在子组件 Consumer 中直接调用 inject('foo') 是在 instance.provides 里面去 找这个属性对应的值,如果没找到则一直在原型链上往上找,最后找到顶级父组件 Provider 上找到包含 'foo' 属性,则返回这个值。

所以, inject&provide 的应用其实就是原型链的应用,目的是为了让父组件可以像子组 件注入一些值。

  1. 那么为什么注入的值直接变成了 <div>1</div> 子节点了?

✨ api computed

这个 api 是对 reactivity>computed 的一次简单封装,详情请直接查看 reactivity对应的 computed 一节

代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
export function computed<T>(getter: ComputedGetter<T>): ComputedRef<T>
export function computed<T>(
  options: WritableComputedOptions<T>
): WritableComputedRef<T>
export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
  const c = _computed(getterOrOptions as any)
  recordInstanceBoundEffect(c.effect)
  return c
}

🌀 api lifecycle

feat(add): api lifecycle init · gcclll/stb-vue-next@071d868 · GitHub

feat(add): api lifecycle exports · gcclll/stb-vue-next@ee88856 · GitHub 组件声明周期 api 。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21

export function injectHook(
  type: LifecycleHooks,
  hook: Function & { __weh?: Function },
  target: ComponentInternalInstance | null = currentInstance,
  prepend: boolean = false
): Function | undefined {
  // TODO
  return
}

export const createHook = <T extends Function = () => any>(
  lifecycle: LifecycleHooks
) => (hook: T, target: ComponentInternalInstance | null = currentInstance) =>
  !isInSSRComponentSetup && injectHook(lifecycle, hook, target)

export const onBeforeMount = createHook(LifecycleHooks.BEFORE_MOUNT)
export const onMounted = createHook(LifecycleHooks.MOUNTED)
export const onBeforeUpdate = createHook(LifecycleHooks.BEFORE_UPDATE)
export const onUpdated = createHook(LifecycleHooks.UPDATED)
export const onUnMount = createHook(LifecycleHooks.UNMOUNTED)

如上,所有的声明周期函数都是通过 createHook -> injectHook 实现,所以这部分的重点 就是这个 injectHook(type, hook, targt, prepend)

createHook() 里面有检测是不是 SSR 环境,只有非 SSR 环境下才会有声明周期。

feat(add): api lifecycle injectHook · gcclll/stb-vue-next@6e9cd14 · GitHub

 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
export function injectHook(
  type: LifecycleHooks,
  hook: Function & { __weh?: Function },
  target: ComponentInternalInstance | null = currentInstance,
  prepend: boolean = false
): Function | undefined {
  if (target) {
    const hooks = target[type] || (target[type] = []);
    // 将 hook 的执行封装成一个 error handling warpper 函数,并且缓存到 hook.__weh
    // 上,这样在调度器里面调用的时候可以进行去重,因为 scheduler 里面执行的时候
    // 有去重操作,这里的 __weh = 'with error handling'
    const wrappedHook =
      hook.__weh ||
      (hook.__weh = (...args: unknown[]) => {
        if (target.isUnmounted) {
          return;
        }
        // 在所有的声明周期函数中都 disable tracking
        // 因为它们有可能在 effects 中被调用
        pauseTracking();
        // 在 hook 执行期间设置 currentInstance = targt
        // 假设 hook 没有同步地触发其他 hooks,即在一个 hook 里面同步调用
        // 另一个声明周期函数?
        setCurrentInstance(target);
        const res = callWithAsyncErrorHandling(hook, target, type, args);
        setCurrentInstance(null);
        resetTracking();
        return res;
      });

    prepend ? hooks.unshift(wrappedHook) : hooks.push(wrappedHook);
    return wrappedHook;
  } else if (__DEV__) {
    // ...
  }
  return;
}
  1. lifecycle hook 可以在 setup() 中执行

  2. 这里的所有声明周期函数都以事件形式存在,意味着可以通过这些 api 注册哪个声明周 期需要执行操作的函数

  3. 在声明周期函数执行期间不进行 track 收集依赖操作,它有可能在 setup() 中进行

使用原理:调用 onXxx(fn, ins) 声明周期函数时,实际上只是向当前的实例 currentInstane[type] = [] 上注册了一个回调函数 fn。比如:

target.onMounted = [fn1, fn2] // target -> currentInstance 当前实例

然后在组件 render/update 过程中根据声明周期的阶段调用对应的 fns 。

test base

 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
// 源文件:/js/vue/lib.js
const {
  rcTest: {
    render,
    nodeOps,
    onBeforeMount,
    h,
    serializeInner,
    onMounted,
    onBeforeUpdate,
    onUpdated,
    onBeforeUnmount,
    onBeforeUnmounted,
    ref,
    nextTick,
  },
  f,
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");

const root = nodeOps.createElement("div");
let called = 0;
const s = () => serializeInner(root);
const fn = (stage) => {
  called++;
  log(`\n-> ${stage}, called = ${called}, serialize root = "${s()}" }`);
  // count.value++
};

const count = ref(0);
const run = async () => {
  const Comp = {
    setup() {
      // 这里故意将 onMounted 放在前面
      // 结果会发现这个还是最后才执行
      onMounted(() => fn("onMounted"));
      onBeforeMount(() => fn("onBeforeMount"));
      onBeforeUpdate(() => fn("onBeforeUpdate"));
      onUpdated(() => fn("onUpdated"));

      return () => h("div", count.value);
    },
  };
  render(h(Comp), root);
  count.value++;
  await nextTick();
  log("after set value...");
  log(s());
};
run();

-> onBeforeMount, called = 1, serialize root = "" }

-> onMounted, called = 2, serialize root = "<div>0</div>" }
undefined
-> onBeforeUpdate, called = 3, serialize root = "<div>0</div>" }

-> onUpdated, called = 4, serialize root = "<div>1</div>" }
after set value...
<div>1</div>

test update

测试在 onBeforeUpdate 中执行值更新操作会怎样?

 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
// 源文件:/js/vue/lib.js
const {
  rcTest: {
    render,
    nodeOps,
    onBeforeMount,
    h,
    serializeInner,
    onBeforeUpdate,
    onUpdated,
    ref,
    nextTick,
  },
  f,
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");

const root = nodeOps.createElement("div");
let called = 0;
const s = () => serializeInner(root);
const fn = (stage) => {
  called++;
  log(`\n-> ${stage}, called = ${called}, serialize root = "${s()}" }`);
  count.value++
};

const count = ref(0);
const run = async () => {
  const Comp = {
    setup() {
      onUpdated(() => log('-> onUpdated, count.value = ' + count.value + ', ' + s()))
      onBeforeUpdate(() => fn("onBeforeUpdate"));

      return () => h("div", count.value);
    },
  };
  render(h(Comp), root);
  count.value++;
  await nextTick();
  log("after set value...");
  log(s());
};
run();
undefined
-> onBeforeUpdate, called = 1, serialize root = "<div>0</div>" }
-> onUpdated, count.value = 2, <div>2</div>
after set value...
<div>2</div>
  1. 最后 count.value 渲染出来是 2?

test unmount

 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
// 源文件:/js/vue/lib.js
const {
  rcTest: {
    render,
    nodeOps,
    onBeforeMount,
    h,
    serializeInner,
    onBeforeUpdate,
    onUpdated,
    onBeforeUnmount,
    onUnmounted,
    onMounted,
    ref,
    nextTick,
  },
  f,
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");

const root = nodeOps.createElement("div");
let called = 0;
const s = () => serializeInner(root);
const fn = (stage) => {
  called++;
  log(`\n-> ${stage}, called = ${called}, serialize root = "${s()}" }`);
};

const toggle = ref(true);
const run = async () => {
  const Comp = {
    setup() {
      return () => (toggle.value ? h(Child) : null);
    },
  };

  const Child = {
    setup() {
      onBeforeUnmount(() => fn("onBeforeUnmount"));
      onUnmounted(() => fn("onUnmounted"));
      onMounted(() => {
        // 这等于是等组件加载完成之后在注册 unmount
        onBeforeUnmount(() => log("-> onBeforeUnmount called in onMounted"));
      });
      return () => h("div");
    },
  };
  render(h(Comp), root);
  toggle.value = false;
  await nextTick();
  log("\nafter set value...");
  log(s());
};
run();
undefined
-> onBeforeUnmount, called = 1, serialize root = "<div></div>" }
-> onBeforeUnmount called in onMounted

-> onUnmounted, called = 2, serialize root = "<!---->" }

after set value...
<!---->

test order

测试声明周期函数执行顺序(和调用顺序无关)

 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
// 源文件:/js/vue/lib.js
const {
  rcTest: {
    render,
    nodeOps,
    h,
    serializeInner,
    onBeforeMount,
    onMounted,
    onBeforeUpdate,
    onUpdated,
    onBeforeUnmount,
    onUnmounted,
    ref,
    nextTick,
  },
  f,
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");

const root = nodeOps.createElement("div");
let calls = [];

const count = ref(0);
const run = async () => {
  const Root = {
    setup() {
      onBeforeMount(() => calls.push('root onBeforeMount'))
      onMounted(() => calls.push('root onMounted'))
      onBeforeUpdate(() => calls.push('root onBeforeUpdate'))
      onUpdated(() => calls.push('root onUpdated'))
      onBeforeUnmount(() => calls.push('root onBeforeUnmount'))
      onUnmounted(() => calls.push('root onUnmounted'))
      return () => h(Mid, { count: count.value });
    },
  };

  const Mid = {
    setup(props) {
      onBeforeMount(() => calls.push('Mid onBeforeMount'))
      onMounted(() => calls.push('Mid onMounted'))
      onBeforeUpdate(() => calls.push('Mid onBeforeUpdate'))
      onUpdated(() => calls.push('Mid onUpdated'))
      onBeforeUnmount(() => calls.push('Mid onBeforeUnmount'))
      onUnmounted(() => calls.push('Mid onUnmounted'))
      return () => h(Child, { count: props.count });
    },
  };

  const Child = {
    setup(props) {
      onBeforeMount(() => calls.push('Child onBeforeMount'))
      onMounted(() => calls.push('Child onMounted'))
      onBeforeUpdate(() => calls.push('Child onBeforeUpdate'))
      onUpdated(() => calls.push('Child onUpdated'))
      onBeforeUnmount(() => calls.push('Child onBeforeUnmount'))
      onUnmounted(() => calls.push('Child onUnmounted'))
      return () => h('div', props.count);
    },
  };


  render(h(Root), root)
 log(['\n0. before update value >', calls])
  calls.length = 0

  // update
  count.value++
  await nextTick()
  log(['\n1. update value >', calls])
  calls.length = 0

  // unmount
  render(null, root)
  log(['\n2. unmount >', calls])
  calls.length = 0
};
run();

0. before update value > [
  'root onBeforeMount',
  'Mid onBeforeMount',
  'Child onBeforeMount',
  'Child onMounted',
  'Mid onMounted',
  'root onMounted'
]
undefined
1. update value > [
  'root onBeforeUpdate',
  'Mid onBeforeUpdate',
  'Child onBeforeUpdate',
  'Child onUpdated',
  'Mid onUpdated',
  'root onUpdated'
]

2. unmount > [
  'root onBeforeUnmount',
  'Mid onBeforeUnmount',
  'Child onBeforeUnmount',
  'Child onUnmounted',
  'Mid onUnmounted',
  'root onUnmounted'
]

从上面的结果可知整个组件声明周期发生过程。

  1. 组件 mount 顺序, child -> mid -> comp,子组件 -> … -> 父组件

  2. 组件 update 顺序, child -> mid -> comp, 子组件 -> … -> 父组件

  3. 组件 unmount 顺序, child -> mid -> comp, 子组件 -> … -> 父组件

组件的 before 声明周期触发顺序是: child -> mid -> comp 完成之后的顺序刚好相反。

test render track & trigger

 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
// 源文件:/js/vue/lib.js
const {
  rcTest: {
    render,
    nodeOps,
    h,
    serializeInner,
    ref,
    nextTick,
    reactive,
    onRenderTracked,
    onRenderTriggered
  },
  f,
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");

const run = () => {
  const events = [];
  let called = 0;
  const fn = (e) => {
    called++;
    events.push(e);
  };
  const obj = reactive({ foo: 1, bar: 2 });
  const Comp = {
    setup() {
      onRenderTriggered(fn)
      onRenderTracked(fn);
      return () => h("div", [obj.foo, "bar" in obj, Object.keys(obj).join("")]);
    },
  };

  render(h(Comp), nodeOps.createElement('div'))
  log('called = ' + called) // 3
  log('>> track events')
  events.forEach(e => log(f(e, ['target', 'type', 'key'])))
  events.length = 0
  log('>> trigger events')
  obj.foo++
  events.forEach(e => log(f(e, ['target', 'type', 'key'])))
  events.length = 0
  delete obj.bar
  events.forEach(e => log(f(e, ['target', 'type', 'key'])))
  events.length = 0
  obj.baz = 3
  events.forEach(e => log(f(e, ['target', 'type', 'key'])))
  events.length = 0

};
run();
called = 3
>> track events
{ target: { foo: 1, bar: 2 }, type: 'get', key: 'foo' }
{ target: { foo: 1, bar: 2 }, type: 'has', key: 'bar' }
{ target: { foo: 1, bar: 2 }, type: 'iterate', key: Symbol(iterate) }
>> trigger events
{ target: { foo: 2, bar: 2 }, key: 'foo', type: 'set' }
{ target: { foo: 2 }, key: 'bar', type: 'delete' }
{ target: { foo: 2, baz: 3 }, key: 'baz', type: 'add' }
undefined

🦉 api define component

1
2
3
4
// implementation, close to no-op
export function defineComponent(options: unknown) {
  return isFunction(options) ? { setup: options, name: options.name } : options
}

🆘 api setup helpers

feat(add): api setup helpers · gcclll/stb-vue-next@3e71787 · GitHub

 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
// runtime-core/src/apiSetupHelpers.ts
// implementation
export function defineProps() {
  if (__DEV__) {
    warn(
      `defineProps() is a compiler-hint helper that is only usable inside ` +
        `<script setup> of a single file component. Its arguments should be ` +
        `compiled away and passing it at runtime has no effect.`
    );
  }
  return null as any;
}

export function defineEmit() {
  if (__DEV__) {
    warn(
      `defineEmit() is a compiler-hint helper that is only usable inside ` +
        `<script setup> of a single file component. Its arguments should be ` +
        `compiled away and passing it at runtime has no effect.`
    );
  }
  return null as any;
}

export function useContext(): SetupContext {
  const i = getCurrentInstance()!;
  if (__DEV__ && !i) {
    warn(`useContext() called without active instance.`);
  }
  return i.setupContext || (i.setupContext = createSetupContext());
}

这里包含三个函数,其中 defineProps&defineEmit 都是且只会在 <script setup> 里 面使用,他们的作用只是用于 compiler-sfc 包中在 babel/parser 解析语法树期间, 用来标识他们是 props或 emits 的一部分,然后给他们的参数会被当做 props 和 emits 合并到最终 export 出去的对应属性上,所以这两个 define 函数实际上什么都没做。

看实例吧 ->

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 源文件:/js/vue/lib.js
const { compile, f, log } = require(process.env.BLOG_JS + '/vue/lib.js')

const { content } = compile(`
<script setup>
import { defineProps, defineEmit } from 'vue'
ref: value = 1
const myEmit = defineEmit(['foo', 'bar'])
const props = defineProps({
  fox: String,
  foy: () => baz > 1
})
</script>
<script>
export default {
  props: {
    box: 2
  }
}
</script>
`)
log(content)
import { ref as _ref } from 'vue'


function setup(__props, { emit: myEmit }) {

const props = __props
const value = _ref(1)



return { value, myEmit, props }
}


const __default__ = {
  props: {
    box: 2
  }
}

export default /*#__PURE__*/ Object.assign(__default__, {
  expose: [],
  props: {
  fox: String,
  foy: () => baz > 1
},
  emits: ['foo', 'bar'],
  setup
})
undefined

我们把上面结果格式化下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { ref as _ref } from "vue";

function setup(__props, { emit: myEmit }) {
  const props = __props;
  const value = _ref(1);

  return { value, myEmit, props };
}

const __default__ = {
  props: {
    box: 2,
  },
};

export default /*#__PURE__*/ Object.assign(__default__, {
  expose: [],
  props: {
    fox: String,
    foy: () => baz > 1,
  },
  emits: ["foo", "bar"],
  setup,
});

从上面的代码可发现:

  1. defineProps 最后被合并成一个 props 对象,并且会和 <script> 中的 export default 中的 props 进行再次合并,并且这种合并属于简单的替换操作

    所以如果 definePropsscript 中同时存在 props 的情况最终 <script> 中 的 props 会被丢弃掉,所以尽量避免这种情况发生。

  2. defineEmit 中的属性会被合并到 emits

更多有关 <script setup> 的内容请查看 compiler-sfc 包的分析 ->

useContext() 是获取当前实例的上下文对象,这个需要继续实现复杂的 render 部分才 可能会使用到。

🌊 TODO api async comopnent

feat(add): async component · gcclll/stb-vue-next@cb4474a · GitHub

定义异步组件(defineAsyncComponent(source))。

新增初始化:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
export const isAsyncWrapper = (i: ComponentInternalInstance | VNode) =>
  !!(i.type as ComponentOptions).__asyncLoader;

export function defineAsyncComponent<
  T extends Component = { new (): ComponentPublicInstance }
>(source: AsyncComponentLoader<T> | AsyncComponentOptions<T>): T {
  // 1. TODO retry 封装
  //
  // 2. TODO 函数封装
  //
  // 3. TODO 返回组件,检测如果是对象直接返回,or 函数当做 setup() 函数处理
  return defineComponent({} as any) as any;
}

通过搜索 __asyncLoader 只在 hydration.ts 中有使用到,而这个貌似又和 SSR 服务 端渲染由关系,所以这里先暂时不继续了,先完成 renderer.ts 的 render 函数部分,然 后在实现了 SSR + hydration 之后再回头来继续。

implementation(init)

feat(add): async component->load · gcclll/stb-vue-next@bd6e903 · GitHub

feat: async component pause await render · gcclll/stb-vue-next@1c73e72 · GitHub 实现主要分几步:

  1. 封装 retry() 重试函数

  2. load() 函数,分支 source.loader() 异步函数

  3. 组装组件返回 defineComponent({__asyncLoader: load, setup() {...}, ...})

    前面 1, 2 都是对 source.loader 进行封装处理,这一步是真正的创建异步组件。

    defineComponent(option) 实现很简单,就是检测 option 如果是函数当做

    {setup: option, name: option.name} 处理,否则直接返回 option

测试:

 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
// 源文件:/js/vue/lib.js
const {
  rc: { nextTick, h, ref, defineAsyncComponent },
  rcTest: { createApp, nodeOps, serializeInner: si },
  f,
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");

const run = async () => {
  const timeout = (n) => new Promise((r) => setTimeout(r, n));
  let resolve;
  const Foo = defineAsyncComponent(() => new Promise((r) => (resolve = r)));

  const toggle = ref(true);
  const root = nodeOps.createElement("div");
  createApp({
    render: () => (
      console.log(toggle.value, "xxx"), toggle.value ? h(Foo) : null
    ),
  }).mount(root);
  console.log(si(root));

  // 手动 resolve, 触发 loader().then(comp => { ... })
  resolve(() => "resolved");
  await timeout();
  si(root);

  // 到这里 component 已经 resolved 了
  toggle.value = false;
  await nextTick();
  si(root); // -> null

  toggle.value = true;
  await nextTick();
  si(root); // -> resolved
};
run();
true xxx

 resolved comp before
loaded.value =  false
<!---->
undefined
async comp load ok,  resolved

🔥 render function

🚒 scheduler 任务调度机制

让我们跟着 scheduler.spec.ts 测试用例来逐步属性 scheduler 的调度机制。

在做这个之前先把 scheduler.ts 中逻辑代码全清空,这个文件还是相对独立的

feat: rc->reset scheduler.ts · gcclll/stb-vue-next@a54cc00 · GitHub

我们从零开始一步步来分析实现。

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

这部分包含三种任务的 flush 逻辑代码:

  1. queue jobs -> flushIndex -> queue[] -> queueJob() -> queueFlush() -> flushJobs()

  2. pre jobs -> preFlushIndex -> pendingPreFlushCbs[] -> activePreFlushCbs[] -> queuePreFlushCb() -> flushPreFlushCbs() -> flushJobs()

  3. TODO post jobs -> …

    nextTick

feat(add): rc->scheduler -> nextTick · gcclll/stb-vue-next@32b4827 · GitHub

在 queue 所有队列清空之后执行的一个异步操作,有重要关联的两个变量:

  1. resolvedPromise,一个空的 promise then

  2. currentFlushPromise,当 queue 队列中的所有任务执行完成之后返回的一个 promise

    是的,是所有 queue jobs 完成之后,因为 flushJobs 函数里面都是同步操作,重要代 码:

    1
    2
    3
    4
    5
    6
    7
    
    for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
      const job = queue[flushIndex];
      if (job) {
        // TODO DEV -> 检查递归更新问题
        callWithErrorHandling(job, null, ErrorCodes.SCHEDULER);
      }
    }
    

所以 nextTick 任务总是在 queue jobs 所有任务完成之后执行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const resolvedPromise: Promise<any> = Promise.resolve();
// 当前正在被执行的 promise 任务
let currentFlushPromise: Promise<void> | null = null;

export function nextTick(
  this: ComponentPublicInstance | void,
  fn?: () => void
): Promise<void> {
  const p = currentFlushPromise || resolvedPromise;
  return fn ? p.then(this ? fn.bind(this) : fn) : p;
}

函数作用:在当前正在执行的 job promise 之后执行 nextTick 的任务,等于说 nextTick 属于个插队任务。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// 源文件:/js/vue/lib.js
const {
  rc: { nextTick },
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");

const run = async () => {
  const calls = [];
  const pr = Promise.resolve();
  const dummyThen = Promise.resolve().then();
  const job1 = () => calls.push("job1");
  const job2 = () => calls.push("job2");
  nextTick(job1);
  job2();
  log(["\nbefore await, ", calls.length, "\n"]);
  await dummyThen;
  log(["\nafter await, ", calls.length, "\n"]);
  log(calls.join("-"));
};

run();

before await,  1

after await,  2

job2-job1

Tip. nextTick() 异步代码执行,经过 babel 转换后的代码,请查看 nextTick question

queueJob

feat(add): rc->scheduler->queueJob · gcclll/stb-vue-next@eb33b40 · GitHub

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

 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 queueJob(job: SchedulerJob) {
  // the dedupe search uses the startIndex argument of Array.includes()
  // by default the search index includes the current job that is being run
  // so it cannot recursively trigger itself again.
  // if the job is a watch() callback, the search will start with a +1 index to
  // allow it recursively trigger itself - it is the user's responsibility to
  // ensure it doesn't end up in an infinite loop.
  if (
    (!queue.length ||
      !queue.includes(
        job,
        isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex
      )) &&
    job !== currentPreFlushParentJob
  ) {
    queue.push(job)
    queueFlush
  }
}

function queueFlush() {
  if (!isFlushing && !isFlushPending) {
    isFlushPending = true
    currentFlushPromise = resolvedPromise.then(flushJobs)
  }
}

// 请查看下一节的实现
function flushJobs(seen?: CountMap) {
  // TODO
}

需要 flushJobs 支持,请到 flushJobs(👇) 一节查看测试情况。

flushJobs

feat(add): rc->scheduler->flushJobs function · gcclll/stb-vue-next@e23be11 · GitHub

  1. isFlushPending, isFlushing 标识重置

  2. flushPreFlushCbs, 对 pre 类型的 jobs 进行 flush 操作,有关函数 flushPreFlushCbs(flush函数)queuePreFlushCb(入列函数)

  3. flush 之前进行排序

  4. try -> callWithErrorHandling 执行任务回调

  5. finally -> 重置,清空 queue 队列内容和标识

  6. TODO flushPostFlushCbs, 对 post 类型的 jobs 进行 flush 操作,有关函数 flushPostFlushCbsqueuePostFlushCb

 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 flushJobs(seen?: CountMap) {
  isFlushPending = false;
  isFlushing = true;

  if (__DEV__) {
    seen = seen || new Map();
  }

  // flushPreFLushCbs(seen),默认的 job 类型

  // flush 之前对 queue 排序
  // 1. 组件更新顺序:parent -> child,因为 parent 总是在 child 之前
  //    被创建,因此 parent render effect 有更低的优先级数字(数字越小越先创建?)
  // 2. 如果组件在 parent 更新期间被卸载了,那么它的更新都会被忽略掉

  queue.sort((a, b) => getId(a) - getId(b));

  // 开始 flush
  try {
    for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
      const job = queue[flushIndex];
      if (job) {
        // TODO DEV -> 检查递归更新问题
        callWithErrorHandling(job, null, ErrorCodes.SCHEDULER);
      }
    }
  } finally {
    // 情况队列
    flushIndex = 0;
    queue.length = 0;

    // TODO flush `post` 类型的 flush cbs

    isFlushing = false;
    currentFlushPromise = null;

    // TDOO 代码执行到当前 tick 的时候,有可能有新的 job 加入
    // some postFlushCb queued jobs!
    // keep flushing until it drains.
  }
}

测试:

 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
// 源文件:/js/vue/lib.js
const {
  rc: { queueJob, nextTick },
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");
const run = async () => {
  const calls = [];
  const job1 = () => { // #1
    log.newline("job1 running");
    calls.push("job1");
  };
  const job2 = () => { // #2
    log.newline("job2 running");
    calls.push("job2");
  };
// 支持去重
  queueJob(job1); // #3
  queueJob(job2); // #4
  queueJob(job1);
  queueJob(job2);
  log("before await  " + calls); // #5
  await nextTick(); // #6
  log("after await  " + calls); // #7
};

run();
before await
undefined

job1 running


job2 running
after await  job1,job2

如果在没有 #6 的情况下,在所有 Log 之后会立即执行 queue jobs。

1
2
3
4
5
6
function queueFlush() {
  if (!isFlushing && !isFlushPending) {
    isFlushPending = true;
    currentFlushPromise = resolvedPromise.then(flushJobs);
  }
}

这里 nextTick() 调用并没有传递 fn ,因此 await nextTick() 在这里的作用就是等 resolvedPromise 执行完成(此时并没有正在执行的 promise)

const resolvedPromise: Promise<any> = Promise.resolve()

再执行后面的代码。

queueJob 函数分为两步:

  1. push 收集任务 queue.push(job) ,同步执行

  2. 随后立即调用 queueFlush() 刷掉任务,任务异步 flush

在这个实例中,按照同步执行顺序,

  1. queueJob(job1) 执行,将 job1 -> push -> queue 中, queueFlush 中的 promise 等待

  2. queueJob(job2) 执行,将 job2 -> push -> queue 中, queueFlush 中的 promise 继续等待

  3. log before 执行,由于 job 虽然已经在 queue 中了,但是需要等待 queueFlush 去 异步执行他们,所以这里 calls 依旧是空的

  4. await nextTick() 异步操作

    这一句目的只是为了让后面的 log 在 job1,job2 后面执行。

    1
    2
    
     const p = currentFlushPromise || resolvedPromise;
     return fn ? p.then(this ? fn.bind(this) : fn) : p;
    

    nextTick 会在刚刚执行完毕的 promise 后面取执行后面的任务,所以 log after 肯定是后于 job1,job2 的执行的。

  5. 所有同步任务执行完成,开始进入异步任务执行,由于 job1,job2 先入队列,在事件循 环中会先于 log after 执行,然后在执行 log after,所以就有了上面的输出结果。

实例执行脑图:

/img/tmp/20210112173934.png

queueJob while flushing

当 queue 中 jobs 正在被执行的时候调用 queueJob 进入新的任务。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// 源文件:/js/vue/lib.js
const {
  rc: { queueJob, nextTick },
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");
const run = async () => {
  const calls = [];
  const job1 = () => {
    calls.push("job1");
    // job2 任务会在 job1 执行到这里的时候加入到了 queue
    // 但是它的执行需等到 queue 中的任务执行完成之后再执行
    // 因为任务收集是同步的,任务执行是异步的,而 queue flush 操作又是同步的
    queueJob(job2);
  };
  const job2 = () => calls.push("job2");
  queueJob(job1);
  await nextTick();
  log(["\nafter await\n", calls]);
};
run();
undefined
after await
 [ 'job1', 'job2' ]

看下面的测试代码(在 for 循环过程中改变数组长度,会检测到这种改变):

1
2
3
4
5
6
const nums = [1, 2, 3];
const add = (i) => nums.push(++i);
for (let i = 0; i < nums.length; i++) {
  if (i === 1) add(i);
  console.log({ i, v: nums[i], l: nums.length });
}
{ i: 0, v: 1, l: 3 }
{ i: 1, v: 2, l: 4 }
{ i: 2, v: 3, l: 4 }
{ i: 3, v: 2, l: 4 }
undefined

所以上面的 Job 实例,就很好理解了

在 for queue jobs 过程中发现有新的 job 进入,之前说过了 queue 的入列操作是同步 的,所以会立即执行改变 queue 长度,最后加入的任务会在 for 循环过程中最后得到执行。

queuePreFlushCb

feat(add): rc->scheduler->queuePreFlushCb -> pre jobs, pendingPreFlus… · gcclll/stb-vue-next@2c72cdc · GitHub

新增代码:

  1. queuePreFlushCb, 入列 pre jobs 函数

  2. flushPreFlushCbs, flush pre jobs 函数

  3. flushJobs 中调用 flushPreFlushCbs() 刷掉 pre jobs

这个是用来收集和 flush pre 类型(默认类型的任务)的队列 pendingPreFlushCbs[] 的函数。

逻辑脑图: /img/tmp/20210113103504.png

相关代码:

 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 queuePreFlushCb(cb: SchedulerCb) {
  queueCb(cb, activePreFlushCbs, pendingPreFlushCbs, preFlushIndex);
}

function queueCb(
  cb: SchedulerCbs,
  activeQueue: SchedulerCb[] | null,
  pendingQueue: SchedulerCb[],
  index: number
) {
  if (!isArray(cb)) {
    if (
      !activeQueue ||
      !activeQueue.includes(
        cb,
        (cb as SchedulerJob).allowRecurse ? index + 1 : index
      )
    ) {
      pendingQueue.push(cb);
    }
  } else {
    pendingQueue.push(...cb);
  }
  queueFlush();
}

对比 queueCb 和 queueJob 会发现两者没多大的差别,先同步收集再异步 flush,两者判 断条件有细微差别,另外 queueJob 支持数组形式的 cb:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// queueJob
if (
  (!queue.length ||
    !queue.includes(
      job,
      isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex
    )) &&
  job !== currentPreFlushParentJob
) {
  queue.push(job);
  queueFlush();
}

最后也都是调用 queueFlush() -> flushJobs() 来清空队列 pendingQueue/queue 。

所以下面还需要在 flushJobs() 里面去实现对 pre -> pendingQueue 类型队列 flush 操 作(flushPreFlushCbs())。

flushPreFlushCbs

有关函数和变量

nametypedescription
preFlushIndexnumberused in `for` to flush pre jobs
pendingPreFlushCbsarraythe queue to store pre jobs
activePreFlushCbsarraythe non-repeat copy of pendingPreFlushCbs, used to flushing
queuePreFlushCbfunction与 flushPreFlushCbs 对应的 pre job 入列函数
queueFlushfunction执行队列任务的函数,三个类型的任务都在这里面执行(pre,post,queue)
flushJobsfunction具体执行任务的函数,三种任务执行顺序是: pre -> queue -> post

Tip. activePreFlushCbspendingPreFlushCbs 的关系: 前者是后者的一个拷贝, 拷贝完会立即清空 pending, 目的是为了让 pending 在 active flushing 期间能继续收集 新的任务,这样如果在执行期间有新的任务入列,那么在函数最后的递归操作会对这些新入 列的任务继续 flush 掉,直到再也没有新的任务入列为止。

注意点 :当 queuePreFlushCb 在 queueJob 中使用时不会主动触发 cbs 执行,如果 需要立即执行这些 cbs 需要手动调用 flushPreFlushCbs(seen, parentJob) 去刷掉 pre cbs 任务,或者等到当前 job 执行完了下一个 flushJobs() 调用中执行,因为 queueJob() 执行期间 isFlushing = true ,而在 queueFlush() 中有检测这个值, 如果正在执行 flushing 是不会继续执行的,更多详情查看后面的测试和分析。

源码:

 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
export function flushPreFlushCbs(
  seen?: CountMap,
  parentJob: SchedulerJob | null = null
) {
  if (pendingPreFlushCbs.length) {
    currentPreFlushParentJob = parentJob;
    activePreFlushCbs = [...new Set(pendingPreFlushCbs)];
    pendingPreFlushCbs.length = 0;
    if (__DEV__) {
      seen = seen || new Map();
    }

    for (
      preFlushIndex = 0;
      preFlushIndex < activePreFlushCbs.length;
      preFlushIndex++
    ) {
      // TODO 检查递归更新问题
      activePreFlushCbs[preFlushIndex]();
    }

    activePreFlushCbs = null;
    preFlushIndex = 0;
    currentPreFlushParentJob = null;
    // 递归 flush 直到所有 pre jobs 被执行完成
    flushPreFlushCbs(seen, parentJob);
  }
}

用途: api watch 里面对默认类型(pre)的任务的入列操作,如下代码:

 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
// default: 'pre'
function doWatch(
  source: WatchSource | WatchSource[] | WatchEffect | object,
  cb: WatchCallback | null,
  { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ,
  instance = currentInstance
): WatchStopHandle {
  // ...
  let scheduler: ReactiveEffectOptions["scheduler"];
  if (flush === "sync") {
    // ...
  } else if (flush === "post") {
    // ...
  } else {
    // default: 'pre'
    scheduler = () => {
      if (!instance || instance.isMounted) {
        queuePreFlushCb(job);
      } else {
        // with 'pre' option, the first call must happen before
        // the component is mounted so it is called synchronously.
        job();
      }
    };
  }
  // ...
}

测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 源文件:/js/vue/lib.js
const {
  rc: { queueJob, queuePreFlushCb, flushPreFlushCbs, nextTick },
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");

const run = async () => {
  const calls = [];
  const job1 = () => { // #1
    queuePreFlushCb(cb1); // #2
    queuePreFlushCb(cb2); // #3
    // 手动触发 cb1, cb2
    flushPreFlushCbs(undefined, job1); // #4
    calls.push("job1"); // #5
  };
  const cb1 = () => calls.push("cb1"); // #6
  const cb2 = () => calls.push("cb2"); // #7

  queueJob(job1); // #8
  await nextTick(); // #9
  log.newline(calls); // #10
};
run();
undefined

cb1 cb2 job1

测试分析代码脑图: /img/vue3/runtime-core/vue-runtime-core-test-preflush-inside-queuejob.jpg

文字分析:

  1. #8 先执行, queueJob -> push job1 -> queue:[job1] -> queueFlush()

    在 queueFlush() 中调用 resolvedPromise.then(flushJobs) 异步执行 flushJobs() 函数刷掉所有任务(pre/job/post)

    并且记录当前 tick 下的 promise: currentFlushPromise

    此时的 pendingPreFlushCbs[] 中是没有任何任务的,所以继续执行 try{…} 开始 flush queue[] jobs,这个时候 flushIndex = 0 得到 job1,开始按顺序执行 job1

  2. #1 开始执行

  3. #2 将 cb1 push -> pendingPreFlushCbs=[cb1]

  4. #3 将 cb2 push -> pendingPreFlushCbs=[cb1, cb2]

  5. #4 手动 flush pre cbs

    flushPreFlushCbs(undefind, job1) 中会记录 currentPreFlushParentJob = job1 这个变量将会在 queueJob(job) 中用来检测 job 是不是当前的 job1 如果是 就不允许 push,因为 job1 下有子任务正在执行,必须等这些子任务(cb1, cb2) 执行完。

  6. #6 开始执行, push 'cb1' -> calls: ['cb1']

  7. #7 开始执行, push 'cb2' -> calls: ['cb1', 'cb2']

  8. #5 开始执行, push 'job1' -> alls: ['cb1', 'cb2', 'job1']

  9. #9 开始执行,因为 nextTick()

    1
    2
    3
    4
    5
    6
    7
    
    export function nextTick(
      this: ComponentPublicInstance | void,
      fn?: () => void
    ): Promise<void> {
      const p = currentFlushPromise || resolvedPromise;
      return fn ? p.then(this ? fn.bind(this) : fn) : p;
    }
    

    这里的 await 会等 job1 queueFlush() 触发的 promise.then(flushJobs) 返回的 promise 完成之后再执行后面的代码。

  10. #10 log 输出 'cb1,cb2,job1'

queuePostFlushCb + flushPostFlushCbs

feat(add): rc->scheduler->queuePostFlushCb+flushPostFlushCbs · gcclll/stb-vue-next@845c21b · GitHub

逻辑脑图: /img/tmp/20210113143628.png

有了 queue job 和 pre cb 的基础分析,这部分也就很好理解了。

 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
export function queuePostFlushCb(cb: SchedulerCbs) {
  queueCb(cb, activePostFlushCbs, pendingPostFlushCbs, postFlushIndex);
}

export function flushPostFlushCbs(seen?: CountMap) {
  if (pendingPostFlushCbs.length) {
    const deduped = [...new Set(pendingPostFlushCbs)];
    pendingPostFlushCbs.length = 0;

    // #1947 already has active queue, nested flushPostFlushCbs call
    if (activePostFlushCbs) {
      activePostFlushCbs.push(...deduped);
      return;
    }

    activePostFlushCbs = deduped;
    if (__DEV__) {
      seen = seen || new Map();
    }

    activePostFlushCbs.sort((a, b) => getId(a) - getId(b));

    for (
      postFlushIndex = 0;
      postFlushIndex < activePostFlushCbs.length;
      postFlushIndex++
    ) {
      // TODO 递归 update 检查
      activePostFlushCbs[postFlushIndex]();
    }

    activePostFlushCbs = null;
    postFlushIndex = 0;
  }
}

和 pre cb 的处理有两个不同点:

  1. 非回调形式处理 flushing 期间接受到的新任务,而是通过改变执行器 activePostFlushCbs 来实现(和 queue job 类似)

  2. 没有递归回调形式处理后续的新任务,参考 1

测试:

 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
// 源文件:/js/vue/lib.js
const {
  rc: { queuePostFlushCb, nextTick, queueJob },
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");

// len = activePostFlushCbs.length
const run = async () => {
  const calls = [];
  const cb1 = () => {
    calls.push("cb1");
    // 会在同一个 tick 期间执行,因为它在for flushing 期间改变了
    // activePostFlushCbs,并且紧随 cb1,cb2,cb3 之后执行
    queuePostFlushCb(cb4);
  };
  const cb2 = () => calls.push("cb2");
  const cb3 = () => calls.push("cb3");
  // job1 会在 cb4 之后执行,因为 flushJobs 在按顺序执行完
  // pre -> job -> post 最后的 finally 里面对 queue 进行了检测
  // 此时 queue = [job1] 随意会递归调用 flushJobs() 继续刷
  // 但是为什么 cb5 会在 job1 之后呢???
  // 因为 queuePostFlushCb push 的是 pendingPostFlushCbs 而不是
  // activePostFlushCbs,所以在 queuePostFlushCb 中调用自身增加的新
  // cbs 会在 finally 后面的检测递归 flushJobs() 调用中执行
  // 而 post 的优先级又低于 job 所以 job1 会优先输出
  const cb4 = () => (queuePostFlushCb(cb5), queueJob(job1), calls.push("cb4"));
  // 会在 job1,cb5 之后执行
  const job1 = () => (queuePostFlushCb(cb6), calls.push("job1"));
  const cb5 = () => calls.push("cb5");
  const cb6 = () => calls.push("cb6");

  queuePostFlushCb([cb1, cb2]);
  queuePostFlushCb(cb3);

  // 应该去重
  queuePostFlushCb([cb1, cb3]);
  queuePostFlushCb(cb2);
  await nextTick();
  log.newline(calls);
};
run();
undefined

cb1 cb2 cb3 cb4 job1 cb5 cb6

对于 queuePostFlushCbqueueJob 的混用只要记住一点, queuePostFlushCb 不 会触发 activePostFlushCbs 改变,因为 isFlushing = true,所以只会在当前 flushJobs() 执行到最后递归检测的时候才会进入下一次的 post+job 调用。

test nested(pre/job/post)

完整的测试用例,结合 pre, post, queue 三种类型的任务进行测试。

 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
// 源文件:/js/vue/lib.js
const {
  rc: { queueJob, queuePreFlushCb, nextTick, flushPreFlushCbs },
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");

const run = async () => {
  const calls = [];
  const cb1 = () => {
    calls.push("cb1");
  };
  const cb2 = () => {
    calls.push("cb2");
    // queueJob 和 queuePreFlushCb 结合使用
    queueJob(job1);
  };
  const cb3 = () => {
    calls.push("cb3");
    // 链式使用,cb4 会在 cb1,2,3 执行完成之后才会执行
    queuePreFlushCb(cb4);
  };
  const cb4 = () => {
    calls.push("cb4");
  };
  const cb5 = () => {
    calls.push("cb5");
  };
  const job1 = () => {
    calls.push("job1");
    // queuePreFlushCb 在 queueJob 中调用
    // pre cbs 在 job 中调用的时候不会被执行,除非在这后面手动 flush
    // 或者有新的任务进来,发起 flushJobs 调用才会执行
    queuePreFlushCb(cb5);
    // 必须手动触发, 这样 cb5 才会输出
    flushPreFlushCbs(undefined, job1 /* currentPreFlushParentJob */);
  };
  const cb6 = () => {
    calls.push("cb6");
  };

  queuePreFlushCb(cb1);
  queuePreFlushCb(cb2);
  queuePreFlushCb(cb1);
  queuePreFlushCb(cb2);
  queuePreFlushCb(cb3);

  await nextTick();
  log("\n" + calls);
};
run();
undefined
cb1,cb2,cb3,cb4,job1,cb5
  1. pendingPreFlushCbs 虽然是个数组,但是 flush 期间通过 [...new Set(pendingPreFlushCbs)] 进行了去重操作。

  2. 链式操作,因为在执行期间使用的是 activePreFlushCbs 且此时的 pendingPreFlushCbs 清空了,等待新任务入列

    在执行 cb3 期间,调用 queuePreFlushCb(cb4) 此时 push cb4 -> pendingPreFlushCbs ,但实际不会影响本次的 for 循环执行

    这点和 queueJob 有点不同,它直接使用的是 queue -> for 所以有新的任务入列会改 变 for 的执行长度(queue.length)

    pre 处理会等到 activePreFlushCbs for 执行循环结束后,在函数的最后递归调用 flushPreFlushCbs() 来刷掉新入列的任务(如: cb4)

  3. queueJob 在 queuePreFlushCb 中调用的时候, queue job 总是在 pre cb 之后被执行,这也 是 flushJobs 中处理代码应体现出的结果。

    1
    2
    3
    4
    5
    
    function flushJobs() {
      // 1. flush pre -> flushPreFlushCbs()
      // 2. for -> queue job -> callWithErrorHandling(job, ...)
      // 3. flush post -> flushPostFlushCbs()
    }
    

    并且如上面实例结果 cb4 嵌套在 cb3 ,job1 嵌套在了 cb2 中,但是最后还是 cb4 先 得到执行了,job1 再执行。

    Tip. 因此,对于 pre cbs 和 queue jobs 两个类型的任务,不管什么时机入列的,都会 是先执行 pre cbs 再执行 queue jobs

  4. queuePreFlushCb 在 queueJob 中调用的时候,新的 pre job 会在 queue job 后执行

    fix: rc->scheduler->flushJobs recursive · gcclll/stb-vue-next@b0155c5 · GitHub

    原因: flushPreFlushCbs 先于 queue jobs 执行,因此 queue jobs(job1) 执行 的时候 queuePreFlushCb() 加入的任务(cb5)此时不会执行,而是等 queue jobs 都执行完之后在finally 里面会做一次检测

    1
    2
    3
    
    if (queue.length || pendingPreFlushCbs.length) {
       flushJobs(seen)
     }
    

    这个时候会去递归 flushJobs() 此时才发现有新的 pendingPreFlushCbs (如: cb5),则将执行他们,所以结果是 job1,cb5

invalidateJob(job)

feat(add): rc->scheduler->invalidateJob · gcclll/stb-vue-next@24808b1 · GitHub

是任务失效,其实就是单纯的将 Job 从 queue 中删除了。

1
2
3
4
5
6
export function invalidateJob(job: SchedulerJob) {
  const i = queue.indexOf(job);
  if (i > -1) {
    queue.splice(i, 1);
  }
}

测试:

 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
// 源文件:/js/vue/lib.js
const {
  rc: { queueJob, queuePostFlushCb, invalidateJob, nextTick },
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");

const run = async () => {
  const calls = [];
  const job1 = () => {
    calls.push("job1");
    invalidateJob(job2); // 这里将 job2 从 queue[] 中删除了
    job2(); // 注释这个结果会是: job1 job3 job4
  };
  const job2 = () => {
    calls.push("job2");
  };
  const job3 = () => {
    calls.push("job3");
  };
  const job4 = () => {
    calls.push("job4");
  };

  queueJob(job1);
  queueJob(job2);
  queueJob(job3);
  queuePostFlushCb(job4);
  await nextTick();
  log.newline(calls);
};
run();
undefined

job1 job2 job3 job4

job sort id 任务可以排序

只有 post 和 job 支持排序。

测试:

 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
// 源文件:/js/vue/lib.js
const {
  rc: { queueJob, queuePostFlushCb, nextTick },
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");

const run = async () => {
  const calls = [];
  const job1 = () => calls.push("job1");
  const job2 = () => calls.push("job2");
  const job3 = () => calls.push("job3");
  // job1 no id
  job2.id = 2;
  job3.id = 1;

  const cb1 = () => calls.push("cb1");
  const cb2 = () => calls.push("cb2");
  const cb3 = () => calls.push("cb3");
  cb1.id = 2;
  // cb2 no id
  cb3.id = 1;

  queueJob(job1);
  queueJob(job2);
  queueJob(job3);
  queuePostFlushCb(cb1);
  queuePostFlushCb(cb2);
  queuePostFlushCb(cb3);
  await nextTick();
  log.newline(calls);
};
run();
undefined

job3 job2 job1 cb3 cb1 cb2

allowRecurse 自身递归

用 job.allowRecurse 来控制 job 是否可以自己触发自己执行(PS. pre/job/post 都支持 该属性)。

/img/vue3/runtime-core/vue-runtime-core-job-allowRecurse.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
// 源文件:/js/vue/lib.js
const {
  rc: { queueJob, nextTick },
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");

const run = async () => {
  let count = 0;
  const job = () => {
    if (count < 3) {
      count++;
      queueJob(job);
    }
  };
  queueJob(job);
  queueJob(job);
  await nextTick();
  log.newline("before count: " + count);
  // 设置 allowRecurse = true 允许自我调度
  count = 0;
  job.allowRecurse = true;
  // 重复入列同一个任务会在 push 阶段就检测和自身递归调用不同
  queueJob(job);
  queueJob(job);
  await nextTick();
  log.newline("after count: " + count);
};
run();
undefined

before count: 1


after count: 3

checkRecursiveUpdates

feat(add): rc->scheduler->checkRecursiveUpdates · gcclll/stb-vue-next@7bcc14b · GitHub

限制调用自身的次数,在 allowRecurse = true 情况下使用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 源文件:/js/vue/lib.js
const {
  rc: { queueJob, nextTick },
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");

const run = async () => {
  let count = 0;
  const job = () => {
    if (count < 101) {
      count++;
      queueJob(job);
    }
  };
  job.allowRecurse = true;
  queueJob(job);
  try {
    await nextTick();
  } catch (e) {
    log.newline(e.message);
  }
};
run();
undefined

Maximum recursive updates exceeded. This means you have a reactive effect that is mutating its own dependencies and thus recursively triggering itself. Possible sources include component template, render function, updated hook or watcher source function.

小结

/img/vue3/runtime-core/vue-runtime-core-scheduler-comparation.jpg

pre cbs: 执行优先级最高,在同一 tick 中会递归调用自身清空 pendingPreFlushCbs 中的任务,在 queueJob 中调用时不会自动触发需要手动触发执行,因为此时 isFlushing = true

job: 执行优先级次之,在同一 tick 中同一个 for queue -> flushIndex 下会处理此 时接受到的新任务,在 pre cbs 中调用时会在所有 pre cbs 执行之后执行。

post cbs: 执行优先级最低,在同一 tick 同一次 flushPostFlushCbs() 调用中不会 处理新的 post 任务,而是在 flushJobs() 执行到最后 finally 部分检 测 pendingPostFlushCbs 任务队列来处理当前 tick 下新接受到的任务, 在 queuePreFlushCb()queueJob() 中调用的时候会在他们的任务之后执行。

🐛 BUGs fix & Questions

fix: no import EMPTY_ARR · gcclll/stb-vue-next@2a1ab04 · GitHub

nextTick() 后面的代码最后执行?

测试代码: nextTick

先看一段代码,以及 babeljs.io 转换之后的结果:

babel 之前:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const run = async () => {
  const p = Promise.resolve().then();

  const p1 = p.then(() => console.log("before await"));
  console.log("between await and p1");
  await p1;
  console.log("after await");
  const p2 = Promise.resolve().then();
  await p2;
  console.log("after p2");
};
run();

babel 之后(只贴出核心部分):

 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
while (1) {
  switch ((_context.prev = _context.next)) {
    case 0:
      p = Promise.resolve().then();
      p1 = p.then(function () {
        return console.log("before await");
      });
      console.log("between await and p1");
      _context.next = 5;
      return p1;

    case 5:
      console.log("after await");
      p2 = Promise.resolve().then();
      _context.next = 9;
      return p2;

    case 9:
      console.log("after p2");

    case 10:
    case "end":
      return _context.stop();
  }
}

即上面的代码被转换之后变成了一个 switch,里面是一个 while 循环,异步代码最终的顺 序执行由 _context.next 来衔接。

case 0 -> next = 5 -> case 5 -> next = 9 -> …

所以说 nextTick() 后面的代码都会被放到异步代码

runtime-test 模块简介

这里测试需要用到这个模块,所以简单用脑图描述下这里面有哪些东西和干什么的。

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

重要代码:

 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
// nodeOpts.ts

// 1. createElement
// 2. createText
// 3. createComment
// 上面三个函数分别创建了三种类型节点
const node = {
  id: nodeId++,
  type: NodeTypes.ELEMENT, // TEXT | COMMENT
  text: value, // 节点具体内容
  parentNode: null,
  // 下面是 ELEMENT 类型拥有的属性
  children: [],
  props: {},
  tag,
  eventListeners: null,
};

// 4. setText(node, text), 直接修改 node.text
function setText(node: TestText, text: string) {
  logNodeOp({
    type: NodeOpTypes.SET_TEXT,
    targetNode: node,
    text,
  });
  node.text = text;
}

// 5. 插入 insert(child, parent, ref?)
function insert(child: TestNode, parent: TestElement, ref?: TestNode | null) {
  let refIndex;
  if (ref) {
    refIndex = parent.children.indexOf(ref);
    if (refIndex === -1) {
      console.error("ref: ", ref);
      console.error("parent: ", parent);
      throw new Error("ref is not a child of parent");
    }
  }
  // ...log
  // remove the node first, but don't log it as a REMOVE op
  remove(child, false);
  // re-calculate the ref index because the child's removal may have affected it
  refIndex = ref ? parent.children.indexOf(ref) : -1;
  if (refIndex === -1) {
    parent.children.push(child);
    child.parentNode = parent;
  } else {
    parent.children.splice(refIndex, 0, child);
    child.parentNode = parent;
  }
}

// 6. remove(child)
function remove(child: TestNode, logOp: boolean = true) {
  const parent = child.parentNode;
  if (parent) {
    const i = parent.children.indexOf(child);
    if (i > -1) {
      parent.children.splice(i, 1);
    } else {
      console.error("target: ", child);
      console.error("parent: ", parent);
      throw Error("target is not a childNode of parent");
    }
    child.parentNode = null;
  }
}

// 7. setElementText(), 直接清空替换 children,不如 patchChildren 中
// 对 new/old 类型不同的时候需要 full diff 时更新节点
function setElementText(el: TestElement, text: string) {
  // ...
  el.children.forEach((c) => {
    c.parentNode = null;
  });
  if (!text) {
    el.children = [];
  } else {
    el.children = [
      {
        id: nodeId++,
        type: NodeTypes.TEXT,
        text,
        parentNode: el,
      },
    ];
  }
}

重要类型声明

  1. 异步组件选项

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
    export interface AsyncComponentOptions<T = any> {
    loader: AsyncComponentLoader<T>
    loadingComponent?: Component
    errorComponent?: Component
    delay?: number
    timeout?: number
    suspensible?: boolean
    onError?: (
        error: Error,
        retry: () => void,
        fail: () => void,
        attempts: number
    ) => any
    }
    
  2. Vue App 类型

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    
     export interface App<HostElement = any> {
       version: string;
       config: AppConfig;
       use(plugin: Plugin, ...options: any[]): this;
       mixin(mixin: ComponentOptions): this;
       component(name: string): Component | undefined;
       component(name: string, component: Component): this;
       directive(name: string): Directive | undefined;
       directive(name: string, directive: Directive): this;
       mount(
         rootContainer: HostElement | string,
         isHydrate?: boolean
       ): ComponentPublicInstance;
       unmount(rootContainer: HostElement | string): void;
       provide<T>(key: InjectionKey<T> | string, value: T): this;
    
       // internal, but we need to expose these for the server-renderer and devtools
       _uid: number;
       _component: ConcreteComponent;
       _props: Data | null;
       _container: HostElement | null;
       _context: AppContext;
     }
    

    App 配置:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
     export interface AppConfig {
       // @private
       readonly isNativeTag?: (tag: string) => boolean;
    
       performance: boolean;
       optionMergeStrategies: Record<string, OptionMergeFunction>;
       globalProperties: Record<string, any>;
       isCustomElement: (tag: string) => boolean;
       errorHandler?: (
         err: unknown,
         instance: ComponentPublicInstance | null,
         info: string
       ) => void;
       warnHandler?: (
         msg: string,
         instance: ComponentPublicInstance | null,
         trace: string
       ) => void;
     }
    

    Vue 插件类型:

    1
    2
    3
    4
    5
    6
    
     type PluginInstallFunction = (app: App, ...options: any[]) => any;
     export type Plugin =
       | (PluginInstallFunction & { install?: PluginInstallFunction })
       | {
           install: PluginInstallFunction;
         };
    
  3. api watch 类型

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
     export interface WatchOptionsBase {
       flush?: "pre" | "post" | "sync";
       onTrack?: ReactiveEffectOptions["onTrack"];
       onTrigger?: ReactiveEffectOptions["onTrigger"];
     }
    
     export interface WatchOptions<Immediate = boolean> extends WatchOptionsBase {
       immediate?: Immediate;
       deep?: boolean;
     }
    
  4. component 组件类型

     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
    
     // 内部选项
     export interface ComponentInternalOptions {
       /**
     ,* @internal
     ,*/
       __props?: NormalizedPropsOptions;
       /**
     ,* @internal
     ,*/
       __emits?: ObjectEmitsOptions | null;
       /**
     ,* @internal
     ,*/
       __scopeId?: string;
       /**
     ,* @internal
     ,*/
       __cssModules?: Data;
       /**
     ,* @internal
     ,*/
       __hmrId?: string;
       /**
     ,* This one should be exposed so that devtools can make use of it
     ,*/
       __file?: string;
     }
    
     // 函数式组件
     export interface FunctionalComponent<P = {}, E extends EmitsOptions = {}>
       extends ComponentInternalOptions {
       // use of any here is intentional so it can be a valid JSX Element constructor
       (props: P, ctx: Omit<SetupContext<E>, "expose">): any;
       props?: ComponentPropsOptions<P>;
       emits?: E | (keyof E)[];
       inheritAttrs?: boolean;
       displayName?: string;
     }
    
     // 类组件
     export interface ClassComponent {
       new (...args: any[]): ComponentPublicInstance<any, any, any, any, any>;
       __vccOpts: ComponentOptions;
     }
    
     // 生命周期函数缩写
     export const enum LifecycleHooks {
       BEFORE_CREATE = "bc",
       CREATED = "c",
       BEFORE_MOUNT = "bm",
       MOUNTED = "m",
       BEFORE_UPDATE = "bu",
       UPDATED = "u",
       BEFORE_UNMOUNT = "bum",
       UNMOUNTED = "um",
       DEACTIVATED = "da",
       ACTIVATED = "a",
       RENDER_TRIGGERED = "rtg",
       RENDER_TRACKED = "rtc",
       ERROR_CAPTURED = "ec",
     }
    
     // setup 函数
     export interface SetupContext<E = EmitsOptions> {
       attrs: Data;
       slots: Slots;
       emit: EmitFn<E>;
       expose: (exposed: Record<string, any>) => void;
     }
    
  5. component internal instance

    这里涵盖了一个组件都有哪些属性:

    uid, type, parent, root, appContext, vnode, next, subTree, update,

    render, ssrRender, provides, effects, accessCache, renderCache,

    components, directives, propsOptions, emitsOptions,

    proxy, exposed, withProxy, ctx,

    data, props, attrs, slots, refs, emit,

    emitted, setupState, devtoolsRawSetupState, setupContext,

    suspense, suspenseId, asyncDep, asyncResolved,

    isMounted, isUnmounted, isDeactivated,

    bc, c, bm, m, bu, u, bum, um, da, a, rtg, rtc, ec

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
     export const enum LifecycleHooks {
       BEFORE_CREATE = "bc",
       CREATED = "c",
       BEFORE_MOUNT = "bm",
       MOUNTED = "m",
       BEFORE_UPDATE = "bu",
       UPDATED = "u",
       BEFORE_UNMOUNT = "bum",
       UNMOUNTED = "um",
       DEACTIVATED = "da",
       ACTIVATED = "a",
       RENDER_TRIGGERED = "rtg",
       RENDER_TRACKED = "rtc",
       ERROR_CAPTURED = "ec",
     }
    

    类型:

      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
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    
     /**
      * We expose a subset of properties on the internal instance as they are
      * useful for advanced external libraries and tools.
      */
     export interface ComponentInternalInstance {
       uid: number;
       type: ConcreteComponent;
       parent: ComponentInternalInstance | null;
       root: ComponentInternalInstance;
       appContext: AppContext;
       /**
        * Vnode representing this component in its parent's vdom tree
        */
       vnode: VNode;
       /**
        * The pending new vnode from parent updates
        * @internal
        */
       next: VNode | null;
       /**
        * Root vnode of this component's own vdom tree
        */
       subTree: VNode;
       /**
        * The reactive effect for rendering and patching the component. Callable.
        */
       update: ReactiveEffect;
       /**
        * The render function that returns vdom tree.
        * @internal
        */
       render: InternalRenderFunction | null;
       /**
        * SSR render function
        * @internal
        */
       ssrRender?: Function | null;
       /**
        * Object containing values this component provides for its descendents
        * @internal
        */
       provides: Data;
       /**
        * Tracking reactive effects (e.g. watchers) associated with this component
        * so that they can be automatically stopped on component unmount
        * @internal
        */
       effects: ReactiveEffect[] | null;
       /**
        * cache for proxy access type to avoid hasOwnProperty calls
        * @internal
        */
       accessCache: Data | null;
       /**
        * cache for render function values that rely on _ctx but won't need updates
        * after initialized (e.g. inline handlers)
        * @internal
        */
       renderCache: (Function | VNode)[];
    
       /**
        * Resolved component registry, only for components with mixins or extends
        * @internal
        */
       components: Record<string, ConcreteComponent> | null;
       /**
        * Resolved directive registry, only for components with mixins or extends
        * @internal
        */
       directives: Record<string, Directive> | null;
       /**
        * reoslved props options
        * @internal
        */
       propsOptions: NormalizedPropsOptions;
       /**
        * resolved emits options
        * @internal
        */
       emitsOptions: ObjectEmitsOptions | null;
    
       // the rest are only for stateful components ---------------------------------
    
       // main proxy that serves as the public instance (`this`)
       proxy: ComponentPublicInstance | null;
    
       // exposed properties via expose()
       exposed: Record<string, any> | null;
    
       /**
        * alternative proxy used only for runtime-compiled render functions using
        * `with` block
        * @internal
        */
       withProxy: ComponentPublicInstance | null;
       /**
        * This is the target for the public instance proxy. It also holds properties
        * injected by user options (computed, methods etc.) and user-attached
        * custom properties (via `this.x = ...`)
        * @internal
        */
       ctx: Data;
    
       // state
       data: Data;
       props: Data;
       attrs: Data;
       slots: InternalSlots;
       refs: Data;
       emit: EmitFn;
       /**
        * used for keeping track of .once event handlers on components
        * @internal
        */
       emitted: Record<string, boolean> | null;
    
       /**
        * setup related
        * @internal
        */
       setupState: Data;
       /**
        * devtools access to additional info
        * @internal
        */
       devtoolsRawSetupState?: any;
       /**
        * @internal
        */
       setupContext: SetupContext | null;
    
       /**
        * suspense related
        * @internal
        */
       suspense: SuspenseBoundary | null;
       /**
        * suspense pending batch id
        * @internal
        */
       suspenseId: number;
       /**
        * @internal
        */
       asyncDep: Promise<any> | null;
       /**
        * @internal
        */
       asyncResolved: boolean;
    
       // lifecycle
       isMounted: boolean;
       isUnmounted: boolean;
       isDeactivated: boolean;
       /**
        * @internal
        */
       [LifecycleHooks.BEFORE_CREATE]: LifecycleHook;
       /**
        * @internal
        */
       [LifecycleHooks.CREATED]: LifecycleHook;
       /**
        * @internal
        */
       [LifecycleHooks.BEFORE_MOUNT]: LifecycleHook;
       /**
        * @internal
        */
       [LifecycleHooks.MOUNTED]: LifecycleHook;
       /**
        * @internal
        */
       [LifecycleHooks.BEFORE_UPDATE]: LifecycleHook;
       /**
        * @internal
        */
       [LifecycleHooks.UPDATED]: LifecycleHook;
       /**
        * @internal
        */
       [LifecycleHooks.BEFORE_UNMOUNT]: LifecycleHook;
       /**
        * @internal
        */
       [LifecycleHooks.UNMOUNTED]: LifecycleHook;
       /**
        * @internal
        */
       [LifecycleHooks.RENDER_TRACKED]: LifecycleHook;
       /**
        * @internal
        */
       [LifecycleHooks.RENDER_TRIGGERED]: LifecycleHook;
       /**
        * @internal
        */
       [LifecycleHooks.ACTIVATED]: LifecycleHook;
       /**
        * @internal
        */
       [LifecycleHooks.DEACTIVATED]: LifecycleHook;
       /**
        * @internal
        */
       [LifecycleHooks.ERROR_CAPTURED]: LifecycleHook;
     }
    
  6. emit fn 事件

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
     export type EmitFn<
       Options = ObjectEmitsOptions,
       Event extends keyof Options = keyof Options
     > = Options extends Array<infer V>
       ? (event: V, ...args: any[]) => void
       : {} extends Options // if the emit is empty object (usually the default value for emit) should be converted to function
       ? (event: string, ...args: any[]) => void
       : UnionToIntersection<
           {
             [key in Event]: Options[key] extends (...args: infer Args) => any
               ? (event: key, ...args: Args) => void
               : (event: key, ...args: any[]) => void;
           }[Event]
         >;