Vue3响应式设计细节与实现原理(一)

前言

vue 做为前端三大框架之一,在国内受欢迎程度远高于其他框架,特别是 vue3 版本的发布,把 vue 的发展又推上了一个台阶。下面我们就以《Vue的设计与实现》为教材来窥探 vue 的设计原理与实现细节吧。

不是大佬,文章如有错误欢迎下方留言,共同学习。

案例代码:github

副作用

最早是在 react hook 中听说过这个概念。其实也很好理解,映射在现实社会就是说 你做一件事影响到了别人

那对于一个函数来说,只要它做的事和函数外部有交集(获取或者修改了外部的变量),那它就产生了副作用也就是变成了副作用函数。

var a = 1;
function fn(){
   a = 2
}

所以如果按照这种方式给函数分类的话可以把函数分为 纯函数副作用函数

那副作用和我们进行要学习的响应式有什么关系呢?

响应式原型

思考一下,如果用白话来描述响应式你会怎么描述呢?我想大概是这样的:我修改了一个变量,页面自己重新渲染了

我们也可以理解为,我修改了一个变量同时执行的它的渲染函数所以页面又重新渲染了,这个时候执行的渲染函数也就是我们说的 副作用函数,他和这个变量有着强依赖的关系。


let name = '掘金'

function render(){
    document.body.innerHtml = name
}

// 改变变量
name = '你好,掘金';
// 执行渲染函数
render();

上面的代码不够自动,每次变量改变还得自己手动的去执行渲染函数,那有没有办法把这一过程变得自动化些呢,那就要提到 JavaScript 标准内置对象 Proxy ,而 vue3 响应式实现也是依赖这个 Proxy。下面我们使用 Proxy 来改造我们的代码。

let data = {
    name: '掘金',
};
const obj = new Proxy(data, {
get(target, key) {
  return target[key];
},
set(target, key, val) {
  target[key] = val;
  render();
},
});
function render() {
    document.body.innerHTML = obj.name;
}
// 首次渲染
render();
// 两秒后改变nam
setTimeout(() => {
    obj.name = '你好,掘金';
}, 2000);

效果如下所示:

11111.gif

看上去我们实现了个简单的响应式原型,但我们需要思考,Vue 的副作用只能执行 render 函数渲染页面吗,显然不是。当一个函数内部依赖了响应式变量的时候,如果响应式变量发生了变化,那这个函数也需要重新执行。

所以上面对响应式的总结显然不完整,应该是:我修改了一个变量,依赖这个变量的函数又重新执行了 这个函数可能是渲染函数,也可能是其他函数。

所以我们的代码应该是这样的

// 定义一个副作用函数,如果obj.name发生变化,这个函数就会自动执行
effect(() => {
  document.body.innerHTML = obj.name;
});

 setTimeout(() => {
    obj.name = '你好,掘金';
 }, 2000);

从使用上来看 effect 入参接收的是一个函数 fn,所以 effect 是个高阶函数。当 obj 触发 set 时需要执行这个 fn,所以 fn 函数需要暴露出来以便使用。

 let activeEffect;
 function effect(fn) {
    activeEffect = fn;
    fn();
 }

当然 effect 副作用函数不止一个,所以我们需要有个数组去存放所有的副作用函数,先用 Set 来存放好了。

//存放所有的副作用
let effects = new Set();

现在我们需要思考一个问题,那就是什么时候需要收集这些副作用?什么时候要去执行这些副作用?当然是触发get的时候去收集,触发set的时候去执行,所以完整代码如下所示:

  let activeEffect;
  let effects = new Set();
  let data = {
    name: '掘金',
  };
  const obj = new Proxy(data, {
    get(target, key) {
      if (activeEffect) {
      // 收集副作用
        effects.add(activeEffect);
      }
      return target[key];
    },
    set(target, key, val) {
      target[key] = val;
      //执行所有的副作用
      effects.forEach((effect) => effect());
    },
  });
  function effect(fn) {
    activeEffect = fn;
    fn();
  }
  //   当 obj.name 改变的时候,希望 effect可以再次被执行
  effect(() => {
    document.body.innerHTML = obj.name;
  });
