vue知识点梳理

vue 原理

vue是一MVVM渐进式框架,在vue框架中数据会自动驱动视图。

MVVM设计模式

MVVM

  • View:视图,即DOM,UI组件,负责将数据转换成UI展示
  • Model:模型,是vue组件里的data,或者vuex里数据
  • ViewModel:视图模型,负责监听数据变化,控制视图行为,处理用户交互。

View和Model之间无直接联系,而是通过ViewModel进行交互,ViewModel通过双向数据绑定把View和Model连接起来,ViewModel通常要实现一个observer观察者,当数据发生变化,ViewModel能够监听到数据的这种变化,然后通知到对应的视图做自动更新,而当用户操作视图,ViewModel也能监听到视图的变化,然后通知数据做改动,因此,开发者只需关注业务逻辑,不需手动操作DOM,也不需关注数据状态的同步问题,vue是响应式的

响应式

Vue的响应式原理:通过Object.defineProperty中访问器属性中getset方法。data中声明的属性都被添加了访问器属性,当读取data中数据时调用get方法,当修改data中数据时,自动调用set方法,在检测到数据变化,会通知观察者(Watcher),观察者(Watcher)会触发 重新渲染当前组件,生成新的虚拟DOM树,Vue遍历并对比新旧虚拟DOM树不同,并记录下来,最后,重新渲染视图,将局部修改到真实DOM树上。

每个组件实例都对应一个watcher实例,它会在组件渲染的过程中把“接触”过的数据property记录为依赖(getter函数通知watcher对象将其声明为依赖),之后当依赖项的setter触发时,会通知watcher,从而使它关联的组件重新渲染。

Vue 在 3.x 版本之后改用 Proxy 进行实现

vuegraph

变化检测问题

  • 对于对象,vue无法检测到对象属性的添加或移除。由于vue会在初始化实例时对property执行 getter/setter转化,所有 property 必须在 data对象上存在 才能让vue将它转换为响应式的。

    • 使用 Vue.set(object, propertyName, value)方法向对象添加响应式的property

    • 使用Object.assign()_.extend()这样混合进到对象的新property不会触发更新。应该使用原对象与要混合进去的对象的property一起创建一个新对象,并替换原对象

      1
      2
      this.someObj = Object.assign({}, this.someObj, {a: 1, b: 22})
      // 替代, Object.assign(this.someObj, {a: 1, b: 22})
  • 对于数组,以下2种数组变动,vue无法检测

    • 利用索引直接设置数组项时,例如:vm.items[index] = newValue。解决方法:

      1
      2
      3
      4
      // Vue.set()
      Vue.set(vm.items, index, newValue)
      // 数组 中 变异方法 Array.prototype.splice()
      vm.items.splice(index, 1, newValue)
    • 直接修改数组的length时,例如:vm.items.length = newLength。解决方法:

      1
      2
      // 删除 新 长度 以后的数组项
      vm.items.splice(newLength)

声明响应式property

由于Vue不允许动态添加 根级响应式property,所以必须在初始化实例前 声明所有根级响应式property,哪怕是一个空值。好处:

  1. 可维护性上,因data对象就像是组件状态的结构,所以提前声明好,可以让组件代码在未来修改或阅读时更易于理解

异步更新 队列

Vue 在更新DOM时是异步执行的。只要侦听到数据变化,Vue将开启一个队列,并缓冲在 同一事件循环中发生 的所有数据变更。如果一个watcher被多次触发,只会被推入到队列中一次。在缓冲时去掉 重复数据,避免了不必要的计算和DOM操作。

简而言之,就是在一个事件循环期间 发生的 所有数据改变 都会在 下一个事件循环的Tick中来触发 视图更新。关于事件循环 可参考博客: EventLoop梳理

Vue在内部对异步队列尝试使用原生的Promise.then,MutationObserversetImmediate.如果环境不支持,则会采用setTimeout(fn, 0)代替

响应数据 发生变更后,该组件不会立即重新渲染,而会在下一次事件循环”tick”中更新。如果想基于更新后的DOM状态做点什么,需要在数据变化后使用Vue.nextTick(callback),回调函数将在DOM更新完成后被调用,回调函数中的this将自动绑定到调用它的 Vue 实例上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div id="example">{{message}}</div>



<script>
var vm = new Vue({
el: '#example',
data: {
message: '123'
}
})
vm.message = 'new message' // 更改数据
vm.$el.textContent === 'new message' // false
Vue.nextTick(function () {
vm.$el.textContent === 'new message' // true
})
</script>

因为 $nextTick()返回一个Promise对象,所以可以使用 async/await语法

1
2
3
4
5
6
7
8
methods: {
updateMessage: async function () {
this.message = '已更新'
console.log(this.$el.textContent) // => '未更新'
await this.$nextTick()
console.log(this.$el.textContent) // => '已更新'
}
}

Vue 生命周期

lifecycle2

Vue在整个生命周期中会有8个钩子函数供我们在不同时刻进行操作。注意,最后不要在生命周期钩子函数内使用箭头函数,因为箭头函数并没有this,this会作为变量一直向上级词法作用域查找,直至找到为止。在普通函数内部,this就是该Vue 实例

Creation (Initialization)

