官方文档:vue 介绍
1. 创建一个 vue
官方推荐初学者先使用单个 html 引入 js 的方式来使用 vue,如创建 vue01-base.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
{{ message }}
</div>
<div id="app-2">
<span v-bind:title="message">
鼠标悬停几秒钟查看此处动态绑定的提示信息!
</span>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
var app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
}
})
</script>
</body>
</html>
例子解析:
- script 标签内:
new Vue
: 创建一个 vue 实例,实例挂载在el
指定的 DOM 元素上,data
内的为实例数据对象,关于挂载和实例数据的知识见后续笔记,初期这么使用就好; - div 内:
{{ message }}
模板插值语法,将当前 DOM 挂载的实例数据作为文本渲染在页面上。
关于模板插值语法:
- 使用双大括号包裹。
- 支持:
- 文本,html;
- JavaScript 表达式,如
{{ "abcd".split('').reverse().join('') }}
,但这些计算不适合出现在模板中。
2. 常用指令
指令
- 释义:Directives,是带有
v-
前缀的特殊 attribute; - 语法:
指令="表达式"
; - 作用:当表达式的值改变时,将其产生的连带影响,响应式地作用于 DOM。
2.1 判断 v-if
v-if
:
- 语法:
v-if="expr/value"
- 值为 Truthy 时渲染元素,除
false
、0
、""
、null
、undefined
和NaN
之外都是真值,包括空数组[]
、空对象{}
。
<p v-if="seen1">{{seen1}}</p>
<p v-if="seen2">{{seen2}}</p>
<p v-if="seen3">{{seen3}}</p>
<script>
var app = new Vue({
el: '#app-4',
data: {
seen1: [],
seen2: {},
seen3: 0,
}
})
</script>
2.2 循环 v-for
v-for
:
- 语法:
v-for="item in Obj"
- 遍历数组,位置参数: (item, index),第一个为取出的项,第二个为其索引,第二个非必须。
<ol> <li v-for="todo in todos"> {{ todo.text }} </li> </ol> <script> var app = new Vue({ el: '#app-4', data: { todos: [ { text: '1'}, { text: '2'}, { text: '3'} ] } }) </script>
- 遍历对象的属性,位置参数: (value, name),第一个为属性值,第二个为属性名,第二个非必须。
<ul> <li v-for="(value, name) in object"> {{name}} : {{value}} </li> </ul> <script> var app = new Vue({ el: '#app-4', data: { object: { favorite: "Python", using: "Java" } } }) </script>
2.3 表单输入绑定 v-model
实际中组件的 data
必须是一个函数,否则将会在此组件的所有实例中共享,官方文档 组件数据。故下面例子全部将 data 定义为函数,在返回即可。关于组件,见下面笔记。
v-model
:
- 语法:
v-model="value"
- 在 html 的 input, select 等等元素上创建双向数据绑定,即输入和元素的数据保持同步;
- 在 data 中声明初始值;
- 对于不同的 html 元素使用不同的 property 并抛出不同事件,后续使用 v-on 监听事件:
- text 和 textarea 元素使用
value
property 和input
事件; - checkbox 和 radio 使用
checked
property 和change
事件; - select 字段将
value
作为 prop 并将change
作为事件。
- text 和 textarea 元素使用
如,radio 的 change 事件:
<div id="app-vmodel">
<el-radio-group size='mini' v-model="selectRegion" @change="getInfo">
<el-radio-button v-for="region in regionMap" :key="region.name" :label="region.name" :value="region.value">
</el-radio-button>
</el-radio-group>
<span id="message"></span>
</div>
<script>
var app = new Vue({
el: '#app-vmodel',
data() {
return {
selectRegion: null,
regionMap: [{
name: 'r1',
value: 'r1-val'
}, {
name: 'r2',
value: 'r2-val'
}, ]
}
},
methods: {
getInfo() {
document.getElementById("message").innerHTML = '已选择的 region: ' + this.selectRegion;
}
}
})
</script>
2.4 动态绑定属性 v-bind
v-bind
:
- 语法:
v-bind:attr="value"
。 - 简写:v-bind 可省略,如上面 v-model 例子中
:key
实际上为v-bind:key
- 动态绑定 attribute,可以使用修饰符
.prop
绑定为 DOM property,关于 HTML property 与 attribute ; - 也可以绑定 prop,关于 prop 见下面笔记;
- 绑定的 attribute 也可以动态传入,
v-bind:[key]="value"
。
例子结合上面 v-model 例子。
2.5 绑定事件监听器 v-on
v-on
:
- 语法:
v-on:click="expr"
; - 简写:
v-on:
缩为@
,如@click="expr"
; - 事件类型由参数指定。表达式可以是一个方法的名字或一个内联语句,方法可以传入多个参数;
- 支持众多修饰符,如 stop, prevent 等等,如果没有修饰符也可以省略;
- 用在普通元素上只能监听原生 DOM 事件;用在自定义元素组件上,也可以监听子组件触发的自定义事件;
- 监听原生事件时,可以访问 $event,如
v-on:click="handle('ok', $event)"
;
例子一:radio 的监听,见上面 v-model 的例子;
例子二:将 table 当前行的值作为参数传入事件触发方法,需要配合 slot-scope(弃用,高版本使用v-slot)。
<div id="app-von">
<el-table :data="tableData" style="width: 40%">
<el-table-column prop="name" label="LastName">
</el-table-column>
<el-table-column prop="age" label="Age">
</el-table-column>
<el-table-column fixed="right" label="Update">
<template slot-scope="scope">
<el-button @click="handleClick(scope.row.name)" type="text" size="small">Update</el-button>
<el-button type="text" size="small">delete</el-button>
</template>
</el-table-column>
</el-table>
<span id="message"></span>
</div>
<script>
var app = new Vue({
el: '#app-von',
data() {
return {
tableData: [{
name: "Morty",
age: 12
}, {
name: "Rick",
age: 70
}]
}
},
methods: {
handleClick(param) {
document.getElementById("message").innerHTML = '已选择的 row name: ' + param;
}
}
})
</script>
2.6 插槽 v-slot
见组件。
3. 计算属性与侦听器
虽然模板中支持 JavaScript 表达式,但实际中任何复杂逻辑都不应该放在模板中。可以使用计算属性或侦听器。
3.1 计算属性 computed
computed:
- 声明:在组件 computed 中声明;
- 作用:当数据改变时,计算属性也会被自动执行;
- 所有 getter/setter 的 this 上下文自动绑定为 Vue 实例,即通过
this
访问当前实际的属性和方法,如:this.tableData
; - 如果使用了箭头函数,则 this 不会绑定为当前 Vue 实例,可以将实例作为第一个参数传入。
例:
<div id="app-computed">
<p>origin message: {{message}}</p>
<p>Computed objUpdated: {{objUpdated}}</p>
<p>Computed message: {{messageUpdated}}</p>
<button @click="changeOrigin">Update</button>
<button @click="testComputerSetter">testComputerSetter</button>
</div>
<script>
var app = new Vue({
el: '#app-computed',
data() {
return {
message: 'origin message.',
obj: {
prop1: 2121,
prop2: 'sasas'
}
}
},
computed: {
// 1. 普通声明,默认只有 getter,this 指向实例
// messageUpdated: function () {
// return this.message + '- computed 1 in computed.'
// }
// 2. 箭头函数声明,默认只有 getter,使用的实例需要传入
messageUpdated: vm => {
return vm.message + '- computed in computed.'
},
messageUpdater2: {
get: function () {
return this.message + '- computed in computed.'
},
set: function (v) {
this.message = v
}
},
now: function () {
return Date.now()
},
objUpdated: function () {
console.log(this.obj.prop1) // 监听对象的一个属性
return "obj.prop1 changed to " + this.obj.prop1
}
},
methods: {
changeOrigin() {
this.message += '- change in methods.'
this.obj.prop1 += 100;
this.messageUpdated = '100' // messageUpdated 无 setter,所以此处赋值并没有任何效果
},
testComputerSetter() {
this.messageUpdater2 = '100' // messageUpdater2 有 setter
},
}
})
</script>
可以看到:
- 实例中 message 变化时,messageUpdated 也会随之变化;
- 这样的 computed 声明,默认只有 getter,无 setter,可以声明 setter。
3.2 侦听器
侦听器:更通用的观察和响应实例数据变化
- 声明:在组件 watch 中声明,声明语法:
watch: { 要侦听的数据: function(newVal, oldVal) { // 位置参数获取新设置的值和上次的值 操作 } }
- 侦听器默认不会监听对象属性的变化,如果需要监听,则需要加
deep: true
,如果只需要监听某一个属性推荐写为字符串形式:"obj.prop1": function...
; - 侦听器可以通过位置参数,获取到新设置的值以及上次的值,如上;
- 无返回值;
- 注意:在侦听器中不能更改当前侦听的数据,否则会造成无限调用的死循环。
例:
<div id="app-watch">
<p>origin message: {{message}}</p>
<button @click="changeOriginMess">UpdateMessage</button>
<div id='message'></div>
<div id='obj'></div>
</div>
<script>
var app = new Vue({
el: '#app-watch',
data() {
return {
message: 'origin message.',
obj: {
prop1: 2121,
prop2: 'sasas'
}
}
},
watch: {
message: function (newVal, oldVal) {
this.printDebugInfo('message', 'newVal: ' + newVal + '--- oldVal: ' + oldVal)
},
'obj.prop1': function (newVal, oldVal) {
// 监听 obj.prop1
this.printDebugInfo('obj', 'newVal: ' + newVal + '--- oldVal: ' + oldVal)
},
},
methods: {
changeOriginMess() {
this.message += 'changed in methods.'
this.obj.prop1 += 100
},
printDebugInfo(id, info) {
document.getElementById(id).innerText = 'change: ' + info
}
}
})
</script>
3.3 计算属性、侦听器、方法区别
3.3.1 计算属性与方法
计算属性与方法:
- 相同点:都可以完成跟随数据变化而变化;
- 不同点 – 缓存:
- 计算属性基于它们的响应式依赖进行缓存,对于一个响应式依赖,如果未发生变化,多次调用 computed 是不会进行重复计算,除非响应式依赖发生变化;
- 关于响应式,目前可以理解为在组件 data 中声明的数值为响应式(数组和对象有额外要求,见后续笔记),像下面这样的并不是响应式依赖,computed 无法同步计算:
<script> var app = new Vue({ el: '#app-computed', data() { return { message: 'origin message.' } }, computed: { now: function () { return Date.now() // Date.now() 不是响应式依赖,computed 无法同步计算 } }, }) </script>
- 而方法会在每次调用时均进行计算。
3.3.2 计算属性与侦听器
计算属性与侦听器:
- 相同点:都只能监听响应式依赖变化;
- 不同点:
- 声明不同,计算属性声明时需要写新创建的数据名,内部写监听的数据;而侦听器在声明时写侦听的数据;
- 计算属性作为一个“属性”,需要在 getter 内写返回;而侦听器通常是侦听数据变化而进行某一系列操作,不会有返回值,且不能在侦听器内更改当前侦听的数据;
- 计算属性可以一次监听多个数据;侦听器只能监听声明时写的数据。
关于计算属性监听多个数据,直接写在内部即可,例:
<div id="app-computedVSwatch">
<p>Full Name by computed: {{fullNameComp}}</p>
<p>Full Name by watch: {{fullNameWatch}}</p>
<button @click="changeLastName">changeLastName</button>
</div>
<script>
var app = new Vue({
el: '#app-computedVSwatch',
data() {
return {
firstName: 'Jerry',
lastName: 'Smith',
fullNameWatch: 'Jerry Smith',
}
},
computed: {
fullNameComp: function() {
return this.firstName + ' ' + this.lastName
}
},
watch: {
firstName: function (newVal) {
this.fullNameWatch = newVal + ' ' + this.lastName
},
lastName: function (newVal, oldVal) {
this.fullNameWatch = this.firstName + ' ' + newVal
},
},
methods: {
changeLastName() {
this.lastName = 'Foo'
},
}
})
</script>
可以看到 watch 不适合处理多个关联数据变化。
vue 官方文档中说“使用 watch
选项允许我们执行异步操作,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。”,我此时不明白,试着用 computed 好像也能做到?
<div id="computedVSwatch-asyncReq">
<div id='watch'>
<p>
Ask a yes/no question by watch:
<input v-model="questionForWatch">
</p>
<p>{{ answerByWatch }}</p>
<button @click="cntAddOne">{{cnt}}</button>
</div>
<div id='computed'>
<p>
Ask a yes/no question by computed:
<input v-model="questionForComp">
</p>
<p>{{ answerByComp }}</p>
<button @click="cntAddOne">{{cnt}}</button>
</div>
<span id="message"></span><br>
<span>{{answerByCompInfo}}</span>
</div>
<script src="https://cdn.jsdelivr.net/npm/axios@0.12.0/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lodash@4.13.1/lodash.min.js"></script>
<script>
var watchExampleVM = new Vue({
el: '#computedVSwatch-asyncReq',
data() {
return {
cnt: 0,
questionForWatch: '',
answerByWatch: 'I cannot give you an answer until you ask a question!',
questionForComp: '',
answerByComp: 'I cannot give you an answer until you ask a question!',
}
},
computed: {
answerByCompInfo: function () {
this.printDebugInfo('questionForComp: ' + this.questionForComp)
this.answerByComp = 'Waiting for you to stop typing...'
this.debouncedGetAnswerByComp(this.questionForComp)
}
},
watch: {
questionForWatch: function (newQuestion, oldQuestion) {
this.answerByWatch = 'Waiting for you to stop typing...'
this.debouncedGetAnswer()
}
},
created: function () {
this.debouncedGetAnswer = _.debounce(this.getAnswer, 500)
this.debouncedGetAnswerByComp = _.debounce(this.getAnswerByComp, 500)
},
methods: {
cntAddOne: function () {
this.cnt += 1
},
printDebugInfo(info) {
document.getElementById("message").innerHTML = 'debugInfo: ' + info;
},
getAnswer: function () {
if (this.questionForWatch.indexOf('?') === -1) {
this.answerByWatch = 'Questions usually contain a question mark. ;-)'
return
}
this.answerByWatch = 'Thinking...'
var vm = this
axios.get('https://yesno.wtf/api')
.then(function (response) {
vm.answerByWatch = _.capitalize(response.data.answer)
})
.catch(function (error) {
vm.answerByWatch = 'Error! Could not reach the API. ' + error
})
},
getAnswerByComp: function (questionForWatch) {
if (questionForWatch.indexOf('?') === -1) {
answerByWatch = 'Questions usually contain a question mark. ;-)'
return
}
this.answerByComp = 'Thinking...'
var vm = this
axios.get('https://yesno.wtf/api')
.then(function (response) {
vm.answerByComp = _.capitalize(response.data.answer)
})
.catch(function (error) {
vm.answerByComp = 'Error! Could not reach the API. ' + error
})
}
}
})
</script>
4. 挂载和响应式原理
4.1 挂载
提供的元素只能作为挂载点,且所有挂载元素会被 Vue 生成的 DOM 替换,所以父级向子组件分发内容时要使用插槽,否则开始标签和结束标签的内容都会被删除。
4.2 响应式原理
vue 会将 data 选项的所有对象的属性转为 getter/setter,例如:
console.log(Object.getOwnPropertyDescriptors(this.aObject))
每个实例都会对应一个 watcher,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。
4.2.1 对象的注意事项
Vue 对于 data 中的对象可以检测与不可检测的解决:
- 可检测原有属性的值的变化;
- 不可检测属性的添加 – 解决:使用
Vue.set/this.$set
,Object.assign()混入原对象和新对象
:this.$set
为实例方法,在实例内绑定比较方便,如:
this.$set(this.obj, "addedProp2", 1111);
Object.assign()混入原对象和新对象
:this.obj = Object.assign({}, *this*.obj, { addedProp2: 2222 });
- 不可检测添加属性的值的变化;
- 不可检测任何属性的删除。
如下面例子(以使用 watch 为例):
<div id="app-reactive-object">
obj 对象的:
<div style="margin-left: 2em">obj: {{ obj }}</div>
<button @click="addPropToObj">addPropToObj</button>
<div id="obj"></div>
<button @click="changeOrgProp1">changeOrgProp1</button>
<div id="obj-prop1"></div>
<button @click="delOrgProp1">delOrgProp1</button><br>
<button @click="changeAddedProp1">changeAddedProp1</button>
<div id="obj-addedProp1"></div>
<button @click="changeAddedProp2">changeAddedProp2</button>
<div id="obj-addedProp2"></div>
</div>
<script>
var app = new Vue({
el: "#app-reactive-object",
name: "App-11",
data: function () {
return {
obj: {
prop1: 1,
},
};
},
watch: {
obj: function (newVal, oldVal) {
this.printDebugInfo(
"obj",
"newVal: " + newVal + "--- oldVal: " + oldVal
);
},
"obj.prop1": function (newVal, oldVal) {
this.printDebugInfo(
"obj-prop1",
"newVal: " + newVal + "--- oldVal: " + oldVal
);
},
"obj.addedProp1": function (newVal, oldVal) {
this.printDebugInfo(
"obj-addedProp1",
"newVal: " + newVal + "--- oldVal: " + oldVal
);
},
"obj.addedProp2": function (newVal, oldVal) {
this.printDebugInfo(
"obj-addedProp2",
"newVal: " + newVal + "--- oldVal: " + oldVal
);
},
},
methods: {
addPropToObj() {
this.obj.addedProp1 = "1111"; // 这样添加的无 getter 和 setter
// 1. $set 添加的有 getter 和 setter
// this.$set(this.obj, "addedProp2", 2222);
// 2. Object.assign()混入原对象和新对象,会将所有属性都添加 getter/setter
// 上面的 addedProp1 本没有 getter/setter,经过下面 assign 后也会有getter/setter
this.obj = Object.assign({}, this.obj, { addedProp2: 2222 });
console.log(this.obj);
},
changeOrgProp1() {
this.obj.prop1 += 1111;
},
delOrgProp1() {
delete this.obj.prop1;
},
changeAddedProp1() {
this.obj.addedProp1 += "1111";
},
changeAddedProp2() {
this.obj.addedProp2 += 2222;
},
printDebugInfo(id, info) {
document.getElementById(id).innerText = "change: " + info;
},
},
});
</script>
4.2.2 数组的注意事项
Vue 对于 data 中的数组可以检测与不可检测的解决:
- 可检测数组新添加或者删除元素;
- 不可检测索引方式的添加或修改 – 解决:使用
Vue.set/this.$set
:this.$set(this.arr, 1, 654) // 指定的位置存在,则更改 this.$set(this.arr, 10, 654) // 指定的位置不存在,则添加,且中间的会添加为 null
- 不可检测直接修改数组长度,如
this.arr.length = newLength
不可被检测 – 解决:使用splice
:this.arr.splice(1);
如下面例子(以使用 watch 为例):
<div id="app-reactive-array">
arr 数组的:
<div style="margin-left: 2em">obj: {{ arr }}</div>
<button @click="addEle">addEle</button>
<div id="arr"></div>
<button @click="changeEleByIndex">changeEleByIndex</button><br />
<button @click="popEle">popEle</button><br />
<button @click="changeOrAddEleBySet">changeOrAddEleBySet</button><br />
<button @click="changeLenByDirect">changeLenByDirect</button><br />
<button @click="changeLenBySplice">changeLenBySplice</button><br />
</div>
<script>
var app = new Vue({
el: "#app-reactive-array",
data: function () {
return {
arr: [1, 2, 3],
};
},
watch: {
arr: function (newVal, oldVal) {
this.printDebugInfo(
"arr",
"newVal: " + newVal + "--- oldVal: " + oldVal
);
},
},
methods: {
addEle() {
this.arr.push(9);
},
changeEleByIndex() {
this.arr[0] = 123;
},
popEle() {
this.arr.pop();
},
changeOrAddEleBySet() {
this.$set(this.arr, 1, 654); // 指定的位置存在,则更改
this.$set(this.arr, 10, 654); // 指定的位置不存在,则添加,且中间的会添加为 null
},
changeLenByDirect() {
this.arr.length = 1;
},
changeLenBySplice() {
this.arr.splice(1);
},
printDebugInfo(id, info) {
document.getElementById(id).innerText = "change: " + info;
},
},
});
</script>