1.在使用React的生命周期方法时,应该注意哪些关键问题? #

1.1 getDerivedStateFromProps #

1.1.1 When to use derived state #

import React from "react";
import ReactDOM from "react-dom/client";
function loadMyAsyncData(id) {
  let cancel;
  let asyncRequest = new Promise((resolve, reject) => {
    cancel = reject;
    setTimeout(() => {
      resolve(`Loaded data for id: ${id}`);
    }, 1000);
  });
  asyncRequest.cancel = cancel;
  return asyncRequest;
}
class User extends React.Component {
  state = {
    externalData: null,
    prevId: null,
  };
  static getDerivedStateFromProps(props, state) {
    if (props.id !== state.prevId) {
      return {
        externalData: null,
        prevId: props.id,
      };
    }
    return null;
  }
  componentDidMount() {
    this._loadAsyncData(this.props.id);
  }
  componentDidUpdate(prevProps, prevState) {
    if (
      this.state.externalData === null &&
      this.state.prevId !== prevState.prevId
    ) {
      this._loadAsyncData(this.props.id);
    }
  }
  componentWillUnmount() {
    if (this.asyncRequest) {
      this.asyncRequest.cancel();
    }
  }
  render() {
    if (this.state.externalData === null) {
      return <div>Loading...</div>;
    } else {
      return <div>{this.state.externalData}</div>;
    }
  }
  _loadAsyncData(id) {
    if (this.asyncRequest) {
      this.asyncRequest.cancel();
    }
    this.asyncRequest = loadMyAsyncData(id).then((externalData) => {
      this.asyncRequest = null;
      this.setState({ externalData });
    });
  }
}
class App extends React.Component {
  state = { id: "1" };
  handleIdChange = (event) => {
    this.setState({ id: event.target.value });
  };
  render() {
    return (
      <div>
        <User id={this.state.id} />
        <input
          type="text"
          value={this.state.id}
          onChange={this.handleIdChange}
        />
      </div>
    );
  }
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);

1.1.2 Common bugs when using derived state #

1.1.2.1 反模式:无条件地将 props 复制到 state #

一个常见的误解是,getDerivedStateFromPropscomponentWillReceiveProps 只在 props “改变”时被调用。 这些生命周期函数在父组件重新渲染时都会被调用,无论 props 是否与之前“不同”。因此,使用这些生命周期函数无条件覆盖 state 一直是不安全的。这样做会导致 state 更新丢失。

import React, { Component } from "react";
import ReactDOM from "react-dom/client";
class EmailInput extends Component {
  state = {
    email: this.props.email,
  };
  static getDerivedStateFromProps(nextProps) {
    return { email: nextProps.email };
  }
  render() {
    return <input onChange={this.handleChange} value={this.state.email} />;
  }
  handleChange = (event) => {
    this.setState({
      email: event.target.value,
    });
  };
}
class App extends Component {
  state = {
    email: "123456@qq.com",
  };
  render() {
    return (
      <div>
        <EmailInput email={this.state.email} />
        {}
      </div>
    );
  }
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
1.1.2.2 Anti-pattern: Erasing state when props change #

我们可以通过仅在props.email变化时更新状态来避免意外地擦除状态:

我们刚刚做了一个很大的改进。现在我们的组件只会在属性实际发生变化时才擦除我们所输入的内容。

import React, { Component } from "react";
import ReactDOM from "react-dom/client";
class EmailInput extends Component {
  state = {
    email: this.props.email,
  };
  static getDerivedStateFromProps(nextProps,prevState) {
+   if (nextProps.email !== prevState.email) {
+     return { email: nextProps.email };
+   }
+    return null;
  }
  render() {
    return <input onChange={this.handleChange} value={this.state.email} />;
  }
  handleChange = (event) => {
    this.setState({
      email: event.target.value,
    });
  };
}
class App extends Component {
  state = {
    email: "123456@qq.com",
  };
  render() {
    return (
      <div>
        <EmailInput email={this.state.email} />
        {}
      </div>
    );
  }
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);

这里仍然存在一个微妙的问题。想象一下一个使用上述输入组件的密码管理应用。当在两个使用相同电子邮件的账户详情之间导航时,输入框将无法重置。这是因为传递给组件的prop值对于这两个账户来说是一样的!这对用户来说会是一个意外,因为对一个账户未保存的更改看起来会影响到其他偶然使用相同电子邮件的账户。

import React, { Component } from "react";
import ReactDOM from "react-dom/client";
class EmailInput extends Component {
  constructor(props) {
    super(props);
    this.state = {
      inputValue: props.email
    };
  }
  static getDerivedStateFromProps(nextProps, prevState) {
    if (nextProps.email !== prevState.inputValue) {
      return { inputValue: nextProps.email };
    }
    return null;
  }
  handleChange = (event) => {
    this.setState({ inputValue: event.target.value });
  }
  render() {
    return (
      <input
        value={this.state.inputValue}
        onChange={this.handleChange}
        placeholder="Email"
      />
    );
  }
}
const PasswordManagerApp = () => {
  const [currentAccount, setCurrentAccount] = React.useState({ id: 1, email: 'user@example.com' });
  const switchAccount = () => {
    setCurrentAccount({ id: currentAccount.id === 1 ? 2 : 1, email: 'user@example.com' });
  };
  return (
    <div>
      <h1>Password Manager</h1>
      <button onClick={switchAccount}>Switch Account</button>
      <EmailInput email={currentAccount.email} />
    </div>
  );
};
export default PasswordManagerApp;
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<PasswordManagerApp />);

1.1.3 Preferred solutions #

1.1.3.1 Recommendation: Fully controlled component #

推荐:完全受控组件 解决上述问题的一种方法是完全去除组件的状态。如果电子邮件地址仅作为一个属性存在,那么我们就不必担心与状态的冲突。我们甚至可以将 EmailInput 转换为一个更轻量级的函数组件:

import React, { Component } from "react";
import ReactDOM from "react-dom/client";
class EmailInput extends Component {
  render() {
    return <input onChange={this.props.handleChange} value={this.props.email} />;
  }

}
class App extends Component {
  state = {
    email: "123456@qq.com",
  };
  handleChange = (event) => {
    this.setState({
      email: event.target.value,
    });
  };
  render() {
    return (
      <div>
        <EmailInput email={this.state.email}  handleChange={this.handleChange}/>
        {}
      </div>
    );
  }
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
1.1.3.2 Recommendation: Fully uncontrolled component with a key #

另一种替代方案是让我们的组件完全掌控“草稿”电子邮件状态。在这种情况下,我们的组件仍然可以接受一个属性作为初始值,但它会忽略对该属性的后续更改:

import React, { Component } from "react";
import ReactDOM from "react-dom/client";
class EmailInput extends Component {
    state = { email: this.props.defaultEmail };

    handleChange = event => {
      this.setState({ email: event.target.value });
    };

    render() {
      return <input onChange={this.handleChange} value={this.state.email} />;
    }

}
class App extends Component {
  state = {
    email: "123456@qq.com",
  };
  render() {
    return (
      <div>
        <EmailInput defaultEmail={this.state.email}  />
        {}
      </div>
    );
  }
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);

1.1.4 What about memoization? #

我们也见过派生状态用于确保在渲染中使用的昂贵值仅在输入改变时重新计算。这种技术被称为记忆化。

使用派生状态进行记忆化并不一定是坏事,但通常不是最佳解决方案。管理派生状态本身就有固有的复杂性,而且随着额外属性的增加,这种复杂性会增加。例如,如果我们在组件状态中添加第二个派生字段,那么我们的实现就需要分别跟踪这两个字段的变化。

让我们看一个例子,这个组件接受一个属性——一个项目列表——并渲染与用户输入的搜索查询匹配的项目。我们可以使用派生状态来存储过滤后的列表:

import React, { Component, Fragment } from 'react';
import ReactDOM from "react-dom/client";

class Todos extends Component {
  state = {
    filterText: "",
    filteredList: this.props.list,
    prevPropsList: this.props.list,
    prevFilterText: "",
  };

  static getDerivedStateFromProps(props, state) {
    if (
      props.list !== state.prevPropsList ||
      state.prevFilterText !== state.filterText
    ) {
      return {
        prevPropsList: props.list,
        prevFilterText: state.filterText,
        filteredList: props.list.filter(item => item.text.includes(state.filterText))
      };
    }
    return null;
  }

  handleChange = event => {
    this.setState({ filterText: event.target.value });
  };

  render() {
    return (
      <>
        <input onChange={this.handleChange} value={this.state.filterText} />
        <ul>{this.state.filteredList.map(item => <li key={item.id}>{item.text}</li>)}</ul>
      </>
    );
  }
}
class App extends Component {
  render() {
    const list = [
      { id: 1, text: 'Item 1' },
      { id: 2, text: 'Item 2' },
      { id: 3, text: 'Item 3' }
    ];

    return <Todos list={list} />;
  }
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);

这种实现避免了不必要地频繁重新计算filteredList。但是它比实际需要的更复杂,因为它必须分别跟踪并检测属性和状态的变化,以便正确更新过滤后的列表。在这个例子中,我们可以通过使用PureComponent并将过滤操作移动到render方法中来简化事情:

import React, { Component, Fragment } from "react";
import ReactDOM from "react-dom/client";

class Todos extends PureComponent  {
  state = {
    filterText: ""
  };

  handleChange = (event) => {
    this.setState({ filterText: event.target.value });
  };

