debounce
1 | import isObject from './isObject.js' |
throttle
1 | import debounce from './debounce.js' |
Stay foolish, stay humble.
1 | import isObject from './isObject.js' |
1 | import debounce from './debounce.js' |
1 | import React, { Component } from 'react'; |
1 | import React, { Component } from 'react'; |
1 | /* eslint-disable no-console */ |
github地址。
调用了 rc-notification
模块。
1 | /* global Promise */ |
事件循环是Node.js处理非阻塞I/O操作的机制——尽管Javascript是单线程处理的——当有可能的时候,它们会把操作转移到系统内核中区。
既然目前大多数内核都是多线程的,它们可在后台处理多种操作。当其中的一个操作完成的时候,内核通知Node.js将适合的回调函数添加到轮询队列中等待时机执行。我们在本文后面会进行详细介绍。
当Node.js启动后,它会初始化事件轮询;处理已提供的输入脚本(或丢入REPL
,本文不涉及到),它可能会调用一些异步的API函数调用,安排任务处理事件,或者调用process.nextTick()
,然后开始处理事件循环。
下面的图表显示了事件循环的概述以及操作顺序。
1 | ┌───────────────────────────┐ |
注意:每个框框里每一步都是事件循环机制的一个阶段
每个阶段都有一个FIFO队列来执行回调。虽然每个阶段都是特殊的,但通常情况下,当事件循环进入给定的阶段时,它将执行特定于该阶段的任何操作,然后在该阶段的队列中执行回调,直到队列用尽或最大回调数已执行。当该队列已用尽或达到回调限制,事件循环将移动到下一阶段,等等。
由于这些操作中的任何一个都可能计划更多的操作,并且在轮询阶段处理的新事件由内核排队,因此在处理轮询事件时,轮询事件可以排队。因此,长时间运行回调可以允许轮询阶段运行大量长于计时器的阈值。有关详细信息,请参阅计时器和轮询部分。
注意 :在Windows和Unix/Linux实现之间存在细微的差异,但这对演示来说并不重要。最重要的部分在这里。实际上由七或八个步骤,但我们关心但是Node.js实际上使用以上的某些步骤。
setTimeout()
和setInterval()
的回调函数setImmediate()
排定的之外),其余情况node将在此处阻塞。setImmediate()
回调函数在这里执行。socket.on('close', ...)
。在每次运行的事件循环之间,Node.js检查它是否在等待任何异步I/O或计时器,如果没有的话,则关闭干净。
定时器指定可执行所提供回调的阈值,而不是用户希望其执行的确切时间。计时器回调将尽可能早地运行,因为它们可以在指定的时间间隔后进行调度。但是操作系统调度或其它调度的运行可能会延迟它们。
注意: 轮询阶段控制何时定时器执行。
例如,假设计划在100毫秒后执行超时阈值,然后脚本开始异步读取文件,这需要95毫秒:
1 | const fs = require('fs'); |
当事件循环进入轮询阶段时,它有一个空队列(此时fs.readFile()
尚未完成),因此它将等待毫秒数,直到达到最快的计时器阈值为止。当它等待95毫秒通过时,fs.readFile()
完成读取文件,它需要10毫秒完成的回调将添加到轮询队列中并执行。当回调完成时,队列中不再有回调,因此事件循环将看到已达到最快计时器的阈值,然后将回滚到计时器阶段,以执行定时器的回调。在本实例中,将看到计划中的定时器和执行的回调之间的总延迟将为105毫秒。
注意: 为了防止 轮询 阶段饿死事件循环,libuv(实现Node.js事件循环和平台的所有异步行为的C函数库),在停止轮询以获得更多事件之前,还有一个最大的(系统依赖)。
轮询阶段有两个重要的功能:
当事件循环进入轮询阶段且没有计划计时器时,将发生以下两种情况之一:
setImmediate()
排定,则事件循环将结束轮询阶段,并继续检查阶段以执行这些计划脚本。setImmediate()
排定,则事件循环将等待回调添加到队列中,然后立即执行。一旦轮询队列唯恐,事件循环将检查已达到时间阈值的计时器。如果一个或多个计时器已准备就绪,则事件循环将绕回计时器阶段以执行这些计时器的回调。
此阶段允许人员在轮询阶段完成后立即执行回调。如果轮询阶段变为空闲状态,并且脚本已排队使用setImmediate()
,则事件循环可以继续到检查阶段而不是等待。
setImmediate()
实际上是一个在事件循环的单独阶段运行的特殊计时器。它使用一个libuv API来安排回调在轮询阶段完成后执行。
通常,在执行代码时,事件循环最终会命中轮询阶段,等待传入连接、请求等。但是,如果回调已计划为setImmediate()
,并且轮询阶段变为空闲状态,则它将结束并继续到检查阶段而不是等待轮询事件。
如果套接字或处理函数突然关闭(例如socket.destroy())
,则'close'
事件将在这个阶段发出。否则它将通过process.nextTick()
发出。
setImmediate()
对比setTimeout()
setImmediate()
和setTimeout()
很类似,但何时调用行为完全不同。
setImmediate()
设计为在当前轮询阶段完成后执行脚本。setTimeout()
计划在毫秒的最小阈值经过后运行的脚本。执行计时器但顺序将根据调用它们的上下文而异。如果二者都从主模块内调用,则计时将首进程性能的约束(这可能会受到计算机上运行的其它应用程序的影响)。
例如,如果运行的是不属于I/O周期(即主模块)的以下脚本,则执行两个计时器的顺序是非确定性的,因为它受进程性能的约束:
1 | // timeout_vs_immediate.js |
1 | $ node timeout_vs_immediate.js |
但是,如果你把这两个函数放入一个I/O循环内调用,setImmediate总是被优先调用:
1 | // timeout_vs_immediate.js |
1 | $ node timeout_vs_immediate.js |
使用setImmediate()
超过setTimeout()
的主要优点是setImmediate()
在任何计时器(如果在 I/O 周期内)都将始终执行,而不依赖于存在多少个计时器。
process.nextTick()
process.nextTick()
您可能已经注意到process.nextTick()
在关系图中没有显示,即使它是异步API的一部分。这是因为process.nextTick()
在技术上不是事件循环的一部分。相反,无论事件循环的当前阶段如何,都将在当前操作完成后处理nextTickQueue
。这里的一个操作被视作为一个从 C++ 底层处理开始过渡,并且处理需要执行的 JavaScript 代码。
回顾我们的关系图,任何时候在给定的阶段中调用process.nextTick()
,所有传递到process.nextTick()
的回调将在事件循环继续之前得到解决。这可能会造成一些糟糕的情况, 因为它允许您通过进行递归process.nextTick()
来“饿死”您的I/O调用,阻止事件循环到达轮询阶段。
为什么这样的事情会包含在 Node.js 中?它的一部分是一个设计理念,其中 API 应该始终是异步的,即使它不必是。以此代码段为例:
1 | function apiCall(arg, callback) { |
代码段进行参数检查。如果不正确,则会将错误传递给回调函数。最近对API进行了更新,允许将参数传递给process.nextTick()
,允许它在回调后传递任何参数作为回调的参数传播,这样就不必嵌套函数了。
我们正在做的是将错误传递给用户,但仅在我们允许用户的其余代码执行之后。通过使用process.nextTick()
,我们保证apiCall()
始终在用户代码的其余部分之后运行其回调函数,并在允许事件循环之前继续进行。为了实现这一点,JS调用栈被允许展开,然后立即执行提供的回调,允许进行递归调用process.nextTick()
,而不达到RangeError: 超过 v8 的最大调用堆栈大小
。
这种哲学可能会导致一些潜在的问题。 以此代码段为例:
1 | let bar; |
用户将someAsyncApiCall()
定义为具有异步签名,但实际上它是同步运行的。当调用它时,提供给someAsyncApiCall()
的回调在同一阶段调用事件循环,因为someAsyncApiCall()
实际上并没有异步执行任何事情。因此,回调尝试引用 bar,即使它在范围内可能还没有该变量,因为脚本无法运行到完成。
通过将回调置于process.nextTick()
中,脚本仍具有运行完成的能力,允许在调用回调之前初始化所有变量、函数等。它还具有不允许事件循环继续的优点。在允许事件循环继续之前,对用户发出错误警报可能很有用。下面是使用process.nextTick()
的上一个示例:
1 | let bar; |
这又是另外一个真实的例子:
1 | const server = net.createServer(() => {}).listen(8080); |
只有端口通过时,端口才会立即被绑定。之后可以立即调用'listening'
回调。问题是.on('listening')
回调在这个时候还没有被设置。
为了绕过此现象,'listening'
事件在nextTick()
中排队,以允许脚本运行到完成阶段。这允许用户设置所需的任何事件处理程序。
process.nextTick()
对比setImmediate()
就用户而言我们有两个类似的调用,但它们的名称令人费解。
process.nextTick()
在同一个阶段立即执行。setImmediate()
在以下迭代或 ‘tick’ 上触发事件循环。实质上,这两个名称应该交换,因为process.nextTick()
比setImmediate()
触发得更直接,但这是过去遗留问题,因此不太可能改变。如果贸然进行名称交换,将破坏 npm 上的大部分软件包。每天都有新的模块在不断增长,这意味着我们我们每等待一天,就有更多的潜在破损在发生。所以说尽管这些名称使人感到困惑,但它们的名字本身不会改变。
我们建议开发人员在所有情况下都使用setImmediate()
,因为它更容易被推理(并且它导致代码与更广泛的环境,如浏览器 JS 所兼容。)
主要有两个原因:
一个例子就是要符合用户的期望。简单示例:
1 | const server = net.createServer(); |
假设listen()
在事件循环开始时运行,但侦听回调被放置在setImmediate()
中。除非通过主机名,否则将立即绑定到端口。为使事件循环继续进行,它必须命中轮询阶段,这意味着可能会收到连接,从而允许在侦听事件之前激发连接事件。
另一个示例运行的函数构造函数是从EventEmitter
继承的,它想调用构造函数:
1 | const EventEmitter = require('events'); |
不能立即从构造函数中发出事件。因为脚本不会处理到用户为该事件分配回调的点。因此,在构造函数本身中可以使用process.nextTick()
来设置回调,以便在构造函数完成后发出该事件,从而提供预期的结果:
1 | const EventEmitter = require('events'); |
GPU实际上可以看作一个独立的计算机,它有自己的处理器和存储器及数据处理模型。当浏览器向GPU发送消息的时候,就像向一个外部设备发送消息。
你可以把浏览器向GPU发送数据的过程,与使用ajax向服务器发送消息非常类似。想一下,你用ajax向服务器发送数据,服务器是不会直接接受浏览器的存储的信息的。你需要收集页面上的数据,把它们放进一个载体里面(例如JSON),然后发送数据到远程服务器。
同样的,浏览器向GPU发送数据也需要先创建一个载体;只不过GPU距离CPU很近,不会像远程服务器那样可能几千里那么远。但是对于远程服务器,2秒的延迟是可以接受的;但是对于GPU,几毫秒的延迟都会造成动画的卡顿。
浏览器向GPU发送的数据载体是什么样?这里给出一个简单的制作载体,并把它们发送到GPU的过程。
所以你可以看到,每次当你添加transform:translateZ(0)或will-change:transform给一个元素,你都会做同样的工作。重绘是非常消耗性能的,在这里它尤其缓慢。在大多数情况,浏览器不能增量重绘。它不得不重绘先前被复合层覆盖的区域。
一个或多个没有自己复合层的元素要出现在有复合层元素的上方,它就会拥有自己的复合层;这种情况被称为隐式合成。
浏览器将元素提升为一个复合层有很多种原因,下面列举了一些:
translate3d
,translateZ
等等(js一般通过这种方式,使元素获得复合层)<video><iframe><canvas><webgl>
等元素opacity
和transform
做 CSS 动画will-change
属性position:fixed
z-index
较低且包含一个复合层的兄弟元素(换句话说就是该元素在复合层上面渲染)这看起来css动画的性能瓶颈是在重绘上,但是真实的问题是在内存上:
使用GPU动画需要发送多张渲染层的图像给GPU,GPU也需要缓存它们以便于后续动画的使用。
一个渲染层,需要多少内存占用?为了便于理解,举一个简单的例子;一个宽、高都是300px
的纯色图像需要多少内存?
300 * 300 * 4 = 360000
字节,即360kb。这里乘以4是因为,每个像素需要四个字节计算机内存来描述。
假设我们做一个轮播图组件,轮播图有10张图片;为了实现图片间平滑过渡的交互;为每个图像添加了will-change:transform
。这将提升图像为复合层,它将多需要19MB的空间。800 * 600 * 4 * 10 = 1920000
。
仅仅是一个轮播图组件就需要19MB的额外空间!
在chrome的开发者工具中打开Setting
——>Experiments
——>layers
可以看到每个层的内存占用。
现在我们可以总结一下GPU动画的优点和缺点:
缺点:
保持动画的对象的z-index
尽可能的高。理想的,这些元素应该是body元素的直接子元素。当然,这不是总可能的。所以你可以克隆一个元素,把它放在body元素下仅仅是为了做动画。
将元素上设置will-change
CSS属性,元素上有了这个属性,浏览器会提升这个元素成为一个复合层(不是总是)。这样动画就可以平滑的开始和结束。但是不要滥用这个属性,否则会大大增加内存消耗。
transform
和opacity
对于图片,你要怎么做呢?你可以将图片的尺寸减少5%—10%,然后使用scale
将它们放大;用户不会看到什么区别,但是你可以减少大量的存储空间。
css动画有一个重要的特性,它是完全工作在GPU上。因为你声明了一个动画如何开始和如何结束,浏览器会在动画开始前准备好所有需要的指令;并把它们发送给GPU。而如果使用js动画,浏览器必须计算每一帧的状态;为了保证平滑的动画,我们必须在浏览器主线程计算新状态;把它们发送给GPU至少60次每秒。除了计算和发送数据比css动画要慢,主线程的负载也会影响动画; 当主线程的计算任务过多时,会造成动画的延迟、卡顿。
所以尽可能地使用基于css的动画,不仅仅更快;也不会被大量的js计算所阻塞。
一般情况下,主线程负责:
相应地,合成线程负责:
当用户滚动页面时,合成线程会通知主线程更新页面中最新可见部分到位图。但是,如果主线程响应慢,合成线程不会等待,而是马上绘制已经生成到位图,还没准备好的部分用白色进行填充。
快在于:
慢在于:
因此,修改元素的height要比修改transform属性性能消耗大。这是因为对height的修改需要不断的relayout、repaint,这两个步骤的计算量可能是巨大的。而transform属性不会更改元素或它周围的元素的布局。transform属性会对元素的整体产生影响,它会对整个元素进行缩放、旋转、移动处理。
以下CSS属性在动画处理方面是比较快的:
用循环语句迭代数据时,必须要初始化一个变量来记录每一次迭代在数据集合中的位置,迭代器的使用可以极大的简化数据操作。
迭代器是一种特殊的对象,具有专门的接口,所有迭代器对象都有一个next方法,每次调用都返回一个结果对象。
结果对象有两个属性:一个是value
,表示下一个将要返回的值;另一个是done
,是一个布尔值,当没有更多可返回数据时返回true。
迭代器还会保存一个内部指针,用来指向当前集合中值的位置,没调用一次next()
方法,都会返回下一个可用的值。
如果最后一个值返回后再调用next()
方法,返回的对象中属性done
为true,value
则包含迭代器最终返回的值,这个返回值不是数据集的一部分,与函数的返回值类似,是函数调用过程中最后一次给调用者传递信息的方法,如果没有相关数据则返回undefined
。
1 | // ES5 语法创建一个迭代器 |
生成器是一种返回迭代器的函数,通过function
关键字后的星号(*
)来表示,函数中会用到新的关键字yield
。星号可以紧挨着function
关键字,也可以在中间添加一个空格:
1 | function *createIterator() { |
yield
关键字也是ES6的新特性,可以通过它来指定调用迭代器的next()
方法时的返回值及返回顺序。
生成器函数最有序的部分大概是:每当执行完一条yield
语句后函数就会自动停止执行,直到再次调用迭代器的next()
方法才会继续执行下一个yield
语句。
使用yield
关键字可以返回任何值或表达式,因此可以通过生成器函数批量的给迭代器添加元素。
1 | function *createIterator(items) { |
yield关键字只可在生成器内部使用,在其它地方使用会导致程序抛出语法错误,即便在生成器内部的函数里使用也是如此。常见案例:
1 | function *createIterator(items) { |
for-of
循环可迭代对象具有Symbol.iterator
属性,是一种与迭代器密切相关的对象。Symbol.iterator
通过指定的函数可以返回一个作用于附属对象的迭代器。在ES6中,所有的集合对象(数组、Set
集合及Map
集合)和字符串都是可迭代对象,这些对象中都有默认的迭代器。
for-of
循环每执行一次都会调用可迭代对象的next()
方法,并将迭代器返回的结果对象的value
属性存储在一个变量中,循环将持续执行这一过程直到返回对象的done
属性的值为true。
可以通过Symbol.iterator
来访问对象默认的迭代器。
由于具有Symbol.iterator
属性的对象都有默认的迭代器,因此可以用它来检测对象是否为可迭代对象:
1 | function isIterable(object) { |
默认情况下,开发者定义的对象都是不可迭代对象,但如果给Symbol.iterator
属性添加一个生成器,则可以将其变为可迭代对象:
1 | let collection = { |
在ES6中有3中类型但集合对象:数组、Map
集合与Set
集合。为了更好的访问对象中的内容,这3种对象都内建来三种迭代器:
由于方括号操作的是编码单元而非字符,因此无法正确访问双字节字符。由于双字节字符被视作两个独立的编码单元,在使用方括号获取双字节字符时得到的是两个空。
所幸,ES6的目标是全面支持Unicode,并且我们可以通过改变字符串的默认迭代器来解决这个问题,使其操作字符而不是编码单元。
NodeList
迭代器自从ES6添加了默认迭代器后,DOM定义中的NodeList
类型(定义在HTML标准而不是ES6标准中)也拥有了默认迭代器,其行为与数组的默认迭代器完全一致。所以可以将NodeList
应用于for-of
循环及其他支持对象默认迭代器的地方。
由于展开运算符可以作用于任意可迭代对象,因此如果想将可迭代对象转换为数组,这是最简单的方法。你既可以将字符串中的每一个字符(不是编码单元)存入新数组中,也可以将浏览器中的NodeList
对象中的每一个节点存入新数组中。
1 | let set = new Set([1, 2, 3]), |
使用React处理数据相对来说比较容易,因为React的设计就是把数据当作状态。但是当你需要处理的数据量很大的时候,麻烦就来了。比如你要处理一个包含500-1000条记录的数据集,这会产生巨大的计算量并导致性能问题。下面我们将学习如何使用虚拟列表来“看起来”渲染了一个长列表。
我们将使用React Virtualized组件来实现我们的需求。它让我们可以以很小的代价渲染大集合数据。
React Virtualized官方已经有很详细的介绍了,可以去它们的github去看看。
我们需要大量的数据,下面我们就来造一些。
1 | function createRecord(count) { |
下面,我们设置一个数字来创造我们需要的数据:
const records = createRecord(1000);
好了,现在我们有需要渲染的数据了。
这里是我们创建的一个列表,我们引入使用了库提供的一些展示样式,本篇post不讨论这个。
现在开始感受一下这个demo,速度超级快,是不是?
你可能想知道这背后到底发生了什么,结果发现是一系列很疯狂和酷的sizing、positioning、transform和transitions,是这些技术让一条条记录进入/离开可视区。数据都在那里并渲染了,React Virtualized创建了一个window,当用户scroll的时候,一条条记录将滑入/出我们的视野。
为了渲染虚拟列表,我们需要List
组件,它内部渲染了一个Grid
组件。
首先,我们从设置rowRenderer
,开始,它是负责渲染单挑数据的组件。
1 | rowRenderer = ({ index, isScrolling, key, style }) => { |
它返回一个包含两个div
的div,里面的两个
div分别是
username和
email`。可以看出,这是一个简单的展示用户信息的列表。
rowRenderer
接受几个参数,下面是这些参数的细节:
index
: 记录的数值IDisScrolling
: 代表List
组件是否发生scrollingisVisible
: 代表这条数据是否在可视区内key
: 这条记录在数组中的位置parent
: 定义这个列表是否是另一个列表的parent/childstyle
: 定位这条数据的style对象下面我们再深入了解一些rowRenderer
函数,我们把它放到List
组件中:
1 | <List |
你可能注意到这里的几个参数:
rowCount
: 接收代表列表长度的数字width
: 列表的宽度height
: 列表的高度rowHeight
: 每条数据的高度rowRenderer
: 用来渲染每条数据的模板,我们将传入之前定义的rowRenderer
函数overscanRowCount
:最后,代码应该是这样的:
1 | const { List } = ReactVirtualized |
文档里介绍,cell
measure是一个高阶组件,用来暂时渲染列表。现在我们还看不到它,但数据已经在里面被处理并准备好展示了。
什么时候我们需要关心cell measure?最常见的use
case是当我们需要动态计算rowHeight
的时候。React
Virtualized在渲染每一行的时候,会缓存它们的高度值,这样当数据滑出可视区的时候我们也不用再计算它的高度——不管里面的内容是什么,高度都是对的。
首先,我们创建自己的缓存cache
,在我们组件的constructor
里用CellMeasureCache
:
1 | constructor() { |
当我们设置List
组件的时候,把cache
带上:
1 | <List |
传给deferredMeasurementCache
的值会被用来暂时渲染数据,接着——当rowHeight
的计算结果出来的时候——额外的行会流入,就像它们一直在那里。
接着我们将在rowRenderer
函数里使用CellMeasure
替换我们之前的div
:
1 | rowRenderer = ({ index, parent, key, style }) => { |
现在数据已经被获取、缓存并准备好展示在虚拟window里了!
虽然本片post主要说列表,但万一当我们需要渲染table怎么办?React
Virtualized也帮我们做了这件事情。这时我们需要使用Table
和Column
组件。
下面是代码:
1 | class App extends React.Component { |
这个Table
组件包括下面的参数:
width
height
headerHeight
rowHeight
rowCount
rowGetter
: 返回这行的数据如果看一下Column
组件,你会发现我们设置了一个dataKey
参数。它是每条数据拥有的独一无二的id。
希望这篇post可以帮你了解React
Virtualized可以做哪些事情,它如何让列表渲染变得很快,并如何在项目中使用它。
我们只讨论了皮毛,这个库覆盖了更多的use case,如在scroll的时候为记录generate
placeholders、实时获取/缓存数据的无限加载组件等等。
它将给你很多可以play with的东西!
此外,这个包维护的很好,实际上你可以加入Slack group来跟踪这个项目,贡献它,和其他folks取得联系。
还有一条值得注意的是,React Virtualized在StackOverflow上有它自己的标签,这是一个寻找问题的答案的地方,也可以po出你的问题。
周震南,2000年的小弟弟,今年6月才19岁,但真的很酷,我完全被圈粉了。
在创造营2019的第二期,和一个老前辈battle,唱的小星星,那段rap真的太酷了。
创造营vlog-周震南,就是酷酷的,到了切苹果的时候,才暴露出18岁小孩子的一面,无厘头和幼稚。但还是透着酷。
作为队长,对队员的表现不满意,连续两天练习到凌晨五、六点。有队员对这样的练习计划表示质疑,太累了,效率很低,不如早点休息,然后第二天早点开始。周震南回应很简单:“谁都不想练到五六点,为什么练到五六点,不是因为还没练好吗?练完了大家都可以回去睡觉。”这里表达的是唯结果论,几点睡有什么关系,达不到目标就不要休息。
第一次公演结束,投票结果周震南队赢了,接着就是周震南啜泣的声音。为什么哭了?不知道,只有自己知道。我觉得他应该是压力太大了。自己对自己要求很高,同时其他人对他要求也很高,公演练习的过程很辛苦,最终的结果是好的,让他觉得付出有了收获。
00后都已经这么努力了。