我写的 switch…break 被 Babel 偷偷吞了

省流版:一个 switch-case 里的 break,源码运行正常,构建后却失效并穿透到了下一个 case。根因是 @babel/plugin-transform-block-scoping 在做块级作用域转换时,把 break 降级成了函数返回值信号,而内层新生成的 switch 把它吞掉了。升级该插件到 7.10.5+ 即可修复。

事件篇:源码没错,构建后崩了

我曾一直以为,switch 不过是 if-else 的方便写法罢了,直到有一天,我写了这样一段代码:

javascript 复制代码
function serialize(items, ignore) {
  const data = []

  items.forEach(item => {
    switch (item.templateStyle) {
      case 'selected-brand-module': {
        const maxRank = 5
        if (!ignore && !item.title) return


        for (let i = 0; i < item.groups.length; i++) {
          item.groups[i].sort(
            (a, b) =>
              Math.min(a.rank, maxRank) - Math.min(b.rank, maxRank)
          )
        }

        data.push({
          templateStyle: item.templateStyle,
          templateName: 'brand'
        })
        break
      }

      case 'theme-template': {
        data.push({
          templateStyle: item.templateStyle,
          templateName: 'category'
        })
        break
      }
    }
  })

  return data
}

const items = [{
  templateStyle: 'selected-brand-module',
  title: '',
  groups: [[{ rank: 2 }, { rank: 1 }]]
}]

console.log(serialize(items, true))

显而易见,传入的 items 里只有一个 selected-brand-module 元素,因此输出的应该是一个长度为 1 的数组:

css 复制代码
[  {templateStyle: 'selected-brand-module', templateName: 'brand'}]

可实际上,最终输出的是:

css 复制代码
[  {templateStyle: 'selected-brand-module', templateName: 'brand'},  {templateStyle: 'selected-brand-module', templateName: 'category'}]

你大概会把我前面的代码复制到 Node 或浏览器控制台里跑一遍,发现并没有我说的两个元素,而是正常的一个元素------我当时的疑惑比你还要大。我以为是自己 Vue 项目里出了诡异情况,而将代码原封不动复制到独立运行环境中,确实正确。

初步可以判断的是,selected-brand-module 分支中的 break 并没有打断逻辑,代码以一个诡异的方式继续运行下去,进入了 theme-template 分支!

我的 Vue 项目染上脏东西了!

解谜篇:凶手是 Babel

净手、沐浴、焚香之后,大喊了三声"我是无神论者!!!",我开始冷静分析这其中的差异,一个单词便浮现在我眼前:

babel

我把最小示例直接交给现代浏览器和 Node 执行,都只得到一个对象;但在当前 Vue CLI 项目里,浏览器运行的是经过构建链转换的代码。于是我对比了浏览器实际执行的脚本与源码,发现异常出现在 Babel 对块级作用域的转换中。为了排除其他构建步骤的影响,我只启用 @babel/plugin-transform-block-scoping 转译最小示例:

php 复制代码
const { transformSync } = require('@babel/core')

const source = `
function serialize(items, ignore) {
  const data = []
  
  items.forEach(item => {
    switch (item.templateStyle) {
    case 'selected-brand-module': {
    const maxRank = 5
    if (!ignore && !item.title) return
    
    for (let i = 0; i < item.groups.length; i++) {
    item.groups[i].sort(
    (a, b) =>
    Math.min(a.rank, maxRank) - Math.min(b.rank, maxRank)
  )
        }

        data.push({
          templateStyle: item.templateStyle,
          templateName: 'brand'
        })
        break
      }

      case 'theme-template': {
        data.push({
          templateStyle: item.templateStyle,
          templateName: 'category'
        })
        break
      }
    }
  })

  return data
}
`

const input = () => [{
  templateStyle: 'selected-brand-module',
  title: '',
  groups: [[{ rank: 2 }, { rank: 1 }]]
}]

const run = code => Function(`${code}; return serialize`)()(
  input(),
  true
).map(({ templateStyle, templateName }) => ({
  templateStyle,
  templateName
}))

