[译] 对比 React Hooks 和 Vue Composition API

原文:dev.to/voluntadpea…html

Vue 最近提出了 Composition API RFC,一种新的书写 Vue 组件的 API;该 API 受到 React Hooks 的启发,但有一些有趣的差别,也就是本文要探讨的内容。该 RFC 始自于在社区某些部分受到 大量非议 的以前一个叫作 Function-based Component API 的版本 -- 人们担忧 Vue 开始变得更复杂而不像你们最初喜欢它时那样是个简单的库了。vue

参阅《在 React 和 Vue 中尝鲜 Hooks》一文react

Vue 核心团队解决了围绕首个 RFC 的困惑并在新的版本中提出了一些引人关注的调整,也对提案改变的背后动机提供了进一步的看法。若是你对向 Vue 核心团队给出一些关于新提案反馈方面感兴趣,能够参与到 github.com/vuejs/rfcs/… 中。git

注意: Vue Composition API 仍在不断改进,会收到特性改变的影响。在 Vue 3.0 到来以前不要把 Vue Composition API 视为 100% 肯定的。github

React Hooks 容许你 "勾入" 诸如组件状态和反作用处理等 React 功能中。Hooks 只能用在函数组件中,并容许咱们在不须要建立类的状况下将状态、反作用处理和更多东西带入组件中。自从 2018 年被引入,社区对其一见钟情。npm

React 核心团队奉上的采纳策略是不反对类组件,因此你能够升级 React 版本、在新组件中开始尝试 Hooks,并保持既有组件不作任何更改。api

那么,开始学习 React Hooks 和 Vue Composition API 不一样的方面并记录某些咱们会遇到的区别吧 ⏯数组

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> ); }; 复制代码

useStateuseEffect 是 React Hooks 中的一些例子,使得函数组件中也能增长状态和运行反作用;稍后咱们还会看到其余 hooks,甚至能自定义一个。这些 hooks 打开了代码复用性和扩展性的新大门。性能优化

Vue Composition 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>

复制代码

Vue Composition API 围绕一个新的组件选项 setup 而建立。setup() 为 Vue 组件提供了状态、计算值、watcher 和生命周期钩子。

这个新的 API 并无让原来的 API(如今被称做 "Options-based API")消失。提案的当前迭代甚至容许开发者 结合使用新旧两种 APIs

注意:能够在 Vue 2.x 中经过 @vue/composition-api 插件尝试新 API。

代码的执行

Vue Composition API 的 setup() 晚于 beforeCreate 钩子(在 Vue 中,“钩子”就是一个生命周期方法)而早于 created 钩子被调用。这是咱们能够分辨 React Hooks 和 Vue Composition API 的首个区别, React hooks 会在组件每次渲染时候运行,而 Vue setup() 只在组件建立时运行一次。由于前者能够屡次运行,因此 render 方法必须遵照 某些规则,其中之一是:

不要在循环内部、条件语句中或嵌套函数里调用 Hooks

直接贴一段 React 文档中的代码来展现这一点:

function Form() {
  // 1. 使用 name 状态变量
  const [name, setName] = useState('Mary');

  // 2. 使用一个持久化表单的反作用
  if (name !== '') {
    useEffect(function persistForm() {
      localStorage.setItem('formData', name);
    });
  }
  // 3. 使用 surname 状态变量
  const [surname, setSurname] = useState('Poppins');

  // 4. 使用一个更新 title 的反作用
  useEffect(function updateTitle() {
    document.title = `${name} ${surname}`;
  });

  // ...
}

复制代码

React 在内部保持了对咱们用于组件中全部 hooks 的跟踪。在本例中,咱们用了四个 hooks。注意第一个 useEffect 调用是如何条件性的完成的,因为首次渲染中 name 会被默认值 'Mary' 赋值,条件会被评估为 true,React 也会知道须要按顺序的保持对全部四个 hooks 的跟踪。但如若在另外一次渲染中 name 为空会发生什么?在那种状况下,React 将不知道第二个 useState hook 该返回什么 😱(译注:React 默认靠 hook 调用的顺序为其匹配对应的状态,连续两个 useState 会形成后面的 hook 提早执行)。要避免相似的问题,强烈推荐在处理 React Hooks 时使用一个 eslint-plugin-react-hooks 插件,它也默认包含在了 Create React App 中。

