🔥【React 最新版本】面试题源码解析 🚀

688 阅读9分钟

找工作中,有公司招人吗?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[高优打断低优]

问题 饥饿问题

xie.infoq.cn/article/729…

每次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
}