目录
一、响应式编程的原理及在vue中的应用
响应式的本质是对变量的监听,当监听到变量发生变化时,我们可以做一些预定义的逻辑。例如对于数据绑定技术来说,需要做的是在变量发生改变时及对页面的元素进行刷新。
1、手动追踪变量的变化
<script>
let a = 1
let b = 2
let sum = a + b
console.log(sum);
a = 3
b = 4
console.log(sum);
</script>
结果:
两次输出的sum变量的值都是3,虽然从逻辑上理解,sum值得意义是变量a和变量b的值得和,但是当变量a和变量b发生改变时,变量sum的值并不会响应式地进行改变。
如何为sum这类变量增加响应式?在js中,可以使用Proxy对原对象进行包装,从而实现对对象属性设置和获取的监听。
<script>
let a = {
value: 1
}
let b = {
value: 2
}
//定义触发器,用来刷新数据
let trigger = null
handlerA = {
get(target, prop) {
return target[prop]
},
set(target, key, value) {
target[key] = value
if (trigger) {
trigger()
}
}
}
handlerB = {
get(target, prop) {
return target[prop]
},
set(target, key, value) {
target[key] = value
if (trigger) {
trigger()
}
}
}
let pa = new Proxy(a, handlerA)
let pb = new Proxy(b, handlerB)
let sum = 0
trigger = () => {
sum = pa.value + pb.value
}
trigger()
console.log(sum);
pa.value = 3
pb.value = 4
console.log(sum);
</script>
结果:
2、vue中的响应式对象
在vue3.0中引入了组合式API的新特性,这种新特性允许我们在setup方法中定义组件需要的数据和函数,set方法可以在组件被创建前定义组件需要的数据和函数。
<div class="app">
<h1>测试数据:{{myData.value}}</h1>
<button @click="click">点击</button>
</div>
<script>
const app = Vue.createApp({
setup() {
//定义数据
let myData = {
value: 0
}
//定义点击事件
function click() {
myData.value++
console.log(myData.value);
}
//返回数据和方法
return {
myData,
click
}
}
})
app.mount('.app')
</script>
结果:
为什么页面没有被刷新?这是因为myData对象是我们自己定义的普通js对象,本身没有响应式,对其进行修改也不会同步刷新到页面上,这与我们常规使用组件的data方法返回的数据不同,data方法返回的数据会被默认保存到proxy对象,从而获得响应式。
为了解决上面的额问题,vue3提供了reactive方法,使用这个方法对自定义的js对象进行包装,即可以方便地为其添加响应式。
//定义数据
let myData = Vue.reactive({
value: 0
})
结果:
3、独立的响应式值Ref的应用
在实际开发中,很多时候我们需要的只是一个独立的原始值,对于独立的原始值,不需要手动将其包装为对象的属性,可以直接使用vue提供的ref方法来定义响应式独立值,ref方法会帮我们完成对象的包装。使用ref方法创建了响应式对象后,在setup方法内要修改数据,就需要对myObject中的value属性值进行修改,value属性值是vue内部生成的,但是对于setup方法导出的数据来说,我们在模板中使用myObject数据已经是最终的独立值,可以直接使用。
<div class="app">
<h1>测试数据:{{myObj}}</h1>
<button @click="click">点击</button>
</div>
<script>
const app = Vue.createApp({
setup() {
//定义数据
let myObj = Vue.ref(0)
//定义点击事件
function click() {
myObj.value++
console.log(myObj.value);
}
//返回数据和方法
return {
myObj,
click
}
}
})
app.mount('.app')
</script>
结果:
vue还提供了一个名为toRefs的方法来支持响应式对象的解构赋值。
什么是解构赋值?
const myObj = {
name: 'zs',
age: 20,
sex: '男',
play() {
console.log('打游戏!');
}
}
const { name, age, sex, play } = myObj
console.log(name, age, sex);
play()
结果:
<div class="app">
<h1>测试数据:{{value}}</h1>
<button @click="click">点击</button>
</div>
<script>
const app = Vue.createApp({
setup() {
//定义数据
let myObj = Vue.reactive({
value: 1
})
//对myObj进行解构赋值
let { value } = myObj
//定义点击事件
function click() {
value++
console.log(value);
}
//返回数据和方法
return {
value,
click
}
}
})
app.mount('.app')
</script>
结果:
改写后的代码可以正常运行,但是已经失去了响应式,对于这种场景,我们可以使用vue中提供的toRefs方法来进行对象的解构,其会自动解构出的变量转换为ref变量,从而获得响应式。
<div class="app">
<h1>测试数据:{{value}}</h1>
<button @click="click">点击</button>
</div>
<script>
const app = Vue.createApp({
setup() {
//定义数据
let myObj = Vue.reactive({
value: 1
})
//对myObj进行解构赋值
let { value } = Vue.toRefs(myObj)
//定义点击事件
function click() {
value.value++
console.log(value);
}
//返回数据和方法
return {
value,
click
}
}
})
app.mount('.app')
</script>
结果:
注意,Vue.toRefs解构时,会将解构出的属性设置为key,vue内部会自动创建一个value属性,用于保存解构出的值。要想改变解构出的属性,必需调用 属性.value才能更改其值。
二、响应式的计算与监听
1、关于计算变量
<div class="app">
<h2>测试结果:{{sum}}</h2>
<button @click="click">点击</button>
</div>
<script>
const app = Vue.createApp({
setup() {
let a = Vue.ref(1)
let b = Vue.ref(2)
let sum = Vue.computed({
get() {
return a.value + b.value
},
set(value) {
a.value = value
b.value = value
}
})
function click() {
a.value += 1
b.value += 2
if (sum.value >= 10) {
sum.value = 0
}
}
return {
sum,
click
}
}
})
app.mount('.app')
</script>
结果:
2、监听响应式变量
有时候,当响应式变量发生变化时,我们需要监听其变化行为。在vue3中,watchEffect方法可以自动对其内部用到的响应式变量进行变化监听,由于其原理是在组件初始化时对所有依赖进行收集,因此在使用时无须手动指定要监听的变量。
<div class="app">
<div>{{a.value}}</div>
</div>
<script>
const app = Vue.createApp({
setup() {
let a = {
value: 1
}
Vue.watchEffect(() => {
console.log(a);
})
a.value = 2
return { a }
}
})
app.mount('.app')
</script>
结果:
在调用watchEffect方法时,其会立即执行传入的函数参数,并会追踪其内部的响应式变量,在其变更时再次调用此函数参数。需要注意,watchEffect在setup方法中被调用后,其会和当前组件的生命周期绑定在一起,组件卸载时会自动停止监听,如果需要手动停止监听。
<div class="app">
<div>{{a.value}}</div>
</div>
<script>
const app = Vue.createApp({
setup() {
let a = Vue.reactive({ value: 1 })
let stop = Vue.watchEffect(() => {
console.log(a.value);
})
stop()
a.value = 2
return { a }
}
})
app.mount('.app')
</script>
watch是一个与watchEffect类似的方法,与watchEffect方法相比,watch方法能够更加精准地监听指定的响应式数据的变化。
<div class="app">
<div></div>
</div>
<script>
const app = Vue.createApp({
setup() {
let a = Vue.ref(1)
Vue.watch(a, (newVal, oldVal) => {
console.log(newVal, oldVal);
})
a.value = 2
}
})
app.mount('.app')
</script>
结果:
watch方法比watchEffect方法强大的地方在于,其可以分别获取到变化前的值和变化后的值,也可以监听多个数据源。
<div class="app">
<div></div>
</div>
<script>
const app = Vue.createApp({
setup() {
let a = Vue.ref(1)
let b = Vue.reactive({ value: 2 })
Vue.watch([a, () => { return b.value }], ([newValA, newValB], [oldValA, oldValB]) => {
console.log(newValA, newValB, oldValA, oldValB);
})
a.value = 3
b.value = 4
}
})
app.mount('.app')
</script>
注意,通过Vue.reactive声明的变量需要使用箭头函数,watch才能监听到。
三、组合式API的应用
组合式API的使用能够帮助我们更好地梳理复杂组件的逻辑分布,能够从代码层面上将分离的相关逻辑点进行聚合,更适合进行复杂模块组件的开发。
1、关于setup方法
setup方法是组合式API的核心方法。
setup方法是组合式API功能的入口方法,如果使用组合式API模式进行组件开发,则逻辑代码都要编写在setup方法中。需要注意的是,setup方法会在组件创建之前执行,即对应组件的生命周期方法beforeCrete方法调用之前被执行。由于setup方法特殊的执行时机,除了可以访问组件的传参外部属性props之外,在其内部我们不能使用this来引用组件的其他属性,在set方法的最后,我们可以将定义的组件所需要的数据、函数等内容暴露给组件的其他选项。
setup方法可以接受两个参数:props和context。props术组件使用时设置的外部参数,其实响应式;context则是一个js对象,其中可用的属性有attrs、slots和emit。
<div class="app">
<com name="组件名"></com>
</div>
<script>
const app = Vue.createApp({})
app.component('com', {
template: `<div></div>`,
setup(props, context) {
//props属性值
console.log(props.name);
//属性
console.log(context.attrs);
//触发事件
console.log(context.emit);
//插槽
console.log(context.slots);
},
props: {
name: String
}
})
app.mount('.app')
</script>
结果:
注意,在setup方法中不要使用this关键字,setup方法中的this与当前组件实例并不是同一个对象。
2、在setup方法中定义生命周期行为。
setup中的生命周期定义方法需要在周期函数前加上on,注意:setup方法执行时机与beforeCreate和created执行时机基本一致。
组合和setup方法中窦定义了同样的生命周期方法,他们之间并不会冲突。
<div class="app">
<com></com>
</div>
<script>
const app = Vue.createApp({})
app.component('com', {
template: `<div></div>`,
setup() {
Vue.onMounted(() => {
console.log("setup中定义的mounted方法");
})
},
mounted() {
console.log('组件内定义的mounted方法');
}
})
app.mount(".app")
</script>
结果:
四、范例
实现支持搜索和筛选的用户列表
1、常规风格的示例工程开发
<div class="app">
<div class="radio">
<input type="radio" v-model="sex" :value="-1">全部
<input type="radio" v-model="sex" :value="0">男
<input type="radio" v-model="sex" :value="1">女
</div>
<div class="search">
搜索:<input type="search" v-model="searchKeyword">
</div>
<div class="table" style="margin:10px 0">
<table border=" 1" width="500px" align="center">
<tr>
<th>姓名</th>
<th>性别</th>
</tr>
<tr v-for="item in allData">
<td>{{item.name}}</td>
<td>{{item.sex==0?'男':'女'}}</td>
</tr>
</table>
</div>
</div>
<script>
let mock = [
{
name: '小王',
sex: 0
},
{
name: '小红',
sex: 1
},
{
name: '小张',
sex: 0
},
{
name: '小李',
sex: 1
}
]
const app = Vue.createApp({
data() {
return {
sex: -1,
searchKeyword: '',
allData: []
}
},
mounted() {
this.allData = mock
},
methods: {
sexFilter() {
this.searchKeyword = ''
if (this.sex == -1) {
this.allData = mock
} else {
this.allData = mock.filter((item) => {
return item.sex == this.sex
})
}
},
keywordFilter() {
this.sex = -1
this.allData = mock.filter((item) => {
return item.name.search(this.searchKeyword) != -1
})
}
},
watch: {
sex(oldVal, newVal) {
this.sexFilter()
},
searchKeyword(oldVal, newVal) {
this.keywordFilter()
}
}
})
app.mount(".app")
</script>
结果:
2、使用组合式API重构用户列表
<style>
.app {
width: 800px;
margin: auto;
text-align: center;
}
</style>
<body>
<div class="app">
<div class="radio">
<input type="radio" v-model="sex" :value="-1">全部
<input type="radio" v-model="sex" :value="0">男
<input type="radio" v-model="sex" :value="1">女
</div>
<div class="search">
搜索:<input type="search" v-model="searchKeyword">
</div>
<div class="table" style="margin:10px 0">
<table border=" 1" width="500px" align="center">
<tr>
<th>姓名</th>
<th>性别</th>
</tr>
<tr v-for="item in allData">
<td>{{item.name}}</td>
<td>{{item.sex==0?'男':'女'}}</td>
</tr>
</table>
</div>
</div>
<script>
let mock = [
{
name: '小王',
sex: 0
},
{
name: '小红',
sex: 1
},
{
name: '小张',
sex: 0
},
{
name: '小李',
sex: 1
}
]
const app = Vue.createApp({
setup() {
let sex = Vue.ref(-1)
let searchKeyword = Vue.ref("")
let allData = Vue.ref([])
Vue.onMounted(() => {
allData.value = mock
})
let sexFilter = () => {
searchKeyword.value = ""
if (sex.value == -1) {
allData.value = mock
} else {
allData.value = mock.filter((item) => {
return item.sex == sex.value
})
}
}
let searchKeywordFilter = () => {
sex.value = -1
allData.value = mock.filter((item) => {
return item.name.search(searchKeyword.value) != -1
})
}
Vue.watch(sex, sexFilter)
Vue.watch(searchKeyword, searchKeywordFilter)
return { sex, searchKeyword, allData }
}
})
app.mount(".app")
</script>