创建钩子 在Vue实例开始初始化过程中运行,它允许我们在 将Vue实例添加到 DOM之前执行操作。同其他钩子不同,创建钩子也是在服务器呈现期间运行的,在此期间,不能访问DOM或this.$el

  • beforeCreate: data未被激活,events还没被设置
  • created:vue实例已经初始化完成,data,computed,watch,methods,events,已激活。常用于,通过网络请求,为组件获取一些必要数据

Mounting (DOM Insertion )

将vue实例挂载到 指定元素时运行(即组件第一次渲染呈现之前、之后运行)。常用于在组件初始呈现之前或之后立即访问或修改组件中的DOM

  • beforeMount: vue实例挂载到DOM之前以及模板或render函数已经编译好之后,此时,不能操作DOM,this.$el仍不可用,页面还是旧的。
  • mounted:vue实例已被挂载,可以访问组件、模板和DOM,常用于操作DOM和集成非Vue库

Updating (Diff & Re-render)

每当组件使用的响应数据发生更改,或其他原因导致其重新渲染时,就会调用更新钩子函数。

可使用: 想要知道组件何时重新渲染(re-redner),可能用于调试或分析

不可使用:想知道组件内的data数据时何时更改的,建议用computedwatch 替代

  • beforeUpdate: 组件中data数据更改之后,修补和重新渲染DOM(patched and re-redner)之前运行。此时 data数据是最新的,但页面中尚未和最新数据同步
  • updated: vue实例中数据更改和DOM也重新渲染呈现 之后运行。如果需要在属性更改后访问DOM,这里是最安全的

Destruction (Teardown)

销毁钩子允许在组件被消耗是执行操作,例如清理或分析发送。当组件被拆下并从DOM中移除是时,它们会触发。

  • beforeDestroy: 在销毁实例之前触发,此阶段,该实例和其所有功能仍然可用,常用于执行资源管理,删除变量,清理事件、取消订阅等
  • destroyed: 实例别销毁,此阶段,所有的子vue实例也已经被销毁,事件监听器,所有指令都被解除绑定。调用vm.$destroy()会触发

render 函数

每一个vue组件都会实现一个render函数,大多数情况下,该函数由vue编译器创建。当在组件上指定一个模板(template),该模板的内容将被vue编译器处理,并返回一个redner函数。render函数实际上会返回一个虚拟DOM节点,并由vue在浏览器DOM中呈现。再结合vue响应系统,vue能够智能的计算处最少需要重新渲染多少组件,并把 DOM操作次数减到最少。

每当组件的响应性属性更像时,都会再次调用redner函数


虚拟DOM

Vue通过渲染函数返回的虚拟DOM节点(在vue中通常成为VNode,包含vue所需的所有信息)建立起一个虚拟DOM,并用其来追踪自己要如何改变真实DOM。当vue要更新真实DOM时,vue会将更新后的虚拟DOM与之前的DOM进行比较,并仅使用修改的部分来更新真实DOM,这意味较少的元素被修改,从而提高性能。

vuegraph

createElement

在Vue生态中通常使用h作为createElement的别名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Vue.component('anchored-heading', {
render: function (createElement) {
return createElement(
'h' + this.level, // 标签名称
this.$slots.default // 子节点数组
)
},
props: {
level: {
type: Number,
required: true
}
}
})

createElement,更准确的名字可能是createNodeDescription,因为它返回VNode,就是节点的描述信息。createElement接受3个参数

  • HTML标签名,组件的选项对象,或 resolve 了 二者中 任一种的 一个的 async函数 {String | Object | Function}

    1
    2
    3
    4
    5
    6
    7
    //  组件的选择对象  
    import App from "./App.vue"
    new Vue({
    router,
    store,
    render: h => h(App)
    }).$mount("#app");
  • 一个与模板中 attribute 对应 的 数据对象 {Object}

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
 {
// v-bind:class
class: {
foo: true,
bar: false
},
// v-bind:style
style: {
fontSize: "20px"
},
// 普通 HTML attribute
attrs: {
id: "app"
},
// 组件 prop
props: {
myProp: "bar"
},
// DOM property
domProps: {
innerHTML: "bar"
},
// 事件监听器,vue内事件
on: {
click: function() {}
},
// 监听js原生事件,`vm.$emit()`不会触发
nativeOn: {
click: function() {}
},
// 自定义 组件
// `v-myDirective:arg.bar="1+1"`
directives: [
{
name: "myDirective", // 指令名
value: "2", // 指令绑定的值
expression: " 1+ 1", // 指令表达式
arg: "foo", // 传给指令的参数
modifiers: { // 修改符
bar: true
}
}
],
// 作用域插槽
// {name: props => Vnode | Array<VNode>}
scopedSlots: {
default: props => createElement("span", props.text)
},
slot: "name-of-slot",
key: "myKey",
ref: "myRef"
}
  • 子级虚拟节点 列表(VNodes),也是由createElement()构建而成,使用字符串直接生成”文本虚拟节点” {Array | String }

模板功能的替代

v-if 和 v-for

1
2
3
4
<ul v-if="items.length">
<li v-for="item in items">{{ item.name }}</li>
</ul>
<p v-else>No items found.</p>
1
2
3
4
5
6
7
8
9
10
11
props: ['items'],
render: h => {
const self = this;
if(self.items.length) {
return h('ul', self.items.map(item => {
return h('li', item.name)
}))
}else {
return h('p', 'No items found.')
}
}

