【Vue原理】Computed - 源码版

写文章不容易,点个赞呗兄弟
专一 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工做原理,源码版助于了解内部详情,让咱们一块儿学习吧
研究基于 Vue版本 【2.5.17】

若是你以为排版难看,请点击 下面连接 或者 拉到 下面关注公众号也能够吧缓存

【Vue原理】Computed - 源码版 函数

今天要记录 computed 的源码,有时候想,理解不就行了吗,为何要记录一遍源码。如今终于想通了学习

过了一段时间以后,你就会忘记你的所谓理解是怎么来的优化

“哎,为何会这么作,关系为何是这样,我c....”this

因而,记录并简化源码,就有助咱们迅速找到根源,解决咱们的疑惑,还能增强咱们的理解lua

好吧spa

嗯,这篇文章很长很详细哦,作好阅读的准备,唔该prototype

咱们重点说明,几个问题的源码实现3d

一、computed 的 月老身份的来源

二、computed 怎么计算

三、computed 的缓存是怎么作的

四、computed 何时初始化

五、computed 是怎么能够直接使用实例访问到的

问题不会按顺序解析,由于这些问题会互相关联,在探索源码的过程当中,你天然会获得答案code

首先,从这个问题开始咱们今天的探索之旅,请看源码


何时初始化

function Vue(){
    ... 其余处理
    initState(this)

    ...解析模板,生成DOM 插入页面

}



function initState(vm) {    

    var opts = vm.$options;    

    if (opts.computed) { 

        initComputed(vm, opts.computed); 

    }

    .....

}

没错,当你调用 Vue 建立实例过程当中,会去处理各类选项,其中包括处理 computed

处理 computed 的方法是 initComputed,下面就呈上 源码


initComputed

function initComputed(vm, computed) {    

    var watchers = vm._computedWatchers = 

            Object.create(null);    

    for (var key in computed) {        

        var userDef = computed[key];        

        var getter = 

            typeof userDef === 'function' ? 

                userDef: userDef.get;      



        // 每一个 computed 都建立一个 watcher

        // watcher 用来存储计算值,判断是否须要从新计算

        watchers[key] = 

        new Watcher(vm, getter, { 

             lazy: true 

        });        

        

        // 判断是否有重名的属性

        if (! (key in vm)) {
            defineComputed(vm, key, userDef);
        }
    }
}

initComputed 这段代码作了几件事

一、每一个 computed 配发 watcher

二、defineComputed 处理

三、收集全部 computed 的 watcher

好的,这三件事,一件一件说哈

一、每一个 computed 配发 watcher

computed 到底和 watcher 有什么猫腻呢?

一、保存 computed 计算函数

二、保存计算结果

三、控制缓存计算结果是否有效

看下 Watcher 源码构造函数

function Watcher(vm, expOrFn, options) {    

    this.dirty = this.lazy = options.lazy;    

    this.getter = expOrFn;    

    this.value = this.lazy ? undefined: this.get();

};

从这段源码中,咱们再看 computed 传了什么参数

new Watcher(vm, getter, { lazy: true })

因而,咱们就知道了上面说的三个猫腻是怎么回事

一、保存设置的 getter。

把用户设置的 computed-getter,存放到 watcher.getter 中,用于后面的计算

二、watcher.value 存放计算结果,可是这里有个条件,由于 lazy 的缘由,不会新建实例并立刻读取值

这里能够算是 Vue 的一个优化,只有你再读取 computed,再开始计算,而不是初始化就开始计算值了

虽然没有一开始计算,可是计算 value 仍是这个 watcher.get 这个方法,来看下源码(已省略部分代码,下面讲其余问题,会更详细展现出来)

这个方法,其实就是执行 保存的 getter 函数,从而获得计算值,灰常简单

Watcher.prototype.get = function() {    

    // getter 就是 watcher 回调

    var value = this.getter.call(vm, vm);    

    return value

};

三、computed 新建 watcher 的时候,传入 lazy

没错,做用是把计算结果缓存起来,而不是每次使用都要从新计算

而这里呢,还把 lazy 赋值给了 dirty,为何呢?

由于 lazy 表示一种固定描述,不可改变,表示这个 watcher 须要缓存

而 dirty 表示缓存是否可用,若是为 true,表示缓存脏了,须要从新计算,不然不用

dirty 默认是 false 的,而 lazy 赋值给 dirty,就是给一个初始值,表示 你控制缓存的任务开始了

因此记住,【dirty】 是真正的控制缓存的关键,而 lazy 只是起到一个开启的做用

具体,怎么控制缓存,下面会说

二、defineComputed 处理

请看源码

function defineComputed(

    target, key, userDef

) {    

    // 设置 set 为默认值,避免 computed 并无设置 set

    var set = function(){}      
    //  若是用户设置了set,就使用用户的set

    if (userDef.set) set = userDef.set   



    Object.defineProperty(target, key, {        

        // 包装get 函数,主要用于判断计算缓存结果是否有效

        get:createComputedGetter(key),        

        set:set

    });
}

源码已经被我简短不少,可是意思是不变的

一、使用 Object.defineProperty 在 实例上computed 属性,因此能够直接访问

二、set 函数默认是空函数,若是用户设置,则使用用户设置

三、createComputedGetter 包装返回 get 函数

重点就在第三点,为何重要?

