前言
在上一篇中,我们大致了解了创建一个Vue实例的核心在于运行其中的_init()
函数,而在_init()
函数中又执行了许多初始化操作。
数据初始化
在这篇中我们来整理initState()
,这是其中负责数据初始化的函数。
php
/**
* Vue实例的数据初始化
* @param {Vue} vm Vue实例
*/
function initState(vm) {
const opts = vm.$options;
if (opts.data) {
initData(vm)
}
}
kotlin
function initData(vm) {
let data = vm.$options.data; // 获取创建实例时的options中的data数据
// options中的data有时候会携程函数形式data() {return {}}
data = vm._data = isFunction(data) ? data.call(vm) : data || {}; // 使用data.call(vm)而不是data(),因为书写时data()中的会用到this参与赋值,本意肯定是指向当前vm的,需要保证this指向正确
// observe data
const ob = observe(data)
}
在数据初始化中,最重要的就是vue的核心:响应式。这通过observe(data)
实现
数据劫持
通过设置getter\setter,观测数据的读写。
单层劫持
javascript
// observe/index.js
function observe(value) {
// observe只处理object, 其他的不需要响应式,直接 return
if(!isObject(value)){
return;
}
// 返回一个响应式对象
return new Observer(value);
}
class Observer {
constructor(value) {
this.walk(value);
}
walk(data) {
// 为data中的值逐个创建响应式
Object.keys(data).forEach(key => { // Object.keys(data)返回data中所有可枚举的键的数组,用于遍历
defineReactive(data, key, data[key]);
});
}
}
/**
* 使用Object.defineProperty重新定义data对象中的属性
* 通过设置getter和setter,我们在数据被读、写时都放入了钩子
* @param {*} obj 需要定义属性的对象
* @param {*} key 键
* @param {*} value 值
*/
function defineReactive(obj, key, value) {
Object.defineProperty(obj, key, {
get() {
return value; // 整理通过闭包取值,如果通过Obj[key]获取的话会触发死循环
},
set(newValue) {
if (newValue === value) return;
value = newValue;
}
})
}
至此,我们已经可以observe对象中每个值的读、写操作了。
深层劫持
但是细心的我们已经发现,这样做,只能对类似data: {message: 'Hello Vue'}
这样的单层对象完成数据劫持,但是对于data: {obj: {key: 'v'}}
这样的对象是无法对深层数据进行劫持的。
对于这样的场景,我们很容易联想到,通过递归来实现:
javascript
function defineReactive(obj, key, value) {
observe(value); // 对值进行递归,以实现深层劫持
Object.defineProperty(obj, key, {
get() {
return value;
},
set(newValue) {
if (newValue === value) return;
value = newValue;
}
})
}
数组劫持
数组劫持是一个经典的vue2问题,vue2并非完全不能处理数组数据,类似arr.push()
这样的操作是可以被响应式处理的,但类似arr[0] = 1
这样的操作却不行。这并非代码无法实现,而是如果采用上述的方式来处理数组数据,那么当数组数据量变大后,对每一项创建响应式对象,将带来性能问题(比如对10000条数据添加get&set,再想想我们再get中使用了闭包进行取值...)。
所以考虑到需求和性能的平衡,vue2并不通过defineProperty
处理数组,而是另辟蹊径对数组原型中的拓展方法采取行动。
vue2认为这 7 个方法能够改变原数组:push、pop、splice、shift、unshift、reverse、sort,于是重写了这7个方法。
javascript
class Observer {
constructor(value) {
// 将Observer实例记录到data属性中
Object.defineProperty(value, '__ob__', {
value:this,
enumerable:false // 不可被枚举, 这就是不适用value.__ob__ = this; 的原因,一旦可枚举就会在
});
if (isArray(value)) {
value.__proto__ = arrayMethods; // 重新设置原型链,使得钩子能被放入数组原型方法中this.walk(value);中走入死循环
this.observeArray(value);
} else {
this.walk(value);
}
}
walk(data) {
// 为data中的值逐个创建响应式
Object.keys(data).forEach(key => { // Object.keys(data)返回data中所有可枚举的键的数组,用于遍历
defineReactive(data, key, data[key]);
});
}
// 对改变原数组的值的处理方法,由于创建的响应式是一个Observer对象,所以对于其中数组的值也需要在这个对象中处理
observeArray(value) {
for (let i = 0, l = value.length; i < l; i++) {
observe(value[i]);
}
}
}
TODO1:目前看来Observer类中的逻辑似乎是可以单独执行的,那么为什么vue2使用了通过创建Observer类的实例的方式来实现响应式呢?这个问题在后续会揭晓答案。
TODO2:我们可以看出,到目前位置,只有initData()
中返回的Observer
对象被使用,其他的都仅仅用于数据劫持,就被"抛弃"。这是为什么呢?
ini
// Observer/array.js
let oldArrayPrototype = Array.prototype;
let arrayMethods = Object.create(oldArrayPrototype); // 原型继承,以oldArrayPrototype为原型创建新对象,将原型链往后移动
let methods = [
'push',
'pop',
'shift',
'unshift',
'reverse',
'sort',
'splice'
]
methods.forEach(method => {
arrayMethods[method] = function (...args) {
// 调用数组原生方法逻辑(绑定到当前调用上下文)
oldArrayPrototype[method].call(this, ...args);
// 数组新增的属性如果是属性,要继续观测
let inserted = null;
let ob = this.__ob__; // 此时this指向的是调用函数的数组,这个数组在observe()阶段已经被添加了__ob__属性,这个属性指向数据对应的Observer实例
switch(method) {
case 'splice':
inserted = args.slice(2);
break;
case 'push':
case 'unshift':
inserted = args;
break;
}
if (inserted) ob.observeArray(inserted); // 这里inserted有值就一定是数组
}
});
TODO3: 明明只有3个原型方法会导致数组中有新增数据,需要进行数据劫持,为什么要重写7个方法而不是3个呢?
边界情况
在完成了对数据新增\初始化时的劫持后,我们并没有大功告成。众所周知,js是弱类型的语言,变量是可以修改的甚至可以修改成其他的数据类型。所以在实际使用中难免会出现一下情况:
数据修改为对象\数组
scss
function defineReactive(obj, key, value) {
observe(value); // 对值进行递归,以实现深层劫持
Object.defineProperty(obj, key, {
get() {
return value;
},
set(newValue) {
if (newValue === value) return;
observe(newValue); // 一旦newValue不是对象类型,observe的代码逻辑会停下,不会造成副作用
value = newValue;
}
})
}
[Warning] 对象数据新增属性无法添加数据劫持,Vue2提供了.$set()
API来解决这个问题。
总结
这篇中我们对数据劫持做了一个整理,包括:
- 使用
Observer
类进行数据劫持 - 对对象、数组两大类型的数据劫持通过不同的方式进行
- 采用递归思想解决嵌套属性的数据劫持问题
- 对边界情况中的数据类型修改为对象\数组进行了处理