同样是毕达哥拉斯树动画,同样是在2012年中的视膜屏 MacBook Pro 中。所有动画都使用 LICEcap 录制,使用常规设置,可以在 Chrome、Spotify、Emacs 中运行。点击 GIF 可以看到它的代码(预览处)。
— Angular 2 和 CycleJS 在 12 月 23 日加入 —
人们都想知道这个测试的重要性及其重要的原因。
快速跳转:React,Preact,Inferno,Vue,Angular 2,CycleJS,总结。
预览
很多 GIF 都能在它们的 GitHub 上找到链接。甚至可以复制库然后在本地运行,非常有趣。
实现者: 本文作者
实现者: Jason Miller,Preact 的创造者
实现者: Dominic Gannaway,Inferno 的创造者
实现者: Evan You,Vue 的创造者
实现者: Tero Parviainen,JavaScript 顾问
实现者: Wayne Maurer,Lambda IT 的创始人
感谢 Jason、Dominic 和 Evan 创建分支,同样感谢 Tero 和 Wayne 加入了他们自己的版本。如果有人能不使用框架,直接用原生 JavaScript 实现,那一定很酷。
让我们通过代码了解它是如何工作的。
React
在 Jason 和 Evan 的提示下,对鼠标事件进行节流能使 demo 更快。结果证明原始版本的动画树运行缓慢不是 React 自身的原因,而是每个刷新周期内太多的请求让渲染引擎不堪重负。
我尝试过对 requestAnimationFrame 进行节流,但是实际效果并不好。相比之下限制 React 重绘周期的方法就简单而有效。
onMouseMove(event) {
if (this.running) return;
this.running = true; // calculate stuff this.setState({
heightFactor: scaleFactor(y),
lean: scaleLean(x)
});
this.running = false;
}
先检查是否在进行更新,如果没有就手动更新。这个之所以生效是因为 React 的引擎是同步的。
要是没有 React Fiber,我觉得它可能会挂掉。¯\(ツ)/¯
Jason 用 preact-compat 层使得 Preact 看起来很像 React。这很有可能影响它的性能。
我喜欢 Preact 的示例是因为它使用异步渲染让效果更流畅。鼠标移动后,你能看到重绘周期滞后而产生的神奇效果。我很喜欢这效果。
代码实现:diff on github
在 package.json 中,他添加了 preact,preact-compat 和 React 库的 preact-compat 克隆,后者能让你不需要改变 imports。
他把无状态的 Pythagoras 功能组件转换成一个有状态的组件从而实现异步渲染。
// src/Pythagoras.jsexport default class {
render(props) {
return Pythagoras(props);
}
}
并启用去抖动异步渲染:
// src/index.js
import { options } from 'preact';
options.syncComponentUpdates = false;
//option 1: rIC + setTimeout fallback
let timer;
options.debounceRendering = f => {
clearTimeout(timer);
timer = setTimeout(f, 100);
requestIdleCallback(f);
};
我最喜欢 Preact 的部分是,它可以作为 React 的替代品,并且运行良好。从我目前的应用程序来看,它的性能优化会有不错的发展前景。
你可以用 Inferno 替代 React。Dominic 说这会影响性能,所以他就创建了新分支。你可以在 github 上比对差异。
Dominic 把所有相关的 react-scripts 改成 inferno-scripts。他同时也把 react 改成 inferno-beta36,这意味着我的CTO肯定不允许我在生产环境中使用它。
从上面看出,它最主要的是各种导入的改变——React 变成 Inferno,还把许多类方法改成绑定箭头函数。我不知道这是出于风格的选择还是 Inferno 的需要。
他也把基于字符串引用改成基于回调引用,Inferno 因为性能的原因不能使用基于字符串引用。取而代之,我们可以用 D3 来检测 SVG 上的鼠标位置。这比起我们自己弄简单很多。
// src/App.js
class App extends Component {
// ...
svgElemeRef = (domNode) => {
this.svgElement = domNode;
}
// ...
render() {
// ..
}
在 Pythagoras 核心组件上,他添加两个 Inferno 特殊属性:noNormalize 和 hasNonKeyedChildren.
从八天前的 issue 知道,noNormalize 是提高性能的一个基准, hasNonKeyedChildren 的作用尚不明确。我猜想这两个属性都是用来为虚拟 DOM diffing 算法优化性能。
虽然我不是 Angular 的粉丝,但我得承认它的确很好使用。
我不知道为什么,也许 TypeScript 的那些类型检查在转译后增加了运行时额外开销?
显然代码是重写了,Tero 需要将代码迁移到 TypeScript,这真厉害,我可做不到。
我很好奇,编程语言的隔阂会怎样影响你在网上找的随机库的可重用性。
代码看起来似乎包含很多文件。而 App 是分成 app.module.ts,app.component.ts,app.component.html 和 app.component.css 几个文件。和 Pythagoras 一样。
当你看到 Angular 的 html 文件时,意味着 Angular 坚持了一个文件对应一种语言的传统。
<div class="App-header">
<h2>This is a dancing Pythagoras tree</h2>
</div>
<p class="App-intro">
<svg #svg
[attr.width]="width"
[attr.height]="height"
style="border: 1px solid lightgray">
<g app-pythagoras
[w]="baseW"
[heightFactor]="heightFactor"
[lean]="lean"
[x]="width / 2 - 40"
[y]="height - baseW"
[lvl]="0"
[maxlvl]="currentMax" />
</svg>
</p>
这在 HTML 中看起来很有趣。
我对模块和组件之间区别的还没有完全理解。似乎模块定义了某些确定的引入,子组件等。但是每个组件仍然会定义它自己的 CSS 和 模板导入。
也因为我对这一块内容的了解不够深入,在此不对其用例的好处做过多描述。
这在我的设备上显示很流畅,可能是因为我刚刚看完 Angular 版本的演示。
Wayne 把所有东西都转译成 TypeScript,但似乎不是 CycleJS 要求的那样。尽管如此,他能保持与原始文件相同的简单结构,这一点深得人心。
在此我无法详细说明 Wayne 因 TypeScript 和 CycleJS 做的一些改变,他没有使用类定义 CycleJS 组件,其结构更像是在学校学的闭包结构。
export function App(sources: Sources): Sinks {
const factorAndLean$ = sources.DOM.select('#the-svg') //...
const args$ = xs.combine(factorAndLean$, xs.periodic(500) //...
const pythagoras$ = Pythagoras(args$);
const vtree$ = pythagoras$.map(x =>
div(Styles.App, [ // ...
return {
DOM: vtree$
};
}
这需要时间适应。
我不喜欢的部分是 Wayne 在 CycleJS 中指定 DOM 的方式。主程序看起来像是这样:
div(Styles.App, [
div(Styles.AppHeader, [
img(Styles.AppLogo, { attrs: { src: 'cyclejs_logo.svg' } }),
h2('This is a dancing Pythagoras tree')
]),
p(Styles.AppIntro, [
svg('#the-svg', { attrs: { height: svgDimensions.height, width: svgDimensions.width, style: 'border: 1px solid lightgray' } }, [ x ])
])
])
这似乎与使用 React.createElement 方法非常类似,但可读性较差。 CycleJS 确实支持 JSX,但作者并没有调用它,这一点让我纳闷。
编辑于12月24日:正如 @spion 指出的,结果证明 CycleJS 示例仅渲染 2^10 个矩形。由于 .take 工作方式的不同,相比其他示例多了近一半。这对 jankiness 有着巨大的影响。
@spion @dan_abramov @waynemaurer @andrestaltz 刚刚检查, document.getElementsByTagName('rect').length 确是只返回 1024 。
— Swizec (@Swizec) 12. 24, 2016
编辑2:修正本地副本中的代码,更新 gif 图,效果非常平滑。我对此印象非常深刻。
总结
它们看起来都很平滑,但我不知道谁是最快的,可能是 VUE,也可能是 Inferno。我比较喜欢 Preact,因为它可以异步渲染,非常酷,CycleJS 也给我留下了极为深刻的印象,但我仍然对 Angular 持有偏见。在我用过的框架中,口碑好也的确好用的要算 React 了。