offer收割之React篇

991 阅读52分钟

react原理

首先,我对 React 的核心原理有着较为深入的理解。

React 的工作基于虚拟 DOM(Virtual DOM)的概念虚拟 DOM 是一个轻量级的 JavaScript 对象树,它代表了真实 DOM 的结构和属性。当组件的状态或属性发生变化时,React 会创建一个新的虚拟 DOM 树,并通过高效的算法(如 Diff 算法)与旧的虚拟 DOM 树进行比较,只对发生变化的部分进行实际 DOM 的更新操作,从而提高了页面的渲染性能。

在组件的生命周期方面,React 提供了一系列的钩子函数,如 componentDidMountcomponentDidUpdatecomponentWillUnmount 等,让我们能够在不同的阶段执行特定的逻辑,例如数据获取、副作用处理和资源清理等。

对于状态管理,React 有自身的状态机制,通过 useState 钩子或类组件中的 this.state 来管理组件内部的状态。而对于复杂的应用,可能会结合 Redux 或 MobX 等状态管理库来实现全局的状态共享和管理。

另外,React 的渲染过程是基于单向数据流的,数据从父组件向子组件传递通过属性(props),子组件通过回调函数将数据传递回父组件。这种单向的数据流动方式使得组件之间的关系更加清晰,易于理解和维护。

总的来说,通过对这些原理的研究和理解,我能够更好地开发高效、可维护的 React 应用,并在遇到问题时能够快速定位和解决。

对react19的理解

1.react19新增了api use,主要是可以在组件渲染函数(render)执行时进行数据获取,19之前数据都是在useEffect中异步获取的,19中直接用use(获取对象的promise)

2.forwardre被舍弃,ref可以直接在组件的props中传递获取(以前是不行的)

3.React Compiler React Compiler 是 React 团队打造的一款编译器,在 Compiler 中一切的数据都会被 memoized,就比如19之前为了子组件不重复渲染使用useMemo去优化,19以后会自动添加

react源码的结构

React 源码的结构主要包含以下几个核心部分:

  • packages 目录:这是 React 各个模块的存放位置。

    • react:包含了 React 核心的 API 和组件相关的逻辑。
    • react-dom:处理与 DOM 操作和渲染相关的功能。
    • react-reconciler:协调器模块,负责协调更新和渲染过程。
    • scheduler:调度模块,用于任务的优先级排序和调度。^^ [来源:对 React 源码结构的常见分析]
  • react/packages/react/src 目录:包含了 React 核心实现的细节。

    • ReactBaseClasses.js:定义了 React 组件的基础类。
    • ReactElement.js:处理 React 元素的创建和操作。
    • ReactChildren.js:与子元素的处理相关。
    • ReactUpdates.js:管理组件的更新过程。^

1、react中key的作用?

Keys是React用于追踪列表中哪些元素被修改、被添加或者被移除的辅助标识。最好是列表中独一无二的字符,如果不显式指定key,react默认用列表元素的索引做为key。

在一个列表组件中,key只需要在兄弟组件中保持唯一,并不需要全局唯一,当我们生成两个不同的数组时,我们可以使用相同的 key 值

为啥不推荐使用数组index作为key?

假如一个列表项abcd key分别为0 1 2 3然后删除第一项a,这样在diffi新旧dom时发现bcd的key由123变为了012也就是都变了 所以要全部渲染一次,然而如果key唯一的话,只需要渲染改变的哪一项。

react如何避免父组件渲染,子组件不必要的重新渲染

类组件情况: 错误示例:

import React, { Component } from "react";
class Parent extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
    };
  }
  handleClick = () => {
    const { count } = this.state;
    this.setState({
      count: count + 1,
    });
  };
  render() {
    const { count } = this.state;
    return (
      <div className="parent">
        <h5>错误示例</h5>
        <p>父组件Count--{count}</p>
        <button onClick={this.handleClick}>增加</button>
        <Son />
      </div>
    );
  }
}

class Son extends Component {
  constructor(props) {
    super(props);
  }
  render() {
    console.log("子组件重新渲染了!!");
    return <div className="son">子组件</div>;
  }
}

export { Parent, Son };

错误示例中子组件直接放在了父组件return里面导致父组件重新渲染,子组件也会跟着渲染 1.子组件中通过借用了 PureComponent 继承这个类,React会自动帮我们执行 shouldComponentUpdate 对 Props 进行浅比较优化更新,只做浅层比较,如果对象复杂就不适合

class Son extends PureComponent

在React中组件会被 React.createElement(Son) 执行,所得到的组件的Props引用每次都是新的,因此会引发重新渲染

2.子组件中通过shouldComponentUpdate(nextProps, nextState),可以将 this.props 与 nextProps 以及 this.state 与nextState 进行比较,并返回 false 以告知 React 可以跳过更新。请注意,返回 false 并不会阻止子组件在 state 更改时重新渲染

3.通过children

import React, { Component } from "react";
class Parent extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
    };
  }
  handleClick = () => {
    const { count } = this.state;
    this.setState({
      count: count + 1,
    });
  };
  render() {
    const { count } = this.state;
    const { children } = this.props;
    return (
      <div className="parent">
        <h5>正确示例2</h5>
        <p>父组件Count--{count}</p>
        <button onClick={this.handleClick}>增加</button>
        {children}
      </div>
    );
  }
}

export default Parent;

// 把子组件作为父组件的children 以组件嵌套的方式,然后在父组件render中使用this.props.children获取
<Parent>
  <Son />
</Parent>

因为直接在状态组件中使用children直接渲染子组件可以避免在组件中React使用React.createElement(Son)  渲染子组件!!这样也可以做到优化!

HOOKS情况:

与Class组件相比,Function组件 的特性是每一次的组件重新渲染,都会重新执行一次函数

错误示例:

import { useState } from "react";
const Son = () => {
  console.log("子组件重新渲染了!!");
  return <div className="son">子组件</div>;
};

const Parent = () => {
  const [count, setCount] = useState(0);
  const handleClick = () => {
    setCount((old) => old + 1);
  };
  return (
    <div className="parent">
      <h5>错误示例</h5>
      <p>父组件Count--{count}</p>
      <button onClick={handleClick}>增加</button>
      <Son />
    </div>
  );
};

export { Son, Parent };

1.hooks也可以使用children优化重新渲染

import { useState } from "react";

const Parent = ({ children }) => {
  const [count, setCount] = useState(0);
  const handleClick = () => {
    setCount((old) => old + 1);
  };
  return (
    <div className="parent">
      <h5>正确示例1</h5>
      <p>父组件Count--{count}</p>
      <button onClick={handleClick}>增加</button>
      {children}
    </div>
  );
};

export default Parent;

<Parent>
  <Son />
</Parent

说明: 认真的讲,结合函数组件的特性这个优化手段其实是治标不治本的

2.使用useMemo包裹子组件放在父组件的return里面(推荐)

import { useState, useMemo } from "react";
import { Son } from "./Bad";
const Parent = () => {
  const [count, setCount] = useState(0);
  const handleClick = () => {
    setCount((old) => old + 1);
  };
  return (
    <div className="parent">
      <h5>正确示例2</h5>
      <p>父组件Count--{count}</p>
      <button onClick={handleClick}>增加</button>
      // 子组件直接用useMemo 依赖项是props 
      {useMemo(
        () => (
          <Son />
        ),
        []
      )}
    </div>
  );
};

export default Parent;

