深度了解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 新浪的一个图片播放图片轮换效果代码
Jul 15 Javascript
让人印象深刻的10个jQuery手风琴效果应用
May 08 Javascript
JS仿百度搜索自动提示框匹配查询功能
Nov 21 Javascript
javascript函数中参数传递问题示例探讨
Jul 31 Javascript
javascript中arguments,callee,caller详解
Mar 16 Javascript
Bootstrap每天必学之响应式导航、轮播图
Apr 25 Javascript
老生常谈javascript变量的命名规范和注释
Sep 29 Javascript
jQuery生成假加载动画效果
Dec 01 Javascript
微信小程序 自定义对话框实例详解
Jan 20 Javascript
vue mint-ui 实现省市区街道4级联动示例(仿淘宝京东收货地址4级联动)
Oct 16 Javascript
详解基于DllPlugin和DllReferencePlugin的webpack构建优化
Jun 28 Javascript
Node.js Domain 模块实例详解
Mar 18 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
十大感人催泪爱情动漫 第一名至今不忍在看第二遍
2020/03/04 日漫
中篇:安装及配置PHP
2006/12/13 PHP
在php和MySql中计算时间差的方法详解
2015/03/27 PHP
PHP互换两个变量值的方法(不用第三变量)
2016/11/14 PHP
PJ Blog修改-禁止复制的代码和方法
2006/10/25 Javascript
在jQuery1.5中使用deferred对象 着放大镜看Promise
2011/03/12 Javascript
JavaScript XML和string相互转化实现代码
2011/07/04 Javascript
PHP配置文件php.ini中打开错误报告的设置方法
2015/01/09 PHP
JS实现仿新浪黄色经典滑动门效果代码
2015/09/27 Javascript
JS转换HTML转义符的方法
2016/08/24 Javascript
Bootstrap table 定制提示语的加载过程
2017/02/20 Javascript
微信小程序 navbar实例详解
2017/05/11 Javascript
在vue中封装可复用的组件方法
2018/03/01 Javascript
JavaScript轮播停留效果的实现思路
2018/05/24 Javascript
javascript对HTML字符转义与反转义
2018/12/13 Javascript
Vue使用watch监听一个对象中的属性的实现方法
2019/05/10 Javascript
详解ES6 export default 和 import语句中的解构赋值
2019/05/28 Javascript
layui 对table中的数据进行转义的实例
2019/09/12 Javascript
微信小程序实现身份证取景框拍摄
2020/09/09 Javascript
[48:39]Ti4主赛事胜者组第一天 EG vs NEWBEE 2
2014/07/19 DOTA
python实现统计代码行数的方法
2015/05/22 Python
python3新特性函数注释Function Annotations用法分析
2016/07/28 Python
Python 获得命令行参数的方法(推荐)
2018/01/24 Python
python判断设备是否联网的方法
2018/06/29 Python
python 将列表中的字符串连接成一个长路径的方法
2018/10/23 Python
详解Python文件修改的两种方式
2019/08/22 Python
Pyorch之numpy与torch之间相互转换方式
2019/12/31 Python
python3.8.3安装教程及环境配置的详细教程(64-bit)
2020/11/28 Python
深入理解css中vertical-align属性
2017/04/18 HTML / CSS
几个解决兼容IE6\7\8不支持html5标签的几个方法
2013/01/07 HTML / CSS
美国温暖商店:The Warming Store
2018/12/15 全球购物
英国最受信任的在线眼镜商之一:Fashion Eyewear
2019/10/31 全球购物
乡镇纠风工作实施方案
2014/03/22 职场文书
工作说明书格式
2014/07/29 职场文书
小学运动会报道稿
2015/07/22 职场文书
将MySQL的表数据全量导入clichhouse库中
2022/03/21 MySQL