vue-hooks学习笔记(含源码解读)

背景

hooks 百度翻译为钩子,不要把 Hooks 和 Vue 的 生命周期钩子(Lifecycle Hooks) 弄混了,Hooks 是 React 在 V16.7.0-alpha 版本中引入的,而且几天后 Vue 发布了其概念验证版本。
最近尤大发布了一个最新的npm包
Hook是react中得一项新功能提案,可以让开发人员在不编写Class的情况下使用状态和其他React功能。

定义

Hooks 主要是对模式的复用提供了一种更明确的思路 —— 避免重写组件本身,并允许有状态逻辑的不同部分能无缝地进行协同工作。

无状态函数式组件也非常受欢迎,但由于它们只能单纯地渲染,所以它们的用途仅限于展示任务。

Hooks 允许我们使用函数调用来定义组件的有状态逻辑,从而解决这些问题。这些函数调用变得更具有组合性、可复用性,并且允许我们在使用函数式组件的同时能够访问和维护状态。

为什么 Vue 中需要 Hooks?

Hooks 在 Vue 中必须提供什么。这似乎是一个不需要解决的问题。毕竟,类并不是 Vue 主要使用的模式。Vue 提供无状态函数式组件(如果需要它们),但为什么我们需要在函数式组件中携带状态呢?我们有 mixins 用于组合可以在多个组件复用的相同逻辑。问题解决了。

源码解读

h函数是createElement,生产一个VNode节点,即html DOM节点

createElement(也就是h)是vuejs里的一个函数。这个函数的作用就是生成一个 VNode节点,render 函数得到这个 VNode 节点之后,返回给 Vue.js 的 mount 函数,渲染成真实 DOM 节点,并挂载到根节点上。

1、useEffect 做了什么?

通过使用这个 Hook,通知 React 组件需要在渲染后执行什么操作。React 将记住传递的 function(把这个 function 成为 “effect”),并在执行 DOM 更新后调用这个 function。在这个效果中,主要的功能仍旧是设置 document.title,但是也可以执行数据获取,或者是调用其他的命令式的 API。

isMounting:是否为首次渲染

vue options上声明的几个本地变量:

  • _state:放置响应式数据
  • _refsStore:放置非响应式数据,且返回引用类型
  • _effectStore:存放副作用逻辑和清理逻辑
  • _computedStore:存放计算属性

vue-hooks暴露了一个hooks函数,开发者在入口Vue.use(hooks)之后,可以将内部逻辑混入所有的子组件。这样,我们就可以在SFC组件中使用hooks啦。

Hooks 和 mixins 之间的主要区别之一是 Hooks 实际上可以互相传值
_vnode初始化为null,在mounted阶段会被赋值为当前组件的v-dom
Hooks的思路是将一个组件拆分为较小的函数,而不是基于生命周期方法强制拆分。
seEffect提供了类似于 componentDidMount等生命周期钩子的功能 vue里面的mounted

hooks的方法 useData useState只能在hooks或者widthHooks中使用

hooks中的数据是根据useState出现的顺序来定的

借助withHooks,我们可以发挥hooks的作用,但牺牲来很多vue的特性,比如props,attrs,components等。

所谓的 “Effect” 对应的概念叫做 “side effect”。指的是状态改变时,相关的远端数据异步请求、事件绑定、改变 DOM 等;因为此类操作要么会引发其他组件的变化,要么在渲染周期中并不能立刻完成,所以就称其为“副作用”。

REACT
useEffect 能够在组件 render 之后进行不同类型的副作用。某些 effect 可能需要清理,因此可以在 effect 中返回一个 function:

参考文档
react
http://www.ptbird.cn/react-hoot-useEffect.html
vue
https://www.jianshu.com/p/f1e6597b19de 简书
http://www.sohu.com/a/321909448_500651 精度vue-hooks
https://juejin.im/post/5c7784d5f265da2de713629c 掘金
https://mp.weixin.qq.com/s/p2f3jsko91iGhrbtjgmt7g?utm_medium=hao.caibaojian.com&utm_source=hao.caibaojian.com 云前端
https://1byte.io/react-hooks/ react
https://blog.csdn.net/liuyingv8/article/details/84068075 react 30分钟

传统vue组件的缺点

  • 跨组件代码难以复用
  • 大组件,维护困难,颗粒度不好控制,细粒度划分时,组件嵌套存层次太深-影响性能
  • 类组件,this不可控,逻辑分散,不容易理解
  • mixins具有副作用,逻辑互相嵌套,数据来源不明,且不能互相消费

