# 👉 Vue 的双向绑定
Vue.js 主要采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()
来劫持各个属性的 setter,getter,在数据变动时发布消息给订阅者,然后触发相应的监听回调。
# Object.defineProperty()是用来做什么的
Object.defineProperty
可以在一个对象上定义新属性或修改现有属性,并返回此对象。同时,它也可以用于控制一个对象属性的一些特有操作,比如读写权、是否可以枚举。
const obj1 = {};
Object.defineProperty(obj1, "property1", {
value: "i am property1",
writable: false, // 不可写
});
obj1.property1 = "i has changed";
console.log(obj1); // {property1: "i am property1"}
这里先简单了解一下Object.defineProperty
的两个描述属性 get 和 set 用法:
一个简单的栗子:
const book1 = {};
Object.defineProperty(book1, "name", {
set: (val) => {
name = val;
console.log("将 book1 取名为:", val);
},
get: () => {
return "《" + name + "》";
},
});
book1.name = "锦鲤";
console.log("获取 book1 的书名:", book1.name);
// 将 book1 取名为: 锦鲤
// 获取 book1 的书名:《锦鲤》
# 简述
Vue 在初始化数据的时候,通过 Object.defineProperty 的 getter(数据依赖收集处理) 和 setter(监听数据的变化,并通知订阅) 对 data 里面的每个属性对象进行劫持监听和属性代理。
当页面使用对应属性时,首先会通过 Dep 进行依赖收集(收集 Watcher) ;如果属更新了,会通知相关的依赖进行更新操作(发布订阅) 。
过程:
(1)初次渲染,根据传入的 data 和 render 函数,生成对应的 VNode Tree。
这期间会有一次依赖收集过程,建立一个直接对应 render 的 Watcher,且 v-if
为 false 的元素不会出现在生成的 VNode Tree 中,最后调用 patch,因为没有 oldVnode, 会直接将 VNode 渲染为真实的 DOM。
(2)新建实例时,vue 会调用 Compile 将 el 转换成 VNode(解析 new Vue 传入的 el 或 template 元素中的元素,生成 AST 树,返回构建虚拟节点 VNode);
(3)开始监听数据变化,创建 props、data 的钩子以及其对象成员的 Observer,同时进行 data 的属性代理;
// new Vue 执行流程。
// 1. Vue.prototype._init(option)
// 2. vm.$mount(vm.$options.el)
// 3. render = compileToFunctions(template) ,编译 Vue 中的 template 模板,生成 render 方法。
// 4. Vue.prototype.$mount 调用上面的 render 方法挂载 dom。
// 5. mountComponent
// 6. 创建 Watcher 实例
// 结合上文,我们就能得出,updateComponent 就是传入 Watcher 内部的 getter 方法。
new Watcher(vm, updateComponent);
// 7. new Watcher 会执行 Watcher.get 方法
// 8. Watcher.get 会执行 this.getter.call(vm, vm) ,也就是执行 updateComponent 方法
// 9. updateComponent 会执行 vm._update(vm._render())
function updateComponent() {
// 将生成的JS模版当参数传递
vm._update(vm._render())
}
// 10.调用 vm._render 会生成虚拟 dom
Vue.prototype._render = function () {
const vm = this;
const { render } = vm.$options;
let vnode = render.call(vm._renderProxy, vm.$createElement);
return vnode;
};
// 11. 调用 vm._update(vnode) 渲染虚拟 dom
Vue.prototype._update = function (vnode:) {
const vm = this;
// 上一次更新的vnode
const prevVnode = vm._vnode;
// 下一次对比的vnode
vm._vnode = vnode;
if (!prevVnode) {
// 第一次执行,初次渲染
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false);
} else {
// 更新
vm.$el = vm.__patch__(prevVnode, vnode);
}
};
// 12. vm.__patch__ 方法就是做的 dom diff 比较,然后更新 dom。
(4)之后,每当属性更新时,都将通知相应的 watcher 执行回调函数,更新视图:
a. 当给这个对象的某属性赋值时,就会触发 set 方法;
b. set 函数调用,触发属性消息器 Dep 的 notify 向对应的 watcher 通知变化;
c. watcher 调用 upddate 方法更新视图。
# 实现思路
要实现响应式( MVVM 的双向绑定),主要需要实现以下几个部分:
(1)Observe:数据监听器,用于监听数据对象的所有属性,有变动时获取最新值并通知订阅者;
(2)Dep和Watcher:发布者和订阅者,连接 Observe 和 Compile,用于订阅并且接收到每个属性变动的消息,并触发解析器中绑定的相应回调函数,从而跟新视图。
(3)Compile:指令解析器,用于对每个元素节点指令的扫描和解析,以及绑定对应的回调函数;
(4)MVVM 入口函数
# 实现代码
# Observer
Observer 的主要功能是监听数据对象的所有属性,通过遍历所有属性,为属性添加订阅者,并在属性发生变化后通知订阅者。
// Observer
const Observer = function(data) {
this.data = data;
this.mapToConvet(data);
};
const observer = function(data) {
if (!data || typeof data !== "object") {
return;
}
return new Observer(data);
};
Observer.prototype = {
constructor: Observer,
mapToConvet: function(data) {
const _this = this;
// 遍历每个属性进行监听
Object.keys(data).forEach((key) => {
_this.defineReactive(data, key, data[key]);
});
},
defineReactive: function(data, key, val) {
const dep = new Dep();
// 监听子属性
let childObj = observer(val);
Object.defineProperty(data, key, {
enumerable: true, // 可枚举
configurable: false, // 不能再define
get: function() {
// 需要为属性添加订阅者,而订阅者是Watcher,因此需要在闭包内添加 watcher,所以通过Dep定义一个全局target属性,暂存 watcher, 添加完移除
if (Dep.target) {
dep.depend();
}
console.log("get: ", key, " = ", val);
return val;
},
set: function(newVal) {
if (val === newVal) {
return;
}
console.log("change detected: ", val, " -> ", newVal);
val = newVal;
// 新的值是object的话,需要进行监听
childObj = observer(newVal);
// 通知所有订阅者
dep.notify();
},
});
},
};
# Dep
Dep 相当于发布者的角色,主要负责收集订阅者,并在属性更新后触发 notify,去通知对应的订阅者,调用订阅者的 update。
let uid = 0;
const Dep = function() {
//每个Dep都有唯一的ID
this.id = uid++;
//subs用于存放依赖
this.subs = [];
};
// 用于暂时缓存 watcher
Dep.target = null;
Dep.prototype = {
// 向subs数组添加依赖
addSub: function(sub) {
this.subs.push(sub);
},
// 移除依赖
removeSub: function(sub) {
const index = this.subs.indexOf(sub);
if (index > -1) {
this.subs.splice(index, 1);
}
},
// 设置某个Watcher的依赖
// 这里添加了Dep.target是否存在的判断,目的是判断是不是Watcher的构造函数调用
// 也就是说判断是否通过的this.get调用的,而不是普通调用
depend: function() {
if (Dep.target) {
// 触发watcher的addDep
Dep.target.addDep(this);
}
},
notify: function() {
this.subs.forEach(function(sub) {
// 调用订阅者的update方法,通知变化
sub.update();
});
},
};
# Watcher
Watcher,作为订阅者主要做的事情是:
(1)需要在自身实例化的时候往属性订阅器里添加自己;
(2)自身需有一个 update()
的方法;
(3)在属性变动触发属性订阅器 dep.notice()
时,能调用自身的 update()
方法,并且进一步触发 Compile 中绑定的更新回调函数。
/**
@param vm 节点
@param exp 节点指令绑定的变量名/函数事件名
@param cb 节点指令的更新回调函数
*/
const Watcher = function(vm, expOrFn, cb) {
this.callback = cb;
this.vm = vm;
this.exp = expOrFn;
// 自己被哪些订阅收集器给收集
this.depsIds = {};
if (typeof expOrFn === "function") {
this.getter = expOrFn;
} else {
this.getter = this.parseGetter(expOrFn.trim());
}
// 此处为了触发属性的getter,从而在dep添加自己
this.value = this.get();
};
Watcher.prototype = {
constructor: Watcher,
get: function() {
// 将当前订阅者指向自己
// 通过Dep.target = watcherInstance标记订阅者是当前watcher实例,强行触发属性定义的getter方法,getter方法执行的时候,就会在属性的订阅器dep添加当前watcher实例,从而在属性值有变化的时候,watcherInstance就能收到更新通知。
Dep.target = this;
// 强制触发执行监听器里的getter,把自己添加到属性订阅器中
const value = this.getter.call(this.vm, this.vm);
// 添加完毕,释放自己重置
Dep.target = null;
return value;
},
parseGetter: function(exp) {
// 不匹配字母、数字、下划线、点号和$符号
if (/[^\w.$]/.test(exp)) return;
const exps = exp.split(".");
return function(obj) {
for (let i = 0; i < exps.length; i++) {
if (!obj) return;
obj = obj[exps[i]];
}
return obj;
};
},
update: function() {
// dep.notice()触发通知后执行
this.run();
},
run: function() {
const newVal = this.get();
const oldVal = this.value;
if (newVal !== oldVal) {
// 更新最新值
this.value = newVal;
// 执行Compile中绑定的回调,更新视图
this.callback.call(this.vm, newVal, oldVal);
}
},
addDep: function(dep) {
// 1.每次调用 run 的时候都会触发相应属性的getter,然后触发到这里的addDep。进而往属性订阅器里面添加自己,并将这个属性发布者id存进depsId里。
// 2.假如相应属性的dep.id已存在watcher的depIds里,说明这不是个新属性,仅需改变其值,而不需将当前watcher添加至该属性的dep里。
// 3.假如相应属性是新的属性,则将当前watcher添加到新属性订阅器dep里。
// 若通过 vm.child = {name: 'a'},改变了 child.name 的值,child.name就是个新属性,则需要将当前 watcher(child.name) 加入到新的child.name的dep中
// 若通过 child.name = xxx 进行重新赋值的时候,对应的 watcher 则不会收到通知,等于失效了
// 4.每个子属性的 watcher 添加到子属性的 dep 同时,也会添加到父属性的 dep
// 监听子属性的同时监听父属性的变更,这样子父属性改变时,子属性的 watcher 也能收到通知进行 update
// 这一步是在 this.get() -> this.getVMVal 中完成,forEach 时会从父级开始取值,间接调用了它的 getter,进而触发了 addDep()
// addDep()中,在整个forEach过程,当前wacher都会加入到每个父级过程属性的dep
// 如: 当前 wacther 是 child.child.name,那么 child、child.child、child.child.name 这三个属性的dep都会加入当前watcher
if (!this.depsIds.hasOwnProperty(dep.id)) {
dep.addSub(this);
this.depsIds[dep.id] = dep;
}
},
};
# Compile
对 template 模板中进行编译,编译成真正的 html,在编译的过程中对 vue 的指令解析。
Compile 主要做的事情是:
1) 首先是深度遍历 dom 树,遍历每个节点以及子节点;
2) 解析模板指令,将模板的变量替换成数据,然后初始化渲染页面视图;
3) 添加监听数据的订阅者,将每个指令对应的节点绑定更新函数。一旦数据有变动,收到通知,更新视图。
const Compile = function(el, vm) {
this.$vm = vm;
this.$el = this.isElementNode(el) ? el : document.querySelector(el);
if (this.$el) {
// 因为遍历解析过程有多次操作DOM节点,为提高性能和效率,会先将vue实例根节点的 el 转换成文档碎片 fragments 进行解析编译操作,再将 fragments 添加回原来的真实Dom节点中
this.$fragments = this.nodeTofragments(this.$el);
// 初始化
this.init();
this.$el.appendChild(this.$fragments);
}
};
Compile.prototype = {
constructor: Compile,
isElementNode: function(node) {
// nodeType 为 1 时代表为元素 Element
return node.nodeType === 1;
},
isTextNode: function(node) {
// nodeType 为 3 时代表为元素 Element
return node.nodeType === 3;
},
isDirective: function(attr) {
return attr.indexOf("v-") === 0;
},
isEventDirective: function(directive) {
return directive.indexOf("on") === 0;
},
init: function() {
this.compileElement(this.$fragments);
},
nodeTofragments: function(el) {
const fragments = document.createDocumentfragments();
let child;
// 将原生节点拷贝到 fragments
// while(child = el.firstChild) 执行两个操作:
// 先把el.firstChild(即el.children[0])抽出赋值给child,然后将child插入至fragments中。
// 这个操作是移动dom,一旦el.children[0]被抽出,在下次循环执行child = el.firstChild时,读取的是el.children[1]
while ((child = el.firstChild)) {
// 将从文档树中删除,然后按序重新插入fragments当前节点的 childNodes[] 数组的末尾
// 相当于把el中节点移动过去
fragments.appendChild(child);
}
return fragments;
},
// 遍历所有节点及其子节点,进行扫描解析编译,调用对应的指令渲染函数进行数据渲染,并调用对应指令更新函数进行绑定
compileElement: function(el) {
const childNodes = el.childNodes;
const _this = this;
[].slice.call(childNodes).forEach(function(node) {
// 获取节点文本内容
const text = node.textContent;
// 模板数据匹配正则
const reg = /\{\{(.*)\}\}/;
// 根据不同节点类型选择编译方式
if (_this.isElementNode(node)) {
_this.compileElementNode(node);
} else if (_this.isTextNode(node) && reg.test(text)) {
_this.compileTextNode(node, RegExp.$1.trim());
}
// 遍历编译其子节点
if (node.hasChildNodes() && node.childNodes.length) {
_this.compileElement(node);
}
});
},
compileElementNode: function(node) {
const _this = this;
const nodeAttrs = node.attributes;
[].slice.call(nodeAttrs).forEach(function(attr) {
// 规定:指令以 v-xxx 命名
// 如:v-on:click="open"、v-text="content"
const attrName = attr.name;
// 处理指令属性
if (_this.isDirective(attrName)) {
// 指令绑定的函数事件名
const exp = attr.value;
// 如:on:click、text
const directive = attrName.substring(2);
// 事件指令
if (_this.isEventDirective(directive)) {
compileUtils.eventHandler(node, _this.$vm, exp, directive);
// 普通指令
} else {
compileUtils[directive] &&
compileUtils[directive](node, _this.$vm, exp);
}
node.removeAttribute(attrName);
}
});
},
compileTextNode: function(node, exp) {
compileUtils.text(node, this.$vm, exp);
},
};
const compileUtils = {
// 事件处理
eventHandler: function(node, vm, exp, directive) {
const eventType = directive.split(":")[1];
const fn = vm.$options.methods && vm.$options.methods[exp];
if (eventType && fn) {
node.addEventListener(eventType, fn.bind(vm), false);
}
},
model: function(node, vm, exp) {
this.bind(node, vm, exp, "model");
const _this = this;
let val = this._getVMVal(vm, exp);
node.addEventListener("input", function(e) {
const newVal = e.target.value;
if (val === newVal) {
return;
}
_this._setVMVal(vm, exp, newVal);
val = newVal;
});
},
text: function(node, vm, exp) {
this.bind(node, vm, exp, "text");
},
bind: function(node, vm, exp, directive) {
const updaterFn = updater[directive + "Updater"];
// 第一次初始化视图
updaterFn && updaterFn(node, this._getVMVal(vm, exp));
// 实例化订阅者,此操作会在对应的属性消息订阅器中添加该订阅者 watcherInstance
new Watcher(vm, exp, function(value, oldVal) {
// 一旦属性值有变化,会收到通知执行此更新函数,更新视图
updaterFn && updaterFn(node, value, oldVal);
});
},
_getVMVal: function(vm, exp) {
let val = vm;
exp = exp.split(".");
// 获取链式调用最后一个事件的值??
exp.forEach(function(k) {
val = val[k];
});
return val;
},
_setVMVal: function(vm, exp, value) {
let val = vm;
exp = exp.split(".");
// 执行链式调用最后一个事件并赋值
exp.forEach(function(key, index) {
if (index < exp.length - 1) {
val = val[key];
} else {
// 执行最后一个key,更新val
val[key] = value;
}
});
},
};
// 更新函数
const updater = {
textUpdater: function(node, value) {
node.textContent = typeof value == "undefined" ? "" : value;
},
htmlUpdater: function(node, value) {
node.innerHTML = typeof value == "undefined" ? "" : value;
},
modelUpdater: function(node, value) {
node.value = typeof value == "undefined" ? "" : value;
},
classUpdater: function(node, vlaue, oldValue) {
const className = node.className;
className = className.replace(oldValue, "").replace(/\s$/, "");
const space = className && String(value) ? " " : "";
node.className = className + space + value;
},
};
# MVVM
MVVM 是数据绑定的入口,整合了 Observe、Compile 和 Watcher 三者。
简单的 MVVM 构造器实现是:
const MVVM = function(options) {
this.$options = options;
this._data = this.$options.data;
const data = this._data;
const _this = this;
// 属性代理,实现 vm.xxx === vm._data.xxx
Object.keys(data).forEach(function(key) {
_this._proxy(key);
});
this._initComputed();
observer(data, this);
this.$compile = new Compile(options.el || document.body, this);
};
MVVM.prototype = {
constructor: MVVM,
// 通过 Object.defineProperty()来劫持vm实例对象属性的读写权限,使得对 vm.xxx 的读写都转成 vm._data.xxx的方式
_proxy: function(key) {
const _this = this;
Object.defineProperty(this, key, {
configurable: false,
enumerable: true,
get: function() {
return _this._data[key];
},
set: function(newVal) {
_this._data[key] = newVal;
},
});
},
_initComputed: function() {
const _this = this;
const computed = this.$options.computed;
if (typeof computed === "object") {
Object.keys(computed).forEach(function(key) {
Object.defineProperty(_this, key, {
get:
typeof computed[key] === "function"
? computed[key]
: computed[key].get,
set: function() {},
});
});
}
},
};
- 实现例子
<div id="mvvm-app">
<input type="text" v-model="word" />
<p>{{word}}</p>
<button v-on:click="sayHi">change model</button>
</div>
var vm = new MVVM({
el: "#mvvm-app",
data: {
word: "Hello World!",
},
methods: {
sayHi: function() {
this.word = "Hi, everybody!";
},
},
});
参考文章:
https://www.freesion.com/article/62001224032/
https://www.cnblogs.com/libin-1/p/6893712.html
https://github.com/DMQ/mvvm
https://segmentfault.com/a/1190000016208088#item-3
https://blog.51cto.com/zhoulujun/2350337
# Vue3.x 响应式数据原理
Vue3.x 和 Vue2.x 本质上就是基于 Proxy
和基于 Object.defineProperty
之间的差异。
# Vue2.x 的响应规则
对象: 递归遍历每个属性,给每个属性增加 getter 和 setter;数组: 重写数组的方法,调用数组方法会触发更新,也会监控数组中的每一项;缺点: 对象只能监控自带属性,新增属性不能监控(除非 vue.$set
);数组的索引更新或长度更新不会触发实体更新,针对数组需要重写数组方法。
export function set(target: Array<any> | Object, key: any, val: any): any {
// target 为数组
if (Array.isArray(target) && isValidArrayIndex(key)) {
// 修改数组的长度, 避免索引>数组长度导致splcie()执行有误
target.length = Math.max(target.length, key);
// 利用数组的splice变异方法触发响应式
target.splice(key, 1, val);
return val;
}
// key 已经存在,直接修改属性值
if (key in target && !(key in Object.prototype)) {
target[key] = val;
return val;
}
const ob = (target: any).__ob__;
// target 本身就不是响应式数据, 直接赋值
if (!ob) {
target[key] = val;
return val;
}
// 对属性进行响应式处理
defineReactive(ob.value, key, val);
ob.dep.notify();
return val;
}
# Vue3.x 的 proxy
Vue3.x 为什么要用 proxy 替代 defineProperty?
Proxy 在对目标对象的操作之前提供了拦截,可以对外界操作进行过滤或改写。通过操作对象的代理对象来间接操作对象,而无需直接操作对象本身。例子:
const obj = { name: { name: "hhh" }, arr: ["eat", "drink", "play"] };
const handler = {
get(target, key) {
console.log("get:", target);
return target[key];
},
set(target, key, value) {
target[key] = value;
console.log("set:", target);
},
};
const proxyObj = new Proxy(obj, handler);
console.log(proxyObj.arr);
// get: {arr: ["eat", "drink", "play"], name: "ccc"}
// ["eat", "drink", "play"]
proxyObj.name = "ccc";
// set: {name: "ccc", arr: ["eat", "drink", "play"]}
Proxy 支持的拦截操作一共 13 种,更详细的可以戳 MDN (opens new window)。
新的写法中会用 Reflect
处理,Reflect 是内置对象,为操作对象而提供的新 API。
将 Object 对象的属于语言内部的方法放到 Reflect 对象上,即从 Reflect 对象上拿 Object 对象内容方法。
上面的例子改写:
const obj = { name: { name: "hhh" }, arr: ["eat", "drink", "play"] };
const handler = {
get(target, key) {
console.log("get:", target, target[key]);
// Reflect 不是函数对象,因此不能和 new 运算符一起用,类似于 Math 对象,这个方法里面包含了多个和 Object 对应 API
return Reflect.get(target, key);
},
set(target, key, value) {
// 这种写法设置时如果不成功也不会报错 比如这个对象默认不可配置
// target[key] = value;
console.log("set:", target, key, target[key], " -> ", value);
return Reflect.set(target, key, value);
},
};
const proxyObj = new Proxy(obj, handler);
console.log(proxyObj.arr);
// get: {arr: ["eat", "drink", "play"], name: "ccc"}
// ["eat", "drink", "play"]
proxyObj.name = "ccc";
// set: {name: "ccc", arr: ["eat", "drink", "play"]},name,{name: "hhh"} -> ccc
# Proxy 代理对象是数组时,多次触发 set / get,如何优化?
由上一个问题的最后实例代码可知,当 Proxy 代理一个数组时,会引起多次 get 或 set 的问题。
set 会执被触发两次,一次赋值时触发,另外还会在修改 length 时触发。对此,Vue3.0 的优化方法:
let obj = { name: { name: "hhh" }, arr: ["eat", "drink", "play"] };
const reactive = function(data, fn) {
const handler = {
get(target, key, receiver) {
console.log("get:", target, key, target[key]);
if (typeof target[key] === "object" && target[key] !== null) {
// 递归代理
return reactive(target[key]);
}
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
const hadKey = Object.hasOwnProperty.call(target, key);
const oldVal = target[key];
const result = Reflect.set(target, key, value, receiver);
// 新增属性首次触发set,hadKey为false,会触发
// 修改属性时,hadKey为false,当属性值和旧值不一样时会触发
// 当代理对象为数组时,传入的key为length时,length为自身属性因此hadKey为false,接着因为value是数组当前的length,oldValue也为target['length'],因此判断oldVal !== value的值也为false,所以会跳过trigger,实现重复触发trigger的效果
if (!hadKey || oldVal !== value) {
console.log(
"trigger set:",
target,
key,
oldVal,
" -> ",
value
);
}
return result;
},
};
return new Proxy(data, handler);
};
const proxyObj = reactive(obj);
proxyObj.arr.push("sleeping");
// get: {name: {…}, arr: ["eat", "drink", "play"]} arr ["eat", "drink", "play"]
// get: ["eat", "drink", "play"] push ƒ push() { [native code] }
// get: ["eat", "drink", "play"] length 3
// set: ["eat", "drink", "play", "sleeping"] 3 undefined -> sleeping
proxyObj.arr[1] = "play";
// get: {name: {…}, arr: ["eat", "drink", "play"]} arr ["eat", "drink", "play"]
// trigger set:["eat", "play", "play"] 1 drink -> play
# Proxy 深度侦测数据
Proxy 只会代理对象的第一层,Vue3 是怎样处理这个问题(深度侦测数据)的呢?
判断当前 Reflect.get 的返回值是否为 Object,如果是则再通过 reactive 方法做代理, 这样就实现了深度观测。
proxy 可以直接监听整个对象。省去了对对象属性 for in 的遍历过程,提升了效率,并且可以监听数组,不用再去单独的对数组做特异性操作。
但值得注意的是,当对象是多层嵌套的对象时,对多层级的对象操作时,set 并不能感知到,但是 get 会被触发,此时可利用 Reflect.get() 返回的“多层级对象中内层”,判断返回值是否为 Object,如果是则再对“内层数据” reactive(递归进行new proxy()
)方法做代理,这样才能实现深度观测。
// 我们可以通过它们,找到任何代理过的数据是否存在,以及通过代理数据找到原始的数据。
// 存放原始数据
const rawToReactive = new WeakMap();
// 存放响应数据
const reactiveToRaw = new WeakMap();
const handler = {
get(target, key, receiver) {
let result = Reflect.get(target, key, receiver);
console.log("get:", target, key, target[key]);
return typeof result === "object" ? reactive(target[key]) : result;
},
set(target, key, value, receiver) {
const hadKey = Object.hasOwnProperty.call(target, key);
const oldVal = target[key];
// 从响应数据中获取
value = reactiveToRaw.get(value) || value;
const result = Reflect.set(target, key, value, receiver);
// 新增属性首次触发set,hadKey为false,会触发
// 修改属性时,hadKey为false,当属性值和旧值不一样时会触发
// 当代理对象为数组时,传入的key为length时,length为自身属性因此hadKey为false,接着因为value是数组当前的length,oldValue也为target['length'],因此判断oldVal !== value的值也为false,所以会跳过trigger,实现重复触发trigger的效果
if (!hadKey || oldVal !== value) {
console.log("trigger set:", target, key, oldVal, " -> ", value);
}
return result;
},
};
const reactive = function(target) {
let observed = rawToReactive.get(target);
// 原数据已经有相应的可响应数据, 返回可响应数据
if (observed) {
return observed;
}
// 原数据已经是可响应数据
if (reactiveToRaw.has(target)) {
return target;
}
observed = new Proxy(target, handler);
// 原数据保存对应的可响应数据
reactiveToRaw.set(target, observed);
// 保存响应数据
rawToReactive.set(observed, target);
return observed;
};
// 实例
const obj = { name: { name: "hhh" }, arr: ["eat", "drink", "play"] };
const proxyObj = reactive(obj);
proxyObj.name.name = "ccc";
// get: {name: {name: "ccc"}, arr: Array(3)} name {name: "hhh"}
// trigger set: {name: "ccc"} name hhh -> ccc
proxyObj.arr.push("sleeping");
// get:{name: {name: "ccc"}, arr: ["eat", "drink", "play"]} arr ["eat", "drink", "play"]
// get:["eat", "drink", "play"] push ƒ push() { [native code] }
// get:["eat", "drink", "play"] length 3
// trigger set: (4) ["eat", "drink", "play", "sleeping"] 3 undefined -> sleeping
参考文章: https://juejin.im/post/5d99be7c6fb9a04e1e7baa34
由上代码可知, Vue3 也并非简单的通过 Proxy 来递归侦测数据。深度侦测数据主要利用 get 操作递归侦测来实现内部数据的代理,并且结合 WeakMap 来对数据保存,这将大大提高响应式数据的性能。