Vue3 源码头脑风暴之 3 ☞compiler-core - transform + codegen
文章目录
stb-vue-next 完全拷贝于 vue-next ,主要目的用于学习。
声明 :该篇为 ts 源码(commit)版本,之前做过一遍完整的 js 版本,更详细,也可参考
Vue3.0 源码系列(二)编译器核心 - Compiler core 3: compile.ts - 若叶知秋
由于 transform 阶段直接测试不太好直观的看出结果,因此这里会结合 codegen 来一起实 现,即该文包含 compiler-core 三大阶段的最后两个阶段(transform + generate)
调试 :所有测试用例可通过
<F12>
控制台查看更新日志&Todos :
DONE [2020-12-12 18:33:33] 阶段性完成,浏览器支持的所有基本功能(浏览器环境所 有用例测试均已通过,可通过控制台查看测试用例及其运行过程结果)
TODO ssr 服务端渲染
TODO <setup> 标签处理
TODO 非浏览器环境支持(
prefixIdentifiers, cacheHandlers
选项需要非浏览器环境)TODO ref 类型处理
关键知识点
脑图
e03a03c init transform module
feat(init): transform section · gcclll/stb-vue-next@e03a03c
初始化函数。
|
|
fc6f1f1 add transform function
feat: transform function · gcclll/stb-vue-next@fc6f1f1
create transform context
traverse nodes, 递归遍历所有节点,构造器 codegenNode
hoist static, 静态节点提升,复用
ssr render, 不需要创建根节点 codegenNode
复制 context 属性到 -> root
transform 作用就是通过 traverseNode()
递归遍历所有节点,解析,构造对应的节点
codegenNode 。
b0d72da add compile.ts>compile()
feat(add): compile function · gcclll/stb-vue-next@b0d72da
对外的 compile 函数,执行分为三个阶段:
ast(
baseParse()
) -> 解析出 ast 结构transform(
transform()
) -> 解析 ast 得到 codegenNodecodegen(
generate()
) -> 将 codegenNode 解析成 Render 函数
这是后面测试的基础,所以得提前实现了。
|
|
35248ce add exports maybe needs
feat(add): compiler-core exports · gcclll/stb-vue-next@35248ce
增加 compiler-core 模块的导出(export
)内容
05a223b add transform pure text
feat(add): transform pure text · gcclll/stb-vue-next@05a223b
|
|
transform 阶段代码毕竟的三个阶段
收集 transformXxx 函数到 exitFns
根据 ast节点类型递归遍历子孙节点
按照收集时相反的顺序执行 exitFns,解析出 codegenNode
为了方便测试,在 generate()
中直接返回 ast :
test: generate return ast for test · gcclll/stb-vue-next@999d8d6
|
|
+RESULTS:
{ type: 2, content: 'pure text', loc: { start: { column: 1, line: 1, offset: 0 }, end: { column: 10, line: 1, offset: 9 }, source: 'pure text' } }
结果显示并没有 codegenNode 因为在transformText 中满足条件
children.length === 1 && node.type === NodeTypes.ROOT
而直接退出了。
至于 root.codegenNode = undefined
需要实现 createRootCodegen()
61ce406 add createRootCodegen() to create root.codegenNode
feat: createRootCodegen() for pure text · gcclll/stb-vue-next@61ce406
只增加了针对非 ELEMENT 类型或者孩子节点没有 codegenNode 的情况实现(当前 commit 最简化)。
当 root.children 只有一个孩子节点且该节点没有自己的 codegenNode 时候:
|
|
测试
|
|
{ type: 0, children: [ { type: 2, content: 'pure text', loc: [Object] } ], helpers: [], components: [], directives: [], hoists: [], imports: [], cached: 0, temps: 0, codegenNode: { type: 2, content: 'pure text', loc: { start: [Object], end: [Object], source: 'pure text' } }, loc: { start: { column: 1, line: 1, offset: 0 }, end: { column: 10, line: 1, offset: 9 }, source: 'pure text' } }
注意 codegenNode 其实就是 root.children[0]
节点本身。
b9f3cb7 add transform text
feat: transformText function · gcclll/stb-vue-next@b9f3cb7
必须是文本节点或者类型是组合表达式类型(
COMPOUND_EXPRESSION
)patch flag 处理
构造 TEXT_CALL 类型节点
codegenNode -> createCallExpression
f6d5271 add generate text codegen
codegen 阶段目的是将 codegenNode 解析成 Render 函数的一部分。
f6d5271 add
createCodegenContext()
feat(add): codegen context creator · gcclll/stb-vue-next@f6d5271
上下文对象创建函数,重点方法有两个(
push(code, node)
和helper(key)
)。FIX1: lint errors, fix: f6d5271 lint errors · gcclll/stb-vue-next@0ac8c2f
2ef2699 增加 text codegen generator 实现
feat: generate text codegen · gcclll/stb-vue-next@2ef2699
该部分涉及到一个较为完整的 codegen generator 流程,所以增加内容较多,因此这里 不直接贴代码了,请点击上面 commit 链接查看实际增加的源码。
处理流程:
preamble 处理,如果是 Node 环境需要通过
import { ...} from 'vue'
语法,如 果是浏览器环境使用const { ... } = Vue
解构语法。是否使用
with() {}
作用域语法,默认是使用的return ...
返回实际 render 函数返回结果,这里将返回最后被渲染的 DOM 结构。genNode()
递归处理 ast 生成 render 函数的对应部分代码
6b901f9 增加 node 环境或 module 环境处理(
genModulePreamble
)feat: module preamble · gcclll/stb-vue-next@6b901f9 modue preamble :
export { ... } from 'vue'
function preamble:const { ... } = Vue
重点增加的 genXxx 函数 genText(node, context)
专门用来处理文本节点的。
|
|
测试
测试将分为两个部分,
function preamble 形式(作为全局 Vue
对象引入)
|
|
return function render(_ctx, _cache) { with (_ctx) { return "pure text" } } undefined
module preamble 形式(es6 模块化导出导入)
|
|
return function render(_ctx, _cache) { return "pure text" } undefined
这里好像看不出啥区别,后面再说吧。
2f749b2 add interpolation generator
feat(add): transform -> generate interpolation · gcclll/stb-vue-next@2f749b2
|
|
这里实现分几个部分:
transform: traverseNode() 增加对插值的处理,后面增加了 traverseChildren 处理,因为所有的
ast 都是挂在 root.children
中的,所以最开始解析的是 ROOT
节点,因此这里必须
要增加 ROOT
类型的解析,调用 traverseChildren(node, ctx)
去递归解析 root.children
transform() -> traverseNode(): ROOT 解析 -> traverseChildren() -> traverseNode(): INTERPOLATION
新增核心函数:遍历所有 children[]
调用 traverseNode()
|
|
codegen: genNode()
中新增 INTERPOLATION
和 SIMPLE_EXPRESSION
类型的处理,
因为 INTERPOLATION 的 ast.content(如上面代码执行结果) 类型是 SIMPLE_EXPRESSION。
|
|
feat(add): comment generator · gcclll/stb-vue-next@2d0e2a6
拓展:add comment generator
|
|
const _Vue = Vue return function render(_ctx, _cache) { with (_ctx) { const { createCommentVNode : _createCommentVNode } = _Vue return _createCommentVNode(" i'm a comment ") } } undefined
add element transfrom and generator
准备工作 compiler-core/src/utils.ts
feat: utils for compiler-core · gcclll/stb-vue-next@9436d8f
相关正则: const memberExpRE = /^[A-Za-z_$][\w$]*(?:\s*\.\s*[A-Za-z_$][\w$]*|\[[^\]]+\])*$/
feat(add): resolveComponentType · gcclll/stb-vue-next@2265e46
解析出组件的类型,大体分为四类:
动态组件:
<component is="xx">
或<component v-is="xx">
内置组件:
Teleport, Transition, KeepAlive, Suspense
用户组件:
$setup[]
上的组件用户组件:
context.components[]
上的组件
87339d2 add element transform
feat(add): transformElement function · gcclll/stb-vue-next@87339d2
普通标签的 transform codegenNode阶段。
add
createVNodeCall()
函数,创建基本的 ELEMENT 类型节点 codegenNode根据
isBlock
参数决定使用 BLOCK 函数还是 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 26 27 28 29 30 31 32 33 34
export function createVNodeCall( context: TransformContext | null, tag: VNodeCall['tag'], props?: VNodeCall['props'], children?: VNodeCall['children'], patchFlag?: VNodeCall['patchFlag'], dynamicProps?: VNodeCall['dynamicProps'], directives?: VNodeCall['directives'], isBlock: VNodeCall['isBlock'] = false, disableTracking: VNodeCall['disableTracking'] = false, loc = locStub ): VNodeCall { if (context) { if (isBlock) { context.helper(OPEN_BLOCK) context.helper(CREATE_BLOCK) } else { context.helper(CREATE_VNODE) } } return { type: NodeTypes.VNODE_CALL, tag, props, children, patchFlag, dynamicProps, directives, isBlock, disableTracking, loc } }
add
createObjectExpression()
函数1 2 3 4 5 6 7 8 9 10
export function createObjectExpression( properties: ObjectExpression['properties'], loc: SourceLocation = locStub ): ObjectExpression { return { type: NodeTypes.JS_OBJECT_EXPRESSION, loc, properties } }
add
getStaticType()
判断节点是否需要做静态提升处理add
transformElement: postTransformElement()
函数add
stringifyDynamicPropNames()
将属性转成数组结构
测试:
|
|
root codegenNode: undefined const _Vue = Vue return function render(_ctx, _cache) { with (_ctx) { const { createVNode : _createVNode } = _Vue return null } } undefined
正确结果:
ƒ render(_ctx, _cache) { with (_ctx) { const { createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue return (_openBlock(), _createBlock("div")) } }
问题:
根节点 codegenNode 为空
render 函数内没有
openBlock, createBlock
导出return 后面没内容(这是 generator 范畴,此节不展开)
问题1,2都是在同一个地方处理的,因为当 ROOT 节点只有一个孩子节点的时候,不会用 CREATE_VNODE 创建,而是改用 CREATE_BLOCK,所以这两个问题一起处理
FIX 1,2: fix: no export open/create block function from Vue · gcclll/stb-vue-next@97cf290
修改: createRootCodegen(root: RootNode, context: TransformContext)
2f58786 add element generator
feat: element generator · gcclll/stb-vue-next@2f58786
路径:
VNODE_CALL
->genVNodeCall()
->genNodeList([], ctx)
->string:
push(node)
array:
genNodeListAsArray(node, ctx)
other:
genNode(node, ctx)
测试:
|
|
root codegenNode: { type: 13, tag: '"div"', props: undefined, children: undefined, patchFlag: undefined, dynamicProps: undefined, directives: undefined, isBlock: true, disableTracking: false, loc: { start: { column: 1, line: 1, offset: 0 }, end: { column: 12, line: 1, offset: 11 }, source: '<div></div>' } } const _Vue = Vue return function render(_ctx, _cache) { with (_ctx) { const { createVNode : _createVNode, openBlock : _openBlock, createBlock : _createBlock } = _Vue return (_openBlock(), _createBlock("div")) } }
05ca2f8 root.children 有多个孩子
feat: root.children has multi child · gcclll/stb-vue-next@05ca2f8 · GitHub
当有多个孩子节点的时候,会创建一个 fragment
将他们包起来。
FIX: 死循环, genNode(node.codegenNode, ctx)
测试:
|
|
const _Vue = Vue return function render(_ctx, _cache) { with (_ctx) { const { createVNode : _createVNode, Fragment : _Fragment, openBlock : _openBlock, createBlock : _createBlock } = _Vue return (_openBlock(), _createBlock(_Fragment,null,[ _createVNode("div"), _createVNode("div") ],64 /* STABLE_FRAGMENT */)) } } undefined
FIX: 参数之间少了空格(feat: root.children has multi child · gcclll/stb-vue-next@05ca2f8)
正解:
const _Vue = Vue const { createVNode: _createVNode } = _Vue const _hoisted_1 = /*#__PURE__*/_createVNode("div", null, null, -1 /* HOISTED */) const _hoisted_2 = /*#__PURE__*/_createVNode("div", null, null, -1 /* HOISTED */) return function render(_ctx, _cache) { with (_ctx) { const { createVNode: _createVNode, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock } = _Vue return (_openBlock(), _createBlock(_Fragment, null, [ _hoisted_1, _hoisted_2 ], 64 /* STABLE_FRAGMENT */)) } }
正确答案中做了静态提升处理,代码在 transform()
函数中 hoistStatic(root,
context)
的调用,会从 ROOT 节点开始遍历,将需要提升的节点进行提升处理。
7cb3dbf add hoist static 静态提升
满足提升的三种情况:
tag 和 tagType 都是 ELEMENT 且整棵树都是静态
包含动态孩子节点,但是有静态属性的,将属性提升
纯文本节点
feat(add): hoist static · gcclll/stb-vue-next@7d7dbd4
transform() 中增加静态提升处理:
|
|
feat: hoist static · gcclll/stb-vue-next@7cb3dbf
修改
genFunctionPreamble(ast: RootNode, context: CodegenContext)
解构出需要用到的函数(_createVNode
)增加
genHoists()
函数,生成ast.hoists
中需要提升的节点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
function genHoists(hoists: (JSChildNode | null)[], context: CodegenContext) { if (!hoists.length) { return } context.pure = true const { push, newline, helper, scopeId, mode } = context const genScopeId = !__BROWSER__ && scopeId != null && mode !== 'function' newline() // push scope Id before initializing hoisted vnodes so that these vnodes // get the proper scopeId as well. if (genScopeId) { push(`${helper(PUSH_SCOPE_ID)}("${scopeId}")`) newline() } hoists.forEach((exp, i) => { if (exp) { push(`const _hoisted_${i + 1} = `) genNode(exp, context) newline() } }) if (genScopeId) { push(`${helper(POP_SCOPE_ID)}()`) newline() } context.pure = false }
测试:
|
|
const _Vue = Vue const { createVNode: _createVNode } = _Vue const _hoisted_1 = /*#__PURE__*/_createVNode("div", null, null, -1 /* HOISTED */) const _hoisted_2 = /*#__PURE__*/_createVNode("div", null, null, -1 /* HOISTED */) return function render(_ctx, _cache) { with (_ctx) { const { createVNode : _createVNode, Fragment : _Fragment, openBlock : _openBlock, createBlock : _createBlock } = _Vue return (_openBlock(), _createBlock(_Fragment, null, [ _hoisted_1, _hoisted_2 ], 64 /* STABLE_FRAGMENT */)) } } undefined
PS: 静态属性提升 feat: props hoist static · gcclll/stb-vue-next@1e58eeb
prop transform and generator
在这之前我们完成了以下几个基本部分:
接下来需要完成属性的解析才能进行下一步,因为 v-if, v-for, v-slot, ...
都需要属
性解析。
属性转换这里异常复杂,需要慢慢展开来讲,并且涉及到各种指令,因此对于完整的测试需 要等所有指令 transform 完成之后再进行。
1792f93 props transform
buildProps
feat(add): transform props · gcclll/stb-vue-next@1792f93
将
codegenNode.props
构建成 如下结构:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
{ "type":15, "properties":[ { "type":16, "key":{ "type":4, "isConstant":false, "content":"class", "isStatic":true }, "value":{ "type":4, "isConstant":false, "content":"second", "isStatic":true } }, { "type":16, "key":{ "type":4, "isConstant":false, "content":"onClick", "isStatic":true }, "value":{ "type":4, "content":"clickHandle", "isStatic":false, "isConstant":false, } } ] }
v-bind,v-on 指令,没有参数,需要将 props 合并
e4acc0d props generator
feat: props generator · gcclll/stb-vue-next@e4acc0d
修改点:
add
genExpressionAsPropertyKey()
生成属性 key 函数三种可能的属性名
静态属性名:
<div class="value">
->{ class: "value" }
动态属性名:
<div :[propName]="value"
->{ [propName]: "value"}
组合表达式属性名:TODO
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
// 生成对象的属性 key (可能是静态,动态) function genExpressionAsPropertyKey( node: ExpressionNode, context: CodegenContext ) { const { push } = context if (node.type === NodeTypes.COMPOUND_EXPRESSION) { // TODO 动态属性名或表达式 } else if (node.isStatic) { // only quote key if necessary const text = isSimpleIdentifier(node.content) ? node.content : JSON.stringify(node.content) push(text, node) } else { push(`[${node.content}]`, node) } }
add
genObjectExpression()
将属性列表生成对象遍历节点的
node.properties
先生成 key(genExpressionAsPropertyKey(key)
) 再生成 value(genNode(value)
) 。
测试:
|
|
const _Vue = Vue const { createVNode: _createVNode } = _Vue const _hoisted_1 = { class: "first", name: "div" } return function render(_ctx, _cache) { with (_ctx) { const { createVNode : _createVNode, openBlock : _openBlock, createBlock : _createBlock } = _Vue return (_openBlock(), _createBlock("div", _hoisted_1)) } } undefined
实例中最后是用的
createBlock()
是因为 root.children 只有一个 child 。
static props
修改函数: transforms/transformElement
|
|
{ type: 6, name: 'class', value: { type: 2, content: 'first', loc: { start: [Object], end: [Object], source: '"first"' } }, loc: { start: { column: 6, line: 1, offset: 5 }, end: { column: 19, line: 1, offset: 18 }, source: 'class="first"' } } undefined
6951dd1 merge props
feat: merge props · gcclll/stb-vue-next@6951dd1
合并属性的条件:存在没有参数的指令,如: <div v-bind="{...}" v-on="{...}"
FIX: fix: merge toHandlers props · gcclll/stb-vue-next@12a66f0
|
|
>>> 无参数的指令,合并所有属性 const _Vue = Vue return function render(_ctx, _cache) { with (_ctx) { const { toHandlers : _toHandlers, mergeProps : _mergeProps, createVNode : _createVNode, openBlock : _openBlock, createBlock : _createBlock } = _Vue return (_openBlock(), _createBlock("div", _mergeProps({ class: "first" }, _toHandlers({ click: clickHandle }), { style: 'color:red' }), null, 16 /* FULL_PROPS */)) } } >>> 有参数的指令,不合并 const _Vue = Vue return function render(_ctx, _cache) { with (_ctx) { const { resolveDirective : _resolveDirective, createVNode : _createVNode, openBlock : _openBlock, createBlock : _createBlock } = _Vue return _withDirectives((_openBlock(), _createBlock("div", { class: "second" }, null, 512 /* NEED_PATCH */)), ) } } undefined
有参数指令时,需要结合 v-on
指令解析,因此需要先实现了 transform 指令才能得到下面的正确结果。
不合并(mergeProps()
) 的正解:
|
|
下面将继续完成指令相关的 transform
6c43451 add v-on transform
init: feat(init): v-on directive · gcclll/stb-vue-next@98dcc96
实现:feat: v-on directive transform · gcclll/stb-vue-next@6c43451
|
|
const _Vue = Vue return function render(_ctx, _cache) { with (_ctx) { const { resolveDirective : _resolveDirective, createVNode : _createVNode, openBlock : _openBlock, createBlock : _createBlock } = _Vue return _withDirectives((_openBlock(), _createBlock("div", { class: "second", onClick: clickHandle }, null, 8 /* PROPS */, ["onClick"])), ) } } undefined
问题: v-bind
没有被解析出来。
如果 v-on的 exp 是个简单的表达式,需要将其转成函数 $event => (i++)
feat(add): v-on with simple expression as handler · gcclll/stb-vue-next@1542b41
判断是简单表达式的依据:
const isInlineStatement = !(isMemberExp || fnExpRE.test(exp.content))
即不是 member expression 也不是 function expression 。
member expression: /^[A-Za-z_$][\w$]*(?:\s*\.\s*[A-Za-z_$][\w$]*|\[[^\]]+\])*$/
function expresstion: /^\s*([\w$_]+|\([^)]*?\))\s*=>|^\s*function(?:\s+[\w$]+)?\s*\(/
测试:
|
|
const _Vue = Vue return function render(_ctx, _cache) { with (_ctx) { const { createVNode : _createVNode, openBlock : _openBlock, createBlock : _createBlock } = _Vue return (_openBlock(), _createBlock("div", { onClick: $event => (i++) }, null, 8 /* PROPS */, ["onClick"])) } } >>> event name { type: 4, loc: { start: { column: 11, line: 1, offset: 10 }, end: { column: 16, line: 1, offset: 15 }, source: 'click' }, content: 'onClick', isStatic: true, constType: 3 } >>> event handler { type: 8, loc: { source: '', start: { line: 1, column: 1, offset: 0 }, end: { line: 1, column: 1, offset: 0 } }, children: [ '$event => (', { type: 4, content: 'i++', isStatic: false, constType: 0, loc: [Object] }, ')' ] } undefined
options.cacheHandlers
属性要配合 options.prefixIdentifiers
使用。
作用是缓存事件处理函数,原理是:
|
|
缓存的附加条件: let shouldCache: boolean = context.cacheHandlers && !exp
没有表达式值的情况下才缓存,因为此时会创建一个空的函数作为事件 handler,为了避免
创建过多的无意义的空函数,使用缓存是个不错的选择(但,一般绑定了事件应该不至于不
给处理函数吧!!!)。
f805858 add v-bind transform
feat(add): v-bind transform · gcclll/stb-vue-next@f805858
|
|
v-bind is missing expression. >>> render function const _Vue = Vue return function render(_ctx, _cache) { with (_ctx) { const { createVNode : _createVNode, openBlock : _openBlock, createBlock : _createBlock } = _Vue return (_openBlock(), _createBlock("div", { name: test, age: 100, [propName || ""]: myName, [first+second || ""]: thrid, "no-need-camel-prop": noNeedCamelProp, needCamelProp: needCamelProp, noExpProp: "" }, null, 16 /* FULL_PROPS */, ["name","age","no-need-camel-prop","needCamelProp","noExpProp"])) } } undefined
v-bind 属性支持以下几种方式:
v-bind:name="test"
无缩写属性,最普通的一种用法:age="100"
缩写形式:[propName]="myName"
普通动态属性名:[first+second]="third"
表达式动态属性名:no-need-camel-prop="noNeedCamelProp"
不需要转驼峰的属性名:need-camel-prop.camel="needCamelProp"
需要转成驼峰的属性名,需要制定.camel
修饰符no-exp-prop.camel
无属性值的属性,会给默认""
值,同时给出警告,不建议使用。
更多测试用例(<f12>
)打开控制台查看 ->> 。
0cc76f0 add v-model transform
feat(add): v-model transform · gcclll/stb-vue-next@0cc76f0
<input v-model="model" />
经过 transformModel
之后的 node.props:
|
|
compiler-core 阶段的解析脑图:
从图中可以看出, v-model 指令的解析也是在 buildProps 中完成的,关于这个函数的脑 图也可以查看 buildProps(node, context) 如何构建 props ?
vue/baseCompile 解析之后的结果:
|
|
vue/compile 经过 compile-dom package(未完成) 的 transformModel 之后的结果:
|
|
fix: v-model no value · gcclll/stb-vue-next@a537be0
修复之后(genNode
没有实现 8,COMPOUND_EXPRESSION
类型),测试
不带参数的
v-model
1 2 3 4 5 6 7
const { baseParse, baseCompile } = require(process.env.PWD + '/../../static/js/vue/compiler-core.global.js') const { code } = baseCompile(`<input v-model="model" />`) console.log(code)
const _Vue = Vue return function render(_ctx, _cache) { with (_ctx) { const { createVNode : _createVNode, openBlock : _openBlock, createBlock : _createBlock } = _Vue return (_openBlock(), _createBlock("input", { modelValue: model, "onUpdate:modelValue": $event => (model = $event) }, null, 8 /* PROPS */, ["modelValue","onUpdate:modelValue"])) } }
指令
{ prefixIdentifiers: true }
选项(需要 node 环境, TODO)1 2 3 4 5 6 7 8 9
const { baseParse, baseCompile } = require(process.env.PWD + '/../../static/js/vue/compiler-core.global.js') const { code } = baseCompile(`<input v-model="model" />`, { prefixIdentifiers: true }) console.log(code)
const _Vue = Vue return function render(_ctx, _cache) { with (_ctx) { const { createVNode : _createVNode, openBlock : _openBlock, createBlock : _createBlock } = _Vue return (_openBlock(), _createBlock("input", { modelValue: model, "onUpdate:modelValue": $event => (model = $event) }, null, 8 /* PROPS */, ["modelValue","onUpdate:modelValue"])) } } undefined
组合表达式(
8,COMPOUND_EXPRESSION
)1 2 3 4 5 6 7
const { baseParse, baseCompile } = require(process.env.PWD + '/../../static/js/vue/compiler-core.global.js') const { code } = baseCompile(`<input v-model="model[index]" />`) console.log(code)
const _Vue = Vue return function render(_ctx, _cache) { with (_ctx) { const { createVNode : _createVNode, openBlock : _openBlock, createBlock : _createBlock } = _Vue return (_openBlock(), _createBlock("input", { modelValue: model[index], "onUpdate:modelValue": $event => (model[index] = $event) }, null, 8 /* PROPS */, ["modelValue","onUpdate:modelValue"])) } } undefined
带参数
1 2 3 4 5 6 7 8
const { baseParse, baseCompile } = require(process.env.PWD + '/../../static/js/vue/compiler-core.global.js') const { code } = baseCompile(`<input v-model:value="model" />`) console.log(code)
const _Vue = Vue return function render(_ctx, _cache) { with (_ctx) { const { createVNode : _createVNode, openBlock : _openBlock, createBlock : _createBlock } = _Vue return (_openBlock(), _createBlock("input", { value: model, "onUpdate:value": $event => (model = $event) }, null, 40 /* PROPS, HYDRATE_EVENTS */, ["value","onUpdate:value"])) } } undefined
不带参数的时候参数名会给一个默认值:
modelValue
, 如果有自己的参数会直接使 用提供的参数名。动态参数
1 2 3 4 5 6 7 8
const { baseParse, baseCompile } = require(process.env.PWD + '/../../static/js/vue/compiler-core.global.js') const { code } = baseCompile(`<input v-model:[value]="model" />`) console.log(code)
有问题结果:
const _Vue = Vue return function render(_ctx, _cache) { with (_ctx) { const { createVNode : _createVNode, openBlock : _openBlock, createBlock : _createBlock } = _Vue return (_openBlock(), _createBlock("input", { [value]: model, : $event => (model = $event) }, null, 16 /* FULL_PROPS */)) } }
结果显示,动态属性的事件名没有被解析出来
: $event => (model = $event)
。修复之后结果(fix: v-model dynamic arg generate · gcclll/stb-vue-next@94a7a85):
const _Vue = Vue return function render(_ctx, _cache) { with (_ctx) { const { createVNode : _createVNode, openBlock : _openBlock, createBlock : _createBlock } = _Vue return (_openBlock(), _createBlock("input", { [value]: model, ["onUpdate:" + value]: $event => (model = $event) }, null, 16 /* FULL_PROPS */)) } }
缓存事件回调函数(
cacheHandlers: true
, TODO)需要结合
prefixIdentifiers: true
使用。
针对 v-model 还需要 compile-runtime 阶段的支持,由于这里还没完成,所以这里的结果
和 vue-next 测试结果会有些出入,出入点在于 compiler-runtime 期会将 modelValue:
model
删除。
更多测试用例(<f12>
)打开控制台查看 ->> 。
bf18a84 add v-once transform
feat(add): v-once · gcclll/stb-vue-next@bf18a84
|
|
v-once
指令的实现看似挺简单的,将解析后的 node 节点缓存到 seen: WeakSet
中,
下次使用的时候直接取缓存(context.cache(...)
),而不是重新生成 codegenNode
JS_CACHE_EXPRESSION
结构:
|
|
generator 阶段实现:feat(add): v-once generator · gcclll/stb-vue-next@8bacf14
在 genNode()
中增加 JS_CACHE_EXPRESSION
类型的分支处理。
|
|
测试:
|
|
>>> <div :id="foo" v-once /> const _Vue = Vue return function render(_ctx, _cache) { with (_ctx) { const { setBlockTracking : _setBlockTracking, createVNode : _createVNode } = _Vue return _cache[1] || ( _setBlockTracking(-1), _cache[1] = _createVNode("div", { id: foo }, null, 8 /* PROPS */, ["id"]), _setBlockTracking(1), _cache[1] ) } } >>> 标签中嵌套使用 const _Vue = Vue return function render(_ctx, _cache) { with (_ctx) { const { setBlockTracking : _setBlockTracking, createVNode : _createVNode, openBlock : _openBlock, createBlock : _createBlock } = _Vue return (_openBlock(), _createBlock("div", null, [ _cache[1] || ( _setBlockTracking(-1), _cache[1] = _createVNode("div", { id: foo }, null, 8 /* PROPS */, ["id"]), _setBlockTracking(1), _cache[1] ) ])) } } >>> 在自定义组件上 const _Vue = Vue return function render(_ctx, _cache) { with (_ctx) { const { setBlockTracking : _setBlockTracking, resolveComponent : _resolveComponent, createVNode : _createVNode, openBlock : _openBlock, createBlock : _createBlock } = _Vue return (_openBlock(), _createBlock("div", null, [ _cache[1] || ( _setBlockTracking(-1), _cache[1] = _createVNode(_component_Comp, { id: foo }, null, 8 /* PROPS */, ["id"]), _setBlockTracking(1), _cache[1] ) ])) } } undefined
TODO 缺少: const _component_Comp = _resolveComponent("Comp")
更多测试用例(<f12>
)打开控制台查看 ->> 。
acdea14 add v-if transform
v-if
指令源码脑图可参考: 05 v-if 指令(git:0a591b6)
对于 v-if|else|else-if
指令在 transform 阶段,转换收集 transformXxx 函数过程中,
会先针对指令进行处理,比如: v-else, v-else-if
指令的组件会被解析到 v-if
节
点的 node.branches[]
分支数组里面之后被删除,这些都是在收集 transformXxx 之前需要完成的。
包括 v-for
指令都需要经过 createStructuralDirectiveTransform()
函数封装一层
之后,返回对应的 transformXxx
函数。
|
|
通过 for (...)
将所有 v-if/v-for 相关指令经过他们自己的处理函数(比如:
processIf
) 之后得到最终的 onExit
收集到 exitFns
中,在处理过程中随时会出
现节点的删除操作(比如: v-else
节点会在解析完之后被删除),在正常的 traverse 过
程中这些节点都不会再存在。
PS: 正确理解应该属于移动操作,因为原始的 AST 结构并没改变,只不过是在原有的 AST 数结构中移除到新的 AST 节点下面了。
acdea14 v-if transform init
feat(init): v-if transform · gcclll/stb-vue-next@acdea14
|
|
初始化 v-if process 函数, processIf 函数里面会针对 v-if 节点甚至它的兄弟节点做
一系列操作,比如将下一个是 v-else
的兄弟节点删除移到自己的 branches[]
里面。
9039a3e v-if transform processIf
feat: v-if processIf · gcclll/stb-vue-next@9039a3e
这里增加了两个函数的实现:
processIf, 解析 if,创建
IF,9
类型的结构,替换 v-if 原来的 ast1 2 3 4 5
const ifNode: IfNode = { type: NodeTypes.IF, loc: node.loc, branches: [branch] }
其中 branches 保存着所有 v-else, v-else-if 分支节点,这里其实是创建了一个默认 的分支节点,因为
v-if
系列指令在render
函数中是以三元运算符(?:
)形式存 在的,所以 if 后面必须要有一个分支,即condition ? node1 : node2
中的 node2 必须是个有效的值,才能正常使用?:
运算符。所以,如果只有
v-if
指令的时候三元符后面的值起始是个空值(好像是null
)createIfBranch, 创建
v-if
的分支节点的1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
function createIfBranch(node: ElementNode, dir: DirectiveNode): IfBranchNode { return { type: NodeTypes.IF_BRANCH, loc: node.loc, // condition ? v-if node : v-else node condition: dir.name === 'else' ? undefined : dir.exp, // 如果用的是 <template v-if="condition" ... 就需要 node.children // 因为 template 本身是不该被渲染的 children: node.tagType === ElementTypes.TEMPLATE && !findDir(node, 'for') ? node.children : [node], // 对于 v-for, v-if/... 都应该给它个 key, 这里是用户编写是的提供的唯一 key // 如果没有解析器会默认生成一个全局唯一的 key userKey: findProp(node, `key`) } }
注意看最后一个属性,
v-if
分支也是需要一个key
属性的。
44985b4 v-if transform createIfBranch
feat: v-if createIfBranch · gcclll/stb-vue-next@44985b4
|
|
这里的结构(v-if
)在 render 函数中的对应关系:
test ? consequent : alternate
如果有 v-else-if 时候, alternate
结构会是个完整的 JS_CONDITIONAL_EXPRESSION
,即: alternate: { test, consequent, alternate, ...}
所以:
test ? consequent : test1 ? consequent 1 : alternate
fix: no v-if transform · gcclll/stb-vue-next@1e24eb7
到这里 v-if 指令 transform 阶段已经完成,测试结果:
|
|
>>> ast.codegenNode 结果 { type: 9, loc: { start: { column: 1, line: 1, offset: 0 }, end: { column: 17, line: 1, offset: 16 }, source: '<div v-if="ok"/>' }, branches: [ { type: 10, loc: [Object], condition: [Object], children: [Array], userKey: undefined } ], codegenNode: { type: 19, test: { type: 4, content: 'ok', isStatic: false, isConstant: false, loc: [Object] }, consequent: { type: 13, tag: '"div"', props: [Object], children: undefined, patchFlag: undefined, dynamicProps: undefined, directives: undefined, isBlock: true, disableTracking: false, loc: [Object] }, alternate: { type: 14, loc: [Object], callee: Symbol(createCommentVNode), arguments: [Array] }, newline: true, loc: { source: '', start: [Object], end: [Object] } } } undefined
+RESULTS: 错误结果
const _Vue = Vue return function render(_ctx, _cache) { with (_ctx) { const { createVNode : _createVNode, openBlock : _openBlock, createBlock : _createBlock, createCommentVNode : _createCommentVNode } = _Vue return (_openBlock(), _createBlock("div", { key: 0 })) } } >>> ast.codegenNode 结果 { type: 13, tag: '"div"', props: { type: 15, loc: { source: '', start: [Object], end: [Object] }, properties: [ [Object] ] }, children: undefined, patchFlag: undefined, dynamicProps: undefined, directives: undefined, isBlock: true, disableTracking: false, loc: { start: { column: 1, line: 1, offset: 0 }, end: { column: 17, line: 1, offset: 16 }, source: '<div v-if="ok"/>' } } undefined
结果显示是不对的,因为创建的 IF
结构没有替换 ast 🌲中原来的节点,追踪后发现是
漏掉了 context.replaceNode(node)
的实现。
fix: v-if codegenNode is incorrect · gcclll/stb-vue-next@47c30d2
traverseNode 中需要增加 case 9,IF
分支处理,遍历所有的 branches[]
。
fix: v-if branches no codegenNode · gcclll/stb-vue-next@179f06f
742757e v-if generator
feat: v-if generator · gcclll/stb-vue-next@742757e
genNode 增加 JS_CONDITIONAL_EXPRESSION
分支处理(genConditionalExpression
)
|
|
genConditionalExpression
处理分为三个部分
test
生成条件表达式,这里是:ok
,如果是表达式需要括号:(a + b)
consequent
用来生成?
后面的表达式,即ok
结果为 truth 时执行alternate
用来生成:
后面的表达式,即ok
结果为 falsy 时执行alternate
中的结构可能也是个JS_CONDITIONAL_EXPRESSION
结构,代表可能有v-else-if
分支,如:(a + b) ? node1 : (c + d) ? node2 : othernode
。
测试:
|
|
>>> basic v-if const _Vue = Vue const { createVNode: _createVNode, createCommentVNode: _createCommentVNode } = _Vue const _hoisted_1 = { key: 0 } return function render(_ctx, _cache) { with (_ctx) { const { createVNode : _createVNode, openBlock : _openBlock, createBlock : _createBlock, createCommentVNode : _createCommentVNode } = _Vue return ok ? (_openBlock(), _createBlock("div", _hoisted_1)) : _createCommentVNode("v-if", true) } } >>> template v-if const _Vue = Vue const { createVNode: _createVNode, createCommentVNode: _createCommentVNode, createTextVNode: _createTextVNode } = _Vue const _hoisted_1 = /*#__PURE__*/_createVNode("div", null, null, -1 /* HOISTED */) const _hoisted_2 = /*#__PURE__*/_createTextVNode("hello") const _hoisted_3 = /*#__PURE__*/_createVNode("p", null, null, -1 /* HOISTED */) return function render(_ctx, _cache) { with (_ctx) { const { createVNode : _createVNode, createTextVNode : _createTextVNode, Fragment : _Fragment, openBlock : _openBlock, createBlock : _createBlock, createCommentVNode : _createCommentVNode } = _Vue return ok ? (_openBlock(), _createBlock(_Fragment, { key: 0 }, [ _hoisted_1, _hoisted_2, _hoisted_3 ], 64 /* STABLE_FRAGMENT */)) : _createCommentVNode("v-if", true) } } undefined
BUG: 这里居然少了个 _hoisted_2
???
|
|
答: genNode()
中缺少对 4,TEXT_CALL
纯文本类型处理。
解:fix: v-if TEXT_CALL gen node · gcclll/stb-vue-next@2372b5f
更多测试用例(<f12>
)打开控制台查看 ->> 。
fa77b51 v-else/v-else-if
feat(add): v-else · gcclll/stb-vue-next@fa77b51
修改点:
processCodegen()
函数里面增加分支处理这里有一个需要注意的点:
getParentCondition()
会一直查找JS_CONDITIONAL_EXPRESSION
类型节点的alternate
,如果它依旧是个JS_CONDITIONAL_EXPRESSION
类型,说明是多级的if/else
条件语句,直到找到最 后一个不是为止。相当于 :
c1 ? cons1 : c2 ? cons2 : c3 ? cons3 : alt
会一直从c1
节点开始 查找直到找到最后的那个alt
节点为止,然后将新的分支挂到alt
后面组织成新 的分支:c1 ? cons1 : c2 ? cons2 : c3 ? cons3 : c4 ? cons4 : newalt
PS: c1, c2, c3, c4 分别代表分支节点的
test
,最后追加的c4 ? cons4 : newalt
三个对象都属于新加的节点,{test -> c4, cons4 -> consequent, alternate -> newalt }
processIf()
里增加分支处理新增代码里有个
while
循环去从当前的分支节点开始在它的兄弟节点里面往回找,直 到找到第一个9,IF
节点,这中间不允许出现其他有效节点(除注释,空文本节点外), 因为v-if/else
指令节点必须紧靠着。找到之后,要将当前分支节点删除,并且同时要去手动
traverseNode(branch)
一次, 因为他在原来的 ast 树种删除了,所以原来的 traverse 进程不会遍历它,因此需要手 动执行 traverse 去处理它及其孩子节点生成对应的 codegenNode 。然后将其 push 到
9,IF
节点的node.branches
里面作为分支。isSameKey(a,b)
新增,检测两个 key 属性是不是相同几种判定为不相同的条件:
key 类型不同 (
a.type !== b.type
)key 值不同 (
a.value.content !== b.value.content
)key 如果是指令类型,检测表达式类型,静态属性异同(
isStatic
)
getParentCondition()
新增,递归9,IF
节点的node.alternate.alternate.alternate...
直到找到alternate
不是JS_CONDITIONAL_EXPRESSION
的情况
FIX: fix: v-else current node dont removed · gcclll/stb-vue-next@464d681
测试:
|
|
const _Vue = Vue return function render(_ctx, _cache) { with (_ctx) { const { createVNode : _createVNode, openBlock : _openBlock, createBlock : _createBlock, createCommentVNode : _createCommentVNode } = _Vue return ok ? (_openBlock(), _createBlock("div", { key: 0 })) : (_openBlock(), _createBlock("p", { key: 1 })) } } undefined
更多测试(<f12>
)打开控制台查看 ->> 。
BUG: v-else-if 被解析成了 else
因为 parser 阶段匹配正则不对。
fix: parser v-else-if failed · gcclll/stb-vue-next@5b83d1c
6c82066 add v-for transform
feat(init): v-for · gcclll/stb-vue-next@3a1662e
feat: v-for directive · gcclll/stb-vue-next@6c82066
v-for 指令实现过程中需要用到的几个函数:
transformFor()
最终生成的 tranformXxx 函数createStructuralDirectiveTransform()
同v-if
指令processFor()
处理v-for
指令入口processCodegen()
同v-if
用来生成codegenNode
的函数parseForExpression()
将v-for="item in items"
表达式解析成ForParseResult{source, value, key, index}
类型 AST 。createAliasExpression()
给value, key, index
创建SIMPLE_EXPRESSION
类型 结构。createForLoopParams()
创建_renderList
函数回调的参数[value, key, index]
,如果没有使用默认变量:_
或__
,如:(_, __, index)
其中 parseForExpression()
函数是解析 v-for
表达式的核心函数,里面使用了三个
正则,用来匹配指令表达式:
const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
匹配
v-for="item in items"
中的值部分1 2 3 4 5 6 7 8
const re = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/ const log = (params) => console.log(params.map((p, i) => `${i}, ${p}`).join(`\n`)) log.title = console.log log.title(`>>> 匹配 item in items`) log("item in items".match(re)) log.title(`>>> 匹配 (item, key) in items`) log("( item, key ) in items".match(re))
>>> 匹配 item in items 0, item in items 1, item 2, items >>> 匹配 (item, key) in items 0, ( item, key ) in items 1, ( item, key ) 2, items undefined
const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
这个正则表达式用来匹配
(item, key) in items
中的item
和key
1 2 3 4 5 6 7 8
const re = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/ const log = (params) => console.log(params.map((p, i) => `${i}, ${p}`).join(`\n`)) log.title = console.log log.title(`>>> 匹配 'item, key, index' 中的 key 和 index`) log("item, key, index".match(re)) log.title(`>>> 匹配 "item, key" 中的 key`) log("item, key".match(re))
>>> 匹配 'item, key, index' 中的 key 和 index 0, , key, index 1, key 2, index >>> 匹配 "item, key" 中的 key 0, , key 1, key 2, undefined undefined
const stripParensRE = /^\(|\)$/g
这个用来匹配(item, key, index)
前后括号
parseForExpression()
核心实现:
source
数据源,forAliasRE
匹配后的RHS
值1 2 3 4 5 6 7
source: { type: 4, // SIMPLE_EXPRESSION loc: { source: 'obj', start: [Object], end: [Object] }, isConstant: false, content: 'obj', isStatic: false }
value
的取值,在 AST 中对应valueAlias
valueContent = valueContent.replace(forIteratorRE, '').trim()
通过匹配
key, index
的正则,反向替换得到value
1 2 3 4
const re = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/ console.log(`item, key, index`.replace(re, '').trim()) console.log(`>>> 支持解构`) console.log(`[ id, value ], key, index`.replace(re, '').trim())
item >>> 支持解构 [ id, value ] undefined
解析后的结构:
1 2 3 4 5 6 7
valueAlias: { type: 4, // SIMPLE_EXPRESSION loc: { source: 'value', start: [Object], end: [Object] }, isConstant: false, content: 'value', isStatic: false }
key
取值处理,在 AST 中对应keyAlias
1 2 3 4 5 6 7
keyAlias: { type: 4, loc: { source: 'key', start: [Object], end: [Object] }, isConstant: false, content: 'key', isStatic: false }
index
取值处理,在 AST中对应objectIndexAlias
1 2 3 4 5 6 7
objectIndexAlias: { type: 4, loc: { source: 'index', start: [Object], end: [Object] }, isConstant: false, content: 'index', isStatic: false }
测试:
|
|
type: 11 >>> 数据源 { type: 4, loc: { source: 'obj', start: { column: 37, line: 1, offset: 36 }, end: { column: 40, line: 1, offset: 39 } }, isConstant: false, content: 'obj', isStatic: false } >>> value { type: 4, loc: { source: 'value', start: { column: 15, line: 1, offset: 14 }, end: { column: 20, line: 1, offset: 19 } }, isConstant: false, content: 'value', isStatic: false } >>> key { type: 4, loc: { source: 'key', start: { column: 22, line: 1, offset: 21 }, end: { column: 25, line: 1, offset: 24 } }, isConstant: false, content: 'key', isStatic: false } >>> index { type: 4, loc: { source: 'index', start: { column: 27, line: 1, offset: 26 }, end: { column: 32, line: 1, offset: 31 } }, isConstant: false, content: 'index', isStatic: false } >>> _renderList(obj, (value, key, index) => {...}) 第二个参数 { type: 18, // JS_FUNCTION_EXPRESSION params: [ { type: 4, loc: [Object], isConstant: false, content: 'value', isStatic: false }, { type: 4, loc: [Object], isConstant: false, content: 'key', isStatic: false }, { type: 4, loc: [Object], isConstant: false, content: 'index', isStatic: false } ], returns: { type: 13, tag: '"span"', props: undefined, children: undefined, patchFlag: undefined, dynamicProps: undefined, directives: undefined, isBlock: true, disableTracking: false, loc: { start: [Object], end: [Object], source: '<span v-for="(value, key, index) in obj" />' } }, newline: true, isSlot: false, loc: { source: '', start: { line: 1, column: 1, offset: 0 }, end: { line: 1, column: 1, offset: 0 } } } undefined
39a20fe add v-for generator
feat(add): v-for generator · gcclll/stb-vue-next@39a20fe
codegen 阶段新增对应的实现: 18,JS_FUNCTION_EXPRESSION
这个主要是用来解析 _renderList(source, (value, key, index) => { ... })
函数的
第二个参数的,这是个用来 render 列表项的函数。
测试:
|
|
const _Vue = Vue return function render(_ctx, _cache) { with (_ctx) { const { renderList : _renderList, Fragment : _Fragment, openBlock : _openBlock, createBlock : _createBlock, createVNode : _createVNode } = _Vue return (_openBlock(true), _createBlock(_Fragment, null, _renderList(items, (item) => { return (_openBlock(), _createBlock("span")) )), 256 /* UNKEYED_FRAGMENT */)) } } undefined
更多测试用例请
<f12>
打开控制台查看。
7cb8908 add slot outlet transform
feat(add): v-slot transform · gcclll/stb-vue-next@7cb8908
transform <slot />
标签。
<slot/>
在 render 函数中是以 _renderSlot($slot, name, props, children)
形式存在。
<slot> 上不允许自定义的指令存在?
相关函数/参数:
transformSlotOutlet()
该阶段的tranformXxx
函数SlotOutletProcessResult
类型定义{slotName, slotProps}
processSlotOutlet()
,<slot/>
的处理过程首先是解析插槽名称(
name
属性),该属性可以是动态(<slot :name="myslot"/>
)也 可以是静态的(<slot name="myslot"/>
)。然后解析出插槽上定义的一些属性(静态),除了
:name
之外插槽上 不允许有其他的 指令类型的属性存在 。children
参数<slot><div/><p/></slot>
在
<slot>
标签内的所有元素(<div/><p/>
)会被解析成_renderSlot
的第四个参数。
测试:
|
|
{ type: 1, ns: 0, tag: 'slot', tagType: 2, props: [], isSelfClosing: true, children: [], loc: { start: { column: 1, line: 1, offset: 0 }, end: { column: 8, line: 1, offset: 7 }, source: '<slot/>' }, codegenNode: { type: 14, // JS_CALL_EXPRESSION loc: { start: [Object], end: [Object], source: '<slot/>' }, callee: Symbol(renderSlot), arguments: [ '$slots', '"default"' ] } } undefined
更多 codegenNode 结果请
<f12>
打开控制台查看。
ebdb1ed add track slot scopes
feat: add track slot scopes · gcclll/stb-vue-next@ebdb1ed
还不知道干吗的❓
|
|
36c1f36 (v-slot)build user component as slots
组件是如何当做 slot 处理的
feat(add): user component treat as slot to build · gcclll/stb-vue-next@36c1f36
完整用户组件当 slot 处理流程图:
<Comp><div/></Comp>
这里 Comp
是组件类型(tagType=1,COMPONENT
) 会在 transformElement
中被当做
slot
来处理调用 buildSlots()
。
更多测试用例(<f12>
)打开控制台查看 ->> 。
fbed5cf add component with default slot without <template> transform
feat(add): default slot without template · gcclll/stb-vue-next@fbed5cf
<Comp><div/></Comp>
这种情况,用户组件上既没有 v-slot
孩子节点里面也没有 <template v-slot>
最后
的处理是 <Comp></Comp>
里面的所有 children 被当做默认插槽处理。
该示例符合:
组件上没有
v-slot
指令孩子节点里面没有
<template v-slot:name="slotProps">
因此,这里的处理是将 <div/>
即 comp.children
全部作为默认的 slot:default
来处理。
处理流程: <Comp>
-> transformElement
-> isComponent
-> buildSlots()
->
{slots, hasDynamicSlots}
-> vnodeChildren
-> {type: 13,VNODE_CALL,
children: vnodeChildren, ... }
|
|
{ type: 13, tag: '_component_Comp', props: undefined, children: { type: 15, loc: { start: [Object], end: [Object], source: '<Comp><div/></Comp>' }, properties: [ [Object], [Object] ] }, patchFlag: undefined, dynamicProps: undefined, directives: undefined, isBlock: true, disableTracking: false, loc: { start: { column: 1, line: 1, offset: 0 }, end: { column: 20, line: 1, offset: 19 }, source: '<Comp><div/></Comp>' } } undefined
另外在 transformElement()
函数中通过 isComponent = node.tagType ===
ElementTypes.COMPONENT
检测是不是用户组件,如果是继续解析该组件类型(resolveComponentType()
)。
这里最终得到的结果是: vnodeTag = _component_Comp
作为标签名,也是该 <Comp/>
组件在 Vue 实例过程中的存在的标签名(组件名称)。
8bed175 add component with default slot without <template> generator
feat(add): user component resolver generator · gcclll/stb-vue-next@8bed175
因为在 transform 阶段 transformElement 过程中,检测到 <Comp>
是个用户组件,所
以将其增加到了 context.components.add('Comp')
中了,在 generator 阶段会去检测
这个 components
用来解析组件得到组件的引用:
_component_Comp = _resolveComponent("Comp")
新增的代码主要由两部分:
新增
genAssets()
函数处理context.components
处理之后的结果就是增加
_component_Comp = _resolveComponent("Comp")
在
generate()
中增加ast.components
检测,如果有内容调用genAssets()
解 析
|
|
测试:
|
|
const _Vue = Vue const { createVNode: _createVNode } = _Vue const _hoisted_1 = /*#__PURE__*/_createVNode("div", null, null, -1 /* HOISTED */) return function render(_ctx, _cache) { with (_ctx) { const { createVNode : _createVNode, resolveComponent : _resolveComponent, withCtx : _withCtx, openBlock : _openBlock, createBlock : _createBlock } = _Vue const _component_Comp = _resolveComponent("Comp") return (_openBlock(), _createBlock(_component_Comp, null, { default: _withCtx(() => [ _hoisted_1 ]), _: 1 })) } } undefined
缺少 1 /* STABLE */
注释: fix: slot flag text · gcclll/stb-vue-next@08a6fca
注意 _createBlock()
的第三个参数变成了一个对象(children
) 对象里面包含了两个
属性: default
和 _
分别代表了默认插槽下的孩子节点,和该插槽标识(1-STABLE,2-FORWARDED,3-DYNAMIC
)
9f58154 fix: 文本节点没有合并(transformText
)
fix: adjacent text node need merge · gcclll/stb-vue-next@9f58154
对于 `<Comp v-slot="{ foo }">{{ foo }}{{ bar }}</Comp>`
用例得到的结果非预期。
vue-next 正确结果:
|
|
You are running a development build of Vue. Make sure to use the production build (*.prod.js) when deploying for production. [ { type: 12, content: { type: 8, loc: [Object], children: [Array] }, loc: { start: [Object], end: [Object], source: '{{ foo }}' }, codegenNode: { type: 14, loc: [Object], callee: Symbol(createTextVNode), arguments: [Array] } } ] >>> 解析后的 {{ foo }} {{ bar }} 应该合并 { type: 8, loc: { start: { column: 24, line: 1, offset: 23 }, end: { column: 33, line: 1, offset: 32 }, source: '{{ foo }}' }, children: [ { type: 5, content: [Object], loc: [Object] }, ' + ', { type: 5, content: [Object], loc: [Object] } ] } >>> 下面正是合并之后的两个插值 [ { type: 5, content: { type: 4, isStatic: false, constType: 0, content: 'foo', loc: [Object] }, loc: { start: [Object], end: [Object], source: '{{ foo }}' } }, ' + ', { type: 5, content: { type: 4, isStatic: false, constType: 0, content: 'bar', loc: [Object] }, loc: { start: [Object], end: [Object], source: '{{ bar }}' } } ] undefined
看下现阶段 stb-vue-next 输出结果:
|
|
{ type: 5, content: { type: 4, isStatic: false, isConstant: false, content: 'foo', loc: { start: [Object], end: [Object], source: 'foo' } }, loc: { start: { column: 24, line: 1, offset: 23 }, end: { column: 33, line: 1, offset: 32 }, source: '{{ foo }}' } } { type: 5, content: { type: 4, isStatic: false, isConstant: false, content: 'bar', loc: { start: [Object], end: [Object], source: 'bar' } }, loc: { start: { column: 33, line: 1, offset: 32 }, end: { column: 42, line: 1, offset: 41 }, source: '{{ bar }}' } } undefined
stb-vue-next 明显 returns 里面包含了两个元素分别是: {{ foo }}
和 {{ bar }}
这是因为在 transformText
里面没有进行文本(插值也算)合并。
0773374 add component nested slots scoping
fix: patchFlag should |= but != · gcclll/stb-vue-next@0773374
插槽嵌套使用。
|
|
插槽嵌套使用时的解析是先里后外,因为在 transform 阶段 ast 阶段转换之后,会进行回 溯,回溯过程是相反的。
如:
ast 结构 transform:
Comp.children -> [template] -> template.children -> [Inner, foo, bar, baz] ->
Inner.children -> [foo, bar, baz]
ast回溯:
Inner.children -> Inner -> template.children -> template -> Comp.children -> Comp
所以首先执行 transformElement()
的是 Inner
所以它会先进入 buildSlots()
构
建插槽结构,完了之后是 <template>
最后是 <Comp>
render 推导过程:
<Inner v-slot="{bar}">{{foo}}{{bar}}{{baz}}</Inner>
是在用户组件上应用了
v-slot
且是默认插槽,因此它的所有孩子节点都会成为默认 插槽一部分。结果:
1 2 3 4 5 6
_createVNode(_component_Inner, null, { default: _withCtx(({ bar }) => [ _createTextVNode(_toDisplayString(foo) + _toDisplayString(bar) + _toDisplayString(baz), 1 /* TEXT */) ]), _: 2, /* DYNAMIC */ }, 1024 /* DYNAMIC_SLOTS */)
这里使用的是
_createVNode
因为Inner
非唯一的孩子节点。<template #default="{foo}">...</template>
1 2 3 4 5 6 7 8 9 10 11 12
(_openBlock(), _createBlock(_component_Comp, null { default: _withCtx(({ foo }) => [ _createVNode(_component_Inner, null, { default: _withCtx(({ bar }) => [ _createTextVNode(_toDisplayString(foo) + _toDisplayString(bar) + _toDisplayString(baz), 1 /* TEXT */) ]), _: 2 /* DYNAMIC */ }, 1024 /* DYNAMIC_SLOTS */), _createTextVNode(_toDisplayString(foo) + _toDisplayString(bar) + _toDisplayString(baz), 1 /* TEXT */) ]), _: 1 /* STABLE */ }))
这里有个判断
Inner
为动态 slot 的关键点:context.scopes.vSlot > 0
而这个 值是在收集transformXxx
阶段递增 +1 而后回溯过程中 -1 的。在收集阶段
<template>
收集到trackSlotScopes()
函数此时context.scopes.vSlot = 1
然后递归 children执行到Inner
收集阶段的时候context.scopes.vSlot = 2
直到递归结束。开始回溯,先是在
Inner
上应用 transformElement 直到Inner
回溯到执行trackSlotScopes()
应为它也有v-slot
指令,所以Inner
能收集到这个函数, 它回溯结束执行trackSlotScopes()
随之context.scopes.vSlot--
所以此时,在 回溯Inner
结束之后在开始回溯<template>
之前context.scopes.vSlot = 1
。这就是
Inner
为什么没有动态属性名但是依旧会判断为动态插槽的原理。一句话:如果在
<template v-slot>
里面嵌套另一个v-slot
那个这个不管有没有 动态属性名都会被当做动态插槽来处理。
c1ace74 add component with v-slot inside v-for
|
|
let hasDynamicSlots = context.scopes.vSlot > 0 || context.scopes.vFor > 0;
根据这个判断决定 v-for 里面的 v-slot 为动态插槽。
cfef20e add named slot with v-if
feat(add): slot with v-if · gcclll/stb-vue-next@cfef20e
|
|
测试:
|
|
const _Vue = Vue return function render(_ctx, _cache) { with (_ctx) { const { createTextVNode : _createTextVNode, resolveComponent : _resolveComponent, withCtx : _withCtx, createSlots : _createSlots, createVNode : _createVNode, openBlock : _openBlock, createBlock : _createBlock } = _Vue const _component_Comp = _resolveComponent("Comp") return (_openBlock(), _createBlock(_component_Comp, null, _createSlots({ _: 2 /* DYNAMIC */ }, [ ok ? { name: "one", fn: _withCtx(() => [ _createTextVNode("hello") ]) } : undefined ]), 1024 /* DYNAMIC_SLOTS */)) } } undefined
动态 slots 处理之后:
_createSlots({ /* 静态 slots */, [ /* 动态 slots 列表 */ ] })
看下面的运行时的 createSlots(slots, dynamicSlots)
其实就是讲两者合并在一起了
v-if 是个对象 { fn, name }
v-for 是该对象的数组。
|
|
拓展: v-else, v-else-if
feat(add): v-else/v-else-if with v-slot · gcclll/stb-vue-next@e48d46a
与普通的 v-else/v-else-if 处理机制一样,首先是要找到他们兄弟节点前面的 v-if 节点, 然后将该节点挂接到 v-if 节点后面。
核心代码:
|
|
c1ace74 add v-for on v-slot component
feat(add): v-slot inside v-for · gcclll/stb-vue-next@c1ace74
v-for 的处理和 v-if 原理是一样的,最后返回的都是 {name, fn}
类型,只不过
v-for 返回的是这个类型的数组。
v-for _renderList 和 slot 的结合:
|
|
测试:
|
|
const _Vue = Vue return function render(_ctx, _cache) { with (_ctx) { const { toDisplayString : _toDisplayString, createTextVNode : _createTextVNode, resolveComponent : _resolveComponent, withCtx : _withCtx, renderList : _renderList, createSlots : _createSlots, createVNode : _createVNode, openBlock : _openBlock, createBlock : _createBlock } = _Vue const _component_Comp = _resolveComponent("Comp") return (_openBlock(), _createBlock(_component_Comp, null, _createSlots({ _: 2 /* DYNAMIC */ }, [ _renderList(list, (name) => { return { name: name, fn: _withCtx(() => [ _createTextVNode(_toDisplayString(name), 1 /* TEXT */) ]) } )) ]), 1024 /* DYNAMIC_SLOTS */)) } } undefined
无非就是不同类型元素、组件 render 方式的组合。
小结:v-slot 几种用法
<Comp><div/></Comp>
, 用户组件上的默认插槽1 2 3 4 5 6 7
const _hoisted_1 = /*# __PURE__ */_createVNode('div', null, null, -1 /* HOISTED */) (_openBlock(), _createBlock(_component_Comp, null, { default: _withCtx(() => [ _hoisted_1 ]), _: 1 /* STABLE */ }))
<Comp v-slot="slotProps"><div/></Comp>
, 用户组件上带slotProps
的默认插槽1 2 3 4 5 6 7
const _hoisted_1 = /*# __PURE__ */_createVNode('div', null, null, -1 /* HOISTED */) (_openBlock(), _createBlock(_component_Comp, null, { default: _withCtx((slotProps) => [ _hoisted_1 ]), _: 1 /* STABLE */ }))
<Comp v-slot:named="slotProps">
, 用户组件上具名插槽1 2 3 4 5 6
const _hoisted_1 = /*# __PURE__ */_createVNode('div', null, null, -1 /* HOISTED */) (_openBlock(), _createBlock(_component_Comp, null, { named: _withCtx((slotProps) => [ _hoisted_1 ]) }))
<Comp v-slot:[named]="slotProps">
, 用户组件上动态具名插槽1 2 3 4 5 6
const _hoisted_1 = /*# __PURE__ */_createVNode('div', null, null, -1 /* HOISTED */) (_openBlock(), _createBlock(_component_Comp, null, { [named]: _withCtx((slotProps) => [ _hoisted_1 ]) }))
<template> 默认插槽
<Comp><template v-slot="slotProps"><div/></template></Comp>
<template>
上的默认插槽,这个时候<Comp>
不能在使用v-slot
,下同<template> 具名插槽
<Comp><template v-slot:named="slotProps"></Comp>
1 2 3 4
return (_openBlock(), _createBlock(_component_Comp, null, { named: _withCtx((slotProps) => []), _: 1 /* STABLE */ }))
<template> 动态具名插槽
<Comp><template v-slot:[named]="slotProps"></Comp>
1 2 3 4
return (_openBlock(), _createBlock(_component_Comp, null, { [named]: _withCtx((slotProps) => []), _: 1 /* STABLE */ }))
<template> 具名插槽,其余非 template 元素当做默认插槽处理
<Comp><template v-slot:named="slotProps"><div/></template><div :id="defaultSlotId"/></Comp>
<template>
上的具名插槽 + 默认插槽,在组件内的非 <template> 元素(即:<div/>
)都会被动作默认插槽来处理1 2 3 4 5 6 7 8 9 10 11 12
const _hoisted_1 = /*# __PURE__ */_createVNode("div", null, null, -1 /* HOISTED */) _createBlock(_component_Comp, null, { named: _withCtx((slotProps) => [ _hoisted_1 ]), default: _withCtx(() => [ _createVNode('div', { id: defaultSlotId }, null, 8 /* PROPS */, ["id"]) ]) _: 1 /* STABLE */ })
v-slot + v-if 插槽使用
<Comp><template v-if="ok" #named="slotProps">{{ bar }}</template></Comp>
配合 v-if 使用的 slot template, 最后解析成 :
ok ? { name: 'named', fn : _withCtx(() => [ _createTextVNode(_toDisplayString(bar)) ]) } : undefined
1 2 3 4 5 6 7 8 9 10 11
return (_openBlock(), _createBlock(_component_Comp, null, _renderSlots( { _: 2 /* DYNAMIC */ }, [ ok ? { name: 'named', fn: _withCtx((slotProps) => [ _createTextVNode(_toDisplayString(bar), 1 /* TEXT */) ]) } : undefined ] )))
v-slot + v-for 插槽上使用
<Comp><template v-for="i in list" #named="{ prop }">{{ bar }}</template></Comp>
1 2 3 4 5 6 7 8 9 10 11
return (_openBlock(), _createBlock(_component_Comp, null, _renderSlots( { _: 2 /* DYNAMIC */ }, _renderList(list, (i) => { return { name: 'named', fn: _withCtx(({ prop }) => [ _createTextVNode(_toDisplayString(bar), 1 /* TEXT */) ]) } }) )))
插槽按状态分为:
静态插槽,动态插槽除外的插槽
动态插槽,有
v-if/v-for/v-else[-if]
指令或v-slot:[named]
或作为v-for
节点的孩子节点都视为动态插槽
update vue-next merge into stb-vue-next[2020-12-11 16:54:06]
更新 vue-next 合并到 stb-vue-next。
ast.ts 更新
SimpleExpressionNode
: 去掉isConstant
属性,增加constType
去掉了
StaticType
增加ConstantType
1 2 3 4 5 6 7 8 9 10 11
/** * Static types have several levels. * Higher levels implies lower levels. e.g. a node that can be stringified * can always be hoisted and skipped for patch. */ export const enum ConstantTypes { NOT_CONSTANT = 0, CAN_SKIP_PATCH, CAN_HOIST, CAN_STRINGIFY }
codegen.ts 更新
fix: merge vue-next codegen.ts · gcclll/stb-vue-next@521b879
parse.ts 更新
ast 结构中的
isConstant
改成ConstantTypes
类型值fix: merge vue-next parse.ts isConstant -> constType · gcclll/stb-vue-next@fdbdc4f
transform.ts 更新
fix: merge vue-next transform.ts · gcclll/stb-vue-next@22a8f0b
add
filename
add
isTS, inline
transformElement.ts
fix: merge vue-next transformElement.ts · gcclll/stb-vue-next@1d85990
transformSlotOutlet.ts
fix: merge vue-next transformSlotOutlet.ts · gcclll/stb-vue-next@58e87f3 修改
<slot></slot>
属性解析逻辑。
6efbc86 add expression transform(node-env)
feat: transformExpression -> processExpression · gcclll/stb-vue-next@6efbc86
transformExpression 处理的是复杂表达式情况,且需要在非浏览器环境,因为它需要依赖 一些 JavaScript 解析器来协助完成。
需要处理的两种类型:
INTERPOLATION
插值类型ELEMENT
类型且没有v-for
指令的标签,对于指令需要处理的有arg, exp
如果 参数是动态的情况
两者最终都是调用 processExpression(node, context, asPrarms, asRawStatement)
这
个函数,这个函数有点复杂,需要慢慢消化🐕 🐕 🐕
TODOs(ssr render, node env, setup meta)
在此之前都是基于浏览器去完成和测试的,但是由于后面 compiler-dom 有些测试同样需要
node 环境支持,因此这里必须先完成所有浏览器的支持,这样 prefixIdentifiers
和 cacheHandlers
也将支持。
add generate imports and module mode: feat: gen imports · gcclll/stb-vue-next@ea7f16b
TODO ssr template literal: feat(add): gen template literal · gcclll/stb-vue-next@356bc93
TODO ssr if generate: feat(add): ssr gen if statement · gcclll/stb-vue-next@1c424be
TODO ssr assigment expression generate: 生成赋值表达式 feat(add): ssr gen assignment expression · gcclll/stb-vue-next@1f99a11
TODO ssr generate sequence exprssion: feat(add): ssr gen sequence expression · gcclll/stb-vue-next@81aa0b0
TODO ssr generate return statement: feat(add): ssr gen return statement · gcclll/stb-vue-next@c85406c
TODO ssr v-on transform: feat(add): ssr v-on transform · gcclll/stb-vue-next@adefeec
TODO setup mode v-model on ref: feat(add): setup mode v-model on ref · gcclll/stb-vue-next@20bb40d
TODO process expression in vIf.ts feat(add): v-if process expression · gcclll/stb-vue-next@56c49c7
TODO setup mode ref in transformElement feat(add): transform ref in setup mode in transformElement · gcclll/stb-vue-next@54f467f
non-browser testing(非浏览器环境测试)
本章节测试都是基于 node 环境进行测试的,测试代码位于各个 package 目录下的
__nests__
(node tests 缩写)里面。
由于测试输出结果较多,因此采用控制台形式输出,请 <f12>
打开控制台查看
jest 跑🏃用例
fix: jest errors · gcclll/stb-vue-next@2085dd6
@vue/runtime-dom (guessing 'runtimeDom') created packages/vue/dist/vue.global.js in 5.1s PASS packages/compiler-core/__tests__/transforms/transformElement.spec.ts (9.278 s) PASS packages/compiler-core/__tests__/transforms/vModel.spec.ts PASS packages/compiler-core/__tests__/transforms/vFor.spec.ts PASS packages/compiler-core/__tests__/parse.spec.ts PASS packages/compiler-core/__tests__/transforms/hoistStatic.spec.ts PASS packages/compiler-core/__tests__/codegen.spec.ts PASS packages/compiler-core/__tests__/transforms/vIf.spec.ts PASS packages/compiler-core/__tests__/transforms/vSlot.spec.ts PASS packages/compiler-core/__tests__/transforms/transformExpressions.spec.ts PASS packages/compiler-core/__tests__/transforms/vOn.spec.ts PASS packages/compiler-core/__tests__/transform.spec.ts PASS packages/compiler-core/__tests__/compile.spec.ts PASS packages/compiler-core/__tests__/transforms/transformSlotOutlet.spec.ts PASS packages/compiler-core/__tests__/transforms/transformText.spec.ts PASS packages/compiler-core/__tests__/transforms/vOnce.spec.ts PASS packages/compiler-core/__tests__/scopeId.spec.ts PASS packages/compiler-core/__tests__/transforms/vBind.spec.ts PASS packages/compiler-core/__tests__/transforms/noopDirectiveTransform.spec.ts PASS packages/compiler-core/__tests__/utils.spec.ts Test Suites: 19 passed, 19 total Tests: 480 passed, 480 total Snapshots: 205 passed, 205 total Time: 17.699 s
全部通过测试,期间出现两个小问题:
transformElement 里面解析动态属性(
vnodeDynamicProps
)的时候会将这些属性组成 字符串数组,这里少了个逗号genFunctionExpression()
里面将函数}
错写成了)
综合测试
|
|
>>> 复杂应用场景 const _Vue = Vue const { createVNode: _createVNode, createCommentVNode: _createCommentVNode, createTextVNode: _createTextVNode } = _Vue const _hoisted_1 = { class: "title" } const _hoisted_2 = /*#__PURE__*/_createVNode("p", null, "我是默认插槽的一部分", -1 /* HOISTED */) const _hoisted_3 = /*#__PURE__*/_createVNode("p", null, "我也是默认插槽的一部分", -1 /* HOISTED */) const _hoisted_4 = /*#__PURE__*/_createVNode("p", null, "我也是。。。。。。。", -1 /* HOISTED */) const _hoisted_5 = /*#__PURE__*/_createVNode("p", null, "我们都是。。。。。。", -1 /* HOISTED */) const _hoisted_6 = /*#__PURE__*/_createVNode("template", null, [ /*#__PURE__*/_createVNode("p", null, "我是默认插槽") ], -1 /* HOISTED */) const _hoisted_7 = /*#__PURE__*/_createVNode("template", null, [ /*#__PURE__*/_createCommentVNode(" 我是默认插槽,给你们注释用的!!!!!! ") ], -1 /* HOISTED */) const _hoisted_8 = /*#__PURE__*/_createVNode("p", null, "带 v-if + v-slot + template 的动态插槽,我应该要用到 _createSlots(staticSlots, dynamicSLots) 函数", -1 /* HOISTED */) const _hoisted_9 = /*#__PURE__*/_createVNode("p", null, "带 v-for + v-slot + template 的动态插槽,我应该要用到 _createSlots(staticSlots, dynamicSLots) 函数", -1 /* HOISTED */) return function render(_ctx, _cache) { with (_ctx) { const { createVNode : _createVNode, openBlock : _openBlock, createBlock : _createBlock, createCommentVNode : _createCommentVNode, toHandlers : _toHandlers, mergeProps : _mergeProps, renderList : _renderList, Fragment : _Fragment, toDisplayString : _toDisplayString, createTextVNode : _createTextVNode, resolveComponent : _resolveComponent, withCtx : _withCtx, createSlots : _createSlots } = _Vue const _component_List = _resolveComponent("List") return (_openBlock(), _createBlock(_Fragment, null, [ _createVNode("div", { id: status, class: "status", style: { color: 'red' } }, [ opened ? (_openBlock(), _createBlock("button", { key: 0, onClick: $event => (opened = !opened) }, "关闭", 8 /* PROPS */, ["onClick"])) : invalid ? (_openBlock(), _createBlock("button", _mergeProps({ key: 1 }, _toHandlers({ clicked: toggleValid })), "有效", 16 /* FULL_PROPS */)) : (_openBlock(), _createBlock("button", { key: 2, onClick: () => opened = true }, "打开", 8 /* PROPS */, ["onClick"])), _createVNode("p", { name: vbind }, "v-bind 缩写", 8 /* PROPS */, ["name"]), _createVNode("p", _mergeProps({ class: "vbind-no-arg" }, { name: 'vbind' }), "无参数的 v-bind", 16 /* FULL_PROPS */) ], 12 /* STYLE, PROPS */, ["id"]), _createVNode("div", { id: "list", class: list, style: "color:red;" }, [ _createVNode("ul", null, [ (_openBlock(true), _createBlock(_Fragment, null, _renderList(list, (value, key, index) => { return (_openBlock(), _createBlock("li", null, _toDisplayString(value), 1 /* TEXT */)) )), 256 /* UNKEYED_FRAGMENT */)) ]) ], 2 /* CLASS */), _createVNode("div", { id: "once", class: once }, _toDisplayString(once), 3 /* TEXT, CLASS */), _createVNode("div", { id: "user-comp", class: user-comp }, [ _createCommentVNode(" 我是注释:用户组件 "), _createVNode(_component_List, null, { default: _withCtx(() => [ _createTextVNode(_toDisplayString("我是 <List/> 的默认插槽"), 1 /* TEXT */) ]), _: 1 /* STABLE */ }), _createVNode(_component_List, null, { default: _withCtx(({ prop }) => [ _createVNode("p", _hoisted_1, "我是带 slotProps(" + _toDisplayString(prop) + ") 的默认插槽", 1 /* TEXT */) ]), _: 1 /* STABLE */ }), _createVNode(_component_List, null, { title: _withCtx(({ prop }) => [ _createTextVNode("\"我是带名字" + _toDisplayString(prop) + "的插槽\"", 1 /* TEXT */) ]), _: 1 /* STABLE */ }), _createVNode(_component_List, null, { one: _withCtx((slotProps) => [ _createVNode("p", null, _toDisplayString(slotProps.title), 1 /* TEXT */) ]), default: _withCtx(() => [ _hoisted_2, _hoisted_3, _hoisted_4, _hoisted_5 ]), _: 1 /* STABLE */ }), _createVNode(_component_List, null, { one: _withCtx(({ prop }) => [ _createVNode("p", null, "我是名字为 one 的具名插槽插槽(" + _toDisplayString(prop) + ")", 1 /* TEXT */) ]), default: _withCtx(() => [ _hoisted_6 ]), _: 1 /* STABLE */ }), ok ? (_openBlock(), _createBlock(_component_List, { key: 0 }, { one: _withCtx((slotProps) => [ _createVNode("p", null, "我是带 v-if 指令的且 v-slot 引用在组件上的具名 \"one\" 插槽,标题属性: " + _toDisplayString(slotProps.title), 1 /* TEXT */) ]), _: 1 /* STABLE */ })) : _createCommentVNode("v-if", true), _createVNode(_component_List, null, _createSlots({ default: _withCtx(() => [ _hoisted_7 ]), _: 2 /* DYNAMIC */ }, [ ok ? { name: "one", fn: _withCtx(({ prop }) => [ _hoisted_8 ]) } : undefined, _renderList(list, (value, key, index) => { return { name: "two", fn: _withCtx((slotProps) => [ _hoisted_9 ]) } )) ]), 1024 /* DYNAMIC_SLOTS */) ], 2 /* CLASS */) ], 64 /* STABLE_FRAGMENT */)) } } > 注释 1. v-on 表达式为简单表达式时会被处理成一个箭头函数 2. v-on 如果没有参数时,会触发该组件上的所有属性合并(_mergeProps(...)) 3. v-on 表达式为一个函数式,不需要额外处理 4. v-for 调用 _renderList(list, fn) 渲染列表,list 列表数据来源,fn 列表项渲染函数 5. v-slot 应用在用户组件上时,里面就不能在使用 v-slot 6. 只要在 template 上应用了 v-if/v-for 或者将用户组件放在 v-for 下面这些插槽都会被当做动态插槽处理 undefined
更多测试用例(
<f12>
)打开控制台查看 ->> 。