Vue3语法与组件基础

218 阅读1分钟

前言:简介与安装

Vue.js是什么?

Vue是一套用于构建用户界面的渐进式框架,并被设计为自底向上逐层应用。Vue的核心库只关心视图层。

安装

  1. CDN包导入:制作原型或学习时,可以这样使用最新版本: <script src="https://unpkg.com/vue@next"></script>
  2. 下载JavaScript文件并自行托管:下载相关文件并自行托管到在你的服务器上,然后通过<script>标签引入。
  3. npm:在用. Vue 构建大型应用时推荐使用 npm 安装
# 最新稳定版
$ npm install vue@next

如果想使用单文件组件,还需要安装@vue/compiler-sfc:

$ npm install -D @vue/compiler-sfc
  1. Vue CLI:为单页面快速搭建繁杂的脚手架,为现代前端工作流提供了功能完备的构建设置,并带有热重载、保存时lint校验。
yarn global add @vue/cli
# 或
npm install -g @vue/cli

然后:

vue upgrade --next
  1. Vite(推荐):Vite也是一个web开发构建工具,可以实现闪电般的冷服务器启动。使用npm时:
 # npm 6.x
 $ npm init vite@latest <project-name> --template vue

 # npm 7+,需要加上额外的双短横线
 $ npm init vite@latest <project-name> -- --template vue

 $ cd <project-name>
 $ npm install
 $ npm run dev

一、语法

1.模板语法

Vue.js使用了基于HTML的模板语法,允许开发者声明式地将DOM绑定至底层组件实例的数据。在底层的实现上,Vue将模板编译成虚拟DOM渲染函数,结合响应式系统,Vue可以智能计算出最少需要重新渲染多少组件,把DOM操作数减到最少。所有的Vue模板都是合法的HTML。

  • 文本插值:最常见的形式是“Mustache”双大括号语法的文本插值:

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

    该Mustache标签将被替换为对应的组件实例中msgproperty的值,且响应式更新。(使用v-once指令则执行一次性插值,但可能会影响该节点上的其他数据绑定。)

  • 原始HTML字符串插值:由于双大括号会渲染为普通文本。为了输出真正的HTML,需要使用v-html指令:

    <div id="example1" class="demo">
    <p>Using v-html directive: <span v-html="rawHtml"></span></p>
    </div>
    
    const RenderHtmlApp = {
        data() {
            return {
              rawHtml: '<span style="color: red">This should be red.</span>'
            }
        }
    }
    Vue.createApp(RenderHtmlApp).mount('#example1')
    

    其中rawHtmlproperty的字符串将被渲染为html代码到span中。

  • HTML特性插值:双大括号语法不能在HTML特性中使用,替代办法是使用v-bind指令与组件实例的property进行绑定。

    <div v-bind:id="dynamicId"></div>
    

    如果绑定的值是nullundefined,则该特性不会包含在渲染的元素上。

  • 使用JavaScript表达式:在模板中,既可以只绑定简单的 property 键值,也可以支持完全的JavaScript表达式的支持。例如:

    {{ number + 1 }}
    
    {{ ok ? 'YES' : 'NO' }}
    
    {{ message.split('').reverse().join('') }}
    
    <div v-bind:id="'list-' + id"></div>
    

2.Data属性、计算属性、侦听器

(1)Data属性与方法

  • Data 属性 组件的data选项是一个函数,返回一个对象,Vue会通过响应式系统将其包裹起来,并以$data的形式存储在组件实例中。同时,该对象的任何顶级property也会直接通过组件实例暴露出来。Vue 将使用 $ 前缀通过组件实例暴露自己的内置 API。
const app = Vue.createApp({
  data() {
    return { count: 4 }
  }
})

const vm = app.mount('#app')

console.log(vm.$data.count) // => 4
console.log(vm.count)       // => 4

// 修改 vm.count 的值也会更新 $data.count
vm.count = 5
console.log(vm.$data.count) // => 5

