对运行时转译的一次尝试

函数级转译

现在大部分 JavaScript 转译器transpiler的正确使用方法都是在发布之前先生成目标代码JavaScript然后直接交由浏览器执行比如说 Babel几乎所有的使用案例都是让浏览器执行预先编译之后的代码很少有人会把 Babel 也放进浏览器里让客户端去转译

原因当然有很多比如转译器体积太大影响加载速度又比如客户端配置不高转译影响性能再比如光转译不够大多数情况下还需要打包但是如果说这个转译器很小呢它可能不像 Babel 那样致力于转译整个文件甚至整个项目如果它的目标只是转译某个函数呢

事实上在 5 年前 async / await 提案还没有出现的时候有个项目叫 Wind.js 风靡一时用的就是这个套路

var fib = eval(Wind.compile("async", function () {
  var a = 0, current = 1;
  while (true) {
    var b = a;
    a = current;
    current = a + b;
    $await(Wind.Async.sleep(1000));
    console.log(current);
  }
}));
fib().start();

Wind.compile() 会把传入的函数编译成 CPS 风格就是这种回调套回调的写法

(function() {
  var _builder_$0 = Wind.builders["async"];
  return _builder_$0.Start(this,
    _builder_$0.Delay(function() {
      var a = 0,
        current = 1;
      return _builder_$0.While(
        function() {
          return true;
        },
        _builder_$0.Delay(function() {
          var b = a;
          a = current;
          current = a + b;
          return _builder_$0.Bind(Wind.Async.sleep(1000), function() {
            console.log(current);
            return _builder_$0.Normal();
          });
        })
      );
    })
  );
})

为了拿到 Wind.compile() 所在位置的上下文不得不用 eval() 来执行编译后的代码

工作原理其实很简单利用 Function.prototype.toString() 可以拿到函数的源代码

(function() { foo.bar }) + ''
// => "function () { foo.bar }"

对箭头函数一样适用

((ans = 42) => ans) + ''
// => "(ans = 42) => ans"

上面的例子里有个 foo.barWind.js 的例子里有个 $await()这些可能都是没有定义的如果直接执行会抛异常但是只要没有语法错误单单转化成字符串是没问题的这也是为什么 Wind.js 只能定义一个虚拟的 $await() 函数却不能增加一个 await 关键字一样——函数体里的代码会被 JavaScript 引擎解析必须没有语法错误可以看到Wind.js 转译后的代码中已经没有 $await() 了

这样要转译传进来的函数只需要把它转化成字符串得到源码转译成目标代码继而返回或者用它来构造 Function

const doge = fn => {
  let [, body] = /^\(\)\s*=>\s*([^]*)/.exec(fn) || [];
  if (!body) return fn;
  body = body.replace(/\d+/, $0 => parseInt($0, 10) + 1);
  return new Function(body);
};

const print42 = doge(() => console.log(41));
print42(); // => 42

上面代码里的

() => console.log(41)

经过 doge 转译器转译之后变成了一个打印 42 的函数

转译函数相比较转译文件规模更小不过很少有人会用这种方式去转译自己的语言除非设计的语言语法上与 JavaScript 兼容

我个人认为比较好的一个应用是 CoffeeScript 里描述 grammar 的时候定义了一个 Jison grammar 生成的 DSL

# Since we're going to be wrapped in a function by Jison in any case, if our
# action immediately returns a value, we can optimize by removing the function
# wrapper and just returning the value directly.
unwrap = /^function\s*\(\)\s*\{\s*return\s*([\s\S]*);\s*\}/

# Our handy DSL for Jison grammar generation, thanks to
# [Tim Caswell](https://github.com/creationix). For every rule in the grammar,
# we pass the pattern-defining string, the action to run, and extra options,
# optionally. If no action is specified, we simply pass the value of the
# previous nonterminal.
o = (patternString, action, options) ->
  patternString = patternString.replace /\s{2,}/g, ' '
  patternCount = patternString.split(' ').length
  return [patternString, '$$ = $1;', options] unless action
  action = if match = unwrap.exec action then match[1] else "(#{action}())"

  # All runtime functions we need are defined on "yy"
  action = action.replace /\bnew /g, '$&yy.'
  action = action.replace /\b(?:Block\.wrap|extend)\b/g, 'yy.$&'

  # Returns a function which adds location data to the first parameter passed
  # in, and returns the parameter.  If the parameter is not a node, it will
  # just be passed through unaffected.
  addLocationDataFn = (first, last) ->
    if not last
      "yy.addLocationDataFn(@#{first})"
    else
      "yy.addLocationDataFn(@#{first}, @#{last})"

  action = action.replace /LOC\(([0-9]*)\)/g, addLocationDataFn('$1')
  action = action.replace /LOC\(([0-9]*),\s*([0-9]*)\)/g, addLocationDataFn('$1', '$2')

  [patternString, "$$ = #{addLocationDataFn(1, patternCount)}(#{action});", options]

