Skip to content

列表渲染与jsx中的事件(Day2 3/4) ​

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

学习目标 ​

  • 能使用 JSX 插值表达式完成列表渲染与事件绑定,并解释事件回调函数的调用者、this 指向、事件对象获取方式及传参写法。
  • 能分别编写 JSX 中的单行注释、多行注释和行内属性注释,并说明单行注释在 JSX 中需换行以保留插值表达式边界的原因。
  • 能使用原生 DOM 的 DocumentFragment 批量创建并插入多个 li,说明其减少重排重绘的原理,并对比直接循环 appendChild 的性能差异。
  • 能使用 React.Fragment、解构后的 Fragment 和空标签 <>...</> 三种方式消除多余根节点,并说明空标签不能写属性、需要属性时改用 Fragment 的适用场景。
  • 能正确使用对象解构和数组解构提取嵌套数据与指定位置元素,能区分解构赋值与扩展运算符的作用,并写出对应示例。
  • 能说明 React 组件由结构、逻辑、样式及必要静态资源组成,能按功能拆分待办事项界面为多个组件,并区分类组件与函数组件及当前主流开发方式。

知识点 ​

先看需求:为什么需要列表渲染 ​

页面里的列表数据通常来自数组,例如 [1,2,3,4],或者一组用户对象。如果手写多个 <li>,数据一多就难维护;数据变化时还要手动改结构。因此 React 列表渲染的目标是:让页面结构由数据数组自动生成。转写稿里反复提到 JSX 的“插值表达式”,这里纠正为“插值表达式”,它里面放的是 JS 表达式;列表渲染正是利用这一点,把“数据数组”先变成“React 元素数组”,再交给插值表达式渲染。

核心思路:用 map 把数据数组映射成 React 元素数组 ​

为什么用 map?因为 map 会返回一个新数组,新数组长度与原数组一致,每个元素由回调返回值决定。怎么想?先把普通数据数组 arr1 映射成 React 元素数组 arr3。怎么做?先写完整回调,调试时打印 item 和 arr3,确认每一步结果:

jsx
let arr1 = [1, 2, 3, 4];

let arr3 = arr1.map(item => {
    console.log('item: ', item); // 遍历时数组中的每一项
    return (
        <li>{item}</li>
    );
});
console.log(arr3);

确认无误后,可以省略 return 和花括号,写成更简洁的箭头函数:

jsx
let arr4 = arr1.map(item => (
    <li>{item}</li>
));

最终常见写法是直接在 JSX 中映射:

jsx
<ul>
    {arr1.map(item => (
        <li>{item}</li>
    ))}
</ul>

易错点:map 回调必须有返回值;箭头函数直接返回 JSX 时要用圆括号包住;不要写成 {arr1.map(item => <li>{item}</li>)} 后又忘记整体放在插值表达式里。

插值表达式为什么能接住数组 ​

为什么能渲染?因为 {arr3} 本质上是 JSX 插值表达式,里面是 JS 表达式,而 arr3 是 React 元素数组。React 渲染数组时,会按顺序展开其中的元素。笔记里 arr2 = [<li>1</li>, <li>2</li>, ...] 就是为了说明:数组元素可以是 React 元素。易错点:普通对象不能直接放进 JSX 插值表达式渲染;数组可以,但数组里的元素要是可渲染内容。

key:列表渲染的“身份证” ​

为什么需要 key?React 需要识别列表中每个元素是谁,更新时才能稳定复用。怎么做?给 map 返回的最外层元素加唯一不重复的 key,推荐用数据里的 id:

jsx
{users.map(user => (
    <li key={user.id}>
        <h3 style={{ color: 'red' }}>姓名: {user.username}</h3>
        <p className="f1">年龄: {user.age}</p>
    </li>
))}

如果没有 id,笔记说可以考虑索引,但有些情况会有问题,后面讨论。易错点:key 要写在 map 回调返回的最外层元素上;key 是 React 内部用的特殊属性,不要在组件里当普通数据读取;className 不要写成 class;style 要写成对象。

对象数组与条件渲染:评论列表案例 ​

对象数组的渲染方式与简单数组一致,只是取值变成 item.name、item.content。评论列表还要处理“没有数据”的情况:有评论就渲染 ul > li > a > h3/p,没有评论就渲染“暂无评论!”。这里用三元表达式:

jsx
const list = [
    { id: 1, name: 'jack', content: 'rose, you jump i jump', time: '03:21' },
    { id: 2, name: 'rose', content: 'jack, you see you, one day day', time: '03:22' },
    { id: 3, name: 'tom', content: 'jack,。。。。。', time: '03:23' }
];

root.render((
    <div>
        {list.length === 0 ? <h1>暂无评论!</h1> : (
            <ul>
                {list.map(item => (
                    <li key={item.id}>
                        <a href="">
                            <h3 style={{ fontSize: 25 }}>{item.name}</h3>
                            <p className="f20">{item.content}</p>
                        </a>
                    </li>
                ))}
            </ul>
        )}
    </div>
));

易错点:条件渲染的三元表达式要放在插值表达式 {} 内;list.length === 0 不要写成赋值;fontSize: 25 在 React 中会按像素处理,但团队里也可显式写 '25px';每个 li 仍要加 key。

调试与易错点小结 ​

调试列表渲染时,先打印 item,确认遍历到的数据;再打印 map 的结果,确认是不是 React 元素数组;最后再看页面。易错点集中在:忘记 return、key 位置写错、用索引当 key 却不理解风险、把对象直接渲染、class 与 className 混用、样式对象写成字符串。把“数据数组 → React 元素数组 → 插值表达式渲染”这条链路走通,列表渲染就稳了。

先回顾:JSX 事件绑定的位置 ​

为什么需要: 前面用 JSX 创建 React 元素、生成页面结构,重点用了插值表达式——也就是花括号里写 JS 表达式。事件绑定同样放在插值表达式里,所以要先明确:JSX 里写标签本身学习成本不高,真正要小心的是花括号里最终放进去的是什么。

怎么想: 事件名从原生写法的小写,变成 JSX 里首字母大写的写法,例如 onclick 要写成 onClick。绑定事件时,花括号里要放“函数”,更准确地说,要放函数的引用地址,而不是随便写一段执行语句。

怎么做:

jsx
function click5() {
  console.log("按钮被点击了");
}

<button onClick={click5}>特殊按钮</button>

易错点: 不要只死记“不能写括号”。真正规则是:onClick 最终接收到的必须是一个函数。写成 onClick={click5()} 时,传进去的是 click5 的返回值;如果返回值不是函数,事件回调就不对。

函数调用作为返回值:不要学死 ​

为什么需要: 转写里专门举了“函数调用”的例子,是为了说明规则的本质不是“看到括号就错”,而是看最终传进去的是不是函数。这样才能理解后面传参、包装函数的写法。

怎么想: 如果写成 onClick={click5()},React 拿到的是 click5() 执行后的结果。只要这个结果本身是一个函数,它就可以作为真正的事件回调。

怎么做:

jsx
function click5() {
  return function (e) {
    console.log(e);
  };
}

<button onClick={click5()}>特殊按钮</button>

这段代码本质上等价于:

jsx
<button
  onClick={function (e) {
    console.log(e);
  }}
>
  特殊按钮
</button>

因为 click5() 返回了内部函数,真正被绑定的是这个内部函数。

易错点: 不要把“函数调用”一棍子打死,也不要把“函数名”当成唯一形式。判断标准是:最终传给 onClick 的值是不是函数。内部函数才能接收到事件对象。

需要传参时:用外层函数包一层 ​

为什么需要: 实际开发中经常既要传普通参数,又要拿到事件对象。直接写 onClick={handleClick(id)} 会立刻执行,不是等点击再执行。

怎么想: 用一个外层函数包住,让 React 在点击时调用外层函数,外层函数再去调用真正逻辑,并把 id 和事件对象传进去。

怎么做:

jsx
function handleClick(id, e) {
  console.log(id, e);
}

<button onClick={(e) => handleClick(id, e)}>删除</button>

易错点: 参数顺序要自己约定清楚;不要写成 onClick={handleClick(id, e)},因为 e 此时并不存在,而且函数会被提前执行。

事件对象、this 与 onChange ​

为什么需要: 事件回调里经常要读事件对象,比如目标元素、输入值等。转写里强调:React 给到的事件对象是一个包装过的事件对象,真正的原生事件对象在 nativeEvent 属性上。

怎么想: 先记住两个结论:事件回调函数的调用者是 window,所以 this 的指向要特别留意,后续类组件还会用到;事件对象不是直接等同于原生事件对象,需要原生对象时看 e.nativeEvent。

怎么做:

jsx
function handleChange(e) {
  console.log(e);            // 包装后的事件对象
  console.log(e.nativeEvent); // 原生事件对象
}

<input onChange={handleChange} />

另外,转写里提醒:onChange 实际对应原生 onInput 的行为,不要和原生 change 完全混为一谈。

