background

I recently looked at the latest developments in Vue3.0 and found that there was a lot of change. In general, VUE is also starting to resemble hooks, and the vUE author himself said that the features of Vue3.0 were inspired by hooks. So do some research on hooks before Vue3.0 is officially released.

Source code address: vue-rex-POc

Why hooks?

Class-component /vue-options:

  • Cross-component code is difficult to reuse
  • Large components, difficult to maintain, granularity is not easy to control, fine granularity partition, component nesting level is too deep – affecting performance
  • Class component, this is uncontrollable, logically scattered, not easy to understand
  • Mixins have the side effects of nested logic, unknown data sources, and non-consumption of each other

When a template relies on many mixins, it is easy to have unclear data sources or name conflicts, and when developing mixins, the logic and logic-dependent properties are dispersed and cannot be consumed by each other. These are painful points in development, so it makes sense to introduce hooks related features in VUe3.0.

vue-hooks

Before delving into VUe-hooks, a quick review of vUE’s responsive systems: First, the vUE component initializes the properties mounted on data in a reactive manner (mount the dependency manager). Then, when the template is compiled into the V-DOM, a Watcher observer is instantiated to observe the entire vNode and access the properties of these dependencies. Trigger the dependency manager to collect the dependencies (associated with the Watcher observer). When a dependency property changes, the Watcher observer is notified to reevaluate (setter->notify-> Watcher ->run), which is re-render in the template.

Note: Vue internally places the Re-render process in the microtask queue by default, and the current render is evaluated during the last Render Flush phase.

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
    }
  }
}
Copy the code

WithHooks provides hooks+ JSX development for vue components as follows:

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

WithHooks return options as a vue Component configuration item. Subsequent hooks related properties are mounted on locally supplied Options.

First, let’s examine some global variables that vue-hooks need to use:

  • CurrentInstance: Caches the current VUE instance
  • IsMounting: Render Whether the render is the first time
isMounting = ! this._vnodeCopy the code

Here _vnode is quite different from $vnode, where $vnode represents the parent component (vm._vnode.parent).

_vnode is initialized to null. In mounted, the value is assigned to the V-DOM of the current component

In addition to controlling the internal data initialization stage, isMounting also prevents repeated re-render.

  • CallIndex: attribute index. When mounting attributes to options, use callIndex as the unique index identifier.

Several local variables declared on vue Options:

  • _state: Places responsive data
  • _refsStore: Places non-responsive data and returns a reference type
  • _effectStore: stores side effect logic and cleanup logic
  • _computedStore: stores computing attributes

Finally, the withHooks callback, which takes attrs and $props as inputs, and, after rendering the current component, resets the global variables for rendering the next component.

useData

const data = useData(initial)
Copy the code
export function useData(initial) {
  const id = ++callIndex
  const state = currentInstance.$data._state
  if (isMounting) {
    currentInstance.$set(state, id, initial)
  }
  return state[id]
}
Copy the code

We know that responding to a data change requires some processing in VUE, and the scenario is limited. Using useData to declare variables also mounts a reactive data on the internal data._state. However, the drawback is that it does not provide an updater, and it is possible to lose responsive listening when the data returned externally changes.

useState

const [data, setData] = useState(initial)
Copy the code
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]
}
Copy the code

Use updater to update data in a responsive manner. Re-render is triggered when the data is changed. The next render process does not need to be initialized again using $set. Instead, it takes the cached value since the last update.

useRef

const data = useRef(initial) // data = {current: initial}
Copy the code
export function useRef(initial) {
  ensureCurrentInstance()
  const id = ++callIndex
  const { _refsStore: refs } = currentInstance
  return isMounting ? (refs[id] = { current: initial }) : refs[id]
}
Copy the code

Initialization with useRef returns a reference with current, which points to the initialized value. When I first started using useRef, I didn’t understand the scenarios, but when I got really used to it, I got a feel for it.

For example:

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>
  )
})
Copy the code

Clicking the button will add the value +1 and print the corresponding variable at the same time. The output result is:

0, 1, 1, 2, 2, 3, 3, 4, 4, 5Copy the code

As you can see, num.current is always the latest value, while count gets the last render value. In fact, the same effect can be achieved by raising num to global scope. So you can expect useRef scenarios:

  • Save the latest values during multiple re-render sessions
  • This value does not require reactive processing
  • Does not contaminate other scopes

useEffect

useEffect(function()=>{// Side effect logicreturn()=> {// clean logic}}, [deps])Copy the code
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 } } }Copy the code

UseEffect is also one of the very important apis in hooks that handle side effects and clean up logic. The side effects here can be understood as operations that can be performed selectively based on dependencies, not necessarily every re-render, such as DOM operations, network requests, etc. These operations can cause side effects such as the need to clear DOM listeners, clear references, and so on.

