专栏首页腾讯NEXT学位干货 | 前端模板引擎知多少

干货 | 前端模板引擎知多少

前端框架日新月异,而其中的数据绑定已经作为一个框架最基础的功能。我们常常使用的单向绑定、双向绑定、事件绑定、样式绑定等,里面具体怎么实现,而当我们数据变动的时候又会触发怎样的底部流程呢?

模板数据绑定

数据绑定的过程其实不复杂:

1. 解析语法生成AST。

2. 根据AST结果生成DOM。

3. 将数据绑定更新至模板。

解析语法生成AST

抽象语法树(Abstract Syntax Tree)也称为AST语法树,指的是源代码语法所对应的树状结构。也就是说,对于一种具体编程语言下的源代码,通过构建语法树的形式将源代码中的语句映射到树中的每一个节点上。

其实我们的DOM结构树,也是AST的一种,把HTML DOM语法解析并生成最终的页面。而模板引擎中常用的,则是将模板语法解析生成HTML DOM。

1

捕获特定语法

生成AST的过程涉及到编译器的原理,一般经过以下过程:

语法分析

语法分析的任务是在词法分析的基础上将单词序列组合成各类语法短语,如“程序”,“语句”,“表达式”等等。 语法分析程序判断源程序在结构上是否正确,源程序的结构由上下文无关文法描述。语法分析程序可以用YACC等工具自动生成。

语义分析

语义分析是编译过程的一个逻辑阶段,语义分析的任务是对结构上正确的源程序进行上下文有关性质的审查,进行类型审查。语义分析是审查源程序有无语义错误,为代码生成阶段收集类型信息。 一般类型检查也会在这个过程中进行。

生成AST

AST的结构则根据使用者需要定义,下面的一些对象都是本人根据需要假设定义的。

2

DOM元素捕捉

最简单的,我们来捕获一个<div>元素,然后生成一个<div>元素。

例如我们可以将以下这样的DOM进行捕获:

<div>    <a>123</a>    <p>456<span>789</span></p></div>

捕获后我们或许可以得到这样的一个对象:

thisDiv = {    dom: {        type: 'dom', ele: 'div', nodeIndex: 0, children: [            {type: 'dom', ele: 'a', nodeIndex: 1, children: [                {type: 'text', value: '123'}            ]},            {type: 'dom', ele: 'p', nodeIndex: 2, children: [                {type: 'dom', ele: 'span', nodeIndex: 3, children: [{type: 'text', value: '456'}]},                {type: 'text', value: '789'}            ]},        ]    }}

原本就是一个<div>,经过AST生成一个对象,最终还是生成一个<div>,这是多余的步骤吗?不是的,在这个过程中我们可以实现一些功能:

1. 排除无效DOM元素,并在构建过程可进行报错。

2. 使用自定义组件的时候,可匹配出来。

3. 可方便地实现数据绑定、事件绑定等功能。

4. 为虚拟DOM Diff过程打下铺垫。

3

 数据绑定捕捉

这里我们拿来做例子的是,在Angular和Vue里面都有,是双大括号的数据绑定的语法。

在前面DOM元素捕获的基础上,我们来添加数据绑定:

<div>{{ data }}</div>

这么一个简单的数据,我们可以获得这样一个对象:

thisDiv = {    dom: {        type: 'dom', ele: 'div', nodeIndex: 0, children: [            {type: 'text', value: '123'}        ]    },    binding: [        {type: 'dom', nodeIndex: 0, valueName: 'data'}    ]}

这样,我们在生成一个DOM的时候,同时添加对data的监听,数据更新时我们会找到对应的nodeIndex,更新值:

// 假设这是一个生成DOM的过程,包括数据绑定和function generateDOM(astObject){    const {dom, binding = []} = astObject;    // 生成DOM,这里假装当前节点是baseDom    baseDom.innerHTML = getDOMString(dom);    // 对于数据绑定的,来进行监听更新吧    baseDom.addEventListener('data:change', (name, value) => {        // 寻找匹配的数据绑定        const obj = binding.find(x => x.valueName == name);        // 若找到值绑定的对应节点,则更新其值。        if(obj){            baseDom.find(`[data-node-index="${obj.nodeIndex}"]`).innerHTML = value;        }    });}// 获取DOM字符串,这里简单拼成字符串function getDOMString(domObj){    // 无效对象返回''    if(!domObj) return '';    const {type, children = [], nodeIndex, ele, value} = domObj;    if(type == 'dom'){        // 若有子对象,递归返回生成的字符串拼接        const childString = '';        children.forEach(x => {            childString += getDOMString(x);        });        // dom对象,拼接生成对象字符串        return `<${ele} data-node-index="${nodeIndex}">${childString}</${ele}>`;    }else if(type == 'text'){        // 若为textNode,返回text的值        return value;    }}

我们来对上面的代码进行说明。

1. 根据节点信息生成对应的HTML string,即getDOMString()方法。

