UzumakiItachi
首页
  • JavaSript
  • Vue

    • Vue2
    • Vue3
  • React

    • React_18
  • WebPack
  • 浏览器相关
  • 工程化相关
  • 工作中遇到的问题以及解决方案
  • Git
  • 面试
  • 学习
  • 心情杂货
  • 实用技巧
  • 友情链接
关于
  • 个人产出
  • 实用工具
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

UzumakiItachi

起风了,唯有努力生存。
首页
  • JavaSript
  • Vue

    • Vue2
    • Vue3
  • React

    • React_18
  • WebPack
  • 浏览器相关
  • 工程化相关
  • 工作中遇到的问题以及解决方案
  • Git
  • 面试
  • 学习
  • 心情杂货
  • 实用技巧
  • 友情链接
关于
  • 个人产出
  • 实用工具
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 手写组合api reactive和ref
  • vue3的计算属性和监视
  • history 路由404,nginx配置
  • 手写Vue3响应式原理
    • 前言
    • 开始
    • reactive
      • 依赖的数据解构
    • 生成依赖
    • 收集依赖
    • 总结
  • 手写Vue3计算属性和Watch
  • vue3在jsx中优雅的使用动态组件
  • 《Vue3》
hanhanbuku
2023-08-07
目录

手写Vue3响应式原理

# 前言

提到Vue3的响应式,大部分人都知道是通过proxyApi来实现的,就像提到Vue2响应式大家都会想到Object.defineProperty。但是你真的了解vue是如何通过这些api来实现的吗?其本身肯定不是单纯的调用一下Api就行了,本文将通过手写一个建议的响应式来深入理解一下vue3

# 开始

在开始之前,让我们先回顾一下Vue2的响应式。在Vue2中是通过Object.defineProperty去劫持data中的数据,在get某个属性的时候将观察者实例添加进依赖数组,然后在set的时候去触发依赖数组里的观察者对象的更新函数。而观察者对象的更新函数则是在模板编译的时候保存下来的一个函数。更详细的介绍可以看另一篇文章 (opens new window)

而在vue3中,则是将响应式从data中剥离了出来。使得我们可以在任意地方将一个变量变成响应式的,并且由于api的不同,也规避了Object.defineProperty的一些劣势,譬如无法对新增的属性进行拦截,只能通过扩展api来实现($set)。下面就来简单实现一下Vue3的响应式吧

# reactive

