React面试积累

175 阅读30分钟

面试题视频

拉勾React面试

React面试题金编

面试题

state 和 props 有什么区别

相同点:

  • 两者都是 JavaScript 对象
  • 两者都是用于保存信息
  • props 和 state 都能触发渲染更新

区别:

  • props 是外部传递给组件的,而 state 是在组件内被组件自己管理的,一般在 constructor 中初始化
  • props 在组件内部是不可修改的,但 state 在组件内部可以进行修改
  • state 是多变的、可以修改

super() 和 super(props)

React 中,类组件基于 ES6,所以在 constructor 中必须使用 super(super将父类的this传递给子类,子类不能在super前使用this)

在调用 super 过程,无论是否传入 propsReact 内部都会将 porps 赋值给组件实例 porps 属性中

如果只调用了 super(),那么 this.propssuper() 和构造函数结束之间仍是 undefined

React事件机制

合成事件是 React模拟原生 DOM事件所有能力的一个事件对象,即浏览器原生事件的跨浏览器包装器

  • React 所有事件都挂载在 document 对象上
  • 当真实 DOM 元素触发事件,会冒泡到 document 对象后,再处理 React 事件
  • 所以会先执行原生事件,然后处理 React 事件
  • 最后真正执行 document 上挂载的事件

执行顺序:

import  React  from 'react';
class App extends React.Component{
​
  constructor(props) {
    super(props);
    this.parentRef = React.createRef();
    this.childRef = React.createRef();
  }
  componentDidMount() {
    console.log("React componentDidMount!");
    this.parentRef.current?.addEventListener("click", () => {
      console.log("原生事件:父元素 DOM 事件监听!");
    });
    this.childRef.current?.addEventListener("click", () => {
      console.log("原生事件:子元素 DOM 事件监听!");
    });
    document.addEventListener("click", (e) => {
      console.log("原生事件:document DOM 事件监听!");
    });
  }
  parentClickFun = () => {
    console.log("React 事件:父元素事件监听!");
  };
  childClickFun = () => {
    console.log("React 事件:子元素事件监听!");
  };
  render() {
    return (
      <div ref={this.parentRef} onClick={this.parentClickFun}>
        <div ref={this.childRef} onClick={this.childClickFun}>
          分析事件执行顺序
        </div>
      </div>
    );
  }
}
export default App;
​
​
​
//原生事件:子元素 DOM 事件监听! 
//原生事件:父元素 DOM 事件监听! 
//React 事件:子元素事件监听! 
//React 事件:父元素事件监听! 
//原生事件:document DOM 事件监听!

所以想要阻止不同时间段的冒泡行为,对应使用不同的方法,对应如下:

  • 阻止合成事件间的冒泡,用e.stopPropagation()
  • 阻止合成事件与最外层 document 上的事件间的冒泡,用e.nativeEvent.stopImmediatePropagation()
  • 阻止合成事件与除最外层document上的原生事件上的冒泡,通过判断e.target来避免

React事件机制总结如下

  • React 上注册的事件最终会绑定在document这个 DOM 上,而不是 React 组件对应的 DOM(减少内存开销就是因为所有的事件都绑定在 document 上,其他节点没有绑定事件)
  • React 自身实现了一套事件冒泡机制,所以这也就是为什么我们 event.stopPropagation()无效的原因。
  • React 通过队列的形式,从触发的组件向父组件回溯,然后调用他们 JSX 中定义的 callback
  • React 有一套自己的合成事件 SyntheticEvent

React中的事件绑定方式和区别

  • render方法中使用bind

    class App extends React.Component {
      handleClick() {
        console.log('this > ', this);
      }
      render() {
        return (
          <div onClick={this.handleClick.bind(this)}>test</div>
        )
      }
    }
    //这种方式在组件每次render渲染的时候,都会重新进行bind的操作,影响性能
    
  • render方法中使用箭头函数

    class App extends React.Component {
      handleClick() {
        console.log('this > ', this);
      }
      render() {
        return (
          <div onClick={e => this.handleClick(e)}>test</div>
        )
      }
    }
    
  • constructor中bind

    class App extends React.Component {
      constructor(props) {
        super(props);
        this.handleClick = this.handleClick.bind(this);
      }
      handleClick() {
        console.log('this > ', this);
      }
      render() {
        return (
          <div onClick={this.handleClick}>test</div>
        )
      }
    }
    
  • 定义阶段使用箭头函数绑定

    class App extends React.Component {
      constructor(props) {
        super(props);
      }
      handleClick = () => {
        console.log('this > ', this);
      }
      render() {
        return (
          <div onClick={this.handleClick}>test</div>
        )
      }
    }
    

区别:

  • 编写方面:方式一、方式二写法简单,方式三的编写过于冗杂
  • 性能方面:方式一和方式二在每次组件render的时候都会生成新的方法实例,性能问题欠缺。若该函数作为属性值传给子组件的时候,都会导致额外的渲染。而方式三、方式四只会生成一个方法实例

React中的key有什么作用