v-model

1
<input v-model="bar" />
1
2
3
4
5
6
7
8
9
10
11
12
13
14
props: ['value'],
render: h => {
const self = this;
return h('input', {
domProps: {
value: self.value
},
on: {
input: event => {
self.$emit('input', event.target.value)
}
}
})
}

事件&按键 修饰符

.passivecaptureonce这些事件修饰符,vue提供了对应的前缀可用于on

修饰符 前缀
.passive (滚动事件默认行为(滚动行为),立即触发,而非等待‘onScroll 完成) &
.capture !
.once ~
.capture.once.once.capture ~!
1
2
3
4
5
on: {
'!click': this.doThisInCapturingMode,
'~keyup': this.doThisOnce,
'~!mouseover': this.doThisOnceInCapturingMode
}

其他修饰符,不需前缀,可在事件处理函数中找到等价的操作

修饰符 处理函数中等价操作
.stop event.stopPropagation()
.prevent event.preventDefault()
.self if (event.target !== event.curentTarget) return
按键:.enter,.12 if (event.keyCode !== 12) return
修饰键:.ctrl.alt.shiftmeta if (!event.ctrlKey) return // altKey shiftKey metaKey

插槽

  • 通过this.$slots访问静态 插槽,{[name: string]: ?Array<VNode>},每个插槽都是一个VNode数组。每个具名插槽有其相对应的property,例如, v-slot:foo中的内容,在this.$slots.foo中, this.$slots.defalut内容为 所有没有名字插槽中的节点

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <blog-post>
    <template v-slot:header>
    <h1>Header</h1>
    </template>
    <p>content 1</p>
    <template v-slot:footer>
    <h2>Footer</h2>
    </template>
    <p>content 2</p>
    </blog-post>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    Vue.component('blog-post', {
    redner: h => {
    let header = this.$slots.header
    let footer = this.$slots.footer
    let body = this.$slots.default
    return h('div', [
    h('header', header),
    h('main', body),
    h('footer', footer)
    ])
    }
    })
  • 通过this.$scopedSlots访问作用域插槽,{[name:string]: props => Array<VNode> | undefined},每个作用域插槽都是一个返回若干 VNode 的函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    props: ['message'],
    render: h => {
    cosnt slef = this
    // `<div><slot :text="message"></slot></div>`
    return h('div', [
    slef.$scopedSlots.defalut({
    text: this.message
    })
    ])
    }

从2.6.0开始,所有$slots都会作为函数暴露$scopedSlots中 。所有在使用render函数,不论当前插槽是否带有作用域,都始终通过$scopedSlots访问它们

Slot 插槽

在2.6.0后,具名插槽和作用域插槽使用统一语法: v-slot指令,它取代了slotslot-scope的attribute

父级模板里的所有内容都在父级作用域中编译,同理,子模版亦是如此。

具名插槽

当需要向组件添加多个插槽时,则插槽需要有名称,可使用<slot>元素的一个特殊的attribute: name来定义。一个不带name<slot>name默认为default

1
2
3
4
5
6
7
8
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
</div>

在向具名插槽提供内容时,可在一个<template>元素上使用v-slot指令,用以v-slot参数的形式 指定插槽名称

1
2
3
4
5
6
7
8
<base-layout>
<template v-slot:header>
<h1> page title</h1>
</template>
<template v-slot:defalut>
<p>the main content</p>
</template>
</base-layout>

作用域插槽

作用域插槽,可让插槽内容访问子组件中的数据

1
2
3
4
5
<span>
<slot v-bind:user="user">
{{user.lastName}}
</slot>
</span>

想替换备用的内容,用 firstName 而不是 lastName 。**因只有在<current-user>组件内部才可以访问到user,为了让user在父级的插槽内容中可用 ,需要将user作为<slot>元素的一个 attrubute 绑定 **

绑定在<slot>元素上的 attrubute 称为 插槽prop,这样就可以暴露给 插槽内容 。 现在 在父级作用域中,可以使用带值的v-slot来定义插槽prop 对象 的名字,从而接收 插槽prop内容。

1
2
3
4
5
<current-user>
<template v-slot:default="slotProps">
{{ slotProps.user.firstName }}
</template>
</current-user>

在上例中,将包含所有插槽prop的对象命名为slotProps

风格指南

