欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

淺談React中key的作用

 更新時(shí)間:2025年09月03日 08:20:43   作者:CaptainDrake  
React中key用于唯一標(biāo)識元素,提升列表渲染性能并確保狀態(tài)一致性,key幫助React高效比較新舊節(jié)點(diǎn),本文主要介紹了React 中 key 的作用,感興趣的可以了解一下

key 概念

在 React 中,key 用于識別哪些元素是變化、添加或刪除的。

在列表渲染中,key 尤其重要,因?yàn)樗芴岣咪秩拘阅芎痛_保組件狀態(tài)的一致性。

key 的作用

1)唯一性標(biāo)識:

React 通過 key 唯一標(biāo)識列表中的每個(gè)元素。當(dāng)列表發(fā)生變化(增刪改排序)時(shí),React 會(huì)通過 key 快速判斷:

  • 哪些元素是新增的(需要?jiǎng)?chuàng)建新 DOM 節(jié)點(diǎn))
  • 哪些元素是移除的(需要銷毀舊 DOM 節(jié)點(diǎn))
  • 哪些元素是移動(dòng)的(直接復(fù)用現(xiàn)有 DOM 節(jié)點(diǎn),僅調(diào)整順序)

如果沒有 key,React 會(huì)默認(rèn)使用數(shù)組索引(index)作為標(biāo)識,這在動(dòng)態(tài)列表中會(huì)導(dǎo)致 性能下降狀態(tài)錯(cuò)誤。

2)保持組件狀態(tài):

使用 key 能確保組件在更新過程中狀態(tài)的一致性。不同的 key 會(huì)使 React 認(rèn)為它們是不同的組件實(shí)例,因而會(huì)創(chuàng)建新的組件實(shí)例,而不是重用現(xiàn)有實(shí)例。這對于有狀態(tài)的組件尤為重要。

// 如果初始列表是 [A, B],用索引 index 作為 key:
<ul>
  {items.map((item, index) => (
    <li key={index}>{item}</li>
  ))}
</ul>

// 在頭部插入新元素變?yōu)?[C, A, B] 時(shí):
// React 會(huì)認(rèn)為 key=0 → C(重新創(chuàng)建)
// key=1 → A(復(fù)用原 key=0 的 DOM,但狀態(tài)可能殘留)
// 此時(shí),原本屬于 A 的輸入框狀態(tài)可能會(huì)錯(cuò)誤地出現(xiàn)在 C 中。

3)高效的 Diff 算法:

在列表中使用 key 屬性,React 可以通過 Diff 算法快速比較新舊元素,確定哪些元素需要重新渲染,哪些元素可以復(fù)用。這減少了不必要的 DOM 操作,從而提高渲染性能。

源碼解析

以下是 React 源碼中與 key 相關(guān)的關(guān)鍵部分:

1)生成 Fiber樹

在生成 Fiber 樹時(shí),React 使用 key 來匹配新舊節(jié)點(diǎn)。

