使用场景
本章节需要完成 $set的定义以及完成前两节留下的一个bug,调用数组方法修改数组时没有响应式。
vue中两个不会触发响应式的修改数据方式:
- 通过数组的索引修改数组
- 向 data中添加新的属性
原因:
- 由于 vue通过修改 data中数组的原型链,在原型链中修改 7个改变数组的方法来劫持数组,并未对数组中的元素设置 getter、setter,导致通过索引修改数组,并不能被监听到。
- 新增的属性并未在初始化时设置 getter、setter,导致属性没有被劫持。
解决办法:以上两种场景通过 $set对数据进行修改。
实现思路:为数组和对象创建一个 __ob__属性来存储 Observer实例,并为Observer实例挂载一个 Dep实例来收集 watcher,并在 $set中手动触发。
$set
如果是数组,则将其添加到数组,并观察新增的值,如果是对象,则将其进行劫持。
$set(target, key, value) {
if (Array.isArray(target)) {
target[key] = value
observer(value)
} else {
defindReactive(target, key, value)
}
}
在 Observer实例在挂载一个 Dep实例,并将 Observer实例挂载到属性的 __ob__上
class Observer {
dep = new Dep()
constructor(data) {
// 略...
Object.defineProperty(data, '__ob__', {
value: this,
enumerable: false
})
}
// ...
}
如果是引用数据类型,则返回 Observer实例
function observer(data) {
const dataType = Object.prototype.toString.call(data)
if (dataType !== '[object Object]' && dataType !== '[object Array]') {
return
}
// 已经有 __ob__属性说明添加过 Observer实例,直接返回该实例
if (data.__ob__) return data.__ob__
return new Observer(data)
}
在 defindReactive函数中接收返回的 observer,并收集依赖。
function defindReactive(target, key, value) {
const ob = observer(value) // 接收返回的 Observer实例
const dep = new Dep()
Object.defineProperty(target, key, {
get() {
dep.depend()
ob?.dep.depend() // 如果 ob存在,则里面的 dep实例也对依赖进行收集
return value
},
set(val) {
// ...
}
})
}
手动调用
$set(target, key, value) {
if (Array.isArray(target)) {
target[key] = value
observer(value)
} else {
defindReactive(target, key, value)
}
target.__ob__.dep.notify() // 手动派发任务
}
// 略 ...
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)
this.__ob__.dep.notify() // 调用函数方法时,也需要手动触发
return res
}
return obj
}, Object.create(Array.prototype))
完整代码
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)
}
$set(target, key, value) {
if (Array.isArray(target)) {
target[key] = value
observer(value)
} else {
defindReactive(target, key, value)
}
target.__ob__.dep.notify()
}
}
function observer(data) {
const dataType = Object.prototype.toString.call(data)
if (dataType !== '[object Object]' && dataType !== '[object Array]') {
return
}
if (data.__ob__) return data.__ob__
return new Observer(data)
}
function defindReactive(target, key, value) {
const ob = observer(value)
const dep = new Dep()
Object.defineProperty(target, key, {
get() {
dep.depend()
ob?.dep.depend()
return value
},
set(val) {
if (val === value) return
dep.notify()
value = val
}
})
}
class Observer {
dep = new Dep()
constructor(data) {
if (Array.isArray(data)) {
data.__proto__ = arrayMethods
this.observeArray(data)
} else {
this.walk(data)
}
Object.defineProperty(data, '__ob__', {
value: this,
enumerable: false
})
}
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)
this.__ob__.dep.notify()
return res
}
return obj
}, Object.create(Array.prototype))