Vue一样,React 也存在 Diff算法,而元素key属性的作用是用于判断元素是新创建的还是被移动的元素,从而减少不必要的元素渲染,因此key的值需要为每一个元素赋予一个确定的标识

  • key 应该是唯一的
  • key不要使用随机值(随机数在下一次 render 时,会重新生成一个数字)
  • 使用 index 作为 key值,对性能没有优化

说说对React refs的理解

refs的使用方式

refs使用场景:

  • 对Dom元素的焦点控制、内容选择、控制
  • 对Dom元素的内容设置及媒体播放
  • 对Dom元素的操作和对组件实例的操作
  • 集成第三方 DOM 库

受控组件和非受控组件

受控组件,简单来讲,就是受我们控制的组件,组件的状态全程响应外部数据。受控组件我们一般需要初始状态和一个状态更新事件函数

非受控组件,简单来讲,就是不受我们控制的组件。一般情况是在初始化的时候接受外部数据,然后自己在内部存储其自身状态(当需要时,可以使用ref 查询 DOM并查找其当前值)

img

React引入CSS的方式

  1. 在组件内直接使用(组件中写style样式)

    这种方式优点:

    • 内联样式, 样式之间不会有冲突
    • 可以动态获取当前state中的状态

    缺点:

    • 写法上都需要使用驼峰标识
    • 某些样式没有提示
    • 大量的样式, 代码混乱
    • 某些样式无法编写(比如伪类/伪元素)
  2. 组件中引入 .css 文件

    这种方式存在不好的地方在于样式是全局生效,样式之间会互相影响

  3. 组件中引入 .module.css 文件

    css文件作为一个模块引入,这个模块中的所有css只作用于当前组件。不会影响当前组件的后代组件。这种方式是webpack特供的方案,只需要配置webpack配置文件中modules:true即可

    这种方式能够解决局部作用域问题,但也有一定的缺陷:

    • 引用的类名,不能使用连接符(.xxx-xx),在 JavaScript 中是不识别的
    • 所有的 className 都必须使用 {style.className} 的形式来编写
    • 不方便动态来修改某些样式,依然需要使用内联样式的方式;
  4. CSS in JS

    CSS-in-JS, 是指一种模式,其中CSSJavaScript生成而不是在外部文件中定义

    此功能并不是 React 的一部分,而是由第三方库提供,例如:

    • styled-components

      // style.js
      export const SelfLink = styled.div`
        height: 50px;
        border: 1px solid red;
        color: yellow;
      `;
      ​
      export const SelfButton = styled.div`
        height: 150px;
        width: 150px;
        color: ${props => props.color};
        background-image: url(${props => props.src});
        background-size: 150px 150px;
      `;
      
      import React, { Component } from "react";
      ​
      import { SelfLink, SelfButton } from "./style";
      ​
      class Test extends Component {
        constructor(props, context) {
          super(props);
        }  
       
        render() {
          return (
           <div>
             <SelfLink title="People's Republic of China">app.js</SelfLink>
             <SelfButton color="palevioletred" style={{ color: "pink" }} src={fist}>
                SelfButton
              </SelfButton>
           </div>
          );
        }
      }
      ​
      export default Test;
      
    • emotion

    • glamorous

四种方式的区别:

  • 在组件内直接使用css该方式编写方便,容易能够根据状态修改样式属性,但是大量的演示编写容易导致代码混乱
  • 组件中引入 .css 文件符合我们日常的编写习惯,但是作用域是全局的,样式之间会层叠
  • 引入.module.css 文件能够解决局部作用域问题,但是不方便动态修改样式,需要使用内联的方式进行样式的编写
  • 通过css in js 这种方法,可以满足大部分场景的应用,可以类似于预处理器一样样式嵌套、定义、修改状态等

React Router 有几种模式,实现原理

BrowserRouter => history模式 HashRouter => hash模式

  • hash 模式:在url后面加上#,如http://127.0.0.1:5500/home/#/page1
  • history 模式:允许操作浏览器的曾经在标签页或者框架里访问的会话历史记录

实现原理:路由描述了 URLUI之间的映射关系,这种映射是单向的,即 URL 变化引起 UI 更新(无需刷新页面)。下面以hash模式为例子,改变hash值并不会导致浏览器向服务器发送请求,浏览器不发出请求,也就不会刷新页。hash 值改变,触发全局 window 对象上的 hashchange 事件。所以 hash 模式路由就是利用 hashchange 事件监听 URL 的变化,从而进行 DOM 操作来模拟页面跳转。react-router也是基于这个特性实现路由的跳转

Immutable.js 的理解

Immutable,不可改变的,在计算机中,即指一旦创建,就不能再被更改的数据

Immutable对象的任何修改或添加删除操作都会返回一个新的 Immutable对象

Immutable 实现的原理是 Persistent Data Structure(持久化数据结构):

  • 用一种数据结构来保存数据
  • 当数据被修改时,会返回一个对象,但是新的对象会尽可能的利用之前的数据结构而不会对内存造成浪费

