跳到主要内容

Vue3

回顾之前的学习

  • node.js:我们的服务器是MVC的模式,服务器直接将渲染后的页面发送给客户端
  • ajax:我们的服务器是rest风格的服务器,服务器将数据以json形式发送给客户端,由客户端去渲染。
    • 此时我们的模式改变成mvvm,我们需要view model来将服务器返回的模型变成视图,在这个学习阶段我们通过DOM来实现
    • 使用DOM实现这个功能有两个缺点:麻烦和性能不好,所以我们需要前端js框架来代替DOM

什么是vue

  • vue官网

  • vue是一个前端的框架,主要负责帮助我们构建用户界面(user-interface)

  • MVVM:Model - View - View Model

  • vue负责vm的工作(视图模型),通过vue可以将视图和模型相关联。

    • 当模型发生变化时,视图会自动更新
    • 也可以通过视图去操作模型
  • vue思想:

    • 组件化开发:把整个网页拆分成多个组件
    • 声明式的编程
      • 命令式编程(Imperative):详细的命令机器怎么(How)去处理一件事情以达到你想要的结果(What)
      • 声明式编程( Declarative):只告诉你想要的结果(What),机器自己摸索过程(How)
    • 渐进式:基于其他前端框架的项目也可以部分采用vue编写

引入vue3

  1. 直接在网页中使用(像jQuery一样)

    • <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
      //创建一个按钮,可以显示按钮的点击的次数
    //创建组件
    const Root = {
    data() {
    return {
    count: 0 // 记录点击次数
    }
    },
    template:
    `<button @click='count++'>点我一下</button> - 点了{{count}}次`
    }
    //创建app实例并挂载
    Vue.createApp(Root).mount("#app")
  2. 使用vite

    • npm install vite -D
    • npm install vue
      // 这里引入的vue,默认不支持通过template属性来设置模板
    import {createApp} from "vue/dist/vue.esm-bundler.js"
    const App={
    data(){
    return{
    message:"Vue3"
    }
    },
    template:`<h1>hello{{message}}</h1>`
    }
    createApp(App).mount("#app")
  3. 代码:

    // 组件,就是一个普通js对象
    const App = {}

    // 返回一个应用实例,将根组件关联到应用上
    const app = createApp(App)

    // 将应用挂载到页面,生成一个组件实例(视图模型),组件实例类型是一个Proxy对象(代理对象)
    const vm=app.mount("#root")
    //该方法接收一个参数,可以是一个实际的 DOM 元素或是一个 CSS 选择器字符串
  4. 自动创建vue项目

    • npm init vue@latest
    • npm install安装依赖

选项式API

组件

  • vue3中组件就是一个普通的js对象

  • 组件用来创建组件实例(vm),组件是组件实例的模板

  • 组件 --> 组件生成组件实例 --> 虚拟DOM --> DOM(在页面中呈现)

  • 组件的作用:组件使得我们可以生成多个组件实例在不同的地方使用,组件实例相互独立不会互相影响,大大提高了代码的复用性

  • 分类:

    • 根组件:createApp(根组件名)直接使用,一个项目一般只有一个根组件(项目部分使用vue可以创建多个app)

    • 子组件:

      • 引入子组件
      • 在组件中注册子组件
      import MyButton from "./components/MyButton"
      export default {
      data() {
      return {
      message: "Vue3"
      }
      },
      //注册组件
      components:{
      MB:MyButton
      },
      template: `<h1>hello{{message}}</h1>
      <MB></MB>
      <MB></MB>
      <MB></MB>
      `
      }

data

  • data是一个方法,需要一个对象作为返回值,用来指定实例对象中的响应式属性

  • data方法返回的对象,vue会对该对象进行代理,从而将其转换为响应式数据,响应式数据可以直接通过组件实例访问

    • 响应式数据改变,页面会跟着改变
    • 直接向组件实例中添加的属性不会被vue所代理,不是响应式数据,修改后页面不会改变
    • 组件实例vm.$data才是真正的代理对象,我们可以使用这个属性动态添加响应式数据,但不建议这么做
  • 在data中,this就是当前的组件实例vm

    • vue给组件中的data方法绑定了this为vm
  • 如果使用箭头函数,则无法通过this来访问组件实例

    • 绑定对箭头函数无效
  • 内置第一个参数就是组件实例vm,箭头函数可以以此来访问vm

  • 在vue中使用选项式API,尽量减少箭头函数的使用

  • 注意点

    • 返回唯一对象
    <script>
    //问题1:返回唯一对象
    const obj={count :0}
    export default{
    data(){
    return obj
    }
    }
    </script>
    <template>
    <button @click="count++">{{ count }}</button>
    </template>

    • 代理的是同一个对象,这导致一个组件实例改变数据,其他组件实例跟着改变,开发中要避免这种写法

    • 深层对象&浅层对象

      • 深层对象指的是一个对象里有多层对象(对象套娃)
      • data返回的数据都会被vue代理,无论该数据处于第几层
       <script>
      import Button from './components/Button.vue'
      import {shallowReactive} from"vue"
      export default{
      data(){
      this.name="lusiyan"//非响应式数据
      console.log(this)
      window.vm=this
      return shallowReactive({
      msg:"hi vue",
      stu:{
      name:"啊伟",
      age:"20",
      gender:"男",
      friend:{
      name:"啊强",
      age:"28"
      }
      }
      })
      },
      components:{
      Button
      }
      }
      </script>
      • shallowReactive使得data返回的对象变为浅层对象,嵌套对象的属性不再是响应式数据,只有表面的msg,stu才是
      • 动态的响应式数据
        • 上述vm.$data这种方式不被我们所鼓励使用,原因是导致数据分布杂乱,不利于代码维护
        • 我们可以直接在return{}对象里声明待响应的数据,在响应事件触发再对该数据进行赋值
      • data返回的对象属性是数组
        • 由于vue3底层使用代理,可以直接在模板用索引引用数组中的数据