必要的 (可规避错误)

  • 组件名应该始终时多个单词,根组件App及Vue内置组件除外。避免跟现有、或未来的HTML元素相冲突,因为所有的HTML元素名称都是单个单词

  • 组件的data必须是一个函数。如果是对象,它会在组件的所有实例之间共享。为了避免如此,每个实例必须生成一个独立的数据对象,在函数返回对象即可。

  • prop定义应该尽量详细。细致的prop定义可很容易看懂组件的用法,在开发环境中,不符合的prop,vue会告警。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    Vue.component('my-component', {
    props: {
    propA: {
    type: Number,
    defalut: 100,
    required: true,
    validator: function(value) {
    return value < 1000
    }
    }
    }
    })
  • 必须用key配合v-for, 且不要用idnex作为key. key的类型number | string

    key主要用在Vue的虚拟DOM算法,在新、旧 虚拟DOM对比时 辨别 Vnodes,从而高效的更新虚拟DOM

    • 不使用key,Vue会使用一种最大限度减少动态元素,并尽可能的尝试就地修改/复用相同类型元素的算法。
    • 使用key, Vue会基于key的变化重新排列元素顺序,并且移除 key 不存在的元素
  • 避免在同一个元素上使用v-ifv-for

    因为v-forv-if具有更高的优先级,所有在每次重新渲染的时候回遍历整个列表。另外 在 渲染层的 添加了逻辑,可维护性不强。

    • 为了 过滤一个列表中的项目,应该使用计算属性,让其返回过滤后的列表

      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
      // bad
      <ul>
      <li
      v-for="user in users"
      v-if="user.isActive"
      :key="user.id"
      >
      {{ user.name }}
      </li>
      </ul>

      // good
      <ul>
      <li v-for="user in activeUsers" :key="user.id">
      {{user.name}}
      </li>
      </ul>

      <script>
      computed: {
      activeUsers: function () {
      return this.users.filter(user => user.isActive)
      }
      }
      </script>
    • 为了避免渲染本应该被隐藏的列表,将v-if移至容器元素,这样只需检查一次,而不会对列表中的每项都检查

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      // bad
      <ul>
      <li
      v-for="user in users"
      v-if="shouldShowUsers"
      :key="user.id"
      >
      {{ user.name }}
      </li>
      </ul>

      // good
      <ul v-if="shouldShowUsers">
      <li
      v-for="user in users"
      :key="user.id"
      >
      {{ user.name }}
      </li>
      </ul>
  • 为 组件 css d样式 设置 作用域 scoped,避免冲突。 除 顶级App组件,布局组件中css样式可以时全局的,其它的组件 都应该是 有作用域的。 覆写子组件的样式,可使用 >>>操作符, 对于lesssass等预编译css,可使用/deep/

  • 私有property名,在使用模块作用域时要保持不允许外部访问的函数的 私有性。可在不考虑作为对外公共API的自定义私有property 使用$_前缀,并附带一个命名空间以避免同其他作者冲突。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // bad
    var myMixin = {
    methods: {
    update: function() {}
    }
    }

    // good
    var myMixin = {
    methods: {
    $_myMixin_update: function() {}
    }
    }

强烈推荐 (可增强可读性)

  • 将每个组件都单独分出文件,这样在需要编辑或查找一个组件时,更快速找到。

  • 单文件组件的文件名应该 始终 是 单词大小开头( 对自动补全友好 ),或 短横线连接(对大小写不敏感的文件系统 )

  • 应用特定样和约定的基础组件(即,展示类的,无逻辑,无状态的组件)名称应该全部以一个特定的前缀开头,比如Base,App

    基础组件的名称*通常包含所包裹元素的名称**,比如 BaseButton,BaseTable

    1
    2
    3
    4
    5
    // good
    components/
    |- BaseButton.vue
    |- BaseTable.vue
    |- BaseIcon.vue
  • 只应该有单个活跃的实例的组件(单例组件)应该以The前缀命名,以表示其唯一性。

    单例组件不意味着该组件只用于一个单页应用中,而是每个页面只使用一次。这些组件永远不接受prop

  • 和父组件紧密耦合的子组件应该以父组件名作为前缀命名

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // bad
    components/
    |- TodoList.vue
    |- TodoItem.vue
    |- TodoButton.vue


    // good
    components/
    |- TodoList.vue
    |- TodoListItem.vue
    |- TodoLListItemButton.vue
  • 组件名中的单词顺序,以高级别的(通常是一般化的描述)单词开头,以描述性的修饰符结尾

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // bad
    components/
    |- ClearSearchButton.vue
    |- RunSearchButton.vue

    //good
    components/
    |- SearchButtonClear.vue
    |- SerchButtonRun.vue
  • 自闭合组件,在单文件组件、字符串模板、JSX中没有内容的组件应该是自闭合的; 但在DOM模板中永远不要,因为HTML不支持自闭合的自定义元素,只用官方的”空”元素(<br/> <hr/> <input/>)可以。

    自闭合组件表示它们不仅没有内容,而是刻意没有内容

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // bad
    <!-- 在 单文件组件、字符串模板、JSX中 -->
    <MyComponent></MyComponent>
    <!-- 在 DOM 模板-->
    <my-component>

    // good
    <!-- 在 单文件组件、字符串模板、JSX中 -->
    <MyComponent/>
    <!-- 在 DOM 模板-->
    <my-component></my-component>
  • 模板中的组件名大小写,对大多数项目,在单文件组件和字符串模板中组件名总是PascalCase,在DOM模板中总是kebab-case的。 另外,在所有的地方都使用kebab-case 亦可

  • 在JS/JSX中的组件名应该始终是PascalCase的。对于只通过Vue.component定义的全局组件,推荐使用kebab-case

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // good
    Vue.component('my-component', {
    // ...
    })

    import MyComponent from './MyComponent,vue'

    export default {
    name: 'MyComponent'
    }
  • prop名大小写,在声明是,使用camelCase,在模板或JSX中使用kebab-case

    1
    2
    3
    4
    5
    6
    // good
    props: {
    greetingText: String
    }

    <WelcomeMessage greeting-text="hi">
  • 组件模板应该只包含简单的表达式,复杂的表达式应该重构为计算属性 或 方法

  • 应该把复杂的计算属性分割为尽可能多的简单计算属性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // bad
    computed: {
    price: function () {
    var basePrice = this.manufactureCost / (1 - this.profitMargin)
    return (
    basePrice -
    basePrice * (this.discountPercent || 0)
    )
    }
    }

    // good
    computed: {
    basePrice: function() {
    return this.manufactureCost / (1 - this.profitMargin)
    },
    discount: function() {
    return this.basePrice * (this.discountPercent || 0)
    },
    finalPrice: function() {
    return this.basePrice - this.discount
    }
    }

