React Diff “陨石坑”填坑记

某一周周五,18:00整,“需求终于提测了,待会儿上线后就可以开启美妙的周末时光,听说xx开了家老北京火锅店,味道还行,晚上要去试试”,小光同学憧憬着。
“bi~,小光,boe环境官网备案号的跳转出错了,本来应该是跳到备案系统的,现在跳到营业执照页去了”,QA同学发来一条信息。
“不应该啊,我只修改了样式啊,没有改逻辑,我看看吧”,小光同学开始查看代码……嗯嗯,代码看起来没问题~
再看看html源,咦?怎么备案号a标签的href变成了营业执照的了?!
然后……晚上10点……小光同学看着周围空荡荡的座位……自闭了~

本示例代码只保持逻辑结构一致,内容经脱敏处理,代码使用的react、ts版本:React16.19.8,typescript3.9.3

问题表现

原始代码:

import React from 'react';
import './index.less';

export default function Footer(): JSX.Element {
  const currentYear: number = new Date().getFullYear();
  return (
    <div className='footer__info custom-min-width'>
      <div className='footer__info__title'>
        {currentYear}这是1号TextNode
        <a
          href='https://www.google.com/'
          target='_blank'
          className='google'
          key='google'
        >
          www.google.com/
        </a>
        这是2号TextNode
        <a
          href='https://www.baidu.com/'
          target='_blank'
          className='baidu'
          key='baidu'
        >
          www.baidu.com/
        </a>
        这是3号TextNode
        <a
          href='https://www.douyin.com/'
          target='_blank'
          className='douyin'
          key='douyin'
        >
          www.douyin.com/
        </a>
      </div>
    </div>
  );
}
复制代码

代码效果:

image.png

期望(正确)html内容:

image.png

实际(错误)html内容:

image.png

问题分析

SSR渲染是否正确?

答:正确

image.png

简单CSR是否正确?

答:正确

SSR+CSR是否正确?

答:错误

image.png

好吧,问题就在SSR之后的CSR阶段。
CSR做了什么呢?客户端重新渲染。
谁做的呢?react。
react采用什么策略生成virtual dom tree呢?react diff。
首先看一下index.tsx编译后的产物,执行:

tsc --jsx react --module ESNext index.tsx
复制代码

产物:

import React from 'react';
import './index.less';
export default function Footer() {
    var currentYear = new Date().getFullYear();
    return (React.createElement("div", { className: 'footer__info custom-min-width' },
        React.createElement("div", { className: 'footer__info__title' },
            currentYear,
            "\u8FD9\u662F1\u53F7TextNode",
            React.createElement("a", { href: 'https://www.google.com/', target: '_blank', className: 'google', key: 'google' }, "www.google.com/"),
            "\u8FD9\u662F2\u53F7TextNode",
            React.createElement("a", { href: 'https://www.baidu.com/', target: '_blank', className: 'baidu', key: 'baidu' }, "www.baidu.com/"),
            "\u8FD9\u662F3\u53F7TextNode",
            React.createElement("a", { href: 'https://www.douyin.com/', target: '_blank', className: 'douyin', key: 'douyin' }, "www.douyin.com/"))));
}
复制代码

React virtual dom tree结构:

image.png

SSR产物dom tree结构:

image.png

可能的Diff产生过程

以下Diff过程为笔者推测,不保证正确性

UML 图.jpg

问题修复

既然知道问题的可能原因是复用了错误的Fiber Node导致,那就从源头上阻止这种复用,如何阻止呢?
答:让第一个<a>节点和其他<a>节点“不一样”即可!因此这里用<span>标签包裹第一个<a>标签,让其和后续<a>标签不“相等”。
修复代码如下:

import React from 'react';
import './index.less';

export default function Footer(): JSX.Element {
  const currentYear: number = new Date().getFullYear();

  return (
    <div className='footer__info custom-min-width'>
      <div className='footer__info__title'>
        <span>{currentYear}这是1号TextNode </span>
        <a
          href='https://www.google.com/'
          target='_blank'
          className='google'
          key='google'
        >
          www.google.com/
        </a>
        这是2号TextNode
        <a
          href='https://www.baidu.com/'
          target='_blank'
          className='baidu'
          key='baidu'
        >
          www.baidu.com/
        </a>
        这是3号TextNode
        <a
          href='https://www.douyin.com/'
          target='_blank'
          className='douyin'
          key='douyin'
        >
          www.douyin.com/
        </a>
      </div>
    </div>
  );
}
复制代码

总结

上面问题的产生带有一定巧合,首先是dom结构高度相似的巧合,其次是SSR吐出的HTML结构和React渲染的结构出现分歧。再加上react本身的TextNode和React Element diff策略的差异。在这几种巧合的共同作用下,出现了渲染问题。
由此可以知道:

  1. element添加key只在react的控制域下有效,一旦脱离react作用域,key将丢失;
  2. react对每一个“{}”表达式都会生成一个独立的Fiber Node;
  3. 需要注意dom 树中同一层次下的节点,让出现相同类型的节点时,其很有可能被diff策略所复用。
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享