那么若是咱们想要在 name 为空时也运行对应的反作用呢?能够简单的将条件判断语句移入 useEffect 回调内部:

useEffect(function persistForm() {
  if (name !== '') {
    localStorage.setItem('formData', name);
  }
});
复制代码

回过头看看 Vue,和上例等价的写法大概是这样:

export default {
  setup() {
    // 1. 使用 name 状态变量
    const name = ref("Mary");
    // 2. 使用一个 watcher 以持久化表单
    if(name.value !== '') {
      watch(function persistForm() => {
        localStorage.setItem('formData', name.value);
      });
    }
   // 3. 使用 surname 状态变量
   const surname = ref("Poppins");
   // 4. 使用一个 watcher 以更新 title
   watch(function updateTitle() {
     document.title = `${name.value} ${surname.value}`;
   });
  }
}
复制代码

由于 setup() 只会运行一次,咱们是能够将 Composition API 中不一样的函数 (reactiverefcomputedwatch、生命周期钩子等) 做为循环或条件语句的一部分的。

可是,if 语句一样只运行一次,因此它在 name 改变时也一样没法做出反应,除非咱们将其包含在 watch 回调的内部:

watch(function persistForm() => {
  if(name.value !== '') {
    localStorage.setItem('formData', name.value);
  }
});
复制代码

声明状态

useState 是 React Hooks 声明状态的主要途径。你能够向调用中传入一个初始值做为参数;而且若是初始值的计算代价比较昂贵,也能够将其表达为一个函数,这样就只会在初次渲染时才会被执行了。

const [name, setName] = useState("Mary");
const [age, setAge] = useState(25);
console.log(`${name} is ${age} years old.`);
复制代码

useState() 返回一个数组,第一项是 state,第二项是一个 setter 函数。一般可使用 Array destructuring 语法获得它们。

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 则因为其自然的反应式特性,有着不一样的作法。存在两个主要的函数来声明状态:refreactive

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),会失去其反应性(译注:由于是对整个对象作的代理)。因此你须要定义一个指向对象的引用,并经过其访问状态属性。

Composition API 提供了两个助手函数以处理 refs 和 reactive 对象。

若是必要的话,isRef() 可被用来条件性地获取 value 属性(好比 isRef(myVar) ? myVar.value : myVar)。

toRefs() 则将反应式对象转换为普通对象,该对象上的全部属性都自动转换为 ref。这对于从自定义组合式函数中返回对象时特别有用(这也容许了调用侧正常使用结构的状况下还能保持反应性)。

function useFeatureX() {
  const state = reactive({
    foo: 1,
    bar: 2
  })

  return toRefs(state)
}

const {foo, bar} = useFeatureX();
复制代码

RFC 中用 一整个章节 比较了 refreactive,在其结尾总结了使用这两个函数时可能的处理方式:

  1. 像你在正常的 JavaScript 中声明基本类型变量和对象变量那样去使用 refreactive 便可。在这种方式下,推荐使用一个 IDE 支持的类型系统。

  2. 只要用到 reactive 的时候,要记住从 composition 函数中返回反应式对象时得使用 toRefs()。这样作减小了过多使用 ref 时的开销,但并不会消减熟悉该概念的必要。

如何跟踪依赖

React 中的 useEffect hook 容许咱们在每次渲染以后运行某些反作用(如请求数据或使用 storage 等 Web APIs),并视须要在下次执行回调以前或当组件卸载时运行一些清理工做。默认状况下,全部用 useEffect 注册的函数都会在每次渲染以后运行,但咱们能够定义真实依赖的状态和属性,以使 React 在相关依赖没有改变的状况下(如由 state 中的其余部分引发的渲染)跳过某些 useEffect hook 执行。回到以前 Form 的例子,咱们能够传递一个依赖项的数组做为 useEffect hook 的第二个参数:

function Form() {
  const [name, setName] = useState('Mary');
  const [surname, setSurname] = useState('Poppins');
  useEffect(function persistForm() {
      localStorage.setItem('formData', name);
  }, [name]);

  // ...
}
复制代码

这样一来,只有当 name 改变时才会更新 localStorage。使用 React Hooks 时一个常见的 bug 来源就是忘记在依赖项数组中详尽地声明全部依赖项;这可能让 useEffect 回调以依赖和引用了上一次渲染的陈旧数据而非最新数据从而没法被更新而了结。幸运的是,eslint-plugin-react-hooks 也包含了一条 lint 提示关于丢失依赖项的规则。

