本文作者:IMWeb 孙世吉 原文出处:IMWeb社区 未经同意,禁止转载
想必你已经完成了官方的第一个 React.js 教程,本文将介绍并讨论五个 React 的进阶概念,希望可以将你的 React 技能提升一个新的等级。
原文链接:These are the concepts you should know in React.js (after you learn the basics)
如果你对 React 还不太熟悉,那么建议你先尝试完成 官方教程(中文版),回头再来看看这篇文章吧。
本列表中最重要的一个概念就是要去理解组件的生命周期。顾名思义,组件生命周期完整地描述了组件的整个生命过程,就跟人类一样,组件从出生开始,在他们还活着的的时间内一直做一些事情,然后走向死亡。
跟人类不一样,组件每个生命的阶段都还有些不一样,如下图所示:
(译者注:对 React 16.3新生命周期有所了解的同学对这张图肯定已经熟悉)
不熟悉的同学也别慌,我们一步一步来看上面的那张图。 每个着色的水平矩形都表示了生命周期方法(除了“React updates DOM and refs”)。图中的每一列则表示组件生命周期的不同阶段,可以看到包含挂载(Mounting),更新(Updating)和卸载(UnMounting)三个阶段。
每个组件在任意时间都只能处于其中某个阶段,开始于挂载阶段,紧接着进入更新阶段。组件将一直保持在更新阶段,直到该组件从虚拟 DOM 中移除。然后组件就进入了卸载阶段并从 DOM 中移除。
生命周期方法允许我们在组件生命周期的特定时间点运行指定的代码,或者对外界的更新做出响应。
让我们一起通览组件的每个阶段以及相关的方法吧。
基于类的组件被实例化时,第一个被执行的方法就是构造函数。一般来讲,我们会通过构造函数来初始化组件的状态。
紧接着,组件执行 getDerivedStateFromProps
方法。本文就不对该方法进行详细的介绍了,因为到目前为止应用的场景太少了。(译者注,希望了解的朋友可以参考官网文档)
现在我们来到了render
方法,该方法会返回你的 JSX 模板。到目前为止 React 就“挂载”到 DOM 上了。
最后,componentDidMount
方法被调用,在这个方法中你可以做一些对数据库的异步调用或者有需要的话直接操作 DOM。
每当 state 或者 props 更新的时候,本阶段都会被触发。跟在挂载(mouting)阶段一样,getDerivedStateFromProps
方法被调用了,但是这次不会调用构造函数。
接下来是shouldComponentUpdate
,在这个方法里面,你可以对比老的 props/state 和新的 props/state,并通过返回值 true 或者 false来控制你的组件是否重新渲染。这可以使你的Web应用程序更有效地减少额外的重渲染。如果shouldComponentUpdate
返回 false ,则更新周期结束。
否则的话,React 将重新渲染并执行getSnapshotBeforeUpdate
方法,这个方法目前使用的人一样很少。React 紧接着执行 componentDidUpdate
,就跟 componentDidMount
方法一样,你可以在该方法内执行一步调用或者进行 DOM 操作。
我们的组件一生都过得很好,但是所有美好的事物终将逝去。卸载阶段就是组件生命周期的最后一个阶段。当你从 DOM 一处一个组件时,React 将在这之前立马执行 componentWillUnmount
方法。你应该使用该方法来清除任何打开的连接,例如 WebSocket。
在我们开始下个议题之前,让我们简短的讨论一下forceUpdate
和getDerivedStateFromError
。
forceUpdate
是一个会立即导致重新渲染的方法,虽然可能有一些应用场景,但通常我们应该避免使用这个方法。
getDerivedStateFromError
是一个生命周期方法,但其不是构成组件生命周期的直接部分。当组件出现错误的时候,getDerivedStateFromError
方法就被调用了,这时候你可以更新组件状态来向外界反馈错误的发生。你应该大量的使用这个方法。
下面的CodePen 代码片段演示了挂载阶段的每个步骤。
class App extends React.Component {
constructor(props) {
super(props)
console.log('Hello from constructor')
}
static getDerivedStateFromProps() {
console.log('Hello from before rendering')
}
componentDidMount() {
console.log('Hello from after mounting')
}
render() {
console.log('Hello from render')
return (
<div>Hello!</div>
);
}
}
ReactDOM.render(
<App />,
document.getElementById('app')
);
代码输出结果:
理解 React 的组件生命周期和方法将帮助你更好在应用中的维护数据流和事件控制。
你也许早已经使用过高阶组件(HOCs)。例如Redux 的connect
就是一个返回高阶组件的方法。但是到底什么是高阶组件呢?
来自 React 文档:
高阶组件就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件
反过头来看看 React 的 connect 方法,我们可以看到下列代码片段:
const hoc = connect(state => state)
const WrappedComponent = hoc(SomeComponent)
当我们调用 connect
时,我们得到了一个 HOC,并且可以用它来包装组件。我们将我们的组件传给 HOC,就可以得到一个新的组件。
HOC允许我们做的是将组件之间的共享逻辑抽象为单个重用组件。
一个使用 HOC 的例子就是授权系统。你可以在每个需要授权的独立组件中都写上授权相关的代码,这会出现大量重复代码并且快速膨胀你的代码量。
让我们看看如果没有使用 HOC 你该怎么为组件实现授权。
class RegularComponent extends React.Component {
render() {
if (this.props.isLoggedIn) {
return <p>hi</p>
}
return <p>You're not logged in ☹️</p>
}
}
// 重复的代码!
class OtherRegularComponent extends React.Component {
render() {
if (this.props.isLoggedIn) {
return <p>hi</p>
}
return <p>You're not logged in ☹️</p>
}
}
// 注意我们需要为函数组件准备一套不同的逻辑
const FunctionalComponent = ({ isLoggedIn }) => ( isLoggedIn ? <p>Hi There</p> : <p>You're not logged in ☹️</p> )
一大堆重复的代码和混乱的逻辑!
使用 HOC,代码如下:
function AuthWrapper(WrappedComponent) {
return class extends React.Component {
render() {
if (this.props.isLoggedIn) {
return <WrappedComponent {...this.props} />
}
return <p>You're not logged in ☹️</p>
}
}
}
class RegularComponent extends React.Component {
render() {
return <p>hi</p>
}
}
class OtherRegularComponent extends React.Component {
render() {
return <p>hello</p>
}
}
const FunctionalComponent = () => (<p>Hi There</p>)
const WrappedOne = AuthWrapper(RegularComponent)
const WrappedTwo = AuthWrapper(OtherRegularComponent)
const WrappedThree = AuthWrapper(FunctionalComponent)
你也可以通过CodePen来查看上述的演示的代码。
回顾上面的代码,你会发现我们可以将常规组件保持的十分简单,并给它们都加上了授权相关的功能。AuthWrapper
组件将所有认证逻辑提升为统一的组件。它所做的事情其实只是获取一个名为isLoggedIn
的属性并根据该属性的值返回WrappedComponent
或者一段话。
正如我们所演示的,HOC 可以帮助我们重用代码并消除膨胀。
相信大部分阅读本文的人都使用过 React 状态(state),我们在上文的 HOC 样例中也用到了。但是理解什么时候会出现状态更新是非常重要的,React 会触发组件的重渲染(除非你在shouldComponentUpdate
中标识不需要更新)。
我们先讨论一下我们是如何改变 state 的,唯一一个你可以更新 state 的途径就是通过 setState
方法。该方法接收一个对象作为参数并将该对象合并进当前的状态中。除此之外,还有一些你应该知道的事情。
首先,setState
方法是异步的。这就意味着状态并不会在你调用 setState
后就立马更新,这可能导致一些严重的行为,我们希望现在就能够避免!
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
counter: 0
}
}
onClick = () => {
this.setState({ counter: this.state.counter + 1})
// 这里预期会展示1,但是实际上会展示0
console.log(this.state.counter)
}
render() {
return(
<button onClick={this.onClick}>Click Me</button>
);
}
}
ReactDOM.render(
<App />,
document.getElementById('app')
);
看上述的图片,你会发现当我们调用 setState
的之后立马执行 console.log
。我们的新计数值应该是1,但是实际上输出了0。所以我怎么们在 setState
后获取实际上真正更新过后的状态呢?
这就引出了一个小知识点—— setState
方法可以传入一个回调函数,让我们修改一下代码!
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
counter: 0
}
}
onClick = () => {
this.setState({ counter: this.state.counter + 1}, () => console.log('callback: ' + this.state.counter))
console.log('after: ' + this.state.counter)
}
render() {
return(
<button onClick={this.onClick}>Click Me</button>
);
}
}
ReactDOM.render(
<App />,
document.getElementById('app')
);
正如我们想象的那样,代码可以正常工作了!那现在我们正确的完成了吗?并没有。
我们在这个示例中没有正确的使用 setState
方法。不应该传一个对象实例给 setState
,我们应该传入一个方法。这个模式在你使用当前的状态来更新新状态的时候非常有用,例如我们的示例代码。如果你不是这样的使用场景,尽情的传递新的对象给 setState
吧,并没有什么毛病。
让我们再次更新代码!
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
counter: 0
}
}
onClick = () => {
this.setState((prevState, props) => {
return ({ counter: prevState.counter + 1})
},
() => console.log('callback: ' + this.state.counter)
)
console.log('after: ' + this.state.counter)
}
render() {
return(
<button onClick={this.onClick}>Click Me</button>
);
}
}
ReactDOM.render(
<App />,
document.getElementById('app')
);
上面关于 setState
的代码也可以通过 CodePen进行访问。
传递一个函数而不是一个对象有什么意义呢?因为 setState
是异步的,依赖它来创建一个新的值将有一些陷阱的里面。例如当 setState
调用的时候,另一个 setState
也可能修改了状态。传递给 setState
一个方法有两个好处:
setState
的调用排序,保证调用执行的顺序;看看下面的示例,我们尝试通过执行两次 setState
将 counter 设置到2:
onClick = () => {
this.setState({counter: this.state.counter + 1 })
this.setState({counter: this.state.counter + 1 })
}
render() {
console.log(this.state.counter)
return(
<button onClick={this.onClick}>Click Me</button>
);
}
下面的是采用新的方案解决的代码:
this.setState((prevState, _) => ({ counter: prevState.counter + 1 }))
this.setState((prevState, _) => {
console.log(prevState)
return { counter: prevState.counter + 1 }
})
}
render() {
console.log(this.state.counter)
return(
<button onClick={this.onClick}>Click Me</button>
);
}
上述代码的CodePen链接。
在第一次尝试中,setState
方法都直接使用 this.state.counter
。就如上文我们讨论的,this.state.counter
的值在第一次调用 setState
后依旧是0,由于两次调用都是将 counter
的值设置为1,因此当调用两次 setState
后,counter
的值是1而不是2。
在第二次尝试中,我们传递给 setState
一个方法,这将保证两个 setState
方法将按顺序执行。在这个基础上,它使用的是 state 的副本而不是当前的值(即未更新的状态)。这就能保证我们得到的值跟我们期待的一样,为2。
这就是你所需要知道的关于 React state 的全部内容!
众所周知,React context是一个组件间共享的全局状态。
React context接口允许你创建全局的上下文对象,该对象可以传递给你创建的任何组件。这就使得我们可以在组件间共享数据,而不需要通过 DOM 树来一层层传递 Props。
就像官方文档说的那样:
Context 提供了一种在组件之间共享此类值的方式,而不必通过组件树的每个层级显式地传递 props 。
我们该如何使用上下文呢?
首先创建一个上下文对象:
const ContextObject = React.createContext({ foo: "bar" })
React 文档描述可以为组件设置上下文:
MyClass.contextType = MyContext;
然而,在 CodePen(React 16.4.2),这无法正常工作。我们将使用一个高阶组件来使用上下文,就如 Dan Abramov所建议的那样。
function contextWrapper(WrappedComponent, Context) {
return class extends React.Component {
render() {
return (
<Context.Consumer>
{ context => <WrappedComponent context={context} { ...this.props } /> }
</Context.Consumer>
)
}
}
}
我们要做的其实就是将我们的组件用 Context.Consumer
包装起来,并将上下文作为 props 进行传递。
接下来我们就可以使用高阶组件:
class Child extends React.Component {
render() {
console.log(this.props.context)
return <div>Child</div>
}
}
const ChildWithContext = contextWrapper(Child, AppContext)
我们就可以通过 props 传递的上下文对象来访问 foo
了。
也许你会发问我们如何更新上下文。不幸的是,有点复杂。但是我们可以使用高阶组件来克服,代码大概长这样:
function contextProviderWrapper(WrappedComponent, Context, initialContext) {
return class extends React.Component {
constructor(props) {
super(props)
this.state = { ...initialContext }
}
// define any state changers
changeContext = () => {
this.setState({ foo: 'baz' })
}
render() {
return (
<Context.Provider value={{
...this.state,
changeContext: this.changeContext
}} >
<WrappedComponent />
</Context.Provider>
)
}
}
}
让我们一步步的来看代码。首先我们获取初始化的上下文状态,其实就是我们传递给 React.createContext()
的对象,然后将其设为我们包装组件的状态。接着我们定义了一些用于更新状态的方法。最后我们将我们的组件用 Context.Provider
组件包装起来,将上面定义的状态和方法通过 props 传递。所有子组件只要通过 Context.Consumer
组件进行包装,都可以获取这些上下文。
将所有的东西放在一起(为了简洁,省略了 HOC 的代码)
const initialContext = { foo: 'bar' }
const AppContext = React.createContext(initialContext);
class Child extends React.Component {
render() {
return (
<div>
<button onClick={this.props.context.changeContext}>Click</button>
{this.props.context.foo}
</div>
)
}
}
const ChildWithContext = contextConsumerWrapper(Child, AppContext)
const ChildWithProvide = contextProviderWrapper(ChildWithContext, AppContext, initialContext)
class App extends React.Component {
render() {
return (
<ChildWithProvide />
);
}
}
我们的子组件就可以访问全局的上下文了,也就有了将 foo
属性的值改成baz
的能力了。
完整的关于上下文的代码可以查看 CodePen 链接。
最后一个内容大概是最容易理解的了,就是跟进 React 的最新发布版本。React 最近已经发生了一些剧烈的变化,它也会继续增长和发展。
例如,在 React 16.3,某些生命周期方法被弃用;在 React 16.6 我们获得了异步组件:而在16.7 我们得到了 React Hooks,其目的是完全替换类组件。
(译者注:React的一些新特性也是挺有意思的,例如hooks,最近阅读了一篇不错的文章30分钟精通React Hooks,特别是在日常工程应用中要积极推动基础依赖模块的升级,既是对业界最新动态的关注,实际研发往往也可以从中获取不少收益,包括性能、代码维护成本等。)