如果用 Jison 的语法来描述代码需要作为字符串传入

const grammar = [
  Root: [
    [ '', '$$ = yy.addLocationDataFn(1)(new yy.Block());' ],
    [ 'Body', '$$ = $1;' ],
  ],

  ...
]

一来编辑器没法高亮代码二来 yy.yy.addLocationDataFn() 的频率很高定义了 o 函数之后就可以大大简化而且很漂亮

grammar =

  # The **Root** is the top-level node in the syntax tree. Since we parse bottom-up,
  # all parsing must end here.
  Root: [
    o '',                                       -> new Block
    o 'Body'
  ]

  # Any list of statements and expressions, separated by line breaks or semicolons.
  Body: [
    o 'Line',                                   -> Block.wrap [$1]
    o 'Body TERMINATOR Line',                   -> $1.push $3
    o 'Body TERMINATOR'
  ]

  ...

代码片段级转译

根据 ES2015 的语法TemplateLiteral 前面可以加 CallExpression但是很少见人这么用大多数 template literal 的使用场景还是拿来拼接字符串的不过还是可以举个例子React 的一个组件库 styled-components

要写一个带样式的按钮组件只需要写

import styled from 'styled-components';

const Button = styled.button`
  /* Adapt the colors based on primary prop */
  background: ${props => props.primary ? 'palevioletred' : 'white'};
  color: ${props => props.primary ? 'white' : 'palevioletred'};

  font-size: 1em;
  margin: 1em;
  padding: 0.25em 1em;
  border: 2px solid palevioletred;
  border-radius: 3px;
`;

export default Button;
<Button>Normal</Button>
<Button primary>Primary</Button>

我很喜欢这种设计思想有的人可能并不喜欢不过这篇文章不打算深究 styled-components 的设计好不好而是想探讨一下它对于 template literal 的用法

它的 template literal 是 CSS——一种 DSL领域专用语言MDN 上也举了一个用 template literal 来嵌入 LaTeX 的例子

latex`\unicode`
// Throws in older ECMAScript versions (ES2016 and earlier)
// SyntaxError: malformed Unicode character escape sequence

这里还有个有趣的故事ES2016 以及之前的版本上述代码会抛异常因为在字符串里 \u 有特殊含义是用来转译 Unicode 字符的后面跟的 niconiconi 自然不是合法的十六进制数有人提了个提案 Template Literal Revision允许 template literal 里出现不合法的转义字符同时加了一个 raw 属性来读取未经转译的字符串

function tag(strs) {
  strs[0] === undefined
  strs.raw[0] === "\\unicode and \\u{55}";
}
tag`\unicode and \u{55}`

还有个 String.raw() 供我们使用像极了 C# 里的 verbatim string

String.raw`C:\Users\foo\bar\baz` === "C:\\Users\\foo\\bar\\baz"

总而言之template literal 还是比较适合用来嵌入其他语言尤其是 DSL也可能看作是运行时对其他语言的转译比如说我们可以用 tagged template 来生成和表示 HTML 元素

const el = (strs, ...values) => {
  const raw = String.raw(strs, ...values);
  const [ , tagName, innerHTML ] = /^\s*<([^\s>]+)[^]*?>([^]*?)<\/\1>\s*$/.exec(raw) || [];

  // 这里检测还是很粗略的可以绕过
  if (!tagName) {
    throw new Error('There should be exactly one root element');
  }

  let element = document.createElement(tagName);
  element.innerHTML = innerHTML;
  return element;
};

document.body.appendChild(el`
  <div>
    <p>Hello ${'World'}!</p>
  </div>
`);

嵌入的语言是没有限制的但是如果是世界上最好的语言就比较尴尬了——本身就到处是美元符号需要频繁转义

我写了个完全没有优化的 Brainfxxk 转译器

