ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
[TOC] # 组件 ## 定义组件 1.createClass 工厂方法 ```js const Button == React.createClass({ render() { return <button /> } }) ``` 以上代码创建了一个按钮组件,并且应用的其他组件也可以引用它 可以用纯 JavaScript 对其进行改写,如下所示 ```js const Button = React.createClass({ render() { return React.createElement('button') } }) ``` 无须用 Babel 进行转译即可在任何地方运行以上代码。 2.继承 React.component(推荐) 使用以下代码重写上例中的按钮 ```js class Button extends React.component { render() { return <button /> } } ``` 这两种方法的区别是什么呢?除了语法上的差异,其还有如下的区别: <span style="font-size: 20px; color: #ff502c;">1.prop</span> 第一个区别在于如何定义组件期望接收的 prop 及其默认值 createClass 方法需要在作为参数传入函数的对象内定义 prop,同时在 getDefaultProps 内返回默认值 ```js const Button = React.createClass({ propTypes: { // 该属性列出了能够传递给该组件的所有值 text: React.PropTypes.string }, // 定义了 prop 的默认值,如果父组件中传递了 prop,那么对应的默认值会被覆盖 getDefaultProps() { return { text: 'Click me' } }, render() { return <button>{this.props.text}</button> } }) ``` 可以用类实现同样的目的: ```js class Button extends React.Component { render() { return <button>{this.props.text}</button> } Button.propTypes = { text: React.PropTypes.string } Button.defaultProps = { text: 'Click me' } } ``` <span style="font-size: 20px; color: #ff502c;">2.状态</span> createClass 工厂方法和 extends React.Component 方法的另一个重大区别是,组件初始状态的定义方式不同。 和前面一样,使用 createClass 需要调用函数,而使用类则需要设置实例的属性 来看一个示例: ```js const Button = React.createClass({ getInitialState() { return { text: 'Click me!' } }, render() { return <button>{this.state.text}</button> } }) ``` getInitialState 方法期望返回一个对象,该对象包含每个状态属性的默认值。 如果用类来定义初始状态,则需要在类的构造器方法内设置实例的状态属性: ```js class Button extends React.Component { constructor(props) { super(props) this.state = { text: 'Click me!' } } render() { return <button>{this.state.text}</button> } } ``` 定义状态的这两种方式等效,使用类的好处是无须使用 React 特有的 API,直接在实例上定义属性即可 <span style="font-size: 20px; color: #ff502c;">3.自动绑定</span> createClass 有一项非常方便的特性,但该特性也会隐藏 JavaScript 的工作原理,从而造成误解。这项特性允许我们创建事件处理器,并且当调用事件处理器时 this 会指向组件本身: ```js const Button = React.createClass({ handleClick() { console.log(this) }, render() { return <button onClick={this.handleClick} /> } }) ``` createClass 允许我们按照以上方式设置事件处理器,这样一来,函数内部的 this 就会指向组件本身。这允许我们调用同一组件实例的其他方法。例如,调用 this.setState() 等其他方法所产生的结果都能符合预期。 现在我们来看看 this 在类中的差别以及如何才能实现同样的行为: ```js class Button extends React.Component { handleClick() { console.log(this) } render() { return <button onClick={() => this.handleClick()} /> } } ``` 方案之一就是使用箭头函数,但是在渲染方法中绑定函数会带来无法预料的副作用,因为每次渲染组件(应用在生命周期内会多次渲染组件)时都会触发箭头函数。 解决这一问题的最佳方案是在构造器内进行绑定操作,这样即使多次渲染组件,它也不会发生任何改变 ```js class Button extends React.Component { constructor(props) { super(props) this.handleClick = this.handleClick.bind(this) } handleClick() { console.log(this) } render() { return <button onClick={() => this.handleClick()} /> } } ``` 如果觉得 bind 很麻烦,React 也提供了如下的 “实验性” 语法 ``` class LoggingButton extends React.Component { // 此语法确保 `handleClick` 内的 `this` 已被绑定。 // 注意: 这是 *实验性* 语法。 handleClick = () => { console.log('this is:', this); } render() { return ( <button onClick={this.handleClick}> Click me </button> ); } } ``` ## 函数组件 定义组件最简单的方式就是编写 JavaScript 函数: ```js function Welcome (props) { return <h1>Hello, {props.name}</h1>; } ``` 该函数是一个有效的 React 组件,因为它接收唯一带有数据的 “props”(代表属性)对象与并返回一个 React 元素。这类组件被称为“函数组件”,因为它本质上就是 JavaScript 函数。相比于使用 class 来定义组件,我们不需要考虑函数组件的状态和生命周期等特性。 ## state 与 prop <span style="font-size: 20px;">props 的只读性</span> 组件无论是使用函数声明还是通过 class 声明,都决不能修改自身的 props。来看下这个`sum`函数 ```js function sum (a, b) { return a + b; } ``` 这样的函数被称为 <span style="font-family:楷体;font-weight: 700;">纯函数</span>,因为该函数不会尝试更改入参,且多次调用下相同的入参始终返回相同的结果。 相反,下面这个函数则不是纯函数,因为它更改了自己的入参: ```js function withdraw(account, amount) { account.total -= amount; } ``` React 非常灵活,但它也有一个严格的规则: > 所有 React 组件都必须像纯函数一样保护它们的 props 不被更改。 当然,应用程序的 UI 是动态的,并会伴随着时间的推移而变化。因此就有了 “state”。在不违反上述规则的情况下,state 允许 React 组件随用户操作、网络响应或者其他变化而动态更改输出内容。 <span style="font-size: 20px;">state</span> React 使用状态来生成新的 UI - 访问状态: state 对象是组件的属性对象,可以通过 this 引用来访问,例如`this.state.name` - 初始化状态:在组件的 constructor() 方法中直接设置 this.state 的初始值 - 更新状态:切忌使用 this.state = xxx 来更新状态,需要使用类方法 this.setState(data, callback) 来改变状态,React 会将 data 和当前状态 **合并(部分)**,然后调用 render() 方法。之后 React 会调用 callback。 为 setState() 方法添加 callback 参数非常重要,因为该方法可以被异步调用。如果依赖新的状态,可以使用回调来确保这个新的状态可用。 <span style="font-size: 20px;">state 与 props</span> this.state 和 this.props 的区别大致如下: ① 前者可变,后者不可变(组件不应该修改自身的 props) ② 属性(props)从父组件传递,状态在组件内部而非父组件中定义;即属性只能从父组件变更,而不能从组件本身变更。 ![](https://box.kancloud.cn/6145763a8ab6bb97421f46aaeafdbd04_554x166.png) <span style="font-size: 20px;">setState 注意事项</span> Ⅰ. 不要直接修改 state,例如,此代码不会重新渲染组件: ```js // Wrong this.state.comment = 'Hello'; ``` 而是应该使用 setState(): ```js // Correct this.setState({comment: 'Hello'}); ``` 构造函数是唯一可以给 `this.state` 赋值的地方: <br /> Ⅱ. state 的更新可能是异步的 出于性能考虑,React 可能会把多个 setState() 调用合并成一个调用。 因为 this.props 和 this.state 可能会异步更新,所以你不要依赖他们的值来更新下一个状态。例如,此代码可能会无法更新计数器: ```js // Wrong this.setState({ counter: this.state.counter + this.props.increment, }); ``` 要解决这个问题,可以让`setState()`接收一个函数而不是一个对象。这个函数用上一个 state 作为第一个参数,将此次更新被应用时的 props 做为第二个参数: ```js // Correct this.setState((state, props) => ({ counter: state.counter + props.increment })); // 与下面的代码等价 // Correct this.setState(function(state, props) { return { counter: state.counter + props.increment }; }); ``` ## 组件通信 参考链接:[https://www.jianshu.com/p/fb915d9c99c4](https://www.jianshu.com/p/fb915d9c99c4) Ⅰ. 父子组件通信 父组件可以通过 prop 向子组件传递数据和回调函数,子组件可以利用回调函数来与父组件通信。 ```js // App.js import React,{ Component } from "react"; import Sub from "./SubComponent.js"; import "./App.css"; export default class App extends Component{ callback(msg){ console.log(msg); } render(){ return( <div> <Sub callback = { this.callback.bind(this) } /> </div> ) } } ``` ```js // SubComponent.js import React from "react"; const Sub = (props) => { const cb = (msg) => { return () => { props.callback(msg) } } return( <div> <button onClick = { cb("我们通信吧") }>点击我</button> </div> ) } ``` Ⅱ. 跨级组件通信(嵌套关系) 所谓跨级组件通信,就是父组件向子组件的子组件通信,向更深层的子组件通信。跨级组件通信可以采用下面两种方式: - 中间组件层层传递 props - 使用 context 对象 第一种方式在组件嵌套层次在三层之内还可以考虑,组件嵌套过深时显然是不可行的。 context 相当于一个全局变量,是一个大容器,我们可以把要通信的内容放在这个容器中,这样一来,不管嵌套有多深,都可以随意取用。 使用 context 也很简单,需要满足两个条件: - 上级组件要声明自己支持 context,并提供一个函数来返回相应的 context 对象 - 子组件要声明自己需要使用 context 具体使用见参考链接。 Ⅲ. 没有嵌套关系的组件之间的通信 非嵌套组件,就是没有任何包含关系的组件,包括兄弟组件以及不在同一个父级中的非兄弟组件。对于非嵌套组件,可以采用下面两种方式: - 利用二者共同父组件的 context 对象进行通信 - 使用自定义事件的方式 前者会增加子组件和父组件的耦合度,且如何找公共父组件也是一个问题;后者类似中介者模式 + 发布-订阅 ? 就类似 Vue 的中央事件总线 bus 一样吧。 这里只记下思路,具体代码见参考链接。一般都直接上 Redux 了管他那么多... # 生命周期 ## 老版本的生命周期 **所谓生命周期函数即在某一时刻会被组件自动调用的函数** React 提供了一种基于生命周期事件的方法来控制和自定义组件行为。这些事件可以归为以下几类: - 挂载事件(仅调用一次):发生在 React 元素(组件类的实例)被绑定到真实 DOM 节点上时 - 更新事件(调用多次):发生在 React 元素有新的属性或状态需要更新时 - 卸载事件(仅调用一次):发生在 React 元素从 DOM 中卸载时 constructor 在某种意义上也算挂载事件因为其只会执行一次 ![](https://box.kancloud.cn/2f2f03b67b80ee50531b0a7b6bd4b6e5_2000x924.png) <span style="font-size:20px; ">挂载</span> `componentWillMount()`发生在挂载到 DOM 之前 `componentDidMount()`发生在挂载和渲染之后。推荐放置一些和其它框架与库集成的代码,以及向服务器发送 XHR 请求。因为此时组件已经存在于 DOM 中,所有元素(包括子节点)都已经可以访问。 在同构代码(在服务器和浏览器中使用相同的组件)中也非常有用,可以在该方法中添加仅浏览器端的逻辑,并确保只会在浏览器中调用,而不在服务器端调用。 <span style="font-size:20px; ">更新</span> **props 发生变化和 states 发生变化都会触发更新** `componentWillReceiveProps(nextProps)`发生在组件即将接收属性时,如果组件没有 props 参数那么该生命周期函数不会被调用;只要父组件的 render 函数被重新执行了,子组件的该生命周期函数就会被执行(mount 时不算?) `shouldComponentUpdate(nextProps, nextState)`通过判断何时需要更新、何时不需要更新、允许对组件的渲染进行优化,返回值为一个布尔值(如果使用了,可以自己定义是否更新) `componentWillUpdate(nextProps, nextState)`发生在组件将要更新之前 `componentDidUpdate(prevProps, prevState)`发生在组件更新完成之后 <span style="font-size:20px; ">卸载</span> `componentWillUnmount()`允许在组件卸载之前解绑所有的事件监听器或者做其他清理操作。 另外,` this.forceUpdate() `会强制更新,可以在因为各种原因导致更新状态或属性不会触发期望的重新渲染时使用。例如,这可能发生在 render() 中使用不属于状态和属性的数据,那么数据发生变更时,这种情形下需要手动触发更新。一般情况下应该避免使用。 ## 新的生命周期 ![](https://box.kancloud.cn/84d7348576b53d9ef9317beb963f2a27_1000x584.png) 1. React16 新的生命周期弃用了 componentWillMount、componentWillReceivePorps,componentWillUpdate 2. 新增了 getDerivedStateFromProps、getSnapshotBeforeUpdate 来代替弃用的三个钩子函数(componentWillMount、componentWillReceivePorps,componentWillUpdate) 3. React16 并没有删除这三个钩子函数,但是不能和新增的钩子函数(getDerivedStateFromProps、getSnapshotBeforeUpdate)混用,React17 将会删除 componentWillMount、componentWillReceivePorps,componentWillUpdate 4. 新增了对错误的处理(componentDidCatch) - `getDerivedStateFromProps(props, state)` 组件每次被 render 的时候,包括在组件构建之后(虚拟 dom 之后,实际 dom 挂载之前),每次获取新的 props 或 state 之后;每次接收新的 props 之后都会返回一个对象作为新的 state,返回 null 则说明不需要更新 state;配合 componentDidUpdate,可以覆盖 componentWillReceiveProps 的所有用法 - `getSnapshotBeforeUpdate(prevProps, prevState)` 触发时间: update 发生的时候,在 render 之后,在组件 dom 渲染之前;返回一个值,作为 componentDidUpdate 的第三个参数;配合 componentDidUpdate, 可以覆盖 componentWillUpdate 的所有用法 ## 生命周期开发中的应用 ⒈考虑这么一种情况,父组件的 render 函数被执行了,那么其子组件的 render 函数也会被执行(即使其 props 没有任何改变);针对这么一种情况,可以使用 shouldComponentUpdate() 这一生命周期函数 ```js shouldComponentUpdate(nextProps, nextState) if (nextProps.xxx !== this.props.xxx) return true // 下一“状态”的 props 和当前的比较 else return false ``` ⒉AJAX 请求的位置 AJAX 请求一般放在 ComponentDidMount 生命周期函数中(只执行一次的) # 事件处理 在循环中,通常我们会为事件处理函数传递额外的参数。例如,若 `id` 是你要删除那一行的 ID,以下两种方式都可以向事件处理函数传递参数: ```js <button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button> <button onClick={this.deleteRow.bind(this, id)}>Delete Row</button> ``` 上述两种方式是等价的,分别通过`箭头函数`和`Function.prototype.bind`来实现。 在这两种情况下,React 的事件对象`e`会被作为第二个参数传递。如果通过箭头函数的方式,事件对象必须显式的进行传递,而通过`bind`的方式,事件对象以及更多的参数将会被隐式的进行传递。 # 组合(相当于 Vue 的 Slot) 有些组件无法提前知晓它们子组件的具体内容。在`Sidebar`(侧边栏)和`Dialog`(对话框)等展现通用容器(box)的组件中特别容易遇到这种情况。 这些组件可以使用一个特殊的`children prop` 来将他们的子组件传递到渲染结果中: ```js function FancyBorder(props) { return ( <div className={'FancyBorder FancyBorder-' + props.color}> {props.children} </div> ); } ``` 这使得别的组件可以通过 JSX 嵌套,将任意组件作为子组件传递给它们。 ```js function WelcomeDialog() { return ( <FancyBorder color="blue"> <h1 className="Dialog-title"> Welcome </h1> <p className="Dialog-message"> Thank you for visiting our spacecraft! </p> </FancyBorder> ); } ``` `<FancyBorder>`JSX 标签中的所有内容都会作为一个`children prop` 传递给`FancyBorder`组件。因为`FancyBorder`将`{props.children}`渲染在一个`<div>`中,被传递的这些子组件最终都会出现在输出结果中。 ***** 少数情况下,你可能需要在一个组件中预留出几个“洞”。这种情况下,我们可以不使用`children`,而是自行约定:将所需内容传入 props,并使用相应的 prop。 ```js function SplitPane(props) { return ( <div className="SplitPane"> <div className="SplitPane-left"> {props.left} </div> <div className="SplitPane-right"> {props.right} </div> </div> ); } function App() { return ( <SplitPane left={ <Contacts /> } right={ <Chat /> } /> ); } ``` # 高级指引 这部分内容整理自:[https://react.docschina.org/docs/code-splitting.html](https://react.docschina.org/docs/code-splitting.html) ## 代码分割 通过动态`import()`语法实现代码分割,当 Webpack 解析到该语法时,它会自动地进行代码分割。 ```js // 使用之前 import { add } from './math'; console.log(add(16, 26)); ``` ```js // 使用之后 import("./math").then(math => { console.log(math.add(16, 26)); }); ``` 使用 Create React App 该功能已配置好,如果自己配置 Webpack 需要配置代码分割用到的插件以及让 Babel 能解析动态 import 语法而不是将其转换。 <span style="font-size: 20px;">React.lazy</span> `React.lazy`函数能让你像渲染常规组件一样处理动态引入(的组件)。 ```js // 使用之前 import OtherComponent from './OtherComponent'; function MyComponent() { return ( <div> <OtherComponent /> </div> ); } ``` ```js // 使用之后 const OtherComponent = React.lazy(() => import('./OtherComponent')); function MyComponent() { return ( <div> <OtherComponent /> </div> ); } ``` 这个代码将会在渲染组件时,自动导入包含`OtherComponent`组件的包。 `React.lazy`接受一个函数,这个函数需要动态调用`import()`。它必须返回一个`Promise`,该 Promise 需要 resolve 一个`defalut`export 的 React 组件。 <span style="font-size: 20px;">Suspense</span> 如果在`MyComponent`渲染完成后,包含`OtherComponent`的模块还没有被加载完成,我们可以使用加载指示器为此组件做优雅降级。这里我们使用`Suspense`组件来解决。 ```js const OtherComponent = React.lazy(() => import('./OtherComponent')); function MyComponent() { return ( <div> <Suspense fallback={<div>Loading...</div>}> <OtherComponent /> </Suspense> </div> ); } ``` `fallback`属性接受任何在组件加载过程中你想展示的 React 元素。你可以将`Suspense`组件置于懒加载组件之上的任何位置。你甚至可以用一个`Suspense`组件包裹多个懒加载组件。 ```js const OtherComponent = React.lazy(() => import('./OtherComponent')); const AnotherComponent = React.lazy(() => import('./AnotherComponent')); function MyComponent() { return ( <div> <Suspense fallback={<div>Loading...</div>}> <section> <OtherComponent /> <AnotherComponent /> </section> </Suspense> </div> ); } ``` <span style="font-size: 20px;">基于路由的代码分割</span> 使用`React.lazy`和`React Router`来配置基于路由的代码分割 ```js import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; import React, { Suspense, lazy } from 'react'; const Home = lazy(() => import('./routes/Home')); const About = lazy(() => import('./routes/About')); const App = () => ( <Router> <Suspense fallback={<div>Loading...</div>}> <Switch> <Route exact path="/" component={Home}/> <Route path="/about" component={About}/> </Switch> </Suspense> </Router> ); ```