Vue.js源码解析-Vue初始化流程

2. 初始化相关代码分析

2.1 initGlobalAPI(Vue) 初始化Vue的全局静态API

平时开发通过 new Vue({...}) 去创建了根实例,当然在此之前,Vue已经做了一些前期的准备工作。Vue 的核心代码都在 src/core 目录中,我们先来看看 core/index.js 这个入口文件,这部分代码逻辑很简单。

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'

// 初始化全局API

initGlobalAPI(Vue)

// 下面代码是服务端ssr渲染使用,web端可以忽略

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 版本号这个静态变量

Vue.version = '__VERSION__'

export default Vue

我们主要关注的 initGlobalAPI(Vue) 这个函数,它定义在 core/global-api/index.js 文件中,主要给构造函数,添加诸如 Vue.set/delete/use/mixin/extend/component/directive/filter 这些静态方法。

/* @flow */

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

  // 这个是给 Vue 设置的 config 属性,不要手动的去替换这个对象,

  // 如果替换,vue 会给 warn 提示

  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.'

      )

    }

  }

  Object.defineProperty(Vue, 'config', configDef)


  // exposed util methods.

  // NOTE: these are not considered part of the public API - avoid relying on

  // them unless you are aware of the risk.

  Vue.util = {

    warn,

    extend,

    mergeOptions,

    defineReactive

  }

  // Vue的静态方法: Vue.set/delete/nextTick

  Vue.set = set

  Vue.delete = del

  Vue.nextTick = nextTick

  // 2.6 explicit observable API

  Vue.observable = <T>(obj: T): T => {

    observe(obj)

    return obj

  }


  Vue.options = Object.create(null)

  ASSET_TYPES.forEach(type => {

    Vue.options[type + 's'] = Object.create(null)

  })

  // 用于标识 Weex 多实例场景中,通过“base”标识普通对象组件的构造函数。

  // 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


  extend(Vue.options.components, builtInComponents)

  // Vue的静态方法: Vue.use/mixin/extend

  initUse(Vue)

  initMixin(Vue)

  initExtend(Vue)

  // Vue的静态属性方法:Vue.component/directive/filter

  initAssetRegisters(Vue)

}

其中 initAssetRegisters(Vue),通过静态变量数组 [ 'component', 'directive','filter'] 遍历创建了Vue.component/directive/filter 这三个静态属性方法。 静态变量配置在 src/shared/constants.js 文件中,方法定义在 core/global-api/assets.js 文件中。

export const SSR_ATTR = 'data-server-rendered'

// 注册全局API时候使用

export const ASSET_TYPES = [

  'component',

  'directive',

  'filter'

]

// 生命周期函数使用

export const LIFECYCLE_HOOKS = [

  'beforeCreate',

  'created',

  'beforeMount',

  'mounted',

  'beforeUpdate',

  'updated',

  'beforeDestroy',

  'destroyed',

  'activated',

  'deactivated',

  'errorCaptured',

  'serverPrefetch'

]

/* @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 => {

    // Vue.comoponent/directive/filter 静态方法的绑定

    Vue[type] = function (

      id: string,

      definition: Function | Object

    ): Function | Object | void {

      if (!definition) {

        return this.options[type + 's'][id]

      } else {

        /* istanbul ignore if */

        if (process.env.NODE_ENV !== 'production' && type === 'component') {

          validateComponentName(id)

        }

        if (type === 'component' && isPlainObject(definition)) {

          definition.name = definition.name || id

          definition = this.options._base.extend(definition)

        }

        if (type === 'directive' && typeof definition === 'function') {

          definition = { bind: definition, update: definition }

        }

        this.options[type + 's'][id] = definition

        return definition

      }

    }

  })

}

2.2 定义Vue构造函数、实例方法

Vue 这个构造函数,定义在 core/instance/index.js 文件中。从代码中可以看到,用工厂模式,执行不同的混入函数,对 Vue.prototype 原型进行加工,给实例添加对应的属性方法。

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')

  }

  // 构造函数中执行 Vue.prototype._init 方法

  this._init(options) 

}

// 实例初始化方法: Vue.prototype._init

initMixin(Vue)

// 实例数据状态相关方法: Vue.prototype.$data/$props/$set/$delete,$watch

stateMixin(Vue)

// 实例事件相关方法: Vue.prototype.$on/$once/$off/$emit

eventsMixin(Vue)

// 实例生命周期相关方法:Vue.prototype._update/$forceUpdate/$destory

lifecycleMixin(Vue)

// 实例渲染相关方法:Vue.prototype.$nextTick/_render

renderMixin(Vue)

export default Vue

2.3 new Vue(options)

