博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
收集一些观点
阅读量:5908 次
发布时间:2019-06-19

本文共 6425 字,大约阅读时间需要 21 分钟。

严格的模板引擎的定义,输入模板字符串 + 数据,得到渲染过的字符串。实现上,从正则替换到拼 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 快: (注意 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 的理由。

假设我们有一个数组,每个元素是一个人。你面前站了一排人。
foreach 就是你按顺序一个一个跟他们做点什么,具体做什么,随便:
people.forEach(function (dude) {  dude.pickUpSoap();});复制代码
map 就是你手里拿一个盒子(一个新的数组),一个一个叫他们把钱包扔进去。结束的时候你获得了一个新的数组,里面是大家的钱包,钱包的顺序和人的顺序一一对应。
var wallets = people.map(function (dude) {  return dude.wallet;});复制代码
reduce 就是你拿着钱包,一个一个数过去看里面有多少钱啊?每检查一个,你就和前面的总和加一起来。这样结束的时候你就知道大家总共有多少钱了。
var totalMoney = wallets.reduce(function (countedMoney, wallet) {  return countedMoney + wallet.money;}, 0);复制代码
补充一个 filter 的:
你一个个钱包数过去的时候,里面钱少于 100 块的不要(留在原来的盒子里),多于 100 块的丢到一个新的盒子里。这样结束的时候你又有了一个新的数组,里面是所有钱多于 100 块的钱包:
var fatWallets = wallets.filter(function (wallet) {  return wallet.money > 100;});复制代码
最后要说明一点这个类比和实际代码的一个区别,那就是 map 和 filter 都是 immutable methods,也就是说它们只会返回一个新数组,而不会改变原来的那个数。

要说简单,async 是最简单的,只是在 callback 上加了一些语法糖而已。在不是很复杂的用例下够用了,前提是你已经习惯了 callback 风格的写法。

then.js 上手也是比较简单的,因为也是基于 callback 和 continuation passing,并不引入额外的概念,比起 async,链式 API 更流畅,个人挺喜欢的。我挺久以前写过一个在 Node 里面跑 shell 命令的小工具,思路差不多:

Callback-based 方案的最大问题在于异常处理,每个 callback 都得额外接受一个异常参数,发生异常就得一个一个往后传,异常发生后的定位很麻烦。

ES6 Promise, Q, Bluebird 核心都是 Promise,缺点嘛就是必须引入这个新概念并且要用就得所有的地方都用 Promise。对于 Node 的原生 API,需要进行二次封装。Q 和 Bluebird 都是在实现 Promise A+ 标准的基础上提供了一些封装和帮助方法,比如 Promise.map 来进行并行操作等等。Promise 的一个问题就是性能,而 Bluebird 号称速度是所有 Promise 库里最快的。ES6 Promise 则是把 Promise 的包括进 js 标准库里,这样你就不需要依赖第三方实现了。

关于 Promise 能够如何改进异步流程,建议阅读:

co 是 TJ 大神基于 ES6 generator 的异步解决方案。要理解 co 你得先理解 ES6 generator,这里就不赘述了。co 最大的好处就是能让你把异步的代码流程用同步的方式写出来,并且可以用 try/catch:

co(function *(){  try {    var res = yield get('http://badhost.invalid');    console.log(res);  } catch(e) {    console.log(e.code) // ENOTFOUND }})()复制代码

但用 co 的一个代价是 yield 后面的函数必须返回一个 Thunk 或者一个 Promise,对于现有的 API 也得进行一定程度的二次封装。另外,由于 ES6 generator 的支持情况,并不是所以地方都能用。想用的话有两个选择:

1. 用支持 ES6 generator 的引擎。比如 Node 0.11+ 开启 --harmony flag,或者直接上 iojs;

2. 用预编译器。比如 Babel () , Traceur () 或是 Regenerator () 把带有 generator 的 ES6 代码编译成 ES5 代码。

(延伸阅读:基于 ES6 generator 还可以模拟 go 风格的、基于 channel 的异步协作:)

但是 generator 的本意毕竟是为了可以在循环过程中 yield 进程的控制权,用 yield 来表示 “等待异步返回的值” 始终不太直观。因此 ES7 中可能会包含类似 C# 的 async/await :

async function showStuff () {  var data = await loadData() // loadData 返回一个 Promise  console.log(data) // data 已经加载完毕}async function () {  await showStuff() // async 函数默认返回一个 Promise, 所以可以 await 另一个 async 函数  // 这里 showStuff 已经执行完毕}复制代码

可以看到,和用 co 写出来的代码很像,但语意上更清晰。因为本质上 ES7 async/await 就是基于 Promise + generator 的一套语法糖。深入阅读:

想要今天就用 ES7 async/await 也是可以的!Babel 的话可以用配套的 asyncToGenerator transform:

Traceur 和 Regenerator 对其也已经有实验性的支持了。

在 main.js 开头加上
#!/usr/bin/env node复制代码
在 package.json 里面添加
"bin": {  "mytool": "main.js"}复制代码
如果你只是想本地使用,运行 npm link(相当于将一个本地包 install -g)mytool 可以直接作为命令使用了。

转载地址:http://clvpx.baihongyu.com/

你可能感兴趣的文章
Linux的经典shell命令整理
查看>>
我的友情链接
查看>>
克隆的CentOS6.6系统,网卡显示不存在的解决方法
查看>>
Python基础:编码规范(4)
查看>>
Git分支介绍
查看>>
计算机账户SID重复的谣传
查看>>
linuxapache ssl设置及部分功能示例
查看>>
[作业] 马哥2016全新Linux+Python高端运维班第六周作业
查看>>
vs2010自动展开选中当前代码所在的文件位置的功能
查看>>
Exchange 2016 邮件在队列中不能发送
查看>>
glusterfs隐藏参数-使用详解
查看>>
Linux学习之LVM文件系统
查看>>
我的友情链接
查看>>
JAVA NIO服务器间连续发送文件(本地测试版)
查看>>
Python之禅---2、python介绍和应用场景介绍
查看>>
手把手教你安装Zabbix3.2开源监控系统
查看>>
MySQL基础面试题
查看>>
perl 与sqlite 摘自扶凯
查看>>
Android中Toast的常用使用方式总结
查看>>
CCNP学习之路之NTP SERVER
查看>>