React 是一个声明式,高效且灵活的用于构建用户界面的 JavaScript 库。架构模式基于 MVC, UI 系统基于 JSX

🌄 安装

1、外部引入

  1. //部署时,将 "development.js" 替换为 "production.min.js"
  2. <script src="https://unpkg.com/react@16/umd/react.development.js"></script>
  3. <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
  4. //引入 jsx(不适用于生产环境)
  5. <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
  6. //使用 jsx(不适用于生产环境)
  7. <script type="text/babel">
  8. ReactDOM.render(<h1>Hello, world!</h1>,document.getElementById('root'));
  9. </script>
  • React:React顶级 API
  • React DOM:添加 DOM 特定的方法
  • Babel:一种JavaScript编译器,可让我们在开发环境旧版浏览器中使用ES6 +

2、CLI

  • npx安装cra:npx create-react-app my-app=>cd my-app=>npm start
  • yarn 安装 typescript+cra:yarn create react-app my-app --template typescript=>cd my-app=>yarn start
  • 安装storybook:
    • 在cra基础上安装:yarn add -D @storybook/preset-create-react-app
    • 安装story:npx -p @storybook/cli sb init --type react_scripts

🔰 JSX 及 React 元素

1、JSX

JSX 是 JavaScript 的语法扩展,描述 UI 呈现出它应有交互的本质形式。可拆分为多行,内容包裹在括号中。

  • JSX 表示对象:Babel 会把 JSX 转译成React.createElement(component, props, ...children)函数调用,创建对象被称为 “React 元素”。
  • JSX 可防止注入攻击;JSX 可嵌入表达式
  • JSX 是一个表达式:if 语句、for 循环不是表达式不能在 JSX 中直接使用。可使用 conditional (三元运算) 表达式来替代

1.1 JSX 类型

  • 使用点语法:在一个模块中导出许多 React 组件时,使用点语法来引用
  1. import React from "react";
  2. const MyComponents = {
  3. DatePicker: function DatePicker(props) {
  4. return <div>Imagine a {props.color} datepicker here.</div>;
  5. }
  6. };
  7. function BlueDatePicker() {
  8. return <MyComponents.DatePicker color="blue" />;
  9. }
  • 运行时选择类型:通过通用表达式来(动态)决定元素类型,需将类型赋值给一个大写字母开头的变量。根据 prop 来渲染不同组件:
  1. import React from "react";
  2. import { PhotoStory, VideoStory } from "./stories";
  3. const components = {
  4. photo: PhotoStory,
  5. video: VideoStory
  6. };
  7. function Story(props) {
  8. // 正确!JSX 类型可以是大写字母开头的变量。
  9. const SpecificStory = components[props.storyType];
  10. return <SpecificStory story={props.story} />;
  11. }

1.2 JSX 特定属性

使用 camelCase(小驼峰命名)来定义 props 属性的名称,Props 默认值为 “True”

  • 字符串字面量:const element = <div tabIndex="0"></div>;
  • JS 表达式:const element = <img src={user.avatarUrl}></img>;
  • 属性展开:易将不必要的 props 传递给不相关的组件。建议谨慎使用
  1. const Button = props => {
  2. const { kind, ...other } = props;
  3. const className = kind === "primary" ? "PrimaryButton" : "SecondaryButton";
  4. return <button className={className} {...other} />;
  5. };

1.3 JSX 中的子元素

  • 字符串字面量:<Component>Hello world!</Component>
  • JSX 子元素:可与字符串字面量同使用。组件能返回存储在数组中的一组元素
  • JS 表达式:{todos.map(msg => (<Item key={msg} msg={msg} />))}
  • 函数:{index => <div key={index}>This is item {index} </div>}
  • true、false、null、undefined 是合法的子元素但不会被渲染:
  1. //确保 && 之前的表达式总是布尔值:
  2. <div>
  3. {props.messages.length > 0 && <MessageList messages={props.messages} />}
  4. </div>

2、 React 元素

React 元素是不可变对象,代表某个特定时刻的 UI。React 只更新它需要更新的部分

  • ReactDOM.render(<Clock />, document.getElementById("root"));

🍎 组件及 Props、State

1、基本概念

组件 Vue React
定义 可复用的 Vue 实例 Props 作为参数返回 React 元素的 JavaScript 函数
命名 推荐使用 kebab-case 必须以大写字母开头
VirtualDOM 挂载 实例 render 函数创建虚拟节点 VNode,如render: h => h(App)
,el 选项或 vm.$mount() 再将其挂载在 DOM 节点
函数 render 方法返回 React 元素,再由 ReactDOM 的 render 方法将其挂载到 DOM 节点

2、组件分类