  render() {
+      const filteredList = this.props.list.filter(item => item.text.includes(this.state.filterText))
    return (
      <>
        <input onChange={this.handleChange} value={this.state.filterText} />
        <ul>
          {filteredList.map((item) => (
            <li key={item.id}>{item.text}</li>
          ))}
        </ul>
      </>
    );
  }
}
class App extends Component {
  render() {
    const list = [
      { id: 1, text: "Item 1" },
      { id: 2, text: "Item 2" },
      { id: 3, text: "Item 3" },
    ];

    return <Todos list={list} />;
  }
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);

这种方法比派生状态版本更简洁、更清晰。但有时这还不够好——对于大型列表,过滤可能会很慢,而且如果另一个属性发生变化,PureComponent也无法阻止重新渲染。为了解决这两个问题,我们可以添加一个备忘录辅助功能,以避免不必要地重新过滤我们的列表:

memoize-one 是一个 JavaScript 库,用于创建一个记忆化(memoized)函数。这意味着当你用相同的参数多次调用该函数时,它只会在第一次计算结果,之后会返回缓存的结果。这在处理重计算成本高的函数时非常有用,比如渲染、复杂的计算等。

import React, { Component, PureComponent } from "react";
import ReactDOM from "react-dom/client";
+function memoizeOne(func) {
+    let lastArgs = null;
+    let lastResult = null;
+    return function(...args) {
+
+        if (lastArgs !== null && args.length === lastArgs.length && args.every((val, index) => val === lastArgs[index])) {
+            return lastResult; 
+        }
+        lastArgs = args;
+        lastResult = func.apply(this, args);
+        return lastResult;
+    };
+}
class Todos extends PureComponent {
  state = {
    filterText: ""
  };
+ filter = memoizeOne(
+   (list, filterText) => list.filter(item => item.text.includes(filterText))
+ );
  handleChange = (event) => {
    this.setState({ filterText: event.target.value });
  };
  render() {
    const filteredList = this.filter(this.props.list, this.state.filterText);
    return (
      <>
        <input onChange={this.handleChange} value={this.state.filterText} />
        <ul>
          {filteredList.map((item) => (
            <li key={item.id}>{item.text}</li>
          ))}
        </ul>
      </>
    );
  }
}
class App extends Component {
  render() {
    const list = [
      { id: 1, text: "Item 1" },
      { id: 2, text: "Item 2" },
      { id: 3, text: "Item 3" },
    ];
    return <Todos list={list} />;
  }
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);

1.2 ErrorBoundary #

React 中的 ErrorBoundary 是一种 React 组件,用于捕获其子组件树中的 JavaScript 错误,并记录这些错误,并显示一个回退的 UI,而不是使整个组件树崩溃。Error boundaries 仅捕获其子组件树中的错误,无法捕获其自身的错误。

要使用 Error Boundary,你需要创建一个类组件,并在其中定义至少一个错误生命周期方法,即 static getDerivedStateFromError()componentDidCatch()

下面是一个 ErrorBoundary 组件的示例,以及如何在应用程序中使用它:

import React from "react";
import ReactDOM from "react-dom/client";
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      hasError: false,
    };
  }
  static getDerivedStateFromError(error) {
    return {
      hasError: true,
    };
  }
  componentDidCatch(error, errorInfo) {
    console.error("Uncaught error:", error, errorInfo);
  }
  render() {
    if (this.state.hasError) {
      return <h1>Something went wrong.</h1>;
    }
    return this.props.children;
  }
}
function Main() {
  console.log(undefined.a);
  return <h1>Main</h1>;
}
class ParentComponent extends React.Component {
  render() {
    return (
      <div>
        <div>Header</div>
        <ErrorBoundary>
          <Main />
        </ErrorBoundary>
        <div>Footer</div>
      </div>
    );
  }
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<ParentComponent />);

在上述示例中,ErrorBoundary 组件会在其子组件(这里是 SomeOtherComponent)出错时,显示一个简单的错误信息。你可以根据需要自定义这个回退 UI。需要注意的是,Error Boundary 并不能捕获事件处理器内部的错误,异步代码(例如 setTimeoutrequestAnimationFrame 回调函数),服务器端渲染,它自己抛出来的错误(而不是其子组件)。

2. React函数组件和类组件有什么相同点和不同点? #

相同点

  1. 渲染 UI: 无论是函数组件还是类组件,都用于渲染 UI,并且可以接受 props 来定制显示内容。
  2. 使用方式和表达效果 函数组件和类组件的使用方式和表达效果是一样的

不同点

  1. 设计思想 类组件是面向对象编程,可以实现继承,函数组件是函数式编程
  2. 语法和结构: 类组件使用 ES6 类语法定义,拥有多个生命周期方法,而函数组件是普通的 JavaScript 函数,通常更简洁。
  3. Hooks使用: 只有函数组件可以使用 React Hooks(如 useState, useEffect),类组件不能使用这些钩子。
  4. 状态管理: 类组件通过 this.statethis.setState 管理状态,而函数组件可以通过 useState 钩子管理状态。
  5. this 关键字: 类组件中经常需要使用 this 来访问 props、状态和类方法。而函数组件中没有 this,一切都通过函数参数或钩子访问。
  6. 生命周期方法: 类组件有完整的生命周期方法(如 componentDidMount, componentDidUpdate, componentWillUnmount),而函数组件则使用钩子如 useEffect 来模拟这些生命周期。
  7. 组件重用和组合: 函数组件倾向于促进重用和组合的小型、专注的组件。而类组件更适合于需要复杂状态逻辑和生命周期管理的场景。
  8. Context 使用: 类组件通过static contextTypes使用Context,函数组件使用ConsumeruseContext
  9. 性能优化: 函数组件使用React.memo、useMemo、useCallback进行性能优化,而类组件使用PureComponent和重写shouldComponentUpdate
  10. 逻辑复用: 函数组件使用自定义Hooks、高阶组件、组合、Render Props等方式进行性能优化,而类组件使用高阶组件(HOCs)、继承(Inheritance)、Mixins等方式进行逻辑复用
  11. 趋势 随着 React 的发展,函数组件因其简洁性和对钩子的支持而变得越来越流行,但类组件在某些复杂场景中仍有其独特的优势。

3.React中如何进行逻辑复用? #

在React中,逻辑复用可以通过几种不同的方式实现,这取决于你是在使用函数组件还是类组件。下面是一些常见的方法:

3.1 函数组件 #

3.1.1 自定义Hooks #

React 的自定义 Hooks 是 React 功能的一个扩展,允许你在函数组件中重用状态逻辑,而无需改变组件结构。自定义 Hooks 本质上是 JavaScript 函数,但它们遵循一些特定的规则并利用 React 的基础 Hooks(如 useState, useEffect, useContext 等)。

以下是自定义 Hooks 的一些关键点:

  1. 重用状态逻辑:自定义 Hooks 允许你将组件逻辑提取到可重用的函数中。这意味着相同的状态管理和副作用逻辑可以在多个组件中共享,而无需重复代码。

  2. 命名约定:自定义 Hooks 应以 use 开头,这不仅是一种命名约定,也是 React 自动应用 Hooks 规则的一部分。

  3. 封装和组织:通过自定义 Hooks,你可以将相关逻辑组织在一起,使代码更易于理解和维护。例如,你可以创建一个 useForm Hook 来处理表单输入和验证。

  4. 使用基础 Hooks:自定义 Hooks 可以调用其他 Hooks,这意味着你可以在自定义 Hook 中使用 useState, useEffect, useContext 等基础 Hooks。

import React, { useState } from "react";
import ReactDOM from "react-dom/client";
const useForm = (initialValues) => {
  const [values, setValues] = useState(initialValues);
  const handleChange = (event) => {
    const { name, value } = event.target;
    setValues({
      ...values,
      [name]: value,
    });
  };

  const handleSubmit = (event) => {
    event.preventDefault();
    console.log(values);
  };
  return { values, handleChange, handleSubmit };
};
const LoginForm = () => {
  const { values, handleChange, handleSubmit } = useForm({ username: "", password: "" });
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label>Username</label>
        <input
          type="text"
          name="username"
          value={values.username}
          onChange={handleChange}
        />
      </div>
      <div>
        <label>Password</label>
        <input
          type="password"
          name="password"
          value={values.password}
          onChange={handleChange}
        />
      </div>
      <button type="submit">Login</button>
    </form>
  );
};
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<LoginForm />);

3.1.2 高阶组件 #

高阶组件(HOCs)是 React 中用于重用组件逻辑的一种高级技术。简单来说,它们是参数为组件、返回值为新组件的函数。HOCs 让你可以把组件当作参数传入,然后返回一个新的“增强”组件,这个新组件拥有额外的属性或行为。

这里有几个关键点来理解高阶组件:

  1. 函数式编程概念:HOC 是一种基于函数式编程的模式。在函数式编程中,函数可以接受函数作为参数,并返回一个函数。
  2. 组件重用:HOC 允许你抽象出共通的逻辑,避免代码重复。例如,你可能有几个组件都需要进行数据获取,通过使用 HOC,你可以只编写一次数据获取逻辑,并将其应用于所有需要它的组件。
  3. 不修改原组件:HOC 不应该修改传入的组件,而是返回一个新的组件。这有助于保持原始组件的纯净性和可重用性。
  4. 组合:HOC 可以相互组合。你可以将一个组件通过多个 HOC 传递,每个都添加自己的功能。
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom/client";
const withLoading = WrappedComponent => {
  return props => {
    const [isLoading, setIsLoading] = useState(true);
    useEffect(() => {
      setTimeout(() => {
        setIsLoading(false);
      }, 2000);
    }, []);
    if (isLoading) {
      return <div>Loading...</div>;
    }
    return <WrappedComponent {...props} />;
  };
};
const MyComponent = () => {
  return <div>内容加载完成!</div>;
};
const MyComponentWithLoading = withLoading(MyComponent);
const App = () => {
  return<MyComponentWithLoading />;
};
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);

3.1.3 Render Props #

使用一个值为函数的prop(属性),这个函数返回要渲染的React元素。通过这种方式,可以在多个组件间共享逻辑。

