completeWork:真实 DOM 是在哪里被创建的

上一篇讲到,reconcileChildren 会在 beginWork 阶段根据 ReactElement 创建或复用子 Fiber。

例如:

复制代码
function App() {
  return (
    <div className="app">
      <h1>Hello</h1>
      <p>React 19</p>
    </div>
  )
}

beginWork 阶段,React 会一层一层创建出 Fiber 结构:

复制代码
App Fiber
  child -> div Fiber
            child -> h1 Fiber
                      child -> HostText Fiber
            sibling -> p Fiber
                       child -> HostText Fiber

但是到这里为止,React 还没有创建真实 DOM。

div Fiber 不是 div DOM

h1 Fiber 不是 h1 DOM

HostText Fiber 也不是文本 DOM 节点。

真实 DOM 的创建发生在 render 阶段的另一半:

复制代码
completeWork

这一篇就围绕 completeWork 展开。

重点讲清楚:

复制代码
beginWork 和 completeWork 的分工是什么

completeUnitOfWork 如何向上归并

HostComponent 的真实 DOM 是在哪里创建的

HostText 的文本节点是在哪里创建的

appendAllChildren 为什么只追加直接 DOM 后代

bubbleProperties 如何收集 flags

render 阶段为什么只是创建 DOM,还不插入页面

一、render 阶段分为 begin 和 complete

React render 阶段不是只有 beginWork

它实际有两个方向:

复制代码
beginWork:向下

completeWork:向上

beginWork 负责从当前 Fiber 计算子 Fiber。

也就是:

复制代码
根据当前 Fiber 的 props、state、updateQueue、context 等信息,得到下一层 children,并生成 child Fiber

completeWork 负责在子节点都处理完成之后,完成当前 Fiber。

也就是:

复制代码
创建真实 DOM

收集子树 flags

准备 commit 阶段需要的信息

可以用一个组件结构理解:

复制代码
function App() {
  return (
    <div>
      <span>Hello</span>
    </div>
  )
}

render 阶段大概是:

复制代码
beginWork App
  创建 div Fiber

beginWork div
  创建 span Fiber

beginWork span
  创建 HostText Fiber

beginWork HostText
  没有子节点,返回 null

completeWork HostText
  创建文本节点

completeWork span
  创建 span DOM,并把文本节点 append 到 span DOM

completeWork div
  创建 div DOM,并把 span DOM append 到 div DOM

completeWork App
  函数组件没有 DOM,只做 flags 冒泡

所以真实 DOM 不是从上往下创建的。

而是从下往上创建的。

原因也很简单:

父 DOM 创建完成后,需要把子 DOM append 进去。

但父 Fiber 在 complete 时,必须确保它的子 Fiber 已经完成,子 DOM 已经准备好。

所以 DOM 创建发生在向上归并阶段更合理。

二、completeUnitOfWork 是 complete 阶段的控制器

completeWork 不是直接被 workLoop 调用的。

workLoop 调的是:

复制代码
performUnitOfWork

beginWork 返回 null 时,说明当前 Fiber 没有 child 需要继续向下处理。

这时会进入:

复制代码
completeUnitOfWork(unitOfWork)

简化结构如下:

复制代码
function performUnitOfWork(unitOfWork) {
  const current = unitOfWork.alternate

  const next = beginWork(
    current,
    unitOfWork,
    renderLanes
  )

  unitOfWork.memoizedProps = unitOfWork.pendingProps

  if (next === null) {
    completeUnitOfWork(unitOfWork)
  } else {
    workInProgress = next
  }
}

completeUnitOfWork 的职责是:

复制代码
完成当前 Fiber

如果有 sibling,进入 sibling

如果没有 sibling,继续向父 Fiber 归并

直到找到下一个 sibling,或者一路归并到 Root

简化伪代码:

复制代码
function completeUnitOfWork(unitOfWork) {
  let completedWork = unitOfWork

  do {
    const current = completedWork.alternate
    const returnFiber = completedWork.return

    completeWork(
      current,
      completedWork,
      renderLanes
    )

    const siblingFiber = completedWork.sibling

    if (siblingFiber !== null) {
      workInProgress = siblingFiber
      return
    }

    completedWork = returnFiber
    workInProgress = completedWork
  } while (completedWork !== null)
}

