vue2源码解析(二):数据劫持
你好呀,我是小九,很高兴见到你。
摘要
vue2源码学习之路。
第一篇文章中介绍了环境搭建,点击查看
项目结构
本文项目结构
初始化
1.创建类
vue2中,使用关键字new来创建一个vue实例,因此需要一个Vue类。
vue2源码中没有使用class,而是使用构造函数的形式。
一般来说,class是一个整体,需要把所有的方法都写在一起。
使用构造函数的形式扩展方法更灵活一些。
源码截图
按照这种形式,在src目录下创建一个index.js文件,创建一个Vue类,并将这个类导出。
完整代码
import { initMixin } from "./instance/init";
function Vue(options) {
this._init(options);
}
// 将initMixin引入,并将Vue传过去,相当于扩展了init方法
initMixin(Vue)
// 导出Vue
export default Vue;
options
是传给Vue的配置项。
2.初始化init
一般来说,如果初始化需要调用很多代码,就会封装一个初始化函数。
vue2源码中,创建一个单独的初始化文件,解耦合,便于管理。
创建init.js文件,路径src-->instance-->init.js
export function initMixin(Vue) {
Vue.prototype._init = function (options) {}
}
options这个参数,后面很多地方都会用到,所以需要把options放到实例上。
在_init函数内部,需要初始化状态,将数据做一个初始化劫持,当修改数据时应该更新视图。
完整代码
import { initState } from "./state";
export function initMixin(Vue) {
Vue.prototype._init = function (options) {
const vm = this;
vm.$options = options;
initState(vm);
};
}
3.初始化状态
创建state.js文件,路径src-->instance-->state.js
初始化状态函数中,主要是针对不同情况做不同的初始化。
例如传入data,传入props,传入methods等等,需要分别初始化。
源码截图
代码实现,此处只关注data部分
import { isFunction } from "../shared/until";
export function initState(vm) {
// 获取options
const opts = vm.$options;
// 针对不同情况做不同初始化
// 判断是否传递了data
if (opts.data) {
initData(vm);
}
// 判断是否传递了props
if (opts.props) {}
// 判断是否传递了methods
if (opts.methods) {}
// 判断是否传递了watch
if (opts.watch) {}
// 判断是否传递了computed
if (opts.computed) {}
}
function initData(vm) {
data = isFunction(data) ? data.call(vm) : data || {};
if (!isPlainObject(data)) {
data = {};
console.error("[Vue warn] data functions should return an object");
}
}
function initProps() {}
function initMethods() {}
function initWatch() {}
function initComputed() {}
initData、initProps、initMethods等,不是公共的方法, 只是特定针对某个属性操作,所以这些方法不需要挂载到原型上,
vue2中data有两种情况,一种是对象,一种是函数。
根实例可以是对象,可以是函数,组件中data必须是函数。
判断data是不是函数,如果是函数,让data执行,并且让data中的this指向Vue实例。
无论哪种情况,data值的最外层,都是一个对象。
new Vue({
data: {}
)
new Vue({
data() {
return {}
}
)
// 组件
export default{
data() {
return {}
}
}
创建until.js文件,路径src-->shared-->until.js
这个文件主要是一些公共的方法。
// 判断是不是函数类型
export function isFunction(value) {
return typeof value === "function";
}
// 判断是不是对象类型
const _toString = Object.prototype.toString;
export function isPlainObject(obj) {
return _toString.call(obj) === "[object Object]";
}
vue的核心是响应式处理, 因此需要做数据劫持。
数据劫持
1.对象劫持
vue2中,劫持对象使用的是Object.defineProperty()
,重新设置set和get方法,取值和设置值之前可以自定义处理操作。
源码中定义了一个defineReactive函数,其中封装了Object.defineProperty()。
对象的操作有几种情况
(1)对象可能有多层级,如果某个属性值也是对象,这个值需要劫持
(2)修改某个属性值,将值设置为一个新的对象,这个新对象需要劫持
(3)对象新增了一个属性,值是一个新对象
(4)对象删除了某个属性
由于Object.defineProperty
只能劫持已经存在的属性,新增的和删除的无法劫持,所以vue中单独增加了些api,解决这个问题,比如,$set
,$delete
。
此处先不做介绍。
export function defineReactive(data, key, value) {
observer(value);
Object.defineProperty(data, key, {
get() {
return value;
},
set(newValue) {
// 新值和原来的值相等,不进行操作,直接返回
if (newValue === value) return;
observer(newValue);
value = newValue;
},
});
}
这个函数是一个闭包的形式,生成不销毁的栈内存,value的值在get和set内部都可以使用。
闭包有两个作用,一是保护,二是保存。
关于闭包的更多内容,请点击查看攻克作用域和闭包
然后,创建一个Observer类,对数据进行监测。
如果是对象,就遍历对象的所有属性,对每个属性进行劫持
如果对象中包含数组,就调用数组的监测方法
class Observer {
constructor(data) {
if (Array.isArray(data)) {
this.observeArray(data);
} else {
this.observeObject(data);
}
}
observeObject(data) {
let keys = Object.keys(data);
keys.forEach((key) => {
defineReactive(data, key, data[key]);
});
}
observeArray(data) {
// 数组中也可能包含对象,依次遍历数组
data.forEach((item) => observer(item));
}
}
对象劫持有两个条件,一是数据是对象,二是没有劫持过,劫持过就不需要重复操作。
所以添加一个标识,判断是否监测过。
在Observer类中添加一个__ob__
,属性值设置为this(当前Observer的实例)。
这个属性需要设置成不可枚举,也就是说在遍历对象的时候不能被遍历到。
否则,遍历时,__ob__
的属性this也是一个对象,然后会继续遍历,导致死循环。
Object.defineProperty(data, "__ob__", {
enumerable: false,
configurable: false,
value: this,
});
不能直接写成下面的形式,也会产生死循环
value.__ob__ = this;
2.数组劫持
使用Object.defineProperty
,数据的层级越深,性能就越不好。
所以出于性能方面考虑,vue2源码中并未对数组劫持,而是重写了数组原型上能够改变数组的几个方法。
let methods = ["push", "pop", "shift", "unshift", "reverse", "sort", "splice"];
为了避免将原型上的方法覆盖,所以对原型进行了继承
let oldArrayProtoMethods = Array.prototype;
export let arrayMethods = Object.create(oldArrayProtoMethods);
然后对methods中的几个方法进行重写。
原理是调用重写的方法会调用原来原型上的方法,然后将参数传过去。
数组中新增的项也需要监测,新增方法有三个:push、unshift、splice。
splice可以用来添加,修改,删除操作。添加时,传给splice的第二项是添加的内容。
methods.forEach((method) => {
arrayMethods[method] = function (...args) {
const result = oldArrayProtoMethods[method].call(this, ...args);
let insert = null;
let ob = this.__ob__;
switch (method) {
case "push":
case "unshift":
insert = args;
break;
case "splice":
insert = args.slice(2);
default:
break;
}
if (insert) ob.observeArray(insert);
return result;
};
});
3.数据代理
为了方便用户操作数据,vue源码中实现了数据代理。
const app = new Vue({
data:{
a:1
}
)
可以直接通过app.a来获取或修改a的值。
源码中定义了一个proxy函数(不是新增的Proxy,是自己手写的proxy函数)。
代理的本质是将vm._data用vm代理。
for (const key in data) {
proxy(vm, "_data", key);
}
获取某个值时,相当于直接从vm._data获取,设置值相当于修改vm._data。
proxy函数
function proxy(vm, target, key) {
Object.defineProperty(vm, key, {
get() {
return vm[target][key];
},
set(newValue) {
vm[target][key] = newValue;
},
});
}
完整流程
首先创建了一个Vue类,可以扩展原型上的方法。
每次执行new Vue,都会调用init初始化方法,初始化函数其中有一步就是初始化状态。
状态分为很多,包括data,methods,props等,需要分别处理。
data可能是函数也可能是对象,将data处理后的结果挂载vm._data
上,同时为了用户能够方便使用取值,做了一层代理。
对数据进行劫持,有两个原则,一是数据是对象,二是没有劫持过,给每个数据都增加一个观测的实例。
如果是数组,就把数组的方法重写,并且对数组里每个对象都进行劫持。
如果调用能改变原数组方法,就会调用重写的方法;如果新增值的也是数组,就通过Observer实例的方法再次对数组进行代理。
如果是对象,将数据进行劫持,如果对象的值是对象,也要劫持,如果修改的值也是对象,还要劫持。
完整代码
src/index.js
import { initMixin } from "./instance/init";
function Vue(options) {
this._init(options);
}
initMixin(Vue)
export default Vue;
src/instance/init.js
import { initState } from "./state";
export function initMixin(Vue) {
Vue.prototype._init = function (options) {
const vm = this;
vm.$options = options;
initState(vm);
};
}
src/instance/state.js
import { observer } from "../observer/index";
import { isFunction, isPlainObject } from "../shared/until";
export function initState(vm) {
const opts = vm.$options;
if (opts.data) {
initData(vm);
}
}
// 设置一个数据代理,取值和设置值的时候直接操作vm._data
function proxy(vm, target, key) {
Object.defineProperty(vm, key, {
get() {
return vm[target][key];
},
set(newValue) {
vm[target][key] = newValue;
},
});
}
function initData(vm) {
let data = vm.$options.data;
// 将data方到实例vm上
vm._data = data = isFunction(data) ? data.call(vm) : data || {};
if (!isPlainObject(data)) {
data = {};
console.error("[Vue warn] data functions should return an object");
}
observer(data);
// 将vm._data用vm代理
for (const key in data) {
proxy(vm, "_data", key);
}
}
src/observer/index.js
import { isObject } from "../shared/until";
import { arrayMethods } from "./array";
class Observer {
constructor(data) {
Object.defineProperty(data, "__ob__", {
enumerable: false,
configurable: false,
value: this, // 此处的this是Observer的实例
});
if (Array.isArray(data)) {
data.__proto__ = arrayMethods;
this.observeArray(data);
} else {
this.observeObject(data);
}
}
observeArray(data) {
data.forEach((item) => observer(item));
}
observeObject(data) {
let keys = Object.keys(data);
keys.forEach((key) => {
defineReactive(data, key, data[key]);
});
}
}
export function defineReactive(data, key, value) {
observer(value);
Object.defineProperty(data, key, {
get() {
return value;
},
set(newValue) {
if (newValue === value) return;
observer(newValue);
value = newValue;
},
});
}
export function observer(data) {
if (!isObject(data)) {
return;
}
// 如果被劫持过,就不需要再劫持了
if (data.__ob__) {
return data;
}
return new Observer(data);
}
src/observer/array.js
// 数组原来原型上的方法
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) {
// 此处的this是调用方法的数组
const result = oldArrayProtoMethods[method].call(this, ...args);
let insert = null;
let ob = this.__ob__;
switch (method) {
case "push":
case "unshift":
insert = args;
break;
case "splice":
insert = args.slice(2);
default:
break;
}
if (insert) ob.observeArray(insert);
return result;
};
});
src/shared/until.js
export function isFunction(value) {
return typeof value === "function";
}
const _toString = Object.prototype.toString;
export function isPlainObject(obj) {
return _toString.call(obj) === "[object Object]";
}
export function isObject(obj) {
return obj !== null && typeof obj === "object";
}
关于this的指向,请点击查看如何判断this的指向
测试
const app = new Vue({
data: {
a: 100,
obj: {
name: "tom",
arr: [1, 2, 3],
},
},
});
console.log(app.a);
console.log(app.obj.name);
app.obj.arr.push(10);
console.log(app.obj.arr);
app.obj1 = { name: "jack" };
console.log(app.obj1);
结果
至此,能够实现基本的数据代理和劫持。
文章就到这里,下次再见!
转载自:https://juejin.cn/post/7137574093147013157