React Hooks基础理念,手写React Hooks简易源码

324 阅读6分钟

Hooks的理念是Sebastian Markbage提出的。useEffect,UseState,UseContext,UseCallback,UseMemo等,hooks可以理解为函数组件的零件箱,因为函数组件相对于类组件比较简单,没有生命周期,注入操作state等功能,facebook就开发了hooks来完善功能,因为函数组件与react设计理念更相融合,函数式开发,真正做到了函数输入数据,输出UI,但是也不能简单这样理解,比如useEffect不仅仅简单理解为组件的生命周期。下面是通过手写useState的源码,来还原hooks实现过程。 首先来了解一下重要的概念-代数效应(通常所说的代数去除代数效应就是把副作用从函数式编程中剥离出去)。先来看看一个函数,传入产品的id,得到产品的总数:

function getTotalNum(...arg) {

  let total = 0

  arg.forEach(id=> {

const num = getCommonNum(id)

total = total + num

})

return total

}

getCommonNum是一个通过id获得产品总数的函数,如果要调用接口,发出http请求,这就是一个有副作用的方法,很明显也是一个异步方法,那这个方法返回的total就是有问题,用async,await改造如下:

async function getTotalNum(...arg) {

  let total = 0

  arg.forEach(id=> {

const num = await getCommonNum(id)

total = total + num

})

return total

}

但是这样改变改变了函数的结构,就会让调用getTotalNum的函数变成async,可能也会让调用getTotalNum的函数的调用函数变成async,这样就太复杂了,有没有语法糖不改变函数的结构,来让函数支持异步的方法?首先我们虚构一个语法,如下:

function getCommonNum(id) {

  const num = perform id

  return num

}

执行perform id,相当于执行一个throw new Error(id),将到特殊的处理,如果特殊的处理是异步的方法,也会等结果返回后执行return,相当于:

try {

  getTotalNum(1, 2, 3)

} handle (id) {

  // 执行到perform  id, 执行跑到这里,获得得到id,异步请求获得产品数量,请求到结果后, perform  id 执行结束, num结果就是正确的,

}

相当于把从其余地方得到的数据,无论是同步的,还是异步的过程得到数据都不影响调用他的函数同步拿到数据结果,这就叫消除副作用,hooks就是主要为了解决这一过程:

function totalComponent(props) {

  const num1 = useCommentNum(props.id1)

  const num2 = useCommentNum(props.id2)

  return <div> {num1 + num2}</div>

}

function useCommentNum(id) {

  const [num, updateNum]useState(0)

  useEffect(() => {

    fetch(`https://xxx.com?id=${id}`).then(res => res.json).then({num} => {

      updateNum(num)

    })

  }, [id])

  return num

}

seCommentNum请求数据是异步的,但是同步渲染的组件totalComponent也会渲染正确。

这样让函数组件更加纯粹。

现在来简单实现一下useState的源码:

首先有一个模拟一个组件: 首先简易模拟一个react的源码,React 现在是fiber架构,定义一个fiber对象,相当于一个组件的对象 ,对象里面的stateNode属性保存的是函数组件本身,组件对象里面有一个属性menoizedState保存hooks对象的链表(hook数据,链表(组件调用多个useState),用来操作useState整体),再定义一个调度函数schedule方法(相当于react.render),挂载更新组件,一个全局变量isMount(为true,表示第一次挂载组件,为false表示更新组件),一个全局变量workInProgressHook,表示当前组件正在处理的hook,每次重新渲染时(调用schedule时)都要把当前正在执行的workInProgressHook指向组件的第一个hook,代码如下:


let isMount = true

let workInProgressHook = null  *// 正在处理的hook***

const fiber = {

  stateNodeApp,

  menoizedStatenull  // 就是hook,hook数据,链表(多个useState),用来操作useState整体

}

function schedule() {

  const app = fiber.stateNode()

  isMount = false

  workInProgressHook = fiber.menoizedState

  return app

}

function App() {

  const [num, updateNum] = useState(0)

  const [num2, updateNum2] = useState(2)

  console.log('ismount', isMount)

  console.log(num)

  console.log(num2)

  return {

    onClick() {

      updateNum2(num => num + 1)

    },

    onfocus() {

      updateNum(num => num + 1)

    }

  }

}

window.app = schedule()

app.onClick() // 模拟组件里按钮点击

现在开始写useState函数:useState返回一个数组,新的变量state(num),改变state的方法(updateNum),新建一个hook对象链表,因为组件里面可以调用多次useState,所以hook定义了一个链表,用next随性保存下一个hook对象,并用menoizedState保存了state变量的值,isMount为true时,挂载阶段,收集所用的state对象,保存到链表中,isMount为false 时,把挂载阶段收集到的链表从组件对象fiber的menoizedState的属性中依次拿出来赋值给正在执行的workInProgressHook全局变量中(这里看出来useState不能写到if语句、点击事件中的原因了,因为state的hook对象链表收集在挂载阶段已经完成了,且顺序不能改变,但是if语句、点击事件可能在组件更新阶段才能执行,但更新阶段不收集state链表了,就会拿到已有的链表里的hook对象,拿到的对象可能不是当前需要的对象,就会赋值错误,从而渲染错误):

