Vue 源码学习(一)

vue 无疑是一个很是棒的前端MVVM库,怀着好奇的心情开始看VUE源码,固然遇到了不少的疑问,也查了不少的资料看了一些文章。可是这些资料不少都忽略了很重要的部分或者是一些重要的细节,亦或是一些很重要的部分没有指出,特别是在computed的实现上。因此才打算写这篇文章,记录一下本身的学习过程,固然也但愿能给其余想了解VUE源码的童鞋一点参考。若是笔者在某些地方理解有误,也欢迎批评指正出来,一块儿学习。html

为了加深理解,我按着源码的思路造了一个简易的轮子,基本核心的实现是与VUE源代码一致。测试 demo。仓库的地址:eltonchan/rollup-ts前端

VUE的源码采用rollup flow至于为何不采用typescript,主要考虑工程上成本和收益的考量, 这一点尤大在知乎也有说过。(3.0+版本肯定改用typesript)vue

Vue 2.0 为何选用 Flow 进行静态代码检查而不是直接使用TypeScriptnode

不懂rollup 与typescript 也不要紧,本项目已经配置好了, 只须要先执行npm i (或者cnpm i)安装相应依赖,而后 npm start 启动就能够。 npm run build 构建,默认是输出umd格式,若是须要cmd或者amd 能够在rollup.config.js配置文件修改。git

output: {
        file: 'dist/bundle.js',
        format: 'umd',
        name: 'myBundle',
        sourcemap: true
    }
复制代码

questions ? 带着问题去了解一个事物每每能带来更好的收益,那咱们就从下面几个问题开始github

  • 如何对http://this.xxx的访问代理到http://this._data.xxx 上 ?
  • 如何实现数据劫持,监听数据的读写操做 ?
  • 如何实现依赖缓存 ?
  • template 改变的时候 如何清理依赖项集合? eg: v-if 、组件销毁
  • 如何实现数据修改 dom更新 ?

vue实现双向绑定原理,主要是利用Object.defineProperty getter/setter(事实上,大多数响应式编程的库都是利用这个实现的,好比很是棒的mobx.js)和发布订阅模式(定义了对象间的一对多的依赖关系,当一个对象的状态发生改变时,全部依赖于它的对象都将得到通知),而在vue中,watcher 就是订阅者,而一对多的依赖关系 就是指data上的属性与watcher,而data上的属性如何与watcher 关联起来, dep 就是桥梁, 因此搞懂 dep, watcher, observe三者的关系,天然就搞懂了vue实现双向绑定的原理了。typescript

总体的流程图


1、 Proxy 回到第一个问题, 答案实际上是:对于每个 data 上的key,都在 vm 上作一个代理,实际操做的是 this._data、 实现的代码以下:express

export function proxy (target: IVue, sourceKey: string, key: string) {
    Object.defineProperty(target, key, {
        enumerable: true,
        configurable: true,
        get() {
            return this[sourceKey][key];
        },

        set(val: any) {
            this[sourceKey][key] = val;
        }
    });
}
复制代码

能够看出获取和修改this.xx 都是在获取或者修改this.data.xx;npm


2、Observer 用于把data上的属性封装成可观察的属性 用Object.defineProperty来拦截对象的读写gettet/setter操做, 在获取的时候收集依赖, 在修改的时候通知相关的依赖。编程

walk(data): void {
        if (!data || typeof data !== 'object') return;
        Object.keys(data).forEach(key => {
            this.defineReactive({
                data,
                key, 
                value: data[key]
            });
        });
    }

    defineReactive({ data, key, value }: IReactive): void {
        const dep = new Dep();
        this.walk(value);
        const self = this;
        Object.defineProperty(data, key, {
            enumerable: true,
            configurable: true,
            get() {
                if (Dep.target) {
                    Dep.target.addDep(dep);
                }
                return value;
            },
    
            set(newVal: any): void {
                if (value === newVal) return;
                self.walk(value);
                value = newVal;
                dep.notify();
            }
    
        });
    }
复制代码

能够看出, 在get的时候收集依赖,而Dep.target 其实就是watcher, 等下讲到watcher的时候再回过来, 这里要关注dep 其实dep在这里是一个闭包环境,在执行get 或者set的时候 还能够访问到建立的dep. 好比 this.name当在获取this.name的值的时候 会建立一个Dep的实例, 把watcher 添加到这个dep中。

为何对象上新增属性不会监听,而修改整个对象为何能检测到子属性的变化 ?

