万字文章带你了解什么是React,React有什么,如何使用React。(二)

Jsx的本质是什么

Jsx是语法糖,无法直接被浏览器解析,需要转换为js,通过babel 创建tagNode,createElement

// 自定义组件jsx代码
return (
	<div>
		<Input addTitle={this.addTitle.bind(this)} />
		<List data={this.state.list} />
	</div>
);

// 解析结果
return React.creatElement('div', null,
	React.createElement(Input, { addTitle: this.addTitle.bind(this) }),
	React.createElement(List, { data: this.state.list })
)

可以看到React.createElement传的第一个参数不是字符串形式的了,而是一个函数类型,其实就是构造函数。

解析得出:

  • div 直接渲染即可,vdom可以做到

  • Input和List,是自定义组件(class),vdom默认不认识

  • 因此Input和List定义的时候必须声明 render函数

  • 根据props初始化实例,然后执行实例的render函数

  • render函数返回的还是vnode对象 最后替换node,使用了React.createElement()方法进行的jsx转换

  • 初次渲染 - ReactDOM.render(, container) :会触发patch(container, vnode)

  • re-render - setState:会触发patch(vnode, newVnode)

  • 为何需要vdom: JSX需要渲染成html, 数据驱动视图

  • React.createElement和h,都生成vnode

  • 何时patch: ReactDOM.render(…)和setState

  • 自定义组件的解析:初始化实例,然后执行render

范式化

多数情况下我们的应用是要配合 Redux 或者 MobX 使用的。拿 Redux 举例,Redux store 的组织是一门大学问,Redux 官方推荐将 store 组织得扁平化和范式化,所谓扁平化,就是整个 store 的嵌套关系不要太深,实体之下不再挂载实体,扁平化带来的好处是:
当某些数据需要在不同的地方出现时,就会存在必然重复。例如,可能存在很多 state 部分都要存储同一份“用户评论列表”,这样需要花费很多心思去保障多处“用户评论列表”数据状态一致,否则就会造成页面数据不同步的 Bug;
嵌套深层的数据结构,会直接造成你 reducers 编写复杂。比如,你想更新一个很深层次的数据片段,很容易代码就变得丑陋。
造成负面的性能影响。即便你使用了类似 immutable.js 这样的不可变数据类库,最大限度的想保障深层数据带来的性能压力,那你是否知道 immutable.js 采用的 'Persistent data structure' 思路,更新节点会造成同一条链儿上的祖先节点的更新。更恐怖的是,也许这些都会关联到众多 React 组件的 re-render;
范式化是指尽量去除数据的冗余,因为这样会给维护数据的一致性带来困难,就像官方推荐 state 记录尽可能少的数据,不应该存放计算得到的数据和 props 的副本,而是将他们直接在 render 中使用,这也是避免了维护数据一致性的困难,并且避免了相同数据满天飞不知道源头数据是哪个的尴尬。

state VS store

首先要明确的是,不要将所有的状态全部放在store中,其实再延伸一下可以延伸出render() {}中的变量,也就是store VS state VS render,store中应该存放异步获取的数据或者多个组件需要访问的数据等等,redux官方文档中也有写什么数据应该放入store中。

  1. 应用中的其他部分需要用到这部分数据吗?
  2. 是否需要根据这部分原始数据创建衍生数据?
  3. 这部分相同的数据是否用于驱动多个组件?
  4. 是否需要能够将数据恢复到某个特定的时间点?
  5. 是否需要缓存数据?

store 中不应该保存 UI 的状态(除非符合上面的某一点,比如回退时页面的滚动位置)。UI 的状态应该被限定在 UI 的 state 中,随着组件的卸载而销毁。而 state 也应该用最少的数据表示尽可能多的信息。在 render 函数中,根据 state 去衍生其他的信息而不是将这样冗余的信息都存在 state 中。storestate 都应该尽可能的做到熵最小,具体的可以看 redux store取代react state合理吗?。而 render 中的变量应该尽可以去承担一个衍生数据的责任,这个过程是无副作用的,可以减少在 state 中产生冗余数据的情况。

React常见优化

1. 重新渲染 / 多组件优化:

存在维度的划分:层级,class组件和函数组件,节点多寡

  • 节点少:class : scu–>purecomponent
  • 节点少:函数:memo
  • 节点多:immutable+scu/memo

