Vue组件注册原理-为什么可以使用自定义标签

                                    Vue组件注册原理

一、Vue组件的注册

    在Vue中使用组件,可能需要用到注册组件,它提供了全局注册和局部注册两种方式。

//全局注册
Vue.component('my-component-name', { /* ... */ })

//局部注册
new Vue({
  el: '#app',
  components: {
    'component-a': ComponentA
  }
})

    于是你会看到这种标签:<button-counter>,它并不是合法的HTML标签,不能直接new VNode()创建vnode,却可以跟普通的HTML一样在模板中直接使用。那么,当Vue解析到自定义的组件标签时是如何处理的呢?

<!--使用组件-->
<div id="components-demo">
  <button-counter></button-counter>
  <button-counter></button-counter>
  <button-counter></button-counter>
</div>

 

二、全局注册的实现

    全局注册方法有两种写法,如下所示,我们以Vue.component('my-component', Vue.extend({ /* ... */ }))为例,来描述组件的注册过程。

// 注册组件,传入一个扩展过的构造器
Vue.component('my-component', Vue.extend({ /* ... */ }))

// 注册组件,传入一个选项对象 (自动调用 Vue.extend)
Vue.component('my-component', { /* ... */ })

    本文使用的源码版本2.6.12:

1、初始化调用initGlobalAPI

    initGlobalAPI 初始化了 Vue 的大部分静态成员,并通过调用initAssetRegisters()进行组件注册。(源码位置:src/core/global-api/index.js)

    initGlobalAPI 主要做了这几件事情:

    1)劫持了Vueconfig属性

    2)定义 Vue.util一些公用的方法,但是不推荐外部使用

    3)初始化了 Vue.options

    4)注册内置组件 keep-alive

    5)为Vue注册了几个静态方法:Vue.use()、Vue.mixin()、Vue.extend、Vue.component、Vue.directive、Vue.filter

import config from '../config'
import { initUse } from './use'
import { initMixin } from './mixin'
import { initExtend } from './extend'
import { initAssetRegisters } from './assets'
import { set, del } from '../observer/index'
import { ASSET_TYPES } from 'shared/constants'
import builtInComponents from '../components/index'
import { observe } from 'core/observer/index'

import {
  warn,
  extend,
  nextTick,
  mergeOptions,
  defineReactive
} from '../util/index'

export function initGlobalAPI (Vue: GlobalAPI) {
    // config
    var configDef = {};
    configDef.get = function () { return config; }; //定义 config 的属性描述符
    {
      configDef.set = function () { //开发环境增加一个 set 方法,禁止给 config 属性直接赋值
        warn(// 警告:不要对Vue.config重新赋值,可以挂载或修改对象中的属性。
          'Do not replace the Vue.config object, set individual fields instead.'
        );
      };
    }
    //初始化 Vue.config 对象
    Object.defineProperty(Vue, 'config', configDef);

    // exposed util methods. 定义 Vue.util:一些公用的方法
    // NOTE: these are not considered part of the public API - avoid relying on 这些工具方法不视作全局 API 的一部分,除非你已经意识到某些风险,否则不要去依赖它们
    // them unless you are aware of the risk. 调用这个方法可能会发生意外,不建议外部使用
    Vue.util = { //挂载了一些API
      warn: warn,
      extend: extend,
      mergeOptions: mergeOptions,
      defineReactive: defineReactive$$1
    };

    //定义了常用的全局方法
    Vue.set = set;
    Vue.delete = del;
    Vue.nextTick = nextTick;

    // 2.6 explicit observable API 定义 observable 方法:让一个对象可响应
    Vue.observable = function (obj) {
      observe(obj);
      return obj
    };
    //初始化了 Vue.options 挂载3个成员,并初始化为空对象
    Vue.options = Object.create(null); //没有原型,可以提高性能
    ASSET_TYPES.forEach(function (type) {
      Vue.options[type + 's'] = Object.create(null);
    });

    // this is used to identify the "base" constructor to extend all plain-object
    // components with in Weex's multi-instance scenarios.
    Vue.options._base = Vue; //在Vue.options._base中记录 Vue 的构造函数

    extend(Vue.options.components, builtInComponents); //注册内置组件 keep-alive

    initUse(Vue); //注册 Vue.use() 用来注册插件
    initMixin$1(Vue); // 注册 Vue.mixin() 实现混入
    initExtend(Vue); // 注册 Vue.extend() 基于传入的options返回一个组件的构造函数
    initAssetRegisters(Vue); // 注册 Vue.directive()、Vue.component()、Vue.filter()
  }

