React
先申明本系列基于 React 版本 16.8.6
V16 Lifecycle
Lifecycle Map
Usage
class ExampleComponent extends React.Component {
// 用于初始化 state
constructor() {}
// 用于替换 `componentWillReceiveProps` ,该函数会在初始化和 `update` 时被调用
// 因为该函数是静态函数,所以取不到 `this`
// 如果需要对比 `prevProps` 需要单独在 `state` 中维护
static getDerivedStateFromProps(nextProps, prevState) {}
// 判断是否需要更新组件,多用于组件性能优化
shouldComponentUpdate(nextProps, nextState) {}
// 组件挂载后调用
// 可以在该函数中进行请求或者订阅
componentDidMount() {}
// 用于获得最新的 DOM 数据
getSnapshotBeforeUpdate() {}
// 组件即将销毁
// 可以在此处移除订阅,定时器等等
componentWillUnmount() {}
// 组件销毁后调用
componentDidUnMount() {}
// 组件更新后调用
componentDidUpdate() {}
// 渲染组件函数
render() {}
// 以下函数不建议使用
UNSAFE_componentWillMount() {}
UNSAFE_componentWillUpdate(nextProps, nextState) {}
UNSAFE_componentWillReceiveProps(nextProps) {}
}
Mounting
在该阶段包含生命周期函数
- constructor()
- static getDerivedStateFromProps()
- render()
- componentDidMount()
constructor()
构造函数的作用有两个 一个通过分配对象来初始化本地状态this.state,另一个是将事件处理程序方法绑定到实例。 在构造函数中不用使用this.setState
constructor(props) {
super(props);
// Don't call this.setState() here!
this.state = {
counter: 0
};
this.handleClick = this.handleClick.bind(this);
}
static getDerivedStateFromProps()
static getDerivedStateFromProps(props, state)
getDerivedStateFromProps在调用render方法之前调用,无论是在初始安装还是后续更新。它会返回一个对象去更新状态,或者返回null不更新任何东西
该生命周期是在16.3版本中新增的,当props或者state改变都会触发改生命周期,与这个相似的UNSAFE_componentWillReceiveProps()生命周期在之后的版本将会逐渐被替代,避免使用
render()
render()方法是类组件中唯一必需的方法。并且它是一个纯函数,意味着不会修改组件状态,每次调用时都返回相同的结果,并且它不直接与浏览器交互。 调用时它会校验this.state和this.props, 然后返回下列的几种类型的返回值
- React elements
- Arrays and fragments
- Portals
- String and numbers
- Booleans or null
不能使用this.setState在该生命周期
componentDidMount()
在该生命周期中可以进行dom的操作和数据的网络请求
Updating
- static getDerivedStateFromProps()
- shouldComponentUpdate()
- render()
- getSnapshotBeforeUpdate()
- componentDidUpdate()
- UNSAFE_componentWillUpdate()
- UNSAFE_componentWillReceiveProps()
shouldComponentUpdate()
shouldComponentUpdate(nextProps, nextState)
shouldComponentUpdate 在接受到新的props和新的state的 在渲染之前会调用 默认的是返回true。该方法不会在初始的时候和使用forceUpdate()方法的时候调用。
在该生命周期中,可以进行性能的优化。也可以使用继承PureComponent组件,该组件已经对shouldComponentUpdate做了处理但是是浅比较。例如 state中有数组和对象时,你改变state的数组和对象它可能不会更新,不会深入的比较数组和对象。此时可以引入immutable.js进行结合使用。
getSnapshotBeforeUpdate()
getSnapshotBeforeUpdate(prevProps, prevState)
在该生命周期中 state 已经更新,可以进行一些dom 操作,在render更新之前
componentDidUpdate()
componentDidUpdate(prevProps, prevState, snapshot)
componentDidUpdate()更新发生后立即调用。初始渲染不会调用此方法。 该生命周期你也可以去操作dom,或者进行网络请求,当你发现props改变时。但是不能使用直接setState那样会导致无限循环,你可以再某种判断条件下使用。 如果组件使用了 getSnapshotBeforeUpdate()生命周期,则它返回的值将作为第三个“快照”参数传递给componentDidUpdate()。否则此参数将是未定义的。
UNSAFE_componentWillUpdate()
UNSAFE_componentWillUpdate(nextProps, nextState)
此生命周期之前已命名componentWillUpdate。该名称将继续有效,直到版本17. 使用rename-unsafe-lifecyclescodemod自动更新组件。 UNSAFE_componentWillUpdate()在收到新的props或state时,在渲染之前调用。使用此作为在更新发生之前执行准备的机会。初始渲染不会调用此方法 不能再此使用this.setState
UNSAFE_componentWillReceiveProps()
UNSAFE_componentWillReceiveProps(nextProps)
此生命周期之前已命名componentWillReceiveProps。该名称将继续有效,直到版本17. 使用rename-unsafe-lifecyclescodemod自动更新组件。
该生命周期在初始化的时候不会被调用,只有当props被改变的时候会被调用, this.setState不会触发它
Unmounting
- componentWillUnmount()
componentWillUnmount()
componentWillUnmount()在卸载和销毁组件之前立即调用。在此方法中执行任何必要的清理,例如使计时器无效,取消网络请求或清除在componentDidMount()其中创建的任何订阅。
不能调用setState(),componentWillUnmount()因为组件永远不会被重新呈现。卸载组件实例后,将永远不会再次mount它。
Error Handling
- static getDerivedStateFromError()
- componentDidCatch()
static getDerivedStateFromError()
static getDerivedStateFromError(error)
在子组件抛出错误后会调用此生命周期。它接收作为参数抛出的错误,并返回值以更新状态。 在组件 “render” 阶段的时候就会被调用,不允许副作用
componentDidCatch()
componentDidCatch(error, info)
在子组件抛出错误的时候回调用此生命周期,它有2个参数,一个是错误,还有一个是对象,key对应的是错误来自哪个子组件。 该生命周期在 “ commit” 阶段调用所以可以有副作用
Finally
16 版本新增的生命周期
- static getDerivedStateFromProps()
- getSnapshotBeforeUpdate()
- static getDerivedStateFromError()
- componentDidCatch()
16 版本废除和减少使用的生命周期
- UNSAFE_componentWillUpdate()
- UNSAFE_componentWillReceiveProps()
- UNSAFE_componentWillMount()
this.setState 不能调用的生命周期
- constructor()
- render()
- componentDidUpdate() 不能直接使用
- UNSAFE_componentWillUpdate()
- UNSAFE_componentWillMount()
Advanced Guides
Lazy and Suspense
React.lazy 接受一个函数,这个函数需要动态调用 import()。它必须返回一个 Promise,该 Promise 需要 resolve 一个 default export 的 React 组件。
然后应在 Suspense 组件中渲染 lazy 组件,如此使得我们可以使用在等待加载 lazy 组件时做优雅降级(如 loading 指示器等)。
React.lazy 目前只支持默认导出(default exports)
示例:
import React, { Suspense } from 'react';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent />
</Suspense>
</div>
);
}
fallback 属性接受任何在组件加载过程中你想展示的 React 元素。你可以将 Suspense 组件置于懒加载组件之上的任何位置。你甚至可以用一个 Suspense 组件包裹多个懒加载组件。
总结:使用该组方法可以实现代码的动态加载 更好的进行代码分割
Context
Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。
Context使用场景
当一个父组件需要给子组件传递一个props时,但是嵌套层数比较多时,比如4-5层,那去维护这个props就显得复杂。那么就可以使用context来共享这些数据。
如何使用
API
- React.createContext
- Context.Provider
- Class.contextType
- Context.Consumer
- Context.displayName
React.createContext
const MyContext = React.createContext(defaultValue);
创建一个 Context 对象。当 React 渲染一个订阅了这个 Context 对象的组件,这个组件会从组件树中离自身最近的那个匹配的 Provider 中读取到当前的 context 值。
只有当组件所处的树中没有匹配到 Provider 时,其 defaultValue 参数才会生效。
注意:将 undefined 传递给 Provider 的 value 时,消费组件的 defaultValue 不会生效。
Context. Provider
<MyContext.Provider value = {/* 某个值 */ } >
Provider 接收一个 value 属性,传递给消费组件。一个 Provider 可以和多个消费组件有对应关系。多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据。
当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染。Provider 及其内部 consumer 组件都不受制于 shouldComponentUpdate 函数,因此当 consumer 组件在其祖先组件退出更新的情况下也能更新。
通过新旧值检测来确定变化,使用了与 Object.is 相同的算法。
Class.contextType
挂载在 class 上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象。这能让你使用 this.context 来消费最近 Context 上的那个值。你可以在任何生命周期中访问到它,包括 render 函数中。
class MyClass extends React.Component {
static contextType = MyContext // 该写法需要安装babel插件转义
componentDidMount() {
let value = this.context;
/* 在组件挂载完成后,使用 MyContext 组件的值来执行一些有副作用的操作 */
}
componentDidUpdate() {
let value = this.context;
/* ... */
}
componentWillUnmount() {
let value = this.context;
/* ... */
}
render() {
let value = this.context;
/* 基于 MyContext 组件的值进行渲染 */
}
}
MyClass.contextType = MyContext;
Context.Consumer
<MyContext.Consumer>
{value => /* 基于 context 值进行渲染*/}
</MyContext.Consumer>
这需要函数作为子元素(function as a child)这种做法。这个函数接收当前的 context 值,返回一个 React 节点。传递给函数的 value 值等同于往上组件树离这个 context 最近的 Provider 提供的 value 值。如果没有对应的 Provider,value 参数等同于传递给 createContext() 的 defaultValue。
Context.displayName
context 对象接受一个名为 displayName 的 property,类型为字符串。React DevTools 使用该字符串来确定 context 要显示的内容。
const MyContext = React.createContext(/* some value */);
MyContext.displayName = 'MyDisplayName';
<MyContext.Provider> // "MyDisplayName.Provider" 在 DevTools 中
<MyContext.Consumer> // "MyDisplayName.Consumer" 在 DevTools 中
组合使用例子
// Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树。
// 为当前的 theme 创建一个 context(“light”为默认值)。
const ThemeContext = React.createContext('light');
class App extends React.Component {
render() {
// 使用一个 Provider 来将当前的 theme 传递给以下的组件树。
// 无论多深,任何组件都能读取这个值。
// 在这个例子中,我们将 “dark” 作为当前的值传递下去。
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
}
// 中间的组件再也不必指明往下传递 theme 了。
function Toolbar() {
return (
<div>
<ThemedButton />
</div>
);
}
class ThemedButton extends React.Component {
// 指定 contextType 读取当前的 theme context。
// React 会往上找到最近的 theme Provider,然后使用它的值。
// 在这个例子中,当前的 theme 值为 “dark”。
static contextType = ThemeContext;
render() {
return <Button theme={this.context} />;
}
}
ErrorBoundary
错误边界是一种 React 组件,这种组件可以捕获并打印发生在其子组件树任何位置的 JavaScript 错误,并且,它会渲染出备用 UI,而不是渲染那些崩溃了的子组件树。错误边界在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误。
注意事项:
错误边界无法捕获以下场景中产生的错误:
- 事件处理
- 异步代码(例如 setTimeout 或 requestAnimationFrame 回调函数)
- 服务端渲染
- 它自身抛出来的错误(并非它的子组件)
形成条件
如果一个 class 组件中定义了 static getDerivedStateFromError()
或 componentDidCatch()
这两个生命周期方法中的任意一个(或两个)时,那么它就变成一个错误边界。当抛出错误后,请使用 static getDerivedStateFromError()
渲染备用 UI ,使用 componentDidCatch()
打印错误信息。
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// 更新 state 使下一次渲染能够显示降级后的 UI
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// 你同样可以将错误日志上报给服务器
logErrorToMyService(error, errorInfo);
}
render() {
if (this.state.hasError) {
// 你可以自定义降级后的 UI 并渲染
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
Fragments
React 中的一个常见模式是一个组件返回多个元素。Fragments 允许你将子列表分组,而无需向 DOM 添加额外节点。
用法:
render() {
return (
<React.Fragment>
<ChildA />
<ChildB />
<ChildC />
</React.Fragment>
);
}
//或者短语法
render() {
return (
<>
<ChildA />
<ChildB />
<ChildC />
</>
);
}
使用显式 <React.Fragment>
语法声明的片段可能具有 key ;短语法不支持key.
React.forwardRef
Ref forwarding 是一项将 ref 自动地通过组件传递到其一子组件的技巧。
为什么会有这个功能呢 因为refs 不会被props透传下去。这是因为 ref 不是 prop 属性。就像 key 一样,其被 React 进行了特殊处理。
用法:React.forwardRef 接受一个渲染函数,其接收 props 和 ref 参数并返回一个 React 节点。
function logProps(Component) {
class LogProps extends React.Component {
componentDidUpdate(prevProps) {
console.log('old props:', prevProps);
console.log('new props:', this.props);
}
render() {
const {forwardedRef, ...rest} = this.props;
// 将自定义的 prop 属性 “forwardedRef” 定义为 ref
return <Component ref={forwardedRef} {...rest} />;
}
}
// 注意 React.forwardRef 回调的第二个参数 “ref”。
// 我们可以将其作为常规 prop 属性传递给 LogProps,例如 “forwardedRef”
// 然后它就可以被挂载到被 LogProps 包裹的子组件上。
return React.forwardRef((props, ref) => {
return <LogProps {...props} forwardedRef={ref} />;
});
}
该方法常用于高阶函数。
Higher-Order Components
高阶组件(HOC)是React中一个复用组件逻辑的高级技术。简单的说,就是获取一个组件返回一个新的组件。常见的如Redux的connect方法等。 它是一个纯函数,没有副作用
用法
function logProps(WrappedComponent) {
return class extends React.Component {
componentDidUpdate(prevProps) {
console.log('Current props: ', this.props);
console.log('Previous props: ', prevProps);
}
render() {
return <WrappedComponent {...this.props} />;
}
}
}
注意点:
- HOC 应该透传与自身无关的 props
- HOC创建的容器在调试的时候会显示一样的名字 可以用displayname 来处理
- 不能在render中使用HOC
- 静态方法必须复制 higherOrderComponent.staticMethod = WrappedComponent.staticMethod;
- Refs 不会被传递 可以只用React.forwardRef解决
React.memo
React.memo 为高阶组件。它与 React.PureComponent 非常相似,但只适用于函数组件,而不适用 class 组件。
如果你的函数组件在给定相同 props 的情况下渲染相同的结果,那么你可以通过将其包装在 React.memo 中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。这意味着在这种情况下,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。
React.memo 仅检查 props 变更。如果函数组件被 React.memo 包裹,且其实现中拥有 useState 或 useContext 的 Hook,当 context 发生变化时,它仍会重新渲染。
默认情况下其只会对复杂对象做浅层对比,如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现。
用法
function MyComponent(props) {
/* 使用 props 渲染 */
}
function areEqual(prevProps, nextProps) {
/*
如果把 nextProps 传入 render 方法的返回结果与
将 prevProps 传入 render 方法的返回结果一致则返回 true,
否则返回 false
*/
}
export default React.memo(MyComponent, areEqual);
性能优化
性能和渲染(Render)正相关
React 基于虚拟 DOM 和高效 Diff 算法的完美配合,实现了对 DOM 最小粒度的更新。但在个别复杂业务场景下,性能问题依然会困扰我们。此时需要采取一些措施来提升运行性能,其很重要的一个方向,就是避免不必要的渲染(Render)。
渲染(Render)时影响性能的点
React 处理 render 的基本思维模式是每次一有变动就会去重新渲染整个应用。Virtual DOM 厉害的地方并不是说它比直接操作 DOM 快,而是说不管数据怎么变,都会尽量以最小的代价去更新 DOM。React 将 render 函数返回的虚拟 DOM 树与老的进行比较,从而确定 DOM 要不要更新、怎么更新。当 DOM 树很大时,遍历两棵树进行各种比对还是相当耗性能的,特别是在顶层 setState 一个微小的修改,默认会去遍历整棵树。尽管 React 使用高度优化的 Diff 算法 ,但是这个过程仍然会损耗性能。
渲染(Render)何时会被触发
触发render的条件有:
组件挂载
React 组件构建并将 DOM 元素插入页面的过程称为挂载。当组件首次渲染的时候会调用 render,这个过程不可避免。
setState 方法的调用
通常情况下,执行 setState 会触发 render。但当 setState 传入 null 的时候,并不会触发 render 。
父组件重新渲染
只要父组件重新渲染了,即使传入子组件的 props 未发生变化,那么子组件也会重新渲染,进而触发 render。
如何优化
根本思路减少不必要的render。
根据类组件和函数组件的不同具体有不同的方法。
类组件的性能优化
shouldComponentUpdate 和 PureComponent
在React类组件中,可以利用shouldComponentUpdate
或者 PureComponent
来减少因父组件更新而触发子组件的render。
shouldComponentUpdate
生命周期,可以通过返回true代表需要重新渲染,返回false代表不渲染
PureComponent
通过对props和state的浅比较结果来实现shouldComponentUpdate
,但是当对象包含复杂的数据结构时,可能就不灵啦,对象深层的数据改变但是没有触发render。
在React中PureComponent
源码如下
if (this._compositeType === CompositeTypes.PureClass) {
shouldUpdate = !shallowEqual(prevProps, nextProps) || ! shallowEqual(inst.state, nextState);
}
shallowEqual
的实现代码
const hasOwnProperty = Object.prototype.hasOwnProperty;
/**
* is 方法来判断两个值是否是相等的值,为何这么写可以移步 MDN 的文档
* https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/is
*/
function is(x: mixed, y: mixed): boolean {
if (x === y) {
return x !== 0 || y !== 0 || 1 / x === 1 / y;
} else {
return x !== x && y !== y;
}
}
function shallowEqual(objA: mixed, objB: mixed): boolean {
// 首先对基本类型进行比较
if (is(objA, objB)) {
return true;
}
if (typeof objA !== 'object' || objA === null ||
typeof objB !== 'object' || objB === null) {
return false;
}
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
// 长度不相等直接返回false
if (keysA.length !== keysB.length) {
return false;
}
// key相等的情况下,再去循环比较
for (let i = 0; i < keysA.length; i++) {
if (
!hasOwnProperty.call(objB, keysA[i]) ||
!is(objA[keysA[i]], objB[keysA[i]])
) {
return false;
}
}
return true;
}
高阶组件
在函数组件中,并没有shouldComponnetUpdate
这个生命周期,但是可以利用高阶组件来实现一个类型的功能。
const shouldComponentUpdate = areEqual => BaseComponent => {
class ShouldComponentUpdate extends React.Component{
shouldComponentUpdate(nextProps){
return areEqual(this.props,nextProps)
}
render(){
return <BaseComponent {...this.props}/>
}
}
ShouldComponentUpdate.displayName = `Pure(${BaseComponent.displayName})`
return ShouldComponentUpdate
}
const Pure = BaseComponent => {
const hoc = shouldComponentUpdate(
(props, nextProps) => !shallowEqual(props, nextProps)
)
return hoc(BaseComponent);
}
使用Pure
高阶组件时,只需要对我们子组件进行装饰即可
import React from'react';
const Child = (props) =><div>{props.name}</div>;
export default Pure(Child);
函数组件 用例子来说明具体的用法,有如下一个场景:
// parent.js
import React, { useState } from "react";
function parent(){
const [title,setTitle] = useState('ye')
return(
<div>
<h1>{title}</h1>
<button onClick={() => setTitle("ye1")}>更改名称</button>
<Child name="lewisye"></Child>
</div>
)
}
// child.js
import React from "react";
function Child(props) {
console.log("child")
return <h1>{props.name}</h1>
}
export default Child
当parent组件初次渲染的时候,控制台会打印出child字符串,这表示子组件也渲染了。但是当你点击去更改名称按钮时,控制台有一次打印了。但是这种情况是我们不想看到的。因为你传入给child的props并没有改变,这需要减少子组件的重新渲染来提高性能。那我们可以用到的就是React.memo
React.memo
React.memo 为高阶组件。它与 React.PureComponent 非常相似,但只适用于函数组件,而不适用 class 组件。
默认情况下其只会对复杂对象做浅层对比,如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现。
function MyComponent(props) {
/* 使用 props 渲染 */
}
function areEqual(prevProps, nextProps) {
/*
如果把 nextProps 传入 render 方法的返回结果与
将 prevProps 传入 render 方法的返回结果一致则返回 true,
否则返回 false
*/
}
export default React.memo(MyComponent, areEqual);
注意
与 class 组件中 shouldComponentUpdate() 方法不同的是,如果 props 相等,areEqual 会返回 true;如果 props 不相等,则返回 false。这与 shouldComponentUpdate 方法的返回值相反。
那具体怎么使用呢,其实只需要讲child组件用React.memo包裹起来就可以.
import React from "react";
function Child(props) {
console.log("child")
return <h1>{props.name}</h1>
}
export default React.memo(Child)
useCallback
我们将上述的例子改变一下,当改变标题的方法在子组件触发,并再添加一个触发事件
// parent.js
import React, { useState } from "react";
function parent(){
const [title,setTitle] = useState('ye')
const print = () => {
console.log('print')
}
const callback = () => {
setTitle("ye1")
}
return(
<div>
<h1>{title}</h1>
<button onClick={print}>输出</button>
<Child name="lewisye" onClick={callback}></Child>
</div>
)
}
// child.js
import React from "react";
function Child(props) {
console.log("child")
return(
<>
<button onClick={props.onClick}>改标题</button>
<h1>{props.name}</h1>
</>
)
}
export default React.memo(Child);
首次渲染你可以看到打印了child字符,当点击输出按钮的时候,你会发现再一次打印了字符串。这是为什么呢,我们之前不是用React.memo处理了吗。
分析一下,一个组件重新重新渲染,一般三种情况:
要么是组件自己的状态改变
要么是父组件重新渲染,导致子组件重新渲染,但是父组件的 props 没有改变
要么是父组件重新渲染,导致子组件重新渲染,但是父组件传递的 props 改变
显然现在符合我们场景的只有第三种,第一种在该例子中没有用到,第二种我们上门已经用React.memo处理了。那是哪个props改变了呢,一个是name一个是onClick函数方法,显然是oClick函数方法。
在函数式组件里每次重新渲染,函数组件都会重头开始重新执行,那么onClick方法就变得不同啦。那如何解决呢,当然需要我们的useCallback
useCallback 的使用语法
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
返回一个 memoized 回调函数。
把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新
useCallback(fn, deps) 相当于 useMemo(() => fn, deps)
那怎么在例子使用呢? /通过 useCallback 进行记忆 callback,并将记忆的 callback 传递给 Child const memoizedCallback = useCallback(callback, [])
// parent.js
import React, { useState } from "react";
function parent(){
const [title,setTitle] = useState('ye')
const print = () => {
console.log('print')
}
const callback = () => {
setTitle("ye1")
}
const memoizedCallback = useCallback(callback, [])
return(
<div>
<h1>{title}</h1>
<button onClick={print}>输出</button>
<Child name="lewisye" onClick={memoizedCallback}></Child>
</div>
)
}
useMemo
React 的性能优化方向主要是两个:一个是减少重新 render 的次数(或者说减少不必要的渲染),另一个是减少计算的量。
上述介绍的 React.memo 和 useCallback 都是为了减少重新 render 的次数。对于如何减少计算的量,就是 useMemo 来做的,接下来我们看例子。
function App() {
const [num, setNum] = useState(0);
// 一个非常耗时的一个计算函数
// result 最后返回的值是 49995000
function expensiveFn() {
let result = 0;
for (let i = 0; i < 10000; i++) {
result += i;
}
console.log(result) // 49995000
return result;
}
const base = expensiveFn();
return (
<div>
<h1>count:{num}</h1>
<button onClick={() => setNum(num + base)}>+1</button>
</div>
);
}
这个例子功能很简单,就是点击 +1 按钮,然后会将现在的值(num) 与 计算函数 (expensiveFn) 调用后的值相加,然后将和设置给 num 并显示出来,在控制台会输出 49995000。
先我们把 expensiveFn 函数当做一个计算量很大的函数(比如你可以把 i 换成 10000000),然后当我们每次点击 +1 按钮的时候,都会重新渲染组件,而且都会调用 expensiveFn 函数并输出 49995000。由于每次调用 expensiveFn 所返回的值都一样,所以我们可以想办法将计算出来的值缓存起来,每次调用函数直接返回缓存的值,这样就可以做一些性能优化。
针对上面产生的问题,就可以用 useMemo 来缓存 expensiveFn 函数执行后的值。
useMemo基本用法
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b])
返回一个 memoized 值。
把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算
记住,传入 useMemo 的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect 的适用范畴,而不是 useMemo。
如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值
在例子中使用优化后的代码:
function App() {
const [num, setNum] = useState(0);
function expensiveFn() {
let result = 0;
for (let i = 0; i < 10000; i++) {
result += i;
}
console.log(result)
return result;
}
const base = useMemo(expensiveFn, []);
return (
<div className="App">
<h1>count:{num}</h1>
<button onClick={() => setNum(num + base)}>+1</button>
</div>
);
}
执行上面的代码,然后现在可以观察无论我们点击 +1多少次,只会输出一次 49995000,这就代表 expensiveFn 只执行了一次,达到了我们想要的效果。
合理拆分组件
试想当整个页面只有一个组件时,无论哪处改动都会触发render,那么对于组件进行拆分,颗粒度更细,render就可以得到更细的控制,性能也有一定的提升
HOOKS
Hook 是一些可以让你在函数组件里“钩入” React state 及生命周期等特性的函数。Hook 不能在 class 组件中使用 —— 这使得你不使用 class 也能使用 React。
Hook 的产生
Hook的产生为了解决什么问题 或者 带来了什么便利呢?
Hook 使你在无需修改组件结构的情况下复用状态逻辑
在React 中 复用状态逻辑很难,常用的方法有 高阶组件 和 render props 。但是这些方法需要你重新组织你的组件结构,使得代码难以理解。
Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据)
组件常常在 componentDidMount 和 componentDidUpdate 中获取数据。但是,同一个 componentDidMount 中可能也包含很多其它的逻辑,如设置事件监听,而之后需在 componentWillUnmount 中清除。相互关联且需要对照修改的代码被进行了拆分,而完全不相关的代码却在同一个方法中组合在一起。如此很容易产生 bug,并且导致逻辑不一致。
Hook 使你在非 class 的情况下可以使用更多的 React 特性
在使用class 你需要理解js中的this机制
参考链接:React为什么需要Hook
State Hook
useState
是React内置的一个Hook,以它为例实现一个计数器:
import React, { useState } from 'react';
function Example() {
// 声明一个叫 "count" 的 state 变量
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
useState
方法定义了一个 state 变量,变量名叫做count。
useState
方法有一个唯一的参数,代表是变量的初始化,可以是数字、字符、对象等类型
useState
方法的返回值为 当前 state 以及更新 state 的函数
在一个函数组件中也可以声明多个state变量
function ExampleWithManyStates() {
// 声明多个 state 变量!
const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
// ...
}
Effect Hook
Effect Hook 可以让你在函数组件中执行副作用操作。数据获取,设置订阅以及手动更改 React 组件中的 DOM 都属于副作用。
我们为计数器增加了一个小功能:将 document 的 title 设置为包含了点击次数的消息。使用到useEffect:
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// 类似于 componentDidMount and componentDidUpdate:
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
通过使用这个 useEffect Hook,你可以告诉 React 组件需要在渲染后执行某些操作。React 会保存你传递的函数(我们将它称之为 “effect”),并且在执行 DOM 更新之后调用它。
在 React 组件中有两种常见副作用操作:需要清除的和不需要清除的。effect Hook 使用同一个 API 来满足这两种情况。通过在函数中 返回一个函数来处理。
useEffect(() => {
// 使用
return () => {
// 清除
};
});
使用多个 Effect 实现关注点分离
Hook 允许我们按照代码的用途分离他们, 而不是像生命周期函数那样。React 将按照 effect 声明的顺序依次调用组件中的每一个 effect。
function FriendStatusWithCounter(props) {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
// ...
}
为什么每次更新的时候都要运行 Effect
当组件已经显示在屏幕上时,prop 发生变化时会发生什么? 我们的组件将继续展示原来状态 这是一个 bug。在class写法中需要在componentDidUpdate生命周期中处理。但是effect 并不需要特定的代码来处理更新逻辑,因为 useEffect 默认就会处理。它会在调用一个新的 effect 之前对前一个 effect 进行清理
通过跳过 Effect 进行性能优化
在某些情况下,每次渲染后都执行清理或者执行 effect 可能会导致性能问题。在 class 组件中,我们可以通过在 componentDidUpdate 中添加对 prevProps 或 prevState 的比较逻辑解决。
所以它被内置到了 useEffect 的 Hook API 中。如果某些特定值在两次重渲染之间没有发生变化,你可以通知 React 跳过对 effect 的调用,只要传递数组作为 useEffect 的第二个可选参数即可:
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新
如果你要使用此优化方式,请确保数组中包含了所有外部作用域中会随时间变化并且在 effect 中使用的变量,否则你的代码会引用到先前渲染中的旧变量
如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数。
Hook Rules
Hook 本质就是 JavaScript 函数,但是在使用它时需要遵循两条规则
- 只在最顶层使用 Hook
- 只在 React 函数中调用 Hook
只在最顶层使用 Hook
不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层调用他们
遵守这条规则,你就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。这让 React 能够在多次的 useState 和 useEffect 调用之间保持 hook 状态的正确
如果我们想要有条件地执行一个 effect,可以将判断放到 Hook 的内部
只在 React 函数中调用 Hook
不要在普通的 JavaScript 函数中调用 Hook
你可以:
- ✅ 在 React 的函数组件中调用 Hook
- ✅ 在自定义 Hook 中调用其他 Hook
自定义 Hook
自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook。
自定义 Hook 是一种重用状态逻辑的机制(例如设置为订阅并存储当前值),所以每次使用自定义 Hook 时,其中的所有 state 和副作用都是完全隔离的。在两个组件中使用相同的 Hook 不会共享 state。
自定义 Hook 解决了以前在 React 组件中无法灵活共享逻辑的问题
API概览
- 基础Hook
- useState
- useEffect
- useContext
- 额外的Hook
- useReducer
- useCallback
- useMemo
- useRef
- useImperativeHandle
- useLayoutEffect
- useDebugValue
useState
const [state,setState] = useState(initialState)
返回一个state,以及更新state的函数
在初始渲染期间,返回的状态 (state) 与传入的第一个参数 (initialState) 值相同。
setState 函数用于更新 state。它接收一个新的 state 值并将组件的一次重新渲染加入队列。setState(newState)
在后续的重新渲染中,useState 返回的第一个值将始终是更新后最新的 state。
函数式更新
如果新的 state 需要通过使用先前的 state 计算得出,那么可以将函数传递给 setState。该函数将接收先前的 state,并返回一个更新后的值。
setState(pervState => pervState + 1)
如果你的更新函数返回值与当前 state 完全相同,则随后的重渲染会被完全跳过。
与 class 组件中的 setState 方法不同,useState 不会自动合并更新对象。你可以用函数式的 setState 结合展开运算符来达到合并更新对象的效果。
setState(prevState => {
// 也可以使用 Object.assign
return {...prevState, ...updatedValues};
});
惰性初始state
如果初始 state 需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用
const [state, setState] = useState(() => {
const initialState = someExpensiveComputation(props);
return initialState;
});
useEffect
useEffect(didUpdate)
该Hook接收一个包含命令式、且可能有副作用代码的函数
使用 useEffect 完成副作用操作。赋值给 useEffect 的函数会在组件渲染到屏幕之后执行。
清除effect
通常,组件卸载时需要清除 effect 创建的诸如订阅或计时器 ID 等资源。要实现这一点,useEffect 函数需返回一个清除函数。以下就是一个创建订阅的例子:
useEffect(()=>{
const subscription = props.source.subscribe()
return ()=>{
//清除订阅
subscription.unsubscribe()
}
})
为防止内存泄漏,清除函数会在组件卸载前执行。另外,如果组件多次渲染(通常如此),则在执行下一个 effect 之前,上一个 effect 就已被清除。
effect的条件执行
默认情况下,effect 会在每轮组件渲染完成后执行。这样的话,一旦 effect 的依赖发生变化,它就会被重新创建。但某些场景并不需要每次组件更新时,被执行。
要实现这一点,可以给 useEffect 传递第二个参数,它是 effect 所依赖的值数组。
useEffect(
()=>{
const subscription = props.source.subscribe()
return ()=>{
subscription.unsubscribe()
}
},
[props.source]
)
此时,只有当 props.source 改变后才会重新创建订阅。
注意:
如果你要使用此优化方式,请确保数组中包含了所有外部作用域中会发生变化且在 effect 中使用的变量,否则你的代码会引用到先前渲染中的旧变量。
如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数。
useContext
const value = useContext(MyContext)
接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定。
当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。即使祖先使用 React.memo 或 shouldComponentUpdate,也会在组件本身使用 useContext 时重新渲染。
示例:
const themes = {
foreground:"#000000",
background:"#eeeeee"
}
const ThemeContext = React.createContext(themes);
function App(){
return(
<ThemeContext.Provider value={themes}>
<Toolbar/>
</ThemeContext.Provider>
)
}
function Toolbar(props){
return(
<div>
<ThemedButton/>
</div>
)
}
function ThemedButton(){
const theme = useContext(ThemeContext)
return(
<button style={{background:theme.background,color:theme.foreground}}>
I am styled by theme context!
</button>
)
}
useReducer
const [state,dispacth] = useReducer(reducer,initialArg,init);
useState 的替代方案。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法
在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数
const initialState = {count: 0};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
初始化state
有两种不同初始化 useReducer state 的方式,你可以根据使用场景选择其中的一种。将初始 state 作为第二个参数传入 useReducer 是最简单的方法:
const [state, dispatch] = useReducer(
reducer,
{count: initialCount}
);
也可以选择惰性地创建初始 state。为此,需要将 init 函数作为 useReducer 的第三个参数传入,这样初始 state 将被设置为 init(initialArg)。
useCallback
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
返回一个 memoized 回调函数。
把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新
useCallback(fn, deps) 相当于 useMemo(() => fn, deps)
useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b])
返回一个 memoized 值。
把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算
记住,传入 useMemo 的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect 的适用范畴,而不是 useMemo。
如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值
useRef
const refContainer = useRef(initialValue)
useRef 返回一个可变的ref对象,其.current属性被初始化为传入的参数(initialValue).返回的ref对象在组件的整个生命周期内保持不变
示例:
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` 指向已挂载到 DOM 上的文本输入元素
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
事件机制
本文是基于V16.13.1进行的分析。
事件注册与存储
注册与存储的大体函数调用顺序如下:
// ReactFiberCompleteWork.old.js
finalizeInitialChildren // ReactDOMHostConfig.js
setInitialProperties // ReactDOMComponent.js
setInitialDOMProperties // ReactDOMComponent.js
ensureListeningTo // ReactDOMComponent.js
listenToReactEvent // DOMPluginEventSystem.js
getEventListenerMap // ReactDOMComponentTree.js
listenToNativeEvent // DOMPluginEventSystem.js
addTrappedEventListener // DOMPluginEventSystem.js
createEventListenerWrapperWithPriority // ReactDOMEventListener.js 注入了dispatchEvent
addEventCaptureListenerWithPassiveFlag、addEventCaptureListener、addEventBubbleListenerWithPassiveFlag 、addEventBubbleListener // EventListener.js
target.addEventListener(eventType, listener, false); // EventListener.js
setInitialDOMProperties 方法
function setInitialDOMProperties(
tag: string,
domElement: Element,
rootContainerElement: Element | Document,
nextProps: Object,
isCustomComponentTag: boolean,
): void {
for (const propKey in nextProps) {
if (!nextProps.hasOwnProperty(propKey)) {
continue;
}
const nextProp = nextProps[propKey];
if (propKey === STYLE) {
// ...
} else if (registrationNameDependencies.hasOwnProperty(propKey)) {
if (nextProp != null) {
if (!enableEagerRootListeners) {
ensureListeningTo(rootContainerElement, propKey, domElement);
}
}
}
}
}
当propKey在registrationNameDependencies
列表中时,会调用ensureListeningTo
方法。这里的registrationNameDependencies
存储了React事件类型与浏览器原生事件类型映射的一个map对象。
其中onChange
的dependences
listenToReactEvent 方法
export function listenToReactEvent(
reactEvent: string, // 例如onChange
rootContainerElement: Element,
targetElement: Element | null,
): void {
// dependences这边可以理解为事件依赖,就是说注册某个事件,react会强制依赖其他事件。 如上图的onChange
const dependencies = registrationNameDependencies[reactEvent];
const dependenciesLength = dependencies.length;
const isPolyfillEventPlugin = dependenciesLength !== 1;
if (isPolyfillEventPlugin) {
// 首次返回一个空的map对象
const listenerMap = getEventListenerMap(rootContainerElement);
// listenerMap不包含当前事件属性,就进入判断(has是判断属性是否存在,即使内容为null,也是返回true)
// 也就是同一种事件只会注册一遍,onChange、onClick等等
if (!listenerMap.has(reactEvent)) {
// 给对象添加一个reactEvent属性,值为null
listenerMap.set(reactEvent, null); // 这个listenerMap会变成{onChange: null}
for (let i = 0; i < dependenciesLength; i++) {
// 循环遍历dependencies
listenToNativeEvent(
dependencies[i], // dependence
false,
rootContainerElement,
targetElement,
);
}
}
} else {
const isCapturePhaseListener =
reactEvent.substr(-7) === 'Capture' &&
reactEvent.substr(-14, 7) !== 'Pointer';
listenToNativeEvent(
dependencies[0],
isCapturePhaseListener,
rootContainerElement,
targetElement,
);
}
}
getEventListenerMap
const randomKey = Math.random().toString(36).slice(2);
const internalEventHandlersKey = '__reactEvents$' + randomKey;
export function getEventListenerMap(node: EventTarget): ElementListenerMap {
let elementListenerMap = (node: any)[internalEventHandlersKey];
if (elementListenerMap === undefined) {
elementListenerMap = (node: any)[internalEventHandlersKey] = new Map();
}
return elementListenerMap;
}
🔥 listenToNativeEvent 存储
export function listenToNativeEvent(
domEventName: DOMEventName,
isCapturePhaseListener: boolean,
rootContainerElement: EventTarget,
targetElement: Element | null,
isPassiveListener?: boolean,
listenerPriority?: EventPriority,
eventSystemFlags?: EventSystemFlags = 0,
): void {
let target = rootContainerElement; // div#root
// ...
// 这边去获取上面提到的那个map对象
const listenerMap = getEventListenerMap(target);
// export function getListenerMapKey(
// domEventName: DOMEventName,
// capture: boolean,
// ): string {
// return `${domEventName}__${capture ? 'capture' : 'bubble'}`;
// }
// 获取到的listenerMapKey值是 onChange_bubble
const listenerMapKey = getListenerMapKey(
domEventName,
isCapturePhaseListener, // false
);
// 判断listenerMap中是否存在listenerMapKey
const listenerEntry = ((listenerMap.get(
listenerMapKey,
): any): ElementListenerMapEntry | void);
// 判断是否需要更新
const shouldUpgrade = shouldUpgradeListener(listenerEntry, isPassiveListener);
// 如果不存在当前事件,或者需要更新,进入判断
if (listenerEntry === undefined || shouldUpgrade) {
if (shouldUpgrade) {
removeEventListener(
target,
domEventName,
((listenerEntry: any): ElementListenerMapEntry).listener,
isCapturePhaseListener,
);
}
if (isCapturePhaseListener) {
eventSystemFlags |= IS_CAPTURE_PHASE;
}
// addTrappedEventListener内部就是做了:在target上进行事件监听,并返回dispatchEvent函数
const listener = addTrappedEventListener(
target, // div#root
domEventName, // onChange
eventSystemFlags, // 0
isCapturePhaseListener, //false
false,
isPassiveListener,
listenerPriority,
);
// 最终这个listenerMap会变成{onChange: null, change_bubble: {passive: isPassiveListener, listener}}
listenerMap.set(listenerMapKey, {passive: isPassiveListener, listener});
}
}
listenerMap的结构
🔥 addTrappedEventListener 事件注册
function addTrappedEventListener(
targetContainer: EventTarget, // div#root
domEventName: DOMEventName, // onChange
eventSystemFlags: EventSystemFlags, // 0
isCapturePhaseListener: boolean, // false
isDeferredListenerForLegacyFBSupport?: boolean,
isPassiveListener?: boolean,
listenerPriority?: EventPriority,
): any => void {
// 这段代码尤为重要,通过传入的domEventName获取当前事件的优先级,返回的是经过包装过的三类dispatchEvent事件
// 分别为dispatchDiscreteEvent =>0 | dispatchUserBlockingUpdate =>1 | dispatchEvent=>2
// export function createEventListenerWrapperWithPriority(
// targetContainer: EventTarget,
// domEventName: DOMEventName,
// eventSystemFlags: EventSystemFlags,
// priority?: EventPriority,
// ): Function {
// const eventPriority =
// priority === undefined
// ? getEventPriorityForPluginSystem(domEventName)
// : priority;
// let listenerWrapper;
// switch (eventPriority) {
// case DiscreteEvent:
// listenerWrapper = dispatchDiscreteEvent;
// break;
// case UserBlockingEvent:
// listenerWrapper = dispatchUserBlockingUpdate;
// break;
// case ContinuousEvent:
// default:
// listenerWrapper = dispatchEvent;
// break;
// }
// return listenerWrapper.bind(
// null,
// domEventName,
// eventSystemFlags,
// targetContainer,
// );
// }
let listener = createEventListenerWrapperWithPriority(
targetContainer,
domEventName,
eventSystemFlags,
listenerPriority,
);
// ...
if (isCapturePhaseListener) {
if (isPassiveListener !== undefined) {
unsubscribeListener = addEventCaptureListenerWithPassiveFlag(
targetContainer,
domEventName,
listener,
isPassiveListener,
);
} else {
unsubscribeListener = addEventCaptureListener(
targetContainer,
domEventName,
listener,
);
}
} else {
if (isPassiveListener !== undefined) {
unsubscribeListener = addEventBubbleListenerWithPassiveFlag(
targetContainer,
domEventName,
listener,
isPassiveListener,
);
} else {
unsubscribeListener = addEventBubbleListener(
targetContainer,
domEventName,
listener,
);
}
}
return unsubscribeListener;
}
addEventCaptureListenerWithPassiveFlag、addEventCaptureListener、addEventBubbleListenerWithPassiveFlag 、addEventBubbleListener
这四个方法本质都是调用的是target.addEventListener(eventType, listener, true);
稍微有点差别
到此 注册和存储已经完成啦
总结:事件注册的流程就是遍历props中的event,然后将事件和其依赖事件都挂载到target上,当中所有的事件的回调函数走的都是dispatchEvent,并且相同类型的事件只会挂在一次。还有如果我绑定一个onChange事件,那么react不仅仅只绑定一个onChange事件到target上,还会绑定许多依赖事件上去,如focus,blur,input等等,组件中声明的事件并不会保存起来,而仅仅是将事件类型以及dispatchEvent函数绑定到target元素上,实现事件委派。
事件分发与执行
函数调用顺序如下:
dispatchEvent // ReactDOMEventListener.js
dispatchEventForPluginEventSystem // DOMPluginEventSystem.js
batchedEventUpdates // ReactDOMUpdateBatching.js
dispatchEventsForPlugins // DOMPluginEventSystem.js // 该函数先合成事件 然后再执行
extractEvents // DOMPluginEventSystem.js
// 以ChangeEventPlugin为例子
createAndAccumulateChangeEvent // // react-dom/src/client/events/ChangeEventPlugin.js
accumulateTwoPhaseListeners // DOMPluginEventSystem.js
processDispatchQueue // 在dispatchEventsForPlugins函数中调用
executeDispatch // DOMPluginEventSystem.js
invokeGuardedCallbackAndCatchFirstError // shared/ReactErrorUtils
dispatchEvent
export function dispatchEvent(
domEventName: DOMEventName, //onChange
eventSystemFlags: EventSystemFlags, // 0
targetContainer: EventTarget, //div#root
nativeEvent: AnyNativeEvent,
): void {
// ....
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
null,
targetContainer,
);
}
dispatchEventForPluginEventSystem
export function dispatchEventForPluginEventSystem(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
nativeEvent: AnyNativeEvent,
targetInst: null | Fiber,
targetContainer: EventTarget,
): void {
// ...
//批量更新
batchedEventUpdates(() =>
dispatchEventsForPlugins(
domEventName,
eventSystemFlags,
nativeEvent,
ancestorInst,
targetContainer,
),
);
}
batchedEventUpdates
export function batchedEventUpdates(fn, a, b) {
if (isBatchingEventUpdates) {// 初始是false
return fn(a, b);
}
isBatchingEventUpdates = true;
try {
return batchedEventUpdatesImpl(fn, a, b);
} finally {
isBatchingEventUpdates = false;
finishEventHandler();
}
}
dispatchEventsForPlugins
function dispatchEventsForPlugins(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
nativeEvent: AnyNativeEvent,
targetInst: null | Fiber,
targetContainer: EventTarget,
): void {
const nativeEventTarget = getEventTarget(nativeEvent); // 获取当前dom元素
const dispatchQueue: DispatchQueue = [];
// 进行事件合成
extractEvents(
dispatchQueue, // []
domEventName, // onChange
targetInst,
nativeEvent, // 原生事件
nativeEventTarget, // 获取当前dom元素
eventSystemFlags, // 0
targetContainer, // div#root
);
// 按顺序执行事件队列,此时dispatchQueue已经变成[onChange, [{instance, listener, currentTarget}, ...]]
processDispatchQueue(dispatchQueue, eventSystemFlags);
}
🔥 extractEvents事件合成
extractEvents这个方法就是调用各种插件来创建相应函数的合成事件,一共有6种插件,这边用到了5个。事件的合成,冒泡的处理以及事件回调的查找都是在合成阶段完成的。
function extractEvents(
dispatchQueue: DispatchQueue, // 初始为[]
domEventName: DOMEventName, // dependence
targetInst: null | Fiber, //
nativeEvent: AnyNativeEvent, // 原生事件
nativeEventTarget: null | EventTarget, // 当前dom元素
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
) {
SimpleEventPlugin.extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer,
);
const shouldProcessPolyfillPlugins = (eventSystemFlags & SHOULD_NOT_PROCESS_POLYFILL_EVENT_PLUGINS) === 0;
if (shouldProcessPolyfillPlugins) {
EnterLeaveEventPlugin.extractEvents(
...
);
ChangeEventPlugin.extractEvents(
...
);
SelectEventPlugin.extractEvents(
...
);
BeforeInputEventPlugin.extractEvents(
...
);
}
}
我们以ChangeEventPlugin插件举例:
// react-dom/src/client/events/ChangeEventPlugin.js
function extractEvents(
dispatchQueue: DispatchQueue,
domEventName: DOMEventName,
targetInst: null | Fiber,
nativeEvent: AnyNativeEvent,
nativeEventTarget: null | EventTarget,
eventSystemFlags: EventSystemFlags,
targetContainer: null | EventTarget,
) {
const targetNode = targetInst ? getNodeFromInstance(targetInst) : window;
let getTargetInstFunc, handleEventFunc;
// 这边判断当前的节点符不符合当前插件创建相应合成事件的要求
if (shouldUseChangeEvent(targetNode)) {
getTargetInstFunc = getTargetInstForChangeEvent;
} else if (isTextInputElement(((targetNode: any): HTMLElement))) {
if (isInputEventSupported) {
getTargetInstFunc = getTargetInstForInputOrChangeEvent;
} else {
getTargetInstFunc = getTargetInstForInputEventPolyfill;
handleEventFunc = handleEventsForInputEventPolyfill;
}
} else if (shouldUseClickEvent(targetNode)) {
getTargetInstFunc = getTargetInstForClickEvent;
}
if (getTargetInstFunc) {
const inst = getTargetInstFunc(domEventName, targetInst);
if (inst) {
createAndAccumulateChangeEvent(
dispatchQueue,
inst,
nativeEvent,
nativeEventTarget,
);
return;
}
}
if (handleEventFunc) {
handleEventFunc(domEventName, targetNode, targetInst);
}
if (domEventName === 'focusout') {
handleControlledInputBlur(((targetNode: any): HTMLInputElement));
}
}
createAndAccumulateChangeEvent
function createAndAccumulateChangeEvent(
dispatchQueue,
inst,
nativeEvent,
target,
) {
// 生成合成事件
const event = new SyntheticEvent(
'onChange',
'change',
null,
nativeEvent,
target,
);
// Flag this event loop as needing state restore.
enqueueStateRestore(((target: any): Node));
accumulateTwoPhaseListeners(inst, dispatchQueue, event);
}
🔥 accumulateTwoPhaseListeners 事件分发
export function accumulateTwoPhaseListeners(
targetFiber: Fiber | null,
dispatchQueue: DispatchQueue, // []
event: ReactSyntheticEvent, // onChange合成事件
): void {
const bubbled = event._reactName; // 就是“onChange”
const captured = bubbled !== null ? bubbled + 'Capture' : null; // 就是“onChangeCapture”
const listeners: Array<DispatchListener> = [];
let instance = targetFiber;
// 这边向上查找到所有当前类型事件的回调函数,重要!!!
while (instance !== null) {
const {stateNode, tag} = instance;
if (tag === HostComponent && stateNode !== null) {
const currentTarget = stateNode;
if (captured !== null) {
// 返回当前节点的回调函数
// export default function getListener(
// inst: Fiber, // 当前实例
// registrationName: string, // “onChange”
// ): Function | null {
// ...
// // 返回dom上的props
// const props = getFiberCurrentPropsFromNode(stateNode);
// if (props === null) {
// // Work in progress.
// return null;
// }
// // 获取到当前事件的回调函数
// const listener = props[registrationName]
// return listener;
// }
const captureListener = getListener(instance, captured);
if (captureListener != null) {
// 捕获,插入数组头部
listeners.unshift(
// 工具函数,返回对象{instance, listener, currentTarget}
createDispatchListener(instance, captureListener, currentTarget),
);
}
}
if (bubbled !== null) {
// 返回当前节点的回调函数
const bubbleListener = getListener(instance, bubbled);
if (bubbleListener != null) {
// 冒泡,插入数组尾部
listeners.push(
// 工具函数,返回对象{instance, listener, currentTarget}
createDispatchListener(instance, bubbleListener, currentTarget),
);
}
}
}
instance = instance.return;
}
// listeners即某一类合成事件的所有回调函数的集合,[{instance, listener, currentTarget}, ...]
if (listeners.length !== 0) {
// createDispatchEntry返回的是对象{event, listeners};
// dispatchQueue最后为[{event, listeners}, ...], 即[{onChange, [{instance, listener, currentTarget}, ...]}, ...]
dispatchQueue.push(createDispatchEntry(event, listeners));
}
}
🔥 processDispatchQueue事件执行
export function processDispatchQueue(
dispatchQueue: DispatchQueue,
eventSystemFlags: EventSystemFlags,
): void {
const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
// 循环取出合成事件和对应的回调函数队列
for (let i = 0; i < dispatchQueue.length; i++) {
const {event, listeners} = dispatchQueue[i];
// 逐个执行每个回调函数
processDispatchQueueItemsInOrder(event, listeners, inCapturePhase);
// event system doesn't use pooling. 不在使用事件池
}
// This would be a good time to rethrow if any of the event handlers threw.
rethrowCaughtError();
}
processDispatchQueueItemsInOrder
function processDispatchQueueItemsInOrder(
event: ReactSyntheticEvent,
dispatchListeners: Array<DispatchListener>,
inCapturePhase: boolean,
): void {
let previousInstance;
if (inCapturePhase) {
for (let i = dispatchListeners.length - 1; i >= 0; i--) {
const {instance, currentTarget, listener} = dispatchListeners[i];
if (instance !== previousInstance && event.isPropagationStopped()) {
return;
}
executeDispatch(event, listener, currentTarget);
previousInstance = instance;
}
} else {
for (let i = 0; i < dispatchListeners.length; i++) {
const {instance, currentTarget, listener} = dispatchListeners[i];
if (instance !== previousInstance && event.isPropagationStopped()) {
return;
}
executeDispatch(event, listener, currentTarget);
previousInstance = instance;
}
}
}
executeDispatch
function executeDispatch(
event: ReactSyntheticEvent, // onChange
listener: Function, // 对应的回调函数
currentTarget: EventTarget,
): void {
// "onChange"
const type = event.type || 'unknown-event';
// 将当前dom元素赋值给合成事件的currentTarget
event.currentTarget = currentTarget;
// 执行回调函数,listener为回调函数, event为合成事件,最后执行listener(event)这个方法调用
// 这样就回调到了我们在JSX中注册的callback。比如onClick={(event) => {console.log(1)}}
// 现在就明白了callback怎么被调用的,以及event参数怎么传入callback里面的了
invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event);
event.currentTarget = null;
}
// invokeGuardedCallbackAndCatchFirstError 函数的本质是如下函数
// shared/invokeGuardedCallbackImpl.js
function invokeGuardedCallbackProd<A, B, C, D, E, F, Context>(
name: string | null,
func: (a: A, b: B, c: C, d: D, e: E, f: F) => mixed,
context: Context,
a: A,
b: B,
c: C,
d: D,
e: E,
f: F,
) {
const funcArgs = Array.prototype.slice.call(arguments, 3);
try {
//最后就是在这里执行的回调函数
func.apply(context, funcArgs);
} catch (error) {
this.onError(error);
}
}
FAQ
为什么需要手动绑定this
invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event)
该方法第三个参数是undefined
对应底层invokeGuardedCallbackProd
函数调用时的参数context
那么 func.apply(context, funcArgs)
函数的this 就是undefined 所以需要你定义回调函数的this指向,比如使用箭头函数
React事件和原生事件的执行顺序
当点击test时,如下代码的输出顺序是什么呢?
componentDidMount() {
this.parent.addEventListener('click', (e) => {
console.log('dom parent');
})
this.child.addEventListener('click', (e) => {
console.log('dom child');
})
document.addEventListener('click', (e) => {
console.log('document');
})
}
childClick = (e) => {
console.log('react child');
}
parentClick = (e) => {
console.log('react parent');
}
render() {
return (
<div onClick={this.parentClick} ref={ref => this.parent = ref}>
<div onClick={this.childClick} ref={ref => this.child = ref}>
test
</div>
</div>)
}
输出结果如下:
dom child
dom parent
react child
react parent
document
由上面的流程我们可以理解:
- react的所有事件都挂载在document中
- 当真实dom触发后冒泡到document后才会对react事件进行处理
- 所以原生的事件会先执行
- 然后执行react合成事件
- 最后执行真正在document上挂载的事件
React v17中的事件
更改事件委托
在 React 17 中,React 将不再向 document 附加事件处理器。而会将事件处理器附加到渲染 React 树的根 DOM 容器中
在 React 16 或更早版本中,React 会对大多数事件执行 document.addEventListener()。React 17 将会在底层调用 rootNode.addEventListener()。
例如,如果模块中使用 document.addEventListener(...) 手动添加了 DOM 监听,你可能希望能捕获到所有 React 事件。在 React 16 或更早版本中,即使你在 React 事件处理器中调用 e.stopPropagation(),你创建的 DOM 监听仍会触发,这是因为原生事件已经处于 document 级别。使用 React 17 冒泡将被阻止(按需),因此你的 document 级别的事件监听不会触发:
去除事件池
React 17 中移除了 “event pooling(事件池)“。它并不会提高现代浏览器的性能,甚至还会使经验丰富的开发者一头雾水。
Diffing Algorithm
三个基本策略
- 只对同级的 react element进行对比。如果一个DOM节点在前后两次更新中跨域了层级,那么则不会复用它。比较点为父节点的不同。
- 不同类型节点(type 值不同 和key值)生成的dom树不同,此时会直接销毁老节点及子孙节点,并新建节点
- 可以通过key来对元素diff的过程提供复用的线索
同级节点Diff
同级节点Diff 分为 同级单节点Diff 和 同级多节点Diff
同级节点Diff
同级单节点diff有如下几种情况:
- key 和 type 相同代表可以复用
- key 不同直接删除标记节点 新建节点
- key相同type不同,标记删除该节点和兄弟节点,然后新创建节点
同级多节点Diff
同级多节点时有如下几种情况
节点更新(类型、属性更新)
节点新增或者删除
节点移动
同级节点Diff需要两次遍历,React团队认为在日常开发中,组件更新的频率最高。所以第一次遍历会处理更新的节点,第二轮遍历:处理剩下的不属于更新
的节点。
第一轮遍历
因为老的节点存在于current Fiber中,所以它是个链表结构,还记得Fiber双缓存结构嘛,节点通过child、return、sibling连接,而newChildren存在于jsx当中,所以遍历对比的时候,首先让newChildren[i]与
oldFiber对比,然后让i++、nextOldFiber = oldFiber.sibling。
- 从第一个节点开始遍历(i = 0),判断新、旧节点的类型(type)是否相同和 key 是否相同,如果 type 和 key 都相同,则说明对应的 DOM 可复用;
- 如果这个节点对应的 DOM 可复用,则 i++,去判断下一组新、旧节点的 type 和 key,看它们对应的 DOM 是否可复用,如果可以复用,则重复步骤 2;
- 如果不可复用,分两种情况:
key
不同导致不可复用,立即跳出整个遍历,第一轮遍历结束。key
相同type
不同导致不可复用,会将oldFiber
标记为DELETION
,并继续遍历
- 如果
newChildren
遍历完(即i === newChildren.length - 1
)或者oldFiber
遍历完(即oldFiber.sibling === null
),跳出遍历,第一轮遍历结束。
当遍历结束后,会有两种结果:
- 步骤3跳出的遍历,此时 newChildren 没有遍历完,oldFiber 也没有遍历完
- 步骤4跳出的遍历 可能是 newChildren 遍历完 或者 oldFiber 遍历完 或者他们同时遍历完
第二轮遍历
对于第一轮遍历的结果,我们分别讨论
newChildren 和 oldFiber 同时遍历完,这是最理想的情况,只要一轮遍历进行组件更新,此时diff结束
newChildren 没有遍历完、oldFiber遍历完
已有的
DOM节点
都复用了,这时还有新加入的节点,意味着本次更新有新节点插入,我们只需要遍历剩下的newChildren
为生成的workInProgress fiber
依次标记 Placement 新增。newChildren 遍历完、oldFiber没有遍历完
意味着本次更新比之前的节点数量少,有节点被删除了。所以需要遍历剩下的
oldFiber
,依次标记Deletion 删除
。newChildren 与 oldFiber都没有遍历完 这意味着有节点在这次更新中改变了位置,这是
Diff算法
最精髓也是最难懂的部分为了快速找到key对应的oldFiber 我们将所有还未处理的 oldFiber 存入以key 为key ,oldFiber 为value 的Map中。这个 map 叫做
existingChildren
接下来遍历剩余的newChildredn 通过newChildren[i].key 就能在map中找到key相同的oldFiber
如果能找到 key相同的oldFiber 接下来就是判断它们的 type 是否相同:
- 假如 key 相同、type 也相同,说明该节点对应的 DOM 可复用,只是位置发生了变化;
- 假如 key 相同、type 不同,则该节点对应的 DOM 不可复用,需要销毁原来的节点,并重新插入一个新的节点;
如果找不到的话,代表是一个新增节点
以上两种情况处理了新增和删除的 剩下节点移动的
这里有一个基准点的概念 React 使用
lastPlacedIndex
这个变量来存放「参考点」lastPlacedIndex
这个变量表示当前最后一个可复用的节点,对应在「旧同级节点链表」中的索引。初始值为 0在遍历剩下的 newChildren时,每一个新节点会通过
existingChildren
找到对应的旧节点,然后就可以得到旧节点的索引oldIndex
(即在「旧同级节点链表」中的位置)。接下来会进行以下判断:
- 假如
oldIndex
>=lastPlacedIndex
,代表该复用节点不需要移动位置,并将 lastPlacedIndex = oldIndex; - 假如
oldIndex
<lastPlacedIndex
,代表该节点需要向右移动,并且该节点需要移动到上一个遍历到的新节点的后面;
Redux
三大原则
- 单一数据源
一个应用永远只有唯一的数据源
- 状态是只读的
不能直接的修改应用的状态,但是可以利用store.dispatch达到修改状态的目的
- 状态修改均由纯函数完成
通过定义reducer来确定状态的修改,每一个reducer 都是纯函数。
数据流如图:
核心API
Redux 的核心是一个 store
,这个 store
由Redux提供的 createStore(reducers[,initialState])
方法生成。
createStore
函数具有2个参数,第一个参数为必须传入的 reducers
,第二个参数为可以选的初始化状态 initialState
reducer
在Redux里,负责响应action并修改数据的角色就是reducer。reducer本质上是一个函数,其函数签名为 reducer(perviousState,action)=>newState
。可以看出reducer的职责是根据perviousState和action 计算出新的 newState
当reducer第一次执行的时候,并没有任何的perviousState, 但是需要返回一个新的newState,但是就会需要一个初始值initialState
createStore
createStore是Redux中最核心的API。通过该方法可以生成一个store 对象。该store对象本身具有4个方法。
- getState():获取store中当前的状态
- dispatch(action):分发一个action,并返回这个action,这是唯一能改变store中数据的方式
- subscribe(listener):注册一个监听者,它在store发生变化时被调用
- replaceReducer(nextReducer):更新当前store里的reducer,一般只会在开发模式中调用
react-redux 的核心组件只有两个,Provider 和 connect,Provider 存放 Redux 里 store 的数据到 context 里,通过 connect 从 context 拿数据,通过 props 传递给 connect 所包裹的组件。
Recoil
Recoil是React的状态管理库,由Facebook官方推出,更加的贴合react内部的调用机制。官网文档链接
核心概念
使用Recoil,可以创建一个数据流图,该图从atoms(共享状态)通过selectors(纯函数)一直流到React组件。
Atom是组件可以预订的状态单位。
selectors是可以同步或异步转换此状态。
Atoms(原子)
Atom是最小状态单位。它们是可更新和可订阅的,当Atom被更新时,每个订阅的组件都将用新值重新呈现。如果从多个组件中使用同一个 Atom ,所有这些组件都会共享它们的状态。
使用atom函数来创建Atoms:
const fontSizeState = atom({
key: 'fontSizeSstate',
default: 12
})
原子需要一个独一无二的key,全局唯一。你可以使用Symobl
类型作为key值。
Selectors
Selector 是一个入参为 Atom 或者其他 Selector 的纯函数。当它的上游 Atom 或者 Selector 更新时,它会进行重新计算。Selector 可以像 Atom 一样被组件订阅,当它更新时,订阅它的组件将会重新渲染。
使用 selector 方法创建 Selector 实例。
const fontSizeLabelState = selector({
key: 'fontSizeLabelState',
get: ({get}) => {
const fontSize = get(fontSizeState);
const unit = 'px';
return `${fontSize}${unit}`;
},
});
get 属性是一个计算函数,它可以使用入参 get 字段来访问输入的 Atom 和 Selector。当它访问其他 Atom 和 Selector 时,这层依赖关系会保证更新状态的同步。
接下来我们简单的来学习使用Recoil
初始化
使用Recoil需要使用RecoilRoot
将组件包裹
import React from 'react'
import {
RecoilRoot,
atom,
selector,
useRecoilState,
useRecoilValue,
} from 'recoil';
function App() {
return (
<RecoilRoot>
<CharacterCounter />
</RecoilRoot>
);
}
订阅和更新状态
Recoil 采用 Hooks 方式订阅和更新状态,常用的是下面三个 API:
useRecoilState
类似useState的一个Hook,可以取到 atom 的值 和 setter 函数useSetRecoilState
只获取setter函数 如果只使用了这个函数,状态变化不会导致组件重新渲染useRecoilValue
只获取状态
const atomKey = Symobl('atom')
const textState = atom({
key: atomKey, // unique ID (with respect to other atoms/selectors)
default: '', // default value (aka initial value)
});
function CharacterCounter() {
return (
<div>
<TextInput />
<CharacterCount />
</div>
);
}
function TextInput() {
const [text, setText] = useRecoilState(textState);
const onChange = (event) => {
setText(event.target.value);
};
return (
<div>
<input type="text" value={text} onChange={onChange} />
<br />
Echo: {text}
</div>
);
}
派生状态
selector 表示一段派生状态,它使我们能够建立依赖于其他 atom 的状态。它有一个强制性的 get 函数。
const selectorKey = Symobl('selector')
const charCountState = selector({
key: selectorKey, // unique ID (with respect to other atoms/selectors)
get: ({get}) => {
const text = get(textState);
return text.length;
},
});
function CharacterCount() {
const count = useRecoilValue(charCountState);
return <>Character Count: {count}</>;
}
异步状态
Recoil提供了一种通过数据流图将状态和派生状态映射到React组件的方法。真正强大的功能是图中的函数也可以是异步的。这使得在异步React组件渲染函数中轻松使用异步函数成为可能. 只需从选择器get回调中将Promise返回值,而不是返回值本身.
例如下面的例子,如果用户名存储在我们需要查询的某个数据库中,那么我们要做的就是返回一个 Promise 或使用一个 async 函数。如果任何依赖项发生更改,则将重新评估选择器并执行新查询。结果将被缓存,因此查询将仅对每个唯一输入执行一次。
const currentUserNameQuery = selector({
key: 'CurrentUserName',
get: async ({get}) => {
const response = await myDBQuery({
userID: get(currentUserIDState),
});
return response.name;
},
});
function CurrentUserInfo() {
const userName = useRecoilValue(currentUserNameQuery);
return <div>{userName}</div>;
}
Recoil 推荐使用 Suspense,Suspense 将会捕获所有异步状态,另外配合 ErrorBoundary 来进行错误捕获:
function MyApp() {
return (
<RecoilRoot>
<ErrorBoundary>
<React.Suspense fallback={<div>Loading...</div>}>
<CurrentUserInfo />
</React.Suspense>
</ErrorBoundary>
</RecoilRoot>
);
}
总结:上诉内容只是简单的介绍和使用了Recoil,属于入门级,需要深入的理解和项目中使用可以查看文档,和社区成熟示例
参考资料链接:
requestIdleCallback
用法与说明
window.requestIdleCallbacck()
方法将在浏览器的空闲时段内调用的函数排队。这使得开发者能够在主事件循环上执行后台和低优先级工作。、
语法:var handle = window.requestIdleCallback(callback[,options])
返回值是一个ID,可以把它传入window.cancelIdleCallback()
方法来结束回调
参数:
callback 一个在事件循环空闲时即将被调用的函数引用。函数接收一个名为 IdleDeadline 的参数。该参数具有一个
timeRemaining()
方法 返回当前frame还剩多少时间和didTimeout属性用来判断当前的回调函数是否被执行。如果没有执行didTimeout属性将为tureoptions 可选 包括可选的配置参数。具有如下属性:
- timeout:如果指定了timeout并具有一个正值,并且尚未通过超时毫秒数调用回调,那么回调会在下一次空闲时期被强制执行,尽管这样很可能会对性能造成负面影响。
requestIdleCallback中可以传递{timeout: 2000}表明2s内必须调用myNonEssentialWork,如果myNonEssentialWork调用是因为timeout则didTimeout为true。
示例:
requestIdelCallback(myNonEssentialWork);
function myNonEssentialWork (deadline) {
// deadline.timeRemaining()可以获取到当前帧剩余时间
while (deadline.timeRemaining() > 0 && tasks.length > 0) {
doWorkIfNeeded();
}
if (tasks.length > 0){
requestIdleCallback(myNonEssentialWork);
}
}
兼容性与缺陷
requestIdleCallback is called only 20 times per second - Chrome on my 6x2 core Linux machine, it's not really useful for UI work。—— from Releasing Suspense
requestIdleCallback 的 FPS 只有 20, 这远远低于页面流畅度的要求!(一般 FPS 为 60 时对用户来说是感觉流程的, 即一帧时间为 16.7 ms)
requestIdleCallback 的兼容性不太好在safari中不支持
React中的polyfill版本
非DOM环境下
在不能操作DOM的环境下可以使用setTimeout来实现。比如node环境中
requestIdleCallback = (callback) => {
setTimeout(callback({
timeRemaining() {
return Infinity
}
}))
}
DOM环境下
requestAnimationFrame + 计算帧时间及下一帧时间 + MessageChannel 就是我们实现 requestIdleCallback 的三个关键点了。
但是requestAnimationFrame又一点小瑕疵。页面处于后台时该回调函数不会执行,因此我们需要对于这种情况做个补救措施
React中当 requestAnimationFrame 不执行时,会有 setTimeout 去补救,两个定时器内部可以互相取消对方。
const ANIMATION_FRAME_TIMEOUT = 100;
let rAFID;
let rAFTimeoutID;
// 调用 requestAnimationFrame, 并对执行时间超过 100 ms 的任务用 setTimeout 进行处理
const requestAnimationFrameWithTimeout = function (callback) {
rAFID = requestAnimationFrame(function (timestamp) {
clearTimeout(rAFTimeoutID);
callback(timestamp);
});
// 如果在一帧中某个任务执行时间超过 100 ms 则终止该帧的执行并将该任务放入下一个事件队列中
rAFTimeoutID = setTimeout(function () {
cancelAnimationFrame(rAFID);
callback(getCurrentTime());
}, ANIMATION_FRAME_TIMEOUT);
};
requestHostCallback(也就是 requestIdleCallback) 这部分源码的实现比较复杂, 可以将其分解为以下几个重要的步骤(有一些细节点可以看注释):
步骤一: 如果有优先级更高的任务, 则通过 postMessage 触发步骤四, 否则如果 requestAnimationFrame 在当前帧没有安排任务, 则开始一个帧的流程;
步骤二: 在一个帧的流程中调用 requestAnimationFrameWithTimeout 函数, 该函数调用了 requestAnimationFrame, 并对执行时间超过 100ms 的任务用 setTimeout 放到下一个事件队列中处理;
步骤三: 执行 requestAnimationFrame 中的回调函数 animationTick, 在该回调函数中得到当前帧的截止时间 frameDeadline, 并通过 postMessage 触发步骤四;
步骤四: 通过 onmessage 接受 postMessage 指令, 触发消息事件的执行。在 onmessage 函数中根据 frameDeadline - currentTime <= 0 判断任务是否可以在当前帧执行,如果可以的话执行该任务, 否则进入下一帧的调用。
let scheduledHostCallback = null; // 调度器回调函数
let isMessageEventScheduled = false; // 消息事件是否执行
let timeoutTime = -1;
let isAnimationFrameScheduled = false;
let isFlushingHostCallback = false;
let frameDeadline = 0; // 当前帧的截止时间
// 假设最开始的 FPS(feet per seconds) 为 30, 但这个值会随着动画帧调用的频率而动态变化
let previousFrameTime = 33; // 一帧的时间: 1000 / 30 ≈ 33
let activeFrameTime = 33;
// 建立通道
const channel = new MessageChannel();
const port = channel.port2;
shouldYieldToHost = function () {
return frameDeadline <= getCurrentTime();
};
getCurrentTime = function () {
return performance.now();
};
// 步骤一
requestHostCallback = function (callback, absoluteTimeout) {
scheduledHostCallback = callback; // 这里的 callback 为调度器回调函数
timeoutTime = absoluteTimeout;
if (isFlushingHostCallback || absoluteTimeout < 0) {
// 针对优先级较高的任务不等下一个帧,在当前帧通过 postMessage 尽快执行
port.postMessage(undefined);
} else if (!isAnimationFrameScheduled) {
// 如果 rAF 在当前帧没有安排任务, 则开始一个帧的流程
isAnimationFrameScheduled = true;
requestAnimationFrameWithTimeout(animationTick);
}
};
// 步骤二 上述代码requestAnimationFrameWithTimeout部门
// 步骤三 requestAnimationFrame 的回调函数。传入的 rafTime 为执行该帧的时间戳。
const animationTick = function (rafTime) {
// 如果存在调度器回调函数则在一帧的开头急切地安排下一帧的动画回调(急切是因为如果在帧的后半段安排动画回调的话, 就会增大下一帧超过 100ms 的几率, 从而会浪费一个帧的利用, 可以结合步骤②来理解这句话), 如果不存在调度器回调函数否则立马终止执行。
if (scheduledHostCallback !== null) {
requestAnimationFrameWithTimeout(animationTick);
} else {
isAnimationFrameScheduled = false;
return;
}
let nextFrameTime = rafTime - frameDeadline + activeFrameTime; // 当前帧开始调用动画的时间 - 上一帧调用动画的截止时间 + 当前帧执行的时间,这里的 nextFrameTime 仅仅是临时变量
// 如果连续两帧的时间都小于当前帧的时间, 则说明得调高 FPS
if (nextFrameTime < activeFrameTime && previousFrameTime < activeFrameTime) {
// 将 activeFrameTime 的值减小相当于调高 FPS。同时取 nextFrameTime 与 previousFrameTime 中较大的一个以让前后两帧都不出问题。
activeFrameTime = nextFrameTime < previousFrameTime ? previousFrameTime : nextFrameTime;
} else {
previousFrameTime = nextFrameTime;
}
frameDeadline = rafTime + activeFrameTime; // 当前帧的截止时间(上面几行代码的目的是得到该 frameDeadline 值, 该值在 postMessage 会用来判断)
if (!isMessageEventScheduled) {
isMessageEventScheduled = true;
port.postMessage(undefined); // 最后进入第④步, 通过 postMessage 触发消息事件。
}
};
// 步骤四 接受 `postMessage` 指令, 触发消息事件的执行。在其中判断任务是否在当前帧执行,如果在的话执行该任务
channel.port1.onmessage = function (event) {
isMessageEventScheduled = false;
const prevScheduledCallback = scheduledHostCallback;
const prevTimeoutTime = timeoutTime;
scheduledHostCallback = null;
timeoutTime = -1;
const currentTime = getCurrentTime();
let didTimeout = false; // 是否超时
// 如果当前帧已经没有时间剩余, 检查是否有 timeout 参数,如果有的话是否已经超过这个时间
if (frameDeadline - currentTime <= 0) {
if (prevTimeoutTime !== -1 && prevTimeoutTime <= currentTime) {
// didTimeout 为 true 后, 在当前帧中执行(针对优先级较高的任务)
didTimeout = true;
} else {
// 在下一帧中执行
if (!isAnimationFrameScheduled) {
isAnimationFrameScheduled = true;
requestAnimationFrameWithTimeout(animationTick);
}
scheduledHostCallback = prevScheduledCallback;
timeoutTime = prevTimeoutTime;
return;
}
}
if (prevScheduledCallback !== null) {
isFlushingHostCallback = true;
try {
prevScheduledCallback(didTimeout);
} finally {
isFlushingHostCallback = false;
}
}
};
// 取消
cancelHostCallback = function () {
scheduledHostCallback = null;
isMessageEventScheduled = false;
timeoutTime = -1;
};
requestAnimationFrame
window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行
语法
window.requestAnimationFrame(callback)
该callback会被传入DOMHighResTimeStamp参数,该参数与performance.now()的返回值相同,它表示requestAnimationFrame() 开始去执行回调函数的时刻。
缺陷
为了提高性能和电池寿命,因此在大多数浏览器里,当requestAnimationFrame() 运行在后台标签页或者隐藏的<iframe>
里时,requestAnimationFrame()
会被暂停调用以提升性能和电池寿命。