Vue 教程

Vue

 Vue 是一款用于构建用户界面的 JavaScript 框架,于2014年开源。它基于标准 HTML、CSS 和 JavaScript 构建,并提供了一套声明式的、组件化的编程模型,帮助你高效地开发用户界面。无论是简单还是复杂的界面,Vue 都可以胜任。

作者

​ 尤雨溪(Evan You),前端框架Vue.js 的作者,中国人,上海读中学,美国读大学。独立开源开发者。

特性

​ Vue.js 构建的是单页面应用程序,单页面应用是一种 Web 应用程序架构,它通过动态更新当前页面的方式来提供用户体验,而不需要加载整个新页面。在单页面应用中,页面内容的更新和切换是通过 JavaScript 动态加载和更新页面的不同部分,而不是通过传统的页面刷新和跳转来实现。

Vue.js 在单页面应用中有很好的表现,因为它提供了以下特性和优势:

  1. 组件化开发:Vue.js 使用组件化开发的方式,将应用程序拆分为多个独立的组件,每个组件负责管理自己的状态和视图。这样的架构使得单页面应用更易于维护和扩展。
  2. 虚拟 DOM:Vue.js 使用虚拟 DOM 技术,能够高效地更新页面的部分内容,提高了页面的渲染性能和用户体验。
  3. 路由管理:Vue.js 提供了 vue-router 插件,用于管理单页面应用中的路由。开发者可以通过定义路由来实现页面之间的导航和切换。
  4. 状态管理:Vue.js 提供了 Vuex 插件,用于集中式管理应用程序的状态。在大型单页面应用中,可以使用 Vuex 来管理全局的应用状态,使得状态管理更加清晰和可控。
  5. 响应式数据绑定:Vue.js 提供了响应式数据绑定的能力,能够将数据和视图进行双向绑定,使得页面的数据和视图保持同步。

由于以上特性,Vue.js 成为了构建单页面应用的流行选择之一,许多现代的 Web 应用程序都采用了 Vue.js 来实现。

SPA(单页应用)

​ SPA(Single-page application,单页应用)是只加载一个单独网页的 web 应用实现,当需要显示不同的内容时,它通过 JavaScript API(例如 XMLHttpRequest 和 Fetch)更新主体内容。

​ 这使得用户在访问网站时无需加载新的页面,可以获得性能提升和动态的体验,但会相应地丧失诸如 SEO(搜索引擎优化)的优势,同时需要更多精力维护状态、实现导航以及做一些有意义的性能监控。

渐进式 JS 框架

​ Vue.js 之所以被称为 “渐进式 JavaScript 框架”,是因为它的设计理念和使用方式使得开发者可以逐步地将其引入到现有的项目中,而不需要一次性地进行全面的重构或转换。

以下是 Vue.js 被称为 “渐进式 JavaScript 框架”的原因:

  1. 逐步采用:Vue.js 可以被逐步地引入到现有的项目中,开发者可以选择性地将 Vue.js 应用于部分页面或组件中,而不需要对整个应用程序进行重构。这样可以降低学习曲线和风险,使得团队更容易接受和采用 Vue.js。
  2. 组件化开发:Vue.js 提倡组件化开发的理念,开发者可以将页面拆分成多个独立的组件,每个组件都有自己的模板、脚本和样式。这种组件化的开发方式使得代码更易于维护和重用。
  3. 数据驱动:Vue.js 采用了数据驱动的方式来管理页面状态和视图更新,通过将数据和视图进行绑定,当数据发生变化时,视图会自动更新。这种方式使得开发者更加专注于应用程序的业务逻辑,而不需要手动操作 DOM。
  4. 灵活性:Vue.js 的设计是非常灵活的,它可以与其他 JavaScript 框架和库(如React、Angular)结合使用,也可以与现有的项目进行无缝集成。这种灵活性使得开发者可以根据项目的需求和特点选择合适的技术栈。

​ 总的来说,Vue.js 作为一种渐进式 JavaScript 框架,可以逐步地引入到现有的项目中,帮助开发者构建高性能、可维护的用户界面。这种渐进式的设计理念使得 Vue.js 在前端开发领域得到了广泛的应用和认可。

优缺点

优点:

  • 渐进式
  • 组件化开发
  • 虚拟dom
  • 响应式数据
  • 单页面路由
  • 数据与视图分开

缺点:

  • 单页面不利于SEO(搜索引擎优化)
  • 首屏加载时间长
  • 不兼容IE

Vue 基础

Vue 的 Hello World 代码如下:

<head>
    <script src="./vue.js" charset="UTF-8"></script>
</head>
<body>
<div id="app">
    {{message}}
</div>
<script>
    var app = new Vue({
        el: '#app',
        data:{
            message:"Hello Vue!"
        }
    })
</script>
</body>

Vue 实例

每个 Vue 应用都是通过用 Vue 函数创建一个新的 Vue 实例开始的:

var vm = new Vue({
	//选项
})

响应式

​ 当一个 Vue 实例被创建时,它将 data 对象中的所有的 property 加入到 Vue 的响应式系统中。当这些 property 的值发生改变时,视图将会产生“响应”,即匹配更新为新的值。

​ 当这些数据改变时,视图会进行重渲染。值得注意的是只有当实例被创建时就已经存在于 data 中的 property 才是响应式的。也就是说如果你添加一个新的 property,比如:

vm.b = 'hi'

​ 那么对 b 的改动将不会触发任何视图的更新。如果你知道你会在晚些时候需要一个 property,但是一开始它为空或不存在,那么你仅需要设置一些初始值。

不需要响应式的数据应该怎么处理?

  • 定义在data的return之外
  • 使用Object.freeze进行数据冻结

生命周期

函数 描述
beforeCreate 实例Vue,未初始化和响应式数据
created 已初始化和响应式数据,可访问数据
beforeMount render调用,虚拟DOM生成,未转真实DOM
mounted 真实DOM挂载完成
beforeUpdate 数据更新,新虚拟DOM生成。
updated 新旧虚拟DOM进行对比,打补丁,然后进行真实DOM更新
beforeDestroy 实例销毁前,仍可访问数据
destroyed 实例销毁后调用。该钩子被调用后,该Vue 实例的所有指令都被解绑,所有的事件监听器被移除,所有的子实例也都被销毁。

生命周期图示:

image-20210816223419222

父子组件生命周期顺序:

父beforeCreate -> 父created -> 父beforeMount -> 子beforeCreate -> 子created -> 子beforeMount -> 子mounted -> 父mounted

常用属性

Vue 实例的常用属性:

el:指定页面元素,定义Vue实例要管理哪一个DOM。

(1)数据

data:Vue 实例的数据对象

components:Vue实例配置局部注册组件

(2)类方法

methods:实例方法

computed:计算属性

watch:侦听属性

filters:过滤器

render:渲染函数,创建虚拟DOM

生命周期
created:在实例创建完成后被立即调用,完成初始化操作
mounted:el挂载到Vue实例上了,开始业务逻辑操作
beforeDestroy:实例销毁之前调用

注:methods 中的方法会作为属性加到this中去了,所以要注意data中的命名不能和methods中冲突;

模板语法

插值

文本(双大括号) Mustache语法

<p>Message: {{ msg }}</p>

双大括号会将表达式解析为文本格式;

双大括号中不仅可以写简单的变量,还可以写 JavaScript表达式,如拼接、运算等。Vue提供了完整的JS表达式支持;

{{ number + 1 }}
{{ ok ? 'YES' : 'NO' }}
{{ message.split('').reverse().join('') }}

HTML(v-html)

<div id="app">
    <p>{{ htmlText }} </p>
    <p v-html="htmlText"></p>
</div>

<script>
    var vm = new Vue({
        el: '#app',
        data:{
            htmlText:"<h1>www</h1>"
        }
    })
</script>

v-html 指令会将表达式解析为HTML格式

指令

指令是带有 v- 前缀的特殊属性,常见的vue 指令如下:

指令 描述
v-text 将表达式的值解析为普通文本;
v-html 将表达式的值解析为HTML;
v-bind 当表达式的值发生变化时,响应式的作用于DOM;
v-on 绑定事件
v-model 在表单元素上创建双向数据绑定
v-if 条件渲染
v-else-if
v-else
v-show
v-for 列表渲染
v-cloak 提升用户体验, 防止网速慢的情况下刷新页面出现等数据格式
v-once 提升性能, 只渲染一次组件和元素;随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。

参数

一些指令能够接收一个“参数”,在指令名称之后以冒号 : 表示。

<a v-bind:href="url">...</a>

在这里 href 是参数, 表示将 表达式 url 与a 标签的 href 属性进行绑定, 从而响应式的更新超链接的访问地址;

事件修饰符

修饰符是以英文半角句号 . 指明的特殊后缀。

<form v-on:submit.prevent="onSubmit">...</form>

缩写

v-bind (缩写::)只能实现数据单向绑定