两大问题都在这里获得解决,【月老牵线问题+缓存控制问题】

立刻呈上 createComputedGetter 源码

function createComputedGetter(key) {    

    return function() {        

        // 获取到相应 key 的 computed-watcher

        var watcher = this._computedWatchers[key];        

        // 若是 computed 依赖的数据变化,dirty 会变成true,

                    从而从新计算,而后更新缓存值 watcher.value

        if (watcher.dirty) {
            watcher.evaluate();

        }        

        // 这里是 月老computed 牵线的重点,让双方创建关系

        if (Dep.target) {
            watcher.depend();

        }        

        return watcher.value

    }
}

一、缓存控制

下面这段代码做用就是缓存控制,请往下看

if (watcher.dirty) {       

    watcher.evaluate()

}

一、watcher.evaluate 用来从新计算,更新缓存值,并重置 dirty 为false,表示缓存已更新

下面是源码

Watcher.prototype.evaluate = function() {    

    this.value = this.get();    

    // 执行完更新函数以后,当即重置标志位

    this.dirty = false;
};

二、只有 dirty 为 true 的时候,才会执行 evaluate

全部说经过 控制 dirty 从而控制缓存,可是怎么控制dirty 呢?

先说一个设定,computed数据A 引用了 data数据B,即A 依赖 B,因此B 会收集到 A 的 watcher

当 B 改变的时候,会通知 A 进行更新,即调用 A-watcher.update,看下源码

Watcher.prototype.update = function() {    

    if (this.lazy)  this.dirty = true;

    ....还有其余无关操做,已被省略

};

当通知 computed 更新的时候,就只是 把 dirty 设置为 true,从而 读取 comptued 时,便会调用 evalute 从新计算

二、月老牵线

月老牵线的意思,在白话版中也说清楚了,这里简单说一下

现有 页面-P,computed- C,data- D

一、P 引用了 C,C 引用了 D

二、理论上 D 改变时, C 就会改变,C 则通知 P 更新。

三、实际上 C 让 D 和 P 创建联系,让 D 改变时直接通知 P

没错,就是下面这段代码搞的鬼

if (Dep.target) {
   watcher.depend();
}

你别看这段代码短啊,涉及的内容真很多啊且须要点脑筋的,看源码分分钟绕不过来,真的服尤大怎么写出来的

来看看 watcher.depend 的源码

Watcher.prototype.depend = function() {    

    var i = this.deps.length;    

    while (i--) {        

        // this.deps[i].depend();

        dep.addSub(Dep.target)
    }
};

这段的做用就是!(依然使用上面的例子 PCD 代号来讲明)

让 D 的依赖收集器收集到 Dep.target,而 Dep.target 当前是什么?

没错,就是 页面 的 watcher!

因此这里,D 就会收集到 页面的 watcher 了,因此就会直接通知 页面 watcher

看累了吗.....

公众号

你会问了,为何 Dep.target 是 页面 watcher?

你好,这里内容就有点多了有点繁杂了,坐好了兄die

先送你一份源码大礼,收好了

Watcher.prototype.get = function() {    
    // 改变 Dep.target

    pushTarget()    

    // getter 就是 watcher 回调

    var value = this.getter.call(this.vm, this.vm);    
    // 恢复前一个 watcher

    popTarget()    

    return value

};



Dep.target = null;

var targetStack = [];

function pushTarget(_target) {    

    // 把上一个 Dep.target 缓存起来,便于后面恢复

    if (Dep.target) {
        targetStack.push(Dep.target);
    }
    Dep.target = _target;

}



function popTarget() {

    Dep.target = targetStack.pop();
}

注解几个词

一、页面 watcher.getter 保存 页面更新函数,computed watcher.getter 保存 计算getter

二、watcher.get 用于执行 watcher.getter 并 设置 Dep.target

三、Dep.target 会有缓存

下面开始 月老牵线的 详细流程

一、页面更新,读取 computed 的时候,Dep.target 会设置为 页面 watcher。

二、computed 被读取,createComputedGetter 包装的函数触发,第一次会进行计算

computed-watcher.evaluted 被调用,进而 computed-watcher.get 被调用,Dep.target 被设置为 computed-watcher,旧值 页面 watcher 被缓存起来。

三、computed 计算会读取 data,此时 data 就收集到 computed-watcher

同时 computed-watcher 也会保存到 data 的依赖收集器 dep(用于下一步)。

computed 计算完毕,释放Dep.target,而且Dep.target 恢复上一个watcher(页面watcher)

四、手动 watcher.depend, 让 data 再收集一次 Dep.target,因而 data 又收集到 恢复了的页面watcher

再额外记一个data改变后续流程

综上,此时 data 的依赖收集器=【computed-watcher,页面watcher】

data 改变,正序遍历通知,computed 先更新,页面再更新,因此,页面才能读取到最新的 computed 值

公众号

公众号

三、收集全部 computed 的 watcher

从源码中,你能够看出为每一个computed 新建watcher 以后,会所有收集到一个对象中,并挂到实例上

为何收集起来,我暂时的想法是

为了在 createComputedGetter 获取到对应的 watcher

其实能够经过传递 watcher ,可是这里的作法是传递 key,而后使用key 去找到对应 watcher

哎哟,个人妈,终于写完了,瞧瞧多少字,7000多,写得我

场外:尼玛大部分是源码好吗

okok,行行行

公众号