连鹏飞,微医云服务团队前端开发工程师,带着“偏见”去理解技术的世界javascript
先理解什么是 Hook,拿 React 的介绍来看,它的定义是:html
它可让你在不编写 Class 的状况下,让你在函数组件里“钩入” React state 及生命周期等特性的函数前端
对于 Vue 提出的新的书写 Vue 组件的 API:Composition API RFC,做用也是相似,因此咱们也能够像 React 同样叫作 Vue Hooksvue
框架是服务于业务的,业务中很难避免的一个问题就是 -- 逻辑复用,一样的功能,一样的组件,在不同的场合下,咱们有时候不得不去写 2+次,为了不耦合,后来各大框架纷纷想出了一些办法,好比 minix, render props, 高阶组件等实现逻辑上的复用,可是都有一些额外的问题java
Hook 的出现是划时代的,经过 function 抽离的方式,实现了复杂逻辑的内部封装:react
React Hooks 容许你 "勾入" 诸如组件状态和反作用处理等 React 功能中。Hooks 只能用在函数组件中,并容许咱们在不须要建立类的状况下将状态、反作用处理和更多东西带入组件中。web
React 核心团队奉上的采纳策略是不反对类组件,因此你能够升级 React 版本、在新组件中开始尝试 Hooks,并保持既有组件不作任何更改api
例子:数组
import React, { useState, useEffect } from "React";
const NoteForm = ({ onNoteSent }) => { const [currentNote, setCurrentNote] = useState(""); useEffect(() => { console.log(`Current note: ${currentNote}`); }); return ( <form onSubmit={e => { onNoteSent(currentNote); setCurrentNote(""); e.preventDefault(); }} > <label> <span>Note: </span> <input value={currentNote} onChange={e => { const val = e.target.value && e.target.value.toUpperCase()[0]; const validNotes = ["A", "B", "C", "D", "E", "F", "G"]; setCurrentNote(validNotes.includes(val) ? val : ""); }} /> </label> <button type="submit">Send</button> </form> ); }; 复制代码
Vue Composition API 围绕一个新的组件选项 setup 而建立。setup()
为 Vue 组件提供了状态、计算值、watcher 和生命周期钩子缓存
API 并无让原来的 API(如今被称做 "Options-based API")消失。容许开发者 结合使用新旧两种 APIs
能够在 Vue 2.x 中经过
@vue/composition-api
插件尝试新 API
例子:
<template>
<form @submit="handleSubmit"> <label> <span>Note:</span> <input v-model="currentNote" @input="handleNoteInput"> </label> <button type="submit">Send</button> </form> </template> <script> import { ref, watch } from "vue"; export default { props: ["divRef"], setup(props, context) { const currentNote = ref(""); const handleNoteInput = e => { const val = e.target.value && e.target.value.toUpperCase()[0]; const validNotes = ["A", "B", "C", "D", "E", "F", "G"]; currentNote.value = validNotes.includes(val) ? val : ""; }; const handleSubmit = e => { context.emit("note-sent", currentNote.value); currentNote.value = ""; e.preventDefault(); }; return { currentNote, handleNoteInput, handleSubmit, }; } }; </script> 复制代码
React Hook 底层是基于链表实现,调用的条件是每次组件被 render 的时候都会顺序执行全部的 Hooks,因此下面的代码会报错
function App(){
const [name, setName] = useState('demo'); if(condition){ const [val, setVal] = useState(''); } } 复制代码
由于底层是链表,每个 Hook 的 next 是指向下一个 Hook 的,if 会致使顺序不正确,从而致使报错,因此 React 是不容许这样使用 Hook 的。
Vue Hook 只会被注册调用一次,Vue 能避开这些麻烦的问题,缘由在于它对数据的响应是基于 proxy 的,对数据直接代理观察。这种场景下,只要任何一个更改 data 的地方,相关的 function 或者 template 都会被从新计算,所以避开了 React 可能遇到的性能上的问题
React 数据更改的时候,会致使从新 render,从新 render 又会从新把 Hooks 从新注册一次,因此 React 的上手难度更高一些
固然 React 对这些都有本身的解决方案,好比 useCallback,useMemo 等 Hook 的做用,这些官网都有介绍
Vue 中,“钩子”就是一个生命周期方法
Vue Composition API
的
setup()
晚于 beforeCreate 钩子,早于 created 钩子被调用
因为 React Hooks 会屡次运行,因此 render 方法必须遵照某些规则,好比:
不要在循环内部、条件语句中或嵌套函数里调用 Hooks
// React 文档中的示例代码:
function Form() { // 1. Use the name state variable const [name, setName] = useState('Mary'); // 2. Use an effect for persisting the form if (name !== '') { useEffect(function persistForm() { localStorage.setItem('formData', name); }); } // 3. Use the surname state variable const [surname, setSurname] = useState('Poppins'); // 4. Use an effect for updating the title useEffect(function updateTitle() { document.title = `${name} ${surname}`; }); // ... } 复制代码
若是想要在 name 为空时也运行对应的反作用, 能够简单的将条件判断语句移入 useEffect 回调内部:
useEffect(function persistForm() {
if (name !== '') { localStorage.setItem('formData', name); } }); 复制代码
对于以上的实现,Vue 写法以下:
export default {
setup() { // 1. Use the name state variable const name = ref("Mary"); // 2. Use a watcher for persisting the form if(name.value !== '') { watch(function persistForm() => { localStorage.setItem('formData', name.value); }); } // 3. Use the surname state variable const surname = ref("Poppins"); // 4. Use a watcher for updating the title watch(function updateTitle() { document.title = `${name.value} ${surname.value}`; }); } } 复制代码
Vue 中 setup() 只会运行一次,能够将 Composition API 中不一样的函数 (reactive、ref、computed、watch、生命周期钩子等) 做为循环或条件语句的一部分
但 if 语句 和 React Hooks 同样只运行一次,因此它在 name 改变时也没法做出反应,除非咱们将其包含在 watch 回调的内部
watch(function persistForm() => {
if(name.value !== '') { localStorage.setItem('formData', name.value); } }); 复制代码
useState
是 React Hooks 声明状态的主要途径
useState() 返回一个数组,第一项是 state,第二项是一个 setter 函数
const [name, setName] = useState("Mary");
const [age, setAge] = useState(25); console.log(`${name} is ${age} years old.`); 复制代码
useReducer
是个有用的替代选择,其常见形式是接受一个 Redux 样式的 reducer 函数和一个初始状态:
const initialState = {count: 0};
function reducer(state, action) { switch (action.type) { case 'increment': return {count: state.count + 1}; case 'decrement': return {count: state.count - 1}; default: throw new Error(); } } const [state, dispatch] = useReducer(reducer, initialState); dispatch({type: 'increment'}); // state 就会变为 {count: 1} 复制代码
useReducer 还有一种 延迟初始化 的形式,传入一个 init 函数做为第三个参数
Vue 使用两个主要的函数来声明状态:ref 和 reactive。
ref()
返回一个反应式对象,其内部值可经过其 value 属性被访问到。能够将其用于基本类型,也能够用于对象
const name = ref("Mary");
const age = ref(25); watch(() => { console.log(`${name.value} is ${age.value} years old.`); }); 复制代码
reactive()
只将一个对象做为其输入并返回一个对其的反应式代理
const state = reactive({
name: "Mary", age: 25, }); watch(() => { console.log(`${state.name} is ${state.age} years old.`); }); 复制代码
注意:
总结使用这两个函数的处理方式:
// toRefs() 则将反应式对象转换为普通对象,该对象上的全部属性都自动转换为 ref
function useFeatureX() { const state = reactive({ foo: 1, bar: 2 }) return toRefs(state) } const {foo, bar} = useFeatureX(); 复制代码
React 中的 useEffect Hook 容许在每次渲染以后运行某些反作用(如请求数据或使用 storage 等 Web APIs),并在下次执行回调以前或当组件卸载时运行一些清理工做
默认状况下,全部用 useEffect 注册的函数都会在每次渲染以后运行,但能够定义真实依赖的状态和属性,以使 React 在相关依赖没有改变的状况下(如由 state 中的其余部分引发的渲染)跳过某些 useEffect Hook 执行
// 传递一个依赖项的数组做为 useEffect Hook 的第二个参数,只有当 name 改变时才会更新 localStorage
function Form() { const [name, setName] = useState('Mary'); const [surname, setSurname] = useState('Poppins'); useEffect(function persistForm() { localStorage.setItem('formData', name); }, [name]); // ... } 复制代码
显然,使用 React Hooks 时忘记在依赖项数组中详尽地声明全部依赖项很容易发生,会致使 useEffect 回调 "以依赖和引用了上一次渲染的陈旧数据而非最新数据" 从而没法被更新而了结
解决方案:
eslint-plugin-React-Hooks
包含了一条 lint 提示关于丢失依赖项的规则
useCallback 和 useMemo
也使用依赖项数组参数,以分别决定其是否应该返回缓存过的( memoized)与上一次执行相同的版本的回调或值。
在 Vue Composition API 的状况下,可使用 watch() 执行反作用以响应状态或属性的改变。依赖会被自动跟踪,注册过的函数也会在依赖改变时被反应性的调用
export default {
setup() { const name = ref("Mary"); const lastName = ref("Poppins"); watch(function persistForm() => { localStorage.setItem('formData', name.value); }); } } 复制代码
Hooks 在处理 React 组件的生命周期、反作用和状态管理时表现出了心理模式上的彻底转变。 React 文档中也指出:
若是你熟悉 React 类生命周期方法,那么能够将 useEffect Hook 视为 componentDidMount、componentDidUpdate 及 componentWillUnmount 的合集
useEffect(() => {
console.log("This will only run after initial render."); return () => { console.log("This will only run when component will unmount."); }; }, []); 复制代码
强调的是,使用 React Hooks 时中止从生命周期方法的角度思考,而是考虑反作用依赖什么状态,才更符合习惯
Vue Component API
经过 onMounted、onUpdated 和 onBeforeUnmount
:
setup() {
onMounted(() => { console.log(`This will only run after initial render.`); }); onBeforeUnmount(() => { console.log(`This will only run when component will unmount.`); }); } 复制代码
故在 Vue 的状况下的心理模式转变动多在中止经过组件选项(data、computed, watch、methods、生命周期钩子等)管理代码,要转向用不一样函数处理对应的特性
React 团队聚焦于 Hooks 上的缘由之一,Custom Hooks 是能够替代以前社区中采纳的诸如 Higher-Order Components 或 Render Props 等提供给开发者编写可复用代码的,一种更优秀的方式
Custom Hooks 就是普通的 JavaScript 函数,在其内部利用了 React Hooks。它遵照的一个约定是其命名应该以 use 开头,以明示这是被用做一个 Hook 的。
// custom Hook - 用于当 value 改变时向控制台打印日志
export function useDebugState(label, initialValue) { const [value, setValue] = useState(initialValue); useEffect(() => { console.log(`${label}: `, value); }, [label, value]); return [value, setValue]; } // 调用 const [name, setName] = useDebugState("Name", "Mary"); 复制代码
Vue 中,组合式函数(Composition Functions)与 Hooks 在逻辑提取和重用的目标上是一致的在 Vue 中实现一个相似的 useDebugState 组合式函数
export function useDebugState(label, initialValue) {
const state = ref(initialValue); watch(() => { console.log(`${label}: `, state.value); }); return state; } // elsewhere: const name = useDebugState("Name", "Mary"); 复制代码
注意:根据约定,组合式函数也像 React Hooks 同样使用 use 做为前缀以明示做用,而且表面该函数用于 setup() 中
React 的 useRef 和 Vue 的 ref 都容许你引用一个子组件 或 要附加到的 DOM 元素。
React:
const MyComponent = () => {
const divRef = useRef(null); useEffect(() => { console.log("div: ", divRef.current) }, [divRef]); return ( <div ref={divRef}> <p>My div</p> </div> ) } 复制代码
Vue:
export default {
setup() { const divRef = ref(null); onMounted(() => { console.log("div: ", divRef.value); }); return () => ( <div ref={divRef}> <p>My div</p> </div> ) } } 复制代码
React Hooks 在每次渲染时都会运行,没有 一个等价于 Vue 中 computed 函数的方法。因此你能够自由地声明一个变量,其值基于状态或属性,并将指向每次渲染后的最新值:
const [name, setName] = useState("Mary");
const [age, setAge] = useState(25); const description = `${name} is ${age} years old`; 复制代码
Vue 中,setup() 只运行一次。所以须要定义计算属性,其应该观察某些状态更改并做出相应的更新:
const name = ref("Mary");
const age = ref(25); const description = computed(() => `${name.value} is ${age.value} years old`); 复制代码
计算一个值开销比较昂贵。你不会想在组件每次渲染时都计算它。React 包含了针对这点的 useMemo Hook
:
function fibNaive(n) {
if (n <= 1) return n; return fibNaive(n - 1) + fibNaive(n - 2); } const Fibonacci = () => { const [nth, setNth] = useState(1); const nthFibonacci = useMemo(() => fibNaive(nth), [nth]); return ( <section> <label> Number: <input type="number" value={nth} onChange={e => setNth(e.target.value)} /> </label> <p>nth Fibonacci number: {nthFibonacci}</p> </section> ); }; 复制代码
React 建议你使用 useMemo 做为一个性能优化手段, 而非一个任何一个依赖项改变以前的缓存值
React advice you to use useMemo as a performance optimization and not as a guarantee that the value will remain memoized
Vue 的 computed 执行自动的依赖追踪,因此它不须要一个依赖项数组
React 中的 useContext Hook,能够做为一种读取特定上下文当前值的新方式。返回的值一般由最靠近的一层 <MyContext.Provider>
祖先树的 value 属性肯定
// context object
const ThemeContext = React.createContext('light'); // provider <ThemeContext.Provider value="dark"> // consumer const theme = useContext(ThemeContext); 复制代码
Vue 中相似的 API 叫 provide/inject
。在 Vue 2.x 中做为组件选项存在,在 Composition API 中增长了一对用在 setup() 中的 provide 和 inject
函数:
// key to provide
const ThemeSymbol = Symbol(); // provider provide(ThemeSymbol, ref("dark")); // consumer const value = inject(ThemeSymbol); 复制代码
若是你想保持反应性,必须明确提供一个 ref/reactive 做为值
在 React 的状况下
因此你对做用域中的任何值拥有彻底访问能力,就像在任何 JavaScript 代码中的同样:
const Fibonacci = () => {
const [nth, setNth] = useState(1); const nthFibonacci = useMemo(() => fibNaive(nth), [nth]); return ( <section> <label> Number: <input type="number" value={nth} onChange={e => setNth(e.target.value)} /> </label> <p>nth Fibonacci number: {nthFibonacci}</p> </section> ); }; 复制代码
Vue 的状况下
因为要暴露的值极可能过多,返回语句也容易变得冗长
<template>
<section> <label> Number: <input type="number" v-model="nth" /> </label> <p>nth Fibonacci number: {{nthFibonacci}}</p> </section> </template> <script> export default { setup() { const nth = ref(1); const nthFibonacci = computed(() => fibNaive(nth.value)); return { nth, nthFibonacci }; } }; </script> } 复制代码
要达到 React 一样简洁表现的一种方式是从 setup() 自身中返回一个渲染函数。不过,模板在 Vue 中是更经常使用的一种作法,因此暴露一个包含值的对象,是你使用 Vue Composition API 时必然会多多遭遇的状况。
React 和 Vue 都有属于属于本身的“惊喜”,无优劣之分,自 React Hooks 在 2018 年被引入,社区利用其产出了不少优秀的做品,自定义 Hooks 的可扩展性也催生了许多开源贡献。
Vue 受 React Hooks 启发将其调整为适用于本身框架的方式,这也成为这些不一样的技术如何拥抱变化且分享灵感和解决方案的成功案例