React 知识点

React 基础知识点综合

React 和 Vue 的区别

相同点:

  1. 数据驱动视图:从直接操作DOM到数据驱动视图
  2. 组件化:将页面分成细碎的组件,组件之间的组合嵌套就形成最后的页面
  3. 虚拟DOM

不同点:

  1. 响应式原理

    Vue 提供的是反应式的数据,Vue 会遍历 data 数据对象,为每一个属性都设置 getter(访问属性时会调用该函数) 和 setter(修改属性时会调用该函数),这样就能清楚的知道哪些属性在变动。当属性值发生变动的时候,寻找所有使用到该属性的组件,重新渲染该组件。

    而 React 则是需要给系统一个明确的信号(setState)来告诉系统重新渲染界面,系统在接受到指令后会使用 Diff 算法对虚拟 DOM 进行比较,计算出需要更新的部分,然后进行部分渲染。

  2. 写法差异:React all in js,Vue推荐使用 template 单文件组件格式

  3. Diff算法不同

数据流

单向数据流:数据从父组件流向子组件,子组件的数据更新的请求必须发送到父组件,让父组件作出相应的更改,再将数据传递给子组件,即子组件不能自行修改父组件传递过来的数据。

双向数据绑定:就是 UI 行为改变 Modal 层的数据,Modal 层的数据能够即时反映到 UI 上,同时在 Modal 层修改的数据(即不通过 UI 行为触发)也能够反应到 UI 上。

JSX

JSX 语法实际上是 React.createElement(component,props,...children)的语法糖,所有的 JSX 语法都会被转换成这个方法调用,举个例子:

const element = <div className='foo'>Hello, React</div>
// 等价于
const elemnt = React.createElement( 'div', { className:'foo' }, 'Hello,React' );

在编译时 Babel 读取代码并解析,生成 AST,再将 AST 传入插件层进行转换,在转换时就可以将 JSX 的结构转换为 React.createElement 的函数。

Render

组件是 React 的核心,将组件挂载到页面的 DOM 节点中需要使用 react-dom 库中的ReactDOM.render()方法:

// index.js 
import React from "react"; 
import ReactDOM from "react-dom"; 
import App from "./App";

ReactDOM.render(<App />, document.getElementById("root"));

render 方法可以返回单个元素、数组和字符串(16.0+),这样就不需要在多个元素外再包裹一个 div 了;

数组:

class PostList extends Component { 
    render() { 
        return [ 
            <li key="A">First item</li>,
            <li key="B">Second item</li>,
            <li key="C">Third item</li> 
        ]; 
    } 
}

字符串:

class StringComponent extends Component { 
    render() {
        return "Just a strings"; 
    } 
}

生命周期

react 的生命周期有:

constructor

用于初始化 state

render

负责返回组件

componentDidMount

在组件被挂载之后触发,且只会触发一次,推荐在这里调用 Ajax 请求获取服务端数据

需要注意的是:当所有子元素都被加载完成之后,才会触发父元素的 componentDidMount 事件

shouldComponentUpdate

组件更新之前触发,常用与减少组件不必要的更新,举个例子:

shouldComponentUpdate(nextProps, nextState){
  if(nextState.Number === this.state.Number){
    return false  // 如果state中前后number的值不变则不更新组件
  }
}

componentDidUpdate

组件更新之后触发

componentWillUnmount

组件被卸载之前触发

完整生命周期(16.4)图示:

componentDidCatch

错误边界的作用:在 react 16 之后,任何未被错误边界包裹的错误都会导致整个React树被卸载。

如果一个 class 组件设置了static getDerivedStateFromError()componentDidCatch(error,errorInfo)中的一个或者两个,那么这个组件就会成为一个错误边界组件,当子组件错误发生时,作为父级的错误边界组件就能够获取到错误并作出对应的操作。

getDerivedStateFromError 用于在发生错误的时候修改 state 以显示错误信息或降级内容

componentDidCatch 用于在发生错误的时候对错误作出相应的操作

举个例子:

// 创建一个 ErrorBoundary 组件
class ErrorBoundary extends Component{
    constructor(props){
        super(props);
        this.state={
            hasError:false
        }
    }

      static getDerivedStateFromError(){
      return { hasError:true }
    }

    componentDidCatch(error,errorInfo){
            logErrorToMyService(error, errorInfo);
    }

    render() {
        return this.state.hasError ? (
            // 当出现错误的时候显示提示
            <h1>Oops, something went wrong!</h1>
        ) : (
            this.props.children
        );
    }
}

用错误边界将可能发生错误的组件包裹起来,这样就能捕获到子组件的错误信息了:

class App extends Component{
    render(){
        return (
          <ErrorBoundary>
            <Something></Something>
          </ErrorBoundary>
        );
    }
}

不过错误边界能够捕获到的错误是有限的,只能捕获渲染期间、生命周期内的和组件构造函数内的错误,但是事件处理器(例如 onClick)内的、定时器内的、错误边界组件自身的错误无法被捕获。这些无法被捕获的错误可以通过try/catch来捕获。

