最近研究了vue3.0的最新进展,发现变动很大,总体上看,vue也开始向hooks靠拢,而且vue作者本人也称vue3.0的特性吸取了很多hooks的灵感。所以趁着vue3.0未正式发布前,抓紧时间研究一下hooks相关的东西。
源码地址:vue-hooks-poc
为什么要用hooks?
首先从class-component/vue-options说起:
跨组件代码难以复用
大组件,维护困难,颗粒度不好控制,细粒度划分时,组件嵌套存层次太深-影响性能
类组件,this不可控,逻辑分散,不容易理解
mixins具有副作用,逻辑互相嵌套,数据来源不明,且不能互相消费
当一个模版依赖了很多mixin的时候,很容易出现数据来源不清或者命名冲突的问题,而且开发mixins的时候,逻辑及逻辑依赖的属性互相分散且mixin之间不可互相消费。这些都是开发中令人非常痛苦的点,因此,vue3.0中引入hooks相关的特性非常明智。
vue-hooks
在探究vue-hooks之前,先粗略的回顾一下vue的响应式系统:首先,vue组件初始化时会将挂载在data上的属性响应式处理(挂载依赖管理器),然后模版编译成v-dom的过程中,实例化一个Watcher观察者观察整个比对后的vnode,同时也会访问这些依赖的属性,触发依赖管理器收集依赖(与Watcher观察者建立关联)。当依赖的属性发生变化时,会通知对应的Watcher观察者重新求值(setter->notify->watcher->run),对应到模版中就是重新render(re-render)。
注意:vue内部默认将re-render过程放入微任务队列中,当前的render会在上一次render flush阶段求值。
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为vue组件提供了hooks+jsx的开发方式,使用方式如下:
export default withHooks((h)=>{ ... return <span></span> })
不难看出,withHooks依旧是返回一个vue component的配置项options,后续的hooks相关的属性都挂载在本地提供的options上。
首先,先分析一下vue-hooks需要用到的几个全局变量:
currentInstance:缓存当前的vue实例
isMounting:render是否为首次渲染
isMounting = !this._vnode
这里的_vnode与$vnode有很大的区别,$vnode代表父组件(vm._vnode.parent)
_vnode初始化为null,在mounted阶段会被赋值为当前组件的v-dom
isMounting除了控制内部数据初始化的阶段外,还能防止重复re-render。
callIndex:属性索引,当往options上挂载属性时,使用callIndex作为唯一当索引标识。
vue options上声明的几个本地变量:
_state:放置响应式数据
_refsStore:放置非响应式数据,且返回引用类型
_effectStore:存放副作用逻辑和清理逻辑
_computedStore:存放计算属性
最后,withHooks的回调函数,传入了attrs和$props作为入参,且在渲染完当前组件后,重置全局变量,以备渲染下个组件。
useData
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上挂载一个响应式数据。但缺陷是,它没有提供更新器,对外返回的数据发生变化时,有可能会丢失响应式监听。
useState
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初始化,而是会取上一次更新后的缓存值。
useRef