前言
不是分享每个框架的历史。今日早读文章由网易考拉海购@Gloria投稿分享。
阅读本文可能需要8分钟
正文从这开始~
刀耕火种时代
<p id="userInfo">
姓名:<span id="name">Gloria
性别:<span id="sex">男
职业:<span id="job">前端工程师
有以上html片段,想将其中个人信息替换为jabbla的,我们的做法:
document.getElementById('name').innerHTML = users.jabbla.name;
document.getElementById('sex').innerHTML = users.jabbla.sex;
document.getElementById('job').innerHTML = users.jabbla.job;
存在的问题
仔细想一想,这种开发方式,在users.jabbla和对应的html结构中间,总感觉有一层膜,除了需要在模板里定义某个元素的id外,还要在js中经过getDom(获取dom元素)和setDom(设置dom元素)操作。
有没有一种方法,可以帮助我们省去getDom和setDom,直接将users.jabbla和html结构对应起来。
当然有,由于innerHTML这个属性,我们可以利用模板系统。
引入模板
有了模板系统之后,我们可以写下这样的html片段:
<script id="userInfoTemplate">
姓名:<span>{name}</span>
性别:<span>{sex}</span>
职业:<span>{job}</span>
js中渲染模板,替换userInfo内容:
var userInfo = document.getElementById('userInfo');
var userInfoTemplate = document.getElementById('userInfoTemplate').innerHTML;
userInfo.innerHTML = templateEngine.render(userInfoTemplate, users.jabbla);
使用这种开发方式,省去了上面getDom和setDom的过程,可以说是”一气呵成”。
存在的问题
现在我们只实现了初始的内容渲染,如果需要在点击某个按钮切换用户信息:
<script id="userInfoTemplate">
姓名:<span>{name}</span>
性别:<span>{sex}</span>
职业:<span>{job}</span>
<button id="nextUserBtn">下个用户</button>
切换部分的逻辑:
var nextUserBtn = document.getElementById('nextUserBtn');
var currentUser;
nextUserBtn.addEventListener('click', function(){
currentUser = users.Gloria;
userInfo.innerHTML = templateEngine.render(userInfoTemplate, currentUser);
});
在这里,我们还是需要获取nextUserBtn元素,有没有不需要获取元素,在写userInfoTemplate的时候就指定好点击事件的方法呢?当然有,可以升级模板系统。
增强后的模板系统
我们想要的效果:
<script id="userInfoTemplate">
姓名:<span>{name}</span>
性别:<span>{sex}</span>
职业:<span>{job}</span>
<button cilck={nextUser()}>下个用户</button>
js代码:
var currentUser = users.jabbla;
function nextUser(){
currentUser = users.Gloria;
userInfo.innerHTML = templateEngine.render(userInfoTemplate, currentUser);
}
如何实现
可以看到,我们现在可以将事件直接放在userInfoTemplate中,然后在js中直接写替换逻辑,但是这个是如何实现的呢?
如果直接在html结构上绑定事件,事件处理函数无法获取到js中的作用域。所以换个思路,直接在html结构上绑定这个方式行不通。
想要获取函数的作用域,必须在dom元素上绑定。只要能将userInfoTemplate中的html结构解析成DOM树,然后遍历元素上的属性获取事件处理函数标识,再进行绑定就可以了。
于是在这一过程有很多种方案可以选择,但是思路都是先从userInfoTemplate生成一个类似Dom结构的Tree,再通过遍历Tree生成最终的DomTree。
template --> Tree --> DomTree
在这个阶段,现有轮子总体上可以分为以下3个派别:
特定的template语法,使用Parser解析出来的AST生成目标DomTree。
模板语法使用html规范,借助innerHTML,让浏览器自己生成一个带有template字符串的fakeDomTree,通过fakeDomTree生成最终设置好的DomTree。
直接将userInfoTemplate的内容写在js中,通过预处理的方式将模板字符串转换成声明virtualDom结构的js代码,最终使用完整的virtualDom生成DomTree。
各自特点
生成template和DomTree中间结构的过程不依赖浏览器环境,支持丰富的模板语法,但是其中Parser是在浏览器环境中运行,会带来一些性能问题,理论上首屏渲染速度会比其它两种方式逊色一点。
直接利用浏览器构建fakeDomTree,可以说性能方面要比带Parser的方案好很多,理论上首屏渲染速度要比第1种好很多。但是这种方案强依赖于浏览器环境,而浏览器的不同实现是不稳定因素。
通过对模板语法的预处理,直接生成virtualDom结构声明的代码,理论上这种方案是3个方案中首屏渲染速度最快的,它没有从template到virtualDom这一过程。比较受争议的一点是:模板与js必须写在一起,有人说这种方案是图灵完备的,模板也具有完整的编程能力。又有声音说:html,css,js写在一起的这种方式是有悖潮流的。见仁见智吧。
存在问题
使用以上讨论的三种方式解决了事件绑定之后,还有一个问题需要我们亟待解决。
可以看到,上面我们在点击“下一个用户”按钮之后,会再次调用templateEngine.render()这个方法。例子中还只是一个html片段,而现在我们写的template是整个页面的模板,如果再重新渲染整个页面,重复template``到DomTree这一过程,一点非常小的改动都会导致整个页面的重新渲染,这样会使得页面性能会非常差。
所以,我们需要局部更新功能。
引入局部更新
什么是局部更新呢?
为了避免整个页面重新渲染,使得在每次数据发生变化之后,只渲染那些需要更新的部分。
更新策略
主流框架分为两个派别:
将Dom元素与数据绑定在一起(创建观察者),当数据发生变化之后,执行更新Dom元素的操作
对比数据变化前后生成的两个类Dom结构树,将改变映射到真实的Dom树
对比:
会创建很多观察者常驻内存,随着页面越来越复杂,性能可能是个问题
每次改变都会重新生成新的Dom元素,所以数据与Dom元素无法形成绑定的关系。另外一点,diff算法的好坏直接决定了局部更新的性能。
检测变化
三种方式:
脏检查:遍历观察者,判断改变前后值是否发生变化,也就是脏检查,是主动的。
懒检测:劫持数据的改变行为,每当行为发生,就做一次变化检测,是”懒”的,在需要的时候进行。
不检测:不关心某个数据是否已经发生变化,只关心前后两个最后输出的类Dom结构的变化。
方案组合
结合更新策略和检测变化的几种方案,得到局部更新的几个方案
基于脏检查
脏检查+数据绑定Dom元素:在某些特定时刻,自动执行脏检查,更新脏数据对应的Dom元素。
脏检查+类Dom结构树对比:脏检查检测的单位可以具体到每个属性,这样会让类Dom结构树对比的范围更加精确,diff算法的性能会更好。
这两种方案都比较依赖脏检查,而脏检查最大的缺陷就是性能问题,它会遍历所有的观察者,不管这些观察者对应的数据是否已经发生变化。所以说这两种方案最大的缺陷就是脏检查的性能问题。
基于懒检测
懒检测+数据绑定Dom元素:每次属性的改变行为发生,对比前后值,更新对应Dom元素
懒检测+类Dom结构树对比:与脏检查的方案一致,都会让类Dom结构树的对比范围更加精确,会有比较好的diff算法效率。
看上去这种基于懒检测的方案,比脏检查的方案好很多,其实未必,如果不进行特殊处理,频繁发生数据改变行为,Dom元素会频繁更新或者频繁运行diff算法。需要做的就是合并一定时期内的所有变化,统一进行Dom更新或者diff。
基于不检测
不检测+数据绑定Dom元素:这种方案显然行不通,不检测数据变化我怎么更新Dom。。
不检测+类Dom结构树对比:手动触发数据集合到类Dom结构树的映射,对比前后两棵树,将改变映射到真实的Dom树。
这种方案对比前两种优势是不会有很多观察者常驻内存,不会频繁触发更新。而在diff算法效率方面,前两种方案会比较有优势。
存在的问题
现在,貌似已经解决了大部分的问题,这么多方案已经足够解决局部更新这个问题了。
又有个问题来了,如果我想复用下面这个结构片段,页面希望存在一个用户信息列表,怎么办?
<script id="userInfoTemplate">
<p>
姓名:<span>{name}</span>
性别:<span>{sex}</span>
职业:<span>{job}</span>
<button cilck={nextUser()}>下个用户</button>
</p>
按照之前的思路,我们想在构建视图的时候就需要声明式地复用这个userInfoTemplate片段。
引入可复用概念
暂无评论内容