国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

React diff原理探究以及應用實踐

EasonTyler / 1774人閱讀

摘要:但是加了一定要比沒加的性能更高嗎我們再來看一個例子現在有一集合渲染成如下的樣子現在我們將這個集合的順序打亂變成。不加操作修改第個到第個節點的如果我們對這個集合進行增刪的操作改成。

拋磚引玉

React通過引入Virtual DOM的概念,極大地避免無效的Dom操作,已使我們的頁面的構建效率提到了極大的提升。但是如何高效地通過對比新舊Virtual DOM來找出真正的Dom變化之處同樣也決定著頁面的性能,React用其特殊的diff算法解決這個問題。Virtual DOM+React diff的組合極大地保障了React的性能,使其在業界有著不錯的性能口碑。diff算法并非React首創,React只是對diff算法做了一個優化,但卻是因為這個優化,給React帶來了極大的性能提升,不禁讓人感嘆React創造者們的智慧!接下來我們就探究一下React的diff算法。

傳統diff算法

在文章開頭我們提到React的diff算法給React帶來了極大的性能提升,而之前的React diff算法是在傳統diff算法上的優化。下面我們先看一下傳統的diff算法是什么樣子的。

傳統diff算法通過循環遞歸對節點進行依次對比,效率低下,算法復雜度達到 O(n^3),其中 n 是樹中節點的總數。具體是怎么算出來的,可以查看知乎上的一個回答。

react的diff 從O(n^3)到 O(n) ,請問 O(n^3) 和O(n) 是怎么算出來?

O(n^3) 到底有多可怕呢?這意味著如果要展示 1000 個節點,就要依次執行上十億次 的比較,這種指數型的性能消耗對于前端渲染場景來說代價太高了。而React卻這個diff算法時間復雜度從O(n^3)降到O(n)。O(n^3)到O(n)的提升有多大,我們通過一張圖來看一下。

從上面這張圖來看,React的diff算法所帶來的提升無疑是巨大無比的。接下來我們再看一張圖:


從1979到2011,30多年的時間,才將時間復雜度搞到O(n^3),而React從開源到現在不過區區幾年的時間,卻一下子干到O(n),到這里不禁再次膜拜一下React的創造者們。
那么React這個牛逼的diff算法是如何做到的呢?

React diff原理

前面我們講到傳統diff算法的時間復雜度為O(n^3),其中n為樹中節點的總數,隨著n的增加,diff所耗費的時間將呈現爆炸性的增長。react卻利用其特殊的diff算法做到了O(n^3)到O(n)的飛躍性的提升,而完成這一壯舉的法寶就是下面這三條看似簡單的diff策略:

Web UI中DOM節點跨層級的移動操作特別少,可以忽略不計

擁有相同類的兩個組件將會生成相似的樹形結構,擁有不同類的兩個組件將會生成不同的樹形結構

對于同一層級的一組子節點,它們可以通過唯一 id 進行區分

在上面三個策略的基礎上,React 分別將對應的tree diff、component diff 以及 element diff 進行算法優化,極大地提升了diff效率。

tree diff

基于策略一,React 對樹的算法進行了簡潔明了的優化,即對樹進行分層比較,兩棵樹只會對同一層次的節點進行比較。

既然 DOM 節點跨層級的移動操作少到可以忽略不計,針對這一現象,React只會對相同層級的 DOM 節點進行比較,即同一個父節點下的所有子節點。當發現節點已經不存在時,則該節點及其子節點會被完全刪除掉,不會用于進一步的比較。這樣只需要對樹進行一次遍歷,便能完成整個 DOM 樹的比較。

策略一的前提是Web UI中DOM節點跨層級的移動操作特別少,但并沒有否定DOM節點跨層級的操作的存在,那么當遇到這種操作時,React是如何處理的呢?


接下來我們通過一張圖來展示整個處理過程:

A 節點(包括其子節點)整個被移動到 D 節點下,由于 React 只會簡單地考慮同層級節點的位置變換,而對于不 同層級的節點,只有創建和刪除操作。當根節點發現子節點中 A 消失了,就會直接銷毀 A;當 D 發現多了一個子節點 A,則會創 建新的 A(包括子節點)作為其子節點。此時,diff 的執行情況:create A → create B → create C → delete A。

由此可以發現,當出現節點跨層級移動時,并不會出現想象中的移動操作,而是以 A 為根節點的整個樹被重新創建。這是一種影響React性能的操作,因此官方建議不要進行 DOM 節點跨層級的操作。