React 19 当前源码中,completeUnitOfWork 位于 ReactFiberWorkLoop.js,它会调用 completeWork(current, completedWork, entangledRenderLanes) 完成当前 Fiber;如果存在 sibling,就把 workInProgress 切到 sibling,否则继续向 return Fiber 归并。

三、completeWork 是真正完成当前 Fiber 的地方

completeWork 位于:

复制代码
packages/react-reconciler/src/ReactFiberCompleteWork.js

它会根据 Fiber tag 分发不同逻辑。

简化结构:

复制代码
function completeWork(
  current,
  workInProgress,
  renderLanes
) {
  const newProps = workInProgress.pendingProps

  switch (workInProgress.tag) {
    case HostComponent:
      return completeHostComponent(
        current,
        workInProgress,
        newProps
      )

    case HostText:
      return completeHostText(
        current,
        workInProgress
      )

    case HostRoot:
      bubbleProperties(workInProgress)
      return null

    case FunctionComponent:
      bubbleProperties(workInProgress)
      return null

    case Fragment:
      bubbleProperties(workInProgress)
      return null
  }
}

不是所有 Fiber 都会创建 DOM。

只有宿主相关 Fiber 才和真实宿主节点有关。

对于 ReactDOM 来说,主要是:

复制代码
HostComponent:div、span、button 等 DOM 元素

HostText:文本节点

HostRoot:根容器,不直接创建 DOM

函数组件、Fragment、Memo、Context 等 Fiber 本身没有对应真实 DOM。

它们在 complete 阶段主要做:

复制代码
向上冒泡 flags

维护子树信息

把 commit 阶段需要知道的副作用聚合到父级

React Reconciler README 对 Host Config 的 createInstanceappendInitialChildfinalizeInitialChildren 有明确说明:createInstance 创建宿主实例,appendInitialChild 在初始挂载时把子节点加入父实例,finalizeInitialChildren 调用时子节点已经被加入实例,但实例还没有连接到屏幕上的树。

四、HostText:文本 DOM 节点在哪里创建

先看最简单的文本节点:

复制代码
<span>Hello</span>

Hello 会在 Fiber 树里对应一个:

复制代码
HostText Fiber

当 work loop 处理到 HostText Fiber 时:

复制代码
beginWork HostText

会直接返回 null。

因为文本节点没有 children。

于是立刻进入:

复制代码
completeWork HostText

在 mount 阶段,completeWork 会创建文本实例:

复制代码
const textInstance = createTextInstance(
  newText,
  rootContainerInstance,
  currentHostContext,
  workInProgress
)

workInProgress.stateNode = textInstance

对于 ReactDOM 来说,createTextInstance 最终会创建真实文本节点。

可以理解成:

复制代码
document.createTextNode('Hello')

创建完之后,文本节点会保存在:

复制代码
HostTextFiber.stateNode

所以:

复制代码
Fiber.stateNode 对 HostText 来说,指向真实文本 DOM 节点

但注意:

复制代码
此时文本节点还没有插入页面

它只是被创建出来,并挂在 Fiber 上。

真正插入页面要等 commit 阶段。

五、HostComponent:元素 DOM 节点在哪里创建

再看:

复制代码
<div className="app">
  <span>Hello</span>
</div>

div 对应:

复制代码
HostComponent Fiber

div Fiber 进入 completeWork 时,它的子节点已经完成。

也就是说:

复制代码
span Fiber 已经 complete

span.stateNode 已经有 span DOM

span 下面的 HostText 也已经有文本 DOM

这时 div FibercompleteWork 会创建 div DOM:

复制代码
const instance = createInstance(
  type,
  newProps,
  rootContainerInstance,
  currentHostContext,
  workInProgress
)

appendAllChildren(
  instance,
  workInProgress,
  false,
  false
)

finalizeInitialChildren(
  instance,
  type,
  newProps,
  currentHostContext
)

workInProgress.stateNode = instance

对于 ReactDOM 来说:

复制代码
createInstance('div', ...)

可以理解成创建:

复制代码
document.createElement('div')

然后:

复制代码
workInProgress.stateNode = instance

所以:

复制代码
Fiber.stateNode 对 HostComponent 来说,指向真实 DOM Element

例如:

复制代码
div Fiber.stateNode -> HTMLDivElement
span Fiber.stateNode -> HTMLSpanElement
HostText Fiber.stateNode -> Text

六、为什么 DOM 是从子到父创建的

