前端局部刷新

简介: 本文讲述除目前流行的`virtual-dom`更新界面外,另外一种不需要保留虚拟`dom`树,同时又可以高性能的更新界面的方案

引子

virtual-dom(简称vdom)的概念大规模的推广还是得益于react出现。

相比于频繁的手动去操作domvdom出现后,
框架根据标签结构生成虚拟dom树(virtual-dom-tree简称vtree),然后保留着这个vtree不能释放,当下一次数据变化后,再生成一个新的vtree与上一个比较,然后把差异部分更新到真实dom

我们只需要改变数据,由框架自动diff vdom然后再更新到dom上。所以后续我们只关注数据的变化即可,不再与dom直接打交道。

目前许多前端框架使用vdom已然是标配了。

问题

vdom虽好,但不是在任何场景下都适用,比如在寸土寸金的移动端。

由于框架需要保留着完整的vtree、变化后需要整个diff vtree,所以整体来看性能并不高(网上一些文章说引入vdom可以提升性能的说法是不准确的),在移动端的某些场景下,vdom的使用反而会带来性能问题。

移动端的列表场景,活动页面等,通常数据变化时需要对大块区域进行更新,在追求性能的要求下,我们能否仍然保留对开发人员友好,也能达到更高性能的刷新方式呢?

方案

原生dom操作一片html最高效的方法是innerHTML

再看一下我们的模板写法(这里以underscote.template的写法为例,其它写法也是同样的道理)

<div class="teachers">
    <%for(var i=0;i<teachers.length;i++){%>
      <span><%=teachers[i].name%></span>
    <%}%>
</div>
<div class="divider"></div>
<ul class="students">
    <%for(var i=0;i<students.length;i++){%>
        <li><%=students[i].name%></li>
    <%}%>
</ul>

我们会发现大多数模板命令(我们把<% expr %>这样的称之为模板命令)都是穿插在HTML标签中,对于上述示例,我们会发现模板中有2个变量teachers students。即当teachersstudents数据有变化时,界面上只会有div[class="teachers"]div[class="students"]2个节点会变化,其它节点是固定不变的,即当students变化时,我们只需要更新div[class="students"]节点即可。

我们把完整的模板根据变量所在的HTML标签进行子模板拆分,当有数据变化时,我们只更新对应的子模板即可,这样不用保留vtree对象,同时使用innerHTMLAPI,在大块更新html时可以达到最好的性能

实现

我们用以下简单的模板进行解释实现

<div>
    <%for(var i=0;i<list.length;i++){%>
        <span><%=list[i].name%></span>
    <%}%>
</div>

当我们拿到这样的模板后,主要识别html中的模板命令,我们要把前面的模板字符串变成合法的js语句,使用的是es6中的标签模板,即上述模板经简单处理后变成如下的字符串

`<div>
    <%`;for(var i=0;i<list.length;i++){`%>
        <span><%=`;list[i].name`%></span>
    <%`;}`%>
</div>`

加上反撇号(`),这一步使用正则即可完成。
变成上面的字符串后,它本身就是合法的js代码了。我们为了识别局部变量和全局变量,使用acorn这个工具进行语法分析

然后对语法树遍历IdentifierVariableDeclarator,找到变量使用和变量声明的地方,这时候要自己做一下处理。因为从外到内,从上到下遍历语法树,如果当前出现的变量未进行声明,则认为是全局变量,反之则认为局部变量

我们为了后续的其它分析,对模板做了入侵修改,即识别完后我们对全局变量和局部变量做了“手脚“,我们在全局变量前增加了 \u0003.,在使用变量的地方增加了\u0001这个字符 ,在变量声明的地方增加了 \u0002 这个字符。这样在做其它分析时会比较方便。

处理后即为

`<div>
    <%`;for(var \u0002i=0;\u0001i<\u0003.list.length;i++){`%>
        <span><%=`;\u0003.list[\u0001i].name`%></span>
    <%`;}`%>
</div>`

这时候已经不是合法的js代码了,不过这已经不重要了。我们再把反撇号(`)去掉,则最终为

<div>
    <%for(var \u0002i=0;\u0001i<\u0003.list.length;i++){%>
        <span><%=\u0003.list[\u0001i].name%></span>
    <%}%>
</div>

接下来为了要分析上面的模板,我们要把它变成合法的html标签(因为underscore.template的命令使用<%%>的形式和标签<tag>有冲突,所以要移除)。对模板命令进行移除,移除后的结果为

<div>
    &0
        <span>&1</span>
    &2
</div>

我们把命令移除,同时使用特殊的占位符占位,因为我们还要把命令还原回去,所以我们还要知道每个占位符对应的模板命令。我们用一个对象来记录每个占位符对应的原始模板内容
此时用于记录占位符对应模板命令的对象如下