组件分类

类组件(Class Component):

传统的组件,继承于 React.Component

class App extends React.Component{
    ...
}

纯函数组件(PureComponent):

由 React 自行管理 shouldComponentUpdate 的 Component。

对于传统组件来说,一旦 state 或 props 发生了变化,组件默认会被重新渲染,当然你也可以通过 shouldComponentUpdate 来对 state 和 props 进行对比,手动控制组件的更新。

pureComponent 简化了这个过程,由 React 自行管理,对比没有设置 shouldComponentUpdate 的传统组件性能上有一定程度上的提升。

函数组件(Function component):

函数式组件,指只需要处理 props 不需要拥有 state 的组件,举个例子:

function App(){
    return <div>...</div>
}

Function component 的终极形式:React Hook

有状态组件和无状态组件

组件可以根据 state 的有无分为有状态组件和无状态组件,无状态组件一般使用函数组件,常用于展示数据和通知父元素,举个例子:

function PostItem(props){
    const { handleClick,postContent } = props;
      return <div onClick={ handleClick }>{ postContent.title }</div>
}

受控组件与非受控组件

受控组件:将表单元素的值交给 react 管理,通过 state 和 onChange 事件来修改 value 值;

非受控组件:通过操作表单元素的 DOM 来修改 value 值。

非受控组件因为值不由 react 管理,当我们需要获取该表单元素值的时候,就需要用到 ref,React.createRef()方法能够创建 Refs 并通过 ref 属性联系到 react 组件或 DOM 元素。

举个例子:

import React,{ Component, createRef } from 'react';

class Login extends Component{
    constructor(props){
        super(props)
        this.state = {
            account:''
        }
        this.account = createRef();
    }

    handleSubmit=()=>{
        console.log(this.account.value)
    }

    render(){
        // 将input元素赋给 this.account,之后便可在其他地方通过 this.account 调用该元素
        return <input type='text' ref={this.account}/>
    }
}

注意:

  • 如果要给非受控组件赋予初始值,需要使用defaultValue或者defaultChecked属性,不能设置value属性;
  • 只能为类组件定义 ref 属性,函数组件不支持 ref 属性(函数组件内部可以使用 ref 属性);

类组件的 this

在组件中调用方法时,this 默认不指向当前类,此时函数内获取到的 this 为 undefined,举个例子:

class PostList extends Component {
  handleClick() {
    console.log(this); // undefined
  }

  render() {
    return <div onClick={this.handleClick}>Click Me</div>;
  }
}

绑定 this 主要有以下三种方法:

  1. 在回调中绑定:

    <button onClick={this.deleteRow.bind(this)}>Delete Row</button>
    
  2. 在构造函数中绑定:

    constructor(props) {
        super(props);
        this.handleClick = this.handleClick.bind(this);
    }
    
    render(){
        return(
            <button onClick={this.handleClick}>Delete Row</button>
        )
    }
    
  3. 回调使用箭头函数:

    <button onClick={()=>{this.deleteRow()}}>Delete Row</button>
    
  4. 使用箭头函数进行属性初始化:

    class PostList extends Component {
      handleClick=()=>{
        console.log(this); 
      }
    
      render() {
        return <div onClick={this.handleClick}>Click Me</div>;
      }
    }
    

组件间的通信

父子之间

父组件通过 props 向子组件传递数据,子组件通过调用父组件传递过来的 props 里面的回调函数向父组件传递数据。

兄弟之间

通过一父多子(多个父子间通信结合)的方式实现,将共同的父组件作为中转来实现通信。

多层级组件之间

使用 Context 来处理不同层级之间的数据传递问题,避免复杂的 props 传递,常用于用户信息、主题或者首选语言。

Context 有两个主要的 API:createContext 和 Provider,createContext 用来创建 Context 对象,Provider 用来传递数据给组件,举个例子:

import React,{createContext} from 'react';
class App extends Component{
  const MyContext = createContext('light'); // 创建 Context 对象并设定默认值 light
    render(){
    return (
      <MyContext.Provider value={'dark'}>  /* Context 传递的值为 dark */
        <childComponent></childComponent>
      </MyContext.Provider>
    )
  }
}

Context 在 class 中可以使用.contextType来设置目标 Context,挂载后允许使用this.context的形式获取 Context 的值:

class Child extends Component{
    componentDidMount(){
      console.log(this.context)
  }
}

Child.contextType = MyContext;

对于函数组件,可以使用.Consumer方法订阅到 Context,举个例子:

<MyContext.Consumer>
  {value => /* 基于 context 值进行渲染*/}
</MyContext.Consumer>

在 Hook 中。只需要使用 useContext 方法即可订阅 Context:

const myContext = useContext(MyContext);

Context 的替代方式

利用插槽直接将元素传递下去,这样就可以在父元素直接赋值:

// Contacts 组件是被传递的元素
function App() {
  return <SplitPane left={<Contacts />} />;
}

function SplitPane(props) {
  return <div>{props.left}</div>; // 接收并渲染传递过来的元素
}

