Vue3之响应式
Posted 英杰丶
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Vue3之响应式相关的知识,希望对你有一定的参考价值。
1. 响应式基础
1.1 如何创建一个响应式对象或数组(数组本质上也是对象)呢?(如何让一个普通对象使其具有响应性呢?)
使用 vue 的reactive()
函数。
import reactive from 'vue';
//这行代码可以放在<script setup> 或者在 setup() 函数中
const state = reactive(count : 0);
state
就是响应式对象。
1.2 如何使用这个响应式对象呢?
在 <script setup>
中的顶层的导入和变量声明,可在同一模板中直接使用。
<template>
<!-- 直接使用state.count -->
<div>当前的值:state.count</div>
</template>
<script setup>
import reactive from "vue";
const state = reactive(count:0);
//延迟5秒后,count数加1
setTimeout(()=>
state.count +=1;//直接使用
,5000)
</script>
1.3 如果一个普通对象的某个深层属性也是对象或数组,这个普通对象在生成响应式对象时,这个普通对象的深层对象或数组响应式的吗?
reactive()
创建的响应式对象都是深层的,深层对象或数组也是响应式的,他们的改变都能被监测到。响应式对象内的对象依然是代理。
<template>
<!-- 直接使用state.count -->
<div>当前的值:state.count</div>
<div>当前的值:state.taskInfo.status</div>
</template>
<script setup>
import reactive from "vue";
const state = reactive(count:0,taskInfo:status:"未开始");
state.taskInfo= status:"暂停中";//赋值新对象后,state.taskInfo属性还是响应式的
setTimeout(()=>
state.count +=1;
state.taskInfo.status ="进行中";
,5000)
</script>
1.4 普通对象属性值改变后,界面上也会同步显示吗?
不会,因为原对象就是一个普通对象,通过reactive()
生成的是一个代理对象,可以理解为有一个对象包裹住这个原对象,这个对象就叫原对象的包装对象,响应性是通过这个包装对象实现的。所以普通对象的属性值改变后不会触发更新。
import reactive from "vue";
const obj = count:0,taskInfo:status:"未开始",username:['张三','李四'];
const state = reactive(obj);
//obj.count = 15;如果在这里改值的话 界面上显示的值直接就变成15了 ??
setTimeout(()=>
obj.count = 5;//改变原始对象的属性值
obj.taskInfo = status:"暂停中"
obj.username =['马冬梅']
console.log(obj.count)
console.log(obj.taskInfo)
console.log(obj.username)
console.log(state.count)//响应式对象的值也变了,输出的是5,但是不会触发DOM刷新,界面上显示还是原来的值 0,下同
console.log(state.taskInfo)
console.log(state.username)
,5000)
1.5 reactive()
函数存在一些局限
- 仅对对象类型有效,原始类型无效。
- 我们必须始终保持对改响应式对象的引用。如果赋值为新的,那么将和旧的引用断开响应性连接。
1.6 对于原始类型,我们如何把他们变成响应式呢?
使用ref()
可以创建任何值类型的响应式ref
,ref()
函数会将传入的值包装成一个带有value
属性的ref
对象,value
属性的值就是传入的值。当传入的值是对象,将会调用reactive()
自动转换他的.value
。ref
能让我们创造一种对任意值得引用,并且在不丢失响应性的前提下传递这些引用。这是因为传递的都是这个ref
对象。
import ref from "vue";
const btnState = ref(0)
setTimeout(()=>
btnState.value = 1
,5000)
1.7 如何使用这个ref
对象呢?
在模板中属于顶层属性时就不需要解包,他们会自动解包。如果不是顶层属性则不行。我们可以通过解构把他们变为顶层属性。
<template>
<!-- 必须得加上value 不然显示的是[Obejct Object]1,而不是李四1-->
用户名:obj2.username.value+'1'
<!-- 这个就不需要加value了 ,因为username本身就是顶级属性了-->
用户名:username+'1'
</template>
<script setup>
import ref from "vue";
const obj2 = username:ref('张三')
setTimeout(()=>
obj2.username.value = '李四'
,5000)
const username = obj2;
</script>
当这个ref
对象在深层响应式对象中,作为属性被访问时也会自动解包,就像普通对象属性一样。但是浅层响应式对象则不行。
<template>
<!-- 这个就不需要加value了 ,因为是在深层响应式对象中的一个属性,自动解包-->
年龄:obj3.age+10
</template>
<script setup>
import ref from "vue";
const obj3 = reactive(age:ref(20))
setTimeout(()=>
obj3.age = 50;//不用加value 自动解包
,5000)
</script>
当ref
对象是响应式数组的一个元素时,使用这个ref
对象也不会自动解包,需要加上value
1.8 不是所有的情况下都会自动解包,每个ref
对象要加一个value
会很麻烦。我们可以使用语法糖来替换。
将ref()改为$ref()即可。
const btnState = $ref(0)
const obj2 = username:$ref('张三')
要使用语法糖,需要显示启用。
vite.config.js
需要 “@vitejs/plugin-vue”>=“^2.0.0”,
export default defineConfig(
plugins: [
vue(
reactivityTransform:true,//显示启用语法糖,配置完之后要重启
),
]
);
2.响应式进阶–组合式API
2.1 setup()
基本使用
setup()
是一个钩子函数,是在组件中使用组合API的入口,当我们在选项式API的组件中使用组合API时需要这个函数,组合API都要写在这个函数内。我们在这个函数内使用组合API声明响应式状态,比如使用ref()
、reactive()
等函数,然后在这个函数返回的对象会暴露这些响应式状态给模板和组件实例。其他的选项可以通过组件实例访问了。
<script>
import ref from 'vue';
export default
setup()
const username = ref('张三');
return
username//在调用这个属性不用加value,因为在暴露的时候自动解包了
,
methods:
getName()
console.log(this.username);//使用this就可以直接调用暴露的属性,
return this.username;
,
mounted()
this.getName();
</script>
<template>
<div>username</div>
</template>
setup()
函数有参数 ,第一个参数是组件的props
。和标准组件一致,这个props
也是响应式的,在传入新的props
时会同步更新。如果解构了props
对象,那么解构出的变量就会失去响应性,所以推荐使用props.xxx
的形式使用props
.
export default
props:
username,
,
setup(props)
console.log(props.username);
如果需要解构props
,或者将某个prop
传到外部函数并保持响应性,可以使用toRefs()
或toRef()
函数。
export default
props:
username,
,
setup(props)
const username = toRefs(props);
//username 是一个ref对象,value是props.username
第二个参数是一个Setup上下文对象,这个对象暴露了一些可能在setup
中用到的值。上下文对象是非响应式的,可以解构。attrs
和 slots
都是有状态的对象,它们总是会随着组件自身的更新而更新。attrs
和 slots
的属性都不是响应式的。
export default
setup(props, context)
// 透传 Attributes(非响应式的对象,等价于 $attrs)
console.log(context.attrs)
// 插槽(非响应式的对象,等价于 $slots)
console.log(context.slots)
// 触发事件(函数,等价于 $emit)
console.log(context.emit)
// 暴露公共属性(函数)
console.log(context.expose)
2.2 组合式API核心函数
2.2.1 ref()
- 参数:所有值类型,如果传的参数是一个对象,那么这个对象将通过
reactive()
转为具有深层次响应式的对象。 - 返回值:响应式的
ref
对象,此对象内部只有指向内布置的属性value
。所有对value
的操作都将被追踪。 - 语法糖: $ref()
- 例子:可以参照前面举的例子。
2.2.2 computed()
- 参数:
- 接收一个
getter
函数,返回一个只读的响应式ref
对象。 - 接收一个带有
get
和set
函数的对象,返回一个可写的ref
对象。
- 接收一个
- 返回值:返回一个
ref
对象,根据不同参数返回只读或者可写的ref
对象。该ref
通过属性value
暴露getter
函数的返回值。 - 语法糖:$computed()
- 例子
<template>
<div>One的值:comCountOne</div>
<div>Two的值:comCountTwo</div>
</template>
<script setup>
import computed from "vue";
let count = $ref(0)
//传入一个getter函数 ,返回一个只读的ref对象
const comCountOne = computed(()=>
return count++;
)
//传入一个带有get和set的对象,返回一个可写的ref对象
const comCountTwo = computed(
get()
return count
,
set(newVale)
count=newVale + 5;
)
setTimeout(()=>
comCountTwo.value = 100
,5000)
</script>
2.2.3 reactive()
- 参数:一个对象
- 返回值:一个对象的响应式代理对象
- 例子:可以参照前面举的例子。
2.2.4 readonly()
- 参数:接受一个对象,无论是普通对象还是
ref
对象 - 返回值:返回一个原值的只读代理,只读代理是深层的,任何嵌套属性的访问都将是只读的。
2.2.5 watchEffect()
- 参数:
- 第一个参数是要运行的副作用函数。这个副作用函数的参数也是一个函数,这个函数用来注册清理回调。清理回调会在该副作用函数下一次执行钱被调用,可以用来清理无效的副作用。
- 第二个参数是可选的选项。用来调整副作用函数的刷新时机或调试副作用的依赖。默认情况下,侦听器在组件渲染之前执行,设置
flush:'post'
将会是侦听器延迟到组件渲染之后再执行(watchPostEffect()
函数)。在某些特殊情况下可能需要响应式依赖发生改变时立即触发侦听器,这可以通过flush:'sync'
来实现(watchSyncEffect()
函数)。
- 返回值:返回一个用来停止该副作用的函数。
- 例子:
import watchEffect from "vue";
let count = $ref(0)
watchEffect(()=>
console.log(count);
)
count++;
2.2.6 watch()
- 参数:
- 第一个参数,一个或多个响应式数据源,在数据源变化时会调用所给的回调函数。
watch()
是懒监听,只有当数据源变化时才执行回调函数。- 一个函数,返回一个值
- 一个
ref
- 一个响应式对象
- 由以上类型组成的数组
- 第二个参数,发生变化时要调用的回调函数,这个函数有三个参数:新值,旧值,以及一个用于注册副作用清理的回调函数,可以清楚无效的副作用。当监听多个来源时,回调函数接收两个数组,分别对应源数组中的新值和旧值。
- 第三个参数,是可选的,一个对象,有以下选项:
immediate
: 在侦听器创建时立即触发回调。第一次调用时旧值是undefined
deep
: 如果源是对象,强制深度遍历,一遍在深层级变更时触发回调。flush
: 调整回调函数的刷新时机。onTrack
、onTrigger
:调试侦听器的依赖。
- 第一个参数,一个或多个响应式数据源,在数据源变化时会调用所给的回调函数。
- 返回值:返回一个用来停止该副作用的函数。
import watch,ref,reactive from "vue";
let one = ref(0)//用$ref()创建的ref对象无法使用watch
const state = reactive(count:0)
//监听ref对象
watch(one,(val,oldVal)=>
console.log("--"+val)
console.log("--"+oldVal)
)
//一个getter函数
watch(()=>return state.count,(count,oldCount)=>
console.log(count)
console.log(oldCount)
)
//监听多个来源
watch([one,state],([newValOne,newState],[old1,old2])=>
console.log("-newValOne-"+newValOne)
console.log("-newState-"+newState.count)
console.log("-old1-"+old1)
console.log("-old2-"+old2.count)
)
setTimeout(()=>
one.value = 100;
state.count = 1000;
,5000)
2.3 组合API工具函数
2.3.1 isRef()
- 参数:要检查的值,判断这个值是否为
ref
对象 - 返回值:
true
代表是,否则不是
2.3.2 unref()
- 参数:值
- 返回值:如果参数是
ref
对象,则返回内部值,否则返回参数本身。
2.3.3 toRef()
基于响应式对象上的一个属性,创建一个对应的ref
对象。这样创建的ref
与其源属性保持同步。
- 参数:
- 第一个参数,响应式对象
- 第二个参数,响应式对象的属性名称
- 返回值:
ref
对象 - 语法糖:
$toRef()
,提示未声明? - 例子:
<template>
<div>addressRef的值:addressRef</div>
<div>user.address的值:user.address</div>
</template>
<script setup>
import watch,ref,reactive,toRef from "vue";
const user = reactive(
username:'张三',
address:'长春'
)
const addressRef = toRef(user,'address')
setTimeout(()=>
user.address = "松原"
,5000)
setTimeout(()=>
addressRef.value = "白山"
,10000)
</script>
2.3.4 toRefs()
将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是指向源对象相应属性的ref
对象。每个ref
都是使用toRef()
创建的。toRefs()
在调用时只会为源对象上可以枚举的属性创建ref
,对可能不存在的不会。
- 参数:响应式对象
- 返回值:普通对象
- 例子:
const user = reactive(
username:'张三',
address:'长春'
)
const userOfRefs = toRefs(user);
const usernameRef,addressRef = userOfRefs;//解构也不会丢失响应性
2.3.5 isProxy()
检查一个对象是否有reactive()
、readonly()
、shallowReactive()
、shallowReadonly()
创建的代理
2.3.6 isReactive()
检查一个对象是否有reactive()
、shallowReactive()
创建的代理
2.3.7 isReadonly()
检查一个对象是否有readonly()
、shallowReadonly()
创建的代理
2.4 组合式API–依赖注入
2.4.1 provide()
提供一个值,可以被后代组件注入。provide()
要和setup()
同步调用。
- 参数:
- 第一个参数,要注入的key,可以是一个字符串或一个
symbol
- 第二个参数,是要注入的值。这个值可以是
ref
对象,后代组价可以由此和提供者建立响应式的联系。
- 第一个参数,要注入的key,可以是一个字符串或一个
- 返回值:无
- 例子:
import provide, reactive,ref from 'vue'
const obj = reactive(
user:
username:'张三',
address:'长春'
,
task:
completeNumber:0,
craft:
version:1
)
provide('obj',obj)
const countRef = ref(0)
provide('count',countRef)
setTimeout(()=>
countRef.value = 1000;
obj.user.address='成都'
obj.task.completeNumber = 100
obj.task.craft.version = 2.4
,5000)
2.4.2 应用层Provide
vue实例也有一个provide()
函数,此函数提供的数据在该应用内的所有组件中都可以注入。
2.4.3 inject()
在当前组件注入上层组件提供的数据。inject()
要和setup()
同步调用。
- 参数:
- 第一个参数,key值。就是祖先组件提供的数据的key。如果祖先组件上有多个组件对同一个key提供了值,那么近的组件会覆盖远的组件提供的值,如果没有会返回
undefined
或默认值。 - 第二个参数,默认值。
- 如果默认值是一个函数,那么需要传第三个参数,这个参数传的值为
false
- 第一个参数,key值。就是祖先组件提供的数据的key。如果祖先组件上有多个组件对同一个key提供了值,那么近的组件会覆盖远的组件提供的值,如果没有会返回
- 返回值:返回祖先组件提供的数据。
- 例子:
<template>
<div>countRef</div>
<div>地址:obj.user.address</div>
<div>数量:obj.task.completeNumber + 10 </div>
<div>版本: obj.task.craft.version</div>
</template>
<script setup>
import inject from "vue";
const obj = injecthttps://www.bilibili.com/video/av51444410/?p=5
https://github.com/amandakelake/blog/issues/63
https://mp.weixin.qq.com/s/X3s4ysLfwclEOXIuKzOK2g
Vue 进阶系列之响应式原理及实现
以下文章来源于高级前端进阶 ,作者木
高级前端进阶
木易杨,资深前端工程师,跟着我每周重点攻克一个前端面试重难点。接下来让我带你走进高级前端的世界,在进阶的路上,共勉!
(给前端大全加星标,提升前端技能)
转自: 高级前端进阶
什么是响应式Reactivity
Reactivity表示一个状态改变之后,如何动态改变整个系统,在实际项目应用场景中即数据如何动态改变Dom。
需求
现在有一个需求,有a和b两个变量,要求b一直是a的10倍,怎么做?
简单尝试1:
let a = 3;let b = a * 10;console.log(b); // 30
乍一看好像满足要求,但此时b的值是固定的,不管怎么修改a,b并不会跟着一起改变。也就是说b并没有和a保持数据上的同步。只有在a变化之后重新定义b的值,b才会变化。
a = 4;console.log(a); // 4console.log(b); // 30b = a * 10;console.log(b); // 40
简单尝试2:
将a和b的关系定义在函数内,那么在改变a之后执行这个函数,b的值就会改变。伪代码如下。
onAChanged(() => {
b = a * 10;
})
所以现在的问题就变成了如何实现onAChanged
函数,当a改变之后自动执行onAChanged
,请看后续。
结合view层
现在把a、b和view页面相结合,此时a对应于数据,b对应于页面。业务场景很简单,改变数据a之后就改变页面b。
<span class="cell b"></span>document
.querySelector(‘.cell.b‘)
.textContent = state.a * 10
现在建立数据a和页面b的关系,用函数包裹之后建立以下关系。
<span class="cell b"></span>onStateChanged(() => { document
.querySelector(‘.cell.b’)
.textContent = state.a * 10})
再次抽象之后如下所示。
<span class="cell b">
{{ state.a * 10 }}
</span>
onStateChanged(() => {
view = render(state)
})
view = render(state)
是所有的页面渲染的高级抽象。这里暂不考虑view = render(state)
的实现,因为需要涉及到DOM结构及其实现等一系列技术细节。这边需要的是onStateChanged
的实现。
实现
实现方式是通过Object.defineProperty
中的getter
和setter
方法。具体使用方法参考如下链接。
MDN之Object.defineProperty
需要注意的是get
和set
函数是存取描述符,value
和writable
函数是数据描述符。描述符必须是这两种形式之一,但二者不能共存,不然会出现异常。
实例1:实现convert()
函数
要求如下:
-
1、传入对象obj
作为参数
-
2、使用Object.defineProperty
转换对象的所有属性
-
3、转换后的对象保留原始行为,但在get或者set操作中输出日志
示例:
const obj = { foo: 123 }
convert(obj)
obj.foo // 输出 getting key "foo": 123obj.foo = 234 // 输出 setting key "foo" to 234obj.foo // 输出 getting key "foo": 234
在了解Object.defineProperty
中getter
和setter
的使用方法之后,通过修改get
和set
函数就可以实现onAChanged
和onStateChanged
。
实现:
function convert (obj) { // 迭代对象的所有属性
// 并使用Object.defineProperty()转换成getter/setters
Object.keys(obj).forEach(key => {
// 保存原始值
let internalValue = obj[key]
Object.defineProperty(obj, key, {
get () {
console.log(`getting key "${key}": ${internalValue}`) return internalValue
},
set (newValue) {
console.log(`setting key "${key}" to: ${newValue}`)
internalValue = newValue
}
})
})
}
实例2:实现Dep
类
要求如下:
-
1、创建一个Dep
类,包含两个方法:depend
和notify
-
2、创建一个autorun
函数,传入一个update
函数作为参数
-
3、在update
函数中调用dep.depend()
,显式依赖于Dep
实例
-
4、调用dep.notify()
触发update
函数重新运行
示例:
const dep = new Dep()
autorun(() => {
dep.depend() console.log(‘updated‘)
})// 注册订阅者,输出 updateddep.notify()// 通知改变,输出 updated
首先需要定义autorun
函数,接收update
函数作为参数。因为调用autorun
时要在Dep
中注册订阅者,同时调用dep.notify()
时要重新执行update
函数,所以Dep
中必须持有update
引用,这里使用变量activeUpdate
表示包裹update的函数。
实现代码如下。
let activeUpdate = null function autorun (update) { const wrappedUpdate = () => {
activeUpdate = wrappedUpdate // 引用赋值给activeUpdate
update() // 调用update,即调用内部的dep.depend
activeUpdate = null // 绑定成功之后清除引用
}
wrappedUpdate() // 调用}
wrappedUpdate
本质是一个闭包,update
函数内部可以获取到activeUpdate
变量,同理dep.depend()
内部也可以获取到activeUpdate
变量,所以Dep
的实现就很简单了。
实现代码如下。
class Dep { // 初始化
constructor () {
this.subscribers = new Set()
} // 订阅update函数列表
depend () { if (activeUpdate) {
this.subscribers.add(activeUpdate)
}
} // 所有update函数重新运行
notify () {
this.subscribers.forEach(sub => sub())
}
}
结合上面两部分就是完整实现。
实例3:实现响应式系统
要求如下:
-
1、结合上述两个实例,convert()
重命名为观察者observe()
-
2、observe()
转换对象的属性使之响应式,对于每个转换后的属性,它会被分配一个Dep
实例,该实例跟踪订阅update
函数列表,并在调用setter
时触发它们重新运行
-
3、autorun()
接收update
函数作为参数,并在update
函数订阅的属性发生变化时重新运行。
示例:
const state = { count: 0}
observe(state)
autorun(() => { console.log(state.count)
})// 输出 count is: 0state.count++// 输出 count is: 1
结合实例1和实例2之后就可以实现上述要求,observe
中修改obj
属性的同时分配Dep
的实例,并在get
中注册订阅者,在set
中通知改变。autorun
函数保存不变。 实现如下:
class Dep { // 初始化
constructor () {
this.subscribers = new Set()
} // 订阅update函数列表
depend () { if (activeUpdate) {
this.subscribers.add(activeUpdate)
}
} // 所有update函数重新运行
notify () {
this.subscribers.forEach(sub => sub())
}
}function observe (obj) { // 迭代对象的所有属性
// 并使用Object.defineProperty()转换成getter/setters
Object.keys(obj).forEach(key => { let internalValue = obj[key] // 每个属性分配一个Dep实例
const dep = new Dep() Object.defineProperty(obj, key, {
// getter负责注册订阅者
get () {
dep.depend() return internalValue
}, // setter负责通知改变
set (newVal) { const changed = internalValue !== newVal
internalValue = newVal
// 触发后重新计算
if (changed) {
dep.notify()
}
}
})
}) return obj
}let activeUpdate = nullfunction autorun (update) { // 包裹update函数到"wrappedUpdate"函数中,
// "wrappedUpdate"函数执行时注册和注销自身
const wrappedUpdate = () => {
activeUpdate = wrappedUpdate
update()
activeUpdate = null
}
wrappedUpdate()
}
结合Vue文档里的流程图就更加清晰了。
Job Done!!!
本文内容参考自VUE作者尤大的付费视频
以上是关于Vue3之响应式的主要内容,如果未能解决你的问题,请参考以下文章