博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
探索react hook的诞生背景和实现过程
阅读量:6102 次
发布时间:2019-06-20

本文共 5173 字,大约阅读时间需要 17 分钟。

最近终于是下定决心将我负责的一个公司内部系统由react v15.4.1升级到v16.8.6。v16.8.6中最被推崇的一个特性应该就是react hook了,实际上手后为能更顺手的使用也是花了2个多小时的时间把react hook的实现源码看了一遍。

useState

一,探索版本

一个函数式组件

const FunctionComponent = () => {  return (    
hello functional component
)}复制代码

要在这个组件中触发更新要怎么做,下面我们就做点努力,改造一下。

改造1

// 下面的实现中click事件被触发然后执行sayHello对say赋值hello,// 这个过程并没有触发react的更新机制,所以页面不会显示helloconst FunctionComponent = () => {  let say = 'hello functional component'  const sayHello = () => {    say = 'hello'  }  return (    <>      
{say}
)}复制代码

改造2

在1中我们已经改变了say的值,现在我只需想办法触发react的更新就可以了。如何手动触发react的更新,我想到了forceUpdate

// 需要将say作为一个FunctionComponent的外部变量// 避免在更新触发后函数式组件执行对say重新赋值let say = 'hello functional component'const FunctionComponent = props => {  const sayHello = () => {    // 改变状态    say = 'hello'    // 手动触发父组件的更新从而更新自己    props.update()  }  return (    <>      
{say}
)}class Stage extends React.Component { // 通过触发父组件的更新来更新函数式组件 // update属性用于触发更新函数式组件 handleUpdate = () => { this.forceUpdate() } render() { return (
); }}复制代码

到此我们已经达到了使用functional组件实现类似class组件更新机制的目的。

通过上面的探索在不借助useState的情况我们也能实现functional组件的状态更新。

问题是我们每次都得手动调用forceUpdate(),不够友好。而且很容易相信这个方式性能极低。

二,useState版本:

在整个hook的使用中有两个对象你不能忽视(具体区别请查看源码):

  • HooksDispatcherOnMountInDEV —— functional组件在首次调用是使用
  • HooksDispatcherOnUpdateInDEV —— functional组件在更新时调用

使用示例

const [count, setCount] = useState(0)复制代码

1,useState(0)发生了什么

每次执行useState都会生成一个hook对象

var hook = {    memoizedState: 传入的初时值,如果useState接收的是函数则是函数执行的结果,    baseState: null,    queue: 保存对state的更新队列,    baseUpdate: null,    next: null};复制代码

在一个functional组件中调用多少次useState()则会新建多少个hook对象,为了维护这些hook对象react使用了一个WorkInProgressHook的链表保存——这一步处理你可以理解成我们在探索版本中将functional组件的状态变量say做全局处理相同的目的

2,返回了什么count = ?、setCount = ?

var queue = hook.queue = {  last: null,  dispatch: null,  lastRenderedReducer: reducer,  lastRenderedState: initialState};var dispatch = queue.dispatch = dispatchAction.bind(null,    currentlyRenderingFiber$1, queue);return [hook.memoizedState, dispatch];复制代码
  • hook.memoizedState —— 其实就是我们探索版本中的say只不过我们是通过window.say保存,而它通过之前创建的WorkInProgressHook.hook保存
  • dispatch —— 其实也就是我们探索版本中的props.update,探索版本中我们使用forceUpdate更新父组件的方式来触发更新,而它采用了一个更底层的方法scheduleWork。不知道scheduleWork?请参考我之前的

总结:

functional组件有两个特性注定他不能拥有状态:

1,一个纯函数每次调用都是全新的东西。要破坏这种特性就需要让这个函数产生副总用——依赖全局变量(window.say、WorkInProgressHook.hook)。
2,之前版本中react官方提供setState用于改变状态触发更新,但这个只存在与class组件,新版本的dispatch给了我们多一个选择

小知识点:functional组件执行过程中生成的WorkInProgressHook最终会保存到当前组件对应fiber对象上

// WorkInProgressHook是一个链表结构// firstWorkInProgressHook表示这个链表头部指针var renderedWork = currentlyRenderingFiber$1;renderedWork.memoizedState = firstWorkInProgressHook;复制代码

useEffect

当你认真看完前面useState的实现过程,你会发现这样一个过程:

  • 生成hook
  • 用WorkInProgressHook链表保存
  • 最终将WorkInProgressHook保存为当前fiber的memoizedState

如果你对react的fiber有一定了解的话对memoizedState一定不会陌生,我们在class组件中的state最终也是保存到fiber的memoizedState