Vue 组件

组件注册

组建名

组建名就是Vue.component的第一个参数,当直接在DOM中使用一个组件时,推荐使用W3C规范中的自定义组件名,使用kebab-case,在引用该组件时也必须使用kebab-case

1
Vue.component('my-component', {})

全局注册

通过Vue.component来创建的组件是全局注册的,该组件可用在任何新创建的Vue根(new Vue)实例的模板中。全局组成的行为必须在根Vue实例创建之前发生

被频繁使用的基础 组件可配置webpack,实现自动化的全局注册。使用require.context来全局注册这些通用的基础组件。以下是在应用入口文件(src/main.js)中全局 注册/导入基础组件

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
import Vue from 'vue';
import { upperFirst, camelCase } from "lodash";

const requireComponent = require.context(
'./components', // 组件目录的相对路径
false, // 是否查询其子目录
/Base[A-Z]\w+\.(vue|js)$/ // 匹配 基础组件文件名 `Base${...}.vue`或`Base${...}.js`
);
requireComponent.keys().forEach(fileName => {
console.log("requireComponent fileName >>>", fileName);
// 组件配置
const componentConfig = requireComponent(fileName);

// 组件名 PascalCae
const componentName = upperFirst(
camelCase(
fileName
.split("/")
.pop()
.replace(/\.\w+$/, "")
)
);
// 全局注册,
Vue.component(
componentName,
// 如果该组件选项是通过`export default`导出,则优先使用`.default1default`
// 否则 回退 使用模块的根
componentConfig.default || componentConfig
);
});

局部注册

components选项中定义想要使用的组件

1
2
3
4
5
6
7
8
9
10
var ComponentA = {}
var ComponentB = {}
new Vue({
el: '#app',
components: {
'component-a': ComponentA,
'component-b': ComponentB
}
})

Prop

Prop大小写

因HTML中的attribute名是大小写不敏感的,所以浏览器会把所有的大写字符解析为小写字符。所有在使用DOM模板时,camelCase的prop名 需要 使用其等价的 kebab-case

Prop类型检查和验证

1
2
3
4
5
6
7
8
9
10
11
12
props: {
propA: {
type: Object, // 类型
default: function() { // 默认值,对象和数组默认值必须从一个工厂函数返回
return {message: 'hello, jiang!'}
},
// 自定义验证函数
validator: function(value) {
return value.length < 15;
}
}
}

类型检测,type可以是以下原生构造函数,也可以是自定义的构造函数,并且通过instanceof来进行检查确认

注意:Prop检查和验证 是 在一个组件实例创建之前,所有实例的property 在 defaultvalidator函数中是不可用的

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

传递静态或动态Prop

在传递非字符串类型时,即便是静态的,仍然要使用v-bind来告诉Vue,其是JavaScript表达式,而不是一个字符串

1
2
3
4
5
6
7
8
9
<!-- 即便 `42` 是静态的,我们仍然需要 `v-bind` 来告诉 Vue -->
<!-- 这是一个 JavaScript 表达式而不是一个字符串。-->
<blog-post v-bind:likes="42"></blog-post>

<!-- 传递的prop 是 字符串 '42' -->
<blog-post likes="42"></blog-post>

<!-- 用一个变量进行动态赋值。-->
<blog-post v-bind:likes="post.likes"></blog-post>

传入一个对象的所有property

使用不带参数的v-bind,可将一个对象的所有property都作为prop传入

1
2
3
4
5
6
7
8
9
10
11
12
post: {
id: 1,
title: 'Jiang with Vue'
}

<blog-post v-bind="post"></blog-post>

// 等价于
<blog-post
v-bind:id="post.id"
v-bind:title="post.title"
></blog-post>

单项数据流

prop数据都是从父级流向子级的,即父级prop的更新会向下流到子组件。例外,每次父级组件发生变更时,子组件中所有的prop也将更新为最新的值。这意味不应该在子组件内部改变prop。但有2种常见的视图变更prop的情况:

  1. prop用来传递一个初值,子组件内后续可能会变更。此情况,最好定义一个本地的data property 并将这个prop 用作其初始值
1
2
3
4
5
6
props: ['initialCounter'],
data: function() {
return {
counter: this.initialCounter
}
}
  1. prop作为原始的值传入并需要进行转换。此情况,最好使用这个prop来定义一个计算属性

    1
    2
    3
    4
    5
    6
    7
    props: ['name'],
    computed: {
    normalizedName: function() {
    return this.name.trim().toLowerCase()
    }
    }

