2925 lines
68 KiB
Markdown
Executable File
2925 lines
68 KiB
Markdown
Executable File
# VUE 2.x
|
||
|
||
https://cn.vuejs.org/
|
||
|
||
## 介绍
|
||
|
||
**mvvm**, 数据驱动视图的渐进式框架
|
||
|
||
* M: model, data中的数据
|
||
* V: view, 模板代码
|
||
* VM: ViewModel, 连接 View 和 Model, Vue实例
|
||
|
||
## 安装
|
||
|
||
* 兼容性: Vue **不支持** IE8 及以下版本, 因为 Vue 使用了 IE8 无法模拟的 ECMAScript 5 特性
|
||
* 项目开发中一般通过vue-cli搭建开发环境 https://cli.vuejs.org/zh/guide/
|
||
|
||
## 全家桶
|
||
|
||
脚手架: `vue-cli`
|
||
|
||
路由管理: `vueRouter`
|
||
|
||
状态管理: `vuex`
|
||
|
||
http库: `axios`
|
||
|
||
ui框架: `element`, `ant design vue`
|
||
|
||
## 优秀的相关项目
|
||
|
||
https://github.com/vuejs/awesome-vue#libraries--plugins
|
||
|
||
## 生命周期
|
||
|
||

|
||
|
||
1. `beforeCreate `:在内存中创建出vue实例,数据观测 (data observer) 和 event/watcher 事件配置还没调用(data 和 methods 属性还没初始化)
|
||
2. 【执行数据观测 (data observer) 和 event/watcher 事件配置】
|
||
3. `created`: 实例已完成数据观测 (data observer),property 和方法的运算,watch/event 事件回调。(data 和 methods属性完成初始化,还没开始编译模板,可以进行Ajax请求)
|
||
4. beforeMount:模板编译完成,还没有挂载到页面中,相关的 render 函数首次被调用
|
||
5. 【挂载模板到页面】
|
||
6. `mounted `:模板已挂载到页面中,真实的DOM渲染完成。可以操作DOM了!
|
||
1. mounted 不保证所有子组件也一起被挂载, 需要子组件都挂载后才执行的操作, 使用 `vm.$nextTick`
|
||
7. 至此,实例结束创建期,进入运行期,等待数据发生变化。
|
||
8. 【数据变化】数据变化时,会触发beforeUpdate和updated,但一般用watch
|
||
9. `beforeUpdate`:状态更新之前执行此函数,此时data中的状态值是最新的,但是界面上显示的数据还是旧的,因为此时还没有开始重新渲染DOM节点
|
||
10. 【更新页面】
|
||
11. `updated`:实例更新完毕后调用此函数,此时 data 中的状态值和界面上显示的数据,都已经完成了更新,界面已经被重新渲染好了。
|
||
1. 可用于子组件向父组件传递数据的变化 `updated(){ this.$emit('contentChange',this.content) }`
|
||
12. `beforeDestroy`:实例销毁之前调用。在这一步,实例仍然完全可用。常在这里清除定时器和事件绑定
|
||
13. 【销毁实例】
|
||
14. `destroyed`:Vue 实例销毁后调用。此时,Vue实例绑定的所有东西都会解绑,所有的事件监听器会被移除,所有的子实例也会被销毁
|
||
|
||
* 不要在选项 property 或回调上使用箭头函数,比如 `created: () => console.log(this.a)` 或 `vm.$watch('a', newValue => this.myMethod())`, 因为箭头函数并没有 `this`
|
||
|
||
## 术语
|
||
|
||
* 前缀 `$`, 表示Vue内部提供的一些属性或方法
|
||
* **vm** (ViewModel 的缩写) 这个变量名表示 Vue 实例
|
||
* {{}} 双大括号: 将数据解析成文本
|
||
* js表达式和js代码: 表达式会产生一个值, 可以放在需要值的地方. `x==y?'a':'b'`
|
||
|
||

