vue首次渲染过程(粗析)

611 阅读7分钟

本文章主要简单介绍vue首次渲染过程到底做了什么事情。更多参考

Vue的运行过程

模板({{}}语法)—> ast(抽象语法树)—> f(x)(渲染函数)—>虚拟Dom—>DOM(diff算法)

vue初始化

首先就是main.js的import Vue from 'vue'
一般我们用vue/cli创建项目的时候用的是vue.runtime.esm.js运行时版本,引入vue时会引入vue.esm.js这个版本,而我们一般使用vue/cli是配合vue-loader来解析.vue文件为h函数,所以不需要带编译器的完整版

  • 完整版和运行时版本区别
    • 完整版和运行时版本区别就是完整版存在多一个编译器,大小比运行时版本多一个编译器。所以一般简单在demo引入选择带编译器的完整版,而配合webpack工具使用的vue/cli使用的为不带编译器版本,因为配合vue-loader一起使用,而vue-loader内部也是包含了vue-template-compiler可以负责把.vue文件转换为h函数
  • vue打包后的dist文件夹中不同版本主要解析:
    • 通用版本(UMD):完整版vue.js和运行时版本vue.runtime.js

    • Comnonjs版本:完整版vue.common.js和运行时版本vue.runtime.common.js

    • ESModule版本:完整版vue.esm.js和运行时版本vue.runtime.esm.js

vue类查找(src/core/instace/index.js)

vue文件加载顺序,本文按照完整版entry-runtime-with-compiler.js来说,不同的入口会生成不同的文件:

//执行流程为1234
//查找到vue类要从4321上找
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主要内容:

    image.png
    • 定义vue构造函数,并初始化vue的实例属性和实例方法,即在vue.prototype原型下新增各种方法和属性。总共的方法有下面图示:

    • initMixin方法:在vue原型上挂载_init方法,主要是初始化options对象

    • stateMixin方法:在vue原型上新增$data和$props两个属性、$set和$delete和$watch这三个方法

    • eventsMixin方法:在vue原型上新增$on、$once、$off、$emit四个方法

    • lifecycleMixin方法:在vue原型上新增_update(该方法主要调用__patch__方法去对比新旧节点从而 更新dom)、$forceUpdate和$destory三个方法

    • renderMixin方法:在vue原型上新增$nextTick和_render方法(该方法主要执行用户传入的render函数或者编译生成的render函数,并生成vnode)

  • src/core/index.js主要内容:

    image.png
    • initGlobalAPI方法:初始化很多vue静态方法和属性(nextTick,del,set等)
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)
}
  • src/platforms/web/runtime/index.js

    image.png

    • 新增__patch__方法,该方法主要为进行更新的时候进行新旧节点diff对比,然后更新dom操作

    • 新增$mount方法,该方法作用是渲染render函数,将render函数转换为虚拟dom

  • src/platforms/web/entry-runtime-with-compiler.js

    image.png

    • 最主要的作用就重写了vue原型下的$mount方法。为什么要重写,因为重写代码需要判断是否有render函数,如果没有render函数就会查找tempalte属性,而执行tempalte属性的内容就会使用到编译器进行编辑模板使用。而重写前的mount方法是只能给render函数使用。

vue初始化总结

上面的所有操作就是我们引入vue的时候,即import Vue from 'vue'进行的一系列操作,执行完这行代码后就会继续执行main.js文件的内容,就可以调用vue的构造函数。

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

主要执行了_init方法,从这里开始,vue的生命周期开始执行。_init方法主要函数为下面所示,更多请参考源码: image.png 生命钩子:

  • beforeCreate钩子:在该生命周期钩子之前,vue主要做的事情是给vue原型新增各种属性和方法,给vue新增各种静态属性和方法,以及给vm实例添加各种属性和方法
  • created钩子可以获取到data,methods等属性):在beforeCreated结束后主要执行了三个方法:initInjections, initState, initProvide,重点在initState方法,该方法中初始化了vm实例的$props,$data,$methods,comouted,watch等,同时还调用了一个initData()方法,该方法调用了Observer()方法,将data中的数据转换为响应式数据。因此在created生命周期之前,vm的$data等属性都会初始化完成,所以我们可以在created中调用data中的各种属性以及调用props或者methods各种方法等。
    // 把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')
    
  • created生命周期走完后,会进行vm.$options.el判断当前el是否传入,即
new  Vue({
   el:'#app',
   route,
   ...
})

