新聞中心
很多教程都把 React 作為一個(gè) UI 庫(kù)來引入。這是很有道理的,因?yàn)?React 本身就是一個(gè) UI 庫(kù)。就像官網(wǎng)上所說的那樣。

目前成都創(chuàng)新互聯(lián)公司已為上千多家的企業(yè)提供了網(wǎng)站建設(shè)、域名、網(wǎng)站空間、成都網(wǎng)站托管、企業(yè)網(wǎng)站設(shè)計(jì)、蘄春網(wǎng)站維護(hù)等服務(wù),公司將堅(jiān)持客戶導(dǎo)向、應(yīng)用為本的策略,正道將秉承"和諧、參與、激情"的文化,與客戶和合作伙伴齊心協(xié)力一起成長(zhǎng),共同發(fā)展。
我曾經(jīng)寫過關(guān)于構(gòu)建用戶界面中遇到的挑戰(zhàn)的文章。但是本文將會(huì)用另外一種方式來講述 React —— 因?yàn)樗袷且环N編程運(yùn)行時(shí)。
本文不會(huì)教你任何有關(guān)如何創(chuàng)建界面的技巧。 但是它可能會(huì)幫你更加深入地理解 React 編程模型。
? 注意:如果你還在學(xué)習(xí) React ,請(qǐng)移步到官方文檔進(jìn)行學(xué)習(xí)
本文將會(huì)非常深入 —— 所以不適合初學(xué)者閱讀。在本文中,我會(huì)從***原則的角度盡可能地闡述 React 編程模型。我不會(huì)解釋如何使用它 —— 而是講解它的工作原理。
本文面向有經(jīng)驗(yàn)的程序員,還有使用過其他 UI 庫(kù),但在項(xiàng)目中權(quán)衡利弊之后最終選擇了 React 的人,我希望它會(huì)對(duì)你有所幫助!
一些人用了很多年 React 卻從沒有考慮過接下來我要講述的主題。 這絕對(duì)是以程序員而不是以設(shè)計(jì)者的角度來看待 React。但我認(rèn)為站在兩個(gè)不同的角度來重新認(rèn)識(shí) React 并沒有什么壞處。
廢話少說,讓我們開始深入理解 React 吧!
宿主樹
一些程序輸出數(shù)字。另一些程序輸出詩(shī)詞。不同的語(yǔ)言和它們的運(yùn)行時(shí)通常會(huì)對(duì)特定的一組用例進(jìn)行優(yōu)化, React 也不例外。
React 程序通常會(huì)輸出一個(gè)會(huì)隨時(shí)間變化的樹。 它有可能是 DOM 樹 、iOS 視圖層、PDF 原語(yǔ) ,或者是 JSON 對(duì)象 。不過通常我們希望用它來展示 UI 。我們稱它為“宿主樹”,因?yàn)樗?React 之外宿主環(huán)境中的一部分 —— 就像 DOM 或 iOS 。宿主樹通常有它自己的命令式 API 。而 React 就是它上面的那一層。
所以 React 到底有什么用呢?非常抽象,它可以幫助你編寫可預(yù)測(cè)的,并且能夠操控復(fù)雜的宿主樹進(jìn)而響應(yīng)像用戶交互、網(wǎng)絡(luò)響應(yīng)、定時(shí)器等外部事件的應(yīng)用程序。
當(dāng)一個(gè)專業(yè)的工具可以施加特定的約束,并且能從中獲益時(shí),它就比一般的工具要好。React 就是這樣的典范,并且它堅(jiān)持兩個(gè)原則:
- 穩(wěn)定性。 宿主樹是相對(duì)穩(wěn)定的,大多數(shù)情況的更新并不會(huì)從根本上改變其整體結(jié)構(gòu)。如果應(yīng)用程序每秒都會(huì)將其所有可交互的元素重新排列為完全不同的組合,那將會(huì)變得難以使用。那個(gè)按鈕去哪了?為什么我的屏幕在跳舞?
- 通用性。 宿主樹可以被拆分為外觀和行為一致的 UI 模式(例如按鈕、列表和頭像)而不是隨機(jī)的形狀。
這些原則恰好適用于大多數(shù) UI 。 不過當(dāng)輸出沒有穩(wěn)定的“模式”時(shí) React 并不適用。例如,React 也許可以幫你寫一個(gè) Twitter 客戶端,但對(duì)于一個(gè) 3D 管道屏幕保護(hù)程序并沒有太大用處。
宿主實(shí)例
宿主樹由節(jié)點(diǎn)組成,我們稱之為“宿主實(shí)例”。
在 DOM 環(huán)境中,宿主實(shí)例就是我們通常所說的 DOM 節(jié)點(diǎn) —— 就像當(dāng)你調(diào)用 document.createElement('div') 時(shí)獲得的對(duì)象。在 iOS 中,宿主實(shí)例可以是從 JavaScript 到原生視圖唯一標(biāo)識(shí)的值。
宿主實(shí)例有它們自己的屬性(例如 domNode.className 或者 view.tintColor )。它們也有可能將其他的宿主實(shí)例作為子項(xiàng)。
(這和 React 沒有任何聯(lián)系 — 因?yàn)槲以谥v述宿主環(huán)境。)
通常會(huì)有原生 API 用于操控這些宿主實(shí)例。例如,在 DOM 環(huán)境中會(huì)提供像 appendChild、 removeChild、 setAttribute 等一系列的 API 。在 React 應(yīng)用中,通常你不會(huì)調(diào)用這些 API ,因?yàn)槟鞘?React 的工作。
渲染器
渲染器告訴 React 如何與特定的宿主環(huán)境通信,以及如何管理它的宿主實(shí)例。React DOM、React Native 甚至 Ink 都可以被稱作 React 渲染器。你也可以創(chuàng)建自己的 React 渲染器 。
React 渲染器能以下面兩種模式之一進(jìn)行工作。
絕大多數(shù)渲染器都被用作“突變”模式。這種模式正是 DOM 的工作方式:我們可以創(chuàng)建一個(gè)節(jié)點(diǎn),設(shè)置它的屬性,在之后往里面增加或者刪除子節(jié)點(diǎn)。宿主實(shí)例是完全可變的。
但 React 也能以”不變“模式工作。這種模式適用于那些并不提供像 appendChild 的 API 而是克隆雙親樹并始終替換掉***子樹的宿主環(huán)境。在宿主樹級(jí)別上的不可變性使得多線程變得更加容易。React Fabric 就利用了這一模式。
作為 React 的使用者,你永遠(yuǎn)不需要考慮這些模式。我只想強(qiáng)調(diào) React 不僅僅只是從一種模式轉(zhuǎn)換到另一種模式的適配器。它的用處在于以一種更好的方式操控宿主實(shí)例而不用在意那些低級(jí)視圖 API 范例。
React 元素
在宿主環(huán)境中,一個(gè)宿主實(shí)例(例如 DOM 節(jié)點(diǎn))是最小的構(gòu)建單元。而在 React 中,最小的構(gòu)建單元是 React 元素。
React 元素是一個(gè)普通的 JavaScript 對(duì)象。它用來描述一個(gè)宿主實(shí)例。
- // JSX 是用來描述這些對(duì)象的語(yǔ)法糖。
- //
- {
- type: 'button',
- props: { className: 'blue' }
- }
React 元素是輕量級(jí)的,因?yàn)闆]有任何宿主實(shí)例與它綁定在一起。同樣,它只是對(duì)你想要在屏幕上看到的內(nèi)容的描述。
就像宿主實(shí)例一樣,React 元素也能形成一棵樹:
- // JSX 是用來描述這些對(duì)象的語(yǔ)法糖。
- //
- //
- //
- //
- {
- type: 'dialog',
- props: {
- children: [{
- type: 'button',
- props: { className: 'blue' }
- }, {
- type: 'button',
- props: { className: 'red' }
- }]
- }
- }
(注意:我省略了一些對(duì)此解釋不重要的屬性)
但是請(qǐng)記住 React 元素并不是永遠(yuǎn)存在的 。它們總是在重建和刪除之間不斷循環(huán)。
React 元素具有不可變性。例如你不能改變 React 元素中的子元素或者屬性。如果你想要在稍后渲染一些不同的東西,需要從頭創(chuàng)建新的 React 元素樹來描述它。
我喜歡將 React 元素比作電影中放映的每一幀。它們捕捉 UI 在特定的時(shí)間點(diǎn)的樣子。它們永遠(yuǎn)不會(huì)再改變。
入口
每一個(gè) React 渲染器都有一個(gè)“入口”。正是那個(gè)特定的 API 讓我們告訴 React ,將特定的 React 元素樹渲染到真正的宿主實(shí)例中去。
例如,React DOM 的入口就是 ReactDOM.render :
- ReactDOM.render(
- // { type: 'button', props: { className: 'blue' } }
- ,
- document.getElementById('container')
- );
當(dāng)我們調(diào)用 ReactDOM.render(reactElement, domContainer) 時(shí),我們的意思是:“親愛的 React ,將我的 reactElement 映射到 domContaienr 的宿主樹上去吧?!?/p>
React 會(huì)查看 reactElement.type (在我們的例子中是 button )然后告訴 React DOM 渲染器創(chuàng)建對(duì)應(yīng)的宿主實(shí)例并設(shè)置正確的屬性:
- // 在 ReactDOM 渲染器內(nèi)部(簡(jiǎn)化版)
- function createHostInstance(reactElement) {
- let domNode = document.createElement(reactElement.type);
- domNode.className = reactElement.props.className;
- return domNode;
- }
在我們的例子中,React 會(huì)這樣做:
- let domNode = document.createElement('button');
- domNode.className = 'blue';
- domContainer.appendChild(domNode);
如果 React 元素在 reactElement.props.children 中含有子元素,React 會(huì)在***次渲染中遞歸地為它們創(chuàng)建宿主實(shí)例。
協(xié)調(diào)
如果我們用同一個(gè) container 調(diào)用 ReactDOM.render() 兩次會(huì)發(fā)生什么呢?
- ReactDOM.render(
- , document.getElementById('container')
- );
- // ... 之后 ...
- // 應(yīng)該替換掉 button 宿主實(shí)例嗎?
- // 還是在已有的 button 上更新屬性?
- ReactDOM.render(
- , document.getElementById('container')
- );
同樣,React 的工作是將 React 元素樹映射到宿主樹上去。確定該對(duì)宿主實(shí)例做什么來響應(yīng)新的信息有時(shí)候叫做協(xié)調(diào) 。
有兩種方法可以解決它。簡(jiǎn)化版的 React 會(huì)丟棄已經(jīng)存在的樹然后從頭開始創(chuàng)建它:
- let domContainer = document.getElementById('container');
- // 清除掉原來的樹
- domContainer.innerHTML = '';
- // 創(chuàng)建新的宿主實(shí)例樹
- let domNode = document.createElement('button');
- domNode.className = 'red';
- domContainer.appendChild(domNode);
但是在 DOM 環(huán)境下,這樣的做法效率很低,而且會(huì)丟失 focus、selection、scroll 等許多狀態(tài)。相反,我們希望 React 這樣做:
- let domNode = domContainer.firstChild;
- // 更新已有的宿主實(shí)例
- domNode.className = 'red';
換句話說,React 需要決定何時(shí)更新一個(gè)已有的宿主實(shí)例來匹配新的 React 元素,何時(shí)該重新創(chuàng)建新的宿主實(shí)例。
這就引出了一個(gè)識(shí)別問題。React 元素可能每次都不相同,到底什么時(shí)候才該從概念上引用同一個(gè)宿主實(shí)例呢?
在我們的例子中,它很簡(jiǎn)單。我們之前渲染了
這與 React 如何思考并解決這類問題已經(jīng)很接近了。
如果相同的元素類型在同一個(gè)地方先后出現(xiàn)兩次,React 會(huì)重用已有的宿主實(shí)例。
這里有一個(gè)例子,其中的注釋大致解釋了 React 是如何工作的:
- // let domNode = document.createElement('button');
- // domNode.className = 'blue';
- // domContainer.appendChild(domNode);
- ReactDOM.render(
- ,
- document.getElementById('container')
- );
- // 能重用宿主實(shí)例嗎?能!(button → button)
- // domNode.className = 'red';
- ReactDOM.render(
- ,
- document.getElementById('container')
- );
- // 能重用宿主實(shí)例嗎?不能!(button → p)
- // domContainer.removeChild(domNode);
- // domNode = document.createElement('p');
- // domNode.textContent = 'Hello';
- // domContainer.appendChild(domNode);
- ReactDOM.render(
Hello
,- document.getElementById('container')
- );
- // 能重用宿主實(shí)例嗎?能!(p → p)
- // domNode.textContent = 'Goodbye';
- ReactDOM.render(
Goodbye
,- document.getElementById('container')
- );
同樣的啟發(fā)式方法也適用于子樹。例如,當(dāng)我們?cè)? 中新增兩個(gè) ,React 會(huì)先決定是否要重用 ,然后為每一個(gè)子元素重復(fù)這個(gè)決定步驟。
條件
如果 React 在渲染更新前后只重用那些元素類型匹配的宿主實(shí)例,那當(dāng)遇到包含條件語(yǔ)句的內(nèi)容時(shí)又該如何渲染呢?
假設(shè)我們只想首先展示一個(gè)輸入框,但之后要在它之前渲染一條信息:
- // ***次渲染
- ReactDOM.render(
- ,
- domContainer
- );
- // 下一次渲染
- ReactDOM.render(
I was just added here!
- ,
- domContainer
- );
在這個(gè)例子中, 宿主實(shí)例會(huì)被重新創(chuàng)建。React 會(huì)遍歷整個(gè)元素樹,并將其與先前的版本進(jìn)行比較:
- dialog → dialog :能重用宿主實(shí)例嗎?能 — 因?yàn)轭愋褪瞧ヅ涞摹?
- input → p :能重用宿主實(shí)例嗎?不能,類型改變了! 需要?jiǎng)h除已有的 input 然后重新創(chuàng)建一個(gè) p 宿主實(shí)例。
- (nothing) → input :需要重新創(chuàng)建一個(gè) input 宿主實(shí)例。
因此,React 會(huì)像這樣執(zhí)行更新:
- let oldInputNode = dialogNode.firstChild;
- dialogNode.removeChild(oldInputNode);
- let pNode = document.createElement('p');
- pNode.textContent = 'I was just added here!';
- dialogNode.appendChild(pNode);
- let newInputNode = document.createElement('input');
- dialogNode.appendChild(newInputNode);
這樣的做法并不科學(xué)因?yàn)槭聦?shí)上 并沒有被 所替代 — 它只是移動(dòng)了位置而已。我們不希望因?yàn)橹亟?DOM 而丟失了 selection、focus 等狀態(tài)以及其中的內(nèi)容。
所替代 — 它只是移動(dòng)了位置而已。我們不希望因?yàn)橹亟?DOM 而丟失了 selection、focus 等狀態(tài)以及其中的內(nèi)容。
雖然這個(gè)問題很容易解決(在下面我會(huì)馬上講到),但這個(gè)問題在 React 應(yīng)用中并不常見。而當(dāng)我們探討為什么會(huì)這樣時(shí)卻很有意思。
事實(shí)上,你很少會(huì)直接調(diào)用 ReactDOM.render 。相反,在 React 應(yīng)用中程序往往會(huì)被拆分成這樣的函數(shù):
- function Form({ showMessage }) {
- let message = null;
- if (showMessage) {
- message =
I was just added here!
;- }
- return (
- {message}
- );
- }
這個(gè)例子并不會(huì)遇到剛剛我們所描述的問題。讓我們用對(duì)象注釋而不是 JSX 也許可以更好地理解其中的原因。來看一下 dialog 中的子元素樹:
- function Form({ showMessage }) {
- let message = null;
- if (showMessage) {
- message = {
- type: 'p',
- props: { children: 'I was just added here!' }
- };
- }
- return {
- type: 'dialog',
- props: {
- children: [
- message,
- { type: 'input', props: {} }
- ]
- }
- };
- }
不管 showMessage 是 true 還是 false ,在渲染的過程中 總是在第二個(gè)孩子的位置且不會(huì)改變。
如果 showMessage 從 false 改變?yōu)?true ,React 會(huì)遍歷整個(gè)元素樹,并與之前的版本進(jìn)行比較:
- dialog → dialog :能夠重用宿主實(shí)例嗎?能 — 因?yàn)轭愋推ヅ洹?
- (null) → p :需要插入一個(gè)新的 p 宿主實(shí)例。
- input → input :能夠重用宿主實(shí)例嗎?能 — 因?yàn)轭愋推ヅ洹?/li>
之后 React 大致會(huì)像這樣執(zhí)行代碼:
- let inputNode = dialogNode.firstChild;
- let pNode = document.createElement('p');
- pNode.textContent = 'I was just added here!';
- dialogNode.insertBefore(pNode, inputNode);
這樣一來輸入框中的狀態(tài)就不會(huì)丟失了。
列表
比較樹中同一位置的元素類型對(duì)于是否該重用還是重建相應(yīng)的宿主實(shí)例往往已經(jīng)足夠。
但這只適用于當(dāng)子元素是靜止的并且不會(huì)重排序的情況。在上面的例子中,即使 message 不存在,我們?nèi)匀恢垒斎肟蛟谙⒅?,并且再?zèng)]有其他的子元素。
而當(dāng)遇到動(dòng)態(tài)列表時(shí),我們不能確定其中的順序總是一成不變的。
- function ShoppingList({ list }) {
- return (
- {list.map(item => (
- You bought {item.name}
- Enter how many do you want:
- ))}
- )
- }
如果我們的商品列表被重新排序了,React 只會(huì)看到所有的 p 以及里面的 input 擁有相同的類型,并不知道該如何移動(dòng)它們。(在 React 看來,雖然這些商品本身改變了,但是它們的順序并沒有改變。)
所以 React 會(huì)對(duì)這十個(gè)商品進(jìn)行類似如下的重排序:
- for (let i = 0; i < 10; i++) {
- let pNode = formNode.childNodes[i];
- let textNode = pNode.firstChild;
- textNode.textContent = 'You bought ' + items[i].name;
- }
React 只會(huì)對(duì)其中的每個(gè)元素進(jìn)行更新而不是將其重新排序。這樣做會(huì)造成性能上的問題和潛在的 bug 。例如,當(dāng)商品列表的順序改變時(shí),原本在***個(gè)輸入框的內(nèi)容仍然會(huì)存在于現(xiàn)在的***個(gè)輸入框中 — 盡管事實(shí)上在商品列表里它應(yīng)該代表著其他的商品!
這就是為什么每次當(dāng)輸出中包含元素?cái)?shù)組時(shí),React 都會(huì)讓你指定一個(gè)叫做 key 的屬性:
- function ShoppingList({ list }) {
- return (
- {list.map(item => (
- You bought {item.name}
- Enter how many do you want:
- ))}
- )
- }
key 給予 React 判斷子元素是否真正相同的能力,即使在渲染前后它在父元素中的位置不是相同的。
當(dāng) React 在 中發(fā)現(xiàn) ,它就會(huì)檢查之前版本中的 是否同樣含有 。即使 中的子元素們改變位置后,這個(gè)方法同樣有效。在渲染前后當(dāng) key 仍然相同時(shí),React 會(huì)重用先前的宿主實(shí)例,然后重新排序其兄弟元素。
需要注意的是 key 只與特定的父親 React 元素相關(guān)聯(lián),比如 。React 并不會(huì)去匹配父元素不同但 key 相同的子元素。(React 并沒有慣用的支持對(duì)在不重新創(chuàng)建元素的情況下讓宿主實(shí)例在不同的父元素之間移動(dòng)。)
給 key 賦予什么值***呢?***的答案就是:什么時(shí)候你會(huì)說一個(gè)元素不會(huì)改變即使它在父元素中的順序被改變? 例如,在我們的商品列表中,商品本身的 ID 是區(qū)別于其他商品的唯一標(biāo)識(shí),那么它就最適合作為 key 。
組件
我們已經(jīng)知道函數(shù)會(huì)返回 React 元素:
- function Form({ showMessage }) {
- let message = null;
- if (showMessage) {
- message =
I was just added here!
;- }
- return (
- {message}
- );
- }
這些函數(shù)被叫做組件。它們讓我們可以打造自己的“工具箱”,例如按鈕、頭像、評(píng)論框等等。組件就像 React 的面包和黃油。
組件接受一個(gè)參數(shù) — 對(duì)象哈希。它包含“props”(“屬性”的簡(jiǎn)稱)。在這里 showMessage 就是一個(gè) prop 。它們就像是具名參數(shù)一樣。
純凈
React 組件中對(duì)于 props 應(yīng)該是純凈的。
- function Button(props) {
- // 沒有作用
- props.isActive = true;
- }
通常來說,突變?cè)?React 中不是慣用的。(我們會(huì)在之后講解如何用更慣用的方式來更新 UI 以響應(yīng)事件。)
不過,局部的突變是絕對(duì)允許的:
- function FriendList({ friends }) {
- let items = [];
- for (let i = 0; i < friends.length; i++) {
- let friend = friends[i];
- items.push(
- );
- }
- return
{items} ;- }
當(dāng)我們?cè)诤瘮?shù)組件內(nèi)部創(chuàng)建 items 時(shí)不管怎樣改變它都行,只要這些突變發(fā)生在將其作為***的渲染結(jié)果之前。所以并不需要重寫你的代碼來避免局部突變。
同樣地,惰性初始化是被允許的即使它不是完全“純凈”的:
- function ExpenseForm() {
- // 只要不影響其他組件這是被允許的:
- SuperCalculator.initializeIfNotReady();
- // 繼續(xù)渲染......
- }
只要調(diào)用組件多次是安全的,并且不會(huì)影響其他組件的渲染,React 并不關(guān)心你的代碼是否像嚴(yán)格的函數(shù)式編程一樣***純凈。在 React 中,冪等性比純凈性更加重要。
也就是說,在 React 組件中不允許有用戶可以直接看到的副作用。換句話說,僅調(diào)用函數(shù)式組件時(shí)不應(yīng)該在屏幕上產(chǎn)生任何變化。
遞歸
我們?cè)撊绾卧诮M件中使用組件?組件屬于函數(shù)因此我們可以直接進(jìn)行調(diào)用:
- let reactElement = Form({ showMessage: true });
- ReactDOM.render(reactElement, domContainer);
然而,在 React 運(yùn)行時(shí)中這并不是慣用的使用組件的方式。
相反,使用組件慣用的方式與我們已經(jīng)了解的機(jī)制相同 — 即 React 元素。這意味著不需要你直接調(diào)用組件函數(shù),React 會(huì)在之后為你做這件事情:
- // { type: Form, props: { showMessage: true } }
- let reactElement = ;
- ReactDOM.render(reactElement, domContainer);
然后在 React 內(nèi)部,你的組件會(huì)這樣被調(diào)用:
- // React 內(nèi)部的某個(gè)地方
- let type = reactElement.type; // Form
- let props = reactElement.props; // { showMessage: true }
- let result = type(props); // 無論 Form 會(huì)返回什么
組件函數(shù)名稱按照規(guī)定需要大寫。當(dāng) JSX 轉(zhuǎn)換時(shí)看見 而不是 ,它讓對(duì)象 type 本身成為標(biāo)識(shí)符而不是字符串:
- console.log(.type); // 'form' 字符串
- console.log(.type); // Form 函數(shù)
我們并沒有全局的注冊(cè)機(jī)制 — 字面上當(dāng)我們輸入 時(shí)代表著 Form 。如果 Form在局部作用域中并不存在,你會(huì)發(fā)現(xiàn)一個(gè) JavaScript 錯(cuò)誤,就像平常你使用錯(cuò)誤的變量名稱一樣。
因此,當(dāng)元素類型是一個(gè)函數(shù)的時(shí)候 React 會(huì)做什么呢?它會(huì)調(diào)用你的組件,然后詢問組件想要渲染什么元素。
這個(gè)步驟會(huì)遞歸式地執(zhí)行下去,更詳細(xì)的描述在這里 ??偟膩碚f,它會(huì)像這樣執(zhí)行:
- 你:
ReactDOM.render(, domContainer) -
React:
App,你想要渲染什么?App:我要渲染包含的。
-
React:
,你要渲染什么?Layout:我要在中渲染我的子元素。我的子元素是所以我猜它應(yīng)該渲染到中去。React:
,你要渲染什么?:我要在中渲染一些文本和。
React:
,你要渲染什么?:我要渲染含有文本的。
- React:好的,讓我們開始吧:
- // 最終的 DOM 結(jié)構(gòu)
- Some text
這就是為什么我們說協(xié)調(diào)是遞歸式的。當(dāng) React 遍歷整個(gè)元素樹時(shí),可能會(huì)遇到元素的 type 是一個(gè)組件。React 會(huì)調(diào)用它然后繼續(xù)沿著返回的 React 元素下行。最終我們會(huì)調(diào)用完所有的組件,然后 React 就會(huì)知道該如何改變宿主樹。
在之前已經(jīng)討論過的相同的協(xié)調(diào)準(zhǔn)則,在這一樣適用。如果在同一位置的 type 改變了(由索引和可選的 key 決定),React 會(huì)刪除其中的宿主實(shí)例并將其重建。
控制反轉(zhuǎn)
你也許會(huì)好奇:為什么我們不直接調(diào)用組件?為什么要編寫 而不是 Form()?
React 能夠做的更好如果它“知曉”你的組件而不是在你遞歸調(diào)用它們之后生成的 React 元素樹。
- // React 并不知道 Layout 和 Article 的存在。
- // 因?yàn)槟阍谡{(diào)用它們。
- ReactDOM.render(
- Layout({ children: Article() }),
- domContainer
- )
- // React知道 Layout 和 Article 的存在。
- // React 來調(diào)用它們。
- ReactDOM.render(
-
, - domContainer
- )
這是一個(gè)關(guān)于控制反轉(zhuǎn)的經(jīng)典案例。通過讓 React 調(diào)用我們的組件,我們會(huì)獲得一些有趣的屬性:
- 組件不僅僅只是函數(shù)。React 能夠用在樹中與組件本身緊密相連的局部狀態(tài)等特性來增強(qiáng)組件功能。優(yōu)秀的運(yùn)行時(shí)提供了與當(dāng)前問題相匹配的基本抽象。就像我們已經(jīng)提到過的,React 專門針對(duì)于那些渲染 UI 樹并且能夠響應(yīng)交互的應(yīng)用。如果你直接調(diào)用了組件,你就只能自己來構(gòu)建這些特性了。
- 組件類型參與協(xié)調(diào)。通過 React 來調(diào)用你的組件,能讓它了解更多關(guān)于元素樹的結(jié)構(gòu)。例如,當(dāng)你從渲染
頁(yè)面轉(zhuǎn)到Profile頁(yè)面,React 不會(huì)嘗試重用其中的宿主實(shí)例 — 就像你用替換掉一樣。所有的狀態(tài)都會(huì)丟失 — 對(duì)于渲染完全不同的視圖時(shí),通常來說這是一件好事。你不會(huì)想要在和之間保留輸入框的狀態(tài)盡管的位置意外地“排列”在它們之間。 - React 能夠推遲協(xié)調(diào)。如果讓 React 控制調(diào)用你的組件,它能做很多有趣的事情。例如,它可以讓瀏覽器在組件調(diào)用之間做一些工作,這樣重渲染大體量的組件樹時(shí)就不會(huì)阻塞主線程。想要手動(dòng)編排這個(gè)過程而不依賴 React 的話將會(huì)十分困難。
- 更好的可調(diào)試性。如果組件是庫(kù)中所重視的一等公民,我們就可以構(gòu)建豐富的開發(fā)者工具,用于開發(fā)中的自省。
讓 React 調(diào)用你的組件函數(shù)還有***一個(gè)好處就是惰性求值。讓我們看看它是什么意思。
惰性求值
當(dāng)我們?cè)?JavaScript 中調(diào)用函數(shù)時(shí),參數(shù)往往在函數(shù)調(diào)用之前被執(zhí)行。
- // (2) 它會(huì)作為第二個(gè)計(jì)算
- eat(
- // (1) 它會(huì)首先計(jì)算
- prepareMeal()
- );
這通常是 JavaScript 開發(fā)者所期望的因?yàn)?JavaScript 函數(shù)可能有隱含的副作用。如果我們調(diào)用了一個(gè)函數(shù),但直到它的結(jié)果不知怎地被“使用”后該函數(shù)仍沒有執(zhí)行,這會(huì)讓我們感到十分詫異。
但是,React 組件是相對(duì)純凈的。如果我們知道它的結(jié)果不會(huì)在屏幕上出現(xiàn),則完全沒有必要執(zhí)行它。
考慮下面這個(gè)含有
的組件:- function Story({ currentUser }) {
- // return {
- // type: Page,
- // props: {
- // user: currentUser,
- // children: { type: Comments, props: {} }
- // }
- // }
- return (
-
-
- );
- }
組件能夠在 中渲染傳遞給它的子項(xiàng):- function Page({ user, children }) {
- return (
-
- {children}
- );
- }
但是要是存在提前返回的情況呢?
- function Page({ user, children }) {
- if (!user.isLoggedIn) {
- return
Please log in
; - }
- return (
-
- {children}
- );
- }
如果我們像函數(shù)一樣調(diào)用
Commonts(),不管Page是否想渲染它們都會(huì)被立即執(zhí)行:- // {
- // type: Page,
- // props: {
- // children: Comments() // Always runs!
- // }
- // }
- {Comments()}
但是如果我們傳遞的是一個(gè) React 元素,我們不需要自己執(zhí)行 Comments :
- // {
- // type: Page,
- // props: {
- // children: { type: Comments }
- // }
- // }
-
讓 React 來決定何時(shí)以及是否調(diào)用組件。如果我們的的
Page組件忽略自身的childrenprop 且相反地渲染了,React 不會(huì)嘗試去調(diào)用Please login
Comments函數(shù)。重點(diǎn)是什么?這很好,因?yàn)樗瓤梢宰屛覀儽苊獠槐匾匿秩疽材苁刮覀兊拇a變得不那么脆弱。(當(dāng)用戶退出登錄時(shí),我們并不在乎
Comments是否被丟棄 — 因?yàn)樗鼜臎]有被調(diào)用過。)狀態(tài)
我們先前提到過關(guān)于協(xié)調(diào)和在樹中元素概念上的“位置”是如何讓 React 知曉是該重用宿主實(shí)例還是該重建它。宿主實(shí)例能夠擁有所有相關(guān)的局部狀態(tài):focus、selection、input 等等。我們想要在渲染更新概念上相同的 UI 時(shí)保留這些狀態(tài)。我們也想可預(yù)測(cè)性地摧毀它們,當(dāng)我們?cè)诟拍钌箱秩镜氖峭耆煌臇|西時(shí)( 例如從
轉(zhuǎn)換到)。局部狀態(tài)是如此有用,以至于 React 讓你的組件也能擁有它。 組件仍然是函數(shù)但是 React 用對(duì)構(gòu)建 UI 有好處的許多特性增強(qiáng)了它。在樹中每個(gè)組件所綁定的局部狀態(tài)就是這些特性之一。
我們把這些特性叫做 Hooks 。例如,useState 就是一個(gè) Hook 。
- function Example() {
- const [count, setCount] = useState(0);
- return (
-
You clicked {count} times
- Click me
- );
- }
它返回一對(duì)值:當(dāng)前的狀態(tài)和更新該狀態(tài)的函數(shù)。
數(shù)組的解構(gòu)語(yǔ)法讓我們可以給狀態(tài)變量自定義名稱。例如,我在這里稱它們?yōu)?
count和setCount,但是它們也可以被稱作banana和setBanana。在這些文字之下,我們會(huì)用setState來替代第二個(gè)值無論它在具體的例子中被稱作什么。(你能在 React 文檔 中學(xué)習(xí)到更多關(guān)于 useState 和 其他 Hooks 的知識(shí)。)
一致性
即使我們想將協(xié)調(diào)過程本身分割成非阻塞的工作塊,我們?nèi)匀恍枰谕降难h(huán)中對(duì)真實(shí)的宿主實(shí)例進(jìn)行操作。這樣我們才能保證用戶不會(huì)看見半更新狀態(tài)的 UI ,瀏覽器也不會(huì)對(duì)用戶不應(yīng)看到的中間狀態(tài)進(jìn)行不必要的布局和樣式的重新計(jì)算。
這也是為什么 React 將所有的工作分成了”渲染階段“和”提交階段“的原因。渲染階段 是當(dāng) React 調(diào)用你的組件然后進(jìn)行協(xié)調(diào)的時(shí)段。在此階段進(jìn)行干涉是安全的且在未來這個(gè)階段將會(huì)變成異步的。提交階段 就是 React 操作宿主樹的時(shí)候。而這個(gè)階段永遠(yuǎn)是同步的。
緩存
當(dāng)父組件通過 setState 準(zhǔn)備更新時(shí),React 默認(rèn)會(huì)協(xié)調(diào)整個(gè)子樹。因?yàn)?React 并不知道在父組件中的更新是否會(huì)影響到其子代,所以 React 默認(rèn)保持一致性。這聽起來會(huì)有很大的性能消耗但事實(shí)上對(duì)于小型和中型的子樹來說,這并不是問題。
當(dāng)樹的深度和廣度達(dá)到一定程度時(shí),你可以讓 React 去緩存子樹并且重用先前的渲染結(jié)果當(dāng) prop 在淺比較之后是相同時(shí):
- function Row({ item }) {
- // ...
- }
- export default React.memo(Row);
現(xiàn)在,在父組件 中調(diào)用 setState 時(shí)如果 中的 item 與先前渲染的結(jié)果是相同的,React 就會(huì)直接跳過協(xié)調(diào)的過程。
你可以通過 useMemo() Hook 獲得單個(gè)表達(dá)式級(jí)別的細(xì)粒度緩存。該緩存于其相關(guān)的組件緊密聯(lián)系在一起,并且將與局部狀態(tài)一起被銷毀。它只會(huì)保留***一次計(jì)算的結(jié)果。
默認(rèn)情況下,React 不會(huì)故意緩存組件。許多組件在更新的過程中總是會(huì)接收到不同的 props ,所以對(duì)它們進(jìn)行緩存只會(huì)造成凈虧損。
原始模型
令人諷刺地是,React 并沒有使用“反應(yīng)式”的系統(tǒng)來支持細(xì)粒度的更新。換句話說,任何在頂層的更新只會(huì)觸發(fā)協(xié)調(diào)而不是局部更新那些受影響的組件。
這樣的設(shè)計(jì)是有意而為之的。對(duì)于 web 應(yīng)用來說交互時(shí)間是一個(gè)關(guān)鍵指標(biāo),而通過遍歷整個(gè)模型去設(shè)置細(xì)粒度的監(jiān)聽器只會(huì)浪費(fèi)寶貴的時(shí)間。此外,在很多應(yīng)用中交互往往會(huì)導(dǎo)致或小(按鈕懸停)或大(頁(yè)面轉(zhuǎn)換)的更新,因此細(xì)粒度的訂閱只會(huì)浪費(fèi)內(nèi)存資源。
React 的設(shè)計(jì)原則之一就是它可以處理原始數(shù)據(jù)。如果你擁有從網(wǎng)絡(luò)請(qǐng)求中獲得的一組 JavaScript 對(duì)象,你可以將其直接交給組件而無需進(jìn)行預(yù)處理。沒有關(guān)于可以訪問哪些屬性的問題,或者當(dāng)結(jié)構(gòu)有所變化時(shí)造成的意外的性能缺損。React 渲染是 O(視圖大小) 而不是 O(模型大小) ,并且你可以通過 windowing 顯著地減少視圖大小。
有那么一些應(yīng)用細(xì)粒度訂閱對(duì)它們來說是有用的 — 例如股票代碼。這是一個(gè)極少見的例子,因?yàn)椤八械臇|西都需要在同一時(shí)間內(nèi)持續(xù)更新”。雖然命令式的方法能夠優(yōu)化此類代碼,但 React 并不適用于這種情況。同樣的,如果你想要解決該問題,你就得在 React 之上自己實(shí)現(xiàn)細(xì)粒度的訂閱。
注意,即使細(xì)粒度訂閱和“反應(yīng)式”系統(tǒng)也無法解決一些常見的性能問題。 例如,渲染一棵很深的樹(在每次頁(yè)面轉(zhuǎn)換的時(shí)候發(fā)生)而不阻塞瀏覽器。改變跟蹤并不會(huì)讓它變得更快 — 這樣只會(huì)讓其變得更慢因?yàn)槲覀儓?zhí)行了額外的訂閱工作。另一個(gè)問題是我們需要等待返回的數(shù)據(jù)在渲染視圖之前。在 React 中,我們用并發(fā)渲染來解決這些問題。
批量更新
一些組件也許想要更新狀態(tài)來響應(yīng)同一事件。下面這個(gè)例子是假設(shè)的,但是卻說明了一個(gè)常見的模式:
- function Parent() {
- let [count, setCount] = useState(0);
- return (
- setCount(count + 1)}>
- Parent clicked {count} times
- );
- }
- function Child() {
- let [count, setCount] = useState(0);
- return (
- Child clicked {count} times
- );
- }
當(dāng)事件被觸發(fā)時(shí),子組件的 onClick 首先被觸發(fā)(同時(shí)觸發(fā)了它的 setState )。然后父組件在它自己的 onClick 中調(diào)用 setState 。
如果 React 立即重渲染組件以響應(yīng) setState 調(diào)用,最終我們會(huì)重渲染子組件兩次:
- *** 進(jìn)入 React 瀏覽器 click 事件處理過程 ***
- Child (onClick)
- - setState
- - re-render Child // 不必要的重渲染Parent (onClick)
- - setState
- - re-render Parent
- - re-render Child
- *** 結(jié)束 React 瀏覽器 click 事件處理過程 ***
***次 Child 組件渲染是浪費(fèi)的。并且我們也不會(huì)讓 React 跳過 Child 的第二次渲染因?yàn)?Parent 可能會(huì)傳遞不同的數(shù)據(jù)由于其自身的狀態(tài)更新。
這就是為什么 React 會(huì)在組件內(nèi)所有事件觸發(fā)完成后再進(jìn)行批量更新的原因:
- *** 進(jìn)入 React 瀏覽器 click 事件處理過程 ***
- Child (onClick)
- - setState
- Parent (onClick)
- - setState
- *** Processing state updates ***
- - re-render Parent
- - re-render Child
- *** 結(jié)束 React 瀏覽器 click 事件處理過程 ***
組件內(nèi)調(diào)用 setState 并不會(huì)立即執(zhí)行重渲染。相反,React 會(huì)先觸發(fā)所有的事件處理器,然后再觸發(fā)一次重渲染以進(jìn)行所謂的批量更新。
批量更新雖然有用但可能會(huì)讓你感到驚訝如果你的代碼這樣寫:
- const [count, setCounter] = useState(0);
- function increment() {
- setCounter(count + 1);
- }
- function handleClick() {
- increment();
- increment();
- increment();
- }
如果我們將 count 初始值設(shè)為 0 ,上面的代碼只會(huì)代表三次 setCount(1) 調(diào)用。為了解決這個(gè)問題,我們給 setState 提供了一個(gè) “updater” 函數(shù)作為參數(shù):
- const [count, setCounter] = useState(0);
- function increment() {
- setCounter(c => c + 1);
- }
- function handleClick() {
- increment();
- increment();
- increment();
- }
React 會(huì)將 updater 函數(shù)放入隊(duì)列中,并在之后按順序執(zhí)行它們,最終 count 會(huì)被設(shè)置成 3 并作為一次重渲染的結(jié)果。
當(dāng)狀態(tài)邏輯變得更加復(fù)雜而不僅僅只是少數(shù)的 setState 調(diào)用時(shí),我建議你使用 useReducer Hook 來描述你的局部狀態(tài)。它就像 “updater” 的升級(jí)模式在這里你可以給每一次更新命名:
- const [counter, dispatch] = useReducer((state, action) => {
- if (action === 'increment') {
- return state + 1;
- } else {
- return state;
- }
- }, 0);
- function handleClick() {
- dispatch('increment');
- dispatch('increment');
- dispatch('increment');
- }
action 字段可以是任意值,盡管對(duì)象是常用的選擇。
調(diào)用樹
編程語(yǔ)言的運(yùn)行時(shí)往往有調(diào)用棧 。當(dāng)函數(shù) a() 調(diào)用 b() ,b() 又調(diào)用 c() 時(shí),在 JavaScript 引擎中會(huì)有像 [a, b, c] 這樣的數(shù)據(jù)結(jié)構(gòu)來“跟蹤”當(dāng)前的位置以及接下來要執(zhí)行的代碼。一旦 c 函數(shù)執(zhí)行完畢,它的調(diào)用棧幀就消失了!因?yàn)樗辉俦恍枰?。我們返回到函?shù) b 中。當(dāng)我們結(jié)束函數(shù) a 的執(zhí)行時(shí),調(diào)用棧就被清空。
當(dāng)然,React 以 JavaScript 運(yùn)行當(dāng)然也遵循 JavaScript 的規(guī)則。但是我們可以想象在 React 內(nèi)部有自己的調(diào)用棧用來記憶我們當(dāng)前正在渲染的組件,例如 [App, Page, Layout, Article /* 此刻的位置 */] 。
React 與通常意義上的編程語(yǔ)言進(jìn)行時(shí)不同因?yàn)樗槍?duì)于渲染 UI 樹,這些樹需要保持“活性”,這樣才能使我們與其進(jìn)行交互。在***次 ReactDOM.render() 出現(xiàn)之前,DOM 操作并不會(huì)執(zhí)行。
這也許是對(duì)隱喻的延伸,但我喜歡把 React 組件當(dāng)作 “調(diào)用樹” 而不是 “調(diào)用?!?。當(dāng)我們調(diào)用完 Article 組件,它的 React “調(diào)用樹” 幀并沒有被摧毀。我們需要將局部狀態(tài)保存以便映射到宿主實(shí)例的某個(gè)地方。
這些“調(diào)用樹”幀會(huì)隨它們的局部狀態(tài)和宿主實(shí)例一起被摧毀,但是只會(huì)在協(xié)調(diào)規(guī)則認(rèn)為這是必要的時(shí)候執(zhí)行。如果你曾經(jīng)讀過 React 源碼,你就會(huì)知道這些幀其實(shí)就是 Fibers) 。
Fibers 是局部狀態(tài)真正存在的地方。當(dāng)狀態(tài)被更新后,React 將其下面的 Fibers 標(biāo)記為需要進(jìn)行協(xié)調(diào),之后便會(huì)調(diào)用這些組件。
上下文
在 React 中,我們將數(shù)據(jù)作為 props 傳遞給其他組件。有些時(shí)候,大多數(shù)組件需要相同的東西 — 例如,當(dāng)前選中的可視主題。將它一層層地傳遞會(huì)變得十分麻煩。
在 React 中,我們通過 Context 解決這個(gè)問題。它就像組件的動(dòng)態(tài)范圍 ,能讓你從頂層傳遞數(shù)據(jù),并讓每個(gè)子組件在底部能夠讀取該值,當(dāng)值
網(wǎng)站標(biāo)題:從設(shè)計(jì)者的角度看React的工作原理
本文網(wǎng)址:http://fisionsoft.com.cn/article/djdgpoh.html


咨詢
建站咨詢
