背景:作为使用三年 react.js 的搬运工,目前正在积极学习 vue.js 语法,而在学习的过程中,总是喜欢和 react.js 进行对比,这里也不是说谁好谁坏,而是怀有他有我也有,他没我还有的思想去学习,去总结。
- React18
- Vue3
在前面的两篇中,
介绍了 React 和 Vue 中的变量及监听方式,这里的变量也就是指所谓的 state。但是经历过开发的人都知道,还是一种变量是 props,从父组件传递下来的变量。本章就来分析变量:props。
在开始之前,先了解一下 props(说说你对 props 的理解):
-
props 是组件之间的内置属性
-
props 只读性(单项数据流)
-
为什么有了 state,还需要 props 呢?
state 和 props 主要的区别在于 props 是不可变的,而 state 可以根据与用户交互来改变。这就是为什么有些容器组件需要定义 state 来更新和修改数据。 而子组件只能通过 props 来传递数据。
文章的编写方向:
- 基本使用
- 默认值
- 类型验证(js 版本)
- 类型验证(ts 版本)
既然知道了方向,就正式开始了哈。
还是老规矩吧,熟悉 React,先从 React 开始。
React: props
props 出现与组件之间的,先定义好两个函数组件吧。
const Son = () => {
return <h2>我是子组件</h2>;
};
const Father = () => {
return (
<>
<h2>我是父组件</h2>
<Son />
</>
);
};
基本使用
传递 name
属性给子组件。
// 父组件: Father
const [name, setName] = useState('copyer')
// ...
<Son name={name}/>
// 子组件: Son
const Son = (props) => {
const { name } = props;
return <title>我是子组件</title>;
};
默认值
在 React 的函数组件中设置默认值有两种方式
- defaultProps
- ES6 的对象解构默认值(推荐,清晰)
// 第一种方式:defaultProps
const Son = (props) => {
const { name } = props;
return <title>我是子组件</title>;
};
Son.defaultProps = {
name: "james", // name默认值
};
// 第二种方式:es6 解构默认值
const Son = (props) => {
const { name = "james" } = props;
return <title>我是子组件</title>;
};
类型验证(js 版本)
从个人角度来说,JS 验证,只是可能为了后面便于阅读;当类型不匹配的时候,只是报出警告 ⚠️,没有实质性拦截。也就适用于开发模式(安装在
devDependencies
即可)
React + js 也能对组件的 props 的类型进行验证,借助于一个第三方库(prop-types
),所以使用之前,先安装一下。
安装
pnpm add prop-types -D
使用
import PropTypes from "prop-types";
const Son = (props) => {
const { name = "james" } = props;
return <title>我是子组件</title>;
};
Son.propTypes = {
name: PropTypes.string, // name 是一个字符串
};
prop-types
的具体语法,可以自行去研究一下。
类型验证(ts 版本)
React + TS 在开发中就比较的常见了,对 props 的限制也比较友好,提供了各种泛型。 就比如最常用:FC
泛型。
type FC<P = {}> = FunctionComponent<P>;
该泛型在
React18
是做了调整的。 在 React17 中,该泛型是包含 children 属性的;但是在 React18 中已经没有了,当存在 children 时,需要自定义。当然,也可以借用
PropsWithChildren
泛型,里面就包含了 children 属性。当然还有一种完整的写法:
React.FC<React.PropsWithChildren<T>>
import type { FC } from "react";
interface Props {
name: string;
children?: JSX.Element; // 如果存在 children 属性
}
// 定义方式一:FC
const Son: FC<Props> = (props) => {
const { name = "james" } = props;
return <title>我是子组件</title>;
};
// 定义方式二:直接定义
const Son = (props: Props) => {
const { name = "james" } = props;
return <title>我是子组件</title>;
};
这样在编译的时候,发现类型不对齐,就会报错,阻止程序运行,强制要求解决。
ReactNode、ReactElement、JSX.Element 在定义子节点的时候,类型是最容易出错的,现在的我都还整不明白。
React 中的 props 大致就这些内容了,有错误的,请指教哟。
Vue: props
跟 React 一样,先创建两个组件。
<!-- Father.vue -->
<script setup lang="ts">
import Son from "./Son.vue";
</script>
<template>
<div class="father">
<h3>我是父组件</h3>
<Son />
</div>
</template>
<!-- Son.vue -->
<script setup lang="ts"></script>
<template>
<div class="son">
<h3>我是子组件</h3>
</div>
</template>
组件定义好之后,就可以开始正题了。 其实相信你也发现了,使用 props,主要的代码集中在子组件。因为父组件只是简单的传递一下,就没有然后了。但是对于 Vue 来说,在传递的过程中还是注意两点:
- props 传递的方式:驼峰式还是短横线式,官方更加推荐的是短横线式,更加接近于 HTML 的书写风格。
- 动态传递值(所有类型基本都一致,要什么传递什么,但是针对对象有一种特殊的写法)
<script>
import { reactive, ref } from 'vue'
const count = ref(0)
const info = reactive({
name: 'copyer',
age: 18
})
</script>
<template>
<!-- 要什么传递什么即可: info 对象,数字10 -->
<Son :count="10" :info="info"/>
<!-- 特殊情况:子组件要对象内的全部解构属性 -->
<!-- 写法一: -->
<Son :name="info.name" :age="info.age"/>
<!-- 写法二:针对对象(直接使用 v-bind) -->
<Son v-bind="info"/>
</template>
基本使用(js 版本)
在上面已经了解到了父组件如何传递值,那么接下来就是重要的子组件编写了。Vue 内部提供了两种写法:
- 数组的形式(简单,功能性弱)
- 对象的形式(复杂,功能性强: 类型检测
type
,默认值default
,是否必传required
)
而子组件又分为两种写法:
都是针对 compositions api 的写法
- 对象形式的 setup 函数(通过属性 props 的方式)为了方便描述,下面叫做:对象式 setup
- setup 的语法糖(通过编译器宏的形式)为了方便描述,下面叫做:语法糖 setup
其实两种声明方式背后其实使用的都是 prop 选项。
<!-- 对象式 setup -->
<script>
export default {
props: ["info", "count"], // 数组形式
props: { // 对象形式
info: {
type: Object,
},
count: {
type: Number,
default: 7,
required: true
}
}
setup(props) {
// setup 两个参数 props, context
console.log(props);
},
};
</script>
<!-- 语法糖 setup -->
<script setup>
// 参数为数组
const props = defineProps(["info", "count"])
// 参数为对象
const props = defineProps({
info: {
type: Object
},
count: {
type: Number,
default: 7,
required: true
}
})
</script>
细节说明:
- type 的值为预期类型的构造函数。
- 所有的 prop 都是可选的,除非声明了
required: true
。 - 几乎所有的 prop 的默认值都是 undefined,只有
type: Boolean
的 prop 是 false。 - 当传递的 props 的值就是 undefined, 则内部采用的是默认值。
基本使用(ts 版本)
子组件的写法也是分为两种,跟上面一样。
针对对象式 setup 组件 ts 的写法,只是添加了一个函数 defineComponent,其 prop 的写法跟上面的写法是基本一样的,就不多说了。
那 defineComponent 有什么作用呢?额外知识:defineComponent
既然对象式 setup 组件的写法没有发生变化,那么再来看看语法糖 setup 的写法又有什么差异呢?
还是借助于编译器宏defineProps
// Son.vue
// ts 语法,一眼就可以看出接受哪些 props,哪些是可选和必选
const props = defineProps<{
info?: object
count: number
}>()
设置默认值,还可以借助编译器宏withDefaults
interface Props {
info?: object
count: number
}
const props = withDefaults(defineProps<Props>(), {
info: {},
count: 7
})
也不难看出,setup 语法糖是真的简便,利于开发。
额外知识
jsx 转化为虚拟 dom 的源码分析
在早期的时候,发布了一篇对 jsx 转化虚拟 DOM 的源码分析:React 系列: jsx 转化为虚拟 DOM,写的不是很好,但是其中想表达的内容还是解释清楚了[偷笑]。
简单回顾一下理论知识:
- 在 React 17 版本之前,jsx 转化采用的是 React.createElement() 进行转化。(这里也就解释了为什么 React17 版本之前的组件必须要导入 React)。
React.createElement(type, config, ...children);
// config 就是对应的属性节点
// children 子节点(如果存在多个,就是从第三个参数后面累加),
- 在 React17 版本,官方与 Babel 进行合作,采用了一种全新的 jsx 转化(react/jsx-runtime)工具函数(在开发中不能直接使用该函数), 也间接解决了每个组件中都需要导入 React 的问题(即使没有使用);当然原来的方式也没有丢弃,还是可以正常的使用(在开发中直接使用)。
// 由编译器引入(禁止自己引入!)
import { jsx as _jsx } from "react/jsx-runtime";
_jsx(type, config, maybeKey);
// config 对应的属性节点(包含 children 属性)
// maybeKey 处理key值的
编译器宏
在 Vue3 中的 setup 中采用了编译器宏,先说结论,我不理解。
我在网上搜了一下,关于宏的定义:
宏是一种批量处理的称谓,简单来说就是根据定义好的规则替换一定的文本,发生在程序编译阶段。
这就是基础薄弱,遇到计算机程序名词,就一脸懵逼的样子,唉,难受。
程序是先编译后执行的。那么就可以简单的理解,在编译阶段,通过一系列的规则进行替换,然后执行的。
在这里呼喊,哪位大佬可以帮我来个具体的解释,感激不尽。
defineComponent 的作用
Vue3 提供的 defineComponent 函数,没有实际的逻辑代码,只是一个提供类型推导的辅助函数,专为 ts 服务。
参数接受一个 object,原模原样的返回对象,只是在使用的时候增加了一个类型推导。
好处就是:
- 在开发的时候,多了类型提示,便捷开发。
- 使用 props 的时候,也能提供一个正确的类型推导。
- 返回值是一个构造函数类型,它的实例类型是根据选项推断出的组件实例类型。这是为了能让该返回值在 TSX 中用作标签时提供类型推导支持。
- Vue3.3 增加了函数签名,与 jsx/tsx 一起使用(也就是所谓的 h 函数)
总结
- props 用于组件之间进行通信的。
- 在 React 中对 props 进行验证,JS 利用 prop-types 来进行验证;ts 就是内部提供的各种泛型来进行验证的(比如: FC)
- 在 Vue 中对 props 验证是内部提供一套书写规则即可验证;其中也分为两种:对象式 setup 的 props 属性 和 语法糖 setup 的编译器宏 defineProps() 。
继续学习 Vue3 的新语法,持续加油。