在開發組件時,保持穩定的 DOM 結構會有助于性能的提升。例如,可以通過 CSS 隱藏或顯示節點,而不是真正地移
除或添加 DOM 節點。
component diff

React 是基于組件構建應用的,對于組件間的比較所采取的策略也是非常簡潔、高效的。

如果是同一類型的組件,按照原策略繼續比較 Virtual DOM 樹即可。

如果不是,則將該組件判斷為 dirty component,從而替換整個組件下的所有子節點。

對于同一類型的組件,有可能其 Virtual DOM 沒有任何變化,如果能夠確切知道這點,那么就可以節省大量的 diff 運算時間。因此,React允許用戶通過shouldComponentUpdate()來判斷該組件是否需要進行diff算法分析,但是如果調用了forceUpdate方法,shouldComponentUpdate則失效。

接下來我們看下面這個例子是如何實現轉換的:

轉換流程如下:

當組件D變為組件G時,即使這兩個組件結構相似,一旦React判斷D和G是不同類型的組件,就不會比較二 者的結構,而是直接刪除組件D,重新創建組件G及其子節點。雖然當兩個組件是不同類型但結構相似時,diff會影響性能,但正如React官方博客所言:不同類型的組件很少存在相似DOM樹的情況,因此這種極端因素很難在實際開發過程中造成重大的影響。

element diff

當節點處于同一層級時,diff 提供了 3 種節點操作,分別為 INSERT_MARKUP (插入)、MOVE_EXISTING (移動)和 REMOVE_NODE (刪除)。

INSERT_MARKUP :新的組件類型不在舊集合里,即全新的節點,需要對新節點執行插入操作。

MOVE_EXISTING :舊集合中有新組件類型,且 element 是可更新的類型,generateComponentChildren 已調用receiveComponent ,這種情況下 prevChild=nextChild ,就需要做移動操作,可以復用以前的 DOM 節點。

REMOVE_NODE :舊組件類型,在新集合里也有,但對應的 element 不同則不能直接復用和更新,需要執行刪除操作,或者舊組件不在新集合里的,也需要執行刪除操作。

舊集合中包含節點A、B、C和D,更新后的新集合中包含節點B、A、D和C,此時新舊集合進行diff差異化對比,發現B!=A,則創建并插入B至新集合,刪除舊集合A;以此類推,創建并插入A、D和C,刪除B、C和D。

我們發現這些都是相同的節點,僅僅是位置發生了變化,但卻需要進行繁雜低效的刪除、創建操作,其實只要對這些節點進行位置移動即可。React針對這一現象提出了一種優化策略:允許開發者對同一層級的同組子節點,添加唯一 key 進行區分。 雖然只是小小的改動,性能上卻發生了翻天覆地的變化!我們再來看一下應用了這個策略之后,react diff是如何操作的。

通過key可以準確地發現新舊集合中的節點都是相同的節點,因此無需進行節點刪除和創建,只需要將舊集合中節點的位置進行移動,更新為新集合中節點的位置,此時React 給出的diff結果為:B、D不做任何操作,A、C進行移動操作即可。

具體的流程我們用一張表格來展現一下:

index 節點 oldIndex maxIndex 操作
0 B 1 0 oldIndex(1)>maxIndex(0),maxIndex=oldIndex
1 A 0 1 oldIndex(0)
2 D 3 1 oldIndex(3)>maxIndex(1),maxIndex=oldIndex
3 C 2 3 oldIndex(2)

index: 新集合的遍歷下標。

oldIndex:當前節點在老集合中的下標。

maxIndex:在新集合訪問過的節點中,其在老集合的最大下標值。

操作一欄中只比較oldIndex和maxIndex:

當oldIndex>maxIndex時,將oldIndex的值賦值給maxIndex

當oldIndex=maxIndex時,不操作

當oldIndex

上面的例子僅僅是在新舊集合中的節點都是相同的節點的情況下,那如果新集合中有新加入的節點且舊集合存在 需要刪除的節點,那么 diff 又是如何對比運作的呢?

index 節點 oldIndex maxIndex 操作
0 B 1 0 oldIndex(1)>maxIndex(0),maxIndex=oldIndex
1 E - 1 oldIndex不存在,添加節點E至index(1)的位置
2 C 2 1 不操作
3 A 0 3 oldIndex(0)
注:最后還需要對舊集合進行循環遍歷,找出新集合中沒有的節點,此時發現存在這樣的節點D,因此刪除節點D,到此 diff 操作全部完成。

