理解React-hooks原理并手写hooks简单版

现在React 16+已经是主流并且挺成熟了,如果还不知道啥是hooks,没用过hooks,或者只知道它是钩子,已经很难在面试时说服面试官选择自己了。
所以很有必要搞清楚,到底啥玩意是钩子,它解决了什么,到底哪里好,它们都是如何实现的?
带着这样的问题,一起学习一下React常用 的hooks。

什么时候使用hooks,它有什么好处?

  1. 类组件可复用性差 (高阶组件复用,多层复用)
  2. 类组件性能稍差 (需要维护类实例)
  3. 生命周期管理起来麻烦(例:componentWillMount,不保证只调用一次)
  4. 函数组件+ hooks可以实现类组件的功能。

useState

let [value, setValue] = useState(initValue)
//value是当前状态
//setValue是变更状态的函数
//useState就是一个hooks
//initValue就是设置的初始状态

使用useState实现一个简单的计数功能:

function Counter(){
  const [number, setNumber] = useState(0);
  return (
    <>
      <p>计数:{number}</p>
      <button onClick={()=> setNumber(number+1)}>+</button>
      <button onClick={()=> setNumber(number-1)}>-</button>
    </>
  )
}

代码参考:useState函数组件计数
根据上面的函数组件,number是维护计数的状态 ,setNumber用来触发更新状态。
useState返回的初始值以及该变更方法,每次变更后,还重新渲染了组件:

/*
* 传参:initialState 初始值
 返回: 值,以及更新方法[state, setState]
*/
//维护一个备忘状态
let memorizedState;
function useState(initialState) {
  memorizedState = memorizedState || initialState;
  function setState(newState) {
    memorizedState = newState;
    render();
  }
  return [memorizedState, setState];
}

代码参考:useState_easy1

但是呢,上面这个实现的useState,存在问题,就是如果再初始化一个名称,也就是多次调用useState,就会状态混乱。因为变量memorizedState就是七秒钟记忆的鱼,只维护了上次的状态,多个状态就蒙了。
所以 ,我们考虑先用数组维护一下各自的状态,每次通过索引获取对应的,把上面的改一下:

//维护一个备忘状态
let memorizedState = [];
let index = 0; //每次useState初始化时,传入这个索引
function useState(initialState) {
  memorizedState[index] = memorizedState[index] || initialState;
  let currentIndex = index
  function setState(newState) {
    memorizedState[currentIndex] = newState;
    index = 0; //渲染前要归0
    render();
  }
  return [memorizedState[index++], setState]; //下次要加1
}

代码参考:useState


useEffect

副作用是一个不得不提的钩子,在中文里副作用似乎不是啥好事,但是在hooks中,副作用的功能可是相当强大,它可能替代类组件中的生命周期,如下图:


根据之前的例子我们添加useEffect,实现计数变更,则打印。

  useEffect(()=>{
    console.log('number1', number);
  });
  useEffect(()=>{
    console.log('number2', number);
  }, [number]);
  
  useEffect(()=>{
    console.log('number3', number);
  }, [number, name]);

参考代码:使用useEffect-1

实现useEffect方法
let lastDep;
//callback:回调函数,deps依赖项
function useEffect(callback, deps){
    if(!deps) return callback(); //如果没有依赖项,直接执行
    let changed = lastDep?!deps.every((item, idx) => item === lastDep[idex]):true;
  
  if(changed){//如果依赖项变更了,会执行回调
    callback();
    lastDep = deps;
  }
}

参考代码:实现useEffect-1
但是,这个方法有点问题,如果依赖项相同,多使用几次useEffect(()=>{}),便不会触发回调,也就是索引问题。
可以试一试:

 useEffect(()=>{
    console.log('number2', number);
  }, [number]);
  
  useEffect(()=>{
    console.log('number2-1', number);
  }, [number]);

useEffect正常是两个都会执行并打印,但是上面写的由于共用一个lastDeps变更,所以才只执行了一次。
那我们按照前面useState借助数组的方法来优化一下。