import React, { useState, useEffect, Component } from "react";
import ReactDOM from "react-dom/client";
class MouseTracker extends Component {
  constructor(props) {
    super(props);
    this.state = {
      x: 0,
      y: 0,
    };
  }
  handleMouseMove = (event) => {
    this.setState({
      x: event.clientX,
      y: event.clientY,
    });
  };
  render() {
    return (
      <div
        style={{
          height: "100vh",
        }}
        onMouseMove={this.handleMouseMove}
      >
        {this.props.render(this.state)}
      </div>
    );
  }
}
const App = () => (
  <div>
    <h1>Move the mouse around!</h1>
    <MouseTracker
      render={({ x, y }) => (
        <p>
          The current mouse position is ({x}, {y})
        </p>
      )}
    />
  </div>
);
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);

3.2 类组件 #

3.2.1 高阶组件 #

函数组件一样,HOCs也可以用于类组件来封装并复用逻辑

import React, { useState } from "react";
import ReactDOM from "react-dom/client";

function withAuthentication(WrappedComponent) {
  class WithAuthentication extends React.Component {
    isLoggedIn() {
      return false;
    }
    render() {
      if (!this.isLoggedIn()) {
        return <div>Please log in to view this page.</div>;
      }
      return <WrappedComponent {...this.props} />;
    }
  }
  return WithAuthentication;
}
class MyComponent extends React.Component {
  render() {
    return <div>Welcome to the protected page!</div>;
  }
}
const ProtectedMyComponent = withAuthentication(MyComponent);
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<ProtectedMyComponent />);

3.2.2 透传静态属性 #

import React, { useState } from "react";
import ReactDOM from "react-dom/client";
//import hoistNonReactStatics from 'hoist-non-react-statics';
function hoistNonReactStatics(targetComponent, sourceComponent) {
    const REACT_STATICS = {
      displayName: true,
      propTypes: true,
      defaultProps: true,
      contextTypes: true,
      childContextTypes: true,
    };
    Object.getOwnPropertyNames(sourceComponent).forEach((key) => {
      if (!REACT_STATICS[key]) {
        try {
          if (
            !Object.getOwnPropertyDescriptor(targetComponent, key) || Object.getOwnPropertyDescriptor(targetComponent, key).configurable
          ) {
            Object.defineProperty(
              targetComponent,
              key,
              Object.getOwnPropertyDescriptor(sourceComponent, key)
            );
          }
        } catch (error) {
          console.log(error);
        }
      }
    });
    return targetComponent;
  }
function withAuthentication(WrappedComponent) {
  class WithAuthentication extends React.Component {
    isLoggedIn() {
      return false;
    }
    render() {
      if (!this.isLoggedIn()) {
        return <div>Please log in to view this page.</div>;
      }
      return <WrappedComponent {...this.props} />;
    }
  }
  hoistNonReactStatics(WithAuthentication, WrappedComponent);
  return WithAuthentication;
}
class MyComponent extends React.Component {
  static someStaticProperty = "Some Value";
  static someStaticMethod() {
    console.log(`someStaticMethod`);
  }
  render() {
    return <div>Welcome to the protected page!</div>;
  }
}
const ProtectedMyComponent = withAuthentication(MyComponent);
console.log(ProtectedMyComponent.someStaticProperty);
ProtectedMyComponent.someStaticMethod();
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<ProtectedMyComponent />);

3.2.3 透传ref #

高阶组件(HOC)在React中是一种重用组件逻辑的常见模式。然而,它们有一个常见的问题:refs属性不会像其他props一样自动透传到被包裹的组件。这是因为ref不是一个prop,而是一个由React特别处理的属性,用于获取组件实例或DOM元素的引用。

为什么refs不会透传?

当你在父组件中使用ref时,React会处理这个ref并将其附加到相应的元素或组件实例上,而不是将其作为prop传递。这意味着,如果你将ref作为prop传递给HOC,HOC本身会捕获这个ref,而不是传递它到子组件。

解决方案:使用React.forwardRef

要解决这个问题,你可以使用React.forwardRef。这个API允许你将ref自动转发到另一个组件。这样,即使是在HOC中,ref也可以正确地传递给被包裹的组件。

import React from "react";
import ReactDOM from "react-dom/client";
class MyComponent extends React.Component {
  render() {
    return (
      <div>
        <h1>Hello, {this.props.name}</h1>
      </div>
    );
  }
}
function withHOC(Component) {
  return React.forwardRef((props, forwardedRef) => (
    <Component {...props} ref={forwardedRef} />
  ));
}
const EnhancedComponent = withHOC(MyComponent);
class App extends React.Component {
  myComponentRef = React.createRef();
  componentDidMount() {
    console.log(this.myComponentRef.current);
  }
  render() {
    return <EnhancedComponent name="World" ref={this.myComponentRef} />;
  }
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);

3.2.4 高阶组件链式调用 #

链式调用高阶组件是React中一个常见的模式,它允许你将多个高阶组件(HOCs)组合在一起,为组件提供额外的功能。

import React, { useState } from "react";
import ReactDOM from "react-dom/client";
import hoistNonReactStatics from 'hoist-non-react-statics';
+function withLogging(WrappedComponent) {
+    return class extends React.Component {
+      componentDidMount() {
+          console.log(`${WrappedComponent.name} has been mounted`);
+      }
+      componentWillUnmount() {
+          console.log(`${WrappedComponent.name} will be unmounted`);
+      } 
+      render() {
+          return <WrappedComponent {...this.props} />;
+      }
+    };
+} 
function withAuthentication(WrappedComponent) {
  class WithAuthentication extends React.Component {
    isLoggedIn() {
      return false;
    }
    render() {
      if (!this.isLoggedIn()) {
        return <div>Please log in to view this page.</div>;
      }
      return <WrappedComponent {...this.props} />;
    }
  }
  hoistNonReactStatics(WithAuthentication, WrappedComponent);
  return WithAuthentication;
}
class MyComponent extends React.Component {
  static someStaticProperty = "Some Value";
  static someStaticMethod() {
    console.log(`someStaticMethod`);
  }
  render() {
    return <div>Welcome to the protected page!</div>;
  }
}
+const EnhancedMyComponent = withLogging(withAuthentication(MyComponent));
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<EnhancedMyComponent />);

3.2.5 渲染劫持 #

渲染劫持是高阶组件(HOC)中的一个概念,它指的是HOC在不改变被包裹组件本身的情况下,改变其渲染输出的能力。这可以包括条件渲染、添加额外的props、甚至是完全改变渲染结构。

渲染劫持的用途

  1. 条件渲染:根据特定的条件决定是否渲染组件。
  2. 属性操作:向被包裹的组件注入新的props或修改现有的props。
  3. 结构变更:改变组件的DOM结构或样式。
import React, { useState } from "react";
import ReactDOM from "react-dom/client";
const MyComponent = (props) => (
  <div>
    <h1>Hello, {props.name}</h1>
  </div>
);
function withRenderHijacking(WrappedComponent) {
  return class extends React.Component {
    render() {
      if(this.props.loading){
        return <div>Loading...</div>
      }
      const elementsTree = <WrappedComponent {...this.props} />;
      if (this.props.hijack) {
        return (
          <div style={{ color: "red" }}>
            <h2>Hijacked!</h2>
            {React.cloneElement(elementsTree, {
              ...this.props,
              name: "Hijacked Name",
              style:{color:'red'}
            })}
          </div>
        );
      }
      return elementsTree;
    }
  };
}
const HijackedMyComponent = withRenderHijacking(MyComponent);
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<HijackedMyComponent hijack loading={false} />);

还可以通过继承实现

import React from "react";
import ReactDOM from "react-dom/client";
class ClassMyComponent extends React.Component {
  render() {
    return (
      <div>
        <h1>Hello, {this.state.name}</h1>
      </div>
    );
  }
}
function withRenderHijacking(WrappedComponent) {
  return class extends WrappedComponent {
    state={name:"Hijacked Name"}
    render() {
      if (this.props.loading) {
        return <div>Loading...</div>;
      }
      const elementsTree = super.render();
      if (this.props.hijack) {
        return (
          <div style={{color: "red"}}>
            <h2>Hijacked!</h2>
            {React.cloneElement(elementsTree, {
              ...this.props
            })}
          </div>
        );
      }
      return elementsTree;
    }
  };
}
const HijackedMyComponent = withRenderHijacking(ClassMyComponent);
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<HijackedMyComponent hijack loading={false} />);

3.2.6 继承 #

React类组件可以通过继承基类组件来复用逻辑

import React, { useState } from "react";
import ReactDOM from "react-dom/client";
class BaseComponent extends React.Component {
  static sharedStaticProperty = "Shared Value";
  constructor(props) {
    super(props);
    this.state = {
      sharedState: "Initial State",
    };
  }
  sharedMethod() {
    console.log("Shared method called");
  }
  render() {
    return (
      <div>
        <h2>Base Component</h2>
        <p>Static Property: {this.constructor.sharedStaticProperty}</p>
        <p>State: {this.state.sharedState}</p>
      </div>
    );
  }
}
class DerivedComponent extends BaseComponent {
  componentDidMount() {
    this.sharedMethod();
  }
  render() {
    return (
      <div>
        <h2>Derived Component</h2>
        <p>Static Property: {this.constructor.sharedStaticProperty}</p>
        <p>State: {this.state.sharedState}</p>
      </div>
    );
  }
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<DerivedComponent />);

3.2.7 Mixins #

在React早期版本中,Mixins被用来共享组件逻辑,但由于各种问题,现在已不推荐使用

4.如何设计React组件 #

4.1 组件的分类 #

4.2 无状态组件 #

无状态组件,也称为功能性组件(Functional Components),是React中最简单的组件形式。它们仅依赖于传入的props来显示内容或行为,并不管理或维护内部状态(即没有state)。

4.2.1 代理组件 #

代理组件(Proxy Component)是一种设计模式,它允许你在不修改原始组件代码的情况下增强或修改该组件的行为

通过这种对第三方组件的封装还可以隔离第三方组件。可以方便以后重构替换和实现公共逻辑

