Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vue - Vue 构造函数 #5

Open
VenenoFSD opened this issue Mar 31, 2019 · 0 comments
Open

Vue - Vue 构造函数 #5

VenenoFSD opened this issue Mar 31, 2019 · 0 comments
Labels
review remember to review Vue Vue.js source code

Comments

@VenenoFSD
Copy link
Owner

VenenoFSD commented Mar 31, 2019

前言

我们在使用 Vue 的时候,要先使用 new 运算符调用 Vue,也就是说 Vue 是一个构造函数。在正式进入源码学习前,先在直观上了解 Vue 这个构造函数的样子。

回顾上一节 Vue.js 源码构建 ,通过 scripts 目录下的 config.jsalias.js,我们可以查到构建出 Vue 完整版的入口文件路径为 /src/platforms/web/entry-runtime-with-compiler.js

Vue 构造函数

找到构建 Vue 的入口文件后,看看它长什么样:

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

/* @flow */

import config from 'core/config'
import { warn, cached } from 'core/util/index'
import { mark, measure } from 'core/util/perf'

import Vue from './runtime/index'
import { query } from './util/index'
import { compileToFunctions } from './compiler/index'
import { shouldDecodeNewlines, shouldDecodeNewlinesForHref } from './util/compat'

const idToTemplate = cached(id => {
  const el = query(id)
  return el && el.innerHTML
})

const mount = Vue.prototype.$mount
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
  }

  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)
    }
    if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }

      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')
      }
    }
  }
  return mount.call(this, el, hydrating)
}

/**
 * Get outerHTML of elements, taking care
 * of SVG elements in IE as well.
 */
function getOuterHTML (el: Element): string {
  if (el.outerHTML) {
    return el.outerHTML
  } else {
    const container = document.createElement('div')
    container.appendChild(el.cloneNode(true))
    return container.innerHTML
  }
}

Vue.compile = compileToFunctions

export default Vue

可以看到这里引入了 Vue 又将其导出,所以入口文件并不是定义 Vue 函数的地方。可以看到入口文件的作用有两个,一是重写了 Vue 函数的原型上的 $mount 函数,二是添加了全局函数 compileToFunctions。继续往上找,来到 /src/platforms/web/runtime/index.js,代码如下:

// /src/platforms/web/runtime/index.js

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

// 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 */
if (inBrowser) {
  setTimeout(() => {
    if (config.devtools) {
      if (devtools) {
        devtools.emit('init', Vue)
      } else if (
        process.env.NODE_ENV !== 'production' &&
        process.env.NODE_ENV !== 'test'
      ) {
        console[console.info ? 'info' : 'log'](
          'Download the Vue Devtools extension for a better development experience:\n' +
          'https://github.com/vuejs/vue-devtools'
        )
      }
    }
    if (process.env.NODE_ENV !== 'production' &&
      process.env.NODE_ENV !== 'test' &&
      config.productionTip !== false &&
      typeof console !== 'undefined'
    ) {
      console[console.info ? 'info' : 'log'](
        `You are running Vue in development mode.\n` +
        `Make sure to turn on production mode when deploying for production.\n` +
        `See more tips at https://vuejs.org/guide/deployment.html`
      )
    }
  }, 0)
}

export default Vue

这个文件同样不是定义 Vue 函数的地方,这里在 Vue 构造函数的原型上定义了 $mount,同时往 Vue.configVue.options 上添加方法。继续往上找,来到 /src/core/index.js

// /src/core/index.js

import Vue from './instance/index'

// ...

export default Vue

省略了中间代码,等下再回来分析。这里依然不是,继续找,来到 /src/core/instance/index.js

// /src/core/instance/index.js

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

可以看到,这里就是我们要找的定义 Vue 函数的地方了。函数体内第一段逻辑是如果用户调用 Vue 的时候没有使用 new 操作符,就会抛出一个警告。然后又调用了 _init 方法,这个方法没有在这里被定义,显然是继承来的。

之后将 Vue 这个构造函数作为参数传给了另外五个函数,推断这五个函数功能类似,先来看第一个 initMixin

// /src/core/instance/init.js

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    // ...
  }
}

initMixin 就是往 Vue 的原型挂载刚刚提到的 _init 方法,大致就是内部初始化的过程,这里不研究具体代码,放在下一节研究。

另外四个函数也一样,都是往 Vue.prototype 上挂载属性和方法,这里不深入研究。本章是入门篇,在于大致地了解 Vue 构造函数的样子,这五个函数分别挂载了什么函数将放在后面深入讲解。

把 Vue 构造函数所在的 /src/core/instance/index.js 文件研究完后,回到上一级,也就是 /src/core/index.js,下面是完整代码:

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

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

这里引入了构造函数 Vue 后作为参数传给 initGlobalAPI 这个函数。之后又在 Vue.prototype 上添加了几个属性。现在来看看 initGlobalAPI ,这个函数在 /src/core/global-api/index.js 中:

// /src/core/global-api/index.js

/* @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
  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.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)
  })

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

  initUse(Vue)
  initMixin(Vue)
  initExtend(Vue)
  initAssetRegisters(Vue)
}

看函数的名称推断出就是给 Vue 构造函数定义全局(静态)的属性和函数上去。

这里创建一个 configDef 代理了 Vue.config,并且在非生产环境下如果尝试修改 config,会抛出一个警告。

接着下面又定义了 util,注意这里的注释:these are not considered part of the public API - avoid relying on them unless you are aware of the risk。也就说 util 这个属性不是公共 api,官网文档没有这个属性的介绍,建议不要去依赖这个属性除非能够预知风险。

之后又在 Vue 上定义了多个属性和方法,然后调用 extend 方法,最后又将 Vue 作为参数传给四个函数,这四个函数都是在 Vue 上创建静态函数,例如 Vue.use 等.

来看看这个extend 函数,这个方法定义在 /src/shared/util.js 里,瞄一眼:

// /src/shared/util.js

export function extend (to: Object, _from: ?Object): Object {
  for (const key in _from) {
    to[key] = _from[key]
  }
  return to
}

其实就是把 builtInComponents 的所有属性混合到 Vue.options.components 对应属性上。

这个 builtInComponents 来自 /src/core/components/index.js

// /src/core/components/index.js

import KeepAlive from './keep-alive'

export default {
  KeepAlive
}

builtInComponents 就是 Keep-Alive 这个组件。

总结

以上,我们把从入口文件一直到定义 Vue 构造函数的基本流程梳理了一遍。整个过程就是定义 Vue 构造函数,然后给 Vue 定义全局(其实就是静态)的属性和方法,同时又定义了 Vue.prototype(其实就是给实例用的)上的属性和方法。这些属性和方法有一部分是暴露给用户使用的,也就是在 Vue 官方文档上能找到的。

Vue 构造函数全局的和原型上的属性和方法在本文没有细讲,这些属性和方法的原理和用途会放在后面细细研究。

下一节:从一个小例子开始

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
review remember to review Vue Vue.js source code
Projects
Learing Vue.js source code
一、准备工作
Development

No branches or pull requests

1 participant