React常用hook以及部分实现

372 阅读9分钟

一、为什么要有Hook

下面有这么个场景, 父组件想要获取子组件的input标签,因为ref不属于props的属性,所以子组件通过this.props.ref会报错
import React, { Component, createRef } from 'react'

class Sub extends Component {
  render() {
    return <input ref={this.props.ref} /> 
  }
}

export default class Parent extends Component {
  // 创建ref对象
  input = createRef()
  // 获取焦点
  focus = () => {
    this.input.current.focus()
  }
  render() {
    return <Sub {...this.props} ref={this.input} />
  }
}

image.png

我们可以使用React.forwardRef进行转发。

先来看个高阶组件ref转发的用法

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

// 增强组件的函数
const insertLog = WrappedComponent => {
   class Log extends Component {
      render() {
          // 把forwardRef取出来 传递给被增强的组件
          const { forwardedRef, ...props } = this.props
          return <WrappedComponnet {...props} ref={forwardedRef} />
      }
   }
      // 通过Log把转发ref递给被包裹组件
      return forwardRef((props, ref) => <Log {...props} forwardedRef={ref} />)
}

class Sub extends Component {
    input = createRef()
    focus = () => {
        // focus ⽅法执⾏时会让 input 元素聚焦。
        this.input.current.focus()
     };
    render() {
        return <input {...this.props} ref={this.input} />
    }
}

export default class Parent extends Component {
    state = {
        value: ''
    }
    input = createRef() // 引用子组件实例, 便于调用实例上方法
    onFocus = () => {
        this.input.current.focus() // 调用子组件实例上的方法
    }
    onChange = (e) => {
        this.setState({value: e.target.value })
    }
    Wrap = insertLog(Sub)
    render() {
        const wrap = this.Wrap
        return (
              <>
                  <Wrap onChange={this.onChange} value={this.state.value} ref={this.input} />
                  <button onClick={this.onFocus}>点击聚焦</button>
              </>
        )
    }
}
graph 
Parernt --> Wrap --> Log --> WrappedComponnet(Sub)

再来看一下hooks的写法

import React, { createRef, forwardRef } from 'react'
// 子组件
const Sub = forwardRef((props, ref) => {
   return <input {...props} ref={ref} />
})
// 父组件
export default class Parent extends React.Component {
   input = createRef()
   // 通过input.current就可以获取到子组件的input标签了
  onFocus = () => {
    this.input.current.focus()
  }
  render() {
    return <>
      <Sub ref={this.input} />
      <button onClick={this.onFocus}>点击聚焦</button>
    </>
  }
}
graph TD
Parent --> Sub

这样嵌套的问题就解决了 代码也简洁了很多

二、常见的hook

1.useState

const [state, setState] = useState(initialState)

useState返回一个state和它的更新函数 可以通过这个更新函数setState 去改变state的值

方式有两种

  1. 直接赋值 直接为之前的state重新赋值为newVal setState(newVal)

  2. 通过函数赋值 接收一个先前的state, 并返回更新后的值 setState(prevState => prevState + 1)

export default function UseState() {
    // 返回一个count当作数据
    // setCount是一个函数用于改变数据 
    // useState(0) 相当于将count设置初始值为0
    const [count, setCount] = useState(0)
    return (
       <button onClick={() => setCount(count+1)}>点击更新count(直接更新) ---  {count}</button>
       <button onClick={() => setCount(prevState => prevState + 1)}>点击更新count(函数式更新)--- {count}</button>
    )
}

2. useEffect

①useEffect在组件初次渲染和更新的时候执行第一个回调函数,

②有时候我们不希望它每次更新执行,可以通过第二个依赖项来控制, 只有依赖项变化第一个回调函数才会执行

③有时候我们希望执行完回调函数后清除副作用, 那么可以返回一个函数用来做清除 操作

useEffect(() => {
    return () => {
    
    }
}, [])

上面提到了副作用, 那么什么是副作用呢,就是在调用函数之后,还做了其他的事情, 下面举一个例子

useEffect(() => {
   let [count, setCount] =  useState(0)
  useEffect(() => {
    // 每隔一秒钟让count + 1
    let timer = setInterval(() => {
      setCount(count + 1)
    }, 1000);
  })
  return <span>{count}</span>
})

