Nancy's Studio.

重读React文档的一些记录

Word count: 8,586 / Reading time: 40 min
2020/01/22 Share

React.Component

组件生命周期

常用生命周期的图谱如下所示:

life-cycle

组件实例被创建并插入DOM中时,其生命周期调用顺序为:

  1. constructor(props)
  2. static getDerivedStateFromProps(props, state)
  3. render()
  4. componentDidMount()

getDerivedStateFromProps()在初始挂载以及后续更新时都会被调用(每次渲染前都会触发此方法),不常用,可以让组件在props变化时更新state,这种派生state需要保守使用,而且如果一个派生state的值也被setState更新,那这个值就不是单一来源了。如果要在props更改时重置某些state可以使组件完全受控或者使用key使组件完全不受控。

componentDidUpdate(prevProps, preState, snapshot)会在更新后立即被调用,首次渲染不会执行。注意如果在componentDidUpdate()中调用setState()要在外面套条件语句,对更新前后的props进行比较,否则会死循环和额外渲染。

getSnapshotBeforeUpdate() 在最近一次渲染输出(提交到 DOM 节点)之前调用,使得组件能在发生更改之前从 DOM 中捕获一些信息(如滚动位置)。 如果组件实现了 getSnapshotBeforeUpdate() 生命周期(不常用),则它的返回值将作为 componentDidUpdate() 的第三个参数 “snapshot” 传递。

componentWillMount(),componentWillReceiveProps(),componentWillUpdate() “已过时”不建议使用。

setState()

setState()异步更新可以接收对象或者函数作为第一个参数,若需要基于之前的state和props更新当前state(后续状态取决于当前状态,避免合并覆盖)则需传入updater函数作为参数。

1
2
3
4
5
6
7
8
9
10
//setState(updater[, callback])
//example1
this.setState((state) => {
return { quantity: state.quantity + 1 };
});
//example2
this.setState((state, props) => {
//参数为上一个state值和此次更新被应用时的props
return { counter: state.counter + props.step };
});

为什么setState是异步批量更新?

  1. 保持内部一致性:React对象的state、props、refs属性在内部是一致的;而props的更新是异步的,因为在父组件重新渲染时传入子组件的props才改变,如果state立即更新,显然此时props还没变,为了保持一致性就不得不在每次改变state的时候立即重新渲染组件,这样做是不够合理的。
  2. 立即同步更新会带来性能损失,因为这样导致组件的反复渲染。
  3. 存在视觉效果和体验上的问题,比如多个组件同时更改loading状态会导致页面闪烁;在输入消息的时候需要立即调用TextBox组件的setState,如果此时接收到了新消息,将新消息的渲染延迟到某个阈值可能比直接阻塞线程更好。

参考:关于setState异步更新的issue

props属性

props只读,所有 React 组件都必须像纯函数一样保护它们的 props 不被更改。this.props.children是一个特殊的prop,通常由JSX表达式中的子组件构成,非自定义。

render prop:在 React 组件间使用一个值为函数的 prop 来共享代码。具有 render prop 的组件接受一个函数,该函数返回一个 React 元素并调用它而不是实现自己的渲染逻辑。 (React Router库中就使用了render prop)

1
2
3
<DataProvider render={data => (
<h1>Hello {data.target}</h1>
)}/>

一个获取鼠标位置的Mouse组件被复用的例子:

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
40
41
42
43
44
45
46
class Mouse extends React.Component {
constructor(props) {
super(props);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.state = { x: 0, y: 0 };
}

handleMouseMove(event) {
this.setState({
x: event.clientX,
y: event.clientY
});
}

render() {
return (
<div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
{/*动态渲染render prop中的内容*/}
{ this.props.render(this.state) }
</div>
);
}
}

class Cat extends React.Component {
render() {
const mouse = this.props.mouse;
return (
<img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top: mouse.y }} />
);
}
}

class MouseTracker extends React.Component {
render() {
return (
<div>
<h1>移动鼠标!</h1>
{/*使用render prop*/}
<Mouse render={mouse => (
<Cat mouse={mouse} />
)}/>
</div>
);
}
}

可以使用render prop构造HOC

1
2
3
4
5
6
7
8
9
10
11
function withMouse(Component) {
return class extends React.Component {
render() {
return (
<Mouse render={mouse => (
<Component {...this.props} mouse={mouse} />
)}/>
);
}
}
}

ReactDOM

render

render 会在提供的 container 里渲染一个 React 元素,并返回对该组件的引用。render 会控制传入容器节点里的内容,首次调用时容器节点里的所有 DOM 元素都会被替换,后续调用则使用 diff 算法进行更新。

hydrate

render 相同,但它用于在 ReactDOMServer渲染的容器中对 HTML 的内容进行 hydrate(“注水”) 操作。React 会尝试在已有标记上绑定事件监听器。

