React 源码:useEffect 和 useLayoutEffect 源码实现

最近在学习慕课网 手写 React 高质量源码迈向高阶开发,之前自己也尝试看过源码,不过最终放弃了

放弃的最主要原因是 react 内部的调用链太长了,每天在缕清调用链上都花了不少时间,createRoot 都没有看完

最近看到慕课网有一个 react 源码课,就想着跟着课程然后在自己源码,看看这次能够看到什么地步

它这个课程前八章 是 react@16 的源码,从第九章开始才是 react@18 的源码

从本章开始学习的是 react@18 的源码,从 fiber 开始,涉及 beginWorkcompleteWorkcommitWork,调度策略,优先级等

React 源码系列:

我们在上一篇中实现了 useStateuseReducer

这一篇来实现 useEffect

它也是和 useState 一样,定义在 react 包中

js 复制代码
// react/src/ReactHooks.js
function useEffect(create, deps) {
  const dispatcher = resolveDispatcher();
  return dispatcher.useEffect(create, deps);
}

然后通过 react/index.js 暴露给开发者使用,__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED 给内部包使用

ReactFiberHooks 文件中分别给 HooksDispatcherOnMountHooksDispatcherOnUpdate 添加 useEffect 方法

js 复制代码
// 函数组件挂载时执行的 hooks
const HooksDispatcherOnMount = {
  useReducer: mountReducer,
  useState: mountState,
  useEffect: mountEffect,
};

// 函数组件更新时执行的 hooks
const HooksDispatcherOnUpdate = {
  useReducer: updateReducer,
  useState: updateState,
  useEffect: updateEffect,
};

这两个对象实在 renderWithHooks 中调用的,而 renderWithHooks 又是在 beginWork 阶段调用的,所以也就是说 useEffect 初次挂载和更新都是在 beginWork 时执行

mountEffect

mountEffect 方法初始渲染时调用的

第一个参数是 useEffect 的函数,第一个参数是 useEffect 的依赖,内部调用了 mountEffectImpl 方法,传入的两个副作用的标记 PassiveEffectHookPassive

PassiveEffectHookPassive 都是 Passive,这里是给它重命名了一下

他们的区别是:

  • PassiveEffect 是一个 FiberFlags 表示当前的 Fiber 有一个副作用
  • HookPassive 是一个 HookFlags 表示当前的 Hook 有一个副作用

他们使用要到 commitWork 阶段,所以这里暂时先不用管

js 复制代码
// react-reconciler/src/ReactFiberHooks.js
function mountEffect(create, deps) {
  return mountEffectImpl(PassiveEffect, HookPassive, create, deps);
}

mountEffectImpl

这个函数主要做了这种;

  1. 为当前的 useEffect 初始化一个 hook
    • 通过 mountWorkInProgressHook 初始化,这个函数我们在 useReducer 中已经讲过了,它的作用就是初始化一个 hook 等待更新时使用
    • 这个 hook 是保存在 currentRenderingFiber.memoizedState 中的,从当前的 Fiber 中的第一个 hook 依次向下通过 next 的链接在一起
  2. 将当前的 Fiber.flags 设置为 Passive
  3. 将刚刚初始化的 hook.memoizedState 挂载一个副作用函数 effect
    • useReducermemoizedState 中挂载的是 useReducer 的初始值
js 复制代码
// react-reconciler/src/ReactFiberHooks.js
function mountEffectImpl(fiberFlags, hookFlags, create, deps) {
  // 创建一个 hook 这个 hook 会关联在 currentlyRenderingFiber.memoizedState 上
  // currentyRenderingFiber 是从函数组件中编写的的第一个 hook 开始一次将里面的 hook 用 next 的方式串联起来
  const hook = mountWorkInProgressHook();
  // useEffect 的依赖
  const nextDeps = deps === undefined ? null : deps;
  // 将当前的 Fiber 标记为 PassiveEffect
  currentlyRenderingFiber.flags |= fiberFlags;
  // 当前的 hook.memoizedState 是一个 effect 函数
  hook.memoizedState = pushEffect(HookHasEffect | hookFlags, create, undefined, nextDeps);
}

pushEffect

pushEffect 作用是创建一个 effect 对象,并将 useEffect 之间形成链表,通过 lastEffect 链接

