背景
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
}
}
}
})
}