Vue3 响应式原理简易实现,包会。不会来找我

1. 响应式思想

要理解响应式原理,先理解一下什么是响应式。看下面的简单代码

let count = 10;

// 使用到count的一段代码,暂且封装成一个函数
function useCountFn() {
	console.log(count * 2);
}

// 修改count的值
count = 20;
  • count 有一个初始化的值,有一段代码使用到了这个值;
  • 那么在count 有一个新的值时,这段使用到count 的一段代码可以自动重新执行;
    这就是响应式最本质的思想。

类比我们在使用 Excel 电子表格 时的场景。
请添加图片描述
这里单元格 A3 中的值是通过公式 = A1 + A2 来定义的 (你可以在 A3 上点击来查看或编辑该公式),因此最终得到的值为 3。你可以试着更改 A1 或 A2,你会注意到 A3 也会随即 自动更新

2. 对象的响应式

在vue中,响应式更多的体现在对象中。即使普通类型想要变成响应式的,也会封装在ref对象中。

const obj = {
	name: 'mono',
	age: 18,
}

// 使用到对象中某个字段的一段代码
function useObjFn() {
	console.log(obj.name);
}

// 在某处修改该字段
obj.name = 'haha'

即当我们有一段代码使用到某个响应式对象中的字段时,再其他地方修改了这个字段,使用到该字段的代码也会自动执行。

下面就来一步步实现这种思想!

3. 响应式函数的封装

// 封装一个响应式函数
const reactivesFns = [];
function watchFn(fn) {
  reactiveFns.push(fn);
}

const obj = {
  name: 'mono',
	age: 18,
}
// 将需要执行的代码放入响应式函数中
watchFn(function() {
  console.log(obj.name);
});

watchFn(() => {
  console.log("另一个用到name字段的函数", obj.name);
})

obj.name = 'haha';

// 现在需要手动来执行收集的响应式函数
reactiveFns.forEach(fn => fn());

现在收集到的响应式函数全放在一个全局变量reactiveFns里,而且真实开发中也不是只有一个对象,显然这样并不够合理。进行下一步的封装。

4. 封装依赖收集类

// 定义一个依赖管理的类
class Depend {
  constructor() {
    this.reactiveFns = [];
  }

  addDepend(fn) {
    this.reactiveFns.push(fn)
  }

  notify() {
    this.reactiveFns.forEach(fn => fn());
  }
}
// 针对每一个对象的属性都新建一个对象来管理
const depend = new Depend();
function watchFn(fn) {
  depend.addDepend(fn)
}

const obj = {
  name: 'mono',
  age: 18,
}

watchFn(function() {
  console.log(obj.name);
});

watchFn(() => {
  console.log("另一个用到name字段的函数", obj.name);
})

obj.name = 'haha';
depend.notify()

这样封装之后,每一个对象的属性都对应一个Depend对象来管理个字的依赖。离理想越来越近了。但现在总不能每次改变都要手动去notify吧。是时候要自动监听对象的变化了。那么要怎么样才能监听对象属性的变化呢?有2种方式: ProxyObject.definePropertyProxy就是Vue3的响应式原理,Object.defineProperty则是Vue2的响应式原理。

5. 自动监听对象变化

class Depend {
  constructor() {
    this.reactiveFns = [];
  }

  addDepend(fn) {
    this.reactiveFns.push(fn)
  }

  notify() {
    this.reactiveFns.forEach(fn => fn());
  }
}

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

const obj = {
  name: 'mono',
	age: 18,
}
// 监听对象变化
const proxyObj = new Proxy(obj, {
  get(target, key, receiver) {
    return Reflect.get(target, key, receiver)
  }, 
  set(target, key, value, receiver) {
    Reflect.set(target, key, value, receiver);
    // 设置完值之后通知响应式函数执行
    depend.notify();
  }
})

// 之后的操作,都应该是对代理对象 peoxyObj 的操作
watchFn(function() {
  console.log(proxyObj.name);
});

watchFn(() => {
  console.log("另一个用到name字段的函数", proxyObj.name);
})
// 只有对代理对象的操作才会被监听到
proxyObj.name = 'haha';

现在每次改变name属性,均能自动执行需要响应的代码。但是,却有一个问题就是,如果改变的是age属性,响应式函数也执行了,这并不是我们想要的。
那么到底该如何管理各个字段的依赖呢?
这里就涉及到Map这个数据结构了。
每个对象的字段对应自己的depend对象。而且每一个对象也需要保存在Map里,这里推荐使用的是WeakMap,大家可以思考一下为什么要使用WeakMap呢?
最终的数据结构如下:
在这里插入图片描述

6. 依赖收集的管理

// 获取依赖的方法
const targetMap = new WeakMap();
function getDepend(target, key) {
  let map = targetMap.get(target);
  if(!map) {
    map = new Map();
    targetMap.set(target, map)
  }

  // 获取depend对象
  let depend = map.get(key);
  if(!depend) {
    depend = new Depend();
    map.set(key, depend);
  }

  return depend;
}