的el属性。如果el存在,则继续执行$mount。如果不存在则不继续往下走,而要想代码继续往下找,必须执行该$mount方法。但是用户可以在new Vue({...}).$mount('#app')在new出来的vue实例去调用$mount

  • $mount函数,无论是new的时候把el传入还是实例后自己调用$mount(),都是执行$mount函数。
    • 使用的$mount()就是我们重写后的那个mount方法。重写前的mount方法是只能给render函数使用,即传入的new vue上的render属性,而传入的vue实例中如果没有render属性,会去寻找template属性(template->render),如果也没有才会找el属性(以el指定dom为模板时候,会把 el下的dom赋值给template,即el=>template=>render)。所以mount函数寻找模板优先级:render>template>el$mount最终目的都是转化为render函数挂载到options.render属性下。转化为render函数后,最终执行重写前的$mount。而该重写前的mount函数作用就是渲染render函数的,将render函数转换为虚拟dom。模板转换为render函数通过的为compileToFunctions()进行转换。
    • 拓展:render函数获取的三种途径
      • 用户自己传入render
        new Vue({
            router,
            store,
            render: h=>{
                h('div',{class: 'vdom'},[
                    h('p',['aaa'])
                ])
            }
        }).$mount('#app')
        
      • .vue文件编译成render,一般借助vue-loader的vue-template-compiler把.vue文件解析为render函数。
        new Vue({
            router,
            store,
            render: h=>h(App)
        }).$mount('#app')
        
      • template模板编译成render
      new Vue({
          router,
          store,
          template: '<div>哈哈哈</div>'
      }).$mount('#app')
      
  • beforeMount生命周期钩子(可以获取到render函数
    • 该钩子函数在重写前的mount函数中的return的mountComponnet函数中。

      image.png

      image.png

    • mountComponnet函数做了4件事。

      • 执行了beforeMount钩子,所以在beforeMount之前,我们就初始化和得到render函数。而beforeMount之后,才是开始render函数渲染成虚拟dom,然后更新真实dom。
      • updateComponent函数该函数执行了_update方法_update方法的第一个参数为vm._render()方法执行后的结果,即虚拟dom。_render方法作用为内部渲染了render函数称为虚拟dom,即返回一个vnode
        _update方法内部如图示,主要执行了__patch__方法进行新旧vnode的对比,找出差异,更新真实dom。如果首次渲染,则直接将当前的vnode生成真实dom。 image.png
      结论:updateComponent方法执行的_update方法,_update方法则调用__patch__实现新旧vnode对比,生成真实dom。updateComponent方法主要作用就是渲染render函数,更新dom,返回真实dom。
      • new Watcher,new一个Wacther实例的时候会传入updateComponent函数作为参数传入。然后执行Watcher构造函数如图所示。watcher分为3种,渲染watcher,$watch函数的watcher,computed的watcher。我们这里渲染页面的是渲染watcher。而这个渲染页面的watcher就是主vue实例的watcher,可以理解为整个页面的wacther。可以看出我们传入给Watcher实例参数的updateComponent方法(渲染render函数,并触发虚拟dom更新真实dom,返回真实dom)最终由get()方法触发,最后赋值给this.value,而this.value最后会用于更新依赖者。当我们调用this.$foceUpdate()时,就会触发这个实例的update方法,更新整个页面。new Wacher的时候 updateComponent会自动调用一次,即默认调用一个get方法,这就是我们的首次渲染。 image.png

      image.png

      • mounted钩子函数。执行完new Wacther后,代码继续往下执行,即图示。判断当前vnode为null,如果为null说明之前没有生成过虚拟dom,即首次渲染。此时,vm._isMounted设置为true。并执行mounted钩子函数。此时首次渲染完成。 image.png
  • mounted生命周期钩子 从beforeMount到mounted过程中,主要做了工作是(_render将render函数转换为虚拟dom_update将虚拟dom转换为真实dom并挂载):
  • 渲染render函数成为虚拟dom,该工作为重写的$mount来实现模板变成render,然后通过原来的$mount的mountComponnet的updateComponent的第一个参数vm._render()实现将render函数转换为虚拟dom
  • 执行updateComponnet,即会执行vm._update函数,将虚拟dom转换为真实dom 如果是beforeUpdate到update钩子之间,说明不是首次渲染,那么虚拟dom会有新旧两个。此时vm._update函数的作用就是对比新旧两个vnode,得出差异,更新需要更新的地方。

简述

  • 进行vue初始化,对于一些类似于v-if/v-model的指令,transition等组件以及挂载_patch_、$mount等方法的挂载
  • 然后实例化vue,即执行this._init(),该方法主要进行了以下操作
    • 调用第一个this.$mount,判断当前传入对象是否携带render,tempalte以及el,按照顺序进行获取,如果携带的不为render则需要进行模板编译,即通过complieToFunctions()进行模板编译为render()。
    • 经过第一个重写的mount调用之后,会进行第二个mount调用之后,会进行第二个mount的调用,该$mount返回的是一个this.mountComponent方法,如果没有携带render或者携带的是模板则进行提示,运行环境会有问题。否则则进行updateComponent方法的定义,然后之执行beforeMount钩子
    • 创建的this.updateComponent,该方法主要有_render()以及_update(),_render方法主要为把render函数变成虚拟DOM,_update方法主要为将虚拟DOM变为真实DOM,并挂载。
    • 最后new一个Watcher并调用get方法。每个组件都有一个watcher,该wacther传递的参数为updateComponent方法,而在get方法中,首先调用 pushTarget(this) 将当前 watcher 对象入栈,因为每一个组件都对应一个 watcher 对象,watcher 回去渲染视图,如果组件嵌套有多个内部组件,则要先渲染内部组件,所以需要先把父组件 watcher 入栈保存。
    • watcher调用的get方法,即内部会将传入的updateComponent会通过this.getter存储起来,最后调用this.get()来调用this.getter即调用传递的updateComponent进行更新操作

源码执行流程图示

  • vue自身初始化的全过程,即执行import Vue from 'vue'的时候做的事情 image.png
  • 初始化后在内存的结构为:(由于没有new所以实例对象还不存在)

image.png

  • new Vue()后:

image.png

参考文章

浅谈vue首次渲染全过程
Vue 首次渲染的过程(源码分析)