v-on (缩写:@

事件处理 v-on

可以用 v-on 指令监听 DOM 事件,并在触发时运行一些 JavaScript 代码。

监听事件

<div class="button1">
    <button v-on:click="counter +=1">点击加1</button>
    <p>按钮被点击了{{ counter }} 次.</p>
</div>

<script>
    var vm = new Vue({
        el: '.button1',
        data:{
            counter: 0
        }
    })
</script>

注: v-on: 可缩写为@ ,如 v-on:click 可缩写为 @click

事件的处理逻辑通常是比较复杂的,这时候需要我们单独写一个事件处理方法,使用 methods 属性定义方法:

<div class="button1">
    <button v-on:click="myEvent">点击加1</button>
</div>
<script>
    var vm = new Vue({
        el: '.button1',
        data:{
            counter: 0
        },
        methods:{
            myEvent(){
                this.counter +=1
                alert('按钮被点击了'+ this.counter+'次')
            }
        }
    })
</script>

第二种用法是父组件监听子组件自定义事件,(上面的button按钮是HTML原生自带的元素和点击事件):

子组件:Layout.vue

<template>
    <div ref="mainContainer" class="main_container">
        <entityTree :entityViewKey="entityViewKey" @onNodeSelect="onNodeSelect" />
    </div>
</template>
<script>
import entityTree from "@ynwm/yn-entity-tree/src/yn-entity-tree.vue";	//引入其他子组件
export default {
    components: {
        entityTree
    },
    data() {
        return {
            
        }
    },
    created() {
    },
    mounted() {
    },
    methods: {
        onNodeSelect: function(node,isDefault) {
            this.$emit("onNodeSelect",node,isDefault);
        },
    },
    props: {
        entityViewKey:{
            type: String,
        },
    }
}
</script>

父组件:

<template>
    <div ref="mainContainer" class="main_container">
         <Layout :entityViewKey="entityViewKey" @onNodeSelect="onNodeSelect"></Layout>
    </div>
</template>
<script>
import layout from "./layout.vue";
export default {
    components: {
        "Layout": layout,
    },
    data() {
        return {
            entityViewKey:null,
        }
    },
    methods: {
        onNodeSelect(node,isDefault) {
            //TODO
        },
    }
}
</script>

数据绑定 v-bind

简单示例:

<body>
<div id="app">
    <a v-bind:href = "url">百度一下</a>
</div>
<script>
    var app = new Vue({
        el: '#app',
        data:{
            url:"https://www.baidu.com"
        }
    })
</script>
</body>

v-bind 紧跟在DOM元素的属性上,表示该属性的值交给Vue来解析。

是否加v-bind

Vue中父组件通过prop传值给子组件时,是否加v-bind呢?

结论:

只有传递字符串常量时,不采用v-bind形式,其余情况均采用v-bind形式传递

场景1:传入的值为字符串常量(静态prop)时,不加v-bind。

<blog-post title="My journey with Vue"></blog-post>

场景2:传入的值为变量时,加v-bind。

<blog-post v-bind:title="titleValue"></blog-post>

下面的场景都需要加v-bind:

//无论静态的'42'还是变量totalNumber(动态)的值为42,都要用 `v-bind` 
<blog-post v-bind:total="42"></blog-post>
<blog-post v-bind:total="totalNumber"></blog-post>

无论静态的'false'还是变量booleanValue(动态)的值为false,都要用 v-bind 
<base-input v-bind:favorited="false">
<base-input v-bind:favorited="booleanValue">

无论是静态对象(比如"{name:'bob'}")还是变量对象(比如:postObjec,值为{name:'bob'}),都需要用 v-bind
<blog-post v-bind:post="{name:'bob'}"></blog-post>
<blog-post v-bind:post="postObject"></blog-post>

动态绑定 class

我们可以传给 v-bind:class 一个对象,以动态地切换 class:

双向绑定 v-model

表单输入绑定

​ 可以用 v-model 指令在表单 <input><textarea><select> 元素上创建双向数据绑定。它会根据控件类型自动选取正确的方法来更新元素。尽管有些神奇,v-model 本质上不过是语法糖。它负责监听用户的输入事件以更新数据。

<input v-model="searchText">

等价于:

<input
  v-bind:value="searchText"
  v-on:input="searchText = $event.target.value"
>

v-model 在内部为不同的输入元素使用不同的 property 并抛出不同的事件:

  • text 和 textarea 元素使用 value data属性和 input 事件;
  • checkbox 和 radio 使用 checked data属性和 change 事件;
  • select 字段将 value 作为 prop 并将 change 作为事件。

:warning: 特别注意:

v-model 会忽略所有表单元素的 valuecheckedselected 属性的初始值而总是将 Vue 实例的数据作为数据来源。应该在组件的 data 选项中声明初始值。

文本

<input v-model="message" placeholder="edit me">
<p>Message is: {{ message }}</p>

多行文本

<textarea v-model="message"></textarea>

单选框

<input type="radio" name="mi" v-model="picked" value="100米">100米
<input type="radio" name="mi" v-model="picked" value="200米">200米
<p>picked is: {{ picked }}</p>

多选框

<input type="checkbox" value="100米" v-model="checkedNames"/>100米
<input type="checkbox" value="200米" v-model="checkedNames"/>200米
<span>Checked names: {{ checkedNames }}</span>

选择框

<select v-model="selected">
    <option disabled value="">请选择</option>
    <option>A</option>
    <option>B</option>
    <option>C</option>
  </select>
<span>Selected: {{ selected }}</span>

自定义组件的 v-model

一个自定义组件上的 v-model 默认会利用名为 value 的 prop (子)和名为 input 的事件(父)。

手写一个例子可以更好的理解 v-model 的运行机制:

父组件:

<template>
<div class="app">
    <span>data->title1: {{title1}}</span><br/>
    <second-child1 :value="title1" @cus-event="(v)=>{title1=v}"></second-child2>
    
    <span>data->title2: {{title2}}</span><br/>
    <second-child2 v-model="title2"></second-child1>
</div>
</template>
<script>
import SecondChild1 from './secondChild1.vue';
import SecondChild2 from './secondChild2.vue';
export default {
    data() {
        return {
            title1: "绚丽的父标题1",
            title2: "绚丽的父标题2",
        }
    },
    components: {
        SecondChild2,
        SecondChild1
    }
}
</script>

父组件中使用了两个子组件,写法不同,运行效果是一样的。其中SecondChild1 组件使用 prop + event 的方式实现双向绑定, SecondChild2 组件使用 v-model 实现同样的效果。

子组件 second-child1 内容:

<template>
<div class="app">
    <input type="text" :value="value" @input="inputChange"/>
</div>
</template>
<script>
export default {
    props: {
        value: {
            type: String,
            default: "子组件默认值"
        }
    },
    methods: {
        inputChange(event){
            let v = event.target.value;
            this.$emit("cus-event",v);
        },
    },
}
</script>

子组件 second-child2 内容:

<template>
<div class="app">
    <input type="text" :value="value" @input="inputChange"/>
</div>
</template>
<script>
export default {
    props: {
        value: {
            type: String,
            default: "子组件默认值"
        }
    },
    methods: {
        inputChange(event){
            let v = event.target.value;
            this.$emit("input",v);
        },
    },
}
</script>

如果使用 v-model,子组件的props属性名必须为value,发送给父组件的事件名必须为 input。

可以使用 model 选项来设置接收值的prop 属性和对应的响应事件:(避免属性名或事件名冲突)

<template>
<div class="app">
    <input type="text" :value="value2" @input="inputChange"/>
</div>
</template>
<script>
export default {
    model:{
        prop: "value2",
        event: "input2",
    },
    props: {
        value2: {
            type: String,
            default: "子组件默认值"
        }
    },
    methods: {
        inputChange(event){
            let v = event.target.value;
            this.$emit("input2",v);
        },
    },
}
</script>

修饰符

.lazy 修饰符

​ 默认情况下,v-model 在每次 input 事件触发后将输入框的值与数据进行同步。你可以添加 lazy 修饰符,从而转为在 change 事件_之后_进行同步:

<!-- 在“change”时而非“input”时更新 -->
<input v-model.lazy="msg">

.number 修饰符

如果想自动将用户的输入值转为数值类型,可以给 v-model 添加 number 修饰符:

<input v-model.number="age" type="number">

trim 修饰符

如果要自动过滤用户输入的首尾空白字符,可以给 v-model 添加 trim 修饰符:

<input v-model.trim="msg">
  • .lazy:输入框失焦时才会更新v-model的值
  • .trim:讲v-model绑定的值首位空格给去掉
  • .number:将v-medol绑定的值转数字
  • .stop:阻止事件冒泡
  • .capture:事件的捕获
  • .self:点击事件绑定本身才触发
  • .once:事件只触发一次
  • .prevent:阻止默认事件
  • .native:绑定事件在自定义组件上时,确保能执行
  • .left、.middle、.right:鼠标左中右键的触发
  • passive:相当于给移动端滚动事件加一个.lazy
  • camel:确保变量名会被识别成驼峰命名
  • .sync:简化子修改父值的步骤

条件与循环

条件

  • v-if
  • v-else-if
  • v-else

简单if 判断:

<div class="button1">
    <p v-if="seen">v-if, 能看到我吗?</p>
</div>
<script>
    var vm = new Vue({
        el: '.button1',
        data:{
            seen:false
        }
    })
</script>

if, else-if, else 组合判断:

<div class="button1">
    <button v-on:click="addEvent">加1</button>
    <button @click="subEvent">减1</button>
    <p>counter: {{ counter }} </p>

    <p v-if="counter==0 "> counter等于0</p>
    <p v-else-if="counter>0">counter大于0</p>
    <p v-else> counter小于0</p>
</div>
<script>
    var vm = new Vue({
        el: '.button1',
        data:{
            counter: 0
        },
        methods:{
            addEvent(){
                this.counter +=1
            },
            subEvent(){
                this.counter -=1
            }
        }
    })
</script>

注:v-else 元素必须紧跟在带 v-if 或者 v-else-if 的元素的后面,否则它将不会被识别。

循环

使用 v-for 指令可以基于一个数组渲染一个列表。

<div id="app">
    <ul>
        <li v-for="item in movies">{{item}}</li>
    </ul>
</div>
<script>
    var vm = new Vue({
        el: '#app',
        data:{
            movies:["少年派","泰坦","金刚"]
        }
    })
</script>

为什么v-if和v-for不建议用在同一标签?

v-for优先级高于v-if,每项都通过v-for渲染出来后再去通过v-if判断显隐,做了很多无用功。

显示-隐藏 v-show

v-if 和v-show 区别:

  • v-if:通过操作DOM来控制显隐,适用于偶尔显隐的情况
  • v-show:通过改变样式display属性控制显隐,适用于频繁显隐的情况

v-if 是动态添加,当值为false 时,是完全移除该元素,即dom 树中不存在该元素。

v-show 仅是隐藏/ 显示,值为false 时,该元素依旧存在于dom 树中。 若其原有样式设置了display:none 从而达到隐藏的效果。

计算属性 computed

为什么有计算属性?

**计算属性缓存 vs 方法 **

​ 使用方法 methods 同样可以实现和 computed 一样的结果,不同的是计算属性是基于它们的响应式依赖进行缓存的。只在相关响应式依赖发生改变时它们才会重新求值。这就意味着只要 message 还没有发生改变,多次访问计算属性会立即返回之前的计算结果,而不必再次执行函数。

​ 相比之下,每当触发重新渲染时,调用方法将总会再次执行函数。

​ 我们为什么需要缓存?假设我们有一个性能开销比较大的计算属性 A,它需要遍历一个巨大的数组并做大量的计算。然后我们可能有其他的计算属性依赖于 A。如果没有缓存,我们将不可避免的多次执行 A 的 getter!如果你不希望有缓存,请用方法来替代。

简单示例:

<div id="example">
  <p>Original message: "{{ message }}"</p>
  <p>Computed reversed message: "{{ reversedMessage }}"</p>
</div>
<script>
var vm = new Vue({
  el: '#example',
  data: {
    message: 'Hello'
  },
  computed: {
    // 计算属性的 getter (默认是get方法)
    reversedMessage: function () {
      return this.message.split('').reverse().join('')
    }
  }
})
</script>

计算属性默认只有 getter 方法,不过在需要时你也可以提供一个 setter:

// ...
computed: {
  fullName: {
    // getter
    get: function () {
      return this.firstName + ' ' + this.lastName
    },
    // setter
    set: function (newValue) {
      var names = newValue.split(' ')
      this.firstName = names[0]
      this.lastName = names[names.length - 1]
    }
  }
}

侦听器 watch

​ 计算属性的setter 方法可以监听值的变化,Vue 也提供了一种更通用的方式监听数据的变化,当需要在数据发生变化时执行异步或开销较大的操作时,就可以使用侦听器。watch 指令用于自定义侦听器。

<script>
export default {
    data() {
        return {
            question: '',
        }
    },
  	watch: {
    	question: function (newVal, oldVal) {	// 如果 question发生改变,这个函数就会运行
			//TODO
    	}
    }
}
</script>

watch 属性

  • immediate:初次加载时立即执行
  • deep:是否进行深度监听
  • handler:监听的回调函数

深度监听

<script>
export default {
data () {
    return {
      val1: '',
      value1: '',
      obj: {
        val2: ''
      },
      value2: ''
    }
  },
  watch: {
    val1 (val, oval) {
      this.value1 = val
    },
    obj: {
      handler (val, oval) {
        this.value2 = val.val2
      },
      deep: true
    }
  },
}
</script>

监听器不起效的常见原因:

  1. 写了多个 watch
  2. 期望监听对象中的某个属性,但没有设置深度监听

computed和watch的区别

  • computed:依赖多个变量计算出一个变量,且具有缓存机制,依赖值不变的情况下,会复用计算值。computed中不能进行异步操作
  • watch:通常监听一个变量的变化,而去做一些事,可异步操作
  • 简单记就是:一般情况下computed的多对一,watch一对多

Vue 组件

​ 组件化是Vue中最重要的思想,他提供一种思想,让我们可以开发一个个独立可复用的组件来构建我们的应用,任何应用都可以被抽象成一棵组件树。

组件是可复用的 Vue 实例,且带有一个名字。因为组件是可复用的 Vue 实例,所以它们与 new Vue 接收相同的选项,例如 datacomputedwatchmethods 以及生命周期钩子函数等。

你可以将组件进行任意次数的复用:

<div id="components-demo">
  <button-counter></button-counter>
  <button-counter></button-counter>
  <button-counter></button-counter>
</div>

注意当点击按钮时,每个组件都会各自独立维护它的 count。因为你每用一次组件,就会有一个它的新实例被创建。

使用组件

使用组件3步骤:

  1. 创建组件构造器对象
  2. 注册组件
  3. 使用组件
//3.使用组件 (在Vue的管理范围内才能生效)
<div id="app">
  <myComp></myComp>
</div>

<script>
    //1. 创建组件构造器对象
    const myComponent = Vue.extend({
        template: `<h2>组件标题</h2><p>我是模板内容</p>`
    })
    //2.注册组件
    Vue.component('myComp',myComponent)
    
    var app = new Vue({
        el: '#app'
    })
</script>

注册组件的语法糖写法

//3.使用组件 (在Vue的管理范围内才能生效)
<div id="app">
  <myComp></myComp>
</div>

<script>
    //1. 创建组件构造器对象 2.注册组件
    Vue.component('myComp',{
        template: `<h2>组件标题</h2><p>我是模板内容</p>`
    })
    
    var app = new Vue({
        el: '#app'
    })
</script>

局部注册写法:

//3.使用组件 (在Vue的管理范围内才能生效)
<div id="app">
  <myComp></myComp>
</div>

<script>
    //1. 创建组件构造器对象 2.注册组件
    const myComp = {
        template: `<h2>组件标题</h2><p>我是模板内容</p>`
    }
    
    var app = new Vue({
        el: '#app',
        components:{
            myComp
        }
    })
</script>

组件模板的抽离写法

第一种写法:抽离到 <template> 标签中

<template id="app">
<div>
	<h2>组件标题</h2>
	<p>我是模板内容</p>
</div>
</template>

<script>
    //1. 创建组件构造器对象 2.注册组件
    Vue.component('myComp',{
        template: '#app'
    })
</script>

第二种写法:抽离到 <script> 标签中

<script type="text/x-template" id="app">
<div>
	<h2>组件标题</h2>
	<p>我是模板内容</p>
</div>
</script>

<script>
    //1. 创建组件构造器对象 2.注册组件
    Vue.component('myComp',{
        template: '#app'
    })
</script>

​ 组件不可以直接访问Vue实例中的数据,组件的数据存放在自己内部的data 方法中,组件的 data 必须是一个函数,该函数返回一个对象,对象内部保存着数据。

​ 至此,组件模板的写法有3种:

  1. 字符串模板;
  2. <template> 标签中;
  3. <script> 标签中;

这3种都不是常用的写法,实际开发中使用单文件组件写法。

data 必须是一个函数!

​ 当我们定义一个组件时,你可能会发现它的 data 并不是一个对象,而是一个函数。

一个组件的 data 选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝:

data() {
  return {
    count: 0
  }
}

组件注册

组件注册分为两种:

  • 全局注册
  • 局部注册

全局注册

import UEditor from './ueditor/ueditor.vue';
import UEditorNew from './ueditor-new/index.vue';

install(){
    Vue.component('bUEditor', UEditor);
    Vue.component('bUEditorNew', UEditorNew);
}

全局组件意味着可以在多个Vue实例下使用;

组件名两种风格:

  • 字母全小写且必须包含一个连字符(W3C规范推荐) my-component-name;
  • 帕斯卡命名法 MyComponentName;

全局注册的组件,可以用在任何新创建的Vue实例(new Vue)的模板中。

注:全局注册的组件,一般分为两种,一种是开源的Vue组件库,如Element UI 或 iView 中的组件,一种则是自己团队/平台开发出来的共用组件,推荐将自己平台的共用组件的组件名起一个统一的前缀名,如英文字母 b开头,这样就容易和其他组件库作区分了。

局部注册

在使用 <script setup> 的单文件组件中,导入的组件可以直接在模板中使用,无需注册:

<script setup>
import ComponentA from './ComponentA.vue'
</script>

<template>
  <ComponentA />
</template>

如果没有使用 <script setup>,则需要使用 components 选项来显式注册:

import ComponentA from './ComponentA.js'

export default {
  components: {
    ComponentA
  },
  setup() {
    // ...
  }
}

父子组件

父组件与子组件之间需要通信:

组件之间的传值方式有哪些?

  • 父传子,子组件通过props接收
  • 子传父,子组件使用$emit对父组件进行传值
  • 父子之间通过$parent$chidren获取实例进而通信
  • 通过vuex进行状态管理
  • 通过eventBus进行跨组件值传递
  • provideinject,官方不建议使用
  • $ref获取实例,进而传值
  • 路由传值
  • localStorage、sessionStorage

父组件通过 props 向下传递数据给子组件,子组件通过 events 给父组件发送消息。

组件通信-父传子 props

props: {
    value: {
      type: Boolean,
      required: true
    },
    dataId: {
      type: String,
      default: ''
      required: true
    },
	movies: {
      type: Array,
	  default(){
		return [];
	}
      required: true
    }
  }

type 支持的数据类型:

  • String
  • Number
  • Boolean
  • Array
  • Object
  • Date
  • Function
  • Symbol

注:在Vue 2.5.x 版本之后,要求 Array 和 Object类型的default 必须是一个函数;

当去改变 props 的属性值时,Vue会发出警告:

[Vue 警告]:避免直接改变 prop,因为每当父组件重新渲染时,值都会被覆盖。 相反,根据道具的值使用数据或计算属性。

组件通信-子传父 $emit

子组件往父组件传递信息,需要使用自定义事件

this.$emit('myEvent')

v-on 不仅可以绑定DOM事件,还可以绑定自定义事件。

组件访问-父访问子 $refs

父组件访问子组件有两种方式:

  • this.$children[index]
  • this.$refs.refname

实例:

<div id="app">
    <cpn></cpn>
    <cpn></cpn>
    <x-cpn ref="x-cpn"></x-cpn>
    <cpn></cpn>
</div>
<!-- 下面用两种方式分别访问 x-cpn 组件:-->
<script>
	//1. $refs 方式
	this.$refs.x-cpn
    //2.$children 方式
	this.$children[2]
</script>

注:

​ 实际开发中,$children 用的比较少,因为它通过数组索引下标访问具体的子组件,当子组件的数量或顺序发生变化,这种写法就不再适用。通常都会使用 $refs 来访问子组件;

组件访问-子访问父

子组件访问父组件在实际开发中并不常用,也不推荐这样写。因为这样会使组件的复用性降低,耦合度升高。

//访问父组件
this.$parent  
//访问根组件
this.$root

单文件组件

文件扩展名为 .vue 的 单文件组件,文件内容有3部分:

  • template
  • script
  • style

示例:

<style scoped>
	/** 这里定义组件的样式 */
</style>

<template>
	<!-- 这里定义组件的模板内容 -->
</template>

<script>
	//这里定义组件的业务逻辑
</script>

注:在实际开发中,单文件组件是最常用的写法。

原理

​ Vue 单文件组件是一个框架指定的文件格式,因此必须交由 @vue/compiler-sfc 编译为标准的 JavaScript 和 CSS,一个编译后的单文件组件是一个标准的 JavaScript(ES) 模块。

​ 使用单文件组件必须使用构建工具,在实际项目中,我们一般会使用集成了单文件组件编译器的构建工具,比如 Vite 或者 Vue CLI。

为什么data是个函数并且返回一个对象呢?(Vue2)

防止组件被多个页面使用时,造成的变量互相污染。

组件作用域 CSS

<style> 标签带有 scoped attribute 的时候,它的 CSS 只会影响当前组件的元素

它的实现方式是通过 PostCSS 将以下内容:

<style scoped>
.example {
  color: red;
}
</style>

<template>
  <div class="example">hi</div>
</template>

转换为:

<style>
.example[data-v-f3f3eg9] {
  color: red;
}
</style>

<template>
  <div class="example" data-v-f3f3eg9>hi</div>
</template>

可以看到,转换之后使用的不再是类选择器,而是属性选择器。

子组件的根元素

​ 使用 scoped 后,父组件的样式将不会渗透到子组件中。不过,子组件的根节点会被父组件的作用域样式和子组件的作用域样式同时影响。这样设计是为了让父组件可以从布局的角度出发,调整其子组件根元素的样式。

当子组件只有一个根节点时,父组件中声明的样式会影响到子组件的根节点。但当子组件有多个根节点,父组件的样式不会影响到子组件。

​ 使用 scoped 后,同一个单文件组件内的所有html元素的属性都是一样的,比如都是 [data-v-f3f3eg9]。

插槽-slot

为什么需要插槽?

通过插槽分发内容

编译作用域

规则:

​ 父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。

父组件将内容传给子组件,但内容实在父组件中编译的

默认内容

如果子组件的插槽内容在大多数情况下都是一种,那么可以设置默认内容:

<button type="submit">
  <slot>Submit</slot>
</button>

当父组件使用该子组件时,如果不往插槽里面填值,子组件会有默认的样式进行展示,如果往插槽填值,则会替换默认样式。

具名插槽 slot(废弃)

当我们需要多个插槽时,就需要通过名字来辨别,这就有了具名插槽。

<div class="container">
  <div>
    <slot name="header"></slot>  <!-- 我们希望把页头放这里 -->
  </div>
  <div>
    <slot></slot>		<!-- 我们希望把主要内容放这里 -->
  </div>
  <div>
    <slot name="footer"></slot>		<!-- 我们希望把页脚放这里 -->
  </div>
</div>

注:一个不带 name<slot> 出口会带有隐含的名字“default”。

父组件代码如下:

<base-layout>
  <template v-slot:header>
    <h1>Here might be a page title</h1>
  </template>

  <p>A paragraph for the main content.</p>
  <p>And another one.</p>

  <template v-slot:footer>
    <p>Here's some contact info</p>
  </template>
</base-layout>

作用域插槽 slot-scope (废弃)

首先有一个子组件 bDefaultCustomQuery,我们在父组件中使用bDefaultCustomQuery:

<template>
<div style="height: 100%">
	<bDefaultCustomQuery :queryConfig="queryConfig">
        <template slot="middle" slot-scope="props">
                <label style="font-weight: normal">考核单位:{{props.user.name}}</label>
        </template>
    </bDefaultCustomQuery>
</div>
</template>

​ 在 bDefaultCustomQuery 组件中可以访问 user.name ,但在父组件中不可以直接这么写,因为插槽内容是在父组件中提供并编译的,要想访问 子组件中的属性,需要在子组件的插槽上绑定值,然后父组件使用 slot-scope 进行访问。

v-slot 指令

在 2.6.0 中,我们为具名插槽和作用域插槽引入了一个新的统一的语法 (即 v-slot 指令)。它取代了 slotslot-scope 这两个目前已被废弃但未被移除的 attribute;

指令缩写

​ 跟 v-onv-bind 一样,v-slot 也有缩写,即把参数之前的所有内容 (v-slot:) 替换为字符 #。例如 v-slot:header 可以被重写为 #header

Vue.extend

Vue.extend 是 Vue.js 中的一个全局 API,它可以创建一个 Vue 的子类(构造函数),使得我们可以在应用中注册并使用自定义组件。它的用法如下:

// 创建一个子类
var MyComponent = Vue.extend({
  template: '<div>Hello World</div>'
})

// 注册这个子类
Vue.component('my-component', MyComponent)

​ 在上面的示例中,我们使用 Vue.extend 创建了一个名为 MyComponent 的子类,这个子类有一个 template 选项,用于定义组件的模板内容。然后,我们使用 Vue.component 方法将这个子类注册为一个全局组件,名为 my-component。最后,我们可以在应用中使用 <my-component> 标签来渲染这个自定义组件。

​ 除了 template 选项,Vue.extend 还支持其他的组件选项,比如 propsdatacomputedmethods 等。通过这些选项,我们可以更加灵活地定义和定制自己的组件。

​ 需要注意的是,使用 Vue.extend 创建的子类,不会像普通的 Vue 实例那样自动地挂载到 DOM 上,我们需要手动地将其注册为一个组件或者在某个 Vue 实例中使用它。

// 创建构造器
var Profile = Vue.extend({
  template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>',
  data: function () {
    return {
      firstName: 'Walter',
      lastName: 'White',
      alias: 'Heisenberg'
    }
  }
})
// 创建 Profile 实例,并挂载到一个元素上。
new Profile().$mount('#mount-point')

两者的区别

Vue.extend 和 new Vue 是用于创建 Vue 组件的两种方式,它们有以下区别:

  1. 使用方式:Vue.extend 是一个工厂函数,用于创建一个 Vue 组件的构造器,需要先调用 Vue.extend 创建构造器,然后再通过 new 构造器创建组件实例。而 new Vue 直接创建一个根组件的实例(即创建一个Vue实例)。

  2. 组件复用:Vue.extend 创建的组件构造器可以被多次使用,可以通过多次调用构造器创建多个独立的组件实例,每个实例都具有相同的选项和方法。而 new Vue 创建的组件实例只能是根组件,无法被复用。

  3. 组件选项:Vue.extend 创建的组件构造器可以继承父级组件的选项,可以在构造器中添加/重写选项,比如添加新的生命周期钩子、计算属性等。而 new Vue 创建的组件实例只能使用根组件的选项。

  4. 组件注册:Vue.extend 创建的组件构造器需要手动进行注册,使用 Vue.component() 方法将构造器注册为全局或局部组件。

动态组件

在Vue中,可以通过component标签可以动态切换组件,is 属性用来指定要切换的组件。

<component :is="componentName"></component>

使用场景:

  • 多个页签之间切换。

示例:

<template>

</template>
<script>
export default {

}
</script>

路由

Vue Router 是 Vue.js 的官方路由。它与 Vue.js 核心深度集成,让用 Vue.js 构建单页应用变得轻而易举。功能包括:

  • 嵌套路由映射
  • 动态路由选择
  • 模块化、基于组件的路由配置
  • 路由参数、查询、通配符
  • 展示由 Vue.js 的过渡系统提供的过渡效果
  • 细致的导航控制
  • 自动激活 CSS 类的链接
  • HTML5 history 模式或 hash 模式
  • 可定制的滚动行为
  • URL 的正确编码

router.push、router.replace、router.go的区别

router.push:跳转,并向history栈中加一个记录,可以后退到上一个页面
router.replace:跳转,不会向history栈中加一个记录,不可以后退到上一个页面
router.go:传正数向前跳转,传负数向后跳转

示例:

this.$router.push({path: '/security/user/add'});

定义路由
 {
          path: '/assignment/:id',
          name: 'assignment-detail',
          component: () => import('views/assignment/detail')
        },
		
在组件中获取参数
this.$route.params.id

跳转路由
this.$router.push('/home');

router 和 route

  • router 表示路由器实例对象
  • route 表示当前路由

​ 在本教程中,我们常常以 router 作为路由器实例提及。即由 createRouter() 返回的对象。在应用中,访问该对象的方式取决于上下文。例如,在组合式 API 中,它可以通过调用 useRouter() 来访问。在选项式 API 中,它可以通过 this.$router 来访问。

类似地,当前路由会以 route 被提及。基于不同 API 风格的组件,它可以通过 useRoute()this.$route 来访问。

路由模式

  • hash:哈希模式,根据hash值的更改进行组件切换,而不刷新页面
  • history:历史模式,依赖于HTML5的pushState和replaceState
  • abstract:适用于Node

钩子函数

全局钩子

  • beforeEach:跳转路由前

  • to:将要跳转进入的路由对象

    from:将要离开的路由对象

    next:执行跳转的方法

  • afterEach:路由跳转后

  • to:将要跳转进入的路由对象

路由独享钩子

routes: [
  {
    path: '/xxx',
    component: xxx,
    beforeEnter: (to, from, next) => {
      
    }
  }
]

组件内路由钩子

  • beforeRouteEnter(to, from, next):跳转路由渲染组件时触发
  • beforeRouteUpdate(to, from, next):跳转路由且组件被复用时触发
  • beforeRouteLeave(to, from, next):跳转路由且离开组件时触发

状态管理

Pinia

Pinia 就是一个实现了上述需求的状态管理库,由 Vue 核心团队维护,对 Vue 2 和 Vue 3 都可用。

​ 现有用户可能对 Vuex 更熟悉,它是 Vue 之前的官方状态管理库。由于 Pinia 在生态系统中能够承担相同的职责且能做得更好,因此 Vuex 现在处于维护模式。它仍然可以工作,但不再接受新的功能。对于新的应用,建议使用 Pinia。

​ 事实上,Pinia 最初正是为了探索 Vuex 的下一个版本而开发的,因此整合了核心团队关于 Vuex 5 的许多想法。最终,我们意识到 Pinia 已经实现了我们想要在 Vuex 5 中提供的大部分内容,因此决定将其作为新的官方推荐。相比于 Vuex,Pinia 提供了更简洁直接的 API,并提供了组合式风格的 API,最重要的是,在使用 TypeScript 时它提供了更完善的类型推导。

Vue3 最火的全局状态管理工具肯定是 Pinia 了,那么你们知道 Pinia 的原理是什么吗?原理就是依赖了 effectScope

所以我们完全可以自己使用 effectScope 来实现自己的局部状态管理,比如我们封装一个通用组件,这个组件层级比较多,并且需要共享一些数据,那么这个时候肯定不会用 Pinia 这种全局状态管理,而是会自己写一个局部的状态管理,这个时候 effectScope 就可以排上用场了

Vuex

Vuex 现在处于维护模式。它仍然可以工作,但不再接受新的功能。对于新的应用,建议使用 Pinia。

插件

因为通用工具和Ajax库的生态已经相当丰富,Vue 核心代码没有重复。这也可以让你自由选择自己更熟悉的工具。

axios Http库

Axios 是一个简单的基于 Promise 的 HTTP 客户端,适用于浏览器和 Node.js。 Axios 在一个小包中提供了一个简单易用的库,具有非常可扩展的接口。

GitHub 开源地址 :https://github.com/axios/axios

安装:

npm install axios

执行 GET 请求

// 为给定 ID 的 user 创建请求
axios.get('/user?ID=12345')
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    console.log(error);
  });

