做者: 长峰javascript
初次听到 React Hooks,是在其刚发布的那几天,网上铺天盖地的文章介绍它。看字面意思是 「React 钩子」,就想固然地理解应该是修改 React 组件的钩子吧。React 延伸的概念很是多,高阶组件、函数式、Render Props、Context、等等。又来了一个新概念,前端开发已经够复杂了!近两年一直用 Vue,以为 React 相关的诸多特性,在 Vue 中也都有相似的解决方案,因此就没有当即去了解它。html
后来看到尤大在 Vue 3.0 最近进展 的视频中也提到了 Hooks API,并写了一个在 Vue 中使用 Hooks 的 POC。看来 Hooks 仍是挺重要的,因而立刻找到官方的 React Hooks 文档与发布会的视频 -- 又一轮的恶补。前端
看了相关资料,以为 Hooks 的应用前景仍是挺诱人的,解决了目前前端开发中的诸多痛点。不过 React Hooks 目前还在 alpha 阶段,不太完善,内置 Hooks 也不丰富。而 Vue 只有个 Hooks POC,Vue3.0 极可能会加上,但须要再等几个月。因此暂不建议在正式代码中使用。vue
本篇文章着重解释一下我对 Hooks 的理解,以及 Hooks API 在 Vue 中的源码实现。也说明一下 Hooks 是个中立的概念,能够在任何框架中使用,非 React 所独有 :)。java
在开始以前,咱们先复述一下 Hooks 会帮咱们解决什么问题。node
按照 Dan 的说法,React 项目的开发中遇到了如下几个痛点:react
固然 Vue 项目也是同样,这些问题其实也是相关联的。git
组件化的开发方式,咱们将页面拆分红不一样的组件,按自上而下的数据流,层层嵌套。代码结构的最小颗粒是组件。github
若是某些组件太大,咱们就继续拆分红更小的组件,而后在父组件中调用它。 若是多组件之间有很多通用逻辑,咱们就用 mixin 或 构建组件的继承体系。web
问题是,组件拆分,会使咱们很容易不当心就把组件的层级搞得很深,增长系统复杂度不说,性能也可能受到影响。而且,有些组件的交互逻辑确实比较复杂,拆分不得,系统长期迭代下来,累积的代码量很大,相关联的逻辑分散在组件不一样的生命周期中,难以维护。
跨组件逻辑复用更加棘手!mixin 是一个双刃剑(参考: mixin 是有害的);组件继承也不可取,虽然在强类型的面向对象语言(如:Java/C#)中,继承用着很好,但在 JavaScript 中总感到力不从心,也使得代码晦涩难懂;抽取 util 包也是一个惯用的作法,但,若是要抽取的公用逻辑需关联组件的本地状态呢,若是相关联的公用逻辑须要分散在组件的不一样生命周期中呢,就搞不定了!这时候,咱们每每就妥协了 -- 大组件/重复逻辑产生了。
类组件也让人爱恨交织,一直以来咱们也提倡用面向对象的方式抽象代码结构,在没有更好的解决方案以前,确实是个不错的选择。但我我的以为在 JavaScript 中,特别是在基于 React/Vue 组件体系的开发中,并不很合适。咱们常常须要奇技淫巧的手段使 JavaScript 类型可以支持 super、私有成员,并当心地处理函数中 this 的指向。得益于 JavaScript 的灵活性与强大的表现力,总可以找到正确书写代码的方式。问题是,这样的代码怎么维护呢,咱们但愿代码简洁明了,符合惯例写法,而非晦涩难懂、雷区遍及。单说 this,咱们知道 JavaScript 的基于静态做用域的,即从源码上看,就可以推断变量的做用域,但 this 倒是个例外,它是基于动态做用域的,就是说 this 的值是由调用者决定的。同一个方法,用不一样的方式调用,其 this 指向彻底不同,使得咱们不得不大量使用 bind,以保证 this 的指向。
关于 JavaScript 中的 this 用法,感兴趣的同窗,可参考:详解 this。
如何解决这些痛点呢 -- Hooks !
wikipedia 上关于 hooks 的定义是:
The term hooking covers a range of techniques used to alter or augment the behavior of an operating system, of applications, or of other software components by intercepting function calls or messages or events passed between software components. Code that handles such intercepted function calls, events or messages is called a hook.
翻译成中文含义是:Hooks 包含了一系列技术,用于改变或加强操做系统、应用程序、软件组件的行为。这些技术经过拦截软件运行过程当中的函数调用、消息、事件来实现。
就是说经过 Hooks,咱们可以后期改变或加强已有系统的运行时行为。那么,对应到 React/Vue 组件系统,则 Hooks 是能够改变或加强组件运行时行为的代码模块。
经过阅读 React Hooks 的技术文档,的确如此。React 提供了两个重要的内置 Hooks :
React 还提供了其它一些,组件特性相关的内置 Hooks,如 useContext、useReducer、useMemo、useRef 等等。将来应该会出现更多的内置 Hooks,切入组件运行时的方方面面。咱们也能够基于这些内置 Hooks,实现自定义 Hooks。
React 中强调 Hooks 只能在函数式组件中使用。函数式组件本质上是一个单纯的渲染函数,无状态、数据来源于外部。那么如何给组件添加本地状态,以及各类生命周期相关的业务逻辑呢?答案是:经过 Hooks。React 团队但愿将来「函数式组件 + Hooks」成为开发组件的主要方式,那么 Hooks 应该有能力侵入组件生命周期的每一个环节,以便为组件添加状态与行为。虽然目前 React 提供的 Hooks 还不够丰富,后续会逐渐完善。
综上所述,咱们发现,Hooks 可使咱们模块化开发的粒度更细,更函数式。组件的功能变成了由 Hooks 一点点地装配起来。这样的特性,也解决了上面提到的4个痛点:代码复用、大组件、组件树过深、类组件问题。
关于 React Hooks 的背景及诸多示例,请参考:Introducing Hooks
对于 Vue ,除了 useState、useEffect、useRef 与 React Hooks API 一致外,还能够实现 useComputed、useMounted、useUpdated、useWatch 等内置 Hooks,以便可以更细致地为组件添加功能。
这里分析一下 尤大的Hooks POC of Vue 的源码实现,以便加深对 Hooks API 的理解。
咱们知道 React Hooks 只能在函数式组件中使用,Vue 中也要这样定义。
withHooks 用于包装一个 Vue 版的「函数式组件」,在这个函数式组件中,您可使用 Hooks API。
withHooks 使用示例:
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 包装了一个函数式组件(渲染函数),经过 Hooks 为组件添加了一个本地状态 data,及一个计算属性 double。
注意:代码中的 useData 与 useState 相似,下文会解释。
withHooks 实现细节:
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
}
}
}
复制代码
代码中:
withHooks 为组件添加了一个私有本地状态 _state,用于存储 useState、useData 所关联的状态值。
在 created 中,为组件注入了一些支持 Hooks ( useEffect、useRef、useComputed ) 所须要的存储类对象。
重点是代码中的 render 函数:
useState 用于为组件添加一个响应式的本地状态,及该状态相关的更新器。
方法签名为:
const [state, setState] = useState(initialState);
setState 用于更新状态:
setState(newState);
useState 使用示例:
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 为组件添加了一个本地状态 count 与更新状态值用的函数 setCount。
useState 实现细节:
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]
}
复制代码
以上代码,很清晰地描述了 useState 是在组件中建立了一个本地响应式状态,并生成了一个状态更新器。
须要注意的是:
useEffect 用于添加组件状态更新后,须要执行的反作用逻辑。
方法签名:
void useEffect(rawEffect, deps)
useEffect 指定的反作用逻辑,会在组件挂载后执行一次、在每次组件渲染后根据指定的依赖有选择地执行、并在组件卸载时执行清理逻辑(若是指定了的话)。
调用示例 1:
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) } }, "+" )
])
})
复制代码
代码中,经过 useEffect 使每当 count 的状态值变化时,都会重置 document.title。
注意:这里没有指定 useEffect 的第二个参数 deps,表示只要组件从新渲染都会执行 useEffect 指定的逻辑,不限制必须是 count 变化时。
useEffect 详细的参数说明,请参考:Using the Effect Hook
调用示例 2:
import { withHooks, useState, useEffect } from "vue-hooks"
const Foo = withHooks(h => {
const [width, setWidth] = useState(window.innerWidth)
const handleResize = () => {
setWidth(window.innerWidth)
};
useEffect(() => {
window.addEventListener("resize", handleResize)
return () => {
window.removeEventListener("resize", handleResize)
}
}, [])
return h("div", [
h("div", `window width is: ${width}`)
])
})
复制代码
代码中,经过 useEffect 控制在窗口改变时从新获取其宽度。
useEffect 第一个参数的返回值,若是是函数的话,则定义其为清理逻辑。清理逻辑会在组件须要从新执行 useEffect 逻辑以前,或组件被销毁时执行。
这里在 useEffect 逻辑中,为 window 对象添加了 resize 事件,那么就须要在组件销毁时或须要从新执行该反作用逻辑时,先把 resize 事件注销掉,以免没必要要的事件处理。
注意,这里 useEffect 的第二个参数的值是 [],代表无依赖项,反作用逻辑只在组件 mounted 时执行一次,这样处理也符合这里的上下文场景。
useEffect 实现细节:
export function useEffect(rawEffect, 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
}
}
}
复制代码
能够看到,useEffect 的实现比较精巧,涉及到了组件的三个生命周期:mounted、updated、destroyed,反作用逻辑的执行细节由参数 deps 控制:
经过参数,咱们能够为 useEffect 指定 3 种信息:
其中,清理逻辑,会在 2 种状况下执行:
至关于为组件添加一个本地变量(非组件状态)。
方法签名:
const refContainer = useRef(initialValue)
useRef 实现细节:
export function useRef(initial) {
ensureCurrentInstance()
const id = ++callIndex
const { _refsStore: refs } = currentInstance
return isMounting ?
(refs[id] = {
current: initial
}) :
refs[id]
}
复制代码
代码中,useRef 指定的初始值,连同组件自己的 refs 定义,被存储到了内部对象 _refsStore 中。在组件的渲染函数中,随时可拿到 ref 对象:refContainer,获取或修改其中的 current 属性。
useData 与 useState 相似,不一样的是,useData 不提供更新器。
useData 实现细节:
export function useData(initial) {
const id = ++callIndex
const state = currentInstance.$data._state
if (isMounting) {
currentInstance.$set(state, id, initial)
}
return state[id]
}
复制代码
添加须要在 mounted 事件中执行的逻辑。
useMounted 实现细节:
export function useMounted(fn) {
useEffect(fn, [])
}
复制代码
这个就比较简单了,上文中提到,若是 useEffect 的参数 deps 指定为空数组的话,fn 就不在 updated 后执行了 -- 即仅在 mounted 时执行一次.
添加须要在 destroyed 阶段执行的逻辑。
useDestroyed 实现细节:
export function useDestroyed(fn) {
useEffect(() => fn, [])
}
复制代码
上文中提到 useEffect 第一个参数的返回值,若是是函数的话,会在 destroyed 阶段做为清理逻辑执行。
这里,经过设置参数 deps 的值为空数组,并把 fn 指定为 useEffect 的反作用逻辑的返回值,避免了 fn 在组件更新时被执行,使 fn 仅在 destroyed 阶段执行。
添加须要在组件更新后执行的逻辑。
useUpdated 实现细节:
export function useUpdated(fn, deps) {
const isMount = useRef(true) // 经过 useRef 生成一个标识符。
useEffect(() => {
if (isMount.current) {
isMount.current = false // 跳过 mounted.
} else {
return fn()
}
}, deps)
}
复制代码
也是经过 useEffect 实现,经过 useRef 声明一个标志变量,避免 useEffect 的反作用逻辑在 mounted 中执行。
为组件添加 watch.
useWatch 实现细节:
export function useWatch(getter, cb, options) {
ensureCurrentInstance()
if (isMounting) {
currentInstance.$watch(getter, cb, options)
}
}
复制代码
直接经过组件实例的 $watch 方法实现。
为组件添加 computed 属性。
useComputed 实现细节:
export function useComputed(getter) {
ensureCurrentInstance()
const id = ++callIndex
const store = currentInstance._computedStore
if (isMounting) {
store[id] = getter()
currentInstance.$watch(
getter,
val => { store[id] = val },
{ sync: true }
)
}
return store[id]
}
复制代码
这里把计算属性的值存储在了内部对象 _computedStore 中。本质上,也是经过组件实例的 $watch 实现。
请参考:POC of vue-hooks
熟悉了 Hooks 出现的背景、Hooks 定义、以及在 React/Vue 中的实现后,基本上能够得出如下结论:
原文连接: tech.meicai.cn/detail/82, 也可微信搜索小程序「美菜产品技术团队」,干货满满且每周更新,想学习技术的你不要错过哦。