# React
- React 两大工作循环
React 中存在两大工作循环:任务调度循环和 fiber 构造循环,任务调度循环用于从任务堆中取出任务并进行执行,fiber 构造循环用于构造 workInProgressFiber。
- 响应式原理
React 响应式原理在于 React 内部维护了一个最小堆用于存储更新任务,堆的排序指标是依据 task 的 timeout 属性,这与 React 的任务优先级调度相关联,当使用 setState 时,就利用闭包的原理改变了 fiber 节点中对应 hook 节点的memoziedState属性,该属性存储了组件全部的 state,由于 state 变化了,就会使得组件后续被 fiber 构造循环打上对应的 lane 并放到任务堆中,任务堆会在浏览器空闲时间去执行任务 - 优先级调度
当组件 state 变化时,react 会为当前的更新标记为某个 31 位二进制数据,称之为 lane(车道),然后通过 lane 车道为当前的任务标记上对应的 timeout,推入到任务堆中,任务堆会自动根据 timeout 进行排序,从而实现优先级调度。
- lane 模型动机
react 旧版本采用
expirationTime属性表示是否是高优先级任务,这样会导致高优先级任务不停打断低优先级任务的情况,采用lane模型使用二进制强大的类型表述能力精细化了任务优先级,并将任务更新问题由:是否是高优先级转变为了属于哪一条车道
- SynsticEvent 事件
react 跨平台特性使得它需要自行进行事件处理,而不是和特定浏览器事件强绑定。react 采用 SynsticEvent 机制统一将事件绑定到跟组件上(原来是绑定到 globalThis 上,由于支持微前端改了),触发事件时本质上都是在执行
dispatchEvent函数,并传入该组件,然后会根据组件名执行跟组件上绑定的对应的事件处理函数(实际过程往往更加复杂,react 会从当前组件开始向父组件冒泡并尝试获取事件,遇到 stopPropgation 就会停止向上冒泡的过程)
# Fiber
Fiber 架构
Fiber 架构:
Fiber架构是一个增量渲染,架构风格类似协程,Fiber 架构出现的原因是由于 JS 单线程执行的特性当遇到繁琐的执行任务时,原来 React15 的协调过程就会很长,从而延迟 DOM 的渲染更新,进而出现掉帧。Fiber 架构将任务放入任务最小堆中,并且使迭代器的执行过程发生在浏览器的空闲时间,从而最大程度利用了这部分资源,使得 UI 渲染不会被大量执行任务所阻塞。
Fiber 树
fiber 树:fiber树是一个数据结构,也就是虚拟DOM,在 Fiber 结构中的任务是可以中断执行的,继续执行时会丢弃掉原来的工作从头再来,并且重新执行中断的任务,对用户来说也是无感的,因为 Fiber 架构的视图更新是后缓冲区视图 替换 前缓冲区视图的过程。
fiber 架构对生命周期的影响
由于 Fiber 架构下的 reconciler 协调阶段是异步可中断的,且会被反复重新执行,使得反复执行时有可能触发的生命周期钩子被废弃,例如:componentWillMount、componentWillReceiveProps、componentWillUpdate 这些。
fiber 树的遍历
fiber 树(fiber 节点构成的树状链表)的遍历与执行:
- fiber 树遍历:
children子节点----sibling兄弟节点----return父节点 - fiber 树的执行:遍历到最底层子节点 A----A 的兄弟节点 -----A 的父节点 B------B 的兄弟节点(深度优先遍历)
fiber 节点
fiber 节点(对象):一个 fiber 节点对应一个 React 组件,fiber 节点里包含了组件的 work 任务等信息,比如组件的 Hooks 执行逻辑(它是一个链表,可以通过 memorizedState 拿到跟节点,Hooks 执行逻辑里面存储了 state)、生命周期、对于 html 组件的增删改查等副作用让出机制
当执行 fiber 节点中的任务时,每次执行完之后 React 就会检查当前帧还剩多少时间,没有时间就会将控制权让出去。
# fiber 树、VDOM 和 diff 算法
fiber 树就是 VDOM,fiber 树的变化反映了组件的状态变化
diff 算法的本质就是:对比current fiber和JSX对象生成work-in-progress-fiber
即使是最前沿的算法,完全对比两棵树的时间复杂度也需要 O (n^3), 其中为了提高性能,如果 type 类型不同就认为节点不相同.
# Hooks
注意:react Hooks 只能在组件顶层进行调用并且不能写在条件判断中,这是因为 hooks 以链表的形式存放在 fiber 节点的
memozied-state属性中(类组件的副作用也存在 fiber 节点中),每次更新时会基于链表的顺序进行调用,而调用 hooks 所产生的 state 就存在于 hooks 节点之中,如果 hooks 写在条件判断中则导致 hooks 链表执行混乱,使得状态更新出错。
# workLoop 工作循环
workLoop 中会根据当前帧的剩余时间去执行 fiber 节点中的任务,如果时间不够就将控制权转给 UI 渲染,并保存当前的执行上下文 (包括当前 fiber 节点的状态), 当 UI 渲染完毕后,恢复其执行 (执行到一半的任务会重新执行)
# 双缓冲策略
双缓冲策略是用于减少组件渲染过程中的闪烁和卡顿。双缓冲策略是对于 fiber 树来说的
双缓冲策略会维护两个 Fiber 树:Work-in-progress-fiber 树 和 current-fiber 树。协调过程中 React 会比较新旧两个 fiber 树的差异,从而确定哪些组件需要更新。一旦新的 fiber 树构建完成,React 就会使用 diff 算法去更新真实 DOM。更新完成后会将工作 Fiber 树的根节点与当前 Fiber 树的根节点进行交换,这个过程叫做提交。
# Fiber 更新的三个阶段
- 开始阶段:
ReactFiberBeginWork
对照旧的 fiber 树,从根节点遍历新的 fiber 树(根据 child 指向),将 state 变化的副作用根据
lane和hooks调用方式打上timeout放入最小任务堆中
- 这个阶段 react 需要决定哪些组件需要更新、哪些组件可以复用、哪些组件需要被挂载或卸载
- React 通过比较新旧 Fiber 树来确定变化,这个过程称为协调算法(Reconciliation)。
- 此阶段会创建一个新的工作进度树(work-in-progress tree),表示 UI 的最新状态。
- 这个阶段是可中断的,React 可以决定挂起渲染过程,稍后再恢复。
- 完成阶段:
ReactFiberCompeleteWork
根据子节点的 return 指向执行注册的生命周期钩子。
- 这个阶段发生在实际将更新应用到屏幕之前。
- React 执行生命周期方法,如 getSnapshotBeforeUpdate,允许组件捕获当前的 DOM 状态或执行捕获操作。
- 这个阶段用于执行那些需要在提交前知道布局效果的副作用,例如,测量组件的尺寸或位置。
- 提交阶段:
ReactFiberCommitWork
在不影响 UI 渲染的前提下,执行最小任务堆中的所有任务
- 这是实际将更改应用到真实 DOM 的阶段。
- React 处理所有副作用,如 componentDidMount、componentDidUpdate 和 componentWillUnmount 生命周期方法。
- 更新 DOM 元素和属性,添加或删除 DOM 节点,以确保真实 DOM 与工作进度树同步。这个阶段是连续的,不能被中断,因为 DOM 更新通常需要原子性地完成。
# 为什么 hooks 不能写在条件判断
hooks 函数最终会被存在组件对应的 fiber 节点的 memoizatedState 中,组件每次更新会按照顺序执行 (fiber 节点的 memozieState 属性存着 hooks 节点,hook 节点的 momelized 属性存着 state 状态),如果组件每次更新时的 hooks 链表顺序乱了,就会导致 state 对应不上、状态混乱。
# react 不可变数据
例如当 setData (data + 1) 副作用在组件内连续调用三次时,其实是相当于只调用了一次,这就是 react 不可变数据或者说:当前快照只能操作当前快照的值;
出现 react 不可变数据现象的原因是由于: hooks 执行逻辑最后会以链表的形式存储在 fiber 节点之中,多次调用这个 hooks 返回的值相同因此没有变化,上例正确的写法是写成一个函数,这样在存储 hooks 执行逻辑到 fiber 节点中的就是一个函数,最后会以闭包的原理进行更新,例如应该写成:setData (()=>data+1)。
# 为什么 React 宏任务用 MessageChannel ?
settimeout 具有 4ms 延迟问题,由于 react 的跨平台特性就使得不能使用一些兼容性较差的 api,并且 requestAnimationFrame 调度不稳定并且因为是微任务所以不能实现让出机制,requestIdleCallback 有 fps 的限制,即 1s 最多调用 20 次,所以采用 MessageChannel 配合 performance.now 来执行让出任务机制,当需要调度任务时,就会向 messageChannel 广播消息,监听到消息就会在宏任务队列中添加任务调度循环函数。
# ReactFiberLane 模型 (并发模式)
并发
React 并发模式:首先,并发和并行不一样,并行是同一时刻多件事情同时进行,
而并发是只要一段时间内同时发生多件事情就行。React 并发模式允许多个状态在同一时间段进行更新,Fiber 节点上的 Lanes 标记告诉 React 如何并发处理这些状态更新。
并发模式
为了让高优先级的更新能先渲染,react 实现了并发模式。React 中更新 State 有两种模式:同步模式是循环处理 fiber 节点,并发模式多了个 shouldYield 的判断,每 5ms 打断一次,也就是时间分片。并且之后会重新调度渲染。
React 并发
React 并发是指:对于每一个次 State 状态变化要执行的 Hooks 链表的更新,在循环执行时都会判断一下是否需要打断这次循环,从而将更新让给其他任务
Lane
React 使用 31 位二进制来表示优先级车道,也就是 Lane, 一共 31 条,位数越小 (1 的位置越靠右) 表示优先级越高.
这些并发特性的 api 都是通过设置 Lane 实现的,react 检测到对应的 Lane 就会开启带有时间分片的 workLoopConcurrent 循环。时间分片的 workLoop + 优先级调度,这就是 React 并发机制的实现原理。这就是 React 并发机制的实现原理。
基于 Lane 的优先级实现的 api, 例如:useTransition、useDeferredValue。当被用到 的时候,react 才会启用 workLoopConcurrent 带时间分片的循环。
Lane 与更新
React 每次更新状态会将同类型的 Lane (通过位运算与) 合并形成 Lanes (通过位运算或), 然后从同类型的 Lanes 中找出优先级最高的事件
当一个 Fiber 节点需要更新时,React 会根据状态的变化创建更新类型,通过更新类型在该节点上标记相应的Lanes, 指示了该节点的优先级类型,React这种并发模式允许多个更新同时进行处理,React 会根据 Fiber 节点上的 Lanes 来决定哪些更新可以并发执行,以及他们的执行顺序,如果一个高优先级的更新需要立即处理,React 可以中断当前正在进行的低优先级更新,转而处理高优先级的更新。一旦高优先级的更新完成,之前中断的更新可以恢复。
lane 优先级和 scheduler 优先级
其实在 react 中主要分为两类优先级,scheduler 优先级和 lane 优先级,lane 优先级下面又派生出 event 优先级
lane 优先级:主要用于任务调度前,对当前正在进行的任务和被调度任务做一个优先级校验,判断是否需要打断当前正在进行的任务
event 优先级:本质上也是 lane 优先级,lane 优先级是通用的,event 优先级更多是结合浏览器原生事件,对 lane 优先级做了分类和映射
scheduler 优先级:主要用在时间分片中任务过期时间的计算
被打断的更新任务
被打断的 Hooks 链表的更新任务会被丢弃,由于没有渲染完所以需要再添加一个任务进任务队列
Lane 模型是 React 中的一种状态更新机制,目的是提高应用的性能和响应速度,核心思想是将 UI 中的状态变化抽象成一系列的 lane 变化,每个 lane 只描述了一个状态的变化,而不是一次完整得状态更新,这样可以使得状态变化更加清晰易于处理和维护
打断是根据时间切片
react 的并发模式的打断只会根据时间片,也就是每 5ms 就打断一次,并不会根据优先级来打断,优先级只会影响任务队列的任务排序。
优先级转换
React 通过 Scheduler 调度任务时,会先把 Lane 转为事件优先级,再把事件优先级转为 Scheduler 的五种优先级
并发渲染和同步渲染
所谓的并发渲染就是加了一个 5ms 一次的时间分片,react18 里同时存在着这两种循环方式,普通的循环和带时间分片的循环。也不是所有的特性都要时间分片,只有部分需要,如果这次 setState 更新里包含了并发特性,就是用workLoopConcurrent,否则走workLoopSync就好了。
例如
比如上面有两个 setState,其中一个优先级高,另一个优先级低,那就把低的那个用 startTransition 包裹起来。就可以实现高优先级的那个优先渲染。实现原理是:在调用回调函数之前设置了更新的优先级为 ContinuousEvent 的优先级,也就是连续事件优先级,比 DiscreteEvent 离散事件优先级更低,所以会比另一个 setState 触发的渲染的优先级低,在调度的时候排在后面。这里设置的其实就是 Lane 的优先级:那渲染的时候就会走workLoopConcurrent 的带时间分片的循环,然后通过 Scheduler 对任务按照优先级排序,就实现了高优先级的渲染先执行的效果。
# useState 钩子
创建可以直接更新的状态变量
# useReducer 钩子
与 useState 相似,创建状态变量,同时可以自定义 reducer(内部变量变化的调度机制)
function App() { | |
// 注意:reducer 中的返回值就是新的 state | |
const dataReducer = (state, aciton) => { | |
switch (aciton) { | |
case 0: return "你好"; | |
case 1: return "世界"; | |
case 2: return "你好世界"; | |
default: return "世界你好"; | |
} | |
} | |
const [data, dispatchData] = useReducer(dataReducer, "你好世界"); | |
return ( | |
<> | |
<h1>{data}</h1> | |
<button onClick={() => dispatchData(0)}>你好</button> | |
<button onClick={() => dispatchData(1)}>世界</button> | |
<button onClick = {()=>dispatchData(2)}>你好世界</button> | |
<button onClick={() => dispatchData(3)}>世界你好</button> | |
</> | |
) | |
} |
# createContext 和 useContext 钩子
createContext 和 useContext 直接使用相当于是创建可用的变量
const testContext = createContext("初始化数据") | |
// 在组件中就可以拿到 testContextData, 然后使用: | |
const testContextData = useContext(testContext); |
createContext 和 useContext 高级用法本质上是依赖注入,他返回一个对象,对象的 Provider 属性是一个组件,用于注入数据(在 Provider 组件上绑定 value 属性),注入的数据可以通过 useContext 获取。
const ThemeContext = createContext(null); // 创建 context | |
function MyPage() { | |
const [theme, setTheme] = useState('dark'); | |
// 将依赖注入到组件内部 | |
return ( | |
<ThemeContext.Provider value={theme}> | |
<ThemeText /> | |
</ThemeContext.Provider> | |
); | |
} | |
// 组件内部使用注入的依赖 | |
function ThemeText(){ | |
const themeData = useContext(ThemeContext); | |
return <>{themeData}</> | |
} |
# useRef 钩子
useRef 其实是 useState 的封装 (不返回 setState 方法,只返回 state)
帮助引用一个不需要渲染的值 (不会触发组件重新渲染), 返回一个具有 current 属性的对象,通常用于保存 DOM 节点
注意,改变 ref 不会触发重新渲染,所以 ref 不适合用于存储期望显示在屏幕上的信息。如有需要,使用 state 代替。React 希望不要在渲染期间写入或者读取 ref.current, 如果不得不在渲染期间读取 或者写入,那么应该 使用 state 代替。
function App() { | |
const inputFocus = (ref) => { | |
ref.current.focus(); | |
}; | |
const inputRef = useRef(null); | |
return ( | |
<><input ref={inputRef} /> | |
<button onclick={(e) => inputFocus(inputRef)}>聚焦输入框</button></> | |
); | |
} | |
export default App |
# useEffect 钩子
接收两个参数:副作用函数和依赖项数组,当依赖项变化时自动执行副作用函数,副作用函数的返回值是一个清理函数,会在每次组件更新前进行执行
useEffect 中第二个参数不传代表每次渲染组件后都执行一次,传空数组代表只会在第一次挂载后执行,传依赖项代表在 ** 依赖项变化时执行。** 并且默认组件挂载时就会自动执行一次,以便可以读取到依赖项
useEffect 依赖项中传入 ref 通常是无效的,因为 ref 相当于不使用渲染赋值的 state 状态,传递过去的 ref 引用始终相同(不随着快照的渲染而变化)。
某些逻辑不能放在 effect 中执行,因为 effect 的执行是和组件渲染强绑定的(例如不能在 effect 中写购买商品的逻辑,这样会导致组件以任何方式被渲染都会执行购买逻辑,这样是不对的)
effect 中 return 的函数会在下一次 effect 执行前被执行,常用于执行清理函数(清除定时器等)
# useLayoutEffect 钩子
DOM 更新之前执行,可以在此处测量布局信息。
# useInsertionEffect 钩子
在 React 对 DOM 进行更改之前触发,库可以在此处插入动态 CSS。
# useMemo 钩子
缓存函数的计算结果,只有当依赖项发生变化时,才会重新计算
# useCallback 钩子
缓存函数的定义,接收的参数是:缓存函数和依赖项,只有当依赖项 (栈值) 发生变化时,才会更新
function ProductPage({ productId, referrer, theme }) { | |
// 在多次渲染中缓存函数 | |
const handleSubmit = useCallback((orderDetails) => { | |
post('/product/' + productId + '/buy', { | |
referrer, | |
orderDetails, | |
}); | |
}, [productId, referrer]); // 只要这些依赖没有改变 | |
return ( | |
<div className={theme}> | |
{/* ShippingForm 就会收到同样的 props 并且跳过重新渲染 */} | |
<ShippingForm onSubmit={handleSubmit} /> | |
</div> | |
); | |
} |
useCallback 是由 useMemo 封装而来: (useCallback 内部存储的不是原来的函数体,而是一个普通函数返回函数体被 useMemo 缓存的结果)
function useCallback (fn,dependencies){ | |
return useMemo(()=>fn,dependencies) | |
} |
# useTransition 钩子
允许将状态转换标记为非阻塞,并允许其他更新中断它。为了更好地控制组件更新和动画而设计
# useDeferredValue 钩子
允许延迟更新 UI 的非关键部分,以让其他部分先更新。
# Fragment 组件
<Fragment> 通常使用 <>...</> 代替,它们都允许你在不添加额外节点的情况下将子元素组合。
# Profiler 组件
<Profiler> 允许你编程式测量 React 树的渲染性能。接受一个 id 用于表示测量的 UI 部分,接受一个回调函数,当包裹的组件树更新时会传入渲染信息进行调用。
<Profiler id="App" onRender={onRender}> | |
<App /> | |
</Profiler> |
# StrictMode 组件
开启严格模式,开发阶段会渲染两次,使得尽早地发现组件中错误
# Suspense 组件
展示子组件加载完成前渲染的内容.
<Suspense fallback={<Loading />}> | |
<AsyncComponent /> | |
</Suspense> |
# memo 方法
memo 允许你的组件在道具没有改变的情况下跳过重新渲染
# createPortal 方法
createPortal 允许你将 JSX 作为 children 渲染至 DOM 的指定部分。
# createRoot 方法
createRoot 允许在浏览器的 DOM 节点中创建根节点以显示 React 组件。
# hydrateRoot 方法
hydrateRoot 函数允许你在先前由 react-dom/server 生成的浏览器 HTML DOM 节点中展示 React 组件。
# act 方法
行为测试助手,用于测试
# forwardRef 方法
允许组件使用 ref 将 DOM 节点指向给父组件。
# lazy 方法
延迟加载组件 (懒加载)。常配合 Suspense 组件使用
# startTransition 方法
可以在不阻止 UI 的情况下更新状态。
# Vue
# 高级用法
# watchEffect 函数
立即运行传入的函数,同时自动追踪其依赖,并在依赖更改时重新执行。
watchEffect 的返回值是用于清除该副作用的函数。
const data = ref(0); | |
const stop = watchEffect(()=>console.log(data.value,'data变化了')); | |
stop(); // 清除响应性监听 |
watchEffect 的第二个参数是 options 配置项,可以配置 flush、onTrack 函数和 onTrigger 函数
watchEffect(()=>{},{ | |
flush:"post", //flush 配置项配置回调函数的刷新时机,post 会在 DOM 渲染之后触发、sync 会在 vue 进行任何更新之前进行触发 | |
onTrack(e){debugger}; | |
onTrigger(e){debugger}; | |
}) |
formData -- applice/json
map 和 obj
# watchSyncEffect 函数
回调函数会在 DOM 渲染之后触发,相当于 watchEffect 中配置了 fulsh:"post"
# watchSyncEffect 函数
回调函数在 vue 进行任何更新之前触发,相当于 watchEffect 中配置了 fulsh:"sync"
# effectScope 函数
effectScope 函数创建一个 effect 作用域,可以捕获其中所创建的响应式副作用(即计算属性和侦听器),这样捕获到的副作用可以一起处理。
const scope = effectScope(); | |
scope.run(()=>{ | |
const doubled = computed(() => counter.value * 2) | |
watch(doubled, () => console.log(doubled.value)) | |
watchEffect(() => console.log('Count: ', doubled.value)) | |
}) | |
scope.stop(); // 清除掉作用域内所有的 effect |
# getCurrentScope 函数
获取当前活跃的 effect 作用域
# onScopeDispost 函数
在当前活跃的 effect 作用域上注册一个处理回调函数。当相关的 effect 作用域停止时会调用这个回调函数。
# shallowRef 函数
用于浅层响应式,避免深层比较带来的效率问题
# triggerRef 函数
强制触发依赖于一个浅层 ref 的副作用,通常在对浅引用的内部值进行深度变更后使用
# customRef 函数
customRef 函数创建一个自定义的 ref,显示声明对其依赖追踪和更新触发的控制方式。预期接受一个工厂函数作为参数,这个工厂函数接收 track 和 trigger 两个函数作为参数,并返回一个带有 get 和 set 方法的对象
// 定义一个返回懒执行响应式数据的函数 | |
function useDebouncedRef(value,delay=200){ | |
let timeout; | |
return customRef((track,trigger)=>{ | |
return { | |
get(){ | |
track(); | |
return value; | |
}, | |
set(newValue){ | |
clearTimeout(timeout); | |
timeout = setTimeout(()=>{ | |
value = newValue; | |
trigger(); | |
},delay) | |
} | |
} | |
}) | |
} |
# shallowReactive
shallowReactive 是 reactive 的浅层作用形式,只有跟级别的属性是响应式的,属性的值会被原样存储和暴露,这也意味着值为 ref 的属性不会被自动解包了。
# shallowReadonly
shallowReadonly 是 readonly 的浅层作用形式。
# isRef 函数
用于检查某个值是否是 ref
# unref 函数
如果是 ref,返回 ref 内部的值,否则返回参数本身
# toRef 函数
- 传入 ref 返回 ref 本身
- 传入 props.key 创建一个只读的 ref
- 传入 number 或者 string 相当于 ref 函数
- 传入响应式数据和键值会封装为一个 ref,但是相比于直接封装 ref 来说,会与源属性进行同步
const state = reactive({foo:1,bar:2});
const fooRef = toRef(state,'foo'); //fooRef 会和 state 的响应性相关联
const fooRef2 = ref(state.foo); //fooRef2 不会和 state.foo 的响应性相关联
# toRefs 函数
vue3 中的 ref 将 reactive 响应性绑定到.value 属性上,其本质就是为了防止开发者错误的将响应式数据进行解构后的变量又其当作响应式数据。所以加了一层隔离。toRefs 函数也是用于解决这个问题。
将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是指向源对象相应属性的 ref。每个单独的 ref 都是使用 toRef 创建的
const state = reactive({ | |
name:"张三", | |
age:18 | |
}) | |
const stateAsRefs = toRefs(state); | |
state.name = "李四"; //toRefs 返回的响应式数据和原响应式数据相互关联 | |
console.log(stateAsRefs.name.value) // 李四 | |
//toRefs 的存在是为了保证 reactive 响应式被解构之后仍然存在响应性 | |
const {name,age} = toRefs(state) | |
// 解构之后的 name 和 age 都是响应式对象 |
# toRaw 函数
toRaw 返回由 reactive、readonly、shallowReactive 或者 shallowReadonly 创建的代理对应的原始对象(栈赋值),返回的对象不再具有响应式,栈赋值时不会影响到页面的展示,但如果改变该对象的堆中的属性,原对象的依赖项也会随之变化
# markRaw 函数
将一个对象标记为不可被转为代理,返回该对象本身
# toValue 函数
将值、函数、或响应式数据规范化为普通值,toValue (ref (1)) ---> 1
# isProxy 函数
检查一个对象是否是由 reactive、readonly、shallowReactive 或者 shallowReadonly 创建的代理。
# isReactive 函数
检查一个对象是否是由 reactive 或 shallowReactive 创建的代理
# isreadonly 函数
检查传入的值是否是只读对象,只读对象的属性可以更改,但是不能通过传入的对象直接赋值。
# Transition 组件
- 使用过渡样式
- mode 属性定义动画进行的顺序,常用 out-in
- name 属性定义动画的名称,配合 css 使用
- appear 属性初次渲染过渡
- 生命周期钩子用于自定义过渡中执行过程,依次是 before-enter,enter,after-enter,enter-cancelled,before-leave,leave,leave-cancelled, 可用的参数有 el 和 done ()
- css 样式过渡:name-enter-from\name-enter-active\name-enter-to\name-leave-from\name-leave-active\name-leave-to 定义对应时期的样式 (只对可以过渡变化的样式生效)
# TransitionGroup 组件
- TransitionGroup 组件用于对 v-for 列表中的元素或组件插入 、移除或顺序变化添加动画效果.
- 属性和生命周期和 Transition 一样
- tag 属性可以指定 TransitionGroup 为一个容器
# KeepAlive 组件
- KeepAlive 用于在多个组件间切换状态时缓存被移除的组件实例 (使组件保存状态)
- KeepAlive 的原理就是当 KeepAlive 中的组件被移除时,用变量将整个组件缓存起来,需要切换时直接使用缓存起来的变量
- include 和 exclude 属性用于包含或排除对应的组件 name
- KeepAlive 的组件包含 onActivated 和 onDeactived 两个生命周期钩子
# Teleport 组件
- Teleport 组件,用于将组件内部的一部分模板传送到外部结构中去
- to 属性用于指定传送到的组件或者 DOM 元素
# Suspense 组件
- Suspense 组件用于显示异步组件加载中的显示状态
- Suspense 组件中 fallback 具名插槽用于显示加载内容
- Suspense 组件嵌套 Suspense 组件时,给内部 Suspense 组件加上 suspensible 属性表示为异步组件,否则则会被父级 Suspense 组件视为同步组件
# watch 的 options 配置项
watch 的 options 配置项中可以使用:
- immediate, 为 true 时会在初始化时立即执行一次
- deep, 为 true 时会深度监听对象堆中变化
- flush, 调整回调函数的执行时机
- once, 回调函数只会执行一次
- onTrack 函数,当响应式被收集时进行触发只在开发模式下有效
- onTrigger 函数,当依赖项变更时进行触发只在开发模式下有效
# computed 的 options 配置项
- onTrack 函数,当响应式被收集时进行触发只在开发模式下有效
- onTrigger 函数,当依赖项变更时进行触发只在开发模式下有效
# 优势
# vue 对 jsx 的支持友好
- 在 vue 中也可以很方便的去集成 jsx 或 tsx 语法,tsx 语法需要在 tsconfig.json 中配置:jsx:preserve,最终的 jsx 语法会被转换为 h 函数
- 对于事件和案件修饰符,可以使用 vue 中的 withModifiers 函数
# vue 对 web component 的支持友好
- 在 Vue 应用中使用自定义元素基本上与使用原生 HTML 元素的效果相同
- 需要在构建工具中配置 compilerOptions.isCustomElement 这个选项
- 传递 ODM 属性时,需要使用 v-bind 绑定,通过.prop 修饰符进行设置
<my-element :user.prop="{ name: 'jack' }"></my-element>
<!-- 等价简写 --><my-element .user="{ name: 'jack' }"></my-element>
- 使用 vue 构建 web component 需要使用 defineCustomElement 这个方法定义出组件,然后通过 customElement.define 这个方法将 vue 组件添加标签到 HTML 中
# web component 的优缺点
- 全部使用自定义元素来构建应用的方式可以使得应用永不过时和多平台、框架共享
- 但是设想与显示总是存在偏差:
- 1、原生 web component 并不具备响应式的系统
- 2、原生 web component 并不具备一个声明式的、高效的模板系统
- 3、SSR 渲染时,web component 需要在 node.js 中模拟出 DOM,这将增大服务器端的压力
- 4、当下要想使用 shadow DOM 书写局部作用域的 CSS,必须要将样式嵌入到 JavaScript 中才可以在运行时注入到 shadow root 上,这将导致 SSR 场景下需要渲染大量重复的样式标签。
# vue 结合 js 动画库
- 以 gsap 为例,vue 结合 js 动画库实现动画效果时,不能直接对响应式变量进行动画设置,因为是响应式完成之后才被监听到,此时响应式变量已经是最新的值,所以应该再来一个响应式变量中转一下,页面动画效果绑定的是中转的变量。
# vue 生命周期
- vue3 中 setup 替代了 beforeCreate 和 created
- beforeMount、monuted、beforeUpdate、updated、beforeUnmount、unMounted
- 错误捕获钩子:onErrorCaptured,如果在 onErrorCaptured 中抛出一个错误,则会被 app.config.errorHandler 捕获到
- 开发时钩子:onRenderTracked(组件渲染过程中追踪到响应式依赖时调用)和 onRenderTriggered(当响应式依赖触发了组件渲染时调用)
- SSR 钩子:onServerPrefetch(注册一个异步函数,在组件实例在服务器上被渲染之前调用),SSR 渲染时,组件作为初始请求的一部分被渲染,这时可以在服务器上预请求数据,因为它比在客户端上更快。
- keepAlive 组件下的钩子:onActivated 和 onDeactivated 两个,用于当组件激活和失活时调用
# Vue 的响应式原理
Vue 内部维护了一个响应式桶结构,为:WeakMap-Map-Set 的模型,依次对应:对象 - 属性 - 副作用函数集合,vue3 采用 Proxy 对对象进行代理,getter 时就收集对应的副作用函数到桶结构中,setter 时,就会将桶结构取出并遍历执行。
# Vue 的任务调度
Vue 的任务调度使用 Promise,将任务放在微任务中去执行,低优先级任务会被存储到 pendingPreFlushCbs 队列中,在微任务中被遍历执行。
# Vue 的运行时
运行时包含:
- h 函数:用于根据组件或标签生成 vnode
- patchProp 函数:给 vnode 绑定属性和方法
- render 函数:根据 vnode 生成真实 dom 并渲染到页面上
- path 函数:进行双端的 diff 算法(最长递增子序列)
vue 的运行时还存在很多优化:
# 1、静态提升
静态的(不存在动态变化)的 template 提升到父组件进行缓存。
# 2、预字符串化
没有变化的 template 直接缓存成 render 函数字符串。
# 3、缓存事件处理函数
事件处理函数不会变化,直接缓存为字符串。
# 4、Block Tree
将 v-if 和 v-for 等封装到一个单独的 block 中,尽量避免影响到别的节点。
# 5、树结构打平
vue3 中每一块都会追踪其所有带更新类型标记的后代节点,动态内容表示会更新,静态内容表示不会更新,当虚拟 DOM 树需要进行更新时,就会将当前虚拟 DOM 分块打平为一个数组,数组中仅包含动态的节点而会跳过静态节点的比对,从而提升效率
# 6 、patchFlag
静态(不存在动态变化)的 template 进行标识,diff 算法直接跳过。
# Vue 的编译时
编译时就是将 template 模板转为运行时的代码
Vue 的编译时将我们书写的 template 模板编译为对应的 render 函数,这个过程在工程化环境中是由 vue-loader 完成的,这也是为什么 vue 用声明式的编程方式开发的代码性能并不差于命令式框架的原因,它利用工程化将编译前置了。此外,这非常利用 vite 的热更新机制,因为需要更新的模块是一个 render 函数,进行执行即可完成更新
# 转换过程
将 template 模板转为 render 函数。这个过程中共经历了三大步骤:1、词法分析(解析 template 模板,生成 AST 抽象语法树);2、语法分析(转化 AST,生成 JavaScript AST);3、中间代码生成(生成 render 函数)
# Vue 和 React 比对
# 使用不同
vue 当中使用烤串写法来区分组件和原生 dom, 而在 react 当中使用驼峰写法来进行区分,并且 vue 当中 class 在 react 当中要写为 className
- 原因就是因为:vue 的 parser 是类似 xml 的 parser,xml 是大小写不敏感的,而 react 的 parser 是 jsdom 的 parser, 是大小写敏感的,由于 class 是 js 中的关键字,所以在 jsdom 中,使用 className 来对照到 dom 的 class 属性
# setup 和 Hooks
- React Hooks 在组件每次更新时,如果不做优化就会重新调用,这也带来一些性能问题
- Hooks 有严格的调用顺序,并且不能写在条件分支中,还必须要写在 react 组件里面
- 昂贵的计算需要使用 useMemo, 也需要传入正确的数组
- 要解决变量闭包导致的问题,再结合并发功能,使得很难推理出一段钩子代码是什么时候运行的,并且很不好处理需要在多次渲染间保持引用的可变状态
# vue 缺少优先级调度渲染
vue 为什么不优先级调度渲染?
vue 的组件更新一旦开始就不能结束,使用 queueMicTask 在微任务中执行。
- react 更新是任务一起更新的,更新粒度较大,会存在阻塞 UI 渲染的问题,但 vue 由于更新粒度很小,所以不存在这个问题。
# react 缺少编译优化
react 为什么不进行 vue 的缓存等优化?
- 像 vue 这种静态类型的模板比较好优化(vue 的 vdom 是基于静态的 template,方便进行缓存和标记处理),但是像 react 这样的动态类型的编译就很难进行优化处理(react 的 vdom 是基于动态的 jsdom,不太好处理)。
# react 类组件和函数式组件
类组件的状态不利于状态的集中式管理与维护,函数式编程的方式使得代码的可维护性更强
类组件和函数式的比对实际上是:面向对象和函数式编程这两大编程思想的碰撞
函数式编程关心的是:需要做什么,而不是怎么去做,而面向对象关心的是:数据和对象
# 面向对象编程
完成某项任务关心的是:数据和对象
面向对象编程主要围绕着数据或者对象而不是功能和逻辑实现,他将关注点放在对于数据的操作方法,面向对象将数据和操作方法封装为一个类中,这样有利于代码的可复用性和可扩展性
面向对象编程的优点是:效率高 (符合现实世界)、容易维护 (结构清晰)、易扩展 (面向对象的程序往往高内聚而低耦合)、可重用 (得益于对象的继承)
面型对象编程的缺点是:过度的对象化、状态过于共享导致推理复杂、状态共享导致的并发问题 (可变状态复杂的共享机制导致面向对象的代码几乎不可能并行化,需要复杂的线程锁定、互斥等机制)、消耗内存、性能低 (会创建很多的类和实例)
# 面向对象编程三大特点:
- 封装
封装意味着所有的数据和方法都被封装在对象内,由开发者自己选择性的去公开哪些属性和方法,对于创建的实例来说他能访问的只能是这些公开的属性和方法,而对于其他对象来说是无权访问此类或者进行更改,封装这一特性为程序提供了更高的安全性。
- 继承
继承意味着代码的可重用性,子类和父类这两个概念就是很好的体现,子类拥有父类所有的属性和方法避免数据和方法的重复定义,同时也能够保持独特的层析结构,继承这一特性为程序提供了可重用性。
- 多态
- 多态意味着设计对象以共享行为,使用继承子类可以用新的方法去覆盖父类共享的行为,多态性允许同一个方法执行两种不同的行为:覆盖和重载。
# 函数式编程
完成某项任务关心的是:需要做什么,而不是怎么去做
函数式编程又称声明式编程,最明显的特点就是我们不太关心函数的具体实现,而只关心自己的业务逻辑线路
函数式编程的优点是:代码可读性强、有一定的逻辑复用能力、并发速度快、出错率少易排查;
函数式编程的缺点是:性能消耗大 (主要是创建执行上下文的消耗) 和 资源占用大 (数据不可变导致要创建很多重复的对象), 同时不利于实现时间旅行等操作 (状态很难回滚)
# 函数式编程三大特点:
- 函数是一等公民:在 JS 中函数和其他数据类型一样处于平等地位,可以作为变量赋值给其他变量,并且可以作为参数和返回值
- 声明式编程:函数式编程又称声明式编程,我们不太关心函数内部的具体实现,而是关心业务逻辑的执行流程
- 纯函数:纯函数特点:无副作用、引用透明 和 数据不可变
- 无副作用:本身不会依赖和修改外部变量
- 引用透明:输入相同的值一定会得到相同的结果
- 数据不可变:针对引用数据类型的入参,最好的方式是重新生成一份数据