使用了useMemo 这个优化Hook,我们将 Son 组件进行缓存,只有当依赖改变,我们再去重新执行函数完成重新渲染,其他时机保证memoized相同,这样有助于避免在每次渲染时都进行高开销的计算。也避免了 每次在子组件中 都要重新声明变量,函数,作用域

因为 useMemo 保存了组件的引用,没有重新执行函数组件,因此避免了组件内的变量,函数声明,和作用域的声明。从而优化了性能

3.使用React.memo优化函数组件

import { useState, memo } from "react";
import { Son } from "./Bad";

const SonMemo = memo(Son);

const Parent = () => {
  const [count, setCount] = useState(0);
  const handleClick = () => {
    setCount((old) => old + 1);
  };
  return (
    <div className="parent">
      <h5>正确示例3</h5>
      <p>父组件Count--{count}</p>
      <button onClick={handleClick}>增加</button>
      <SonMemo />
    </div>
  );
};

export default Parent;

使用React.memo(MyComponent,比较函数)包裹子组件它默认会对props进行浅层比较如果props相等则不渲染,也可以使用第二个参数自定义比较函数

实现ahooks中的usePrevious

这个hook的作用是可以保存上一次渲染时的状态。他传入一个需要记录变化的值,返回他的上一轮的状态,记录的值初始为undefined

思路:

  • 要保存上一轮的状态,我们就需要一个存状态的容器。React官方提供的useRef就是一个很好的选择;

  • 使用useRef的current属性保存上一轮的值,并配合useEffect一起使用,当数据发生变化时,更新current的值;

  • 将current值返回;

实现如下:

function usePrevious(value) {
  const ref = useRef();
  useEffect(()=>{
    ref.current = value;
  }, [value]);
  return ref.current;
}

如何实现虚拟列表

虚拟列表即只渲染可视区域的数据,使得在列表数据庞大的情况下,只显示可视区域的数据,顶部和底部不可见的区域以一个空的dom来代替(留白),这样就能大量减少dom的渲染量,使得列表能够流畅地无限滚动,这在移动端是十分重要的。

对于PC端 一般是通过分页来解决数据量大的情况

react和vue的异同

相同点:

  • 都使用虚拟dom
  • 都是创建UI的js库 不同点:
  • react写法是jsx,vue是模板写法(js css html在一个文件里分开写)
  • vue约定更多,比如v-if指令等,react相对自由灵活
  • react是函数式不可变数据,单向数据流,vue是双向绑定,声明式写法

react为啥需要不可变数据?

1.首先state如果是一个引用嵌套很深的对象,每次改变state的属性,react需要遍历去比较state判断是否需要重新渲染,浪费性能。 2.污染数据,造成一些奇怪的bug

解决办法:1.immer.js 每次都改变都新建一个新数据 2.使用扩展运算符新增对象

React 组件之间通信(数据传递)的几种方式

在我们使用 React 中,不可避免的要进行组件之间的通信(数据传递)。

需要注意一点:React中数据的流动是单向的

组件之间的数据传递大致分为一下几种:

  • 父组件向子组件传递数据
  • 子组件向父组件传递数据
  • 跨级组件之间传递数据(例如父组件向子子组件传递数据(可以使用context))
  • 非嵌套组件之间传递数据(一般引入状态库 redux)

下面我们使用两种方法 (props和context) ,分别写段代码来实现上面四种通信的方式

父传子:通过props传 子传父:通过在父组件定义一个回调方法,回调里面setState ,然后通过props传递这个方法给子组件,然后子组件触发这个回调 并且把数据作为参数,这样就实现了 子传父 并把这个数据更新到了父state中

如果是大型项目的话,最好使用Redux来管理我们的状态

对context的理解

简单说就是,当你不想在组件树中通过逐层传递props或者state的方式来传递数据时,可以使用Context来实现跨层级的组件数据传递

受控组件和非受控组件?

受控组件:比如input组件的value与state绑定,onchange也是通过改变state,这就是受控组件 缺点:需要大量state维护 非受控组件:不通过state绑定,可以通过ref获取dom来获取组件的值

为什么 useState 要使用数组而不是对象?

// 第一次使用
const { state, setState } = useState(false);
// 第二次使用
const { state: counter, setState: setCounter } = useState(0) 
使用数组直接可以随便命名,使用对象必须第一个名字与返回的key相同,多次使用还要起别名

总结:useState 返回的是 array 而不是 object 的原因就是为了降低使用的复杂度,返回数组的话可以直接根据顺序解构,而返回对象的话要想使用多次就需要定义别名了。

讲讲React Hooks的闭包陷阱,你是怎么解决的?

React Hooks 存在“闭包渲染”的问题,每次 render 都会闭包缓存当前render对应的 state,比如 一个setTimeout回调函数中引用了state(2s以后输出state),当2s之内组件重新渲染多次,但是第一次回调函数只记住(闭包陷阱)那个state只会输出第一次的state 无法输出最新值。

解决闭包陷阱的方案:

  • 使用 useRef 解决闭包陷阱的问题,定义一个ref,初值指定为state,然后在useEffect里面对ref.current进行赋值,这就解决了每次重新渲染都返回一个新的独立的state的问题,因为ref在组件每次rerender返回同一个引用类型的对象(state每次重新渲染都是返回一个独立的,所以函数只能获取那一次渲染的state,而ref一直是那个值,所以函数引用它就好了)
const [value, setValue] = useState(1)
const countRef = useRef(value) 
useEffect(() => { countRef.current = value }, [value])
  • 更新 State 时的回调函数
const [value, setValue] = useState(0)

  useEffect(() => {
    const timer = setInterval(() => {
      // 回调函数的最新值
      setValue(value => value + 1)
    }, 1000);
    return () => {
      clearInterval(timer)
    }
  }, [])

这里假如直接用setValue(value +1),由于useEffect里面的函数只在第一次渲染时定义了且引用了第一次的value,即使之后重复执行,它里面的value也一直是0,而使用回调函数更新state 就可以解决。

闭包陷阱和 Hooks 依赖

useEffectuseLayoutEffectuseCallbackuseMemo 的第二个参数为依赖数组,依·赖数组中任意一个依赖变化(浅比较)会有如下效果:

  1. useEffectuseLayoutEffect 内部的副作用函数会执行,并且副作用函数可以获取到当前所有依赖的最新值。
  2. useCallbackuseMemo 会返回新的函数或对象,并且内部的函数也能获取到当前所有依赖的最新值。

浅比较和深比较

浅比较也称引用相等,在 javascript 中, === '==' 以及Object.is()都是作浅比较,只检查左右两边是否是同一个对象的引用,也就是对于基本数据类型,只比较值,值相等就认为相等,对于引用数据类型就比较引用地址,引用地址相等才相等

const a = { a: 'key' };
const b = a;
b.c = 'xx'

console.log(Object.is(a, b)); // true

深比较*也称原值相等,深比较是指检查两个对象的所有属性是否都相等,深比较需要以递归的方式遍历两个对象的所有属性,操作比较耗时,深比较不管这两个对象是不是同一对象的引用

比如:lodash.isEqual(a, b),

console.log(_.isEqual(m, n)) // true

React中 useEffect useMemo的依赖数组项判断是否更新就是对前后两个值进行浅比较

== 比较