也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变,同时为了避免 deepCopy把所有节点都复制一遍带来的性能损耗,Immutable 使用了 Structural Sharing(结构共享)

如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享

immutable的基本操作

React diff原理

diff算法原理 三个层级

谈谈你对react的理解

概念:React是一个网页UI框架,通过组件化的方式解决视图层开发复用问题,本质上是一个组件化框架。React中基本单位是组件,React只有组件,没有页面,没有控制器也没用模型。 React只关心数据与组件 view = fn(props, state, context)。 React虚拟dom在适用场景上十分广泛

核心思路:

  1. 声明式 直观,可以做到一目了然也便于组合
  2. 组件化 降低系统间功能的耦合性,提高功能內部聚合性
  3. 通用性 虚拟dom可以使用于更多平台

React不是一揽子框架,需要更多第三方插件完善,在技术选型和学习上有比较高的成本

为什么React要用jsx

jsx是js的语法拓展或者说是类似于XML的的ECMAScript拓展

React本身并不强制使用jsx(使用createElement),React需要将组件转化为虚拟dom树,XML在树结构描述上天生具有可读性强的优势,jsx可以看作是createElement的语法糖

使用jsx是通过合理的关注点分离保持组件开发的纯粹性

模板(template) 方案对比 模板使关注度减弱、引入概念多

模板字符串 结构描述复杂 语法提示差

JXON 语法提示差

进阶提问:Babel插件如何将jsx转换为js的编译

image-20221223220803678

如何避免生命周期遇到的坑

“如何避免坑” => “踩过哪些坑”

“为什么会有坑” = 在不恰当的时机调用了不合适的代码 + 需要调用时忘记了调用

生命周期

  1. 周期梳理(react16.4)

    • 挂载/创建阶段

      • constructor
      • getDerivedStateFromProps
      • render
      • componentDidMount
    • 更新阶段

      • getDerivedStateFromProps
      • shouldComponentUpdate
      • render
      • getSnapshotBeforeUpdate
      • componentDidUpdate
    • 卸载阶段

      • componentWillUnmount
  2. 职责梳理

    • 状态变更
    • 错误处理

待完善...

类组件和函数组件的区别

使用方式和最终呈现的效果是一样的

本质上代表两种不同设计思想和心智模式

  • 类组件根基是OOP,面向对象编程
  • 函数式组件根基是FP, 函数式编程(更纯粹、简单、易测试)

在不使用Recompose或者Hooks的情况下,只能通过类组件使用生命周期

类组件可以实现继承,函数式组件缺少继承能力

性能优化

  • 类组件通过shouldComponentUpdate函数阻断渲染
  • 函数组件靠React.memo来优化

加入hooks后,函数式组件更适合未来趋势,类组件劣势在于 this的模糊性、业务逻辑散落在生命周期中

如何设计React组件(React组件的设计模式)

基于场景分类

无状态组件(哑组件/展示组件): 只作展示、独立运行、不额外增加功能的组件,复用性更强

  • 受制于外部的props控制
  • 具有极强的通用性、复用率极高
  • 代理组件、布局组件

有状态组件(灵巧组件): 处理业务逻辑和数据状态的组件,至少包含一个灵巧组件或展示组件,专注于业务本身

  • 容器组件
  • 高阶组件:React中服用组件逻辑的高级技术,是基于React组合特性形成的设计模式。高阶组件的参数是组件,返回值是新组件的函数

setState是同步更新还是异步更新

(“是A还是B?”,通常不同场景有不同答案)

合成事件:react给document挂上监听,DOM事件触发后冒泡到document,React找到相应组件造成一个合成事件,并按照组件树模拟一边事件冒泡

setState只是看起来像是异步执行,由isBatchingUpdates控制,为true则异步更新,为false则同步更新。isBatchingUpdates在react可以控制的地方为true

同步场景

原生事件中addEventListener、setTimeout、setInterval

异步场景 (需要保持內部(props )一致性,启用并发更新减少性能损耗)

react生命周期事件、合成事件

state = {count: 0}
componentDidMount() {
    this.setState({count: this.state.count + 1}); // this.state.count = 0
    consoloe.log(this.state.count);    // 0
    this.setState({count: this.state.count + 1}); // this.state.count = 0
    consoloe.log(this.state.count);    // 0
    setTimeout(()=>{
        this.setState({count: this.state.count + 1}); // this.state.count = 1
        consoloe.log(this.state.count); // 2
        this.setState({count: this.state.count + 1}); // this.state.count = 2
        consoloe.log(this.state.count); // 3
    }, 0)
}
​

React如何面向组件跨层级通信

  • 父与子

    props

  • 子与父

    父组件的回调函数通过props传递给子

  • 兄弟

    共同父组件中转

  • 无直接关系

    Context

Virtual Dom的工作原理

通过js对象模拟dom节点

React主要是组件实现、更新调度等

ReactDOM提供了在网页上渲染的基础,iOS、Android开发时通过React Native

虚拟dom的优势:大量直接操作DOM容易引起网页性能下降,React基于虚拟DOM的diff处理和批处理操作可降低DOM的操作范围和频次,提升页面性能。规避XSS攻击,跨平台成本更低

