我們通常使用vue的時候是這麼用的:
<div id="app">
{{ message }}
</div>
<script>
var app = new Vue({
el: '#app',
data() {
return {
message: 'hello Vue!'
}
}
})
</script>
這一次將圍繞這段代碼來進行分析。當我們執行這段代碼的時候頁面上將顯示Hello World
,當我們改變message
的值的時候,頁面上的值也會發生改變,這就是數據驅動。
vue
的核心思想是**數據驅動 **。以前我們在使用JQuery
的時候,我們每次改變完數據,就要調用DOM
操作(例如先獲得id='app’的元素,然後改變他們的innerHTML
),但是vue
不用,只要一改變message
的值,頁面上的值就發生了改變。這就是數據驅動。
接下來我們開始來分析代碼。首先我們new Vue()
,這個操作就像之前說的一樣會調用_init()
這個方法,這個方法定義在src/core/instance/init.js
中:
// src/core/instance/init.js
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)
}
}
我們來分析一下,代碼的一開始和結束時出現了這麼幾行,這跟vue的性能追蹤有關
,當我們在vue.config.js
中設置performace: true
時,這段代碼就會執行,
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)
}
...
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
從代碼中可知,在非生產環境下,會根據配置判斷是否使用window.performance的api進行時間監測。其中mark
方法出現在import { mark, measure } from '../util/perf'
中。
這樣就可以檢測到_init()的過程中的性能了。
在_init()
中,主要是合併了配置項、初始化生命週期、初始化事件中心、初始化渲染、初始化 data、props、computed、watcher 等等。
那麼vue是如何初始化data
的呢。我們來看一下initState(vm)
,它被定義在src/core/instance/state.js
中:
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
// 備註: 對props的處理
if (opts.props) initProps(vm, opts.props)
// 備註: 對methods的處理
if (opts.methods) initMethods(vm, opts.methods)
// 備註: 對data的處理
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
// 備註: 對computed的處理
if (opts.computed) initComputed(vm, opts.computed)
// 備註: 對watch的處理
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
研究一下initData(vm)
,它被定義在src/core/instance/state.js
:
// src/core/instance/state.js
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 */)
}
在這裏中,會對data的數據進行處理,放在_data上,然後判斷類型,判斷和props
、methods
是否重名,然後進行代理(proxy),最後就是調用observe
對數據做了響應式處理。
補充:proxy
的代碼。我們在使用this.message
就可以獲得它的值,離不開proxy
的功勞。
// src/core/instance/state.js
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)
}
這裏其實是做了一層代理,先是定義一個屬性描述符sharedPropertyDefinition
,然後調用Object.defineProperty
,意思就是當訪問/修改target.key
時會觸發get/set
,代理到this[sourceKey][key]
(即vm[_data][key]
)上。(這就是爲啥一開始把data放在_data上)
總結
在new Vue
的時候,會調用_init
方法,在這個方法中,會做這麼幾件事情:
- 判斷是否開啓
性能追蹤
,進行相應的處理; - 合併了配置項、初始化生命週期、初始化事件中心、初始化渲染、初始化 data、props、computed、watcher 等等;
- 最後通過判斷有無
vm.$options.el
進行vm.$mount
掛載。
本節中還着重分析了vue如何處理data
,會調用_init()
在中的initState
方法,現將data
掛載到vue._data
中,對她進行類型判斷,然後在跟props
、methods
進行對比,之後用proxy
,確保每次調用this.data
.xx時就可以調用到$vm._data.xx
,最後還對data進行了響應式處理。