Vue3+TypeScript从入门到进阶(二)——Vue2和Vue3的区别——附沿途学习案例及项目实战代码
一、简介二、Vue2和Vue3区别见后续文章三、Vue知识点学习见后续文章四、TypeScript知识点见后续文章五、项目实战见后续文章六、项目打包和自动化部署见后续文章七、沿途学习代码地址及案例地址1、沿途学习代码地址https://gitee.com/wu_yuxin/vue3-learning.git2、项目案例地址https://gitee.com/wu_yuxin/vue3-ts-cms
一、简介
Vue3+TypeScript从入门到进阶(一)——Vue3简介及介绍——附沿途学习案例及项目实战代码
二、Vue2和Vue3区别
1、生命周期,Vue2 vs Vue3
选项式 API(Vue2) | Hook inside setup(Vue3) |
---|---|
beforeCreate | Not needed |
created | Not needed |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeUnmount | onBeforeUnmount |
unmounted | onUnmounted |
errorCaptured | onErrorCaptured |
renderTracked | onRenderTracked |
renderTriggered | onRenderTriggered |
activated(组件被<keep-alive> 包含) | onActivated(组件被<keep-alive> 包含) |
deactivated(组件被<keep-alive> 包含) | onDeactivated(组件被<keep-alive> 包含) |
Hook inside setup,顾名思义,VCA 建议在 setup
这个大方法里面写我们的各种逻辑功能点。
2、vue2和vue3双向数据绑定原理
vue2 的双向数据绑定是利用ES5 的一个 API Object.definePropert()对数据进行劫持 结合 发布订阅模式的方式来实现的。
vue3 中使用了 es6 的 ProxyAPI 对数据代理。
相比于vue2.x,使用proxy的优势如下
defineProperty只能监听某个属性,不能对全对象监听
可以省去for in、闭包等内容来提升效率(直接绑定整个对象即可)
可以监听数组,不用再去单独的对数组做特异性操作 vue3.x可以检测到数组内部数据的变化
Vue2 响应式的基本原理,就是通过 Object.defineProperty
,但这个方式存在缺陷。使得 Vue 不得不通过一些手段来 hack,如:
- Vue.$set() 动态添加新的响应式属性
- 无法监听数组变化,Vue 底层需要对数组的一些操作方法,进行再封装。如
push
,pop
等方法。
而在 Vue3 中优先使用了 Proxy 来处理,它代理的是整个对象而不是对象的属性,可对于整个对象进行操作。不仅提升了性能,也没有上面所说的缺陷。
简单举两个例子:
- 动态添加响应式属性
const targetObj = { id: '1', name: 'zhagnsan' };
const proxyObj = new Proxy(targetObj, {
get: function (target, propKey, receiver) {
console.log(`getting key:${propKey}`);
return Reflect.get(...arguments);
},
set: function (target, propKey, value, receiver) {
console.log(`setting key:${propKey},value:${value}`);
return Reflect.set(...arguments);
}
});
proxyObj.age = 18;
// setting key:age,value:18
如上,用 Proxy
我们对 proxyObj
对象动态添加的属性也会被拦截到。
Reflect
对象是ES6 为了操作对象而提供的新 API。它有几个内置的方法,就如上面的 get
/ set
,这里可以理解成我们用 Reflect
更加方便,否则我们需要如:
get: function (target, propKey, receiver) {
console.log(`getting ${propKey}!`);
return target[propKey];
},
- 对数组的操作进行拦截
const targetArr = [1, 2];
const proxyArr = new Proxy(targetArr, {
set: function (target, propKey, value, receiver) {
console.log(`setting key:${propKey},value:${value}`);
return Reflect.set(...arguments);
}
});
proxyArr.push('3');
// setting key:2,value:3
// setting key:length,value:3
3、Vue3支持碎片(Fragments)
组件来说,大多代码在Vue2和Vue3都非常相似。Vue3支持碎片(Fragments)
,就是说在组件可以拥有多个根节点。
这种新特性可以减少很多组件之间的div
包裹元素。在开发vue的时候,我们会发现每一个组件都会有个div
元素包裹着。就会出现很多层多余的div
元素。碎片(Fragments)
解决了这个问题。
Vue2 表格template
<template>
<div class='form-element'>
<h2> {{ title }} </h2>
<input type='text' v-model='username' placeholder='Username' />
<input type='password' v-model='password' placeholder='Password' />
<button @click='login'>
Submit
</button>
<p>
Values: {{ username + ' ' + password }}
</p>
</div>
</template>
在Vue3的唯一真正的不同在于数据获取。Vue3中的反应数据(Reactive Data)
是包含在一个反应状态(Reactive State)
变量中。— 所以我们需要访问这个反应状态来获取数据值。
<template>
<div class='form-element'>
<h2> {{ state.title }} </h2>
<input
type='text'
v-model='state.username'
placeholder='Username'
/>
<input
type='password'
v-model='state.password'
placeholder='Password'
/>
<button @click='login'>
Submit
</button>
<p>
Values: {{ state.username + ' ' + state.password }}
</p>
</div>
</template>
4、Optinos Api 与 Composition API
Vue2与Vue3 最大的区别 — Vue2使用选项类型API(Options API)对比Vue3合成型API(Composition API)
// vue2
export default {
props: {
title: String
},
data() {
return {
username: '',
password: ''
}
},
methods: {
login() {
// 登陆方法
}
},
components: {
"buttonComponent": btnComponent
},
computed: {
fullName() {
return this.firstName + " " + this.lastName;
}
}
}
// Vue3
export default {
props: {
title: String
},
setup () {
const state = reactive({ //数据
username: '',
password: '',
lowerCaseUsername: computed(() => state.username.toLowerCase()) //计算属性
})
//方法
const login = () => {
// 登陆方法
}
return {
login,
state
}
}
}
但是目前 Vue3 还保留了 Vue2 的 options api 写法,就是可以“并存”,如:
// ...
setup() {
const val = ref<string>('');
const fn = () => {};
return {
val,
fn
};
},
mounted() {
// 在 mounted 生命周期可以访问到 setup return 出来的对象
console.log(this.val);
this.fn();
},
// ...
上述写法是完全没有任何问题的
5、建立数据 data
Vue2 - 这里把两个数据放入data属性中
export default {
props: {
title: String
},
data () {
return {
username: '',
password: ''
}
}
}
在Vue3.0,我们就需要使用一个新的setup()
方法,此方法在组件初始化构造的时候触发。
为了可以让开发者对反应型数据有更多的控制,我们可以直接使用到 Vue3 的反应API(reactivity API)
。
使用以下三步来建立反应性数据
:
- 从vue引入
reactive
- 使用
reactive()
方法来声名我们的数据为反应性数据 - 使用
setup()
方法来返回我们的反应性数据,从而我们的template可以获取这些反应性数据
import { reactive } from 'vue'
export default {
props: {
title: String
},
setup () {
const state = reactive({
username: '',
password: ''
})
return { state }
}
}
在新版当中setup等效于之前2.0版本当中得到beforeCreate,和created,它是在组件初始化的时候执行,甚至是比created更早执行。值得注意的是,在3.0当中如果你要想使用setup里的数据,你需要将用到值return出来,返回出来的值在模板当中都是可以使用的。
假设如果你不return出来,而直接去使用的话浏览器是会提醒你:
runtime-core.esm-bundler.js?5c40:37 [Vue warn]: Property “name” was accessed during render but is not defined on instance.
at
at (Root)
6、 vue3 Teleport瞬移组件
Teleport一般被翻译成瞬间移动组件,实际上是不好理解的.我把他理解成"独立组件", 将组件的 DOM 元素挂载在任意指定的一个 DOM 元素,与 React Portals 的概念是一致的。
他可以那你写的组件挂载到任何你想挂载的DOM上,所以是很自由很独立的
编写一个例子
// HelloWorld.vue
<template>
<div>
<h2>Hello World</h2>
</div>
</template>
<script>
export default {
}
</script>
<style scoped>
</style>
在App.vue中使用的时候跟普通组件调用是一样的
// App.vue
<template>
<div class="app">
<teleport to="#why">
<h2>当前计数</h2>
<button>+1</button>
<hello-world></hello-world>
</teleport>
<teleport to="#why">
<span>呵呵呵呵</span>
</teleport>
</div>
</template>
<script>
import { getCurrentInstance } from "vue";
import HelloWorld from './HelloWorld.vue';
export default {
components: {
HelloWorld
},
setup() {
const instance = getCurrentInstance();
console.log(instance.appContext.config.globalProperties.$name);
},
mounted() {
console.log(this.$name);
},
methods: {
foo() {
console.log(this.$name);
}
}
}
</script>
<style scoped>
</style>
要是在App.vue文件中使用的时候,hello-world组件是在App的 DOM节点之下的,父节点的dom结构和css都会给hello-world组件产生影响
于是产生的问题
hello-world组件被包裹在其它组件之中,容易被干扰
样式也在其它组件中,容易变得非常混乱
Teleport 可以把hello-world组件渲染到任意你想渲染的外部Dom上,不必嵌套在#app中,这样就可以互不干扰了,可以把Teleport看成一个传送门,把你的组件传送到任何地方
使用的时候 to属性可以确定想要挂载的DOM节点下面
<template>
<div class="app">
<teleport to="#why">
<h2>当前计数</h2>
<button>+1</button>
<hello-world></hello-world>
</teleport>
<teleport to="#why">
<span>呵呵呵呵</span>
</teleport>
</div>
</template>
在public文件夹下的index.html中增加一个节点
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<div id="why"></div>
<!-- built files will be auto injected -->
</body>
</html>
7、watchEffect vs watch
Vue3 的 watch 方法与 Vue2 的概念类似,watchEffect 会让我们有些疑惑。其实 watchEffect 与 watch 大体类似,区别在于:
watch 可以做到的
- 懒执行副作用
- 更具体地说明什么状态应该触发侦听器重新运行
- 访问侦听状态变化前后的值
对于 Vue2 的 watch 方法,Vue3 的 “watch” 多了一个「清除副作用」 的概念,我们着重关注这点。
这里拿 watchEffect
来举例:
watchEffect:它立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。
watchEffect 方法简单结构
watchEffect(onInvalidate => {
// 执行副作用
// do something...
onInvalidate(() => {
// 执行/清理失效回调
// do something...
})
})
执行失效回调,有两个时机
- 副作用即将重新执行时,也就是监听的数据发生改变时
- 组件卸载时
8、计算属性 - Computed Properties
我们一起试试添加一个计算属性来转换username
成小写字母。
在 Vue2 中实现,我们只需要在组件内的选项属性中添加即可
export default {
// ..
computed: {
lowerCaseUsername () {
return this.username.toLowerCase()
}
}
}
Vue3 的设计模式给予开发者们按需引入需要使用的依赖包。这样一来就不需要多余的引用导致性能或者打包后太大的问题。Vue2就是有这个一直存在的问题。
所以在 Vue3 使用计算属性,我们先需要在组件内引入computed
。
使用方式就和反应性数据(reactive data)
一样,在state
中加入一个计算属性:
import { reactive, onMounted, computed } from 'vue'
export default {
props: {
title: String
},
setup () {
const state = reactive({
username: '',
password: '',
lowerCaseUsername: computed(() => state.username.toLowerCase())
})
// ...
}
9、父子传参不同–setup() 函数特性
总结:
1、setup 函数时,它将接受两个参数:(props、context(包含attrs、slots、emit))
2、setup函数是处于 生命周期函数 beforeCreate 和 Created 两个钩子函数之前的函数
3、执行 setup 时,组件实例尚未被创建(在 setup() 内部,this 不会是该活跃实例的引用,即不指向vue实例,Vue 为了避免我们错误的使用,直接将 setup函数中的this修改成了 undefined)
4、与模板一起使用:需要返回一个对象 (在setup函数中定义的变量和方法最后都是需要 return 出去的 不然无法再模板中使用)
5、使用渲染函数:可以返回一个渲染函数,该函数可以直接使用在同一作用域中声明的响应式状态
注意事项:
1、setup函数中不能使用this。Vue 为了避免我们错误的使用,直接将 setup函数中的this修改成了 undefined)
2、setup 函数中的 props 是响应式的,当传入新的 prop 时,它将被更新。但是,因为 props 是响应式的,你不能使用 ES6 解构,因为它会消除 prop 的响应性。
如果需要解构 prop,可以通过使用 setup 函数中的toRefs 来完成此操作:
接收 Props
接收组件props
参数传递这一块为我们带来了Vue2和Vue3之间最大的区别。—this
在vue3中与vue2代表着完全不一样的东西。
在 Vue2,this
代表的是当前组件,不是某一个特定的属性。所以我们可以直接使用this
访问prop属性值。就比如下面的例子在挂载完成后打印处当前传入组件的参数title
。
mounted () {
console.log('title: ' + this.title)
}
但是在 Vue3 中,this
无法直接拿到props属性,emit events(触发事件)和组件内的其他属性。不过全新的setup()
方法可以接收两个参数:
props
- 不可变的组件参数context
- Vue3 暴露出来的属性(emit,slots,attrs)
所以在 Vue3 接收与使用props就会变成这样:
setup (props) {
// ...
onMounted(() => {
console.log('title: ' + props.title)
})
// ...
}
事件 - Emitting Events
在 Vue2 中自定义事件是非常直接的,但是在 Vue3 的话,我们会有更多的控制的自由度。
举例,现在我们想在点击提交按钮时触发一个login
的事件。
在 Vue2 中我们会调用到this.$emit
然后传入事件名和参数对象。
login () {
this.$emit('login', {
username: this.username,
password: this.password
})
}
但是在 Vue3中,我们刚刚说过this
已经不是和vue2代表着这个组件了,所以我们需要不一样的自定义事件的方式。
在setup()
中的第二个参数content
对象中就有emit
,这个是和this.$emit
是一样的。那么我们只要在setup()
接收第二个参数中使用分解对象法取出emit
就可以在setup方法中随意使用了。
然后我们在login
方法中编写登陆事件:
setup (props, { emit }) {
// ...
const login = () => {
emit('login', {
username: state.username,
password: state.password
})
}
// ...
}
另外:context 是一个普通的 JavaScript 对象,也就是说,它不是响应式的,这意味着你可以安全地对 context 使用 ES6 解构
setup()内使用响应式数据时,需要通过.value获取
import { ref } from 'vue'
const count = ref(0)
console.log(count.value) // 0
从 setup() 中返回的对象上的 property 返回并可以在模板中被访问时,它将自动展开为内部值。不需要在模板中追加 .value
setup函数只能是同步的不能是异步的
10、v-model
在Vue2,假设我需要封装一个弹框组件 Modal,用 show
变量来控制弹框的显示隐藏,这肯定是一个父子组件都要维护的值。因为单向数据流,所以需要在 Modal 组件 emit 一个事件,父组件监听事件接收并修改这个 show
值。
为了方便我们会有一些语法糖,如 v-model,但是在 Vue2 一个组件上只能有一个 v-model ,因为语法糖的背后是 value
和 @input
的组成, 如果还有多个类似这样的 “双向修改数据”,我们就需要用语法糖 .sync
同步修饰符。
Vue3 把这两个语法糖统一了,所以我们现在可以在一个组件上使用 多个 v-model 语法糖,举个例子:
先从父组件看
<VModel v-model="show"
v-model:model1="check"
v-model:model2.hello="textVal" />
hello为自定义修饰符
我们在一个组件上用了 3 个 v-model 语法糖,分别是
v-model 语法糖 | 对应的 prop | 对应的 event | 自定义修饰符对应的 prop |
---|---|---|---|
v-model(default) | modelValue | update:modelValue | 无 |
v-model:model1 | model1 | update:model1 | 无 |
v-model:model2 | model2 | update:model2 | model2Modifiers |
这样子我们就更清晰的在子组件我们要进行一些什么封装了,如:
VModel.vue
// ...
props: {
modelValue: { type: Boolean, default: false },
model1: { type: Boolean, default: false },
model2: { type: String, default: '' },
model2Modifiers: {
type: Object,
default: () => ({})
}
},
emits: ['update:modelValue', 'update:model1', 'update:model2'],
// ...
11、key attribute
<template>
<input type="text"
placeholder="请输入账号"
v-if="show" />
<input type="text"
placeholder="请输入邮箱"
v-else />
<button @click="show=!show">Toggle</button>
</template>
类似这样的 v-if/v-else,在 Vue2 中,会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染,所以当我们在第一个 input 中输入,然后切换第二个
input 。第一个 input 的值将会被保留复用。
有些场景下我们不要复用它们,需要添加一个唯一的 key ,如:
<template>
<input type="text"
placeholder="请输入账号"
v-if="show"
key="account" />
<input type="text"
placeholder="请输入邮箱"
v-else
key="email" />
<button @click="show=!show">Toggle</button>
</template>
但是在 Vue3 我们不用显式的去添加 key ,这两个 input 元素也是完全独立的,因为 Vue3 会对 v-if/v-else 自动生成唯一的 key。
12、默认进行懒观察(lazy observation)
在 2.x 版本里,不管数据多大,都会在一开始就为其创建观察者。当数据很大时,这可能会在页面载入时造成明显的性能压力。3.x 版本,只会对「被用于渲染初始可见部分的数据」创建观察者,而且 3.x 的观察者更高效。
13、更精准的变更通知
举例来说:2.x 版本中,使用 Vue.set 来给对象新增一个属性时,这个对象的所有 watcher 都会重新运行;3.x 版本中,只有依赖那个属性的 watcher 才会重新运行。
14、3.0 新加入了 TypeScript 以及 PWA 的支持
3.0 的一个主要设计目标是增强对 TypeScript 的支持。原本我们期望通过 Class API 来达成这个目标,但是经过讨论和原型开发,我们认为 Class 并不是解决这个问题的正确路线,基于 Class 的 API 依然存在类型问题。——尤雨溪
基于函数的 API 天然 与 TS 完美结合。
defineComponent
在 TS 下,我们需要用 Vue 暴露的方法 defineComponent,它单纯为了类型推导而存在的。
props 推导
import { defineComponent } from 'vue';
export default defineComponent({
props: {
val1: String,
val2: { type: String, default: '' },
},
setup(props, context) {
props.val1;
}
})
当我们在 setup 方法访问 props 时候,我们可以看到被推导后的类型,
- val1 我们没有设置默认值,所以它为
string | undefined
- 而 val2 的值有值,所以是
string
,如图:
PropType
我们关注一下 props 定义的类型,如果是一个复杂对象,我们就要用 PropType 来进行强转声明,如:
interface IObj {
id: number;
name: string;
}
obj: {
type: Object as PropType<IObj>,
default: (): IObj => ({ id: 1, name: '张三' })
},
或 联合类型
type: {
type: String as PropType<'success' | 'error' | 'warning'>,
default: 'warning'
},
pwa 全称 Progressive Web App,是一种理念,使用多种技术来增强web app的功能,可以让网站的体验变得更好,能够模拟一些原生功能,比如通知推送。
在vue3中使用pwa非常简单,在使用 vue create [projectName] 时,选择pwa如下图
15、部分命令发生了变化
1.下载安装 npm install -g vue@cli
2.删除了vue list
3.创建项目 vue create
4.启动项目 npm run serve
16、默认项目目录结构发生了变化
移除了配置文件目录,config 和 build 文件夹
移除了 static 文件夹,新增 public 文件夹,并且 index.html 移动到 public 中
在 src 文件夹中新增了 views 文件夹,用于分类 视图组件 和 公共组件
17、打包出来的包体积变小了
通过webpack的tree-shaking功能,可以将无用模块“剪辑”,仅打包需要的
能够tree-shaking,有两大好处:
对开发人员,能够对vue实现更多其他的功能,而不必担忧整体体积过大
对使用者,打包出来的包体积变小了
tree-shaking是干啥的:
// app.js
export function A(a, b) {
return a + b
}
export function B(a, b) {
return a + b
}
// index.js
import {A} from '/app.js'
A(1, 2)
当index.js引用了app.js的A函数,如果tree-shaking起了作用,B函数是不会被打包进最后的bundle的。
但是
世界上有很多但是,而且往往但是后面的内容更加重要。
relies on the static structure of ES2015 module syntax, i.e. import and export.
在webpack官网当中有这样一句话,翻译成人话就是tree-shaking依赖es6的模块引入或输出语法。如果你的模块引入方式是require等等等乱七八糟的东西。tree-shaking将不会起到任何作用。
但是!
基于函数的 API 每一个函数都可以用 import { method1,method2 } from "xxx";
,这就对 tree-sharking 非常友好,而且函数名同变量名都可以被压缩,对象去不可以。举个例子,我们封装了一个工具,工具提供了两个方法,用 method1
,method2
来代替。
我们把它们封装成一个对象,并且暴露出去,如:
// utils
const obj = {
method1() {},
method2() {}
};
export default obj;
// 调用
import util from '@/utils';
util.method1();
经过webpack打包压缩之后为:
a={method1:function(){},method2:function(){}};a.method1();
我们不用对象的形式,而用函数的形式来看看:
// utils
export function method1() {}
export function method2() {}
// 调用
import { method1 } from '@/utils';
method1();
经过webpack打包压缩之后为:
function a(){}a();
用这个例子我们就可以了解 Vue3 为什么能更好的 tree-sharking ,因为它用的是基于函数形式的API,如:
import {
defineComponent,
reactive,
ref,
watchEffect,
watch,
onMounted,
toRefs,
toRef
} from 'vue';
18、v-if和v-for
vue2中v-for 具有比 v-if 更高的优先级
vue3中v-if 具有比 v-for 更高的优先级
三、Vue知识点学习
Vue3+TypeScript从入门到进阶(三)——Vue3基础知识点(上)——附沿途学习案例及项目实战代码
Vue3+TypeScript从入门到进阶(四)——Vue3基础知识点(中)——附沿途学习案例及项目实战代码
Vue3+TypeScript从入门到进阶(五)——Vue3基础知识点(下)——附沿途学习案例及项目实战代码
四、TypeScript知识点
Vue3+TypeScript从入门到进阶(六)——TypeScript知识点——附沿途学习案例及项目实战代码
五、项目实战
Vue3+TypeScript从入门到进阶(七)——项目实战——附沿途学习案例及项目实战代码
六、项目打包和自动化部署
Vue3+TypeScript从入门到进阶(八)——项目打包和自动化部署——附沿途学习案例及项目实战代码
七、沿途学习代码地址及案例地址
1、沿途学习代码地址
https://gitee.com/wu_yuxin/vue3-learning.git
2、项目案例地址
https://gitee.com/wu_yuxin/vue3-ts-cms.git
八、知识拓展
1、ES6数组与对象的解构赋值详解
数组的解构赋值
基本用法
ES6允许按照一定的模式,从数组和对象中提取值,对变量进行赋值,这被称之为解构(Destructuring)
// 以前为变量赋值,只能直接指定值
var a = 1;
var b = 2;
var c = 3;
// ES6允许写成这样
var [a,b,c] = [1,2,3];
本质上,这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。
下面是一些使用嵌套数组进行解构的例子:
let [foo,[[bar],baz]] = [1,[[2],3]];
foo // 1
bar // 2
baz // 3
let [,,third] = ["foo","bar","baz"];
third // "baz"
let [head,...tail] = [1,2,3,4];
head // 1
tail // [2,3,4]
let [x,y,...z] = ['a'];
x // "a"
y // undefined
z // []
默认值
解构赋值允许制定默认值
var [foo = true] = [];
foo // true
[x,y='b'] = ['a'];
// x='a', y='b'
注意,ES6内部使用严格相等运算符(===),判断一个位置是否有值。
所以,如果一个数组成员不严格等于undefined,默认值是不会生效的。
var [x=1] = [undefined];
x //1
var [x=1] = [null];
x // null
如果默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到的时候,才会求值:
function f(){
console.log('aaa');
}
let [x=f()] = [1];
上面的代码中,因为x能取到值,所以函数f()根本不会执行。上面的代码其实等价于下面的代码:
let x;
if([1][0] === undefined){
x = f();
}else{
x = [1][0];
}
默认值可以引用解构赋值的其他变量,但该变量必须已经声明:
let [x=1,y=x] = [];
// x=1; y=1
let [x=1,y=x] = [2];
// x=2; y=2
let [x=1,y=x] = [1,2];
// x=1; y=2
let [x=y,y=1] = []; // ReferenceError
上面最后一个表达式,因为x用到默认值是y时,y还没有声明。
对象的解构赋值
1、最简单的案例
看下面的案例
let person = {
name: 'yhb',
age: 20
}
/*
注意:下面虽然看起来是创建了一个对象,对象中有两个属性 name 和 age
但是:其实是声明了两个变量
name:等于对象person 中的name属性的值
age:等于对象person 中的 age属性的值
*/
let { name, age } = person
console.log(name,age)
如上面注释中所说,声明了变量 name和age,然后分别从对象person中寻找与变量同名的属性,并将属性的值赋值给变量
所以,这里的关键,就是首先要知道对象中都有哪些属性,然后再使用字面量的方式声明与其同名的变量
2、属性不存在怎么办
如果不小心声明了一个对象中不存在的属性怎么办?
或者,实际情况下,可能是我们就是想再声明一个变量,但是这个变量也不需要从对象中获取值,这个时候,此变量的值就是 undefined
let person = {
name: 'yhb',
age: 20
}
let { name, age,address } = person
console.log(name,age,address)
此时,可以给变量加入一个默认值
let { name, age,address='北京' } = person
3、属性太受欢迎怎么办
当前声明了 name 和 age 变量,其值就是person对象中name和age属性的值,如果还有其他变量也想获取这两个属性的值怎么办?
let { name, age, address = '北京' } = person
console.log(name, age, address)
let { name, age } = person
console.log(name, age)
上面的方法肯定不行,会提示定义了重复的变量 name 和 age
那怎么办呢?
难道只能放弃结构赋值,使用老旧的方式吗?
let l_name=person.name
let l_age=person.age
console.log(l_name,l_age)
其实不然!
let {name:l_name,age:l_age}=person
console.log(l_name,l_age)
说明:
声明变量 l_name 并从对象person中获取name属性的值赋予此变量
声明变量 l_age, 并从对象person中获取age属性的值赋予此变量
这里的重点是下面这行代码
let {name:l_name,age:l_age}=person
按照创建对象字面量的逻辑,name 为键,l_name 为值。但注意,这里是声明变量,并不是创建对象字面量,所以争取的解读应该是
声明变量 l_name,并从person 对象中找到与 name 同名的属性,然后将此属性的值赋值给变量 l_name
所以,我们最后输出的是变量 l_name和l_age
console.log(l_name,l_age)
当然这种状态下,也是可以给变量赋予默认值的
let { name:l_name, age:l_age, address:l_address='北京' }=person
4、嵌套对象如何解构赋值
let person = {
name: 'yhb',
age: 20,
address: {
province: '河北省',
city: '保定'
}
}
// 从对象 person 中找到 address 属性,并将值赋给变量 address
let {address}=person
// 从对象 address 中找到 province 属性,并将值赋给变量 province
let {province}=address
console.log(province)
上面代码一层层的进行结构赋值,也可以简写为如下形式
let {address:{province}}=person
从peson 对象中找到 address 属性,取出其值赋值给冒号前面的变量 address,然后再将 变量address 的值赋值给 冒号 后面的变量 {province},相当于下面的写法
let {province}=address
字符串的解构赋值
1、字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象。
const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"
类似数组的对象都有一个length属性,因此还可以对这个属性解构赋值。
let {length : len} = 'hello';
len // 5
2、JavaScript的 …(展开运算符)
三个连续的点具有两个含义:展开运算符(spread operator)和剩余运算符(rest operator)。
展开运算符
展开运算符允许迭代器在接收器内部分别展开或扩展。迭代器和接收器可以是任何可以循环的对象,例如数组、对象、集合、映射等。你可以把一个容器的每个部分分别放入另一个容器。
const newArray = ['first', ...anotherArray];
剩余参数
剩余参数语法允许我们将无限数量的参数表示为数组。命名参数的位置可以在剩余参数之前。
const func = (first, second, ...rest) => {};
用例
定义是非常有用的,但是很难仅从定义中理解概念。我认为用日常用例会加强对定义的理解。
复制数组
当我们需要修改一个数组,但又不想改变原始数组(其他人可能会使用它)时,就必须复制它。
const fruits = ['apple', 'orange', 'banana'];
const fruitsCopied = [...fruits]; // ['apple', 'orange', 'banana']
console.log(fruits === fruitsCopied); // false
// 老方法
fruits.map(fruit => fruit);
它正在选择数组中的每个元素,并将每个元素放在新的数组结构中。我们也可以使用 map 操作符实现数组的复制并进行身份映射。
唯一数组
如果我们想从数组中筛选出重复的元素,那么最简单的解决方案是什么?
Set 对象仅存储唯一的元素,并且可以用数组填充。它也是可迭代的,因此我们可以将其展开到新的数组中,并且得到的数组中的值是唯一的。
const fruits = ['apple', 'orange', 'banana', 'banana'];
const uniqueFruits = [...new Set(fruits)]; // ['apple', 'orange', 'banana']
// old way
fruits.filter((fruit, index, arr) => arr.indexOf(fruit) === index);
串联数组
可以用 concat 方法连接两个独立的数组,但是为什么不再次使用展开运算符呢?
const fruits = ['apple', 'orange', 'banana'];
const vegetables = ['carrot'];
const fruitsAndVegetables = [...fruits, ...vegetables]; // ['apple', 'orange', 'banana', 'carrot']
const fruitsAndVegetables = ['carrot', ...fruits]; // ['carrot', 'apple', 'orange', 'banana']
// 老方法
const fruitsAndVegetables = fruits.concat(vegetables);
fruits.unshift('carrot');
将参数作为数组进行传递
当传递参数时,展开运算符能够使我们的代码更具可读性。在 ES6 之前,我们必须将该函数应用于 arguments。现在我们可以将参数展开到函数中,从而使代码更简洁。
const mixer = (x, y, z) => console.log(x, y, z);
const fruits = ['apple', 'orange', 'banana'];
mixer(...fruits); // 'apple', 'orange', 'banana'
// 老方法
mixer.apply(null, fruits);
数组切片
使用 slice 方法切片更加直接,但是如果需要的话,展开运算符也可以做到。但是必须一个个地去命名其余的元素,所以从大数组中进行切片的话,这不是个好方法。
const fruits = ['apple', 'orange', 'banana'];
const [apple, ...remainingFruits] = fruits; // ['orange', 'banana']
// 老方法
const remainingFruits = fruits.slice(1);
将参数转换为数组
Javascript 中的参数是类似数组的对象。你可以用索引来访问它,但是不能调用像 map、filter 这样的数组方法。参数是一个可迭代的对象,那么我们做些什么呢?在它们前面放三个点,然后作为数组去访问!
const mixer = (...args) => console.log(args);
mixer('apple'); // ['apple']
将 NodeList 转换为数组
参数就像从 querySelectorAll 函数返回的 NodeList 一样。它们的行为也有点像数组,只是没有对应的方法。
[...document.querySelectorAll('div')];
// 老方法
Array.prototype.slice.call(document.querySelectorAll('div'));
复制对象
最后,我们介绍对象操作。复制的工作方式与数组相同。在以前它可以通过 Object.assign 和一个空的对象常量来实现。
const todo = { name: 'Clean the dishes' };
const todoCopied = { ...todo }; // { name: 'Clean the dishes' }
console.log(todo === todoCopied); // false
// 老方法
Object.assign({}, todo);
合并对象
合并的唯一区别是具有相同键的属性将被覆盖。最右边的属性具有最高优先级。
const todo = { name: 'Clean the dishes' };
const state = { completed: false };
const nextTodo = { name: 'Ironing' };
const merged = { ...todo, ...state, ...nextTodo }; // { name: 'Ironing', completed: false }
// 老方法
Object.assign({}, todo, state, nextTodo);
需要注意的是,合并仅在层次结构的第一级上创建副本。层次结构中的更深层次将是相同的引用。
将字符串拆分为字符
最后是字符串。你可以用展开运算符把字符串拆分为字符。当然,如果你用空字符串调用 split 方法也是一样的。
const country = 'USA';
console.log([...country]); // ['U', 'S', 'A']
// 老方法
country.split('');
3、export ‘defineEmit’ (imported as ‘defineEmit’) was not found in ‘vue’
在学习vue3的顶层编写方式时的父子组件通信的时候,我们会看到一些比较老(2020、2021年初)的博客里面会有使用defineEmit的,但是如果我们用比较新版本的Vue3的话,就会报错。原因是,新版本的Vue3将defineEmit改成了defineEmits了
九、其他知识学习
1、Webpack学习
2、数据可视化-echarts
数据可视化-echarts入门、常见图表案例、超详细配置解析及项目案例
3、预备知识与后续知识及项目案例
HTML入门与进阶以及HTML5
CSS
JS-上
JS-下
jQuery
Node.js + Gulp 知识点汇总
MongoDB + Express 入门及案例代码
4、Vue2学习
5、JavaScript面向对象和设计模式
6、微前端学习
更多推荐
所有评论(0)