向 props.children 传值

子组件可能通过插槽(slot)的方式引入,此时父组件内子组件用props.children代替,父组件无法直接操作子组件,也无法直接传值,React.cloneElement()很好的解决了这个问题,他可以克隆出子组件,并传递参数:

// parent component
// old
{ props.children }

// new
{props.children &&
    React.cloneElement(props.children, {...props})}

上面的方法在只有一个子组件的时候是能够正常运行的,但是在拥有多个子组件时会发生错误,这时候就需要用到另外一个方法:React.Children.map(),他能够对子组件数组进行循环:

{props.children &&
    React.Children.map(props.children, child =>
        React.cloneElement(child, {...props})
)}

上面的代码实现了多个 React 组件的正常传值与渲染,但如果子组件不是 React 组件,例如字符串,如果依然使用React.cloneElement()就会发生错误,需要使用React.isValidElement()方法判断子组件是否为 React 组件,如果不是,则直接返回即可:

{props.children &&
    React.Children.map(props.children, child =>
        React.isValidElement(child)
        ? React.cloneElement(child, {...props})
        : props.children
)}

父组件获取子组件Ref

当我们需要获取某个子组件内部的 DOM 元素的时候,可以这样将 childRef 属性通过 props 传递给子组件,子组件再将 childRef 赋给 ref,这样父组件就能取到子组件的 DOM 节点了:

import React,{ Component, createRef } from 'react';

class Parent extends Component{
    constructor(props){
        super(props)
        this.childRef = createRef();
    }

    render(){
        return <Child exportRef={this.childRef}></Child>
    }
}

class Child extends Component{
    constructor(props){
        super(props);
    }

    render(){
        return <input ref={this.props.exportRef} />
    }
}

这里使用了 childRef 作为变量名是因为 ref 不存在与 props 内,不能通过 props 进行传递,但是换一个变量名就可以了,如果要使用变量 ref 进行传递,就需要使用到 React.forwardRef(props,ref)来实现,ref 作为第二个参数传入,那么上面的 Child 组件就应该如下改造:

const Child = React.forwardRef((props,ref)=>{
    render(){
        return <input ref={ref} />
    }
})

Ref

Ref 常用于获取获取 DOM 元素或 React 组件实例,当 ref 附加在 DOM 元素上时,可以通过ref.current获取到对该节点的引用,当 ref 附加在 Class 组件上时(函数组件不可以,因为其没有实例),ref.current获取到的是组件的挂载实例。

React 在组件挂载时给current属性传入 DOM 元素(或实例),并在组件卸载时传入 null 值,ref 会在 componentDidMount 或 componentDidUpdate 生命周期钩子触发前更新。

竞态问题

异步获取数据最常见的方法仍是使用 useEffect,但是useEffect会报以下错误:Effect callbacks are synchronous to prevent race conditions,问题的原因是请求结果返回的顺序不能保证一致。

举个例子,现在有一个组件当接收关键词不同时,会重新渲染组件。那么先请求关键词为 react 的文章,然后请求关键词为 vue 的文章,但关键词为 vue 的请求更先返回。请求更早但返回更晚的情况会错误地覆盖状态值 data(此时关键词是vue,但显示的是后返回react的结果),这就叫做竞态(race conditions),查看例子

解决方法:增加一个 didCancel 的变量,来判断当前操作是否已经被取消,查看例子

React-Router

使用 a 标签进行渲染相当于新开了一个页面,页面需要重新被渲染且有可能会有白屏闪烁,而 Link 根据 React-router 的 Diff 算法,能够实现对页面的部分刷新,避免了不必要的渲染,有更高的效率。

React 的路由是如何实现的

路由需要解决两个核心问题:

  1. 如何改变URL且不刷新页面
  2. 如何监听URL的变化

Hash 常用于锚点定位,所以本身就不会引起页面的刷新,History 是一个对象,保存了当前窗口访问过的所有网址,并提供一系列的方法来进行无刷新的前进和后退。

不论是 Hash 路由还是 History 路由,都是通过监听浏览器事件来监听URL变化的,Hash 路由监听的是hashchange事件,History 路由监听的是popstate事件。

React 在侦听到路由变化后,页面不进行刷新而是进行页面的部分替换来实现路由效果

React18 新特性

  1. React 18 默认执行批处理,原本在 promise、定时器和原生事件处理函数内的 setState 是同步的,无法被批处理,但是在 React 18 中都会被批量合并成一次处理。如果需要退出批量更新,可以使用flushSyncAPI

    <div
      onClick={() => {
        flushSync(() => {
          setCount1(count => count + 1);
        });
      }}
    />
    
  2. 增加了新的 API:useId、useDeferredValue、useTransition

  3. 并发模式,当你使用 startTransition 或者 useDeferredValue 时就会开启并发模式,并发模式通过 fiber 实现,能将线权的控制权交还给浏览器。

作者

BiteByte

发布于

2020-08-08

更新于

2024-01-11

许可协议