拿来吧你,Vue响应式!

494 阅读6分钟

大家好,我是麦当当(小组队名起的感觉还不错),Vue的响应式用过的人都快手称赞,但是我只知其一不知其二。或许都知道用proxy代理啊,可是在整个设计系统上没有践行。那单单是proxy还有其他设计吗?

正好在《Vue.js设计与实现》中对响应式系统的设计由0到1设计,这样让人学到了。

理解什么叫响应式

在最新出版的《Vue.js设计与实现》中第四章解释了Vue的响应式构建。此书有了很好的定义。首先需要理解什么是响应式数据和副作用函数。

function effect (){
	document.body.innerText='hello vue3'
}
effect()

effect函数的执行会直接修改body文本内容的值,这就称为产生了副作用的函数。举个例子:

let indexValue = 1
function effect(){
	indexValue=2  //修改了全局变量,产生副作用
}

那么假设副作用函数中读取了某个对象的属性值

const mjc = {text:"hellow"}
function effect(){
	document.body.innerText=mjc.text //访问了mjc属性触发读取操作
}

mjc.text  = 'mjc'

这句话修改了mjc.text的值,如果能让effect函数重新执行一遍,那么这个对象就是响应式数据。这个定义我一开始也是懵逼的,响应式数据和副作用函数有什么关联?

Vue最独特的特性之一,是其非侵入性的响应式系统。数据模型仅仅是普通的JavaScript对象。而当你修改它们时,视图会进行更新,所订阅的会被更新。

换句话说,副作用函数其实就是一个订阅者,而响应式数据就是发布者。两者密切联系,发布者在数据变化的时候去通知订阅了对应数据的人。

初步封装一个响应式

//封装一个响应式的函数
let reactiveFns = [];
function watchFn(fn) {
  reactiveFns.push(fn);
  fn&& fn()
}
let obj = {
  name: "mjc"age:18
};
const objProxy = new Proxy(obj, {
  get: function (target, key, receiver) {
   return target[key]
   
  },
  set: function (target, key, newVal, receiver) {
    target[key] = newVal
    reactiveFns.forEach((fn) => {
  		fn();
	})
  },
});
//假设当前对obj.name进行响应式收集
watchFn(function () {
  const newName = objProxy.name;
  console.log("obj.name的执行");
  console.log(objProxy.name);
});

//假设obj.name改变,自动执行桶中的fn
objProxy.name = "kobe";

假设当前对obj.name进行响应式,用proxy代理了obj对象的get和set的劫持操作,当访问name时get会返回数据,设置name会修改name的值,同时将reactiveFns桶遍历执行,通过注册一个watchFn副作用函数,将副作用函数存入到属于obj.name的桶中,一旦obj.name改变了,就去桶中取出来遍历执行副作用函数。

思路已经大致清晰了:

当读取操作发生时,将对应的副作用函数收集到桶里。

当设值操作发生时,将桶里的函数遍历取出执行。

设计完善一个响应式系统

现在再处理细节,为了设计一个更完善的响应系统考虑一下几点:

1.这个桶目前只能装name属性的,如果要装入age的呢?又如果还有别的对象响应呢?这个桶的结构就得分层次了如下

一个对象:

多个对象:

let obj = {
  name: "mjc",
  age: 18,
};
let info = {
  address:"江门大学"
};

开始设计代码

//封装一个桶
let busket = new WeakMap(); //新增  存储所有响应式数据的副作用函数的桶

//封装存储全局注册的副作用函数
let reactiveFns = null; //修改为普通变量,注册副作用函数
function watchFn(fn) {
  reactiveFns = fn; //新增 副作用执行一次注册到变量中
  fn && fn
  reactiveFns = null; //恢复初始状态null,便于判断收不收集访问没有注册的属性
}
let obj = {
  name: "mjc",
  age: 18,
};
const objProxy = new Proxy(obj, {
  get: function (target, key, receiver) {
    if (!reactiveFns) return target[key];//新增 判断有无注册这个函数
    let targetDepend = busket.get(target);//新增 获取桶里被访问的对象
    if (!targetDepend) {//新增 如果不存在,创建map对象存储
      targetDepend = new Map();
      busket.set(target, targetDepend);
    }
    let depend = targetDepend.get(key);//新增 通过key再去获取下一层对象
    if (!depend) {//新增 如果没有,创建一个Set对象,可以去重
      depend = new Set();
      targetDepend.set(key, depend);
    }
    depend.add(reactiveFns);//新增 添加副作用函数进去深层桶中
    return target[key];
  },
  set: function (target, key, newVal, receiver) {
    target[key] = newVal;
    let targetDepend = busket.get(target);//新增 通过桶拿到被访问
    if (!targetDepend) return;//对象的桶,再从对象的桶中通过key取出
    let depend = targetDepend.get(key);//属性
    if (!depend) return;
    depend.forEach((fn) => {
      fn();
    });
  },
});
//假设当前对obj进行响应式收集
watchFn(function () {
  const newName = objProxy.name;
  console.log("obj.name的执行");
  console.log(objProxy.name);
});

