日本综合一区二区|亚洲中文天堂综合|日韩欧美自拍一区|男女精品天堂一区|欧美自拍第6页亚洲成人精品一区|亚洲黄色天堂一区二区成人|超碰91偷拍第一页|日韩av夜夜嗨中文字幕|久久蜜综合视频官网|精美人妻一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務時間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
從設計者的角度看React的工作原理

很多教程都把 React 作為一個 UI 庫來引入。這是很有道理的,因為 React 本身就是一個 UI 庫。就像官網(wǎng)上所說的那樣。

目前成都創(chuàng)新互聯(lián)公司已為上千多家的企業(yè)提供了網(wǎng)站建設、域名、網(wǎng)站空間、成都網(wǎng)站托管、企業(yè)網(wǎng)站設計、蘄春網(wǎng)站維護等服務,公司將堅持客戶導向、應用為本的策略,正道將秉承"和諧、參與、激情"的文化,與客戶和合作伙伴齊心協(xié)力一起成長,共同發(fā)展。

我曾經(jīng)寫過關(guān)于構(gòu)建用戶界面中遇到的挑戰(zhàn)的文章。但是本文將會用另外一種方式來講述 React —— 因為它更像是一種編程運行時。

本文不會教你任何有關(guān)如何創(chuàng)建界面的技巧。 但是它可能會幫你更加深入地理解 React 編程模型。

? 注意:如果你還在學習 React ,請移步到官方文檔進行學習

本文將會非常深入 —— 所以不適合初學者閱讀。在本文中,我會從***原則的角度盡可能地闡述 React 編程模型。我不會解釋如何使用它 —— 而是講解它的工作原理。

本文面向有經(jīng)驗的程序員,還有使用過其他 UI 庫,但在項目中權(quán)衡利弊之后最終選擇了 React 的人,我希望它會對你有所幫助!

一些人用了很多年 React 卻從沒有考慮過接下來我要講述的主題。 這絕對是以程序員而不是以設計者的角度來看待 React。但我認為站在兩個不同的角度來重新認識 React 并沒有什么壞處。

廢話少說,讓我們開始深入理解 React 吧!

宿主樹

一些程序輸出數(shù)字。另一些程序輸出詩詞。不同的語言和它們的運行時通常會對特定的一組用例進行優(yōu)化, React 也不例外。

React 程序通常會輸出一個會隨時間變化的樹。 它有可能是 DOM 樹 、iOS 視圖層、PDF 原語 ,或者是 JSON 對象 。不過通常我們希望用它來展示 UI 。我們稱它為“宿主樹”,因為它往往是 React 之外宿主環(huán)境中的一部分 —— 就像 DOM 或 iOS 。宿主樹通常有它自己的命令式 API 。而 React 就是它上面的那一層。

所以 React 到底有什么用呢?非常抽象,它可以幫助你編寫可預測的,并且能夠操控復雜的宿主樹進而響應像用戶交互、網(wǎng)絡響應、定時器等外部事件的應用程序。

當一個專業(yè)的工具可以施加特定的約束,并且能從中獲益時,它就比一般的工具要好。React 就是這樣的典范,并且它堅持兩個原則:

  • 穩(wěn)定性。 宿主樹是相對穩(wěn)定的,大多數(shù)情況的更新并不會從根本上改變其整體結(jié)構(gòu)。如果應用程序每秒都會將其所有可交互的元素重新排列為完全不同的組合,那將會變得難以使用。那個按鈕去哪了?為什么我的屏幕在跳舞?
  • 通用性。 宿主樹可以被拆分為外觀和行為一致的 UI 模式(例如按鈕、列表和頭像)而不是隨機的形狀。

這些原則恰好適用于大多數(shù) UI 。 不過當輸出沒有穩(wěn)定的“模式”時 React 并不適用。例如,React 也許可以幫你寫一個 Twitter 客戶端,但對于一個 3D 管道屏幕保護程序并沒有太大用處。

宿主實例

宿主樹由節(jié)點組成,我們稱之為“宿主實例”。

在 DOM 環(huán)境中,宿主實例就是我們通常所說的 DOM 節(jié)點 —— 就像當你調(diào)用 document.createElement('div') 時獲得的對象。在 iOS 中,宿主實例可以是從 JavaScript 到原生視圖唯一標識的值。

宿主實例有它們自己的屬性(例如 domNode.className 或者 view.tintColor )。它們也有可能將其他的宿主實例作為子項。

(這和 React 沒有任何聯(lián)系 — 因為我在講述宿主環(huán)境。)

通常會有原生 API 用于操控這些宿主實例。例如,在 DOM 環(huán)境中會提供像 appendChild、 removeChild、 setAttribute 等一系列的 API 。在 React 應用中,通常你不會調(diào)用這些 API ,因為那是 React 的工作。

渲染器

渲染器告訴 React 如何與特定的宿主環(huán)境通信,以及如何管理它的宿主實例。React DOM、React Native 甚至 Ink 都可以被稱作 React 渲染器。你也可以創(chuàng)建自己的 React 渲染器 。

React 渲染器能以下面兩種模式之一進行工作。

