VUE源码浅析(一.VUE构造函数)

栏目: JavaScript · 发布时间: 6年前

内容简介:最近一直忙于找实习,一直奔波于北京和学校两地之间,很荣幸能加入一家小公司,由于之前一直使用的React,现在的公司用VUE,所以花费了两天时间学习了VUE的基本使用,这两天心血来潮准备看看VUE的源码,我只是一个大二的初生牛犊,有错误的地方还望大家指出来,我们共同学习。喜欢的请点赞今天开始,我准备浅谈一下自己对于VUE源码的理解,同时配套源码的注释,具体地址过两天将会发布 ,同时文章里面所用到的代码都会有一个单独的文章进行汇总 ,今天先说说Vue这个 构造函数使用VUE的时候我们需要new一下,故追本寻源,

最近一直忙于找实习,一直奔波于北京和学校两地之间,很荣幸能加入一家小公司,由于之前一直使用的React,现在的公司用VUE,所以花费了两天时间学习了VUE的基本使用,这两天心血来潮准备看看VUE的源码,我只是一个大二的初生牛犊,有错误的地方还望大家指出来,我们共同学习。喜欢的请点赞

本篇简介

今天开始,我准备浅谈一下自己对于VUE源码的理解,同时配套源码的注释,具体地址过两天将会发布 ,同时文章里面所用到的代码都会有一个单独的文章进行汇总 ,今天先说说Vue这个 构造函数

VUE是个构造函数

使用VUE的时候我们需要new一下,故追本寻源,在 ./instance/index 文件中找到定义VUE构造函数的代码

// 从五个文件导入五个方法(不包括 warn)
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'

// 定义 Vue 构造函数
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)
}

// 将 Vue 作为参数传递给导入的五个方法
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

// 导出 Vue
export default Vue

复制代码

可以看出使用率安全模式提醒你使用new操作符来调用VUE,接着将VUE作为参数,传递给了五个引入的方法,最后导出VUE。

那么这五个方法又做了什么?

initMixin

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    // ... _init 方法的函数体,此处省略
  }
}
复制代码

原来是在VUE的原型上添加了 _init 方法,这个方法应该是内部初始化的一个方法,在上面我们看到过这个方法。

也就是说当我们调用 new VUE() 的时候会执行 this._init(options)

stateMixin

const dataDef = {}
  dataDef.get = function () { return this._data }
  const propsDef = {}
  propsDef.get = function () { return this._props }
  if (process.env.NODE_ENV !== 'production') {
    dataDef.set = function (newData: Object) {
      warn(
        'Avoid replacing instance root $data. ' +
        'Use nested data properties instead.',
        this
      )
    }
    propsDef.set = function () {
      warn(`$props is readonly.`, this)
    }
  }
  Object.defineProperty(Vue.prototype, '$data', dataDef)
  Object.defineProperty(Vue.prototype, '$props', propsDef)
复制代码

最后面两句很熟悉,使用 Object.definePropetyVue.prototype 上定义了两个属性,分别是 $data$props ,这两个属性的定义分别写在了 dataDefpropsDef 这两个对象上 ,仔细看上面的代码,首先分别是 get ,可以可以看到 $data 属性实际上代理的是 _data 这个属性,而 $props 代理的是 _props 这个实力属性,然后有一个生产环境的判断,如果不是生产环境的话,就为 $data$props 这两个属性设置了set,实际上就是想提醒一下你: 别想修改我 ,也就是说 $data$props 是两个只读的属性。(又get到新的知识点,开心不:smile:)

接下来, stateMixin 又在 Vue.prototype 上定义了三个方法

Vue.prototype.$set = set
  Vue.prototype.$delete = del

  Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
  	// ...
  }
  
复制代码

分别是 $set , $delete , $watch ,实际上你都见过这些东西

eventsMixin

Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {}
Vue.prototype.$once = function (event: string, fn: Function): Component {}
Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {}
Vue.prototype.$emit = function (event: string): Component {}
复制代码

这个方法又在 Vue.prototype 上添加了四个方法,如上

lifecycleMixin

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {}
Vue.prototype.$forceUpdate = function () {}
Vue.prototype.$destroy = function () {}
复制代码

这个方法在 Vue.prototype 上添加了三个方法,是不是感觉很熟悉 ?

renderMixin

