詳解 React 16 的 Diff 策略

2021-03-02 前端大全

(給前端大全加星標,提升前端技能)

作者:前端桃園 公號 / 桃翁 (本文來自作者投稿)

這是我 Deep In React 系列的第二篇文章,如果還沒有讀過的強烈建議你先讀第一篇:詳談 React Fiber 架構(1)

前言

我相信在看這篇文章的讀者一般都已經了解過 React 16 以前的 Diff 算法了,這個算法也算是 React 跨時代或者說最有影響力的一點了,使 React 在保持了可維護性的基礎上性能大大的提高,但 Diff 過程不僅不是免費的,而且對性能影響很大,有時候更新頁面的時候往往 Diff 所花的時間 js 運行時間比 Rendering 和 Painting 花費更多的時間,所以我一直傳達的觀念是 React 或者說框架的意義是為了提高代碼的可維護性,而不是為了提高性能的,現在所做的提升性能的操作,只是在可維護性的基礎上對性能的優化。具體可以參考我公眾號以前發的這兩篇文章:

別再說虛擬 DOM 快了,要被打臉的

深入理解虛擬 DOM,它真的不快

如果你對標題不滿意,請把文章看完,至少也得把文章最後的結論好好看下

在上一篇將 React Fiber 架構中,已經說到過,React 現在將整體的數據結構從樹改為了鍊表結構。所以相應的 Diff 算法也得改變,以為以前的 Diff 算法就是基於樹的。

老的 Diff 算法提出了三個策略來保證整體界面構建的性能,具體是:

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

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

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

基於以上三個前提策略,React 分別對 tree diff、component diff 以及 element diff 進行算法優化。

具體老的算法可以見這篇文章:React 源碼剖析系列 - 不可思議的 react diff

說實話,老的 Diff 算法還是挺複雜的,你僅僅看上面這篇文章估計一時半會都不能理解,更別說看源碼了。對於 React 16 的 Diff 算法(我覺得都不能把它稱作算法,最多叫個 Diff 策略)其實還是蠻簡單的,React 16 是整個調度流程感覺比較難,我在前面將 Fiber 的文章已經簡單的梳理過了,後面也會慢慢的逐個攻破。

接下來就開始正式的講解 React 16 的 Diff 策略吧!

Diff 簡介

做 Diff 的目的就是為了復用節點。

鍊表的每一個節點是 Fiber,而不是在 16 之前的虛擬DOM 節點。

我這裡說的虛擬 DOM 節點是指 React.createElement 方法所產生的節點。虛擬 DOM tree 只維護了組件狀態以及組件與 DOM 樹的關係,Fiber Node 承載的東西比 虛擬 DOM 節點多很多。

Diff 就是新舊節點的對比,在上一篇中也說道了,這裡面的 Diff 主要是構建 currentInWorkProgress 的過程,同時得到 Effect List,給下一個階段 commit 做準備。

React16 的 diff 策略採用從鍊表頭部開始比較的算法,是層次遍歷,算法是建立在一個節點的插入、刪除、移動等操作都是在節點樹的同一層級中進行的。

對於 Diff, 新老節點的對比,我們以新節點為標準,然後來構建整個 currentInWorkProgress,對於新的 children 會有四種情況。

那麼我們就來一步一步的看這四種類型是如何進行 diff 的。

前置知識介紹

這篇文章主要是從 React 的源碼的邏輯出發介紹的,所以介紹之前了解下只怎麼進入到這個 diff 函數的,react 的 diff 算法是從 reconcileChildren 開始的

export function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderExpirationTime: ExpirationTime,
) {
  if (current === null) {
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderExpirationTime,
    );
  } else {
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderExpirationTime,
    );
  }
}

reconcileChildren 只是一個入口函數,如果首次渲染,current 空 null,就通過 mountChildFibers 創建子節點的 Fiber 實例。如果不是首次渲染,就調用 reconcileChildFibers去做 diff,然後得出 effect list。

接下來再看看 mountChildFibers 和 reconcileChildFibers 有什麼區別:

export const reconcileChildFibers = ChildReconciler(true);
export const mountChildFibers = ChildReconciler(false);

他們都是通過 ChildReconciler 函數來的,只是傳遞的參數不同而已。這個參數叫shouldTrackSideEffects,他的作用是判斷是否要增加一些effectTag,主要是用來優化初次渲染的,因為初次渲染沒有更新操作。

function reconcileChildFibers(
  returnFiber: Fiber,
  currentFirstChild: Fiber | null,
  newChild: any,
  expirationTime: ExpirationTime,
): Fiber | null {
  // 主要的 Diff 邏輯
}

reconcileChildFibers 就是 Diff 部分的主體代碼,這個函數超級長,是一個包裝函數,下面所有的 diff 代碼都在這裡面,詳細的源碼注釋可以見這裡。

參數介紹

returnFiber 是即將 Diff 的這層的父節點。

currentFirstChild是當前層的第一個 Fiber 節點。

newChild 是即將更新的 vdom 節點(可能是 TextNode、可能是 ReactElement,可能是數組),不是 Fiber 節點

expirationTime 是過期時間,這個參數是跟調度有關係的,本系列還沒講解,當然跟 Diff 也沒有關係。

再次提醒,reconcileChildFibers 是 reconcile(diff) 的一層。

前置知識介紹完畢,就開始詳細介紹每一種節點是如何進行 Diff 的。

Diff TextNode

首先看 TextNode,因為它是最簡單的,擔心直接看到難的,然後就打擊你的信心。

看下面兩個小 demo:

// demo1:當前 ui 對應的節點的 jsx
return (
  <div>
  // ...
      <div>
          <xxx></xxx>
          <xxx></xxx>
      </div>
  //...
    </div>
)

// demo2:更新成功後的節點對應的 jsx

return (
  <div>
  // ...
      <div>
          前端桃園
      </div>
  //...
    </div>
)

對應的單鍊表結構圖:

對於 diff TextNode 會有兩種情況。

currentFirstNode 是 TextNode

currentFirstNode 不是 TextNode

currentFirstNode 是當前該層的第一個節點,reconcileChildFibers 傳進來的參數。

為什麼要分兩種情況呢?原因就是為了復用節點

第一種情況。xxx 是一個 TextNode,那麼就代表這這個節點可以復用,有復用的節點,對性能優化很有幫助。既然新的 child 只有一個 TextNode,那麼復用節點之後,就把剩下的 aaa 節點就可以刪掉了,那麼 div 的 child 就可以添加到 workInProgress 中去了。

源碼如下:

if (currentFirstChild !== null && currentFirstChild.tag === HostText) {
      // We already have an existing node so let's just update it and delete
      // the rest.
      deleteRemainingChildren(returnFiber, currentFirstChild.sibling);
      const existing = useFiber(currentFirstChild, textContent, expirationTime);
      existing.return = returnFiber;
      return existing;
}

在源碼裡 useFiber 就是復用節點的方法,deleteRemainingChildren 就是刪除剩餘節點的方法,這裡是從 currentFirstChild.sibling 開始刪除的。

第二種情況。xxx 不是一個 TextNode,那麼就代表這個節點不能復用,所以就從 currentFirstChild開始刪掉剩餘的節點,對應到上面的圖中就是刪除掉 xxx 節點和 aaa 節點。

對於源碼如下:

deleteRemainingChildren(returnFiber, currentFirstChild);
const created = createFiberFromText(
    textContent,
    returnFiber.mode,
    expirationTime,
);
created.return = returnFiber;

其中 createFiberFromText 就是根據 textContent 來創建節點的方法。

注意:刪除節點不會真的從鍊表裡面把節點刪除,只是打一個 delete 的 tag,當 commit 的時候才會真正的去刪除。

Diff React Element

