SakuraSnow's blog SakuraSnow's blog
首页
  • JavaScript
  • TypeScript
  • Vue
  • React
  • Git
  • Node
  • Linux
  • 技术文档
  • 博客搭建
  • 数据结构
  • leetcode
  • 关于
  • 友链
  • 收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

SakuraSnow

一只前端咸鱼
首页
  • JavaScript
  • TypeScript
  • Vue
  • React
  • Git
  • Node
  • Linux
  • 技术文档
  • 博客搭建
  • 数据结构
  • leetcode
  • 关于
  • 友链
  • 收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 基本使用

  • 实现原理

    • 一步步带你看懂vue2的响应式原理
    • 简单说说计算属性的原理
      • 聊聊$nextTick源码
    • Vue笔记
    • 实现原理
    SakuraSnow
    2020-01-29

    简单说说计算属性的原理

    Vue中的计算属性,在开发中经常会用到,但是理解它似乎不是那么容易,这篇文章会用尽量简单的代码介绍计算属性的运行过程。

    首先讲讲计算属性大概的思路,计算属性其实是一个getter函数,这个getter函数通过Object.defineProperty来定义,当每次试图获取计算属性的值的时候,getter函数就会执行,返回值就是获得的计算属性的值。计算属性的值有可能依赖于data,prop,computed中的其他项,我们要进行一个依赖收集的操作,找出这个计算属性依赖于其他的什么,并订阅依赖项的变化(就是在依赖项的值发生变化时,通知计算属性),在计算属性收到通知后,重新计算值,并缓存,然后触发视图更新。

    我做了一个简单的demo,最后的效果是这样的

    可以看到,当a和b的值改变的时候,total(a+b)也会改变,随即触发刷新视图的逻辑,我们接下来通过讲解这个demo来理解computed的大致运行过程

    # 依赖收集

    这里我们使用一个简单的发布-订阅模式,来对依赖进行收集和通知,下面是Dep类的定义。

    
    class Dep {
        constructor() {
            // 初始化订阅者列表
            this.subs = new Set();
        }
        // 添加订阅者
        addSub(sub) {
            !this.subs.has(sub) && this.subs.add(sub);
        }
        // 通知所有订阅者数据变动
        notify(val) {
            this.subs.forEach((sub) => {
                sub.update(val);
            })
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17

    那么computed的依赖是什么呢,先来看一个简单的例子

    
    let extend = {
        data : {
            a : 1,
            b : 2,
            obj : {
                c : 3
            }
        },
        computed : {
            total() {
                let a = this.a;
                let b = this.b;
                let c = this.obj.c;
                return a + b + c;
            }
        }
    };
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

    这里的total是根据data里的a,b和c计算出来的,所以total的依赖就是data里的a,b,c,那么我们要做的是就是把total让total作为订阅者,订阅a和b的数据变动,这样当a和b和c的数据发生变化时,total就能及时得到通知,并且更新它的值。

    要是想得知a或b或c的数据变动,有两种办法,一个是使用 Object.defineProperty定义属性的set和get方法,另一个是用proxy拦截get和set方法,这里为了方便演示(简化代码),我们使用proxy,如果你不了解proxy,可以看看这里 (opens new window)。

    我们来看看proxy拦截的具体实现

    
    function observe(obj) {
        if (typeof obj !== "object") return;
        let proxy = getProxyObj(obj);
        Object.keys(proxy).forEach((key) => {
            if (typeof proxy[key] === "object") {
                proxy[key] = observe(proxy[key]);
            }
        });
        return proxy;
    }
    
    function getProxyObj(obj) {
        let map = {};
        let proxy = new Proxy(obj, {
            get(target, key, proxy) {
                if (global[depSymbol]) {
                    let dep = map[key] ? map[key] : (map[key] = new Dep());
                    dep.addSub(global[depSymbol]);
                }
                return target[key];
            },
            set(target, key, value, proxy) {
                target[key] = value;
                map[key] && map[key].notify(value);
            }
        });
        return proxy;
    }
    
    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

    我们在把一个对象传入observe方法之后,observe方法会先生成一个这个对象的proxy,然后遍历这个对象的属性,如果属性的值是一个对象,则递归调用observe,最后代理整个对象。

    代理一个对象的方法是getProxyObj,这个方法返回的代理,拦截了访问对象属性时的get和set方法。那么get和set方法分别做了什么呢。

    在get中,对象除了会正常返回属性的值,还会检查全局上的一个属性global[depSymbol]是否为空,为什么要去检查这个位置呢,其实这个位置放置的就是依赖于当前属性的值(当然不是真的值,是一个订阅者对象,但是这个订阅者对象是根据值的一些信息生成的),如果我们在计算 计算属性的值时候,提前把计算属性对应的订阅者放到这个位置,那么在计算属性计算的时候,必然会触发它所依赖的属性的get方法,从而被依赖的属性就能知道某个计算属性依赖于自身,从而把它添加到订阅者列表里。

    在set方法中,除了会重新设定属性的值,还会检查有没有订阅了当前属性变化的订阅者,如果有,通知他们属性已经变化。也就是在被通知后,计算属性会重新计算它的值。

    这里出现了一些陌生的变量,depSymbol和global,global就是全局,任何地方都能访问,depSymbol是个symbol,global[depSymbol]会作为订阅者保存的位置,他们的定义如下。

    
    let depSymbol = Symbol("depSymbol");
    let global = typeof window !== "undefined" ? window : {};
    
    1
    2
    3

    # 加工计算属性

    计算属性其实是一个特殊的getter函数,我们在传入的计算属性,一般是一个函数,那么这个函数需要做一些特殊的处理,转化成getter。

    
    function initComputed(obj, bindThis) {
        if (typeof obj.computed !== "object") return;
        Object.keys(obj.computed).forEach((key) => {
            // 处理每一个计算属性
            if (typeof obj.computed[key] === "function") {
                wrapComputed(obj.computed, key, bindThis)
            }
        })
    }
    
    function wrapComputed(computedObj, key, bindThis) {
        // obj是订阅者对象
        let obj = {};
        // 计算属性值的缓存
        let valCache = undefined;
        // 计算 计算属性的源函数
        let originFun = computedObj[key];
        // 给订阅者对象设置update函数
        obj.update = function () {
            // 重新计算计算属性的值
            valCache = originFun.call(bindThis);
            // 通知所有的订阅者数据更新
            // 订阅者可能是视图或者是其他值
            computedDepMap[key].notify(valCache);
        }.bind(bindThis);
        // 把计算属性定义成访问器属性
        Object.defineProperty(computedObj, key, {
            get() {
                return valCache;
            },
            set(v) {
                valCache = v;
            }
        });
        // 计算一次计算属性的值
        // 计算前先把计算属性对应的订阅者放到全局置顶位置
        // 方便被收集
        (function () {
            global[depSymbol] = obj;
            valCache = originFun.call(bindThis);
            global[depSymbol] = undefined;
        })();
    }
    
    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

    initComputed会遍历所有的计算属性,然后调用wrapComputed处理这些计算属性,wrapComputed中用闭包保存了一个属性valCache,这就是计算属性的缓存,然后我们新建这个计算属性对应的订阅者对象,设置update函数(数据变动时会执行这个函数并传入对应参数),然后把计算属性设置成访问器属性,这样就可以直接使用vm.total这样的方式获取值,而不是vm.total(),最后我们把订阅者对象设置到global[depSymbol]上,然后执行一次计算属性的计算函数,这样计算属性的依赖就可以得知这个计算属性依赖于自身,并把这个订阅者对象添加到列表里。

    # 全部代码

    这是测试用的代码

    index.html

    
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
        <script src="proxy.js" defer></script>
    </head>
    <body>
        <div id="app">
            
        </div>
    </body>
    </html>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14

    proxy,js

    
    let depSymbol = Symbol("depSymbol");
    let global = typeof window !== "undefined" ? window : {};
    
    class Dep {
        constructor() {
            this.subs = new Set();
        }
        addSub(sub) {
            !this.subs.has(sub) && this.subs.add(sub);
        }
        notify(val) {
            this.subs.forEach((sub) => {
                sub.update(val);
            })
        }
    }
    // 保存依赖于某个计算属性(比如视图或者其他计算属性)Dep
    let computedDepMap = {};
    computedDepMap["total"] = new Dep();
    computedDepMap.total.addSub({
        update(val) {
            document.getElementById("app").innerText =  `a + b + c = ${val}\na = ${proxy.a}\nb = ${proxy.b}\nc = ${proxy.obj.c}`;
        }
    });
    
    function observe(obj) {
        if (typeof obj !== "object") return;
        let proxy = getProxyObj(obj);
        Object.keys(proxy).forEach((key) => {
            if (typeof proxy[key] === "object") {
                proxy[key] = observe(proxy[key]);
            }
        });
        return proxy;
    }
    
    function getProxyObj(obj) {
        let map = {};
        let proxy = new Proxy(obj, {
            get(target, key, proxy) {
                if (global[depSymbol]) {
                    let dep = map[key] ? map[key] : (map[key] = new Dep());
                    dep.addSub(global[depSymbol]);
                }
                return target[key];
            },
            set(target, key, value, proxy) {
                target[key] = value;
                map[key] && map[key].notify(value);
            }
        });
        return proxy;
    }
    
    function initComputed(obj, bindThis) {
        if (typeof obj.computed !== "object") return;
        Object.keys(obj.computed).forEach((key) => {
            if (typeof obj.computed[key] === "function") {
                wrapComputed(obj.computed, key, bindThis)
            }
        })
    }
    
    function wrapComputed(computedObj, key, bindThis) {
        let obj = {};
        let valCache = undefined;
        let originFun = computedObj[key];
        obj.update = function () {
            valCache = originFun.call(bindThis);
            computedDepMap[key].notify(valCache);
        }.bind(bindThis);
        Object.defineProperty(computedObj, key, {
            get() {
                return valCache;
            },
            set(v) {
                valCache = v;
            }
        });
        (function () {
            global[depSymbol] = obj;
            valCache = originFun.call(bindThis);
            global[depSymbol] = undefined;
        })();
    }
    
    
    
    
    
    
    let extend = {
        data : {
            a : 1,
            b : 2,
            obj : {
                c : 3
            }
        },
        computed : {
            total() {
                let a = this.a;
                let b = this.b;
                let c = this.obj.c;
                return a + b + c;
            }
        }
    };
    
    
    let proxy = observe(extend.data);
    initComputed(extend, proxy);
    console.log(extend.computed.total);          // 6
    proxy.a = 10;
    proxy.b = 20;
    proxy.obj.c = 30;
    console.log(extend.computed.total);          // 60
    
    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
    115
    116
    117
    118

    这里有些地方上面没有讲到,computedDepMap的属性对应用来保存订阅了某个计算属性的列表,比如在vue中,这样的语法,就说明这里依赖于total这个计算属性, computedDepMap["total"]是一个Dep,Dep.subs里会保存这个依赖。至于我为什么要直接把它写出来,因为原本这部分在vue里是用模板编译生成AST来完成的,实现比较复杂,这里为了让代码简洁一点就直接写结果了。

    #vue
    上次更新: 2022/03/05, 15:57:30
    一步步带你看懂vue2的响应式原理
    聊聊$nextTick源码

    ← 一步步带你看懂vue2的响应式原理 聊聊$nextTick源码→

    最近更新
    01
    009-Palindrome Number[回文数]
    03-10
    02
    008-String to Integer (atoi)[字符串转整数]
    03-10
    03
    004-Reverse-integer[整数反转]
    03-09
    更多文章>
    Theme by Vdoing | Copyright © 2019-2022 Evan Xu | MIT License
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式
    ×