刚开始我们发现count正常的变化,等过了一段时间后, 显示开始出现鬼畜的效果,这是因为useEffect中的回调函数每次更新都会生成一个新的定时器,这样就导致了定时器越来越多,就出现了显示异常的后果,所以我们要返回一个函数用来清除这个副作用

useEffect(() => {
   let [count, setCount] =  useState(0)
  useEffect(() => {
    // 每隔一秒钟让count + 1
    let timer = setInterval(() => {
      setCount(count + 1)
    }, 1000); 
    // 这里用来清除这一次定时器
    return () => {
        clearInterval(timer)
    }
  })
  return <span>{count}</span>
})
function UseEffect() {
   const [count, setCount] = useState(0)
   // 相当于componentDidMount和componentDidUpdate 只要本组件有更新,里面的回调函数就会调用
   useEffect(() => {
      console.log(`mount update: ${count}`)
   })
   useEffect(() => {
     console.log(`mount: ${count}`) 
     // 第二个参数是依赖项, 只有当依赖项发生改变,才会继续执行, 这里是空数组 所以只执行第一次 相当于componentDidMount
   }, [])
   useEffect(() => {
      console.log(`mount + update count: ${count}`) // 只要count发生变化,才会继续执行
   }, [count])
   return (<button onClick={() => setCount(c => c + 1)}>父组件+1 {count}</button>)
}

3. useRef

const refContainer = useRef(initVal)
  • 返回一个对象, 改对象只有一个current属性,初始值为initVal
  • 当更新current值,组件是不会重新渲染的, 这点和state不同
  • 返回的对象可以绑定在dom节点上,在挂载完毕后可以拿到他

下面是一个获取dom元素的例子

  import React, { useRef, useEffect } from 'react'
  export default function UseRef() {
      console.log('render')
      let refObj = useRef(null)
      console.log('在渲染之前获取', refObj)
      useEffect(() => {
        console.log('在dom元素挂载之后获取', refObj)
      })
      const handleClick = () => {
        console.log(count.current)
        count.current = count.current + 1
      }
      return (<>
        <button onClick={() => refObj.current.focus()}>点击input获取焦点</button>
        <input ref={refObj} />
        <div>
          <button onClick={handleClick}>点击增加count --- {count.current}</button>
        </div>
      </>)
}

ref.png

在我们点击增加count的按钮后, 触发了handleClick函数 发现count.current 在变化,但是render没有重新打印, 说明UseRef函数没有被重新执行,而useState则是每次改变state,函数就会重新调用 并且渲染最新的状态

所以我们可以用它来模拟componentDidMount和componentDidUpdate

export default function UseRef() {
  const container = useRef(false)
  let [count, setCount] = useState(0)
  useEffect(() => {
    if(container.current) {
     // 模拟componentDidUpdate
      console.log('DidUpdate')
    }else {
     // 模拟componentDidMount
      console.log('DidMount')
      container.current = true
    }
  })
  return (<div>模拟生命周期
    <button onClick={() => setCount(count + 1)}>+1</button>
    <span>count: {count}</span>
  </div>)
}

4. forwardRef

forwarRef((props, ref) => {})
  • forwardRef会创建一个组件
  • 组件可以接收到父组件传递的ref
  • 子组件可以把ref挂在自己的dom元素上
  • 父组件通过ref就能获取到该dom元素
export default function Parent() {
  let sonRef = useRef(null)
  return (<>
    <Son ref={sonRef}></Son>
    <button onClick={() => sonRef.current.focus()}>点击获取子组件input焦点</button>
  </>
  )
}

const Son = forwardRef((props, ref) => {
  return <>
    <input ref={ref} />
  </>
})

5. useImperativeHandle

它和forwardRef经常配套使用,有时候我们不希望父组件操作子组件ref的过多dom属性, 就需要用useImperativeHandle, 它用来限制子组件暴露的信息, 只有他定义的第二个参数的属性和方法,父组件才能拿取到

   useImperativeHandle(ref, createHandle, [deps])
  • ref: 定义暴露哪个ref
  • createHandle: 定义暴露的信息
  • deps: 当前依赖的列表,当其中的依赖发生变化时, 才会重新将子组件的实例属性输出到父组件
