React-过渡技术(memo,context),Hook(useState,useEffect)

本文介绍了React中的过渡技术,包括React.memo用于优化函数组件性能,以及React.context API用于跨层级数据传递。此外,还详细讨论了React Hook,如useState和useEffect,以及它们在管理状态和处理副作用方面的应用。文章强调在适当情况下使用Hook以避免性能问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

过渡技术

1、memo

1.React核心开发团队一直都努力地让React变得更快。在React中可以用来优化组件性能的方法大概有以下几种:

  • 组件懒加载: React.lazy(…) 和 < Suspense />
  • Pure Component
  • shouldComponentUpdate(…){…}生命周期函数

React16.6加入的另外一个专门用来优化函数组件(Functional Component)性能的方法: React.memo

1.1无用的渲染

组件是构成React视图的一个基本单元。有些组件会有自己本地的状态(state), 当它们的值由于用户的操作而发生改变时,组件就会重新渲染。在一个React应用中,一个组件可能会被频繁地进行渲染。这些渲染虽然有一小部分是必须的,不过大多数都是无用的,它们的存在会大大降低我们应用的性能

import React from 'react';
​
class TestC extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            count: 0
      }
  }
    
    componentWillUpdate(nextProps, nextState) {
        console.log('componentWillUpdate')
  }
    
    componentDidUpdate(prevProps, prevState) {
        console.log('componentDidUpdate')
        
  }
    
    render() {
        return (
            <div >
          {this.state.count}
            <button onClick={()=>this.setState({count: 1})}>Click Me</button>
            </div>
      );
  }
}
export default TestC;
//TestC组件有一个本地状态count,它的初始值是0(state = {count: 0})。当我们点击Click Me按钮时,count的值被设置为1。这时候屏幕的数字将会由0变成1。当我们再次点击该按钮时,count的值还是1, 这时候TestC组件不应该被重新渲染,可是现实是这样的吗?
//为了测试count重复设置相同的值组件会不会被重新渲染, 我为TestC组件添加了两个生命周期函数: componentWillUpdate和componentDidUpdate。componentWillUpdate方法在组件将要被重新渲染时被调用,而componentDidUpdate方法会在组件成功重渲染后被调用。
//在浏览器中运行我们的代码,然后多次点击Click Me按钮,你可以看到打印了多次更新操作
//所以即使count被设置相同的值,TestC组件还是会被重新渲染,这些就是所谓的无用渲染。
​ 

1.2 shouldComponentUpdate / Pure Component

语法:

//为了避免React组件的无用渲染,我们可以实现自己的shouldComponentUpdate生命周期函数。
//当React想要渲染一个组件的时候,它将会调用这个组件的shouldComponentUpdate函数, 这个函数会告诉它是不是真的要渲染这个组件。
shouldComponentUpdate(nextProps, nextState) {
    return true        
}
shouldComponentUpdate(nextProps, nextState) {
    return false
}
​
//其中各个参数的含义是:
//nextProps: 组件将会接收的下一个参数props
//nextState: 组件的下一个状态state
//shouldComponentUpdate函数一直返回true,这就告诉React,无论何种情况都要重新渲染该组件。
//这个方法的返回值是false,React永远都不会重新渲染我们的组件。 

案例:

用shouldComponentUpdate重写TestC组件:

import React from 'react';
​
class TestC extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            count: 0
      }
  }
    
    componentWillUpdate(nextProps, nextState) {
        console.log('componentWillUpdate')
  }
    
    componentDidUpdate(prevProps, prevState) {
        console.log('componentDidUpdate')
  }
    
    shouldComponentUpdate(nextProps, nextState) {
        if (this.state.count === nextState.count) {
            return false
      }
        return true
  }
    
    render() {
        return ( 
            <div> 
          { this.state.count } 
            <button onClick = {
              () => this.setState({ count: 1 }) }> Click Me </button> 
            </div>
      );
  }
}
​
export default TestC; 

1.3 Pure Component

React在v15.5的时候引入了Pure Component组件。React在进行组件更新时,如果发现这个组件是一个PureComponent,它会将组件现在的state和props和其下一个state和props进行浅比较,如果它们的值没有变化,就不会进行更新。要想让你的组件成为Pure Component,只需要extends React.PureComponent

import React from 'react';
​
class TestC extends React.PureComponent {
    constructor(props) {
        super(props);
        this.state = {
            count: 0
      }
  }
    
    componentWillUpdate(nextProps, nextState) {
        console.log('componentWillUpdate')
  }
    
    componentDidUpdate(prevProps, prevState) {
        console.log('componentDidUpdate')
  }
    
    /*shouldComponentUpdate(nextProps, nextState) {
      if (this.state.count === nextState.count) {
          return false
      }
      return true
  }*/
    