Portals

Portals可以将子节点渲染到存在于父组件以外的DOM节点。

1
2
//child是任何可渲染的React子元素,container是一个DOM元素
ReactDOM.createPortal(child, container)

从portal内部触发的事件会一直冒泡至包含React树的祖先。

一个Modal组件的例子:

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// 两个兄弟级DOM容器
const appRoot = document.getElementById('app-root');
const modalRoot = document.getElementById('modal-root');

class Modal extends React.Component {
constructor(props) {
super(props);
this.el = document.createElement('div');
}

componentDidMount() {
modalRoot.appendChild(this.el);
}

componentWillUnmount() {
modalRoot.removeChild(this.el);
}

render() {
return ReactDOM.createPortal(
this.props.children,
this.el,
);
}
}

class Parent extends React.Component {
constructor(props) {
super(props);
this.state = {clicks: 0};
this.handleClick = this.handleClick.bind(this);
}

handleClick() {
// 子元素里的按钮被点击会触发更新父元素的state
// 即使这个按钮在 DOM 中不是直接关联的后代
this.setState(state => ({
clicks: state.clicks + 1
}));
}

render() {
return (
<div onClick={this.handleClick}>
<p>Number of clicks: {this.state.clicks}</p>
<p>
Open up the browser DevTools
to observe that the button
is not a child of the div
with the onClick handler.
</p>
<Modal>
<Child />
</Modal>
</div>
);
}
}

function Child() {
// 这个按钮的点击事件会冒泡到父元素
// 因为这里没有定义 'onClick' 属性
return (
<div className="modal">
<button>Click</button>
</div>
);
}

ReactDOM.render(<Parent />, appRoot);

ReactDOMServer

ReactDOMServer 允许将组件渲染成静态标记,通常被使用在 Node 服务端上。

renderToString

1
ReactDOMServer.renderToString(element)

将 React 元素渲染为初始 HTML,返回一个 HTML 字符串。可以使用此方法在服务端生成 HTML,并在首次请求时将标记下发,以加快页面加载速度,并允许搜索引擎爬取页面以达到 SEO 优化的目的。如果在已有服务端渲染标记的节点上调用 ReactDOM.hydrate() 方法,React 会保留节点且只进行事件处理绑定,从而让用户拥有高性能的首次加载体验。

renderToNodeStream

1
ReactDOMServer.renderToNodeStream(element)

返回可输出 HTML 字符串的可读流,可读流输出的HTML与renderToString输出的一样。(仅用于服务端)

事件处理

绑定this

给事件处理函数绑定this的“实验性”public class fields语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Example extends React.Component {
handleClick = () => {
console.log(this);
}

render() {
return (
<button onClick={this.handleClick}>
click me
</button>
)
}
}

传参

1
2
3
4
//事件对象e作为第二个参数被传递
<button onClick={(e) => this.handleClick0(id, e)}>click0</button>
//通过bind方式绑定事件对象及其他参数被隐式传递
<button onClick={this.handleClick1.bind(this, id)}>click1</button>

合成事件SyntheticEvent

SyntheticEvent 是浏览器原生事件的跨浏览器包装器。除兼容所有浏览器外,它还拥有和浏览器原生事件相同的接口,包括 stopPropagation()preventDefault()

合成对象属性

SyntheticEvent 对象都包含以下属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
boolean bubbles
boolean cancelable
DOMEventTarget currentTarget
boolean defaultPrevented
number eventPhase
boolean isTrusted
DOMEvent nativeEvent
void preventDefault()
boolean isDefaultPrevented()
void stopPropagation()
boolean isPropagationStopped()
DOMEventTarget target
number timeStamp
string type

注:由于SyntheticEvent对象可能会被重用,且在事件回调函数被调用后所有属性会无效,所以不能异步访问事件。若想异步访问事件属性需要在事件上调用event.persist(),此方法会从事件池中移除合成事件,允许保留对事件的引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function onClick(event) {
console.log(event); // => nullified object.
console.log(event.type); // => "click"
const eventType = event.type; // => "click"

setTimeout(function() {
console.log(event.type); // => null
console.log(eventType); // => "click"
}, 0);

// 不起作用,this.state.clickEvent 值为 null
this.setState({clickEvent: event});

// 仍然可以导出事件属性
this.setState({eventType: event.type});
}