如果当前的 Fiber 上不存在 updateQueue,说明现在处理的是第一个 useEffect,就初始化一个 updateQueue,并将创建的 effect 放入 updateQueue

如果当前的 Fiber 上存在 updateQueue,说明现在处理的 hook 不是 Fiber 中的第一个 hook,由于 pushEffect 只处理 useEffect,就有可能没有 lastEffect

没有 lastEffect,就将 effect 赋值 lastEffect,如果有的话就将 useEffect 单独做成一个链表,lastEffect 是指向最后一个 useEffecteffect

js 复制代码
// react-reconciler/src/ReactFiberHooks.js
function pushEffect(tag, create, destroy, deps) {
  const effect = { tag, create, destroy, deps, next: null };
  // 如果当前的 Fiber 上不存在 updateQueue,说明当前处理的是 Fiber 的第一个 hook 是 useEffect
  let componentUpdateQueue = currentlyRenderingFiber.updateQueue;
  // 如果不存在 updateQueue,就初始化一个 updateQueue
  if (componentUpdateQueue === null) {
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    currentlyRenderingFiber.updateQueue = componentUpdateQueue;
    // lastEffect 指向 Fiber 中最后一个 useEffect
    componentUpdateQueue.lastEffect = effect.next = effect;
  } else {
    // 如果存在 updateQueue,说明当前处理的不是 Fiber 的第一个 hook
    const lastEffect = componentUpdateQueue.lastEffect;
    // 如果不存在 lastEffect,说明当前处理的的是 Fiber 中的第一个 useEffect
    if (lastEffect === null) {
      // lastEffect 指向 Fiber 中最后一个 useEffect
      componentUpdateQueue.lastEffect = effect.next = effect;
    } else {
      // 如果存在 lastEffect,说明当前处理的是 Fiber 中的第二个及之后的 useEffect
      const firstEffect = lastEffect.next;
      lastEffect.next = effect;
      effect.next = firstEffect;
      // lastEffect 指向 Fiber 中最后一个 useEffect
      componentUpdateQueue.lastEffect = effect;
    }
  }
  return effect;
}

function createFunctionComponentUpdateQueue() {
  return { lastEffect: null };
}

updateEffect

updateEffect 方法是在更新时调用的,它的参数和 mountEffect 一样

js 复制代码
// react-reconciler/src/ReactFiberHooks.js
function updateEffect(create, deps) {
  return updateEffectImpl(PassiveEffect, HookPassive, create, deps);
}

至于什么时候调用 mountEffectupdateEffect,是在 renderWithHooks 中决定的

js 复制代码
// react-reconciler/src/ReactFiberHooks.js
function renderWithHooks(current, workInProgress, Component, props) {
  // 这一步骤其实很关键,在更新时如果 updateQueue 没有设置为 null
  // 那么更新时的 effect 会和初次渲染的 effect 组成链表,这是有问题的
  // 所有 react 在 renderWithHooks 执行时将 updateQueue 设置为 null
  workInProgress.updateQueue = null;
  // 如果 current 有值,说明是更新,否则是初次渲染
  // 当然,如果没有要更新的 state,也不用走更新逻辑
  if (current !== null && current.memoizedState !== null) {
    // 更新,setXXX 时,给 useReducer, useEffect 等 hook 赋值
    ReactCurrentDispatcher.current = HooksDispatcherOnUpdate;
  } else {
    // 初始渲染,给 useReducer, useEffect 等 hook 赋值
    ReactCurrentDispatcher.current = HooksDispatcherOnMount;
  }
}

updateEffectImpl

updateWorkInProgressHook 我们在 useState 文章中讲到过,在一个组件中更新时,每一个 hook 都会运行一遍 updateEffectImpl

因为 hook 是由链表串联起来的,所以这里就用 updateWorkInProgressHook 取出当前正在运行的 hook

具体来说,你在一个组件中写了一个 useState,一个 useEffect,可以通过 updateWorkInProgressHook 函数找到 useState 或者 useEffect 对应的 hook

js 复制代码
const [number, setNumber] = useState(1);
useEffect(() => {
  console.log("useEffect");
}, []);

其实这里 hook 的值是从 currentHook 中拷贝过来的,但不是同一个对象

如果当前的 useEffect 依赖有变化,就将当前的 Fiber 标记一个 PassiveEffectFlags,同时将 effect 中的 tag 标记为 HookHasEffect

也就是说在更新时需要比较 nextDepsprevDeps 是否相同

相同时:

  • 当前的 Fiber.flags 不标记
  • 将当前处理的 useEffecteffect.tagHookPassive

不同时:

  • 将当前的 Fiber.flags 标记为 PassiveEffect
  • 将当前处理的 useEffecteffect.tag 标记为HookHasEffect | HookPassive
js 复制代码
// react-reconciler/src/ReactFiberHooks.js
function updateEffectImpl(fiberFlags, hookFlags, create, deps) {
  // 取出当前 Fiber 中正在运行的 hook
  // 有一个 hook,这个函数就会运行一次,运行之后这个 hook 就是当前需要处理的 hook
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  let destroy;
  // 老 hook 存在
  if (currentHook !== null) {
    const prevEffect = currentHook.memoizedState;
    // 拿到上一个 hook 的 effect
    destroy = prevEffect.destroy;
    // 有依赖
    if (nextDeps !== null) {
      const prevDeps = prevEffect.deps;
      // 依赖相同的情况
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        // 给 effect tag 标记为 HookHasEffect | HookPassive
        hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps);
        return;
      }
    }
  }
  // 依赖不相同的情况,说明当前的 Fiber 需要处理副作用
  currentlyRenderingFiber.flags |= fiberFlags;
  // 给 effect tag 标记为 HookHasEffect | HookPassive
  hook.memoizedState = pushEffect(HookHasEffect | hookFlags, create, destroy, nextDeps);
}

使用 Object.is 比较两个依赖是否相同

js 复制代码
// react-reconciler/src/ReactFiberHooks.js
function areHookInputsEqual(nextDeps, prevDeps) {
  if (prevDeps === null) return null;
  for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
    // 是同一个对象
    if (Object.is(nextDeps[i], prevDeps[i])) continue;
    return false;
  }
  return true;
}

commitWork

beginWork 阶段主要做的事情是将 useEffecteffect 函数和依赖保存起来

保存之后,在什么时候使用呢?

那肯定是在 commitWork 阶段使用了

commitWork 的入口函数是 commitRoot

我们检查 root 节点上有没有 Passive 这个标记,如果有存在的话,就执行 scheduleCallback

js 复制代码
// react-reconciler/src/ReactFiberWorkLoop.js
function commitRoot(root) {
  const { finishedWork } = root;
  if ((finishedWork.subtreeFlags & Passive) !== NoFlags || (finishedWork.flags & Passive) !== NoFlags) {
    // 如果根节点上没有 Passive 这个标记,就将根节点标记为 Passive
    // 这么设置的原因是防止在同一个 useEffect 中多次执行 commitWork
    if (!rootDoseHavePassiveEffect) {
      rootDoseHavePassiveEffect = true;
      scheduleCallback(flushPassiveEffects);
    }
  }
}

设置了之后就需要释放,释放是要等到 commitWork 执行结束之后

js 复制代码
// react-reconciler/src/ReactFiberWorkLoop.js
function commitRoot(root) {
  const subtreeHasEffects = (finishedWork.subtreeFlags & MutationMask) !== NoFlags;
  // 查看 RootFiber 是否有处理
  const rootHasEffect = (finishedWork.flags & MutationMask) !== NoFlags;
  if (subtreeHasEffects || rootHasEffect) {
    // commitWork
    commitMutationEffectsOnFiber(finishedWork, root);
    // 释放 rootDoseHavePassiveEffect
    if (rootDoseHavePassiveEffect) {
      rootDoseHavePassiveEffect = false;
      rootWithPendingPassiveEffects = root;
    }
  }
}

flushPassiveEffects

flushPassiveEffects 函数的主要逻辑就是执行 useEffecteffect 函数和 destroy 函数

js 复制代码
// react-reconciler/src/ReactFiberWorkLoop.js
function flushPassiveEffects() {
  if (rootWithPendingPassiveEffects !== null) {
    const root = rootWithPendingPassiveEffects;
    // 先执行 useEffect 的 destroy 函数
    commitPassiveUnmountEffects(root.current);
    // 再执行 useEffect 的 effect 函数
    commitPassiveMountEffects(root, root.current);
  }
}

