React
React
React setState 异步同步
- 在 setTimeout、Promise 等原生事件 API 调用中
- setState 和 useState 是同步执行的,立即执行 render
- Class Component 能获取到最新值 => this.state => 引用类型
- Function Component 不能获取到最新值 => 只能得到之前的值 => 闭包
- 多次执行 setState 和 useState,每一次执行都会调用一次 render
- setState 和 useState 是同步执行的,立即执行 render
- 在 React 合成事件中
- setState 和 useState 是异步执行的,不会立即执行 render,都不能立即获取到更新后的值
- 多次执行 setState 和 useState,只会调用一次 render
- setState 可能会进行 state 合并
- 传入一个对象,会进行 state 合并 => +1
- 传入一个函数,则不会进行 state 合并 => +2
- useState 不会进行 state 合并
React Fiber
在 Fiber 出现之前 React 的问题
- 在 Fiber 出现之前对比更新虚拟 DOM 使用递归加循环,这种对比方式有一个问题,就是任务一旦开始则无法中断
- 如果应用组件过多,层级较深,那么主线程将会被一直占用
- 这会导致一些用户操作或者动画无法立即得到执行,页面会产生卡顿
- 总结 => 递归无法中断,执行任务耗时长,JavaScript 是单线程的,比较虚拟 DOM 过程中无法执行其他任务,导致任务延迟页面卡顿
Fiber 架构思路
React16版本的架构可以分为三层,调度层,协调层,渲染层。
Scheduler调度层
react15的版本中,采用了循环加递归的方式进行了vdom的比对,由于递归使用js自身的执行栈,一旦开始就无法停止。直到任务执行完成。如果dom树的层级较深,就容易出现长期占用js主线程,导致gui渲染线程无法得到工作,造成页面卡顿。
在16的版本中,放弃了js递归方式进行vdom比对,而是采用了循环模拟递归,而且比对的过程是利用浏览器的空闲时间完成的,不会占用主线程,这就解决了vdom对比造成页面卡顿原因。
在window中提供了requestIdCallback的api,它可以利用浏览器空闲的时间执行任务,但是他自设能触发频率不稳定,并且不是所有浏览器都支持他。
react自己实现了任务调度库,叫做Scheduler,如果浏览器支持postMessage,那么他就会采用postMessage来进行调度,不支持再使用setTimoute,setTimeout的缺点是连续调用setTimeout(()=>{},0),最后会发现他的触发频率变成了4ms一次。并且Scheduler还采用了小顶堆算法,实现了任务优先级的概念。
Reconciler协调层
react15的版本中,协调器和渲染器交替执行,找到了差异就更新差异,这也是15无法中断的原因,因为会造成页面渲染不完全。
react16中,则是Scheduler和Reconciler交替工作,Scheduler负责调度,Reconciler负责找出差异,打上标记。等所有差异找完之后,才会交给Renderer统一进行DOM更新。这也是为什么react16可以实现可中断的异步更新的原因。
Renderer渲染层
渲染层工作的时候,是同步的,也就是无法中断的。可中断的异步更新的概念是描述Scheduler和Reconciler,他们是在内存中完成的。而Renderer是无法被中断的。
既然无法被中段,那么就不会出现dom渲染不完全的情况,因为渲染器的工作是一气呵成的,从0到1。
Fiber 节点
type Fiber = {
// 用于标记fiber的WorkTag类型,主要表示当前fiber代表的组件类型如FunctionComponent、ClassComponent等
tag: WorkTag,
// ReactElement里面的key
key: null | string,
// ReactElement.type,调用`createElement`的第一个参数
elementType: any,
// The resolved function/class/ associated with this fiber.
// 表示当前代表的节点类型
type: any,
// 表示当前FiberNode对应的element组件实例
stateNode: any,
// 指向他在Fiber节点树中的`parent`,用来在处理完这个节点之后向上返回
return: Fiber | null,
// 指向自己的第一个子节点
child: Fiber | null,
// 指向自己的兄弟结构,兄弟节点的return指向同一个父节点
sibling: Fiber | null,
index: number,
ref: null | (((handle: mixed) => void) & { _stringRef: ?string }) | RefObject,
// 当前处理过程中的组件props对象
pendingProps: any,
// 上一次渲染完成之后的props
memoizedProps: any,
// 该Fiber对应的组件产生的Update会存放在这个队列里面
updateQueue: UpdateQueue<any> | null,
// 上一次渲染的时候的state
memoizedState: any,
// 一个列表,存放这个Fiber依赖的context
firstContextDependency: ContextDependency<mixed> | null,
mode: TypeOfMode,
// Effect
// 用来记录Side Effect
effectTag: SideEffectTag,
// 单链表用来快速查找下一个side effect
nextEffect: Fiber | null,
// 子树中第一个side effect
firstEffect: Fiber | null,
// 子树中最后一个side effect
lastEffect: Fiber | null,
// 代表任务在未来的哪个时间点应该被完成,之后版本改名为 lanes
expirationTime: ExpirationTime,
// 快速确定子树中是否有不在等待的变化
childExpirationTime: ExpirationTime,
// fiber的版本池,即记录fiber更新过程,便于恢复
alternate: Fiber | null,
}
双缓存技术介绍
在react中,DOM得更新采用了双缓存技术,双缓存技术致力于更快速得DOM更新。
举个例子,canvas绘制动画的时候,绘制每一帧动画之前就需要清除上一帧得动画,如果当前帧动画计算较长,就会导致出现替换白屏出现。为了解决这个问题,可以现在内存中构建当前帧动画,绘制完毕后直接替换上一帧,这样就不会出现白屏问题,这种在内存中构建并直接替换的技术叫做双缓存。
React使用双缓存技术完成Fiber树的构建和替换,实现DOM的快速更新。
在react中最多同时存在两颗fiber树,当前屏幕显示的是current Fiber,发生更新的时候,react在内存中构建workInporgress fiber。并且在两颗中之间对应的fiber节点有一个alternate指针,通过这个指针,workInporgress的构建就可以最大化的复用current fiber。进而更快速的构建玩workInporgress fiber。
useState是怎么实现的
// React Hooks模拟 // 用于保存组件是update还是mount let isMount = true; // 保存我们当前正在处理的hook let workInProgressHook = null; // React 中每一个组件对应一个fiber节点 const fiber = { // 保存组件本身 stateNode: App, // 对于函数式组件,保存的是hooks的链表,对于类组件保存的是state // 问: Hooks信息为什么要使用链表存储而不是数组 memoizedState: null, } // 用于调度组件执行 function schedule() { // 每次执行组件的时候,需要将workInProgressHook复位,重新指向第一个hook workInProgressHook = fiber.memoizedState; const app = fiber.stateNode(); isMount = false; return app; } /** * useState实现 */ function useState(initialState) { let hook; if (isMount) { // 首次渲染的时候需要初始化一个hook hook = { // hook上的memoizedState保存的是hook的状态 memoizedState: initialState, // 指向下一个hook next: null, // 保存hook的更新信息 比如 setNum(n => n+1)中的n => n+1函数,为什么事一个队列,因为我们可以在一次操作中调用多次setNum(n => n+1)函数 // 比如说在onClick事件中 queue: { pending: null } } // 表示是第一个hook // 这就是为什么我们执行多个useState他们之间可以相互对应的原因 if (!fiber.memoizedState) { fiber.memoizedState = hook; } else { workInProgressHook.next = hook; } workInProgressHook = hook; // 指针继续向下移动 } else { // 表示已经有了一条链表 // 此时workInProgressHook指向这个链表的正在执行的hook // 获取到我们当前要执行的hook hook = workInProgressHook; workInProgressHook = workInProgressHook.next; } console.log(hook) // 找到计算state依据的基础的state let baseState = hook.memoizedState; // 说明本次的状态改变有update要执行 if (hook.queue.pending) { let firstUpdate = hook.queue.pending.next; // 遍历链表 do { const action = firstUpdate.action; // setState传入的值这里是函数 // 更新基础的state baseState = action(baseState); firstUpdate = firstUpdate.next; } while (firstUpdate !== hook.queue.pending.next) // 状态计算完成之后需要将hook.queue.pending = null hook.queue.pending = null; } hook.memoizedState = baseState; return [baseState, dispatchAction.bind(null, hook.queue)] } // state的update函数在React中有自己的名字,叫dispatchAction function dispatchAction(queue, action) { // 那么我们如何知道dispatchAction是来更新哪一个state呢? // 创建一个数据结构,这个数据结构就记录着一次更新, 也是一个链表的形式 // hook的链表是一个单向链表,但是update的链表却是一个环形链表。因为React中的update是有优先级的 const update = { action, next: null } // 说明当前hook上还有要触发的更新,说明这个update是我们要触发的第一次更新 if (queue.pending === null) { update.next = update; } else { // 不是第一次更新 // queue.pending保存的是hook的最后一个update // queue.pending.next则就是这个环的第一个update,所以queue.pending也是一个环形链表 update.next = queue.pending.next; queue.pending.next = update; } // 移动指针 queue.pending = update; // 组件执行 schedule(); } function App() { const [num, setNum] = useState(0); const [num1, setNum1] = useState(10); console.log(isMount); console.log(num); console.log(num1); return { onClick() { setNum(n => n + 1) }, onFocus() { setNum1(n => n + 10) } } } window.app = schedule();
React 合成事件
React 所有事件都委托在root 对象上;
• 当真实 DOM 元素触发事件,会冒泡到 root 对象后,再处理 React 事件;
• 在捕获阶段,先注册的先执行,且React合成事件先于原生事件执行;冒泡阶段,先注册的后执行,且原生事件先于React事件执行
合成事件和原生事件区别
React 合成事件 | 原生事件 | |
---|---|---|
命名 | 小驼峰(onClick) | 纯小写(onclick) |
事件处理函数 | 函数 | 字符串 |
阻止默认行为 | event.preventDefault() | return false |
虚拟 DOM 的原理是什么?
- 虚拟 DOM 就是虚拟节点。React 使用 JS 对象来模拟 DOM 节点,之后将其渲染成真实的 DOM 节点
- 使用 JSX 语法写出来的 div 其实就是一个虚拟节点
<div className="container"> <span className="red">hi</span> </div>
- 上面的代码其实是调用了 React.createElement 函数,之后生成了一个 JS 对象
{ tag: 'div', props: {className: 'container'}, children: [ { tag: 'span', props: { className: 'red' }, children: [ 'hi' ] } ] }
- 之后会根据 JS 对象信息即虚拟节点渲染为真实节点
- 如果节点发生改变,并不会把新的虚拟节点直接重新渲染为真实节点,而是要先经过 diff 算法得到一个 patch 再更新到真实节点上
- 虚拟 DOM 大大提升了 DOM 操作性能问题。通过虚拟 DOM 和 diff 算法减少不必要的 DOM 操作,保证性能。
- 之前 DOM 操作并不是很方便,但是现在只需要 setState 即可
- 但是 React 为虚拟 DOM 创造了合成事件,和原生 DOM 事件不太一样。所有 React 事件都绑定到了根元素,自动实现事件委托,如果混用合成事件和原生 DOM 事件,可能会出现 bug
React DOM diff 算法
- DOM diff 就是对比两颗虚拟 DOM 树的算法
- 当组件变化时,会 render 出一个新的虚拟 DOM,diff 算法对比新旧虚拟 DOM 之后,得到一个 patch,然后 React 用 patch 来更新真实 DOM
- 首先对比两棵树的根节点然后同时遍历两棵树的子节点,每个节点的对比过程如上
- 如果根节点类型改变了,比如 div 变为了 p,那么直接认为整颗树都变了,不再对比子节点。此时直接删除对应的真实 DOM 树,创建新的真实 DOM 树
- 如果根节点类型没有改变,就看看属性变了没有
- 属性没变,保留对应的真实节点
- 属性变了,就只更新该节点的属性,不重新创建节点 => case: 更新 style 时,如果多个 css 属性只有一个改变了,那么 React 只更新改变的
- case1:React 依次对比 A-A,B-B,空-C,发现 C 是新增的,最终会创建真实 C 节点插入页面
<ul> <li>A</li> <li>B</li> </ul> // updated <ul> <li>A</li> <li>B</li> <li>C</li> </ul>
- case2: React 对比 B-A,删除 B 节点新建 A 节点;对比 C-B,删除 C 节点新建 B 节点(注意:并不是边对比边删除新建,而是把操作汇总到 patch 里在进行 DOM 操作,会进行标记);对比 空-C,新建 C 节点
<ul> <li>B</li> <li>C</li> </ul> // updated <ul> <li>A</li> <li>B</li> <li>C</li> </ul>
- case2 其实只需要创建 A 节点,保留 B C 节点即可,此时 React 需要你添加 key
<ul> <li key="b">B</li> <li key="c">C</li> </ul> // updated <ul> <li key="a">A</li> <li key="b">B</li> <li key="c">C</li> </ul>
- 此时 React 先对比 key,发现 key 增加了 a,此时保留 B C,新建 A 节点
Vue Dom diff
Vue 双端交叉对比
- 头头对比 => 对比两个数组的头部,如果找到,把新节点 patch 到旧节点,头指针后移
- 尾尾对比 => 对比两个数组的尾部,如果找到,把新节点 patch 到旧节点,尾指针前移
- 旧尾新头对比 => 交叉对比,旧尾新头,如果找到,把新节点 patch 到旧节点,旧尾指针前移,新头指针后移
- 旧头新尾对比 => 交叉对比,旧头新尾,如果找到,把新节点 patch 到旧节点,新尾指针前移,旧头指针后移
- 利用 Key 对比 => 用新指针对应节点的 key 去旧数组中寻找对应的节点
- 没有对应的 key => 创建新节点
- 有 key 并且是相同的节点 => 把新节点 patch 到旧节点
- 有 key 但是不是相同的节点 => 创建新节点
React 有哪些声明周期钩子函数?数据请求放在哪个钩子里?
- 挂载时调用 constructor,更新时不调用
- 更新时调用 shouldComponentUpdate 和 getSnapshotBeforeUpdate,挂载时不调用
- shouldComponentUpdate 在 render 前调用,getSnapshotBeforeUpdate 在 render 后调用
- 请求放置在 componentDidMount 里
React 如何实现组件间通信
- 父子组件通信 => props + 函数
- 爷孙组件通信 => 两层父子通信 | Context.Provider 和 Context.Consumer
- 任意组件通信 => 状态管理 => Redux | Mobx
如何理解 Redux
- Redux 就是一个状态管理库,可以实现任意组件之间的通信,其实就是将信息放置在顶部,如有需要其余组件去顶部获取即可
- Redux 核心概念
- store => 信息/状态存放的地方
- action => 可以更改 state 的唯一途径,根据 type 和 payload 进行更改 state
- dispatch => 用于派发事件
- reducer => 就是一个函数,传递给 reducer 一个旧的 state + action,他会返回一个新的 state
- Middleware => 中间件
- Redux 常与 ReactRedux 联合使用,ReactRedux 提供了以下 Api
- connect()(Component)
- mapStateToProps
- mapDispatchToProps
- Redux 常用中间件:
- redux-thunk => 扩展 redux 可以支持异步 action,如果 action 是一个函数,就直接调用它,否则就进入下一个中间件
- redux-promise => 如果 payload 是一个 Promise,就执行个这个 Promise,并在 then 里面去 dispatch
redux,react-redux,redux-saga,dva的区别与联系
【redux】
1、定位:它是将flux和函数式编程思想结合在一起形成的架构;
2、思想:视图与状态是一一对应的;所有的状态,都保存在一个对象里面;
3、API:
store:就是一个数据池,一个应用只有一个;
state:一个 State 对应一个 View。只要 State 相同,View 就相同。
action:State 的变化,会导致 View 的变化。但是,用户接触不到 State,只能接触到 View。所以,State 的变化必须是 View 导致的。Action 就是 View 发出的通知,表示 State 应该要发生变化了。Action 是一个对象。其中的type属性是必须的,表示 Action 的名称。其他属性可以自由设置。
dispatch:它是view发出action的唯一方法;
reducer:view发出action后,state要发生变化,reducer就是改变state的处理层,它接收action和state,通过处理action来返回新的state;
subscribe:监听。监听state,state变化view随之改变;
【react-redux】
1、定位:react-redux是为了让redux更好的适用于react而生的一个库,使用这个库,要遵循一些规范;
2、主要内容
UI组件:就像一个纯函数,没有state,不做数据处理,只关注view,长什么样子完全取决于接收了什么参数(props)
容器组件:关注行为派发和数据梳理,把处理好的数据交给UI组件呈现;React-Redux规定,所有的UI组件都由用户提供,容器组件则是由React-Redux自动生成。也就是说,用户负责视觉层,状态管理则是全部交给它。
connect:这个方法可以从UI组件生成容器组件;但容器组件的定位是处理数据、响应行为,因此,需要对UI组件添加额外的东西,即mapStateToProps和mapDispatchToProps,也就是在组件外加了一层state;
mapStateToProps:一个函数, 建立一个从(外部的)state对象到(UI组件的)props对象的映射关系。 它返回了一个拥有键值对的对象;
mapDispatchToProps:用来建立UI组件的参数到store.dispatch方法的映射。 它定义了哪些用户的操作应该当作动作,它可以是一个函数,也可以是一个对象。
以上,redux的出现已经可以使react建立起一个大型应用,而且能够很好的管理状态、组织代码,但是有个棘手的问题没有很好地解决,那就是异步;在react-redux中一般是引入middleware中间件来处理,redux-thunk
【redux-saga】:
1、定位:react中间件;旨在于更好、更易地解决异步操作(有副作用的action),不需要像在react-redux上还要额外引入redux-thunk;redux-saga相当于在Redux原有数据流中多了一层,对Action进行监听,捕获到监听的Action后可以派生一个新的任务对state进行维护;
2、特点:通过 Generator 函数来创建,可以用同步的方式写异步的代码;
3、API:
Effect: 一个简单的对象,这个对象包含了一些给 middleware 解释执行的信息。所有的Effect 都必须被 yield 才会执行。
put:触发某个action,作用和dispatch相同;
【dva】
1、定位:dva 首先是一个基于redux-saga 的数据流方案,然后为了简化开发体验,dva 还额外内置了 react-router 和 fetch,所以也可以理解为一个轻量级的应用框架。dva = React-Router + Redux + Redux-saga;
2、核心:
State:一个对象,保存整个应用状态;
View:React 组件构成的视图层;
Action:一个对象,描述事件(包括type、payload)
connect 方法:一个函数,绑定 State 到 View
dispatch 方法:一个函数,发送 Action 到 State
3、model:dva 提供 app.model 这个对象,所有的应用逻辑都定义在它上面。
4、model内容:
namespace:model的命名空间;整个应用的 State,由多个小的 Model 的 State 以 namespace 为 key 合成;
state:该命名空间下的数据池;
effects:副作用处理函数;
reducers:等同于 redux 里的 reducer,接收 action,同步更新 state; subscriptions:订阅信息;
dva 是基于现有应用架构 (redux + react-router + redux-saga 等)的一层轻量封装,没有引入任何新概念,全部代码不到 100 行。dva 实现上尽量不创建新语法,而是用依赖库本身的语法,比如 router 的定义还是用 react-router 的 JSX 语法的方式(dynamic config 是性能的考虑层面,之后会支持)。
他最核心的是提供了 app.model 方法,用于把 reducer, initialState, action, saga 封装到一起
什么是高阶组件 HOC
- 参数是组件,返回值也是组件的函数
- React.forwardRef
const FancyButton = React.forwardRef((props, ref) => { <button ref={ref} className="fancyButton"> {props.children} </button> }); // You can now get a ref directly to the DOM button const ref = React.createRef(); <FancyButton ref={ref}>Click me!</FancyButton>
- ReactRedux 的 connect
useEffrct 原理
// 重新渲染函数 function render() { stateIndex = 0; effectIndex = 0; ReactDOM.render(<App />, document.getElementById('root')); } // 存储上一次的依赖值 const preDepsAry = []; let effectIndex = 0; function useEffect(callback, depsAry) { // 判断callback是否是函数 if (Object.prototype.toString.call(depsAry) !== '[object Array]') { throw new Error('useEffect 函数的第二个参数必须是数组'); } else { // 获取上一次的状态值 const prevDeps = preDepsAry[effectIndex]; // 如果存在就去做对比,如果不存在就是第一次执行 // // 将当前的依赖值和上一次的依赖值做对比, every如果返回true就是没变化,如果false就是有变化 const hasChanged = prevDeps ? depsAry.every((dep, index) => dep === prevDeps[index]) === false : true; // 值如果有变化 if (hasChanged) { callback(); } // 同步依赖值 preDepsAry[effectIndex] = depsAry; // 累加 effectIndex++; } }