import React from "react";
import ReactDOM from "react-dom/client";
import { Button } from "antd";
class ButtonWithLogging extends React.Component {
  handleClick = (e) => {
    if (this.props.onClick) {
      this.props.onClick(e);
    }
    console.log("Button was clicked!");
  };
  render() {
    return <Button type="primary" {...this.props} onClick={this.handleClick} />;
  }
}
const App = () => (
  <div>
    <ButtonWithLogging type="primary">Click me</ButtonWithLogging>
  </div>
);
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);

4.2.2 样式组件 #

在React中使用样式组件(Styled Components)是一种流行的方式来封装和复用组件的样式

styled-components库允许你编写实际的CSS代码来为你的React组件设置样式,同时保持了样式与组件的封装性。

import React from "react";
import ReactDOM from "react-dom/client";
//import styled from 'styled-components';
const styled = {
  h1: styles => {
    return props => <h1 style={parseStyles(styles[0])} {...props} />;
  },
  button: styles => {
    return props => <button style={parseStyles(styles[0])} {...props} />;
  }
};
function convertToCamelCase(cssProperty) {
    return cssProperty.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
}
function parseStyles(styles) {
  return styles.split(";").reduce((styleObject, style) => {
    const [property, value] = style.split(":");
    if (property && value) {
      styleObject[convertToCamelCase(property.trim())] = value.trim();
    }
    return styleObject;
  }, {});
}
const Title = styled.h1`
  color: palevioletred;
`;
const StyledButton = styled.button`
  background-color: palevioletred;
  color: white;
`;
const App = () => (
  <div>
    <Title>Welcome to Styled Components!</Title>
    <StyledButton>Click me</StyledButton>
  </div>
);
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);

4.2.3 布局组件 #

布局组件是一种常见的模式,用于封装和复用页面布局

import React from "react";
import ReactDOM from "react-dom/client";
const Header = () => (
  <div
    style={{
      background: "lightblue",
      padding: "10px",
      textAlign: "center",
    }}
  >
    <h1>Header</h1>
  </div>
);
const Footer = () => (
  <div
    style={{
      background: "lightgreen",
      padding: "10px",
      textAlign: "center",
    }}
  >
    <p>Footer</p>
  </div>
);
const MainContent = () => (
  <div
    style={{
      padding: "20px",
    }}
  >
    MainContent
  </div>
);
const Layout = React.memo(() => (
  <div>
    <Header />
    <MainContent />
    <Footer />
  </div>
));
const App = () => <Layout />;
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);

4.3 有状态组件 #

有状态组件是指在其内部维护一个或多个状态的React组件。这些状态的变化可能会导致组件重新渲染。

4.3.1 容器组件 #

在React中,容器组件是一种专门用于处理数据获取和业务逻辑的组件,而展示组件则关注于如何展示这些数据。容器组件负责从外部源(如API、Redux store等)获取数据,然后将这些数据传递给展示组件。

import React, { useState, useEffect } from 'react';
import ReactDOM from 'react-dom/client';
const UserList = ({ users}) => <div>
    <h2>User List</h2>
    <ul>
      {users.map(user => <li key={user.id}>{user.name}</li>)}
    </ul>
  </div>;
const UserListContainer = () => {
  const [users, setUsers] = useState([]);
  const [isLoading, setIsLoading] = useState(true);
  useEffect(() => {
    setTimeout(() => {
      setUsers([{id: 1,name: '张三'}, {id: 2,name: '李四'}]);
      setIsLoading(false);
    }, 1000);
  }, []);
  if (isLoading) {
    return <div>Loading...</div>;
  }
  return <UserList users={users} />;
};
const App = () => <div>
    <UserListContainer />
  </div>;
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

4.4 项目目录结构划分 #

src/
|-- hoc/                 // 高阶组件
|-- components/          // 通用组件
|   |-- Button/
|   |   |-- index.js     // Button组件
|   |   |-- styles.css   // Button的样式
|-- pages/            // 页面
|   |-- pageA/
|   |   |-- components/  // pageA的专用组件
|   |   |-- hooks/       // pageA的hooks
|   |   |-- index.js     // pageA的主组件
|   |-- FeatureB/
|   |   ...
|-- hooks/               // 通用的自定义hooks
|-- utils/               // 工具函数
|-- app.js               // 应用入口组件
|-- index.js             // ReactDOM渲染入口

5.React中如何实现组件通信 #

5.1 父组件向子组件传递数据 #

在React中,父组件向子组件传递数据通常是通过props(属性)来实现的。这是一种单向数据流,确保了组件间的数据传输清晰且易于追踪。

import React, { useState } from "react";
import ReactDOM from "react-dom/client";
class ChildComponent extends React.Component {
  render() {
    return <div>{this.props.text}</div>;
  }
}
class ParentComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      text: "加载中...",
    };
  }
  componentDidMount() {
    setTimeout(() => {
      this.setState({
        text: "从网络请求获得的文案",
      });
    }, 2000);
  }
  render() {
    return (
      <div>
        <h1>父组件</h1>
        <ChildComponent text={this.state.text} />
      </div>
    );
  }
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<ParentComponent />);

5.2 子组件向父组件传递数据 #

5.2.1 回调函数 #

在React中,子组件向父组件传递数据通常通过回调函数来实现。父组件定义一个函数并通过props传递给子组件,子组件在某个事件(如用户操作)发生时调用这个回调函数,从而将数据传递回父组件。

import React from "react";
import ReactDOM from "react-dom/client";
class ChildComponent extends React.Component {
  sendDataToParent = () => {
    this.props.onReceiveData("子组件发送的数据");
  };
  render() {
    return <button onClick={this.sendDataToParent}>发送数据给父组件</button>;
  }
}
class ParentComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      dataFromChild: "",
    };
  }
  handleDataFromChild = (data) => {
    this.setState({
      dataFromChild: data,
    });
  };
  render() {
    return (
      <div>
        <h1>父组件</h1>
        <ChildComponent onReceiveData={this.handleDataFromChild} />
        <p>接收到的子组件数据:{this.state.dataFromChild}</p>
      </div>
    );
  }
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<ParentComponent />);

5.2.2 实例方法 #

import React,{Component} from "react";
import ReactDOM from "react-dom/client";
class ModalComponent extends Component {
  constructor(props) {
    super(props);
    this.state = { isVisible: false };
  }
  show = () => {
    this.setState({ isVisible: true });
  };
  hide = () => {
    this.setState({ isVisible: false });
  };
  render() {
    const { isVisible } = this.state;
    return (
      <div style={{ display: isVisible ? "block" : "none" }}>
        <h2>模态窗口</h2>
        <button onClick={this.hide}>关闭</button>
      </div>
    );
  }
}
class ParentComponent extends Component {
  modalRef = React.createRef();
  openModal = () => {
    this.modalRef.current.show();
  };
  render() {
    return (
      <div>
        <button onClick={this.openModal}>打开模态窗口</button>
        <ModalComponent ref={this.modalRef} />
      </div>
    );
  }
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<ParentComponent />);

5.3 兄弟组件传递数据 #

在React中,兄弟组件之间直接传递数据较为复杂,因为React的数据流是单向的,从父组件向子组件流动。因此,当需要在兄弟组件之间传递数据时,通常通过它们共同的父组件来协调这一过程。具体来说,一个兄弟组件将数据传递给父组件,然后父组件再将这些数据传递给另一个兄弟组件。

import React from "react";
import ReactDOM from "react-dom/client";
class SiblingComponentOne extends React.Component {
  sendData = () => {
    this.props.onSendData("来自兄弟组件一的数据");
  };
  render() {
    return <button onClick={this.sendData}>发送数据到兄弟组件二</button>;
  }
}
class SiblingComponentTwo extends React.Component {
  render() {
    return <div>从兄弟组件一接收的数据:{this.props.data}</div>;
  }
}
class ParentComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      data: "",
    };
  }
  handleData = (data) => {
    this.setState({
      data: data,
    });
  };
  render() {
    return (
      <div>
        <h1>父组件</h1>
        <SiblingComponentOne onSendData={this.handleData} />
        <SiblingComponentTwo data={this.state.data} />
      </div>
    );
  }
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<ParentComponent />);

5.4 无直接关系组件通信 #

5.4.1 Context API #

React的Context API允许我们跨组件树传递数据,非常适合用于管理应用的本地化(国际化)设置

import React,{useState,useContext} from "react";
import ReactDOM from "react-dom/client";
const LanguageContext = React.createContext();
export const LanguageProvider = ({ children }) => {
  const [language, setLanguage] = useState("en");
  const switchLanguage = (lang) => {
    setLanguage(lang);
  };
  return (
    <LanguageContext.Provider value={{ language, switchLanguage }}>
      {children}
    </LanguageContext.Provider>
  );
};
const texts = {
  en: {
    greeting: "Hello",
    farewell: "Goodbye",
  },
  cn: {
    greeting: "你好",
    farewell: "再见",
  },
};
const LanguageSwitcher = () => {
  const { switchLanguage } = useContext(LanguageContext);
  return (
    <div>
      <button onClick={() => switchLanguage("en")}>English</button>
      <button onClick={() => switchLanguage("cn")}>中文</button>
    </div>
  );
};
const LocalizedText = () => {
  const { language } = useContext(LanguageContext);
  return (
    <div>
      <p>{texts[language].greeting}</p>
      <p>{texts[language].farewell}</p>
    </div>
  );
};
const App = () => {
  return (
    <LanguageProvider>
      <LanguageSwitcher />
      <LocalizedText />
    </LanguageProvider>
  );
};
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
import React,{useState,useContext} from "react";
import ReactDOM from "react-dom/client";
const LanguageContext = React.createContext();
const withLanguage = (WrappedComponent) => {
  return (props) => {
      const { language, switchLanguage } = useContext(LanguageContext);
      return <WrappedComponent language={language} switchLanguage={switchLanguage} {...props} />;
  };
};
const LanguageSwitcher = withLanguage(({ switchLanguage }) => {
  return (
      <div>
          <button onClick={() => switchLanguage('en')}>English</button>
          <button onClick={() => switchLanguage('cn')}>中文</button>
      </div>
  );
});
const LocalizedText = withLanguage(({ language }) => {
  return (
      <div>
          <p>{texts[language].greeting}</p>
          <p>{texts[language].farewell}</p>
      </div>
  );
});
export const LanguageProvider = ({ children }) => {
  const [language, setLanguage] = useState("en");
  const switchLanguage = (lang) => {
    setLanguage(lang);
  };
  return (
    <LanguageContext.Provider value={{ language, switchLanguage }}>
      {children}
    </LanguageContext.Provider>
  );
};
const texts = {
  en: {
    greeting: "Hello",
    farewell: "Goodbye",
  },
  cn: {
    greeting: "你好",
    farewell: "再见",
  },
};
const App = () => {
  return (
    <LanguageProvider>
      <LanguageSwitcher />
      <LocalizedText />
    </LanguageProvider>
  );
};
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);

