likes
comments
collection
share

React&Vue系列:变量的定义

作者站长头像
站长
· 阅读数 17

背景:作为使用三年 react.js 的搬运工,目前正在积极学习 vue.js 语法,而在学习的过程中,总是喜欢和 react.js 进行对比,这里也不是说谁好谁坏,而是怀有他有我也有,他没我还有的思想去学习,去总结。

  • React18
  • Vue3

后面打算陆续更新一系列的 关于 React 和 Vue 基本语法对比的文章,其中的好与坏,对与错,都可以多多指教,相互学习。

当然这里只讨论函数式编程。因为无论是 React18,还是 Vue3 都是采用了函数编程。类似 React 类组件或者 Vue3 options api 写法这里就不多做解释了。

话不多说,正题开始。

先从简单的开始,变量的定义

对 React 比较熟悉,React 有优先权。

React 定义变量

在 React 中,如果不讨论类组件的写法,那么就是函数组件。而函数组件又可以分为:

  • 无状态的函数组件
  • 有状态的函数组件

无状态的函数组件,就仅仅展示数据而已。而针对有状态的函数组件,就需要利用 hooks 的写法来定义状态(也就是变量)。

基本使用

可以采用两种方式来定义变量:

  1. useState 的方式
import { useState } from 'react'

// 定义变量
const [state, setState] = useState(initialState);
  • state 变量名称
  • setState 修改变量名称的方法
  • initialState 用来给变量设置初始值
  1. 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 写法

  1. useState 方式
function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>];

// example
const [count, setCount] = useState<number>(0)
  • useState 函数接受一个泛型,及 state 的类型。
  1. 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);

变量更新

上面的两种方式,在定义的时候都提供了修改变量的方法,只需要调用该方法,并传递对应的参数即可。

但是在调用该函数之前,先暂停一下,先来了解一个小的知识点:

  1. JavaScript 的数据类型分为基本数据类型复杂数据类型。针对基本数据类型的比较是值比较,而针对复杂数据类型的比较是引用比较。
  2. 那么 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) ,提供了一大堆的函数,其中有两个函数(主要的函数,当然还有其他的辅助函数),就是用来定义变量的,该两个函数为 refreactive

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 模版中,都是跟对象使用方式一样,拿到响应对象获取属性。

这里主要理解第三点,就是在平时开发过程中,数据复杂了,就会出现这种情况。那么这时候,就需要清楚的知道哪些变量是响应式的,相互影响的。

cn.vuejs.org/api/reactiv…

看看官网简单的示例,针对 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;

需要了解到:

  1. 只是针对复杂数据类型的第一层做了比较,也就是所谓的浅比较。
  2. 内部采用的是 Object.is 方法,或者是 Object.is 的 polyfill 方法。

Object.is 不同于 === 两点:

  • +0-0 的比较
  • NaNNaN的比较

React&Vue系列:变量的定义

Vue3 为什么设计了两个定义变量的函数?

reactive() 的种种限制归根结底是因为 JavaScript 没有可以作用于所有值类型的 “引用” 机制。为此,Vue 提供了一个 ref()方法来允许我们创建可以使用任何值类型的响应式 ref

简单的来说,就是 JavaScript 没有提供可以针对所有类型的方法,只有手动实现一个。

那么为什么不要 reactive(),只要 ref() 呢?

个人猜测:

  1. ref 接受对象的底层处理还是 reactive 的处理方式(proxy 拦截)。既然底层已经实现一个,暴露出来也无妨。
  2. ref 的写法在 JavaScript 的逻辑中,稍显麻烦,每次都需要 .value;而 reactive 就是正常的对象写法,利于开发。

结语

无论是 React 还是 Vue 定义变量的知识点都还不少,都是需要慢慢消化一下的,最好的学习就是实战,写多了自然就会了。

这一章写了 React 和 Vue 的变量定义,下章打算写 React 和 Vue 的监听变量修改,执行相关逻辑

有误,欢迎指教~~~