// 上面的请求也可以这样做
axios.get('/user', {
    params: {
      ID: 12345
    }
  })
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    console.log(error);
  });

执行 POST 请求

axios.post('/user', {
    firstName: 'Fred',
    lastName: 'Flintstone'
  })
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    console.log(error);
  });

执行多个并发请求

function getUserAccount() {
  return axios.get('/user/12345');
}

function getUserPermissions() {
  return axios.get('/user/12345/permissions');
}

axios.all([getUserAccount(), getUserPermissions()])
  .then(axios.spread(function (acct, perms) {
    // 两个请求现在都执行完成
  }));

Lodash 工具库

Lodash 是一个一致性、模块化、高性能的 JavaScript 实用工具库。遵循 MIT 开源协议发布。

使用lodash:

<script>
import _ from "lodash";
export default {
    data() {
        return {
            dscbOptions: {...}
        }
    },
    methods: {       
        loadDscbOptions() {
            var options = _.cloneDeep(this.dscbOptions);	//深拷贝
        }
    }
}
</script>

Moment 时间库

Moment.js 在 JavaScript 中解析、校验、操作、显示日期和时间。

代码检测 ESlint

ESlint 是一个插件化的javascript代码检测工具,于2013年开源。它的目标是保证代码的一致性和避免错误。

