Skip to content

注意事项与虚拟dom和真实dom 的区别(Day1 4/4) ​

本页对应 React + TypeScript + 医通项目,共 6 段视频,约 52 分钟。

学习目标 ​

  • 能针对单分支、双分支和多分支场景,分别使用逻辑与运算、三元表达式、函数封装或 if/else 实现条件渲染,并说明各写法的适用场景。
  • 能使用 style 行内样式对象为 React 元素设置样式,正确采用小驼峰属性名,并对 px 单位的数值省略单位。
  • 能使用 className 添加单个或多个样式类,并能将样式类数组通过 join(' ') 转为字符串后赋给 className。
  • 能使用 map 将普通对象数组映射为 React 元素数组,并通过插值表达式渲染为 ul/li 列表,正确组织 li 内的嵌套结构。
  • 能在列表渲染中结合条件渲染处理空数组,显示“暂无评论”等占位内容,并依据给定评论数据完成静态列表作业。
  • 能说明虚拟 DOM 与真实 DOM 的区别,并能使用 React.createElement 创建指定结构的 React 元素。

知识点 ​

先把页面骨架搭起来 ​

这一节讨论的是 ReactDOM.createRoot() 的三个使用注意事项。在看这三条之前,先把最小可运行的页面准备好:引入 React 和 ReactDOM 的开发版脚本,再准备一个挂载容器。

html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="./lib/react.development.js"></script>
    <script src="./lib/react-dom.development.js"></script>
</head>
<body>
    <div id="root"></div>
</body>
<script>
    // const root = ReactDOM.createRoot(document.querySelector("#root"));
    
    // root.render((
    //     <div></div>
    // ))
</script>
</html>

为什么需要:React 本身只负责“算”出界面长什么样,真正把结果放进浏览器页面的是 react-dom。所以必须有两个脚本,缺一个 ReactDOM 就是 undefined。

怎么做:createRoot(容器) 拿到根对象,root.render(内容) 把内容渲染进容器。

易错点:上面注释掉的 <div></div> 是 JSX 写法,浏览器不认识,需要 Babel 之类的编译步骤才能直接跑。如果只是验证本节的三条注意事项,用字符串(如 root.render('第一次渲染'))就够了,不必引入编译工具。

注意事项一:DOM 元素和 React 根节点必须一一对应 ​

js
// 1. dom容器 和 react的根节点需要是一一对应的,虽然可以正常渲染,但是控制台会有警告
const root1 = ReactDOM.createRoot(document.querySelector("#root"));
const root2 = ReactDOM.createRoot(document.querySelector("#root"));

root2.render('root2')
root1.render('root1')

为什么需要:createRoot 会在容器上登记“我已经被某个根接管了”。一个容器只应该有一个主人,否则 React 内部对这块 DOM 的更新、卸载、事件委托都会出现两套账本,谁也不知道该听谁的。

怎么想:把 createRoot 理解成“认领”动作。同一个 #root 被认领两次,第二个根并不知道第一个根的存在,两边各自渲染,DOM 就被反复改写。