2.1 class 组件

  • 定义:作为 React.Component 子类,通过 props 从父组件向子组件传递数据
  • 渲染方法:render()方法是类组件中唯一需要的方法,用于渲染DOM节点
  • 构造函数:初始化 state 或进行方法绑定,一定需要调用 super(props)
    • 通过给 this.state 赋值对象来初始化内部 state。
    • 为事件处理函数绑定实例
  • 状态维护:state 用于组件状态维护,被视为一个组件的私有属性
    • 每次定义子类的构造函数时,都需要调用 super 方法。即 super(props) 开头
    • 每次在组件中调用 setState 时,React 都会自动更新其子组件
  • 状态提升:需共享 state 向上移动到最近共同父组件中的 state 中用作“数据源”,即“状态提升”。任何可变数据应当只有一个相对应的唯一“数据源”。
    • 受控组件:由 React 控制并且所有的表单数据统一存放的组件。响应数据改变时,子组件调用 this.props.onChange() 而非 this.setState()。
    • 不可变性:不直接修改原数据/底层数据以便跟踪数据的改变,确定在 React 中何时重新渲染
  1. class Square extends React.Component {
  2. render() {
  3. return (
  4. <button className="square" onClick={() => this.props.onClick()}>
  5. {this.props.value}
  6. </button>
  7. );
  8. }
  9. }
  10. class Board extends React.Component {
  11. constructor(props) {
  12. super(props);
  13. this.state = {
  14. squares: Array(9).fill(null)
  15. };
  16. }
  17. handleClick(i) {
  18. const squares = this.state.squares.slice();
  19. squares[i] = "X";
  20. this.setState({ squares: squares });
  21. }
  22. renderSquare(i) {
  23. return (
  24. <Square
  25. value={this.state.squares[i]}
  26. onClick={() => this.handleClick(i)}
  27. />
  28. );
  29. }
  30. render() {
  31. return (
  32. <div>
  33. {this.renderSquare(0)}
  34. {this.renderSquare(1)}
  35. {this.renderSquare(2)}
  36. </div>
  37. );
  38. }
  39. }

2.2 函数组件

  • 定义:不需定义一个继承于 React.Component 的类,可定义一个接收 props 作为参数的函数,然后返回需要渲染的元素
  1. //把两个 this.props 都替换成了 props,注意两侧括号不再有
  2. function Square(props){
  3. return (
  4. <button className="square" onClick={props.onClick}>
  5. {props.value}
  6. </button>
  7. )
  8. }

3、Props 的只读性

组件无论是使用函数声明还是通过 class 声明,都决不能修改自身的 props所有 React 组件都必须像纯函数一样保护其 props 不被更改。但状态 state 允许组件随时间更改其输出以响应用户操作,网络响应以及其他任何情况。

4、State

state 与 props 类似,但是 state 是私有的,并且完全受控于当前组件。

  • 不要直接修改 State,应该拷贝一份再使用 setState()
  • this.props 和 this.state 可能会异步更新,不要依赖他的值来更新下一个状态。可以让 setState() 接收一个函数而不是一个对象
  • state 的更新会被合并。调用 setState() 的时候,React 会把你提供的对象合并到当前的 state
  • 自上而下数据流:任何状态始终归某个特定组件所有,且从该状态派生的任何数据或UI都只影响树中“下方”的组件。
  • 如果某些数据可以由 props 或 state 推导得出,那么它就不应该存在于 state 中

5、生命周期

React Component Lifecycle.png

😎 事件处理

1、与原生区别

  • 使用小驼峰:onClick={handleClick}、onChange等等…
  • 只能显示阻止默认行为:handleClick(e){ e.preventDefault();}
  • 注意:在JavaScript中,默认情况下不绑定类方法。如果忘记绑定 this.handleClick 并将其传递给onClick,则在实际调用该函数时将无法定义
    • 通常,如果引用的方法后面没有(),例如onClick = {this.handleClick},则应绑定该方法
      • 箭头函数绑定:onClick={() => this.handleClick()}
      • 构造函数绑定:this.handleClick = this.handleClick.bind(this);
      • 在大多数情况下,箭头函数很好。但如果将此回调作为对较低组件的支持传递,则这些组件可能会进行额外的重新渲染。建议在构造函数中绑定或使用类字段语法,以避免此类性能问题。

2、TypeScript 分类事件

  • 合成事件:MouseEvent、KeyboardEvent…

🌮 条件渲染

1、变量存储组件

一般用{}包裹变量组件

2、与运算符

true && expression 总返回 expression,而 false && expression 总返回 false

3、三目运算符

三目运算符condition ? true : false可用于较复杂的表达式。若条件变得过于复杂,应考虑如何提取组件

4、阻止组件渲染

需要隐藏组件,render 方法中直接返回 null,不影响组件的生命周期,如 componentDidUpdate仍将被调用。

  1. function WarningBanner(props) {
  2. if (!props.warn) {
  3. return null;
  4. }
  5. return (
  6. <div className="warning">
  7. Warning!
  8. </div>
  9. );
  10. }

💥 列表组件及 Key

1、列表组件

map 方法把数组转化为元素列表。在数组 map() 方法中的元素需要设置 key 属性,以便识别哪些元素被改变。

2、key属性

