# Vue.js

记录学习 Vue.js 过程

# Vue

Vue 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue 也完全能够为复杂的单页应用提供驱动。

  • 数据驱动视图 (数据发生变化,视图自动更新)

# 基础语法

数据修改: v-on:click="add" 或者 @click="add"

属性绑定: v-bind:href="url" 或者 :title="title"

双向绑定(视图修改数据,数据渲染视图): v-model="search" eg: input 表单中的 value 会修改 data 中的 search,search 会渲染在页面上展示出来

条件渲染: v-if="isShow" method 中有一个 isShow 方法 v-show="isShow" 子组件不会卸载,仅仅显示隐藏

列表渲染: v-for="value in items".

自定义组件:

<div id="app">
 <fieldset>
    <legend>⾃定义组件</legend>
    <ul>
        <todo-item v-for="todo in items" :todo="todo" :key="todo.id"></todo-item>
    </ul>
 </fieldset>
 </div>
 <script>
 const App = {
    data() {
        return {
            items: [
                { id: 0, text: 'item0' },
                { id: 1, text: 'item1' },
                { id: 2, text: 'item2' },
            ]
        }
    }
 }
 const app = Vue.createApp(App)
 app.component('todo-item', {
    props: ['todo'],
    template: '<div>{{todo.text}}</div>'
 })
 app.mount('#app')
 </script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 组件实例与生命周期

