在 React 和 Vue 中尝鲜 Hooks
栏目: JavaScript · 发布时间: 6年前
内容简介:而就在同月 28 日左右,作为
在美国当地时间 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 特性
在琢磨这个定义之前,先直观感受下官网中给出的第一个例子:
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
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
以上所述就是小编给大家介绍的《在 React 和 Vue 中尝鲜 Hooks》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
嵌入式系统软件设计中的常用算法
周航慈 / 2010-1 / 24.00元
《嵌入式系统软件设计中的常用算法》根据嵌入式系统软件设计需要的常用算法知识编写而成。基本内容有:线性方程组求解、代数插值和曲线拟合、数值积分、能谱处理、数字滤波、数理统计、自动控制、数据排序、数据压缩和检错纠错等常用算法。从嵌入式系统的实际应用出发,用通俗易懂的语言代替枯燥难懂的数学推导,使读者能在比较轻松的条件下学到最基本的常用算法,并为继续学习其他算法打下基础。 《嵌入式系统软件设计中的......一起来看看 《嵌入式系统软件设计中的常用算法》 这本书的介绍吧!