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

相关推荐
黑狼传说8 小时前
从一行 var a = 1 开始,深入理解 V8 引擎的心脏
前端·javascript·v8
原生高钙8 小时前
var, let 和 const
前端·javascript·面试
huabuyu8 小时前
Taro微信小程序高性能无限下拉列表实现
前端
DevRen9 小时前
实现Google原生PIN码锁屏密码效果
android·前端·kotlin
ZSQA9 小时前
mac安装Homebrew解决网络问题
前端
烽学长9 小时前
(附源码)基于Vue的教师档案管理系统的设计与实现
前端·javascript·vue.js
前端一课9 小时前
前端监控 SDK,支持页面访问、性能监控、错误追踪、用户行为和网络请求监控
前端
lee5769 小时前
UniApp + SignalR + Asp.net Core 做一个聊天IM,含emoji 表情包
前端·vue.js·typescript·c#
✎﹏赤子·墨筱晗♪9 小时前
Shell函数进阶:返回值妙用与模块化开发实践
前端·chrome
再学一点就睡9 小时前
从 npm 到 pnpm:包管理器的进化与 pnpm 核心原理解析
前端·npm