react api 整理

本文档意在整理 react 顶层 api 的内容和实现,可参阅react 官方文档。React 包主要提供 api 接口,功能上的核心逻辑点通常由 react-dom 等包实现。

组件

React 组件可将视图内容拆分为独立的、可复用的单元,以便在组件的模块制作过程中独立实现视图逻辑和业务逻辑。在 es6 语法背景下,React 组件可基于 React.Component, React.PureComponent 类加以制作。如果没有使用 es6 语法,可以使用 create-react-class 库。除此以外,也可以编写函数式组件。该函数式组件能用 React.memo 加以包裹。

在源码中,React.Component, React.PureComponent 类由 ReactBaseClasses 模块提供。

Component

Component(props, context, updater) 基类在构造函数中初始化 props, context, refs, updater 实例属性,并包含 isReactComponent, setState, forceUpdate 原型方法。其中,updater 属性在实例化阶段将赋值为默认的 ReactNoopUpdateQueue,渲染阶段在注入实际的 updater。setState, forceUpdate 原型方法基于 updater.enqueueSetState, updater.enqueueForceUpdate 构建。

PureComponent

PureComponent(props, context, updater) 与 Component 基类拥有相同的实例属性,其原型对象也通过桥接函数赋值的形式重新构造、且混入了 Component 基类的原型方法,除此之外,PureComponent 还具有 isPureReactComponent 原型方法。

生命周期

可参考 React 组件生命周期

  1. 挂载阶段:
    • constructor(props): 实例化。
    • static getDeriverdStateFromProps 从 props 中获取 state。
    • render 渲染。
    • componentDidMount: 完成挂载。
  2. 更新阶段:
    • static getDeriverdStateFromProps 从 props 中获取 state。
    • shouldComponentUpdate 判断是否需要重绘。
    • rendere 渲染。
    • getShapshotBeforeUpdate 获取快照。
    • componentDidUpdate 渲染完成后回调。
  3. 卸载阶段:
    • componentWillUnmount 即将卸载。
  4. 错误处理:
    • static getDerivedStateFromError 从错误中获取 state。
    • componentDidCatch 捕获错误并进行处理。

React.memo

React.memo(funcComponent, compare) 用于创建高阶组件,使函数式组件具有如 PureComponent 的效果。在默认情况下,它会浅比较接受到的 props,当然,在提供 compare(prevProps, nextProps) 参数的场景中,你也可以定制重绘时机。

1
2
3
4
function MyComponent(props){};
function areEqual(prevProps, nextProps){};

export default React.memo(MyComponent, areEqual);

实现上,memo 函数会构建类 React 元素数据如 { $$typeof, type, compare }。其中,$$typeof 为来自 shared/ReactSymbols 包下的常量 REACT_MEMO_TYPE;type 为首参函数式组件;compare 即次参对比函数。

元素

React 元素可使用 JSX 语法书写,也可以使用 createElement, createFactory 方法构建。react-hyperscript, hyperscript-helpers 这两个类库也提供了创建 React 元素的便捷语法糖。

除了 createElement, createFactory 方法以外,React 还提供 cloneElementAndReplaceKey, cloneElement, isValidElement 用于克隆元素或者校验元素。这些方法均由 ReactElement 模块输出。而 React.children 用于处理对子元素的操作。

Element

ReactElement(type, key, ref, self, source, owner, props) 作为创建元素的工厂函数,将构建 element 常量并返回。其中,element 包含可枚举可赋值的 $$typeof, type, key, ref, props, _owner 属性,$$typeof 属性为特定常量 REACT_ELEMENT_TYPE。在开发环境中,element 又包含 _store 存储校验标识(_store.validated),不可枚举不可赋值的 _self, _source 属性;且 element, element.props 均使用 Object.freeze 冻结。

