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中通过render函数给子组件设置ref操作
Nov 17 Vue.js
Vue用mixin合并重复代码的实现
Nov 27 Vue.js
vue 通过base64实现图片下载功能
Dec 19 Vue.js
vue 动态生成拓扑图的示例
Jan 03 Vue.js
vue.js实现点击图标放大离开时缩小的代码
Jan 27 Vue.js
手写Vue2.0 数据劫持的示例
Mar 04 Vue.js
vue实现水波涟漪效果的点击反馈指令
May 31 Vue.js
详细聊聊vue中组件的props属性
Nov 02 Vue.js
Vue自定义铃声提示音组件的实现
Jan 22 Vue.js
Vue全局事件总线你了解吗
Feb 24 Vue.js
Vue OpenLayer 为地图绘制风场效果
Apr 24 Vue.js
vue如何清除浏览器历史栈
May 25 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
mcrypt启用 加密以及解密过程详细解析
2013/08/07 PHP
PHP调试的强悍利器之PHPDBG
2016/02/22 PHP
PHP 多任务秒级定时器的实现方法
2018/05/13 PHP
javascript笔试题目附答案@20081025_jb51.net
2008/10/26 Javascript
理解Javascript_09_Function与Object
2010/10/16 Javascript
利用jQuery插件扩展识别浏览器内核与外壳的类型和版本的实现代码
2011/10/22 Javascript
ztree获取选中节点时不能进入可视区域出现BUG如何解决
2015/12/03 Javascript
浅析jQuery事件之on()方法绑定多个选择器,多个事件
2016/04/27 Javascript
Vuejs第十三篇之组件——杂项
2016/09/09 Javascript
JavaScript学习笔记--常用的互动方法
2016/12/07 Javascript
Bootstrap列表组学习使用
2017/02/09 Javascript
Angular.js实现动态加载组件详解
2017/05/28 Javascript
微信小程序模板(template)使用详解
2018/01/31 Javascript
Vuex持久化插件(vuex-persistedstate)解决刷新数据消失的问题
2019/04/16 Javascript
微信小程序之下拉列表实现方法解析(附完整源码)
2019/08/23 Javascript
vue实现select下拉显示隐藏功能
2019/09/30 Javascript
部署vue+Springboot前后端分离项目的步骤实现
2020/05/31 Javascript
解决Vue大括号字符换行踩的坑
2020/11/09 Javascript
[02:51]DOTA2 Supermajor小组分组对阵抽签仪式
2018/06/01 DOTA
python脚本实现xls(xlsx)转成csv
2016/04/10 Python
浅谈Pandas Series 和 Numpy array中的相同点
2019/06/28 Python
python装饰器使用实例详解
2019/12/14 Python
详解python logging日志传输
2020/07/01 Python
俄罗斯儿童和青少年服装、鞋子及配件的在线商店:Orby
2020/02/20 全球购物
Lancer Skincare官方网站:抗衰老皮肤护理
2020/11/20 全球购物
当我正在为表建立索引的时候,SQL Server 会禁止对表的访问吗
2014/04/28 面试题
远程调用的原理
2014/07/05 面试题
在职人员函授期间自我评价分享
2013/11/08 职场文书
高校教师思想汇报
2014/01/11 职场文书
单位提档介绍信
2014/01/17 职场文书
《一件运动衫》教学反思
2014/02/19 职场文书
遗嘱公证书标准样本
2014/04/08 职场文书
四风剖析查摆对照检查材料思想汇报
2014/09/24 职场文书
民政局离婚协议书范本
2014/10/20 职场文书
2016秋季幼儿园开学寄语
2015/12/03 职场文书
mysql使用 not int 子查询隐含陷阱
2022/04/12 MySQL