Skip to content

README

目录


钩子

react 给人特别明显的区别就是 react钩子

jsx
function CounterButton() {
  const [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount(count + 1)}>Clicked {count} times</button>
  );
}

以及可能为了监听某些请求需要使用useEffect 或者 useCallback

钩子的起源是渲染机制

造成这些问题的原因是 React 渲染机制

如果你使用过 React.component 编写页面,你的代码应该长这样

jsx
class Counter extends Component {
  state = {
    age: 42,
  };

  handleAgeChange = () => {
    this.setState({
      age: this.state.age + 1,
    });
  };

  render() {
    return (
      <>
        <h1>Hello, {this.props.name}!</h1>;
        <button onClick={this.handleAgeChange}>Increment age</button>
        <p>You are {this.state.age}.</p>
      </>
    );
  }
}

react 的渲染机制是:当status 或者 props的内容发生改变时,React会重新渲染组件(调用组件的render方法)。

如果改变了其他变量,如render或者全局的变量,都不会触发重新渲染

渲染之后的render里的内容就会作为一个静态的内容被返回,并渲染到页面上

所以如果在render中编写一些变量,那么这个变量的值就会在每次render的时候被重新计算,并返回给React。

如果需要能全局保存的值,那就需要定义在 status 中(使用setStatus),或者由 props 传入

render 中不能使用 useState 这一类 Hooks 原因后面再解释

再来看现在比较新的语法(函数式)

jsx
export default function Counter(props) {
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
      <button onClick={() => setCount(0)}>Reset</button>
    </div>
  );
}

函数和 render的功能类似,都是渲染一次,但不同的是,因为不是类,没有 status,所以使用 hooks 来实现状态管理

如果需要实现类似 status 的功能,需要使用 useState 来定义一个 state 变量

这变量会交给react管理(类似放到this.status中)

当使用 setXxx 的时候,相当与修改了state变量,就会触发渲染一次

如果一个变量依赖于某个 status 变量,并且计算方式唯一,那么就不需要将这个变量定义为状态变量(所以React中没有 Vue中的 computed 函数

渲染和对比

:::Info 参考资料

:::

如果每次更新状态变量,都会重新渲染,那么就会产生性能问题

所以有如下2个机制,来优化算法:

  1. 合并渲染:如果短期内有多个状态变量更新,那么就合并成一个更新,然后进行渲染
  2. 使用了Fiber算法,将渲染任务拆分为 比较更新这两个步骤

::: Info 为什么使用 Fiber 浏览器运行的时候有一个主线程和 worker线程,主线程可以修改页面DOM而worker线程无法修改DOM

如果将计算放在主线程上会拖慢页面渲染速度

Fiber算法将渲染任务拆分为渲染和计算两个步骤,将计算任务(对比节点,判断是否需要更新)放在worker线程上,将渲染任务(单纯负责渲染DOM)放在主线程上,这样主线程就不会被阻塞,从而提升页面渲染速度和性能 :::

Fiber 进行比较时候

如果某个组件的 status 和 Props 都没有更新,那个这个组件也不会更新,直接复用

setXxxx 更新 State -> 触发ststue涉及的 re-render -> 计算更新/比较 -> 渲染更新 DOM

hooks管理

React 内部靠 调用顺序 来识别每个 hooks

jsx
function App() {
  const [count, setCount] = useState(0); // hook1
  const [name, setName] = useState("Tom"); // hook2
  useEffect(() => {
    console.log(count);
  }, [count]); // hook3

  return (
    <div>
      {count} {name}
    </div>
  );
}
yaml
App Fiber
  -> Hook1: { memoizedState: 0, queue: setCount }
  -> Hook2: { memoizedState: "Tom", queue: setName }
  -> Hook3: { effect callback, deps: [count] }

当调用某个hook的时候,获取找到对应的hook数据,并修改值,然后触发重新渲染

当发现某个值有了更新,就会返回新的值

这也就是React.component的render函数中为什么不能使用setState,因为status和 hooks的分开了,不能确定谁先调用,也就无法确定hooks的顺序

key 在对比中的作用

如果你使用 Array.Map 这个方法来生成元素,那么 React 会根据元素的 key 属性来识别它们,以确定它们是添加、删除还是重新排列。

如果你不定义 key 浏览器将会有一行报错

Warning: Each child in a list should have a unique “key” prop.

官方不建议你把 index作为key,并建议使用和列表项绑定的数据作为 key

这个例子可以作为参考

jsx
const [initMap, setInitMap] = useState([1, 2, 3, 4]);
const handleClick = () => {
  setInitMap([3, 2, 1, 4]);
};
return (
  <div className="App" id="app">
    {initMap.map((it, index) => (
      <div key={index}>
        <input type="radio" />
        {it}
      </div>
    ))}
    <button onClick={() => handleClick()}>点击</button>
  </div>
);

这个代码展示4个单选框,当你选择一个单选框之后点击按钮,会发现单选框的值会被改变,而选中的位置不会改变

1 2 3x 4 -> 3 2 1x 4

如果你在一个函数内修改了一个状态变量2次,并且开始和结束的状态变量值相同,则React可能会复用之前的节点。因为React 会将临近的渲染合并为一次渲染以提升性能。那么在渲染的时候会发现,state 变量值没有改变,那么 React 就不会生成新的节点,而是直接复用之前的节点。

为什么要其他各种钩子

useCallback

如果不使用 useCallback ,一个函数在 组件渲染时,会重新创建,造成额外的性能开销

使用 useCallback 可以避免函数在每次渲染时重新创建,只在函数的依赖项改变时重新创建。

usEffect

jsx
function Comp() {
  const [data, setData] = useState(null);

  fetch("/api/data")
    .then((res) => res.json())
    .then(setData); // ❌ 渲染阶段发请求,setData 会触发再次渲染

  return <div>{data}</div>;
}

function Comp() {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetch("/api/data")
      .then((res) => res.json())
      .then(setData);
  }, []); // 空依赖数组 → 只在首次渲染后执行一次

  return <div>{data}</div>;
}

