React&Vue系列:变量的定义
背景:作为使用三年 react.js 的搬运工,目前正在积极学习 vue.js 语法,而在学习的过程中,总是喜欢和 react.js 进行对比,这里也不是说谁好谁坏,而是怀有他有我也有,他没我还有的思想去学习,去总结。
- React18
- Vue3
后面打算陆续更新一系列的 关于 React 和 Vue 基本语法对比的文章,其中的好与坏,对与错,都可以多多指教,相互学习。
当然这里只讨论函数式编程。因为无论是 React18,还是 Vue3 都是采用了函数编程。类似 React 类组件或者 Vue3 options api 写法这里就不多做解释了。
话不多说,正题开始。
先从简单的开始,变量的定义。
对 React 比较熟悉,React 有优先权。
React 定义变量
在 React 中,如果不讨论类组件的写法,那么就是函数组件。而函数组件又可以分为:
- 无状态的函数组件
- 有状态的函数组件
无状态的函数组件,就仅仅展示数据而已。而针对有状态的函数组件,就需要利用 hooks 的写法来定义状态(也就是变量)。
基本使用
可以采用两种方式来定义变量:
- useState 的方式
import { useState } from 'react'
// 定义变量
const [state, setState] = useState(initialState);
- state 变量名称
- setState 修改变量名称的方法
- initialState 用来给变量设置初始值
- useReducer 的方式
import { useReducer } from 'react'
function reducer(state, action) {
switch (action.type) {
case 'increment':
return state + 1;
case 'decrement':
return state - 1;
default:
return state;
}
}
const initialArg = 0 // 初始值
// 定义变量
const [state, dispatch] = useReducer(reducer, initialArg, init?);
- state 变量名称
- dispatch 更新变量状态的方法
- reducer 函数,根据传入的参数(state,action),返回新的状态值
- initialArg 初始值
- init (可选) 如果存在,就是调用 init(initialArg) 的返回值作为初始值
TypeScript 写法
- useState 方式
function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>];
// example
const [count, setCount] = useState<number>(0)
- useState 函数接受一个泛型,及 state 的类型。
- useReducer 方式
// 重载(有四种情况,就不列举出来了,常用的,下面标识了)
// 多个泛型参数
function useReducer<R extends Reducer<any, any>, I>(
reducer: R,
initializerArg: I,
initializer: (arg: I) => ReducerState<R>
): [ReducerState<R>, Dispatch<ReducerAction<R>>];
// 一个泛型参数(常用)
function useReducer<R extends Reducer<any, any>>(
reducer: R,
initialState: ReducerState<R>,
initializer?: undefined
): [ReducerState<R>, Dispatch<ReducerAction<R>>];
// example
type State = { num: number };
type Action = {
type: "increment" | "decrement";
payload?: number;
};
function reducer(state: State, action: Action) {
switch (action.type) {
case "increment":
return {
...state,
num: state.num + (action.payload ?? 1),
};
case "decrement":
return {
...state,
num: state.num - (action.payload ?? 1),
};
default:
return state;
}
}
// 这里就已经能够自动推断类型,以及友好的提示了
const [state, dispatch] = useReducer(reducer, initState);
变量更新
上面的两种方式,在定义的时候都提供了修改变量的方法,只需要调用该方法,并传递对应的参数即可。
但是在调用该函数之前,先暂停一下,先来了解一个小的知识点:
- JavaScript 的数据类型分为基本数据类型和复杂数据类型。针对基本数据类型的比较是值比较,而针对复杂数据类型的比较是引用比较。
- 那么 React 在定义变量的时候,也是会分成两种情况的。一种是基本数据类型,当值发生了变量,就触发更新操作。那么对于复杂数据类型来说,React 内部有着它自己独有的比较方式:React 浅比较。
React 知道了变量发生了改变,才会触发更新操作。
当了解 React 的比较方式之后,那么在调用更新方法的时候,那么就会知道这样写:
// 针对基本数据类型
setState(2)
// 针对复杂数据类型(就需要返回一个新的对象)
setState({...state, num: 10})
useReducer 的 dispatch 触发的 action 执行逻辑也是一样。
总结
常用的定义方式还是采用 useState 的方式,useReducer 面对多变量定义,也是一种不错的选择。
当然还有一种定义方式:useRef
,这里就不多做解释了。
Vue 定义变量
Vue3 的写法相对于 Vue2 的写法发生了巨大的变化(当然也兼容 Vue2 的写法,options api), 这里主要看 Vue3 定义变量的方式。
基本使用
Vue3 推出了 组合式写法( compositions api) ,提供了一大堆的函数,其中有两个函数(主要的函数,当然还有其他的辅助函数),就是用来定义变量的,该两个函数为 ref
和 reactive
。
import { ref, reactive } from 'vue'
该如何选择呢?
ref 用来定义基本数据类型,reactive 用来定义复杂数据类型 是我最初的印象。但是呢,如果将一个对象赋值给 ref,那么这个对象将通过 reactive 转为具有深层次响应式的对象。所以说,ref 也是用来定义复杂数据类型的。
小结一下:
- 针对基本数据类型,必须使用 ref 定义变量,reactive 是不支持的。
- 针对复杂数据类型,ref 或者 reactive 都是可以定义的,只是使用变量的方式不同。
那么,针对复杂数据类型,你们是使用 ref,还是使用 reactive 呢?
ref 知识点
import { ref } from 'vue'
const count = ref(0)
- 返回一个可响应的 ref 对象,里面有个 value 属性保存着变量值。对 value 属性的所有操作都会被跟踪,然后执行副作用。
- 在 JavaScript 逻辑代码中,使用
count.value
获取值 - 如果在 template 模版中,就会自动解包,直接使用
count
reactive 知识点
import { reactive } from 'vue'
const count = reactive({
count: 0
})
- 返回一个对象的响应式代理。
- 返回的对象都会被 proxy 包裹(进行代理操作),与源对象是不同的。
- 如果给 reactive 传递的参数,存在 ref 对象,那么 reactive 也会 ref 进行解包。但是如果传递的是数组或者 Map 则不会进行解包。
- 无论是在 JavaScript 代码逻辑中,还是在 template 模版中,都是跟对象使用方式一样,拿到响应对象获取属性。
这里主要理解第三点,就是在平时开发过程中,数据复杂了,就会出现这种情况。那么这时候,就需要清楚的知道哪些变量是响应式的,相互影响的。
看看官网简单的示例,针对 JavaScript 的原生集合(数组,Map, Set)等不会自动解包。
import { ref, reactive } from "vue"; const name = ref("copyer"); const info = reactive(new Map([["key", name]])); // 返回的是一个 ref 对象,想要获取值,还需要 .value console.log("info.key======>", info.get("key"));
也可以思考一下,Vue3 为什么需要设置两种定义变量的方法?
TypeScript 写法
Vue3 是使用 TypeScript 进行开发的,那么 ts 的类型肯定比较友好。
import { ref } from 'vue'
import type { Ref } from 'vue'
const name = ref('copyer') // 推导出的类型:Ref<string>
// 显示指定
const name: Ref<string> = ref('copyer')
const name = ref<string>('copyer')
const name = ref<string>() // 推导得到的类型:Ref<string | undefined>
import { reactive } from 'vue'
const info = reactive({name: 'copyer'}) // 推导出:{name: string}
// 显示指定
interface Info {
name: string
}
const info: Info = reactive({ name: 'copyer' })
reactive()
函数不推荐使用泛型,是因为存在自动解包过程,返回的类型是不一致的。
变量更新
vue 的更新操作,没有 react 的更新操作那么多讲究(必须返回一个新的对象,并且还不能立马获取到变量的新值)。
Vue 的更新操作就是正常的对象操作。
// ref 操作 value 属性
const name = ref('copyer')
name.value = 'james'
console.log(name.value) // james, 能立即获取到改变后的值
// reactive
const info = reactive({name: 'copyer'})
info.name = 'james'
console.log(info.name) // james, 能立即获取到改变后的值
对 info.name
设置操作,执行里面的拦截器是 set 拦截器
对 info.name
获取操作,执行里面的拦截器是 get 拦截器
- ref 的基本数据类型,执行的是 类 class get set 的拦截,也就是 Object.definePropety() 的拦截方式
- 针对复杂数据类型,执行的是 proxy 的拦截器
额外知识补充
React 浅比较
// 源码位置:packages/shared/shallowEqual.js
function shallowEqual(objA: mixed, objB: mixed): boolean {
// 原始类型,或引用相同的对象(数组和对象)
if (is(objA, objB)) {
return true;
}
// 处理掉,比较对象其中一个为null(如果两个为null,上就已经拦截。其中一个为null, 就肯定不相等了)
if (
typeof objA !== 'object' ||
objA === null ||
typeof objB !== 'object' ||
objB === null
) {
return false;
}
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
if (keysA.length !== keysB.length) {
return false;
}
// Test for A's keys different from B.
for (let i = 0; i < keysA.length; i++) {
const currentKey = keysA[i];
// 判断objB是否有key,没有返回false,如果有,则跟objA比较是否相等,不同则返回false
if (
!hasOwnProperty.call(objB, currentKey) ||
!is(objA[currentKey], objB[currentKey])
) {
return false;
}
}
return true;
}
工具函数:
// packages/shared/objectIs.js
function is(x: any, y: any) {
return (
(x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) // eslint-disable-line no-self-compare
);
}
const objectIs: (x: any, y: any) => boolean =
// $FlowFixMe[method-unbinding]
typeof Object.is === 'function' ? Object.is : is;
export default objectIs;
// packages/shared/hasOwnProperty.js
const hasOwnProperty = Object.prototype.hasOwnProperty;
export default hasOwnProperty;
需要了解到:
- 只是针对复杂数据类型的第一层做了比较,也就是所谓的浅比较。
- 内部采用的是 Object.is 方法,或者是 Object.is 的 polyfill 方法。
Object.is 不同于
===
两点:
+0
与-0
的比较NaN
与NaN
的比较
Vue3 为什么设计了两个定义变量的函数?
reactive()
的种种限制归根结底是因为 JavaScript 没有可以作用于所有值类型的 “引用” 机制。为此,Vue 提供了一个 ref()
方法来允许我们创建可以使用任何值类型的响应式 ref
简单的来说,就是 JavaScript 没有提供可以针对所有类型的方法,只有手动实现一个。
那么为什么不要 reactive()
,只要 ref()
呢?
个人猜测:
- ref 接受对象的底层处理还是 reactive 的处理方式(proxy 拦截)。既然底层已经实现一个,暴露出来也无妨。
- ref 的写法在 JavaScript 的逻辑中,稍显麻烦,每次都需要
.value
;而 reactive 就是正常的对象写法,利于开发。
结语
无论是 React 还是 Vue 定义变量的知识点都还不少,都是需要慢慢消化一下的,最好的学习就是实战,写多了自然就会了。
这一章写了 React 和 Vue 的变量定义,下章打算写 React 和 Vue 的监听变量修改,执行相关逻辑。
有误,欢迎指教~~~
转载自:https://juejin.cn/post/7256971663997141029