同樣操作一欄中只比較oldIndex和maxIndex,但是oldIndex可能有不存在的情況:

oldIndex存在

當oldIndex>maxIndex時,將oldIndex的值賦值給maxIndex

當oldIndex=maxIndex時,不操作

當oldIndex

oldIndex不存在

新增當前節點至index的位置

當然這種diff并非完美無缺的,我們來看這么一種情況:


實際我們只需對D執行移動操作,然而由于D在舊集合中的位置是最大的,導致其他節點的oldIndex < maxIndex,造成D沒有執行移動操作,而是A、B、C全部移動到D節點后面的現象。針對這種情況,官方建議:

在開發過程中,盡量減少類似將最后一個節點移動到列表首部的操作。當節點數量過大或更新操作過于頻繁時,這在一定程度上會影響React的渲染性能。

由于key的存在,react可以準確地判斷出該節點在新集合中是否存在,這極大地提高了diff效率。我們在開發過中進行列表渲染的時候,若沒有加key,react會拋出警告要求開發者加上key,就是為了提高diff效率。但是加了key一定要比沒加key的性能更高嗎?我們再來看一個例子:

現在有一集合[1,2,3,4,5],渲染成如下的樣子:
1
2
3
4
5
--------------- 現在我們將這個集合的順序打亂變成[1,3,2,5,4]。 1.加key
1
1
2
3
3
========>
2
4
5
5
4
操作:節點2移動至下標為2的位置,節點4移動至下標為4的位置。 2.不加key
1
1
2
3
3
========>
2
4
5
5
4
操作:修改第1個到第5個節點的innerText --------------- 如果我們對這個集合進行增刪的操作改成[1,3,2,5,6]。 1.加key
1
1
2
3
3
========>
2
4
5
5
6
操作:節點2移動至下標為2的位置,新增節點6至下標為4的位置,刪除節點4。 2.不加key
1
1
2
3
3
========>
2
4
5
5
6
操作:修改第1個到第5個節點的innerText --------------- 通過上面這兩個例子我們發現: 由于dom節點的移動操作開銷是比較昂貴的,沒有key的情況下要比有key的性能更好。

通過上面的例子我們發現,雖然加了key提高了diff效率,但是未必一定提升了頁面的性能。因此我們要注意這么一點:

對于簡單列表頁渲染來說,不加key要比加了key的性能更好

根據上面的情況,最后我們總結一下key的作用:

準確判斷出當前節點是否在舊集合中

極大地減少遍歷次數

應用實踐

示例代碼地址:https://github.com/ruichengpi...

頁面指定區域刷新

現在有這么一個需求,當用戶身份變化時,當前頁面重新加載數據。猛一看過去覺得非常簡單,沒啥難度的,只要在componentDidUpdate這個生命周期里去判斷用戶身份是否發生改變,如果發生改變就重新請求數據,于是就有了以下這一段代碼:

import React from "react";
import {connect} from "react-redux";
let oldAuthType = "";//用來存儲舊的用戶身份
@connect(
  state=>state.user
)
class Page1 extends React.PureComponent{
  state={
    loading:true
  }
  loadMainData(){
    //這里采用了定時器去模擬數據請求
    this.setState({
      loading:true
    });
    const timer = setTimeout(()=>{
      this.setState({
        loading:false
      });
      clearTimeout(timer);
    },2000);
  }
  componentDidUpdate(){
    const {authType} = this.props;
    //判斷當前用戶身份是否發生了改變
    if(authType!==oldAuthType){
      //存儲新的用戶身份
      oldAuthType=authType;
      //重新加載數據
      this.loadMainData();
    }
  }
  componentDidMount(){
    oldAuthType=this.props.authType;
    this.loadMainData();
  }
  render(){
    const {loading} = this.state;
    return (
      

{`頁面1${loading?"加載中...":"加載完成"}`}

) } } export default Page1;

看上去我們僅僅通過加上一段代碼就完成了這一需求,但是當我們頁面是幾十個的時候,那這種方法就顯得捉襟見肘了。哪有沒有一個很好的方法來實現這個需求呢?其實很簡單,利用react diff的特性就可以實現它。對于這個需求,實際上就是希望當前組件可以銷毀在重新生成,那怎么才能讓其銷毀并重新生成呢?通過上面的總結我發現兩種情況,可以實現組件的銷毀并重新生成。

當組件類型發生改變

當key值發生變化

接下來我們就結合這兩個特點,用兩種方法去實現。

第一種:引入一個loading組件。切換身份時設置loading為true,此時loading組件顯示;切換身份完成,loading變為false,其子節點children顯示。