有了上面 TextNode 的 Diff 經驗,那麼來理解 React Element 的 Diff 就比較簡單了,因為他們的思路是一致的:先找有沒有可以復用的節點,如果沒有就另外創建一個。

那麼就有一個問題,如何判斷這個節點是否可以復用呢?

有兩個點:1. key 相同。2. 節點的類型相同。

如果以上兩點相同,就代表這個節點只是變化了內容,不需要創建新的節點,可以復用的。

對應的源碼如下:

if (child.key === key) {
  if (
    child.tag === Fragment
    ? element.type === REACT_FRAGMENT_TYPE
    : child.elementType === element.type
  ) {
    // 為什麼要刪除老的節點的兄弟節點?
    // 因為當前節點是只有一個節點,而老的如果是有兄弟節點是要刪除的,是多於的。刪掉了之後就可以復用老的節點了
    deleteRemainingChildren(returnFiber, child.sibling);
    // 復用當前節點
    const existing = useFiber(
      child,
      element.type === REACT_FRAGMENT_TYPE
      ? element.props.children
      : element.props,
      expirationTime,
    );
    existing.ref = coerceRef(returnFiber, child, element);
    existing.return = returnFiber;
    return existing;
}

相信這些代碼都很好理解了,除了判斷條件跟前面 TextNode 的判斷條件不一樣,其餘的基本都一樣,只是 React Element 多了一個跟新 ref 的過程。

同樣,如果節點的類型不相同,就將節點從當前節點開始把剩餘的都刪除。

deleteRemainingChildren(returnFiber, child);

到這裡,可能你們就會覺得接下來應該就是講解當沒有可以復用的節點的時候是如果創建節點的。

不過可惜你們猜錯了。因為 Facebook 的工程師很厲害,另外還做了一個工作來優化,來找到復用的節點。

我們現在來看這種情況:

這種情況就是有可能更新的時候刪除了一個節點,但是另外的節點還留著。

那麼在對比 xxx 節點和 AAA 節點的時候,它們的節點類型是不一樣,按照我們上面的邏輯,還是應該把 xxx 和 AAA 節點刪除,然後創建一個 AAA 節點。

但是你看,明明 xxx 的 slibling 有一個 AAA 節點可以復用,但是被刪了,多浪費呀。所以還有另外有一個策略來找 xxx 的所有兄弟節點中有沒有可以復用的節點。

這種策略就是從 div 下面的所有子節點去找有沒有可以復用的節點,而不是像 TextNode 一樣,只是找第一個 child 是否可以復用,如果當前節點的 key 不同,就代表肯定不是同一個節點,所以把當前節點刪除,然後再去找當前節點的兄弟節點,直到找到 key 相同,並且節點的類型相同,否則就刪除所有的子節點。

你有木有這樣的問題:為什麼 TextNode 不採用這樣的循環策略來找可以復用的節點呢?這個問題留給你思考,歡迎在評論區留下你的答案。

對應的源碼邏輯如下:

// 找到 key 相同的節點,就會復用當前節點
while (child !== null) {
  if (child.key === key) {
    if (
      child.tag === Fragment
      ? element.type === REACT_FRAGMENT_TYPE
      : child.elementType === element.type
    ) {
      // 復用節點邏輯,省略該部分代碼,和上面復用節點的代碼相同
      // code ...
      return existing;
    } else {
      deleteRemainingChildren(returnFiber, child);
      break;
    }
  } else {
    // 如果沒有可以復用的節點,就把這個節點刪除
    deleteChild(returnFiber, child);
  }
  child = child.sibling;
}

在上面這段代碼我們需要注意的是,當 key 相同,React 會認為是同一個節點,所以當 key 相同,節點類型不同的時候,React 會認為你已經把這個節點重新覆蓋了,所以就不會再去找剩餘的節點是否可以復用。只有在 key 不同的時候,才會去找兄弟節點是否可以復用。

接下來才是我們前面說的,如果沒有找到可以復用的節點,然後就重新創建節點,源碼如下:

// 前面的循環已經把該刪除的已經刪除了,接下來就開始創建新的節點了
if (element.type === REACT_FRAGMENT_TYPE) {
  const created = createFiberFromFragment(
    element.props.children,
    returnFiber.mode,
    expirationTime,
    element.key,
  );
  created.return = returnFiber;
  return created;
} else {
  const created = createFiberFromElement(
    element,
    returnFiber.mode,
    expirationTime,
  );
  created.ref = coerceRef(returnFiber, currentFirstChild, element);
  created.return = returnFiber;
  return created;
}

對於 Fragment 節點和一般的 Element 節點創建的方式不同,因為 Fragment 本來就是一個無意義的節點,他真正需要創建 Fiber 的是它的 children,而不是它自己,所以 createFiberFromFragment 傳遞的不是 element,而是 element.props.children。

Diff Array

Diff Array 算是 Diff 中最難的一部分了,比較的複雜,因為做了很多的優化,不過請你放心,認真看完我的講解,最難的也會很容易理解,廢話不多說,開始吧!

因為 Fiber 樹是單鍊表結構,沒有子節點數組這樣的數據結構,也就沒有可以供兩端同時比較的尾部遊標。所以React的這個算法是一個簡化的兩端比較法,只從頭部開始比較。

前面已經說了,Diff 的目的就是為了復用,對於 Array 就不能像之前的節點那樣,僅僅對比一下元素的 key 或者 元素類型就行,因為數組裡面是好多個元素。你可以在頭腦裡思考兩分鐘如何進行復用節點,再看 React 是怎麼做的,然後對比一下孰優孰劣。

1. 相同位置(index)進行比較

相同位置進行對比,這個是比較容易想到的一種方式,還是舉個例子加深一下印象。

這已經是一個非常簡單的例子了,div 的 child 是一個數組,有 AAA、BBB 然後還有其他的兄弟節點,在做 diff 的時候就可以從新舊的數組中按照索引一一對比,如果可以復用,就把這個節點從老的鍊表裡面刪除,不能復用的話再進行其他的復用策略。

那如果判斷節點是否可以復用呢?有了前面的 ReactElement 和 TextNode 復用的經驗,這個也類似,因為是一一對比嘛,相當於是一個節點一個節點的對比。

不過對於 newChild 可能會有很多種類型,簡單的看下源碼是如何進行判斷的。

 const key = oldFiber !== null ? oldFiber.key : null;

前面的經驗可得,判斷是否可以復用,常常會根據 key 是否相同來決定,所以首先獲取了老節點的 key 是否存在。如果不存在老節點很可能是 TextNode 或者是 Fragment。

接下來再看 newChild 為不同類型的時候是如何進行處理的。

當 newChild 是 TextNode 的時候

if (typeof newChild === 'string' || typeof newChild === 'number') {
  // 對於新的節點如果是 string 或者 number,那麼都是沒有 key 的,
  // 所有如果老的節點有 key 的話,就不能復用,直接返回 null。
  // 老的節點 key 為 null 的話,代表老的節點是文本節點,就可以復用
  if (key !== null) {
    return null;
  }

  return updateTextNode(
    returnFiber,
    oldFiber,
    '' + newChild,
    expirationTime,
  );
}

如果 key 不為 null,那麼就代表老節點不是 TextNode,而新節點又是 TextNode,所以返回 null,不能復用,反之則可以復用,調用 updateTextNode 方法。

注意,updateTextNode 裡面包含了首次渲染的時候的邏輯,首次渲染的時候回插入一個 TextNode,而不是復用。

當 newChild 是 Object 的時候

newChild 是 Object 的時候基本上走的就是 ReactElement 的邏輯了,判斷 key 和 元素的類型是否相等來判斷是否可以復用。

if (typeof newChild === 'object' && newChild !== null) {
  // 有 `$$typeof` 代表就是 ReactElement
  switch (newChild.$$typeof) {
    case REACT_ELEMENT_TYPE: {
                // ReactElement 的邏輯 
    }
    case REACT_PORTAL_TYPE: {
                // 調用 updatePortal
    }
  }

  if (isArray(newChild) || getIteratorFn(newChild)) {
    if (key !== null) {
      return null;
    }

    return updateFragment(
      returnFiber,
      oldFiber,
      newChild,
      expirationTime,
      null,
    );
  }
}

首先判斷是否是對象,用的是 typeof newChild === 'object' && newChild !== null ,注意要加 !== null,因為 typeof null 也是 object。

然後通過 $$typeof 判斷是 REACT_ELEMENT_TYPE 還是 REACT_PORTAL_TYPE,分別調用不同的復用邏輯,然後由於數組也是 Object ,所以這個 if 裡面也有數組的復用邏輯。

我相信到這裡應該對於應該對於如何相同位置的節點如何對比有清晰的認識了。另外還有問題,那就是如何循環一個一個對比呢?

這裡要注意,新的節點的 children 是虛擬 DOM,所以這個 children 是一個數組,而對於之前提到的老的節點樹是鍊表。

那麼循環一個一個對比,就是遍歷數組的過程。

let newIdx = 0 // 新數組的索引
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
  // 遍歷老的節點
  nextOldFiber = oldFiber.sibling; 
  // 返回復用節點的函數,newFiber 就是復用的節點。
  // 如果為空,就代表同位置對比已經不能復用了,循環結束。
  const newFiber = updateSlot(
    returnFiber,
    oldFiber,
    newChildren[newIdx],
    expirationTime,
  );

  if (newFiber === null) {
    break;
  }

  // 其他 code,比如刪除復用的節點
}

