Vue Composition API新体验以及与React Hooks的比较

in FEcoding with 0 comment

一 如何使用vue3的新特性

1. 创建项目

由于vue3的新特性需要@vue/cli v4版本以上支持,因此如果想要体验vue3的新特性的话需要先把@vue/cli升级到v4版本哦

npm install -g @vue/cli 
# OR 
yarn global add @vue/cli

然后使用vue -V查看当前@vue/cli的版本,如果已经变成4.x.x,则说明已经更新成功。(我自己更新后一直还是显示3.x.x,先把@vue/cli卸载后重新安装也是3.x.x,重启后才变成4.x.x,如果遇到了一样的情况,可以先试下重启)
确定更新完成后,执行以下命令,创建一个vue项目,并增加vue-next支持。

vue create appname
vue add vue-next
# 如果需要Typescript语言支持,则增加以下这一行
vue add typescript

2. 使用vue3语法

vue3已经不支持@component了,使用template和setup来定义组件,同时为了做更好的类型检查,使用defineComponent包装setup.

<template>
  <div id="app">
    <Parent :message="message"/>
  </div>
</template>

<script lang="ts">
import { defineComponent, reactive } from 'vue';
import Parent from '@/components/Parent.vue';

export default defineComponent({
  components: {Parent},
  setup(){
    return reactive({
      message: "Hello, Vue3.0 with Typescript"
    })
  }
})
</script>

二 Vue Compostion API

Vue Compostion API是vue3 beta新添加的API,不会影响现有的API,其目的是用来提供可复用逻辑。2.x版本的用于复用的混入(mixin) 将不再作为推荐使用, Composition API可以实现更灵活且无副作用的复用代码。
我们先来简单看下Vue提供的这些VCA分别如何使用

reactive

等价于2.x版本中的Vue.observable,响应式reactive使得数据可以在渲染期间被使用,当reactive数据发生变化时视图会自动刷新。

import { defineComponent, reactive } from 'vue';

interface ParentReactive {
    message: string,
    count: number
}

export default defineComponent({
    setup(){
        const data:ParentReactive = reactive<ParentReactive>({
            message: "This is a message from parent",
            count: 1
        })
    }
})

在DOM中渲染呈现某些东西视为"副作用",为了基于reactive状态自动地应用与重应用副作用,我们可以使用watchEffect.

watchEffect

watchEffect约等于 2.x中的watch,它返回一个暂停监听的函数,入参为一个函数,传入函数会立即执行,并会跟踪所有它使用到的reactive属性,当reactive属性发生变化时执行传入的函数。如下,当count>=5时,调用watchEffect返回的停止函数,可以停止对其使用到的reactive属性count的监听,count继续++,但是watchEffect传入的监听函数不会再被执行。

const stop = watchEffect(()=>{
    console.log("watchEffect");
    console.log(data.count)
})

function addCount(){
    data.count++;
    countRef.value++;
    if(data.count >= 5) {
        // 当count>=5时,停止监听
        stop();
        stopWatch();
    }
}

watch

watch相当于2.x版本中的watch选项

const stopWatch = watch<number>(() => data.count, (newCount, oldCount) => {
        console.log(newCount, oldCount);
    })

ref

2.x版本只存在template refs,现在可以用于logical state和template refs了。ref的作用和reactive一样,由于Javascript本身的语言限制,我们永远都无法对基本类型数据的变化进行追踪,因此会想到把这些基本类型数据放入一个对象中以实现变化追踪,Ref正是对这一功能的实现。它把基本数据类型封装到对象的value中.

const countRef:Ref<number> = ref<number>(100);
console.log(countRef.value);

注意以下情况可以直接使用ref,而不需要ref.value

  1. ref在渲染上下文中会直接暴露出内部的value值,因此在template中直接使用{{ countRef }}

  2. 当ref在reactive中也可以直接使用,不需要.value取值

    const state = reactive({
    count: 0,
    double: computed(() => state.count * 2)})

computed

计算属性,使用示例如下:

const computedCount:ComputedRef<number> = computed<number>(() => data.count * 10);

其中, ComputedRef是对计算属性ref化的包装,是一个泛型接口。computed函数之所以要返回ComputedRef是为了对实际计算的值做包装,因为JS中的primitive类型是传值的。

增加VCA特性的动机

Vue会在3.0版本中做出VCA的变更,其动机主要在于:

  1. vue虽然很适合中小型项目的开发,但是随着项目的发展,组件也逐渐变得复杂,如果要在组件之间解耦与复用逻辑则十分不易,2.x版本需要借助this上下文,而VCA却可以只依赖于传入的参数,可以直接把一个组件当成函数来import就可以复用这个组件的逻辑。(可以结合下面提供的useMousePosition来理解)
  2. 从代码组织来看,对于非个人开发的代码,在理解时,我们会更倾向于从“这个组件正在处理X,Y,Z”开始而非是”这个组件有这些属性、计算属性以及方法“。
  3. vue2.x所使用的mixin会带来属性名冲突、命名空间冲突等问题。

说了那么多概念性的东西,下面我们把官方的useMousePosition改写为Typescript版本来解释下如何使用Composition API实现逻辑复用。
useMousePosition封装了一个监听鼠标位置的功能。