非 Prop的 Attribute

非prop的 attribute 会被添加到组件的根元素上

对于绝大多数 attribute 来说,从外部提供给组件的值会替换到组件内部 根元素 设置好的值。classstyle 会被合并

如果不希望组件的根元素继承 attribute,可以在组件的选项中设置 iheritAttrs: false,从而禁用 attribute 继承

1
2
3
Vue.component('my-component', {
inheritAttrs: false
})

例外,实例的$attrs,包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 ,class,style 除外,在通过v-bind="$attrs"传入内部组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Vue.component('base-input', {
inheritAttrs: false,
props: ['label', 'value'],
template: `
<label>
<input
v-bind="$attrs"
v-bind:value="value"
v-on:input="$emit('input', $event.target.value)"
/>
</label>

`
})

使用该组件就像使用原始的input元素一样设置attribute,因为这些attribute都被设置在组件内的 input 元素上,而非根元素上

1
2
3
4
5
<base-input
v-model="username"
required
placeholder="Enter your username"
></base-input>

自定义事件

事件名

触发事件的事件名必须要完全匹配 监听该事件所用的名称,并且v-on事件监听器在DOM模板中会别自动转换为全小写,v-on:myEvent被转换成v-on:myevent,因此推荐始终使用kebab-case的事件名

自定义组件的v-model

组件的v-model默认利用名为value的prop,和名为input的事件,·**model选项可以定制prop 和 event**,这样就可以将valueprop用作其他地方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<input
type="checkbox"
v-bind:checked="checked"
v-on:change="$emit('change', $event.target.checked)"
/>
</template>

<script>
export default {
name: "BaseCheckobx",
model: {
prop: "checked",
event: "change"
},
props: {
checked: Boolean
},
data() {
return {};
}
};
</script>

将原生事件绑定到组件

v-on.native修饰符,监听原生事件。

$listeners是一个对象,包含了作用于该组件上 v-on监听器,不包括带.native修饰符的.可通过v-on="$isteners"传入内部组件的某个特定的子元素

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
Vue.component('base-input', {
inheritAttrs: false,
props: ['label', 'value'],
computed: {
inputListeners: function () {
var vm = this
// `Object.assign` 将所有的对象合并为一个新对象
return Object.assign({},
// 我们从父级添加所有的监听器
this.$listeners,
// 然后我们添加自定义监听器,
// 或覆写一些监听器的行为
{
// 这里确保组件配合 `v-model` 的工作
input: function (event) {
vm.$emit('input', event.target.value)
}
}
)
}
},
template: `
<label>
{{ label }}
<input
v-bind="$attrs"
v-bind:value="value"
v-on="inputListeners"
>
</label>
`
})

此时<base-input>组件就是一个完全透明的<input>包裹器,可以完全像一个普通的<input>元素一样 使用。

1
2
3
4
5
<!-- 监听 foucs,失败,$listeners 不包含带`.native`的`v-on`监视器  -->
<base-input v-on:focus.native="onFocus"></base-input>

<!-- 可正常监听 foucs -->
<base-input v-on:focus="onFocus"></base-input>

动态组件

<component>vue的内置组件,动态组件,依据is的值,来决定那个组件被渲染.is的值,可以是已注册组件的名字,或 一个组件的选项对象。

使用<keep-alive>元素将动态组件包裹起来,可使失活的组件被缓存

1
2
3
<keep-alive>
<component :is="currentTabComponent"></component>
</keep-alive>

异步组件

在大型 应用中,需要将应用分割成多个小模块,并且只在需要的时候才从服务器加载该模块。Vue允许 以一个工厂函数的方式定义组件,并异步解析。只在该组件 被渲染的时候才会触发该工厂函数,并会把结果缓存起来 供以后重新渲染。

也可以在工厂 函数返回一个Proomise,通过webpackimport语法,可写成 如下:

1
2
3
4
5
6
7
8
9
10
11
// 全局注册
Vue.component(
'async-component',
// 这个 `import` 函数会返回一个 `Promise` 对象。
() => import('./my-async-component')
)

// 局部注册
components: {
'async-component': () => import('./my-async-component')
}

处理异步组件的加载状态

1
2
3
4
5
6
7
8
9
10
11
12
13
const AsyncComponent = () => ({
// 需要加载的组件 (应该是一个 `Promise` 对象)
component: import('./MyComponent.vue'),
// 异步组件加载时使用的组件
loading: LoadingComponent,
// 加载失败时使用的组件
error: ErrorComponent,
// 展示加载时组件的延时时间。默认值是 200 (毫秒)
delay: 200,
// 如果提供了超时时间且组件加载也超时了,
// 则使用加载失败时使用的组件。默认值是:`Infinity`
timeout: 3000
})

过渡 、动画

Vue在插入、更新、移除DOM时,提供了4种方式的应用过渡效果。

  1. 在CSS 过渡和动画中自动应用class
  2. 配合使用第三方CSS动画库,如 Animate.css
  3. 过渡钩子函数中使用JavaScript直接操作DOM
  4. 配合使用第三方JavaScript动画库,如 Velocity.js

基本

过渡的类名

