一、Hooks的基本理念
1、理念的提出
Sebastian Markbåge 首先提出了Hooks的理念,由Team成员落地。此人主修心理学
2、Hoos的理念
-
Hoos践行了“代数效应”的思想:在函数式编程中把副作用从函数调用中剥离出去。
-
代数效应的本质就是在正常的程序流程中,允许我们停下来,去做另外一件事,做完之后,我们可以再从被打断的地方继续往下执行,而另外的那件事,可以是同步的也可以是异步的,它的执行过程与我们当前的流程无关,我们只关心它的结果。
-
而useState就是利用了代数效应的思想,函数组件不能拥有状态,也不需要管理状态,当通过useState定义一个状态的时候,相当于perform语法,终止程序,当React处理完这个状态的时候,再恢复程序。将副作用从函数组件中剥离。
二、什么是Hooks
-
React官方一直都提倡使用函数组件,但是有时候需要使用state或者其他一些功能时,只能使用类组件,因为函数组件没有实例,也没有生命周期,只有类组件有
-
Hooks是React16.8新增的特性,它可以让你在函数组件中使用state以及其他的React特性。
-
在React中,凡是以use开头的API都是Hooks
三、常用的Hook
1、useState
-
一个钩子,相当于可以让你“钩入”React的特性。
-
useState是一个允许在函数组件中添加state的Hook
用法:
const [count, setCount] = useState(initailState)
count是定义的变量,setCount是更改这个变量的方法,更改可以引起DOM的重新渲染。
initailState为初始值,可以是一个变量也可以是一个函数,这个函数需要返回一个变量。
注意:useState不能在条件语句中声明
2、useEffect
这是一个可以在函数组件中执行副作用的Hook
用法:
useEffect(fun, deps)
-
fun接收一个函数,这个函数可能在不同的生命周期被调用
-
deps如果为undefined,那么useEffect会在组件挂载后以及更新后被调用
-
deps如果为空数组[],那么useEffect只会在组件挂载后被调用
-
deps如果为包含state的数组,那么useEffect会在该state改变时被调用
四、useState简易版实现
let isMount = true // 是否是首次渲染
let workInProgressHook = null // 存储当前组件的hook链表
// 每一个函数组件对应一个fiber
const fiber = {
stateNode: App, // 对应组件本身
memoizedState: null, // 保存相应的hook数据,这是一个链表的头指针
// updateQueue: null, // 存放useEffect的effect
}
function mountWorkInProgressHook () {
const hook = {
memoizedState: null, // hook的数据存在这里
next: null, // 指向下一个hook
queue: { // 创建一个新的链表来更新队列,用来存放更新(setXXX())
pending: null // 最近一个等待执行的更新
},
}
// workInProgressHook 指向当前组件的Hook链表
if (!workInProgressHook) {
// 如果当前组件的Hook链表为空,那么就将刚刚新建的Hook作为Hook链表的第一个节点
fiber.memoizedState = workInProgressHook = hook
} else {
// 如果不为空,则将新建的hook添加到链表的下一个,即next指向
workInProgressHook = workInProgressHook.next = hook
}
return workInProgressHook
}
function useState (initialState) {
let hook
if (isMount) {
// 首次渲染
hook = mountWorkInProgressHook()
hook.memoizedState = initialState
} else {
hook = workInProgressHook
workInProgressHook = workInProgressHook.next
}
let baseState = hook.memoizedState // 上一次数据
if (hook.queue.pending) {
let firstUpdate = hook.queue.pending.next // 第一个update
// 遍历环装链表 计算新的状态
do {
let action = firstUpdate.action
action = typeof action === 'function' ? action(baseState) : action
baseState = action
firstUpdate = firstUpdate.next
} while (firstUpdate !== hook.queue.pending.next)
hook.queue.pending = null
}
hook.memoizedState = baseState // 更新hook的值
return [baseState, dispatchAction.bind(null, hook.queue)]
}
function dispatchAction (queue, action) {
// 为当前更新操作新建update对象
const update = {
action,
next: null
}
const pending = queue.pending
if (pending === null) {
// 如果更新队列为空,那么将当前的更新作为更新队列的第一个节点,并且指向自身,以保证是一个循环链表
update.next = update
} else {
// 如果队列不为空,那么就将当前的更新对象插入到列表的头部
update.next = pending.next
// 链表的尾结点指向最新的头结点,以保持为一个循环链表
pending.next = update
// update0 -> update0
// update0 -> update1 -> update0
// update0 -> update1 -> update2 -> update0
// queue.pending -> update2(最新的更新操作)
}
queue.pending = update
// schedule()
setTimeout(() => {
schedule()
}, 0) // 触发一次更新
}
function schedule () {
workInProgressHook = fiber.memoizedState
const app = fiber.stateNode() // 触发了组件的 render
isMount = false // 首次渲染之后就改成成update状态
return app
}
function App () {
// 如果多个useState的话,必须按照固定的顺序声明,不能写在条件语句中,因为条件会变化,会导致hook的顺序改变
const [num, setNum] = useState(0)
const [num1, setNum1] = useState(0)
console.log('isMount?', isMount)
console.log('num:', num)
console.log('num1:', num1)
return {
onClick () {
setNum(x => x + 1)
setNum1(num => num + 10)
}
}
}
window.app = schedule() // 首次渲染
useState实现原理:
- 首先调用App()函数,调用第一个useState方法,进入useState函数体。
- 因为是首次渲染,所以创建一个新的hook。
- 判断workInProgressHook(当前组件所有hook链表的头指针)是否为null,如果为null,则将新创建的hook作为第一个节点,此时workInProgressHook指向第一个hook,并且fiber的memoizedState也指向第一个hook。
- 然后进行数据更新(其实源码中并不会在这里进行状态更新,源码中会分为mountState和updateState)。
- 首次使用useState,hook中的queue队列还是空的,所以不做数据更新。
- 返回一个数组,第一项是数据值,第二项是更新该数据的action。
- 调用第二个useState方法,同样创建一个新的hook。
- 但此时workInProgressHook不为null,它指向的是上一个hook,所以,将workInProgressHook的next指针指向新的hook,也就是上一个hook的next指向新的hook,然后将workInProgressHook也就是头指针指向新的hook。
- 同样不需要更新数据,并且返回[baseState, dispatchAction.bind(null, hook.queue)]。
- 此时将isMount改为false。
上面的过程为函数组件挂载时发生的事
- 此时调用app.onClick(),触发了第一个hook的dispatchAction,其中的action是新的值。
- 进入dispatchAction函数,首先创建一个update对象,其中包含action和next指向。
- 然后判断这个hook的pending,也就是更新操作链表是不是空的,如果是空的,那么就将这个update对象指向自己,形成一个循环链表,并且让queue.pending(头指针,指向当前执行的更新操作)指向这个update。
- 将更新操作添加到链表后,执行一次组件渲染,执行schedule()。
- 在schedule中,将fiber中的链表指针指向workInProgressHook,此时为第一个hook,然后执行App()函数,也就是fiber中的指针指向的永远是第一个hook。
- 在App函数中,执行第一个useState,此时isMount为false,所以为更新数据。
- 将hook指向workInProgressHook也就是第一个hook,(所以这里一定不能将useState放在条件语句中,如果这里第一个执行的不是第一个hook,那么数据将会出错)。
- 然后workInProgressHook指向下一个hook。
- 开始更新数据,将当前hook中的数据给baseState保存,然后判断该数据是否需要更新,即判断hook的链表中是否有update,如果没有,则还将baseState赋值给hook。
- 如果链表中有操作的update,则让保存第一个操作指针给firstUpdate,接下来循环这些操作。
- 首先拿到操作需要更新的action,这个action支持单纯的值或者一个函数。
- 然后将新值赋给baseState,然后查看下一个指向的操作是不是自己,如果是则结束循环,如果还有操作,那么则进行值的覆盖。
- 更新完值后,对操作的循环列表清空。
- 最后更新hook中的数据,并将该数据返回。
- 第二个hook同理,也将数据返回给到App组件中的变量并且按需渲染DOM。
挂载的时候第一次执行:
调用app.onClick():
onClick中进行了两次set,在这个简易版的useState中会进行两次渲染,而React中进行多次set,会整合在一起执行一次渲染,提升性能。
完~