某一周周五,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>
);
}
复制代码
代码效果:
期望(正确)html内容:
实际(错误)html内容:
问题分析
SSR渲染是否正确?
答:正确
简单CSR是否正确?
答:正确
SSR+CSR是否正确?
答:错误
好吧,问题就在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结构:
SSR产物dom tree结构:
可能的Diff产生过程
以下Diff过程为笔者推测,不保证正确性
问题修复
既然知道问题的可能原因是复用了错误的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策略的差异。在这几种巧合的共同作用下,出现了渲染问题。
由此可以知道:
- element添加key只在react的控制域下有效,一旦脱离react作用域,key将丢失;
- react对每一个“{}”表达式都会生成一个独立的Fiber Node;
- 需要注意dom 树中同一层次下的节点,让出现相同类型的节点时,其很有可能被diff策略所复用。