虚拟 DOM #
我们知道,React 给我们创造了一个虚拟 DOM 的概念,它允许我们非常方便地用 JavaScript 来编写 DOM,React 会负责将这棵 DOM 树转化成真实的 DOM,并且更新它们的状态。
那么我们就至少得有一个真实的 DOM 结点,它就是一个空的 root 结点。
<div id="root"></div>这个 root 结点里的一切内容,将由 React 来操控。
我们调用 ReactDOM.render(),传入 root 结点,和要绘制的 DOM 树,就可以将虚拟 DOM 绘制出来。
ReactDOM 会进行差异对比,只更新有变化的那一部分。
DOM 转化 #
如下的 JSX 代码:
const element = (
<h1 className="greeting">
Hello, world!
</h1>
);会被转化成如下对象:
// Note: this structure is simplified
const element = {
type: 'h1',
props: {
className: 'greeting',
children: 'Hello, world!'
}
};这就是 React 元素,它描述了要在屏幕上呈现什么。React-DOM 会读取这个对象,用其来构建 DOM,并且保持更新。
Props and States #
Props 只读 #
永远不在 component 内部去改变 props 的值。所有的 component,在它们的 props 面前,必须表现出纯函数的行为。
State #
State 是每个 component 自己维护的状态。component 可以根据自己的逻辑,去更新它。并且只有 component 自己可以更新自己的 state。而 state 的更新,会触发 DOM 绘制的更新。
State 的使用要注意如下三件事:
-
不能直接修改 this.state,而要使用 this.setState()
// Wrong this.state.comment = 'Hello'; // Correct this.setState({comment: 'Hello'});你唯一能给 state 赋值的地方是构造函数 constructor()
-
setState() 是异步的
setState() 不会立即触发 DOM 的更新。React 有一个队列机制,它会将队列中的更新动作糅合成一个最终结果去更新。
由于 this.props 和 this.state 是异步更新的,所以你不能依赖于它们的值去计算下一个状态值。
比如像下面的代码是可能出错的:
// Wrong this.setState({ counter: this.state.counter + this.props.increment, });正确的方法应该是下面这样:
// Correct this.setState((state, props) => ({ counter: state.counter + props.increment }));setState() 方法接受传入一个 function 作为参数。这个 function 的两个参数分别为 state 和 props,二者的值是对应的同一时刻的状态。
-
setState() 会对 state 对象进行 merge
我们的 state 是一个 object,有很多的 object 成员变量。我们在调用 this.setState 的时候,不需要写全所有的变量,只写我们想要更新的即可。setState() 会 merge 进去。
生命周期 #
-
componentDidMount
当我们的组件被绘制到 DOM 中了,该方法将被调用。这是一个适合于进行初始化的地方。
-
componentWillUnmount
当我们的组件将从 DOM 中销毁的时候,该方法将被调用。
自上而下的数据流 #
组件应该尽量成为一个纯函数。父组件可以控制子组件的状态,而子组件则不应该控制父组件的状态。简单来说,我们应该把状态保存在父组件,而将父组件的状态和钩子函数(对状态的操作)传递给子组件,保持子组件的纯洁。
屏蔽 DOM 元素的事件默认行为 #
在纯 HTML 中,我们可以通过返回 false 来屏蔽标签的事件默认行为。
<form onsubmit="console.log('You clicked submit.'); return false">
<button type="submit">Submit</button>
</form>而在 react 中,我们要调用 preventDefault 方法。
function Form() {
function handleSubmit(e) {
e.preventDefault();
console.log('You clicked submit.');
}
return (
<form onSubmit={handleSubmit}>
<button type="submit">Submit</button>
</form>
);
}DOM 事件 #
在 react 中,我们没有必要调用 addEventListener 方法来添加 listener。直接添加事件处理函数就可以了。
this 的绑定 #
You have to be careful about the meaning of this in JSX callbacks. In JavaScript, class methods are not bound by default. If you forget to bind this.handleClick and pass it to onClick, this will be undefined when the function is actually called.
class Toggle extends React.Component {
constructor(props) {
super(props);
this.state = {isToggleOn: true};
}
handleClick() {
this.setState(prevState => ({
isToggleOn: !prevState.isToggleOn
}));
}
render() {
return (
// This binding is necessary to make `this` work in the callback
<button onClick={this.handleClick.bind(this)}>
{this.state.isToggleOn ? 'ON' : 'OFF'}
</button>
);
}
}
ReactDOM.render(
<Toggle />,
document.getElementById('root')
);如果你觉得在 class 组件中老是要绑定 this 太麻烦了,可以采用如下两种方法。
class LoggingButton extends React.Component {
// This syntax ensures `this` is bound within handleClick.
// Warning: this is *experimental* syntax.
handleClick = () => {
console.log('this is:', this);
}
render() {
return (
<button onClick={this.handleClick}>
Click me
</button>
);
}
}class LoggingButton extends React.Component {
handleClick() {
console.log('this is:', this);
}
render() {
// This syntax ensures `this` is bound within handleClick
return (
<button onClick={() => this.handleClick()}>
Click me
</button>
);
}
}但是,如上的方法是会有性能问题的。因为本质上,箭头函数是每次 render 的时候都创建了一个副本。如果这个函数作为 props 传给子组件,那么就相当于每次 render,这个 props 就会变,然后下面的子组件也跟着 render。所以如果考虑性能的话,最好还是老老实实 bind。
控制组件的可见性 #
组件的可见性除了可以用 visible 属性来控制之外,还可以通过子组件通过条件判断返回 null 来实现。
function WarningBanner(props) {
// 通过 props.warn 来控制返回内容
if (!props.warn) {
return null;
}
return (
<div className="warning">
Warning!
</div>
);
}
class Page extends React.Component {
constructor(props) {
super(props);
this.state = {showWarning: true};
this.handleToggleClick = this.handleToggleClick.bind(this);
}
handleToggleClick() {
this.setState(state => ({
showWarning: !state.showWarning
}));
}
render() {
return (
<div>
// 如果 this.state.showWarning 是 false 的话,则 <WarningBanner/> 为 null
<WarningBanner warn={this.state.showWarning} />
<button onClick={this.handleToggleClick}>
{this.state.showWarning ? 'Hide' : 'Show'}
</button>
</div>
);
}
}
ReactDOM.render(
<Page />,
document.getElementById('root')
);列表的 key #
由于列表这种数据结构只表征前后关系,而没有唯一标识。(下标是会变的,如果我们在列表中进行插入操作的话。)
而 react 在渲染的时候,要实现只进行差异渲染,要明白到底列表中那个元素发生了变化,显然没有一个唯一标识是不行的。所以我们要给每个列表元素一个唯一的标识 key。前面说了,下标 index 是会变的,所以请不要用 index 当key。
ref #
ref 用来获取自组件(标签)的状态。
ref 是一种比较 dirty 的功能,除非你是要去操作一些非 react 的元素,或者其它迫不得已的原因,否则不要用 ref,还是尽量保持自上而下的数据流。
class NameForm extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
// 创建一个 ref 对象
this.input = React.createRef();
}
handleSubmit(event) {
// 获取子组件状态
alert('A name was submitted: ' + this.input.current.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Name:
// 绑定 ref
<input type="text" ref={this.input} />
</label>
<input type="submit" value="Submit" />
</form>
);
}
}state 提升 #
有时候,不同的子组件需要对同一数据做出反应。最好的方法是把他们共享的数据 state 提升到他们最近的公共父组件来,以满足自上而下的数据流模型。
优化 #
-
动态 import
// 之前 import { add } from './math'; console.log(add(16, 26));// 之后 import("./math").then(math => { console.log(math.add(16, 26)); });如果使用了 webpack 的话,以上转换其实是会自动进行的。
-
React.lazy
// 之前 import OtherComponent from './OtherComponent';// 之后 const OtherComponent = React.lazy(() => import('./OtherComponent'));使用 Suspense 来显示加载中。。。
import React, { Suspense } from 'react'; const OtherComponent = React.lazy(() => import('./OtherComponent')); function MyComponent() { return ( <div> <Suspense fallback={<div>Loading...</div>}> <OtherComponent /> </Suspense> </div> ); }使用 ErrorBoundary 来处理动态加载错误。
ErrorBoundary 就是指实现了
static getDerivedStateFromError()或componentDidCatch()的组件。其中static getDerivedStateFromError()用来更新加载失败后的 state,以更新 UI。componentDidCatch()用来记录错误信息。class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(error) { // Update state so the next render will show the fallback UI. return { hasError: true }; } componentDidCatch(error, errorInfo) { // You can also log the error to an error reporting service logErrorToMyService(error, errorInfo); } render() { if (this.state.hasError) { // You can render any custom fallback UI return <h1>Something went wrong.</h1>; } return this.props.children; } }import React, { Suspense } from 'react'; import ErrorBoundary from './ErrorBoundary'; const OtherComponent = React.lazy(() => import('./OtherComponent')); const AnotherComponent = React.lazy(() => import('./AnotherComponent')); const MyComponent = () => ( <div> <ErrorBoundary> <Suspense fallback={<div>Loading...</div>}> <section> <OtherComponent /> <AnotherComponent /> </section> </Suspense> </ErrorBoundary> </div> );
路由 #
Context 上下文 #
Context 给我们提供了一种方法,让我可以把数据传递给子组件树,而不用一层层地往下传 props。在传统的 react 模式中,数据流是自上而下的,但是对于某些大的数据来说(比如说语言偏好、主题),这显然是非常累赘的。context 正是提供了这样一个可以共享,却又不用层层传递的数据。
// Context lets us pass a value deep into the component tree
// without explicitly threading it through every component.
// Create a context for the current theme (with "light" as the default).
const ThemeContext = React.createContext('light');
class App extends React.Component {
render() {
// Use a Provider to pass the current theme to the tree below.
// Any component can read it, no matter how deep it is.
// In this example, we're passing "dark" as the current value.
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
}
// A component in the middle doesn't have to
// pass the theme down explicitly anymore.
function Toolbar() {
return (
<div>
<ThemedButton />
</div>
);
}
class ThemedButton extends React.Component {
// Assign a contextType to read the current theme context.
// React will find the closest theme Provider above and use its value.
// In this example, the current theme is "dark".
static contextType = ThemeContext;
render() {
return <Button theme={this.context} />;
}
}-
React.createContext
创建一个 context,当 react 渲染一个订阅了该 context 的组件,react 将从最近的 Provider 标签中读取当前该 context 的值。
-
Context.Provider
每一个 context 都有一个 Provider,它允许其子组件订阅该 context 的值。当 context 的值变化时,所有订阅了该 context 的组件也会更新渲染。
-
Class.contextType
class MyClass extends React.Component { componentDidMount() { let value = this.context; /* perform a side-effect at mount using the value of MyContext */ } componentDidUpdate() { let value = this.context; /* ... */ } componentWillUnmount() { let value = this.context; /* ... */ } render() { let value = this.context; /* render something based on the value of MyContext */ } } MyClass.contextType = MyContext;类属性 contextType 可以被赋值为一个 context 对象,这样你就可以通过 this.context 来使用该 context 对象。但是 contextType 仅允许你订阅一个 context 对象。要订阅多个 context,我们先介绍 Context.Consumer。
-
Context.Consumer
Context.Consumer 使我们更方便的订阅一个 context。
<MyContext.Consumer> {value => /* render something based on the context value */} </MyContext.Consumer>订阅多个 context:
// Theme context, default to light theme const ThemeContext = React.createContext('light'); // Signed-in user context const UserContext = React.createContext({ name: 'Guest', }); class App extends React.Component { render() { const {signedInUser, theme} = this.props; // App component that provides initial context values return ( <ThemeContext.Provider value={theme}> <UserContext.Provider value={signedInUser}> <Layout /> </UserContext.Provider> </ThemeContext.Provider> ); } } function Layout() { return ( <div> <Sidebar /> <Content /> </div> ); } // A component may consume multiple contexts function Content() { return ( <ThemeContext.Consumer> {theme => ( <UserContext.Consumer> {user => ( <ProfilePage user={user} theme={theme} /> )} </UserContext.Consumer> )} </ThemeContext.Consumer> ); } -
Context.displayName
Context.displayName 可以方便我们在 DevTools 中调试的时候区分 context。
const MyContext = React.createContext(/* some value */); MyContext.displayName = 'MyDisplayName'; <MyContext.Provider> // "MyDisplayName.Provider" in DevTools <MyContext.Consumer> // "MyDisplayName.Consumer" in DevTools