常用事件

  • Form Events:onChange onInput onInvalid onSubmit

    onChange通过event.target.value或者event.target.checked获取值

  • Focus Events:onFocus onBlur (在React DOM上的所有元素都有效)

  • Keyboard Events:onKeyDown onKeyPress onKeyUp

    可能用到的属性:key,keyCode,charCode

  • Mouse Events:onClick onContextMenu onDoubleClick onDrag onDragEnd onDragEnter onDragExitonDragLeave onDragOver onDragStart onDrop onMouseDown onMouseEnter onMouseLeaveonMouseMove onMouseOut onMouseOver onMouseUp

    可能用到的属性:screenX,screenY,clientX,clientY

  • Select Event:onSelect

  • Touch Events:onTouchCancel onTouchEnd onTouchMove onTouchStart

    可能用到的属性:touches,targetTouches,changedTouches

  • UI Event:OnScroll

    属性:detail,view

  • Media Events:onAbort onCanPlay onCanPlayThrough onDurationChange onEmptied onEncryptedonEnded onError onLoadedData onLoadedMetadata onLoadStart onPause onPlayonPlaying onProgress onRateChange onSeeked onSeeking onStalled onSuspendonTimeUpdate onVolumeChange onWaiting

  • Image Events:onLoad onError

  • Animation Events:onAnimationStart onAnimationEnd onAnimationIteration

  • Transition Event:onTransitionEnd

  • Toggle Event:onToggle

  • Clipboard Events:onCopy onCut onPaste

    属性:clipboardData

条件渲染

使用&&运算符

如果条件是 true&& 右侧的元素就会被渲染;如果是 false,React 会忽略并跳过它。

1
2
3
4
5
6
7
8
9
10
11
12
13
function Mailbox(props) {
const unreadMessages = props.unreadMessages;
return (
<div>
<h1>Hello!</h1>
{unreadMessages.length > 0 &&
<h2>
You have {unreadMessages.length} unread messages.
</h2>
}
</div>
);
}

需要注意的是如果在上面的例子里直接用unreadMessages.length的值作为判断条件,即使值为0后面的元素仍会被渲染,所以需要确保&&前的表达式总是布尔值。

阻止组件渲染

可以让 render 方法直接返回 null,而不进行任何渲染。

1
2
3
4
5
6
7
8
9
10
11
function Example(props) {
if (!props.flag) {
return null;
}

return (
<div className="test">
render!
</div>
);
}

在组件的 render 方法中返回 null 并不会影响组件的生命周期。

表单

受控组件

受控组件中的表单数据由React组件管理,使 React 的 state 成为“唯一数据源”,大多数情况下都使用受控组件来处理表单数据。

非受控组件

非受控组件中的数据交由DOM节点处理,使用ref从DOM节点中获取表单数据,可以指定defaultValue属性赋予组件初始值。

<input type='file' /> 始终是一个非受控组件,因为它的值只能由用户设置,而不能通过代码控制。(附:DOM File API文档

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
class FileInput extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
this.fileInput = React.createRef();
}
handleSubmit(event) {
event.preventDefault();
alert(
`Selected file - ${
this.fileInput.current.files[0].name
}`
);
}

render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Upload file:
<input type="file" ref={this.fileInput} />
</label>
<br />
<button type="submit">Submit</button>
</form>
);
}
}

由于非受控组件将真实数据存储在DOM节点中,所以在使用非受控组件时,有时候反而更容易同时集成 React 和非 React 代码。 如果表单数据比较简单可能适合使用非受控的方式,但大多数情况下还是使用受控组件。

关于受控和非受控组件的适用场景,参考这篇文章:链接

Context

basic

当组件树中不同层级的组件需要访问相同的数据时,Context 能将这些数据向组件树下所有的组件进行“广播”,让所有的组件都能访问到这些数据以及后续的数据更新,适用于全局共享的属性。

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
// 为当前的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(props) {
return (
<div>
<ThemedButton />
</div>
);
}

class ThemedButton extends React.Component {
// 指定contextType读取当前的theme context
// React会往上找到最近的theme Provider,然后读取它的值,即'dark'
static contextType = ThemeContext; //"实验性"public class fields语法
render() {
return <Button theme={this.context} />;
}
}

React.createContext(defaultValue)可以创建一个Context对象,订阅这个Context对象的组件会从组件树中离自己最近的匹配的Provider中读取到当前context值,若没找到匹配的Provider则使用defaultValue的值。

多个Provider可嵌套,里层会覆盖外层的数据;Provider一般通过Object.is方法检测值的变化,当value发生改变时,其内部所有的消费组件都会重新渲染,不受制于shouldComponentUpdate函数。

设置Class.contextType属性为已创建的某个Context对象(或者使用上面例子中的实验性方法设置,用类属性static初始化contextType),就可以通过this.context获取到最近的匹配的值,在任何生命周期中都可访问到(包括render函数)。

在嵌套组件中更新Context

参考官方给出的例子:

theme-context.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export const themes = {
light: {
foreground: '#000000',
background: '#eeeeee',
},
dark: {
foreground: '#ffffff',
background: '#222222',
},
};

export const ThemeContext = React.createContext({
theme: themes.dark,
toggleTheme: () => {},
});

themed-toggle-button.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { ThemeContext } from './theme-context';