以这个结构为例:

复制代码
<div>
  <span>Hello</span>
</div>

执行顺序是:

复制代码
completeWork HostText
  创建 Text 节点

completeWork span
  创建 span DOM
  append Text 到 span DOM

completeWork div
  创建 div DOM
  append span DOM 到 div DOM

也就是说,子节点先完成。

父节点后完成。

这样父节点在 complete 时,就可以直接通过 appendAllChildren 把已经完成的子 DOM 收集起来。

如果父 DOM 先创建,也不是不行。

但 React 的 Fiber 遍历模型是深度优先。

当它回到父节点 complete 时,天然就拥有完整的子树结果。

这和 React 的 render 阶段设计完全一致:

复制代码
beginWork 向下展开 Fiber

completeWork 向上归并结果

七、appendAllChildren 为什么只追加直接 DOM 后代

appendAllChildrencompleteWork 里非常关键的函数。

它的作用是:

复制代码
从当前 Fiber 的子树中,找到所有直接宿主子节点,并 append 到当前 DOM 实例里

这里的"直接宿主子节点"很重要。

看一个例子:

复制代码
function App() {
  return (
    <div>
      <Header />
    </div>
  )
}

function Header() {
  return <span>Hello</span>
}

Fiber 树大概是:

复制代码
div Fiber
  child -> Header Fiber
            child -> span Fiber
                      child -> HostText Fiber

Header Fiber 是函数组件。

它没有 DOM。

div Fiber complete 时,appendAllChildren(divDOM, divFiber) 不能只看 divFiber.child

因为 divFiber.child 是:

复制代码
Header Fiber

Header 没有 DOM。

React 要继续向下找,直到找到:

复制代码
span Fiber

也就是最近的宿主节点。

最终应该执行的是:

复制代码
divDOM.appendChild(spanDOM)

而不是:

复制代码
divDOM.appendChild(Header)

因为 Header 不是 DOM。

所以 appendAllChildren 会遍历当前 Fiber 的子树:

复制代码
遇到 HostComponent 或 HostText,就 append 它的 stateNode

遇到 FunctionComponent、Fragment 等非宿主 Fiber,就继续往下找它们的 child

处理完一个分支,再找 sibling

不要越过当前 Fiber 的边界

这就是为什么 React 可以支持组件嵌套。

函数组件本身不会生成 DOM,但它下面的宿主节点最终会被收集到最近的宿主父节点里。

八、appendAllChildren 不会重复 append 深层 DOM

还是这个结构:

复制代码
<div>
  <span>Hello</span>
</div>

span Fiber complete 时:

复制代码
spanDOM.appendChild(textNode)

div Fiber complete 时:

复制代码
divDOM.appendChild(spanDOM)

注意:

复制代码
divDOM 不会直接 append textNode

因为 textNode 已经是 spanDOM 的子节点了。

appendAllChildren 在遇到 HostComponent 时,会 append 当前 HostComponent 的 stateNode,然后不会继续深入它的 children。

否则就会把深层 DOM 重复 append 到父节点。

所以它的核心规则是:

复制代码
遇到宿主节点,append 当前宿主节点,然后停止深入这一支

遇到非宿主节点,继续深入寻找宿主节点

九、finalizeInitialChildren 做什么

DOM 创建完成、子 DOM append 完成后,会调用:

复制代码
finalizeInitialChildren(
  instance,
  type,
  newProps,
  currentHostContext
)

这个函数来自 Host Config。

对于 ReactDOM 来说,它会处理初始属性。

例如:

复制代码
<button
  className="primary"
  disabled
  onClick={handleClick}
>
  Submit
</button>

finalizeInitialChildren 会完成初始属性设置相关工作。

可以粗略理解成:

复制代码
设置 className

设置 disabled

设置 style

设置事件相关属性

处理 autoFocus 等特殊属性

不同宿主环境实现不同。

ReactDOM 创建 DOM。

React Native 创建 Native View。

自定义 renderer 可以创建自己的宿主实例。

这也是 React Reconciler 和 Renderer 分离的原因:

复制代码
Reconciler 负责 Fiber、调和、调度、commit 流程

Renderer 通过 Host Config 提供 createInstance、appendInitialChild、commitUpdate 等宿主操作

十、mount 阶段和 update 阶段的 completeWork 不一样

completeWork 会根据:

