头部背景图片
吉水于人的笔记 |
吉水于人的笔记 |

vue内部运行机制源码分析——初始化和挂载

vue全局运行机制

vue的内部流程分为5个步骤:

  1. 进行初始化和挂载:init及mount
  2. 模版编译:compile,编译成渲染函数render function
  3. 进行响应式依赖收集:render function => getter、setter => watcher 进行 update => patch 的过程以及使用队列异步更新的策略
  4. 依赖收集的同时产生Virtal DOM ,render function 被转为VNode节点
  5. 通过diff算法进行patch更新视图

博客拆分几篇文章进行归纳,本章讲解一下初始化和挂载。

初始化和挂载

new Vue()之后,vue 调用 _init函数进行初始化,它会初始化:生命周期、事件、props、methods、data、computed和watch等。

初始化之后调用$mount挂载组件,如果运行时编译,不存在render function 但是存在template的情况,会进行编译。

其中最重要的是会通过Object.defineProperty设置getter和setter函数,用来实现响应式依赖收集

源码解析

响应式系统基本原则

vue.js是MMVVM框架,数据结果是JavaScript对象,但是它能通过对这些对象进行操作时,影响对应对视图,其核心实现就是响应式系统
(1)Object.defineProperty
讲到响应式系统,不可避免要说到Object.defineProperty,Vue是基于它实现响应式系统的。


/*
    obj: 目标对象
    prop: 需要操作的目标对象的属性名
    descriptor: 描述符
    return value 传入对象
*/
Object.defineProperty(obj, prop, descriptor)

descriptor具有一些属性:

  • enumerable,属性是否可以枚举,默认false
  • configurable,属性是否可以被修改或删除,默认false
  • get,获取属性的方法
  • set,设置属性的方法

(2)observer(可观察的)

我们知道在init的阶段会进行初始化,从而对数据进行响应化,我们定义一个defineReactive方法,该方法通过Object.defineProperty来实现对对象的响应化,入参是一个obj(需要绑定的对象)key(obj的某个属性)val(具体的值)。经过defineReactive方法处理后,obj的属性key在「读」的时候会触发reactiveGetter方法,而属性在「写」的时候会触发reactiveSetter方法。

function defineReactive (obj, key, val) {
    Object.defineProperty(obj, key, {
        enumerable: true,       /* 属性可枚举 */
        configurable: true,     /* 属性可被修改或删除 */
        get: function reactiveGetter () {
            return val;         /* 实际上会依赖收集,下一小节会讲 */
        },
        set: function reactiveSetter (newVal) {
            if (newVal === val) return;
            cb(newVal);
        }
    });
}

// 更新视图
function cb (val) {
    /* 渲染视图 */
    console.log("视图更新啦~");
}

当然,这只是对一个对象进行该操作,我们需要通过递归的方式对所有的对象进行defineReactive处理。下面的方法,使用循环的方式来实现一个observer的逻辑。

function observer (value) {
    if (!value || (typeof value !== 'object')) {
        return;
    }

    Object.keys(value).forEach((key) => {
        defineReactive(value, key, value[key]);
    });
}

响应式的一个大致的逻辑已经介绍完成,最后,我们来将observer封装为一个Vue。在Vue的构造函数中,对optionsdata进行处理,这里的data就是我们平时在vue组件中写的data属性(实际上是一个函数,这里当作一个对象来简单处理)。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <script>
    function observer (value) {
        if (!value || (typeof value !== 'object')) {
            return;
        }

        Object.keys(value).forEach((key) => {
            defineReactive(value, key, value[key]);
        });
    }

    function cb (val) {
        console.log("视图更新啦~", val);
    }

    function defineReactive (obj, key, val) {
        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: true,
            get: function reactiveGetter () {
                return val;         
            },
            set: function reactiveSetter (newVal) {
                if (newVal === val) return;
                val = newVal;
                cb(newVal);
            }
        });
    }

    class Vue {
        constructor(options) {
            this._data = options.data;
            observer(this._data);
        }
    }

    let o = new Vue({
        data: {
            test: "I am test."
        }
    });
    o._data.test = "hello,test.";
    </script>
</body>
</html>

依赖收集追踪原理

(1)为什么要依赖收集?
举个🌰
我们现在有这样一个对象:

new Vue({
    template: 
        `<div>
            <span>{{text1}}</span> 
            <span>{{text2}}</span> 
        <div>`,
    data: {
        text1: 'text1',
        text2: 'text2',
        text3: 'text3'
    }
});

我们修改text3的数据

this.text3 = 'modify text3';

但是因为视图中不需要使用到text3,这时,我们不可能去通知上述所讲的cb函数去进行视图的修改。

再举个🌰

这时我们拥有一个全局对象,有多个地方使用到该全局对象。

let globalObj = {
    text1: 'text1'
};

let o1 = new Vue({
    template:
        `<div>
            <span>{{text1}}</span> 
        <div>`,
    data: globalObj
});

let o2 = new Vue({
    template:
        `<div>
            <span>{{text1}}</span> 
        <div>`,
    data: globalObj
});

如果这时候我们修改gloablObj的text1的内容,就需要同时通知o1和o2两个实例进行视图的更新。这就是我们为什么需要进行依赖收集的原因。

(2)订阅者
我们实现一个订阅者Dep,用来存放watcher观察者对象。

class Dep {
    constructor () {
        /* 用来存放Watcher对象的数组 */
        this.subs = [];
    }

    /* 在subs中添加一个Watcher对象 */
    addSub (sub) {
        this.subs.push(sub);
    }

    /* 通知所有Watcher对象更新视图 */
    notify () {
        this.subs.forEach((sub) => {
            sub.update();
        })
    }
}

(3)观察者watcher

class Watcher {
    constructor () {
        /* 在new一个Watcher对象时将该对象赋值给Dep.target,在get中会用到 */
        Dep.target = this;
    }

    /* 更新视图的方法 */
    update () {
        console.log("视图更新啦~");
    }
}

Dep.target = null;

(4)依赖收集
我们把上面的defineReactive方法修改,增加一个Dep类对象,用来收集watcher对象。在对象被读的时候,触发reactiveGetter函数,把当前的watcher对象收集到Dep类中,之后当对象被写时触发reactiveSetter方法,通知Dep类调用notify来触发Watcher对象的update方法更新对应的视图。

function defineReactive (obj, key, val) {
    /* 一个Dep类对象 */
    const dep = new Dep();

    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter () {
            /* 将Dep.target(即当前的Watcher对象存入dep的subs中) */
            dep.addSub(Dep.target);
            return val;         
        },
        set: function reactiveSetter (newVal) {
            if (newVal === val) return;
            /* 在set的时候触发dep的notify来通知所有的Watcher对象更新视图 */
            dep.notify();
        }
    });
}

class Vue {
    constructor(options) {
        this._data = options.data;
        observer(this._data);
        /* 新建一个Watcher观察者对象,这时候Dep.target会指向这个Watcher对象 */
        new Watcher();
        /* 在这里模拟render的过程,为了触发test属性的get函数 */
        console.log('render~', this._data.test);
    }
}

小结

到这里,响应式系统和依赖收集就讲完了,现在进行总结一下。

  • 首先在observer的过程中会注册get方法,该方法用来进行「依赖收集」。
  • 在它的闭包中会有一个Dep对象,用这个对象来存放watcher对象,依赖收集的过程就是将watcher实例存放到对应的Dep对象中去的。
  • get方法让当前的watcher对象存放到它的subs(addSub)中。
  • 在数据更新时,set会调用Dep对象的notify方法,通知它内部的所有watcher对象进行视图更新。