开发者社区> 行列> 正文
阿里云
为了无法计算的价值
打开APP
阿里云APP内打开

前端局部刷新

简介: 本文讲述除目前流行的`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

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
前端-vue基础57-局部组件注册
前端-vue基础57-局部组件注册
4 0
局部变量和全局变量
一、局部变量 二、全局变量
17 0
前端必须熟悉的几种布局方式
本章主要是回顾Flex使用 和 一些常用布局的写法。
22 0
你根本不知道“她“的全貌,「可视化」前端项目内部依赖 🍉
你根本不知道“她“的全貌,「可视化」前端项目内部依赖 🍉
23 0
前端国际化的另类方式
前端国际化的另类方式
52 0
微前端
内容引用自《前端架构:从入门到微前端》第9章。 微前端是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将单页面前端应用由单一的单体应用转变为把多个小型前端应用聚合为一的应用。各个前端应用还可以独立开发、独立部署。 微前端的实现意味着对前端应用的拆分。拆分的目的并不只是为了在架构上好看,还可以提升开发效率。比如10万行的代码拆解成10个项目,每个项目1万行,要独立维护每个项目就会容易得多。
48 0
前端容灾
什么是容灾 容灾的概念始于后端,指当遇到某个服务器或某个机房发生自然灾害、断网断电等情况下的应急办法,可以保证服务依然可用。 新入行的小伙伴可能有疑问,都断网断电了怎么可能保证网站还可以正常访问那?其实这是对大型网站,理解不深导致的,你认为的网站是这样的 像这种单机的服务自然没法做什么容灾了,这一台机器挂了服务也就挂了。
2148 0
局部加权线性回归
通常,选择交给学习算法处理特征的方式对算法的工作过程有很大影响。      例如:在前面的例子中,用\(x1\)表示房间大小。通过线性回归,在横轴为房间大小,纵轴为价格的图中,画出拟合曲线。回归的曲线方程为:\(\theta_0+\theta_1x_1\),如下边最左边的图。
1426 0
+关注
行列
作品Magix 区块管理框架:https://github.com/thx/magix 欢迎试用、star与fork
15
文章
1
问答
文章排行榜
最热
最新
相关电子书
更多
低代码开发师(初级)实战教程
立即下载
阿里巴巴DevOps 最佳实践手册
立即下载
冬季实战营第三期:MySQL数据库进阶实战
立即下载