这个fetch写在“render”(也就是外层)函数中,这样每次计算的时候都会触发一次渲染,任何上游的 stste 改变都会触发请求

  • 如果在外层设定一个比较慢的功能,那就会导致渲染变慢
  • 如果在外层设定一个 setInterval 那么就会重复创建导致泄漏
jsx
useEffect(() => {
  const id = setInterval(() => console.log(count), 1000);
  return () => clearInterval(id);
}, [count]);

function Counter() {
  const [count, setCount] = useState(0);

  const id = setInterval(() => console.log(count), 1000); // ❌

  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

useContext

可以将父组件的内容跨层传入子组件,适用于全局数据这种

jsx
import React, { useState, useContext } from 'react';
import ReactDOM from 'react-dom/client';

// 1️⃣ 创建 Context
interface User {
  name: string;
  loggedIn: boolean;
  setUser: React.Dispatch<React.SetStateAction<User>>;
}

const defaultUser = {
  name: 'Guest',
  loggedIn: false,
  setUser: () => {},
};

const UserContext = React.createContext<User>(defaultUser);

// 2️⃣ Context Provider
function UserProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState({ name: 'Guest', loggedIn: false });
  return (
    <UserContext.Provider value={{ ...user, setUser }}>
      {children}
    </UserContext.Provider>
  );
}

// 3️⃣ 子组件读取 Context
function UserProfile() {
  const { name, loggedIn } = useContext(UserContext);
  return (
    <div>
      <h2>User Profile</h2>
      <p>Name: {name}</p>
      <p>Status: {loggedIn ? 'Logged in' : 'Guest'}</p>
    </div>
  );
}

// 4️⃣ 子组件修改 Context
function LoginButton() {
  const { setUser } = useContext(UserContext);

  const login = () => {
    setUser({ name: 'Alice', loggedIn: true });
  };

  const logout = () => {
    setUser({ name: 'Guest', loggedIn: false });
  };

  return (
    <div>
      <button onClick={login}>Login</button>
      <button onClick={logout}>Logout</button>
    </div>
  );
}

// 5️⃣ App 组件
function App() {
  return (
    <UserProvider>
      <UserProfile />
      <LoginButton />
    </UserProvider>
  );
}

// 渲染
const root = ReactDOM.createRoot(document.getElementById('root')!);
root.render(<App />);

React 哲学

将UI渲染和逻辑分开,UI放在子组件中, 逻辑放在父组件中

其哲学来源于:构建一个静态版本需要写大量的代码,并不需要什么思考; 但添加交互需要大量的思考,却不需要大量的代码。

大概这样:


逻辑组件
| \
UI组件 逻辑子组件
| \ \
UI子组件 UI子组件 UI子组件

分开之后, UI组件就可以复用了

UI组件需要做的,仅仅是将props中的数据展示在页面上,而逻辑组件需要做的

所有需要变化的东西都尽量放在逻辑组件中,如果有一个status父子组件都需要用到,那么就放到父组件里面去(React官方称之为:Lifting state up(状态提升))

数据通过 props 和 state 在父子组件之间传递

props可以将数据从父组件单向传递到子组件中,适合比如 xx列表、xx信息 这样的数据

status可以利用props将组件的交互信息反向传递给父组件,适合比如 xx表单、输入框等等

评价:由类转为函数

看上去可以实现的功能变多了,结合React哲学整个项目也得到了整理,但是钩子的概念对于初学者来说不容易理解,容易引发混乱

为了结局问题导入了多种 hooks 很难说到底是更方便了还是更麻烦了

Copyright © 2022 田园幻想乡 浙ICP备2021038778号-1