前端框架:React中经常遇到的问题

597 阅读8分钟

1.Hooks 原理

  • 为什么只能在函数组件中调用Hooks,不要在循环,条件判断,函数嵌套中使用Hooks
  • 为什么useEffect第二个参数是空数组,就相当于ComponentDidMount,只会执行一次
  • 自定义的Hook是如何影响使用它的函数组件的
  • Capture Value 特性是如何产生的 举个例子: useState,React会在第一次渲染时将这个hook放入Hooks链表中,下次渲染时,同样的hooks以相同的顺序被调用。如果放在条件语句中使用hooks就会导致条件满足时有值,条件不满足时无值导致react顺序混乱。

2.React.createElement 的逻辑

  • 如果传入一个字符串div, 就会创建一个虚拟dom.
  • 如果传入一个函数, 就会调用函数,获取其返回值
  • 如果传入一个类, 就会调用一个new, 新建一个对象, 调用对象的render方法, 获取其返回值

3.React Fiber

进程(Progress),线程(Thread),在计算机中还有一个概念叫做Fiber,英文含义“纤维”,意指比线程控制的更精密的并发处理机制。 上面说的Fiber和React Fiber不是相同的概念,但是React团队将这个功能命名为Fiber,含义也是更加紧密的处理机制,比Thread更细。

Stack Reconciler是通过递归的方式进行渲染,使用的是JS引擎自身的函数调用栈,它会一直执行到栈空为止。而Fiber Reconciler实现了自己的组件调用栈,它以链表的形式遍历组件树,可以灵活的暂停、继续和丢弃执行的任务。实现方式是使用了浏览器的requestIdleCallback这个API。官方解释:

window.requestIdleCallback()会在浏览器空闲时期依次调用函数,这就可以让开发者在主事件循环中执行后台或低优先级的任务,而不会对关键的事件(用户行为)产生影响。

react 框架内部的运作可以分为3层:

  • Virtual Dom层,描述页面长什么样子
  • Reconciler层,负责调用组件声明周期方法,进行Diff运算等
  • Renderer层,根据不同的平台,渲染出响应的页面,比较常见的是ReactDom和ReactNative。 Fiber优化,就发生在Reconciler层,因此也叫做Fiber Reconciler Fiber其实是一种数据结构,它可以用一个纯JS对面来表示:
const fiber = {
    stateNode, // 节点实例
    child, // 子节点
    sibling, // 兄弟节点
    return, // 父节点
}

以前的Reconciler被命名为stack Reconciler,stackReconciler运作的过程是不能被打断的,必须一直运行完。 而Fiber Reconciler每执行一段时间,都会将控制权交给浏览器,可以分段执行, 为了达到这种效果,就需要一个调度器(Scheduler)来进行任务分配。任务的优先级有六种:

  • synchronous,与之前的Stack Reconciler操作一样,同步执行
  • task,在next tick之前执行
  • animation,下一帧之前执行
  • high,在不久的将来立即执行
  • low,稍微延迟执行也没关系
  • offscreen,下一次render时或scroll时才执行 优先级高的任务可以打断优先级低的任务(如diff)的执行,从而更快的生效 其中Fiber Reconciler在执行过程中,分为2个阶段
  • 阶段一,生成Fiber树,得出需要更新的节点信息,这一步是一个渐进的过程,可以被打断
  • 阶段二,将需要更新的节点一次批量更新,这个过程不能被打断。

Fiber树

Fiber Reconciler在阶段一进行Diff计算的时候,会生成一颗Fiber树。这棵树是在VirtualDOM树的基础上增加额外的信息来生成的,它的本质是一个链表

4.setState

setState并不是异步的方法,但是异步的表现是因为react框架本身的性能机制。 react中优化除了virtualDOM的优化,减少数据更新的频率是另外的一种手段,这就是React的批量更新

  • 在合成事件和生命周期钩子函数中(除componentDidUpdate),setState的表现都是异步的。这是因为,当更新策略正在事务流的执行中时,组件更新会被推入dirtyComponents队列中等待执行,否则开始执行batchedUpdates队列更新。而在生命周期钩子调用中,更新策略都处于更新之前,组件仍处于事务流中,而componentDidUpdate是在更新之后,此时组件已经不在事务流中,因此表现为同步。在合成事件中,react是基于事务流完成的事件委托机制实现,也是处于事务流中。 总结,合成事件和生命周期钩子函数都是在事务流中执行,而setState(组件更新)运行是在事务流之后,所以表现为异步。

这是无法在setState后马上获得更新之后的值。解决方法,在setState(updater,callback),在回调中获取最新值

  • 在原生事件和setTimeout中,setState表现为同步,可以马上获取更新后的值

原因:原生事件是浏览器本身的实现,与事务流无关,而setTimeout是放置在宏任务中延后执行,此时事务流已经结束。

  • 批量更新,在合成事件和声明周期钩子中,setState更新队列,存储的是合并状态,因此前面设置的key值会被后面的所覆盖,最终只会执行一次更新。
  • 函数式:由于Fiber及合并的问题,官方推荐可以传入函数的形式,在fn中返回新的State对象,this.setState((state,props)=>newState)使用函数式可以用于避免setState的批量更新的逻辑,传入的函数将会被顺序调用。

5.HOC(高阶组件)

  • 高阶组件不是组件,是增强函数,可以输入一个元组件,返回一个新的增强组件
  • 稿件组件的主要作用是代码复用,操作状态和参数

6. Hooks

useState:组件更新 useEffect:执行副作用 useRef:保存数据 useMemo:缓存优化

7.useEffect(fn,[]) 和componentDidMount有什么区别

  • 其实是fn与componentDidMount的执行时机有什么不同而对于useEffect的回调函数fn来说,它的执行是依赖于useEffect的第二个参数 因此我们可以将这个题概括为两个题
  1. useEffect第二个参数[]如何影响fn的执行?
  2. fn和componentDidMount的执行时机

react的生命周期函数,在调用this.setState后首先我们会计算出状态变化,接着将状态变化渲染在视图中,在react中计算出状态变化这部分被称为render阶段,将状态变化渲染到视图之中这部分被称之为commit阶段。render阶段是通过一种被称为effect的数据结构(在新版react中又被称之为flags)将状态变化传递给commit阶段的。render阶段到commit阶段传递了一条包含了不同fiber节点的effect的链表

  • 对于要插入DOM的元素我们会在对应的fiber节点上增加Placement的effect,
  • 对于需要更新DOM的元素,我们会在对应的fiber节点上增加Update的effect,
  • 对于需要删除DOM的元素,我们会在对应的fiber节点上增加Deletion的effect,
  • 对于需要更新ref属性的DOM元素,我哦们会在对应的fiber上增加Ref effect,
  • 对于包含useEffect回调执行的fiber来说,我们会在对应的fiber上增加passive effect,
  • 总之,所有与视图相关的操作都有对应的effect useEffect的第二个参数是如何影响fn执行的?换句话说,它的第二个参数是如何影响对应的fiber创建passive effect

条件:

  1. 没有第二个参数,
  2. 第二个参数是空数组,
  3. 第二个参数有值

组件状态: mount时,update时,dep变化时 结果:创建passive

  • 第一种条件,在没有第二个参数时,会在组件mount时创建passiveEffect,在组件update时创建passiveEffect,因此在每次render时都会创建passiveEffect
  • 第二种条件,那么它会在mount时创建passiveEffect
  • 第三种条件,包含一个依赖项,那么它会在mount时以及依赖项发生变化时创建passiveEffect

在commit阶段是如何处理这条链表的每个effect的, 我们知道commit阶段将状态变化渲染在视图中,更进一步是将effect渲染在视图中,我们可以将commit阶段分为三个子阶段,

  1. 渲染视图前(beforeMutation阶段)
  2. 渲染视图(mutation阶段)
  3. 渲染视图后(layout阶段)
  • placement来说它会在mutation阶段执行对DOM节点appendChild操作,这样我们的DOM节点就会被插入到视图中
  • 接下来会在layout阶段调用componentDidMount
  • 而对于PassiveEffect来说它会在commit阶段的三个子阶段完成以后异步的调用useEffect的回调函数

因此它们之间的区别: 调用时机是完全不同

  1. useEffect会在commit阶段执行完成以后异步的调用回调函数
  2. componetDidMount则会在layout这个子阶段同步的调用

useLayoutEffect hook它的调用时机和componentDidMount一致的,它也会在layout阶段同步调用

useEffect异步执行的原因主要是防止同步执行时阻塞浏览器渲染。

8.react生命周期

16775500-8d325f8093591c76.webp

9.JSX与Fiber节点

JSX是一种描述当前组件内容的数据结构,它不包含组件schedule、reconcile、render所需的相关信息 比如以下信息就不包括在JSX中:

  • 组件在更新中的优先级
  • 组件的state
  • 组件被打上的用于renderer的标记 这些内容都包含在Fiber节点中 所以,
  • 在组件mount时,Reconciler根据JSX描述的组件内容生成组件对应的Fiber节点。
  • 在update时,Reconciler将JSX与Fiber节点保存的数据对比,生成组件对应的Fiber节点,并根据对比结果为Fiber节点打上标记。