src/react/packages/react-reconciler/src/ReactChildFiber.js

    // * 協(xié)調(diào)子節(jié)點(diǎn),構(gòu)建新的子fiber結(jié)構(gòu),并且返回新的子fiber
  function reconcileChildFibers(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null, // 老fiber的第一個(gè)子節(jié)點(diǎn)
    newChild: any,
    lanes: Lanes,
  ): Fiber | null {
    // This indirection only exists so we can reset `thenableState` at the end.
    // It should get inlined by Closure.
    thenableIndexCounter = 0;
    const firstChildFiber = reconcileChildFibersImpl(
      returnFiber,
      currentFirstChild,
      newChild,
      lanes,
      null, // debugInfo
    );

    thenableState = null;
    // Don't bother to reset `thenableIndexCounter` to 0 because it always gets
    // set at the beginning.
    return firstChildFiber;
  }
  
  function reconcileChildrenArray(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChildren: Array<any>,
    lanes: Lanes,
    debugInfo: ReactDebugInfo | null,
  ): Fiber | null {
    let resultingFirstChild: Fiber | null = null; // 存儲(chǔ)新生成的child
    let previousNewFiber: Fiber | null = null;

    let oldFiber = currentFirstChild;
    let lastPlacedIndex = 0;
    let newIdx = 0;
    let nextOldFiber = null;
    // ! 1. 從左邊往右遍歷,比較新老節(jié)點(diǎn),如果節(jié)點(diǎn)可以復(fù)用,繼續(xù)往右,否則就停止
    for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
      if (oldFiber.index > newIdx) {
        nextOldFiber = oldFiber;
        oldFiber = null;
      } else {
        nextOldFiber = oldFiber.sibling;
      }
      const newFiber = updateSlot(
        returnFiber,
        oldFiber,
        newChildren[newIdx],
        lanes,
        debugInfo,
      );
      if (newFiber === null) {
        // TODO: This breaks on empty slots like null children. That's
        // unfortunate because it triggers the slow path all the time. We need
        // a better way to communicate whether this was a miss or null,
        // boolean, undefined, etc.
        if (oldFiber === null) {
          oldFiber = nextOldFiber;
        }
        break;
      }
      if (shouldTrackSideEffects) {
        if (oldFiber && newFiber.alternate === null) {
          // We matched the slot, but we didn't reuse the existing fiber, so we
          // need to delete the existing child.
          deleteChild(returnFiber, oldFiber);
        }
      }
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
      if (previousNewFiber === null) {
        // TODO: Move out of the loop. This only happens for the first run.
        resultingFirstChild = newFiber;
      } else {
        // TODO: Defer siblings if we're not at the right index for this slot.
        // I.e. if we had null values before, then we want to defer this
        // for each null value. However, we also don't want to call updateSlot
        // with the previous one.
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
      oldFiber = nextOldFiber;
    }

    // !2.1 新節(jié)點(diǎn)沒了,(老節(jié)點(diǎn)還有)。則刪除剩余的老節(jié)點(diǎn)即可
    // 0 1 2 3 4
    // 0 1 2 3
    if (newIdx === newChildren.length) {
      // We've reached the end of the new children. We can delete the rest.
      deleteRemainingChildren(returnFiber, oldFiber);
      if (getIsHydrating()) {
        const numberOfForks = newIdx;
        pushTreeFork(returnFiber, numberOfForks);
      }
      return resultingFirstChild;
    }
    // ! 2.2 (新節(jié)點(diǎn)還有),老節(jié)點(diǎn)沒了
    // 0 1 2 3 4
    // 0 1 2 3 4 5
    if (oldFiber === null) {
      // If we don't have any more existing children we can choose a fast path
      // since the rest will all be insertions.
      for (; newIdx < newChildren.length; newIdx++) {
        const newFiber = createChild(
          returnFiber,
          newChildren[newIdx],
          lanes,
          debugInfo,
        );
        if (newFiber === null) {
          continue;
        }
        lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
        if (previousNewFiber === null) {
          // TODO: Move out of the loop. This only happens for the first run.
          resultingFirstChild = newFiber;
        } else {
          previousNewFiber.sibling = newFiber;
        }
        previousNewFiber = newFiber;
      }
      if (getIsHydrating()) {
        const numberOfForks = newIdx;
        pushTreeFork(returnFiber, numberOfForks);
      }
      return resultingFirstChild;
    }

    // !2.3 新老節(jié)點(diǎn)都還有節(jié)點(diǎn),但是因?yàn)槔蟜iber是鏈表,不方便快速get與delete,
    // !   因此把老fiber鏈表中的節(jié)點(diǎn)放入Map中,后續(xù)操作這個(gè)Map的get與delete
    // 0 1|   4 5
    // 0 1| 7 8 2 3
    // Add all children to a key map for quick lookups.
    const existingChildren = mapRemainingChildren(returnFiber, oldFiber);

    // Keep scanning and use the map to restore deleted items as moves.
    for (; newIdx < newChildren.length; newIdx++) {
      const newFiber = updateFromMap(
        existingChildren,
        returnFiber,
        newIdx,
        newChildren[newIdx],
        lanes,
        debugInfo,
      );
      if (newFiber !== null) {
        if (shouldTrackSideEffects) {
          if (newFiber.alternate !== null) {
            // The new fiber is a work in progress, but if there exists a
            // current, that means that we reused the fiber. We need to delete
            // it from the child list so that we don't add it to the deletion
            // list.
            existingChildren.delete(
              newFiber.key === null ? newIdx : newFiber.key,
            );
          }
        }
        lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
        if (previousNewFiber === null) {
          resultingFirstChild = newFiber;
        } else {
          previousNewFiber.sibling = newFiber;
        }
        previousNewFiber = newFiber;
      }
    }

     // !3. 如果是組件更新階段,此時(shí)新節(jié)點(diǎn)已經(jīng)遍歷完了,能復(fù)用的老節(jié)點(diǎn)都用完了,
    // ! 則最后查找Map里是否還有元素,如果有,則證明是新節(jié)點(diǎn)里不能復(fù)用的,也就是要被刪除的元素,此時(shí)刪除這些元素就可以了
    if (shouldTrackSideEffects) {
      // Any existing children that weren't consumed above were deleted. We need
      // to add them to the deletion list.
      existingChildren.forEach(child => deleteChild(returnFiber, child));
    }

    if (getIsHydrating()) {
      const numberOfForks = newIdx;
      pushTreeFork(returnFiber, numberOfForks);
    }
    return resultingFirstChild;
  }

