vite5.0 hmr中是如何处理循环依赖的

vite是依靠浏览器支持原生es模块来解析文件的,所以我们先看看es模块(简称为esm)中是如何处理循环依赖的。

案例

假如有3个文件,a.js, b.js, c.js,其中a.js导入了b.js且a.js 中有对b.js内容的使用,b.js中导入了c.js,c.js中导入了a.js。它们的代码如下:

javascript 复制代码
// a.js
import b from './b.js'
// a.js 中导入了b模块的内容,这一块就要求b模块一定要在a.js模块模块加载之前加载
console.log(b)

const a = 'aaaaaaa'
export default a
javascript 复制代码
// b.js
import c from './c.js'
export default 'moduleB'
javascript 复制代码
// c.js
import a from './a.js'
const c = 'ccc'
export default c

再创建一个indexEntryA.htmlindexEntryB.html

html 复制代码
<!-- indexEnteryA.html  -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>circle import in vite/webpack</title>
</head>
<body>
    <!-- 入口是a.js -->
    <script src="./a.js" type="module"></script>
</body>
</html>
xml 复制代码
<!-- indexEnteryB.html  -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>circle import in vite/webpack</title>
</head>
<body>
  	<!-- 入口是b.js -->
    <script src="./b.js" type="module"></script>
</body>
</html>

两个文件,script标签不同,我们放到浏览器中执行它们。

indexEntryA.html中所有js文件能够执行成功,而indexEntryB.html中会报错,错误如下:

Uncaught ReferenceError: Cannot access 'b' before initialization

这个错误产生的原因是,当前b这个变量还没有定义,而我们却使用了。

那为什么indexEntryA.html中导入a.js不会报错,而indexEntryB.html导入的b.js就会报错呢?

esm处理循环依赖

esm处理循环依赖的方式和nodejscommonjs相似,都是使用了缓存,深度优先遍历。

只不过浏览器中的esm加载模块的时候是异步的,commonjs加载模块的时候是同步的。而且esm的导入值和commonjs中也有很大区别。

具体如何处理循环依赖的可以看下面文章:

从模块的循环加载看ESM与CJS - 掘金

ES6 入门教程

简单描述下indexEntryA.htmla.js的加载过程:

  • indexEntryA.html发现导入a.js,此时浏览器去缓存中查找,没有找到去请求a.js的文件
  • a.js下载好后,开始执行。执行第一行发现导入了b.js,还是先去查找缓存,当发现没有缓存时候会去下载b.js
  • b.js下载好后,开始执行。执行第一行发现导入了c.js,还是先去查找缓存,当发现没有缓存时候会去下载c.js
  • c.js下载好后,开始执行。执行第一行发现导入了a.js,还是先去查找缓存,此时发现了缓存中a.js。此时直接使用缓存中的a.js,继续执行c.js剩余代码内容,然后c.js执行完毕。
  • c.js执行完毕后,执行b.js剩余代码内容 直到模块加载完毕。
  • b.js执行完毕后,最后执行a.js剩余内容直到模块执行完毕。
  • 这边a.js能够导入不报错的原因是,b.jsa.js加载之前已经全部加载完毕了。b.js中的内容对a.js是完全可访问的。

同理indexEntryB.html中执行b.js时,当代码执行到a.js时此时b.js中已经在模块缓存中,但由于深度优先遍历的原因,此时模块b.js没有导出内容,而此时a.js中使用了b.js中的内容,由于esm默认使用了严格模式,变量必须定义才能使用(函数可能会有提升)。导致了报错

所以对于 a.js -> b.js -> c.js -> a.js 这种循环依赖,从a.js加载是可以成功加载文件的,但是如果我们从b.js加载是会报错的。

Vite5.0 判断循环依赖

这几天在看vite热更新的源码,其中发现当文件内容改变后,vite会根据文件判断页面是热更新,还是页面重载。这两者的区别是:热更新vite只会请求触发热更新的文件(xxx.js?v=时间戳),而页面重载的vite会重新请求项目入口文件(一般是main.{js,ts})。

vite找到热更新文件b.js后,vite5.0新增了一个查找b.js文件是否存在于一个循环依赖中。

就拿上面例子来说,a.js -> b.js -> c.js -> a.js 如果b.js是个热更新边界,当c.js中代码变动后,vite找到了热更新边界b.js,此时vite会查找导入b.js模块(vite源码里面的变量叫importers),查找importers中有没有被c.js导入从而形成了循环依赖,如果有的话,停止热更新,直接重载页面。没有的话则继续热更新。

具体的代码:

javascript 复制代码
// vite/packages/vite/src/node/server/hmr.ts
/**
 * Check importers recursively if it's an import loop. An accepted module within
 * an import loop cannot recover its execution order and should be reloaded.
 * 递归的检查接受热更新的模块的importers是否形成了循环依赖,一个循环依赖不能恢复重新执行的顺序
 * 并且应该重载
 *
 * @param node The node that accepts HMR and is a boundary
 * 接受hmr的moduleNode
 * @param nodeChain The chain of nodes/imports that lead to the node.
 * 文件发生变动的moduleNode到接受hmr moduleNode之间的路径
 *   (The last node in the chain imports the `node` parameter)
 * @param currentChain The current chain tracked from the `node` parameter
 * hmr moduleNode到其importer之间的路径
 * @param traversedModules The set of modules that have traversed
 */
function isNodeWithinCircularImports(
  node: ModuleNode,
  nodeChain: ModuleNode[],
  currentChain: ModuleNode[] = [node],
  traversedModules = new Set<ModuleNode>(),
): HasDeadEnd {
  // To help visualize how each parameters work, imagine this import graph:
  //
  // A -> B -> C -> ACCEPTED -> D -> E -> NODE
  //      ^--------------------------|
  //
  // ACCEPTED: the node that accepts HMR. the `node` parameter.
  // NODE    : the initial node that triggered this HMR.
  //
  // This function will return true in the above graph, which:
  // `node`         : ACCEPTED
  // `nodeChain`    : [NODE, E, D, ACCEPTED]
  // `currentChain` : [ACCEPTED, C, B]
  //
  // It works by checking if any `node` importers are within `nodeChain`, which
  // means there's an import loop with a HMR-accepted module in it.

  if (traversedModules.has(node)) {
    return false
  }
  traversedModules.add(node)

  for (const importer of node.importers) {
    // Node may import itself which is safe
    // 自己导入自己的模块
    if (importer === node) continue

    // a PostCSS plugin like Tailwind JIT may register
    // any file as a dependency to a CSS file.
    // But in that case, the actual dependency chain is separate.
    if (isCSSRequest(importer.url)) continue
    

    // Check circular imports
    // 这边判断importer 是否在nodeChain中
    const importerIndex = nodeChain.indexOf(importer)
    if (importerIndex > -1) {
      // Log extra debug information so users can fix and remove the circular imports
      if (debugHmr) {
        // Following explanation above:
        // `importer`                    : E
        // `currentChain` reversed       : [B, C, ACCEPTED]
        // `nodeChain` sliced & reversed : [D, E]
        // Combined                      : [E, B, C, ACCEPTED, D, E]
        const importChain = [
          importer,
          ...[...currentChain].reverse(),
          ...nodeChain.slice(importerIndex, -1).reverse(),
        ]
        debugHmr(
          colors.yellow(`circular imports detected: `) +
            importChain.map((m) => colors.dim(m.url)).join(' -> '),
        )
      }
      return 'circular imports'
    }

    // Continue recursively
    // 递归查找importer的importer
    if (!currentChain.includes(importer)) {
      const result = isNodeWithinCircularImports(
        importer,
        nodeChain,
        currentChain.concat(importer),
        traversedModules,
      )
      if (result) return result
    }
  }
  return false
}

拓展:

深拷贝中如何处理循环依赖?

javascript 复制代码
// 使用weakmap 来保存已经创建的对象,避免重复创建
const wm = new WeakMap();
const deepClone = (obj) => {
  let cloneObj = {};
  // 非对象直接返回
  if (typeof obj !== "function" && typeof obj !== "object") {
    return obj;
  }

  // 查看是否有缓存
  if (wm.has(obj)) {
    return wm.get(obj);
  }

  // 对象
  wm.set(obj, cloneObj);
  for (let key of Object.keys(obj)) {
    cloneObj[key] = deepClone(obj[key]);
  }

  return cloneObj;
};

链表中里面有个环节点,如何找到环开始的节点?

leetcode链接

javascript 复制代码
var detectCycle = function(head) {
    let cur = head
    const ws = new WeakSet()

    while(!ws.has(cur) && cur) {
        ws.add(cur)
        cur = cur.next
    }

    return cur ?  cur : null
    
};
相关推荐
拾光拾趣录1 天前
Vite 与 Webpack 热更新原理
前端·webpack·vite
20262 天前
11. vite打包优化
前端·javascript·vite
AverageJoe19912 天前
一次vite热更新不生效问题排查
前端·debug·vite
做梦都在学习前端6 天前
发布一个monaco-editor 汉化包
前端·npm·vite
前端进阶者6 天前
vite调试node_modules下面插件
前端·vite
天天鸭6 天前
写个vite插件自动处理系统权限,降低99%重复工作
前端·javascript·vite
charlee448 天前
nginx部署发布Vite项目
nginx·性能优化·https·部署·vite
微风好飞行19 天前
Vite 打包 vscode 扩展遇到的模块问题
javascript·vscode·vite
风吹一夏v22 天前
webpack到vite的改造之路
webpack·vue·vite
EndingCoder22 天前
性能优化中的工程化实践:从 Vite 到 Webpack 的最佳配置方案
webpack·性能优化·vite·devops·工程化实践