精讀vue-hooks

原文地址:juejin.im/post/5d0243…css

原文做者:微笑向暖_Tinihtml

背景

最近研究了vue3.0的最新進展,發現變更很大,整體上看,vue也開始向hooks靠攏,並且vue做者本人也稱vue3.0的特性吸收了不少hooks的靈感。因此趁着vue3.0未正式發佈前,抓緊時間研究一下hooks相關的東西。vue

源碼地址:vue-hooks-pocnode

爲何要用hooks?

首先從class-component/vue-options提及:react

  • 跨組件代碼難以複用
  • 大組件,維護困難,顆粒度很差控制,細粒度劃分時,組件嵌套存層次太深-影響性能
  • 類組件,this不可控,邏輯分散,不容易理解
  • mixins具備反作用,邏輯互相嵌套,數據來源不明,且不能互相消費

當一個模版依賴了不少mixin的時候,很容易出現數據來源不清或者命名衝突的問題,並且開發mixins的時候,邏輯及邏輯依賴的屬性互相分散且mixin之間不可互相消費。這些都是開發中使人很是痛苦的點,所以,vue3.0中引入hooks相關的特性很是明智。git

vue-hooks

image

在探究vue-hooks以前,先粗略的回顧一下vue的響應式系統:首先,vue組件初始化時會將掛載在data上的屬性響應式處理(掛載依賴管理器),而後模版編譯成v-dom的過程當中,實例化一個Watcher觀察者觀察整個比對後的vnode,同時也會訪問這些依賴的屬性,觸發依賴管理器收集依賴(與Watcher觀察者創建關聯)。當依賴的屬性發生變化時,會通知對應的Watcher觀察者從新求值(setter->notify->watcher->run),對應到模版中就是從新render(re-render)。github

注意: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上。bash

首先,先分析一下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]
}
複製代碼

useStatehooks很是核心的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並同步來監聽依賴屬性變化,雖然會增長計算開銷,但這樣能夠保證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真香(逃)

參考文章

相關文章
相關標籤/搜索