找工作中,有公司招人吗?chrislol199810@gmail.com
问题 总流程
-
xzc 执行组件函数生成jsx,再生成fiber,通过fiber生成dom。
-
xzc 当我们setState的时候,会创建一个更新,然后重头遍历fiber树进行重渲染,重新执行函数组件,在useState里面会把对应车道的更新执行,拿到最新的值来生成新的jsx当前页面对应的fiber进行domDiff来最大程度复用fiber节点还有标记一些副作用
-
xzc 最后在dom上应用这些副作用。
-
xzc 在遍历fiber过程中使用时间切片,浏览器每一帧给5ms时间让组件重渲染,这样在组件渲染的过程中还可以进行页面的交互。
ReactDOM.createRoot(document.getElementById("root")).render(<App/>)
function App() {
return <Parent></Parent>
}
__jsx(Parent /**type*/, {}/**config*/); {
const props = {};
for (propName in config) {
props[propName] = config[propName]
}
return ReactElement(type, key, ref, props); {
return {
$$typeof: REACT_ELEMENT_TYPE/**Symbol.for('react.element')*/,
type,
key,
ref,
props
}
}
}
export function createRoot(container/**document.getElementById("root")*/) {
//react 创建了第一颗fiber树
const root = createContainer(container);
return new ReactDOMRoot(root);
}
ReactDOMRoot.prototype.render = function (children) {
const root = this._internalRoot;
updateContainer(children/**element*/, root/**container*/); {
const current = container.current;
const update = createUpdate(lane);
update.payload = { element };
const root = enqueueUpdate(current, update, lane);
scheduleUpdateOnFiber(root, current, lane, eventTime);
}
};
let workInProgress = null;
export function scheduleUpdateOnFiber(root, fiber, lane, eventTime) {
ensureRootIsScheduled(root, eventTime/**currentTime*/); {
newCallbackNode = Scheduler_scheduleCallback(schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root)); { // performConcurrentWorkOnRoot 内容
const exitStatus = shouldTimeSlice ? renderRootConcurrent(root, lanes) : renderRootSync(root, lanes); { // renderRootConcurrent 内容
prepareFreshStack(root, lanes); { // 构建新的 fiber 头结点
// react 复用 current fiber
workInProgress = createWorkInProgress(root.current, null); {
let workInProgress = current.alternate;
if (workInProgress === null) {
workInProgress = createFiber(current.tag, pendingProps, current.key);
workInProgress.alternate = current;
current.alternate = workInProgress;
}
workInProgress.child = current.child;
}
}
workLoopConcurrent(); {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress/**unitOfWork*/); {
const current = unitOfWork.alternate;
const next = beginWork(current, unitOfWork, workInProgressRootRenderLanes);
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
}
}
}
}
if (exitStatus !== RootInProgress) {
commitRoot(root);
}
}
}
}
问题 子组件更新,父组件和兄弟组件不会重渲染,所有孙子组件都会重渲染
- xzc 1.在当前组件内setState,会请求一个车道给当前的fiber,然后从根fiber开始新一次更新
- xzc 2.每个组件都会走到beginWork里面,他会判断当前组件的属性和车道,来决定是否重渲染组件
- xzc 3.对于父组件,他的props没变,所以不会重渲染
- xzc 4.对于当前组件,由于他有车道,所以会重渲染。他一重渲染。就是产生出新的jsx,jsx的子组件的props都变了,所以所有子组件也都会重渲染。
function beginWork() {
// hasContextChanged暂时不清楚什么作用
if (oldProps !== newProps || hasContextChanged()) {
} else {
var hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(current, renderLanes);
if (!hasScheduledUpdateOrContext && (workInProgress.flags & DidCapture) === NoFlags/*跟错误边界有关好像*/) {
didReceiveUpdate = false;
return attemptEarlyBailoutIfNoScheduledUpdate(current, workInProgress, renderLanes); {
bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
}
}
}
问题 挂载阶段和更新阶段是什么?
- xzc 粒度在组件上,如果这个组件还没有fiber,那么他就是挂载阶段,执行mountEffect。如果是已经有fiber,那么他就是更新阶段,执行updateEffect。
问题 为什么不能在条件循环语句写 hook updateWorkInProgressHook mountWorkInProgressHook
- xzc 每一个组件对应一个fiber,他的memoizedState.hook存储了一条链表,挂载的时候,我们调用useEffect或者useState的时候,都会从链表头开始按顺序创建一个hook对象。之后重渲染就会按顺序去拿,如果用条件语句导致useEffect拿到了useState的hook,那肯定出问题了。
- xzc 而且每一个hook保存的东西不一样,比如useState存的是计算后的值【memoizedState】,而useEffect存放了要执行的effect,他们要要进行的逻辑也不一样。
updateState/updateEffect
updateWorkInProgressHook
问题 12.函数组件和类组件, 用的 hooks 多一点 还是 class 多一点?
- xzc 新项目全部都用函数组件,class可能只在维护老项目用到
问题 useEffect 中调用 setState 的闭包问题
zhuanlan.zhihu.com/p/509036942
- xzc React的更新原理就是通过setState触发函数组件重渲染,重新执行useState拿到的返回值才是最新的,用它来得到新的jsx,来构建新的fiber树。
- xzc 如果hook的依赖漏传了,导致hook没有随着组件的重渲染重新执行,那么他拿到的肯定就是上一次组件函数执行时的值。
- xzc 如果想要拿到最新渲染之后的值,ahooks 里面有一个useLatest。就是把useState返回的最新值给到ref.current来保存。其实就是用了ref永远都是组件函数第一次渲染时候的值,所以不管函数有没有重新创建,他每次拿到的都是同一个ref。
const [count, setCount] = useState(0);
const latestCountRef = useLatest(count);
function useLatest(value) {
const ref = useRef(value);
ref.current = value;
return ref;
}
问题 React 的错误边界有了解吗? 什么业务场景下,会用到错误边界,它用在哪个地方会比较好?
- xzc 可以捕获useEffect、setState和子组件这些react的周边抛出的错误,把出错的部分渲染一个代替的ui,而不让整个项目奔溃
const useThrowAsyncError = () => {
const [state, setState] = useState();
return (error) => {
setState(() => throw error)
}
}
class ErrorBoundary extends React.Component {
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, info) {
logErrorToMyService(error, info.componentStack);
}
render() {
if (this.state.hasError) {
return this.props.fallback;
}
return this.props.children;
}
}
function App() {
return <ErrorBoundary fallback={<p>Something went wrong</p>}>
<Profile />
</ErrorBoundary>
}
问题 9.纯函数,高阶函数,高阶组件
- xzc 纯函数相同输入就会有相同的输出,没有副作用,比如修改外部数据,发请求等等
- xzc 高阶函数和高阶组件其实就是拿函数和组件当参数或者返回值
问题 setState是同步还是异步
- xzc setState是一个同步函数,创建一个对应优先级的更新任务update推到并发队列,调用一下scheduleUpdateOnFiber进行组件重渲染,也就是我们说的这个render阶段。 // 这个scheduleUpdateOnFiber大部分会通过messageChannel创建一个宏任务。他会在浏览器下一帧从根fiber出发,把对应优先级和优先级比他低的全部更新任务执行,然后返回新的jsx跟现在的fiber进行domdiff,然后修改dom节点。
- xzc 这个render阶段根据我们执行setState的上下文不同,他可能是微任务、宏任务。比如在点击事件里面,他的车道优先级是最高的1,他会进行微任务赶快执行。其他大部分上下文的setState都是用的scheduler这个包来调度的,那么他在浏览器端是用到了messageChannel这个宏任务。如果我们用到了useTRansition,在这个上下文里面setSTate,不仅会宏任务执行,还会开启时间切片。
问题 优先级是什么?车道是什么?
React本质上就是我们通过setState会给这个组件的fiber设置了一个带有优先级的更新,触发一个对应优先级的重渲染调度,从根fiber出发,把所有组件里面的这些优先级跟setState相对或者比他小的更新全部计算拿到新的值,通过新的值拿到新的jsx,就可以通过这个jsx来修改dom节点。
-
xzc REACT一共有4种优先级,从高到低是离散事件优先级、连续事件、默认事件、空闲事件。我们在不同上下文里面setState,会设置不同优先级的更新,点击事件里面就是离散事件优先级的更新,他的优先级最高是1,会把当前的调度任务放到一个同步队列里面,然后再微任务里面把所有同步调度一次执行。比如useEffect里面就是默认优先级的更新16,他就是使用了scheduler来调度进行重渲染,里面用到了messageChannel宏任务。
-
xzc 因为4种优先级不够用,所以用31条车道划成了4个部分。
问题 不同优先级对比
-
xzc 1 除了同步车道是微任务,同步重渲染所有组件
-
xzc 2 4 8 16 其他大部分都是宏任务,默认车道不进行时间切片
-
xzc 比如用useTransition请求一个车道就会开启时间切片,组件树渲染变成可中断,让用户在渲染过程中还可以跟浏览器交互
function dispatchSetState(fiber, queue, action) {
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
scheduleUpdateOnFiber(root, fiber, lane, eventTime); {
ensureRootIsScheduled(root, eventTime); {
if (newCallbackPriority === SyncLane) {
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
queueMicrotask(flushSyncCallbacks);
} else {
let schedulerPriorityLevel;
switch (lanesToEventPriority(nextLanes)) {
case DiscreteEventPriority:
schedulerPriorityLevel = ImmediateSchedulerPriority;
break;
case ContinuousEventPriority:
schedulerPriorityLevel = UserBlockingSchedulerPriority;
break;
case DefaultEventPriority:
schedulerPriorityLevel = NormalSchedulerPriority;
break;
case IdleEventPriority:
schedulerPriorityLevel = IdleSchedulerPriority;
break;
default:
schedulerPriorityLevel = NormalSchedulerPriority;
break;
}
newCallbackNode = Scheduler_scheduleCallback(schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root)); {
}
}
}
}
}
function prepareFreshStack(root, renderLanes) {
finishQueueingConcurrentUpdates(); // 把 update 放入对应 fiber.memoizedState(hook).queue.update 中
}
问题 why messageChannel? why not requestidlecallback
- xzc 在客户端react调度默认用的是messageChannel,他的优先级比定时器和requestAnimationFrame还高。requestidlecallback在每一帧有剩余时间才执行,不太稳定
- xzc 如果在node的话就用定时器
requestAnimationFrame 会在每一帧重绘前执行一次。
问题 useLayoutEffect useEffect 区别
- xzc 在重渲染完成后。他会执行commitRoot来根据fiber树增删查改dom节点嘛。
- xzc useEffect也是通过scheduler进行调度,也就是用messageChannel宏任务。
- xzc 对应useLayoutEffect,他是在commitMutationEffectsOnFiber之后就同步执行的。也就是在dom节点增删查改并插入页面之后执行的,所以他能访问到修改后的dom节点。所以渲染引擎得等到useLayout执行完之后才能实际去渲染dom节点。
//
- xzc 虽然dom树已经调用了插入页面的api了,但是下一步执行的清空LayoutEffect是同步代码嘛,所以dom节点还得等你代码执行完才能插入页面,所以阻塞到了gui引擎执行,所以这是他不会页面闪烁的原因。
function commitRootImpl(root) {
// useEffect
Scheduler_scheduleCallback(NormalSchedulerPriority, flushPassiveEffect); {
commitPassiveUnmountEffects(root.current);
commitPassiveMountEffects(root, root.current);
}
commitMutationEffectsOnFiber(finishedWork, root); // 先处理所有节点的突变,构建dom树,挂载到页面上
// useLayoutEffect
commitLayoutEffects(finishedWork, root); // useLayoutEffect同步的,可以操作修改后的dom节点。调用时机是在DOM更新完成后,但是页面渲染之前(虽然已经挂载了,但是js代码还在执行,所以gui引擎还没进行页面渲染),不会造成页面闪烁
}
问题 useCallback 和 useMemo 用过吗? 区别是?
- xzc useCallback缓存函数,useMemo缓存大数据的计算和值的。和react.memo配合来减少重渲染
- xzc 封裝自定义hook,如果要返回函数或者值,就要用到缓存,别人用的时候可能要加在依赖数组里面
问题 useContext 哪个场景会用到它,怎么优化
- xzc 一些全局的不怎么变的配置比如主题色、语言包、css前缀,可以用他来保存。但要注意一下使用了useContext会导致memo无效。
- xzc 如果要比较细粒度的控制组件的重渲染的话,不要直接把数据存储在context里面。可以用redux存储,provider只存储操作数据的方法。或者有一些比如useContextSelector这些东西,其实也是更redux类似的思路,只用provider存api,他的value是不变的,我们自己实现他的更新逻辑。
- xzc 分成多个provider,把一些变化的属性单独拆分出一个provider.之后改变这些属性只会对用到这个context的组件进行重渲染
问题 useContext原理
zhuanlan.zhihu.com/p/637515015 juejin.cn/post/724483…
-
xzc 一般我们都是通过setState修改provider的value值,只要value变了,用到了对应的useContext的所有子组件都会重渲染,用react.memo包裹也没用。
-
xzc 当我们在组件里面用到了useContext,他会把context保存到当前组件fiber的dependencies。
-
xzc 只要provider组件重渲染,并且value变了,他会遍历所有的子组件,判断子组件的dependencies有没有这个provider对应的context组件,如果有,这个子组件的fiber.lanes会标记一个车道。
-
xzc 如果子组件有车道,那么memo组件的重组渲染就不会bailout,还是返回子组件进行重渲染【bailout:在工作循环处理fiber的时候,子fiber返回null,就是不处理子fiber保留原样】
-
xzc 当我们执行到这个子组件的beginWork的时候,由于他有车道,所以会触发他的重渲染逻辑。
TODO React 性能优化手段 legacy.reactjs.org/docs/optimi…
-
xzc useCallback缓存函数,useMemo缓存大数据的计算和值的。和react.memo配合来减少重渲染
-
xzc React.lazy和suspense懒加载路由
-
xzc 如果某一个组件节点比较多或者计算量大,可以用useTransition申请一个车道。可以让这个组件的渲染变成可中断。不会阻塞ui的交互
-
xzc 如果要比较细粒度的控制组件的重渲染的话,不要直接把数据存储在context里面。可以用redux存储。
-
xzc 多用一下react.fragment让标签结构好看一点
问题 react17之前jsx文件为什么要声明import React from 'react',之后为什么不需要了
- xzc 有一个preset-react的预设,之前编译成react.createElement,得自己映入react。现在默认编译成jsx并且自动引入jsx函数。
@babel/preset-react
runtime.classic/automatic
自动引入 @babel/plugin-transform-react-jsx
问题 子组件的 effect 先执行还是父组件 recursivelyTraverseLayoutEffects
- xzc 重渲染完成后他会执行commitRoot,里面会执行一个flushPassiveEffect,从根fiber深度优先遍历里到子组件,先执行子组件的effect,再往上执行父组件的effect
function flushPassiveEffect() {
commitPassiveMountEffects(root, root.current); {
recursivelyTraversePassiveMountEffects()
commitHookPassiveMountEffects() // effect.destroy = create()
}
}
问题 hook不传依赖会重新计算,传空数组不会重新计算
- xzc 他里面有一个函数判断是否要重计算,如果不传依赖他会将undefined转成null,依赖是null就重计算。如果数组的每一项都相等也要重计算,如果是空数组不用重计算。
function areHookInputsEqual(nextDeps, prevDeps) {
if (prevDeps === null) {
return false;
}
for (var i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
if (objectIs(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
return true;
}
问题 不同事件中的setState为什么有不同的优先级
dispatchDiscreteEvent; // 离散 点击
const previousPriority = getCurrentUpdatePriority();
setCurrentUpdatePriority(DiscreteEventPriority); // 设置优先级1
dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent); // 执行 dispatchSetState
setCurrentUpdatePriority(previousPriority);
dispatchSetState
const lane = requestUpdateLane() // 拿到的优先级1
ensureRootIsScheduled
const nextLanes = getNextLanes(root, workInProgressRootRenderLanes); // 获取当前优先级最高的车道
let newCallbackPriority = getHighestPriorityLane(nextLanes); //获取新的调度优先级
if (newCallbackPriority === SyncLane) {
//先把performSyncWorkOnRoot添回到同步队列中
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
//再把flushSyncCallbacks放入微任务
queueMicrotask(flushSyncCallbacks);
//如果是同步执行的话
newCallbackNode = null;
}
问题 react的路由有几种模式,项目主要用哪个路由模式
- xzc hash(hashchange) 和 history (replaceState、pushState)
- xzc 用hash简单一点。history刷新页面的话会发请求,可能在nginx配置如果没有匹配到路径的话返回首页
TODO 比如说封装过哪些 hooks ? ok,你可以写几个平时封装过的hooks吗?
// 大概懂为什么用useLatest?不要每次fn变化都执行一次useEFfect
const useUnmount = fn => {
const fnRef = useLatest(fn);
useEffect(() => () => fnRef.current(), []);
};
// 解决闭包问题 其实就是用ref来保存和更新值,ref每次渲染的的都是最开始的值。
function useLatest(val) {
const ref = useRef(val)
ref.current = val
return ref;
}
function useMount(fn) {
useEffect(() => {
fn?.()
}, [])
}
//例子 https://juejin.cn/post/6963159418640793614#heading-5
// 組件卸載的時候執行()=> clearTimeout(timer) 還是每次重新渲染的時候執行()=> clearTimeout(timer) 答:每次重渲染的时候,重渲染就是函数组件的卸载
//不懂 函数组件卸载(应该就是标记了delete)和重渲染都会触发destroy(),为什么卸载的时候也会?
function useTimeout(callback, delay) {
const memorizeCallback = useLatest(callback)
useEffect(() => {
if (delay !== null) {
const timer = setTimeout(() => {
memorizeCallback.current();
}, delay);
return () => {
clearTimeout(timer);
};
}
}, [delay]);
};
// 区别
function useTimeout(callback, delay) {
useEffect(() => {
if (delay !== null) {
const timer = setTimeout(() => {
callback();
}, delay);
return () => {
clearTimeout(timer);
};
}
}, [delay, callback]);
};
function useRequestImplement(service, options = {}, plugins = []) {
const serviceRef = useLatest(service);
const update = useUpdate(); // 重渲染
const fetchInstance = useCreation(() => {
const initState = plugins.map((p) => p?.onInit?.(fetchOptions)).filter(Boolean);
return new Fetch(serviceRef, fetchOptions, update, Object.assign({}, ...initState));
}, []);
useMount(() => {
if (!manual) {
const params = fetchInstance.state.params || options.defaultParams || [];
fetchInstance.run(...params);
}
});
useUnmount(() => {
fetchInstance.cancel();
});
return {
run: useMemoizedFn(fetchInstance.run.bind(fetchInstance)), /
};
}
问题 生命周期
cloud.tencent.com/developer/a… 只看两张图
问题 componentWillMount、componentWillReceiveProps、componentWillUpdate为什么标记UNSAFE
-
xzc React就是构建fiber树,然后应用到dom上。之后加了车道优先级,如果fiber树构建了一半,如果来了一个高优先级的任务,那么当前的构建会直接被丢掉。他会从根节点开始重新构建fiber树。这样就会导致组件多次render,这些render前的生命周期函数可能执行多次。
-
xzc getDerivedStateFromProps 在组件实例化、接收到新的 props 、组件状态更新时会被调用
-
xzc getSnapshotBeforeUpdate 在这个阶段我们可以拿到上一个状态 Dom 元素的坐标、大小的等相关信息。用于替代旧的生命周期中的 componentWillUpdate。该函数的返回值将会作为 componentDidUpdate 的第三个参数出现。
问题 副作用冒泡,选择执行
- xzc 每一个fiber节点都会有一个subLanes标记子节点的副作用,可以减少遍历的fiber节点,相当于剪枝操作
问题 ssr原理 神说要有光
TODO 介绍 ReactDOMDIFF 算法
问题 能解释一下 React 虚拟DOM的工作原理:React 虚拟DOM 对性能方面有什么帮助?
问题 对 React Fiber 的理解,为什么要fiber? jsx和fiber的关系? 说说virtual Dom的理解
问题 【react元素$$typeof属性什么】【react怎么通过dom元素,找到与之对应的 fiber对象的? 答:通过internalInstanceKey对应】
问题【不太懂】双缓存 react.iamkasong.com/process/dou…
zhufengpeixun.com/strong/html… domdiff例子
-
xzc 为什么dom-diff?【zhuanlan.zhihu.com/p/344702969… 当我setState根据新数据产生出新的jsx的时候,我们需要对比新产生的jsx和当前页面的fiber,来创建新的fiber树嘛。正常原地对比修改两颗树的节点复杂度是On3。所以react改造了fiber树的结构,fiber有一个sibling属性指向了他的兄弟节点,形成了一条链表。通过遍历jsx数组,然后只和同层级的fiber链表进行domdiff,让复杂度变成了On。 // react把对比和修改放到两个阶段执行,在render阶段他只遍历fiber节点而不修改,只是标记了副作用,在commit节点才会实际修改dom节点。所以render阶段他的复杂度其实就是节点的个数。因为整个性能损耗其实就是在我们render阶段去创建或者修改fiber嘛,commit阶段其实只是根据副作用插入和删除dom节点。他还通过domdiff去最大程度的复用旧fiber。
-
xzc 第一次循环,同步遍历jsx数组和页面当前的对应fiber链表,创建一条新的fiber链表,他会判断key和标签名,看看能不能复用。【这也是为什么fiber也要有一个sibling指针指向他兄弟节点的原因,这样他也能遍历到它的兄弟节点】
-
xzc 如果老fiber还有,把剩下的老fiber标记删除。如果jsx还有,把剩下的jsx标记插入。
-
xzc 如果jsx和fiber都有剩余的话,把剩下fiber的放入map中。遍历剩下的jsx,在map中找key和标签名相同的fiber,没有的话直接创建并标记插入副作用。如果相同可以复用则判断是否要移动fiber,比对当前这个复用fiber跟上一个复用fiber的相对位置,如果在他的前面,那么就需要移动了,移动其实就是标记插入。复用了就会把fiber从map中去掉。最后把map中剩下的fiber全部标记删除并存起来【fiber.deletions】。在commit阶段将这些副作用应用到实际的dom节点上
//
最开始没有fiber只有虚拟dom组件是全量渲染。用了fiber之后让react变成可中断的,细粒度到组件渲染嘛,每一帧会给浏览器请求5ms来渲染组件,如果超过了5ms,他会把控制权给到浏览器,等浏览器空闲的时候再次从下一个组件开始渲染。
Fiber双缓存可以在构建好wip Fiber树之后切换成current Fiber,内存中直接一次性切换,提高了性能
Fiber可以在reconcile的时候进行相应的diff更新,让最后一次性更新应用在真实节点上,避免直接去操作dom节点
//
element.$$typeof jsx的类型
fiber.tag fiber的类型
React.createElement函数返回的就是虚拟dom,用js对象描述真实dom的js对象
mount时通过jsx对象(调用createElement的结果)调用createFiberFromElement生成Fiber,update时通过reconcileChildFibers或reconcileChildrenArray对比新jsx和老的Fiber(current Fiber)生成新的wip Fiber树
问题 scheduler
-
xzc 创建一个任务,根据过期时间放到任务队列里面【不同优先级有不同的过期时间】。通过messageChannel调度一个宏任务。拿到任务队列的堆顶,如果没过期或者超过5ms退出本次工作循环重新调度一个在下一帧执行。如果过期了就开始执行这个任务,如果任务没有返回值,这个任务就从堆中去掉,然后再拿一个堆顶的任务来执行。
-
xzc 如果这个函数的返回值是一个函数,就退出本次的循环,这个任务保留,重新开始调度一个新的循环。比如我们用useTransition开启一个时间切片的重渲染,超过5ms他就会停止渲染返回一个函数。scheduler的任务就会保留,重新调度一个宏任务,等一下又从堆顶把这个重渲染拿出来,从中断的fiber开始重渲染。
-
xzc 他还有一个延时任务,但是react没用到。除非设置了delay让开始时间大于当前时间。
// react中用到的地方: useEffect,重渲染
unstable_scheduleCallback(任意优先级, flushPassiveEffects) 或者 unstable_scheduleCallback(任意优先级,performConcurrentWorkOnRoot)
const taskQueue = []; // react //任务的最小堆 [flushPassiveEffects(),performConcurrentWorkOnRoot.bind(null, root),performConcurrentWorkOnRoot.bind(null, root),flushPassiveEffects()]
const channel = new MessageChannel();
const port = channel.port2;
function getCurrentTime() {
return performance.now();
}
const frameInterval = 5;
function shouldYieldToHost() {
const timeElapsed = getCurrentTime() - startTime;
if (timeElapsed < frameInterval) {
return false;
}
return true;
}
// step1
function unstable_scheduleCallback(priorityLevel, callback, options) {
var currentTime = getCurrentTime();
var startTime;
if (typeof options === 'object' && options !== null) {
var delay = options.delay;
if (typeof delay === 'number' && delay > 0) {
startTime = currentTime + delay;
} else {
startTime = currentTime;
}
} else {
startTime = currentTime;
}
var timeout;
switch (priorityLevel) {
case ImmediatePriority:
timeout = -1;
break;
case UserBlockingPriority:
timeout = userBlockingPriorityTimeout;
break;
case IdlePriority:
timeout = maxSigned31BitInt;
break;
case LowPriority:
timeout = lowPriorityTimeout;
break;
case NormalPriority:
default:
timeout = normalPriorityTimeout;
break;
}
var expirationTime = startTime + timeout;
var newTask = {
id: taskIdCounter++,
callback,
priorityLevel,
startTime,
expirationTime,
sortIndex: -1,
};
//react 没过期,放入延时队列
if (startTime > currentTime) {
newTask.sortIndex = startTime;
push(timerQueue, newTask);
if (peek(taskQueue) === null && newTask === peek(timerQueue)) { //问题 全部都是延时任务,并且新任务是堆顶 newTask === peek(timerQueue)为什么要加这个判断条件
if (isHostTimeoutScheduled) {
cancelHostTimeout();
} else {
isHostTimeoutScheduled = true;
}
requestHostTimeout(handleTimeout/*callback*/, startTime - currentTime/*ms*/); {
taskTimeoutID = localSetTimeout(() => {
callback(getCurrentTime()/*currentTime*/); {
isHostTimeoutScheduled = false;
advanceTimers(currentTime); { //react 把所有过期任务的放到任务队列
let timer = peek(timerQueue);
while (timer !== null) {
if (timer.callback === null) {
pop(timerQueue);
} else if (timer.startTime <= currentTime) {
pop(timerQueue);
timer.sortIndex = timer.expirationTime;
push(taskQueue, timer);
} else {
return;
}
timer = peek(timerQueue);
}
}
if (!isHostCallbackScheduled) {
if (peek(taskQueue) !== null) {
isHostCallbackScheduled = true;
requestHostCallback();
} else {
const firstTimer = peek(timerQueue);
if (firstTimer !== null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
}
}
}
}, ms);
}
}
} else {
newTask.sortIndex = expirationTime;
push(taskQueue, newTask);
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
requestHostCallback(); {
if (!isMessageLoopRunning) {
isMessageLoopRunning = true;
schedulePerformWorkUntilDeadline(); {
port.postMessage(null);
}
}
}
}
}
return newTask;
}
// step2
channel.port1.onmessage = performWorkUntilDeadline; {
if (isMessageLoopRunning) {
const currentTime = getCurrentTime();
startTime = currentTime;
let hasMoreWork = true;
try {
hasMoreWork = flushWork(currentTime/*initialTime*/); {
isHostCallbackScheduled = false;
if (isHostTimeoutScheduled) {
isHostTimeoutScheduled = false;
cancelHostTimeout();
}
isPerformingWork = true;
const previousPriorityLevel = currentPriorityLevel;
try {
hasMoreWork = return workLoop(initialTime) {
let currentTime = initialTime;
advanceTimers(currentTime);
currentTask = peek(taskQueue);
while (currentTask !== null &&!(enableSchedulerDebugging && isSchedulerPaused)) {
if (currentTask.expirationTime > currentTime && shouldYieldToHost()) { // react 任务过期,执行。 任务不过期,看看要不要控制权给浏览器。
break;
}
const callback = currentTask.callback;
if (typeof callback === 'function') {
currentTask.callback = null;
currentPriorityLevel = currentTask.priorityLevel;
const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
const continuationCallback = callback(didUserCallbackTimeout);
currentTime = getCurrentTime();
if (typeof continuationCallback === 'function') { // react 函数返回了函数,重新执行
currentTask.callback = continuationCallback;
advanceTimers(currentTime);
return true;
} else {
if (currentTask === peek(taskQueue)) {
pop(taskQueue);
}
advanceTimers(currentTime);
}
} else {
pop(taskQueue);
}
currentTask = peek(taskQueue);
}
if (currentTask !== null) {
return true;
} else {
const firstTimer = peek(timerQueue);
if (firstTimer !== null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
return false;
}
}
} finally {
currentTask = null;
currentPriorityLevel = previousPriorityLevel;
isPerformingWork = false;
}
}
} finally {
if (hasMoreWork) {
schedulePerformWorkUntilDeadline();
} else {
isMessageLoopRunning = false;
}
}
}
needsPaint = false;
}
问题 并发
//问题 什么是并发
同一时间多个任务在执行 performConcurrentWorkOnRoot[高优打断低优]
问题 饥饿问题
每次setState都会创建一个update,并创建一条对应优先级的车道,然后重新进行重渲染,但是他只会执行当前优先级和比他高的setState。如果低优先级的重渲染一直被打断,那么这些低优先级的setState就一直不会执行。如果之后重新通过setState创建了这一条低优先级的车道,并且发现过期了【他会在每次调度开始后,都会去添加检查有没有过期的车道,如果有会保存起来。】他不会开启时间切片,也就是他会同步执行,所以不会被打断了。
之前17版本如果有过期车道和高优先级车道,他会优先拿过期车道来执行。现在去掉了这个逻辑,现在如果如果我们用useTransition开启了时间切片的任务,如果不断被高优先级打断导致过期了,之后等高优先级执行完,轮到这个任务他会把时间切片关闭。
// 全局搜
// let bCounter = 0;
//搜索
// ensureRootIsScheduled(root);
// return getContinuationForRoot(root, originalCallbackNode);
//不懂 过期了会不会重新执行
function FiberRootNode(containerInfo) {
this.expirationTimes = createLaneMap(NoTimestamp); //过期时间 存放每个赛道过期时间
this.expiredLanes = NoLanes; //过期的赛道
}
export function scheduleUpdateOnFiber(root, fiber, lane, eventTime) {
ensureRootIsScheduled(root, eventTime); {
markStarvedLanesAsExpired(root, currentTime); {
root.expiredLanes |= lane;
}
const nextLanes = getNextLanes(root, workInProgressRootRenderLanes);
newCallbackNode = Scheduler_scheduleCallback(schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root)); {
const nonIncludesExpiredLane = !includesExpiredLane(root, lanes); {
return (lanes & root.expiredLanes) !== NoLanes;
}
const shouldTimeSlice = nonIncludesBlockingLane && nonIncludesExpiredLane && nonTimeout;
const exitStatus = shouldTimeSlice ? renderRootConcurrent(root, lanes) : renderRootSync(root, lanes);
ensureRootIsScheduled(root, now());
if (root.callbackNode === originalCallbackNode) {
return performConcurrentWorkOnRoot.bind(null, root);
}
}
}
}
问题 为什么fiber中断后能保持state计算顺序?不同优先级的更新中断之后如何保证数据正确?
-
xzc setState会创建一个带有车道的update并放到链表里面,然后进行重渲染,重新执行组件函数,重新执行useState,通过这条update链表,计算出最新的值返回出来,然后渲染出新的jsx。但是他只会计算链表里面相同优先级和比他高的。优先级低的他会保存起来【hook.baseQueue】。等到下一个低优先级的setState执行,他会把保存的链表和新的update连起来执行,这样就能通过链表来保存这些更新的计算顺序。
-
xzc hook的pending存了update,计算过后就会置为null丢掉。
function updateState(initialState) {
return updateReducer(baseStateReducer, initialState);
}
function baseStateReducer(state, action) {
return typeof action === 'function' ? action(state) : action;
}
function updateReducer(reducer) {
const pendingQueue = queue.pending; //把新旧更新链表合并
if (pendingQueue !== null) { // pendingQueue => enqueueUpdate + finishQueueingConcurrentUpdates(为什么要多一个队列)
if (baseQueue !== null) { // baseQueue(跳过的update -> update -> update -> 头部update)
const baseFirst = baseQueue.next;
const pendingFirst = pendingQueue.next;
baseQueue.next = pendingFirst;
pendingQueue.next = baseFirst;
}
current.baseQueue = baseQueue = pendingQueue;
queue.pending = null;
}
const shouldSkipUpdate = !isSubsetOfLanes(renderLanes, updateLane); // 只执行优先级相同或者更高的update,其他跳过
if (shouldSkipUpdate) {
const clone = {};
if (newBaseQueueLast === null) {
newBaseQueueFirst = newBaseQueueLast = clone;
newBaseState = newState;
} else {
newBaseQueueLast = newBaseQueueLast.next = clone;
}
currentlyRenderingFiber.lanes = mergeLanes(currentlyRenderingFiber.lanes, updateLane);
} else {
if (newBaseQueueLast !== null) {
const clone = {};
newBaseQueueLast = newBaseQueueLast.next = clone;
}
if (update.hasEagerState) {
newState = update.eagerState; // 优化: 如果有eagerState,不用计算直接赋值
} else {
const action = update.action;
newState = reducer(newState, action);
}
}
hook.memoizedState = newState;
hook.baseState = newBaseState; //跳过更新前的计算过的state
hook.baseQueue = newBaseQueueLast;
}
function mountState(initialState) {
hook.memoizedState // 当前hook的真正状态
= hook.baseState // 跳过/中断更新之前的装填
= initialState;
const queue = {
pending: null,
dispatch: null,
lastRenderedReducer: baseStateReducer, // return typeof action === 'function' ? action(state) : action;
lastRenderedState: initialState, //上一个state
};
}
function dispatchSetState(fiber, queue, action) {
const lane = requestUpdateLane();
const update = {
lane,
action,
hasEagerState: false, //是否有急切的更新
eagerState: null, //急切的更新状态
next: null,
};
const alternate = fiber.alternate;
if (fiber.lanes === NoLanes && (alternate === null || alternate.lanes == NoLanes)) { // 只有第一次更新才能这么做,不然会出错
const { lastRenderedReducer, lastRenderedState } = queue;
const eagerState = lastRenderedReducer(lastRenderedState, action); // 优化:由上一次state和这次的action算出现在的值,如果和上一次相同,不进行调度
update.hasEagerState = true;
update.eagerState = eagerState;
if (Object.is(eagerState, lastRenderedState)) {
return;
}
}
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
const eventTime = requestEventTime();
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
}
//问题 【时间切片/fiber增量渲染】VS 高优打断低优更新
//TODO 时间切片,fiber中断后从下一个组件执行。因为workInProgress没变
if (root !== workInProgressRoot || workInProgressRootRenderLanes !== renderLanes) {
prepareFreshStack(root, renderLanes);
}
如果不是同一个根节点,或者渲染优先级不同,会重置workInProgress。如果没重置这个workInProgress就能继续往下执行
//TODO 高优打断低优更新fiber中断后,低优先级任务抛弃掉,从根节点重新渲染。
被高优先级打断后,会cancelCallback 把 task.callback 也就是 performConcurrentWorkOnRoot 置为 null , 低优先级会完全抛弃。重新从根节点开始,而不是从当前节点开始【写一个demo】。
问题 setState批量更新
- xzc 在不同的上下文中,setState会触发不同优先级的渲染。在同一个上下文中如果setState多次,在render阶段,他有一个if语句,只会执行第一次的更新,之后的更新由于优先级相同都中断了。不同的上下文会产生一次render阶段,其实就是应用这些优先级和优先级比他高的更新产生出新的fiber树,所以每次只需要有一次对应优先级的render阶段就行了。
export function scheduleUpdateOnFiber(root, fiber, lane, eventTime) {
ensureRootIsScheduled(root, eventTime); {
const existingCallbackNode = root.callbackNode;
const nextLanes = getNextLanes(root, workInProgressRootRenderLanes);
let newCallbackPriority = getHighestPriorityLane(nextLanes); // 高优打断低优更新 获取当前优先级最高的车道,所以低优先级的不会打断高优先级
if (existingCallbackPriority === newCallbackPriority) return; // 批量更新,setState两次,第二次的ensureRootIsScheduled直接返回
if (existingCallbackNode !== null) Scheduler_cancelCallback(existingCallbackNode); // 高优打断低优更新
if (newCallbackPriority === SyncLane) {
} else {
let schedulerPriorityLevel;
switch (lanesToEventPriority(nextLanes)) {
case DiscreteEventPriority:
schedulerPriorityLevel = ImmediateSchedulerPriority;
break;
case ContinuousEventPriority:
schedulerPriorityLevel = UserBlockingSchedulerPriority;
break;
case DefaultEventPriority:
schedulerPriorityLevel = NormalSchedulerPriority;
break;
case IdleEventPriority:
schedulerPriorityLevel = IdleSchedulerPriority;
break;
default:
schedulerPriorityLevel = NormalSchedulerPriority;
break;
}
newCallbackNode = Scheduler_scheduleCallback(schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root)); { // react 并发 performConcurrentWorkOnRoot
// 4种车道并发但是不时间分片 InputContinuousHydrationLane | InputContinuousLane | DefaultHydrationLane | DefaultLane;
// SyncLane 不并发同步
// DefaultLane 默认车道不时间分片,useEffect
const nonIncludesBlockingLane = !includesBlockingLane(root, lanes); {
const SyncDefaultLanes = InputContinuousHydrationLane | InputContinuousLane | DefaultHydrationLane | DefaultLane;
return (lanes & SyncDefaultLanes) !== NoLane;
}
//是否不包含过期的车道
const nonIncludesExpiredLane = !includesExpiredLane(root, lanes);
//时间片没有过期
const nonTimeout = !didTimeout;
const shouldTimeSlice = nonIncludesBlockingLane && nonIncludesExpiredLane && nonTimeout; //react 时间切片
const exitStatus = shouldTimeSlice ? renderRootConcurrent(root, lanes) : renderRootSync(root, lanes); { // renderRootConcurrent
workLoopConcurrent(); {
//如果有下一个要构建的fiber并且时间片没有过期
while (workInProgress !== null && !shouldYield()) { // react fiber增量渲染
performUnitOfWork(workInProgress);
}
}
}
//如果不是渲染中的话,那说明肯定渲染完了
if (exitStatus !== RootInProgress) {
const finishedWork = root.current.alternate;
root.finishedWork = finishedWork;
commitRoot(root);
}
//说明任务没有完成
if (root.callbackNode === originalCallbackNode) {
//把此函数返回,下次接着干
return performConcurrentWorkOnRoot.bind(null, root);
}
return null;
}
}
}
}
function commitRootImpl(root) {
ensureRootIsScheduled(root, now()); //react 高优打断低优更新 在提交之后,因为根上可能会有之前取消的更新,所以需要重新再次调度
}
问题 事件
- xzc 对每一个事件,都会在root节点上绑定一个冒泡和捕获的代理事件。拿到触发事件的dom对应的fiber【internalInstanceKey】,往上遍历收集父fiber节点上注册的冒泡或者捕获事件到两个数组里面,最后一次性执行完。
不同的事件他会设置不同优先级上下文,所以会触发不同优先级的setState
// const instance = createInstance(type, newProps, workInProgress);
fiber初次挂载,创建对应dom元素的时候会把props和离他最近的fiber节点保存在dom上
//问题 子事件父事件原生事件执行顺序?
//答 所有react事件都绑在#root上,原生事件肯定绑在#root的子dom上,所以一定慢执行
父react捕获 子react捕获 父原生捕获 子原生捕获 子原生冒泡 父原生冒泡 子React冒泡 父react冒泡
//问题 合成事件
//问题 react 事件委托/事件代理?
function functionThatReturnsTrue() {
return true;
}
function functionThatReturnsFalse() {
return false;
}
function SyntheticBaseEvent(reactName, reactEventType, targetInst, nativeEvent, nativeEventTarget) {
//是否已经阻止默认事件
this.isDefaultPrevented = functionThatReturnsFalse;
//是否已经阻止继续传播
this.isPropagationStopped = functionThatReturnsFalse;
return this;
}
assign(SyntheticBaseEvent.prototype, {
preventDefault() {
const event = this.nativeEvent;
if (event.preventDefault) {
event.preventDefault();
} else {
event.returnValue = false;
}
this.isDefaultPrevented = functionThatReturnsTrue;
},
stopPropagation() {
const event = this.nativeEvent;
if (event.stopPropagation) {
event.stopPropagation();
} else {
event.cancelBubble = true;
}
this.isPropagationStopped = functionThatReturnsTrue;
}
});
// 初始化 allNativeEvents,['click', 'onClick']
const simpleEventPluginEvents = ['click']; // 所有事件数组
SimpleEventPlugin.registerEvents();
for (let i = 0; i < simpleEventPluginEvents.length; i++) {
registerSimpleEvent(domEventName, `on${capitalizeEvent}`);//click,onClick
//cmt onClick在哪里可以取到 pendingProps
//cmt 最开始的props就是虚拟dom的props: react.createElement('div', props)
//cmt 赋值给workInProgress.pendingProps
//cmt const newProps = workInProgress.pendingProps;
//cmt 在源码里 让真实DOM元素 updateFiberProps(domElement, props);
//cmt const internalPropsKey = "__reactProps$" + randomKey;
//cmt 真实DOM元素[internalPropsKey] = props; 从真实dom中props.onClick取到
topLevelEventsToReactNames.set(domEventName, reactName); // 可以通过 click 拿到 onClick
registerTwoPhaseEvent(reactName, [domEventName]);//'onClick' ['click']
registerDirectEvent(registrationName, dependencies);//注册冒泡事件的对应关系
registerDirectEvent(registrationName + 'Capture', dependencies);//注意捕获事件的对应的关系
for (let i = 0; i < dependencies.length; i++) {
allNativeEvents.add(dependencies[i]);//click
}
}
// 绑定合成事件到根节点
// 点击触发合成事件里面的dispatchEvent
createRoot {
listenToAllSupportedEvents(container) { // container = dom#root
allNativeEvents.forEach((domEventName) => {
listenToNativeEvent(domEventName, true, rootContainerElement); // react 捕获事件
listenToNativeEvent(domEventName, false, rootContainerElement); { // react 冒泡事件
addTrappedEventListener(target, domEventName, eventSystemFlags, isCapturePhaseListener); {
const listener = createEventListenerWrapperWithPriority(targetContainer, domEventName, eventSystemFlags); { // react 绑定的合成事件就是这个
const listenerWrapper = dispatchDiscreteEvent; { // react 不同事件中的setState为什么有不同的优先级
const previousPriority = getCurrentUpdatePriority();
try {
//把当前的更新优先级设置为离散事件优先级 1
setCurrentUpdatePriority(DiscreteEventPriority);
dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent); {
const nativeEventTarget = getEventTarget(nativeEvent);
const targetInst = getClosestInstanceFromNode(nativeEventTarget); // react 获取离dom最近的fiber
dispatchEventForPluginEventSystem {
dispatchEventForPlugins(domEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer) {
const dispatchQueue = [];
extractEvents(dispatchQueue,domEventName,nativeEvent) {
SimpleEventPlugin.extractEvents {
const reactName = topLevelEventsToReactNames.get(domEventName);// react 映射派上用场了
let SyntheticEventCtor;//合成事件的构建函数
switch (domEventName) {
case 'click':
SyntheticEventCtor = SyntheticMouseEvent;
break;
default:
break;
}
const listeners = accumulateSinglePhaseListeners(targetInst,reactName,nativeEvent.type,isCapturePhase);{
let instance = targetFiber;
while (instance !== null) {
const { stateNode, tag } = instance;//stateNode 当前的执行回调的DOM节点
if (tag === HostComponent && stateNode !== null) {
const listener = getListener(instance, reactEventName); // react 真正的onClick函数
if (listener) {
listeners.push(createDispatchListener(instance, listener, stateNode)); // react return { instance, listener, currentTarget }
}
}
instance = instance.return;
}
return listeners;
}
const event = new SyntheticEventCtor(reactName, domEventName, null, nativeEvent, nativeEventTarget); // react 合成事件
dispatchQueue.push({
event,//合成事件实例
listeners//监听函数数组
});
}
}
processDispatchQueue(dispatchQueue, eventSystemFlags); // react 开始真正执行事件了
}
}
}
} finally {
setCurrentUpdatePriority(previousPriority);
}
}
return listenerWrapper.bind(null, domEventName, eventSystemFlags, targetContainer);
}
if (isCapturePhaseListener) {
addEventCaptureListener(targetContainer, domEventName, listener); // 监听 createEventListenerWrapperWithPriority
} else {
addEventBubbleListener(targetContainer, domEventName, listener);
}
}
}
});
}
}
//问题 redux //问题 redux全局的store, store是怎么跟react组件关联在一起的
zhufengpeixun.com/strong/html…
createStore会通过reducer初始化一个要保存的数据,返回一个对象暴露出方法来让我们可以修改数据。把这个对象通过provider保存起来,所有子组件就可以通过useContext拿到最新方法。通过useDispatch,内部就是用到了useContext拿到了这个对象上面的dispatch方法。
我们在useSelector里面,可以拿到最新的值。他还会通过useSyncExternalStore这个hook,订阅一个函数给这个对象。
当调用 dispatch,他会再次通过reducer修改这个对象,执行所有订阅的函数。这个回调就是看看修改后的值跟之前的值是不是一样,如果不一样,他会给当前fiber一个同步车道,然后同步重渲染,这样组件有车道就会重渲染,函数组件重新执行,useSelector返回最新值。
import { Provider } from './react-redux';
createRoot(document.getElementById('root')).render(
<Provider store={store}>
<Counter1 />
<hr />
<Counter2 />
</Provider>);
export default function Provider(props) {
return (
<ReactReduxContext.Provider value={{ store: props.store }}>
{props.children}
</ReactReduxContext.Provider>
)
}
function useDispatch() {
const { store } = useContext(ReactReduxContext);
return store.dispatch;
}
// action只能是类型,通过中间件,action可以是函数或者promise
// redux-thunk,action可以传函数
// redux-promise,action可以传promise
通过compose把所有中间件串起来,他会在dispatch前后加一下切面逻辑,跟洋葱一样
function applyMiddleware(...middlewares) {
return function (createStore) {
return function (reducer, preloadedState) {
const store = createStore(reducer, preloadedState);
let dispatch; //此变量会指向新的dispatch方法 newDispatch
let middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action),
};
let chain = middlewares.map((middleware) => middleware(middlewareAPI));
dispatch = compose(...chain)(store.dispatch);
return {
...store,
dispatch,
};
};
};
}
function compose(...funcs) {
return function (args) {
for (let i = funcs.length - 1; i >= 0; i--) {
args = funcs[i](args);
}
return args;
}
}
function thunk({ getState, dispatch }) {
// compose 进行组合并执行
return function (next) {
// compose返回的函数,也是封装后的dispatch
return function (action) {
if (typeof action === 'function') {
return action(getState, dispatch);
}
return next(action);
}
}
}
function thunkAdd() {
return function (getState, dispatch) {
setTimeout(() => {
debugger
dispatch(function (getState, dispatch) {
setTimeout(() => {
debugger
dispatch({ type: ADD1 });
}, 1000);
});
}, 1000);
}
}
function promise({ getState, dispatch }) {
return function (next) {
return function (action) {
if (action.then && typeof action.then === 'function') {
action.then(dispatch)
} else {
next(action);
}
}
}
}
function promiseAdd() {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ type: ADD1 });
}, 1000);
});
}
const store = applyMiddleware(thunk, promise)(createStore)(combinedReducer);
//TODO immer medium.com/@mysteryven…
不修改原来的数据。只要修改了草稿,他就会返回一个浅拷贝,除了根节点的值跟他父节点的引用修改了,其他节点都复用原来的。
// 函数式编程的话要深拷贝,但是数据量大的话性能差
// 深拷贝的优化版,只改变修改的节点和其父节点,其他复用
当我们修改草稿,每一层他都会创建一个proxy对象,并浅拷贝一份,我们修改都在浅拷贝上修改,不会修改原数据。
比如当我们修改a.b.c = 1,他会触发b的setter,会修改b.c的值=1,.然后修改a对b的引用,最后返回a的浅拷贝
在redux配合使用produce,比起深拷贝性能好,并且操作也简单
长列表,可以用immer配置react.memo,其他没有改变的表单项不会重渲染。memo会比较前后组件每一个属性是否相等
// 用法
// 场景零
reducer里面使用 immer
function createReducer(initialState, reducers={}) {
return function (state = initialState, action) {
let reducer = reducers[action.type];
if (reducer)
return produce(state, draft => {
reducer(draft, action);
});
return state;
}
}
const tabs = (state: TabsState = tabsState, action: AnyAction) =>
produce(state, draftState => {
switch (action.type) {
case types.SET_TABS_LIST:
draftState.tabsList = action.tabsList;
break;
case types.SET_TABS_ACTIVE:
draftState.tabsActive = action.tabsActive;
break;
default:
return draftState;
}
});
// setState如果前后两次传入同一个对象,里面有一个优化,不会进行调度,也就是不会重渲染
const eagerState = lastRenderedReducer(lastRenderedState, action);
update.hasEagerState = true;
update.eagerState = eagerState;
if (Object.is(eagerState, lastRenderedState)) {
return;
}
// 可以深拷贝一份
import { useState } from 'react'
function App() {
const [countRef, setCount] = useState({ count: 1 })
console.log('rerendered')
function handleClick() {
countRef.count += 1
setCount(countRef)
}
return <button onClick={handleClick}>count is {countRef.count}</button>
}
export default App
//问题 redux-toolkit // 源码先不看,收益太低了 // 只看一下action怎么创建的
// 记一下api
//TODO5月底有时间才看 zustand 链接:juejin.cn/post/731324…
const useXxxStore = create((set) => ({
aaa: '',
updateAaa: (value) => set(() => ({ aaa: value })),
}))
const updateAaa = useXxxStore((state) => state.updateAaa)
const aaa = useXxxStore((state) => state.aaa)
function logMiddleware(func) {
return function(set, get, store) {
function newSet(...args) {
console.log('调用了 set,新的 state:', get());
return set(...args)
}
return func(newSet, get, store)
}
}
1通过create创建一个store,并生成初始值
2通过返回的函数,传入一个selector,通过他拿到仓库的最新的值的一部分
怎么拿值的?
里面用到了useSyncExternalStore这个api,订阅一个回调给store,只要store发送变化会执行所有的回调,拿到最新的快照也就是最新的值
export const create = (createState) => {
//1. 创建一个store
const api = createStore(createState)
//2. 返回一个函数,通过selector拿到最新值
const useBoundStore = (selector) => useStore(api, selector)
// 小功能,能调用api
Object.assign(useBoundStore, api);
return useBoundStore
}
function useStore(api, selector) {
function getState() {
return selector(api.getState());
}
return useSyncExternalStore(api.subscribe, getState)
}
function useSyncExternalStore(subscribe, getSnapShot) {
let [state, setState] = useState(getSnapShot());
useLayoutEffect(() => {
subscribe(() => {
setState(getSnapShot())
});
}, []);
return state;
}
const createStore = (createState) => {
let state;
const listeners = new Set();
const setState = (partial, replace) => {
const nextState = typeof partial === 'function' ? partial(state) : partial
if (!Object.is(nextState, state)) {
const previousState = state;
if(!replace) {
state = (typeof nextState !== 'object' || nextState === null)
? nextState
: Object.assign({}, state, nextState);
} else {
state = nextState;
}
listeners.forEach((listener) => listener(state, previousState));
}
}
const getState = () => state;
const subscribe= (listener) => {
listeners.add(listener)
return () => listeners.delete(listener)
}
const destroy= () => {
listeners.clear()
}
const api = { setState, getState, subscribe, destroy }
state = createState(setState, getState, api)
return api
}