2015-11-01 |

前端杂谈 - 前端组件化之路(二)

前端组件化之路(二)

接上篇:

前端杂谈 - 前端组件化之路(一)

7. 我们需要关注什么

目前来看,前端框架/库仍然处于混战期,可比中国历史上的春秋战国,百家齐放,作为跟随者来说,这是很痛苦的,因为无所适从,很可能你作为一个企业的前端架构师或者技术经理,需要做一些选型工作,但选哪个能保证几年后不被淘汰呢?基本没有。

虽然我们不知道将来什么框架会流行,但我们可以从一些细节方面去关注,某个具体的方面,将来会有什么,也可以了解一下在某个具体领域存在什么样的方案。一个完整的框架方案,无非是以下多个方面的综合。

7.1 模块化

这块还是不讲了,支付宝seajs还有百度ecomfe这两个团队的人应该都能比我讲得好得多。

7.2 Web Components

本文前面讨论过一些,也不深入了。

7.3 变更检测

我们知道,现代框架的一个特点是自动化,也就是把原有的一些手动操作提取。在前端编程中,最常见的代码是在干什么呢?读写数据和操作DOM。不少现代的框架/库都对这方面作了处理,比如说通过某种配置的方式,由框架自动添加一些关联,当数据变更的时候,把DOM进行相应修改,又比如,当DOM发生变动的时候,也更新对应的数据。

这个关联过程可能会用到几种技术。首先我们看怎么知道数据在变化,这里面有三种途径:

一、存取器的封装。这个的意思也就是对数据进行一层包装,比如:

前端杂谈 - 前端组件化之路(二)

这样,不允许用户直接调用data.name,而是调用对应的两个函数。Backbone就是通过这样的机制实现数据变动观测的,这种方式适用于几乎所有浏览器,缺点就是比较麻烦,要对每个数据进行包装。

这个机制在稍微新一点的浏览器中,也有另外一种实现方式,那就是defineProperty相关的一些方法,使用更优雅的存取器,这样外界可以不用调用函数,而是直接用data.name这样进行属性的读写。

国产框架avalon使用了这个机制,低版本IE中没有defineProperty,但在低版本IE中不止有JavaScript,还存在VBScript,那里面有存取器,所以他巧妙地使用了VBS做了这么一个兼容封装。

基于存取器的机制还有个麻烦,就是每次动态添加属性,都必须再添加对应的存取器,否则这个属性的变更就无法获取。

二、脏检测。

以Angular 1.x为代表的框架使用了脏检测来获知数据变更,这个机制的大致原理是:

保存数据的新旧值,每当有一些DOM或者网络、定时器之类的事件产生,用这个事件之后的数据去跟之前保存的数据进行比对,如果相同,就不触发界面刷新,否则就刷新。

这个方式的理念是,控制所有可能导致数据变更的来源(也就是各种事件),在他们可能对数据进行操作之后,判断新旧数据是否有变化,忽略所有中间变更,也就是说,如果你在同一个事件中,把某个数据任意修改了很多次,但最后改回来了,框架会认为你什么都没干,也就不会通知界面去刷新了。

不可否认的是,脏检测的效率是比较低的,主要是不能精确获知数据变更的影响,所以当数据量更大的情况下,浪费更严重,需要手动作一些优化。比如说一个很大的数组,生成了一个界面上的列表,当某个项选中的时候,改变颜色。在这种机制下,每次改变这个项的数据状态,就需要把所有的项都跟原来比较一遍,然后,还要再全部比较一次发现没有关联引起的变化了,才能对应刷新界面。

三、观察机制。

在ES7里面,引入了Object的observe方法,可以用于监控对象或数组的变动。

这是目前为止最合理的观测方案。这个机制很精确高效,比如说,连长跟士兵说,你去观察对面那个碉堡里面的动静。这个含义很复杂,包括什么呢?

  • 是不是加人了

  • 是不是有人离开了

  • 谁跟谁换岗了

  • 上面的旗子从太阳旗换成青天白日了

所谓观察机制,也就是观测对象属性的变更,数组元素的新增,移除,位置变更等等。我们先思考一下界面和数据的绑定,这本来就应当是一个外部的观察,你是数据,我是界面,你点头我微笑,你伸手我打人。这种绑定本来就应当是个松散关系,不应当因为要绑定,需要破坏原有的一些东西,所以很明显更合理。

除了数据的变动可以被观察,DOM也是可以的。但是目前绝大多数双向同步框架都是通过事件的方式把DOM变更同步到数据上。比如说,某个文本框绑定了一个对象的属性,那很可能,框架内部是监控了这个文本框的键盘输入、粘贴等相关事件,然后取值去往对象里写。

这么做可以解决大部分问题,但是如果你直接myInput.value="111",这个变更就没法获取了。这个不算大问题,因为在一个双向绑定框架中,一个既被监控,又手工赋值的东西,本身也比较怪,不过也有一些框架会尝试从HTMLInputELement的原型上去覆盖value赋值,尝试把这种东西也纳入框架管辖范围。