严格相等运算符(===)不同,它会尝试强制类型转换并且比较不同类型的操作数。

  • 如果两个操作数都是对象,则仅当两个操作数都引用同一个对象时才返回true

  • 如果一个操作数是null,另一个操作数是undefined,则返回true

  • 如果两个操作数是不同类型的,就会尝试在比较之前将它们转换为相同类型:

    • 当数字与字符串进行比较时,会尝试将字符串转换为数字值。

    • 如果操作数之一是Boolean,则将布尔操作数转换为 1 或 0。

      • 如果是true,则转换为1
      • 如果是 false,则转换为0
    • 如果操作数之一是对象,另一个是数字或字符串,会尝试使用对象的valueOf()toString()方法将对象转换为原始值。

  • 如果操作数具有相同的类型,则将它们进行如下比较:

    • Stringtrue仅当两个操作数具有相同顺序的相同字符时才返回。
    • Numbertrue仅当两个操作数具有相同的值时才返回。+0并被-0视为相同的值。如果任一操作数为NaN,则返回false
    • Booleantrue仅当操作数为两个true或两个false时才返回true

什么是 SPA,有什么优点和缺点

SPA 仅在 Web 页面初始化时加载相应的 HTMLJavaScript 和 CSS。一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现 HTML 内容的变换,UI 与用户的交互,避免页面的重新加载

优点:

  • 用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染;
  • 有利于前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理;

缺点:

  • 初次加载耗时多:为实现单页 Web 应用功能及显示效果,需要在加载页面的时候将 JavaScriptCSS 统一加载,部分页面按需加载;
  • 不利于 SEO:由于所有的内容都在一个页面中动态替换显示,所以在 SEO 上其有着天然的弱势。

自定义Hooks

1.自定义的useTitle hooks其实使用场景也很多,因为我们目前大部分项目都是采用SPA或者混合SPA的方式开发,对于不同的路由我们同样希望想多页应用一样能切换到对应的标题,这样可以让用户更好的知道页面的主题和内容。这个hooks的实现也很简单,我们直接上代码:

import { useEffect } from 'react'

const useTitle = (title) => {
    useEffect(() => {
      document.title = title
    }, [])
  
    return
  }

export default useTitle

const Home = () => {
    // ...
    useTitle('趣谈前端')
    
    return <div>home</div>
}

2. 实现自定义的useDebounce

import { useEffect, useRef } from 'react'

const useDebounce = (fn, ms = 30, deps = []) => {
    let timeout = useRef()
    useEffect(() => {
        if (timeout.current) clearTimeout(timeout.current)
        timeout.current = setTimeout(() => {
            fn()
        }, ms)
    }, deps)

    const cancel = () => {
        clearTimeout(timeout.current)
        timeout = null
    }
  
    return [cancel]
  }

export default useDebounce

自定义hooks必须以use开头,react需要根据这个标识检查内部hooks是否标准

一、组件基础

1. React 事件机制

<div onClick={this.handleClick.bind(this)}>点我</div>

button上绑定的事件

我们可以看到 ,button上绑定了两个事件,一个是document上的事件监听器,另外一个是button,但是事件处理函数handle,并不是我们的handerClick事件,而是noop

noop是什么呢?我们接着来看。

原来noop就指向一个空函数。

noop.jpg

然后我们看document绑定的事件

document.jpg

可以看到click事件被绑定在document上了。

接下来我们再搞搞事情😂😂😂,在demo项目中加上一个input输入框,并绑定一个onChange事件。睁大眼睛看看接下来会发生什么?

class Index extends React.Component{
    componentDidMount(){
        console.log(this)
    }
    handerClick= (value) => console.log(value) 
    handerChange=(value) => console.log(value)
    render(){
        return <div style={{ marginTop:'50px' }} >
            <button onClick={ this.handerClick } > 按钮点击 </button>
            <input  placeholder="请输入内容" onChange={ this.handerChange }  />
        </div>
    }
}
复制代码

我们先看一下input dom元素上绑定的事件

22BEC470-233A-4C50-9C47-D21D343C055D.jpg

然后我们看一下document上绑定的事件

8E1D3BDB-ACFB-4E49-A5FF-CF990C47A60E.jpg

我们发现,我们给<input>绑定的onChange,并没有直接绑定在input上,而是统一绑定在了document上,然后我们onChange被处理成很多事件监听器,比如blur , change , input , keydown , keyup 等。

综上我们可以得出结论:

  • 我们在 jsx 中绑定的事件(demo中的handerClickhanderChange),根本就没有注册到真实的dom上。是绑定在document上统一管理的。
  • 真实的dom上的click事件被单独处理,已经被react底层替换成空函数。
  • 我们在react绑定的事件,比如onChange,在document上,可能有多个事件与之对应。
  • react并不是一开始,把所有的事件都绑定在document上,而是采取了一种按需绑定,比如发现了onClick事件,再去绑定document click事件。

那么什么是react事件合成呢?

react中,我们绑定的事件onClick等,并不是原生事件,而是由原生事件合成的React事件,比如 click事件合成为onClick事件。比如blur , change , input , keydown , keyup等 , 合成为onChange

那么react采取这种事件合成的模式呢?

一方面,将事件绑定在document统一管理,防止很多事件直接绑定在原生的dom元素上。造成一些不可控的情况

另一方面, React 想实现一个全浏览器的框架, 为了实现这种目标就需要提供全浏览器一致性的事件系统,以此抹平不同浏览器的差异。

接下来的文章中,会介绍react是怎么做事件合成的。 以上是react 16的事件系统

关于react v17版本的事件系统

React v17 整体改动不是很大,但是事件系统的改动却不小,首先上述的很多执行函数,在v17版本不复存在了。我来简单描述一下v17事件系统的改版。

1 事件统一绑定container上,ReactDOM.render(app, container);而不是document上,这样好处是有利于微前端的,微前端一个前端系统中可能有多个应用,如果继续采取全部绑定在document上,那么可能多应用下会出现问题。

react_17_delegation.png

2 对齐原生浏览器事件

React 17 中终于支持了原生捕获事件的支持, 对齐了浏览器原生标准。同时 onScroll 事件不再进行事件冒泡。onFocusonBlur 使用原生 focusinfocusout 合成。

3 取消事件池 React 17 取消事件池复用,也就解决了上述在setTimeout打印,找不到e.target的问题。

为什么要自定义事件机制?

  • 抹平浏览器差异,实现更好的跨平台。
  • 避免垃圾回收,React 引入事件池,在事件池中获取或释放事件对象,避免频繁地去创建和销毁。
  • 方便事件统一管理和事务机制。

参考文章: react进阶:一文吃透react事件系统原理

与普通事件的区别?

区别:

  • 对于事件名称命名方式,原生事件为全小写,react 事件采用小驼峰;
  • 对于事件函数处理语法,原生事件为字符串,react 事件为函数;
  • react 事件不能采用 return false 的方式来阻止浏览器的默认行为,而必须要地明确地调用preventDefault()来阻止默认行为。

React 组件中怎么做事件代理?它的原理是什么?

React基于Virtual DOM实现了一个SyntheticEvent层(合成事件层),定义的事件处理器会接收到一个合成事件对象的实例,它符合W3C标准,且与原生的浏览器事件拥有同样的接口,支持冒泡机制,所有的事件都自动绑定在最外层上。

在React底层,主要对合成事件做了两件事:

  • 事件委派:React会把所有的事件绑定到结构的最外层,使用统一的事件监听器,这个事件监听器上维持了一个映射来保存所有组件内部事件监听和处理函数。
  • 自动绑定:React组件中,每个方法的上下文都会指向该组件的实例,即自动绑定this为当前组件。

2、class component

不排除现在还会有面试官问关于 class component 的问题。

2.1 生命周期

  • 初始化阶段。

发生在 constructor 中的内容,在 constructor 中进行 stateprops 的初始化,在这个阶段修改 state,不会执行更新阶段的生命周期,可以直接对 state 赋值。

  • 挂载阶段。
