最新文章專題視頻專題問答1問答10問答100問答1000問答2000關(guān)鍵字專題1關(guān)鍵字專題50關(guān)鍵字專題500關(guān)鍵字專題1500TAG最新視頻文章推薦1 推薦3 推薦5 推薦7 推薦9 推薦11 推薦13 推薦15 推薦17 推薦19 推薦21 推薦23 推薦25 推薦27 推薦29 推薦31 推薦33 推薦35 推薦37視頻文章20視頻文章30視頻文章40視頻文章50視頻文章60 視頻文章70視頻文章80視頻文章90視頻文章100視頻文章120視頻文章140 視頻2關(guān)鍵字專題關(guān)鍵字專題tag2tag3文章專題文章專題2文章索引1文章索引2文章索引3文章索引4文章索引5123456789101112131415文章專題3
問答文章1 問答文章501 問答文章1001 問答文章1501 問答文章2001 問答文章2501 問答文章3001 問答文章3501 問答文章4001 問答文章4501 問答文章5001 問答文章5501 問答文章6001 問答文章6501 問答文章7001 問答文章7501 問答文章8001 問答文章8501 問答文章9001 問答文章9501
當(dāng)前位置: 首頁 - 科技 - 知識(shí)百科 - 正文

深度了解vue.js中hooks的相關(guān)知識(shí)

來源:懂視網(wǎng) 責(zé)編:小采 時(shí)間:2020-11-27 21:55:20
文檔

深度了解vue.js中hooks的相關(guān)知識(shí)

深度了解vue.js中hooks的相關(guān)知識(shí):背景 最近研究了vue3.0的最新進(jìn)展,發(fā)現(xiàn)變動(dòng)很大,總體上看,vue也開始向hooks靠攏,而且vue作者本人也稱vue3.0的特性吸取了很多hooks的靈感。所以趁著vue3.0未正式發(fā)布前,抓緊時(shí)間研究一下hooks相關(guān)的東西。 源碼地址:vue-hooks-poc 為
推薦度:
導(dǎo)讀深度了解vue.js中hooks的相關(guān)知識(shí):背景 最近研究了vue3.0的最新進(jìn)展,發(fā)現(xiàn)變動(dòng)很大,總體上看,vue也開始向hooks靠攏,而且vue作者本人也稱vue3.0的特性吸取了很多hooks的靈感。所以趁著vue3.0未正式發(fā)布前,抓緊時(shí)間研究一下hooks相關(guān)的東西。 源碼地址:vue-hooks-poc 為

背景

最近研究了vue3.0的最新進(jìn)展,發(fā)現(xiàn)變動(dòng)很大,總體上看,vue也開始向hooks靠攏,而且vue作者本人也稱vue3.0的特性吸取了很多hooks的靈感。所以趁著vue3.0未正式發(fā)布前,抓緊時(shí)間研究一下hooks相關(guān)的東西。

源碼地址:vue-hooks-poc

為什么要用hooks?

