带你一行一行手写Vue3.0源码系列(二) -依赖收集和派发更新

前言

上一篇中我们主要讲的Vue3.0响应式的实现,其主要是利用Proxy的来对数据进行劫持。本节课接上一章,主要是讲解Vue3.0是如何在被劫持时进行收集依赖的,同时在修改时是如何派发更新的。

我个人推荐的看源码的方式最好是通过视频文章自己能手写一边然后再去看,否则就会造成这样的结果。

真是卷...

Vue 3.0 中没有了watcher 的概念,但是取而代之的是一个叫 effect的东西 ,所以接下来讲的东西会和 effect有关。

正文

effect(类似于Watcher)

ini 复制代码
// reactivity/effect.js

export function effect(fn, options = {}) { // 类似于vue2的watcher
  const effect = createReactiveEffect(fn, options);
  if (!options.lazy) {
    effect();
  }
  return effect;
}

let uid = 0; // effect的id
let activeEffect; // 保存当前的effect
const effectStack = []; // effect栈结构

function createReactiveEffect(fn, options = {}) {
  const effect = function reactiveEffect() {
    if (!effectStack.includes(effect)) {
      try {
        activeEffect = effect; // 当前最顶层的effect
        effectStack.push(activeEffect); // 入栈
        return fn(); // 执行用户的方法
      } finally {
        // 出栈
        effectStack.pop(); // 出栈
        activeEffect = effectStack[effectStack.length - 1]; // 重置栈结构
      }
    }
  }
  effect.id = uid++; // 用于区别effect
  effect._isEffect = true; // 用户区分我们effect是不是响应式的
  effect.raw = fn; // 保存用户的方法
  effect.options = options; // 保存用户的属性
  effect.active = true;
  return effect;
}

组件在初始化渲染的时候会创建一个effect,和Watcher会传入一个函数。当这个effect被执行的时候会存入全局栈,初次创建effect的时候会判断当前栈是否存在当前effect。文中的activeEffectVue2.0中的Dep.target是一个意思,都是指的当前执行的作用域。

依赖收集Track

csharp 复制代码
// reactivity/effect.js

let targetMap = new WeakMap(); // 存储effect的一种结构
export function Track(target, type, key) { // target:目标对象 key: 对象中的值
  if (!activeEffect) return
  let depMap = targetMap.get(target);
  if (!depMap) {
    targetMap.set(target, (depMap = new Map()))
  }
  let dep = depMap.get(key);
  if (!dep) {
    depMap.set(key, (dep = new Set()))
  }
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect); // 收集effect
  }
}

Vue3.0的依赖收集和Vue2.0的有所区别,vue2中是利用闭包对每个属性都创建了一个dep来进行依赖收集。而在Vue3中是创建了一种特殊的存储结构:目标对象 --> 对象属性key --> effect数组。

在GET处收集

javascript 复制代码
// reactivity/baseHandlers.js

function createGetter(isReadonly = false, shallow = false) {
  return function get(target, key, receiver) {
    /*
    *******
    *******此处省略
    */
    if (!isReadonly) {
      // 收集依赖
      Track(target, TrackOpTypes.GET, key); // +++++++++++++++++
    }
    return res;
  }
}

Proxy中,如果不是只读就对当前属性进行依赖收集。

派发更新trigger

scss 复制代码
// reactivity/baseHandlers.js

// 触发更新
export function trigger(target, type, key, newValue, oldValue) {
  const depsMap = targetMap.get(target); // 获取当前对象的属性集合
  if (!depsMap) return;
  let effects = new Set(); // 需要执行的effect执行列
  const add = (effectAdd) => { // 把effect添加到effect执行列中
    if (effectAdd) {
      effectAdd.forEach(effect => {
        effects.add(effect);
      })
    }
  }
  add(depsMap.get(key)); // 根据属性获取当前属性的effect列
  if (key === 'length' && isArray(target)) {
    depsMap.forEach((dep, key) => { // 如果修改的是数组的长度,这里需要做特殊处理
      if (key === 'length' || key >= newValue) {
        add(dep)
      }
    })
  } else {
    if (key !== undefined) {
      add(depsMap.get(key));
    }
    switch (type) {
      case TriggerOpTypes.ADD: {
        if (isArray(target) && isIntergetKey(key)) {
          add(depsMap.get('length'))
        }
        break;
      }
    }
  }
  effects.forEach(effect => { // 执行effect
    if (effect.options.scheduler) { // 在computed和watch中会执行
      effect.options.scheduler(effect);
    } else {
      effect();
    }
  })
}

trigger() 函数,首先获取当前 targetMapdata 对应的主题对象的 depsMap,而这个 depsMap 即我们在依赖收集时在 track 中定义的。

测试

xml 复制代码
<body>
  <div id="app"></div>
  <button id="updateButton">改变</button>
</body>
ini 复制代码
let state = reactive({
  age: 22,
  name: '小明'
});

const el = document.getElementById('app')

effect(() => { // 模拟页面使用响应式数据
  el.innerHTML = state.age;
})

document.getElementById('updateButton').onclick = function() {
  state.age++;
}

我们使用effect来模拟我们在页面使用reactive的响应式数据,因为目前还没有实现模版编译以及渲染的代码,发现我们点击按钮页面会一直更新。

小结

对比Vue2.0通过Object.defineProperty在数据定义时为指定具体属性添加 getter/setter 拦截、为每个对象添加deps依赖数组的响应式系统实现方案。Vue3.0采用Proxy结合WeakMap、WeakSet,通过代理在运行时拦截对象的基本操作来收集依赖并全局管理,在性能上就得到了很大的提升,也为开发者带来了更好的开发体验。

如果觉得本文有帮助 记得点赞三连哦 十分感谢!

vue2.0 和 vue3.0系列文章(后续更新)

相关推荐
拉不动的猪8 分钟前
前端常见数组分析
前端·javascript·面试
小吕学编程24 分钟前
ES练习册
java·前端·elasticsearch
Asthenia041232 分钟前
Netty编解码器详解与实战
前端
袁煦丞36 分钟前
每天省2小时!这个网盘神器让我告别云存储混乱(附内网穿透神操作)
前端·程序员·远程工作
Mr.app1 小时前
vue mixin混入与hook
vue.js
一个专注写代码的程序媛2 小时前
vue组件间通信
前端·javascript·vue.js
一笑code2 小时前
美团社招一面
前端·javascript·vue.js
懒懒是个程序员2 小时前
layui时间范围
前端·javascript·layui
NoneCoder2 小时前
HTML响应式网页设计与跨平台适配
前端·html
凯哥19702 小时前
在 Uni-app 做的后台中使用 Howler.js 实现强大的音频播放功能
前端