在 reconcileChildFibers 中的關(guān)鍵使用:

頂層“單個(gè)元素”分支(如 reconcileSingleElement):先在兄弟鏈表里按 key 查找可復(fù)用的老 Fiber;若 key 相同再比類型,復(fù)用成功則刪除其他老兄弟,否則刪到尾并新建。

  function reconcileSingleElement(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    element: ReactElement,
    lanes: Lanes,
    debugInfo: ReactDebugInfo | null,
  ): Fiber {
    const key = element.key;
    let child = currentFirstChild;
    // 檢查老的fiber單鏈表中是否有可以復(fù)用的節(jié)點(diǎn)
    while (child !== null) {
      if (child.key === key) {
        ...
        if (child.elementType === elementType || ... ) {
          deleteRemainingChildren(returnFiber, child.sibling);
          const existing = useFiber(child, element.props);
          ...
          return existing;
        }
        deleteRemainingChildren(returnFiber, child);
        break;
      } else {
        deleteChild(returnFiber, child);
      }
    }
    ...
  }
  • 頂層對 Fragment(無 key)特殊處理:若是未帶 key 的頂層 Fragment,會(huì)直接把 children 取出來按數(shù)組/迭代器邏輯繼續(xù)走。

2)比較新舊節(jié)點(diǎn)

在比較新舊節(jié)點(diǎn)時(shí),React 通過 key 來確定節(jié)點(diǎn)是否相同:

src/react/packages/react-reconciler/src/ReactChildFiber.js

  function updateSlot(
    returnFiber: Fiber,
    oldFiber: Fiber | null,
    newChild: any,
    lanes: Lanes,
    debugInfo: null | ReactDebugInfo,
  ): Fiber | null {
    // Update the fiber if the keys match, otherwise return null.
    const key = oldFiber !== null ? oldFiber.key : null;

    if (
      (typeof newChild === 'string' && newChild !== '') ||
      typeof newChild === 'number'
    ) {
      // Text nodes don't have keys. If the previous node is implicitly keyed
      // we can continue to replace it without aborting even if it is not a text
      // node.
      if (key !== null) {
        return null;
      }
      return updateTextNode(
        returnFiber,
        oldFiber,
        '' + newChild,
        lanes,
        debugInfo,
      );
    }

    if (typeof newChild === 'object' && newChild !== null) {
      switch (newChild.$$typeof) {
        case REACT_ELEMENT_TYPE: {
          if (newChild.key === key) {
            return updateElement(
              returnFiber,
              oldFiber,
              newChild,
              lanes,
              mergeDebugInfo(debugInfo, newChild._debugInfo),
            );
          } else {
            return null;
          }
        }
        case REACT_PORTAL_TYPE: {
          if (newChild.key === key) {
            return updatePortal(
              returnFiber,
              oldFiber,
              newChild,
              lanes,
              debugInfo,
            );
          } else {
            return null;
          }
        }
        case REACT_LAZY_TYPE: {
          const payload = newChild._payload;
          const init = newChild._init;
          return updateSlot(
            returnFiber,
            oldFiber,
            init(payload),
            lanes,
            mergeDebugInfo(debugInfo, newChild._debugInfo),
          );
        }
      }

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

        return updateFragment(
          returnFiber,
          oldFiber,
          newChild,
          lanes,
          null,
          mergeDebugInfo(debugInfo, newChild._debugInfo),
        );
      }

      // Usable node types
      //
      // Unwrap the inner value and recursively call this function again.
      if (typeof newChild.then === 'function') {
        const thenable: Thenable<any> = (newChild: any);
        return updateSlot(
          returnFiber,
          oldFiber,
          unwrapThenable(thenable),
          lanes,
          debugInfo,
        );
      }

      if (newChild.$$typeof === REACT_CONTEXT_TYPE) {
        const context: ReactContext<mixed> = (newChild: any);
        return updateSlot(
          returnFiber,
          oldFiber,
          readContextDuringReconcilation(returnFiber, context, lanes),
          lanes,
          debugInfo,
        );
      }

      throwOnInvalidObjectType(returnFiber, newChild);
    }

    if (__DEV__) {
      if (typeof newChild === 'function') {
        warnOnFunctionType(returnFiber, newChild);
      }
      if (typeof newChild === 'symbol') {
        warnOnSymbolType(returnFiber, newChild);
      }
    }

    return null;
  }

實(shí)際案例

1)簡單列表

假設(shè)我們有一個(gè)簡單的列表:

const items = this.state.items.map(item => 
	<li key={item.id}>{ item.text }</li>
)

在上述代碼中,每個(gè)

元素都有一個(gè)唯一的 key。

如果 items 數(shù)組發(fā)生變化(如添加或刪除元素),React將根據(jù) key 來高效地更新DOM:

2)錯(cuò)誤案例演示

import React, { useState } from 'react'

// 錯(cuò)誤案例:使用數(shù)組索引作為 key,導(dǎo)致組件在插入/重排時(shí)狀態(tài)錯(cuò)亂
// 復(fù)現(xiàn)實(shí)驗(yàn):
// 1) 在下方兩個(gè)輸入框分別輸入不同文本(對應(yīng) A、B)
// 2) 點(diǎn)擊“在頭部插入 C” → 列表從 [A, B] 變?yōu)?[C, A, B]
// 3) 使用 index 作為 key 時(shí):
//    key=0 → C(重新創(chuàng)建)
//    key=1 → A(復(fù)用原 key=0 的 DOM,狀態(tài)可能殘留)
//    因此原本屬于 A 的輸入框狀態(tài)可能會(huì)錯(cuò)誤地出現(xiàn)在 C 中

function InputItem({ label }: { label: string }) {
	const [text, setText] = useState<string>('')
	return (
		<div
			style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}
		>
			<span style={{ width: 80 }}>{label}</span>
			<input
				placeholder="在此輸入以觀察狀態(tài)"
				value={text}
				onChange={e => setText(e.target.value)}
			/>
		</div>
	)
}

export default function TestDemo() {
	const [labels, setLabels] = useState<string[]>(['A', 'B'])

	const prependC = () => {
		setLabels(prev => ['C', ...prev])
	}

	return (
		<div style={{ padding: 16 }}>
			<h3>錯(cuò)誤示例:使用 index 作為 key(頭部插入觸發(fā)狀態(tài)錯(cuò)亂)</h3>
			<button onClick={prependC} style={{ marginBottom: 12 }}>
				在頭部插入 C
			</button>
			{labels.map((label, index) => (
				// 錯(cuò)誤:使用 index 作為 key,頭部插入 C 后會(huì)發(fā)生狀態(tài)錯(cuò)位
				<InputItem key={index} label={label} />
			))}
		</div>
	)
}

  • 當(dāng)一個(gè)元素被刪除時(shí),React僅刪除對應(yīng) key 的DOM節(jié)點(diǎn)。
  • 當(dāng)一個(gè)元素被添加時(shí),React 僅在相應(yīng)的位置插入新的DOM節(jié)點(diǎn)。
  • 當(dāng)一個(gè)元素被移動(dòng)時(shí),React 會(huì)識別到位置變化并重新排列 DOM 節(jié)點(diǎn)。