1. componentWillMount
   发生在 render 函数之前,还没有挂载 Dom
2. render
3. componentDidMount
   发生在 render 函数之后,已经挂载 Dom
  • 更新阶段。

更新阶段分为由 state 更新引起和 props 更新引起。

props 更新时:
1. componentWillReceiveProps(nextProps,nextState)
   这个生命周期主要为我们提供对 props 发生改变的监听,如果你需要在 props 发生改变后,相应改变组件的一些 state。在这个方法中改变 state 不会二次渲染,而是直接合并 state。
2. shouldComponentUpdate(nextProps,nextState)
   这个生命周期需要返回一个 Boolean 类型的值,判断是否需要更新渲染组件,优化 react 应用的主要手段之一,当返回 false 就不会再向下执行生命周期了,在这个阶段不可以 setState(),会导致循环调用。
3. componentWillUpdate(nextProps,nextState)
   这个生命周期主要是给我们一个时机能够处理一些在 Dom 发生更新之前的事情,如获得 Dom 更新前某些元素的坐标、大小等,在这个阶段不可以 setState(),会导致循环调用。
    **一直到这里 this.props 和 this.state 都还未发生更新**
4. render
5. componentDidUpdate(prevProps, prevState)
   在此时已经完成渲染,Dom 已经发生变化,state 已经发生更新,prevProps、prevState 均为上一个状态的值。

state 更新时(具体同上)
1. shouldComponentUpdate
2. componentWillUpdate
3. render
4. componentDidUpdate
复制代码
  • 卸载阶段。
1. componentWillUnmount
   在组件卸载及销毁之前直接调用。在此方法中执行必要的清理操作,例如,清除 timer,取消网络请求或清除在 componentDidMount  中创建的订阅等。componentWillUnmount 中不应调用 setState,因为该组件将永远不会重新渲染。组件实例卸载后,将永远不会再挂载它。

复制代码

在 React 16 中官方已经建议删除以下三个方法,非要使用必须加前缀:UNSAVE_

componentWillMount;
componentWillReceiveProps;
componentWillUpdate;
复制代码

取代这两三个生命周期的以下两个新的。

1. static getDerivedStateFromProps(nextProps,nextState)
   在组件实例化、接收到新的 props 、组件状态更新时会被调用
2. getSnapshotBeforeUpdate(prevProps,prevState)
   在这个阶段我们可以拿到上一个状态 Dom 元素的坐标、大小的等相关信息。用于替代旧的生命周期中的 componentWillUpdate。
   该函数的返回值将会作为 componentDidUpdate 的第三个参数出现。
复制代码

需要注意的是,一般都会问为什么要废弃三个生命周期,原因是什么。

类组件的bind绑定

类组件中函数this绑定分三种形式

1.constructor中进行函数的bind

class Test {
    constructor() {
        this.consoleName = this.consoleName.bind(this);
        this.con = this.con.bind(this);
    }

2.使用箭头函数定义方法

class Test {
    consoleName = () => {
      this.con('Hello Test');
    }
    con = (name) => {
      console.log(name);
    }
}
  1. 在render方法中直接bind
render () {
    return (
        <a onClick={this.clickEvent}>方法1 constructor绑定</a>
        <a onClick={() => this.clickEvent()}>方法2 箭头函数绑定</a>
        <a onClick={this.clickEvent.bind(this)}>方法3 直接绑定</a>
    )
}

为啥要bind呢?

render() {
    return (
        <div>
            <a onClick={this.clickEvent}>test</a>
        </div>
    )
}

如代码所示,我们给a标签绑定了一个onClick事件。我们大家都知道,react是从虚拟DOM映射成为真实DOM,onClick也不是真实DOM中的click事件,只是在从虚拟DOM生成真实DOM的过程中,将这个this.clickEvent赋值到真实DOM上的click事件。因此,render中的onClick相当于一个中间量的存在,因为中间量的存在当真实DOM调用this.clickEvent时,此时的this自然无法指向到我们定义的组件实例,导致this指向丢失,因此必须进行bind。

ES6的class内部默认严格模式,严格模式下,如果 this 没有被执行环境定义(即没有作为对象的属性或者方法调用),那它将保持为进入执行环境时的值,即undefined

2.2 setState 同步还是异步

setState 本身代码的执行肯定是同步的,这里的异步是指是多个 state 会合成到一起进行批量更新。 同步还是异步取决于它被调用的环境。

  • 如果 setState 在 React 能够控制的范围被调用,它就是异步的。比如合成事件处理函数生命周期函数, 此时会进行批量更新,也就是将状态合并后再进行 DOM 更新。
  • 如果 setState 在原生 JavaScript 控制的范围被调用,它就是同步的。比如原生事件处理函数,定时器回调函数,Ajax 回调函数中,此时 setState 被调用后会立即更新 DOM 。

3、对函数式编程的理解

这篇文章写的真的太好了,一定要读:简明 JavaScript 函数式编程——入门篇

总结一下: 函数式编程有两个核心概念。

  • 数据不可变(无副作用): 它要求你所有的数据都是不可变的,这意味着如果你想修改一个对象,那你应该创建一个新的对象用来修改,而不是修改已有的对象。
  • 无状态: 主要是强调对于一个函数,不管你何时运行,它都应该像第一次运行一样,给定相同的输入,给出相同的输出,完全不依赖外部状态的变化。

纯函数带来的意义。

  • 便于测试和优化:这个意义在实际项目开发中意义非常大,由于纯函数对于相同的输入永远会返回相同的结果,因此我们可以轻松断言函数的执行结果,同时也可以保证函数的优化不会影响其他代码的执行。
  • 可缓存性:因为相同的输入总是可以返回相同的输出,因此,我们可以提前缓存函数的执行结果。
  • 更少的 Bug:使用纯函数意味着你的函数中不存在指向不明的 this,不存在对全局变量的引用,不存在对参数的修改,这些共享状态往往是绝大多数 bug 的源头。

4、react hooks

现在应该大多数面试官会问 hooks 相关的啦。这里我强烈推荐三篇文章,即使没看过源码,也能比较好地理解一些原理:

那么 Hooks 的出现又是为了解决什么问题呢?我们可以试图总结一下类组件颇具代表性的痛点

  1. 令人头疼的 this 管理,容易引入难以追踪的 Bug
  2. 生命周期的划分并不符合“内聚性”原则,例如 setIntervalclearInterval 这种具有强关联的逻辑被拆分在不同的生命周期方法中
  3. 组件复用(数据共享或功能复用)的困局,从早期的 Mixin,到高阶组件(HOC),再到 Render Props,始终没有一个清晰直观又便于维护的组件复用方案,(hooks 使用自定义hook进行组件逻辑封装复用)

没错,随着 Hooks 的推出,这些痛点都成为了历史!

每次渲染相互独立,因此每次渲染时组件中的状态、事件处理函数等等都是独立的,或者说只属于所在的那一次渲染

useEffect 使用浅析

useEffect(effectFn, deps)
复制代码

effectFn 是一个执行某些可能具有副作用的 Effect 函数(例如数据获取、设置/销毁定时器等),它可以返回一个清理函数(Cleanup),例如大家所熟悉的 setIntervalclearInterval

useEffect(() => {
  const intervalId = setInterval(doSomething(), 1000);
  return () => clearInterval(intervalId);
});
复制代码

可以看到,我们在 Effect 函数体内通过 setInterval 启动了一个定时器,随后又返回了一个 Cleanup 函数,用于销毁刚刚创建的定时器。

