一,前言
上篇,生成 ast 语法树 - 构造树形结构部分
- 基于 html 特点,使用栈型数据结构记录父子关系;
- 分析开始标签,结束标签及文本的ast构建逻辑和代码实现;
- 重构 html 解析与 ast 语法树构建部分代码;
- 对 ast 语法树构建的过程分析;
本篇,使用 ast 语法树生成 render 函数 - 代码拼接部分
二,前文回顾
1,流程梳理
第十二篇提到过,compileToFunction方法是 Vue 编译的入口;
在compileToFunction方法中,主要做了两件事:
parserHTML:将模板内容编译为 ast 语法树;generate:再根据 ast 语法树生成为 render 函数;
Vue 编译阶段的最终产物就是 render 函数:
// src/compiler/index.js
export function compileToFunction(template) {
// 1,将模板编译称为 AST 语法树
let ast = parserHTML(template);
// 2,使用 AST 生成 render 函数
let code = generate(ast);
}
之前几篇中,通过parserHTML方法完成了html模板的解析并构建出ast语法树;
接下来,继续实现generate方法:使用ast语法树生成render函数;
2,模板编译示例
之前提到过 render 函数,模板编译示例:
由左侧的html模板 -> 生成为右侧的render函数:
_c等价于createElement创建一个元素;_v等价于_vnode_s等价于JSON.stringify()转换为字符串;
二,生成 render 函数 - 代码拼接
1,render 函数的实现方式
generate方法:根据 ast 语法树生成为 render 函数;
即当外部调用generate方法时,传入ast语法树,返回render函数;
根据“模板编译示例”中render函数的结构,通过字符串拼接的方式生成render函数代码;
2,render 函数之代码拼接:generate(ast)
仿照“模板编译示例”中render函数的语法和结构,根据ast语法树中的元素节点信息,采用字符串拼接的方式构建出render函数:
// src/compiler/index.js#generate
function generate(ast) {
// 字符串拼接 render 函数
let code = `_c('${ast.tag}',${
// 暂不处理属性,后面单独处理
ast.attrs.length? JSON.stringify({}):'undefined'
}${
ast.children?`,[]`:'' // 暂不处理儿子,后面单独处理
})`
return code;
}
// 输出结果:_c('div',{},[]}
以上操作拼接出了render函数的大致结构,接下来继续处理属性和儿子;
3,处理属性:genProps(ast.attrs)
创建用于处理属性的genProps方法:通过拼接字符串方式格式化属性信息;(将数组格式化为字符串)
// src/compiler/index.js
// 将 attrs 数组格式化为:{key=val,key=val}
function genProps(attrs) {
let str = '';
for(let i = 0; i< attrs.length; i++){
let attr = attrs[i];
// 使用 JSON.stringify 将 value 转为 string 类型
// 注意:每个属性值的最后都会添加一个逗号
str += `${attr.name}:${JSON.stringify(attr.value)},`
}
return `{${str.slice(0, -1)}}`;// 去掉最后一位多余的逗号,在外层以{}包裹
}
// 在 generate 中调用 genProps 处理属性
function generate(ast) {
let code = `_c('${ast.tag}',${
ast.attrs.length? genProps(ast.attrs):'undefined'
}${
ast.children?`,[]`:''
})`
return code;
}
export function compileToFunction(template) {
let ast = parserHTML(template);
let code = generate(ast);
console.log(code)
}
// _c('div',{id:"app",a:"1",b:"2"},[]}
4,处理属性中的样式
在style属性中会存在样式信息,也需要在处理属性时一并处理:
<div id="app" a='1' b=2 style="color: red;background: blue;">
<p>{{message}}
<span>Hello Vue</span>
</p>
</div>
继续,还需要将样式数组也格式化为对象形式:
// src/compiler/index.js#genProps
// 将 attrs 数组格式化为:{key=val,key=val}
function genProps(attrs) {
let str = '';
for(let i = 0; i< attrs.length; i++){
let attr = attrs[i];
// ****** 将样式数组处理为对象 {name:id, value:'app'} ****** //
// <div id="app" style="color: red;background: blue;"></div>
// 使用 replace 进行正则匹配,对样式进行 key,value 替换
if(attr.name == "style"){
let styles = {};
// 正则解析: 中间以分号隔开,两测不能有冒号(分割)和分号(结尾)
attr.value.replace(/([^;:]+):([^;:]+)/g, function () {
// key:arguments[1]、value:arguments[2]
styles[arguments[1]] = arguments[2]
})
// 替换为处理后的样式对象
attr.value = styles;
}
str += `${attr.name}:${JSON.stringify(attr.value)},`
}
return `{${str.slice(0, -1)}}`;
}
// 打印输出:
// _c('div',
// {id:"app",a:"1",b:"2",style:{"color":" red","background":" blue"}},
// []}
测试输出:
注意:这里并没有对
onclick等事件进行处理,仅以属性作为实现的示例;
5,处理儿子(递归):genChildren(el)
处理好属性之后,继续处理儿子,示例如下:
<div id="app" a='1' b=2 style="color: red;background: blue;">
<p>{{message}}
<span>Hello Vue1</span>
<span>Hello Vue2</span>
<span>Hello Vue3</span>
</p>
</div>
- 儿子可能是标签,也可能是文本;
- 当儿子为标签时,标签中可能还存在多个儿子,需要对当前标签做递归处理;
// src/compiler/index.js
// 输出结果:_c(div,{},c1,c2,c3...)
function generate(ast) {
// 处理儿子
let children = genChildren(ast);
let code = `_c('${ast.tag}',${
ast.attrs.length? genProps(ast.attrs):'undefined'
}${
children?`,${children}`:''
})`
return code;
}
function genChildren(el) {
console.log("===== genChildren =====")
let children = el.children;
if(children){
console.log("存在 children, 开始遍历处理子节点...", children)
let result = children.map(item => gen(item)).join(',');
console.log("子节点处理完成,result = " + JSON.stringify(result))
return result
}
console.log("不存在 children, 直接返回 false")
return false;
}
function gen(el) {
console.log("===== gen ===== el = ",el)
if(el.type == 1){
console.log("元素标签 tag = "+el.tag+",generate继续递归处理")
// 标签类型:标签中可能还存在多个儿子,需要继续递归处理当前元素(标签)
return generate(el);
}else{
console.log("文本类型,text = " + el.text)
// 文本类型:直接返回
return el.text ;
}
}
// _c('div',{id:"app",a:"1",b:"2",style:{"color":" red","background":" blue"}},
// _c('p',undefined,_v(_s(message)),
// _c('span',undefined,_v('HelloVue1')),
// _c('span',undefined,_v('HelloVue2')),
// _c('span',undefined,_v('HelloVue3'))
// )
// )
根据 render 函数特征,对文本类型包装
_v、对变量类型包装_s;
6,为文本类型包装 _v
function gen(el) {
console.log("===== gen ===== el = ",el)
if(el.type == 1){//
console.log("元素标签 tag = "+el.tag+",generate继续递归处理")
return generate(el);// 如果是元素就递归的生成
}else{// 文本类型
let text = el.text
console.log("文本类型,text = " + text)
return `_v('${text}')` // 包装 _v
}
}
在type == 2的文本类型中,还有可能存在插值表达式的情况,如:{{ msg }}
而插值表达式需要包装_s,需通过正则匹配的方式将其过滤掉;
7,为变量包装 _s
对照render函数的生成结果:
处理逻辑如下:
- 文本 -> 包装
_v - 变量 -> 包装
_s - 字符串 -> 包装
""
模板中的插值表达式{{ name }}:
name可能是一个对象,因此需要使用_s(同JSON.stringify)转换成为字符串;
通过正则defaultTagRE 检查 text文本中是否包含{{}}:
- 若不包含,直接返回
_v('${text}'); - 若包含,说明是表达式,需要对表达式 和 普通值执行拼接操作:将表达式和普通值按顺序放入
tokens数组中,执行拼接操作后再返回,即:_v(${tokens.join('+')});
['aaa',_s(name),'bbb'].join('+') ==> _v('aaa' + s_(name) + 'bbb')
8,完整实现
// src/compiler/index.js#gen
// 匹配 {{}} 表达式
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g
function gen(el) {
console.log("===== gen ===== el = ",el)
// ************ 标签类型 ************ //
if(el.type == 1){
console.log("元素标签 tag = "+el.tag+",generate继续递归处理")
// 标签元素需要递归执行生成逻辑
return generate(el);
// ************ 文本类型 ************ //
}else{
let text = el.text
console.log("文本类型,text = " + text)
// ************ 普通文本 ************ //
if(!defaultTagRE.test(text)){
// 普通文本,包装_v
return `_v('${text}')`
// ************ {{}} 表达式文本 ************ //
}else{
// 存在 {{}} 表达式,对 表达式 和 普通值 执行拼接操作
// 目标:['aaa',_s(name),'bbb'].join('+') ==> _v('aaa' + s_(name) + 'bbb')
let lastIndex = defaultTagRE.lastIndex = 0; // 重置索引标记
let tokens = []; // <div>aaa {{name}} bbb</div>
let match
// ************ 捕获 {{}} 表达式并处理 ************ //
// exec 循环捕获结果
while(match = defaultTagRE.exec(text)){
console.log("匹配内容" + text)
// match.index:获取当前捕获到的位置
let index = match.index;
console.log("当前的 lastIndex = " + lastIndex)
console.log("匹配的 match.index = " + index)
// index > lastIndex:说明匹配到了内容,将前一段'<div>aaa '中的 aaa 放入 tokens 数组中
if(index > lastIndex){
let preText = text.slice(lastIndex, index)
console.log("匹配到表达式-找到表达式开始前的部分:" + preText)
tokens.push(JSON.stringify(preText))// 利用 JSON.stringify 添加双引号
}
console.log("匹配到表达式:" + match[1].trim())
// 继续放入 match 到的表达式,如:{{ name }}
// match[1]:表示花括号中间的内容 name,需要处理表达式部分可能存在的换行或回车
tokens.push(`_s(${match[1].trim()})`)
// 更新 lastIndex 长度到'<div>aaa {{name}}',用于下一次 while 对比匹配结果
lastIndex = index + match[0].length;
}
// ************ 处理剩余部分 ************ //
// while 循环结束后,可能还剩余了一段,比如:’ bbb</div>’
// lastIndex < text.length:说明还有剩余内容需要处理,将 bbb 放入 tokens 数组中
if(lastIndex < text.length){
let lastText = text.slice(lastIndex);
console.log("表达式处理完成后,还有内容需要继续处理:"+lastText)
tokens.push(JSON.stringify(lastText))// 从 lastIndex 到最后
}
return `_v(${tokens.join('+')})`
}
}
}
基于以上完整实现,再做一次总结性说明:
todo:后续优化描述,添加表达式部分的截取分析和 log 跟踪 todo:这里的总结性说明与上边的部分内容有重复,有些啰嗦了,可以合并到一起;
对文本的处理逻辑:
- 如果没有特殊的表达式,直接返回;
- 如果有表达式,需进行匹配和截取处理;
使用正则进行捕获处理,可能存在较复杂的情况,如:
<div>aaa {{name}} bbb</div>
<!-- 或 -->
<div>aaa {{name}} bbb {{age}} ccc</div>
-
使用正则
defaultTagRE捕获表达式,将表达式前面的一段<div>aaa中的aaa放入tokens数组;备注:本次捕获完成后,得到偏移量在表达式后,待表达式处理完成后统一调整即可;
-
将捕获到的表达式名称
name放入tokens数组中并修改匹配偏移量;同理,继续处理其余表达式;备注:每次捕获成功之后,重复
1,2两个步骤; -
当表达式全部捕获完成后,若文本长度仍大于当前匹配偏移量,说明还有最后一段没有处理, 将
bbb</div>中的bbb也放入tokens数组中; -
全部放入
tokens数组后,拼接返回_v(${tokens.join('+')})
最终,就完成了code字符串的拼接,即render函数内部return的代码;
至此,距离得到最终的render函数,还差外边的两层:
- 向外一层
with代码块的包裹; - 再向外一层
Function函数的包裹;
三,结尾
本篇,主要介绍了生成 render 函数 - 代码拼接
- render 函数的分析和实现方案;
- 拼接 render 函数结构:generate(ast);
- 处理属性及属性值中的样式:genProps(ast.attrs);
- 递归处理儿子:genChildren(el);
- 对文本和变量进行包装处理,详细分析文本处理流程;
至此,render 函数中 with 内部的代码拼接已经完成;
下一篇,生成 render 函数 - 函数生成
维护日志:
- 20230126:调整了目录结构,添加了部分代码注释,补充了对关键逻辑的描述,更新专栏摘要;
- 20230127:添加了部分代码注释和适当的代码换行,添加 todo;