易错点: 不要想当然认为 e 就是原生事件对象;不要在普通函数里随意依赖 this;onChange 和原生 change 的触发时机要区分。调试时可以先打印 e 和 e.nativeEvent,确认拿到的是什么。

先明确本节要解决的问题 ​

原生 DOM 事件可以用 addEventListener 绑定,也可以在 HTML 标签上写事件属性。本节聚焦后者:通过标签属性绑定事件。它适合快速演示、理解事件回调的调用过程,也便于看清 this、事件对象、默认行为这些基础概念。转写稿里反复强调“事件回调函数的调用者是 window”,这一点在本节同样要牢记;但要注意,转写稿里讲的 JSX 事件写法不是本节重点,不要和 HTML 标签属性写法混在一起。

语法:on事件名="事件回调函数()" ​

为什么需要:标签属性绑定事件写法短,能直接把“哪个元素、什么事件、执行什么函数”写在一起。
怎么想:属性名是 on 加事件名,例如 onclick;属性值是“要执行的代码”,所以要写成函数调用 fn(),而不是只写函数名。
怎么做:如 onclick="fn()"。需要事件对象时,把内联环境里的 event 传进去:onclick="fn(event)"。
易错点:onclick="fn" 只是求值函数本身,不会执行回调;要执行就写 fn()。事件对象不是自己凭空创建的,要在调用时把 event 作为实参传进去。

this 指向:回调里默认是 window ​

为什么需要:很多初学者以为 onclick="fn()" 里 fn 的 this 是按钮,实际不是。
怎么想:fn() 是普通函数调用,笔记明确说“事件回调函数的调用者是 window,所以 this 指向 window”。
怎么做:函数里打印 this 可验证;如果确实要拿按钮,就在标签里传 this:onclick="f2(this)",函数用形参接收。
易错点:不要在 fn 里直接写 this 就以为拿到按钮;要区分“标签内联环境里的 this”和“函数内部执行时的 this”。

事件对象与阻止默认行为 ​

为什么需要:要读取输入值、阻止链接跳转,都离不开事件对象。
怎么想:通过实参 event 获取事件对象;阻止默认行为调用 e.preventDefault()。
怎么做:onclick="fn(event)",函数接收 e 后执行 e.preventDefault()。
易错点:不传 event,函数里就没有事件对象;preventDefault() 要写在事件对象上,不是写在 window 上。转写稿里“实验对象”是口误,应纠正为“事件对象”。

同时传普通参数和事件对象 ​

为什么需要:实际开发常要“既知道点了哪个按钮,又拿到事件对象”。
怎么想:标签属性里按函数形参顺序传实参即可。
怎么做:f1(1,2)、f1(1,event);想拿按钮 this 就 f2(this)。
易错点:参数顺序要和函数形参对应;this 不是事件对象,别把两者混用。

oninput 与 onchange 的区别 ​

为什么需要:输入框实时校验用 oninput,等用户确认后再取值常用 onchange。
怎么想:oninput 是键盘有输入就立刻触发;onchange 是内容有变化并且失去焦点时触发。
怎么做:分别绑定并打印 e.target.value 与事件对象类型。
易错点:oninput 的事件对象是 InputEvent,onchange 是 Event;不要以为两者触发时机一样。

完整示例(保留笔记代码) ​

html
<body>
    <!-- window.event -->
    <p><button onclick="fn(event);">按钮1</button></p>
    <p><a href="http://baidu.com" onclick="fn(event)">百度</a></p>
    <p><button onclick="f1(1,2)">自定义参数和事件对象同时传1</button></p>
    <p><button onclick="f1(1,event)">自定义参数和事件对象同时传2</button></p>
    <p><button onclick="f2(this)">实参this,指向当前按钮</button></p>
    <hr/>
    <input type="text" name="" id="" oninput="f3(event)">
    <hr/>
    <input type="text" name="" id="" onchange="f4(event)">
    <script>
        // let oBtn = document.querySelector('button');
        // oBtn.onclick = function(){

        // }
        // fn是click事件的事件回调函数
        /**
         * 1. fn 是谁调用的,this!  事件回调的函数的调用者是 window
         * 2. 事件对象: 调用是传递实参event
         * 3. 阻止默认行为:e.preventDefault();
         * 
         */
        function fn(e){
            console.log('this: ', this);
            console.log('e: ', e);
            e.preventDefault();
        }
        function f1(a,b){
            console.log('a: ', a);
            console.log('b: ',b);
        }
        
        function f2(_this){
            console.log('_this: ', _this);// 指向当前按钮
            console.log(this);// window 调用者是window
        }
        
        
        function f3(e){
            //oninput 触发时机,有输入就触发
            console.log('oninput value: ',e.target.value)
            console.log('oninput e: ', e);//InputEvent
        }

        function f4(e){
            // onchange 内容有变化,并且失去交点触发
            console.log('onchange value: ',e.target.value)
            console.log('onchange e: ', e);//Event 
        }

    </script>
