likes
comments
collection

不能手写Vue3模板编译的你,再强大也是假的!

作者站长头像
站长
· 阅读数 14

手写 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