系列文章:
前面的章节都是针对某一部分代码的逻辑解析,最后我们连贯起来过一遍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
应用的初始化:
因为我们在项目中创建Vue时,一般不会传递常规的选项参数,所以init
下面的内容没有可初始化的,直接进入$mount
方法。
Vue.$mount
进入$mount
开始应用的加载:
Vue项目的容器元素就是我们的#app
。
mountComponent
进入组件加载方法mountComponent
,开始加载根组件【在Vue2中,Vue实例也是组件,根实例就是根组件】。
new Watcher()
进入watcher
创建过程:
这里要注意:在创建watcher
时,就会默认执行一次getter
方法【计算属性的watcher除外】。
开始渲染Vue
根实例即Vue应用。
updateComponent
进入updateComponent
方法:
vm._render
首先进入vm._render
方法:
vm._update
进入vm._update
方法:
patch
将vnode
传递给patch
函数:
createElm
进入createElm
方法:
createComponent
进入createComponent
方法:
这里的init
方法是虚拟dom的init
方法,不是组件初始化的,不要搞混了。
vnode init
createComponentInstanceForVnode
根据vnode
对象创建组件实例:
2,App组件
new VueComponent()
创建组件实例,执行thsi.init
开始组件的初始化:
回到vnode init
,开始App
组件的加载:
vm.$mount(*)
调用$mount
,开始App组件的加载【开始Vue
应用的递归渲染过程】:
mountComponent
进入组件加载方法mountComponent
,开始加载App
组件:
new Watcher()
和前面的逻辑一样,创建watcher
实例,并且默认调用一次getter
函数:
updateComponent
进入updateComponent
方法:
vm._render
首先进入vm._render
方法:
vm._update
进入vm._update
方法:
patch
进入patch
函数:
createElm
进入createElm
方法:
根据tag
属性的值,进行不同的加载。如果没有tag
属性,则为注释或者文本节点。
开始创建子节点:
createChildren
进入createChildren
方法:循环children
,分别调用createElm
方法创建子节点。
第一个child
是文本节点:
注意: 这里的创建完成的文本节点会插入到app组件的根节点一个div
。
第二个child
是组件:
createComponent
进入createComponent
方法:
vnode init
3,Index组件
new VueComponent()
开始index组件的初始化:
initMethods
方法挂载:
initData
生成响应式数据data
:
initComputed
初始化计算属性:
initWatch
初始化watch选项:
控制台会打印出created
字符。
后面的$mount
的过程和前面的一样,基本上就是这样递归渲染了【跳过】。
4,真实dom挂载
最后要提及一下真实的dom
元素是怎么加载到页面的。
当index
组件的内容加载完成后:
回到createComponent
方法内:
initComponent
首先进入initComponent
方法:
我们查看当前Index组件的真实dom内容:
// index组件真实dom内容
<div>
<div>我是首页</div>
<div>我是name:tom</div>
<button>修改data数据</button>
</div>
这里的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
组件验证:
组件内部内容渲染完成,代表app组件加载完成:
然后将app组件内容插入到Vue根实例:
// Vue根组件
<div id="app">
<div>
<div>我是根组件APP</div>
<div>
<div>我是首页</div>
<div>我是name:tom</div>
<button>修改data数据</button>
</div>
</div>
</div>
页面渲染完成,应用加载完成。
三,应用更新
我们来点击按钮,修改data
数据,触发组件的更新。
点击按钮,进入断点:
1,修改name
首先触发响应式数据name
的setter
,进入修改逻辑:
proxySetter
进入代理的setter
:
name[setter]
进入真正响应式数据的setter
:
Dep.notify()
响应式数据name
只在template
模板中使用了,所以收集到了Index
组件的renderWatcher
实例。
注意:这里虽然在计算属性myName中使用了,但是组件加载时没有触发计算属性的getter,所以就没有收集到这个
watcher
。
我们来查看验证:
watcher.update()
进入watcher.update
方法:
queueWatcher
进入queueWatcher
方法,将组件的renderWatcher
推送到queue
任务队列:
注意:这里已经缓存了
index
组件的renderWatcher.id
,所以即使本轮更新中有index
组件多个响应式数据更新,触发多次queueWatcher
,也只会缓存一次index
组件的renderWatcher
,即本轮更新中queue
队列中只会存在一个index
组件的renderWatcher
,这就是Vue
异步队列更新的作用。
注意:这就是我们为什么要将操作dom的
nextTick
回调放在修改data数据之后,就是为了保证更新组件的函数优先执行,我们才可以在nextTick
回调中获取到最新的dom。
nextTick
进入nextTick
方法:
timerFunc
进入timerFunc
方法:
到此为止,响应式数据name的修改逻辑就已经执行完成。
2,修改obj
开始触发响应式数据obj
的setter
,进入修改逻辑:
因为响应式数据
obj
只被watch
引用了,没有其他引用,这里没有在模板中引用,所以没有收集到index
组件的renderWatcher
。
3,访问计算属性
computedGetter
进入计算属性的computedGetter
:
watcher.evaluate()
执行watcher.evaluate()
,重新计算【计算属性】值。
getter()
本轮同步代码执行完成【可以看出页面还未更新】,下面开始执行异步更新任务。
4,异步更新
flushCallbacks
开始执行微任务队列中的flushCallbacks
方法:
flushSchedulerQueue
进入flushSchedulerQueue
方法:
排序之后:
因为我们在项目中一般都会在watch
里面监听一些状态,然后根据状态变化操作其他的响应式数据,所以组件的renderWatcher
在watch
之后触发,能保证组件更新时获取的都是最新的数据【注意:因为组件的renderWatcher
在初始化最后才创建,所以它的watcher.id
一定是本组件最大的】。
watcher.run()
最终目的:执行watcher.run()
:
第一个watch
回调:
第二个为index
组件更新的回调:
到这里一个Vue
应用的加载与更新过程就结束了,案例截图展示的是主要逻辑过程。