本文章主要简单介绍vue首次渲染过程到底做了什么事情。更多参考
Vue的运行过程
模板({{}}语法)—> ast(抽象语法树)—> f(x)(渲染函数)—>虚拟Dom—>DOM(diff算法)
vue初始化
首先就是main.js的import Vue from 'vue'
一般我们用vue/cli创建项目的时候用的是vue.runtime.esm.js运行时版本,引入vue时会引入vue.esm.js这个版本,而我们一般使用vue/cli是配合vue-loader来解析.vue文件为h函数,所以不需要带编译器的完整版
- 完整版和运行时版本区别
- 完整版和运行时版本区别就是完整版存在多一个编译器,大小比运行时版本多一个编译器。所以一般简单在demo引入选择带编译器的完整版,而配合webpack工具使用的vue/cli使用的为不带编译器版本,因为配合vue-loader一起使用,而vue-loader内部也是包含了vue-template-compiler可以负责把.vue文件转换为h函数
- vue打包后的dist文件夹中不同版本主要解析:
-
通用版本(UMD):完整版vue.js和运行时版本vue.runtime.js
-
Comnonjs版本:完整版vue.common.js和运行时版本vue.runtime.common.js
-
ESModule版本:完整版vue.esm.js和运行时版本vue.runtime.esm.js
-
vue类查找(src/core/instace/index.js)
vue文件加载顺序,本文按照完整版entry-runtime-with-compiler.js来说,不同的入口会生成不同的文件:
//执行流程为1234
//查找到vue类要从4321上找
src/core/instace/index.js ===> 1
src/core/index.js ===> 2
src/platforms/web/runtime/index.js ===> 3
src/platforms/web/entry-runtime-with-compiler.js 4
-
src/core/instace/index.js主要内容:
-
定义vue构造函数,并初始化vue的实例属性和实例方法,即在vue.prototype原型下新增各种方法和属性。总共的方法有下面图示:
-
initMixin方法:在vue原型上挂载
_init
方法,主要是初始化options对象 -
stateMixin方法:在vue原型上新增
$data和$props
两个属性、$set和$delete和$watch
这三个方法 -
eventsMixin方法:在vue原型上新增
$on、$once、$off、$emit
四个方法 -
lifecycleMixin方法:在vue原型上新增
_update
(该方法主要调用__patch__方法去对比新旧节点从而 更新dom)、$forceUpdate和$destory
三个方法 -
renderMixin方法:在vue原型上新增
$nextTick和_render方法
(该方法主要执行用户传入的render函数或者编译生成的render函数,并生成vnode)
-
-
src/core/index.js主要内容:
- initGlobalAPI方法:初始化很多vue静态方法和属性(nextTick,del,set等)
export function initGlobalAPI (Vue: GlobalAPI) {
// config
const configDef = {}
configDef.get = () => config
if (process.env.NODE_ENV !== 'production') {
configDef.set = () => {
warn(
'Do not replace the Vue.config object, set individual fields instead.'
)
}
}
// 新增了一个config属性
Object.defineProperty(Vue, 'config', configDef)
// 新增了一个静态成员 util
Vue.util = {
warn,
extend,
mergeOptions,
defineReactive
}
// 新增了3个静态成员set delete nextTick
Vue.set = set
Vue.delete = del
Vue.nextTick = nextTick
// 新增了一个静态成员 observable
Vue.observable = <T>(obj: T): T => {
observe(obj)
return obj
}
// 初始化了options 此时options是空对象</T>
Vue.options = Object.create(null)
ASSET_TYPES.forEach(type => {
Vue.options[type + 's'] = Object.create(null)
})
Vue.options._base = Vue
// 注册了一个全局组件keep-alive builtInComponents内部就是keep-alive的组件导出
extend(Vue.options.components, builtInComponents)
// 下面是分别初始化了Vue.use() Vue.mixin() Vue.extend()
initUse(Vue)
initMixin(Vue)
initExtend(Vue)
// 初始化Vue.directive(), Vue.component(), vue.filter()
initAssetRegisters(Vue)
}
-
src/platforms/web/runtime/index.js
-
新增__patch__方法,该方法主要为进行更新的时候进行新旧节点diff对比,然后更新dom操作
-
新增$mount方法,该方法作用是渲染render函数,将render函数转换为虚拟dom
-
-
src/platforms/web/entry-runtime-with-compiler.js
- 最主要的作用就重写了vue原型下的$mount方法。为什么要重写,因为重写代码需要判断是否有render函数,如果没有render函数就会查找tempalte属性,而执行tempalte属性的内容就会使用到编译器进行编辑模板使用。而重写前的mount方法是只能给render函数使用。
vue初始化总结
上面的所有操作就是我们引入vue的时候,即import Vue from 'vue'
进行的一系列操作,执行完这行代码后就会继续执行main.js文件的内容,就可以调用vue的构造函数。
new Vue({
router,
store,
render: h=>h(App)
}).$mount('#app')
主要执行了_init方法,从这里开始,vue的生命周期开始执行。_init
方法主要函数为下面所示,更多请参考源码:
生命钩子:
- beforeCreate钩子:在该生命周期钩子之前,vue主要做的事情是给vue原型新增各种属性和方法,给vue新增各种静态属性和方法,以及给vm实例添加各种属性和方法。
- created钩子(可以获取到data,methods等属性):在beforeCreated结束后主要执行了三个方法:initInjections, initState, initProvide,重点在initState方法,该方法中初始化了vm实例的
$props,$data,$methods,comouted,watch
等,同时还调用了一个initData()方法,该方法调用了Observer()方法,将data中的数据转换为响应式数据。因此在created生命周期之前,vm的$data
等属性都会初始化完成,所以我们可以在created中调用data中的各种属性以及调用props或者methods各种方法等。// 把inject注入到vm实例 callHook(vm, 'beforeCreate') // 把inject注入到vm实例 initInjections(vm) // 初始化vm的$props,$methods,$data,computed,watch initState(vm) // 把provide注入vm实例 initProvide(vm) // 执行created生命周期 callHook(vm, 'created')
- created生命周期走完后,会进行
vm.$options.el
判断当前el是否传入,即
new Vue({
el:'#app',
route,
...
})
的el属性。如果el存在,则继续执行$mount
。如果不存在则不继续往下走,而要想代码继续往下找,必须执行该$mount
方法。但是用户可以在new Vue({...}).$mount('#app')
在new出来的vue实例去调用$mount
。
$mount函数
,无论是new的时候把el传入还是实例后自己调用$mount()
,都是执行$mount
函数。- 使用的
$mount()
就是我们重写后的那个mount方法。重写前的mount方法是只能给render函数使用,即传入的new vue上的render属性,而传入的vue实例中如果没有render属性,会去寻找template属性(template->render),如果也没有才会找el属性(以el指定dom为模板时候,会把 el下的dom赋值给template,即el=>template=>render)。所以mount函数寻找模板优先级:render>template>el。$mount
最终目的都是转化为render函数挂载到options.render属性下。转化为render函数后,最终执行重写前的$mount
。而该重写前的mount函数作用就是渲染render函数的,将render函数转换为虚拟dom。模板转换为render函数通过的为compileToFunctions()进行转换。 - 拓展:render函数获取的三种途径
- 用户自己传入render
new Vue({ router, store, render: h=>{ h('div',{class: 'vdom'},[ h('p',['aaa']) ]) } }).$mount('#app')
- .vue文件编译成render,一般借助vue-loader的vue-template-compiler把.vue文件解析为render函数。
new Vue({ router, store, render: h=>h(App) }).$mount('#app')
- template模板编译成render
new Vue({ router, store, template: '<div>哈哈哈</div>' }).$mount('#app')
- 用户自己传入render
- 使用的
- beforeMount生命周期钩子(可以获取到render函数)
-
该钩子函数在重写前的mount函数中的return的mountComponnet函数中。
-
mountComponnet函数做了4件事。
- 执行了beforeMount钩子,所以在beforeMount之前,我们就初始化和得到render函数。而beforeMount之后,才是开始render函数渲染成虚拟dom,然后更新真实dom。
- updateComponent函数,该函数执行了_update方法。
_update
方法的第一个参数为vm._render()
方法执行后的结果,即虚拟dom。_render
方法作用为内部渲染了render函数称为虚拟dom,即返回一个vnode_update
方法内部如图示,主要执行了__patch__
方法进行新旧vnode的对比,找出差异,更新真实dom。如果首次渲染,则直接将当前的vnode生成真实dom。
结论:updateComponent方法执行的_update方法,_update方法则调用__patch__实现新旧vnode对比,生成真实dom。updateComponent方法主要作用就是渲染render函数,更新dom,返回真实dom。
- new Watcher,new一个Wacther实例的时候会传入updateComponent函数作为参数传入。然后执行Watcher构造函数如图所示。watcher分为3种,渲染watcher,
$watch
函数的watcher,computed的watcher。我们这里渲染页面的是渲染watcher。而这个渲染页面的watcher就是主vue实例的watcher,可以理解为整个页面的wacther。可以看出我们传入给Watcher实例参数的updateComponent方法(渲染render函数,并触发虚拟dom更新真实dom,返回真实dom)最终由get()方法触发,最后赋值给this.value,而this.value最后会用于更新依赖者。当我们调用this.$foceUpdate()时,就会触发这个实例的update方法,更新整个页面。new Wacher的时候 updateComponent会自动调用一次,即默认调用一个get方法,这就是我们的首次渲染。
- mounted钩子函数。执行完new Wacther后,代码继续往下执行,即图示。判断当前vnode为null,如果为null说明之前没有生成过虚拟dom,即首次渲染。此时,vm._isMounted设置为true。并执行mounted钩子函数。此时首次渲染完成。
-
- mounted生命周期钩子
从beforeMount到mounted过程中,主要做了工作是(
_render将render函数转换为虚拟dom
,_update将虚拟dom转换为真实dom并挂载
): - 渲染render函数成为虚拟dom,该工作为重写的
$mount
来实现模板变成render,然后通过原来的$mount
的mountComponnet的updateComponent的第一个参数vm._render()
实现将render函数转换为虚拟dom - 执行updateComponnet,即会执行
vm._update
函数,将虚拟dom转换为真实dom 如果是beforeUpdate到update钩子之间,说明不是首次渲染,那么虚拟dom会有新旧两个。此时vm._update
函数的作用就是对比新旧两个vnode,得出差异,更新需要更新的地方。
简述
- 进行vue初始化,对于一些类似于v-if/v-model的指令,transition等组件以及挂载_patch_、$mount等方法的挂载
- 然后实例化vue,即执行
this._init()
,该方法主要进行了以下操作- 调用第一个
this.$mount
,判断当前传入对象是否携带render,tempalte以及el,按照顺序进行获取,如果携带的不为render则需要进行模板编译,即通过complieToFunctions()进行模板编译为render()。 - 经过第一个重写的mount的调用,该$mount返回的是一个
this.mountComponent
方法,如果没有携带render或者携带的是模板则进行提示,运行环境会有问题。否则则进行updateComponent
方法的定义,然后之执行beforeMount
钩子 - 创建的
this.updateComponent
,该方法主要有_render()以及_update(),_render方法主要为把render函数变成虚拟DOM,_update方法主要为将虚拟DOM变为真实DOM,并挂载。 - 最后new一个Watcher并调用get方法。每个组件都有一个watcher,该wacther传递的参数为updateComponent方法,而在get方法中,首先调用 pushTarget(this) 将当前 watcher 对象入栈,因为每一个组件都对应一个 watcher 对象,watcher 回去渲染视图,如果组件嵌套有多个内部组件,则要先渲染内部组件,所以需要先把父组件 watcher 入栈保存。
- watcher调用的get方法,即内部会将传入的updateComponent会通过this.getter存储起来,最后调用this.get()来调用this.getter即调用传递的updateComponent进行更新操作
- 调用第一个
源码执行流程图示
- vue自身初始化的全过程,即执行
import Vue from 'vue'
的时候做的事情 - 初始化后在内存的结构为:(由于没有new所以实例对象还不存在)
- new Vue()后: