JSX vs Template

“为什么要使用 JSX,而不用其他传统模版引擎?”

·

2 min read

Table of contents

前言

之前会上,加缪老师对我发起了灵魂拷问,“为什么要使用 JSX,而不用其他传统模版引擎?”。这个问题太过耳熟,耳熟到我一度以为我能脱口而出一个可靠的答案。但张了张嘴又无奈合上,并试图顾左右而言他,试图临时得到一个的答案。因为在发出声响前,那些我曾信以为真的答案,在时间和知识的增长下,突然变得脆弱不堪。一张嘴便暴露了我的无知,所以我需要更可靠的知识来重新审视这个问题,于是便有了这篇文章。

选择 JSX 到底是在选择什么?

JSX is an XML-like syntax extension to ECMAScript without any defined semantics.

JSX 本身只是一个 JavaScript 的句法拓展,并没有增加额外的语言特性,即便没有 JSX,单纯的 JavaScript 也能够在不改变代码心智模型的前提下快速实现相同的效果。

所以在讨论具体 JSX 在句法上的优点之前,我们还需要讨论另外一个在选择 JSX 前默认会选择的选项。为什么我们选择将“视图描述”和“视图逻辑”聚合在一起,而不是将两者解耦。

为什么选择将“视图描述”和“视图逻辑”聚合在一起?

换言之,将“视图描述”和“视图逻辑”解耦有什么问题吗?

低耦合、高内聚

耦合性(英语:Coupling,Dependency)或称耦合力耦合度,是一种软件度量,是指一程序中,模块及模块之间信息或参数依赖的程度

内聚性是一个和耦合性相对的概念,一般而言低耦合性代表高内聚性,反之亦然。耦合性和内聚性都是由提出结构化设计概念的赖瑞·康斯坦丁所提出[1]。低耦合性是结构良好程序的特性,低耦合性程序的可读性及可维护性会比较好。

“低耦合、高内聚”这一软件度量标准的出现,本质是因为人脑对于事物的认知存在局限性,所以软件设计者们希望通过在软件工程上去配合人脑的特性,从而优化软件工程在开发和维护层面的效率。其中人脑的局限性有:

  • 记忆是短暂而非永久的

  • 在同一时间, 人能考虑到的因素是有限的

为了配合上述人脑的局限性,于是我们就希望软件工程中的每一个模块能够是单一职责的、能够被人脑快速演绎的、副作用较小的。因为这样的模块,才可以被人非常快速的理解和组织。而不是看着眼前的模块,还要注意另外一个模块的内容,从而增加大脑的负担,降低效率。

基于上述前提,也产生了诸如“单一功能原则”、“关注点分离”等概念。但还没完,在具体开发场景中却产生了进一步的问题:

  • 这个解耦合理吗?

  • 这个聚合正确吗?

  • 根据什么内容或知识去划分模块?

所以我们此刻要回答的问题是:我们应当以什么准则去解耦、分离或整合我们的视图代码?

根据人脑对于图像的理解方式去解耦

前文提到,“低耦合、高内聚”是根据人脑的缺陷而设定的软件工程标准。所以我们也理应根据人脑对于图像的认知去划分我们的视图代码。我们以下图为例:

From: The complexity that lives in the GUI

根据认知心理学中格式塔理论的描述,及我们日常知识经验的影响下,我们会先从左至右,并因为“完整和闭合倾向”,将整个视图划分为左右两个部分。再因为右侧上方的每一栏布局具备“接近性”和“连续性”,我们又会将它们整合在一起视为一个整体,而下方则成为整个右侧内容的附属。

而当我们通过点击“Logout”、“Edit”、“New”三个按钮,触发相关逻辑并引起相关视图变化后。我们会将作为客体的按钮和其后续变化视为一个整体去记忆,而不是将两者分开

所以我们如果仅遵循 HTML 与 JS 分离的传统去分离“视图描述”和“视图逻辑”是违反直觉的。而先前 HTML 与 JS 分离的原因也是因为网页发展的过程中,JS 的发明与出现相对滞后,才导致两者只能各自单干。

综上所述,“视图描述”和“视图逻辑”解耦只是经验主义的产物,仅根据技术实现去分离关注点,而不是根据我们对于视图的认知去分离关注点。

即便选择“视图描述”和“视图逻辑”聚合,为什么就一定要选择 JSX?

为什么不选择模板引擎?

function Component(todo) {
    return ejs`
        <div class="view">
          <input class="toggle" type="checkbox" <% if (todo.isDone) { %> checked <% } %>>
          <label><%= todo.name %></label>
          <button class="destroy"></button>
        </div>`
}

不论什么模板引擎,它们的实质都是选择强化 HTML,使之拥有更强的能力,甚至接近于编程语言的逻辑描述能力。但为了达成这个目的就必须创造许多新的概念,EJS 为此便定义了 9 种特殊的标签,及各类编程语言才存在的特性。更不必说 Vue.js、Angular.js 等甚至为此创建了各自特殊的指令、插槽、数据绑定等概念。同时每个模版引擎创造的新概念又是不统一的,仅为了配合其运行时所需,而各自创建各自的新语法。所以这些新概念更准确的说法其实是它们运行时的特性,而非视图描述层面的通用语法。

这毫无疑问会增加开发者额外的学习成本,并且脱离了所归属的运行时,这些模板引擎也失去了意义。而 JSX 则是选择让 JS 能够像 HTML 一样更清晰的描述视图,一是没有创造新的概念、二是更加通用。即便没有 React,你也能非常快速的使用 JSX 编写你想要的视图。

为什么一定要使用 XML 风格,而不是使用 JS 能直接识别的对象风格?