</body>

一、绑定事件:标签属性 + 函数引用 ​

为什么需要:JSX 里写的是页面结构,但按钮点击、输入框输入都需要 JS 逻辑,所以要通过标签属性把事件和函数接起来。
怎么想:事件名用 React 的写法,首字母大写;属性值给“函数本身”,不是函数执行结果。
怎么做:<button onClick={click1}>click1</button>。转写稿里“GS叉”即 JSX,“差值表达式”即插值表达式,事件也是用 {} 绑定。
易错点:写成 onClick={click1()} 是“渲染时立即调用”,填进去的是返回值;只有返回值仍是函数时,才可能成为真正的事件处理函数。事件名不要写成原生小写 onclick。

二、this 指向:调用者是 window,严格模式下是 undefined ​

为什么需要:普通函数里 this 容易让人误以为是组件实例,必须先明确 React 事件回调的调用方式。
怎么想:转写稿强调“React 的事件回调函数的调用者是 window”;又因为 React 使用严格模式,所以普通函数里的 this 为 undefined。
怎么做:在 click1 中打印 this,看到 undefined。
易错点:不要按类组件思维默认 this 指向组件;本节示例用普通函数,看到 undefined 是预期结果。

三、事件对象:第一个形参接收,原生对象在 nativeEvent ​

为什么需要:阻止默认行为、读取输入值都要事件对象。
怎么想:事件回调由 window 调用,会默认把事件对象作为第一个实参传入;React 又做了一层包装和兼容处理。
怎么做:function click2(e){ e.preventDefault(); };需要原生对象时用 e.nativeEvent。
易错点:不要以为 e 就是原生事件对象;常用属性基本都有,但原生对象要从 nativeEvent 取。

四、事件回调传参:用箭头函数包裹 ​

为什么需要:直接 onClick={click3(1,2)} 会在渲染时执行,不是点击时执行。
怎么想:把“调用”放进一个函数体里,让 React 点击时再调用这个包裹函数。
怎么做:onClick={() => click3(2,3)};既要参数又要事件对象:onClick={(e) => click4(e,100,222)}。
易错点:传参必须包裹;包裹后事件对象要在箭头函数形参上接,再传给业务函数。

五、onChange:实际对应原生 oninput ​

为什么需要:输入框要实时拿到用户输入。
怎么想:React 的 onChange 在键盘输入时就触发,实际是原生 oninput 的行为。
怎么做:<input onChange={change} />,在 change 里读 e.target.value。
易错点:不要按原生 change 的“失焦才触发”理解;这里的事件对象是 InputEvent 一类,读值仍用 e.target.value。

六、补充推导:onClick={fn()} 什么时候合法 ​

为什么需要:转写稿专门演示了“填函数调用”与“填函数引用”的区别。
怎么想:onClick={click3()} 在渲染时执行 click3,返回 undefined,点击时没有函数可调用;若 click5() 返回一个内部函数,则填进去的是这个内部函数,合法。
怎么做:

jsx
function click5() {
  return function (e) {
    console.log(e);
  };
}
<button onClick={click5()}>特殊按钮</button>

它等价于直接写内联函数。
易错点:不要死记“只能写函数名”;本质是“最终传进去的必须是函数”。调试时先看控制台是否在渲染阶段就打印,若立即打印,多半是误写了调用。

七、完整示例代码(按笔记保留) ​

jsx
<!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>
    <script src="./lib/babel.min.js"></script>
</head>

<body>
    <div id="root"></div>