  • 每个 Effect 必然在渲染之后执行(且是异步执行),因此不会阻塞渲染,提高了性能
  • 在运行每个 Effect 之前,运行前一次渲染的 Effect Cleanup 函数(如果有的话)
  • 当组件销毁时,运行最后一次 Effect 的 Cleanup 函数

自定义 Hook

我们可以发现自定义 Hook 具有以下特点:

  • 表面上:一个命名格式为 useXXX 的函数,但不是 React 函数式组件
  • 本质上:内部通过使用 React 自带的一些 Hook (例如 useState 和 useEffect )来实现某些通用的逻辑

比如ahooks一个hooks库

自定义 Hook 本质上只是把调用内置 Hook 的过程封装成一个个可以复用的函数,并不影响 Hook 链表的生成和读取

useCallback 和 useMemo 的关系

useCallback 主要是为了解决函数的”引用相等“问题,而 useMemo 则是一个”全能型选手“,能够同时胜任引用相等和节约计算的任务。

实际上,useMemo 的功能是 useCallback超集。与 useCallback 只能缓存函数相比,useMemo 可以缓存任何类型的值(当然也包括函数)。useMemo 的使用方法如下:

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

useReducer

Reducer 函数的前生今世

Reducer 函数有两个必要规则

  • 只返回一个值
  • 不修改输入值,而是返回新的值

Redux 基本思想

Redux 的核心思想之一就是将状态放到唯一的全局对象(一般称为 Store)中,而修改状态则是调用对应的 Reducer 函数去更新 Store 中的状态

什么时候该用 useReducer

如果你的状态管理出现了至少一个以下所列举的问题:

  • 需要维护的状态本身比较复杂,多个状态之间相互依赖
  • 修改状态的过程比较复杂

在React中页面重新加载时怎样保留数据?

这个问题就设计到了数据持久化,主要的实现方式有以下几种:

  • Redux:将页面的数据存储在redux中,在重新加载页面时,获取Redux中的数据.
  • sessionStorge:在进入选择地址页面之前,componentWillUnMount的时候,将数据存储到sessionStorage中,每次进入页面判断sessionStorage中有没有存储的那个值,有,则读取渲染数据;没有,则说明数据是初始化的状态。返回或进入除了选择地址以外的页面,清掉存储的sessionStorage,保证下次进入是初始化的数据

为什么使用jsx的组件中没有看到使用react却需要引入react?

本质上来说JSX是React.createElement(component, props, ...children)方法的语法糖。在React 17之前,如果使用了JSX,其实就是在使用React, babel 会把组件转换为 CreateElement 形式。在React 17之后,就不再需要引入,因为 babel 已经可以帮我们自动引入react。

4.1 为什么不能在条件语句中写 hook

hook 在每次渲染时的查找是根据一个“全局”的下标对链表进行查找的,如果放在条件语句中使用,有一定几率会造成拿到的状态出现错乱。

hooks使用的条件:hooks只能在函数组件中使用,且不能在循环条件语句中使用

必须按照顺序调用从根本上来说是因为 useState 这个钩子在设计层面并没有“状态命名”这个动作,也就是说你每生成一个新的状态,React 并不知道这个状态名字叫啥,所以需要通过顺序来索引到对应的状态值

4.2 HOC 和 hook 的区别

hoc 能复用逻辑和视图,hook 只能复用逻辑

高阶组件是参数为组件,返回值为新组件的函数,是 React 中用于复用组件逻辑的一种高级技巧(把公共的组件逻辑抽出来), 比如Redux 的 connect就是高阶组件 。

4.3 useEffect 和 useLayoutEffect 区别

对于 React 的函数组件来说,其更新过程大致分为以下步骤:

  1. 因为某个事件 state 发生变化。
  2. React 内部更新 state 变量。
  3. React 处理更新组件中 return 出来的 DOM 节点(进行一系列 dom diff 、调度等流程)。
  4. 将更新过后的 DOM 数据绘制到浏览器中。
  5. 用户看到新的页面。

useEffect 在第 4 步之后执行,且是异步的,保证了不会阻塞浏览器进程。 useLayoutEffect 在第 3 步至第 4 步之间执行,且是同步代码,所以会阻塞后面代码的执行。

4.4 useEffect 依赖为空数组与 componentDidMount 区别

render 执行之后,componentDidMount 会执行,如果在这个生命周期中再一次 setState ,会导致再次 render ,返回了新的值,浏览器只会渲染第二次 render 返回的值,这样可以避免闪屏。

但是 useEffect 是在真实的 DOM 渲染之后才会去执行,这会造成两次 render ,有可能会闪屏。

实际上 useLayoutEffect 会更接近 componentDidMount 的表现,它们都同步执行且会阻碍真实的 DOM 渲染的。

4.5 React.memo() 和 useMemo() 的区别

  • memo 是一个高阶组件,默认情况下会对 props 进行浅比较,如果相等不会重新渲染。多数情况下我们比较的都是引用类型,浅比较就会失效,所以我们可以传入第二个参数手动控制。
  • useMemo 返回的是一个缓存值,只有依赖发生变化时才会去重新执行作为第一个参数的函数,需要记住的是,useMemo 是在 render 阶段执行的,所以不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect 的适用范畴。

4.6 React.useCallback() 和 React.useMemo() 的区别

  • useCallback 可缓存函数,其实就是避免每次重新渲染后都去重新执行一个新的函数。
  • useMemo 可缓存值。

有很多时候,我们在 useEffect 中使用某个定义的外部函数,是要添加到 deps 数组中的,如果不用 useCallback 缓存,这个函数在每次重新渲染时都是一个完全新的函数,也就是引用地址发生了变化,这就会导致 useEffect 总会无意义的执行。

4.7 React.forwardRef 是什么及其作用

一般在父组件要拿到子组件的某个实际的 DOM 元素时会用到。 子组件使用React.forwardRef()包裹,父组件定义ref 然后通过React.forwardRef()的ref参数传递给子组件具体的DOM

const FancyButton = React.forwardRef((props, ref) => (  <button ref={ref} className="FancyButton">    {props.children}
 </button>
));

// 你可以直接获取 DOM button 的 ref:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;

React中可以在render访问refs吗?为什么?

不可以 因为render时候还没有生成dom

为什么要用虚拟DOM

DOM操作很慢,轻微的操作都可能导致⻚面重新排 版,⾮常耗性能。相对于DOM对象,js对象处理起来更快, 而且更简单。通过diff算法对比新旧vdom之间的差异,可以 批量的、最⼩化的执行dom操作,从而提高性能

7、介绍 React dom diff 算法

我们来梳理一下整个DOM-diff的过程:

  1. 用JS对象模拟DOM(虚拟DOM)jsx语法最后编译成的就是对象
  2. 把此虚拟DOM转成真实DOM并插入页面中(render)
  3. 如果有事件发生修改了虚拟DOM,通过算法遍历比较两棵虚拟DOM树的差异,得到差异对象(diff)
  4. 把差异对象应用到真正的DOM树上(patch)

比较规则

传统的diff算法首先递归比较两颗树的节点(O(n^2)),然后寻找最短的转换路径。最终达到的时间复杂度是O(n^3)。如果节点过多,那么性能消耗是巨大的。因此,react简单粗暴的修改了diff算法,将时间复杂度降到O(n)。

React 的 diff 算法,触发更新的时机主要在 state 变化与 hooks 调用之后。此时触发虚拟 DOM 树变更遍历,采用了深度优先遍历算法。但传统的遍历方式,效率较低。为了优化效率,使用了分治的方式。将单一节点比对转化为了 3 种类型节点的比对,分别是树、组件及元素,以此提升效率。