function ThemeTogglerButton() {
//获取到theme值和toggleTheme函数
return (
<ThemeContext.Consumer>
{({theme, toggleTheme}) => (
<button
onClick={toggleTheme}
style={{backgroundColor: theme.background}}
>
Toggle Theme
</button>
)}
</ThemeContext.Consumer>
);
}

export default ThemeTogglerButton;

app.js

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
40
41
import { ThemeContext, themes } from './theme-context';
import ThemeTogglerButton from './theme-toggler-button';

class App extends React.Component {
constructor(props) {
super(props);

this.toggleTheme = () => {
this.setState(state => ({
theme:
state.theme === themes.dark
? themes.light
: themes.dark,
}));
};

this.state = {
theme: themes.light,
toggleTheme: this.toggleTheme,
};
}

render() {
// 整个 state 都被传递进 provider
return (
<ThemeContext.Provider value={this.state}>
<Content />
</ThemeContext.Provider>
);
}
}

function Content() {
return (
<div>
<ThemeTogglerButton />
</div>
);
}

ReactDOM.render(<App />, document.root);

消费多个Context

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
40
41
42
43
const ThemeContext = React.createContext('light');
const UserContext = React.createContext({
name: 'Guest',
});

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

// 提供初始context值
return (
<ThemeContext.Provider value={theme}>
<UserContext.Provider value={signedInUser}>
<Layout />
</UserContext.Provider>
</ThemeContext.Provider>
);
}
}

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

// 一个组件可能会消费多个context
function Content() {
return (
<ThemeContext.Consumer>
{theme => (
<UserContext.Consumer>
{user => (
<ProfilePage user={user} theme={theme} />
)}
</UserContext.Consumer>
)}
</ThemeContext.Consumer>
);
}

Refs and the DOM

basic

可以通过Refs访问DOM节点或render中创建的React元素。适用于:管理焦点、触发强制动画、集成第三方DOM库。

用React.createRef()创建Refs,通过ref属性附加到React元素;当ref属性用于HTML元素时,ref接收底层DOM元素作为其current属性;当ref用于自定义class组件时,ref接收组件的挂载实例作为current属性。

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
class CustomTextInput extends React.Component {
constructor(props) {
super(props);
// 创建ref存储textInput的DOM元素
this.textInput = React.createRef();
this.focusTextInput = this.focusTextInput.bind(this);
}

focusTextInput() {
// 直接使用原生API使text输入框获得焦点
// 通过"current"属性访问DOM节点
this.textInput.current.focus();
}

render() {
return (
<div>
{/*把<input>的ref关联到`textInput`上*/}
<input
type="text"
ref={this.textInput} />

<input
type="button"
value="Focus the text input"
onClick={this.focusTextInput}
/>
</div>
);
}
}

React 会在组件挂载时给 current 属性传入 DOM 元素,并在组件卸载时传入 null 值。ref 会在 componentDidMountcomponentDidUpdate 生命周期钩子触发前更新。

不能在函数组件上使用ref属性,因为无实例。如果要在函数组件中使用ref可以使用forwardRefuseImperativeHandle实现。

回调Refs

回调Refs需要向ref属性传递一个函数,这个函数接受 React 组件实例或 HTML DOM 元素作为参数,以使它们能在其他地方被存储和访问。

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
class CustomTextInput extends React.Component {
constructor(props) {
super(props);

this.textInput = null;

this.setTextInputRef = element => {
//将DOM元素存储到this.textInput
this.textInput = element;
};

this.focusTextInput = () => {
if (this.textInput) this.textInput.focus();
};
}

componentDidMount() {
// 组件挂载后让文本框自动获得焦点
this.focusTextInput();
}

render() {
return (
<div>
<input
type="text"
ref={this.setTextInputRef}
/>
<input
type="button"
value="Focus the text input"
onClick={this.focusTextInput}
/>
</div>
);
}
}

如果 ref 回调函数是以内联函数的方式定义的,在更新过程中会被执行两次,第一次传入参数 null,然后第二次传入 DOM 元素。这是因为每次渲染时都会创建一个新的函数实例,所以会清空旧的 ref 并且设置新的。

Refs转发

Refs转发可以将ref自动通过组件传递到其子组件,这样就可以很方便地获取其子组件的DOM元素。

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

// 可以直接获取<button>的ref
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;

在上面的例子中,ref挂载完成时ref.current将指向<button>DOM节点。

应用到HOC上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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} />;
}
}

// 可将参数Ref作为常规prop属性传递给LogProps
return React.forwardRef((props, ref) => {
return <LogProps {...props} forwardedRef={ref} />;
});
}

Fragments

Fragments允许返回多个元素,无需向DOM添加额外节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
render() {
return (
<React.Fragment>
<ChildA />
<ChildB />
<ChildC />
</React.Fragment>
);
}

