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 更新錯數據。

源碼解讀

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM