Object.defineProperty
实现的Proxy+Reflect
来实现的vue2中存在的问题
vm.$set(obj,key.val)/vm.$delete(obj,key)
新增属性/删除属性vm.$set(arr,index.value)
,调用数组的splice
方法vue3中的优化
vue2是监听对象的属性,vue3是监听对象
关于惰性深度监听的笔记:https://juejin.cn/post/6979368550225936392#heading-4
Proxy可以创建一个代理对象,实现对其他对象的代理。外界对被代理对象进行的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。
Proxy是一个构造函数,第一个参数是被代理对象,第二个参数是一个对象,里面存放拦截的操作。
const p = new Proxy(obj,{
get(){},
set(){}
})
Proxy可以拦截哪些方法?
proxy只能拦截一个对象的基本操作
proxy.foo
或proxy['foo']
: get(被代理对象, 读取的属性, [简单理解为this,谁在读取属性])
key in obj
:has(被代理对象, 判断的属性)
ownKeys(被代理对象)
方法用来拦截对象自身属性的读取操作。
Object.getOwnPropertyNames()
Object.getOwnPropertySymbols()
Object.keys()
for...in
循环proxy.foo = v
或proxy['foo'] = v
: get(被代理对象, 设置的属性, [简单理解为this,谁在设置属性])
apply(被代理对象,被代理对象的上下文(this),目标对象的参数数组)
Reflect有哪些静态方法
Proxy
和Reflect
的方法都是一一对应的,代码会更易读。
常用方法
Reflect.get()
: 获取对象身上某个属性的值,类似于 target[name]。Reflect.set()
: 将值分配给属性的函数。返回一个Boolean,如果更新成功,则返回true。Reflect.has()
: 判断一个对象是否存在某个属性,和 in 运算符 的功能完全相同。Reflect.deleteProperty()
: 作为函数的delete操作符,相当于执行 delete target[name]。1.修改某些 Object 方法的返回结果,让语义变的更加规范化
比如使用Proxy代理对象obj,当通过代理对象修改obj上的不可修改属性时,会抛出错误阻塞后面的代码。使用Reflect.set
返回false,代码正常执行
//1.假设对象中的name属性不允许修改
const obj = {}
Object.defineProperty(obj,"name",{
value:"name",
writable: false
})
//2.通过Proxy代理该对象之后,对obj的name属性就行修改会抛出错误,阻塞后面代码的执行
const proxy = new Proxy(obj, {
get(target, key,) {
// 注意,这里我们没有使用 Reflect 来进行读取
return target[key]
},
set(target, key, value) {
// 注意,这里同样没有使用 Reflect 来进行设置
return target[key] = value
}
})
proxy.name = '王五'
console.log('阻塞了')
Reflect.get(target, key, receiver)
类似于 Reflect.get(target, key).call(receiver)
改变 this 的指向。const obj = {
name: 'raman',
get value() {
console.log('value 中的 this:', this, this === obj)
return this.name
}
}
obj.value; //name
const proxy = new Proxy(obj, {
get(target, key) {
return target[key];
}
})
effect(()=>{console.log(proxy.value));
//通过代理proxy.value访问时,先触发get返回obj.value,触发访问器,此时访问器中this指向的obj
/*
这里的this指向obj,obj对象是一个原始数据,并不是响应式对象,所以将无法和副作用函数建立联系。
effect(()=>{console.log(obj.value));
*/
编译模板–编译模板过程
绑定响应式: Object.defineProperty + 观察者模式
数据改变时更新视图–触发diff-patch过程
总结
Object.defineProperty函数
都转换成getter/setter形式。dep
对象,dep
用来收集数据的依赖也就是watcher
对象。在get的时候,将依赖存进dep
中,在set数据改变的时候,通知dep
中的依赖数据发生改变了。dep
通知(notify)watcher数据改变,触发watcher对象的更新函数->更新视图-触发diff-patch过程。问题1:数据被修改后,Vue内部如何监听message数据的改变的? --> Object.defineProperty -> 监听对象属性的改变
问题2:当数据发生改变,Vue如何知道需要通知哪些地方更新界面? -->观察者模式
function defineReactive(data,key,val){
Objcet.defineProperty(data,key,{//代码①
enumerable:true,
configurable:true,
get:function(){
return val;
},
set:function(){
if(val===newVal)return;
val = newVal;
}
})
}
//{a:"x",b:{c:"y",d:"z"}}
//Observer类的作用将正常的object转换为被侦测的object
class Observer{
constructor(data){
this.data = data;
if(!Array.isArray(data)){ //这里数组和对象都会进来,只处理对象
Object.keys(data).forEach(key=>{
defineReactive(this.data,key,data[key]);
})
}
}
}
function defineReactive(data,key,val){
//如果val是对象,比如b:{c:"y",d:"z"},说明不是最里层,还需要对{c:"y",d:"z"}进行转换getter/setter
if(typeof val === 'object')new Observer(val);//递归的目的是每一层的属性都应该被绑定响应式
//..代码①
}
<template>
{{name}}
template>
定义Dep类,Dep类的目的是对依赖进行管理,比如存储依赖,删除依赖、给依赖发更新通知等等,那么Dep类与某数据的关系应该是一一对应,所以我们在绑定响应式时,可以为每个属性绑定一个dep对象。在get的时候,将使用了name的地方(依赖)存进dep中,在set数据改变的时候,通知dep中的依赖数据发生改变了,Object在getter中收集依赖,在stter中触发依赖
class Dep {//Dep类存储依赖,添加依赖, 删除依赖,通知依赖等
constructor(){
this.subs = [];//subs存储依赖
}
addSub(sub){
this.subs.push(sub);
}
depend(){
//if(依赖){
// this.addSub(依赖);
//}
if(window.target){
this.addSub(window.target);
}
}
notify(){
this.subs.forEach(sub =>{
sub.update(); //通知这个属性的所有依赖数据更新
})
}
}
function defineReactive(data,key,val){
if(typeof val === 'object')new Observer(val);
let dep = new Dep(); //绑定dep对象
Objcet.defineProperty(data,key,{
enumerable:true,
configurable:true,
get:function(){
dep.depend();//修改dep对象,这里应该添加依赖
return val;
},
set:function(){
if(val===newVal)return;
val = newVal;
dep.notify();//通知依赖,数据发生了修改
}
})
}
class Watcher{
constructor(vm,name,node){
this.node = node;
this.vm = vm;
this.name = name;
window.target = this, //我们给依赖命名为window.target = watcher对象,所以依赖就是watcher对象
this.update();
window.target = null; //并没有绑定在实例上,全局仅有一个,数据更新后会重新调用get,防止一个watcher对象被多次加入dep.sub数组中
}
update(){//将{{name}}的name更新为vm里面的值,这里是在vm上代理了_data的值
this.node.nodeValue = this.vm[this.name];
//从vm中取某个属性,相当于调用该属性的getter方法,此时window.target是有值的,值为watcher对象,所以这个watcher对象会被存储进dep.sub数组里面
}
}
//watcher对象在解析模板中的指令的时候会被创建new Watcher
重写了数组的七个方法,这7个方法都会改变原数组
push
:向数组的结尾添加一个或多个元素pop
:从数组的结尾删除一个元素unshift
:向数组的开头添加一个或多个元素shift
:删除数组的第一个元素splice
:删除数组中的指定元素,并为数组添加新元素sort
:数组元素进行排序reverse
:反转数组思路
__proto__
隐式原型指向Array.prototype
,拦截器对象重写上述的7个方法,重写的目的是增加响应式,但是最终调用的函数原型上的方法__proto__
隐式原型指向拦截器对象的显式原型const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto); //arrayMethods.__proto__ = Array.prototype
const methodsNeedChange = ['push','pop','shift','unshift','splice','sort','reverse']; //需要被改写的七个方法
methodsNeedChange.forEach(function(method){
const original= arrayProto[method]; //缓存原来的方法,最终还是会被调用
Object.defineProperty(arrayMethods, method, {//为拦截器对象增加7个方法
//value:重写的方法
value:function mutator(...args){
return original.apply(this,args); //最终调用的是原型的方法,this指向拦截器
}
enumerable:false;//不可以被枚举
writable:true; //可以被遍历
configurable:true; //可以被删除
})
}
__proto__
隐式原型指向拦截器对象,Observer类的作用就是增加响应式。//引入arrayMethods
class Observer{
constructor(data){
this.data = data;
if(Array.isArray(data)){//为数组增加响应式
data.__proto__ = arrayMethods;
}
else{ //为对象增加响应式
Object.keys(data).forEach(key=>{
defineReactive(this.data,key,data[key]);
})
}
}
}
注意:有些浏览器不支持
__proto__
,如果不支持,Vue直接把arrayMethods身上的改写方法设置到被侦测的数组上
还是需要考虑一个问题,数组改变了去通知谁?如何收集依赖?在哪里触发依赖?
需要注意的一个问题是data是一个对象,如果有数组也是存在对象中的,所以数组是在getter中收集依赖,在拦截器中触发依赖。
数组中元素的修改时通过拦截器重写的方法,那么如果触发了重写方法说明数据改变了,此时我们就需要通知依赖,所以是在拦截器中触发依赖。
拦截器怎么能看见依赖?我们要把依赖放在哪里?
object是一个属性对应一个Dep数组,所以写在了defineReactive函数中,因为需要在getter中收集依赖,在setter中触发依赖,在getter、setter的时候需要看得见依赖。
同理,我们需要把依赖保存在getter和拦截器都能看见依赖的地方,也就是Observer实例中。
因为在getter中可以访问到Observer实例,在拦截器中也可以访问到Observer实例(这个地方在后面讲,先认定这个结论)
//引入arrayMethods
class Observer{
constructor(data){
//....
this.dep = new Dep();//在Observer实例上新增dep
if(Array.isArray(data)){//为数组增加响应式
data.__proto__ = arrayMethods;
}
else{ //为对象增加响应式
//....
}
}
}
怎么在拦截器中访问到Oberver实例?给数组增加一个属性__ob__
,这个__ob__
指向Oberver实例
//工具函数给obj身上的key添加val
function def(obj,key,val,enumerable){
Object.defineProperty(obj,key,{
value:val,
enumerable:!!enumerable,
writeable:true,
configurable:true
})
}
class Observer{
constructor(data){
//....
this.dep = new Dep();//在Observer实例上新增dep
def(data,'__ob__',this); //拦截器可以通过数组身上的__ob__属性访问到Observer实例
if(Array.isArray(data)){//为数组增加响应式
data.__proto__ = arrayMethods;
}
else{ //为对象增加响应式
//....
}
}
}
现在我们在可以在拦截器里看见依赖了,也就是通过数组身上的__ob__属性,那么我们就可以在拦截器中通知依赖,告诉Watcher数据变啦。
[...].forEach(function(method){
const original = arrayProto[method];//缓存Array原型上的方法
def(arrayMethods,method,function mutator(...args){
const result = original.apply(this,args);//this指向拦截器
this.__ob__.dep.notify(); //向依赖发送数据!!!
return result;
})
})
function defineReactive(data,key,val){
//if(typeof val === 'object')new Observer(val);
let childOb = observe(val); //上面的判断也会在这个函数中判断
let dep = new Dep(); //绑定dep对象
Objcet.defineProperty(data,key,{
enumerable:true,
configurable:true,
get:function(){
dep.depend();//对象的依赖收集
if(childOb){
childOb.dep.depend(); //数组的依赖收集,收集在observer实例的Dep上
}
return val;
},
set:function(){
if(val===newVal)return;
val = newVal;
dep.notify();//通知依赖,数据发生了修改
}
})
}
export function observe (value){ //observe函数:为数组和对象返回observe实例
if(!isObject(value))return;
let ob
if(hasOwn(value,'__ob__') && value.__ob___ instanceof Observer){//如果该数组已经有了__ob__,已经创建了observer实例,已经是响应式的了
ob = value.__ob__;
}else{ //数组没有observer实例 或者是对象则会创建observer实例
ob = new Observer(value);
}
return ob;
}
__ob__
的作用
1.让拦截器可以访问到observer实例:通过将observer实例绑定在数组的__ob__
属性上
2.用来标识当前value是否已经被Observer类转化为响应式数据
class Observer{
constructor(data){
//...
if(Array.isArray(value)){
this.observeArray(value); //侦察Array中的每一项
//...
}
}
//....
observeArray(items){//循环侦察Array中的每一项
for(let i=0,l=items.length;i<1;i++){
observe(items[i]);//observe函数:为数组和对象返回observe实例
}
}
在哪里收集依赖在哪里通知依赖?
哪里创建Dep实例
在收集依赖的地方和触发依赖的地方都能看见
Observer类:侦察对象和数组的变化
主要目的:为数组和对象增加响应式
思路
1.为数组准备好Dep实例和为数组添加__ob__
属性,该属性指向Observer实例,每一个数组都会绑定一个observer实例
__ob__
的作用
1.让拦截器可以访问到observer实例:通过将observer实例绑定在数组的__ob__
属性上
2.用来标识当前value是否已经被Observer类转化为响应式数据
2.循环侦察Array中的每一项,让数组的隐式原型指向拦截器对象
observeArray方法:循环侦察Array中的每一项,数组中的每一项调用observe函数
observe函数:为数组和对象返回observe实例
3.循环将对象中的每一个属性都转化为getter/setter模式
defineReactive:将对象的每一个属性转化为getter/setter,为对象的 对象的每一个属性对应一个Dep实例,对象的Dep实例在这个函数中创建。在getter收集依赖,在setter中触发依赖通知Dep
数组在getter中通过observer实例的Dep实例收集依赖
//工具函数给obj身上的key添加val
function def(obj,key,val,enumerable){
Object.defineProperty(obj,key,{
value:val,
enumerable:!!enumerable,
writeable:true,
configurable:true
})
}
class Observer{
constructor(data){
this.dep = new Dep();//在Observer实例上新增dep
def(data,'__ob__',this); //为数组绑定observer实例
if(Array.isArray(value)){//处理数组
this.observeArray(value); //侦察Array中的每一项
data.__proto__ = arrayMethods; //让数组的隐式原型指向拦截器对象
}
else{ //处理对象
Object.keys(data).forEach(key=>{//循环将每一个属性转化为getter/setter
defineReactive(this.data,key,data[key]);
})
}
}
observeArray(items){//循环侦察Array中的每一项
for(let i=0,l=items.length;i<1;i++){
observe(items[i]);//observe函数:为数组和对象返回observe实例
}
}
function defineReactive(data,key,val){
let childOb = observe(val); //返回observe实例
let dep = new Dep(); //绑定dep对象
Objcet.defineProperty(data,key,{
enumerable:true,
configurable:true,
get:function(){
dep.depend();//对象的依赖收集
if(childOb){
childOb.dep.depend(); //数组的依赖收集,收集在observer实例的Dep上
}
return val;
},
set:function(){
if(val===newVal)return;
val = newVal;
dep.notify();//通知依赖,数据发生了修改
}
})
}
}
export function observe (value){ //observe函数:为数组和对象返回observe实例
if(!isObject(value))return;
let ob
if(hasOwn(value,'__ob__') && value.__ob___ instanceof Observer){//如果该数组已经有了__ob__,已经创建了observer实例,已经是响应式的了
ob = value.__ob__;
}else{ //数组没有observer实例 或者是对象则会创建observer实例
ob = new Observer(value);
}
return ob;
}
数组专有:拦截器:重写七个方法,使其加入响应式
拦截器的作用
1.拦截数组的七个方法,将拦截器的__proto__
隐式原型指向Array.prototype显式原型
2.通知依赖数据改变了,通过调用数组绑定的observer对象找到observer对象绑定的dep实例通知依赖
const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto); //arrayMethods.__proto__ = Array.prototype
const methodsNeedChange = ['push','pop','shift','unshift','splice','sort','reverse']; //需要被改写的七个方法
methodsNeedChange.forEach(function(method){
const original= arrayProto[method]; //缓存原来的方法,最终还是会被调用
def(arrayMethods,method,function mutator(...args){
const result = original.apply(this,args);//this指向拦截器,调用原型的方法
this.__ob__.dep.notify(); //向依赖发送数据!!!
return result;
})
}
共有 Dep类:对依赖进行管理 没有对数组和对象分别处理
1.使用数据的时候收集依赖
2.通知依赖数据发生变化
3.对依赖进行管理,比如增加依赖、删除依赖、通知依赖等
Dep实例和数据的关系是一一对应的
class Dep {//Dep类存储依赖,添加依赖, 删除依赖,通知依赖等
constructor(){
this.subs = [];//subs存储依赖
}
addSub(sub){
this.subs.push(sub);
}
depend(){
if(window.target){
this.addSub(window.target);
}
}
notify(){
this.subs.forEach(sub =>{
sub.update(); //通知这个属性的所有依赖数据更新
})
}
}
共有 Watcher类:依赖 没有对数组和对象分别处理
中介,数据发生改变通知外界。外界通过Watcher类读取数据
当模板解析时会创建Watcher实例,此时调用update方法,从vm中取某个属性,相当于调用该属性的getter方法,此时window.target是有值的,值为watcher对象,所以这个watcher对象会被存储进dep.sub数组里面
Dep类通知依赖时也会调用update方法
export class Watcher{
constructor(vm,name,node){
this.node = node;
this.vm = vm;
this.name = name;
window.target = this, //我们给依赖命名为window.target, = watcher对象,所以依赖就是watcher对象
this.update();
window.target = null; //并没有绑定在实例上,全局仅有一个,数据更新后会重新调用get,防止一个watcher对象被多次加入dep.sub数组中
}
update(){//将{{name}}的name更新为vm里面的值,这里是在vm上代理了_data的值
this.node.nodeValue = this.vm[this.name];
//从vm中取某个属性,相当于调用该属性的getter方法,此时window.target是有值的,值为watcher对象,所以这个watcher对象会被存储进dep.sub数组里面
}
}
//watcher对象在解析模板中的指令的时候会被创建new Watcher
new Vue()
首先执行初始化,数据传给Observer类,该类的作用是把一个object中的所有属性(包括子属性)都转换成响应式的,它利用defineProperty方法,为对象中的每个属性绑定getter和setter方法以及dep实例,dep实例和属性一一对应,它的作用是对该属性的依赖进行管理,比如·在getter方法里收集依赖,在setter方法里触发依赖,利用dep通知watcher数据发生改变,watcher再通知界面数据发生改变(数据变化之后,通过patch方法来进行视图渲染)__proto__
指向拦截器的显式原型,拦截器对象的隐式原型__proto__
指向Array.prototype,在拦截器中拦截数组中的七个方法,进行一些处理后,再调用Array.prototype上的对应方法。这样调用数组中的七个方法会先调用拦截器中重写的方法,调用重写方法时说明数组发生了变化,在重写方法中会通知该数组oberver实例上dep数据修改了,dep通知依赖(watcher)数据修改了,最后watcher通知界面数据修改了。依赖的收集是在getter中进行的,依赖的触发在拦截器中进行的。质上是通过 Proxy
劫持了数据对象的读写,当我们访问数据时,会触发 getter 执行依赖收集;修改数据时,会触发 setter 派发通知。
effect()
,effect的执行会直接或间接影响其他函数的执行。//当obj发生变化时,effect函数自动重新执行,我们就称obj是响应式的
const obj = {text:'xxx'}
function effect(){
console.log(obj.text);
}
//手动执行
obj.text = 'yyy'
effect()
track函数
:track函数收集依赖,对象的每个属性都有自己的dep
,将所有依赖于该属性的**effect
函数**都收集起来,放在dep
里。trigger函数
: trigger函数通知依赖。将依赖搜集起来之后,只要变量一改变,就执行trigger
函数通知dep里所有依赖该变量的effect函数
执行,实现依赖变量的更新。1.对象的每个属性都有自己的dep,将所有依赖于该属性的**effect
函数**都收集起来,放在dep
里。
2.每个对象会建立一个Map
来存储各个属性和dep的对应关系,key为属性名,value为该属性的dep(使用Set来存储)。
3.使用WeakMap
来存储多个对象的Map
Proxy-Reflect
实现自动收集和触发依赖track-trigger函数
//reactive 将对象变成响应式的
function reactive(target) {
const handler = {
get(target, key, receiver) {
track(receiver, key) // 访问时收集依赖
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
Reflect.set(target, key, value, receiver)
trigger(receiver, key) // 设值时自动通知更新
}
}
return new Proxy(target, handler)
}
响应式代码
function reactive(target = {}) {
if (typeof target !== "object" || target == null) {
return target
}
// 代理配置
const proxyConf = {
get(target, key, receiver) {
//只监听对象本身(非原型)属性
const ownKeys = Reflect.ownKeys(target)
if (ownKeys.includes(key)) {
//如果是本身的属性就监听,如果是对象原型的属性就不监听
console.log("get", key)
}
const result = Reflect.get(target, key, receiver)
//(惰性)深度监听-->提升性能
return reactive(result)
},
set(target, key, val, receiver) {
// 重复的数据不处理
if (val === target[key]) {
return true
}
// 监听是否是新增的key
const ownKeys = Reflect.ownKeys(target)
if (ownKeys.includes(key)) {
console.log("已有的key", key)
} else {
console.log("新增的key", key)
}
const result = Reflect.set(target, key, val, receiver)
return result //通过return的值可以看出是否设置成功
},
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key)
console.log("delete property", key)
return result //是否删除成功
},
}
// 生成代理对象
const observed = new Proxy(target, proxyConf)
return observed
}