【Vue源码】Vue核心应用之数据劫持与属性代理
Vue全家桶的系统学习,其中包括Vue源码分析、Vue-Router的使用和原理、Vuex的用法和原理、Vue-ssr 和 一些常见的Vue面试题。
数据(data)初始化
扩展 initData
方法之前,我们需要先知道一个概念。什么是数据劫持?
数据劫持 就是通过 Object.defineProperty
来重写对象的 getter
和 setter
。数据更新时视图会发生改变,而视图改变时数据也会跟着更新,从而达到一个 视图和数据 互相影响的效果。
这种设计模式我们又称呼它为 观察者模式。
import {observe} from './observer/index.js'
function initData(vm){
let data = vm.$options.data;
data = vm._data = typeof data === 'function' ? data.call(vm) : data; // 如果data是函数,改变其this指向
observe(data);
}
vm._data
的作用其实就是为了让 vm实例上可以获取到 data 的值。
下面我们就来利用观察者模式,实现Vue中的数据劫持。
对象属性劫持
数据劫持主要包括 对象劫持 和 数组劫持 两个方面。
先来看一下对象属性劫持的完整代码。
class Observer {
// 观测值
constructor(value) {
this.walk(value);
}
walk(data) {
let keys = Object.keys(data);
keys.forEach((key) => {
defineReactive(data, key, data[key]);
});
}
}
function defineReactive(data, key, value) {
observe(value);
Object.defineProperty(data, key, {
get() {
return value;
},
set(newValue) {
if (newValue == value) return;
observe(newValue);
value = newValue;
},
});
}
export function observe(data) {
if (typeof data !== "object" || data == null) {
return;
}
return new Observer(data);
}
-
导出
observe
函数,进行类型判断。当前文件,我们只处理
Object
类型的值。不是Object
类型的值不做处理。export function observe(data) { if (typeof data !== "object" || data == null) { return; } return new Observer(data); }
-
完善
Observer
类,让对象上的所有属性依次进行观测。class Observer { // 观测值 constructor(value) { this.walk(value); } // 让对象上的所有属性依次进行观测 walk(data) { let keys = Object.keys(data); // 获取对象上的key值 keys.forEach((key) => { defineReactive(data, key, data[key]); }); } }
-
定义
defineReactive
函数,用来对Object.defineProperty
进行封装。循环调用
observe
函数,保证当前对象内的所有属性都被监听到。(注:主要是为了处理 {a:{a:{a:1}}} 这种情况。但这样处理,层级越深性能越差,所以在 Vue3 中将其替换成了
proxy
进行处理。)function defineReactive(data, key, value) { observe(value); Object.defineProperty(data, key, { get() { return value; }, set(newValue) { if (newValue == value) return; observe(newValue); value = newValue; }, }); }
在
setter
方法中我们会注意到,我们又重新监听了一次newValue
值的变化。这个是因为用户在赋值时,可能会传入一个新的Object
类型的对象,所以我们需要对新传入的值,也进行一次监听。保证所有值的get
和set
都是被重写的。
如果我们使用这种方法去处理数组,也是可以行得通的。但是只能通过索引去进行监听,我们在处理数组时一般会使用专门的方法去进行处理(如 push
、shift
、pop
等等)。为了性能考虑,我们还需要单独对数组方法进行处理。
数组方法劫持
其实数组的劫持,就是对原型上数组的方法进行重写。
首先,需要先判断当前传入的值是否是数组。
import {arrayMethods} from './array';
class Observer { // 观测值
constructor(value) {
if (Array.isArray(value)) {
value.__proto__ = arrayMethods; // 重写数组原型方法
this.observeArray(value);
} else {
this.walk(value);
}
}
// 循环并观测数组上的每一个值
observeArray(value) {
value.forEach((item) => {
observe(item);
});
}
}
observeArray
可以保证数组上的每一个值都是被检测的,包括 Object
和 其他类型。
arrayMethods
就是重写了当前数组原型上的方法。
重写数组原型方法
下面我们来看一下 arrayMethods
到底是什么。
let oldArrayProtoMethods = Array.prototype;
export let arrayMethods = Object.create(oldArrayProtoMethods);
let methods = ["push", "pop", "shift", "unshift", "reverse", "sort", "splice"]; // 需要重写的方法
methods.forEach((method) => {
arrayMethods[method] = function (...args) {
const result = oldArrayProtoMethods[method].apply(this, args);
// ...
return result;
};
});
上面的意思是将 原始数组原型上的方法继承一份,并导出。然后将我们需要重写的方法循环出来,依次进行处理。
我们这里使用 Object.create
相当于 arrayMethods.__proto__ = oldArrayProtoMethods
,这样会只处理重写后的方法,没重写的方法不会被处理。
现在,我们需要对一些特定的方法进行处理。
// ...
methods.forEach((method) => {
arrayMethods[method] = function (...args) {
// ...
const ob = this.__ob__;
let inserted; // 需要添加的项
switch (method) {
case "push":
case "unshift":
inserted = args;
break;
case "splice":
inserted = args.slice(2); // 截取新添加的项
default:
break;
}
if (inserted) ob.observeArray(inserted); // 对新添加的每一项进行观测
return result;
};
});
push
和 unshift
都是添加项的意思,一个是在头部添加,一个是在尾部添加。splice
从第三个参数开始,就是添加的项。
因为新添加的项可能也是 Object
类型,所以我们需要再次 对新添加的项进行劫持。
但是这里就出现了一个问题,我们并没有办法使用劫持方法。又已知当前方法中的this就是被调用数组的value(上面的 apply
改变了 this
指向)。所以我们就可以在value上增加一个属性__ob__
。
增加 __ ob __ 属性
__ob__
属性的作用是给所有响应式数据增加标识,用来判断当前对象是否被劫持过。并且可以在响应式上获取Observer
实例上的方法。
class Observer {
constructor(value){
// 添加 __ob__
Object.defineProperty(value, "__ob__", {
enumerable: false, // 不可枚举,也就是不能被循环出来
configurable: false, // 不能删除
value: this,
});
// ...
}
}
这样我们就在每一项中都追加了一个 __ob__
属性,现在我们还需要进行一步处理。
// ...
export function observe(data) {
if (typeof data !== "object" || data == null) {
return data;
}
if (data.__ob__) {
return data;
}
return new Observer(data);
}
如果当前项已经被添加了 __ob__
属性,则不进行处理。这样可以防止数据被重复劫持。
这样我们就完成了对数组的数据劫持。
(注:在Vue中不能直接修改指定项,需要通过Array数组中的方法来进行修改。Vue.$set
可以修改特定值,后续我会详细介绍一下其使用和原理。)
属性代理
我们已经对数据进行了相应的劫持,现在可以对 data
进行相应的操作了。
let vm = new Vue({
// ...
})
vm._data.arr.push({
b: 2
});
vm._data.arr[1].b = 3;
set
和 get
方法都可以被正常触发。
但是这种操作太麻烦了,所以我们现在需要对属性进行代理,让用户可以直接通过 vm.arr
这种方式直接修改值。
function proxy(vm, source, key) {
Object.defineProperty(vm, key, {
get() {
return vm[source][key];
},
set(newValue) {
vm[source][key] = newValue;
},
});
}
function initData(vm) {
let data = vm.$options.data;
data = vm._data = typeof data === "function" ? data.call(vm) : data;
for (let key in data) {
// 将_data上的属性全部代理给vm实例
proxy(vm, "_data", key);
}
observe(data);
}
同样是使用 Object.defineProperty
对传入的属性进行劫持监听。
当用户到 vm
中获取属性时,属性的值被代理到 vm._data
上。
转载自:https://juejin.cn/post/7082077057827962894