VUE源码浅析(一.VUE构造函数)
栏目: JavaScript · 发布时间: 5年前
内容简介:最近一直忙于找实习,一直奔波于北京和学校两地之间,很荣幸能加入一家小公司,由于之前一直使用的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源码解析-构造函数
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
ACM国际大学生程序设计竞赛题解
赵端阳//袁鹤 / 电子工业 / 2010-7 / 39.00元
随着各大专院校参加ACM/ICPC热情的高涨,迫切需要有关介绍ACM国际大学生程序设计竞赛题解的书籍。《ACM国际大学生程序设计竞赛题解(2)》根据浙江大学在线题库的部分题目,经过分类、筛选、汇编,并进行了解答(个别特别简单或者特别复杂的题目未选择),比较详细地分析和深入浅出地讲解了解题的方法和用到的算法。题目的类型包括基础编程、模拟、字符串处理、搜索、动态规划、回溯、图论、几何和数学题。 ......一起来看看 《ACM国际大学生程序设计竞赛题解》 这本书的介绍吧!