对于react的性能优化还是有必要的,不能因为父组件渲染而子组件没有变化也得跟着渲染从而产生不必要的花销。解决的原理就是通过props和state的浅对比来判断子组件是否渲染,具体操作是在class组件下通过生命周期的scu来判断,也可以通过继承purecomponent来减少每次重复写法;在函数组件下通过memo来判断;在大量节点的情况下就考虑用immutable配合了。

浅对比:对象的对比他们的内存地址,只要内存地址一致,就不重新渲染,反之,对象的内存地址不一致,就渲染
immutable:每次操作都会产生一个新的对象出来,由于它会复用之前数据的数据结构,所以产生新的数据也很快,ImmutableJS提供了不可变的数据,即要让数据改变只能通过创建新数据的方式,而不能直接修改,这很大程度的降低了前后两个数据比较时的复杂度。

2. 传参优化

切记将props/state以展开形式传递给子组件,除非子组件需要用到父组件的所有props/state

3. Key

对于数组形式的数据,遍历时React会要求你为每一个数据值添加Key,而Key必须时独一无二的,在选取Key值时尽量不要用索引号,因为如果当数据的添加方式不是顺序添加,而是以其他方式(逆序,随机等),会导致每一次添加数据,每一个数据值的索引号都不一样,这就导致了Key的变化,而当Key变化时,React就会认为这与之前的数据值不相同,会多次执行渲染,会造成大量的性能浪费。所以只在万不得已时,才将数据的索引号当做Key

4. 按需加载

使用React.LazyReact.Suspense做分片打包,实现组件的按需加载,大大提高页面速度

5. 调整CSS而不是强制组件加载和卸载

尽量的减少组件的创建和销毁,这样对于性能还是有一定的损耗的,我们可以将组件隐藏掉,比如加hidden属性,控制cssdisplayopacityvisibility等等隐式隐藏的方法。

diff算法的key值是如何比较的

* MOVE_EXISTING -- 存在相同的节点则复用以前的 DOM 节点,做移动操作。
*  INSERT_MARKUP -- 新的节点不在旧集合里则插入新的节点。
* REMOVE_NODE -- 新集合里在旧集合中对应的 node 不同,不能直接复用和更新,需要执行删除操作,或者旧集合中的节点不在新集合里的。
*  遍历 newChildrens,基于 key 判断 newChild 是否在 oldChildrens 存在相同的节点。 如果存在相同节点(prevChild === nextChild) ,先判断原先节点的变化顺序(不考虑头部新插入的节点),节点的挂载顺序变大(从前往后),移动节点 。节点的挂载顺序变小(从后往前或不变),不做操作 。节点的 _mountIndex 变为新集合中的 index 。如果不存在相同节点,之前存在相同,在上一个新集合中的节点后插入新节点
*  遍历 oldChildrens,移除在新集合中不存在的节点

React的渲染机制

React 在内部维护了一套虚拟 DOM(VDOM),在内部维护着一颗 VDOM 树,这颗 VDOM 树映射到浏览器真实的 DOM 树,React 通过更新 VDOM 树来对真实 DOM 更新,VDOMplain object 所以很明显操作 VDOM 的开销要比操作真实 DOM 快得多,再加上 React 内部的 reconciler(调节器,这个模块用于发起顶层组件或者子组件的挂载渲染重绘),React 会在 reconsilation (重新编译)之后最小化的进行 VDOM 的更新,再 patch (修复)到真实 DOM 上最终完成用户看得到的更新。

React的错误处理机制

错误边界介绍

部分 UI 中的 JavaScript 错误不应该破坏整个应用程序。 为了解决 React 用户的这个问题,React 16引入了一个 “错误边界(Error Boundaries)” 的新概念。
错误边界是 React 组件,它可以在子组件树的任何位置捕获 JavaScript 错误,记录这些错误,并显示一个备用 UI ,而不是使整个组件树崩溃。 错误边界(Error Boundaries) 在渲染,生命周期方法以及整个组件树下的构造函数中捕获错误。

使用方法

如果一个类组件定义了生命周期方法中的任何一个(或两个)static getDerivedStateFromError() 或 componentDidCatch(),那么它就成了一个错误边界。 使用static getDerivedStateFromError()在抛出错误后渲染回退UI。 使用 componentDidCatch() 来记录错误信息。