怎么做:一个容器只调用一次 createRoot,把返回的 root 存到变量里复用;需要多个根,就准备多个容器(如 #root1、#root2)。

易错点:这段代码“看起来能跑”,页面上确实会出现内容,但控制台会给出警告——这正是最容易被忽略的地方。开发阶段一定要养成看控制台的习惯,别只看页面有没有东西。另外注意 createRoot 的参数是 DOM 元素,document.querySelector("#root") 返回的正是元素,不要传成 "#root" 字符串。

注意事项二:render 可以重复调用,后面的覆盖前面的 ​

js
// 2. render函数可以重复调用,后面的会覆盖前面的
const root = ReactDOM.createRoot(document.querySelector("#root"));
root.render('第一次渲染');
root.render('第二次渲染');

为什么需要:真实项目里界面会随数据变化。React 的设计就是“给同样的根,不断喂新的描述”,由 React 去对比、更新,而不是让你手动去删节点、加节点。

怎么想:render 不是“追加”,而是“声明当前应该长什么样”。可以把它当成“把整个容器的内容重新声明一遍”。

怎么做:先 createRoot 一次,之后想更新就再调 root.render(...)。页面上最终只会看到“第二次渲染”。

易错点:新手容易误以为多次 render 会累加,于是拼命在 render 外面拼字符串。另外不要每次更新都重新 createRoot,那就又踩回第一条的坑了。

注意事项三:不要用 body 和 html 作为根节点 ​

js
// 3. 根节点不要使用 body 和 html
// const root = ReactDOM.createRoot(document.body); // body
const root = ReactDOM.createRoot(document.documentElement); // html
root.render('第一次渲染');

为什么需要:createRoot 的容器会被 React 当作自己的“地盘”,渲染时容器里的内容由 React 掌管。body 和 html 是浏览器文档结构的根,里面还有 script 等由浏览器解析、执行的内容,交给 React 托管容易冲突,也会出现警告。

怎么想:React 只应该接管页面中的“一小块”,这一块最好是你自己写的一个空 div,位置、层级都由你控制。

怎么做:老老实实写 <div id="root"></div>,把 document.querySelector("#root") 传进去。

易错点:有人图省事写 document.body,甚至写 document.documentElement(就是上面被注释掉的那一行)。页面可能仍会渲染出文字,但这是不推荐的做法。另外注意 document.body 是元素本身,不要再套 querySelector。

三条合起来的正确姿势 ​

把三条注意事项收拢成一套固定写法:

html
<body>
    <div id="root"></div>
</body>
<script>
    const root = ReactDOM.createRoot(document.querySelector("#root"));
    root.render('第一次渲染');
    root.render('第二次渲染'); // 覆盖上面这次
</script>

一句话记忆:容器唯一、根只建一次、render 可反复调、容器别用 body/html。这三条本身不复杂,但它们决定了后面所有渲染代码的写法基础,先养成习惯,后面写组件时就不会莫名其妙冒出警告。

先看结论:真实 DOM 与虚拟 DOM 的分工 ​

为什么需要:笔记先给出一个前提:操作真实 DOM 时,如果改变 DOM 元素的几何属性,会导致自身及相关元素位置重新计算,这个过程叫重排;重排后重新渲染的过程叫重绘。直接操作真实 DOM 容易带来大量重排和重绘,效率较低。
怎么想:React 引入虚拟 DOM 的概念,是为了减少页面的重排和重绘。注意笔记的表述是“减少”,不是“完全避免”。
怎么做:把“用真实 DOM 直接描述页面”改成“先用虚拟 DOM 描述,再由 React 转成真实 DOM”。虚拟 DOM 在 React 中也叫 React 元素。
易错点:不要把虚拟 DOM 当成真实 DOM;也不要把“减少重排重绘”理解成“没有重排重绘”。

真实 DOM 操作慢在哪里:重排与重绘 ​

为什么需要:理解重排、重绘,才能理解为什么要引入虚拟 DOM。笔记明确说:改变 DOM 元素的几何属性,会导致自身及相关元素位置重新计算,这个重新计算叫重排,重排后重新渲染叫重绘。
怎么想:真实 DOM 的属性很多,笔记说真实 DOM 本质也是一个对象,属性较多,200 多个。属性多、直接操作页面结构,就容易触发页面重新计算。
怎么做:在 React 中不要一上来就想着“拿到真实 DOM 改一改”,而是先写 React 元素,让 React 去处理最终渲染。
易错点:真实 DOM 不是“不是对象”,它也是对象,只是属性多;虚拟 DOM 也不是“假的页面”,它是描述页面的对象。

虚拟 DOM 的本质:一个属性较少的对象 ​

为什么需要:如果虚拟 DOM 很复杂,就失去“轻量描述”的意义。笔记用 5 条说明它的本质。
怎么想:虚拟 DOM 本质就是一个对象,属性较少,笔记说 9 个属性;虚拟 DOM 有的属性和真实 DOM 的属性是一一对应的;虚拟 DOM 通过 render 方法可以转化为真实 DOM;在 React 中虚拟 DOM 也叫 React 元素。
怎么做:在 React 中创建虚拟 DOM 有两种方式:

  1. React.createElement
  2. jsx 创建

易错点:不要把“9 个属性”套到所有真实 DOM 上;真实 DOM 属性有 200 多个。虚拟 DOM 属性少,是因为它只描述 React 渲染需要的关键信息。

虚拟 DOM 与真实 DOM 的属性对应关系 ​

为什么需要:虚拟 DOM 要能转成真实 DOM,就必须和真实 DOM 的属性存在对应关系。
怎么想:笔记说“虚拟 DOM 有的属性和真实 DOM 的属性是一一对应的”,这说明虚拟 DOM 不是随意对象,而是按真实 DOM 需要来描述的。
怎么做:写 JSX 或 React.createElement 时,你写的标签、属性、子节点,最终会形成 React 元素;React 再通过 render 转成真实 DOM。
易错点:不要以为虚拟 DOM 属性少就等于“信息丢失”;它保留的是渲染所需信息。真实 DOM 属性多,是它作为浏览器对象的特点。

虚拟 DOM 如何变成真实 DOM:render 与 React 元素 ​

为什么需要:只创建虚拟 DOM 不会自动出现在页面上,必须经过转换。
怎么想:笔记说“虚拟 DOM 通过 render 方法,可以转化为真实 DOM”;在 React 中虚拟 DOM 也叫 React 元素。转写稿里多次说“插值表达式也可以渲染 React 元素”“列表渲染是把普通数据数组 map 成 React 元素数组,再用插值表达式渲染”。这里“差值表达式”应纠正为“插值表达式”,“reactive 元素”应纠正为“React 元素”。
怎么做:用 React.createElement 或 JSX 创建 React 元素,再交给 React 渲染。转写稿中列表渲染的例子说明:普通数组要先用 map 映射成 React 元素数组,再插值渲染;这正是“先描述、后渲染”的思路。
易错点:转写稿里 class 样式用数组直接插值会变成逗号连接,不符合 CSS 语法,要用 join(' ') 用空格连接;style 行内样式要传对象,复合属性用小驼峰,px 可省略单位。这些调试现象说明:React 元素最终仍要落成真实 DOM 的字符串样式和结构。

从转写稿看调试:React 元素最终仍落到真实 DOM ​

为什么需要:理解虚拟 DOM 不是悬空概念,最终要看真实 DOM 的结果。
怎么想:转写稿中样式最终“渲染成真实 DOM 的时候,就是字符串的行内样式,并且加上单位”;列表最终渲染成 ul 里的 li。这说明 React 元素是中间描述,真实 DOM 是最终结果。
怎么做:写样式时,行内样式用对象;类名可用字符串,也可用数组后 join(' ');列表用 map 生成 React 元素数组再插值。
易错点:转写稿提到列表 key 警告与 diff 算法有关(转写稿里的“def 算法”应纠正为 diff 算法),但本节先不展开。不要因为页面能显示就忽略警告;也不要直接把普通数组塞进 class 属性。

小结:记住三句话 ​

为什么需要:直接操作真实 DOM 容易带来大量重排重绘,效率较低。
怎么想:虚拟 DOM 是属性较少的对象,是 React 元素,和真实 DOM 属性一一对应,通过 render 转成真实 DOM。
怎么做:用 React.createElement 或 JSX 创建虚拟 DOM;真实 DOM 本质也是对象,只是属性有 200 多个。
易错点:虚拟 DOM 不是真实 DOM;减少重排重绘不等于完全没有;创建方式只有笔记列出的两种。本节笔记未附图片与代码块,以上保留原要点并用行内代码标注关键字。

先明确:createElement 到底在创建什么 ​

为什么需要:在不使用 JSX 的情况下,React 也要能创建“页面结构”。React.createElement 就是 React 提供的创建 React 元素的方法。它创建出来的不是真实 DOM,而是一个描述页面结构的对象,也就是常说的虚拟 DOM。

怎么想:先记住函数签名:React.createElement(标签名, 标签属性, 子元素1, 子元素2, ...)。第一个参数决定标签类型,第二个参数是属性对象,后面的参数都是子元素。创建完成后,还要通过 root.render 渲染到页面。

怎么做:看笔记中的代码,oDiv 是一个对象,里面有 type(标签名)、props(属性)、props.children(子元素)。root.render(oDiv) 才会把 React 元素渲染到页面。

js
const root = ReactDOM.createRoot(document.querySelector("#root"));
/**
 * 创建react元素[虚拟dom]
 * create:创建
 * Element:元素
 * React.createElement(标签名,标签属性, 子元素1, 子元素2,....)
 * 
 */
const oDiv = React.createElement('div',{id:'box', school:'atguigu'},'我是div','我真的是div')

console.log('oDiv: ', oDiv);// oDiv就是一个对象  type是标签名 props属性 props.children子元素

root.render(oDiv);// 通过render方法,将react元素渲染到页面
// 如果没有属性,怎么创建?第二个参数需要占位 {}、null、undefined

// const oSpan = React.createElement('span',{},'我是span标签');
// const oSpan = React.createElement('span',null,'我是span标签');
// const oSpan = React.createElement('span',undefined,'我是span标签');

// 用任何值占位都可以,但是不推荐,推荐以上三种
// const oSpan = React.createElement('span',[],'我是span标签');
// const oSpan = React.createElement('span',1231232132435,'我是span标签');
root.render(oSpan);

易错点:oDiv 不是真实 DOM,不能直接对它做 appendChild 之类的 DOM 操作;真正上屏要靠 render。调试时可以用 console.log(oDiv) 观察对象结构,确认 type、props、props.children 是否符合预期。

第二个参数:没有属性时为什么要占位 ​

为什么需要:createElement 的参数位置是固定的。第二个参数专门放属性,第三个参数开始才是子元素。如果某个标签没有属性,也不能随便把子元素挪到第二个参数的位置,否则参数含义就乱了。

怎么想:没有属性时,第二个参数仍然要占位。笔记推荐用 {}、null、undefined 三种方式。它们都能表达“这里没有属性”,其中空对象最直观,null 和 undefined 也常见。

怎么做:例如创建 span 时写成 React.createElement('span', null, '我是span标签')。注意子元素仍然放在第三个参数。

易错点:笔记里说“用任何值占位都可以,但是不推荐”,并举例 []、数字等。学习时不要因为“能跑”就随便写,推荐固定使用 {}、null、undefined,代码更清晰。另一个易错点是漏写占位参数,导致子元素和属性位置混淆。

特殊属性 className:为什么不是 class ​

为什么需要:给元素加类名时,HTML 里通常写 class,但在 React 创建元素时要用 className。原因是 class 是 ES6 定义类的关键字;同时虚拟 DOM 的属性最终要对应真实 DOM 的属性,而真实 DOM 用的就是 className。转写稿里也强调:“虚拟dom真实dom属性一一对应的,虚拟dom最终会转化为真实dom,所以真实dom用的是className。”

怎么想:写 createElement 的属性对象时,把类名写成 className: 'wrapper',而不是 class: 'wrapper'。渲染后,真实 DOM 上会体现为 className。

怎么做:笔记代码如下:

js
const root = ReactDOM.createRoot(document.querySelector("#root"));
    // 属性:普通属性、自定义属性
    const oDiv = React.createElement('div', { id: 'box', school: 'atguigu',className:'wrapper' }, '内容');

    /**
     * 特殊属性className
     * class 是 es6 定义类的关键字,为什么clas使用className?
     * 虚拟dom真实dom属性一一对应的,虚拟dom最终会转化为真实dom,所以真实dom用的是className
     * 
     */
    root.render(oDiv)

	// 真实dom用的就是className
    console.dir(document.querySelectorAll('.rapper')[0]);

易错点:第一,属性对象里写成 class 不符合这里的要求,应使用 className。第二,调试真实 DOM 时选择器要和类名一致:上面 className:'wrapper',查询时应写 .wrapper;笔记代码中写的是 .rapper,少了一个 w,实际会选不到元素,[0] 可能是 undefined。这正是调试时要检查的地方:先 console.log(oDiv) 看虚拟 DOM 的 props.className,再用 console.dir 看真实 DOM 的 className,两边对照。

从弊端理解:为什么后面要引入 JSX ​

为什么需要:createElement 能创建元素,但当页面结构复杂、标签层层嵌套时,每个节点都要写一次函数调用,属性和子元素的位置很容易看错。笔记用红色标出弊端:“当创建复杂页面结构,标签需要嵌套的时候,写法非常的麻烦,所以,引入了jsx语法,用来快速创建复杂页面的结构的react元素。”

怎么想:简单结构用 createElement 便于理解原理;复杂结构继续手写会降低可读性。先掌握 createElement,再理解 JSX 是为了更快速地描述同样结构的 React 元素。

怎么做:本节先把单个 div、span 的创建、属性占位、className 和 render 练熟。遇到嵌套结构时,要意识到每个子元素仍然对应 createElement 的参数位置,不能把属性和子元素混在一起。

易错点:不要误以为 createElement 创建的是页面上的真实标签;它是对象描述,最终由 render 转换并渲染。也不要把“没有属性”写成“没有第二个参数”,推荐用 {}、null、undefined 占位。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

示例 1:根节点注意事项 + createElement 创建 react 元素 ​

html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>注意事项 & createElement</title>
    <script src="./lib/react.development.js"></script>
    <script src="./lib/react-dom.development.js"></script>
</head>
<body>
    <div id="root"></div>
</body>
<script>
    // 1. dom容器 和 react的根节点需要是一一对应的,虽然可以正常渲染,但控制台会有警告
    // const root1 = ReactDOM.createRoot(document.querySelector("#root"));
    // const root2 = ReactDOM.createRoot(document.querySelector("#root"));
    // root2.render('root2')
    // root1.render('root1')

    // 2. render函数可以重复调用,后面的会覆盖前面的
    // const root = ReactDOM.createRoot(document.querySelector("#root"));
    // root.render('第一次渲染');
    // root.render('第二次渲染');

    // 3. 根节点不要使用 body 和 html
    // const root = ReactDOM.createRoot(document.body);
    // const root = ReactDOM.createRoot(document.documentElement);

    const root = ReactDOM.createRoot(document.querySelector("#root"));

    /**
     * 创建react元素[虚拟dom]
     * React.createElement(标签名, 标签属性, 子元素1, 子元素2, ...)
     */
    const oDiv = React.createElement(
        'div',
        { id: 'box', school: 'atguigu', className: 'wrapper' },
        '我是div',
        '我真的是div'
    );

    console.log('oDiv: ', oDiv); // oDiv就是一个对象:type是标签名,props是属性,props.children是子元素
    root.render(oDiv);           // 通过render方法,将react元素渲染到页面

    // 如果没有属性,第二个参数需要占位 {}、null、undefined
    // const oSpan = React.createElement('span', {}, '我是span标签');
    // const oSpan = React.createElement('span', null, '我是span标签');
    // const oSpan = React.createElement('span', undefined, '我是span标签');
    // root.render(oSpan);
</script>
</html>

关键行与易错点

  • createRoot 一个容器只调用一次,调用两次会渲染成功但控制台报警告,根节点与容器必须一一对应。
  • render 可以重复调用,后一次会覆盖前一次,不是追加。
  • 根节点不要用 body / html,用页面里的一个 div 容器。
  • 没有属性时第二个参数必须占位,推荐 {}、null、undefined;用 []、数字也能跑但不推荐。
  • 属性里类名用 className,因为虚拟 dom 和真实 dom 属性一一对应,真实 dom 用的就是 className。
  • 自定义属性(如 school)可以直接挂在 props 上,console.log 出来能看到 type / props / props.children。

示例 2:条件渲染 + style 行内样式 + className 类名 ​

html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>条件渲染 & 样式处理</title>
    <script src="./lib/react.development.js"></script>
    <script src="./lib/react-dom.development.js"></script>
    <script src="./lib/babel.min.js"></script>
    <style>
        .box { width: 200px; height: 100px; border: 1px solid blue; }
        .c1 { color: red; }
        .f1 { font-size: 40px; }
    </style>
</head>
<body>
    <div id="root"></div>
</body>
<script type="text/babel">
    const root = ReactDOM.createRoot(document.querySelector("#root"));

    // 单分支:逻辑与
    const flag = true;

    // 双分支:三元表达式
    const isLoading = true;

    // 多分支:封装成函数,通过返回值给 react 元素
    function fn(age) {
        if (age < 18) {
            return <div>请在父母的陪同下观看</div>;
        } else if (age >= 18 && age < 60) {
            return <div>敬请欣赏</div>;
        } else if (age >= 60 && age < 80) {
            return <div>保重身体</div>;
        }
    }

    // style 行内样式:值必须是一个对象,复合属性用小驼峰,px 单位可省略
    const styleObj = {
        color: 'red',
        border: '1px solid blue',
        backgroundColor: 'pink',
        width: 200,
        height: 100,
        fontSize: 30
    };

    // className:多个类名用数组存,再用 join(' ') 拼成字符串
    const class1 = ['box', 'c1', 'f1'];

    const oDiv = (
        <div>
            {flag && <h3>一起去看电影</h3>}
            {isLoading ? <h3>页面正在加载中...</h3> : <div>我是页面内容</div>}
            {fn(16)}

            <div style={styleObj}>行内样式</div>
            <div className={class1.join(' ')}>class 样式</div>
        </div>
    );

    root.render(oDiv);
</script>
</html>

关键行与易错点

  • 单分支用 flag && react元素:条件为真渲染内容,为假时整个表达式值为 false,插值里什么都不渲染。
  • 双分支用三元 isLoading ? A : B;多分支封装成函数,在函数里写 if / else if / return,用返回值当 react 元素。
  • style 的值必须是对象,所以是 style={styleObj};写成 style={{color:'red'}} 时外层花括号是插值,内层才是对象。
  • 复合属性要小驼峰:backgroundColor、fontSize,写成 background-color 无效。
  • 单位是 px 时可以只写数字(width: 200、fontSize: 30),写了带引号的字符串也对。
  • className 传数组时会被用逗号连接成 "box,c1,f1",不符合 CSS 语法导致样式失效,必须 .join(' ')。
  • 变量需要重新赋值时用 let,否则用 const(const 重新赋值会报 Assignment to constant variable)。
  • JSX 需要 babel 编译:页面引入 babel.min.js 并把 script 的 type 改成 text/babel。

示例 3:列表渲染(简单数组 + 对象数组) ​

html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>列表渲染</title>
    <script src="./lib/react.development.js"></script>
    <script src="./lib/react-dom.development.js"></script>
    <script src="./lib/babel.min.js"></script>
</head>
<body>
    <div id="root"></div>
</body>
<script type="text/babel">
    const root = ReactDOM.createRoot(document.querySelector("#root"));

    // 简单数据类型数组
    const arr1 = [1, 2, 3, 4];

    // 复杂数据类型数组(服务端返回的数据)
    const users = [
        { id: 1, userName: '爱的硅谷', age: 19 },
        { id: 2, userName: '迪丽热巴', age: 29 },
        { id: 3, userName: '热一热', age: 33 }
    ];

    const oDiv = (
        <div>
            {/* 简单数组:直接渲染是逗号连接,所以要用 map 映射成 react 元素数组 */}
            <ul>
                {arr1.map((item) => (
                    <li>{item}</li>
                ))}
            </ul>

            {/* 对象数组:map 映射成 react 元素数组,再用插值表达式渲染 */}
            <ul>
                {users.map((user) => (
                    <li>
                        <h3 style={{ color: 'red' }}>姓名:{user.userName}</h3>
                        <p>年龄:{user.age}</p>
                    </li>
                ))}
            </ul>
        </div>
    );

    root.render(oDiv);
</script>
</html>

关键行与易错点

  • 列表渲染的原理:把普通数据的数组用 map 映射成 react 元素的数组,再用插值表达式渲染。直接渲染 arr1 会用逗号连接,不能形成 <li> 结构。
  • map 返回的新数组,其元素由回调函数的返回值决定;忘记 return 会得到一堆 undefined。
  • 箭头函数只有一行且直接返回 react 元素时,用圆括号包裹 ( <li>...</li> ),省掉 return 和花括号;结构复杂时圆括号更好维护。
  • 渲染的值必须放在插值表达式里:<li>{item}</li>,写成 <li>item</li> 只会输出字符串。
  • 对象数组要取属性:user.userName、user.age,别忘了插值花括号。
  • 映射时同样能加样式:行内用 style={{ color: 'red' }},类名用 className={...}。
  • 控制台会提示每个子元素需要唯一 key(Each child in a list should have a unique "key" prop),示例中先不管,后续再处理。
  • react 里 <p> 标签不能包 <div>,会报错并给警告,但 <div> 可以包 <p>。

常见错误 ​

1. 对同一个容器调用两次 createRoot

  • 现象:页面能渲染,但控制台报警告,两次 render 结果互相覆盖,只剩最后一个。
  • 原因:DOM 容器与 React 根节点必须一一对应,第二次创建会接管同一个容器。
  • 修法:全局只创建一次 root,后续所有更新都复用这个 root.render(...)。

2. 把 body 或 html 当作根节点

  • 现象:控制台出现警告,页面结构/样式容易被 React 接管后搞乱。
  • 原因:React 会整体接管根节点内部内容,body/html 属于文档级容器,还可能被第三方脚本操作。
  • 修法:在 body 内放一个专属容器(如 <div id="root"></div>)作为根节点。

3. 以为 render 是追加,连续调用想"加内容"

  • 现象:第二次 render 后第一次的内容消失。
  • 原因:render 是覆盖语义,页面最终内容 = 最后一次传入的元素树。
  • 修法:要多个内容就组织进同一棵元素树(包一层 div 或用 <>...</>),一次 render。

4. style 写成字符串,或属性名用连字符

  • 现象:style="color:red" 直接报错;写 { 'background-color': 'pink' } 样式不生效。
  • 原因:style 的值必须是对象,且 CSS 属性名要用小驼峰。
  • 修法:style={{ color: 'red', backgroundColor: 'pink' }};px 单位可省略写数字 width: 200,写成字符串则必须带单位 '200px'。

5. class 写成 class=,或把数组直接塞给 className

  • 现象:前者报警告且类名不生效;后者渲染成 box,c1,f1(逗号连接),CSS 全部失效。
  • 原因:JSX 属性名是 className;数组插值默认用逗号拼接,不符合 CSS 语法。
  • 修法:className={arr.join(' ')},或直接写模板/字符串 className={&#96;box ${c1}&#96;}。

6. map 里用了花括号却忘了 return

  • 现象:列表渲染出来是空白,或数组里全是 undefined。
  • 原因:{ } 是函数体而非返回值,箭头函数带花括号必须显式 return。
  • 修法:改成圆括号直接返回 list.map(item => (<li>{item.name}</li>)),或补 return。

7. 直接把对象数组插值渲染

  • 现象:报错 Objects are not valid as a React child。
  • 原因:插值能渲染的是字符串/数字/元素/元素数组,对象不行。
  • 修法:先 map 映射成 React 元素数组再插值。

8. 在 JSX 里写 if 语句做条件渲染

  • 现象:编译/运行报错,或条件不生效。
  • 原因:JSX 的 {} 里只能放表达式,不能放语句。
  • 修法:单分支用 cond && <X/>,双分支用三元,多分支封装成函数调用 renderByAge(age);复杂判断可以先算好变量再插值。

9. 用 const 声明条件渲染的结果变量后又重新赋值

  • 现象:Assignment to constant variable。
  • 原因:if/else 分支里对 const 变量二次赋值。
  • 修法:需要重新赋值的用 let;能用 const 的(含引用类型内部修改)优先 const。

10. <p> 里嵌 <div>

  • 现象:页面照常渲染,但控制台报 validateDOMNesting 警告。
  • 原因:p 是段落型标签,内容模型不允许块级元素。
  • 修法:换容器标签(div/section),或把 div 提到 p 外面。

练习 ​

练习 1|三种条件渲染一次写全 用同一份数据 const loading = true; const age = 24;,在一个 root.render 里分别实现:

  • 单分支:loading 为真时显示"加载中…",为假时什么都不渲染(用 &&);
  • 双分支:loading 为真显示"加载中…",否则显示"我是页面内容"(用三元);
  • 多分支:把年龄判断(<18 请在父母陪同下观看 / 18~60 敬请欣赏 / >60 保重身体)封装成函数,插值调用。 然后分别把 loading 改成 false、age 改成 16 和 70 验证。

练习 2|给一个卡片加行内样式 创建一个 div,要求:宽 200、高 100、背景粉色、文字红色、字号 30、边框 1px solid blue。 要求全部用 style 对象写,并说明哪些地方可以省略 px、哪些必须写成字符串。 (进阶)把这个 style 对象抽成变量 const cardStyle = {...},再用 style={cardStyle} 复用给第二个 div。

练习 3|类名数组拼接 定义 .box{width:200px;height:100px;border:1px solid #333}、.c1{color:red}、.f1{font-size:40px}。 准备一个数组 const cls = ['box', 'c1', 'f1']:

  • 直接 className={cls} 观察渲染结果,解释为什么失效;
  • 改成正确写法让三个类名同时生效;
  • 再写一段逻辑:当某个布尔变量 isRed 为 false 时把 'c1' 从数组里去掉,保证拼接后不多出空格。

练习 4|列表渲染 + 空态 给定数据:

js
const comments = [
  { id: 1, name: 'jack', content: '今天天气不错' },
  { id: 2, name: 'rose', content: '楼主说得对' }
];

要求渲染成 ul > li,每个 li 内是 <a> 包裹 <h3>(用户名,行内样式红色)和 <p>(内容,className 控制字号 20px)。 同时满足:comments.length === 0 时渲染"暂无评论",有数据时才渲染列表。 最后把 comments 清空,验证空态是否生效。

练习 5|找错 下面代码有 6 处问题,逐条指出并改正:

js
const root = ReactDOM.createRoot(document.querySelector('#root'));
const root2 = ReactDOM.createRoot(document.querySelector('#root'));
const list = [];
const App = (
  <div class="box" style="color:red">
    <p><div>标题</div></p>
    {list.map(item => { <li>{item.name}</li> })}
  </div>
);
root.render(App);

原视频对照 ​

#视频时长
1条件渲染16:33
2style行内样式04:34
3style行内样式补充说明00:45
4class样式处理07:17
5列表渲染20:12
6作业03:02

小结 ​

  1. 根节点是"入口资源":一个容器只 createRoot 一次,拿到 root 后长期复用,重复创建只会制造警告和互相覆盖。
  2. render 是覆盖语义——页面长什么样,完全由这一次传入的元素树决定,所以"要多块内容"靠组织树结构,不靠多次调用。
  3. 样式只有两条路:style(对象、小驼峰、px 可省数字)和 className(字符串、多类名靠 join(' ')),两者都不接受"原生 HTML 写法"。
  4. 条件渲染的本质是"表达式求值",不是"写语句":&&、三元、函数返回值三者覆盖单/双/多分支,遇到复杂判断就抽函数,别在 JSX 里硬塞。
  5. 列表渲染 = 数据数组 →(map)→ 元素数组 → 插值,map 一一对应的特性天然适配;return 漏写是最高频翻车点。
  6. 变量声明默认 const,报"重新赋值"错误再改 let;引用类型内部修改不影响地址,仍然用 const。
  7. 控制台警告不是噪音:validateDOMNesting、className、重复根节点、key 缺失,都是真实缺陷的早期信号,看到就当场修,别攒。