export default function Parent() { 
  let sonRef = useRef(null)
  useEffect(() => {
    console.log('parent')
  })
  return (<>
    <Son ref={sonRef}></Son>
    <button onClick={() => sonRef.current.focus()}>点击获取子组件input焦点</button>
    <button onClick={() => console.log(sonRef.current)}>点击查看子组件绑定的ref</button>
  </>
  )
}

const Son = forwardRef((props, ref) => {
  useEffect(() => {
    console.log('son')
  })
  useImperativeHandle(ref, () => ({
    focus: () => {
      ref.current.focus()
    }
  }), [])
  return <>
    <input ref={ref} />
  </>
})

当点击第二个按钮 可以看到只能访问定义的focus函数

useImperative.png

6. useMemo和useCallback

先看一个例子

const UseCallbackSub = ({value, onChange}) => {
  console.log('子元素发生了渲染value', value)
  return <input onChange={onChange} value={value} type="number" />
}

export default function UseCallback() {
  const [count, setCount] = useState(0)
  const [value, setVal] = useState('')
  const onChange = (e) => {
    setVal(e.target.value)
  }
  return (
    <>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <UseCallbackSub value={value} onChange={onChange}></UseCallbackSub>
      <div>value: {value}</div>
    </>
  )
}

useCallback.png

useCallback2.png

父组件更新了count,导致重新渲染 也会引起子组件的渲染,但是我们子组件的props没有发生变化,这样的话这次渲染就浪费了性能,那么我们怎么根据props的变化控制子组件的重新渲染呢?

const UseCallbackSub = memo(({value, onChange}) => {
  console.log('子元素发生了渲染value', value)
  return <input onChange={onChange} value={value} type="number" />
})

我们可以通过memo的方式来进行控制,memo会对函数式的所有props进行对比, 现在我们传递的props是onChange和value 当onChange和value发生变化后, 我们才更新子组件, 不变的话,始终返回上一次渲染过的组件,这里利用了缓存.

但是这时候发现子元素还是重新渲染了, 问题就在onChange, 每次父组件重新渲染导致了每次的onChange函数都是新的引用值,这样每次给子组件传递的onChange就都不一样, 所以我们需要用useCallback给onChange函数包裹一下 ,

 const onChange = useCallback((e) => {
    setVal(e.target.value)
  }, [])
  • 第一个参数缓存的函数
  • 第二个参数依赖项, 当依赖项发生改变时,生成新的函数, 否则用之前缓存过的

如果我们想直接在memo里控制默认不比较onChange的话 也可以这么写

const UseCallbackSub = memo(({value, onChange}) => {
  console.log('子元素发生了渲染value', value)
  return <input onChange={onChange} value={value} type="number" />
  // 如果之前的value和这次的value相同的话, 子组件不会重新渲染,走缓存
}, (prev, cur) => prev.value === cur.value)

7.useContext

import React, { useState, createContext, useContext } from "react";

const Context = createContext()

首先通过createContext生成Context对象, Context对象可以跨层级传递变量,实现共享

使用Context的时候 将组件用 Context.Provider包裹, 并传入value value就是你时你需要传递的变量

<Context.Provider value={store}>
    组件
</Context.Provider>
export default function Parent() {
  const [count, setCount] = useState(0)
  const store = {
    count, setCount
  }
  return (
    <Context.Provider value={store}>
      <button onClick={() => setCount(count + 1)}>+1 {count}</button>
      <Sub1 />
    </Context.Provider>
  )
}

当组件需要使用Context时,通过useContext(Context)可以得到传递过来的value

function Sub1() {
  const ctx = useContext(Context)
  return (<>
    <button onClick={() => ctx.setCount(c => c + 1)}>
      Sub1能通过 Context访问数据源 {ctx.count}
    </button>
    <Sub2 />
  </>)
}

function Sub2() {
  const ctx = useContext(Context)
  return (<>
   <button onClick={() => ctx.setCount(c => c + 1)}>
      Sub2能通过 Context访问数据源 {ctx.count}
    </button>
  </>)
}

无论嵌套多少层组件,都可以通过Context拿到传递的值

三、useState和useEffect实现

