Vue源码之实现简单的响应式系统

750 阅读5分钟

前言:通过自己实现一个简单的响应式系统,来帮助我们理解Vue的响应式原理。

其实响应式系统的本质就是,收集跟响应式数据有关的依赖函数,并且在响应式数据发生改变时触发这些收集到的依赖函数。

要实现的基本功能

我们需要有一个数组来保存所有依赖函数,也需要一个watchFn来收集响应式函数

//保存所有在变量改变需要执行的依赖函数的数组
let reactiveFns = []

//用于收集传入的依赖函数
function watchFn(fn){
  reactiveFns.push(fn)
}

// 先假设这个obj是响应式数据
let obj = {
    name:"wjj",
    age:10
}

//name发生改变要执行的依赖函数
watchFn(function(){
  console.log('这是依赖函数1'+obj.name)
})
//name发生改变要执行的函数
watchFn(function(){
  console.log('这是依赖函数2'+obj.name)
})

//修改响应式数据
obj.name = 'jzsp'
//当name改变时,执行响应式函数
reactiveFns.forEach(fn=>{
  fn()
})

基本响应式的封装

首先我们用类来封装,因为我们可能用到多个响应式数据,不可能把所有依赖函数都保存在同一个数组中。 一个实例保存与某个响应式数据有关的依赖函数(其实就是用到响应式数据的函数

//用于收集某个属性的所有响应式函数
class Depend{
  constructor() {
    this.reactiveFns = []
  }
  //添加响应式函数
  addDepend(fn){
    this.reactiveFns.push(fn)
  }
  //封装的notify方法,用于执行该实例中保存的所有响应式函数
  notify(){
    this.reactiveFns.forEach(fn => {
      fn()
    })
  }
}
let obj = {
    name:"wjj",
    age:10
}

const nameDepend = new Depend()
function watchFn(fn){
  depend.addDepend(fn)
}

//name发生改变要执行的依赖函数
watchFn(function(){
  console.log('这是依赖函数1'+obj.name)
})
//name发生改变要执行的函数
watchFn(function(){
  console.log('这是依赖函数2'+obj.name)
})

//修改响应式数据
obj.name = 'jzsp'

//当name改变时,执行响应式函数
nameDepend.notify()

用Proxy监听对象属性的变化

因为我们总不能每次都手动调用notify函数来触发依赖。所以我们可以利用Proxy代理的set监听,在通过Proxy代理给对象的属性设置值的时候自动调用notify函数。

class Depend {
  constructor() {
    this.reactiveFn = [];
  }
  addDepend(fn) {
    this.reactiveFn.push(fn);
  }
  notify() {
    this.reactiveFn.forEach((item) => item());
  }
}

let obj = {
  name: "wjj",
  age: 20,
};
let nameDepend = new Depend();
function watchFn(fn) {
  nameDepend.addDepend(fn);
}

//监听对象属性的变化:Vue3(Proxy) / Vue2(Object.defineProperty)
const objProxy = new Proxy(obj, {
  get(target, p, receiver) {
    return Reflect.get(target, p, receiver);
  },
  set(target, p, value, receiver) {
    Reflect.set(target, p, value, receiver);
    //当name改变时,执行响应式函数
    nameDepend.notify();
  },
});

//name发生改变要执行的函数
watchFn(function () {
  console.log("这是依赖函数1" + objProxy.name);
});
//name发生改变要执行的函数
watchFn(function () {
  console.log("这是依赖函数2" + objProxy.name);
});

objProxy.name = "jzsp";

使用weakMap来保存每个对象的属性的Depend实例

我们在这一步修改了watchFn函数,默认将传入的函数执行一次,在执行的时候如果用到了响应式数据,就会进入对应的Proxy的get方法

同时我们封装了getDepend方法,用WeakMap保存每个对象的map(保存的内容是<obj,map>),每个对象的map(保存的内容是<key,depend>)里又保存了每个属性的depend实例。通过getDepend方法来获取target对象的key属性的depend实例。

get方法中,获取对应的depend实例,收集依赖

set方法中,获取对应的depend实例,触发依赖

let globalFn;
class Depend {
  constructor() {
    this.reactiveFn = [];
  }
  addDepend(fn) {
    this.reactiveFn.push(fn);
  }
  notify() {
    this.reactiveFn.forEach((item) => item());
  }
}

let obj = {
  name: "wjj",
  age: 20,
};

//收集依赖的函数,传入的函数默认会执行一次,如果用到了响应式数据,会进入对应Proxy的get方法中
function watchFn(fn) {
  globalFn = fn;
  fn();
  globalFn = null;
}

let wm = new WeakMap();

//获取target对象对应属性的depend实例
function getDepend(target, key) {
  let map = wm.get(target);
  if (!map) {
    map = new Map();
    wm.set(target, map);
  }
  let depend = map.get(key);
  if (!depend) {
    depend = new Depend();
    map.set(key, depend);
  }
  return depend;
}

//监听对象属性的变化:Vue3(Proxy) / Vue2(Object.defineProperty)
const objProxy = new Proxy(obj, {
  get(target, p, receiver) {
    //获取target对象的对应属性的depend实例
    let depend = getDepend(target, p);
    // 收集依赖
    depend.addDepend(globalFn);
    return Reflect.get(target, p, receiver);
  },
  set(target, p, value, receiver) {
    Reflect.set(target, p, value, receiver);
    //当name改变时,执行响应式函数
    let depend = getDepend(target, p);
    //触发依赖
    depend.notify();
  },
});

//name发生改变要执行的函数
watchFn(function () {
  console.log("这是依赖函数1" + objProxy.name);
});
//name发生改变要执行的函数
watchFn(function () {
  console.log("这是依赖函数2" + objProxy.name);
});

setTimeout(() => {
  objProxy.name = "jzsp";
}, 1000);

进一步封装,创建响应式对象的reactive函数

let globalFn;
class Depend {
  constructor() {
    this.reactiveFn = [];
  }
  addDepend(fn) {
    this.reactiveFn.push(fn);
  }
  notify() {
    this.reactiveFn.forEach((item) => item());
  }
}

// 创建响应式对象
let obj = reactive({
  name: "wjj",
  age: 20,
});

function watchFn(fn) {
  globalFn = fn;
  fn();
  globalFn = null;
}

let wm = new WeakMap();

function getDepend(target, key) {
  let map = wm.get(target);
  if (!map) {
    map = new Map();
    wm.set(target, map);
  }
  let depend = map.get(key);
  if (!depend) {
    depend = new Depend();
    map.set(key, depend);
  }
  return depend;
}

//传入一个对象,返回这个对象的代理
function reactive(obj) {
  return new Proxy(obj, {
    get(target, p, receiver) {
      getDepend(target, p).addDepend(globalFn);
      return Reflect.get(target, p, receiver);
    },
    set(target, p, value, receiver) {
      Reflect.set(target, p, value, receiver);
      getDepend(target, p).notify();
    },
  });
}

//name发生改变要执行的函数
watchFn(function () {
  console.log("这是依赖函数1" + obj.name);
});
//name发生改变要执行的函数
watchFn(function () {
  console.log("这是依赖函数2" + obj.name);
});

setTimeout(() => {
  obj.name = "jzsp";
}, 1000);

Vue2的响应式原理

vue2的响应式跟vue3的区别在于,创建响应式对象的地方。在vue2中是通过遍历传入的obj对象的key,对每个key调用Object.defineProperty(obj,key,options)来给obj对象添加存取属性描述符

let globalFn;
class Depend {
  constructor() {
    this.reactiveFn = [];
  }
  addDepend(fn) {
    this.reactiveFn.push(fn);
  }
  notify() {
    this.reactiveFn.forEach((item) => item());
  }
}

let obj = reactive({
  name: "wjj",
  age: 20,
});

//收集依赖的
function watchFn(fn) {
  globalFn = fn;
  fn();
  globalFn = null;
}

let wm = new WeakMap();

//获取target对象对应属性的depend实例
function getDepend(target, key) {
  let map = wm.get(target);
  if (!map) {
    map = new Map();
    wm.set(target, map);
  }
  let depend = map.get(key);
  if (!depend) {
    depend = new Depend();
    map.set(key, depend);
  }
  return depend;
}

//监听对象属性的变化:Vue3(Proxy) / Vue2(Object.defineProperty)
function reactive(obj) {
  Object.keys(obj).forEach((key) => {
    let v = obj[key];
    Object.defineProperty(obj, key, {
      get() {
        getDepend(obj, key).addDepend(globalFn);
        return v;
      },
      set(newV) {
        v = newV;
        getDepend(obj, key).notify();
      },
    });
  });
  return obj;
}

//name发生改变要执行的函数
watchFn(function () {
  console.log("这是依赖函数1" + obj.name);
});
//name发生改变要执行的函数
watchFn(function () {
  console.log("这是依赖函数2" + obj.name);
});

setTimeout(() => {
  obj.name = "jzsp";
}, 1000);

以上代码都可以直接复制运行,欢迎大家在评论区多多指正哈~