深入理解 React

2019-08-14 14:28 更新

這是一篇源自官方博客 文章。

在我看來(lái), React 是較早使用 JavaScript 構(gòu)建大型、快速的 Web 應(yīng)用程序的技術(shù)方案。它已經(jīng)被我們廣泛應(yīng)用于 Facebook 和 Instagram 。

React 眾多優(yōu)秀特征中的其中一部分就是,教會(huì)你去重新思考如何構(gòu)建應(yīng)用程序。

本文中,我將跟你一起使用 React 構(gòu)建一個(gè)具備搜索功能的產(chǎn)品列表。

注意:

如果你無(wú)法看到本頁(yè)內(nèi)嵌的代碼片段,請(qǐng)確認(rèn)你不是用 https 協(xié)議加載本頁(yè)的。

從原型( mock )開(kāi)始

假設(shè)我們已經(jīng)擁有了一個(gè) JSON API 和設(shè)計(jì)師設(shè)計(jì)的原型。我們的設(shè)計(jì)師顯然不夠好,因?yàn)樵涂雌饋?lái)如下:

Mockup

JSON接口返回?cái)?shù)據(jù)如下:

[
  {category: "Sporting Goods", price: "$49.99", stocked: true, name: "Football"},
  {category: "Sporting Goods", price: "$9.99", stocked: true, name: "Baseball"},
  {category: "Sporting Goods", price: "$29.99", stocked: false, name: "Basketball"},
  {category: "Electronics", price: "$99.99", stocked: true, name: "iPod Touch"},
  {category: "Electronics", price: "$399.99", stocked: false, name: "iPhone 5"},
  {category: "Electronics", price: "$199.99", stocked: true, name: "Nexus 7"}
];

第一步:拆分用戶界面為一個(gè)組件樹(shù)

你要做的第一件事是,為所有組件(及子組件)命名并畫(huà)上線框圖。假如你和設(shè)計(jì)師一起工作,也許他們已經(jīng)完成了這項(xiàng)工作,所以趕緊去跟他們溝通!他們的 Photoshop 圖層名也許最終可以直接用于你的 React 組件名。

然而你如何知道哪些才能成為組件?想象一下,當(dāng)你創(chuàng)建一些函數(shù)或?qū)ο髸r(shí),用到一些類(lèi)似的技術(shù)。其中一項(xiàng)技術(shù)就是單一功能原則,指的是,理想狀態(tài)下一個(gè)組件應(yīng)該只做一件事,假如它功能逐漸變大就需要被拆分成更小的子組件。

由于你經(jīng)常需要將一個(gè)JSON數(shù)據(jù)模型展示給用戶,因此你需要檢查這個(gè)模型結(jié)構(gòu)是否正確以便你的 UI (在這里指組件結(jié)構(gòu))是否能夠正確的映射到這個(gè)模型上。這是因?yàn)橛脩艚缑婧蛿?shù)據(jù)模型在 信息構(gòu)造 方面都要一致,這意味著將你可以省下很多將 UI 分割成組件的麻煩事。你需要做的僅僅只是將數(shù)據(jù)模型分隔成一小塊一小塊的組件,以便它們都能夠表示成組件。

Component diagram

由此可見(jiàn),我們的 app 中包含五個(gè)組件。下面我已經(jīng)用斜體標(biāo)示出每個(gè)組件對(duì)應(yīng)的數(shù)據(jù)。

  1. FilterableProductTable (橘色): 包含整個(gè)例子的容器

  2. SearchBar (藍(lán)色): 接受所有 用戶輸入( user input )

  3. ProductTable (綠色): 根據(jù) 用戶輸入( user input ) 過(guò)濾和展示 數(shù)據(jù)集合( data collection )

  4. ProductCategoryRow (青色): 為每個(gè) 分類(lèi)( category ) 展示一列表頭

  5. ProductRow (紅色): 為每個(gè) 產(chǎn)品( product ) 展示一列

