Vue.js源码解读--(1)
Vue框架对于前端来说有多重要就不多提了,三天前决定看看源码,奈何自己是个菜鸡,只能慢慢的一点一点啃,进行扫荡式学习,初有收获,特将笔记所记内容记下,逻辑略乱,各位客官觉得乱或者有问题的话请评论说下,我会重新组织语言并回答您。
本文为小白从头扫荡式教程,我都能懂你肯定也能的~
好的,下面开始。
首先你要去github把源码下载下来啦-- https://github.com/vuejs/vue
首先我们进入package.json文件 在我们 npm run dev后执行这个语句
"scripts": {
"dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev",
-w为watch,监听,-c修改执行文件路径,进入scripts/config.js文件内,最底部。
if (process.env.TARGET) { module.exports = genConfig(process.env.TARGET) } else { exports.getBuild = genConfig exports.getAllBuilds = () => Object.keys(builds).map(genConfig) }
process.env.TARGET存在,执行
function genConfig (name) { const opts = builds[name] const config = { input: opts.entry, external: opts.external, sourceMap:true, plugins: [ replace({ __WEEX__: !!opts.weex, __WEEX_VERSION__: weexVersion, __VERSION__: version }), flow(), buble(), alias(Object.assign({}, aliases, opts.alias)) ].concat(opts.plugins || []), output: { file: opts.dest, format: opts.format, banner: opts.banner, name: opts.moduleName || 'Vue' } } if (opts.env) { config.plugins.push(replace({ 'process.env.NODE_ENV': JSON.stringify(opts.env) })) } Object.defineProperty(config, '_name', { enumerable: false, value: name }) return config }
上面方法为把builds相同属性的内容赋给opts,所以我们的opts如下
'web-full-dev': { entry: resolve('web/entry-runtime-with-compiler.js'), dest: resolve('dist/vue.js'), format: 'umd', env: 'development', alias: { he: './entity-decoder' }, banner },
接下来看到入口文件为resolve('web/entry-runtime-with-compiler.js')
const aliases = require('./alias') const resolve = p => { const base = p.split('/')[0] if (aliases[base]) { return path.resolve(aliases[base], p.slice(base.length + 1)) } else { return path.resolve(__dirname, '../', p) } }
resolve方法将传入参数经过aliases方法改变,所以最后我们得到的入口路径为src/platforms/web/entry-runtime-with-compiler.js。
在该文件内,Vue由./runtime/index导入,进入后,Vue由core/index导入,进入后,Vue由./instance/index导入(instance有实例的意思),进入后,即可发现Vue的构造函数。
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构造函数中,前五行判断开发者必须用NEW实例化vue,
new vue时,其中的el等值传到option内,然后调用_init方法。
构造函数下方,调用initMixin(),进入该文件内
1 let uid = 0 2 3 export function initMixin (Vue: Class<Component>) { 4 Vue.prototype._init = function (options?: Object) { 5 const vm: Component = this 6 // a uid 7 vm._uid = uid++ 8 9 let startTag, endTag 10 /* istanbul ignore if */ 11 if (process.env.NODE_ENV !== 'production' && config.performance && mark) { 12 startTag = `vue-perf-start:${vm._uid}` 13 endTag = `vue-perf-end:${vm._uid}` 14 mark(startTag) 15 } 16 17 // a flag to avoid this being observed 18 vm._isVue = true 19 // merge options 20 if (options && options._isComponent) { 21 // optimize internal component instantiation 22 // since dynamic options merging is pretty slow, and none of the 23 // internal component options needs special treatment. 24 initInternalComponent(vm, options) 25 } else { 26 console.log(Vue.options) 27 vm.$options = mergeOptions( 28 resolveConstructorOptions(vm.constructor), 29 options || {}, 30 vm 31 ) 32 } 33 /* istanbul ignore else */ 34 if (process.env.NODE_ENV !== 'production') { 35 initProxy(vm) 36 } else { 37 vm._renderProxy = vm 38 } 39 // expose real self 40 vm._self = vm 41 initLifecycle(vm) 42 initEvents(vm) 43 initRender(vm) 44 callHook(vm, 'beforeCreate') 45 initInjections(vm) // resolve injections before data/props 46 initState(vm) 47 initProvide(vm) // resolve provide after data/props 48 callHook(vm, 'created') 49 50 /* istanbul ignore if */ 51 if (process.env.NODE_ENV !== 'production' && config.performance && mark) { 52 vm._name = formatComponentName(vm, false) 53 mark(endTag) 54 measure(`vue ${vm._name} init`, startTag, endTag) 55 } 56 57 if (vm.$options.el) { 58 vm.$mount(vm.$options.el) 59 } 60 } 61 }
第一行为被调用方法,第二行在vue构造函数中挂载了_init方法,所以在构造函数或实例内都可以直接调用。
参数写法为flow,第一行参数意思为传入参数类型为component,第二行为传入参数类型为Object,加问号意思为有无皆可,但类型必须为Object,问号在后同理。继续
函数内第一行的this指向被调用函数,即为Vue实例化的对象,然后将函数挂载两个属性:_uid,_isVue,然后判断options是否存在且其中是否有_isComponent,在寻找构造函数Vue时,core/index中将构造函数挂载了options,下图为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) console.log(Vue.prototype) 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 }) console.log(Vue.options) Vue.version = '__VERSION__' export default Vue
在initGlobalAPI(下图)方法中,前十行将config值赋给configDef,并命名为config到Vue上,且不允许修改configDef的值。然后又加了util,set,delete,nexttick。
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 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) }
下图为被加的config值
export type Config = { // user optionMergeStrategies: { [key: string]: Function }; silent: boolean; productionTip: boolean; performance: boolean; devtools: boolean; errorHandler: ?(err: Error, vm: Component, info: string) => void; warnHandler: ?(msg: string, vm: Component, trace: string) => void; ignoredElements: Array<string | RegExp>; keyCodes: { [key: string]: number | Array<number> }; // platform isReservedTag: (x?: string) => boolean; isReservedAttr: (x?: string) => boolean; parsePlatformTagName: (x: string) => string; isUnknownElement: (x?: string) => boolean; getTagNamespace: (x?: string) => string | void; mustUseProp: (tag: string, type: ?string, name: string) => boolean; // legacy _lifecycleHooks: Array<string>; };
继续initGlobalAPI方法,Vue加了options方法,并循环ASSET_TYPES的值末尾加s作为键,值为null,后加了options._base,再用extend方法将components对象中添加KeepAlive,extend方法目前理解为将第二个值复制给第一个值,(keepalive是一个内容很多的重要文件,在src/core/components中,标记以后看),最下面四个方法分别加了use,mixin,extend(和上边的不是一个),并在extend中使Vue.cid=0,最后一个方法被直接调用,如下图二。
export const ASSET_TYPES = [ 'component', 'directive', 'filter' ]
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[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 } } }) }
该方法中,将Vue[component]加上了方法,由于没有definition值,options内也无id,所以返回的也是空(该过程仅仅是将Vue添加了三个空方法)。在initglobalapi方法经历一番后,vue如下(仅为在initglobalapi方法内所加)。
Vue.config Vue.util = util Vue.set = set Vue.delete = del Vue.nextTick = util.nextTick Vue.options = { components: { KeepAlive }, directives: {}, filters: {}, _base: Vue } Vue.use Vue.mixin Vue.cid = 0 Vue.extend Vue.component = function(){} Vue.directive = function(){} Vue.filter = function(){}
let _isServer export const isServerRendering = () => { if (_isServer === undefined) { /* istanbul ignore if */ if (!inBrowser && !inWeex && typeof global !== 'undefined') { // detect presence of vue-server-renderer and avoid // Webpack shimming the process _isServer = global['process'].env.VUE_ENV === 'server' } else { _isServer = false } } return _isServer }
最后回到initglobalapi方法调用处,第二,三个皆为在Vue.prototype调用引号内属性时,使用get内方法,isServerRendering方法(上图)为当调用时使当前环境变为server,第三个同理(暂时不知道$vnode是什么),第四个在Vue加上了FunctionalRenderContext属性名及调用该方法的值(无参数,)最后Vue加上了版本。
再往前回到initMixin方法中,options存在但没有options._isComponent,走else,
export function resolveConstructorOptions (Ctor: Class<Component>) { let options = Ctor.options if (Ctor.super) { const superOptions = resolveConstructorOptions(Ctor.super) const cachedSuperOptions = Ctor.superOptions if (superOptions !== cachedSuperOptions) { // super option changed, // need to resolve new options. Ctor.superOptions = superOptions // check if there are any late-modified/attached options (#4976) const modifiedOptions = resolveModifiedOptions(Ctor) // update base extend options if (modifiedOptions) { extend(Ctor.extendOptions, modifiedOptions) } options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions) if (options.name) { options.components[options.name] = Ctor } } } return options }
resolveConstructorOptions方法中,参数为vm.constructor,即为Vue构造函数,暂无super值,直接返回options,调用mergeOptions方法,传入三个参数,分别为Vue.options,实例化时所传参数,和Vue实例化对象。在mergeOptions方法中,
export function mergeOptions ( parent: Object, //Vue.options child: Object, //实例化传入参数 vm?: Component //Vue实例化对象 ): Object { if (process.env.NODE_ENV !== 'production') { checkComponents(child) } if (typeof child === 'function') { child = child.options } normalizeProps(child, vm) normalizeInject(child, vm) normalizeDirectives(child) const extendsFrom = child.extends if (extendsFrom) { parent = mergeOptions(parent, extendsFrom, vm) } if (child.mixins) { for (let i = 0, l = child.mixins.length; i < l; i++) { parent = mergeOptions(parent, child.mixins[i], vm) } } const options = {} let key for (key in parent) { //循环Vue.options的键 mergeField(key) } for (key in child) { //循环实例化参数的键 if (!hasOwn(parent, key)) { //如果当前的键与Vue.options所有的键不重复 mergeField(key) } } function mergeField (key) { const strat = strats[key] || defaultStrat; //strats[key]寻找方法,strat定义方法 //defaultStrat为哪个有值返回哪个 options[key] = strat(parent[key], child[key], vm, key); //左边在options定义相同的键名,右边调用上边的方法 //相当于调starts.key方法并传参 } //console.log(parent) //console.log(child) //console.log(options) return options }
function checkComponents (options: Object) { for (const key in options.components) { validateComponentName(key) } } export function validateComponentName (name: string) { if (!/^[a-zA-Z][\w-]*$/.test(name)) { warn( 'Invalid component name: "' + name + '". Component names ' + 'can only contain alphanumeric characters and the hyphen, ' + 'and must start with a letter.' ) } if (isBuiltInTag(name) || config.isReservedTag(name)) { warn( 'Do not use built-in or reserved HTML elements as component ' + 'id: ' + name ) } }
checkComponents方法为检查实例化所传参数components的值是否合法(两个方法分别为检查是否与内置或保留元素重名)。然后检查child是否为function,传入参数的prop.inject,directives,以及两个判断。
然后定义options,循环options内的名及传入参数的名,进入方法。
function mergeAssets ( parentVal: ?Object, childVal: ?Object, vm?: Component, key: string ): Object { const res = Object.create(parentVal || null) if (childVal) { process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm) console.log(extend(res, childVal)) return extend(res, childVal) } else { return res } } ASSET_TYPES.forEach(function (type) { strats[type + 's'] = mergeAssets })
strats[属性名]对应相应方法,ASSET_TYPES中有三个,可为这三个添加方法,其余在该js上有独自方法,options[key]直接调用mergeAssets或defaultStrat或本页js上定义好的方法,最后合成options,重新梳理:循环Vue.options的键(为父,实例化参数为子)并定义同名键至新options,循环调用方法,ASSET_TYPES中有三个调用mergeAssets,该方法为如果同键名的子属性有值,则返回子属性的值,没有则返回空,添加至新的options,当没有strats.key方法时,则调用defaultStrat方法,该方法为如果子属性有值则返回子属性的值,没有则返回副属性的值,并添加至新options,调用strats.data时,有单独的方法,该方法返回了一个方法,最后返回options。总结:mergeOptions方法新建options对象,将ASSET_TYPES中三个参数设为新options的键并将值设为空,如果实例化传入的参数有相同的键则传该值,然后将Vue.options其余值添加键名至新options并单独处理其值,实例化传参的对象也同样,最后合并成一个拥有所有的键但值被处理过的对象。
继续回溯,我们在寻找Vue构造函数时在src/platforms/web/runtime/index.js停留过一会,现在看它,
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) }
前五行为我们之前Vue内的config内的键赋新值,然后将Vue.options内的两个对象加新值,挂载patch,和$mount方法。query方法为获取该元素(获取不到则报错)。再回溯看
entry-runtime-with-compiler.js,
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, { 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) }
同样挂载了$mount方法,但我们先执行的是这个然后才是上一个,然后在最下方加上了Vue.compile。compileToFunctions 函数的作用,就是将模板 template 编译为render函数。
下面回到_init()方法内...本周就到这里
{
下面为小贴士~
在script/config.js内genConfig方法中,加上框内的可在浏览器调试(出现src目录),接下来就可以尽情的debugger了
}