在进入/离开的过渡中,会有6个class切换

transition

  1. v-enter: 过渡开始状态
  2. v-enter-active:过渡生效状态,此类被用来定义进入过渡的过程时间、延迟、曲线函数
  3. v-enter-to:过渡结束状态
  4. v-leave: 离开过渡的开始状态
  5. v-leave-active:离开过渡的生效状态,此类被用来定义 离开过渡的 过程时间、延迟、曲线函数
  6. v-leaver-to: 离开过渡的结束状态

没有设置name attribute 的<transition>时,v-这些类名的默认前缀。设置了name attribute 的 <transition name="myTransitionName">,则name的值会替换V, 即myTransitionName-是这些类名的前缀

自定义过渡的类名

通过<transition>一些的 attrubute 可定义过渡的类名。自定义过渡的类名优先级 _高于_普通类名,因此可轻松的使用其他第三方CSS过渡/动画库。

  • enter-class
  • enter-active-class
  • enter-to-class
  • leave-class
  • leave-active-class
  • leave-to-class

结合Animate.css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    <div class="example-2">
<button v-on:click="show2 = !show2">Toggle</button>
<transition
name="custom-classes-transition"
enter-active-class="animate__animated animate__tada"
leave-active-class="animate__animated animate__bounceOutRight"
>
<p v-if="show2">Jiang Jiang</p>
</transition>
</div>
<script>
import "animate.css/animate.css"; // 引用外部 animate 样式
export default {
name: "TransitionDemo",
data() {
return {
show1: true,
show2: true
};
}
};
</script>

JavaScript钩子函数

JS钩子函数,其实就是transition组件上的事件:

  • before-enter,befroe-leave
  • enter,leave
  • afer-enter,after-leave

当只用javaScript过渡时,在enterleave中必须使用done进行回调,否则,它们被同步调用,过渡会立即完成。

对于仅使用JavaScript过渡的元素,transition 组件的cssprop设置为false(v-bind:css="false"),Vue会跳过CSS检查,将只通过事件 触发 注册的JavaScript钩子。

结合Velocity.js

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
    <div id="example-3" style="width:150px">
<button v-on:click="show3 = !show3">Toggle</button>
<transition
v-on:before-enter="beforeEnter"
v-on:enter="enter"
v-on:leave="leave"
v-bind:css="false"
>
<p v-if="show3">volecity.js</p>
</transition>
</div>
<script>
import "animate.css/animate.css";
import Velocity from "velocity-animate"; // 引用velocity.js
export default {
name: "TransitionDemo",
data() {
return {
show1: true,
show2: true,
show3: true
};
},
mounted() {
//
},
methods: {
beforeEnter(el) {
console.log("el>>", el);
el.style.opacity = 0;
el.style.transformOrigin = "left";
},
enter(el, done) {
Velocity(el, { opacity: 1, fontSize: "1.4em" }, { duration: 300 });
Velocity(el, { fontSize: "1em" }, { complete: done });
},
leave(el, done) {
Velocity(el, { translateX: "15px", rotateZ: "50deg" }, { duration: 600 });
Velocity(el, { rotateZ: "100deg" }, { loop: 2 });
Velocity(
el,
{
rotateZ: "45deg",
translateY: "30px",
translateX: "30px",
opacity: 0
},
{ complete: done }
);
}
}
};
</script>

过渡模式

<transition>的默认行为-进入和离开同时发生,但是在滑动过渡时不能满足要求,所以Vue提供了过渡模式, mode

1
2
<transition name="fade" mode="out-in">
</transition>
  • in-out: 新元素先进行过渡,完成后 当前元素再过渡离开
  • out-in: 当前元素先进行过渡,完成后 新元素再过渡进入

单元素/组件的 过渡

