前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >在 React 和 Vue 中尝鲜 Hooks

在 React 和 Vue 中尝鲜 Hooks

作者头像
江米小枣
发布2020-06-15 21:40:08
4.2K0
发布2020-06-15 21:40:08
举报
文章被收录于专栏:云前端

在美国当地时间 10 月 26 日举办的 React Conf 2018 上,React 官方宣布 React v16.7.0-alpha 将引入名为 Hooks 的新特性,在开发社区引发震动。

而就在同月 28 日左右,作为 “摸着鹰酱过河” 优秀而典型代表的 Vue.js 社区,其创始人 Evan You 就在自己的 github 上发布了 vue-hooks 工具库,其简介为 “实验性的 React hooks 在 Vue 的实现”。

到底是怎样的一个新特性,让大家如此关注、迅速行动呢?本文将尝试做出简单介绍和比较,看看其中的热闹,并一窥其门道。

I. 新鲜的 React Hooks

在 React v16.7.0-alpha 版本中,React 正式引入了新特性 Hooks,其定义为:

Hooks 是一种新特性,致力于让你不用写类也能用到 state 和其他 React 特性

在琢磨这个定义之前,先直观感受下官网中给出的第一个例子:

代码语言:javascript
复制
import { useState } from 'react';function Example() {
 // Declare a new state variable, which we'll call "count"
 const [count, setCount] = useState(0); return (
   <div>
     <p>You clicked {count} times</p>
     <button onClick={() => setCount(count + 1)}>
       Click me
     </button>
   </div>
 );
}

换成之前的写法,则等价于:

代码语言:javascript
复制
class Example extends React.Component {
 constructor(props) {
   super(props);
   this.state = {
     count: 0
   };
 } render() {
   return (
     <div>
       <p>You clicked {this.state.count} times</p>
       <button onClick={() => this.setState({ count: this.state.count + 1 })}>
         Click me
       </button>
     </div>
   );
 }
}

猜也差不多猜得到,useState() 这个新的内建方法抽象了原有的 this.setState() 逻辑。

为什么又有新 API ?

自从 React 诞生后,其创建组件的方式从 ES5 时期声明式的 createClass,到支持原生 ES6 class 的 OOP 语法,再到发展出 HOC 或 render props 的函数式写法,官方和社区一直在探索更方便合理的 React 组件化之路。

随之而来的一些问题是:

  • 组件往往变得嵌套过多
  • 各种写法的组件随着逻辑的增长,都变得难以理解
  • 尤其是基于类写法的组件中,this 关键字暧昧模糊,人和机器读起来都比较懵
  • 难以在不同的组件直接复用基于 state 的逻辑
  • 人们不满足于只用函数式组件做简单的展示组件,也想把 state 和生命周期等引入其中

Hooks 就是官方为解决类似的问题的一次最新的努力。

II. 几种可用的 Hooks

对开头的官方定义稍加解释就是:Hooks 是一种函数,该函数允许你 “勾住(hook into)” React 组件的 state 和生命周期。可以使用内建或自定义的 Hooks 在不同组件之间复用、甚至在同一组件中多次复用基于 state 的逻辑。

Hooks 在类内部不起作用,官方也并不建议马上开始重写现有的组件类,但可以在新组件中开始使用。

Hooks 主要分为以下几种:

  • 基础 Hooks
    • useState
    • useEffect
    • useContext
  • 其他内建 Hooks
    • useReducer
    • useCallback
    • useMemo
    • useRef
    • useImperativeMethods
    • useMutationEffect
    • useLayoutEffect
  • 自定义 Hooks

2.1 State Hook

文章开头的计数器例子就是一种 State Hook 的应用:

代码语言:javascript
复制
import { useState } from 'react';function Example() {
 const [count, setCount] = useState(0); return (
   <div>
     <p>You clicked {count} times</p>
     <button onClick={() => setCount(count + 1)}>
       Click me
     </button>
   </div>
 );
}

这种最常用的 Hook 也展示了 Hooks 的通用逻辑:

  • 调用 useState 方法,返回一个数组
  • 这里使用了 “array destructuring” 语法
  • 数组首个值相当于定义了 this.state.count,命名随意
  • 数组第二个值用来更新以上值,命名随意,相当于 this.setState({count: })
  • useState 方法唯一的参数,就是所定义值的初始值
多次调用 Hooks

当需要用到多个状态值时,不同于在 state 中都定义到一个对象中的做法,可以多次使用 useState() 方法:

代码语言:javascript
复制
const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);

应该注意到,同样有别于传统 state 的是,调用相应更新函数后,只会用新值替换旧值,而非合并两者。

2.2 Effect Hook

所谓的 “Effect” 对应的概念叫做 “side effect”。指的是状态改变时,相关的远端数据异步请求、事件绑定、改变 DOM 等;因为此类操作要么会引发其他组件的变化,要么在渲染周期中并不能立刻完成,所以就称其为“副作用”。

传统做法中,一般在 componentDidMount、componentDidUpdate、componentWillUnmount 等生命周期中分别管理这些副作用,逻辑分散而复杂。

