• 场景
  • Hook 的时代意义
  • React Hooks
  • Vue Composition API
  • React Hooks vs Vue Composition API
  • 总结

场景

先了解什么是hook,拿react的介绍来看,它的定义是:

它能够让你在不编写 class 的状况下,让你在函数组件里“钩入” React state 及生命周期等个性的函数

对于 Vue 提出的新的书写 Vue 组件的 API:Composition API RFC,作用也是相似,所以咱们也能够像react一样叫做 vue hooks

  • 该 API 受到 React Hooks 的启发
  • 但有一些乏味的差别,躲避了一些react的问题

hook的时代意义

框架是服务于业务的,业务中很难防止的一个问题就是 -- 逻辑复用,同样的性能,同样的组件,在不一样的场合下,咱们有时候不得不去写2+次,为了防止耦合,起初各大框架纷纷想出了一些方法,比方 minix, render props, 高阶组件等实现逻辑上的复用,然而都有一些额定的问题

  • minix 与组件之间存在隐式依赖,可能产生抵触。偏向于减少更多状态,升高了利用的可预测性
  • 高阶组件 多层包裹嵌套组件,减少了复杂度和了解老本,对于外层是黑盒
  • Render Props 应用繁琐,不好保护, 代码体积过大,同样容易嵌套过深
  • ...

hook的呈现是划时代的,通过function抽离的形式,实现了简单逻辑的外部封装:

  • 逻辑代码的复用
  • 减小了代码体积
  • 没有this的懊恼

React Hooks

React Hooks 容许你 "勾入" 诸如组件状态和副作用解决等 React 性能中。Hooks 只能用在函数组件中,并容许咱们在不须要创立类的状况下将状态、副作用解决和更多货色带入组件中。

React 外围团队奉上的驳回策略是不拥护类组件,所以你能够降级 React 版本、在新组件中开始尝试 Hooks,并放弃既有组件不做任何更改

例子:

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>  );};
  • useState 和 useEffect 是 React Hooks 中的一些例子,使得函数组件中也能减少状态和运行副作用
  • 还有更多其余 hooks, 甚至能自定义一个,hooks 关上了代码复用性和扩展性的新大门

Vue Composition API

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 Hooks vs Vue Composition API

原理

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 会在组件每次渲染时候运行,而 Vue setup() 只在组件创立时运行一次

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

申明状态(Declaring state)

react

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

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

留神

  • 应用 ref 时须要 用 value 属性拜访其蕴含的值(除非在 template 中,Vue 容许你省略它)
  • 用 reactive 时,要留神如果应用了对象解构(destructure),会失去其反馈性。所以须要定义一个指向对象的援用,并通过其拜访状态属性。

总结应用这两个函数的解决形式:

  • 像在失常的 JavaScript 中申明根本类型变量和对象变量那样去应用 ref 和 reactive 即可
  • 只有用到 reactive 的时候,要记住从 composition 函数中返回反应式对象时得应用 toRefs()。这样做缩小了过多应用 ref 时的开销
// toRefs() 则将反应式对象转换为一般对象,该对象上的所有属性都主动转换为 reffunction useFeatureX() {  const state = reactive({    foo: 1,    bar: 2  })   return toRefs(state)} const {foo, bar} = useFeatureX();

如何跟踪依赖(How to track dependencies)

React 中的 useEffect hook 容许在每次渲染之后运行某些副作用(如申请数据或应用 storage 等 Web APIs),并在下次执行回调之前或当组件卸载时运行一些清理工作

默认状况下,所有用 useEffect 注册的函数都会在每次渲染之后运行,但能够定义实在依赖的状态和属性,以使 React 在相干依赖没有扭转的状况下(如由 state 中的其余局部引起的渲染)跳过某些 useEffect hook 执行

// 传递一个依赖项的数组作为 useEffect hook 的第二个参数,只有当 name 扭转时才会更新 localStoragefunction 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);    });  }}

拜访组件生命周期(Access to the lifecycle of the component)

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 的状况下的心理模式转变更多在进行通过<font color="#d10">组件选项</font>(data、computed, watch、methods、生命周期钩子等)治理代码,要转向用不同函数解决对应的个性

自定义代码(Custom code)

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() 中

Refs

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

附加的函数(Additional functions)

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 执行主动的依赖追踪,所以它不须要一个依赖项数组

Context 和 provide/inject

React 中的 useContext hook,能够作为一种读取特定上下文以后值的新形式。返回的值通常由最靠近的一层 <MyContext.Provider> 先人树的 value 属性确定

// context objectconst ThemeContext = React.createContext('light');// provider<ThemeContext.Provider value="dark">// consumerconst theme = useContext(ThemeContext);

Vue 中相似的 API 叫 provide/inject。在 Vue 2.x 中作为组件选项存在,在 Composition API 中减少了一对用在 setup() 中的 provide 和 inject 函数:

// key to provideconst ThemeSymbol = Symbol();// providerprovide(ThemeSymbol, ref("dark"));// consumerconst value = inject(ThemeSymbol);
如果你想放弃反馈性,必须明确提供一个 ref/reactive 作为值

在渲染上下文中裸露值(Exposing values to render context)

在 React 的状况下

  • 所有 hooks 代码都在组件中定义
  • 且你将在同一个函数中返回要渲染的 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 或 render 选项中定义模板
  • 第二,应用单文件组件,就要从 setup() 中返回一个蕴含了你想输入到模板中的所有值的对象
因为要裸露的值很可能过多,返回语句也容易变得简短
<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 时必然会多多遭逢的状况。

总结(Conclusion)

React 和 Vue都有属于属于本人的“惊喜”,无优劣之分,自 React Hooks 在 2018 年被引入,社区利用其产出了很多优良的作品,自定义 Hooks 的可扩展性也催生了许多开源奉献。

Vue 受 React Hooks 启发将其调整为实用于本人框架的形式,这也成为这些不同的技术如何拥抱变动且分享灵感和解决方案的胜利案例

最初, 心愿大家早日实现:成为前端高手的平凡幻想!
欢送交换~


参考

  1. Composition API RFC
  2. React hooks
  3. Comparing React Hooks with Vue Composition API