學習任何一門框架,都不可能一股腦兒的從入口代碼從上到下,把代碼看完,
這樣其實是很枯燥的,我想也很少有人這么干,或者這么干着干着可能干不下去了。
因為肯定很無聊。
我們先從一個最最簡單的小例子,來查看new Vue(options)實例,這個過程發生了什么。
vm實例上的屬性又如何添加上去的,又如何渲染到瀏覽器頁面上的。
關於vue的數據依賴和虛擬dom都是重點,必然會在以后的帖子記錄。
這篇帖子就根據下例子,看看實例化一個vm實例做了啥吧。
先把小例子貼出來:
<div id="app"> <p>這是<span>靜態內容</span></p> <p>{{message}}</p> </div> <script src="../../dist/vue.js"></script> <script> var vm = new Vue({ el: '#app', data: { message: 'hi vue!' } }) console.log(vm) </script>
根據上篇介紹了vue的調式筆記,那我們快快進入源碼吧
根據vue構造函數那篇筆記,我們知道了Vue原型上有哪些方法,_init方法就是其中一個方法
我們看到_init就把實例要做的事情都做完了,當然其中有的語句所做的事,太多了。我們先一點一點開see see吧。
看圖不好玩,我把源碼取出 來 好好瞧瞧
export function initMixin (Vue: Class<Component>) { Vue.prototype._init = function (options?: Object) { const vm: Component = this // a uid vm實例唯一標識 vm._uid = uid++ let startTag, endTag /* istanbul ignore if 性能統計相關 */ if (process.env.NODE_ENV !== '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 vm._isVue = true // merge options _isComponent是內部創建子組件時才會添加為true的屬性 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 { // mergeOptions 方法 合並構造器及構造器父級上定義的options 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(`${vm._name} init`, startTag, endTag) } if (vm.$options.el) { vm.$mount(vm.$options.el) } } }
1. 給實例添加了唯一標識uid
2.性能統計相關,先忽略
3. 給實例添加_isVue,監聽對象變化時用於過濾vm
4. 選項對象如果有_isComponent,就初始化內部組件,_isComponent是內部創建子組件時才會添加為true的屬性
5. 小例子會走分支,mergeOptions 方法 合並構造器及構造器父級上定義的options,resolveConstructorOptions方法后面筆記會詳解,
mergeOptions方法接受3個參數。我們先簡單看下resolveConstructorOptions方法的定義
export function resolveConstructorOptions (Ctor: Class<Component>) { let options = Ctor.options // 有super屬性,說明Ctor是通過Vue.extend()方法創建的子類 if (Ctor.super) { const superOptions = resolveConstructorOptions(Ctor.super) const cachedSuperOptions = Ctor.superOptions if (superOptions !== cachedSuperOptions) { // super option changed, // need to resolve new options. Ctor.superOptions = superOptions // check if there are any late-modified/attached options (#4976) const modifiedOptions = resolveModifiedOptions(Ctor) // update base extend options if (modifiedOptions) { extend(Ctor.extendOptions, modifiedOptions) } options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions) if (options.name) { options.components[options.name] = Ctor } } } return options }
可以看出Ctor.options其實就是Vue構造函數自身,在Vue構造函數靜態屬性那篇筆記,Vue是擁有options屬性的,且有截圖,等下會再截圖看下,
接着在該函數中有個if語句,我們小例子會跳過的,直接返回options。因為有super屬性,說明Ctor是通過Vue.extend()方法創建的子類。那么
options是啥呢,如下圖,
回到_init方法中,mergeOptions方法的第二個參數就是我們傳入的options,第三個參數就是vm實例,把參數一起截個圖吧,好回憶
mergeOptions是Vue中處理屬性的合並策略的地方, 先看下它的定義
export function mergeOptions ( parent: Object, child: Object, vm?: Component ): Object { if (process.env.NODE_ENV !== 'production') { // 如果有options.components,則判斷是否組件名是否合法 checkComponents(child) } // 格式化child的props normalizeProps(child) // 格式化child的directives normalizeDirectives(child) const extendsFrom = child.extends if (extendsFrom) { parent = typeof extendsFrom === 'function' ? mergeOptions(parent, extendsFrom.options, vm) : mergeOptions(parent, extendsFrom, vm) } if (child.mixins) { for (let i = 0, l = child.mixins.length; i < l; i++) { let mixin = child.mixins[i] if (mixin.prototype instanceof Vue) { mixin = mixin.options } parent = mergeOptions(parent, mixin, vm) } } const options = {} let key for (key in parent) { mergeField(key) } for (key in child) { if (!hasOwn(parent, key)) { mergeField(key) } } function mergeField (key) { const strat = strats[key] || defaultStrat options[key] = strat(parent[key], child[key], vm, key) } return options }
該函數主要返回一個數據合並后的options,
我們的小例子比較簡單, so關於判斷是否組件名是否合法,
格式化child的props, 格式化child的directives
extends, mixins 先跳過。
我們直接看怎么把屬性合並到options = {}這個對象上的
首先遍歷parent對象,然后通過mergeField函數,把components,
directives, filters, _base屬性先添加到options對象上,值為
strats對象上的靜態方法。
然后遍歷child對象,把el, data屬性也添加到options = {} 這個對象上
值為strats對象上對應的靜態方法。
那我們先看看strats這個對象上有哪些靜態方法,源碼如下(src/util/options.js)
const strats = config.optionMergeStrategies if (process.env.NODE_ENV !== 'production') { strats.el = strats.propsData = function (parent, child, vm, key) { /**/ } } strats.data = function ( parentVal: any, childVal: any, vm?: Component ): ?Function { /**/ } function mergeHook ( parentVal: ?Array<Function>, childVal: ?Function | ?Array<Function> ): ?Array<Function> { return childVal ? parentVal ? parentVal.concat(childVal) : Array.isArray(childVal) ? childVal : [childVal] : parentVal } config._lifecycleHooks.forEach(hook => { strats[hook] = mergeHook }) function mergeAssets (parentVal: ?Object, childVal: ?Object): Object { /**/ } config._assetTypes.forEach(function (type) { strats[type + 's'] = mergeAssets }) strats.watch = function (parentVal: ?Object, childVal: ?Object): ?Object { /* istanbul ignore if */ /**/ } strats.props = strats.methods = strats.computed = function (parentVal: ?Object, childVal: ?Object): ?Object { if (!childVal) return Object.create(parentVal || null) if (!parentVal) return childVal /**/ } const defaultStrat = function (parentVal: any, childVal: any): any { return childVal === undefined ? parentVal : childVal }
以上是縮減版的代碼,其實看下截圖,會一目了然
可以看到其實就是我們new Vue(options) 中的options對象中的可選參數。我們小例子只傳了el, data,
我們看看通過mergeOptions方法合並后的options長的什么鳥樣,如圖:
其實小例子只是走個過程,沒必要把所有函數代碼弄懂,先把大體流程走完,后續代碼在一一分析。
6. 回到vm_init()方法中,接着走initProxy(vm)這個語句,這個語句其實就是給vm實例添加了一個_renderProxy屬性,值為為一個Proxy代理對象,生產環境就是vm自身。
接下來的每個語句都有好多代碼啊,我們一個個 look see see
7. initLifecycle 方法的定義
export 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 }
初始化生命周期,該函數只是給vm添加了
$parent, $root, $children, $refs, _watcher,
_inactive, _directInactive, _isMounted, _isDestroyed
_isBeingDestroyed屬性。
options.abstract用於判斷是否是抽象組件,
組件的父子關系建立會跳過抽象組件,抽象組件比如keep-alive、transition等。
所有的子組件$root都指向頂級組件。
8. initEvents方法的定義
export function initEvents (vm: Component) { vm._events = Object.create(null) vm._hasHookEvent = false // init parent attached events const listeners = vm.$options._parentListeners if (listeners) { updateComponentListeners(vm, listeners) } }
該方法初始化事件相關的屬性,給vm實例添加了_events, _hasHookEvent屬性
_parentListeners是父組件中綁定在自定義標簽上的事件,供子組件處理。
9. initRender方法的定義
export function initRender (vm: Component) { vm.$vnode = null // the placeholder node in parent tree vm._vnode = null // the root of the child tree vm._staticTrees = null const parentVnode = vm.$options._parentVnode const renderContext = parentVnode && parentVnode.context vm.$slots = resolveSlots(vm.$options._renderChildren, renderContext) vm.$scopedSlots = emptyObject // bind the createElement fn to this instance // so that we get proper render context inside it. // args order: tag, data, children, normalizationType, alwaysNormalize // internal version is used by render functions compiled from templates vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false) // normalization is always applied for the public version, used in // user-written render functions. vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true) }
這里先給vm添加了$vnode, _vnode, _staticTrees, $slots, $scopedSlots, _c, $createElement
屬性或者方法(添加了一些虛擬dom、slot等相關的屬性和方法)
10. 調用beforeCreate鈎子
11. 調用initInjections(vm)方法,我們小例子比較簡單,不會進入if語句中
export function initInjections (vm: Component) { const inject: any = vm.$options.inject if (inject) { // inject is :any because flow is not smart enough to figure out cached // isArray here const isArray = Array.isArray(inject) const keys = isArray ? inject : hasSymbol ? Reflect.ownKeys(inject) : Object.keys(inject) for (let i = 0; i < keys.length; i++) { const key = keys[i] const provideKey = isArray ? key : inject[key] let source = vm while (source) { if (source._provided && provideKey in source._provided) { /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production') { defineReactive(vm, key, source._provided[provideKey], () => { 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, source._provided[provideKey]) } break } source = source.$parent } } } }
將父組件provide中定義的值,通過inject注入到子組件,且這些屬性不會被觀察
12. initState(vm)
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) initWatch(vm, opts.watch) }
該方法主要就是操作數據了,props、methods、data、computed、watch,
從這里開始就涉及到了Observer、Dep和Watcher,下個筆記再記錄
13. initProvide(vm)
export function initProvide (vm: Component) { const provide = vm.$options.provide if (provide) { vm._provided = typeof provide === 'function' ? provide.call(vm) : provide } }
也不會進分支,先略過
14. 調用created鈎子函數。
可以看到在created鈎子函數調用前, 基本就是對傳入數據的格式化、數據的雙向綁定、以及一些屬性的初始化。
現在來看下實例的屬性和方法
接下來看看怎么把html模板中的屬性出來的
15. vm.$mount(vm.$options.el)
const mount = Vue.prototype.$mount // 重寫Vue構造函數原型上的$mount方法 Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && query(el) /* istanbul ignore if */ if (el === document.body || el === document.documentElement) { process.env.NODE_ENV !== 'production' && warn( `Do not mount Vue to <html> or <body> - mount to normal elements instead.` ) return this } const options = this.$options // resolve template/el and convert to render function if (!options.render) { let template = options.template if (template) { if (typeof template === 'string') { if (template.charAt(0) === '#') { template = idToTemplate(template) /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && !template) { warn( `Template element not found or is empty: ${options.template}`, this ) } } } else if (template.nodeType) { template = template.innerHTML } else { if (process.env.NODE_ENV !== 'production') { warn('invalid template option:' + template, this) } return this } } else if (el) { template = getOuterHTML(el) } if (template) { /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { mark('compile') } const { render, staticRenderFns } = compileToFunctions(template, { shouldDecodeNewlines, delimiters: options.delimiters }, this) options.render = render options.staticRenderFns = staticRenderFns /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { mark('compile end') measure(`${this._name} compile`, 'compile', 'compile end') } } } return mount.call(this, el, hydrating) }
該方法主要拿到template模板,然后通過compileToFunctions方法的返回值給vm實例
的$options添加render屬性,值為一個匿名函數該匿名函數返回值為:
with(this){return _c('div',{attrs:{"id":"app"}},[_m(0),_v(" "),_c('p',[_v(_s(message))])])}
還添加了一個staticRenderFns屬性,值為一個數組,數組元素為匿名函數:
anonymous() {
with(this){return _c('p',[_v("這是"),_c('span',[_v("靜態內容")])])}
}
至於compileToFunctions函數先不拿出來看了,目前先知道它干了啥,就行了(至於實例的_c,_v,_m這些方法何時掛載上去的,前面筆記已經說過了)
之后調用 mount.call(this, el, hydrating) 方法
Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && inBrowser ? query(el) : undefined return mountComponent(this, el, hydrating) }
該方法又調用mountComponent(this, el, hydrating)
export function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean ): Component { vm.$el = el if (!vm.$options.render) { vm.$options.render = createEmptyVNode if (process.env.NODE_ENV !== 'production') { /* istanbul ignore if */ if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') || vm.$options.el || el) { warn( 'You are using the runtime-only build of Vue where the template ' + 'compiler is not available. Either pre-compile the templates into ' + 'render functions, or use the compiler-included build.', vm ) } else { warn( 'Failed to mount component: template or render function not defined.', vm ) } } } callHook(vm, 'beforeMount') let updateComponent /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { updateComponent = () => { const name = vm._name const id = vm._uid const startTag = `vue-perf-start:${id}` const endTag = `vue-perf-end:${id}` mark(startTag) const vnode = vm._render() mark(endTag) measure(`${name} render`, startTag, endTag) mark(startTag) vm._update(vnode, hydrating) mark(endTag) measure(`${name} patch`, startTag, endTag) } } else { updateComponent = () => { vm._update(vm._render(), hydrating) } } vm._watcher = new Watcher(vm, updateComponent, noop) hydrating = false // manually mounted instance, call mounted on self // mounted is called for render-created child components in its inserted hook if (vm.$vnode == null) { vm._isMounted = true callHook(vm, 'mounted') } return vm }
小例子在該方法大致流程是這么走的
因為vm.$options.render就是給匿名函數,所以不會走if分支,
然后調用beforeMount鈎子函數
再然后定義一個updateComponent函數,這個函數怎么執行是個關鍵
然后給實例添加了一個_watcher屬性,值為Watcher實例
然后如果vm.$vnode == null則把vm._isMounted變量置為true,然后調用mounted鈎子函數
最后返回vm實例,可以鏈式調用。
觸發updateCOMPONENT函數是new Watcher,先看看Watcher類的定義
constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: Object ) { this.vm = vm vm._watchers.push(this) if (options) { this.deep = !!options.deep this.user = !!options.user this.lazy = !!options.lazy this.sync = !!options.sync } else { this.deep = this.user = this.lazy = this.sync = false } ... this.expression = process.env.NODE_ENV !== 'production' ? expOrFn.toString() : '' if (typeof expOrFn === 'function') { this.getter = expOrFn } else { this.getter = parsePath(expOrFn) if (!this.getter) { this.getter = function () {} process.env.NODE_ENV !== 'production' && warn( `Failed watching path: "${expOrFn}" ` + 'Watcher only accepts simple dot-delimited paths. ' + 'For full control, use a function instead.', vm ) } } this.value = this.lazy ? undefined : this.get() } get () { pushTarget(this) let value const vm = this.vm if (this.user) { try { value = this.getter.call(vm, vm) } catch (e) { handleError(e, vm, `getter for watcher "${this.expression}"`) } } else { value = this.getter.call(vm, vm) } if (this.deep) { traverse(value) } popTarget() this.cleanupDeps() return value }
在構造函數中,expOrFn也就是updateComponent賦值給this.getter,
並且在獲取this.value的值時會調用this.get(),這里的this.lazy默認值是false,
在computed屬性中創建的Watcher會傳入true。
在this.get()中,會調用this.getter,所以上面的例子中,updateComponent方法會被調用,
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
回到該函數,先執行實例的_render函數,該函數主要生成虛擬dom
然后執行實例的update方法
Vue.prototype._render = function (): VNode { const vm: Component = this const { render, staticRenderFns, _parentVnode } = vm.$options if (vm._isMounted) { // clone slot nodes on re-renders for (const key in vm.$slots) { vm.$slots[key] = cloneVNodes(vm.$slots[key]) } } vm.$scopedSlots = (_parentVnode && _parentVnode.data.scopedSlots) || emptyObject if (staticRenderFns && !vm._staticTrees) { vm._staticTrees = [] } // set parent vnode. this allows render functions to have access // to the data on the placeholder node. vm.$vnode = _parentVnode // render self let vnode try { vnode = render.call(vm._renderProxy, vm.$createElement) } catch (e) { handleError(e, vm, `render function`) // return error render result, // or previous vnode to prevent render error causing blank component /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production') { vnode = vm.$options.renderError ? vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e) : vm._vnode } else { vnode = vm._vnode } } // return empty vnode in case the render function errored out if (!(vnode instanceof VNode)) { if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) { warn( 'Multiple root nodes returned from render function. Render function ' + 'should return a single root node.', vm ) } vnode = createEmptyVNode() } // set parent vnode.parent = _parentVnode return vnode }
該函數主要是 vnode = render.call(vm._renderProxy, vm.$createElement)語句
函數調用過程中的this,是vm._renderProxy,是一個Proxy代理對象或vm本身。暫且把它當做vm本身。
_c是(a, b, c, d) => createElement(vm, a, b, c, d, false)。
createElement函數做了這些事:
a是要創建的標簽名,這里是div。
接着b是data,也就是模板解析時,添加到div上的屬性等。
c是子元素數組,所以這里又調用了_c來創建一個p標簽。
_v是createTextVNode,也就是創建一個文本結點。
_s是_toString,也就是把message轉換為字符串,在這里,因為有with(this),
所以message傳入的就是我們data中定義的第一個vue實例。
所以,render函數返回的是一個VNode對象,也就是我們的虛擬dom對象。
它的返回值,將作為vm._update的第一個參數。
接着看下update方法
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { const vm: Component = this if (vm._isMounted) { callHook(vm, 'beforeUpdate') } const prevEl = vm.$el const prevVnode = vm._vnode const prevActiveInstance = activeInstance activeInstance = vm vm._vnode = vnode // Vue.prototype.__patch__ is injected in entry points // based on the rendering backend used. if (!prevVnode) { // initial render vm.$el = vm.__patch__( vm.$el, vnode, hydrating, false /* removeOnly */, vm.$options._parentElm, vm.$options._refElm ) } else { // updates vm.$el = vm.__patch__(prevVnode, vnode) } activeInstance = prevActiveInstance // update __vue__ reference if (prevEl) { prevEl.__vue__ = null } if (vm.$el) { vm.$el.__vue__ = vm } // if parent is an HOC, update its $el as well if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) { vm.$parent.$el = vm.$el } // updated hook is called by the scheduler to ensure that children are // updated in a parent's updated hook. }
從mountComponent中知道創建Watcher對象先於vm._isMounted = true。
所以這里的vm._isMounted還是false,不會調用beforeUpdate鈎子函數。
下面會調用vm.__patch__,在這一步之前,
頁面的dom還沒有真正渲染。該方法包括真實dom的創建、虛擬dom的diff修改、dom的銷毀等,
具體細節后續筆記在記錄。