// 反之亦然
vm.$data.count = 6
console.log(vm.count) // => 6
  • 方法: 我们使用methods选项向组件实例添加方法,它也是一个包含所需方法的对象:
const app = Vue.createApp({
  data() {
    return { count: 4 }
  },
  methods: {
    increment() {
      // `this` 指向该组件实例
      this.count++
    }
  }
})

const vm = app.mount('#app')

console.log(vm.count) // => 4

vm.increment()

console.log(vm.count) // => 5

Vue会为methods自动绑定this,并始终指向组件实例。所以在定义方法时应避免使用箭头函数。 这些methods和组件实例的其他所有property一样均可在组件的模板中访问。在模板中,它们通常被当作事件监听使用:

<button @click="increment">Up vote</button>

当然也可以直接从模板(双大括号)中直接调用方法,但是换做计算属性会更好:

<span :title="toTitleDate(date)">
  {{ formatDate(date) }}
</span>

注意,从模板中调用方法时不应该存在任何副作用(更改数据或者触发异步进程)。

(2)计算属性、侦听器

  • 计算属性:如果模板中放入太多的逻辑会使模板过重且难以维护,对于任何包含响应式数据的复杂逻辑,应该使用计算属性。例如:
<div id="computed-basics">
  <p>Has published books:</p>
  <span>{{ publishedBooksMessage }}</span>
</div>
Vue.createApp({
  data() {
    return {
      author: {
        name: 'John Doe',
        books: [
          'Vue 2 - Advanced Guide',
          'Vue 3 - Basic Guide',
          'Vue 4 - The Mystery'
        ]
      }
    }
  },
  computed: {
    // 计算属性的 getter
    publishedBooksMessage() {
      // `this` 指向 vm 实例
      return this.author.books.length > 0 ? 'Yes' : 'No'
    }
  }
}).mount('#computed-basics')

上面的例子声明了一个计算属性publishedBooksMessage,且Vue知道 vm.publishedBookMessage 依赖于 vm.author.books,因此当 vm.author.books 发生改变时,所有依赖 vm.publishedBookMessage 的绑定也会更新。

计算属性默认只有getter,当然在需要时也可以手动提供一个setter。

3.渲染

(1)条件渲染

  • v-if:用于条件性渲染,只有在指令表达式返回truthy的时候被渲染。如果要应用于多个元素上,可以在包裹元素上面使用v-if,最终的渲染结果将不包含该包裹元素。
<template v-if="ok">
  <h1>Title</h1>
  <p>Paragraph 1</p>
  <p>Paragraph 2</p>
</template>

同时也可以使用v-else指令来表示v-if的else块,使用v-else-if指令来表示v-if的else-if块。

  • v-show:用于条件性展示,只是简单地切换元素的display CSS属性。 注意不推荐同时使用 v-if 和 v-show。

(2)列表渲染

  • v-for:用于将一个数组映射为一组元素。v-for 指令需要使用 item in items 形式的特殊与发展,其中的 items 是源数据数组,而 item 则是被迭代的数组元素的别名。
<ul id="array-rendering">
  <li v-for="item in items">
    {{ item.message }}
  </li>
</ul>
Vue.createApp({
    data() {
        return {
            items: [{ message: 'Foo'}, { message: 'Bar' }]
        }
    }
}).mount('#array-rendering')

使用 v-for 时有三个参数,分别是value、key(可选)、index(可选)。

当 v-for 渲染的元素列表更新时,默认使用“就地更新”的策略。如果数据项的顺序被改变,Vue不会移动DOM元素来匹配新顺序,而是就地更新每个元素,非常高效,但是只适用于不依赖子组件状态或临时DOM状态(例如表单输入值)的列表渲染输出。

为了能够跟踪每个节点的身份,从而重用和重排序现有的元素,你需要为每项提供一个唯一的keyattribute。

<div v-for="item in items" :key="item.id">
  <!-- 内容 -->
</div>

