VUE内置 HOOKS钩子函数,HOOKS API的Vue实现
一、HOOKS
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
后来看到尤大在 Vue 3.0 最近进展 的视频中也提到了 Hooks API,并写了一个在 Vue 中使用 Hooks 的 POC。
可以把hooks当作更强、更实用的mixins。
二、使用步骤
1.withHooks
withHooks 用于包装一个 Vue 版的「函数式组件」,在这个函数式组件中,您可以使用 Hooks API。
使用
import { withHooks, useData, useComputed } from "vue-hooks"
const Foo = withHooks(h => {
const data = useData({
count: 0
})
const double = useComputed(() => data.count * 2)
return h('div', [
h('div', `count is ${data.count}`),
h('div', `double count is ${double}`),
h('button', { on: { click: () => {
data.count++
}}}, 'count++')
])
})
withHooks 实现细节
- callIndex,为 Hooks 相关的存储对象提供 key。这里每次渲染,都重置为 0,是为了能够根据调用次序匹配对应的 Hooks。这样处理也限制了 Hooks 只能在顶级代码中调用。
- currentInstance,结合 ensureCurrentInstance 函数,用于确保 Hooks 只能在函数式组件中使用。
- isMounting,用于标识组件的挂载状态
let currentInstance = null
let isMounting = false
let callIndex = 0
function ensureCurrentInstance() {
if (!currentInstance) {
throw new Error(
`invalid hooks call: hooks can only be called in a function passed to withHooks.`
)
}
}
export function withHooks(render) {
return {
data() {
return {
_state: {}
}
},
created() {
this._effectStore = {}
this._refsStore = {}
this._computedStore = {}
},
render(h) {
callIndex = 0
currentInstance = this
isMounting = !this._vnode
const ret = render(h, this.$attrs, this.$props)
currentInstance = null
return ret
}
}
}
2.useState
useState 用于为组件添加一个响应式的本地状态,及该状态相关的更新器。
方法签名为: const [state, setState] = useState(initialState);
setState 用于更新状态: setState(newState);
使用
import { withHooks, useState } from "vue-hooks"
const Foo = withHooks(h => {
const [count, setCount] = useState(0)
return h("div", [
h("span", `count is: ${count}`),
h("button", { on: { click: () => setCount(count + 1) } }, "+" )
])
})
useState 实现细节
- 函数 ensureCurrentInstance 是为了确保 useState 必须在 render 中执行,也就是限制了必须在函数式组件中执行。
- 以 callIndex 生成的自增 id 作为存储状态值的 key。说明 useState 需要依赖第一次渲染时的调用顺序来匹配过去的 state(每次渲染 callIndex 都要重置为0)。这也限制了 useState 必须在顶层代码中使用。
- 其它 hooks 也必须遵循以上两点。
export function useState(initial) {
ensureCurrentInstance()
const id = ++callIndex
// 获取组件实例的本地状态。
const state = currentInstance.$data._state
// 本地状态更新器,以自增id为键值,存储到本地状态中。
const updater = newValue => {
state[id] = newValue
}
if (isMounting) {
// 通过$set保证其是响应式状态。
currentInstance.$set(state, id, initial)
}
// 返回响应式状态与更新器。
return [state[id], updater]
}
2.useEffect
useEffect 用于添加组件状态更新后,需要执行的副作用逻辑。
useEffect 指定的副作用逻辑,会在组件挂载后执行一次、在每次组件渲染后根据指定的依赖有选择地执行、并在组件卸载时执行清理逻辑(如果指定了的话)。
方法签名为: void useEffect(rawEffect, deps)
使用
import { withHooks, useState, useEffect } from "vue-hooks"
const Foo = withHooks(h => {
const [count, setCount] = useState(0)
useEffect(() => {
document.title = "count is " + count
})
return h("div", [
h("span", `count is: ${count}`),
h("button", { on: { click: () => setCount(count + 1) } }, "+" )
])
})
useState 实现细节
- mounted 时,固定地执行一次。
- 如果 deps 未指定,则每次 updated 后都执行一次。
- 如果 deps 为空数组,则 updated 后不执行。
- 如果 deps 指定了依赖项,则当相应的依赖项的值改变时,执行一次。
ensureCurrentInstance()
const id = ++callIndex
if (isMounting) {
// 组件挂载前,重新包装「清理逻辑」与「副作用逻辑」。
const cleanup = () => {
const { current } = cleanup
if (current) {
current()
// 清理逻辑执行完,则重置回 null;
// 如果副作用逻辑二次执行,cleanup.current 会被重新赋值。
cleanup.current = null
}
}
const effect = () => {
const { current } = effect
if (current) {
// rawEffect 的返回值,如果是一个函数的话,则定义为 useEffect副作用 的清理函数。
cleanup.current = current()
// rawEffect 执行完,则重置为 null;
// 如果相关的 deps 发生变化,需要二次执行 rawEffect 时 effect.current 会被重新赋值。
effect.current = null
}
}
effect.current = rawEffect
// 在组件实例上,存储 useEffect 相关辅助成员。
currentInstance._effectStore[id] = {
effect,
cleanup,
deps
}
// 组件实例 mounted 时,执行 useEffect 逻辑。
currentInstance.$on('hook:mounted', effect)
// 组件实例 destroyed 时,执行 useEffect 相关清理逻辑。
currentInstance.$on('hook:destroyed', cleanup)
// 若未指定依赖项或存在明确的依赖项时,组件实例 updated 后,执行 useEffect 逻辑。
// 若指定依赖项为 [], 则 useEffect 只会在 mounted 时执行一次。
if (!deps || deps.length > 0) {
currentInstance.$on('hook:updated', effect)
}
} else {
const record = currentInstance._effectStore[id]
const { effect, cleanup, deps: prevDeps = [] } = record
record.deps = deps
if (!deps || deps.some((d, i) => d !== prevDeps[i])) {
// 若依赖的状态值有变动时,在副作用重新执行前,执行清理逻辑。
cleanup()
// useEffect 执行完毕后,会将 current 的属性置为 null. 这里为 effect.current 重新赋值,
// 是为了在 updated 后执行 rawEffect 逻辑。
effect.current = rawEffect
}
}
}