key属性必须保证其在同级中 (siblings) 是唯一的,但不要求全局唯一

  • 索引作为键:深入了解使用索引作为键的负面影响。案例:使用key=id使用key=index
    • 使用场景:当以下所有这些都满足时,您以安全地将索引用作键
      • 列表和项目是静态的——它们不会被计算并且不会更改;
      • 列表中的项目没有ID;
      • 该列表永远不会重新排序或过滤
    • 不建议使用场景:项目的顺序可能更改,不建议使用
  • 录入ID 属性

    • 将抽象中的编号向上移动一级。使用全局索引可确保任何两个项目具有不同的ID

      1. todoCounter = 1;
      2. function createNewTodo(text) {
      3. return {
      4. completed: false,
      5. id: todoCounter++,
      6. text
      7. }
      8. }
    • 生产解决方案建议使用shortid。它可以快速生成“简短的,非顺序的,URL友好的唯一” ID

      1. var shortid = require('shortid');
      2. function createNewTodo(text) {
      3. return {
      4. completed: false,
      5. id: shortid.generate(),
      6. text
      7. }
      8. }
  • 传递给子组件:key 只是React的 Hint,不会传递给您的组件。如果在组件中需要相同的值,则将其作为 props以不同的名称显式传递。如key={id} id={id}使用 props.id 而不是 props.key

🎭 表单

React 中,表单状态通常保存在组件的 state 属性中,且只能通过 setState() 来更新

1、受控组件

state“唯一数据源”。被 React 控制取值的表单输入元素叫做“受控组件”。input等value属性始终由 state 驱动

  1. class NameForm extends React.Component {
  2. constructor(props) {
  3. super(props);
  4. this.state = { value: "" };
  5. }
  6. handleChange=(event)=>{
  7. this.setState({ value: event.target.value });
  8. }
  9. handleSubmit=(event)=>{
  10. alert("提交的名字: " + this.state.value);
  11. event.preventDefault();
  12. }
  13. render() {
  14. return (
  15. <form onSubmit={this.handleSubmit}>
  16. <label>名字:
  17. <input type="text" value={this.state.value}
  18. onChange={this.handleChange}
  19. />
  20. </label>
  21. <input type="submit" value="提交" />
  22. </form>
  23. );
  24. }
  25. }

2、textarea 及 select 标签

React 使用 value 属性而非 selected 属性,可传入数组以支持多选。如<select multiple={true} value={['B', 'C']}>

  1. class FlavorForm extends React.Component {
  2. constructor(props) {
  3. super(props);
  4. this.state = { value: "coconut" };
  5. }
  6. handleChange=(event)=> {
  7. this.setState({ value: event.target.value });
  8. }
  9. handleSubmit=(event)=> {
  10. alert("你喜欢的风味是: " + this.state.value);
  11. event.preventDefault();
  12. }
  13. render() {
  14. return (
  15. <form onSubmit={this.handleSubmit}>
  16. <label>选择你喜欢的风味:
  17. <select value={this.state.value} onChange={this.handleChange}>
  18. <option value="coconut">椰子</option>
  19. <option value="mango">芒果</option>
  20. </select>
  21. </label>
  22. <input type="submit" value="提交" />
  23. </form>
  24. );
  25. }
  26. }

3、文件 input 标签

<input type=“file” /> 允许用户从存储设备中选择多个文件上传到服务器。非受控组件,value 只读

4、处理多个输入

  • 每个元素添加 name 属性,根据 **event.target.name** 的值选择要执行的操作。 ```jsx class Reservation extends React.Component { constructor(props) { super(props); this.state = {

    1. isGoing: true,
    2. numberOfGuests: 2

    }; }

    handleInputChange=(event)=> { const target = event.target; const value = target.type === “checkbox” ? target.checked : target.value; const name = target.name; this.setState({

  1. [name]: value
  2. });

}

render() { return (


); } }

  1. - 计算属性:
  2. ```javascript
  3. let param = 'size'
  4. let config = {
  5. [param]: 12,
  6. ['mobile' + param.charAt(0).toUpperCase() + param.slice(1)]: 4
  7. }
  8. console.log(config) // {size: 12, mobileSize: 4}

😺 组合与继承

推荐使用组合而非继承来实现组件间的代码重用

1、包含关系

  • 使用特殊的 children prop 来将子组件传递到渲染结果中预留位置。 ```javascript function FancyBorder(props) { return (
    1. {props.children}
    ); }

function WelcomeDialog() { return (

Welcome

Thank you for visiting our spacecraft!

); }

  1. - 可不使用 children,自行约定,同“槽”slot
  2. ```javascript
  3. function SplitPane(props) {
  4. return (
  5. <div className="SplitPane">
  6. <div className="SplitPane-left">
  7. {props.left}
  8. </div>
  9. <div className="SplitPane-right">
  10. {props.right}
  11. </div>
  12. </div>
  13. );
  14. }
  15. function App() {
  16. return (
  17. <SplitPane
  18. left={
  19. <Contacts />
  20. }
  21. right={
  22. <Chat />
  23. } />
  24. );
  25. }

2、特例关系

组件看作是其他组件的特殊实例。“特殊”组件可以通过 props 定制并渲染“一般”组件:

  1. function Dialog(props) {
  2. return (
  3. <FancyBorder color="blue">
  4. <h1 className="Dialog-title">{props.title}</h1>
  5. <p className="Dialog-message">{props.message}</p>
  6. {props.children}
  7. </FancyBorder>
  8. );
  9. }
  10. class SignUpDialog extends React.Component {
  11. constructor(props) {
  12. super(props);
  13. this.state = { login: "" };
  14. }
  15. handleChange=(e)=> {this.setState({ login: e.target.value });}
  16. handleSignUp=()=> {alert(`Welcome aboard, ${this.state.login}!`);}
  17. render() {
  18. return (
  19. <Dialog
  20. title="Mars Exploration Program"
  21. message="How should we refer to you?"
  22. >
  23. <input value={this.state.login} onChange={this.handleChange} />
  24. <button onClick={this.handleSignUp}>Sign Me Up!</button>
  25. </Dialog>
  26. );
  27. }
  28. }