复制代码
current !== null && workInProgress.stateNode !== null

区分 mount 和 update。

mount 阶段

mount 阶段没有旧 DOM。

所以需要:

复制代码
创建 DOM

append 子 DOM

初始化属性

stateNode 指向 DOM

大概是:

复制代码
const instance = createInstance(...)

appendAllChildren(instance, workInProgress, ...)

finalizeInitialChildren(instance, type, newProps, ...)

workInProgress.stateNode = instance

update 阶段

update 阶段已经有旧 DOM。

所以不需要重新创建 DOM。

而是复用:

复制代码
const instance = workInProgress.stateNode

然后比较旧 props 和新 props。

如果有变化,就标记更新。

在较新的 React DOM 实现里,更新相关信息会通过 Host Config 的 prepareUpdate 或 commitUpdate 路径处理,最终在 commit 阶段应用到 DOM。

核心思想不变:

复制代码
render 阶段计算差异

commit 阶段应用差异

也就是说,update 阶段的 completeWork 不直接修改 DOM。

它只是准备 commit 阶段需要的更新信息,并打上对应 flags。

十一、为什么 render 阶段创建 DOM 但不插入页面

这里很容易混乱。

render 阶段确实可能创建 DOM 节点。

比如 mount 阶段的:

复制代码
document.createElement('div')
document.createTextNode('Hello')

但是它不会把这些节点插入真实页面。

也就是不会执行最终的:

复制代码
container.appendChild(...)

或者:

复制代码
parent.insertBefore(...)

原因是 render 阶段可以被中断、重试、丢弃。

如果 render 阶段已经把 DOM 插入页面,那并发渲染就不安全了。

例如低优先级更新 render 到一半,React 决定让出主线程。

如果这时 DOM 已经部分插入页面,用户会看到半成品 UI。

所以 React 必须保证:

复制代码
render 阶段可以计算,可以创建离屏 DOM,但不能把结果提交到屏幕

commit 阶段才可以真正修改页面

这就是 render 和 commit 的根本边界。

十二、completeWork 如何处理 FunctionComponent

函数组件没有真实 DOM。

例如:

复制代码
function Header() {
  return <h1>Hello</h1>
}

Header FibercompleteWork 不会创建 DOM。

它主要做:

复制代码
bubbleProperties(workInProgress)
return null

也就是把子树的 flags、lanes 等信息向上冒泡。

因为 Header 本身没有 DOM,但它的子树可能有 DOM 插入、更新、删除。

比如:

复制代码
Header Fiber
  child -> h1 Fiber
            child -> HostText Fiber

h1 Fiber 可能有 Placement。

HostText Fiber 可能有文本节点。

这些副作用需要让父级知道。

所以函数组件 complete 时不能什么都不做。

它虽然不创建 DOM,但要完成子树副作用收集。

十三、bubbleProperties:flags 如何向上冒泡

bubbleProperties(workInProgress) 是 complete 阶段非常关键的一步。

它的作用是:

复制代码
把子 Fiber 的 flags 和 subtreeFlags 汇总到当前 Fiber 的 subtreeFlags 上

简化逻辑:

复制代码
function bubbleProperties(completedWork) {
  let subtreeFlags = NoFlags
  let child = completedWork.child

  while (child !== null) {
    subtreeFlags |= child.subtreeFlags
    subtreeFlags |= child.flags

    child.return = completedWork

    child = child.sibling
  }

  completedWork.subtreeFlags |= subtreeFlags
}

这一步很重要。

commit 阶段不可能盲目遍历整棵 Fiber 树。

它需要快速知道:

复制代码
某个子树里有没有需要提交的副作用

如果一个 Fiber 的:

复制代码
subtreeFlags === NoFlags

说明这棵子树没有需要处理的 commit 副作用。

commit 阶段就可以跳过很多遍历。

所以 bubbleProperties 是 render 阶段为 commit 阶段做索引。

它把分散在子 Fiber 上的 flags 向父级聚合。

十四、flags 和 subtreeFlags 的区别

Fiber 上有两个容易混淆的字段:

复制代码
flags
subtreeFlags

flags 表示当前 Fiber 自己的副作用。

例如:

复制代码
当前 Fiber 需要插入

当前 Fiber 需要更新

当前 Fiber 有 ref

当前 Fiber 有 passive effect

subtreeFlags 表示子树中的副作用。

例如:

复制代码
当前 Fiber 的某个后代节点需要插入

当前 Fiber 的某个后代节点需要更新

当前 Fiber 的某个后代节点有 effect

举个例子:

复制代码
function App() {
  return <div><span>Hello</span></div>
}

如果 span Fiber 有 Placement:

复制代码
span.flags 包含 Placement

那么 complete div Fiber 时,会把 span 的 flags 冒泡到:

复制代码
div.subtreeFlags

complete App Fiber 时,又会继续冒泡到:

复制代码
App.subtreeFlags

最后 Root 就能知道:

复制代码
这棵树下面有需要 commit 的副作用

十五、HostRoot 的 completeWork 做什么

HostRootFiber 不创建 DOM。

它对应的是 React 根 Fiber,不是实际 DOM 节点。

真实根容器在:

复制代码
FiberRootNode.containerInfo

例如:

复制代码
<div id="root"></div>

HostRoot complete 时,主要做:

复制代码
popHostContainer

处理 pending context

bubbleProperties

标记 render 阶段完成相关状态

真正把整棵新 DOM 子树插入 root container,是 commit 阶段的事情。

也就是说:

复制代码
HostRoot complete 完成后,render 阶段基本就接近结束

但页面仍然没有被修改

十六、completeWork 和 commit 的关系

completeWork 会创建 DOM,但不插入页面。

commit 会把 DOM 插入页面。

它们之间的关系是:

复制代码
completeWork:
  创建 DOM 实例
  设置初始属性
  把子 DOM append 到父 DOM
  收集 flags
  构建完成的 workInProgress tree

commit:
  找到带有 flags 的 Fiber
  执行 DOM 插入、更新、删除
  切换 root.current
  执行 layout effect
  调度 passive effect

例如初次渲染:

复制代码
root.render(<App />)

在 render 阶段,completeWork 可能已经构建出一棵离屏 DOM 树:

复制代码
div
  h1
    "Hello"
  p
    "React 19"

但这棵 DOM 树还没有挂到:

复制代码
<div id="root"></div>

等到 commit mutation 阶段,React 才会把它插入到 root container。

十七、为什么 completeWork 不是 commit

很多人会把这两个阶段混在一起。

因为 completeWork 里确实创建了 DOM。

但创建 DOM 不等于提交 DOM。

这里要区分两个动作:

复制代码
创建节点

插入节点

render 阶段可以创建节点,因为这些节点还没有影响页面。

commit 阶段才能插入节点,因为插入节点会改变页面。

所以:

复制代码
completeWork 属于 render 阶段

commitMutationEffects 才是真正修改页面的阶段

这个边界是 React 并发渲染安全性的基础。

十八、完整例子:从 beginWork 到 completeWork

代码:

复制代码
function App() {
  return (
    <div className="app">
      <h1>Hello</h1>
      <p>React 19</p>
    </div>
  )
}

begin 阶段:

复制代码
beginWork App
  执行 App()
  创建 div Fiber

beginWork div
  读取 div.props.children
  创建 h1 Fiber 和 p Fiber

beginWork h1
  读取 h1.props.children
  创建 HostText Fiber "Hello"

beginWork HostText
  返回 null

complete 阶段开始:

复制代码
completeWork HostText "Hello"
  createTextInstance("Hello")
  HostText.stateNode = textNode

然后回到 h1:

复制代码
completeWork h1
  createInstance("h1")
  appendAllChildren(h1DOM, h1Fiber)
  把 textNode append 到 h1DOM
  finalizeInitialChildren(h1DOM, "h1", props)
  h1Fiber.stateNode = h1DOM
  bubbleProperties(h1Fiber)

然后处理 p:

复制代码
beginWork p
  创建 HostText Fiber "React 19"

completeWork HostText "React 19"
  createTextInstance("React 19")

completeWork p
  createInstance("p")
  append textNode 到 pDOM
  pFiber.stateNode = pDOM

然后回到 div:

复制代码
completeWork div
  createInstance("div")
  appendAllChildren(divDOM, divFiber)
  append h1DOM 到 divDOM
  append pDOM 到 divDOM
  finalizeInitialChildren(divDOM, "div", props)
  divFiber.stateNode = divDOM

最后回到 App:

复制代码
completeWork App
  不创建 DOM
  bubbleProperties(AppFiber)

最终 render 阶段得到:

复制代码
App Fiber
  child -> div Fiber stateNode=divDOM
            child -> h1 Fiber stateNode=h1DOM
                      child -> HostText Fiber stateNode=textNode
            sibling -> p Fiber stateNode=pDOM
                       child -> HostText Fiber stateNode=textNode

十九、completeWork 结束后得到什么

当整棵树 complete 完成后,React 得到的是:

复制代码
一棵完成计算的 workInProgress Fiber tree

每个 HostComponent / HostText Fiber 上挂着 stateNode

Fiber 上带着 flags 和 subtreeFlags

Root 上可以拿到 finishedWork

但页面仍然还没变。

此时 React 只是准备好了:

复制代码
即将提交到页面的结果

后续会进入:

复制代码
commitRoot

然后再进入 commit 的几个阶段:

复制代码
beforeMutation

mutation

layout

passive

其中真正插入、更新、删除 DOM 的是:

复制代码
mutation 阶段

二十、这一篇的完整调用链

把这一篇串起来:

复制代码
workLoopSync 或 workLoopConcurrent

performUnitOfWork(fiber)

beginWork(current, fiber, renderLanes)

如果 beginWork 返回 child:
  workInProgress = child
  继续向下

如果 beginWork 返回 null:
  completeUnitOfWork(fiber)

completeUnitOfWork

completeWork(current, completedWork, renderLanes)

根据 completedWork.tag 分支:

HostText:
  createTextInstance
  workInProgress.stateNode = textInstance
  bubbleProperties

HostComponent:
  createInstance
  appendAllChildren
  finalizeInitialChildren
  workInProgress.stateNode = instance
  bubbleProperties

FunctionComponent:
  不创建 DOM
  bubbleProperties

Fragment:
  不创建 DOM
  bubbleProperties

HostRoot:
  不创建 DOM
  pop host context
  bubbleProperties

complete 当前 Fiber 后:

如果有 sibling:
  workInProgress = sibling
  继续 performUnitOfWork

如果没有 sibling:
  completedWork = returnFiber
  继续向上 complete

直到 HostRoot complete 完成

render 阶段得到 finished workInProgress tree

二十一、这一篇最重要的结论

第一,render 阶段分为 beginWorkcompleteWork 两个方向。

第二,beginWork 负责向下创建或复用子 Fiber。

第三,completeWork 负责向上完成当前 Fiber。

第四,真实 DOM 的创建主要发生在 completeWork 阶段。

第五,HostText 会在 complete 阶段通过 createTextInstance 创建文本节点。

第六,HostComponent 会在 complete 阶段通过 createInstance 创建元素节点。

第七,appendAllChildren 会把当前 Fiber 子树中最近的宿主子节点 append 到当前 DOM 实例上。

第八,函数组件、Fragment 等 Fiber 本身没有 DOM,它们在 complete 阶段主要做 flags 冒泡。

第九,bubbleProperties 会把子 Fiber 的 flagssubtreeFlags 汇总到父 Fiber 的 subtreeFlags 上。

第十,flags 描述当前 Fiber 自己的副作用,subtreeFlags 描述子树里的副作用。

第十一,completeWork 可以创建 DOM,但不会把 DOM 插入页面。

第十二,真正插入、更新、删除 DOM,要等 commit 阶段。

第十三,render 阶段可以被中断、重试、丢弃,所以不能在 render 阶段修改页面。

第十四,commit 阶段不可中断,因为它会真正修改宿主环境。

相关推荐
bbq粉刷匠1 小时前
了解HTML、CSS与JavaScript
javascript·css·html
希冀1231 小时前
【CSS学习第六篇】
前端
Python大数据分析@1 小时前
说说Markdown为什么不会被HTML取代
前端·html
史迪仔01121 小时前
[QML] Qt5/6图像色彩空间处理
开发语言·前端·c++·qt
白嫖叫上我1 小时前
Vue3+iconfont图标选择器封装
前端·vue
橙淮1 小时前
jQuery性能优化终极指南
javascript·jquery
小小编程路1 小时前
增强版 JavaScript 读取 Excel
开发语言·javascript·excel
ID_180079054731 小时前
淘宝店铺所有商品 API 接口:核心能力与数据返回参考
java·服务器·前端
Hello--_--World1 小时前
vite:什么是热更新?vite 和 webpack 有什么区别?vite常见配置和优化手段?
前端·webpack·node.js