面试:React相关

文章目录

  • React
    • 简单介绍一下React
    • 使用React开发和原生开发的区别,为什么React效率高
    • 虚拟dom
      • 优缺点
      • 创建虚拟DOM的两种方式
    • diff算法
      • 1.tree diff
      • 2.component diff
      • 3.element diff
      • 总结
    • react和react-dom的区别
      • react API
        • 组件
        • 创建 React 元素
        • 转换元素
        • Fragments
        • Refs
        • Hooks
      • ReactDOM API
    • 生命周期
      • class生命周期
        • getDerivedStateFromProps
      • Hooks生命周期
    • react + redux
      • 概念
      • 解决什么问题
      • 使用场景
      • Redux 的特点
      • store改变重新渲染view
      • redux中间件
        • redux异步数据流
        • redux-thunk
        • redux-saga
          • take/takeEvery/takeLatest
          • delay
          • call/fork
          • put/select
          • cancel/cancelled
        • 区别
        • redux-saga的优缺点
        • redux-thunk的优缺点
      • connect方法
        • mapStateToProps
        • mapDispatchToProps
    • hooks
      • react是怎么保证多个useState的相互独立的?
      • hook如何保存数据
        • 多个hook如何获取数据
      • 使用规则
      • useState
        • 原理 [demo](https://codesandbox.io/s/usestate-xc6xh?file=/src/myUseState.js)
        • useState和setState区别
      • useEffect
        • 第二个参数的三种情况
      • 执行时机
      • useLayoutEffect
      • useContext
        • 问题
      • useReducer
        • useReducer + useContext代替Redux
      • useCallback / useMemo
        • 和React.memo区别
      • Redux hooks
      • react-router-dom hooks
      • useRef
        • useRef 的特点
        • 和createRef区别
        • 使用场景
        • 和普通global变量存储的区别
      • hooks实现getDerivedStateFromProps
    • hooks优缺点
      • 优点
      • 缺点
      • 和普通函数的区别
      • 和类组件的区别
    • React Router
    • 组件传值
      • 父子组件
        • 父传子:props
        • 子传父:传函数改参数
      • useRef/useImperativeHandle
      • 发布订阅者模式
      • redux/react-redux
      • EventBus
      • context/useContext钩子(上下文)
      • 事件冒泡
    • 父组件更新会导致子组件更新吗
    • Component和pureComponent
      • 原理
      • 优势
      • 缺点
    • React性能优化
    • 高阶组件
      • 基本原理
      • 高阶组件的意义
      • 使用场景
      • 问题
      • 高阶组件的约定/使用需要注意什么
    • 渲染属性Render Props
      • 优势
      • 问题
    • fiber
      • 起源/含义
      • 数据结构
      • fiber树
      • 工作原理
        • 挂起
        • 恢复
        • 终止
    • 合成事件和原生事件的区别
    • 16和15的区别
      • 生命周期
      • stack到fiber
    • props和state区别
    • class组件/函数组件的问题
      • class 组件存在的问题

React

简单介绍一下React

  • react是一个用于构建用户界面的JS库,遵循组件化的设计模式。
  • 通过render方法接收输入的数据,并返回要展示的内容

特性:

  • jsx语法
  • 单向数据绑定
  • 虚拟DOM
  • 声明式编程
  • Component

使用React开发和原生开发的区别,为什么React效率高

React(一):React的特征及优势

  1. JSX
    JSX(JavaScript XML)是js内定义的一套XML语法,最终被解析成js。在JSX中可以将HTML于JS混写。
ReactDOM.render(
     <div>
        <h1>{1+1}</h1>     //JavaScript表达式由{}表示
     </div> ,
     document.getElementById('example')
);
  1. 虚拟DOM,高效速度快,跨浏览器兼容
    React之所以速度快,是因为其独特的特征——虚拟DOM(Document Object Model)。为什么直接操作DOM很慢,而虚拟DOM很快,和浏览器的渲染机制有关。

  2. 组件
    组件是react的核心,一个完整的react应用是由若干个组件搭建起来的,每个组件有自己的数据和方法,组件具体如何划分,需要根据不同的项目来确定。
    组件的特征是可复用,可维护性高。

虚拟dom

虚拟DOM可以看做一棵模拟了DOM树的JavaScript对象树。
面试:React相关_第1张图片
既然原来 DOM 树的信息都可以用 JavaScript 对象来表示,反过来,你就可以根据这个用 JavaScript 对象表示的树结构来构建一棵真正的DOM树。

状态变更->重新渲染整个视图的方式可以稍微修改一下:用 JavaScript 对象表示 DOM 信息和结构,当状态变更的时候,重新渲染这个 JavaScript 的对象结构。当然这样做其实没什么卵用,因为真正的页面其实没有改变。

但是可以用新渲染的对象树去和旧的树进行对比,记录这两棵树差异。记录下来的不同就是我们需要对页面真正的 DOM 操作,然后把它们应用在真正的 DOM 树上,页面就变更了。

这样就可以做到:视图的结构确实是整个全新渲染了,但是最后操作DOM的时候确实只变更有不同的地方。

这就是所谓的 Virtual DOM 算法。包括几个步骤:

  1. 用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树,插到文档当中
  2. 当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异
  3. 把2所记录的差异应用到步骤1所构建的真正的DOM树上,视图就更新
    面试:React相关_第2张图片
    Virtual DOM 本质上就是在 JS 和 DOM 之间做了一个缓存。 可以类比 CPU 和硬盘,既然硬盘这么慢,我们就在它们之间加个缓存:既然 DOM 这么慢,我们就在它们 JS 和 DOM 之间加个缓存。CPU(JS)只操作内存(Virtual DOM),最后的时候再把变更写入硬盘(DOM)

优缺点

  • 优点:
  1. 它打开了函数式的UI编程的大门,即UI = f(data)这种构建UI的方式。 我们不再需要手动去操作 DOM,只需要写好 View-Model 的代码逻辑,框架会根据虚拟 DOM 和 数据双向绑定,帮我们以可预期的方式更新视图,极大提高我们的开发效率;

  2. 可以将JS对象渲染到浏览器DOM以外的环境中,也就是支持了跨平台开发,比如ReactNative。

  3. 保证性能下限: 框架的虚拟 DOM 需要适配任何上层 API 可能产生的操作,它的一些 DOM 操作的实现必须是普适的,所以它的性能并不是最优的;但是比起粗暴的 DOM 操作性能要好很多,因此框架的虚拟 DOM 至少可以保证在你不需要手动优化的情况下,依然可以提供还不错的性能,即保证性能的下限;

  • 真实DOM的属性很多,创建DOM节点开销很大

  • 虚拟DOM只是普通JavaScript对象,描述属性并不需要很多,创建开销很小

  • 复杂视图情况下提升渲染性能(操作dom性能消耗大,减少操作dom的范围可以提升性能)

  • 缺点:
    无法进行极致优化: 虽然虚拟 DOM + 合理的优化,足以应对绝大部分应用的性能需求,但在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化。
    首次渲染大量DOM时,由于多了一层虚拟DOM的计算,会比innerHTML插入慢。

创建虚拟DOM的两种方式

(1)纯js(一般不用)

React.createElement('h1', {id:'myTitle'}, title)

(2)JSX
页面结构:

<h1 id='myTitle'>{title}h1>
<div id='example1'>div>
<div id='example2'>div>

JS:
面试:React相关_第3张图片
ReactDOM.render()即为react项目中index.js中的:

ReactDOM.render(<App />, document.querySelector('#root'));

而app.js一般汇总了各个模块,组成一棵完整的DOM树。

diff算法

多点diff更新的JSX对象 newChildren为数组形式,但是和newChildren中每个组件进行比较的是current fiber,同级的Fiber节点是由sibling指针链接形成的单链表,即不支持双指针遍历。
即 newChildren[0]与fiber比较,newChildren[1]与fiber.sibling比较。

当Node节点的更新,虚拟DOM会比较两棵DOM树的区别,保证最小化的DOM操作,使得执行效率得到保证。

diff过程整体策略:深度优先,同层比较

计算两棵树的常规算法是O(n^3)级别,所以需要优化深度遍历(先序) 的算法。React diff算法的时间复杂度为O(n)。

diff 策略

  1. Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计。

  2. 拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构。

  3. 对于同一层级的一组子节点,它们可以通过唯一 id 进行区分。

React 分别对 tree diffcomponent diff 以及 element diff 进行算法优化。

在实际的代码中,会对新旧两棵树进行一个深度优先的遍历,这样每个节点都会有一个唯一的标记
面试:React相关_第4张图片
在深度优先遍历的时候,每遍历到一个节点就把该节点和新的的树进行对比。如果有差异的话就记录到一个对象里面。

1.tree diff

DOM 节点跨层级的移动操作少到可以忽略不计,针对这一现象,

React 通过 updateDepth 对 Virtual DOM 树进行层级控制,只会对相同颜色方框内的 DOM 节点进行比较,即同一个父节点下的所有子节点。

当发现节点已经不存在,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。

这样只需要对树进行一次遍历,便能完成整个 DOM 树的比较。
面试:React相关_第5张图片

2.component diff

React 是基于组件构建应用的,对于组件间的比较所采取的策略也是简洁高效。

  1. 如果是同一类型的组件,按照原策略继续比较 virtual DOM tree。
  2. 如果不是,则将该组件判断为 dirty component,从而替换整个组件下的所有子节点。
  3. 对于同一类型的组件,有可能其 Virtual DOM 没有任何变化,如果能够确切的知道这点那可以节省大量的 diff 运算时间,因此 React 允许用户通过shouldComponentUpdate() 来判断该组件是否需要进行 diff。

如下图,当 component D 改变为 component G 时,即使这两个 component 结构相似,
一旦 React 判断 D 和 G 是不同类型的组件,就不会比较二者的结构,而是直接删除 component D,重新创建 component G 以及其子节点

虽然当两个 component 是不同类型但结构相似时,React diff 会影响性能,但正如 React 官方博客所言:不同类型的 component 是很少存在相似 DOM tree 的机会,因此这种极端因素很难在实现开发过程中造成重大影响的。
面试:React相关_第6张图片

3.element diff

当节点处于同一层级时,React diff 提供了三种节点操作,分别为:

INSERT_MARKUP(插入)、MOVE_EXISTING(移动)和 REMOVE_NODE(删除)。

  • INSERT_MARKUP,新的 component 类型不在老集合里, 即是全新的节点,需要对新节点执行插入操作。

  • MOVE_EXISTING,在老集合有新 component 类型,且 element 是可更新的类型,generateComponentChildren 已调用 receiveComponent,这种情况下 prevChild=nextChild,就需要做移动操作,可以复用以前的 DOM 节点。

  • REMOVE_NODE,老 component 类型,在新集合里也有,但对应的 element 不同则不能直接复用和更新,需要执行删除操作,或者老 component 不在新集合里的,也需要执行删除操作。

面试:React相关_第7张图片

总结

  • React 通过制定大胆的 diff 策略,将 O(n3) 复杂度的问题转换成 O(n) 复杂度的问题;

  • React 通过分层求异的策略,对 tree diff 进行算法优化;

  • React 通过相同类生成相似树形结构,不同类生成不同树形结构的策略,对 component diff 进行算法优化;

  • React 通过设置唯一 key的策略,对 element diff 进行算法优化;

react和react-dom的区别

React 在v0.14之前是没有 ReactDOM 的,所有功能都包含在 React 里。从v0.14(2015-10)开始,React 才被拆分成React 和 ReactDOM。为什么要把 React 和 ReactDOM 分开呢?因为有了 ReactNative。React 只包含了 Web 和 Mobile 通用的核心部分,负责 Dom 操作的分到 ReactDOM 中,负责 Mobile 的包含在 ReactNative 中。

  • ReactDOM 只做和浏览器或DOM相关的操作,例如:ReactDOM.render() 和 ReactDOM.findDOMNode()。如果是服务器端渲染,可以 ReactDOM.renderToString()。
  • React 不仅能通过 ReactDOM 和Web页面打交道,还能用在服务器端SSR,移动端ReactNative和桌面端Electron。

react API

组件

  • React.Component
  • React.PureComponent
  • React.memo

创建 React 元素

强烈建议使用 JSX 来编写你的 UI 组件。因为每个 JSX 元素都是调用 React.createElement() 的语法糖。

转换元素

React 提供了几个用于操作元素的 API:

  • cloneElement()
  • isValidElement()
  • React.Children

Fragments

React 还提供了用于减少不必要嵌套的组件。

  • React.Fragment

Refs

  • React.createRef
  • React.forwardRef

Hooks

ReactDOM API

react-dom 的 package 提供了可在应用顶层使用的 DOM(DOM-specific)方法,如果有需要,你可以把这些方法用于 React 模型以外的地方。不过一般情况下,大部分组件都不需要使用这个模块。

  • render()
  • hydrate()
  • unmountComponentAtNode()
  • findDOMNode()
  • createPortal()

生命周期

React的生命周期从广义上分为三个阶段:挂载、渲染、卸载

因此可以把React的生命周期分为两类:挂载卸载过程和更新过程

面试:React相关_第8张图片
面试:React相关_第9张图片

React的生命周期

面试:React相关_第10张图片
react的生命周期函数(超详细)

class生命周期

getDerivedStateFromProps

在源码里Mount时和Update时都会触发,并且执行时机是同步的,在源码里就是简单的值的修改,所以也不会发起新的更新。

Hooks生命周期

Hooks 与 React 生命周期的关系

react + redux

Redux 入门教程(一):基本用法

Redux 入门教程(二):中间件与异步操作

Redux 入门教程(三):React-Redux 的用法
面试:React相关_第11张图片
面试:React相关_第12张图片

概念

面试:React相关_第13张图片

  • Action Creator:View 要发送多少种消息,就会有多少种 Action。如果都手写,会很麻烦。可以定义一个函数来生成 Action,这个函数就叫 Action Creator。
  • Action:Action 是一个对象。其中的type属性是必须的,表示 Action 的名称。根据传入的参数执行一些操作,然后为这些操作赋予一个类型,最后把操作执行后的结果传给dispacher,
  • Dispatcher:接受一个 Action 对象作为参数,将它发送出去。将action的结果发送到Store
  • Store: 用于存储状态,Store 就是保存数据的地方,你可以把它看成一个容器。整个应用只能有一个 Store。
  • State:如果想得到某个时点的数据,就要对 Store 生成快照。这种时点的数据集合,就叫做 State。
  • View:用户视图,用于产生action和接收渲染store中状态的改变
  • Reducer:Store 收到 Action 以后,必须给出一个新的 State,这样 View 才会发生变化。这种 State 的计算过程就叫做 Reducer。
    Reducer 是一个函数,它接受 Action 和当前 State 作为参数,返回一个新的 State。

面试:React相关_第14张图片

  1. 在Redux中,状态也存储在store中。存储的状态通过 actions改变。 Action 是对象,它至少有一个字段(type)确定操作的类型。
  2. Action 对应用程序状态的影响是通过使用一个 reducer 来定义的。 实际上, reducer 是一个纯函数,它以当前状态(state)和 action 为参数。 它返回一个基于传入的action的新状态(state)。state的状态不应该被修改,所以返回的state值应该是一个新创建的state。
  3. Reducer 不直接从应用程序中调用。 Reducer 只作为创建store,即 createStore 的一个参数给出
  4. store 现在使用 reducer 来处理actions,这些action通过 dispatch方法 被分派或“发送”到 store 中。
  5. store发生改变之后,由react来将改变渲染到页面上(View)

Action 对应用程序状态的影响是通过使用一个 reducer 来定义的。 实际上,reducer 是一个函数,它以当前状态和 action 为参数。 它返回一个新的状态
面试:React相关_第15张图片
多个reducer可以被结合到一起,在结合的时候为每个reducer创建一个标识符,在action中可以使用该标识符获取对应reducer的state(即数据)。
面试:React相关_第16张图片

解决什么问题

redux是为了解决react组件间通信和组件间状态共享而提出的一种解决方案

1、组件间通信
由于connect后,各connect组件是共享store的,所以各组件可以通过store来进行数据通信,当然这里必须遵守redux的一些规范,比如遵守 view -> aciton -> reducer的改变state的路径

2、通过对象驱动组件进入生命周期
对于一个react组件来说,只能对自己的state改变驱动自己的生命周期,或者通过外部传入的props进行驱动。通过redux,可以通过store中改变的state,来驱动组件进行update

3、方便进行数据管理和切片
redux通过对store的管理和控制,可以很方便的实现页面状态的管理和切片。通过切片的操作,可以轻松的实现redo之类的操作

使用场景

  • 同一个 state 需要在多个 Component 中共享
  • 需要操作一些全局性的常驻 Component,比如 Notifications,Tooltips 等
  • 太多 props 需要在组件树中传递,其中大部分只是为了透传给子组件
  • 业务太复杂导致 Component 文件太大,可以考虑将业务逻辑拆出来放到 Reducer 中

Redux 的特点

  • 单向数据流。View 发出 Action (store.dispatch(action)),Store 调用 Reducer 计算出新的 state ,若 state 产生变化,则调用监听函数重新渲染 View (store.subscribe(render)
  • 单一数据源,只有一个 Store
  • state 是只读的,每次状态更新之后只能返回一个新的 state
  • 没有 Dispatcher ,而是在 Store 中集成了 dispatch 方法,store.dispatch() 是 View 发出 Action 的唯一途径
  • 支持使用中间件(Middleware)管理异步数据流

store改变重新渲染view

State 一旦有变化,Store 就会调用监听函数。

// 设置监听函数
store.subscribe(listener);

listener可以通过store.getState()得到当前状态。如果使用的是 React,这时可以通过component.setState()触发重新渲染 View。

function listerner() {
  let newState = store.getState();
  component.setState(newState);   
}

redux中间件

redux异步数据流


面试:React相关_第17张图片

redux-thunk

它允许我们创建asynchronous actions。Redux-thunk 是所谓的redux-中间件,它必须在store的初始化过程中初始化。

import { createStore, combineReducers, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import { composeWithDevTools } from 'redux-devtools-extension'

import noteReducer from './reducers/noteReducer'
import filterReducer from './reducers/filterReducer'

const reducer = combineReducers({
  notes: noteReducer,
  filter: filterReducer,
})

const store = createStore(
  reducer,
  composeWithDevTools(
    applyMiddleware(thunk)
  )
)

export default store

redux-thunk主要的功能就是可以让我们dispatch一个函数,而不只是普通的 Object。

通过 redux-thunk,可以定义action creators,这样它们就可以返回一个函数,其参数是 redux-store 的dispatch-method。 因此,可以创建异步action创建器,它们首先等待某个action完成,然后分派真正的action

export const initializeNotes = () => {
  return async dispatch => {
    const notes = await noteService.getAll()
    dispatch({
      type: 'INIT_NOTES',
      data: notes,
    })
  }
}

首先执行一个异步操作,然后调度改变store态的action。

redux-saga

一个 saga 就像是应用程序中一个单独的线程,它独自负责处理副作用。 redux-saga 是一个 redux 中间件,意味着这个线程可以通过正常的 redux action 从主应用程序启动,暂停和取消,它能访问完整的 redux state,也可以 dispatch redux action。

使用流程:
在sagas中定义saga:

import { delay } from 'redux-saga'
import { put, takeEvery, all, call } from 'redux-saga/effects'

export function* helloSaga() {
    console.log('Hello Sagas!');
}

// Our worker Saga: 将执行异步的 increment 任务
// Sagas 被实现为 Generator functions,它会 yield 对象到 redux-saga middleware。 
// 被 yield 的对象都是一类指令,指令可被 middleware 解释执行。
// 当 middleware 取得一个 yield 后的 Promise,middleware 会暂停 Saga,直到 Promise 完成。 
//在上面的例子中,incrementAsync 这个 Saga 会暂停直到 delay 返回的 Promise 被 resolve,这个 Promise 将在 1 秒后 resolve。
// 一旦 Promise 被 resolve,middleware 会恢复 Saga 接着执行,直到遇到下一个 yield。
export function* incrementAsync() {
    // yield delay(1000)
    yield call(delay, 1000)
    yield put({ type: 'INCREMENT' })
}

// Our watcher Saga: 在每个 INCREMENT_ASYNC action spawn 一个新的 incrementAsync 任务
// 用于监听所有的 INCREMENT_ASYNC action,并在 action 被匹配时执行 incrementAsync 任务
export function* watchIncrementAsync() {
    yield takeEvery('INCREMENT_ASYNC', incrementAsync)
}


// 现在我们有了 2 个 Sagas,我们需要同时启动它们。为了做到这一点,我们将添加一个 rootSaga,负责启动其他的 Sagas。
// 这个 Saga yield 了一个数组,值是调用 helloSaga 和 watchIncrementAsync 两个 Saga 的结果。
// 意思是说这两个 Generators 将会同时启动。
export default function* rootSaga() {
    yield all([
        helloSaga(),
        watchIncrementAsync()
    ])
}

在main.js中引入sagas,创建saga中间件:

'INCREMENT_ASYNC' 这个action为前一步用takeEvery创建的action,用于监听所有的 INCREMENT_ASYNC action,并在 action 被匹配时执行 incrementAsync 任务

import "babel-polyfill"

import React from 'react'
import ReactDOM from 'react-dom'
import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'

import Counter from './Counter'
import reducer from './reducers'
import rootSaga from './sagas'

const sagaMiddleware = createSagaMiddleware()
const store = createStore(
  reducer,
  applyMiddleware(sagaMiddleware)
)
sagaMiddleware.run(rootSaga)

const action = type => store.dispatch({type})


function render() {
  ReactDOM.render(
    <Counter
      value={store.getState()}
      onIncrement={() => action('INCREMENT')}
      onDecrement={() => action('DECREMENT')}
      onIncrementAsync = {() => action('INCREMENT_ASYNC')} />,
    document.getElementById('root')
  )
}

render()
store.subscribe(render) 
take/takeEvery/takeLatest

take:yield take([‘LOGOUT’, ‘LOGIN_ERROR’])。意思是监听 2 个并发的 action

redux-saga中如果在非阻塞调用下(fork),那么遵循的是worker/watcher模式,通过take可以监听某个action是否被发起,此外通过take结合fork,可以实现takeEvery和takeLatest的效果。

takeEvery 允许多个 saga 实例同时启动。在某个特定时刻,尽管之前还有一个或多个saga 尚未结束,我们还是可以启动一个新的 saga 任务,

如果我们只想得到最新那个请求的响应(例如,始终显示最新版本的数据)。我们可以使用 takeLatest 辅助函数。

和 takeEvery 不同,在任何时刻 takeLatest 只允许一个 fetchData 任务在执行。并且这个任务是最后被启动的那个。 如果已经有一个任务在执行的时候启动另一个 fetchData ,那之前的这个任务会被自动取消。

delay

工具函数 delay,这个函数返回一个延迟 1 秒再 resolve 的 Promise 我们将使用这个函数去 block(阻塞) Generator。

call/fork
  • call是一个会阻塞的 Effect:Generator 在调用结束之前不能执行或处理任何其他事情。
  • fork是一个非阻塞的Effect:当我们 fork 一个 任务,任务会在后台启动,调用者也可以继续它自己的流程,而不用等待被 fork 的任务结束。
put/select

put对应的是middleware中的dispatch方法,参数是一个plain object,一般在异步调用返回结果后,接着执行put。select相当于getState,用于获取store中的相应部分的state。

cancel/cancelled
  • cancel:yield fork 的返回结果是一个 Task Object。 我们将它们返回的对象赋给一个本地常量 task。如果我们收到一个 action,我们将那个 task 传入给 cancel Effect。 如果任务仍在运行,它会被中止。如果任务已完成,那什么也不会发生,取消操作将会是一个空操作(no-op)。最后,如果该任务完成了但是有错误, 那我们什么也没做,因为我们知道,任务已经完成了。
  • 在 finally 区块可以处理任何的取消逻辑(以及其他类型的完成逻辑)。由于 finally 区块执行在任何类型的完成上(正常的 return, 错误, 或强制取消),如果你想要为取消作特殊处理,有一个 cancelled Effect:
function* authorize(user, password) {
  try {
    const token = yield call(Api.authorize, user, password)
    yield put({type: 'LOGIN_SUCCESS', token})
    yield call(Api.storeItem, {token})
    return token
  } catch(error) {
    yield put({type: 'LOGIN_ERROR', error})
  } finally {
    if (yield cancelled()) {
      // ... put special cancellation handling code here
    }
  }
}

区别

面试:React相关_第18张图片

redux-saga的优缺点

  • 优点:
    (1)异步解耦:集中处理了所有的异步操作,异步接口部分一目了然
    (2)dispatch 的参数依然是⼀个纯粹的 action (FSA),action是普通对象,这跟redux同步的action一模一样
    (3)通过Effect,方便异步接口的测试,提供了各种case的测试⽅案,包括mock task,分⽀覆盖等等
    (4)异常处理: 受益于 generator function 的 saga 实现,代码异常/请求失败 都可以直接通过 try/catch 语法直接捕获处理
    (5) 异步操作的流程是可以控制的,可以随时取消相应的异步操作。

  • 缺点:

    1. 太复杂,学习成本较高,redux-saga不仅在使⽤难以理解的 generator function,⽽且有数⼗个API,学习成本远超redux-thunk
    1. 体积庞⼤: 体积略⼤,代码近2000⾏,min版25KB左右
    1. ts⽀持不友好: yield⽆法返回TS类型

redux-thunk的优缺点

  • 优点:
  1. 体积⼩: redux-thunk的实现⽅式很简单,只有不到20⾏代码
  2. 使⽤简单: redux-thunk没有引⼊像redux-saga或者redux-observable额外的范式,上⼿简单
  • 缺点:
  1. thunk仅仅做了执行这个函数,并不在乎函数主体内是什么,也就是说thunk使得redux可以接受函数作为action,但是函数的内部可以多种多样。
  2. 耦合严重: 异步操作与redux的action偶合在⼀起,不⽅便管理

connect方法

面试:React相关_第19张图片

mapStateToProps

Connect 函数接受所谓的mapStateToProps函数作为它的第一个参数。 这个函数可以用来定义基于 Redux 存储状态的连接组件 的props。

const mapStateToProps = (state) => {
  return {
    notes: state.notes,
    filter: state.filter,
  }
}

const ConnectedNotes = connect(mapStateToProps)(Notes)

export default ConnectedNotes

mapDispatchToProps

Connect 函数的第二个参数可用于定义mapDispatchToProps ,它是一组作为props传递给连接组件的 action creator 函数。

const mapDispatchToProps = {
  toggleImportanceOf,
}

const ConnectedNotes = connect(
  mapStateToProps,
  mapDispatchToProps
)(Notes)

现在这个组件可以通过它的props调用函数直接调用toggleImportanceOf action creator 定义的action:
当使用 connect 时,我们可以简单地这样做:

props.toggleImportanceOf(note.id)

不需要单独调用 dispatch 函数,因为 connect 已经将 toggleImportanceOf action creator 修改为包含 dispatch 的形式。

hooks

react是怎么保证多个useState的相互独立的?

答案是,react是根据useState出现的顺序来定的。

鉴于此,react规定我们必须把hooks写在函数的最外层,不能写在ifelse等条件语句当中,来确保hooks的执行顺序一致

https://zh-hans.reactjs.org/docs/hooks-rules.html

  • 为什么不能在循环/条件语句中执行
    以useState为例:
    和类组件存储state不同,React并不知道我们调用了几次useState,对hooks的存储是按顺序的(参见Hook结构),一个hook对象的next指向下一个hooks。 所以当我们建立示例代码中的对应关系后,Hook的结构如下:
// hook1: const [count, setCount] = useState(0) — 拿到state1
{
  memorizedState: 0
  next : {
    // hook2: const [name, setName] = useState('Star') - 拿到state2
    memorizedState: 'Star'
    next : {
      null
    }
  }
}

// hook1 => Fiber.memoizedState
// state1 === hook1.memoizedState
// hook1.next => hook2
// state2 === hook2.memoizedState

所以如果把hook1放到一个if语句中,当这个没有执行时,hook2拿到的state其实是上一次hook1执行后的state(而不是上一次hook2执行后的)。这样显然会发生错误。

hook如何保存数据

在react中通过currentRenderingFiber来标识当前渲染节点,每个组件都有一个对应的fiber节点,用来保存组件的相关数据信息。
每次函数组件渲染时,currentRenderingFiber就被赋值为当前组件对应的fiber,所以实际上hook是通过currentRenderingFiber来获取状态信息的。

多个hook如何获取数据

react hook允许我们在一个组件中多次使用hook。如下:

function App(){
      const [num, setNum] = useState(0);
      const [name, setName] = useState('ashen');
      const [age, setAge] = useState(21);
}

currentRenderingFiber.memorizedState中保存一条hook对应数据的单向链表。

const hookNum = {
      memorizedState: null,
      next: hookName
}
hookName.next = hookAge;
currentRenderingFiber.memorizedState.next = hookNum;

当函数组件渲染时,每执行到一个hook,就会将currentRenderingFiber.memorizedState的指针向后移一下。这也是hook的调用顺序不能改变的原因(不能在条件语句中使用hook)

  • Hook对象的结构如下:
// ReactFiberHooks.js
export type Hook = {
  memoizedState: any, 
  baseState: any,    
  baseUpdate: Update | null,
  queue: UpdateQueue | null,  
  next: Hook | null, 
};

react hooks 源码分析 — useState
面试:React相关_第20张图片

重点关注memoizedState和next

  • memoizedState是用来记录当前useState应该返回的结果的
  • queue:缓存队列,存储多次更新行为
  • next:指向下一次useState对应的Hook对象。

更新时:

  1. 调用dispatcher函数,按序插入update(其实就是一个action)
  2. 收集update,调度一次React的更新
  3. 在更新的过程中将ReactCurrentDispatcher.current指向负责更新的Dispatcher
  4. 执行到函数组件App()时,useState会被重新执行,在resolve dispatcher的阶段拿到了负责更新的dispatcher。
  5. useState会拿到Hook对象,Hook.query中存储了更新队列,依次进行更新后,即可拿到最新的state
  6. 函数组件App()执行后返回的nextChild中的count值已经是最新的了
  7. FiberNode中的memorizedState也被设置为最新的state
  8. Fiber渲染出真实DOM。更新结束。

面试:React相关_第21张图片

使用规则

  • 不在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层调用他们。
  • 不在普通的 JavaScript 函数或class组件中调用 Hook,在 React 的函数组件或者自定义 Hook 中调用 Hook。

useState

原理 demo

使用一个数组来保存不同的state,和一个index指针指向当前的state。通过调用的顺序来指明中间值 x 保存在哪里。

深究useState的原理

let x = [];
let index = 0;
const myUseState = initial => {
    let currentIndex = index;
    x[currentIndex] = x[currentIndex] === undefined ? initial : x[currentIndex];
    const setState = value=>{
      x[currentIndex] = value;
      render();
    }
    index += 1;
    return [x[currentIndex],setState]
}

useState异步回调的问题:
当使用usestate对数据进行更新,并不能立刻获取到最新的数据。

React.PureComponent使用useState钩子更新状态是异步的

同样,这里的问题不仅是异步的性质,而且函数更新状态的当前闭包和状态更新使用状态值这一事实将放映在下一次重新渲染中,现有的闭包不会受到影响,而是会创建新的闭包。 现在,在当前状态下,挂钩中的值是由现有的闭包获得的,并且在重新渲染时,将根据是否再次创建函数来更新闭包。

即使添加了一个setTimeout函数,尽管超时将在重新渲染的一段时间后运行,但setTimeout仍将使用其先去关闭而不是更新后的值。

解决方法:

  1. useEffect
    如果要对状态更新执行操作,则需要使用useEffect钩子,将对应的state作为依赖项。就像componentDidUpdate在类组件中使用一样,因为useState返回的setter没有回调模式。
  2. useRef.current

useState和setState区别

对setState的解释:

  • state 是 Immutable 的,setState 后一定会生成一个全新的 state 引用。
  • 但 Class Component 通过 this.state 方式读取 state,这导致了每次代码执行都会拿到最新的 state 引用。

对useState 的解释:

  • useState 产生的数据也是 Immutable 的,通过数组第二个参数 Set 一个新值后,原来的值在下次渲染时会形成一个新的引用。
  • 但由于对 state 的读取没有通过 this. 的方式,使得每次 setTimeout 都读取了当时渲染闭包环境的数据,虽然最新的值跟着最新的渲染变了,但旧的渲染里,状态依然是旧值。

对象/数组:

  • 在 Hook 中直接修改 state 的一个对象(或数组)属性的某个子属性或值,然后直接进行 set,不会触发重新渲染

  • 在 class component ,setState 之后无论传给方法的是个什么值,都会触发重新渲染

useEffect

面试:React相关_第22张图片
Hook 使用了 JavaScript 的闭包机制,useEffect 会在每次渲染后都,默认情况下,它在第一次渲染之后每次更新之后都会执行,但是你可以选择只在某些值发生变化时才调用。

useEffect有两个参数

  1. 第一个参数:需要执行的函数,在页面第一次渲染和更新的时候执行
  2. useEffect的第二个参数(一个数组)用于指定effect运行的频率。

第二个参数的三种情况

  1. 如果第二个参数为空,那么在每次渲染都会执行useEffect的第一个参数
  2. 如果第二个参数是一个空数组 [],那么只会在第一次渲染时执行第一个参数
  3. 而第二个参数是一个不为空的数组,那么数组中的参数发生变化时,就执行第一个参数

面试:React相关_第23张图片

执行时机

面试:React相关_第24张图片
useLayoutEffect调用时机和componentDidMount相同:
面试:React相关_第25张图片

了解了 useEffect 可以在组件渲染后实现各种不同的副作用。有些副作用可能需要清除,所以需要返回一个函数:
面试:React相关_第26张图片

useLayoutEffect

深入理解 React useLayoutEffect 和 useEffect 的执行时机

  • 一句话总结:useLayoutEffect是在commit阶段的mutation完成后,在浏览器的layout阶段同步执行;而useEffect则是在commit阶段结束后,即浏览器layout完成后异步调用
    即useLayoutEffect比useEffect先执行。

  • useEffect(create, deps):
    该 Hook 接收一个包含命令式、且可能有副作用代码的函数。在函数组件主体内(这里指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性。使用 useEffect 完成副作用操作。赋值给 useEffect 的函数会在组件渲染到屏幕之后执行。你可以把 effect 看作从 React 的纯函数式世界通往命令式世界的逃生通道。

  • useLayoutEffect(create, deps):
    其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。

useEffect和useLayoutEffect的区别 带demo

  • useEffect 和 useLayoutEffect 的区别?
    useEffect 在渲染时是异步执行,并且要等到浏览器将所有变化渲染到屏幕后才会被执行。
    useLayoutEffect 在渲染时是同步执行,其执行时机与 componentDidMount,componentDidUpdate 一致

  • 对于 useEffect 和 useLayoutEffect 哪一个与 componentDidMount,componentDidUpdate 的是等价的?
    useLayoutEffect,因为从源码中调用的位置来看,useLayoutEffect的 create 函数的调用位置、时机都和 componentDidMount,componentDidUpdate 一致,且都是被 React 同步调用,都会阻塞浏览器渲染。

  • useEffect 和 useLayoutEffect 哪一个与 componentWillUnmount 的是等价的?
    同上,useLayoutEffect 的 detroy 函数的调用位置、时机与 componentWillUnmount 一致,且都是同步调用。useEffect 的 detroy 函数从调用时机上来看,更像是 componentDidUnmount (注意React 中并没有这个生命周期函数)。

  • 为什么建议将修改 DOM 的操作里放到 useLayoutEffect 里,而不是 useEffect?
    可以看到在流程9/10期间,DOM 已经被修改,但但浏览器渲染线程依旧处于被阻塞阶段,所以还没有发生回流、重绘过程。由于内存中的 DOM 已经被修改,通过 useLayoutEffect 可以拿到最新的 DOM 节点,并且在此时对 DOM 进行样式上的修改,假设修改了元素的 height,这些修改会在步骤 11 和 react 做出的更改一起被一次性渲染到屏幕上,依旧只有一次回流、重绘的代价。
    如果放在 useEffect 里,useEffect 的函数会在组件渲染到屏幕之后执行,此时对 DOM 进行修改,会触发浏览器再次进行回流、重绘,增加了性能上的损耗。

useContext

  1. useContext可以帮助我们跨越组件层级直接传递变量,实现共享。

需要注意的是useContext和redux的作用是不同的!
useContext:解决的是组件之间值传递的问题
redux:是应用中统一管理状态的问题
但通过和useReducer的配合使用,可以实现类似Redux的作用。

在组建外部建立一个Context ->

import React, { createContext, useContext } from "react";
import ReactDOM from "react-dom";

// 全局创建Context
const TestContext= createContext({});

const Navbar = () => {
  const { username } = useContext(TestContext)

  return (
    <div className="navbar">
      <p>{username}</p>
    </div>
  )
}

const Messages = () => {
  const { username } = useContext(TestContext)

  return (
    <div className="messages">
      <p>1 message for {username}</p>
    </div>
  )
}

function App() {
  return (
	<TestContext.Provider 
		value={{
			username: 'superawesome',
		}}
	>
		<div className="test">
			<Navbar />
			<Messages />
		</div>
	<TestContext.Provider/>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

TestContext.Provider提供了一个Context对象,这个对象是可以被子组件共享的;
在子组件内,使用 useContext() 钩子函数用来引入Context对象,从中获取username属性(解构赋值)。

2.Context的作用就是对它所包含的组件树提供全局共享数据的一种技术。

问题

Context.Provider重新渲染的时候,它所有的子组件都被重新渲染了,比如上面例子中子组件有Header和Content,Content作为Consumer之一重画没问题,但是Header不是Consumer,也不依赖于Context的value,根本没有必要重画;Context.Provider说到底还是组件,也按照组件基本法来办事,当value发生变化时,它也可以不引发子组件的渲染,前提是,子组件作为一个属性(this.props.children)也要保持不变才行

useReducer

const [state, dispatch] = useReducer(reducer, initState);

第一个参数:reducer函数。第二个参数:初始化的state返回值为最新的state和dispatch函数(用来触发reducer函数,计算对应的state)。按照官方的说法:对于复杂的state操作逻辑,嵌套的state的对象,推荐使用useReducer。

好处:

  • 更好的可读性,我们也能更清晰的了解state的变化逻辑
  • 所有的state处理都集中到了一起,使得我们对state的变化更有掌控力,同时也更容易复用state逻辑变化代码
  • 更容易构建自动化测试用例

使用场景:

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

官方文档

useReducer + useContext代替Redux

  • useContext 创建全局状态,不用一层一层的传递状态。
  • useReducer 创建 reducer 根据不同的 dispatch 更新 state。
  • 代码写到哪里状态就加到哪里,不用打断思路跳到 redux 里面去写。
  • 全局状态分离,避免项目变大导致 Redux 状态树难以管理。

这一次彻底搞定 useReducer - useContext使用

useCallback / useMemo

useMemo与useCallback使用指南

在hooks出来之后,我们能够使用function的形式来创建包含内部state的组件。但是,使用function的形式,失去了shouldComponentUpdate,我们无法通过判断前后状态来决定是否更新。而且,在函数组件中,react不再区分mount和update两个状态,这意味着函数组件的每一次调用都会执行其内部的所有逻辑,那么会带来较大的性能损耗。因此useMemo 和useCallback就是解决性能问题的杀手锏。

function useMemo<T>(factory: () => T, deps: DependencyList | undefined): T; 
function useCallback<T extends (...args: any[]) => any>(callback: T, deps: DependencyList): T;

useCallback和useMemo的参数跟useEffect一致,他们之间最大的区别有是useEffect会用于处理副作用,而前两个hooks不能。

useMemo和useCallback都会在组件第一次渲染的时候执行,之后会在其依赖的变量发生改变时再次执行;并且这两个hooks都返回缓存的值,useMemo返回缓存的变量,useCallback返回缓存的函数

共同作用:
1.仅 依赖数据 发生变化, 才会重新计算结果,也就是起到缓存的作用

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

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

和React.memo区别

React.memo

React.memo 和 React.PureComponent 类似, React.PureComponent 在类组件中使用,而React.memo 在函数组件中使用

memo针对一个(函数)组件的渲染是否重复执行:
在子组件不需要父组件的值和函数的情况下,使用memo函数包裹子组件即可。

默认情况下其只会对复杂对象做浅层对比,如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现。

Redux hooks

https://react-redux.js.org/api/hooks

  1. useDispatch
    useDispatch-hook 提供了所有 React 组件对dispatch-函数的访问,这个 redux-store 的 dispatch-函数是在index.js 中定义的 。

这就允许所有组件对 redux-store 的状态进行更改。

  1. useSelector
    组件可以通过 react-redux 库的useSelector-hook访问存储在store中的便笺。

react-router-dom hooks

  1. useHistory

  2. useRouteMatch

useRef

useRef官方文档

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内持续存在。

useRef 的特点

  • 每次渲染 useRef 返回值都不变;
  • ref.current 发生变化并不会造成 re-render;
  • ref.current 发生变化应该作为 Side Effect(因为它会影响下次渲染),所以不应该在 render 阶段更新 current 属性。

ref.current 不可以作为其他 hooks(useMemo, useCallback, useEffect)依赖项;ref.current 的值发生变更并不会造成 re-render, Reactjs 并不会跟踪 ref.current 的变化

和普通变量的区别:
普通变量在每次渲染后都会重新赋值,而useRef会保留上一次的返回值

ref转发

  1. useImperativeHandle(ref,createHandle,[deps])可以自定义暴露给父组件的实例值。如果不使用,父组件的ref(chidlRef)访问不到任何值(childRef.current==null)
  2. useImperativeHandle应该与forwradRef搭配使用
  3. React.forwardRef会创建一个React组件,这个组件能够将其接受的ref属性转发到其组件树下的另一个组件中。
  4. React.forward接受渲染函数作为参数,React将使用prop和ref作为参数来调用此函数。

在下面的示例中,FancyButton 使用 React.forwardRef 来获取传递给它的 ref,然后转发到它渲染的 DOM button:

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>;

这样,使用 FancyButton 的组件可以获取底层 DOM 节点 button 的 ref ,并在必要时访问,就像其直接使用 DOM button 一样。

以下是对上述示例发生情况的逐步解释:

  1. 我们通过调用 React.createRef 创建了一个 React ref 并将其赋值给 ref 变量。
  2. 我们通过指定 ref 为 JSX 属性,将其向下传递给
  3. React 传递 ref 给 forwardRef 内函数 (props, ref) => …,作为其第二个参数。
  4. 我们向下转发该 ref 参数到
  5. 当 ref 挂载完成,ref.current 将指向 DOM 节点。

例子:
面试:React相关_第27张图片
使用useRef hook创建博客表单的ref;
面试:React相关_第28张图片
然后传递给Togglable wrapper组件;
面试:React相关_第29张图片
在Togglable组件中使用forwardRef获取ref,并使用useImperativeHandle将ref绑定到toggleVisibility方法上;

这样就可以在Togglable的父组件BlogForm中使用blogFormRef.current.toggleVisibility()来访问Togglable组件中的toggleVisibility方法,在表单提交的时候改变表单的显隐状态。

和createRef区别

组件随着 count 的值的改变重新渲染,每次 count 改变,DOM 重新渲染之后,createRef 就会重新创建,生成一个新的引用地址,新的 ref 初始值为 null,页面渲染时被赋予当前 count 的值。

但是 useRef 一旦被创建,在组件的整个生命周期中,react 都会给它保持同一个引用,useRef 返回的 {current: xxx} 对象也将在整个生命周期中保持同一个值,除非手动改变 current 的值。

所以 refByCreateRef count 会随着 count 改变,但是 refByUseRef count 将始终保持在第一次渲染时的值。

createRef 每次渲染都会返回一个新的引用,而 useRef 每次都会返回相同的引用。

当 useRef 的 current 发生变化时,页面不会收到通知,也就是说更改 .current 属性不会导致页面重新渲染,因为引用地址始终是同一个

使用场景

  1. 用于绑定 DOM,获取dom节点
  2. 父组件调用子组件方法,子函数组件需要使用 forwardRef 包裹,需要配合useImperativeHandle使用
  3. 函数组件访问之前(previous)渲染的变量。
function Example () {
  const [count, setCount] = useState(0);

  const prevCount = usePrevious(count)
  console.log(prevCount, count, '之前的状态和现在的状态')
  return (
    <div>
      <div>{count}</div>
      <button onClick={() => {setCount(count+1)}}>+</button>
    </div>
  )
}
function usePrevious (value) {
  const ref = useRef()
  useEffect(() => {
    ref.current = value
  })
  return ref.current
}

和普通global变量存储的区别

  • useRef 是定义在实例基础上的,如果代码中有多个相同的组件,每个组件的 ref 只跟组件本身有关,跟其他组件的 ref 没有关系。
  • 组件前定义的 global 变量,是属于全局的。如果代码中有多个相同的组件,那这个 global 变量在全局是同一个,他们会互相影响。

hooks实现getDerivedStateFromProps

在状态中保存先前属性,紧接着比对先前属性和当前属性。如果不一样就更新状态,实现属性和状态的同步。在设置完新的状态后,react不会继续当前的渲染过程,而是会重新调度一次渲染,所以开销并不是很昂贵。

function ScrollView({row}) {
  const [isScrollingDown, setIsScrollingDown] = useState(false);
  const [prevRow, setPrevRow] = useState(null);

  if (row !== prevRow) {
    // Row changed since last render. Update isScrollingDown.
    setIsScrollingDown(prevRow !== null && row > prevRow);
    setPrevRow(row);
  }

  return `Scrolling down: ${isScrollingDown}`;
}

hooks优缺点

优点

没有hooks之前的问题:

  • 在组件之间复用状态逻辑很难
  • 逻辑复杂组件变得难以理解,开发与维护
  • 难以理解的 class/this

hooks带来的好处:

  • 使你在无需修改组件结构的情况下复用状态逻辑,使功能代码聚合,方便阅读维护;更容易复用代码,解决了类组件有些时候难以复用逻辑的问题:通过自定义hooks,便可以轻松复用逻辑
  • 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据),使组件树层级变浅
  • 在非 class 的情况下可以使用更多的 React 特性,不用再去考虑 this 的指向问题
  • 函数式编程风格,函数式组件、状态保存在运行环境、每个功能都包裹在函数中,整体风格更清爽,更优雅
  • 代码量更少,更容易拆分组件

简洁: React Hooks解决了HOC和Render Props的嵌套问题,更加简洁
解耦: React Hooks可以更⽅便地把 UI 和状态分离,做到更彻底的解耦
组合: Hooks 中可以引⽤另外的 Hooks形成新的Hooks,组合变化万千
函数友好: React Hooks为函数组件⽽⽣,从⽽解决了类组件的⼏⼤问题:

  1. this指向容易错误
  2. 分割在不同声明周期中的逻辑使得代码难以理解和维护
  3. 代码复⽤成本⾼(⾼阶组件容易使代码量剧增)

缺点

  • 状态不同步:不好用的useEffect,函数的运行是独立的,每个函数都有一份独立的作用域。函数的变量是保存在运行时的作用域里面,当我们有异步操作的时候,经常会碰到异步回调的变量引用是之前的,也就是旧的(这里也可以理解成闭包)。

  • 代码从主动式变成响应式:必须把深入理解useEffect和useCallback这些api的第二个参数的作用;useEffect依赖某个函数的不可变性,这个函数的不可变性又依赖于另一个函数的不可变性,这样便形成了一条依赖链。一旦这条依赖链的某个节点意外地被改变了,你的useEffect就被意外地触发了

  • 额外的学习成本(Functional Component 与Class Component之间的困惑)

  • 写法上有限制(不能出现在条件、循环中),并且写法限制增加了重构成本

  • 破坏了PureComponent、React.memo浅⽐较的性能优化效果(为了取最新的props和state,每次render()都要重新创建事件处函数)

  • 在闭包场景可能会引⽤到旧的state、props值

  • 内部实现上不直观(依赖⼀份可变的全局状态,不再那么“纯”)

  • React.memo并不能完全替代shouldComponentUpdate(因为拿不到state change,只针对 props change

和普通函数的区别

  • 函数式组件是用函数描述的组件,本质上还是一个组件。因此它可以:使用hooks、拥有自己的状态、使用memo缓存上次渲染;在devtools里显示为组件、设置displayName;渲染可中断、可恢复;有自己的生命周期。
  • 而返回值为JSX的函数仅仅是代码片段的复用而已,相当于直接在写JSX。

和类组件的区别

  1. hooks组件每次渲染都会有独立props/state,而class组件总是会通过this拿到最新的props/state
  2. function组件逻辑聚合,而class组件逻辑分散
  3. function组件逻辑复用简单,而class组件逻辑复用困难

React Router

React-router路由基本原理

面试:React相关_第30张图片
react路由基础(Router、Link和Route)

组件传值

父子组件

父传子:props

子传父:传函数改参数

通过在父组件引入的子组件中传递一个函数并传参,子组件去触发这个函数更改参数完成数据更新

const Blogs = ( { user } ) => {

    const blogs = useSelector(state => state.blogs)
    const dispatch = useDispatch()

    const addLikes = (id) => {
        // ...
      }
    
    const deleteBlog = (id) => {
        // ...
      }

    return (
      <div id='showblogs'>
        {blogs.sort((a, b) => parseFloat(b.likes) - parseFloat(a.likes)).map(blog =>
          <Blog key={blog.id} blog={blog} user={user.username}
            addLikes={() => addLikes(blog.id)} deleteBlog={() => deleteBlog(blog.id)}/>
        )}
      </div>
    )
  }

useRef/useImperativeHandle

Refs适用于父子组件的通信,Refs提供了一种方式,允许我们访问DOM节点或在render方法中创建的React元素,在某些情况下,需要在典型数据流之外强制修改子组件,被修改的子组件可能是一个React组件的实例,也可能是一个DOM元素,渲染组件时返回的是组件实例,而渲染DOM元素时返回是具体的DOM节点,React提供的这个ref属性,表示为对组件真正实例的引用,其实就是ReactDOM.render()返回的组件实例。

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变

你应该熟悉 ref 这一种访问 DOM 的主要方式。如果你将 ref 对象以

形式传入组件, 则无论该节点如何改变,React 都会将 ref 对象的 .current 属性设置为相应的 DOM 节点

然而,useRef() 比 ref 属性更有用。它可以很方便地保存任何可变值,其类似于在 class 中使用实例字段的方式。

这是因为它创建的是一个普通 Javascript 对象。而 useRef() 和自建一个 {current: …} 对象的唯一区别是,useRef 会在每次渲染时返回同一个 ref 对象。

请记住,当 ref 对象内容发生变化时,useRef 并不会通知你。变更 .current 属性不会引发组件重新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用回调 ref 来实现。

发布订阅者模式

pubsub-js

redux/react-redux

EventBus

// event-bus.js
var PubSub = function() {
    this.handlers = {};
}

PubSub.prototype = {
    constructor: PubSub,
    on: function(key, handler) { // 订阅
        if(!(key in this.handlers)) this.handlers[key] = [];
        if(!this.handlers[key].includes(handler)) {
             this.handlers[key].push(handler);
             return true;
        }
        return false;
    },

    once: function(key, handler) { // 一次性订阅
        if(!(key in this.handlers)) this.handlers[key] = [];
        if(this.handlers[key].includes(handler)) return false;
        const onceHandler = (...args) => {
            handler.apply(this, args);
            this.off(key, onceHandler);
        }
        this.handlers[key].push(onceHandler);
        return true;
    },

    off: function(key, handler) { // 卸载
        const index = this.handlers[key].findIndex(item => item === handler);
        if (index < 0) return false;
        if (this.handlers[key].length === 1) delete this.handlers[key];
        else this.handlers[key].splice(index, 1);
        return true;
    },

    commit: function(key, ...args) { // 触发
        if (!this.handlers[key]) return false;
        console.log(key, "Execute");
        this.handlers[key].forEach(handler => handler.apply(this, args));
        return true;
    },

}

export default new PubSub();
Copy
<!-- 子组件 -->
import React from "react";
import eventBus from "./event-bus";


class Child extends React.PureComponent{

    render() {
        return (
            <>
                <div>接收父组件的值: {this.props.msg}</div>
                <button onClick={() => eventBus.commit("ChangeMsg", "Changed Msg")}>修改父组件的值</button>
            </>
        )
    }
}

export default Child;
Copy
<!-- 父组件 -->
import React from "react";
import Child from "./child";
import eventBus from "./event-bus";

class Parent extends React.PureComponent{

    constructor(props){
        super(props);
        this.state = { msg: "Parent Msg" };
        this.child = React.createRef();
    }

    changeMsg = (msg) => {
        this.setState({ msg });
    }

    componentDidMount(){
        eventBus.on("ChangeMsg", this.changeMsg);
    }

    componentWillUnmount(){
        eventBus.off("ChangeMsg", this.changeMsg);

    }

    render() {
        return (
            <div>
                <Child msg={this.state.msg} ref={this.child} />
            </div>
        )
    }
}

export default Parent;

context/useContext钩子(上下文)

如果需要在组件之间共享状态,可以使用useContext()。

接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 的 value prop 决定

当组件上层最近的 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。即使祖先使用 React.memo 或 shouldComponentUpdate,也会在组件本身使用 useContext 时重新渲染。

import React, { useContext } from "react";
import ReactDOM from "react-dom";

const TestContext= React.createContext({});

const Navbar = () => {
  const { username } = useContext(TestContext)

  return (
    <div className="navbar">
      <p>{username}</p>
    </div>
  )
}

const Messages = () => {
  const { username } = useContext(TestContext)

  return (
    <div className="messages">
      <p>1 message for {username}</p>
    </div>
  )
}

function App() {
  return (
	<TestContext.Provider 
		value={{
			username: 'superawesome',
		}}
	>
		<div className="test">
			<Navbar />
			<Messages />
		</div>
	<TestContext.Provider/>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

事件冒泡

父组件更新会导致子组件更新吗

只要父组件的render了,那么默认情况下就会触发子组件的render过程,子组件的render过程又会触发它的子组件的render过程,一直到React元素(即jsx中的

这样的元素)。当render过程到了叶子节点,即React元素的时候,diff过程便开始了,这时候diff算法会决定是否切实更新DOM元素。

React不能检测到你是否给子组件传了属性,所以它必须进行这个重渲染过程(术语叫做reconciliation)。

React提供了shouldComponentUpdate回调函数,让程序员根据情况决定是否决定是否要重render本组件。如果某个组件的shouldComponentUpdate总是返回false, 那么当它的父组件render了,会触发该组件的render过程,但是进行到shouldComponentUpdate判断时会被阻止掉,从而就不调用它的render方法了,它自己下面的组件的render过程压根也不会触发了。

Component和pureComponent

面试:React相关_第31张图片

PureComponent 是优化 React 应用程序最重要的方法之一,易于实施,只要把继承类从 Component 换成 PureComponent 即可,可以减少不必要的 render操作的次数,从而提高性能,而且可以少写 shouldComponentUpdate 函数,节省了点代码。

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

  • render中不建议声明对象主要是为了激活PureComponent的渲染过滤规则,防止不必要的组件渲染
  • PureComponent渲染过滤规则为:先校验state和props中的属性数量是否改变,然后用Object.is比较state和props中的属性值是否改变(用Object.is进行比较)
  • 如果state和props中的属性值是引用地址,每次想要触发页面渲染需要保证引用地址更新

原理

当组件更新时,如果组件的 props 和 state 都没发生改变, render 方法就不会触发,省去 Virtual DOM 的生成和比对过程,达到提升性能的目的。具体就是 React 自动帮我们做了一层浅比较:

if (this._compositeType === CompositeTypes.PureClass) {
     shouldUpdate = !shallowEqual(prevProps, nextProps) || !shallowEqual(inst.state, nextState);
}

shallowEqual 会比较 Object.keys(state | props) 的长度是否一致,每一个 key 是否两者都有,并且是否是一个引用,也就是只比较了第一层的值,确实很浅,所以深层的嵌套数据是对比不出来的。

优势

不需要开发者自己实现shouldComponentUpdate,就可以进行简单的判断来提升性能。

缺点

可能会因深层的数据不一致而产生错误的否定判断,从而shouldComponentUpdate结果返回false,界面得不到更新。
例如:
在MainComponent中去修改arr时,ChildComponent并没有得到刷新。原因在于js使用的是引用赋值,新的对象简单引用了原始对象,改变新对象虽然影响了原始对象,但对象的地址还是一样,使用===比较的方式相等。而在PureComponent中,会被判定prop相等而不触发render()。

React性能优化

React 性能优化技巧总结

从render/渲染上入手:

什么时候会执行「render」?
render 函数会在两种场景下被调用:

  1. 状态更新时:继承了 React.Component 的 class 组件,即使状态没变化,只要调用了setState 就会触发 render;对函数式组件来说,状态值改变时才会触发 render 函数的调用。
  2. 父容器重新渲染时:无论组件是继承自 React.Component 的 class 组件还是函数式组件,一旦父容器重新 render,组件的 render 都会再次被调用。

在「render」过程中会发生什么?
Diffing
在此步骤中,React 将新调用的 render 函数返回的树与旧版本的树进行比较,这一步是 React 决定如何更新 DOM 的必要步骤。虽然 React 使用高度优化的算法执行此步骤,但仍然有一定的性能开销。
Reconciliation
基于 diffing 的结果,React 更新 DOM 树。这一步因为需要卸载和挂载 DOM 节点同样存在许多性能开销。

  • React.memo/useMemo/useCallback/pureComponent
    避免组件不必要的渲染的方法有:React.memo 包裹的函数式组件,继承自 React.PureComponent 的 class 组件。

  • 按需传递props
    只传递组件用到的props

  1. 使用纯组件
  2. 使用React.memo进行组件记忆
  3. 使用shouldComponentUpdate生命周期事件
  4. 懒加载组件(?)
  5. 使用React Fragments避免额外标记(?)
  6. 不要使用内联函数定义(?)
  7. 避免componentWillMount()中的异步请求
  8. 在Constructor的早期绑定函数(?)
  9. 箭头函数与构造函数中的绑定(?)
  10. 避免使用内联样式属性
  11. 优化React中的条件渲染
  12. 不要在render方法中导出数据(?)
  13. 为组件创建错误边界(?)
  14. 组件的不可变数据结构(?)
  15. 使用唯一键迭代
  16. 事件节流和防抖
  17. 使用CDN
  18. 用CSS动画代替JavaScript动画
  19. 在Web服务器上启用gzip压缩
  20. 使用Web Workers处理CPU密集任务
  21. React组件的服务端渲染(?)

高阶组件

我们写的纯函数组件只负责处理展示,很多时候会发现,由于业务需求,组件需要被“增强”,例如响应浏览器事件等。如果只有一两个组件我们大可以全部重写为class形式,但如果有许多组件需要进行相似或相同的处理(例如都响应浏览器窗口改变这个事件)时,考虑到代码的复用性,很容易想到用函数处理,HOC也正是为了解决这样的问题而出现的。

一个高阶组件只是一个包装了另外一个 React 组件的 React 组件。返回的新组件拥有了输入组件所不具备的功能。这里提到的组件并不是组件实例,而是组件类,也可以是一个无状态组件的函数。

如果一个函数 接受一个或多个函数作为参数或者返回一个函数 就可称之为 高阶函数。

定义中的『包装』一词故意被定义的比较模糊,因为它可以指两件事情:

  1. 属性代理(Props Proxy):高阶组件操控传递给 WrappedComponent 的 props;一个函数接受一个 WrappedComponent 组件作为参数传入,并返回一个继承了 React.Component 组件的类,且在该类的 render() 方法中返回被传入的 WrappedComponent 组件。
  • 操作 props
  • 抽离 state
  • 通过 ref 访问到组件实例
  • 用其他元素包裹传入的组件 WrappedComponent
  1. 反向继承(Inheritance Inversion):高阶组件继承(extends)WrappedComponent。 一个函数接受一个 WrappedComponent 组件作为参数传入,并返回一个继承了该传入 WrappedComponent 组件的类,且在该类的 render() 方法中返回 super.render() 方法。
  • 操作 state
  • 渲染劫持(Render Highjacking)

其中,渲染劫持可以:

  • 有条件地展示元素树(element tree)
  • 操作由 render() 输出的 React 元素树
  • 在任何由 render() 输出的 React 元素中操作 props
  • 用其他元素包裹传入的组件 WrappedComponent (同 属性代理)

概括的讲,高阶组件允许你做:

  • 代码复用,逻辑抽象,抽离底层准备(bootstrap)代码
  • 渲染劫持
  • State 抽象和更改
  • Props 更改

高阶组件有如下好处:

  1. 适用范围广,它不需要es6或者其它需要编译的特性,有函数的地方,就有HOC。
  2. Debug友好,它能够被React组件树显示,所以可以很清楚地知道有多少层,每层做了什么。

基本原理

基本原理
HOC的基本原理可以写成这样:

const HOCFactory = (Component) => {
  return class HOC extends React.Component {
    render(){
      return <Component {...this.props} />
    }
  }
}

很明显HOC最大的特点就是:接受一个组件作为参数,返回一个新的组件。

高阶组件的意义

(1)重用代码有时候很多React组件都需要公用同样一个逻辑,比如说React-Redux中容器组件的部分,没有必要让每个组件都实现一遍shouldComponentUpdate这些生命周期函数,把这部分逻辑提取出来,利用高阶组件的方式应用出去,就可以减少很多组件的重复代码
(2)修改现有React组件的行为。有些现成的React组件并不是开发者自己开发的,来自于第三方,或者即便是我们自己开发的,但是我们不想去触碰这些组件的内部逻辑,这时候可以用高阶组件。通过一个独立于原有组件的函数,可以产生新的组件,对原有组件没有任何侵害。

  • 优点:提取公共逻辑,不会影响内层组件的状态, 降低了耦合度

使用场景

React 中的高阶组件及其应用场景

  • 权限控制
    利用高阶组件的 条件渲染 特性可以对页面进行权限控制,权限控制一般分为两个维度:页面级别 和 页面元素级别。
  • 组件渲染性能追踪
    借助父组件子组件生命周期规则捕获子组件的生命周期,可以方便的对某个组件的渲染时间进行记录
    withTiming 是利用 反向继承 实现的一个高阶组件,功能是计算被包裹组件(这里是 Home 组件)的渲染时间。
  • 页面复用
    withFetching 其实和前面的 withAuth 函数类似,把 变的部分(fetching(type)) 抽离到外部传入,从而实现页面的复用。
  • 装饰者模式?高阶组件?AOP?
    高阶组件其实就是装饰器模式在 React 中的实现:通过给函数传入一个组件(函数或类)后在函数内部对该组件(函数或类)进行功能的增强(不修改传入参数的前提下),最后返回这个组件(函数或类),即允许向一个现有的组件添加新的功能,同时又不去修改该组件,属于 包装模式(Wrapper Pattern) 的一种。

什么是装饰者模式:在不改变对象自身的前提下在程序运行期间动态的给对象添加一些额外的属性或行为。

使用装饰者模式实现 AOP:

面向切面编程(AOP)和面向对象编程(OOP)一样,只是一种编程范式,并没有规定说要用什么方式去实现 AOP。

// 在需要执行的函数之前执行某个新添加的功能函数
Function.prototype.before = function(before = () => {}) {
    return () => {
        before.apply(this, arguments);
        return this.apply(this, arguments);
    };
}
// 在需要执行的函数之后执行某个新添加的功能函数
Function.prototype.after = function(after = () => {}) {
    return () => {
        const result = after.apply(this, arguments);
        this.apply(this, arguments);
        return result;
    };
}

可以发现其实 before 和 after 就是一个 高阶函数,和高阶组件非常类似。

面向切面编程(AOP)主要应用在 与核心业务无关但又在多个模块使用的功能比如权限控制、日志记录、数据校验、异常处理、统计上报等等领域。

问题

高阶组件存在的问题

  • 静态方法丢失
    因为原始组件被包裹于一个容器组件内,也就意味着新组件会没有原始组件的任何静态方法,所以必须将静态方法做拷贝。hoc 会将原始组件使用容器组件包装成新组件,静态方法通过原组件暴露,而新组件则缺少这些静态方法,可以使用 hoist-non-react-statics 自动拷贝所有非 React 静态方法。
  • refs 属性不能透传,需要转发 ref
    因为 ref 不是 prop 属性。就像 key 一样,其被 React 进行了特殊处理。如果你向一个由高阶组件创建的组件的元素添加 ref 引用,那么 ref 指向的是最外层容器组件实例的,而不是被包裹的 WrappedComponent 组件。。使用React.forwardRef回调的第二个参数ref,可以将其作为常规 prop 属性传递给 HOC,例如 “forwardedRef”,然后它就可以被挂载到被 HOC包裹的子组件上。
function logProps(Component) {
  class LogProps extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('old props:', prevProps);
      console.log('new props:', this.props);
    }
 
    render() {
      const {forwardedRef, ...rest} = this.props;
 
      // 将自定义的 prop 属性 “forwardedRef” 定义为 ref
      return <Component ref={forwardedRef} {...rest} />;
    }
  }
 
  // 注意 React.forwardRef 回调的第二个参数 “ref”。
  // 我们可以将其作为常规 prop 属性传递给 LogProps,例如 “forwardedRef”
  // 然后它就可以被挂载到被 LogPros 包裹的子组件上。
  return React.forwardRef((props, ref) => {
    return <LogProps {...props} forwardedRef={ref} />;
  });
}
  • 反向继承不能保证完整的子组件树被解析
    如果渲染 elements tree 中包含了 function 类型的组件的话,这时候就不能操作组件的子组件了。

  • 无法清晰地标识数据的来源:当有多个HOC一同使用时,无法直接判断子组件的props是哪个HOC负责传递的

  • 重复命名的问题(固定的 props 可能会被覆盖):若父子组件有同样名称的props,或使用的多个HOC中存在相同名称的props,则存在覆盖问题,而且react并不会报错。当然可以通过规范命名空间的方式避免。

  • 在react开发者工具中观察HOC返回的结构,可以发现HOC产生了许多无用的组件,加深了组件层级。

  • 同时,HOC使用了静态构建,即当AppWithMouse被创建时,调用了一次withMouse中的静态构建。而在render中调用构建方法才是react所倡导的动态构建。与此同时,在render中构建可以更好的利用react的生命周期。

高阶组件的约定/使用需要注意什么

高阶组件带给我们极大方便的同时,我们也要遵循一些 约定:

  • props 保持一致
    高阶组件在为子组件添加特性的同时,要尽量保持原有组件的 props 不受影响,也就是说传入的组件和返回的组件在 props 上尽量保持一致。

  • 不能在函数式(无状态)组件上使用 ref 属性,因为它没有实例

  • 不要以任何方式改变原始组件 WrappedComponent
    在高阶组件的内部对 WrappedComponent 进行了修改,一旦对原组件进行了修改,那么就失去了组件复用的意义,所以请通过 纯函数(相同的输入总有相同的输出) 返回新的组件

  • 透传不相关 props 属性给被包裹的组件 WrappedComponent

  • 不要在 render() 方法中使用高阶组件
    因为 hoc 的作用是返回一个新的组件,如果直接在 render 中调用 hoc 函数,每次 render 都会生成新的组件。对于复用的目的来说,这毫无帮助,之前此外生成的旧组件因此被不断卸载。

  • 使用 compose 组合高阶组件
    使用 compose 组合高阶组件使用,可以显著提高代码的可读性和逻辑的清晰度。

  • 包装显示名字以便于调试
    高阶组件创建的容器组件在 React Developer Tools 中的表现和其它的普通组件是一样的。为了便于调试,可以选择一个显示名字,传达它是一个高阶组件的结果。

渲染属性Render Props

渲染属性(Render Props)

术语 “render prop” 是指一种简单的技术,用于使用一个值为函数的 prop 在 React 组件之间的代码共享

通过函数回调的方式提供一个接口,用以渲染外部组件

Render Props 的核心思想是,通过一个函数将class组件的state作为props传递给纯函数组件

import React from 'react'
import ReactDOM from 'react-dom'
import PropTypes from 'prop-types'

class Mouse extends React.Component {
  static propTypes = {
    render: PropTypes.func.isRequired
  }

  state = { x: 0, y: 0 }

  handleMouseMove = (event) => {
    this.setState({
      x: event.clientX,
      y: event.clientY
    })
  }

  render() {
    return (
      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
        {this.props.render(this.state)}
      </div>
    )
  }
}

const App = () => (
  <div style={{ height: '100%' }}>
    <Mouse render={({ x, y }) => (
      <h1>The mouse position is ({x}, {y})</h1>
    )}/>
  </div>
)

ReactDOM.render(<App/>, document.getElementById('root'))

分析:

从demo中很容易看到,新建的Mouse组件的render方法中返回了{this.props.render(this.state)}这个函数,将其state(Mouse组件的state)作为参数传入其props.render方法中,调用时直接取组件所需要的state即可。

  • render是Mouse组件的一个props
  • render的值是一个纯函数或组件
  • 将Mouse组件的state作为参数传入render指向的函数/组件,实现渲染逻辑

优势

  • 支持ES6,和HOC一样
  • 不用担心prop的命名问题,在render函数中只取需要的state
  • 相较于HOC,不会产生无用的空组件加深层级
  • 最重要的是,这里的构建模型是动态的,所有改变都在render中触发,能更好的利用react的生命周期。

问题

  • 无法在 return 语句外访问数据(state)
  • 嵌套:它很容易导致嵌套地狱

fiber

React Fiber可以理解为:

React内部实现的一套状态更新机制。支持任务不同优先级,可中断与恢复,并且恢复后可以复用之前的中间状态。
其中每个任务更新单元为React Element对应的Fiber节点。

起源/含义

在React15及以前,Reconciler采用递归的方式创建虚拟DOM,递归过程是不能中断的。如果组件树的层级很深,递归会占用线程很多时间,造成卡顿。

为了解决这个问题,React16 将递归的无法中断的更新重构为异步的可中断更新, 由于曾经用于递归的虚拟DOM数据结构已经无法满足需要。于是,全新的Fiber架构应运而生。

将recocilation(递归diff),拆分成⽆数个⼩任务;它随时能够停⽌,恢复。停⽌恢复的时机取决于当前的⼀帧(16ms)内,还有没有⾜够的时间允许计算。

时间分片指的是一种将多个粒度小的任务放入一个时间切片(一帧)中执行的一种方案,在 React Fiber 中就是将多个任务放在了一个时间片中去执行

Fiber包含三层含义:

  1. 作为架构来说,之前React15的Reconciler采用递归的方式执行,数据保存在递归调用栈中,所以被称为stack Reconciler。React16的Reconciler基于Fiber节点实现,被称为Fiber Reconciler
  2. 作为静态的数据结构来说,每个Fiber节点对应一个React element,保存了该组件的类型(函数组件/类组件/原生组件…)、对应的DOM节点等信息。
  3. 作为动态的工作单元来说,每个Fiber节点保存了本次更新中该组件改变的状态、要执行的工作(需要被删除/被插入页面中/被更新…)。

数据结构

面试:React相关_第32张图片

  • 整个fiber是个单链表的属性结构
  • 根FiberRoot
  • 通过current指向一个FiberNode,这个current也称为RootFiber,是一个HostRoot类型fiber,也是所有fiber节点的根
  • 每个Fiber节点通过child指向下一个Fiber节点
  • Fiber节点中在通过return指向父节点
  • 通过sibling指向兄弟节点

fiber树

每个Fiber节点有个对应的React element,多个Fiber节点是如何连接形成树呢?靠如下三个属性:

// 指向父级Fiber节点
this.return = null;
// 指向子Fiber节点
this.child = null;
// 指向右边第一个兄弟Fiber节点
this.sibling = null;

面试:React相关_第33张图片
作为一种静态的数据结构,保存了组件相关的信息:

// Fiber对应组件的类型 Function/Class/Host...
this.tag = tag;
// key属性
this.key = key;
// 大部分情况同type,某些情况不同,比如FunctionComponent使用React.memo包裹
this.elementType = null;
// 对于 FunctionComponent,指函数本身,对于ClassComponent,指class,对于HostComponent,指DOM节点tagName
this.type = null;
// Fiber对应的真实DOM节点
this.stateNode = null;

作为动态的工作单元,Fiber中如下参数保存了本次更新相关的信息

// 保存本次更新造成的状态改变相关信息
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;

this.mode = mode;

// 保存本次更新会造成的DOM操作
this.effectTag = NoEffect;
this.nextEffect = null;

this.firstEffect = null;
this.lastEffect = null;

工作原理

react如何刷新当前页面_前端工程师的自我修养:React Fiber 是如何实现更新过程可控的…

Fiber节点可以保存对应的DOM节点。相应的,Fiber节点构成的Fiber树就对应DOM树。

React使用 “双缓存” 来完成Fiber树的构建与替换——对应着DOM树的创建与更新。

在React中最多会同时存在两棵Fiber树。当前屏幕上显示内容对应的Fiber树称为current Fiber树,正在内存中构建的Fiber树称为workInProgress Fiber树。

current Fiber树中的Fiber节点被称为current fiber,workInProgress Fiber树中的Fiber节点被称为workInProgress fiber,他们通过alternate属性连接。

currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;

mount和update过程:
https://react.iamkasong.com/process/doubleBuffer.html#%E5%8F%8C%E7%BC%93%E5%AD%98fiber%E6%A0%91

  • mount:ReactDOM.render先创建fiberRootNode(源码中叫fiberRoot)和rootFiber其中fiberRootNode是整个应用的根节点,rootFiber是所在组件树的根节点。 fiberRootNodecurrent会指向当前页面上已渲染内容对应Fiber树,即current Fiber树。此时没有挂载dom,current Fiber树为空;render阶段,根据组件返回的JSX在内存中依次创建Fiber节点并连接在一起构建Fiber树,被称为workInProgress Fiber树。构建完后在commit阶段渲染到页面。fiberRootNode的current指针指向workInProgress Fiber树使其变为current Fiber 树。
  • update:触发状态改变后,会开启一次新的render阶段并构建一棵新的workInProgress Fiber 树。和mount时一样,workInProgress fiber的创建可以复用current Fiber树对应的节点数据(这个决定是否复用的过程就是Diff算法)。workInProgress Fiber 树在render阶段完成构建后进入commit阶段渲染到页面上。渲染完毕后,workInProgress Fiber 树变为current Fiber 树。
  • 在组件mount时,Reconciler根据JSX描述的组件内容生成组件对应的Fiber节点。
  • 在update时,Reconciler将JSX与Fiber节点保存的数据对比,生成组件对应的Fiber节点,并根据对比结果为Fiber节点打上标记。

挂起

当第一个小任务完成后,先判断这一帧是否还有空闲时间,没有就挂起下一个任务的执行,记住当前挂起的节点,让出控制权给浏览器执行更高优先级的任务。

恢复

在浏览器渲染完一帧后,判断当前帧是否有剩余时间,如果有就恢复执行之前挂起的任务。如果没有任务需要处理,代表调和阶段完成,可以开始进入渲染阶段。 这样完美的解决了调和过程一直占用主线程的问题。

  1. 浏览器刷新率(帧)
    页面的内容都是一帧一帧绘制出来的,浏览器刷新率代表浏览器一秒绘制多少帧。目前浏览器大多是 60Hz(60帧/s),每一帧耗时也就是在 16ms 左右。原则上说 1s 内绘制的帧数也多,画面表现就也细腻。那么在这一帧的(16ms) 过程中浏览器又干了啥呢?

面试:React相关_第34张图片
通过上面这张图可以清楚的知道,浏览器一帧会经过下面这几个过程:

  1. 接受输入事件
  2. 执行事件回调
  3. 开始一帧
  4. 执行 RAF (RequestAnimationFrame)
  5. 页面布局,样式计算
  6. 渲染
  7. 执行 RIC (RequestIdleCallback)

如何判断一帧是否有空闲时间的呢?
答案就是我们前面提到的 RIC (RequestIdleCallback) 浏览器原生 API,React 源码中为了兼容低版本的浏览器,对该方法进行了 Polyfill。
RIC 事件不是每一帧结束都会执行,只有在一帧的 16ms 中做完了前面 6 件事儿且还有剩余时间,才会执行。这里提一下,如果一帧执行结束后还有时间执行 RIC 事件,那么下一帧需要在事件执行结束才能继续渲染,所以 RIC 执行不要超过 30ms,如果长时间不将控制权交还给浏览器,会影响下一帧的渲染,导致页面出现卡顿和事件响应不及时。

当恢复执行的时候又是如何知道下一个任务是什么呢?
答案在前面提到的链表。在 React Fiber 中每个任务其实就是在处理一个 FiberNode 对象,然后又生成下一个任务需要处理的 FiberNode。

  • 在一次任务结束后返回该处理节点的子节点或兄弟节点或父节点
  • 只要有节点返回,说明还有下一个任务,下一个任务的处理对象就是返回的节点。
  • 通过一个全局变量记住当前任务节点,当浏览器再次空闲的时候,通过这个全局变量,找到它的下一个任务需要处理的节点恢复执行
  • 就这样一直循环下去,直到没有需要处理的节点返回,代表所有任务执行完成。最后大家手拉手,就形成了一颗 Fiber 树。

终止

其实并不是每次更新都会走到提交(commit)阶段。当在调和过程中触发了新的更新,在执行下一个任务的时候,判断是否有优先级更高的执行任务,如果有就终止原来将要执行的任务,开始新的 workInProgressFiber 树构建过程,开始新的更新流程。这样可以避免重复更新操作。这也是在 React 16 以后生命周期函数 componentWillMount 有可能会执行多次的原因。

合成事件和原生事件的区别

React合成事件一套机制:React并不是将click事件直接绑定在dom上面,而是采用事件冒泡的形式冒泡到document上面,然后React将事件封装给正式的函数处理运行和处理。

如果DOM上绑定了过多的事件处理函数,整个页面响应以及内存占用可能都会受到影响。React为了避免这类DOM事件滥用,同时屏蔽底层不同浏览器之间的事件系统差异,实现了一个中间层——SyntheticEvent

  1. 当用户在为onClick添加函数时,React并没有将Click时间绑定在DOM上面。
  2. 而是在document处监听所有支持的事件,当事件发生并冒泡至document处时,React将事件内容封装交给中间层SyntheticEvent(负责所有事件合成)
  3. 所以当事件触发的时候,对使用统一的分发函数dispatchEvent将指定函数执行。

原生绑定快于合成事件绑定。

  1. React组件上声明的事件最终绑定到了document这个DOM节点上,而不是React组件对应的DOM节点。故只有document这个节点上面才绑定了DOM原生事件,其他节点没有绑定事件。这样简化了DOM原生事件,减少了内存开销
  2. React以队列的方式,从触发事件的组件向父组件回溯,调用它们在JSX中声明的callback。也就是React自身实现了一套事件冒泡机制。我们没办法用event.stopPropagation()来停止事件传播,应该使用event.preventDefault()
  3. React有一套自己的合成事件SyntheticEvent,不同类型的事件会构造不同的SyntheticEvent
  4. React使用对象池来管理合成事件对象的创建和销毁,这样减少了垃圾的生成和新对象内存的分配,大大提高了性能

16和15的区别

生命周期

1.React16新的生命周期弃用了componentWillMount、componentWillReceivePorps,componentWillUpdate

2.新增了getDerivedStateFromProps、getSnapshotBeforeUpdate来代替弃用的三个钩子函数(componentWillMount、componentWillReceivePorps,componentWillUpdate)

3.React16并没有删除这三个钩子函数,但是不能和新增的两个钩子函数(getDerivedStateFromProps、getSnapshotBeforeUpdate)混用。

注意:React17将会删除componentWillMount、componentWillReceivePorps,componentWillUpdate

4.React16新增了对错误处理的钩子函数(componentDidCatch)

stack到fiber

见上方fiber起源/含义

props和state区别

属性 props 是外界传递过来的,状态 state 是组件本身的,状态可以在组件中任意修改

class组件/函数组件的问题

class 组件存在的问题

  1. 大型组件很难拆分和重构,变得难以测试
  2. 相同业务逻辑分散到各个方法中,可能会变得混乱
  3. 复用逻辑可能变得复杂,如 HOC 、Render Props

你可能感兴趣的:(React,基础知识,前端)