一切皆Hooks

Vue3 的composition api是一次巨大的更新,它几乎完全改变了Vue组件的编写模式,响应式体系更加方便与智能,同时与其他第三方库的配合也更加完美,因此我建议在所有的Vue3项目中,都只使用setup模式来编写组件。

即使只是简单地把Vue2组件中的选项式api换成setup模式,也能从这个过程中整理出许多可以复用的逻辑,这种逻辑抽离的快感会让你完全唾弃选项式API,且此后对于任何一处重复次数超过两次的代码你都会无可救药地想要把它拆成一个新的hooks,并且一遍遍地诅咒万恶的mixins被扫进历史的垃圾桶,这,就是全新的composition api。

如何给Hook下一个定义呢?其实官方并没有用hook这个说法,而是统称为composable,意为组合,把许多响应式变量组合起来并暴露给其他组件或者composable使用,就可以称为hook函数,而在Vue3中,几乎所有东西都可以使用hooks实现。

组件即Hooks

这很容易理解,因为setup模式下所有的响应式变量都可以被抽离到单独的文件中,即使是props也可以使用toRefs将其变为响应式变量导出。因此理论上,所有的组件都可以写成一个template.vue+一个useXXX.ts文件,不过这么做显然有点矫枉过正,在编写hook的过程中,尽量只抽离与props无关的变量,例如纯函数组件,就明显不需要写成hook形式。hook适用于表示纯粹的状态,例如一个撤销重做的记录栈,hook专注于给出值,而组件只负责展示和调用hook暴露出来的方法。如果你想对hook的创建和用法有一个深入的认知,建议看看这个项目 @vue/use,里面有着许许多多的hooks用法,多到令人发指。

指令也可以是Hooks

得益于Vue提供了生命周期钩子函数,在大部分情况下,hook可以用来代替指令。

一直以来,编写Vue的自定义指令都有一个非常麻烦的地方,那就是如果需要在mount期间监听某个事件,并且希望在beforeUnmount的时候取消监听时,这个指令的写法会变的十分复杂:

typescript
const clickOutside: Directive={
	mounted:(el) => {
		const fn = (e)=>{
			// click outside
		}
		window.addEventListener('click', fn)
	},
	beforeUnmount: ()=> {
		// 没有办法直接在这里取消监听事件,必须通过外部变量或者dom元素来传递监听器
		window.removeListener('click', fn)
	}
}

但是如果使用hook来做就很方便:

typescript
const useClickOutside = (el:Ref<HtmlElement|undefined>) => {
	onMounted(()=>{
		const fn= () => {
			// click outside
		};
		window.addEventListener('click', fn);
		onBeforeUnmount(()=>{
			window.removeListener('click', fn)
		})
	})
}

得益于Vue3把生命周期做成了独立的钩子函数,使得组件的生命周期也能拆分成独立的逻辑

Hooks > Stores

使用pinia或者Vuex这样的第三方状态库也可以实现全局响应式变量的效果,不过对我而言,使用这些库的唯一好处是可以在开发时使用vue devtools实时看到和修改store里的state,其他方面,createStore完全可以被hook取代。hook作为一个单纯的函数,不需要在main.ts里引入文件,初始化也更简单,同样支持循环引用,并且,它还可以完全利用到Vue3响应式变量的大部分特性,例如readonly,对于typescript的支持也更友好:

typescript
// pinia
const store = createPinia({
	state:()=>({
		visible:false,
	}),
	actions:{
		// 无法保证调用者只使用show来修改visible的值
		show(){
			this.visible = false;
		}
	},
})

// hooks
const useVisible = ()=>{
	const visible = ref(false)
	const show = ()=>{
		visible.value = true
	}
	return {
		// 使用readonly确保暴露出的变量不会被意外修改
		visible: readonly(visible),
		show
	}
}

store的另一个优点是它能与vue devtools做良好的集成,方便开发使用,不过在一些小项目中,使用自定义hooks也能完全满足需求。

需要注意的是,每一个store都是单例的,而hook只是一个普通的函数,所以如果要想实现全局hook,需要使用闭包或者直接在全局作用域导出,不过我并不推荐后者,因为那会导致意外的副作用和tree-shaking失效,并且在不支持顶级await的情况下,编写某些异步操作会变得比较丑陋。

typescript
// 使用闭包确保每次调用useVisible得到的变量为同一份
export const useVisible = (()=>{
	const visible = ref(false)
	const show = ()=>{
		visible.value = true
	}
	return () => {
		visible: readonly(visible),
		show
	}
})()

// 或者直接全局导出,不建议
const visible = ref(false)
const show = ()=>{
		visible.value = true
}
export {
		visible: readonly(visible),
		show
	}

不同于React Hooks

由于我并没有太多使用React工程化的经验,因此我只能粗浅地说一下我的二者区别的理解。在我看来,React Hook是一套依托于React渲染管线而存在的状态抽象集合,它必须依赖于React存在,否则便会失去意义,失去“状态”,并且没有生命周期的概念;而Vue则更像是基于vue/reactivity 搭建了一套响应式变量体系,再基于这个体系建立了Vue的render系统,刚好与React相反。正如Vue的官方文档中写的那样,基于vue/reactivity,我们甚至可以重新写一套组件渲染系统。除此之外,我们还可以单独使用reactivity库,而不仅仅只用在界面渲染上,例如在node中搭配socket与浏览器同步信息等等,比起React,Vue的响应式变量有更广泛的使用场景。