ESLint 是一个开源项目,可以帮助你发现并修复 JavaScript 代码中的问题。 不论你的 JavaScript 是在浏览器还是在服务器,是否使用框架,ESLint 都可以帮助你的代码变得更好。

禁用检查

4种禁用检查的方式

/* eslint-disable */		// 当前代码块禁用检查
consle.log("foo");
consle.log("bar");
/* eslint-disable */

consle.log("foo"); // eslint-disable-line		当前行禁用检查

// eslint-disable-next-line	下一行禁用检查
console.log("foo")

/* eslint-disable */		//当前文件禁用检查:放在代码第一行
consle.log("foo");
consle.log("bar");

关闭检查

打开 build\webpack.base.conf.js 这个文件,找到rules下的config.dev.useEslint ,将createLintingRule方法删除。

脚手架

create-vue

Vue3 官方推荐使用 create-vue 来创建基于 Vite 的新项目

npm create vue@latest

vue-cli

Vue CLI 是官方提供的基于 Webpack 的 Vue 工具链,它现在处于维护模式。我们建议使用 Vite 开始新的项目,除非你依赖特定的 Webpack 的特性。在大多数情况下,Vite 将提供更优秀的开发体验。

使用 vue-cli 可以快速搭建 Vue 开发环境以及对应的webpack 配置,cli (Command-Line-Interface)命令行界面,俗称脚手架。

vue-cli 依赖 Node,vue-cli 4.x 需要Node.js v10 以上

安装 vue-cli:

npm install -g @vue/cli

注:vue cli 工具 在1.x 和 2.x 版本时叫 vue-cli,新版本改名为 @vue/cli,本文档使用 @vue/cli 4.x

初始化项目:

vue init webpack my-project

用法:

vue <command> [options]

create [options] <app-name>                # 创建一个新项目(vue-cli-service)
ui [options]                               # 启动并打开 vue-cli ui
init [options] <template> <app-name>       # 从远程模板生成项目(旧 API,需要 @vue/cli-init)

add [options] <plugin> [pluginOptions]     # 在已创建的项目中安装插件并调用其生成器
invoke [options] <plugin> [pluginOptions]  # 在已经创建的项目中调用插件的生成器
inspect [options] [paths...]               # 使用 vue-cli-service 检查项目中的 webpack 配置
serve [options] [entry]                    # 在开发模式下以零配置提供 .js 或 .vue 文件
build [options] [entry]                    # 使用零配置在生产模式下构建 .js 或 .vue 文件
config [options] [value]                   # 检查和修改配置
info                                       # 打印有关您的环境的调试信息
# 安装依赖
npm install
# 开发环境下启动服务(热更新)
npm run dev
# 为生产环境构建(体积小)
npm run build

构建打包

Vite

Vite 是一个基于 ES Module (ESM)Rollup 的现代化前端构建工具,专为 Vue 3 设计,但也支持其他框架(如 React)。其核心设计理念是通过 按需编译原生 ESM 加载 来提升开发体验。

​ Vite 是一个轻量级的、速度极快的构建工具,对 Vue SFC 提供第一优先级支持。作者是尤雨溪,同时也是 Vue 的作者!

Webpack

Vue3

API 描述
ref
shallowRef ref()的浅层作用形式
reactive()
shallowReactive() reactive() 的浅层作用形式
nextTick DOM更新时机

应用实例

每个 Vue 应用都是通过 createApp 函数创建一个新的 应用实例

import { createApp } from 'vue'

const app = createApp({
  /* 根组件选项 */
})
//将应用实例挂载在一个容器元素中。
app.mount('#app')
//app.mount(document.body.firstChild)

第一个参数是根组件。第二个参数可选,它是要传递给根组件的 props。

挂载

app.mount()

参数可以是一个实际的 DOM 元素或一个 CSS 选择器 (使用第一个匹配到的元素)。返回根组件的实例。

对于每个应用实例,mount() 仅能调用一次。

注册组件

app.component()

如果同时传递一个组件名字符串及其定义,则注册一个全局组件;如果只传递一个名字,则会返回用该名字注册的组件 (如果存在的话)。

// 注册一个选项对象
app.component('my-component', {
  /* ... */
})

// 得到一个已注册的组件
const MyComponent = app.component('my-component')

注册指令

app.directive()

如果同时传递一个名字和一个指令定义,则注册一个全局指令;如果只传递一个名字,则会返回用该名字注册的指令 (如果存在的话)。

// 注册(对象形式的指令)
app.directive('my-directive', {
  /* 自定义指令钩子 */
})

// 注册(函数形式的指令)
app.directive('my-directive', () => {
  /* ... */
})

// 得到一个已注册的指令
const myDirective = app.directive('my-directive')

安装插件

app.use()

第一个参数应是插件本身,可选的第二个参数是要传递给插件的选项。

插件可以是一个带 install() 方法的对象,亦或直接是一个将被用作 install() 方法的函数。插件选项 (app.use() 的第二个参数) 将会传递给插件的 install() 方法。

app.use() 对同一个插件多次调用,该插件只会被安装一次。

import MyPlugin from './plugins/MyPlugin'

app.use(MyPlugin)

应用配置

app.config

每个应用实例都会暴露一个 config 对象,其中包含了对这个应用的配置设定。你可以在挂载应用前更改这些属性 (下面列举了每个属性的对应文档)。

​ 一个用于注册能够被应用内所有组件实例访问到的全局属性的对象。如果全局属性与组件自己的属性冲突,组件自己的属性将具有更高的优先级。

app.config.globalProperties.msg = 'hello'

这使得 msg 在应用的任意组件模板上都可用,并且也可以通过任意组件实例的 this 访问到。

响应式

声明响应式状态 ref

在组合式 API 中,推荐使用 ref() 函数来声明响应式状态。

ref() 接收参数,并将其包裹在一个带有 .value 属性的 ref 对象中返回,要在组件模板中访问 ref,请从组件的 setup() 函数中声明并返回它们:

<script>
import { ref } from 'vue'

export default {
  // `setup` 是一个特殊的钩子,专门用于组合式 API。
  setup() {
    const count = ref(0)

    // 将 ref 暴露给模板
    return {
      count
    }
  }
}
</script>
<template>
    <div>{{ count }}</div>