method

  • 用来指定实例对象中的方法
  • 它是一个对象,可以在它里边定义多个方法
  • 这些方法最终将会被挂载到组件实例上
  • 可以直接通过组件实例来调用这些方法,所有组件实例上的属性都可以在模板中直接访问
  • methods中函数的this会被自动绑定为组件实例,箭头函数与data同理
 <script>

export default{
data(){
window.vm=this
return{
msg:"hhhhhhh"
}
},
methods:{
test(){
alert("哈哈哈哈哈")
},
HelloWorld(){
console.log(this)
return "hello world"
},
changeMsg(){
this.msg="嘻嘻嘻"
}
}
}
</script>
<template>
<h1>{{ msg }}</h1>
<h2>{{ HelloWorld() }}</h2>
<button @click=changeMsg>点一下</button>
</template>

computed

  • 用来指定计算属性

  • 定义与调用方式

    computed:{
    计算属性名:{
    getter,
    setter
    }
    }
    //如果只有getter,则可以直接简写成属性名(){}
    //getter只做读取相关的逻辑,不要去执行会产生作用的代码(例如修改)
    //在开发中我们很少用setter,原因是可能让代码看起来复杂,可读性变差
    {{计算属性名}}//调用getter
    组件实例.计算属性名=xxx//计算属性同样会挂载在组件实例上,setter在计算属性被修改时调用
  • 作用

    • 相比data中生成的属性,计算属性用函数返回结果,在函数中可以写一些逻辑。
    • 相比使用方法,计算属性有缓存,只在其依赖的数据发生变化时才会重新执行。methods中的方法每次组件重新渲染都会调用,他没有缓存。这样性能更好
    • 证明计算属性实现缓存:控制台输入组件实例.计算属性名,计算属性方法不会重复调用
    • 当你的属性不是直接显示,而是需要一定逻辑,就可以用计算属性

template

  • 模板,它决定了组件最终的样子

  • 定义模板的方式有三种:

    • 1.在组件中通过template属性去指定

      • 如果在组件中定义template,则会优先使用template作为模板,同时根元素中的所有内容,都会被替换
      • 如果在组件中没有定义template,则会使用根元素的innerHTML作为模板使用
    • 2.直接在网页的根元素中指定

      • 如果直接将模板定义到网页中,此时模板必须符合html的规范

        • 自定义组件名为My-Button,在html里会自动转换成小写my-button,对我们使用组件有一定限制
    • 3.组件中通过render()直接渲染

data & template

  • 在模板中可以直接访问组件实例中的属性
    • 在模板中可以通过 {{属性名}} 来访问到组件实例中的属性
  • data中的响应式数据发生变化视图会自动更新

单文件组件

上述template是用字符串的形式编写的模板

  • 这些字符串会在项目运行时,在浏览器中被编译为js的函数(性能不太好)
  • 在字符串中编写代码,体验很差

为了解决这个问题,Vue为我们提供了一种单文件组件(SFC)

  • 单文件组件的格式是vue(vscode需要安装插件 Vue Language Features (Volar)来辅助实现代码高亮和提示)
  • vue文件用来编写单文件组件,vue文件本身并不能被浏览器所识别,所以它必须要被构建工具打包后,才可使用
  • 同时vue文件在打包时,构建工具会直接将template转换为函数,无需再浏览器中在去编译,这样一来性能也会有所提升

使用

  • 安装包扩展vite功能 npm install -D @vitejs/plugin-vue

  • 修改配置文件

    import vue from "@vitejs/plugin-vue"

    export default {
    plugins: [vue()]
    }
  • 创建单文件组件 .vue

    • js代码写进<script></script>
    • 模板代码写进<template></template>
  • 示例

    <script>
    import MyButton from "./components/Button.vue";
    export default {
    data(){
    return {
    message:"hi vue3"
    }
    },
    components:{
    MyButton
    }
    }
    </script>
    <template>
    <h1>{{ message }}</h1>
    <MyButton></MyButton>
    <MyButton></MyButton>
    <MyButton></MyButton>
    </template>

响应式原理-代理

  • 直接修改对象的属性,那么就是仅仅修改了属性,没有去做其他的事情,这种操作只会影响对象自身,不会导致元素的重新的渲染

  • 我们希望在修改一个属性的同时,可以进行一些其他的操作,比如触发元素重新渲染。

  • 要实现这个目的,必须要对对象进行改造,vue3中使用的是的代理模式来完成对象的改造

  • 生成代理对象的示例:

    const obj={
    name:"啊伟",
    age:20
    }
    //用来指定代理的行为
    const handler={
    get(target, prop, receiver){
    //返回值之前做一些其他的操作...
    return target[prop]
    },
    set(target, prop, value, receiver){
    target[prop]=value
    //值修改之后做一些其他的操作
    }
    }
    //创建代理
    const proxy=new Proxy(obj,handler)
    console.log(proxy.name)//啊伟
    proxy.name="阿强"
    console.log(obj.name)//啊强
  • 使用代理对象读取属性,会调用handler中的get方法

    • get用来指定读取数据时的行为,它的返回值就是最终读取到的值
    • target:被代理的对象
    • prop:读取的属性
    • receiver:代理对象
  • 使用代理对象修改属性,会调用handler中的set方法

    • target:被代理的对象
    • prop:修改的属性
    • value:值
    • receiver:代理对象
  • 代理对象的意义:可以在不会对原对象产生影响的前提下,拓展对象的读取功能,例如自动渲染

  • vue的代理原理

    • 在vue中,data()返回的对象会被vue所代理
    • vue代理后,当我们通过代理去读取属性时,返回值之前,它会先做一个跟踪的操作
      • 源码做了一个track() //追踪谁用了我这个属性,并记录下来
    • 当我们通过代理去修改属性时,修改后,会通知之前所有用到该值的位置进行更新
      • 源码做了一个一个trigger() //触发所有的使用该值的位置进行更新