  • 树比对:由于网页视图中较少有跨层级节点移动,两株虚拟 DOM 树只对同一层次的节点进行比较。
  • 组件比对:如果组件是同一类型,则进行树比对,如果不是,则直接放入到补丁中。
  • 元素比对:主要发生在同层级中,通过标记节点操作生成补丁,节点操作对应真实的 DOM 剪裁操作。

8、对 React Fiber 的理解

  1. 首先,简要介绍 React Fiber 的概念。您可以说:“React Fiber 是 React 16 引入的一种新的协调算法,用于更高效地更新和渲染组件。”
  2. 解释其主要目标和解决的问题。例如:“它的主要目标是提高 React 应用的性能,尤其是在处理大型复杂组件树和动画等需要高帧率的场景中。解决了之前同步渲染可能导致的卡顿和长时间阻塞用户交互的问题。”
  3. 提及一些关键特性。比如:“Fiber 实现了任务的分片和优先级调度,能够暂停、恢复和重新安排渲染任务。它还引入了新的渲染流程,将渲染过程拆分成多个小的工作单元。”
  4. 可以结合实际项目经验,如果有的话。比如:“在我之前的项目中,使用了 React 16 及以上版本,通过 React Fiber 的优化,显著提升了页面的交互响应性和流畅度。”
  5. 最后,总结一下其对 React 开发的重要性。例如:“总的来说,React Fiber 是 React 框架发展中的重要改进,使开发者能够构建更复杂、高性能的用户界面。”

5. 对React-Fiber的理解,它解决了什么问题?

React V15 在渲染时,会递归比对 VirtualDOM 树,找出需要变动的节点,然后同步更新它们, 一气呵成。这个过程期间, React 会占据浏览器资源,这会导致用户触发的事件得不到响应,并且会导致掉帧,导致用户感觉到卡顿

高优先级的任务比如:用户点击事件的响应(比如一个动画渲染,用户可能并不关心渲染,只想点击以后能快速响应),资源的加载,脚本的执行

为了给用户制造一种应用很快的“假象”,不能让一个任务长期霸占着资源。 可以将浏览器的渲染、布局、绘制、资源加载(例如 HTML 解析)、事件响应、脚本执行视作操作系统的“进程”,需要通过某些调度策略合理地分配 CPU 资源,从而提高浏览器的用户响应速率, 同时兼顾任务执行效率。

所以 React 通过Fiber 架构,让这个执行过程变成可被中断。“适时”地让出 CPU 执行权,除了可以让浏览器及时地响应用户的交互,还有其他好处:

  • 分批延时对DOM进行操作,避免一次性操作大量 DOM 节点,可以得到更好的用户体验;
  • 给浏览器一点喘息的机会,它会对代码进行编译优化(JIT)及进行热代码优化,或者对 reflow 进行修正。

**核心思想:**Fiber 也称协程或者纤程。它和线程并不一样,协程本身是没有并发或者并行能力的(需要配合线程),它只是一种控制流程的让出机制。让出 CPU 的执行权,让 CPU 能在这段时间执行其他的操作。渲染的过程可以被中断,可以将控制权交回浏览器,让位给高优先级的任务,浏览器空闲后再恢复渲染。

9、React 性能优化手段

  • 使用 React.memo 来缓存组件。父组件的每次状态更新,都会导致子组件重新渲染,即使传入子组件的状态没有变化,为了减少重复渲染,我们可以使用React.memo来缓存子组件,这样只有当传入组件的状态值发生变化时才会重新渲染。如果传入相同的值,则返回缓存的组件 浅比较props 比较的是引用地址,判断是否重新渲染 如果浅比较不适用的话,可以自定义比较函数作为React.memo()的第二个参数(这个比较函数用来比较两个props是否一样)
  • 使用 useMemo() 缓存大量的计算。
  • shouldComponentUpdate(nextProps)这个生命周期函数判断组件是否需要重新渲染,通过对比this.props === nextprops,
  • 避免使用匿名函数。
  • 利用 React.lazyReact.Suspense 延迟加载不是立即需要的组件。
  • 尽量使用 CSS 而不是强制加载和卸载组件,不要 {a&& <div>},可以通过设置组件的style={opacity: 1}来让其隐藏,不会造成回流
  • 使用 React.Fragment 避免添加额外的 DOM,有时候规定必须有一个父元素,这时候没必要插入一个真实的dom造成性能浪费。

10、React Redux

Redux 三大概念:Store,Action,Reducers,将所有状态都保存在一个公共的store中,使用connect()将store和组件绑定,组件对state是只读的,组件通过dispatch({type: 'xx'})来操作reducer 进而更新state。 达到state可以在所有组件中传递。

10 react中如何避免重新渲染

6. React.Component 和 React.PureComponent 的区别

PureComponent表示一个纯组件,可以用来优化React程序,减少render函数执行的次数,从而提高组件的性能。

在React中,当prop或者state发生变化时,可以通过在shouldComponentUpdate生命周期函数中执行return false来阻止页面的更新,从而减少不必要的render执行。React.PureComponent会自动执行 shouldComponentUpdate。

不过,pureComponent中的 shouldComponentUpdate() 进行的是浅比较,也就是说如果是引用数据类型的数据,只会比较不是同一个地址,而不会比较这个地址里面的数据是否一致。浅比较会忽略属性和或状态突变情况,其实也就是数据引用指针没有变化,而数据发生改变的时候render是不会执行的。如果需要重新渲染那么就需要重新开辟空间引用数据。PureComponent一般会用在一些纯展示组件上。

使用pureComponent的好处:当组件更新时,如果组件的props或者state都没有改变,render函数就不会触发。省去虚拟DOM的生成和对比过程,达到提升性能的目的。这是因为react自动做了一层浅比较。

7. Component, Element, Instance 之间有什么区别和联系?

  • **元素:**一个元素element是一个普通对象(plain object),描述了对于一个DOM节点或者其他组件component,你想让它在屏幕上呈现成什么样子。元素element可以在它的属性props中包含其他元素(译注:用于形成元素树)。创建一个React元素element成本很低。元素element创建之后是不可变的。
  • **组件:**一个组件component可以通过多种方式声明。可以是带有一个render()方法的类,简单点也可以定义为一个函数。这两种情况下,它都把属性props作为输入,把返回的一棵元素树作为输出。
  • **实例:**一个实例instance是你在所写的组件类component class中使用关键字this所指向的东西(译注:组件实例)。它用来存储本地状态和响应生命周期事件很有用。

函数式组件(Functional component)根本没有实例instance。类组件(Class component)有实例instance,但是永远也不需要直接创建一个组件的实例,因为React帮我们做了这些。

11. 哪些方法会触发 React 重新渲染?重新渲染 render 会做些什么?

(1)哪些方法会触发 react 重新渲染?

  • setState()方法被调用

setState 是 React 中最常用的命令,通常情况下,执行 setState 会触发 render。但是这里有个点值得关注,执行 setState 的时候不一定会重新渲染。当 setState 传入 null 时,并不会触发 render。

class App extends React.Component {
  state = {
    a: 1
  };

  render() {
    console.log("render");
    return (
      <React.Fragement>
        <p>{this.state.a}</p>
        <button
          onClick={() => {
            this.setState({ a: 1 }); // 这里并没有改变 a 的值
          }}
        >
          Click me
        </button>
        <button onClick={() => this.setState(null)}>setState null</button>
        <Child />
      </React.Fragement>
    );
  }
}
  • 父组件重新渲染

只要父组件重新渲染了,即使传入子组件的 props 未发生变化,那么子组件也会重新渲染,进而触发 render

(2)重新渲染 render 会做些什么?

