持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第 10 天,点击查看活动详情
6.响应式原理-4.数组的处理方式
start
- 为什么需要单独处理数组?这篇文章来阅读一下对数组类型的数据处理
Observer
// 7. 如果是数组
if (Array.isArray(value)) {
// 7.1 可以使用对象的 __proto__ 属性
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
// 7.2 不可以使用对象的 __proto__ 属性
copyAugment(value, arrayMethods, arrayKeys)
}
// 7.3执行 observeArray
this.observeArray(value)
}
// hasProto
export const hasProto = '__proto__' in {}
- 判断是否是数组;
- 判断是否可以使用
对象的 __proto__ 属性; - 根据判断执行
protoAugment或者copyAugment; - 最后执行
observeArray
protoAugment && copyAugment
/**
* Augment a target Object or Array by intercepting
* the prototype chain using __proto__
*/
/**
* 1.
* 通过拦截来增强目标对象或数组
* 使用原型链的 __proto__
*/
// 这里的函数名可以翻译为 原始增加
function protoAugment(target, src: Object) {
/* eslint-disable no-proto */
// 2. 这里做的操作就是,把数组的原型指向了我们定义的新对象`arrayMethod` 。新对象的原型是数组正式的原型。
target.__proto__ = src
/* eslint-enable no-proto */
}
/**
* Augment a target Object or Array by defining
* hidden properties.
*/
/**
* 3.
* 通过定义来扩大目标对象或数组
* 隐藏属性
*/
/* istanbul ignore next */
// 这里的函数名可以翻译为 拷贝增加
function copyAugment(target: Object, src: Object, keys: Array<string>) {
// 4. 遍历我们定义的 7 种方法;
for (let i = 0, l = keys.length; i < l; i++) {
// 4.1 拿到 方法名
const key = keys[i]
// 4.2 给目标数组添加同名方法,
def(target, key, src[key])
}
}
protoAugment:将数组的隐式原型对象指向src,src就是我们定义的新对象arrayMethod;copyAugment:在需要处理的数组上添加我们定义的 7 种方法; 两个方法本身不难,主要需要做的弄懂传入的参数是什么。
看一下 arrayMethods, arrayKeys 这两个传入的参数是什么。
\src\core\observer\index.js
import { arrayMethods } from './array'
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
\src\core\observer\array.js
/*
* not type checking this file because flow doesn't play well with
* dynamically accessing methods on Array prototype
* 没有类型检查该文件,因为 flow 不能很好地发挥作用
* 动态访问数组原型的方法
*/
import { def } from '../util/index'
// 1. 数组的原型
const arrayProto = Array.prototype
// 2. 创建一个对象,原型指向数组的原型 `arrayMethods.__proto__ === Array.prototype` true
export const arrayMethods = Object.create(arrayProto)
// 3. 定义需要处理的数组的方法,这里可以看到改写了 7 种方法;
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse',
]
/**
* Intercept mutating methods and emit events
*/
// 4. 拦截变化的方法并发出事件;
// 遍历我们需要拦截的方法
methodsToPatch.forEach(function (method) {
// cache original method
// 5. 缓存 原本数组身上的方法,用来后续调用
const original = arrayProto[method]
// 6. 在 arrayMethods上; 定义push,pop,shift,unshift,splice,sort,reverse方法;
def(arrayMethods, method, function mutator(...args) {
// 7. 先触发 数组原本对应方法
const result = original.apply(this, args)
// 8. 获取到 数据实例上的 Observer实例。
const ob = this.__ob__
let inserted // inserted : 插入项
// 9. 选择方法
switch (method) {
case 'push':
case 'unshift':
// 9.1 push unshift 传入的参数都是需要存入数组的参数,所以直接 =
inserted = args
break
case 'splice':
// 9.2 splice 参数依次为 ①从何处处理 ②处理多少 ③要添加到数组的新元素 ,所以这里取第二个参数以后的参数。
inserted = args.slice(2)
break
}
// 10. 有新添加来的数据,需要处理成响应式的
if (inserted) ob.observeArray(inserted)
// notify change
// 11. 通知更改
// 这个地方着重注意一下,我们自身实现数组的 7 种方法,使用它们的时候,也会触发视图更新,根本原因,就是因为这里`ob.dep.notify();`
ob.dep.notify()
// 12. result存储的是什么? 存储的是数组本身对应的方法
return result
})
/*
所以最终返回的arrayMethod如下:
{
pop: ƒ mutator(...args)
push: ƒ mutator(...args)
reverse: ƒ mutator(...args)
shift: ƒ mutator(...args)
sort: ƒ mutator(...args)
splice: ƒ mutator(...args)
unshift: ƒ mutator(...args)
}
*/
})
整体代码看下来,就返回了一个对象,arrayMethods,结构如注释所示。
该对象有这么几个特点:
- 7 个数组同名方法名,作为 key 值;
arrayMethods隐式原型指向数组的显式原型,arrayMethods.__proto__ === Array.prototype;- 本质上是,拿到数组原本的方法,在数组原本方法的基础外,包一层,处理新添加进来的数据(
ob.observeArray(inserted)); 通知更新ob.dep.notify()
需要注意的是 ,这里的通知更新借助了数据上的
__ob__属性来访问 dep 从而通知更新。这就是__ob__存储了Observer实例的作用之一。
上述代码,就是对数组方法的处理,后续还有数组每一项的处理observeArray。
observeArray
/**
* Observe a list of Array items.
*/
// 11.观察Array项
observeArray(items: Array<any>) {
// 12. 遍历数组的每一项,全部都observe一下。
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i]);
}
}
这里主要是遍历数组,给每一项进行 observe()。
提一个问题,上面使用的 ob.dep 是什么时候初始化的?
/**
* Define a reactive property on an Object.
*/
// 1. 在对象上定义响应式属性
export function defineReactive(
obj: Object, // 传入的 对象:
key: string, // 对象的 属性;
val: any, //对象的属性值,在没有 getset的时候,直接返回对应的值。
customSetter?: ?Function, // 自定义 setter
shallow?: boolean // 是否是 浅层的响应式
) {
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
get: function reactiveGetter() {
if (Dep.target) {
dep.depend()
// 1. 子对象的 Observer实例,存在,子对象也收集依赖 (递归下去,就可以导致所有的属性都收集了依赖)
if (childOb) {
childOb.dep.depend()
// 2. 数组处理,数组的每一项并不会(observe), 所以如果是数组,手动变量在dep中收集依赖
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
}
function dependArray(value) {
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i];
// e存在; e.__ob__存在; e.__ob__.dep.depend()收集依赖;
e && e.__ob__ && e.__ob__.dep.depend();
if (Array.isArray(e)) {
dependArray(e);
}
}
}
有关 dep 后续我会专门写一篇文章研究研究。
思考
- 数组的响应式处理方式?
observe数组每一项的值;- 重写数组自带的 7 种方法:
'push','pop','shift','unshift','splice','sort','reverse',, 调用时: 1. 利用observeArray处理新增的项,2.利用__ob__.dep.notify来通知更新;
- 为什么要给我们的数据绑定一个
__ob__属性?
__ob__上存储的是Observer实例;Observer实例上又存储着依赖的dep绑定了__ob__属性,方便我们在代码中手动触发__ob__.dep.notify来通知更新。(例如:处理数组的方法,就有具体的使用案例) 我们熟悉的 $set 方法其实也会借助__ob__.dep.notify,来通知更新。
- 其他注意事项
- 这里可以看到 Vue.js 源码对 对象的
__proto__属性做了兼容处理。- 这里可以学习到如何重写数组原型上的方法。1.修改隐式原型; 2.直接暴力塞入对应方法; 以后如果有类似的需求可以模仿。
- Vue.js 只重写了
'push','pop','shift','unshift','splice','sort','reverse',这七种方法。 因为 Vue.js 重写了这些方法,所以是通过这几个方法新增了数据,新增的数据也是响应式的,放心使用。- 见到了
__ob__存储Observer实例的使用场景(之一)