实现可数据可响应的方式
通过可响应对象,实现对数据的侦测,从而告知外界数据变化。
- 单一的访问器getter和setter
- Object.defineProperty()--实现了vue2.0响应式原理
- Proxy--实现3.0版本的原理
vue2.0响应式原理的弊端
需要对 Object 和 Array 两种类型采用不同的处理方式。
为了感知 Array 的变化,对 Array 原型上几个改变数组自身的内容的方法做了拦截,虽然实现了对数组的可响应,但同样存在一些问题。vue2.0的数组方法拦截
defineProperty 通过递归实现 getter/setter 也存在一定的性能问题。
vue3.0主要使用的语法
- proxy
- reflect
- WeakMap
1.reflect
Reflect对象与Proxy对象一样,也是拥有get、set方法。
- Reflect对象的设计目的:
- 将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上
- 修改某些Object方法的返回结果,让其变得更合理
- 让Object操作都变成函数行为
- Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。
2.关于proxy
Proxy 可以代理数组,并且 API 提供了get 、 set。
let data = { foo: 'foo' }
let p = new Proxy(data, {
get(target, key, receiver) {
return target[key]
},
set(target, key, value, receiver) {
console.log(receiver)// receiver是proxy对象本身
console.log('set value')
target[key] = value // target就是data对象
}
})
p.foo = 123
//Proxy{}
//{foo: "foo"} "set value"
// 此时data被修改,p是代理data的代理对象
let data = [1,2,3]
let p = new Proxy(data, {
get(target, key, receiver) {
return target[key]
},
set(target, key, value, receiver) {
console.log('set value')
target[key] = value
}
})
p.push(4) // Uncaught TypeError: 'set' on proxy: trap returned falsish for property '3'
- 产生问题:set也需要返回值
需要修改为
let data = [1,2,3]
let p = new Proxy(data, {
get(target, key, receiver) {
return target[key]
},
set(target, key, value, receiver) {
console.log('set value')
target[key] = value
//++
return true
//++
}
})
p.push(4)
// set value // 打印2次
打印两次是因为处理数组时候push不但修改数组的项还修改了length
let data = [1,2,3]
let p = new Proxy(data, {
get(target, key, receiver) {
console.log('get value:', key)
return target[key]
},
set(target, key, value, receiver) {
console.log('set value:', key, value)
target[key] = value
return true
}
})
p.push(1)
// get value: push
// get value: length
// set value: 3 1
// set value: length 4
- 产生问题:多次触发set和get就会多次触发view的更新,此时只需要触发一次set更新
可以用类似于 debounce 的操作处理多次执行的问题(vue3.0有更好的方式)
function reactive(data, cb) {
let timer = null
return new Proxy(data, {
get(target, key, receiver) {
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
clearTimeout(timer)
timer = setTimeout(() => {
cb && cb()
}, 0);
return Reflect.set(target, key, value, receiver)
}
})
}
let ary = [1, 2]
let p = reactive(ary, () => {
console.log('trigger')
})
p.push(3)
// trigger
但是vue3.0提供了更好的方式处理执行重复的问题,利用私有属性
(以下没有完全解决重复问题,对于数组只是避免了length对数组更新的重复渲染,像splice,unshift这种多次修改索引的方法还会触发多次)
function trigger(){
console.log('触发视图更新')
}
function isObject(target){
return typeof target==='object'&&target!==null;
}
function reactive(target){
if(!isObject(target)){
return target;
}
const handler={
set(target,key,value,receiver){
console.log('setValue',value)
//如果触发的是私有属性的话,触发视图的更新
if(!target.hasOwnProperty(key)){//添加新属性
trigger();
}else if(value!==target[key]){//set的值变化,更新属性
trigger();
}
return Reflect.set(target,key,value,receiver)
},
get(target,key,receiver){
const res=Reflect.get(target,key,receiver);
console.log('getValue',res)
return res;
},
deleteProperty(target,key){
return Reflect.deleteProperty(target,key);
}
}
let observer=new Proxy(target,handler);
return observer;
}
let obj=[1,2,3];
let p=reactive(obj)
p.unshift(1);
监听深层的对象
let obj={
a:[1,2,3],
b:{c:4}
};
let p=reactive(obj)
p.b.c = 2
// getValue: {c:4} 不触发set
- 产生问题:proxy 只能代理一层;
因为没有触发 set 的输出,反而是触发了 get ,因为 set 的过程中访问了 bar 这个属性。
深度数据监听,可以用 递归 处理问题,把对象的每一层都进行代理(同vue2.0),但是优于2.0,3.0的处理方式是只有使用数据的当前属性的时候才会去代理,2.0则是全部代理,3.0的处理方式节约的大量不必要的工作。
function trigger(){
console.log('触发视图更新')
}
function isObject(target){
return typeof target==='object'&&target!==null;
}
function hasOwn(){
}
function reactive(target){
if(!isObject(target)){
return target;
}
const handler={
set(target,key,value,receiver){
//如果触发的是私有属性的话,触发视图的更新
if(!target.hasOwnProperty(key)){//添加新属性
trigger();
}else if(value!==target[key]){//set的值变化,更新属性
trigger();
}
return Reflect.set(target,key,value,receiver)
},
get(target,key,receiver){
const res=Reflect.get(target,key,receiver);
if(isObject(res)){
return reactive(res)
}
return res;
},
deleteProperty(target,key){
return Reflect.deleteProperty(target,key);
}
}
let observer=new Proxy(target,handler);
return observer;
}
let obj={
a:[1,2,3],
b:{c:1}
}
let p=reactive(obj)
p.b.c=4
// 触发视图更新
有些数据并非需要侦测,我们需要对数据侦测做更细的控制,全部监听浪费一部分的性能,vue3.0有更好的解决办法,weakMap。
3.weakMap对象,实现完整的reactive数据监听
WeakMap对象也是键值对的集合。它的键必须是对象类型,值可以是任意类型。它的键被弱保持,也就是说,当其键所指对象没有其他地方引用的时候,它会被GC回收掉。WeakMap
提供的接口与Map
相同。
- 我认为使用WeakMap主要是因为
- WeakMap的键是对象类型,保持弱引用。
- 列表是否存在取决于垃圾回收器的状态,性能比object更高
//++ ----------
const toProxy= new WeakMap()
const toRaw = new WeakMap()
// ++ ----------
function trigger(){
console.log('触发视图更新')
}
function isObject(target){
return typeof target==='object'&&target!==null;
}
function reactive(target){
if(!isObject(target)){
return target;
}
// ++ ----------
//如果代理表中存在target了就说明,target已经被代理过了,就返回代理过的对象
if(toProxy.get(target)){
console.log('target已经被代理过了,是一个obj对象')
return toProxy.get(target)
}
if(toRaw.has(target)){//如果这个对象被代理过了,就把对象原封不动的返回
console.log('target已经被代理过了,是一个proxy对象')
return target
}
// ++ ----------
const handler={
set(target,key,value,receiver){
const oldValue = target[key];
//如果触发的是私有属性的话,触发视图的更新
if(!target.hasOwnProperty(key)){//添加新属性,用hasOwnProperty是因为可以屏蔽一些无用的属性更新,像数组的length等
trigger();
}else if(value!==oldValue){//set的值变化,更新属性
trigger();
}
return Reflect.set(target,key,value,receiver)
},
get(target,key,receiver){
const res=Reflect.get(target,key,receiver);
if(isObject(res)){
return reactive(res)
}
return res;
},
deleteProperty(target,key){
return Reflect.deleteProperty(target,key);
}
}
let observer=new Proxy(target,handler);
// ++ ----------
toProxy.set(target,observer);//记录原对象代理过的结果
toRaw.set(observer,target);
// ++ ----------
return observer;
}
let obj={
a:[1,2,3],
b:{c:1}
}
let obj2={
a:[1,2,3],
b:{c:1}
}
let p=reactive(obj)
p=reactive(p);// p是被代理过的对象
let n=reactive(obj2);
let n1=reactive(obj2);//多次代理一个对象obj1=
以上实现了一个简易的数据监听
4. 依赖收集(发布订阅),实现响应式
更新视图主要依赖于effect函数。
let obj=reactive({name:'zf'});
effect(()=>{ // effect会执行两次,默认先执行一次 之后依赖的数据变化了 会再次执行。
console.log(obj.name)//会调用get方法
});
obj.name='jw';//代理的属性值被修改
effect的作用是首先会执行一次传入的函数,之后如果代理的属性有变化,会继续触发传入effect的函数。
所以此时需要把传入effect的函数包装成一个响应式的函数。
//栈 先进后出
let activeEffectStacks=[]; //
//响应式 副作用
function effect(fn){
// 需要把fn这个函数包装成响应式的函数,在把这个函数默认先执行一次
let effect = createReactiveEffect(fn);
effect()//默认执行一次
}
function createReactiveEffect(fn){
let effect=function(){//这个就是创建的响应式的effect
return run(effect,fn) // 运行 1.让fn执行 2.把这个effect存入到栈中
}
return effect;// 返回这个包装好的函数
}
function run (){//运行fn并把effect存起来
try{//try包裹防止fn内报错
activeEffectStacks.push(effect);
fn();//利用了js是单线程的。
//fn触发,里边读取了proxy的属性,所以会进入到reactive的get中。
}finally{
activeEffectStacks.pop();//用完释放
}
}
reactive的get中收集依赖
const toProxy= new WeakMap()
const toRaw = new WeakMap()
function trigger(){
console.log('触发视图更新')
}
function isObject(target){
return typeof target==='object'&&target!==null;
}
function reactive(target){
if(!isObject(target)){
return target;
}
//如果代理表中存在target了就说明,target已经被代理过了,就返回代理过的对象
if(toProxy.get(target)){
console.log('target已经被代理过了,是一个obj对象')
return toProxy.get(target)
}
if(toRaw.has(target)){//如果这个对象被代理过了,就把对象原封不动的返回
console.log('target已经被代理过了,是一个proxy对象')
return target
}
const handler={
set(target,key,value,receiver){
// const oldValue = target[key];
const res = Reflect.set(target, key, value, receiver)
//如果触发的是私有属性的话,触发视图的更新
//if(!target.hasOwnProperty(key)){//添加新属性,用hasOwnProperty是因为可以屏蔽一些无用的属性更新,像数组的length等(经测试会引起bug)
//++++++++++++++++++++++++++++
// trigger(target,'add',key);
//++++++++++++++++++++++++++++
//}else if(value!==oldValue){//set的值变化,更新属性
//++++++++++++++++++++++++++++
// trigger(target,'set',key);
//++++++++++++++++++++++++++++
//}
if (target.hasOwnProperty(key)) {
trigger(target, '', key);
}
return res
},
get(target,key,receiver){
const res=Reflect.get(target,key,receiver);
//++++++++++++++++++++++++++++++++++++++++++++++++
// 收集依赖 订阅 ,把当前的key和effect对应起来
track(target,key) // 如果target上的这个key变化了重新让数组中的effect执行即可,所以需要建立target中的key于effect的关联(关联的结构如下图所示)
//++++++++++++++++++++++++++++++++++++++++++++++++
if(isObject(res)){
return reactive(res)
}
return res;
},
deleteProperty(target,key){
return Reflect.deleteProperty(target,key);
}
}
let observer=new Proxy(target,handler);
toProxy.set(target,observer);//记录原对象代理过的结果
toRaw.set(observer,target);
return observer;
}
track用于对应target中的key与effect的关联
let targetMap=new WeakMap();
function track(target,key){//如果这个target中的key变化了 就执行数组里activeEffectStacks的方法。
let effect=activeEffectStacks[activeEffectStacks.length-1];
if(effect){//有对应的关系 才创建关联
let depsMap=targetMap.get(target);
if(!depsMap){
targetsMap.set(target,depsMap=new Map())
}
let deps=depsMap.get(key);
if(!deps){
depsMap.set(key,deps=new Set());
}
if(!deps.has(effect)){
deps.add(effect);
}
}
// 以上是动态创建依赖关系。
}
trigger 触发key对应的effect,更新视图
function trigger(target,type,key){
let depsMap=targetMap.get(target);
if(depsMap){
let deps=depsMap.get(key);
if(deps){//将当前key对应的effect依此执行
deps.forEach(effect=>effect())
}
}
}
完整vue3.0响应式代码
const toProxy = new WeakMap()
const toRaw = new WeakMap()
function isObject(target) {
return typeof target === 'object' && target !== null;
}
function reactive(target) {
if (!isObject(target)) {
return target;
}
// 如果代理表中存在target了就说明,target已经被代理过了,就返回代理过的对象
if (toProxy.get(target)) {
console.log('target已经被代理过了,是一个obj对象')
return toProxy.get(target)
}
if (toRaw.has(target)) { // 如果这个对象被代理过了,就把对象原封不动的返回
console.log('target已经被代理过了,是一个proxy对象')
return target
}
const handler = {
set(target, key, value, receiver) {
// const oldValue = target[key];
const res = Reflect.set(target, key, value, receiver)
// 如果触发的是私有属性的话,触发视图的更新
// if (!target.hasOwnProperty(key)) { // 添加新属性,用hasOwnProperty是因为可以屏蔽一些无用的属性更新,像数组的length等(经测试会引起bug)
// trigger(target, 'add', key);
// } else if (value !== oldValue) { // set的值变化,更新属性
// console.log(target, key, oldValue, value, 24)
// trigger(target, 'set', key);
// }
if (target.hasOwnProperty(key)) {
trigger(target, '', key);
}
return res
},
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
// 收集依赖 订阅 ,把当前的key和effect对应起来
track(target, key) // 如果target上的这个key变化了重新让数组中的effect执行即可,所以需要建立target中的key于effect的关联
if (isObject(res)) {
return reactive(res)
}
return res;
},
}
const observer = new Proxy(target, handler);
toProxy.set(target, observer);// 记录原对象代理过的结果
toRaw.set(observer, target);
return observer;
}
// 栈 先进后出
const activeEffectStacks = []; //
// 响应式 副作用
function effect(fn) {
// 需要把fn这个函数包装成响应式的函数,在把这个函数默认先执行一次
const effect = createReactiveEffect(fn);
effect()// 默认执行一次
}
function createReactiveEffect(fn) {
const effect = function () { // 这个就是创建的响应式的effect
return run(effect, fn) // 运行 1.让fn执行 2.把这个effect存入到栈中
}
return effect;// 返回这个包装好的函数
}
function run(effect, fn) { // 运行fn并把effect存起来
try { // try包裹防止fn内报错
activeEffectStacks.push(effect);
fn();// 利用了js是单线程的。
// fn触发,里边读取了proxy的属性,所以会进入到reactive的get中。
} finally {
activeEffectStacks.pop();// 用完释放
}
}
const targetMap = new WeakMap();
function track(target, key) { // 如果这个target中的key变化了 就执行数组里activeEffectStacks的方法。
console.log(activeEffectStacks, target, '-----------track')
const effect = activeEffectStacks[activeEffectStacks.length - 1];
if (effect) { // 有对应的关系 才创建关联
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, depsMap = new Map())
}
let deps = depsMap.get(key);
if (!deps) {
depsMap.set(key, deps = new Set());
}
if (!deps.has(effect)) {
deps.add(effect);
}
}
}
// 以上是动态创建依赖关系。
function trigger(target, type, key) {
const depsMap = targetMap.get(target);
console.log(target, depsMap, '--------trigger')
if (depsMap) {
const deps = depsMap.get(key);
if (deps) { // 将当前key对应的effect依此执行
console.log(deps.size)
deps.forEach(effect => effect())
}
}
}
let obj={name:'haha'};
let observer= reactive(obj);
effect(()=>{
console.log(observer.name);
})
observer.name='xixi';
observer.name='xixi';
//执行两次haha和xixi
(以下不会再次触发更新的情况)
//1.对象只监听到了外层
const obj = { name: { n: 'haha' } };
const observer = reactive(obj);
effect(() => {
console.log(observer.name, '-------------108');
})
observer.name.n = 'xixi';
//必须这样监听
const obj = { name: { n: 'haha' } };
const observer = reactive(obj);
effect(() => {
console.log(observer.name.n);
})
observer.name.n = 'xixi';
//2.对象内层的数组操作问题
const obj1 = { a: [1, 2, 3] };
const oarr = reactive(obj1);
effect(() => {
oarr.a.forEach((item)=>{
console.log(item,'更新视图');
})
})
oarr.a[0]=2;
oarr.a.push(4);
原因是:
set(target, key, value, receiver) {
// const oldValue = target[key];
const res = Reflect.set(target, key, value, receiver)
// 添加的新属性或数组的项会导致更新失效
// if (!target.hasOwnProperty(key)) {
// // trigger(target, 'add', key);
// } else if (value !== oldValue) {
// console.log(target, 'key:', key, oldValue, value)
// trigger(target, 'set', key);
// console.log(value, oldValue)
// }
//以上处理方式需要改为
//如果数组方法过滤掉length等属性会出问题
if (target.hasOwnProperty(key)) {
trigger(target, '', key);
}
return res
},
备上html测试方式
+