Reack hooks源码

133 阅读7分钟

为什么使用ReactHooks?

为了解决类组件的一些问题,this指向不明的问题,业务逻辑分散在不同的生命周期方法中,复用逻辑不方便比较复杂。函数组件虽然简单,但是他没有状态,为了让函数组件有状态,所以有了hooks。

useReducer:

接受一个reducer函数和一个初始状态作为参数,返回当前的state和一个与该reducer函数关联的dispatch方法。

const [state, dispatch] = useReducer(reducer, initialState)

实现:在第一次挂载函数组件的时候需要创建保存hooks状态的对象,在vdom上保存一个属性,用于存放当前函数组件上的hooks信息,信息包括当前hooks的索引,以及当前索引对应的状态,当调用useReducer的之前会在全局保存当前正在渲染的函数组件的Vdom,当前组件的根节点对象,对象中存在一个update的方法,当调用useReducer的之后,会取到当前Vdom上保存的hooks信息,调用dispatch方法的时候根据老状态和派发的动作计算新的状态,覆盖老的状态,最后并且让hooks信息中的索引++,在更新之后将hooks信息中的索引重置为0。

vodm.hooks = {
    hooksIndex: 0, // 当前hook的索引,
    hookStates: []
}
let currentVdom = null; // 当前正在渲染的函数组件,在初次渲染组件的时候保存 
let currentRoot= null; // 组件在创建根的时候保存的根节点对象
function useReducer (reducer, initialState) {
  const {hooks} = currentVdom //获取hooks对象
  const {hookIndex, hookState} = hooks; //获取当前hooks对象上保存的索引和状态
  const hookState = hookStates[hookIndex] //获取当前索性对应的状态
    if( isUndefined(hookState) ){  //判断当前hookState不存在
        hooksStates[hookIndex] = initialState
    }
    // 派发事件的方法
    function dispatch(action){
        const oldState = hooksStates[hookIndex];
        // 根据老状态和派发的动作计算新的状态,覆盖老的状态
         const newState = reducer( hooksStates[hookIndex], action )
        // 如果新的状态和老的状态一样则不更新
        if( newState  !== hookState ) { 
        hooksStates[hookIndex] = newState
            //这里要调用组件的更新
        currentRoot.update()
         }
    }
    // 当执行完useReducer函数的时候,索引++
    return [ hooksStates[hooks.hookIndex++] , dispatch ]
}

useState:

源码中它是一种特殊的useReducer ,特殊在于直接给状态,不需要计算,他会内置一个reducer,这个reducer会把dispatch派发的动作当成新的状态

const defaultReducer = (state, action) => typeof action === "function" ? action(state) : action;
function useState(initialState) {
    return useReducer( defaultReducer,initialState)
}

useMemo:

返回一个记忆后的值,只有当依赖项发生变化的时候,才会重新计算这个值

//factory 创建对象的函数
// deps依赖的数组
function useMemo( factory , deps) {
    const {hooks} = currentVdom;
    const {hooksIndex, hookStates} = hooks;
    // 获取状态数组中的hookIndex对应的值[newMemo, deps]
    const prevHook = hookStates[hookIndex];
    if( prevHook ) {
        // 取出上一个对象和上一个依赖数组
        const [prevMemo, prevDeps] = prevHook;
        // 对比新的依赖数组和老的依赖数组中的每一项,如果全部相等的话
        if( deps.every((dep, index) => dep === prevDeps[index])) {
            // 直接索引加1,往后走下一个hook,并且返回上一次缓存的值
            hooks.hookIndex++;
            return prevMemo;
        }
    }
    // 第一次执行工厂方法,返回newMemo
    const newMemo = factory();
    // 把计算出来的依赖数组保存在hookStates数组中
    hookStates[hookIndex] = [newMemo, deps]
    hooks.hookIndex++;
    return newMemo;
}

useCallback:

返回一个记忆后的callback函数,他会返回一个不变的函数,直到依赖项发生变化在重新返回一个新的callback函数

//callback 回调函数
// deps依赖的数组
function useCallback( callback, deps) {
    // 使用useMemo实现useCallback
    return useMemo( () => callback, deps )
}

useContext:

它允许无需明确的传递props,就能让组件订阅context的变化。结合React.createContext(),内部会存储一个_currentValue,直接染回。

// 结合React.createContext(),内部会存储一个_currentValue,直接染回
function useContext (context) {
    return context._currentValue
}

useEffect:

允许你在函数组件中执行有副作用的操作(副作用:定时器,操作dom,请求接口)。它与组件中的生命周期方法,componentDidMount,componentDiaUpdate,componentWillUnmount

注意事项:

1.不要在循环条件,if判断和嵌套函数中调用(原因是:hooks的关系是一对一的,如果在函数嵌套和循环条件中使用会导致hooks对应关系错乱产生错误)。

2.useEffect会返回一个函数,这个函数会在组件卸载前或者重新执行新的副作用之前被调用。

