深度了解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 相关文章推荐
javascript 设置某DIV区域内的checkbox复选框
Nov 30 Javascript
jquery与prototype框架的详细对比
Nov 21 Javascript
jquery中使用循环下拉菜单示例代码
Sep 24 Javascript
14个有用的Jquery技巧分享
Jan 08 Javascript
H5移动端图片压缩上传开发流程
Nov 09 Javascript
angular4模块中给标签添加背景图的实现方法
Sep 15 Javascript
BACKBONE.JS 简单入门范例
Oct 17 Javascript
postman+json+springmvc测试批量添加实例
Mar 31 Javascript
layui select获取自定义属性方法
Aug 15 Javascript
详解JavaScript的内存空间、赋值和深浅拷贝
Apr 17 Javascript
微信小程序下拉加载和上拉刷新两种实现方法详解
Sep 05 Javascript
js+h5 canvas实现图片验证码
Oct 11 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/01/17 PHP
PHP文件上传、客户端和服务器端加限制、抓取错误信息、完整步骤解析
2017/01/12 PHP
php获取是星期几的的一些常用姿势
2019/12/15 PHP
自己开发Dojo的建议框架
2008/09/24 Javascript
javascript与CSS复习(二)
2010/06/29 Javascript
可自己添加html的伪弹出框实现代码
2013/09/08 Javascript
javascript:void(0)的作用示例介绍
2013/10/28 Javascript
js控制容器隐藏出现防止样式变化的两种方法
2014/04/25 Javascript
fixedBox固定div漂浮代码支持ie6以上大部分主流浏览器
2014/06/26 Javascript
jQuery添加/改变/移除CSS类及判断是否已经存在CSS
2014/08/20 Javascript
JavaScript 模块化编程(笔记)
2015/04/08 Javascript
javascript解三阶幻方(九宫格)
2015/04/22 Javascript
jquery实现列表上下移动功能
2016/02/25 Javascript
javascript实现列表切换效果
2016/05/02 Javascript
JS上传组件FileUpload自定义模板的使用方法
2016/05/10 Javascript
AngularJS extend用法详解及实例代码
2016/11/15 Javascript
jQuery实现参数自定义的文字跑马灯效果
2018/08/15 jQuery
Vue插件从封装到发布的完整步骤记录
2019/02/28 Javascript
构建一个JavaScript插件系统
2020/10/20 Javascript
详解JavaScript中的this指向问题
2021/02/05 Javascript
python3使用tkinter实现ui界面简单实例
2014/01/10 Python
PyTorch学习笔记之回归实战
2018/05/28 Python
Django渲染Markdown文章目录的方法示例
2019/01/02 Python
在django中图片上传的格式校验及大小方法
2019/07/28 Python
Python解析json时提示“string indices must be integers”问题解决方法
2019/07/31 Python
树莓派4B+opencv4+python 打开摄像头的实现方法
2019/10/18 Python
Python搭建HTTP服务过程图解
2019/12/14 Python
网络维护管理员的自我评价分享
2013/11/11 职场文书
写好自荐信的几个要点
2013/12/26 职场文书
农民工创业典型事迹
2014/01/25 职场文书
入股协议书
2014/04/14 职场文书
研发工程师岗位职责
2014/04/28 职场文书
省文明单位申报材料
2014/05/08 职场文书
学校感恩教育活动总结
2014/07/07 职场文书
校运会班级霸气口号
2015/12/24 职场文书
jdbc使用PreparedStatement批量插入数据的方法
2021/04/27 MySQL