Vue2.6x源码解析(五):Vue应用加载流程【多图预警!推荐收藏】

341 阅读5分钟

系列文章:

前面的章节都是针对某一部分代码的逻辑解析,最后我们连贯起来过一遍Vue应用的完整加载流程。

一,应用案例

创建一个简单的应用:因为我们重点分析Vue应用内部的加载过程,所以案例尽量简洁即可。

// main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')
// App.vue
<template>
    <div>
        <div>我是根组件APP</div>
        <index></index>
    </div>
</template><script>
import index from "./views/index.vue";
export default {
    components: { index }
};
</script>
// index.vue
<template>
    <div>
        <div>我是index首页</div>
        <div>我是name:{{ name }}</div>
        <button @click="handleClick">修改data数据</button>
    </div>
</template>
<script>
export default {
    data() {
        return {
            name: "tom",
            obj: {
                age: 20
            }
        };
    },
    created() {
        console.log('created')
    },
    methods: {
        handleClick() {
            this.name = "zhangsan";
            this.obj.age = 18;
            console.log(this.myName);
        },
    },
    computed: {
        myName() {
            return this.name + '123'
        }
    },
    watch: {
        obj: {
            handler(val) {
                console.log("watch的回调", val);
            },
            deep: true,
            immediate: true
        }
    }
};
</script>

二,应用加载

1,Vue实例【根实例】

new Vue()

执行this._init开始Vue应用的初始化:

image-20230412092945094.png

image-20230412093036049.png

image-20230412093202776.png

image-20230412092845114.png

因为我们在项目中创建Vue时,一般不会传递常规的选项参数,所以init下面的内容没有可初始化的,直接进入$mount方法。

Vue.$mount

进入$mount开始应用的加载:

image-20230412101739264.png

Vue项目的容器元素就是我们的#app

mountComponent

进入组件加载方法mountComponent,开始加载根组件【在Vue2中,Vue实例也是组件,根实例就是根组件】。

1.png

2.png

3.png

4.png

new Watcher()

进入watcher创建过程:

5.png

6.png

这里要注意:在创建watcher时,就会默认执行一次getter方法【计算属性的watcher除外】。

7.png

开始渲染Vue根实例即Vue应用。

updateComponent

进入updateComponent方法:

8.png

vm._render

首先进入vm._render方法:

9.png

10.png

11.png

vm._update

进入vm._update方法:

1.png

patch

vnode传递给patch函数:

2.png

3.png

4.png

createElm

进入createElm方法:

5.png

createComponent

进入createComponent方法:

6.png

这里的init方法是虚拟dom的init方法,不是组件初始化的,不要搞混了。

vnode init

7.png

createComponentInstanceForVnode

根据vnode对象创建组件实例:

8.png

2,App组件

new VueComponent()

创建组件实例,执行thsi.init开始组件的初始化:

1.png

2.png

3.png

4.png

5.png

回到vnode init,开始App组件的加载:

6.png

vm.$mount(*)

调用$mount,开始App组件的加载【开始Vue应用的递归渲染过程】:

7.png

mountComponent

进入组件加载方法mountComponent,开始加载App组件:

8.png

9.png

new Watcher()

和前面的逻辑一样,创建watcher实例,并且默认调用一次getter函数:

1.png

updateComponent

进入updateComponent方法:

2.png

vm._render

首先进入vm._render方法:

3.png

vm._update

进入vm._update方法:

4.png

patch

进入patch函数:

5.png

createElm

进入createElm方法:

6.png

根据tag属性的值,进行不同的加载。如果没有tag属性,则为注释或者文本节点。

7.png

开始创建子节点:

8.png

createChildren

进入createChildren方法:循环children,分别调用createElm方法创建子节点。

1.png

第一个child是文本节点:

2.png

注意: 这里的创建完成的文本节点会插入到app组件的根节点一个div

第二个child是组件:

3.png

createComponent

进入createComponent方法:

4.png

vnode init

5.png

3,Index组件

new VueComponent()

1.png

开始index组件的初始化:

2.png

3.png

4.png

initMethods

方法挂载:

5.png

6.png

initData

生成响应式数据data:

7.png

8.png

9.png

initComputed

初始化计算属性:

1.png

2.png

3.png

initWatch

初始化watch选项:

4.png

5.png

6.png

控制台会打印出created字符。

7.png

后面的$mount的过程和前面的一样,基本上就是这样递归渲染了【跳过】。