這並不是源碼的全部源碼,我只是把思路給貼出來了。

這是第一次遍歷新數組,通過調用 updateSlot 來對比新老元素,前面介紹的如何對比新老節點的代碼都是在這個函數裡。這個循環會把所以的從前面開始能復用的節點,都復用到。比如上面我們畫的圖,如果兩個鍊表裡面的 ??節點,不相同,那麼 newFiber 為 null,這個循環就會跳出。

跳出來了,就會有兩種情況。

2. 新節點已經遍歷完畢

如果新節點已經遍歷完畢的話,也就是沒有要更新的了,這種情況一般就是從原來的數組裡面刪除了元素,那麼直接把剩下的老節點刪除了就行了。還是拿上面的圖的例子舉例,老的鍊表裡??還有很多節點,而新的鍊表??已經沒有節點了,所以老的鍊表??不管是有多少節點,都不能復用了,所以沒用了,直接刪除。

if (newIdx === newChildren.length) {
  // 新的 children 長度已經夠了,所以把剩下的刪除掉
  deleteRemainingChildren(returnFiber, oldFiber);
  return resultingFirstChild;
}

注意這裡是直接 return 了哦,沒有繼續往下執行了。

3. 老節點已經遍歷完畢

如果老的節點在第一次循環的時候就被復用完了,新的節點還有,很有可能就是新增了節點的情況。那麼這個時候只需要根據把剩餘新的節點直接創建 Fiber 就行了。