{
  '&0':'<%for(var \u0002i=0;\u0001i<\u0003.list.length;i++){%>',
  '&1':'<%=\u0003.list[\u0001i].name%>',
  '&2':'<%}%>'
}

这个时候剩余的html就是合法的html了。

接下来给这段html添加guid的操作。

先给每个节点都添加一个guid,当然除了特别的节点如:没有模板命令的自闭合标签

添加完guid后变成为:

<div mx-guid="g0">
    &0
        <span mx-guid="g1">&1</span>
    &2
</div>

然后再次遍历,对不符合要求的guid进行移除。

移除保留规则如下:

1.如果移除子节点后,属性和剩余的内容中不存在模板命令,则移除guid
2.如果剩余内容+属性中的模板命令,变量声明和变量使用不配对,则删除guid
3.如果剩余内容+属性中的模板命令,变量声明和使用配对,则保留guid

即上述带guidhtml第一次被处理时
对外层的div移除配对的子标签后(为什么要先移除子标签?因为我们要把局部刷新做到最近的节点上),在本例中把子标签span移除

<div mx-guid="g0">
    &0
    &2
</div>

这时候我们把这个html片断还原模板命令语句,即把&0&2还原回旧样子

<div mx-guid="g0">
    <%for(var \u0002i=0;\u0001i<\u0003.list.length;i++){%>
    <%}%>
</div>

然后对这段代码进行变量声明和变量使用分析。因为每个变量前都有特殊的前缀,所以我们可以很方便的用正则识别出来:
在当前范围内声明的变量有 i 使用的局部变量有 i,当然还有一个全局变量list,全局变量我们不用管它,因为它在整个模板中都可以访问到。

我们可以看出声明和使用的变量是配对的。即:没有出现使用的局部变量未声明的情况
那么这个div标签的guid则可以保留

然后再分析子标签,这时候进行第二次的处理
子标签为

<span mx-guid="g1">&1</span>

同样命令还原

<span mx-guid="g1"><%=\u0003.list[\u0001i].name%></span>

同样变量识别,我们发现当前范围内使用到了局部变量i,但在当前范围内并未找到声明它的地方,则说明这段html不能被单独存在,只能做为其它标签的一部分。所以span上的guid要移除。

最终添加的guid如下

<div mx-guid="g0">
    &0
        <span>&1</span>
    &2
</div>

最后删除其中的\u0002\u0001占位符,最终存放模板命令的对象中存放的结果如下

{
  '&0':'<%for(var i=0;i<\u0003.list.length;i++){%>',
  '&1':'<%=\u0003.list[i].name%>',
  '&2':'<%}%>'
}

guid标识后,然后在进行子模板拆分分析时,只需要匹配带mx-guid的标签即可,因为只有这些节点才能做为局部刷新的容器节点,识别出相应的标签和内容,再从模板命令中提取出根节点对应的数据key,即完成了整个识别内容

当然在这一步还要考虑嵌套刷新等细节,比如

<div mx-guid="g0">
    <%=x%>
    <div mx-guid="g1"><%=y%></div>
</div>

当数据x,y都发生改变时,mx-guid="g1"的不需要更新,因为x的变化会让g1变到最新
y变化时,只需要更新g1即可

细节处理

关于以下代码

<div>
    <%for(var i=0;i<10;i++){%>
      <span><%=i%></span>
    <%}%>
</div>

这段代码从div的角度看,确实变量声明和使用都是配对的,但也会被移除mx-guid,凡是不带mx-guid标识的节点都不会被局部刷新。局部刷新的首要条件是当前代码片断中要包含全局变量

再比如这段代码

<div>
    <%for(var i=0;i<10;i++){%>
      <span><%=x%></span>
    <%}%>
</div>

这段代码只有span会被识别为局部刷新

再比如这样的情况

<%for(var i=0;i<10;i++){%>
  <span><%=x%></span>
<%}%>

外层缺少包括的标签,这种情况就需要整体做为子模板了

变量追踪

前面讲模板拆分时提到使用acorn对变量识别问题,这里详细讲一下一些变量的识别与处理

先把模板变成合法的js代码,前文已说方案,然后使用acron把这个js代码转成ast,然后多次遍历处理这个ast

第一次遍历

  1. 遇到变量声明时,记录下当前模板中声明了这个变量。记录声明变量的对象为gv,同时对这个变量所在的位置做特殊标记,标明它是声明语句
  2. 遇到变量使用时,如果这个变量存在gv中,则是在使用局部变量,否则就是在使用全局变量
  3. 遇到function时,function体内出现的变量需要看是否在function的参数中
  4. 当直接对某个变量赋值时,这个变量也将是全局变量

第二次遍历

  1. 记录变量声明时的id与初始化的值(如果有的话)
  2. 记录赋值语句,哪个变量在哪个位置赋了什么样的值(后期追踪变量时,会根据位置信息取合适的值,解决同一个变量被反复赋值的问题)

