详解react hooks(含高阶组件)

8 篇文章 0 订阅
1 篇文章 0 订阅

一. 面试中出现的关于hooks的题目

1. 简单介绍下什么是hooks,hooks产生的背景?hooks的优点?

hooks是针对在使用react时存在以下问题而产生的:

  • 组件之间复用状态逻辑很难,在hooks之前,实现组件复用,一般采用高阶组件和 Render Props,它们本质是将复用逻辑提升到父组件中,很容易产生很多包装组件,带来嵌套地域。
  • 组件逻辑变得越来越复杂,尤其是生命周期函数中常常包含一些不相关的逻辑,完全不相关的代码却在同一个方法中组合在一起。如此很容易产生 bug,并且导致逻辑不一致。
  • 复杂的class组件,使用class组件,需要理解 JavaScript 中 this 的工作方式,不能忘记绑定事件处理器等操作,代码复杂且冗余。除此之外,class组件也会让一些react优化措施失效。

针对上面提到的问题,react团队研发了hooks,它主要有两方面作用:

  • 用于在函数组件中引入状态管理和生命周期方法
  • 取代高阶组件和render props来实现抽象和可重用性

优点也很明显:

  • 避免在被广泛使用的函数组件在后期迭代过程中,需要承担一些副作用,而必须重构成类组件,它帮助函数组件引入状态管理和生命周期方法。
  • Hooks 出现之后,我们将复用逻辑提取到组件顶层,而不是强行提升到父组件中。这样就能够避免 HOC 和 Render Props 带来的「嵌套地域」
  • 避免上面陈述的class组件带来的那些问题
2. 知道hoc和render props吗,它们有什么作用?有什么弊端?

Render Props 组件和高阶组件主要用来实现抽象和可重用性。
弊端就是高阶组件和 Render Props 本质上都是将复用逻辑提升到父组件中,很容易产生很多包装组件,带来的「嵌套地域」。由于所有抽象逻辑都被其他 React 组件所隐藏,应用变成了一棵没有可读性的组件树。而那些可见的组件也很难在浏览器的 DOM 中进行跟踪。

2.1 Render Props

什么是Render Props
render props模式是一种非常灵活复用性非常高的模式,它可以把特定行为或功能封装成一个组件,提供给其他组件使用让其他组件拥有这样的能力。他把组件可以动态渲染的地方暴露给外部,你不用再关注组件的内部实现,只要把数据通过函数传出去就好。
使用场景:

  • 通用业务逻辑的抽取
  • 当两个平级组件之间需要单向依赖的时候,比如两个同级组件A、B,A组件需要跟随B组件的内部状态来改变自己的内部状态,我们就说A依赖B;或者B依赖A

render props demo参考

2.2 Hoc

hoc的应用demo
hoc是 React 中用于重用组件逻辑的高级技术,它是一个函数,能够接受一个组件并返回一个新的组件。
实现高阶组件的两种方式:

  • 属性代理。高阶组件通过包裹的React组件来操作props
  • 反向继承。高阶组件继承于被包裹的React组件
2.2.1属性代理
a. 什么是属性代理

属性代理组件继承自React.Component,通过传递给被包装的组件props得名

// 属性代理,把高阶组件接收到的属性传递给传进来的组件
function HOC(WrappedComponent) {
  return class PP extends React.Component {
    render() {
      return <WrappedComponent {...this.props}/>
    }
  }
}
b. 属性代理的用途
  • 更改 props,可以对传递的包裹组件的WrappedComponent的props进行控制
  • 通过 refs 获取组件实例
/*
可以通过 ref 获取关键词 this(WrappedComponent 的实例)
当 WrappedComponent 被渲染后,ref 上的回调函数 proc 将会执行,此时就有了这个 WrappedComponent 的实例的引用
*/
function refsHOC(WrappedComponent) {
  return class RefsHOC extends React.Component {
    proc(wrappedComponentInstance) {
      wrappedComponentInstance.method()
    }
    render() {
      const props = Object.assign({}, this.props, {ref: this.proc.bind(this)})
      return <WrappedComponent {...props}/>
    }
  }
}
  • 把 WrappedComponent 与其它 elements 包装在一起
2.1.2 反向继承

反向继承是继承自传递过来的组件

function iiHOC(WrappedComponent) {
  return class Enhancer extends WrappedComponent {
    render() {
      return super.render()
    }
  }
}