2、调用initUse(Vue)-注册 Vue.use()

    注册插件。 (源码位置: vue/src/core/global-api/use.js)

import { toArray } from '../util/index'

export function initUse (Vue: GlobalAPI) {
  // 定义use,接收一个参数:插件
  Vue.use = function (plugin: Function | Object) {
    // 定义一个常量,获取已注册的插件
    // Vue实例中的_installedPlugins存储所有已注册的插件
    const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
    // 判断插件是否注册,防止重复注册。
    if (installedPlugins.indexOf(plugin) > -1) {
      return this
    }

    // additional parameters
    // 获取注册插件用的参数,去除参数1(插件)
    // toArray:用于将参数1转化为数组,并去除前n(参数2)个元素
    const args = toArray(arguments, 1)
    // 把 Vue 插入到第一个元素的位置
    args.unshift(this)
    // 如果插件参数是一个包含install方法的对象,执行install方法
    // 如果插件参数是一个函数,直接执行
    if (typeof plugin.install === 'function') {
      plugin.install.apply(plugin, args)
    } else if (typeof plugin === 'function') {
      plugin.apply(null, args)
    }
    // 将插件添加到已安装的记录中,避免重复安装
    installedPlugins.push(plugin)
    return this
  }
}

     很多UI库中用Vue.use(Vue.use(plugin),plugin格式为{install:function(){}},则运行install方法,若plugin本身是function则直接运行)来完成组件注册(Vue.component才是真正的去注册组件),默认的项目中router/index.js中也使用了Vue.use(Router)。其实也是在plugin的install方法中去执行Vue.component罢了。

    为什么要使用Vue.use?Vue.component只能一个一个注册组件。Vue.use(plugin)可以注册多个组件,并且install可以做更多的事情。

MyPlugin.install=function(Vue,options){
  //1.添加全局方法或属性
  Vue.myGlobalMethod=function(){//逻辑...
  }
  //2.注册组件,可以同时注册多个
  Vue.component()
  //3.添加全局资源
  Vue.directive('my-directive',{bind(el,binding,vnode,oldVnode){
    //逻辑...
  }})
  //4.注入组件选项
  Vue.mixin({created:function(){//逻辑...
  }})
  //5.添加实例方法
  Vue.prototype.$myMethod=function(methodOptions){
    //逻辑...
  }
  //...
}

3、调用initMixin$1(Vue)-注册 Vue.initMixin()

   注册混入,将传入的对象中的成员,拷贝到 Vue.options。(源码位置: vue/src/core/global-api/mixin.js)

import { mergeOptions } from '../util/index'

export function initMixin (Vue: GlobalAPI) {
  Vue.mixin = function (mixin: Object) {
    // 将传入的对象中的成员,拷贝到 Vue.options 中,所以 mixin 注册的选项,是全局注册
    this.options = mergeOptions(this.options, mixin)
    return this
  }
}

4、调用initExtend(Vue)-注册 Vue.extend()

    Vue.extend() 返回一个组件的构造函数(一个带有附加Option的vue构造器,这个构造器被命名为Sub,等待render时候初始化),用于自定义组件。(源码位置: vue/src/core/global-api/extend.js)

    子类将获得:Sub.cid 、Sub.options、Sub.extend、Sub.mixin、Sub.use、

                         Sub.super  // 父类

                         Sub.superOptions // 父类选项

                         Sub.extendOptions  // 传入子类选

                         Sub.sealedOptions // 子类目前的所有选项(父+自己)

/* @flow */

import { ASSET_TYPES } from 'shared/constants'
import { defineComputed, proxy } from '../instance/state'
import { extend, mergeOptions, validateComponentName } from '../util/index'