这个方法的一开始以 Vue.prototype 为参数调用了 installRenderHelpers 函数,这个函数来自于与 render.js 文件相同目录下的 render-helpers/index.js 文件,找到这个函数

export function installRenderHelpers (target: any) {
  target._o = markOnce
  target._n = toNumber
  target._s = toString
  target._l = renderList
  target._t = renderSlot
  target._q = looseEqual
  target._i = looseIndexOf
  target._m = renderStatic
  target._f = resolveFilter
  target._k = checkKeyCodes
  target._b = bindObjectProps
  target._v = createTextVNode
  target._e = createEmptyVNode
  target._u = resolveScopedSlots
  target._g = bindObjectListeners
}
复制代码

不难发现,这个函数的作用就是在 Vue.prototype 上添加一系列的方法

renderMixin 方法在执行完 installRenderHelpers 函数之后,又在 Vue.prototype 上添加了两个方法,分别是 $nextTick_render ,最终经过 renderMixin 之后, Vue.prototype 又被添加了如下方法:

// installRenderHelpers 函数中
Vue.prototype._o = markOnce
Vue.prototype._n = toNumber
Vue.prototype._s = toString
Vue.prototype._l = renderList
Vue.prototype._t = renderSlot
Vue.prototype._q = looseEqual
Vue.prototype._i = looseIndexOf
Vue.prototype._m = renderStatic
Vue.prototype._f = resolveFilter
Vue.prototype._k = checkKeyCodes
Vue.prototype._b = bindObjectProps
Vue.prototype._v = createTextVNode
Vue.prototype._e = createEmptyVNode
Vue.prototype._u = resolveScopedSlots
Vue.prototype._g = bindObjectListeners

Vue.prototype.$nextTick = function (fn: Function) {}
Vue.prototype._render = function (): VNode {}
复制代码

至此, instance/index.js 文件中的代码就运行完毕了(具体指 npm run dev 命令时构建的运行).大概了解了每个 Mixin 方法的作用骑士就是包装 Vue.prototype ,在其上挂载一些属性和方法,下面我会把代码合并在一块,以便于以后查看

VUE构造函数的静态属性和方法(全局API)

依旧按照追本溯源的原则,我们找到前一个文件 core/index.js ,下面是其全部代码 ,同样高效简短

// 从 Vue 的出生文件导入 Vue
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'

// 将 Vue 构造函数作为参数,传递给 initGlobalAPI 方法,该方法来自 ./global-api/index.js 文件
initGlobalAPI(Vue)

// 在 Vue.prototype 上添加 $isServer 属性,该属性代理了来自 core/util/env.js 文件的 isServerRendering 方法
Object.defineProperty(Vue.prototype, '$isServer', {
  get: isServerRendering
})

// 在 Vue.prototype 上添加 $ssrContext 属性
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 存储了当前 Vue 的版本号
Vue.version = '__VERSION__'

// 导出 Vue
export default Vue

复制代码

上面的代码中,首先从 Vue 的出生文件,也就是 instance/index.js 文件导入 Vue ,然后分别从三个文件导入了三个变量 ,

其中 initGlobalAPI 是一个函数,并且以 Vue 构造函数作为参数进行调用

initGlobalAPI(Vue)

然后在 Vue.prototype 上分别添加了两个只读的属性,分别是: $isServer$ssrContext 。接着在 Vue 构造函数上定义了 FunctionalRenderContext 静态属性,并且 FunctionalRenderContext 属性的值来自于 core/vdom/create-functional-component 文件,从命名来看,这个属性是为了在SSR中使用它。

最后,在 Vue 构造函数上添加了一个静态属性 version ,存储了当前 Vue 的版本值,但是这里的 '_VERSION' 是什么东西呢?找了半天打开 scripts.config.js 文件,找到了 getConfig 方法,其中有这么一句话: _VERSION_:version 。也就是说最后的 _VERSION_ 最终将被 version 的值替换,而 version 的值就是 Vu 的版本号

回头继续看看 initGlobalAPI(Vue) 这段代码,貌似是要给 Vue 上添加一些全局的 API ,实际上就是这样的,这些全局 API 以静态属性和方法的形式被添加到 Vue 构造函数上,找到这个方法,看看主要做了什么

// 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)
复制代码