反向继承允许高阶组件通过 this 关键词获取 WrappedComponent,意味着它可以获取到 state,props,组件生命周期(component lifecycle)钩子,以及渲染方法(render),所以我们主要用它来做渲染劫持,比如在渲染方法中读取或更改 React Elements tree,或者有条件的渲染等。

2.1.3 高阶组件相关的面试题

1. 这怎么在高阶组件里面访问组件实例
答案见上面

2. 你的项目中怎么使用的高阶组件
a. 项目中经常存在在配置系统中配置开关/全局常量,然后在页面需要请求配置来做控制,如果在每个需要调用全局设置的地方都去请求一下接口,就会有一种不优雅的感觉,这个时候我就想到利用高阶组件抽象一下。
b. 项目开发过程中,经常也会遇到需要对当前页面的一些事件的默认执行做阻止,我们也可以写一个高阶组件等。

3. hooks和hoc和render props有什么不同?

它们之间最大的不同在于,后两者仅仅是一种开发模式,而自定义的hooks是react提供的API模式,它既能更加自然的融入到react的渲染过程也更加符合react的函数编程理念。

4. 介绍下常用的hooks?
  • useState(),状态钩子。为函数组建提供内部状态
// 我们实现一个简易版的useState
let memoizedStates = [ ]  // 多个useState 时需要使用数组来存
let index = 0
function useState (initialState) {
   memoizedStates[index] = memoizedStates[index] || initialState
   let currentIndex = index;
   function setState (newState) {
      memoizedStates[currentIndex] = newState
      render()
   }
   return [memoizedStates[index++], setState]
}
  • useContext(),共享钩子。该钩子的作用是,在组件之间共享状态。 可以解决react逐层通过Porps传递数据,它接受React.createContext()的返回结果作为参数,使用useContext将不再需要Provider 和 Consumer。
  • useReducer(),Action 钩子。useReducer() 提供了状态管理,其基本原理是通过用户在页面中发起action, 从而通过reducer方法来改变state, 从而实现页面和状态的通信。使用很像redux
  • useEffect(),副作用钩子。它接收两个参数, 第一个是进行的异步操作, 第二个是数组,用来给出Effect的依赖项
  • useRef(),获取组件的实例;渲染周期之间共享数据的存储(state不能存储跨渲染周期的数据,因为state的保存会触发组件重渲染)
    useRef传入一个参数initValue,并创建一个对象{ current: initValue }给函数组件使用,在整个生命周期中该对象保持不变。
  • useMemo和useCallback:可缓存函数的引用或值,useMemo用在计算值的缓存,注意不用滥用。经常用在下面两种场景(要保持引用相等;对于组件内部用到的 object、array、函数等,如果用在了其他 Hook 的依赖数组中,或者作为 props 传递给了下游组件,应该使用 useMemo/useCallback)
  • useLayoutEffect:会在所有的 DOM 变更之后同步调用 effect,可以使用它来读取 DOM 布局并同步触发重渲染
5. 描述下hooks下怎么模拟生命周期函数,模拟的生命周期和class中的生命周期有什么区别吗?
// componentDidMount,必须加[],不然会默认每次渲染都执行
useEffect(()=>{
}, [])

// componentDidUpdate
useEffect(()=>{
document.title = `You clicked ${count} times`;
return()=>{
// 以及 componentWillUnmount 执行的内容       
}
}, [count])

//  shouldComponentUpdate, 只有 Parent 组件中的 count state 更新了,Child 才会重新渲染,否则不会。
function Parent() {
  	const [count,setCount] = useState(0);
  	const child = useMemo(()=> <Child count={count} />, [count]);
  	return <>{count}</>
}

function Child(props) {
    return <div>Count:{props.count}</div>
}

这里有一个点需要注意,就是默认的useEffect(不带[])中return的清理函数,它和componentWillUnmount有本质区别的,默认情况下return,在每次useEffect执行前都会执行,并不是只有组件卸载的时候执行。
useEffect在副作用结束之后,会延迟一段时间执行,并非同步执行,和compontDidMount有本质区别。遇到dom操作,最好使用useLayoutEffect。

