浅谈vue首次渲染全过程

目录
  • 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吧

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多行代码)
什么意思呢,即

<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构造函数的创建在哪创建的。如上图,从runtime/index中导入了vue,那么我们去看runtime/index

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

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

在这里,我们找到了我们的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的哪些实例属性或方法

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执行完后,会继续执行下一个文件

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.2、src/core/index.js

可以看出,这个文件,主要是给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

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

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

1.5、vue初始化总结

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

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

new Vue({
  router,
  store,
  render: h => h(App)
}).$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

// 把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生命周期走完以后,继续往下看

可以看出,这里判断了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,所以源码中的

肯定是不会走的。但是,用户在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首次渲染内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 详解vue3中渲染函数的非兼容变更

    目录 渲染函数API变更 Render函数参数 渲染函数签名更改 VNode Props 格式化 slot统一 移除$listeners $attrs现在包括class和style 渲染函数API变更 此更改不会影响到<template>用户 h现在全局导入,而非作为参数传递给渲染函数 渲染函数参数更改为在有状态组件和函数组件之间更加一致 vnode现在又一个扁平的prop结构 Render函数参数 // 2.0 渲染函数 export default { render(h) { return

  • Vue绑定对象与数组变量更改后无法渲染问题解决

    项目场景: 在页面显示上有一个<ul>标签,我们需要动态进行列表数据的展示,而由于我们的页面上除了列表值,还有其他的值要进行展示,因此列表数据的数据结构是某个对象下面的数组,在动态修改数据后发现没有进行自动渲染. 问题描述: 在点击按钮"click me!"时虽然数据有变化且在控制台进行了输出,但是列表数据并没有发生渲染. 代码如下: <script src="https://unpkg.com/vue/dist/vue.js"></s

  • vue的ssr服务端渲染示例详解

    为什么使用服务器端渲染 (SSR) 更好的 SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面. 请注意,截至目前,Google 和 Bing 可以很好对同步 JavaScript 应用程序进行索引.在这里,同步是关键.如果你的应用程序初始展示 loading 菊花图,然后通过 Ajax 获取内容,抓取工具并不会等待异步完成后再行抓取页面内容.也就是说,如果 SEO 对你的站点至关重要,而你的页面又是异步获取内容,则你可能需要服务器端渲染(SSR)解决此问题. 更快的内容到达时间 (ti

  • Vue单页面应用中实现Markdown渲染

    之前渲染 Markdown 的时候, 笔者使用的是 mavonEditor 的预览模式, 使用起来比较爽, 只需要引入组件即可, 但是在最近的开发中, 遇到了困难. 主要问题在于作为单页面应用, 站内链接必须是使用 router-link 跳转, 如果使用 mavonEditor 默认渲染的 a 标签, 就会重新加载页面, 用户体验较差. 动态渲染 想要实现在前端动态地根据用户内容渲染router-link , 需要使用动态渲染, 根据 官方文档, 直接修改vue.config.js 即可: /

  • vue+elementUI组件递归实现可折叠动态渲染多级侧边栏导航

    早就实现了功能,但是发现点击的时候,选中的菜单项背景色会变白,周五时候仔细观察了一下,发现并不是调整样式的问题,而是选项没有被选中,于是好好研究了一下组件递归这块,总结记录一下心路历程 一.概念 递归:递归其实说白了,就是自己调用自己,样子就像是套娃一个套一个的,小时候玩过一个游戏汉诺塔就是利用的递归原理: 函数递归:函数利用函数名还调用自己 组件递归:所以组件递归利用的是vue组件中的name属性来实现的 二.需求 实现可折叠动态渲染多级侧边栏导航 三.分析 1.观察到侧边栏导航是一级一级的,

  • 浅谈vue首次渲染全过程

    目录 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.

  • 浅谈vue单一组件下动态修改数据时的全部重渲染

    今天在学习vue的过程中,发现一个有趣的现象. 在某一组件下的某一数据通过点击事件被动态修改的时候,对应view中的数据同步的进行了修改,没错,这不是废话吗,vue的一大特色就是数据的双向绑定.可有趣的是,该组件下我写的另一个用Math.random()的data值对应的值和视图也发生了变化 这就让我这个刚入门的小白有点奇怪了,我修改一个,怎么变了两个????脑洞放开一想,会不会数据在双向同步的时候,发生了什么,比如.是不是只要有一个节点变了,node都重新进行了加载??? 我想这其中的缘由必定

  • 浅谈vue异步数据影响页面渲染

    今天遇到一个问题,要保证页面渲染前请求的数据已经得到了 由于user是在异步请求之后保存在session中,而在页面渲染时session中还没有user,页面直接报错. 因此我希望能在所有请求都得到后再去做页面的渲染. 1.先把id为app的div用v-if="appShow",定义appShow为false进行隐藏,避免渲染 2.写计数器,每1ms进行一次查询,如果session中已经有user,删除过滤器,移除滤布,appShow为true,开始渲染页面,这样可以保证页面的正常渲染

  • 浅谈Vue.js 1.x 和 2.x 实例的生命周期

    在Vue.js中,在实例化Vue之前,它们都是以HTML的文本形式存在文本编辑器中.当实例化后将经历创建.编译.销毁三个主要阶段. 以下是Vue.js 1.x 实例的生命周期图示: Vue.js 1.x 的生命周期钩子 1. init 在实例开始初始化时同步调用.此时数据观测.事件和Watcher都尚未初始化. 2. created 在实例化创建之后同步调用.此时实例已经结束解析选项,已建立:数据绑定.计算属性.方法.Watcher/事件回调,但是还没有开始DOM编译,$el还不存在. 3. b

  • 浅谈vue首屏加载优化

    本文介绍了浅谈vue首屏加载优化,分享给大家,具体如下: 库使用情况 vue vue-router axios muse-ui material-icons vue-baidu-map 未优化前 首先我们在正常情况下build 优化 1. 按需加载 当前流行的UI框架如iview,muse-ui,Element UI都支持按需加载,只需稍微改动一下代码. 修改前: import MuseUI from 'muse-ui' import 'muse-ui/dist/muse-ui.css' imp

  • 浅谈vue的第一个commit分析

    为什么写这篇vue的分析文章? 对于天资愚钝的前端(我)来说,阅读源码是件不容易的事情,毕竟有时候看源码分析的文章都看不懂.每次看到大佬们用了1-2年的vue就能掌握原理,甚至精通源码,再看看自己用了好几年都还在基本的使用阶段,心中总是羞愧不已.如果一直满足于基本的业务开发,怕是得在初级水平一直待下去了吧.所以希望在学习源码的同时记录知识点,可以让自己的理解和记忆更加深刻,也方便将来查阅. 目录结构 本文以vue的第一次 commit a879ec06 作为分析版本 ├── build │ └─

  • 浅谈vue单页面中有多个echarts图表时的公用代码写法

    html中: <div class="charts1"/> <div class="charts2"/> <div class="charts3"/> <div class="charts4"/> <div class="charts5"/> <div class="charts6"/> <div class=

  • 浅谈vue 多个变量同时赋相同值互相影响

    首先,该项目用到了element-ui中的Tabs 标签:然后来龙去脉是酱紫的: 一个项目中需动态渲染一个列表billItemLIsts,列表中包含n组小列表,其中小列表的state_pj用于改变该小列表中的radio,于是: <el-tabs tab-position="left" style="max-height:280px;"> <el-tab-pane v-for="(itema,index) in billItemLIsts&

  • 浅谈vue生命周期共有几个阶段?分别是什么?

    一共8个阶段 1.beforeCreate(创建前) 2.created(创建后) 3.beforeMount(载入前) 4.mounted(载入后) 5.beforeUpdate(更新前) 6.updated(更新后) 7.beforeDestroy(销毁前) 8.destroyed(销毁后) vue第一次页面加载会触发哪几个钩子函数? beforeCreate.created.beforeMount.mounted DOM 渲染在哪个周期中就已经完成? mounted 补充知识:记录一次vu

  • 浅谈Vue使用Cascader级联选择器数据回显中的坑

    业务场景 由于项目需求,需要对相关类目进行多选,类目数据量又特别大,业务逻辑是使用懒加载方式加载各级类目数据,编辑时回显用户选择的类目. 问题描述 使用Cascader级联选择器过程中主要存在的应用问题如下: 1.由于在未渲染节点数据的情况下编辑时无法找到对应的类目数据导致无法回显,如何自动全部加载已选择类目的相关节点数据: 2.提前加载数据后,点击相应父级节点出现数据重复等: 3.使用多个数据源相同的级联选择器,产生只能成功响应一个加载子级节点数据: 4.Vue中级联选择器相应数据完成加载,依

随机推荐