跳到主要内容

源码简单分析

Vue 源码分析

基本实现

深入理解 Vue 完整版和 runtime 版

一步一步实现一个 VUe

核心功能:响应式的数据绑定、虚拟 DOM、diff 算法、patch 方法(用于更新真实 DOM)

当 new Vue() 的时候发生了什么?

我们的实现会参考源码的套路,但会大量的简化其中的细节。为了理解源码的结构,最好的突破口就是了解程序的起点 new Vue() 的背后究竟发生了什么。

简单梳理下源码的执行流:

=> 初始化生命周期

=> 初始化事件系统

=> 初始化 state,依次处理 props、data、computed …

=> 开始渲染 _mount() => _render() 返回 vdom=> _update() => patch() 更新真实 DOM

更详细的说明可以参考这篇文章,我们只会实现其中最核心的部分

第一步:将虚拟 DOM 树渲染到真实的 DOM

每一个 DOM 节点都是一个 node 对象,这个对象含有大量的属性与方法,虚拟 DOM 其实就是超轻量版的 node 对象。

img

我们要生成的 DOM 树看上去是这样的:

img

关于 data 参数的属性,请参考官方文档

随后我们会通过 createElm 方法和 createChildren 方法的相互调用,遍历整棵虚拟节点树,生成真实的 DOM 节点树,最后替换到挂载点。

完整代码

第二步:修改数据,执行 diff 算法,并将变化的部分 patch 到真实 DOM

img

diff 算法的逻辑比较复杂,可以单独摘出来研究,由于我们的目的是理解框架的核心逻辑,因此代码实现里只考虑了最简单的情形。

完整代码

第三步:对数据做响应式处理,当数据变化时,自动执行更新方法

img

data 中的每一个属性都会被处理为存取器属性,同时每一个属性都会在闭包中维护一个属于自己的 dep 对象,用于存放该属性的依赖项。当属性被赋予新的值时,就会触发 set 方法,并通知所有依赖项进行更新。

完整代码

初始化、更新流程分析

vue 更新流程

<div id="demo">
<child :list="list"></child>
<button @click="handleAdd">add</button>
</div>
<script>
Vue.component('child', {
props: {
list: {
type: Array,
default: () => []
}
},
template: '<p>{{ list }}</p>'
})

new Vue({
el: "#demo",
data() {
return {
list: [1,2]
}
},
methods: {
handleAdd() {
this.list.push(Math.random())
}
}
})
</script>

很简单的例子,一个父组件一个子组件,子组件接受一个 list,父组件有个按钮,可以往 list 里 push 数据改变 list