组合式API

  • 上述语法是选项式的写法,在开发中大多用组合式写法

  • 相比之前的写法:

    • 对象有语法限制,只能写属性和方法,函数什么代码都能写
    • 对象没有作用域,函数有作用域,局部变量对其他地方无任何影响
  • 定义变量

    • 在组合式api中直接声明的变量,就是一个普通的变量,不是响应式属性,修改这些属性时,不会在视图中产生效果
    • reactive()来创建一个响应式的对象
  • 返回值

    • 通过返回值来指定那些内容要暴露给外部。暴露后的内容,可以在模板中直接使用
  • 组合式API可以在模板中直接写子组件,无需注册

  • 代码

    <script>
    import { reactive } from 'vue'
    export default{
    setup(){
    let msg="组合式api"
    const countB=reactive({count:0})
    function clickB(){
    countB.count+=1
    }
    return{
    mssge,
    clickB,
    countB
    }
    }
    }
    </script>
    <template>
    <button @click=clickB>点我一下</button>--点了{{ countB.count }}
    <h1>{{ msg }}</h1>
    </template>>
    <script setup>
    import { reactive } from 'vue';
    let msg="简写组合式api"
    const stu=reactive({
    name:"阿伟",
    age:18
    })
    </script>
    <template>
    <h1>{{ msg }}</h1>
    <h2>{{ stu.name }}--{{ stu.age }}</h2>
    </template>

响应式代理

  • reactive()

    • 返回一个对象的响应式代理
    • 返回的是一个深层响应式对象
    • 也可以使用shallowReactive()创建一个浅层响应式对象
    • 缺点:只能返回对象的响应式代理!不能处理原始值
  • ref()

    • 接收一个任意值,把它转换为对象,并返回对象的响应式代理
    • 改变量只会影响到变量自己(给变量重新赋一个地址值),在js中,无法实现对一个变量的代理
    • ref在生成响应式代理时,它是将值包装为了一个对象0 --> {value:0}
    • 访问ref对象时,必须通过 对象.value 来访问其中的值
    • 在模板中,ref对象会被自动解包(要求ref对象必须是顶层对象)
    <script setup>
    import { reactive,ref,shallowReactive } from 'vue';
    const stu=reactive(shallowReactive({
    name:"阿伟",
    age:18,
    friend:{
    name:"阿强",
    age:25
    }
    }))
    let count=ref(0)
    function clickCount(){
    count.value++
    }
    </script>
    <template>
    <h1>{{ msg }}</h1>
    <button @click=clickCount>点我一下</button>--{{ count }}
    <h2>{{ stu.friend.name }}</h2>
    </template>
    <script setup>
    import { ref } from 'vue';
    let msg="组合式api补充"
    const obj=ref({
    name:"阿伟",
    age:18
    })//{value:{name:"阿伟",age:18}}
    const obj1={
    name:ref("啊强"),//{value:"阿强"}
    age:ref(18)
    }
    </script>
    <template>
    <h1>{{ msg }}</h1>
    <h2>{{ obj.name }}</h2>
    <h2>{{ obj1.name }}</h2><!--"阿强",调用了对象的toString方法-->
    <h2>{{ obj1.name.value }}</h2>
    </template>

计算属性

<script setup>
import { computed } from 'vue';
let msg="hello vue"
const newMsg=computed(()=>{
return msg+="你好vue"
})
</script>
<template>
<h1>{{ msg }}</h1>
<h1>{{ newMsg }}</h1>
</template>

模板的语法

  • 在模板中,可以直接访问到组件中声明的变量
  • 除了组件中的变量外,vue也为我们提供了一些全局对象可以访问:比如:Date、Math、RegExp ...
  • 受限的全局访问
  • 除此之外,也可以通过app对象来向vue中添加一些全局变量app.config.globalProperties
  • 使用插值(双大括号),只能使用表达式【表达式,就是有返回值的语句】
  • 插值实际上就是在修改元素的textContent,如果内容中含有标签,标签会被转义显示,不会作为标签生效
  • <template></template>作为容器,会被浏览器解析隐藏,直接呈现标签内的内容

