Vue

209 阅读8分钟

搭建一个‘新项目’详细过程

  1. 安装node和npm node -v 如果未安装,则到官网下载安装, 配置环境变量(用户变量和环境变量) (1)用户变量:D:\Develop\nodejs\node_modules

image.png (2)系统变量:D:\Develop\nodejs;D:\Develop\nodejs\node_global image.png

npm -v 如果未安装,则安装npm镜像

  1. 搭建vue项目环境并创建vue项目 安装vue-cli cnpm install vue-cli -g //全局安装 vue-cli npm install -g @vue/cli

如果安装失败,则安装一遍vue [npm install vue]

查看vue-cli是否成功,不能检查vue-cli,需要检查vue

vue list vue --version

进入你的项目目录,创建一个基于 webpack 模板的新项目(这步开始我以WebStorm软件为例,创建基于脚手架的Vue项目) vue init webpack vue-demo vue create vue-demo

最后,一个新项目创建完成。

配置环境变量

npm config set prefix "D:\Program Files\nodejs\node_global" npm config set cache "D:\Program Files\nodejs\node_cache"

image.png

image.png

image.png

我的电脑↓↓↓↓↓↓

image.png

image.png

vue查看版本 vue -V 报错解决方法

原因:未安装vue脚手架vue-cli 或 未配好环境变量 npm i -g vue-cli npm config list npm config get prefix

image.png

image.png

image.png

image.png

Vue3.x的更新

reactive && ref

  • 从原理角度对比:

    • ref用来创建一个包含响应式的数据的引用对象

接收数据可以是:基本数据类型、对象类型

基本类型的数据:响应式依然是靠object.defineProperty()的get与set完成的

对象类型:内部求助vue3.0中一个新函数reactive函数通过proxy实现

  • reactive用来定义:对象和数组通过使用Proxy来实现响应式(数据劫持), 并通过Reflect操作源对象内部的数据。

  • 从使用角度对比:

    • ref定义的数据:操作数据需要.value,读取数据时模板中不需要.value直接使用即可。
    • reactive定义的数据:操作数据与读取数据:均不需要.value

【ref: 模板里不用通过value获取,在js中必须通过value获取】 底层原理:reactive是proxy,ref是defineProperty reactive只能定义引用类型,ref可以任意类型 【数组,Set,Map用reactive】 补充&回顾:JS基本数据类型 { 基本数据类型:Number、String、Boolean、Undefined、Null; 复杂数据类型:Object、Array、Function、RegExp、Date、Map、Set、Symbol等等, 复杂数据类型都是引用类型;}

watch

image.png

watch参数: { deep: true } { immediate: true }

// watch默认是惰性的。【加上参数{ immediate: true }是非惰性,可以立即执行回调函数】

image.png

watchEffect:

image.png

Watch的补充:

watch的属性
  1. immediate:是否在初始值被设置之后立即执行回调函数,默认为false。
  2. deep:是否深度监听对象的变化,默认为false。
  3. flush:触发回调函数的时机,默认为'pre',表示在更新视图之前触发回调函数。也可以设置为'post',表示在更新视图之后触发回调函数。
  4. onTrack:当被监听的数据被访问时触发的回调函数。
  5. onTrigger:当被监听的数据被修改时触发的回调函数。

示例代码:

watch(count, (newValue, oldValue) => { console.log('count has changed:', newValue, oldValue) }, { immediate: true, deep: true, flush: 'post', onTrack: (event) => { console.log('count has been accessed:', event) }, onTrigger: (event) => { console.log('count has been modified:', event) } })

自定义Hooks

开篇的时候我们使用 Vue2.x 写了一个实现加减的例子, 这里可以将其封装成一个 hook, 我们约定这些「自定义 Hook」以 use 作为前缀,和普通的函数加以区分。 useCount.ts 实现:

import { ref, Ref, computed } from "vue"; 
type CountResultProps = { 
    count: Ref<number>;
    multiple: Ref<number>; 
    increase: (delta?: number) => void; 
    decrease: (delta?: number) => void; 
}; 
export default function useCount(initValue = 1): CountResultProps {
    const count = ref(initValue); 
    const increase = (delta?: number): void => { 
        if (typeof delta !== "undefined") {
            count.value += delta; 
        } else {
            count.value += 1;
        } 
    }; 
    const multiple = computed(() => count.value * 2); 
    const decrease = (delta?: number): void => {
        if (typeof delta !== "undefined") {
            count.value -= delta; 
        } else {
            count.value -= 1; 
        }
    }; 
    return { 
        count, 
        multiple, 
        increase, 
        decrease, 
    }; 
}