const stmt = {
  '>': '++ptr;',
  '<': '--ptr;',
  '+': 'data[ptr] = ~~data[ptr] + 1;',
  '-': 'data[ptr] = ~~data[ptr] - 1;',
  '.': 'output += String.fromCharCode(~~data[ptr]);',
  ',': 'if ((data[ptr] = input.charCodeAt(i++)) !== data[ptr]) data[ptr] = -1;',
  '[': 'while (~~data[ptr]) {',
  ']': '}'
};

const brainfxxk = (strs, ...values) => {
  const raw = String.raw(strs, ...values);
  let code = [ ...raw ].map(token => stmt[token]).join('');
  code = 'const data = [];' + code + 'return output;';
  return new Function('ptr', 'i', 'output', 'input', code).bind(this, 0, 0, '');
};

然后找了段 rot13 的实现跑了跑

const rot13 = brainfxxk`
-,+[                         Read first character and start outer character reading loop
    -[                       Skip forward if character is 0
        >>++++[>++++++++<-]  Set up divisor (32) for division loop
                               (MEMORY LAYOUT: dividend copy remainder divisor quotient zero zero)
        <+<-[                Set up dividend (x minus 1) and enter division loop
            >+>+>-[>>>]      Increase copy and remainder / reduce divisor / Normal case: skip forward
            <[[>+<-]>>+>]    Special case: move remainder back to divisor and increase quotient
            <<<<<-           Decrement dividend
        ]                    End division loop
    ]>>>[-]+                 End skip loop; zero former divisor and reuse space for a flag
    >--[-[<->+++[-]]]<[         Zero that flag unless quotient was 2 or 3; zero quotient; check flag
        ++++++++++++<[       If flag then set up divisor (13) for second division loop
                               (MEMORY LAYOUT: zero copy dividend divisor remainder quotient zero zero)
            >-[>+>>]         Reduce divisor; Normal case: increase remainder
            >[+[<+>-]>+>>]   Special case: increase remainder / move it back to divisor / increase quotient
            <<<<<-           Decrease dividend
        ]                    End division loop
        >>[<+>-]             Add remainder back to divisor to get a useful 13
        >[                   Skip forward if quotient was 0
            -[               Decrement quotient and skip forward if quotient was 1
                -<<[-]>>     Zero quotient and divisor if quotient was 2
            ]<<[<<->>-]>>    Zero divisor and subtract 13 from copy if quotient was 1
        ]<<[<<+>>-]          Zero divisor and add 13 to copy if quotient was 0
    ]                        End outer skip loop (jump to here if ((character minus 1)/32) was not 2 or 3)
    <[-]                     Clear remainder from first division if second division was skipped
    <.[-]                    Output ROT13ed character from copy and clear it
    <-,+                     Read next character
]                            End character reading loop
`;

alert(rot13('Uryyb Jbeyq!'));

有兴趣的可以在 JSFiddle 里跑跑看

全局转译

虽说 Babel 有 standalone 版本可以用在浏览器内但是很少有人这么用官方也不推荐

<div id="output"></div>
<!-- Load Babel -->
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<!-- Your custom script here -->
<script type="text/babel">
const getMessage = () => "Hello World";
document.getElementById('output').innerHTML = getMessage();
</script>

一方面是因为有性能问题毕竟要找出每个 script[type="text/babel"] 分析转译执行相比较普通的 <script> 标签大概会慢不少另一方面这与普通的 <script> 标签行为不一致不论是否内联有没有设置 async 属性都是异步执行的也就没法用 document.write()能做的事情也会打一些折扣

想要做全局的转译并不容易甚至在以前都不可能实现

当然我们可以妥协不做全局转译而是做模块级转译比如说以前火过一阵子的 require.js如果所有代码都是由 require.js 加载的那么自然可以在它的身上动手写一个插件来转译就可以了但这要求编码者接受 AMD 的风格——如果要用工具来转换那么为什么不索性预转译呢

Service worker 给了全局转译一丝转机

我不是很了解 service worker只知道它是 Web 应用浏览器和网络之间的一个代理但是我知道大多数人用它来构建 PWA利用这层代理实现 offline first

既然是一层代理那就可以做很多事情比如说前一阵子看到的 Planktos就利用 service worker 和 WebTorrent 构建浏览器端的点对点网站

当时感觉很惊艳这层代理确实可以做很多事情既然都可以把网站变成点对点了用来转译代码说白了就是修改 response自然没什么问题

