最近研究了vue3.0的最新进展,发现变更很大,整体上看,vue也开始向hooks靠拢,并且vue做者本人也称vue3.0的特性吸收了不少hooks的灵感。因此趁着vue3.0未正式发布前,抓紧时间研究一下hooks相关的东西。css
源码地址:vue-hooks-pochtml
首先从class-component/vue-options提及:vue
当一个模版依赖了不少mixin的时候,很容易出现数据来源不清或者命名冲突的问题,并且开发mixins的时候,逻辑及逻辑依赖的属性互相分散且mixin之间不可互相消费。这些都是开发中使人很是痛苦的点,所以,vue3.0中引入hooks相关的特性很是明智。node
在探究vue-hooks
以前,先粗略的回顾一下vue
的响应式系统:首先,vue
组件初始化时会将挂载在data
上的属性响应式处理(挂载依赖管理器),而后模版编译成v-dom的过程当中,实例化一个Watcher
观察者观察整个比对后的vnode
,同时也会访问这些依赖的属性,触发依赖管理器收集依赖(与Watcher
观察者创建关联)。当依赖的属性发生变化时,会通知对应的Watcher
观察者从新求值(setter->notify->watcher->run),对应到模版中就是从新render(re-render)。react
注意:vue内部默认将re-render过程放入微任务队列中,当前的render会在上一次render flush阶段求值。git
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
为vue组件提供了hooks
+jsx
的开发方式,使用方式以下:github
export default withHooks((h)=>{
...
return <span></span>
})
复制代码
不难看出,withHooks依旧是返回一个vue component的配置项options,后续的hooks相关的属性都挂载在本地提供的options上。数组
首先,先分析一下vue-hooks
须要用到的几个全局变量:缓存
isMounting = !this._vnode
复制代码
这里的
_vnode
与$vnode
有很大的区别,$vnode
表明父组件(vm._vnode.parent)bash
_vnode初始化为null,在mounted阶段会被赋值为当前组件的v-dom
isMounting除了控制内部数据初始化的阶段外,还能防止重复re-render。
vue options上声明的几个本地变量:
最后,withHooks的回调函数,传入了attrs
和$props
做为入参,且在渲染完当前组件后,重置全局变量,以备渲染下个组件。
const data = useData(initial)
复制代码
export function useData(initial) {
const id = ++callIndex
const state = currentInstance.$data._state
if (isMounting) {
currentInstance.$set(state, id, initial)
}
return state[id]
}
复制代码
咱们知道,想要响应式的监听一个数据的变化,在vue中须要通过一些处理,且场景比较受限。使用useData
声明变量的同时,也会在内部data._state上挂载一个响应式数据。但缺陷是,它没有提供更新器,对外返回的数据发生变化时,有可能会丢失响应式监听。
const [data, setData] = useState(initial)
复制代码
export function useState(initial) {
ensureCurrentInstance()
const id = ++callIndex
const state = currentInstance.$data._state
const updater = newValue => {
state[id] = newValue
}
if (isMounting) {
currentInstance.$set(state, id, initial)
}
return [state[id], updater]
}
复制代码
useState
是hooks
很是核心的API
之一,它在内部经过闭包提供了一个更新器updater
,使用updater
能够响应式更新数据,数据变动后会触发re-render,下一次的render过程,不会在从新使用$set初始化,而是会取上一次更新后的缓存值。
const data = useRef(initial) // data = {current: initial}
复制代码
export function useRef(initial) {
ensureCurrentInstance()
const id = ++callIndex
const { _refsStore: refs } = currentInstance
return isMounting ? (refs[id] = { current: initial }) : refs[id]
}
复制代码
使用useRef初始化会返回一个携带current的引用,current指向初始化的值。我在初次使用useRef的时候老是理解不了它的应用场景,但真正上手后仍是多少有了一些感觉。
好比有如下代码:
export default withHooks(h => {
const [count, setCount] = useState(0)
const num = useRef(count)
const log = () => {
let sum = count + 1
setCount(sum)
num.current = sum
console.log(count, num.current);
}
return (
<Button onClick={log}>{count}{num.current}</Button>
)
})
复制代码
点击按钮会将数值+1,同时打印对应的变量,输出结果为:
0 1
1 2
2 3
3 4
4 5
复制代码
能够看到,num.current永远都是最新的值,而count获取到的是上一次render的值。 其实,这里将num提高至全局做用域也能够实现相同的效果。 因此能够预见useRef的使用场景:
useEffect(function ()=>{
// 反作用逻辑
return ()=> {
// 清理逻辑
}
}, [deps])
复制代码
export function useEffect(rawEffect, deps) {
ensureCurrentInstance()
const id = ++callIndex
if (isMounting) {
const cleanup = () => {
const { current } = cleanup
if (current) {
current()
cleanup.current = null
}
}
const effect = function() {
const { current } = effect
if (current) {
cleanup.current = current.call(this)
effect.current = null
}
}
effect.current = rawEffect
currentInstance._effectStore[id] = {
effect,
cleanup,
deps
}
currentInstance.$on('hook:mounted', effect)
currentInstance.$on('hook:destroyed', cleanup)
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()
effect.current = rawEffect
}
}
}
复制代码
useEffect
一样是hooks
中很是重要的API
之一,它负责反作用处理和清理逻辑。这里的反作用能够理解为能够根据依赖选择性的执行的操做,不必每次re-render都执行,好比dom操做,网络请求等。而这些操做可能会致使一些反作用,好比须要清除dom监听器,清空引用等等。
先从执行顺序上看,初始化时,声明了清理函数和反作用函数,并将effect的current指向当前的反作用逻辑,在mounted阶段调用一次反作用函数,将返回值当成清理逻辑保存。同时根据依赖来判断是否在updated阶段再次调用反作用函数。
非首次渲染时,会根据deps依赖来判断是否须要再次调用反作用函数,须要再次执行时,先清除上一次render产生的反作用,并将反作用函数的current指向最新的反作用逻辑,等待updated阶段调用。
useMounted(function(){})
复制代码
export function useMounted(fn) {
useEffect(fn, [])
}
复制代码
useEffect依赖传[]
时,反作用函数只在mounted阶段调用。
useDestroyed(function(){})
复制代码
export function useDestroyed(fn) {
useEffect(() => fn, [])
}
复制代码
useEffect依赖传[]
且存在返回函数,返回函数会被看成清理逻辑在destroyed
调用。
useUpdated(fn, deps)
复制代码
export function useUpdated(fn, deps) {
const isMount = useRef(true)
useEffect(() => {
if (isMount.current) {
isMount.current = false
} else {
return fn()
}
}, deps)
}
复制代码
若是deps固定不变,传入的useEffect会在mounted和updated阶段各执行一次,这里借助useRef声明一个持久化的变量,来跳过mounted阶段。
export function useWatch(getter, cb, options) {
ensureCurrentInstance()
if (isMounting) {
currentInstance.$watch(getter, cb, options)
}
}
复制代码
使用方式同$watch。这里加了一个是否初次渲染判断,防止re-render产生多余Watcher观察者。
const data = useData({count:1})
const getCount = useComputed(()=>data.count)
复制代码
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]
}
复制代码
useComputed首先会计算一次依赖值并缓存,调用$watch来观察依赖属性变化,并更新对应的缓存值。
实际上,vue底层对computed对处理要稍微复杂一些,在初始化computed时,采用lazy:true(异步)的方式来监听依赖变化,即依赖属性变化时不会马上求值,而是控制dirty变量变化;并将计算属性对应的key绑定到组件实例上,同时修改成访问器属性,等到访问该计算属性的时候,再依据dirty来判断是否求值。
这里直接调用watch会在属性变化时,当即获取最新值,而不是等到render flush阶段去求值。
export function hooks (Vue) {
Vue.mixin({
beforeCreate() {
const { hooks, data } = this.$options
if (hooks) {
this._effectStore = {}
this._refsStore = {}
this._computedStore = {}
// 改写data函数,注入_state属性
this.$options.data = function () {
const ret = data ? data.call(this) : {}
ret._state = {}
return ret
}
}
},
beforeMount() {
const { hooks, render } = this.$options
if (hooks && render) {
// 改写组件的render函数
this.$options.render = function(h) {
callIndex = 0
currentInstance = this
isMounting = !this._vnode
// 默认传入props属性
const hookProps = hooks(this.$props)
// _self指示自己组件实例
Object.assign(this._self, hookProps)
const ret = render.call(this, h)
currentInstance = null
return ret
}
}
}
})
}
复制代码
借助withHooks
,咱们能够发挥hooks的做用,但牺牲来不少vue的特性,好比props,attrs,components等。
vue-hooks
暴露了一个hooks
函数,开发者在入口Vue.use(hooks)
以后,能够将内部逻辑混入全部的子组件。这样,咱们就能够在SFC
组件中使用hooks
啦。
为了便于理解,这里简单实现了一个功能,将动态计算元素节点尺寸封装成独立的hooks:
<template>
<section class="demo">
<p>{{resize}}</p>
</section>
</template>
<script>
import { hooks, useRef, useData, useState, useEffect, useMounted, useWatch } from '../hooks';
function useResize(el) {
const node = useRef(null);
const [resize, setResize] = useState({});
useEffect(
function() {
if (el) {
node.currnet = el instanceof Element ? el : document.querySelector(el);
} else {
node.currnet = document.body;
}
const Observer = new ResizeObserver(entries => {
entries.forEach(({ contentRect }) => {
setResize(contentRect);
});
});
Observer.observe(node.currnet);
return () => {
Observer.unobserve(node.currnet);
Observer.disconnect();
};
},
[]
);
return resize;
}
export default {
props: {
msg: String
},
// 这里和setup函数很接近了,都是接受props,最后返回依赖的属性
hooks(props) {
const data = useResize();
return {
resize: JSON.stringify(data)
};
}
};
</script>
<style>
html,
body {
height: 100%;
}
</style>
复制代码
使用效果是,元素尺寸变动时,将变动信息输出至文档中,同时在组件销毁时,注销resize监听器。
hooks返回的属性,会合并进组件的自身实例中,这样模版绑定的变量就能够引用了。
在实际应用过程当中发现,hooks
的出现确实能解决mixin带来的诸多问题,同时也能更加抽象化的开发组件。但与此同时也带来了更高的门槛,好比useEffect在使用时必定要对依赖忠诚,不然引发render的死循环也是分分钟的事情。
与react-hooks
相比,vue能够借鉴函数抽象及复用的能力,同时也能够发挥自身响应式追踪的优点。咱们能够看尤在与react-hooks
对比中给出的见解:
为了可以在vue3.0发布后更快的上手新特性,便研读了一下hooks相关的源码,发现比想象中收获的要多,并且与新发布的RFC对比来看,恍然大悟。惋惜工做缘由,开发项目中不少依赖了vue-property-decorator
来作ts适配,看来三版本出来后要大改了。
最后,hooks真香(逃)
文章内容若有错误,欢迎指出!
转载请注明出处!