useCallbackuseMemo 也使用依赖项数组参数,以分别决定其是否应该返回缓存过的( memoized)与上一次执行相同的版本的回调或值。

在 Vue Composition API 的状况下,可使用 watch() 执行反作用以响应状态或属性的改变。多亏了 Vue 的反应式系统,依赖会被自动跟踪,注册过的函数也会在依赖改变时被反应性的调用。回到例子中:

export default {
  setup() {
    const name = ref("Mary");
    const lastName = ref("Poppins");
    watch(function persistForm() => {
      localStorage.setItem('formData', name.value);
    });
  }
}
复制代码

在 watcher 首次运行后,name 会做为一个依赖项被跟踪,而稍后当其值改变时,watcher 会再次运行。

访问组件生命周期

Hooks 在处理 React 组件的生命周期、反作用和状态管理时表现出了心理模式上的彻底转变。React 社区中的一位活跃分子 Ryan Florence,曾表示从类组件切换到 hooks 有一个心理转换过程,而且 React 文档中也指出:

若是你熟悉 React 类生命周期方法,那么能够将 useEffect Hook 视为 componentDidMountcomponentDidUpdatecomponentWillUnmount 的合集

但其实也有可能控制 useEffect 什么时候运行,并让咱们更接近生命周期中运行反作用的心理模式:

useEffect(() => {
  console.log("这段只在初次渲染后运行");
  return () => { console.log("这里会在组件将要卸载时运行"); };
}, []);
复制代码

但要再次强调的是,使用 React Hooks 时中止从生命周期方法的角度思考,而是考虑反作用依赖什么状态,才是更符合习惯的。顺便一提的是,Svelte 的建立者 Rich Harris 发表了他在 NYC React meetup 上演讲的 some insightful slides,其间他探究了 React 为了未来的新特性(好比 concurrent mode)可用所作的妥协以及 Svelte 何其的区别。这将帮助你理解从思考反作用发生在组件生命周期何处到 做为渲染自己一部分的反作用 的转变。来自 React 核心团队的 Sebastian Markbåge 写的 further expands here 也解释了 React 前进的方向和为相似 Svelte 或 Vue 式的反应性系统做出的妥协。

另外一方面的 Vue Component API,让咱们经过 onMountedonUpdatedonBeforeUnmount 等仍能够访问 生命周期钩子 (Vue 世界中对生命周期方法的等价称呼):

setup() {
  onMounted(() => {
    console.log(`这段只在初次渲染后运行`);
  });
  onBeforeUnmount(() => {
    console.log(`这里会在组件将要卸载时运行`);
  });
}
复制代码

故而在 Vue 的状况下的心理模式转变动多在中止经过组件选项(datacomputed, watchmethods、生命周期钩子等)管理代码这点上,要转向用不一样函数处理对应的特性。RFC 包含一个经过选项 vs. 经过逻辑关注点管理代码的 示例和对照大全

自定义代码

React 团队意图聚焦于 Hooks 上的一方面,是比之于先前社区中采纳的诸如 Higher-Order ComponentsRender Props 等替代方式,提供给开发者编写可复用代码的更佳方式。Custom Hooks 正是他们带来的答案。

Custom Hooks 就是普通的 JavaScript 函数,在其内部利用了 React Hooks。它遵照的一个约定是其命名应该以 use 开头,以明示这是被用做一个 hook 的。

export function useDebugState(label, initialValue) {
  const [value, setValue] = useState(initialValue);
  useEffect(() => {
    console.log(`${label}: `, value);
  }, [label, value]);
  return [value, setValue];
}
复制代码

这个 Custom Hook 的小例子可被做为一个 useState 的替代品使用,用于当 value 改变时向控制台打印日志:

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

// 在其余某处:
const name = useDebugState("Name", "Mary");
复制代码

注意:根据约定,组合式函数也像 React Hooks 同样使用 use 做为前缀以明示做用,而且表面该函数用于 setup()

Refs

React 的 useRef 和 Vue 的 ref 都容许你引用一个子组件(若是是 React 则是一个类组件或是被 React.forwardRef 包装的组件)或要附加到的 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>
    )
  }
}
复制代码