//假设obj.name改变,自动执行obj.name桶中的fn
objProxy.name = "kobe";

控制台运行代码

控制台运行代码,可以发现obj.name改变了,副作用函数执行了两次。一次是我们主动执行,这样才能访问副作用中的响应式。第二次由obj.name被设置触发。通过Set对象也防止了同一副作用重复执行。

为什么使用weakMap作为桶的原因:

浏览器window对象中的obj指向了堆内存中的地址,根据Gc机制引用计数,从根对象引用的对象不会被垃圾回收处理。而weakMap的对obj的引用是一个弱引用,Gc机制会判断弱引用指向的是否还有别的引用指向,如果obj = null,那么弱引用无法维持联系会被垃圾回收销毁,防止内存泄露。

再考虑边境情况,分别执行以下

objProxy.age = 22; //age属性没有跟副作用函数建立联系,当变化时不能触发副作用

由于拦截了proxy的set拦截器,从busket中取出会判断,自然不会响应

console.log(objProxy.age) //读取没有联系的age属性,不能收集非副作用注册的依赖,必须要经过副作用函数注册,实现订阅

访问没有与桶建立联系的age属性,不会触发收集依赖

不同副作用函数

  • 注册多个副作用函数
watchFn(function () {
  const newName = objProxy.name;
  console.log("obj.name的执行");
  console.log(objProxy.name);
});
watchFn(function () {
  console.log(objProxy.name + "--------------------");
});

objProxy.name = "kobe";

  • 分别注册不同的副作用属性函数

watchFn(function () {
  const newName = objProxy.name;
  console.log("obj.name的执行");
  console.log(objProxy.name);
});
watchFn(function () {
  console.log(objProxy.age + "--------------------");
});
//假设obj.name改变,自动执行桶中的fn
objProxy.age = 18;

  • 注册同一副作用不同属性

watchFn(function () {
  const newName = objProxy.name;
  console.log("obj.name的执行");
  console.log(objProxy.name);
  console.log(objProxy.age + "--------------------");
});
objProxy.age = 22;
objProxy.name = "麦某人";//当name或者age改变时都会触发收集的副作用依赖

proxy代理的过程可以抽离出来优化一下

function reactive(obj) {
  return new Proxy(obj, {
  get: function (target, key, receiver) {
    if (!reactiveFns) return target[key];
    let targetDepend = busket.get(target);
    if (!targetDepend) {
      targetDepend = new Map();
      busket.set(target, targetDepend);
    }
    let depend = targetDepend.get(key);
    if (!depend) {
      depend = new Set();
      targetDepend.set(key, depend);
    }
    depend.add(reactiveFns);
    return target[key];
  },
  set: function (target, key, newVal, receiver) {
    target[key] = newVal;
    let targetDepend = busket.get(target);
    if (!targetDepend) return;
    let depend = targetDepend.get(key);
    if (!depend) return;
    depend.forEach((fn) => {
      fn();
    });
  },
});
}

那么不觉得有点熟悉的感觉了吗?比如vue3composition API 中的reactive() ,当我们需要对某个对象进行响应式的时候,只需要如下调用:

const objProxy = reactive({
  name: "mjc",
  age: 18,
})

到了这一步都知道了,这就是vue响应式代理基本原理了,如果是vue2的话用Object.defineProperty() 遍历数据劫持一下,如下

function reactive(obj) {
  Object.keys(obj).forEach((key) => {
    let value = obj[key];
    Object.defineProperty(obj, key, {
      get: function () {
         if (!reactiveFns) return value;
    	let targetDepend = busket.get(target);
    	if (!targetDepend) {
      	targetDepend = new Map();
      	busket.set(target, targetDepend);
    }
    	let depend = targetDepend.get(key);
    	if (!depend) {
      	depend = new Set();
      	targetDepend.set(key, depend);
    }
    	depend.add(reactiveFns);
    	return value;
      },
      set: function (newValue) {
        value = newValue;
        const depend = getDepend(obj, key);
        depend.noticfy();
      },
    });
  });
}

以上代码以及原理设计仅供参考。所以毫不夸张地说,理解了响应式原理,无论写什么代码,至少优势在我!