侦听器
当监听的属性改变时,会触发相应的回调函数,多次改变时,只会触发一次,并且是异步执行。
实现思路:为 data中的每一个属性都创建一个 Dep实例来存储 Watcher实例,在属性的 getter函数中收集依赖,setter函数中派发任务。当属性改变时会触发 setter函数,进而对 Dep实例中的 Watcher实例进行循环调用触发回调。
Dep
Dep实例主要依赖收集依赖和派发任务。
class Dep {
// 存储 watcher
subs = []
// 收集 watcher
depend() {
if (Dep.target) {
this.subs.push(Dep.target)
}
}
// 调用 watcher的run方法触发回调
notify() {
for (const watcher of this.subs) {
// 触发回调
watcher.run()
}
}
}
// ...
function defindReactive(target, key, value) {
observer(value)
const dep = new Dep() // 为每一个属性创建dep
Object.defineProperty(target, key, {
get() {
dep.depend() // 收集依赖
return value
},
set(val) {
if (val === value) return
dep.notify() // 派发任务
value = val
}
})
}
Watcher
Watcher实例主要用来存储回调并触发。在实例化时将实例挂载到 Dep.target上方便对其进行获取,再读取属性触发属性的 getter方法,从而对watcher进行收集。
class Watcher {
constructor(vm, key, cb) {
this.vm = vm
this.key = key
this.cb = cb
this.get()
}
get() {
Dep.target = this
this.vm[this.key] // 读取属性,触发属性的 getter函数
Dep.target = null
}
run() {
this.cb.call(this.vm)
}
}
初始化
vue有两种方法创建侦听器,一是在 watch对象中定义,二是调用 $watch方法创建。
class Vue {
constructor(options) {
// 略...
this.initData()
this.initWatch() // 在 initData后调用,需要依赖 initData创建的 Dep实例。
}
// 略...
initWatch() {
const watch = this.$options.watch
if (!watch) return
const keys = Object.keys(watch)
for (const key of keys) {
this.$watch(key, watch[key])
}
}
$watch(key, cb) {
new Watcher(this, key, cb)
}
}
改进
此时 watch的主要功能已经实现,但还有两点需要改进.。
- watch的回调是异步调用
- 多次触发时,只会执行一次
异步
通过 Promise的 then方法将回调加入到异步队列 或 queueMicrotask
run() {
Promise.resolve().then(() => {
this.cb.call(this.vm)
})
}
多次触发
实现思路:定义一个数组来收集待执行的 watcher的id,如果已经在数组中,则不再触发回调。
let watcherId = 0
const watcherQueue = []
class Watcher {
constructor(vm, key, cb) {
// ...
this.id = watcherId++
// ...
}
run() {
if (watcherQueue.includes(this.id)) return
watcherQueue.push(this.id) // 数组中添加待执行 watcher的id
Promise.resolve().then(() => {
this.cb.call(this.vm)
// 回调执行完,在数组中删除对应watcher的id
watcherQueue.splice(watcherQueue.indexOf(this.id), 1)
})
}
}
完整代码
class Vue {
constructor(options) {
const data = options.data
this.$options = options
this._data = typeof data === 'function' ? data() : data
this.initData()
this.initWatch()
}
initData() {
const data = this._data
observer(data)
const keys = Object.keys(data)
for (const key of keys) {
Object.defineProperty(this, key, {
get() {
return data[key]
},
set(value) {
data[key] = value
}
})
}
}
initWatch() {
const watch = this.$options.watch
if (!watch) return
const keys = Object.keys(watch)
for (const key of keys) {
this.$watch(key, watch[key])
}
}
$watch(key, cb) {
new Watcher(this, key, cb)
}
}
function observer(data) {
const dataType = Object.prototype.toString.call(data)
if (dataType !== '[object Object]' && dataType !== '[object Array]') {
return
}
// 对象、数组处理...
new Observer(data)
}
function defindReactive(target, key, value) {
// 递归观察 value
observer(value)
const dep = new Dep()
Object.defineProperty(target, key, {
get() {
dep.depend()
return value
},
set(val) {
if (val === value) return
dep.notify()
value = val
}
})
}
class Observer {
constructor(data) {
if (Array.isArray(data)) {
data.__proto__ = arrayMethods
this.observeArray(data)
} else {
this.walk(data)
}
}
// 处理对象
walk(data) {
const kyes = Object.keys(data)
for (const key of kyes) {
defindReactive(data, key, data[key])
}
}
// 处理数组
observeArray(arr) {
arr.forEach(observer)
}
}
class Dep {
subs = []
depend() {
if (Dep.target) {
this.subs.push(Dep.target)
}
}
notify() {
for (const watcher of this.subs) {
watcher.run()
}
}
}
let watcherId = 0
const watcherQueue = []
class Watcher {
constructor(vm, key, cb) {
this.vm = vm
this.key = key
this.cb = cb
this.id = watcherId++
this.get()
}
get() {
Dep.target = this
this.vm[this.key]
Dep.target = null
}
run() {
if (watcherQueue.includes(this.id)) return
watcherQueue.push(this.id)
Promise.resolve().then(() => {
this.cb.call(this.vm)
watcherQueue.splice(watcherQueue.indexOf(this.id), 1)
})
}
}
const methods = ['push', 'pop', 'shift', 'unshift', 'reverse', 'sotr', 'splice']
const arrayMethods = methods.reduce((obj, method) => {
obj[method] = function (...args) {
const res = Array.prototype[method].apply(this, args)
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
default:
break
}
inserted?.forEach(observer)
return res
}
return obj
}, Object.create(Array.prototype))