5.4.2 全局事件和全局变量 #

import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom/client";
const globalData = {
  message: "",
};
const MessageSetter = () => {
  const setMessage = (msg) => {
    globalData.message = msg;
    window.dispatchEvent(new Event("globalDataChanged"));
  };
  return (
    <div>
      <button onClick={() => setMessage("Hello from Setter!")}>
        Set Message
      </button>
    </div>
  );
};
const MessageViewer = () => {
  const [message, setMessage] = useState(globalData.message);
  useEffect(() => {
    const handler = () => setMessage(globalData.message);
    window.addEventListener("globalDataChanged", handler);
    return () => {
      window.removeEventListener("globalDataChanged", handler);
    };
  }, []);
  return (
    <div>
      <p>Message: {message}</p>
    </div>
  );
};
const App = () => {
  return (
    <div>
      <MessageSetter />
      <MessageViewer />
    </div>
  );
};
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);

5.4.3 第三方状态管理库 #

React的生态系统中出现了多种第三方状态管理库,它们各自以不同的方式解决了React应用中的状态管理问题。从它们首次出现的顺序来看,以下是一些最常见的状态管理库:

  1. Flux(2014年)

    • Flux是由Facebook开发的,是一种早期的React状态管理模式。它引入了单向数据流的概念,对React应用的状态管理产生了深远影响。尽管它本身不是一个库,但它启发了许多后续的状态管理解决方案。
  2. Redux(2015年)

    • Redux是由Dan Abramov和Andrew Clark开发的。它是基于Flux思想,但提供了更简化的实现。Redux通过单一的状态树(store)来管理状态,这使得状态更加可预测和易于维护。Redux的核心原则包括单一数据源、状态是只读的、使用纯函数来执行修改。
  3. MobX(2016年)

    • MobX由Michel Weststrate开发,它采用了不同于Redux的响应式编程范式。MobX通过透明地应用函数式响应式编程(FRP)原则,简化了状态管理。它允许状态可以被可变更,并通过观察者模式自动追踪依赖和更新UI。
  4. Recoil(2020年)

    • Recoil是由Facebook的React团队开发的一个状态管理库。它提供了一种更加轻量和React原生的方式来管理全局状态。Recoil通过原子(atoms)和选择器(selectors)的概念来管理状态,这使得状态共享在组件树中更加灵活和高效。
  5. Zustand(2020年)

    • Zustand是一个简单、小巧、快速的状态管理库。它不强制使用Redux或Flux的模式,而是提供了一个简洁直观的API来创建全局状态。Zustand非常适合那些希望避免模板代码和复杂概念的React开发者。
  6. Jotai(2020年)

    • Jotai 是一个非常轻量级的状态管理库,它的设计理念是基于原子概念。在Jotai中,状态被分解为多个小的、独立的单元,称为"原子"(atoms)。这些原子可以在任何React组件中使用,并且当它们被修改时,只有依赖于这些原子的组件会重新渲染。
    • Jotai的目标是保持简单和最小化,同时提供足够的灵活性和优化性能。它不需要大量的样板代码,使得状态的共享和管理更加直观。
    • Jotai特别适合于那些寻求轻量级解决方案且倾向于更声明式的编程风格的项目。
  7. Valtio(2020年)

    • Valtio 采用了代理(Proxy)的方式来管理状态,这使得状态的读写操作变得直观且自然。在Valtio中,您可以直接修改状态,而无需通过特殊的函数或方法。当状态改变时,只有依赖于这些状态的组件会重新渲染。
    • 它提供了一种更加接近JavaScript本身的方式来处理状态,减少了学习曲线,并且使得代码更容易理解和维护。
    • Valtio的这种方式非常适合那些喜欢直接操作和简单直观API的开发者,同时它也非常适用于大型应用。

6.如何减少组件的重新渲染? #

6.1 什么时候需要性能优化? #

6.1.1 FPS #

FPS指的是“每秒帧数”(Frames Per Second)或“每秒画面数”。这是衡量视频、游戏、动画或任何动态视觉媒体流畅度的关键指标 计算 Web 页面的每秒帧数(FPS)通常涉及跟踪浏览器在单位时间内能渲染多少帧。

FPS 范围及其体验

6.1.2 如何计算 Web 页面的 FPS #

6.1.2.1 使用浏览器开发者工具: #

6.1.2.2 使用 JavaScript 计算 FPS #

计算 Web 页面的 FPS(每秒帧数)通常需要使用浏览器的性能监测工具或编写特定的 JavaScript 代码。下面是一种常用的方法来计算网页的 FPS:

  1. 定义变量:

    • 设置用于存储帧数的变量和计算帧率的时间间隔(通常是一秒)。
  2. 使用 requestAnimationFrame:

    • requestAnimationFrame 方法用于告诉浏览器你希望执行动画,并请求浏览器在下次重绘之前调用你指定的回调函数。这个回调函数的执行频率通常与浏览器的重绘频率一致。
  3. 计算帧数:

    • 每次 requestAnimationFrame 的回调函数被执行时,增加帧数的计数。
    • 使用一个定时器(例如 setInterval),每秒计算一次过去一秒内的帧数,然后重置帧数计数器。
  4. 显示 FPS:

    • 将每秒的帧数显示在网页上,以便观察。
let frameCount = 0;
let lastSecond = Date.now();
let fps = 0;
function updateFrameCount() {
  frameCount++;
  const now = Date.now();
  if (now - lastSecond >= 1000) {
    fps = frameCount;
    frameCount = 0;
    lastSecond = now;
    console.log(`FPS: ${fps}`); 
  }
  requestAnimationFrame(updateFrameCount);
}
requestAnimationFrame(updateFrameCount);

6.2 React组件在什么情况下会重新渲染 #

React 组件的重新渲染(re-render)通常是由于其内部状态(state)或传入的属性(props)发生变化。以下是触发 React 组件重新渲染的主要情况:

  1. 状态(State)变化:

    • 当组件的状态通过 setState 方法更新时,React 会安排重新渲染。
    • 例如,如果你有一个计数器组件,每次点击按钮时调用 setState 增加计数,这将导致组件重新渲染。
  2. 属性(Props)变化:

    • 如果父组件的渲染导致传递给子组件的属性(props)发生变化,子组件将重新渲染。
    • 例如,如果父组件传递一个不同的用户名给子组件,子组件会因为接收到新的 props 而重新渲染。
  3. 强制渲染(Force Update):

    • 使用 forceUpdate 方法可以强制组件进行重新渲染,尽管这种做法并不推荐,因为它绕过了 React 的正常更新机制。
  4. 父组件重新渲染:

    • 如果一个父组件重新渲染,其所有的子组件也会默认重新渲染,除非子组件通过某种方式进行了优化(如使用 React.memoshouldComponentUpdate)。
  5. Context 变化:

    • 如果组件依赖于 React 的 Context,并且该 Context 的值发生变化,所有使用了这个 Context 的组件都将重新渲染。
  6. 使用 Hooks 导致的渲染:

    • 某些 React Hooks(如 useState, useReducer, useContext)在其依赖的数据变化时会触发组件的重新渲染。

为了优化性能,避免不必要的渲染,可以使用 React.memo 用于函数组件,或在类组件中使用 shouldComponentUpdate 生命周期方法。这些方法可以帮助你确定在特定的 props 或 state 更新时是否需要重新渲染组件。

6.3 如何发现无效渲染 #

6.3.1 React Developer Tools #

6.3.2 why-did-you-render #

@welldone-software/why-did-you-render 是一个用于React应用的开发者工具,其主要目的是帮助开发者识别和避免不必要的组件重新渲染,从而优化React应用的性能。这个库通过跟踪组件的渲染并在检测到可能的性能问题时提供警告和详细信息,使开发者能够更容易地理解和优化他们的组件。

主要特性

  1. 检测不必要的渲染:

    • 自动检测和记录那些没有必要的重新渲染的组件,例如,当组件的props和state没有发生变化时却重新渲染。
  2. 详细的日志信息:

    • 提供详细的日志信息,包括组件的名称、导致重新渲染的props或state的具体变化等,帮助开发者快速定位问题。
  3. 灵活的配置选项:

    • 允许开发者对特定组件或整个应用进行监控。可以配置跟踪所有组件、只跟踪特定组件或排除特定组件。
  4. 支持函数组件和类组件:

    • 适用于React的各种组件类型,包括类组件和函数组件。
  5. 与React开发者工具集成:

    • 可以与React DevTools一起使用,提供更加丰富的调试体验。

使用场景

注意事项