//或者用一种新的短语法
render() {
return (
<>
<ChildA />
<ChildB />
<ChildC />
</>
);
}

Error Boundaries

错误边界是一种 React 组件,这种组件可以捕获并打印发生在其子组件树任何位置的 JavaScript 错误,错误边界在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误。 可以使用 static getDerivedStateFromError() 渲染备用 UI ,使用 componentDidCatch() 打印错误信息。

例子:

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { error: null, errorInfo: null };
}

componentDidCatch(error, errorInfo) {
// Catch errors in any components below and re-render with error message
this.setState({
error: error,
errorInfo: errorInfo
})
// You can also log error messages to an error reporting service here
}

render() {
if (this.state.errorInfo) {
// Error path
return (
<div>
<h2>Something went wrong.</h2>
<details style={{ whiteSpace: 'pre-wrap' }}>
{this.state.error && this.state.error.toString()}
<br />
{this.state.errorInfo.componentStack}
</details>
</div>
);
}
// Normally, just render children
return this.props.children;
}
}

class BuggyCounter extends React.Component {
constructor(props) {
super(props);
this.state = { counter: 0 };
this.handleClick = this.handleClick.bind(this);
}

handleClick() {
this.setState(({counter}) => ({
counter: counter + 1
}));
}

render() {
if (this.state.counter === 5) {
// Simulate a JS error
throw new Error('I crashed!');
}
return <h1 onClick={this.handleClick}>{this.state.counter}</h1>;
}
}

function App() {
return (
<div>
<ErrorBoundary>
<BuggyCounter />
</ErrorBoundary>
</div>
);
}

ReactDOM.render(
<App />,
document.getElementById('root')
);

注意:错误边界无法捕获事件处理器内部、异步代码中(如setTimeout和requestAnimationFrame)、服务端渲染、非子组件抛出的错误。

高阶组件HOC

高阶组件是参数为组件并返回新组件的函数,用于复用组件逻辑。HOC是纯函数,没有副作用,将传入的组件包装在容器中而不进行修改,可以将HOC视为参数化容器组件。

一个例子:

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
function withSubscription(WrappedComponent, selectData) {
return class extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
data: selectData(DataSource, props)
};
}

componentDidMount() {
DataSource.addChangeListener(this.handleChange);
}

componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange);
}

handleChange() {
this.setState({
data: selectData(DataSource, this.props)
});
}

render() {
return <WrappedComponent data={this.state.data} {...this.props} />;
}
};
}

需要注意的点:

  1. 不要在render()中使用HOC,因为每次重新渲染时HOC都会被再次创建,导致子树每次渲染都要卸载并重新挂载,这不仅引起性能问题,还会导致该组件及其所有子组件的状态丢失。所以一般应在组件之外创建HOC,保证每次render都是同一个组件。

  2. 需要复制原始组件的静态方法,否则新组件没有原始组件的任何静态方法。可以通过组件名.静态方法名的方式添加,但这样必须知道要拷贝哪些方法;所以可以使用hoist-non-react-statics自动拷贝所有静态方法:

    1
    2
    3
    4
    5
    6
    import hoistNonReactStatic from 'hoist-non-react-statics';
    function enhance(WrappedComponent) {
    class Enhance extends React.Component {/*...*/}
    hoistNonReactStatic(Enhance, WrappedComponent);
    return Enhance;
    }

    或者可以额外导出静态方法。

  3. Refs不会被传递,不过可以用forwardRef解决,详见上面的Refs转发部分。

Diff算法

对比两颗树时,先比较两颗树的根节点;若根节点为不同类型的元素,React会直接拆卸原来的树(对应的DOM节点和关联的state也被销毁)并建立新树。

当对比两个相同类型的React元素时,React会保留DOM节点,仅对比更新改变的属性。当一个组件更新时,组件实例保持不变,React将更新props使其与最新的元素保持一致。

当递归DOM节点的子元素时,React会同时遍历两个子元素的列表;若出现差异,则生成一个mutation。通常为子元素添加key值提升性能。

The performance cost model of React is very simple to understand: every setState re-renders the whole sub-tree. If you want to squeeze out performance, call setState as low as possible and use shouldComponentUpdate to prevent re-rendering an large sub-tree.

拓展阅读:

Hook

使用 Hook 可以在不编写 class 的情况下使用 state 以及其他 React 特性;可以很方便地在无需修改组件结构的情况下复用状态逻辑;还可以将组件中相互关联的部分拆分成更小的函数。

生命周期方法对应Hook

  • constructor:函数组件不需要构造函数。可以通过调用 useState 初始化 state。若计算的代价比较昂贵,可以传一个函数给 useState
  • getDerivedStateFromProps:改为渲染时安排一次更新
  • shouldComponentUpdate: React.memo
  • render:函数组件体本身
  • componentDidMount, componentDidUpdate, componentWillUnmount:useEffect