6. hooks中的坑,以及为什么?
  • 不要在循环,条件或嵌套函数中调用Hook,必须始终在React函数的顶层使用Hook。这是因为React需要利用调用顺序来正确更新相应的状态,以及调用相应的钩子函数。一旦在循环或条件分支语句中调用Hook,就容易导致调用顺序的不一致性,从而产生难以预料到的后果。
    useState实现原理
  • 使用useState时候,使用push,pop,splice等直接更改数组对象的坑,demo中使用push直接更改数组无法获取到新值,应该采用析构方式,但是在class里面不会有这个问题。(这个的原因是push,pop,splice是直接修改原数组,react会认为state并没有发生变化,无法更新)
    这里的坑很多的,经常出现的就是每次修改数组的时候:
 const [firstData, setFirstData]:any = useState([])
 const handleFirstAdd = () => {
 		// let temp = firstData // 不要这么写,直接修改原数组相当于没有更新
        let temp = [...firstData]  // 必须这么写,多层数组也要这么写
        temp.push({
           value: '',
        })
        setFirstData(temp)
  }
function Indicatorfilter() {
    let [num,setNums] = useState([0,1,2,3])
    const test = () => {
        // 这里坑是直接采用push去更新num,setNums(num)是无法更新num的,必须使用num = [...num ,1]
        num.push(1)
        // num = [...num ,1]
        setNums(num)
    }
    return (
        <div className='filter'>
            <div onClick={test}>测试</div>
            <div>
                {num.map((item,index) => (
                    <div key={index}>{item}</div>
                ))}
            </div>
        </div>
    )
}


class Indicatorfilter extends React.Component<any,any>{
    constructor(props:any){
        super(props)
        this.state = {
            nums:[1,2,3]
        }
        this.test = this.test.bind(this)
    }

    test(){
         // class采用同样的方式是没有问题的
        this.state.nums.push(1)
        this.setState({
            nums: this.state.nums
        })
    }

    render(){
        let {nums} = this.state
        return(
            <div>
                <div onClick={this.test}>测试</div>
                    <div>
                        {nums.map((item:any,index:number) => (
                            <div key={index}>{item}</div>
                        ))}
                    </div>
            </div>
                       
        )
    }
}
  • useState设置状态的时候,只有第一次生效,后期需要更新状态,必须通过useEffect
// TableDeail是一个公共组件,在调用它的父组件里面,我们通过set改变columns的值,以为传递给TableDeail的columns是最新的值,所以tabColumn每次也是最新的值,但是实际tabColumn是最开始的值,不会随着columns的更新而更新
const TableDeail = ({
    columns,
}:TableData) => {
    const [tabColumn, setTabColumn] = useState(columns) 
}

// 正确的做法是通过useEffect改变这个值
const TableDeail = ({
    columns,
}:TableData) => {
    const [tabColumn, setTabColumn] = useState(columns) 
    useEffect(() =>{setTabColumn(columns)},[columns])
}
  • 必包带来的坑,参考demo里面的state变量
    因为每次 render 都有一份新的状态,因此上述代码中的 setTimeout 使用产生了一个闭包,捕获了每次 render 后的 state,也就导致了输出了 0、1、2。如果你希望输出的内容是最新的 state 的话,可以通过 useRef 来保存 state。前文讲过 ref 在组件中只存在一份,无论何时使用它的引用都不会产生变化,因此可以来解决闭包引发的问题。
  • 滥用useContent
7. useState中的第二个参数更新状态和class中的this.setState区别?
8. useEffect和useLayoutEffect区别?

useEffect是render结束后,callback函数执行,但是不会阻断浏览器的渲染,算是某种异步的方式吧。但是class的componentDidMount 和componentDidUpdate是同步的,在render结束后就运行,useEffect在大部分场景下都比class的方式性能更好.

useLayoutEffect里面的callback函数会在DOM更新完成后立即执行,但是会在浏览器进行任何绘制之前运行完成,阻塞了浏览器的绘制.

9. 使用hooks实现一个计时器?(demo)

计时器demo
注意第一个计时器错误的写法,在useEffect里面重复定义setInterval,正确写法是setInterval只定义一次,它的回调函数保存状态的更新,重点是把count更新和setInterval定义分开。

10. 使用hooks实现自定义hooks, 一个计算组件从挂载到卸载的时间?(demo,hooks抽取公共逻辑的应用)

参考:
深入理解 React 高阶组件
常用的hooks

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值