    render() {
        return ( 
            <div> 
          { this.state.count } 
            <button onClick = {() => this.setState({ count: 1 }) }> Click Me </button> 
            </div >
      );
  }
}
​
export default TestC;
​ 

1.4 React.memo

React.memo()是React v16.6引进来的新属性。它的作用和React.PureComponent类似,是用来控制函数组件的重新渲染的。React.memo(…)其实就是函数组件的React.PureComponent

语法:

const Funcomponent = ()=> {
    return (
        <div>
           I am a Funtional component
        </div>
  )
}
const MemodFuncComponent = React.memo(FunComponent)
//React.memo会返回一个纯化的组件MemoFuncComponent,这个组件将会在JSX标记中渲染出来。当组件的参数props和状态state发生改变时,React将会检查前一个状态和参数是否和下一个状态和参数是否相同,如果相同,组件将不会被渲染,如果不同,组件将会被重新渲染。
​ 

案例:

​
let TestC = (props) => {
    console.log('Rendering TestC :', props)
    return ( 
        <div>
      { props.count }
        </div>
  )
}
TestC = React.memo(TestC)
//就这么简单 只想问一句牛皮吗? 
2、context

1.Context被翻译为上下文,在编程领域,这是一个经常会接触到的概念,React中也有:

In Some Cases, you want to pass data through the component tree without having to pass the props down manuallys at every level. you can do this directly in React with the powerful “context” API

React文档官网并未对Context给出“是什么”的定义,更多是描述使用的Context的场景,以及如何使用Context,在React的官方文档中,Context被归类为高级部分,属于React的高级API,但官方并不建议在稳定版的App中使用Context。

不过,这并非意味着我们不需要关注Context。事实上,很多优秀的React组件都通过Context来完成自己的功能,比如react-redux的< Provider />,就是通过Context提供一个全局态的store,拖拽组件react-dnd,通过Context在组件中分发DOM的Drag和Drop事件,路由组件react-router通过Context管理路由状态等等。在React组件开发中,如果用好Context,可以让你的组件变得强大,而且灵活。

简单说就是,当你不想在组件树中通过逐层传递props或者state的方式来传递数据时,可以使用Context来实现跨层级的组件数据传递,来,我们学它吧.

使用props或者state传递数据,数据自顶下流:

使用Context,可以跨越组件进行数据传递:

2.如何使用Context

如果要Context发挥作用,需要用到两种组件,一个是Context生产者(Provider),通常是一个父节点,另外是一个Context的消费者,通常是一个或者多个子节点。所以Context的使用基于生产者消费者模式

对于父组件,也就是Context生产者,需要通过一个静态属性childContextTypes声明提供给子组件的Context对象的属性,并实现一个实例getChildContext方法,返回一个代表Context的纯对象 。

import React from 'react';
const ThemeContext = React.createContext({
  background: 'red',
  color: 'white'
});
export default ThemeContext;
//通过静态方法React.createContext()创建一个Context对象,这个Context对象包含两个组件,<Provider />和<Consumer /> 
import React from 'react';
import ThemeContext from './ThemeContext.js';
import Header from './Header.js';
class App extends React.Component {
  render () {
    return (
      <ThemeContext.Provider value={{background: 'green', color: 'white'}}>
        <Header />
      </ThemeContext.Provider>
  );}
} 
import React from 'react';
import Title from './Title.js';
class Header extends React.Component {
  render () {
    return (
      <Title>Hello React Context API</Title>
  );}
}
export default Header; 
​
import React from 'react';
import ThemeContext from './ThemeContext.js';
class Title extends React.Component {
  render () {
    return (
      <ThemeContext.Consumer>
      {context => (
          <h1 style={{background: context.background, color: context.color}}>
          {this.props.children}
          </h1>
      )}
      </ThemeContext.Consumer>
  );}
}
Title.contextType = ThemeContext
export default Title;
​
//<Consumer />的children必须是一个函数,通过函数的参数获取<Provider />提供的Context 

3.可以直接获取Context的地方

注意:接受值的后代组件名.contextType = ThemeContext

实际上,除了实例的context属性(this.context),React组件还有很多个地方可以直接访问父组件提供的Context

类组件:

  • constructor(props, context)

  • componentWillReceiveProps(nextProps, nextContext)

  • shouldComponentUpdate(nextProps, nextState, nextContext)

  • componetWillUpdate(nextProps, nextState, nextContext)

  • 所有能访问this的地方都可以==>this.context

无状态的函数组件:

const StatelessComponent = (props, context) => (

)

Hook

React Hook是React 16.8版本之后添加的新属性,用最简单的话来说,React Hook就是一些React提供的内置函数,这些函数可以让函数组件和类组件一样能够拥有组件状态(state)以及进行副作用(side effect)