export function initExtend (Vue: GlobalAPI) {
  /**
   * Each instance constructor, including Vue, has a unique 每个实例构造函数(包括Vue)都又一个唯一的 cid。
   * cid. This enables us to create wrapped "child
   * constructors" for prototypal inheritance and cache them. 这使我们能够创建用于原型继承的 child 构造函数并缓存它们。
   */
  Vue.cid = 0
  let cid = 1

  /**
   * Class inheritance
   */
  Vue.extend = function (extendOptions: Object): Function {
    extendOptions = extendOptions || {}
    const Super = this  // 获取 Vue 构造函数
    const SuperId = Super.cid
     // 先判断是否可以从缓存中获取
    const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
    if (cachedCtors[SuperId]) {
      return cachedCtors[SuperId]
    }
    //如果有name属性,检验name拼写是否合法
    const name = extendOptions.name || Super.options.name
    if (process.env.NODE_ENV !== 'production' && name) {
      validateComponentName(name)
    }
    // 创建组件的构造函数
    const Sub = function VueComponent (options) {
      this._init(options) // 内部调用 _init() 初始化
    }
    // 改造构造函数的原型,使其继承自 Vue,所以所有的 Vue 组件都继承自 Vue
    Sub.prototype = Object.create(Super.prototype)
    Sub.prototype.constructor = Sub
    Sub.cid = cid++
    Sub.options = mergeOptions( // 合并选项
      Super.options,
      extendOptions
    )
    Sub['super'] = Super

    // For props and computed properties, we define the proxy getters on
    // the Vue instances at extension time, on the extended prototype. This
    // avoids Object.defineProperty calls for each instance created.
    //将 Vue 的成员拷贝到 Sub 组件中
    if (Sub.options.props) { 
      initProps(Sub)
    }
    if (Sub.options.computed) { 
      initComputed(Sub)
    }

    // allow further extension/mixin/plugin usage
    Sub.extend = Super.extend
    Sub.mixin = Super.mixin
    Sub.use = Super.use

    // create asset registers, so extended classes
    // can have their private assets too.
    ASSET_TYPES.forEach(function (type) {
      Sub[type] = Super[type]
    })
    // enable recursive self-lookup
    if (name) {
      Sub.options.components[name] = Sub
    }

    // keep a reference to the super options at extension time.
    // later at instantiation we can check if Super's options have
    // been updated.
    Sub.superOptions = Super.options
    Sub.extendOptions = extendOptions
    Sub.sealedOptions = extend({}, Sub.options)

    // cache constructor 缓存组件构造器在extendOptions上
    cachedCtors[SuperId] = Sub
    return Sub // 返回构造函数
  }
}

function initProps (Comp) {
  const props = Comp.options.props
  for (const key in props) {
    proxy(Comp.prototype, `_props`, key)
  }
}

function initComputed (Comp) {
  const computed = Comp.options.computed
  for (const key in computed) {
    defineComputed(Comp.prototype, key, computed[key])
  }
}

5、调用initAssetRegisters(Vue)

   注册 Vue.directive()、Vue.component()、Vue.filter()。全局注册组件就是Vue实例化前创建一个基于Vue的子类构造器,并将组件的信息加载到实例options.components对象中。 (源码位置: vue/src/core/global-api/assets.js)

   Vue.directive():为Vue实例添加指令,注册后指令函数被 bind 和 update 调用

   Vue.component():传来的component作为子类继承到父类Vue实例上。执行this.options._base.extend(definition),其实就是Vue.extend。

   Vue.filter():用于注册或获取全局过滤器。

/* @flow */

import { ASSET_TYPES } from 'shared/constants'
import { isPlainObject, validateComponentName } from '../util/index'

export function initAssetRegisters (Vue: GlobalAPI) {
  /**
   * Create asset registration methods.
   */
  ASSET_TYPES.forEach(type => { //ASSET_TYPES 指的就是组件component,指令directive,过滤器filter 在constants.js中定义
    Vue[type] = function (
      id: string,
      definition: Function | Object
    ): Function | Object | void {
      if (!definition) { //判断是否有第二个参数,如果未传入definition,则视为获取该资源并返回Vue.component()的第一个参数作为组件名,当组件选项有name属性时,则用name属性覆盖。当Vue.component()不传递第二个选项参数时,会返回已经注册过的子类构造器。
        return this.options[type + 's'][id]
      } else { // 否则视为注册资源
        /* istanbul ignore if */ // 非生产环境下给出检验组件名称的错误警告
        if (process.env.NODE_ENV !== 'production' && type === 'component') {
          validateComponentName(id)
        }
        // 如果是注册component,并且definition是对象类型
        if (type === 'component' && isPlainObject(definition)) {
          definition.name = definition.name || id // 设置definition.name属性
          definition = this.options._base.extend(definition) // 在initGlobalAPI方法中,执行了initExtend(Vue),所以Vue上有extend方法。调用Vue.extend扩展定义,并重新赋值。this.options._base.extend其实就是Vue.extend
        }
        // 如果是注册directive且definition为函数
        if (type === 'directive' && typeof definition === 'function') {
          definition = { bind: definition, update: definition } // 重新定义definition为格式化的对象
        }
        //将构造器赋值给 this.options[‘component’+ 's'][id]
        //全局的组件,指令和过滤器,统一挂在Vue.options上。在init的时候利用mergeOptions合并策略侵入实例,供实例使用。
        this.options[type + 's'][id] = definition 
        return definition // 返回definition
      }
    }
  })
}