到此這篇關(guān)于淺談React中key的作用的文章就介紹到這了,更多相關(guān)React key內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • 淺談React深度編程之受控組件與非受控組件

    淺談React深度編程之受控組件與非受控組件

    這篇文章主要介紹了淺談React深度編程之受控組件與非受控組件,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2017-12-12
  • React報(bào)錯(cuò)之組件不能作為JSX組件使用的解決方法

    React報(bào)錯(cuò)之組件不能作為JSX組件使用的解決方法

    本文主要介紹了React報(bào)錯(cuò)之組件不能作為JSX組件使用的解決方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2022-07-07
  • React模仿網(wǎng)易云音樂實(shí)現(xiàn)一個(gè)音樂項(xiàng)目詳解流程

    React模仿網(wǎng)易云音樂實(shí)現(xiàn)一個(gè)音樂項(xiàng)目詳解流程

    這篇文章主要介紹了React模仿網(wǎng)易云音樂實(shí)現(xiàn)一個(gè)音樂項(xiàng)目的詳細(xì)流程,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2022-08-08
  • React的Props、生命周期詳解

    React的Props、生命周期詳解

    “Props” 是 React 中用于傳遞數(shù)據(jù)給組件的一種機(jī)制,通常作為組件的參數(shù)進(jìn)行傳遞,在 React 中,props 是只讀的,意味著一旦將數(shù)據(jù)傳遞給組件的 props,組件就不能直接修改這些 props 的值,這篇文章主要介紹了React的Props、生命周期,需要的朋友可以參考下
    2024-06-06
  • JavaScript React如何修改默認(rèn)端口號方法詳解

    JavaScript React如何修改默認(rèn)端口號方法詳解

    這篇文章主要介紹了JavaScript React如何修改默認(rèn)端口號方法詳解,文中通過步驟圖片解析介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-07-07
  • react中如何使用定義數(shù)據(jù)并監(jiān)聽其值

    react中如何使用定義數(shù)據(jù)并監(jiān)聽其值

    這篇文章主要介紹了react中如何使用定義數(shù)據(jù)并監(jiān)聽其值問題,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2024-01-01
  • 詳解如何構(gòu)建自己的react hooks

    詳解如何構(gòu)建自己的react hooks

    我們組的前端妹子在組內(nèi)分享時(shí)談到了 react 的鉤子,趁此機(jī)會(huì)我也對我所理解的內(nèi)容進(jìn)行下總結(jié),方便更多的同學(xué)了解。在 React 的 v16.8.0 版本里添加了 hooks 的這種新的 API,我們非常有必要了解下他的使用方法,并能夠結(jié)合我們的業(yè)務(wù)編寫幾個(gè)自定義的 hooks。
    2021-05-05
  • react?app?rewrited替代品craco使用示例

    react?app?rewrited替代品craco使用示例

    這篇文章主要為大家介紹了react?app?rewrited替代品craco使用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-11-11
  • 詳解React Native 屏幕適配(炒雞簡單的方法)

    詳解React Native 屏幕適配(炒雞簡單的方法)

    React Native 可以開發(fā) ios 和 android 的 app,在開發(fā)過程中,勢必會(huì)遇上屏幕適配,這篇文章主要介紹了詳解React Native 屏幕適配(炒雞簡單的方法),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2018-06-06
  • React組件的生命周期深入理解分析

    React組件的生命周期深入理解分析

    組件的生命周期就是React的工作過程,就好比人有生老病死,自然界有日月更替,每個(gè)組件在網(wǎng)頁中也會(huì)有被創(chuàng)建、更新和刪除,如同有生命的機(jī)體一樣
    2022-12-12

最新評論