一,前言
上篇,生成 ast 语法树 - 构造树形结构部分
本篇,使用 ast 语法树生成 render 函数 - 代码拼接部分
二,生成 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 函数
generate 方法:将 ast 生成为 render 函数
通过调用 generate 方法,传入 ast,生成 code
之前简单提到了 render 函数
左边的模板 -> 生成为 -> 右边的 render 函数
2,render 函数之代码拼接:generate(ast)
代码生成的方式,就是进行字符串拼接
仿造上图 render 方法,进行字符串的拼接
// src/compiler/index.js
function generate(ast) { let code = `_c('${ast.tag}',${ ast.attrs.length? JSON.stringify({}):'undefined' // 暂不处理属性,后面单独处理 }${ ast.children?`,[]`:'' // 暂不处理儿子,后面单独处理 })`
return code;}
// _c('div',{},[]}
复制代码
3,处理属性:genProps(ast.attrs)
// 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)}}`;// 去掉最后一位多余的逗号,再在外边套上{} }
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'} if(attr.name == "style"){ // <div id="app" style="color: red;background: blue;"></div> // 使用 replace 进行正则匹配,对样式进行 key,value 替换 // ^;: 不是分号(分割属性和值)、冒号(结尾) let styles = {}; attr.value.replace(/([^;:]+):([^;:]+)/g, function () { 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"}},// []}
复制代码
测试输出:
5,递归深层处理儿子:genChildren
继续处理儿子,demo 如下:
<div id="app" a='1' b=2 style="color: red;background: blue;"> <p>{{message}} <span>Hello Vue 1</span> <span>Hello Vue 2</span> <span>Hello Vue 3</span> </p></div>
复制代码
// _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'))// )// )
复制代码
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 }}
复制代码
7,为变量包装 _s
TODO:后续优化描述,添加表达式部分的截取分析和 log 跟踪
文本 -> 包装 _v
变量 -> 包装 _s
字符串 -> 包装 ""
模板中`{{ name }}`:
检查 text 中,是否包含{{}}:
判断是否包含{{}},可以使用正则 defaultTagRE:
如果包含,说明是表达式,需要做表达式 和 普通值的拼接
['aaa',_s(name),'bbb'].join('+') ==> _v('aaa' + s_(name) + 'bbb')
复制代码
先放数组 tokens 中再拼接一下,最后返回 `_v(${tokens.join('+')})`
8,完整实现
// src/compiler/index.js#genconst 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)){ return `_v('${text}')` // 普通文本,包装_v }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 while(match = defaultTagRE.exec(text)){ console.log("匹配内容" + text) let index = match.index;// match.index:指当前捕获到的位置 console.log("当前的 lastIndex = " + lastIndex) console.log("匹配的 match.index = " + index) if(index > lastIndex){ // 将前一段 ’<div>aaa '中的 aaa 放入 tokens 中 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]是花括号中间的部分,并处理可能存在的换行或回车) tokens.push(`_s(${match[1].trim()})`) // 更新 lastIndex 长度到'<div>aaa {{name}}' lastIndex = index + match[0].length; // 更新 lastIndex 长度到'<div>aaa {{name}}' }
// while 循环后可能还剩余一段,如:’ bbb</div>’,需要将 bbb 放到 tokens 中 if(lastIndex < text.length){ let lastText = text.slice(lastIndex); console.log("表达式处理完成后,还有内容需要继续处理:"+lastText) tokens.push(JSON.stringify(lastText))// 从 lastIndex 到最后 } return `_v(${tokens.join('+')})` } }}
复制代码
对文本的处理逻辑:
如果没有特殊的表达式,直接返回
如果有表达式,需进行匹配和截取处理
使用正则进行捕获处理,可能存在较复杂的情况,如:
`<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('+')})`
三,结尾
又成功水了一篇,还剩 5 篇
本篇,主要介绍了生成 render 函数 - 代码拼接
至此,render 函数中 with 内部的代码拼接已经完成
下一篇,生成 render 函数 - 函数生成
评论