Vue.js 的核心是一个允许采用 简洁的模板语法 来声明式地 将数据渲染进 DOM 的系统:
// HTML
<div id="demo">
<h1> Hello,{{ name.toUpperCase() }},{{ address }} h1>
div>
// JS
var app = new Vue({
// id选择器
el: '#app', // el用于指定当前Vue实例为哪个容器服务,值通常为css选择器字符串。
data:{
name:'atguigu',
address:'北京'
},
})
lzl:这其实也是模块化的思想,div中的变量放在 Vue实例对象中,实现隔离。
为了让div中的变量知道从哪里读取变量,所以需要 el: '#app'
建立连接。
现在 数据 和 DOM 已经被建立了关联,所有东西都是 响应式的。我们要怎么确认呢?打开你的浏览器的 JavaScript 控制台 (就在这个页面打开),并修改 app.name
、app.address
的值,你将看到上例相应地更新。
lzl:app 是对象名,message 是对象的属性,所以
app.name
、app.address
能读取和修改值。这是this
对象:
注意我们不再和 HTML 直接交互了。一个 Vue 应用会将其挂载到一个 DOM 元素上 (对于这个例子是 #app
) 然后对其进行完全控制。
——————
除了文本插值,我们还可以像这样来 绑定元素 attribute:
<div id="app-2">
<span v-bind:title="message">
鼠标悬停几秒钟查看此处动态绑定的提示信息!
span>
div>
var app2 = new Vue({
el: '#app-2',
data: {
message: '页面加载于 ' + new Date().toLocaleString()
}
})
你看到的 v-bind
attribute 被称为 指令。指令带有前缀 v-
,以表示它们是 Vue 提供的特殊 attribute。
如果你再次打开浏览器的 JavaScript 控制台,输入 app2.message = '新消息'
,就会再一次看到这个绑定了 title
attribute 的 HTML 已经进行了更新。
控制切换一个元素是否显示 也相当简单:
<div id="app-3">
<p v-if="seen">现在你看到我了p>
div>
var app3 = new Vue({
el: '#app-3',
data: {
seen: true //
}
})
在控制台输入 app3.seen = false
,你会发现之前显示的消息消失了。
这个例子演示了我们不仅可以 把数据 绑定到 DOM 文本或 attribute,还可以 绑定到 DOM 结构。
此外,Vue 也提供一个强大的过渡效果系统,可以在 Vue 插入/更新/移除元素时 自动应用过渡效果。
——————
还有其它很多指令,每个都有特殊的功能。例如,v-for
指令可以绑定数组的数据来渲染一个项目列表:
<div id="app-4">
<ol>
<li v-for="todo in todos">
{{ todo.text }}
li>
ol>
div>
var app4 = new Vue({
el: '#app-4',
data: {
todos: [
{ text: '学习 JavaScript' },
{ text: '学习 Vue' },
{ text: '整个牛项目' }
]
}
})
在控制台里,输入 app4.todos.push({ text: '新项目' })
,你会发现列表最后添加了一个新项目。
为了让用户和你的应用进行交互,我们可以用 v-on
指令添加一个 事件监听器,通过它 调用在 Vue 实例中定义的方法:
<div id="app-5">
<p>{{ message }}p>
<button v-on:click="reverseMessage">反转消息button>
div>
var app5 = new Vue({
el: '#app-5',
data: {
message: 'Hello Vue.js!'
},
methods: {
reverseMessage() {
this.message = this.message.split('').reverse().join('')
}
}
})
注意在
reverseMessage
方法中,我们更新了应用的状态,但没有触碰 DOM——所有的 DOM 操作都由 Vue 来处理,你编写的代码只需要关注逻辑层面即可。
——————
Vue 还提供了 v-model 指令,它能轻松实现 表单输入和应用状态 之间的双向绑定。
<div id="app-6">
<p>{{ message }}p>
<input v-model="message">
div>
var app6 = new Vue({
el: '#app-6',
data: {
message: 'Hello Vue!'
}
})
组件系统是 Vue 的另一个重要概念,因为它是一种抽象,允许我们使用 小型、独立和通常可复用的组件 构建大型应用。仔细想想,几乎任意类型的应用界面都可以抽象为一个组件树:
在 Vue 里,一个组件本质上是一个拥有预定义选项的一个 Vue 实例。在 Vue 中注册组件很简单:
// 定义名为 todo-item 的新组件
Vue.component('todo-item', {
template: '这是个待办项 '
})
var app = new Vue(...)
现在你可以用它构建另一个组件模板:
<ol>
<todo-item>todo-item>
ol>
——————
但是这样会 为每个待办项渲染同样的文本,这看起来并不炫酷。我们应该能从父作用域将数据传到子组件才对。让我们来修改一下组件的定义,使之能够接受一个 prop:
Vue.component('todo-item', {
// todo-item 组件现在接受一个
// "prop",类似于一个自定义 attribute。
// 这个 prop 名为 todo。
props: ['todo'],
template: '{{ todo.text }} '
})
现在,我们可以使用 v-bind 指令将待办项传到循环输出的每个组件中:
<div id="app-7">
<ol>
<todo-item
v-for="item in groceryList"
v-bind:todo="item"
v-bind:key="item.id"
>todo-item>
ol>
div>
// JS
Vue.component('todo-item', {
props: ['todo'],
template: '{{ todo.text }} '
})
var app7 = new Vue({
el: '#app-7',
data: {
groceryList: [
{ id: 0, text: '蔬菜' },
{ id: 1, text: '奶酪' },
{ id: 2, text: '随便其它什么人吃的东西' }
]
}
})
尽管这只是一个刻意设计的例子,但是我们已经 设法将应用分割成了两个更小的单元。子单元通过 prop 接口与父单元进行了良好的解耦。
我们现在可以 进一步改进
组件,提供更为复杂的模板和逻辑,而不会影响到父单元。
在一个大型应用中,有必要将整个应用程序划分为组件,以使开发更易管理。在后续教程中我们将详述组件,不过这里有一个 (假想的) 例子,以展示使用了组件的应用模板是什么样的:
<div id="app">
<app-nav>app-nav>
<app-view>
<app-sidebar>app-sidebar>
<app-content>app-content>
app-view>
div>
每个 Vue 应用都是通过用 Vue 函数创建一个新的 Vue 实例 开始的:
var vm = new Vue({
// 选项
})
…
当一个 Vue 实例被创建时,它将 data
对象中的所有的 property 加入到 Vue 的响应式系统中。当这些 property 的值发生改变时,视图将会产生"响应",即匹配更新为新的值。
// 我们的数据对象
var data = { a: 1 }
// 该对象被加入到一个 Vue 实例中
var vm = new Vue({
data: data // 将data对象中的属性与属性值 放到vm身上,所以有vm.a
})
// 获得这个实例上的 property
// 返回源数据中对应的字段
vm.a == data.a // => true
// 设置 property 也会影响到原始数据
vm.a = 2
data.a // => 2
// ……反之亦然,即都指向同一个堆地址
data.a = 3
vm.a // => 3
——————
当这些数据改变时,视图会进行重渲染。值得注意的是,只有当实例被创建时,就已经存在于 data 中的 property 才是响应式的。 也就是说如果你添加一个新的 property,比如:
vm.b = 'hi'
那么对 b
的改动将不会触发任何视图的更新。如果你知道你会在晚些时候需要一个 property,但是一开始它为空或不存在,那么你仅需要设置一些初始值。比如:
data: {
// 提前在data中配置好属性,并设定初始值,这样这些属性才是响应式的!
newTodoText: '',
visitCount: 0,
hideCompletedTodos: false,
todos: [],
error: null
}
——————
这里唯一的例外是使用 Object.freeze()
,这会阻止修改现有的 property,也意味着响应系统无法再追踪变化。
var obj = {
foo: 'bar'
}
Object.freeze(obj)
new Vue({
el: '#app',
data: obj // 把obj存储的堆地址,传递给data
})
<div id="app">
<p>{{ foo }}p>
<button v-on:click="foo = 'baz'">Change itbutton>
div>
——————
除了数据 property,Vue 实例还暴露了一些有用的实例 property 与方法。它们都有前缀 $
,以便 与用户定义的 property(属性) 区分开来。
// vm.$data、vm.$el、vm.$watch 都是自带的
var data = { a: 1 }
var vm = new Vue({
el: '#example',
data: data
})
vm.$data === data // => true
vm.$el === document.getElementById('example') // => true
// $watch 是一个实例方法
vm.$watch('a', function (newValue, oldValue) {
// 这个回调将在 `vm.a` 改变后调用
})
可以在 API 参考中查阅到完整的实例 property 和方法的列表。
每个 Vue 实例在被创建时都要经过一系列的初始化过程——例如,需要设置 数据监听、编译模板、将实例挂载到 DOM , 并在数据变化时更新 DOM 等。
同时在这个过程中也会运行一些叫做 生命周期钩子的函数,这给了用户在不同阶段 添加自己的代码的机会。
比如 created
钩子可以用来 在一个实例被创建之后执行代码:
new Vue({
data: {
a: 1
},
created() { // 生命周期钩子都是些函数,放在vm对象中,与data同级
// `this` 指向 vm 实例
console.log('a is: ' + this.a)
}
})
// => "a is: 1"
也有一些其它的钩子,在实例生命周期的不同阶段被调用,如 mounted
、updated
和 destroyed
。生命周期钩子的 this
上下文指向 调用它的 Vue 实例。
不要在选项 property 或回调上使用 箭头函数,比如 created: () => console.log(this.a)
或 vm.$watch('a', newValue => this.myMethod())
。
因为箭头函数并没有 this,this 会作为变量一直向上级词法作用域查找,直至找到为止,经常导致 Uncaught TypeError: Cannot read property of undefined
或 Uncaught TypeError: this.myMethod is not a function
之类的错误。
下图展示了实例的生命周期。随着你的不断学习和使用,它的参考价值会越来越高。
实例的生命周期图
数据绑定 最常见的形式就是使用 “Mustache”语法 (双大括号)
的 文本插值:
<span>Message: {{ msg }}span>
Mustache 标签将会 被替代为 对应数据对象上 msg
property(属性) 的值。
无论何时,绑定的数据对象上 msg
property 发生了改变,插值处的内容都会更新。
——————
通过使用 v-once
指令,你也能执行一次性地插值,当数据改变时,插值处的内容不会更新。但请留心这会影响到该节点上的其它数据绑定:
<span v-once> 这个将不会改变: {{ msg }} span>
双大括号会将数据解释为普通文本,而非 HTML 代码。为了输出真正的 HTML,你需要使用 v-html
指令:
<p> Using mustaches: {{ rawHtml }} p>
<p> Using v-html directive: <span v-html="rawHtml">span> p>
这个 span
的内容将会被替换成为 property 值 rawHtml
,直接作为 HTML——会忽略解析 property 值中的数据绑定。
注意,你不能使用 v-html
来复合局部模板,因为 Vue 不是基于字符串的模板引擎。 反之,对于用户界面 (UI),组件更适合作为可重用和可组合的基本单位。
你的站点上动态渲染的任意 HTML 可能会非常危险,因为它很容易导致 XSS 攻击。请只对可信内容使用 HTML 插值,绝不要对用户提供的内容使用插值。
Mustache 语法(双大括号)不能作用在 HTML attribute 上,遇到这种情况应该使用 v-bind
指令:
<div v-bind:id="dynamicId">div>
<button v-bind:disabled="isButtonDisabled">Buttonbutton>
如果 isButtonDisabled
的值是 null
、undefined
或 false
,则 disabled
attribute 甚至不会被包含在渲染出来的 元素中。
迄今为止,在我们的模板中,我们一直都只绑定简单的 property 键值。但实际上,对于所有的数据绑定,Vue.js 都提供了完全的 JavaScript 表达式 支持。
{{ number + 1 }} // 表达式
{{ ok ? 'YES' : 'NO' }} // 三元表达式
{{ message.split('').reverse().join('') }} // 表达式
<div v-bind:id="'list-' + id">div>
这些表达式会在所属 Vue 实例的数据作用域下作为 JavaScript 被解析。有个限制就是,每个绑定都只能包含 单个表达式,所以下面的例子都不会生效。
{{ var a = 1 }}
{{ if (ok) { return message } }}
模板表达式都被放在沙盒中,只能访问 全局变量的一个白名单,如 Math
和 Date
。你不应该在模板表达式中试图访问 用户定义的全局变量。
指令 (Directives) 是带有 v-
前缀的特殊 attribute。 指令 attribute 的值预期是 单个 JavaScript 表达式 (v-for 是例外情况,稍后我们再讨论)。
指令的职责是,当表达式的值改变时,将其产生的连带影响,响应式地作用于 DOM。
一些指令能够接收一个 “参数”,在指令名称之后以冒号表示。例如,此处是 href
,v-bind 指令可以用于响应式地更新 HTML attribute:
<a v-bind:href="url">...a>
在这里 href
是参数,告知 v-bind
指令将该元素的 href
attribute 与 表达式 url
的值绑定。
——————
另一个例子是 v-on 指令,它 用于监听 DOM 事件:
<a v-on:click="doSomething">...a>
在这里 参数 是监听的事件名 click
。
2.6.0 新增
从 2.6.0 开始,可以用 方括号括起来的 JavaScript 表达式 作为一个指令的参数:
<a v-bind:[attributeName]="url"> ... a>
这里的 attributeName
会被作为一个 JavaScript 表达式 进行动态求值,求得的值将会作为最终的参数来使用。例如,如果你的 Vue 实例有一个 data
property attributeName
,其值为 "href"
,那么这个绑定将等价于 v-bind:href
。
——————
同样地,你可以使用动态参数为一个 动态的事件名 绑定处理函数:
<a v-on:[eventName]="doSomething"> ... </a>
在这个示例中,当 eventName
的值为 "focus"
时,v-on:[eventName]
将等价于 v-on:focus
。(不同动态事件绑定同一个处理函数)
动态参数预期会求出一个 字符串,异常情况下值为 null
。这个特殊的 null 值可以被显性地用于移除绑定。任何其它非字符串类型的值都将会触发一个警告。
动态参数表达式有一些语法约束,因为某些字符,如空格和引号,放在 HTML attribute 名里是无效的。例如:
<a v-bind:['foo' + bar]="value"> ... a>
变通的办法是使用 没有空格或引号的表达式,或用 计算属性 替代这种复杂表达式。
在 DOM 中使用模板时 (直接在一个 HTML 文件里撰写模板),还需要避免使用大写字符来命名键名,因为浏览器 会把 attribute 名全部强制转为小写:
<a v-bind:[someAttr]="value"> ... a>
修饰符 (modifier) 是以半角句号.
指明的特殊后缀,用于指出一个指令应该以特殊方式绑定。例如,.prevent
修饰符告诉 v-on
指令对于触发的事件调用 event.preventDefault()
:
<form v-on:submit.prevent="onSubmit">...form>
在接下来对 v-on
和 v-for
等功能的探索中,你会看到修饰符的其它例子。
v-
前缀作为一种视觉提示,用来识别模板中 Vue 特定的 attribute。
v-bind
缩写
<a v-bind:href="url">...a>
<a :href="url">...a>
<a :[key]="url"> ... a>
v-on
缩写
<a v-on:click="doSomething">...a>
<a @click="doSomething">...a>
<a @[event]="doSomething"> ... a>
它们不会出现在最终渲染的标记中。
模板内的表达式非常便利,但是设计它们的初衷是用于简单运算的。在模板中放入 太多的逻辑 会让模板过重且难以维护 。例如:
<div id="example">
{{ message.split('').reverse().join('') }}
div>
对于任何复杂逻辑,你都应当使用计算属性。
<div id="example">
<p>Original message: "{{ message }}"p>
<p>Computed reversed message: "{{ reversedMessage }}"p>
div>
var vm = new Vue({
el: '#example',
data: {
message: 'Hello'
},
computed: {
// 计算属性的 getter
reversedMessage() {
return this.message.split('').reverse().join('')
}
}
})
这里我们声明了一个计算属性 reversedMessage
。我们提供的函数将 用作 property vm.reversedMessage
的 getter 函数:
console.log(vm.reversedMessage) // => 'olleH'
vm.message = 'Goodbye'
console.log(vm.reversedMessage) // => 'eybdooG'
Vue 知道 vm.reversedMessage
依赖于 vm.message
,因此当 vm.message
发生改变时,所有依赖 vm.reversedMessage
的绑定也会更新。
而且最妙的是我们已经以声明的方式创建了这种依赖关系:计算属性的 gette
r 函数是没有副作用 (side effect) 的,这使它更易于测试和理解。
你可能已经注意到我们可以通过 在表达式中调用方法来达到同样的效果:
<p>Reversed message: "{{ reversedMessage() }}"p>
// 在组件中
methods: {
reversedMessage: function () {
return this.message.split('').reverse().join('')
}
}
我们可以将同一函数定义为一个方法而不是一个计算属性。两种方式的最终结果确实是完全相同的。
然而,不同的是计算属性是基于它们的 响应式依赖 进行缓存的。
1、只在相关响应式依赖发生改变时,它们才会重新求值。 ---- 【响应式】
这就意味着:2、只要 message
还没有发生改变,多次访问 reversedMessage
计算属性 会立即返回之前的计算结果,而不必再次执行函数。 ---- 【提高内存时间效率】
这也同样意味着下面的计算属性将不再更新,因为 Date.now() 不是响应式依赖:
computed: {
now() {
return Date.now()
}
}
——
相比之下,每当触发重新渲染时,调用方法将总会再次执行函数。
<script type="text/javascript" src="../js/vue.js">script>
<div id="root">
姓:<input type="text" v-model="firstName"> <br/><br/>
名:<input type="text" v-model="lastName"> <br/><br/>
全名:<span>{{ fullName() }}span>
div>
new Vue({
el:'#root',
data:{
firstName:'张',
lastName:'三'
},
methods: {
fullName(){
return this.firstName + '-' + this.lastName
} // 每当firstName、lastName在输入框被改变时,
}, // 页面就会重新渲染,再次调用methods中的函数计算值
})
————
我们为什么需要缓存?假设我们有一个性能开销比较大的计算属性 A,它需要遍历一个巨大的数组并做大量的计算。然后我们可能有其他的计算属性依赖于 A。如果没有缓存,我们将不可避免的多次执行 A 的 getter!如果你不希望有缓存,请用方法来替代。
Vue 提供了一种更通用的方式来观察和响应 Vue 实例上的数据变动:侦听属性。当你有一些数据需要随着其它数据变动而变动时,你很容易滥用 watch
——特别是如果你之前使用过 AngularJS。然而,通常更好的做法是使用计算属性computed
,而不是命令式的 watch
回调。
细想一下这个例子:
<div id="demo">{{ fullName }}div>
var vm = new Vue({
el: '#demo',
data: {
firstName: 'Foo',
lastName: 'Bar',
fullName: 'Foo Bar'
},
watch: {
// 监听 firstName、lastName有变化时,更新this.fullName
firstName(val) {
this.fullName = val + ' ' + this.lastName
},
lastName(val) {
this.fullName = this.firstName + ' ' + val
}
}
})
上面代码是命令式且重复的。将它与计算属性的版本进行比较:
var vm = new Vue({
el: '#demo',
data: {
firstName: 'Foo',
lastName: 'Bar'
},
computed: {
fullName() {
return this.firstName + ' ' + this.lastName
}
}
})
好得多了,不是吗?
计算属性 默认只有 getter,不过在需要时你也可以提供一个 setter:
如果计算属性要被修改,那必须写 set
函数去响应修改,且 set
中要引起计算时依赖的数据发生改变。
// ...
computed: {
fullName: {
// getter
get() {
return this.firstName + ' ' + this.lastName
},
// setter
set(newValue) { // vm.fullName='John Doe' 会启动setter
var names = newValue.split(' ')
this.firstName = names[0]
this.lastName = names[names.length - 1]
}
}
}
// ...
现在再运行 vm.fullName = 'John Doe'
时,setter 会被调用,vm.firstName
和 vm.lastName
也会相应地被更新。
const vm = new Vue({
// ...
watch:{
//【对象的形式、函数的形式】
//当isHot发生改变时, handler调用
isHot:{
immediate:true, // 初始化时让handler调用一下
deep:true, // 监视多级结构中所有属性的变化
handler(newValue, oldValue){
console.log('isHot被修改了', newValue, oldValue)
}
},
//简写:官方写法
isHot(newValue, oldValue){
console.log('isHot被修改了',newValue,oldValue,this)
}
// 深度监听
'numbers.a':{
handler(){
console.log('a被改变了')
}
},
/* numbers:{
deep:true,
handler(){
console.log('numbers改变了')
}
} */
}
})
const vm = new Vue({
//...
})
vm.$watch('isHot',{
immediate:true,
handler(newValue, oldValue){
console.log('isHot被修改了',newValue, oldValue)
}
})
//简写
vm.$watch('isHot',(newValue, oldValue)=>{
console.log('isHot被修改了', newValue, oldValue, this)
})
虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器。
这就是为什么 Vue 通过 watch
选项提供了一个更通用的方法,来响应数据的变化。当需要在数据变化时,执行异步或开销较大的操作时,这个方式是最有用的。
例如:
<div id="watch-example">
<p>
Ask a yes/no question:
<input v-model="question">
p>
<p>{{ answer }}p>
div>
<!-- 因为 AJAX 库和通用工具的生态已经相当丰富,Vue 核心代码没有重复 -->
<!-- 提供这些功能以保持精简。这也可以让你自由选择自己更熟悉的工具。 -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js"></script>
<script>
var watchExampleVM = new Vue({
el: '#watch-example',
data: {
question: '',
answer: 'I cannot give you an answer until you ask a question!'
},
watch: {
// 如果 `question` 发生改变,这个函数就会运行
question(newQuestion, oldQuestion) {
this.answer = 'Waiting for you to stop typing...'
this.debouncedGetAnswer()
}
},
created() {
// `_.debounce` 是一个通过 Lodash 限制操作频率的函数。
// 在这个例子中,我们希望限制访问 yesno.wtf/api 的频率
// AJAX 请求直到用户输入完毕才会发出。想要了解更多关于
// `_.debounce` 函数 (及其近亲 `_.throttle`) 的知识,
// 请参考:https://lodash.com/docs#debounce
this.debouncedGetAnswer = _.debounce(this.getAnswer, 500)
},
methods: {
getAnswer: function () {
if (this.question.indexOf('?') === -1) {
this.answer = 'Questions usually contain a question mark. ;-)'
return
}
this.answer = 'Thinking...'
var vm = this
axios.get('https://yesno.wtf/api')
.then(function (response) {
vm.answer = _.capitalize(response.data.answer)
})
.catch(function (error) {
vm.answer = 'Error! Could not reach the API. ' + error
})
}
}
})
</script>
除了 watch 选项之外,您还可以使用命令式的 vm.$watch
API。
Vue官网:侦听器