虚拟dom的缺点:内存占用高,无法极致优化

与其他框架相比,React的diff算法有何不同

文章地址

其他框架(Vue,类React框架如Preact、inferno等兼容React API的框架)

diff算法流程:真是DOM映射为虚拟DOM => 当虚拟DOM发生变化后根据差距计算生成patch(patch补丁是结构化的数据,包含新增、更新、移除等) => 更具patch更新真是DOM,反馈在界面上

  • 更新时机

  • 遍历算法

    React的diff算法采用深度优先遍历算法,保证生命周期时许不会错乱

  • 优化策略

    分治

    1. 忽略节点跨层级操作场景,提升对比效率

      需进行树比对,对树进行分层比较,两棵树只对同一层级节点比较,如发现节点不存在,则该节点及其子节点被完全删除

    2. 如果组件class一致,则默认为相似的树结构,否则默认为不同的树结构。同一类型进行树比对,不同类型直接放入补丁中

    3. 同一层级的子节点,可以通过标记key的方式进行列表对比。元素对比主要发生在同层级,通过标记节点操作(增加、移动、删除等)生成补丁,标记key,React可以直接移动DOM节点,降低内耗

    4. Fiber Fiber机制下节点和树采用FiberNode(双链表可直接找到兄弟节点和子节点)和FiberTree进行重构 Fiber机制下整个更新过程由current与workInProgress两株树双缓冲完成

Preact 没有生成patch的过程,直接更新DOM节点

Vue2.0使用snabbdom,整体和React相同,但在元素对比是,如果新旧是同一元素,且没有设置key,snabbdom在diff子元素中会一次性对比旧节点、新节点以及它们的首尾元素四个节点,以及验证列表是否有变化。

如何解释React的渲染流程

文章地址

关于渲染流程的知识点:React 渲染节点的挂载、React 组件的生命周期、setState 触发渲染更新、diff 策略与 patch 方案。你会发现渲染流程中包含的内容很繁杂,有各种大大小小需要处理的事,而这些事用计算机科学中的专业术语来说,就是事务事务是无法被分割的,必须作为一个整体执行完成,不可能存在部分完成的事务。所以这里需要注意,事务具有原子性,不可再分。

事务是通过调度的方式协调执行的

协调Reconciler 是协助 React 确认状态变化时要更新哪些 DOM 元素的 diff 算法。React 源码中还有一个叫作 reconcilers 的模块,它通过抽离公共函数与 diff 算法使声明式渲染、自定义组件、state、生命周期方法和 refs 等特性实现跨平台工作。

Reconciler 模块以 React 16 为分界线分为两个版本。

  • Stack Reconciler是 React 15 及以前版本的渲染方案,其核心是以递归的方式逐级调度栈中子节点到父节点的渲染。
  • Fiber Reconciler是 React 16 及以后版本的渲染方案,它的核心设计是增量渲染(incremental rendering),也就是将渲染工作分割为多个区块,并将其分散到多个帧中去执行。它的设计初衷是提高 React 在动画、画布及手势等场景下的性能表现。

Fiber 同样是一个借来的概念,在系统开发中,指一种最轻量化的线程。与一般线程不同的是,Fiber 对于系统内核是不可见的,也不能由内核进行调度。它的运行模式被称为协作式多任务,而线程采用的模式是抢占式多任务

  • 在协作式多任务模式下,线程会定时放弃自己的运行权利,告知内核让下一个线程运行;
  • 而在抢占式下,内核决定调度方案,可以直接剥夺长耗时线程的时间片,提供给其他线程

class组件中,事件中的this为什么是undefined

class组件中箭头函数可以获得this,标准函数不能获得this需要在constructor中bind绑定this

class中标准函数this与上下文有关,源码中定义时传值为undefined

合成事件

React基于浏览器的事件机制自身实现了一套事件机制,包括事件注册、事件的合成、事件冒泡、事件派发等,在React中这套事件机制被称之为合成事件。

与原生事件直接在元素上注册的方式不同的是,React的合成事件不会直接绑定到目标DOM节点上,用事件委托机制,以队列的方式,从触发事件的组件向父组件回溯,直到Root节点。因此,React组件上声明的事件最终绑定到了Root对象(React17之前是Document)上。在Root节点,用一个统一的监听器去监听,这个监听器上保存着目标节点与事件对象的映射。当组件挂载或卸载时,只需在这个统一的事件监听器上插入或删除对应对象;当事件发生时(即Root上的事件处理函数被执行),在映射里按照冒泡或捕获的路径去组件中收集真正的事件处理函数,然后,由这个统一的事件监听器对所收集的事件逐一执行。

这样做的好处:

• 对事件进行归类,可以在事件产生的任务上包含不同的优先级

• 提供合成事件对象,抹平浏览器的兼容性差异

• 减少内存消耗,提升性能,不需要注册那么多的事件了,一种事件类型只在 Root上注册一次

• 对开发者友好