这里我们只是简单完成了一种实现方式,根据节点生成DOM也有其他方式,例如使用.createElement()、.appendChild()、textContent等等。

我们称通过生成HTML string的方式为字符串模板,同时我们将通过createElement()/appendChild()的方式生成DOM称为节点模板

2. 通过监听数据变更,同时根据绑定的数值获取对应节点,并进行局部更新。

在使用字符串模版的时候,我们将nodeIndex绑定在元素属性上,主要是用于数据更新时追寻节点进行内容更新。 在使用节点模版的时候,我们可在创建节点的时候,将该节点保存下来,直接用于数据更新。

当然,即使在字符串模版,我们也可以遍历一遍binding来获取所有绑定数据的节点并保存,这样就不用每次数据更新事件触发的时候重新进行获取,毕竟DOM节点的匹配也是会有一定的消耗的。

3. 无论是数据还是事件、属性、样式等的绑定,都可以通过相似的方法进行。

虽然这里我们只介绍了数据的绑定,但其实事件的绑定、属性和样式的绑定都可以用相似的方式进行,当然事件监听和事件的触发都是我们自己定义的,对于传递的内容都可以用自己想要的方式来传。

AST生成模板

1

生成模板的方法

我们在捕获得到一个AST树结构后,会将其生成对应的DOM。一般来说我们有这些方式:

1.字符串模版:使用拼接的方式生成DOM字符串,直接通过innderHTML()插入页面。

2.节点模板:使用createElementappendChild()textContent

等方法,动态地插入DOM节点,根节点使用innderHTML()插入页面。

3.使用createElement()/appendChild()/textContent方法动态地插入DOM节点,但是根节点使用innderHTML()插入页面。

这几个有什么区别呢?

刚开始的时候,我们每次更新页面数据和状态,通常通过innerHTML方法来用新的HTML String替换旧的,这种方法写起来很简单,无非是将各种节点使用字符串的方式拼接起来而已。但是如果我们更新的节点范围比较大,这时候我们需要替换掉很大一片的HTML String

对于浏览器,这样的一次HTML String替换并不只是更新一些字符串那么简单。

2

浏览器的渲染机制

浏览器的一次页面渲染其实开销并不小,首先浏览器会解析三种文件:

· 解析 HTML / SVG / XHTML ,会生成一个DOM结构树

· 解析 CSS ,会生成一个 CSS规则树

· 解析 JS,可通过DOM API 和 CSS API 来操作DOM结构树和 CSS规则树

DOM结构树 与 CSS规则树结合,最终生成一个Render 树(即最终呈现的页面,例如其中会移除DOM结构树中匹配到 CSS 里面display:none;的DOM节点)。其中,CSS匹配DOM结构的过程是很复杂的,曾经在机器配置不高的日子也会出现过性能问题。

一般来说浏览器绘制页面的过程是:1.计算CSS规则树=> 2.生成Render数 => 3.计算各个节点的大小/position/z-index=> 4.绘制。其中计算的环节也是消耗较大的地方。

我们使用DOM APICSS API的时候,通常会触发浏览器的两种操作:RepaintReflow

Repaint:页面部分重画,通常不涉及尺寸的改变,常见于颜色的变化。这时候一般只触发绘制过程的第4个步骤。

Reflow:意味着节点需要重新计算和绘制,常见于尺寸的改变。

这时候会触发3和4两个步骤。

所以我们在写页面的时候会注意一些问题,例如不要一条一条地修改DOM的样式(会触发多次的计算或绘制),在写动画的时候多使用fixed/absolute等(Reflow的范围小),等等。

回到话题,如果我们直接每次更新页面数据和状态,都使用innerHTML的方式,无疑会增加浏览器的负担,所以需要跟踪节点进行局部跟新。当然,innerHTML也有它的优势,那就是我们可以使用一个innerHTML替代很多很多的createElement()/appendChild()/textContent方法,这在我们较少使用数据绑定和更新的情况下高效得多。

模板数据更新

我们讲了模版生成AST,以及通过AST生成DOM、并进行数据绑定的过程,接下来说明下模版数据更新的过程。

1

数据更新监听

前面将数据绑定的时候,也讲了使用事件监听的方式监听数据更新。这里接着介绍一些其他的方式。

脏检测:在Angular中,并不直接监听数据的变动,而是监听常见的事件如用户交互(点击、输入等)、定时器、生命周期等。在每次事件触发完毕后,计算数据的新值和旧值是否有差异,若有差异则更新页面,并触发下一次的脏检测,直到没有差异或是次数达到设定阈值。

脏检测是Angular的一大特色。由于事件触发的时候,并不能知道哪些数据会有变化,所以会进行大面积数据的新旧值Diff,这也毫无疑问会导致一些性能问题。在Angular2版本之后,由于使用了zone.js对异步任务进行跟踪,把这个计算放进worker,完了更新回主线程,是个类似多线程的设计,也提升了性能。