接下来看一下在组件中使用useCount这个 hook:

<template> 
    <p>count: {{ count }}</p> 
    <p>倍数: {{ multiple }}</p> 
    <div> 
        <button @click="increase()">加1</button> 
        <button @click="decrease()">减一</button> 
    </div> 
</template> 
<script lang="ts"> 
    import useCount from "../hooks/useCount"; 
    setup() {
        const { count, multiple, increase, decrease } = useCount(10); 
        return { count, multiple, increase, decrease, }; 
    }, 
</script>

简单对比 vue2.x 与 vue3.x 响应式

其实在 Vue3.x 还没有发布 bate 的时候, 很火的一个话题就是Vue3.x 将使用 Proxy 取代 Vue2.x 版本的 Object.defineProperty。 没有无缘无故的爱,也没有无缘无故的恨。为何要将Object.defineProperty换掉呢,咋们可以简单聊一下。 我刚上手 Vue2.x 的时候就经常遇到一个问题,数据更新了啊,为何页面不更新呢?什么时候用$set更新,什么时候用$forceUpdate强制更新,你是否也一度陷入困境。后来的学习过程中开始接触源码,才知道一切的根源都是 Object.defineProperty。 对这块想要深入了解的小伙伴可以看这篇文章 为什么 Vue3.0 不再使用 defineProperty 实现数据监听?要详细解释又是一篇文章,这里就简单对比一下Object.defineProperty 与 Proxy

  1. Object.defineProperty只能劫持对象的属性, 而 Proxy 是直接代理对象

由于Object.defineProperty只能劫持对象属性,需要遍历对象的每一个属性,如果属性值也是对象,就需要递归进行深度遍历。但是 Proxy 直接代理对象, 不需要遍历操作

  1. Object.defineProperty对新增属性需要手动进行Observe

因为Object.defineProperty劫持的是对象的属性,所以新增属性时,需要重新遍历对象, 对其新增属性再次使用Object.defineProperty进行劫持。也就是 Vue2.x 中给数组和对象新增属性时,需要使用$set才能保证新增的属性也是响应式的, $set内部也是通过调用Object.defineProperty去处理的。

Teleport

Teleport 就像是哆啦 A 梦中的「任意门」,任意门的作用就是可以将人瞬间传送到另一个地方。有了这个认识,我们再来看一下为什么需要用到 Teleport 的特性呢,看一个小例子: 在子组件Header中使用到Dialog组件,我们实际开发中经常会在类似的情形下使用到 Dialog ,此时Dialog就被渲染到一层层子组件内部,处理嵌套组件的定位、z-index和样式都变得困难。 Dialog从用户感知的层面,应该是一个独立的组件,从 dom 结构应该完全剥离 Vue 顶层组件挂载的 DOM;同时还可以使用到 Vue 组件内的状态(data或者props)的值。简单来说就是,即希望继续在组件内部使用Dialog, 又希望渲染的 DOM 结构不嵌套在组件的 DOM 中。 此时就需要 Teleport 上场,我们可以用<Teleport>包裹Dialog, 此时就建立了一个传送门,可以将Dialog渲染的内容传送到任何指定的地方。 接下来就举个小例子,看看 Teleport 的使用方式

我们希望 Dialog 渲染的 dom 和顶层组件是兄弟节点关系, 在index.html文件中定义一个供挂载的元素:

定义一个`Dialog`组件`Dialog.vue`, 留意 `to` 属性, 与上面的`id`选择器一致: ``` ```

最后在一个子组件Header.vue中使用Dialog组件, 这里主要演示 Teleport 的使用,不相关的代码就省略了。header组件

...
...

DOM渲染效果如下: image.png 图片. png 可以看到,我们使用 teleport 组件,通过 to 属性,指定该组件渲染的位置与 <div id="app"></div> 同级,也就是在 body 下,但是 Dialog 的状态 dialogVisible 又是完全由内部 Vue 组件控制.

Suspense

