本文是《React 思维模式》的预览连载之一,这是我最近在写的一本小说体裁的 React 书。想知道我创作背后的心酸故事吗?点这里从头看。
拍照墙有什么好玩的?用得着这么奋不顾身么?我从来都讨厌拍照,便由艾伦自拍去了。
又忙了大概半小时,小路全都加宽,这下安全了,我想。咦?艾伦怎么还没回来?自拍能玩到这么投入?我决定去看看他在拍什么写真。来到拍照墙跟前,我才发现艾伦早已不见踪影,只剩他的三脚架孤零零地立在那里,上面的照相机还在不停地工作,相片散落了一地。
又跟我玩消失?我大喊艾伦,周围却是一片寂静,除了咔嚓咔嚓的拍照声。我开始在满地的相片中翻找线索,在艾伦的各种卖萌耍酷表情中,一张相片吸引了我的注意。相片中,一只灰白色的动物回过头来面对着镜头,口中含着什么东西,挂在嘴边的仿佛是……两条人腿!那脚上穿着的分明是艾伦的阿迪达斯运动鞋!我不禁全身寒毛倒竖,抬头望见那堵灰白色的拍照墙,那是和相片中的动物一样的灰白色。我跌跌撞撞倒退几步,哆嗦着又捡起了几张相片,终于看到那惊悚的真相。原来,拍照墙是那怪物用来吸引猎物的伪装,那镂空洞口正是它的血盆大口,将可怜的艾伦囫囵吞下。
我脑中一片空白,完了完了!怎么救艾伦?还有救吗?正在心神不定中,眼前视野中忽然出现了一行发着蓝光的字:
React 思维模型:JSX 是伪装成 HTML 的 JavaScript 代码。
哦,脑机又要给我灌知识了,但是我得去救人啊!不过,这个思维模型也许跟救人有关?我坐下来开始冥想。
JSX
吞掉艾伦的那个墙怪就住在下面这个组件里:
function App() {
return <div> </div>
}
其实,这个组件是一个 JavaScript 函数,并返回了一个值(return
后面的部分)。那么,这个函数到底返回了什么?
……
一个 HTML 标签?
一个字符串(string)?
一个特殊的 html-tag 值?跟 JavaScript 里的数字(number) 或者布尔值(boolean)差不多?
……
对不起,猜错了!
原来,那拍照墙并不是真正的拍照墙,混写在 React 代码里的标签也不是真正的 HTML,而是一种特殊的标签,大名叫做 JSX。这里的“JS”指的是 JavaScript,“X”有扩展(extension)的意思,也表示它跟另外一种标记语言 XML 其实更接近,不过如果你没听说过 XML 也不妨碍理解和使用 JSX。
JSX 只是伪装成 HTML 标签,其实质是 JavaScript 代码。在发送到浏览器执行之前, React 开发工具将 JSX 标签自动转换为相应的 JavaScript 代码。比如:
<div> </div>
跟下面的代码是等效的:
_jsx("div", { children: " " })
这里的 _jsx 是开发工具自动导入的一个函数:
import {jsx as _jsx} from 'react/jsx-runtime'
注:在 React 17 发布之前,该 JSX 标签与这个函数调用等效:React.createElement('div', {}, " &q
uot;)。不过,因为 React 核心团队计划将 jsx-runtime 移植到 React 17 之前的版本,所以在此我们仅讨论 _jsx 这种形式。
所以,本节开头的组件可以重写为:
import {jsx as _jsx} from 'react/jsx-runtime'
function App() {
return _jsx("div", { children: " " })
}
这样的代码看起来更合理,对吧?在 App 函数里,我们调用了_jsx 函数并且返回其结果,这个函数接收了两个参数:"div"
和 { children: " &qu
ot; }。
JSX 属性
既然 JSX 标签实际上是一个函数调用,猜猜看下面这个标签转换成 JavaScript 是什么样子的?
<div className="mr-wall" />
答案:
_jsx("div", { className: "mr-wall" })
这个函数调用仍然有两个参数,第一个参数是元素的类型,第二个参数则是包含了所有属性的一个对象。
嵌套标签
那么,这个标签呢?
<div>
<button />
</div>
答案:
_jsx('div', { children: _jsx('button') })
这个跟前面的类似,children 是一个特殊的属性,包含了嵌套在 div 里的标签,或者说是 div 的“孩子”。而 button 同时也是一个 jsx 标签,所以 children 的值是再一次调用 _jsx 的结果,而不是一个简单的字符串。
再来看一个:
<div> {alan} </div>
答案:
_jsx("div", { children: [" ", alan, " "] })
当有花括号时,标签内容将被拆分为多个“孩子”,并包括在一个数组内。
最后一个例子:
<div className="container">
<div className="mr-wall"> {alan} </div>
<button />
</div>
转换成 JavaScript:
_jsx('div', { className: "container", children: [
_jsx("div", { className: "mr-wall", children: [" ", alan, " "] }),
_jsx('button')
]})
有了 JSX,我们可以轻松把 HTML 代码移植到 JavaScript 里,并且保持其简练易读的特性。
_jsx 的返回值
那么,_jsx 到底返回了一个什么结果呢?是不是 DOM 元素?我们不妨把函数的返回值打印出来瞧瞧:
function App() {
const result = _jsx("input")
console.log(result) // 打印到控制台
return result
}
控制台的结果如下:
Object {type: "input", key: null, ref: null, props: Object, _owner: null}
看到了吧?_jsx 函数所创建并返回了一个简单的 JavaScript 对象,跟 DOM 元素没啥关系。这个对象的正式名称是 React 元素(React element),其作用只是描述我们所期望在浏览器中看到的结果。
原来就是一个表达式!
你知道吗?我们可以把 JSX 标签赋值给一个变量:
let content = <div>咔嚓</div>
或者作为参数在调用函数时传过去:
showAlert(<input />)
或者打印在控制台上:
console.log(<div> </div>)
为什么可以做到这些呢?
这并不是魔法。这仅仅是因为 JSX 标签是函数调用(_jsx(...)
)。既然是函数调用,JSX 标签就是一个 JavaScript 表达式,可以写在任何能容纳表达式的地方。
另外,你知道为什么下面这段代码不能工作吗?
const div = <div />
div.appendChild("input")
_jsx
所创建的只是一个简单的对象,并不是 DOM 节点。所以它没有 appendChild
方法供我们调用!
你看,这就是了解 JSX 的实质好处。在 React 星上,这是生死攸关的大事。而作为造物主,只有对底层的知识有足够的了解,你才能更加自如地呼风唤雨:真正理解自己写的代码,自由表达你的想法,充分利用各种语言和框架的不同特性。
理解 JSX 和 HTML 的区别
不管 JSX 标签怎么伪装,它毕竟不是 HTML。实际上,JSX 跟 HTML 之间有很多不同的地方,比如我们在前面看到的使用 CSS 样式上的区别:
// JSX
<input style={{ minWidth: 200 }} />
// HTML
<input style="min-width: 200px" />
为什么这里有两层大括号?为什么不是单层大括号?为什么不是引号?
这第一层括号实际上就是墙上的那个“洞”(怪物的嘴),而第二层括号是 JavaScript 对象的界定符。所以,这里的 style 属性的值是一个对象,这也解释了为什么括号内是{ minWidth: 200 }
,而不是{ minWidth: 200px }
,或者{ min-width: 200px }
,因为后面两个都不是对象的正确写法。基于同样的原因,当 CSS 属性值不是数字的时候,我们需要使用引号,比如,设置背景颜色是用 <div style={{ background: "red" }}>
,而不是 <div style={{ background: red }}>
。
当然,上述是标准 React 所支持的方法,有一些第三方库(如 styled-components、emotion)可以让我们在 JavaScript 代码里加入真正的 CSS 代码,其格式完全原汁原味,我们甚至可以直接从网上拷贝一段 CSS 代码放到程序里,不过这些库的实现仍然是基于上述的标准方法。
再举一个例子,HTML 的按钮是这样写的:
<button onclick="alert('OK')">OK</button>
而 JSX 版本则是这样的:
<button onClick={() => alert("OK")}>OK</button>
至少有两个地方不一样:第一,HTML 版本的属性名是全小写的,而在 JSX 里的属性是驼峰式命名(camel case);第二,两者的onclick
属性值很不一样。
为什么会有这些区别?原因只有一个,JSX 标签根本就是 JavaScript 代码。如果把按钮的 JSX 改写成 JavaScript,就真相大白了:
_jsx('button', { onClick: () => alert('OK')}, "OK")
这里,onClick
的取值是一个匿名的箭头函数(arrow function),所以才会有那些括号和箭头。
为什么不把两种标记语言做得一模一样呢?记住,其根源还是因为 JSX 就是 JavaScript 代码,就要遵照 JavaScript 代码规则。当然,我们还可以把 JSX 看成增强型的 HTML,因为它可以支持自定义标签等高级功能,这个是后话。
最后,我把 JSX 和 HTML 两者之间一些常见的区别列出来供你参考,见表 TODO 。
表 TODO:JSX 和 HTML 的区别
小结
怪物伪装的拍照墙,JavaScript 伪装的 HTML,这就是 JSX 的真实面目。它是:
- 一个函数调用;
- 一个表达式;
- 其值是一个简单的对象。
艾伦,要挺住啊,我这就来救你!
看完以后觉得有用吗?后面还希望我怎么写?请拍砖!