//...
function useEffect(callback, deps){
    if(!deps) { 
      index++;
      return callback()
    }; //如果没有依赖项,直接执行
  
  let lastDeps = memorizedState[index];
    let changed = lastDeps?!deps.every((item, idx) => item === lastDeps[idx]):true;
  
  if(changed){
    callback();
    lastDeps = deps;
    memorizedState[index] = deps;
  }
  index++;
  
}

代码参考:useState&&useEffect


useReducer

useReducer是个高级hook,如果了解过redux,那就会发现useReducer跟redux里的reducer超级像。
它也可以进行复杂状态的维护,我们先使用useReducer实现一下上面的计数器功能:

let initalArg = 0;
/*
* 处理并返回新状态
*/
function reducer(state, action){
    switch(action.type){
      case 'add':return { number: state.number+1};
      case 'minus':return { number:state.number-1};
      default:
        return state;
        break;
    }
}

/*
* 初始化状态的方法
*/
function init(initalArg){
    return { number: initalArg};
}


function Counter(){
  let [state, dispatch] = useReducer(reducer, initalArg, init);
  
  return (
    <>
      <p>计数器:{state.number}</p>
      <button onClick={()=> dispatch({type:'add'}) } >+</button>
      <button onClick={()=> dispatch({type:'minus'}) }>-</button>
    </>
  )
}

代码参考:useReducer计数器

逻辑图如下:


useReducer逻辑图

那我们就实现一下useReducer:

let memoizedState;
function useReducer(reducer, initialArgs,init){
   let initState;
  if(typeof init !== 'undefined'){
    initState = init(initialArgs);
  }else {
    initState = initialArgs;
  }
  
  memoizedState = memoizedState || initState;
  function dispatch(action){
    memoizedState = reducer(memoizedState, action);
    render(); //重新渲染
  }
  return [memoizedState, dispatch];
}

代码参考:实现useReducer-1

变更多个状态,比如名称也修改一下,需要在dispatch派发事件传payload对象。

//reducer中追加一个editName的行为
function reducer(state, action){
    switch(action.type){
      case 'add':return { ...state, number: state.number+1};
      case 'minus':return {...state, number:state.number-1};
      case 'editName': 
        let { name } = action.payload;
        return {...state, name }; 
      default:
        return state;
        break;
    }
}

//初始化状态时name
/*
* 初始化状态的方法
*/
function init(initalArg){
    return { number: initalArg, name: '计数器'};
}

//页面中加dom
 <button onClick={()=> dispatch({type:'editName', payload:{
          name:'计数器'+ Date.now()
        }})}>变更名称:</button>

代码参考:useReducer-2

使用useReducer实现useState:

function useState(initState){
  //useReducer参数:reducer, initValue,init(可选)
  return useReducer((oldState, newState) =>{
    return newState;
  },initState);
}

代码参考:使用useReducer实现useState

链表实现

先理解一下链表结构:


链表.png

那我们定义一个对象来表示链接结构:

//第一个节点的next指向下一个元素,hook = hook.next,就可能实现往后移动指针并遍历。
let hook = {
  state: null,
  next: null,
}

再看一下useState用链表实现的逻辑图:


//链表实现
let firstWorkInProgressHook = {
  memoizedState:null,
  next:null,
};//第一个钩子
let workInProgressHook = firstWorkInProgressHook; //当前工作中的hook

function useState(initialState){
  //判断如果没有就初始化一个,有的话,当前指针向后移。
  let currentHook = workInProgressHook.next?workInProgressHook.next:{
    memoizedState:null,
    next:null,
  }
  
  function setState(newState){
    currentHook.memoizedState = newState;
    workInProgressHook = firstWorkInProgressHook; //重新渲染时,重置work
    render(); //重新渲染
  }
  
  if(workInProgressHook.next){ //工作中的hook如果存在next
    workInProgressHook = workInProgressHook.next; //往后移位。
  }else {
    workInProgressHook.next = currentHook; //如果当前工作的没有下一个要执行钩子,则给它挂上。
    workInProgressHook = currentHook
  }
  
  return [currentHook.memoizedState, setState]
}

参考代码:链表实现useState

简单版本的hooks就先学习到这里。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。