const proxyObj = new Proxy(obj, {
  get(target, key, receiver) {
    return Reflect.get(target, key, receiver)
  }, 
  set(target, key, value, receiver) {
    Reflect.set(target, key, value, receiver);
    // 在此处获取各自对象和key对应的depend对象
    const depend = getDepend(target, key);
    depend.notify();
  }
})

此时针对每一个对象和每一个对象的字段都做到了各自的管理,但是还无法自动收集依赖项。于是我们想到了,要在get时来收集依赖,来吧,一步步实现它。

7. 正确的收集依赖

// 需要定义一个全局变量来保存当前依赖需要收集的响应式函数
let activeReactiveFn = null;
// 修改 watchFn 函数 如下
function watchFn(fn) {
  activeReactiveFn = fn;
  // 只有执行了fn,才能知道当前使用了哪个属性,才能保存对应的依赖。
  fn();
  activeReactiveFn = null;
}

const proxyObj = new Proxy(obj, {
  get(target, key, receiver) {
    // 在此处自动收集依赖
    const depend = getDepend(target, key);
    depend.addDepend(activeReactiveFn);
    return Reflect.get(target, key, receiver)
  }, 
  set(target, key, value, receiver) {
    Reflect.set(target, key, value, receiver);
    // 在此处获取各自对象和key对应的depend对象
    const depend = getDepend(target, key);
    depend.notify();
  }
})

这时我们就能自动收集各个属性各自对应的依赖了。
但是,在监听对象变化时,我们却依赖了一个全局变量。这样不利于后面的封装,所以可以对Depend类进行重构。
还有一个问题就是,如果收集依赖时,有相同属性的相同依赖时,会重复收集的问题。
下面来解决这些问题。

8. 重构Depend, 修复重复收集问题

// 需要定义一个全局变量来保存当前依赖需要的响应式函数
let activeReactiveFn = null; // 将此变量提升到最上方
class Depend {
  constructor() {
    // 重复依赖收集,采用set特性来去重
    this.reactiveFns = new Set();
  }

  addDepend(fn) {
    this.reactiveFns.add(fn)
  }
  
  depend() {
    if(activeReactiveFn) {
      // set对应的是add方法
      this.reactiveFns.add(activeReactiveFn);
    }
  }

  notify() {
    this.reactiveFns.forEach(fn => fn());
  }
}

9. 对响应式对象的操作封装

到这里还存在一个问题就是,我们如果再想要创建一个响应式对象,则还需要复制一遍监听对象的操作那部分代码,于是我们应该要把响应式对象的操作进行封装。

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      // 在此处自动收集依赖
      const depend = getDepend(target, key);
      depend.depend();
      return Reflect.get(target, key, receiver)
    }, 
    set(target, key, value, receiver) {
      Reflect.set(target, key, value, receiver);
      // 在此处获取各自对象和key对应的depend对象
      const depend = getDepend(target, key);
      depend.notify();
    }
  })
}

// 此时要创建响应式对象时,只需调用这个方法即可
const info = reactive({
  height: 1.88
})
// 使用响应式对象的字段时会自动收集依赖
watchFn(() => {
  console.log("r创建另一个响应式对象->", info.height);
})
// 当修改该字段时,自动触发响应式函数
info.height = 2.0

10.完整代码以及使用方法

// 需要定义一个全局变量来保存当前依赖需要的响应式函数
let activeReactiveFn = null;
class Depend {
  constructor() {
    // 重复依赖收集,采用set特性来去重
    this.reactiveFns = new Set();
  }

  addDepend(fn) {
    this.reactiveFns.add(fn)
  }
  
  depend() {
    if(activeReactiveFn) {
      // set对应的是add方法
      this.reactiveFns.add(activeReactiveFn);
    }
  }

  notify() {
    this.reactiveFns.forEach(fn => fn());
  }
}

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      // 在此处自动收集依赖
      const depend = getDepend(target, key);
      depend.depend();
      return Reflect.get(target, key, receiver)
    }, 
    set(target, key, value, receiver) {
      Reflect.set(target, key, value, receiver);
      // 在此处获取各自对象和key对应的depend对象
      const depend = getDepend(target, key);
      depend.notify();
    }
  })
}

// 获取依赖的方法
const targetMap = new WeakMap();
function getDepend(target, key) {
  let map = targetMap.get(target);
  if(!map) {
    map = new Map();
    targetMap.set(target, map)
  }

  // 获取depend对象
  let depend = map.get(key);
  if(!depend) {
    depend = new Depend();
    map.set(key, depend);
  }

  return depend;
}

// 定义一个响应式函数
function watchFn(fn) {
  activeReactiveFn = fn;
  // 只有执行了fn,才能知道当前使用了哪个属性,才能保存对应的依赖。
  fn();
  activeReactiveFn = null;
}

const obj = {
  name: 'mono',
	age: 18,
}
const proxyObj = reactive(obj);
watchFn(function() {
  console.log(proxyObj.name);
});

watchFn(() => {
  console.log("另一个用到name字段的函数", proxyObj.name);
})

proxyObj.name = 'haha';

// 创建另一个响应式对象
const info = reactive({
  height: 1.88
})
watchFn(() => {
  console.log("r创建另一个响应式对象->", info.height);
})

info.height = 2.0

100行代码简单理解响应式原理。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值