React进阶之高阶组件HOC、react hooks、自定义hooks

React高级

拓展:

ios、安卓、h5、小程序、app在移动端开发上有啥区别?

应用场景不同:

  1. 原生的native app开发,native需要客户端的发版才能做更新迭代
  2. 客户端嵌套页面webview 和 混合应用hybrid app开发 --- 要做快的发版,更新迭代的话
  3. mini program - 小程序开发

高阶组件 HOC

hoc:higher order component

本身不是复杂的内容,也不是React官方提供,他是一种设计模式组成的高阶用法,接收一个组件作为参数并返回一个新的组件,React是用组件拼装页面的

简单来说,hoc是组件作为参数,返回值也是组件

HOC 主要用于代码复用,类似于Vue中的mixin

javascript 复制代码
function HOC(WrappedComponent){
   return props=><WrappedComponent {...props} />
}

这里的props传递可以看作是 vue中的,this.$slots.defaults传递一样

javascript 复制代码
//类的使用
function HOC(WrappedComponent){
   return class extends React.Component{
       constructor(props){
         super(props)
	   }
 
  	   render(){
  	      const newProps={
  	      	 name:"zhangsan"
		  }
		  return <WrappedComponent {...props} {...newProps} />
  	   }
   }
}

class类的完整示例:

javascript 复制代码
import React from 'react';

// 创建 HOC,增强组件功能:withCounter 是我们定义的高阶组件,它接收一个组件(WrappedComponent)并返回一个新的组件。
function withCounter(WrappedComponent) {
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.state = { count: 0 };
    }

    increment = () => {
      this.setState(prevState => ({ count: prevState.count + 1 }));
    };

    render() {
      return (
        <WrappedComponent
          {...this.props}
          count={this.state.count}        // 将新的 state(count)传递给原组件
          increment={this.increment}      // 将新的方法(increment)传递给原组件
        />
      );
    }
  };
}

// 创建一个基础组件,接收 count 和 increment 作为 props
function Counter({ count, increment }) {
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}

// 使用 HOC 包装 Counter 组件
const EnhancedCounter = withCounter(Counter);

// 使用增强后的组件
export default function App() {
  return <EnhancedCounter />;
}

分类:

  1. 属性代理
  2. 反向继承

属性代理

返回的是原本的组件

  1. 代理props
javascript 复制代码
function HOC(WrappedComponent){
   const newProps = {
      name:'zhangsan' 
   }
   return props=><WrappedComponent {...props} {...newProps} />
}
  1. 模拟state
javascript 复制代码
function HOC(WrappedComponent){
   return class extends React.Component {
       constructor(props){
           super(props)
           this.state = {
               name:"zhangsan"
           }
           this.onChange = this.onChange.bind(this)
       }
       onChange=(e)=>{
           this.setState({
               name:e.target.value
           })
       }
 
  	   render(){
  	      const newProps={
            name: {
                value: this.state.name,
                onChange:this.onChange
            }
		  }
		  return <WrappedComponent {...this.props} {...newProps} />
  	   }
   }
}

function Demo(props) {
    const { name } = props
    const { value, onChange } = name;//props
    // 定义新的state方法
}

// 在外部的组件中定义的state state:name,methods:onChange
// 传递给内部的 WrappedComponent,通过props的方式去传递给他,能够获取value和onChange事件,然后去消费它
  1. 条件渲染,基于返回组件做的自定义处理
javascript 复制代码
function HOC(WrappedComponent) {
    // custom
    if (show = false) {
        return <div>暂无数据</div>
    }
    return props=> <WrappedComponent {...props} />
}

反向继承

使用类组件的方式

返回的是新的组件

javascript 复制代码
function HOC(WrappedComponent) {
    return class extends WrappedComponent{
        render() {
            return super.render();
        }
    }
}

这里,返回的新组件是原先的组件一比一复制过来的。

使用:

javascript 复制代码
function HOC(WrappedComponent) {
    // componentDidMount 新加功能
    const didMount = WrappedComponent.prototype.componentDidMount;//原本组件的mount方法
    
    return class extends WrappedComponent{
        // constructor() { this.state=WrappedComponent.prototype.state}
        componentDidMount() {
            if (didMount) {
                //需要将这样的生命周期关联的事件绑定到自定义的类中
                didMount.bind(this)  //新的组件里面,将原本组件生命周期方法执行了一遍
            }
            // handle custom 自定义逻辑
            this.setState({
                number:2
            })
        }
        render() {
            return super.render();
        }
    }
}

举例使用:

计算组件渲染时间:

javascript 复制代码
function withTiming(WrappedComponent) {
    return class extends WrappedComponent{
        constructor(props) {
            super(props);
            start = 0;
            end = 0;
        }
        componentWillMount() {  //继承链上的生命周期方法依次执行,由子到父的顺序执行,也就是说,先执行 withTiming 中的 componentWillMount,然后执行 WrappedComponent 中的 componentWillMount
            if (super.componentWillMount) {
                super.componentWillMount();
            }
            start+=Date.now()
        }
        componentDidMount() { //同理,先执行 withTiming 中的 componentDidMount,然后执行 WrappedComponent 中的 componentDidMount
            if (super.componentDidMount) {
                super.componentDidMount();
            }
            end += Date.now()
            console.error(`${WrappedComponent.name}渲染时间为${end - start}ms`) //也就是说,这里统计的渲染时间实际是继承链上所有组件生命周期渲染时间的总和
        }
        render() {
            return super.render();
        }
    }
}
export default withTiming(Home)

属性代理和反向继承的区别

属性代理本质上是在外部操作这个组件,由外到内;但是,反向继承是由内到外,返回一个新的组件。所以说,由内部操作的话,是可以操作组件的所有逻辑的,内部逻辑相对比较危险。

实例

实例一

javascript 复制代码
// views/PageA.js
import React from 'react';
import fetchMovieListByType from '../lib/utils';
import MovieList from '../components/MovieList';

class PageA extends React.Component {
  state = {
    movieList: [],
  }
  /* ... */
  async componentDidMount() {
    const movieList = await fetchMovieListByType('comedy');
    this.setState({
      movieList,
    });
  }
  
  render() {
    return <MovieList data={this.state.movieList} emptyTips="暂无喜剧"/>
  }
}
export default PageA;


// views/PageB.js
import React from 'react';
import fetchMovieListByType from '../lib/utils';
import MovieList from '../components/MovieList';

class PageB extends React.Component {
  state = {
    movieList: [],
  }
  // ...
  async componentDidMount() {
    const movieList = await fetchMovieListByType('action');
    this.setState({
      movieList,
    });
  }
  render() {
    return <MovieList data={this.state.movieList} emptyTips="暂无动作片"/>
  }
}
export default PageB;


// 冗余代码过多
// HOC
import React from 'react';

const withFetchingHOC = (WrappedComponent, fetchingMethod, defaultProps) => {
  return class extends React.Component {
    async componentDidMount() {
      const data = await fetchingMethod();
      this.setState({
        data,
      });
    }
    
    render() {
      return (
        <WrappedComponent 
          data={this.state.data} 
          {...defaultProps} 
          {...this.props} 
        />
      );
    }
  }
}

// 使用:
// views/PageA.js
import React from 'react';
import withFetchingHOC from '../hoc/withFetchingHOC';
import fetchMovieListByType from '../lib/utils';
import MovieList from '../components/MovieList';

const defaultProps = {emptyTips: '暂无喜剧'}

export default withFetchingHOC(MovieList, fetchMovieListByType('comedy'), defaultProps);

// views/PageB.js
import React from 'react';
import withFetchingHOC from '../hoc/withFetchingHOC';
import fetchMovieListByType from '../lib/utils';
import MovieList from '../components/MovieList';

const defaultProps = {emptyTips: '暂无动作片'}

export default withFetchingHOC(MovieList, fetchMovieListByType('action'), defaultProps);;

// views/PageOthers.js
import React from 'react';
import withFetchingHOC from '../hoc/withFetchingHOC';
import fetchMovieListByType from '../lib/utils';
import MovieList from '../components/MovieList';
const defaultProps = {...}
export default withFetchingHOC(MovieList, fetchMovieListByType('some-other-type'), defaultProps);