即 尽可能在使用 v-for 时提供keyattribute。

类似于 v-if ,也可以利用带有 v-for 的包裹元素来循环渲染一段包含多个元素的内容:

<ul>
    <template v-for="item in items" :key="item.msg">
        <li>{{ item.msg }}</li>
        <li class="divider" role="presentation"></li>
</ul>

当 v-for 与 v-if 一同使用时,v-if 的优先级更高,即v-if 没有权限访问 v-for 中的变量。

4.表单绑定(双向绑定应用)

(1)基本用法:

  • 文本
<div id="v-model-basic" class="demo">
    <input v-model="message" placeholder="edit me" />
    <p>Message is: {{ message }}</p>
</div>
Vue.createApp({
    data() {
        return {
            message: ''
        }
    }
}).mount('#v-model-basic')
  • 多行文本
  • 复选框:单个复选框,绑定到布尔值;多个复选框,绑定到同一数组。
  • 单选框
  • 选择框

(2)值绑定

v-model 绑定的值通常是静态字符串,但有时我们想将值绑定到当前活动实例的一个动态 property 上,这时可以使用 v-bind 。

  • 复选框
<input type="checkbox" true-value="yes" false-value="no" />
// 当选中时:
vm.toggle === 'yes'
// 当未选中时:
vm.toggle === 'no'
  • 单选框
<input type="radio" v-model="pick" v-bind:value="a" />
//当被选中时
vm.pick === vm.a
  • 选择框选项
<select v-model="selected">
    <!-- 内联对象字面量 -->
    <option :value="{ number: 123 }">123</option>
</select>
//当选中时
typeof vm.selected // => 'object'
vm.selected.numver // => 123

二、组件基础

1.组件注册

注意区分应用实例和组件实例

每个 Vue 应用都是通过createApp函数创建一个新的应用实例开始的,而应用实例的作用是在该应用中注册子组件

const app = Vue.createApp({
    /* options */
})

传递给createApp的 options 用于配置根组件,挂载应用到某个DOM元素时,这个根组件将被用作渲染的起点。

const RootComponent ={
    /* options */
}
const app = Vue.createApp(RootComponent)
const vm = app.mount('#app')

用于挂在应用实例到DOM元素的方法mount并不返回应用本身,而是返回的根组件实例

大多数的真实应用都是被组织成一个嵌套的、可重用的组件树。例如:

Root Component
└─ TodoList
   ├─ TodoItem
   │  ├─ DeleteTodoButton
   │  └─ EditTodoButton
   └─ TodoListFooter
      ├─ ClearTodosButton
      └─ TodoListStatistics

每个组件都有自己的组件实例。某些组件在任何时候都可能有多个组件实例渲染,则这个应用中的所有组件实例共享同一个应用实例

组件名

在注册组件时需要给它一个名字。在全局注册时,该组件名就是app.component的第一个参数。

const app = Vue.createApp({...})

app.component('my-component-name', {
    /* ... */
})
  • 组件的组织: 一个应用通常以一个组件树的形式组织,为了能在模板中使用组件,这些组件必须先注册。
  • 全局注册:通过app.component来创建组件。这些组件在注册之后可以用于任何新创建的组件实例的模板中。例如:
const app = Vue.createApp({})

app.component('component-a', {
  /* ... */
})
app.component('component-b', {
  /* ... */
})
app.component('component-c', {
  /* ... */
})

app.mount('#app')
<div id="app">
  <component-a></component-a>
  <component-b></component-b>
  <component-c></component-c>
</div>

上述代码中,三个组件在注册之后可以用于任何新创建的组件实例的模板中(指<div>代码块)

  • 局部注册:一般情况下全局注册是不够理想的,全局注册所有的组件意味着即便不再使用其中的一个组件了,它仍然会被包含在最终的构建结果中。 这种情况下,可以通过一个普通的JavaScript对象来定义组件:
const ComponentA = {
    /* ... */
}
const ComponentB = {
    /* ... */
}
const ComponentC = {
    /* ... */
}

