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

相关推荐
董世昌411 分钟前
创建对象的方法有哪些?
开发语言·前端
问道飞鱼2 分钟前
【前端知识】前端项目不同构建模式的差异
前端·webpack·构建·开发模式·生产模式
be or not to be7 分钟前
CSS 布局机制与盒模型详解
前端·css
码界奇点15 分钟前
基于Spring Boot和Vue.js的房屋出租管理系统设计与实现
vue.js·spring boot·后端·车载系统·毕业设计·源代码管理
runepic21 分钟前
Vue3 + Element Plus 实现PDF附件上传下载
前端·pdf·vue
程序员修心34 分钟前
CSS 盒子模型与布局核心知识点总结
开发语言·前端·javascript
Cshaosun37 分钟前
阿里云宝塔面板部署vue+nodejs项目并实现https访问操作流程
vue.js·阿里云·https·node.js·宝塔·文件下载
elangyipi12340 分钟前
前端面试题:CSS BFC
前端·css·面试
程序员龙语40 分钟前
CSS 核心基础 —— 长度单位、颜色表示与字体样式
前端·css
shuishen491 小时前
视频尾帧提取功能实现详解 - 纯前端Canvas API实现
前端·音视频·尾帧·末帧