Vue.createApp(App)会创建一个 app 应用实例, app.mount(#app) 会生成并挂载根组件实例。 (对于其他子组件同理,都会有自己对应的组件实例)

beforeCreate 在实例初始化以后,数据观测和 event/watcher 事件配置之前被调用

created 实例创建完成后被立即调用。 在这一步,实例已经完成以下的配置: 数据观测(data observer) property 和方法的运算, watch/event 事件回调。 然而挂载阶段还没开始, $elproperty 还不可用

beforeMount 在挂载开始之前被调用: 相关的 render 函数首次被调用

mounted 实例被挂载后调用。 这时Vue.createApp({}).mount()被新创建的vm.$el替换了。 如果根实例挂载到了一个文档内的元素上,当 mounted 被调用时vm.$el也在文档内

beforeUpdate 数据更新时调用,发生在虚拟 DOM 打补丁之前

updated 由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这以后会调用这个钩子

beforeUnmount 在卸载组件实例之前调用。这个阶段实例是完全正常的

unmounted 卸载组件实例后调用,调用这个钩子时,组件实例的所有指令都被解除绑定,所有事件侦听器都被移除,所有子组件实例被卸载

Vue-lifeCycle

  • 如果需要发送 ajax 请求,应该放在哪个生命周期? 在 created 以后的声明周期都可以,因为已经有数据存在

  • 父子组件嵌套时, 父组件视图和子组件视图渲染完成谁先谁后? 不确定,从生命周期来看无法判断谁先谁后,子组件渲染完成,父组件不渲染也看不到

  • 父子组件嵌套时,如果希望在所有组件视图都渲染完成后再执行操作,该怎么做?

mounted(){
    this.$nextTick(function(){
        // 仅在渲染整个视图之后运行的代码
        // 当修改数据后, 如果想尽早得到渲染后的DOM
    })
}
1
2
3
4
5
6

# 模板语法

  1. 文本 <span>Message: </span> same as <span v-text="msg"></span> <span v-once>这个将不会改变: </span> 仅仅渲染元素和组件一次。随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。可以用于优化性能

  2. 表达式 <span>NaN</span>

  3. 原始 HTML <span v-html="rawHtml"></span> 用于更新元素的 innerHTML。注意可能带来 XSS 攻击,仅仅使用在确认信任的内容上

  4. 属性 <a v-bind:href="url"> ... </a> same as <a :href="url"> ... </a>

  5. 事件 <a v-on:click="doSomething"> ... </a> same as <a @click="doSomething"> ... </a> <a @[event]="doSomething"> ... </a>

# Data Property 和 methods

  • data 选项是一个函数,返回一个对象(Vue2 的 data 可以是对象,但 vue3 只能是函数,否则报错)
  • 对尚未提供所需值的 property 使用 null、undefined 等占位
  • 实例创建后再添加的 property,响应式系统不会跟踪

# 计算属性 computed

  • 计算属性 count 会依赖 data 中的属性 books,books 发生改变自动出发 count 的变化
<div id="app">
 <p>{{name}} published {{count}} books:</p>
 <button @click="add">Add book</button>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
 Vue.createApp({
    data() {
        return {
            name: 'John Doe',
            books: ['book1']
            }
        },
        methods: {
            add() {
            this.books.push('book')
            }
        },
        computed: {
            count() {
            return this.books.length
        }
    }
 }).mount('#app')
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
  • 注意: 计算属性有缓存机制,如果依赖的数据未发⽣改变,则不会重新计算⽽是直接使⽤缓存值
  • 注意 methods 和 computed ⾥⾯的⽅法不要使⽤箭 头函数,否则 this 就不是 vm 对象了
  • methods 和 computed 差异:前者为方法,无论修改是否是依赖的值都会发生调用。 后者是属性,仅仅在依赖的属性发生变化的时候才会触发

# Watch

watch 会监控 data 中某个 property 的变化,执行函数

const vm = Vue.createApp({
    data() {
        return {
        name: 'jirengu'
        }
    },
    watch: {
        name(newname, oldname) {
        console.log(oldname + '-> + newname)
        }
    }
 }).mount('#app')
1
2
3
4
5
6
7
8
9
10
11
12
  • 什么时候需要用 watch?
    • 当只需要根据 data 中某个 property 的变化做出反应,不一定需要结果时。
    • 当有异步操作时
    • 当需要用到旧值时

# 响应式原理

  • 到底什么是数据响应式 追踪数据的变化,在读取数据或者设置数据的时候能劫持做一些操作。

    使用 object.defineProperty -> Vue2

    使用 Proxy -> Vue3

var obj = {}
var age
Object.defineProperty(obj, 'age', {
    get: function(){
        console.log('get age...')
        return age
    },
    set: function(val){
        console.log('set age...')
        age = val
    }
})
obj.age = 100  // 'set age...'
console.log(obj.age) // 'get age...', 100
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# Proxy 和 Reflect

proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找,赋值,枚举,函数调用等)

Reflect 是一个内置的对象,它提供拦截 Javascript 操作的方法,这些方法和 Proxy handlers 相同。

const dinner = {
  meal: 'tacos'
}

const handler = {
  get(target, prop) {
    console.log('get...', prop)
    //return target[prop]
    return Reflect.get(...arguments)
  },
  set(target, key, value) {
    console.log('set...', key, value)
    //target[key] = value
    return Reflect.set(...arguments)
  }
}

const proxy = new Proxy(dinner, handler)
console.log(proxy.meal)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

但是这里仅能响应式一层的对象,可以封装实现:

function reactive(obj){
    const handler = {
        set(target, key, value, receiver){
            return Reflect.set(...arguments)
        },
        get(target,prop){
            const value = Reflect.get(...arguments);
            if(typeof value === 'object'){
                reactive(value)
            }
            else{
                return value;
            }
        }
    }
    return new Proxy(obj,handler)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  • Vue2 用 Object.defineProperty 实现响应式, Vue3 使用 Proxy 实现响应式。 对比有什么优缺点?
    • Proxy 能劫持整个对象, 而 Object.defineProperty 只能劫持对象的属性; 前者递归返回属性对应的值的代理即可实现响应式。 后者需要深度遍历每个属性。 后者对数组的操作不友好。

# 条件渲染

  • v-if 的变化会创建/删除元素, v-show变化只是元素的展示/隐藏(display:none)
  • 对于多个元素的控制可以用<template>包裹

# v-for 列表渲染

  • v-for可基于数组渲染列表
  • 也可基于对象渲染列表
  • 可以使用值的范围
  • 可以在组件上循环渲染
  • v-for默认使用‘就地更新’ 策略, 数据项的顺序被改变,Vue 就不会移动 DOM 元素来匹配数据项的顺序, 而是就地更新每个元素。
  • 为能跟踪每个节点的身份,重用和重新排序现有元素,提升性能,需要使用 key
<li v-for="item in array">{{item}}</li>
<li v-for="(item, index) in array">{{item}}</li>
<li v-for="value in myObject">{{value}}</li>
<li v-for="(value, key) in myObject">{{key}}:
{{value}}</li>
<span v-for="n in 10">{{ n }} </span>
<!--1,...,10-->
<my-component v-for="(item, index) in
items" :item="item" :key="item.id"></mycomponent>
1
2
3
4
5
6
7
8
9

# 事件处理

  • @click 的值既可以是 methods 里的函数名,执行函数时参数是点击事件
  • 也可以是函数的调用,执行函数时参数时调用时传递的参数,可以传递固定值,可传递 data 的属性,也可传递$event
<span v-on:click="sayHello">click</span>
<span @click="sayHello">click</span>
<span @click="sayHello('hunger')">click</span>
<span @click="sayHello($event),
sayHi('hunger')">click</span>
<span @click.once="sayHello(name)">click</span> // 仅仅执行一遍
<span @click.stop="sayHello">click</span> // 指的是阻止父组件(节点)的事件冒泡
1
2
3
4
5
6
7

# v-model 双向绑定(针对输入框,表单等)

<input v-model="message" /> {{ message }} //相应onInput事件
<textarea v-model.lazy="message"></textarea> {{ message }} // 相应onChange事件,当鼠标从输入移走时才会更新
<input type="checkbox" v-model="checked" /> {{checked}}
<!-- 复选框 -->
<input type="checkbox" value="a" v-model="list" />
<input type="checkbox" value="b" v-model="list" /> {{list}} //多选一定要有value
<!-- 单选框 -->
<input type="radio" value="a" v-model="theme" />
<input type="radio" value="b" v-model="theme" /> {{theme}}
<!-- select -->
<select v-model="selected">
 <option value="AA">A</option>
 <option value="BB">B</option>
 <option value="CC">C</option>
</select>
{{selected}}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 组件基础

  • 每个组件维护独立的数据
  • app.component('组件名',{})注册组件
  • 通过 prop 向子组件传递数据
  • 子组件触发事件来实现子传父
<div id="app">
 <font-size step="1" :val="fontSize" @plus="fontSize += $event"
 @minus="fontSize -= $event"></font-size>
 <font-size step="3" :val="fontSize" @plus="fontSize += $event"
 @minus="fontSize -= $event"></font-size>
 <p :style="{fontSize:fontSize+'px'}">Hello {{fontSize}}</p>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
 const app = Vue.createApp({
 data() { return { fontSize: 16 } }
 })
 app.component('font-size', {
 props: ['val', 'step'],
 template: `
 <div>step: {{step}}
 <button @click="onPlus">+</button>
 <button @click="$emit('minus', step)">-</button>
 </div>`,
 methods: {
 onPlus() { this.$emit('plus', parseInt(this.step)) }
 }
 })
 app.mount('#app')
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# v-model 实现双向绑定

  • 父组件通过 v-model="属性" 把属性传递给子组件
  • 子组件内有一个 modelValue 的 prop,接受父组件传递的数据
  • 子组件通过触发 update:modelValue 修改父组件绑定的属性
<input v-model="searchText" />
<!--等价于-->
<input :value="searchText" @input="searchText = $event.target.value" />
1
2
3
<div id="app">
 <font-size step="1" v-model="fontSize"></font-size>
 <font-size step="4" v-model="fontSize"></font-size>
 <p :style="{fontSize:fontSize+'px'}">Hello {{fontSize}}</p>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
 const app = Vue.createApp({
 data() { return { fontSize: 16 } }
 })
 app.component('font-size', {
 props: ['modelValue', 'step'],
 template: `
 <div>fontSize: {{modelValue}}
 <button @click="$emit('update:modelValue',+step+modelValue)">+</button>
 <button @click="$emit('update:modelValue', modelValue-step)">-</button>
 </div>`
 })
 app.mount('#app')
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 单向数据流

  • 什么是单向数据流? 父组件能直接传递数据给子组件,子组件不能随意修改父组件状态

  • 为什么单向? 目的是让数据传递变得简单,可控,可追溯。

  • 如何实现双向? 父组件可以通过设置子组件的 props 直接传递数据给子组件。 子组件想传递数据给父组件时,可以在内部 emit 一个自定义事件,父组件可以在子组件上绑定该事件的监听,来处理子组件 emit 的事件和数据。

在 Vue 中, v-model 实现的所谓双向绑定,本质就是这样

# 全局组件和局部组件

app.component('component-a',{...}) // 全局组件

const component = {components: {...}} // 局部组件
1
2
3

# 组件命名

  • kebab-case 短横线连接全小写的单词
  • 声明时使用 kebab-case,模板里必须用 kebab-case
app.component('component-a',({})
<component-a></component-a>
1
2
  • PascalCase 多个首字母大写单词直接连接
  • 声明时使用 PascalCase,模板里可以用 kebab-case 和 PascalCase
app.component('ComponentB',({})
<component-b></component-b>
<ComponentB></Component-B>
1
2
3

# Props

# 写法:

props: ['name', 'other']
props: {
name: String,
other: Number //Boolean, Array, Object, Function
}
props: {
name: {
//传递的prop如果不满足条件,控制台会有警告
type: String,
required: true,
//default: 'hello'
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14

# v-bind

post: {
id: 1,
title: 'My Journey with Vue'
}
以下两种写法等价
<blog-post v-bind="post"></blog-post>
<blog-post v-bind:id="post.id"
v-bind:title="post.title"></blog-post>
1
2
3
4
5
6
7
8

# 非 prop 的 attribute

  • 指的是父组件模板里在使用子组件时设置了属性,但子组件内没有通过 Props 接收
  • 当组件返回单个根节点时,非 prop attribute 将自动添加到根节点的 attribute 中
  • 在子组件里可以通过$attrs / this.$attrs / context.attrs 获取 attributes
  • 如果想在非根节点应用传递的 attribute,使用v-bind="$attrs"
  • 默认所有属性都绑定到根元素
  • 使用inheritAttrs:false来取消默认绑定
<div id="app">
<user class="username"
:data-user="username"></user>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
const User = {
template: `<div>{{$attrs['data-user']}}</div>`
}
Vue.createApp({
components: { User },
data() {
return { username: 'hunger' }
}
}).mount('#app')
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
div id="app">
<username class="username" :error="errorMsg"
@input="onInput"></username>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
const Username = {
props: ['error'],
template: `
<fieldset>
<legend>用户名</legend>
<input v-bind="$attrs">
<div>{{error}}</div>
</fieldset>
`
}
Vue.createApp({
components: { Username },
data() {
return { errorMsg: '' }
},
methods: {
onInput(e){
this.errorMsg = e.target.value.length<6?"长度不够":""
}
}
}).mount('#app')
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# 自定义事件

  • 子组件内触发事件用 this.$emit('my-event')
  • 父组件使用子组件时绑定<component-a @my-event="doSomething"></component-a>
  • 推荐 kebab-case 事件名,区分大小写
<div id="app">
      <h1>{{username}}</h1>
      <user
        class="username"
        :data-user="username"
        @user-change="username=$event"
      ></user>
    </div>
    <script src="https://unpkg.com/vue@next"></script>
    <script>
      const User = {
        props: ["dataUser"],
        events: ["user-change"],
        template: `<div>{{dataUser}} <button @click='updateUser'>update</button></div>`,
        methods: {
          updateUser() {
            this.$emit("user-change", this.dataUser + "!");
          },
        },
      };
      Vue.createApp({
        components: { User },
        data() {
          return {
            username: "hunger",
          };
        },
      }).mount("#app");
    </script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# v-model 自定义事件语法糖

<com v-model:foo="bar"></com> //等价于
<com :foo="bar" @update:foo="bar=$event">
1
2
<div id="app">
<h1>{{username}}</h1>
<user class="username"
v-model:user="username"></user>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
const User = {
props: ['user'],
template: `<div>{{user}} <button @click="update
User">update</button></div>`,
methods: {
updateUser() {
this.$emit('update:user', this.user + '!')
}
}
}
Vue.createApp({
components: { User },
data() {
return { username: 'hunger' }
}
}).mount('#app')
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 插槽

  • 子组件的模板预留一个空位(slot)
  • 父组件使用子组件时可以在子组件标签内插入内容/组件,即向子组件内预留的空位插入
  • 父组件往插槽插入的内容只能使用父组件实例的属性
<div id="app">
<x-button> <icon name="yes"></icon> {{text}} </xbutton>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
const Icon = {
props: ['name'],
template: `<span>{{type}}</span>`,
computed: {
type() { return this.name==='yes'?'✔':'✘' }
}
}
const XButton = {
template: `<button>
<slot></slot>
</button>`
}
Vue.createApp({
components: { XButton, Icon },
data() {
return { text: '正确' }
}
}).mount('#app')
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 具名插槽

<div id="app">
<layout>
<template v-slot:header>
<h1>页面header</h1>
</template>
<template #default>
<p>页面content</p>
</template>
<template #footer>
<div>页面footer</div>
</template>
</layout>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
const Layout = {
template: `<div class="container">
<header> <slot name="header"></slot> </header>
<main> <slot></slot></main>
<footer><slot name="footer"></slot></footer>
</div>`
}
Vue.createApp({
components: { Layout },
}).mount(
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# 作用域插槽

div id="app">
<news>hello world</news>
<news v-slot="props">👉 {{props.item}}</news>
<news v-slot="{ item }">✔ {{item}}</news>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
const News = {
data() { return { news: ['first news', 'second news'] } },
template: `<ul>
<li v-for="item in news">
<slot :item="item"></slot>
</li>
</ul>`
}
Vue.createApp({
components: { News },
}).mount('#app')
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 动态组件与 keep-alive

  • 页面第一次进入,钩子的触发顺序 created-> mounted-> activated,
  • 退出时触发 deactivated
  • 当再次进入时,只触发 activated
<div id="app">
<button vfor="tab in tabs" :key="tab" :class="{ active: currentTab === tab }"
@click="currentTab = tab">
{{ tab }}
</button>
<keep-alive>
<component :is="currentTabComponent" class="tab"></component>
</keep-alive>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
const app = Vue.createApp({
data() {
return {
currentTab: 'Tab1',
tabs: ['Tab1', 'Tab2']
}
},
computed: {
currentTabComponent() { return this.currentTab.toLowerCase() }
}
})
app.component('tab1', {
template: `<div>Tab1 content</div>`
})
app.component('tab2', {
template: `<div>
<input v-model="value" /> {{value}}
</div>`,
data() { return { value: 'hello' } },
created() { console.log('tab2 created') },
activated() { console.log('tab2 activated') }
})
app.mount('#app')
</script>
<style>
.active { background: #e0e0e0; }
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

# Provide Inject

  • 适用于深度嵌套的组件,父组件可以为所有子组件直接提供数据
div id="app">
<toolbar></toolbar>
<button @click="isDark=!isDark">切换</button>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
const ThemeButton = {
inject: ['theme'],
template: `
<div :class="['button', theme]" ><slot></slot><
/div>
`
}
const Toolbar = {
components: { ThemeButton },
inject: ['theme'],
template: `<div :class="['toolbar', theme]">
<theme-button>确定</theme-button>
</div>`
}
Vue.createApp({
data() { return { isDark: false } },
provide: { theme: 'dark'},
// provide() {
// return { theme: this.isDark?'dark':'white'
}
// },
components: { Toolbar },
}).mount('#app')
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# Provide Inject 响应式(使用 computed 箭头函数)

<div id="app">
<toolbar></toolbar>
<button @click="isDark=!isDark">切换</button>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
const ThemeButton = {
inject: ['theme'],
template: `
<div :class="['button', theme.value]" ><slot></slot></
div>
`
}
const Toolbar = {
components: { ThemeButton },
inject: ['theme'],
template: `<div :class="['toolbar', theme.value]">
<theme-button>确定</theme-button>
</div>`
}
Vue.createApp({
data() {
return { isDark: false }
},
provide() {
return { theme: Vue.computed(()=>this.isDark?'dark':
'white') }
},
components: { Toolbar },
}).mount('#app')
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

# Vue 动画

  • 在 css 里配置好样式,通过切换 class 实现效果切换
  • 修改 style 和 data 中数据绑定
  • transition 组件
    • v-enter-from:在元素被插入之前生效,在元素被插入之后的下一帧移除
    • v-enter-active:定义进入过渡生效时的状态
    • v-enter-to:定义进入过渡的结束状态。在元素被插入之后下一帧生效 ,在过渡/动画完成之后移除
    • v-leave-from:在离开过渡被触发时立刻生效,下一帧被移除
    • v-leave-active:定义离开过渡生效时的状态
    • v-leave-to:离开过渡的结束状态。在离开过渡被触发之后下一帧生效 ,在过渡/动画完成之后移除
  • 多元素过渡: 指的是多元素进行切换,同一时间只显示一个
  • 使用不同的 key 提升性能
  • mode 属性解决两个元素同时存在的现象
    • out-in 当前元素先出,下一个元素再进
    • in-out 下一个元素先进,当前元素在出
  • 多组件切换: 使用动态组件实现Tab切换效果 -> 如果动态组件使用了keep-alive,需要放在transition内部
  • 使用transition-group实现列表过渡
Last Updated: 12/12/2021, 3:06:49 PM