因为 JavaScript 的限制,Vue 不能检测对象属性的添加或删除(固然mobx也不例外的)。因此可观察的对象属性的添加或者删除没法触发set 方法,而直接修改对象则能够,而在set 方法中则会判断新值是不是对象数组类型,若是是 则子属性封装成可观察的属性,这也是set中self.walk(value);的做用。


3、Watcher 刚才提到了watcher,从上图中也能够看到了watcher的做用 事实上,每个computed属性和watch 都会new 一个 Watcher。接下来会讲到。先来看watcher的实现。

constructor(
        vm: IVue,
        expression: Function | string,
        cb: Function,
    ) {
        this.vm = vm;
        vm._watchers.push(this);

        this.cb = cb || noop;

        this.id = ++uid;

        // 处理watch 的状况
        if (typeof expression === 'function') {
            this.getter = expression;
        } else {
            this.getter = () => vm[expression];
        }

        this.expression = expression.toString();

        this.depIds = new Set();
        this.newDepIds = new Set();
        this.deps = [];
        this.newDeps = [];

        this.value = this.get();
    }
复制代码

这里的expression,对于初始化用来渲染视图的watcher来讲,就是render方法,对于computed来讲就是表达式,对于watch才是key,因此这边须要判断是字符串仍是函数,而getter方法是用来取value的。这边有个depIds,deps,可是又有个newDepIds,newDeps,为何这样设计,接下去再讲,先看this.value = this.get();能够看出在这里给watcher的value赋值,再来看get方法。

get() :void {
        Dep.target = this;
        const value = this.getter.call(this.vm); // 执行一次get 收集依赖
        Dep.target = null;
        this.cleanupDeps(); // 清除依赖
        return value;
    }
复制代码

能够看到getter是用来取值的,当执行这一行代码的时候,以render的那个watcher为例,会执行VNode render 当遇到{{ msg }}的表达式的时候会取值,这个时候会触发msg的get方法,而此时的Dep.target 就是这个watcher, 因此咱们会把这个render的watcher和msg这个属性关联起,也就是msg的dep已经有render的这个watcher了。这个就是Watcher,Dep,Observer的关系。咱们再来看Dep:

export default class Dep implements IDep {
    static target: any = null;
    subs:any = [];
    id;

    constructor () {
        this.id = uid++;
        this.subs = [];
    }

    addSub(sub: IWatcher): void {
        if (this.subs.find(o => o.id === sub.id)) return;
        this.subs.push(sub);
    }

    removeSub (sub: IWatcher) {
        const idx = this.subs.findIndex(o => o.id === sub.id);
        if (idx >= 0) this.subs.splice(idx, 1);
    }

    notify():void {
        this.subs.forEach((sub: Isub) => {
            sub.update();
        })
    }

    depend () {
        if (Dep.target) {
            Dep.target.addDep(this);
        }
    }
}
复制代码

Dep的实现很简单,这边看notify的方法,咱们知道在修改data上的属性的时候回触发set,而后触发notify方法,而后咱们知道sub就是watcher,因此watcher.update方法就是修改属性所执行的方法,回到watcher看这个update的实现。

update() {
        // 推送到观察者队列中,下一个tick时调用。*/
        queueWatcher(this);
    }

    run(cb) {
        const value = this.get();
        if (value !== this.value) {
            const oldValue = this.value;
            this.value = value;
            cb.call(this.vm, value, oldValue);
        }
    }
复制代码

update方法并无直接render vNode。而是把watcher推到一个队列中,事实上vue是的更新dom是异步的,为何要异步更新队列,这边摘抄了一下官网的描述:Vue 异步执行 DOM 更新。只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的全部数据改变。若是同一个 watcher 被屡次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免没必要要的计算和 DOM 操做上很是重要。而后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工做。Vue 在内部尝试对异步队列使用原生的 Promise.then 和 MessageChannel,若是执行环境不支持,会采用 setTimeout(fn, 0) 代替。其实这是一种很是好的性能优化方案,咱们设想一下若是在mounted中循环赋值,若是不采用异步更新策略,每个赋值都更新,彻底是一种浪费。

4、nextTick 关于nextTick其实不少文章写的都不错,这边就不详细介绍了。涉及到的概念能够点击下面连接查看:

JavaScript 运行机制详解:再谈Event Loop

关于 macrotask 和 microtask


