在 React 和 Vue 中尝鲜 Hooks

栏目: JavaScript · 发布时间: 5年前

内容简介:而就在同月 28 日左右,作为

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

在 React 和 Vue 中尝鲜 Hooks

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

在 React 和 Vue 中尝鲜 Hooks

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

I. 新鲜的 React Hooks

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

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

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

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

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

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 的应用:

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() 方法:

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()   后执行这些逻辑。

同样看一个示例:

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   分组不同的副作用,使逻辑更清晰;而非像原来一样都方针同一个生命周期中

跳过副作用以优化性能

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

componentDidUpdate(prevProps, prevState) {
  if (prevState.count !== this.state.count) {
    document.title = `You clicked ${this.state.count} times`;
  }
}

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

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]);

2.3 自定义 Hooks

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

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

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

在组件中调用:

function FriendStatus(props) {
  const isOnline = useFriendStatus(props.friend.id);

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

在另一个组件中调用:

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

在 React 和 Vue 中尝鲜 Hooks

在 React 和 Vue 中尝鲜 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” 代码:

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   属性的情况下,也实现了一种更函数式的开发体验:

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 和方法等。

首先大体看一下:

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

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

//vue/src/core/instance/lifecycle.js

Vue.prototype.$destroy = function () {
    //...
    callHook(vm, 'destroyed')
    //...
}

//...

export function callHook (vm, hook) {
  //...
  if (vm._hasHookEvent) {
    vm.$emit('hook:' + hook)
  }
  //...
}

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

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

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

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

export function useData(initial) {
  //...
  if (isMounting) {
    currentInstance.$set(state, id, initial)
  }
  return state[id]
}

同样利用实例方法的:

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

--End--

以上所述就是小编给大家介绍的《在 React 和 Vue 中尝鲜 Hooks》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Algorithms on Strings, Trees and Sequences

Algorithms on Strings, Trees and Sequences

Dan Gusfield / Cambridge University Press / 1997-5-28 / USD 99.99

String algorithms are a traditional area of study in computer science. In recent years their importance has grown dramatically with the huge increase of electronically stored text and of molecular seq......一起来看看 《Algorithms on Strings, Trees and Sequences》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

html转js在线工具
html转js在线工具

html转js在线工具