指令

  • 指令模板中为标签设置的一些特殊属性,它可以用来设置标签如何显示内容。使用指令时,不需要通过{{}}来指定表达式

  • 指令使用v-开头

    • v-text 将表达式的值作为元素的textContent插入,作用同{{}}

    • v-html 将表达式的值作为元素的innerHTML插入,有xss注入的风险

      <div v-text="s"></div>
      <div v-html="s"></div>
    • v-bind 为标签动态地设置属性,可简写为":"

      • 在vue中不能把插值直接设置为标签的属性

      • v-bind设置布尔值属性(属性名=属性值)时,若设置的变量非布尔值会自动转换,转换规则除 ""==true外与js中相同

        v-bind:属性名="变量名"
        v-bind="对象名"//绑定多个属性
        v-bind:[name]="value"//动态添加属性
      • 使用v-bind绑定class和style属性

        • class和style可以和其他属性一样使用v-bind将它们和动态的字符串绑定;但是,在处理比较复杂的绑定时,通过拼接生成字符串是麻烦且易出错的;因此,Vue专门为class和style的v-bind用法提供了特殊的功能增强;除了字符串外,表达式的值也可以是"对象"或"数组"
         <!-- style:可以直接写对象,但是对象的属性名当样式属性(驼峰命名法,kebab-cased形式) -->
        <button :style="{
        backgroundColor: backColor,
        color,
        'border-radius': borRadius + 'px'
        }">我是一个普通的按钮</button>
    • v-show=Boolean 底层通过"display"来设置一个内容是否显示

      • 通过css样式,不会涉及到组件的重新渲染,切换的性能比较高
      • 但是初始化对所有组件初始化(包括隐藏的组件),所有它的初始化性能差
    • v-if=表达式 可以根据表达式的值来决定是否显示元素

      • 通过删除添加元素的方式来切换元素的显示,切换时反复地渲染组件,切换的性能比较差
      • 只会初始化需要用到的组件,所以它初始化性能比较好
      • 可以搭配其他指令一起使用,例如v-else,v-else-if,适用于一些更复杂的场景
      • 可以配合template使用
      • <script setup>
        import {ref} from "vue"
        const isShow=ref(true)
        </script>
        <template>
        <button @click="isShow=!isShow">切换</button>
        <template v-if="isShow"><!--统一控制多行代码,经过浏览器编译后template会隐藏-->
        <h1>1111</h1>
        <h2>22222</h2>
        <!--很多条代码-->
        </template>
        </template>
    • v-for="xxx in 数组",数组里有多少项就会遍历几次

    • Vue 默认按照“就地更新”的策略来更新通过 v-for 渲染的元素列表。也就是默认根据元素顺序对比新旧列表的差距来最小程度地修改DOM,如果打乱顺序,可能导致与我们要的结果不同。

      <script setup>
      import {reactive} from"vue";
      const arr=reactive([{
      id:1,
      name:"啊伟",
      age:18
      },{
      id:2,
      name:"啊强",
      age:20
      },{
      id:3,
      name:"啊洪",
      age:16
      }])
      </script>
      <template>
      <button @click="arr.unshift({id:4,name:'啊飘',age:15})">点我</button>
      <ul>
      <li v-for="{name,age} in arr">{{ name }}--{{ age }} <input type="text"></li>
      </ul>
      </template>
      1 2 3
      4 1 2 3
      由于vue默认是根据顺序对比比较来修改DOM,它会把1变为4,2变为1,3变为2,最后添加3
      由于我们只修改数组,并不改变表单,所以表单没有做DOM修改,造成错位的现象。
    • 为了解决这个错位的问题,我们可以在标签内添加key属性来唯一标识每一项。

      添加后:
      1 2 3
      4 1 2 3
      vue只会在列表最前面插入一个DOM元素

注册组件

全局注册

  • 使用app.component(name,组件)全局注册
//main.js中
import { createApp } from 'vue'
import App from './App.vue'
// 1:引入需要被注册的组件
import Login from './components/Login.vue'

const app = createApp(App)

// 2:全局注册组件
app.component('MLogin', Login)

app.mount('#app')
  • 选项式api组件无需注册,就能直接使用全局注册的组件
  • 组合式api组件无需引入,就能直接使用全局注册的组件

局部注册

  • 选项式api通过components选项进行注册
  • 组合式api直接import相关组件,就能使用

public

  • 在模板中可以直接引用public中的内容
  • public里的内容会直接打包到dist目录下,在书写路径直接从"/..."开始写

样式

全局样式

  • 在组件中写会影响到当前页面的所有组件

  • <style></style>

    <style scoped>
    :global(h1){
    font-size: 6px;
    }
    </style>