4,真实dom挂载

最后要提及一下真实的dom元素是怎么加载到页面的。

index组件的内容加载完成后:

1.png

回到createComponent方法内:

2.png

initComponent

首先进入initComponent方法:

3.png

我们查看当前Index组件的真实dom内容:

4.png

// index组件真实dom内容
<div>
    <div>我是首页</div>
    <div>我是name:tom</div>
    <button>修改data数据</button>
</div>

5.png

这里的parenElm就是app组件的根元素div

我们这里可以记录一下三个组件的vnode.elm内容:

// Vue根组件
<div id="app">
    
</div>
// app组件
<div>
    <div>我是根组件APP</div>
</div>
// index组件真实dom内容
<div>
    <div>我是首页</div>
    <div>我是name:tom</div>
    <button>修改data数据</button>
</div>

index组件执行insert方法后:

// app组件更新
<div>
    <div>我是根组件APP</div>
    <div>
        <div>我是首页</div>
        <div>我是name:tom</div>
        <button>修改data数据</button>
    </div>
</div>

查看app组件验证:

6.png

组件内部内容渲染完成,代表app组件加载完成:

7.png

然后将app组件内容插入到Vue根实例:

// Vue根组件
<div id="app">
    <div>
        <div>我是根组件APP</div>
        <div>
            <div>我是首页</div>
            <div>我是name:tom</div>
            <button>修改data数据</button>
        </div>
    </div>
</div>

8.png

页面渲染完成,应用加载完成。

三,应用更新

我们来点击按钮,修改data数据,触发组件的更新。

1.png

点击按钮,进入断点:

2.png

1,修改name

首先触发响应式数据namesetter,进入修改逻辑:

proxySetter

进入代理的setter

3.png

name[setter]

进入真正响应式数据的setter

4.png

5.png

Dep.notify()

响应式数据name只在template模板中使用了,所以收集到了Index组件的renderWatcher实例。

注意:这里虽然在计算属性myName中使用了,但是组件加载时没有触发计算属性的getter,所以就没有收集到这个watcher

我们来查看验证:

1.png

2.png

watcher.update()

进入watcher.update方法:

3.png

queueWatcher

进入queueWatcher方法,将组件的renderWatcher推送到queue任务队列:

4.png

注意:这里已经缓存了index组件的renderWatcher.id,所以即使本轮更新中有index组件多个响应式数据更新,触发多次queueWatcher,也只会缓存一次index组件的renderWatcher,即本轮更新中queue队列中只会存在一个index组件的renderWatcher,这就是Vue异步队列更新的作用。

5.png

注意:这就是我们为什么要将操作dom的nextTick回调放在修改data数据之后,就是为了保证更新组件的函数优先执行,我们才可以在nextTick回调中获取到最新的dom。

nextTick

进入nextTick方法:

6.png

7.png

timerFunc

进入timerFunc方法:

8.png

到此为止,响应式数据name的修改逻辑就已经执行完成。

2,修改obj

1.png

开始触发响应式数据objsetter,进入修改逻辑:

2.png

3.png

因为响应式数据obj只被watch引用了,没有其他引用,这里没有在模板中引用,所以没有收集到index组件的renderWatcher

4.png

5.png

6.png

3,访问计算属性

1.png

computedGetter

进入计算属性的computedGetter

2.png

watcher.evaluate()

执行watcher.evaluate(),重新计算【计算属性】值。

3.png

getter()

4.png

5.png

6.png

7.png

本轮同步代码执行完成【可以看出页面还未更新】,下面开始执行异步更新任务。

4,异步更新

flushCallbacks

开始执行微任务队列中的flushCallbacks方法:

1.png

2.png

3.png

flushSchedulerQueue

进入flushSchedulerQueue方法:

4.png

排序之后:

5.png

因为我们在项目中一般都会在watch里面监听一些状态,然后根据状态变化操作其他的响应式数据,所以组件的renderWatcherwatch之后触发,能保证组件更新时获取的都是最新的数据【注意:因为组件的renderWatcher在初始化最后才创建,所以它的watcher.id一定是本组件最大的】。

watcher.run()

最终目的:执行watcher.run()

6.png

第一个watch回调:

7.png

第二个为index组件更新的回调:

8.png

9.png

10.png

11.png

12.png

13.png

14.png

到这里一个Vue应用的加载与更新过程就结束了,案例截图展示的是主要逻辑过程。