前言
Vue 将被侦听的数组的变更方法进行了包裹,所以它们也将会触发视图更新。这些被包裹过的方法包括:
push()
pop()
shift()
unshift()
splice()
sort()
reverse()
已知Vue通过data中的数据进行递归遍历,然后用Object.defineProperty
对其设置存取描述符从而达到响应式。然而其实在上述步骤时,是针对数据类型是对象的变量时采取的方式。
在针对数据类型为数组的数据,Vue会采用另一种处理方式,在给数组中的元素设置响应式的同时,给该数组的变更方法进行响应式增强。下面来分析一下,Vue是如何对数组进行处理的:
Observer类
首先要知道,Vue针对每个data都用Observer
类进行处理,我们先从Vue的构造函数出发,分析流程是如何走到Obeserver
这一步的。
src\core\instance\index.js
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
// 注册vm的_init()方法
initMixin(Vue)
// 注册vm的$data/$props/$set/$delete/$watch
stateMixin(Vue)
// 事件相关 $on/$once/$off/$emit
eventsMixin(Vue)
// 初始化声明周期相关的混入方法
lifecycleMixin(Vue)
// $nextTick/_render
renderMixin(Vue)
export default Vue
从构造函数可知,初始化时会调用实例的_init
方法,而该方法在initMixin
函数中设置的,接下来看initMixin
函数。
src\core\instance\init.js
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
// ... 省略
// vm状态的初始化
initState(vm)
// ... 省略
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
这个函数代码过多我直接省略不涉及到响应式处理的,initState
方法用于处理vm.$options
的属性,就是props
,data
,methods
那些,接下来看一下initState
方法的内部逻辑。
src\core\instance\state.js
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
// 处理props
if (opts.props) initProps(vm, opts.props)
// 处理methods
if (opts.methods) initMethods(vm, opts.methods)
// 处理data
if (opts.data) {
// 把组件中data的成员注入到实例中,且转换成响应式
initData(vm)
} else {
// 初始化vm._data且把其转换为响应式,由此看出observe函数为响应式处理函数
observe(vm._data = {}, true /* asRootData */)
}
// 处理computed
if (opts.computed) initComputed(vm, opts.computed)
// 处理watch
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
从上可知,处理data
时,如果vm.$options.data
不为空,则改用initData
方法处理。否则把vm._data
置为空对象且调用observe
方法,该方法用于把传入的形参置为响应式。其实在initData
函数的逻辑里,到最后也是调用observe
方法。可见observe
方法就是响应式处理函数。接下来再看observe
方法的内部逻辑。
src\core\observer\index.js
export function observe (value: any, asRootData: ?boolean): Observer | void {
// 如果传入的value不是一个对象或者是VNode的实例,则直接返回
if (!isObject(value) || value instanceof VNode) {
return
}
// ob用于存放observer变量
let ob: Observer | void
// 如果value中有__ob__属性(observer对象)且该属性为Observer的实例
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if ( // 判断是否可以进行响应式处理
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
可见,经过条件判断后初始化Observer
实例并把value
传入到构造函数中。
到目前为止,从初始化Vue实例到初始化Observer实例的整个过程可以总结为下:
如何做到数组增强
终于到文章的核心了,现在从Observer
类的内部逻辑进行分析:
src\core\observer\index.js
import { arrayMethods } from './array'
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
export class Observer {
// 要置为响应式的数据
value: any;
// 依赖对象
dep: Dep;
// 实例计数器
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
// 通过Object.defineProperty把实例挂载到value.__ob__中
def(value, '__ob__', this)
if (Array.isArray(value)) { // value为数组的情况下的处理
// 当浏览器支持访问__proto__属性时
if (hasProto) {
// 通过原型继承改变在数组的原型属性,让其__proto__指向arrayMethods
protoAugment(value, arrayMethods)
} else {
// 利用Object.defineProperty覆盖数组中的方法
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else { // value为对象的情况下的处理
// 遍历对象中的属性,将其添加存取描述符(get/set)
this.walk(value)
}
}
// 把obj以及obj中的属性设置为响应式,非文章重点不展示细节
walk (obj: Object) {}
// 把数组中的元素通过observe方法置为响应式
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
处理数组用到的两个方法protoAugment
和copyAugment
都用到外部引入的arrayMethods
。这里先不看protoAugment
和copyAugment
的内部逻辑,先分析arrayMethods
涉及到的文件的源码:
src\core\observer\array.js
const arrayProto = Array.prototype
// 使用数组的原型创建一个新的对象
export const arrayMethods = Object.create(arrayProto)
// 会修改数组的方法,arrayMethods中的原型会对这些方法进行增强,从而达到响应式的效果
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(function (method) {
// cache original method
// 根据方法名获取Array.prototype中的数组原方法且暂存下来
const original = arrayProto[method]
// 调用Object.defineProperty重新定义修改数组的方法
def(arrayMethods, method, function mutator (...args) {
// 原始方法
const result = original.apply(this, args)
// 获取数组的observer
const ob = this.__ob__
// 存储部分新增元素的方法的形参中传进来的新增的元素
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
// 对插入的新元素,重新遍历数组元素设置为响应式数据
if (inserted) ob.observeArray(inserted)
// notify change
// 调用了修改数组的方法后,通知dep触发依赖
ob.dep.notify()
return result
})
})
其中,def
方法代码如下:
/**
* Define a property.
*/
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
从上可知,通过遍历methodsToPatch
中的字符串元素,把这些字符串作为属性名通过def
方法定义到arrayMethods
中,传入数据描述符
的value
统一为mutator
方法。mutator
方法主要做了三件事:
- 先执行数组原方法
- 若是新增元素,则把新增的元素置为响应式
- 通知dep触发页面更新
回到Observer
类中继续分析,如果hasProto
为真,即'__proto__' in {}
为真,则使用protoAugment
方法使value
的__proto__
指向arrayMethods
。protoAugment
代码如下:
function protoAugment (target, src: Object) {
target.__proto__ = src
}
如果hasProto
为假,则使用copyAugment
把arrayMethods
中的方法都定义到value
上。copyAugment
代码如下:
function copyAugment (target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}
看到这里,应该基本清楚数组增强是一个怎样的过程,把上面的流程总结为一张图,如下:
后记
本文持续未完,等我看到Vue3的源码后,会把Vue3中的响应式处理与Vue2的作对比,把这篇文章继续拓展下去。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!