盤點(diǎn) Solid.js 源碼中的那些迷惑行為
前言
我研究 Solid.js 源碼已經(jīng)有一段時(shí)間了,在鉆研的過(guò)程中我發(fā)現(xiàn)了其中的一些迷惑行為,在搞懂之后終于恍然大悟,忍不住想要分享給大家。不過(guò)這么說(shuō)其實(shí)也不太準(zhǔn)確,因?yàn)樵趪?yán)格意義上來(lái)講 Solid.js 其實(shí)是被劃分為了兩個(gè)部分的。我只認(rèn)真鉆研了其中一個(gè)部分,所以也不能說(shuō)鉆研 Solid.js 源碼,因?yàn)榱硗庖粋€(gè)部分壓根就不叫 Solid。
兩部分
有些同學(xué)看到這可能就會(huì)感到疑惑了,哪兩個(gè)部分?Solid、.js?其實(shí)是這樣:大家應(yīng)該都聽(tīng)說(shuō)過(guò) Solid.js 是一個(gè)重編譯、輕運(yùn)行的框架吧,所以它可以被分為編譯器和運(yùn)行時(shí)兩個(gè)部分。
那有人可能會(huì)問(wèn):你要是這么說(shuō)的話那豈不是 Vue 也可以被分為兩部分,畢竟 Vue 也有編譯器和運(yùn)行時(shí),為什么從來(lái)沒(méi)有人說(shuō)過(guò) Vue 是兩部分組成的呢?是這樣,Vue 的編譯器和運(yùn)行時(shí)全都放在了同一倉(cāng)庫(kù)內(nèi)的 Monorepo 中:
你可以說(shuō) Vue2 和 Vue3 是兩個(gè)部分,因?yàn)樗鼈z被放在了兩個(gè)不同的倉(cāng)庫(kù)中:
雖然它倆已經(jīng)是兩個(gè)不同的倉(cāng)庫(kù)了,但好歹也都是 vuejs 名下的吧:
而 Solid.js 的兩部分不僅不在同一個(gè)倉(cāng)庫(kù)內(nèi),甚至連組織名都不一樣:
一個(gè)是 solidjs/solid:
而另一個(gè)則是 ryansolid/dom-expressions:
ryan 是 Solid.js 作者的名字,所以 ryan + solid = ryansolid(有點(diǎn)迷,為啥不放在 solidjs 旗下非要單獨(dú)開一個(gè) ryansolid)
這個(gè) dom-expressions 就是 Solid.js 的編譯器,那為啥不像 Vue 編譯器似的都放在同一個(gè)倉(cāng)庫(kù)內(nèi)呢?因?yàn)?nbsp;Vue 的編譯器就是專門為 Vue 設(shè)計(jì)的,你啥時(shí)候看非 Vue項(xiàng)目中使用 xxx.vue 這樣的寫法過(guò)?
.vue 這種單文件組件就只有 Vue 使用,雖說(shuō)其他框架也有單文件組件的概念并且有著類似的寫法(如:xxx.svelte)但人家 Svelte也不會(huì)去用 Vue 的編譯器去編譯人家的 Svelte 組件。不過(guò) Solid 不一樣,Solid沒(méi)自創(chuàng)一個(gè) xxx.solid,而是明智的選擇了 xxx.jsx。
SFC VS JSX
單文件組件和 jsx 各有利弊,不能說(shuō)哪一方就一定比另一方更好。但對(duì)于一個(gè)聲明式框架作者而言,選擇單文件組件的好處是可以自定義各種語(yǔ)法,并且還可以犧牲一定的靈活性來(lái)?yè)Q取更優(yōu)的編譯策略。缺點(diǎn)就是成本太高了,單單語(yǔ)法高亮和 TS 支持度這一方面就得寫一個(gè)非常復(fù)雜的插件才能填平。
好在 Vue 的單文件組件插件 Volar 已經(jīng)可以支持自定義自己的單文件組件插件了,這有效的降低了框架作者的開發(fā)成本。但 Solid 剛開始的時(shí)候還沒(méi)有 Volar 呢(可以去看看 Volar 的源碼有多復(fù)雜 這還僅僅只是一個(gè)插件就需要花費(fèi)那么多時(shí)間和精力),甚至直到現(xiàn)在 Volar 也沒(méi)個(gè)文檔,就只有 Vue 那幫人在用 Volar(畢竟是他們自己研究的):
并且人家選擇 jsx 也有可能并非是為了降低開發(fā)成本,而是單純的鐘意于 jsx 語(yǔ)法而已。那么為什么選擇 jsx 會(huì)降低開發(fā)成本呢?首先就是不用自己寫 parser、generator 等一堆編譯相關(guān)的東西了,一個(gè) babel 插件就能識(shí)別 jsx 語(yǔ)法。語(yǔ)法高亮、TS 支持度這方面更是不用操心,甚至用戶都不需要為編輯器安裝任何插件(何時(shí)聽(tīng)過(guò) jsx 插件)。
并且由于 React 是全球占有率最高的框架,jsx 已被廣泛接受(甚至連 Vue 都支持 jsx)但如果選擇單文件組件的話又會(huì)產(chǎn)生有人喜歡這種寫法有人喜歡那種寫法的問(wèn)題,比方說(shuō)同樣使用 sfc 的 Vue 和 Svelte,if-else 寫法分別是這樣:
<template>
<h1 v-if="xxx" />
<div v-else />
</template>
{#if xxx}
<h1 />
{:else}
<div />
{/if}
有人喜歡上面那種寫法就有人喜歡下面那種寫法,眾口難調(diào),無(wú)論選擇哪種寫法可能都會(huì)導(dǎo)致另一部分的用戶失望。而 jsx 就靈活的多了,if-else 想寫成什么樣都可以根據(jù)自己的喜好來(lái):
if (xxx) {
return <h1 />
} else {
return <div />
}
// 或者
return xxx ? <h1 /> : <div />
// 亦或
let Title = 'h1'
if (xxx) Title = 'div'
return <Title />
jsx 最大程度的融合了 js,正是因?yàn)樗鼘?duì) js 良好的兼容性才導(dǎo)致它的適用范圍更廣,而不是像 Vue、Svelte 那樣只適用于自己的框架。
畢竟每種模板語(yǔ)言的 if-else、循環(huán)等功能寫法都不太一樣,當(dāng)然 jsx 里的 if-else 也可以有各種千奇百怪的寫法,但畢竟還是 js 寫法,而不是自創(chuàng)的 ng-if、v-else、{:else if} {% for i in xxx %}等各種不互通的寫法。
正是由于 jsx 的這個(gè)優(yōu)勢(shì)導(dǎo)致了很多非 React 框架(如:Preact、Stancil、Solid 等)用 jsx 也照樣用的飛起,那么既然 jsx 可以不跟 React 綁定,那 Ryan 自創(chuàng)的 jsx編譯策略也同樣可以不跟 Solid 綁定啊對(duì)不對(duì)?
這是一款可以和 Solid.js 搭配使用的 babel 插件,也同樣是一款可以和 MobX、和 Knockout、和 S.js、甚至和 Rx.js 搭配使用的插件,只要你有一款響應(yīng)式系統(tǒng),那么 dom-expressions 就可以為你提供 jsx 服務(wù)。
Solid.js
所以這才是 Ryan 沒(méi)把 dom-expressions 放在 solidjs/solid 里的重要原因之一,但 Solid.js 又是一個(gè)注重編譯的框架,沒(méi)了 dom-expressions 還不行,所以只能說(shuō) Solid.js 是由兩部分組成的。
DOM Expressions
DOM Expressions 翻譯過(guò)來(lái)就是 DOM 表達(dá)式的意思,有人可能會(huì)問(wèn)那你標(biāo)題為啥不寫成《盤點(diǎn) DOM Expressions 源碼中的那些迷惑行為》?拜托!誰(shuí)知道 DOM Expressions 到底是個(gè)什么鬼!
如果不是我苦口婆心的說(shuō)了這么多,有幾個(gè)能知道這玩意就是 Solid.js 的編譯器,甭說(shuō)國(guó)內(nèi)了,就連國(guó)外都沒(méi)幾個(gè)知道 DOM Expressions的。你要說(shuō) Solid.js 那別人可能會(huì)豎起大拇指說(shuō)聲 Excellent,但你要說(shuō) DOM Expressions 那別人說(shuō)的很可能就是 What the fuck is that? 了。不信你看它倆的??對(duì)比:
再來(lái)看看 Ryan 在油管上親自直播 DOM Expressions時(shí)的慘淡數(shù)據(jù):
這都沒(méi)我隨便寫篇文章的點(diǎn)贊量高,信不信如果我把標(biāo)題中的 Solid.js 換成了 DOM Expression 的話點(diǎn)贊量都不會(huì)有 Ryan 直播的數(shù)據(jù)好?好歹人家還是 Solid的作者,都只能獲得如此慘淡的數(shù)據(jù),那更別提我了。
言歸正傳,為了防止大家不知道 Solid.js 編譯后的產(chǎn)物與 React 編譯后的產(chǎn)物有何不同,我們先來(lái)寫一段簡(jiǎn)單的 jsx:
import c from 'c'
import xxx from 'xxx'
export function Component () {
return (
<div a="1" b={2} c={c} onClick={() => {}}>
{ 1 + 2 }
{ xxx }
</div>
)
}
React 編譯產(chǎn)物:
import c from 'c';
import xxx from 'xxx';
import { jsxs as _jsxs } from "react/jsx-runtime";
export function Component() {
return /*#__PURE__*/_jsxs("div", {
a: "1",
b: 2,
c: c,
onClick: () => {},
children: [1 + 2, xxx]
});
}
Solid 編譯產(chǎn)物:
import { template as _$template } from "solid-js/web";
import { delegateEvents as _$delegateEvents } from "solid-js/web";
import { insert as _$insert } from "solid-js/web";
import { setAttribute as _$setAttribute } from "solid-js/web";
const _tmpl$ = /*#__PURE__*/_$template(`<div a="1" b="2">3`);
import c from 'c';
import xxx from 'xxx';
export function Component() {
return (() => {
const _el$ = _tmpl$(),
_el$2 = _el$.firstChild;
_el$.$$click = () => {};
_$setAttribute(_el$, "c", c);
_$insert(_el$, xxx, null);
return _el$;
})();
}
_$delegateEvents(["click"]);
Solid 編譯后的產(chǎn)物乍一看有點(diǎn)不太易讀,我來(lái)給大家寫一段偽代碼,用來(lái)幫助大家快速理解 Solid 到底把那段 jsx 編譯成了啥:
import c from 'c';
import xxx from 'xxx';
const template = doucment.createElement('template')
template.innerHTML = '<div a="1" b="2">3</div>'
const el = template.content.firstChild.cloneNode(true) // 大家可以簡(jiǎn)單的理解為 el 就是 <div a="1" b="2">3</div>
export function Component() {
return (() => {
el.onclick = () => {};
el.setAttribute("c", c);
el.insertBefore(xxx);
return el;
})();
}
這樣看上去就清晰多了吧?直接編譯成了真實(shí)的 DOM 操作,這也是它性能為何能夠如此強(qiáng)悍的原因之一,沒(méi)有中間商(虛擬DOM)賺差價(jià)。但大家有沒(méi)有感覺(jué)有個(gè)地方看起來(lái)好像有點(diǎn)多此一舉,就是那個(gè)自執(zhí)行函數(shù):
export function Component() {
return (() => {
el.onclick = () => {};
el.setAttribute("c", c);
el.insertBefore(xxx);
return el;
})();
}
為何不直接編譯成這樣:
export function Component() {
el.onclick = () => {};
el.setAttribute("c", c);
el.insertBefore(xxx);
return el;
}
效果其實(shí)都是一樣的,不信你試著運(yùn)行下面這段代碼:
let num = 1
console.log(num) // 1
num = (() => {
return 1
})()
console.log(num) // 還是 1 但感覺(jué)多了一個(gè)脫褲子放屁的步驟
看了源碼才知道,原來(lái)看似多此一舉的舉動(dòng)實(shí)則是有苦衷的。因?yàn)槲覀冞@是典型的站在上帝視角來(lái)審視編譯后的代碼,源碼的做法是只對(duì) jsx 進(jìn)行遍歷,在剛剛那種情況下所編譯出來(lái)的代碼確實(shí)不是最優(yōu)解,但它能保證在各種的場(chǎng)景下都能正常運(yùn)行。
我們來(lái)寫一段比較罕見(jiàn)的代碼大家就能明白過(guò)來(lái)怎么回事了:
if (<div a={value} onClick={() => {}} />) {
// do something…
}
當(dāng)然這么寫沒(méi)有任何的意義,這是為了幫助大家理解為何 Solid 要把它的 jsx 編譯成一段自執(zhí)行函數(shù)才會(huì)寫成這樣的。我們來(lái)寫一段偽代碼,實(shí)際上 Solid 編譯出來(lái)的并不是這樣的代碼,但相信大家能夠明白其中的含義:
<div a={value} notallow={() => {}} />
// 將會(huì)被編譯成
const el = document.createElement('div')
el.setAttribute('a', value)
el.onclick = () => {}
發(fā)現(xiàn)問(wèn)題所在了么?原本 jsx 只有一行代碼,但編譯過(guò)后卻變成三行了。所以如果不加一個(gè)自執(zhí)行函數(shù)的話將會(huì)變成:
if (const el = document.createElement('div'); el.setAttribute('a', value); el.onclick = () => {}) {
// do something…
}
這很明顯是錯(cuò)誤的語(yǔ)法,if 括號(hào)里根本不能寫成這樣,會(huì)報(bào)錯(cuò)的!但如果把 if 括號(hào)里的代碼放在自執(zhí)行函數(shù)中那就沒(méi)問(wèn)題了:
if ((() => {
const el = document.createElement('div')
el.setAttribute('a', value)
el.onclick = () => {}
return el
})()) {
// do something…
}
我知道肯定有人會(huì)說(shuō)把那三行代碼提出去不就得了么:
const el = document.createElement('div')
el.setAttribute('a', value)
el.onclick = () => {}
if (el) {
// do something…
}
還記得我之前說(shuō)過(guò)的那句:我們是站在上帝視角來(lái)審判 Solid 編譯后代碼的么?理論上來(lái)說(shuō)這么做確實(shí)可以,但編譯成本無(wú)疑會(huì)高上許多,因?yàn)檫€要判斷 jsx 到底寫在了哪里,根據(jù)上下文的不同來(lái)生成不同的代碼,但這樣肯定沒(méi)有只編譯 jsx 而不管 jsx 到底是被寫在了哪里來(lái)的方便。而且我們上述的那種方式也不是百分百?zèng)]問(wèn)題的,照樣還是會(huì)有一些意想不到的場(chǎng)景:
for (let i = 0, j; j = <div a={i} />, i < 3; i++) {
console.log(j)
}
但假如按照我們那種策略來(lái)編譯代碼的話:
const el = document.createElement('div')
el.setAttribute('a', i)
for (let i = 0, j; j = el, i < 3; i++) {
console.log(j)
}
此時(shí)就會(huì)出現(xiàn)問(wèn)題,因?yàn)?nbsp;el 用到了變量 i,而 el 又被提到外面去了所以訪問(wèn)不到 i變量,所以 el 這幾行代碼必須要在 jsx 的原位置上才行,只有自執(zhí)行函數(shù)能夠做到這一點(diǎn)。由于 js 是一門極其靈活的語(yǔ)言,各種騷操作數(shù)不勝數(shù),所以把編譯后的代碼全都加上一段自執(zhí)行函數(shù)才是性價(jià)比最高并且最省事的選擇之一。
迷之嘆號(hào)??
有次在用 playground.solidjs.com 編譯 jsx 時(shí)驚奇的發(fā)現(xiàn):
不知大家看到這段 <h1>Hello, <!>!</h1> 時(shí)是什么感受,反正我的第一感覺(jué)就是出 bug 了,把我的嘆號(hào) ! 給編譯成 <!> 了。
但令人摸不著頭腦的是,這段代碼完全可以正常運(yùn)行,沒(méi)有出現(xiàn)任何的 bug。隨著測(cè)試的深入,發(fā)現(xiàn)其實(shí)并不是把我的嘆號(hào) ! 給編譯成 <!> 了,只是恰巧在那個(gè)位置上我寫了個(gè)嘆號(hào),就算不寫嘆號(hào)也照樣會(huì)有這個(gè) <!>的:
發(fā)現(xiàn)沒(méi)?<!> 出現(xiàn)的位置恰巧就是 {xxx} 的位置,我們?cè)谡{(diào)試的時(shí)候發(fā)現(xiàn)最終生成的代碼其實(shí)是這樣:
<h1>1<!---->2</h1>
也就是說(shuō)當(dāng)我們 .innerHTML = '<!>' 的時(shí)候其實(shí)就相當(dāng)于 .innerHTML = '' 了,很多人看到這個(gè)空注釋節(jié)點(diǎn)以后肯定會(huì)聯(lián)想到 Vue,當(dāng)我們?cè)?nbsp;Vue 中使用 v-if="false" 時(shí),按理說(shuō)這個(gè)節(jié)點(diǎn)就已經(jīng)不復(fù)存在了。但每當(dāng)我們打開控制臺(tái)時(shí)就會(huì)看到原本 v-if 的那個(gè)位置變成了這樣:
尤雨溪為何要留下一個(gè)看似沒(méi)有任何意義的空注釋節(jié)點(diǎn)呢?廣大強(qiáng)迫癥小伙伴們?nèi)滩涣肆?,趕忙去 GitHub 里開個(gè) issue 問(wèn)尤雨溪:
尤雨溪給出的答案是這樣:
那 Solid 加一個(gè)這玩意也是和 Vue 一樣的原由么?隨著對(duì)源碼的深入,我發(fā)現(xiàn)它跟 Vue 的 原由并不一樣,我們?cè)賮?lái)用一段偽代碼來(lái)幫助大家理解 Solid 為什么需要一段空注釋節(jié)點(diǎn):
<h1>1{xxx}2</h1>
// 將會(huì)被編譯成:
const el = template('<h1>12</h1>')
const el1 = el.firstChild // 1
const el2 = el1.nextSibling //
const el3 = el2.nextSibling // 2
// 在空節(jié)點(diǎn)之前插入 xxx 而空節(jié)點(diǎn)恰好就在 1 2 之間 所以就相當(dāng)于在 1 2 之間插入了 xxx
el.insertBefore(xxx, el2)
看懂了么,Solid 需要在 1 和 2 之間插入 xxx,如果不加這個(gè)空節(jié)點(diǎn)的話那就找不到該往哪插了:
<h1>1{xxx}2</h1>
// 假如編譯成沒(méi)有空節(jié)點(diǎn)的樣子:
const el = template('<h1>12</h1>')
const el1 = el1.firstChild // 12
const el2 = el2.nextSibling // 沒(méi)有兄弟節(jié)點(diǎn)了 只有一個(gè)子節(jié)點(diǎn):12
el.insertBefore(xxx, 特么的往哪插?)
所以當(dāng)大家在 playground.solidjs.com 中發(fā)現(xiàn)有 <!> 這種奇怪符號(hào)時(shí),請(qǐng)不要覺(jué)得這是個(gè) bug,這是為了留個(gè)占位符,方便 Solid 找到插入點(diǎn)。只不過(guò)大多數(shù)人都想不到,把這個(gè) <!> 賦值給 innerHTML 后會(huì)在頁(yè)面上生成一個(gè) <!---->。
迷之 ref
無(wú)論是 Vue 還是 React 都是用 ref 來(lái)獲取 DOM 的,Solid 的整體 API 設(shè)計(jì)的與 React 較為相似,ref 自然也不例外:
但它也有自己的小創(chuàng)新,就是 ref 既可以傳函數(shù)也可以傳普通變量。如果是函數(shù)的話就把 DOM 傳進(jìn)去,如果是普通變量的話就直接賦值:
// 偽代碼
<h1 ref={title} />
// 將會(huì)編譯成:
const el = document.createElement('h1')
typeof title === 'function'
? title(el)
: title = el
但在查看源碼時(shí)發(fā)現(xiàn)了一個(gè)未被覆蓋到的情況:
// 簡(jiǎn)化后的源碼
transformAttributes () {
if (key === "ref") {
let binding,
isFunction =
t.isIdentifier(value.expression) &&
(binding = path.scope.getBinding(value.expression.name)) &&
binding.kind === "const";
if (!isFunction && t.isLVal(value.expression)) {
...
} else if (isFunction || t.isFunction(value.expression)) {
...
} else if (t.isCallExpression(value.expression)) {
...
}
}
}
稍微給大家解釋一下,這個(gè) transformAttributes 是用來(lái)編譯 jsx 上的屬性的:
當(dāng) key 等于 ref 時(shí)需要進(jìn)行一些特殊處理,非常迷的一個(gè)命名就是這個(gè) isFunction,看名字大家肯定會(huì)認(rèn)為這個(gè)變量代表的是屬性值是否為函數(shù)。我來(lái)用人話給大家翻譯一下這個(gè)變量賦的值代表什么含義:t.isIdentifier(value.expression)的意思是這個(gè) value 是否為變量名:
比方說(shuō) ref={a} 中的 a 就是個(gè)變量名,但如果是 ref={1}、ref={() => {}}那就不是變量名,剩下那倆條件是判斷這個(gè)變量名是否是 const 聲明的。也就是說(shuō):
const isFunction = value 是個(gè)變量名 && 是用 const 聲明的
這特么就能代表 value 是個(gè) function 了?
在我眼里看來(lái)這個(gè)變量叫 isConst 還差不多,我們?cè)賮?lái)梳理一下這段邏輯:
// 簡(jiǎn)化后的源碼
transformAttributes () {
if (key === "ref") {
const isConst = value is 常量
if (!isConst && t.isLVal(value.expression)) {
...
} else if (isConst || t.isFunction(value.expression)) {
...
} else if (t.isCallExpression(value.expression)) {
...
}
}
}
接下來(lái)就是 if-else 條件判斷里的條件了,再來(lái)翻譯下,t.isLVal 代表的是:value 是否可以放在等號(hào)左側(cè),這是什么意思呢?一個(gè)例子就能讓大家明白:
// 此時(shí) key = 'ref'、value = () => {}
<h1 ref={() => {}} />
// 現(xiàn)在我們需要寫一個(gè)等號(hào) 看看 value 能不能放在等號(hào)的左側(cè):
() => {} = xxx // 很明顯這是錯(cuò)誤的語(yǔ)法 所以 t.isLVal(value.expression) 是 false
// 但假如寫成這樣:
<h1 ref={a.b.c} />
a.b.c = xxx // 這是正確的語(yǔ)法 所以 t.isLVal(value.expression) 現(xiàn)在為 true
明白了 t.isLVal 接下來(lái)就是 t.isFunction 了,這個(gè)從命名上就能看出來(lái)是判斷是否為函數(shù)的。然后就是 t.isCallExpression,這是用來(lái)判斷是否為函數(shù)調(diào)用的:
// 這就是 callExpression
xxx()
翻譯完了,接下來(lái)咱們就來(lái)分析一遍:
當(dāng) value 不是常量并且不能放在等號(hào)左側(cè)時(shí)(這種情況有處理)
當(dāng) value 是常量或者是一個(gè)函數(shù)字面量時(shí)(這種情況有處理)
當(dāng) value 是一個(gè)正在調(diào)用的函數(shù)時(shí)(這種情況有處理)
不知大家看完這仨判斷后有什么感悟,反正當(dāng)我捋完這段邏輯的時(shí)候感覺(jué)有點(diǎn)迷,因?yàn)楹孟駢焊鶅壕蜎](méi)覆蓋掉全部情況??!咱們先這么分一下:value 肯定是變量名、字面量以及常量中的其中一種對(duì)吧?是常量的情況下有覆蓋,不是常量時(shí)就有漏洞了,因?yàn)樗昧藗€(gè)并且符號(hào) &&,也就是說(shuō)當(dāng) value 不是常量時(shí)必須還要同時(shí)滿足不能放在等號(hào)左側(cè)這種情況才會(huì)進(jìn)入到這個(gè)判斷中去,那假如我們寫一個(gè)三元表達(dá)式或者二元表達(dá)式那豈不就哪個(gè)判斷也沒(méi)進(jìn)么?不信我們來(lái)試一下:
可以看到編譯后的 abc 三個(gè)變量直接變暗了,哪都沒(méi)有用到這仨變量,也就是說(shuō)相當(dāng)于吞掉了這段邏輯(畢竟哪個(gè)分支都沒(méi)進(jìn)就相當(dāng)于沒(méi)處理)不過(guò)有人可能會(huì)感到疑惑,三元表達(dá)式明明能放到等號(hào)左側(cè)?。?/p>
實(shí)際上并不是你想的那樣,等號(hào)和三元表達(dá)式放在一起時(shí)有優(yōu)先級(jí)關(guān)系,調(diào)整一下格式你就明白是怎樣運(yùn)行的了:
const _tmpl$ = /*#__PURE__*/_$template(`<h1>Hello`)
a ? b : c = 1
// 實(shí)際上相當(dāng)于
a
? b
: (c = 1)
// 相當(dāng)于
if (a) {
b
} else {
c = 1
}
如果我們用括號(hào)來(lái)把優(yōu)先級(jí)放在三元這邊就會(huì)直接報(bào)錯(cuò)了:
二元表達(dá)式也是同理:
我想在 ref 里寫成這樣沒(méi)毛病吧:
<h1 ref={a || b} />
雖然這種寫法比較少見(jiàn),但這也不是你漏掉判斷的理由呀!畢竟好多用 Solid.js 的人都是用過(guò) React 的,他們會(huì)把在 React 那養(yǎng)成的習(xí)慣不自覺(jué)的帶到 Solid.js 里來(lái),而且這不也是 Solid.js 把 API 設(shè)計(jì)的盡可能與 React 有一定相似性的重要原因之一嗎?
但人家在 React 沒(méi)問(wèn)題的寫法到了你這就出問(wèn)題了的話,是會(huì)非常影響你這框架的口碑的!而且在文檔里還沒(méi)有提到任何關(guān)于 ref 不能寫表達(dá)式的說(shuō)明:
后來(lái)我仔細(xì)想了一下,發(fā)現(xiàn)還真不是他們不小心漏掉的,而是有意為之。至于為什么會(huì)有意為之那就要看它編譯后的產(chǎn)物了:
// 偽代碼
<div ref={a} />
// 將會(huì)被編譯為:
const el = template(`<div>`)
typeof a === 'function' ? a(el) : a = el
其中咱們重點(diǎn)看 a = el 這段代碼,a 就是我們寫在 ref 里的,但假如我們給它換成一個(gè)二元表達(dá)式就會(huì)變成:
// 偽代碼
<div ref={a || b} />
// 將會(huì)被編譯為:
const el = template(`<div>`)
a || b = el
a || b 不能放在等號(hào)左側(cè),所以源碼中的 isLVal 就是為了過(guò)濾這種情況的。那為什么不能編譯成:
(a = el) || (b = el)
這么編譯是錯(cuò)的,因?yàn)榧偃?nbsp;a 為 false,a 就不應(yīng)該被賦值,但實(shí)際上 a 會(huì)被賦值為 el:
所以要把二元編譯成三元:
如果是并且符號(hào)就要編譯成取反:
// 偽代碼
<div ref={a && b} />
// 將會(huì)被編譯為:
const el = template(`<div>`)
!a ? a = el : b = el
然后三元表達(dá)式以及嵌套三元表達(dá)式:
<div
ref={
Math.random() > 0.5
? refFactory() && refArr[0] && (refTarget1 = refTarget2) && (refTarget1 > refTarget2)
: refTarget1
? refTarget2
: refTarget3
}
/>
當(dāng)然可能并不會(huì)有人這么寫,Solid 那幫人也是這么想的,所以就算了,太麻煩了,如果真要是有復(fù)雜的條件的話可以用函數(shù):
<div
ref={
el => Math.random() > 0.5
? refTarget1 = el
: refTarget2 = el
}
/>
就先不管 isLVal 為 false 的情況了,不過(guò)我還是覺(jué)得至少要在官網(wǎng)上提一嘴,不然真有人寫成這樣的時(shí)候又搜不到答案的話那多影響口碑??!
總結(jié)
看過(guò)源碼之后感覺(jué)有的地方設(shè)計(jì)的很巧妙,但有些地方又不是很嚴(yán)謹(jǐn)。也怪 jsx 太靈活了,不可能做判斷把所有情況都做到面面俱到,當(dāng)你要寫一些在 React 里能運(yùn)行的騷操作可能在 Solid 里就啞火了。