函数式组件中如何实现forceUpdate

  1. const [, forceUpdate] = useReducer((x) => x + 1, 0)
  2. function useForceUpdate() {
      	const [state, setState] = useState(0);
        const update = useCallback(() => {
            setState((prev) => prev + 1);
        }, [])
        
        return update;
    }
    

React类组件的constructor为什么一定要使用super

super作为函数调用,代表父类的构造函数。es6中规定,子类构造函数必须要执行一次super函数,否则报错

super只能在派生类构造函数和静态方法中调用,

高阶组件和高阶函数

高阶函数的维基百科定义:至少满足以下条件之一:

  • 接受一个或多个函数作为输入;
  • 输出一个函数;

JavaScript中比较常见的filter、map、reduce都是高阶函数

高阶组件的英文是 Higher-Order Components,简称为 HOC;

官方的定义:高阶组件是参数为组件,返回值为新组件的函数;

我们可以进行如下的解析:

  • 首先, 高阶组件 本身不是一个组件,而是一个函数
  • 其次,这个函数的参数是一个组件返回值也是一个组件

高阶组件高阶函数应用

函数组件和类组件怎么选择

  • 函数组件颗粒度更小
  • 类组件有实例,函数组件没有,需要用到实例例如this首选类组件
  • 类组件通过hoc、render props容易形成嵌套地狱

函数组件setState没有回调函数怎么办

// 类组件
this.setState({count: count + 1}, () => {console.log(this.state.count)})

// 函数组件
const [state, setState] = React.useState(0)
useEffect(() => {
    console.log(state)
})

react.PureComponent

React.PureComponent 与 React.Component 几乎完全相同,但 React.PureComponent 通过props和state的浅对比来实现 shouldComponentUpate()。 文章

export default oneComponent extends Component {
  setCount = () => {
    this.setState({num: 100})
  }
  shouldComponentUpdate(nextProps, nextState) {
    return nextState.num !== this.state.num
  }
  render() {
    console.log('render')
    return <div>{this.state.num}</div>
  }
}

==> 
// 不需要亲自动手实现shouldComponentUpdate
export default oneComponent extends PureComponent {
  
}

浅对比:通过遍历对象上的键执行相等性,并在任何键具有参数之间不严格相等的值时返回false。 当所有键的值严格相等时返回true。shallowEqual

注意PureComponent只是对数据进行浅对比,如果存在obj或者复杂数据结构则不可用。 在PureComponent中,如果包含比较复杂的数据结构,可能会因深层的数据不一致而产生错误的否定判断,从而shouldComponentUpdate结果返回false,导致界面得不到更新

React异步加载

import ReactDOM from 'react-dom'
import React, {Component, lazy, Suspense}

const Sub = lazy(() => import('./Sub'))

class App extends Component {
    render() {
        return (
        	<div>
            	<Suspense fallback={ <div>loading</div> }> 
                	<Sub />
                </Suspense>
            </div>
        )
    }
}

React事件冒泡

react事件冒泡 踩坑

import React, { useEffect, useRef } from 'react';
export default function Demo() {
    const appRef = useRef();
    const btnRef = useRef();

    useEffect(() => {
        document.addEventListener('click', function () {
            console.log('document click')
        })
        btnRef.current.addEventListener('click', function () {
            console.log('btn click');
        })
        appRef.current.addEventListener('click', function () {
            console.log('app click');
        })
    }, []);

    function onBtnClick(e) {
        console.log('react button click');
    }

    function onAppClick(e) {
        console.log('react app click');
    }

    return (
        <div className="App" ref={appRef} onClick={onAppClick}>
            <button ref={btnRef} onClick={onBtnClick}>按钮</button>
        </div>
    )
}

// 打印结果
// btn click
// app click
// react button click
// react app click
// document click

//... react按钮点击事件上添加阻止冒泡
    function onBtnClick(e) {
        e.stopPropagation();
        console.log('react button click');
    }

// 打印结果
// btn click
// app click
// react button click
// document click
//...

//... 原生按钮点击事件上添加阻止冒泡
    btnRef.current.addEventListener('click', function (e) {
        e.stopPropagation();
        console.log('btn click');
    })

// 打印结果
// btn click
//...

react 事件中添加的阻止冒泡,只能阻止react 类的事件。

当按钮原生事件上阻止冒泡后,app 和 document上的原生事件无法触发是很容易理解的。然后,由于document上接收不到 按钮原生事件的冒泡,所以react的事件代理机制就失效了,进而导致react绑定的 app click btn click事件都无法触发了。

解决方案

  1. 通过原生事件阻止冒泡,阻止 document 原生事件;

    btnRef.current.addEventListener('click', function (e) {
    	e.stopPropagation();
        console.log('btn click');
    })
    
  2. 在react事件中调用 e.nativeEvent.stopImmediatePropagation() stopImmediatePropagation 会阻止元素上所有同类型的事件监听器调用, 具体参考mdn。 这种方式要求用户在document上绑定的事件 要晚于react render

       function onBtnClick(e) {
            // e.stopPropagation();
            e.nativeEvent.stopImmediatePropagation();
            console.log('react button click');
        }
        
        <button ref={btnRef} onClick={onBtnClick}>按钮</button>
    
  3. 在window 上绑定事件 因为window上事件的冒泡顺序位于document之后,所以react 事件中的e.stopPropagation阻止了react在document上的代理事件的冒泡,进而不会触发window上的事件

