CD's blog CD's blog
首页
  • HTMLCSS
  • JavaScript
  • Vue
  • TypeScript
  • React
  • Node
  • Webpack
  • Git
  • Nestjs
  • 小程序
  • 浏览器网络
  • 学习笔记

    • 《TypeScript 从零实现 axios》
    • Webpack笔记
  • JS/TS教程

    • 《现代JavaScript》教程
🔧工具方法
  • 网站
  • 资源
  • Vue资源
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

CD_wOw

内卷的行情,到不了的梦
首页
  • HTMLCSS
  • JavaScript
  • Vue
  • TypeScript
  • React
  • Node
  • Webpack
  • Git
  • Nestjs
  • 小程序
  • 浏览器网络
  • 学习笔记

    • 《TypeScript 从零实现 axios》
    • Webpack笔记
  • JS/TS教程

    • 《现代JavaScript》教程
🔧工具方法
  • 网站
  • 资源
  • Vue资源
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Vue2基础及原理

  • Vue3基础及原理

    • Vue3 题解QA
    • 实现一个mini-vue3
    • Vue3实现原理
    • Vue3的响应式源码浅析
      • 前言
      • 目录结构
      • 书写时刻
        • vue3.0 的使用方式
        • index.js
        • reactive.js
        • shared/utils
        • baseHandlers
        • effect.js
        • computed.js
    • Vue3的patch原理
  • 周边工具

  • Vue笔记
  • Vue3基础及原理
CD
2020-07-05
目录

Vue3的响应式源码浅析

# 前言

vue3.0 近期一直都是前端界的一个热点,虽然说我知道一个新的迭代从出生到投入生产还需要走较长的一段时间,但是听闻它对打包和编译速度的大大优化让我对其新的构造产生了好奇

# 目录结构

走到 github 打开其 vue 项目 vue-next (opens new window)我们可以看到其项目的目录结构,采用的是 rollup 和 vite 进行项目的打包编译,取代了之前使用 webpack 的方式,其原因照我分析应该是因为 webpack 想进行一个项目的合理、高效的打包需要进行许多繁杂化的配置,且需要一定的学习成本,像 cli4 之前人们手动修改 webpack 配置,之后也还是需要了解 webpack-clain,现在相比之下我认为是更近一步了的。rollup、gulp 这类的打包工具也是十分的轻量。

并打开 package.json 看看

"workspaces": [
    "packages/*"
  ]
1
2
3

package 有很多种书写类型,这次 vue 自己定义了一个工作空间 packages 文件夹,所以我们来看看里面有什么 pacakges

可以看到 vue 的核心源码全部都被放在了 packages 里了,而其他一些文件大致是使用 node 完成对文件的读写等操作。按照 1.X 和 2.x 的习惯 我们可以顾名思义的来分门别类一下:

  • compiler 类文件:对虚拟 dom 元素进行编译成真是 dom 结构并准备挂载。
  • runtime 类文件:对整体的大局进行一个函数的调用执行,在 core 核心模块里导出了 vue3.0 所用到的各种 api、生命周期。
  • shared:存放常用公共函数。
  • reactivity:proxy 绑定依赖收集的函数。
  • vue: 也就是 vue 的入口了。

可以从项目的脉络。并且点击进去看,能够发现 3.0 的源码阅读非常的舒服,脉络相比之前来说分明了太多,连我这样的小白阅读起来也觉得比以往轻松了太多。这篇记录就先不从入口开始步步解析了,直接来看 proxy 先吧。打开 reactivity 文件。 pacakges

# 书写时刻

