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.definePropety
在 Vue.prototype
上定义了两个属性,分别是 $data
和 $props
,这两个属性的定义分别写在了 dataDef
和 propsDef
这两个对象上 ,仔细看上面的代码,首先分别是 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()
创建了一个新的空对象,添加到 Vue
的 options
属性上,接着给 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
目录,可以发现有两个子目录 web
和 weex
。这两个子目录的作用就是分别为相应的平台对核心的 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
是什么样的,经过这样一番折腾,变成啥了?
我们先看看 platformDirectives
和 platformComponents
长什么样,顺着导入的文件地址,我们看到 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构造函数)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- Java类 静态代码块、构造代码块、构造函数初始化顺序
- TS 的构造签名和构造函数类型是啥?傻傻分不清楚
- 只有你能 new 出来!.NET 隐藏构造函数的 n 种方法(Builder Pattern / 构造器模式)
- 构造函数、原型、原型链、继承
- Vue源码: 构造函数入口
- Hashmap源码解析-构造函数
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
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》 这本书的介绍吧!