前言
此系列文章用于记录vue2的简单实现过程,用于检验自己对库创作及vue原理的理解
本系列文章的目的是模仿vue实现一个支持双向绑定的js库,本篇分析vue响应式原理实现用户data对象属性劫持及深度劫持
响应式基础
vue2实现响应式的基础是 Object.defineProperty() api
Object.defineProperty() 是 JavaScript 中的一个函数,它用于定义对象的属性。它接受三个参数:要定义属性的对象、属性的名称和一个描述符对象,其中包含属性的相关信息,如属性的值、是否可读写等。例如:
const obj = {};
Object.defineProperty(obj, 'prop', {
value: 'Hello',
writable: false,
enumerable: true,
configurable: true
});
这段代码定义了一个新的对象 obj,并为它添加了一个名为 prop 的属性,该属性的值为 Hello,不可更改,可枚举,且可配置。
Object.defineProperty() 和 Object.defineProperties() 不同,它只能定义一个属性,而 Object.defineProperties() 则可以定义多个属性。
代码实现
入口
index.js 入口文件
import { initMixin } from "./_init/init";
function Vue(options) {
this._init(options);
}
initMixin(Vue);
export default Vue;
initMixin 方法在vue的原型上添加了_init()方法用来进行vue的初始化操作
初始化封装
init.js 实现了initMixin和_init 将用户的options挂载到vue实例上
import { initData } from "./initData";
/**
* 初始化配置项
* @param {*} Vue vue对象
* @param {*} options 用户配置
*/
export function initMixin(Vue) {
Vue.prototype._init = function (options) {
const vm = this;
vm.$options = options; //挂载配置项到vm上
initState(vm);
};
}
/**
* 初始化状态
* @param {*} vm
*/
function initState(vm) {
const opts = vm.$options;
if (opts.data) {
initData(vm); //data响应式
}
}
代理data到vm示例上
initData.js 实现对象的劫持和代理 主要是用 Object.defineProperty实现get 和 set方法,通过递归调用observer实现深度劫持
import { observer } from "./observer/index";
/**
* data初始化
* @param {Object} vm
* @param {Function} data
*/
export function initData(vm) {
let data = vm.$options.data;
data = typeof data === "function" ? data() : data;
//挂载data到vm实例上并观测对象
vm._data = data;
observer(data);
proxy(vm, data, "_data");
}
/**
* 将_data上的属性代理到vm根节点上,方便用户访问
* @param {*} vm
* @param {*} data
* @param {*} target
*/
export function proxy(vm, data, target) {
Object.keys(data).forEach((key) => {
if (key.indexOf("$") === 0) return; //不代理首字母是'$'的属性
Object.defineProperty(vm, key, {
get() {
return vm[target][key];
},
set(newValue) {
vm[target][key] = newValue;
},
});
});
}
实现observer类(对属性进行劫持)
import newArrayProto from "./array";
/**
* 构造observer类用于注册、管理劫持对象
*/
class Observer {
constructor(data) {
//保存observer实例到data上,设置不可枚举,防止死循环
Object.defineProperty(data, "__ob__", {
value: this,
enumerable: false,
});
if (Array.isArray(data)) {
//替换数组原型链为劫持原型链
data.__proto__ = newArrayProto;
this.observerArray(data);
} else {
this.walk(data);
}
}
/**
* 遍历data,劫持所有属性
* @param {*} data
*/
walk(data) {
Object.keys(data).forEach((key) => {
defineReactive(data, key, data[key]);
});
}
/**
* 劫持数组中的引用类型属性 优化性能
* @param {*} data
*/
observerArray(data) {
Object.keys(data).forEach((key) => {
observer(data[key]);
});
}
}
/**
* 使用 Object.defineProperty实现响应式对象
* @param {*} target
* @param {*} key
* @param {*} value
*/
export function defineReactive(target, key, value) {
observer(value); //劫持所有对象的属性
Object.defineProperty(target, key, {
get() {
console.log("getObserver", key);
return value;
},
set(newValue) {
console.log("setObserver", key);
observer(newValue);
if (value === newValue) return;
value = newValue;
},
});
}
/**
* 观测对象
* @param {} data
*/
export function observer(data) {
//不劫持非对象类型
if (typeof data !== "object" || data == null) return;
if (data.__ob__ instanceof Observer) return;
return new Observer(data);
}
其中对数组的响应式实现需要单独处理,需要重写数组对象中可以改变原数组的方法,如’push’,'unshift’等
const oldArrayProto = Array.prototype;
const newArrayProto = Object.create(oldArrayProto); //不直接修改数组的原型链
let methods = ["push", "shift", "unshift", "pop", "reverse", "sort", "splice"]; //加强这些方法,可以监控到数组的修改
methods.forEach((funcName) => {
newArrayProto[funcName] = function (...args) {
const result = oldArrayProto[funcName].call(this, args);
console.log(`劫持到:${funcName}`);
//对新增的对象属性进行劫持
let adds = args;
const ob = this.__ob__;
switch (funcName) {
case "push":
case "unshift":
adds = args;
break;
case "splice":
adds = args.slice(2);
break;
}
if (adds) {
ob.observerArray(adds);
}
return result;
};
});
export default newArrayProto;
上面的代码重写了数组的原型链,实现数组中引用类型属性的劫持,以及修改数组方法的监听
结语
相关代码已上传gitee 点击跳转
评论区