Vue2源码笔记(2)编译时-模板代码是怎么生效的之从AST生成渲染代码

前言

虽然现在已经是Vue3的版本,也已经用了相当一段时间的Vue3。但想起来Vue2的源码之前断断续续也看了蛮长的时间,就再回头整理一遍。有始有终。

我这里把接下来要写的内容分为编译时运行时

这是第二篇,我们将继续探索,Vue2编译时从parse()得到AST树之后,将继续干什么。

我们使用这个AST树帮助我们加深认识和理解:

json 复制代码
{
    tag: 'div',
    type: 1,
    attrs: [{name: 'class', value: 'container'}, {name: 'style', value: 'color:red; font-size:14px'}],
    parent: null,
    children: [
        {
            tag: 'div',
            type: 1,
            attrs: [],
            parent,
            children: []
        },
        {
            type: 3,
            text: 'hello{{name}}'
        }
    ]
}

代码逻辑

在拿到这个ast树之后,Vue2将处理它成为一串代码,这个过程就是genCode,在Vue2仓库中的内容要更详细,使用更多gen函数处理各种情况。在这里先做一个简要的版本来理解其中的思想,所以跟源码相比会简略不少。

js 复制代码
/**
 * @param {ast-node} node
 * @returns code
 */
export default function generate(ast) {
    let code;
    if (ast) {
        if (ast.atg === 'script') {
            code = 'null';
        } else {
            code = genElement(ast); // 这里我们仅考虑这种情况
        }
    } else {
        code = '_c("div")';
    }
    return {
        render: `with(this){return ${code}}`
    }
}

根据传入的元素节点,genElement将生成渲染函数的code,就是形如_c('div')这样的代码文本

js 复制代码
function genElement(el) {
    let code = `_c(${el.tag}${
        el.attrs.length ? `,${genProps(el.attrs)}` : '' // attrs
    }${
        children ? `,${children}` : '' // children
    })`
    return code
}

接下来我们需要处理ast.children中的子节点和属性

子节点

js 复制代码
function genElement(el) {
    let children = genChildren(el); // 处理子节点,children为undefined或形如 `[_v('xxx'+_s(yyy)),_v('xxx'+_s(yyy))]`
    let code = `_c(${el.tag}${
        el.attrs.length ? `,${genProps(el.attrs)}` : '' // attrs
    }${
        children ? `,${children}` : '' // children
    })`;
    return code
}

function genChildren(el) {
    const children = el.children;
    if (children) {
        return `[${children.map((c) => gen(c)).join(",")}]`
    }
}

function gen(node) {
    if (node.type == 1) {
        // 元素
        return genElement(node); // 递归处理
    } else {
        // 文本
    }
}

节点内容处理:

js 复制代码
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g; //匹配花括号 {{  }} 捕获花括号里面的内容

function gen(node) {
    if (node.type == 1) {
        // 元素
        return genElement(node); // 递归处理
    } else {
        // 文本
        let text = node.text
        if (!defaultTagRE.test(text)) { // 非花括号变量表达式
            return `_v(${JSON.stringify(text)})`
        }
        // 正则是全局模式 每次需要重置正则的lastIndex属性  不然会引发匹配bug
        // 文本
        let text = node.text
        if (!defaultTagRE.test(text)) { // 非花括号变量表达式
            return `_v(${JSON.stringify(text)})`
        }
        // 正则是全局模式 每次需要重置正则的lastIndex属性  不然会引发匹配bug
        let lastIndex = (defaultTagRE.lastIndex = 0)
        let tokens = []
        let match, index

        // eg.假设text = 'Hello, {{ name }}! Today is {{ date }}.'
        while ((match = defaultTagRE.exec(text))) {
            index = match.index; // 匹配到花括号变量的位置
            if (index > lastIndex) {
                // 匹配到的 {{ 位置  在 tokens 里面放入普通文本
                tokens.push(JSON.stringify(text.slice(lastIndex, index))); // eg.截取'Hello, '
            }
            // 放入捕获到的变量内容
            tokens.push(`_s(${match[1].trim()})`); // eg.match[1]捕获到 name
            // 匹配指针后移
            lastIndex = index + match[0].length;
        }
        // eg.结束后tokens = ['Hello, ', '_s(name)', '! Today is ', '_s(date)']

        // 如果匹配完了花括号  text里面还有剩余的普通文本 那么继续push
        if (lastIndex < text.length) {
            tokens.push(JSON.stringify(text.slice(lastIndex))); // eg.还剩下'.',需要再放入tokens数组中
        }
        // tokens形如['xxx', '_s(yyy)']
        return `_v(${tokens.join("+")})`; // 拼接返回
    }
}