一般在工作中使用到的都是属性代理 ,很少用到反向继承,在统计性能指标的场景中可以用反向继承。

属性代理效果一般做公用逻辑的抽离

属性代理实例:

不用HOC:

views/PageA.js:

javascript 复制代码
// views/PageA.js
import React from 'react';
import fetchMovieListByType from '../lib/utils';
import MovieList from '../components/MovieList';

// PageA:喜剧片的渲染
class PageA extends React.Component {
  state = {
    movieList: [],
  }
  /* ... */
  async componentDidMount() {
    const movieList = await fetchMovieListByType('comedy');
    this.setState({
      movieList,
    });
  }
  
  render() {
    return <MovieList data={this.state.movieList} emptyTips="暂无喜剧"/>
  }
}
export default PageA;

views/PageB.js

javascript 复制代码
// views/PageB.js
import React from 'react';
import fetchMovieListByType from '../lib/utils';
import MovieList from '../components/MovieList';

// pageB:动作片的渲染
class PageB extends React.Component {
  state = {
    movieList: [],
  }
  // ...
  async componentDidMount() {
    const movieList = await fetchMovieListByType('action');
    this.setState({
      movieList,
    });
  }
  render() {
    return <MovieList data={this.state.movieList} emptyTips="暂无动作片"/>
  }
}
export default PageB;

=>冗余代码过多,使用HOC抽离:

javascript 复制代码
// 冗余代码过多
// HOC
import React from 'react';
                        //  包裹的组件        请求的方法     上述的emptyTips
const withFetchingHOC = (WrappedComponent, fetchingMethod, defaultProps) => {
  return class extends React.Component {
    async componentDidMount() {
      const data = await fetchingMethod();
      this.setState({
        data,
      });
    }
    
    render() {
      return (
        <WrappedComponent 
          data={this.state.data} 
          {...defaultProps} 
          {...this.props} 
        />
      );
    }
  }
}

views/PageA.js:

javascript 复制代码
// 使用:
// views/PageA.js
import React from 'react';
import withFetchingHOC from '../hoc/withFetchingHOC';
import fetchMovieListByType from '../lib/utils';
import MovieList from '../components/MovieList';

const defaultProps = {emptyTips: '暂无喜剧'}

export default withFetchingHOC(MovieList, fetchMovieListByType('comedy'), defaultProps);

views/PageB.js:

javascript 复制代码
// views/PageB.js
import React from 'react';
import withFetchingHOC from '../hoc/withFetchingHOC';
import fetchMovieListByType from '../lib/utils';
import MovieList from '../components/MovieList';

const defaultProps = {emptyTips: '暂无动作片'}

export default withFetchingHOC(MovieList, fetchMovieListByType('action'), defaultProps);

其他页面:

javascript 复制代码
// views/PageOthers.js
import React from 'react';
import withFetchingHOC from '../hoc/withFetchingHOC';
import fetchMovieListByType from '../lib/utils';
import MovieList from '../components/MovieList';
const defaultProps = {...}
export default withFetchingHOC(MovieList, fetchMovieListByType('some-other-type'), defaultProps);

实例二

在vue中有自定义指令,像v-permission,通过自定义指令实现按钮的逻辑展示,但是在这里也能通过HOC来实现。

例如:

javascript 复制代码
import React from 'react';
import { whiteListAuth } from '../lib/utils'; // 鉴权方法

function AuthWrapper(WrappedComponent) {
  return class AuthWrappedComponent extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        permissionDenied: -1,
      };
    }
    
    async componentDidMount() {
      try {
        await whiteListAuth(); // 请求鉴权接口
        this.setState({
          permissionDenied: 0,
        });
      } catch (err) {
        this.setState({
          permissionDenied: 1,
        });
      }
    }
    
    render() {
      if (this.state.permissionDenied === -1) {
        return null; // 鉴权接口请求未完成
      }
      if (this.state.permissionDenied) {
        return <div>功能即将上线,敬请期待~</div>;
      }
      return <WrappedComponent {...this.props} />;
    }
  }
}

export default AuthWrapper;

Hooks