捕获范围

组件内异常主要包括:

  1. 渲染过程中异常;
  2. 生命周期方法中的异常;
  3. 子组件树中各组件的constructor构造函数中的异常;
  4. 事件处理器中的异常;(使用try/catch进行捕获)
  5. 异步任务的异常,如setTimeout,ajax请求异常等;(使用全局事件window.addEventListener捕获)
  6. 服务端渲染异常;
  7. 异常边界组件自身内的异常;(将边界组件和业务组件分离,各司其职,不能在边界组件中处理逻辑代码,也不能在业务组件中使用didcatch
    错误边界尽可以捕获其子组件的错误,无法捕获其自身的错误;如果一个错误边界无法渲染错误信息,则错误会向上冒泡至最接近的错误边界。这也类似于 JavaScript 中 catch {} 的工作机制)

如何放置错误边界

错误边界的粒度完全取决于你的应用。你可以将其包装在最顶层的路由组件并为用户展示一个 “发生异常(Something went wrong)“的错误信息,就像服务端框架通常处理崩溃一样。你也可以将单独的插件包装在错误边界内部以保护应用不受该组件崩溃的影响。
借鉴Facebook的message项目,他们应用错误边界的方式是将大的模块应用错误边界包裹,这样当一个主要模块因为意外的错误崩溃后,其它组件仍然能够正常交互

错误边界示例

// 首先我定义了一个高阶组件
import React from 'react'
const ErrorBoundary = errorInfo => WrapComponent => {
    
    
  return class ErrorBoundary extends React.Component {
    
    
    constructor(props) {
    
    
      super(props);
      this.state = {
    
     hasError: false };
    }
    // 这个静态方法和componentDidCatch方法定义一个即可
    static getDerivedStateFromError(error) {
    
    
      // 当发生错误时,设置hasError为true,然后展示自己的错误提示组件
      return {
    
     hasError: true };
    }
    componentDidCatch(error, info) {
    
    
      // 这里可以将报错信息上报给自己的服务
      // logErrorToMyService(error, info);
    }
    render() {
    
    
      if (this.state.hasError) {
    
    
        return {
    
     errorInfo }
          ;
      }
      return;
    }
  }
} export default ErrorBoundary
// 接下来可以使用边界组件包裹业务组件,这里列举我认为react项目中可以处理的错误方式,例如事件处理器的错误,异步错误,promise错误,渲染错误等
import React from 'react'
import ErrorBoundary from '../../utils/ErrorBoundary'
@ErrorBoundary('i am not ok')
export default class Error extends React.Component {
    
    
  constructor() {
    
    
    super()
  }
  componentWillMount() {
    
    
    window.addEventListener('error', event => {
    
    
      console.log(event)
    }, true)
    window.addEventListener('unhandledrejection', event => {
    
    
      console.log(event)
    })
  }
  // 这个异步错误 ErrorBoundary组件不会捕获到 但是在入口写的全局window.onerror事件捕获到了
  componentDidMount() {
    
    
    setTimeout(() => {
    
    
      // console.log(b)
    }, 100)
  }
  // 事件处理器中的错误 onerror也可以捕获到
  // 这里如果想要hold住错误 需要使用try catch
  handleEventError = () => {
    
    
    console.log(error)
  }
  // promise 如果reject 但是没有写catch语句的话 会报错
  // 但是onerror和try-catch和ErrorBoundary组件都无法捕获
  // 需要写一个全局unhandledrejection 事件捕获
  handlePromiseError = () => {
    
    
    const promise = new Promise((resolve, reject) => {
    
    
      reject()
    })
    promise.then()
  }
  render() {
    
    
    return
  } }

受控组件和非受控组件

受控以及非受控组件的边界划分取决于当前组件对于子组件值的变更是否拥有控制权。如若有则该子组件是当前组件的受控组件; 如若没有则该子组件是当前组件的非受控组件。
受控组件: 在HTML的表单元素中,它们通常自己维护一套state,并随着用户的输入自己进行UI上的更新,这种行为是不被我们程序所管控的。而如果将React里的state属性和表单元素的值建立依赖关系,再通过onChange事件与setState()结合更新state属性,就能达到控制用户输入过程中表单发生的操作。被React以这种方式控制取值的表单输入元素就叫做受控组件。
非受控组件:要编写一个非受控组件,而不是为每个状态更新都编写数据处理函数,你可以使用ref来从DOM 节点中获取表单数据。

