vue-hooks学习笔记(含源码解读)

1,903 阅读6分钟

背景

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
www.ptbird.cn/react-hoot-…
vue
www.jianshu.com/p/f1e6597b1… 简书
www.sohu.com/a/321909448… 精度vue-hooks
juejin.cn/post/684490… 掘金
mp.weixin.qq.com/s/p2f3jsko9… 云前端
1byte.io/react-hooks… react
blog.csdn.net/liuyingv8/a… 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
        }
      }
    }
  })
}