// 可以存在多个effect
  effect(() => {
    console.log(obj.name);
  });
  setTimeout(() => {
    obj.name = '你好,掘金';
  }, 2000);

这种依赖收集和触发的模式也是我们常说的发布订阅模式

让响应式更加精准

我们来执行这段代码

  let activeEffect;
  let effects = new Set();
  let data = {
    name: '掘金',
    age:10,
  };
  const obj = new Proxy(data, {
    get(target, key) {
      if (activeEffect) {
        effects.add(activeEffect);
      }
      return target[key];
    },
    set(target, key, val) {
      target[key] = val;
      effects.forEach((effect) => effect());
    },
  });
  function effect(fn) {
    activeEffect = fn;
    fn();
  }
  //   当 obj.name 改变的时候,希望 effect可以再次被执行
  effect(() => {
    document.body.innerHTML = obj.name;
    console.log(obj.name);
  });

  setTimeout(() => {
  // 注意改变的是age而不是name
    obj.age = 18;
  }, 2000);

我们发现虽然 effect 中没有依赖 obj.age ,但当我们改变 obj.age 时,effect 还是会重新执行。

Mar-20-2022 16-33-00.gif

这是因为我们的响应式系统的依赖收集和触发的颗粒度不够,我们现在的解决方案是只要 obj 里面的值发生变化都会触发副作用的更新,这显然是不对的。

所以收集依赖时,必须精确到 obj 的key,大致的数据结构设计如下所示:

vue3 中,使用的是 WeakMap来描述这一关系

不清楚 WeakMapMapSet 请恶补。

代码如下,关键地方已给出注释。

  let activeEffect;
  let effects = new WeakMap(); // 存放所有的对象及副作用的关系
  let data = {
    name: '掘金',
  };
  const obj = new Proxy(data, {
    get(target, key) {
       // 判断,有没有 target的关系树
      let depsMap = effects.get(target);
      //如果没有就创建,以当前 obj 为 key
      if (!depsMap) {
        effects.set(target, (depsMap = new Map()));
      }
      // 看 obj.xxx 具体的key有没有创建依赖关系
      let deps = depsMap.get(key);
      // 如果没有就创建
      if (!deps) {
        depsMap.set(key, (deps = new Set()));
      }
      // 如果有依赖 就添加到对应的 key上
      if (activeEffect) {
        deps.add(activeEffect);
      }
      return target[key];
    },
    set(target, key, val) {
      target[key] = val;
      // 从WeakMap中取出对应的依赖关系
      const depsMap = effects.get(target);
      if (depsMap) {
      // 取出obj对应的key
        const effect = depsMap.get(key);
        //如果有副作用函数就执行所有的副作用函数
        effect && effect.forEach((fn) => fn());
      }
    },
  });
  function effect(fn) {
    activeEffect = fn;
    fn();
  }
  effect(() => {
    document.body.innerHTML = obj.name;
    console.log(obj.name);
  });
  setTimeout(() => {
    obj.age = 18;
  }, 2000);

对应的数据结构如下:

Vue3 响应式设计的巧妙之处就在于此,通过这样一种数据结构就把整个响应式的依赖收集以及对应关系描述的清清楚楚。当然我们今天实现的内容是 vue3 响应式的核心,但不是全部,一个完整的响应式系统会非常复杂,需要考虑到的情况也非常的多,但最终都会基于以上这种数据结构去缝缝补补。

最后

  • 副作用嵌套了会不会进入死循环?
  • 如果副作用函数中存在判断逻辑要怎么处理?
  • Vue3watch 函数又是如何实现的?

接下来我们一起探索!

如有帮助,请点赞关注😘

在这里插入图片描述

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值