Vue3.0 中响应式原理的作用是什么? 为什么比 Vue2.0 性能高? 3。0 中改用了 proxy 进行依赖收集,对对象进行递归劫持,对处理值的变化进行判断。 2。0 的时候是通过 Dep 和 Watcher 进行依赖的收集和分发,3.0 使用了 effect 将视图渲染和数据响应式分离开来。且采用模块化的引入的形式,在不使用某一些功能的时候即不需要引入功能模块,可以减少打包编译的大小和时间。vue3。0 是在使用到当前对象时才初始化代理,而 vue2.0 的响应式是在一开始便用递归重写被代理对象的 Object.defineProperty。 proxy 和 defineProperty 有什么区别嘛? 一个是劫持操作,一个是劫持属性。 proxy 只需要代理了一个对象的 set、get 操作,那么相当于对这个对象的所有子属性做了劫持。 而 defineProperty 则需要对每一个属性进行遍历然后劫持。因此在 2.x 里,vue 需要对 data 数据进行递归遍历,对 data 数据做深度的 Observer 处理,因此如果传入的 data 是比较深的层级,或者有很多没有用到的数据,都会影响到 vue 的初始化的速度。而 3.x 只需要在对象上做一层代理,而这个对象上就算有很多无需做响应式处理的数据也没关西,因为子对象是否需要被递归代理是在用到的时候才决定的。可以说相比 2.x 基本上没有了 initData 这部分的时间。也无须要通过$set 来添加新属性的监听了。

# vue3.0 的使用方式

import { reactive, effect, computed, ref } from 'vue'; // 也就是component-api
export defaults {
  setup() {
    const state= reactive( { name: 'cd', arr: [1, 2, 3] } )
    state.name = 'xgzz';
    return { state }
  }
}
1
2
3
4
5
6
7
8

我们按照 vue 的方法在本地创建 reactivity 文件夹,目录形式为: image.png 还需要创建一个 shared 文件存放公共的函数,之前有说过。 将引入改写为

import { reactive, effect, computed, ref } from "./reactivity";
const state = reactive({ name: "cd", arr: [1, 2, 3] });
state.name = "xgzz";
1
2
3

# index.js

index.js 是将所有的函数做一个统一的导出

export { computed } from "./computed";
export { effect } from "./effect";
export { ref } from "./ref";
export { reactive } from "./reactive";
1
2
3
4

真实的源码当然不止这么点,还有 computed、effect、ref 和他们配对的函数-。-不过我看不完那么多,大概看一些能跑的原理就行。

# reactive.js

reactive 的作用是创建出一个响应式的对象, 目标对象可能不一定是数组或者对象, 可能还有 set map

reactive.js