</template>

注意,在模板中使用 ref 时,我们不需要附加 .value。为了方便起见,当在模板中使用时,ref 会自动解包.

script setup

​ 在 setup() 函数中手动暴露大量的状态和方法非常繁琐。幸运的是,我们可以通过使用单文件组件 (SFC) 来避免这种情况。我们可以使用 <script setup> 来大幅度地简化代码:

<script setup>
import { ref } from 'vue'

const count = ref(0)

function increment() {
  count.value++
}
</script>

<template>
  <button @click="increment">
    {{ count }}
  </button>
</template>

<script setup> 中的顶层的导入、声明的变量和函数可在同一组件的模板中直接使用。你可以理解为模板是在同一作用域内声明的一个 JavaScript 函数。它自然可以访问与它一起声明的所有内容。

深层响应性

Ref 可以持有任何类型的值,包括深层嵌套的对象、数组或者 JavaScript 内置的数据结构,比如 Map

Ref 会使它的值具有深层响应性。这意味着即使改变嵌套对象或数组时,变化也会被检测到:

import { ref } from 'vue'

const obj = ref({
  nested: { count: 0 },
  arr: ['foo', 'bar']
})

function mutateDeeply() {
  // 以下都会按照期望工作
  obj.value.nested.count++
  obj.value.arr.push('baz')
}

​ 可以通过 shallowRef 来放弃深层响应性。对于浅层 ref,只有 .value 的访问会被追踪。浅层 ref 可以用于避免对大型数据的响应性开销来优化性能、或者有外部库管理其内部状态的情况。

非原始值将通过 reactive() 转换为响应式代理,该函数将在后面讨论。

reactive()

​ 还有另一种声明响应式状态的方式: reactive() API。与将内部值包装在特殊对象中的 ref 不同,reactive() 将使对象本身具有响应性:

<script setup>
import { reactive } from 'vue'

const state = reactive({ count: 0 })
</script>

<template>
   <button @click="state.count++">
    {{ state.count }}
  </button>
</template>

​ 响应式对象是 JavaScript Proxy,其行为就和普通对象一样。不同的是,Vue 能够拦截对响应式对象所有属性的访问和修改,以便进行依赖追踪和触发更新。

reactive() 将深层地转换对象:当访问嵌套对象时,它们也会被 reactive() 包装。当 ref 的值是一个对象时,ref() 也会在内部调用它。与浅层 ref 类似,这里也有一个 shallowReactive() API 可以选择退出深层响应性。

​ 只有代理对象是响应式的,更改原始对象不会触发更新。因此,使用 Vue 的响应式系统的最佳实践是仅使用你声明对象的代理版本。为保证访问代理的一致性,对同一个原始对象调用 reactive() 会总是返回同样的代理对象,而对一个已存在的代理对象调用 reactive() 会返回其本身。

reactive() 的局限性

reactive() API 有一些局限性:

  1. 有限的值类型:它只能用于对象类型 (对象、数组和如 MapSet 这样的集合类型)。它不能持有如 stringnumberboolean 这样的原始类型。

  2. 不能替换整个对象:由于 Vue 的响应式跟踪是通过属性访问实现的,因此我们必须始终保持对响应式对象的相同引用。这意味着我们不能轻易地“替换”响应式对象,因为这样的话与第一个引用的响应性连接将丢失:

    let state = reactive({ count: 0 })
    
    // 上面的 ({ count: 0 }) 引用将不再被追踪
    // (响应性连接已丢失!)
    state = reactive({ count: 1 })
  3. 对解构操作不友好:当我们将响应式对象的原始类型属性解构为本地变量时,或者将该属性传递给函数时,我们将丢失响应性连接:

    const state = reactive({ count: 0 })
    
    // 当解构时,count 已经与 state.count 断开连接
    let { count } = state
    // 不会影响原始的 state
    count++
    
    // 该函数接收到的是一个普通的数字
    // 并且无法追踪 state.count 的变化,我们必须传入整个对象以保持响应性
    callSomeFunction(state.count)

由于这些限制,我们建议使用 ref() 作为声明响应式状态的主要 API。

toRefs 和 toRef

  • toRefs可以将reactive返回的对象中的属性都转成ref

    使用场景:想使用响应式对象中的多个或者所有属性做为响应式数据

  • toRef则是转换一个reactive对象中的某个属性为ref

    使用场景:有一个响应式对象数据,但是模版中只需要使用其中一项数据

<script setup>
import { reactive } from 'vue'

const info = reactive({ name:"jack", age: 22 })
//let {age} = info; 如果直接解构响应式对象,则取出的数据会失去响应式能力
let age = toRef(info,"age");
function changeAge(){
    age.value++;    //因为返回的是ref对象,所以需要在脚本中操作.value
}
</script>

toRefs

<script setup>
import { reactive } from 'vue'

const info = reactive({ name:"jack", age: 22 })
//let {name, age} = info; 如果直接解构响应式对象,则取出的数据会失去响应式能力
let {name, age} = toRefs(info);
function changeAge(){
    name.value="jackpot";
    age.value++;    //因为返回的是ref对象,所以需要在脚本中操作.value
}
</script>

DOM 更新时机

​ 当你修改了响应式状态时,DOM 会被自动更新。但是需要注意的是,DOM 更新不是同步的。Vue 会在“next tick”更新周期中缓冲所有状态的修改,以确保不管你进行了多少次状态修改,每个组件都只会被更新一次。

要等待 DOM 更新完成后再执行额外的代码,可以使用 nextTick() 全局 API:

import { nextTick } from 'vue'

async function increment() {
  count.value++
  await nextTick()
  // 现在 DOM 已经更新了
}

模板中自动解包的注意事项

在模板渲染上下文中,只有顶级的 ref 属性才会被解包。

在下面的例子中,countobject 是顶级属性,但 object.id 不是:

const count = ref(0)
const object = { id: ref(1) }

因此,这个表达式按预期工作:

{{ count + 1 }}

但这个不会

{{ object.id + 1 }}

组合式 API

​ 组合式 API (Composition API) 是一系列 API 的集合,使我们可以使用函数而不是声明选项的方式书写 Vue 组件。它是一个概括性的术语,涵盖了以下方面的 API:

  • 响应式 API:例如 ref()reactive(),使我们可以直接创建响应式状态、计算属性和侦听器。
  • 生命周期钩子:例如 onMounted()onUnmounted(),使我们可以在组件各个生命周期阶段添加逻辑。
  • 依赖注入:例如 provide()inject(),使我们可以在使用响应式 API 时,利用 Vue 的依赖注入系统。

​ 组合式 API 是 Vue 3 及 Vue 2.7 的内置功能。对于更老的 Vue 2 版本,可以使用官方维护的插件 @vue/composition-api。在 Vue 3 中,组合式 API 基本上都会配合 script 语法在单文件组件中使用。下面是一个使用组合式 API 的组件示例:

<script setup>
import { ref, onMounted } from 'vue'

// 响应式状态
const count = ref(0)

// 更改状态、触发更新的函数
function increment() {
  count.value++
}

// 生命周期钩子
onMounted(() => {
  console.log(`The initial count is ${count.value}.`)
})
</script>

<template>
  <button @click="increment">Count is: {{ count }}</button>
</template>

​ 虽然这套 API 的风格是基于函数的组合,但组合式 API 并不是函数式编程。组合式 API 是以 Vue 中数据可变的、细粒度的响应性系统为基础的,而函数式编程通常强调数据不可变。

setup()

setup() 钩子是在组件中使用组合式 API 的入口,通常只在以下情况下使用:

  1. 需要在非单文件组件中使用组合式 API 时。
  2. 需要在基于选项式 API 的组件中集成基于组合式 API 的代码时。

注意:对于结合单文件组件使用的组合式 API,推荐通过 <script setup> 以获得更加简洁及符合人体工程学的语法。

​ 我们可以使用响应式 API 来声明响应式的状态,在 setup() 函数中返回的对象会暴露给模板和组件实例。其他的选项也可以通过组件实例来获取 setup() 暴露的属性:

<script>
import { ref } from 'vue'

export default {
  setup() {
    const count = ref(0)

    // 返回值会暴露给模板和其他的选项式 API 钩子
    return {
      count
    }
  },

  mounted() {
    console.log(this.count) // 0
  }
}
</script>

<template>
  <button @click="count++">{{ count }}</button>
</template>

​ 在模板中访问从 setup 返回的 ref 时,它会自动浅层解包,因此你无须再在模板中为它写 .value。当通过 this 访问时也会同样如此解包。

setup() 自身并不含对组件实例的访问权,即在 setup() 中访问 this 会是 undefined。你可以在选项式 API 中访问组合式 API 暴露的值,但反过来则不行。

setup 函数的第一个参数是组件的 props。和标准的组件一致,一个 setup 函数的 props响应式的,并且会在传入新的 props 时同步更新。

​ 请注意如果你解构了 props 对象,解构出的变量将会丢失响应性。因此我们推荐通过 props.xxx 的形式来使用其中的 props。

export default {
  props: {
    title: String
  },
  setup(props, context) {
    console.log(props.title)

    // 透传 Attributes(非响应式的对象,等价于 $attrs)
    console.log(context.attrs)

    // 插槽(非响应式的对象,等价于 $slots)
    console.log(context.slots)

    // 触发事件(函数,等价于 $emit)
    console.log(context.emit)

    // 暴露公共属性(函数)
    console.log(context.expose)
  }
}

defineProps

一个组件需要显式声明它所接受的 props,这样 Vue 才能知道外部传入的哪些是 props,哪些是透传 attribute

父组件向子组件传值的时候,子组件是通过props来接收的。

子组件Child.vue

<script setup>
const props = defineProps({
    title: String,
})

</script>
<template>
<div class="sub">
    <h1>{{title}}</h1>
</div>
</template>

父组件Father.vue

<script setup>
import Child from "./Child.vue";
import {ref} from "vue"

const title = ref("parentMsg")
</script>

<template>
  <div class="parent">
    <h1>This is an parent page</h1>
    <Child :title="title"></Child>
  </div>
</template>

单向数据流

​ 所有的 props 都遵循着单向绑定原则,props 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。这避免了子组件意外修改父组件的状态的情况,不然应用的数据流将很容易变得混乱而难以理解。

​ 另外,每次父组件更新后,所有的子组件中的 props 都会被更新到最新值,这意味着你不应该在子组件中去更改一个 prop。若你这么做了,Vue 会在控制台上向你抛出警告:

const props = defineProps(['foo'])

// ❌ 警告!prop 是只读的!
props.foo = 'bar'

Prop 校验

Vue 组件可以更细致地声明对传入的 props 的校验要求。

<script setup>
defineProps({
  // 基础类型检查
  // (给出 `null` 和 `undefined` 值则会跳过任何类型检查)
  propA: Number,
  // 多种可能的类型
  propB: [String, Number],
  // 必传,且为 String 类型
  propC: {
    type: String,
    required: true
  },
  // 必传但可为 null 的字符串
  propD: {
    type: [String, null],
    required: true
  },
  // Number 类型的默认值
  propE: {
    type: Number,
    default: 100
  },
  // 对象类型的默认值
  propF: {
    type: Object,
    // 对象或数组的默认值
    // 必须从一个工厂函数返回。
    // 该函数接收组件所接收到的原始 prop 作为参数。
    default(rawProps) {
      return { message: 'hello' }
    }
  },
  // 自定义类型校验函数
  // 在 3.4+ 中完整的 props 作为第二个参数传入
  propG: {
    validator(value, props) {
      // The value must match one of these strings
      return ['success', 'warning', 'danger'].includes(value)
    }
  },
  // 函数类型的默认值
  propH: {
    type: Function,
    // 不像对象或数组的默认,这不是一个
    // 工厂函数。这会是一个用来作为默认值的函数
    default() {
      return 'Default function'
    }
  }
})
</script>

透传 attribute

Attributes 继承

“透传 attribute”指的是传递给一个组件,却没有被该组件声明为 props 或 emits 的 attribute 或者 v-on 事件监听器。最常见的例子就是 classstyleid