Vue提供 transition的封装组件,以下情况,可以元素/组件 添加 进入/离开 过渡

  • 条件渲染(v-if
  • 条件展示(v-show
  • 动态组件 (is
  • 组件根节点

简单例子:

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
<template>
<div id="example-1">
<h2>Transitions & Animation</h2>
<div>
<button v-on:click="show = !show">Toggle</button>
<transition name="fade">
<p v-if="show">Jiang Jiang</p>
</transition>
</div>
</div>
</template>

<script>
export default {
name: "TransitionDemo",
data() {
return {
show: true
};
}
};
</script>

<style lang="less" scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
</style>

多个元素的过渡

当有相同标签名的元素时,需要通过给key设置唯一的值来标记,以让Vue区分它们,否则Vue未来效率会替换相同标签内部的内容。所以,在给<transition>组件中多个元素设置key是一个更好的实践。

通过给同一个元素的key设置不同状态来代替v-ifv-else

1
2
3
4
5
<transition>
<button v-bind:key="isEditing">
{{isEditing ? 'Save' : 'Edit'}}
</button>
</transition>

例外,多个v-if的多元素的过渡,也可重写为绑定了动态的key的单个元素

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
<transition>
<button v-if="docState === 'saved'" key="saved">
Edit
</button>
<button v-if="docState === 'edited'" key="edited">
Save
</button>
<button v-if="docState === 'editing'" key="editing">
Cancel
</button>
</transition>

// 重写
<transition>
<button v-bind:key="docState">
{{ buttonMessage }}
</button>
</transition>
<script>
// ...
data() {
return {
docState: 'Save'
}
},
computed: {
buttonMessage: function () {
switch (this.docState) {
case 'saved': return 'Edit'
case 'edited': return 'Save'
case 'editing': return 'Cancel'
}
}
}
</script>

多个组件的过渡

多个组件不需要使用key ,只需要使用动态组件即可

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
<div id="example-4">
<label>
<input type="radio" name="" id="" value="v-a" v-model="view" />A</label
>
<label>
<input type="radio" name="" id="" value="v-b" v-model="view" />B</label
>
<transition name="component-fade" mode="out-in">
<component v-bind:is="view"></component>
</transition>
</div>

<script>
// ....
components: {
"v-a": {
// template: "<div>aaaa</div>"
render: h => h("div", "aaaa")
},
"v-b": {
// template: "<div>bbbbb</div>"
render: h => h("div", "bbbb")
}
},

</script>
<style>
.component-fade-enter-active, .component-fade-leave-active {
transition: opacity .3s ease;
}
.component-fade-enter, .component-fade-leave-to {
opacity: 0;
}
</style>

列表过渡

transition的过渡,只是

  • 单个节点
  • 同一时间渲染多个节点中的一个

<transition-group>,同时渲染整个列表,或使用v-for时同时渲染多个节点。该组件的特点:

  1. 不同transition,它会以一个 真实元素呈现,默认span,通过tag attribute 更换为其他元素
  2. 内部元素总需要提供唯一的key
  3. CSS过渡的类会应用到内部元素中,而不是这个组件/容器本身
  4. 内部元素在改变定位的过程中,也可以添加过渡效果。使用新增的过渡的类名 v-move,通过name来自定义前缀,通过move-class attribute 手动设置

可复用的过渡

要创建一个可复用的过渡组件,只需将<transition>transition-group作为根组件,然后将任何子组件放置其中即可

动态过渡

  1. 过渡 attribute 都时可以动态绑定的,通过name来绑定动态值,从而在不同的过渡间切换
  2. 通过事件钩子获取上下文中的所有数据,即 可根据组件的状态,设置不同的Javascript过渡表现

状态过渡

对于数据元素本身的动效,例如 _数字和运算_,可以结合Vue的响应式和组件系统,使用第三方库来实现过渡状态。

使用GreenSock,通过watch,监听数据更新,应用过渡状态

混入 Mixin

混入一般用来分发Vue组件中可复用的功能。一个混入对象可以包含任意组件的选项。当组件属于混入对象时,所有混入对象将混入“进该组件本身的选项。

选项合并

当组件和混入对象含有同名选项时,这些选项按选项合并策略进行”合并“

  • 数据对象data在内部会进行递归合并,在发生冲突时以组件内数据优先
  • 同名钩子函数将合并成一个数组,因此都将被调用。且混入对象的钩子函数将在组件自身钩子之前被调用
  • 值为对象的选项,例如methods,components,directives,computed,将被合并为同一对象,当有键名冲突时,取组件对象的键值对。

自定义选项合并策略

如果想要自定义选项合并策略,可以向Vue.configin.optionMergeStrategies添加一个函数:

1
2
3
4
5
6
7
8
9
10
const merge = Vue.config.optionMergeStrategies.computed
Vue.config.optionMergeStrategies.vuex = function (toVal, fromVal) {
if (!toVal) return fromVal
if (!fromVal) return toVal
return {
getters: merge(toVal.getters, fromVal.getters),
state: merge(toVal.state, fromVal.state),
actions: merge(toVal.actions, fromVal.actions)
}
}

自定义指令

如果需要对普通DOM元素进行底层操作,可使用自定义指令。全局注册使用Vue.directive(),局部注册, 使用组件的选项directives

利用指令,实现输入框聚焦

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 全局注册
Vue.directive('focus', {
inserted: function(el) {
el.focus();
}
});


// 组件内 局部注册
directives: {
focus: {
inserted: function(el) {
el.focus();
}
}
},

钩子 函数

指令对象可以提供5个可选的钩子函数

  • bind只调用一次,指令在第一次 绑定 到元素时调用。在这里通常 进行 一次性的初始化设置
  • inserted:被绑定元素插入父节点时调用,但不一定被插入文档,仅仅保证了父节点的存在
  • unpdate: 所在组件的VNode更新时调用,可能发生在其子VNode更新之前
  • componentUpdated:所在组件的VNode及其子VNode全部更新后调用
  • unbind只调用一次,指令与元素解绑时调用

钩子函数可接受4个参数,除了el外,其他参数都应该是可读的,切勿修改

  • el: 指令所绑定的元素,可用来直接操作DOM
  • binding: 一个对象,包含如下property
    • name:指令名 不包含v-前缀
    • value:指令绑定的值
    • oldValue:指令绑定的前一个值
    • expression:字符串形式的指令表达式
    • arg: 传给指令的参数。 例如:v-my-directive:foo,参数 为foo
    • modifiers:包含修饰符的对象。例如:v-my-directive:foo.bar.zar,修饰符对象为{bar:true, zar:true}
  • vnode: Vue编译生成的虚拟节点
  • oldVnode:上一个虚拟节点