上一篇Vue3源码编译器compile基础已经处理了编译器的基础情况,可以把模板编译成render函数
从而进行渲染
但是对于响应性数据、多层级节点、指令(v-if
等),目前还没有做处理
响应性数据的编译
响应性数据在Vue中用的是插值表达式{{}}
,所以我们需要识别出这一部分,总共也是走三个步骤
- 生成
AST
对象 AST
对象转换为JavaScript AST
(添加codegen
属性)- 拼接生成
render
函数字符串
AST对象生成
响应性数据的AST对象生成主要就是识别出插值表达式,这一部分之前在parseChildren
中留了一个位置
function parseChildren(context: ParserContext, ancestors) {
const nodes = [];
while (!isEnd(context, ancestors)) {
const s = context.source;
let node;
if (startsWith(s, "{{")) {
// 模板语法
node = parseInterpolation(context);
} else if (s[0] === "<") {
......
}
......
}
return nodes;
}
parseInterpolation
这个方法主要就是取出插值表达式大括号内的变量名字,并让光标右移
function parseInterpolation(context: ParserContext) {
// 模板表达式以{{ XX }}格式呈现
const [open, close] = ["{{", "}}"];
// 光标先右移到开始位置之后
advanceBy(context, open.length);
// 获取到变量名,并去除前后的空格
const closeIndex = context.source.indexOf(close, open.length);
const preTrimContext = parseTextData(context, closeIndex);
const content = preTrimContext.trim();
// 光标右移到插值表达式之后
advanceBy(context, close.length);
return {
type: NodeTypes.INTERPOLATION,
content: {
type: NodeTypes.SIMPLE_EXPRESSION,
isStatic: false,
content,
},
};
}
AST对象转换成JavaScript AST
之前我们处理文本节点的时候,已经埋好了插值表达式和普通文本的拼接逻辑
现在需要做的,是让最后拼接render
函数字符串的时候,能给插值表达式添加上_toDisplayString
方法
这样一来,做法就明确了,直接在traverseNode
中给插值表达式这种情况添加helper
方法即可
function traverseNode(node, context: TransformContext) {
......
switch (node.type) {
// 处理子节点
case NodeTypes.ELEMENT:
case NodeTypes.ROOT:
traverseChildren(node, context);
break;
// 插值表达式处理:添加上toDisplayString方法
case NodeTypes.INTERPOLATION:
context.helper(TO_DISPLAY_STRING);
break;
}
......
}
render函数字符串拼接
可以先观察Vue3源码中最后拼接出来的render
函数字符串
const _Vue = Vue
return function render(_ctx, _cache) {
with(_ctx) {
const { toDisplayString: _toDisplayString, createElementBlock: _createElementBlock } = _Vue
return _createElementBlock("div", null, "hello " + _toDisplayString(msg))
}
}
和之前已经完成的部分,差异有三
render
函数中有一个with
函数包裹- 添加了
_toDisplayString
方法 - 子节点的
_toDisplayString
方法中的变量名是msg
(with
的作用使得方法可以找到msg
)
所以这里的处理就围绕这三点差异展开
with方法的包裹
with可以扩展一个语句的作用域链,但不推荐使用
这个处理起来比较简单,直接在拼接方法中添加上with
就行
function generate(ast) {
......
push(`with (_ctx) {`);
indent();
......
// 结束后要退回缩进并再添加一个结束的括号
deindent();
push("}");
return {
ast,
code: context.code,
};
}
但是在实际使用最后生成的render
函数的时候,会有一个问题:因为msg
是_ctx
的msg
,但是现在_ctx
还没有值,所以我们在render
函数中应该补充传入data
作为_ctx
function renderComponentRoot(instance) {
// 给data添加默认值
const { vnode, render, data = {} } = instance;
let result;
try {
if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
// 第一个参数改this,第二个参数是为了ctx改变作用域的
result = normalizeVNode(render!.call(data, data));
}
} catch (err) {
console.error(err);
}
return result;
}
toDisplayString方法的添加
这里需要区分几种情况
- 简单表达式(插值表达式的变量名)
- 插值表达式
- 复合表达式(插值表达式+文本的组合)
这几种情况的区分放在genNode
里面
function genNode(node, context) {
switch (node.type) {
......
// 简单表达式(插值表达式的变量名)
case NodeTypes.SIMPLE_EXPRESSION:
genExpression(node, context);
break;
// 插值表达式
case NodeTypes.INTERPOLATION:
genInterpolation(node, context);
break;
// 复合表达式(即简单+插值)
case NodeTypes.COMPOUND_EXPRESSION:
genCompoundExpression(node, context);
break;
}
}
简单表达式的处理
用的genExpression
处理,其实就是直接推入render
函数字符串
function genExpression(node, context) {
const { content, isStatic } = node;
context.push(isStatic ? JSON.stringify(content) : content);
}
插值表达式的处理
插值表达式用了genInterpolation
处理,核心就是把变量名用_toDisplayString
给包裹一下
至于里面的变量名用genNode
递归,交给genExpression
处理
function genInterpolation(node, context) {
const { push, helper } = context;
push(`${helper(TO_DISPLAY_STRING)}(`);
genNode(node.content, context);
push(`)`);
}
复合表达式的处理
复合表达式处理用的genCompoundExpression
,核心就是把其中的子节点再挨个判断,递归到genNode
去判断(文本/插值)
function genCompoundExpression(node, context) {
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i];
// +,直接推入
if (isString(child)) {
context.push(child);
}
// 文本/插值表达式的处理在genNode中,需要递归
else {
genNode(child, context);
}
}
}
多层级节点的编译
多层级节点指的就是节点嵌套,例如下面这个
<div>
<h1>hello world</h1>
</div>
这种嵌套节点编译器出来的render
函数应该也是嵌套的
处理思路也简单,只要在逻辑上保证让这种类型的节点可以递归处理就好了
function genNode(node, context) {
switch (node.type) {
......
// 多层级节点
case NodeTypes.ELEMENT:
genNode(node.codegenNode, context);
break;
}
}
指令的编译
指令是Vue中的特色,Vue的内部封装好了若干指令,例如v-if
、v-for
、v-on
等等
而对于指令的编译,其实也是要经历核心的三个步骤,即:
AST
生成AST
转换成JavaScript AST
render
函数拼接
阅读源码可以知道,v-if
指令其实是在render
函数中创建了一个三元表达式,动态根据v-if
的变量的值,决定是渲染v-if
节点,还是一个v-if
注释节点
(插入源码的分析,即render函数)
下面以v-if
的指令编译为例,说明Vue中的指令编译过程
指令的AST生成
我们知道,v-if
是写在标签之后,像属性一样用空格分隔开
<div v-if="test">hello world</div>
所以对它的解析其实也是对属性的一种解析,只是我们要正确识别出v-if
指令即可
属性的解析是在标签之后的,所以放在了标签解析parseTag
方法里面
function parseTag(context: ParserContext, type: TagType) {
......
// 根据tag的名称长度,右移source位置(<+tag名字)
advanceBy(context, match[0].length);
// 属性和指令的处理
// 先把tag和属性之间的空格给去掉,光标右移到属性开始的位置
advanceSpaces(context);
// 对属性做解析
let props = parseAttributes(context, type);
......
return {
type: NodeTypes.ELEMENT,
tag,
tagType: ElementTypes.ELEMENT,
props,
children: [],
};
}
// 把tag和属性之间的空格去掉,右移到属性开始的位置
function advanceSpaces(context: ParserContext): void {
const match = /^[\t\r\n\f ]+/.exec(context.source);
if (match) {
advanceBy(context, match[0].length);
}
}
属性的解析
对于属性来说,都是密密麻麻一串放在tag
标签名字之后的,我们借着指令的识别,把所有的属性都给识别出来,同时用特定的正则去单独处理指令
属性列表的解析处理用parseAttributes
方法,核心就是循环识别各个属性,拿到属性名和属性值,存放起来最后一并挂在节点的props
属性中
function parseAttributes(context, type) {
const props: any = [];
// 属性名的存放
const attributeNames = new Set<string>();
// 当源码长度不为0,且剩下的内容不是标签结束的时候,说明还有属性,要循环处理
while (
context.source.length > 0 &&
!startsWith(context.source, ">") &&
!startsWith(context.source, "/>")
) {
// 拿到属性名字和值的AST
const attr = parseAttribute(context, attributeNames);
// 只有是开始标签,才去存放属性
if (type === TagType.Start) {
props.push(attr);
}
// 两个属性之间有空格,要右移到下一个属性/结尾位置
advanceSpaces(context);
}
return props;
}
单个属性用parseAttribute
做解析,基本逻辑就是识别出属性名和属性值,对于v-
指令和普通属性分开处理(用正则识别出来v-
指令)
function parseAttribute(context: ParserContext, nameSet: Set<string>) {
// 拿到属性名
const match = /^[^\t\r\n\f />][^\t\r\n\f />=]*/.exec(context.source)!;
const name = match[0];
nameSet.add(name);
// 右移继续准备拿到属性值
advanceBy(context, name.length);
let value: any = undefined;
// 如果能有=出现,后面就是属性值了
if (/^[^\t\r\n\f ]*=/.test(context.source)) {
advanceSpaces(context);
advanceBy(context, 1);
advanceSpaces(context);
// 获取属性值
value = parseAttributeValue(context);
}
// v-指令处理
if (/^(v-[A-Za-z0-9-]|:|\.|@|#)/.test(name)) {
// 拿到v-指令的名字(例如if/for等等)
const match =
/(?:^v-([a-z0-9-]+))?(?:(?::|^\.|^@|^#)(\[[^\]]+\]|[^\.]+))?(.+)?$/i.exec(
name
)!;
let dirName = match[1];
return {
type: NodeTypes.DIRECTIVE,
name: dirName,
// 指令绑定的值
exp: value && {
type: NodeTypes.SIMPLE_EXPRESSION,
content: value.content,
isStatic: false,
loc: {},
},
art: undefined,
modifiers: undefined,
loc: {},
};
}
// 普通属性处理
return {
type: NodeTypes.ATTRIBUTE,
name,
value: value && {
type: NodeTypes.TEXT,
content: value.content,
loc: {},
},
loc: {},
};
}
// 单个属性值的处理
function parseAttributeValue(context: ParserContext) {
// 属性内容
let content = "";
// 第一位是引号(单双不确定,所以要拿到它,后面对应去匹配)
const quote = context.source[0];
// 右移引号宽度
advanceBy(context, 1);
const endIndex = context.source.indexOf(quote);
// 没有找到结束引号,则后面内容都是属性值
if (endIndex === -1) {
content = parseTextData(context, context.source.length);
} else {
content = parseTextData(context, endIndex);
// 右移引号宽度
advanceBy(context, 1);
}
return { content, isQuoted: true, loc: {} };
}
指令AST到JavaScript AST的转化
JavaScript AST
主要还是codegenNode
属性的变化
首先,封装一个通用处理指令逻辑的createStructuralDirectiveTransform
方法,这样后续所有指令可以通过这个方法,匹配指令名后,再返回对应的处理逻辑
function createStructuralDirectiveTransform(name: string | RegExp, fn) {
// 检测指令是否匹配
const matches = isString(name)
? (n: string) => n === name
: (n: string) => name.test(n);
return (node, context) => {
// 因为所有的指令都绑定在ELEMENT节点上,所以只处理ELEMENT节点
if (node.type === NodeTypes.ELEMENT) {
const { props } = node;
const exitFns: any = [];
// 因为指令实际存在属性中,因此遍历属性,找到其中的指令
for (let i = 0; i < props.length; i++) {
const prop = props[i];
if (prop.type === NodeTypes.DIRECTIVE && matches(prop.name)) {
// 删除这个属性(因为它本来就不是属性,而是因为位置在属性上)
props.splice(i, 1);
i--;
// 执行传入的方法,当有返回值的时候,推入这个exitFns列表(traverseNode的逻辑)
const onExit = fn(node, prop, context);
if (onExit) exitFns.push(onExit);
}
}
return exitFns;
}
};
}
封装了指令通用的处理方法后,接下来写一个v-if
指令专门的处理方法transformIf
其中,第二个参数又被封装了一层,而这个参数实际上是traverseNode
中的onExit
函数,将会在最后处理v-if
指令创建codegenNode
属性
const transformIf = createStructuralDirectiveTransform(
/^(if|else|else-if)$/,
// 第二个onExit的参数如下
// 第一个参数是node自己
// 第二个参数实际上是v-if的prop属性
// 第三个参数是上下文
// 第四个参数是一个匿名函数,真实目的是往这个ifNode上面创建codegenNode属性
(node, dir, context) => {
return processIf(node, dir, context, (ifNode, branch, isRoot) => {
let key = 0;
return () => {
if (isRoot) {
ifNode.codegenNode = createCodegenNodeForBranch(branch, key, context);
}
};
});
}
);
function processIf(
node,
dir,
context: TransformContext,
processCodegen?: (node, branch, isRoot: boolean) => () => void
) {
// 只有if指令才处理
if (dir.name === "if") {
// 创建一个branch属性
const branch = createIfBranch(node, dir);
const ifNode = {
type: NodeTypes.IF,
loc: {},
branches: [branch],
};
// 上下文指向的node改成当前的ifNode
context.replaceNode(ifNode);
if (processCodegen) {
return processCodegen(ifNode, branch, true);
}
}
}
function createIfBranch(node, dir) {
return {
type: NodeTypes.IF_BRANCH,
loc: {},
condition: dir.exp,
children: [node],
};
}
processCodegen
就是针对v-if
预创建的codegenNode
属性,目的就是在最后拼接render
函数字符串的时候,如果有条件语句,根据条件语句创建三元表达式
function createCodegenNodeForBranch(
branch,
keyIndex,
context: TransformContext
) {
// 如果v-if后面有条件,要根据条件创建三元表达式
if (branch.condition) {
return createConditionalExpression(
branch.condition,
createChildrenCodegenNode(branch, keyIndex),
createCallExpression(context.helper(CREATE_COMMENT), ['"v-if"', "true"])
);
}
// 没有条件,则只针对子节点创建codegenNode
else {
return createChildrenCodegenNode(branch, keyIndex);
}
}
function createConditionalExpression(
test,
consquent,
alternate,
newline = true
) {
return {
type: NodeTypes.JS_CONDITIONAL_EXPRESSION,
test,
consquent,
alternate,
newline,
loc: {},
};
}
// 创建子节点的codegenNode属性
function createChildrenCodegenNode(branch, keyIndex: number) {
const keyProperty = createObjectProperty(
`key`,
createSimpleExpression(`${keyIndex}`, false)
);
const { children } = branch;
const firstChild = children[0];
const ret = firstChild.codegenNode;
// 这个vnodeCall这里比较简单,就是node本身
const vnodeCall = getMemoedVNodeCall(ret);
injectProp(vnodeCall, keyProperty);
return ret;
}
// 把属性注入到node的props中
function injectProp(node, prop) {
let propsWithInjection;
let props =
node.type === NodeTypes.VNODE_CALL ? node.props : node.arguments[2];
if (props === null || isString(props)) {
propsWithInjection = createObjectExpression([prop]);
}
if (node.type === NodeTypes.VNODE_CALL) {
node.props = propsWithInjection;
}
}
function createObjectExpression(properties) {
return {
type: NodeTypes.JS_OBJECT_EXPRESSION,
loc: {},
properties,
};
}
处理完这些,最后在traverseNode
方法中,添加对于if
类型节点的处理逻辑
function traverseNode(node, context: TransformContext) {
......
switch (node.type) {
// 处理子节点
case NodeTypes.IF_BRANCH:
case NodeTypes.ELEMENT:
case NodeTypes.ROOT:
traverseChildren(node, context);
break;
......
case NodeTypes.IF:
for (let i = 0; i < node.branches.length; i++) {
traverseNode(node.branches[i], context);
}
break;
}
......
}
render函数字符串拼接
最后render
函数的拼接核心就是拼接三元表达式了
- 如果满足条件,渲染
- 不满足条件,挂载一个
v-if
的注释节点上去
const _Vue = Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { createElementVNode: _createElementVNode, createCommentVNode: _createCommentVNode } = _Vue
return _createElementVNode("div", [], ["hello ", isShow
? _createElementVNode("h1", null, ["world"])
: _createCommentVNode("v-if", true)
])
}
}
三元表达式的部分主要是由genConditionalExpression
处理的
此外,对于v-if
判定为false
的情况,渲染注释节点的逻辑由genCallExpression
处理
function genConditionalExpression(node, context) {
const { test, alternate, consequent, newline: needNewLine } = node;
const { push, newline, indent, deindent } = context;
// 添加v-if的条件值
if (test.type === NodeTypes.SIMPLE_EXPRESSION) {
genExpression(test, context);
}
needNewLine && indent();
context.indentLevel++;
// 问号(准备填充v-if判定为true时候的渲染内容)
needNewLine || push(` `);
push(`? `);
// v-if为true的时候的展示内容
genNode(consequent, context);
context.indentLevel--;
needNewLine && newline();
needNewLine || push(` `);
// 冒号(准备填充v-if判定false时候的渲染内容)
push(`: `);
const isNested = alternate.type === NodeTypes.JS_CONDITIONAL_EXPRESSION;
if (!isNested) {
context.indentLevel++;
}
// v-if为false的时候的处理内容(渲染v-if的注释节点)
genNode(alternate, context);
if (!isNested) {
context.indentLevel--;
}
needNewLine && deindent(true);
}
// 用来处理v-if判定是false时候的渲染(执行方法的方法名+参数)
function genCallExpression(node, context) {
const { push, helper } = context;
const callee = isString(node.callee) ? node.callee : helper(node.callee);
push(callee + `(`, node);
genNodeList(node.arguments, context);
push(`)`);
}
另外需要补充一下创建注释节点的方法createCommentVNode
,并在全局导出以供render
函数识别使用
function createCommentVNode(text) {
return createVNode(Comment, null, text);
}
// 类似于createVNode方法,封装Symbol做映射
export const CREATE_COMMENT = Symbol("createCommentVNode");
export const helperNameMap = {
......
[CREATE_COMMENT]: "createCommentVNode",
};
最后,要在genNode
上添加两种情况的处理,分别是JS调用表达式(v-if
为false
时的渲染内容)以及JS条件表达式(渲染三元表达式)
function genNode(node, context) {
switch (node.type) {
......
// JS调用表达式(用来渲染v-if为false时候的内容)
case NodeTypes.JS_CALL_EXPRESSION:
genCallExpression(node, context);
break;
// JS的条件表达式(用来渲染三元表达式)
case NodeTypes.JS_CONDITIONAL_EXPRESSION:
genConditionalExpression(node, context);
break;
}
}