vue 官网这样定义 vuejs:渐进式的javascript框架: 如何理解渐进式:若是你有一个现成的服务端应用,你能够将 vue 做为应用的一部分嵌入到应用中以带来更加丰富的业务体验,若是你想要更多的交互逻辑放在前端实现,vue的核心库以及生态也能够知足需求javascript
通俗的讲响应式就是视图层与数据层的双向绑定。响应式有两个核心要素:视图改变,更新数据;数据改变,变化视图。这意味着咱们在开发的时候只须要进行数据管理,不须要频繁操做dom元素。html
视图变化更新数据,其实比较容易实现,事件监听便可实现,好比input标签监听 'input' 事件。可是当数据改变的时候,如何更新视图呢?这就须要知道什么时候数据发生变化,当知道数据发生变化,咱们就能够对视图进行更新。前端
vue2.0是用 Object.defineProperty() 方法中setter与getter的观察者模式。vue
vue3.0是基于 Proxy 代理对属性的拦截,实现双向绑定。java
Object.defineProperty() 会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。 这个函数接受三个参数,一个参数是obj,表示要定义属性的对象,一个参数是 prop ,是要定义或者更改的属性名字,另外是 descriptor 描述符,来定义属性的具体描述。Object.defineProperty(obj, prop, descriptor)
react
javascript中一共有三种属性:git
普通属性: 即常规的数据属性,这种属性是用户来添加修改等,把它设置成什么样,就返回出来什么样,不会作些额外的事情。 咱们一般使用点或者方括号对普通属性进行赋值和访问es6
let obj = {};
obj.x = 1;
obj['y']= 12;
console.log(obj.x); // 1
console.log(obj.y); // 12
let proprrty = Object.keys(obj);
console.log(proprrty) // ["x", "y"]
delete obj.x;
console.log(obj.x); // undefined
复制代码
内部属性: 好比数组arr的length属性,函数的prototype属性,DOM节点的innerHTML属性,用户赋完值之后,取值的时候,不必定按预期,有时候还会额外的作一些事情,也难以改变他们的行为。 好比说某一数组,它的长度为10, 当咱们设置它为11时,它就会增长一个undefined元素,再设置为9时,就会从后面删掉两个元素。(后文数组监听能够观察到此变化) 函数的prototype若是被改变,至关于将其父类改变了,会new不一样类型的实例。 DOM的innerHTML,咱们赋值时是一个字符串,再取出时,这字符串可能会与原来的不同, 而且在原元素上生成了不同的子节点。github
访问器属性: 即经过 Object.defineProperty() 定义的属性,用户在赋值或取值都通过预先设定的函数,从而实现内部属性的那一种特殊效果,。数组
对于普通属性,对象的属性能够修改能够删除也能够枚举,可是经过 Object.defineProperty() 定义属性,经过描述符的设置能够进行更精准的实现对控制对象属性的控制。
属性描述符分为两种:数据描述符与存取描述符。 数据描述符是一个具备值的属性,该值能够是可写的,也能够是不可写的。(value, writable, configurable, enumerable). 存取描述符是由 getter 函数和 setter 函数所描述的属性。(get, set, configurable, enumerable). 布尔值的键configurable、enumerable 和 writable 的默认值都是 false。 属性值和函数的键 value、get 和 set 字段的默认值为 undefined。
数据描述符
const obj = {};
// x属性 value不可修改 Writable Enumerable 不可修改
Object.defineProperty(obj, 'x', {
value: 1,
// 能否修改 默认false
writable: false,
// 能否被删除,以及除 value 和 writable 特性外的其余特性是否能够被修改
configurable: false,
// 能否在for...in循环和Object.keys() 中被枚举
enumerable: false,
});
obj.x = 77;
console.log(obj.x); // 1
delete obj.x
let property = Object.keys(obj);
console.log(property) //[]
Object.defineProperty(obj, 'x', {configurable: true}) // throw error
// y属性 value不可修改 Writable Enumerable 可修改
Object.defineProperty(obj, 'y', {
value: 1,
writable: false,
configurable: true,
enumerable: false,
});
obj.y = 12;
console.log(obj.y); // 1
// configurable true 可修改 writable enumerable
Object.defineProperty(obj, 'y', {writable: true});
obj.y = 12;
console.log(obj.y); // 12
property = Object.keys(obj);
console.log(property) //[]
Object.defineProperty(obj, 'y', {enumerable: true});
property = Object.keys(obj);
console.log(property) //["y"]
复制代码
存取描述符
const obj = {};
let val;
Object.defineProperty(obj, 'x', {
get() {
console.log('触发get')
return val;
},
set(newValue) {
console.log('触发set')
val = newValue;
},
})
obj.x = 12; // 触发set
console.log(obj.x) // 触发get 12
复制代码
实现代码:
<div>
<input type="text" id="value"/>
<span id="bindValue"></span>
</div>
复制代码
// 视图交互控制
let inputEle = document.getElementById('value');
let spanEle = document.getElementById('bindValue');
const MessageObj = {};
Object.defineProperty(MessageObj, 'msg', {
set: function (newVal) {
inputEle.value = newVal;
spanEle.innerText = newVal
}
})
// 监听input输入 视图变化 更新数据
inputEle.addEventListener('keyup', function (event) {
MessageObj.msg = event.target.value
})
复制代码
实现效果:
vue.js 则是采用数据劫持结合观察者模式(发布者-订阅者模式)的方式,经过 Object.defineProperty() 来劫持各个属性的 setter,getter,在数据变更时发布消息给订阅者,触发相应的监听回调。 经过上文,咱们已经对Object.defineProperty()有了必定了解,那么咱们继续看什么是观察者模式。 概念
**观察者模式(Observer)**一般又被称为 发布-订阅者模式 或 消息机制,它定义了对象间的一种一对多的依赖关系,只要当一个对象的状态发生改变时,全部依赖于它的对象都获得通知并被自动更新,解决了主体对象与观察者之间功能的耦合,即一个对象状态改变给其余对象通知的问题。
上文的定义比较官方,总而言之,订阅者模式分为注册和发布两个环节,咱们能够用生活中的场景更加通俗的理解。
故事性的通俗理解
假设咱们去银行办业务,由于办业务的人比较多,咱们会被要求在取号机上去一个号码,这就是注册环节。银行职员与办理业务的人员就是一对多的依赖关系,银行职员对号牌任务一个一个进行处理,没处理完一个任务,都须要通知全部依赖对象(办业务人员)当前状态的改变,银行办公大厅的led显示牌会时刻显示当前正在办业务的号码以及下一位将要办理业务的号码,银行处理业务的职员就至关于发布者,led显示屏维护了一个观察者列表,时刻关注led显示屏的咱们就是订阅者。当轮到咱们的号牌办理业务时,咱们就会被通知到指定柜台办理业务,这就是发布环节。
简单的观察者模型
function observer () {
this.dep = [];
this.register = (fn) => {
this.dep.push(fn);
}
this.notify = () => {
this.dep.forEach(item => item())
}
}
let bankObserver = new observer();
bankObserver.register( () => {console.log("取现金")} );
bankObserver.register( () => {console.log("办卡")} );
bankObserver.notify();
复制代码
经过上面例子咱们能够提炼出观察者模式的三个要素:发布者、订阅者、缓存列表。简而言之观察者模式就是一个对象(发布者)维护一个依赖他的对象(缓存列表)列表,当自身状态发生变化时,自动通知全部订阅者。当某个对象不须要得到通知时,能够从对象列表中删除掉。
javascript中的观察模式
JavaScript是一个事件驱动型语言,观察模式的实现主要依靠事件模型,例如经常使用的一些onclick、 attachEvent 、addEventListener 都是观察者模式的具体应用。
// html
<span id="span-click">点击</span>
// javascript
let spanEle = document.getElementById('span-click');
spanEle.addEventListener('click',function(){
alert('我是一个观察者,你一点击,我就知道了');
});
复制代码
依赖就是一个个的大括号表达式和指令表达式
// v-model="value" 是一种依赖 v-指令(非事件) 都是依赖
<input type="text" id="value" v-model="value"/>
// {{bindValue}} 也是依赖 大括号表达式都是一个依赖
<span>{{bindValue}}</span>
复制代码
很显然,每个依赖都对应着data中的数据,所以依赖能够简单理解为视图层对数据层的关系:视图层显示依赖于数据层的数据-依赖关系。依赖收集的目的就是为了应对未来数据的变化,从而当依赖的数据发生变化时可以准确的批量的更新依赖所在地的显示。
数据流程图
代码实现以下:
// 定义观察者 一个数据监听器 若是要对全部属性都进行监听的话,那么能够经过递归方法遍历全部属性值,并对其进行Object.defineProperty () 处理
function observe(data){
if (!data || typeof data !== 'object') {
return;
}
Object.keys(data).forEach((key)=>{
defineReactive(data, key, data[key])
})
}
function defineReactive (obj, key, value){
let dep = new Dep();
observe(value);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
set: (newValue) => {
console.log('值被设置')
if( newValue !== value ){
value = newValue;
console.log(dep,'dep')
//更新视图
dep.notify();
}
},
get: () => {
console.log('值被获取',Dep)
if(Dep.target) {
dep.addSub(Dep.target);
}
return value;
},
})
}
// 依赖收集 订阅者列表
// 主要负责收集订阅者,而后再属性变化的时候执行对应订阅者的更新函数
function Dep () {
this.subs = [];
}
Dep.prototype = {
addSub: function(sub) {
this.subs.push(sub);
},
notify: function() {
console.log(this.subs,'this.subs')
this.subs.forEach(function(sub) {
sub.update();
});
}
};
// 订阅者
function Watcher(data, key,cb) {
this.cb = cb;
this.data = data;
this.key = key;
// 此处为了触发属性的getter,从而在dep添加本身,结合Observer更易理解
this.value = this.get();
}
Watcher.prototype = {
update: function() {
this.run(); // 属性值变化收到通知
},
run: function() {
var value = this.get(); // 取到最新值
console.log(value,'取到最新值')
var oldVal = this.value;
if (value !== oldVal) {
this.value = value;
this.cb(); // 执行Compile中绑定的回调,更新视图
}
},
get: function() {
Dep.target = this; // 将当前订阅者指向本身
var value = this.data[this.key]; // 触发getter,添加本身到属性订阅器中
Dep.target = null; // 添加完毕,重置
return value;
}
};
function SelfVue (data, key, cb) {
this.data = data;
observe(data);
cb(); // 初始化模板数据的值
new Watcher(this.data, key,cb);
return this;
}
复制代码
<script src="./vue/vue.js"></script>
<div id="box">
<span id="app"></span>
<span id="add" style="margin-left: 10px;display: inline-block;width: 10px;height: 10px;cursor: pointer;">+</span>
</div>
<script> let ele = document.getElementById('app'); let btn = document.getElementById('add'); let o = { number: 1, } var selfVue = new SelfVue(o , 'number', ()=>{ ele.innerHTML = o.number} ); btn.addEventListener('click', () => { o.number += 1; },false) </script>
复制代码
效果实现以下:
defineProperty()与观察者模式是vue2.0实现双向绑定的方式,而vue3.0使用es6的新语法中的代理内建工具Proxy和发射工具reflect来实现,接下来咱们看下如何用Proxy实现一样的功能。
proxy的概念
Proxy 对象用于定义基本操做的自定义行为(如属性查找、赋值、枚举、函数调用等)。
proxy英文原意是代理的意思,在ES6中,能够翻译为"代理器"。 它主要用于改变某些操做的默认行为,proxy在目标对象的外层搭建了一层拦截,外界对目标对象的某些操做,必须经过这层拦截。 此方法接受两个参数:target — 要使用 Proxy 包装(处理)的目标对象(能够是任何类型的对象,包括原生数组,函数,甚至另外一个代理)。handler — 一般以函数做为属性的对象,各属性中的函数分别定义了在执行各类操做时代理 p 的行为。
基本用法
数据拦截:支持13 种的拦截,相对Object.defineProperty更加丰富。 示例代码以下: (1) Object数据拦截
let target = {};
let handler = {
// 拦截对象属性的读取,好比proxy.a和proxy['a']
get (target, key) {
console.info(`Get on property "${key}"`);
return target[key];
},
// 拦截对象属性的设置,好比proxy.a = 1 和proxy['a'] = 1
set (target, key, value) {
console.info(`Set on property "${key}" new value "${value}"`);
target[key] = value;
},
// 拦截propKey in proxy 返回布尔值
has (target, key) {
console.log(`is "${key}" in object ?`);
// 隐藏 某些属性
if( key === 'a'){
return false;
}else{
return key in target;
}
// return key in target;
},
// delete 操做符的捕捉器 删除前拦截 进行处理 返回布尔值
deleteProperty(target, key){
console.log(`delete key: "${key}"`);
if(key === 'a'){
delete target[key];
}else{
return target[key];
}
},
// 拦截对象自身属性的读取操做,具体拦截如下操做:Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols object.keys()
ownKeys (target) {
console.log(`key in target`)
// (1) 正常返回
// return Reflect.ownKeys(target);
// (2)error 'ownKeys' on proxy: trap result did not include 'd'
// enumerable 的属性必须在数组中返回
// return ['b']
// (3)
return ['b','d']
},
// 拦截Object.setPrototypeOf方法
setPrototypeOf (target, newProto) {
console.log(`new prototype set object`);
return true
}
// 当读取代理对象的原型时,该方法就会被调用。 返回值必须是一个对象或者null
// Object.getPrototypeOf .instanceof
// .__proto__ object.prototype.isPrototypeOf()
// object.prototype.__proto__
getPrototypeOf (target) {
console.log(`prototype in object`);
// return Object.getPrototypeOf(target);
return null;
},
// 拦截Object.preventExtensions(),且必须在内部调用此方法
preventExtensions(target) {
console.log("called_pre");
Object.preventExtensions(target);
return true;
},
// 拦截Object.isExtensible 默认状况下,对象是可扩展的:便可觉得他们添加新的属性。
//以及它们的 __proto__ 属性能够被更改。
//Object.preventExtensions,Object.seal 或 Object.freeze 方法均可以标记一个对象为不可扩展(non-extensible)
// 它的返回值必须与目标对象的isExtensible属性保持一致
// Object.isExtensible(proxy) === Object.isExtensible(target)
isExtensible: function(target) {
console.log("called");
return Object.isExtensible(target);;
},
// 拦截Object.getOwnPropertyDescriptor(),返回一个属性描述对象或者undefined。
getOwnPropertyDescriptor(target, key){
if (key === 'b') {
// return; // error 'b' which exists in the non-extensible proxy target
return Object.getOwnPropertyDescriptor(target, key);
}
return Object.getOwnPropertyDescriptor(target, key);
},
// 拦截 Object.defineProperty() 返回布尔值 表示操做成功与否
defineProperty(target, key, descriptor){
console.log(`Object.defineProperty property ${key}`);
Object.defineProperty(target, key, descriptor)
return true;
}
}
let proxy = new Proxy(target, handler);
proxy.a = 1; // Set on property "a" new value "1"
proxy.b = 2; // Set on property "b" new value "2"
console.log(proxy.a);// Get on property "a" // 1
console.log("a" in proxy );// is "a" in object ? // false
console.log("b" in proxy );// is "b" in object ? // true
delete proxy.a; // delete key: "a"
console.log(proxy.a);// Get on property "a" // undefined
delete proxy.b; // delete key: "b"
console.log(proxy.b);// Get on property "b" // 2
proxy.c = 3;
Object.defineProperty(proxy,'d',{
value:'4',
enumerable: false,
})
// (1) 过滤掉三类属性
//目标对象上不存在的属性 属性名为 Symbol 值 不可遍历(enumerable)的属性
// console.log(Object.keys(proxy)); // ['b', 'c']
//(3) 返回拦截 只返回b
console.log(Object.keys(proxy)); // ['b']
let newProxy = {};
Object.setPrototypeOf(proxy,newProxy);// new prototype set object
console.log(Object.getPrototypeOf(proxy));// prototype get object // {}
let flag = Object.isExtensible(proxy);// called
console.log(flag); // true
flag = Object.preventExtensions(proxy)// called_pre
console.log(flag); // Proxy {b: 2, c: 3, d: "4"}
flag = Object.isExtensible(proxy);// called
console.log(flag);//false
console.log("------------");
console.log(proxy) // Proxy {b: 2, c: 3, d: "4"}
flag = Object.getOwnPropertyDescriptor(proxy, 'a');
console.log(flag) // undefined
flag = Object.getOwnPropertyDescriptor(proxy, 'c');
console.log(flag) // {value: 3, writable: true, enumerable: true, configurable: true}
flag = Object.getOwnPropertyDescriptor(proxy, 'b'); // 不可扩展必须返回
console.log(flag) // {value: 2, writable: true, enumerable: true, configurable: true}
console.log("+++++++++++++++");
// Object.defineProperty(proxy, 'name', {
// value: 'proxy',
// }); // error the non-extensible proxy target 对象不可扩展了 前面代码执行的结果
Object.defineProperty(proxy, 'c', {
value: 'proxy',
});
console.log(proxy) // Proxy {b: 2, c: "proxy", d: "4"}
复制代码
(2)函数调用拦截 apply construct
let handler1 = {
//拦截函数调用、call、apply操做 三个参数分别是:
//1.目标对象 2.目标对象的上下文对象(this) 3.目标对象的参数数组
apply (target, thisArg, argumentsList) {
console.log(`Calculate sum: ${argumentsList}`);
return 'Proxy ok'
},
// 用于拦截new命令 返回的必须是一个对象
construct (target, args, newTarget) {
console.log(`args is ${args}`)
return new target('修改的小明');
}
}
function sum (a,b){
return a+b;
}
let proxy1 = new Proxy(sum, handler1)
console.log(sum(1,2)); // 3
console.log(proxy1(1,2));// Calculate sum: 1,2 // Proxy ok
function person (name) {
this.name = name;
}
let proxy2 = new Proxy(person, handler1)
let person1 = new person();
console.log(person1); // person {name: "小明"}
let person2 = new proxy2();
console.log(person2); // person {name: "修改的小明"}
复制代码
示例效果:
示例代码:
<div>
<input type="text" id="value"/>
<span id="bindValue"></span>
</div>
复制代码
let inputEle = document.getElementById('value');
let spanEle = document.getElementById('bindValue');
const MessageObj = {};
let basehandler = {
set(target, key, newVal){
target[key] = newVal
spanEle.innerText = newVal
}
}
let proxy = new Proxy(MessageObj, basehandler)
// 监听input输入
inputEle.addEventListener('keyup', function (event) {
proxy.msg = event.target.value
})
复制代码
Reflect对象与Proxy对象同样,也是 ES6 为了操做对象而提供的新 API。 Proxy代理经过拦截修改某些方法,而Reflect是将一些方法移植到该对象上,使某些方法变为更加合理。现阶段,某些方法同时在Object和Reflect对象上部署,将来的新方法将只部署在Reflect对象上。也就是说,从Reflect对象上能够拿到语言内部的方法。 示例代码以下:
// ES5写法
try{
Object.defineProperty(target,property,attributes);
//success
} catch(e){
//failure
}
//ES6写法
if(Reflect.defineProperty(target,property,attributes)){
//success
} else{
//failure
}
// ES5写法
Function.prototype.apply.call(Math.floor,undefined,[1.75]) //1
//ES6写法
Reflect.apply(Math.floor,undefined,[1.75]) //1
复制代码
Reflect对象一共有 13 个静态方法,大部分与Object对象的同名方法的做用都是相同的,并且它与Proxy对象的方法是一一对应的,能够说只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象能够方便地调用对应的Reflect方法,完成默认行为,做为修改行为的基础。也就是说,无论Proxy怎么修改默认行为,你总能够在Reflect上获取默认行为。 也就是说,Reflect.fn表示handler中的fn的默认行为。 示例用法:
var obj = new Proxy({}, {
get: function (target, key, receiver) {
console.log(`getting ${key}!`);
// 在浏览器console中,get方法会默认打印出值
// 若是没有Reflect.get执行默认行为,就没法正确打印出值,而会打印undefined
return Reflect.get(target, key, receiver);
},
set: function (target, key, value, receiver) {
console.log(`setting ${key}!`);
return Reflect.set(target, key, value, receiver);
}
});
obj.a = 12;
console.log(obj.a)
复制代码
数据流程图
首先用proxy代理完成数据劫持,代码以下:
// 将响应式与原始数据保存 防止重复代理 以下图所示
// let obj = {
// a:1
// }
// let p = new Proxy(obj, basehandler) // 一样数据 重复代理
// let p1 = new Proxy(obj, basehandler) // 一样数据 重复代理
// let p2 = new Proxy(obj, basehandler) // 一样数据 重复代理
// 原始=>响应式 存放代理后的对象
let toProxy = new WeakMap();
// 响应式=>原始 存放代理前的对象
let toRaw = new WeakMap();
// 收集依赖 动态建立依赖关系
function track(target, key){
// 收集依赖
}
// 更新视图
function trigger(target, key, info){
// 更新视图
}
// **** 观察者 代理 与vue2 不一样 ****
const baseHandler = {
get (target, key) {
console.log('收集依赖,数据初始化')
const res = target[key];
// 收集依赖 将effect key 对应起来 订阅
// @todo
track(target, key);
// 判断是否为对象 为对象的话继续代理
return typeof res === "object" ? reactive(res) : res;
},
set (target, key, newVal) {
console.log('数据变化')
const info = {oldValue:target[key], newValue:newVal};
// target[key] = newVal; 若是设置没成功 这对象不能够被设置 会报错 Reflect设置反射 有返回值
const res = Reflect.set(target, key, newVal );
// 触发私有属性才更新 如 arr length变化 不触发更新
if(target.hasOwnProperty(key)){
// 通知更新
// @todo
trigger(target, key, info);
}
return res;
}
}
function reactive (target) {
console.log(target,'响应式数据')
// 若是不是对象 则返回
if(typeof target !== 'object' && target === null){
return target;
}
// 若是代理的数据中存在 则再也不进行代理
let proxy = toProxy.get(target)
if(proxy){
return proxy;
}
// 若是对象已经代理过了 则返回该对象
if(toRaw.has(target)){
return target;
}
// 建立观察者
const observed = new Proxy(target, baseHandler);
toProxy.set(target, observed); // 原对象为键 代理后对象为值
toRaw.set(observed, target); // 原对象为键 代理后对象为值
console.log(observed,'响应完成')
return observed;
}
复制代码
在上文中,咱们知道,全部响应式的数据,在vue2.0中都叫作依赖,vue3.0中叫作effect,即反作用,能够浅显理解为,数据改变的反作用---->更新视图。要想数据变化,依赖可以自动执行视图更新的方法,就须要建立响应式依赖。代码以下:
// let obj = {name:'aa'}
// effect(()=>{
// console.log(obj.name) // 此处触发get 而后收集依赖 将key effect 对应起来
// })
// obj.name = 'bb'
// effect 数据变化 更新视图的方法 默认执行一次进行数据初始化 数据变化 再执行
// 建立响应式依赖,并放在依赖的数组中
let effectStack = [] // {name:effect()} // 收集依赖对应关系
// {
// target:{
// key:[fn,fn,fn]
// }
// }
function effect (fn) {
// 数据变化 函数执行 变成响应式函数
let effect = createReactiveEffect(fn);
effect(); // 默认先执行一次
return effect;
}
function createReactiveEffect(fn){
let effect = function(){ // 响应式的effect
return run(effect,fn); // fn执行 且 存在栈中
};
return effect;
}
function run (effect, fn) {
try{
effectStack.push(effect);
return fn();
}finally{
effectStack.pop();
}
}
复制代码
接下来咱们实现依赖收集以及数据更新页面变化,即执行effect。代码以下:
let targetMap = new WeakMap(); // 收集依赖
// 收集依赖 动态建立依赖关系
function track (target, key) {
let effect = effectStack[effectStack.length-1];
if(effect){ // 有对应关系 才建立关联
let depMap = targetMap.get(target);
if(depMap === undefined){
depMap = new Map();
targetMap.set(target, depMap);
}
let dep = depMap.get(key);
if(dep === undefined) {
dep = new Set();
depMap.set(key, dep);
}
if(!dep.has(effect)){
dep.add(effect)
}
}
}
// 更新视图
function trigger (target, key, info) {
const depMap = targetMap.get(target);
if(!depMap){
return;
}
const effects = new Set();
if(key){
let deps = depMap.get(key);
if(deps){
deps.forEach(effect=>{
effects.add(effect)
})
}
}
effects.forEach(effect => effect())
}
复制代码
<div id="box" style="margin-left: 20px;">
<span id="app"></span>
<span id="add" style="margin-left: 10px;display: inline-block;width: 10px;height: 10px;cursor: pointer;">+</span>
</div>
<script src="./vue/vue3.js"></script>
<script> let ele = document.getElementById('app'); let btn = document.getElementById('add'); let o = { number: 1, } let reactiveData = reactive(o); effect(()=>{ console.log('数据变了') ele.innerHTML = reactiveData.number }) btn.addEventListener('click', () => { reactiveData.number += 1; },false) </script>
复制代码
// vue3.js
// 将响应式与原始数据保存 防止重复代理 以下图所示
// let obj = {
// a:1
// }
// let p = new Proxy(obj, basehandler)
// let p1 = new Proxy(obj, basehandler)
// let p2 = new Proxy(obj, basehandler)
// 原始=>响应式 存放代理后的对象
let toProxy = new WeakMap();
// 响应式=>原始 存放代理前的对象
let toRaw = new WeakMap();
let targetMap = new WeakMap(); // 收集依赖
let effectStack = [] // {name:effect()} // 收集依赖对应关系
// {
// target:{
// key:[fn,fn,fn]
// }
// }
// 收集依赖 动态建立依赖关系
function track (target, key) {
let effect = effectStack[effectStack.length-1];
if(effect){ // 有对应关系 才建立关联
let depMap = targetMap.get(target);
if(depMap === undefined){
depMap = new Map();
targetMap.set(target, depMap);
}
let dep = depMap.get(key);
if(dep === undefined) {
dep = new Set();
depMap.set(key, dep);
}
if(!dep.has(effect)){
dep.add(effect)
}
}
}
// 更新视图
function trigger (target, key, info) {
const depMap = targetMap.get(target);
if(!depMap){
return;
}
const effects = new Set();
if(key){
let deps = depMap.get(key);
if(deps){
deps.forEach(effect=>{
effects.add(effect)
})
}
}
effects.forEach(effect => effect())
}
// 观察者 代理 与vue2 不一样
const baseHandler = {
get (target, key) {
const res = target[key];
// 收集依赖 将effect key 对应起来 订阅
track(target, key);
return typeof res === "object" ? reactive(res) : res; // 判断是否为对象 为对象的话继续代理
},
set (target, key, newVal) {
const info = {oldValue:target[key], newValue:newVal};
// target[key] = newVal; 若是设置没成功 这对象不能够被设置 会报错 Reflect设置反射 有返回值
const res = Reflect.set(target, key, newVal );
// 通知更新
// 触发私有属性才更新 如 arr length变化 不触发更新
if(target.hasOwnProperty(key)){
trigger(target, key, info);
}
return res;
}
}
function reactive (target) {
// 若是不是对象 则返回
if(typeof target !== 'object' && target === null){
return target;
}
// 若是代理的数据中存在 则再也不进行代理
let proxy = toProxy.get(target)
if(proxy){
return proxy;
}
// 若是对象已经代理过了 则返回该对象
if(toRaw.has(target)){
return target;
}
// 建立观察者
const observed = new Proxy(target, baseHandler);
toProxy.set(target, observed); // 原对象为键 代理后对象为值
toRaw.set(observed, target); // 原对象为键 代理后对象为值
console.log(observed,'响应完成')
return observed;
}
// let obj = {name:'aa'}
// effect(()=>{
// console.log(obj.name) // 此处触发get 而后收集依赖 将key effect 对应起来
// })
// obj.name = 'bb'
// effect 数据变化 更新视图的方法 默认执行一次 数据变化 再执行
function effect (fn) {
// 数据变化 函数执行 变成响应式函数
let effect = createReactiveEffect(fn);
effect(); // 默认先执行一次
return effect;
}
function createReactiveEffect(fn){
let effect = function(){ // 响应式的effect
return run(effect,fn); // fn执行 且 存在栈中
};
return effect;
}
function run (effect, fn) {
try{
effectStack.push(effect);
return fn();
}finally{
effectStack.pop();
}
}
复制代码
vue3.0 摒弃了Object.defineProperty() 而采用了 proxy ,最后咱们总结一下这二者的区别:
(1) defineProperty() 只能监听某个属性,不能对全对象监听,proxy监听整个对象,并返回一个新对象,能够省去for in、闭包等内容来提高效率。
let obj1 = {
a: 1,
b: 2,
}
// proxy 写法
let proxy1 = new Proxy(obj1, {
set (target, key, value){
console.log(`setting ${key}!`);
return Reflect.set(target, key, value, receiver);
},
get (target, key){
console.log(`getting ${key}!`);
return Reflect.get(target, key, receiver);
}
})
// Object.defineProperty() 写法
function observe(data){
if(!data || typeof data !== 'object') {
return;
}
// 取出全部属性遍历
Object.keys(data).forEach(function(key) {
defineReactive(data, key, data[key]);
});
}
function defineReactive(data, key, val){
observe(val); // 监听子属性
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function() {
console.log(key + '值获取')
return val;
},
set: function(newVal) {
console.log(key + '值设置', val, ' --> ', newVal);
val = newVal;
}
});
}
observe(obj1);
console.log(obj1.a);
console.log(obj1.b);
obj1.a = 12;
复制代码
(2) 能够监听数组,不用再去单独的对数组作特异性操做。
对比代码以下:
let arr = [1,2,3];
let p = new Proxy(arr, {
get(target, key,) {
console.log('获取数组属性',target,key)
return target[key];
},
set(target, key, value) {
console.log('设置数组属性',key,+','+target[key] + ' -->' + value )
target[key] = value;
return true;
}
})
console.log(p) // Proxy {0: 1, 1: 2, 2: 3}
p.push(4);
// 发生四步骤:(1)获取数组属性 (3) [1, 2, 3] push
// (2)获取数组属性 (3) [1, 2, 3] length
// (3)设置数组属性 3 NaN -->4
// (4)设置数组属性 length NaN -->4
console.log('++') // 设置数组属性 0 NaN -->10
p[0] = 10;
console.log('-----------');
let arrObj = {
b:1,
}
let bValue = arrObj.b;
Object.defineProperty(arrObj, "b", {
enumerable: true,
configurable: true,
get: function() {
let key = "b"
console.log(key + '值获取', bValue)
return bValue;
},
set: function(newVal) {
let key = "b"
console.log(key + '值设置 --> ', newVal);
bValue = newVal;
return bValue
}
})
console.log(arrObj.b) // 1
arrObj.b = [1,2,3]; // b值设置 --> (3) [1, 2, 3]
arrObj.b.push(4); // b值获取 (3) [1, 2, 3] 只获取了旧值 设置push没有监听到
arrObj.b[0] = 10; // b值获取 (4) [1, 2, 3, 4] 只获取了旧值 b[0]设置没有监听到
复制代码
原理老是晦涩难懂的,学习过程也很痛苦,跌跌撞撞的看完,特此总结与你们共享,有表述不正确的地方,但愿多多指正。
git地址:github.com/aicai0/vue3…
若有问题,欢迎探讨,若是满意,请手动点赞,谢谢!🙏
及时获取更多姿式,请您关注!!