5、Computed 计算属性是基于它们的依赖进行缓存的。只在相关依赖发生改变时它们才会从新求值 回到开始的那个问题,如何实现依赖缓存? name的更新如何让info也更新,若是name不变,info如何取值?

刚才在讲watcher的时候,提到过每一个computed会实例化一个Watcher,从下面代码中也能够看出来,每个computed属性都有一个订阅者watcher。

initComputed(computed) {
        if (!computed || typeof computed !== 'object') return;
        const keys = Object.keys(computed);
        const watchers = this._computedWatchers;
        let i = keys.length;
        while(i--) {
            const key = keys[i];
            const func = computed[key];
            watchers[key] = new Watcher(
                this,
                func || noop,
                noop,
            );

            defineComputed(this, key);
        }
    }
复制代码

看这个例子:

computed: {
        info() {
            console.info('computed update');
            return this.name + 'hello';
        }
    },
复制代码

watcher 的getter方法就是computed属性的表达式,而在执行this.value = this.get();这个value就会是表达式的运行结果,因此其实Vue是把info的值存储在它的watcher的value里面的,而后又知道在取name的值的时候,会触发name的get方法,此时的Dep.target 就是这个info的watcher,而dep是一个闭包,仍是以前收集name的那个dep, 因此name的dep就会有两个watcher,[renderWatcher, computedWatcher], 当name更新的时候,这两个订阅者watcher都会收到通知,这也就是name的更新让info也更新。

那info的值是watcher的value, 因此这边要作一个代理,把computed属性的取值代理到对应watcher的value,实现起来也很简单。

export default function defineComputed(vm: IVue, key: string) {
    Object.defineProperty(vm, key, {
        enumerable: true,
        configurable: true,
        get() {
            const watcher = vm._computedWatchers && vm._computedWatchers[key];
            return watcher.value;
        },
    });
}
复制代码

6、依赖更新

<p v-if="switch">{{ name }}</p>
复制代码

假设switch由true切换成false时候,是须要把name上面的renderWatcher删除掉的,因此须要用depIds和deps的属性来记录dep。

addDep(dep: Dep) {
        const id = dep.id;
        if (!this.newDepIds.has(id)) {
            this.newDepIds.add(id);
            this.newDeps.push(dep);
            if (!this.depIds.has(id)) {
                dep.addSub(this);
            }
        }
    }

    cleanupDeps() {
        let i = this.deps.length;
        while (i--) {
            const dep = this.deps[i];
            if (!this.depIds.has(dep.id)) {
                dep.removeSub(this);
            }
        }

        const tmp = this.depIds;
        this.depIds = this.newDepIds;
        this.newDepIds = tmp;
        this.newDepIds.clear();

        const deps = this.deps;
        this.deps = this.newDeps;
        this.newDeps = deps;
        this.newDeps.length = 0;
    }
复制代码

这里把newDepIds赋值给了depIds, 而后newDepIds再清空,deps也是这样的操做,这是一种效率很高的操做,避免使用了深拷贝。添加依赖的时候都是用newDepIds,newDeps来记录,删除的时候会去deps里面遍历查找,等删除了再把newDepIds赋值给depIds,这样能保证在更新依赖的时候,没有使用的依赖会从这个watcher中移除。

7、watch 为何watch 一个对象的时候 oldValue == value ?

watch的属性也是一个实例化的Watcher,只是这个时候的expression是key,value 是vm[key],而cb就是回调函数,因此这个时候对应属性的dep中天然就有这个watcher。

initWatch(watch) {
        if (!watch || typeof watch !== 'object') return;
        const keys = Object.keys(watch);
        let i = keys.length;

        while(i--) {
            const key = keys[i];
            const cb = watch[key];
            new Watcher(this, key, cb);
        }
    }
复制代码

当属性更新的时候,会执行到这个run方法, 当watch一个对象的时候,watcher的value实际上是一个引用,修改这个属性的时候,this.value也同步修改了,因此也就是为何oldValue == value了, 至于做者为何这么设计,我想确定是有他缘由的。

run(cb) {
        const value = this.get();
        if (value !== this.value) {
            const oldValue = this.value;
            this.value = value;
            cb.call(this.vm, value, oldValue);
        }
    }
复制代码

8、Compile vue 2+ 已经使用VNode了,这部分尚未细致研究过,因此我这边本身写了个简易的Compile,这部分已经和源码没有关系了。主要用到了DocumentFragment和闭包而已,有兴趣的童鞋能够到这个仓库查看。

components vnode 待补充...

相关文章
相关标签/搜索