Q1. React最新的⽣命周期是怎样的?
React 16之后有三个⽣命周期被废弃(但并未删除)
- componentWillMount componentWillReceiveProps
- componentWillUpdate
官⽅计划在17版本完全删除这三个函数,只保留UNSAVE_前缀的三个函数,⽬的是为了向下兼容,但是对于开发者⽽⾔应该尽量避免使⽤他们,⽽是使⽤新增的⽣命周期函数替代它们。
⽬前React 16.8 +的⽣命周期分为三个阶段,分别是挂载阶段、更新阶段、卸载阶段挂载阶段:
- constructor: 构造函数,最先被执⾏,我们通常在构造函数⾥初始化state对象或者给⾃定义⽅法绑定this
- getDerivedStateFromProps: static getDerivedStateFromProps(nextProps, prevState) ,这是个静态⽅法,当我们接收到新的属性想去修改我们state,可以使⽤getDerivedStateFromProps
- render: render函数是纯函数,只返回需要渲染的东⻄,不应该包含其它的业务逻辑,可以返回原⽣的DOM、React组件、Fragment、Portals、字符串和数字、Boolean和null等内容
- componentDidMount: 组件装载之后调⽤,此时我们可以获取到DOM节点并操作,⽐如对canvas,svg的操作,服务器请求,订阅都可以写在这个⾥⾯,但是记得在componentWillUnmount中取消订阅
更新阶段:
- getDerivedStateFromProps: 此⽅法在更新个挂载阶段都可能会调⽤
- shouldComponentUpdate: shouldComponentUpdate(nextProps, nextState) ,有两个参数nextProps和nextState,表示新的属性和变化之后的state,返回⼀个布尔值,true表示会触发重新渲染,false表示不会触发重新渲染,默认返回true,我们通常利⽤此⽣命周期来优化React程序性能render: 更新阶段也会触发此⽣命周期
- getSnapshotBeforeUpdate: getSnapshotBeforeUpdate(prevProps, prevState) , 这 个 ⽅ 法 在 render之后,componentDidUpdate之前调⽤,有两个参数prevProps和prevState,表示之前的属性和之前的state,这个函数有⼀个返回值,会作为第三个参数传给componentDidUpdate,如果你不想要返回值,可以返回null,此⽣命周期必须与componentDidUpdate搭配使⽤
- componentDidUpdate: componentDidUpdate(prevProps, prevState, snapshot),该⽅法在getSnapshotBeforeUpdate
- ⽅法之后被调⽤,有三个参数prevProps,prevState,snapshot,表示之前的props,之前的state,和snapshot。第三个参数是getSnapshotBeforeUpdate返回的,如果触发某些回调函数时需要⽤到 DOM 元素的状态,则将对⽐或计算的过程迁移⾄ getSnapshotBeforeUpdate,然后在 componentDidUpdate 中统⼀触发回调或更新状态。
卸载阶段:
- componentWillUnmount: 当我们的组件被卸载或者销毁了就会调⽤,我们可以在这个函数⾥去清除⼀些定时器,取消⽹络请求,清理⽆效的DOM元素等垃圾清理⼯作
Q2. React的请求应该放在哪个⽣命周期中?
React的异步请求到底应该放在哪个⽣命周期⾥,有⼈认为在 componentWillMount 中可以提前进⾏异步请求,避免⽩屏,其实这个观点是有问题的。
由于JavaScript中异步事件的性质,当您启动API调⽤时,浏览器会在此期间返回执⾏其他⼯作。当React渲染⼀个组件时,它不会等待componentWillMount它完成任何事情 - React继续前进并继续render,没有办法“暂停”渲染以等待数据到达。
⽽且在componentWillMount 请求会有⼀系列潜在的问题,⾸先,在服务器渲染时,如果在 componentWillMount ⾥获取数据,fetch data会执⾏两次,⼀次在服务端⼀次在客户端,这造成了多余的请求,其次,在React 16进⾏React Fiber重写后, componentWillMount 可能在⼀次渲染中多次调⽤。
⽬前官⽅推荐的异步请求是在 componentDidmount 中进⾏。如果有特殊需求需要提前请求,也可以在特殊情况下在 constructor 中请求:
react 17之后 componentWillMount 会被废弃,仅仅保留 UNSAFE_componentWillMount
Q3. setState到底是异步还是同步?
先给出答案: 有时表现出异步,有时表现出同步
- setState只在合成事件和钩⼦函数中是“异步”的,在原⽣事件和setTimeout中都是同步的。
- setState的“异步”并不是说内部由异步代码实现,其实本身执⾏的过程和代码都是同步的,只是合成事件和钩⼦
- 函数的调⽤顺序在更新之前,导致在合成事件和钩⼦函数中没法⽴⻢拿到更新后的值,形成了所谓的“异步”,当然可以通过第⼆个参数setState(partialState, callback)中的callback拿到更新后的结果。
- setState的批量更新优化也是建⽴在“异步”(合成事件、钩⼦函数)之上的,在原⽣事件和setTimeout 中不会批量更新,在“异步”中如果对同⼀个值进⾏多次 setState ,setState的批量更新策略会对其进⾏覆盖,取最后⼀次的执⾏,如果是同时setState多个不同的值,在更新时会对其进⾏合并批量更新。
Q4. React组件通信如何实现?
React组件间通信⽅式:
- ⽗组件向⼦组件通讯: ⽗组件可以向⼦组件通过传 props 的⽅式,向⼦组件进⾏通讯
- ⼦组件向⽗组件通讯: props+回调的⽅式,⽗组件向⼦组件传递props进⾏通讯,此props为作⽤域为⽗组件⾃身的函数,⼦组件调⽤该函数,将⼦组件想要传递的信息,作为参数,传递到⽗组件的作⽤域中
- 兄弟组件通信: 找到这两个兄弟节点共同的⽗节点,结合上⾯两种⽅式由⽗节点转发信息进⾏通信
- 跨层级通信: Context 设计⽬的是为了共享那些对于⼀个组件树⽽⾔是“全局”的数据,例如当前认证的⽤户、主题或⾸选语⾔,对于跨越多层的全局数据通过 Context 通信再适合不过
- 发布订阅模式: 发布者发布事件,订阅者监听事件并做出反应,我们可以通过引⼊event模块进⾏通信
- 全局状态管理⼯具: 借助Redux或者Mobx等全局状态管理⼯具进⾏通信,这种⼯具会维护⼀个全局状态中⼼Store,并根据不同的事件产⽣新的状态
Q5. React有哪些优化性能是⼿段?
性能优化的⼿段很多时候是通⽤的详情⻅前端性能优化加载篇
Q6. React如何进⾏组件/逻辑复⽤?
抛开已经被官⽅弃⽤的Mixin,组件抽象的技术⽬前有三种⽐较主流:
- ⾼阶组件:
- 属性代理
- 反向继承
- 渲染属性
- react-hooks
Q7. 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 的输⼊输出
Q8. HOC相⽐Mixin的优势
- HOC通过外层组件通过 Props 影响内层组件的状态,⽽不是直接改变其 State不存在冲突和互相⼲扰,这就降低了耦合度
- 不同于 Mixin 的打平+合并,HOC 具有天然的层级结构(组件树结构),这⼜降低了复杂度
Q9. HOC的缺陷
- 扩展性限制: HOC ⽆法从外部访问⼦组件的 State因此⽆法通过shouldComponentUpdate滤掉不必要的更新,React 在 ⽀ 持 ES6 Class之后提供了React.PureComponent 来解决这个问题
- Ref 传递问题: Ref 被隔断,后来的React.forwardRef 来解决这个问题
- Wrapper Hell: HOC可能出现多层包裹组件的情况,多层抽象同样增加了复杂度和理解成本
- 命名冲突: 如果⾼阶组件多次嵌套,没有使⽤命名空间的话会产⽣冲突,然后覆盖⽼属性
- 不可⻅性: HOC相当于在原有组件外层再包装⼀个组件,你压根不知道外层的包装是啥,对于你是⿊盒
Q10. Render Props优点:
- 上述HOC的缺点Render Props都可以解决
Q11. Render Props缺陷:
- 使⽤繁琐: HOC使⽤只需要借助装饰器语法通常⼀⾏代码就可以进⾏复⽤,Render Props⽆法做到如此简单
- 嵌套过深: Render Props虽然摆脱了组件多层嵌套的问题,但是转化为了函数回调的嵌套
Q12. React Hooks优点:
- 简洁: React Hooks解决了HOC和Render Props的嵌套问题,更加简洁
- 解耦: React Hooks可以更⽅便地把 UI 和状态分离,做到更彻底的解耦组合
- Hooks 中可以引⽤另外的 Hooks形成新的Hooks,组合变化万千
- 函数友好:React Hooks为函数组件⽽⽣,从⽽解决了类组件的⼏⼤问题:
- this 指向容易错误
- 分割在不同声明周期中的逻辑使得代码难以理解和维护
- 代码复⽤成本⾼(⾼阶组件容易使代码量剧增)
Q13. React Hooks缺陷:
- 额外的学习成本(Functional Component 与 Class Component 之间的困惑)
- 写法上有限制(不能出现在条件、循环中),并且写法限制增加了重构成本
- 破坏了PureComponent、React.memo浅⽐较的性能优化效果(为了取最新的props和state,每次render()都要重新创建事件处函数)
- 在闭包场景可能会引⽤到旧的state、props值
- 内部实现上不直观(依赖⼀份可变的全局状态,不再那么“纯”)
- React.memo并不能完全替代shouldComponentUpdate(因为拿不到 state change,只针对 props change)
Q14. 你是如何理解fiber的?
React Fiber 是⼀种基于浏览器的单线程调度算法.
React 16之前,reconcilation 算法实际上是递归,想要中断递归是很困难的,React 16 开始使⽤了循环来代替之前的递归.
Fiber:⼀种将recocilation(递归 diff),拆分成⽆数个⼩任务的算法;它随时能够停⽌,恢复。停⽌恢复的时机取决于当前的⼀帧(16ms)内,还有没有⾜够的时间允许计算。
Q15. 你对 Time Slice的理解?
时间分⽚
- React 在渲染(render)的时候,不会阻塞现在的线程如果你的设备⾜够快,你会感觉渲染是同步的
- 如果你设备⾮常慢,你会感觉还算是灵敏的
- 虽然是异步渲染,但是你将会看到完整的渲染,⽽不是⼀个组件⼀⾏⾏的渲染出来
- 同样书写组件的⽅式
也就是说,这是React背后在做的事情,对于我们开发者来说,是透明的,具体是什么样的效果呢?
有图表三个图表,有⼀个输⼊框,以及上⾯的三种模式。
这个组件⾮常的巨⼤,⽽且在输⼊框每次输⼊东⻄的时候,就会进去⼀直在渲染。为了更好的看到渲染的性能,Dan为我们做了⼀个表。
我们先看看,同步模式:
同步模式下,我们都知道,我们没输⼊⼀个字符,React就开始渲染,当React渲染⼀颗巨⼤的树的时候,是⾮常卡的, 所以才会有shouldUpdate的出现,在这⾥Dan也展示了,这种卡!
我们再来看看第⼆种(Debounced模式):
Debounced模式简单的来说,就是延迟渲染,⽐如,当你输⼊完成以后,再开始渲染所有的变化。这么做的坏处就是,⾄少不会阻塞⽤户的输⼊了,但是依然有⾮常严重的卡顿。
切换到异步模式:
异步渲染模式就是不阻塞当前线程,继续跑。在视频⾥可以看到所有的输⼊,表上都会是原谅⾊的。
时间分⽚正是基于可随时打断、重启的Fiber架构,可打断当前任务,优先处理紧急且重要的任务,保证⻚⾯的流畅运⾏.
Q16. 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的唯⼀⽅法。
然后我们过下整个⼯作流程:
1.⾸先,⽤户(通过View)发出Action,发出⽅式就⽤到了dispatch⽅法。
2.然后,Store⾃动调⽤Reducer,并且传⼊两个参数:当前State和收到的Action,Reducer会返回新的State
3.State⼀旦有变化,Store就会调⽤监听函数,来更新View。
到这⼉为⽌,⼀次⽤户交互流程结束。可以看到,在整个流程中数据都是单向流动的,这种⽅式保证了流程的清晰。
Q17. 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及其⼦组件的重新渲染
Q18. redux与mobx的区别?
两者对⽐:
- redux将数据保存在单⼀的store中,mobx将数据保存在分散的多个store中
- redux使⽤plain object保存数据,需要⼿动处理变化后的操作;mobx适⽤observable保存数据,数据变化后⾃动处理响应的操作
- redux使⽤不可变状态,这意味着状态是只读的,不能直接去修改它,⽽是应该返回⼀个新的状态,同时使⽤纯函数;mobx中的状态是可变的,可以直接对其进⾏修改
- mobx相对来说⽐较简单,在其中有很多的抽象,mobx更多的使⽤⾯向对象的编程思维;redux会⽐较复杂,因为其中的函数式编程思想掌握起来不是那么容易,同时需要借助⼀系列的中间件来处理异步和副作⽤
- mobx中有更多的抽象和封装,调试会⽐较困难,同时结果也难以预测;⽽redux提供能够进⾏时间回溯的开发⼯具,同时其纯函数以及更少的抽象,让调试变得更加的容易
场景辨析:
基于以上区别,我们可以简单得分析⼀下两者的不同使⽤场景。
- mobx更适合数据不复杂的应⽤: mobx难以调试,很多状态⽆法回溯,⾯对复杂度⾼的应⽤时,往往⼒不从⼼;
- redux适合有回溯需求的应⽤: ⽐如⼀个画板应⽤、⼀个表格应⽤,很多时候需要撤销、重做等操作,由于redux不可变的特性,天然⽀持这些操作;
- mobx适合短平快的项⽬: mobx上⼿简单,样板代码少,可以很⼤程度上提⾼开发效率。
当然mobx和redux也并不⼀定是⾮此即彼的关系,你也可以在项⽬中⽤redux作为全局状态管理,⽤mobx作为组件局部状态管理器来⽤.
Q19. redux中如何进⾏异步操作?
当然,我们可以在 componentDidmount 中直接进⾏请求⽆须借助redux。
但是在⼀定规模的项⽬中,上述⽅法很难进⾏异步流的管理,通常情况下我们会借助redux的异步中间件进⾏异步处理。redux异步流中间件其实有很多,但是当下主流的异步中间件只有两种:redux-thunk、redux-saga,当然redux-observable可能也有资格占据⼀席之地,其余的异步中间件不管是社区活跃度还是npm下载量都⽐较差了。
Q20. redux异步中间件之间的优劣?
redux-thunk优点:
- 体积⼩: redux-thunk的实现⽅式很简单,只有不到20⾏代码
- 使⽤简单: redux-thunk没有引⼊像redux-saga或者redux-observable额外的范式,上⼿简单
redux-thunk缺陷:
- 样板代码过多: 与redux本身⼀样,通常⼀个请求需要⼤量的代码,⽽且很多都是重复性质的
- 耦合严重: 异步操作与redux的action偶合在⼀起,不⽅便管理
- 功能孱弱: 有⼀些实际开发中常⽤的功能需要⾃⼰进⾏封装
redux-saga优点:
- 异步解耦:异步操作被被转移到单独 saga.js 中,不再是掺杂在action.js或component.js中
- action摆脱thunk function: dispatch 的参数依然是⼀个纯粹的 action (FSA),⽽不是充满 “⿊魔法”
- thunk function 异常处理: 受益于 generator function 的 saga 实现,代码异常/请求失败都可以直接通过 try/catch 语法直接捕获
- 处理功能强⼤: redux-saga提供了⼤量的Saga 辅助函数和Effect 创建器供开发者使⽤,开发者⽆须封装或者简单封装即可使⽤
- 灵活: redux-saga可以将多个Saga可以串⾏/并⾏组合起来,形成⼀个⾮常实⽤的异步flow
- 易测试,提供了各种case的测试⽅案,包括mock task,分⽀覆盖等等
redux-saga缺陷:
- 额外的学习成本: redux-saga不仅在使⽤难以理解的 generator function,⽽且有数⼗个API,学习成本远超redux- thunk,最重要的是你的额外学习成本是只服务于这个库的,与redux-observable不同,redux-observable虽然也有额外学习成本但是背后是rxjs和⼀整套思想
- 体积庞⼤: 体积略⼤,代码近2000⾏,min版25KB左右
- 功能过剩: 实际上并发控制等功能很难⽤到,但是我们依然需要引⼊这些代码
- ts⽀持不友好: yield⽆法返回TS类型
redux-observable优点:
- 功能最强: 由于背靠rxjs这个强⼤的响应式编程的库,借助rxjs的操作符,你可以⼏乎做任何你能想到的异步处理
- 背靠rxjs: 由于有rxjs的加持,如果你已经学习了rxjs,redux-observable的学习成本并不⾼,⽽且随着rxjs的升级redux- observable也会变得更强⼤
redux-observable缺陷:
- 学习成本奇⾼: 如果你不会rxjs,则需要额外学习两个复杂的库
- 社区⼀般: redux-observable的下载量只有redux-saga的1/5,社区也不够活跃,在复杂异步流中间件这个层⾯redux- saga仍处于领导地位