收集一些观点

461 阅读10分钟

严格的模板引擎的定义,输入模板字符串 + 数据,得到渲染过的字符串。实现上,从正则替换到拼 function 字符串到正经的 AST 解析各种各样,但从定义上来说都是差不多的。字符串渲染的性能其实也就在后端比较有意义,毕竟每一次渲染都是在消耗服务器资源,但在前端,用户只有一个,几十毫秒的渲染时间跟请求延迟比起来根本不算瓶颈。倒是前端的后续更新是字符串模板引擎的软肋,因为用渲染出来的字符串整个替换 innerHTML 是一个效率很低的更新方式。所以这样的模板引擎如今在纯前端情境下已经不再是好的选择,意义更多是在于方便前后端共用模板。

相比之下 Angular 是 DOM-based templating,直接解析 live DOM 来提取绑定,如果是字符串模板则是先转化成 live DOM 再解析。数据更新的时候直接通过绑定做局部更新。其他 MVVM 如 Knockout, Vue, Avalon 同理。缺点是没有现成的服务端渲染,要做服务端渲染基本等于重写一个字符串模板引擎。不过其实也不难,因为 DOM-based 的模板都是合法的 HTML,直接用现成的 HTML parser 预处理一下,后面的工作就相对简单了。

框架里面也有在前端解析字符串模板到静态 AST 再生成 live DOM 做局部更新的,比如 Ractive 和 Regular。这一类的实现因为解析到 AST 的这步已经在框架内部完成了,所以做服务端渲染几乎是现成的,另外也可以在构建时进行预编译。

然后说说 React,JSX 根本就不是模板,它就是带语法糖的手写 AST,并且把语法糖的处理放到了构建阶段。因为运行时不需要解析,所以 virtual DOM 可以每次渲染都重新生成整个 AST,在客户端用 diff + patch,在服务端则直接 serialize 成字符串。所有其他 virtual DOM 类方案同理。像 virtual-dom,mithril 之类的连语法糖都不带。

最后说下我的看法:如果是静态内容为主,那就直接服务端渲染好了,首屏加载速度快。如果是动态的应用界面,那就不应该用拼模板的思路去做,而是用做应用的架构(MV*,组件树)思路去做。

3. MVVM vs. Virtual DOM

相比起 React,其他 MVVM 系框架比如 Angular, Knockout 以及 Vue、Avalon 采用的都是数据绑定:通过 Directive/Binding 对象,观察数据变化并保留对实际 DOM 元素的引用,当有数据变化时进行对应的操作。MVVM 的变化检查是数据层面的,而 React 的检查是 DOM 结构层面的。MVVM 的性能也根据变动检测的实现原理有所不同:Angular 的脏检查使得任何变动都有固定的 O(watcher count) 的代价;Knockout/Vue/Avalon 都采用了依赖收集,在 js 和 DOM 层面都是 O(change)
  • 脏检查:scope digest O(watcher count) + 必要 DOM 更新 O(DOM change)
  • 依赖收集:重新收集依赖 O(data change) + 必要 DOM 更新 O(DOM change)

可以看到,Angular 最不效率的地方在于任何小变动都有的和 watcher 数量相关的性能代价。但是!当所有数据都变了的时候,Angular 其实并不吃亏。依赖收集在初始化和数据变化的时候都需要重新收集依赖,这个代价在小量更新的时候几乎可以忽略,但在数据量庞大的时候也会产生一定的消耗。

MVVM 渲染列表的时候,由于每一行都有自己的数据作用域,所以通常都是每一行有一个对应的 ViewModel 实例,或者是一个稍微轻量一些的利用原型继承的 "scope" 对象,但也有一定的代价。所以,MVVM 列表渲染的初始化几乎一定比 React 慢,因为创建 ViewModel / scope 实例比起 Virtual DOM 来说要昂贵很多。这里所有 MVVM 实现的一个共同问题就是在列表渲染的数据源变动时,尤其是当数据是全新的对象时,如何有效地复用已经创建的 ViewModel 实例和 DOM 元素。假如没有任何复用方面的优化,由于数据是 “全新” 的,MVVM 实际上需要销毁之前的所有实例,重新创建所有实例,最后再进行一次渲染!这就是为什么题目里链接的 angular/knockout 实现都相对比较慢。相比之下,React 的变动检查由于是 DOM 结构层面的,即使是全新的数据,只要最后渲染结果没变,那么就不需要做无用功。

Angular 和 Vue 都提供了列表重绘的优化机制,也就是 “提示” 框架如何有效地复用实例和 DOM 元素。比如数据库里的同一个对象,在两次前端 API 调用里面会成为不同的对象,但是它们依然有一样的 uid。这时候你就可以提示 track by uid 来让 Angular 知道,这两个对象其实是同一份数据。那么原来这份数据对应的实例和 DOM 元素都可以复用,只需要更新变动了的部分。或者,你也可以直接 track by $index 来进行 “原地复用”:直接根据在数组里的位置进行复用。在题目给出的例子里,如果 angular 实现加上 track by $index 的话,后续重绘是不会比 React 慢多少的。甚至在 dbmonster 测试中,Angular 和 Vue 用了 track by $index 以后都比 React 快: dbmon (注意 Angular 默认版本无优化,优化过的在下面)

顺道说一句,React 渲染列表的时候也需要提供 key 这个特殊 prop,本质上和 track-by 是一回事。

4. 性能比较也要看场合

