当前2.x的vue响应是基于Object.defineProperty
的,其存在一些局限
- 无法跟踪响应对象属性的添加 / 删除。
- 无法监控到数组的下标变化 -- vue里hack了八大数组的操作方法,像push\pop\shift等。
- 在进行响应绑定时,如果属性值是对象,还需要进行深度遍历。
如何解决? 使用 Vue.set 和 Vue.delete 来保证。
* Object.defineProperty(obj, prop, descriptor)
* 定义或修改的属性描述符
* https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
下面我们来验证一下以上提到的几点。 案例:
/**
* Object.defineProperty(obj, prop, descriptor)
* 定义或修改的属性描述符
* https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
*/
function definePropertyFn(data, prop, value) {
Object.defineProperty(data, prop, {
enumerable: true,
configurable: true,
get: function () {
console.log(`get key--- ${prop} ---- value ${value}`);
return value;
},
set: function (newVal) {
console.log(`set key--- ${prop} ---- value ${newVal}`);
value = newVal;
},
});
}
function observe(data) {
// 由于 Object.defineProperty 只能对属性进行劫持,因此需要遍历对象的每个属性
if (!data || typeof data !== "object") {
return false;
}
Object.keys(data).forEach((key) => {
definePropertyFn(data, key, data[key]);
});
}
这个时候,如何你observe
一个数组,可以看出是没法正常触发getter or setter 的。
let arr = [1, 2, 3];
observe(arr);
document.getElementById("arrPush1").onclick = function () {
console.log("-------arrPush-----");
arr.push(4);
};
document.getElementById("arrPush2").onclick = function () {
console.log("------arr赋值-----");
arr[0] = new Date().getTime();
};
因而需要对数组shift
push
等进行特殊处理。利用Object.create()
与 属性重定义,实现hack 处理。
(function () {
const arrayProto = Array.prototype;
const arrayMethods = Object.create(arrayProto);
const methodsToPatch = [
"push",
"pop",
"shift",
"unshift",
"splice",
"sort",
"reverse",
];
// 对特定的方法进行拦截,特殊通知
methodsToPatch.forEach((method) => {
const original = arrayProto[method];
Object.defineProperty(arrayMethods, method, {
enumerable: true,
writable: true,
configurable: true,
value: function (...args) {
// const result = original.apply(this, args);
const result = Reflect.apply(original, this, args);
switch (method) {
case "push":
console.log("拦截通知了push操作");
// emit & notify update
break;
case "splice":
case "pop":
console.log("拦截通知了xxx操作");
break;
}
return result;
},
});
});
// 看下是否通知
let testArr = new Array();
testArr.__proto__ = arrayMethods;
testArr.push("11111");
// 打开tool看下原型的展示
// console.log(testArr);
})();
有打开过vue2.x源码的,应该可以看出,上面的大致也是vue2.x 里源码的处理方式。打开node_modules 下的源码
core/observer/index.js
附上官方响应图:
vue3.x 使用的是ES6的Proxy 作为其观察者机制,取代之前的Object.defineProperty
有以下优点:
- 可以劫持整个对象
- 有13种形式劫持操作
还有一个与proxy
紧密使用的 Reflect
可以走下面这个test 进一步熟悉proxy
/**
* const p = new Proxy(target, handler)
* target 可以是任何类型的对象,包括原生数组、函数、甚至另一个代理
* https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy
* https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect
*/
let obj = {
a: 1,
ddd: 2222,
name: "hhh",
};
let proxyObj = new Proxy(obj, {
get: function (target, prop) {
console.log(`-------get-${prop}----`);
return target[prop] ? target[prop] : 0;
},
set: function (target, prop, value) {
// 可以做一些赋值校验
console.log(`-------set-${prop}-----`);
target[prop] = 8888;
},
getPrototypeOf: function (target) {
console.log(`-------getPrototypeOf------`);
// 读取代理对象的原型时,会被调用
return Array.prototype;
},
deleteProperty: function (target, prop) {
// 删除属性
const WhiteArr = ["name", "phone"];
if (WhiteArr.indexOf(prop) > -1) {
console.error("不可以删除该属性");
return false;
}
delete target[prop];
return true;
},
has: function (target, prop) {
// 判断对象是否具有某个属性
const WhiteArr = ["money", "phone"];
if (WhiteArr.indexOf(prop) > -1) {
console.warn("这是私有属性");
return false;
}
return true;
},
apply: function () {
// 函数调用的拦截 (代理的是函数)
},
construct: function () {
// new 操作的拦截
},
isExtensible: function () {
// 拦截 判断一个对象是否是可扩展
},
});
// 利用它进行数据的二次处理、可以进行数据合法性的校验
proxyObj.a;
proxyObj.b;
proxyObj.ccc = 666;
var aaa_prototype = Object.getPrototypeOf(proxyObj);
console.log(aaa_prototype === Object.prototype);
console.log(aaa_prototype === Array.prototype);
// 拦截delete 操作
delete proxyObj.name;
console.log(obj);
// 用 has方法隐藏了属性 money
console.log("money" in proxyObj);
基本认识proxy 之后,就可以升级个vue3.x 使用一番,可参考这个文章,一步步搭建vue-next
环境。
vue3.x 的各各模块,相对都是比较独立的,可以取单独的一部分进行使用,比如 Vue 3.0 的数据响应式系统是独立的模块,可以完全脱离Vue 而使用
/**
* Vue 3.0 的数据响应式系统是独立的模块,可以完全脱离 Vue 而使用
* https://juejin.cn/post/6844903959660855309
* https://jrainlau.github.io/#/article?number=20
* yarn dev reactivity 调试
*/
const { reactive, effect } = VueReactivity;
const data = {
count: 1,
};
// 将data转成proxy对象state
const state = reactive(data);
const fn = () => {
const count = state.count;
document.getElementById("txt").innerHTML = count;
console.log(`set count to ${count}`);
9;
};
// effect把fn()作为响应的回调,当state.count 发生变化时,便触发了 fn()
// 默认会立即执行一次,进而把依赖进行收集
effect(fn);
// 点击事件
document.getElementById("btn").addEventListener("click", () => {
state.count++;
});
// 源码
// ├── compiler-core # 所有平台的编译器
// ├── compiler-dom # 针对浏览器而写的编译器
// ├── reactivity # 数据响应式系统
// ├── runtime-core # 虚拟 DOM 渲染器 ,Vue 组件和 Vue 的各种API
// ├── runtime-dom # 针对浏览器的 runtime。其功能包括处理原生 DOM API、DOM 事件和 DOM 属性等。
// ├── runtime-test # 专门为测试写的runtime
// ├── server-renderer # 用于SSR
// ├── shared # 帮助方法
// ├── template-explorer
// └── vue # 构建vue runtime + compiler
vue3.x 收集的大致流程如下,看源码就在reactivity 目录下。
下次梳理一下 typescript & hook
tips:
2020 年 Vue.js 的重大变化无疑是 Vue.js3.0 的发布,有了非常多新特性,总结如下:
- 对 Vue.js 进行了完全 Typescript 重构,让 Vue.js 源码易于阅读、开发和维护;
- 重写了虚拟 Dom 的实现,对编译模板进行优化、组件初始化更高效, 性能上有较大的提升;Vue.js2 对象式组件存在一些问题:难以复用逻辑代码、难以拆分超大型组件、代码无法被压缩和优化、数据类型难以推倒等问题;而 CompositionAPI 则是基于函数理念,去解决上述问题,使用函数可以将统一逻辑的组件代码收拢一起达到复用,也更有利于构建时的 tree-shaking 检测,这个使用起来有些类似于 React 的 hook;
- 以上变化都秉持着 VUE 的“渐进式框架“ 理念, Vue.js3.0 持续开发兼容旧的版本,即使升级了 Vue.js3.0 也可以按照之前的组件开发模式进行开发。