在前后端交互获取数据时, 是一个异步过程,一般我们都会提供一个加载中的动画,当数据返回时配合v-if来控制数据显示。 如果你使用过vue-async-manager这个插件来完成上面的需求, 你对Suspense可能不会陌生,Vue3.x 感觉就是参考了vue-async-manager. Vue3.x 新出的内置组件Suspense, 它提供两个template slot, 刚开始会渲染一个 fallback 状态下的内容, 直到到达某个条件后才会渲染 default 状态的正式内容, 通过使用Suspense组件进行展示异步渲染就更加的简单。:::warning 如果使用 Suspense, 要返回一个 promise :::Suspense 组件的使用:

<Suspense> 
    <template #default> 
        <async-component></async-component> 
    </template> 
    <template #fallback> 
        <div> Loading... </div> 
    </template> 
</Suspense>

asyncComponent.vue:

<template> 
    <div> 
        <h4>这个是一个异步加载数据</h4> 
        <p>用户名:{{user.nickname}}</p> 
        <p>年龄:{{user.age}}</p> 
    </div> 
</template> 
<script> 
import { defineComponent } from "vue" import axios from "axios" 
export default defineComponent({ 
    setup(){ 
        const rawData = await axios.get("http://xxx.xinp.cn/user") 
        return { user: rawData.data } 
    } 
}) 
</script>

从上面代码来看,Suspense 只是一个带插槽的组件,只是它的插槽指定了default 和 fallback 两种状态。

片段(Fragment)

在 Vue2.x 中, template中只允许有一个根节点:

<template>
    <div>
        <span></span>
        <span></span>
    </div>
</template>

但是在 Vue3.x 中,你可以直接写多个根节点, 是不是很爽:

<template>
    <span></span>
    <span></span>
</template>

更好的 Tree-Shaking

Vue3.x 在考虑到 tree-shaking的基础上重构了全局和内部 API, 表现结果就是现在的全局 API 需要通过 ES Module的引用方式进行具名引用, 比如在 Vue2.x 中,我们要使用 nextTick:

// vue2.x import Vue from "vue" 
Vue.nextTick(()=>{ ... })

Vue.nextTick() 是一个从 Vue 对象直接暴露出来的全局 API,其实 $nextTick() 只是 Vue.nextTick() 的一个简易包装,只是为了方便而把后者的回调函数的 this 绑定到了当前的实例。虽然我们借助webpacktree-shaking, 但是不管我们实际上是否使用Vue.nextTick(), 最终都会进入我们的生产代码, 因为 Vue 实例是作为单个对象导出的, 打包器无法坚持出代码总使用了对象的哪些属性。 在 Vue3.x 中改写成这样:

import { nextTick } from "vue"

nextTick(() =>{
    ...
})

受影响的 API

这是一个比较大的变化, 因为以前的全局 API 现在只能通过具名导入,这一更改会对以下 API 有影响:

  • Vue.nextTick
  • Vue.observable(用 Vue.reactive 替换)
  • Vue.version
  • Vue.compile(仅限完整版本时可用)
  • Vue.set(仅在 2.x 兼容版本中可用)
  • Vue.delete(与上同)

内置工具

出来上面的 API 外, 还有许多内置的组件 以上仅适用于 ES Modules builds,用于支持 tree-shaking 的绑定器——UMD 构建仍然包括所有特性,并暴露 Vue 全局变量上的所有内容 (编译器将生成适当的输出,以使用全局外的 api 而不是导入)。::: 前面都是 Vue3.0 的一些新特性,后面着重介绍一下相对于 Vue2.x 来说, 有什么变更呢?

变更

slot 具名插槽语法

image.png

在 Vue2.x 中具名插槽和作用域插槽分别使用slotslot-scope来实现, 在 Vue3.0 中将slotslot-scope进行了合并同意使用。 Vue3.0 中v-slot

<!-- 父组件中使用 -->
 <template v-slot:content="scoped">
   <div v-for="item in scoped.data">{{item}}</div>
</template>

<!-- 也可以简写成: -->
<template #content="{data}">
    <div v-for="item in data">{{item}}</div>
</template>

自定义指令

首先回顾一下 Vue 2 中实现一个自定义指令:

// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {
  // 当被绑定的元素插入到 DOM 中时……
  inserted: function (el) {
    // 聚焦元素
    el.focus()
  }
})

在 Vue 2 中, 自定义指令通过以下几个可选钩子创建:

  • bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
  • inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
  • update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下)。
  • componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
  • unbind:只调用一次,指令与元素解绑时调用。