if (oldFiber === null) {
  // 如果老的節點已經被復用完了,對剩下的新節點進行操作
  for (; newIdx < newChildren.length; newIdx++) {
    const newFiber = createChild(
      returnFiber,
      newChildren[newIdx],
      expirationTime,
    );
  }
  return resultingFirstChild;
}

oldFiber === null 就是用來判斷老的 Fiber 節點變量完了的代碼,Fiber 鍊表是一個單向鍊表,所以為 null 的時候代表已經結束了。所以就直接把剩餘的 newChild 通過循環創建 Fiber。

到這裡,目前簡單的對數組進行增、刪節點的對比還是比較簡單,接下來就是移動的情況是如何進行復用的呢?

4. 移動的情況如何進行節點復用

對於移動的情況,首先要思考,怎麼能判斷數組是否發生過移動操作呢?

如果給你兩個數組,你是否能判斷出來數組是否發生過移動。

答案是:老的數組和新的數組裡面都有這個元素,而且位置不相同。

從兩個數組中找到相同元素(是指可復用的節點),方法有很多種,來看看 React 是如何高效的找出來的。

把所有老數組元素按 key 或者是 index 放 Map 裡,然後遍歷新數組,根據新數組的 key 或者 index 快速找到老數組裡面是否有可復用的。

