重学Vue源码,根据黄轶大佬的vue技术揭秘,逐个过一遍,巩固一下vue源码知识点,毕竟嚼碎了才是自己的,所有文章都同步在 公众号(道道里的前端栈) 和 github 上。
正文
我们一般在 main.js
中会这样写:
import Vue from "vue";
new Vue({
el: "#app"
})
// 或者
new Vue({
}).$mount("#app")
接着在里面定义 data
, methods
,mounted
等,既然 Vue
是可以 new 出来的,那 Vue 就应该是一个构造函数,在源码中,分为 定义Vue构造函数 和 扩展Vue构造函数,总体的代码定义在:src/platforms/web/runtime/index.js
中:
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, isChrome } 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
// 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)
}
// ...
export default Vue
可以看出来第一句:import Vue from 'core/index'
引入了定义好的 Vue,后面基本上都是对 Vue 这个对象进行了一些扩展,所以可以分为两步看,先看初始化 Vue,再看扩展。
定义 Vue 的代码在 src/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)
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
})
Vue.version = '__VERSION__'
export default Vue
这里除了使用 Object.defineProperty
去定义 Vue 的东西之外,比较关键的就是 import Vue from './instance/index'
和 initGlobalAPI(Vue)
了,我们一个一个来看,先看第一行的 Vue 的定义,进入 src/core/instance/index.js
中,可以看到:
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 的构造函数,this instanceof Vue
这行代码规定了 Vue 只能是一个构造函数,所以我们的代码中使用了 new Vue({})
去实例化 Vue。
后面的就是一堆 Mixin,比如 initMixin初始化混入,stateMixin状态混入,eventMixi事件混入,lifecycleMixin生命周期混入,renderMixin渲染混入。
来简单看一个 initMixin 方法,看它做了什么事情,代码在 src/core/instance/init.js
。
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
// a flag to avoid this being observed
vm._isVue = true
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
挑一个说一下:里面的 mergeOptions
其实就是把传入的 options 最终合并到 vm 的 $options
上,所以就可以通过 $options.el
访问 new Vue({})
代码里面的 el,通过 $options.data
访问 new Vue({})
代码里面的 data,methods 和 mounted 等也是同样的道理。紧接着后面初始化了生命周期,事件,渲染,状态等等,最后有这样的代码:
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
意思就是说我们代码里定义的的 el
会通过 $mount
挂载到 vm 上,也就是上面的 #app
了。一旦执行完 $mount
之后,dom上的双向绑定就会生效,定义的数据就会渲染到dom上。
再来看上面提到的 initGlobalAPI(Vue)
,这个方法定义在:src/core/global-api/index.js
中:
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)
}
可以看出来它在 Vue 上扩展了一些全局方法,扩展的方法都可以在官方文档的API中找到。
现在想一个问题:为什么 mounted
里面可以通过 this.
来获取到 data
里的 name
呢?
import Vue from "vue";
new Vue({
el: "#app",
mounted(){
console.log(this.name);
},
//先写成这样,后面说为什么报错
data: {
name: "abc"
}
})
还记得上面的初始化方法 initMixin 么,里面有一个 initState(vm)
,我们来看下它,在 src/core/instance/state.js
中:
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
可以看到它的逻辑,如果在 $options
定义了 props
,就初始化 props,如果在 $options
定义了 methods
,就初始化 methods,如果在 $options
定义了 data
,就初始化 data,现在重点看下 initData
,因为例子中是访问的 data 里的属性name。
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(key)) {
proxy(vm, `_data`, key)
}
}
// observe data
observe(data, true /* asRootData */)
}
export function getData (data: Function, vm: Component): any {
// #7573 disable dep collection when invoking data getters
pushTarget()
try {
return data.call(vm, vm)
} catch (e) {
handleError(e, vm, `data()`)
return {}
} finally {
popTarget()
}
}
它的逻辑大致是,判断 data 是不是一个 function,如果是就走下面的 getData
,把 vm 指向 data,这样就可以使用 this.data
了,同时把这个方法赋值给了 vm._data
,如果不是一个 function,就重新定义这个 data 为一个对象,紧接着拿到 props,keys 和 methods做一层遍历,如果在 data 中定义一个变量,就不能在 props 中定义这个变量了,methods 中也是如此,因为它们最终都会挂载到vm上,也就是new Vue
实例上,**这就是为什么我们定义同样的变量在 methods 或者 props 中就会报错的原因。**所以上面的例子中的 data 要返回一个对象:
import Vue from "vue";
new Vue({
el: "#app",
mounted(){
console.log(this.name);
},
data() {
return {
name: "abc"
}
}
})
那为什么可以通过 this.name 拿到 data 里的 name 呢?就是后面的 proxy
函数。
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
export function proxy (target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
proxy
定义了 get 和 set,然后通过 Object.defineProperty
在 target(就是vm) 上定义了 _data
属性,get 方法可以拿到 vm[_data][key]
,这个 key 就是代码中传入的 name ,也就是说可以通过 this._data.name
获取代码中的 data 里的 name,也就是说上面的 console.log(this.name)
其实就是调用了 console.log(this._data.name)
。
这就是 proxy 的作用,把访问 this.name
中的 name,代理到了 this._data.name
,前面的把 vm._data
赋值给 data,代码中的 proxy(vm, "_data", key)
就是把 data 赋值给了 this._data
,所以在 methods 中,可以通过 this.name
获取到 data 里面定义的 name。