属性

接下来是属性的处理:

js 复制代码
function genElement(el) {
    let children = genChildren(el);
    let code = `_c(${el.tag}${
        el.attrs.length ? `,${genProps(el.attrs)}` : '' // attrs
    }${
        children ? `,${children}` : '' // children
    })`;
    return code
}

function genProps(attrs) {
    let str = ""
    for (let i = 0; i < attrs.length; i++) {
        let attr = attrs[i]
        // 对attrs属性里面的style做特殊处理
        if (attr.name === "style") {
            let obj = {};
            attr.value.split(":").forEach((item) => {
                let [key, value] = item.split(":")
                obj[key] = value
            })
            attr.value = obj
        }
        str += `${attr.name}:${JSON.stringify(attr.value)},`;
    }
    // 返回值形如:{class:"container",style:{color:"red",font-size:"14px"}}
    return `{${str.slice(0, -1)}}`
}

以上简略介绍了Vue2是如何将AST树转化成渲染函数的,它们最终会得到形如_c('div', {'class':'container', style: {'color':'red','font-size':'14px'}}, [_v('xxx'+_s(yyy))])的字符串并在generate()

js 复制代码
return {
    render: `with(this){return ${code}}`
}

进行包装。

结语

在这一章中我们通过简略的代码了解Vue2的编译时是如何将ast树转换为渲染函数的,在完成这一步之后,就可以到运行时去绘制页面了(当然我们省略了很多处理,实际使用中要处理<script>的内容,打包时其实还要处理文件依赖......但主要目的是理解Vue2的运行逻辑)。

总结一下我们这章做的:

  1. 通过generate()genElement()处理ast节点
  2. 递归处理子节点
  3. 处理节点内容
  4. 处理attrs属性

下一篇,我们将进入Vue的运行时,了解在浏览器中的Vue2运行逻辑。

【参考】

手写vue2源码系列之将html转换成AST语法树(三)

相关推荐
前端搬运侠2 分钟前
📝从零到一封装 React 表格:基于 antd Table 实现多条件搜索 + 动态列配置,代码可直接复用
前端
歪歪1004 分钟前
Vue原理与高级开发技巧详解
开发语言·前端·javascript·vue.js·前端框架·集成学习
zabr4 分钟前
我让AI一把撸了个算命网站,结果它比我还懂玄学
前端·aigc·ai编程
xianxin_5 分钟前
CSS Fonts(字体)
前端
用户2519162427115 分钟前
Canvas之画图板
前端·javascript·canvas
快起来别睡了32 分钟前
前端设计模式:让代码更优雅的“万能钥匙”
前端·设计模式
EndingCoder1 小时前
Next.js API 路由:构建后端端点
开发语言·前端·javascript·ecmascript·全栈·next.js·api路由
2301_810970391 小时前
wed前端第三次作业
前端
程序猿阿伟1 小时前
《深度解构:React与Redux构建复杂表单的底层逻辑与实践》
前端·react.js·前端框架
酒酿小圆子~1 小时前
【Agent】ReAct:最经典的Agent设计框架
前端·react.js·前端框架