commitPassiveMountEffects

commitPassiveMountEffects 函数是 useEffecteffect 函数,内部直接调用了 commitPassiveMountOnFiber

js 复制代码
// react-reconciler/src/ReactFiberCommitWork.js
function commitPassiveMountEffects(root, finishedWork) {
  commitPassiveMountOnFiber(root, finishedWork);
}

commitPassiveMountOnFiber 函数的作用是根据 tag 做不同的处理

因为 useEffect 只会出现在函数组件中,也就是说 tagFunctionComponent

tag 是其他值的话,继续往下遍历,与 recursivelyTraversePassiveMountEffects 组成递归

如果是函数组件的话,就检查当前的 Fiber 是否有 Passive 标记,如果有的话就执行 commitHookPassiveMountEffects 函数

也就说 useEffect 是深度优先函数,叶子节点的 useEffect 会先执行

js 复制代码
// react-reconciler/src/ReactFiberCommitWork.js
function commitPassiveMountOnFiber(finishedRoot, finishedWork) {
  const { flags } = finishedWork;
  switch (finishedWork.tag) {
    case HostRoot: {
      recursivelyTraversePassiveMountEffects(finishedRoot, finishedWork);
      break;
    }
    case FunctionComponent: {
      // 深度优先
      // 因为 recursivelyTraversePassiveMountEffects 内部会检查当前 Fiber 的子节点,如果有子节点又会继续调用 commitPassiveMountOnFiber,形成深度优先
      recursivelyTraversePassiveMountEffects(finishedRoot, finishedWork);
      // 当前 Fiber 有 Passive 标记
      if (flags & Passive) {
        // 处理当前 Fiber 的 effect 函数
        commitHookPassiveMountEffects(finishedWork, HookHasEffect | HookPassive);
      }
      break;
    }
    default: {
      recursivelyTraversePassiveMountEffects(finishedRoot, finishedWork);
      break;
    }
  }
}

recursivelyTraversePassiveMountEffects 函数检查当前 Fiber 是否有兄弟节点,如果有兄弟节点的话就依此调用 commitPassiveMountOnFiber

js 复制代码
// react-reconciler/src/ReactFiberCommitWork.js
function recursivelyTraversePassiveMountEffects(root, parentFiber) {
  if (parentFiber.subtreeFlags & Passive) {
    let child = parentFiber.child;
    while (child !== null) {
      commitPassiveMountOnFiber(root, child);
      child = child.sibling;
    }
  }
}

commitHookPassiveMountEffects

commitHookPassiveMountEffects 函数转发到 commitHookEffectListMount

js 复制代码
// react-reconciler/src/ReactFiberCommitWork.js
function commitHookPassiveMountEffects(finishedWork, hookFlags) {
  commitHookEffectListMount(hookFlags, finishedWork);
}

useEffecteffect 函数主要就是由 commitHookEffectListMount 函数处理

从当前 Fiber 中取出 updateQueue,因为在 beginWork 阶段时,一个函数组件所有 useEffect 都会放入 updateQueue 中形成链表

这里就是取出 effect 链表,挨个执行 effect 函数

