我写的 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,彻底避免逻辑进入下一个分支
相关推荐
葡萄城技术团队8 小时前
1000 人协同压测的环境瓶颈排查:CPU 争用与 Wi\-Fi
前端
excel9 小时前
Marked 与 Markdown 有什么区别?该怎么选?
前端
Da Da 泓10 小时前
五子棋 Web 端测试用例报告
前端·网络·websocket·网络协议·测试用例·项目
zxcvb15310 小时前
远程查看本地文档:一条轻量 Web 文件服务的工程化链路
前端·远程查看文档
niucloud-admin11 小时前
NIUCLOUD PHP SAAS上门家政使用文档:城市管理
java·前端·php
程序员大阳11 小时前
副队长JavaScript教程(50)-DOM:网页开发的起点
javascript·dom·es·节点·概念·document·关系
北岛贰11 小时前
啵啵音乐迎来全新升级!
前端
hkNaruto13 小时前
【AI】基于大模型的AI游戏地图生成、控制方案:AI Native Game Harness 迁移操作手册
前端·人工智能·游戏
IT_陈寒13 小时前
Java序列化踩坑记:对象循环引用让我加班两小时
前端·人工智能·后端
Acro13 小时前
快给macOS 上的Grok Bot换一套皮肤
前端