“React 和 Vue 哪個更好?” 論壇上經常看到這樣的問題,然後評論區就直接開戰了。也有朋友轉行做前端,問我該學React還是Vue。幾年前,可能確實有必要考慮下到底該選擇哪一個,畢竟前端圈子這麼亂,誰又知道Vue能走多遠?React會不會不維護了呢?可現在兩者生態都很不錯,Vue確實好用,React學習成本也沒有傳聞中那麼高,template很好用,jsx也更靈活。可以兩者都去玩玩,根據個人喜好和專案需要來選擇用哪個。而如果能夠結合兩者的優點,那豈不是很有趣?
我剛轉前端的時候,用的是vue版本好像還是1.0,那時候的感覺就是資料繫結比jquery操作dom省事兒太多了。後來又接觸了React,之後的專案大部分用React寫的,現在偶爾也用vue,總體感覺就是vue單檔案元件結構比較清晰,模板指令也很好用,而jsx更加靈活,之前有在react狀態管理部分做一些嘗試,可以像普通function一樣去更新狀態,也一直想在jsx中加上類似vue裡面的模板指令,直到前幾天比較閒,總算實現了 “條件渲染”和 “列表渲染”,結果還算不錯,過程也挺有趣。
先來看看結果吧,以前要根據不同狀態來控制模組是否顯示,我們大概要寫這樣的程式碼:
render(){
const visible = true
return(
<div>
{
visible ? <div>content<div>
: ''
}
</div>
)
}
複製程式碼
現在可以這麼玩:
render(){
const visible = true
return(
<div>
<div r-if = {visible}>content</div>
</div>
)
}
複製程式碼
另一種常見的場景就是根據一個陣列來渲染出一個列表,一般是這麼寫:
render(){
const list = [1, 2, 3, 4, 5]
return(
<div>
{
list.map((item,index)=>(
<div key={index}>{item}</div>
))
}
</div>
)
}
複製程式碼
現在可以更簡潔:
render(){
const list = [1, 2, 3, 4, 5]
return(
<div>
<div r-for = {item in list}>{item}</div>
</div>
)
}
複製程式碼
以上程式碼會自動設定key,值為當前元素的索引。如果你想要自定義key,也可以加上,改成
<div r-for = {(item,index) in list} key = {index+1}>{item}</div>
複製程式碼
結果還算不錯吧,程式碼更簡短,語義也比較明確,體驗也不必vue裡面的模板指令差,個人感覺在“”裡面寫js有點奇怪。而在{}裡面寫就很自然,就是普通的js程式碼塊嘛。
至於實現方法嘛,其實很簡單,總共才幾十行程式碼,就是寫了一個babel外掛。
我們寫的jsx也是通過babel轉譯成普通js程式碼的,然後才能在瀏覽器中執行,而babel編譯主要分為三個階段:解析、轉換、生成目的碼。解析部分就是將原始碼解析成抽象語法樹ast,轉換過程是對ast做一些處理,而生成目的碼部分就是將ast再轉換成js程式碼。babel-plugin就是在轉換部分做一些工作。
比如對於以下jsx:
<div r-if = { visible }>{content}</div>
複製程式碼
轉換成的ast結構大概是:
{
type: 'CallExpression',
callee: {},
arguments: {
properties: []
}
}
複製程式碼
目的碼為:
React.createElement(
'div',
{'r-if': visible},
content
)
複製程式碼
React.createElement()方法呼叫對應ast中的CallExpression, React和createElement在callee中可以找到,可以以此來找出createElement(), r-if 等屬性以及第三個引數content在arguments的properties陣列中能找到。有了這些資訊,我們就可以遍歷ast,找到那些callee為React.createElement的CallExpression, 然後判斷arguments中如果出現了r-if, 就對ast做以下修改:首先移除r-if屬性,避免死迴圈;然後在CallExpression對應的節點外面再套一層ifStatement, 如此一來,轉換後的ast生成的目的碼大致如下:
if(visible){
React.createElement(
'div',
{'r-if': visible},
content
)
}
複製程式碼
至此,我們的目標就已經達到了,至於r-for列表渲染,原理類似,先找出有r-for屬性的CallExpression, 然後構造一個map方法對應的CallExpression, 當前CallExpression作為引數傳給map方法即可。需要注意的是,在同一次遍歷中解析出 r-if 和 r-for 的話,需要把map放在外層,ifStatement放在裡面,如果覺得這樣做結構比較混亂,可以拆分成不同的外掛。
最後再說一下babel-plugin的寫法,其實也就是一個方法:
module.exports = function ({ types: t }) {
return {
visitor: {
CallExpression(path) {
// 在這裡通過修改path來修改ast。
},
Identifier(path) {
}
}
}
}
複製程式碼
types型別為babel-types, 提供了一些類似loadash的操作方法,比如做一些判斷、構造節點。visitor裡面寫對應型別節點的遍歷方法, 比如遍歷識別符號型別的就寫在Identifier中,方法呼叫就寫在CallExpression中。
本文中提到的r-if 和 r-for 已經寫成了一個外掛,可以在github倉庫中找到:github.com/evolify/bab… 同時也釋出到了npm倉庫,可以直接安裝:
yarn add --dev babel-plugin-react-directive
然後在.babelrc中配置即可:
{
"plugins": [
"react-directive"
]
}
複製程式碼
我想要的目的已經達到了,但這並未結束,才剛剛開始,還可以實現其他的一些指令,比如r-if 是模組渲染或者不渲染的,我們經常也會遇到這種需求:只是單純的控制元素可見或者不可見,但元素還是佔用空間的,也就是控制visibility, 這也可以寫成一個指令。而babel能做的遠遠不止如此,無聊的時候可以好好玩一玩。