数组的响应式
-
数组的依赖收集和发布在哪个环节进行
-
数组完成响应式和对象的区别
-
为什么数组的收集依赖的dep要初始化咋Observer中
首先要解决的就是数组的依赖收集和通知依赖更新应该在哪个环节
假设 data函数里面有
data(){
return {
arr:['singing' , 'dancing' , 'rap']
}
}
当使用这个数组的时候,或者是在template模板中使用,或者是
this.arr
。那么就意味着,数组的依赖收集也是在getter中触发的。那么如果是
this.arr =['hahaha' , 'heiheihei']
是在setter中通知更新的,另外如果是arr数组使用push、pop等方法的话,应该是在使用这个方法之后 然后通知依赖更新状态。
通过上面分析可以发现,数组除了setter/getter中的作用和object相同之外,还有就是array有方法,如push、pop等方法。这里我们也主要将这个数使用push方法之后,仍然能做到响应式。
据统计,vue中能够做到响应式的数组的方法有:'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reserve'
知道了能够做到的响应式的方法之后,我们就应该关注上面提到的在使用上面的这些方法之后,通知依赖更新状态,那么应该如何通知呢?
vue实际上对上面提到的方法做了拦截。
拦截器具体做法:
-
拿到Array上的prototype,prototype里面是有这些方法的!如下图
const arrayProtyo = Array.prototype // 这个对象里面存放的新的方法,里面的发放在下面是要经过拦截处理的 const arrayMethods = Object.create(arrayProtyo)
arrayMethods
的原型上面就是Array.prototype了 -
-
然后对
'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reserve'
这些方法进行拦截,具体做法如下['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reserve'].forEach(method => { // origin拿到的是原始的方法 const origin = arrayMethods[method] // 拦截 Object.defineProperty(arrayMethods, method, { enumerable: false, configurable: true, writable: true, value: function mutator (...arg) { return origin.applay(this, arg) } }) })
在上面对这几个方法进行了拦截 , 当执行
arr.push、arr.pop
等操作的时候,其实执行的是mutator函数,那么我们是不是就可以在这里面做通知依赖更新的事啦!['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reserve'].forEach(method => { // origin拿到的是原始的方法 const origin = arrayMethods[method] // 拦截 Object.defineProperty(arrayMethods, method, { enumerable: false, configurable: true, writable: true, value: function mutator (...arg) { let result = origin.applay(this, arg) //做其他的事 return result; } }) })
现在我们已经对这些方法进行了拦截,并且可以在里面做事了!
整体代码:
const arrayProtyo = Array.prototype
// 这个对象里面存放的新的方法,里面的发放在下面是要经过拦截处理的
const arrayMethods = Object.create(arrayProtyo)
['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reserve'].forEach(method => {
// origin拿到的是原始的方法
const origin = arrayMethods[method]
// 拦截
Object.defineProperty(arrayMethods, method, {
enumerable: false,
configurable: true,
writable: true,
value: function mutator (...arg) {
return origin.applay(this, arg)
}
})
})
使用拦截器覆盖Array的原型
data(){
return {
arr:['singing' , 'dancing' , 'rap']
}
}
以上我们已经做好了一个拦截器,这个拦截器里面的方法都被我们重写了,那么如果我们在data函数里面遇到属性的类型的数组的话,我们就应该将他们的原型换成arrayMethods
,如果将上面的arr数组的原型换成arrayMethods
,那么当其调用arr.push
的时候使用的是mutator方法
现在开始进行覆盖data的中类型为Array方法的原型:
首先我们先了解一下,一个Array实例是有__proto__
属性的(大部分浏览器支持,少部分不支持,一会说明不支持的解决办法)
我们只要将他们的__proto__
属性替换成arrayMethods
就可以了
class Observe {
constructor(value) {
this.value = value
// 如果是数组就要将原型指向我们进行拦截处理过的方法
if (Array.isArray(value)) {
value.__proto__ = arrayMethods
} else {
// walk还是函数执行的是对象的响应式哦
this.walk(value)
}
}
...
}
还有一种情况是__proto__
是没有的情况下,没有的,就直接将这些方法赋给那个数组。(关于def现在不要着急,继续往下看)
const hasProto = '__proto_' in {}
const methodsKey = Object.getOwnPropertyNames(arrayMethods);
class Observe {
constructor(value) {
this.value = value
if (Array.isArray(value)) {
const argument = hasProto ? protoArgument : copyArgument
argument(value, arrayMethods, methodsKey)
} else {
this.walk(value)
}
}
...
}
//支持__proto__属性
function protoArgument (target, methosd, keys) {
target.__proto__ = methosd
}
// 不支持 , 遍历keys的里面的属性名,加入到数组中
function copyArgument (target, methods, keys) {
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
def(target, key, methosd[key])
}
}
如何收集依赖
data(){
return {
arr:['dancing' , 'singing' , 'rap']
}
}
我们肯定要在template模板中或者是在使用代码的过程中用this.arr
,所以一定会触发getter的!!!那么就可以在defineReactive中进行处理。
可以发现,如果只有拦截器的话,我们什么事都做不了。因为我们创建拦截器的目的的为了在数组发生变化的时候能够得到一种能力,一种数组的内容中发生变化的时候,具有通知的能力。
像object的响应式一样,当使用到getter的时候,就会将使用这个属性的依赖加入到dep中。那么数组也应该如此,当我们使用了数组时候,也应该把依赖加入到dep中!!!
但是想一想是有区别的,想想object中的响应式,依赖的收集和通知都是发生在defineReactive中,意思就是,我们只要在这个函数中new一个Dep类就可以在这个函数中共用了。但是数组呢?
数组状态改变,要通知的
-
如果是赋值操作,是在defineReactive中
-
如果是使用push,pop等可以实现响应式的方法,是在拦截器中要拿到这个new的Dep类的吧,因为你使用了这个方法之后,也要通知更新依赖里的状态啊。
那么数组中new的Dep类就不能在defineReactive中了,要在Observe中的构造函数中
原因是因为我们上面讲到的,这个手机依赖的实例,必须要在拦截器和getter中同时都能访问到!
constructor(value) {
this.value = value
this.dep = new Dep()
// 如果是数组就要将原型指向我们进行拦截处理过的方法
if (Array.isArray(value)) {
value.__proto__ = arrayMethods
} else {
// walk还是函数执行的是对象的响应式哦
this.walk(value)
}
...
}
}
收集依赖
上面说到我们收集依赖是在defineReactive中,那么这一步主要也是在defineReactive中操作
defineReactive (data, key, value) {
// if (typeof (value) === 'object') new Observe()
let childOb = observe(value)
let dep = new Dep()
Object.defineProperty(value, key, {
configurable: true,
enumerable: true,
get () {
dep.append()
if (childOb) {
childOb.dep.append()
}
return value
},
set (newValue) {
if (value !== newValue) {
value = newValue
dep.notify()
}
}
})
}
/**
*
* @param {*} value
* @param {*} asRoot
* 尝试为value创建一个Observe实例
* 如果创建成功返回一个Observer实例
* 如果已经存在这个Observe实例,直接返回他
*/
function observe (value, asRoot) {
if (typeof value !== 'object') return
let ob
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observe) {
ob = value.__ob__
} else {
ob = new Observe(value)
}
return ob
}
在上面我们将if (typeof (value) === 'object') new Observe()
注释掉了,然后换上了let childOb = observe(value)
是用observer的主要目的就是,能够拿到observer实例,拿到了observer实例不就是意味着我们可以拿到实例中的dep对象了嘛。所以在这里就可以往执行数组的依赖收集操作。
那么问题来了,关于这个__ob__
这个属性我们并没有设置啊。那么接下来,我们也要改造Observe构造函数中的东西,把这个__ob__
属性加入到value中。
class Observe {
constructor(value) {
this.value = value
this.dep = new Dep()
// 这里的this就是Observe实例
def(value, '__ob__', this)
// 如果是数组就要将原型指向我们进行拦截处理过的方法
if (Array.isArray(value)) {
const argument = hasProto ? protoArgument : copyArgument
argument(value, arrayMethods, methodsKey)
} else {
// walk还是函数执行的是对象的响应式哦
this.walk(value)
}
}
}
function def (data, key, value, enumerable) {
Object.defineProperty(data, key, {
configurable: true,
enumerable: !!enumerable,
value: value,
writable: true
})
}
我们在创建Observe实例的时候,就会向value中加一个__ob__属性
,这个属性就是Observe的实例。那么就可以通过value.__ob__
属性来访问他的Observe实例了,就不需要担心拿不到里面的dep了。
另外,我们可以注意到,每个属性值,都会执行def这个函数,那么每个属性都会有__ob__
属性,而且这都是在劫持阶段,完成响应式的操作,那么就意味着所有可以进行响应式的数据,都会有__ob__
这个属性。
所以__ob__
属性的作用
-
可以拿到Observe实例
-
可以判断一个属性是不是响应式
上面我们已经说到了可以通过value.__ob__
拿到Observe实例,那么在拦截器中我们就可以用value.__ob__.dep
拿到收集依赖的Dep实例了。另外关于Array中的方法,当执行arr.push
的时候,会被拦截器拦截到,转到执行mutator去,那么mutator里面的this指向就是arr了,我们在这里面直接就可以拿到实例了
['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reserve'].forEach(method => {
// origin拿到的是原始的方法
const origin = arrayMethods[method]
// 拦截
Object.defineProperty(arrayMethods, method, {
enumerable: false,
configurable: true,
writable: true,
value: function mutator (...arg) {
// 新增
let ob = this.__ob__
// 通知更新
ob.dep.notify()
return origin.applay(this, arg)
}
})
})
侦测数组中元素的变化
上面我们做到的是对arr数组本身的变化的侦测,但是数组中保存的一些数据也要侦测的。比如数组中的元素如果是object对象,那么当这个对象里面的属性发生变化的时候,我们也应该通知。所以对数组里面的所有元素,我们也应该也要进行劫持操作
class Observe {
constructor(value) {
this.value = value
this.dep = new Dep()
// 这里的this就是Observe实例
def(value, '__ob__', this)
// 如果是数组就要将原型指向我们进行拦截处理过的方法
if (Array.isArray(value)) {
//新增
this.observeArray(value)
} else {
// walk还是函数执行的是对象的响应式哦
this.walk(value)
}
}
observeArray (value) {
const argument = hasProto ? protoArgument : copyArgument
argument(value, arrayMethods, methodsKey)
for (let i = 0; i < this.value.length; i++) {
observe(value[i])
}
}
}
将数组中的每一个元素都执行一遍observer函数,那么里面所有的子元素都会变成响应式
新增元素的响应式
我们只对数组里面所有元素做成了响应式,但是如果是增加进去的元素也要变成响应式的。能够增加的有push、splice、unshift
的操作
所以我们要在拦截器那里对这三个做相应的处理
['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reserve'].forEach(method => {
// origin拿到的是原始的方法
const origin = arrayMethods[method]
// 拦截
// Object.defineProperty(arrayMethods, method, {
// enumerable: false,
// configurable: true,
// writable: true,
// value: function mutator (...arg) {
// // 新增
// let ob = this.__ob__
// ob.dep.notify()
// return origin.applay(this, arg)
// }
// })
def(arrayMethods, method, function mutator (...args) {
let insert
switch (method) {
case 'push':
case 'unshift': insert = args; break;
// splice(index, num , ...) splice的第一个参数是位置,第二个参数是数量,从第三个参数开始是插入的数据
case 'splice': insert = args.slice(2); break
}
let ob = this.__ob__
if (insert) ob.observeArray(insert)
ob.dep.notify()
return origin.applay(this, arg)
})
})
总结:
数组完成响应式的思路
-
将能够实现响应式的方法
push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reserve
进行重写, -
将重写后的方法,放在数组的原型上(data中的数组)
-
收集依赖,依然在defineReactive中,但是收集依赖,更新依赖的地方不都是在defineReactive(对象的响应式是在这里)中,所以要在getter中拿到依赖,并且在上面所有的重写的方法(其实是对这些方法用Object.defiinePropert进行了重新定义 , 执行上面的方法就是执行mutator),也就是mutator里面,也要进行通知依赖的操作。所以在defineReactive中,对判断如果是对象继续进行劫持进行了重新写,重写的observer函数,既可以让每个响应式的数据都有
__ob__
属性,也能根据value.__ob__
来拿到Observer实例拿到收集数组依赖的dep。 -
要对数组中的每项都进行侦测,并且,如果数组里面是对象的话,递归侦测
-
如果是push , splice , unshift进去的东西,要将他们做成响应式,通过使用observer完成
前两篇文章的全部代码
class Dep {
constructor() {
this.subs = []
}
addSub (value) {
this.subs.push(value)
}
append () {
if (window.target) {
this.addSub(window.target)
}
}
removeSub () {
remove(this.subs, sub)
}
notify () {
const temp = this.subs.slice()
for (let i = 0; i < temp.length; i++) {
temp[i].updata()
}
}
}
function remove (subs, removeItem) {
if (subs.length) {
let index = subs.indexOf(removeItem)
if (index !== -1) {
subs.splice(index, 1)
}
}
}
const arrayProtyo = Array.prototype
// 这个对象里面存放的新的方法,里面的发放在下面是要经过拦截处理的
const arrayMethods = Object.create(arrayProtyo)
['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reserve'].forEach(method => {
// origin拿到的是原始的方法
const origin = arrayMethods[method]
def(arrayMethods, method, function mutator (...args) {
let insert
switch (method) {
case 'push':
case 'unshift': insert = args; break;
// splice(index, num , ...) splice的第一个参数是位置,第二个参数是数量,从第三个参数开始是插入的数据
case 'splice': insert = args.slice(2); break
}
let ob = this.__ob__
ob.dep.notify()
if (insert) ob.observeArray(insert)
return origin.applay(this, arg)
})
})
const hasProto = '__proto_' in {}
const methodsKey = Object.getOwnPropertyNames(arrayMethods);
class Observe {
constructor(value) {
this.value = value
this.dep = new Dep()
// 这里的this就是Observe实例
def(value, '__ob__', this)
// 如果是数组就要将原型指向我们进行拦截处理过的方法
if (Array.isArray(value)) {
const argument = hasProto ? protoArgument : copyArgument
argument(value, arrayMethods, methodsKey)
this.observeArray(value)
} else {
// walk还是函数执行的是对象的响应式哦
this.walk(value)
}
}
observeArray (value) {
for (let i = 0; i < this.value.length; i++) {
observe(value[i])
}
}
walk (value) {
let keys = Object.keys(value)
for (let i = 0; i < keys.length; i++) {
this.defineReactive(value, keys[i], value[keys[i]])
}
}
defineReactive (data, key, value) {
// if (typeof (value) === 'object') new Observe()
let childOb = observe(value)
let dep = new Dep()
Object.defineProperty(value, key, {
configurable: true,
enumerable: true,
get () {
dep.append()
if (childOb) {
childOb.dep.append()
}
return value
},
set (newValue) {
if (value !== newValue) {
value = newValue
dep.notify()
}
}
})
}
}
//支持__proto__属性
function protoArgument (target, methods, keys) {
target.__proto__ = methods
}
// 不支持 , 遍历keys的里面的属性名,加入到数组中
function copyArgument (target, methods, keys) {
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
def(target, key, methods[key])
}
}
function def (data, key, value, enumerable) {
Object.defineProperty(data, key, {
configurable: true,
enumerable: !!enumerable,
value: value,
writable: true
})
}
function observe (value, asRoot) {
if (typeof value !== 'object') return
let ob
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observe) {
ob = value.__ob__
} else {
ob = new Observe(value)
}
return ob
}
// watcher
let reg = /^\w.$/
function parsePath (path) {
if (reg.test(path)) return
const sefment = path.split('.')
console.log(sefment);
// sefment=['a' , 'b' , 'c']所以应该是先是a , 然后是a.b, a.b.c
return function (obj) {
for (let i = sefment; i < sefment.length; i++) {
if (!obj) return
obj = obj[obj[i]]
}
return obj
}
}
class Watcher {
constructor(vm, path, cb) {
this.vm = vm;
// this.getter返回的是一个函数,这个函数执行的时候会获取obj.a.b.c里面的数据
this.getter = parsePath(path);
this.cb = cb
this.value = this.get()//初始化获取到的数据,如果没有更新状态,这里是新的,更新了状态之后这里存的是旧值,要用get到的新值覆盖
}
get () {
window.target = this
// 在下面的一步中this.vm执行继承了getter函数(其实this.vm指的就是这个组件),并且执行了该函数,将this.vm作为参数穿进去,另外这一步是获取值的过程,在获取的时候会触发我们定义的响应式里的收集依赖的操作,这个时候window.target是this,也就是watcher这个实例!!将window.target收集完了之后,将其置为空。第二个参数this.vm,把这个组件上所有的挂载的数据事件等传过去
let value = this.getter.call(this.vm, this.vm)
window.target = undefined
return
}
// 上面说到了,状态dep只是通知到watcher中,而watcher应该来通知所有的来更新,另外存储在this.value中的数据是旧的,get到的才是新的
updata () {
const oldValue = this.value;
this.value = this.get()
this.cb.call(this.vm, this.value, oldValue)
}
}