一般我们在vue项目这样写
import Vue from "vue";
new Vue({
el: "#app",
data:(){
return {
message:"vue.js"
}
}
})
<div id="app">
{{ message }}
</div>
通过分析2.7.14版本
来看看vue如何一步步把数据渲染到dom中
定义 Vue
入口 src/core/instance/index.ts
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'
import type { GlobalAPI } from 'types/global-api'
function Vue(options) {
if (__DEV__ && !(this instanceof Vue)) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
//@ts-expect-error Vue has function type
initMixin(Vue)
//@ts-expect-error Vue has function type
stateMixin(Vue)
//@ts-expect-error Vue has function type
eventsMixin(Vue)
//@ts-expect-error Vue has function type
lifecycleMixin(Vue)
//@ts-expect-error Vue has function type
renderMixin(Vue)
export default Vue as unknown as GlobalAPI
vue不使用类的方式来定义,而是使用es5的方式,为了分模块对vue进行扩展,方便维护,如果使用class把vue所有的静态方法和实列方法定义在一个文件中,不方便维护;
从代码中可以看到如果不使用new关键字来调用vue,会报警告;
当new一个vue,会执行vue的实例方法 this._init(options),实列方法 _init定义在 initMixin(Vue)中
Vue.prototype._init
src/core/instance/init.ts
export function initMixin(Vue: typeof Component) {
Vue.prototype._init = function (options?: Record<string, any>) {
const vm: Component = this
// a uid
vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */
if (__DEV__ && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
// a flag to mark this as a Vue instance without having to do instanceof
// check
vm._isVue = true
// avoid instances from being observed
vm.__v_skip = true
// effect scope
vm._scope = new EffectScope(true /* detached */)
vm._scope._vm = 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 as any)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor as any),
options || {},
vm
)
}
/* istanbul ignore else */
if (__DEV__) {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate', undefined, false /* setContext */)
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
/* istanbul ignore if */
if (__DEV__ && 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)
}
}
}
看看里面几个重要的逻辑
首先,合并配置,因为不是组件,是普通元素的挂载,options._isComponent是undefined,所以走的逻辑是
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor as any),
options || {},
vm
)
把new Vue({...})中传入的各种属性合并到实例的 $options 上面, 所以可以在 $options 上面拿到 el,data,methods等。
然后,做一些初始化,
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate', undefined, false /* setContext */)
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
在beforecreted和created中间,初始化injection,初始化状态state(包括props,methods,data,computed,watch),初始化provide,
所以在created生命周期中才能拿到state的数据
最后通过
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
实现挂载
补:访问data中的数据
为什么可以在生命周期或者计算属性,或者方法中通过this.xxx拿到data中的数据?
在实例方法_init中调用一个方法 initState(vm),initState又调用了initData(vm), initData中调用proxy(vm, _data, key)
src/core/instance/state.ts
export function initState(vm: Component) {
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
// Composition API
initSetup(vm)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
const ob = observe((vm._data = {}))
ob && ob.vmCount++
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
function initData(vm: Component) {
let data: any = vm.$options.data
data = vm._data = isFunction(data) ? getData(data, vm) : data || {}
if (!isPlainObject(data)) {
data = {}
__DEV__ &&
warn(
'data functions should return an object:\n' +
'https://v2.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 (__DEV__) {
if (methods && hasOwn(methods, key)) {
warn(`Method "${key}" has already been defined as a data property.`, vm)
}
}
if (props && hasOwn(props, key)) {
__DEV__ &&
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
const ob = observe(data)
ob && ob.vmCount++
}
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: any) {
handleError(e, vm, `data()`)
return {}
} finally {
popTarget()
}
}
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)
}
在initData中,
let data: any = vm.$options.data
data = vm._data = isFunction(data) ? getData(data, vm) : data || {}
拿到data的数据赋值给 vm._data
proxy(vm, _data, key)
可以看到,当通过vm.xxx访问data中的数据实际上是访问vm._data.xxx