</body>
<script type="text/babel">
    /**
     * 在标签属性上通过 on事件名=事件回调函数 进行绑定
     * 1. 事件名首字母大写
     * 2. 事件回调函数填的是函数名[函数的定义、函数的引用地址],而不是函数的调用
     * 
     * -----------
     * this指向问题:undefined 说明 react中的事件回调的调用者也是window,严格模式所以是undefined
     * 
     * 事件对象e:
     * 1. 一个经过react包装后的事件对象,原生的事件对象常用属性,基本都有。这个事件对象更好用,已经做过兼容性处理了
     * 2. 如果想获取原生的事件对象:e.nativeEvent
     * 3. 事件的回调函数是window帮我们调用的,会默认的将事件对象作为第一个实参传递给我
     * 
     * 事件回调函数参数的传递
     * 包裹一个箭头函数,在箭头函数内部调用函数并传递参数
     */
    const root = ReactDOM.createRoot(document.querySelector("#root"));

    function click1() {
        console.log('click1');
        console.log('this: ', this);//undefined
    }

    function click2(e) {
        console.log(e);
        e.preventDefault();
    }

    function click3(a, b) {
        console.log('click3');
        console.log('a: ', a);
        console.log('b: ', b);
    }

    function outFn() {
        click3(1, 2);
    }

    function click4(e, a, b) {
        console.log('e: ', e);
        console.log('a: ', a);
        console.log('b: ', b);
        e.preventDefault();
    }

    /**
     * react中的 onChange事件,实际是原生的 oninput事件
     */
    function change(e){
        console.log('change e: ', e);
        console.log('value: ', e.target.value);
    }
    root.render((
        <div>
            <p><button onClick={click1}>click1</button></p>
            <p><a href="http://baidu.com" onClick={click2}>百度</a></p>

            <h3>事件回调函数传参</h3>
            <p><button onClick={click3()}>click3</button></p>
            <p><button onClick={outFn}>传递参数</button></p>
            <p><button onClick={function () {
                click3(2, 3);
            }}>包裹匿名函数传递参数</button></p>
            <p><button onClick={() => click3(2, 3)}>包裹箭头函数传参</button></p>
            <h3>即传递参数,又同时传递事件对象</h3>
            <p><button onClick={(e) => {
                console.log('e: ', e);
                click3(1, e);
            }}>即传递参数,又同时传递事件对象</button></p>

            <p><a href="http://baidu.com" onClick={(e) => click4(e, 100, 222)}>百度</a></p>

            <hr/>
            <input type="text" onChange={change} name="" id=""/>
        </div>
    ))
</script>

</html>

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

示例 1:列表渲染(简单数组 → React 元素数组 → 带 key 的列表)

html
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>列表渲染</title>
    <script src="https://unpkg.com/react@18/umd/react.development.js"></script>
    <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
    <style>
        .f1{
            font-size: 20px;
            color:blue;
        }
        .f20{
            font-size: 20px;
        }
    </style>
</head>

<body>
    <div id="root"></div>
</body>
<script type="text/babel">
    /**
     *  列表渲染,要求数据类型 99.99% 都是一个数组
     *  react列表渲染的原理,就是将普通数据的数组,映射成一个react元素的数组,然后使用插值表达式渲染
     */
    const root = ReactDOM.createRoot(document.querySelector("#root"));
    let arr1 = [1, 2, 3, 4];
    // 数组中的元素,可以是react元素
    let arr2 = [<li>1</li>, <li>2</li>, <li>3</li>, <li>4</li>];

    // map ==》 返回一个新数组,新数组的长度跟原数组一样
    let arr3 = arr1.map(item => {
        console.log('item: ', item); // 遍历时数组中的每一项
        return (
            <li key={item}>{item}</li>
        )
    })
    console.log(arr3);

    // 优化 map映射
    let arr4 = arr1.map(item => (
        <li key={item}>
            {item}
        </li>
    ))

    let users = [
        { id: 1, username: 'atguigu', age: 19 },
        { id: 2, username: '迪丽热巴', age: 29 },
        { id: 3, username: '古力娜扎', age: 20 },
        { id: 4, username: '热依扎', age: 31 }
    ]

    const list = [
        { id: 1, name: 'jack', content: 'rose, you jump i jump', time: '03:21' },
        { id: 2, name: 'rose', content: 'jack, you see you, one day day', time: '03:22' },
        { id: 3, name: 'tom', content: 'jack,。。。。。', time: '03:23' }
    ]

    root.render((
        <div>
            <h3>简单数组元素渲染</h3>
            {arr1}
            <h3>将简单数组,渲染成 ul li 列表</h3>
            <ul>
                {arr1}
            </ul>
            <hr />
            <ul>
                {arr2}
            </ul>
            <hr />
            <h3>使用map映射成一个react元素的新数组</h3>
            <ul>
                {arr3}
            </ul>
            <h3>map 映射优化后, 省略return 和 { }</h3>
            <ul>
                {arr4}
            </ul>
            <h3>map 映射列表渲染终极版本</h3>
            <ul>
                {arr1.map(item => (
                    <li key={item}>{item}</li>
                ))}
            </ul>

            <hr />
            <h3>数组是对象,复杂数据类型渲染</h3>
            <ul>
                {users.map(user => (
                    <li key={user.id}>
                        <h3 style={{color:'red'}}>姓名: {user.username}</h3>
                        <p className='f1'>年龄: {user.age}</p>
                    </li>
                ))}
            </ul>

            <hr />
            <h3>key值遍历 —— 评论列表</h3>
            {list.length === 0 ? <h1> 暂无评论!</h1> : (
                <ul>
                    {list.map(item => (
                        <li key={item.id}>
                            <a href="">
                                <h3 style={{fontSize:25}}>{item.name}</h3>
                                <p className="f20">{item.content}</p>
                            </a>
                        </li>
                    ))}
                </ul>
            )}
        </div>
    ))