首先来实现一下响应式Api Reactive

    const handleProxy = {
        get(target, key) {
            const res = Reflect.get(target,key)
            // TODO 收集依赖
            return res
        },
        set(target,key,value){
           const res = Reflect.set(target,key,value)
            // TODO 触发依赖
            return res
        }
    }
    // 创建reactive函数
    function reactive(data){
        return new Proxy(data,handleProxy)
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

这样,一个简单的reactive就完成了。接下来就是去补充收集依赖和触发依赖这两个步骤了。在进行这两个步骤之前,我们再回顾一下Vue2的响应式。 不知道大家还记不记得再Vue2的响应式过程中,有一个名叫Dep的角色。他是专门用来收集依赖的。换句话说,也就是用来收集更新试图的函数的。那Vue3里面其实也会需要这么一个角色。

# 依赖的数据解构

在vue3里通过各种各样的map结构来保存依赖。而每个依赖和响应式属性的关系大概如下

下面来解释一下图中的关系,globalMap是最上层的map对象,他采用WeakMap来存储每一个被响应式的对象,key就是这个对象本身,而value则是一个普通Map对象。这个Map的key就是目标对象自身的每一个key,他的value就是这个key所对应的更新函数的集合。而这个更新函数的集合则又是采用set结构来保存的。

这里讲个题外话,解析一下为什么vue要采用这么复杂的结构来保存依赖。首先是最上层的WeakMap,WeakMap是一个以对象为key的map解构,他的特点是对key采用弱引用,也就是说你将一个对象作为WeakMap的key时是不会影响这个对象被垃圾回收机制回收的。 而当这个对象被回收时,WeakMap里就会自动删除这个key以及他的value。这样对性能优化上就会非常的友好。第二层的map结构和第三层的set结构会更加方便我们查找某一个key,并且set还有自动去重的功能,防止依赖函数重复添加。

# 生成依赖

回归正题,下面我们要做的时收集依赖和触发依赖,那么这个所谓的依赖(更新函数)到底从何而来呢?这里需要准备一个创建依赖的函数。

    // 存放所有依赖
    let globalMap = new WeakMap()
    let activityEffect = null //用于保存当前所需要被收集的依赖,对标Vue2中new Watcher时将自身保存在Dep的target属性上一样

    class CreateEffect {
        /**
         * @param fn 更新试图的函数
         * @param scheduler 计算属性的更新函数
         */
        constructor(fn,scheduler) {
            this.fn = fn
            this.scheduler = scheduler;
        }

        run(){
            activityEffect = this //将自身设置为当前需要被收集的依赖
            const res = this.fn()
            activityEffect = null
            return res
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

这个函数会创建出一个依赖实例,实例接受更新函数作为参数,然后提供一个run方法来执行更新函数,并且重置掉当前需要被收集的依赖对象

关于activityEffect的作用,注释里已经标明了,这里再详细的解析一下。在Vue2中,watcher类的初始化函数有如下步骤

    /* Watcher */
    class Watcher {
        constructor(vm, key, cb) {
            //将自身保存在Dep的静态属性target上,供dep收集
            Dep.target = this
            //保存一下key的值,目的是为了触发这个属性的get,从而收集依赖
            this.oldVal = vm[key]
            //因为上一步已经触发了get收集了依赖,所以这里清空Dep.target,防止循环引用。
            Dep.target = null
        }
    }

1
2
3
4
5
6
7
8
9
10
11
12

而observe的代码如下

    /* Observe */

    class Observe {
        ...
        defineReactive(data,key,value){
            ...
            //为当前这个属性创建一个依赖收集器
            const dep = new Dep()
            //开始劫持属性
            Object.defineProperty(data,key,{
                // 设置可枚举
                enumerable: true,
                // 设置可配置
                configurable: true,
                // 获取值
                get() {
                    //收集依赖
                    Dep.target&&dep.addSub(Dep.target)
                    return value
                },
                //设置值
                set(newValue){
                    if(newValue!==value){
                        //将新的值赋值给value
                        value = newValue
                        //如果新的值是引用类型的话,同时给新的值也添加上响应式
                        that.walk(value)
                        //派发通知,触发依赖更新视图
                        dep.notfiy()
                    }
                }
            })
        }
    }

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

注意这里的dep,在拦截属性的时候创建了一个dep。而这个dep的target对象就是指向的实例化后的watcher。这里关键的步骤是当我们实例化一个watcher的时候,会将当前实例保存到dep的target上。然后访问一下data上的属性从而触发get收集依赖。这个时候收集的就是dep.target。而此时dep.target不就正好保存的是我们刚刚实例化出来的watcher吗。 当收集完之后就会重置掉dep.target。而activityEffect就是用来代替dep.target的。

有了依赖实例,下面我们就可以着手去写一下触发和收集啦

# 收集依赖

    // 收集依赖
    function track(target, key){
        if(!activityEffect) return;
        // 1. 获取 target 对应的依赖映射
        let effectMap = globalMap.get(target);
        if(!effectMap) globalMap.set(target, ( effectMap = new Map() ));
        // 2. 获取 key 对应的依赖集合
        let effectSet = effectMap.get(key);
        if(!effectSet) effectMap.set(key, ( effectSet = new Set() ));
        // 3. 收集依赖
        effectSet.add(activityEffect);
    }
    // 触发依赖
    function trigger(target, key){
        // 1. 获取 target 对应的依赖映射
        const effectMap = globalMap.get(target);
        if(!effectMap) return;
        // 2. 获取 key 对应的依赖集合
        const effectSet = effectMap.get(key);
        if(!effectSet) return;
        // 3. 触发依赖
        effectSet.forEach((effect) => effect.scheduler ? effect.scheduler() : effect.run());
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

这两步就比较简单了,就是去globalMap里存值取值,注意前面提到的globalMap结构。用上对应的方法去存取就行了。下面我们把这个步骤加入到reactive中

    const handleProxy = {
        get(target, key) {
            const res = Reflect.get(target,key)
            //  收集依赖
            track(target,key)
            return res
        },
        set(target,key,value){
           const res = Reflect.set(target,key,value)
            // 触发依赖
            trigger(target,key)
            return res
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14

接下来就差最后一步,生成依赖了。在Vue2中这一步交由watcher来完成,而在Vue3中则有一个名为effect的函数来实现

    // 生成依赖实例
    function effect(fn,options){
        const _effect = new CreateEffect(fn,options.scheduler)
        _effect.run()
        return _effect.run.bind(_effect);
    }
1
2
3
4
5
6

下面我们就将这些代码融入到一个例子当中去看看响应式有没有实现吧

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>vue3响应式</title>
</head>
<body>
<input id="input">
<div id="text"></div>
<script>
    const handleProxy = {
        get(target, key) {
            const res = Reflect.get(target,key)
            //  收集依赖
            track(target,key)
            return res
        },
        set(target,key,value){
           const res = Reflect.set(target,key,value)
            // 触发依赖
            trigger(target,key)
            return res
        }
    }
    // 创建reactive函数
    function reactive(data){
        return new Proxy(data,handleProxy)
    }

    // 存放所有依赖
    let globalMap = new WeakMap()
    let activityEffect = null //用于保存当前所需要被收集的依赖,对标Vue2中new Watcher时将自身保存在Dep的target属性上一样

    class CreateEffect {
        /**
         * @param fn 更新试图的函数
         * @param scheduler 计算属性的更新函数
         */
        constructor(fn,scheduler) {
            this.fn = fn
            this.scheduler = scheduler;
        }

        run(){
            activityEffect = this //将自身设置为当前需要被收集的依赖
            const res = this.fn()
            activityEffect = null
            return res
        }
    }
    // 收集依赖
    function track(target, key){
        if(!activityEffect) return;
        // 1. 获取 target 对应的依赖映射
        let effectMap = globalMap.get(target);
        if(!effectMap) globalMap.set(target, ( effectMap = new Map() ));
        // 2. 获取 key 对应的依赖集合
        let effectSet = effectMap.get(key);
        if(!effectSet) effectMap.set(key, ( effectSet = new Set() ));
        // 3. 收集依赖
        effectSet.add(activityEffect);
    }
    // 触发依赖
    function trigger(target, key){
        // 1. 获取 target 对应的依赖映射
        const effectMap = globalMap.get(target);
        if(!effectMap) return;
        // 2. 获取 key 对应的依赖集合
        const effectSet = effectMap.get(key);
        if(!effectSet) return;
        // 3. 触发依赖
        effectSet.forEach((effect) => effect.scheduler ? effect.scheduler() : effect.run());
    }
    // 生成依赖实例
    function effect(fn,options){
        const _effect = new CreateEffect(fn,options?.scheduler)
        _effect.run()
        return _effect.run.bind(_effect);
    }
    const data = reactive({
        name:''
    })

    const input = document.getElementById("input");

    input.addEventListener("change", (event) => {
        data.name = event.target.value
    });
    function updateView (){
        const dom = document.getElementById('text')
        dom.innerText = data.name
    }
    effect(updateView)
</script>

</body>
</html>

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

效果这里就不贴图了,自行测试。到这里我们就完成了一个建议的vue3响应式。里面还有很多细节和边界问题没考虑。想更具体学习的话可以直接去看源码

# 总结

Vue3的响应式主要是通过proxy去代理对象,然后拦截get和set等一系列操作。在响应式系统的内部维护了一个WeakMap结构的依赖对象用于存放所有的响应式key和其对应的依赖(这里其实就对标的是Vue2的Dep实例)。而产生依赖则是通过Effect这个函数去创建依赖,这里其实对应的就是Vue2里的Watcher观察者。然后在get和set的拦截操作里去收集和触发依赖。学习响应式主要搞清楚在整个系统中有哪些角色,以及每个角色是干嘛的包括他们之间是如何串联起来的。搞明白了这些,就会学的非常轻松了。换句大白话概述一下vue的响应式:编译模板的时候生成更新函数也就是大家口中所说的依赖。然后在拦截到get的时候把当前生成的依赖保存起来,接着在set的时候去调用他们就行了。

编辑 (opens new window)
上次更新: 2023/08/07, 17:19:27
history 路由404,nginx配置
手写Vue3计算属性和Watch

← history 路由404,nginx配置 手写Vue3计算属性和Watch→

最近更新
01
小程序实现全局状态管理
07-09
02
前端检测更新,自动刷新网页
06-09
03
swiper渲染大量数据的优化方案
06-06
更多文章>
Theme by Vdoing | Copyright © 2023-2025 UzumakiItachi | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式