编译过程

Clojure 语言本身是编译到 JVM Bytecode, 而 ClojureScript 则是编译到 JavaScript.

Macros

Clojure(Script) 编译过程大概经历三个阶段:

  • 读取: 经字符串, 将 Macro 进行展开
  • 分析: 基于读取的符号构建 AST
  • 生成: 产生目标输出, 比如编译到 JavaScript

这个过程对于 Clojure 和 ClojureScript 来说类似, 细节参考 Compilation Pipeline.

比如这样一段 ClojureScript 代码, 参考 Mike 的文章:

(defn f [long-name]
  (let [process (fn [foo]
                  (str "abc" "def"
                    (+ 1 2 3) foo))]
    (process long-name)))

通过 ClojureScript 的编译器会被编译成:

function foo$core$g(long_name) {
  var process = function(foo__$1) {
    return ["abc", "def",
      cljs.core.str.cljs$core$IFn$_invoke$arity$1(1 + 2 + 3),
      cljs.core.str.cljs$core$IFn$_invoke$arity$1(foo__$1)
    ].join("");
  };
  return process.call(null, long_name);
}

Google Closure Compiler

对于 JavaScript 而言, 在上线之前仍然需要进行优化, 比如前端常用 Uglify 优化代码体积. 在 cljs 社区, 普遍使用 Google Closure Compiler 进行优化, 可以做一些更深层的优化, 可以得到:

function(a) { return ["abcdef",
  cljs.core.str.cljs$core$IFn$_invoke$arity$1(6),
  cljs.core.str.cljs$core$IFn$_invoke$arity$1(a)].join("");
}

可以得到代码被进一步简化了, 甚至部分的代码被预先做了计算.

cljs 编译器生成的代码可读性并不是那么好, 在使用有些函数的情况下会更加难读. cljs 生成的代码本身针对 Google Closure Compiler 优化, 并且严重依赖 Dead Code Elimination 功能来控制体积.

Google Closure Compiler 主要有 4 个编译选项:

  • :none 不做任何优化, 不合并文件
  • :whitespace 去除空白, 合并文件
  • :simple 合并文件, 重命名局部变量
  • :advanced 合并文件, 去除无用代码, 压缩混淆代码

由于 Google Closure Compiler 基于 Java 实现, 所以大部分 cljs 编译过程依赖 JVM. 比如 Lein 和 Boot 就基于 JVM 环境运行, 而 shadow-cljs 会调用系统的 JVM.

另一种办法是把 cljs 的编译器整个编译到 JavaScript, 同时使用 Closure Compiler 的 JavaScript 版本. 这种情况当中使用的 cljs 称为 Self-hosted ClojureScript, 有时也叫 Bootstraped ClojureScript. 比如 Planck 和 Lumo 就用了 Self-hosted ClojureScript, 可以使用 Node.js 来编译 cljs. 使用 Self-hosted cljs 相对而言冷启动较快, 但编译速度较慢, 适合在 REPL 中使用. 不过在 Self-hosted cljs 当中某些语言特性的细节会不同, 比如 Macro 的用法等等.

results matching ""

    No results matching ""