這是官網(wǎng)上一個最簡單的例子
<div id="app">
{{ message }}
</div>
<script>
var app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
}
})
</script>
接下來我通過在chrome中一步步運行代碼來理清其內部邏輯哥蔚。
首先當然是調用構造函數(shù)
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) // 主要就是這個步驟嚷量,調用原型中的_init方法
}
//下面的這些方法都是給Vue.prototype增加方法和屬性的,具體意義看vue官網(wǎng)api就能了解
initMixin(Vue) // 給原型增加了_init()
stateMixin(Vue) // 增加$data,$props,$set(),$delete(),$watch()
eventsMixin(Vue) // 增加$on(),$once(),$off(),$emit()
lifecycleMixin(Vue) // 增加_update(),$forceUpdate(),$destroy()
renderMixin(Vue) // 增加_render()
然后就進入_init()方法了
var uid$1 = 0;
function initMixin (Vue) {
Vue.prototype._init = function (options) {
var vm = this; // // 定義一個vm指向Vue實例(這里就是app)
// a uid
vm._uid = uid$1++;
var startTag, endTag;
/* istanbul ignore if */
if ("development" !== 'production' && config.performance && mark) {
startTag = "vue-perf-init:" + (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屬性疹蛉,這個屬性是合并之后的options磺平。詳細內容看1mergeOptions
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
);
}
/* istanbul ignore else */
{
initProxy(vm);
}
// expose real self
vm._self = vm;
// 下面這些都是和vue生命周期有關的一些操作了
initLifecycle(vm) // 2初始化生命周期
initEvents(vm) // 3初始化事件
initRender(vm) // ?初始化render
callHook(vm, 'beforeCreate') // ?beforeCreate
initInjections(vm) // ?初始化inject
initState(vm) // ?對data/props/computed/watch等進行監(jiān)聽
initProvide(vm) // ?初始化provide
callHook(vm, 'created') // ?create
/* istanbul ignore if */
if ("development" !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false);
mark(endTag);
measure(((vm._name) + " init"), startTag, endTag);
}
if (vm.$options.el) {
vm.$mount(vm.$options.el); // 1?掛載
}
};
}
1.mergeOptions
這個方法顧名思義就是合并分支的。那我們先看沒合并之前的vm
再看這個方法運行之后變成啥樣了
明顯能注意到的就是沃但,vm.$options比之前的options多了components,directives,filters,_base磁滚,然后data變成一個方法了。因此我們大概知道這個方法就是添加vm.$options然后把基礎的options和傳入的options合并(其實看函數(shù)名就差不多知道了)
2.initLifecycle
對比很明顯宵晚,增加了很多屬性垂攘,來看一眼代碼
function initLifecycle (vm: Component) {
const options = vm.$options
// locate first non-abstract parent
let parent = options.parent
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
vm.$parent = parent
vm.$root = parent ? parent.$root : vm // 沒有父實例的話把根實例設成自己了
vm.$children = []
vm.$refs = {}
vm._watcher = null
vm._inactive = null
vm._directInactive = false
vm._isMounted = false
vm._isDestroyed = false
vm._isBeingDestroyed = false
}
看樣子這個函數(shù)是很好理解的,主要目的是為了給vm增加$children淤刃、$parent晒他、$refs、$root钝凶。然后以下劃線開始的這些屬性看命名就能才出來是與vue生命周期有關的一些變量(下面我把以$開頭的稱為實例屬性,把_開頭的稱變量)唁影。這里放一張官網(wǎng)上的生命周期圖片耕陷,具體的在之后的內容中也會涉及到的
-
initEvents
initEvents執(zhí)行前
看上去也就是多了幾個變量,我們看看具體的函數(shù)執(zhí)行情況吧
這么一張圖看上去已經(jīng)很清晰了据沈,
vm.$options._parentListeners
未定義哟沫,因此之后的步驟也不會執(zhí)行的,整個函數(shù)就新增了兩個變量锌介。不過這個函數(shù)本來想干的事嗜诀,看作者注釋的話是想添加父實例的事件猾警,這個功能在我之后調試到相應例子之后會再進行擴散的
4.initRender
好害怕啊突然變長好多~總結一下就是又多了很多屬性($attrs、$listeners隆敢、$slots发皿、$scopedSlots、$vnode)和變量,還增加了方法($createElement)拂蝎。先看一下執(zhí)行過程
這里大致可以分為三個部分穴墅,第一又增加了變量和屬性,同樣我們在這個例子里看不出什么温自,比如說$slots這個實例屬性篙顺,第二是給vm增加了一個$createElement方法熄守,調用這個方法其實就是調用createElement方法,看命名就知道是一個生成元素的方法。第三個是調用了一個defineReactive方法段化,這個方法是什么意思呢
這些步驟基本是在做初始化
但是最重要的這里,事實上對于get堂鲜,在我們這個例子中筒扒,Dep.target為null,因此get就是返回了value也拜,而set也就是普通的設置了值而已以舒,那么這么長的代碼有什么用呢,這其實又和數(shù)據(jù)綁定有關慢哈。關于數(shù)據(jù)綁定我這里也先不再延伸蔓钟。所以這第三部分就是在給$attrs、$listeners重寫get和set
5.callHook(vm, 'beforeCreate')
function callHook (vm, hook) {
var handlers = vm.$options[hook];
if (handlers) {
for (var i = 0, j = handlers.length; i < j; i++) {
try {
handlers[i].call(vm); // 執(zhí)行beforeCreate下的每個函數(shù)
} catch (e) {
handleError(e, vm, (hook + " hook"));
}
}
}
if (vm._hasHookEvent) {
vm.$emit('hook:' + hook);
}
}
這個函數(shù)很簡單卵贱,我們這個簡單的vue實例也沒有傳beforeCreate這個選項滥沫,因此這步也可以忽略,只需要知道在create之前會調用這樣一個東西
6.initInjections
function initInjections (vm: Component) {
const result = resolveInject(vm.$options.inject, vm) //獲得vm.$options.inject的所有屬性
if (result) {
observerState.shouldConvert = false
Object.keys(result).forEach(key => {
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
defineReactive(vm, key, result[key], () => {
warn(
`Avoid mutating an injected value directly since the changes will be ` +
`overwritten whenever the provided component re-renders. ` +
`injection being mutated: "${key}"`,
vm
)
})
} else {
defineReactive(vm, key, result[key]) // 把inject監(jiān)聽起來
}
})
observerState.shouldConvert = true
}
}
瞅瞅這個簡單的代碼键俱,對于我們這個例子來說兰绣,又是可以跳過的。不過我們還需大致看看這個函數(shù)编振,它其實是對inject選項的初始化缀辩,這個眼熟的defineReactive,沒錯就是對inject的每個屬性的get踪央,set都重寫了一下臀玄。
7.initState
這個函數(shù)明顯是很重要的,因為它處理了props/methods/data/computed/watch等選項畅蹂,而這些選項又是我們經(jīng)常需要用到的健无。那我們從源碼看看它怎么處理的
function initState (vm: Component) {
// 首先在vm上初始化一個_watchers數(shù)組,緩存這個vm上的所有watcher
vm._watchers = []
// 獲取options,包括在new Vue傳入的液斜,同時還包括了Vue所繼承的options
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
。
function initData (vm: Component) {
// 獲取data
let data = vm.$options.data
// 因為data有可能是個函數(shù)臼膏,這里給它轉換了一下
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
// 收集data的自身屬性名
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') {
// data的屬性不能同時在method中
if (methods && hasOwn(methods, key)) {
warn(
`method "${key}" has already been defined as a data property.`,
vm
)
}
}
// data的屬性不能同時在props中
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)) {
// 給data里的每個屬性重寫了get和set
proxy(vm, `_data`, key)
}
}
// 時刻觀察data
// observe data
observe(data, true /* asRootData */)
}
這個observe函數(shù)實際上做了很多事情硼被。
8.initProvide
function initProvide (vm: Component) {
const provide = vm.$options.provide
if (provide) {
vm._provided = typeof provide === 'function'
? provide.call(vm)
: provide
}
}
初始化provide選項。超簡單的代碼讶请,誰都能看懂祷嘶,加了個_provided變量,和我這個例子好像也沒有什么關系
- callHook(vm, 'created')
和callHook(vm, 'beforeCreate')一樣一樣的夺溢,不再贅述了
10.mount
if (vm.$options.el) {
vm.$mount(vm.$options.el) //如果傳入了el就掛載
}
像這段代碼论巍,和生命周期圖結合起來看,就一目了然风响,如果有el選項的話就掛載
那么再來看看updateComponent
這里首先要執(zhí)行vm._render()
回到_update
我們傳了舊的和新的node進去
這步執(zhí)行完頁面上是這樣的
直到下圖執(zhí)行完后嘉汰,頁面才算更新完畢
說的可能還有點不太完整。整個_init方法實際上只是到mount部分為止状勤。真的要銷毀的話得手動調用$destroy鞋怀。還有很多函數(shù)沒有仔細擴散,等之后調試到相關例子的時候會再補充持搜。