React的零渲染問題及原始碼分析

趁你還年輕發表於2022-01-29

開門見山,先來看一張bug圖(狀態下面有個00)。
image
預期是:狀態為0時,2個元件不做渲染。
現狀:狀態為0時,2個元件不做渲染,但是渲染出了00。

  • 零渲染 bug 程式碼
  • 如何修復零渲染問題
  • 初窺原始碼
  • 原始碼疑惑
  • 原因總結
  • 原始碼實錘

零渲染 bug 程式碼

什麼是React的零渲染問題?
看下下面這段程式碼,我們會經常這樣寫:

// bug程式碼 0
{obj?.count && <span>{obj?.count}</span>}

假如obj?.count為0,渲染結果為0。
這裡不就1個0麼,上面為什麼是00呢。

// bug程式碼 00 
{obj?.countFoo && <span>{obj?.countFoo}</span>}
{obj?.countBar && <span>{obj?.countBar}</span>}

當obj?.countFoo和obj?.countBar都為0時,渲染結果為00。

如何修復零渲染問題

{!!obj?.count && <span>{obj?.count}</span>}

或者

{obj?.count ? <span>{obj?.count}</span> : null}

或者

{Boolean(obj?.count) && <span>{obj?.count}</span>}

初窺原始碼

原因(點選型別檢視原始碼):
React元件會渲染string,number。不會渲染nullundefinedboolean

原始碼疑惑

既然boolean會被處理為null,那為什麼true && <FooComponent />可以正常渲染呢?
先說結論,因為進行了&&運算,React最終渲染的是jsx與計算後的結果。

const type = typeof children;
if (type === 'undefined' || type === 'boolean') {
    // All of the above are perceived as null.
    children = null;
}

也就是說 此處的children,是jsx計算後的結果。

舉例如下:

// 可渲染值
1 && <FooComponent /> // => jsx計算結果為<FooComponent />,因此渲染<FooComponent/>
"a string" && <FooComponent /> // => jsx計算結果為<FooComponent />,因此渲染<FooComponent />
0 && <FooComponent /> // => jsx計算結果為0,Renders '0'
true && <FooComponent /> // => jsx計算結果為<FooComponent />,因此渲染<FooComponent />

// 不可渲染值
false && <FooComponent /> // => jsx計算結果為false,因此什麼都不渲染
null && <FooComponent /> // => jsx計算結果為null,因此什麼都不渲染
undefined && <FooComponent /> // => jsx計算結果為undefined,因此什麼都不渲染

原因總結

其實,根本不是React渲染什麼的問題,而是&&操作符後返回值的問題。
所以,最根本是因為

  • React渲染string,number,正常元件
  • React不渲染undefined,boolean,null

    {"1"} // 渲染為"1"
    {0} // 渲染為0
    {<FooComponent />} // 假設為正常元件,渲染為<FooComponent />
    
    {undefined} // 不渲染
    {true} // 不渲染
    {false} // 不渲染
    
    {null} // 不渲染

原始碼實錘

  const type = typeof children;

  // React不渲染undefined,boolean
  if (type === 'undefined' || type === 'boolean') {
    // All of the above are perceived as null.
    children = null;
  }

  let invokeCallback = false;

  if (children === null) {
    invokeCallback = true;
  } else {
    switch (type) {
      case 'string':
      case 'number':
        // React渲染string,number
        invokeCallback = true; 
        break;
      case 'object':
        // React渲染正常元件
        switch ((children: any).$$typeof) {
          case REACT_ELEMENT_TYPE:
          case REACT_PORTAL_TYPE:
            invokeCallback = true;
        }
    }
  }

原始值為null,和undefined以及boolean最終被處理為null,React不渲染null的原始碼實錘呢

  render(
    child: ReactNode | null,
    context: Object,
    parentNamespace: string,
  ): string {
    if (typeof child === 'string' || typeof child === 'number') {
      const text = '' + child;
      if (text === '') {
        return '';
      }
      this.previousWasTextNode = true;
      return escapeTextForBrowser(text);
    } else {
      let nextChild;
      ({child: nextChild, context} = resolve(child, context, this.threadID));
      // React不渲染null
      if (nextChild === null || nextChild === false) {
        return '';
      }

對於html標籤渲染空字串而言,空字串會被渲染,例如<div>""</div>會被渲染為<div>""</div>
但對於react而言,完整流程為{null} =>{""} => nothing
例如下面這樣:

<div>{''}</div> // => <div></div>
<div>{'    '}</div> // => <div>    </div>

那麼,React是如何處理空字串的呢?

export function pushTextInstance(
  target: Array<Chunk | PrecomputedChunk>,
  text: string,
  responseState: ResponseState,
): void {
  if (text === '') {
    // Empty text doesn't have a DOM node representation and the hydration is aware of this.
    // 這句註釋的意思是:空文字節點沒有DOM節點表示,它屬於textNode
    return;
  }
  // TODO: Avoid adding a text separator in common cases.
  target.push(stringToChunk(encodeHTMLTextNode(text)), textSeparator);
}

從原始碼我們可以看到,對於空文字節點,React會直接return出去,不會去生成文字例項(TextInstance)。

相關文章