Henry-Diasa / awesome_interview_question

总结前端面试题,更贴近于实战,而非背诵的八股文。
11 stars 0 forks source link

【React】面试题 #502

Open Henry-Diasa opened 3 months ago

Henry-Diasa commented 3 months ago

组件基础

事件机制

image

fiber

image

diff

image q

HOC案例

权限控制、性能追踪、页面复用 image

有状态和无状态组件

image

React中可以在render访问refs吗?

image

React.createPortal

image

受控和非受控组件

image

React.forwardRef 是什么?

image

类组件中使用函数组件(hook)

render props https://cloud.tencent.com/developer/article/1755753

时间切片 idleCallback实现

https://juejin.cn/post/7167335700424196127

Henry-Diasa commented 3 months ago

数据管理

setState原理(过时)

image

redux的state是如何注入的(connect)

image

react中的props为什么是只读的?

image

getDerivedStateFromProps

image

React中检验props

image

Henry-Diasa commented 3 months ago

生命周期

生命周期有哪些

image image

React废弃了哪些生命周期?

image

Henry-Diasa commented 3 months ago

组件通信

通信的方式

image

路由

路由实现原理

image

Link标签和a标签的区别

image

redux

工作原理

image

中间件

image

redux状态管理和window上的区别

image

mobx和redux

image

redux中间件怎么拿到store和action的

image

redux中connect的作用

image

Henry-Diasa commented 3 months ago

hooks

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

image

hooks解决了什么问题

image

useEffect 和 useLayoutEffect

image

react与vue的diff算法的区别

image

其他

react数据持久化实践

redux-persist

react和vue的不同

image

react中的props.children和react.children区别

image

Henry-Diasa commented 3 months ago

hooks原理

二 hooks与fiber(workInProgress)

之前章节讲过,类组件的状态比如 state ,context ,props 本质上是存在类组件对应的 fiber 上,包括生命周期比如 componentDidMount ,也是以副作用 effect 形式存在的。那么 Hooks 既然赋予了函数组件如上功能,所以 hooks 本质是离不开函数组件对应的 fiber 的。 hooks 可以作为函数组件本身和函数组件对应的 fiber 之间的沟通桥梁。

hook1.jpg

hooks 对象本质上是主要以三种处理策略存在 React 中:

一个 hooks 对象应该长成这样:

const HooksDispatcherOnMount = { /* 函数组件初始化用的 hooks */
    useState: mountState,
    useEffect: mountEffect,
    ...
}
const  HooksDispatcherOnUpdate ={/* 函数组件更新用的 hooks */
   useState:updateState,
   useEffect: updateEffect,
   ...
}
const ContextOnlyDispatcher = {  /* 当hooks不是函数内部调用的时候,调用这个hooks对象下的hooks,所以报错。 */
   useEffect: throwInvalidHookError,
   useState: throwInvalidHookError,
   ...
}

函数组件触发

所有函数组件的触发是在 renderWithHooks 方法中,在 fiber 调和过程中,遇到 FunctionComponent 类型的 fiber(函数组件),就会用 updateFunctionComponent 更新 fiber ,在 updateFunctionComponent 内部就会调用 renderWithHooks 。

react-reconciler/src/ReactFiberHooks.js

let currentlyRenderingFiber
function renderWithHooks(current,workInProgress,Component,props){
currentlyRenderingFiber = workInProgress;
workInProgress.memoizedState = null; /* 每一次执行函数组件之前,先清空状态 (用于存放hooks列表)*/
workInProgress.updateQueue = null;    /* 清空状态(用于存放effect list) */
ReactCurrentDispatcher.current =  current === null || current.memoizedState === null ? HooksDispatcherOnMount : HooksDispatcherOnUpdate /* 判断是初始化组件还是更新组件 */
let children = Component(props, secondArg); /* 执行我们真正函数组件,所有的hooks将依次执行。 */
ReactCurrentDispatcher.current = ContextOnlyDispatcher; /* 将hooks变成第一种,防止hooks在函数组件外部调用,调用直接报错。 */
}

workInProgress 正在调和更新函数组件对应的 fiber 树。

  • 对于类组件 fiber ,用 memoizedState 保存 state 信息,对于函数组件 fiber ,用 memoizedState 保存 hooks 信息
  • 对于函数组件 fiber ,updateQueue 存放每个 useEffect/useLayoutEffect 产生的副作用组成的链表。在 commit 阶段更新这些副作用。
  • 然后判断组件是初始化流程还是更新流程,如果初始化用 HooksDispatcherOnMount 对象,如果更新用 HooksDispatcherOnUpdate 对象。函数组件执行完毕,将 hooks 赋值给 ContextOnlyDispatcher 对象。引用的 React hooks都是从 ReactCurrentDispatcher.current 中的, React 就是通过赋予 current 不同的 hooks 对象达到监控 hooks 是否在函数组件内部调用。
  • Component ( props , secondArg ) 这个时候函数组件被真正的执行,里面每一个 hooks 也将依次执行。
  • 每个 hooks 内部为什么能够读取当前 fiber 信息,因为 currentlyRenderingFiber ,函数组件初始化已经把当前 fiber 赋值给 currentlyRenderingFiber ,每个 hooks 内部读取的就是 currentlyRenderingFiber 的内容。

hooks初始化- hooks 如何和 fiber 建立起关系

hooks 初始化流程使用的是 mountState,mountEffect 等初始化节点的hooks,将 hooks 和 fiber 建立起联系,那么是如何建立起关系呢,每一个hooks 初始化都会执行 mountWorkInProgressHook ,接下来看一下这个函数。

react-reconciler/src/ReactFiberHooks.js

function mountWorkInProgressHook() {
const hook = {  memoizedState: null, baseState: null, baseQueue: null,queue: null, next: null,};
if (workInProgressHook === null) {  // 只有一个 hooks
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {  // 有多个 hooks
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}

首先函数组件对应 fiber 用 memoizedState 保存 hooks 信息,每一个 hooks 执行都会产生一个 hooks 对象,hooks 对象中,保存着当前 hooks 的信息,不同 hooks 保存的形式不同。每一个 hooks 通过 next 链表建立起关系。

假设在一个组件中这么写

export default function Index(){
    const [ number,setNumber ] = React.useState(0) // 第一个hooks
    const [ num, setNum ] = React.useState(1)      // 第二个hooks
    const dom = React.useRef(null)                 // 第三个hooks
    React.useEffect(()=>{                          // 第四个hooks
        console.log(dom.current)
    },[])
    return <div ref={dom} >
        <div onClick={()=> setNumber(number + 1 ) } > { number } </div>
        <div onClick={()=> setNum(num + 1) } > { num }</div>
    </div>
}

那么如上四个 hooks ,初始化,每个 hooks 内部执行 mountWorkInProgressHook ,然后每一个 hook 通过 next 和下一个 hook 建立起关联,最后在 fiber 上的结构会变成这样。

效果:

hook2.jpg

hooks更新

更新 hooks 逻辑和之前 fiber 章节中讲的双缓冲树更新差不多,会首先取出 workInProgres.alternate 里面对应的 hook ,然后根据之前的 hooks 复制一份,形成新的 hooks 链表关系。这个过程中解释了一个问题,就是hooks 规则,hooks 为什么要通常放在顶部,hooks 不能写在 if 条件语句中,因为在更新过程中,如果通过 if 条件语句,增加或者删除 hooks,在复用 hooks 过程中,会产生复用 hooks 状态和当前 hooks 不一致的问题。举一个例子,还是将如上的 demo 进行修改。

将第一个 hooks 变成条件判断形式,具体如下:

export default function Index({ showNumber }){
    let number, setNumber
    showNumber && ([ number,setNumber ] = React.useState(0)) // 第一个hooks
}

第一次渲染时候 showNumber = true 那么第一个 hooks 会渲染,第二次渲染时候,父组件将 showNumber 设置为 false ,那么第一个 hooks 将不执行,那么更新逻辑会变成这样。

hook复用顺序 缓存的老hooks 新的hooks
第一次hook复用 useState useState
第二次hook复用 useState useRef

hook3.jpeg

第二次复用时候已经发现 hooks 类型不同 useState !== useRef ,那么已经直接报错了。所以开发的时候一定注意 hooks 顺序一致性。

报错内容:

hookk4.jpg

三 状态派发

useState 解决了函数组件没有 state 的问题,让无状态组件有了自己的状态,useState 在 state 章节已经说了基本使用,接下来重点介绍原理使用, useState 和 useReducer 原理大同小异,本质上都是触发更新的函数都是 dispatchAction。

比如一段代码中这么写:

const [ number,setNumber ] = React.useState(0)  

setNumber 本质就是 dispatchAction 。首先需要看一下执行 useState(0) 本质上做了些什么?

react-reconciler/src/ReactFiberHooks.js

function mountState(initialState){
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {initialState = initialState() } // 如果 useState 第一个参数为函数,执行函数得到初始化state
hook.memoizedState = hook.baseState = initialState;
const queue = (hook.queue = { ... }); // 负责记录更新的各种状态。
const dispatch = (queue.dispatch = (dispatchAction.bind(  null,currentlyRenderingFiber,queue, ))) // dispatchAction 为更新调度的主要函数 
return [hook.memoizedState, dispatch];
}

接下来重点研究一下 dispatchAction ,底层是怎么处理更新逻辑的。

function dispatchAction(fiber, queue, action){
    /* 第一步:创建一个 update */
    const update = { ... }
    const pending = queue.pending;
    if (pending === null) {  /* 第一个待更新任务 */
        update.next = update;
    } else {  /* 已经有带更新任务 */
       update.next = pending.next;
       pending.next = update;
    }
    if( fiber === currentlyRenderingFiber ){
        /* 说明当前fiber正在发生调和渲染更新,那么不需要更新 */
    }else{
       if(fiber.expirationTime === NoWork && (alternate === null || alternate.expirationTime === NoWork)){
            const lastRenderedReducer = queue.lastRenderedReducer;
            const currentState = queue.lastRenderedState;                 /* 上一次的state */
            const eagerState = lastRenderedReducer(currentState, action); /* 这一次新的state */
            if (is(eagerState, currentState)) {                           /* 如果每一个都改变相同的state,那么组件不更新 */
               return 
            }
       }
       scheduleUpdateOnFiber(fiber, expirationTime);    /* 发起调度更新 */
    }
}

原来当每一次改变 state ,底层会做这些事。

接下来就是更新的环节,下面模拟一个更新场景。

export default  function Index(){
    const [ number , setNumber ] = useState(0)
    const handleClick=()=>{
        setNumber(num=> num + 1 ) // num = 1
        setNumber(num=> num + 2 ) // num = 3 
        setNumber(num=> num + 3 ) // num = 6
    }
    return <div>
        <button onClick={() => handleClick() } >点击 { number } </button>
    </div>
}
function updateReducer(){
    // 第一步把待更新的pending队列取出来。合并到 baseQueue
    const first = baseQueue.next;
    let update = first;
   do {
        /* 得到新的 state */
        newState = reducer(newState, action);
    } while (update !== null && update !== first);
     hook.memoizedState = newState;
     return [hook.memoizedState, dispatch];
}

用一幅图来描述整个流程。

hook5.jpg

四 处理副作用

初始化

在 fiber 章节讲了,在 render 阶段,实际没有进行真正的 DOM 元素的增加,删除,React 把想要做的不同操作打成不同的 effectTag ,等到commit 阶段,统一处理这些副作用,包括 DOM 元素增删改,执行一些生命周期等。hooks 中的 useEffect 和 useLayoutEffect 也是副作用,接下来以 effect 为例子,看一下 React 是如何处理 useEffect 副作用的。

下面还是以初始化和更新两个角度来分析。

function mountEffect(create,deps){
    const hook = mountWorkInProgressHook();
    const nextDeps = deps === undefined ? null : deps;
    currentlyRenderingFiber.effectTag |= UpdateEffect | PassiveEffect;
    hook.memoizedState = pushEffect( 
      HookHasEffect | hookEffectTag, 
      create, // useEffect 第一次参数,就是副作用函数
      undefined, 
      nextDeps, // useEffect 第二次参数,deps    
    )
}

为什么 React 会这么设计呢,首先对于类组件有componentDidMount/componentDidUpdate 固定的生命周期钩子,用于执行初始化/更新的副作用逻辑,但是对于函数组件,可能存在多个 useEffect/useLayoutEffect ,hooks 把这些 effect,独立形成链表结构,在 commit 阶段统一处理和执行。

如果在一个函数组件中这么写:

React.useEffect(()=>{
    console.log('第一个effect')
},[ props.a ])
React.useLayoutEffect(()=>{
    console.log('第二个effect')
},[])
React.useEffect(()=>{
    console.log('第三个effect')
    return () => {}
},[])

那么在 updateQueue 中,副作用链表会变成如下样子:

hook6.jpg

更新

更新流程对于 effect 来说也很简单,首先设想一下 useEffect 更新流程,无非判断是否执行下一次的 effect 副作用函数。还有一些细枝末节。

function updateEffect(create,deps){
    const hook = updateWorkInProgressHook();
    if (areHookInputsEqual(nextDeps, prevDeps)) { /* 如果deps项没有发生变化,那么更新effect list就可以了,无须设置 HookHasEffect */
        pushEffect(hookEffectTag, create, destroy, nextDeps);
        return;
    } 
    /* 如果deps依赖项发生改变,赋予 effectTag ,在commit节点,就会再次执行我们的effect  */
    currentlyRenderingFiber.effectTag |= fiberEffectTag
    hook.memoizedState = pushEffect(HookHasEffect | hookEffectTag,create,destroy,nextDeps)
}

更新 effect 的过程非常简单。

不同的effect

关于 EffectTag 的思考🤔:

五 状态获取与状态缓存

1 对于 ref 处理

在 ref 章节详细介绍过,useRef 就是创建并维护一个 ref 原始对象。用于获取原生 DOM 或者组件实例,或者保存一些状态等。

创建:

function mountRef(initialValue) {
  const hook = mountWorkInProgressHook();
  const ref = {current: initialValue};
  hook.memoizedState = ref; // 创建ref对象。
  return ref;
}

更新:

function updateRef(initialValue){
  const hook = updateWorkInProgressHook()
  return hook.memoizedState // 取出复用ref对象。
}

如上 ref 创建和更新过程,就是 ref 对象的创建和复用过程。

2 对于useMemo的处理

对于 useMemo ,逻辑比 useRef 复杂点,但是相对于 useState 和 useEffect 简单的多。

创建:

function mountMemo(nextCreate,deps){
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

更新:

function updateMemo(nextCreate,nextDeps){
    const hook = updateWorkInProgressHook();
    const prevState = hook.memoizedState; 
    const prevDeps = prevState[1]; // 之前保存的 deps 值
    if (areHookInputsEqual(nextDeps, prevDeps)) { //判断两次 deps 值
        return prevState[0];
    }
    const nextValue = nextCreate(); // 如果deps,发生改变,重新执行
    hook.memoizedState = [nextValue, nextDeps];
    return nextValue;
}

react18下的自动批量更新

在18的版本中,react实现了自动的批量处理,内部实现的原理很简单,通过事件的循环实现。即将当前事件循环的宏任务中所有setState更新需要的数据挂载到对应的fiber上,并将第一个setState产生的performWorkOnRoot方法放在本次循环的微任务或者下一次循环的宏任务中执行,当前循环宏任务的其他setState只产生数据,不执行performWorkOnRoot,数据的更新都在微任务或者下次宏任务中批量更新一次。这样即实现了setState的批量更新。详细过程看代码注释。

// setState
enqueueSetState: function (inst, payload, callback) {
    ...
    // 获取优先级,一般在同一个宏任务执行的setState都会返回相同的lane,
    // 在一个宏任务中的多个setState根据相同的lane实现批量更新
    var lane = requestUpdateLane(fiber);
    var update = createUpdate(eventTime, lane); // 创建更新信息
    update.payload = payload;
    ...
    // 将更新数据挂在fiber上,此处update是通过链表结构添加到原有update.next上
    var root = enqueueUpdate(fiber, update, lane);
    if (root !== null) {
      scheduleUpdateOnFiber(root, fiber, lane, eventTime); // 调度进行更新
      ...
    }
    ...
  },

 function scheduleUpdateOnFiber(root, fiber, lane, eventTime) {
     ...
     ensureRootIsScheduled(root, eventTime);
     // 此处和17版本的差别,17版本中不判断fiber.mode,导致异步内的setState如果没有使用batchupdates方法包裹去设置executionContext的值,会进入该分支依次执行syncQueue的任务
     if (lane === SyncLane && executionContext === NoContext && (fiber.mode & ConcurrentMode) === NoMode && ( ReactCurrentActQueue$1.isBatchingLegacy)) {
      resetRenderTimer();
      flushSyncCallbacksOnlyInLegacyMode();
    }
     ...
 }

 function ensureRootIsScheduled(root, currentTime) {
    var existingCallbackNode = root.callbackNode; // 获取暂停的任务
    ...
    // 即将执行任务的lanes
    var nextLanes = getNextLanes(root, root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes);
    ...
    var newCallbackPriority = getHighestPriorityLane(nextLanes); // 获取任务中最高的优先级
    var existingCallbackPriority = root.callbackPriority; // 暂停任务的优先级
    // 暂停任务优先级与即将执行任务的优先级相同,此时退出不再向下执行,
    // 执行到此处,该次setState更新正确结果所需要的数据已存储在对应的fibere
    if (existingCallbackPriority === newCallbackPriority && !( ReactCurrentActQueue$1.current !== null && existingCallbackNode !== fakeActCallbackNode)) {
    {
    ...
    return;
  }
  ...
  var newCallbackNode;
  /* newCallbackPriority === 1,点击等事件会走这个分支
  ** handleClick = () => {
  **   setState();
  **   setState();
  ** }
  */
  if (newCallbackPriority === SyncLane) {
    if (root.tag === LegacyRoot) {
      if ( ReactCurrentActQueue$1.isBatchingLegacy !== null) {
        ReactCurrentActQueue$1.didScheduleLegacyUpdate = true;
      }
      scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));
    } else {
    // 将performSyncWorkOnRoot添加到syncQueue
    // 相同优先级之添加一次,其他次已在上面判断中return出去了
      scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
    }
    {
      if ( ReactCurrentActQueue$1.current !== null) {
        ...
      } else {
      // 创建一个微任务,在该次事件循环的宏任务结束后执行
        scheduleMicrotask(function () {
          if ((executionContext & (RenderContext | CommitContext)) === NoContext) {
           // 依次执行syncQueue的任务
            flushSyncCallbacks();
          }
        });
      }
    }
    newCallbackNode = null;
  } else {
     /* 异步,微任务获取优先级不为1的走该分支
      ** setTimeout(() => {
      **   setState();
      **   setState();
      ** }, 0)
      */
    var schedulerPriorityLevel;
    switch (lanesToEventPriority(nextLanes)) {
       ...
    }
    // scheduleCallback$1方法调用scheduler的scheduleCallback方法
    // 该方法最终执行port.postmessage,
    // 在监听message变化时,即下次事件循环中执行performConcurrentWorkOnRoot方法
    // 在本次事件循环宏任务中所有的setState更新都这次的performConcurrentWorkOnRoot执行
    newCallbackNode = scheduleCallback$1(schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root));
  }

  root.callbackPriority = newCallbackPriority;
  root.callbackNode = newCallbackNode;
}