然后在components选项中定义你想要使用的组件:

const app = Vue.createApp({
    components: {
        'component-a': ComponentA,
        'component-b': ComponentB
    }
})

注意:局部注册的组件在其子组件中不可用,例如如果希望ComponentA 在 ComponentB 中可用,则需要这样写:

const ComponentA = {
    /* ... */
}

const ComponentB = {
    components: {
        'component-a': ComponentA
    }
    /* ... */
}

2.Props

Prop是可以在组件上注册的一些自定义的 attribute,我们将 通过Prop来向子组件传递数据。 例如:

const app = Vue.createApp({})

app.component('blog-post', {
  props: ['title'],
  template: `<h4>{{ title }}</h4>` // 模板
})

app.mount('#blog-post-demo')
<div id="blog-post-demo" class="demo">
  <blog-post title="My journey with Vue"></blog-post> 
  <blog-post title="Blogging with Vue"></blog-post>
  <blog-post title="Why Vue is so fun"></blog-post>
</div>
  • 限定Prop类型 例如:
props: {
  title: String,
  likes: Number,
  isPublished: Boolean,
  commentIds: Array,
  author: Object,
  callback: Function,
  contactsPromise: Promise // 或任何其他构造函数
}
  • 动态传值:通过 v-bind 动态赋值,例如:
<!-- 动态赋予一个变量的值 -->
<blog-post :title="post.title"></blog-post>

<!-- 动态赋予一个复杂表达式的值 -->
<blog-post :title="post.title + ' by ' + post.author.name"></blog-post>
  • 单向数据流:所有的 prop 都使得其父子 prop 之间形成了一个单项下行绑定,即父级 prop 的更新会向下流动到子组件中,但是反过来则不行,这样设计的好处是防止从子组件意外变更父组件的状态。下面是两个常见的试图改变一个 prop 的情形:
/*
1. 这个prop用于传输一个初始值,子组件希望将其作为一个本地的prop数据来使用。
最好使用定义一个 data property 并将这个 prop 作为其初始值。
*/
props: ['initialCounter'],
data() {
  return {
    counter: this.initialCounter
  }
}
/*
2. 这个prop以一种原始的值传入且需要进行转换。
最好使用这个prop的值来定义一个计算属性。
*/
props: ['size'],
computed: {
  normalizedSize() {
    return this.size.trim().toLowerCase()
  }
}

3.非 Prop 的 Attribute

非 Prop 的 Attribute 指的是传向一个组件,但是该组件并没有相应的 props 或 emits 定义的attribute。比如 class, style, id attribute。我们可以通过 $attrs property 来访问这些 attribute。

特性:如果组件返回单个根节点,则 非 prop 的 attribute 将自动添加到根节点的 attribute 中。例如:

app.component('date-picker', {
  template: `
    <div class="date-picker">
      <input type="datetime-local" />
    </div>
  `
})
<!-- 具有非 prop 的 attribute 的 date-picker 组件-->
<date-picker data-status="activated"></date-picker>

<!-- 渲染后的 date-picker 组件 -->
<div class="date-picker" data-status="activated">
  <input type="datetime-local" />
</div>

在事件监听器上也同样适用:

<date-picker @change="submitChange"></date-picker>
app.component('date-picker', {
  created() {
    console.log(this.$attrs) // { onChange: () => {}  }
  }
})

如果不希望组件的根元素继承attribute,可以在组件的选项中设置 inheritAttrs: false。一般这么做的原因是要将 attribute 应用到除了根节点以外的元素中,这时可以通过组件的 $attrs property 将这个attribute 应用到其他元素上。例如:

app.component('date-picker', {
  inheritAttrs: false,
  template: `
    <div class="date-picker">
      <input type="datetime-local" v-bind="$attrs" />
    </div>
  `
})
<!-- date-picker 组件使用非 prop 的 attribute -->
<date-picker data-status="activated"></date-picker>