</script>

</html>

要点解释:

  • 列表渲染的核心就是 map:它返回一个新数组,新数组每个元素由回调函数的返回值决定,所以返回值必须是 React 元素,这样才能被插值表达式 {} 渲染。
  • {arr1} 能直接渲染成 1 2 3 4,是因为插值表达式支持数组;但如果要变成 ul > li 结构,就必须先 map 成 <li> 数组,否则只会把数字平铺输出。
  • key 必须加在 map 返回的最外层元素上(示例中是 <li>),且要求唯一不重复,推荐用 id;没有 id 才考虑索引(有些情况会有问题)。
  • arr2 是手写的静态 React 元素数组,没有 key,控制台会出现警告 —— 这是常见的易错点:只要是用数组渲染列表,就应补 key。
  • style={{color:'red'}} 是双花括号:外层是插值表达式,内层是 JS 对象;fontSize:25 数字会自动补 px。类名要用 className,不能用 class。
  • 条件渲染用三元表达式:list.length === 0 ? <h1>暂无评论!</h1> : (...)

示例 2:原生 DOM 事件(this、事件对象、传参、oninput 与 onchange)

html
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>原生dom事件</title>
</head>

<body>
    <!-- window.event -->
    <p><button onclick="fn(event);">按钮1</button></p>
    <p><a href="http://baidu.com" onclick="fn(event)">百度</a></p>
    <p><button onclick="f1(1,2)">自定义参数和事件对象同时传1</button></p>
    <p><button onclick="f1(1,event)">自定义参数和事件对象同时传2</button></p>
    <p><button onclick="f2(this)">实参this,指向当前按钮</button></p>
    <hr/>
    <input type="text" name="" id="" oninput="f3(event)">
    <hr/>
    <input type="text" name="" id="" onchange="f4(event)">
    <script>
        /**
         * 1. fn 是谁调用的,this!  事件回调的函数的调用者是 window
         * 2. 事件对象: 调用时传递实参 event
         * 3. 阻止默认行为:e.preventDefault();
         */
        function fn(e){
            console.log('this: ', this);
            console.log('e: ', e);
            e.preventDefault();
        }
        function f1(a,b){
            console.log('a: ', a);
            console.log('b: ',b);
        }

        function f2(_this){
            console.log('_this: ', _this);// 指向当前按钮
            console.log(this);// window 调用者是window
        }

        function f3(e){
            // oninput 触发时机:有输入就触发
            console.log('oninput value: ',e.target.value)
            console.log('oninput e: ', e);//InputEvent
        }

        function f4(e){
            // onchange 内容有变化,并且失去焦点触发
            console.log('onchange value: ',e.target.value)
            console.log('onchange e: ', e);//Event
        }
    </script>
</body>

</html>

要点解释:

  • 原生绑定语法是 on事件名="事件回调函数()",事件名全小写;注意这里是“函数调用”而不是函数名。
  • 回调函数的调用者是 window,所以 this 指向 window;想拿到按钮本身必须显式传 this(f2(this))。
  • 事件对象通过实参 event 传入,可以同时传普通参数和事件对象:f1(1, event)。
  • e.preventDefault() 阻止默认行为(如 a 标签跳转)。
  • 易错点:oninput 键盘一有输入就触发,事件对象是 InputEvent;onchange 要内容变化且失焦才触发,事件对象是 Event,两者不要混用。

示例 3:JSX 中的事件(绑定、this、事件对象、传参、onChange)

html
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>jsx中的事件</title>
    <script src="https://unpkg.com/react@18/umd/react.development.js"></script>
    <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>

<body>
    <div id="root"></div>