function mapRemainingChildren(
 returnFiber: Fiber,
 currentFirstChild: Fiber,
): Map<string | number, Fiber> {
  const existingChildren: Map<string | number, Fiber> = new Map();

  let existingChild = currentFirstChild; // currentFirstChild 是老數組鍊表的第一個元素
  while (existingChild !== null) {
  // 看到這裡可能會疑惑怎麼在 Map 裡面的key 是 fiber 的key 還是 fiber 的 index 呢?
  // 我覺得是根據數據類型,fiber 的key 是字符串,而 index 是數字,這樣就能區分了
  // 所以這裡是用的 map,而不是對象,如果是對象的key 就不能區分 字符串類型和數字類型了。
    if (existingChild.key !== null) {
      existingChildren.set(existingChild.key, existingChild);
    } else {
      existingChildren.set(existingChild.index, existingChild);
    }
    existingChild = existingChild.sibling;
    }
    return existingChildren;
}

這個 mapRemainingChildren 就是將老數組存放到 Map 裡面。元素有 key 就 Map 的鍵就存 key,沒有 key 就存 index,key 一定是字符串,index 一定是 number,所以取的時候是能區分的,所以這裡用的是 Map,而不是對象,如果是對象,屬性是字符串,就沒辦法區別是 key 還是 index 了。

現在有了這個 Map,剩下的就是循環新數組,找到 Map 裡面可以復用的節點,如果找不到就創建,這個邏輯基本上跟 updateSlot 的復用邏輯很像,一個是從老數組鍊表中獲取節點對比,一個是從 Map 裡獲取節點對比。

// 如果前面的算法有復用,那麼 newIdx 就不從 0 開始
for (; newIdx < newChildren.length; newIdx++) {
  const newFiber = updateFromMap(
    existingChildren,
    returnFiber,
    newIdx,
    newChildren[newIdx],
    expirationTime,
  );
 // 省略刪除 existingChildren 中的元素和添加 Placement 副作用的情況
}

到這裡新數組遍歷完畢,也就是同一層的 Diff 過程完畢,接下來進行總結一下。

效果演示

以下效果動態演示來自於文章:React Diff 源碼分析,我覺得這個演示非常的形象,有助於理解。

這裡渲染一個可輸入的數組。

1

當第一種情況,新數組遍歷完了,老數組剩餘直接刪除(12345→1234 刪除 5):

新數組沒完,老數組完了(1234→1234567 插入 567):

移動的情況,即之前就存在這個元素,後續只是順序改變(123 → 4321 插入4,移動2 1):

最後刪除沒有涉及的元素。

總結

對於數組的 diff 策略,相對比較複雜,最後來梳理一下這個策略,其實還是很簡單,只是看源碼的時候比較難懂。

我們可以把整個過程分為三個階段:

第一遍歷新數組,新老數組相同 index 進行對比,通過 updateSlot方法找到可以復用的節點,直到找到不可以復用的節點就退出循環。

第一遍歷完之後,刪除剩餘的老節點,追加剩餘的新節點的過程。如果是新節點已遍歷完成,就將剩餘的老節點批量刪除;如果是老節點遍歷完成仍有新節點剩餘,則將新節點直接插入。

把所有老數組元素按 key 或 index 放 Map 裡,然後遍歷新數組,插入老數組的元素,這是移動的情況。

後記

剛開始閱讀源碼的過程是非常的痛苦的,但是當你一遍一遍的把作者想要表達的理解了,為什麼要這麼寫 理解了,會感到作者的設計是如此的精妙絕倫,每一個變量,每一行代碼感覺都是精心設計過的,然後感受到自己與大牛的差距,激發自己的動力。