如果你仔細(xì)觀察 ProductTable ,你會(huì)發(fā)現(xiàn)表頭(包含“ Name ”和“ Price ”標(biāo)簽)并不是單獨(dú)的組件。這只是一種個(gè)人偏好,也有一定的爭(zhēng)論。在這個(gè)例子當(dāng)中,我把表頭當(dāng)做 ProductTable 的一部分,因?yàn)樗卿秩尽皵?shù)據(jù)集合”的一份子,這也是 ProductTable 的職責(zé)。但是,當(dāng)這個(gè)表頭變得復(fù)雜起來(lái)的時(shí)候(例如,添加排序功能),就應(yīng)該單獨(dú)地寫(xiě)一個(gè) ProductTableHeader 組件。

既然我們?cè)谠彤?dāng)中定義了這個(gè)組件,讓我們把這些元素組成一棵樹(shù)形結(jié)構(gòu)。這很簡(jiǎn)單。被包含在其它組件中的組件在屬性機(jī)構(gòu)中應(yīng)該是子級(jí):

  • FilterableProductTable

    • ProductCategoryRow

    • ProductRow

    • SearchBar -ProductTable

    第二步: 利用 React ,創(chuàng)建應(yīng)用的一個(gè)靜態(tài)版本

    既然已經(jīng)擁有了組件樹(shù),是時(shí)候開(kāi)始實(shí)現(xiàn)應(yīng)用了。最簡(jiǎn)單的方式就是創(chuàng)建一個(gè)應(yīng)用,這個(gè)應(yīng)用將數(shù)據(jù)模型渲染到 UI 上,但是沒(méi)有交互功能。拆分這兩個(gè)過(guò)程是最簡(jiǎn)單的,因?yàn)闃?gòu)建一個(gè)靜態(tài)的版本僅需要大量的輸入,而不需要思考;但是添加交互功能卻需要大量的思考和少量的輸入。我們將會(huì)知道這是為什么。

    為了創(chuàng)建一個(gè)渲染數(shù)據(jù)模型的應(yīng)用的靜態(tài)版本,你將會(huì)構(gòu)造一些組件,這些組件重用其它組件,并且通過(guò) props 傳遞數(shù)據(jù)。 props 是一種從父級(jí)向子級(jí)傳遞數(shù)據(jù)的方式。如果你對(duì) state 概念熟悉,那么不要使用 state 來(lái)構(gòu)建這個(gè)靜態(tài)版本。 state 僅用于實(shí)現(xiàn)交互功能,也就是說(shuō),數(shù)據(jù)隨著時(shí)間變化。因?yàn)檫@是一個(gè)靜態(tài)的應(yīng)用版本,所以你并不需要 state 。

    你可以從上至下或者從下至上來(lái)構(gòu)建應(yīng)用。也就是說(shuō),你可以從屬性結(jié)構(gòu)的頂部開(kāi)始構(gòu)建這些組件(例如,從 FilterableProductTable 開(kāi)始),或者從底部開(kāi)始( ProductRow )。在簡(jiǎn)單的應(yīng)用中,通常情況下從上至下的方式更加簡(jiǎn)單;在大型的項(xiàng)目中,從下至上的方式更加簡(jiǎn)單,這樣也可以在構(gòu)建的同時(shí)寫(xiě)測(cè)試代碼。

    在這步結(jié)束的時(shí)候,將會(huì)有一個(gè)可重用的組件庫(kù)來(lái)渲染數(shù)據(jù)模型。這些組件將會(huì)僅有 render() 方法,因?yàn)檫@是應(yīng)用的一個(gè)靜態(tài)版本。位于樹(shù)形結(jié)構(gòu)頂部的組件( FilterableProductTable )將會(huì)使用數(shù)據(jù)模型作為 prop 。如果你改變底層數(shù)據(jù)模型,然后再次調(diào)用 React.render() , UI 將會(huì)更新。查看 UI 如何被更新和什么地方改變都是很容易的,因?yàn)?React 的單向數(shù)據(jù)流(也被稱(chēng)作“單向綁定”)保持了一切東西模塊化,很容易查錯(cuò),并且速度很快,沒(méi)有什么復(fù)雜的。

    如果你在這步中需要幫助,請(qǐng)查看 React 文檔。

    穿插一小段內(nèi)容: props 與 state 比較

    在 React 中有兩種類(lèi)型的數(shù)據(jù)“模型”: props 和 state 。理解兩者的區(qū)別是很重要的;如果你不太確定兩者有什么區(qū)別,請(qǐng)大致瀏覽一下官方的 React 文檔

    第三步:識(shí)別出最小的(但是完整的)代表 UI 的 state

    為了使 UI 可交互,需要能夠觸發(fā)底層數(shù)據(jù)模型的變化。 React 通過(guò) state 使這變得簡(jiǎn)單。

    為了正確構(gòu)建應(yīng)用,首先需要考慮應(yīng)用需要的最小的可變 state 數(shù)據(jù)模型集合。此處關(guān)鍵點(diǎn)在于精簡(jiǎn):不要存儲(chǔ)重復(fù)的數(shù)據(jù)。構(gòu)造出絕對(duì)最小的滿足應(yīng)用需要的最小 state 是有必要的,并且計(jì)算出其它強(qiáng)烈需要的東西。例如,如果構(gòu)建一個(gè) TODO 列表,僅保存一個(gè) TODO 列表項(xiàng)的數(shù)組,而不要保存另外一個(gè)指代數(shù)組長(zhǎng)度的 state 變量。當(dāng)想要渲染 TODO 列表項(xiàng)總數(shù)的時(shí)候,簡(jiǎn)單地取出 TODO 列表項(xiàng)數(shù)組的長(zhǎng)度就可以了。

    思考示例應(yīng)用中的所有數(shù)據(jù)片段,有:

    • 最初的 products 列表

    • 用戶輸入的搜索文本

    • 復(fù)選框的值

    • 過(guò)濾后的 products 列表

    讓我們分析每一項(xiàng),指出哪一個(gè)是 state 。簡(jiǎn)單地對(duì)每一項(xiàng)數(shù)據(jù)提出三個(gè)問(wèn)題:

    1. 是否是從父級(jí)通過(guò) props 傳入的?如果是,可能不是 state 。

    2. 是否會(huì)隨著時(shí)間改變?如果不是,可能不是 state 。

    3. 能根據(jù)組件中其它 state 數(shù)據(jù)或者 props 計(jì)算出來(lái)嗎?如果是,就不是 state 。

    初始的 products 列表通過(guò) props 傳入,所以不是 state 。搜索文本和復(fù)選框看起來(lái)像是 state ,因?yàn)樗鼈冸S著時(shí)間改變,也不能根據(jù)其它數(shù)據(jù)計(jì)算出來(lái)。最后,過(guò)濾的 products 列表不是 state ,因?yàn)榭梢酝ㄟ^(guò)搜索文本和復(fù)選框的值從初始的 products 列表計(jì)算出來(lái)。

    所以最終, state 是:

    • 用戶輸入的搜索文本

    • 復(fù)選框的值

    第四步:確認(rèn) state 的生命周期

    OK,我們辨別出了應(yīng)用的 state 數(shù)據(jù)模型的最小集合。接下來(lái),需要指出哪個(gè)組件會(huì)改變或者說(shuō)擁有這個(gè) state 數(shù)據(jù)模型。

    記住: React 中數(shù)據(jù)是沿著組件樹(shù)從上到下單向流動(dòng)的。可能不會(huì)立刻明白哪個(gè)組件應(yīng)該擁有哪些 state 數(shù)據(jù)模型。這對(duì)新手通常是最難理解和最具挑戰(zhàn)的,因此跟隨以下步驟來(lái)弄清楚這點(diǎn):

    對(duì)于應(yīng)用中的每一個(gè) state 數(shù)據(jù):

    • 找出每一個(gè)基于那個(gè) state 渲染界面的組件。

    • 找出共同的祖先組件(某個(gè)單個(gè)的組件,在組件樹(shù)中位于需要這個(gè) state 的所有組件的上面)。

    • 要么是共同的祖先組件,要么是另外一個(gè)在組件樹(shù)中位于更高層級(jí)的組件應(yīng)該擁有這個(gè) state 。

    • 如果找不出擁有這個(gè) state 數(shù)據(jù)模型的合適的組件,創(chuàng)建一個(gè)新的組件來(lái)維護(hù)這個(gè) state ,然后添加到組件樹(shù)中,層級(jí)位于所有共同擁有者組件的上面。

    讓我們?cè)趹?yīng)用中應(yīng)用這個(gè)策略:

    • ProductTable 需要基于 state 過(guò)濾產(chǎn)品列表,SearchBar 需要顯示搜索文本和復(fù)選框狀態(tài)。

    • 共同擁有者組件是 FilterableProductTable 。

    • 理論上,過(guò)濾文本和復(fù)選框值位于 FilterableProductTable 中是合適的。

    太酷了,我們決定了 state 數(shù)據(jù)模型位于 FilterableProductTable 之中。首先,給 FilterableProductTable 添加 getInitialState() 方法,該方法返回 {filterText: '', inStockOnly: false} 來(lái)反映應(yīng)用的初始化狀態(tài)。然后傳遞 filterText  inStockOnly  ProductTable  SearchBar作為 prop 。最后,使用這些 props 來(lái)過(guò)濾 ProductTable 中的行,設(shè)置在 SearchBar 中表單字段的值。

    你可以開(kāi)始觀察應(yīng)用將會(huì)如何運(yùn)行:設(shè)置 filterText  "ball" ,然后刷新應(yīng)用。將會(huì)看到數(shù)據(jù)表格被正確更新了。

    第五步:添加反向數(shù)據(jù)流

    到目前為止,已經(jīng)構(gòu)建了渲染正確的基于 props 和 state 的沿著組件樹(shù)從上至下單向數(shù)據(jù)流動(dòng)的應(yīng)用?,F(xiàn)在,是時(shí)候支持另外一種數(shù)據(jù)流動(dòng)方式了:組件樹(shù)中層級(jí)很深的表單組件需要更新 FilterableProductTable 中的 state 。

    React 讓這種數(shù)據(jù)流動(dòng)非常明確,從而很容易理解應(yīng)用是如何工作的,但是相對(duì)于傳統(tǒng)的雙向數(shù)據(jù)綁定,確實(shí)需要輸入更多的東西。 React 提供了一個(gè)叫做 ReactLink 的插件來(lái)使其和雙向數(shù)據(jù)綁定一樣方便,但是考慮到這篇文章的目的,我們將會(huì)保持所有東西都直截了當(dāng)。

    如果你嘗試在示例的當(dāng)前版本中輸入或者選中復(fù)選框,將會(huì)發(fā)現(xiàn) React 會(huì)忽略你的輸入。這是有意的,因?yàn)橐呀?jīng)設(shè)置了 input  value 屬性,使其總是與從 FilterableProductTable 傳遞過(guò)來(lái)的 state 一致。

    讓我們思考下我們希望發(fā)生什么。我們想確保無(wú)論何時(shí)用戶改變了表單,都要更新 state 來(lái)反映用戶的輸入。由于組件只能更新自己的 state ,FilterableProductTable 將會(huì)傳遞一個(gè)回調(diào)函數(shù)給 SearchBar ,此函數(shù)將會(huì)在 state 應(yīng)該被改變的時(shí)候觸發(fā)。我們可以使用 input 的 onChange 事件來(lái)監(jiān)聽(tīng)用戶輸入,從而確定何時(shí)觸發(fā)回調(diào)函數(shù)。 FilterableProductTable 傳遞的回調(diào)函數(shù)將會(huì)調(diào)用 setState() ,然后應(yīng)用將會(huì)被更新。

    雖然這聽(tīng)起來(lái)有很多內(nèi)容,但是實(shí)際上僅僅需要幾行代碼。并且關(guān)于數(shù)據(jù)在應(yīng)用中如何流動(dòng)真的非常清晰明確。

    就這么簡(jiǎn)單

    希望以上內(nèi)容讓你明白了如何思考用 React 去構(gòu)造組件和應(yīng)用。雖然可能比你之前要輸入更多的代碼,記住,讀代碼的時(shí)間遠(yuǎn)比寫(xiě)代碼的時(shí)間多,并且閱讀這種模塊化的清晰的代碼是相當(dāng)容易的。當(dāng)你開(kāi)始構(gòu)建大型的組件庫(kù)的時(shí)候,你將會(huì)非常感激這種清晰性和模塊化,并且隨著代碼的復(fù)用,整個(gè)項(xiàng)目代碼量就開(kāi)始變少了 :)。


    以上內(nèi)容是否對(duì)您有幫助:
    在線筆記
    App下載
    App下載

    掃描二維碼

    下載編程獅App

    公眾號(hào)
    微信公眾號(hào)

    編程獅公眾號(hào)