上节内容回顾:

  1. 在props和state两个组件开发过程中,如果要定义组件内部中的变量,使用props还是state?
    state
  2. 在props过程中,props元素随着外部组件变化而变化,如何观察到props的一个变化呢?
    生命周期等,只要能够获取到新的props就可以
  3. props是外部传参的,外部传递参数可能是不同的,想要一个组件要改变它的props,用什么样的方式能够做到呢?
    dispatch,最直接的方式,将props转为state
  4. 将React组件封装的更好,根据这里的思路:官方中文链接,将共有的能力抽离出来,放到对应的state,props上即可。

类组件中不能使用hooks

Hooks API

Hooks官方

结合官网中看下面讲到的几个API就行

use:试验期,在React 19中才会用到,返回的是一个promise的结果,这个方法是用来读取promise的返回值的。能够获取到返回的结果,能够读取到context,不建议使用。

useState:

const [state, setState] = useState(initialState)

initialState:初始默认值,state:默认值的返回,setState就是修改state的方式

javascript 复制代码
import { useState } from 'react';

function MyComponent() {
  const [age, setAge] = useState(28);
  const [name, setName] = useState('Taylor');
  const [todos, setTodos] = useState(() => createTodos());  //初始值能够获取函数的返回结果,但是必须是同步的
 
  const [userInfo,setUserInfo]=useState({
  	  name:"Taylor",
  	  age:42
  })
  // userInfo.age=52  这种方法是不生效的
  //这样去修改对象种某个属性的值
  setUserInfo({
     ...userInfo,
     age:52
  })
  ...
}

useEffect:

添加随着状态的改变来触发它的动作,辅佐用,返回的结果用来清除它的辅佐用

javascript 复制代码
import { useEffect } from 'react';
import { createConnection } from './chat.js';

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

  useEffect(() => { //useEffect参数:1.接收一个函数 2.一个数组
    const connection = createConnection(serverUrl, roomId); //创建长连接
    connection.connect();
    return () => {  //返回这里用来销毁连接 
      connection.disconnect();
    };
  }, [serverUrl, roomId]); //当且仅当serverUrl或roomId发生变化时,会重新执行useEffect中的函数
  // ...
}

使用场景:

可以在请求数据中使用

javascript 复制代码
import { useState, useEffect } from 'react';
import { fetchBio } from './api.js';

export default function Page() {
  const [person, setPerson] = useState('Alice');
  const [bio, setBio] = useState(null);
  useEffect(() => {
    let ignore = false;
    setBio(null);
    fetchBio(person).then(result => {
      if (!ignore) {
        setBio(result);
      }
    });
    return () => {
      ignore = true;
    }
  }, [person]);

  return (
    <>
      <select value={person} onChange={e => {
        setPerson(e.target.value);
      }}>
        <option value="Alice">Alice</option>
        <option value="Bob">Bob</option>
        <option value="Taylor">Taylor</option>
      </select>
      <hr />
      <p><i>{bio ?? 'Loading...'}</i></p>
    </>
  );
}

代码中的useEffect:

javascript 复制代码
  useEffect(() => {
    let ignore = false;
    setBio(null);
    fetchBio(person).then(result => {
      if (!ignore) {
        setBio(result);
      }
    });
    return () => {
      ignore = true;
    }
  }, [person]);

如果第二个返回的数组中person没有值了,那么就是没有依赖项发生变化,就在初始化中执行一次,等同于 componentDidMount

useLayoutEffect:

是useEffect的一个版本,这两个传递的参数都一样,只不过触发setup函数的时间不一样

useEffect: 组件mount -> dom渲染 -> useEffect(()=>{},[deps])

useLayoutEffect:组件mount -> useLayoutEffect(()=>{},[deps]) -> dom渲染

javascript 复制代码
const DemoUseLayoutEffect = () => {
  const target = useRef()
  useLayoutEffect(() => {
      /*我们需要在dom绘制之前,移动dom到制定位置*/
      const { x ,y } = getPositon() /* 获取要移动的 x,y坐标 */
      animate(target.current,{ x,y })
  }, []);
  return (
    <div >
      <span ref={ target } className="animate"></span>
    </div>
  )
}

这里意味着先执行的useLayoutEffect的回调函数(执行动作animate),再去渲染的return返回中的div代码

