【第1265期】那些前端MVVM框架是如何诞生的

前言

不是分享每个框架的历史。今日早读文章由网易考拉海购@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片段。

引入可复用概念

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发
头像
来说点什么吧!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容