基础内置Hook

State Hook - useState

1
2
const [state, setState] = useState(initialState);
setState(newState);

在函数组件中存储内部state(保存在函数作用域中)。在使用useState定义state时返回一个有两个值的数组,可以直接通过数组解构进行赋值;state hook在更新state时直接替换掉原来的state而不是合并。

Effect Hook - useEffect

1
2
//接收一个包含命令式且可能有副作用代码的函数effect
useEffect(didUpdate);

Effect Hook可以看作componentDidMountcomponentDidUpdatecomponentWillUnmount 的组合。 与 componentDidMountcomponentDidUpdate 不同,使用 useEffect 调度的 effect 在浏览器绘制后延迟执行,不会阻塞浏览器更新屏幕。

在class中没有提供每次渲染(包括首次)都会执行的方法,所以需要在componentDidMountcomponentDidUpdate中重复写;而useEffect会告诉React组件需要在渲染后执行哪些操作,在默认情况下useEffect在首次渲染和每次更新后都会执行,每次重新渲染都会生成新的effect,React 会在执行当前 effect 之前清除上一个 effect 。

可以使用多个effect,将不相关逻辑分离到不同的effect中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function FriendStatusWithCounter(props) {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});

const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
// when Chart mounts, do this
// when data updates, do this
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}

ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
//返回一个取消订阅的函数(该清除函数会在组件卸载前执行)
return () => {
// when data updates, do this
// before Chart unmounts, do this
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
// ...
}

由于默认每次渲染后effect都会执行,可以向useEffect传入第二个参数(effect所依赖的值数组)控制是否要跳过对effect的调用,仅在数组中的值发生改变时更新,需要确保数组中包含了所有外部作用域中会随时间变化并且在 effect 中使用的变量,否则会引用到先前渲染中的旧变量。如果想仅在组件挂载或卸载时执行effect,可以直接传递一个空数组,告诉React这个effect不依赖于任何props或state值,所以不需要重复执行。

如果在effect中设置定时器,可能会出现每次依赖值更新定时器都被重置的情况(如下):

1
2
3
4
5
6
7
8
9
10
11
12
function Counter() {
const [count, setCount] = useState(0);

useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, [count]);

return <h1>{count}</h1>;
}

可以使用setState的函数式更新形式解决(如下所示),这样就可以指定state该如何改变而不是引用当前state,effect不依赖于外部的state值也就不会被重复调用,所以定时器就不会被重置了。

1
2
3
4
5
6
7
8
9
10
11
12
function Counter() {
const [count, setCount] = useState(0);

useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(id);
}, []);

return <h1>{count}</h1>;
}

一些注意点:

  • 关于依赖项:若effect引用的外部函数不引用props、state以及由他们衍生而来的值,可以在依赖列表中省略。
  • 只在更新时执行effect:可以使用一个可变的ref手动在.current属性里存储一个布尔值来表示是首次渲染还是后续渲染,在effect中检查这个标识。

useContext

1
2
3
//接收一个context对象
//返回上层组件中距离当前组件最近的<MyContext.Provider>的value值
const value = useContext(MyContext);

当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContextprovider 的 context value 值。 即使祖先使用 React.memo 或 shouldComponentUpdate,调用了 useContext 的组件总会在 context 值变化时重新渲染。

额外内置Hook

useReducer

1
2
//第三个参数是init(initialArg)函数,用于惰性初始化或者重置state
const [state, dispatch] = useReducer(reducer, initialArg, init);

使用useReducer重写计数器的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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>
</>
);
}

React 确保 dispatch 函数的标识是稳定的,并且不会在组件重新渲染时改变,所以可以在 useEffectuseCallback 的依赖列表中省略 dispatch 。

如果 Reducer Hook 的返回值与当前 state 相同(使用 Object.is 比较),React 将跳过子组件的渲染及副作用的执行。

useCallback

通常用于包裹传入子组件的方法来优化性能,只有依赖项改变才更新,避免随渲染发生改变。

1
2
3
4
5
6
7
8
// 传入内联回调函数以及依赖项数组
// 返回传入的回调函数的memorized版本
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);

useCallback(fn, deps) 相当于 useMemo(() => fn, deps)

可以利用 callBack ref 测量DOM节点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function useClientRect() {
const [rect, setRect] = useState(null);
const ref = useCallback(node => {
if (node !== null) {
setRect(node.getBoundingClientRect());
}
}, []);
return [rect, ref];
}

function MeasureExample() {
const [rect, ref] = useClientRect();
return (
<>
<h1 ref={ref}>Hello, world</h1>
{rect !== null &&
<h2>The above header is {Math.round(rect.height)}px tall</h2>
}
</>
);
}

useMemo

用于性能优化,可以单独优化具体的子节点。

1
2
3
// 传入“创建”函数和依赖项数组
// 返回memorized值
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