覺得本文對你有幫助?請分享給更多人

關注「前端大全」加星標,提升前端技能

好文章,我在看❤️

相關焦點

  • 深入理解React Diff算法
    ), type: "div", key: "A" }, {$$typeof: Symbol(react.element), type: "div", key: "B" }, {$$typeof: Symbol(react.element), type: "div", key: "B" }, ]Diff的基本原則對於新舊兩種結構來說
  • React源碼揭秘(三):Diff算法詳解
    代碼參照React 16.13.1什麼是Diff在前兩篇文章中我們分別介紹了 React 的首屏渲染流程1和組件更新流程2,其中首屏渲染會渲染一整棵 DOM 樹組件更新會根據變化的狀態局部更新 DOM 樹那麼 React 如何知道哪些 DOM 節點需要被更新呢?
  • React源碼解析,實現一個React
    每當頁面產生了變化,React的diff算法會先在內存模型中進行比對,提取出差異點,在將Virtual DOM轉化為原生DOM輸出時,按照差異點,只patch出有變動的部分。_reactInternalInstance.updateComponent(null, newState) 這個函數。而 this._reactInternalInstance指向CompositeComponent,困此更新邏輯交回CompositeComponent.updateComponent()來完成。
  • React 16.8發布:hooks終於來了!
    hooks 的 FAQ(https://reactjs.org/docs/hooks-faq.html)談到了 hooks 的逐步採用策略。我們不建議你為了能夠馬上採用 hooks 而對現有應用程式進行重大重寫。相反,可以在一些新組件中嘗試使用 hooks,並讓我們知道你的想法。使用 hooks 的代碼仍然可以與使用類的現有代碼並存。是的!
  • Linux下diff的操作詳解
    本文轉載自【微信公眾號:羽林君,ID:Conscience_Remains】,經微信公眾號授權轉載,如需轉載與原文作者聯繫總述Linux diff命令用於比較文件的差異。diff以逐行的方式,比較文本文件的異同處。特別是比較兩個版本不同的文件,如果指定要比較目錄,則diff會比較目錄中相同文件名的文件,但不會比較其中子目錄。
  • ReactNative學習資源大匯集
    react-native 官方api文檔 http://facebook.github.io/react-native/docs/getting-started.htmlreact-native中文文檔(極客學院) http://wiki.jikexueyuan.com/project/react-native/react-native中文文檔(react
  • tfdiff:多期DID的估計及圖示
    理論推導3. tfdiff 命令3.1 tfdiff 語法介紹3.2 tfdiff 模擬實例3.3 tfdiff 結果展示參考文獻前期相關推文相關課程 1.它與目前常用的兩個命令 (diff 以及 tvdiff) 的區別總結如下:首先,diff命令只限於兩期數據,即政策實施的前後兩階段,在分析多期數據的結果時可能會出問題;其次,相比之下, diff命令可以進行分位數 DID 等多種回歸形式;最後,diff 命令得到的結果展示形式比較單一, 而 Cerulli 所編寫的 tfdiff 命令則可以很便捷地進行輸出,並對提供了較好的圖形展示效果。
  • React 16.8 之 React Hook
    (點擊上方 前端腦洞,可快速關注,感謝分享)Hook 是 React 16.8 的新增特性。它可以讓你在不編寫 class 的情況下使用 state 以及其他的 React 特性.有了 react hook 就不用寫class(類)了,組件都可以用function來寫,不用再寫生命周期鉤子,最關鍵的,不用再面對this了!
  • React系列教程
    Bower的下載及使用8.JSX的基本使用方式及事件處理及部署第二講:React組件及Class、參數、狀態的使用1.面向對象的介紹2.class類的介紹使用3.React使用組件的方式4.組件的傳參方法5.組件的props、state詳解及調用事件方法6.小練習:顯示隱藏-demo
  • 雲計算核心技術Docker教程:cp/diff命令詳解
    來源:TechWeb.com.cn在docker客戶端命令行中我們可以使用cp命令在容器與主機之間拷貝數據,使用diff命令檢查容器裡文件結構的更改。示例docker cp :用於容器與主機之間的數據拷貝。
  • 前端技術:React&Vue對比
    React和vue的業務邏輯是差不多,vue在react上封裝了更簡潔的方法,使用起來更加的便捷,如:提供了便捷的指令(v-for,v-if,v-model),還提供了更多的屬性(computed,watch),我還是比較喜歡用react的,更接近js原生,更容易於理解它。
  • 超強通用的React Native Tab控制器使用詳解-進階篇
    投稿人:hiphonezhu 地址:http://www.jianshu.com/p/b0cfe7f11ee7本文所講知識點在上一篇文章當中,我們學習了react-native-scrollable-tab-view的基本使用方式,包括基本Props的使用介紹等。
  • 每個前端開發必看的React生命周期函數
    在初始化階段有4個生命周期函數:getDefaultProps()是可以設置組件的默認屬性值;componentWillMount()是組件初始化時調用,在整個生命周期中只調用一次;render()是組件在創建虛擬dom,進行diff
  • React Status 中文周刊 #18 - 三種 React 的反面模式
    近來,React 16.8 推出了 Hook,Vue 3.0 推出了 Composition API,但是對於你的項目來說真的需要嘛?電腦訪問,請閱讀原文!🔥 本周熱門Reactochart:由 Spotify 推出的 react 圖表庫 — 此圖表庫由 Spotify 推出,目前已在線上穩定運行了很長時間,同時擁有友好的文檔,每種類型的圖表(折線,條形圖,餅圖,直方圖,熱圖等)都有對應的 demo 參考。
  • React 入門
    當然,這個倉庫基於 React16.8 ,雖然這個版本並不包括當前的航道模型 Lane 等新特性,但是是我個人認為比較穩定且更適合閱讀的一個版本。事實上, React 在使用 fiber 架構之前的 Virtual Dom 和 diff 過程要相對直觀一些。但是在引入了 fiber 架構之後整個流程變得冗長,如果單純想了解 VirtualDom 和 diff 過程的原理也可以通過 simple-virtual-dom[3] 這個倉庫來學習。
  • 從 React 歷史的長河裡聊虛擬DOM及其價值
    DOM 是樹形結構,所以 diff 算法必須是針對樹形結構的。目前已知的完整樹形結構 diff 算法複雜度為 O(n^3) 。完整的 Tree diff 實現算法。[2]但是時間複雜度 O(n^3) 太高了,所以 Facebook 工程師考慮到組件的特殊情況,然後將複雜度降低到了 O(n)。
  • webpack4+react16+react-router-dom4從零配置到優化,實現路由按需...
    上一篇介紹了下webpack的一些配置,接下來講下reactRouter4裡面關於路由的一些配置,如何做到模塊的按需加載,這也是常用的一種優化網站性能的方式
  • React v16.8 發布:帶來穩定版的 Hooks 功能
    從 16.8.0 開始,React 包含穩定的 React Hooks 實現:React DOMReact DOM ServerReact Test RendererReact Shallow Renderer要注意的是,如需使用
  • 修復 React 代碼中煩人的 Warning
    react的diff算法是把key當成唯一id然後比對組件的value來確定是否需要更新的,所以如果沒有key,react將不會知道該如何更新組件。你不傳 key 也能用是因為 react 檢測到子組件沒有 key 後,會默認將數組的索引作為 key。rea
  • Git命令解析-patch、apply、diff
    可以使用gitdiff>patchfile將差異輸出到patch文件,保存或者分享給他人。使用gitdiff命令可以查看工作區修改的內容,gitdiff—cached命令查看添加到暫存區但還未提交的內容。這兩種命令會生成兼容unix系統的標準格式patch。