React Patch

React构建虚拟标签,执行组件的生命周期,更新state,计算diff等,这一系列的操作都是在virtualDOM中执行的,此时浏览器并未显示出更新的数据。React Patch实现了最后这关键的一步,将tree diff算法计算出来的差异队列更新到真实的DOM节点上,最终让浏览器能够渲染出更新的数据。
Patch主要是通过遍历差异队列实现的,遍历差异队列时,通过更新类型进行相应的插入、移动和移除等操作。
React并不是计算出一个差异就执行一次patch,而是计算出全部的差异并放入差异队列后,再一次性的去执行Patch方法完成真实的DOM更新。

React Reconciliation

当你使用 React ,在任何一个单点时刻你可以认为 render() 函数的作用是创建 React 元素树。在下一个 stateprops 更新时,render() 函数将会返回一个不同的 React 元素树。接下来 React 将会找出如何高效地更新 UI 来匹配最近时刻的 React 元素树。
目前存在大量通用的方法能够以最少的操作步骤将一个树转化成另外一棵树。然而,这个算法是复杂度为O(n3),其中n 为树中元素的个数。
如果你在 React 中展示 1000 个元素,那么每次更新都需要10亿次的比较,这样的代价过于昂贵。然而,React 基于以下两个假设实现了时间复杂度为 O(n) 的算法:

1. 不同类型的两个元素将会产生不同的树。
2. 开发人员可以使用一个 key prop 来指示在不同的渲染中那个那些元素可以保持稳定。

