React 学习笔记

158 阅读15分钟

一、生命周期

生命周期.png

1. 挂载

当组件实例被创建并插入 DOM 中时,其生命周期调用顺序如下:

  • constructor()
  • static getDerivedStateFromProps(nextProps, prevState)
  • render()
  • componentDidMount()
  • componentWillMount() 【即将过时】

2. 更新

当组件的 props 或 state 发生变化时会触发更新。组件更新的生命周期调用顺序如下:

  • static getDerivedStateFromProps(nextProps, prevState)
  • shouldComponentUpdate(nextProps, nextState)
  • render()
  • getSnapshotBeforeUpdate(prevProps, prevState)
  • componentDidUpdate(prevProps, prevState, snapshot)
  • componentWillUpdate(nextProps, nextState) 【即将过时】
  • componentWillReceiveProps(nextProps) 【即将过时】

3. 卸载

当组件从 DOM 中移除时会调用如下方法:

  • componentWillUnmount()

4. 错误处理

当渲染过程,生命周期,或子组件的构造函数中抛出错误时,会调用如下方法:

  • static getDerivedStateFromError(error)
  • componentDidCatch(error, info)

5. 一些注意点

  1. render()
  • render() 方法是 class 组件中唯一必须实现的方法。
  • render() 函数应该为纯函数。
  1. shouldComponentUpdate()
  • 用来判断是否需要调用 render 方法重新渲染 dom
  • 首次渲染或使用 forceUpdate() 时不会调用该方法。
  • 返回 false 并不会阻止子组件在 state 更改时重新渲染。
  • 不建议在 shouldComponentUpdate() 中进行深层比较或使用 JSON.stringify()。这样非常影响效率,且会损害性能。
  1. React.PureComponent
  • React.PureComponent 与 React.Component 很相似。两者的区别在于 React.Component 并未实现 shouldComponentUpdate(),而 React.PureComponent 中以浅层对比 prop 和 state 的方式来实现了该函数。
  • 如果赋予 React 组件相同的 props 和 state,render() 函数会渲染相同的内容,那么在某些情况下使用 React.PureComponent 可提高性能。
  • 此外,React.PureComponent 中的 shouldComponentUpdate() 将跳过所有子组件树的 prop 更新。因此,请确保所有子组件也都是“纯”的组件。
  1. React.memo
  • React.memo 为高阶组件。
  • 如果你的组件在相同 props 的情况下渲染相同的结果,那么你可以通过将其包装在 React.memo 中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。这意味着在这种情况下,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。
  • React.memo 仅检查 props 变更。如果函数组件被 React.memo 包裹,且其实现中拥有 useStateuseReduceruseContext 的 Hook,当 state 或 context 发生变化时,它仍会重新渲染。
  • 默认情况下其只会对复杂对象做浅层对比,如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现。
    function MyComponent(props) {
      /* 使用 props 渲染 */
    }
    function areEqual(prevProps, nextProps) {
      /*
      如果把 nextProps 传入 render 方法的返回结果与
      将 prevProps 传入 render 方法的返回结果一致则返回 true,
      否则返回 false
      */
    }
    export default React.memo(MyComponent, areEqual);
    
  • 与 class 组件中 shouldComponentUpdate() 方法不同的是,如果 props 相等,areEqual 会返回 true;如果 props 不相等,则返回 false。这与 shouldComponentUpdate 方法的返回值相反。
  1. static getDerivedStateFromProps()
  • 它返回一个对象来更新 state,如果返回 null 则不更新任何内容。
  • 此方法无权访问组件实例。如果你需要,可以通过提取组件 props 的纯函数及 class 之外的状态,在getDerivedStateFromProps()和其他 class 方法之间重用代码。
  • 请注意,不管原因是什么,都会在每次渲染前触发此方法。这与 componentWillReceiveProps 形成对比,后者仅在父组件重新渲染时触发,而不是在内部调用 setState 时。
  1. getSnapshotBeforeUpdate()
  • getSnapshotBeforeUpdate() 在最近一次渲染输出(提交到 DOM 节点)之前调用。它使得组件能在发生更改之前从 DOM 中捕获一些信息(例如,滚动位置)。此生命周期的任何返回值将作为参数传递给 componentDidUpdate()
  1. getDerivedStateFromError() 会在渲染阶段调用,因此不允许出现副作用,此外,它将抛出的错误作为参数,并返回一个值以更新 state。componentDidCatch() 会在“提交”阶段被调用,因此允许执行副作用。