In the Mounted phase, a cleanup function and a side effect function are declared, and effect current points to the current side effect logic. In the Mounted phase, the return value of the side effect function is saved as the cleanup logic. It also determines whether to call the side effect function again during the updated phase based on the dependency.

When rendering is not the first time, it will judge whether the side effect function needs to be called again according to the DEPS dependency. When it needs to be executed again, it will first clear the side effect caused by the last render, and point the current of the side effect function to the latest side effect logic, and wait for the call in the updated stage.

useMounted

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

UseEffect when [] is passed, the side effect function is called only in the Mounted phase.

useDestroyed

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

UseEffect depends on [] and has a return function. The return function is called as the cleaning logic in destroyed.

useUpdated

useUpdated(fn, deps)
Copy the code
export function useUpdated(fn, deps) {
  const isMount = useRef(true)
  useEffect(() => {
    if (isMount.current) {
      isMount.current = false
    } else {
      return fn()
    }
  }, deps)
}
Copy the code

If deps is fixed, the passed useEffect is executed once in the Mounted and updated phases. In this case, useRef declares a persistent variable to skip the Mounted phase.

useWatch

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

Use the same way as $watch. A first render judgment was added to prevent re-render from generating extra Watcher observers.

useComputed

const data = useData({count:1})
const getCount = useComputed(()=>data.count)
Copy the code
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]
}
Copy the code

UseComputed first evaluates the dependency value and caches it, calling $watch to observe the change in the dependency property and updating the corresponding cached value.

In fact, the underlying vUE for computed is a little more complicated. When computed is initialized, the lazy: True (asynchronous) method is used to monitor dependency changes, that is, the change of dirty variables is controlled instead of being evaluated immediately when the dependency attributes change. In addition, bind the key corresponding to the calculated attribute to the component instance, and change it to the accessor attribute. When accessing the calculated attribute, judge whether to evaluate according to dirty.

So I’m going to callInstead of waiting until the Render Flush phase to evaluate, watch gets the latest value as soon as the property changes.

hooks

export function hooks (Vue) {
  Vue.mixin({
    beforeCreate() {
      const { hooks, data } = this.$options
      if{this._effectStore = {} this._refsStore = {} this._computedStore = {} // Rewrite the data function and inject _state attribute this.$options.data = function () {
          const ret = data ? data.call(this) : {}
          ret._state = {}
          return ret
        }
      }
    },
    beforeMount() {
      const { hooks, render } = this.$options
      if{// Rewrite the component's render function this.$options.render = function(h) { callIndex = 0 currentInstance = this isMounting = ! This. _vnode // Passes the props attribute const hookProps = hooks(this.$props(this._self, hookProps) const ret = render. Call (this, h) currentInstance = nullreturn ret
        }
      }
    }
  })
}
Copy the code

With withHooks, we can do what hooks do, but sacrifice a lot of vue features like props, attrs, Components, etc.

Vue-hooks exposes a hooks function that allows developers to mix internal logic into all child components after the vue.use (hooks) entry. This allows us to use hooks in the SFC component.

To make it easier to understand, there is a simple implementation that wraps the dynamically computed element node dimensions into separate 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;
}

exportDefault {props: {MSG: String}, // This is similar to the setup function, which accepts props, which returns dependent properties.return{ resize: JSON.stringify(data) }; }}; </script> <style> html, body { height: 100%; } </style>Copy the code

The effect is to output the change information to the document when the element size changes, and to unregister the resize listener when the component is destroyed.

Attributes that are returned by hooks in the component’s own instance so that template bound variables can be referenced.

What is the problem with hooks?

In practice, hooks are used to solve many of the problems caused by mixins, and to develop components more abstractly. However, at the same time, it also brings a higher threshold, for example, useEffect must be loyal to dependencies when used, otherwise it will be a matter of minutes to cause the endless loop of render.

Compared to react-hooks, VUE can leverage function abstraction and reuse capabilities, as well as the benefits of responsive tracing. Here’s what You had to say in contrast to react-hooks:

  • More intuitive with JavaScript overall;
  • Can be called conditionally without restriction of call order;
  • It does not cause engine optimization or GC stress by constantly creating a large number of inline functions during subsequent updates;
  • You don’t always need to use useCallback to cache callbacks to child components to prevent overupdating;
  • You don’t need to worry about to send the wrong depend on the array to useEffect/useMemo useCallback leading to use outdated values in the callback – Vue rely on tracking is fully automatic.

feeling

I read the hooks source code in order to get started with the new features faster after VUe3.0 was released. It was more than I expected, and compared to the new RFC, it was clear. Unfortunately, a lot of development projects rely on vue-property-decorators to do TS adaptation.

Finally, hooks smell so good

Refer to the article

  • Vue Function-based API RFC
  • What Hooks Mean for Vue
  • Implementation analysis of the Hooks API in Vue
  • Function Component Primer

If the content of this article is wrong, welcome to point out!

Reprint please indicate the source!