执行 new Vue() 创建组件实例,同时 this._init(options) 初始化方法被执行,合并用户配置、初始化周期、事件、数据、属性等。

new Vue({

    data: {...},

    props: {...},

    methods: {...},

    computed: {...}

    ...

})

这部分处理逻辑在 core/instance/indexjs 文件中,与 _init() 相关的主要看 initMixin 这个函数。

/* @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  // 抛出vue实例本身

    vm._self = vm


    // 初始化属性:vm.$parent/$root/$children/$refs

    initLifecycle(vm)


    // 初始化父组件传入的 _parentListeners 事件。

    initEvents(vm)


    // 初始化render相关:vm.$slot/scopedSlots/_c/$createElement

    initRender(vm)


    // 调用生命钩子 beforeCreate

    callHook(vm, 'beforeCreate')


    // 在data/props之前解析注入

    initInjections(vm) // resolve injections before data/props


    // 初始化相关用户配置的数据响应式:vm._props/_data, 以及computed、watch、methods

    initState(vm)


    // 在 data/props 之后提供数据

    initProvide(vm) // resolve provide after data/props


    // 调用生命钩子 created

    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)

    }

  }

}

......

2.4 执行 $mount 进行挂载

执行 mount执行挂载,目录是为了生成vnode,进而转换为真实DOM执行更新。mount 方法在 web 端相关两个 src/platform/web/entry-runtime-with-compiler.js、src/platform/web/runtime/index.js 构建文件中都有定义。我们这里分析 entry-runtime-with-compiler.js 带 compiler 版本的入口文件。关于 Vue scripts 脚本构建相关的内容,大家可以参考我之前写的 这篇文章 的第2章节。

entry-runtime-with-compiler.js 版本,是在 src/platform/web/runtime/index.js 版本的基础上,加 compiler 相关的功能逻辑。它首先保存 runtime 版本的 mount = Vue.prototype.mount方法。再重写Vue.prototype.mount 方法。如果用户传入 template 模板,就通过编译,转换成 render 函数。最后通过先前保存的 mount 方法进行挂载。下面我们在再来复习一下这个 $mount 实现逻辑。

......

// 1. 保存 runtime 版本 Vue.prototype 上的 $mount 方法

const mount = Vue.prototype.$mount

// 2. 重写 Vue.prototype 上的 $mount(加上 compiler 相关功能逻辑)

Vue.prototype.$mount = function (

  el?: string | Element,

  hydrating?: boolean

): Component {

  el = el && query(el)

  /* istanbul ignore if */

  if (el === document.body || el === document.documentElement) {

    process.env.NODE_ENV !== 'production' && warn(

      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`

    )

    return this

  }

  // 处理 options 配置

  const options = this.$options

  // resolve template/el and convert to render function

  if (!options.render) {

    let template = options.template

    if (template) {

      if (typeof template === 'string') {

        if (template.charAt(0) === '#') {

          template = idToTemplate(template)

          /* istanbul ignore if */

          if (process.env.NODE_ENV !== 'production' && !template) {

            warn(

              `Template element not found or is empty: ${options.template}`,

              this

            )

          }

        }

      } else if (template.nodeType) {

        template = template.innerHTML

      } else {

        if (process.env.NODE_ENV !== 'production') {

          warn('invalid template option:' + template, this)

        }

        return this

      }

    } else if (el) {

      template = getOuterHTML(el)

    }

    // 3. 存在 template 选项内容,就进行编译。

    if (template) {

      /* istanbul ignore if */

      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {

        mark('compile')

      }

      // 编译获取 render 函数

      const { render, staticRenderFns } = compileToFunctions(template, {

        outputSourceRange: process.env.NODE_ENV !== 'production',

        shouldDecodeNewlines,

        shouldDecodeNewlinesForHref,

        delimiters: options.delimiters,

        comments: options.comments

      }, this)

      options.render = render

      options.staticRenderFns = staticRenderFns

      /* istanbul ignore if */

      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {

        mark('compile end')

        measure(`vue ${this._name} compile`, 'compile', 'compile end')

      }

    }

  }


  // 4. 编译结束,调用 runtime 版本的 $mount 方法进行挂载

  return mount.call(this, el, hydrating)

}

......

最后,代码执行 mount.call(this, el, hydrating)。实际上复用了 runtime/index.js 中的定义的 $mount 公共方法,代码注释如下。

/* @flow */

import Vue from 'core/index'

import config from 'core/config'

import { extend, noop } from 'shared/util'

import { mountComponent } from 'core/instance/lifecycle'

import { devtools, inBrowser } from 'core/util/index'

import {

  query,

  mustUseProp,

  isReservedTag,

  isReservedAttr,

  getTagNamespace,

  isUnknownElement

} from 'web/util/index'

import { patch } from './patch'

import platformDirectives from './directives/index'

import platformComponents from './components/index'

// install platform specific utils

Vue.config.mustUseProp = mustUseProp

Vue.config.isReservedTag = isReservedTag

Vue.config.isReservedAttr = isReservedAttr

Vue.config.getTagNamespace = getTagNamespace

Vue.config.isUnknownElement = isUnknownElement

// install platform runtime directives & components

extend(Vue.options.directives, platformDirectives)

extend(Vue.options.components, platformComponents)

// install platform patch function

Vue.prototype.__patch__ = inBrowser ? patch : noop

// 定义了公共的 $mount 方法

// public mount method

Vue.prototype.$mount = function (

  el?: string | Element,

  hydrating?: boolean

): Component {

  el = el && inBrowser ? query(el) : undefined

  return mountComponent(this, el, hydrating)

}

// devtools global hook

/* istanbul ignore next */

....

export default Vue

公共 $mount 方法实际上调用了 mountComponent 函数,它 core/instance/lifecycle.js 文件中定义,在mountComponent 函数中,实例化一个渲染Watcher,此时 Watcher 内部逻辑中调用定义的 updateComponent 函数。updateComponent 被调用, vm._render 执行生成 vnode,最终调用 _update 将 vnode 更新成 DOM,代码注释如下。

...

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

        )

      }

    }

  }

  // 调用 beforeMount 钩子

  callHook(vm, 'beforeMount')

  let updateComponent

  /* istanbul ignore if */  // web端可以忽略

  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方法,渲染 watcher 内部会调用。

    // 如果 updateComponent 被调用,render 方法先执行,生成 vnode。

    // 最后执行 _update 方法,进行DOM更新,new Vue() 走的是创建DOM逻辑。

    updateComponent = () => {

      vm._update(vm._render(), hydrating)

    }

  }

  // 初始化渲染 watcher,内部逻辑会调用 updateComponent。

  // 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

  new Watcher(vm, updateComponent, noop, {

    before () {

      if (vm._isMounted && !vm._isDestroyed) {

        callHook(vm, 'beforeUpdate')

      }

    }

  }, true /* isRenderWatcher */)

  hydrating = false


  // 如果 vm.$vnode === null 当前 vm 的父 vnode 为null。

  // 即判断 vm 当前实例为 Vue 的根实例.

  // vm.$vnode 在上面的 updateChildComponent 方法中有的定义 vm.$vnode = parentVnode

  // manually mounted instance, call mounted on self

  // mounted is called for render-created child components in its inserted hook

  if (vm.$vnode == null) {

    vm._isMounted = true  // 标记该Vue根实例挂载结束

    callHook(vm, 'mounted')  // 执行钩子 mounted。

  }

  return vm

}

...

2.5 执行 _render 生成 vnode

vm._render 方法在之前的内容中有提到,它定义 instance/index.js 文件中,它是在 Vue 构造函数定义的时候,给Vue添加的实例方法。

具体逻辑在 src/core/instance/render.js 文件中。其他代码逻辑可以先不关注,主要关注,vnode = render.call(vm._renderProxy, vm.$createElement) 这部分调用。

export function renderMixin (Vue: Class<Component>) {

  // install runtime convenience helpers

  installRenderHelpers(Vue.prototype)

  Vue.prototype.$nextTick = function (fn: Function) {

    return nextTick(fn, this)

  }

  // 给实例初始化render方法

  Vue.prototype._render = function (): VNode {

    ... 

    try {

      // There's no need to maintain a stack because all render fns are called

      // separately from one another. Nested component's render fns are called

      // when parent component is patched.

      currentRenderingInstance = vm

      // 调用用户定义 render 函数生成vnode

      vnode = render.call(vm._renderProxy, vm.$createElement) 

    }

    ...

    return vnode

  }

}

render.call 执行,传入了 vm.createElement,这里就是用户可以通过手写render函数,用来生成vnode的实现。示例如下,其中h就是vm.createElement。

<div id="app">

  {{title}}

</div>

<script>

  window.app = new Vue({

    data: {

      title: 'vue render'

    },

    // 手写 render 函数,h === vm.$createElement

    render(h) {

      return h(

        'div',

        {

          attrs: {

            id: 'demo'

          }

        },

        this.title

      );

    }

  }).$mount('#app');

</script>

USB Microphone https://www.soft-voice.com/

Wooden Speakers  https://www.zeshuiplatform.com/

亚马逊测评 www.yisuping.cn

深圳网站建设www.sz886.com

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容