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语法树(三)

相关推荐
环境栈笔记12 分钟前
指纹浏览器检查代理失败怎么排查?先分清代理、网络和检测服务
前端·人工智能·后端·自动化
ShineWinsu17 分钟前
对于HTML:HTML常见标签的解析—下
前端·css·面试·html·标签·表单·emmet
恋猫de小郭18 分钟前
Kotlin 2.5 的 Companion 改造,对 KMP 的重要升级
android·前端·flutter
前端双越老师32 分钟前
程序员 1v1 访谈系列第二期:很焦虑但又不知道干啥
前端·ai编程·全栈
雪芽蓝域zzs39 分钟前
Webstorm打开uniapp uvue文件报红处理方案
vue.js
广州华水科技1 小时前
北斗形变监测传感器在大坝安全监测中的应用与优势分析
前端
孔令飞1 小时前
如何学习Go
前端
HUIBUR科技1 小时前
AI 中枢,面向企业场景构建的 AI 智能体
前端·数据库·人工智能
随便写个软件1 小时前
【新版本】EasyExplorer资源管理器 3.12.0——地址栏重要更新
运维·服务器·前端·everything·资源管理器
Jack N1 小时前
【Strapi 系列之二】Collection / Single / Component 怎么选?Strapi 字段大全一篇讲透
前端·javascript·strapi