需要在dom上渲染最终的结果就使用:useLayoutEffect

需要在dom上展示执行callback回调函数的动作,就使用useEffect

useRef:

与Vue3中的Ref类似,通过 ref.current 获取值,但是创建的 ref 并没有关联到元素上,ref 绑定的是真实的js对象

与 state区别:

const [a,setA]=useState(1) => 当执行setA时候,会执行 重新渲染 re-render

但是如果只想改变值,并不希望组件重新渲染,可以借助ref

const a=useRef(1)

a.current=2 => 不会触发视图的响应,也不会触发视图的更新

使用场景:

javascript 复制代码
const inputRef = useRef(null);
javascript 复制代码
.......
return <input ref={inputRef} />;
javascript 复制代码
function handleClick() {
  inputRef.current.focus();
}

useContext:

javascript 复制代码
/* 用useContext方式 */
const DemoContext = ()=> {
  const value = useContext(Context);
  / my name is aaa /
  return <div> my name is { value.name }</div>
}

/ 用Context.Consumer 方式 /
const DemoContext1 = ()=>{
  return <Context.Consumer>
    {/  my name is aaa  */}
    { (value)=> <div> my name is { value.name }</div> }
  </Context.Consumer>
}

export default ()=>{
  return <div>
    <Context.Provider value={{ name:'aaa' }} >
      <DemoContext />
      <DemoContext1 />
    </Context.Provider>
  </div>
}

创建的Context,是能通过Provider和Consumer两个地方去消费的

第一种是用useContext将我们的值关联起来,

第二种,通过Provider将value值关联起来了,就能在provider下层的所有节点上,这里是DemoContext,DemoContext1,

这两个节点,一方面通过 useContext获取到其中关联的value,另一种是通过Context.Consumer之间去获取。但是在平常开发过程中,一般是通过useContext和Provider去获取上层传递下来的状态

useReducer:

javascript 复制代码
const DemoUseReducer = ()=>{
  /* number为更新后的state值,  dispatchNumbner 为当前的派发函数 */
  const [ number , dispatchNumbner ] = useReducer((state, action) => {
    const { payload , name  } = action
    / return的值为新的state /
    switch(name) {
     case 'a':
         return state + 1
     case 'b':
         return state - 1 
     case 'c':
       return payload       
    }
    return state
   }, 0)
   return <div>
      当前值:{ number }
      { / 派发更新 / }
      <button onClick={()=>dispatchNumbner({ name: 'a' })} >增加</button>
      <button onClick={()=>dispatchNumbner({ name: 'b' })} >减少</button>
      <button onClick={()=>dispatchNumbner({ name: 'c' , payload:666 })} >赋值</button>
      { / 把dispatch 和 state 传递给子组件  */ }
      <MyChildren  dispatch={ dispatchNumbner } State={{ number }} />
   </div>
}

上述这两个useContext 和 useReducer 和Redux的逻辑是一样的

useMemo

是平常做响应式依赖的一个动作,类似于vue中的computed

javascript 复制代码
// selectList 不更新时,不会重新渲染,减少不必要的循环渲染
const a=useMemo(() => (
  <div>{
    selectList.map((i, v) => (
      <span
        className={style.listSpan}
        key={v} >
        {i.patentName} 
      </span>
    ))}
  </div>
), [selectList])

当selectList元素变化时候,重新执行回调函数,只不过,useMemo返回的结果,就是这个函数执行的结果,所以,在selectList不变的话,哪怕这个组件变化,这个a的结果也不会发生变化

适用于性能优化

javascript 复制代码
// listshow, cacheSelectList 不更新时,不会重新渲染子组件
useMemo(() => (
  <Modal
    width={'70%'}
    visible={listshow}
    footer={[
      <Button key="back" >取消</Button>,
      <Button
          key="submit"
          type="primary"
       >
          确定
      </Button>
    ]}
  > 
    { /* 减少了PatentTable组件的渲染 */ }
    <PatentTable
      getList={getList}
      selectList={selectList}
      cacheSelectList={cacheSelectList}
      setCacheSelectList={setCacheSelectList}
    />
  </Modal>
 ), [listshow, cacheSelectList])