import { isObject } from "../shared/utils";
import { mutableHandler } from "./baseHandlers";
// 我们学习vue将操作模块化区分开。
export function reactive(target) {
  // 创建一个响应式的对象 目标对象可能不一定是数组或者对象 可能还有 set map
  return createReactiveObject(target, mutableHandler);
}
function createReactiveObject(target, baseHandler) {
  if (!isObject(target)) {
    // 不是对象直接返回即可
    return target;
  }
  const observed = new Proxy(target, baseHandler);
  return observed;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# shared/utils

export const isObject = (val) => typeof val == "object" && val != null;
export const hasOwn = (target, key) =>
  Object.prototype.hasOwnProperty.call(target, key);
export const hasChanged = (newValue, oldValue) => newValue !== oldValue;
export const isFunction = (func) => typeof func == "function";
1
2
3
4
5

# baseHandlers

/**
 * proxy第二个参数为object类型,存放get/set
 * 对一个空对象架设了一层拦截,重定义了属性的读取(get)和设置(set)行为。
 * 它是对语言层面进行了一层拦截,相比以前defineproperty对属性拦截更nice了
 * get传入target, propKey, receiver
 * set传入target, propKey, value, receiver
 * 将get和set剖离开来,这样做的原因是会有很多不同的时候需要用到不同的get/set进行监听
 */

import { TrackOpTypes, TriggerOpTypes } from "./operation";
import { trigger, track } from "./effect";
import { isObject, hasOwn, hasChanged } from "../shared/utils";
import { reactive } from "./reactive";

const get = /*#__PURE__*/ createGetter();
const set = /*#__PURE__*/ createSetter();

// const shallowGet = /*#__PURE__*/ createGetter(false, true)
// const readonlyGet = /*#__PURE__*/ createGetter(true)
// const shallowReadonlyGet = /*#__PURE__*/ createGetter(true, true)
// const shallowSet = /*#__PURE__*/ createSetter(true)

function createGetter() {
  // 忽略isReadonly和shallow的各种判断和做的事情,直接看简单版的get/set
  return function get(target, propKey, receiver) {
    //每一个Proxy对象的拦截操作(get、delete、has),内部都调用对应的Reflect方法,保证原生行为能够正常执行。添加的工作,就是将每一个操作输出一行日志。
    //有了Reflect对象以后,很多操作会更易读。
    const res = Reflect.get(target, propKey, receiver); // 等同于 target[propKey],this = receiver
    track(target, TrackOpTypes.GET, key); // 之前的步骤已经完成了基础的get,现在进行依赖收集
    if (isObject(res)) {
      return reactive(res); // 如果是object类型,循环进行依赖收集
    }
    return res;
  };
}

function createSetter() {
  return function set(target, propKey, value, receiver) {
    // 需要判断是修改属性还是增加属性,如果原来的值和新设置的值一样就退出
    const oldValue = target[propKey];
    const hadKey = hasOwn(target, key);

    //如果 Proxy对象和 Reflect对象联合使用,前者拦截赋值操作,后者完成赋值的默认行为,而且传入了receiver,那么Reflect.set会触发Proxy.defineProperty拦截。
    const result = Reflect.set(target, propKey, value, receiver); // target[propKey] = value
    if (!hadKey) {
      // 之前的步骤已经完成了基础的set,现在进行依赖收集
      // console.log('属性的新增操作',target,key);
      trigger(target, TriggerOpTypes.ADD, key, value);
    } else if (hasChanged(value, oldValue)) {
      // console.log('修改操作',target,key);
      trigger(target, TriggerOpTypes.SET, key, value, oldValue); // 触发依赖更新
    }
    // 值没有变化什么都不用做
    return result;
  };
}

export const mutableHandler = {
  get,
  set,
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61

# effect.js

首先观看一下 effect 的映射结构: 通过 weakMap 进行依赖的收集,其 key 值为一个对象,value 为一个 Map,里面又放了一个 Set 存放 effect effect 的作用相当于完成了 vue2.0 的 watcher、dep 所做的事。其结构为 WepeakMap {Object:Map{Set}}

WeakMap
{
  {name:"cd"}:Map{name: Set{effect, effect}},
  {name:"laoba", age: 30} : Map{
   name: Set {effect},
    age: Set {effect, effect}
  }
}
1
2
3
4
5
6
7
8
import { TriggerOpTypes } from "./operation";

// options默认是一个空对象EMPTY_OBJ,里面通常会有
// interface ReactiveEffectOptions {
//   lazy?: boolean
//   computed?: boolean
//   scheduler?: (job: ReactiveEffect) => void
//   onTrack?: (event: DebuggerEvent) => void
//   onTrigger?: (event: DebuggerEvent) => void
//   onStop?: () => void
// }

// effect要做什么事呢? effect创建数组存储响应式的依赖,它就相当于vue2.0的Watcher
// 在vue的初始化时,会触发一次effect、在effect内的值改变的时候,会再次触发effect

export function effect(fn, options = {}) {
  // watchEffect
  const effect = createReactiveEffect(fn, options);
  if (!options.lazy) {
    // 后续可能有lazy的情况
    effect(); // 默认就要执行
  }
  return effect;
}
// 创建响应式的effect
let uid = 0;
let activeEffect;
const effectStack = []; //  // effect栈结构 const effectStack: ReactiveEffect[] = []
function createReactiveEffect(fn, options) {
  const effect = function reactiveEffect() {
    if (!effectStack.includes(effect)) {
      // 如果在栈中就不要再执行fn了,不然会造成不断更改属性时产生死循环
      try {
        // 防止执行fn时报错
        effectStack.push(effect);
        activeEffect = effect; // 将effect放到了activeEffect上,这时候执行fn的时候就能访问到effect
        return fn();
      } finally {
        effectStack.pop();
        activeEffect = effectStack[effectStack.length - 1];
      }
    }
  };
  effect.options = options;
  effect.id = uid++;
  effect.deps = []; // 依赖了哪些属性
  // todo...
  return effect;
}

const targetMap = new WeakMap(); //用法和map一致  但是弱引用 不会导致内存泄漏
export function track(target, type, key) {
  // 这里要做的事情就是进行依赖收集,结构为下面我所写的结构
  if (activeEffect == undefined) {
    // 判断是否绑定了activeEffect,如果没有,说明取值的属性 不依赖于effect
    return;
  }
  let depsMap = targetMap.get(target); // 将target作为key进行读、取,先看下里面有没有存过该target的依赖

  if (!depsMap) {
    // 没有存储过该target依赖,以target为key创建map数组,随后会在map数组内存放effect
    targetMap.set(target, (depsMap = new Map()));
  }
  let dep = depsMap.get(key); // 这个时候depsMap就为该target收集依赖的数组了
  if (!dep) {
    // 来看看target对象中指定要触发或收集的key有没有收集到依赖,如果没有,则用set创建空数组并指向
    depsMap.set(key, (dep = new Set()));
  }
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect); // { "{name:'cd'}":{name:set(effect)}  }
    // activeEffect.deps.push(dep); // 让这个effect 记录dep属性
  }
}
export function trigger(target, type, key, value, oldValue) {
  const depsMap = targetMap.get(target); // 获取当前对应的map
  if (!depsMap) {
    return;
  }
  // 计算属性要优先于effect执行,这里对computed依赖进行收集
  const effects = new Set();
  const computedRunners = new Set();

  const add = (effectsToAdd) => {
    if (effectsToAdd) {
      effectsToAdd.forEach((effect) => {
        if (effect.options.computed) {
          computedRunners.add(effect);
        } else {
          effects.add(effect);
        }
      });
    }
  };
  if (key !== null) {
    add(depsMap.get(key));
  }
  if (type === TriggerOpTypes.ADD) {
    // 对数组新增属性 会触发length 对应的依赖 在取值的时候回对length属性进行依赖收集
    add(depsMap.get(Array.isArray(target) ? "length" : ""));
  }

  const run = (effect) => {
    // 这里的scheduler是为了作用computed的,computed计算属性通常设置了return的值的缓存,通过dirty变量
    // 进行脏值检测,判断这次依赖更新是否需要重新计算computed的值,scheduler传入一个数组,更改dirty的值并触发依赖更新
    if (effect.options.scheduler) {
      effect.options.scheduler();
    } else {
      effect();
    }
  };
  // 触发更新computed 要在 effect之前触发,计算属性是基于effect实现的。computed是改变数据,effect是更新视图,要先改变再更新
  computedRunners.forEach(run);
  effects.forEach(run);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114

# computed.js

computed 的使用方式基本有两种:

// 1.函数的形式
computed(() => {});
// 2.getter/setter的形式
computed({
  get() {},
  set() {},
});
1
2
3
4
5
6
7
import { isFunction } from "../shared/utils";
import { effect, track, trigger } from "./effect";
import { TrackOpTypes, TriggerOpTypes } from "./operation";

export function computed(getterOrOptions) {
  let getter;
  let setter;
  // 判断使用哪种方式进行computed
  if (isFunction(getterOrOptions)) {
    getter = getterOrOptions;
    setter = () => {};
  } else {
    getter = getterOrOptions.get;
    setter = getterOrOptions.set;
  }
  let dirty = true; // 默认第一次取值是执行getter方法的

  let computed;
  // 计算属性也是一个effect
  let runner = effect(getter, {
    lazy: true, // 懒加载
    computed: true, // 这里仅仅是标识而已 是一个计算属性
    scheduler: () => {
      if (!dirty) {
        dirty = true; // 等会就算属性依赖的值发生变化后 就会执行这个scheduler
        trigger(computed, TriggerOpTypes.SET, "value");
      }
    },
  });
  let value;
  computed = {
    get value() {
      if (dirty) {
        // 多次取值 不会重新执行effect
        value = runner();
        dirty = false;
        track(computed, TrackOpTypes.GET, "value");
      }
      return value;
    },
    set value(newValue) {
      setter(newValue);
    },
  };

  return computed;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
编辑 (opens new window)
#Vue3
上次更新: 2023/03/22, 15:28:00
Vue3实现原理
Vue3的patch原理

← Vue3实现原理 Vue3的patch原理→

最近更新
01
gsap动画库学习笔记 - 持续~
06-05
02
远程组件加载方案笔记
05-03
03
小程序使用笔记
03-29
更多文章>
Theme by Vdoing | Copyright © 2020-2023 CD | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式