1. Reac生命周期是怎样的?
生命周期
答:组件从 被创建 到 被销毁 的过程称为组件的生命周期。
生命周期
常见的生命周期。
- constructor:该方法只会执行一次,组件的初始化工作,例如:初始化组件的 state。
- render:是类组件中唯一必须实现的方法,它的返回值将作为页面渲染的视图。
- componentWillReceiveProps:当从父类接收到 props 并且在调用另一个渲染器之前调用。
- shouldComponentUpdate:这个方法用来判断是否需要调用 render 方法重绘 dom。返回true更新组件,返回false,阻止render渲染,默认返回true。
- componentWillUpdate:在组件即将更新之前执行。(如果 shouldComponentUpdate 函数返回false,则不会调用 componentWillUpdate 方法。)
- componentDidUpdate:在渲染发生后立即调用。
- componentWillMount:在渲染之前执行。
- componentDidMount:仅在第一次渲染后在客户端执行(发异步数据请求)。
- componentWillUnmount:从 DOM 卸载组件后调用。用于清理内存空间。
新(react17)旧比较(废三增三)
-
废弃了
componentWillMountcomponentWillReceivePropscomponentWillUpdate
-
增加了
getDerivedStateFromError:在渲染阶段捕捉到了后代组件中的错误时会执行。getDerivedStateFromProps:让组件在 props 变化时更新 state执行。getSnapshotbeforeUpdate:此生命周期函数在最近一次渲染提交至 DOM 树之前执行,此时 DOM 树还未改变,我们可以在这里获取 DOM 改变前的信息,例如:更新前 DOM 的滚动位置。(它接收两个参数,分别是:prevProps、prevState,上一个状态的 props 和上一个状态的 state。)
面试大白话
答:首先,生命周期是组件从 被创建 到 被销毁 的过程其常见的生命周期render阶段...commit阶段在react17有些改变其中废弃了3个生命周期:componentWillMount、componentWillReceiveProps、componentWillUpdate。新增了getDerivedStateFromError、getDerivedStateFromProps、getSnapshotbeforeUpdate。
2. 虚拟DOM实现原理?
什么是Virtual DOM?
答:将真实的DOM的数据抽取出来,以对象的形式模拟树形结构。
Virtual DOM的作用?
答:避免昂贵的DOM操作与大量无谓的计算量,通过diff算法,一边比较新旧节点一边给真实的DOM打补丁。
diff的原理?
React 分别对 tree diff、component diff 以及 element diff 进行算法优化。
-
tree diff
- React 通过 深度优先 对 Virtual DOM 树进行层级控制,只会对相同 DOM 节点进行比较,即同一个父节点下的所有子节点。
- 当发现节点已经不存在,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。这样只需要对树进行一次遍历,便能完成整个 DOM 树的比较。
-
component diff
- 如果是同一类型的组件,按照原策略继续比较 virtual DOM tree。
- 如果不是,则将该组件判断为 dirty component,从而替换整个组件下的所有子节点。
- 对于同一类型的组件,有可能其 Virtual DOM 没有任何变化,如果能够确切的知道这点那可以节省大量的 diff 运算时间,因此 React 允许用户通过 shouldComponentUpdate() 来判断该组件是否需要进行 diff。
-
element diff
- 节点处于同一层级时,React diff 提供了三种节点操作,分别为:INSERT_MARKUP(插入)、MOVE_EXISTING(移动)和 REMOVE_NODE(删除)。
diff的比较方式?
答:前端操作dom的时候了,不会把当前元素作为上一级元素或下一级元素,很少会跨越层级地移动DOM元素,在采取diff算法比较新旧节点的时候,比较只会在同层级进行, 不会跨层级比较,因此复杂度的问题转换成 O(n) 复杂度的问题。
diff的弊端?
答:相同⽗元素的⼦元素必须有独特的 key,否则重复的 key 会造成渲染错误。
Vue vs React
-
相同点
- 都是两组虚拟dom的对比(react16.8之后是fiber与虚拟dom的对比)
- 只对同级节点进行对比,简化了算法复杂度
- 都用key做为唯一标识,进行查找,只有key和标签类型相同时才会复用老节点
- 遍历前都会根据老的节点构建一个map,方便根据key快速查找
-
不同点
- react会自顶向下全diff.
- vue会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树。
- react在diff遍历的时候,只对需要修改的节点进行了记录,形成effect list,最后才会根据effect list 进行真实dom的修改,修改时先删除,然后更新与移动,最后插入
- vue 在遍历的时候就用真实dom
insertBefore方法,修改了真实dom,最后做的删除操作 - react 采用单指针从左向右进行遍历
- vue采用双指针,从两头向中间进行遍历
- react的虚拟diff比较简单,vue中做了一些优化处理,相对复杂,但效率更高
React的请求应该放在哪个生命周期中?
- 目前官方推荐的异步请求是在componentDidmount中进行.
- 如果有特殊需求需要提前请求,也可以在特殊情况下在constructor中请求
3. setState到底是异步还是同步?
- 有时表现出异步,有时表现出同步
- setState只在合成事件和钩子函数中是“异步”的,在原生事件和setTimeout 中都是同步的。
- setState 的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形成了所谓的“异步”,当然可以通过第二个参数 setState(partialState, callback) 中的callback拿到更新后的结果。
- setState 的批量更新优化也是建立在“异步”(合成事件、钩子函数)之上的,在原生事件和setTimeout 中不会批量更新,在“异步”中如果对同一个值进行多次setState,setState的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时setState多个不同的值,在更新时会对其进行合并批量更新。
4. React组件通信如何实现?
- 父组件向子组件通讯: 父组件可以向子组件通过传 props 的方式,向子组件进行通讯
- 子组件向父组件通讯: props+回调的方式,父组件向子组件传递props进行通讯,此props为作用域为父组件自身的函数,子组件调用该函数,将子组件想要传递的信息,作为参数,传递到父组件的作用域中
- 兄弟组件通信: 找到这两个兄弟节点共同的父节点,结合上面两种方式由父节点转发信息进行通信
- 跨层级通信: Context设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言,对于跨越多层的全局数据通过Context通信再适合不过
- 发布订阅模式: 发布者发布事件,订阅者监听事件并做出反应,我们可以通过引入event模块进行通信
- 全局状态管理工具: 借助Redux或者Mobx等全局状态管理工具进行通信,这种工具会维护一个全局状态中心Store,并根据不同的事件产生新的状态
5. React如何进行组件/逻辑复用?
-
高阶组件:
- 属性代理
- 反向继承
-
渲染属性
- render props
-
react-hooks
mixin、hoc、render props、react-hooks的优劣如何?(上一个问题展开)
-
Mixin
-
缺点
-
组件与 Mixin 之间存在隐式依赖(Mixin 经常依赖组件的特定方法,但在定义组件时并不知道这种依赖关系)
-
多个 Mixin 之间可能产生冲突(比如定义了相同的state字段)
-
Mixin 倾向于增加更多状态,这降低了应用的可预测性(The more state in your application, the harder it is to reason about it.),导致复杂度剧增
-
隐式依赖导致依赖关系不透明,维护成本和理解成本迅速攀升:
- 难以快速理解组件行为,需要全盘了解所有依赖 Mixin 的扩展行为,及其之间的相互影响
- 组价自身的方法和state字段不敢轻易删改,因为难以确定有没有 Mixin 依赖它
- Mixin 也难以维护,因为 Mixin 逻辑最后会被打平合并到一起,很难搞清楚一个 Mixin 的输入输出
-
-
-
HOC
-
优点
- HOC通过外层组件通过 Props 影响内层组件的状态,而不是直接改变其 State不存在冲突和互相干扰,这就降低了耦合度
- 不同于 Mixin 的打平+合并,HOC 具有天然的层级结构(组件树结构),这又降低了复杂度
-
缺点
- 扩展性限制: HOC 无法从外部访问子组件的 State因此无法通过shouldComponentUpdate滤掉不必要的更新,React 在支持 ES6 Class 之后提供了React.PureComponent来解决这个问题
- Ref 传递问题: Ref 被隔断,后来的React.forwardRef 来解决这个问题
- Wrapper Hell: HOC可能出现多层包裹组件的情况,多层抽象同样增加了复杂度和理解成本
- 命名冲突: 如果高阶组件多次嵌套,没有使用命名空间的话会产生冲突,然后覆盖老属性
- 不可见性: HOC相当于在原有组件外层再包装一个组件,你压根不知道外层的包装是啥,对于你是黑盒
-
-
Render Props
-
优点
- 上述HOC的缺点Render Props都可以解决
-
缺点
- 使用繁琐: HOC使用只需要借助装饰器语法通常一行代码就可以进行复用,Render Props无法做到如此简单
- 嵌套过深: Render Props虽然摆脱了组件多层嵌套的问题,但是转化为了函数回调的嵌套
-
简单的说,render props 将如何render组件的事代理给了使用它的组件,但同时以参数的形式提供了需要重用的状态和方法给外部。实现UI的自定义和功能的重用。
-
-
React Hooks
-
优点
-
简洁: React Hooks解决了HOC和Render Props的嵌套问题,更加简洁
-
解耦: React Hooks可以更方便地把 UI 和状态分离,做到更彻底的解耦
-
组合: Hooks 中可以引用另外的 Hooks形成新的Hooks,组合变化万千
-
函数友好: React Hooks为函数组件而生,从而解决了类组件的几大问题:
- this 指向容易错误
- 分割在不同声明周期中的逻辑使得代码难以理解和维护
- 代码复用成本高(高阶组件容易使代码量剧增)
-
-
缺点
- 额外的学习成本(Functional Component 与 Class Component 之间的困惑)
- 写法上有限制(不能出现在条件、循环中),并且写法限制增加了重构成本
- 破坏了PureComponent、React.memo浅比较的性能优化效果(为了取最新的props和state,每次render()都要重新创建事件处函数)
- 在闭包场景可能会引用到旧的state、props值
- 内部实现上不直观(依赖一份可变的全局状态,不再那么“纯”)
- React.memo并不能完全替代shouldComponentUpdate(因为拿不到 state change,只针对 props change)
-
6. 你是如何理解fiber的?
-
React 16之前 ,reconcilation 算法实际上是递归,想要中断递归是很困难的,React 16 开始使用了循环来代替之前的递归.
-
Fiber:一种将 recocilation (递归 diff),拆分成无数个小任务的算法;它随时能够停止,恢复。停止恢复的时机取决于当前的一帧(16ms)内,还有没有足够的时间允许计算。
-
Fiber 节点拥有 parent, child, sibling 三个属性,分别对应父节点, 第一个孩子, 它右边的兄弟, 有了它们就足够将一棵树变成一个链表, 实现深度优化遍历。
-
其他答案
-
概念
- React Fiber 是一种基于浏览器的单线程调度算法.
-
问题
- React 在 V16 之前会面临的主要性能问题是:当组件树很庞大时,更新状态可能造成页面卡顿,根本原因在于——更新流程是同步、不可中断的
- React 16之前 ,reconcilation 算法实际上是递归,想要中断递归是很困难的,React 16 开始使用了循环来代替之前的递归.
-
Fiber 架构怎么做的?
- 让 React 渲染的过程可以被中断,可以将控制权交回浏览器,让浏览器及时地相应用户的交互——异步可中断
- 通过将工作任务拆分成一个个工作单元分别来执行——Fiber
-
Fiber 即是一种数据结构,又是一个工作单位
- React Fiber 机制的实现,就是依赖于下面的这种数据结构-链表实现的。其中每个节点都是一个 Fiber,一个 Fiber 包含了 child(第一个子节点)、sibling(兄弟节点)、parent(父节点)等属性。Fiber 节点中其实还会保存节点的类型、节点的信息(比如 state、props)、节点对应的值等
- 将它视作一个执行单元,每次执行完一个“执行单元”,React 就会检查现在还剩多少时间,如果没有时间就将控制权让出来
-
破解 JavaScript 中同步操作时间过长的方法其实很简单——分片。
-
7. 异步渲染有那两个阶段?
-
reconciliation
- componentWillMount
- componentWillReceiveProps
- shouldComponentUpdate
- componentWillUpdate
-
commit
- componentDidMount
- componentDidUpdate
- componentWillUnmount
-
前者过程是可以打断的,后者不能暂停,会一直更新界面直到完成。
-
因为 Reconciliation 阶段是可以被打断的,所以 Reconciliation 阶段会执行的生命周期函数就可能会出现调用多次的情况,从而引起 Bug。由此对于 Reconciliation 阶段调用的几个函数,除了 shouldComponentUpdate 以外,其他都应该避免去使用,并且 V16 中也引入了新的 API 来解决这个问题。
-
getDerivedStateFromProps 用于替换 componentWillReceiveProps ,该函数会在初始化和 update 时被调用
-
getSnapshotBeforeUpdate 用于替换 componentWillUpdate ,该函数会在 update 后 DOM 更新前被调用,用于读取最新的 DOM 数据。
8. 你对 Time Slice的理解?
-
React 在渲染(render)的时候,不会阻塞现在的线程。如果你的设备足够快,你会感觉渲染是同步的。如果你设备非常慢,你会感觉还算是灵敏的。
-
虽然是异步渲染,但是你将会看到完整的渲染,而不是一个组件一行行的渲染出来。
-
同样书写组件的方式,时间分片正是基于可随时打断、重启的Fiber架构,可打断当前任务,优先处理紧急且重要的任务,保证页面的流畅运行。
-
其他答案(参考)
-
把一个耗时长的任务分成很多小任务,每一个小任务完成了,就把控制权交还给 React 负责任务协调的模块,看看有没有其他其他紧急任务要做,如果没有就继续去更新,如果有紧急任务,那就去做紧急任务
-
如果一个任务还没完成(时间到了),就会被另一个更高优先级的更新过程打算,这个时候,优先级高的更新任务会优先处理,而低优先级更新任务所作的工作则会完全作废,然后等待机会重头再来。
-
React Fiber 更新过程被分为两个阶段(Phase):第一个阶段 Reconciliation Phase 和第二阶段 Commit Phase
- 第一阶段,Fiber 会找到需要更新哪些 DOM,这个阶段可以被打算;但到了第二阶段,就会一鼓作气把 DOM 更新完,绝不会被打断
-
9. redux的工作流程?
-
几个核心概念:
- Store:保存数据的地方,你可以把它看成一个容器,整个应用只能有一个Store。
- State:Store对象包含所有数据,如果想得到某个时点的数据,就要对Store生成快照,这种时点的数据集合,就叫做State。
- Action:State的变化,会导致View的变化。但是,用户接触不到State,只能接触到View。所以,State的变化必须是View导致的。Action就是View发出的通知,表示State应该要发生变化了。
- Action Creator:View要发送多少种消息,就会有多少种Action。如果都手写,会很麻烦,所以我们定义一个函数来生成Action,这个函数就叫Action Creator。
- Reducer:Store收到Action以后,必须给出一个新的State,这样View才会发生变化。这种State的计算过程就叫做Reducer。Reducer是一个函数,它接受Action和当前State作为参数,返回一个新的State。
- dispatch:是View发出Action的唯一方法。
-
工作流程
- 首先,用户(通过View)发出Action,发出方式就用到了dispatch方法。
- 然后,Store自动调用Reducer,并且传入两个参数:当前State和收到的Action,Reducer会返回新的State
- State一旦有变化,Store就会调用监听函数,来更新View。
10. react-redux是如何工作的?
-
Provider: Provider的作用是从最外部封装了整个应用,并向connect模块传递store
-
connect: 负责连接React和Redux
- 获取state: connect通过context获取Provider中的store,通过store.getState()获取整个store tree 上所有state
- 包装原组件: 将state和action通过props的方式传入到原组件内部wrapWithConnect返回一个ReactComponent对象Connect,Connect重新render外部传入的原组 WrappedComponent,并把connect中传入的mapStateToProps, mapDispatchToProps与组件上原有的props合并后,通过属性的方式传 WrappedComponent
- 监听store tree变化: connect缓存了store tree中state的状态,通过当前state状态和变更前state状态进行比较,从而确定是否调用this.setState()方法触发Connect及其子组件的重新渲染
11. redux中如何进行异步操作?
- 当然,我们可以在componentDidmount中直接进行请求无须借助redux.
- 但是在一定规模的项目中,上述方法很难进行异步流的管理,通常情况下我们会借助redux的异步中间件进行异步处理.
- redux异步流中间件其实有很多,但是当下主流的异步中间件只有两种redux-thunk、redux-saga,当然redux-observable可能也有资格占据一席之地,其余的异步中间件不管是社区活跃度还是npm下载量都比较差了.
12. redux异步中间件redux-thunk的优劣?
-
redux-thunk优点:
- 体积小: redux-thunk的实现方式很简单,只有不到20行代码
- 使用简单: redux-thunk没有引入像redux-saga或者redux-observable额外的范式,上手简单
-
redux-thunk缺陷:
- 样板代码过多: 与redux本身一样,通常一个请求需要大量的代码,而且很多都是重复性质的
- 耦合严重: 异步操作与redux的action偶合在一起,不方便管理
- 功能孱弱: 有一些实际开发中常用的功能需要自己进行封装
13. 合成事件是什么?
-
React基于浏览器的事件机制自身实现了一套事件机制。包括事件注册、事件的合成、事件冒泡、事件派发等
-
React 上注册的事件最终会绑定在document这个 DOM 上,而不是 React 组件对应的 DOM(减少内存开销就是因为所有的事件都绑定在 document 上,其他节点没有绑定事件)
-
如果因为某些原因,当你需要使用浏览器的底层事件时,只需要使用 nativeEvent 属性来获取即可。
-
React 自身实现了一套事件冒泡机制,所以这也就是为什么我们 event.stopPropagation()无效的原因。
-
React 通过队列的形式,从触发的组件向父组件回溯,然后调用他们 JSX 中定义的 callback
-
React 有一套自己的合成事件 SyntheticEvent
-
其他答案
-
React 根据 W3C 规范定义了每个事件处理函数的参数,即合成事件。
-
所有事件都挂在到document上
-
event不是原生的,是合成事件对象
- 如果因为某些原因,当你需要使用浏览器的底层事件时,只需要使用 nativeEvent 属性来获取即可。
-
react为何要合成事件机制?
- 更好的兼容性和跨平台
- 挂在到document,减少内存消耗,避免频繁解绑 document,减少内存消耗,避免频繁解绑
- 方便事件统一管理(如事务机制)
-
14. React Hooks 原理?
- 修改核心是将useState,useEffect按照调用的顺序放入memoizedState中,每次更新时,按照顺序进行取值和判断逻辑,我们根据调用hook顺序,将hook依次存入数组memoizedState中,每次存入时都是将当前的currentcursor作为数组的下标,将其传入的值作为数组的值,然后在累加currentcursor,所以hook的状态值都被存入数组中memoizedState。先将旧数组memoizedState中对应的值取出来重新复值,从而生成新数组memoizedState。对于是否执行useEffect通过判断其第二个参数是否发生变化而决定的。
- 这里我们就知道了为啥不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层调用他们。因为我们是根据调用hook的顺序依次将值存入数组中,如果在判断逻辑循环嵌套中,就有可能导致更新时不能获取到对应的值,从而导致取值混乱。同时useEffect第二个参数是数组,也是因为它就是以数组的形式存入的。
15. 为什么ReactHooks中不能有条件判断
- 初次渲染的时候,按照 useState,useEffect 的顺序,把 state,deps 等按顺序塞到 memoizedState 数组中。
- 更新的时候,按照顺序,从 memoizedState 中把上次记录的值拿出来。
16. 用过哪些 Hook
-
useState: 用于定义组件的 State,对标到类组件中this.state的功能
-
useEffect:通过依赖触发的钩子函数,常用于模拟类组件中的componentDidMount,componentDidUpdate,componentWillUnmount方法
-
其它内置钩子:useContext: 获取 context 对象
-
useReducer: 类似于 Redux 思想的实现,但其并不足以替代 Redux,可以理解成一个组件内部的 redux,并不是持久化存储,会随着组件被销毁而销毁;属于组件内部,各个组件是相互隔离的,单纯用它并无法共享数据;配合useContext的全局性,可以完成一个轻量级的 Redux
-
useCallback: 缓存回调函数,避免传入的回调每次都是新的函数实例而导致依赖组件重新渲染,具有性能优化的效果;
-
useMemo: 用于缓存传入的 props,避免依赖的组件每次都重新渲染;
-
useMemo 与 useCallback 的区别
- useCallback 是缓存了函数自身,而 useMemo 是缓存了函数的返回值。
-
-
useRef: 获取组件的真实节点;
-
useLayoutEffect:DOM更新同步钩子。用法与useEffect类似,只是区别于执行时间点的不同。useEffect属于异步执行,并不会等待 DOM 真正渲染后执行,而useLayoutEffect则会真正渲染后才触发;可以获取更新后的 state;
-
自定义钩子(useXxxxx): 基于 Hooks 可以引用其它 Hooks 这个特性,我们可以编写自定义钩子。
17. Class 组件 VS Hook
-
类组件与函数组件有什么异同?
-
相同
- 组件是 React 可复用的最小代码片段,它们会返回要在页面中渲染的 React 元素。也正因为组件是 React 的最小编码单位,所以无论是函数组件还是类组件,在使用方式和最终呈现效果上都是完全一致的。
-
不同点:
- 它们在开发时的心智模型上却存在巨大的差异。类组件是基于面向对象编程的,它主打的是继承、生命周期等核心概念;而函数组件内核是函数式编程,主打的是 immutable、没有副作用、引用透明等特点。
- 之前,在使用场景上,如果存在需要使用生命周期的组件,那么主推类组件;设计模式上,如果需要使用继承,那么主推类组件。但现在由于 React Hooks 的推出,生命周期概念的淡出,函数组件可以完全取代类组件。其次继承并不是组件最佳的设计模式,官方更推崇“组合优于继承”的设计概念,所以类组件在这方面的优势也在淡出。
- 性能优化上,类组件主要依靠 shouldComponentUpdate 阻断渲染来提升性能,而函数组件依靠 React.memo 缓存渲染结果来提升性能。
- 从上手程度而言,类组件更容易上手,从未来趋势上看,由于React Hooks 的推出,函数组件成了社区未来主推的方案。
- 类组件在未来时间切片与并发模式中,由于生命周期带来的复杂度,并不易于优化。而函数组件本身轻量简单,且在 Hooks 的基础上提供了比原先更细粒度的逻辑组织与复用,更能适应 React 的未来发展。
-
18. 自定义过哪些 Hook
- 防抖
- 节流
19. React18有哪些更新?
新特性
scss
复制代码
// 1. setState 自动批处理
React 18 通过在默认情况下执行批处理来实现了开箱即用的性能改进。
(批处理: 在视图层,将多个渲染合并成一次渲染)
在 18 之前,只有在react事件处理函数中,才会自动执行批处理,其它情况会多次更新
在 18 之后,任何情况都会自动执行批处理,多次更新始终合并为一次
arduino
复制代码
// 2. flushSync 退出批量更新
新的API
useId - 同一个组件在客户端和服务端生成相同的唯一的 ID。
useInsertionEffect - 提前注入新的style。
并发模式
-
开启并发模式,
- 1.默认使用批处理。
- 2.使用并发特性API。 startTransition:将特定更新标记为
“过渡”来显著改善用户交互(如搜索引擎的关键词联想)。
20. 说说对React refs 的理解?
Refs 在计算机中称为弹性文件系统(英语:Resilient File System,简称ReFS)
React 中的 Refs提供了一种方式,允许我们访问 DOM节点或在 render方法中创建的 React元素
本质为ReactDOM.render()返回的组件实例,如果是渲染组件则返回的是组件实例,如果渲染dom则返回的是具体的dom节点