在 Vue 3 中对自定义指令的 API 进行了更加语义化的修改, 就如组件生命周期变更一样, 都是为了更好的语义化,变更如下:

image.png 所以在 Vue3 中, 可以这样来自定义指令:

const { createApp } from "vue"

const app = createApp({})
app.directive('focus', {
    mounted(el) {
        el.focus()
    }
})

然后可以在模板中任何元素上使用新的 v-focus指令, 如下:

<input v-focus />

v-model 升级

在使用 Vue 3 之前就了解到 v-model 发生了很大的变化, 使用过了之后才真正的 get 到这些变化, 我们先纵观一下发生了哪些变化, 然后再针对的说一下如何使用:

  • 变更:在自定义组件上使用v-model时, 属性以及事件的默认名称变了
  • 变更:v-bind.sync修饰符在 Vue 3 中又被去掉了, 合并到了v-model
  • 新增:同一组件可以同时设置多个 v-model
  • 新增:开发者可以自定义 v-model修饰符

有点懵?别着急,往下看 在 Vue2 中, 在组件上使用 v-model其实就相当于传递了value属性, 并触发了input事件:

<!-- Vue 2 -->
<search-input v-model="searchValue"><search-input>

<!-- 相当于 -->
<search-input :value="searchValue" @input="searchValue=$event"><search-input>

这时v-model只能绑定在组件的value属性上,那我们就不开心了, 我们就像给自己的组件用一个别的属性,并且我们不想通过触发input来更新值,在.sync出来之前,Vue 2 中这样实现:

// 子组件:searchInput.vue
export default {
    model:{
        prop: 'search',
        event:'change'
    }
}

修改后, searchInput 组件使用v-model就相当于这样:

<search-input v-model="searchValue"><search-input>
<!-- 相当于 -->
<search-input :search="searchValue" @change="searchValue=$event"><search-input>

但是在实际开发中,有些场景我们可能需要对一个 prop 进行 “双向绑定”, 这里以最常见的 modal 为例子:modal 挺合适属性双向绑定的,外部可以控制组件的visible显示或者隐藏,组件内部关闭可以控制 visible属性隐藏,同时 visible 属性同步传输到外部。组件内部, 当我们关闭modal时, 在子组件中以 update:PropName 模式触发事件:

this.$emit('update:visible'false)

然后在父组件中可以监听这个事件进行数据更新:

<modal :visible="isVisible" @update:visible="isVisible = $event"></modal>

此时我们也可以使用v-bind.sync来简化实现:

<modal :visible.sync="isVisible"></modal>

上面回顾了 Vue2 中v-model实现以及组件属性的双向绑定,那么在 Vue 3 中应该怎样实现的呢? 在 Vue3 中, 在自定义组件上使用v-model, 相当于传递一个modelValue 属性, 同时触发一个update:modelValue事件:

<modal v-model="isVisible"></modal>
<!-- 相当于 -->
<modal :modelValue="isVisible" @update:modelValue="isVisible = $event"></modal>

如果要绑定属性名, 只需要给v-model传递一个参数就行, 同时可以绑定多个v-model

<modal v-model:visible="isVisible" v-model:content="content"></modal>

<!-- 相当于 -->
<modal
    :visible="isVisible"
    :content="content"
    @update:visible="isVisible"
    @update:content="content"
/>

不知道你有没有发现,这个写法完全没有.sync什么事儿了, 所以啊,Vue 3 中又抛弃了.sync写法, 统一使用v-model

异步组件

Vue3 中 使用 defineAsyncComponent 定义异步组件,配置选项 component 替换为 loader ,Loader 函数本身不再接收 resolve 和 reject 参数,且必须返回一个 Promise,用法如下:

<template>
  <!-- 异步组件的使用 -->
  <AsyncPage />
</tempate>

<script>
import { defineAsyncComponent } from "vue";

export default {
  components: {
    // 无配置项异步组件
    AsyncPage: defineAsyncComponent(() => import("./NextPage.vue")),

    // 有配置项异步组件
    AsyncPageWithOptions: defineAsyncComponent({
   loader: () => import(".NextPage.vue"),
   delay: 200,
   timeout: 3000,
   errorComponent: () => import("./ErrorComponent.vue"),
   loadingComponent: () => import("./LoadingComponent.vue"),
 })
  },
}
</script>

image.png