☂ 其他

1、代码分割

1.1 React.lazy

用于处理动态引入的组件。Suspense 组件可包裹多个 lazy 组件并置于之上任何位置,fallback 属性接受组件加载过程中任何可展示的 React 元素

  1. const OtherComponent = React.lazy(() => import("./OtherComponent"));
  2. function MyComponent() {
  3. return (
  4. <div>
  5. <Suspense fallback={<div>Loading...</div>}>
  6. <OtherComponent />
  7. </Suspense>
  8. </div>
  9. );
  10. }

1.2 异常捕获边界

模块加载失败(如网络问题)可通过异常捕获边界(Error boundaries)技术来处理,以显示良好的用户体验并管理恢复事宜

  1. import MyErrorBoundary from "./MyErrorBoundary";
  2. const OtherComponent = React.lazy(() => import("./OtherComponent"));
  3. const AnotherComponent = React.lazy(() => import("./AnotherComponent"));
  4. const MyComponent = () => (
  5. <div>
  6. <MyErrorBoundary>
  7. <Suspense fallback={<div>Loading...</div>}>
  8. <section><OtherComponent /><AnotherComponent /></section>
  9. </Suspense>
  10. </MyErrorBoundary>
  11. </div>
  12. );

错误边界作为组件,可捕获并打印发生在其子组件树任何位置的 JS 错误,且渲染出备用 UI。在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误。

  • 当抛出错误后,使用 static getDerivedStateFromError() 渲染备用 UI ,使用 componentDidCatch() 打印错误信息
  • 错误边界仅可以捕获其子组件的错误,它无法捕获其自身的错误

    1. class ErrorBoundary extends React.Component {
    2. constructor(props) {
    3. super(props);
    4. this.state = { hasError: false };
    5. }
    6. static getDerivedStateFromError(error) {
    7. // 更新 state 使下一次渲染能够显示降级后的 UI
    8. return { hasError: true };
    9. }
    10. componentDidCatch(error, errorInfo) {
    11. // 你同样可以将错误日志上报给服务器
    12. logErrorToMyService(error, errorInfo);
    13. }
    14. render() {
    15. if (this.state.hasError) {
    16. // 你可以自定义降级后的 UI 并渲染
    17. return <h1>Something went wrong.</h1>;
    18. }
    19. return this.props.children;
    20. }
    21. }

    1.3 基于路由的代码分割

    使用 React.lazy 和 React Router 这类的第三方库,来配置基于路由的代码分割 ```javascript 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 = () => ( Loading…}> );

  1. <a name="37d994ee"></a>
  2. ### 2、Refs 及其转发
  3. > Refs 提供一种方式,允许访问 DOM 节点或在 render 方法中创建的 React 元素
  4. > Ref 转发是将 ref 自动地通过组件传递到其一子组件,常用于可重用的组件库
  5. Ref 转发是可选特性,其允许某些组件接收 ref,并将其向下传递给子组件。FancyButton 使用 `React.forwardRef` 来获取传递给它的 ref,然后转发到它渲染的 DOM button
  6. - 调用 React.createRef 创建一个 React ref 并将其赋值给 ref 变量。
  7. - 指定 ref 为 JSX 属性,将其向下传递给 `<FancyButton ref={ref}>`。
  8. - React 传递 ref 给 forwardRef 内函数 (props, ref) => ...,作为其第二个参数。
  9. - 向下转发该 ref 参数到 `<button ref={ref} />`,将其指定为 JSX 属性。
  10. - ref 挂载完成,ref.current 将指向 `<button>` DOM 节点
  11. ```javascript
  12. const FancyButton = React.forwardRef((props, ref) => (
  13. <button ref={ref} className="FancyButton">
  14. {props.children}
  15. </button>
  16. ));
  17. // 你可以直接获取 DOM button 的 ref:
  18. const ref = React.createRef();
  19. <FancyButton ref={ref}>Click me!</FancyButton>;

3、Fragments

React 中的一个常见模式是一个组件返回多个元素。Fragments 允许你将子列表分组,而无需向 DOM 添加额外节点。类似 Vue 中的 slot

  • 短语法 <> </>也可声明 Fragments,但不支持 key 或属性
  • 带 key 的 Fragments:key 是唯一可以传递给 Fragment 的属性
  1. function Glossary(props) {
  2. return (
  3. <dl>
  4. {props.items.map(item => (
  5. // 没有`key`,React 会发出一个关键警告
  6. <React.Fragment key={item.id}>
  7. <dt>{item.term}</dt>
  8. <dd>{item.description}</dd>
  9. </React.Fragment>
  10. ))}
  11. </dl>
  12. );
  13. }

4、高阶组件