三、测试案例

    编写一个组件,大概可以分为三步:定义(编写)组件、引用(注册)组件、使用组件。

1、定义(编写)组件

    首先,我们在components目录中新建一个button-counter目录,再新建ButtonCounter.vue文件,编写我们的组件。

<template>
  <div>我是一个按钮组件
    <button @click="handle">you have click me {{count}} times</button>
  </div>
</template>

<script>
export default {
  data () {
    return {
      count: 0
    }
  },
  methods: {
    handle () {
      this.count += 1
    }
  }
}
</script>

<style scoped>
</style>

2、引用(注册)组件

     前面我们介绍过vue项目结构及启动文件加载过程。其中就介绍了:main.js是我们的入口文件,主要作用是初始化vue实例,并引入所需要的插件,会用App.vue替换index.html中的id='app'的div。为了使用组件,我们需要对其进行注册。

    // 引入组件
    import buttonAdd from './components/button-counter/ButtonCounter.vue'
   // 注册全局组件
    Vue.component('button-counter', buttonAdd)

// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import router from './router'
// 引入组件
import buttonAdd from './components/button-counter/ButtonCounter.vue'
// 注册全局组件
Vue.component('button-counter', buttonAdd)
/* eslint-disable no-new */
new Vue({
  el: '#app',
  router,
  components: { App },
  template: '<App/>'
})

3、使用组件

    在HelloWorld.vue中直接使用我们注册的组件button-counter。

<template>
  <div class="hello">
    <button-counter></button-counter>
  </div>
</template>

