架构相关
Fiber
产生原因
传统的虚拟dom在递归更新时无法中断,如果组件树层级较深时会占用大部分线程时间,使得页面卡顿。
Fiber的引入改变了这一情况。Fiber可以理解为是React自定义的一个带有链接关系的DOM树,每个Fiber都代表了一个工作单元,React可以在处理任何Fiber之前判断是否有足够的时间完成该工作,并在必要时中断和恢复工作。
其实也可以理解为增强的虚拟dom。
是什么?
React
内部实现的一套状态更新机制。支持任务不同 优先级
,可中断与恢复,并且恢复后可以复用之前的 中间状态
。
fiber怎么找到断点继续开始
// packages/react-reconciler/src/ReactFiberWorkLoop.js
JSX到页面dom的过程
JSX
经过 babel
转换后会变成 React.createElement(...)
形式的 js 代码,返回的是一个 React Element
实例,也就是虚拟dom。
JSX
是一种描述当前组件内容的数据结构,他不包含组件 schedule 、 reconcile 、render所需的相关信息。
在组件 mount
时,Reconciler
根据 JSX
描述的组件内容生成组件对应的 Fiber节点
。
在组件 update
时,Reconciler
将 JSX
与 Fiber节点
保存的数据对比,生成组件对应的 Fiber节点
,并根据对比结果为 Fiber节点
打上 标记
。
最后进入 Render
阶段,将fiber转化为真实dom并显示在页面上。
React diff
Diff的执行位于render阶段的 update
。
对于
update
的组件,他会将当前组件与该组件在上次更新时对应的Fiber节点比较(也就是俗称的Diff算法),将比较的结果生成新Fiber节点。
一个
DOM节点
在某一时刻最多会有4个节点和他相关。
current Fiber
。如果该DOM节点
已在页面中,current Fiber
代表该DOM节点
对应的Fiber节点
。workInProgress Fiber
。如果该DOM节点
将在本次更新中渲染到页面中,workInProgress Fiber
代表该DOM节点
对应的Fiber节点
。DOM节点
本身。JSX对象
。即ClassComponent
的render
方法的返回结果,或FunctionComponent
的调用结果。JSX对象
中包含描述DOM节点
的信息。
Diff算法
的本质是对比1和4,生成2。
SSR
SSR的整个过程,Next.js做了什么
为什么SSR的首屏时间会更短
之所以说ssr快,主要体现在http请求数量上,ssr只需要请求一个html文件就能展现出页面,虽然在服务器上会调取接口,但服务器之间的通信要远比客户端快,甚至是同一台服务器上的本地接口调取。
spa慢主要慢在需要请求大量的js资源,一般的首页,请求10几个js都算少到,要知道js的加载是同步的,页面逻辑可能藏在最后几个js里面,请求-等待-下载-然后是解析js-再调接口-展现页面。
React各版本区别
函数式组件和类式组件
函数式组件与类组件有何不同? - 知乎 (zhihu.com)
v16
重写底层逻辑,引入fiber架构
引入Hooks (16.7)
v17
全新的jsx转换器
- 用
jsx()
函数替换React.createElement()
- 运行时自动引入
jsx()
函数,无需手写引入react
副作用清理时机
useEffect
副作用清理函数由同步变为异步执行,如果要卸载组件,则清理会在屏幕更新后运行
启发式更新算法更新
- React16的
expirationTimes
模型只能区分是否>=expirationTimes
决定节点是否更新。 - React17的
lanes
模型可以选定一个更新区间,并且动态的向区间中增减优先级,可以处理更细粒度的更新。
v18
Concurrent Mode 并发模式
setState 自动批处理
在 React 18 之前
,我们只在 React 事件处理函数
中进行批处理更新。默认情况下,在 promise
、setTimeout
、原生事件处理函数
中、或 任何其它事件内
的更新都不会进行批处理。
在 React 18
之后,任何情况都会自动执行批处理,多次更新始终合并为一次。
更新 render API
v18 使用 ReactDOM.createRoot()
代替 ReactDOM.render()
,创建一个新的根元素进行渲染,使用该 API,会自动启用并发模式。
Suspense 支持 SSR
可以使用 <Suspense>
来将程序分解成较小的独立单元,允许服务端分组件返回页面,有选择的让重要的部分首先加载与水合。
useTransition
Hooks
useState
执行过程
setState后发生了什么?
update
时,useReducer
与 useState
调用的则是同一个函数updateReducer。
function updateReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: (I) => S
): [S, Dispatch<A>] {
// 获取当前hook
const hook = updateWorkInProgressHook();
const queue = hook.queue;
queue.lastRenderedReducer = reducer;
// ...同update与updateQueue类似的更新逻辑
const dispatch: Dispatch<A> = (queue.dispatch: any);
return [hook.memoizedState, dispatch];
}
整个流程可以概括为一句话:
找到对应的
hook
,根据update
计算该hook
的新state
并返回。
什么时候需要传入一个函数作为参数?
通常用于优化性能,也称为惰性初始化。
const initialState = Number(window.localStorage.getItem("count"));
const [count, setCount] = React.useState(initialState);
每次组件更新时都会重新执行一遍第一行的代码,当localstorage数据偏大时有不小的性能开销。
const initialState = () => Number(window.localStorage.getItem("count"));
const [count, setCount] = React.useState(initialState);
当 initialState
以函数形式传入时,它只会在函数组件初始化的时候执行一次,规避不必要的性能问题。
异步更新与批处理更新时需要注意
在setState时需要传入一个函数,获取之前的state值保证每次获取到的是最新
let [num, setNum] = useState(0)
const add = () => {
let = num = 0;
setNum(num + 1);
setNum(num + 1);
setNum(num + 1);
console.log(num); // 1
}
传入函数作为参数,确保每次访问的值是更新后的值。
let [num, setNum] = useState(0);
const add = () => {
setNum(num => num + 1);
setNum(num => num + 1);
setNum(num => num + 1);
console.log(num); // 3
};
异步操作state值时情况类似,也需要传入函数操作。
useCallback
useMemo
useRef
如何解决闭包?
性能优化
组件
受控/非受控
- 受控组件通过
props
来取得其当前值,并通过诸如onChange
的回调通知更改。父组件通过处理回调并管理自己的状态,将新值作为 props 传递给受控组件来“控制”它。也可以称之为“哑组件”。 - 非受控组件是一种在内部存储自己状态的组件,当需要时,使用
ref
查询 DOM 来查找其当前值。这有点类似于传统的 HTML。
通信方式
Redux
如何理解redux的三大原则,为什么必须这样?
-
单一数据源
使得状态之间更好管理,更容易调试,以及一些撤销/重做的功能更容易。同时解决了Flux中存在的多的Store带来的Store互相依赖问题。
-
State 是只读的
更方便定位问题、追踪数据,避免竞态问题的出现。
-
使用纯函数来修改state
借鉴了函数式编程,纯函数的好处是没有副作用,相同的输入,永远都会有相同的输出。这样做的好处是这个函数可以不依赖外部环境,更加解耦,更容易调试以及更容易的写单元测试。
useContext与useReducer
userReducer
获取state
和dispatch
userContext
将state
和dispatch
共享到子组件
与Redux的区别
仍然强耦合于UI,生命周期局限在函数组件内部,并没有做到分离。一般用于只有少部分组件需要深层传递数据时的简易方案使用。