</body>
<script type="text/babel">
    /**
     * 在标签属性上通过 on事件名=事件回调函数 进行绑定
     * 1. 事件名首字母大写
     * 2. 事件回调函数填的是函数名[函数的定义、函数的引用地址],而不是函数的调用
     *
     * this指向问题:undefined 说明 react中的事件回调的调用者也是window,严格模式所以是undefined
     *
     * 事件对象e:
     * 1. 一个经过react包装后的事件对象,原生的事件对象常用属性基本都有
     * 2. 如果想获取原生的事件对象:e.nativeEvent
     * 3. 事件的回调函数是window帮我们调用的,会默认的将事件对象作为第一个实参传递
     */
    const root = ReactDOM.createRoot(document.querySelector("#root"));

    function click1() {
        console.log('click1');
        console.log('this: ', this);//undefined
    }

    function click2(e) {
        console.log(e);
        e.preventDefault();
    }

    function click3(a, b) {
        console.log('click3');
        console.log('a: ', a);
        console.log('b: ', b);
    }

    function outFn() {
        click3(1, 2);
    }

    function click4(e, a, b) {
        console.log('e: ', e);
        console.log('a: ', a);
        console.log('b: ', b);
        e.preventDefault();
    }

    /**
     * react中的 onChange事件,实际是原生的 oninput事件
     */
    function change(e){
        console.log('change e: ', e);
        console.log('value: ', e.target.value);
    }

    root.render((
        <div>
            <p><button onClick={click1}>click1</button></p>
            <p><a href="http://baidu.com" onClick={click2}>百度</a></p>

            <h3>事件回调函数传参</h3>
            <p><button onClick={outFn}>传递参数</button></p>
            <p><button onClick={function () {
                click3(2, 3);
            }}>包裹匿名函数传递参数</button></p>
            <p><button onClick={() => click3(2, 3)}>包裹箭头函数传参</button></p>

            <h3>即传递参数,又同时传递事件对象</h3>
            <p><button onClick={(e) => {
                console.log('e: ', e);
                click3(1, e);
            }}>即传递参数,又同时传递事件对象</button></p>

            <p><a href="http://baidu.com" onClick={(e) => click4(e, 100, 222)}>百度</a></p>

            <hr/>
            <input type="text" onChange={change} name="" id=""/>
        </div>
    ))
</script>

</html>