Q:
1,currentInstance是如何记录当前实例的
当前hooks文件的this就是当前的vue实例,将this赋值给currentInstance,然后将_effectStore等赋值给当前vue实例即可

2,currentInstance是如何成为proxy对象的 未知
currentInstance为当前vue实例,this即为proxy对象

3,hooks如何解决minix的问题的

  • 数据消费
    hooks能够方位当前vue实例的数据,可以相互消费
  • 数据来源
    hooks为我们手动调用的,所以数据来源为哪里就显然易见了

4,beforeMount里面,将currentInstance赋值了又置为空
赋值后,触发了render函数,注册了事件,置空当前变量

5,reder时,h的两次的用义
foo函数中的h函数是为了将jsx转为option对象,第二个h函数是为了option对象转为虚拟dom

6,id递增是为了每次获取新值?
vue-hooks将数据的获取与设置以id来代替,访问id即可得到映射的值,每个vue实例中的数据所对应的id是固定的

7, currentInstance.$on(‘hook:mounted’)的emit在哪里
vue源码支持,详见截图

8,hooks是否能够生命data或者computed,props
能访问,是否能定义还未知
不需要定义,直接在vue实例中的hooks钩子中return即可,template就能

mixins混入的问题是什么?vue-hooks是怎么解决其问题的
mixins 不能相互消费和使用状态,但 Hooks 可以。
hooks的用法?

9,什么时候会多次渲染

10,hooks钩子在哪个生命周期后面执行
beforeMount

11,不能放在条件或循环中

  • 对 useState() 的调用次数必须是一样的。
  • 与各状态对应的 useState()的调用顺序是一样的。

12,自定义hooks是什么,解决什么问题,怎么使用,会有什么问题?

13,不全局使用vue-hooks,只在相应的hooks文件import可以吗?
不行,withHooks依旧是返回一个vue component的配置项options,后续的hooks相关的属性都挂载在本地提供的options上。

14,不能申明相同的属性_state,会被覆盖

vue-hooks解决的问题

  • 实现了mixins的功能,并且解决了mixins的两个问题
    • 允许相互传递状态
    • 明确指出了逻辑来自哪里
      使用 Hooks,函数的返回值会记录消费的值。
  • vue-hooks是简化组件定义、复用状态逻辑的一种最新尝试,且结合 Vue 实例的特点提供了适用的 Hooks

hooks.js中的this为当前vue实例

react-hooks
hooks只能出现在函数作用域的顶级,不能出现在条件语句、循环语句中、嵌套函数中。

总结

withHooks 返回一个包装过的 Vue 实例配置

hooks 以 mixin 的形式发挥作用,注入两个生命周期

用模块局部变量 currentInstance 记录了 Hooks 生效的 Vue 实例

使用方式

withHooks为vue组件提供了hooks+VNode,使用方式如下:

withHooks 返回一个包装过的 Vue 实例配置

  • Vue式钩子
  • 在普通Vue组件中的用法

使用注意点
如果 useState 被包裹在 condition 中,那每次执行的下标就可能对不上,导致 useState 导出的 setter 更新错数据。

源码解读

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
let currentInstance = null //缓存当前的vue实例
let isMounting = false // render是否为首次渲染
let callIndex = 0 // 当前数据对应的索引,当往options上挂载属性时,使用callIndex作为唯一当索引标识

function ensureCurrentInstance() { // 是否有实例
if (!currentInstance) {
// 无效的挂钩调用:只能在传递给withhooks的函数中调用挂钩
throw new Error(
`invalid hooks call: hooks can only be called in a function passed to withHooks.`
)
}
}

export function useState(initial) {
ensureCurrentInstance()
const id = ++callIndex
const state = currentInstance.$data._state
// 通过闭包提供了一个更新器updater
const updater = newValue => {
state[id] = newValue
}
if (isMounting) {
currentInstance.$set(state, id, initial)
}
// 下一次的render过程,不会在重新使用$set初始化
return [state[id], updater]
}

