《Vue.JS零基础入门到进阶教程》(11)Vue 中用于侦听响应式数据变化的 API vue watch的实现
Vue watch 实现原理 watch 是 Vue 中用于侦听响应式数据变化的 API。其核心实现基于 effect
本文参考视频教程,点击这里查看
1 基本概念
1) 什么是watch
watch叫侦听器, 侦听某个响应式数据, 当数据改变时, 重新执行对应的回调
所以, watch可以建立数据->函数的对应关系
2) 基本使用
第一种: 接收引用了属性的副作用函数做为参数
<!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>Document</title>
<script src="./vue.js"></script>
</head>
<body>
<script>
const { reactive, watch } = Vue
const state = reactive({ name: 'xiaoming', address: { city: '武汉' } })
// watch侦听副作用函数(引用代理对象的属性)
watch(
() => state.name,
() => {
console.log('只有当侦听的某个属性改变时, 才执行')
}
)
setTimeout(() => {
// 只有当state.name属性改变时, 才执行
state.name = 'xiaopang'
// state.address.city = '北京' // 修改city不会触发更新
}, 1000)
</script>
</body>
</html>
第二种: 接收响应式对象做为参数
- watch侦听响应式对象时, 默认是深度侦听
- watch对应的回调默认不执行, 只有当属性改变时执行
<!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>Document</title>
<script src="./vue.js"></script>
</head>
<body>
<script>
const { reactive, watch } = Vue
const state = reactive({ name: 'xiaoming' })
// watch接收响应式对象的情况
watch(state, () => {
console.log('该函数默认不执行, 只有状态更新时执行...')
})
setTimeout(() => {
state.name = 'xiaopang'
}, 1000)
</script>
</body>
</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>Document</title>
<script src="./vue.js"></script>
</head>
<body>
<script>
const { reactive, watch } = Vue
const state = reactive({ name: 'xiaoming', address: { city: '武汉' } })
// watch接收响应式对象的情况
watch(state, () => {
console.log('该函数默认不执行, 只有状态更新时执行...')
})
setTimeout(() => {
// 侦听对象时, 是深度侦听
state.address.city = '北京'
}, 1000)
</script>
</body>
</html>
2 实现侦听函数
watch的实现实际上跟effect非常类似
effect接受两个参数
- 属性关联的副作用函数(类似watch的第一个参数)
- 更新时执行的调度函数(类似watch的第二个参数)
因此, 我们考虑基于effect初步实现watch的功能
如果第一个参数就是一个副作用函数, watch跟effect的参数一致
function watch(source, cb) {
effect(source, {
scheduler() {
cb()
},
})
}
3 实现侦听对象
1) 基本实现
如果第一个参数不是副作用函数, 可以将其包装成一个副作用函数
function watch(source, cb) {
let getter
if (typeof source === 'function') {
getter = source
} else {
getter = () => source
}
effect(getter, {
scheduler() {
cb()
},
})
}
由于并没有触发代理对象的取值操作, 因此不会收集依赖
考虑实现一个函数, 遍历访问source中的每一个属性
function traverse(value) {
for (const k in value) {
// 将代理对象的每个属性访问一次
value[k]
}
}
function watch(source, cb) {
let getter
if (typeof source == 'function') {
getter = source
} else {
getter = () => traverse(source)
}
effect(getter, {
scheduler() {
cb()
},
})
}
2) 支持嵌套
如果代理对象中存在嵌套情况
- 在reactive中要递归为嵌套的对象创建代理
- 在traverse中要递归访问嵌套的属性
示例
get(target, key) {
if (key == '__v_isReactive') return true // 新增
// console.log(`自定义访问${key}`)
// 收集依赖
track(target, key)
if (typeof target[key] == 'object') {
// 递归处理对象类型
return reactive(target[key])
}
return target[key]
},
function traverse(value) {
for (const k in value) {
if (typeof value[k] == 'object') {
traverse(value[k])
} else {
// 将代理对象的每个属性访问一次
value[k]
}
}
}
3) 解决循环引用问题
如果按上述写法, 会出现递归循环引用, 陷入死循环
什么是循环引用
如果一个对象的某个属性引用自身, 在递归时会死循环
问题示例
const state = reactive({ name: 'xiaoming', address: { city: '武汉' } })
state.test = state
// watch接收响应式对象的情况
watch(state, () => {
console.log('该函数默认不执行, 只有状态更新时执行...')
})
setTimeout(() => {
// 侦听对象时, 是深度侦听
state.address.city = '北京'
}, 1000)
以上问题可以简化为
const obj = {
foo: 'foo',
}
// obj的一个属性引用obj本身, 出现循环引用问题
obj.bar = obj
function traverse(value) {
for (const k in value) {
if (typeof value[k] == 'object') {
traverse(value[k])
} else {
// 将代理对象的每个属性访问一次
value[k]
}
}
}
traverse(obj)
为了避免递归循环引用陷入死循环, 改造traverse方法
function traverse(value, seen = new Set()) {
// 如果这个对象已经被遍历过了, 直接返回
if (typeof value !== 'object' || seen.has(value)) return
// 将遍历的对象记录下来, 再递归时判断
seen.add(value)
for (let key in value) {
// 这里取值, 会触发proxy对象的getter操作
traverse(value[key], seen)
}
}
4 实现新旧值
我们知道, 在watch的回调中可以获取新值和旧值. 是如何实现的呢?
基本使用
<!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>Document</title>
<script src="./vue.js"></script>
</head>
<body>
<script>
const { reactive, watch } = Vue
const state = reactive({ name: 'xiaoming', address: { city: '武汉' } })
watch(
() => state.name,
(newValue, oldValue) => {
console.log(newValue, oldValue)
}
)
setTimeout(() => {
state.name = 'xiaopang'
}, 1000)
</script>
</body>
</html>
具体实现
function watch(source, cb) {
let getter
if (typeof source == 'function') {
getter = source
} else {
getter = () => traverse(source)
}
let oldValue, newValue
const _effect = effect(getter, {
lazy: true, // 手动控制run的时机
scheduler() {
newValue = _effect.run()
cb(newValue, oldValue)
oldValue = newValue
},
})
oldValue = _effect.run()
}
5 立即执行的回调
默认情况下, watch中的回调是不执行的. 但是可以通过传入参数让其立即执行
第一次立即执行回调时, 拿到的旧值是undefined
基本使用
<!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>Document</title>
<script src="./vue.js"></script>
</head>
<body>
<script>
const { reactive, watch } = Vue
const state = reactive({ name: 'xiaoming', address: { city: '武汉' } })
watch(
() => state.name,
(newValue, oldValue) => {
console.log('设置immediate选项会立即执行回调')
console.log(newValue, oldValue)
},
{ immediate: true }
)
setTimeout(() => {
state.name = 'xiaopang'
}, 1000)
</script>
</body>
</html>
具体实现
立即执行的函数和更新时执行的函数本质上是没有区别的
因此, 我们可以将scheduler封装起来
function watch(source, cb, options = {}) {
let getter
if (typeof source == 'function') {
getter = source
} else {
getter = () => traverse(source)
}
let oldValue, newValue
const job = () => {
newValue = _effect.run()
cb(newValue, oldValue)
oldValue = newValue
}
const _effect = effect(getter, {
lazy: true,
scheduler: job,
})
if (options.immediate) {
job()
} else {
oldValue = _effect.run()
}
}
6 watchEffect
watchEffect也可以用于侦听属性的改变.
基本用法
- 接受副作用函数作为参数
- 自动收集依赖
- 不关心新旧值
<!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>Document</title>
<script src="./vue.js"></script>
</head>
<body>
<script>
const { reactive, watchEffect } = Vue
const state = reactive({ name: 'xiaoming' })
watchEffect(() => {
console.log(state.name)
})
setTimeout(() => {
state.name = 'xiaopang'
}, 1000)
</script>
</body>
</html>
具体实现
watchEffect跟effect是非常类似的.
由于只接受一个副作用函数作为参数. 注册和更新时都执行这个函数.
基本逻辑可以跟watch复用.
function doWatch(source, cb, options = {}) {
let getter
if (typeof source == 'function') {
getter = source
} else {
getter = () => traverse(source)
}
let oldValue, newValue
const job = () => {
if (cb) {
newValue = _effect.run()
cb(newValue, oldValue)
oldValue = newValue
} else {
_effect.run()
}
}
const _effect = effect(getter, {
lazy: true,
scheduler: job,
})
if (options.immediate) {
job()
} else {
oldValue = _effect.run()
}
}
function watch(source, cb, options = {}) {
doWatch(source, cb, options)
}
function watchEffect(source, options = {}) {
doWatch(source, null, options)
}
若有收获,就点个赞吧!
持续更新webgis开发相关技术/面试/就业内容
关注我学习webgis开发不迷路👇👇👇
更多推荐


所有评论(0)