我写的 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,彻底避免逻辑进入下一个分支
相关推荐
IT_陈寒5 小时前
Vue的数组更新把我坑惨了
前端·人工智能·后端
于航5 小时前
分层上下文压缩,幻觉检测,错误累积概要
前端
数据掘金5 小时前
鸿蒙统计的权限弹窗怎么适配?
前端
数据掘金5 小时前
鸿蒙统计的多设备协同数据怎么打通?
前端
尘中远5 小时前
Qwt7的曲线渲染平滑实现:高斯卷积、Savitzky-Golay 回归与特征保护
前端
Fly6 小时前
我来提需求,你带着 Codex 做项目:AI 全栈课程开始实战了
前端
集智飞行6 小时前
解决mavros2 ros2版本cpu占用高的问题
java·服务器·前端
w2sfot7 小时前
【JS加密在线】jsjiami.online(让JavaScript 更安全!)
开发语言·javascript·安全
HSunR7 小时前
ruoyi 若依 自定义注解 参数校验
java·前端·数据库
拆房老料8 小时前
ONLYOFFICE也能像Microsoft Word和WPS一样分别设置中西文字体
前端·html·word·开源软件·wps