createElement(type, config, children) 使用特定的自定义或内置组件 type 创建元素。参数 config 通过 key, ref, self, source 配置元素的 key, ref, self, source 属性,其余属性将作为元素的 props。children 用于配置元素下割的子元素。在创建元素时,ReactCurrentOwner.current 将作为元素的 _owner 属性。ReactCurrentOwner.current 值为渲染过程中的 Fiber 实例。

createFactory(type) 为特定的组件创建工厂函数。

cloneAndReplaceKey(oldElement, newKey) 使用 oldElement 组件构造器 oldElement.type 以及 ref, props, _owner, _self, _source 属性构建新的元素,该元素的 key 属性指定为 newKey。

cloneElement(element, config, children) 与 cloneAndReplaceKey 不同的是,该方法在克隆元素时可以重新配置 key, ref, props, children 属性。如果重置 ref 属性时,克隆元素的 _owner 属性也将同步更新为渲染过程中的 ReactCurrentOwner.current。

isValidElement(object) 通过校验参数 object 是否为对象且其 $$typeof 属性为 REACT_ELEMENT_TYPE,以判断是否 React 元素。

Children

forEach(children, forEachFunc, forEachContext) 遍历子元素,执行 forEachFunc 函数。

map(children, func, context) 遍历子元素,执行 func 函数。功能点同 forEach 方法,但是返回数组。

toArray(children) 将子元素转化为数组。

count(children) 计算子元素的数目。

only((children)) 校验参数 children 是否单一的 React 元素,并返回。

实现上,React 以 traverseAllChildren(children, callback, traverseContext) 函数作为遍历子元素的 api。traverseAllChildren 函数通过递归调用 traverseAllChildrenImpl(children, nameSoFar, callback, traverseContext) 遍历子元素,并校验子元素集合不能由 Map, Object 对象构建。对于回调的执行机制,React 会先使用 getPooledTraverseContext(mapResult, keyPrefix, mapFunction, mapContext) 将 forEach 方法的参数 forEachFunc, forEachContext 或者 map 方法的参数 func, context 组装成 traverseContext 对象(该对象还包含用于收集子元素的数组 mapResult 和元素 key 键的公共前缀 keyPrefix 属性)。在 traverseContext 对象的基础上,React 对 forEach, map 构建了单独的回调包装函数 forEachSingleChild, mapSingleChildIntoContext,以处理特定的逻辑。具体实现可参阅源码。

多个元素在渲染时可使用 React.Fragment 组件包裹,那样就不必创建额外的 dom 节点。React 输出的 Fragment 组件直接来自于 shared/ReactSymbols 包下的常量 REACT_FRAGMENT_TYPE。

1
2
3
4
<React.Fragment>
Some text.
<h2>A heading</h2>
</React.Fragment>

Refs

createRef

React.createRef 创建 refObject 对象,该对象可以作为 React 元素的 ref 属性,以此引用指定的 React 元素。React 包下只创建 refObject 对象并使用 Object.seal 加以密封,核心逻辑由其他包提供。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MyComponent extends React.Component {
constructor(props){
super(props);
this.input = React.createRef();
}

componentDidMount(){
this.input.current.focus();
}

render(){
return <input type='text' ref={this.input} />
}
}

forwardRef

React.forwardRef 通过创建组件的方式将其所接受的 ref 引用配置长传给其子孙组件。forwardRef 有两个应用场景:为函数式组件指定引用;为高阶组件指定引用。

1
2
3
4
5
6
7
8
cosnt FancyButton = React.forwardRef((props, ref) => (
<button ref={ref} className='FancyButton'>
{props.children}
</button>
));

const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;

上述代码可将创建的 refObject 对象通过 forwardRef 的次参传入函数式组件,以便引用原生 dom 组件。虽然这样创建 ref 引用会增加 FancyButton 与其父元素的层级关联,造成一定的复杂度,但是当 FancyButton 组件被多个应用级组件所使用时,且这些应用级组件都要细微地操作 button 节点,通过 forwardRef 长传 ref 引用就必不可少了。