{loading?:children}

第二種:在刷新區域加上一個key值就可以了,用戶身份一改變,key值就發生改變。

{children}

第一種和第二種取舍上,個人建議的是這樣子的:

如果需要請求服務器的,用第一種,因為請求服務器會有一定等待時間,加入loading組件可以讓用戶有感知,體驗更好。如果是不需要請求服務器的情況下,選用第二種,因為第二種更簡單實用。
更加方便地監聽props改變


針對這個需求,我們喜歡將搜索條件封裝成一個組件,查詢列表封裝成一個組件。其中查詢列表會接收一個查詢參數的屬性,如下所示:

import React from "react";
import {Card} from "antd";
import Filter from "./components/filter";
import Teacher from "./components/teacher";
export default class Demo2 extends React.PureComponent{
  state={
    filters:{
      name:undefined,
      height:undefined,
      age:undefined
    }
  }
  handleFilterChange=(filters)=>{
    this.setState({
      filters
    });
  }
  render(){
    const {filters} = this.state;
    return 
      {/* 過濾器 */}
       
      {/* 查詢列表 */}
      
    
  }
}

現在我們面臨一個問題,如何在組件Teacher中監聽filters的變化,由于filters是一個引用類型,想監聽其變化變得有些復雜,好在lodash提供了比較兩個對象的工具方法,使其簡單了。但是如果后期給Teacher加了額外的props,此時你要監聽多個props的變化時,你的代碼將變得比較難以維護。針對這個問題,我們依舊可以通過key值去實現,當每次搜索時,重新生成一個key,那么Teacher組件就會重新加載了。代碼如下:

import React from "react";
import {Card} from "antd";
import Filter from "./components/filter";
import Teacher from "./components/teacher";
export default class Demo2 extends React.PureComponent{
  state={
    filters:{
      name:undefined,
      height:undefined,
      age:undefined
    },
    tableKey:this.createTableKey()
  }
  createTableKey(){
    return Math.random().toString(36).substring(7);
  }
  handleFilterChange=(filters)=>{
    this.setState({
      filters,
      //重新生成tableKey
      tableKey:this.createTableKey()
    });
  }
  render(){
    const {filters,tableKey} = this.state;
    return 
      {/* 過濾器 */}
       
      {/* 查詢列表 */}
      
    
  }
}

即使后期給Teacher加入新的props,也沒有問題,只需拼接一下key即可:

 
react-router中Link問題


先看一下demo代碼:

import React from "react";
import {Card,Spin,Divider,Row,Col} from "antd";
import {Link} from "react-router-dom";

const bookList = [{
  bookId:"1",
  bookName:"三國演義",
  author:"羅貫中"
},{
  bookId:"2",
  bookName:"水滸傳",
  author:"施耐庵"
}]
export default class Demo3 extends React.PureComponent{
  state={
    bookList:[],
    bookId:"",
    loading:true
  }
  loadBookList(bookId){
    this.setState({
      loading:true
    });
    const timer = setTimeout(()=>{
      this.setState({
        loading:false,
        bookId,
        bookList
      });
      clearTimeout(timer);
    },2000);
  }
  componentDidMount(){
    const {match} = this.props;
    const {params} = match;
    const {bookId} = params;
    this.loadBookList(bookId);
  }
  render(){
    const {bookList,bookId,loading} = this.state;
    const selectedBook = bookList.find((book)=>book.bookId===bookId);
    return 
      
        {
          selectedBook&&(

書名:{selectedBook?selectedBook.bookName:"--"}

作者:{selectedBook?selectedBook.author:"--"}
) } 關聯圖書 { bookList.filter((book)=>book.bookId!==bookId).map((book)=>{ const {bookId,bookName} = book; return

{bookName}

}) }
} }

通過演示gif,我們看到了地址欄的地址已經發生改變,但是并沒有我們想象中那樣從新走一遍componentDidMount去請求數據,這說明我們的組件并沒有實現銷毀并重新生成這么一個過程。解決這個問題你可以在componentDidUpdate去監聽其改變:

  componentDidUpdate(){
    const {match} = this.props;
    const {params} = match;
    const {bookId} = params;
    if(bookId!==this.state.bookId){
      this.loadBookList(bookId);
    }
  }

前面我們說過如果是后期需要監聽多個props的話,這樣子后期維護比較麻煩.同樣我們還是利用key去解決這個問題,首頁我們可以將頁面封裝成一個組件BookDetail,并且在其外層再包裹一層,再去給BookDetail加key,代碼如下:

