# 2020-100 道面试题
# 6: React 项目中有哪些细节可以优化,实际开发中都做过哪些性能优化
对于正常的项目优化,一般涉及到几个方面:开发过程中、上线之后的首屏、运行过程的状态
上线后的首屏及运行状态
- 首屏的优化涉及到几个指标:FP、FCP、FMP;要有一个良好的体验是尽可能的把 FCP 提前,需要做一些工程化的处理,去优化资源的加载
- 方式及分包策略,资源的减少是最有效的加快首屏打开方式
- 对于 CSR 应用,FCP 的过程一般是首先加载 js 与 css 资源,js 在本地执行完成,然后加载数据回来,做内容初始化渲染,这中间就有几次网络反复请求的过程,所以 CSR 可以考虑使用骨架屏及预渲染(部分结构预渲染),suspence 与 lazy 做懒加载动态组件的方式
- 当然还有另一种方式就是 SSR 的方式,SSR 对于首屏的优化有一定的优势,但这种瓶颈一般在 Node 服务端的处理,建议使用 stream 流的方式来处理,对于体验与 node 端的内存管理等,都有优势。
- 不管对于 CSR 还是 SSR,都建议配合使用 ServiceWorker,来控制资源的调配及骨架屏秒开的体验
- react 项目上线后,首先需要保障可用性,所以可以通过 React.Profiler 分析组件渲染次数及耗时的一些任务,但 Profile 记录的是 commit 阶段的数据,所以对于 react 的调和阶段需要结合 performance API 一起分析。
- 由于 React 是父级 props 改变之后,所有与 props 不相关的子组件在没有添加条件控件的情况下,也会触发 render 的渲染,这是没必要的,可以结合 React 的 PureComponent 以及 React.memo 等做浅处理,这中间有涉及到不可变数据的处理,当然也可以结合使用 ShouldComponentUpdate 做深比较处理。
- 所有的运行状态优化,都是减少不必要的 render,React.useMemo 与 React.useCallback 也是可以做很多优化地方
- 在很多应用中,都会涉及到使用 redux 以及使用 context,这两个都可能造成许多不必要的 render,所以在使用的时候,也需要谨慎处理一些数据
- 最后就是保证整个应用的可用性,为组件创建错误边界,可以使用 componentDidCatch 来处理
实际项目中开发过程中还有很多其他优化点:
- 保证数据的不可变性
- 使用唯一的键值迭代
- 使用 web worker 做密集型的任务处理
- 不在 render 中处理数据
- 不必要的标签,使用 React.Fragments
SSR:【服务端渲染】 CSR【客户方渲染】
FP(first paint): 首次绘制
FCP(first contentful paint): 首次内容绘制
LCP(Largest contentful paint): 最大内容渲染
DCL(DomContentloaded)
FMP(First Meaningful Paint): 首次有效绘制
L(onload)
TTL(Time to Interactive): 可交互时间
TBT(Total Blocking Time): 页面阻塞总时长
FID(First Input Delay): 首次输入延迟
CLS(Cumulative Layout Shift): 累积布局偏移
SI(Speed Index)
# 7. react 最新版本解决了什么问题,加了哪些东西,去了哪些东西
- React 16.x 去掉了
componentWillMount
、componentWillReceiveProps
、componentsWillUpdata
三个生命周期,为弥补失去上面三个生命周期的不足,又加了两个,static getDerivedStateFromProps
,getSnapshotBeforeUpdate
,为啥要改,因为新的 React 用 Fiber 优化了算法,当有紧急的事情时,会打断渲染,处理完会从头渲染组件,不能保证只在挂载/拿到 props/状态变化的时候刷新一次,所以被标记为 Unsafe。
- React 16.x 去掉了
- React 16.x 的三大新特性 Time Slicing, Suspense, hooks
- i. Time Slicing(解决 CPU 速度问题),使得在执行任务的期间可以随时暂停,跑去干别的事情,这个特性使得 react 能在性能极其差的机器跑时,仍然能保持良好的性能
- ii. Suspense(解决网络 IO 问题)和 lazy 配合,实现异步加载组件。能暂停当前组件的渲染,当完成某件事以后在继续渲染,解决从 react 出生到现在都存在的【异步副作用】的问题,并且解决得非常优雅,使用的是【异步但是同步的写法】,个人认为,这是最好的解决异步问题的方式
- iii. 此外,还提供了一个内置函数 componentDidCatch,当有错误发生时,我们可以友好地展示 fallback 组件;可以捕捉到它的子元素(包括嵌套子元素)抛出的异常,可以复用错误组件
- React16.8
- 加了 hooks,让 React 函数式组件更加灵活
- hooks 之前,React 存在很多问题
- a. 在组件间复用状态逻辑很难
- b. 复杂组件变得难以理解,高阶组件和函数组件嵌套过深
- c. class 组件的 this 指向问题
- d. 难以记忆的生命周期
- hooks 很好的解决了上述问题,hooks 提供很多方法
- a. useState 返回有状态值,以及更新这个状态值的函数
- b. useEffect 接受包含命令的方式,可能有副作用代码的函数
- c. useContext 接受上下文对象(从 React.createContext 返回值)并返回当前上下文值
- d. useReducer useState 的替代方案,接受类型为(state, action)=> newState 的 reducer,并返回与 dispatch 方法配对的当前状态。
- e. useCallback 返回一个回忆的 memoized 版本,该版本仅在其中一个输入发生更改时才会更改。纯函数的输入输出确定性。
- f. useMemo 纯的记忆函数
- g. useRef 返回一个可变的 ref 对象,其.current 属性被初始化为传递的参数,返回 ref 对象在组件的整个生命周期内保持不变
- h. useImperativeMethods 自定义使用 ref 时公开给父组件的实例值
- i. useMutationEffect 更新兄弟组件之前,它在 react 执行其 DOM 改变的同一阶段同步触发
- j. useLayoutEffect DOM 改变后同步触发。使用它来从 DOM 读取布局并同步重新渲染
- React16.9
- i. 重命名 Unsafe 生命周期方法,新的 UNSAFE_前缀将有助于在代码 review 和 debug 期间,使这些有问题的字样更突出
- ii. 废弃 javascript: 形式的 URL,以 javascript 开头的 URL 非常容易遭受攻击,造成安全漏洞。
- iii. 废弃
Factory
组件,工厂组件会导致 React 变大且变慢 - iv. act()也支持异步函数,并且可以在调用它时使用 await
- v. 使用
<React.Profiler>
进行性能评估,在较大的应用中追踪性能回归可能会很方便
- React16.13.0
- i. 支持在渲染期间调用 setState,但仅适用于同一组件
- ii. 可检测冲突样式规则并记录警告
- iii. 废弃 unstatble_createPortal,使用 createPortal
- iv. 将组件堆栈添加到其开发警告中,使开发人员能够隔离 bug 并调试其程序,这可以清楚地说明问题所在,更快的定位和修复错误
# 17. React 事件绑定原理
React 并不是将 click 事件绑定在该 div 的真实 DOM 上,而是在 document 处监听所有支持的事件,当事件发生并冒泡至 document 处时,react 将事件内容封装并交由真正的处理函数运行。这样的方式不仅减少内存消耗,还能在组件挂载销毁时统一订阅和移除事件。 另外冒泡到 document 上的事件也不是原生浏览器事件,而是 React 自己实现合成事件(SyntheticEvent). 因此我们如果不想要事件冒泡的话,调用 event.stopPropagation 是无效的,而应该调用 event.preventDefault。
- 事件注册
- 组件装载/更新
- 通过 lastProps、nextProps 判断是否新增、删除事件分别调用事件注册、卸载方法
- 通过 EventPluginHub 的 enqueuePutListener 进行事件存储
- 获取 document 对象
- 根据事件名称(如 onClick、onCaptureClick)判断是进行冒泡还是捕获
- 判断是否存在 addEventListener 方法,否则使用 attachEvent(兼容 IE)
- 给 document 注册原生事件回调为 dispatchEvent(统一的事件分发机制)
- 事件存储
- EventPluginHub 负责管理 React 合成事件 callback,它将 callback 存储在 listenerBank 中,另外还存储了负责合成事件的 Plugin
- EventPluginHub 的 putListener 方法是向存储容器中增加一个 listener
- 获取绑定事件的元素的唯一标识 key
- 将 callback 根据事件类型,元素的唯一标识 key 存储在 listenerBank 中
- listenerBank 的结构是:
listenerBank[registrationName][key]
- 事件触发执行
- 触发 document 注册原生事件的回调 dispatchEvent
- 获取到触发这个事件最深一级的元素
- 这里的事件执行利用了 React 的批处理机制
<div onClick={this.parentClick} ref={(ref) => (this.parent = ref)}>
<div onClick={this.childClick} ref={(ref) => (this.child = ref)}>
test
</div>
</div>
/**
* 首先会获取到this.child
* 遍历这个元素的所有父元素,依次对每一级元素进行处理
* 构造合成事件
* 将每一级的合成事件存储在eventQueue事件队列中
* 遍历eventQueue
* 通过isPropagationStopped判断当前事件是否执行了阻止冒泡方法
* 如果阻止了冒泡,停止遍历,否则通过executeDispatch执行合成事件
* 释放处理完成的事件
*/
- 合成事件
- 调用 EventPluginHub 的 extractEvents 方法
- 循环所有类型的 EventPlugin(用来处理不同事件的工具方法)
- 在每个 EventPlugin 中根据不同的事件类型,返回不同的事件池
- 在事件池中取出合成事件,如果事件池为空,则创建一个新的
- 根据元素 nodeid(唯一标识 key)和事件类型从 listenerBink 中取出回调函数
- 返回带有合成事件参数的回调函数
# 25. React 组件通信方式
react 组件间通信通常有以下几种情况
- i. 父组件向子组件通信
- ii. 子组件向父组件通信
- iii. 跨级组件通信
- iv. 非嵌套关系的组件通信
- a. 可以使用自定义事件通信(发布订阅模式)
- b. 可以通过 redux 等进行全局状态管理
- c. 如果是兄弟组件通信,可以找到两个兄弟的共同父节点,结合父子间通信方式进行通信
// i. 父组件向子组件通信
const Child = (props) => {
return <p>{props.name}</p>;
};
const Parent = () => {
return <Child name="baba" />;
};
// ii. 子组件向父组件通信: props+回调方式
const Child = (props) => {
const cb = (msg) => {
return () => {
props.callback(msg);
};
};
return <button onClick={cb("baba")}>click me</button>;
};
const Paret = ()=>{
callback(msg){
console.log(msg);
}
render(){
return <Child callback = {this.callback.bind(this)}/>
}
}
// iii. 跨级组件通信 即: 父组件向组件通信,向更深层子组件通信
// 3.1 使用props利用中间组件传递,果然过深会增加复杂度,并且这些props并不是中间组件自己想要的
// 3.2 使用context,context相当于一个大容器,可以把要通信的内容放到容器里,contex📱目的就是为了共享对于一个组件树而言是全局的数据
const BatteryContext = createConext();
// 子组件的子组件
class GrandChild extends Component{
render(){
return (
<BatteryContext.Consumer>
{
color=> <h1 style={{color: color}}>我的颜色: {color}</h1>
}
</BatterConntext.Consumer>
)
}
}
// 子组件
class Child = ()=>{
return (
<GrandChild />
)
}
// 父组件
class Parant extends Component{
state = {
color: "red"
}
render(){
const {color} = this.state;
return (
<ButteryContext.Provider value={color}>
<Child />
</ButteryContext.Provider>
)
}
}
# 26. redux-saga
和mobx
的比较
saga 是 redux 处理异步的一种方式。saga 需要一个全局监听器(watcher saga),用于监听组件发出 action,将监听的 action 转发给对应的接收器(workersaga),再由接收器执行具体任务,任务完了在发出另一个 action 交由 reducer 修改 state.
mobx 与 redux 的功能相似,mobx 实现思想和 Vue 几乎一样,所以其优点跟 Vue 也差不多:通过监听数据(对象,数组)的属性变化,可以通过直接在数据上更改就能触发 UI 渲染,从而做到MVVM
、响应式、上手成本低、开发效率高。
- redux 将数据保存在单一的 store 中,mobx 将数据保存在分散的多个 store 中
- redux 使用
plain object
保存数据,需要手动处理变化后的操作,mobx 使用 observeble 保存数据,数据变化后自动处理响应操作。 - redux 使用不可变状态,这意味着状态是只读的,不能直接去修改他,而是应该返回一个新的状态,同时使用纯函数;mobx 中状态是可变的,可直接对其进行修改。
- mobx 相对来说比较简单,在其中有很多抽象,mobx 更多的使用面向对象的编程思维;redux 会比较复杂,因为其中的函数式编程思想掌握起来不太容易,同时需要借助一系列的中间件来处理异常和副作用
- mobx 中有更多的抽象和封装,调试会比较困难,同时结果难以预测,而 redux 提供能够进行时间回溯的开发工具,同时其纯函数以及更少的抽象,让调试变的更加容易。
- 状态管理
redux-sage
是redux
的一个异步处理中间件mobx
是数据管理库,和 redux 一样
- 设计思想
redux-sage
属于 flux 体系,函数式编程思想mobx
不属于 flux 体系,面向对象编程和响应式编程
- 主要特点
redux-sage
因为是中间件,更关注异步处理的,通过 Genetator 函数来将异步变为同步,使代码可读性高,结构清晰。action 也不是action creator
而是pure action
- 在
Generator
函数中通过call
或put
方法直接声明式调用,并自带一些方法,如takeEvery
、takeLast
、race
等,控制多异步操作,让多个异步更简单 mobx
是更简单方便灵活的处理数据,store 是包含了 state 和 action。state 包装成一个可被视察的对象,action 可以直接修改 state,之后通过Computed values
将依赖 state 的计算属性更新,之后触发 Reactions 响应依赖 state 的变更,输出相应的副作用,但不生成新的 state.
- 数据可变性
redux-sage
强调 state 不可变,不能直接操作 state,通过 action 和 reducer 在原来的 state 基础上返回一个新的 state 达到改变 state 的目的mobx
直接在方法中更改 state, 同时所有使用 state 都发生变化,不生成新的 state
- 写法难易度
redux-saga
比redux
在actionn
和reducer
上要简单一些,需要用 dispatch 触发 state 的改变,需要 mapStateToProps 订阅 state- mobx 在非严格模式下不用 action 和 reducer,在严格模式下需要 action 中修改 state, 并自动触发相关的依赖更新
- 使用场景
redux-sage
很好的解决了 redux 关于异步处理时的复杂度和代码冗余的问题,数据流向好追踪。但 redux 的学习成本比较高,代码比较冗余,不是特别需要状态管理,可以用别的替换mobx
学习成本低,能快速上手,代码比较简洁,但可能因为代码编写的原因和数据更新时相对黑盒,导致数据流向不利于追踪。
# 27. 说一下 react-fiber
- 背景
- react 在进行组件渲染时,从 setState 开始到渲染完成整个过程是同步的(一气呵成)。如果需要渲染的组件比较庞大,js 执行会占据主线程事件较长,会导致页面响应度变差,使得 react 在动画、手势等应用中效果比较差。
- 页面卡顿:Stackreconciler 的工作流程很像函数调用过程,父组件里调用子组件,可以类比为函数递归,对于特别庞大的 vDOM 树来说,reconciliatio 过程会很长(x00ms),超过 16ms,在这期间,主线程被 js 占用,因此任何交互、布局、渲染都会停止,给用户的感觉就是页面被卡住。
- 实现原理
- 旧版 React 通过递归的方式进行渲染,使用的是 JS 引擎自身的函数调用栈,它会一直执行到栈空为止。而 Fiber 实现了自己的组件调用栈,它以链表的修饰遍历组件树,可以灵活的暂停、继续和丢弃执行的任务。实现方式是使用了浏览器的 requesIdleCallback 这个 api
- Fiber 其实指的是一种数据结构,它可以用一个纯的 js 对象来表示:
const fiber = { stateNode, // 节点实例 child, // 子节点 sibling, // 兄弟节点 return, // 父节点 };
- react 内部运转分为三层:
- Virtual DOM 层,描述页面长什么样子
- Reconciler 层,负责调用组件生命周期方法,进行 Diff 运算等
- Renderer 层,根据不同平台,渲染出相应的页面,比较常见的是 ReactDOM 和 ReactNative
- 为实现不卡顿,就需要有一个调度器(Scheduler)来进行任务分配。优先级高的任务(如:键盘输入)可以打断优先级低的任务(如:Diff)的执行,从而更快的生效。任务的优先级有六种
- synchronous 与之前的 StackReconciler 操作一样,同步执行
- task:在 next tick 之前执行
- animation:下一帧之前执行
- high:在不久的将来执行
- low:稍微延迟执行也没关系
- offscreen 下一次 render 或 scroll 时执行
- Fiber Reconciler(react)执行阶段
- 阶段一:生成 Fiber 树,得出需要更新的节点信息,这一步是一个渐进的过程,可以被打断
- 阶段二:将需要更新的节点一次过批量更新,这个过程不能被打断
- Fiber 树:Fiber Reconciler 在阶段一进行 Diff 计算的时候,会基于 VirtualDOM 树生成一课 Fiber 树,它的本质是链表
- 从 Stack Reconciler 到 FiberReconciler,源码层面其实就是干了一件递归改循环的事情。
# 42. 说一下 React Hooks 在平时开发中需要注意的问题及原因
- 不要在循环,条件或嵌套函数中调用 Hook,必须始终在 React 函数的顶层使用 hook
- 这是因为 React 需要利用调用顺序来正确更新相应的状态,以及调用相应的钩子函数,一旦在循环或条件语句中调用 Hook,就容易导致调用顺序不一致性,从而产生难以预料的后果。
- 使用
useState
时候,使用 push,pop,splice 等直接更改数组对象的坑
- 使用 push 直接更改数组无法获取到最新的值,应该采用析构方式,但在 class 里不会有这个问题
function Indicatorfilter() { let [num, setNums] = useState([0, 1, 2, 3]); const test = () => { // 这里的坑是直接采用push去更新num,setNums(num)是无法更新num的 // 必须使用 num = [...num, 1] setNums(num); num.push(1); setNums(num); }; return ( <div className="filter"> <button onClick={test}>click test</button> <p>{num.join()}</p> </div> ); }
- 使用
useState
设置状态的时候,只有第一次生效,后期需要更新状态,必须通过useEffect
// error
const TableDetail = ({ columns }, TableData) => {
const [tabColumn, setTabColumn] = useState(columns);
};
// success
const TableDetail = ({ columns }: TableData) => {
const [tabColumn, setTabColumn] = useState(columns);
useEffect(() => {
setTabColumn(columns);
}, [columns]);
};
- 善用
useCallback
- 当
useCallback
父组件传递子组件事件句柄时,如果没有任何参数变动的组件即使用 useMeno,也会跟着渲染一次
- 善用
- 不要滥用
useContext
,可以使用基于useContext
封装的状态管理工具
- 不要滥用
← /课程表.html HTML 和 CSS →