JS发展路径:模版字符串->数据驱动
react vue 对比
相同点:
1 都是 MVVM 前端开发的架构模式
2 提供了响应式 (Reactive) 和组件化 (Composable) 的视图组件
3 使用 Virtual DOM 更新视图,react 是环形链表,Vue 是 rectivity VDOM
4 都支持跨平台渲染
5 只专注于核心库的开发和维护,将路由、全局状态管理、UI 组件、打包编译的脚手架等交给其他的库
不同点:
1 开发方式:react是JSX 更加灵活、Vue是模版化Templates,相对上手简单
2 生态:react生态更加丰富(国外),Vue也发展很快(国内)
3 都是单向数据流,但是Vue能够双向数据绑定
4 diff流程:react是从根节点向下调和(判断不可变数据),Vue是数据代理proxy(可变数据),能够直接更新组件
5 设计思想:react 偏原生(Runtime),Vue 偏抽象(权衡) (angular/serverlet 强编译时,很多api接口)
createElement
react.createElement 接受3个参数:type,[props],[...children]
组件本质上就是类和函数,承载UI和更新视图的setState、useState等方法
React在底层逻辑上处理的组件:实例化类和执行函数
对于class组件中,除了继承React.Component ,类组件执行构造函数过程中会在实例上绑定props和context,初始化置空refs属性,原型链上绑定update对象等
jsx代码通过react.createElement转换成react element对象后,最终在调和阶段,element对象的会形成与之对应的fiber对象,然后通过sibling、return、child形成单向循环链表关联起来
react/src/ReactBaseClasses.js
function Component(props, context, updater) {
this.props = props; //绑定props *****
this.context = context; //绑定context
this.refs = emptyObject; //绑定ref
this.updater = updater || ReactNoopUpdateQueue; //上面所属的 updater 对象
}
/* 绑定setState 方法 */
Component.prototype.setState = function(partialState, callback) {
this.updater.enqueueSetState(this, partialState, callback, 'setState');
}
/* 绑定forceupdate 方法 */
Component.prototype.forceUpdate = function(callback) {
this.updater.enqueueForceUpdate(this, callback, 'forceUpdate');
}
对于函数组件的执行,是在react-reconciler/src/ReactFiberHooks.js中
function renderWithHooks(
current, // 当前函数组件对应的 `fiber`, 初始化
workInProgress, // 当前正在工作的 fiber 对象
Component, // 我们函数组件
props, // 函数组件第一个参数 props
secondArg, // 函数组件其他参数
nextRenderExpirationTime, //下次渲染过期时间
){
/* 执行我们的函数组件,得到 return 返回的 React.element对象 */
let children = Component(props, secondArg);
}
组件间的通信方式
- 父子组件:props 和 callback 方式
- 跨层级组件:context上下文方式、ref 方式、event bus事件总线
- 全局组件:react-redux状态管理方式
受控组件 非受控组件
组件里的value受到props控制,用onChange回调函数处理value的改变并同步到state中,这样的组件就是受控组件。缺点是如果有很多个value,就需要写很多回调函数控制,代码比较臃肿。
非受控组件即value不与state关联,需要用到的时候,用ref直接去DOM中获取
setState 的执行流程*******
1 首先创建一个update对象和更新的优先级 expirationTime (新lane),放入当前的更新队列 enqueueUpdate 中
2 然后开始调度更新,从fiberRoot根节点向下调和子节点,调和阶段找到发生更新的组件,合并state,然后执行render函数,得到新的UI视图层,完成 render 阶段。
3 接着在commit阶段,替换真实的DOM,完成页面视图更新
4 最后检查setState中是否有callback函数需要执行,有就执行,没有就结束更新流程
setState 批量更新
react事件执行之前, 通过 isBatchingEventUpdates=true 打开开关,然后在scheduleUpdateOnFiber 中根据这个开关来确定是否进行批量更新
在promise、setTimeout异步事件中,可以跳出批量更新
不过也可以用 unstable_batchedUpdates,在异步函数中,也能批量更新
在实际工作中,unstable_batchedUpdates 可以用于多次Ajax数据交互之后,合并多次 setState,或者是多次useState
setState useState
相同点: setState和useState底层都是调用了scheduleUpdateOnFiber方法,都是批量更新
不同点:
1 useState会默认浅比较两次state是否相同,然后决定是否更新组件
2 setState有专门监听state变化的callback,可以获取最新state,
但在函数组件中,改变的state只有在下一次函数组件执行时才能拿到,可以通过useEffect来监听state变化引起的副作用
3 setState是新老state进行合并使用,而useState是重新创建再赋值
props 可以是什么
① props 作为一个子组件渲染数据源
② props 作为一个通知父组件的回调函数
③ props 作为一个单纯的组件传递
④ props 作为渲染函数
⑤ render props,和④的区别是放在了children属性上。
⑥ render component插槽组件
在标签内部的属性和方法会直接绑定在props对象的属性上,对于组件的插槽会被绑定在props的chidren属性中
生命周期
初始化流程
更新流程
卸载
getDerivedStateFromProps (nextProps, prevState)
在初始化和更新流程中,接受父组件的props数据,可以对props进行格式化,过滤等操作,返回值将作为新的state合并到state中
是静态属性方法,内部是访问不到 this 的,它更趋向于纯函数。
getDerivedStateFromProps 作用:
- 取代 componentWillMount 和 componentWillReceiveProps
- 组件初始化或者更新时,将 props 映射到 state 中
- 返回值与state合并完,newState可以作为shouldComponentUpdate第二个参数
UNSAFE_xxx
componentWillMount、componentWillReceiveProps、componentWillUpdate这三个生命周期,都是在render之前执行的,滥用这几个生命周期,可能导致生命周期内的上下文多次被执行,甚至可能发生死循环
ref
用createRef、useRef可以创建出一个ref原始对象,可以把一些没必要更新视图的数据储存到ref对象中,只要组件没有销毁,ref对象就一直存在,这样能够直接修改数据,不会触发重新渲染。
useEffect,useMemo引用useRef对象中的数据,无须将ref对象添加成dep依赖项,因为ref指向一个内存空间,可以 随时访问到变化后的值
ref 挂载位置
类组件有一个实例 instance 能够维护像 ref 的各种信息
函数组件 useRef 产生的ref对象挂到函数组件对应的 fiber 上 (没有实例对象)
ref 执行时机
时机:对Ref的处理,都是在commit阶段发生的。 commit阶段会进行真正的Dom操作,此时ref就是用来获取真实的DOM以及组件实例的。
逻辑:DOM更新之前 commitDetachRef, DOM 更新之后 commitAttachRef
Suspense 异步组件
原理: 调用 Render => 发现异步请求 => 渲染终止、捕获异常、等待异步请求完毕 => 拿到数据、再次渲染
优点: 拿到数据后再渲染 (少渲染一次)
错误边界
componentDidCatch() 会在commit阶段被调用,允许 再次触发 setState,来降级UI渲染,
static getDerivedStateFromError 是静态方法,内部不能调用 setState。它的返回的值可以合并到 state,作为渲染使用。
虚拟列表
原理: 虚拟列表是一种长列表的解决方案, 在长列表滚动过程中,只有视图区域显示的是真实 DOM ,滚动过程中,不断截取视图的有效区域,让人视觉上感觉列表是在滚动。达到无限滚动的效果。
实现思路:
1 通过 useRef 获取元素,缓存变量
2 useEffect 初始化计算容器的高度。截取初始化列表长度。这里需要div占位,撑起滚动条。
3 通过监听滚动容器的onScroll事件,根据scrollTop来计算渲染区域向上偏移量
4 最后重新计算 end 和 start 高度来重新渲染可视区域+缓冲区域的列表
虚拟列表划分可以分为三个区域:视图区 + 缓冲区 + 虚拟区。
合成事件的好处:
• 为了抹平不同浏览器的差异,提供合成事件对象
• 减少内存消耗,提升性能,(不需要注册那么多的事件了,一种事件类型只在 Root上注册一次)
• 对事件进行归类,可以在事件产生的任务上包含不同的优先级
合成事件流程:
1 首先会把原方法,通过 react事件插件映射表,转成react事件
2 在渲染过程中,fiber向上收集事件依赖,形成事件列表,放在fiber的memorizeState中,再向container容器注册事件监听器(document-v17+container),并bind fiber对象
3 最后dispatchEvent触发事件执行队列,模拟捕获、执行、冒泡的过程
// onChange: ['blur', 'change', 'click', 'focus', 'input', 'keydown', 'keyup', 'selectionchange']
Hooks
Hooks 为什么出现
1 让函数组件有状态,处理副作用,能获取ref,也能做数据缓存
2 解决逻辑复用难的问题
3 放弃面向对象编程,拥抱函数式编程
Hooks 保存状态
函数组件的每个hooks都对应着一个workInProgressHook对象,hooks信息保存在对应fiber的memoizedState中,每个hooks通过next链表关联,形成一个Effect副作用链表
useState dispatchAction 原理
首先:每次调用dispatchAction都会先创建一个update,然后把它放入待更新pending队列中
然后:判断如果当前的fiber是否正在更新,如果是,则停止更新流程
最后:如果fiber没有更新任务,则继续更新,会浅比较 state,如果相同,那么直接退出更新;如果不相同,那么发起更新调度任务。
当再次执行useState的时候,会触发更新hooks逻辑,本质上调用的就是 updateReducer,会把待更新的队列 pendingQueue 拿出来,合并到 baseQueue,循环进行更新。
useMemo useCallback 的区别
useMemo 需要执行第一个函数,缓存执行的结果。(只计算一次)
useCallback 缓存第一个参数,针对可能重新创建的函数
useEffect和useLayoutEffect异同
useEffect 是异步执行 ,对于每一个effect的callback,React会向 setTimeout 回调函数一样,放入任务队列,等到主线程任务完成,DOM更新后,才执行
所以useEffect回调函数不会阻塞浏览器绘制视图
useLayoutEffect是在DOM绘制之前,同步执行,会阻塞浏览器绘制。
可以方便修改DOM ,修改后浏览器只会绘制一次
总结:修改DOM,改变布局就用useLayoutEffect,其他情况都用 useEffect
对于函数组件fiber,updateQueue存放每个useEffect/useLayoutEffect产生的副作用组成的链表。在commit阶段更新这些副作用
FC 函数组件 CC 类组件
相同点:
1 是组件的两种类型,两者底层都依赖于fiber架构实现渲染,只是前置的vdom计算过程有区别
不同点:FC CC
1 state状态:
函数本身无状态,不能调用setstate,需要对应的hook来实现
类组件默认绑定了update对象,有自己的状态
2 生命周期:
函数组件没有生命周期,也需要配合useEffect来实现
类组件有完整的生命周期
3 上手难度:
函数组件代码相对轻量级,但是对应的各种hook就需要理解才能正确使用,上手快,升入难
类组件是oop开发方式,有this、state、render、生命周期等概念需要理解,上手慢、使用简单
4 逻辑复用:
函数组件好复用,抽离公共逻辑,写成hook
类组件复用逻辑需要高阶组件HOC来实现
5 更新:
函数组件每次更新都会重新创建变量
类组件只需要实例化一次,后续更新都尽量复用实例上的状态、节点
6 心智模型:
函数组件是一种偏声明式的'数据到视图'的映射
类组件是一种偏命令式的面向对象实践,注重数据与行为的分装
diff 算法*******
调用 reconcileChildrenArray 来调和子代 fiber,进行新老fiber的对比。
1 同层遍历, 同层节点 sibling 指针移动遍历,判断fiber节点上的tag和key是否匹配,没有变化的节点直接复用oldFiber
2 当分层遍历结束时,统一删除没有复用的 oldfiber
3 当oldFiberMap为null,如果还有新的children,统一创建对应的 newFiber
4 当新旧节点都还有剩余,针对 发生移动 的情况, mapRemainingChildren 数组中查找有没有可以复用的 oldFiber
5 最后删除剩余没有复用的 oldFiber
diff O(n^3) 优化到 O(n)
传统diff算法:通过循环递归对节点进行依次对比,再对比插入删除节点,算法复杂度O(n^3)
新的diff算法的3个优化策略:
tree diff:分层遍历,忽略跨层级移动DOM节点,只会遍历一次
component diff:判断组件类型 tag 是否一样,不一样则直接删除老组件、创建新组件
element diff:对于同一层级的子节点,用唯一的key值进行判断
调度 时间片
异步调度-时间片
GUI渲染线程和JS线程是相互排斥的。react的更新是交给浏览器自己控制的,react从根节点开始diff更新时,会通过类似requestIdleCallback去向浏览器做一帧一帧的请求,如果浏览器有绘制任务就先绘制,在空闲时再执行diff更新任务,这样来减少页面卡顿,提升体验
消息通道:目前requestIdleCallback只有谷歌浏览器支持
为了兼容每个浏览器:
IE浏览器 -> setImmidiate
主要用messageChannel高频短间的去请求时间切片 (类似于宏任务的机制)
兜底 -> setTimeout
异步调度流程*******
fiber 调和
fiber 是什么
- 作为架构来说,在旧的架构中,Reconciler(协调器)采用递归的方式执行,无法中断,节点数据保存在递归的调用栈中,被称为Stack Reconciler栈调和, 循环递归遍历VDOM,可能会造成页面卡顿;在新的架构中,Reconciler(协调器)是基于fiber实现的,节点数据保存在fiber中,所以被称为 fiber Reconciler。
- 作为静态数据结构来说,fiber是 React element对象和真实DOM之间的桥梁,每个fiber就是一个节点,保存了这个节点的基本信息,这个时候,fiber节点就是虚拟DOM。
- 作为动态工作单元来说,fiber节点保存了该节点需要更新的状态,以及需要执行的副作用,fiber在React中就是最小粒度的执行单元。
React Fiber是一种新的协调引擎,它具有改进协调算法、支持增量渲染、可中断和恢复、支持并发模式、向后兼容和支持错误边界等特点,从而提高了React应用的性能、可扩展性和健壮性。
fiber 的属性
function FiberNode(){
this.tag = tag; // fiber 标签 证明是什么类型fiber。
this.key = key; // key调和子节点时候用到。
this.type = null; // dom元素是对应的元素类型,比如div,组件指向组件对应的类或者函数。
this.stateNode = null; // 指向对应的真实dom元素,类组件指向组件实例,可以被ref获取。
this.return = null; // 指向父级fiber
this.child = null; // 指向子级fiber
this.sibling = null; // 指向兄弟fiber
this.index = 0; // 索引
this.ref = null; // ref指向,ref函数,或者ref对象。
this.pendingProps = pendingProps;// 在一次更新中,代表element创建
this.memoizedProps = null; // 记录上一次更新完毕后的props
this.updateQueue = null; // 类组件存放setState更新队列,函数组件存放
this.memoizedState = null; // 类组件保存state信息,函数组件保存hooks信息,dom元素为null
this.dependencies = null; // context或是时间的依赖项
this.mode = mode; //描述fiber树的模式,比如 ConcurrentMode 模式
this.effectTag = NoEffect; // effect标签,用于收集effectList
this.nextEffect = null; // 指向下一个effect
this.firstEffect = null; // 第一个effect
this.lastEffect = null; // 最后一个effect
this.expirationTime = NoWork; // 通过不同过期时间,判断任务是否过期, 在v17版本用lane表示。
this.alternate = null; // 双缓存树,指向缓存的fiber。更新阶段,两颗树互相交替。
}
调和流程 **********
1 首先创建fiberRoot根节点和rootFiber组件节点
2 构建出current和workInProgress双缓存fiber树,用alternate关联起来
3 通过beginwork 向下调和 reconcileChildren子节点 ,通过diff算法复用或者增删节点
通过completeUnitOfWork 向上归并,如果有兄弟节点,会返回sibling兄弟,没有则return父级fiber,一直返回到fiebrRoot根节点
4 最后会以workInProgress作为最新的渲染树,替换成current树
调和流程图
中断和恢复的贪心思想:每次有时间都会去尝试执行更新
1 如果能执行完,就 commit 提交指令 更新 DOM
2 没有就继续执行 while 循环
3 有更高优先级的任务就放弃已经执行了的 task
渲染
shallowEqual 浅比较流程***
1 先判断新老props、state 是否是对象
2 接着比较新老props、state 的对象是否相等
3 然后判断对象的属性长度是否相等 (Object.keys)
4 最后再逐个遍历比较 key 是否相等
React 性能优化方案
1 渲染前:减少提交的更新
react.lazy+suspense异步加载组件
useRef挂载不用更新的数据(非受控组件)
避免使用内联对象、匿名函数(每次渲染执行都会产生新的props对象,导致子组件重复渲染)
状态下放,缩小状态影响的范围
列表渲染使用唯一的 key 值
2 渲染中:跳过不必要的更新
FC:React.memo+useCallback、useMemo 缓存组件
CC:shouldComponentUpdate、pureComponent 函数判断是否需要重新渲染
3 常见的前端的通用优化
分页按需加载组件、防抖、节流、虚拟列表
V18 新增了哪些特性
1 createRoot renderer api: Concurrent Mode(并发模式的渲染) 可以同时更新多个状态(从(V17)同步不可中断更新变成了(V18)异步可中断更新)
2 setState 自动批处理(多次更新合并成一次)
3 flushSync 提高state更新的优先级
4 React 组件的返回值可以为undefined、null
5 严格模式下,第二次渲染在控制台的日志将显示为灰色
6 Suspense不再需要fallback来捕获 (展示null)
7 新的Hook:useId(组件ID)、useSyncExternalStore(状态)、useInsertionEffect(注入css)
并发模式的理解
1 并发更新的意义就是交替执行不同的任务,当预留的时间不够用时,React将线程控制权交还给浏览器,等待下一帧时间到来,然后恢复中断的工作
2 并发模式是实现并发更新的基本前提
3 时间切片是实现并发更新的具体手段
4 都是基于 fiber 架构实现的,fiber为状态更新提供了可中断的能力
Lane expirationTime 优先级
LanePriority:
fiber优先级: 位于react-reconciler包的 Lane 车道模型
1:Lane类型被定义为二进制变量, 利用了位掩码的特性, 在频繁计算的时候占用内存少, 计算速度快
2:Lane和Lanes就是单数和复数的关系, 代表单个任务的定义为Lane, 代表多个任务的定义为Lanes
共定义了18 种车道(Lane/Lanes)变量, 每一个变量占有 1 个或多个比特位, 分别定义为Lane和Lanes类型
占有低位比特位的Lane变量对应的优先级越高
最高优先级为SyncLanePriority对应的车道为SyncLane = 0b0000000000000000000000000000001
最低优先级为OffscreenLanePriority对应的车道为OffscreenLane = 0b1000000000000000000000000000000.对应为离屏渲染
3:Lane 是对于 expirationTime 的重构, react 17 里以前使用 expirationTime 表示的字段, 都改为了 lane
使用 Lanes 模型相比 expirationTime 模型的优势
1 Lanes把任务优先级从批量任务中分离出来, 可以更方便的判断单个任务与批量任务的优先级是否重叠
2 Lanes 使用单个 32 位二进制变量即可代表多个不同的任务, 也就是说一个变量即可代表一个组 (group), 要在一个 group 中分离出单个 task, 非常容易
SchedulerPriority
调度优先级
export const NoPriority = 0;
export const ImmediatePriority = 1;
export const UserBlockingPriority = 2;
export const NormalPriority = 3;
export const LowPriority = 4;
export const IdlePriority = 5;
ReactPriorityLevel
优先级等级,负责上面2套优先级体系的转换
react-router
history React-router React-router-dom
模式与核心对象:
/history模式:
www.xxx.com/home
window.history.pushState
/#/hash模式:
www.xxx.com/#/home
window.location.hash
redux mobx
redux 的工作流程
1 createStore 生成数据中心 Store
2 action定义行为
3 dispatch发起action
4 reducer处理action,返回新的state
1 单向数据流
2 state只读,要改变state需要触发action来执行reducer
3 reducer是纯函数
middleware 中间件机制
Redux middleware是一种可插拔的机制,用于在dispatch函数被调用后, reducer处理action之前,对action进行拦截、变换、增强等操作
redux-thunk redux-saga
原生的dispatch是不支持异步的,要使用redux-thunk或者redux-saga等中间件才能支持
redux-thunk:
优点:功能简单、体积小、上手快 (20+行)
缺点:
样板代码过多,而且很多是重复的
异步操作与redux的action偶合在⼀起,不⽅便管理
redux-saga:
优点:
异步解耦: 异步操作被转移到单独saga.js中
异常处理: 代码异常/请求失败 都可以直接通过try/catch语法直接捕获处理
功能强⼤: redux-saga提供了⼤量的Saga辅助函数和Effect创建器供开发者使⽤
缺点:
有一定的上手成本(10+API)
体积庞⼤(2千行代码)
mobx 原理
Mobx 采用了一种'观察者模式'——Observer;
使用流程:
1 初始化绑定状态 observable 、激活状态 makeObservable
2 依赖收集: 通过 @observable 包裹
3 派发更新
Redux Mobx 区别
1 Redux是单向数据流
Mobx依赖于Proxy、Object.defineProperty等,去劫持get 、set ,数据变化多样性
2 Redux可拓展性比较强,可以通过中间件自定义增强 dispatch
3 在Redux中,基本只有一个store ,统一管理store下的状态
在mobx中,可以有多个模块,每一个模块都有一个独立的 store
4 首先Mobx更容易上手。比如Redux想使用异步,需要配合中间件,流程比较复杂