在制作高阶组件时,同样可以使用 React.forwardRef 将 ref 引用转变为特定的 props 属性并传入高阶组件中,那样被包裹的组件就可以使用该 props 属性设置 ref 引用了。代码实现如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;
return <Component ref={forwardedRef} {...rest} />
}
}

return React.forwardRef((props, ref) => {
return <LogProps {...props} forwaredRef={ref} />;
})
}

在实现中,forwardRef 将校验参数是否为函数且包含两个参数(通过 length 属性校验)等,最终返回类 React 元素结构如 { $$typeof, render }。其中,$$typeof 为常量 REACT_FORWARD_REF_TYPE,render 即 forwardRef 的参数。

Suspense

React.Suspense 支持在某事件执行完成后渲染组件。目前只支持一种应用场景:通过 React.lazy 动态加载组件,在组件加载完成后,再行渲染。

1
2
3
4
5
const LazyComponent  = React.lazy(() => import('./OtherComponent.js'));

<Suspense fallback={<div>loading...</div>}>
<LazyComponent>
</Suspense>

在 OtherComponent 组件加载过程中,React 将使用 fallback 渲染元素;当 OtherComponent 加载完成后,视图将显示 OtherComponent 组件。Suspense 组件内允许渲染多个懒加载组件。在组件加载失败的场景中,可以构建实现了 getDerivedStateFromError(error) 静态方法以及 componentDidCatch(error, info) 生命周期方法的 ErrorBoundary 组件捕获错误并加以处理。

使用 React.lazy 动态加载的组件,不止可以作为 Suspense 组件的子元素,还可以作为 Route 组件的 component,以在路由层面实现动态加载。

在实现上,React.lazy 方法将构建类 React 元素的数据结构如 { $$typeof, _ctor, _status, _result }。其中,$$typeof 为常量 REACT_LAZY_TYPE,_ctor 为 React.lazy 接受的参数。React 输出的 Suspense 组件直接来自于 shared/ReactSymbols 包下的常量 REACT_SUSPENSE_TYPE。

createContext

Context 可视为 React 组件树的全局数据(比如验权后的用户信息、网页的主题风格、显示的语言),用于向子孙组件透传数据,而不必通过 props 属性逐层传递、或者将在顶层组件中将实际消费数据的子组件作为 children 传入中介组件。

Context 使用的方式为:

  1. 使用 React.createContext(defaultValue) 创建 Context 对象。当 React 组件订阅了该 Context 对象时,该组件将从就近且匹配的 Provider 中读取 Context 对象。如果没有匹配的 Provider,那就会使用 defaultValue。
  2. Context.Provider 以父组件的形式作为 Context 对象的提供者,当其重绘时,将迫使订阅数据的组件相应重绘。
  3. 在自定义组件中添加 contextType 静态属性,当其值为 Context 对象时,就可以通过组件实例的 context.value 访问实际透传的数据。
  4. 第 3 步也可以替代为第 4 步,使用 Context.Consumer 作为数据的消费者,其下可以用 value => ReactNode 的形式编写函数式组件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
const ThemeContext = React.createContext('light');
const UserContext = React.createContext({ name: 'Guest' });

class App extends React.Component {
render(){
const { signedInUser, theme } = this.props;

return (
<ThemeContext.Provider value={theme}>
<UserContext.Provider value={signedInUser}>
<Layout />
</UserContext.Provider>
</ThemeContext.Provider>
);
}
}

function Layout(){
return (
<div>
<Sidebar />
<Context />
</div>
)
}

function Context(){
return (
<ThemeContext.Consumer>
{theme => (
<UserContext.Consumer>
{user => (
<ProfilePage user={user} theme={theme} />
)}
</UserContext.Consumer>
)}
</ThemeContext.Consumer>
)
}