但是不要什么业务都使用hook,请在合适的时候使用hook,否则会造成性能问题.(能不用的时候就不能,当遇到性能不好优化的时候,自然会想到使用它)

useState

1.useState和类组件的this.state一样,都是用来管理组件状态的。在React Hook没出来之前,函数组件也叫做Functional Stateless Component(FSC:功能无状态的组件),这是因为函数组件每次执行的时候都会生成新的函数作用域所以同一个组件的不同渲染(render)之间是不能够共用状态的,因此开发者一旦需要在组件中引入状态就需要将原来的函数组件改成类组件,这使得开发者的体验十分不好(面试:具体哪些不友好?)。useState就是用来解决这个问题的,它允许函数组件将自己的状态持久化到React运行时的某个地方,这样在组件每次重新渲染的时候都可以从这个地方拿到该状态,而且当该状态被更新的时候,组件也会重渲染。

//语法:
import {useState} from "react"
​
const [state, setState] = useState(initialState)//数组解构赋值
​
//useState接收一个initialState变量作为状态的初始值,返回值是一个数组。返回数组的第一个元素代表当前state的最新值,第二个元素是一个用来更新state的函数。state和setState这两个变量的命名是你自己取的
//state用于组件内部使用的数据
//setState函数用于修改state,当修改后会触发所有使用过state的地方重新取值(调用render)
//可以用多个useState
​ 

案例:

import React, { useState } from 'react'
import ReactDOM from 'react-dom'
​
const App = () => {
  const [counter, setCounter] = useState(0)
  const [text, setText] = useState('')
  const change1 = (event) => {
    setText(event.target.value)}
​
  return (
    <div>
      <div>{counter}</div>
      <button onClick={() => setCounter(counter + 1)} >+</button>
      <input onChange={change1} value={text}/>
    <div/>)
}
ReactDOM.render(<App />, document.getElementById('root'))
​ 

2.useState的initialState也可以是一个用来生成状态初始值的函数,这种做法主要是避免组件每次渲染的时候initialState需要被重复计算

const [state, setState] = useState(() => {
  const initialState = localstorge.getItem("isLogin")
  return initialState
}) 

3.setState是全量替代(面试)

函数组件的setState和类组件的this.setState函数的一个重要区别是:

类组件是将当前设置的state浅归并到旧state的操作。而hook的setState函数则是将新state直接替换旧的state。因此我们在编写函数组件的时候,就要合理划分state,避免将没有关系的状态放在一起管理.

案例:

//不好的设计:
const [state, setState] = useState({ left: 0, top: 0, width: 0, height: 0 })
​
//由于我们将互不关联的DOM位置信息{left: 0, top: 0}和大小信息{width: 0, height: 0}绑定在同一个state,所以我们在更新任意一个状态的时候也要维护一下另外一个状态:
const handleContainerResize = ({ width, height }) => {
  setState({...state, width, height})
}
​
const handleContainerMove = ({ left, top }) => {
  setState({...state, left, top})
}
​
​
​
​
//正确的设计:
const [position, setPosition] = useState({ left: 0, top: 0 })
const [size, setSize] = useState({ width: 0, height: 0})
​
const handleContainerResize = ({ width, height }) => {
  setSize({width, height})
}
​
const handleContainerMove = ({ left, top }) => {
  setPosition({left, top})
}
​
//ps:如果你非要将多个互不关联的状态放在一起的话,可以使用useReducer来管理状态,这样你的代码会更好维护。 

4.setState没有回调函数(面试)

无论是useState还是类组件的this.setState都是异步调用的,也就是说每次组件调用完之后都不能立即拿到最新的state值。

为了解决这个问题,类组件的this.setState允许通过一个回调函数来获取到最新的state值:

this.setState(newState, state => { console.log( state)})

函数组件的setState函数不存在这么一个可以拿到最新state的回调函数,不过我们可以使用useEffect来实现相同的效果.

useEffect

1.useEffect是用来使函数组件也可以进行副作用操作的。那么什么是副作用呢?

In computer science, an operation, function or expression is said to have a side effect if it modifies some state variable value(s) outside its local environment, that is to say has an observable effect besides returning a value (the main effect) to the invoker of the operation.

看不懂的话,直白的说就是:

