template 模板是怎样通过 Compile 编译的

Compile

compile 编译可以分成 parseoptimize 与 generate 三个阶段,最终需要得到 render function。这部分内容不算 Vue.js 的响应式核心,只是用来编译的,笔者认为在精力有限的情况下不需要追究其全部的实现细节,能够把握如何解析的大致流程即可。

由于解析过程比较复杂,直接上代码可能会导致不了解这部分内容的同学一头雾水。所以笔者准备提供一个 template 的示例,通过这个示例的变化来看解析的过程。但是解析的过程及结果都是将最重要的部分抽离出来展示,希望能让读者更好地了解其核心部分的实现。

<div :class="c" class="demo" v-if="isShow"><span v-for="item in sz">{{item}}</span>
</div>
var html = '<div :class="c" class="demo" v-if="isShow"><span v-for="item in sz">{{item}}</span></div>';

接下来的过程都会依赖这个示例来进行。

parse

首先是 parseparse 会用正则等方式将 template 模板中进行字符串解析,得到指令、class、style等数据,形成 AST(在计算机科学中,抽象语法树(abstract syntax tree或者缩写为AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。)。

这个过程比较复杂,会涉及到比较多的正则进行字符串解析,我们来看一下得到的 AST 的样子。

{/* 标签属性的map,记录了标签上属性 */'attrsMap': {':class': 'c','class': 'demo','v-if': 'isShow'},/* 解析得到的:class */'classBinding': 'c',/* 标签属性v-if */'if': 'isShow',/* v-if的条件 */'ifConditions': [{'exp': 'isShow'}],/* 标签属性class */'staticClass': 'demo',/* 标签的tag */'tag': 'div',/* 子标签数组 */'children': [{'attrsMap': {'v-for': "item in sz"},/* for循环的参数 */'alias': "item",/* for循环的对象 */'for': 'sz',/* for循环是否已经被处理的标记位 */'forProcessed': true,'tag': 'span','children': [{/* 表达式,_s是一个转字符串的函数 */'expression': '_s(item)','text': '{{item}}'}]}]
}

最终得到的 AST 通过一些特定的属性,能够比较清晰地描述出标签的属性以及依赖关系。

接下来我们用代码来讲解一下如何使用正则来把 template 编译成我们需要的 AST 的。

正则

首先我们定义一下接下来我们会用到的正则。

const ncname = '[a-zA-Z_][\\w\\-\\.]*';
const singleAttrIdentifier = /([^\s"'<>/=]+)/
const singleAttrAssign = /(?:=)/
const singleAttrValues = [/"([^"]*)"+/.source,/'([^']*)'+/.source,/([^\s"'=<>`]+)/.source
]
const attribute = new RegExp('^\\s*' + singleAttrIdentifier.source +'(?:\\s*(' + singleAttrAssign.source + ')' +'\\s*(?:' + singleAttrValues.join('|') + '))?'
)const qnameCapture = '((?:' + ncname + '\\:)?' + ncname + ')'
const startTagOpen = new RegExp('^<' + qnameCapture)
const startTagClose = /^\s*(\/?)>/const endTag = new RegExp('^<\\/' + qnameCapture + '[^>]*>')const defaultTagRE = /\{\{((?:.|\n)+?)\}\}/gconst forAliasRE = /(.*?)\s+(?:in|of)\s+(.*)/

advance

因为我们解析 template 采用循环进行字符串匹配的方式,所以每匹配解析完一段我们需要将已经匹配掉的去掉,头部的指针指向接下来需要匹配的部分。

function advance (n) {index += nhtml = html.substring(n)
}

举个例子,当我们把第一个 div 的头标签全部匹配完毕以后,我们需要将这部分除去,也就是向右移动 43 个字符。

调用 advance 函数

advance(43);

得到结果

parseHTML

首先我们需要定义个 parseHTML 函数,在里面我们循环解析 template 字符串。

function parseHTML () {while(html) {let textEnd = html.indexOf('<');if (textEnd === 0) {if (html.match(endTag)) {//...process end tagcontinue;}if (html.match(startTagOpen)) {//...process start tagcontinue;}} else {//...process textcontinue;}}
}

parseHTML 会用 while 来循环解析 template ,用正则在匹配到标签头、标签尾以及文本的时候分别进行不同的处理。直到整个 template 被解析完毕。

parseStartTag

我们来写一个 parseStartTag 函数,用来解析起始标签("

"部分的内容)。

function parseStartTag () {const start = html.match(startTagOpen);if (start) {const match = {tagName: start[1],attrs: [],start: index}advance(start[0].length);let end, attrwhile (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {advance(attr[0].length)match.attrs.push({name: attr[1],value: attr[3]});}if (end) {match.unarySlash = end[1];advance(end[0].length);match.end = index;return match}}
}

首先用 startTagOpen 正则得到标签的头部,可以得到 tagName(标签名称),同时我们需要一个数组 attrs 用来存放标签内的属性。

const start = html.match(startTagOpen);
const match = {tagName: start[1],attrs: [],start: index
}
advance(start[0].length);

接下来使用 startTagClose 与 attribute 两个正则分别用来解析标签结束以及标签内的属性。这段代码用 while 循环一直到匹配到 startTagClose 为止,解析内部所有的属性。

let end, attr
while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {advance(attr[0].length)match.attrs.push({name: attr[1],value: attr[3]});
}
if (end) {match.unarySlash = end[1];advance(end[0].length);match.end = index;return match
}

stack

此外,我们需要维护一个 stack 栈来保存已经解析好的标签头,这样我们可以根据在解析尾部标签的时候得到所属的层级关系以及父标签。同时我们定义一个 currentParent 变量用来存放当前标签的父标签节点的引用, root 变量用来指向根标签节点。

const stack = [];
let currentParent, root;

知道这个以后,我们优化一下 parseHTML ,在 startTagOpen 的 if 逻辑中加上新的处理。

if (html.match(startTagOpen)) {const startTagMatch = parseStartTag();const element = {type: 1,tag: startTagMatch.tagName,lowerCasedTag: startTagMatch.tagName.toLowerCase(),attrsList: startTagMatch.attrs,attrsMap: makeAttrsMap(startTagMatch.attrs),parent: currentParent,children: []}if(!root){root = element}if(currentParent){currentParent.children.push(element);}stack.push(element);currentParent = element;continue;
}

我们将 startTagMatch 得到的结果首先封装成 element ,这个就是最终形成的 AST 的节点,标签节点的 type 为 1。

const startTagMatch = parseStartTag();
const element = {type: 1,tag: startTagMatch.tagName,attrsList: startTagMatch.attrs,attrsMap: makeAttrsMap(startTagMatch.attrs),parent: currentParent,children: []
}

然后让 root 指向根节点的引用。

if(!root){root = element
}

接着我们将当前节点的 element 放入父节点 currentParent 的 children 数组中。

if(currentParent){currentParent.children.push(element);
}

最后将当前节点 element 压入 stack 栈中,并将 currentParent 指向当前节点,因为接下去下一个解析如果还是头标签或者是文本的话,会成为当前节点的子节点,如果是尾标签的话,那么将会从栈中取出当前节点,这种情况我们接下来要讲。

stack.push(element);
currentParent = element;
continue;

其中的 makeAttrsMap 是将 attrs 转换成 map 格式的一个方法。

function makeAttrsMap (attrs) {const map = {}for (let i = 0, l = attrs.length; i < l; i++) {map[attrs[i].name] = attrs[i].value;}return map
}

parseEndTag

同样,我们在 parseHTML 中加入对尾标签的解析函数,为了匹配如“

”。

const endTagMatch = html.match(endTag)if (endTagMatch) {advance(endTagMatch[0].length);parseEndTag(endTagMatch[1]);continue;
}

用 parseEndTag 来解析尾标签,它会从 stack 栈中取出最近的跟自己标签名一致的那个元素,将 currentParent 指向那个元素,并将该元素之前的元素都从 stack 中出栈。

这里可能有同学会问,难道解析的尾元素不应该对应 stack 栈的最上面的一个元素才对吗?

其实不然,比如说可能会存在自闭合的标签,如“
”,或者是写了“”但是没有加上“< /span>”的情况,这时候就要找到 stack 中的第二个位置才能找到同名标签。

function parseEndTag (tagName) {let pos;for (pos = stack.length - 1; pos >= 0; pos--) {if (stack[pos].lowerCasedTag === tagName.toLowerCase()) {break;}}if (pos >= 0) {stack.length = pos;currentParent = stack[pos]; }
}

parseText

最后是解析文本,这个比较简单,只需要将文本取出,然后有两种情况,一种是普通的文本,直接构建一个节点 push 进当前 currentParent 的 children 中即可。还有一种情况是文本是如“{{item}}”这样的 Vue.js 的表达式,这时候我们需要用 parseText 来将表达式转化成代码。

text = html.substring(0, textEnd)
advance(textEnd)
let expression;
if (expression = parseText(text)) {currentParent.children.push({type: 2,text,expression});
} else {currentParent.children.push({type: 3,text,});
}
continue;

我们会用到一个 parseText 函数。

function parseText (text) {if (!defaultTagRE.test(text)) return;const tokens = [];let lastIndex = defaultTagRE.lastIndex = 0let match, indexwhile ((match = defaultTagRE.exec(text))) {index = match.indexif (index > lastIndex) {tokens.push(JSON.stringify(text.slice(lastIndex, index)))}const exp = match[1].trim()tokens.push(`_s(${exp})`)lastIndex = index + match[0].length}if (lastIndex < text.length) {tokens.push(JSON.stringify(text.slice(lastIndex)))}return tokens.join('+');
}

我们使用一个 tokens 数组来存放解析结果,通过 defaultTagRE 来循环匹配该文本,如果是普通文本直接 push 到 tokens 数组中去,如果是表达式({{item}}),则转化成“_s(${exp})”的形式。

举个例子,如果我们有这样一个文本。

<div>hello,{{name}}.</div>

最终得到 tokens

tokens = ['hello,', _s(name), '.'];

最终通过 join 返回表达式。

'hello' + _s(name) + '.';

processIf与processFor

最后介绍一下如何处理“v-if”以及“v-for”这样的 Vue.js 的表达式的,这里我们只简单介绍两个示例中用到的表达式解析。

我们只需要在解析头标签的内容中加入这两个表达式的解析函数即可,在这时“v-for”之类指令已经在属性解析时存入了 attrsMap 中了。

if (html.match(startTagOpen)) {const startTagMatch = parseStartTag();const element = {type: 1,tag: startTagMatch.tagName,attrsList: startTagMatch.attrs,attrsMap: makeAttrsMap(startTagMatch.attrs),parent: currentParent,children: []}processIf(element);processFor(element);if(!root){root = element}if(currentParent){currentParent.children.push(element);}stack.push(element);currentParent = element;continue;
}

首先我们需要定义一个 getAndRemoveAttr 函数,用来从 el 的 attrsMap 属性或是 attrsList 属性中取出 name 对应值。

function getAndRemoveAttr (el, name) {let valif ((val = el.attrsMap[name]) != null) {const list = el.attrsListfor (let i = 0, l = list.length; i < l; i++) {if (list[i].name === name) {list.splice(i, 1)break}   }}return val
}

比如说解析示例的 div 标签属性。

getAndRemoveAttr(el, 'v-for');

可有得到“item in sz”。

有了这个函数这样我们就可以开始实现 processFor 与 processIf 了。

“v-for”会将指令解析成 for 属性以及 alias 属性,而“v-if”会将条件都存入 ifConditions 数组中。

function processFor (el) {let exp;if ((exp = getAndRemoveAttr(el, 'v-for'))) {const inMatch = exp.match(forAliasRE);el.for = inMatch[2].trim();el.alias = inMatch[1].trim();}
}function processIf (el) {const exp = getAndRemoveAttr(el, 'v-if');if (exp) {el.if = exp;if (!el.ifConditions) {el.ifConditions = [];}el.ifConditions.push({exp: exp,block: el});}
}

到这里,我们已经把 parse 的过程介绍完了,接下来看一下 optimize

optimize

optimize 主要作用就跟它的名字一样,用作「优化」。

这个涉及到后面要讲 patch 的过程,因为 patch 的过程实际上是将 VNode 节点进行一层一层的比对,然后将「差异」更新到视图上。那么一些静态节点是不会根据数据变化而产生变化的,这些节点我们没有比对的需求,是不是可以跳过这些静态节点的比对,从而节省一些性能呢?

那么我们就需要为静态的节点做上一些「标记」,在 patch 的时候我们就可以直接跳过这些被标记的节点的比对,从而达到「优化」的目的。

经过 optimize 这层的处理,每个节点会加上 static 属性,用来标记是否是静态的。

得到如下结果。

{'attrsMap': {':class': 'c','class': 'demo','v-if': 'isShow'},'classBinding': 'c','if': 'isShow','ifConditions': ['exp': 'isShow'],'staticClass': 'demo','tag': 'div',/* 静态标志 */'static': false,'children': [{'attrsMap': {'v-for': "item in sz"},'static': false,'alias': "item",'for': 'sz','forProcessed': true,'tag': 'span','children': [{'expression': '_s(item)','text': '{{item}}','static': false}]}]
}

我们用代码实现一下 optimize 函数。

isStatic

首先实现一个 isStatic 函数,传入一个 node 判断该 node 是否是静态节点。判断的标准是当 type 为 2(表达式节点)则是非静态节点,当 type 为 3(文本节点)的时候则是静态节点,当然,如果存在 if 或者 for这样的条件的时候(表达式节点),也是非静态节点。

function isStatic (node) {if (node.type === 2) {return false}if (node.type === 3) {return true}return (!node.if && !node.for);
}

markStatic

markStatic 为所有的节点标记上 static,遍历所有节点通过 isStatic 来判断当前节点是否是静态节点,此外,会遍历当前节点的所有子节点,如果子节点是非静态节点,那么当前节点也是非静态节点。

function markStatic (node) {node.static = isStatic(node);if (node.type === 1) {for (let i = 0, l = node.children.length; i < l; i++) {const child = node.children[i];markStatic(child);if (!child.static) {node.static = false;}}}
}

markStaticRoots

接下来是 markStaticRoots 函数,用来标记 staticRoot(静态根)。这个函数实现比较简单,简单来将就是如果当前节点是静态节点,同时满足该节点并不是只有一个文本节点左右子节点(作者认为这种情况的优化消耗会大于收益)时,标记 staticRoot 为 true,否则为 false。

function markStaticRoots (node) {if (node.type === 1) {if (node.static && node.children.length && !(node.children.length === 1 &&node.children[0].type === 3)) {node.staticRoot = true;return;} else {node.staticRoot = false;}}
}

optimize

有了以上的函数,就可以实现 optimize 了。

function optimize (rootAst) {markStatic(rootAst);markStaticRoots(rootAst);
}

generate

generate 会将 AST 转化成 render funtion 字符串,最终得到 render 的字符串以及 staticRenderFns 字符串。

首先带大家感受一下真实的 Vue.js 编译得到的结果。

with(this){return (isShow) ? _c('div',{staticClass: "demo",class: c},_l((sz),function(item){return _c('span',[_v(_s(item))])})): _e()
}

看到这里可能会纳闷了,这些 _c_l 到底是什么?其实他们是 Vue.js 对一些函数的简写,比如说 _c 对应的是 createElement 这个函数。没关系,我们把它用 VNode 的形式写出来就会明白了,这个对接上一章写的 VNode 函数。

首先是第一层 div 节点。

render () {return isShow ? (new VNode('div', {'staticClass': 'demo','class': c}, [ /*这里还有子节点*/ ])) : createEmptyVNode();
}

然后我们在 children 中加上第二层 span 及其子文本节点节点。

/* 渲染v-for列表 */
function renderList (val, render) {let ret = new Array(val.length);for (i = 0, l = val.length; i < l; i++) {ret[i] = render(val[i], i);}
}render () {return isShow ? (new VNode('div', {'staticClass': 'demo','class': c}, /* begin */renderList(sz, (item) => {return new VNode('span', {}, [createTextVNode(item);]);})/* end */)) : createEmptyVNode();
}

那我们如何来实现一个 generate 呢?

genIf

首先实现一个处理 if 条件的 genIf 函数。

function genIf (el) {el.ifProcessed = true;if (!el.ifConditions.length) {return '_e()';}return `(${el.ifConditions[0].exp})?${genElement(el.ifConditions[0].block)}: _e()`
}

genFor

然后是处理 for 循环的函数。

function genFor (el) {el.forProcessed = true;const exp = el.for;const alias = el.alias;const iterator1 = el.iterator1 ? `,${el.iterator1}` : '';const iterator2 = el.iterator2 ? `,${el.iterator2}` : '';return `_l((${exp}),` +`function(${alias}${iterator1}${iterator2}){` +`return ${genElement(el)}` +'})';
}

genText

处理文本节点的函数。

function genText (el) {return `_v(${el.expression})`;
}

genElement

接下来实现一下 genElement,这是一个处理节点的函数,因为它依赖 genChildren 以及g enNode ,所以这三个函数放在一起讲。

genElement会根据当前节点是否有 if 或者 for 标记然后判断是否要用 genIf 或者 genFor 处理,否则通过 genChildren 处理子节点,同时得到 staticClassclass 等属性。

genChildren 比较简单,遍历所有子节点,通过 genNode 处理后用“,”隔开拼接成字符串。

genNode 则是根据 type 来判断该节点是用文本节点 genText 还是标签节点 genElement 来处理。

function genNode (el) {if (el.type === 1) {return genElement(el);} else {return genText(el);}
}function genChildren (el) {const children = el.children;if (children && children.length > 0) {return `${children.map(genNode).join(',')}`;}
}function genElement (el) {if (el.if && !el.ifProcessed) {return genIf(el);} else if (el.for && !el.forProcessed) {return genFor(el);} else {const children = genChildren(el);let code;code = `_c('${el.tag},'{staticClass: ${el.attrsMap && el.attrsMap[':class']},class: ${el.attrsMap && el.attrsMap['class']},}${children ? `,${children}` : ''})`return code;}
}

generate

最后我们使用上面的函数来实现 generate,其实很简单,我们只需要将整个 AST 传入后判断是否为空,为空则返回一个 div 标签,否则通过 generate 来处理。

function generate (rootAst) {const code = rootAst ? genElement(rootAst) : '_c("div")'return {render: `with(this){return ${code}}`,}
}

经历过这些过程以后,我们已经把 template 顺利转成了 render function 了,接下来我们将介绍 patch 的过程,来看一下具体 VNode 节点如何进行差异的比对。

注:本节代码参考《template 模板是怎样通过 Compile 编译的》。

上一篇:实现 Virtual DOM 下的一个 VNode 节点下一篇:数据状态更新时的差异 diff 及 patch 机制

template 模板是怎样通过 Compile 编译的相关推荐

  1. Spring Boot 使用 Kotlin Script Template 模板引擎kts 开发web应用

    Spring Boot 使用 Kotlin Script Template 模板引擎kts 开发web应用 在 Spring Framework 5.0 M4 中引入了一个专门的Kotlin支持. K ...

  2. 第六章-template模板

    第六章-template模板 文章目录 第六章-template模板 函数模板 类模板 函数模板 函数模板可以用来创建一个通用功能的函数,以支持多种不同形参,进一步简化重载函数的函数体设计 函数定义的 ...

  3. template模板函数

    template模板 一个简单的字符串模板 var template = (function() { var cache = {};return function(obj, str) {if (!ty ...

  4. c++模板函数声明定义分离编译错误详解

    今天看到accelerated c++上有个简单的vector容器的实现Vec,就再vs2008上编译了下: /  Vec.h #ifndef GUARD_VEC_H #define GUARD_VE ...

  5. template模板字符串

    在AJAX请求回数据之后,我们要将这些数据渲染到页面DOM树上.此时,我们有三种方式: 1. 创建元素 最最基本的方法 // 1 创建h2 var h2 = document.createElemen ...

  6. C++ template —— 模板基础(一)

    https://www.cnblogs.com/yyxt/p/5085516.html <C++ Template>对Template各个方面进行了较为深度详细的解析,故而本系列博客按书本 ...

  7. 设计模式之——Template模板模式

    Template模式又叫模板模式,是在父类中定义处理流程的框架,在子类中实现具体处理逻辑的模式.当父类的模板方法被调用时程序行为也会不同,但是,不论子类的具体实现如何,处理的流程都会按照父类中所定义的 ...

  8. Ansible PLaybook template 模板详解

    template 模板 模板是一个文本文件,可以做为生成文件的模版,并且模板文件中还可嵌套jinja语法 jinja2语言 网站:https://jinja.palletsprojects.com/e ...

  9. 【小白学习C++ 教程】十五、C++ 中的template模板和泛型

    @Author:Runsen template模板在 C++ 中一个简单但非常强大的工具.简单的想法是将数据类型作为参数传递,这样我们就不需要为不同的数据类型编写相同的代码. C++ 添加了两个新关键 ...

最新文章

  1. Go 分布式学习利器(7)-- 字符串
  2. python PDF 转 图片
  3. python3下载文件-在Python 3中从Web下载文件
  4. Codeforces Gym101257F:Islands II(求割点+思维)
  5. scratch跳一跳游戏脚本_涂鸦骑士3D版强势屠榜,腾讯跳一跳“宝刀未老” | 休闲新游周报...
  6. mysql查询正在执行的存储过程,[转]ms sql server 存储过程,查看正在执行的sql语句...
  7. biztalk在用户代码中构造多部分消息
  8. Selenium UI自动化测试(一)环境配置
  9. MySQL-bin.index no found (errcode:13-perssion)
  10. ubuntu 安装caj阅读器
  11. 盗版 Win7 试用到期后黑屏咋办
  12. 【Nodejs】732- 我为 Express 开了外挂
  13. matlab 拟合光滑曲线图,Matlab光滑曲线多项式拟合与样条曲线拟合的两个案例
  14. 长期戴耳机听歌的危害这么大,这些坏习惯你还在做吗
  15. 元宇宙研讨会-空间设计与交互技术构造的叙事世界
  16. FSA-Net: Learning Fine-Grained Structure Aggregation for Head Pose Estimation from a Single Image
  17. PyTorch模型训练的几个加速技巧
  18. 家庭生活指南杂志家庭生活指南杂志社家庭生活指南编辑部2022年第6期目录
  19. 处理ORA-06512错误方法
  20. 股票量化交易Python——计算收益率

热门文章

  1. Groovy语言之SpringBoot整合JDBC案例
  2. THINKPAD T420(4180J4C)还是THINKPAD T420(4180PLC)好?
  3. 第零课、开启ce自身保护、设置od,让调试不非法
  4. 一个 WordPress 安装,多个博客
  5. 请谨慎使用sp_executesql
  6. MongoDB 教程五: MongoDB固定集合和性能优化
  7. Magento : Make 'Continue Shopping' button redirect to the product index page
  8. delegate和protocol
  9. restorecon命令
  10. Linux LiveCD:从CD光盘运行Linux