高阶组件(HOC)是参数为组件,返回值为新组件的函数。编写一个创建组件函数。该函数将接受一个子组件作为它的其中一个参数,该子组件将订阅数据作为 prop。

  • 不要改变原始组件,使用组合。可以将 HOC 视为参数化容器组件
  • HOC 应该透传与自身无关的 props
  • 常见的 HOC 签名如下:const ConnectedComment = connect(commentSelector, commentActions)(CommentList);
  • 不要在 render 方法中使用 HOC,务必复制静态方法,Refs 不会被传递
  1. function withSubscription(WrappedComponent, selectData) {
  2. // ...并返回另一个组件...
  3. return class extends React.Component {
  4. constructor(props) {
  5. super(props);
  6. this.handleChange = this.handleChange.bind(this);
  7. this.state = {
  8. data: selectData(DataSource, props)
  9. };
  10. }
  11. componentDidMount() {
  12. // ...负责订阅相关的操作...
  13. DataSource.addChangeListener(this.handleChange);
  14. }
  15. componentWillUnmount() {
  16. DataSource.removeChangeListener(this.handleChange);
  17. }
  18. handleChange() {
  19. this.setState({
  20. data: selectData(DataSource, this.props)
  21. });
  22. }
  23. render() {
  24. // ... 并使用新数据渲染被包装的组件!
  25. // 请注意,我们可能还会传递其他属性
  26. return <WrappedComponent data={this.state.data} {...this.props} />;
  27. }
  28. };
  29. }

5、Portals

Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。
ReactDOM.createPortal(child, container)

  • 一个 portal 的典型用例是当父组件有 overflow: hidden 或 z-index 样式时,但你需要子组件能够在视觉上“跳出”其容器。例如,对话框、悬浮卡以及提示框
  • 当在使用 portal 时, 管理键盘焦点尤为重要

    1. render() {
    2. // React 并*没有*创建一个新的 div。它只是把子元素渲染到 `domNode` 中。
    3. // `domNode` 是一个可以在任何位置的有效 DOM 节点。
    4. return ReactDOM.createPortal(
    5. this.props.children,
    6. domNode
    7. );
    8. }
  • 通过 Portal 进行事件冒泡:一个从 portal 内部触发的事件会一直冒泡至包含 React 树的祖先

🎒 Hooks

1、Hooks简介

Hook 让你在函数组件里“钩入” React state 及生命周期等特性的函数,不能在 class 组件中使用。

  • 优化组件间复用状态逻辑:无需修改组件结构的情况下复用状态逻辑
  • 更好理解复杂组件:将组件中相互关联的部分拆分成更小的函数,非强制按照生命周期划分。使用 reducer 来管理组件的内部状态,使其更加可预测
  • class问题:非 class 的情况下可以使用更多的 React 特性,可渐进式地使用

2、Hooks概览