|
||
|
||
## 最简单的vue例子
|
||
|
||
```html
|
||
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>调试</title>
|
||
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
|
||
</head>
|
||
<body>
|
||
<div id="app">
|
||
{{ message }}
|
||
</div>
|
||
|
||
<script>
|
||
const vm = new Vue({
|
||
// 配置el属性
|
||
el: '#app', // el用于指定当前Vue实例为哪个容器服务, css选择器
|
||
// 对象式
|
||
data: { //data中用于存储数据,数据供el所指定的容器去使用
|
||
message: 'Hello Vue!'
|
||
},
|
||
})
|
||
|
||
console.log(vm)
|
||
</script>
|
||
</body>
|
||
</html>
|
||
```
|
||
|
||
el 和 data 的第二种写法:
|
||
|
||
```js
|
||
const vm = new Vue({
|
||
data: function(){ // 函数式 // vue组件中必须使用函数式
|
||
return {
|
||
message: 'Hello Vue!'
|
||
}
|
||
}
|
||
})
|
||
|
||
vm.$mount('#app') // 先创建对象再通过mount指定el
|
||
console.log(vm)
|
||
```
|
||
|
||
ps: 实际项目开发中以单文件模式开发
|
||
|
||
## 模板语法
|
||
|
||
* 插值语法: `{{xxx}}`, 可以直接读取 data 中的值
|
||
* 指令语法: `v-bind:href="xxx"`
|
||
|
||
## 数据绑定
|
||
|
||
* 单向绑定: `v-bind`
|
||
* 双向绑定: `v-model`, 一般在表单元素上(如: input, select)
|
||
|
||
## 响应式数据
|
||
|
||
1. data中的所有属性, 最后都出现在vm上
|
||
2. vm上的属性, 以及Vue原型上的方法, 都可以在Vue模板中直接使用
|
||
|
||
### Object.defineProperty 数据劫持
|
||
|
||
```js
|
||
let number = 18
|
||
let person = {
|
||
name: '张三',
|
||
sex: 'man'
|
||
}
|
||
|
||
Object.defineProperty(person, 'age', {
|
||
// value, // 有value时不能设置get和set
|
||
// enumerable:false, 控制属性是否可以被枚举, 默认false
|
||
// writable:false, //控制属性是否可以被修改,默认值是false
|
||
// configurable:false //控制属性是否可以被删除,默认值是false
|
||
get(){
|
||
console.log('读取age属性')
|
||
return number
|
||
},
|
||
set(val){
|
||
console.log(`设置age属性, 值为${val}`)
|
||
number = val
|
||
}
|
||
})
|
||
```
|
||
|
||
<img src="https://img.081024.xyz/202111191514713.png" width="250px"/>
|
||
|
||
<img src="https://img.081024.xyz/202111191519695.png" width="300px"/>
|
||
|
||
* 如图, age 属性不直接显示, 而是用通过 `getter` 获取, 通过 `setter` 设置.
|
||
* set 和 get 叫 `存取描述符`, 有 value 就是 `数据描述符` (不能同时用)
|
||
* 枚举属性: 不可枚举键是浅色的, 且 Object.keys 等方法读不到
|
||
|
||
<img src="https://img.081024.xyz/202111191527841.png" width="300"/><img src="https://img.081024.xyz/202111191527136.png" width="300"/>
|
||
|
||
* 控制是否可被修改
|
||
|
||
```js
|
||
let person = {
|
||
name: '张三',
|
||
sex: 'man'
|
||
}
|
||
|
||
Object.defineProperty(person, 'age', {
|
||
value = 18, // 有value时不能设置get和set
|
||
writable:true, //控制属性是否可以被修改,默认值是false
|
||
})
|
||
```
|
||
|
||
<img src="https://img.081024.xyz/202111191554832.png" width="300"/><img src="https://img.081024.xyz/202111191554837.png" width="300"/>
|
||
|
||
* configurable 为 false, 删除会返回 false
|
||
|
||
### Vue中的数据劫持
|
||
|
||
1. 通过vm对象来劫持data对象中属性的操作(读/写)
|
||
2. 通过 `Object.defineProperty()` 把data对象中所有属性添加到vm上。
|
||
|
||
```js
|
||
console.log(vm.$data === vm_data) // true
|
||
```
|
||
|
||
## SFC
|
||
|
||
> 单文件组件
|
||
|
||
* 模板
|
||
|
||
```vue
|
||
<template>
|
||
<div class="container">
|
||
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
export default {
|
||
name: '',
|
||
// extends: '',
|
||
// mixins: [],
|
||
components: {},
|
||
props: {},
|
||
// model: { prop: '', event: '' },
|
||
data () {
|
||
return {
|
||
|
||
}
|
||
},
|
||
computed: {},
|
||
watch: {},
|
||
filters: {},
|
||
// directives: {},
|
||
created () {},
|
||
mounted () {},
|
||
// beforeDestroy () {},
|
||
methods: {},
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="less">
|
||
|
||
</style>
|
||
```
|
||
|
||
## vue 常见指令
|
||
|
||
### v-bind(缩写 : ) 动态绑定
|
||
|
||
```js
|
||
<a v-bind:href="url">...</a>
|
||
|
||
// 缩写
|
||
<a :href="url">...</a>
|
||
```
|
||
|
||
* v-bind的值当成js解析, `:rules="/(^1[035789]\d{3}$)|(^[A-Za-z]\w{2,10}$)/"`, 正则需要解析
|
||
* 动态参数
|
||
|
||
```js
|
||
<a v-bind:[attributeName]="url"> ... </a>
|
||
|
||
// 方括号里面作为表达式进行计算, 把结果当成属性名
|
||
// 表达式不能有空格或者引号, 可以用计算属性替代
|
||
// 避免使用大写, 因为会被转成小写
|
||
```
|
||
|
||
#### class 对象语法 数组语法
|
||
|
||
除了 string , 还可以是对象或者数组
|
||
|
||
ps: 其实也可以把复杂的字符串计算放到计算属性里
|
||
|
||
```js
|
||
<div :class="{ red: isRed }"></div> // 对象语法: isRed为true时绑定red类名
|
||
|
||
<div :class="[classA, classB]"></div> //Array语法: 绑定列表里的class
|
||
|
||
<div :class="[classA, { classB: isB, classC: isC }]" /> // 可以混合使用
|
||
|
||
<div v-bind:class="[isActive ? activeClass : '', errorClass]"></div> //根据条件切换class,这样写将始终添加 errorClass,但是只有在 isActive 是 truthy时才添加activeClass。
|
||
|
||
<div v-bind:class="[{ active: isActive }, errorClass]"></div> //使用对象语法简写
|
||
|
||
//错误示范
|
||
<van-cell title="性别" is-link :value="{'男': gender === 1, '女': gender === 0} " /> // value不能使用对象语法绑定, 提示value不能赋值对象
|
||
//改正: 可以写成三目运算符, ""里面的当成js解析
|
||
<van-cell title="性别" is-link :value="gender === 1? '男':'女'" />
|
||
```
|
||
|
||
#### style 对象语法 数组语法
|
||
|
||
* 单个对象
|
||
|
||
```js
|
||
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
|
||
|
||
// ...
|
||
data: {
|
||
activeColor: 'red',
|
||
fontSize: 30
|
||
}
|
||
```
|
||
|
||
更简洁
|
||
|
||
```js
|
||
<div :style="styleObject"></div>
|
||
|
||
// ...
|
||
data: {
|
||
styleObject: {
|
||
color: 'red',
|
||
fontSize: '13px'
|
||
}
|
||
}
|
||
```
|
||
|
||
* 多个对象
|
||
|
||
```js
|
||
<div :style="[baseStyles, overridingStyles]"></div>
|
||
```
|
||
|
||
#### .sync 双向绑定修饰符
|
||
|
||
> 在自定义组件中, 对 porps 进行双向绑定, 和 v-model 相比, 可以多个
|
||
|
||
```js
|
||
// 父组件中
|
||
<Child :prop.sync="value" />
|
||
|
||
// 子组件中
|
||
props: {
|
||
prop: 类型
|
||
}
|
||
// ...
|
||
this.$emit('update:prop', newValue)
|
||
```
|
||
|
||
父组件其实是下面的简写
|
||
|
||
```vue
|
||
<Child :prop="value" @update:prop="prop = $event"/>
|
||
```
|
||
|
||
### v-for
|
||
|
||
> 遍历Array或者Object (并不支持可响应的 `Map` 和 `Set` 值,所以无法自动探测变更)
|
||
|
||
```vue
|
||
<div v-for="(item, index/key) in items" :key="item.id"></div>
|
||
|
||
// 也可以用 of 代替 in, 更贴近js迭代器语法
|
||
<div v-for="(item, inde) of items" :key="item.id"></div>
|
||
```
|
||
|
||
* 不指定key编辑器会报错: key 用来区分每个元素, 确保高效更新和渲染.
|
||
* key 不能用 index, 在增删时可能导致出错
|
||
* map set 可以通过计算属性间接使用
|
||
|
||
#### template 的包裹
|
||
|
||
```vue
|
||
<ul>
|
||
<template v-for="item in items">
|
||
<li>{{ item.msg }}</li>
|
||
<li class="divider" role="presentation"></li>
|
||
</template>
|
||
</ul>
|
||
```
|
||
|
||
#### 数组的更新
|
||
|
||
`push() pop() shift() unshift() splice() sort() reverse()` 这类会改变原数据的方法, 都进行了包裹, 它们也会触发视图更新
|
||
|
||
`filter() concat() slice()` 这类返回新数组的方法, 则需要使用新数组代替旧数组
|
||
|
||
通过 `this.array[index] = newValue` 不能触发视图更新, 可以通过 ` this.array.splice(index, 1, newValue) `
|
||
|
||
或者使用 `this.$set(this.array, index, newValue)`
|
||
|
||
#### 对象的更新
|
||
|
||
对象属性的添加和删除无法触发更新, 因为初始化时就为对象的属性添加了 setter/getter 转化
|
||
|
||
* 添加属性
|
||
|
||
如果要为对象添加响应式的属性, 可以使用 `this.$set(this.obj, key , newValue)`
|
||
|
||
直接使用 `Object.assign()` 也无法添加相应式属性, 应该把混合后的对象赋值给原来的对象 (使用解构会丢失响应性)
|
||
|
||
`this.obj=Object.assign({}, this.obj, {a:1, b:2})`
|
||
|
||
* 删除属性
|
||
`this.$delete(this.obj, key)`
|
||
|
||
### v-show
|
||
|
||
> 实际上是设置 `display: block;` / `display: none;`, 只是简单地基于 CSS 进行切换
|
||
|
||
```vue
|
||
<h1 v-show="true">Hello!</h1>
|
||
```
|
||
|
||
### v-if
|
||
|
||
> 实际上是用`<!---->`注释内容, "真正"的条件渲染
|
||
|
||
* v-if 和v-show 的区别:
|
||
* 手段:v-if 是动态的向DOM树内添加或者删除DOM元素;v-show 是通过设置DOM元素的display样式属性控制显隐;
|
||
* 编译过程:v-if切换有一个 `局部编译/卸载` 的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show只是简单的基于 `css切换`;
|
||
* 编译条件:v-if是惰性的,如果初始条件为假,则什么也不做;只有在条件第一次变为真时才开始局部编译(编译被缓存?编译被缓存后,然后再切换的时候进行局部卸载); v-show是在任何条件下(首次条件是否为真)都被编译,然后被缓存,而且DOM元素保留;
|
||
* 性能消耗:v-if有更高的切换消耗;v-show有更高的初始渲染消耗
|
||
* 场景:
|
||
* 一般来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。
|
||
* 因此,如果需要非常频繁地切换,则使用 v-show 较好;如果在运行时条件很少改变,则使用 v-if 较好。
|
||
* ===v-if和v-for的不能同时使用===
|
||
* `v-for` 具有比 `v-if` 更高的优先级, v-for 的每次迭代都会进行 v-if 判断, 造成性能浪费
|
||
* 逻辑混乱, 展示的数据和被迭代的数据不一样
|
||
* 正确方法: 先用计算属性代替v-if进行过滤, 再对计算属性进行遍历
|
||
|
||
#### template 包裹
|
||
|
||
同样可以把逻辑写在 template 里
|
||
|
||
```vue
|
||
<template v-if="isTrue">
|
||
...
|
||
</template>
|
||
```
|
||
|
||
### v-else
|
||
|
||
和v-if一起使用
|
||
|
||
```vue
|
||
<div v-if="Math.random() > 0.5">
|
||
Now you see me
|
||
</div>
|
||
<div v-else>
|
||
Now you don't
|
||
</div>
|
||
```
|
||
|
||
### v-else-if
|
||
|
||
和v-if v-else-if 一起使用
|
||
|
||
```vue
|
||
<div v-if="type === 'A'">
|
||
A
|
||
</div>
|
||
<div v-else-if="type === 'B'">
|
||
B
|
||
</div>
|
||
<div v-else-if="type === 'C'">
|
||
C
|
||
</div>
|
||
<div v-else>
|
||
Not A/B/C
|
||
</div>
|
||
```
|
||
|
||
### v-model
|
||
|
||
`v-model` 本质上是 `.sync` 的语法糖, 针对下面的这些场景做了内置简化
|
||
|
||
* **限制**:
|
||
* `<input>`
|
||
* `<select>`
|
||
* `<textarea>`
|
||
* 自定义组件
|
||
* 子组件中 props 要接收一个 `value`, 并且通过 `@input="$emit('input', newValue)"` 向父组件传递 input 事件
|
||
|
||
```vue
|
||
<input v-model="searchText">
|
||
```
|
||
|
||
等价于
|
||
|
||
```vue
|
||
<input
|
||
:value="searchText"
|
||
@input="searchText = $event.target.value"
|
||
>
|
||
```
|
||
|
||
#### 修饰符
|
||
|
||
* `.number`: 将用户的输入值转为数值类型
|
||
|
||
```html
|
||
<input v-model.number="age" type="number">
|
||
```
|
||
|
||
* `.trim `输入首尾空格过滤
|
||
* `.lazy` 在 change 事件后进行同步, 默认是每次 input 都会触发
|
||
|
||
#### checkbox radio 不使用 value
|
||
|
||
> input 类型为单选或者多选时, value 有其他作用, 这时可以自定义 model 的属性名
|
||
|
||
子组件: 在 model 里定义新的属性名, 同时需要在 props 里接声明
|
||
|
||
```vue
|
||
<template>
|
||
<input
|
||
type="checkbox"
|
||
:checked="checked"
|
||
@change="$emit('change', $event.target.checked)"
|
||
/>
|
||
</template>
|
||
|
||
<script>
|
||
export default {
|
||
name: 'BaseCheckbox',
|
||
model: {
|
||
prop: 'checked',
|
||
event: 'change',
|
||
},
|
||
props: {
|
||
checked: {
|
||
type: Boolean,
|
||
default: false, // 设置默认值
|
||
},
|
||
},
|
||
};
|
||
</script>
|
||
```
|
||
|
||
父组件:
|
||
|
||
```vue
|
||
<base-checkbox v-model="lovingVue"></base-checkbox>
|
||
```
|
||
|
||
### v-on (缩写 @ )
|
||
|
||
> 绑定事件监听器。事件类型由参数指定。表达式可以是一个方法的名字或一个内联语句,如果没有修饰符也可以省略
|
||
|
||
在监听原生 DOM 事件时,方法以事件为唯一的参数。如果使用内联语句,语句可以访问一个 `$event` property
|
||
|
||
```html
|
||
<button v-on:click="handle('ok', $event)"></button>
|
||
```
|
||
|
||
```html
|
||
<!-- 方法处理器 -->
|
||
<button v-on:click="handle"></button>
|
||
<!-- 缩写 -->
|
||
<button @click="doThis"></button>
|
||
<!-- 阻止默认行为 preventDefault,没有表达式 -->
|
||
<form @submit.prevent></form>
|
||
<!-- 阻止冒泡 stopPropagation -->
|
||
<btuuon @click.stop></button>
|
||
<!-- 串联修饰符, 有顺序 -->
|
||
<button @click.stop.prevent="doThis"></button>
|
||
```
|
||
|
||
* 自定义事件名在渲染成 html 时会自动转化成全小写, 所以官方推荐用短横线命名法. 但在 sfc 中, 小驼峰也能识别到
|
||
* 动态参数
|
||
|
||
```js
|
||
<a @[event]="doSomething"> ... </a>
|
||
```
|
||
|
||
#### 修饰符
|
||
|
||
* `.stop`: 调用 `event.stopPropagation()`阻止事件冒泡(常用)
|
||
* `.prevent`: 调用 `event.preventDefault()`阻止默认事件(常用)
|
||
* `.native`: 监听组件**根元素**的原生事件 (内部组件冒泡的不监听)
|
||
* `.once`: 只触发一次回调。(常用)
|
||
* `.capture`:使用事件的捕获模式
|
||
* `.self`:只有event.target是当前操作的元素时才触发事件
|
||
* `.passive`:事件的默认行为立即执行,无需等待事件回调执行完毕, 能够提高移动端的性能, 不能和 `.prevent` 一起使用
|
||
|
||
```js
|
||
@click.native.prevent
|
||
/**
|
||
阻止默认的冒泡事件
|
||
*/
|
||
```
|
||
|
||
有先后顺序: `v-on:click.prevent.self` 会阻止所有的点击, `v-on:click.self.prevent` 只会阻止对元素自身的点击
|
||
|
||
#### 键盘事件
|
||
|
||
```html
|
||
<!-- 键修饰符,键别名 -->
|
||
<input @keyup.enter="onEnter">
|
||
|
||
<!-- 键修饰符,键代码 -->
|
||
<input @keydown.13="onEnter">
|
||
|
||
<!--
|
||
常见的按键别名
|
||
回车 => enter
|
||
删除 => delete (捕获“删除”和“退格”键)
|
||
退出 => esc
|
||
空格 => space
|
||
换行 => tab (特殊,必须配合keydown去使用)
|
||
上 => up
|
||
下 => down
|
||
左 => left
|
||
右 => right
|
||
-->
|
||
```
|
||
|
||
对于没有别名的按键, 可以自定义别名
|
||
|
||
```js
|
||
Vue.config.keyCodes.自定义键名 = 键码
|
||
```
|
||
|
||
#### $listeners
|
||
|
||
`.native` 只能监听**根元素**的原生事件, 组件内部事件冒泡的并不监听
|
||
|
||
父组件:
|
||
|
||
```vue
|
||
<base-input v-on:focus.native="onFocus"></base-input>
|
||
```
|
||
|
||
子组件:
|
||
|
||
```vue
|
||
<label>
|
||
{{ label }}
|
||
<input
|
||
v-bind="$attrs"
|
||
v-bind:value="value"
|
||
v-on:input="$emit('input', $event.target.value)"
|
||
>
|
||
</label>
|
||
```
|
||
|
||
尝试监听子组件 `<base-input>` 的**根元素**的 `focus` 事件。然而,由于子组件的根元素是 `<label>`,而 `label` 标签本身并不会触发 `focus` 事件,因此父组件的监听器不会被调用,导致 `onFocus` 事件处理函数失效
|
||
|
||
vue 提供了 `$listeners` 的属性来解决这个问题. `$listeners` 是个对象, 里面包含里作用在这个组件上的所有监听器, 然后合并父级监听器和自定义监听器, 指向需要监听的子元素
|
||
|
||
> 把父组件传进来的事件都绑定到子组件某个元素上
|
||
|
||
```vue
|
||
<template>
|
||
<label>
|
||
{{ label }}
|
||
<input
|
||
v-bind="$attrs"
|
||
v-bind:value="value"
|
||
v-on="inputListeners"
|
||
>
|
||
</label>
|
||
</template>
|
||
|
||
<script>
|
||
export default {
|
||
name: 'BaseInput',
|
||
inheritAttrs: false, // 禁用默认继承的属性绑定
|
||
props: {
|
||
label: String, // 定义 label 属性
|
||
value: [String, Number], // 定义 v-model 对应的 value
|
||
},
|
||
computed: {
|
||
inputListeners() {
|
||
const vm = this;
|
||
// 合并父级监听器和自定义监听器
|
||
return Object.assign({},
|
||
this.$listeners,
|
||
{
|
||
// 确保组件配合 v-model 工作
|
||
input(event) {
|
||
vm.$emit('input', event.target.value);
|
||
},
|
||
}
|
||
);
|
||
},
|
||
},
|
||
};
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* 根据需要添加样式 */
|
||
</style>
|
||
|
||
```
|
||
|
||
### v-onece
|
||
|
||
只渲染元素和组件**一次**。随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。这可以用于优化更新性能
|
||
|
||
```vue
|
||
<my-component v-once :comment="msg"></my-component>
|
||
```
|
||
|
||
### v-text
|
||
|
||
```vue
|
||
<span v-text="msg"></span>
|
||
<!-- 和下面的一样 -->
|
||
<span>{{msg}}</span>
|
||
```
|
||
|
||
### v-html
|
||
|
||
```vue
|
||
<div v-html="html"></div>
|
||
```
|
||
|
||
将数据解析成html
|
||
|
||
而{{}}是解析成文本
|
||
|
||
**在网站上动态渲染任意 HTML 是非常危险的,因为容易导致 [XSS 攻击](https://en.wikipedia.org/wiki/Cross-site_scripting)。只在可信内容上使用 `v-html`,**永不**用在用户提交的内容上。**
|
||
|
||
### v-slot 插槽
|
||
|
||
> 在子组件中定义占位符,由父组件传入内容动态填充
|
||
|
||
v-slot官方文档:https://cn.vuejs.org/v2/guide/components-slots.html
|
||
|
||
旧属性 `slot/slot-scope` 被 `v-slot` 取代
|
||
|
||
* 父组件的标签对子组件传 HTML 结构
|
||
|
||
```vue
|
||
<MyButton>注册</MyButton>
|
||
```
|
||
|
||
* 子组件通过 `<slot>` 接收传递过来的 HTML 结构
|
||
|
||
```vue
|
||
<button>
|
||
<slot>按钮</slot>
|
||
<!--
|
||
传递过来的 HTML 结构会替换到 slot 的位置,如果没有传递则使用 slot 的默认值。
|
||
slot位置由子组件决定, 内容由父组件决定
|
||
-->
|
||
</button>
|
||
```
|
||
|
||
#### 后备内容
|
||
|
||
> 子模板可以设置默认内容, 当父组件没有提供内容就会显示这些默认的内容
|
||
|
||
```html
|
||
<!--子组件-->
|
||
<button type="submit">
|
||
<slot>Submit</slot>
|
||
</button>
|
||
|
||
<!--父组件-->
|
||
<submit-button></submit-button>
|
||
```
|
||
|
||
#### 具名插槽
|
||
|
||
> 具有名字的插槽, 父组件想区别子组件中的几个插槽,那就要用slot标签的name属性来标识
|
||
> 而没有name属性的叫匿名插槽
|
||
|
||
```vue
|
||
<!--
|
||
1. 子组件: <slot>元素元素添加name属性, 没有name属性的实际上默认name="default"
|
||
-->
|
||
<div class="container">
|
||
<header>
|
||
<slot name="header"></slot>
|
||
</header>
|
||
<main>
|
||
<slot></slot>
|
||
</main>
|
||
<footer>
|
||
<slot name="footer"></slot>
|
||
</footer>
|
||
</div>
|
||
|
||
<!--
|
||
2. 父组件: <template> 添加 v-slot:插槽名
|
||
-->
|
||
<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>
|
||
|
||
//<template> 元素中的所有内容都将会被传入相应的插槽。任何没有被包裹在带有 v-slot 的 <template> 中的内容都会被视为默认插槽的内容。
|
||
```
|
||
|
||
##### 动态插槽名
|
||
|
||
* 插槽名也可以是动态的
|
||
|
||
```vue
|
||
<base-layout>
|
||
<template v-slot:[dynamicSlotName]>
|
||
...
|
||
</template>
|
||
</base-layout>
|
||
```
|
||
|
||
##### 具名插槽的缩写
|
||
|
||
`v-slot:` 换成 `#`
|
||
|
||
* default 不能省略
|
||
|
||
```vue
|
||
<current-user #default="{ user }">
|
||
{{ user.firstName }}
|
||
</current-user>
|
||
```
|
||
|
||
#### 编译作用域
|
||
|
||
> 父级组件里的所有内容都是在父级作用域中编译的
|
||
> 子组件里的所有内容都是在子作用域中编译的
|
||
> 也就是说, 子组件没办法直接拿到父组件的变量, 父组件也不能直接拿到子组件的变量
|
||
|
||
子组件 `<navigation-link>` :
|
||
|
||
```html
|
||
<a
|
||
v-bind:href="url"
|
||
class="nav-link"
|
||
>
|
||
<slot></slot>
|
||
</a>
|
||
```
|
||
|
||
父组件:
|
||
|
||
```html
|
||
<navigation-link>
|
||
这里实际拿不到子组件的 {{ url }}
|
||
</navigation-link>
|
||
```
|
||
|
||
#### ==作用域插槽==
|
||
|
||
https://blog.csdn.net/willard_cui/article/details/82469114
|
||
|
||
> 在子组件的 slot 标签上绑定属性, 在父组件用 v-slot="xxx" 接收
|
||
> 目的: 父组件访问子组件中的变量, 向子组件的插槽传递带有**子组件变量的模板**
|
||
> 简单理解: 在父组件的插槽中, 控制子组件的变量的展示方式
|
||
|
||
```js
|
||
/**
|
||
1. 子组件在<slot> 绑定数据
|
||
<slot :属性名1="值1" :属性名2="值2"> </slot>
|
||
2. 父组件在 <template> 接收数据
|
||
<template v-slot="slotProps">
|
||
包含所有子组件的所有插槽prop的对象命名为 slotPros, 也可以命名为其他名字
|
||
3. 父组件操作数据, {{slotProps.属性名1}} {{slotPros.属性名2}}
|
||
*/
|
||
```
|
||
|
||
```vue
|
||
<!--子组件-->
|
||
<template>
|
||
<div class="子组件">
|
||
<table>
|
||
<tbody>
|
||
<!-- 必须写:key, 否则eslint报错 -->
|
||
<tr v-for="(item, index) in data" :key="index">
|
||
<!-- 作用域插槽: 把变量绑定到属性上 -->
|
||
<slot :row="item" />
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
export default {
|
||
props: ['data']
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
</style>
|
||
```
|
||
|
||
```vue
|
||
<!--父组件-->
|
||
<template>
|
||
<div>
|
||
<myTable :data="data">
|
||
|
||
<!-- 用v-slot接收插槽的变量 -->
|
||
<template v-slot:default="slotProps">
|
||
<!--使用具名插槽时, name不能省略; 使用默认插槽时, :default可以省略 -->
|
||
<!-- 在父组件中控制子组件的插槽数据以什么形式显示 -->
|
||
<td>{{ slotProps.row.tittle }}</td>
|
||
<td>{{ slotProps.row.content }}</td>
|
||
</template>
|
||
</myTable>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import myTable from '@/components/作用域插槽-子组件.vue'
|
||
export default {
|
||
components: {
|
||
myTable
|
||
},
|
||
data() {
|
||
return {
|
||
data: [
|
||
{
|
||
tittle: '标题1',
|
||
content: '内容1'
|
||
},
|
||
{
|
||
tittle: '标题2',
|
||
content: '内容2'
|
||
},
|
||
{
|
||
tittle: '标题3',
|
||
content: '内容3'
|
||
}
|
||
]
|
||
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style>
|
||
</style>
|
||
```
|
||
|
||
* 只有默认插槽, 可以把 template 省略掉, 把 `v-slot` 写到组件上, default 也可以省略掉
|
||
* 当既有默认插槽, 又有具名插槽, 不可省略
|
||
|
||
```vue
|
||
<myTable :data="data" v-slot="slotProps">
|
||
<td>{{ slotProps.row.tittle }}</td>
|
||
<td>{{ slotProps.row.content }}</td>
|
||
</myTable>
|
||
```
|
||
|
||
#### slotProps 解构
|
||
|
||
`v-slot` 的值实际是 js 表达式, 所以在 slotProps 里的多个值, 可以用解构取出一个, 也可以重命名
|
||
|
||
```vue
|
||
<current-user v-slot="{ user }">
|
||
{{ user.firstName }}
|
||
</current-user>
|
||
```
|
||
|
||
* 重命名
|
||
|
||
```vue
|
||
<current-user v-slot="{ user: person }">
|
||
{{ person.firstName }}
|
||
</current-user>
|
||
```
|
||
|
||
#### $slots
|
||
|
||
$slots是组件插槽集,是组件所有默认插槽、具名插槽的集合,可以用来获取当前组件的插槽集
|
||
|
||
例如:`v-slot:foo` 中的内容将会在 `vm.$slots.foo` 中被找到
|
||
|
||
### v-pre
|
||
|
||
跳过编译过程直接显示
|
||
|
||
```vue
|
||
<div v-pre>{{msg}}</div>
|
||
<!-- 这里就直接显示{{msg}} -->
|
||
```
|
||
|
||
## 自定义指令
|
||
|
||
> 实现对普通 DOM 元素进行底层操作
|
||
|
||
使用方式: `v-xxx=""`
|
||
|
||
### 注册
|
||
|
||
#### 全局注册
|
||
|
||
```js
|
||
// 注册一个全局自定义指令 `v-focus`
|
||
Vue.directive('focus', {
|
||
// 当被绑定的元素插入到 DOM 中时……
|
||
inserted: function (el) {
|
||
// 聚焦元素
|
||
el.focus()
|
||
}
|
||
})
|
||
```
|
||
|
||
#### 局部注册
|
||
|
||
```js
|
||
// 局部注册: 组件中接受一个 directives 的选项
|
||
directives: {
|
||
focus: {
|
||
// 指令的定义
|
||
inserted: function (el) {
|
||
el.focus()
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
使用:
|
||
|
||
```vue
|
||
<input v-focus>
|
||
```
|
||
|
||
### 钩子函数
|
||
|
||
一个指令定义对象可以提供如下几个钩子函数 (均为可选):
|
||
|
||
* `bind`:指令第一次绑定到元素时调用, 可以用来初始化设置。
|
||
* `inserted`:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
|
||
* `update`:所在组件的 VNode 更新时调用,**但是可能发生在其子 VNode 更新之前**。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新
|
||
* `componentUpdated`:指令所在组件的 VNode **及其子 VNode** 全部更新后调用。
|
||
* `unbind`:指令与元素解绑时调用。
|
||
|
||
#### 钩子函数的参数
|
||
|
||
* `el`:指令所绑定的元素,可以用来直接操作 DOM。
|
||
* `binding`:一个对象,包含以下 property:
|
||
* `name`:指令名,不包括 `v-` 前缀。
|
||
* `value`:指令的绑定值,例如:`v-my-directive="1 + 1"` 中,绑定值为 `2`。
|
||
* `oldValue`:指令绑定的前一个值,仅在 `update` 和 `componentUpdated` 钩子中可用。无论值是否改变都可用。
|
||
* `expression`:字符串形式的指令表达式。例如 `v-my-directive="1 + 1"` 中,表达式为 `"1 + 1"`。
|
||
* `arg`:传给指令的参数,可选。例如 `v-my-directive:foo` 中,参数为 `"foo"`。
|
||
* `modifiers`:一个包含修饰符的对象。例如:`v-my-directive.foo.bar` 中,修饰符对象为 `{ foo: true, bar: true }`。
|
||
* `vnode`:Vue 编译生成的虚拟节点。
|
||
* `oldVnode`:上一个虚拟节点,仅在 `update` 和 `componentUpdated` 钩子中可用。
|
||
|
||
### 动态指令的参数
|
||
|
||
* 指令的参数可以是动态的。例如,在 `v-mydirective:[argument]="value"` 中,`argument` 参数可以根据组件实例数据进行更新
|
||
|
||
```vue
|
||
<p v-pin:[direction]="200">I am pinned onto the page at 200px to the left.</p>
|
||
|
||
// ...
|
||
data(){
|
||
return {
|
||
direction: 'left'
|
||
}
|
||
}
|
||
```
|
||
|
||
```js
|
||
Vue.directive('pin', {
|
||
bind: function (el, binding, vnode) {
|
||
el.style.position = 'fixed'
|
||
var s = (binding.arg == 'left' ? 'left' : 'top')
|
||
el.style[s] = binding.value + 'px'
|
||
}
|
||
})
|
||
```
|
||
|
||
* 需要多个值,可以传入一个 JavaScript 对象字面量。记住,指令函数能够接受所有合法的 JavaScript 表达式。
|
||
|
||
```vue
|
||
<div v-demo="{ color: 'white', text: 'hello!' }"></div>
|
||
```
|
||
|
||
## 特殊属性
|
||
|
||
### key
|
||
|
||
> `key` 的特殊 attribute 主要用在 Vue 的虚拟 DOM 算法,在新旧 nodes 对比时辨识 VNodes。如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。而使用 key 时,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素。
|
||
|
||
有相同父元素的子元素必须有**独特的 key**。重复的 key 会造成渲染错误。
|
||
|
||
最常见的用例是结合 `v-for`:
|
||
|
||
```vue
|
||
<ul>
|
||
<li v-for="item in items" :key="item.id">...</li>
|
||
</ul>
|
||
```
|
||
|
||
它也可以用于强制替换元素/组件而不是重复使用它。当你遇到如下场景时它可能会很有用:
|
||
|
||
* 完整地触发组件的生命周期钩子
|
||
* 触发过渡
|
||
|
||
例如:
|
||
|
||
```vue
|
||
<transition>
|
||
<span :key="text">{{ text }}</span>
|
||
</transition>
|
||
```
|
||
|
||
当 `text` 发生改变时,`<span>` 总是会被替换而不是被修改,因此会触发过渡。
|
||
|
||
* 为什么在`v-for`时不能用`index`作为`key`
|
||
|
||
1. 触发的更新变多
|
||
2. 数据可能混乱
|
||
|
||
### ref
|
||
|
||
> `ref` 被用来给元素或子组件**注册引用信息**。引用信息将会注册在父组件的 `$refs` 对象上。如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例:
|
||
|
||
```vue
|
||
<!-- `vm.$refs.p` will be the DOM node -->
|
||
<p ref="p">hello</p>
|
||
|
||
<!-- `vm.$refs.child` will be the child component instance -->
|
||
<child-component ref="child"></child-component>
|
||
```
|
||
|
||
* `$refs` 只会在组件渲染完成后生效, 并且不是响应式的, 不要在模板或者计算属性中使用
|
||
|
||
### is
|
||
|
||
> 用在动态组件上, `<component>` 是 vue 内置的一个特殊组件, 用来动态渲染其他组件
|
||
|
||
```vue
|
||
<!-- 当 `currentView` 改变时,组件也跟着改变 -->
|
||
<component :is="currentTabComponent"></component>
|
||
```
|
||
|
||
`currentTabComponent` 可以包括
|
||
|
||
* 已注册组件的名字,或
|
||
* 一个组件的选项对象
|
||
|
||
## props
|
||
|
||
> property 复数 properties 的缩写
|
||
|
||
* 列出类型, 对象的 key 是属性名, 值是属性类型
|
||
|
||
```js
|
||
props: {
|
||
title: String,
|
||
likes: Number,
|
||
isPublished: Boolean,
|
||
commentIds: Array,
|
||
author: Object,
|
||
callback: Function,
|
||
contactsPromise: Promise // or any other constructor
|
||
}
|
||
|
||
// 使用: this.title, 当成data用
|
||
```
|
||
|
||
* 可以传静态, 也可以传动态 (父组件通过 v-bind)
|
||
* 传入一个对象作为所有属性 (父组件中 v-bind="obj")
|
||
* 添加校验
|
||
|
||
```js
|
||
props: {
|
||
// 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)
|
||
propA: Number,
|
||
// 多个可能的类型
|
||
propB: [String, Number],
|
||
// 必填的字符串
|
||
propC: {
|
||
type: String,
|
||
required: true
|
||
},
|
||
// 带有默认值的数字
|
||
propD: {
|
||
type: Number,
|
||
default: 100
|
||
},
|
||
// 带有默认值的对象
|
||
propE: {
|
||
type: Object,
|
||
// 对象或数组默认值必须从一个工厂函数获取
|
||
default: function () {
|
||
return { message: 'hello' }
|
||
}
|
||
},
|
||
// 自定义验证函数
|
||
propF: {
|
||
validator: function (value) {
|
||
// 这个值必须匹配下列字符串中的一个
|
||
return ['success', 'warning', 'danger'].includes(value)
|
||
}
|
||
}
|
||
}
|
||
|
||
```
|
||
|
||
type 可以是这些构造函数 `String Number Boolean Array Object Date Function Symbol`
|
||
|
||
还可以是自定义构造函数, 通过 instanceOf 来校验
|
||
|
||
```js
|
||
function Person (firstName, lastName) {
|
||
this.firstName = firstName
|
||
this.lastName = lastName
|
||
}
|
||
```
|
||
|
||
```js
|
||
props: {
|
||
p: Person
|
||
}
|
||
```
|
||
|
||
### $attrs
|
||
|
||
> 当一个组件没有声明任何 prop 时, 父组件的属性默认会添加到子组件的**根标签**上, 可通过`this.$attrs.name`直接获取属性
|
||
|
||
```html
|
||
<!--父组件-->
|
||
<MyInput err_message="请输入6到18位的密码"/>
|
||
|
||
<!--子组件-->
|
||
<input>
|
||
|
||
//打印
|
||
cosole.log(this.$attrs) //输出对象, 必须是props中没有声明
|
||
```
|
||
|
||
## data
|
||
|
||
> 响应数据
|
||
|
||
* Vue组件中的data为什么是函数?
|
||
|
||
Object是引用数据类型,如果不用 function 返回,每个组件的 data 都是同一个内存地址,一个数据改变了其他也改变了
|
||
|
||
举个例子:
|
||
|
||
```javascript
|
||
const MyComponent = function( ) {};
|
||
MyComponent.prototype.data = {
|
||
a: 1,
|
||
b: 2,
|
||
}
|
||
const component1 = new MyComponent();
|
||
const component2 = new MyComponent();
|
||
|
||
component1.data.a === component2.data.a; // true
|
||
component1.data.b = 5;
|
||
component2.data.b // 5
|
||
```
|
||
|
||
如果两个实例同时引用一个对象,那么当你修改其中一个属性的时候,另外一个实例也会跟着改;
|
||
|
||
两个实例应该有自己各自的域才对,需要通过下面的方法来进行处理:
|
||
|
||
```javascript
|
||
const MyComponent = function( ) {
|
||
this.data = this.data();
|
||
};
|
||
MyComponent.prototype.data = function( ) {
|
||
return {
|
||
a: 1,
|
||
b: 2,
|
||
}
|
||
};
|
||
```
|
||
|
||
这样么一个实例的data属性都是独立的,不会相互影响了。
|
||
|
||
## components
|
||
|
||
> 注册组件
|
||
|
||
```vue
|
||
<script>
|
||
export default {
|
||
import Hello from './components/Hello.vue'
|
||
// ...
|
||
components: {
|
||
Helllo
|
||
}
|
||
// ...
|
||
}
|
||
|
||
</script>
|
||
```
|
||
|
||
## computed 计算属性
|
||
|
||
> 计算属性: 模板中的复杂逻辑使用**计算属性**
|
||
|
||
* 特点:
|
||
|
||
1. **基于响应式依赖进行缓存的**。只在相关响应式依赖发生改变时它们才会重新求值。
|
||
2. **计算属性不能执行异步任务,计算属性必须同步执行**。也就是说计算属性不能向服务器请求或者执行异步任务。如果遇到异步任务,就交给侦听属性。
|
||
3. computed能做的,watch都能做,反之则不行. 能用computed的尽量用computed
|
||
|
||
```html
|
||
<div>{{testComputed}}</div>
|
||
<div>{{testComputed}}</div>
|
||
<div>{{testComputed}}</div>
|
||
<!-- 只打打印了一次 -->
|
||
```
|
||
|
||
```js
|
||
computed: {
|
||
testComputed(){
|
||
console.log('testComputed')
|
||
return 'testComputed'
|
||
}
|
||
},
|
||
```
|
||
|
||
反例: 函数在模板里每次都会触发
|
||
|
||
```html
|
||
<div>{{testMethod()}}</div>
|
||
<div>{{testMethod()}}</div>
|
||
<div>{{testMethod()}}</div>
|
||
<!-- 会打印3次 -->
|
||
```
|
||
|
||
```js
|
||
methods: {
|
||
testMethod(){
|
||
console.log('abc')
|
||
return 'testMethod'
|
||
}
|
||
},
|
||
```
|
||
|
||
* 应用:
|
||
|
||
1. 复杂的逻辑
|
||
|
||
```html
|
||
<div id="example">
|
||
{{ message.split('').reverse().join('') }}
|
||
</div>
|
||
```
|
||
|
||
改为
|
||
|
||
```html
|
||
<div id="example">
|
||
{{reversedMessage}}
|
||
</div>
|
||
```
|
||
|
||
```js
|
||
computed: {
|
||
// 计算属性的 getter
|
||
reversedMessage: function () {
|
||
// `this` 指向 vm 实例
|
||
return this.message.split('').reverse().join('')
|
||
}
|
||
}
|
||
```
|
||
|
||
2. 数据只用来渲染, 不改变刷新 (放在data里会消耗更多的资源)
|
||
|
||
```js
|
||
computed: {
|
||
test(){ // 如果把数据放data里, 多了很多数据劫持的资源消耗
|
||
return {
|
||
a: 1,
|
||
b: [
|
||
c: 2,
|
||
d: 3
|
||
e: [4, 5]
|
||
]
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
* 完整写法和setter(上面是只有getter时的简写)
|
||
|
||
```js
|
||
data(){
|
||
return {
|
||
message : 'msg'
|
||
}
|
||
},
|
||
computed : {
|
||
test: {
|
||
get(){
|
||
return this.message + '--'
|
||
},
|
||
set(val){
|
||
this.message = val // set里面是对依赖进行修改
|
||
}
|
||
}
|
||
},
|
||
mounted(){
|
||
this.test = 'msg2' // 调用computed里面的set
|
||
}
|
||
```
|
||
|
||
使用场景: 把计算属性双向绑定, 当计算属性修改时, 对依赖的响应数据进行修改
|
||
|
||
## watch 侦听属性
|
||
|
||
> 数据变化时执行异步或者开销较大的操作
|
||
|
||
```js
|
||
<template>
|
||
<div>
|
||
<input v-model="query" placeholder="输入搜索内容" />
|
||
<p v-if="loading">正在搜索...</p>
|
||
<ul>
|
||
<li v-for="result in results" :key="result.id">{{ result.name }}</li>
|
||
</ul>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
export default {
|
||
data() {
|
||
return {
|
||
query: "", // 用户的搜索内容
|
||
results: [], // 搜索结果
|
||
loading: false // 是否正在加载
|
||
};
|
||
},
|
||
watch: {
|
||
query: function (newQuery) {
|
||
// 如果输入为空,清空结果
|
||
if (!newQuery) {
|
||
this.results = [];
|
||
return;
|
||
}
|
||
|
||
this.loading = true; // 开始加载
|
||
|
||
// 模拟异步请求,使用 setTimeout 替代实际的 API 调用
|
||
setTimeout(() => {
|
||
// 假设返回了一些搜索结果
|
||
this.results = [
|
||
{ id: 1, name: `${newQuery} - 结果 1` },
|
||
{ id: 2, name: `${newQuery} - 结果 2` },
|
||
{ id: 3, name: `${newQuery} - 结果 3` }
|
||
];
|
||
this.loading = false; // 加载完成
|
||
}, 1000); // 模拟 1 秒延迟
|
||
}
|
||
}
|
||
};
|
||
</script>
|
||
```
|
||
|
||
## filters
|
||
|
||
> 过滤器
|
||
|
||
用于一些常见的**文本格式化**。过滤器可以用在两个地方:**双花括号插值和 `v-bind` 表达式**
|
||
|
||
备注: `|`叫管道符
|
||
|
||
```vue
|
||
<!-- 在双花括号中 -->
|
||
{{ message | capitalize }}
|
||
|
||
<!-- 在 `v-bind` 中 -->
|
||
<div v-bind:id="rawId | formatId"></div>
|
||
```
|
||
|
||
### 局部注册
|
||
|
||
```js
|
||
filters: {
|
||
capitalize: function (value) {
|
||
if (!value) return ''
|
||
value = value.toString()
|
||
return value.charAt(0).toUpperCase() + value.slice(1)
|
||
}
|
||
}
|
||
```
|
||
|
||
### 全局注册
|
||
|
||
```js
|
||
// 在vue实例化之前
|
||
Vue.filter('capitalize', function (value) {
|
||
if (!value) return ''
|
||
value = value.toString()
|
||
return value.charAt(0).toUpperCase() + value.slice(1)
|
||
})
|
||
```
|
||
|
||
### 过滤器可以串联
|
||
|
||
```vue
|
||
{{ message | filterA | filterB }}
|
||
```
|
||
|
||
### 过滤器可以接收参数
|
||
|
||
```vue
|
||
{{ message | filterA('arg1', arg2) }}
|
||
```
|
||
|
||
## methods
|
||
|
||
===**不应该使用箭头函数来定义 method 函数**===, 箭头函数绑定了父级作用域的上下文,所以 `this` 将不会按照期望指向 Vue 实例
|
||
|
||
## created
|
||
|
||
> 创建前生命周期钩子
|
||
|
||
## mounted
|
||
|
||
> 挂载前生命周期钩子
|
||
|
||
## 自定义组件
|
||
|
||
### ==组件封装==
|
||
|
||
`Hello.vue`
|
||
|
||
```vue
|
||
<template>
|
||
<p>{{msg}}</p>
|
||
</template>
|
||
|
||
<script>
|
||
export default {
|
||
data () {
|
||
return {
|
||
msg: 'hello'
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="less" scoped>
|
||
p {
|
||
color: red;
|
||
}
|
||
</style>
|
||
```
|
||
|
||
组件命名:
|
||
|
||
1. 短横线命名法 "my-component-name", 使用 `<my-component-name>`
|
||
2. 大驼峰命名法 "MyCoomponentName", 在模板中使用 `<my-component-name>` 和 `<MyComponentName>` 都可以, 但直接在 html 中使用必须使用短横线, 因为不识别大小写, 全部转化成小写
|
||
|
||
### 组件使用
|
||
|
||
#### 局部注册
|
||
|
||
> 1. 创建 vue 单文件
|
||
> 2. 导入 vue 文件
|
||
> 3. 注册组件
|
||
> 4. 使用组件
|
||
|
||
在 `Parent.vue` 中
|
||
|
||
```vue
|
||
<template>
|
||
<Hello></Hello>
|
||
</template>
|
||
|
||
<script>
|
||
import Hello from './components/Hello.vue'
|
||
export default {
|
||
// ...
|
||
components: {
|
||
Hello
|
||
}
|
||
// ...
|
||
}
|
||
</script>
|
||
|
||
<style>
|
||
</style>
|
||
```
|
||
|
||
#### 全局注册
|
||
|
||
```js
|
||
import XXX from '@/components/test'
|
||
Vue.component('abc', XXX)
|
||
```
|
||
|
||
示例
|
||
|
||
方法一: 在入口 `main.js` 注册
|
||
|
||
```js
|
||
//Vue.component() 方法传入两个参数, 1.组件名, 2.组件对象(先引入)
|
||
import PageTools from '@/components/PageTools'
|
||
Vue.component('PageTools', PageTools)
|
||
```
|
||
|
||
方法二: 批量注册(像element-ui那样), 自己设计的第三方包
|
||
|
||
```js
|
||
// Vue.use注册第三方包, 第三方包里写多个Vue.component()
|
||
// 1. src/components/index.js
|
||
import PageTools from '@/components/PageTools'
|
||
// import ...
|
||
// import ...
|
||
export default {
|
||
// 固定写法: install方法, 在里面写逻辑, 自动接受一个形参, 就是Vue包
|
||
install(Vue) {
|
||
Vue.component('PageTools', PageTools)
|
||
// Vue.component()...
|
||
// Vue.component()...
|
||
}
|
||
}
|
||
|
||
// 2. src/main.js
|
||
import Component from '@/components'
|
||
Vue.use(Component)
|
||
```
|
||
|
||
### ==组件传值==
|
||
|
||
#### 父传子
|
||
|
||
> 父组件通过 属性传递,子组件通过 `props` 接收
|
||
|
||
**props**
|
||
|
||
```js
|
||
// 简单语法,不做类型检查
|
||
Vue.component('props-demo-simple', {
|
||
props: ['size', 'myMessage']
|
||
})
|
||
|
||
// 对象语法,提供验证
|
||
Vue.component('props-demo-advanced', {
|
||
props: {
|
||
// 检测类型
|
||
height: Number,
|
||
rules: RegExp, // 正则
|
||
// 检测类型 + 其他验证
|
||
age: {
|
||
type: Number,
|
||
default: 0,
|
||
required: true,
|
||
validator: function (value) {
|
||
return value >= 0
|
||
}
|
||
}
|
||
}
|
||
})
|
||
```
|
||
|
||
this.$attrs: 父组件的属性默认会传到子组件的根标签上
|
||
|
||
#### 子传父
|
||
|
||
通过this发出的事件只能由父组件监听到 `this.$emit(事件名, 参数)`
|
||
|
||
* 子组件向父组件传对象数据的时候
|
||
* 如果对象里只有一个数据
|
||
|
||
```js
|
||
//Object.keys(obj)能拿到对象的键, 以数组的形式
|
||
//Object.values(obj)能拿到对象的值
|
||
//console.log(Object.keys(data)[0])
|
||
```
|
||
|
||
* 如果有多个数据`//用for in 遍历`
|
||
|
||
#### 兄弟互传(除了父子都当成兄弟处理)
|
||
|
||
> 1. 创建事件总线(new Vue), 传递和监听事件
|
||
> 2. 子组件(发送方)引入事件总线, 向总线传递事件名和数据
|
||
> 3. 兄弟组件(接收方)引入事件总线, 用钩子函数监听总线里的对应事件名, 把接收的参数赋值给自己的实例
|
||
|
||
示例
|
||
|
||
```js
|
||
/** mybus.js
|
||
1.单文件组件是组件实例
|
||
2.组件实例是可复用的vue实例
|
||
3.意味着使用this可以做的事情,vue实例都能做到
|
||
4.所以创建一个全局的vue实例,来进行事件的发出和监听
|
||
*/
|
||
//事件总线, 用来传递和监听事件
|
||
import Vue from 'vue'
|
||
export default new Vue()
|
||
```
|
||
|
||
```vue
|
||
/**子组件
|
||
*/
|
||
<template>
|
||
<div class="son">
|
||
<h1>子组件</h1>
|
||
<span>跟我的兄弟说:"{{saytobrother}}"</span><button type="button" @click="tellmybrother">say</button>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
//引入事件总线
|
||
import bus from '@/utils/mybus.js'
|
||
export default {
|
||
data () {
|
||
return {
|
||
saytobrother: '你是头猪'
|
||
}
|
||
},
|
||
|
||
methods: {
|
||
//向总线发出事件, 传递数据
|
||
tellmybrother () {
|
||
//第一个参数, 事件名, 第二个参数, 传递的数据
|
||
bus.$emit('brothersay', this.saytobrother)
|
||
}
|
||
}
|
||
|
||
};
|
||
</script>
|
||
|
||
<style lang="less" scoped>
|
||
.son {
|
||
width: 400px;
|
||
background-color: skyblue;
|
||
}
|
||
</style>
|
||
```
|
||
|
||
```vue
|
||
/**兄弟组件
|
||
*/
|
||
<template>
|
||
<div class="brother">
|
||
<h1>子组件的兄弟组件</h1>
|
||
<span>我的兄弟跟我说:"{{mybrothersay}}"</span>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
//引入事件总线
|
||
import bus from '@/utils/mybus.js'
|
||
export default {
|
||
data () {
|
||
return {
|
||
mybrothersay: ''
|
||
}
|
||
},
|
||
// 监听
|
||
created () {
|
||
bus.$on('brothersay', (data) => {
|
||
this.mybrothersay = data
|
||
})
|
||
},
|
||
}
|
||
</script>
|
||
|
||
<style lang="less" scoped>
|
||
.brother {
|
||
width: 500px;
|
||
background-color: orange;
|
||
}
|
||
</style>
|
||
```
|
||
|
||
```js
|
||
/**父组件
|
||
*/
|
||
<template>
|
||
<div class="father">
|
||
<h1>父组件</h1>
|
||
<son></son>
|
||
<brother></brother>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
// 引入
|
||
import son from './子组件.vue'
|
||
import brother from './兄弟组件'
|
||
export default {
|
||
// 注册
|
||
components: {
|
||
son, brother
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="less" scoped>
|
||
.father {
|
||
background-color: pink;
|
||
}
|
||
</style>
|
||
```
|
||
|
||
#### $refs $parent
|
||
|
||
* `this.$refs`: 父传子: 1. 子组件 ref="xxx" 2. this.$refs.xxx.子组件中的方法或data
|
||
* `this.$parent`: 子传父: this.$parent.父组件的方法或data 缺点: 父组件不明确, 不推荐使用
|
||
|
||
### 依赖注入 provide inject
|
||
|
||
> 不仅仅是儿子, 所有后代都能使用
|
||
|
||
1. 父组件在 provide 函数中返回一个对象
|
||
|
||
```vue
|
||
<!-- ParentComponent.vue -->
|
||
<template>
|
||
<div>
|
||
<h1>父组件</h1>
|
||
<ChildComponent />
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import ChildComponent from './ChildComponent.vue';
|
||
|
||
export default {
|
||
name: 'ParentComponent',
|
||
components: { ChildComponent },
|
||
provide() {
|
||
return {
|
||
message: '这是从父组件传递的数据',
|
||
};
|
||
},
|
||
};
|
||
</script>
|
||
|
||
```
|
||
|
||
2. 子组件通过 inject 数组接收
|
||
|
||
```vue
|
||
<!-- ChildComponent.vue -->
|
||
<template>
|
||
<div>
|
||
<h2>子组件</h2>
|
||
<p>接收到的数据: {{ message }}</p>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
export default {
|
||
name: 'ChildComponent',
|
||
inject: ['message'],
|
||
};
|
||
</script>
|
||
```
|
||
|
||
* inject 除了可以是数组, 还能是对象, 方便设置别名或设置默认值
|
||
|
||
```vue
|
||
<!-- ChildComponent.vue -->
|
||
<template>
|
||
<div>
|
||
<h2>子组件</h2>
|
||
<p>默认注入: {{ injectedMessage }}</p>
|
||
<p>别名注入: {{ aliasMessage }}</p>
|
||
<p>默认值注入: {{ defaultMessage }}</p>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
export default {
|
||
name: 'ChildComponent',
|
||
inject: {
|
||
// 简单映射
|
||
injectedMessage: 'message',
|
||
// 使用别名
|
||
aliasMessage: 'anotherMessage',
|
||
// 设置默认值
|
||
defaultMessage: {
|
||
from: 'nonExistMessage',
|
||
default: '这是默认值', // 父组件为未供时设置默认值
|
||
},
|
||
},
|
||
};
|
||
</script>
|
||
|
||
```
|
||
|
||
## 内置的组件
|
||
|
||
### component
|
||
|
||
> 和 `:is` 一起用来渲染动态组件
|
||
|
||
```vue
|
||
<component :is="view"></component>
|
||
```
|
||
|
||
### transition
|
||
|
||
> 内置组件, 给元素或组件添加进入/离开动画
|
||
> v-if、v-show、动态组件、根组件
|
||
|
||
```vue
|
||
<!-- 简单元素 -->
|
||
<transition>
|
||
<div v-if="ok">toggled content</div>
|
||
</transition>
|
||
|
||
<!-- 动态组件 -->
|
||
<transition name="fade" mode="out-in" appear>
|
||
<component :is="view"></component>
|
||
</transition>
|
||
```
|
||
|
||
### keep-alive
|
||
|
||
> 缓存组件的状态
|
||
|
||
```vue
|
||
<!-- 数组 (使用 `v-bind`) -->
|
||
<keep-alive :include="['a', 'b']">
|
||
<component :is="view"></component>
|
||
</keep-alive>
|
||
```
|
||
|
||
* **Props**:
|
||
* `include` - 字符串或正则表达式或数组。只有名称匹配的组件会被缓存。
|
||
* `exclude` - 字符串或正则表达式或数组。任何名称匹配的组件都不会被缓存。
|
||
* `max` - 数字。最多可以缓存多少组件实例。
|
||
* **用法**:
|
||
* `<keep-alive>` 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。
|
||
|
||
注意点:
|
||
|
||
```vue
|
||
<div>
|
||
<keep-alive>
|
||
<router-view v-if="keepAlive"></router-view>
|
||
</keep-alive>
|
||
<router-view v-if="!keepAlive"></router-view>
|
||
</div>
|
||
```
|
||
|
||
1. 直接加上keep-alive的话,会把所有的 router-view 层级下的视图的组件都缓存
|
||
2. 只想缓存部分,不想缓存所有的, 需要使用 `include` , `exclude`
|
||
3. 需要被缓存的组件必须要有名字
|
||
|
||
**组件的名称与include的值相同的才会被缓存**
|
||
|
||
```js
|
||
export default {
|
||
name:XXX // 这里的name
|
||
}
|
||
```
|
||
|
||
* keep-alive的组件以后,组件上就会自动加上了 ``activated`钩子和`deactivated`钩子
|
||
|
||
```text
|
||
初始进入和离开 created ---> mounted ---> activated --> deactivated
|
||
后续进入和离开 activated --> deactivated
|
||
```
|
||
|
||
## vm.$nextTick ()
|
||
|
||
DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
|
||
|
||
(数据更改后不会马上更新视图,而是进入一个队列, 把最终的结果去重再更新视图)
|
||
|
||
```js
|
||
// 修改数据
|
||
vm.msg = 'Hello'
|
||
// DOM 还没有更新
|
||
Vue.nextTick(function () {
|
||
// DOM 更新了
|
||
})
|
||
|
||
// 作为一个 Promise 使用 (2.1.0 起新增)
|
||
Vue.nextTick()
|
||
.then(function () {
|
||
// DOM 更新了
|
||
})
|
||
```
|
||
|
||
## vm.$forceUpdate ()
|
||
|
||
> 强制实例重新渲染
|
||
|
||
## vm.$set()
|
||
|
||
```js
|
||
vm.$set( target, propertyName/index, value )
|
||
// 第一个参数 添加的位置
|
||
// 第二个参数, 键
|
||
// 第三个参数, 值
|
||
```
|
||
|
||
如果在实例创建之后添加新的属性到实例上,它不会触发视图更新。
|
||
|
||
```js
|
||
data () {
|
||
return {
|
||
student: {
|
||
name: '',
|
||
sex: ''
|
||
}
|
||
}
|
||
}
|
||
mounted () {
|
||
this.student.age = 24
|
||
}
|
||
```
|
||
|
||
受 ES5 的限制,Vue.js 不能检测到对象属性的添加或删除。因为 Vue.js 在初始化实例时将属性转为 getter/setter,所以属性必须在 data 对象上才能让 Vue.js 转换它,才能让它是响应的。
|
||
|
||
正确写法:`this.$set(this.data,”key”,value')`
|
||
|
||
* 在vue的内部针对于数组中对象是会通过==Object.defineProperty==来对所有的属性进行劫持,来完成响应式的,所以数组中对象元素都是响应式的,这里并没有通过数组下标去改变值,而是获取相应的对象,而这个对象是响应式的。
|
||
* https://vuejs.bootcss.com/guide/reactivity.html 深入响应式原理
|
||
|
||
## vm.$delete()
|
||
|
||
删除对象的 property。如果对象是响应式的,确保删除能触发更新视图。这个方法主要用于避开 Vue 不能检测到 property 被删除的限制
|
||
|
||
## Vue.use()
|
||
|
||
安装插件
|
||
|
||
```js
|
||
// 调用 `MyPlugin.install(Vue)`
|
||
Vue.use(MyPlugin)
|
||
|
||
new Vue({
|
||
// ...组件选项
|
||
})
|
||
```
|
||
|
||
### 开发插件
|
||
|
||
Vue.js 的插件应该暴露一个 `install` 方法。这个方法的第一个参数是 `Vue` 构造器,第二个参数是一个可选的选项对象:
|
||
|
||
```js
|
||
MyPlugin.install = function (Vue, options) {
|
||
// 1. 添加全局方法或 property
|
||
Vue.myGlobalMethod = function () {
|
||
// 逻辑...
|
||
}
|
||
|
||
// 2. 添加全局资源
|
||
Vue.directive('my-directive', {
|
||
bind (el, binding, vnode, oldVnode) {
|
||
// 逻辑...
|
||
}
|
||
...
|
||
})
|
||
|
||
// 3. 注入组件选项
|
||
Vue.mixin({
|
||
created: function () {
|
||
// 逻辑...
|
||
}
|
||
...
|
||
})
|
||
|
||
// 4. 添加实例方法
|
||
Vue.prototype.$myMethod = function (methodOptions) {
|
||
// 逻辑...
|
||
}
|
||
}
|
||
```
|
||
|
||
## Vue.mixin
|
||
|
||
混入: 是一种分发 Vue 组件中可复用功能的非常灵活的方式。混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被混入该组件本身的选项
|
||
|
||
1. 方法和参数在各组件中不共享
|
||
2. 值为对象的选项,如 methods, components 等,选项会被**递归合并**,发生冲突则使用组件的
|
||
3. 值为函数的选项,如 created, mounted 等,就会被**合并调用**,组件的函数后调用
|
||
|
||
**避免使用**
|
||
|
||
```js
|
||
// mixin.js
|
||
export default {
|
||
data() {
|
||
return {
|
||
message: "Hello from mixin!"
|
||
};
|
||
},
|
||
methods: {
|
||
showMessage() {
|
||
alert(this.message);
|
||
}
|
||
}
|
||
};
|
||
```
|
||
|
||
```vue
|
||
<template>
|
||
<div>
|
||
<p>{{ message }}</p>
|
||
<button @click="showMessage">点击显示消息</button>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import myMixin from "./mixin";
|
||
|
||
export default {
|
||
mixins: [myMixin] // 使用 mixin
|
||
};
|
||
</script>
|
||
```
|
||
|
||
## Vue.version
|
||
|
||
提供字符串形式的 Vue 安装版本号。这对社区的插件和组件来说非常有用,你可以根据不同的版本号采取不同的策略。
|
||
|
||
```js
|
||
var version = Number(Vue.version.split('.')[0])
|
||
|
||
if (version === 2) {
|
||
// Vue v2.x.x
|
||
} else if (version === 1) {
|
||
// Vue v1.x.x
|
||
} else {
|
||
// Unsupported versions of Vue
|
||
}
|
||
```
|
||
|
||
## Vue.observable
|
||
|
||
让一个对象可响应。Vue 内部会用它来处理 `data` 函数返回的对象。
|
||
|
||
返回的对象可以直接用于渲染函数和计算属性,并且会在发生变更时触发相应的更新。也可以作为最小化的跨组件状态存储器,用于简单的场景:
|
||
|
||
```js
|
||
const state = Vue.observable({ count: 0 })
|
||
|
||
const Demo = {
|
||
render(h) {
|
||
return h('button', {
|
||
on: { click: () => { state.count++ }}
|
||
}, `count is: ${state.count}`)
|
||
}
|
||
}
|
||
```
|
||
|
||
## 路由
|
||
|
||
> 路由创建
|
||
> * 导入 `vue-router`
|
||
> * 安装 `VueRouter `
|
||
> * 创建 `VueRouter` 实例,为了方便维护,一般会把路由配置表抽离出去用一个变量存起来。
|
||
> * 导出 `VueRouter` 实例,在 `main.js` 入口文件中使用。
|
||
> * PS:我们日常开发其实只需要关心 `路由配置表` 的配置关系即可。
|
||
|
||
## 在原型上添加变量
|
||
|
||
```js
|
||
//src\utils\baseURL.js
|
||
// 导入 Vue
|
||
import Vue from 'vue'
|
||
// API 服务器的地址
|
||
export const baseURL = `http://127.0.0.1:3000`
|
||
|
||
// 把 baseURL 挂载到 Vue 的原型上,好处:所有的 Vue 组件都能共享该变量
|
||
Vue.prototype.$baseURL = baseURL
|
||
```
|
||
|
||
* 所有组件都能共享变量。
|
||
* 组件的 template 模板中直接通过变量名即可访问 如: `$baseURL`
|
||
* 组件的 script 代码中,可通过 `this.$baseURL` 访问到变量。
|
||
|
||
对于没有导入Vue组件的api, 要使用baseURL还是得先导入baseURL
|
||
|
||
## 生产环境配置dev-tool
|
||
|
||
```js
|
||
Vue.config.devtools = true
|
||
```
|
||
|
||
开发版本默认为 `true`,生产版本默认为 `false`。生产版本设为 `true` 可以启用vue-devtools 。
|
||
|
||
## render 渲染函数
|
||
|
||
> render 函数不方便阅读, 尽量使用 jsx
|
||
|
||
https://v2.cn.vuejs.org/v2/guide/render-function.html
|
||
|
||
### 节点、树、虚拟 DOM
|
||
|
||
这是 html 内容
|
||
|
||
```html
|
||
<div>
|
||
<h1>My title</h1>
|
||
Some text content
|
||
<!-- TODO: Add tagline -->
|
||
</div>
|
||
```
|
||
|
||
在浏览器中, 会创建这样的节点树来追踪内容
|
||
|
||

|
||
|
||
在 vue 模板中
|
||
|
||
```vue
|
||
<h1>{{ blogTitle }}</h1>
|
||
```
|
||
|
||
对应的渲染函数
|
||
|
||
```js
|
||
<script>
|
||
export default {
|
||
render: function (createElement) {
|
||
return createElement('h1', this.blogTitle)
|
||
}
|
||
}
|
||
</script>
|
||
```
|
||
|
||
* createElement 返回的是虚拟节点 **VNode**, 描述的是 vue 该如何渲染这个节点的信息, 多个虚拟节点构成的就是虚拟 DOM
|
||
* createElement 通常简写成 h, 上面的渲染函数可以简写成
|
||
|
||
```js
|
||
<script>
|
||
export default {
|
||
render(h) {
|
||
return h('h1', this.blogTitle)
|
||
}
|
||
}
|
||
</script>
|
||
```
|
||
|
||
### createElement 的参数
|
||
|
||
```js
|
||
createElement(
|
||
tag, // {String | Object | Function}
|
||
data, // {Object}
|
||
children // {String | Array | Function}
|
||
)
|
||
```
|
||
|
||
* tag: 可以是标签, 组件, 或者返回标签/组件的函数
|
||
* data (可选): Vnote 数据对象, 类似 v-bind , 可以设置 class、style、attrs、props、on 等
|
||
|
||
```js
|
||
h('div', {
|
||
class: 'my-class',
|
||
style: { color: 'red' },
|
||
attrs: { id: 'app' },
|
||
props: { value: 'Hello' },
|
||
on: { click: () => alert('Clicked!') }
|
||
}, '内容')
|
||
```
|
||
|
||
* children (可选): 子节点, 和 tag 一样, 可以是标签, 组件, 或者返回标签/组件的函数
|
||
|
||
```js
|
||
h('div', [
|
||
h('p', '我是段落'),
|
||
h('button', { on: { click: () => alert('按钮点击') } }, '点击我')
|
||
])
|
||
|
||
// 第二个参数 data 不需要设置, 自动把第二个参数识别成 children
|
||
// 多个子节点用数组包裹
|
||
// 只有一个子节点直接传
|
||
```
|
||
|
||
### Vnote 不可复用
|
||
|
||
> Vue 基于 Vnote 进行 Diff 算法, 复用会导致无法正常追踪和更新
|
||
|
||
```js
|
||
render(h) {
|
||
var myParagraphVNode = createElement('p', 'hi')
|
||
return h('div', [
|
||
// 错误 - 重复的 VNode
|
||
myParagraphVNode, myParagraphVNode
|
||
])
|
||
}
|
||
```
|
||
|
||
### v-if v-for 代替
|
||
|
||
* v-if 用 if else 代替
|
||
* v-for 用 map 代替
|
||
|
||
```js
|
||
props: ['items'],
|
||
render(h) {
|
||
if (this.items.length) {
|
||
return h('ul', this.items.map( item => {
|
||
return h('li', item.name)
|
||
}))
|
||
} else {
|
||
return h('p', 'No items found.')
|
||
}
|
||
}
|
||
```
|
||
|
||
### v-model
|
||
|
||
需要自己实现
|
||
|
||
* 子组件
|
||
|
||
```js
|
||
export default {
|
||
props: ['value'],
|
||
render(h) {
|
||
var self = this
|
||
return h('input', {
|
||
domProps: {
|
||
value: self.value
|
||
},
|
||
on: {
|
||
input: function (event) {
|
||
self.$emit('input', event.target.value)
|
||
}
|
||
}
|
||
})
|
||
}
|
||
};
|
||
```
|
||
|
||
domProps 是原生 html 的属性, props 是 vue 组件的属性
|
||
|
||
* 父组件
|
||
|
||
```js
|
||
export default {
|
||
data() {
|
||
return {
|
||
message: ''
|
||
};
|
||
},
|
||
render(h) {
|
||
return h(CustomInput, {
|
||
props: {
|
||
value: this.message
|
||
},
|
||
on: {
|
||
input: (val) => {
|
||
this.message = val;
|
||
}
|
||
}
|
||
});
|
||
}
|
||
};
|
||
```
|
||
|
||
### 修饰符
|
||
|
||
| 事件修饰符 | 前缀 |
|
||
| -------------------------------------- | ---- |
|
||
| `.passive` | `&` |
|
||
| `.capture` | `!` |
|
||
| `.once` | `~` |
|
||
| `.capture.once` 或 <br>`.once.capture` | `~!` |
|
||
|
||
例子
|
||
|
||
```js
|
||
on: {
|
||
'!click': this.doThisInCapturingMode,
|
||
'~keyup': this.doThisOnce,
|
||
'~!mouseover': this.doThisOnceInCapturingMode
|
||
}
|
||
```
|
||
|
||
| 修饰符 | 处理函数中的等价操作 |
|
||
| -------------------------------------------- | ----------------------------------------------------------------------------------------- |
|
||
| `.stop` | `event.stopPropagation()` |
|
||
| `.prevent` | `event.preventDefault()` |
|
||
| `.self` | `if (event.target !== event.currentTarget) return` |
|
||
| 按键: <br>`.enter`, `.13` | `if (event.keyCode !== 13) return` (对于别的按键修饰符来说,可将 `13` 改为[另一个按键码](http://keycode.info/)) |
|
||
| 修饰键: <br>`.ctrl`, `.alt`, `.shift`, `.meta` | `if (!event.ctrlKey) return` (将 `ctrlKey` 分别修改为 `altKey`、`shiftKey` 或者 `metaKey`) |
|
||
|
||
### 插槽
|
||
|
||
#### 静态插槽
|
||
|
||
> `this.$slots.插槽名` 是一个 Vnote 数组
|
||
|
||
```js
|
||
render(h) {
|
||
// `<div><slot></slot></div>`
|
||
return h('div', this.$slots.default)
|
||
}
|
||
```
|
||
|
||
#### 作用域插槽
|
||
|
||
> `this.$scopedSlots.插槽名(参数对象)` 一个函数, 返回 Vnote 数组
|
||
|
||
子组件:
|
||
|
||
```js
|
||
props: ['message'],
|
||
render(h) {
|
||
// `<div><slot :text="message"></slot></div>`
|
||
return h('div', [
|
||
this.$scopedSlots.default({
|
||
text: this.message
|
||
})
|
||
])
|
||
```
|
||
|
||
在父组件中设置模板
|
||
|
||
数据对象 `scopedSlots` 的 key 是插槽名称, 值一个返回 Vnote 的函数, 接收的参数是 slotProps
|
||
|
||
```js
|
||
render (h) {
|
||
// `<div><child v-slot="props"><span>{{ props.text }}</span></child></div>`
|
||
return h('div', [
|
||
h('child', {
|
||
// 在数据对象中传递 `scopedSlots`
|
||
// 格式为 { name: props => VNode | Array<VNode> }
|
||
scopedSlots: {
|
||
default: function (props) {
|
||
return h('span', props.text)
|
||
}
|
||
}
|
||
})
|
||
])
|
||
}
|
||
```
|
||
|
||
## jsx
|
||
|
||
> jsx 最终也是解析成 render 函数
|
||
|
||
### 安装 babel 插件
|
||
|
||
https://github.com/vuejs/jsx-vue2
|
||
|
||
```sh
|
||
npm install @vue/babel-preset-jsx @vue/babel-helper-vue-jsx-merge-props
|
||
```
|
||
|
||
`babel.config.js` 添加
|
||
|
||
```js
|
||
module.exports = {
|
||
presets: ['@vue/babel-preset-jsx'],
|
||
}
|
||
```
|
||
|
||
### 使用
|
||
|
||
```js
|
||
render() {
|
||
return <p>hello</p>
|
||
}
|
||
```
|
||
|
||
#### 响应数据
|
||
|
||
```js
|
||
render() {
|
||
return <p>hello { this.message }</p>
|
||
}
|
||
```
|
||
|
||
#### 组件
|
||
|
||
```js
|
||
import MyComponent from './my-component'
|
||
|
||
export default {
|
||
render() {
|
||
return <MyComponent>hello</MyComponent>
|
||
},
|
||
}
|
||
```
|
||
|
||
#### 添加属性
|
||
|
||
```js
|
||
render() {
|
||
return <input type="email" />
|
||
}
|
||
```
|
||
|
||
* 动态属性
|
||
|
||
```js
|
||
render() {
|
||
return <input
|
||
type="email"
|
||
placeholder={this.placeholderText}
|
||
/>
|
||
}
|
||
```
|
||
|
||
* 展开运算符
|
||
|
||
```js
|
||
render() {
|
||
const inputAttrs = {
|
||
type: 'email',
|
||
placeholder: 'Enter your email'
|
||
}
|
||
|
||
return <input {...{ attrs: inputAttrs }} />
|
||
}
|
||
```
|
||
|
||
#### 插槽
|
||
|
||
* 具名插槽
|
||
|
||
```js
|
||
render() {
|
||
return (
|
||
<MyComponent>
|
||
<header slot="header">header</header>
|
||
<footer slot="footer">footer</footer>
|
||
</MyComponent>
|
||
)
|
||
}
|
||
```
|
||
|
||
* 作用域插槽
|
||
|
||
```js
|
||
render() {
|
||
const scopedSlots = {
|
||
header: () => <header>header</header>,
|
||
footer: () => <footer>footer</footer>
|
||
}
|
||
|
||
return <MyComponent scopedSlots={scopedSlots} />
|
||
}
|
||
```
|
||
|
||
#### 指令
|
||
|
||
```js
|
||
<input vModel={this.newTodoText} />
|
||
```
|
||
|
||
* 修饰符
|
||
|
||
```js
|
||
<input vModel_trim={this.newTodoText} />
|
||
```
|
||
|
||
* 监听事件
|
||
|
||
```js
|
||
<input vOn:click={this.newTodoText} />
|
||
```
|
||
|
||
* 事件修饰符
|
||
|
||
```js
|
||
<input vOn:click_stop_prevent={this.newTodoText} />
|
||
```
|
||
|
||
* v-html
|
||
|
||
```js
|
||
<p domPropsInnerHTML={html} />
|
||
```
|
||
|
||
#### 函数式组件
|
||
|
||
* 默认导出
|
||
|
||
```js
|
||
export default ({ props }) => <p>hello {props.message}</p>
|
||
```
|
||
|
||
* 声明为变量
|
||
|
||
```js
|
||
const HelloWorld = ({ props }) => <p>hello {props.message}</p>
|
||
```
|
||
|
||
## 函数式组件
|
||
|
||
> 没有 `响应式数据 data` , 也没有 `this 上下文`, 只依赖 `props` 渲染 (也没有 methods computed 等)
|
||
> 主要用于性能优化,因为它们没有响应式系统的开销,渲染速度更快
|
||
|
||
1. 添加 `functional: true` 选项
|
||
2. `render` 增加第二个参数, ` context `
|
||
|
||
```js
|
||
export default {
|
||
functional: true,
|
||
props: { // props 是可选的
|
||
// ...
|
||
},
|
||
render(h, context) {
|
||
return h('div', 'Hello, Vue!')
|
||
}
|
||
};
|
||
```
|
||
|
||
* `props` 是可选的, 省略 `props` 选项,所有组件上的 attribute 都会被自动隐式解析为 prop
|
||
* 返回的不是 Vnote, 而是 HTMLElement
|
||
* 如果在组件上绑定 `ref=xxx`, 那么 `this.$refs. xxx` 是 HTMLElement
|
||
* 也可以在 template 标签加 functional, 选项里的去掉
|
||
|
||
```vue
|
||
<template functional>
|
||
</template>
|
||
```
|
||
|
||
### context
|
||
|
||
组件需要的一切都是通过 `context` 参数传递,它是一个包括如下字段的对象:
|
||
|
||
* `props`:提供所有 prop 的对象
|
||
* `children`:VNode 子节点的数组
|
||
* `slots`:一个函数,返回了包含所有插槽的对象
|
||
* `scopedSlots`:一个暴露传入的作用域插槽的对象。也以函数形式暴露普通插槽。
|
||
* `data`:传递给组件的整个数据对象,包含组件的 `attrs`、`class`、`style` 等信息, 作为 `createElement` 的第二个参数传入组件
|
||
* `parent`:对父组件的引用
|
||
* `listeners`:一个包含了所有父组件为当前组件注册的事件监听器的对象。这是 `data.on` 的一个别名。
|
||
* `injections`:如果使用了 `inject` 选项,则该对象包含了应当被注入的 property。
|
||
|
||
```vue
|
||
<template functional>
|
||
<!-- 函数式组件不需要 template 内容 -->
|
||
</template>
|
||
|
||
<script>
|
||
export default {
|
||
name: 'PracticalFunctionalDemo',
|
||
functional: true,
|
||
props: {
|
||
title: String
|
||
},
|
||
inject: ['appTheme'],
|
||
|
||
render(h, {
|
||
props,
|
||
children,
|
||
slots,
|
||
scopedSlots,
|
||
data,
|
||
parent,
|
||
listeners,
|
||
injections
|
||
}) {
|
||
return h('div', {
|
||
class: ['functional-box', data.class],
|
||
style: {
|
||
...data.style,
|
||
border: `1px solid ${injections.appTheme === 'dark' ? '#333' : '#eee'}`
|
||
},
|
||
attrs: {
|
||
...data.attrs,
|
||
'data-functional': 'true'
|
||
},
|
||
on: {
|
||
...listeners,
|
||
mouseover: () => console.log('鼠标移入')
|
||
}
|
||
}, [
|
||
// 使用 props
|
||
h('h3', props.title),
|
||
|
||
// 使用 children
|
||
children.length > 0 ? h('div', { class: 'content' }, children) : null,
|
||
|
||
// 使用普通插槽
|
||
slots().default || h('p', '默认内容'),
|
||
|
||
// 使用作用域插槽
|
||
scopedSlots.footer ? scopedSlots.footer({
|
||
parentName: parent.$options.name
|
||
}) : null,
|
||
|
||
// 显示注入的值
|
||
h('p', `当前主题: ${injections.appTheme}`)
|
||
])
|
||
}
|
||
}
|
||
</script>
|
||
```
|
||
|
||
JSX 版本
|
||
|
||
```jsx
|
||
// PracticalFunctionalDemo.jsx
|
||
export default {
|
||
name: 'PracticalFunctionalDemo',
|
||
functional: true,
|
||
props: {
|
||
title: String
|
||
},
|
||
inject: ['appTheme'],
|
||
|
||
render(h, context) {
|
||
const {
|
||
props,
|
||
children,
|
||
slots,
|
||
scopedSlots,
|
||
data,
|
||
parent,
|
||
listeners,
|
||
injections
|
||
} = context
|
||
|
||
// 合并事件(原生事件 + 父组件传递的事件)
|
||
const mergedListeners = {
|
||
...listeners,
|
||
mouseover: () => console.log('鼠标移入')
|
||
}
|
||
|
||
return (
|
||
<div
|
||
class={['functional-box', data.class]}
|
||
style={{
|
||
...data.style,
|
||
border: `1px solid ${injections.appTheme === 'dark' ? '#333' : '#eee'}`
|
||
}}
|
||
{...{ attrs: data.attrs }}
|
||
on={mergedListeners}
|
||
>
|
||
{/* 使用 props */}
|
||
<h3>{props.title}</h3>
|
||
|
||
{/* 使用 children */}
|
||
{children.length > 0 && (
|
||
<div class="content">{children}</div>
|
||
)}
|
||
|
||
{/* 使用普通插槽 */}
|
||
{slots().default || <p>默认内容</p>}
|
||
|
||
{/* 使用作用域插槽 */}
|
||
{scopedSlots.footer && scopedSlots.footer({
|
||
parentName: parent.$options.name
|
||
})}
|
||
|
||
{/* 显示注入的值 */}
|
||
<p>当前主题: {injections.appTheme}</p>
|
||
</div>
|
||
)
|
||
}
|
||
}
|
||
```
|
||
|
||
## css 深度作用选择器
|
||
|
||
> 希望 `scoped` 样式中的一个选择器能够作用得"更深",例如影响子组件
|
||
|
||
https://www.cnblogs.com/CyLee/p/10006065.html
|
||
|
||
https://vue-loader-v14.vuejs.org/zh-cn/features/scoped-css.html
|
||
|
||
```scss
|
||
<style lang="scss" scoped>
|
||
.select {
|
||
width: 100px;
|
||
|
||
/deep/ .el-input__inner {
|
||
border: 0;
|
||
color: #000;
|
||
}
|
||
}
|
||
</style>
|
||
```
|
||
|
||
## 响应式原理
|
||
|
||
当你把一个普通的 JavaScript 对象传入 Vue 实例作为 `data` 选项,Vue 将遍历此对象所有的 property,并使用 [`Object.defineProperty`](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty) 把这些 property 全部转为 [getter/setter](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Working_with_Objects#%E5%AE%9A%E4%B9%89_getters_%E4%B8%8E_setters)。
|
||
|
||
这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在 property 被访问和修改时通知变更。
|
||
|
||
每个组件实例都对应一个 **watcher** 实例,它会在组件渲染的过程中把"接触"过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。
|
||
|
||

|
||
|
||
### 数组的增删
|
||
|
||
`push() pop() shift() unshift() splice() sort() reverse()` 这类会改变原数据的方法, 都进行了包裹, 它们也会触发视图更新
|
||
|
||
`filter() concat() slice()` 这类返回新数组的方法, 则需要使用新数组代替旧数组
|
||
|
||
通过 `this.array[index] = newValue` 不能触发视图更新, 可以通过 ` this.array.splice(index, 1, newValue) `
|
||
|
||
或者使用 `this.$set(this.array, index, newValue)`
|
||
|
||
### 对象属性的增删
|
||
|
||
对象属性的添加和删除无法触发更新, 因为初始化时就为对象的属性添加了 setter/getter 转化
|
||
|
||
* 添加属性
|
||
|
||
如果要为对象添加响应式的属性, 可以使用 `this.$set(this.obj, key , newValue)`
|
||
|
||
直接使用 `Object.assign()` 也无法添加相应式属性, 应该把混合后的对象赋值给原来的对象 (使用解构会丢失响应性)
|
||
|
||
`this.obj=Object.assign({}, this.obj, {a:1, b:2})`
|
||
|
||
* 删除属性
|
||
`this.$delete(this.obj, key)`
|
||
|
||
### data 声明后不允许新增
|
||
|
||
必须在初始化实例前声明所有根级响应式 property,哪怕只是一个空值, 不允许动态添加
|
||
|
||
```vue
|
||
<template>
|
||
<div>{{ message }}</div>
|
||
</template>
|
||
|
||
<script>
|
||
export default {
|
||
data() {
|
||
return {
|
||
// 声明 message 为一个空值字符串
|
||
message: ''
|
||
}
|
||
},
|
||
// 可以在 mounted 钩子中设置初始值
|
||
mounted() {
|
||
this.message = 'Hello!'
|
||
}
|
||
}
|
||
</script>
|
||
```
|
||
|
||
### 异步更新队列
|
||
|
||
Vue 在更新 DOM 时是**异步**执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环"tick"中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 `Promise.then`、`MutationObserver` 和 `setImmediate`,如果执行环境不支持,则会采用 `setTimeout(fn, 0)` 代替。
|
||
|
||
```js
|
||
this.message = '第一次更新'
|
||
this.$nextTick(()=>console.log(this.message))
|
||
this.message = '第二次更新'
|
||
this.message = '第三次更新'
|
||
|
||
// DOM只会更新一次,显示"第三次更新"
|
||
```
|
||
|
||
在数据变化之后立即使用 `vm.$nextTick(callback)`。这样回调函数将在 DOM 更新完成后被调用。
|
||
|
||
1. **同步代码执行阶段**:
|
||
* `this.message = '第一次更新'`:触发响应式更新,将 watcher 加入队列
|
||
* `this.$nextTick(...)`:将回调函数注册到 nextTick 队列
|
||
* `this.message = '第二次更新'`:去重处理(同一个 watcher 不会重复入队)
|
||
* `this.message = '第三次更新'`:同上
|
||
2. **异步更新阶段(下一个事件循环tick)**:
|
||
* **第一步**:执行 DOM 更新(此时 `message` 的最终值是 `'第三次更新'`)
|
||
* **第二步**:执行 `nextTick` 回调函数
|
||
3. 控制台输出结果: `'第三次更新' `
|
||
|
||
* JS 标准的事件循环机制
|
||
|
||
```
|
||
[调用栈] → [微任务队列] → [渲染] → [宏任务队列] → [微任务队列] → ...
|
||
```
|
||
|
||
* 在 Vue 中
|
||
|
||
```txt
|
||
[调用栈]
|
||
│
|
||
├─ 同步代码执行(包括Vue数据修改 → 收集到更新队列)
|
||
│
|
||
[微任务队列](Vue在此插入两个关键点)
|
||
│
|
||
├─ [Vue前置flush] → 处理watch/computed等
|
||
│
|
||
├─ [Vue主更新] → 执行组件re-render (这就是"tick"的核心)
|
||
│
|
||
├─ [nextTick回调] → 此时DOM已更新
|
||
│
|
||
├─ 其他微任务(Promise等)
|
||
│
|
||
[渲染](如需重绘)
|
||
│
|
||
[宏任务队列]
|
||
│
|
||
├─ setTimeout/setInterval等
|
||
│
|
||
├─ 事件回调
|
||
│
|
||
[新的微任务队列](循环继续...)
|
||
```
|
||
|
||
举例:
|
||
|
||
```js
|
||
methods: {
|
||
async update() {
|
||
// ====== 阶段1:同步执行(当前调用栈/当前tick) ======
|
||
this.a = 1 // [同步] 数据变更加入当前tick的更新队列
|
||
console.log('同步代码') // [同步] 立即执行
|
||
|
||
await Promise.resolve() // [微任务边界] 后续代码包装为微任务
|
||
// ------ 此处是tick分界线 ------
|
||
|
||
// ====== 阶段2:微任务执行(下一个tick) ======
|
||
this.b = 2 // [微任务] 数据变更加入新tick的更新队列
|
||
|
||
setTimeout(() => { // [宏任务] 回调推入任务队列
|
||
// ====== 阶段3:宏任务执行(未来tick) ======
|
||
this.c = 3 // [宏任务] 触发独立更新周期
|
||
}, 0)
|
||
}
|
||
}
|
||
|
||
```
|
||
|
||
流程分析:
|
||
|
||
```
|
||
[主线程调用栈]
|
||
│
|
||
├─ 1. this.a = 1 (加入当前tick更新队列)
|
||
├─ 2. console.log (立即执行)
|
||
├─ 3. await Promise (后续代码包装为微任务)
|
||
│
|
||
[微任务阶段]
|
||
│
|
||
├─ 4. Vue DOM更新 (处理a=1的变更)
|
||
├─ 5. this.b = 2 (加入新tick更新队列)
|
||
├─ 6. 注册setTimeout (回调进入宏任务队列)
|
||
│
|
||
[宏任务阶段]
|
||
│
|
||
├─ 7. setTimeout回调执行
|
||
├─ 8. this.c = 3 (触发新的更新周期)
|
||
├─ 9. Vue DOM更新 (处理c=3的变更)
|
||
|
||
``` |