背景
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 更新错数据。
源码解读
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
}
}
}
})
}