既生AngularJs何生ReactJs

勿忘初心2018-10-26 11:44

此文已由作者杨介正授权网易云社区发布。

欢迎访问网易云社区,了解更多网易技术产品运营经验。



angularJs如此优秀的框架为啥一下子就被以reactJs为代表凭借virtual Dom为技术核心的众框架打败。抱着这个疑问,查阅了很多相关文章,结合自己的大胆的臆测和试验,得出了一些结论。我把自己的结论分享在这篇文章里,希望大家一起交流学习,也欢迎各位同学指正错误的地方,毕竟有很多纯属个人理解和臆测。


1.一些基础知识
为了便于说明,我这里先简单的介绍一些基础知识,都是个人理解,很多专业词汇我直接用英文单词了,因为那样更形象,翻译成中文有时候倒说不出那个味道了。
1.1 数据与结构分离
大家或多或少都经历过手持jquery及扩展上打80岁老太,下擒5岁幼儿园小朋友的年代。就像它的口号:write less, do more,jquery是个可以让你写很少的代码就能实现强大功能的基础库。可惜,jquery并没有实现数据和结构的分离。那么什么是数据与结构相分离呢?举个简单的例子:对文章点赞, 文章 的点赞数量加1。一般的实现:ajax到后台,在回调函数里执行$('.commitCount').text(ret.count)。这里,数据处理完成之后,直接通过jquery的选择器获取到Dom,
对dom进行操作,这就是所谓的数据和结构耦合在了一起。在业务处理逻辑里通过getElementById, getElementsByTagName, childNodes, parentNode等结构查找获取到dom并直接操作dom的,都可以叫做数据和结构耦合了。这样的坏处就是,有天某人为了优化页面样式一言不合就把className commitCount给改了,导致了业务逻辑出错了。又或是,同样的功能不同的className,需要到js代码里各种改className。又或是。。。就不一一举了,总之,维护性太差。那么如何将数据和结构分离呢,即不通过结构(className, id, tagname,parentNode等)查找就能获取到dom,或者说直接让dom的操作对用户不可见。一种方法是通过template(如underscore的template, nej的jst,handbar等)技术,将数据表达式直接写在template里,通过解析template的方法将数据写入。第二种方法就是找到一种不通过结构查找就能得到dom的方法,比如在上个例子中,如果把回调函数改成$(element).text(ret.count)这样,也称数据与结构分离了,因为我们没有通过结构就获取到了dom。那么如何得到element呢?框架的作用来了,aunglarjs通过directive得到,reactjs通过virtual dom得到。具体怎么得到,下面再分析。


1.2 js操作dom的渲染原理
js包括ECMAScript、DOM、BOM,不能独立运行,类似于java需要运行在jre里一样,js需要运行在浏览器或者其他解析器里。关于js操作dom的过程,还是直接看例子吧
var
    dom = document.getElementById('test');

dom.style.color = 'green';
上面的代码的目的是改变id为test的dom的文本颜色为绿色:
1.通过document.getElementById的方法获取到dom,这里要注意,这里属于ECMAScript操作Dom,跟纯ECMAScript操作a = b + c是不一样的噢。类似java操作系统API,这里需要上下文的切换。但不同的是,通过试验,切换的速度那是相当的快,几百纳秒级别,大家可以忽略这里的开销了。有兴趣的同学,可以深入研究研究,做做试验。
2.通过style改变dom的样式。这里要注意,浏览器此时不会马上去reflow(或者layout,也就是计算布局),等到js代码已经执行完毕或者接下来的js代码出现需要获取某个dom的reflow后的样式(比如offsetWidth)才会开始reflow。repaint只会在js代码执行完毕了(空闲状态)才开始。
3.dom的渲染(reflow、repaint)和js的执行是同步的,我不确定他们是否在一个线程里,但可以肯定的是,js执行的时候,渲染等待,渲染的时候,js等待。
好了,下面开始分析既生“瑜(ang u lar)”何生“亮( r eact) ”了。

2.AngularJs的工作原理及性能瓶颈
先来看一下angular的工作流

简单的说就是directives作为dom的attributes,这样directives就保留了dom的引用。directives在scope里监听数据变化,当数据改变时,就执行相应处理函数。详细的说,请看例子
<div  ng-controller="ctrl">
<h2 ng-bind="title"></h2>
<div ng-bind="content"></div>
<div>
<span ng-bind="praise"></span>
<a ng-click="praiseHandler">赞</a>
</div>
</div>


这颗dom tree用于显示一篇文章,并允许读者对文章点赞。html片段里总共有5个angular的内建的directives:1个ng-controller,3个ng-bind,1个ng-click。下面在看一下js代码

//ng-bind简易代码
Angular.directive('ng-bind', {
  
  link: function(element, attr, scope){
   //获取绑定数据名称(title/content/praise)
    var key = attr.ngBind;
    scope.$watch(key, function(nVal, oVal){
      element.innerText = nVal;
    })
  }  

});