经过2次遍历后,这些变量都会被打上标记及它们出现的位置。
比如这样的代码

<%=list%>
<input <%:list%> />
<%list=a%>


<input <%:list%> />
<%list=b%>
<input <%:list%> />

处理后可能是:

<%=全.list%>
<input <%:全.list%> />
<%list=全.a%>


<input <%:用32.list%> />
<%list=全.b%>
<input <%:用43.list%> />

追踪变量赋值对象的结果可能是下面这样的对象

{
    list:[{
        pos:27,
        value:"全.a"
    },{
        pos:36,
        value:"全.b"
    }]
}

当接下来处理模板中的变量时,遇到"用pos.var"时,从变量列表中倒序查找比pos小的那一个对应的value即可

当变量追踪遇上函数

函数是一个独立的上下文

考虑如下的模板片断

<%var a=usr.name%>
<%_.each(function(){%>
    <%var a=usr.age%>
    <input <%:a%> />
    <%x(function(){%>
        <%var a=usr.sex%>
        <input <%:a%> />
    <%})%>
<%})%>
<input <%:a%> />

我们在反复声明、使用同一个变量a,每一个使用a的地方所对应的数据都是不一样的。

按前面我们的变量追踪所描述的,变量追踪对象可能如下

{
    a:[{
        pos:4,
        value:"全.usr.name"
    },{
        pos:20,
        value:"全.usr.age"
    },{
        pos:35,
        value:"全.usr.sex"
    }]
}

同时我们有一个记录函数区间的对象,如

[{
    start:9,
    end:50
},{
   start:25,
   end:45
}]

当我们在使用变量a的地方,我们可以找到当前a所在的函数内,即使函数嵌套也可以。
比如我们找到当前a所在的函数范围是[start=25,end=45],那么在根据这个范围,去变量追踪对象上找到这个范围内声明的变量a,我们只能查出{pos:35,value:"全.usr.sex"}在这个函数内,那么这个变量a对应的值即为全.usr.sex

如果查找到对应多个值,则a对应哪个值根据前述“变量的追踪”进行确定

后记

当然这里面提到的思路和方案不是一朝一夕完成的,这篇文章中也只讲了一些原理。完整的方案和代码可以参考

  1. 区块管理框架magix
  2. 离线处理工具magix-combine
  3. 局部更新magix-updater
目录
相关文章
|
9月前
局部刷新功能
局部刷新功能
31 0
|
4月前
Minigui局部刷新 InvalidateRect
Minigui局部刷新 InvalidateRect
15 0
|
5月前
|
JSON 前端开发 数据格式
ajax请求解析json数据渲染在前端界面
ajax请求解析json数据渲染在前端界面
26 0
|
8月前
|
前端开发
如何对ajax请求的后台数据添加到swiper轮播图并展示到页面
如何对ajax请求的后台数据添加到swiper轮播图并展示到页面
98 0
|
10月前
|
XML 前端开发 JavaScript
前端祖传三件套JavaScript的网络请求之Ajax
在前端开发中,Ajax(Asynchronous JavaScript and XML)是一种异步网络请求技术,可以实现无需刷新页面的数据交互。本文将介绍 Ajax 的基本原理和使用方法。
88 0
|
XML JSON 缓存
前端必看之AJAX功能原理详解篇
jax 这个概念是由 Jesse James Garrett 在 2005 年发明的。它本身不是单一技术,是一串 技术的集合,主要有: 1.JavaScript,通过用户或其他与浏览器相关事件捕获交互行为; 2.XMLHttpRequest 对象,通过这个对象可以在不中断其它浏览器任务的情况下向服务 器发送请求; 3.服务器上的文件,以 XML、HTML 或 JSON 格式保存文本数据; 4.其它 JavaScript,解释来自服务器的数据(比如 PHP 从 MySQL 获取的数据)并将其 呈现到页面上。
103 0
前端必看之AJAX功能原理详解篇
|
XML JSON 前端开发
【Ajax入门】实现页面的局部刷新,前后端交互少不了的技术
之前已经学习jQuery了,我们今天在jQuery的基础上继续学习Ajax。如果你不知道jQuery那么你可以先去看看本专栏的上一篇文章。
【Ajax入门】实现页面的局部刷新,前后端交互少不了的技术
|
前端开发
ajax无刷新删除
ajax无刷新删除
55 0
|
JSON 前端开发 API
前端 ajax 请求的优雅方案
前端 ajax 请求的优雅方案
89 0
|
XML 前端开发 JavaScript
异步编程|五分钟让你学会局部刷新Ajax技术
Ajax是Asynchronous JavaScript and XML的缩写,是JavaScript、XML、CSS、DOM等多个技术的组合。Ajax的工作原理是一个页面的指定位置...
147 0