function useState(initialState) {

  let hook

  if (isMount) {

    hook = {

      menoizedState: initialState,

      next: null

    }

    if (!fiber.menoizedState) {

      fiber.menoizedState = hook

    } else {

      workInProgressHook.next = hook

    }

    workInProgressHook = hook

  } else {

    hook = workInProgressHook

    workInProgressHook = workInProgressHook.next

  }

}

dispatchAction中定义一个update对象,来保存修改state变量的值,可以多次调用改变状态的函数(执行得到useState返回值数组的第二个值),所以update是一个链表,又为了执行更新时的循环判断,update定义成了一个环转链表。如:

一个update:

update.next = update

2个update

Update.next = queue.pending.next ,queue.pending.next指向第一个update,这行代码让第二个update指向了第一个update,queue.pending.next = update,这行代码让第一个节点指向了第二个节点,这样形成了一个新的闭环。并把最新的环状链表赋值给queue.pending,并调用schedule方法,触发更新,代码如下:

function dispatchAction(queue, action) {   *// 每次执行updateNum都会执行这个函数  action= (num => num + 1)*

  const update = {

    action,

    next: null   *// 执行对此update方法*

  }

  if (queue.pending === null) { *//还没有* *接收一个update方法*

    update.next = update

  } else {

    *// u0 -> u0*

    *// u1 -> u0 -> u1*

    update.next = queue.pending.next

    queue.pending.next = update

  }

  queue.pending = update
  schedule()
}

调用schedule方法后,会执行组件的函数,从而执行useState方法,下面就写useState更新变量的过程。此时hook对象里的queue.pending上有一条环状链表,遍历执行里面的没节点,。用hook.memoizedState为参数,执行修改,从链表的最后向前遍历执行,如果执行到第一个节点,和最后一个节点相同,则不执行,跳出循环,并更新保存最新的memoizedState的值。再把hook.quequ.pending赋值为null,清空需要更新变量的全部update,最后返回数组,完整的useState代码如下:

function useState(initialState) {

  let hook

  if (isMount) {

    hook = {

      menoizedState: initialState, *//(一个use state,多个回调)*

      next: null,

      queue: { *// 找到当前的队列*

        pending: null  *// 函数队列,闭环的函数链表*

      }

    }

    if (!fiber.menoizedState) {

      fiber.menoizedState = hook

    } else {

      workInProgressHook.next = hook

    }

    workInProgressHook = hook

  } else {

    hook = workInProgressHook

    workInProgressHook = workInProgressHook.next

  }

  *// todo*

  let baseState = hook.menoizedState

  if (hook.queue.pending) {

    let firstUpdate = hook.queue.pending.next

    do {

      const action = firstUpdate.action

      baseState = action(baseState)

      firstUpdate = firstUpdate.next

    } while (firstUpdate != hook.queue.pending.next)

    hook.queue.pending = null

  }

  hook.menoizedState = baseState

  return [baseState, dispatchAction.bind(null, hook.queue)]

}

在这里基本上把useState的执行过程,源码进行了简易的还原,全部完整的代码如下:

let isMounttrue

let workInProgressHook = null

const fiber = {

  stateNode: App,

  menoizedState: null  // 就是hook,hook数据,链表(多个useState),用来操作useState整体

}

function useState(initialState) {

  let hook

  if (isMount) {

    hook = {

      menoizedState: initialState, *//(一个use state,多个回调)*

      next: null,

      queue: { *// 找到当前的队列*

        pending: null  *// 函数队列,闭环的函数链表*

      }

    }

    if (!fiber.menoizedState) {

      fiber.menoizedState = hook

    } else {

      workInProgressHook.next = hook

    }

    workInProgressHook = hook

  } else {

    hook = workInProgressHook

    workInProgressHook = workInProgressHook.next

  }

  // todo

  let baseState = hook.menoizedState

  if (hook.queue.pending) {

    let firstUpdate = hook.queue.pending.next

    do {

      const action = firstUpdate.action

      baseState = action(baseState)

      firstUpdate = firstUpdate.next

    } while (firstUpdate != hook.queue.pending.next)

    hook.queue.pending = null

  }

  hook.menoizedState = baseState

  return [baseState, dispatchAction.bind(null, hook.queue)]

}

function dispatchAction(queue, action) {   // 每次执行updateNum都会执行这个函数  action= (num => num + 1)

  const update = {

    action,

    next: null   // 执行对此update方法

  }

  if (queue.pending === null) { *//还没有触发更新*

    update.next = update

  } else {

    *// u0 -> u0*

    *// u1 -> u0 -> u1*

    update.next = queue.pending.next

    queue.pending.next = update

  }

  queue.pending = update

  schedule()

}

function schedule() {

  const app = fiber.stateNode()

  isMountfalse

  workInProgressHook = fiber.menoizedState

  return app

}

function App() {

  const [num, updateNum] = useState(0)

  const [num2, updateNum2] = useState(2)

  console.log('ismount', isMount)

  console.log(num)

  console.log(num2)

  return {

    onClick() {

      updateNum2(num => num + 1)

    },

    onfocus() {

      updateNum(num => num + 1)

    }

  }

}

window.app = schedule()

app.onClick()

app.onfocus()