可以用React.memo包裹一个组件并对props进行浅比较来实现shouldComponentUpdate。

useRef

1
const refContainer = useRef(initialValue);

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数initialValue(一般直接传入null)。返回的 ref 对象在组件的整个生命周期内保持不变,变更 .current 属性不会引发组件重渲染,如果想要在 React 绑定或解绑 DOM 节点的 ref 时执行某些操作,则需要使用 回调ref 来实现。

useRef可用于获取上一轮的props或state:

1
2
3
4
5
6
7
8
9
10
11
12
13
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}

function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
return <h1>Now: {count}, before: {prevCount}</h1>;
}

useImperativeHandle

通常与forwardRef一起使用,可以在使用ref时自定义暴露给父组件的实例值。

1
useImperativeHandle(ref, createHandle, [deps])

例子:

1
2
3
4
5
6
7
8
9
10
function FancyInput(props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);

引用<FancyInput ref={inputRef} />的父组件可以获取到inputRef.current

自定义Hook

即定义名称以”use”开头的函数,其内部可调用其他Hook,自定义参数和返回值。

例子:

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

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

useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}

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

return isOnline;
}

使用上面自定义的Hook

1
2
3
4
5
6
7
8
function FriendStatus(props) {
const isOnline = useFriendStatus(props.friend.id);

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

实现一个useReducer的简化版本:

1
2
3
4
5
6
7
8
9
10
function useReducer(reducer, initialState) {
const [state, setState] = useState(initialState);

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

return [state, dispatch];
}

相关文章或网站

  1. https://wattenberger.com/blog/react-hooks
  2. https://usehooks.com/

React.lazy && Suspense

React.lazy 接受一个函数,这个函数需要动态调用 import()。它必须返回一个 Promise,该 Promise 需要 resolve 一个 default export 的 React 组件。然后在 Suspense 组件中渲染 lazy 组件,如此使得我们可以使用在等待加载 lazy 组件时做优雅降级(如 loading 指示器等),Suspense 的 fallback 属性接受任何在组件加载过程中希望展示的 React 元素。

1
2
3
4
5
6
7
8
9
10
11
const OtherComponent = React.lazy(() => import('./OtherComponent'));

function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent />
</Suspense>
</div>
);
}

Addition!

element & component & instance

An element is a plain object describing what you want to appear on the screen in terms of the DOM nodes or other components. Elements can contain other elements in their props. Creating a React element is cheap. Once an element is created, it is never mutated.

A component can be declared in several different ways. It can be a class with a render()method. Alternatively, in simple cases, it can be defined as a function. In either case, it takes props as an input, and returns an element tree as the output.

When a component receives some props as an input, it is because a particular parent component returned an element with its type and these props. This is why people say that the props flows one way in React: from parents to children.

An instance is what you refer to as this in the component class you write. It is useful for storing local state and reacting to the lifecycle events.

Function components don’t have instances at all. Class components have instances, but you never need to create a component instance directly—React takes care of this.

Finally, to create elements, use React.createElement(), JSX, or an element factory helper. Don’t write elements as plain objects in the real code—just know that they are plain objects under the hood.

Link:click here

Implementation Notes (pre-React16)

[Based on stack reconciler]

Mounting as a Recursive Process:Consider the first time we mount a component,<App />,React DOM will pass this component to the reconciler. If App is a class, the reconciler will instantiate an App with new App(props), call the componentWillMount() lifecycle method, and then will call the render() method to get the rendered element. If App is a function, the reconciler will call App(props) to get the rendered element. This process is recursive. The reconciler will “drill down” through user-defined components recursively as it learns what each component renders to.

Mounting Host Elements:If element’s type property is a string, we are dealing with a host element (like <div>). When the reconciler encounters a host element, it lets the renderer take care of mounting it. For example, React DOM would create a DOM node. If the host element has children, the reconciler recursively mounts them following the same algorithm as above. The DOM nodes produced by the child components will be appended to the parent DOM node, and recursively, the complete DOM structure will be assembled.

Internal Instances: The key feature of React is that you can re-render everything, and it won’t recreate the DOM or reset the state. To perform updates on the initial tree, all the necessary information should be stored, such as all the publicInstances, or which DOM nodes correspond to which components. The stack reconciler codebase solves this by making the mount() function a method and putting it on a class. (Stack reconciler has been replaced by Fiber reconciler now because of its several drawbacks.) Internal instances hold onto their children and the DOM nodes.

Unmounting:For a composite component, unmounting calls a lifecycle method and recurses. For DOMComponent, unmounting tells each child to unmount.