//controller简易代码
Angular.controller('ctrl', function($scope){  
  $scope.title = '标题';
  $scope.content = 'hello world!';
  $scope.praiseHandler = function(){    
     //ajax...
    $scope.praise = ret.count; 
   
  }

})
Angular.compile(document.getElementsByClassName('article')[0])($rootScope);//一个简易的编译
通过compile方法,就可以将内建的和自定义的directives和dom相结合起来了,是不是很巧妙,也很简单。angular对Dom的操作,都隐藏在了directive里,每个directive都会保留dom的引用,同时可以监听scope里数据的变化。而数据的操作,则放在controller(你也可以叫它config或者defined。因为其主要的作用是初始化scope里的数据和方法)。这样,就实现了数据和结构分离了。看起来很完美对不对,问题就出现在scope的数据监听过程。angular对数据监听的算法是通过循环遍历scope实现的,scope之间又是一颗树的关系,scope和scope之间又会相互影响,angular会一遍又一遍的重复遍历整颗scope树,直到整颗scope树里的数据不发生变化了才停止。为什么用这种算法以及优化方法,我会在最后总结的时候给出。这种算法对于数据量小、不是很复杂的scope树并没有问题,但对于facebook这样的产品,显然是不适用的。人家一个脸可能有几万条评论,不仅数据量大,还掺杂着各种互动。因此,virtual dom来了,一种更快的算法。

3.ReactJs的工作原理
很多人都说,virtual dom可以减少对real dom的操作,将操作累积起来一次性dump进real dom里,从而提高性能。以我个人的理解来说,这是不对的。虽然real dom 的操作有上下文的切换,相对virtual dom 体积也更大,但以现在浏览器上下文切换的速度和hash map的速度加上浏览器自身的各种优化处理,我想不会有人为了这点开销而去建这么大一颗虚拟树然后再进行各种diff。virtual dom之所以会被引用进js里(其他领域也有这个概念),主要是为了实现一种更高效、更彻底的数据和结构分离的开发模式:react用virtual dom保留dom的引用,用diff快速对比出state发生变化的virtual dom,然后将变化dump进real dom。为什么说更彻底,react virtual dom没有提供dom选择器的功能,尽可能的杜绝了数据和结构耦合的可能性。相反,如果你用angular的directive开发组件,你有时候会不得不去获取子节点或者父节点。从这点上说,virtual dom真的好适合做组件化。

接下来,同样先来看下reactjs的工作流
react通过virtual dom将数据监听函数注册在restore里,当dispatcher发来一个action,restore监听到有数据发生变化时,相比较angular反复的查看scope里数据是否稳定,react的处理更为简单粗暴,一次处理完直接走人(所以也称单向数据流)。这带来的坏处就是,为了能让数据间的变化增加互动(一个数据的变化引起其他数据跟着变化等),相比较angular的一劳永逸,在react里你可能需要手动往restore里注册更多的监听函数。virtual dom处理完监听函数之后,这时就进入了diff阶段,最后就是将patch dump进real dom tree。

4.总结
angularjs和reactjs一样,都是要实现数据和结构分离的框架,各有各的优势。先说说angular,它的优势就是结构给人更加MVC的感觉,数据绑定得益于它的循环算法,一劳永逸。directive作为attribute,可以运用到任何dom上。比如你想给一些button、a、img在点击后加上加载中的loading功能,只需要写一个loadingDirective,然后作为attribute分别分配给它们就好了,非常简单。这个给react去实现的话,就稍微复杂一点,你可能要写个mixin,然后分别注入到button类,a类,img类中。angular的劣势就是它的scope dirty check算法。反复的检查,在大型数据结构中真滴是很慢。那么有没有一种改进的方法呢,我想出了一种。scope的dirty check之所以需要从头到脚反复检查,主要原因是scope事先不知道哪些值发生了变化,因为用户都是通过scope.a = 1的方式去改变scope里的数据的。那么,有没有办法可以让scope事先就知道是哪些值发生了变化,然后直接调用其监听函数呢。有,通过函数赋值的方式。不要直接scope.a = 1,而是通过类似scope.set('a', 1)的方式进行赋值,这样scope就能直接记录下a这个key发生了变化。react中需要调用方法setState来改变状态,我猜也有类似的原因,这里不深讲了,欢饮各位感兴趣的同学与我私下交流。react的优势不用多说了吧,快、准、狠,virtual dom的组件化方案能够实现数据和结构的完全分离,这点我比较喜欢。它的劣势就是,没有正儿八经的mvc模式,事件、结构、数据全在一起,干啥事都得写个组件,我只想显示一行hello world呀!纯js,你能看到html文件和浏览器html parser生无可恋的表情吗?不管对于不对,希望这篇文章能给大伙带来一丝丝帮助和灵感。好了,整篇文章充满了个人理解与臆测,恭迎各位同学指正,说好不许打脸!

网易云免费体验馆,0成本体验20+款云产品! 

更多网易技术、产品、运营经验分享请点击