另外一个问题,那就是我们只考虑了特定元素的特定属性,可以通过事件获取变更,如何获得更广泛意义上的DOM变更?比如说,一般属性的变更,或者甚至子节点的增删?

DOM4引入了MutationObserver,用于实现这种变更的观测。在DOM和数据之间,是否需要这么复杂的观测与同步机制,目前尚无定论,但在整个前端开发逐步自动化的大趋势下,这也是一种值得尝试的东西。

复杂的关联监控容易导致预期之外的结果:

  • 慕容复要复国,每天读书练武,各种谋划

  • 王语嫣观察到了这种现象,认为表哥不爱自己了

  • 段誉看到神仙姐姐闷闷不乐,每天也茶饭不思

  • 镇南王妃心疼爱子,到处调查这件事的原委,意外发现段正淳还跟旧爱有联系

  • ……

总之这么下来,最后影响到哪里了都不知道,谁让丘处机路过牛家村呢?

所以,变更的关联监控是很复杂的一个体系,尤其是其中产生了闭环的时候。搭建整个这么一套东西,需要极其精密的设计,否则熟悉整套机制的人只要用特定场景轻轻一推就倒了。灵智上人虽然武功过人,接连碰到欧阳锋,周伯通,黄药师,全部都是上来就直接被抓了后颈要害,大致就是这意思。

polymer实现了一个observe-js,用于观测数组、对象和路径的变更,有兴趣的可以关注。

在有些框架,比如aurelia中,是混合使用了存取器和观察模式,把存取器作为观察模式的降级方案,在浏览器不支持observe的情况下使用。值得一提的是,在脏检测方式中,变更是合并后批量提交的,这一点常常被另外两种方案的使用者忽视。其实,即使用另外两种方式,也还是需要一个合并与批量提交过程。

怎么理解这个事情呢?数据的绑定,最终都是要体现到界面上的,对于界面来说,其实只关注你每一次操作所带来的数据变更的始终,并不需要关心中间过程。比如说,你写了这么一个循环,放在某个按钮的点击中:

前端杂谈 - 前端组件化之路(二)

界面有一个东西绑定到这个a,对框架来说,绝对不应当把中间过程直接应用到界面上,以刚才这个例子来说,合理的情况只应当存在一次对界面DOM的赋值,这个值就是对obj.a进行了10000次赋值之后的值。尽管用存取器或者观察模式,发现了对obj上a属性的这10000次赋值过程,这些赋值还是都必须被舍弃,否则就是很可怕的浪费。

React使用虚拟DOM来减少中间的DOM操作浪费,本质跟这个是一样的,界面只应当响应逻辑变更的结束状态,不应当响应中间状态。这样,如果有一个ul,其中的li绑定到一个1000元素的数组,当首次把这个数组绑定到这个ul上的时候,框架内部也是可以优化成一次DOM写入的,类似之前常用的那种DocumentFragment,或者是innerHTML一次写入整个字符串。在这个方面,所有优化良好的框架,内部实现机制都应当类似,在这种方案下,是否使用虚拟DOM,对性能的影响都是很小的。

7.4 Immutable Data

Immutable Data是函数式编程中的一个概念,在前端组件化框架中能起到一些很独特的作用。

它的大致理念是,任何一种赋值,都应当被转化成复制,不存在指向同一个地方的引用。比如说:

前端杂谈 - 前端组件化之路(二)

这个我们都知道,b跟a的内存地址是不一致的,简单类型的赋值会进行复制,所以a跟b不相等。但是:

前端杂谈 - 前端组件化之路(二)

这时候因为a和b指向相同的内存地址,所以只要修改了b的counter,a里面的counter也会跟着变。

Immutable Data的理念是,我能不能在这种赋值情况下,直接把原来的a完全复制一份给b,然后以后大家各自变各自的,互相不影响。光凭这么一句话,看不出它的用处,看例子:

对于全组件化的体系,不可避免会出现很多嵌套的组件。嵌套组件是一个很棘手的问题,在很多时候,是不太好处理的。嵌套组件所存在的问题主要在于生命周期的管理和数据的共享,很多已有方案的上下级组件之间都是存在数据共享的,但如果内外层存在共享数据,那么就会破坏组件的独立性,比如下面的一个列表控件:

前端杂谈 - 前端组件化之路(二)

我们在赋值的时候,一般是在外层整体赋值一个类似数组的数据,而不是自己挨个在每个列表项上赋值,不然就很麻烦。但是如果内外层持有相同的引用,对组件的封装性很不利。

比如在刚才这个例子里,假设数据源如下:

前端杂谈 - 前端组件化之路(二)通过类似这样的方式赋值给界面组件,并且由它在内部给每个子组件分别进行数据项的赋值:


前端杂谈 - 前端组件化之路(二)赋值之后会有怎样的结果呢?

前端杂谈 - 前端组件化之路(二)

这种方案里面,后面那几个log输出的结果都会是true,意思就是内层组件与外层共享数据,一旦内层组件对数据进行改变,外层中的也就改变了,这明显是违背组件的封装性的。

所以,有一些方案会引入Immutable Data的概念。在这些方案里,内外层组件的数据是不共享的,它们的引用不同,每个组件实际上是持有了自己的数据,然后引入了自动的赋值机制。

