• 深入探讨ui框架


    深入探讨前端UI框架
    1 前言
    先说说这篇文章的由来

    最近看riot的源码,发现它很像angular的dirty check,每个component ( tag )都保存一个expressions数组,更新时,遍历expressions数组,重新求值,对比旧值,如果有变更则更新DOM。

    这不就是dirty check吗?为什么riot还声称它实现了virtual DOM?

    疑惑之下,就去复盘了一下各大前端框架,把一些收获分享给大家

    本文内容很多,实在不知道怎么取标题,最终取了一个泛泛的标题,请读者不要纠结

    本文将会涉及的内容有:

    MV*前端框架,UI框架,UI更新相关介绍
    UI更新机制原理及其代表框架介绍
    深入探讨各个UI更新机制(为什么virtual DOM会快)
    浏览器渲染机制
    riot的真相(virtual DOM的本质,给我自己一个交代!)
    裹脚布较长,读者慎入!

    2 理解前端框架
    2.1 前端的工作

    说起前端的工作,其实很简单,主要是:

    页面加载之后,如果有初始数据的话,则处理这些数据,并将其展示到UI上(通过DOM操作)
    用户与UI交互,比如点击某个button,或者某些异步事件,比如setTimeout,Ajax,产生了一个事件,事件监听者进行相应的处理,然后把变动体现到UI上,或者把用户的输入数据上传到服务器
    2.2 前端框架
    可以看到前端要做的工作还是比较直观,简单的

    但是,当一个页面很复杂,比如SPA的时候,就需要有一个成熟的架构来提升前端开发的效率

    前端框架提供一套成熟的解决方案来组织前端代码,前端数据流等

    前端框架的核心作用有且并不完全是:

    模块化,组件化,提高可复用性
    数据流清晰,提高可维护性
    常见的前端框架模式有:MVC, MVP, MVVM,可以查看阮大大的blog

    上图是MVVM框架的图示,取自阮大大的blog

    MVVM把model和view分离,把model和view的通信以及处理逻辑封装在vm对象中

    使得vm对象可复用,同一个vm对象可以绑定不同的view

    另外view和vm对象进行双向绑定,它们之间的数据流也非常清晰,提高可维护性

    2.3 UI & UI框架
    什么是UI?

    UI实际上是View层,用户看到的内容就是UI

    对于前端,web站点来说,UI就是HTML+CSS

    html在js的表现就是dom tree

    前端可以通过js脚本操作DOM,浏览器会根据最新的dom tree 和 css 进行渲染操作

    这个过程叫做UI更新

    UI框架是针对UI层的一套解决方案,提高了UI的组件化,提高复用性

    另外UI框架同时也会对UI更新有一套解决方案,提高UI更新的效率

    一些大型成熟的前端框架会有自己的一个UI框架,比如ember.js,extjs等

    一个比较典型的UI框架就是大家都熟悉的react

    2.4 UI更新及其策略
    前端界都知道,DOM操作(UI更新)通常都是前端页面的性能高消费者

    因此一个框架需要在UI更新这方面考虑的更加仔细,才能让系统获得更好的性能

    一般UI更新的策略有两种,大家也经常使用到

    直接上代码:

    // 1 需要改的才去改
    $('.我就是要找到你1').text('改文案');
    $('.我就是要找到你2').css('color', '改颜色');
    $('.我就是要找到你3').width('改宽度');

    // 2 使用模板
    $('.我是你们的公共父节点!').html(tpl({
    text: '改文案',
    color: '改颜色',
    '改宽度'
    });
    方式一是找到要改的节点,然后进行相应的DOM操作

    方式二是直接利用模板,直接更新一块dom tree

    方式一的优点是直观;缺点是代码很难维护

    方式二的优点是简单,只有一次UI更新;缺点是不需要改的也更新了!

    不需要变更的都一起更新会引发以下问题:

    重新生成dom tree
    原来绑定的事件没了
    input, textarea会失去焦点
    backbone 是方式二

    3 理解那些你所知道的前端框架
    现在有许多优秀的前端框架,下面分别介绍一下这些框架,以及这些框架与UI更新相关的内容

    3.1 AngularJs ( dirty check )

    AngularJs是mvvm框架,它的组件是vm组件,scope是vm组件的数据集合

    AngularJs通过directive来声明vm的行为,它实现为一个watcher,监听scope的属性的变化,把最新的属性更新UI

    另外当用户操作DOM的时候,产生事件,也通过watcher来把用户的输入修改到scope的属性中,这个技术称为双向绑定

    有一个关键的问题是,AngularJs如何实现监听scope的属性变更的呢?

    AngularJs使用的是dirty check技术,dirty check方案是在某个关键点,进入$digest循环,遍历所有的scope的属性,如果发现变更,则触发相应的watcher

    需要注意的是,watcher在执行的过程中有可能会修改scope的属性值,因此$digest要一直检查,直到scope完全稳定为止

    每个directive都是关注某一个点,比如修改css,class操作,text操作等

    因此Angular的UI更新机制本质上是方式一,它只是把定位元素节点的逻辑封装起来,并绑定了scope的字段,然后自动监控而已

    3.2 Vue、Avalon ( setter & getter )
    这些库的架构基本与AngularJs一致,唯一不同的就是如何实现监听scope的属性变更

    它们使用defineProperty的特性来监听scope的属性变更

    这种方式和使用setter,getter来实现属性变更入口的框架比较类似

    3.3 React ( virtual DOM )
    react和前面的框架不一样,因为它只是单纯的ui框架

    react组件没有scope的概念,虽然可以把state看作scope,但是react组件并不强制要定义state

    另外,react的实现与上面两者也不一样,它的处理逻辑如下图所示

    react组件根据输入:props【静态】& this.state【动态】

    输出一个virtual DOM 树,然后用它与原来的virtual DOM 树通过DIFF算法,找出它们的差异PATCHES

    最后,根据这些差异PATCHES再去执行UI更新

    React与AngularJs比较类似,都是在某些关键点(程序自己决定什么时候开始执行更新算法)

    AngularJs通过dirty check算法找到差异,并更新UI

    React则是通过virtual DOM的对比找到差异,然后更新UI

    React的UI更新策略包含了两种方式

    PATCHES有很多种类型

    它可以是简单的某个属性改变,比如text,class

    它也可以是复杂的整个子树的增删移动,这时就可以使用方式二,重新渲染整个子树

    详情可以参考react的Reconciliation算法

    3.4 那些我不知道的
    前端框架太多了,那些作者没看过的不做任何点评。。。

    4 考虑性能
    4.1 UI更新性能核心
    提起浏览器渲染机制这个高级话题,可能大多数同学只知道大概原理吧(其实作者也是的)

    大部分知道浏览器渲染的基本过程,然后还有repaint和reflow是什么即可

    但是其他呢?

    接下来需要介绍关于浏览器渲染机制的两个话题

    浏览器对渲染的优化
    浏览器UI渲染线程
    4.1.1 浏览器渲染机制的优化
    直接上一个测试代码就能说明这两个话题了

    var ul = document.getElementById('list');
    var e;
    var s = +new Date();
    for (var j = 0, l = 10000; j < l; ++j) {
    e = document.createElement('li');
    e.innerText = j;
    ul.appendChild(e);
    }
    console.log('>>> cost1:', +new Date() - s);

    // 到这句的时候,页面还是一片空白!
    s = +new Date();
    for (var k = 0, kl = 10000; k < kl; ++k) {
    e = document.createElement('li');
    e.innerText = kl;
    ul.appendChild(e);
    ul.offsetHeight; // 这句会引发浏览器渲染
    }
    console.log('>>> cost2:', +new Date() - s);
    // 直到js执行结束,页面才有内容出来!
    这段代码执行之后的结果如下

    可以看到,两个test case只相差了一句代码:ul.offsetHeight

    但是最后测出来的耗时差了1w倍

    原因是这一句代码影响了浏览器渲染机制的优化

    浏览器会缓存一些DOM操作,直到它必须要reflow为止

    一些读取元素的位置信息的代码就让浏览器立刻进行reflow,因为浏览器需要返回元素最新的位置信息

    这个test case也可以看到,reflow对性能的损耗有多大。。。

    另外还需要注意的,在第一个test case执行完了之后,页面还是一片空白,第一个test case插入的节点并没有展示出来

    即使执行了reflow,页面也没有展示UI

    直到js执行完才展示

    原因是reflow并不是就会执行UI渲染,UI渲染需要等待js执行完毕才会执行,可以理解为浏览器对js的执行和UI渲染都是同一个线程(虽然表现是这样,但是底层应该是js一个线程,UI渲染一个线程,只是浏览器只能执行一个线程)

    从上面的例子可以看到,浏览器每次计算reflow都会消耗很多性能,因此浏览器对这块做了优化

    浏览器的优化是浏览器会缓存一些DOM操作,直到以下两个条件之一才会进行真正的reflow

    浏览器必须要立刻进行reflow,比如上面test case展示的那样,浏览器需要返回元素最新的位置信息
    一段时间之后
    详见:Rendering: repaint, reflow/relayout, restyle

    4.1.2 浏览器原生事件循环
    从【2.1 前端的工作】中可以看到,用户对于前端页面的大部分交互都是通过事件

    实际上,浏览器在运行过程中,也有一个原生的事件循环

    当一个事件被触发,浏览器就会执行该事件的注册callbacks,这时浏览器就进入了js的context

    直到js执行完毕,浏览器就会执行UI更新线程,对新的UI改变进行渲染(如果有的话)

    上图是AngularJs解释$digest loop时的配图,很好的说明了浏览器的原生事件循环

    AngularJs提到$digest loop扩展了在js context里的过程

    实际上,$digest loop就是一个类似死循环的逻辑,直到dirty check执行完毕才退出

    因此,AngularJs保证了每次dirty check只有1次UI刷新

    那么图上面的$evalAsyncqueue是什么呢?

    实际上是需要在$digest loop异步执行的callback队列

    要知道平常js的异步callback是插入到浏览器原生的事件循环队列里面的,比如setTimeout等

    在AngularJs,如果需要在$digest loop里面执行异步callback

    就需要把callback放到$evalAsyncqueue里

    让异步callback可以在$digest loop内执行

    4.1.3 UI更新性能目标
    从前面两节可以看到

    reflow是在执行js的过程中执行的,它对性能有很大的影响

    而UI渲染是js执行之后才执行的,它对性能的消耗更加巨大

    因此,UI更新的性能目标有两个:

    减少reflow
    减少UI渲染次数
    4.2 为什么 virtual DOM 快?
    下面我们讨论一下为什么virtual DOM会比其他框架的UI更新(dirty check & setter)策略要快

    首先,使用defineProperty自动检测变化或者setter类型的就不参与讨论了,每次改属性都会进入绑定流程,想想都可怕

    剩下AngularJs和react,他们的更新逻辑的入口都是在关键点调用更新接口

    它们的共同点都是一次更新逻辑只会造成一次UI更新

    AngularJs通过类似死循环的$digest循环扩展浏览器的原生事件循环,所有更新逻辑都是在js中执行完

    react通过virtual DOM的diff得出改动,然后再统一的更新UI,这个过程也是一个js过程结束

    两者都有同样的特征:通过大量的js计算完成所有的DOM操作,结束之后才返回浏览器的UI渲染线程

    下面根据两者不同点来分析:

    AngularJs 的DOM操作是分布式的,DOM操作封装在watcher里面,每当有属性变更,就会触发watcher,然后执行DOM操作
    而react的DOM操作是集中式的,在diff之后,根据最终的patches执行DOM操作

    集中式的DOM操作可以最大限度的利用浏览器的优化机制,详见【4.1.1 浏览器渲染机制的优化】

    AngularJs 组件自带store,组件之间的互相影响可能会引起震荡
    具体的是当组件A的属性变化之后,对应watcher里面的操作导致了B组件的属性变化,这时就需要触发相对应的watcher,这个过程有可能无穷无尽

    另外AngularJs的dirty check是基于循环的,所以有可能watcher改变的是已经经过dirty check的store,因此dirty check要一直循环,直到所有的store都保持稳定,不再有任何新的变化,才能结束,当这个过程很长的时候,页面就会假死,因为浏览器不能执行UI更新,UI事件不能被处理,因为这个过程本身就在一个UI事件的处理期间,其他新的UI事件还在队列里面等着

    这个问题的根本原因是AngularJs不能很好的控制组件之间的store

    react没有这个问题就是因为react不是vm库,它没有store,看到这个估计大家都会傻眼,确实,AngularJs和react根本就不是一个可对比的库,本质都不一样

    react应用,不管是配合flux还是redux,他们都是先把store计算稳定之后,再交给react去更新UI,这整个过程并不会劫持浏览器的原生事件循环,因此不会有页面的假死现象出现

    另外,store计算完全是js计算,不会执行DOM的写操作,需要的只有甚至没有DOM的读操作,对于已经稳定的dom tree来说(浏览器的渲染队列里面已经没有缓存的DOM操作),批量的读操作是不会导致浏览器的repain和reflow的,因此store的计算过程会很快

    因此,结论:store的稳定计算很快,react本身渲染也很快,所以使用virtual DOM的react很快

    然后大家得出:virtual DOM很快

    本质上,需要做的工作都是一样的,只是react把store的计算分离出去而已,但这也正体现了react的内聚性

    另外还有一点也需要提及:

    AngularJs,vue,avalon等vm库,都是用watcher模式,watcher是长存的

    react是实时计算的,在diff之后,old tree就会被销毁,然后保留new tree作为下一次diff的old tree

    因此在内存占用方面,也是react有优势

    5 回到我的疑惑
    5.1 virtual DOM 的本质
    根据前面的讨论,我们得出virtual DOM的本质是

    根据稳定的输入【state & props】,通过js计算,得出UI更新语句序列

    稳定的输入,是指在js计算过程中,不接受新的输入
    如果在js计算过程中,需要改变输入源store,那么会通过另外的机制(事件机制)把这些改变放到下一个UI更新事件

    感兴趣的同学可以去试试,不过我们一般不会在virtual DOM计算过程中改变store,这也算是react的设计模式的约定之一

    通过js计算是指不会插入任何的DOM写操作语句
    得出UI更新的语句序列,在web是DOM写操作,在react native就是app的UI更新语句
    这也是virtual DOM的一大优势,在这里就不详述了

    5.2 riot 做了什么?
    riot主要解决react的两个痛点:

    jsx难以理解
    react库太大
    解决方案:

    参考web component组织html,js,css
    实现粗粒度的virtual DOM
    第一点就不多说了

    关于第二点,粗粒度的virtual DOM的意思是riot为每个组件创建一个tag对象

    tag对象保存了所有它里面的expressions,tag之间和dom tree一样的父子结构组织

    这种方式有点类似vm库,但是riot参考react,也有props(静态)和本身数据(动态),具有和react一样的输入

    检查更新的过程就是dirty check,但是和AngularJs的做法不同,riot只做一轮,它和react一样,没有sotre,因此没有watcher,也不需要等待store稳定

    至于输出,riot没有与react一样,UI更新语句序列也是分布式的

    最终得出的结论,riot的实现实际上就是react + angular,另外组件代码组织方式是参考Polymer

    正如riot官网上介绍的那样,riot是从已有的工具中提取精华

    6 结语
    本文主要讲解UI更新这个主题

    介绍了浏览器的UI更新相关的内容

    并介绍了几个比较流行的前端框架的设计核心

    同时讲解了这些设计核心在UI更新方面的分析

    实际上这些框架都是老生常谈的内容了

    但是通过UI更新这点来剖析这些框架的设计也是一件有趣的事情

    也让作者对这些框架有了更深的认识

    另外,这些框架的设计理念以及设计模式都非常值得回味

    如果有熟悉本文没有介绍到的框架的同学,可以分享出来供大家一起学习

    前端

  • 相关阅读:
    归并、希尔排序
    堆排序
    [模板] 最小树形图/朱刘算法
    [模板] 常系数线性递推
    [模板] Kruskal算法 && 克鲁斯卡尔重构树
    [模板] 斯特林数,性质以及求法
    这几天想干什么
    奇怪的 Markdown / LaTeX 笔记
    [模板] 各种莫队
    [模板] 2-SAT 问题
  • 原文地址:https://www.cnblogs.com/xiaocongcong888/p/9490850.html
Copyright © 2020-2023  润新知