4.响应式
4.1 响应式基础
学习:状态选项data,$data
选用选项式 API 时,会用 data
选项来声明组件的响应式状态。此选项的值应为返回一个对象的函数。Vue 将在创建新组件实例的时候调用此函数,并将函数返回的对象用响应式系统进行包装。此对象的所有顶层属性都会被代理到组件实例 (即方法和生命周期钩子中的 this
) 上。
Vue 在组件实例上暴露的内置 API 使用 $
作为前缀。它同时也为内部属性保留 _
前缀。因此,你应该避免在顶层 data
上使用任何以这些字符作前缀的属性。
从 data 选项函数中返回的对象,会被组件赋为响应式。组件实例将会代理对其数据对象的属性访问。
完整案例18_data.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>vue3解决dom问题</title>
<script src="lib/vue.global.js"></script>
</head>
<body>
<div id="app">
{{ count }}
</div>
</body>
<script>
const { createApp } = window.Vue
const app = createApp({
data () {
return {
count: 100
}
},
created () {
console.log(this)
console.log(this.count) // 100
this.count = 200
console.log(this.$data.count) // 100 // 200
this.$data.count = 300
console.log(this._.data.count) // 100 // 300
this._.data.count = 400
}
})
// 挂载应用
app.mount('#app')
</script>
</html>
4.2 计算属性
学习computed
模板中的表达式虽然方便,但也只能用来做简单的操作。如果在模板中写太多逻辑,会让模板变得臃肿,难以维护
推荐使用计算属性来描述依赖响应式状态的复杂逻辑
计算属性是基于它们的响应式依赖进行缓存的,计算属性比较适合对多个变量或者对象进行处理后返回一个结果值,也就是说多个变量中的某一个值发生了变化则我们监控的这个值也就会发生变化。
计算属性定义在Vue对象中,通过关键词 computed 属性对象中定义一个个函数,并返回一个值,使用计算属性时和 data 中的数据使用方式一致。
4.2.1 一般计算属性以及方法对比
完整案例:19_computed.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>vue3解决dom问题</title>
<script src="lib/vue.global.js"></script>
</head>
<body>
<div id="app">
<!-- js表达式 -->
{{ msg.split('').reverse().join('') }} - {{ msg.split('').reverse().join('') }} - {{ msg.split('').reverse().join('') }}
<!-- 方法 -->
{{ reverseMsgFn() }} - {{ reverseMsgFn() }} - {{ reverseMsgFn() }}
<!-- 计算属性 -->
{{ reverseMsg }} - {{ reverseMsg }} - {{ reverseMsg }}
</div>
</body>
<script>
const { createApp } = window.Vue
const app = createApp({
data () {
return {
msg: 'hello vue'
}
},
methods: {
reverseMsgFn () {
console.log(1)
return this.msg.split('').reverse().join('')
}
},
computed: {
reverseMsg () {
console.log(2)
return this.msg.split('').reverse().join('')
}
}
})
// 挂载应用
app.mount('#app')
</script>
</html>
计算属性具有依赖性,只有当依赖的值发生改变,才会重新计算
同等条件下,计算属性优于 方法 以及 js表达式。
4.2.2 可写计算属性
计算属性默认仅能通过计算函数得出结果。当你尝试修改一个计算属性时,你会收到一个运行时警告。只在某些特殊场景中你可能才需要用到“可写”的属性,你可以通过同时提供 getter 和 setter 来创建:
完整案例:20_computed.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>20_计算属性 setter</title>
<script src="lib/vue.global.js"></script>
</head>
<body>
<div id="app">
<input v-model="firstName" /> + <input v-model="lastName" /> = {{ fullName }}
<button @click="reset">重置</button>
</div>
</body>
<script>
const { createApp } = window.Vue
const app = createApp({
data () {
return {
firstName: 'John',
lastName: 'Doe'
}
},
computed: {
// fullName () {
// return this.firstName + ' ' + this.lastName
// }
fullName: {
get () {
return this.firstName + ' ' + this.lastName
},
set (newValue) {
// 注意:我们这里使用的是解构赋值语法
// [this.firstName, this.lastName] = newValue.split(' ')
this.firstName = newValue.split(' ')[0]
this.lastName = newValue.split(' ')[1]
}
}
},
methods: {
reset () {
console.log('111')
// this.firstName = ''
// this.lastName = ''
this.fullName = 'wu daxun'
}
},
})
// 挂载应用
app.mount('#app')
</script>
</html>
4.3 侦听器
学习:watch以及实例方法$watch
使用watch来侦听data中数据的变化,watch中的属性一定是data 中已经存在的数据。
watch 只能监听data中的数据变化吗?
watch可以监听路由中的数据的变化
使用场景:数据变化时执行异步或开销比较大的操作。
完整案例:21_watch.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>21_侦听属性</title>
<script src="lib/vue.global.js"></script>
</head>
<body>
<div id="app">
<input type="text" v-model="firstName" /> + <input type="text" v-model="lastName" /> = {{ fullName }}
</div>
</body>
<script>
const { createApp } = window.Vue
const app = createApp({
data () {
return {
firstName: '',
lastName: '',
fullName: ''
}
},
watch: {
firstName (newVal, oldVal) {
this.fullName = newVal + this.lastName
},
lastName (newVal, oldVal) {
this.fullName = this.firstName + newVal
}
}
})
// 挂载应用
app.mount('#app')
</script>
</html>
使用计算属性可以简化
完整案例:22_computed.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>22_计算属性PK侦听属性</title>
<script src="lib/vue.global.js"></script>
</head>
<body>
<div id="app">
<input type="text" v-model="firstName" /> + <input type="text" v-model="lastName" /> = {{ fullName }}
</div>
</body>
<script>
const { createApp } = window.Vue
const app = createApp({
data () {
return {
firstName: '',
lastName: ''
}
},
computed: {
fullName () {
return this.firstName + this.lastName
}
}
})
// 挂载应用
app.mount('#app')
</script>
</html>
如何监听一个对象下的属性的变化?
watch
默认是浅层的:被侦听的属性,仅在被赋新值时,才会触发回调函数——而嵌套属性的变化不会触发。如果想侦听所有嵌套的变更,你需要深层侦听器:如果一开始就需要监听数据,建议直接在options Api中添加 watch选项
如果在达到某一个条件下再开启监听,需要使用 this.$watch()手动添加侦听器
如果不使用深度侦听,如何监听对象下的属性的变化,可以通过 监听
对象.属性
的变化(vue2 + vue3),注意this指向完整案例:
23_deep_watch.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>23_深度侦听</title> <script src="lib/vue.global.js"></script> </head> <body> <div id="app"> <input type="text" v-model="user.firstName" /> + <input type="text" v-model="user.lastName" /> = {{ user.fullName }} | {{ full }} <hr /> <button @click="count++">加1</button>{{ count }} <button @click="startWatch">开始监听</button> <button @click="stopWatch">停止监听</button> </div> </body> <script> const { createApp } = window.Vue const app = createApp({ data () { return { user: { firstName: '1', lastName: '2', fullName: '' }, count: 100, unwatch: null } }, computed: { // 计算属性更优 full () { return this.user.firstName + this.user.lastName } }, watch: { // 监听失败 // user (newVal, oldVal) { // this.user.fullName = newVal.firstName + newVal.lastName // } // 深度侦听 user: { deep: true, handler (newVal, oldVal) { this.user.fullName = newVal.firstName + newVal.lastName }, // 强制立即执行回调 --- 自动执行一次监听数据 immediate: true, // 默认情况下,用户创建的侦听器回调,都会在 Vue 组件更新之前被调用。 // 这意味着你在侦听器回调中访问的 DOM 将是被 Vue 更新之前的状态。 // 在侦听器回调中能访问被 Vue 更新之后的DOM,你需要指明 flush: 'post' 选项 flush: 'post' // vue3中新增的 } // 以下的写法只适用于 Vue2 // 'user.firstName': (newVal, oldVal) => { // this.user.fullName = newVal + this.user.lastName // }, // 'user.lastName': (newVal, oldVal) => { // this.user.fullName = this.user.firstName + newVal // } }, methods: { startWatch () { // 开始监听 赋值给一个函数,用于停止监听 this.unwatch = this.$watch('count', (newVal, oldVal) => { console.log(newVal, oldVal) }) }, stopWatch () { this.unwatch() // 停止监听 } } }) // 挂载应用 app.mount('#app') </script> </html>
4.4 深入响应式系统
学习:renderTracked 以及 renderTraggered
Vue 最标志性的功能就是其低侵入性的响应式系统。组件状态都是由响应式的 JavaScript 对象组成的。当更改它们时,视图会随即自动更新。这让状态管理更加简单直观,但理解它是如何工作的也是很重要的,这可以帮助我们避免一些常见的陷阱。
4.4.1 什么是响应性
这个术语在今天的各种编程讨论中经常出现,但人们说它的时候究竟是想表达什么意思呢?本质上,响应性是一种可以使我们声明式地处理变化的编程范式。一个经常被拿来当作典型例子的用例即是 Excel 表格:
这里单元格 A2 中的值是通过公式 = A0 + A1
来定义的 (你可以在 A2 上点击来查看或编辑该公式),因此最终得到的值为 3,正如所料。但如果你试着更改 A0 或 A1,你会注意到 A2 也随即自动更新了。
而 JavaScript 默认并不是这样的。如果我们用 JavaScript 写类似的逻辑:
let A0 = 1
let A1 = 2
let A2 = A0 + A1
console.log(A2) // 3
A0 = 2
console.log(A2) // 仍然是 3
当我们更改 A0
后,A2
不会自动更新。
那么我们如何在 JavaScript 中做到这一点呢?首先,为了能重新运行计算的代码来更新 A2
,我们需要将其包装为一个函数:
let A2
function update() {
A2 = A0 + A1
}
然后,我们需要定义几个术语:
这个
update()
函数会产生一个副作用,或者就简称为作用 (effect),因为它会更改程序里的状态。A0
和A1
被视为这个作用的依赖 (dependency),因为它们的值被用来执行这个作用。因此这次作用也可以说是一个它依赖的订阅者 (subscriber)。
我们需要一个魔法函数,能够在 A0
或 A1
(这两个依赖) 变化时调用 update()
(产生作用)。
whenDepsChange(update)
这个 whenDepsChange()
函数有如下的任务:
当一个变量被读取时进行追踪。例如我们执行了表达式
A0 + A1
的计算,则A0
和A1
都被读取到了。如果一个变量在当前运行的副作用中被读取了,就将该副作用设为此变量的一个订阅者。例如由于
A0
和A1
在update()
执行时被访问到了,则update()
需要在第一次调用之后成为A0
和A1
的订阅者。探测一个变量的变化。例如当我们给
A0
赋了一个新的值后,应该通知其所有订阅了的副作用重新执行。
4.4.2 Vue 中的响应性是如何工作的
后续单独讲解,刨析vue2的响应式和vue3的响应式的区别以及实现原理
5.组件化
5.1 什么是组件化?理解组件化
组件允许我们将 UI 划分为独立的、可重用的部分,并且可以对每个部分进行单独的思考。在实际应用中,组件常常被组织成层层嵌套的树状结构:
这和我们嵌套 HTML 元素的方式类似,Vue 实现了自己的组件模型,使我们可以在每个组件内封装自定义内容与逻辑。
5.2 如何封装一个vue组件
目标:理解一般思路即可
一个 Vue 组件在使用前需要先被“注册”,这样 Vue 才能在渲染模板时找到其对应的实现。
先构建组件的模板
定义组件
注册组件
使用组件
5.3 vue3组件注册和使用
学习:app.component()、components选项、template选项
组件注册有两种方式:全局注册和局部注册。
5.3.1 全局注册组件
我们可以使用 Vue 应用实例的 app.component()
方法,让组件在当前 Vue 应用中全局可用
import { createApp } from 'vue'
const app = createApp({})
app.component(
// 注册的名字
'MyComponent',
// 组件的实现
{
/* ... */
}
)
app.component() 方法可以被链式调用:
app
.component('ComponentA', ComponentA)
.component('ComponentB', ComponentB)
.component('ComponentC', ComponentC)
全局注册的组件可以在此应用的任意组件的模板中使用
<!-- 这在当前应用的任意组件中都可用 -->
<ComponentA/>
<ComponentB/>
<ComponentC/>
所有的子组件也可以使用全局注册的组件,这意味着这三个组件也都可以在彼此内部使用
完整案例:24_component_vue3.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>24_全局注册组件</title>
</head>
<body>
<div id="app">
<!-- 04 使用组件 大驼峰式 短横线式 在html文件中只能使用 短横线式 -->
<!-- 为了方便,Vue 支持将模板中使用 kebab-case 的标签解析为使用 PascalCase 注册的组件。
这意味着一个以 `MyComponent` 为名注册的组件,在模板中可以通过 `<MyComponent>` 或 `<my-component>` 引用。
这让我们能够使用同样的 JavaScript 组件注册代码来配合不同来源的模板。 -->
<my-header></my-header>
<!-- <MyHeader></MyHeader> -->
</div>
</body>
<!-- 01 定义组件的模板 -->
<template id="header">
<header>头部-{{msg}} - {{ reverseMsg }}</header>
</template>
<script src="lib/vue.global.js"></script>
<script>
// 02.定义组件 - 首字母大写
const Header = {
template: '#header', // 绑定页面的模板 --- 必不可少
// 可以写任意的属于vue的选项
data () {
return {
msg: 'hello header'
}
},
computed: {
reverseMsg () {
return this.msg.split('').reverse().join('')
}
}
}
const { createApp } = Vue
const app = createApp({
})
// 03.全局注册组件 --- app.mount('#app') 之前
// app.component('MyHeader', Header) // 大驼峰式
app.component('my-header', Header) // 短横线式
app.mount('#app')
</script>
</html>
vue2全局注册组件
完整案例:25_component_vue2.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>25_vue2全局注册组件</title>
</head>
<body>
<div id="app">
<!-- 04 使用组件 大驼峰式 短横线式 在html文件中只能使用 短横线式 -->
<!-- 为了方便,Vue 支持将模板中使用 kebab-case 的标签解析为使用 PascalCase 注册的组件。
这意味着一个以 `MyComponent` 为名注册的组件,在模板中可以通过 `<MyComponent>` 或 `<my-component>` 引用。
这让我们能够使用同样的 JavaScript 组件注册代码来配合不同来源的模板。 -->
<my-header></my-header>
<!-- <MyHeader></MyHeader> -->
</div>
</body>
<!-- 01 定义组件的模板 -->
<template id="header">
<header>头部-{{msg}} - {{ reverseMsg }}</header>
</template>
<script src="lib/vue.js"></script>
<script>
// 02.定义组件 - 首字母大写
const Header = {
template: '#header', // 绑定页面的模板 --- 必不可少
// 可以写任意的属于vue的选项
data () { // vue2中的所有的组件的 data 必须是函数
return {
msg: 'hello header'
}
},
computed: {
reverseMsg () {
return this.msg.split('').reverse().join('')
}
}
}
// 03.全局注册组件 --- new Vue 实例 之前
// Vue.component('MyHeader', Header) // 大驼峰式
Vue.component('my-header', Header) // 短横线式
new Vue({}).$mount('#app')
</script>
</html>
5.3.2 局部注册组件
全局注册虽然很方便,但有以下几个问题:
全局注册,但并没有被使用的组件无法在生产打包时被自动移除 (也叫“tree-shaking”)。如果你全局注册了一个组件,即使它并没有被实际使用,它仍然会出现在打包后的 JS 文件中。
全局注册在大型项目中使项目的依赖关系变得不那么明确。在父组件中使用子组件时,不太容易定位子组件的实现。和使用过多的全局变量一样,这可能会影响应用长期的可维护性。
相比之下,局部注册的组件需要在使用它的父组件中显式导入,并且只能在该父组件中使用。它的优点是使组件之间的依赖关系更加明确,并且对 tree-shaking 更加友好。
局部注册需要使用 components
选项
对于每个 components
对象里的属性,它们的 key 名就是注册的组件名,而值就是相应组件的实现。
完整案例:26_components_vue3.html
vue3局部注册组件
<!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>26_vue3 局部注册组件</title>
</head>
<body>
<div id="app">
<!-- 04 使用组件 大驼峰式 短横线式 在html文件中只能使用 短横线式 -->
<!-- 为了方便,Vue 支持将模板中使用 kebab-case 的标签解析为使用 PascalCase 注册的组件。
这意味着一个以 `MyComponent` 为名注册的组件,在模板中可以通过 `<MyComponent>` 或 `<my-component>` 引用。
这让我们能够使用同样的 JavaScript 组件注册代码来配合不同来源的模板。 -->
<my-header></my-header>
<!-- <MyHeader></MyHeader> -->
</div>
</body>
<!-- 01 定义组件的模板 -->
<template id="header">
<header>头部-{{msg}} - {{ reverseMsg }}</header>
</template>
<script src="lib/vue.global.js"></script>
<script>
// 02.定义组件 - 首字母大写
const Header = {
template: '#header', // 绑定页面的模板 --- 必不可少
// 可以写任意的属于vue的选项
data () {
return {
msg: 'hello header'
}
},
computed: {
reverseMsg () {
return this.msg.split('').reverse().join('')
}
}
}
const { createApp } = Vue
const app = createApp({
components: { // 03 局部注册组件
// 'my-header': Header
MyHeader: Header
}
})
app.mount('#app')
</script>
</html>
完整案例:27_components_vue2.html
vue2局部注册组件
<!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>27_vue2局部注册组件</title>
</head>
<body>
<div id="app">
<!-- 04 使用组件 大驼峰式 短横线式 在html文件中只能使用 短横线式 -->
<!-- 为了方便,Vue 支持将模板中使用 kebab-case 的标签解析为使用 PascalCase 注册的组件。
这意味着一个以 `MyComponent` 为名注册的组件,在模板中可以通过 `<MyComponent>` 或 `<my-component>` 引用。
这让我们能够使用同样的 JavaScript 组件注册代码来配合不同来源的模板。 -->
<my-header></my-header>
<!-- <MyHeader></MyHeader> -->
</div>
</body>
<!-- 01 定义组件的模板 -->
<template id="header">
<header>头部-{{msg}} - {{ reverseMsg }}</header>
</template>
<script src="lib/vue.js"></script>
<script>
// 02.定义组件 - 首字母大写
const Header = {
template: '#header', // 绑定页面的模板 --- 必不可少
// 可以写任意的属于vue的选项
data () { // vue2中的所有的组件的 data 必须是函数
return {
msg: 'hello header'
}
},
computed: {
reverseMsg () {
return this.msg.split('').reverse().join('')
}
}
}
new Vue({
components: { // 03 局部注册组件
// MyHeader: Header
'my-header': Header
}
}).$mount('#app')
</script>
</html>
局部注册的组件在后代组件中并*不*可用
5.3.3 组件使用注意事项
以上案例使用 PascalCase 作为组件名的注册格式,这是因为:
PascalCase 是合法的 JavaScript 标识符。这使得在 JavaScript 中导入和注册组件都很容易,同时 IDE 也能提供较好的自动补全。
<PascalCase />
在模板中更明显地表明了这是一个 Vue 组件,而不是原生 HTML 元素。同时也能够将 Vue 组件和自定义元素 (web components) 区分开来
在单文件组件和内联字符串模板中,我们都推荐这样做。但是,PascalCase 的标签名在 DOM 模板中是不可用的
为了方便,Vue 支持将模板中使用 kebab-case 的标签解析为使用 PascalCase 注册的组件。这意味着一个以 MyComponent
为名注册的组件,在模板中可以通过 <MyComponent>
或 <my-component>
引用。这让我们能够使用同样的 JavaScript 组件注册代码来配合不同来源的模板。