  • 会对新旧 VNode 进行对比,也就是我们所说的Diff算法。
  • 对新旧两棵树进行一个深度优先遍历,这样每一个节点都会一个标记,在到深度遍历的时候,每遍历到一和个节点,就把该节点和新的节点树进行对比,如果有差异就放到一个对象里面
  • 遍历差异对象,根据差异的类型,根据对应对规则更新VNode

React 的处理 render 的基本思维模式是每次一有变动就会去重新渲染整个应用。在 Virtual DOM 没有出现之前,最简单的方法就是直接调用 innerHTML。Virtual DOM厉害的地方并不是说它比直接操作 DOM 快,而是说不管数据怎么变,都会尽量以最小的代价去更新 DOM。React 将 render 函数返回的虚拟 DOM 树与老的进行比较,从而确定 DOM 要不要更新、怎么更新。当 DOM 树很大时,遍历两棵树进行各种比对还是相当耗性能的,特别是在顶层 setState 一个微小的修改,默认会去遍历整棵树。尽管 React 使用高度优化的 Diff 算法,但是这个过程仍然会损耗性能.

(3)forceUpdate

12 React-Router的实现原理是什么?

客户端路由实现的思想:

  • 基于 hash 的路由:通过监听hashchange事件,感知 hash 的变化
    • 改变 hash 可以直接通过 location.hash=xxx
  • 基于 H5 history 路由:
    • 改变 url 可以通过 history.pushState 和 resplaceState 等,会将URL压入堆栈,同时能够应用 history.go() 等 API
    • 监听 url 的变化可以通过自定义事件触发实现

react-router 实现的思想:

  • 基于 history 库来实现上述不同的客户端路由实现思想,并且能够保存历史记录等,磨平浏览器差异,上层无感知
  • 通过维护的列表,在每次 URL 发生变化的回收,通过配置的 路由路径,匹配到对应的 Component,并且 render

react router的几种传参方式

1.params传参

优点:刷新页面,参数不丢失

缺点:1.只能传字符串,传值过多url会变得很长 2. 参数必须在路由上配置

{ path: '/detail/:id/:name', component: Detail }

2.search传参

优点:刷新页面,参数不丢失

缺点:只能传字符串,传值过多url会变得很长,获取参数需要自定义hooks

history.push('/detail?id=2')

3.state传参 优点:可以传对象

缺点: <HashRouter>刷新页面,参数丢失

history.push(`/user/role/detail`, { id: item });

13. 如何配置 React-Router 实现路由切换

(1)使用 组件

路由匹配是通过比较 的 path 属性和当前地址的 pathname 来实现的。当一个 匹配成功时,它将渲染其内容,当它不匹配时就会渲染 null。没有路径的 将始终被匹配。

// when location = { pathname: '/about' }
<Route path='/about' component={About}/> // renders <About/>
<Route path='/contact' component={Contact}/> // renders null
<Route component={Always}/> // renders <Always/>

(2)结合使用 组件和 组件

用于将 分组。

<Switch>
    <Route exact path="/" component={Home} />
    <Route path="/about" component={About} />
    <Route path="/contact" component={Contact} />
</Switch>

不是分组 所必须的,但他通常很有用。 一个 会遍历其所有的子 元素,并仅渲染与当前地址匹配的第一个元素。

(3)使用 、 、 组件

组件来在你的应用程序中创建链接。无论你在何处渲染一个 ,都会在应用程序的 HTML 中渲染锚()。

<Link to="/">Home</Link>   
// <a href='/'>Home</a>

是一种特殊类型的 当它的 to属性与当前地址匹配时,可以将其定义为"活跃的"。

// location = { pathname: '/react' }
<NavLink to="/react" activeClassName="hurray">
    React
</NavLink>
// <a href='/react' className='hurray'>React</a>

当我们想强制导航时,可以渲染一个,当一个渲染时,它将使用它的to属性进行定向。 router%E6%80%8E%E4%B9%88%E8%AE%BE%E7%BD%AE%E9%87%8D%E5%AE%9A%E5%90%91)

umi中路由的用法?

1.主要是通过config中配置route和component的关系 2.通过history.push('/aa')跳转 3.通过组件式跳转

了解React18

1.setState 自动批处理

React 18 通过在默认情况下执行批处理来实现了开箱即用的性能改进

2.flushSync

批处理是一个破坏性改动,如果你想退出批量更新,你可以使用 flushSync

新的 API

一、useId

支持同一个组件在客户端和服务端生成相同的唯一的 ID,因为我们的服务器渲染时提供的 HTML 是无序的useId 的原理就是每个 id 代表该组件在组件树中的层级结构

Concurrent Mode(并发模式)

CM 本身并不是一个功能,而是一个底层设计,从同步不可中断更新变成了异步可中断更新

redux和Dva

目前最流行的数据流方案

  • redux单向数据流方案
  • 响应式数据流方案,以 Mobx 为代表
  • 其他,比如 rxjs 等

dva简化了redux和redux-saga的API,让开发react更加方便快捷

Model 对象的属性

  • namespace: 当前 Model 的名称。整个应用的 State,由多个小的 Model 的 State 以 namespace 为 key 合成
  • state: 该 Model 当前的状态。数据保存在这里,直接决定了视图层的输出
  • reducers: Action 处理器,处理同步动作,用来算出最新的 State
  • effects:Action 处理器,处理异步动作

state

State 表示 Model 的状态数据,通常表现为一个 javascript 对象(当然它可以是任何值);操作的时候每次都要当作不可变数据(immutable data)来对待,保证每次都是全新对象,没有引用关系,这样才能保证 State 的独立性,便于测试和追踪变化

Effect函数

dva 提供多个 effect 函数内部的处理函数,比较常用的是 call 和 put

  • call:执行异步函数
  • put:发出一个 Action,类似于 dispatch

最常见的就是执行异步操作,dva 为了控制副作用的操作,底层引入了redux-sagas做异步流程控制,由于采用了generator的相关概念,所以将异步转成同步写法,从而将effects转为纯函数.

Reducer函数

用来同步更新state,通过acton传入的值,执行对应的reducer,且reducer必须是纯函数,不应该有副作用,每一次的计算都应该使用不可变数据

State 和 View

State 是储存数据的地方,收到 Action 以后,会更新数据。

View 就是 React 组件构成的 UI 层,从 State 取数据后,渲染成 HTML 代码。只要 State 有变化,View 就会自动更新

connect 方法

connect 是一个函数,绑定 State 到 View。

import { connect } from 'dva';

function mapStateToProps(state) {
  return { todos: state.todos };
}
connect(mapStateToProps)(App);

connect 方法返回的也是一个 React 组件,通常称为容器组件。因为它是原始 UI 组件的容器,即在外面包了一层 State。

connect 方法传入的第一个参数是 mapStateToProps 函数,mapStateToProps 函数会返回一个对象,用于建立 State 到 Props 的映射关系

Connect部分:mapStateToProps和mapDispatchToProps

一、 Connect:使用mapStateToProps提取数据

mapStateToPropsconnect的第一个参数,被用于从 Store 中选择 connected component 需要的部分数据。mapStateToProps通常简称为 mapState

Store 中的 state 每次改变都会调用mapStateToProps mapStateToProps接受整个 store,并返回组件需要的数据对象

