JSX 与 React 元素
JSX 是一个 JavaScript 的语法扩展;在编译之后,JSX 会被 Babel 转为函数调用,对其取值后得到 JS 对象。JSX 使用 camelCase 来定义属性的名称,而不使用 HTML 属性名称的命名约定。
对于 React,JSX 仅仅是 React.createElement(component, props, …children) 函数的语法糖;包含在开始和结束标签之间的 JSX 表达式内容将作为特定属性 props.children 传递给外层组件。
与浏览器的 DOM 元素不同,React 元素是不可变对象;一旦被创建,就无法更改它的子元素或者属性;一个元素就像电影的单帧:它代表了某个特定时刻的 UI。React 元素是创建开销极小的普通对象;ReactDOM 会将元素和它的子元素与它们之前的状态进行比较,通过 Diff 算法使 DOM 与 React 元素保持一致。
组件生命周期
当组件实例被创建并插入 DOM 中时,其生命周期调用顺序如下:
- constructor()
- static getDerivedStateFromProps()
- render()
- componentDidMount()
当组件的 props 或 state 发生变化时会触发更新;其生命周期调用顺序如下:
- static getDerivedStateFromProps()
- shouldComponentUpdate()
- render()
- getSnapshotBeforeUpdate()
- componentDidUpdate()
当组件从 DOM 中移除时,其生命周期调用顺序如下:
当渲染过程、生命周期、或子组件的构造函数中抛出错误时,其生命周期调用顺序如下:
- static getDerivedStateFromError()
- componentDidCatch()
错误边界
错误边界是一种 React 组件,这种组件可以捕获并打印发生在其子组件树任何位置的 JS 错误,并且会渲染出备用 UI,而不是渲染那些崩溃了的子组件树;如果一个错误边界无法渲染错误信息,则错误会冒泡至最近的上层错误边界;自 React 16 起,任何未被错误边界捕获的错误将会导致整个 React 组件树被卸载。
只有 class 组件才可以成为错误边界组件,如果一个 class 组件中定义了 static getDerivedStateFromError 或 componentDidCatch 这两个生命周期方法中的任意一个(或两个)时,那么它就变成一个错误边界。当抛出错误后,请使用 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
|
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>出错啦...</h1>;
}
return this.props.children;
}
}
// 然后你可以将它作为一个常规组件去使用
<ErrorBoundary>
<MyWidget />
</ErrorBoundary>;
|
错误边界无法捕获以下场景中产生的错误:
- 事件处理;
- 异步代码,例如 setTimeout 或 requestAnimationFrame 回调函数;
- 服务端渲染;
- 它自身抛出来的错误(并非它的子组件)。
Refs
1
2
3
4
5
6
7
8
9
10
11
12
|
class AutoFocusTextInput extends React.Component {
constructor(props) {
super(props);
this.textInput = React.createRef();
}
componentDidMount() {
this.textInput.current.focusTextInput();
}
render() {
return <CustomTextInput ref={this.textInput} />;
}
}
|
refs 主要用于:
- 管理焦点,文本选择或媒体播放;
- 触发强制动画;
- 集成第三方 DOM 库。
ref 的值根据节点的类型而有所不同:
- 当 ref 被传递给 render 中的元素时,对该节点的引用可以在 ref 的 current 属性中被访问;
- 当 ref 属性用于 HTML 元素时,构造函数中使用 React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性;
- 当 ref 属性用于自定义 class 组件时,ref 对象接收组件的挂载实例作为其 current 属性。
React.createContext
创建一个 Context 对象,每个 Context 对象都会返回一个 Provider 组件,它允许消费组件订阅 Context 的变化;当 React 渲染一个订阅了这个 Context 对象的组件,这个组件会从组件树中离自身最近的那个匹配的 Provider 中读取到当前的 Context 值。
Provider 接收一个 value 属性,传递给消费组件;一个 Provider 可以和多个消费组件有对应关系;多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据。
当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染;Provider 及其内部 Consumer 组件都不受制于 shouldComponentUpdate 函数,因此当 Consumer 组件在其祖先组件退出更新的情况下也能更新。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
const ThemeContext = React.createContext('light'); // 默认值 light
const App = () => {
/**
* 使用一个 Provider 来将当前的 theme 传递给以下的组件树
* 无论多深,任何组件都能读取这个值
* 在这个例子中,我们将“dark”作为当前的值传递下去
*/
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
};
// 中间的组件再也不必指明往下传递 theme 了
const Toolbar = () => (
<div>
<ThemedButton />
</div>
);
// 指定 contextType 读取当前的 ThemeContext,React 会往上找到最近的 ThemeContext.Provider,然后使用它的值
const ThemedButton = () => <Button theme={this.context} />;
ThemedButton.contextType = ThemeContext;
|
Context 对象接受一个名为 displayName 的 property,类型为字符串。React DevTools 使用该字符串来确定 context 要显示的内容。
1
2
3
4
|
const MyContext = React.createContext(/* some value */);
MyContext.displayName = 'MyDisplayName';
<MyContext.Provider> // "MyDisplayName.Provider" 在 DevTools 中
<MyContext.Consumer> // "MyDisplayName.Consumer" 在 DevTools 中
|
消费多个 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
|
const ThemeContext = React.createContext('light');
const UserContext = React.createContext({
name: 'Guest'
});
const App = ({ signedInUser, theme }) => {
// 提供初始 context 值的 App 组件
return (
<ThemeContext.Provider value={theme}>
<UserContext.Provider value={signedInUser}>
<Layout />
</UserContext.Provider>
</ThemeContext.Provider>
);
};
const Layout = () => (
<div>
<Sidebar />
<Content />
</div>
);
const Content = () => (
<ThemeContext.Consumer>
{theme => (
<UserContext.Consumer>
{user => <ProfilePage user={user} theme={theme} />}
</UserContext.Consumer>
)}
</ThemeContext.Consumer>
);
|
ReactDOM.createPortal
将子节点渲染到父组件以外的 DOM 节点,AntDesign 的弹窗就是使用 ReactDOM.createPortal 进行渲染的;尽管 portal 可以被放置在 DOM 树中的任何地方,但其行为和普通的 React 子节点行为一致,portal 仍存在于 React 树,且与 DOM 中的位置无关;一个从 portal 内部触发的事件会一直冒泡至包含 React 树的祖先,即便这些元素并不是 DOM 中的祖先。
ReactDOM.createPortal 返回的 VitrualDOM 对象会有一个叫做 containerInfo 的属性指向挂载的 DOM 节点;React 内部正是通过判断 containerInfo 属性是否存在来决定 VitrualDOM 的挂载方式的。
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
|
// 在 DOM 中有两个容器是兄弟级
const appRoot = document.getElementById('app-root');
const modalRoot = document.getElementById('modal-root');
const Child = () => {
// 这个按钮的点击事件会冒泡到父元素
return (
<div className="modal">
<button>Click</button>
</div>
);
};
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 };
}
handleClick = () => {
// 当子元素里的按钮被点击时,这个将会被触发更新父元素的 state,即使这个按钮在 DOM 中不是直接关联的后代
this.setState(state => ({
clicks: state.clicks + 1
}));
};
render() {
return (
<div onClick={this.handleClick}>
<p>{this.state.clicks}</p>
<Modal>
<Child />
</Modal>
</div>
);
}
}
ReactDOM.render(<Parent />, appRoot);
|
React.forwardRef
React.forwardRef 用于 refs 转发,将 ref 自动地通过组件传递到其一子组件;React.forwardRef 会创建一个 React 组件,这个组件能够将其接受的 ref 属性转发到其组件树下的另一个组件中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
/**
* 1. 通过调用 React.createRef 创建了一个 ref 并将其赋值给 btn 变量
* 2. 通过指定 ref 为 JSX 属性,将其向下传递给 <FancyButton ref={btn}>
* 3. React 传递 ref 给 forwardRef 内函数 (props, ref) => ...,作为其第二个参数
* 4. 向下转发该 ref 参数到 <button ref={ref}>,将其指定为 JSX 属性
* 5. 当 ref 挂载完成,ref.current 将指向 <button> DOM 节点
*/
const FancyButton = React.forwardRef((props, ref) => (
<button ref={ref} className="FancyButton">
{props.children}
</button>
));
const App = () => {
const btn = React.createRef();
// 可以获取底层 DOM 节点 button 的 ref
return (
<>
<FancyButton ref={btn}>点我</FancyButton>
</>
);
};
|
React.lazy & Suspense
React.lazy 接受一个函数,这个函数需要动态调用 import 并返回一个 Promise,该 Promise 需要 resolve 一个 default export 的 React 组件;是的,React.lazy 目前只支持默认导出。应该在 Suspense 组件中渲染 React.lazy 组件,如此使得在等待加载组件时做优雅降级。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// 基于路由的代码分割
const Home = React.lazy(() => import('./pages/Home'));
const About = React.lazy(() => import('./pages/About'));
const App = () => (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
</Switch>
</Suspense>
</Router>
);
|
高阶组件
高阶组件是参数为组件,返回值为新组件的函数;组件是将 props 转换为 UI,而高阶组件是将组件转换为另一个组件;高阶组件有两种:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
function withEnhancerOne(WrappedComponent) {
class EnhancerComponent extends Component {
// ...
render() {
if (boolean) {
return null;
}
// 将 HOC 接受到的 props 传入被包裹的组件,还传入了一些新的 props
return (
<>
<div style={{ fontSize: 16 }}>
<WrappedComponent {...this.props} {...otherProps} />
</div>
</>
);
}
}
EnhancerComponent.displayName = `EC-(${
WrappedComponent.displayName || WrappedComponent.name || 'Component'
})`;
return EnhancerComponent;
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
function withEnhancerTwo(WrappedComponent) {
return class EnhancerComponent extends WrappedComponent {
// 可以通过 super 访问到 WrappedComponent 中的各种变量和方法
render() {
if (boolean) {
return null;
}
return (
<>
<div>EnhancerComponent</div>
{super.render()}
</>
);
}
};
}
|
组合模式
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
|
const TabItem = ({ active, children, onClick }) => {
const style = {
border: active ? '1px solid red' : 0,
color: active ? 'red' : 'green'
};
return (
<h3 style={style} onClick={onClick}>
{children}
</h3>
);
};
class Tabs extends Component {
state = {
activeIndex: 0
};
render() {
const childrenReplacement = React.children
.toArray(this.props.children)
.map((child, index) => {
if (React.isValidElement(child)) {
/**
* 要想扩展传给子节点的 props,只能通过 React.cloneElement
* 直接修改 props 对象是不行滴
*/
return React.cloneElement(child, {
active: this.state.activeIndex === index,
onClick: () => this.setState({ activeIndex: index })
});
}
})
.filter(({ props }) => !props.disabled);
// 不渲染原有的 children,改为渲染 childrenReplacement
return <>{childrenReplacement}</>;
}
}
<Tabs>
<TabItem>One</TabItem>
<TabItem disabled>Two</TabItem>
<TabItem>Three</TabItem>
</Tabs>;
|