scoped

  • 可以为style标签添加一个scoped属性,这样样式将成为局部样式,只对当前组件生效,降低代码的耦合性

  • 如何实现的?

    • 当我们在组件中使用scoped样式时,vue会自动为组件中的所有元素生成一个随机的属性 形如:data-v-7a7a37b1 生成后,所有的选择器都会在最后添加一个 [data-v-7a7a37b1]

      h1 -> h1[data-v-7a7a37b1]
      .box1 -> .box1[data-v-7a7a37b1]
    • 注意:随机生成的属性,除了会添加到当前组件内的所有元素上,也会添加到当前组件引入的其他组件的单根元素(多根元素不会生效)上,这样设计是为了可以通过父组件来为子组件设置一些样式(透传属性

  • :deep(选择器){样式}--可以给子组件单根元素下的所有符合选择器的元素绑定样式

module

  • 自动的对模块中的类名进行hash化来确保类名的唯一性

  • 在模板中可以通过 :class=$style.类名 使用

  • 也可以通过module的属性值来指定变量名(默认是$style)

    <script setup></script>
    <template>
    <div :class=$style.box1></div>
    </template>
    <style module>
    .box1{
    widows: 200px;
    height: 200px;
    background-color: red;
    }
    </style>

css使用v-bind

  1. 单文件组件的<style>标签支持使用v-bind('状态'),将CSS的值链接到动态的组件状态
  2. 这个语法同样也适用于<script setup>,且支持JavaScript表达式(需要用引号包裹起来)
  3. 实际的值会被编译成哈希化的CSS自定义属性,因此CSS本身仍然是静态的
  4. 自定义属性会通过内联样式的方式应用到组件的某个元素上,并且在源值变更的时候响应式地更新

类和内联样式

<script setup>
const arr=["box1","box2","box3"]
const arr2=[{box1:true},{box2:false},{box3:true}]
const style={
color:"red",
fontSize:"18px"
}
</script>
<template>
<div :class="arr" :style="style">hello vue</div>
<div :class="arr2"></div>
</template>
<style></style>

props

  • 组件的数据在什么时候生成?

    • 应该是在组件被使用时,即生成组件实例的时候产生,这就涉及到我们要怎么从父组件向子组件传递数据
  • 父组件可以通过props来向子组件传递数据

  • 这种传递方式属于单向数据流,props中的数据是只读的,不可修改。

    • 好处:确保数据的安全性,降低项目的复杂度
    • 原则:对于这种响应式数据,对它的修改只能由唯一组件完成(即创建数据的组件)
    • 只读锁定的是变量中存储的地址值,如果props的某个属性是对象,那这个对象的属性还是可以修改的
      • 即使这种情况可以修改,我们还是要尽量避免修改操作,做到数据不创建就不修改。

组合式api

  • 子组件

    const props=defineProps(["属性名","属性名".............])//定义
    {{props.属性名}}//使用
  • 父组件

    <子组件 :属性名="变量名",属性名=字面量></子组件>
  • 属性名规范

    • 子组件定义时采用驼峰命名法"xxYyZz"
    • 父组件模板内采用"xx-yy-zz"
  • 配置

    const props=defineProps({
    属性名:类型,
    属性名:{
    type:,//指定属性类型
    required:true,//必须有这个属性
    default:,//设置默认值
    validator(value){
    return 布尔值
    }//验证数据
    }//配置对象
    })
    • 类型限制并不影响取值,开发中帮助我们做类型检查让我们项目更加严谨
    • 不符合以上配置并不会报错,而是报警告,便于我们对代码进行优化
    • 对于子组件有定义,父组件未传值的属性,除布尔值是false外,其余类型均是undefined

动态组件

  • Component
    • 最终以什么标签呈现由is属性决定
    • <script setup>
      import {ref} from"vue"
      import A from "./components/A.vue"
      import B from "./components/B.vue"
      const isShow=ref(true)
      </script>
      <template>
      <button @click="isShow=!isShow">切换</button>
      <component :is="isShow?A:B"></component>
      </template>

网页的渲染

  • 浏览器在渲染页面时,做了那些事:

    1. 加载页面的html和css(源码)
    2. html转换为DOM,css转换为CSSOM
    3. 将DOM和CSSOM构建成一课渲染树
    4. 对渲染树进行reflow(回流、重排)(计算元素的位置)
    5. 对网页进行绘制repaint(重绘)
  • 构建渲染树(Render Tree)

    • 从根元素开始检查那些元素可见,以及他们的样式
    • 忽略那些不可见的元素(display:none)【在网页占位置的元素应该进树】
  • 重排、回流

    • 计算渲染树中元素的大小和位置
    • 当页面中的元素的大小或位置发生变化时,便会触发页面的重排(回流)
    • 某个元素改变会影响其他元素,所以需要重排确定其他元素的位置
    • 例如width、height、margin、font-size ......
    • 注意:每次修改这类样式都会触发一次重排!所以如果分开修改多个样式会触发重排多次,而重排是非常耗费系统资源的操作(昂贵),重排次数过多后,会导致网页的显示性能变差,在开发时我们应该尽量的减少重排的次数
      • 把所有需要修改的样式包装成一个类,再把类添加到元素上
      • 修改元素为display:"none",做任意的修改操作,在最后再把display改回来
    • 在现代的前端框架中,这些东西都已经被框架优化过了!(框架操作的是虚拟DOM,无论做了多少次修改,最终只会做一次重排)所以使用vue、react这些框架这些框架开发时,几乎不需要考虑这些问题,唯独需要注意的时,尽量减少在框架中修改原生DOM
  • 重绘

    • 绘制页面
    • 当页面发生变化时,浏览器就会对页面进行重新的绘制
  • 例子:

    <!DOCTYPE html>
    <html lang="zh">
    <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" />
    <title>Document</title>
    <style>
    .box1 {
    width: 200px;
    height: 200px;
    background-color: orange;
    }

    .box2 {
    background-color: tomato;
    }

    .box3 {
    width: 300px;
    height: 400px;
    font-size: 20px;
    }
    </style>
    </head>
    <body>
    <button id="btn">点我一下</button>
    <hr />
    <div id="box1" class="box1"></div>
    <script>
    btn.onclick = () => {
    // box1.classList.add("box2")
    // 可以通过修改class来间接的影响样式,来减少重排的次数
    // box1.style.width = "300px"
    // box1.style.height = "400px"
    // box1.style.fontSize = "20px"
    // box1.classList.add("box3")
    // box1.style.display = "none"
    // box1.style.width = "300px"
    // box1.style.height = "400px"
    // box1.style.fontSize = "20px"
    // div.style.display = "block"
    }
    </script>
    </body>
    </html>

插槽

  • 希望在父组件中指定子组件中的内容,可以通过插槽(slot)来实现该需求

  • 插槽入口:

    <子组件>内容</子组件>
  • 插槽出口

    子组件模板内的<slot>XXX</slot>  XXX为默认,当入口无值时使用
  • 具名插槽

    <slot name="xxx"></slot>

    <子组件>
    <template v-slot:xxx>//v-slot:可以简写为#
    <template>
    </子组件>
  • 默认插槽

    • 直接在子组件书写的文本或标签会被当成默认插槽的内容
    <子组件>内容</子组件>
    相当于:
    <子组件>
    <template v-slot:default>
    <template>
    </子组件>

    对应的出口:
    <slot name="default"></slot>
    <slot></slot>

  • 组件在哪引入就是谁的子组件,子组件可以使用父组件定义的变量

  • 子组件可以通过属性将数据传到父组件,其"兄弟组件"就可以使用该数据

    子组件:<slot name="Box" :stu="stu"></slot>
    父组件:<template v-slot:Box="stu">
    {{ stu }}
    </template>

事件

为元素绑定事件:

  • 绑定事件使用v-on指令
    • v-on:事件名
    • 简写为:@事件名
  • 绑定事件的两种方式:
  1. 内联事件处理器(自己调用函数)

    • 事件触发时,直接执行js语句
    • 内联事件处理器,回调函数的参数由我们自己传递
    • 在内联事件处理器中,可以使用$event来访问事件对象
    foo++
    foo()
  2. 方法事件处理器(vue帮我们调用函数)

    • 事件触发时,vue会对事件的函数进行调用
    • 方法事件处理器,回调函数的参数由vue帮我们传
    • 参数就是事件对象,这个事件对象就是DOM中原生的事件对象,它里边包含了事件触发时的相关信息
    foo
    foo.bar
  • vue如何区分两种处理器:

    检查事件的值是否是合法的js标识符或属性访问路径
    如果是,则表示它是方法事件事件处理器。
    否则,表示它是内联事件处理器
    <script setup>
    function f(text){
    alert(text)
    }
    </script>
    <template>
    <div class="box1" v-on:click="f('box1')">
    <div class="box2" v-on:click="f('box2')">
    //使用.stop阻止事件之间的传导,底层就是event.stopPropagation()
    <div class="box3" v-on:click.stop="f('box3')"></div>
    </div>
    </div>
    </template>

事件修饰符

  • 事件名.stop停止事件的传递
  • 事件名.capture事件在捕获阶段触发
  • 事件名.prevent取消事件默认行为
  • 事件名.self只有事件由自身触发时才会有效
  • 事件名.once绑定一个一次性事件
  • 事件名.passive主要用于提升滚动事件的性能
事件修饰符还可以链式书写
@click.prevent.self 会阻止元素及其子元素的所有点击事件的默认行为,而 @click.self.prevent 则只会阻止对元素本身的点击事件的默认行为。

透传属性

  • 给组件设置属性,会自动给该组件的根元素添加该属性

  • 透传发生在没有被声明为props和emit的属性

  • 透传的属性不会覆盖子组件的属性,而是整合在子组件上

  • 自动的透传只适用于单根组件,多根组件无效

  • 在模板中可以通过 {{$attrs}}来访问透传过来的属性,可以手动指定透传的属性要加到哪些元素上

    • $开头的都是模板中的属性,在脚本中无法访问
    • 要想访问attrs,需要用到钩子
    const attrs=useAttrs()
    console.log(attrs);
  • 关闭自动透传,手动透传仍然有效

    <!--在组合式 API 你需要一个额外的<script>块来书写inheritAttrs: false选项声明来禁止-->
    <script>
    export default{
    inheritAttrs:false
    }
    </script>
  • 使用自定义属性时,最好通过props来传递,使用props相对于使用透传提高了代码的可阅读性

  • 虽然这里的 attrs 对象总是反映为最新的透传,但它并不是响应式的 (考虑到性能因素)。

双向数据绑定

  • 手动给表单双向绑定
    • 把表单的value和变量作绑定,当value发生变化时,变量随之一起变化,这属于单向绑定
    • 当value或响应式变量任意一个发生变化,另一个也会随之变化,双向绑定

v-modle

  • v-model使得我们可以快速进行双向数据绑定
  • v-model等价于@input给响应式变量赋值+value=响应式变量
  • 修饰符
    • .lazy使用change代替input事件,响应式变量在表单失去焦点后才会被赋值,而不是边输边赋值。
    • .trim去除前后空格
    • .number将数据转换为数值,如果是非法数值,则不进行转换

emits

  • 父组件可以通过@xx-yy-zz事件名:自定义方法,来传递自定义方法
  • 子组件
    const emits=defineEmits(['xxYYZZ事件名'])
    emits("xxYYzZZ事件名",参数...)

依赖注入

  • 如果项目由很多层的组件构成,数据的传递就会变成麻烦起来
  • 通过依赖注入,可以跨域多层组件向其他组件传递数据
  • 步骤:
    • 1.设置依赖
      • provide(key,value)
    • 2.注入数据
      • value=inject(key)
  • 注意:当不同父组件设置同名依赖,那遵循就近原则

侦听器

有些情况下,我们需要在状态变化时执行一些“副作用”:例如更改 DOM,或是根据异步操作的结果去修改另一处的状态。 这时候我们就需要用到侦听器来监听状态的变化

选项式

函数式侦听器

watch{
//参数1是新数据,参数2是旧数据
//监听响应式的原始值
age(newData,oldData){
console.log("新"+newData);
console.log("旧"+oldData);
},
//监听响应式对象的某个属性,采用字符串路径的形式
'stu.name'(newData,oldData){
console.log("新"+newData);
console.log("旧"+oldData);
}
}

对象式侦听器

watch: {
//侦听的状态发生变化将调用对应侦听器的handler
age: {
handler(newData, oldData) {
console.log("新" + newData);
console.log("旧" + oldData);
},
//设置
deep:true,
immediate:true,
flush:'post'
},
"stu.name": {
handler(newData, oldData) {
console.log("新" + newData);
console.log("旧" + oldData);
}
}
}
  • deep
    • watch是浅层,当侦听一个对象时,仅仅改变对象的某个属性不会触发回调
    • 在侦听对象里配置deep:true获得深度侦听器
    • 深度侦听需要遍历被侦听对象中的所有嵌套的属性,当用于大型数据结构时,开销很大。因此请只在必要时才使用它,并且要留意性能。
  • immediate
    • 默认情况下,watch是懒执行,仅当数据源变化时,才会执行回调。
    • 设置immediate:true,在创建侦听器时会执行handler
  • flush
    • 侦听器默认情况下handler中访问的DOM将是更新之前的状态
    • 设置flush:"post",使得访问DOM将是更新之后的值
    • 原理:这样设置修改了handler的触发机制(DOM更新后)

创建侦听器

语法:通过组件实例.$watch()创建侦听器

this.$watch(data,method,object)
//data:所侦听的数据源,类型为String
//method:回调函数,参数1新值,参数2旧值
//object:配置对象,可以配置deep...那三个属性
  • 停止这种方式创建的侦听器只需调用$watch()返回的函数

组合Api

watch函数

语法:watch(source,callback,options)

  • source:侦听的数据源,

    • ref(包括计算属性),reactive对象
    • 对象的某个属性(必须提供获取该属性的getter函数)
    • 多个数据源组成的数组
  • callback:数据源改变调用的回调函数,参数1newData,参数2oldData

  • options:配置对象,deep...三个属性

  • 注意:reactive是深度代理,所以对应的watch也是深层的,对象的某个属性变了会触发该对象的侦听,只不过newData===oldData

  • 但对于reactive使用getter方式侦听该对象 ()=>对象,watch就是浅层的

  • 数组里的任一数据源发生改变,触发数组侦听

  • 停止watch函数的侦听器,只要调用该函数返回值即可

watchEffect函数

  • watchEffect(回调函数,options)
  • 创建成功后回调函数立即执行一遍
  • 用到哪些数据源,数据源发生变化,会重新执行回调函数
  • 默认情况下,回调函数的第一次执行会在视图渲染之前执行
  • 默认情况下,回调触发机制在DOM更新之前
    • 设置配置对象flush:'post',会先更新页面,再去回调函数
    • 使用watchPostEffect(回调函数)代替
  • 停止侦听器同之前一样,调用返回值

生命周期

每个Vue组件实例在创建时都需要经历一系列的初始化步骤,比如数据侦听,编译模板,挂载实例到DOM,以及在数据改变时更新DOM。在此过程中,它也会运行被称为生命周期钩子的函数,让开发者有机会在特定阶段运行自己的代码。最常用的是created、mounted、updated和unmounted。

  1. beforeCreate

    • beforeCreate选项式声明周期函数
    • 在组件实例初始化之前调用(props解析已解析、data和computed等选项还未处理)
    • 不能访问组件的实例this及其组件中的数据源和函数等
    • 不能访问组件中的视图DOM元素
    • 组合式 API 中的setup()钩子会在所有选项式 API 钩子之前调用
  2. created

    • created选项式生命周期函数
    • 在组件实例化成功后调用
    • 可访问组件的实例this及其组件中的数据源和函数等
    • 不能访问组件中的视图DOM元素
  3. beforeMount/onBeforeMount

    • beforeMount:选项式生命周期函数、onBeforeMount:组合式生命周期钩子
    • 组件视图在浏览器渲染之前调用
    • 可访问组件实例东西(数据源、函数、计算属性等)
    • 不能访问组件视图中的DOM元素
  4. mounted/onMounted

    • mounted:选项式生命周期函数、onMounted:组合式生命周期钩子
    • 组件视图在浏览器渲染之后调用
    • 可访问组件实例东西(数据源、函数、计算属性等)
    • 可以访问组件视图中的DOM元素
  5. beforeUpdate/onBeforeUpdate

    • beforeUpdate:选项式生命周期函数、onBeforeUpdate:组合式生命周期钩子
    • 数据源发生变化时,组件视图重新渲染之前调用
    • 可访问组件实例东西(数据源、函数、计算属性等)
    • 可以访问该组件中在更新之前的DOM元素,但是不能访问该组件中在更新之后的DOM元素
  6. updated/onUpdated

    • updated:选项式生命周期函数、onUpdated:组合式生命周期钩子
    • 数据源发生变化时,组件视图重新渲染之后调用
    • 可访问组件实例东西(数据源、函数、计算属性等)
    • 不可以访问该组件中在更新之前的DOM元素,但是可以访问该组件中在更新之后的DOM元素
  7. beforeUnmount/onBeforeUnmount

    • beforeUnmount:选项式生命周期函数、onBeforeUnmount:组合式生命周期钩子
    • 组件实例被卸载之前调用
    • 可访问组件实例东西(数据源、函数、计算属性等)
    • 可以访问组件视图中的DOM元素
  8. unmounted/onUnmounted

    • unmounted:选项式生命周期函数、onUnmounted:组合式生命周期钩子
    • 组件实例被卸载之后调用
    • 可访问组件实例东西(数据源、函数、计算属性等)
    • 不可以访问组件视图中的DOM元素
    • 一般在这个生命周期函数里,我们可以手动清理一些副作用,例如计时器、DOM事件监听器或者与服务器的连接

模板引用

如果我们需要直接访问组件中的底层DOM元素,可使用vue提供特殊的ref属性来访问

访问模板引用

  • 在视图元素中采用ref属性来设置需要访问的DOM元素
    1. 该ref属性可采用字符值的执行设置
    2. 该ref属性可采用v-bind::ref的形式来绑定函数,其函数的第一个参数则为该DOM元素
  • 如果元素的ref属性值采用的是字符串形式
    1. 在选项式 API JS中,可通过this.$refs来访问模板引用
    2. 在组合式 API JS中,我们需要声明一个同名的ref变量,来获得该模板的引用
<script setup>
import {ref,onUpdated} from "vue"
const name=ref(null)
const value=ref("小明")
onUpdated(()=>{
console.log(name.value);
})
const getAgeDom=(dom)=>{
console.log(dom);
}
</script>
<template>
<div>姓名:<input type="text" ref="name" v-model="value"></div>
<div>年龄:<input type="text" :ref="getAgeDom" name="age"></div>
</template>

<style scoped></style>

v-for中的模板引用

当在v-for中使用模板引用时:

  1. 如果ref值是字符串形式,在元素被渲染后包含对应整个列表的所有元素【数组】
  2. 如果ref值是函数形式,则会每渲染一个列表元素则会执行对应的函数【不推荐使用】
<script setup>
import { onMounted, ref } from 'vue';

// 书本
let books = ref([
{ id: 1, name: '海底两万里' },
{ id: 2, name: '骆驼祥子' },
{ id: 3, name: '老人与海' },
{ id: 4, name: '安徒生童话' },
])

let bookList = ref(null)

onMounted(() => {
console.log(bookList.value); // 获取引用的 DOM 对象,并打印,发现是数组,
})
</script>

<template>
<ul>
<li v-for="b in books" :key="b.id" ref="bookList">
{{ b.name }}
</li>
</ul>
</template>

组件上的ref

模板引用也可以被用在一个子组件上;这种情况下引用中获得的值是组件实例

  1. 如果子组件使用的是选项式 API ,默认情况下父组件可以随意访问该子组件的数据和函数,除非在子组件使用expose选项来暴露特定的数据或函数,expose值为字符串数组
  2. 如果子组件使用的是组合式 API <script setup>,那么该子组件默认是私有的,则父组件无法访问该子组件,除非子组件在其中通过defineExpose({数据,函数....})

状态管理

  • 状态(state)就是能够跟用户交流的数据
  • 视图(view)用来呈现数据,用户通过视图访问数据
  • 交互(actions)用户的操作,状态会根据用户在视图中的操作发生变化

状态的使用

  • state提升:把state提升到共同的父组件里,再通过依赖注入使用
  • 创建状态store
    • src新建一个stroe文件夹
    • 新建js文件存储state
    • 其他组件要使用该state只需引入该js文件
    • 不足:手动创建,没有代码规范
  • 使用pinia

pinia

Pina是Vue专属状态管理库,它允许你跨组件或页面共享状态

  • vue中的状态存储仓库

  • 引入pinia

    • npm install pinia
    • main.js:import { createPinia } from 'pinia'
    • 创建pinia实例:const pinia = createPinia()
    • 将pinia作为中间件引入app.use(pinia)
  • 利用pinia创建store

    • store它有三个概念,state、getters和actions,我们可以l理解成组件中的data、computed和methods
    • 在项目中的src\store文件夹下不同的store.js文件
    • store是用defineStore(name, function | options)定义的,建议其函数返回的值命名为use...Store方便理解

选项式

import { defineStore } from "pinia";
//defineStore需要两个参数:id,options
export const useCountStore=defineStore("count",{
//state,是一个函数
state:()=>{
//对象的数据就是store存储的数据
return {
count:0
}
},
//基于state的计算属性
getters:{
doubleCount: (state) => state.count * 2,
avgCount(){
return this.count/2
}
},
//方法,对state数据进行一些操作
actions:{
addCount(){
this.count++
}
}
})
  • state中的属性和方法都可以使用store实例对象直接访问
  • store实例本身就是一个reactive对象,如果将state中的数据直接解构出来,那么得到的数据将失去响应性
  • 可以通过const {...}=storeToRefs(store实例)来对store进行解构
    • 它可以将state中的属性和方法以及计算属性(无法解构actions中方法)解构为ref属性,从而保留其响应性

组合式

import { defineStore } from "pinia";
import { computed, ref } from "vue";
//defineStore需要两个参数:id,options
export const useCountStore=defineStore("count",()=>{
const count=ref(0)
function addCount(){
count.value++
}
const doubleCount=computed(()=>{
return count.value*2
})
return {count,addCount,doubleCount}
})
  • ref() 就是 state 属性
  • computed() 就是 getters
  • function() 就是 actions

store

  • 修改state:

    • 直接修改
    • 通过$patch({新的state对象})
      • 对于引用类型不太友好
    • 通过$patch((state)=>{})
    • 直接替换state,$state={...}
    • 重置state,$reset(),使得state重置为初始值
  • 订阅state

    • 当store中的state发生变化时,会调用订阅里的回调函数参数
    //mutation表示修改的信息
    //mutation.type 修改方式 'direct' | 'patch object' | 'patch function'
    //mutation.storeId // 在定义store时传入的那个id
    //mutation.payload // 传递给 store实例对象.$patch() 的参数对象。
    //(只有 mutation.type === 'patch object'的情况下才可用)
    store实例对象.$subscribe((mutation,state)=>{},options)
    • 默认情况含有订阅代码的组件一卸载,那么订阅的效果会随之消失
    • 设置options{detached:true},组件卸载,订阅的效果仍会生效
    • 使用订阅时,不要在回调函数中直接修改state,会触发递归
  • 订阅action

    • 监听 action 和它们的结果。传递给它的回调函数会在 action 本身之前执行。
    store实例对象.$onAction(({...})=>{})
    • onAction会自动帮我们传进一个参数对象{...}
      • name:调用的action的名字
      • store:store实例
      • args:action接收的参数
      • after():可以设置一个回调函数,函数会在action成功调用后触发
      • onError():可以设置一个回调函数,函数会在action调用失败后触发
        • 回调函数帮我们传递err,传递失败信息