8. 数组更新的实现原理
数组更新实现原理
之前我们给每个属性添加了dep, 让dep去收集依赖
当使用push方法改变数组的时候, 并没有改变属性, 而是改变了数组本身
因此, 需要让数组,或对象和属性一样, 也能收集依赖, 并且在检测到数组变化的时候触发更新
在Observer 类中对每一个对象对进行依赖收集
class Observer {
constructor(data) {
// 给每个对象都进行依赖收集
this.dep = new Dep()
...
}
}
在定义响应式方法的时候, 如果对value的再次劫持有返回值, observe(value), 说明是一个对象或数组, 需要对对象或数组进行依赖收集, 如果有数组的嵌套, 则需要递归收集
function dependArray(value) {
for(let i = 0; i < value.length; i ++) {
let current = value[i];
// current 可能也是数组,或对象(对象不用管) 也可能不是数组,不是数组就没有__ob__属性
current.__ob__ && current.__ob__.dep.depend()
if(Array.isArray(current)) {
dependArray(current)
}
}
}
export function defineReactive(target, key, value) {
// 再次劫持
let childOb = observe(value) // 这个return的其实是一个Observer的实例, 只有对象和数组才有, 其他return false
// 给每个属性都加上dep, 这个空间不会被销毁, 是个闭包
let dep = new Dep()
// 再次劫持
Object.defineProperty(target, key, {
get() {
// 执行vm._render的时候会在这里取值, 此时Dep.target指向当前渲染watcher
if(Dep.target) {
// 如果有Dep.target, 执行dep上面的depend方法
dep.depend()
if(childOb) {
childOb.dep.depend()
if(Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
...
}
在数组发生改变时, 调用对应dep的notify方法,进行视图的更新
// 先copy旧的数组
let oldArrayProto = Array.prototype
// 定义并导出新的原型
export let newArrayProto = Object.create(oldArrayProto)
// 能改变数组的7种方法
let methods = ['push', 'pop', 'shift', 'unshift', 'reverse', 'sort', 'splice']
methods.forEach(method => {
newArrayProto[method] = function(...args) {
const result = oldArrayProto[method].call(this, ...args)
// 需要对新增的内容做处理, 新增的方法有 : push, unshift, 和 splice
let inserted
// 从实例上获取__ob__属性
let ob = this.__ob__
switch(method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
default:
break
}
// 方法里面的this, 是谁调用push, this指向谁
// 这里的this其实就是 Observer 里面 constructor里面的参数data, 在data里面添加一个__ob__属性, 指向类的实例, 就能从this上面获取observeArray了
if(inserted) {
ob.observeArray(inserted)
}
console.log('mrthios:', method)
ob.dep.notify() // 数组变化了, 也要触发更新
return result
}
})
vue中给不存在的属性赋值时, 不能实现更新, 有个专门的api $set
// a: {a: 1}
// vm.a.b = 100 不能更新
vm.a.__ob__.dep.notify()
// 这个就是$set的实现原理
具体代码
新建文件 6.array.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>array</title>
</head>
<body>
<div id="app">
{{arr}}
<div>{{a}}</div>
</div>
<script src="vue.js"></script>
<script>
// 新建一个vue实例
const vm = new Vue({
el: '#app',
data() {
return {
arr: [1,2,3, [4,5,6]],
a: {a: 1}
}
}
})
// 一般使用数组很少使用 arr[1] = 100, arr.length = 10 这两种方式修改数组, vue2同样也不支持
// vue2中实现数组响应式的方法是重写能改变数组的7个方法
// 特殊情况: 形如: arr: [1,2,3, {num: 100}], 这种数组里面有嵌套对象的, 也要能劫持对象的属性, 添加get和set
// 特殊情况2: arr: [1,2,3], arr.push({num: 20}), 在能改变数组的方法中, 如push一个对象, 那这个对象的属性也需要被劫持
// vm.arr.push(100)
// vm.arr[3].push(39)
vm.a.b = 20
// vm.a.__ob__.dep.notify()
</script>
</body>
</html>
observe/index.js
// 初次里面传过来的是一个对象, 后续可能是null, 或数据, 只需要对对象进行劫持, 数组不需要
// 就是传说中的Object.defineProperty方法, 不过就提的实现是在一个类中, 这里就是判断 + 返回一个class
import { newArrayProto } from "./array"
import { Dep } from "./dep"
// observe 方法 就是对对象进行监控, 添加set和get, 很多地方都有用到
export function observe(data) {
if(typeof data !== 'object' || data === null) {
return
}
// 如果有被标识, 直接返回data__ob__, 这个data__ob__就是一个Observer实例, 同下面一样(new Observer())
if(data.__ob__ instanceof Observer) {
return data.__ob__
}
// 这里的劫持使用的是一个类, 因为里面需要给被劫持的对象添加很多属性,
return new Observer(data)
}
class Observer {
constructor(data) {
// 给每个对象都进行依赖收集
this.dep = new Dep()
// 2) 在data上增加一个属性__ob__, 指向this,便于在array.js里面使用Observer里面的observeArray方法
// 同样这个属性可以作为一个标识, 表明属性是否有被劫持过, 可在observe方法中新增一个判断
// 注意这个属性不可枚举, 不然会死循环
Object.defineProperty(data, '__ob__', {
value: this,
enumerable: false
})
// 1) 针对对象和数组做区分
if(Array.isArray(data)){
// 数组需要实现两个内容:
// 1. 数组方法重写, 新建文件实现 array.js
// 2. 遍历数组, 劫持其中的对象, 并称响应式
data.__proto__ = newArrayProto
this.observeArray(data)
} else {
// 对对象的属性劫持放在walk方法中
this.walk(data)
}
}
// 遍历对象, 给每一个属性添加响应式, defineReactive不在class中, 因为其他地方也可能用到, 写了外面
walk(data) {
Object.keys(data).forEach(key => defineReactive(data, key, data[key]))
}
// 循环数组, 找出其中的对象(这异步已经封装在observe方法里面, 不用做), observe每一项
observeArray(data) {
data.forEach(item => observe(item))
}
}
function dependArray(value) {
for(let i = 0; i < value.length; i ++) {
let current = value[i];
// current 可能也是数组,或对象(对象不用管) 也可能不是数组,不是数组就没有__ob__属性
current.__ob__ && current.__ob__.dep.depend()
if(Array.isArray(current)) {
dependArray(current)
}
}
}
export function defineReactive(target, key, value) {
// 再次劫持
let childOb = observe(value) // 这个return的其实是一个Observer的实例, 只有对象和数组才有, 其他return false
// 给每个属性都加上dep, 这个空间不会被销毁, 是个闭包
let dep = new Dep()
// 再次劫持
Object.defineProperty(target, key, {
get() {
// 执行vm._render的时候会在这里取值, 此时Dep.target指向当前渲染watcher
if(Dep.target) {
// 如果有Dep.target, 执行dep上面的depend方法
dep.depend()
if(childOb) {
childOb.dep.depend()
if(Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set(newValue) {
if(value === newValue) return
observe(newValue)
value = newValue
// 设置新值的时候通知更新
dep.notify()
}
})
}
array.js
// 重写数组方法
// 不能干掉旧的方法
// 重新定义方法, 就是在执行push 方法的时候, 调用旧的push方法,注意this的指向问题,以及返回值 然后在添加一些自己的内容
// 对新增的内容(肯定是数组)进行observeArray ??? 注意进行observeArray是哪里来的
// 先copy旧的数组
let oldArrayProto = Array.prototype
// 定义并导出新的原型
export let newArrayProto = Object.create(oldArrayProto)
// 能改变数组的7种方法
let methods = ['push', 'pop', 'shift', 'unshift', 'reverse', 'sort', 'splice']
methods.forEach(method => {
newArrayProto[method] = function(...args) {
const result = oldArrayProto[method].call(this, ...args)
// 需要对新增的内容做处理, 新增的方法有 : push, unshift, 和 splice
let inserted
// 从实例上获取__ob__属性
let ob = this.__ob__
switch(method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
default:
break
}
// 方法里面的this, 是谁调用push, this指向谁
// 这里的this其实就是 Observer 里面 constructor里面的参数data, 在data里面添加一个__ob__属性, 指向类的实例, 就能从this上面获取observeArray了
if(inserted) {
ob.observeArray(inserted)
}
console.log('mrthios:', method)
ob.dep.notify() // 数组变化了, 也要触发更新
return result
}
})