实现上,createContext(defaultValue, calculateChangeBits) 方法会创建类 React 元素数据如 { $$typeof, _calculateChangedBits, _currentValue, _currentValue2, _threadCount, Provider, Consumer }。其中,$$typeof 为常量 REACT_CONTEXT_TYPE。Context.Provider 创建类 React 元素数据如 { $$typeof, _context }。其中,$$typeof 为常量 REACT_PROVIDER_TYPE;_context 即引用 Context 对象。Context.Consumer 在生产环境中就是Context 引用对象,开发环境将校验不能使用嵌套形式编码如 Context.Consumer.Provider, Context.Consumer.Consumer。

ReactHooks

借助于 HOC 或者 render props,你可以整合组件内的可重用逻辑。比如当弹窗 Modal 组件内包含多个可切换的表单组件时,可以使用 render props 将表单的渲染函数传入 Modal 中。但是这样处理却会破坏组件的结构,容易造成 wrapper 装饰器层叠套用较深,在层叠组件中维护组件的状态。使用 ReactHooks 后,我们可以提取状态处理逻辑,这样可以对状态处理逻辑进行独立测试和复用,且不会改变组件的层级。在多个组件中,共用钩子也是较为方便的。

在编写组件时,随着项目的逐步发展,组件的逻辑将变得极为复杂,比如 componentDidMount 方法内既会包含数据获取的操作,又会包含事件绑定,同时,相同功能点的处理逻辑(事件绑定和解绑)会散落在多个生命周期中。这样就会包含多个执行逻辑,也使代码不易测试、容易出错。借助状态管理器,我们可以将部分执行逻辑写入 store 中,然而这样会引入过多的抽象,执行逻辑也分散在多个模块中,也使组件不便于重用。使用 ReactHooks 后,我们可以基于功能点将彼此相关的处理逻辑拆分为多个小函数,而不是割裂性地分布在组件的多个生命周期中。

如同 Svelte, Angular, Glimer 所展示的,提前预编译组件在未来拥有极高的潜力。React 最近在尝试使用 Prepack 在编译时预处理组件,并且已经看见了一些眉目。然而类组件会使这些优化进展缓慢,同时,类也不能很好的压缩,并使热加载变得不可靠。使用 ReactHooks 后,我们可以使用更多的 React 特性,且不必借助类组件的形式。

从效果上看,ReactHooks 为函数式组件提供状态管理以及相关生命周期特性。

在 React 包中,除却必要的校验外,ReactHooks 提供的 api 都将间接调用 ReactCurrentOwner.currentDispatcher 的同名方法。

State Hook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { useState } from 'react';

function Example(){
const [count, setCount] = useState(0);

return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
)
}

useState 将声明一个状态变量,作为返回数组的首项,该状态变量受到 React 机制的保护,只能通过第二个数组项 setCount 进行修改(其功能点一如类组件中使用的 setState 方法)。useState 的参数为状态的初始值,不限于对象形式。如果要使用两个状态,可调用 useState 两次达成。如前所述,useState 以数组形式返回一对值,前一个是当前的状态,后一个是用于变更状态的函数。

useState 的首参也可以是函数,初始状态由函数的返回值提供。

Effect Hook

Effect Hook 用于组织副作用逻辑,包含远程数据获取、事件绑定、节点操作、日志打印等。假设有针对组件状态的处理逻辑,在类组件的编程形式中,我们需要在 componentDidMount, componentDidUpdate 生命周期中两次组织同一个处理逻辑。当使用 Effect Hook 时,我们只需要组织一次这个处理逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { useState, useEffect } from 'react';

function FriendStatus(props){
const [isOnline, setIsOnline] = useState(null);

function handleStatusChange(status){
setIsOnline(status.isOnline);
}

useEffect(() => {
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);

return () => {
ChatAPI.unsubscribeToFriendStatus(props.friend.id, handleStatusChange);
}
})

if (isOnline === null){
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}