import React from "react";
import ReactDOM from "react-dom/client";
//const whyDidYouRender = require("@welldone-software/why-did-you-render");
function whyDidYouRender(React) {
  const originalCreateElement = React.createElement;
  React.createElement = (type, props, ...children) => {
    if (type.prototype instanceof React.Component) {
      type.prototype.componentDidUpdate = function (prevProps, prevState) {
        const newProps = this.props;
        const newState = this.state;
        const propsChanged = !deepCompare(newProps, prevProps);
        const stateChanged = !deepCompare(newState, prevState);
        if (!(propsChanged || stateChanged)) {
          console.log(
            `[why-did-you-render] ${type.name} re-rendered without any changes in props or state.`
          );
        }
      };
      return originalCreateElement(type, props, ...children);
    }
    return originalCreateElement(type, props, ...children);
  };
}
function deepCompare(obj1, obj2) {
  if (obj1 === obj2) {
    return true;
  }
  if (
    typeof obj1 !== "object" ||
    typeof obj2 !== "object" ||
    obj1 == null ||
    obj2 == null
  ) {
    return false;
  }
  const keys1 = Object.keys(obj1);
  const keys2 = Object.keys(obj2);
  if (keys1.length !== keys2.length) {
    return false;
  }
  for (const key of keys1) {
    if (!keys2.includes(key) || !deepCompare(obj1[key], obj2[key])) {
      return false;
    }
  }
  return true;
}
whyDidYouRender(React, {
  trackAllPureComponents: true,
});
class BigPureComponent extends React.PureComponent {
  render() {
    console.log("BigPureComponent render", this.props.styles);
    return <div style={this.props.styles}>BigPureComponent:</div>;
  }
}
class ParentComponent extends React.Component {
  state = { count: 0 };
  incrementCount = () => {
    this.setState((prevState) => ({
      count: prevState.count + 1,
    }));
  };
  render() {
    return (
      <div>
        <button onClick={this.incrementCount}>{this.state.count}</button>
        <BigPureComponent styles={{ width: Date.now() + `100%` }} />
      </div>
    );
  }
}
ReactDOM.createRoot(document.getElementById("root")).render(
  <ParentComponent />
);

6.4 如何避免无效渲染 #

6.4.1 PureComponent #

PureComponent 是 React 中的一个特殊类型的组件,提供了一种性能优化的方法。这里是它的关键特点:

1. 浅比较 Props 和 State

2. 浅比较的工作原理

3. 使用场景

4. 注意事项

总的来说,PureComponent 是一个用于优化 React 应用性能的有力工具,但需要谨慎使用,以确保它的使用场景和数据结构适合进行浅比较。

import React from "react";
import ReactDOM from "react-dom/client";
class ChildCounter extends React.PureComponent {
  render() {
    console.log("ChildCounter render");
    return (
      <button onClick={this.props.add}>{this.props.count.number}</button>
    );
  }
}
class App extends React.Component {
  state = {
    count: { number: 0 },
    text: ""
  };
  add = () => {
    this.setState({ count: { number: this.state.count.number + 1 } });
  }
  setText = (event) => {
    this.setState({ text: event.target.value });
  }
  render() {
    return (
      <div>
        <input value={this.state.text} onChange={this.setText} />
        <ChildCounter count={this.state.count} add={this.add} />
      </div>
    );
  }
}
ReactDOM.createRoot(document.getElementById("root")).render(<App />);

6.4.2 shouldComponentUpdate #

import React from "react";
import ReactDOM from "react-dom/client";
+function deepCompare(obj1, obj2) {
+  if (obj1 === obj2) {
+    return true;
+  }
+  if (
+    typeof obj1 !== "object" ||
+    obj1 === null ||
+    typeof obj2 !== "object" ||
+    obj2 === null
+  ) {
+    return false;
+  }
+  const keys1 = Object.keys(obj1);
+  const keys2 = Object.keys(obj2);
+  if (keys1.length !== keys2.length) {
+    return false;
+  }
+  for (let key of keys1) {
+    if (typeof obj1[key] === "object" && typeof obj2[key] === "object") {
+      if (!deepCompare(obj1[key], obj2[key])) {
+        return false;
+      }
+    } else if (obj1[key] !== obj2[key]) {
+      return false;
+    }
+  }
+  return true;
+}
+class ChildCounter extends React.Component {
+ shouldComponentUpdate(nextProps) {
+   console.log(this.props.count, nextProps.count,deepCompare(this.props, nextProps));
+   return !deepCompare(this.props, nextProps);
+ }
  render() {
    console.log("ChildCounter render");
    return (
      <button onClick={this.props.add}>{this.props.count.number}</button>
    );
  }
}
class App extends React.Component {
  state = {
    count: { number: 0 },
    text: ""
  };
  add = () => {
+   this.setState({ count: {number:this.state.count.number+0} });
  }
  setText = (event) => {
    this.setState({ text: event.target.value });
  }
  render() {
    return (
      <div>
        <input value={this.state.text} onChange={this.setText} />
        <ChildCounter count={this.state.count} add={this.add} />
      </div>
    );
  }
}
ReactDOM.createRoot(document.getElementById("root")).render(<App />);

6.4.3 immer #

import React from "react";
import ReactDOM from "react-dom/client";
+//import {produce} from "immer";
+function produce(recipe) {
+  return function (base) {
+    const draft = JSON.parse(JSON.stringify(base));
+    recipe(draft);
+    return draft;
+  };
+}
class ChildCounter extends React.PureComponent {
  render() {
    console.log("ChildCounter render");
    return <button onClick={this.props.add}>{this.props.count.number}</button>;
  }
}
class App extends React.Component {
  state = {
    count: { number: 0 },
    text: "",
  };
  add = () => {
+   this.setState(
+     produce((draft) => {
+       draft.count.number += 1;
+     })
+   );
  };
  setText = (event) => {
    this.setState({ text: event.target.value });
  };
  render() {
    return (
      <div>
        <input value={this.state.text} onChange={this.setText} />
        <ChildCounter count={this.state.count} add={this.add} />
      </div>
    );
  }
}
ReactDOM.createRoot(document.getElementById("root")).render(<App />);

6.4.4 reselect #

import React from "react";
import ReactDOM from "react-dom/client";
import {produce} from "immer";
+//import { createSelector } from "reselect";
+function createSelector(...funcs) {
+  let lastArgs = [];
+  let lastResult;
+  let calledOnce = false;
+  const resultFunc = funcs.pop();
+  return function(...args) {
+    const inputsChanged = lastArgs.length !== args.length ||
+                          args.some((arg, index) => arg !== lastArgs[index]);
+    if (calledOnce && !inputsChanged) {
+      return lastResult;
+    }
+    lastArgs = args;
+    lastResult = resultFunc(...args.map((arg, index) => funcs[index](arg)));
+    calledOnce = true;
+    return lastResult;
+  };
+}
class ChildCounter extends React.PureComponent {
  render() {
    console.log("ChildCounter render");
    return <button onClick={this.props.add}>{this.props.doubleCount.number}</button>;
  }
}
class App extends React.Component {
  state = {
    count: { number: 0 },
    text: "",
  };
  add = () => {
    this.setState(
      produce((draft) => {
        draft.count.number += 1;
      })
    );
  };
  setText = (event) => {
    this.setState({ text: event.target.value });
  };
+ doubleCountSelector = createSelector(
+   count => count.number,
+   number => ({number:number * 2})
+ );
  render() {
    //const doubleCount = {number:this.state.count.number * 2};
+   const doubleCount = this.doubleCountSelector(this.state.count);
    return (
      <div>
        <input value={this.state.text} onChange={this.setText} />
        <ChildCounter doubleCount={doubleCount} add={this.add} />
      </div>
    );
  }
}
ReactDOM.createRoot(document.getElementById("root")).render(<App />);

7.关于ReactHook要需要注意哪些内容? #

7.1 为什么会出现React Hooks? #

React Hooks的引入主要是为了解决以下问题:

  1. 逻辑复用难度:类组件中的逻辑难以复用。虽然高阶组件(HOCs)和渲染道具(Render Props)提供了一定程度的复用,但它们往往导致代码结构复杂,产生所谓的"回调地狱",难以理解和维护。

  2. 组件复杂性:在类组件中,复杂的业务逻辑通常散落在不同的生命周期方法中,这使得理解和维护变得困难。一个生命周期方法可能包含多个不相关的逻辑,这缺乏专注性且增加了混乱。

  3. this关键字的混淆:类组件中的this关键字经常导致混淆,特别是在事件处理和异步操作中。此外,类组件的方法需要正确绑定this,这增加了代码的复杂性。

  4. 编译优化限制:类组件的某些特性限制了现代JavaScript编译技术(如Tree Shaking)的应用,这影响了最终打包的效率和大小。

Hooks通过提供更简洁的API和更好的函数式编程模式来解决这些问题,使得组件的编写、理解和维护变得更加容易。

7.2 为什么hooks 需要遵循两条主要规则? #

const hookStates = [];
let hookIndex = 0;
export function useState(initVal) {
  const currentHookIndex = hookIndex;
  if (!hookStates[currentHookIndex]) {
    hookStates[currentHookIndex] = [initVal, (newVal)=> {
      hookStates[currentHookIndex][0] = newVal;
      render();
    }];
  }
  return  hookStates[hookIndex++];
}
function FunctionComponent() {
  const [firstName] = useState("zhang");
  const [lastName, setLastName] = useState("san");
  return setLastName;
}
function render() {
  hookIndex = 0;
  return FunctionComponent();
}
console.log(hookStates.map(item=>item[0]));
const setLastName = render();
console.log(hookStates.map(item=>item[0]));
setLastName("si");
console.log(hookStates.map(item=>item[0]));
const hookStates = [];
let hookIndex = 0;
export function useState(initVal) {
  const currentHookIndex = hookIndex;
  if (!hookStates[currentHookIndex]) {
    hookStates[currentHookIndex] = [
      initVal,
      (newVal) => {
        hookStates[currentHookIndex][0] = newVal;
        render();
      },
    ];
  }
  return hookStates[hookIndex++];
}
let times = 3;
function FunctionComponent() {
  for (let i = 0; i < times; i++) {
    const [firstName] = useState("zhang");
  }
  times = 5;
  const [lastName, setLastName] = useState("san");
  return setLastName;
}
function render() {
  hookIndex = 0;
  return FunctionComponent();
}
console.log(hookStates.map((item) => item[0]));
const setLastName = render();
console.log(hookStates.map((item) => item[0]));
setLastName("si");
console.log(hookStates.map((item) => item[0]));

