Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

vue早期源码学习系列之六:如何实现计算属性 #89

Open
youngwind opened this issue Sep 9, 2016 · 3 comments
Open

vue早期源码学习系列之六:如何实现计算属性 #89

youngwind opened this issue Sep 9, 2016 · 3 comments
Labels

Comments

@youngwind
Copy link
Owner

youngwind commented Sep 9, 2016

前言

vue中有一个非常好用的功能:计算属性(computed)

在模板中绑定表达式是非常便利的,但是它们实际上只用于简单的操作。模板是为了描述视图的结构。在模板中放入太多的逻辑会让模板过重且难以维护。这就是为什么 Vue.js 将绑定表达式限制为一个表达式。如果需要多于一个表达式的逻辑,应当使用计算属性。
你可以像绑定普通属性一样在模板中绑定计算属性。Vue 知道 vm.b 依赖于 vm.a,因此当 vm.a 发生改变时,依赖于 vm.b 的绑定也会更新。

来源:https://vuejs.org.cn/guide/computed.html

问题

我们先来具象化一下问题。

// html
<div id="app">
    <p>姓名:{{user.name}}</p>
    <p>年龄: {{user.age}}</p>
    <p>{{info}}</p>
</div>
// js
const app = new Bue({
    el: '#app',
    data: {
        user: {
            name: 'youngwind',
            age: 24
        }
    },
    computed: {
        info: function () {
            return `计算出来的属性-> 姓名: ${this.user.name}, 年龄: ${this.user.age}`;
        }
    }
});

问题是:如何让info跟着name和age动态改变呢?

我们把这个问题拆解成两个更小的问题,然后逐个击破。

  1. 如何根据name和age计算出info?
  2. 当name或者age改变的时候,重新进行第一步的计算。

静态计算属性

ok,我们先来解决第一个问题。(先把第二个问题放一边)

/**
 * 初始化所有计算属性
 * 主要完成一个功能:将计算属性定义的function当成是该属性的getter函数
 * @private
 */
exports._initComputed = function () {
    // 注意,这里的this指的是bue实例
    let computed = this.$options.computed;
    if (!computed) return;
    for (let key in computed) {
        let def = computed[key];
        if (typeof def === 'function') {
            def = {
                get: def
            };
            def.enumerable = true;
            def.configurable = true;
            Object.defineProperty(this.$data, key, def);
        }
    }
};

关键点说明:

  1. info后面跟的是一个有return值的函数,我们直接把这个函数当成info的getter函数。
  2. 虽然info是计算出来的属性,但是到时候对DOM模板进行遍历的时候也是需要用到它的,所以需要将info定义到$data里面去。

实现效果如下图所示。
bug

从图中我们可以看到,$data里面的info已经有值,并且DOM模板里面的{{info}}也已经正确解析了。
第一个问题解决了,但是,我们同时也看到,当我们改变name和age的时候,info并不会跟着改变!!
下面我们来看看怎么解决这第二个问题。

动态计算属性

动态计算难在什么地方?
难在:当name或者age改变的时候,程序如何知道要改变info?
你可能会说,这不明摆着吗,一眼就看出来。
然而,程序不知道啊!我们拆解一下问题。

  1. 如何让程序知道info依赖于name和age。-> 如何收集计算属性的依赖属性
  2. 当name和age改变的时候,更新info对应的DOM元素

第2个问题好办,因为我们在《如何实现动态数据绑定》 #87 的时候就已经建立起一套完整的Binding、Watcher和Directive的体系。我们只需要把info指令分别push到wathcer name和wathcer age的_subs里面不就可以了,在这儿就不细说了。
我们重点看第一个问题。
解决思路还是从getter入手:定义info的function被当成了getter,那么当我们访问this.$data.info的时候,就会调用这个function。这个function又会去访问this.user.name和this.user.age,这意味着什么呢?
这意味着会去执行name和age的getter函数啊!所以我们可以自定义name和age的getter函数,让它做一些特殊的事情。
那要做什么事情呢?我们触发(notify)一个get事件,然后这个get事件会传播到$data顶层。我们在$data顶层注册一个colletDep(收集依赖)函数,这样我们不就能知道info依赖于user.name和user.age了吗?
嗯,没错,大概思路就是这样。下面展示部分关键代码,完整的代码可以参考这里

function Watcher(vm, expression, cb, ctx) {
    this.id = ++uid;
    this.vm = vm;
    this.expression = expression;
    this.cb = cb;
    this.ctx = ctx || vm;
    this.deps = Object.create(null);

    // 这里的getter可以不去细究,其实就是根据expression(比如user.name)
    // 拼接出它对应的函数,当成getter
    // 你完全可以理解为调用this.getter()方法其实就是为了得到user.name的值
    this.getter = expParser.compileGetter(expression);

    this.initDeps(expression);
}
/**
 * 要注意,这里的getter.call是完成计算属性的核心,
 * 因为正是这里的getter.call, 执行了该计算属性的getter方法,
 * 从而执行该计算属性所依赖的其他属性的get方法
 * 从而发出get事件,冒泡到底层, 触发collectDep事件
 * @param path {String} 指令表达式对应的路径, 例如: "user.name"
 */
Watcher.prototype.initDeps = function (path) {
    this.addDep(path);
    Observer.emitGet = true;
    this.vm._activeWatcher = this;

    // 就是在这儿调用info的getter,进而调用name和age的getter
    // 进入触发和传播get事件
    this.value = this.getter.call(this.vm, this.vm.$data);

    Observer.emitGet = false;
    this.vm._activeWatcher = null;
};
/**
 * 收集依赖。
 * 为什么需要这个东西呢?
 * 因为在实现computed计算属性功能的过程中,
 * 发现程序需要知晓计算出来的属性到底依赖于哪些原先就有的属性
 * 这样才能做到在对应原有的属性的_subs数组中添加新属性指令的watcher事件
 * @param path {String} get事件传播到顶层时的路径,比如"user.name"
 * @private
 */
exports._collectDep = function (event, path) {
    let watcher = this._activeWatcher;
    if (watcher) {
        watcher.addDep(path);
    }
};

// 看,就是在这儿给$data顶层注册收集依赖的事件的
this.observer.on('set', this._updateBindingAt.bind(this))
        .on('get', this._collectDep.bind(this));

有两个小细节务必要注意:

  1. Observer构造函数有一个emitGet属性,默认为false。当emitGet为true时,代表调用属性的getter会触发并且传播get事件,当emitGet为false时,则不会触发。只有在初始化构造Binding才将它开启,因为不能任意时候调用属性getter都触发这个get事件,这个get事件只是我们在收集依赖才需要的。
  2. Watcher实例有一个dep字段,它的作用是为了避免重复收集依赖。如果没有他,那么在当前例子的情况下,user的bingding._subs里面会出现两个user的watcher,name亦然。原因是我们在计算属性的时候再次读取了user和name。为了避免这种情况,需要引入dep,记录下:“哦,原来程序刚刚已经分析过这里了,忽略就好。”

最后构造出来的_rootBinding数据结构如下图所示。
data

具体的实现效果如下图所示,完整的代码参考这里
demo

---EOF---

@ErosZy
Copy link

ErosZy commented Oct 16, 2016

说得太复杂了,没有讲到关键点,其实计算属性就是defineproperty的一个延伸,如果我自己来写我会这样http://www.codesnippet.cn/detail/0510201615088.html

@ErosZy
Copy link

ErosZy commented Oct 16, 2016

其实什么watcher,binding都是对defineproperty自然的抽象而已

@aaawhz
Copy link

aaawhz commented Oct 12, 2017

额, 直接读源码都没那么晕。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants