React面经
目录
1 React和vue的区别
4.2 lmmutable
4.3 无状态组件
4.4 Hooks
4.4.1 useState
4.4.2 useEffect
4.4.3 usecontext
4.4.4 其他
4.4.5 高阶组件
4.4.6 原理
4.5 状态管理
4.5.1 Redux
4.5.2 Mobx
4.5.3 zuStand
4.6 Router
4.6.1 Router
4.6.2 模式
4.6.3 对比Vue
4.7 虚拟DOM
4.7.1 Render
4.7.2 Fiber
4.7.3 Diff
4.8 错误边界
4.9 事件代理
4.10 Class组件
4.12 组件通信
4.13 vue 实现
4.14 受控组件
4.15 优先级
4.1 React和vue的区别
1)相同点
- 都有组件化思想
- 都支持服务器端渲染
- 都有Virtual DOM(虚拟dom)
- 数据驱动视图
- 都有支持native的方案:Vue的weex、React的React native
- 都有自己的构建工具:Vue的vue-cli、React的Create React App
2)区别
写在前面: 前端选型无非是考虑包体积和响应速度两种,目前的前端框架都需要编译这一步骤,分为构建时编译(AOT),宿主环境执行时编译(JIT)【区别:JIT首次加载慢于AOT,因为需要先编译,且体积可能大于AOT,因为运行时会增加编译器代码】,Angular提供了两种方式但是没人用。 借助AOT对模板语法编译时的优化,比如vue3由于模板是固定的,因此在编译时可以分析模板语法中的静态部分和动态部分做出优化,Svelte可以利用AOT直接建立这部分的关系,在运行时当自变量发生变化直接驱动UI变化,但是JSX很灵活导致很难进行静态分析。【react采用prepack进行过改进,但2019年放弃了。也尝试过使用forget自动生成等效于memo等的代码的编译器,但使用模板减少了jsx的灵活性,也有人使用millionJS直接将元素绑定在dom上等】。 既然jsx无法实现AOT,那么就采用了vDom进行优化,并把memo等缓存交给程序员进行work。 前端框架分为元素级(svelte),组件级(vue),应用级(react),对于svelte来说,由于可以确定自变量是否变化,如果组件没有使用store则不会引入这一特性,会使得其对于小型应用比react的体积小,对于大型应用由于元素级直接绑定dom导致体积逐渐增大。vue采用模板语法,建立自变量与组件之间的关系,因此可以受益于AOT。react每次从应用的根节点开始遍历,甚至不知道哪个自变量变了就开始更新,导致不需要细粒度更新和AOT,当然也采用了调度,时间切片等进行优化。 框架性能瓶颈: **react:** 高频率的交互往往会导致明显的性能问题,在 antd 的 Form 组件也使用了将数据下放到每一个 Item 的方式来优化性能,store 中用 useRef 存储数据而不是 useState,antd 内部为每个 Form.Item 定义了 forceUpdate 来强制更新 Item UI。又例如拖拽/resize等事件。此时我们只需要通过操作原生 DOM 的方式来实现对应的逻辑即可。从而绕开高频率的 diff 逻辑。 react 常常因为闭包问题,被各种攻击。认为这是 react 的缺陷。 事实上,原生 DOM 本身在高频交互上也存在明显的性能瓶颈。因此许多前端项目不得不采用抛弃 DOM 渲染的方式来完成整个项目【DOM 换成了 canvas,或者 webGPU..】。但是这些项目我们仍然可以结合 react 来完成,例如著名的前端项目 **Figma**,或者国内有的团队使用 react + skia 的方式来完成一些对性能要求很高的项目 **Solid:** 为了极致的性能体验,完全弃用了虚拟 DOM,也就意味着,他放弃了跨平台的特性。只把主要精力集中在 web 项目上。也就是说,他的全局生态建设,永远也赶不上 react。 **vue:** 丢失响应式,如解构。
- 数据流向的不同。react从诞生开始就推崇单向数据流,而Vue是双向数据流
在双向绑定的建立过程中,有一个理想的结果:我们可以轻易的知道数据与 DOM 节点的对应关系,那么通过数据驱动 UI 的形式来开发代码将会变得非常容易。双向绑定采取的措施是递归遍历监听所有数据,依次建立与对应 UI 的绑定关系。这种解决方案所花费的成本主要体现在对数据的处理上,他面临两个问题: 一是数据的变化需要监听,但是某些数据类型的监听在实现上有难度,比如 forceUpdate,比如大量的 Watcher,还有性能损耗更严重的 Deep Watcher。另一个问题就是数据的层级与变化问题,数据层级越深,我们想要深度监听,就得使用递归的方式。当数据发生变化时,部分数据与 UI 的绑定关系需要重新建立「在 vue 中,就是重复依赖收集的过程」,如果数据量过大,或者数据变化频繁,就会有性能风险。 react 把所有的精力都放在了 UI 层。使用我们现在熟知的 diff 算法,当数据发生变化时,react 会创建一个新的虚拟DOM树,与之前的树做对比,找出需要改变的元素。 从总体思路上来说,vue 的主要压力在于处理数据,react 的主要压力在于处理 UI。react 不建立数据与 UI 的对应关系,那么也就意味着另外一个压力的产生,那就是当数据发生变化时,react 并不知道哪一个 UI 发生了变化,于此同时 react 为了保持自己对于 Js 的弱侵入性,也没有在 setState 上进行任何魔改,例如绑定当前上下文从而得知具体哪个组件的 state 发生了变化。[如果进行了这个魔改,diff 的压力会小一些]。因此,每一次的 state 变化,都是整棵 DOM 树的 diff。 Vue2中借鉴了diff算法,vue3中使用Proxy 能够监听数组的变化,能够监听删除对象字段的变化... 于是 Vue3 的底层实现,在数据侧的代码会简洁很多,并且与此同时,Vue 的后续版本,也可以彻底放弃虚拟 DOM 来进一步提高自己的运行性能。但是,依然有一个问题没有解决,那就是深度监听仍然需要递归。当数据量很大的时候,依赖追踪的压力也会逐渐变大,当你的项目变得越来越大,全局数据变得越来越复杂,层级越来越深,他的性能压力也会逐渐变大。因此这也是目前大多数大厂中后台采用React的原因,而面向用户则采用Vue的原因。
- 数据变化的实现原理不同。react使用的是不可变数据,而Vue使用的是可变的数据
Vue2 响应式的特点就是依赖收集,数据可变,自动派发更新,初始化时通过 Object.defineProperty 递归劫持 data 所有属性添加 getter/setter,触发 getter 的时候进行依赖收集,修改时触发 setter 自动派发更新找到引用组件重新渲染。 Vue3 响应式使用原生 Proxy 重构了响应式,一是 proxy 不存在 Vue2响应式存在的缺陷,二是性能更好,不仅支持更多的数据结构,而且不再一开始递归劫持对象属性,而是代理第一层对象本身。运行时才递归,用到才代理,用 effect 副作用来代替 Vue2 里的 watcher,用一个依赖管理中心 trackMap 来统一管理依赖代替 Vue2 中的 Dep,这样也不需要维护特别多的依赖关系,性能上取得很大进步。 React 则是基于状态,单向数据流,数据不可变(需要创建数据的副本来替换掉原数据,为了保证浅比较的正确性),需要手动 setState 来更新,始终保持state的原值不变,在生命周期 shouldComponentUpdate 中,React会对新旧state进行比较,如果直接修改state去用于其他变量的计算,而实际上state并不需要修改,则会导致怪异的更新以及没必要的更新。第二,可追踪修改痕迹,便于排错。而且当数据改变时会以组件根为目录,默认全部重新渲染整个组件树,只能额外用 pureComponent/shouldComponentUpdate/useMemo/useCallback 等方法来进行控制,更新粒度更大一些。
- 组件化通信的不同。react中我们通过使用回调函数来进行通信的,而Vue中子组件向父组件传递消息有两种方式:事件和回调函数
- diff算法不同。react主要使用diff队列保存需要更新哪些DOM,得到patch树,再统一操作批量更新DOM。Vue 使用双向指针,边对比,边更新DOM
Vue2 是同层比较新老 vnode,新的不存在老的存在就删除,新的存在老的不存在就创建,子节点采用双指针头对尾两端对比的方式,全量diff,然后移动节点时通过 splice 进行数组操作 Vue3 是采用 Map 数据结构以及动静结合的方式,在编译阶段提前标记静态节点,Diff 过程中直接跳过有静态标记的节点,并且子节点对比会使用一个 source 数组来记录节点位置及最长递增子序列算法优化了对比流程,快速 Diff,需要处理的边际条件会更少 React 是递归同层比较,标识差异点保存到 Diff 队列保存,得到 patch 树,再统一操作批量更新 DOM。Diff 总共就是移动、删除、增加三个操作,如果结构发生改变就直接卸载重新创建,如果没有则将节点在新集合中的位置和老集合中的 lastIndex 进行比较是否需要移动,如果遍历过程中发现新集合没有,但老集合有就删除
- vue3 做了自己的一套编译优化处理方式。
3)其他
- React的核心理念是用一个hooks解决所有问题,vue的理念是解决不了就新增api
React一直在淡化hooks(useEffect)和生命周期的联系,甚至淡化其与组件的关系。 如在严格模式下,dev环境会触发多次useEffect的回调,目的是想让开发者将useEffect看做针对某个数据源的同步过程。
【如果react支持keepalive,从生命周期的角度理解,effect回调应该执行,从状态角度理解不应该执行】
react的更新策略是掌握在开发者自己手中的,可以主动开启并发更新,对更新做结果优化缓存优化及渲染优化;vue是自动收集依赖的精准更新,没有并发更新特性
4.2 Immutable
1 Immutable
Immutable.js 源自 Facebook ,一个非常棒的不可变数据结构的库。使用另一套数据结构的 API,将所有的原生数据类型转化成Immutable.js 的内部对象,并且任何操作最终都会返回一个新的Immutable
值
Immutable
实现的原理是 Persistent Data Structure
(持久化数据结构):
- 用一种数据结构来保存数据
- 当数据被修改时,会返回一个对象,但是新的对象会尽可能的利用之前的数据结构而不会对内存造成浪费
其出现场景在于弥补 Javascript 没有不可变数据结构的问题,通过 structural sharing来解决的性能问题,内部提供了一套完整的 Persistent Data Structure,还有很多易用的数据类型,如Collection
、List
、Map
、Set
、Record
、Seq
,其中:
- List: 有序索引集,类似 JavaScript 中的 Array
- Map: 无序索引集,类似 JavaScript 中的 Object
- Set: 没有重复值的集合
主要方法:
- fromJS():将js数据转化成Immutable数据;
- toJS():将一个Immutable数据转换为JS类型的数据;
- is():对两个对象进行比较
- get(key):对数据或对象取值
使用 Immutable
可以给 React
应用带来性能的优化,主要体现在减少渲染的次数
在做react
性能优化的时候,为了避免重复渲染,我们会在shouldComponentUpdate()
中做对比,当返回true
执行render
方法
Immutable
通过is
方法则可以完成对比,而无需像一样通过深度比较的方式比较
2 Immer
深层次的对象在没有修改的情况仍然能保证严格相等。这也是它另外一个特点:深层嵌套对象的结构共享相比与 Immer.js,Immutable.js 的不足:
- 自己维护一套数据结构、JavaScript 的数据类型和 Immutable.js 需要相互转换,有入侵性
- 他的操作结果需要通过 toJS 方法才能得到原生对象,这样导致在开发中需要时刻关注操作的是原生对象还是 Immutable.js 返回的结果
- 库的体积大约在 63KB、而 Immer.js 仅有12KB
- API 丰富、学习成本较高
immer 缺点:
- 兼容性:对于不支持
proxy
的浏览器使用defineProperty
实现,在性能上为proxy
的两倍
4.3 无状态组件
有状态组件: 是一个class类,继承componet (用于需要一些状态去存储和修改数据)
无状态组件: 是一个es6写的箭头函数函数,并不继承 componet(用于一些简单的逻辑,比如,父组件向子组件传属性值)
(1) 最大的区别是无状态组件,无法使用state,因为state是继承 componet
(2)无状态组件,没有生命周期函数,生命周期函数是基于state的
通常,函数(function
)与类(class
)最大的区别是:是否能够维护自己的数据(即状态)。函数基本上仅关注动作(action
),而不关心数据的维护,不用维持一个状态,不用把自己的数据保存在内存中。函数使用的数据是从外部获取(或者不获取数据),函数运行时,会完成一系列的动作,最后将结果返回(也可能不返回,仅仅是完成指定的动作)。相对而言,类有能力维护状态(保存数据),也可以定义自己的一系列动作。
一般来说,函数的速度较快,适合用于做表现层,而类能够处理复杂逻辑和状态,适合做逻辑层和数据层。所以,对于 React
来说,一般选择函数来无状态组件,得到所谓的无状态函数(stateless function
),好处是渲染的速度快,所以多使用无状态组件,尽量不要让数据散落在各个组件中。数据集中管理可以更好的保持数据的一致性和可维护性。
有状态组件就是使用类来生成。类可以有自己的状态,维护自己的数据,也是完全符合有状态组件的要求。但是类相对来说速度比函数慢,影响渲染的性能,同时数据过于分散会给后期的维护带来比较大的困难(这也是为什么状态过多时要使用 Redux
的原因),因此要尽量控制有状态组件的数量。当然,类也可以生成无状态组件,但是既然不需要维护状态的工作,用函数能完成得更好,其实也就没有必要使用类来做无状态组件。
在无状态组件每一次函数上下文执行的时候,react用什么方式记录了hooks的状态?
React 使用了一个叫做 "Fiber" 的数据结构来跟踪组件的状态和其它信息。每个函数组件都有一个与之相关的 Fiber,它的 hooks 是按照声明顺序存储在一个数组中。在组件的不同渲染阶段,React 会利用这个 Fiber 和 hooks 数组来追踪和更新每个 hook 的状态。
4.4 Hooks
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性(钩子函数)。为了解决难以重用和共享组件中的与状态相关的逻辑、this的学习成本、逻辑复杂的组件难以开发与维护(当我们的组件需要处理多个互不相关的 local state 时,每个生命周期函数中可能会包含着各种互不相关的逻辑在里面)。
- 只能在函数内部的最外层调用
Hook
,不要在循环、条件判断或者子函数中调用; - 只能在
React
的函数组件中调用Hook
,不要在其他JavaScript
函数中调用。
参考:
https://juejin.cn/post/6857139132259532814
4.4.1 useState
function函数组件中的useState,和 class类组件 setState有什么区别?
useState
与 setState
的最大区别在于函数组件中的 useState
不会合并更新的状态,而类组件的 setState
会。此外,useState
返回的状态更新函数保证在组件的生命周期内保持不变,而 setState
的回调函数可能在组件重新渲染时变化。
为什么两次传入useState的值相同,函数组件不更新?
当传递给 useState
的值与当前状态相同时,React 将跳过渲染和子组件的重新渲染。这是一个优化,因为重新渲染会带来额外的性能开销。如果你需要强制渲染,可以使用 forceUpdate
,或者更改状态值的引用类型(例如,对于对象和数组)。
【setState(obj)如果obj地址不变,那么React就认为数据没有变化。】
setState 拿不到最新的 state
函数组件每次state
变化重渲染,都是新的函数,拥有自身唯一不变的state
值,即memoizedState
上保存的对应的state
值。(capture value特性)。
这也是为什么明明已经setState
却拿不到最新的state
的原因,渲染发生在state更新之前,所以state是当次函数执行时的值,可以通过setState
的回调或ref
的特性来解决这个问题。
为什么组件都重渲染了,数据不会重新初始化?
可以先从业务上理解,比如两个select组件,初始值都是未选中,select_A选中选项后,select_B再选中,select_A不会重置为未选,只有刷新页面组件重载时,数据状态才会初始化为未选。
知道state
状态是怎样保存的之后,其实就很好理解了。重渲染≠重载,组件并没有被卸载,state
值仍然存在在fiber节点中。并且useState
只会在组件首次加载时初始化state的值。
常有小伙伴遇到组件没正常更新的场景就纳闷,父组件重渲染子组件也会重渲染,但为什么子组件的状态值不更新?就是因为rerender只是rerender,不是重载,你不人为更新它的state
,它怎么会重置/更新呢?
ps:面对有些非受控组件不更新状态的情况,我们可以通过改变组件的key值,使之重载来解决。
React 中 setState 什么时候是同步的,什么时候是异步的
1、由 React 控制的事件处理程序,以及生命周期函数调用 setState 不会同步更新 state 。只在合成事件如onClick
等和钩子函数包括componentDidMount
、useEffect
等中是“异步”的。
这里的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形式了所谓的“异步”。
假如在一个合成事件中,循环调用了setState
方法n
次,如果 React 没有优化,当前组件就要被渲染n
次,这对性能来说是很大的浪费。所以,React 为了性能原因,对调用多次setState
方法合并为一个来执行。当执行setState
的时候,state
中的数据并不会马上更新。
2、React 控制之外的事件中调用 setState 是同步更新的。比如原生 js 绑定的事件,setTimeoutsetInterval
或者直接在 DOM
上绑定原生事件和Promise.then
等异步事件中会同步更新。
每次渲染,函数都会重新执行。我们知道,每当函数执行完毕,所有的内存都会被释放掉。因此想让函数式组件拥有内部状态,并不是一件理所当然的事情。useState就是帮助我们做这个事情,useState利用闭包,在函数内部创建一个当前函数组件的状态。并提供一个修改该状态的方法。
useState()
是使用 useReducer 构建的。纯函数不能有状态,所以把状态放在钩子里面。
initialState
参数只会在组件的初始化渲染中起作用,后续渲染时会被忽略;- 如果初始
state
需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的state
,此函数只在初始渲染时被调用。
useState()
是异步函数 ,我们 setState() 后不会立刻对值进行改变,而是会将其暂时放入 pending 队列中。react 会合并多个 state ,然后值 render 一次,因此一些情况下useState获取不到最新的值。
解决办法:
- 通过对setState传递一个回调函数,回调函数会保证其拿到最新的值。
- 通过Ref,但是ref.current 值的改变,是无法通过 useEffect(),useCallback()来监控到的,因为useRef创建的ref对象在整个组件生命周期内保持不变,即使ref的.current属性改变时。因此,useEffect或useCallback无法直接监听ref的改变,因为他们依赖值的更改来重新运行。在useEffect或useCallback的依赖项数组中添加ref.current没有意义,因为ref对象本身没有改变。但是,你可以结合状态钩子(useState)来监听useRef的.current属性的变化
执行useState 后发生了什么
- 首先生成调用函数生成一个更新对象,这个更新对象带有任务的优先级、fiber实例等。
- 再把这个对象放入更新队列中,等待协调。
- react会以优先级高低先后调用方法,创建Fiber树以及生成副作用列表。
- 在这个阶段会先判断主线程是否有时间,有的话先生成
workInProgress tree
并遍历之。 - 之后进入调教阶段,将
workInProgress tree
与current Fiber
对比,并操作更新真实dom。
mountState
首先会得到初始化的state
,将它赋值给hook
对象的 memoizedState
和baseState
属性,然后创建一个queue
对象,里面保存了负责更新的信息。
//useState和useReducer触发函数更新的方法都是dispatchAction,useState const [ number , setNumber ] = useState(0) //dispatchAction 就是 setNumber , dispatchAction 第一个参数和第二个参数,已经被bind给改成currentlyRenderingFiber和 queue,我们传入的参数是第三个参数action
无论是类组件调用setState
,还是函数组件的dispatchAction
,都会产生一个 update
对象,里面记录了此次更新的信息,然后将此update
放入待更新的pending
队列中,dispatchAction
第二步就是判断当前函数组件的fiber
对象是否处于渲染阶段,如果处于渲染阶段,那么不需要我们在更新当前函数组件,只需要更新一下当前update
的expirationTime
即可。
如果当前fiber
没有处于更新阶段。那么通过调用lastRenderedReducer
获取最新的state
,和上一次的currentState
,进行浅比较,如果相等,那么就退出,这就证实了为什么useState
,两次值相等的时候,组件不渲染的原因了,这个机制和Component
模式下的setState
有一定的区别。
如果两次state
不相等,那么调用scheduleUpdateOnFiber
调度渲染当前fiber
,scheduleUpdateOnFiber
是react
渲染更新的主要函数。
updateState
当一个hook里使用三次setState
产生的update
会暂且放入pending queue
,在下一次函数组件执行时候,三次 update
被合并到 baseQueue
。
接下来会把当前useState或是useReduer对应的hooks上的baseState和baseQueue更新到最新的状态。会循环baseQueue的update,复制一份update,更新expirationTime,对于有足够优先级的update(上述三个setNumber产生的update都具有足够的优先级),我们要获取最新的state状态。,会一次执行useState上的每一个action。得到最新的state。
4.4.2 useEffect
如何初次渲染不更新useEffect?
你可以引入一个额外的 state 变量作为标记来跳过第一次的运行。 const [isFirstRender, setIsFirstRender] = useState(true); useEffect(() => { if (isFirstRender) { setIsFirstRender(false); } else { // 在组件更新后执行你的副作用 console.log('This will not run on the first render'); } }, [count]); // 这里依赖 count 变量 也可以自定义一个hooks const useUpdateEffect = (fn: Function, inputs: any[]) => { const didMountRef = useRef(false); useEffect(() => { if (didMountRef.current) fn(); else didMountRef.current = true; }, inputs); };
dep为数组时会发生什么
由于react出于性能考虑用了Object.is来做浅比较,因此检测不到深层结构。
useEffect监听数据变化时,只有在数组元素类型为基本数据类型时可以起到作用。
useEffect 会检测两次监测的对象 内存地址是否相同,相同就跳过,不同才会执行useEffect
//解决方法 const MyComponent = ({ arrayProp }) => { const serialized = JSON.stringify(arrayProp); //通过js的序列化实现 useEffect(() => { }, [serialized]); } 这种方法的问题是,对于大数组,序列化可能会很消耗性能。另外,这种方法也不能处理数组中包含循环引用的情况。 对于更复杂的情况,你可能需要使用一些库,如lodash的_.isEqual函数,结合useRef和useEffect来手动实现深度比较。
useEffect用于处理大多数副作用,useEffect第一个参数接受一个回调函数,默认情况下,useEffect会在第一次渲染和更新之后都会执行,相当于在componentDidMount和componentDidUpdate两个生命周期函数中执行回调。
其中的回调函数会在render执行之后在调用(渲染时异步调用,渲染完成后再执行),确保不会阻止浏览器的渲染,这跟componentDidMount和componentDidUpdate是不一样的,他们会在渲染时同步执行。useEffect的特点:
- 有两个参数 callback 和 dependencies 数组
- 如果 dependencies 不存在,那么 callback 每次 render 都会执行
- 如果 dependencies 存在,只有当它发生了变化, callback 才会执行
- useEffect的第二个参数为一个空数组,初始化调用一次之后不再执行,相当于componentDidMount。
//如果某些特定值在两次重渲染之间没有发生变化,你可以跳过对 effect 的调用,这时候只需要传入第二个参数 //回调函数中可以返回一个清除函数,这是effect可选的清除机制,相当于类组件中componentwillUnmount生命周期函数。清理规则:首次渲染不会进行清理,会在下一次执行前,清除上一次的副作用;卸载阶段也会执行清除操作。
mountEffect
mountEffect里的pushEffect 创建effect对象,挂载updateQueue。首先创建一个 effect
,判断组件如果第一次渲染,那么创建 componentUpdateQueue
,就是workInProgress
的updateQueue
。然后将effect
放入updateQueue
中。
effect list
可以理解为是一个存储 effectTag
副作用列表容器。它是由 fiber
节点和指针 nextEffect
构成的单链表结构,这其中还包括第一个节点 firstEffect
,和最后一个节点 lastEffect
。 React
采用深度优先搜索算法,在 render
阶段遍历 fiber
树时,把每一个有副作用的 fiber
筛选出来,最后构建生成一个只带副作用的 effect list
链表。 在 commit
阶段,React
拿到 effect list
数据后,通过遍历 effect list
,并根据每一个 effect
节点的 effectTag
类型,执行每个effect
,从而对相应的 DOM
树执行更改。
updateEffect
useEffect
做的事很简单,判断两次deps
相等,如果相等说明此次更新不需要执行,则直接调用 pushEffect
,这里注意 effect
的标签,hookEffectTag
,如果不相等,那么更新 effect
,并且赋值给hook.memoizedState
,这里标签是 HookHasEffect | hookEffectTag
,然后在commit
阶段,react
会通过标签来判断,是否执行当前的 effect
函数。
useLayoutEffect
u seEffect 是官方推荐拿来代替 componentDidMount / componentDidUpdate / componentWillUnmount 这 3 个生命周期函数的,但其实他们并不是完全等价,useEffect 是在浏览器渲染结束之后才执行的,而这三个生命周期函数是在浏览器渲染之前同步执行的,React 还有一个官方的 hook 是完全等价于这三个生命周期函数的,叫 useLayoutEffect。
在大多数情况下,我们都可以使用useEffect处理副作用,但是,如果副作用是跟DOM相关的,就需要使用useLayoutEffect。useLayoutEffect中的副作用会在DOM更新之后同步执行。
由于 JS 线程和浏览器渲染线程是互斥的,即使内存中的真实 DOM 已经变化,浏览器也没有立刻渲染到屏幕上,此时会进行收尾工作,同步执行对应的生命周期方法,我们说的componentDidMount,componentDidUpdate 以及 useLayoutEffect(create, deps) 的 create 函数(已经可以拿到最新的 DOM 节点)都是在这个阶段被同步执行。commit阶段的操作执行完,浏览器把发生变化的 DOM 渲染到屏幕上,到此为止 react 仅用一次回流、重绘的代价,就把所有需要更新的 DOM 节点全部更新完成。浏览器渲染完成后,浏览器通知 react 自己处于空闲阶段,react 开始执行自己调度队列中的任务,此时才开始执行 useEffect(create, deps) 的产生的函数。
在副作用函数中处理异步请求:
在副作用中编写 fetch
调用是一个 请求数据的流行方式,尤其是在完全客户端的应用中。 然而,这是一种非常手动的方法,它有很大的缺点:
- 副作用不在服务器上运行。 这意味着初始服务器渲染的 HTML 将仅包含没有数据的加载状态。 客户端计算机必须下载所有 JavaScript 并渲染你的应用,然后才发现它现在需要加载数据。 这不是很有效。
- 直接在副作用中请求可以轻松创建 “网络瀑布”。 你渲染父组件,它获取一些数据,渲染子组件,然后它们开始获取数据。 如果网络不是很快,这比并行获取所有数据要慢得多。
- 直接在副作用中请求通常意味着你没有预加载或缓存数据。 例如,如果组件卸载然后再次挂载,则它必须再次获取数据。
- 这不是很符合人体工程学。 在以一种不会出现像 竞态条件 这样的错误的方式编写
fetch
调用时,涉及到相当多的样板代码。
这个缺点列表并不是 React 特有的。 它适用于使用任何库在挂载上获取数据。 与路由一样,要做好数据获取并非易事,因此我们推荐以下方法:
- 如果你使用 框架,请使用其内置的数据请求机制。 现代 React 框架集成了高效的数据请求机制,不会出现上述问题。
- 否则,请考虑使用或构建客户端缓存。 流行的开源解决方案包括 React 查询、useSWR 和 React 路由 6.4+。 你也可以构建自己的解决方案,在这种情况下,你可以在后台使用副作用,但添加用于删除重复请求、缓存响应和避免网络瀑布的逻辑(通过预加载数据或提升 路由的数据要求)。
全局或可变值可以是依赖吗?
像 location.pathname
这样的可变值不能是依赖。 它是可变的,因此它可以完全在 React 渲染数据流之外随时更改。 更改它不会触发组件的重新渲染。 因此,即使你在依赖中指定它,React 也不会知道在副作用发生变化时重新同步它。 这也违反了 React 的规则,因为在渲染期间读取可变数据(计算依赖时)会破坏 渲染的纯粹。 而是,你应该使用 useSyncExternalStore
读取和订阅外部可变值
像 ref.current
这样的可变值或你从中读取的内容也不能是依赖。useRef
本身返回的引用对象可以是依赖,但它的 current
属性是有意可变的。 它让你 在不触发重新渲染的情况下跟踪某些内容。 但是因为改变它不会触发重新渲染,它不是一个 React 值,并且 React 不会知道在它改变时重新运行你的副作用。
4.4.3 useContext
接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定。
当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。即使祖先使用 React.memo 或 shouldComponentUpdate,也会在组件本身使用 useContext 时重新渲染。
useContext不能实现精准更新,想要减少二次渲染,可以使用Memo,利用第二个参数做比较(位运算),使用第三方库。。
//类组件 const {Provider, Consumer} = React.createContext(defaultValue); //创建一个 Context 对象。当 React 渲染一个订阅了这个 Context 对象的组件,这个组件会从组件树中离自身最近的那个匹配的 Provider 中读取到当前的 context 值。 //只有当组件所处的树中没有匹配到 Provider 时,其 defaultValue 参数才会生效。这有助于在不使用 Provider 包装组件的情况下对组件进行测试。注意:将 undefined 传递给 Provider 的 value 时,消费组件的 defaultValue 不会生效。 <Provider value={/*共享的数据*/}> /*里面可以渲染对应的内容*/ </Provider> <Consumer> {value => /*根据上下文 进行渲染相应内容*/} </Consumer>
//hooks // createContext主要功能是创建一个context,提供Provider和Consumer。Provider主要将context内容暴露出来,Consumer可以拿到对应context的Provider暴露的内容使用。 const Context = createContext() <UserContext.Provider value={'chuanshi'}> <ComponentC /> </UserContext.Provider> //每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化。 //Provider 接收一个 value 属性,传递给消费组件。一个 Provider 可以和多个消费组件有对应关系。多个 //Provider 也可以嵌套使用,里层的会覆盖外层的数据。 //当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染。Provider 及其内部 consumer 组件都不受制于 shouldComponentUpdate 函数,因此当 consumer 组件在其祖先组件退出更新的情况下也能更新。 <UserContext.Consumer> {(user) => ( <div> User context value {user} </div>)} </UserContext.Consumer> //这里,React 组件也可以订阅到 context 变更。这能让你在函数式组件中完成订阅 context。 //这需要函数作为子元素(function as a child)这种做法。这个函数接收当前的 context 值,返回一个 React 节点。传递给函数的 value 值等同于往上组件树离这个 context 最近的 Provider 提供的 value 值。如果没有对应的 Provider,value 参数等同于传递给 createContext() 的 defaultValue。 当需要多处进行消费时,通过 useContext(Context(React.createContext 的返回值)) //当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定。
原理:
useContext
的原理类似于观察者模式。Provider
是被观察者, Consumer
和useContext
是观察者。当Provider
上的值发生变化, 观察者是可以观察到的,从而同步信息给到组件。
在技术层面上,Provider组件在其内部创建了一个特殊的React对象,这个对象存储了Context的当前值。当组件被渲染时,React就会使用这个对象来决定useContext应该返回什么值。当你改变Provider的值时,React就会重新渲染所有使用了这个Context的组件,以保证它们总是获取到最新的值。
具体来说,在更新状态时, 由ContextProvider
节点负责查找所有ContextConsumer
节点, 并设置消费节点的父路径上所有节点的fiber.childLanes
, 保证消费节点可以得到更新。深度优先遍历所有的子代 fiber ,然后找到里面具有 dependencies 的属性,这个属性中挂载了一个元素依赖的所有 context,对比 dependencies 中的 context 和当前 Provider 的 context 是否是同一个;如果是同一个,它会创建一个更新,设定高 fiber 的更新优先级,类似于调用 this.forceUpdate 带来的更新
Consumer 指向 context 本身,其生成 fiber 时会识别 REACT_CONTEXT_TYPE 类型然后添加 ContextConsumer tag ,当我们识别到这个 tag ,就会调用 updateContextConsumer 进行处理。updateContextConsumer 中的逻辑是先通过 prepareToReadContext 和 readContext 获取最新的 context 的值,再把最新的值传入子组件进行更新操作:
一些点:
- 当创建了一个Context对象,在React渲染出了订阅这个对象的组件(这里是组件B),它能获取到组件树中距离最近Provider中value的值。当没有匹配到Provider的时候创建时传递的初始值会生效。如果value的值是undefined初始值不生效。
- Provider 及其内部 consumer 组件都不受制于
shouldComponentUpdate
函数,因此当 consumer 组件在其祖先组件退出更新的情况下也能更新。 - 通过新旧值检测来确定变化,使用了与 Object.is 相同的算法。
配合 useReducer 做状态管理:
将 dispatch 函数作为 context 的 value,共享给页面的子组件,在 useReducer 结合 useCountext,通过 context 把 dispatch 函数提供给组件树中的所有组件使用,而不是通过 props 添加回调函数到方式一层层传递。
import React, { useReducer, useContext, createContext } from 'react'; // 初始状态 const initialState = { isLoggedIn: false, }; // action类型 const LOGIN = 'LOGIN'; const LOGOUT = 'LOGOUT'; // reducer函数 function loginReducer(state, action) { switch (action.type) { case LOGIN: return { ...state, isLoggedIn: true }; case LOGOUT: return { ...state, isLoggedIn: false }; default: return state; } } // 创建一个context const LoginContext = createContext(); // 创建一个Provider组件 function LoginProvider({ children }) { const [state, dispatch] = useReducer(loginReducer, initialState); return ( <LoginContext.Provider value={{ state, dispatch }}> {children} </LoginContext.Provider> ); } // 在其他组件中使用这个Context function LoginComponent() { const { state, dispatch } = useContext(LoginContext); return ( <div> <p>User is {state.isLoggedIn ? 'logged in' : 'logged out'}.</p > <button onClick={() => dispatch({ type: LOGIN })}>Log In</button> <button onClick={() => dispatch({ type: LOGOUT })}>Log Out</button> </div> ); } // 使用LoginProvider在应用中共享状态 function App() { return ( <LoginProvider> <LoginComponent /> </LoginProvider> ); }
4.4.4 其他
1 useRef
Refs
是一个获取 DOM
节点或 React
元素实例的工具。在 React
中 Refs
提供了一种方式,允许用户访问 DOM
节点或 render
方法中创建的 React
元素。
mountRef
初始化很简单, 创建一个ref对象, 对象的current
属性来保存初始化的值,最后用memoizedState
保存ref
,完成整个操作。
函数组件更新useRef做的事情更简单,就是返回了缓存下来的值,也就是无论函数组件怎么执行,执行多少次,hook.memoizedState内存中都指向了一个对象
类组件 //当 ref 属性用于 HTML 元素时,构造函数中使用 React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性; createRef 每次渲染都会返回一个新的引用,而 useRef 每次都会返回相同的引用(persist)。 //当 ref 属性用于自定义 class 组件时,ref 对象接收组件的挂载实例作为其 current 属性; //不能在函数组件上使用 ref 属性,因为他们没有实例。但可以改成 class 组件,React.forwardRef 进行包装
函数组件 //useRef 返回一个可变的 ref 对象,其 current 属性被初始化为传入的参数( initialValue )。返回的 ref 对象在组件的整个生命周期内保持不变。 const inputRef = useRef(null); return ( <input ref={inputRef} type="text" onChange={handleChange} />) 注意:refObj.current //1 被引用对象的值在重新渲染之间保持不变。 //2 更新被引用对象的值不会触发重新渲染。
useRef和ref的区别:
useRef
用于创建引用对象,本质上就是一个js对象(js对象每次渲染会重新执行),而 ref
用于访问 DOM 节点或将 render
方法中的 react 组件分配给引用对象。另外,可以使用 useRef
hook 或 createRef
函数创建 ref
,这是其他方法无法实现的。
useRef
可以用来引用任何类型的对象,React ref
只是一个用于引用 DOM 元素的 DOM 属性。
useRef获取前一次的值
- useRef保持引用不变;
- 函数式组件的声明周期决定,jsx的渲染比useEffect早;
- 手动修改ref.current并不会触发组件的重新渲染;
拿到前一个值这件事,想到了什么?
想到了class react中的生命周期shouldComponentUpdate(nextProps,nextState)
中比较前后两次属性是否相同来做优化,减少渲染次数,和componentWillReceiveProps(nextProps)
比较子组件前后两次属性值的变化来执行某些方法。
**React.forwardRef作用 ** 如果需要访问自己组件的Ref,需要使用forwardRef
- 转发refs到DOM组件
- 在高阶组件中转发refs
解决函数组件不能直接传递ref的问题。这是因为使用ref
会脱离React的控制。比如:DOM聚焦 需要调用input.focus()
,直接执行DOM API
是不受React
控制的。但为了保证应用的健壮,React
也要尽可能防止他们失控。
首先来看不失控的情况:
- 执行
ref.current
的focus
、blur
等方法 - 执行
ref.current.scrollIntoView
使element
滚动到视野内
那失控的情况:
- 执行
ref.current.remove
移除DOM
- 执行
ref.current.appendChild
插入子节点
限制失控:基于dom封装的组件(低阶组件)是可以直接把ref指向dom,「高阶组件」无法直接将ref指向DOM,这一限制就将「ref失控」的范围控制在单个组件内,不会出现跨越组件的「ref失控」
当 ref
对象内容发生变化时,useRef
并不会通知你。变更 .current
属性不会引发组件重新渲染。如果想要在 React
绑定或解绑 DOM
节点的 ref
时运行某些代码,则需要使用回调 ref
来实现.
const inputEl = useRef(null); const onButtonClick = () => { // `current` 指向已挂载到 DOM 上的文本输入元素 inputEl.current.focus(); };
useImperativeHandle 的第一个参数是定义 current 对象的 ref,第二个参数是一个函数,返回值是一个对象,即这个 ref 的 current 对象
在介绍 useImperativeHandle 之前一定要清楚 React 关于 ref 转发(也叫透传)的知识点,是使用 React.forwardRef 方法实现的,该方法返回一个组件,参数为函数(props callback,并不是函数组件),函数的第一个参数为父组件传递的 props,第二给参数为父组件传递的 ref,其目的就是希望可以在封装组件时,外层组件可以通过 ref 直接控制内层组件或元素的行为useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。
通常与forwardRef一起使用,暴露之后父组件就可以通过 selectFileModalRef.current?.handleCancel();
来调用子组件的暴露方法。
2 useReducer
useReducer 是一个用于状态管理的 Hook Api。是useState 的替代方案。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。useReducer 这个 Hooks 在使用上几乎跟 Redux一模一样,唯一缺点的就是无法使用 redux 提供的中间件。
在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数 。
const [state, dispatch] = useReducer(reducer, initialState, init); //它接受 Reducer 函数和状态的初始值作为参数.第3参数它是一个回调函数且一定要返回一个对象数据,当然你也可以直接返回一个值也可以。如果useReducer它有第3个参数,则第2个参数就没有意义,它以第3个参数优先,第3个参数,惰性初始化,提升性能(调用的时候初始化数据) //返回一个数组。数组的第一个成员是状态的当前值,第二个成员是发送 action 的 dispatch 函数。 //计数器实例: const initialState = 0; const reducer = (state, action) => { //为了避免重复渲染,会定义在组件外进行创建 switch (action) { case 'increment': return state + 1 default: return state}} function UseReducerHook (){ const [count, dispatch] = useReducer(reducer, initialState); return (<div> <div>Count - {count}</div> <button onClick={() => dispatch('increment')}>增加</button> </div>)}
由于 Hooks
可以提供共享状态和 Reducer
函数,所以它在这些方面可以取代 Redux
。但是它没法提供中间件( middleware
)这样高级功能。
3 useCallback 为 useMemo 的语法糖
useCallback后的函数 组件会重新分配地址吗?
其实 useCallback 需要配合经过优化的并使用引用相等性去避免非必要渲染的子组件时,它才能发挥它的作用。useCallback
的真正目的是在于缓存每次渲染时内联函数的实例,这样方便配合上子组件的shouldComponentUpdate
或者React.memo
起到减少不必要的渲染的作用,父子俩层一定需要配对使用,缺了一个都可能导致性能不升反“降”,毕竟无意义的浅比较也是要消耗那么一点的性能消耗的。不管是否使用useCallback
,都无法避免重新创建内部函数。
使用持久化的 function 的 Hook,理论上,可以使用 usePersistFn 完全代替 useCallback。在某些场景中,我们需要使用 useCallback 来记住一个函数,但是在第二个参数 deps 变化时,会重新生成函数导致函数地址变化。使用 ref 的能力保证函数地址永远不会变化,子组建不会因为函数所需要的变量而重新渲染
function usePersistFn(fn) { if (typeof fn !== 'function') { console.error('param is not a function') } const fnRef = useRef(fn) fnRef.current = useMemo(() => fn, [fn]) const persistFn = useRef() if (!persistFn.current) { persistFn.current = function (...args) { return fnRef.current.apply(this, args) } } return persistFn.current }
UseCallBack 基础
把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新,避免非必要渲染。
let lastCallback; let lastCallbackDependencies; function useCallback(callback,dependencies){ if (lastCallbackDependencies) { let changed = !dependencies.every((item,index)=>{ return item === lastCallbackDependencies[index]; }); } else { // 第一次(即lastCallbackDependencies为undefined) // 或者非第一次但没有传入依赖项(即dependencies为[]) lastCallback = callback; lastCallbackDependencies = dependencies; } return lastCallback; }
useEventCallback
是在React中通常用于确保事件处理器的行为的一种Hook。它的主要目的是解决事件处理函数中的闭包问题。
当你在组件中使用函数时,如果这个函数引用了组件的state或props,那么它就会“捕获”那个特定的state或props。这就是所谓的"闭包"。因此,如果你在一个事件处理器中使用这样的函数,并且你希望它总是引用最新的state或props,那么你就需要用到useEventCallback
。
useEventCallback
的基本使用形式如下:
import { useEventCallback } from 'react-event-callback-hook'; function MyComponent() { const [count, setCount] = useState(0); const handleClick = useEventCallback(() => { console.log(count); setCount(count + 1); }, [count]); return <button onClick={handleClick}>Click me</button>; }
在上述代码中,handleClick
函数总是引用最新的count
状态,即使点击事件发生在状态改变之前。因此,它总是打印出最新的状态值。
function useEventCallback(fn, dependencies) { const ref = useRef(() => { throw new Error('Cannot call an event handler while rendering.'); }); // 根据依赖去更新 ref ,保证最终调用的函数是最新的 useEffect(() => { ref.current = fn; }, [fn, ...dependencies]); // useCallback 返回的结果不会改变 return useCallback(() => { const fn = ref.current; return fn(); }, [ref]); }
4 useMemo
接受两个参数,分别是:“创建”函数 和 依赖项数 ,如果不传依赖数组,传入的函数每次都会重新执行计算;如果传空的依赖数组,则返回值被初始化后,不会再变化。在组件首次加载和重渲染期间执行。同步方法,不能异步调用。
- 相当于vue里的计算属性
- useMemo 缓存的结果是回调函数中return回来的值,主要用于缓存计算结果的值,应用场景如需要计算的状态。
- useCallback 缓存的结果是函数,优化针对于子组件渲染,主要用于缓存函数,应用场景如需要缓存的函数,因为函数式组件每次任何一个state发生变化,会触发整个组件更新,一些函数是没有必要更新的,此时就应该缓存起来,提高性能,减少对资源的浪费;另外还需要注意的是,useCallback应该和React.memo配套使用,缺了一个都可能导致性能不升反而下降。
原理:
mountMemo:初始化useMemo
,就是创建一个hook
,然后执行useMemo
的第一个参数,得到需要缓存的值,然后将值和deps
记录下来,赋值给当前hook
的memoizedState
updateMemo:在组件更新过程中,我们执行useMemo
函数,做的事情实际很简单,就是判断两次 deps
是否相等,如果不想等,证明依赖项发生改变,那么执行 useMemo
的第一个函数,得到新的值,然后重新赋值给hook.memoizedState
,如果相等 证明没有依赖项改变,那么直接获取缓存的值。
在update阶段中,areHookInputsEqual
函数接受两个依赖项数组 nextDeps
和 prevDeps
。它首先检查两个数组的长度是否相等,如果不相等,将在开发模式下发出警告。然后,它遍历数组并使用 is
函数(类似于 Object.is
)逐个比较元素。如果发现任何不相等的元素,函数将返回 false
。否则,返回 true
。
不过这里有一点,值得注意,执行,如果里面引用了等信息,变量会被引用,无法被垃圾回收机制回
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
前端校招面经分享,包含CSS、JS、Vue、React、计算机网络、难点项目、手撕题等。这份面经总结了几乎大厂所有的面试题与牛客近几年公开的面经,可以说面试不会超出范围。 因为我只负责总结加一些个人见解,所以一开始免费开源。但互联网戾气真的重,很多人拿着面经还一副理所应当的样子质问我要语雀,还说网上同类的有很多??唉,分享不易,那我只好收费了233。当然也欢迎直接来找我要语雀,语雀会多一些内容。