一、简介

Vue3+TypeScript从入门到进阶(一)——Vue3简介及介绍——附沿途学习案例及项目实战代码

二、Vue2和Vue3区别

vue老项目接入typescript vue2 typescript_API


vue老项目接入typescript vue2 typescript_学习_02

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 底层需要对数组的一些操作方法,进行再封装。如 pushpop 等方法。

而在 Vue3 中优先使用了 Proxy 来处理,它代理的是整个对象而不是对象的属性,可对于整个对象进行操作。不仅提升了性能,也没有上面所说的缺陷。

简单举两个例子:

  1. 动态添加响应式属性
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];
},
  1. 对数组的操作进行拦截
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)

使用以下三步来建立反应性数据:

  1. 从vue引入reactive
  2. 使用reactive()方法来声名我们的数据为反应性数据
  3. 使用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()方法可以接收两个参数:

  1. props - 不可变的组件参数
  2. 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,如图:

vue老项目接入typescript vue2 typescript_学习_03

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如下图

vue老项目接入typescript vue2 typescript_vue.js_04

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 非常友好,而且函数名同变量名都可以被压缩,对象去不可以。举个例子,我们封装了一个工具,工具提供了两个方法,用 method1method2 来代替。

我们把它们封装成一个对象,并且暴露出去,如:

// 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

vue老项目接入typescript vue2 typescript_typescript_05

vue2中v-for 具有比 v-if 更高的优先级

vue老项目接入typescript vue2 typescript_vue.js_06

vue3中v-if 具有比 v-for 更高的优先级

vue老项目接入typescript vue2 typescript_学习_07


vue老项目接入typescript vue2 typescript_API_08

八、知识拓展

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’

vue老项目接入typescript vue2 typescript_学习_09


vue老项目接入typescript vue2 typescript_API_10


在学习vue3的顶层编写方式时的父子组件通信的时候,我们会看到一些比较老(2020、2021年初)的博客里面会有使用defineEmit的,但是如果我们用比较新版本的Vue3的话,就会报错。原因是,新版本的Vue3将defineEmit改成了defineEmits了