絕大多數(shù)渲染器都被用作“突變”模式。這種模式正是 DOM 的工作方式:我們可以創(chuàng)建一個節(jié)點,設置它的屬性,在之后往里面增加或者刪除子節(jié)點。宿主實例是完全可變的。

但 React 也能以”不變“模式工作。這種模式適用于那些并不提供像 appendChild 的 API 而是克隆雙親樹并始終替換掉***子樹的宿主環(huán)境。在宿主樹級別上的不可變性使得多線程變得更加容易。React Fabric 就利用了這一模式。

作為 React 的使用者,你永遠不需要考慮這些模式。我只想強調(diào) React 不僅僅只是從一種模式轉(zhuǎn)換到另一種模式的適配器。它的用處在于以一種更好的方式操控宿主實例而不用在意那些低級視圖 API 范例。

React 元素

在宿主環(huán)境中,一個宿主實例(例如 DOM 節(jié)點)是最小的構(gòu)建單元。而在 React 中,最小的構(gòu)建單元是 React 元素。

React 元素是一個普通的 JavaScript 對象。它用來描述一個宿主實例。

 
 
 
  1. // JSX 是用來描述這些對象的語法糖。
  2. // 
  3. {
  4.   type: 'button',
  5.   props: { className: 'blue' }

React 元素是輕量級的,因為沒有任何宿主實例與它綁定在一起。同樣,它只是對你想要在屏幕上看到的內(nèi)容的描述。

就像宿主實例一樣,React 元素也能形成一棵樹:

 
 
 
  1. // JSX 是用來描述這些對象的語法糖。
  2. // 
  3. //   
  4. //   
  5. // 
  6. {
  7.   type: 'dialog',
  8.   props: {
  9.     children: [{
  10.       type: 'button',
  11.       props: { className: 'blue' }
  12.     }, {
  13.       type: 'button',
  14.       props: { className: 'red' }
  15.     }]
  16.   }

(注意:我省略了一些對此解釋不重要的屬性)

但是請記住 React 元素并不是永遠存在的 。它們總是在重建和刪除之間不斷循環(huán)。

React 元素具有不可變性。例如你不能改變 React 元素中的子元素或者屬性。如果你想要在稍后渲染一些不同的東西,需要從頭創(chuàng)建新的 React 元素樹來描述它。

我喜歡將 React 元素比作電影中放映的每一幀。它們捕捉 UI 在特定的時間點的樣子。它們永遠不會再改變。

入口

每一個 React 渲染器都有一個“入口”。正是那個特定的 API 讓我們告訴 React ,將特定的 React 元素樹渲染到真正的宿主實例中去。

例如,React DOM 的入口就是 ReactDOM.render :

 
 
 
  1. ReactDOM.render(
  2.   // { type: 'button', props: { className: 'blue' } }
  3.   ,
  4.   document.getElementById('container')
  5. ); 

當我們調(diào)用 ReactDOM.render(reactElement, domContainer) 時,我們的意思是:“親愛的 React ,將我的 reactElement 映射到 domContaienr 的宿主樹上去吧?!?/p>

React 會查看 reactElement.type (在我們的例子中是 button )然后告訴 React DOM 渲染器創(chuàng)建對應的宿主實例并設置正確的屬性:

 
 
 
  1. // 在 ReactDOM 渲染器內(nèi)部(簡化版)
  2. function createHostInstance(reactElement) {
  3.   let domNode = document.createElement(reactElement.type);  
  4.   domNode.className = reactElement.props.className;
  5.   return domNode;

在我們的例子中,React 會這樣做:

 
 
 
  1. let domNode = document.createElement('button');
  2. domNode.className = 'blue';
  3. domContainer.appendChild(domNode); 

如果 React 元素在 reactElement.props.children 中含有子元素,React 會在***次渲染中遞歸地為它們創(chuàng)建宿主實例。

協(xié)調(diào)

如果我們用同一個 container 調(diào)用 ReactDOM.render() 兩次會發(fā)生什么呢?

 
 
 
  1. ReactDOM.render(
  2.   ,  document.getElementById('container')
  3. );
  4. // ... 之后 ...
  5. // 應該替換掉 button 宿主實例嗎?
  6. // 還是在已有的 button 上更新屬性?
  7. ReactDOM.render(
  8.   ,  document.getElementById('container')
  9. ); 

同樣,React 的工作是將 React 元素樹映射到宿主樹上去。確定該對宿主實例做什么來響應新的信息有時候叫做協(xié)調(diào) 。

有兩種方法可以解決它。簡化版的 React 會丟棄已經(jīng)存在的樹然后從頭開始創(chuàng)建它:

 
 
 
  1. let domContainer = document.getElementById('container');
  2. // 清除掉原來的樹
  3. domContainer.innerHTML = '';
  4. // 創(chuàng)建新的宿主實例樹
  5. let domNode = document.createElement('button');
  6. domNode.className = 'red';
  7. domContainer.appendChild(domNode); 

但是在 DOM 環(huán)境下,這樣的做法效率很低,而且會丟失 focus、selection、scroll 等許多狀態(tài)。相反,我們希望 React 這樣做:

 
 
 
  1. let domNode = domContainer.firstChild;
  2. // 更新已有的宿主實例
  3. domNode.className = 'red'; 

換句話說,React 需要決定何時更新一個已有的宿主實例來匹配新的 React 元素,何時該重新創(chuàng)建新的宿主實例。

這就引出了一個識別問題。React 元素可能每次都不相同,到底什么時候才該從概念上引用同一個宿主實例呢?

在我們的例子中,它很簡單。我們之前渲染了

這與 React 如何思考并解決這類問題已經(jīng)很接近了。

如果相同的元素類型在同一個地方先后出現(xiàn)兩次,React 會重用已有的宿主實例。

這里有一個例子,其中的注釋大致解釋了 React 是如何工作的:

 
 
 
  1. // let domNode = document.createElement('button');
  2. // domNode.className = 'blue';
  3. // domContainer.appendChild(domNode);
  4. ReactDOM.render(
  5.   ,
  6.   document.getElementById('container')
  7. );
  8. // 能重用宿主實例嗎?能!(button → button)
  9. // domNode.className = 'red';
  10. ReactDOM.render(
  11.   ,
  12.   document.getElementById('container')
  13. );
  14. // 能重用宿主實例嗎?不能!(button → p)
  15. // domContainer.removeChild(domNode);
  16. // domNode = document.createElement('p');
  17. // domNode.textContent = 'Hello';
  18. // domContainer.appendChild(domNode);
  19. ReactDOM.render(
  20.   

    Hello

    ,
  21.   document.getElementById('container')
  22. );
  23. // 能重用宿主實例嗎?能!(p → p)
  24. // domNode.textContent = 'Goodbye';
  25. ReactDOM.render(
  26.   

    Goodbye

    ,
  27.   document.getElementById('container')
  28. ); 

同樣的啟發(fā)式方法也適用于子樹。例如,當我們在

中新增兩個 ,
  •   domContainer
  • );
  • // 下一次渲染
  • ReactDOM.render(
  •   
  •     

    I was just added here!

        
  •       
  •   ,
  •   domContainer
  • ); 
  • 在這個例子中, 宿主實例會被重新創(chuàng)建。React 會遍歷整個元素樹,并將其與先前的版本進行比較:

    • dialog → dialog :能重用宿主實例嗎?能 — 因為類型是匹配的。
      • input → p :能重用宿主實例嗎?不能,類型改變了! 需要刪除已有的 input 然后重新創(chuàng)建一個 p 宿主實例。
      • (nothing) → input :需要重新創(chuàng)建一個 input 宿主實例。

    因此,React 會像這樣執(zhí)行更新:

     
     
     
    1. let oldInputNode = dialogNode.firstChild;
    2. dialogNode.removeChild(oldInputNode);
    3. let pNode = document.createElement('p');
    4. pNode.textContent = 'I was just added here!';
    5. dialogNode.appendChild(pNode);
    6. let newInputNode = document.createElement('input');
    7. dialogNode.appendChild(newInputNode); 

    這樣的做法并不科學因為事實上 并沒有被

    所替代 — 它只是移動了位置而已。我們不希望因為重建 DOM 而丟失了 selection、focus 等狀態(tài)以及其中的內(nèi)容。

    所替代 — 它只是移動了位置而已。我們不希望因為重建 DOM 而丟失了 selection、focus 等狀態(tài)以及其中的內(nèi)容。

    雖然這個問題很容易解決(在下面我會馬上講到),但這個問題在 React 應用中并不常見。而當我們探討為什么會這樣時卻很有意思。

    事實上,你很少會直接調(diào)用 ReactDOM.render 。相反,在 React 應用中程序往往會被拆分成這樣的函數(shù):

     
     
     
    1. function Form({ showMessage }) {
    2.   let message = null;
    3.   if (showMessage) {
    4.     message = 

      I was just added here!

      ;
    5.   }
    6.   return (
    7.     
    8.       {message}
    9.       
    10.     
    11.   );

    這個例子并不會遇到剛剛我們所描述的問題。讓我們用對象注釋而不是 JSX 也許可以更好地理解其中的原因。來看一下 dialog 中的子元素樹:

     
     
     
    1. function Form({ showMessage }) {
    2.   let message = null;
    3.   if (showMessage) {
    4.     message = {
    5.       type: 'p',
    6.       props: { children: 'I was just added here!' }
    7.     };
    8.   }
    9.   return {
    10.     type: 'dialog',
    11.     props: {
    12.       children: [
    13.         message,
    14.         { type: 'input', props: {} }
    15.       ]
    16.     }
    17.   };
    18. }

    不管 showMessage 是 true 還是 false ,在渲染的過程中 總是在第二個孩子的位置且不會改變。

    如果 showMessage 從 false 改變?yōu)?true ,React 會遍歷整個元素樹,并與之前的版本進行比較:

    • dialog → dialog :能夠重用宿主實例嗎?能 — 因為類型匹配。
      • (null) → p :需要插入一個新的 p 宿主實例。
      • input → input :能夠重用宿主實例嗎?能 — 因為類型匹配。

    之后 React 大致會像這樣執(zhí)行代碼:

     
     
     
    1. let inputNode = dialogNode.firstChild; 
    2. let pNode = document.createElement('p'); 
    3. pNode.textContent = 'I was just added here!'; 
    4. dialogNode.insertBefore(pNode, inputNode); 

    這樣一來輸入框中的狀態(tài)就不會丟失了。

    列表

    比較樹中同一位置的元素類型對于是否該重用還是重建相應的宿主實例往往已經(jīng)足夠。

    但這只適用于當子元素是靜止的并且不會重排序的情況。在上面的例子中,即使 message 不存在,我們?nèi)匀恢垒斎肟蛟谙⒅?,并且再沒有其他的子元素。

    而當遇到動態(tài)列表時,我們不能確定其中的順序總是一成不變的。

     
     
     
    1. function ShoppingList({ list }) {
    2.   return (
    3.     
    4.       {list.map(item => (
    5.         

    6.           You bought {item.name}
    7.           
    8.           Enter how many do you want: 
    9.         

    10.       ))}
    11.     
    12.   )

    如果我們的商品列表被重新排序了,React 只會看到所有的 p 以及里面的 input 擁有相同的類型,并不知道該如何移動它們。(在 React 看來,雖然這些商品本身改變了,但是它們的順序并沒有改變。)

    所以 React 會對這十個商品進行類似如下的重排序:

     
     
     
    1. for (let i = 0; i < 10; i++) {
    2.   let pNode = formNode.childNodes[i];
    3.   let textNode = pNode.firstChild;
    4.   textNode.textContent = 'You bought ' + items[i].name;

    React 只會對其中的每個元素進行更新而不是將其重新排序。這樣做會造成性能上的問題和潛在的 bug 。例如,當商品列表的順序改變時,原本在***個輸入框的內(nèi)容仍然會存在于現(xiàn)在的***個輸入框中 — 盡管事實上在商品列表里它應該代表著其他的商品!

    這就是為什么每次當輸出中包含元素數(shù)組時,React 都會讓你指定一個叫做 key 的屬性:

     
     
     
    1. function ShoppingList({ list }) {
    2.   return (
    3.     
    4.       {list.map(item => (
    5.         
    6.           You bought {item.name}
    7.           
    8.           Enter how many do you want: 
    9.         

    10.       ))}
    11.     
    12.   )

    key 給予 React 判斷子元素是否真正相同的能力,即使在渲染前后它在父元素中的位置不是相同的。

    當 React 在

    中發(fā)現(xiàn)

    ,它就會檢查之前版本中的 是否同樣含有

    。即使 中的子元素們改變位置后,這個方法同樣有效。在渲染前后當 key 仍然相同時,React 會重用先前的宿主實例,然后重新排序其兄弟元素。

    需要注意的是 key 只與特定的父親 React 元素相關(guān)聯(lián),比如 。React 并不會去匹配父元素不同但 key 相同的子元素。(React 并沒有慣用的支持對在不重新創(chuàng)建元素的情況下讓宿主實例在不同的父元素之間移動。)

    給 key 賦予什么值***呢?***的答案就是:什么時候你會說一個元素不會改變即使它在父元素中的順序被改變? 例如,在我們的商品列表中,商品本身的 ID 是區(qū)別于其他商品的唯一標識,那么它就最適合作為 key 。

    組件

    我們已經(jīng)知道函數(shù)會返回 React 元素:

      
     
     
    1. function Form({ showMessage }) {
    2.   let message = null;
    3.   if (showMessage) {
    4.     message = 

      I was just added here!

      ;
    5.   }
    6.   return (
    7.     
    8.       {message}
    9.       
    10.     
    11.   );

    這些函數(shù)被叫做組件。它們讓我們可以打造自己的“工具箱”,例如按鈕、頭像、評論框等等。組件就像 React 的面包和黃油。

    組件接受一個參數(shù) — 對象哈希。它包含“props”(“屬性”的簡稱)。在這里 showMessage 就是一個 prop 。它們就像是具名參數(shù)一樣。

    純凈

    React 組件中對于 props 應該是純凈的。

      
     
     
    1. function Button(props) {
    2.   //  沒有作用
    3.   props.isActive = true;

    通常來說,突變在 React 中不是慣用的。(我們會在之后講解如何用更慣用的方式來更新 UI 以響應事件。)

    不過,局部的突變是絕對允許的:

      
     
     
    1. function FriendList({ friends }) {
    2.   let items = [];
    3.   for (let i = 0; i < friends.length; i++) {
    4.     let friend = friends[i];
    5.     items.push(
    6.       
    7.     );
    8.   }
    9.   return 
      {items}
      ;
    10. }

    當我們在函數(shù)組件內(nèi)部創(chuàng)建 items 時不管怎樣改變它都行,只要這些突變發(fā)生在將其作為***的渲染結(jié)果之前。所以并不需要重寫你的代碼來避免局部突變。

    同樣地,惰性初始化是被允許的即使它不是完全“純凈”的:

      
     
     
    1. function ExpenseForm() {
    2.   // 只要不影響其他組件這是被允許的:
    3.   SuperCalculator.initializeIfNotReady();
    4.   // 繼續(xù)渲染......
    5. }

    只要調(diào)用組件多次是安全的,并且不會影響其他組件的渲染,React 并不關(guān)心你的代碼是否像嚴格的函數(shù)式編程一樣***純凈。在 React 中,冪等性比純凈性更加重要。

    也就是說,在 React 組件中不允許有用戶可以直接看到的副作用。換句話說,僅調(diào)用函數(shù)式組件時不應該在屏幕上產(chǎn)生任何變化。

    遞歸

    我們該如何在組件中使用組件?組件屬于函數(shù)因此我們可以直接進行調(diào)用:

      
     
     
    1. let reactElement = Form({ showMessage: true }); 
    2. ReactDOM.render(reactElement, domContainer); 

    然而,在 React 運行時中這并不是慣用的使用組件的方式。

    相反,使用組件慣用的方式與我們已經(jīng)了解的機制相同 — 即 React 元素。這意味著不需要你直接調(diào)用組件函數(shù),React 會在之后為你做這件事情:

      
     
     
    1. // { type: Form, props: { showMessage: true } }
    2. let reactElement = ;
    3. ReactDOM.render(reactElement, domContainer);

    然后在 React 內(nèi)部,你的組件會這樣被調(diào)用:

      
     
     
    1. // React 內(nèi)部的某個地方
    2. let type = reactElement.type; // Form
    3. let props = reactElement.props; // { showMessage: true }
    4. let result = type(props); // 無論 Form 會返回什么

    組件函數(shù)名稱按照規(guī)定需要大寫。當 JSX 轉(zhuǎn)換時看見 而不是 ,它讓對象 type 本身成為標識符而不是字符串:

      
     
     
    1. console.log(.type); // 'form' 字符串 
    2. console.log(.type); // Form 函數(shù) 

    我們并沒有全局的注冊機制 — 字面上當我們輸入 時代表著 Form 。如果 Form在局部作用域中并不存在,你會發(fā)現(xiàn)一個 JavaScript 錯誤,就像平常你使用錯誤的變量名稱一樣。

    因此,當元素類型是一個函數(shù)的時候 React 會做什么呢?它會調(diào)用你的組件,然后詢問組件想要渲染什么元素。

    這個步驟會遞歸式地執(zhí)行下去,更詳細的描述在這里 ??偟膩碚f,它會像這樣執(zhí)行:

    • 你:ReactDOM.render(, domContainer)
    • React:App ,你想要渲染什么?

      • App :我要渲染包含
    • React: ,你要渲染什么?

      • Layout :我要在
        中渲染我的子元素。我的子元素是 所以我猜它應該渲染到
        中去。
    • React: ,你要渲染什么?

      • :我要在
        中渲染一些文本和
        。
    • React:

      ,你要渲染什么?

      • :我要渲染含有文本的
        。
    • React:好的,讓我們開始吧:
              
     
     
    1. // 最終的 DOM 結(jié)構(gòu)
    2.   
    3.     Some text
    4.     
      some more text
    5.   
     

    這就是為什么我們說協(xié)調(diào)是遞歸式的。當 React 遍歷整個元素樹時,可能會遇到元素的 type 是一個組件。React 會調(diào)用它然后繼續(xù)沿著返回的 React 元素下行。最終我們會調(diào)用完所有的組件,然后 React 就會知道該如何改變宿主樹。

    在之前已經(jīng)討論過的相同的協(xié)調(diào)準則,在這一樣適用。如果在同一位置的 type 改變了(由索引和可選的 key 決定),React 會刪除其中的宿主實例并將其重建。

    控制反轉(zhuǎn)

    你也許會好奇:為什么我們不直接調(diào)用組件?為什么要編寫 而不是 Form()?

    React 能夠做的更好如果它“知曉”你的組件而不是在你遞歸調(diào)用它們之后生成的 React 元素樹。

              
     
     
    1. //  React 并不知道 Layout 和 Article 的存在。
    2. // 因為你在調(diào)用它們。
    3. ReactDOM.render(
    4.   Layout({ children: Article() }),
    5.   domContainer
    6. )
    7. //  React知道 Layout 和 Article 的存在。
    8. // React 來調(diào)用它們。
    9. ReactDOM.render(
    10.   ,
    11.   domContainer
    12. )

     這是一個關(guān)于控制反轉(zhuǎn)的經(jīng)典案例。通過讓 React 調(diào)用我們的組件,我們會獲得一些有趣的屬性:

    • 組件不僅僅只是函數(shù)。React 能夠用在樹中與組件本身緊密相連的局部狀態(tài)等特性來增強組件功能。優(yōu)秀的運行時提供了與當前問題相匹配的基本抽象。就像我們已經(jīng)提到過的,React 專門針對于那些渲染 UI 樹并且能夠響應交互的應用。如果你直接調(diào)用了組件,你就只能自己來構(gòu)建這些特性了。
    • 組件類型參與協(xié)調(diào)。通過 React 來調(diào)用你的組件,能讓它了解更多關(guān)于元素樹的結(jié)構(gòu)。例如,當你從渲染 頁面轉(zhuǎn)到 Profile 頁面,React 不會嘗試重用其中的宿主實例 — 就像你用

      替換掉

    • React 能夠推遲協(xié)調(diào)。如果讓 React 控制調(diào)用你的組件,它能做很多有趣的事情。例如,它可以讓瀏覽器在組件調(diào)用之間做一些工作,這樣重渲染大體量的組件樹時就不會阻塞主線程。想要手動編排這個過程而不依賴 React 的話將會十分困難。
    • 更好的可調(diào)試性。如果組件是庫中所重視的一等公民,我們就可以構(gòu)建豐富的開發(fā)者工具,用于開發(fā)中的自省。

    讓 React 調(diào)用你的組件函數(shù)還有***一個好處就是惰性求值。讓我們看看它是什么意思。

    惰性求值

    當我們在 JavaScript 中調(diào)用函數(shù)時,參數(shù)往往在函數(shù)調(diào)用之前被執(zhí)行。

     
     
     
    1. // (2) 它會作為第二個計算
    2. eat(
    3.   // (1) 它會首先計算
    4.   prepareMeal()
    5. );

    這通常是 JavaScript 開發(fā)者所期望的因為 JavaScript 函數(shù)可能有隱含的副作用。如果我們調(diào)用了一個函數(shù),但直到它的結(jié)果不知怎地被“使用”后該函數(shù)仍沒有執(zhí)行,這會讓我們感到十分詫異。

    但是,React 組件是相對純凈的。如果我們知道它的結(jié)果不會在屏幕上出現(xiàn),則完全沒有必要執(zhí)行它。

    考慮下面這個含有 組件:

     
     
     
    1. function Story({ currentUser }) {
    2.   // return {
    3.   //   type: Page,
    4.   //   props: {
    5.   //     user: currentUser,
    6.   //     children: { type: Comments, props: {} }
    7.   //   }
    8.   // }
    9.   return (
    10.     
    11.       
    12.     
    13.   );
    14. }

    組件能夠在 中渲染傳遞給它的子項:

     
     
     
    1. function Page({ user, children }) {
    2.   return (
    3.     
    4.       {children}
    5.     
    6.   );
    7. }

     (在 JSX 中 } /> 相同。)

    但是要是存在提前返回的情況呢?

     
     
     
    1. function Page({ user, children }) {
    2.   if (!user.isLoggedIn) {
    3.     return 

      Please log in

      ;
    4.   }
    5.   return (
    6.     
    7.       {children}
    8.     
    9.   );
    10. }

    如果我們像函數(shù)一樣調(diào)用 Commonts() ,不管 Page 是否想渲染它們都會被立即執(zhí)行:

     
     
     
    1. // {
    2. //   type: Page,
    3. //   props: {
    4. //     children: Comments() // Always runs!
    5. //   }
    6. // }
    7.   {Comments()}
    8.  

    但是如果我們傳遞的是一個 React 元素,我們不需要自己執(zhí)行 Comments :

     
     
     
    1. // {
    2. //   type: Page,
    3. //   props: {
    4. //     children: { type: Comments }
    5. //   }
    6. // }
    7.   
    8.  

    讓 React 來決定何時以及是否調(diào)用組件。如果我們的的 Page 組件忽略自身的 childrenprop 且相反地渲染了

    Please login

    ,React 不會嘗試去調(diào)用 Comments 函數(shù)。重點是什么?

    這很好,因為它既可以讓我們避免不必要的渲染也能使我們的代碼變得不那么脆弱。(當用戶退出登錄時,我們并不在乎 Comments 是否被丟棄 — 因為它從沒有被調(diào)用過。)

    狀態(tài)

    我們先前提到過關(guān)于協(xié)調(diào)和在樹中元素概念上的“位置”是如何讓 React 知曉是該重用宿主實例還是該重建它。宿主實例能夠擁有所有相關(guān)的局部狀態(tài):focus、selection、input 等等。我們想要在渲染更新概念上相同的 UI 時保留這些狀態(tài)。我們也想可預測性地摧毀它們,當我們在概念上渲染的是完全不同的東西時( 例如從 轉(zhuǎn)換到   )。

    局部狀態(tài)是如此有用,以至于 React 讓你的組件也能擁有它。 組件仍然是函數(shù)但是 React 用對構(gòu)建 UI 有好處的許多特性增強了它。在樹中每個組件所綁定的局部狀態(tài)就是這些特性之一。

    我們把這些特性叫做 Hooks 。例如,useState 就是一個 Hook 。

     
     
     
    1. function Example() {
    2.   const [count, setCount] = useState(0);
    3.   return (
    4.     
    5.       

      You clicked {count} times

    6.        setCount(count + 1)}>
    7.         Click me
    8.       
    9.     
  •   );
  • 它返回一對值:當前的狀態(tài)和更新該狀態(tài)的函數(shù)。

    數(shù)組的解構(gòu)語法讓我們可以給狀態(tài)變量自定義名稱。例如,我在這里稱它們?yōu)?countsetCount ,但是它們也可以被稱作 bananasetBanana 。在這些文字之下,我們會用 setState 來替代第二個值無論它在具體的例子中被稱作什么。

    (你能在 React 文檔 中學習到更多關(guān)于 useState 和 其他 Hooks 的知識。)

    一致性 

    即使我們想將協(xié)調(diào)過程本身分割成非阻塞的工作塊,我們?nèi)匀恍枰谕降难h(huán)中對真實的宿主實例進行操作。這樣我們才能保證用戶不會看見半更新狀態(tài)的 UI ,瀏覽器也不會對用戶不應看到的中間狀態(tài)進行不必要的布局和樣式的重新計算。

    這也是為什么 React 將所有的工作分成了”渲染階段“和”提交階段“的原因。渲染階段 是當 React 調(diào)用你的組件然后進行協(xié)調(diào)的時段。在此階段進行干涉是安全的且在未來這個階段將會變成異步的。提交階段 就是 React 操作宿主樹的時候。而這個階段永遠是同步的。

    緩存

    當父組件通過 setState 準備更新時,React 默認會協(xié)調(diào)整個子樹。因為 React 并不知道在父組件中的更新是否會影響到其子代,所以 React 默認保持一致性。這聽起來會有很大的性能消耗但事實上對于小型和中型的子樹來說,這并不是問題。

    當樹的深度和廣度達到一定程度時,你可以讓 React 去緩存子樹并且重用先前的渲染結(jié)果當 prop 在淺比較之后是相同時:

     
     
     
    1. function Row({ item }) {
    2.   // ...
    3. }
    4. export default React.memo(Row); 

    現(xiàn)在,在父組件 中調(diào)用 setState 時如果 中的 item 與先前渲染的結(jié)果是相同的,React 就會直接跳過協(xié)調(diào)的過程。

    你可以通過 useMemo() Hook 獲得單個表達式級別的細粒度緩存。該緩存于其相關(guān)的組件緊密聯(lián)系在一起,并且將與局部狀態(tài)一起被銷毀。它只會保留***一次計算的結(jié)果。

    默認情況下,React 不會故意緩存組件。許多組件在更新的過程中總是會接收到不同的 props ,所以對它們進行緩存只會造成凈虧損。

    原始模型

    令人諷刺地是,React 并沒有使用“反應式”的系統(tǒng)來支持細粒度的更新。換句話說,任何在頂層的更新只會觸發(fā)協(xié)調(diào)而不是局部更新那些受影響的組件。

    這樣的設計是有意而為之的。對于 web 應用來說交互時間是一個關(guān)鍵指標,而通過遍歷整個模型去設置細粒度的監(jiān)聽器只會浪費寶貴的時間。此外,在很多應用中交互往往會導致或小(按鈕懸停)或大(頁面轉(zhuǎn)換)的更新,因此細粒度的訂閱只會浪費內(nèi)存資源。

    React 的設計原則之一就是它可以處理原始數(shù)據(jù)。如果你擁有從網(wǎng)絡請求中獲得的一組 JavaScript 對象,你可以將其直接交給組件而無需進行預處理。沒有關(guān)于可以訪問哪些屬性的問題,或者當結(jié)構(gòu)有所變化時造成的意外的性能缺損。React 渲染是 O(視圖大小) 而不是 O(模型大小) ,并且你可以通過 windowing 顯著地減少視圖大小。

    有那么一些應用細粒度訂閱對它們來說是有用的 — 例如股票代碼。這是一個極少見的例子,因為“所有的東西都需要在同一時間內(nèi)持續(xù)更新”。雖然命令式的方法能夠優(yōu)化此類代碼,但 React 并不適用于這種情況。同樣的,如果你想要解決該問題,你就得在 React 之上自己實現(xiàn)細粒度的訂閱。

    注意,即使細粒度訂閱和“反應式”系統(tǒng)也無法解決一些常見的性能問題。 例如,渲染一棵很深的樹(在每次頁面轉(zhuǎn)換的時候發(fā)生)而不阻塞瀏覽器。改變跟蹤并不會讓它變得更快 — 這樣只會讓其變得更慢因為我們執(zhí)行了額外的訂閱工作。另一個問題是我們需要等待返回的數(shù)據(jù)在渲染視圖之前。在 React 中,我們用并發(fā)渲染來解決這些問題。

    批量更新

    一些組件也許想要更新狀態(tài)來響應同一事件。下面這個例子是假設的,但是卻說明了一個常見的模式:

     
     
     
    1. function Parent() {
    2.   let [count, setCount] = useState(0);
    3.   return (
    4.      setCount(count + 1)}>
    5.       Parent clicked {count} times
    6.       
    7.     
  •   );
  • }
  • function Child() {
  •   let [count, setCount] = useState(0);
  •   return (
  •      setCount(count + 1)}>
  •       Child clicked {count} times
  •     
  •   );
  • 當事件被觸發(fā)時,子組件的 onClick 首先被觸發(fā)(同時觸發(fā)了它的 setState )。然后父組件在它自己的 onClick 中調(diào)用 setState 。

    如果 React 立即重渲染組件以響應 setState 調(diào)用,最終我們會重渲染子組件兩次:

     
     
     
    1. *** 進入 React 瀏覽器 click 事件處理過程 ***
    2. Child (onClick)
    3.   - setState
    4.   - re-render Child //  不必要的重渲染Parent (onClick)
    5.   - setState
    6.   - re-render Parent
    7.   - re-render Child
    8. *** 結(jié)束 React 瀏覽器 click 事件處理過程 *** 

    ***次 Child 組件渲染是浪費的。并且我們也不會讓 React 跳過 Child 的第二次渲染因為 Parent 可能會傳遞不同的數(shù)據(jù)由于其自身的狀態(tài)更新。

    這就是為什么 React 會在組件內(nèi)所有事件觸發(fā)完成后再進行批量更新的原因:

     
     
     
    1. *** 進入 React 瀏覽器 click 事件處理過程 ***
    2. Child (onClick)
    3.   - setState
    4. Parent (onClick)
    5.   - setState
    6. *** Processing state updates           ***
    7.   - re-render Parent
    8.   - re-render Child
    9. *** 結(jié)束 React 瀏覽器 click 事件處理過程  *** 

    組件內(nèi)調(diào)用 setState 并不會立即執(zhí)行重渲染。相反,React 會先觸發(fā)所有的事件處理器,然后再觸發(fā)一次重渲染以進行所謂的批量更新。

    批量更新雖然有用但可能會讓你感到驚訝如果你的代碼這樣寫:

     
     
     
    1. const [count, setCounter] = useState(0);
    2.   function increment() {
    3.     setCounter(count + 1);
    4.   }
    5.   function handleClick() {
    6.     increment();
    7.     increment();
    8.     increment();
    9.   } 

    如果我們將 count 初始值設為 0 ,上面的代碼只會代表三次 setCount(1) 調(diào)用。為了解決這個問題,我們給 setState 提供了一個 “updater” 函數(shù)作為參數(shù):

     
     
     
    1. const [count, setCounter] = useState(0);
    2.   function increment() {
    3.     setCounter(c => c + 1);
    4.   }
    5.   function handleClick() {
    6.     increment();
    7.     increment();
    8.     increment();
    9.   } 

    React 會將 updater 函數(shù)放入隊列中,并在之后按順序執(zhí)行它們,最終 count 會被設置成 3 并作為一次重渲染的結(jié)果。

    當狀態(tài)邏輯變得更加復雜而不僅僅只是少數(shù)的 setState 調(diào)用時,我建議你使用 useReducer Hook 來描述你的局部狀態(tài)。它就像 “updater” 的升級模式在這里你可以給每一次更新命名:

     
     
     
    1. const [counter, dispatch] = useReducer((state, action) => {
    2.     if (action === 'increment') {
    3.       return state + 1;
    4.     } else {
    5.       return state;
    6.     }
    7.   }, 0);
    8.   function handleClick() {
    9.     dispatch('increment');
    10.     dispatch('increment');
    11.     dispatch('increment');
    12.   } 

    action 字段可以是任意值,盡管對象是常用的選擇。

    調(diào)用樹

    編程語言的運行時往往有調(diào)用棧 。當函數(shù) a() 調(diào)用 b() ,b() 又調(diào)用 c() 時,在 JavaScript 引擎中會有像 [a, b, c] 這樣的數(shù)據(jù)結(jié)構(gòu)來“跟蹤”當前的位置以及接下來要執(zhí)行的代碼。一旦 c 函數(shù)執(zhí)行完畢,它的調(diào)用棧幀就消失了!因為它不再被需要了。我們返回到函數(shù) b 中。當我們結(jié)束函數(shù) a 的執(zhí)行時,調(diào)用棧就被清空。

    當然,React 以 JavaScript 運行當然也遵循 JavaScript 的規(guī)則。但是我們可以想象在 React 內(nèi)部有自己的調(diào)用棧用來記憶我們當前正在渲染的組件,例如 [App, Page, Layout, Article /* 此刻的位置 */] 。

    React 與通常意義上的編程語言進行時不同因為它針對于渲染 UI 樹,這些樹需要保持“活性”,這樣才能使我們與其進行交互。在***次 ReactDOM.render() 出現(xiàn)之前,DOM 操作并不會執(zhí)行。

    這也許是對隱喻的延伸,但我喜歡把 React 組件當作 “調(diào)用樹” 而不是 “調(diào)用?!?。當我們調(diào)用完 Article 組件,它的 React “調(diào)用樹” 幀并沒有被摧毀。我們需要將局部狀態(tài)保存以便映射到宿主實例的某個地方。

    這些“調(diào)用樹”幀會隨它們的局部狀態(tài)和宿主實例一起被摧毀,但是只會在協(xié)調(diào)規(guī)則認為這是必要的時候執(zhí)行。如果你曾經(jīng)讀過 React 源碼,你就會知道這些幀其實就是 Fibers) 。

    Fibers 是局部狀態(tài)真正存在的地方。當狀態(tài)被更新后,React 將其下面的 Fibers 標記為需要進行協(xié)調(diào),之后便會調(diào)用這些組件。

    上下文

    在 React 中,我們將數(shù)據(jù)作為 props 傳遞給其他組件。有些時候,大多數(shù)組件需要相同的東西 — 例如,當前選中的可視主題。將它一層層地傳遞會變得十分麻煩。

    在 React 中,我們通過 Context 解決這個問題。它就像組件的動態(tài)范圍 ,能讓你從頂層傳遞數(shù)據(jù),并讓每個子組件在底部能夠讀取該值,當值
    本文名稱:從設計者的角度看React的工作原理
    標題URL:
    http://www.dlmjj.cn/article/djdgpoh.html