前言
在上篇文章中------Vue3响应式原理],我们基本了解了响应式的核心reactive
和effect
,话不多说,我们这节课将剩下的内容写完。
其实watch
和watchEffect
并不在reactivity
响应式模块里,而是在runtime-dom
模块里,那为啥还要在reactivity
这个响应式模块中,来介绍这两个API
呢?一是因为这两个API
我们在项目中太常见,二才是最主要的,是因为watch
和watchEffect
都是基于上篇文章说的effect
进行了封装,从而得到的。所以说么,effect
是最底层的方法,弄懂了上篇文章的内容,那么这篇文章就显得相对好理解很多。
watch
的实现
我们先在reactive.ts
和/shared/src/index.ts
中完善两个工具方法,方便我们在实现watch
时进行导入调用。
javascript
// reactive.ts文件
// 判断传入的值是不是一个响应式的值
export function isReactive(value) {
return !!(value && value[ReactiveFlags.IS_REACTIVE])
}
// shared/src/index.ts 文件
// 判断传入的值,是不是一个函数
export const isFunction = value => {
return value && typeof value === 'function'
}
然后在/reactivity/src
目录下新建apiWatch.ts
文件,来写watch
的主逻辑。首先我们简单回顾下Vue3
中watch
的常见用法:
javascript
const state = reactive({name: '张三', age: 18})
// 用法1:
watch(() => state.name, (newV, oldV) => {
console.log(newV, oldV)
})
// 用法2:
watch(state, (newV, oldV) => {
console.log(newV, oldV)
})
那么在用到watch
的时候,第一个参数我们可以传入一个函数(如用法1)来监听某个属性的变化,有朋友可能会问,为啥要写成一个函数,我直接把第一个参数传入state.name
不行么?醒醒,快醒醒!在这个案例中state.name
就是个定死的值张三
,监听常量,肯定是不会发生变化的啊;
同样,第一个参数还可以传入一个对象(如方法2)但是这种有几个问题,一般不推荐,比如当第一个参数传入的是对象,实际上watch
监听的是这个对象的引用地址,所以,无法区分newV
和oldV
,引用的地址是一直不变的,所以打印的结果会发现,这俩值是一样的,都是最新的值。还有个小问题就是,虽然你传入参数的是一个对象,但是在watch
方法的内部,依旧是遍历了这个对象所有的key
,并且进行取值操作(为的是触发依赖收集)。所以会对性能有所损耗,不过有时候为了方便,还是可以这么去干的(反正内部针对这种情况做了处理,代码写的爽就行了,管他呢)。
我们接下来实现watch
的逻辑:
typescript
// /reactivity/src/apiWatch.ts 文件
import { isReactive } from './reactive'
import { isFunction, isObject } from '@vue/shared'
import { ReactiveEffect } from './effect'
function traverse(value, seen = new Set()) {
if (!isObject(value)) {
return value
}
if (seen.has(value)) {
return value
}
seen.add(value)
for (const key in value) {
if (value.hasOwnProperty(key)) {
// 就是单纯的取了下值,比如state.name,为了触发reactive对象中属性的getter
traverse(value[key], seen)
}
}
return value
}
// 1. 首先导出watch方法
export function watch(source, cb, { immediate } = {} as any) {
// 2. 分两种情况判断,source是一个响应式对象和source是一个函数
// 这个getter就相当于是effect中的回调函数
let getter
if (isReactive(source)) {
// 3. 如果source是一个响应式对象,应该对source递归进行取值
getter = () => traverse(source)
} else if (isFunction(source)) {
getter = source
}
let oldValue
// 6. 当触发更新的时候,也就是属性改变的时候,才会执行这个job方法
const job = () => {
const newValue = effect.run()
cb(newValue, oldValue)
oldValue = newValue
}
// 4. 当new的时候,就会执行getter方法,开始进行依赖收集,数据变化后,会执行cb的回调方法
const effect = new ReactiveEffect(getter, job)
// 需要立即执行,那么就先执行一次任务
if (immediate) {
job()
}
// 5. 将立即执行的effect.run()的结果作为oldValue
oldValue = effect.run()
}
首版代码就算是完成了,代码虽然不多,但是为了方便理解,我们还是需要拆分每个步骤,来进行一一讲解。
- 步骤1:很好理解,导出的
watch
,放入传参,这里第三个参数options
我们只实现immediate
的功能; - 步骤2:就是上文提到的,对于传入的
source
,需要进行类型判断,如果是一个函数的话,那就让getter
赋值为这个函数;如果是对象的话,那就用函数包一层。 - 步骤3:但是单独包一层,并不会触发依赖收集,所以就需要对这个响应式对象
source
进行遍历,然后对每个key
进行取值,从而触发依赖收集;代码看上去的效果就是,只是取了下值,实际没有进行其他任何操作。为什么要包装成一个函数呢?别急,看到第4步就明白了。 - 步骤4:这步是不是非常熟?没错,在上篇写
effect
原理的时候,我们就是通过new ReactiveEffect(fn, options.scheduler)
进行生成的,所以,此步骤中,我们把getter
当成第一个参数进行传参,把job
当成第二个参数,也就是当响应式对象的属性发生变化时候,就会主动来调用job
方法,如果忘了,可以再去复习下上篇文章。 - 步骤5:
new
完后,得到的effect
,我们先执行一次effect.run
方法,就能拿到最开始的返回值,记为oldValue
。 - 步骤6:就是步骤4中需要传入的
job
方法,当响应式对象的属性,发生变化,才会执行这个方法,我们在其中调用cb
,并且传入oldValue
和newValue
,大功告成。
是不是发现,当我们理解了effect
方法原理之后,再去写watch
的实现,就变得非常简单了呢?所以说嘛effect
是底层方法,很多方法都是基于它进行封装的。
接下来,我们再介绍一个Vue3
中watch
提供的一个功能,所谓新功能,不是无缘无故就出来的,一定是为了解决相关的场景,所以才会提出的新功能,我们改动下index.html
中的示例代码,先看看如下场景,该用什么方法来解决:
html
<script>
const state = reactive({ name: '张三', age: 18 })
let timmer = 4000
function getData(data) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(data)
}, timmer -= 1000)
})
}
watch(() => state.age, async (newV, oldV) => {
const result = await getData(newV)
console.log(result)
app.innerHTML = result
})
// 我们这里直接改变响应式对象属性,模拟用户输入
state.age = 20
state.age = 30
state.age = 40
</script>
我们来简单说一下上边代码的含义,设想一下页面里有个输入框,每次输入内容,都会发送一个请求,我们这边模拟用户改变了3次值,所以一共发送了3次请求;第一个请求历时4秒钟能拿到返回结果,第二个请求历时3秒能拿到结果,第三个请求历时2秒能拿到结果,那么我们期望的页面显示内容,是以最后一次输入的结果为准,即页面上显示的是state.age = 40
的结果。但是根据我们现在的逻辑,会发现,页面上过2秒后确实显示的是state.age = 40
的结果,但是又过了1秒钟,state.age = 30
这个请求的结果又被显示到页面上,又过了1秒state.age = 20
的结果最终显示在了页面上,那显然不合理,我们的输入框中,最后明明是40
,但是页面显示的结果却是20
的请求结果。
所以我们此时需要来解决这个问题,我们第一反应就是,能不能在每次触发新请求的时候,屏蔽上次请求的结果呢?(注意,请求已经发送了,不能取消),这样,就能保证就算之前的请求,过了很久才拿到返回值,也不会覆盖最新的结果。那我们来在当前代码中,修改下吧!
html
<script>
const state = reactive({ name: '张三', age: 18 })
let timmer = 4000
// 1. 新建数组,用于存放上一次请求需要的方法
let arr = []
function getData(data) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(data)
}, timmer -= 1000)
})
}
watch(() => state.age, async (newV, oldV) => {
let fn
// 2. 每次发送请求前,利用闭包,将上次的结果flag改为false,从而屏蔽结果
while(arr.length){
fn = arr.shift()
fn()
}
// 3. 新建一个标识,为true才改变app.innerHTML的内容
let flag = true
// 4. 将flag = false的函数,存在arr数组中,方便下次请求前进行调用
arr.push(function(){ flag = false})
const result = await getData(newV)
console.log(result)
flag && (app.innerHTML = result)
})
// 我们这里直接改变响应式对象属性,模拟用户输入
state.age = 20
state.age = 30
state.age = 40
</script>
之后,我们在页面上再次打印结果,发现,页面上始终显示的是40,也就是最后state.age = 40
对应的结果。那么,我们通过在业务逻辑中,的一些代码改良,成功的解决了请求结果顺序错乱的问题。那么在Vue3
,watch
中提供了新的参数,可以把一些逻辑放在watch
的内部,从而达到和上述代码相同的效果,同样,我们先看用法,进而推导下在watch
源码中是如何实现的。
html
<script>
const state = reactive({ name: '张三', age: 18 })
let timmer = 4000
function getData(data) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(data)
}, timmer -= 1000)
})
}
// 第三个参数提供了onCleanup,用户可以传入回调
watch(() => state.age, async (newV, oldV, onCleanup) => {
let flag = true
onCleanup(() => {
flag = false
})
const result = await getData(newV)
console.log(result)
flag && (app.innerHTML = result)
})
// 我们这里直接改变响应式对象属性,模拟用户输入
state.age = 20
state.age = 30
state.age = 40
</script>
是不是发现,代码精简了很多?我们接下来实现一下吧!
typescript
// /reactivity/src/apiWatch.ts 文件
import { isReactive } from './reactive'
import { isFunction, isObject } from '@vue/shared'
import { ReactiveEffect } from './effect'
function traverse(value, seen = new Set()) {
if (!isObject(value)) {
return value
}
if (seen.has(value)) {
return value
}
seen.add(value)
for (const key in value) {
if (value.hasOwnProperty(key)) {
// 就是单纯的取了下值,比如state.name,为了触发reactive对象中属性的getter
traverse(value[key], seen)
}
}
return value
}
// 1. 首先导出watch方法
export function watch(source, cb, { immediate } = {} as any) {
// 2. 分两种情况判断,source是一个响应式对象和source是一个函数
// 这个getter就相当于是effect中的回调函数
let getter
if (isReactive(source)) {
// 3. 如果source是一个响应式对象,应该对source递归进行取值
getter = () => traverse(source)
} else if (isFunction(source)) {
getter = source
}
let oldValue
// 8. 创建cleanup变量,和onCleanup方法
let cleanup
const onCleanup = fn => {
cleanup = fn
}
// 6. 当触发更新的时候,也就是属性改变的时候,才会执行这个job方法
const job = () => {
// 9. 当cleanup存在,就调用我们onCleanup中传入的回调方法
if (cleanup) cleanup()
const newValue = effect.run()
// 7. 首先在cb中添加这个onCleanup参数
cb(newValue, oldValue, onCleanup)
oldValue = newValue
}
// 4. 当new的时候,就会执行getter方法,开始进行依赖收集,数据变化后,会执行cb的回调方法
const effect = new ReactiveEffect(getter, job)
// 需要立即执行,那么就先执行一次任务
if (immediate) {
job()
}
// 5. 将立即执行的effect.run()的结果作为oldValue
oldValue = effect.run()
}
看7、8、9三个步骤,其实就是类似于刚才我们写在外边的逻辑,只不过我们现在把这些逻辑写在了watch
内部,多读几遍,非常巧妙。
至此为止,关于watch
的核心逻辑,我们就已经写完了,是不是看起来,没有想象中的那么难呢?接下来我们还要实现下watchEffect
,莫慌,只需要改动几行代码,便可轻松实现。首先,我们将刚才导出的watch
改个名字换为doWatch
,变成一个通用函数,因为前文说过,watch
和watchEffect
都是基于effect
方法进行封装的,所以二者的逻辑可以说是非常相似的,所以我们没必要再写一遍,那么只要调用通用函数,根据传参不同,即可快速实现:
watchEffect
的实现
typescript
// /reactivity/src/apiWatch.ts 文件
import { isReactive } from './reactive'
import { isFunction, isObject } from '@vue/shared'
import { ReactiveEffect } from './effect'
function traverse(value, seen = new Set()) {
if (!isObject(value)) {
return value
}
if (seen.has(value)) {
return value
}
seen.add(value)
for (const key in value) {
if (value.hasOwnProperty(key)) {
// 就是单纯的取了下值,比如state.name,为了触发reactive对象中属性的getter
traverse(value[key], seen)
}
}
return value
}
// 1. 首先导出doWatch方法
export function doWatch(source, cb, { immediate } = {} as any) {
// 2. 分两种情况判断,source是一个响应式对象和source是一个函数
// 这个getter就相当于是effect中的回调函数
let getter
if (isReactive(source)) {
// 3. 如果source是一个响应式对象,应该对source递归进行取值
getter = () => traverse(source)
} else if (isFunction(source)) {
getter = source
}
let oldValue
// 8. 创建cleanup变量,和onCleanup方法
let cleanup
const onCleanup = fn => {
cleanup = fn
}
// 6. 当触发更新的时候,也就是属性改变的时候,才会执行这个job方法
const job = () => {
// 10. 根据传参不同,判断如果有回调函数的话,那么就是watch,如果没有cb那就是watchEffect
if (cb) {
// 9. 当cleanup存在,就调用我们onCleanup中传入的回调方法
if (cleanup) cleanup()
const newValue = effect.run()
// 7. 首先在cb中添加这个onCleanup参数
cb(newValue, oldValue, onCleanup)
oldValue = newValue
}else {
effect.run()
}
}
// 4. 当new的时候,就会执行getter方法,开始进行依赖收集,数据变化后,会执行cb的回调方法
const effect = new ReactiveEffect(getter, job)
// 需要立即执行,那么就先执行一次任务
if (immediate) {
job()
}
// 5. 将立即执行的effect.run()的结果作为oldValue
oldValue = effect.run()
}
// 导出watch和watchEffect方法
export function watch(source, cb, options) {
return doWatch(source, cb, options)
}
export function watchEffect(source, options) {
return doWatch(source, null, options)
}
改动点仅仅是第10步骤,加了一个判断,那么这样doWatch
就是一个通用函数,只需要根据传参不同,在外边再包一层,就是我们平时中项目常用的watch
和watchEffect
了!怎样,是不是很容易?那我们继续往下看吧
computed
的实现
我们还是简单用一下computed
,看看有哪几种用法:
html
<script>
const state = reactive({ name: '张三', age: 18 })
// 1. 可以传入对象,里边自定义get和set的逻辑
const info = computed({
get() {
console.log('我触发啦!')
return state.name + state.age
}
set(val){
console.log(val)
}
})
// 虽然取了2次值,但是只会打印一次'我触发了',因为computed有缓存的效果,依赖的值不变化,就不会多次触发get,要通过.value来取值
console.log(info.value)
console.log(info.value)
// 2. 传入函数,默认就相当于返回了一个get,取值要通过.value来取
const info = computed(() => {
return state.name + state.age
})
</script>
回顾了下基本用法后,我们还是在reactivity/src
目录下,新建computed.ts
文件,然后在reactivity/src/index.ts
中export * from '.computed'
,进行导出。接下来,我们便可以在computed.ts
中来实现computed
的逻辑了。
javascript
// reactivity/src/computed.ts 文件
import { isFunction } from '@vue/shared'
import { ReactiveEffect } from './effect'
class ComputedRefImpl {
public effect
public _value
public __v_isRef = true
constructor(getter, public setter) {
// 3. 还是通过new ReactiveEffect方法
this.effect = new ReactiveEffect(getter, () => {
})
}
// 4. 给value属性,创建get和set,再取值和赋值的时候,触发相应逻辑
get value() {
this._value = this.effect.run()
return this._value
}
set value(newV){
this.setter(newValue)
}
}
export function computed(getterOrOptions) {
// 1. 对传入的参数进行分类处理,对函数和对象进行不同的处理。
const isGetter = isFunction(getterOrOptions)
let getter, setter
if (isGetter) {
getter = isGetter
setter = () => ({})
} else {
getter = getterOrOptions.get
setter = getterOrOptions.set
}
// 2. 创建计算属性,返回一个响应式对象,访问的时候,通过.value的方式来访问
return new ComputedRefImpl(getter, setter)
}
我们来按步骤一一讲解下
- 步骤1:没错,非常熟悉的套路,和
watch
处理参数的方式几乎是一模一样; - 步骤2:返回一个响应式对象,获取
computed
的值,需要通过.value
的方法; - 步骤3:依旧是通过
new ReactiveEffect
,传入getter
进行依赖收集,生成effect
实例对象; - 步骤4:因为
computed
返回的对象,是通过.value
来访问的,所以要创建get set
,执行相应逻辑;
至此,我们的computed
就可以简单的用起来了,我们先运行一下,其他的问题,我们后边再来解决,我们改变下index.html
的代码,查看打印结果:
html
<script>
const state = reactive({ name: '张三', age: 18 })
const info = computed({
get() {
console.log('我调用啦!')
return state.name + state.age
},
set (val) {
console.log(val)
}
})
// 对info.value取两次值,查看结果
console.log(info.value)
console.log(info.value)
</script>
此时,我们会发现,控制台中打印的结果是:
复制代码我调用啦!
张三18
我调用啦!
张三18
这和我们平时用的computed
好像哪里有些不同?没错,info
中依赖的响应式对象state
中的属性,并没有变化,但是却触发了两次computed
,并没有实现缓存的效果,那么我们接下来就来实现一下吧!
javascript
// reactivity/src/computed.ts 文件
import { isFunction } from '@vue/shared'
import { ReactiveEffect } from './effect'
class ComputedRefImpl {
public effect
public _value
// 5. 创建一个_dirty变量,为true的时候就代表可以重新执行取值操作
public _dirty = true
constructor(getter, public setter) {
// 3. 还是通过new ReactiveEffect方法
this.effect = new ReactiveEffect(getter, () => {
// 7. 依赖的值变了,判断_dirty是否为false,为false的话,就把_dirty改为true
this_dirty = true
})
}
// 4. 给value属性,创建get和set,再取值和赋值的时候,触发相应逻辑
get value() {
// 6. 如果_dirty是true的话,才会重新执行`run`方法,重新取值,否则,直接返回原值
if (this._dirty) {
this._dirty = false
this._value = this.effect.run()
}
return this._value
}
set value(newV){
this.setter(newValue)
}
}
export function computed(getterOrOptions) {
// 1. 对传入的参数进行分类处理,对函数和对象进行不同的处理。
const isGetter = isFunction(getterOrOptions)
let getter, setter
if (isGetter) {
getter = isGetter
setter = () => ({})
} else {
getter = getterOrOptions.get
setter = getterOrOptions.set
}
// 2. 创建计算属性,返回一个响应式对象,访问的时候,通过.value的方式来访问
return new ComputedRefImpl(getter, setter)
}
我们看捕捉5~7,是不是通过一个_dirty
属性,就实现了如果依赖不发生变化,那么就不会多次触发computed
对象中的get
了呢?还是那句话,computed
的实现,依旧是依赖于effect
,所以理解effect
才是重中之重。
看起来是没啥问题了,但是在有一种场景下,存在着问题,我们改一下index.html
代码,来看一下:
html
<script>
const state = reactive({ name: '张三', age: 18 })
const info = computed({
get() {
console.log('我调用啦!')
return state.name + state.age
},
set (val) {
console.log(val)
}
})
effect(() => {
app.innerHTML = info.value
})
console.log(info.value)
setTimeout(() => {
state.age = 22
console.log(info.value)
}, 2000)
</script>
没错,就是当我们在effect
方法中,使用了computed
计算属性,那么页面就不会更新,因为effect
中并没有对计算属性进行依赖收集,而computed
计算属性中也没有对应的effect
方法。那怎么实现呢?我们想一想,是不是很类似于之前写的依赖收集track
和触发更新trigger
方法呢?没错,我们只需要在computed
中增加进行依赖收集和触发更新的逻辑就好了,而这两个逻辑,我们之前也写过,所以可以把通用的代码直接copy
过来:
javascript
// reactivity/src/computed.ts 文件
import { isFunction } from '@vue/shared'
import { ReactiveEffect, activeEffect, trackEffects, triggerEffects } from './effect'
class ComputedRefImpl {
public effect
public _value
public dep = new Set()
// 5. 创建一个_dirty变量,为true的时候就代表可以重新执行取值操作
public _dirty = true
constructor(getter, public setter) {
// 3. 还是通过new ReactiveEffect方法
this.effect = new ReactiveEffect(getter, () => {
// 7. 依赖的值变了,判断_dirty是否为false,为false的话,就把_dirty改为true
this_dirty = true
// 9. 触发更新
triggerEffects(this.dep)
})
}
// 4. 给value属性,创建get和set,再取值和赋值的时候,触发相应逻辑
get value() {
// 8. 如果计算属性在effect中使用的话,那也要做依赖收集
if (activeEffect) {
trackEffects(this.dep)
}
// 6. 如果_dirty是true的话,才会重新执行`run`方法,重新取值,否则,直接返回原值
if (this._dirty) {
this._dirty = false
this._value = this.effect.run()
}
return this._value
}
set value(newV){
this.setter(newValue)
}
}
export function computed(getterOrOptions) {
// 1. 对传入的参数进行分类处理,对函数和对象进行不同的处理。
const isGetter = isFunction(getterOrOptions)
let getter, setter
if (isGetter) {
getter = isGetter
setter = () => ({})
} else {
getter = getterOrOptions.get
setter = getterOrOptions.set
}
// 2. 创建计算属性,返回一个响应式对象,访问的时候,通过.value的方式来访问
return new ComputedRefImpl(getter, setter)
}
scss
// reactivity/src/effect.ts 文件
export function triggerEffects(dep) {
const effects = [...dep]
effects && effects.forEach(effect => {
// 正在执行的effect,不要多次执行,防止死循环
if (effect !== activeEffect) {
// 如果用户传入了scheduler,那么就执行用户自定义逻辑,否则还是执行run逻辑
if(effect.scheduler) {
effect.scheduler()
}else {
effect.run()
}
}
})
}
// computed中收集effect的依赖
export function trackEffects(dep) {
let shouldTrack = !dep.has(activeEffect)
if(shouldTrack) {
// 依赖和effect多对多关系保存
dep.add(activeEffect)
activeEffect.deps.push(dep)
}
}
我们看步骤8,9,依赖收集和触发更新的方法,我们依旧写在effect.ts
文件中(可以对比下trigger
和track
方法,逻辑几乎一模一样)。我们再运行刚才index.html
中的代码,发现页面成功的更新了,那么至此,computed
的核心逻辑我们就写完啦!
ref
的实现
我们在reactivity/src
目录下创建ref.ts
文件
typescript
import { isObject } from '@vue/shared'
import { activeEffect, trackEffects, triggerEffects } from './effect'
import { reactive } from './reactive'
function toReactive(value) {
return isObject(value) ? reactive(value) : value
}
class RefImpl {
public _value
public dep = new Set()
public __v_isRef = true
constructor(public rawValue) {
// 1. 判断传入的值,如果是对象类型,那么就将其包装成响应式对象
this._value = toReactive(rawValue)
}
get value () {
if(activeEffect) {
// 2. 进行依赖收集
trackEffects(this.dep)
}
return this._value
}
set value (newVal) {
if(newVal !== this.rawValue) {
this.rawValue = newVal
this._value = toReactive(newVal)
// 3. 进行触发更新
triggerEffects(this.dep)
}
}
}
有了前边的基础,写起ref
来,就显得非常得心应手,核心其实就这几行代码,通过注释,我们就不难发现,如果传入的是对象,那么就是利用了之前写的reactive
进行包装处理,如果传入了其他类型的数据,那么就和computed
中的方法一模一样,需要进行依赖收集和触发更新。
实现toRef
和toRefs
这两个方法,其实我们开发中,用的会比较少,所以还是先简单介绍下用法,然后再思考下如何实现,最后再来写一下它们的原理:
html
<script>
const state = reactive({name: '张三'}))
// 单独把name取出来
let name = state.name
effect(() => {
app.innerHTML = name
})
setTimeout(() => {
state.name = '李四'
}, 1000)
</script>
这是上边的代码可以看到,当我们将let name = state.name
单独取出来之后,再修改state.name
的值之后,name
的值就不会再发生变化了,页面上的名字也不会随之发生变化,也就是所谓的丢失响应式,那么利用toRef
就可以解决这种问题:
html
<script>
const state = reactive({name: '张三'}))
// 单独把name取出来
let name = toRef(state, 'name')
effect(() => {
app.innerHTML = name.value
})
setTimeout(() => {
state.name = '李四'
}, 1000)
</script>
我们来思考一下如何实现呢?为了不丢失响应式,所以就需要联系,那么肯定就是在name
和state.name
之间存在某种联系,当改变state.name
值的时候,从而能使得name
同步进行变动。既然这样,那不就可以做一层代理,当访问和修改name
的时候,实际是去访问和修改state.name
的值么?思路有了,我们便可以通过代码来实现:
typescript
// reactivity/src/ref.ts
import { isObject } from '@vue/shared'
import { activeEffect, trackEffects, triggerEffects } from './effect'
import { reactive } from './reactive'
function toReactive(value) {
return isObject(value) ? reactive(value) : value
}
class RefImpl {
public _value
public dep = new Set()
public __v_isRef = true
constructor(public rawValue) {
// 1. 判断传入的值,如果是对象类型,那么就将其包装成响应式对象
this._value = toReactive(rawValue)
}
get value () {
if(activeEffect) {
// 2. 进行依赖收集
trackEffects(this.dep)
}
return this._value
}
set value (newVal) {
if(newVal !== this.rawValue) {
this.rawValue = newVal
this._value = toReactive(newVal)
// 3. 进行触发更新
triggerEffects(this.dep)
}
}
}
// 导出ref
export function ref(value) {
return new RefImpl(value)
}
class ObjectRefImpl {
public __v_isRef = true
constructor(public _object, public _key) {
}
get value() {
return this._object[this._key]
}
set value(newVal) {
this._object[this._key] = newVal
}
}
// 导出toRef
export function toRef(object, key) {
return new ObjectRefImpl(object, key)
}
// 导出toRefs
export function toRefs(object) {
// 如果传入数组,就创建一个空数组,如果是对象,那就创建一个新对象
const ret = isArray(object) ? new Array(object.length) : {}
for (const key in object) {
ret[key] = toRef(object, key)
}
return ret
}
代码非常简单,就是进行了一次代理转化,而我们项目中常用的是toRefs
,也是遍历每个属性,并借助toRef
来实现的。
proxyRef
的实现
这个方法可能听起来很陌生,但是只要写过Vue3
的项目,就一定会用到这个方法,举个例子就明白了:
html
<template>
<div>{{ name }}</div>
</template>
<script>
let name = ref('张三')
</script>
当我们在代码中,用ref
声明了一个字符串类型的数据后,如果在代码中使用这个值,是不是需要通过name.value
的方式来调用呢?但是当我们在模板中使用的时候,却可以直接来用这个name
而并不需要再.value
来取值,诶,这就是Vue3
在模板编译的时候,内部调用了这个方法,帮助我们对ref
声明变量,进行自动脱钩,那么细心的朋友也发现了,不管是在computed
,还是ref
代码中,都有一样public __v_isRef = true
这个标识,没错,接下来就要用到这个标识了,这个标识就是为了在自动脱钩的时候,来进行分辨的。那么我们来实现这个proxyRef
方法吧~
typescript
// reactivity/src/ref.ts
import { isObject } from '@vue/shared'
import { activeEffect, trackEffects, triggerEffects } from './effect'
import { reactive } from './reactive'
function toReactive(value) {
return isObject(value) ? reactive(value) : value
}
class RefImpl {
public _value
public dep = new Set()
public __v_isRef = true
constructor(public rawValue) {
// 1. 判断传入的值,如果是对象类型,那么就将其包装成响应式对象
this._value = toReactive(rawValue)
}
get value () {
if(activeEffect) {
// 2. 进行依赖收集
trackEffects(this.dep)
}
return this._value
}
set value (newVal) {
if(newVal !== this.rawValue) {
this.rawValue = newVal
this._value = toReactive(newVal)
// 3. 进行触发更新
triggerEffects(this.dep)
}
}
}
// 导出ref
export function ref(value) {
return new RefImpl(value)
}
class ObjectRefImpl {
public __v_isRef = true
constructor(public _object, public _key) {
}
get value() {
return this._object[this._key]
}
set value(newVal) {
this._object[this._key] = newVal
}
}
// 导出toRef
export function toRef(object, key) {
return new ObjectRefImpl(object, key)
}
// 导出toRefs
export function toRefs(object) {
// 如果传入数组,就创建一个空数组,如果是对象,那就创建一个新对象
const ret = isArray(object) ? new Array(object.length) : {}
for (const key in object) {
ret[key] = toRef(object, key)
}
return ret
}
export function isRef(value) {
return !!(value && value.__v_isRef === true)
}
// 如果是ref则取ref.value
export function proxyRefs(objectWithRefs) {
return new Proxy(objectWithRefs, {
get(target, key, receiver) {
let v = Reflect.get(target, key, receiver)
return isRef(v) ? v.value : v
},
set(target, key, value, receiver) {
const oldValue = target[key]
// 老的值如果是个ref,那么实际上赋值的时候应该给他的.value进行赋值
if(oldValue.__v_isRef) {
oldValue.value = value
return true
}else {
// 其他情况,正常赋值
return Reflect.set(target, key, value, receiver)
}
}
})
}
这个proxyRef
方法,在后续文章中,会用到,这里只是提前介绍下这个方法。
结语
那么至此,我们reactivity
响应式模块中的一些个核心的方法,基本上已经实现了最核心的逻辑,这样,我们再去阅读源码的时候,就不会变得一头雾水了,好好再熟悉一遍reactivity
模块的方法吧,然后再去看下Vue3
源码中的reactivity
逻辑;我们接下来会继续分析Vue3
,其他模块的核心代码。
作者:柠檬soda水 链接:juejin.cn/post/720326... 来源:稀土掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。