曾经想知道如果播放一个由 2048 个 SVG 节点组成的动画,使用哪个前端框架会更流畅?这里有一些 GIF 动画展示。
同样是 毕达哥拉斯树动画 ,同样是在2012年中的视膜屏 MacBook Pro 中。所有动画都使用 LICEcap 录制,使用常规设置,可以在 Chrome、Spotify、Emacs 中运行。点击 GIF 可以看到它的代码。
— Angular 2 和 CycleJS 在 12 月 23 日加入 —
人们都想知道这个测试的重要性及其重要的原因。
很多 GIF 都能在它们的 GitHub 上找到链接。甚至可以复制库然后在本地运行,非常有趣。
实现者: 本文作者
实现者: Jason Miller,Preact 的创造者
实现者: Dominic Gannaway,Inferno 的创造者
实现者: Evan You,Vue 的创造者
实现者: Tero Parviainen,JavaScript 顾问
实现者: Wayne Maurer,Lambda IT 的创始人
感谢 Jason、Dominic 和 Evan 创建分支,同样感谢 Tero 和 Wayne 加入了他们自己的版本。如果有人能不使用框架,直接用原生 JavaScript 实现,那一定很酷。
让我们通过 代码 了解它是如何工作的。
在 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 算法优化性能。
这项工作的工作量比较大,是由Evan和树的创作者Phan An 完成的。
Vue 并没有打算模仿 React 的 API,它拥有自己的一套代码。我想通过 github 展示它们之间的差异,但这比较繁琐。我建议你去 Github 自行查看 。
你可以识别出 Pythagoras 核心组件 。Evan 使用了 transform-vue-jsx,使得 JSX 能在 Vue 里使用。
main.app 文件 在此,你可以点击查看然后理解其代码。
让我们来试一下吧。
把它分成 <template>,<script> 和 <style> 三个部分。这看起来有点像 JSX 或者 HTML,但实际上是模板带上了冒号前缀。
Vue 似乎采用了 React 的 put-it-all-together 组件化提示,但按语言拆分。虽然这看起来很简洁,但按照我以往的经验,实际操作其实很麻烦。
App 组件还是跟过去类似,但它使用 data() 来定义默认的状态,用 $refs 代替 this.refs,用一个 name 属性代替了命名类本身,components 属性定义子节点,用 methods 属性来定义类方法。
虽然我不是 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$ }; }
这需要时间适应。