<script>
export default {
  name: 'HelloWorld'
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

运行效果:

    initAssetRegisters完成之后,options下挂载了全局组件button-counter,输出Vm.options.components和vm.$options.components都可以访问到:

4、局部注册组件

    我们直接在App.vue里面引入并使用局部组件。

<template>
  <div id="app">
    <!-- <img src="./assets/logo.png"> -->
    <router-view />
    <button-counter></button-counter>
  </div>
</template>

<script>
/*
  1、引入组件

  2、挂载组件

  3、在模板中使用
  */
import btCount from './components/button-counter/ButtonCounter.vue'

export default {
  name: 'App',
  components: {
    'button-counter': btCount
  }
}
</script>

<style>
#app {
  font-family: "Avenir", Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

运行效果:

    局部注册和全局注册不同的是,只有该类型的组件才可以访问局部注册的子组件。全局注册组件就是Vue实例化前创建一个基于Vue的子类构造器,并将组件的信息加载到实例options.components对象中。在所有组件创建的过程中,全局组件都会扩展到当前组件的 vm.$options.components 下,这就是全局注册的组件能被任意使用的原因。 

源码分析:

    前面也介绍过初始化的时候回加载此方法:initMixin(),初始化时会判断if (options && options._isComponent) ,并调用initInternalComponent(vm, options);方法

function initMixin (Vue) {
  	console.log(111);
  	console.log(uid$3);
    Vue.prototype._init = function (options) {
      var vm = this;
      // a uid
      vm._uid = uid$3++;

      var startTag, endTag;
      /* istanbul ignore if */
      if (config.performance && mark) {
        startTag = "vue-perf-start:" + (vm._uid);
        endTag = "vue-perf-end:" + (vm._uid);
        mark(startTag);
      }

      // a flag to avoid this being observed
      vm._isVue = true;
      // merge options
      if (options && options._isComponent) {
        // optimize internal component instantiation
        // since dynamic options merging is pretty slow, and none of the
        // internal component options needs special treatment.
        initInternalComponent(vm, options);
      } else {
        vm.$options = mergeOptions(
          resolveConstructorOptions(vm.constructor),
          options || {},
          vm
        );
      }
      /* istanbul ignore else */
      {
        initProxy(vm);
      }
      // expose real self
      vm._self = vm;
      initLifecycle(vm);
      initEvents(vm);
      initRender(vm);
      callHook(vm, 'beforeCreate');
      initInjections(vm); // resolve injections before data/props
      initState(vm);
      initProvide(vm); // resolve provide after data/props
      callHook(vm, 'created');

      /* istanbul ignore if */
      if (config.performance && mark) {
        vm._name = formatComponentName(vm, false);
        mark(endTag);
        measure(("vue " + (vm._name) + " init"), startTag, endTag);
      }

      if (vm.$options.el) {
        vm.$mount(vm.$options.el);
      }
    };
  }

    调用 initInternalComponent (vm, options) 方法。Object.create(vm.constructor.options);继承了构造函数的所有选项

function initInternalComponent (vm, options) {
    //在所有组件创建的过程中,都会从全局的 Vue.options.components 扩展到当前组件的 vm.$options.components 下,这就是全局注册的组件能被任意使用的原因。
    var opts = vm.$options = Object.create(vm.constructor.options);
    // doing this because it's faster than dynamic enumeration.
    var parentVnode = options._parentVnode;
    opts.parent = options.parent;
    opts._parentVnode = parentVnode;

    var vnodeComponentOptions = parentVnode.componentOptions;
    opts.propsData = vnodeComponentOptions.propsData;
    opts._parentListeners = vnodeComponentOptions.listeners;
    opts._renderChildren = vnodeComponentOptions.children;
    opts._componentTag = vnodeComponentOptions.tag;

    if (options.render) {
      opts.render = options.render;
      opts.staticRenderFns = options.staticRenderFns;
    }
  }

四、组件Vnode创建及其源码解析

    组件注册过后,会在实例options.components增加一个组件的配置属性,这个属性是一个子的Vue构造器。然而什么时候进行实例化并挂载,又是如何渲染的呢?如果用一句话概括挂载的过程,可以描述为确认挂载节点,编译模板为render函数,渲染函数转换Virtual DOM,创建真实节点。大致流程如下图所示:

模板:Vue的模板基于纯HTML,基于Vue的模板语法,我们可以比较方便地声明数据和UI的关系。

AST:AST是Abstract Syntax Tree的简称,Vue使用HTML的Parser将HTML模板解析为AST,并且对AST进行一些优化的标记处理,提取最大的静态树,方便Virtual DOM时直接跳过Diff。

渲染函数(render):渲染函数是用来生成Virtual DOM的。Vue推荐使用模板来构建我们的应用界面,在底层实现中Vue会将模板编译成渲染函数,当然我们也可以不写模板,直接写渲染函数,以获得更好的控制。

Virtual DOM:虚拟DOM树,Vue的Virtual DOM Patching算法是基于Snabbdom的实现,并在些基础上作了很多的调整和改进。

Watcher:每个Vue组件都有一个对应的watcher,这个watcher将会在组件render的时候收集组件所依赖的数据,并在依赖有更新的时候,触发组件重新渲染,Vue会自动优化并更新要更新的UI。

1、实例化并挂载

    在源码的src/platforms/web下面放着不同版本的构建entry文件,在入口文件中引入了src/platforms/runtime/index。文件中export的Vue,都是从src/core/instance/index这个文件import过来的。Vue的本质是一个构造器,并且它保证了只能通过new实例的形式去调用,而不能直接通过函数的形式使用。

    src/core/instance/index引入时就定义了很多原型方法:

  1)initMixin(Vue); // 定义Vue原型上的init方法(内部方法)

  2)stateMixin(Vue); // 定义原型上跟数据相关的属性方法($set $get $delete $watch 等

  3)eventsMixin(Vue); //定义原型上跟事件相关的属性方法

  4)lifecycleMixin(Vue); // 定义原型上跟生命周期相关的方法

  5)renderMixin(Vue); // 定义渲染相关的函数(返回虚拟dom

    调用initGlobalAPI定义静态属性方法。

//源码文件 src/platforms/runtime/index
import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
import { isServerRendering } from 'core/util/env'
import { FunctionalRenderContext } from 'core/vdom/create-functional-component'

initGlobalAPI(Vue)

Object.defineProperty(Vue.prototype, '$isServer', {
  get: isServerRendering
})

Object.defineProperty(Vue.prototype, '$ssrContext', {
  get () {
    /* istanbul ignore next */
    return this.$vnode && this.$vnode.ssrContext
  }
})

// expose FunctionalRenderContext for ssr runtime helper installation
Object.defineProperty(Vue, 'FunctionalRenderContext', {
  value: FunctionalRenderContext
})

Vue.version = '__VERSION__'

export default Vue
//src/core/instance/index
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

    当实例化时,会调用this._init(options)方法,这个是initMixin(Vue)初始化时定义的。如果选项中有挂载的元素,则执行 vm.$mount(vm.$options.el)进行挂载。

