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