二、setState()

1. 概述

setState(updater, [callback])
  • updater可以是带有形参的函数:
this.setState((state, props) => {
  return {counter: state.counter + props.step};
});

updater 函数中接收的 state 和 props 都保证为最新。

  • updater也可以是对象:
this.setState({quantity: 2})

2. 合成事件

  • 合成事件不是直接作用在 dom 元素上,而是绑定到 document 上,通过冒泡,然后在 document 上监听,并通过一个统一的 dispatchEvent 函数去执行事件分发,交由真正的处理函数运行。

3. 更新机制

  • 如果是在隶属于原生js执行的空间,比如说 setTimeout 里面,setState 是同步的,那么每次执行 setState 将立即更新 this.state ,然后触发 render 方法,渲染数据。
  • 如果是在被react处理过的空间执行,比如说合成事件里,此时 setState 是异步执行的,并不会立即更新 this.state 的值,当执行 setState 的时候,会将需要更新的 state 放入状态队列,在这个空间最后再合并修改 this.state,触发 render。
  • setState 的批量更新优化也是建立在这种“异步”之上的

三、高阶组件

  • 高阶组件(HOC,Higher Order Component)是 React 中用于复用组件逻辑的一种高级技巧,是一种基于 React 的组合特性而形成的设计模式。具体而言,高阶组件是参数为组件,返回值为新组件的函数。 HOC 是纯函数,没有副作用。
  • 常见的高阶组件:React.memo、Redux 中的 connect
  • HOC 能做什么?
    • 代码复用,逻辑抽象
    • 渲染劫持
    • 状态抽象和控制
    • Props 控制

四、受控组件和非受控组件

  • 受控组件:在表单元素中,state 是唯一数据源,渲染表单的 React 组件控制着用户输入过程中表单发生的操作。被 React 以这种方式控制取值的表单输入元素叫做受控组件。
  • 非受控组件:表单数据由 DOM 节点来处理,而不是用 state 来管理数据,一般可以使用 ref 来从 DOM 节点中获取表单数据。

五、类组件和函数组件的区别

  1. 函数组件是一个函数,返回一个 jsx 元素,而类组件是用 es6 的语法糖 class 来定义的,继承了 React.Component 这个类;
  2. 类组件中可以通过 state 来进行状态管理,而在函数组件中不能使用setState(),在 react16.8 以后,函数组件可以通过 hooks 中的 useState 来模拟类组件中的状态管理;
  3. 类组件中有一系列的生命周期钩子函数,在函数组件中需要借助 hooks 来模拟生命周期函数;
  4. 类组件能够捕获最新的值(永远保持一致),这是因为当实例的 props 属性发生改变时,class 组件能够直接通过 this 捕获到组件最新的 props;而函数组件是捕获渲染所使用的值,因为javascript 闭包的特性,之前的 props 参数保存在内存之中,无法从外部进行修改。
  5. 优化方式不同
  6. 使用 ref 的方式不同

六、Hook

Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

  • 基础 Hook

    • useState:维护状态
    • useEffect:执行副作用,相当于 class 组件中componentDidMount/componentDidUpdate/componentWillUnmount三个生命周期的综合
    • useContext:用于实现组件通信
  • 额外的 Hook

    • useReducer:useState替代方案
    • useCallback:用于函数组件性能优化,返回值是一个函数
    • useMemo:用于函数组件性能优化,返回值可以是函数、对象等都可以
    • useRef:用于绑定组件/元素和保存数据
    • useImperativeHandle:可以让你在使用 ref 时自定义暴露给父组件的实例值,搭配useRef、forwardRef来使用,用于获取子组件(函数组件)中定义的方法
      function FancyInput(props, ref) {
        const inputRef = useRef();
        useImperativeHandle(ref, () => ({
          focus: () => {
            inputRef.current.focus();
          }
        }));
        return <input ref={inputRef} ... />;
      }
      FancyInput = forwardRef(FancyInput);
      
    • useLayoutEffect:用法和 useEffect 一致,但是它们的执行时机不同,useLayoutEffect 在 DOM 更新之后执行,useEffect 在 render 渲染结束后执行,也就是说 useLayoutEffect 比 useEffect 先执行
    • useDebugValue:用于浏览器调试
  • React.forwardRef 会创建一个React组件,这个组件能够将其接受的 ref 属性转发到其组件树下的另一个组件中。

  • Hook规则

    • 只在最顶层使用 Hook

      不要在循环,条件或嵌套函数中调用 Hook,  确保总是在你的 React 函数的最顶层以及任何 return 之前调用他们。

      原因:这是因为React需要利用调用顺序来正确更新相应的状态,以及调用相应的钩子函数。一旦在循环或条件分支语句中调用Hook,就容易导致调用顺序的不一致性,从而产生难以预料到的后果。

    • 只在 React 函数中调用 Hook

      不要在普通的 JavaScript 函数中调用 Hook。

