目前三大主流前端框架中,Angular 作为单页应用的先行者,虽然目前市场占有份额正在被 Vue 和 React 慢慢抢占,但是不可否认的是,Angular 为后来的 React 和 Vue 提供了很多借鉴经验。比如双向绑定、组件解耦、响应式开发,包括配合 TypeScript 的使用等等,它们都给后来的两个框架提供了一个很好的发展方向。但是今天主角是 Vue 和 React,准确的来说是 Vue 3.0 与 React Hook,作为两个框架功能迭代的新版本,它们都与其之前版本有很大的不同,并且也争议不断,那么为什么会有这么多争议呢?就让我们共同探究一下吧。
设计动机
其实每个框架的版本更新目的只有两个:解决问题和解决更多问题。也只有这样,才能让框架不断保持生命力与活力。
Vue 3.0
在 Vue 2.0 的代码模式中,随着功能的不断增长,复杂的代码变得越来越难维护,这种情况在接受别人代码时尤为显著。这种情况出现的根本原因是 Vue 的现有 API 通过选项组织代码,但是在实际的业务场景中我们通常优先选择使用逻辑来组织代码。除此之外,也缺乏一种较为纯净的组件之间的复用逻辑机制,同样的类型推断得不友好,这些问题的出现都让 Vue 需要提出一种更优的方案,于是,Vue 3.0 的设计开始了。
React Hook
传统的 class 类组件中,组件之间的复用状态逻辑很难,由 providers、consumers、高阶组件、render、props 等其他抽象层组成的组件会形成“嵌套地狱”,导致代码的复用性变差,同样可读性也变得更差,而这样最终导致这些复杂组件变得难以理解,项目越到后期,开发维护会变得越来越艰难。而 hook 的设计初衷就是为了解决这些问题。
新特性
通过对两个框架设计动机的比较,我们很容易发现,两个框架需要解决的都是要提高代码的复用性与可读性等问题。所以这两个框架的更新特性与模式就好像两辆从不同起点出发的汽车,要开向相同的终点,所以有些路肯定是一样的。那接下来让我们一起看看究竟有哪些相同与不相同的地方吧。
Vue 3.0
Vue 3.0 的版本发布在社区里引起了巨大的轰动,社区里也出现大片的批评。其中声音最多的莫过于说 Vue 3.0 是抄袭 React Hook,新特性的加入使得 Vue 越来越像 React 了,没有了自己的特征。其实尤大大自己也说过,Vue 3.0 有些设计的确借鉴了 React Hook 的设计思想,因为这些特性 React 的确做得很不错。其实作为一个开源框架设计者而言,愿意去借鉴别人的优点,并且认真去学习,这才能是框架不断成熟的根本。再者说,框架其实只是使用工具,我们使用它们是为了提效的,也没必要去吹毛求疵。好了,说了这么多,让我们来看看 Vue 3.0 到底有哪些新特性吧。
1. setup 函数
在新增的 Composition API 的新特性中,setup 函数就是其统一的入口,setup 函数会在 beforeCreate、created 之前执行。而在 Vue 3.0 中,也没有了 beforeCreate、created 这两个生命周期,所以我们也可以将 setup 当作一个生命周期函数来看。为什么说这是入口呢?因为在 Vue 3.0 中,我们不再单独写 data、methods、watch、computed 等方法,这些方法全部放在 setup 函数中,同样的也不再需要使用 this 去拿取数据,通过 setup 返回即可使用。具体用例如下:
<template>
<div class="example">
<div class="content">
<span>{{ count }}</span>
<span>{{ pushNumber }}</span>
</div>
<div class="action">
<button @click="countNumber">
增加
</button>
<button @click="reduce">
减少
</button>
<button @click="push">
添加数组
</button>
<button @click="resetArray">
重置数组
</button>
</div>
</div>
</template>
<script lang="ts">
import { reactive, ref, toRefs } from 'vue';
export default {
props: {
value: String,
},
setup() {
const count: number = ref<number>(0);
const array: any = reactive({
pushNumber: [],
});
const countNumber = () => {
console.log(this, 'this');
count.value += 1;
};
const reduce = () => {
const newValue = count.value;
count.value -= 1;
return newValue;
};
const push = () => {
array.pushNumber.push(1);
};
const resetArray = () => {
array.pushNumber.splice(0, array.length);
};
return {
count,
...toRefs(array),
countNumber,
reduce,
push,
resetArray,
};
},
};
</script>
在上述代码的样例中,我们可以看到,在 Vue 3.0 中已经没有了单独地 data、method、computed 等方法,而是全部整合在了 setup 函数中,并且眼尖的朋友会立马发现,this 不见了,在 setup 中将数据返回之后便能直接调用,而且为了让大家不再混淆 this 的用法,Vue 3.0 中直接将 this 置为了 undefined。看到 setup 的这个函数,要是熟悉 React Hook 的朋友会立马说,这不就是 useEffect 函数吗?ref 和 reactive 不就和 setState 差不多吗?可以说 Vue 3.0 的这部分的确借鉴了 React Hook 的模式,但是又有些不同,具体的我们稍后可以通过 React Hook 的代码进行比对。通过上述代码,可能大家会不明白,这个 ref 和 reactive 又是个啥?那就让我们来共同探讨一下吧。
-
ref(响应式变量)
在 Vue 3.0 中,没有了
data声明数据,多了一个ref,很多人会一脸迷茫,这是个什么东西,这哪里还是 Vue。但是用了之后就会发现 ref 原来这么好用。我们来看看官方文档是怎么解释 ref 的:“ref 接收参数并将其包裹在一个带有value、property的对象中返回,然后可以使用该 property”。
再来看个例子:
import { ref } from 'vue';
const counter = ref(0);
console.log(counter); // { value: 0 }
console.log(counter.value); // 0
counter.value += 1;
console.log(counter.value); // 1
从例子里可以很清楚地看到,counter 是一个对象,ref 将值封装在了一个对象里面,通过 counter.value 的方法拿到值,那么这就产生疑问了,为什么需要放置在对象里面呢?因为在 JavaScript 中,简单数据类型(如 String 和 Number)是通过值而非引用传递的,在值的外面封装一个对象,就可以保证其在传递时不会在某个时刻失去其响应性。我们可以通过下面的图来理解一下:

总的来说,ref 的作用就是让视图层随时能知道数据的更改,并且做出相应的渲染,达到响应式的引用。
-
reactive(可监听所有变量)
上面说到
ref是监听简单数据类型,那么reactive就是用来监听引用数据类型,传入一个对象并返回一个基于原对象的响应式代理,即返回一个Proxy,相当于 Vue 2.0 版本中的Vue.observer。但是在以往的 Vue 2.0 中,它只会侦听对象的属性,并不能侦听对象。所以 reactive 基于 ES2015 Proxy 实现对数据对象的响应式处理,即在 Vue 3.0 可以往对象中添加属性,并且这个属性也会具有响应式的效果。
同样的我们用个例子来看看如何使用 reactive:
import { reactive,toRefs } from 'vue';
export default {
const state = reactive({
newArr: [1, 23],
newStr: { a: 1, b: 2 },
});
console.log(state); // { newArr:[1, 23], newStr:{ a: 1, b: 2 } };
console.log(state.newArr); // [1, 23]
state.newArr.splice(0, 1);
console.log(state.newArr); // [23]
return{
...toRefs(state),
};
};
但是上述代码中又出现了一个 toRefs,这又是什么呢?让我们来看一下 toRefs 的源码:
function toRefs(object) {
if ((process.env.NODE_ENV !== 'production') && !isReactive(object)) {
console.warn(`toRefs() expects a reactive object but received a plain one.`);
};
for (const key in object) {
ret[key] = toProxyRef(object, key);
};
return ret;
};
function toProxyRef(object, key) {
return {
_isRef: true,
get value() {
return object[key];
},
set value(newVal) {
object[key] = newVal;
};
};
};
从源码中可以清楚地看到,toRefs 的作用是在原有 Proxy 对象的基础上,返回了一个普通的带有 get 和 set 的对象。这样就解决了 Proxy 对象遇到解构和展开运算符后,失去响应的情况的问题。
注意:setup 函数只会执行一次,因为 ref 和 reactive 可以监听响应式对象,所以每次值变化时,由 template 编译而成的 render 函数都会重新执行,这个函数会收集这些值作为依赖,下一次值更新时,函数就会重新执行。
2. 生命周期函数
Vue 3.0 的生命周期改动我们通过下面的图片就能很直观的了解:
因为 setup 是围绕 beforeCreate 与 created 的生命周期钩子运行的,所以除去这两个生命周期函数,其他生命周期都只是加上了 on,功能都没有很大变化。当然,这些生命周期也都直接写在 setup 函数中。
具体使用如下:
export default {
setup() {
// 原来的 mounted 生命周期
onMounted(() => {
console.log('Component is mounted!');
});
},
};
3. 父子组件传值
在 Vue 2.0 中,我们的父子组件传值主要是通过 props 与 emit 在 Vue 3.0 中这一点没有变,但是同样的,还是在 setup 函数中进行的,让我们来看看个例子:
父组件:
<template>
<div class="example">
<Test :date="date" :name="name" />
</div>
</template>
<script lang="ts">
import { ref } from 'vue';
import Test from './components/test.vue';
export default {
components:{
Test,
},
setup() {
const date = ref<String>("2021");
const name = ref<String>("古天乐");
return {
date,
name,
};
},
};
</script>
子组件:
<template>
<div class="example">
<span class="content">{{ newName }}</span>
<span class="content">{{ newDate }}</span>
</div>
</template>
<script lang="ts">
import { toRefs } from 'vue';
export default {
props: {
date: String,
name: String,
},
setup(props: any) {
const { date, name } = toRefs(props);
const newName = name.value;
const newDate = date.value;
return {
newName,
newDate,
};
},
};
</script>
如上代码所示,如果父组件向子组件传递多个值,子组件接收之后,如果想要使用解构的方法,必须使用 toRefs 来确保其不丢失响应性。如果不想额外定义变量,也可以直接 return 回一个 props,然后直接使用 props.xx 即可。
React Hook
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。这个更新简直太棒了,不用再写 class, 也没有 this 让我们晕头转向,直接函数式编程一把梭,那么下面让我们看看 React Hook 的一些新特性吧。
1. useState
在新特性中,setState 控制了页面的渲染。所有的 setState 存储在调用栈中,然后根据 state 的顺序进行调用,所以这也是为什么在 hook 中,强制要求 setState 按顺序编写,以确保正常渲染。
具体用法如下:
import * as React from 'react';
import { useState } from 'react';
const Test = () => {
const [count, setCount] = useState(0);
// 增加
const addCount = () => {
setCount(count + 1);
};
// 重置
const reset = () => {
setCount(0);
};
return (
<div className="content">
<span className="text">{ count }</span>
<button onClick={ addCount } style={{ marginLeft: '12px' }}>
增加
</button>
<button onClick={ reset } style={{ marginLeft: '12px' }}>
重置
</button>
</div>
);
};
export default Test;
从上面的代码我们可以很清楚地发现,我们通过定义初始的 state,然后通过 setState 去更改其状态,而只有 setState 才能引起视图的更改。关于这一点,不知道大家有没有发现,Vue 3.0 的 ref 与 reactive 与 setState 很像?这也是为什么大家说 Vue 3.0 抄袭 React Hook 的其中一个原因了,但是很大的一个不同点是,Vue 3.0 将数据定义细分了,划分了简单与复杂数据类型,并且是实时响应式的,不像 setState 是按照顺序调用的,这也是 Vue 3.0 的一个优化点。
2. useEffect
useEffect 就是一个 Effect Hook,给函数组件增加了操作副作用的能力。它跟 class 组件中的 componentDidMount、componentDidUpdate 和 componentWillUnmount 具有相同的用途,只不过被合并成了一个 API。那么让我们来通过下面的代码样例来认识一下吧:
import * as React from 'react';
import { useEffect, useState } from 'react';
const Father = () => {
const [count, setCount] = useState(0);
useEffect(() => {
console.log(count,'count');
}, [count]);
// 增加
const addCount = () => {
setCount(count + 1);
};
// 重置
const reset = () => {
setCount(0);
};
return (
<div className="content">
<span className="text">{count}</span>
<button onClick={addCount} style={{ marginLeft: '12px' }}>
增加
</button>
<button onClick={reset} style={{ marginLeft: '12px' }}>
重置
</button>
</div>
);
};
useEffect 的作用在于你可以告诉 React 组件需要在渲染后执行某些操作,但是为什么要放在组件之内呢?因为这样让我们可以在 effect 中直接访问 count、state 变量(或其他 props)。与 Vue 3.0 不太一样的是,useEffect 在第一次和每次更新之后都会渲染,而如何去控制它的渲染呢?关键在于其第二个参数,第二个参数监听某个属性,只有当这个属性发生变化之后,才会进行页面的重新渲染。
3. 父子组件传值
由于 React 是单向数据流,所以只能父传子单向传递,所以这一方面与 Vue 相比麻烦了许多,具体例子如下:
<!-- 父组件 -->
import * as React from 'react';
import { useEffect, useState } from 'react';
import './index.css';
import Children from '../children/index';
const Father = () => {
const [color, setColor] = useState('red');
// 更改颜色
const changeColor = () => {
setColor(color === 'red' ? 'green' : 'red');
};
return (
<div className="content">
<button onClick={changeColor} style={{ marginLeft: '12px' }}>
更改子组件颜色
</button>
<Children value={color} />
</div>
);
}
export default Father;
<!-- 子组件 -->
import * as React from 'react';
import { useEffect } from 'react';
import './index.css';
const Children = (props) => {
const { value } = props;
useEffect(() => {
if (!value) {
return 'props值不存在';
};
}, [value]);
return(
<div className="box" style={{backgroundColor: value}}>
子组件
</div>
);
};
export default Children;
在上面的例子中我们可以很清楚地看到 useEffect 的作用,一旦 props 的值发生变化,那么 useEffect 就可以监听 props 值变化。其实这部分 Vue 3.0 与 Hook 也是比较相似的,但是不可否认的是,这样的处理方式让数据看起来更直观,代码结构也比较清晰明了。那么父传子可以通过 props 来传值,那么子传父该怎么处理呢?
a. useRef
<!-- 父组件 -->
import * as React from 'react';
import { useRef } from 'react';
import ChildrenRef from '../childrenRef/index';
const FatherRef = () => {
const childRef = useRef(null);
const handleGetChildData = () => {
const data = childRef.current.getFromData();
console.log('data', data);
};
return (
<div>
<ChildrenRef ref = { childRef } />
<button onClick = {() => handleGetChildData()}>我要获取子组件的数据</button>
</div>
);
};
export default FatherRef;
<!-- 子组件 -->
import { useState, forwardRef, useImperativeHandle } from 'react';
function ChildRef(props, ref) {
const [formValue, setFormValue] = useState({ name: 'ky', age: 18 });
useImperativeHandle(ref, () => ({
getFromData: () => {
return formValue;
},
}));
return <div>我是子组件的信息</div>;
};
ChildRef = forwardRef(ChildRef);
// 这里用 forwardRef 包裹住了,useforward 必须以函数声明,不能是变量声明
export default ChildRef;
useImperativeHandle 为 ref 的转发(透传),是使用 React.forwardRef 方法实现的,该方法返回一个组件,参数为函数(props、callback,并不是函数组件),函数的第一个参数为父组件传递的 props,第二个参数为父组件传递的 ref,其目的就是希望可以在封装组件时,外层组件可以通过 ref 直接控制内层组件或元素的行为。
b. setState
<!-- 父组件 -->
import * as React from 'react';
import { useEffect, useState } from 'react';
import './index.css';
import Children from '../children/index';
const Father = () => {
const [color, setColor] = useState('red');
// 更改颜色
const changeColor = () => {
setColor(color === 'red' ? 'green' : 'red');
};
return (
<div className="content">
<Children value= {color} setColor= {setColor} />
</div>
);
};
export default Father;
<!-- 子组件 -->
import * as React from 'react';
import { useEffect } from 'react';
import './index.css';
const Children = (props) => {
const { value, setColor } = props;
useEffect(() => {
if (!value) {
return 'props值不存在';
}
console.log(value, 'value');
}, [value]);
// 子组件更改颜色
const changeColor = () =>{
setColor('black');
};
return(
<>
<div className="box" style={{backgroundColor: value}}>
子组件
</div>
<button onClick={changeColor}>子组件更改颜色</button>
</>
);
};
export default Children;
除了以上两种,还有通过子组件的点击事件来传递值给父组件的方法,具体实现与 setState 的方法大同小异,也就不多说。因为 React 框架本身单向数据流的特性,所以对于父子组件的传值比 Vue 来说相对麻烦,所以一般需要搭配 redux 或者 mobx 等数据管理库使用。
4. useMemo 与 useCallback
同样的 React Hook 也新增了两个性能优化函数,那么这两个函数同样是性能优化函数,具体有什么不同呢?我们可以通过下面的例子直观感受一下:
<!-- useMemo -->
const memoizedValue = useMemo(
() => computeExpensiveValue(a, b),
[a, b]
);
<!-- useCallback -->
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
这两个函数十分相似,唯一的区别就是 useCallback 是根据依赖(deps)缓存第一个入参的(callback)。useMemo 是根据依赖(deps)缓存第一个入参(callback)执行后的值。简单来说,useMemo 一般的使用场景是用来指定变量值更改时才执行计算,一般用于密集型计算大的一些缓存。而 useCallback 则是用来缓存函数。
总结
通过上面对 Vue 3.0 与 React Hook 的一个简单对比,可以比较直观地感受到一些相同点,比如 useState 与 useRef,useReactive 的一个相似点,还有 setup 函数与 useEffect 函数的相同点,但是又有细致的不同。但是总的来说两个框架还是很好用的,各有各的优势,客观地去看待异同点,才能更好地进步。