javascript 复制代码
 // 减少组件更新导致函数重新声明
 const DemoUseMemo = () => {
  / 用useMemo 包裹之后的log函数可以避免了每次组件更新再重新声明 ,可以限制上下文的执行 /
  const newLog = useMemo(() => {
    const log = () => {
      console.log(123)
    }
    return log
  }, [])
  return <div onClick={()=> newLog() } ></div>
}
javascript 复制代码
// 如果没有加相关的更新条件,是获取不到更新之后的state的值的
const DemoUseMemo = () => {
  const [ number ,setNumber ] = useState(0)
  const newLog = useMemo(() => {
    const log = () => {
      / 点击span之后 打印出来的number 不是实时更新的number值 /
      console.log(number)
    }
    return log
    / [] 没有 number */  
  }, [])
  return <div>
    <div onClick={() => newLog()} >打印</div>
    <span onClick={ () => setNumber( number + 1 )  } >增加</span>
  </div>
}

useCallback

useMemo返回的是函数执行的结果,useCallback返回的是这个函数

挂载时初始化,constructor 对应着 useLayoutEffect 初始化

componentDidMount 对应着 useLayoutEffect第二个参数不传

更新 对应着 useEffect 传递参数

shouldComponentUpdate 对应着 useMemo和useCallback

componentDidUpdate 对应着 useEffect 回调函数执行

componentWillUnmount 对应着 useEffect 返回函数的执行

也就是这样:

javascript 复制代码
useLayoutEffect(() => {
  // componentDidMount
  return () => {
    // componentWillUnmount
  };
}, [])

useEffect(() => {
  // componentDidUpdate
}, [deps])

// shouldComponentUpdate
useMemo(); useCallback()

自定义Hooks

类似:

const [a,b,c,d]=useXXX(params1,parmas2)

这种形式的就叫做自定义hook

自定义hook中使用reactive中已有的hook

本质上是:用已有的hooks做相同某种功能的聚合,聚合后能够在其他某个地方取消费

javascript 复制代码
const useTitle = (title) => { 
  useEffect(() => { 
    document.title = title; 
  }, []);
}

const App = () => {
  useTitle('Hello World');
  return <div>Hello World</div>;
}

保证代码是相对独立的

javascript 复制代码
import { useState, useEffect } from 'react'

const useScroll = (scrollRef) => {
  const [pos, setPos] = useState([0,0])

  useEffect(() => {
    function handleScroll(e){
      setPos([scrollRef.current.scrollLeft, scrollRef.current.scrollTop])
    }
    scrollRef.current.addEventListener('scroll', handleScroll)
    return () => {
      scrollRef.current.removeEventListener('scroll', handleScroll)
    }
  }, [])
  
  return pos
}

export default useScroll

// 用法
import React, { useRef } from 'react'
import { useScroll } from 'hooks'

const Home = (props) => {
  const scrollRef = useRef(null)
  const [x, y] = useScroll(scrollRef)

  return <div>
      <div ref={scrollRef}>
        <div className="innerBox"></div>
      </div>
      <div>{ x }, { y }</div>
    </div>
}
相关推荐
hunter2062061 小时前
ubuntu向一个pc主机通过web发送数据,pc端通过工具直接查看收到的数据
linux·前端·ubuntu
qzhqbb1 小时前
web服务器 网站部署的架构
服务器·前端·架构
刻刻帝的海角1 小时前
CSS 颜色
前端·css
浪浪山小白兔2 小时前
HTML5 新表单属性详解
前端·html·html5
lee5763 小时前
npm run dev 时直接打开Chrome浏览器
前端·chrome·npm
2401_897579653 小时前
AI赋能Flutter开发:ScriptEcho助你高效构建跨端应用
前端·人工智能·flutter
光头程序员3 小时前
grid 布局react组件可以循数据自定义渲染某个数据 ,或插入某些数据在某个索引下
javascript·react.js·ecmascript
limit for me3 小时前
react上增加错误边界 当存在错误时 不会显示白屏
前端·react.js·前端框架
浏览器爱好者3 小时前
如何构建一个简单的React应用?
前端·react.js·前端框架
qq_392794484 小时前
前端缓存策略:强缓存与协商缓存深度剖析
前端·缓存