首先從class-component/vue-options說起:

  • 跨組件代碼難以復(fù)用
  • 大組件,維護(hù)困難,顆粒度不好控制,細(xì)粒度劃分時(shí),組件嵌套存層次太深-影響性能
  • 類組件,this不可控,邏輯分散,不容易理解
  • mixins具有副作用,邏輯互相嵌套,數(shù)據(jù)來源不明,且不能互相消費(fèi)
  • 當(dāng)一個(gè)模版依賴了很多mixin的時(shí)候,很容易出現(xiàn)數(shù)據(jù)來源不清或者命名沖突的問題,而且開發(fā)mixins的時(shí)候,邏輯及邏輯依賴的屬性互相分散且mixin之間不可互相消費(fèi)。這些都是開發(fā)中令人非常痛苦的點(diǎn),因此,vue3.0中引入hooks相關(guān)的特性非常明智。

    vue-hooks

    在探究vue-hooks之前,先粗略的回顧一下vue的響應(yīng)式系統(tǒng):首先,vue組件初始化時(shí)會(huì)將掛載在data上的屬性響應(yīng)式處理(掛載依賴管理器),然后模版編譯成v-dom的過程中,實(shí)例化一個(gè)Watcher觀察者觀察整個(gè)比對(duì)后的vnode,同時(shí)也會(huì)訪問這些依賴的屬性,觸發(fā)依賴管理器收集依賴(與Watcher觀察者建立關(guān)聯(lián))。當(dāng)依賴的屬性發(fā)生變化時(shí),會(huì)通知對(duì)應(yīng)的Watcher觀察者重新求值(setter->notify->watcher->run),對(duì)應(yīng)到模版中就是重新render(re-render)。

    注意:vue內(nèi)部默認(rèn)將re-render過程放入微任務(wù)隊(duì)列中,當(dāng)前的render會(huì)在上一次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的開發(fā)方式,使用方式如下:

    export default withHooks((h)=>{
    ...
    return <span></span>
    })

    不難看出,withHooks依舊是返回一個(gè)vue component的配置項(xiàng)options,后續(xù)的hooks相關(guān)的屬性都掛載在本地提供的options上。

    首先,先分析一下vue-hooks需要用到的幾個(gè)全局變量:

  • currentInstance:緩存當(dāng)前的vue實(shí)例
  • isMounting:render是否為首次渲染
  • isMounting = !this._vnode

    這里的_vnode與$vnode有很大的區(qū)別,$vnode代表父組件(vm._vnode.parent)

    _vnode初始化為null,在mounted階段會(huì)被賦值為當(dāng)前組件的v-dom

    isMounting除了控制內(nèi)部數(shù)據(jù)初始化的階段外,還能防止重復(fù)re-render。

  • callIndex:屬性索引,當(dāng)往options上掛載屬性時(shí),使用callIndex作為唯一當(dāng)索引標(biāo)識(shí)。
  • vue options上聲明的幾個(gè)本地變量:

  • _state:放置響應(yīng)式數(shù)據(jù)
  • _refsStore:放置非響應(yīng)式數(shù)據(jù),且返回引用類型
  • _effectStore:存放副作用邏輯和清理邏輯
  • _computedStore:存放計(jì)算屬性
  • 最后,withHooks的回調(diào)函數(shù),傳入了attrs和$props作為入?yún)?,且在渲染完?dāng)前組件后,重置全局變量,以備渲染下個(gè)組件。

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

    我們知道,想要響應(yīng)式的監(jiān)聽一個(gè)數(shù)據(jù)的變化,在vue中需要經(jīng)過一些處理,且場景比較受限。使用useData聲明變量的同時(shí),也會(huì)在內(nèi)部data._state上掛載一個(gè)響應(yīng)式數(shù)據(jù)。但缺陷是,它沒有提供更新器,對(duì)外返回的數(shù)據(jù)發(fā)生變化時(shí),有可能會(huì)丟失響應(yīng)式監(jiān)聽。

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

    useState是hooks非常核心的API之一,它在內(nèi)部通過閉包提供了一個(gè)更新器updater,使用updater可以響應(yīng)式更新數(shù)據(jù),數(shù)據(jù)變更后會(huì)觸發(fā)re-render,下一次的render過程,不會(huì)在重新使用$set初始化,而是會(huì)取上一次更新后的緩存值。

    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初始化會(huì)返回一個(gè)攜帶current的引用,current指向初始化的值。我在初次使用useRef的時(shí)候總是理解不了它的應(yīng)用場景,但真正上手后還是多少有了一些感受。

    比如有以下代碼:

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

    點(diǎn)擊按鈕會(huì)將數(shù)值+1,同時(shí)打印對(duì)應(yīng)的變量,輸出結(jié)果為:

    0 1
    1 2
    2 3
    3 4
    4 5

    可以看到,num.current永遠(yuǎn)都是最新的值,而count獲取到的是上一次render的值。

    其實(shí),這里將num提升至全局作用域也可以實(shí)現(xiàn)相同的效果。

    所以可以預(yù)見useRef的使用場景:

  • 多次re-render過程中保存最新的值
  • 該值不需要響應(yīng)式處理
  • 不污染其他作用域
  • 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之一,它負(fù)責(zé)副作用處理和清理邏輯。這里的副作用可以理解為可以根據(jù)依賴選擇性的執(zhí)行的操作,沒必要每次re-render都執(zhí)行,比如dom操作,網(wǎng)絡(luò)請求等。而這些操作可能會(huì)導(dǎo)致一些副作用,比如需要清除dom監(jiān)聽器,清空引用等等。

    先從執(zhí)行順序上看,初始化時(shí),聲明了清理函數(shù)和副作用函數(shù),并將effect的current指向當(dāng)前的副作用邏輯,在mounted階段調(diào)用一次副作用函數(shù),將返回值當(dāng)成清理邏輯保存。同時(shí)根據(jù)依賴來判斷是否在updated階段再次調(diào)用副作用函數(shù)。
    非首次渲染時(shí),會(huì)根據(jù)deps依賴來判斷是否需要再次調(diào)用副作用函數(shù),需要再次執(zhí)行時(shí),先清除上一次render產(chǎn)生的副作用,并將副作用函數(shù)的current指向最新的副作用邏輯,等待updated階段調(diào)用。

    useMounted

    useMounted(function(){})
    export function useMounted(fn) {
    useEffect(fn, [])
    }

    useEffect依賴傳[]時(shí),副作用函數(shù)只在mounted階段調(diào)用。

    useDestroyed

    useDestroyed(function(){})
    export function useDestroyed(fn) {
    useEffect(() => fn, [])
    }

    useEffect依賴傳[]且存在返回函數(shù),返回函數(shù)會(huì)被當(dāng)作清理邏輯在destroyed調(diào)用。

    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會(huì)在mounted和updated階段各執(zhí)行一次,這里借助useRef聲明一個(gè)持久化的變量,來跳過mounted階段。

    useWatch

    export function useWatch(getter, cb, options) {
    ensureCurrentInstance()
    if (isMounting) {
    currentInstance.$watch(getter, cb, options)
    }
    }

    使用方式同$watch。這里加了一個(gè)是否初次渲染判斷,防止re-render產(chǎn)生多余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首先會(huì)計(jì)算一次依賴值并緩存,調(diào)用$watch來觀察依賴屬性變化,并更新對(duì)應(yīng)的緩存值。

    實(shí)際上,vue底層對(duì)computed對(duì)處理要稍微復(fù)雜一些,在初始化computed時(shí),采用lazy:true(異步)的方式來監(jiān)聽依賴變化,即依賴屬性變化時(shí)不會(huì)立刻求值,而是控制dirty變量變化;并將計(jì)算屬性對(duì)應(yīng)的key綁定到組件實(shí)例上,同時(shí)修改為訪問器屬性,等到訪問該計(jì)算屬性的時(shí)候,再依據(jù)dirty來判斷是否求值。

    這里直接調(diào)用watch會(huì)在屬性變化時(shí),立即獲取最新值,而不是等到render flush階段去求值。

    hooks

    export function hooks (Vue) {
    Vue.mixin({
    beforeCreate() {
    const { hooks, data } = this.$options
    if (hooks) {
    this._effectStore = {}
    this._refsStore = {}
    this._computedStore = {}
    // 改寫data函數(shù),注入_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函數(shù)
    this.$options.render = function(h) {
    callIndex = 0
    currentInstance = this
    isMounting = !this._vnode
    // 默認(rèn)傳入props屬性
    const hookProps = hooks(this.$props)
    // _self指示本身組件實(shí)例
    Object.assign(this._self, hookProps)
    const ret = render.call(this, h)
    currentInstance = null
    return ret
    }
    }
    }
    })
    }

    借助withHooks,我們可以發(fā)揮hooks的作用,但犧牲來很多vue的特性,比如props,attrs,components等。

    vue-hooks暴露了一個(gè)hooks函數(shù),開發(fā)者在入口Vue.use(hooks)之后,可以將內(nèi)部邏輯混入所有的子組件。這樣,我們就可以在SFC組件中使用hooks啦。

    為了便于理解,這里簡單實(shí)現(xiàn)了一個(gè)功能,將動(dòng)態(tài)計(jì)算元素節(jié)點(diǎn)尺寸封裝成獨(dú)立的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函數(shù)很接近了,都是接受props,最后返回依賴的屬性
    hooks(props) {
    const data = useResize();
    return {
    resize: JSON.stringify(data)
    };
    }
    };
    </script>
    <style>
    html,
    body {
    height: 100%;
    }
    </style>

    使用效果是,元素尺寸變更時(shí),將變更信息輸出至文檔中,同時(shí)在組件銷毀時(shí),注銷resize監(jiān)聽器。

    hooks返回的屬性,會(huì)合并進(jìn)組件的自身實(shí)例中,這樣模版綁定的變量就可以引用了。

    hooks存在什么問題?

    在實(shí)際應(yīng)用過程中發(fā)現(xiàn),hooks的出現(xiàn)確實(shí)能解決mixin帶來的諸多問題,同時(shí)也能更加抽象化的開發(fā)組件。但與此同時(shí)也帶來了更高的門檻,比如useEffect在使用時(shí)一定要對(duì)依賴忠誠,否則引起render的死循環(huán)也是分分鐘的事情。
    與react-hooks相比,vue可以借鑒函數(shù)抽象及復(fù)用的能力,同時(shí)也可以發(fā)揮自身響應(yīng)式追蹤的優(yōu)勢。我們可以看尤在與react-hooks對(duì)比中給出的看法:

    整體上更符合 JavaScript 的直覺;
    不受調(diào)用順序的限制,可以有條件地被調(diào)用;
    不會(huì)在后續(xù)更新時(shí)不斷產(chǎn)生大量的內(nèi)聯(lián)函數(shù)而影響引擎優(yōu)化或是導(dǎo)致 GC 壓力;
    不需要總是使用 useCallback 來緩存?zhèn)鹘o子組件的回調(diào)以防止過度更新;
    不需要擔(dān)心傳了錯(cuò)誤的依賴數(shù)組給 useEffect/useMemo/useCallback 從而導(dǎo)致回調(diào)中使用了過期的值 —— Vue 的依賴追蹤是全自動(dòng)的。

    感受

    為了能夠在vue3.0發(fā)布后更快的上手新特性,便研讀了一下hooks相關(guān)的源碼,發(fā)現(xiàn)比想象中收獲的要多,而且與新發(fā)布的RFC對(duì)比來看,恍然大悟??上Чぷ髟?,開發(fā)項(xiàng)目中很多依賴了vue-property-decorator來做ts適配,看來三版本出來后要大改了。

    最后,hooks真香(逃)

    聲明:本網(wǎng)頁內(nèi)容旨在傳播知識(shí),若有侵權(quán)等問題請及時(shí)與本網(wǎng)聯(lián)系,我們將在第一時(shí)間刪除處理。TEL:177 7030 7066 E-MAIL:11247931@qq.com

    文檔

    深度了解vue.js中hooks的相關(guān)知識(shí)

    深度了解vue.js中hooks的相關(guān)知識(shí):背景 最近研究了vue3.0的最新進(jìn)展,發(fā)現(xiàn)變動(dòng)很大,總體上看,vue也開始向hooks靠攏,而且vue作者本人也稱vue3.0的特性吸取了很多hooks的靈感。所以趁著vue3.0未正式發(fā)布前,抓緊時(shí)間研究一下hooks相關(guān)的東西。 源碼地址:vue-hooks-poc 為
    推薦度:
    標(biāo)簽: VUE vue.js hooks
    • 熱門焦點(diǎn)

    最新推薦

    猜你喜歡

    熱門推薦

    專題
    Top