处理过程:
当执行 setState() 或首次 render() 时,进入工作循环,循环体中处理的单元为 Fiber Node, 即是拆分任务的最小单位,从根节点开始,自顶向下逐节点构造 workInProgress tree(构建中的新 Fiber Tree

Fiber 之前架构卡顿的原因

React 中调用 render()setState() 方法进行渲染和更新时,主要包含两个阶段:

  1. 调度阶段(Reconciler): Fiber 之前的 reconciler(被称为 Stack reconciler)是自顶向下的递归算法,遍历新数据生成新的Virtual DOM,通过 Diff 算法,找出需要更新的元素,放到更新队列中去。
  2. 渲染阶段(Renderer): 根据所在的渲染环境,遍历更新队列,调用渲染宿主环境的 API, 将对应元素更新渲染。在浏览器中,就是更新对应的DOM元素,除浏览器外,渲染环境还可以是 NativeWebGL 等等。
    Fiber 之前的调度策略 Stack Reconciler,这个策略像函数调用栈一样,递归遍历所有的 Virtual DOM 节点,进行 Diff,一旦开始无法中断,要等整棵 Virtual DOM 树计算完成之后,才将任务出栈释放主线程。而浏览器中的渲染引擎是单线程的,除了网络操作,几乎所有的操作都在这个单线程中执行,此时如果主线程上用户交互、动画等周期性任务无法立即得到处理,影响体验。

React事件绑定原理

react中的事件都是合成事件,不是把每一个dom的事件绑定在dom上,而是把事件统一绑定到document中,触发时通过事件冒泡到document进行触发合成事件,因为是合成事件,所以我们无法去使用e.stopPropagation去阻止,而是使用e.preventDefault去阻止。

  1. 事件注册:组件更新或者装载时,在给dom增加合成事件时,需要将增加的target传入到document进行判断,给document注册原生事件回调为dispatchEvent(统一的事件分发机制)。
  2. 事件存储EventPluginHub负责管理React合成事件的callback,它将callback存储到listennerBank中,另外还存储了负责合成事件的PluginEvent存储到listennerbank中,每一个元素在listennerBank中会有唯一的key
  3. 事件触发执行:点击时冒泡到docunment中,触发注册原生事件的回调dispatchEvent,获取到触发这个事件的最深层元素,事件执行利用react的批处理机制。
<div onClick={
    
    this.parentClick} ref={
    
    ref => this.parent = ref}>
  <div onClick={
    
    this.childClick} ref={
    
    ref => this.child = ref}>
    button
  </div>
</div>

点击button后

  • 首先获取到this.child
  • 遍历此元素的所有父元素,依次对每一级元素进行处理
  • 构成合成事件
  • 将每一级的合成事件存储在eventQueen事件队列中
  • 遍历,是否组织冒泡,是则停止,否则继续
  • 释放已经完成的事件
  1. 合成事件:循环所有类型的eventPlugin,对应每个事件类型,生成不同的事件池,如果是空,则生成新的,有则用之前的,根据唯一key获取到指定的回调函数,再返回带有参数的回调函数。
  2. 流程:组件装载/更新 – 新增/删除事件 – eventplugin添加到ListennerBank中监听事件 – 触发事件 – 生成合成事件 – 通过唯一key获取到指定函数 – 执行指定回调函数 – 执行完毕后释放

React Route懒加载

React利用 React.lazyimport() 实现了渲染时的动态加载 ,并利用 Suspense
来处理异步加载资源时页面显示的问题

React.lazy 原理

对于最初 React.lazy() 所返回的 LazyComponent 对象,其 _status 默认是 -1,所以 首次渲染 时,会进入 readLazyComponentType 函数中的 default 的逻辑,这里才会真正异步执行 import(url) 操作,由于并未等待,随后会检查模块是否 Resolved,如果已经Resolved了(已经加载完毕)则直接返回 moduleObject.default (动态加载的模块的默认导出),否则将通过 throwthenable 抛出到上层。

import()原理

function import(url) {
    
    
  return new Promise((resolve, reject) => {
    
    
    const script = document.createElement("script");
    const tempGlobal = "__tempModuleLoadingVariable" + Math.random().toString(32).substring(2);
    script.type = "module";
    script.textContent = `import * as m from "${
      
      url}"; window.${
      
      tempGlobal} = m;`;
    script.onload = () => {
    
    
      resolve(window[tempGlobal]);
      delete window[tempGlobal];
      script.remove();
    };
    script.onerror = () => {
    
    
      reject(new Error("Failed to load module script with URL " + url));
      delete window[tempGlobal];
      script.remove();
    };
    document.documentElement.appendChild(script);
  }); }

React.Suspense原理

React 捕获到异常之后,会判断异常是不是一个 thenable,如果是则会找到 SuspenseComponent ,如果 thenable 处于 pending 状态,则会将其 children 都渲染成 fallback 的值,一旦 thenableresolveSuspenseComponent 的子组件会重新渲染一次。

Hooks

useCallback和useDemo

useMemo 和 useCallback 接收的参数都是一样,第一个参数为回调 第二个参数为要依赖的数据

两者区别:

  1. useMemo 计算结果是 return 回来的值, 主要用于缓存计算结果的值 ,应用场景大多是需要计算的状态
  2. useCallback 计算结果是函数, 主要用于缓存函数,应用场景如:需要缓存的函数,因为函数式组件每次任何一个 state 的变化整个组件都会被重新刷新,一些函数是没有必要被重新刷新的,此时就应该缓存起来,提高性能,和减少资源浪费。

注意: 不要滥用,会造成性能浪费。react中减少render就能提高性能,所以这个仅仅只针对缓存能减少重复渲染时使用和缓存计算结果。

const memoizedCallback = useCallback(
  () => {
    
    
    doSomething(a, b);
  },
  [a, b],
);
// 根据官网文档的介绍我们可理解:在a和b的变量值不变的情况下,memoizedCallback的引用不变。即:useCallback的第一个入参函数会被缓存,从而达到渲染性能优化的目的。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
// 根据官方文档的介绍我们可理解:在a和b的变量值不变的情况下,memoizedValue的值不变。即:useMemo函数的第一个入参函数不会被执行,从而达到节省计算量的目的。

hooks带来了什么

  • 用于在函数组件中引入状态管理和生命周期方法
  • 取代高阶组件和render props来实现抽象和可重用性

hooks的实现原理

核心是将useStateuseEffect按照调用的顺序放入memoizedState中,每次更新时,按照顺序进行取值和判断逻辑,我们根据调用hook顺序,将hook依次存入数组memoizedState中,每次存入时都是将当前的currentcursor作为数组的下标,将其传入的值作为数组的值,然后在累加currentcursor,所以hook的状态值都被存入数组中memoizedState
先将旧数组memoizedState中对应的值取出来重新复值,从而生成新数组memoizedState。对于是否执行useEffect通过判断其第二个参数是否发生变化而决定的。
这里我们就知道了为啥不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层调用他们。因为我们是根据调用hook的顺序依次将值存入数组中,如果在判断逻辑循环嵌套中,就有可能导致更新时不能获取到对应的值,从而导致取值混乱。同时useEffect第二个参数是数组,也是因为它就是以数组的形式存入的。

hooks闭包

useEffectuseMemouseCallback都是自带闭包的。每一次组件的渲染,它们都会捕获当前组件函数上下文中的状态(state, props),所以每一次这三种hooks的执行,反映的也都是当前的状态,你无法使用它们来捕获上一次的状态。

hooks体验

在日常业务开发中,hooks带来的收益是明显大于使用成本的。

  1. hooksTypeScript 支持更加友好。例如 useState 对应的状态类型可以被自动推断出来,useEffect 不需要写类型声明(class 写法下几个生命周期函数的参数签名写起来是相当繁琐的),hooks 能帮我们省去大量手动声明类型的操作。同时 hooks 让重构更加简单,使用 hooks 的代码中不会出现 this,组件的 props 或是 state 在组件代码中都是普通的变量,减少了重构的成本,例如原先 重命名 this.state 中某个字段 ,在 hooks 下就成了重命名某个变量。
  2. hooks 不再要求状态必须在 this.state 中声明,生命周期写在componentDidMount/componentDidUpdate 中。useState/useEffect 的写法更加自由,我们可以按照页面功能/特性来组织 hooks 的书写位置。
  3. 自定义 hooks 提供了更加灵活的逻辑复用机制。自定义 hooks 带来了更多的可能性,当组件/页面变得更为复杂时,hooks 会比 class 写法带来更大的心智负担的,stale closure、过多的 re-renderuseEffect 依赖膨胀、缺少 getDerivedStateFromProps …… ,这个时候就不能像原来那样自由发挥 hooks 了,还是老老实实用回 class 写法了。
  4. hookuseEffect 本质上就是为了模糊生命周期以及渲染周期的概念的,只要 render 函数中不存在副作用、消耗较小(适当的 memo),多渲染几次其实没什么问题的。这里唯一值得注意的是,render 中千万要小心副作用,如果不用 useEffect 基本上都会产生问题。

useEffect和useLayoutEffect的区别

useEffect里面的操作需要处理DOM,并且会改变页面的样式,就需要用这个,否则可能会出现出现闪屏问题, useLayoutEffect里面的callback函数会在DOM更新完成后立即执行,但是会在浏览器进行任何绘制之前运行完成,阻塞了浏览器的绘制。
执行时机:

  • useLayoutEffect和平常写的ClassComponentcomponentDidMountcomponentDidUpdate同时执行。
  • useEffect会在本次更新完成后,也就是第1点的方法执行完成后,在开启一次任务调度,在下次任务调度中执行useEffect

hooks模拟全部生命周期

// constructor
function Example() {
    
    
  const [count, setCount] = useState(0);
  return null;
}
//getDerivedStateFromProps
// 这里注意到其实这个state并不是真实的state,而是一个跟props相关的对象
const useGetDeriveStateFromProps = (state, props, handle) => {
    
    
  const cacheState = useRef(state);
  const newState = handle(cacheState.current, props);
  if (newState) {
    
    
    cacheState.current = newState;
  }

  return cacheState.current;
};

// 使用
const Component = props => {
    
    
  const state = useGetDeriveStateFromProps({
    
     x: 1 }, props, (state, props) => {
    
    
    console.log('new getDerivedStateFromProps')
    if (props.add) {
    
    
      state.x += 1;
      return state;
    }
    return null;
  });
  return <div>{
    
    state.x}</div>;
};

// componentDidMount
function Example() {
    
    
  useEffect(() => console.log('mounted'), [])
}
// shouldComponentUpdate
// React.memo 包裹一个组件来对它的 props 进行浅比较,
// 但这不是一个 hooks,因为它的写法和 hooks 不同,
// 其实React.memo 等效于 PureComponent,但它只比较 props。
const MyComponent = React.memo(_MyComponent, (prevProps, nextProps) => nextProps.count !== prevProps.count)

// getSnapshotBeforeUpdate
// 在最近一次渲染输出(提交到 DOM 节点)之前调用。 它使得组件能在发生更改之前从 DOM 中捕获一些信息(例如,滚动位置)。 此生命周期的任何返回值将作为参数传递给 componentDidUpdate
// 这里有点难解决,可以考虑讲整个生命周期抽象出来

// componentDidUpdate
useEffect(() => console.log('mounted or updated'));
// 值得注意的是,这里的回调函数会在每次渲染后调用,因此不仅可以访问 componentDidUpdate,还可以访问componentDidMount,如果只想模拟 componentDidUpdate,我们可以这样来实现。
const mounted = useRef();
useEffect(() => {
    
    
  if (!mounted.current) {
    
    
    mounted.current = true;
  } else {
    
    
    console.log('I am didUpdate')
  }
});
// useRef 在组件中创建“实例变量”。它作为一个标志来指示组件是否处于挂载或更新阶段。当组件更新完成后在会执行 else 里面的内容,以此来单独模拟 componentDidUpdate。
// componentWillUnmount
useEffect(() => {
    
    
  return () => {
    
    
    console.log('will unmount');
  }
}, []);

useReducer

const [state, dispatch] = useReducer(reducer, initState);
// useReducer接收两个参数:
// 第一个参数:reducer函数。
// 第二个参数:初始化的state。返回值为最新的state和dispatch函数(用来触发reducer函数,计算对应的state)。按照官方的说法:对于复杂的state操作逻辑,嵌套的state的对象,推荐使用useReducer。听起来比较抽象,我们先看一个简单的例子:

// 官方 useReducer Demo
// 第一个参数:应用的初始化
const initialState = {
    
     count: 0 };

// 第二个参数:state的reducer处理函数
function reducer(state, action) {
    
    
  switch (action.type) {
    
    
    case 'increment':
      return {
    
     count: state.count + 1 };
    case 'decrement':
      return {
    
     count: state.count - 1 };
    default:
      throw new Error();
  }
}

function Counter() {
    
    
  // 返回值:最新的state和dispatch函数
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
 {
    
    /* useReducer会根据dispatch的action,返回最终的state,并触发rerender */}
      Count: {
    
    state.count}
 {
    
    /* dispatch 用来接收一个 action参数「reducer中的action」,用来触发reducer函数,更新最新的状态 */}
      <button onClick={
    
    () => dispatch({
    
     type: 'increment' })}>+</button>
      <button onClick={
    
    () => dispatch({
    
     type: 'decrement' })}>-</button>
    </>
  ); }

使用reducer的场景

  • 如果你的state是一个数组或者对象
  • 如果你的state变化很复杂,经常一个操作需要修改很多state
  • 如果你希望构建自动化测试用例来保证程序的稳定性
  • 如果你需要在深层子组件里面去修改一些状态
  • 如果你用应用程序比较大,希望UI和业务能够分开维护

useRef

1. 原理

const useRef = (initialValue) => {
    
    
  const [ref] = useState({
    
     current: initialValue });
  return ref
}

2. 使用

  • 每次渲染useRef返回值都不变;
  • ref.current发生变化并不会造成re-render;
  • ref.current发生变化应该作为Side Effect(因为它会影响下次渲染),所以不应该在render阶段更新current属性;
  • 不可以在render里更新ref.current值,在异步渲染里render阶段可能会多次执行。
  • 可以在render里更新ref.current值,只要保证每次render不会造成意外效果,都可以在render阶段更新ref.current。但最好别这样,容易造成问题,useRef懒初始化毕竟是个特殊的例外;
  • ref.current 不可以作为其他hooksuseMemouseCallbackuseEffect)依赖项;
  • ref作为其他hooksuseMemouseCallbackuseEffect)依赖项;

3. 动机

  • 函数组件访问DOM元素;
  • 函数组件访问之前渲染变量。
    函数组件每次渲染都会被执行,函数内部的局部变量一般会重新创建,利用useRef可以访问上次渲染的变量,类似类组件的实例变量效果。

猜你喜欢

转载自blog.csdn.net/zw7518/article/details/125250541