vue首次渲染全过程


Posted in Vue.js onApril 21, 2021

昨天有朋友问我vue在页面第一次加载时到底做了些什么,看来这个问题在很多朋友心中可能还比较模糊,今天我们一起来详细的看看vue的首次渲染过程
vue源码下载地址:vue源码

了解vue首次渲染全过程,我们应该从哪说起呢,很明显,是不是应该从入口文件说起啊,即main.js

1、vue初始化

首先,我们看main.js中,第一个最关键的肯定是引入vue吧

import vue from 'vue'

其实,vue被打包后,dist文件夹中存在多个版本,分别是
通用版本(UMD):中的完整版 vue.js 和运行时版本 vue.runtime.js
CommonJs版本:中的完整版vue.common.js 和 运行时版本vue.runtime.common.js
ES Module版本:中的完整版vue.esm.js 和 运行时版本vue.runtime.esm.js
一般在vue2.6以后,我们用vue/cli创建的项目用的都是vue.runtime.esm.js运行时版本
即,引入vue时会引入vue.esm.js这个版本

那么,vue引入以后,是不是vue中的相关代码会被执行啊。那最新执行vue源码中的哪块代码呢(引入的vue就是vue源码中被打包后的vue),我们先得知道入口文件在哪

vue入口文件

vue的入口文件主要在vue源码结构的src/platforms/web下
vue首次渲染全过程
vue打包时,可以选择不同的vue入口文件来进行打包,不同的入口文件打包出来的vue版本不同。
这里我们主要来说完整版entry-runtime-with-compiler.js
下面我们先来了解下完整版和运行时版本的区别

完整版和运行时版本的区别

完整版是运行时版本 + 编译器的组合
运行时版本不带编译器compiler,即没有模板编译功能,主要用来创建vue实例,渲染虚拟dom。体积小,更轻量(compiler编译器有3000多行代码)
什么意思呢,即

<body>
	<div id="app">
		<p>我是index.html中的内容</p>
	</div>
</body>
new Vue({
	template: '<div>我是template模板渲染出来的内容</div>'
}).$mount('#app')

上面的情况,
如果是完整版vue,存在compiler编译器,会将new Vue时传入的template编译成render函数,并赋值给options的render属性,然后$mount后,会渲染render函数成虚拟dom,再将虚拟dom转话为真实dom,所以最终页面会出现 我是template模板渲染出来的内容 这句话。原本的那句话会被覆盖

如果是运行时版本,没有编译器,不会编译template中的内容,则页面只会存在原来的dom

下面我们来继续往下看
找到入口文件后,我们开始看看会执行哪些东西

vue首次渲染全过程
可以看出,入口文件先导入了vue,然后经过了一些处理,最终又导出了vue
我们先通过导入vue的路径一步一步找到vue构造函数的创建在哪创建的。如上图,从runtime/index中导入了vue,那么我们去看runtime/index
vue首次渲染全过程
这个文件也是一样,import了vue 经过了一些处理,然后又导出了vue,我们继续往上找,找core/index
vue首次渲染全过程
这个文件也是一样,我们继续往上找,找./instance/index
vue首次渲染全过程
在这里,我们找到了我们的vue构造函数的创建,是在源码的src/core/instance/index.js文件中。

那么,我们从刚刚上面的引用关系,就能发现,vue被我们引入到项目中后,首先会执行的文件的顺序是

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

1.1、src/core/instace/index.js

首先,此文件定义了vue构造函数,并初始化了一些vue的实例属性和实例方法,即,在vue.prototype原型下新增了各种方法和属性
vue首次渲染全过程
下面,我们具体来看下,每一个方法具体初始化了vue的哪些实例属性或方法

1.1.1、initMixin(Vue)

vue首次渲染全过程

1.1.2、stateMixin(Vue)

vue首次渲染全过程

1.1.3、eventsMixin(Vue)

vue首次渲染全过程

1.1.4、lifecycleMixin(Vue)

vue首次渲染全过程

1.1.5、renderMixin(Vue)

vue首次渲染全过程