/* @flow */

import config from '../config'
import { initProxy } from './proxy'
import { initState } from './state'
import { initRender } from './render'
import { initEvents } from './events'
import { mark, measure } from '../util/perf'
import { initLifecycle, callHook } from './lifecycle'
import { initProvide, initInjections } from './inject'
import { extend, mergeOptions, formatComponentName } from '../util/index'

let uid = 0

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    let startTag, endTag
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }

    // a flag to avoid this being observed
    vm._isVue = true
    // merge options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${vm._name} init`, startTag, endTag)
    }

    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

     而$mount方法是在runtime/index.js 中定义的,这里调用了mountComponent方法。

// public mount method
Vue.prototype.$mount = function (
  el?: string | Element,  //要挂载的元素
  hydrating?: boolean  // 服务端渲染相关参数
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)  //位于core/instance/lifecycle.js
}

2、挂载组件mountComponent

    在 mountComponent 方法里实例化了一个渲染 Watcher,并且传入了一个 updateComponent ,这个方法:() => { vm._update(vm._render(), hydrating) } 首先使用 _render 方法生成 VNode,再调用 _update 方法更新DOM

//core/instance/lifecycle.js
...
export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
    if (process.env.NODE_ENV !== 'production') {
      /* istanbul ignore if */
      if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
        vm.$options.el || el) {
        warn(
          'You are using the runtime-only build of Vue where the template ' +
          'compiler is not available. Either pre-compile the templates into ' +
          'render functions, or use the compiler-included build.',
          vm
        )
      } else {
        warn(
          'Failed to mount component: template or render function not defined.',
          vm
        )
      }
    }
  }
  callHook(vm, 'beforeMount') // 调用beforeMount钩子

//渲染watcher,当数据更改,updateComponent作为Watcher对象的getter函数,用来依赖收集,并渲染视图
  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      const name = vm._name
      const id = vm._uid
      const startTag = `vue-perf-start:${id}`
      const endTag = `vue-perf-end:${id}`

      mark(startTag)
      const vnode = vm._render()
      mark(endTag)
      measure(`vue ${name} render`, startTag, endTag)

      mark(startTag)
      vm._update(vnode, hydrating)
      mark(endTag)
      measure(`vue ${name} patch`, startTag, endTag)
    }
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
 // 渲染watcher, Watcher 在这里起到两个作用,一个是初始化的时候会执行回调函数
  // 另一个是当 vm 实例中的监测的数据发生变化的时候执行回调函数
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  //vm.$vnode 表示 Vue 实例的父虚拟 Node,所以它为 Null 则表示当前是根 Vue 的实例
  if (vm.$vnode == null) {
    vm._isMounted = true  // 表示这个实例已经挂载
    callHook(vm, 'mounted')  // 调用mounted钩子
  }
  return vm
}
...

3、执行渲染函数vm._render()

    vm._render会执行前面生成的render渲染函数,并生成一个Virtual Dom tree。而vm._render是在initRender(vm)时定义的。其中 vm._c 是template内部编译成render函数时调用的方法,即调用createElement。

// src/core/instance/render.js
...
export function initRender (vm: Component) {
  vm._vnode = null // the root of the child tree
  vm._staticTrees = null // v-once cached trees
  const options = vm.$options
  const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
  const renderContext = parentVnode && parentVnode.context
  vm.$slots = resolveSlots(options._renderChildren, renderContext)
  vm.$scopedSlots = emptyObject
  // bind the createElement fn to this instance
  // so that we get proper render context inside it.
  // args order: tag, data, children, normalizationType, alwaysNormalize
  // internal version is used by render functions compiled from templates
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)  //其中 vm._c 是template内部编译成render函数时调用的方法
  // normalization is always applied for the public version, used in
  // user-written render functions.
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true) //vm.$createElement是手写render函数时调用的方法

  // $attrs & $listeners are exposed for easier HOC creation.
  // they need to be reactive so that HOCs using them are always updated
  const parentData = parentVnode && parentVnode.data

  /* istanbul ignore else */
  if (process.env.NODE_ENV !== 'production') {
    defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => {
      !isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
    }, true)
    defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => {
      !isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)
    }, true)
  } else {
    defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
    defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
  }
}

4、执行_createElement

    createElement 方法实际上是对 _createElement 方法的封装。_createElement做了一些判断,然后使用new VNode创建虚拟节点,若果是子组件则继续调用createComponent