7.3 useEffect和useLayoutEffect的相同点和不同点有哪些? #

useEffectuseLayoutEffect 在 React Hooks 中都用于处理副作用,但它们有以下相同点和不同点:

相同点

  1. 功能:两者都用于在组件渲染后执行副作用操作。
  2. API:它们具有相同的 API 结构,即可以接收一个函数(副作用函数)和一个依赖数组。
  3. 清理机制:两者都允许返回一个清理函数,用于组件卸载时执行清理操作。

不同点

  1. 执行时机

    • useEffect 在整个页面渲染和绘制完成后异步执行,不会阻塞浏览器的绘制过程。
    • useLayoutEffect 与 DOM 更新同步执行,会在浏览器绘制之前执行。因此,如果在 useLayoutEffect 中执行大量操作,可能会导致页面渲染的性能问题。
  2. 使用场景

    • useEffect 适用于大多数副作用场景,如数据获取、订阅、以及在渲染后需要执行的操作。
    • useLayoutEffect 通常用于需要同步读取或更新 DOM 的场景,或者在 DOM 更新后立即需要执行的操作,例如测量布局。

简而言之,尽管两者在功能上相似,但 useLayoutEffect 的使用应更加谨慎,以避免因同步执行而引起的性能问题。大多数情况下,使用 useEffect 就足够了。

7.4 如何在ReactHooks中获取上一轮的值? #

import React, { useState, useEffect, useRef } from "react";
import ReactDOM from "react-dom/client";
function App() {
  const [value, setValue] = useState("初始值");
  const prevValueRef = useRef();
  useEffect(() => {
    prevValueRef.current = value;
  });
  const prevValue = prevValueRef.current;
  return (
    <div>
      <p>当前值: {value}</p>
      <p>上一轮的值: {prevValue}</p>
      <button onClick={() => setValue("新值")}>更新值</button>
    </div>
  );
}
ReactDOM.createRoot(document.getElementById("root")).render(<App />);
import React, { useState, useEffect, useRef } from "react";
import ReactDOM from "react-dom/client";
function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  }, [value]);
  return ref.current;
}
function App() {
  const [value, setValue] = useState('初始值');
  const prevValue = usePrevious(value);
  return <div>
      <p>当前值: {value}</p>
      <p>上一轮的值: {prevValue}</p>
      <button onClick={() => setValue('新值')}>更新值</button>
    </div>;
}
ReactDOM.createRoot(document.getElementById("root")).render(<App />);

7.5 React Hooks作者说过"忘记生命周期,以effects的方式开始思考",你是如何理解这句话的? #

7.5.1 类组件生命周期 #

import React, {Component, useState} from "react";
import ReactDOM from "react-dom/client";
class ChildComponent extends Component {
  intervalId = null;
  componentDidMount() {
    this.startInterval();
  }
  componentDidUpdate(prevProps) {
    if (prevProps.value !== this.props.value) {
      this.clearInterval();
      this.startInterval();
    }
  }
  componentWillUnmount() {
    this.clearInterval();
  }
  startInterval() {
    this.intervalId = setInterval(() => {
      console.log(this.props.value);
    }, 1000);
  }
  clearInterval() {
    if (this.intervalId) {
      clearInterval(this.intervalId);
    }
  }
  render() {
    return <div>
        <h2>子组件</h2>
        <p>接收的值:{this.props.value}</p>
      </div>;
  }
}
function ParentComponent() {
  const [value, setValue] = useState(0);
  return <div>
      <h1>父组件</h1>
      <button onClick={() => setValue(prev => prev + 1)}>增加值</button>
      <ChildComponent value={value} />
    </div>;
}
ReactDOM.createRoot(document.getElementById("root")).render(<ParentComponent />);

7.5.2 effects #

import React, { useRef, useState ,useEffect} from "react";
import ReactDOM from "react-dom/client";
function ChildComponent({ value }) {
  const intervalRef = useRef(null);
  useEffect(() => {
    intervalRef.current = setInterval(() => {
      console.log(value);
    }, 1000);
    return () => {
      clearInterval(intervalRef.current);
    };
  }, [value]);
  return (
    <div>
      <h2>子组件</h2>
      <p>接收的值:{value}</p>
    </div>
  );
}
function ParentComponent() {
  const [value, setValue] = useState(0);
  return (
    <div>
      <h1>父组件</h1>
      <button onClick={() => setValue((prev) => prev + 1)}>增加值</button>
      <ChildComponent value={value} />
    </div>
  );
}
ReactDOM.createRoot(document.getElementById("root")).render(
  <ParentComponent />
);

抽取自定义Hook

import React, { useRef, useState ,useEffect} from "react";
import ReactDOM from "react-dom/client";
function useIntervalLogger(value) {
  const intervalRef = useRef(null);
  useEffect(() => {
    intervalRef.current = setInterval(() => {
      console.log(value);
    }, 1000);
    return () => {
      clearInterval(intervalRef.current);
    };
  }, [value]);
}
function ChildComponent({ value }) {
  useIntervalLogger(value); 
  return (
    <div>
      <h2>子组件</h2>
      <p>接收的值:{value}</p>
    </div>
  );
}
function ParentComponent() {
  const [value, setValue] = useState(0);
  return (
    <div>
      <h1>父组件</h1>
      <button onClick={() => setValue((prev) => prev + 1)}>增加值</button>
      <ChildComponent value={value} />
    </div>
  );
}
ReactDOM.createRoot(document.getElementById("root")).render(
  <ParentComponent />
);

7.6 React.memo和React.useMemo有什么区别? #

React.useMemoReact.memo 虽然都用于优化性能,但它们服务于不同的场景和目的,所以不能直接比较谁更好。它们的有效性取决于你正在处理的具体问题和使用场景。

React.useMemo

React.memo

React.memo

import React, { useState } from "react";
import ReactDOM from "react-dom/client";
function ChildComponent({ firstProp, secondProp }) {
  console.log("子组件渲染");
  return (
    <div>
      <h2>子组件</h2>
      <p>第一个属性:{firstProp}</p>
      <p>第二个属性:{secondProp}</p>
    </div>
  );
}
const ChildComponentMemo = React.memo(
  ChildComponent,
  (prevProps, nextProps) => {
    return prevProps.firstProp === nextProps.firstProp;
  }
);
function ParentComponent() {
  const [firstProp, setFirstProp] = useState(0);
  const [secondProp, setSecondProp] = useState("初始值");
  return (
    <div>
      <h1>父组件</h1>
      <button onClick={() => setFirstProp((prev) => prev + 1)}>
        改变第一个属性
      </button>
      <button onClick={() => setSecondProp((prev) => prev + " 更新")}>
        改变第二个属性
      </button>
      <ChildComponentMemo firstProp={firstProp} secondProp={secondProp} />
    </div>
  );
}
ReactDOM.createRoot(document.getElementById("root")).render(
  <ParentComponent />
);

React.useMemo

import React, { useState } from "react";
import ReactDOM from "react-dom/client";
function ChildComponent({ firstProp, secondProp }) {
  console.log("子组件渲染");
  return (
    <div>
      <h2>子组件</h2>
      <p>第一个属性:{firstProp}</p>
      <p>第二个属性:{secondProp}</p>
    </div>
  );
}
function ParentComponent() {
  const [firstProp, setFirstProp] = useState(0);
  const [secondProp, setSecondProp] = useState("初始值");
  const ChildComponentMemo = React.useMemo(()=>(
    <ChildComponent firstProp={firstProp} secondProp={secondProp} />
  ),[firstProp]);
  return (
    <div>
      <h1>父组件</h1>
      <button onClick={() => setFirstProp((prev) => prev + 1)}>
        改变第一个属性
      </button>
      <button onClick={() => setSecondProp((prev) => prev + " 更新")}>
        改变第二个属性
      </button>
      {ChildComponentMemo}
    </div>
  );
}
ReactDOM.createRoot(document.getElementById("root")).render(
  <ParentComponent />
);

7.7 封装自定义Hooks的适合什么样的设计模式? #

在使用React Hooks时。可以使用外观模式通过简化复杂系统的接口来提高易用性。 在React中,这可以通过自定义Hooks实现,它们封装和管理相关逻辑,简化组件的使用。

import React, { useState } from "react";
import ReactDOM from "react-dom/client";
const useUsersManagement = () => {
  const [users, setUsers] = useState([]);
  const addUser = (user) => {
    setUsers([...users, user]);
  };
  const deleteUser = (userId) => {
    setUsers(users.filter((user) => user.id !== userId));
  };
  return { users, addUser, deleteUser };
};
const UsersTable = ({ users, onDelete }) => {
  return (
    <table>
      <thead>
        <tr>
          <th>姓名</th>
          <th>操作</th>
        </tr>
      </thead>
      <tbody>
        {users.map((user) => (
          <tr key={user.id}>
            <td>{user.name}</td>
            <td>
              <button onClick={() => onDelete(user.id)}>Delete</button>
            </td>
          </tr>
        ))}
      </tbody>
    </table>
  );
};
const Users = () => {
  const { users, addUser, deleteUser } = useUsersManagement();
  return (
    <>
      <button onClick={() => addUser({ name: `User_${Date.now()}`, id: Date.now() })}>
        增加用户
      </button>
      <UsersTable users={users} onDelete={deleteUser} />
    </>
  );
};
ReactDOM.createRoot(document.getElementById("root")).render(<Users />);

8. 说说你了解的React生态体系? #

8.1 创建新项目 #