2.1 State Hook——useState()

  • 参数调用:在函数组件里调用,参数为初始状态 initState
  • 返回:数组解构为 state、setState
    • 更新 state 函数 setState:若 state 更新取决于以前的 state,需始终在 setState 中使用一个函数
  • 重新渲染 reRender:每次更新 state 函数调用会触发 reRender 重新执行函数组件,因此 state 必须单独存储在useState外作为全局变量,以免重新渲染时使用useState中重置的 state
  • 惰性初始化 state

    • initialState 参数只会在组件的初始化渲染中起作用,后续渲染时会被忽略
    • 初始 state 需要通过复杂计算获得,则可传入函数计算并返回初始的 state,此函数只在初始渲染时被调用

      2.2 Effect Hook——useEffect()

      副作用:React 组件中执行数据获取、订阅、设置定时器或者手动修改 DOM等。默认React 会在每次渲染后调用副作用函数 —— 包括第一次渲染的时候,可访问组件的 props 和 state。

  • useEffect :副作用钩子,使函数组件操作副作用。与 class 组件中的componentDidMount 、componentDidUpdate、componentWillUnmount 功能类似

    1. const Chart = ({ data }) => {
    2. useEffect(() => {
    3. // when Chart mounts, do this
    4. // when data updates, do this
    5. return () => {
    6. // when data updates, do this
    7. // before Chart unmounts, do this
    8. }
    9. }, [data])
    10. return (
    11. <svg className="Chart" />
    12. )
    13. }
  • 参数调用: callback 和 dependencies 数组。callback 即副作用,为异步执行的回调函数,不能为async 函数

    • 如果 dependencies 不存在,那么 callback 每次 render 都会执行
    • 如果 dependencies 存在,只有当它发生了变化, callback 才会二次执行
    • Q:为什么第二个参数是空数组,相当于 componentDidMount
      • A:因为依赖一直不变化,callback 不会二次执行
  • callback 返回:分为无需清除的 effect 和 需要清除的 effect
    • 无返回:无需清除的 effect,如发送网络请求,手动变更 DOM,记录日志等。
    • 返回清除函数:需清除的 effect,如订阅外部数据源,以防止引起内存泄露。将添加和移除订阅的逻辑放在一起,React 会在组件卸载时执行清除操作
  • dependencies 依赖
    • 诚实告知依赖:在依赖中包含所有effect中用到的组件内的值;修改effect内部的代码以确保它包含的值只会在需要的时候发生变更
    • 移除不必要依赖使用类似 setState(state=>state+1); 使用useReducer,可以把更新逻辑 reducer 和描述发生了什么 action 分开,相比于直接在effect里面读取状态,它dispatch了一个action来描述发生了什么。使得 effect 和 step 状态解耦 ```javascript const [state, dispatch] = useReducer(reducer, initialState); const { count, step } = state;

useEffect(() => { const id = setInterval(() => { dispatch({ type: ‘tick’ }); // Instead of setCount(c => c + step); }, 1000); return () => clearInterval(id); }, [dispatch]);

  1. - 函数作为依赖:把query添加到useCallback 的依赖中,任何调用了getFetchUrl fetchData effectquery改变后都会重新运行。useCallback本质上是添加了一层依赖检查。使函数本身只在需要的时候才改变,而不是去掉对函数的依赖
  2. ```javascript
  3. function SearchResults() {
  4. const [query, setQuery] = useState('react');
  5. // ✅ Preserves identity until query changes
  6. const getFetchUrl = useCallback(() => {
  7. return 'https://hn.algolia.com/api/v1/search?query=' + query;
  8. }, [query]); // ✅ Callback deps are OK
  9. useEffect(() => {
  10. const url = getFetchUrl();
  11. // ... Fetch data and do something ...
  12. }, [getFetchUrl]); // ✅ Effect deps are OK
  13. // ...
  14. }
  15. }
  16. //通过属性从父组件传入的函数
  17. function Parent() {
  18. const [query, setQuery] = useState('react');
  19. // ✅ Preserves identity until query changes
  20. const fetchData = useCallback(() => {
  21. const url = 'https://hn.algolia.com/api/v1/search?query=' + query;
  22. // ... Fetch data and return it ...
  23. }, [query]); // ✅ Callback deps are OK
  24. return <Child fetchData={fetchData} />
  25. }
  26. function Child({ fetchData }) {
  27. let [data, setData] = useState(null);
  28. useEffect(() => {
  29. fetchData().then(setData);
  30. }, [fetchData]); // ✅ Effect deps are OK
  31. // ...
  32. }
  • 建议:把不依赖props和state的函数提到你的组件外面,并且把那些仅被effect使用的函数放到effect里面。effect 若需用到组件内的函数(包括通过props传进来的函数),可以在定义它们的地方用useCallback包一层
  • 与 class 组件生命周期对比:useEffect 保证自身 state 的 data 值 与 props 的 dataRange 变量同步

    1. const Chart = ({ dateRange }) => {
    2. const [data, setData] = useState()
    3. useEffect(() => {
    4. const newData = getDataWithinRange(dateRange)
    5. setData(newData)
    6. }, [dateRange])
    7. return (
    8. <svg className="Chart" />
    9. )
    10. }
  • 转换场景:

    • 将数据存储在状态中,以防止每次组件更新时都重新计算数据。可不使用 state 而使用 useMemo( ),它将仅在其依赖项数组更改时重新计算数据,常用于大量数据或复杂计算
      1. const Chart = ({ dateRange }) => {
      2. const data = useMemo(() => (
      3. getDataWithinRange(dateRange)
      4. ), [dateRange])
      5. return (
      6. <svg className="Chart" />
      7. )
      8. }
  • 竞态与异步请求:

    1. function Article({ id }) {
    2. const [article, setArticle] = useState(null);
    3. useEffect(() => {
    4. let didCancel = false;
    5. async function fetchData() {
    6. const article = await API.fetchArticle(id);
    7. if (!didCancel) {
    8. setArticle(article);
    9. }
    10. }
    11. fetchData();
    12. return () => {
    13. didCancel = true;//在清除函数中取消异步请求
    14. };
    15. }, [id]);
    16. // ...
    17. }
  • 自定义异步请求 Hook:useDataApi ```javascript const dataFetchReducer = (state, action) => { switch (action.type) { case ‘FETCH_INIT’:

    1. return {
    2. ...state,
    3. isLoading: true,
    4. isError: false
    5. };

    case ‘FETCH_SUCCESS’:

    1. return {
    2. ...state,
    3. isLoading: false,
    4. isError: false,
    5. data: action.payload,
    6. };

    case ‘FETCH_FAILURE’:

    1. return {
    2. ...state,
    3. isLoading: false,
    4. isError: true,
    5. };

    default:

    1. throw new Error();

    } };

const useDataApi = (initialUrl, initialData) => { const [url, setUrl] = useState(initialUrl);

const [state, dispatch] = useReducer(dataFetchReducer, { isLoading: false, isError: false, data: initialData, });

useEffect(() => { let didCancel = false;

  1. const fetchData = async () => {
  2. dispatch({ type: 'FETCH_INIT' });
  3. try {
  4. const result = await axios(url);
  5. if (!didCancel) {
  6. dispatch({ type: 'FETCH_SUCCESS', payload: result.data });
  7. }
  8. } catch (error) {
  9. if (!didCancel) {
  10. dispatch({ type: 'FETCH_FAILURE' });
  11. }
  12. }
  13. };
  14. fetchData();
  15. return () => {
  16. didCancel = true;
  17. };

}, [url]);

return [state, setUrl]; };

//使用 useDataApi const AnyComponent = props => { const [{ data, isLoading, isError }, doFetch] = useDataApi( ‘http://hn.algolia.com/api/v1/search?query=redux‘, null, );

… };

  1. <a name="473c4484"></a>
  2. #### 2.3 Hooks 复用问题
  3. 对于多个 Hooks,为保证各自 state、deps 相互独立。可以使用数组来解决 Hooks 的复用问题。在 React 中数据存在 fiber node 上,每个组件都有自己的 memoizedState。
  4. - 初次渲染的时候,按照 useState,useEffect 的顺序,把 state,deps 等按顺序塞到 memoizedState 数组中
  5. - **reRender 时会重新执行函数组件,从 memoizedState 中按顺序查询 state、deps 上次记录的值,同时忽略Hooks 传入的 initState 参数**
  6. ![](https://cdn.nlark.com/yuque/0/2020/png/401207/1590817089375-f7f378c1-45d2-40b5-8c3f-edf3e869b1f0.png#crop=0&crop=0&crop=1&crop=1&height=1148&id=Pmrql&originHeight=1148&originWidth=460&originalType=binary&ratio=1&rotation=0&showTitle=false&size=0&status=done&style=none&title=&width=460)
  7. ```javascript
  8. let memoizedState = []; // hooks 存放在这个数组
  9. let cursor = 0; // 当前 memoizedState 下标
  10. function useState(initialValue) {
  11. memoizedState[cursor] = memoizedState[cursor] || initialValue;
  12. // 如果当前没有 state,说明是第一次执行,把 initialValue 复制给它
  13. // 不严谨bug:init为false时
  14. const currentCursor = cursor;
  15. function setState(newState) {
  16. memoizedState[currentCursor] = newState;
  17. cursor = 0;
  18. render();//简化reRender
  19. }
  20. return [memoizedState[cursor++], setState]; // 返回当前 state,并把 cursor 加 1
  21. }
  22. function useEffect(callback, depArray) {
  23. const hasNoDeps = !depArray;
  24. const deps = memoizedState[cursor];// deps 记录 useEffect 上一次的 依赖
  25. const hasChangedDeps = deps
  26. ? !depArray.every((el, i) => el === deps[i])
  27. : true;
  28. /* 如果 dependencies 不存在,或者 dependencies 有变化*/
  29. if (hasNoDeps || hasChangedDeps) {
  30. callback();
  31. memoizedState[cursor] = depArray;
  32. }
  33. cursor++;
  34. }