函数的副作用就是函数除了返回值外对外界环境造成的其它影响。举个例子,假如我们每次执行一个函数,该函数都会操作全局的一个变量,那么对全局变量的操作就是这个函数的副作用。而在React的世界里,我们的副作用大体可以分为两类,一类是调用浏览器的API,例如使用addEventListener来添加事件监听函数等,另外一类是发起获取服务器数据的请求,例如当用户组件挂载的时候去异步获取用户的信息等。在Hook出来之前,如果我们需要在组件中进行副作用的话就需要将组件写成类组件,然后在组件的生命周期函数里面写副作用,这其实会引起很多代码设计上的问题(具体问题:面试题),Hook出来之后,开发者就可以在函数组件中使用useEffect来定义副作用了。虽然useEffect基本可以覆盖componentDidMount, componentDidUpdate,componentWillUnmount等生命周期函数组合起来使用的所有场景,但是useEffect和生命周期函数的设计理念还是存在本质上的区别的,如果一味用生命周期函数的思考方式去理解和使用useEffect的话,可能会引发一些奇怪的问题,大家有兴趣的话,可以看看React核心开发Dan写的这篇文章:A Complete Guide to useEffect(overreacted.io/a-complete-…) 里面阐述了使用useEffect的一个比较正确的思考方式。

2.语法:

import {useEffect} from "react"
useEffect(effect?=>clean, dependencies?)
          
//useEffect的第一个参数effect是要执行的副作用函数,它可以是任意的用户自定义函数,用户可以在这个函数里面   操作一些浏览器的API或者和外部环境进行交互,网络请求等,这个函数会在每次组件渲染完成之后被调用
//useEffect可以有一个返回值,返回一个函数,系统在组件重新渲染之前调用它 
//第二个参数dependencies来限制该副作用的执行条件 

案例1:useEffect基本用法

import React, { useState, useEffect } from 'react'
import ReactDOM from 'react-dom'
​
const UserDetail = ({ userId }) => {
  const [userDetail, setUserDetail] = useState({})
​
  useEffect(() => {
    fetch(`http://ip:port/users/${userId}`)
    .then(response => response.json())
    .then(user => setUserDetail(userDetail))})
​
  return (
    <div>
      <div>User Name: {userDetail.name}</div>
    </div>)
}
​
ReactDOM.render(<UserDetail />, document.getElementById('root'))
//用户详情信息的副作用会在UserDetail组件每次完成渲染后执行,所以当该组件第一次挂载的时候就会向服务器发起获取用户详情信息的请求然后更新userDetail的值,这里的第一次挂载我们可以想象成类组件的componentDidMount。
​
//死循环:但是组件会不断向服务端发起请求。出现这个死循环的原因是useEffect里面调用了setUserDetail,这个函数会更新userDetail的值,从而使组件重渲染,而重渲染后useEffect的effect继续被执行,进而组件再次重渲染
​
//为了避免重复的副作用执行,useEffect允许我们通过第二个参数dependencies来限制该副作用什么时候被执行:指明了dependencies的副作用,只有在dependencies数组里面的元素的值发生变化时才会被执行,因此如果要避免上面的代码进入死循环我们就要将userId指定为我们定义的副作用的dependencies 

案例2:dependencies参数

import React, { useState, useEffect } from 'react'
import ReactDOM from 'react-dom'
​
const UserDetail = ({ userId }) => {
  const [userDetail, setUserDetail] = useState({})
​
  useEffect(() => {
    fetch(`https://myapi/users/${userId}`)
    .then(response => response.json())
    .then(user => setUserDetail(userDetail))}, [userId])
​
  return (
    <div>
      <div>User Name: ${userDetail.name}</div>
    </div>)
}
​
ReactDOM.render(<UserDetail />, document.getElementById('root')) 

案例3:effect返回值函数

import React, { useEffect } from 'react'
import ReactDOM from 'react-dom'
​
const WindowScrollListener = () => {
  useEffect(() => {
    const handleWindowScroll = () => console.log('正在滚')
    window.addEventListener('scroll', handleWindowScroll)
    // 这个就是清除函数
    return () => {
      window.removeEventListener(handleWindowScroll)
  }}, [])
​
  return (
    <div>
       滚起来
    </div>)
}
ReactDOM.render(<WindowScrollListener />, document.getElementById('root'))
​
//上面的代码中在WindowScrollListener组件首次渲染完成后注册一个监听页面滚动事件的函数,并在组件下一次渲染前移除该监听函数。由于我们指定了一个空数组作为这个副作用的dependencies,所以这个副作用只会在组件首次渲染时被执行一次,而它的cleanup函数只会在组件unmount时才被执行,这就避免了频繁注册页面的监听函数从而影响页面的性能。 

总结:

useEffect(() => { }) //每次组件渲染都执行

useEffect(() => { return () => { } }, []) //组件第一次渲染执行,组件移除时触发clean函数

useEffect(() => { return () => { } }, [count])//count改变才会执行,组件重新渲染前触发clean函数

最后

整理了75个JS高频面试题,并给出了答案和解析,基本上可以保证你能应付面试官关于JS的提问。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值