这时候再看看刚才那个例子,就会发现两层的职责很清晰:

  • 外层持有一个类似数组的东西arr,用于形成整个列表,但并不关注每条记录的细节

  • 内层持有某条记录,用于渲染列表项的界面

  • 在整个列表的形成过程中,list组件根据arr的数据长度,实例化若干个listitem,并且把arr中的各条数据赋值给对应的listitem,而这个赋值,就是immutable data起作用的地方,其实是把这条数据复制了一份给里面,而不是把外层这条记录的引用赋值进去。内层组件发现自己的数据改变之后,就去进行对应的渲染

  • 如果arr的条数变更了,外层监控这个数据,并且根据变更类型,添加或者删除某个列表项

  • 如果从外界改变了arr中某一条记录的内容,外层组件并不直接处理,而是给对应的内层进行了一次赋值

  • 如果列表项中的某个操作,改变了自身的值,它首先是把自己持有的数据进行改变,然后,再通过immutable data把数据往外同步一份,这样,外层组件中的数据也就更新了。

所以我们再看这个过程,真是非常清晰明了,而且内外层各司其职,互不干涉。这是非常有利于我们打造一个全组件化的大型Web应用的。各级组件之间存在比较松散的联系,而每个组件的内部则是封闭的,这正是我们所需要的结果。

说到这里,需要再提一个容易混淆的东西,比如下面这个例子:

前端杂谈 - 前端组件化之路(二)如果我们为了给inner-component做一些样式定位之类的事情,很可能在内外层组件之间再加一些额外的布局元素,比如变成这样:

前端杂谈 - 前端组件化之路(二)

这里中间多了一级div,也可能是若干级元素。如果有用过Angular 1.x的,可能会知道,假如这里面硬造一级作用域,搞个ng-if之类,就可能存在多级作用域的赋值问题。在上面这个例子里,如果在最外层赋值,数据就会是outer -> div -> inner这样,那么,从框架设计的角度,这两次赋值都应当是immutable的吗?

不是,第一次赋值是非immutable,第二次才需要是,immutable赋值应当仅存在于组件边界上,在组件内部不是特别有必要使用。刚才的例子里,依附于div的那层变量应当还是跟outer组件在同一层面,都属于outer组件的人民内部矛盾。

这里是facebook实现的immutable-js库

http://facebook.github.io/immutable-js/

7.5 Promise与异步

前端一般都习惯于用事件的方式处理异步,但很多时候纯逻辑的“串行化”场景下,这种方式会让逻辑很难阅读。在新的ES规范里,也有yield为代表的各种原生异步处理方案,但是这些方案仍然有很大的理解障碍,流行度有限,很大程度上会一直停留在基础较好的开发人员手中。尤其是在浏览器端,它的受众应该会比node里面还要狭窄。

前端里面,处理连续异步消息的最能被广泛接受的方案是promise,我这里并不讨论它的原理,也不讨论它在业务中的使用,而是要提一下它在组件化框架内部所能起到的作用。

现在已经没有哪个前端组件化框架可以不考虑异步加载问题了,因为,在前端这个领域,加载就是一个绕不过去的坎,必须有了加载,才能有执行过程。每个组件化框架都不能阻止自己的使用者规模膨胀,因此也应当在框架层面提出解决方案。

我们可能会动态配置路由,也可能在动态加载的路由中又引入新的组件,如何控制这些东西的生命周期,值得仔细斟酌,如果在框架层面全异步化,对于编程体验的一致性是有好处的。将各类接口都promise化,能够在可维护性和可扩展性上提供较多便利。

我们之前可能熟知XMLHTTP这样的通信接口,这个东西虽然被广为使用,但是在优雅性等方面,存在一些问题,所以最近出来了替代方案,那就是fetch。

在不支持的浏览器上,也有github实现的一个polyfill,虽然不全,但可以凑合用window.fetch polyfill

大家可以看到,fetch的接口就是基于promise的,这应当是前端开发人员最容易接受的方案了。

7.6 Isomorphic JavaScript

这个东西的意思是前后端同构的JavaScript,也就是说,比如一块界面,可以选择在前端渲染,也可以选择在后端渲染,值得关注,可以解决像seo之类的问题,但现在还不能处理很复杂的状况,持续关注吧。

8. 小结

很感谢能看到这里,以上这些是我近一年的一些思考总结。从技术选型的角度看,做大型Web应用的人会很痛苦,因为这是一个青黄不接的年代,目前已有的所有框架/库都存在不同程度的缺陷。当你向未来看去,发现它们都是需要被抛弃,或者被改造的,人最痛苦的是在知道很多东西不好,却又要从中选取一个来用。@严清 跟@寸志 @题叶讨论过这个问题,认为现在这个阶段的技术选型难做,不如等一阵,我完全赞同他们的观点。

选型是难,但是从学习的角度,可真的是挺好的时代,能学的东西太多了,我每天路上都在努力看有可能值得看的东西,可还是看不完,只能努力去跟上时代的步伐。

全文完,谢谢观赏!

发表评论

    评价:
    验证码: 点击我更换图片