源码地址
前方高能!!!
这只是一篇个人学习Vue.js源码的笔记,并非教程,鉴于个人水平有限,可能存在错误,还望各路大神指点
文章内容极度粗俗,各种无脑分析,各种疯狂输出,各位看官斟酌而行,切勿走火入魔!!!
Vue.js版本 –2.1.7
之所以选择这个是因为看了这位大神的分析,决定采用同一个版本,目前Vue已经发布了2.5.x了
这里极力推荐大家去看看,据说这位大神的两篇源码分析都是经过尤大佬推荐的哦,本文作为第一篇
也是参考大神的文章作为开头,参考了极大部分再加上自己的理解而写的
Vue2.1.7源码学习
JavaScript实现MVVM之我就是想监测一个普通对象的变化
src/core/instance/index.js
这是整个vue的入口文件,首先引入vue后我们只是引入了一个构造函数,所以
一般我们初始化的时候都是用new Vue的方式启动,即下面的Vue函数,里面执行了一句
this._init(options),这个options即是我们传入的各种参数,即
new Vue({
el: '#app',
data: {
name: 'zhang san',
age: 18
}
})
下面是src/core/instance/index.js的源码,其中引入vue的时候马上就初始化了5个mixin
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构造函数为参数,调用了五个方法,最后导出 Vue。这五个方法分别来自五个文件:init.js state.js render.js events.js 以及 lifecycle.js。
打开这五个文件,找到相应的方法,你会发现,这些方法的作用,就是在 Vue 的原型 prototype 上挂载方法或属性,经历了这五个方法后的Vue会变成这样:
// initMixin(Vue) src/core/instance/init.js **************************************************
Vue.prototype._init = function (options?: Object) {}
// stateMixin(Vue) src/core/instance/state.js **************************************************
Vue.prototype.$data
Vue.prototype.$set = set
Vue.prototype.$delete = del
Vue.prototype.$watch = function(){}
// renderMixin(Vue) src/core/instance/render.js **************************************************
Vue.prototype.$nextTick = function (fn: Function) {}
Vue.prototype._render = function (): VNode {}
Vue.prototype._s = _toString
Vue.prototype._v = createTextVNode
Vue.prototype._n = toNumber
Vue.prototype._e = createEmptyVNode
Vue.prototype._q = looseEqual
Vue.prototype._i = looseIndexOf
Vue.prototype._m = function(){}
Vue.prototype._o = function(){}
Vue.prototype._f = function resolveFilter (id) {}
Vue.prototype._l = function(){}
Vue.prototype._t = function(){}
Vue.prototype._b = function(){}
Vue.prototype._k = function(){}
// eventsMixin(Vue) src/core/instance/events.js **************************************************
Vue.prototype.$on = function (event: string, fn: Function): Component {}
Vue.prototype.$once = function (event: string, fn: Function): Component {}
Vue.prototype.$off = function (event?: string, fn?: Function): Component {}
Vue.prototype.$emit = function (event: string): Component {}
// lifecycleMixin(Vue) src/core/instance/lifecycle.js **************************************************
Vue.prototype._mount = function(){}
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {}
Vue.prototype._updateFromParent = function(){}
Vue.prototype.$forceUpdate = function () {}
Vue.prototype.$destroy = function () {}
这样就结束了吗?并没有,根据我们之前寻找 Vue 的路线,这只是刚刚开始,我们追溯路线往
回走,那么下一个处理 Vue 构造函数的应该是 src/core/index.js 文件,我们打开它:
import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
import { isServerRendering } from 'core/util/env'
initGlobalAPI(Vue)
Object.defineProperty(Vue.prototype, '$isServer', {
get: isServerRendering
})
Vue.version = '__VERSION__'
export default Vue
我们则从这里作为第一出发点开始
首先这里说下这个isServerRendering,找到这个env文件,里面有个方法
export const inBrowser = typeof window !== 'undefined'
let _isServer
export const isServerRendering = () => {
if (_isServer === undefined) {
/* istanbul ignore if */
if (!inBrowser && 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
}
这段代码判断是否为服务端,const inBrowser = typeof window !== ‘undefined’这段代码
大概就明白了
然后绑定在构造函数的原型的一个属性$isServer上面
Object.defineProperty(Vue.prototype, '$isServer', {
get: isServerRendering
})
然后再说initGlobalAPI方法的执行
src\core\global-api\index.js
这个文件导出一个函数,函数接受Vue构造函数作为参数
export function initGlobalAPI (Vue: GlobalAPI) {
// config
const configDef = {}
configDef.get = () => config
if (process.env.NODE_ENV !== 'production') {
configDef.set = () => {
util.warn(
'Do not replace the Vue.config object, set individual fields instead.'
)
}
}
Object.defineProperty(Vue, 'config', configDef)
Vue.util = util
Vue.set = set
Vue.delete = del
Vue.nextTick = util.nextTick
Vue.options = Object.create(null)
config._assetTypes.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
util.extend(Vue.options.components, builtInComponents)
initUse(Vue)
initMixin(Vue)
initExtend(Vue)
initAssetRegisters(Vue)
}
首先定义一个对象configDef,configDef.get关联到了config,打开src\core\config.js
里面其实就是导出一个对象,对象里面有很多的属性,其中有
_assetTypes: [
'component',
'directive',
'filter'
],
_lifecycleHooks: [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed',
'activated',
'deactivated'
],
这两个便是常见的属性和生命周期,然后判断在开发环境下则有个set方法用来发出提示的
最后Object.defineProperty(Vue, ‘config’, configDef),定义构造函数的一个静态
属性config,分别有get和set的方法的,然后绑定util,set,delete,nextTick在Vue
构造函数上面,然后创建一个options空对象,然后遍历config的_assetTypes,也就是
上面说的三个属性,把它们的名字加上s复数,即components,directives,filters绑定
再options下面的属性上,三个都是空对象,再有一个_base = Vue
是自身的赋值,这里暂时不知道为什么要这么做,可能是为了缓存吧,然后执行
util.extend(Vue.options.components, builtInComponents),这里是把自身的默认
组件KeepAlive绑定到了options里面,即
Vue.config
Vue.util = util
Vue.set = set
Vue.delete = del
Vue.nextTick = util.nextTick
Vue.options = {
components: {
KeepAlive
},
directives: {},
filters: {},
_base: Vue
}
最后执行4个方法
initUse(Vue)
initMixin(Vue)
initExtend(Vue)
initAssetRegisters(Vue)
这4个方法对应当前目录的4个js文件,首先是use.js,这里给Vue挂载了静态方法,并非原型上的
这个我们用的比较多,用来安装插件的,比如Vue.use(VueRouter)路由插件就是这样子,这里首先
判断installed是否安装过了,然后执行toArray方法,返回一个数组,toArray方法解释在
methods realizes目录下找到对应名字文件夹,然后args.unshift(this)在这个返回的数组头部
添加Vue构造函数,然后执行这个install方法,一般插件的install方法形式如官方文档所写
MyPlugin.install = function (Vue, options) {
// 1. 添加全局方法或属性
Vue.myGlobalMethod = function () {
// 逻辑...
}
// 2. 添加全局资源
Vue.directive('my-directive', {
bind (el, binding, vnode, oldVnode) {
// 逻辑...
}
})
// 3. 注入组件
Vue.mixin({
created: function () {
// 逻辑...
}
})
// 4. 添加实例方法
Vue.prototype.$myMethod = function (methodOptions) {
// 逻辑...
}
}
Vue.use(MyPlugin)
这里就是执行这个install方法了,即调用Vue的全局方法,注册指令,在prototype上挂载方法
或者使用全局mixin等等,这里还有个else语句,也就是install不是函数的情况,那么这种情况
应该是直接传入一个function,形如下面这样,但最后都是启动这个方法,在Vue构造函数上面做
一些操作
Vue.use(function (Vue, options) {
// 1. 添加全局方法或属性
Vue.myGlobalMethod = function () {
// 逻辑...
}
// 2. 添加全局资源
Vue.directive('my-directive', {
bind (el, binding, vnode, oldVnode) {
// 逻辑...
}
})
// 3. 注入组件
Vue.mixin({
created: function () {
// 逻辑...
}
})
// 4. 添加实例方法
Vue.prototype.$myMethod = function (methodOptions) {
// 逻辑...
}
})
最后则plugin.installed = true,设置为已经安装了,这样就不会重复安装插件了,这样
initUse(Vue)方法大致明白了思路
到了mixin.js文件,也就是第二个方法initMixin(Vue)
到了extend.js文件,也就是第二个方法initExtend(Vue)
这两个文件涉及到比较多的问题,加之个人水平有限,暂且留着,这里简单说下
mixin文件主要绑定了Vue.mixin方法,就是常用的全局混合,里面调用了mergeOptions合并策略
这个非常重要,后面细说,extend文件绑定了Vue.extend方法,这个就是常见的vue构造器,可以
理解为创建一个子组件吧,还添加了一个属性Vue.cid = 0。
这里直接看第4个方法initAssetRegisters(Vue),找到assets.js文件
这里又是遍历这个数组_assetTypes,这里和上面说的有点类似,当初看的时候有点懵逼了
其实是这样的
// 上面绑定是这这样的,在Vue.options上面绑定的
Vue.options = {
components: {
KeepAlive
},
directives: {},
filters: {}
}
// 这里绑定的是这样的
Vue.component = function(id, definition){
}
Vue.directive = function(id, definition){
}
Vue.filter = function(id, definition){
}
// 一个在Vue的options属性上,一个直接挂载在Vue上,一个后面带有复数s,一个没有
这个三个方法都是同一个函数操作,接受两个参数,第一个是str即指令名称,第二个是fun或
者obj,如果没有第二个参数则返回对应的指令,比如
var directiveData = Vue.directive('show')
console.log(directiveData)
// 这样可以看到v-show指令的情况,这个情况一般较少
Vue.filter('capitalize', function (value) {
if (!value) return ''
value = value.toString()
return value.charAt(0).toUpperCase() + value.slice(1)
})
var filterData = Vue.filter('capitalize')
console.log(filterData)
// 这样可以看到自定义过滤器capitalize的方法,如果之前没有注册这个过滤器,那么则返回undefined
// 这里我们还是按照平常使用的方法例子来说,首先是指令
// 文档提供了两种方式注册指令
1.
Vue.directive('demo', {
bind: function (el, binding, vnode) {
var s = JSON.stringify
el.innerHTML =
'name: ' + s(binding.name) + '
' +
'value: ' + s(binding.value) + '
' +
'expression: ' + s(binding.expression) + '
' +
'argument: ' + s(binding.arg) + '
' +
'modifiers: ' + s(binding.modifiers) + '
' +
'vnode keys: ' + Object.keys(vnode).join(', ')
}
})
2.
Vue.directive('color-swatch', function (el, binding) {
el.style.backgroundColor = binding.value
})
// 但是会发现,第2种最后会经过
if (type === 'directive' && typeof definition === 'function') {
definition = { bind: definition, update: definition }
}
// 这个if,最后还是变成了第1种的方式变成了
{
bind: function (el, binding) {
el.style.backgroundColor = binding.value
},
update: function (el, binding) {
el.style.backgroundColor = binding.value
}
}
// 最后挂载在Vue构造函数上面this.options[type + 's'][id] = definition
// return这个方法,这个倒是比较少用到
// 还记得Vue.options这个值是这样的
Vue.options = {
components: {
KeepAlive
},
directives: {
// 这两个是Vue自带的,在另一个地方初始化的,这里暂时这样理解就好,存在这两个方法的
model: {
},
show: {
},
// 如果经过了第一种方法注册指令,那么就会添加下面一个了
demo: {
bind: function () {
// some methods
}
}
},
filters: {}
}
当然,注册自定义指令还有其他的生命周期钩子和参数,可以参考文档自定义指令
// 然后是注册过滤器,方法和指令一样,只不过三个if都不走
// 直接是this.options[type + 's'][id] = definition
// 例如文档例子
Vue.filter('capitalize', function (value) {
if (!value) return ''
value = value.toString()
return value.charAt(0).toUpperCase() + value.slice(1)
})
var filterData = Vue.filter('capitalize')
console.log(filterData)
// 这样就可以注册一个过滤器了
// 最后就剩下component方法了,这个是注册组件用的,按照文档例子说起
Vue.component('my-component', {
name: 'my-name',
template: '{{ message }}',
data () {
return {
message: 'hello'
}
}
})
// 首先走第1个if,这里有个config.isReservedTag(id)方法,方法解释看methods realizes目录
// 这里构建前的源码的config.isReservedTag方法找到的是一个no方法,但其实这个方法在
// platforms\web\util\element.js文件里面,这里直接看构建后的源码即可
// 这个方法就是不允许用户使用html原有的标签作为组件的标签名字,然后到了第2个if语句,这里有个
// 方法isPlainObject,方法解释看methods realizes目录,这里这个是一个obj,如果没有传入name
// 则会使用id也就是注册的组件名字作为name,上面name是my-name,如果没有则name就是my-component
// 然后调用definition = this.options._base.extend(definition),这个其实就是上面跳过的方法
// initExtend(Vue)同样的,其实这里的this.options._base就是Vue构造函数,因为
// console.log(this.options._base === Vue) // => true
// 最后挂载在this.options上面,就变成了这样
// 还记得Vue.options这个值是这样的
Vue.options = {
components: {
KeepAlive: {
},
// 这两个是Vue自带的,在另一个地方初始化的,这里暂时这样理解就好,存在这两个方法的
Transition: {
},
TransitionGroup: {
},
my-component: {
}
},
directives: {
// 这两个是Vue自带的,在另一个地方初始化的,这里暂时这样理解就好,存在这两个方法的
model: {
},
show: {
},
// 如果经过了第一种方法注册指令,那么就会添加下面一个了
demo: {
bind: function () {
// some methods
}
}
},
filters: {}
}
剩下的几个问题
Vue.set = set // 涉及到Vue的数据响应式系统,先保留
Vue.delete = del // 涉及到Vue的数据响应式系统,先保留
Vue.nextTick = util.nextTick // 水平有限,看不懂 - -#
initMixin(Vue) // 这个后面再讲
initExtend(Vue) // 水平有限,看不懂 - -#
综上所述:
initGlobalAPI 的作用是在 Vue 构造函数上挂载静态属性和方法,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(){}
Vue.prototype.$isServer
Vue.version = '__VERSION__'