src/core/instace/index.js执行完后,会继续执行下一个文件

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

1.2、src/core/index.js

vue首次渲染全过程
可以看出,这个文件,主要是给vue新增了很多静态实例方法和属性,具体新增了哪些,
我们继续看被执行的那个方法initGlobalAPI(Vue)

1.2.1 initGlobalAPI(Vue)

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)
}

1.3、src/platforms/web/runtime/index.js

vue首次渲染全过程

1.4、src/platforms/web/entry-runtime-with-compiler.js

vue首次渲染全过程
此文件,最主要的作用就重写了vue原型下的$mount方法。具体$mount方法中做了些什么,我们后面会讲

1.5、vue初始化总结

上面写的整个过程,都是用户在使用vue时,引入vue文件后,立刻会执行的一些东西

这些执行完后,是不是会继续去执行我们项目中的main.js文件啊,
此时会执行到

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

这个时候,会开始调用,我们的vue构造函数

2、vue构造函数执行

此时,会先执行vue构造函数,
vue首次渲染全过程
可以看出,主要是执行了_init方法,从这里开始,vue的生命周期开始执行了
vue首次渲染全过程
上面只是_init()方法中最主要的一部分代码,(代码太多,我就不全部截图了,你们自己到源码中看)。可以看出:

2.1、beforeCreate钩子

在生命周期beforeCreate钩子之前,vue主要做的事情就是给vue原型新增各种属性和方法,给vue新增各种静态属性和方法,以及给vm实例新增各种属性和方法

2.2、created钩子

上图可以看出,beforeCreate钩子执行结束后,主要执行了3个方法:initInjections, initState, initProvide

// 把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')

其实,重点是initState(vm)方法,该方法中,初始化了vm实例的$props, $data, $methods, computed, watch等。同时,在里面调用了一个initData()方法,该方法内会调用observer() 方法,将data中的数据都转化为响应式数据。即添加数据拦截器。

所以可以看出,在created生命周期之前,vm的$props, $data, $methods, computed, watch属性都会初始化完成,
故,这也就是为什么,我们可以在created中调用我们data中的各种数据以及调用props或者methods等下面的各种方法了。

created生命周期走完以后,继续往下看
vue首次渲染全过程
可以看出,这里判断了vm.$options.el是否存在,vm.$options.el是什么啊。
new Vue({})时,传入的那个对象的所有属性,都会被挂载options下,

new Vue({
  el: '#app'
  router,
  store,
  render: h => h(App)
})

故,vm.$options.el就是上面传入的el。
这里判断el是否存在,如果存在,才会继续往下执行$mount

那大家可能会好奇了,如果不存在,那是不是就卡死了,后面都不会走了。是的,如果没有,就不会继续走了,要想代码继续往下走,必然要执行$mount方法。
此时,我们再看一直vue常用的情况

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

这里没有传入el,所以源码中的
vue首次渲染全过程
肯定是不会走的。但是,用户在new Vue的时候可以自己用new 出来的vue实例去调用$mount。这么一来,大家看我们官网的生命周期图,可能就更容易看懂了
vue首次渲染全过程
好了,下面我们继续往下,下一步是执行 $mount,我们来看 $mount方法

2.3、$mount函数

我们之前初始化的时候,重写过$mount还记得吗,所以,此时我们执行$mount时,执行的是重写后的mount
vue首次渲染全过程
这里在重写前先存储了重写前的mount方法,然后在最后调用了重写前的mount方法。
重写后,最关键的代码是判断是否有render函数
vue首次渲染全过程
这一步的主要作用就是判断是否有render函数,
如果有,直接往下执行重写前的$mount方法前渲染render函数,