const compiled = transformSync(source, {
  plugins: ['@babel/plugin-transform-block-scoping'],
  babelrc: false,
  configFile: false
}).code

console.log('原生:', run(source))
console.log('Babel:', run(compiled))

最终的结果显而易见了(新机子哇一直摸你肚子)------凶手就是 babel :

解决篇:编译产物露了馅

发现了凶手之后,只要把 compiled 打印出来,事情就昭然若揭了:

javascript 复制代码
function serialize(items, ignore) {
  var data = [];
  items.forEach(item => {
    switch (item.templateStyle) {
      case 'selected-brand-module':
        {
          var _ret = function () {
            var maxRank = 5;
            if (!ignore && !item.title) return {
              v: void 0
            };
            for (var i = 0; i < item.groups.length; i++) {
              item.groups[i].sort((a, b) => Math.min(a.rank, maxRank) - Math.min(b.rank, maxRank));
            }
            data.push({
              templateStyle: item.templateStyle,
              templateName: 'brand'
            });
            return "break";
          }();
          switch (_ret) {
            case "break":
              break;
            default:
              if (typeof _ret === "object") return _ret.v;
          }
        }
      case 'theme-template':
        {
          data.push({
            templateStyle: item.templateStyle,
            templateName: 'category'
          });
          break;
        }
    }
  });
  return data;
}

重点就在于这一段:

csharp 复制代码
switch (_ret) {
  case "break":
    break;
  default:
    if (typeof _ret === "object") return _ret.v;
}

也就是说,Babel 编译之后,又在里面加了一层 switch,而我们之前代码里的 break 被用来处理内层 switch 的跳出,最终没有成功作用到 selected-brand-module 分支的跳出,从而进入了 theme-template 分支。那么为什么 Babel 要这么编译呢?经过代码修改的反复实验,这个问题由以下几个原因共同造成:

  • 因为 maxRank 的闭包,整个分支的逻辑被封装成了一个函数 _ret()
  • 因为函数中存在多个不同逻辑的退出机制(!ignore && !item.title 时的 return 和这段逻辑最后的 break),Babel 采用了 switch 来判断 _ret() 的返回值
  • 而在新的 switch 中,获取到 break 逻辑时仅跳出了新 switch,没有办法让原来外层 switch 里的代码逻辑中断

所以,如果你手上也在维护一个老版本 Babel 的项目,建议顺手做三件事:

  • 升级 @babel/plugin-transform-block-scoping 到 7.10.5 及以上版本,这个坑官方已经修了(修复版本改用了 if-else,而没有继续套一层 switch)
  • 正确使用闭包:像上面那段示例代码,maxRank 完全可以写到 for 循环的作用域内、根本不需要闭包;如果逻辑的作用域比较复杂,尽量用 if-else 代替 switch
  • switch 分支后如果没有其他逻辑,直接在分支里用 return 退出整个函数来代替 break,彻底避免逻辑进入下一个分支
相关推荐
烈风逍遥42 分钟前
第六篇:RAG 知识库构建与检索全链路
前端·人工智能·后端
爱丶不疚1 小时前
什么是 Jev 决策模型?它适合干什么?
前端·agent
liuchangng1 小时前
Jev 模型研究:从生成式大模型到决策式模型——System One、RLCD 校准与采用边界
java·javascript·人工智能·python·深度学习
江畔柳前堤1 小时前
字节跳动·大模型应用知识手册
前端·人工智能·深度学习·opencv·目标检测·重构·transformer
LEE2 小时前
前端转型全栈 03:接口失败也返回 200,OpenAPI 契约与错误码怎么定
前端·后端·全栈
我命由我123452 小时前
CSS - CSS 媒体查询 orientation
前端·javascript·css·html·css3·html5·js
lichenyang4533 小时前
让 VK 小程序调用 HarmonyOS 原生能力:一次跨端 Bridge SDK 的设计与实践
前端
阳光宅男@李光熠3 小时前
【电子通识】一起学习TDK的EMC基础——电池兼容设计方法概述
java·前端·数据库
南雨北斗4 小时前
vue3项目中的env.d.ts文件
前端