不能手写Vue3模板编译的你,再强大也是假的!
手写 Vue3 编译原理
一.Vue中模板编译原理
Vue中对
template
属性会编译成render
方法。vue-next
源码可以直接运行命令实现在线调试。
npm run dev-compiler
二.模板编译步骤
export function baseCompile(template) {
// 1.生成ast语法树
const ast = baseParse(template);
// 2.转化ast语法树
transform(ast)
// 3.根据ast生成代码
return generate(ast);
}
三.生成AST
语法树
创建解析上下文,开始进行解析
function baseParse(content) {
// 创建解析上下文,在整个解析过程中会修改对应信息
const context = createParserContext(content);
// 解析代码
return parseChildren(context);
}
function createParserContext(content) {
return {
column: 1, // 列数
line: 1, // 行数
offset: 0, // 偏移字符数
originalSource: content, // 原文本不会变
source: content // 解析的文本 -> 不停的减少
}
}
对不同内容类型进行解析
解析节点的类型有:
export const enum NodeTypes {
ROOT,
ElEMENT,
TEXT,
SIMPLE_EXPRESSION = 4,
INTERPOLATION = 5,
ATTRIBUTE = 6,
DIRECTIVE = 7,
COMPOUND_EXPRESSION = 8,
TEXT_CALL = 12,
VNODE_CALL = 13,
JS_CALL_EXPRESSION = 17
}
function isEnd(context) {
const s = context.source; // 字符串解析完毕就结束
return !s;
}
function parseChildren(context){
const nodes = [];
while (!isEnd(context)) {
let node; // 解析节点
const s = context.source;
if (s.startsWith('{{')) { // 解析双括号
node = parseInterpolation(context);
} else if (s[0] == '<') { // 解析标签
node = parseElement(context);
} else { // 文本
node = parseText(context);
}
nodes.push(node);
}
return nodes
}
1.解析文本
文本可能是
我是文本
、
我是文本
function parseText(context) {
const endTokens = ['<', '{{']; // 当遇到 < 或者 {{ 说明文本结束
let endIndex = context.source.length;
for (let i = 0; i < endTokens.length; i++) {
const index = context.source.indexOf(endTokens[i], 1);
if (index !== -1 && endIndex > index) { // 找到离着最近的 < 或者 {{
endIndex = index
}
}
const start = getCursor(context); // 开始
const content = parseTextData(context, endIndex); // 获取文本内容
return {
type: NodeTypes.TEXT, // 文本
content,
loc: getSelection(context, start)
}
}
用于获取当前解析的位置
function getCursor(context) { // 获取当前位置信息
const { column, line, offset } = context;
return { column, line, offset };
}
function parseTextData(context, endIndex) { // 截取文本部分,并删除文本
const rawText = context.source.slice(0, endIndex);
advanceBy(context, endIndex);
return rawText;
}
将解析的部分移除掉,并且更新上下文信息
function advanceBy(context, index) {
let s = context.source
advancePositionWithMutation(context, s, index)
context.source = s.slice(index); // 将文本部分移除掉
}
const advancePositionWithMutation = (context, source, index) => {
let linesCount = 0
let lastNewLinePos = -1;
for (let i = 0; i < index; i++) {
if (source.charCodeAt(i) == 10) {
linesCount++; // 计算走了多少行
lastNewLinePos = i; // 记录换行的首个位置
}
}
context.offset += index; // 更新偏移量
context.line += linesCount; // 更新行号
context.column = lastNewLinePos === -1 ? context.column + index : index - lastNewLinePos
}
解析结果:
const {baseCompile} = VueCompilerDOM;
console.log(baseCompile(`hello`))
content: "hello"
loc:
end: {column: 5, line: 1, offset: 4}
source: "hello"
start: {column: 1, line: 1, offset: 0}
type: 2
2.解析表达式
获取花括号中的内容
function parseInterpolation(context) {
const closeIndex = context.source.indexOf('}}', '{{');
const start = getCursor(context);
advanceBy(context, 2);
const innerStart = getCursor(context);// 获取内部的开始
const innerEnd = getCursor(context);
const rawContentLength = closeIndex - 2; // 内容结束位置
// 去空格前的内容 和 去空格后的内容
const preTrimContent = parseTextData(context, rawContentLength);
const content = preTrimContent.trim();
const startOffset = preTrimContent.indexOf(content);
if (startOffset > 0) { // 根据标签开始位置修改innerStart
advancePositionWithMutation(innerStart, preTrimContent, startOffset)
}
const endOffset = content.length + startOffset;
// 根据标签结束位置修改innerStart
advancePositionWithMutation(innerEnd, preTrimContent, endOffset);
advanceBy(context, 2);
return {
type: NodeTypes.INTERPOLATION,
content: {
type: NodeTypes.SIMPLE_EXPRESSION,
isStatic: false,
loc: getSelection(context, innerStart, innerEnd)
},
loc: getSelection(context, start)
}
}
解析结果:
const { baseCompile } = VueCompilerDOM;
console.log(baseCompile(`{{ name }}`))
content:
isStatic: false
loc: {start: {…}, end: {…}, source: "name"}
type: 4
loc:
end: {column: 13, line: 1, offset: 12}
source: "{{ name }}"
start: {column: 1, line: 1, offset: 0}
type: 5
3.解析元素
获取标签名属性
function isEnd(context) {
const s = context.source;
if (s.startsWith('</')) { // 遇到闭合标签
return true;
}
return !s;
}
}
function parseElement(context) {
const element: any = parseTag(context);
const children = parseChildren(context); // 11.解析儿子 最后
if (context.source.startsWith('</')) {
parseTag(context)
}
element.children = children
element.loc = getSelection(context,element.loc.start)
return element
}
function advanceSpaces(context) {
const match = /^[\t\r\n\f ]+/.exec(context.source)
if (match) {
advanceBy(context, match[0].length)
}
}
function parseTag(context) {
const start = getCursor(context);
const match = /^</?([a-z][^\t\r\n\f />]*)/i.exec(context.source)!
const tag = match[1];
advanceBy(context, match[0].length);
advanceSpaces(context);
let isSelfClosing = context.source.startsWith('/>');
advanceBy(context,isSelfClosing?2:1);
return {
type:NodeTypes.ElEMENT,
tag,
isSelfClosing,
loc:getSelection(context,start)
}
}
解析结果:
const { baseCompile } = VueCompilerDOM;
console.log(baseCompile(`<div><p></p></div>`))
children: Array(1)
children: []
isSelfClosing: false
loc: {start: {…}, end: {…}, source: "<p></p>"}
tag: "p"
type: 1
isSelfClosing: false
loc: {start: {…}, end: {…}, source: "<div><p></p></div>"}
tag: "div"
type: 1
4.解析属性
在开始标签解析完毕后解析属性
function parseTag(context) {
const start = getCursor(context); // 获取开始位置
const match = /^</?([a-z][^\t\r\n\f />]*)/i.exec(context.source)!
const tag = match[1];
advanceBy(context, match[0].length);
advanceSpaces(context)
let props = parseAttributes(context);
// ...
return {
type: NodeTypes.ElEMENT,
tag,
isSelfClosing,
loc: getSelection(context, start),
props
}
}
function parseAttributes(context) {
const props: any = [];
while (context.source.length > 0 && !startsWith(context.source, '>')) {
const attr = parseAttribute(context)
props.push(attr);
advanceSpaces(context); // 解析一个去空格一个
}
return props
}
function parseAttribute(context) {
const start = getCursor(context);
const match = /^[^\t\r\n\f />][^\t\r\n\f />=]*/.exec(context.source)!
const name = match[0];
advanceBy(context, name.length);
let value
if (/^[\t\r\n\f ]*=/.test(context.source)) {
advanceSpaces(context);
advanceBy(context, 1);
advanceSpaces(context);
value = parseAttributeValue(context);
}
const loc = getSelection(context, start)
if (/^(:|@)/.test(name)) { // :xxx @click
let dirName = name.slice(1)
return {
type: NodeTypes.DIRECTIVE,
name: dirName,
exp: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: value.content,
isStatic: false,
loc: value.loc
},
loc
}
}
return {
type: NodeTypes.ATTRIBUTE,
name,
value: {
type: NodeTypes.TEXT,
content: value.content,
loc: value.loc
},
loc
}
}
function parseAttributeValue(context) {
const start = getCursor(context);
const quote = context.source[0];
let content
const isQuoteed = quote === '"' || quote === "'"; // 解析引号中间的值
if (isQuoteed) {
advanceBy(context, 1);
const endIndex = context.source.indexOf(quote);
content = parseTextData(context, endIndex);
advanceBy(context, 1);
}
return { content, loc: getSelection(context, start) }
}
对文本节点稍做处理
function parseChildren(context) {
const nodes: any = [];
while (!isEnd(context)) {
//....
}
for(let i = 0 ;i < nodes.length; i++){
const node = nodes[i];
if(node.type == NodeTypes.TEXT){ // 如果是文本 删除空白文本,其他的空格变为一个
if(!/[^\t\r\n\f ]/.test(node.content)){
nodes[i] = null
}else{
node.content = node.content.replace(/[\t\r\n\f ]+/g, ' ')
}
}
}
return nodes.filter(Boolean)
}
5.处理多个根节点
export function baseParse(content) {
// 创建解析上下文,在整个解析过程中会修改对应信息
const context = createParserContext(content);
// 解析代码
const start = getCursor(context);
return createRoot(
parseChildren(context),
getSelection(context,start)
)
}
将解析出的节点,再次进行包裹
ast.ts
export function createRoot(children,loc){
return {
type:NodeTypes.ROOT,
children,
loc
}
}
transform实现
定义转化标识
export const CREATE_VNODE = Symbol('createVnode');
export const TO_DISPALY_STRING = Symbol('toDisplayString');
export const OPEN_BLOCK = Symbol('openBlock');
export const CREATE_BLOCK = Symbol('createBlock')
export const FRAGMENT = Symbol('Fragment');
export const CREATE_TEXT = Symbol('createTextVNode');
export const helperNameMap: any = {
[FRAGMENT]: `Fragment`,
[OPEN_BLOCK]: `openBlock`,
[CREATE_BLOCK]: `createBlock`,
[CREATE_VNODE]: `createVNode`,
[TO_DISPALY_STRING]: "toDisplayString",
[CREATE_TEXT]: "createTextVNode"
}
对AST语法树进行转化,主要是对AST语法树进行优化操作
export function baseCompile(template) {
// 1.生成ast语法树
const ast = baseParse(template);
// 得到对应的转化方法 元素转化、文本转化... 还有指令转化都应该在这里实现
const nodeTransforms = getBaseTransformPreset();
transform(ast, { nodeTransform })
}
function transformElement(node,context) {
// 转化标签 需要处理他的子节点,所以需要等待子节点遍历完成在处理
if(!(node.type === 1)){ // 不是元素就不必执行了
return;
}
console.log('转化元素',node)
}
function transformText(node,context) {
// 处理文本 需要处理他的同级 表达式/文本,所以需要处理完同级后在处理
if (node.type === NodeTypes.ROOT || node.type === NodeTypes.ElEMENT) {
console.log('内部可能包含文本', node);
}
}
function getBaseTransformPreset() {
return [
transformElement,
transformText
]// ...指令转化
}
开始进行转化,会先创建转化上下文,之后遍历ast树
function createTransformContext(root, { nodeTransforms }) {
const context = {
root, // 转化的完整ast
currentNode: root, // 当前转化的节点
nodeTransforms, // 转化方法
helpers: new Set(), // 收集导入的包
helper(name) {
context.helpers.add(name);
return name;
}
}
return context;
}
function transform(root, options) {
const context = createTransformContext(root, options);
traverseNode(root, context)
}
深度遍历节点,调用transform函数
function traverseNode(node, context) { // 遍历树
context.currentNode = node;
const { nodeTransforms } = context;
for (let i = 0; i < nodeTransforms.length; i++) {
nodeTransforms[i](node, context);
}
switch (node.type) {
case NodeTypes.ROOT:
case NodeTypes.ElEMENT:
traverseChildren(node, context); // 递归遍历子节点
break;
}
}
function traverseChildren(parent, context) {
for (let i = 0; i < parent.children.length; i++) {
const child = parent.children[i];
traverseNode(child, context); // 遍历节点
}
}
1.退出函数
返回一个函数等递归完成后在执行
function transformElement(node,context) {
// 转化标签 需要处理他的子节点,所以需要等待子节点遍历完成在处理
if(!(node.type === 1)){ // 不是元素就不必执行了
return;
}
return ()=>{
console.log('转化元素',node)
}
}
function transformText(node,context) {
// 处理文本 需要处理他的同级 表达式/文本,所以需要处理完同级后在处理
if (node.type === NodeTypes.ROOT || node.type === NodeTypes.ElEMENT) {
return ()=>{
console.log('内部可能包含文本', node);
}
}
}
function traverseNode(node, context) { // 遍历树
context.currentNode = node;
const { nodeTransforms } = context;
const exitFns = [];
for (let i = 0; i < nodeTransforms.length; i++) {
const onExit = nodeTransforms[i](node, context);
if (onExit) exitFns.push(onExit)
}
switch (node.type) {
case NodeTypes.ROOT:
case NodeTypes.ElEMENT:
traverseChildren(node, context);
break;
}
let i = exitFns.length;
context.currentNode = node; // 保证退出方法的context是正确的
while (i--) {
exitFns[i]()
}
}
2.文本转化
export const enum PatchFlags {
TEXT = 1,
CLASS = 1 << 1,
STYLE = 1 << 2,
PROPS = 1 << 3,
FULL_PROPS = 1 << 4,
HYDRATE_EVENTS = 1 << 5,
STABLE_FRAGMENT = 1 << 6,
KEYED_FRAGMENT = 1 << 7,
UNKEYED_FRAGMENT = 1 << 8,
NEED_PATCH = 1 << 9,
DYNAMIC_SLOTS = 1 << 10,
DEV_ROOT_FRAGMENT = 1 << 11,
HOISTED = -1,
BAIL = -2
}
function isText(node) {
return node.type == NodeTypes.INTERPOLATION || node.type == NodeTypes.TEXT;
}
export function createCallExpression(callee,args) {
return {
type: NodeTypes.JS_CALL_EXPRESSION,
callee,
arguments: args
}
}
function transformText(node, context) { // 转化文本 核心就是相邻的合并
if (node.type === NodeTypes.ROOT || node.type === NodeTypes.ElEMENT) {
return () => {
// 遍历的是元素,儿子可能是文本。 这里就对标签内的儿子做处理
let hasText = false;
const children = node.children;
let currentContainer = undefined // 合并儿子
for (let i = 0; i < children.length; i++) {
let child = children[i];
if (isText(child)) {
hasText = true;
for (let j = i + 1; j < children.length; j++) {
const next = children[j];
if(isText(next)){
if(!currentContainer){
currentContainer = children[i] = { // 合并表达式
type:NodeTypes.COMPOUND_EXPRESSION,
loc:child.loc,
children:[child]
}
}
currentContainer.children.push(` + `,next);
children.splice(j,1);
j--;
}else{
currentContainer = undefined;
break;
}
}
}
}
if (!hasText || children.length == 1) { // 一个元素不用管,可以执行innerHTML
return
}
for (let i = 0; i < children.length; i++) {
const child = children[i]
if (isText(child) || child.type === NodeTypes.COMPOUND_EXPRESSION) {
const callArgs = []
callArgs.push(child)
if (child.type !== NodeTypes.TEXT) {
callArgs.push(PatchFlags.TEXT + '')
}
children[i] = {
type: NodeTypes.TEXT_CALL,
content: child,
loc: child.loc,
codegenNode: createCallExpression(
context.helper(CREATE_TEXT),
callArgs
)
}
}
}
}
}
}
switch (node.type) {
case NodeTypes.ROOT:
case NodeTypes.ElEMENT:
traverseChildren(node, context);
case NodeTypes.INTERPOLATION: // 给表达式新增导入方法
context.helper(TO_DISPALY_STRING);
break;
}
3.元素转化
function createVNodeCall(context, tag, props, children, patchFlag) {
context.helper(CREATE_VNODE);
return {
type: NodeTypes.VNODE_CALL,
tag,
props,
children,
patchFlag
}
}
function transformElement(node, context) {
if (!(node.type === 1)) {
return;
}
return () => { // 对元素的处理
const { tag, children } = node;
const vnodeTag = `"${tag}"`; // 标签名
let vnodeProps;
let vnodeChildren;
let vnodePatchFlag;
let patchFlag = 0;
if (node.children.length > 0) {
if (node.children.length === 1) {
const child = node.children[0];
const type = child.type;
const hasDynamicTextChild = type === NodeTypes.INTERPOLATION || type == NodeTypes.COMPOUND_EXPRESSION;
if (hasDynamicTextChild) {
patchFlag |= PatchFlags.TEXT;
}
vnodeChildren = child; // 一个儿子去掉数组
} else {
vnodeChildren = children;
}
}
if (patchFlag !== 0) {
vnodePatchFlag = String(patchFlag);
}
node.codegenNode = createVNodeCall(context, vnodeTag, vnodeProps, vnodeChildren, vnodePatchFlag);
}
}
4.根元素的转化
function createRootCodegen(root, context) {
const { helper } = context
const { children } = root
if (children.length == 1) {
const child = children[0];
// 就一个元素
const codegenNode = child.codegenNode;
codegenNode.isBlock = true
helper(OPEN_BLOCK)
helper(CREATE_BLOCK);
root.codegenNode = codegenNode; // 单个节点就转化成一个block
} else if (children.length > 1) { // 增加fragment
root.codegenNode = createVNodeCall(
context,
helper(FRAGMENT),
undefined,
root.children,
PatchFlags.STABLE_FRAGMENT
)
}
}
export function transform(root, options) {
const context = createTransformContext(root, options);
traverseNode(root, context);
createRootCodegen(root, context)
root.helpers = [...context.helpers]
}
一. codegen实现
export const helperNameMap: any = {
[FRAGMENT]: `Fragment`,
[OPEN_BLOCK]: `openBlock`,
[CREATE_BLOCK]: `createBlock`,
[CREATE_VNODE]: `createVNode`,
[TO_DISPALY_STRING]: "toDisplayString",
[CREATE_TEXT]: "createTextVNode"
}
用于做生成代码时产生的方法名
1.生成上下文
function createCodegenContext(ast) {
function newline(n) {
context.push('\n' + ` `.repeat(n))
}
const context = {
code: ``, // 拼接出的代码
indentLevel: 0, // 缩进
helper(key){
return `${helperNameMap[key]}`;
},
push(code) {
context.code += code;
},
newline() { // 新行
newline(context.indentLevel)
},
indent() { // 新行 + 2空格
newline(++context.indentLevel);
},
deindent() {// 缩进 2空格
newline(--context.indentLevel);
}
}
return context;
}
function generate(ast) {
const context = createCodegenContext(ast); // 生成代码时所需要的上下文
}
2.生成函数添加with
function generate(ast) {
const context = createCodegenContext(ast); // 生成代码时所需要的上下文
const { indent, deindent, push, newline } = context;
push(`function render(_ctx){`);
indent();
push(`with(_ctx){`);
indent();
push('// todo import..');
newline()
push(`return `);
push(`// todo vnode`)
deindent();
push(`}`);
deindent();
push('}');
return context.code
}
生成导入语句
push(`const {${ast.helpers.map(s => `${helperNameMap[s]}`).join(',')}} = _Vue`);
生成return语句
genNode(ast.codegenNode, context)
3.对不同类型做转化
function genNode(node, context) {
if (isString(node)) {
context.push(node)
return
}
if (isSymbol(node)) {
context.push(context.helper(node))
return
}
switch (node.type) {
case NodeTypes.VNODE_CALL:
break;
case NodeTypes.ElEMENT:
break;
case NodeTypes.TEXT:
break;
case NodeTypes.INTERPOLATION:
break;
case NodeTypes.COMPOUND_EXPRESSION:
break;
case NodeTypes.SIMPLE_EXPRESSION:
break;
case NodeTypes.TEXT_CALL:
break
case NodeTypes.JS_CALL_EXPRESSION:
break
default:
break;
}
}
二.转化方式
1.对block节点转化
case NodeTypes.VNODE_CALL:
// 1.最外层元素
genVNodeCall(node, context)
function genVNodeCall(node, context) {
const { push, helper } = context
const { tag, props, children, patchFlag, isBlock } = node;
if (isBlock) {
push(`(${helper(OPEN_BLOCK)}(),`)
}
push(helper(isBlock ? CREATE_BLOCK : CREATE_VNODE) + '(', node)
genNodeList([tag, props, children, patchFlag], context)
push(`)`);
if (isBlock) {
push(`)`)
}
}
function genNodeList(nodes, context) {
const { push } = context
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (isString(node)) {
push(node);
} else if (node == null) {
push(`null`);
} else if (isArray(node)) { // 是数组就循环
genNodeListAsArray(node, context)
} else { // 否则genNode
genNode(node, context)
}
if (i < nodes.length - 1) {
push(',')
}
}
}
function genNodeListAsArray(nodes, context) {
context.push(`[`)
context.indent();
console.log(nodes)
genNodeList(nodes, context);
context.deindent();
context.push(']')
}
2.对元素节点转化
case NodeTypes.ElEMENT:
genNode(node.codegenNode, context)
3.对文本节点转化
case NodeTypes.TEXT:
genText(node, context);
function genText(node,context) {
context.push(JSON.stringify(node.content), node)
}
4.对表达式的转化
case NodeTypes.INTERPOLATION:
genInterpolation(node, context)
function genInterpolation(node, context) {
const { push, helper } = context
push(`${helper(TO_DISPALY_STRING)}(`)
genNode(node.content, context)
push(`)`)
}
5.简单表达式
case NodeTypes.SIMPLE_EXPRESSION:
genExpression(node, context)
function genExpression(node, context) {
const { content, isStatic } = node
context.push(content)
}
6.复合表达式
case NodeTypes.COMPOUND_EXPRESSION:
genCompoundExpression(node, context)
function genCompoundExpression(
node,
context
) {
for (let i = 0; i < node.children!.length; i++) {
const child = node.children![i]
if (isString(child)) {
context.push(child)
} else {
genNode(child, context)
}
}
}
7.文本函数处理
case NodeTypes.TEXT_CALL:
genNode(node.codegenNode, context)
break
case NodeTypes.JS_CALL_EXPRESSION:
genCallExpression(node, context)
break
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(`)`)
}
完结
看完了的你,再强大 也是真的!(bushi
转载自:https://juejin.cn/post/7126782315052662792