如果没有,就会前判断是否存在template模板(options.template是否存在,options.template可能是id选择器,可能是dom),如果存在模板,就会获取到模板中的内容,并赋值给template,options.template不存在,那么会直接以el指定的dom为模板(即#app),获取到el下的dom,赋值给template

template取到dom后,然后继续往下,将此template编译成render函数,并将编译出来的render函数挂载options.render属性下
vue首次渲染全过程

然后会继续执行重写前的$mount,理解了这,我们就能理解生命周期图中的另一部分了
vue首次渲染全过程

2.4、beforeMount

下面,我们继续来看重写前的$mount函数的执行
vue首次渲染全过程
可以看出\ $mount中主要是执行了函数mountComponent,我们继续看mountComponent函数
vue首次渲染全过程

可以看出,此函数,主要做了以下4件事
我们一件一件来看
1、执行了beforeMount钩子,所以可以得出结论,再beforeMount之前,我们主要是初始化和得到render函数。而beforeMount之后,才是开始将render函数渲染成虚拟dom,然后更新真实dom
render函数得到的途径有3种
第一:用户自己传入render
vue首次渲染全过程
第二:.vue文件编译成render
vue首次渲染全过程
这种方式,就是自己传入了一个render函数,函数内用h函数前执行了App.vue文件。
.vue文件最终转化为render函数需要借助vue-loader来完成

第三、将template模板编译成render函数
vue首次渲染全过程
2、定义了一个updateComponent函数,此函数内调用了vm的_update方法,同时执行了vm._render()方法,并将执行后的结果当做参数传给_update方法。_render方法我们前面说过,他内部渲染了render函数成为虚拟dom,故_render()的返回值是一个vnode。

我们先来看下_render()函数内部如何将render函数转化为虚拟dom的
vue首次渲染全过程
然后我们再看_update函数内部做了啥
vue首次渲染全过程
可以看出,_update函数中,执行了__patch__方法去对比两个新旧dom,从而找出差异,更新真实dom。如果是首次渲染,则直接将当前的vnode,生成真实的dom。

故得出结论,整个updateComponent方法的主要作用就是渲染render函数,更新dom
而什么时候更新dom的关键,就在于什么时候去调用这个updateComponent函数了

3、new 了一个watcher实例
vue首次渲染全过程
可以看出,new一个watcher实例的同时,传入了updateComponent函数作为参数。
此时,我们看new Watcher时,会执行Watcher构造函数,我们看Watcher构造函数内做了啥
vue首次渲染全过程
watcher分为3种,渲染watcher,$watch函数的watcher,computed的watcher。我们这里渲染页面的是渲染watcher

上面将我们传入的函数传给了getter

vue首次渲染全过程
继续往下走,调用了get()

vue首次渲染全过程
可以看出,get()中调用了我们传入的函数,而我们传入的函数就是渲染render函数,并触发虚拟dom更新真实dom,而返回的值,就是渲染后的真实dom,最后赋值给了this.value,而this.value最后会用于更新依赖者。而我们当前这个wather实例,是主vue实例的watcher,故可以理解为整个页面的watcher。当我们调用this.$fouceUpdate()时,就是调用这个实例的update方法,去更新整个页面。
所以说,new Wacher的时候 updateComponent会自动调用一次,这就是我们的首次渲染。

此时,我们继续往下看
vue首次渲染全过程
这内部,还做了个判断,如果vm._isMounted为true(即Mounted钩子已经执行过了),而vm._isDestroyed为fase时(即当前组件还未销毁)。此时,如果产生更新,则说明并非首次渲染,那么执行beforeUpdate钩子,后续肯定还会走updated。这里我们就不说updated的事了

new Watcher后,代码继续往下走
vue首次渲染全过程
判断了当前vnode如果null,说明之前没有生成过虚拟dom,也就说明这次肯定是首次渲染,此时,vm._isMounted置为true。并执行mounted钩子函数,此时,首次渲染完成。

2.5、mounted

可以看出,整个beforeMount 到 mounted过程中,主要做的工作就是
1、渲染render函数成为虚拟dom vnode
2、执行vm._update函数,将虚拟dom转化为真实dom
如果是beforeUpdate 到 updated钩子之间,说明不是首次渲染,那么虚拟dom会有新旧两个。此时vm._update函数的作用就是对比新旧两个vnode,得出差异,更新需要更新的地方

首次渲染整个过程就是这样。有问题欢迎下方评论或者私信我

Vue.js 相关文章推荐
Vue开发中常见的套路和技巧总结
Nov 24 Vue.js
详解vue之自行实现派发与广播(dispatch与broadcast)
Jan 19 Vue.js
vue实现简易计算器功能
Jan 20 Vue.js
如何在vue 中使用柱状图 并自修改配置
Jan 21 Vue.js
Vue 集成 PDF.js 实现 PDF 预览和添加水印的步骤
Jan 22 Vue.js
vue打开新窗口并实现传参的图文实例
Mar 04 Vue.js
vue使用v-model进行跨组件绑定的基本实现方法
Apr 28 Vue.js
Vue Element UI自定义描述列表组件
May 18 Vue.js
vue Element-ui表格实现树形结构表格
Jun 07 Vue.js
详解Vue的列表渲染
Nov 20 Vue.js
一起来看看Vue的核心原理剖析
Mar 24 Vue.js
vue实现省市区联动 element-china-area-data插件
Apr 22 Vue.js
浅谈vue2的$refs在vue3组合式API中的替代方法
Vue.js 带下拉选项的输入框(Textbox with Dropdown)组件
vue backtop组件的实现完整代码
vue中三级导航的菜单权限控制
Mar 31 #Vue.js
vue3中的组件间通信
vue前端工程的搭建
vue中data改变后让视图同步更新的方法
You might like
php设计模式之观察者模式的应用详解
2013/05/21 PHP
使用Appcan客户端自动更新PHP版本号(全)
2015/07/31 PHP
PHP判断用户是否已经登录(跳转到不同页面或者执行不同动作)
2016/09/22 PHP
Thinkphp5框架使用validate实现验证功能的方法
2019/08/27 PHP
如何简单地用YUI做JavaScript动画
2007/03/10 Javascript
理解Javascript_06_理解对象的创建过程
2010/10/15 Javascript
实现变速回到顶部的JavaScript代码
2011/05/09 Javascript
JS获取客户端IP地址、MAC和主机名的7个方法汇总
2014/07/21 Javascript
js 判断图片是否加载完以及实现图片的预下载
2014/08/14 Javascript
jquery和js实现对div的隐藏和显示方法
2014/09/26 Javascript
js监听鼠标事件控制textarea输入字符串的个数
2014/09/29 Javascript
总结Javascript中的隐式类型转换
2016/08/24 Javascript
jQuery实现获取隐藏div高度的方法示例
2017/02/09 Javascript
JS正则表达式判断有效数实例代码
2017/03/13 Javascript
vue中的event bus非父子组件通信解析
2017/10/27 Javascript
Python编程之列表操作实例详解【创建、使用、更新、删除】
2017/07/22 Python
Python内存管理方式和垃圾回收算法解析
2017/11/11 Python
人生苦短我用python python如何快速入门?
2018/03/12 Python
Django中使用Whoosh进行全文检索的方法
2019/03/31 Python
使用python动态生成波形曲线的实现
2019/12/04 Python
Python 实现将numpy中的nan和inf,nan替换成对应的均值
2020/06/08 Python
浅谈优化Django ORM中的性能问题
2020/07/09 Python
python解压zip包中文乱码解决方法
2020/11/27 Python
英国网上花店:Bunches
2016/11/29 全球购物
毕业生自荐信的主要内容
2013/10/29 职场文书
党校学习思想汇报
2014/01/06 职场文书
违纪检讨书2000字
2014/02/08 职场文书
《菜园里》教学反思
2014/04/17 职场文书
艺术教育实施方案
2014/05/03 职场文书
农民工预备党员思想汇报
2014/09/14 职场文书
助学贷款贫困证明
2014/09/23 职场文书
医院领导班子整改方案
2014/10/01 职场文书
离婚协议书标准格式
2014/10/04 职场文书
单位实习鉴定评语
2015/01/04 职场文书
2015年大学班主任工作总结
2015/04/30 职场文书
初级职称评定工作总结
2015/08/13 职场文书