搜了一下半年前就有人想到去做这件事情了但是我也只找到两个相关项目 / 讨论

值得一提的是Safari 技术预览版和 Edge Build 14342 已经支持 import两者配合起来可能挺有可玩性的可以把某种语言在 service worker 里转译成 ES6配合 import / export都不需要预编译和打包了

当然这种语言不是 ES6 本身因为支持 service worker 和 import 的浏览器应该已经百分百支持 ES6 了


编译技术对前端来说有什么用?

今天看到了一个断言库power-assertAPI 兼容 assert但是对于

assert(this.ary.indexOf(zero) === two)

会输出

  1) Array #indexOf() should return index when the value is present:
     AssertionError: # path/to/test/mocha_node.js:10

  assert(this.ary.indexOf(zero) === two)
              |   |       |     |   |
              |   |       |     |   2
              |   -1      0     false
              [1,2,3]

  [number] two
  => 2
  [number] this.ary.indexOf(zero)
  => -1

可以看到它用最简单的表达式来断言但是当表达式不成立时输出的内容却非常详细

再来一个输出的例子

    assert(this.types[index].name === bob.name)
                |    ||      |    |   |   |
                |    ||      |    |   |   "bob"
                |    ||      |    |   Person{name:"bob",age:5}
                |    ||      |    false
                |    |11     "alice"
                |    Person{name:"alice",age:3}
                ["string",98.6,true,false,null,undefined,#Array#,#Object#,NaN,Infinity,/^not/,#Person#]

    --- [string] bob.name
    +++ [string] this.types[index].name
    @@ -1,3 +1,5 @@
    -bob
    +alice

它把每一级的值都直观地输出了出来

没错它并不是一个级别的东西JavaScript 语言本身的能力还不足以实现这样的库确切地说这是一个 transpilerespower对测试文件 example_test.js

var assert = require('power-assert');
var truthy = 'true';
var falsy = 'false';
assert(falsy);
assert.equal(truthy, falsy);

它会转译成

var _PowerAssertRecorder1 = function () {
    function PowerAssertRecorder() {
        this.captured = [];
    }
    PowerAssertRecorder.prototype._capt = function _capt(value, espath) {
        this.captured.push({
            value: value,
            espath: espath
        });
        return value;
    };
    PowerAssertRecorder.prototype._expr = function _expr(value, source) {
        var capturedValues = this.captured;
        this.captured = [];
        return {
            powerAssertContext: {
                value: value,
                events: capturedValues
            },
            source: source
        };
    };
    return PowerAssertRecorder;
}();
var _rec1 = new _PowerAssertRecorder1();
var _rec2 = new _PowerAssertRecorder1();
var _rec3 = new _PowerAssertRecorder1();
var assert = require('power-assert');
var truthy = 'true';
var falsy = 'false';
assert(_rec1._expr(_rec1._capt(falsy, 'arguments/0'), {
    content: 'assert(falsy)',
    filepath: 'example_test.js',
    line: 4
}));
assert.equal(_rec2._expr(_rec2._capt(truthy, 'arguments/0'), {
    content: 'assert.equal(truthy, falsy)',
    filepath: 'example_test.js',
    line: 5
}), _rec3._expr(_rec3._capt(falsy, 'arguments/1'), {
    content: 'assert.equal(truthy, falsy)',
    filepath: 'example_test.js',
    line: 5
}));

写编译器不意味着创造一种新的语言近几年的前端呈现出一种编译器爆发的状态从最早的 CoffeeScriptWind.js到后来的 Babelespower……编译器做的事情越来越简单CoffeeScript 尚且算是一门新语言其他的就只是 JavaScript => JavaScript 的编译器了

其实对于 JavaScript 来说更需要各种各样的 transpilerC 有 macroC++ 有 template赋予了它们在编译期间的代码生成能力也就是所说的元编程JavaScript 里实现元编程却有一定的门槛而且由于是在运行时实现的因此会存在一些性能上的问题和局限性


一些有趣的 ECMAScript 提案

看了下 ECMAScript 的 active proposals 列表发现了一些有趣的提案这些提案从 stage 1 ~ stage 3 不等有些提案我很期待能实现

1. SIMD.JS - Stage 3

这是个很老的提案但似乎目前只有 Firefox Nightly 实现了SIMD 是指单指令流多数据流同时对一个数据向量中的每一项分别执行操作从而实现在空间上的并行性

上图来自 Wikipedia图中的 4 个处理单元PU分别对每一维数据进行处理SIMD 的应用很明显可以想到 GPU 计算

2. Object Rest / Spread Properties - Stage 3

这个不必多说React 程序员已经用得很溜了

let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
x; // 1
y; // 2
z; // { a: 3, b: 4 }

let n = { x, y, ...z };
n; // { x: 1, y: 2, a: 3, b: 4 }

3. Asynchronous Iterators - Stage 3

这个提案主要提出了异步生成器用于遍历异步生成器的 for-await-of 语句和异步生成器函数

异步生成器与现有的生成器的不同之处在于目前生成器的 next() 返回一个 { value, done } 结构异步生成器则返回一个 Promise<{ value, done }>——因为异步生成器没法在调用 next() 的时候同步返回done要等到异步操作执行完毕之后才知道完成状态

for-await-of 语句则是用于遍历异步生成器的比如

for await (const line of readLines(filePath)) {
  console.log(line);
}

异步生成器函数则是一种异步生成器的构造函数

async function* readLines(path) {
  let file = await fileOpen(path);

  try {
    while (!file.EOF) {
      yield await file.readLine();
    }
  } finally {
    await file.close();
  }
}

看起来跟生成器函数差不多只不过它返回的不再是一个普通的生成器而是一个异步生成器

4. Public Class Fields - Stage 2

现在可以为类定义方法但是没法定义字段这个提议扩充了定义 public field 的语义

class Foo {
  bar = 42;
  static baz = f() + g();
}

new Foo().bar; // => 42
Foo.baz; // f() + g()

5. Promise.prototype.finally - Stage 2

总觉得这是一个本来就应该有的东西许多 Promise方言也有类似的实现

promise.finally(f)promise.then(f, f) 的区别在于

6. Decorators - Stage 2

这个想必也不用说了使用 TypeScript 或者 Babel 的早已经用上了一颗很甜的糖

@frozen class Foo {
  @configurable(false) @enumerable(true) method() {}
}

7. Arbitrary precision integers - Stage 2

提升整型的精度比如说找第 n 个素数

// Takes an Integer as an argument and returns an Integer
function nthPrime(nth) {
  function isPrime(p) {
    for (let i = 2n; i < p; i++) {
      if (p % i === 0n) return false;
    }
    return true;
  }
  for (let i = 2n; ; i++) {
    if (isPrime(i)) {
      if (--nth === 0n) return i;
    }
  }
}

虽然我觉得字面量的表示很难看但是 javascript 里确实迫切需要处理 int64 / uint64 和高精度整型的方法

8. Observable - Stage 1

我是先知道的 ReactiveX后知道有这个提案的ReactiveX 是一个异步编程的 API有各种语言的实现其中 JavaScript 的实现就叫做 RxJS

9. Null Propagation - Stage 1

类似的运算符其实在 CoffeeScript 里就有虽然可能会被滥用但是有了这个运算符之后有些代码真的可以变得优雅

比如说

let firstName = user?.info?.()?.firstName;

原本可能要写成

let firstName;

if (user !== void 0 && user !== null) {
  let info = user.info();
  firstName = (info !== void 0 && info !== null) ? info.firstName : void 0;
}

唯一的问题就是会增加词法分析的成本词法分析器看到 ?. 的时候不知道这是存在运算符也就是这个提案里提出的运算符还是 ?: 三目运算符可能会有人写 foo?.5:1 这样的表达式只有发现 ?. 后面跟的不是十进制数才能把 ?. 作为一个 token

10. do Expressions - Stage 1

函数式的一个提案不过我喜欢

let x = do {
  if (foo()) { f() }
  else if (bar()) { g() }
  else { h() }
};

会返回 do 块里的计算结果上面的代码执行后 x 根据条件可能是 f() 或者 g() 或者 h() 的结果

不过跟上面一个提案一样这个提案同样会增加语法分析的复杂度而且会复杂得多因为要区分 dodo-while只有读到块结束才能知道除非很明显是一个表达式而不是语句


方法还是函数

有时写代码会考虑到底是用方法还是函数当然从本质上来说在 JavaScript 里都是函数只不过前者在原型链上罢了方法给人一种面向对象编程的感觉是某一个类或者某一个对象特有的东西一般不会去把它 apply 到别的对象上函数则有种面向接口编程的感觉只要传入的对象实现了函数所假象出来的某种接口那么就可以由这个函数来操作

比如说下面这个函数

function propOf(target, name) {
  let result = [];
  for (let item of target) {
    result.push(item[name]);
  }
  return result;
}

它用于遍历某一个迭代器把每一项的 name 属性拿出来

那么只要是迭代器并且产生的每一项都是一个 Object就都可以作为这个函数的参数

propOf([ { name: 'Foo' }, { name: 'Bar' } ], 'name'); // => [ 'Foo', 'Bar' ]

propOf(document.querySelectorAll('[href]'), 'href');
// => [ 'https://s.w.org/', 'https://ljh.io/', ... ]

如果它是某个类的方法那么用起来就需要写一堆奇怪的东西来绑定 this 了就像是有时候我们不得不写成

Array.prototype.map.call(elements, ...);

当然ES6 下可以写得更优雅

[ ...elements ].map(...);

但是本质上是先把它变成了 Array

有些比较通用的函数虽说可以操作实现了某一类接口的任意数据类型但是为了在使用的时候更加面向对象我们往往不会考虑把它抽离出来作为单独的函数依然写在某一个类的原型上如果抽离出来作为 utility就会像上面的 propOf() 一样在调用的时候需要手动把对象作为第一个参数传入这两种风格调用方法和把对象作为第一个参数传入的差异会在阅读上造成一定的困扰

propOf(document.querySelectorAll('[href]'), 'href');

就是这样的一个例子这种情况下往往需要拆分成两行才比较易读

let elements = document.querySelectorAll('[href]');
propOf(elements, 'href');

有没有更优雅的方式呢两年前有个提案 ECMAScript This-Binding Syntax提议增加一个 :: 运算符来绑定 this有了这个运算符上面的代码就可以改写成

function propOf(name) {
  let result = [];
  for (let item of this) {
    result.push(item[name]);
  }
  return result;
}

document.querySelectorAll('[href]')::propOf('href');

还可以像链式调用一样接一路

const { map, filter } = Array.prototype;

let sslUrls = document.querySelectorAll('a')
                     ::map(node => node.href)
                     ::filter(href => href.substring(0, 5) === 'https');

console.log(sslUrls);

如果想要体验这个运算符可以用 Babel 的 Function bind transform


Flex item 的 min-width / min-height

弹性盒子布局早已被现代浏览器支持了个遍用起来简单方便能满足大多数场景下的布局需求对于典型的左右上下结构的页面可以抛弃绝对定位转而用弹性盒子来布局了

不过我在使用的过程中遇到过一个问题有时即便设置 overflow 为 autoscroll仍然会撑开 flex item

比如说下面这个例子

<div class="page">
  <div class="sidebar">
    Sidebar
  </div>
  <div class="main">
    <div class="content">
      <div style="width: 300px">Very wide content</div>
    </div>
  </div>
</div>

<style>
body {
  padding: 2em;
}
.page {
  background: rgba(0, 0, 0, .1);
  display: flex;
  width: 200px;
}
.sidebar {
  background: rgba(0, 0, 255, .1);
  width: 100px;
}
.main {
  background: rgba(255, 0, 0, .1);
  display: flex;
  flex: 1;
}
.content {
  flex: 1;
  overflow: auto;
}
</style>

一个简单的左右两栏布局效果是这样的

Sidebar
Very wide content

侧边栏是固定的 100px 宽右边的主区域自适应宽度虽然为 .content 设置了overflow: auto但是内容依然撑开了.main侧边栏也被挤得不成模样

原因在于 CSS 弹性盒子布局的规范为 flex item 设置了一个隐含的最小的大小最小宽度和最小高度min-widthmin-height 的默认值也就不再是0而是新定义的值auto

On a flex item whose overflow is visible in the main axis, when specified on the flex items main-axis min-size property, specifies an automatic minimum size. It otherwise computes to 0 (unless otherwise defined by a future specification).

大致是说如果对一个 overflowvisible 的 flex item 设置 min-sizemin-width / min-heightauto默认值就会指定一个自动的最小值这个最小值一般是内容大小content size指定大小specified size的较小值但是如果没有指定大小但是指定了比例比如上面的 .main那么自动的最小值将会是内容大小和 transferred size 之间的较小值

如果想让内容区域出现滚动条而不是撑开主区域可以为 .main 显式设置 min-width0

main {
  background: rgba(255, 0, 0, .1);
  display: flex;
  flex: 1;
  min-width: 0;
}

效果如下

Sidebar
Very wide content