携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第23天,点击查看活动详情
这次讲一下怎么 transform 几个特殊的指令: v-if
, v-for
v-model
,还有组件
v-for
示例:
<div v-for="(item, index) in items">{{item + index}}</div>
此 div
会根据 items
的数量而去渲染相应数量的 div。而 items
变量来源于 runtime
,因此 v-for
指令不能只靠编译完成,需要 runtime
配合。
编译目标:
h(
Fragment,
null,
renderList(items, (item, index) => h('div', null, item + index))
);
其中 h('div', null, item + index)
就是节点删掉 for
属性后正常编译的结果
实现
// 遍历节点时,代理一层
export function traverseNode(node, parent) {
switch (node.type) {
。。。
case NodeTypes.ELEMENT:
// 替换createElementVNode
return resolveElementASTNode(node, parent);
function resolveElementASTNode(node, parent) {
// 判断是否是v-for节点
const forNode = pluck(node.directives, 'for');
if (forNode) {
const { exp } = forNode;
// 以 in 或者 of 分隔开,得到 v-for="(item, index) in items" 中的 (item, index) 和 items
// renderList(items, (item, index) => h('div', null, item + index))
const [args, source] = exp.content.split(/\sin\s|\sof\s/);
return `h(Fragment, null, renderList(${source.trim()}, ${args.trim()} => ${resolveElementASTNode(
node
)}))`;
}
//如果正常就正常解析咯, <div>{{item + index}}</div> 解析成 h('div', null, item + index)
return createElementVNode(node);
}
// 判断是否是特殊节点,且删除掉这个属性,for属性不像id,不能往下传
function pluck(directives, name, remove = true) {
const index = directives.findIndex((dir) => dir.name === name);
const dir = directives[index];
if (remove && index > -1) {
directives.splice(index, 1);
}
return dir;
}
# runtime
// source在这边能获取到ctx的值,就能得到items的长度,从而知道该分成多少个子节点,用fragment包
export function renderList(source, renderItem) {
const vnodes = [];
if (isNumber(source)) {
// items为数字时,如果是10,则是1到10
for (let i = 0; i < source; i++) {
vnodes.push(renderItem(i + 1, i));
}
// 数组和字符串都是一样
} else if (isString(source) || isArray(source)) {
for (let i = 0; i < source.length; i++) {
vnodes.push(renderItem(source[i], i));
}
} else if (isObject(source)) {
// 对象时,三个参数 value,key,index in items
const keys = Object.keys(source);
keys.forEach((key, index) => {
vnodes.push(renderItem(source[key], key, index));
});
}
return vnodes;
}
v-if
vue-next-template-explorer.netlify.app/
每个 if
语句都包函三个部分:condition
, consequent
, alternate
组成 condition ? consequent : alternate
<div v-if='ok'>Counter</div>
ok? <div>Counter</div> : null
<div v-if='ok'>Counter</div>
——————本地实现
ok ? h('div', null, 'Counter') : h(Text, null, '');
——————源
return (_openBlock(), _createElementBlock(_Fragment, null, [
ok
? (_openBlock(), _createElementBlock("div", { key: 0 }, "Counter"))
: _createCommentVNode("v-if", true), //注释节点
实现单纯 v-if
function resolveElementASTNode(node, parent) {
const ifNode = pluck(node.directives, 'if')
if (ifNode) {
const consequent = resolveElementASTNode(node);
const { exp } = ifNode;
return `${exp.content} ? ${consequent} : ${createTextVNode()}`;
}
v-else
<div v-if='ok'>Counter</div>
<h2 v-else></h2>
——————本地实现
[
ok ? h("h1", null, 'Counter') : h("h2")
]
——————源
ok
? (_openBlock(), _createElementBlock("div", { key: 0 }, "Counter"))
: (_openBlock(), _createElementBlock("h2", { key: 1 })),
还有 其他情况
<h1 v-if="ok"></h1>
<h2 v-else-if="ok2"></h2>
<h3 v-else-if="ok3"></h3>
ok
? h('h1')
: ok2
? h('h2')
: ok3
? h('j3')
: h(Text, null, '');
<h1 v-if="ok"></h1>
<h2 v-else-if="ok2"></h2>
<h3 v-else></h3>
ok
? h('h1')
: ok2
? h('h2')
: h('h3');
实现完整
- 为了知道下个节点是否是
v-else
,需要从父节点的children
里获取,所以改一下,获取到parent
export function traverseNode(node, parent) {
switch (node.type) {
case NodeTypes.ROOT:
if (node.children.length === 1) {
// 这里传入根父节点
return traverseNode(node.children[0], node);
}
const result = traverseChildren(node);
return node.children.length > 1 ? `[${result}]` : result;
case NodeTypes.ELEMENT:
// traverseNode在 traverseChildren 里调用,很容易 拿到 parent 传给他。
return resolveElementASTNode(node, parent);
。。。
}
function resolveElementASTNode(node, parent) {
const ifNode =
pluck(node.directives, 'if') || pluck(node.directives, 'else-if');
if (ifNode) {
// 递归必须用resolveElementASTNode,因为一个元素可能有多个指令
// 所以处理指令时,移除当下指令也是必须的
const consequent = resolveElementASTNode(node, parent);
let alternate;
// 如果有ifNode,则需要看它的下一个元素节点是否有else-if或else
const { children } = parent;
let i = children.findIndex((child) => child === node) + 1;
for (; i < children.length; i++) {
const sibling = children[i];
// <div v-if="ok"/> <p v-else-if="no"/> <span v-else/>
// 为了处理上面的例子,需要将空节点删除
// 也因此,才需要用上for循环
if (sibling.type === NodeTypes.TEXT && !sibling.content.trim()) {
children.splice(i, 1);
i--;
continue;
}
// 下个else节点
if (
sibling.type === NodeTypes.ELEMENT &&
(pluck(sibling.directives, 'else') ||
// else-if 既是上一个条件语句的 alternate,又是新语句的 condition
// 因此pluck时不删除指令,下一次循环时当作ifNode处理,第三个参数false就是不删
// v-else-if的处理和v-if一样,所以不要删了else-if,用if的逻辑去做
pluck(sibling.directives, 'else-if', false))
) {
alternate = resolveElementASTNode(sibling, parent);
// 用完就要删了节点
children.splice(i, 1);
}
// 只用向前寻找一个相临的元素,因此for循环到这里可以立即退出
break;
}
const { exp } = ifNode;
return `${exp.content} ? ${consequent} : ${alternate || createTextVNode()}`;
}
举个例子:
<h1 v-if="ok"></h1> <h2 v-else-if="ok2"></h2> <h3 v-else></h3> ok ? h('h1') : ok2 ? h('h2') : h('h3');
找到
v-if
时,就要去找后面有没有v-else-if
,如果有,用v-if
的逻辑去处理它,处理结果当成ok?a:b
里的b
就行了,说难不难,说简单也不简单
v-if 和 v-for 的优先级
不建议 v-if
和 v-for
一起使用,因为会造成语义混乱,谁应该优先呢?
但它两在一起又确实是合法的。有意思的是 vue2
是 v-for
优先,vue3
是 v-if
优先。
v-model
最早期的 vue
是这样描述 v-model
的:v-model
本质上是一个语法糖。
如下代码
<input v-model="test">
本质上是
<input :value="test" @input="test = $event.target.value">
但现在的 v-model
有着完全不同的实现。它是利用 vue
的自定义指令实现的。
为了图简便,我们沿用以前的设定去实现,直接将 vModel
改为两个指令。
const vModel = pluck(node.directives, 'model');
if (vModel) {
node.directives.push(
{
type: NodeTypes.DIRECTIVE,
name: 'bind',
exp: vModel.exp, // 表达式节点
arg: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'value',
isStatic: true,
}, // 表达式节点
},
{
type: NodeTypes.DIRECTIVE,
name: 'on',
exp: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: `($event) => ${vModel.exp.content} = $event.target.value`,
isStatic: false,
}, // 表达式节点
arg: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'input',
isStatic: true,
}, // 表达式节点
}
);
}
但是这种方法只对 input
凑效。v-model
有着非常多的使用场合,每种场合效果也不尽相同,甚至还能用于组件上,因此完整的实现还是很麻烦的。还有 radio
, checkbox
的。
比如
<input v-model="test" @input='foo'>
这样按照我们的写法会出现两个input事件冲突
不展开了
组件的引入
组件的使用方法:需在 createApp
里申明 components
(只支持全局组件)
createApp({
components: { TreeItem },
});
————使用
<ul id="demo">
<tree-item class="item" :model="treeData"></tree-item>
</ul>
const TreeItem = {
template: '#item-template',
。。。
}
<script type="text/x-template" id="item-template">。。。</script>
在 runtime
里使用 resolveComponent
配合,
- codegen.js
得到 h(resolveComponent(TreeItem))
function createElementVNode(node) {
const { children, directives } = node;
const tag =
node.tagType === ElementTypes.ELEMENT
? `"${node.tag}"`
: `resolveComponent("${node.tag}")`;
...
return `h(${tag}, ${propStr}, ${childrenStr})`;
}
- createApp.js
let components;
export function createApp(rootComponent) {
// 拿到{ Foo },
components = rootComponent.components || {};
...
}
export function resolveComponent(name) {
return (
components &&
(components[name] ||
components[camelize(name)] ||
components[capitalize(camelize(name))])
);
// tree-item,treeItem,TreeItem 如果组件名 tree-item找不到就找 treeItem,TreeItem
}
得到
h({
template: '#item-template',
。。。
})
怎么解析看这个 :juejin.cn/post/713576…