function useEffect(effect, deps) {
  // 获取当前函数组件的虚拟dom内部存放的hooks
  const { hooks } = currentVdom;
  // 从此对象上结构出当前hooks的索引,和hookStates数组
  const {hookIndex, hookStates} = hooks;
  let shouldRunEffect = true; // 是否要执行effect函数
  // 尝试读取老的useEffect,第一次执行的时候值为undefined
  const previousHookState = hookStates[hookIndex];
  let prevCleanup; // 上一个销毁函数
  if(previousHookState){
      // 第二次执行的时候previousHookState已经保存了上次effect执行返回的销毁函数和依赖数组。
      const {cleanup, prevDeps} = previousHookState;
      prevCleanup = cleanup;
      if(deps){
          // 判断新的依赖数组中是否有某一项和老的依赖数组相对应的值不相等。
          // 判断数组中的每一项和就数组中的每一项是否完全相同,相同不执行,不同要重新执行
          shouldRunEffect = deps.some((dep, index) => !Object.is(dep,prevDeps[index]))
          // shouldRunEffect = !deps.every((deps,index) => Object.is(deps[index], prevDeps[index]))
      }
  }
  // 如果shouldRenEffect的值为true,标识要重新执行effect
  if(shouldRunEffect){
      // 其实这个effect不是立刻执行的,而是包装成一个宏任务,在浏览器绘制渲染页面时候执行
      setTioumt(() => {
          prevCleanup?.(); // 如果上一次执行的effect函数返回一直清理函数,需要在下一次执行effect函数之前执行。
          // 执行副作用函数,返回一个清理函数
          const cleanup = effect();
          // 在hooksState数组中保存执行effect得到的清理函数,以及当前的依赖数组
          hookStates[hookIndex] = {cleanup, prevDeps: deps}
      })
  }
  // 完事之后让当前的hooks索引向后走一步
  hooks.hoookIndex++
}

为啥副作用一定要在这个钩子中调用呢,在外面调用会有什么问题吗?

  1. react规定useEffect是唯一进行副作用的地方。

  2. 写在外面函数组件每次渲染都会执行。

  3. 函数组件是纯函数,不能这么做

useLayoutEffect:

和useEffect有相同的签名,也有一些差异,

  1. 执行时机不同,useEffect是在浏览器绘制之后异步执行,会有延迟

  2. useLayoutEffect在浏览器执行绘制之前执行,不会延迟

  3. 因为useEffect是在绘制之后执行的,所以不会阻止页面的渲染,因为useLayoutEffect是在浏览器执行绘制之前同步执行的

function useLayoutEffect(effect, deps) {
  function useEffect(effect, deps) {
  // 获取当前函数组件的虚拟dom内部存放的hooks
  const { hooks } = currentVdom;
  // 从此对象上结构出当前hooks的索引,和hookStates数组
  const {hookIndex, hookStates} = hooks;
  let shouldRunEffect = true; // 是否要执行effect函数
  // 尝试读取老的useEffect,第一次执行的时候值为undefined
  const previousHookState = hookStates[hookIndex];
  let prevCleanup; // 上一个销毁函数
  if(previousHookState){
      // 第二次执行的时候previousHookState已经保存了上次effect执行返回的销毁函数和依赖数组。
      const {cleanup, prevDeps} = previousHookState;
      prevCleanup = cleanup;
      if(deps){
          // 判断新的依赖数组中是否有某一项和老的依赖数组相对应的值不相等。
          // 判断数组中的每一项和就数组中的每一项是否完全相同,相同不执行,不同要重新执行
          shouldRunEffect = deps.some((dep, index) => !Object.is(dep,prevDeps[index]))
          // shouldRunEffect = !deps.every((deps,index) => Object.is(deps[index], prevDeps[index]))
      }
  }
  // 如果shouldRenEffect的值为true,标识要重新执行effect
  if(shouldRunEffect){
      queueMicrotas(() => { // queueMicrotas是将当前任务添加到微任务队列中
          prevCleanup?.(); // 如果上一次执行的effect函数返回一直清理函数,需要在下一次执行effect函数之前执行。
          // 执行副作用函数,返回一个清理函数
          const cleanup = effect();
          // 在hooksState数组中保存执行effect得到的清理函数,以及当前的依赖数组
          hookStates[hookIndex] = {cleanup, prevDeps: deps}
      })
  }
  // 完事之后让当前的hooks索引向后走一步
  hooks.hoookIndex++
}

useRef:

它会返回一个可变的ref对象,这个对象的current属性会被修改,并且修改current属性并不会导致组件的重新渲染。

function useRef(initialValue){
  const { hooks } = currentVdom;
  // 从此对象上结构出当前hooks的索引,和hookStates数组
  const {hookIndex, hookStates} = hooks;
  if(hookStates[hookIndex] === "undefined"){
      hookStates[hookIndex] = {current: initialValue}
  }
  return hookStates[hookIndex++]
}

useImperativeHandle:

是一个高级的hook,通常与forwardRef配合使用,允许你在使用ref的时候自定义暴露给父组件的实例值,而不是默认的实例。

function FancyInput(props, forwardRef){
    const inputRef = React.useRef();
    // 第一个参数是forwardRef 转发过来的ref, 第二个参数是一个工厂函数,可以返回一个对象,此对象将转发给传递forwardRef转发过来的ref
    //的forwardRef
    React.useImperativeHandle(forwardRef, () => {
        return {
            foucs(){
                inputRef.current.focus();
            }
        }
    })
    return <input ref={inputRef} />
}
const forwardFancyInputRef = React.forwordRef(FancyInput)
function ParentComponent(){
    const inputRef = React.useRef()
    
    const handlefocus = () => {
        // 调用的是通过子组件useImperativeHandle返回的方法
        inputRef.current?.focus();
     }
    return(
        <div>
            <forwardFancyInputRef ref={inputRef}/>
            <button onClick={handlefocus} >focus</button>
        </div>
    )
}
// 实现
function useImperativeHandle(forwardRef, fanctory) {
    forwardRef.current = fanctory()
}