当一个组件以单个元素为根作渲染时,透传的 attribute 会自动被添加到根元素上。举例来说,假如我们有一个 <MyButton> 组件,它的模板长这样:

一个父组件使用了这个组件,并且传入了 class

<MyButton class="large" />

最后渲染出的 DOM 结果是:

<button class="large">Click Me</button>

这里,<MyButton> 并没有将 class 声明为一个它所接受的 prop,所以 class 被视作透传 attribute,自动透传到了 <MyButton> 的根元素上。

defineEmits

子组件向父组件传递值

子组件Child.vue

<script setup>
const emit = defineEmits(['clickChild'])

function click() {
    let param = {
        name: "childName"
    }
    emit('clickChild', param); //触发事件,传值
}
</script>

<template>
<div class="sub">
    <button v-on:click="click">sub-button</button>
</div>
</template>

父组件Father.vue

<script setup>
import Child from "./Child.vue";

function clickChild(param){
  console.log("父组件接收到子组件的点击事件,参数:",param)
}
</script>

<template>
  <div class="parent">
    <h1>This is an parent page</h1>
    <Child @clickChild="clickChild" ></Child>
  </div>
</template>

defineExpose

Vue3中的 setup 默认是封闭的,如果要从子组件向父组件暴露属性和方法,需要用到defineExpose。

注:defineProps、defineEmits、defineExpose 这三个函数都是内置的,不需要import。

子组件Child.vue

<script setup>
const name = "sub-name";
defineExpose({name});

</script>

父组件Father.vue

<script setup>
import Child from "./Child.vue";
import {ref} from "vue"

const sub = ref()
function clickChild(param){
  console.log("获取子组件属性:",sub.value.name)
}
</script>
<template>
  <div class="parent">
    <h1>This is an parent page</h1>
    <Child ref="sub" @clickChild="clickChild" ></Child>
  </div>
</template>

类和样式绑定

​ 数据绑定的一个常见需求场景是操纵元素的 CSS class 列表和内联样式。因为 classstyle 都是 attribute,我们可以和其他 attribute 一样使用 v-bind 将它们和动态的字符串绑定。但是,在处理比较复杂的绑定时,通过拼接生成字符串是麻烦且易出错的。因此,Vue 专门为 classstylev-bind 用法提供了特殊的功能增强。除了字符串外,表达式的值也可以是对象或数组。

绑定class

我们可以给 :class (v-bind:class 的缩写) 传递一个对象来动态切换 class:

你可以在对象中写多个字段来操作多个 class。此外,:class 指令也可以和一般的 class attribute 共存。

<script setup>
const isActive = ref(true)
const hasError = ref(false)
</script>
<template>
<div class="static"
  :class="{ active: isActive, 'text-danger': hasError }"
></div>
</template>

渲染的结果会是:

<div class="static active"></div>

isActive 或者 hasError 改变时,class 列表会随之更新。举例来说,如果 hasError 变为 true,class 列表也会变成 "static active text-danger"

绑定样式

:style 支持绑定 JavaScript 对象值,对应的是 HTML 元素的 style 属性:

直接绑定一个样式对象通常是一个好主意,这样可以使模板更加简洁:

const styleObject = reactive({
  color: 'red',
  fontSize: '30px'
})
<div :style="styleObject"></div>

模版引用

虽然 Vue 的声明性渲染模型为你抽象了大部分对 DOM 的直接操作,但在某些情况下,我们仍然需要直接访问底层 DOM 元素。要实现这一点,我们可以使用特殊的 ref attribute:

<input ref="input">

ref 是一个特殊的 attribute,和 v-for 章节中提到的 key 类似。它允许我们在一个特定的 DOM 元素或子组件实例被挂载后,获得对它的直接引用。这可能很有用,比如说在组件挂载时将焦点设置到一个 input 元素上,或在一个元素上初始化一个第三方库。

访问模版引用

为了通过组合式 API 获得该模板引用,我们需要声明一个匹配模板 ref attribute 值的 ref:

<script setup>
import { ref, onMounted } from 'vue'

// 声明一个 ref 来存放该元素的引用,必须和模板里的 ref 同名
const input = ref(null)

onMounted(() => {
  input.value.focus()
})
</script>

<template>
  <input ref="input" />
</template>

​ 注意,你只可以在组件挂载后才能访问模板引用。如果你想在模板中的表达式上访问 input,在初次渲染时会是 null。这是因为在初次渲染前这个元素还不存在呢!

生命周期

常用生命周期函数:

API 描述
onMounted() 注册一个回调函数,在组件挂载完成后执行。
onUpdated() 注册一个回调函数,在组件因为响应式状态变更而更新其 DOM 树之后调用。
onBeforeUpdate() 注册一个钩子,在组件即将因为响应式状态变更而更新其 DOM 树之前调用。
onBeforeMount() 注册一个钩子,在组件被挂载之前被调用。
onUnmounted() 注册一个回调函数,在组件实例被卸载之后调用。

示例:

<script setup>
import { ref, onMounted,onUpdated } from 'vue'

const el = ref()

onMounted(() => {
  el.value // <div>
})
    
onUpdated(() => {
  // 文本内容应该与当前的 `count.value` 一致
  console.log(document.getElementById('count').textContent)
})
    
</script>

<template>
  <div ref="el"></div>
</template>

侦听器

在组合式 API 中,我们可以使用 watch 函数在每次响应式状态发生变化时触发回调函数:

watchEffect

​ 一种相对简单的监听 API,你不用显式指定要监听的响应式数据,你只需传入一个函数 source ,然后 Vue 会在初始化时执行 source,执行过程中会触发函数中响应式数据的 get 并收集当前 effect 为依赖。

<script setup>
import { ref, watchEffect } from 'vue'