首先是这样 一段代码,意思是给 vue···添加一个 config```属性,也是一个只读属性,你修改它会在非生产模式下给你一个友好的提示。

那么 Vue.config 的值是什么呢?在 src/core/global-api/index/js 文件开头有这样一行代码

import config from '../config' 所以是从这个文件导出的对象。

接着是下面这段代码

// 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 中添加了 util 属性,这是一个对象,这个对象拥有四个属性分别是: warn , extend , ergeOptions 以及 defineReactive ,这四个属性来自于 core/util/index.js 文件 大概意思就是 Vue.util 以及 util 下的四个方法都是不被公认是公共API的一部分,要避免依赖他们,但是你依然可以用,不过你还是不要用的好

然后是这样一段代码

Vue.set = set
  Vue.delete = del
  Vue.nextTick = nextTick
复制代码

接着给 Vue 添加了三个属性

// 2.6 explicit observable API
  Vue.observable = <T>(obj: T): T => {
    observe(obj)
    return obj
  }
复制代码

这段代码给 Vue 添加了 observable 方法,这个方法先是调用了 observe 这个方法,然后返回了 obj (传入的参数)

上面的注释说明这个API是添加 Vue 2.6,接着是下面的代码

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)
复制代码

这段代码先是通过 object.create() 创建了一个新的空对象,添加到 Vueoptions 属性上,接着给 options 属性添加了值( ASSET_TYPES 是一个数组),这段代码执行完其实就是这样

Vue.options = {
	components: Object.create(null),
	directives: Object.create(null),
	filters: Object.create(null),
	_base: Vue
}
复制代码

接着就是将 builtInComponents 的属性混合到 Vue.options.components

最终 Vue.options.components 的值如下:

Vue.options.components = {
	KeepAlive
}
复制代码

那么到现在为止, Vue.options 已经变成了这个亚子

Vue.options = {
	components: {
		KeepAlive
	},
	directives: Object.create(null),
	filters: Object.create(null),
	_base: Vue
}
复制代码

我们继续看代码

initUse(Vue)
  initMixin(Vue)
  initExtend(Vue)
  initAssetRegisters(Vue)
复制代码

这四个方法来自于四个文件 ,我们分别来看看

initUse

/* @flow */

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

export function initUse (Vue: GlobalAPI) {
  Vue.use = function (plugin: Function | Object) {
    // ...
  }
}

复制代码

其实很简单,就是在 Vue 函数上添加 use 方法,也就是我们经常在 main.js 文件中用的全局API,用来安装 VUE 插件。

initMixin

/* @flow */

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

export function initMixin (Vue: GlobalAPI) {
  Vue.mixin = function (mixin: Object) {
    this.options = mergeOptions(this.options, mixin)
    return this
  }
}
复制代码

这段代码就是在 Vue 中添加 mixin 这个全局API

initExtend

export function initExtend (Vue: GlobalAPI) {
  /**
   * Each instance constructor, including Vue, has a unique
   * cid. This enables us to create wrapped "child
   * constructors" for prototypal inheritance and cache them.
   */
  Vue.cid = 0
  let cid = 1

  /**
   * Class inheritance
   */
  Vue.extend = function (extendOptions: Object): Function {
    // ...
  }
}
复制代码

initExtend 方法在 Vue 中添加了 Vue.cid 静态属性,和 Vue.extend 静态方法

initAssetRegisters

export function initAssetRegisters (Vue: GlobalAPI) {
  /**
   * Create asset registration methods.
   */
  ASSET_TYPES.forEach(type => {
    Vue[type] = function (
      id: string,
      definition: Function | Object
    ): Function | Object | void {
      // ......
    }
  })
}
复制代码

其中某个东西我们之前见过了 ,所以最终 initAssetRegisters 方法, Vue 又多了三个静态方法

Vue.component 

Vue.directive

Vue.filter
复制代码

这三个方法大家肯定不陌生,分别用来全局注册组件,指令和过滤器。

这样 我们大概了解了上述几个文件的作用

Vue平台化的包装

现在,我们弄清了 Vue 构造函数的过程中的两个主要的文件,分别是: core/instance/index.js , core/index.js 文件,我们知道 core 目录下的文件存放的是与平台无关的代码,但是, Vue 是一个 Multi-platform 的项目,不同平台可能会内置不同的组件,指令或者一些平台特有的功能等,那么就需要根据不同的平台进行平台化地包装。

我们打开 platforms 目录,可以发现有两个子目录 webweex 。这两个子目录的作用就是分别为相应的平台对核心的 Vue 进行包装的。我们先去看看 web 的根文件

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

复制代码

首先依旧是导入了很多文件,然后对 core/config.js 文件进行一些修改吧(原本文件里的对象大部分属性都是初始化了一个初始值),注释的意思是这个配置是与平台有关的,很可能会被覆盖掉,这个时候我们回来再看看代码,其实就是在覆盖默认导出的 config 对象的属性,至于这些东西的作用,暂时还不知道

接着是下面两句代码

/ install platform runtime directives & components
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)
复制代码

安装特定平台运行时的指令和组件,之前我们已经看到过 Vue.options 是什么样的,经过这样一番折腾,变成啥了?

我们先看看 platformDirectivesplatformComponents 长什么样,顺着导入的文件地址,我们看到 platformDirectives 实际是这样

platformDirectives = {
  model,
  show
}
复制代码

也就是经过 extend(Vue.options.directives, platformDirectives) 之后, Vue.options 将变成:

Vue.options = {
	components: {
		KeepAlive
	},
	directives: {
		model,
		show
	},
	filters: Object.create(null),
	_base: Vue
}
复制代码

同样的道理,变化之后是下面这样的

Vue.options = {
	components: {
		KeepAlive,
		Transition,
		TransitionGroup
	},
	directives: {
		model,
		show
	},
	filters: Object.create(null),
	_base: Vue
}
复制代码

现在我们搞清楚了作用,就是 Vue.options 上添加 web 平台运行时特定的指令和组件。

接着往下看看

// 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)
}
复制代码

首先在 Vue.prototype 上添加了 _patch_ 方法,如果浏览器环境运行的话这个方法的值为 patch 函数,否则是一个空函数 noop ,然后又在 Vue.prototype 上添加了 $mount 方法,暂时不需要关注方法的作用和内容吧。

再往下的一段代码

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-tools 的全局钩子,它被包裹在 setTimeout 中,最后导出了Vue

with compiler

在看完上面这个文件之后,其实 运行 时版本的 Vue 构造函数已经"成型",我们看到 entry-runtime.js 文件只有两行代码,

import Vue from './runtime/index'

export default Vue
复制代码

可以发现, 运行时 版的入口文件,导出的 Vue 就在 ./runtime/index.js 为止。然后我们需要了解完整的 Vue ,入口文件是 entry-runtime-with-compiler.js ,所以完整版和运行版差别就在compiler,所以我们要看的这个文件作用就是在运行时版本的基础上添加 compiler ,先看看文件的代码

/* @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,还从 ./compiler/index.js 文件中导入 compileToFunctions ,后边根据id获取元素的 innerHTML ,接着使用 mount 变量缓存了 Vue.prototype.$mount 方法,然后重写了 Vue.prototype.$mount 方法,后续接着获取元素的 outerHTML ,最终在 Vue 上添加了一个全局的API: compileToFunctions ,导出了 Vue

看完是不是有点懵逼?这个文件运行下来对Vue的影响有两个,第一个影响是它重写了 Vue.prototype.$mount 方法;第二个是添加了 Vue.compile 全局API,至于具体做什么,我们一步一步看!

首先,它待遇 el 做了限制,Vue不能挂载在 body , html 这样的根节点上,接下来的是很关键的逻辑:如果没有定义 redner 方法,则会把 el 或者 template 字符串转换成 render 方法。刚学习Vue不了解以前什么情况,在Vue2.0版本上,所有的Vue的组件的渲染最终都需要 render 方法,无论我们是用单文件的 .vue 文件还是写了 el 或者 template 属性,最终都会转换成 render 方法,这个过程是一个"在线编译"的过程,它是调用 compileToFunctions 方法实现的,这个后续介绍。最后,调用原先原型上的 $mount 方法挂载。


以上所述就是小编给大家介绍的《VUE源码浅析(一.VUE构造函数)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Cracking the Coding Interview

Cracking the Coding Interview

Gayle Laakmann McDowell / CareerCup / 2015-7-1 / USD 39.95

Cracking the Coding Interview, 6th Edition is here to help you through this process, teaching you what you need to know and enabling you to perform at your very best. I've coached and interviewed hund......一起来看看 《Cracking the Coding Interview》 这本书的介绍吧!

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具