# React
# 什么是DOM?
DOM 是一项 W3C (World Wide Web Consortium) 标准,DOM 定义了访问文档的标准,
“W3C 文档对象模型(DOM)是中立于平台和语言的接口,它允许程序和脚本动态地访问、更新文档的内容、结构和样式。”
W3C DOM 标准被分为 3 个不同的部分:
- Core DOM 所有文档类型的标准模型
- XML DOM XML 文档的标准模型
- HTML DOM HTML 文档的标准模型
# 虚拟DOM是什么?
virtual DOM 虚拟DOM,用普通js对象来描述DOM结构,它通过JS的Object对象模拟DOM中的节点,然后再通过特定的render方法将其渲染成真实的DOM节点。因为不是真实DOM,所以称之为虚拟DOM。virtual DOM
就是Dom的抽象化
# 为什么操作真实DOM的成本比较高?
- dom树的实现模块和 js 模块是分开的这些跨模块的通讯增加了成本
- dom 操作引起的浏览器的回流和重绘,使得性能开销巨大
# 为何虚拟Dom性能更优?
虚拟 dom 是相对于浏览器所渲染出来的真实 dom而言的,在react,vue等技术出现之前,我们要改变页面展示的内容只能通过遍历查询 dom 树的方式找到需要修改的 dom 然后修改样式行为或者结构,来达到更新ui 的目的。 这种方式相当消耗计算资源,因为每次查询 dom 几乎都需要遍历整颗 dom 树,如果建立一个与 dom 树对应的虚拟 dom 对象( js 对象),以对象嵌套的方式来表示dom树及其层级结构,那么每次 dom 的更改就变成了对 js 对象的属性的增删改查,这样一来查找 js 对象的属性变化要比查询 dom 树的性能开销小
# React Diff 算法
流程:
- 生成一个虚拟DOM树
- 操作导致DOM变化,生成新的虚拟DOM树
- 对比两个虚拟DOM树
- 规则:深度优先遍历,同层比较
- 如果节点类型相同的话:
- DOM元素类型: 对比属性和值,只改变需要改变的属性
- 组件类型: 根据新节点的props去更新原来根节点的组件实例,触发一个更新过程,最后在对所有的child节点在进行diff的递归比较更新
- 生成Patch
- 打入Patch,更新DOM树
- 真实渲染
# 策略
- 策略一:忽略Web UI中DOM节点跨层级移动;
- 策略二:拥有相同类型的两个组件产生的DOM结构也是相似的,不同类型的两个组件产生的DOM结构则不近相同
- 策略三:对于同一层级的一组子节点,通过分配唯一id进行区分(key值)
在Web UI的场景下,基于以上三个点,React对tree diff
、component diff
、element diff
进行优化,将普适diff的复杂度降低到一个数量级,保证了整体UI界面的构建性能!
# tree diff
基于策略一,React的做法是把DOM tree分层级,对于两个DOM tree只比较同一层次的节点,忽略DOM中节点跨层级移动操作,只对同一个父节点下的所有的子节点进行比较。如果对比发现该父节点不存在则直接删除该节点下所有子节点,不会做进一步比较,这样只需要对DOM tree进行一次遍历就完成了两个tree的比较。
# component diff
React应用是基于组件构建的,对于组件的比较优化侧重于以下几点:
- 同一类型组件遵从tree diff比较v-DOM树
- 不通类型组件,先将该组件归类为dirty component,替换下整个组件下的所有子节点
- 同一类型组件Virtual DOM没有变化,React允许开发者使用shouldComponentUpdate来判断该组件是否进行diff,运用得当可以节省diff计算时间,提升性能
# element diff
对于同一层级的element节点,diff提供了以下3种节点操作
- INSERT_MARKUP 插入节点:对全新节点执行节点插入操作
- MOVE_EXISTING 移动节点:组件新集合中有组件旧集合中的类型,且element可更新,即组件调用了receiveComponent,这时可以复用之前的DOM,执行DOM移动操作
- REMOVE_NODE 移除节点:此时有两种情况:组件新集合中有组件旧集合中的类型,但对应的element不可更新、旧组件不在新集合里面,这两种情况需要执行节点删除操作
# React Key有什么用
总结:是一种用空间换时间的方式,使用key能够对同一层级的同组子节点进行区分和标识,避免对比的时候频繁的创建新的元素
bad:
不使用key
old: A B C D
new: B A D C
结果:虽然仅仅只是元素的位置发生改变了,但是按照diff的规则(不使用key),那么B A D C都会是重新创建的新的元素
使用key
old: A B C D
new: B A D C
结果:使用key后,diff算法会对元素和位置进行比较,仅移动小范围的元素即可达到高效的结果
局限性
React的diff也有自己的不足之处,比如新旧集合元素全部可以复用,只是新集合中将旧集合最后一个元素放到了第一个位置,短板就会出现
ABCD -> DABC
A B C D
D A B C
比如 按照React的diff算法,不是D移动一次,而是需要将A,B,C移动三次到最后,所以应该尽量减少类似将最后一个节点移动到列表首部的操作
,当节点数量过大或更新操作过于频繁时,在一定程度上会影响 React 的渲染性能
# React 优势和不足
优势
- Virtual DOM提升性能
- 通过React,开发者将不再需要关注某个数据的变化如何更新到一个或多个具体的DOM元素,而只需要关心在任意一个数据状态下,整个界面是如何Render的,降低了逻辑复杂性和开发难度
- JSX
- 组件化开发
不足
- 想实现一个大型的项目还要其他很多的框架/库的配合去处理 数据存储、路由、数据请求
# React 中的性能优化
- React Hook useCallback 缓存函数
- React 动态import,利用 React.lazy 和 React.Suspense 延迟加载不是立即需要的组件
- React pureComponent/ shouldComponentUpdate
- React useMemo 缓存大量的计算
- 使用 React.memo 来缓存组件
- 使用Key
- 尽量使用 CSS 而不是强制加载和卸载组件
- 使用 React.Fragment 避免添加额外的 DOM
# React 16 调度原理
问题 如果 JS 在执行代码,那么渲染引擎工作就会被停止。假如我们有一个很复杂的复合组件需要重新渲染,那么调用栈可能会很长调用栈过长, 再加上如果中间进行了复杂的操作,就可能导致长时间阻塞渲染引擎带来不好的用户体验,调度就是来解决这个问题的
为了实现时间分片
React 会根据任务的优先级去分配各自的expirationTime
,在过期时间到来之前先去处理更高优先级的任务,并且高优先级的任务还可以打断低优先级的任务(因此会造成某些生命周期函数多次被执行),从而实现在不影响用户体验的情况下去分段计算更新(也就是时间分片)。
如何实现 React 实现调度主要靠两块内容:
- 计算任务的expirationTime
- 实现 requestIdleCallback的polyfill版本 (requestAnimationFrame + 计算帧时间及下一帧时间 + MessageChannel)
计算任务的expirationTime = 当前时间 + 一个常量(取决于任务的优先级)
var ImmediatePriority = 1;
var UserBlockingPriority = 2;
var NormalPriority = 3;
var LowPriority = 4;
var IdlePriority = 5;
对应的数值
var maxSigned31BitInt = 1073741823;
// Times out immediately
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
// Eventually times out
var USER_BLOCKING_PRIORITY = 250;
var NORMAL_PRIORITY_TIMEOUT = 5000;
var LOW_PRIORITY_TIMEOUT = 10000;
// Never times out
var IDLE_PRIORITY = maxSigned31BitInt;
实现 requestIdleCallback的polyfill 版本 因为requestIdleCallback的兼容性不好,并且1秒只能调用20次
所以自己实现 采用requestAnimationFrame + setTimeout
的组合
何时渲染:在浏览器空闲时且是渲染后才调用回调方法
了解一帧: 在一帧当中,浏览器可能会响应用户的交互事件、执行 JS、进行渲染的一系列计算绘制 如果以上这些操作超过了 16.6 毫秒,那么就会导致渲染没有完成并出现掉帧的情况,继而影响用户体验; 如果以上这些操作没有耗时 16.6 毫秒的话,那么我们就认为当下存在空闲时间让我们可以去执行任务;
我们需要计算出当前帧是否还有剩余时间让我们使用;
简单来说就是假设当前时间为 5000,浏览器支持 60 帧,那么 1 帧近似 16 毫秒,那么就会计算出下一帧时间为 5016。 得出下一帧时间以后,我们只需对比当前时间是否小于下一帧时间即可,这样就能清楚地知道是否还有空闲时间去执行任务。
最后一步:如何在渲染以后才去执行任务,利用事件循环
这里选择宏任务,因为在渲染以后只有宏任务是最先会被执行的。
生成一个宏任务有很多种方式并且各自也有优先级,那么为了最快地执行任务,我们肯定得选择优先级高的方式。 在这里我们选择了 MessageChannel 来完成这个任务,不选择 setImmediate 的原因是因为兼容性太差
总结:
首先每个任务都会有各自的优先级,通过当前时间加上优先级所对应的常量我们可以计算出 expirationTime,高优先级的任务会打断低优先级任务 在调度之前,判断当前任务是否过期,过期的话无须调度,直接调用 port.postMessage(undefined),这样就能在渲染后马上执行过期任务了 如果任务没有过期,就通过 requestAnimationFrame 启动定时器,在重绘前调用回调方法 在回调方法中我们首先需要计算每一帧的时间以及下一帧的时间,然后执行 port.postMessage(undefined) channel.port1.onmessage 会在渲染后被调用,在这个过程中我们首先需要去判断当前时间是否小于下一帧时间。 如果小于的话就代表我们尚有空余时间去执行任务;如果大于的话就代表当前帧已经没有空闲时间了,这时候我们需要去判断是否有任务过期, 过期的话不管三七二十一还是得去执行这个任务。如果没有过期的话,那就只能把这个任务丢到下一帧看能不能执行了
剖析 React 源码:调度原理 (opens new window)
# React 16 Fiber 架构介绍
为什么会出现fiber
React fiber 产生的根本原因,是大量的同步计算任务阻塞了浏览器的 UI 渲染
。默认情况下,JS 运算、页面布局和页面绘制都是运行在浏览器的主线程当中,他们之间是互斥的关系。如果 JS 运算持续占用主线程,页面就没法得到及时的更新。当我们调用setState
更新页面的时候,React 15 会递归的遍历应用的所有节点,计算出差异,然后再更新 UI。如果页面元素很多,整个过程占用的时机就可能超过16
毫秒,就容易出现掉帧的现象,用户会感觉到卡顿
为了给用户制造一种应用很快的“假象”,不能让一个任务长期霸占着资源。 可以将浏览器的渲染、布局、绘制、资源加载(例如 HTML 解析)、事件响应、脚本执行视作操作系统的“进程”,需要通过某些调度策略合理地分配 CPU 资源,从而提高浏览器的用户响应速率, 同时兼顾任务执行效率。 所以 React 通过Fiber 架构,让这个执行过程变成可被中断。"适时"地让出 CPU 执行权。
提出了异步渲染(时间分片)
Fiber 其实指的是一种数据结构,它可以用一个纯 JS 对象来表示:
const fiber = {
stateNode, // 节点实例
child, // 子节点
sibling, // 兄弟节点
return, // 父节点
}
实现原理:
为了实现不卡顿,就需要有一个调度器 (Scheduler) 来进行任务分配
。
优先级高的任务(如键盘输入)可以打断优先级低的任务(如Diff)的执行,从而更快的生效。任务的优先级有六种
synchronous,与之前的Stack Reconciler操作一样,同步执行
task,在next tick之前执行
animation,下一帧之前执行
high,在不久的将来立即执行
low,稍微延迟执行也没关系
offscreen,下一次render时或scroll时才执行
Fiber Reconciler执行过程分为2个阶段:
- 阶段一:Reconciliation 生成 Fiber 树,得出需要更新的节点信息。这一步是一个渐进的过程,可以被打断。阶段一可被打断的特性,让优先级更高的任务先执行,从框架层面大大降低了页面掉帧的概率
- 阶段二:Commit 将需要更新的节点一次过批量更新,这个过程不能被打断
# React 生命周期
安装 当组件的实例被创建并插入到 DOM 中时,这些方法按以下顺序调用:
constructor()
static getDerivedStateFromProps()
render()
componentDidMount()
更新中 更新可能由道具或状态的更改引起。当重新渲染组件时,这些方法按以下顺序调用:
static getDerivedStateFromProps()
shouldComponentUpdate()
render()
getSnapshotBeforeUpdate()
componentDidUpdate()
卸载 当组件从 DOM 中移除时调用此方法:
componentWillUnmount()
# React的生命周期 react 16
React 通常将组件生命周期分为三个阶段:
- 装载阶段(Mount),组件第一次在DOM树中被渲染的过程
- 更新过程(Update),组件状态发生变化,重新更新渲染的过程;
- 卸载过程(Unmount),组件从DOM树中被移除的过程;
Mount:
- constructor
- getDerivedStateFromProps: 静态方法
- render
- componentDidMount: 发请求
Update:
- getDerivedStateFromProps
- shouldComponentUpdate
- getSnapshotBeforeUpdate:
getSnapshotBeforeUpdate(prevProps, prevState)
- componentDidUpdate
UnMount:
- componentWillUnmount
除此之外
- componentDidCatch
# React Hooks
背景: 为什么有Hooks?
- 组件复用的几个模式的问题: mixin -> HOC -> render props -> class 组件/function 组件
类组件是使用ES6 的 class 来定义的组件。 函数组件是接收一个单一的 props 对象并返回一个React元素。关于React的两套API(类(class)API 和基于函数的钩子(hooks) API)。官方推荐使用钩子(函数),而不是类。因为钩子更简洁,代码量少,用起来比较"轻",而类比较"重"。而且,钩子是函数,更符合 React 函数式的本质。
类组件的缺点:
- 大型组件很难拆分和重构,也很难测试。
- 业务逻辑分散在组件的各个方法之中,导致重复逻辑或关联逻辑。
- 组件类引入了复杂的编程模式,比如 render props 和高阶组件。
- 难以理解的 class,理解 JavaScript 中 this 的工作方式
Hooks 解决了哪些问题
- 在组件之间复用状态逻辑很难
- 复杂组件变得难以理解
- 难以理解的 class
# React Hooks的底层原理 ??
双向链表?
# React Hooks的使用限制
- hook只能在组件顶层使用,不可在分支语句中使用
- 不要在循环、条件或嵌套函数中调用hook
- 不能在类组件中使用hook
# 常见的Hooks
- useState
- useContext
- useReducer
- useEffect
- useLayoutEffect
- useRef
- useMemo
- useCallback
# useEffect 与 useLayoutEffect 的区别
- useEffect 在 React 的渲染过程中是被
异步调用
的,用于绝大多数场景; - useLayoutEffect 会在所有的 DOM 变更之后
同步调用
,主要用于处理 DOM 操作
、调整样式
、避免页面闪烁
等问题。也正因为是同步处理,所以需要避免在 useLayoutEffect 做计算量较大的耗时任务从而造成阻塞 - useEffect 的执行时机是浏览器完成渲染之后,而 useLayoutEffect 的执行时机是浏览器把内容真正渲染到界面之前,和 componentDidMount 等价
useLayoutEffect总是比useEffect先执行。
- 优先使用 useEffect,因为它是异步执行的,不会阻塞渲染
- 会影响到渲染的操作尽量放到 useLayoutEffect中去,避免出现闪烁问题
- useLayoutEffect和componentDidMount是等价的,会同步调用,阻塞渲染
- 在服务端渲染的时候使用useLayoutEffect会有一个 warning,因为它可能导致首屏实际内容和服务端渲染出来的内容不一致。
# React 事件原理
React并不是将click事件绑在该div的真实DOM上,而是在document
处监听所有支持的事件,当事件发生并冒泡至document处时,React将事件内容封装并交由真正的处理函数运行。这样的方式不仅减少了内存消耗,还能在组件挂载销毁时统一订阅和移除事件
另外冒泡到 document 上的事件也不是原生浏览器事件,而是 React 自己实现的合成事件SyntheticEvent
。TODO:// ??
因此我们如果不想要事件冒泡的话,调用 event.stopPropagation
是无效的,而应该调用 event.preventDefault
实现合成事件的目的如下:
合成事件首先抹平了浏览器之间的兼容问题,另外这是一个跨浏览器原生事件包装器,赋予了跨浏览器开发的能力;
对于原生浏览器事件来说,浏览器会给监听器创建一个事件对象。如果你有很多的事件监听,那么就需要分配很多的事件对象,造成高额的内存分配问题。但是对于合成事件来说,有一个事件池专门来管理它们的创建和销毁,当事件需要被使用时,就会从池子中复用对象,事件回调结束后,就会销毁事件对象上的属性,从而便于下次复用事件对象
e.stopPropagation → 用来阻止 React 模拟的事件冒泡
e.stopImmediatePropagation → 没有这个函数
e.nativeEvent.stopPropagation → 原生事件对象的用于阻止 DOM 事件的进一步捕获或者冒泡
e.nativeEvent.stopImmediatePropagation → 原生事件对象的用于阻止 DOM 事件的进一步捕获或者冒泡,且该元素的后续绑定的相同事件类型的事件也被一并阻止。
# React的优化方法
- useEffect 参数
- 添加key
- PureComponent/useMemo
- useCallBack
- 避免跨层级节点的移动
# setState是同步还是异步的
setState之后会发生什么?
在代码中调用setState函数之后,React 会将传入的参数对象与组件当前的状态合并,然后触发调和过程 Reconciliation
。经过调和过程,React 会以相对高效的方式根据新的状态构建 React 元素树并且着手重新渲染整个UI界面。
在 React 得到元素树之后,React 会自动计算出新的树与老树的节点差异,然后根据差异对界面进行最小化重渲染。在差异计算算法中,React 能够相对精确地知道哪些位置发生了改变以及应该如何改变,这就保证了按需更新,而不是全部重新渲染。
如果在短时间内频繁setState。React会将state的改变压入栈中,在合适的时机,批量更新state和视图,达到提高性能的效果
同步还是异步?
setState 并不是单纯同步/异步的,它的表现会因调用场景的不同而不同。在源码中,通过isBatchingUpdates(任务锁)
来判断setState 是先存进 state 队列还是直接更新,如果值为 true 则执行异步操作,为 false 则直接更新
异步: 在 React 可以控制的地方,就为 true,比如在 React 生命周期事件和合成事件中,都会走合并操作,延迟更新的策略。 同步: 在 React 无法控制的地方,比如原生事件,具体就是在 addEventListener 、setTimeout、setInterval 等事件中,就只能同步更新
setState设计为异步,可以显著的提升性能。如果每次调用 setState都进行一次更新,那么意味着render函数会被频繁调用,界面重新渲染,这样效率是很低的;最好的办法应该是获取到多个更新,之后进行批量更新
# React 与 Vue的对比 - 重点
数据流
- react主张函数式编程,所以推崇纯组件,数据不可变,单向数据流。
- vue的思想是响应式的,也就是基于是数据可变的,通过对每一个属性建立Watcher来监听,当属性变化的时候,响应式的更新对应的虚拟dom。
监听数据变化实现原理
- Vue 通过 getter/setter 以及一些函数的劫持,能精确知道数据变化,会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树,不需要特别的优化就能达到很好的性能
- React 默认是通过比较引用的方式进行的,如果不优化(PureComponent/shouldComponentUpdate)可能导致大量不必要的VDOM的重新渲染
组件化 React与Vue最大的不同是模板的编写
- Vue鼓励写近似常规HTML的模板。写起来很接近标准 HTML元素,只是多了一些属性
- React推荐你所有的模板通用JavaScript的语法扩展 —— JSX书写
# React的事件和普通的HTML事件有什么不同
区别:
- 对于事件名称命名方式,原生事件为全小写,react 事件采用小驼峰;
- 对于事件函数处理语法,原生事件为字符串,react 事件为函数;
- react 事件不能采用
return false
的方式来阻止浏览器的默认行为,而必须要地明确地调用preventDefault()
来阻止默认行为
事件的执行顺序为原生事件先执行,合成事件后执行,合成事件会冒泡绑定到 document
上,所以尽量避免原生事件与合成事件混用,如果原生事件阻止冒泡,可能会导致合成事件不执行,因为需要冒泡到document 上合成事件才会执行。
# React 组件中怎么做事件代理?它的原理是什么?
React基于Virtual DOM实现了一个SyntheticEvent层(合成事件层)
,定义的事件处理器会接收到一个合成事件对象的实例,它符合W3C标准,且与原生的浏览器事件拥有同样的接口,支持冒泡机制,所有的事件都自动绑定在最外层上
在React底层,主要对合成事件做了两件事:
- 事件委派: React会把所有的事件绑定到结构的最外层,使用统一的事件监听器,这个事件监听器上维持了一个映射来保存所有组件内部事件监听和处理函数。
- 自动绑定: React组件中,每个方法的上下文都会指向该组件的实例,即自动绑定this为当前组件
# React 高阶组件、Render props、hooks 有什么区别
高阶组件 高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。
// hoc的定义
function withSubscription(WrappedComponent, selectData) {
return class extends React.Component {
constructor(props) {
super(props);
this.state = {
data: selectData(DataSource, props)
};
}
// 一些通用的逻辑处理
render() {
// ... 并使用新数据渲染被包装的组件!
return <WrappedComponent data={this.state.data} {...this.props} />;
}
};
// 使用
const BlogPostWithSubscription = withSubscription(BlogPost,
(DataSource, props) => DataSource.getBlogPost(props.id));
HOC的优缺点∶
- 优点∶ 逻辑服用、不影响被包裹组件的内部逻辑。
- 缺点∶ hoc传递给被包裹组件的props容易和被包裹后的组件重名,进而被覆盖
render props
"render props" 是指一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术
具有render prop 的组件接受一个返回React元素的函数,将render的渲染逻辑注入到组件内部。在这里,"render"的命名可以是任何其他有效的标识符。
// DataProvider组件内部的渲染逻辑如下
class DataProvider extends React.Components {
state = {
name: 'Tom'
}
render() {
return (
<div>
<p>共享数据组件自己内部的渲染逻辑</p>
{ this.props.render(this.state) }
</div>
);
}
}
// 调用方式
<DataProvider render={data => (
<h1>Hello {data.name}</h1>
)}/>
由此可以看到,render props的优缺点也很明显∶
- 优点:数据共享、代码复用,将组件内的state作为props传递给调用者,将渲染逻辑交给调用者。
- 缺点:无法在 return 语句外访问数据、嵌套写法不够优雅
Hooks
Hook是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。通过自定义hook,可以复用代码逻辑。
hook解决了hoc的prop覆盖的问题,同时使用的方式解决了render props的嵌套地狱的问题。hook的优点如下∶
- 使用直观,简单,像函数调用一样且存有内部的状态;
- 解决hoc的prop 重名问题;
- 解决render props 因共享数据 而出现嵌套地狱的问题;
- 能在return之外使用数据的问题
# React.Component 和 React.PureComponent 的区别
PureComponent表示一个纯组件,可以用来优化React程序,减少render函数执行的次数,从而提高组件的性能。
在React中,当prop或者state发生变化时,可以通过在shouldComponentUpdate生命周期函数中执行return false来阻止页面的更新,从而减少不必要的render执行。React.PureComponent会自动执行 shouldComponentUpdate,但是进行的是浅比较
# React Portals
Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案
ReactDOM.createPortal(child, container);
# React中什么是受控组件和非控组件?
受控:授react state的状态值控制,同步更改 非受控:状态值由dom节点保存,通过ref等方式获取
# React 组件通信
- 父组件向子组件通信:父组件通过 props 向子组件传递需要的信息
- 子组件向父组件通信:props+回调的方式
- 跨级组件通信:多层props或者context
- 非嵌套关系的组件通信:可以使用自定义事件通信(发布订阅模式)或者 全局状态管理 (Redux/Mobx)
# React Router的实现原理是什么
两种模式:
- hash
- history
基于 hash 的路由
通过监听hashchange
事件,感知 hash 的变化,改变 hash 可以直接通过 location.hash=xxx
基于 H5 history 路由
改变 url 可以通过 history.pushState
和 history.replaceState
等,会将URL压入堆栈,同时能够应用 history.go() 等 API
react-router 实现的思想:
基于 history 库来实现上述不同的客户端路由实现思想,并且能够保存历史记录等,磨平浏览器差异,上层无感知。通过维护的列表,在每次 URL 发生变化的回收,通过配置的 路由路径,匹配到对应的 Component,并且 render
# React 18 新功能
3 大新特性
- Automatic batching 自动 Batching:解决
回调函数
中的 setState 异步合并渲染的问题 - Concurrent APIS:可中断渲染设计架构
- startTransition
- useDeferredValue
- SSR for Suspense
- pipeToNodeWritable
# Redux 是什么
Redux 是一个状态机,提供了一个叫store
的数据管理的能力,组件通过 dispatch 将 state 直接传入store,不用通过其他的组件。并且组件通过 subscribe 从 store获取到 state 的改变。使用了 Redux,所有的组件都可以从 store 中获取到所需的 state,他们也能从store 获取到 state 的改变。这比组件之间互相传递数据清晰明朗的多。同时结合react-redux
可以在组件维度上订阅到这些状态的变化,自动的更新UI。
action
一个JavaScript对象,描述动作相关信息,主要包含type属性和payload属性∶
{
type∶ action 类型;
payload∶ 负载数据
}
reducer 定义应用状态如何响应不同动作(action),如何更新状态;
store 管理action和reducer及其关系的对象,主要提供以下功能
- 维护应用状态并支持访问状态(getState())
- 支持监听action的分发,更新状态(dispatch(action))
- 支持订阅store的变更(subscribe(listener))
# Redux的工作原理
- compose.js 提供从右到左进行函数式编程
- createStore.js 提供作为生成唯一store的函数
- combineReducers.js 提供合并多个reducer的函数,保证store的唯一性
- bindActionCreators.js 可以让开发者在不直接接触dispatch的前提下进行更改state的操作
- applyMiddleware.js 这个方法通过中间件来增强dispatch的功能
- 用于触发Action,发出方式就用到了dispatch方法
- Store自动调用Reducer,并且传入两个参数:当前State和收到的Action,Reducer会返回新的State
- State—旦有变化,Store就会调用监听函数,来更新View
# Redux 中异步的请求怎么处理
使用redux-thunk
或 redux-saga
import {createStore, applyMiddleware, compose} from 'redux';
import reducer from './reducer';
import thunk from 'redux-thunk'
// 设置调试工具
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose;
// 设置中间件
const enhancer = composeEnhancers(
applyMiddleware(thunk)
);
const store = createStore(reducer, enhancer);
export default store;
# Redux 中间件是什么?接受几个参数?柯里化函数两端的参数具体是什么?
Redux 的中间件提供的是位于 action 被发起之后,到达 reducer 之前的扩展点,换而言之,原本 view -> action -> reducer -> store 的数据流加上中间件后变成了 view -> action -> middleware -> reducer -> store ,在这一环节可以做一些"副作用"的操作,如异步请求、打印日志等
//applyMiddleware.js
export default function applyMiddleware(...middlewares) {
return createStore => (...args) => {
// 利用传入的createStore和reducer和创建一个store
const store = createStore(...args)
let dispatch = () => {
throw new Error()
}
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
// 让每个 middleware 带着 middlewareAPI 这个参数分别执行一遍
const chain = middlewares.map(middleware => middleware(middlewareAPI))
// 接着 compose 将 chain 中的所有匿名函数,组装成一个新的函数,即新的 dispatch
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
# mobx 和 redux 有什么区别
# 共同点
- 为了解决状态管理混乱,无法有效同步的问题统一维护管理应用状态;
- 某一状态只有一个可信数据来源(通常命名为store,指状态容器);
- 操作更新状态方式统一,并且可控(通常以action方式提供更新状态的途径);
- 支持将store与React组件连接,如
react-redux
,mobx-react
;
# 区别点
Redux更多的是遵循Flux模式的一种实现,是一个 JavaScript库
- Action∶ 一个JavaScript对象,描述动作相关信息,主要包含type属性和payload属性
- Reducer∶ 定义应用状态如何响应不同动作(action),如何更新状态
- Store∶ 管理action和reducer及其关系的对象
Mobx是一个透明函数响应式编程
的状态管理库,它使得状态管理简单可伸缩∶
- Action∶ 定义改变状态的动作函数,包括如何变更状态;
- Store: 集中管理模块状态(State)和动作(action)
- Derivation: 从应用状态中派生而出,且没有任何其他影响的数据
对比:
- redux将数据保存在单一的store中,mobx将数据保存在分散的多个store中
- redux使用
plain object
保存数据,需要手动处理变化后的操作; mobx适用observable保存数据,数据变化后自动处理响应的操作 - mobx相对来说比较简单,在其中有很多的抽象,mobx更多的使用面向对象的编程思维; redux会比较复杂,因为其中的
函数式编程思想
掌握起来不是那么容易,同时需要借助一系列的中间件来处理异步和副作用 - mobx中有更多的抽象和封装,调试会比较困难,同时结果也难以预测; 而redux提供能够进行时间回溯的开发工具,同时其纯函数以及更少的抽象,让调试变得更加的容易
# Redux 中间件是怎么拿到 store 和 action? 然后怎么处理?
redux中间件本质就是一个函数柯里化。redux的applyMiddleware Api 源码中每个middleware 接受2个参数,Store 的getState 函数和dispatch 函数,分别获得store和action,最终返回一个函数。该函数会被传入 next 的下一个 middleware 的 dispatch 方法,并返回一个接收 action 的新函数,这个函数可以直接调用 next(action),或者在其他需要的时刻调用,甚至根本不去调用它。调用链中最后一个 middleware 会接受真实的 store的 dispatch 方法作为 next 参数,并借此结束调用链。所以,middleware 的函数签名是({ getState,dispatch })=> next => action
# Redux中的connect有什么作用
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及其子组件的重新渲染
← 网络 Typescript →