Vue3 源码头脑风暴之 7 ☞ runtime-core(3) - render component
文章目录
stb-vue-next 完全拷贝于 vue-next ,主要目的用于学习。
本文为 runtime-core(2) 续集,上篇: Vue3 源码头脑风暴之 7 ☞ runtime-core(2) - render
流程图(脑图)
这一节新增内容较多,主要新增以下几个函数
processComponent()
在 patch() 中执行 switch default 分支,满足ShapeFlags.COMPONENT
条件mountComponent(n2,...)
首次加载组件时调用的函数setupComponent(instance)
建立组件实例,做一些结构初始化操作(如:props和 slots)等setupStatefulComponent(instance,isSSR)
创建有状态组件,执行setup()
函数setupRenderEffect()
通过 effect() 函数返回instance.update
创建一个监听- 更新函数。finishComponentSetup(instance,isSSR)
这个函数在setupStatefulComponent()
中调用,主要做的事情是处理 SSR,没有 render 函数有 template 时调用 compile 编 译出 render 函数,兼容 2.x 的 options api
processComponent(如何patch组件的?)
问题修复: TypeError: Cannot read property 'allowRecurse' of null
processComponent(n1,n2,...)
函数主要分三种情况
mount, 没有 n1 old 时候,属于纯 mount 操作
keep-alive 类型,只需要重新激活 activate
否则执行 mountComponent(n2, ….) 首次加载组件
update, 非首次加载执行更新操作
|
|
undefinedroot: component stateful ? 4 call setup no setup [Function: render] render mount component normalize vnode patch component component stateful ? 4 call setup no setup [Function: render] render mount component normalize vnode patch component root: <div>child 1</div> root: <div>child 1</div> component update
流程简图:
这里执行就是 mountComponent(n2,...)
行为,首次加载组件,完成:
setupComponent(instance)
执行 setup 函数,初始化 props&slots 等setupRenderEffect(instance,...)
注册 instance.update effect当实例状态发生改变时执行这个 effect fn,如果是首次(父级调用 processComponent) 执行!isMounted 分支进行组件首次加载,否则当组件自身状态改变是触发的 update 操 作
在 setupComponent
中,主要完成
initProps
initSlots
setupStatefulComponent(instance,isSSR) 有状态组件(非函数组件)
紧接着 setupStatefulComponent(instance,isSSR)
中检测 setup 函数,并执行它,如
果没有 setup 函数就进入 finishComponentSetup(instance) 检测 render 或 template
最终目的是获得 render 函数,如果没有 render 会通过 compile(template) 编译出
render 函数,最后在 instance.update 中执行 render 函数(在这前后会触发
beforeMount 和 mounted 周期函数)。
所以,一套流程下来可以简单描述为
mount -> props&slots 初始化 -> setup() -> 有状态组件处理得到 render 函数 -> 最后 通过 instance.update effect 来监听实例状态变化,触发 mount 或者 update。
在 effect mount 阶段会触发生命周期函数:
beforeMount + mounted
onVnodeBeforeMount + onVnodeMounted(针对 vnode 结构变化而言)
activated(如果是 keep-alive 的话)
组件的渲染就发生在 beforeMount 之后 mounted 之前的 renderComponentRoot() 得到 vnode 交给 patch 去进行渲染。
示例代码中,后面修改了 value.value=false
后面 dom 并没改变,但是输出了
component update 说明进入了 instance.update effect
的 else 分支,因为不是第
一次,所以这里需要实现更新组件部分。
effect update component
因为 instance.update 是通过 effect()
封装的函数,且这个函数中使用到了 instance
实例而这个实例又在 setupComponent 中有做过代理,因此对它的访问会触发 effect
track,状态更新会触发 effect trigger(响应式原理)。
feat(add): component update · gcclll/stb-vue-next@1254465
涉及的修改:
|
|
和 updateComponentPreRender 实现这个函数让 instance.update 在 nextTick() 之后执 行 pre 优先于 post 和 job 任务(详情查看任务调度->):
|
|
之前的用例再测试一遍:
|
|
undefinedroot: component stateful ? 4 call setup no setup [Function: render] render mount component normalize vnode parent render patch component component stateful ? 4 call setup no setup [Function: render] render mount component normalize vnode child render patch component root: <div id="parent"><div>child 1</div></div> before change value component update normalize vnode child render after change value root: <div id="parent"><span>child 2</span></div> before id change component update normalize vnode parent render after id change root: <div id="parent"><span>child 2</span></div>
这里要让输出达到效果,需要将 resolve 改成 async function 并且要在 nextTick() 后
输出更新后的结果,因为 instance.update 调用了 flushPreFlushCbs(null,
instane.update)
也就是说这个函数是个异步更新,且会在 nextTick()
后触发,详情
分析查看“任务调度机制分析”
问题: 如上面的结果,当我们改变
idValue.value="parent-id"
的时候,实际结果并没 有改变?答: 因为在
setupComponent()
中的initProps()
以及updateComponentPreRender()
中的updateProps()
还没实现,下一节揭晓。
normalize props options
feat(add): normalize props options · gcclll/stb-vue-next@7d6ac55
对应官方文档内容: Props | Vue.js
这里作用简单描述就是,将 props 的定义在组件加载初始化时解析成具体的值,如:
props: ['foo']
解析成foo={}
因为字符串数组的 props 会给每个属性初始化一个空 对象。
比如:
数组:
props: ['foo', 'bar', 'foo-bar']
转成
{foo: {}, bar: {}, fooBar: {}}
对象:
props: { foo: [Boolean, String], bar: Function }
表示 foo 可以是布尔值或字符串,bar 是个函数
转换过程(0:
BooleanFlags.shouldCast
, 1:BooleanFlags.shouldCastTrue
)foo = { type: [Boolean, String] }
-> 找 Booleanfoo = { type: [Boolean, String], 0: true }
->找 String 需满足
stringIndex < 0 || booleanIndex < stringIndex
foo = { type: [Boolean, String], 0: true, 1: true }
最后决定
foo
是不是应该进行 cast ? 条件是布尔类型或者有 default 默认值。
源码:
|
|
然后这个处理之后的 props,会被保存到组件的 comp.__props=[normalied,
needCastKeys]
上,而这个会在 resolvePropValue()
中进一步处理,这里的
needCastKeys
非常重要,它会决定最后的值应该如何被处理(resolvePropValue
中处
理)。
比如: { type: String, default: () => 'xxx' }
那么满足 type!==Function &&
isFunction(dfault)
则会直接执行 default() 得到属性默认值。
如果属性的 opt[BooleanFlags.shouldCast]
为 true
如最开始的说明,其实就是
prop["0"]
的值,只要 prop 的类型中有 Boolean
这个值就是 true
。
此时需要将属性的值转成
true : 类型声明中有
Boolean
且有String
的时候,它的值如果是''
或者key === value
情况下转成true
, 因为指定了可以是String
类型,所以空字符 串是允许的。false :
(!hasOwn(props, key) && !hasDefault)
, raw props 中没有这个属性且 没有default
默认值的时候转成false
, 等于是假值类型。
component props
feat(add): init component props · gcclll/stb-vue-next@9a6aa70
新增代码:
|
|
componentProps.ts > initProps()
def -> attrs.__vInterval = 1
setFullProps 处理 rawProps 将结果反馈到 props 和 attrs
有状态组件?将 props reactive 化,SSR下不支持属性响应式其实就是服务器返回的属 性都是带有最终值的而不是在客户端动态能改变的
函数组件的 props 可选属性和必须属性?可选用 attrs 否则用 props
|
|
componentProps.ts > setFullProps() 这个函数目的是将 rawProps 组件的 props 解析出来根据各自特性 分派到 props 或 attrs
key, ref 属性不保留,因为组件更新时 key 可能发生改变,ref引用也会变好指向更新后的 DOM 元素
options 啥意思?
事件属性(
onClick
)会存放到 attrs !needCastKeys ? 这是做啥呢 resolvePropValue?
|
|
componentProps.ts -> resolvePropValue()
props:{name: {default: v=> myname }, type: String}
当 type 非函数时,说明
name
是个字符串类型,但是它的default
又是个函数? 那么这种情况会在这里被处理,最后将 name 的值赋值为default(props)
执行之后的结果props:{name: {default: v=> myname }, type: Function}
这种情况,说明
name
本身就是函数,不需要执行 default。props:{name: value, type: String|Number}
普通类型情况boolean 类型的值处理,最后都会转成
true
或false
|
|
❓ 然后与 props 有关的 propsOptions 是来自哪里?
回顾下 component render 过程:
patch -> switch default -> PatchFlags.COMPONENT ->
processComponent -> mountComponent ->
createComponentInstance -> setupComponent -> setupRenderEffect
有了?
是的,就是它 -> createComponentInstance
创建组件实例中,进行了初始化,其中组织
的结构里面就有一个
propsOptions: normalizePropsOptions(type, appContext)
和
emitsOptions: normalizeEmitsOptions(type, appContext)
component setup
setup 如果返回值是函数直接是 render 函数
setup 返回值是对象,则当做和 data 一样的组件状态处理
更多分析见注释,相关代码:
|
|
测试:
|
|
undefinedroot: >>>component setup return object component stateful ? 4 call setup setup... [Function (anonymous)] render mount component normalize vnode patch component { bar: 2 } { foo: 1 } { bar: 2 } { foo: 1 } { bar: 2 } { foo: 1 } root:
component update
需要修改点:
在
processComponent
中增加updateComponent
更新组件在 instance.update effect 函数中增加
updateProps()
diff->update props
这里主要包含了 props 的更新规则,对于 children 的 diff 和 update 规则分析可以查 看 patchKeyedChildren diff 和 更新原理分析!
组件更新,代码执行流程:
状态变更 -> instance.update effect 执行 ->
如果有 next vnode 触发 updateComponentPreRender()
更新 props 和 slots
执行 beforeUpdate hook
执行 onVnodeBeforeUpdate hook
得到新树🌲 nextTree = renderComponentRoot(instance)
老树🌲 prevTree = instance.subTree
进行 patch(prevTree, nextTree) 操作
执行 updated hook 和 onVnodeUpdated hook
测试:
|
|
undefinedroot: { type: { props: { foo: [Object], bar: [Object], baz: [Object] }, render: [Function: render] }, shapeFlag: 4 } fn called 1 component stateful ? 4 call setup no setup [Function: render] render mount component update effect normalize vnode patch component { type: Symbol(Comment), shapeFlag: 0 } >>> first proxy.foo = 2 prevBar === oa: true proxy.baz === defaultBaz, true proxy.bar === prevBar, true { type: { props: { foo: [Object], bar: [Object], baz: [Object] }, render: [Function: render], __props: [ [Object], [Array] ] }, shapeFlag: 4 } update component should update component has changed props should update component.... normal update update effect component update update comp pre render normalize vnode Cannot read property 'parentNode' of null
❓ 没有触发
instance.update
?fix: props update invalid · gcclll/stb-vue-next@3771bfb
修复后,回去重新测试。
FIX 增加代码:
|
|
❓ Cannot read property 'parentNode' of null
这个报错发生在 instance.update effect 的 else 更新组件中,
patch(… hostParentNode(prevTree.el!)!, …)
的时候,去取值 prevTree.el 得到的是空值,进入 hostParentNode 调用 node.parentNode 报错的。
这里为什么 prevTree.el 是 null ? 更新的话之前的 node 不应该已经加载好了吗?
component slots
feat(add): init&update slots · gcclll/stb-vue-next@a788430
修改点:
初始化,
setupComponent()
中的initSlots()
updateComponent()
->updateComponentPreRender()
中updateSlots()
更新 slots
对应动作: init -> update
对应组件阶段: 初始化(initSlots()) -> 更新(updateSlots())
初始化(initSlots()
):
|
|
要分析整个,需要回顾下 normalizeChildren(vnode, children) 处理逻辑,要搞清楚什么
情况下会是 SLOTS_CHILDREN
。
根据 normalizeChildren()
的实现中,可知需要满足下面几个条件:
|
|
children = { _: ... }
内部插槽?normalizeObjectSlots: children 是对象类型:
{named: slotFn1, default: slotFn2 }
遍历所有 key-value =>
(推荐)如果 value 是函数需要将 slotFn 用 withCtx 封装一层,让其在当前实例的上下文中正确✅执行。
1 2 3 4 5 6 7 8 9
const normalizeSlot = ( key: string, rawSlot: Function, ctx: ComponentInternalInstance | null | undefined ): Slot => withCtx((props: any) => { // warn: 在 Render 函数外执行了 slot function return normalizeSlotValue(rawSlot(props)); }, ctx);
(不推荐)如果 value 不是函数,经过
1 2 3 4
const normalizeSlotValue = (value: unknown): VNode[] => isArray(value) ? value.map(normalizeVNode) : [normalizeVNode(value as VNodeChild)]
处理之后转成函数赋值
slots[key] = () => normalized
最终都是将 slot value 转成一个函数保存到
instance.slots{}
中非
SLOTS_CHILDREN
,那只有一种情况children 中没有
<template v-slot:named ...>
,此时它所有的 child 都会被当做 默认插槽来处理。1 2 3 4 5 6 7
const normalizeVNodeSlots = ( instance: ComponentInternalInstance, children: VNodeNormalizedChildren ) => { const normalized = normalizeSlotValue(children); instance.slots.default = () => normalized; };
如:
1 2 3 4 5 6 7 8 9 10 11 12 13
const { log, shuffle, runtime_test, renderChildren } = require(process.env .BLOG_DIR_VUE + "/lib.js"); import(process.env.BLOG_DIR_VUE + "/runtime-test.global.js").then( async ({ h, createVNode: c }) => { log.br(); const Comp = { template: "<div/>" }; const slot = () => {}; const node = h(Comp, slot); log(">>> 函数作为 children 解析为默认插槽"); log.f(node, ["children", "type"]); log(node.children); } );
undefined >>> 函数作为 children 解析为默认插槽 { type: { template: '<div/>' }, children: { default: [Function: slot], _ctx: null } } { default: [Function: slot], _ctx: null }
更新(updateSlots()
)
更新插槽步骤:
合并 instance.slots 和 children
然后删除 children 中没有的插槽
|
|
props tests
传入的 rawProps 和组件自身的 props 经过处理之后(setFullProps()) 会将 rawProps 根 据一定规则分派到组件 props 或 attrs 中去。
这里的 rawProps 代表是 parent 在渲染子组件的时候传递给它的 props ,如:
render(h(Child, { foo:1, bar:2}),root)
中的 {foo:1,bar:2}
即 parent props,然后组件可以定义自身的 props 属性:
defineComponent({ props: ['foo'] })
意味着,该子组件只接受 'foo'
作为 props
而其他的会被解析成 attrs 。
component props 测试:
|
|
undefinedroot: >>>stateful { type: { props: [ 'fooBar', 'barBaz', 'foo-baz' ], render: [Function: render] }, shapeFlag: 4 } component stateful ? 4 call setup no setup [Function: render] render mount component update effect normalize vnode comp render patch component { type: Symbol(Comment), shapeFlag: 0 } proxy.fooBar=1 { fooBar: 1, fooBaz: 3 } { bar: 2 } root:
component unmount
feat(add): add unmount component · gcclll/stb-vue-next@79c5061
主要工作:
执行
beforeUnmount
周期函数停掉所有 effects 依赖
检查 update 函数,处理在异步 update 之前执行了 unmount
在 post queue 中执行
unmounted
周期函数在 post queue 中标记
instance.isUnmounted=true
标记组件已经卸载了
三种队列任务,
pre, post, job
执行顺序: pre > job > post,详情查看
|
|
normalize emits options
问题
TypeError: Cannot read property 'allowRecurse' of null
TypeError: Cannot read property 'allowRecurse' of null at createReactiveEffect (/Users/simon/blog/cheng92.com/static/js/vue/runtime-test.global.js:251:39) at effect (/Users/simon/blog/cheng92.com/static/js/vue/runtime-test.global.js:199:22) at setupRenderEffect (/Users/simon/blog/cheng92.com/static/js/vue/runtime-test.global.js:2738:29) at mountComponent (/Users/simon/blog/cheng92.com/static/js/vue/runtime-test.global.js:2733:11) at processComponent (/Users/simon/blog/cheng92.com/static/js/vue/runtime-test.global.js:2724:19) at patch (/Users/simon/blog/cheng92.com/static/js/vue/runtime-test.global.js:2616:23) at render (/Users/simon/blog/cheng92.com/static/js/vue/runtime-test.global.js:3099:15) at /private/var/folders/1n/xw58p9v90tn42m87q527fvgr0000gn/T/babel-orafVD/js-script-Vmw0ga:29:5
因为实现问题:
|
|
文字内的测试是基于 node development 环境测试的,这里 effect options 是 null 所以 报错。
processText|Comment|Static
feat(add): processText updte · gcclll/stb-vue-next@636e870 · GitHub
本节包含(主要源码,没啥好分析的):
文本节点
注释节点
静态节点
Text
|
|
因为在 compiler-core parse 阶段的文本处理中,如果是响铃的文本节点会被合并,如:
<div>{{ text1 }} {{ text2 }}</div>
最终会合并:
<div>{{ text1 + ' ' + text2 }}</div>
最终替换的是 <div/>
整个内容。
Comment
feat(add): process comment node · gcclll/stb-vue-next@4489366 · GitHub
|
|
Static
patch -> case Static:
|
|
没有 old vnode -> mount
有 old node -> patch
mount:
|
|
mount 时用到的 hostInsertStaticContent()
是在 runtime-dom 包中实现的,先预览下
代码:
|
|
可以看到 temp.innerHTML = content
一个简单的内容全替换操作。
patchStaticNode: 因为静态节点在生产环境中会被提升,重用,因此不存在 patch 阶段。
|
|
moveStaticNode: 在 diff -> update 阶段 move() 中触发
|
|
removeStaticNode: remove()
中触发
|
|
processFragment
Fragment 的情况: children 有多个 child 的时候,会用一个 fragment 事先包起来。
STABLE_FRAGMENT
情况:
v-if
首先要满足 children.length !== 1 即有一个以上的 children, 如:
<div><p/><p/></div>
或者非第一个 child ELEMENT 类型,如:
<div><Comp/></div>
其要满足
(children.length === 1 && firstChild.type === NodeTypes.FOR)
如:<div v-for="item in list"><p/></div>
才会被当做
PatchFlags.STABLE_FRAGMENT
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
// vIf.ts const needFragmentWrapper = children.length !== 1 || firstChild.type !== NodeTypes.ELEMENT; if (needFragmentWrapper) { if (children.length === 1 && firstChild.type === NodeTypes.FOR) { // ... } else { return createVNodeCall( // ... PatchFlags.STABLE_FRAGMENT + (__DEV__ ? ` /* ${PatchFlagNames[PatchFlags.STABLE_FRAGMENT]} */` : ``), // ... ); } }
v-for
1 2 3 4 5 6 7 8 9
// vFor.ts const isStableFragment = forNode.source.type === NodeTypes.SIMPLE_EXPRESSION && forNode.source.constType > 0 const fragmentFlag = isStableFragment ? PatchFlags.STABLE_FRAGMENT : keyProp ? PatchFlags.KEYED_FRAGMENT : PatchFlags.UNKEYED_FRAGMENT
源码:
|
|
TELEPORT
feat(init): Teleport · gcclll/stb-vue-next@0fcfa32 · GitHub
新增代码:
TeleportImpl: 组件模板
|
|
resolveTarget: 根据选择器找到目标元素
|
|
moveTeleport: 执行移动
|
|
hydrateTeleport:
|
|
导出组件 ~Teleport~:
|
|
process()
|
|
对于 teleport 的 mount 和 update 两个共同点(也是重点):
当 new teleport 是 disabled 时,不直接渲染到目标元素中,而是挂在当前 container 中待用
当 new teleport 状态 enabled 时,不论 old 什么状态,都会讲新的 teleport children 渲染到目标元素下面。
Teleport 的移动类型有:
TARGET_CHANGE
目标发生了变化, teleport 的to
属性变化1 2 3 4
// move target anchor if this is a target change. if (moveType === TeleportMoveTypes.TARGET_CHANGE) { insert(vnode.targetAnchor!, container, parentAnchor); }
TOGGLE
状态发生了变化 enable -> disable 或 disable -> enableREORDER
目标元素内进行重新排序 ?1 2 3 4
// move main view anchor if this is a re-order. if (isReorder) { insert(anchor!, container, parentAnchor); }
TODO 测试
|
|
undefinedcomponent stateful ? 0 mount component update effect patch component >>> root >>> target
❓ 没结果!!!!!!
SUSPENSE
feat(add): suspense · gcclll/stb-vue-next@fd651ab
Suspense 组件和 Teleport 一样的组织结构和使用方式
结构:
|
|
然后在 process 中处理 mount 或 patch 流程,这里面和普通标签或普通组件的处理是一 样的, mount or patch。
下面来看下这个组件是如何实现的,功能又是如何?
新增函数:
|
|
列表:
名称 | 描述 |
---|---|
SuspenseImpl | - |
mountSuspense() | - |
patchSuspense() | - |
SuspenseBoundary | - |
createSuspenseBoundary() | - |
hydrateSuspense() | - |
脑图:
重点逻辑:
Suspense 的渲染转折点发生在
mountComponent
中,将setupRenderEffect
做了 一次封装,让其在setup()
返回的 Promise 状态完成之后去执行在整个 Suspense mount 或 patch 过程中,使用了
suspense.deps
来记录异步事件, 只有当这个值为0
的时候说明可以进行解析并挂在到真实DOM上了(比如. 服务器端数 据请求完成)
SuspenseBoundary 数据结构
只列出部分与 Suspense 关联性强的字段:
名称 | 描述 |
---|---|
vnode | VNode 结构 |
hiddenContainer | - |
activeBranch | 请求完成之后显示的组件分支 #default |
pendingBranch | 请求中显示的分支 #fallback |
deps | 组件依赖 |
timeout | 超时时间 |
isInFallback | - |
isHydrating | - |
effects | [] 依赖列表 |
resolve(force) | - |
fallback() | 参数: fallbackVnode |
move() | - |
next() | - |
registerDep() | 注册实例依赖 |
|
|
mountSuspense()
feat(add): suspense mount · gcclll/stb-vue-next@802b9ad
|
|
创建一个 DOM 之后的 div,即还没渲染到 DOM 结构中的
构建 Suspense 组件结构,这个结构非 VNode ,而是挂在 vnode.suspense 上的一个
SuspenseBoundary
结构开始 mount 内容里的子树
检测 Suspense 有没异步依赖,如果有,则需要先解析这些异步依赖,完成之后再激活 branch
没有异步依赖直接拿到结果解析出组件
也就是说这里面需要重点关注的其实是“有没异步依赖的问题”。
没有依赖的时候用到了 suspense.resolve()
这个应该是将创建的 off-dom div 挂到真
实 DOM 上去。
suspense.resolve()
|
|
分析如上面的注释, resolve()
主要目的就是将 off-dom div 上的 suspense 组件在异
步事件完成后根据结果解析出对应的分支,将这个分支挂载到真实的 DOM 上去,同时激活
它(显示出来)。
其他处理:
transition
的延迟进入处理,通过将 move() 操作注册到afterLeave()
回调实现effects 任务处理,这里的任务处理机制是:
只有在 parent 没有任何挂起的任务时候才会立即得到执行,否则只会进行合并操作。
因为代码最后需要执行 move()
操作将 #default
替换 #fallback
,所以下面先实
现 suspense.move()
再来测试。
suspense.move()
实现这个 move 有几个地方需要修改
SuspenseBoundary 中的 move()
1 2 3 4 5 6 7
var foo = { move(container, anchor, type) { suspense.activeBranch && move(suspense.activeBranch, container, anchor, type); suspense.container = container; }, };
renderer.ts 中的 move() 函数,实现 SUSPENSE 组件的处理
1 2 3 4 5 6 7 8 9 10
const move = () => { // ... // SUSPENSE if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) { console.log("move suspense"); vnode.suspense!.move(container, anchor, moveType); return; } // ... };
另外 mountComponent 中漏了对 SUSPENSE 的处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
const mountComponent = () => { // ... create instance // ... setupComponent // setup() 是个异步函数,返回了 promise ,在 setupComponent // 中会将 setup 执行结果赋值给 instance.asyncDep,即 SUSPENSE 处理 if (__FEATURE_SUSPENSE__ && instance.asyncDep) { // 将 setupRenderEffect 注册到 parent deps 这里的 deps // 执行由一定的规则, 如果 parent suspense 没有结束,child deps // 不会立即执行,而是将它们合并到 parent suspense deps 中等待 parent 状态完成了才会执行,对于 // parent deps 也遵循这个规则,直到没有未完成的 parent suspense为止 parentSuspense && parentSuspense.registerDep(instance, setupRenderEffect); // 这里等于是说先用一个注释节点占位,等异步完成之后替换 if (!initialVNode.el) { const placeholder = (instance.subTree = createVNode(Comment)); processCommentNode(null, placeholder, container!, anchor); } return; } // ... setupRenderEffect SUSPENSE 不会进入到这里 };
测试:
|
|
undefined component stateful ? 4 call setup [Function (anonymous)] render mount component update effect normalize vnode patch component component stateful ? 4 call setup mount component process element mount elment { shapeFlag: 9 } before <div>fallback</div> [Function (anonymous)] render update effect normalize vnode patch component component stateful ? 4 call setup no setup [Function: render] render mount component update effect normalize vnode patch component process element mount elment { shapeFlag: 9 } moving... move component moving... move component moving... move host insert after <div>async</div>
❓. 结果发现并没变化???
before <!----> after <!---->既没有渲染 fallback 也没有渲染 default 的,为何?
上面的第二点有说到在 renderer.ts 的 mountComponent()
中增加了对 SUSPENSE
的处理,这里面有个注册依赖的动作,这里注册的是 setupRenderEffect
函数,这个函
数正是用来 mount & update 组件的,而在 components/Suspense.ts 中并没有实现,所
以问题就出在这里了!!!
suspense.registerDep()
feat(add): suspense registerDeps · gcclll/stb-vue-next@e0fa81e
|
|
这个函数主要实现点:
接受
setup()
执行的结果(Promise
) asyncSetupResult 并捕获异常,对结果进行 分析处理检测组件是不是已经卸载了,或者 suspense 被移除,就不需要继续处理了,退出即可
1 2 3 4 5 6 7
if ( instance.isUnmounted || suspense.isUnmounted || suspense.pendingId !== instance.suspenseId ) { return; }
使用
handleSetupResult(instance, asyncSetupResult, false)
处理 setup 执行结 果,到底是 render 还是状态,需要解析然后执行 setupRenderEffect 执行组件的 mount 或 update 操作
移除占位的注释节点
suspense.deps 执行完成之后就可以开始解析 suspense 组件 进行 move 操作了。
patchSuspense()
suspense.unmount&fallback&其他
feat(add): suspense unmount & fallback… · gcclll/stb-vue-next@080898d
新增:
patchSuspense()
和其他普通类型的处理差不多,无非就是检测 old 和 new branch 的 类型,进行 patch(),期间触发onPending
事件suspense.fallback()
处理,当异步事件未完成时显示的#fallback
分支处理,期间 触发onFallback
事件suspense.unmount()
检测 activeBranch 和 pendingBranch 先卸载 active 随后卸载 pending 分支(前提是存在的情况下)
Suspense 组件测试
|
|
小结
SUSPENSE 组件的大致执行流程
patch 进入 switch default 检测到 shapeFlag 是 SUSPENSE
调用 type.process(n1,n2,…) 处理 Suspense 组件,根据 n1 决定是 mountSuspense 还是 patchSuspense 这里和其他类型组件处理逻辑一致
首次(mount), 进行 mountSuspense 创建 Suspense 组件,对 pendingBranch 进行 patch 操作 (挂在到一个非DOM树中的 'div' 元素(off-dom),待用),即异步操作还未完成时显示 的分支,如:
#fallback
非首次(update),进行 patchSuspense 对比新旧的 branch 进行 patch
要点:在 mountComponent()
中不是直接调用 setupRenderEffect()
而是调
用 suspense.registerDep()
去处理 setup
执行的结果(instance.asyncDep
),它是
个Promise 在其后的 then() 中接受 setup 执行结果,然后开始调用 setupRenderEffect
mount 或 update 子树节点,待 suspense 上的所有依赖都完成之后开始 resolve()
Suspense 组件将其挂在到真实的 DOM 中。
原理: setup() 返回 Promise,render 过程中注册渲染函数,待 promise 状态完成调用 then 接受异步结果来渲染 Suspense 组件(任务为
post
类型)。
KEEP_ALIVE
feat(add): keep-alive render · gcclll/stb-vue-next@a192cb4
KeepAlive 组件的 render 入口在 processComponent()
中,当 n1 == null
情况下,
会去检测该组件是不是 keep-alive
类型,如果是直接调用 activate()
激活。
|
|
unmount 操作时,如果是 keep-alive 直接调用 deactivate()
失效,而不是真正的从
DOM 移除。
feat(add): keep-alive render unmount · gcclll/stb-vue-next@024b24b
|
|
在 mountComponent()
中:
|
|
这里将 keep-alive 组件的 setup() 函数中用到的一些 renderer 函数保存引用到
ctx.renderer 上供后面 setup()
中使用。
|
|
keep-alive 作为内部组件,内置了 setup()
函数的实现,所以在
patch -> processComponent -> mountComponent -> setupComponent
时调用的就是这个内置的 setup() 函数。
setup() 函数体大致代码:
|
|
上面代码提供了一下信息:
activate
&deactivate()
函数是挂在 VNode 的 ctx 上的,并且是在 setup() 调 用期间产生缓存机制
只注册了
mounted, unmounted, update
声明周期最后返回的函数可以得到最原始的 VNode 节点
注意看 processComponent()
中的判断:
if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) { activate() }
而 COMPONENT_KEPT_ALIVE
标记的赋值又是发生在 setup()
函数中,也就是说对于
keep-alive
组件首次加载不会进入到 activate() 而是直接按照普通组件处理调用
mountComponent()
去调用 setup()
初始化该 keep-alive
组件的一些函数等(其中
就包含 activate
和 deactivate()
函数)
当状态发生变化时根据特定条件最后执行激活才会去调用 activate()
而不是进入 mountComponent()
activate()
feat(add): keep-alive ctx.activate · gcclll/stb-vue-next@267fdbd
|
|
激活 keep-alive
组件的函数,只有当非首次的时候,状态发生变更时会被调用,注意上
面的任务类型 post
,周期函数的调用是异步发生的,会在下一个 tick 中赋值。
deactivate()
feat(add): keep-alive ctx.deactivate · gcclll/stb-vue-next@b340d57
|
|
如果看这里失活状态下组件是如何进行更新的?
storageContainer
是在 setup 中创建的一个空的 off-dom div 元素,这里等于是当组
件失活时会将 keep-alive 先挂载到这个 off-dom div 上去.
unmount()
feat(add): keep-alive unmount · gcclll/stb-vue-next@4ce0b9b
|
|
include & exclude props
feat(add): keep-alive include & exclude props · gcclll/stb-vue-next@fdcc306
主要增加两个函数实现,一个监听动作(watch([include, exclude],...)
):
|
|
include 指定需要缓存的组件名称
exclude 指定不需要进行缓存的组件名称
类型: String, RegExp, Array
cache subtree
feat(add): keep-alive cache subtree · gcclll/stb-vue-next@efb7577
对 <keep-alive/>
的孩子节点🌲进行缓存。
|
|
在 <keep-alive/>
卸载之前将已经缓存 deactivated
钩子函数推入队列等待执行,没
有缓存的直接调用 unmount()
卸载掉。
setup() -> render 函数
feat(add): keep-alive return render function · gcclll/stb-vue-next@9b75803
在 setupComponent()
中,最后调用 setup() 得到 setupResult ,最后会将这个
setupResult 传递给 handleSetupResult() 去处理,返回结果,这里面当检测到
setupResult 是个函数的时候,那么这个函数会被当做是 instance.render
函数处理。
即,这里的 setupResult 是 <keep-alive/>
组件 children 的 render 函数。
|
|
最后返回的是 children[0]
节点。
里面有个置位标识值得注意: ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
这个作用是啥?
unmount()
中调用 deactivate() 的条件1 2 3 4 5
// keep-alive if (shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) { (parentComponent!.ctx as KeepAliveContext).deactivate(vnode); return; }
instance.update
的 effect 中触发activated
周期函数的条件1 2 3 4 5 6 7
// activated hook for keep-alive roots. // #1742 activated hook must be accessed after first render // since the hook may be injected by a child keep-alive const { a } = instance; if (a && initialVNode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) { queuePostRenderEffect(a, parentSuspense); }
测试
|
|
undefined Cannot read property '_vnode' of undefined
⚠ 有错误,待完成。。。vue-next 测试见最后一章节《测试》
set ref
官方使用文档: Special Attributes | Vue.js
官方示例:
|
|
patch() 函数中对 ref 属性的处理(set ref):
feat(add): set ref · gcclll/stb-vue-next@a0a1344
源码实现主要有几个步骤:
ref 支持数据?
是不是异步组件 value = null
有状态组件(
STATEFULL_COMPONENT
, 分函数组件)value = vnode.component!.exposed || vnode.component!.proxy
其他情况下 value = vnode.el
即 2,3,4 都是为了设置 value 指向哪个引用,比如 vnode.el 在渲染之后会被赋值为当 前 vnode 对应的那个 DOM 元素。
断开 oldRef 引用
设置 ref,分三种情况
ref 是字符串直接
refs[ref] = value
取 key 设值ref 是 Ref 类型,
ref.value = value
ref 是函数类型,
ref(value, refs)
调用
在设值的时候会根据 value 是否为空值来控制是否进行异步设置,等 Render 执行完 成之后再设置
|
|
源码:
|
|
这里可以简单理解为:将 ref
设值为 vnode.el
这是个引用,因此当它有值的时候也
等于是 ref 有值了,然后分为异步和同步,异步需要等 render 完成再去设置。
direcitve hooks
feat(add): directive hooks · gcclll/stb-vue-next@1343be2
执行指令声明周期钩子函数:
|
|
给组件注册指令集:
|
|
created, befoureMounted, mounted 发生在 mountElement()
|
|
beforeUpdate, updated 发生在 mountChildren()
|
|
unmounted 发生在 unmount()
|
|
测试:
|
|
undefinedcomponent stateful ? 4 call setup [Function: render] render normalize vnode >>> before mounted el.tag = div el.parentNode = null root.children.length = 0 { dir: { beforeMount: [Function: beforeMount], mounted: [Function: mounted], beforeUpdate: [Function: beforeUpdate], updated: [Function: updated], beforeUnmount: [Function: beforeUnmount], unmounted: [Function: unmounted] }, instance: {}, value: 0, oldValue: undefined, arg: 'foo', modifiers: { ok: true } } vnode = _vnode, true prev vnode = null >>> mounted el.tag = div el.parentNode = roottrue root.children[0] = elfalse { dir: { beforeMount: [Function: beforeMount], mounted: [Function: mounted], beforeUpdate: [Function: beforeUpdate], updated: [Function: updated], beforeUnmount: [Function: beforeUnmount], unmounted: [Function: unmounted] }, instance: {}, value: 0, oldValue: undefined, arg: 'foo', modifiers: { ok: true } } vnode = _vnode, true prev vnode = null
TODO component props
feat(add): patch props · gcclll/stb-vue-next@6f6a0be
|
|
undefinedcomponent stateful ? 4 call setup no setup [Function: render] render normalize vnode >>> test proxy.fooBar = 1 > props { fooBar: 1 } > attrs { bar: 2 } should update component has changed props normalize vnode >>> foo-bar 会转成 fooBar proxy.fooBar = 3 > props { fooBar: 3, barBaz: 5 } > attrs { bar: 3, baz: 4 } should update component has changed props normalize vnode >>> 删除 camel case proxy.fooBar = undefined > props { fooBar: undefined, barBaz: undefined } > attrs { qux: 5 }
测试
Teleport Testing...
Suspense Testing...
KeepAlive Testing...
Directive Testing...
脑图 & 测试结果 GIF
keep-alive 测试变化 GIF(13M):
结合源码
1 2 3
// deactivate() const storageContainer = createElement("div"); move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense);
即, deactivated 的 DOM 节点其实并非直接删除了,而是移到到了一个
off-dom
的元素上了,待重新激活的时候再移回来(在源码的 deactivate 和 activate 函数中增 加storageContainer
打印).测试脑图: