• 售前

  • 售后

热门帖子
入门百科

浅谈vue首次渲染全过程

[复制链接]
贺老师 显示全部楼层 发表于 2021-10-26 13:34:08 |阅读模式 打印 上一主题 下一主题
目次


  • 1、vue初始化
  • vue入口文件
  • 完整版和运行时版本的区别

    • 1.1、src/core/instace/index.js
    • 1.2、src/core/index.js
    • 1.3、src/platforms/web/runtime/index.js
    • 1.4、src/platforms/web/entry-runtime-with-compiler.js
    • 1.5、vue初始化总结

  • 2、vue构造函数实行

    • 2.1、beforeCreate钩子
    • 2.2、created钩子
    • 2.3、$mount函数
    • 2.4、beforeMount
    • 2.5、mounted

昨天有朋侪问我vue在页面第一次加载时到底做了些什么,看来这个问题在很多朋侪心中大概还比较含糊,本日我们一起来具体的看看vue的初次渲染过程

相识vue初次渲染全过程,我们应该从哪提及呢,很明显,是不是应该从入口文件提及啊,即main.js

1、vue初始化

起首,我们看main.js中,第一个最关键的肯定是引入vue吧
  1. 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版本不同。
这里我们主要来说完整版entry-runtime-with-compiler.js
下面我们先来相识下完整版和运行时版本的区别

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

完整版是运行时版本 + 编译器的组合
运行时版本不带编译器compiler,即没有模板编译功能,主要用来创建vue实例,渲染假造dom。体积小,更轻量(compiler编译器有3000多行代码)
什么意思呢,即
  1. <body>
  2.         <div id="app">
  3.                 <p>我是index.html中的内容</p>
  4.         </div>
  5. </body>
复制代码
  1. new Vue({
  2.         template: '<div>我是template模板渲染出来的内容</div>'
  3. }).$mount('#app')
复制代码
上面的情况,
如果是完整版vue,存在compiler编译器,会将new Vue时传入的template编译成render函数,并赋值给options的render属性,然后$mount后,会渲染render函数成假造dom,再将假造dom转话为真实dom,以是最终页面会出现 我是template模板渲染出来的内容 这句话。原本的那句话会被覆盖
如果是运行时版本,没有编译器,不会编译template中的内容,则页面只会存在原来的dom
下面我们来继续往下看
找到入口文件后,我们开始看看会实行哪些东西


可以看出,入口文件先导入了vue,然后经过了一些处置惩罚,最终又导出了vue
我们先通过导入vue的路径一步一步找到vue构造函数的创建在哪创建的。如上图,从runtime/index中导入了vue,那么我们去看runtime/index



这个文件也是一样,import了vue 经过了一些处置惩罚,然后又导出了vue,我们继续往上找,找core/index



这个文件也是一样,我们继续往上找,找./instance/index



在这里,我们找到了我们的vue构造函数的创建,是在源码的src/core/instance/index.js文件中。

那么,我们从刚刚上面的引用关系,就能发现,vue被我们引入到项目中后,起首会实行的文件的次序是
  1. src/core/instace/index.js ===> 1
  2. src/core/index.js ===> 2
  3. src/platforms/web/runtime/index.js ===> 3
  4. 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的哪些实例属性或方法
1.1.1、initMixin(Vue)

1.1.2、stateMixin(Vue)

1.1.3、eventsMixin(Vue)

1.1.4、lifecycleMixin(Vue)

1.1.5、renderMixin(Vue)

src/core/instace/index.js实行完后,会继续实行下一个文件
  1. export function initGlobalAPI (Vue: GlobalAPI) {
  2.   // config
  3.   const configDef = {}
  4.   configDef.get = () => config
  5.   if (process.env.NODE_ENV !== 'production') {
  6.     configDef.set = () => {
  7.       warn(
  8.         'Do not replace the Vue.config object, set individual fields instead.'
  9.       )
  10.     }
  11.   }
  12.   // 新增了一个config属性
  13.   Object.defineProperty(Vue, 'config', configDef)
  14.   // 新增了一个静态成员 util
  15.   Vue.util = {
  16.     warn,
  17.     extend,
  18.     mergeOptions,
  19.     defineReactive
  20.   }
  21.   // 新增了3个静态成员set  delete  nextTick
  22.   Vue.set = set
  23.   Vue.delete = del
  24.   Vue.nextTick = nextTick
  25.   // 新增了一个静态成员 observable
  26.   Vue.observable = <T>(obj: T): T => {
  27.     observe(obj)
  28.     return obj
  29.   }
  30.   // 初始化了options  此时options是空对象</T>
  31.   Vue.options = Object.create(null)
  32.   ASSET_TYPES.forEach(type => {
  33.     Vue.options[type + 's'] = Object.create(null)
  34.   })
  35.   Vue.options._base = Vue
  36.   // 注册了一个全局组件keep-alive builtInComponents内部就是keep-alive的组件导出
  37.   extend(Vue.options.components, builtInComponents)
  38.   // 下面是分别初始化了Vue.use() Vue.mixin() Vue.extend()
  39.   initUse(Vue)
  40.   initMixin(Vue)
  41.   initExtend(Vue)
  42.   // 初始化Vue.directive(), Vue.component(), vue.filter()
  43.   initAssetRegisters(Vue)
  44. }
复制代码
1.2、src/core/index.js



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

1.2.1 initGlobalAPI(Vue)
  1. export function initGlobalAPI (Vue: GlobalAPI) {
  2.   // config
  3.   const configDef = {}
  4.   configDef.get = () => config
  5.   if (process.env.NODE_ENV !== 'production') {
  6.     configDef.set = () => {
  7.       warn(
  8.         'Do not replace the Vue.config object, set individual fields instead.'
  9.       )
  10.     }
  11.   }
  12.   // 新增了一个config属性
  13.   Object.defineProperty(Vue, 'config', configDef)
  14.   // 新增了一个静态成员 util
  15.   Vue.util = {
  16.     warn,
  17.     extend,
  18.     mergeOptions,
  19.     defineReactive
  20.   }
  21.   // 新增了3个静态成员set  delete  nextTick
  22.   Vue.set = set
  23.   Vue.delete = del
  24.   Vue.nextTick = nextTick
  25.   // 新增了一个静态成员 observable
  26.   Vue.observable = <T>(obj: T): T => {
  27.     observe(obj)
  28.     return obj
  29.   }
  30.   // 初始化了options  此时options是空对象</T>
  31.   Vue.options = Object.create(null)
  32.   ASSET_TYPES.forEach(type => {
  33.     Vue.options[type + 's'] = Object.create(null)
  34.   })
  35.   Vue.options._base = Vue
  36.   // 注册了一个全局组件keep-alive builtInComponents内部就是keep-alive的组件导出
  37.   extend(Vue.options.components, builtInComponents)
  38.   // 下面是分别初始化了Vue.use() Vue.mixin() Vue.extend()
  39.   initUse(Vue)
  40.   initMixin(Vue)
  41.   initExtend(Vue)
  42.   // 初始化Vue.directive(), Vue.component(), vue.filter()
  43.   initAssetRegisters(Vue)
  44. }
复制代码
1.3、src/platforms/web/runtime/index.js



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



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


1.5、vue初始化总结

上面写的整个过程,都是用户在利用vue时,引入vue文件后,立刻会实行的一些东西
这些实行完后,是不是会继续去实行我们项目中的main.js文件啊,
此时会实行到
  1. new Vue({
  2.   router,
  3.   store,
  4.   render: h => h(App)
  5. }).$mount('#app')
复制代码
这个时间,会开始调用,我们的vue构造函数

2、vue构造函数实行

此时,会先实行vue构造函数,



可以看出,主要是实行了_init方法,从这里开始,vue的生命周期开始实行了



上面只是_init()方法中最主要的一部门代码,(代码太多,我就不全部截图了,你们自己到源码中看)。可以看出:


2.1、beforeCreate钩子

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

2.2、created钩子

上图可以看出,beforeCreate钩子实行竣事后,主要实行了3个方法:initInjections, initState, initProvide
  1. // 把inject注入到vm实例
  2. callHook(vm, 'beforeCreate')
  3. // 把inject注入到vm实例
  4. initInjections(vm)
  5. // 初始化vm的$props,$methods,$data,computed,watch
  6. initState(vm)
  7. // 把provide注入vm实例
  8. initProvide(vm)
  9. // 执行created生命周期
  10. 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生命周期走完以后,继续往下看



可以看出,这里判定了vm.$options.el是否存在,vm.$options.el是什么啊。
new Vue({})时,传入的那个对象的全部属性,都会被挂载options下,
  1. new Vue({
  2.   el: '#app'
  3.   router,
  4.   store,
  5.   render: h => h(App)
  6. })
复制代码
故,vm.$options.el就是上面传入的el。
这里判定el是否存在,如果存在,才会继续往下实行$mount
那大家大概会好奇了,如果不存在,那是不是就卡死了,后面都不会走了。是的,如果没有,就不会继续走了,要想代码继续往下走,必然要实行$mount方法。
此时,我们再看不停vue常用的情况
  1. new Vue({
  2.   router,
  3.   store,
  4.   render: h => h(App)
  5. }).$mount('#app')
复制代码
这里没有传入el,以是源码中的



肯定是不会走的。但是,用户在new Vue的时间可以自己用new 出来的vue实例去调用$mount。这么一来,大家看我们官网的生命周期图,大概就更容易看懂了



好了,下面我们继续往下,下一步是实行 $mount,我们来看 $mount方法

2.3、$mount函数

我们之前初始化的时间,重写过$mount还记得吗,以是,此时我们实行$mount时,实行的是重写后的mount



这里在重写前先存储了重写前的mount方法,然后在最后调用了重写前的mount方法。
重写后,最关键的代码是判定是否有render函数



这一步的主要作用就是判定是否有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属性下


然后会继续实行重写前的$mount,明白了这,我们就能明白生命周期图中的另一部门了



2.4、beforeMount

下面,我们继续来看重写前的$mount函数的实行



可以看出\ $mount中主要是实行了函数mountComponent,我们继续看mountComponent函数


可以看出,此函数,主要做了以下4件事
我们一件一件来看
1、实行了beforeMount钩子,以是可以得出结论,再beforeMount之前,我们主要是初始化和得到render函数。而beforeMount之后,才是开始将render函数渲染成假造dom,然后更新真实dom
render函数得到的途径有3种
第一:用户自己传入render



第二:.vue文件编译成render



这种方式,就是自己传入了一个render函数,函数内用h函数前实行了App.vue文件。
.vue文件最终转化为render函数需要借助vue-loader来完成
第三、将template模板编译成render函数


2、定义了一个updateComponent函数,此函数内调用了vm的_update方法,同时实行了vm._render()方法,并将执

行后的结果当做参数传给_update方法。_render方法我们前面说过,他内部渲染了render函数成为假造dom,故_render()的返回值是一个vnode。
我们先来看下_render()函数内部如何将render函数转化为假造dom的



然后我们再看_update函数内部做了啥



可以看出,_update函数中,实行了__patch__方法去对比两个新旧dom,从而找出差异,更新真实dom。如果是初次渲染,则直接将当前的vnode,生成真实的dom。
故得出结论,整个updateComponent方法的主要作用就是渲染render函数,更新dom
而什么时间更新dom的关键,就在于什么时间去调用这个updateComponent函数了
3、new 了一个watcher实例



可以看出,new一个watcher实例的同时,传入了updateComponent函数作为参数。
此时,我们看new Watcher时,会实行Watcher构造函数,我们看Watcher构造函数内做了啥



watcher分为3种,渲染watcher,$watch函数的watcher,computed的watcher。我们这里渲染页面的是渲染watcher
上面将我们传入的函数传给了getter


继续往下走,调用了get()



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

此时,我们继续往下看



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



判定了当前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初次渲染全过程的文章就先容到这了,更多相关vue初次渲染内容请搜索脚本之家以前的文章或继续欣赏下面的相关文章盼望大家以后多多支持脚本之家!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

x

帖子地址: 

回复

使用道具 举报

分享
推广
火星云矿 | 预约S19Pro,享500抵1000!
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

草根技术分享(草根吧)是全球知名中文IT技术交流平台,创建于2021年,包含原创博客、精品问答、职业培训、技术社区、资源下载等产品服务,提供原创、优质、完整内容的专业IT技术开发社区。
  • 官方手机版

  • 微信公众号

  • 商务合作