const count = ref(0)
watchEffect(() => console.log(count.value) // -> logs 0
</script>

watch

watch API 至少需要指定两个参数: sourcecallback,其中 callback 被明确指定只能为函数,所以不同是用方式的差别其实只在 source 。被侦听的对象有以下三种:

  • 响应式数据
  • 函数
  • 数组

响应式数据

source 是由 ref/reactive 初始化的响应式数据时,使用方式如下:

<script setup>
import { ref, watch } from 'vue'

    // ref
    const count = ref(0)
    watch(count, (nv, ov) => {
        
    })
    
    // reactive
    const info = reactive({
      firstName: 'mo',
      lastName: 'dy'
    })    
    watch(info, (nv, ov) => {

    })
</script>

函数

source 是函数时,使用方式如下:

<script setup>
import { ref, watch } from 'vue'
    const count = ref(0)
    watch(() => count.value, (nv, ov) => {
      console.log(`watch count: ${nv}`)
    })

</script>

数组

source 是数组时,数组元素可以是函数和响应式数据之一(不能为数组,限制套娃),使用方式如下:

<script setup>
import { ref, watch } from 'vue'

    const count = ref(0)
    const info = reactive({
      firstName: 'mo',
      lastName: 'dy'
    })
    watch([() => count.value, info], ([newCount, newInfo], [oldCount, oldInfo]) => {

    })
</script>

深层侦听器

直接给 watch() 传入一个响应式对象,会隐式地创建一个深层侦听器——该回调函数在所有嵌套的变更时都会被触发:

const obj = reactive({ count: 0 })

watch(obj, (newValue, oldValue) => {
  // 在嵌套的属性变更时触发
  // 注意:`newValue` 此处和 `oldValue` 是相等的
  // 因为它们是同一个对象!
})

obj.count++

相比之下,一个返回响应式对象的 getter 函数,只有在返回不同的对象时,才会触发回调:

watch(
  () => state.someObject,
  () => {
    // 仅当 state.someObject 被替换时触发
  }
)

你也可以给上面这个例子显式地加上 deep 选项,强制转成深层侦听器:

watch(
  () => state.someObject,
  (newValue, oldValue) => {
    // 注意:`newValue` 此处和 `oldValue` 是相等的
    // *除非* state.someObject 被整个替换了
  },
  { deep: true }
)

谨慎使用

深度侦听需要遍历被侦听对象中的所有嵌套的属性,当用于大型数据结构时,开销很大。因此请只在必要时才使用它,并且要留意性能。

立即执行的侦听器

我们可以通过传入 immediate: true 选项来强制侦听器的回调立即执行:

watch(
  source,
  (newValue, oldValue) => {
    // 立即执行,且当 `source` 改变时再次执行
  },
  { immediate: true }
)

一次性侦听器

每当被侦听源发生变化时,侦听器的回调就会执行。如果希望回调只在源变化时触发一次,请使用 once: true 选项。

watch(
  source,
  (newValue, oldValue) => {
    // 当 `source` 变化时,仅触发一次
  },
  { once: true }
)

组件v-model

v-model 可以在组件上使用以实现双向绑定。

插槽

​ 我们已经了解到组件能够接收任意类型的 JavaScript 值作为 props,但组件要如何接收模板内容呢?在某些场景中,我们可能想要为子组件传递一些模板片段,让子组件在它们的组件中渲染这些片段。

举例来说,这里有一个 <FancyButton> 组件,可以像这样使用:

<FancyButton>
  Click me! <!-- 插槽内容 -->
</FancyButton>

<FancyButton> 的模板是这样的:

<button class="fancy-btn">
  <slot></slot> <!-- 插槽出口 -->
</button>

<slot> 元素是一个插槽出口 (slot outlet),标示了父元素提供的插槽内容 (slot content) 将在哪里被渲染。

最终渲染出的 DOM 是这样:

<button class="fancy-btn">Click me!</button>

通过使用插槽,<FancyButton> 仅负责渲染外层的 <button> (以及相应的样式),而其内部的内容由父组件提供。

渲染作用域

插槽内容可以访问到父组件的数据作用域,因为插槽内容本身是在父组件模板中定义的。举例来说:

<span>{{ message }}</span>
<FancyButton>{{ message }}</FancyButton>

这里的两个 {{ message }} 插值表达式渲染的内容都是一样的。

插槽内容无法访问子组件的数据。Vue 模板中的表达式只能访问其定义时所处的作用域,这和 JavaScript 的词法作用域规则是一致的。换言之:

父组件模板中的表达式只能访问父组件的作用域;子组件模板中的表达式只能访问子组件的作用域。

默认内容

在外部没有提供任何内容的情况下,可以为插槽指定默认内容。比如有这样一个 <SubmitButton> 组件:

<button type="submit">
  <slot></slot>
</button>

如果我们想在父组件没有提供任何插槽内容时在 <button> 内渲染“Submit”,只需要将“Submit”写在 <slot> 标签之间来作为默认内容:

<button type="submit">
  <slot>
    Submit <!-- 默认内容 -->
  </slot>
</button>

​ 当我们在父组件中使用 <SubmitButton> 且没有提供任何插槽内容时,“Submit”将会被作为默认内容渲染,但如果我们提供了插槽内容,那么被显式提供的内容会取代默认内容。

<SubmitButton />
<SubmitButton>Save</SubmitButton>

具名插槽

在一个组件中可以包含多个插槽出口,对于这种场景,<slot> 元素可以有一个特殊的 attribute name,用来给各个插槽分配唯一的 ID,以确定每一处要渲染的内容:

<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

这类带 name 的插槽被称为具名插槽 (named slots)。没有提供 name<slot> 出口会隐式地命名为“default”。

在父组件中使用 <BaseLayout> 时,我们需要一种方式将多个插槽内容传入到各自目标插槽的出口。此时就需要用到具名插槽了:

要为具名插槽传入内容,我们需要使用一个含 v-slot 指令的 <template> 元素,并将目标插槽的名字传给该指令:

<BaseLayout>
  <template v-slot:header>
    <!-- header 插槽的内容放这里 -->
  </template>
</BaseLayout>

v-slot 有对应的简写 #,因此 <template v-slot:header> 可以简写为 <template #header>。其意思就是“将这部分模板片段传入子组件的 header 插槽中”。

下面我们给出完整的、向 <BaseLayout> 传递插槽内容的代码,指令均使用的是缩写形式:

<BaseLayout>
  <template #header>
    <h1>Here might be a page title</h1>
  </template>

  <template #default>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </template>

  <template #footer>
    <p>Here's some contact info</p>
  </template>
</BaseLayout>

当一个组件同时接收默认插槽和具名插槽时,所有位于顶级的非 <template> 节点都被隐式地视为默认插槽的内容。所以上面也可以写成:

<BaseLayout>
  <template #header>
    <h1>Here might be a page title</h1>
  </template>

  <!-- 隐式的默认插槽 -->
  <p>A paragraph for the main content.</p>
  <p>And another one.</p>

  <template #footer>
    <p>Here's some contact info</p>
  </template>
</BaseLayout>

现在 <template> 元素中的所有内容都将被传递到相应的插槽。最终渲染出的 HTML 如下:

<div class="container">
  <header>
    <h1>Here might be a page title</h1>
  </header>
  <main>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </main>
  <footer>
    <p>Here's some contact info</p>
  </footer>
</div>

渲染机制

​ Vue 是如何将一份模板转换为真实的 DOM 节点的,又是如何高效地更新这些节点的呢?我们接下来就将尝试通过深入研究 Vue 的内部渲染机制来解释这些问题。

虚拟DOM

你可能已经听说过“虚拟 DOM”的概念了,Vue 的渲染系统正是基于这个概念构建的。

虚拟 DOM (Virtual DOM,简称 VDOM) 是一种编程概念,意为将目标所需的 UI 通过数据结构“虚拟”地表示出来,保存在内存中,然后将真实的 DOM 与之保持同步。这个概念是由 React 率先开拓,随后被许多不同的框架采用,当然也包括 Vue。

与其说虚拟 DOM 是一种具体的技术,不如说是一种模式,所以并没有一个标准的实现。我们可以用一个简单的例子来说明:

const vnode = {
  type: 'div',
  props: {
    id: 'hello'
  },
  children: [
    /* 更多 vnode */
  ]
}

​ 这里所说的 vnode 即一个纯 JavaScript 的对象 (一个“虚拟节点”),它代表着一个 <div> 元素。它包含我们创建实际元素所需的所有信息。它还包含更多的子节点,这使它成为虚拟 DOM 树的根节点。

​ 一个运行时渲染器将会遍历整个虚拟 DOM 树,并据此构建真实的 DOM 树。这个过程被称为挂载 (mount)。

​ 如果我们有两份虚拟 DOM 树,渲染器将会有比较地遍历它们,找出它们之间的区别,并应用这其中的变化到真实的 DOM 上。这个过程被称为更新 (patch),又被称为“比对”(diffing) 或“协调”(reconciliation)。

​ 虚拟 DOM 带来的主要收益是它让开发者能够灵活、声明式地创建、检查和组合所需 UI 的结构,同时只需把具体的 DOM 操作留给渲染器去处理。

渲染管线

从高层面的视角看,Vue 组件挂载时会发生如下几件事:

  1. 编译:Vue 模板被编译为渲染函数:即用来返回虚拟 DOM 树的函数。这一步骤可以通过构建步骤提前完成,也可以通过使用运行时编译器即时完成。
  2. 挂载:运行时渲染器调用渲染函数,遍历返回的虚拟 DOM 树,并基于它创建实际的 DOM 节点。这一步会作为响应式副作用执行,因此它会追踪其中所用到的所有响应式依赖。
  3. 更新:当一个依赖发生变化后,副作用会重新运行,这时候会创建一个更新后的虚拟 DOM 树。运行时渲染器遍历这棵新树,将它与旧树进行比较,然后将必要的更新应用到真实 DOM 上去。
image-20241202231908795

模板 vs 渲染函数

Vue 模板会被预编译成虚拟 DOM 渲染函数。Vue 也提供了 API 使我们可以不使用模板编译,直接手写渲染函数。在处理高度动态的逻辑时,渲染函数相比于模板更加灵活,因为你可以完全地使用 JavaScript 来构造你想要的 vnode。

那么为什么 Vue 默认推荐使用模板呢?有以下几点原因:

  1. 模板更贴近实际的 HTML。这使得我们能够更方便地重用一些已有的 HTML 代码片段,能够带来更好的可访问性体验、能更方便地使用 CSS 应用样式,并且更容易使设计师理解和修改。
  2. 由于其确定的语法,更容易对模板做静态分析。这使得 Vue 的模板编译器能够应用许多编译时优化来提升虚拟 DOM 的性能表现 (下面我们将展开讨论)。

在实践中,模板对大多数的应用场景都是够用且高效的。渲染函数一般只会在需要处理高度动态渲染逻辑的可重用组件中使用。

渲染函数 & JSX

​ Vue 推荐在绝大多数情况下使用模板来创建你的 HTML。然而在一些场景中,你真的需要 JavaScript 的完全编程的能力。这时你可以用渲染函数,它比模板更接近编译器。

渲染函数是用来生成 虚拟 DOM 的,在底层实现中Vue 会将模板编译成渲染函数,所以理论上,我们也可以不写模板,直接写渲染函数,以获得更好的控制。

h()hyperscript 的简称——意思是“能生成 HTML 的 JavaScript”。这个名字来源于许多虚拟 DOM 实现默认形成的约定。一个更准确的名称应该是 createVnode(),但当你需要多次使用渲染函数时,一个简短的名字会更省力。

基本用法

Vue 提供了一个 h() 函数用于创建虚拟 DOM 节点 vnodes:

import { h } from 'vue'

const vnode = h(
  'div', // type
  { id: 'foo', class: 'bar' }, // props
  [
    /* children */
  ]
)

参数:

  • 第一个参数既可以是一个字符串 (用于原生元素) 也可以是一个 Vue 组件定义。
  • 第二个参数是要传递的 prop
  • 第三个参数是子节点。

创建原生元素

h() 函数的使用方式非常的灵活:

import { h } from 'vue'

// 除了 type 外,其他参数都是可选的
h('div')
h('div', { id: 'foo' })

// attribute 和 property 都可以用于 prop
// Vue 会自动选择正确的方式来分配它
h('div', { class: 'bar', innerHTML: 'hello' })

// class 与 style 可以像在模板中一样
// 用数组或对象的形式书写
h('div', { class: [foo, { bar }], style: { color: 'red' } })

// 事件监听器应以 onXxx 的形式书写
h('div', { onClick: () => {} })

// children 可以是一个字符串
h('div', { id: 'foo' }, 'hello')

// 没有 prop 时可以省略不写
h('div', 'hello')
h('div', [h('span', 'hello')])

// children 数组可以同时包含 vnode 和字符串
h('div', ['hello', h('span', 'hello')])

得到的 vnode 为如下形式:

const vnode = h('div', { id: 'foo' }, [])

vnode.type // 'div'
vnode.props // { id: 'foo' }
vnode.children // []
vnode.key // null

注意事项:

​ 完整的 VNode 接口还包含其他内部属性,但是强烈建议避免使用这些没有在这里列举出的属性。这样能够避免因内部属性变更而导致的不兼容性问题

创建组件

import Foo from './Foo.vue'

// 传递 prop
h(Foo, {
  // 等价于 some-prop="hello"
  someProp: 'hello',
  // 等价于 @update="() => {}"
  onUpdate: () => {}
})

// 传递单个默认插槽
h(Foo, () => 'default slot')

// 传递具名插槽
// 注意,需要使用 `null` 来避免插槽对象被当作是 prop
h(MyComponent, null, {
  default: () => 'default slot',
  foo: () => h('div', 'foo'),
  bar: () => [h('span', 'one'), h('span', 'two')]
})

JSX

JSX 是 JavaScript 的一个类似 XML 的扩展,有了它,我们可以用以下的方式来书写代码:

JSX 最早是由 React 引入,但Vue的JSX和React的JSX 并不完全相同。

const vnode = <div>hello</div>

在 JSX 表达式中,使用大括号来嵌入动态值:

const vnode = <div id={dynamicId}>hello, {userName}</div>

create-vue 和 Vue CLI 都有预置的 JSX 语法支持。

渲染函数案例

下面我们提供了几个常见的用等价的渲染函数 / JSX 语法,实现模板功能的案例:

v-if

模板:

<div>
  <div v-if="ok">yes</div>
  <span v-else>no</span>
</div>

等价于使用如下渲染函数 / JSX 语法:

渲染函数:

h('div', [ok.value ? h('div', 'yes') : h('span', 'no')])

JSX 语法:

<div>{ok.value ? <div>yes</div> : <span>no</span>}</div>

v-for

模板:

<ul>
  <li v-for="{ id, text } in items" :key="id">
    {{ text }}
  </li>
</ul>

等价于使用如下渲染函数 / JSX 语法:

渲染函数:

h(
  'ul',
  // assuming `items` is a ref with array value
  items.value.map(({ id, text }) => {
    return h('li', { key: id }, text)
  })
)

JSX 语法:

<ul>
  {items.value.map(({ id, text }) => {
    return <li key={id}>{text}</li>
  })}
</ul>

v-on

on 开头,并跟着大写字母的 props 会被当作事件监听器。比如,onClick 与模板中的 @click 等价。

渲染函数:

h(
  'button',
  {
    onClick(event) {
      /* ... */
    }
  },
  'Click Me'
)

JSX 语法:

<button
  onClick={(event) => {
    /* ... */
  }}
>
  Click Me
</button>

组件

在给组件创建 vnode 时,传递给 h() 函数的第一个参数应当是组件的定义。这意味着使用渲染函数时不再需要注册组件了 —— 可以直接使用导入的组件:

import Foo from './Foo.vue'
import Bar from './Bar.jsx'

function render() {
  return h('div', [h(Foo), h(Bar)])
}

JSX 语法:

function render() {
  return (
    <div>
      <Foo />
      <Bar />
    </div>
  )
}

不管是什么类型的文件,只要从中导入的是有效的 Vue 组件,h 就能正常运作。

通用API

version

当前所使用的 Vue 版本

import { version } from 'vue'

console.log(version)

nextTick

等待下一次 DOM 更新刷新的工具方法。

当你在 Vue 中更改响应式状态时,最终的 DOM 更新并不是同步生效的,而是由 Vue 将它们缓存在一个队列中,直到下一个“tick”才一起执行。这样是为了确保每个组件无论发生多少状态改变,都仅执行一次更新。

nextTick() 可以在状态改变后立即使用,以等待 DOM 更新完成。你可以传递一个回调函数作为参数,或者 await 返回的 Promise。

<script setup>
import { ref, nextTick } from 'vue'

const count = ref(0)

async function increment() {
  count.value++

  // DOM 还未更新
  console.log(document.getElementById('counter').textContent) // 0

  await nextTick()
  // DOM 此时已经更新
  console.log(document.getElementById('counter').textContent) // 1
}
</script>

<template>
  <button id="counter" @click="increment">{{ count }}</button>
</template>

依赖注入

Prop 逐级透传问题

​ 通常情况下,当我们需要从父组件向子组件传递数据时,会使用 props。想象一下这样的结构:有一些多层级嵌套的组件,形成了一棵巨大的组件树,而某个深层的子组件需要一个较远的祖先组件中的部分数据。在这种情况下,如果仅使用 props 则必须将其沿着组件链逐级传递下去,这会非常麻烦。

​ 注意,虽然这里的 <Footer> 组件可能根本不关心这些 props,但为了使 <DeepChild> 能访问到它们,仍然需要定义并向下传递。如果组件链路非常长,可能会影响到更多这条路上的组件。这一问题被称为“prop 逐级透传”,显然是我们希望尽量避免的情况。

provideinject 可以帮助我们解决这一问题 。一个父组件相对于其所有的后代组件,会作为依赖提供者。任何后代的组件树,无论层级有多深,都可以注入由父组件提供给整条链路的依赖。

Provide (提供)

要为组件后代提供数据,需要使用到 provide() 函数:

<script setup>
import { provide } from 'vue'

provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
</script>

provide() 函数接收两个参数。第一个参数被称为注入名,可以是一个字符串或是一个 Symbol。后代组件会用注入名来查找期望注入的值。一个组件可以多次调用 provide(),使用不同的注入名,注入不同的依赖值。

第二个参数是提供的值,值可以是任意类型,包括响应式的状态,比如一个 ref:

import { ref, provide } from 'vue'

const count = ref(0)
provide('key', count)

注:提供的响应式状态使后代组件可以由此和提供者建立响应式的联系。

应用层 Provide

除了在一个组件中提供依赖,我们还可以在整个应用层面提供依赖:

import { createApp } from 'vue'

const app = createApp({})
app.provide('message','hello!')

​ 在应用级别提供的数据在该应用内的所有组件中都可以注入。这在你编写插件时会特别有用,因为插件一般都不会使用组件形式来提供值。

Inject (注入)

要注入上层组件提供的数据,需使用 inject() 函数:

<script setup>
import { inject } from 'vue'

const message = inject('message')
</script>

​ 如果提供的值是一个 ref,注入进来的会是该 ref 对象,而不会自动解包为其内部的值。这使得注入方组件能够通过 ref 对象保持了和供给方的响应性链接。

组件库

iView

iView 是一套基于Vue.js 的高质量UI组件库。

Form 表单

<template>
    <Form :model="formItem" :label-width="80">
        <FormItem label="Input">
            <Input v-model="formItem.input" placeholder="Enter something..."></Input>
        </FormItem>
        <FormItem label="Select">
            <Select v-model="formItem.select">
                <Option value="beijing">New York</Option>
                <Option value="shanghai">London</Option>
                <Option value="shenzhen">Sydney</Option>
            </Select>
        </FormItem>
        <FormItem label="DatePicker">
            <Row>
                <Col span="11">
                    <DatePicker type="date" placeholder="Select date" v-model="formItem.date"></DatePicker>
                </Col>
                <Col span="2" style="text-align: center">-</Col>
                <Col span="11">
                    <TimePicker type="time" placeholder="Select time" v-model="formItem.time"></TimePicker>
                </Col>
            </Row>
        </FormItem>
        <FormItem label="Radio">
            <RadioGroup v-model="formItem.radio">
                <Radio label="male">Male</Radio>
                <Radio label="female">Female</Radio>
            </RadioGroup>
        </FormItem>
        <FormItem label="Checkbox">
            <CheckboxGroup v-model="formItem.checkbox">
                <Checkbox label="Eat"></Checkbox>
                <Checkbox label="Sleep"></Checkbox>
                <Checkbox label="Run"></Checkbox>
                <Checkbox label="Movie"></Checkbox>
            </CheckboxGroup>
        </FormItem>
        <FormItem label="Switch">
            <i-switch v-model="formItem.switch" size="large">
                <span slot="open">On</span>
                <span slot="close">Off</span>
            </i-switch>
        </FormItem>
        <FormItem label="Slider">
            <Slider v-model="formItem.slider" range></Slider>
        </FormItem>
        <FormItem label="Text">
            <Input v-model="formItem.textarea" type="textarea" :autosize="{minRows: 2,maxRows: 5}" placeholder="Enter something..."></Input>
        </FormItem>
        <FormItem>
            <Button type="primary">Submit</Button>
            <Button style="margin-left: 8px">Cancel</Button>
        </FormItem>
    </Form>
</template>
<script>
    export default {
        data () {
            return {
                formItem: {
                    input: '',
                    select: '',
                    radio: 'male',
                    checkbox: [],
                    switch: true,
                    date: '',
                    time: '',
                    slider: [20, 50],
                    textarea: ''
                }
            }
        }
    }
</script>

Select 选择器

单选、多选

Cascader 级联选择器

image-20211230001509329

DatePicker 日期选择器

概述:选择日期

image-20211230000742200

代码示例:

<template>
      <DatePicker type="date" :value="value1" placeholder="Select date" style="width: 200px"></DatePicker>
</template>
<script>
    export default {
        data () {
            return {
                value1: '2016-01-01',
            }
        }
    }
</script>

TimePicker 时间选择器

代码示例:

Upload 上传

<template>
    <Upload
        multiple
        action="//jsonplaceholder.typicode.com/posts/">
        <Button icon="ios-cloud-upload-outline">Upload files</Button>
    </Upload>
</template>
<script>
    export default {
        
    }
</script>

multiple 上传多个文件

Row/Col 栅格

​ 概述:iView 采用了24栅格系统,将每一行进行24等分,这样可以轻松应对大部分布局问题。使用栅格系统进行网页布局,可以使页面排版美观、舒适。

我们定义了两个概念,行row和列col,具体使用方法如下:

  • 使用row在水平方向创建一行
  • 将一组col插入在row
  • 在每个col中,键入自己的内容
  • 通过设置colspan参数,指定跨越的范围,其范围是1到24
  • 每个row中的col总和应该为24
image-20211230220456336

代码示例:

<template>
    <Row>
        <Col span="12">col-12</Col>
        <Col span="12">col-12</Col>
    </Row>
</template>

Tabs 页签

Table 表格

Page 分页

image-20211230232640675

代码示例:

<template>
    <Page :total="100" show-total />
</template>
<script>
    export default {
        
    }
</script>

props

show-total 显示总数

total 总数

page-size 每页条数

show-sizer 显示每页条数

Drawer 抽屉

<template>
    <Button @click="value1 = true" type="primary">Open</Button>
    <Drawer title="Basic Drawer" :closable="false" v-model="value1">
        <p>Some contents...</p>
        <p>Some contents...</p>
        <p>Some contents...</p>
    </Drawer>
</template>
<script>
    export default {
        data () {
            return {
                value1: false
            }
        }
    }
</script>

Card 卡片

List 列表

<template>
    <div>
        <strong>Default Size:</strong>
        <br><br>
        <List header="Header" footer="Footer" border>
            <ListItem>This is a piece of text.</ListItem>
            <ListItem>This is a piece of text.</ListItem>
            <ListItem>This is a piece of text.</ListItem>
        </List>
    </div>
</template>

提示框

Message 全局提示

在顶部居中显示,并自动消失。

image-20211230001914639

Notice 通知提醒

在界面右上角显示可关闭的全局通知,常用于以下场景:

  • 通知内容带有描述信息
  • 系统主动推送
image-20211230002034820

Alert 警告提示

概述:静态地呈现一些警告信息,可手动关闭。

image-20211230233813138

代码示例:

<template>
    <Alert show-icon>An info prompt</Alert>
    <Alert type="success" show-icon>A success prompt</Alert>
    <Alert type="warning" show-icon>A warning prompt</Alert>
    <Alert type="error" show-icon>An error prompt</Alert>
</template>

show-icon 显示图标

closable 可关闭

模态对话框,在浮层中显示,引导用户进行相关操作。

image-20211230002300961

Split 面板分割

概述:可将一片区域,分割为可以拖拽调整宽度或高度的两部分区域。

image-20211230155023988

代码示例:

<template>
    <div class="demo-split">
        <Split v-model="split1">
            <div slot="left" class="demo-split-pane">
                Left Pane
            </div>
            <div slot="right" class="demo-split-pane">
                Right Pane
            </div>
        </Split>
    </div>
</template>
<script>
    export default {
        data () {
            return {
                split1: 0.4
            }
        },
    }
</script>

Tree 树形控件

简单示例:

<template>
    <Tree :data="data1"></Tree>
</template>
<script>
    export default {
        data () {
            return {
                data1: [
                    {
                        title: 'parent 1',
                        expand: true,
                        children: [
                            {
                                title: 'parent 1-1',
                                expand: true,
                                children: [
                                    {
                                        title: 'leaf 1-1-1'
                                    },
                                    {
                                        title: 'leaf 1-1-2'
                                    }
                                ]
                            },
                            {
                   				.....
                            }
                        ]
                    }
                ]
            }
        }
    }
</script>

Timeline 时间轴

概述:对一系列信息进行时间排序时,垂直地展示。

image-20211230000004452

示例:

<template>
    <Timeline>
        <TimelineItem>
            <p class="time">1976年</p>
            <p class="content">Apple I 问世</p>
        </TimelineItem>
        <TimelineItem>
            <p class="time">1984年</p>
            <p class="content">发布 Macintosh</p>
        </TimelineItem>
        <TimelineItem>
            <p class="time">2011年10月5日</p>
            <p class="content">史蒂夫·乔布斯去世</p>
        </TimelineItem>
    </Timeline>
</template>
<script>
    export default {
        
    }
</script>
<style scoped>
    .time{
        font-size: 14px;
        font-weight: bold;
    }
    .content{
        padding-left: 5px;
    }
</style>

概述:显示网站的层级结构,告知用户当前所在位置,以及在需要向上级导航时使用。

image-20211230001002258
<template>
    <Breadcrumb>
        <BreadcrumbItem to="/">Home</BreadcrumbItem>
        <BreadcrumbItem to="/components/breadcrumb">Components</BreadcrumbItem>
        <BreadcrumbItem>Breadcrumb</BreadcrumbItem>
    </Breadcrumb>
</template>
<script>
    export default {
        
    }
</script>

概述:常用于一组图片的轮播展示,也叫轮播图

image-20211229230428701

Steps 步骤条

概述:拆分某项流程的步骤,引导用户按流程完成任务。

image-20211229234843451

Badge 徽标数

概述:

image-20211229234315850

Transfer 穿梭框

image-20211230001612342

Spin 加载中

Element UI

Element Plus(基于 Vue 3)

Element UI(基于 Vue 2)

Uni APP

​ uni-app 是一个使用 Vue.js 开发所有前端应用的框架,开发者编写一套代码,可发布到iOS、Android、Web(响应式)、以及各种小程序(微信/支付宝/百度/头条/飞书/QQ/快手/钉钉/淘宝)、快应用等多个平台。

Cube UI

滴滴团队开发的一套基于 Vue.js 实现的精致移动端组件库

最佳实践

自定义事件中心

  1. 在顶级父组件中定义 eventHub 事件中心对象,实际上就是实例化一个Vue 新对象:
data() {
    return {
        eventHub:new Vue(),
    }
}

2.该对象以 v-bind 的方式传入所有有可能触发和监听事件的子组件中:

<template>
    <div class="front_right_container">
        <FrontRighttView :eventHub="eventHub"></FrontRighttView>
    </div>
</template>

子组件需要在自己的props 中声明eventHub属性,以接收父组件传来的eventHub:

props: {
    eventHub: {
        type: Object,
        require: true,
    },
}

3.子组件监听指定事件,并接收参数:

mounted: function () {
    this.eventHub.$on("unitChange",(_node)=>{
        this.queryData(_node);
    });
},

4.该模块的其他任何引用该eventHub对象的组件,不分层级和父子,都可以触发事件:

this.eventHub.$emit("unitChange",_node);

nextTick的用处

修改数据时不能马上得到最新的DOM信息,所以需要使用nextTick,在nectTick回调中可以获取最新DOM信息

this.$nextTick(() => {
                    this.showHidden = true;
                })

Class 绑定-替换

绑定 class 属性

  • 对象方式
  • 间隔方式
<div :class="{main_container:true,main_container_2:isError}">
    <div :class="'circle '+state+' '+shape">

    </div>
</div>

Style 绑定-替换

<div :style="{ backgroundImage: 'url(' + imageUrl + ')' }">
        
</div>

Tmeplate 和 JSX

Vue 3 支持两种模板语法:JSX 和 Template。它们都有自己的优缺点和适用场景。

JSX 是一种 JavaScript 语法扩展,它允许开发者在 JavaScript 代码中嵌入 HTML。JSX 代码在编译时会被转换成普通的 JavaScript 代码,这样就可以被浏览器或者 Node.js 运行环境识别和执行。

Tmeplate 和 JSX 的性能孰优孰劣?

  • 编译时:JSX 编译比 Template 快
  • 运行时:Template 性能比 JSX 好

因为 Template 解析时会有静态节点提升这一步,而 JSX 没有,所以编译肯定是 JSX 更快,但是到了运行时的时候, Template 的性能会更好,因为它的更新效率更高。

README

作者:银法王

版权声明:本文遵循知识共享许可协议3.0(CC 协议): 署名-非商业性使用-相同方式共享 (by-nc-sa)

参考:

Vue3 官方文档

Vue2 官方教程 【Vue 2 已于 2023 年 12 月 31 日停止维护】

View UI(iView)官网

「Vue2+Vue3」 的 62 个知识点,看看你掌握了几个?

修改记录:

  2020-08-08 第一次修订

​ 2022-01-01 第二次修订 基于iView开发前端页面

​ 2022-01-06 补充组件基础知识


Vue 教程
http://jackpot-lang.online/2021/01/08/前端之路/Vue使用心得/
作者
Jackpot
发布于
2021年1月8日
许可协议