js 复制代码
// react-reconciler/src/ReactFiberCommitWork.js
function commitHookEffectListMount(flags, finishedWork) {
  const updateQueue = finishedWork.updateQueue;
  // 取出 updateQueue 中保存的 effect 链表
  let lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
  if (lastEffect !== null) {
    const firstEffect = lastEffect.next;
    // 第一个 effect 函数
    let effect = firstEffect;
    do {
      // 如果 effect.tag  是  HookHasEffect | HookPassive 标记就执行 effect.create 函数,并将返回的函数保存到 destroy 中,等待 Unmount 时执行
      if ((effect.tag & flags) === flags) {
        const create = effect.create;
        // 执行 create 函数,得到 destroy 函数
        // 将 destroy 函数保存到 effect.destroy 中
        effect.destroy = create();
      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}

commitPassiveUnmountEffects

commitPassiveUnmountEffects 的逻辑和 commitPassiveMountEffects 一样,只是调用的函数不一样

这里需要知道的一点是,整个 Fiber 树的 destroy 函数执行完之后,才会执行 effect 函数

destroy 执行也是遵循深度优先的策略,叶子节点的 destroy 会先执行

js 复制代码
// react-reconciler/src/ReactFiberCommitWork.js
function commitPassiveUnmountEffects(finishedWork) {
  commitPassiveUnmountOnFiber(finishedWork);
}

最后会调用 commitHookEffectListUnmount 函数,执行 useEffectdestroy 函数

js 复制代码
function commitHookEffectListUnmount(flags, finishedWork) {
  const updateQueue = finishedWork.updateQueue;
  let lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
  if (lastEffect !== null) {
    const firstEffect = lastEffect.next;
    let effect = firstEffect;
    do {
      if ((effect.tag & flags) === flags) {
        const destroy = effect.destroy;
        // 执行 destroy 函数
        if (destroy !== undefined) {
          destroy();
        }
      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}

useLayoutEffect

useLayoutEffectuseEffect 特别像,它们的区别是 useEffect 是在渲染后执行,useLayoutEffect 是在渲染时执行的

useEffect 是异步执行的,useLayoutEffect 是同步执行

commitLayoutEffectsuseLayoutEffect 提交阶段的入口函数,我们看下是实在哪里执行的

js 复制代码
// react-reconciler/src/ReactFiberWorkLoop.js
function commitRoot(root) {
  const { finishedWork } = root;
  const subtreeHasEffects = (finishedWork.subtreeFlags & MutationMask) !== NoFlags;
  const rootHasEffect = (finishedWork.flags & MutationMask) !== NoFlags;
  if (subtreeHasEffects || rootHasEffect) {
    // commitWork 入口函数
    commitMutationEffectsOnFiber(finishedWork, root);
    // useLayoutEffect commitWork 阶段的入口函数
    commitLayoutEffects(finishedWork, root);

    if (rootDoseHavePassiveEffect) {
      rootDoseHavePassiveEffect = false;
      rootWithPendingPassiveEffects = root;
    }
  }

  root.current = finishedWork;
}

所以 useLayoutEffect 是在 commitWork 执行完成之后立马就会执行,

作为对比 useEffect 提交阶段是在 flushPassiveEffects 中执行,而 flushPassiveEffects 是异步执行的

js 复制代码
// react-reconciler/src/ReactFiberWorkLoop.js
function commitRoot(root) {
  const { finishedWork } = root;

  if (
    (finishedWork.subtreeFlags & Passive) !== NoFlags ||
    (finishedWork.flags & Passive) !== NoFlags
  ) {
    if (!rootDoseHavePassiveEffect) {
      rootDoseHavePassiveEffect = true;
      // useEffect commitWork 阶段的入口函数,异步执行
      scheduleCallback(flushPassiveEffects);
    }
  }
」
function flushPassiveEffects() {
  if (rootWithPendingPassiveEffects !== null) {
    const root = rootWithPendingPassiveEffects;
    commitPassiveUnmountEffects(root.current);
    commitPassiveMountEffects(root, root.current);
  }
}

useLayoutEffect 函数是先于 useEffect 执行的,也就是说 useLayoutEffectdestroy 都执行完了之后会执行 useLayoutEffecteffect 函数

等到当前 useLayoutEffect 所有函数都执行完了之后才会一次执行 useEffect(这个所有指的是整个 Fiber 树的 useLayoutEffect 函数)

useLayoutEffect 的 effect 函数

useLayoutEffecteffect 函数是在 destroy 函数之后执行的,这里先看 effect 函数

commitLayoutEffects 函数是 useLayoutEffect 的入口函数,它的作用就是调用 commitLayoutEffectOnFiber 根据节点的 tag,递归处理,如果处理到了函数组件的话就调用 commitHookLayoutEffects 函数

执行 commitHookLayoutEffects 函数会传入 HookLayout,也就是说当前 FiberuseLayoutEffecteffect 函数都会执行

js 复制代码
// react-reconciler/src/ReactFiberCommitWork.js
// 递归处理 Fiber 节点,如果 Fiber 是函数组件的话,就调用 commitHookLayoutEffects 函数,处理 useLayoutEffect 的 effect 函数
function commitLayoutEffectOnFiber(finishedRoot, current, finishedWork) {
  const flags = finishedWork.flags;
  switch (finishedWork.tag) {
    case FunctionComponent: {
      recursivelyTraverseLayoutEffects(finishedRoot, finishedWork);
      if (flags & Update) {
        // useLayoutEffect 执行 mount 阶段的 effect 函数
        commitHookLayoutEffects(finishedWork, HookLayout | HookHasEffect);
      }
      break;
    }
    default: {
      recursivelyTraverseLayoutEffects(finishedRoot, finishedWork);
      break;
    }
  }
}

function recursivelyTraverseLayoutEffects(root, parentFiber) {
  if (parentFiber.subtreeFlags & LayoutMask) {
    let child = parentFiber.child;
    while (child !== null) {
      const current = child.alternate;
      commitLayoutEffectOnFiber(root, current, child);
      child = child.sibling;
    }
  }
}

function commitHookLayoutEffects(finishedWork, hookFlags) {
  commitHookEffectListMount(hookFlags, finishedWork);
}

useLayoutEffect 的 destroy 函数

useLayoutEffectdestroy 函数是在哪里执行的呢?

刚开始在看的源码的时候不理解,commitLayoutEffectOnFiber 函数中只有执行了 commitHookEffectListMount,这个函数是执行 effect 函数

destroy 函数是在哪里执行的呢,源码翻了很久才找到的

其实 useLayoutEffectdestroy 函数是在 commitWork 阶段执行的

commitMutationEffecsOnFibercommitWork 的入口函数,reactFunctionComponent 分支中加了一行代码用来执行 useLayoutEffectdestroy 函数

js 复制代码
// react-reconciler/src/ReactFiberCommitWork.js
function commitMutationEffectsOnFiber(finishedWork, root) {
  const flags = finishedWork.flags;
  const current = finishedWork.alternate;
  switch (finishedWork.tag) {
    case FunctionComponent: {
      recursivelyTraverseMutationEffects(root, finishedWork);
      commitReconciliationEffects(finishedWork);
      // LayoutMask 中是有 Update 属性的,所以这里会执行 useLayoutEffect 的 destroy 函数
      if (flags & Update) {
        // useLayoutEffect 会先执行 unmount 阶段的 destroy 函数
        commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork);
      }
      break;
    }
    default: {
      recursivelyTraverseMutationEffects(root, finishedWork, lanes);
      commitReconciliationEffects(finishedWork);
      return;
    }
  }
}

总结

useLayoutEffectuseEffect 相同点:

  1. 他们会互相组成链表,保存在 updateQueue.lastEffect 属性中
  2. 初始渲染时执行 effect 函数,更新是先执行 destroy 函数,再执行 effect 函数

不同点:

  1. useEffect 是异步执行的,useLayoutEffect 是同步执行的
  2. useEffect 是在渲染后执行的,useLayoutEffect 是在渲染时执行的
  3. useLayoutEffect 所有函数(effectdestroy 函数)都是在 useEffect 函数执行执行的
  4. useLayoutEffect 的标识是 HookLayoutuseEffect 的标识是 HookPassive

源码

  1. useEffect-beginWork 阶段
  2. useEffect-commitWork 阶段
  3. useLayoutEffect-beginWork 阶段
  4. useLayoutEffect-commitWork 阶段
相关推荐
Henry_Wu0012 分钟前
从swagger直接转 vue的api
前端·javascript·vue.js
SameX11 分钟前
初识 HarmonyOS Next 的分布式管理:设备发现与认证
前端·harmonyos
M_emory_38 分钟前
解决 git clone 出现:Failed to connect to 127.0.0.1 port 1080: Connection refused 错误
前端·vue.js·git
Ciito41 分钟前
vue项目使用eslint+prettier管理项目格式化
前端·javascript·vue.js
成都被卷死的程序员1 小时前
响应式网页设计--html
前端·html
fighting ~1 小时前
react17安装html-react-parser运行报错记录
javascript·react.js·html
老码沉思录1 小时前
React Native 全栈开发实战班 - 列表与滚动视图
javascript·react native·react.js
mon_star°2 小时前
将答题成绩排行榜数据通过前端生成excel的方式实现导出下载功能
前端·excel
Zrf21913184552 小时前
前端笔试中oj算法题的解法模版
前端·readline·oj算法
老码沉思录2 小时前
React Native 全栈开发实战班 - 状态管理入门(Context API)
javascript·react native·react.js