初始化流程:

  1. 首先从 new Vue({el: "#app"}) 开始,会执行 _init 方法。

    function Vue(options) {
    // 省略...
    this._init(options);
    }
  2. _init 方法的最后执行了 vm.$mount 挂载实例。

    Vue.prototype._init = function (options) {
    var vm = this;
    // 省略...
    if (vm.$options.el) {
    vm.$mount(vm.$options.el);
    }
    };
  3. 如果此时运行的版本是 runtime with compiler 版本,这个版本的 $mount 会被进行重写,增加了把 template 模板转成 render 渲染函数的操作,但最终都会走到 mountComponent 方法。

    Vue.prototype.$mount = function (el, hydrating) {
    el = el && inBrowser ? query(el) : undefined;
    return mountComponent(this, el, hydrating);
    };

    var mount = Vue.prototype.$mount; //缓存上一次的Vue.prototype.$mount

    Vue.prototype.$mount = function (el, hydrating) {
    //重写Vue.prototype.$mount
    // 省略,将template转化为render渲染函数
    return mount.call(this, el, hydrating);
    };
  4. mountComponent 里触发了 beforeMountmounted 生命周期,更重要的是创建了 Watcher,传入的 updateComponent 就是 Watcher 的 getter

    function mountComponent(vm, el, hydrating) {
    // 执行生命周期函数 beforeMount
    callHook(vm, "beforeMount");

    var updateComponent;
    //如果开发环境
    if ("development" !== "production" && config.performance && mark) {
    // 省略...
    } else {
    updateComponent = function () {
    vm._update(
    vm._render(), // 先执行_render,返回vnode
    hydrating
    );
    };
    }

    new Watcher(
    vm,
    updateComponent,
    noop,
    null,
    true // 是否渲染过得观察者
    );

    if (vm.$vnode == null) {
    vm._isMounted = true;
    // 执行生命周期函数mounted
    callHook(vm, "mounted");
    }
    return vm;
    }
  5. 在创建 Watcher 时会触发 get() 方法,pushTarget(this)Dep.target 设置为当前 Watcher 实例。

    function Watcher(vm, expOrFn, cb, options, isRenderWatcher) {
    if (typeof expOrFn === "function") {
    this.getter = expOrFn;
    }
    this.value = this.lazy // 这个有是组件才为真
    ? undefined
    : this.get(); //计算getter,并重新收集依赖项。 获取值
    }

    Watcher.prototype.get = function get() {
    pushTarget(this);
    var value;
    var vm = this.vm;
    try {
    value = this.getter.call(vm, vm);
    } catch (e) {
    } finally {
    popTarget();
    }
    return value;
    };
  6. Watcherget() 里会去读取数据,触发 initData 时使用 Object.defineProperty 为数据设置的 get,在这里进行依赖收集。我们知道 Vue 中每个响应式属性都有一个 __ob__ 属性,存放的是一个 Observe 实例,这里的 childOb 就是这个 __ob__,通过 childOb.dep.depend() 往这个属性的__ob__中的 dep 里收集依赖,如下图。 WX20220315-161349@2x.png

    export function defineReactive(
    obj: Object,
    key: string,
    val: any,
    customSetter?: Function
    ) {
    /*在闭包中定义一个dep对象*/
    const dep = new Dep();

    let childOb = observe(val);
    Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
    /*如果原本对象拥有getter方法则执行*/
    const value = getter ? getter.call(obj) : val;
    if (Dep.target) {
    /*进行依赖收集*/
    dep.depend();
    if (childOb) {
    childOb.dep.depend();
    }
    if (Array.isArray(value)) {
    dependArray(value);
    }
    }
    return value;
    },
    set: function reactiveSetter(newVal) {},
    });
    }
  7. 在我们的例子中,这个 list 会收集两次依赖,所以它 __ob__ 的 subs 里会有 两个Watcher,第一次是在父组件 data 中的 list,第二次是在创建组件时调用 createComponent ,然后又会走到 _init => initState => initProps ,在 initProps 内对 props 传入的属性进行依赖收集。有两个 Watcher 就说明 list 改变时要通知两个地方,这很好理解。 .

  8. 最后,触发 getter,上面说过 getter 就是 updateComponent,里面执行 _update 更新视图。

下面来说说更新的流程:

  1. 点击按钮往数组中添加一个数字,在 Vue 中,为了监听数组变化,对数组的常用方法做了重写,所以先会走到 ob.dep.notify() 这里,ob 就是 list 的 __ob__ 属性,上面保存着 Observe 实例,里面的 dep 中有两个 Watcher,调用 notify 去通知所有 Watcher 对象更新视图。

    ["push", "pop", "shift", "unshift", "splice", "sort", "reverse"].forEach(
    function (method) {
    const original = arrayProto[method];
    def(arrayMethods, method, function mutator() {
    let i = arguments.length;
    const args = new Array(i);
    while (i--) {
    args[i] = arguments[i];
    }
    /*调用原生的数组方法*/
    const result = original.apply(this, args);

    const ob = this.__ob__;
    let inserted;
    switch (method) {
    case "push":
    inserted = args;
    break;
    case "unshift":
    inserted = args;
    break;
    case "splice":
    inserted = args.slice(2);
    break;
    }
    if (inserted) ob.observeArray(inserted);

    /*dep通知所有注册的观察者进行响应式处理*/
    ob.dep.notify();
    return result;
    });
    }
    );
  2. notify 方法里去通知所有 Watcher 更新,执行 Watcherupdate 方法,update 里的 queueWatcher 过滤了一些重复的 Watcher, 但最终会走到 Watcherrun() 方法。

    Dep.prototype.notify = function notify() {
    var subs = this.subs.slice();
    for (var i = 0, l = subs.length; i < l; i++) {
    subs[i].update();
    }
    };

    Watcher.prototype.update = function update() {
    if (this.lazy) {
    this.dirty = true;
    } else if (this.sync) {
    this.run();
    } else {
    queueWatcher(this);
    }
    };
  3. run 方法里会调用 get(), get 方法里回去触发 Watcher 的 getter,上面说过,getter 就是 updateComponent

    Watcher.prototype.run = function run() {
    if (this.active) {
    /* get操作在获取value本身也会执行getter从而调用update更新视图 */
    const value = this.get();
    }
    };

    updateComponent = function () {
    vm._update(vm._render(), hydrating);
    };
  4. 最后在 _update 方法中,进行 patch 操作