重学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。