在 Hooks 中的方案是使用 useEffect 方法,这相当于告诉 React 在每次更新变化到 DOM 后,就调用这些副作用;React 将在每次(包括首次)render() 后执行这些逻辑。

同样看一个示例:

代码语言:javascript
复制
function FriendStatusWithCounter(props) {
 const [count, setCount] = useState(0);
 useEffect(() => {
   document.title = `You clicked ${count} times`;
 }); const [isOnline, setIsOnline] = useState(null);
 useEffect(() => {
   ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
   return () => {
     ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
   };
 }); function handleStatusChange(status) {
   setIsOnline(status.isOnline);
 }
 // ...
}

可以看出:

  • useEffect 一般可以搭配 useState 使用
  • useEffect 接受一个函数作为首个参数,在里面书写副作用代码,比如绑定事件
  • 若该函数返回一个函数,则返回的这个函数就作为相应副作用的 “cleanup”,比如解绑事件
  • 同样可以用多个 useEffect 分组不同的副作用,使逻辑更清晰;而非像原来一样都方针同一个生命周期中
跳过副作用以优化性能

副作用往往都带来一些性能消耗,传统上我们可能这样避免不必要的执行:

代码语言:javascript
复制
componentDidUpdate(prevProps, prevState) {
 if (prevState.count !== this.state.count) {
   document.title = `You clicked ${this.state.count} times`;
 }
}

useEffect 中的做法则是传入第二个可选参数:一个数组;数组中的变量用来告诉 React,在重新渲染过程中,只有在其变化时,对应的副作用才应该被执行。

代码语言:javascript
复制
useEffect(() => {
 document.title = `You clicked ${count} times`;
}, [count]);

2.3 自定义 Hooks

传统上使用 HOC 或 render props 的写法实现逻辑共享;而定义自己的 Hooks,可以将组件中的逻辑抽取到可服用的函数中去。

比如将之前例子中的 isOnline 状态值逻辑抽取出来:

代码语言:javascript
复制
import { useState, useEffect } from 'react';function useFriendStatus(friendID) {
 const [isOnline, setIsOnline] = useState(null); function handleStatusChange(status) {
   setIsOnline(status.isOnline);
 } useEffect(() => {
   ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
   return () => {
     ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
   };
 }); return isOnline;
}

在组件中调用:

代码语言:javascript
复制
function FriendStatus(props) {
 const isOnline = useFriendStatus(props.friend.id); if (isOnline === null) {
   return 'Loading...';
 }
 return isOnline ? 'Online' : 'Offline';
}

在另一个组件中调用:

代码语言:javascript
复制
function FriendListItem(props) {
 const isOnline = useFriendStatus(props.friend.id); return (
   <li style={{ color: isOnline ? 'green' : 'black' }}>
     {props.friend.name}
   </li>
 );
}

如上所示:

  • 自定义 Hook 函数的参数是自由定义的
  • 因为只是个纯函数,所以不同组件都可以各自调用
  • 使用 use 前缀不是硬性要求,但确实是推荐使用的约定
  • 不同组件只共享状态逻辑,而不共享任何状态

2.4 调用 Hooks 的两个原则

  • 只在 top level 调用 Hooks,而不能在循环、条件或嵌套函数中使用
  • 只在 React 函数组件或自定义 Hooks 中调用,而不能在普通 JS 函数中

可以使用官方提供的 eslint 插件保证以上原则: https://www.npmjs.com/package/eslint-plugin-react-hooks

III. Vue.js 社区的追赶

vue-hooks 库: https://github.com/yyx990803/vue-hooks

目前该库也声明为实验性质,并不推荐在正式产品中使用。

3.1 “React-style” 的 Hooks

vue-hooks 支持以下 Hooks,嗯呢,看着相当眼熟:

  • useState
  • useEffect
  • useRef

以及一个辅助方法:

  • withHooks

结合 Vue.js 中的 render(),可以写出非常函数式的 “React-like” 代码:

代码语言:javascript
复制
import Vue from "vue"
import { withHooks, useState, useEffect } from "vue-hooks"// a custom hook...
function useWindowWidth() {
 const [width, setWidth] = useState(window.innerWidth)
 const handleResize = () => {
   setWidth(window.innerWidth)
 };
 useEffect(() => {
   window.addEventListener("resize", handleResize)
   return () => {
     window.removeEventListener("resize", handleResize)
   }
 }, [])
 return width
}const Foo = withHooks(h => {
 // state
 const [count, setCount] = useState(0) // effect
 useEffect(() => {
   document.title = "count is " + count
 }) // custom hook
 const width = useWindowWidth() return h("div", [
   h("span", `count is: ${count}`),
   h(
     "button",
     {
       on: {
         click: () => setCount(count + 1)
       }
     },
     "+"
   ),
   h("div", `window width is: ${width}`)
 ])
})new Vue({
 el: "#app",
 render(h) {
   return h("div", [h(Foo), h(Foo)])
 }
})

3.2 “Vue-style” 的 Hooks

vue-hooks 也支持以下 Hooks,这些就非常接地气了:

  • useData
  • useMounted
  • useDestroyed
  • useUpdated
  • useWatch
  • useComputed