要点解释:

  • 绑定语法是 onClick={函数名},事件名首字母大写,值填函数的引用地址(函数名),不能写成 click3() 这种调用。
  • this 是 undefined 而不是 window:调用者仍是 window,但 React 内部是严格模式,所以指向 undefined。
  • 事件对象由 React 包装过,常用属性都有并做了兼容处理;需要原生对象时用 e.nativeEvent,阻止默认行为仍是 e.preventDefault()。
  • 传参必须“包裹一层函数”:onClick={() => click3(2, 3)},等价于 onClick={function(){ click3(2, 3) &#125;&#125;。易错点:直接写 onClick={click3()} 会把函数执行结果(undefined)赋给事件,点击时不会执行任何东西;除非该函数返回一个函数(如 onClick={click5()} 而 click5 内部 return (e) => {...}),这才是合法的,内部函数能接收到事件对象。
  • 既要普通参数又要事件对象:onClick={(e) => click4(e, 100, 222)},把 e 显式传进去。
  • onChange 在 React 中实际对应原生 oninput:键盘一输入就触发,事件对象是 InputEvent,e.target.value 取输入值。

常见错误 ​

  1. 点击按钮没反应,但控制台在页面加载时就打印了日志

    • 现象:onClick={click3()} 一刷新就执行一次,之后点击毫无反应。
    • 原因:填入的是函数调用后的返回值,React 拿到的是 undefined,没有可绑定的回调。
    • 修法:onClick={click3} 只传引用;确需立即调用,就让返回值本身是函数:onClick={click5()} 里 click5 必须 return () => {...}。
  2. map 之后列表空白,或报 “Nothing was returned from render”

    • 现象:{arr.map(item => { <li>{item}</li> })} 渲染出 undefined。
    • 原因:箭头函数用了 {} 函数体却没写 return。
    • 修法:改成 item => (<li key={item.id}>{item}</li>),或补 return。
  3. 控制台报警告:Each child in a list should have a unique “key” prop

    • 现象:列表能渲染但满屏警告;后续做增删/勾选时状态串行。
    • 原因:map 生成的元素没写 key,或图省事用 index,重排后 key 与数据错位。
    • 修法:优先 key={item.id};只有在纯静态、不增删不排序的列表里才可退而用索引。
  4. 想用 this 拿到按钮,结果拿到 undefined

    • 现象:function fn(){ console.log(this) } 绑定后输出 undefined,不是 <button>。
    • 原因:React 事件回调由 window 调用,且运行在严格模式下,this 为 undefined。
    • 修法:从事件对象取 e.currentTarget(绑定事件的元素)或 e.target(真正触发的元素),不要依赖 this。
  5. 传了自定义参数就收不到事件对象

    • 现象:onClick={() => fn(1, 2)} 里拿不到 e;或写成 onClick={fn(1, 2)} 直接执行。
    • 原因:箭头函数形参没声明 e,React 传进来的事件对象被丢弃;后一种是把调用结果当回调。
    • 修法:onClick={(e) => fn(e, 1, 2)},形参位置和实参位置要一一对应。
  6. 顶层写两个兄弟标签直接编译报错

    • 现象:Adjacent JSX elements must be wrapped in an enclosing tag。
    • 原因:render 返回的 JSX 只允许唯一根节点。
    • 修法:用 <>...</> 或 <React.Fragment>...</React.Fragment>;需要给包裹层加 key/属性时只能用后者,空标签不能写属性。
  7. JSX 里注释把结构“注掉”了,甚至直接报错

    • 现象:{ // 注释 } 后面整块语法报错;写 <!-- --> 也不行。
    • 原因:单行注释把差值表达式的右花括号一起吞掉;HTML 注释在 JSX 中非法。
    • 修法:统一用 {/* 注释 */};单行注释必须换行,让 } 独立成行;属性注释写在标签内部 className={/* */} 位置之外。
  8. 原生批量插入 li 时页面明显卡顿

    • 现象:循环 3 次 ul.appendChild(li),每次插入都触发一次重排重绘。
    • 原因:每插入一个节点都改变 ul 的几何属性。
    • 修法:先 document.createDocumentFragment(),循环里往碎片追加,循环结束后只 ul.appendChild(fragment) 一次。

练习 ​

  1. 评论列表(列表渲染 + 空态) 给定 list = [{id,name,content,time}],要求:有数据渲染 ul>li>a>[h3,p],无数据渲染 <h1>暂无评论!</h1>,用户名 25px、内容 20px,key 用 id。追加一条新评论后检查 key 警告是否消失。

  2. 事件三连 同一个按钮上分别实现:① 只传自定义参数;② 只拿事件对象并 preventDefault();③ 既传参数又传事件对象。再写一个“点我输出当前按钮文字”的按钮,要求不使用 this。

  3. Fragment 改造 把一段用 <div> 包住两个兄弟节点的结构改成 <>...</>,用 DevTools 确认 root 下少了一层 div;再改成 <React.Fragment key={...}> 放进 map 里,说明为什么此时不能用空标签。

  4. 文档碎片性能对比 用原生 JS 写两个按钮:“直接插入 3 个 li”和“碎片插入 3 个 li”,各点 10 次,用 Performance 面板观察 Layout 次数差异,并解释原因。

  5. 注释与属性纠错 给出 5 行有问题的 JSX(含 // 注释、<!-- -->、class=、style="color:red"、onclick=),逐行改正并说明每处错误属于哪一类。

原视频对照 ​

#视频时长
1回顾小结07:29
2jsx中的三种注释方式03:51
3原生dom操作-文档碎片09:53
4React中的文档碎片07:25
5解构赋值复习07:20
6组件的基本认识09:15

小结 ​

  1. 列表渲染的固定套路只有一句:数组 → map → React 元素数组 → 插值渲染,其余都是写法糖。
  2. key 是列表的身份标识,不是样式也不是索引编号;能用 id 就别用 index。
  3. 事件绑定的判断标准不是“有没有括号”,而是这个位置最终拿到的是不是一个函数。
  4. React 事件回调的 this 是 undefined,任何需要元素的场景一律走 e.currentTarget / e.target。
  5. React 的事件对象是包装过的,preventDefault 照常用,需要原生对象时取 e.nativeEvent。
  6. React 的 onChange 等价于原生 oninput,需要“失焦才触发”请用 onBlur。
  7. 唯一根节点的限制由 Fragment(<> 或 <React.Fragment>)解除;要挂属性就用后者。
  8. 组件的本质是“结构 + 逻辑 + 样式(+ 静态资源)”,拆多细没有标准答案,能拼回整体即可;当前主线是函数组件。