1. setState批量处理
在React18之前,promise、setTimeout、原生事件处理函数不会被批量处理。只有React事件处理函数会批量处理(例如鼠标点击,鼠标悬浮,键盘输入)。
React18之前
React事件处理
![图片转存失败,建议将图片保存下来直接上传
import React, { useState } from 'react';
// React 18 之前
const App: React.FC = () => {
console.log('z');
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
return (
<button
onClick={() => {
setCount1(count => count + 1);
setCount2(count => count + 1);
// 在React事件中被批处理
}}
>
{`count1 is ${count1}, count2 is ${count2}`}
</button>
);
};
image.png(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/53380caceba04e6e95897fecb3935bc4~tplv-k3u1fbpfcp-watermark.image?)
export default App;
]()```
效果:点一次按钮,App组件渲染一次。
按理来说,`setCount1(count => count + 1); setCount2(count => count + 1);`改变了2次state,组件应该渲染2次。但是由于React可以批量处理,因此只渲染一次。
### SettimeOut
```js
import React, { useState } from 'react';
// React 18 之前
const App: React.FC = () => {
console.log('App组件渲染了!');
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
return (
<div
onClick={() => {
setTimeout(() => {
setCount1(count => count + 1);
setCount2(count => count + 1);
});
// 在 setTimeout 中不会进行批处理
}}
>
<div>count1: {count1}</div>
<div>count2: {count2}</div>
</div>
);
};
export default App;
效果:每点1次button,App组件渲染两次。由此可见,React没有批量处理setCount1(count => count + 1); setCount2(count => count + 1);。
原生js事件
import React, { useEffect, useState } from 'react';
// React 18 之前
const App: React.FC = () => {
console.log('App组件渲染了!');
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
useEffect(() => {
document.body.addEventListener('click', () => {
setCount1(count => count + 1);
setCount2(count => count + 1);
});
// 在原生js事件中不会进行批处理
}, []);
return (
<>
<div>count1: {count1}</div>
<div>count2: {count2}</div>
</>
);
};
export default App;
可以看到,在原生js事件中,结果跟情况二是一样的,每次点击更新两个状态,组件都会渲染两次,不会进行批量更新。
在React18中
上面的三个例子都只会执行一次render。如果实在想把批量操作分开(每执行一次setState,就执行一次render)。那么可以使用flushSync包裹setState/
import React, { useState } from 'react';
import { flushSync } from 'react-dom';
const App: React.FC = () => {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
return (
<div
onClick={() => {
flushSync(() => {
setCount1(count => count + 1);
});
// 第一次更新
flushSync(() => {
setCount2(count => count + 1);
});
// 第二次更新
}}
>
<div>count1: {count1}</div>
<div>count2: {count2}</div>
</div>
);
};
export default App;
2.React18引入并发模式
api对比
React18中,引入createRoot()
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
const root = document.getElementById('root');
// 使用 React 18 的新 API 创建并渲染根组件
ReactDOM.createRoot(root).render(<App />);
在React18之前,使用render()渲染
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
const root = document.getElementById('root');
// 使用旧 API 将组件渲染到 DOM 中
ReactDOM.render(<App />, root);
如何理解并发模式
import React, { useState } from 'react';
function App() {
const [count, setCount] = useState(0);
const [data, setData] = useState(null);
const handleClick = async () => {
setCount(count + 1);
// 模拟耗时 API 请求
const result = await new Promise((resolve) =>
setTimeout(() => resolve('API 请求结果'), 3000)
);
console.log("console会不会被阻塞呢?") //console仍会被阻塞
setData(result);//状态更新不会被阻塞,点击按钮页面立即更新
};
return (
<div>
<button onClick={handleClick}>点击我</button>
<p>计数器:{count}</p>
<p>数据:{data}</p>
</div>
);
}
export default App;
3、JSX是什么,它和JS有什么区别
JSX是React.createElement的语法糖,不能直接被浏览器识别。需要先通过webpack,babel转换为JS。
为什么React自定义组件首字母要大写
如果小写,在创建虚拟dom的时候,会被当作html标签React.createElement("app",null,"lyllovelemon")大写的话,会被当作自定义组件处理。
以下是大写和小写,创建的虚拟Dom的区别:
自定义组件(首字母大写)
// 自定义组件
function MyComponent() {
return <div>Hello, World!</div>;
}
// 使用自定义组件创建虚拟 DOM
const element = React.createElement(MyComponent, null);
// element 对象表示如下的虚拟 DOM 结构:
/*
{
type: MyComponent, // 注意这里的 type 是一个函数(自定义组件)
props: {},
children: [],
...
}
*/
原生html标签(首字母小写)
// 使用原生 HTML 标签创建虚拟 DOM
const element = React.createElement('div', { className: 'example' }, 'Hello, World!');
// element 对象表示如下的虚拟 DOM 结构:
/*
{
type: 'div', // 注意这里的 type 是一个字符串(原生 HTML 标签)
props: {
className: 'example',
children: 'Hello, World!'
},
children: [],
...
}
*/
可以发现,主要区别在type,一个是字符串,一个是函数。那么type函数会在哪用到呢?在render函数内用到。以下是简易版render函数。
function render(virtualDOM, container) {
if (typeof virtualDOM.type === 'string') {
// 处理原生 HTML 标签的逻辑保持不变
// ...
} else if (typeof virtualDOM.type === 'function') {
// 处理自定义组件
const Component = virtualDOM.type;
// 如果是函数式组件,直接调用 Component 函数
const componentVirtualDOM = Component(virtualDOM.props);
/*
componentVirtualDOM结构如下:
{
type: 'div',
props: {
children: 'Hello, World!'
},
children: [],
...
}
*/
// 递归地处理子树
render(componentVirtualDOM, container);
}
}
React组件为什么不能返回多个元素
因为react.createElement的第一个参数只能传一个值,这样才能形成一个树状结构
class App extends React.Component{
render(){
return(
<div>
<h1 className="title">lyllovelemon</h1>
<span>内容</span>
</div>
)
}
//编译后
class App extends React.Component{
render(){
return React.createElement('div',null,[
React.createElement('h1',{className:'title'},'lyllovelemon'),
React.createElement('span'),null,'内容'
])
}
}
如果想要返回多个元素怎么办?
把多个函数包裹在React.Fragment内,它不会创建新的dom:
renderList(){
this.state.list.map((item,key)=>{
return (<React.Fragment>
<tr key={item.id}>
<td>{item.name}</td>
<td>{item.age}</td>
<td>{item.address}</td>
</tr>
</React.Fragment>)
})
}
简述React的生命周期
class组件才有生命周期:挂载,更新,卸载
为什么不用class组件 :
繁琐,不如function易懂、简洁。比如回调函数需要绑定this。
class CounterClassComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
};
this.onIncrementClick = this.handleIncrement.bind(this);
}
handleIncrement() {
this.setState({ count: this.state.count + 1 });
}
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.onIncrementClick}>Increment</button>
</div>
);
}
}
React事件机制
juejin.cn/post/707976… (讲的React16,不用看) juejin.cn/post/695563…
合成事件和原生事件的区别
合成事件包含原生事件,可以通过e.nativeEvent访问原生事件。
// React的合成事件
import React from 'react';
function handleClick(event) {
console.log('React Synthetic Event:', event);
}
function MyComponent() {
return <button onClick={handleClick}>Click me (React Event)</button>;
}
// 原生事件
document.addEventListener('DOMContentLoaded', function () {
const button = document.querySelector('#nativeEventButton');
button.addEventListener('click', function (event) {
console.log('Native DOM Event:', event);
});
});
// HTML
// <button id="nativeEventButton">Click me (Native Event)</button>
React中事件绑定在哪
React16的事件绑定在document上, React17以后事件绑定在container
如何查看某个元素的Fiber
在开发者工具中的element 里,鼠标点击某个 dom 元素后,在『控制台』tab 里执行 console.dir($0) 即可,$0 表示刚才点击的那个 dom 元素。(来源评论区:juejin.cn/post/695563…
如果你想直接console.log dom元素,需要把它包在括号里 console.log([myRef.current]);
为什么要使用合成事件
完整版回答
1. 不用开发者处理浏览器兼容性问题
比如,原生js代码,阻止事件冒泡。IE8没有stopPropagation()方法。所以需要自己处理兼容性。
// 原生事件处理
function handleClick(event) {
// 阻止冒泡
if (event.stopPropagation) {
event.stopPropagation();
} else {
// 用于兼容不支持 stopPropagation 方法的浏览器(如 IE8 及更早版本)
event.cancelBubble = true;
}
console.log("按钮被点击");
}
const button = document.getElementById("myButton");
button.addEventListener("click", handleClick);
React中不需要考虑这种兼容性。
import React from "react";
function App() {
function handleClick(event) {
// 阻止冒泡
event.stopPropagation();
console.log("按钮被点击");
}
return (
<div>
<button onClick={handleClick}>点击我</button>
</div>
);
}
2.事件池优化。合成事件使用完毕,不会被立即销毁,而是存放到事件池。这样就不用在每次触发回调的时候,创建一个新的合成事件对象。
在高频交互场景下可以优化性能,例如每次滚动打印scrollTop
import React, { useState } from "react";
function App() {
const [scrollPosition, setScrollPosition] = useState(0);
function handleScroll(event) {
// 不使用事件池,每次滚动都会创建一个新的合成event事件对象
setScrollPosition(event.target.scrollTop);
}
return (
<div
style={{
height: "100px",
overflowY: "scroll",
border: "1px solid black"
}}
onScroll={handleScroll}
>
<div style={{ height: "3000px" }}>
当前滚动位置: {scrollPosition}px
</div>
</div>
);
}
3.减少内存开销
如果在每个dom上添加click监听器,那么就有很个回调函数() => createClickHandler(item.id)
function createClickHandler(id) {
console.log("点击了项目", id);
}
const items = [
{ id: 1, name: "Item 1" },
{ id: 2, name: "Item 2" },
// 更多列表项...
];
for (const item of items) {
const listItem = document.createElement("li");
listItem.textContent = item.name;
listItem.addEventListener("click", () => createClickHandler(item.id));
// ...
}
父子组件传值方式汇总
回调函数(子传父)
父组件定义一个函数,用于改变自身state,然后传给子子组件,于是子组件可以改变父组件内的值。
props(父传子)
兄弟组件传(可忽略)
本质上让父组件中转
import React from "react"
class Parent extends React.Component{
constructor(props){
super(props)
this.state={
count:0
}
}
increment(){
this.setState({
count:this.state.count+1
})
}
render(){
return (
<div>
<ChildOne count={this.state.count} />
<ChildTwo onClick={this.increment} />
</div>
)
}
}
context(父传子)
父组件内context = createContext(),然后用provider包裹子组件。
子组件用useContext(context)获取值。
React中常见函数
forwardRef
forwardRef是一个高阶组件,用于包裹子组件。如果不用forwardRef,你不能给子组件添加ref={inputRef}
//父组件
import React, { useRef } from 'react';
import CustomInput from './CustomInput';
function ParentComponent() {
const inputRef = useRef();
const handleClick = () => {
alert(`Input value: ${inputRef.current.value}`);
};
return (
<>
<CustomInput ref={inputRef} />
<button onClick={handleClick}>Get Input Value</button>
</>
);
}
export default ParentComponent;
//子组件,forwardRef是一个高阶组件,把ref变成props
import React, { forwardRef } from 'react';
const CustomInput = forwardRef((props, ref) => {
return <input ref={ref} type="text" placeholder="Type something..." />;
});
export default CustomInput;
useImperativeHandle
第一个参数是父组件传给它的ref,给这个ref绑定一些方法,相当于把子组件内部的方法暴露给父组件。
//子组件
import React, { forwardRef, useImperativeHandle, useRef } from 'react';
const CustomInput = forwardRef((props, ref) => {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
},
}));
return <input ref={inputRef} type="text" placeholder="Type something..." />;
});
export default CustomInput;
//父组件
import React, { useRef } from 'react';
import CustomInput from './CustomInput';
function ParentComponent() {
const inputRef = useRef();
const handleClick = () => {
inputRef.current.focus();
};
return (
<>
<CustomInput ref={inputRef} />
<button onClick={handleClick}>Focus Input</button>
</>
);
}
export default ParentComponent;
函数式组件与类组件的区别
语法和结构不同
函数式组件是简单的 JavaScript 函数。
类组件是基于 ES6 类的组件,它们扩展了 React.Component 类,并实现了 render 方法。
为什么类组件需要继承自React.Component?
例如,React.Component 类提供了 setState 方法用于更新组件状态,同时还提供了生命周期方法,如 componentDidMount、componentDidUpdate 和 componentWillUnmount 等。
状态管理区别
类组件使用 this.setState来管理状态。
import React from 'react';
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
increment = () => {
this.setState({ count: this.state.count + 1 });
};
render() {
return (
<>
<p>Count: {this.state.count}</p>
<button onClick={this.increment}>Increment</button>
</>
);
}
}
生命周期函数
类组件具有生命周期方法,如 componentDidMount、componentDidUpdate 和 componentWillUnmount。而在函数式组件中,通过使用 useEffect Hook,可以实现类似的功能。
this的指向不同(或者说函数式组件有hook)
在类组件中,this指向组件实例,需要使用 this 关键字来访问 props、state 和组件方法。
而在函数式组件中,可以使用hooks。hooks使得函数式组件具有状态和副作用等功能。
class Greeting extends React.Component {
render() {
return <h1>Hello, {this.props.name}!</h1>;
}
}
各自优缺点
函数组件:
1.语法更简洁,不需要extend React.Component。
2.hooks使得状态管理更容易,比如给按钮添加点击事件,不需要绑定this.
类组件:
1.更直观的生命周期函数。componentDidMount、componentDidUpdate 和 componentWillUnmount。看到名字就知道意思,useEffect似乎难以理解。
2.更直观的状态管理。this.state 和 this.setState比较易于新手理解。
脚手架是什么?
不使用脚手架如何写代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello World</title>
<script src="https://unpkg.com/react/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom/umd/react-dom.development.js"></script>
</head>
<body>
<div id="root"></div>
<script>
// 创建一个 React 组件
function HelloWorld() {
return React.createElement('h1', null, 'Hello World');
}
// 渲染组件到根元素
ReactDOM.render(
React.createElement(HelloWorld),
document.getElementById('root')
);
</script>
</body>
</html>
脚手架有什么好处?
- 文件目录结构已经划分好。
- 初始代码已经写好,比如入口文件app.js,并加它挂在到root上
- 基本的开发环境,依赖包已经安装好,比如webpack