Vue3 源码头脑风暴之 7 ☞ runtime-core(1)
文章目录
stb-vue-next 完全拷贝于 vue-next ,主要目的用于学习。
声明 :vue-next runtime-core 运行时核心代码,这部分内容较多,可能会分为几篇来 叙述,
f
过滤掉对象空值属性。更新日志&Todos :
[2021-01-08 10:12:50] 创建
DONE [2021-01-15 10:24:00] scheduler
TODO apiWatch -> post cb & ssr
TODO STATEFUL_COMONENT
TODO patchFlag 测试和用途
TODO transformVNodeArgs
TODO Suspense 组件
TODO shouldTrack, currentBlock 和 block 相关函数的作用
TODO setup() 返回值用来做了啥?
TODO setup() 里面是如何收集生命周期函数,又是如何?在什么时候?执行他们的?
TODO async component
模块初始化: feat(init): runtime-core · gcclll/stb-vue-next@b22b4db · GitHub
⚠ Tips
class 支持数组(
['foo', 'bar']
),对象({foo:true,bar:false}
),字符串('foo bar'
)style 支持数组(
['color:red', {foo:'foo'}]
),对象({color:'red',foo:'foo'}
),字符串(color:red
)class component 条件:
function
含
__vccOpts = { template: '<div />'}
h() 和 createVNode 函数多种使用方式组合?
h(type, propsOrChildren, ...children)
, 参数个数多变,对于这个函数的使用方法 记忆只要记住一点:props 总是对象,children 可以是对象(必须是 VNode 类型
__v_isVNode
)也可以是 数组,所以:argc = 2, 如果是数组就一定是 children
argc = 2, 如果是对象且有 __v_isVNode 标识,一定是 children 否则是 props
argc = 3, 按照
h(type, props, children)
处理argc > 3, 按照
h(type, props, ...children)
处理,从第三个开始都是 children
createVNode(type, props, children)
, 固定三个参数,第二个一定是 props, 第三 个一定是数组类型的 children,因为它后面还有更多的其他参数(patchFlag, dynamicProps, isBlockNode),所以前三个必须确定下来。api watch(source, cb, option) 中的 source 只能是
reactive/ref/function/array
类型, 如果是数组时其元素只能是reactive/ref/function
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 又是什么? });
provide(key,value) 向组件
provides[key] = value
设置inject(key) 从组件
provides[key]
取值TODO setup() 返回值用来做了啥?
🐂 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 函数初始化。
|
|
实现:
|
|
h, 接受不定参数
逻辑脑图:
从脑图分支得出支持的情况代码示例:
h('div')
无参数无孩子h('div', { id: 'foo' })
有 props 无 childrenh('div', ['foo'])
数组当做 chilrenh('div', vnode)
有 __v_isVNode 标识当做 children,并转成数组[vnode]
h('div', {}, ['foo'])
有 props 有 childrenh('div', {}, vnode)
有 props, 有 children 且 =[vnode]
接下来需要具体去实现 createVNode
函数。
🌿 createVNode function
feat(add): rc->createVNode · gcclll/stb-vue-next@194f72f · GitHub
这个函数最终是构造了 vnode: VNode 虚拟节点结构,返回。
这里面分为以下几个步骤实现:
type 是 vnode 时候处理
class 组件处理
props 处理
shapeFlag 检测,是什么类型 的 vnode
组件对象不应该 reactive(有状态的组件, STATEFUL_COMONENT)
构建 vnode: VNode 对象
检测 vnode.key 是不是
NaN
normalize children
normalize suspense children
currentBlock 处理
返回 vnode 节点
|
|
>>> 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 属性。
|
|
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(); }
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; } }
测试:
|
|
>>> 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
是类组件前提是:
必须是函数
必须包含
__vccOpts
属性
|
|
测试:
|
|
{ __v_isVNode: true, __v_skip: true, type: { template: '<div />' }, shapeFlag: 4 // STATEFUL_COMPONENT } undefined
TODO stateful component & key NaN
有状态的组件?
即 type 为对象时候视为有状态的组件。
如果是 STATEFUL_COMPONENT 且是个 proxy 的时候,开发模式下给出警告⚠️。
|
|
{ __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
|
|
cloneVNode 绝大部分属性都是直接引用自 vnode,上面列出的都是需要处理的属性,比如:
props 会将 vnode 和 cloneVNode 传入的 props 进行合并,并且是传入的 props 覆盖 vnode.props。
key 属性,取合并之后的 key(测试->)
1 2 3 4 5
// normalize 合并后的 key const key = mergedProps && normalizeKey(mergedProps); const normalizeKey = ({ key }: VNodeProps): VNode["key"] => key != null ? key : null;
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; };
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;
ssContent 递归调用
cloneVNode(vnode.ssContent)
ssFallback 递归调用
cloneVNode(vnode.ssFallback)
测试:
|
|
>>> 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 操作,属于单纯的值覆盖操作。
|
|
>>> 保留 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
流程脑图:
测试
|
|
>>> 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.
|
|
shapeFlag test
|
|
>>> ELEMENT { shapeFlag: 1 } >>> STATEFUL_COMONENT { shapeFlag: 4 } >>> FUNCTION_COMONENT { shapeFlag: 2 } >>> Text { shapeFlag: 0 } undefined
mergeProps test
|
|
>>> 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
|
|
TODO transformVNodeArgs test
TODO 7ec1d30 suspense component
feat(add): rc->createVNode, type is suspense component · gcclll/stb-vue-next@7ec1d30 · GitHub
Suspense 的 children 必须有且只有一个根节点。
|
|
检测是不是 single root 函数: filterSingleRoot
|
|
TODO 23fc943 currentBlock 优化
feat(add): rc->createVNode, optimize diff, currentBlock · gcclll/stb-vue-next@23fc943 · GitHub
这里的处理没怎么搞明白❓
注意这里增加的几个变量‼
blockStack, currentBlock:
|
|
shouldTrack:
|
|
新增处理逻辑:
|
|
跟这几个变量有关的函数:
normalizeChildren function
shapeFlag 初始值检测:
|
|
测试:
|
|
>>> 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:
|
|
测试:
|
|
>>> 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
|
|
普通类型处理中如果是 ShapeFlags.TELETPORT
当做 ARRAY_CHILDREN
处理,且
children 按照文本节点处理。
|
|
>>> 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
|
|
>>> 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
脑图:
为了更好的完成 apiWatch, 需要先完成了 scheduler 任务调度部分。
watch(source, cb, options)
函数以下种使用方式(下面的 cb 均可选参数):
watch(fn)
等价于watchEffect(fn)
, 无 cbwatch(fn, cb)
监听函数watch(ref(0), cb)
watch(reactive({ count: 0}), cb)
, reactive 对象默认deep = true
watch([ref(0), reactive({count: 0})], cb)
watch(fn, cb, { immediate: true })
此时, cb 必须为函数, job->fn 被立即执 行一次, cb 接受新旧值watch(ref({ count: 0}), cb, { deep: true })
手动指定deep: true
深度监听…
执行具体实现的函数: doWatch()
Arg | value | description |
---|---|---|
source | WatchSource, WatchSource[], WatchEffect, object | object watched |
cb | WatchCallback or null | callback |
options | WatchOptions = EMPTY_OBJ | |
immediate | ||
deep | ||
flush | ||
onTrack | ||
onTrigger | ||
instance | currentInstance | - |
watch(source, cb, options?)
函数中的 cb 是必选项,如果想直接 watch effect,可使 用watchEffect(fn, options?)
api 。
watch 函数基本流程:
cb, immediate, deep 检测
getter, 根据 source 不同类型设置 getter
cb + deep: true
SSR node env
将 cb 封装成 job
runner = effect(getter, option)
runner 如何执行?
stop, remove,函数返回一个 stop+remove 该 runner 操作的函数
下面章节中测试的用例分析脑图:
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 测试:
|
|
undefined value changed: 0 1 0
有关代码(doWatch):
|
|
source is reactive
如果要 watch 的对象是个 reactive ,需要进行递归 watch ,得到 getter.
fix: watch->source is reactive · gcclll/stb-vue-next@697f7f2 · GitHub
新增相关代码:
|
|
递归监听 reactive 对象任意层级上的属性变化。
|
|
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 自身。
soure is array
feat(add): apiWatch->source is array · gcclll/stb-vue-next@af1e590 · GitHub
如果要监听的对象是个数组的时候,需要检测数组元素的类型,针对不同类型进行处理。
要点:
数组元素不能是除 ref/reactive/function 之外的类型
对数组元素设值时必须通过元素原始设值方式进行(比如: ref 要
ref.value = xxx
), 因为该数组本身不是 reactive 的
|
|
isRef -> 监听 item.value
isReactive -> traverse(item) 递归
isFunction -> callWithErrorHandling(item, instance, …) 监听函数返回值
其他类型不支持 -> warn invalid source
测试:
|
|
undefined [ 1 ]
数组混合模式(元素只支持 ref, reactive, function):
|
|
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])
直接执行这个函数去收集依赖。
新增代码:
|
|
feat(add): rc->api watch->source is function without cb · gcclll/stb-vue-next@9565b4a · GitHub
测试:
|
|
undefined { dummy: 1 } with cb [ 100, 1 ]
feat(add): rc->api watch->source invalid warning · gcclll/stb-vue-next@11ee8ef · GitHub
这里有个容易搞混淆的地方,
watch(fn, cb)
的时候,虽然 fn 和 cb 都是函数,但 是要区分开这两者,并搞清楚他们是啥和关系是啥。
fn 是被检测的对象,如果是 function 那在被监听之前需要先执行它,等于是监听 函数里面的内容,比如:函数内有访问某个 reactive 变量
而 cb 是属于回调性质,且是当数据有更新的时候的回调函数,它只会在一个地方被 执行,即封装 job 的时候,需要将数据更新前后的变化值通过它传递出来(如下面👇的 代码)
|
|
option deep
对于深度监听主要是因为 traverse()
函数对 reactive 对象进行了递归遍历,对每个属
性进行了访问,从而让它收集到当前的 effect 作为依赖,这样将来这些被遍历到的值发生
改变时就会触发这个收集到的 effect 执行,达到深度监听效果。
|
|
traverse()
作用就是递归遍历所有属性通过return value
来执行 get 操作收集依赖。
测试:
|
|
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 立即执行一次,而不是在队列中等待异步执行。
新增代码只需要加一行:
|
|
测试:
|
|
改变值之前 > 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 会给出警告,直接看源码吧:
|
|
也就是说,
deep
和immediate
建议在watch(s, cb, options)
形式下使用,即在 有 cb 参数的情况下使用。那为什么呢?
option onTrack + onTrigger
这部分实现逻辑主要在 reactivity 模块。
onTrack 在 reactivity 中使用的,用来在触发 get 取值操作时调用 track() 函数收集依 赖时的一个自定义事件回调。
|
|
这里会将 当前 target 的 key 属性所收集的依赖 activeEffect 暴露出来。
测试:
|
|
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 }
stop & cleanup
stop: watch() 的返回值,用来停掉 effect 使其 effect.active = false,让 effect 失效。
|
|
cleanup: 清理工作,这有两个被调用的地方(cleanup + onStop它们被注册了同一个函数), 一个是调动 cb/fn 之前,一个是 runner effect 调用 stop 的时候。
|
|
stop
stop 是 watch 调用的返回值,里面会 stop runner 然后将 runner 从 instance.effects
里面删除。
|
|
undefined { dummy: 1 } { dummy: 1 }
可以看到 stop 之后两次输出结果是一样,即 stop 后面的 state.count 失效了,因为 stop effect 会将 effect.active 置为 false ,有如下代码被执行:
|
|
又, watch 函数里面无论如何 scheduler 都是有值的,所以当 effect 为非激活状态,什 么都不会干。
cleanup(无 cb)
cleanup 相关源码,可能有点绕:
|
|
测试:
|
|
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 的第三个参数暴露出来
|
|
undefined called 1 times, dummy = 1 called 2 times, dummy = 100 { n: 2 }
脑图分析:
文字分析:
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。
执行时机,该阶段和注册时机相辅相成,且在 cb/fn 执行之前就会被执行,因此 cb/fn 的第一次执行都属于对 cleanup 的注册
flush sync
feat(add): rc->api watch->option flush=sync · gcclll/stb-vue-next@e1436f2 · GitHub
支持同步代码,即所有任务立即执行(在值发生改变之后),而不是进入队列异步执行。
只需要增加一行代码就行:
|
|
新增 scheduler = job
直接让任务函数赋值给调度器,这个时候如果有值发生变化,会
触发 effect> trigger()
在这里面会检测是不是有 option.scheduler
如果有会立即执行这
个函数。
|
|
测试:
|
|
{ calls: 0 } { calls: 1 } undefined
注意看上面的测试用例并没有用 await nextTick()
,而是同步代码执行。
shallow ref
|
|
undefined should not trigger: 0 should not trigger: 0 should trigger now: 2
ref 这一块还没深入去分析过,先暂停⏸去完成下这部分。
DONE [2021-01-20 15:18:37] ref 完成,可以往下继续了
triggerRef 作用是手动触发 ref.value 上收集的所有依赖。
结果分析:
#1 shallowRef 意味着
{a: 1}
中的属性 a 非 reactive#2 watch v 基于 1 所以只是对 ref value 进行了监听,后面是值变更回调
#3 值没发生改变,所有 #4 输出还是 0
#5 由于
a
属性非 reactive 所以它没有依赖收集所以不会执行 cb,所以 #6 出 依然是 0#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
新增代码:
|
|
TODO ssr support
TODO instance watch
feat(add): rc->watch->instance watch · gcclll/stb-vue-next@9ec5d51 · GitHub
|
|
将监听源,与当前实例绑定,如果是字符串转成函数。
🍺 小结
这一节所描述的主要是 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),防止执行报错阻碍整体页面的执行。
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 暴露出 去的一个清理函数它会被绑定到cleanup
和option.onStop
上,在 effect 被 stop 或在调用之前做的一些清理工作。这个最终目的是为了对每个属性进行一次 getter 调用,用来 effect -> track 收集每 个属性的依赖列表(reactivity: targetMap -> depsMap -> dep)。
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)。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"
cb && immediate
时候立即执行job()
不走 runnercb && !immediate
立即执行runner()
根据任务性质决定是同步还是异步!cb && flush==='post'
检测如果是 suspense 加入到instance.suspense.effects
否则直接queuePostFlushCb(runner)
这个需要开启
Suspense
组件支持,这个 ast render 函数其实就是在一个async
函数里面执行的代码。else
直接执行runner()
🍎 api createApp
declaration
这里就两个函数
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), }; }
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 | - | |
isNativeTag | NO | |
performance | false | |
globalProperties | {} | |
optionMergeStrategies | {} | |
isCustomElement | NO | |
errorHandler | undefined | |
warnHandler | undefined | |
mixins | [] | |
components | {} | |
directives | {} | |
provides | Object.create(null) |
implementation
feat(add): createApp app apis · gcclll/stb-vue-next@1facd1b · GitHub
context = createAppContext()
创建上下文installedPlugins = new Set()
插件列表isMounted = false
加载完成标识app = context.app = {...}
return app
所以重点就在 4 构造 app,其包含的 API 如下:
name | function |
---|---|
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
|
|
所以插件 plugin 必须要么自己是函数。
app.mixin(mixin)
feat(add): createApp mixin api · gcclll/stb-vue-next@b96afcf · GitHub
|
|
可根据需要在打包的时候由 __FEATURE_OPTIONS_API__
决定是否继续支持 mixin 功能。
app.component(name, component)
feat(add): createApp component api · gcclll/stb-vue-next@9b2579d · GitHub
内置标签: slot,component
|
|
验证组件名,不能用内置(
isBuiltInTag()
)或自定义(isCustomElement()
)的标签作为组件名没有 component 参数的时候视为根据 name 去获取组件
当 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
|
|
app.mount(rootContainer, isHydrate)
|
|
vnode = createVNode(rootComponent, rootProps)
创建虚拟节点vnode.appContext = context
保存上下文到虚拟节点上,组件初始化时使用context.reload = () => render(cloneVNode(vnode), rootContainer)
HMR 开发时 有变更的热加载rootContainer.__vue_app__ = app
devtool 开发工具相关
返回
vnode.component.proxy
这个是干啥的?
最后,如果 app 已经被加载了不能重复 mount。如果需要对一个app做重复 mount,可能的 需求是存在同时创建多个 app 情况?
那么这个时候应该使用函数方式使用,如:
myCreateApp = (...) => createApp(App)
app.unmount()
feat(add): createApp unmount api · gcclll/stb-vue-next@63354d3 · GitHub
|
|
render(null, ...)
?
app.provide(key, value)
feat(add): createApp provide api · gcclll/stb-vue-next@18fb22b · GitHub
|
|
就是简单的给 context.provides
设置操作。
做什么的呢?
test
这里目前只能测试 mount 和 unmount 其他的 api(provide/…) 需要等它们被实现了才能 继续测试。
|
|
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)
在子组件中可以取到该值。
|
|
涉及内容
provides 继承 parent provides,需要自己的就创建新对象继承自 parent provides, 所以查找 injections 可以在原型链查找。
inject(key, defaultValue, treatDefaultAsFactory = false)
你只是个简单的取值 操作 (provides[key]
)?😭?😭?
Imp. provide 和 inject 只能在
setup()
或函数式组件中使用,它们的作用是根据原 型链特性实现从父组件向子组件传递一些值。
provide(key, value)
feat(add): provide & inject provide implementation · gcclll/stb-vue-next@42f3d05 · GitHub
|
|
Object.create(parentProvides)
创新新的对象来容纳新的 key-value
inject(key, defaultValue, treatDefaultAsFactory = false)
|
|
inject 就是个简单的取值操作而已。
取值来源需要检测是不是根节点,如果是根节点
provides = instance.vnode.appContext.provides
否则,使用 instance.parent.provides
然后根据 key
取出值 provides[key]
如果有默认值需要返回默认值?
defaultValue
或 defaultValue()
居然如此简单~~~,还是先看下怎么用吧!!!
test
|
|
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
的应用其实就是原型链的应用,目的是为了让父组件可以像子组
件注入一些值。
那么为什么注入的值直接变成了
<div>1</div>
子节点了?
✨ api computed
这个 api 是对 reactivity>computed 的一次简单封装,详情请直接查看 reactivity对应的 computed 一节。
代码:
|
|
🌀 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 。
|
|
如上,所有的声明周期函数都是通过 createHook -> injectHook 实现,所以这部分的重点
就是这个 injectHook(type, hook, targt, prepend)
。
在 createHook()
里面有检测是不是 SSR 环境,只有非 SSR 环境下才会有声明周期。
feat(add): api lifecycle injectHook · gcclll/stb-vue-next@6e9cd14 · GitHub
|
|
lifecycle hook 可以在 setup() 中执行
这里的所有声明周期函数都以事件形式存在,意味着可以通过这些 api 注册哪个声明周 期需要执行操作的函数
在声明周期函数执行期间不进行
track
收集依赖操作,它有可能在 setup() 中进行
使用原理:调用
onXxx(fn, ins)
声明周期函数时,实际上只是向当前的实例currentInstane[type] = []
上注册了一个回调函数 fn。比如:
target.onMounted = [fn1, fn2] // target -> currentInstance 当前实例
然后在组件 render/update 过程中根据声明周期的阶段调用对应的 fns 。
test base
|
|
-> 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
中执行值更新操作会怎样?
|
|
undefined -> onBeforeUpdate, called = 1, serialize root = "<div>0</div>" } -> onUpdated, count.value = 2, <div>2</div> after set value... <div>2</div>
最后 count.value 渲染出来是 2?
test unmount
|
|
undefined -> onBeforeUnmount, called = 1, serialize root = "<div></div>" } -> onBeforeUnmount called in onMounted -> onUnmounted, called = 2, serialize root = "<!---->" } after set value... <!---->
test order
测试声明周期函数执行顺序(和调用顺序无关)
|
|
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' ]
从上面的结果可知整个组件声明周期发生过程。
组件 mount 顺序, child -> mid -> comp,子组件 -> … -> 父组件
组件 update 顺序, child -> mid -> comp, 子组件 -> … -> 父组件
组件 unmount 顺序, child -> mid -> comp, 子组件 -> … -> 父组件
组件的 before 声明周期触发顺序是: child -> mid -> comp 完成之后的顺序刚好相反。
test render track & trigger
|
|
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
|
|
🆘 api setup helpers
feat(add): api setup helpers · gcclll/stb-vue-next@3e71787 · GitHub
|
|
这里包含三个函数,其中 defineProps&defineEmit
都是且只会在 <script setup>
里
面使用,他们的作用只是用于 compiler-sfc
包中在 babel/parser
解析语法树期间,
用来标识他们是 props或 emits 的一部分,然后给他们的参数会被当做 props 和 emits
合并到最终 export 出去的对应属性上,所以这两个 define 函数实际上什么都没做。
看实例吧 ->
|
|
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
我们把上面结果格式化下:
|
|
从上面的代码可发现:
defineProps 最后被合并成一个 props 对象,并且会和
<script>
中的 export default 中的 props 进行再次合并,并且这种合并属于简单的替换操作所以如果
defineProps
和script
中同时存在 props 的情况最终<script>
中 的 props 会被丢弃掉,所以尽量避免这种情况发生。defineEmit 中的属性会被合并到
emits
上
更多有关 <script setup> 的内容请查看 compiler-sfc
包的分析 ->
useContext()
是获取当前实例的上下文对象,这个需要继续实现复杂的 render 部分才 可能会使用到。
🌊 TODO api async comopnent
feat(add): async component · gcclll/stb-vue-next@cb4474a · GitHub
定义异步组件(defineAsyncComponent(source)
)。
新增初始化:
|
|
通过搜索
__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 实现主要分几步:
封装
retry()
重试函数load()
函数,分支source.loader()
异步函数组装组件返回
defineComponent({__asyncLoader: load, setup() {...}, ...})
前面 1, 2 都是对
source.loader
进行封装处理,这一步是真正的创建异步组件。defineComponent(option) 实现很简单,就是检测 option 如果是函数当做
{setup: option, name: option.name}
处理,否则直接返回 option
测试:
|
|
true xxx resolved comp before loaded.value = false <!----> undefined async comp load ok, resolved
🔥 render function
篇幅太长,另起单独文章 -> Vue3 源码头脑风暴之 7 ☞ runtime-core(2) - render
🚒 scheduler 任务调度机制
让我们跟着 scheduler.spec.ts
测试用例来逐步属性 scheduler 的调度机制。
在做这个之前先把 scheduler.ts 中逻辑代码全清空,这个文件还是相对独立的
feat: rc->reset scheduler.ts · gcclll/stb-vue-next@a54cc00 · GitHub
我们从零开始一步步来分析实现。
这部分包含三种任务的 flush 逻辑代码:
queue jobs ->
flushIndex
->queue[]
->queueJob()
->queueFlush()
->flushJobs()
pre jobs ->
preFlushIndex
->pendingPreFlushCbs[]
->activePreFlushCbs[]
->queuePreFlushCb()
->flushPreFlushCbs()
->flushJobs()
TODO post jobs -> …
nextTick
feat(add): rc->scheduler -> nextTick · gcclll/stb-vue-next@32b4827 · GitHub
在 queue 所有队列清空之后执行的一个异步操作,有重要关联的两个变量:
resolvedPromise,一个空的 promise then
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 所有任务完成之后执行。
|
|
函数作用:在当前正在执行的 job promise 之后执行 nextTick 的任务,等于说 nextTick 属于个插队任务。
|
|
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
|
|
需要 flushJobs 支持,请到 flushJobs(👇) 一节查看测试情况。
flushJobs
feat(add): rc->scheduler->flushJobs function · gcclll/stb-vue-next@e23be11 · GitHub
isFlushPending, isFlushing 标识重置
flushPreFlushCbs, 对 pre 类型的 jobs 进行 flush 操作,有关函数
flushPreFlushCbs(flush函数)
和queuePreFlushCb(入列函数)
flush 之前进行排序
try -> callWithErrorHandling 执行任务回调
finally -> 重置,清空 queue 队列内容和标识
TODO flushPostFlushCbs, 对 post 类型的 jobs 进行 flush 操作,有关函数
flushPostFlushCbs
和queuePostFlushCb
|
|
测试:
|
|
before await undefined job1 running job2 running after await job1,job2
如果在没有 #6 的情况下,在所有 Log 之后会立即执行 queue jobs。
|
|
这里 nextTick() 调用并没有传递 fn ,因此 await nextTick()
在这里的作用就是等
resolvedPromise
执行完成(此时并没有正在执行的 promise)
const resolvedPromise: Promise<any> = Promise.resolve()
再执行后面的代码。
queueJob 函数分为两步:
push 收集任务
queue.push(job)
,同步执行随后立即调用
queueFlush()
刷掉任务,任务异步 flush
在这个实例中,按照同步执行顺序,
queueJob(job1)
执行,将 job1 -> push -> queue 中, queueFlush 中的 promise 等待queueJob(job2)
执行,将 job2 -> push -> queue 中, queueFlush 中的 promise 继续等待log before
执行,由于 job 虽然已经在 queue 中了,但是需要等待 queueFlush 去 异步执行他们,所以这里 calls 依旧是空的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 的执行的。
所有同步任务执行完成,开始进入异步任务执行,由于 job1,job2 先入队列,在事件循 环中会先于 log after 执行,然后在执行 log after,所以就有了上面的输出结果。
实例执行脑图:
queueJob while flushing
当 queue 中 jobs 正在被执行的时候调用 queueJob 进入新的任务。
|
|
undefined after await [ 'job1', 'job2' ]
看下面的测试代码(在 for 循环过程中改变数组长度,会检测到这种改变):
|
|
{ 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
新增代码:
queuePreFlushCb
, 入列 pre jobs 函数flushPreFlushCbs
, flush pre jobs 函数flushJobs
中调用flushPreFlushCbs()
刷掉 pre jobs
这个是用来收集和 flush pre 类型(默认类型的任务)的队列 pendingPreFlushCbs[]
的函数。
逻辑脑图:
相关代码:
|
|
对比 queueCb 和 queueJob 会发现两者没多大的差别,先同步收集再异步 flush,两者判 断条件有细微差别,另外 queueJob 支持数组形式的 cb:
|
|
最后也都是调用 queueFlush() -> flushJobs() 来清空队列 pendingQueue/queue 。
所以下面还需要在 flushJobs() 里面去实现对 pre -> pendingQueue 类型队列 flush 操
作(flushPreFlushCbs()
)。
flushPreFlushCbs
有关函数和变量
name | type | description |
---|---|---|
preFlushIndex | number | used in `for` to flush pre jobs |
pendingPreFlushCbs | array | the queue to store pre jobs |
activePreFlushCbs | array | the non-repeat copy of pendingPreFlushCbs , used to flushing |
queuePreFlushCb | function | 与 flushPreFlushCbs 对应的 pre job 入列函数 |
queueFlush | function | 执行队列任务的函数,三个类型的任务都在这里面执行(pre,post,queue) |
flushJobs | function | 具体执行任务的函数,三种任务执行顺序是: pre -> queue -> post |
Tip.
activePreFlushCbs
和pendingPreFlushCbs
的关系: 前者是后者的一个拷贝, 拷贝完会立即清空 pending, 目的是为了让 pending 在 active flushing 期间能继续收集 新的任务,这样如果在执行期间有新的任务入列,那么在函数最后的递归操作会对这些新入 列的任务继续 flush 掉,直到再也没有新的任务入列为止。注意点 :当
queuePreFlushCb
在 queueJob 中使用时不会主动触发 cbs 执行,如果 需要立即执行这些 cbs 需要手动调用flushPreFlushCbs(seen, parentJob)
去刷掉 pre cbs 任务,或者等到当前 job 执行完了下一个flushJobs()
调用中执行,因为queueJob()
执行期间isFlushing = true
,而在queueFlush()
中有检测这个值, 如果正在执行 flushing 是不会继续执行的,更多详情查看后面的测试和分析。
源码:
|
|
用途: api watch 里面对默认类型(pre
)的任务的入列操作,如下代码:
|
|
测试:
|
|
undefined cb1 cb2 job1
测试分析代码脑图:
文字分析:
#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#1 开始执行
#2 将 cb1 push ->
pendingPreFlushCbs=[cb1]
#3 将 cb2 push ->
pendingPreFlushCbs=[cb1, cb2]
#4 手动 flush pre cbs
在
flushPreFlushCbs(undefind, job1)
中会记录currentPreFlushParentJob = job1
这个变量将会在queueJob(job)
中用来检测 job 是不是当前的 job1 如果是 就不允许 push,因为 job1 下有子任务正在执行,必须等这些子任务(cb1, cb2) 执行完。#6 开始执行, push 'cb1' -> calls: ['cb1']
#7 开始执行, push 'cb2' -> calls: ['cb1', 'cb2']
#5 开始执行, push 'job1' -> alls: ['cb1', 'cb2', 'job1']
#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 log 输出
'cb1,cb2,job1'
queuePostFlushCb + flushPostFlushCbs
feat(add): rc->scheduler->queuePostFlushCb+flushPostFlushCbs · gcclll/stb-vue-next@845c21b · GitHub
逻辑脑图:
有了 queue job 和 pre cb 的基础分析,这部分也就很好理解了。
|
|
和 pre cb 的处理有两个不同点:
非回调形式处理 flushing 期间接受到的新任务,而是通过改变执行器 activePostFlushCbs 来实现(和 queue job 类似)
没有递归回调形式处理后续的新任务,参考 1
测试:
|
|
undefined cb1 cb2 cb3 cb4 job1 cb5 cb6
对于
queuePostFlushCb
和queueJob
的混用只要记住一点,queuePostFlushCb
不 会触发activePostFlushCbs
改变,因为 isFlushing = true,所以只会在当前flushJobs()
执行到最后递归检测的时候才会进入下一次的 post+job 调用。
test nested(pre/job/post)
完整的测试用例,结合 pre, post, queue 三种类型的任务进行测试。
|
|
undefined cb1,cb2,cb3,cb4,job1,cb5
pendingPreFlushCbs 虽然是个数组,但是 flush 期间通过
[...new Set(pendingPreFlushCbs)]
进行了去重操作。链式操作,因为在执行期间使用的是
activePreFlushCbs
且此时的pendingPreFlushCbs
清空了,等待新任务入列在执行 cb3 期间,调用
queuePreFlushCb(cb4)
此时 push cb4 ->pendingPreFlushCbs
,但实际不会影响本次的 for 循环执行这点和 queueJob 有点不同,它直接使用的是 queue -> for 所以有新的任务入列会改 变 for 的执行长度(queue.length)
pre 处理会等到 activePreFlushCbs for 执行循环结束后,在函数的最后递归调用
flushPreFlushCbs()
来刷掉新入列的任务(如: cb4)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
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 中删除了。
|
|
测试:
|
|
undefined job1 job2 job3 job4
job sort id 任务可以排序
只有 post 和 job 支持排序。
测试:
|
|
undefined job3 job2 job1 cb3 cb1 cb2
allowRecurse 自身递归
用 job.allowRecurse 来控制 job 是否可以自己触发自己执行(PS. pre/job/post 都支持 该属性)。
|
|
undefined before count: 1 after count: 3
checkRecursiveUpdates
feat(add): rc->scheduler->checkRecursiveUpdates · gcclll/stb-vue-next@7bcc14b · GitHub
限制调用自身的次数,在 allowRecurse = true 情况下使用。
|
|
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.
小结
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 之前:
|
|
babel 之后(只贴出核心部分):
|
|
即上面的代码被转换之后变成了一个 switch,里面是一个 while 循环,异步代码最终的顺 序执行由 _context.next 来衔接。
case 0
-> next = 5
-> case 5
-> next = 9
-> …
所以说 nextTick() 后面的代码都会被放到异步代码
runtime-test 模块简介
这里测试需要用到这个模块,所以简单用脑图描述下这里面有哪些东西和干什么的。
重要代码:
|
|
重要类型声明
异步组件选项
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 }
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; };
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; }
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; }
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; }
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] >;
这里还需要开发环境才能测试 onTrack,只能改一改去掉
__DEV__
试试。