// wrapper function for providing a more flexible interface
// without getting yelled at by flow
export function createElement (
  context: Component, // vm 实例
  tag: any, // 标签
  data: any, // 节点相关数据,属性
  children: any, // 子节点
  normalizationType: any,
  alwaysNormalize: boolean // 区分内部编译生成的render还是手写render
): VNode | Array<VNode> {
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE
  }
  return _createElement(context, tag, data, children, normalizationType) // 真正生成Vnode的方法
}

export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  if (isDef(data) && isDef((data: any).__ob__)) {
    process.env.NODE_ENV !== 'production' && warn(
      `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
      'Always create fresh vnode data objects in each render!',
      context
    )
    return createEmptyVNode()
  }
  // object syntax in v-bind
  if (isDef(data) && isDef(data.is)) {
    tag = data.is
  }
  if (!tag) {
    // in case of component :is set to falsy value
    return createEmptyVNode()
  }
  // warn against non-primitive key
  if (process.env.NODE_ENV !== 'production' &&
    isDef(data) && isDef(data.key) && !isPrimitive(data.key)
  ) {
    if (!__WEEX__ || !('@binding' in data.key)) {
      warn(
        'Avoid using non-primitive value as key, ' +
        'use string/number value instead.',
        context
      )
    }
  }
  // support single function children as default scoped slot
  if (Array.isArray(children) &&
    typeof children[0] === 'function'
  ) {
    data = data || {}
    data.scopedSlots = { default: children[0] }
    children.length = 0
  }
  if (normalizationType === ALWAYS_NORMALIZE) {
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children)
  }
  let vnode, ns
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    if (config.isReservedTag(tag)) { // 子节点的标签为普通的html标签,直接创建Vnode
      // platform built-in elements
      if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn)) {
        warn(
          `The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
          context
        )
      }
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) { // 子节点标签为注册过的组件标签名,则子组件Vnode的创建过程
      // component
      vnode = createComponent(Ctor, data, context, children, tag)  // 创建子组件Vnode
    } else {
      // unknown or unlisted namespaced elements
      // check at runtime because it may get assigned a namespace when its
      // parent normalizes children
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  } else {
    // direct component options / constructor
    vnode = createComponent(tag, data, context, children)
  }
  if (Array.isArray(vnode)) {
    return vnode
  } else if (isDef(vnode)) {
    if (isDef(ns)) applyNS(vnode, ns)
    if (isDef(data)) registerDeepBindings(data)
    return vnode
  } else {
    return createEmptyVNode()
  }
}

5、如果是组件则调用createComponent

    最终会通过new Vue实例化一个名称以vue-component-开头的Vnode节点。

export function createComponent (
  Ctor: Class<Component> | Function | Object | void, // 子类构造器
  data: ?VNodeData,
  context: Component, // vm实例
  children: ?Array<VNode>, // 子节点
  tag?: string
): VNode | Array<VNode> | void {
  if (isUndef(Ctor)) {
    return
  }

  const baseCtor = context.$options._base //Vue.options里的_base属性存储Vue构造器

  // plain options object: turn it into a constructor
  if (isObject(Ctor)) { // 针对局部组件注册场景
    Ctor = baseCtor.extend(Ctor)
  }

  // if at this stage it's not a constructor or an async component factory,
  // reject.
  if (typeof Ctor !== 'function') {
    if (process.env.NODE_ENV !== 'production') {
      warn(`Invalid Component definition: ${String(Ctor)}`, context)
    }
    return
  }

  // async component
  let asyncFactory
  if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor) 
    if (Ctor === undefined) {
      // return a placeholder node for async component, which is rendered
      // as a comment node but preserves all the raw information for the node.
      // the information will be used for async server-rendering and hydration.
      return createAsyncPlaceholder(
        asyncFactory,
        data,
        context,
        children,
        tag
      )
    }
  }

  data = data || {}

  // resolve constructor options in case global mixins are applied after
  // component constructor creation
  resolveConstructorOptions(Ctor) // 构造器配置合并

  // transform component v-model data into props & events
  if (isDef(data.model)) {
    transformModel(Ctor.options, data) 
  }

  // extract props
  const propsData = extractPropsFromVNodeData(data, Ctor, tag)

  // functional component
  if (isTrue(Ctor.options.functional)) {
    return createFunctionalComponent(Ctor, propsData, data, context, children)
  }

  // extract listeners, since these needs to be treated as
  // child component listeners instead of DOM listeners
  const listeners = data.on
  // replace with listeners with .native modifier
  // so it gets processed during parent component patch.
  data.on = data.nativeOn

  if (isTrue(Ctor.options.abstract)) {
    // abstract components do not keep anything
    // other than props & listeners & slot

    // work around flow
    const slot = data.slot
    data = {}
    if (slot) {
      data.slot = slot
    }
  }

  // install component management hooks onto the placeholder node
  installComponentHooks(data) // 挂载组件钩子

  // return a placeholder vnode
  const name = Ctor.options.name || tag
  const vnode = new VNode( // 创建子组件vnode,名称以 vue-component- 开头
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )

  // Weex specific: invoke recycle-list optimized @render function for
  // extracting cell-slot template.
  // https://github.com/Hanks10100/weex-native-directive/tree/master/component
  /* istanbul ignore if */
  if (__WEEX__ && isRecyclableComponent(vnode)) {
    return renderRecyclableComponentTemplate(vnode)
  }

  return vnode
}

