React相关(第一部分)

140 阅读8分钟

参考资料:juejin.cn/post/718238…

第二部分:juejin.cn/post/721797…

1. setState批量处理

在React18之前,promisesetTimeout原生事件处理函数不会被批量处理。只有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]);

为什么要使用合成事件

完整版回答

fe.ecool.fun/topic-answe…

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 方法用于更新组件状态,同时还提供了生命周期方法,如 componentDidMountcomponentDidUpdatecomponentWillUnmount 等。

状态管理区别

类组件使用 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>
      </>
    );
  }
}

生命周期函数

类组件具有生命周期方法,如 componentDidMountcomponentDidUpdatecomponentWillUnmount。而在函数式组件中,通过使用 useEffect Hook,可以实现类似的功能。

this的指向不同(或者说函数式组件有hook)

在类组件中,this指向组件实例,需要使用 this 关键字来访问 propsstate 和组件方法。

而在函数式组件中,可以使用hooks。hooks使得函数式组件具有状态和副作用等功能。

class Greeting extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}!</h1>;
  }
}

各自优缺点

函数组件:

1.语法更简洁,不需要extend React.Component。

2.hooks使得状态管理更容易,比如给按钮添加点击事件,不需要绑定this.

类组件:

1.更直观的生命周期函数。componentDidMountcomponentDidUpdatecomponentWillUnmount。看到名字就知道意思,useEffect似乎难以理解。

2.更直观的状态管理。this.statethis.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>

脚手架有什么好处?

  1. 文件目录结构已经划分好。
  2. 初始代码已经写好,比如入口文件app.js,并加它挂在到root上
  3. 基本的开发环境,依赖包已经安装好,比如webpack