七、useEffect 中如何使用 async/await

  • 此时可以选择再包装一层 async 函数,置于 useEffect 的回调函数中,变相使用 async/await
    async function fetchMyAPI() {
      let res = await fetch('api/data')
      let response = await res.json()
      dataSet(response)
    }
    
    useEffect(() => {
      fetchMyAPI();
    }, []);
    

八、虚拟DOM

  • 虚拟DOM是什么? 模拟真实DOM树的JS对象
  • 为什么需要虚拟DOM? 为了性能优化,原因就是操作真实DOM是很耗费性能的,所以react建立的自己的规则与玩法
  • 作用是什么? 避免频繁的操作真实DOM,也避免了大规模渲染,能够很好地提高性能。
  • diff算法
    • React 通过制定大胆的 diff 策略,将 O(n3) 复杂度的问题转换成 O(n) 复杂度的问题;
    • React 通过分层求异的策略,对 tree diff 进行算法优化;
    • React 通过相同类生成相似树形结构,不同类生成不同树形结构的策略,对 component diff 进行算法优化;
    • React 通过设置唯一 key的策略,对 element diff 进行算法优化;
    • 建议,在开发组件时,保持稳定的 DOM 结构会有助于性能的提升;
    • 建议,在开发过程中,尽量减少类似将最后一个节点移动到列表首部的操作,当节点数量过大或更新操作过于频繁时,在一定程度上会影响 React 的渲染性能。
  • key的作用
    • key 的作用主要是用于判断元素是新创建的、要被移除的还是被修改的元素,从而减少不必要的 diff

九、React 16.8 更新了啥?

  • Hooks

十、Redux

  • Redux 是 JavaScript 状态容器,它提供可预测的状态管理。
  • Redux 工作流程:视图(View)数据来自 store,如果要对数据进行修改,就只能通过派发 action。action 会被 reducer 读取,进而会根据 action 的内容对数据进行修改,生成新的 state。这个新的 state 会更新到 store 中,进而驱动视图做出相应的改变。
  • Redux设计和使用的三大原则:
    • 单一的数据源:整个应用的 state 被储存在唯一一个 store中;
    • 状态是只读的:在任何时候都不能直接修改应用状态,只能通过发送一个 action,由这个 action 描述如何去修改 state;
    • 状态修改均由纯函数完成:在Redux中,通过纯函数 reducer 来确定状态的改变,因为 reducer 是纯函数,所以相同的输入,一定会得到相同的输出,同时也不支持异步;返回值是一个全新的state。
  • Redux 优点:
    • 结果的可预测性
    • 良好的可维护性
    • 服务端渲染
    • 强大的社区和生态系统
    • 易于测试
  • connect 的作用是连接 React 组件与 Redux store

十一、React 事件绑定原理

  • react 并没有使用原生的浏览器事件,而是在基于 Virtual DOM 的基础上实现了合成事件;采用小驼峰命名法;默认的事件传播方式是冒泡,如果想改为捕获的话,直接在事件名后面加上 Capture 即可;事件对象 event 也不是原生事件对象,而是合成对象,但通过 nativeEvent 属性可以访问原生事件对象。
  • react 合成事件主要分为以下三个过程:
    1. 事件注册
      • 在该阶段主要做了两件事:在 document 上注册、存储事件回调。所有事件都会注册到 document 上,拥有统一的回调函数 dispatchEvent 来执行事件分发,类似于document.addEventListener("click", dispatchEvent)。
    2. 事件合成
      • 事件触发后,会执行以下过程:
      1. 进入统一的事件分发函数 dispatchEvent;
      2. 找到触发事件的 ReactDOMComponent;
      3. 开始事件的合成:
        • 根据当前事件类型生成指定的合成对象
        • 封装原生事件和冒泡机制
        • 查找当前元素以及他所有父级
        • 在 listenerBank 根据 key 值查找事件回调并合成到 event(合成事件结束)
    3. 批处理
      • 批量处理合成事件内的回调函数