手动分割线-----------------------------------------

useEffect发生了什么

  • 生成effect
var effect = {  tag: tag,  create: useEffect的第一个参数传入的函数,  destroy: destroy,  deps: useEffect的第二个参数更新依赖,  // Circular  next: null};复制代码
  • 用componentUpdateQueue链表保存
  • 最终将componentUpdateQueue保存为当前fiber的updateQueue,
ReactCurrentDispatcher$1.current = ContextOnlyDispatcher;renderedWork.updateQueue = componentUpdateQueue;复制代码

如果你对fiber的调用过程一定了解的话updateQueue你也不会陌生,这个地方存有react diff出来的副作用,用于在commit阶段执行。

总结:

和useState有相同的实现过程。

不同点:

  • useState需要处理影响页面更新的数据,而在react fiber中将此类数据全部保存到memoizedState,对memoizedState的使用发送在react fiber的reconciler阶段
  • useEffect的目的是在组件mounted后进行作用,fiber中将此类操作作为effect维护这一个链表,effect的触发过程发生在react fiber的commit阶段

useCallback

和useState的生成过程基本类似,不过生成的hook对象有点区别

var hook = {    memoizedState: [传入的callback,传人的更新依赖],    ...};复制代码

useCallback算是我个人比较喜欢的一个功能

class Stage extends React.Component{  handleClick = () => {    console.log('what are you 弄啥呢?')  }  render () {    return (      <>        
弄你
this.handleClick()}>弄你
) }}复制代码

第一种写法保证执行render时属性onClick的值都相同;而第二种写法每次执行render都会生成一个新的函数,所以在diff时onClick每次都不同。

useCallback就用来帮助我们在functional组件中实现了第一种性能更优的方式

useMemo

和useState的生成过程基本类似,不过生成的hook对象有点区别

var hook = {    memoizedState: [计算过程的结果,传人的更新依赖],    ...复制代码

useMemo同样是我个人比较喜欢的一个功能,在这之前一直希望有官方的支持,简单说就是对一个计算过程的结果进行缓存。使用过vue的同学可以把它想象成vue的computed属性()。从这一点上看vue领先react好几年~~~~哈哈哈哈哈哈~~~~

hook的第二个参数

官方提供的hook api很多,其中有几个可以接受第二个参数(不传,或者是一个数组)。

作用:在某个依赖项改变时重新操作第一个参数

如果你使用过React.PureComponent就会知道,他通过对新旧props做一个浅比较来判断是否需要更新组件。这第二个参数的原理同样如此。

// 首先对deps做是否为空的判断if (nextDeps !== null) {  var prevDeps = prevState[1];  // 然后比较新旧deps  if (areHookInputsEqual(nextDeps, prevDeps)) {    return prevState[0];  }}// 比较新旧depsfunction areHookInputsEqual(nextDeps, prevDeps) {  if (prevDeps === null) {    return false;  }  // 对传入的数组成员做浅比较  for (var i = 0; i < prevDeps.length && i < nextDeps.length; i++) {    if (is(nextDeps[i], prevDeps[i])) {      continue;    }   return false;  }    return true;}复制代码

其它hook

剩下的几个hook API大家有兴趣可以自行研读源码,实现过程基本和上面几个相同的思路

总结

最后想说的是没有黑魔法,如果你react的更新机制、fiber的过程、以及函数的特性有清晰的认识,理解hook也是很容易的。

这篇文章本质上也是一片源码解读类型的,但是很少涉及到一些具体实现。通过开篇的一个探索版本模糊认识hook的设计思路。所有这一切都是建立在reactv16版本fiber的优秀设计上。

转载地址:http://kqsza.baihongyu.com/

你可能感兴趣的文章
vagrant设置虚拟机的名字
查看>>
ethers.js-6-Low-Level API
查看>>
poj1637
查看>>
C++单链表基本算法实现
查看>>
LeetCode 345. Reverse Vowels of a String
查看>>
VirtualBox中安装Android-x86详解
查看>>
1023 Have Fun with Numbers
查看>>
SQL查询语句
查看>>
2_C语言中的数据类型 (四)整数与无符号数
查看>>
算法导论 6.2-5
查看>>
单例集合的体系
查看>>
【笔记】《深入浅出MFC》第6章 MFC程序的生死因果
查看>>
git 基础 拾遗
查看>>
Python线程学习
查看>>
js 内存小记
查看>>
Hibernate中Criteria的完整用法
查看>>
putc,fputc,和putchar
查看>>
web安全问题总结
查看>>
Elasticsearch match_phrase用法
查看>>
递归调用顺序问题
查看>>