以及一个 mixin 插件:

  • hooks

这样在不提供 Vue 实例的显式 data 属性的情况下,也实现了一种更函数式的开发体验:

代码语言:javascript
复制
import { hooks, useData, useMounted, useWatch, useComputed } from 'vue-hooks'

Vue.use(hooks)

new Vue({
  el: "#app",
  hooks() {

    const data = useData({
      count: 0
    })

    const double = useComputed(() => data.count * 2)

    useWatch(() => data.count, (val, prevVal) => {
      console.log(`count is: ${val}`)
    })

    useMounted(() => {
      console.log('mounted!')
    })

    return {
      data,
      double
    }
  }
})

3.3 实现浅析

vue-hooks 的源码目前只有不到 200 行, 非常简明扼要的实现了以上提到的 Hooks 和方法等。

首先大体看一下:

代码语言:javascript
复制
let currentInstance = null
let isMounting = false
let callIndex = 0//...export function useState(initial) {
 //...
}
export function useEffect(rawEffect, deps) {
 //...
}
export function useRef(initial) {
 //...
}
export function useData(initial) {
 //...
}
export function useMounted(fn) {
 //...
}
export function useDestroyed(fn) {
 //...
}
export function useUpdated(fn, deps) {
 //...
}
export function useWatch(getter, cb, options) {
 //...
}
export function useComputed(getter) {
 //...
}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.$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 = {}
         return ret
       }
     }
   },
   beforeMount() {
     const { hooks, render } = this.$options
     if (hooks && render) {
       this.$options.render = function(h) {
         callIndex = 0
         currentInstance = this
         isMounting = !this._vnode
         const hookProps = hooks(this.$props)
         Object.assign(this._self, hookProps)
         const ret = render.call(this, h)
         currentInstance = null
         return ret
       }
     }
   }
 })
}

基本的结构非常清楚,可以看出:

  • withHooks 返回一个包装过的 Vue 实例配置
  • hooks 以 mixin 的形式发挥作用,注入两个生命周期
  • 用模块局部变量 currentInstance 记录了 Hooks 生效的 Vue 实例

其次值得注意的是处理副作用的 useEffect

代码语言:javascript
复制
export function useEffect(rawEffect, deps) {
 //...
 if (isMounting) {
   const cleanup = () => {
     const { current } = cleanup
     if (current) {
       current()
       cleanup.current = null
     }
   }
   const effect = () => {
     const { current } = effect
     if (current) {
       cleanup.current = current()
       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.lenght > 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
   }
 }
}

其核心大致轨迹如下:

  • 声明 effect 函数和 cleanup 函数
  • 将调用 Hook 时传入的 rawEffect 赋值到 effect.current 属性上
  • effect() 运行后,将 rawEffect 运行后的返回值赋值到 cleanup.current 上
  • 在 Vue 本身就支持的几个 hook:xxx 生命周期钩子事件中,调用 effect 或 cleanup
代码语言:javascript
复制
//vue/src/core/instance/lifecycle.jsVue.prototype.$destroy = function () {
   //...
   callHook(vm, 'destroyed')
   //...
}//...export function callHook (vm, hook) {
 //...
 if (vm._hasHookEvent) {
   vm.$emit('hook:' + hook)
 }
 //...
}

这样再去看这两个 Hook 就敞亮多了:

代码语言:javascript
复制
export function useMounted(fn) {
 useEffect(fn, [])
}export function useDestroyed(fn) {
 useEffect(() => fn, [])
}

另外常用的 useData 也是利用了 Vue 实例的 $set 方法,清晰易懂:

代码语言:javascript
复制
export function useData(initial) {
 //...
 if (isMounting) {
   currentInstance.$set(state, id, initial)
 }
 return state[id]
}

同样利用实例方法的:

代码语言:javascript
复制
export function useWatch(getter, cb, options) {
 //...
 if (isMounting) {
   currentInstance.$watch(getter, cb, options)
 }
}

其余几个 Hooks 的实现大同小异,就不逐一展开说明了。

IV. 总结

  • React Hooks 是简化组件定义、复用状态逻辑的一种最新尝试
  • vue-hooks 很好的实现了相同的功能,并且结合 Vue 实例的特点提供了适用的 Hooks

V. 参考资料

  • https://reactjs.org/docs/hooks-intro.html
  • https://github.com/yyx990803/vue-hooks/blob/master/README.md
  • https://www.zhihu.com/question/300049718/answer/518641446
  • https://mp.weixin.qq.com/s/GgJqG82blfNnNWqRWvSbQA
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2018-11-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 云前端 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • I. 新鲜的 React Hooks
    • 为什么又有新 API ?
    • II. 几种可用的 Hooks
      • 2.1 State Hook
        • 多次调用 Hooks
      • 2.2 Effect Hook
        • 跳过副作用以优化性能
      • 2.3 自定义 Hooks
        • 2.4 调用 Hooks 的两个原则
        • III. Vue.js 社区的追赶
          • 3.1 “React-style” 的 Hooks
            • 3.2 “Vue-style” 的 Hooks
              • 3.3 实现浅析
              • IV. 总结
              • V. 参考资料
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档