function Component(todo) {
    return jsx({
      div: {
        class: 'view',
      children: {
          input: {class: 'toggle', type: 'checkbox', checked: todo.isDone},
        label: {children: todo.name},
        button: {class: 'destroy'}
      }
    } 
  })
}

使用 JSX 并不意味使用纯 JS 的风格不能被接受,只是使用对象风格后,其花括号不能很好的提示开发者当前节点的开始和结束位置,同时也很容易混淆属性和子节点的概念。这也是为什么网页内容描述会使用 HTML 这种 XML 风格的标记语言的原因。

总结

JSX 是一个反对将视图描述和视图逻辑解耦、反对强化 HTML 的结果,是遵循经典软件设计原则后的必然产物。

选择 JSX 就是选择顺从我们的大脑。

后话

JSX 的主流应用方式会从运行时转向编译时吗?为什么?

Q&A

选择 JSX 到底是在选择什么?是选择将视图描述和视图逻辑聚合在一起么?如果是这样,正常的 html 文件里就有 script 标签,直接在 script 标签里写逻辑不就好了?这个观点显然有问题,我认为不管选择 JSX 还是选择模板引擎,最大的目的都是方便在 dom 中绑定动态数据,通过数据直接驱动视图变化。既然使用 JSX 不是单纯为了聚合,你后面的一通关于『高内聚低耦合』以及『格式塔』的论述也就没有意义。

这个问题有考虑到也有解,但问题后半段的因果关系有问题。我并没有说 JSX 与 “视图描述和视图逻辑聚合”是强绑定的,JSX 并不是“视图描述和视图逻辑聚合”的因,只是它的果,原文是“我们还需要讨论另外一个在选择 JSX 前默认会选择的选项”,如果你把 JSX 当成 Template 使用自然也没人阻拦,它并不影响“视图描述和视图逻辑聚合”结论的得出。其中第一个问题我将其理解为:

即便聚合,为什么要选择“HTML in JS”,而不是选择“JS in HTML”?

正如楚门所言"方便在 dom 中绑定动态数据"是使用 JSX 与模版最重要的目的之一,于是其中便产生了先后关系,我们需要先产生动态数据才能更好的去生成对应的 DOM,而产生数据的方式则是通过编程语言的能力获取的,所以我们需要 JS 先行。最重要的是赋予 HTML 动态内容的行为主体是 JS,所以我们希望在代码描述上符合我们的心智模型,让“HTML in JS”,而不是“JS in HTML”。

关于高内聚低耦合,也并不是像你说的一样是『根据人脑的缺陷而设定的软件工程标准』,能得出这个结论说明你只考虑了软件的『可读性』。但高内聚低耦合不仅能提高可读性,也能提高可维护性和可扩展性。我们根据功能将关系紧密的逻辑内聚在一起,当需要变更和扩展功能的时候只需要关注这一个模块,而因为低耦合,变更和扩展的时候,其他模块受到的影响也非常小。文中提到的『我们以什么准则去聚合和分离代码』,如果仅仅从可读性出发,自然没有答案,但从可维护性和可扩展性出发,就能以抽象层级、易变程度等标准为依据进行划分,这里就不展开了。

根据人脑的缺陷而设定的软件工程标准,并不是只考虑软件的“可读性”,原文是“因为这样的模块,才可以被人非常快速的理解和组织”。而所谓的可维护性和可拓展性也是从人的角度出发,如何让人能更好的维护代码,如何让人能更好的去理解客体之间的关系。假设我们的人脑记忆能力相当强大,演绎能力极强,我们大可一个功能一个函数,不管函数的副作用,面向过程编程完全没有问题,完全没必要去追求所谓的抽象和模块化。典型如“GO TO”的被批判,其最重要的依据之一也是从人脑出发,因为我们人脑更适合组织静态关系,而不是组织随着时间而变化的动态关系。

My second remark is that our intellectual powers are rather geared to master static relations and that our powers to visualize processes evolving in time are relatively poorly developed. For that reason we should do (as wise programmers aware of our limitations) our utmost best to shorten the conceptual gap between the static program and the dynamic process, to make the correspondence between the program (spread out in text space) and the process (spread out in time) as trivial as possible.

模板引擎引入的所谓『新概念』,其实都是命令式语言中的常用逻辑,分支、循环等等,能理解 if 就不能理解 v-if 么?况且有些模板引擎也支持直接在双括号里写 if、forEach 等逻辑。至于 slot 之类的概念,相比于 renderProps,语义化程度反而更高,自然也更有利于机器分析(更好做编译优化),所以『概念多』也不一定是坏事。

HTML 它本身是个标记语言,而 JS 本身已经是图灵完备的编程语言,为何不直接使用已经具备逻重组逻辑表达能力的 JS 呢。v-if 并不完全等于 if,与之呼应的 v-else 和节点之间的组织关系也需要开发者重新吸收。因为强化 HTML 增加的不只是几个特殊的语法糖,同时增加的成本是,你要重新思考其传达的组合思想和模块化思想,而 JSX 直接沿用了 HTML 已经存在心智,而不是强化 HTML 使之成为一门新的“编程语言”。

概念多确实不一定是坏事。问题是,这些概念是否是由统一的、全面的思想指导产生的,如果只是拆东墙补西墙,一个补丁一个补丁打上去的新概念,很容易变成不稳定容易变化的。比如 vue 里的 slot 在 vue2.0 和 vue3.0 里的呈现就大相径庭,对于用户来说就又需要新的时间去适应,而 JSX 不管你使不使用 React,它的表现形式和思想都是通用且可靠的。

参考