import { reactive, onMounted, onUnmounted, toRefs } from 'vue';
interface Position {
    x: number;
    y: number
}
export default function useMousePosition() {
    const position: Position = reactive<Position>({
        x: 0,
        y: 0
    });
    function getPostion(e: MouseEvent){
        position.x = e.pageX;
        position.y = e.pageY;
    }
    onMounted(() => {
        window.addEventListener('mousemove', getPostion);
    })
    onUnmounted(() => {
        window.removeEventListener('mousemove', getPostion);
    })
    return toRefs(position); //toRefs防止外部解构!! 为了保持reactivity,必须要保持对返回对象的引用! toRefs会使得x和y都被ref包装
}

现在在外部的Parent组件调用这个组件(或者也可以说是函数)

const { x, y } = useMousePosition();

看,现在我们只需要简单引入这个组件就可以调用了,不需要通过mixin以及考虑命名冲突等问题。

三 VS React Hooks

1. 代码的执行不同

vue3的setup函数是在组件的beforeCreate hook和created hook之间被调用的,属于生命周期函数。首先这是和react hook的第一个不同点,setup函数只在组件被创建时执行一次,而react hook却是在每次渲染时都会执行,因此react hook有以下规则

Don’t call Hooks inside loops, conditions, or nested functions.

2. 状态声明不同

React Hooks主要是使用useSate来声明状态,返回的是一个数组。(除此之外还有useRudcer),而Vue3.0有ref和reactive两种声明方法。ref和reactive其实并没有多大的不同,只是ref可以传入基本属性类型或者对象,它们都是深度reactive的。reactive只能传入对象,需要注意的是reactivity会影响到所有的嵌套属性。RFC指出:

  1. Use ref and reactive just like how you'd declare primitive type variables and object variables in normal JavaScript. It is recommended to use a type system with IDE support when using this style.
  2. Use reactive whenever you can, and remember to use toRefs when returning reactive objects from composition functions. This reduces the mental overhead of refs but does not eliminate the need to be familiar with the concept.

3.跟踪变化的方式不同

React Hooks使用useEffect来执行副作用,作用时机包括每次渲染之后以及依赖数据发生变化,或者是当组件unmount的时候执行返回的函数。

useEffect(() => {
  console.log("This will only run after initial render.");
  return () => { console.log("This will only run when component will unmount."); };}, 
  [ ]);

useCallback 和 useMemo同样使用依赖参数数组来决定是否返回和上次执行结果相同记忆版本的回调或者值。

Vue使用watchEffect和watch自动追踪依赖值变化.

4.可访问组件生命周期不同

React Hooks代表了心智模型的完全转换,Hooks相当于
componentDidMount, componentDidUpdate, 和 componentWillUnmount的结合。而Vue Composition API仍然可以访问生命周期函数。

setup() {
  onMounted(() => {
    console.log(`This will only run after initial render.`); 
  });
  onBeforeUnmount(() => {
    console.log(`This will only run when component will unmount.`);
  });}

5. Refs

二者都可以用于template refs 和 logical refs,react的template refs示例如下:

const MyComponent = () => {
  const divRef = useRef(null);
  useEffect(() => {
    console.log("div: ", divRef.current)
  }, [divRef]);

  return (
    <div ref={divRef}>
      <p>My div</p>
    </div>
  )}

React的logical refs可以用来保留一些在渲染之间的可变值,但不属于状态的一部分(因此这些值发生变化的时候不会触发重渲染),示例如下:

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

return (
  <button
    onClick={() => {
      clearInterval(timerRef.current);
    }}
  >
    Stop timer
  </button>)

而Vue Composition API中的 ref 和 reactive 是统一的,均是用来定义响应式状态。

6.额外的函数

当状态B依赖于状态A生成时,由于React Hooks是会执行多次的,因此不需要computed函数,而在Vue中,setup函数只会执行一次,所以需要监听依赖状态的变化。
假如状态B依赖于状态A生成的这个计算函数是比较昂贵的操作,React可以使用 useMemo 和 useCallback 进行优化,但是Vue由于本身设计为setup函数只会执行一次,所以不需要 useMemo 和 useCallback的等价

7. Context 和 provide/inject

两者使用方式不同,React的使用方式如下:

// context object
const ThemeContextReact.createContext('light');

// provider
<ThemeContext.Provider value="dark">

// consumer
const theme = useContext(ThemeContext);

VCA则是使用 provide 和 inject API

// key to provideconst 
ThemeSymbol = Symbol();

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

// consumer
const value = inject(ThemeSymbol);

8. 在渲染上下文中暴露值的方式不同

React由于代码都是在同一个组件中的定义,因此可以完全访问作用域中的所有值。

Vue由于template的语法,需要return返回dom所需要用到的变量。

<template>
    <div>{{ data.message }}</div>
    <div>{{ data.count }}</div>
</template>
export default defineComponent({
    setup(){
        const data:ParentReactive = reactive<ParentReactive>({
            message: "This is a message from parent",
            count: 1
        })
        return {
            data
        }
    }
})

或者也可以在setup函数中返回render函数

export default {
    setup(){
        const data:ParentReactive = reactive<ParentReactive>({
            message: "This is a message from parent",
            count: 1
        })
        return () => <div>
            <div>{{ data.message }}</div>
             <div>{{ data.count }}</div>
        </div>
    }
}

总结

Vue从React Hooks那里汲取了灵感,并以一种适合框架的方式对其进行了调整,为所有这些不同技术如何拥抱变化并共享思想和解决方案的做出一个示例。 等不及 Vue3 的到来,看看它能释放出什么潜力呢?

参考文章:

Vue Composition API :

https://composition-api.vuejs.org/#usage-alongside-existing-api

https://dev.to/voluntadpear/comparing-react-hooks-with-vue-composition-api-4b32

Responses