import React from "react";
import BookDetail from "./bookDetail";
export default class Demo3 extends React.PureComponent{
  render(){
    const {match} = this.props;
    const {params} = match;
    const {bookId} = params;
    return 
  }
}

這樣的好處是我們代碼結構更加清晰,后續拓展新功能比較簡單。

結語:

React的高效得益于其Virtual DOM+React diff的體系。diff算法并非react獨創,react只是在傳統diff算法做了優化。但因為其優化,將diff算法的時間復雜度一下子從O(n^3)降到O(n)。

React diff的三大策略:

Web UI中DOM節點跨層級的移動操作特別少,可以忽略不計。

擁有相同類的兩個組件將會生成相似的樹形結構,擁有不同類的兩個組件將會生成不同的樹形結構。

對于同一層級的一組子節點,它們可以通過唯一 id 進行區分。

在開發組件時,保持穩定的 DOM 結構會有助于性能的提升。

在開發過程中,盡量減少類似將最后一個節點移動到列表首部的操作。

key的存在是為了提升diff效率,但未必一定就可以提升性能,記住簡單列表渲染情況下,不加key要比加key的性能更好。

懂得借助react diff的特性去解決我們實際開發中的一系列問題。

文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。

轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/103643.html

相關文章

  • 2017文章總結

    摘要:歡迎來我的個人站點性能優化其他優化瀏覽器關鍵渲染路徑開啟性能優化之旅高性能滾動及頁面渲染優化理論寫法對壓縮率的影響唯快不破應用的個優化步驟進階鵝廠大神用直出實現網頁瞬開緩存網頁性能管理詳解寫給后端程序員的緩存原理介紹年底補課緩存機制優化動 歡迎來我的個人站點 性能優化 其他 優化瀏覽器關鍵渲染路徑 - 開啟性能優化之旅 高性能滾動 scroll 及頁面渲染優化 理論 | HTML寫法...

    dailybird 評論0 收藏0
  • 2017文章總結

    摘要:歡迎來我的個人站點性能優化其他優化瀏覽器關鍵渲染路徑開啟性能優化之旅高性能滾動及頁面渲染優化理論寫法對壓縮率的影響唯快不破應用的個優化步驟進階鵝廠大神用直出實現網頁瞬開緩存網頁性能管理詳解寫給后端程序員的緩存原理介紹年底補課緩存機制優化動 歡迎來我的個人站點 性能優化 其他 優化瀏覽器關鍵渲染路徑 - 開啟性能優化之旅 高性能滾動 scroll 及頁面渲染優化 理論 | HTML寫法...

    hellowoody 評論0 收藏0
  • 2017文章總結

    摘要:歡迎來我的個人站點性能優化其他優化瀏覽器關鍵渲染路徑開啟性能優化之旅高性能滾動及頁面渲染優化理論寫法對壓縮率的影響唯快不破應用的個優化步驟進階鵝廠大神用直出實現網頁瞬開緩存網頁性能管理詳解寫給后端程序員的緩存原理介紹年底補課緩存機制優化動 歡迎來我的個人站點 性能優化 其他 優化瀏覽器關鍵渲染路徑 - 開啟性能優化之旅 高性能滾動 scroll 及頁面渲染優化 理論 | HTML寫法...

    wwolf 評論0 收藏0
  • 前方來報,八月最新資訊--關于vue2&3的最佳文章推薦

    摘要:哪吒別人的看法都是狗屁,你是誰只有你自己說了才算,這是爹教我的道理。哪吒去他個鳥命我命由我,不由天是魔是仙,我自己決定哪吒白白搭上一條人命,你傻不傻敖丙不傻誰和你做朋友太乙真人人是否能夠改變命運,我不曉得。我只曉得,不認命是哪吒的命。 showImg(https://segmentfault.com/img/bVbwiGL?w=900&h=378); 出處 查看github最新的Vue...

    izhuhaodev 評論0 收藏0
  • 平時積累的前端資源,持續更新中。。。

    本文收集學習過程中使用到的資源。 持續更新中…… 項目地址 https://github.com/abc-club/f... 目錄 vue react react-native Weex typescript Taro nodejs 常用庫 css js es6 移動端 微信公眾號 小程序 webpack GraphQL 性能與監控 高質文章 趨勢 動效 數據結構與算法 js core 代碼規范...

    acrazing 評論0 收藏0

發表評論

0條評論

EasonTyler

|高級講師

TA的文章

閱讀更多
最新活動
閱讀需要支付1元查看
<