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

相关推荐
苹果酱056712 分钟前
Golang的文件解压技术研究与应用案例
java·vue.js·spring boot·mysql·课程设计
csdnLN20 分钟前
$.ajax() 对应事件done() 、fail()、always() 的用法
前端·javascript·ajax
甜味橘阳20 分钟前
echarts地图可视化展示
前端·javascript·echarts
bloxed1 小时前
前端文件下载多方式集合
前端·filedownload
余生H1 小时前
前端Python应用指南(三)Django vs Flask:哪种框架适合构建你的下一个Web应用?
前端·python·django
LUwantAC1 小时前
CSS(四)display和float
前端·css
cwtlw1 小时前
CSS学习记录20
前端·css·笔记·学习
界面开发小八哥1 小时前
「Java EE开发指南」如何用MyEclipse构建一个Web项目?(一)
java·前端·ide·java-ee·myeclipse
谢道韫6662 小时前
今日总结 2024-12-24
javascript·vue.js·elementui
一朵好运莲2 小时前
React引入Echart水球图
开发语言·javascript·ecmascript