下面是对提到的几个脚手架工具的简要说明和它们的比较,包括它们的适用场景:

1. Create-React-App (CRA)

2. Dva

3. Umi

4. Create-React-Library

5. Storybook

总结

8.2 路由 #

react-router-dom 是 React 的一个库,它提供了在 React 应用程序中进行路由管理的功能。这个库专门为浏览器环境设计,提供了一系列组件和钩子(hooks),以支持在 Web 应用程序中的路由导航和渲染。以下是 react-router-dom 的一些关键特性和组件:

主要特性

  1. 声明式路由:通过声明式的组件(如 <Route><Link>),可以更容易地管理和构建路由。
  2. 动态路由匹配:根据 URL 动态渲染不同的组件。
  3. 嵌套路由:支持嵌套路由配置,使得布局和子视图的管理更为直观。
  4. 导航控制:提供了编程式导航的能力,如通过 history 对象进行页面跳转。

核心组件

  1. BrowserRouter:使用 HTML5 历史 API(pushState, replaceStatepopstate 事件)来保持 UI 和 URL 的同步。
  2. Route:定义 URL 和 UI 之间的映射关系。当 URL 匹配时,路由会渲染指定的组件。
  3. Link:用于创建导航链接,允许用户在应用中导航。点击 <Link> 组件将不重新加载页面,而是触发历史记录的更改。
  4. Switch(在 react-router-dom v5 中):用于渲染第一个与当前位置匹配的 <Route><Redirect>。在 v6 中,此组件被 Routes 替代。
  5. useHistory, useLocation, useParams, useRouteMatch:这些是 React Hooks,用于在函数组件中获取路由的状态和执行导航。

使用场景

版本注意

总的来说,react-router-dom 是 React 应用中进行路由管理的强大工具,它通过声明式路由和组件化的方式,使得构建复杂的路由系统变得更简单、更直观。

8.3 样式 #

React 应用中有多种样式解决方案,每种方案都有其独特的特点和最适合的使用场景。下面是一些常见的样式解决方案及其比较:

1. CSS Stylesheets

2. Inline Styles

3. CSS Modules

4. Styled Components

5. Emotion

6. Tailwind CSS

总结

8.4 基础组件库 #

React 生态系统中有许多组件库,每个库都有其独特的特点和适用场景。以下是一些流行的 React 组件库及其比较:

1. Material-UI

2. Ant Design

3. Bootstrap React

4. React Bootstrap

5. Semantic UI React

6. Chakra UI

7. Blueprint UI

**总结

选择组件库时,应考虑项目的需求、设计偏好、以及开发团队的熟悉度。

8.5 功能组件库 #

  1. react-query
    • 特点:用于处理和缓存异步数据请求,提高应用数据获取的效率和一致性。
    • 适用场景:适用于需要频繁进行数据请求和状态管理的应用。
  2. video-react
    • 特点:专为 React 定制的视频播放器,提供丰富的视频控制功能和自定义选项。
    • 适用场景:适用于需要集成视频播放功能的项目。
  3. react-toastify
    • 特点:用于创建和管理应用中的通知消息,提供灵活的配置和定制选项。
    • 适用场景:适用于需要展示通知、提示或消息的应用。
  4. react-window & react-virtualized
    • 特点:用于高效渲染大量列表和表格数据。react-window 更轻量,而 react-virtualized 提供更多功能。
    • 适用场景:适用于需要处理大量数据渲染的场景,react-window 适用于简单列表,react-virtualized 适用于更复杂的需求。
  5. react-table
    • 特点:用于构建和管理表格数据,支持排序、筛选、分页等功能。
    • 适用场景:适用于需要构建复杂数据表格的应用。
  6. react-dnd & react-draggable
    • 特点react-dnd 用于实现复杂的拖放界面,react-draggable 专注于基本的拖动功能。
    • 适用场景react-dnd 适用于需要复杂拖放逻辑的应用,而 react-draggable 适用于简单拖动功能的场景。
  7. react-helmet
    • 特点:用于管理文档的头部信息(如标题、描述和元数据)。
    • 适用场景:适用于需要控制页面头部信息和改善 SEO 的应用。
  8. react-pdf-viewer
    • 特点:为 React 应用设计的 PDF 查看器,提供基本的 PDF 浏览和交互功能。
    • 适用场景:适用于需要在应用中集成 PDF 查看功能的项目。
  9. react-motion
    • 特点:提供平滑的动画效果,使界面变得生动和响应式。
    • 适用场景:适用于需要实现复杂动画效果的应用。
  10. react-select
    • 特点:提供了丰富的下拉选择框功能,包括多选、异步加载等。
    • 适用场景:适用于需要定制化或功能丰富的下拉选择框的表单。

8.6 状态管理库 #

React 状态管理库提供了不同的方式来管理和维护应用的状态。每个库都有其独特的特点和最适合的使用场景。下面是一些主流的 React 状态管理库及其比较:

1. Redux

2. @reduxjs/toolkit

3. MobX

4. Jotai

5. Valtio

6. Zustand

7. Recoil

总结

8.7 打包和构建工具 #

构建 React 应用时,有几种常用的构建工具,每种工具都有自己的特点和最适合的使用场景。下面是一些主流的构建工具,包括 Webpack、Rollup 和 esbuild 的对比:

1. Webpack

2. Rollup

3. esbuild

总结

选择合适的构建工具取决于项目的特定需求,如构建速度、包的大小、以及对复杂资源处理的需求。随着项目的发展和技术的迭代,这些工具的选择和使用方式也可能随之变化。

8.8 代码规范检查工具 #

1. ESLint

2. Prettier

3. Stylelint

4. CommitLint

5. EditorConfig

总结

8.9 测试工具 #

在 React 项目中,测试是保证应用质量的关键环节。以下是一些主要的测试工具,它们各自的特点、适应场景以及它们之间的关系:

1. Jest

2. Enzyme

3. React Testing Library

4. React Hooks Testing Library

关系

8.10 发布上线 #

发布 React 项目涉及将开发的应用部署到服务器上,使其可供用户访问。这个过程通常包括打包、优化、上传文件到服务器等步骤。以下是一些常用的发布方式和工具,以及它们的特点和适应场景:

1. 手动部署

2. Netlify

3. Vercel

4. Docker 容器化

5. CI/CD 流程(如 GitHub Actions、GitLab CI/CD)

总结

9.参考 #

1. BailOut(逃生) #

在 React 中,"bailout" 是指在某些情况下,React 可以跳过不必要的组件更新来提高性能。这是一种避免不必要的渲染和相关工作的机制,有助于提高应用程序的效率。下面是几种常见的组件更新时的 bailout 情况:

1. 使用 React.memoReact.PureComponent

2. shouldComponentUpdate 方法

3. 返回相同的 stateprops

4. 使用 React.memo 的自定义比较函数

5. 使用 Hooks 的稳定性

重要注意事项

import React from "react";
import ReactDOM from "react-dom/client";
class ChildComponent extends React.Component {
  state = {count: 0,};
  render() {
    console.log("ChildComponent render");
    return (
      <button onClick={() => this.setState({ count: this.state.count + 1 })}>
        Count: {this.state.count}
      </button>
    );
  }
}
class ParentComponent extends React.PureComponent {
  render() {
    console.log("ParentComponent render");
    return <ChildComponent/>;
  }
}
class App extends React.Component {
  render() {
    console.log("App render");
    return <ParentComponent />;
  }
}
ReactDOM.createRoot(document.getElementById("root")).render(<App />);

2.immer #

immer 是一个在 JavaScript 和 TypeScript 中用于管理不可变状态的流行库。它主要用于应用程序中状态管理的场景,特别是在 React 和 Redux 等库中。下面是 immer 的一些关键特点和使用方式:

核心概念

  1. 不可变性(Immutability): immer 旨在帮助维护不可变的状态。在 JavaScript 中,不可变性是一种确保数据结构不被改变的实践。这对于避免副作用、编写可预测的代码以及优化性能(特别是在 React 中)非常重要。

  2. 草稿(Draft)状态: 当你使用 immer 时,你实际上是在操作一个草稿状态。这意味着你可以像修改普通 JavaScript 对象一样修改这个草稿状态,而不用担心直接更改原始状态。

  3. 生产(Produce): immer 提供了一个名为 produce 的函数。这个函数接受当前状态(可以是任何类型的数据)和一个修改这个状态的函数。在这个函数内部,你可以“自由”修改传递的状态,而 immer 会在内部处理这些修改,最终生成一个新的不可变状态。

使用方式

优势

注意事项

3.reselect #

reselect 是一个用于 Redux 的选择器库,主要用于创建可记忆(memoized)的选择器。在 Redux 和类似的状态管理库中,选择器是用于从状态树中派生数据的函数。reselect 的主要目的是提高性能和可组合性,特别是在处理复杂的状态和计算密集型派生数据时。

关键特性

  1. 可记忆化(Memoization): reselect 的核心特性是它可以记忆选择器的计算结果。当你用相同的参数多次调用一个选择器时,如果这些参数自上次调用以来没有改变,reselect 会返回上一次的计算结果,而不是重新计算。

  2. 组合选择器: reselect 允许你将多个选择器组合成一个新的选择器。这是通过将其他选择器作为输入传递给 createSelector 函数来实现的。

使用场景

基本用法

下面是一个 reselect 的基本使用例子:

import { createSelector } from 'reselect';

// 假设你有一个 Redux 状态,它包含一个用户列表
const getUsers = state => state.users;

// 使用 createSelector 创建一个可记忆的选择器
const getActiveUsers = createSelector(
  [getUsers],
  users => users.filter(user => user.isActive)
);

// 现在,getActiveUsers 只有在 users 发生改变时才会重新计算

注意事项

综上所述,reselect 是一个在处理复杂 Redux 应用中非常有用的库,它通过可记忆化选择器来提高性能,并使状态派生逻辑更加清晰和维护。