效果上,useEffect(effect, inputs) 等同于告知 React 在组件渲染完成后需要执行 effect 副作用(React 会记录这个副作用函数,并在组件渲染完成后调用);参数 inputs 以数组形式告知 React 当某些数据变更时,才执行副作用(可以是函数式组件内的任何变量或属性,并在 effect 中有所使用)。在函数式组件中使用 useEffect,其意义在于便捷地通过 useState 访问状态;同时,每次重绘将会构建新的 effect,其效果等同于每次渲染都会调度不同的副作用,属于一次性消费。与 componentDidMount, componentDidUpdate 不同的是,useEffect 副作用不会阻塞视图更新。useLayoutEffect 方法与 useEffect 类似,可用于测算布局。

当所需执行的副作用为事件订阅类时,在组件卸载时,我们需要解绑事件,以防内存溢出。当 effect 返回函数(比如用于解绑事件)时,React 会在组件即将更新时执行这个函数。以下是

基于 useEffect,我们可以将相同功能点的处理逻辑写在一个 effect 中,组件内使用多个 effect 涵盖不同的功能点,而不是像类组件那样使相同功能点的处理逻辑散落在不同生命周期中。同时,我们也不需要在 componentDidUpdate 编写一套 props 变更的处理逻辑,因为 useEffect 在组件重置后均会得到调用。

Custom Hook

自定义 hook 允许在不同组件重用状态处理逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import { useState, useEffect } from 'react';

function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);

function handleStatusChange(status) {
setIsOnline(status.isOnline);
}

useEffect(() => {
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});

return isOnline;
}

function FriendStatus(props) {
const isOnline = useFriendStatus(props.friend.id);

if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}

function FriendListItem(props) {
const isOnline = useFriendStatus(props.friend.id);

return (
<li style={{ color: isOnline ? 'green' : 'black' }}>
{props.friend.name}
</li>
);
}

自定义 hook 名需要使用 ‘use’ 起始,这样才能满足 eslint-plugin-react-hooks,React 也能侦测出这是一个 hook。其次,在不同组件中使用的自定义 hook,会构建不同的 state。

useReducer

当组件的状态管理略显复杂时,React 提供 useReducer 钩子以 Redux 风格管理状态。

useReducer 方法的首参为 reducer,次参为 initialState,尾参为 initialAction 如 {type: ‘reset’, payload: initialCount}。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function todosReducer(state, action) {
switch (action.type) {
case 'add':
return [...state, {
text: action.text,
completed: false
}];
// ... other actions ...
default:
return state;
}
}

function useReducer(reducer, initialState) {
const [state, setState] = useState(initialState);

function dispatch(action) {
const nextState = reducer(state, action);
setState(nextState);
}

return [state, dispatch];
}

function Todos() {
const [todos, dispatch] = useReducer(todosReducer, []);

function handleAddClick(text) {
dispatch({ type: 'add', text });
}

// ...
}

其他

  • useContext(context): 使用 React.createContext 创建的 context 对象作为参数,从就近的 Provider 中获取 context 对象。当 context 对象在 Provider 中更新时,组件将重绘。
  • useCallback(() => { doSomething(realInputs) }, inputs): 当 inputs 数组数据变更时,执行 doSomething 回调。useCallback(fn, inputs) 等价于 useMemo(() => fn, inputs)。
  • useMemo(() => { computeExpensiveValue(realInputs) }, inputs): 当 inputs 数组数据变更时,执行 computeExpensiveValue 函数,重新计算新值。
  • useRef(initialValue): 将使用接受的首参构建引用,通过 current 属性访问。该 ref 引用将在组件的生命周期中得到维持。
  • useImperativeMethods(ref, createInstance, [inputs]): 用于对外导出操纵子元素的 ref 引用方法,只能配合 forwardRef 方法使用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// useRef
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` points to the mounted text input element
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}

// useImperativeMethods
function FancyInput(props, ref) {
const inputRef = useRef();
useImperativeMethods(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);