React优先级

每次setState会触发重新渲染,就会启动上面链接提到的render+commit渲染流程,应用可能会同时调用很多次setState,那怎么去处理他们的调用顺序

react模式

  • legacy 模式: ReactDOM.render(<App />, rootNode)。这是当前 React 18之前使用的方式。当前没有计划删除本模式,但是这个模式可能不支持这些新功能。
  • concurrent 模式: ReactDOM.createRoot(rootNode).render(<App />)。 React 18的默认开发模式。这个模式开启了所有的新功能。(并发模式)

同步流程

多次的setState按顺序处理,分多次进行render+commit渲染流程,如果input输入过程中,input关联的state的变化引起了其他state的变化,两个 setState 一起发生,如果这个渲染流程中处理的 fiber 节点比较多,渲染一次就比较慢,这时候用户输入的内容可能就不能及时的渲染出来,用户就会感觉卡,体验不好。可能会影响到input输入的体感,会产生卡顿的感觉(这个我就有真实遇到过,后来是把某个颗粒化的列表元素组件进行了useCallback+React.memo缓存,减少了每次渲染处理的fiber节点)

并发流程

  • 同步模式是循环处理 fiber 节点,并发模式多了个 shouldYield 的判断,每 5ms 打断一次,也就是时间分片。并且之后会重新调度渲染。通过这种打断和恢复的方式实现了并发。
  • 然后 Scheduler 可以根据优先级来对任务排序,这样就可以实现高优先级的更新先执行。
  • 每次 setState 引起的渲染都是由 Scheduler 调度执行的,它维护了一个任务队列,上个任务执行完执行下个,被打断的任务会添加到Scheduler的任务队列里面
function workLoop() {
// 每处理一个fiber节点,都判断下是否打断
**// shouldYiled 方法就是判断待处理的任务队列有没有优先级更高的任务,有的话就先处理那边的 fiber,这边的先暂停一下。
//shouldYiled根据过期时间,每次开始处理时记录个时间,如果处理完这个 fiber 节点,时间超了,那就打断。这个就是时间分片
//优先级高低会影响 Scheduler 里的 taskQueue 的排序结果,但打断只会根据过期时间。**
  while (wip && shouldYield()) {
    performUnitOfWork();
  }

  if (!wip && wipRoot) {
    commitRoot();
  }
}
// workInProgress记录每个workLoop处理的fiber节点,然后根据workInProgress是否是null判断是否全部 fiber 节点都渲染完
// 根据wip 是不是 null判断是不是中断(中断的话wip不为null)

优先级调度

Schedular的优先级:Immediate 是离散的一些事件,比如 click、keydown、input 这种。

UserBlocking 是连续的一些事件,比如 scroll、drag、mouseover 这种。

然后是默认的优先级 NormalPriority、再就是低优先级 LowPriority,空闲优先级 IdlePriority。

Untitled.png

react 内部有 31 种 Lane 优先级,但是调度 Scheduler 任务的时候,会先转成事件优先级,然后再转成 Scheduler 的 5 种优先级。

  • 采用Lane 优先级的原因是因为,二进制位运算比较快

  • 然后转换成事件优先级是因为能和Scheduler 的 5 种优先级对应上

  • 为什么不直接转换成Schduler的五种优先级呢?

    • 因为 Schduler 是分离的一个包了,它的优先级机制也是独立的,而lane优先级和事件优先级都是react自己的一套优先级机制

总结

  • 同步模式就是按顺序依次渲染
  • 并发模式就是在 workLoop 里通过 shouldYield 的判断来打断渲染,之后把剩下的节点加入 Schedule 调度,来恢复渲染。
  • 时间分片的 workLoop + 优先级调度,这就是 React 并发机制的实现原理。这就是 React 并发机制的实现原理

合理拆分组件

对下面的组件进行性能优化

export default function App() {
    let [color, setColor] = useState('blue')
    return (
    	<div>
        	<input value={color} onChange={e => setColor(e.target.value) } />
            <p style={{color}}>hello world</p>
            <ExpensiveTree />
        </div>
    )
}

function ExpensiveTree() {
    let now = performance.now()
    while(performance.now() - now < 100) {
        // Artificial delay -- do nothing for 100ms
    }
    console.log('ExpensiveTree执行了')
    return <p>这是一个渲染非常耗时的组件</p>
}

//let ExpensiveTree = memo(() => {
//    let now = performance.now()
//    while(performance.now() - now < 100) {
//        // Artificial delay -- do nothing for 100ms
//    }
//    console.log('ExpensiveTree执行了')
//    return <p>这是一个渲染非常耗时的组件</p>
//})

第一想法可能是通过memo缓存ExpensiveTree组件, 但是可以通过合理的组件拆分达到效果不需要使用memo。