6、vm.update()方法创建真实的DOM

    紧接着执行Vnode渲染真实DOM的过程,这个过程是vm.update()方法的执行,而其核心是vm.__patch__。vm.__patch__内部会通过 createElm去创建真实的DOM元素,期间遇到子Vnode会递归调用createElm方法。

// src/core/instance/lifecycle.js
export function lifecycleMixin (Vue: Class<Component>) {
  Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const restoreActiveInstance = setActiveInstance(vm)
    vm._vnode = vnode
    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.
    if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    restoreActiveInstance()
    // update __vue__ reference
    if (prevEl) {
      prevEl.__vue__ = null
    }
    if (vm.$el) {
      vm.$el.__vue__ = vm
    }
    // if parent is an HOC, update its $el as well
    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      vm.$parent.$el = vm.$el
    }
    // updated hook is called by the scheduler to ensure that children are
    // updated in a parent's updated hook.
  }

  Vue.prototype.$forceUpdate = function () {
    const vm: Component = this
    if (vm._watcher) {
      vm._watcher.update()
    }
  }

  Vue.prototype.$destroy = function () {
    const vm: Component = this
    if (vm._isBeingDestroyed) {
      return
    }
    callHook(vm, 'beforeDestroy')
    vm._isBeingDestroyed = true
    // remove self from parent
    const parent = vm.$parent
    if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
      remove(parent.$children, vm)
    }
    // teardown watchers
    if (vm._watcher) {
      vm._watcher.teardown()
    }
    let i = vm._watchers.length
    while (i--) {
      vm._watchers[i].teardown()
    }
    // remove reference from data ob
    // frozen object may not have observer.
    if (vm._data.__ob__) {
      vm._data.__ob__.vmCount--
    }
    // call the last hook...
    vm._isDestroyed = true
    // invoke destroy hooks on current rendered tree
    vm.__patch__(vm._vnode, null)
    // fire destroyed hook
    callHook(vm, 'destroyed')
    // turn off all instance listeners.
    vm.$off()
    // remove __vue__ reference
    if (vm.$el) {
      vm.$el.__vue__ = null
    }
    // release circular reference (#6759)
    if (vm.$vnode) {
      vm.$vnode.parent = null
    }
  }
}
// 创建真实dom
function createElm (vnode,insertedVnodeQueue,parentElm,refElm,nested,ownerArray,index) {
  ···
  // 递归创建子组件真实节点,直到完成所有子组件的渲染才进行根节点的真实节点插入
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return
  }
  ···
  var children = vnode.children;
  // 
  createChildren(vnode, children, insertedVnodeQueue);
  ···
  insert(parentElm, vnode.elm, refElm);
}
function createChildren(vnode, children, insertedVnodeQueue) {
  for (var i = 0; i < children.length; ++i) {
    // 遍历子节点,递归调用创建真实dom节点的方法 - createElm
    createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i);
  }
}// 创建真实dom
function createElm (vnode,insertedVnodeQueue,parentElm,refElm,nested,ownerArray,index) {
  ···
  // 递归创建子组件真实节点,直到完成所有子组件的渲染才进行根节点的真实节点插入
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return
  }
  ···
  var children = vnode.children;
  // 
  createChildren(vnode, children, insertedVnodeQueue);
  ···
  insert(parentElm, vnode.elm, refElm);
}
function createChildren(vnode, children, insertedVnodeQueue) {
  for (var i = 0; i < children.length; ++i) {
    // 遍历子节点,递归调用创建真实dom节点的方法 - createElm
    createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i);
  }
}

 

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

穆瑾轩

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值