理解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就先学习到这里。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345