在比较性能的时候,要分清楚初始渲染、小量数据更新、大量数据更新这些不同的场合。Virtual DOM、脏检查 MVVM、数据收集 MVVM 在不同场合各有不同的表现和不同的优化需求。Virtual DOM 为了提升小量数据更新时的性能,也需要针对性的优化,比如 shouldComponentUpdate 或是 immutable data。

  • 初始渲染:Virtual DOM > 脏检查 >= 依赖收集
  • 小量数据更新:依赖收集 >> Virtual DOM + 优化 > 脏检查(无法优化) > Virtual DOM 无优化
  • 大量数据更新:脏检查 + 优化 >= 依赖收集 + 优化 > Virtual DOM(无法/无需优化)>> MVVM 无优化

不要天真地以为 Virtual DOM 就是快,diff 不是免费的,batching 么 MVVM 也能做,而且最终 patch 的时候还不是要用原生 API。在我看来 Virtual DOM 真正的价值从来都不是性能,而是它 1) 为函数式的 UI 编程方式打开了大门;2) 可以渲染到 DOM 以外的 backend,比如 ReactNative。

5. 总结

以上这些比较,更多的是对于框架开发研究者提供一些参考。主流的框架 + 合理的优化,足以应对绝大部分应用的性能需求。如果是对性能有极致需求的特殊情况,其实应该牺牲一些可维护性采取手动优化:比如 Atom 编辑器在文件渲染的实现上放弃了 React 而采用了自己实现的 tile-based rendering;又比如在移动端需要 DOM-pooling 的虚拟滚动,不需要考虑顺序变化,可以绕过框架的内置实现自己搞一个。


从风格上来说 new 不是必要的,但是在主流的 JS 引擎里 new 出来的对象更容易优化,因为 constructor + prototype 可以映射到 hidden class。实际应用中 new 出来的对象相比 Object.create() 可能会有数倍的性能差异。


任何情况下你问『我们应不应该用框架 X 换掉框架 Y』,这都不是单纯的比较 X 和 Y 的问题,而得先问以下问题:

1. 现有的项目已经开发了多久?代码量多大? 2. 现有的项目是否已经投入生产环境? 3. 现有的项目是否遇到了框架相关的问题,比如开发效率、可维护性、性能?换框架是否能解决这些问题?

(1) 事关替换的成本,(2) 事关替换的风险,(3) 事关替换的收益。把这些具体信息放在台面上比较,才有可能得出一个相对靠谱的结论。

--- (1) 跟 (2) 要具体情况具体分析,所以就不谈了。至于 (3),以下是 Vue 有而 ko 没有的:

更好的性能,CLI,Webpack 深度整合,热更新,模板预编译,中文文档,官方路由方案,官方大规模状态管理方案,服务端渲染,render function / JSX 支持,Chrome 开发者插件,更多的社区组件和教程,尤其是中文内容。

这里没有什么说 ko 不好的意思。作为前端 mvvm 的鼻祖,ko 对 Vue 的设计显然有很多启发,但是今天的 Vue 在各方面都实实在在地比 ko 强。如果上新项目,我想不出什么继续用 ko 的理由。


  • 在 main.js 开头加上
    #!/usr/bin/env node
    

  • 在 package.json 里面添加
    "bin": {
      "mytool": "main.js"
    }
    

  • 如果你只是想本地使用,运行 npm link(相当于将一个本地包 install -g)
  • mytool 可以直接作为命令使用了。


  • 什么是DOM

    牢记:站高一个维度去理解问题 !


    为了理解DOM,我们至少需要站在浏览器的角度来思考。


    DOM概念本身很简单,请先完全跟着我的思路来:

    1. 普通文档(*.txt)和HTML/XML文档(*.html/*.xml)的区别仅仅是因为后者是有组织的结构化文件;
    2. 浏览器将结构化的文档以树的数据结构读入浏览器内存,并将每个树的子节点定义为一个NODE(想象这颗树,从根节点到叶子节点都被建模为一个NODE对象);
    3. 这每个节点(NODE)都有自己的属性(名称、类型、内容...);
    4. NODE之间有层级关系(parents、child、sibling...);
    5. 以上已经完成文档的建模工作(将文档内容以树形结构写入内存),此时再编写一些方法来操作节点(属性和位置信息),即为NODE API。

    抽象一下:

    • DOM是一种将HTML/XML文档组织成对象模型建模过程
    • DOM建模重点在于如何解析HTML/XML文档和开放符合DOM接口规范的节点操作API接口

    再抽象一下:

    • 解析文档,建模成对象模型,开放API接口。

    最后:

    • DOM:Document Object Model 文档对象模型


    再回顾下整个过程,每个步骤都可以问自己几个问题,比如:DOM到底是建模过程,还是最后建的那个模型,还是指操作节点的API接口呢,还是...?


    以上是站在浏览器的角度思考DOM,你还可以站在浏览器设计人员、网页编码人员等角度考虑:

    • DOM跟JavaScript什么关系?
      • DOM很显然诞生在浏览器,一开始是用JS实现的;
      • 但随着DOM本身的发展,已经形成规范,你可以用任何一种语言比如Python来解析文档,生成对像树,只要满足DOM标准,包括开放标准的操作接口,那你实现的就是一个DOM。
    • DOM开放的接口如何操作?
      • JS原生接口使用。
      • JQuery高纬度封装如何使用。
    • ...

    链接:https://www.zhihu.com/question/34219998/answer/268568438