注意 Vue 2.x 且用 @vue/composition-api 插件的状况下,不支持setup() 返回的渲染函数中经过 JSX 分配模版 refs, 但根据 当前的 RFC,以上语法在 Vue 3.0 中是合法的。

React 中的 useRef Hook 不止对于取得 DOM 元素的访问有用。亦可用在你想保持在渲染函数中但并非 state 一部分的(也就是它们的改变触发不了从新渲染)任何类型的可变值(mutable value)上。可将这些可变值视为类组件中的 "实例变量" 。 这是一个例子:

const timerRef = useRef(null);
useEffect(() => {
  timerRef.current = setInterval(() => {
    setSecondsPassed(prevSecond => prevSecond + 1);
  }, 1000);
  return () => {
    clearInterval(timerRef.current);
  };
}, []);

return (
  <button onClick={() => { clearInterval(timerRef.current); }} > 中止 timer </button>
)
复制代码

在 Vue Composition API 中,如咱们在几乎全部文中以前的例子中所见,ref 可被用于定义反应式状态。使用 Composition API 的时候,模版 refs 和反应式 refs 是一致的。

附加的函数

因为 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`);
复制代码

照例,记住 refs 是容器,而值要经过访问 value 属性得到 :p

若是计算一个值开销比较昂贵又如何呢?你不会想在组件每次渲染时都计算它。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> ); }; 复制代码

useMemo 一样指望一个依赖项数组以获知其在什么时候应该计算一个新值。React 建议你使用 useMemo 做为一个性能优化手段而非一个直到任何一个依赖项改变以前的缓存值

做为一个补充说明:Kent C. Dodds 有一篇很是棒的文章 "useMemo 和 useCallback" 说明了不少 useMemouseCallback 非必要的场景。

Vue 的 computed 执行自动的依赖追踪,因此它不须要一个依赖项数组。

useCallback 相似于 useMemo,但它是用来缓存一个回调函数的。事实上 useCallback(fn, deps) 等价于 useMemo(() => fn, deps)。其理想用例是当咱们须要在屡次渲染间保持引用相等性时,好比将回调传递给一个用 React.memo 定义的已优化子组件,而咱们想要避免其没必要要的重复渲染时。

鉴于 Vue Composition API 的自然特性,并无等同于 useCallback 的函数。setup() 中的任何回调函数都只会定义一次。

Context 和 provide/inject

React 中的 useContext hook,能够做为一种读取特定上下文当前值的新方式。返回的值一般由最靠近的一层 <MyContext.Provider> 祖先树的 value 属性肯定。其等价于一个类中的 static contextType = MyContext ,或是 <MyContext.Consumer> 组件。

// context 对象
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() 中的 provideinject 函数:

// key to provide
const ThemeSymbol = Symbol();

// provider
provide(ThemeSymbol, ref("dark"));

// consumer
const value = inject(ThemeSymbol);
复制代码

注意,若是你想保持反应性,必须明确提供一个 ref/reactive 做为值。

在渲染上下文中暴露值

在 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 的状况下,你要在 templaterender 选项中定义模板;若是你使用单文件组件,就要从 setup() 中返回一个包含了你想输出到模板中的全部值的对象。因为要暴露的值极可能过多,你的返回语句也容易变得冗长,这一点在 RFC 的 Verbosity of the Return Statement 章节 中有所说起:

<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() 自身中返回一个渲染函数:

export default {
  setup() {
    const nth = ref(1);
    const nthFibonacci = computed(() => fibNaive(nth.value));
    return () => (
      <section> <label> Number: <input type="number" vModel={nth} /> </label> <p>nth Fibonacci number: {nthFibonacci}</p> </section> ); } }; 复制代码

不过,模板在 Vue 中是更流行的一种作法,因此暴露一个包含值的对象,是你使用 Vue Composition API 时必然会多多遭遇的状况。

总结

每一个框架都有惊喜时刻。自从 React Hooks 在 2018 年被引入,社区利用它们杰做频出,而且自定义 Hooks 的可扩展性也催生了 许多开源贡献 ,让咱们能够轻易的加入本身的项目中。Vue 受 React Hooks 启发并将其调整为适用于其框架的方式,这也成为这些不一样的技术如何拥抱变化并分享灵感和解决方案的成功案例。我对 Vue 3 的到来已经急不可耐,迫切想看到它的解锁能带来的可能性了。



--End--

搜索 fewelife 关注公众号

转载请注明出处

相关文章
相关标签/搜索