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

背景

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

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真香(逃)

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

(0)

相关推荐

  • 后台使用freeMarker和前端使用vue的方法及遇到的问题

    一:freeMarker的使用 1:java后台使用freeMarker是通过Model,将值传给前端: 如: @Controller public class MobileNewsFreeMarkerController { @RequestMapping("page/test") public String Test(Model model,HttpServletRequest request){ //获取项目路径 String basePath = request.getSche

  • 移动端底部导航固定配合vue-router实现组件切换功能

    在我们平时练习或者实际项目中也好,我们常常遇到这么一个需求:移动端中的导航并不是在顶部也不是在底部,而是在最底部且是固定的,当我们点击该导航项时会切换到对应的组件. 相信对于很多朋友而言,这是一个很简单的需求,而且市面上有很多开源的组件库就可以实现,像比如说:cube-ui等!那么对于一个要是还在练习以及对第三方组件库不是很了解的朋友不妨看看我这篇,相信会对你有所收获的! 首先,在实现这个需求之前,我们先分析或者回想下和自己做过的demo中哪个类似,相信很多朋友立马就会想起来---tab栏切换,

  • 在Vue中使用icon 字体图标的方法

    1.使用线上的阿里iconfont图标库 1.打开 iconFont官网 选择自己喜欢的图标,并且添加购物车 2.点击购物车,添加至项目 3 生成链接 4在我们的vue项目中,找到index.html文件,引入css样式,记住这里要放上你的链接地址 5接下来我们就可以在任何组件地方使用我们的图标了,我这里就是用上面生成的三个图标其中的一个. 2但是考虑网络及用户体验 阿里iconfont下载本地使用 1 阿里iconfont图标直接下载到本地 2 在assets文件下创建iconfont文件夹

  • Vue 实现前进刷新后退不刷新的效果

    需求一: 在一个列表页中,第一次进入的时候,请求获取数据. 点击某个列表项,跳到详情页,再从详情页后退回到列表页时,不刷新. 也就是说从其他页面进到列表页,需要刷新获取数据,从详情页返回到列表页时不要刷新. 解决方案在 app.vue 设置: <keep-alive include="list"> <router-view/> </keep-alive> 假设列表页为 list.vue ,详情页为 detail.vue ,这两个都是子组件. 我们在

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

    背景 最近研究了vue3.0的最新进展,发现变动很大,总体上看,vue也开始向hooks靠拢,而且vue作者本人也称vue3.0的特性吸取了很多hooks的灵感.所以趁着vue3.0未正式发布前,抓紧时间研究一下hooks相关的东西. 源码地址:vue-hooks-poc 为什么要用hooks? 首先从class-component/vue-options说起: 跨组件代码难以复用 大组件,维护困难,颗粒度不好控制,细粒度划分时,组件嵌套存层次太深-影响性能 类组件,this不可控,逻辑分散,不

  • 深入理解vue.js中的v-if和v-show

    本文主要给大家介绍了关于vue.js中v-if和v-show的相关内容,分享出来供大家参考学习,下面来一起看看详细的介绍: 1.共同点 都是动态显示DOM元素 2.区别 (1)手段:v-if是动态的向DOM树内添加或者删除DOM元素:v-show是通过设置DOM元素的display样式属性控制显隐: (2)编译过程:v-if切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件:v-show只是简单的基于css切换: (3)编译条件:v-if是惰性的,如果初始条件为假,

  • 深入浅析Vue.js中 computed和methods不同机制

    在vue.js中,有methods和computed两种方式来动态当作方法来用的 1.首先最明显的不同 就是调用的时候,methods要加上() 2.我们可以使用 methods 来替代 computed,效果上两个都是一样的,但是 computed 是基于它的依赖缓存,只有相关依赖发生改变时才会重新取值. 而使用 methods ,在重新渲染的时候,函数总会重新调用执行 为了方便理解,先上一段源码 <!DOCTYPE html> <html> <head> <m

  • vue.js中created方法作用

    这是它的一个生命周期钩子函数,就是一个vue实例被生成后调用这个函数.一个vue实例被生成后还要绑定到某个html元素上,之后还要进行编译,然后再插入到document中.每一个阶段都会有一个钩子函数,方便开发者在不同阶段处理不同逻辑. 一般可以在created函数中调用ajax获取页面初始化所需的数据. 实例生命周期 每个 Vue 实例在被创建之前都要经过一系列的初始化过程.例如,实例需要配置数据观测(data observer).编译模版.挂载实例到 DOM ,然后在数据变化时更新 DOM

  • Vue.js中的高级面试题及答案

    Vue-loader 是 Webpack 的加载模块,它使我们可以用 Vue 文件格式编写单文件组件. 单文件组件文件有三个部分,(模板.脚本和样式). vue-loader 模块允许 webpack 使用单独的加载模块 (例如 SASS 或 SCSS 加载器) 提取和处理每个人部分.该设置使我们可以使用 Vue 文件无缝编写程序. vue-loader 模块还允许把静态资源视为模块依赖性,并允许使用 webpack 加载器进行处理. 而且还允许还开发过程中进行热重装. 2.prop 如何指定其

  • vue.js中使用微信扫一扫解决invalid signature问题(完美解决)

    1.点击按钮,实现微信扫一扫功能: <template> <a class="btn" @click="scan">扫一扫</a> </template> 2.使用config接口注入配置信息,wx.config调用方法如下: (其中appId,timestamp,nonceStr,signature必须从后台获取,传参当前网页的URL,不包含#及其后面部分,location.href.split('#')[0]获取)

  • Vue.js 中制作自定义选择组件的代码附演示demo

    定制 select 标签的设计非常困难.有时候,如果不使用样式化的 div 和自定义 JavaScript 的结合来构建自己的脚本,那是不可能的.在本文中,你将学习如何构建使用完全自定义 CSS 设置样式的 Vue.js 组件. Demo: https://codesandbox.io/s/custom-vuejs-select-component-8nqgd HTML <template> <div class="custom-select" :tabindex=&

  • Vue.js中使用Vuex实现组件数据共享案例

    当组件中没有关联关系时,需要实现数据的传递共享,可以使用Vuex 先不管图片 一.安装 在vue cli3中创建项目时勾选这个组件就可以了 或者手动安装 npm install store --save 二.使用 main.js store.js .vue文件 图片中的js文件中有 三部分 分别与图片上对应 1. state中存储数据 2. 而数据的修改需要先经过action的dispatch方法 (不需要异步获取的数据可以不经过这一步,如上图) 3. 然后经过matations的commit方

  • 如何在Vue.JS中使用图标组件

    原文链接:https://gist.github.com/Justineo/fb2ebe773009df80e80d625132350e30 本文对原文进行一次翻译,并从React开发者的角度简单地做了一些解读. 此文不包含字体图标和SVG sprite.仅在此讨论允许用户按需导入的图标系统. There are three major ways of exposing API of an icon component in Vue.js and each one of them has its

  • 在Vue.js中使用TypeScript的方法

    虽然 vue2.x 对TypeScript的支持还不是非常完善,但是从今年即将到来的3.0版本在GitHub上的仓库 vue-next看,为TS提供更好的官方支持应该也会是一个重要特性,那么,在迎接3.0之前,不妨先来看看目前版本二者的搭配食用方法吧~ 创建项目 虽然GitHub上有各种各样相关的Starter,但是使用 Vue CLI 应该是目前相对比较好的方式,在使用 vue create 创建新项目时,对 preset 选择 Manually select features 选项,之后添加

随机推荐