// 负责副作用处理和清理逻辑
// 这里的副作用可以理解为可以根据依赖选择性的执行的操作
// 没必要每次re-render都执行,比如dom操作,网络请求等。
// 而这些操作可能会导致一些副作用,比如需要清除dom监听器,清空引用等等。
export function useEffect(rawEffect, deps) {
ensureCurrentInstance()
const id = ++callIndex

// 初始化时,声明了清理函数和副作用函数,并将effect的current指向当前的副作用逻辑,
// 在mounted阶段调用一次副作用函数,将返回值当成清理逻辑保存。
// 同时根据依赖来判断是否在updated阶段再次调用副作用函数。
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指向当前的副作用逻辑,在mounted阶段调用一次副作用函数
effect.current = rawEffect

currentInstance._effectStore[id] = {
effect,
cleanup,
deps
}
// \vue-dev\src\core\instance\lifecycle.js
currentInstance.$on('hook:mounted', effect)
currentInstance.$on('hook:destroyed', cleanup)
if (!deps || deps.length > 0) {
currentInstance.$on('hook:updated', effect)
}
} else {
// 非首次渲染时,会根据deps依赖来判断是否需要再次调用副作用函数,
// 需要再次执行时,先清除上一次render产生的副作用,
// 并将副作用函数的current指向最新的副作用逻辑,等待updated阶段调用。
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
}
}
}

// f初始化会返回一个携带current的引用,current指向初始化的值
export function useRef(initial) {
ensureCurrentInstance()
const id = ++callIndex
const { _refsStore: refs } = currentInstance
return isMounting ? (refs[id] = { current: initial }) : refs[id]
}

// 挂载一个响应式数据,但是没有提供更新器
export function useData(initial) {
const id = ++callIndex
const state = currentInstance.$data._state
if (isMounting) {
currentInstance.$set(state, id, initial)
}
return state[id]
}

// useEffect依赖传[]时,副作用函数只在mounted阶段调用。
export function useMounted(fn) {
useEffect(fn, [])
}

// useEffect依赖传[]且存在返回函数,返回函数会被当作清理逻辑在destroyed调用。
export function useDestroyed(fn) {
useEffect(() => fn, [])
}

// 如果deps固定不变,传入的useEffect会在mounted和updated阶段各执行一次,
// 这里借助useRef声明一个持久化的变量,来跳过mounted阶段。
export function useUpdated(fn, deps) {
const isMount = useRef(true)
useEffect(() => {
if (isMount.current) {
isMount.current = false
} else {
return fn()
}
}, deps)
}

export function useWatch(getter, cb, options) {
ensureCurrentInstance()
// 加了一个是否初次渲染判断,防止re-render产生多余Watcher观察者。
if (isMounting) {
currentInstance.$watch(getter, cb, options)
}
}

export function useComputed(getter) {
ensureCurrentInstance()
const id = ++callIndex
const store = currentInstance._computedStore
if (isMounting) {
// 先会计算一次依赖值并缓存
store[id] = getter()
// 调用$watch来观察依赖属性变化,并更新对应的缓存值。
currentInstance.$watch(getter, val => {
store[id] = val
}, { sync: true })
}
return store[id]
}

export function withHooks(render) {
return {
data() {
return {
_state: {} // 不能申明相同的属性_state,会被覆盖
}
},
created() {
this._effectStore = {}
this._refsStore = {}
this._computedStore = {}
},
render(h) {
callIndex = 0
currentInstance = this // 将当前的
isMounting = !this._vnode // _vnode初始化为null,在mounted阶段会被赋值为当前组件的v-dom,isMounting除了控制内部数据初始化的阶段外,还能防止重复re-render
const ret = render(h, this.$attrs, this.$props) // 传入了attrs和$props作为入参,且在渲染完当前组件后
currentInstance = null // 重置全局变量,以备渲染下个组件。
return ret
}
}
}

export function hooks (Vue) {
Vue.mixin({ // 换入两个生命周期
beforeCreate() {
const { hooks, data } = this.$options
if (hooks) {
this._effectStore = {}
this._refsStore = {}
this._computedStore = {}
this.$options.data = function () {
const ret = data ? data.call(this) : {}
ret._state = {} // 重置_state属性
return ret
}
}
},
beforeMount() {
const { hooks, render } = this.$options
if (hooks && render) {
this.$options.render = function(h) {
callIndex = 0
currentInstance = this
isMounting = !this._vnode // _vnode初始化为null,在mounted阶段会被赋值为当前组件的v-dom
const hookProps = hooks(this.$props) // 调用hooks方法,将return的字段放到实例本身上,即可得到响应数据
Object.assign(this._self, hookProps)
const ret = render.call(this, h)
currentInstance = null
return ret
}
}
}
})
}