ExpensiveTree并不使用到color,可以将使用到color的地方重新拆分成新的组件,与ExpensiveTree形成兄弟组件

export default function App() {
   
    return (
    	<div>
        	<Form />
            <ExpensiveTree />
        </div>
    )
}

function ExpensiveTree() {
    let now = performance.now()
    while(performance.now() - now < 100) {
        // Artificial delay -- do nothing for 100ms
    }
    console.log('ExpensiveTree执行了')
    return <p>这是一个渲染非常耗时的组件</p>
}

function Form() {
     let [color, setColor] = useState('blue')
     return (
     	<input value={color} onChange={e => setColor(e.target.value) } />
        <p style={{color}}>hello world</p>
     )
}

如果父标签中也使用到color,可以通过组件的Props获取子组件children,children从保存上一次外部获取的属性不会重新渲染

export default function App() {
    return (
    	<colorPicker>
            <ExpensiveTree />
        </colorPicker>
    )
}

function ExpensiveTree() {
    let now = performance.now()
    while(performance.now() - now < 100) {
        // Artificial delay -- do nothing for 100ms
    }
    console.log('ExpensiveTree执行了')
    return <p>这是一个渲染非常耗时的组件</p>
}

function colorPicker({children}) {
    let [color, setColor] = useState('blue')
    return (
    	<div style={{color}}>
        	<input value={color} onChange={e => setColor(e.target.value) } />
            <p style={{color}}>hello world</p>
            {children}
        </div>
    )
}

hooks

useState 和 useReducer为什么返回一个数组而不是对象

返回对象则key值固定,变量值不能自定义命名,使用数组可以重复自定命名值

hook解决了哪些问题/hook的好处

useEffect 和 useLayoutEffect 的区别

共同点

  • useEffect 的函数签名与 useLayoutEffect 相同(源码中调用了相同的函数,useEffect 先调用了 mountEffect 再调用 mountEffectImpl; useLayoutEffect 先调用了 mountEffectImpl再调用 mountEffect)
  • 都是处理副作用

不同点

  • 如果有直接操作dom样式或引起dom样式更新的场景推荐useLayoutEffect
  • useEffect 是异步处理副作用;useLayoutEffect 是同步处理副作用

hooks的使用限制

  • 不要在循环、条件或嵌套函数中调用hook,只能在函数最顶层调用(保证顺序稳定性)
  • 在React函数组件中调用hook

原因:hooks是作为一个单链表存储在fiber.memoizedState(普通函数没有dom节点,没有fiber)上的,因为这些hook没有名字,为了区分它们,我们必须保证链表节点顺序稳定

防范措施:在ESlint中引入eslint-plugin-react-hooks完成自动化检查

useState

const [count, setCount] = useState(6) // 每次更新页面都执行
const [count, setCount] = useState(() => {return 6}) // 只执行第一次渲染
const [count, setCount] = useState(6)

fun() {
    setCount(count - 1)  // count=6 , return 5
    setCount(count - 1)  // count=6 , return 5
}

fun1() {
    setCount(preCount => preCount - 1)  // count=6 , return 5
    setCount(preCount => preCount - 1)  // count=5 , return 4
}

useRef 和 useState

  1. useState的值在每个rernder中都是独立存在的。而useRef.current则更像是相对于render函数的一个全局变量,每次他会保持render的最新状态。(useState异步更新其值,useRef同步更新。)
  2. useState触发重新渲染,useRef不触发
  3. useRef 钩子不仅用于DOM引用。“ ref”对象是通用容器,其当前属性是可变的,并且可以保存任何值,类似于类的实例属性。
  4. 变量是决定视图图层渲染的变量,请使用useState,其他用途useRef
  5. useRef特性:可变的ref对象,持久化 ———————————————— 原文链接:blog.csdn.net/qq_44864082…

useEffect有依赖项和没有依赖项时return内外的代码执行顺序

文章地址

总结:

  1. 无依赖项时,首次加载会执行useEffect第一个参数函数的return外的部分,每次更新时会先执行return内部分,再执行return外的部分。
  2. 依赖项为空数组([])时,会在页面首次加载时运行useEffect 第一个参数的那个函数,类似于执行componentDidMount,且只执行一遍,函数内return 的函数会在页面即将销毁时或移除组件时执行,类似于执行componentWillUnMount
  3. 依赖项不为空时,首次加载会执行useEffect第一个参数函数的return外的部分,每次依赖项更新时会先执行return内部分,再执行return外的部分。

也就是说,除了初次挂载和移除组件时会单独执行return外和return内的函数,其余更新的时候都会依次执行return内再执行return外的部分。

生命周期

useEffect

React的Class Component中有 componentDidMountcomponentDidUpdatecomponentWillUnmount,但Function Component并没有

A、componentDidMount

useEffect(()=>{
  console.log('componentDidMount')
}, [])	// 空数组表示不检测任何数据变化
 

B、comopnentDidUpdate

