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中activated的用法
Jan 03 Vue.js
vue项目如何监听localStorage或sessionStorage的变化
Jan 04 Vue.js
vue中父子组件的参数传递和应用示例
Jan 04 Vue.js
如何封装Vue Element的table表格组件
Feb 06 Vue.js
vue3中的组件间通信
Mar 31 Vue.js
Vue项目中如何封装axios(统一管理http请求)
May 02 Vue.js
Vue全家桶入门基础教程
May 14 Vue.js
Vue CLI中模式与环境变量的深入详解
May 30 Vue.js
vue中使用mockjs配置和使用方式
Apr 06 Vue.js
vue使用localStorage持久性存储实现评论列表
Apr 14 Vue.js
Vue OpenLayer 为地图绘制风场效果
Apr 24 Vue.js
Vue Mint UI mt-swipe的使用方式
Jun 05 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
手把手教你使用DedeCms V3的在线采集图文教程
2007/04/03 PHP
php读取csv文件后,uft8 bom导致在页面上显示出现问题的解决方法
2013/08/10 PHP
ThinkPHP实现将SESSION存入MYSQL的方法
2014/07/22 PHP
ThinkPHP无限级分类原理实现留言与回复功能实例
2014/10/31 PHP
php注册审核重点解析(数据访问)
2017/05/23 PHP
JQuery UI皮肤定制
2009/07/27 Javascript
imgAreaSelect 中文文档帮助说明
2011/10/08 Javascript
JS模拟自动点击的简单实例
2013/08/08 Javascript
用JavaScript计算在UTF-8下存储字符串占用字节数
2013/08/08 Javascript
jquery实现鼠标滑过小图时显示大图的方法
2015/01/14 Javascript
轻松学习Javascript闭包函数
2015/12/15 Javascript
工作中比较实用的JavaScript验证和数据处理的干货(经典)
2016/08/03 Javascript
深入理解ES6的迭代器与生成器
2017/08/19 Javascript
Vue上传组件vue Simple Uploader的用法示例
2017/08/25 Javascript
如何用RxJS实现Redux Form
2018/12/29 Javascript
vue element动态渲染、移除表单并添加验证的实现
2019/01/16 Javascript
nodeJs的安装与npm全局环境变量的配置详解
2020/01/06 NodeJs
JavaScript实现好看的跟随彩色气泡效果
2020/02/06 Javascript
JS错误处理与调试操作实例分析
2020/04/13 Javascript
JavaScript点击按钮生成4位随机验证码
2021/01/28 Javascript
将Python中的数据存储到系统本地的简单方法
2015/04/11 Python
Windows中使用wxPython和py2exe开发Python的GUI程序的实例教程
2016/07/11 Python
win与linux系统中python requests 安装
2016/12/04 Python
Python实现决策树C4.5算法的示例
2018/05/30 Python
python实现126邮箱发送邮件
2020/05/20 Python
CSS3支持IE6, 7, and 8的边框border属性
2012/12/28 HTML / CSS
意大利领先的线上奢侈品销售电商:Eleonora Bonucci
2017/10/17 全球购物
蛋白质世界:Protein World
2017/11/23 全球购物
学习雷锋倡议书
2014/04/15 职场文书
安全责任书模板
2014/07/22 职场文书
村安全生产责任书
2014/08/25 职场文书
2014基层党员批评与自我批评范文
2014/09/24 职场文书
2015年毕业生个人自荐书
2015/03/24 职场文书
介绍信范文大全
2015/05/07 职场文书
正规欠条模板
2015/07/03 职场文书
2015年大学迎新晚会总结
2015/07/16 职场文书