写点什么

【Vue2.x 源码学习】第十六篇 - 生成 render 函数 - 代码拼接

用户头像
Brave
关注
发布于: 2021 年 06 月 16 日
【Vue2.x 源码学习】第十六篇 - 生成 render 函数 - 代码拼接

一,前言


上篇,生成 ast 语法树 - 构造树形结构部分


  • 基于 html 特点,使用栈型数据结构记录父子关系

  • 开始标签,结束标签及文本的处理方式

  • 代码重构及 ast 语法树构建过程分析


本篇,使用 ast 语法树生成 render 函数 - 代码拼接部分


二,生成 render 函数 - 代码拼接

1,前文回顾


第十二篇提到过,compileToFunction 方法是 Vue 编译的入口


compileToFunction 方法中做了两件事:

  1. parserHTML:将模板内容编译为 ast 语法树

  2. 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 函数

  • _c 等价于 createElement 创建一个元素

  • _v 等价于 _vode

  • _s 等价于 stringify


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 }}`:

  • name 有可能是一个对象,需要使用 JSON.stringify 将对象转换成为字符串


检查 text 中,是否包含{{}}:

  • 包含,说明是表达式;

  • 如果不包含,直接返回 _v('${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('+')})` } }}
复制代码

对文本的处理逻辑:

  1. 如果没有特殊的表达式,直接返回

  2. 如果有表达式,需进行匹配和截取处理


使用正则进行捕获处理,可能存在较复杂的情况,如:

`<div>aaa {{name}} bbb</div>` 或 `<div>aaa {{name}} bbb {{age}} ccc</div>`:

  1. 使用正则 defaultTagRE 捕获表达式,将表达式前面的一段'<div>aaa ' 中的 aaa 放入 tokens 数组

备注:本次捕获完成后,得到偏移量在表达式后,待表达式处理完成后统一调整即可;

  1. 将捕获到表达式名称 name 放入 tokens 数组中并修改匹配偏移量,同理继续处理其余表达式

备注:每次捕获成功后,重复 1,2 两个步骤

  1. 当表达式全部捕获完成后,若文本长度仍大于当前匹配偏移量,说明还有最后一段没有处理,

将 ' bbb</div>' 中的 bbb 也放入 tokens 数组

  1. 都放入 tokens 数组后,拼接返回 `_v(${tokens.join('+')})`




三,结尾


又成功水了一篇,还剩 5 篇


本篇,主要介绍了生成 render 函数 - 代码拼接

  • render 函数的代码拼接:generate(ast)

  • 处理属性:genProps(ast.attrs)

  • 处理属性中的样式

  • 递归深层处理儿子:genChildren


至此,render 函数中 with 内部的代码拼接已经完成

下一篇,生成 render 函数 - 函数生成

用户头像

Brave

关注

还未添加个人签名 2018.12.13 加入

还未添加个人简介

评论

发布
暂无评论
【Vue2.x 源码学习】第十六篇 - 生成 render 函数 - 代码拼接