1.useState

  • 本质都用到了闭包
  • 通过全局的一个数组储存每次的状态值,用index作为游标 当组件重新渲染后,index归为0 取出之前缓存的状态
import ReactDOM from "react-dom";

// 定义一个缓存所有状态的数组
let state = [];
// 定义其实索引
let index = 0;

function myuseState(initVal) {
  // 记录这个state的索引 ,因为index会一直变化
  let currentIndex = index
  // 第一次state[index]为undefined 取 初始值, 之后渲染会拿第一次缓存的值
  state[currentIndex] = state[currentIndex] || initVal;
  function dispatch(newVal) {
    // 这里用到了闭包,改变当前state的值为newVal
    state[currentIndex] = newVal; 
    // 重新渲染
    render()
    }
    // 把当前的state 和 对应的更新函数 return出去, 并把索引值+1
    return [state[index++], dispatch];
  }
  
   function render() {
     // 这里为了让下次渲染起始index为0 这样就可以拿到上次缓存的值
     index = 0;
     // 组件重新渲染
     ReactDOM.render(<App />, document.getElementById('root'))
  }
  
   export default function App() {
    let [count, setCount] = myuseState(0);
    let [name, setName] = myuseState("xiaoming");
    return (<div>
      <div>count: {count}</div>
      
      <div>name: {name}</div>
      <div>
        <button onClick={() => setCount(count + 1)}>count + 1</button>
        <button onClick={() => setName('xiaohong')}>changeName</button>
      </div>
    </div>)
  }

2. useEffect

和useState的实现大致相同,也需要在render里面清空index

import ReactDOM from "react-dom";
//定义一个缓存所有状态的数组
let state = [];
//定义其实索引
let index = 0;

function myuseState(initVal) {
  // 记录这个state的索引 ,因为index会一直变化
  let currentIndex = index
  // 第一次state[index]为undefined 取 初始值, 之后渲染会拿第一次缓存的值
  state[currentIndex] = state[currentIndex] || initVal;
  function dispatch(newVal) {
    // 这里用到了闭包,改变当前state的值为newVal
    state[currentIndex] = newVal;
    render()
    }
    // 把当前的state 和 对应的更新函数 return出去
    return [state[index++], dispatch];
  }
  
  // 定义effect数组
  let effects = []
  // 定义effect索引,作为游标,来缓存多次的useEffect的依赖项
  let effectIndex = 0

  function myUseEffect(callback, deps) {
    // 说明deps没传, 每次callback执行
    if(!deps) {
      callback()
    }else {
      if(effects[effectIndex]) { // 说明不是第一次
        // 拿到上一次的依赖数组, 目的是和这一次的进行比较
        let lastDeps = effects[effectIndex]
        let same = deps.every((item, index) => item === lastDeps[index])
        if(same) {
          // 如果相同了, 代表依赖没有变化, 则让索引+1, 找到下一个useEffect
          effectIndex++
        }else {
          // 如果变化了, 则把最新的依赖值赋给effects, 并让索引+1, 找到下一个useEffect
          effects[effectIndex++] = deps
          callback()
        }
      }else {// 说明是第一次渲染,  并让索引+1, 找到下一个useEffect
        effects[effectIndex++] = deps
        callback()
      }
    }
  }

  function render() {
     // 这里为了让下次渲染起始index为0 这样就可以拿到上次缓存的值
     index = 0;
     // 同理, 让effectIndex为0 获取先前的deps依赖值
     effectIndex = 0
     // 组件重新渲染
     ReactDOM.render(<App />, document.getElementById('root'))
  }

  export default function App() {
    let [count, setCount] = myuseState(0);
    let [name, setName] = myuseState("xiaoming");
    myUseEffect(() => {
      console.log('只触发一次', count)
    }, [])
    myUseEffect(() => {
      console.log('只有count变化才会触发', count)
    }, [count])
    return (<div>
      <div>count: {count}</div>
      
      <div>name: {name}</div>
      <div>
        <button onClick={() => setCount(count + 1)}>count + 1</button>
        <button onClick={() => setName('xiaohong'+Math.random())}>changeName</button>
      </div>
    </div>)
  }

⚠记得把严格模式关闭,否则运行会出问题

image.png