Updating:The goal of the reconciler is to reuse existing instances where possible to preserve the DOM and the state. This is the part that is often described as “virtual DOM diffing” although what really happens is that we walk the internal tree recursively and let each internal instance receive an update. When a composite component receives a new element, we run the componentWillUpdate()lifecycle method. Then we re-render the component with the new props, and get the next rendered element. If the type of the rendered element has not changed since the last render, we can just tell the corresponding internal instance to receive() the next element. However, if the next rendered element has a different type than the previously rendered element, we can’t update the internal instance. Instead, we have to unmount the existing internal instance and mount the new one corresponding to the rendered element type. Host component implementations, such as DOMComponent, update differently. When they receive an element, they need to update the underlying platform-specific view. In case of React DOM, this means updating the DOM attributes. Then, host components need to update their children. As the last step, we execute the DOM operations.

Link:click here

Virtual DOM

The virtual DOM (VDOM) is a programming concept where an ideal, or “virtual”, representation of a UI is kept in memory and synced with the “real” DOM by a library such as ReactDOM. This process is called reconciliation. In React world, the term “virtual DOM” is usually associated with React elements since they are the objects representing the user interface. React also uses internal objects called “fibers” to hold additional information about the component tree. They may also be considered a part of “virtual DOM” implementation in React.

XSS Vulnerability Protection

由于之前人为构造的JSON可以通过伪造成ReactElement的形式而被误认为是真的Element而被渲染,如果再利用dangerouslySetInnerHTML属性就可以构造XSS攻击。比如像如下方式构造:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
_isReactElement: true,
_store: {},
type: "body",
props: {
dangerouslySetInnerHTML: {
__html:
"<h1>Arbitrary HTML</h1>
<script>alert('No CSP Support :(')</script>
<a href='http://danlec.com'>link</a>"
}
}
}

转成HTML字符串输出是这样:

1
2
3
4
5
6
7
> React.renderToString(React.createElement("span", null,
{ _isReactElement: true, …}))

"<span data-reactid=".9" data-react-checksum="-1151650166">
<body data-reactid=".9.0"><h1>Arbitrary HTML</h1>
<script>alert('No CSP Support :(')</script>
<a href='http://danlec.com'>link</a></body></span>"

恶意代码就会被插入到页面中被渲染出来。

所以有了$$typeof: REACT_ELEMENT_TYPE这个属性 [ 即$$typeof: Symbol.for('react.element') ],这是个Symbol类型的值,可以有效地将ReactElement与普通对象区分开,若$$typeof属性丢失或无效则React不会对其进行处理。对于不支持Symbol的浏览器,React仍然在元素上保留$$typeof字段并将其设置为一个数字,值为0xeac7(为啥设成这个数呢??因为看起来有点像React…… 233)

Links:

  1. XSS via a spoofed React element
  2. Use a Symbol to tag every ReactElement #4832
  3. How Much XSS Vulnerability Protection is React Responsible For?
  4. Why Do React Elements Have a $$typeof Property?
CATALOG
  1. 1. React.Component
    1. 1.1. 组件生命周期
    2. 1.2. setState()
    3. 1.3. props属性
  2. 2. ReactDOM
    1. 2.1. render
    2. 2.2. hydrate
    3. 2.3. Portals
  3. 3. ReactDOMServer
    1. 3.1. renderToString
    2. 3.2. renderToNodeStream
  4. 4. 事件处理
    1. 4.1. 绑定this
    2. 4.2. 传参
    3. 4.3. 合成事件SyntheticEvent
      1. 4.3.1. 合成对象属性
      2. 4.3.2. 常用事件
  5. 5. 条件渲染
    1. 5.1. 使用&&运算符
    2. 5.2. 阻止组件渲染
  6. 6. 表单
    1. 6.1. 受控组件
    2. 6.2. 非受控组件
  7. 7. Context
    1. 7.1. basic
    2. 7.2. 在嵌套组件中更新Context
    3. 7.3. 消费多个Context
  8. 8. Refs and the DOM
    1. 8.1. basic
    2. 8.2. 回调Refs
    3. 8.3. Refs转发
  9. 9. Fragments
  10. 10. Error Boundaries
  11. 11. 高阶组件HOC
  12. 12. Diff算法
  13. 13. Hook
    1. 13.1. 生命周期方法对应Hook
    2. 13.2. 基础内置Hook
      1. 13.2.1. State Hook - useState
      2. 13.2.2. Effect Hook - useEffect
      3. 13.2.3. useContext
    3. 13.3. 额外内置Hook
      1. 13.3.1. useReducer
      2. 13.3.2. useCallback
      3. 13.3.3. useMemo
      4. 13.3.4. useRef
      5. 13.3.5. useImperativeHandle
    4. 13.4. 自定义Hook
    5. 13.5. 相关文章或网站
  14. 14. React.lazy && Suspense
  15. 15. Addition!
    1. 15.1. element & component & instance
    2. 15.2. Implementation Notes (pre-React16)
    3. 15.3. Virtual DOM
    4. 15.4. XSS Vulnerability Protection