useEffect(()=>{
  console.log('comopnentDidUpdate')
}, [num])	// 如果数组中包含了所有页面存在的字段,也可以直接不写

如果监听路由的变化:

// 需要先安装路由,而且是react-router-dom@v6.x
useEffect(()=>{
  console.log('路由变化')
}, [location.pathname])

C、componentWillUnmount

useEffect(()=>{
  return ()=>{
    // callback中的return代表组件销毁时触发的事件
  }
}, [])

memo、useMemo与useCallback

useMemo 与 useCallback 区别及使用场景

useMemo 和 useCallback|React.memo使用场景

react | memo与useMemo的作用以及区别

useEffect、useMemo、useCallback使用场景分析

  1. useCallback 和 useMemo 的参数跟 useEffect 一致,他们之间最大的区别有是 useEffect 会用于处理副作用,而前两个hooks不能。
  2. useCallback 和 useMemo 都会在组件第一次渲染的时候执行,之后会在其依赖的变量发生改变时再次执行。 并且这两个hooks都返回缓存的值,useMemo 返回缓存的 变量,useCallback 返回缓存的 函数。如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。
  3. React.useMemo将调用fn函数并返回其结果,而React.useCallback将返回fn函数而不调用它:useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。
  4. useMemo缓存的变量所代表的意义不是组件的state,组件的state改变会触发组件重渲染,而useMemo缓存的变量可能会因state改变而改变,是“被动的”;
  • useCallback 对于子组件渲染优化,当做函数传递时候。
  • useMemo 对于当前组件高开销的计算优化。

在Function Component中,也不再区分mountupdate两个状态,这意味着函数组件的每一次调用都会执行内部的所有逻辑,就带来了非常大的性能损耗。useMemouseCallback都是解决上述性能问题的。

来看下面这段代码:

import React, { useState, useMemo, useCallback } from "react";

const Sub = () => {
  console.log("Sub被渲染了");	// 这行代码在父组件App2更新时,它也被迫一直更新
  return <h3>Sub组件</h3>;
};

export default function App2() {
  const [num, setNum] = useState<number>(0);

  const changeNum = () => setNum(num + 1)

  return (
    <div>
      <h2>num的值:{num}</h2>
      <button onClick={changeNum}>累加num</button>
      <Sub />
    </div>
  );
}
    

以上代码中可以测试出来,Sub组件的 console.log 在App2组件更新时,一直被迫触发,这就是典型的性能浪费。

#A. memo

使用memo这个hook可以解决这一问题:

import React, { useState, memo } from "react";

// Sub组件需要被memo包裹
const Sub = memo(() => {
    console.log("Sub被渲染了");
    return <h3>Sub组件</h3>;
  });

export default function App2() {
  const [num, setNum] = useState<number>(0);
    
  const changeNum = () => setNum(num + 1)

  return (
    <div>
      <h2>num的值:{num}</h2>
      <button onClick={changeNum}>累加num</button>
      <Sub />
    </div>
  );
}
    

memo可以缓存组件,当组件的内容不受修改时,可以不更新该组件。

#B. useCallback(与useMemo作用类似)

但我们希望num的变化不造成Sub组件的更新:

import React, { useState, memo, useCallback } from "react";

interface ISubProps {
  changeNum: () => void;
}

// Sub组件需要被memo包裹
const Sub = memo((props: ISubProps) => {
  console.log("Sub被渲染了");
  return (
    <>
      <button onClick={props.changeNum}>累加num</button>
      <h3>Sub组件</h3>
    </>
  );
});

export default function App2() {
  const [num, setNum] = useState<number>(0);

  // 将这个changeNum函数使用useCallback包裹一次
  const changeNum = useCallback(()=>{
      setNum((num)=>num+1)
  }, [])

  return (
    <div>
      <h2>num的值:{num}</h2>
      <Sub changeNum={changeNum} />
    </div>
  );
}
    

#C. useMemo(类似vue中的computed)

useMemo与useCallback大致相同,只是useMemo需要在回调函数中再返回一个函数,我们称之为高阶函数:

import React, { useState, memo, useMemo } from "react";

interface ISubProps {
  changeNum: () => void;
}

// Sub组件需要被memo包裹
const Sub = memo((props: ISubProps) => {
  console.log("Sub被渲染了");
  return (
    <>
      <button onClick={props.changeNum}>累加num</button>
      <h3>Sub组件</h3>
    </>
  );
});

export default function App2() {
  const [num, setNum] = useState<number>(0);

  // 将这个changeNum函数改为useMemo
  const changeNum = useMemo(() => {
    return () => setNum((num) => num + 1);
  }, []);

  return (
    <div>
      <h2>num的值:{num}</h2>
      <Sub changeNum={changeNum} />
    </div>
  );
}
 

提升性能(减少render次数)

image-20221207152439073

React effect hooks各种使用场景

React effect hooks各种使用场景

Router

React学习之——Router(v6)

函数式组件性能优化

React学习之——函数式组件性能优化

Redux

React学习之——Redux

hooks常见使用错误

All useEffect Mistakes Every Junior React Developer Makes

Fiber