深度了解vue.js中hooks的相关知识


Posted in Javascript onJune 14, 2019

背景

最近研究了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.js中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

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的使用场景:

  • 多次re-render过程中保存最新的值
  • 该值不需要响应式处理
  • 不污染其他作用域

useEffect

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

useMounted(function(){})
export function useMounted(fn) {
useEffect(fn, [])
}

useEffect依赖传[]时,副作用函数只在mounted阶段调用。

useDestroyed

useDestroyed(function(){})
export function useDestroyed(fn) {
useEffect(() => fn, [])
}

useEffect依赖传[]且存在返回函数,返回函数会被当作清理逻辑在destroyed调用。

useUpdated

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阶段。

useWatch

export function useWatch(getter, cb, options) {
ensureCurrentInstance()
if (isMounting) {
currentInstance.$watch(getter, cb, options)
}
}

使用方式同$watch。这里加了一个是否初次渲染判断,防止re-render产生多余Watcher观察者。

useComputed

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阶段去求值。

hooks

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存在什么问题?

在实际应用过程中发现,hooks的出现确实能解决mixin带来的诸多问题,同时也能更加抽象化的开发组件。但与此同时也带来了更高的门槛,比如useEffect在使用时一定要对依赖忠诚,否则引起render的死循环也是分分钟的事情。
与react-hooks相比,vue可以借鉴函数抽象及复用的能力,同时也可以发挥自身响应式追踪的优势。我们可以看尤在与react-hooks对比中给出的看法:

整体上更符合 JavaScript 的直觉;
不受调用顺序的限制,可以有条件地被调用;
不会在后续更新时不断产生大量的内联函数而影响引擎优化或是导致 GC 压力;
不需要总是使用 useCallback 来缓存传给子组件的回调以防止过度更新;
不需要担心传了错误的依赖数组给 useEffect/useMemo/useCallback 从而导致回调中使用了过期的值 —— Vue 的依赖追踪是全自动的。

感受

为了能够在vue3.0发布后更快的上手新特性,便研读了一下hooks相关的源码,发现比想象中收获的要多,而且与新发布的RFC对比来看,恍然大悟。可惜工作原因,开发项目中很多依赖了vue-property-decorator来做ts适配,看来三版本出来后要大改了。

最后,hooks真香(逃)

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
用一段js程序来实现动画功能
Mar 06 Javascript
ASP 过滤数组重复数据函数(加强版)
May 31 Javascript
js渐变显示渐变消失示例代码
Aug 01 Javascript
Jquery显示、隐藏元素以及添加删除样式
Aug 09 Javascript
js中的时间转换—毫秒转换成日期时间的示例代码
Jan 26 Javascript
BooStrap对导航条的改造实践小结
Sep 21 Javascript
鼠标拖动改变DIV等网页元素的大小的实现方法
Jul 06 Javascript
vue过滤器用法实例分析
Mar 15 Javascript
详解Vue中的基本语法和常用指令
Jul 23 Javascript
详解微信小程序开发之formId使用(模板消息)
Aug 27 Javascript
在Vue中使用antv的示例代码
Jun 29 Javascript
element tree树形组件回显数据问题解决
Aug 14 Javascript
Vue 实现前进刷新后退不刷新的效果
Jun 14 #Javascript
在Vue中使用icon 字体图标的方法
Jun 14 #Javascript
移动端底部导航固定配合vue-router实现组件切换功能
Jun 13 #Javascript
后台使用freeMarker和前端使用vue的方法及遇到的问题
Jun 13 #Javascript
vue路由插件之vue-route
Jun 13 #Javascript
在JavaScript中使用严格模式(Strict Mode)
Jun 13 #Javascript
详解vuex之store源码简单解析
Jun 13 #Javascript
You might like
PHP通过正则表达式下载图片到本地的实现代码
2011/09/19 PHP
小谈php正则提取图片地址
2014/03/27 PHP
php获取flash尺寸详细数据的方法
2016/11/12 PHP
php数组遍历类与用法示例
2019/05/24 PHP
PHP上传图片到数据库并显示的实例代码
2019/12/20 PHP
用JavaScript实现仿Windows关机效果
2007/03/10 Javascript
才发现的超链接js导致网页中GIF动画停止的解决方法
2007/11/02 Javascript
jQuery 注意事项 与原因分析
2009/04/24 Javascript
Colortip基于jquery的信息提示框插件在IE6下面的显示问题修正方法
2010/12/06 Javascript
JS this作用域以及GET传输值过长的问题解决方法
2013/08/06 Javascript
json数据与字符串的相互转化示例
2013/09/18 Javascript
使用JS读秒使用示例
2013/09/21 Javascript
IE的事件传递-event.cancelBubble示例介绍
2014/01/12 Javascript
javaScript给元素添加多个class的简单实现
2016/07/20 Javascript
vue 使用eventBus实现同级组件的通讯
2018/03/02 Javascript
egg.js的基本使用和调用数据库的方法示例
2019/05/18 Javascript
解决Layui 表格自适应高度的问题
2019/11/15 Javascript
Vue实现boradcast和dispatch的示例
2020/11/13 Javascript
JS实现简易日历效果
2021/01/25 Javascript
[52:20]VP vs VG Supermajor小组赛 B组胜者组决赛 BO3 第一场 6.2
2018/06/03 DOTA
python3操作微信itchat实现发送图片
2018/02/24 Python
对Python 文件夹遍历和文件查找的实例讲解
2018/04/26 Python
pycharm设置注释颜色的方法
2018/05/23 Python
python直接获取API传递回来的参数方法
2018/12/17 Python
Django模板Templates使用方法详解
2019/07/19 Python
对python中基于tcp协议的通信(数据传输)实例讲解
2019/07/22 Python
jupyter notebook实现显示行号
2020/04/13 Python
Django DRF APIView源码运行流程详解
2020/08/17 Python
夏威夷咖啡公司:Hawaii Coffee Company
2019/09/19 全球购物
教学评估实施方案
2014/03/16 职场文书
有关九一八事变的演讲稿
2014/09/14 职场文书
2014年实习生工作总结
2014/11/27 职场文书
毕业生登记表班级意见
2015/06/05 职场文书
2016年“5.12”护士节致辞
2015/07/31 职场文书
2019学校请假条格式及范文
2019/06/25 职场文书
Python预测分词的实现
2021/06/18 Python