react心得
# 前言
React 就干两件事,打造用户界面,响应各种事件。16.0版本提出fiber,16.8版本提出hooks。
React原来是php项目,后来改了编译器,换成js来编译,然后加入虚拟dom。所以React只做两件事,一件事是渲染ui,一件事是响应事件。mvc和mvvm都是早些年angular的设计理念。react并不是什么mvvm和mvc,它只是一个很小的东西,只做那两件事儿。它只是个工具。backbone才是真正的mvc,它是模仿java的spring,挪到前端来了。
# 虚拟dom
json结构的描述,一个带有规则定义的js的对象(schema 数据模型)。比如你定义的菜单按钮权限的json,比如你定义低代码的json配置文件 如拖拉拽的图表生成器。
react的虚拟dom就是一个json描述的数据结构,一个能够映射出真实dom的数据schema。
# fiber比虚拟dom更快
以前的虚拟dom是一颗树,操作这颗树的时间复杂度是O(n^3),而且递归一棵树是没法暂停的。这样当树结构过大,必须得遍历时,那么页面会发生严重的卡顿。
而fiber是一个链表,操作一个链表的时间复杂度是O(n),而且fiber是可以暂停的,它不容易造成卡顿。
# 性能提升
react提升性能,使用16.0之前的版本,使用虚拟dom时,不改变版本的情况下,要减少跨层级的移动、删除节点的业务。
因为那样会很卡,虚拟dom是一颗树,时间复杂度是O(n^3),例如排序,就可以使用第三方别的库来做,改变dom的顺序一般不需要改变dom的内容,react的强项是改变dom的内容。
# fiber 怎么做到的?
利用浏览器空闲时间执行,不会长时间占用主线程。
将对比、更新dom的操作碎片化了。diff完成dom在内存中已经存在了,只是没有放在页面中,存到了fiber对象中的stateNode中去了。
碎片化的任务,可以根据需要来被暂停。
# requestIdleCallback
这个是浏览器提供的api,可以利用浏览器空闲时间执行任务。fiber就是借用这个api外加它的任务调度来做的。
# 将虚拟dom转成fiber对象
会将虚拟dom对象构建成fiber对象,根据fiber对象渲染成真实dom。
# feiber 的流程(迷你流程)
// 创建任务队列
const taskQueue = createTaskQueue()
//空闲时间执行的具体方法
const performTask = deadline => {
//执行务workLoop方法后续补充
workLoop(deadline)
//实现持续调用
if (subTask || !taskQueue.isEmpty()) {
requestIdleCallback(performTask)
}
}
// 暴露的render方法
export const render = (element, dom) => {
//1.添加任务 -> 构建fiber对象
taskQueue.push({
dom,
props: { children: element }
})
//2.指定浏览器空闲时间执行performTask 方法
requestIdleCallback(performTask)
}
// 子任务
let subTask = null
//commit操作标志
let pendingCommit = null
const workLoop = deadline => {
//1.构建根对象
if (!subTask) {
subTask = getFirstTask()
}
//2.通过while循环构建其余对象
while (subTask && deadline.timeRemaining() > 1) {
subTask = executeTask(subTask)
}
//3.执行commit操作。实现Dom挂载
if (pendingCommit) {
commitAllwork(pendingCommit)
}
}
const getFirstTask = () => {
// 获取任务队列中的任务
const task = taskQueue.pop()
//返回Fiber对象
return {
props: task.props,
stateNode: task.dom,
//代表拟Dom挂载的节点
tag: "host_root",
effects: [],
child: null
}
}
# React 生命周期
react只是删除了那几个生命周期函数,并没有删除那几个生命周期,生命周期函数在16.0之后发生了变化。生命周期的变化是加入fiber之后,才发生了那些变化。
# 16.0之前
初始化=》挂载props=》初始化state=》render dom=》完成
获取数据完毕=》更新state=》diff=》render dom =》完成
用的比较多的是componentDidMount。
shouldComponentUpdate 是手动干预提升react性能的唯一手段。
如果你能手动控制每一个组件的shouldComponentUpdate,就算你不用fiber,就算虚拟dom是树,你也未必慢。
所以这也体现了有经验的人写react和没经验的人写react是不一样的。
# 16.0之后
之前是树,循环加递归的去判断它是否发生变化。现在是链表,循环加打打标记的来判断它是否发生变化。
fiber的diff算法是在render中执行的,为了保证应对fiber的链式渲染,所以render之前的那些方法。
比如 componentWillMount、componentWillReceiveProps、componentWillUpdate,它们就不需要了,因为它可能会在render之前改变state属性,会造成优先级不正确的问题。
于是react觉得这些方法会存在风险,所以把它们直接干掉了。不过shouldComponentUpdate保留下来了,它不会改变state。
getDerivedStateFromProps 允许你在render之前改一下state,返回null就不更新state。
getSnapshotBeforeUpdate 可以获得上一次真实渲染的快照。
# diff算法
# 16.0之前
只要类型和key中任何一个发生变化,就算你节点变化。
树的节点遍历,只需要遍历一遍就行了,但是把树这个结构的遍历应用到diff算法中就是O(n^3),因为它进行三层遍历,分别时tree diff、component diff、element diff。
针对树结构(tree diff):只要存在UI层的DOM节点跨层级的操作,直接干掉,重新创建。
针对组件结构(component diff):拥有相同嘞的两个组件生成相似的树形结构,拥有不同类的两个组件会生成不同的树形结构。
针对元素结构(element diff):对于同一层级的一组节点,使用具有唯一性的id区分(key属性来区分)。
# 16.0之后
fiber的diff是可中断的,其实就是循环三遍,时间复杂度其实是O(3n)去除系数就是O(n)。
先通过state计算出新的fiber节点。
对比节点的tag和key确定节点操作(修改,删除,新增,移动)。移动是最复杂的。
用effectTag标记出fiber对象,第一次循环结束之后,它就能知道所有节点都干了啥。
收集所有标记的fiber对象,形成effctList。没有打上标记的,就表示它没变,就不用进行接下来的循环操作了。
然后在commit阶段一次性处理完,处理完所有变化的节点。
# 单节点
新旧相同 不变
新旧不同 更新
新有旧无 新增
# 多节点
新旧相同 不变 新有旧无 新增 新无旧有 删除 新旧顺序不同 位置移动或者互换 新旧顺序不同并且旧有新无 先删除再位置移动或者互换
# React 状态管理
React 默认没带数据流(状态管理),那些实现了数据流的库不光可以在react中使用,也可以在vue中使用。
每一个好的东西都有它自己适合的范围。
# flux
UI产生动作消息,将动作传递给分发器。(这个动作就是 比如 点击一个按钮或者ajax返回数据)
分发器广播给所有store。(以广播的方式告诉所有store。)
订阅的store做出反应,传递新的state给UI。
在react中flux叫rflux,flux中有多个store。
# redux
redux与flux不同,它只有一个单一的store。整个应用的state存储在一个单一的store树中。
state状态为只读,你不能直接修改state,应该要通过action触发state修改。
得使用纯函数进行状态修改,需要你写reducers纯函数来进行处理,reducer通过当前状态树和action来进行计算,返回一个新的state。(这个reducer可以当它是一个merge函数,merge完了之后返回一个新的state来更新掉那个旧的。)
不过如果一个应用的state都存到一个store中,那么效率就会很低,于是它提出了一个分支的概念,不同的state放到不同的分支上。
redux:https://react-redux.js.org/ (opens new window)
# flux 和 redux对比
flux和redux都可以作为react的状态管理,flux是多store,redux是单store。
flux有调度器来进行事件处理,redux依赖reducer来实现事件处理。
flux的state可以直接修改,redux 的state不能直接修改,要通过reducer进行state的merge方式来产生一个新的state。
# mbox
mbox是一个状态管理机制,是全自动的,action一改变state,views上就直接发生变化,不需要你像redux一样去写reducer函数了。
定义状态并使其可观察,使用修饰符@observer来使其可观察。
创建视图以响应状态变化。
更改状态(自动响应UI变化)。
mobx:https://cn.mobx.js.org/ (opens new window)
# hooks
组件之间复用状态逻辑很难,得使用redux或mox来实现状态逻辑的复用。
复杂组件变的难以理解
难以理解的class
hooks的使用降低了理解成本,也让react组件更像是函数。
它没有什么魔法,就是数组,还是两个数组,一个存所有state,一个存所有的state的setter。
# useState
用两个数组实现的,一个存所有的state,另一个存state的setter。
所以如果是在循环和判断的逻辑下用useState,会导致state和setter错乱,最好在函数顶部使用。
# useEffect
治病的过程中产生了额外的影响。
useEffect 就是在你初始化之后,再次render之前,产生的额外影响。比如 数组的slice 和 splice函数。
useEffect 函数就是hooks中副作用函数,第一个参数就是callback函数,第二个参数是一个数组,就是你要监听的变量。如果你放一个空数组,它会起到一个componentDidMount的作用,没有监听任何变量,那么就不存在任何变化,那么就只会执行一次。
如果你想销毁state,可以在useEffect函数return的第一个函数。
# useLayoutEffect
useEffect是在浏览器渲染后执行,useLayoutEffect是在浏览器渲染前执行。
官方不建议你是用它,但是官方还是出了这个hooks。
useLayoutEffect 主要在useEffect函数结果不满意的时候才被用到,一般是当处理dom改变带的副作用,这个hook执行时,浏览器并未对dom进行渲染,相对于useEffect执行要早。
# useEffect 和 useLayoutEffect
useEffect 和 useLayoutEffect在mount和update阶段执行方法一样,传参不一样。
useEffect异步执行,而useLayoutEffect同步执行。
Effect 用 HookPassive 标记useEffect,用HookLayout标记useLayoutEffect。
# useMemo
老的是memo,新的hooks中是useMemo。传递⼀个函数和依赖项,当依赖项发⽣变 化,执⾏函数,该函数需要返回⼀个值
类似于vue中的computed 计算属性,但是写法不同。
# useCallback
返回一个函数,当监听的数据发生变化,才会执行回调函数。
# useRef
相当于一个id的作用,id能够找真实dom,这个可以用来找虚拟dom,可以拿到fiber对象。
# useReducer
redux、mbox是完整独立的状态管理方案。未必一定是为react打造的,它们只是和react进行了结合。react本身关注的是渲染UI和响应各种事件,为了提高效率,所以官方才出了这个hook。
聚合参数,以达到开发效率的提升以及代码的简洁。
useReducer在re-render时候不会改变存储位置,state作为props传给子component不会产生diff的效率问题(useMemo优化)
useReducer也许是替代useState,由于数据并未能共享,所以 并不能替代redux、mbox。
# useContext
useContext能够产生一个Provider,能够使得你的Provider标签下面所有的组件共享一组数据。类似于一个局部的window哟。
# 自定义Hook
自定义的hook其实并不是hook,只是一个自定义的函数,只是它用了use标识开头,看起来像是符合了Hook的规则。
useXXX 和vue3的composition api很像。
# class和hook的对比
官方不考虑废除class。class只是一种编程方式,在16.0之后依然是使用fiber和新的diff算法,效率上并不比class差,提升效率使用的是fiber。
hooks延申的写法太多,也许页面中一堆useState,可能显得比较乱,有时很难搞清楚这些state之间的关联,当然你可以尝试根据相应的业务逻辑做一下聚合。容易发生内存泄露。
class语法虽然难以理解,都是代码逻辑比较清晰,它面向对象嘛。
hook往往作为那种单节点组件,比如几十个表单域。最后使用class来写一个整个表单的数据流的入口。
useEffect是浅监听。
# react性能优化
- 避免跨层级的移动
- sholudComponentUpdate
- Memo/useMemo
- useReducer
# 总结
看changelog的方法:看当中长的、大版本的,看你能看懂的,看不懂的就过。你能看懂,你还想了解的更深,点击#中的issue,然后你还可以通过有道的划词翻译,查看具体的译文。关键节点一定要记到时间点,至少要记到月份。
react 的fiber 让 react得到了一个质的提升,但react的生命周期并没有很大的变化,废弃掉了一些生命周期钩子,这些生命周期应该还有,只是不暴露到外面,防止你影响fiber的正常运行。
看源码:github桌面版,能够看到那些changelog的具体变化。具体的代码用vscode来看。
看源码不要广泛的去看,文件太多不那么容易看,看源码要学会放下,要学会存疑,看不懂就假想它是一个符号,你不用关系这个符号是干什么的,只需要知道这个符号是干什么的。
先看你认识的,你感兴趣的,不认识的先别管。官方有一些demo的,比如你要学什么,你通过看那些大牛写的demo,就能够学会怎么用以及部分的原理。
一个不恰当的例子吧,它和看毛片类似,先看你感兴趣的地方,然后再看看其它地方有没有漏掉的,你不可能每一部都从头看到尾。
先看react-dom,react-dom中没有fiber相关的内容,fiber相关的在react-reconciler中。
react的diff算法在fiber之后也有了很大的变化,之前三层遍历,分别时tree diff、component diff、element diff。
而现在fiber的diff是可中断的,其实就是循环三遍,时间复杂度其实是O(3n)去除系数就是O(n)。
react的状态管理有很多 flux、redux、mbox等等,没有谁好谁坏,只是每一个好的东西都有它自己适合的范围。
react的hooks降低了理解成本,也让react组件更像是函数。它没有什么魔法,就是数组,还是两个数组,一个存所有state,一个存所有的state的setter。用到了闭包,容易发生内存泄露。
class语法虽然难以理解,都是代码逻辑比较清晰,毕竟是面向对象,官方并没有废除class。
hooks往往作为那种单节点组件,比如几十个表单域。最后使用class来写一个整个表单的数据流的入口。
就算你使用了老的vdom,最终也会被转成fiber dom的,fiber dom就是react新一代的vdom。