带你一行一行手写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系列文章(后续更新)

相关推荐
吃杠碰小鸡12 分钟前
commitlint校验git提交信息
前端
天天进步201533 分钟前
Vue+Springboot用Websocket实现协同编辑
vue.js·spring boot·websocket
虾球xz43 分钟前
游戏引擎学习第20天
前端·学习·游戏引擎
我爱李星璇1 小时前
HTML常用表格与标签
前端·html
疯狂的沙粒1 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员1 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
野槐1 小时前
前端图像处理(一)
前端
程序猿阿伟1 小时前
《智能指针频繁创建销毁:程序性能的“隐形杀手”》
java·开发语言·前端
疯狂的沙粒1 小时前
对 TypeScript 中函数如何更好的理解及使用?与 JavaScript 函数有哪些区别?
前端·javascript·typescript
瑞雨溪1 小时前
AJAX的基本使用
前端·javascript·ajax