mapStateToProps 的第一个参数state为全局的,第二个参数ownProps,如果你的组件想用自己的props数据来从 store 中检索数据,你可以使用第二个参数 ownProps 来定义,使用ownProps作为返回state的一部分

  • mapStateProps返回的是一个包含其所在组件所需数据的普通对象,返回的对象中的值用于确定组件是否需要重新渲染 ,返回的state作为当前组件的props的一部分

  • 每当 store 发生变化时,所有连接组件的所有 mapStateToProps 函数都会运行。因此,您的 mapStateToProps 函数应该尽可能快地运行

  • mapStateToProps函数应是纯函数和同步函数,应该只是单纯返回该组件需要的state,不应该执行异步操作

function mapStateToProps(state,ownProps) {
  const { todos } = state
  return { todoList: todos.allIds }
}

export default connect(mapStateToProps)(TodoList)
  • React Redux 在内部实现了 shouldComponentUpdate 方法,这样包装器组件就会在组件需要的数据发生变化时准确地重新渲染。默认情况下,React Redux 通过对返回对象的每个字段(仅仅对比对象第一层key对应的value)使用 === 比较(“浅相等”检查)来决定从 mapStateToProps 返回的对象的内容是否不同。如果任何字段发生更改,则您的组件将被重新渲染,以便它可以接收更新的值作为 props。。请注意,返回相同引用的变异对象是一个常见错误(可能是reducers是不纯函数导致的错误),它可能导致您的组件无法按预期重新渲染。

  • 数据转换尽量放在reducers中,别放在mapStateToprops中,因为数据转化很容易生成新的引用的对象或者数组,即使值相等但引用变了,所以在浅比较时会导致意外的渲染,如果实在要在mapStateprops中做的话就包裹在useMemo或者useCallback这种记忆类hook

行为和陷阱

每次store变化时,store都会执行所有组件的mapStateToProps,通过对比每一个组件state中的每一个key的value(新旧两个state的key的value,浅比较),如果都===相等那么久不重新渲染,所以如果当前组件state为{a: {b:'xx'}}(即key的值为引用数据),reducer没有返回新的对象还是旧的state,这样即使{a:{b:‘aa’}} 但是a对应的value引用地址没变 因为组件是否重新渲染是根据mapStateToProps的返回值决定的,所以不会重新渲染.

Connect: 用mapDispatchToProps来分派操作行为

作为传递给 connect 的第二个参数,mapDispatchToProps 用于将操作分派到store。

dispatch 是 Redux store 的一个函数。你调用 store.dispatch 来调度一个动作。这是触发状态更改的唯一方法。

使用 React Redux,你的组件永远不会直接访问store——connect 会为你做这件事。 React Redux 为您提供了两种让组件调度操作的方法:

  • 默认情况下,a connected component 接收 props.dispatch 并且可以自己调度动作。
  • connect 可以接受一个名为 mapDispatchToProps 的参数,它允许您创建调用时调度的函数,并将这些函数作为 props 传递给您的组件。

mapDispatchToProps 函数通常简称为 mapDispatch,但实际使用的变量名称可以是您想要的任何名称。

  • 默认不指定connect的第二个参数时,就只有props.dispatch,来出发store的更新

  • 手动指定connect的第二个参数mapDispatchToProps,这时props.dispatch将不生效,而是使用自定义的dispatch

mapDispatchToProps有两种写法,一种是函数式,一种是对象简写式(推荐)

const mapDispatchToProps = {
  increment,
  decrement,
  reset,
}

export default connect(mapState, mapDispatchToProps)(Counter)

mapDispatchToProps 定义为一个对象

你已经看到在 React 组件中调度 Redux 动作的设置遵循一个非常相似的过程:定义一个动作创建者,将它包装在另一个看起来像 (...args) => dispatch(actionCreator(...args)) 的函数中,然后将该包装函数作为prop传递给组件。

因为这很常见,connect 支持 mapDispatchToProps 参数的 “对象简写” 形式:如果你传递一个由action creators组成的对象,而不是一个函数,connect 将在内部自动为你调用 bindActionCreators

建议始终使用 mapDispatchToProps 的“对象简写”形式

注意:

  • mapDispatchToProps 对象的每个字段都被假定为一个action creator
  • 您的组件将不再作为props接收dispatch
// React Redux 为你自动做了以下代码
;(dispatch) => bindActionCreators(mapDispatchToProps, dispatch)

// 所以你只需把mapDispatchToProps简写为
const mapDispatchToProps = {
  increment,
  decrement,
  reset,
}

我可以在 Redux 中没有 mapStateToProps 的时候mapDispatchToProps吗?

yes!你可以通过传递undefined或null来跳过第一个参数,组件将不会订阅store,且仍然能收到mapdispatchToProps定义的dispatch props

connect(null, mapDispatchToProps)(MyComponent)

context VS redux

首先context分为旧版(16.3以前)和新版,旧版的context存在props传递截断问题,所谓的“传递截断”就是指在如果在使用旧版 context API来跨层级去传递props过程中,假如承载了数据源的父组件和最终接收数据的子组件之间的某个组件通过shouldComponentUpdate来跳过自己的更新的话(shouldComponentUpdate函数里面return false),那么子组件也会被动跳过更新,因而无法拿到最新的prop值

新版的context解决了这个问题,在新版Context API的实现里面,只要某个子组件订阅了context对象实例所承载的数据,只要这些数据发生了改变,不管该子组件的上层组件做了什么,这个子组件都会得到更新

Context API + useReducer 实现简易redux

通过将Context API的跨任何组件层级运输数据的能力和useReducer组件状态创建和更新能力结合起来,最终把它们提升到组件树的根组件,我们就可以实现“Context API版的redux范式

怎么用?

  1. 在根组件上(假设是<App>),使用useReducer来创建状态:

    // App.js
    const initialState = {count: 0};
    
    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 App(){
        const [state,dispatch] = useReducer(reducer,initialState)
    
        return (......)
    }
    
  2. 在根组件上,使用Context API来创建context对象实例,并使用它的Provider将状态和改变状态的方法(这里是dispatch)传递下去:

    // App.js
    const initialState = {count: 0};
    export const MyContext = React.createContext()
    
    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 App(){
        const [state,dispatch] = React.useReducer(reducer,initialState)
    
        return (
            <MyContext.Provider value={{state,dispatch}}>{.....}</MyContext.Provider>
        )
    }
    
  3. 最后是在需要消费的子组件里面导入context实例对象,使用useContext()来将状态和更新状态的方法取回来:

    // ChildComponent.js
    import {MyContext} from './App'
    
    
    function ChildComponent(){
        const {state,dispatch} = useContext(MyContext)
        
        return (
            <div>
             current count is {state.count}
             <button onClick={() => dispatch({type: 'decrement'})}>-</button>
             <button onClick={() => dispatch({type: 'increment'})}>+</button>
            <div>
        )
    }
    

通过上面三个步骤,我们就可以实现一个redux范式。在这个实现里面:Context API负责数据的分发,useReducer复杂状态的管理(创建状态,更新状态),最后通过在根组件上把两者结合起来,那么子组件就可以通过useContext这个hook来消费了。

context+useReducer 和 redux的区别

  • 订阅了context实例对象的组件在context值变化的时候一定会被强制更新,也就是说没办法跳过更新。
  • 在这种范式下,只要你的组件订阅了context实例对象,那么一旦context对象的某部分值发生改变,你的组件都会被迫更新,即使你没有消费这部分的值。但是在 redux+ react-redux 的实现里面就不会这样。只有当前组件所消费的状态值发生改变了,当前组件才会被更新。这就避免了因为不必要更新所带来的性能问题。这也是context和connect的区别