十二、React 中 refs 的作用是什么?

  • ref 是 React 提供的用来操纵 React 组件实例或 DOM 元素的接口。主要用来做文本框聚焦、触发强制动画等。

十三、Flux(Redux 和 Vuex 的设计思想)

  • Flux 的核心思想就是单向数据流。由三大部分组成:dispatcher(负责分发事件),store(负责保存数据,同时响应事件并更新数据)和 view(负责订阅 store 中的数据,并使用这些数据渲染相应的页面)。Redux 和 Vuex 是 Flux 思想的具体实现。

十四、性能优化

  • 类组件

  1. shouldComponentUpdate
  2. React.PureComponent
  3. immutable
  4. 尽量在 constructor 中用 bind 绑定来改变 this 指向:
    • 在 React 类组件中改变this 指向有三种方法:
      1. 在 constructor 中用 bind 绑定
      2. 在使用时用 bind 绑定
      3. 使用箭头函数
    • 第一种方法只在组件初始化时执行一次,第二种方法组件每次 render 都要重新绑定,第三种方法每次 render 都会生成新的箭头函数,因此要尽量在 constructor 中用 bind 绑定来改变 this 指向
  • 函数组件

  1. useCallback
  2. useMemo
  • 两者都可使用

  1. React.memo
  2. 在列表渲染时使用 key
  3. 不滥用 props:尽量只传需要的数据,避免多余的更新,尽量避免使用 {...props}

十五、React 复用组件和增强功能的方法

  • Render Props

    • 指一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术
  • 高阶组件

  • 自定义 hook

十六、prop drilling

  • 定义:从一个外部组件一层层将 prop 传递到内部组件
  • 缺点:使原本不需要数据的组件变得复杂且难以维护,代码也变得冗余、不优雅
  • 如何避免:使用 Context

十七、为什么类方法需要绑定到类组件实例?

  • 在 JS 中,this 指向会根据当前上下文变化。在 React 类组件方法中,开发人员通常希望 this 指向组件的当前实例,因此有必要将这些方法绑定到实例。

十八、JSX

  • 全称为 JavaScript XML,是一个 JavaScript 的语法扩展,它可以让我们在 js 代码中脱离字符串直接编写 html 代码。JSX 本身不能被浏览器读取,必须使用 babel 和 webpack 等工具将其转换为传统的 JS。
  • 优点:使代码结构更加清晰

十九、在构造函数中调用 super 并将 props 作为参数传入的作用是啥?

  • 在调用 super() 方法之前,子类构造函数无法使用 this 引用,在 react 的类组件中也是如此;将 props 参数传递给 super() 调用的主要原因是在子构造函数中能够通过 this.props 来获取传入的 props。

二十、纯函数

  • 纯函数:函数的返回结果只依赖于它的参数并且执行过程中没有副作用的函数。
  • 副作用:一个函数执行对外部的变量产生了影响和改变,那么就说这个函数是有副作用的。

二十一、为什么要废弃componentWillMountcomponentWillReceivePropscomponentWillUpdate这三个钩子函数?

  • 在 React 16的 Fiber 架构中,这些生命周期在进行一次更新时可能存在多次执行的情况,此时如果我们在里面使用 ref 操作 dom 的话,就会造成页面频繁重绘,影响性能。

二十二、函数组件中useStatesetstate与类组件中setState的异同点

  • 相同点:在 React 控制的事件中都是异步的,多次执行状态更新会进行合并,最终只执行一次,但此时要使用回调函数的方式来改变状态
  • 不同点:
    1. 在函数组件中改变状态,传入的值不会和原来的数据进行合并,而是直接替换,所以修改对象的时候需要将之前的对象保存下来,而在类组件中则不需要
    2. 类组件中可以通过给setState添加第二个参数来获取修改后的值,函数组件中则可以通过useEffect来监听值的变化

二十三、React 如何判断组件是函数组件还是类组件?

  • 如果[组件名].prototype.isReactComponent的值为{},那么该组件为类组件,否则就是函数组件。

二十四、为什么useState返回的是 array 而不是 object ?

  • 为了降低使用的复杂度。返回数组可以直接按顺序命名解构,如果返回对象的话则只能使用对象中的属性名,多次使用还需要设置别名。

二十五、React 的优点

  • 减少重绘次数(将多次数据操作汇集成一次DOM更新)
  • 减少手动操作DOM(不用再像以前写jQuery那样,先获取DOM元素,再设置属性)