<!-- 渲染后的 date-picker 组件 -->
<div class="date-picker">
  <input type="datetime-local" data-status="activated" />
</div>

如果存在多个根节点,则该组件不具有自动的 attribute 隐式贯穿行为。这就需要手动显式地绑定 $attrs 了,否则运行时将发出警告。例如:

<custom-layout id="custom-layout" @click="changeValue"></custom-layout>
// 这将发出警告
app.component('custom-layout', {
    template: `
    <header>...</header>
    <main>...</main>
    <footer>...</footer>
    `
})

// 下面这样写将不会发出警告,$attrs 被显式地传递到了 <main> 元素
app.component('custom-layout', {
    template: `
        <header>...</header>
        <main v-bind="$attrs">...</main>
        <footer>...</footer>  
    `
})

4.自定义事件

(1)事件名与自定义事件

和组件和 prop 一样,事件名提供了自动的大小写转换,且如果在子组件中触发了一个 驼峰式命名 的事件,则可以在父组件中添加一个 短横线分隔命名 的监听器。

定义自定义事件使用 emits 选项:

app.component('custom-form', {
    emits: ['inFocus', 'submit']
})

这样就定义了新的事件侦听器

(2)对抛出的事件的验证

与 prop 的类型检验类似,如果使用对象语法而不是数组语法定义发出的事件,则可以对其进行验证,即为事件分配一个函数,且该函数接收传递给 $emit 调用的参数,并返回一个布尔值以指示事件是否有效:

app.component('custom-form', {
    emits: {
        //没有验证
        click: null,
        
        //验证 submit 事件
        submit :({ email, password }) => {
            if (email && password) {
                return true
            } else {
                console.warn('Invalid submit event payload')
                return false
            }
        }
    },
    
    methods: {
        submitForm(email, password) {
            this.$emit('submit', { email, password })
        }
    }
})

(3)监听子组件事件

当我们在开发与一个组件时,它的一些功可能需要与父组件进行沟通,即 通过父组件来监听子组件上触发的事件:

const App = {
    data() {
        return {
            posts: [
                /* ... */
            ],
            postFontSize: 1
        }
    }
}

const app = Vue.createApp(App)

app.component('blog-post', {
    props: ['title'],
    template: `
        <div class="blog-post">
            <h4>{{ title }}</h4>
            <button @click="$emit('enlargeText')">
                Enlarge text
            </button>
        </div>
    `
})

当点击按钮时,组件实例提供一个自定义事件系统,父组件就可以像处理原生DOM事件一样通过v-on@来监听子组件实例的任意事件

<blog-post ... @enlarge-text="postFontSize += 0.1"></blog-post>

如果我们要检查组件所抛出的所有事件以及对它们进行验证,可以在组件的emits选项中列出已抛出的事件:

app.component('blog-post', {
    props: ['title'],
    emits: ['enlargeText']
})

如果要让子组件触发事件时同时抛出一个值,可以使用 $emit 的第二个参数提供该值:

<button @click="$emit('enlargeText', 0.1)">
    Enlarge text
</button>

然后父组件监听这个事件的时候,可以通过 $event 访问到这个被抛出的值:

<blog-post ... @enlarge-text="postFontSize += $event"></blog-post>

5.v-model、在组件中使用v-model

(1)v-model 表单绑定

v-model 用于表单的双向绑定,自定义事件也可以用于创建支持v-model的自定义输入组件。 先看<input>的一个例子,请记住下面的例子:

<input v-model="searchText" />
//等价于下面的形式
<input 
    v-bind:value="searchText" 
    @input="searchText = $event.target.value" />

(2)组件中使用 v-model

但是如果在组件上使用v-model时,则会变成下面这样:

<custom-input v-model="searchText"></custom-input>
// 等价于下面的形式
<custom-input
    v-bind:model-value="searchText"
    @update:model-value="searchText = $event"
></custom-input>

对应的组件实现如下:

app.component('custom-input', {
    props: ['modelValue'],
    emits: ['update:modelValue'],
    template: `
        <input
            v-bind:value="modelValue"
            @input="$emit('update:modelValue', $event.target.value)"
        >
    `
})

如何理解上面的例子呢?从流程的最开始,父组件接收一个与model-value attribute 绑定的searchText的值,并通过 prop 传递到子组件,使子组件也可用该值,通过v-bind将该值绑定到子组件的value的attribute上,并当 [ 输入时 ] 在子组件上抛出事件名update:modelValue和相应的表单输入值$event.target.value,父组件监听这一update:modelValue自定义事件,事件触发后执行searchText = $event的操作。即 子组件需要一个 prop 并发出一个 自定义事件 来进行同步。

(3)组件中多个 v-model 的绑定

如上面的组件单个 v-model 的绑定,每个 v-model 将同步到对应的 prop ,且不需要在组件中添加额外的选项了:

<user-name
  v-model:first-name="firstName"
  v-model:last-name="lastName"
></user-name>
app.component('user-name', {
  props: {
    firstName: String,
    lastName: String
  },
  emits: ['update:firstName', 'update:lastName'],
  template: `
    <input 
      type="text"
      :value="firstName"
      @input="$emit('update:firstName', $event.target.value)">

    <input
      type="text"
      :value="lastName"
      @input="$emit('update:lastName', $event.target.value)">
  `
}

6.父子组件间的简单通信

父组件与子组件的概念容易混淆,简单来说,我们将某段代码封装成一个组件(一个.vue拓展名的文件),而这个组件又能被另一个组件(另一个.vue拓展名的文件)引入(通过import形式),那么引入该封装的组件的文件就叫父组件,被引入的文件称为子组件。下面看一个来自这里的例子:

test.vue,作为子组件:

<template>
    <div>
        {{ message }}
    </div>
    <p>{{ hello }}</p>
    <p>{{ hellotests }}</p>
</template>
<script>
    export default{
        // props 用于接收从父组件传过来的数据
        props:['msgTest', 'helloTest', 'CCCount']
    
        data() {
            return{
            // 子组件中声明的变量
            msg: '',
            hello: '',
            hellotest: ''
            }
        },
        
        created() {
        // ...
        },
        
        watch: {
            // 监听从父组件传来的数据是否发生了变化,然后赋值给变量
            CCCount() {
                this.msg = this.msgTest;
                this.hello = this.helloTest
            }
        },
        
        methods: {
            // 点击按钮,子组件操作父组件中的函数
            btns() {
                this.$emit('childsClick', '子组件操作父组件中的函数')
            },
            
            // 被父组件调用的子组件函数
            msgs1() {
                this.hellotests = '哈哈,我是父组件调用子组件的函数'
            }
        }
    }
</script>

作为父组件

<template>
    <div>test</div>
    <p>hello world</p>
    <span>{{ btnVal }}</span>
    <msgChild
        ref="msgRef"
        @childsClick="btnClick"
        :msgTest="msgTest2"
        :helloTest="helloTest2"
        :CCCount="CCCount"
    >
    <msgChild>
</template>
<script>
    // 引入子组件
    import msgChild form './test.vue'
    export default{
        data() {
            return{
                // 父组件中声明的变量
                msgTest2: '',
                helloTest2: '',
                CCCount: 0,
                btnVal: ''
            }
        },
        
        component: {
            // 声明子组件的名字,即局部注册
            msgChild
        },
        
        created() {
            this.msg()
        },
        
        methods: {
            msg() {
                // 组件变量赋值以传递给子组件
                this.msgTest2 = 'test01';
                this.helloTest = 'test02';
            },
            btnClick(val) {
                this.CCCount++;
                thos.btnVal = val;
                // 触发子组件中的函数
                this.$refs.msgRef.msgs1()
            }
        }
    }
</script>

7.插槽

8.Provide/Inject

9.边界情况

10.生命周期钩子