2.4 Hook 使用规则

  • 只能在函数最外层调用 Hook,不要在循环、条件判断或者子函数中调用。为什么?
    • 确保 Hooks 执行顺序一致:memoizedState 数组是按 hook定义的顺序来放置数据,若 hook不在最外层被调用,则重新渲染时会找不到相应数据而报错
  • 只能在 React 的函数组件、自定义的 Hook 中调用 Hook。不要在其他 JavaScript 函数中调用

    2.5 自定义 Hook

    自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook。用于共享组件之间的状态逻辑(另有高阶组件、render props)

  • 自定义 Hook 是一种重用状态逻辑的机制,每次使用自定义 Hook 时,所有 state 和副作用都是完全隔离的

  • Hook 的每次调用都有一个完全独立的 state
  • useInterval ```javascript import React, { useState, useEffect, useRef } from ‘react’;

//Hook function useInterval(callback, delay) { const savedCallback = useRef();

// Remember the latest callback. useEffect(() => { savedCallback.current = callback; }, [callback]);

// Set up the interval. useEffect(() => { function tick() { savedCallback.current(); } if (delay !== null) { let id = setInterval(tick, delay); return () => clearInterval(id); } }, [delay]); }

//Use Hook function Counter() { let [count, setCount] = useState(0);

useInterval(() => { // Your custom logic here setCount(count + 1); }, 1000);

return

{count}

; }

  1. <a name="6dVsf"></a>
  2. #### 2.6 重新渲染过程
  3. - 每一次渲染都有它自己的 Props 和 State,在单次渲染的范围内,props和state始终保持不变
  4. - 每一次渲染都是独立的闭包,每一次渲染都有它自己的事件处理函数,有它自己的 Effects
  5. - 每一个组件内的函数(包括事件处理函数,effects,定时器或者API调用等等)会捕获某次渲染中定义的props和state
  6. - 在 effect 的回调函数中获取最新的 state :使用 ref
  7. ```javascript
  8. function Example() {
  9. const [count, setCount] = useState(0);
  10. const latestCount = useRef(count);
  11. useEffect(() => {
  12. // Set the mutable latest value
  13. latestCount.current = count;
  14. setTimeout(() => {
  15. // Read the mutable latest value
  16. console.log(`You clicked ${latestCount.current} times`);
  17. }, 3000);
  18. });

3、useReducer

action 钩子,用于复杂 state 管理且包含多个子值,或下一 state 依赖于之前 state 等。当你写类似setSomething(something => …)这种代码的时候,也许就是考虑使用reducer的契机。const [state, dispatch] = useReducer(reducer, initState,init);

  • 参数:reducer、initState、init
    • reducer 纯函数:接收状态 state、行为 action,返回新状态 newState
    • initState 初始状态:用于首次渲染的状态取值,同 useState 中的状态一样
    • init 惰性初始化函数:可选,()=>(initState)
  • 返回:state、dispatch
    • state 状态:同 useState中的状态一样
    • dispatch 分发函数:用于传入 action 并分发给相应的 reducer
  • 与useState 区别: 将 setState 函数细拆分为 dispatch、reducer。组件发出 action 与状态管理器通信。状态管理器收到 action 以后,使用 reducer 函数算出新的状态 ```javascript const todoReducer = (state, action) => { //action 至少有一个类型 type 属性,还有一个有效负载 payload,涉及行为变动的数据 switch (action.type) { case ‘DO_TODO’:
    1. return state.map(todo => {
    2. if (todo.id === action.id) {
    3. return { ...todo, complete: true };
    4. } else {
    5. return todo;
    6. }
    7. });
    case ‘UNDO_TODO’:
    1. return state.map(todo => {
    2. if (todo.id === action.id) {
    3. return { ...todo, complete: false };
    4. } else {
    5. return todo;
    6. }
    7. });
    default:
    1. return state;
    } }; const initialTodos = [ { id: ‘a’, task: ‘Learn React’, complete: false, }, { id: ‘b’, task: ‘Learn Firebase’, complete: false, }, ];

const App = () => { const [todos, dispatch] = React.useReducer( todoReducer, initialTodos );

const handleChange = todo => { dispatch({ type: todo.complete ? ‘UNDO_TODO’ : ‘DO_TODO’, id: todo.id, }); };

return (

    {todos.map(todo => (
  • ))}
); };

  1. <a name="ubahh"></a>
  2. ### 4、useContext
  3. 共享状态钩子。如果需要在组件之间共享状态,可以使用useContext()。useContext(MyContext) 相当于 class 组件中的 static contextType = MyContext 或者 <MyContext.Consumer>
  4. - 参数:一个 context 对象(React.createContext 的返回值)
  5. - 返回: context 的当前值,由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定
  6. - 当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值
  7. ```javascript
  8. const AppContext = React.createContext({});
  9. <AppContext.Provider value={{
  10. username: 'superawesome'
  11. }}>
  12. <div className="App">
  13. <Navbar/>
  14. <Messages/>
  15. </div>
  16. </AppContext.Provider>
  17. const Navbar = () => {
  18. const { username } = useContext(AppContext);
  19. return (
  20. <div className="navbar">
  21. <p>AwesomeSite</p>
  22. <p>{username}</p>
  23. </div>
  24. );
  25. }
  26. const Messages = () => {
  27. const { username } = useContext(AppContext)
  28. return (
  29. <div className="messages">
  30. <h1>Messages</h1>
  31. <p>1 message for {username}</p>
  32. </div>
  33. )
  34. }


5、useRef

useRef 返回一个可变的 ref 对象,其 current 属性被初始化为传入的参数(initialValue)。

  • useRef 返回的 ref 对象在组件的整个生命周期内保持不变,即每次重新渲染函数组件时,返回的ref 对象都是同一个(使用 React.createRef ,每次重新渲染组件都会重新创建 ref) ```javascript import React, { useState, useEffect, useRef } from ‘react’; import ReactDOM from ‘react-dom’;

function Parent() { let [number, setNumber] = useState(0); return ( <> </> ) } let input; function Child() { const inputRef = useRef(); console.log(‘input===inputRef’, input === inputRef); input = inputRef; function getFocus() { inputRef.current.focus(); } return ( <> </> ) } ReactDOM.render(, document.getElementById(‘root’));

  1. - forwardRef
  2. - **forwardRef 可以在父组件中操作子组件的 ref 对象**
  3. - **forwardRef 可以将父组件中的 ref 对象转发到子组件中的 dom 元素上**
  4. - **子组件接受 props ref 作为参数**
  5. - **配合useImperativeHandle forwarfRef传入的ref元素进行绑定,指定该子组件对外暴露的方法或属性**
  6. ```javascript
  7. function InputWithLabel(props) {
  8. const { label, myRef } = props;
  9. const [value, setValue] = useState("");
  10. const _innerRef = useRef(null);
  11. const handleChange = e => {
  12. const value = e.target.value;
  13. setValue(value);
  14. };
  15. const getValue = () => {
  16. return value;
  17. };
  18. useImperativeHandle(myRef, () => ({
  19. getValue,
  20. focus() {
  21. const node = _innerRef.current;
  22. node.focus();
  23. }
  24. }));
  25. return (
  26. <div>
  27. <span>{label}:</span>
  28. <input
  29. type="text"
  30. ref={_innerRef}
  31. value={value}
  32. onChange={handleChange}
  33. />
  34. </div>
  35. );
  36. }
  37. const RefInput = React.forwardRef((props, ref) => (
  38. <InputWithLabel {...props} myRef={ref} />
  39. ));
  40. function App() {
  41. const myRef = useRef(null);
  42. const handleFocus = () => {
  43. const node = myRef.current;
  44. console.log(node);
  45. node.focus();
  46. };
  47. return (
  48. <div className="App">
  49. <RefInput label={"姓名"} ref={myRef} />
  50. <button onClick={handleFocus}>focus</button>
  51. </div>
  52. );
  53. }