同时,在Angular2中应用的组织类似DOM,也是树结构的,脏检查会从根组件开始,自上而下对树上的所有子组件进行检查。相比Angular1中的带有环的结构,这样的单向数据流效率更高,而且容易预测。

Getter/Setter:在Vue中,主要是使用Proxy的方式,在相关的数据写入时进行模版更新。

手动Function:在React中,通过手动调用set()的方式写入数据来更新模版。

使用Proxy或者是set()的时候,我们可以通过event emit或是callback回调的方法,来触发数据的计算以及模版的更新。

2

数据监听Diff

说到数据更新的Diff,更多的则是Diff + 更新模板这样一个过程。

在这个过程中,最突出的也就是虚拟DOM,它解决了常见的局部数据更新的问题,例如数组中值位置的调换、部分更新。一般来说计算过程如下:

1. 用JS对象模拟DOM树。

不知道大家仔细研究过DOM节点对象没,一个真正的DOM元素非常庞大,拥有很多的属性值。而其中很多的属性对于计算过程来说是不需要的,所以我们的第一步就是简化DOM对象。 我们用一个JavaScript对象结构表示DOM树的结构,然后用这个树构建一个真正的DOM树。

2. 比较两棵虚拟DOM树的差异。

当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异。通常来说这样的差异需要记录:

  • · 需要替换掉原来的节点
  • · 移动、删除、新增子节点
  • · 修改了节点的属性
  • · 对于文本节点的文本内容改变

经过差异对比之后,我们能获得一组差异记录,接下里我们需要使用它。

3. 把差异应用到真正的DOM树上。

对差异记录要应用到真正的DOM树上,例如节点的替换、移动、删除,文本内容的改变等。

结束语

当然上面的介绍以个人理解为主,部分源码验证为辅。 还是那句话,多思考多总结,不管结论是否正确,结果是否所期望,过程中的收获也会让人成长。

原文作者:腾讯高级工程师  王贝珊

  -前端好课-  

【Web前端从小白到大师】全新升级

更新比例高达50%,你值得拥有

若需了解更多,请扫码添加小助手咨询~

也可直接查找微信号:TencentNext

▲ NEXT学院 官方课程助教 ▲

点击阅读原文,开始课程试学

本文分享自微信公众号 - 腾讯NEXT学院(Next_Academy)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-05-31

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 干货 | 小程序自定义组件知多少

    基于小程序的双线程设计,视图层(Webview 线程)和逻辑层(JS 线程)之间通信(表现为 setData),是基于虚拟 DOM 来实现数据通信和模版更新的。

    腾讯NEXT学位
  • 【干货】解剖小程序的 setData

    双线程的渲染机制、通信机制,setData 的出现、工作原理、使用建议等,应该要怎么去理解呢?

    腾讯NEXT学位
  • Python 工匠: 异常处理的三个好习惯

    “ 如果你用 Python 编程,那么你就无法避开异常,因为异常在这门语言里无处不在。打个比方,当你在脚本执行时按 ctrl+c 退出,解释器就会产生一个 K...

    腾讯NEXT学位
  • 「微信小程序」剖析(四):原生的实时DOM转Virtual DOM

    在之前的几篇文章里,我们讨论了MINA的一些原理。晚上在想着怎么结合Vux + Virtual Dom实现一个名为WINV框架的时候,在探索WCC功能才发现:自...

    Phodal
  • 苹果对医疗科技动真格了,已招募 50 多名医生

    雷锋网《AI掘金志》频道:只做 AI +「安防、医疗、零售」三大传统领域的深度采访报道。

    AI掘金志
  • Hacklab WebIDE在线调试ESP32笔记

    1.什么是Hacklab WebIDE1.1 优势1.2 趋势2. 使用方法2.1 功能介绍2.2 编译第一个程序2.3 搭建esp32的开发环境2.4 建立开...

    bigmagic
  • 如何从请求、传输、渲染3个方面提升Web前端性能

    什么是WEB前端呢?就是用户电脑的浏览器所做的一切事情。我们来看看用户访问网站,浏览器都做了哪些事情:

    宜信技术学院
  • 简易远程消息交换协议SRMP

    经过十多年实战经验积累以及多方共同讨论,新生命团队(https://github.com/newlifex)制订了一种简单而又具有较好扩展性的RPC(Remot...

    大石头
  • 电信公司拥抱AI苦于招人难,Facebook把自家人才“共享”了出来

    夏乙 发自 凹非寺 量子位 出品 | 公众号 QbitAI Bengio的AI研究机构Element AI曾经公开表示,有能力进行严肃人工智能研究的,全世界加起...

    量子位
  • 抢饭碗的来了,毕业干哪行都可能没前途!

    科技时代,我们更加怀念温暖邂逅的时光 全文共2553字,预计阅读时长3分钟 ? 这年头,找一份好工作简直比找对象还难,为了混口饭吃,几千万人假装在生活。但现在人...

    企鹅号小编

扫码关注云+社区

领取腾讯云代金券

玩转腾讯云 有奖征文活动