vue的原始碼學習之六——2.createComponent虛擬DOM元件
1. 介紹
版本:2.5.17。
我們使用vue-vli建立基於Runtime+Compiler的vue腳手架。
學習文件:https://ustbhuangyi.github.io/vue-analysis/components/create-component.html
2. 連線上一節
在上一節中我們從主線上把模板和資料如何渲染成最終的 DOM 的過程分析完畢了。
render(參生虛擬DOM) 是呼叫 createElement 的實現的時候,它最終會呼叫 _createElement 方法,其中有一段邏輯是對引數 tag 的判斷,如果是一個普通的 html 標籤,像上一章的例子那樣是一個普通的 div,則會例項化一個普通 VNode 節點,否則通過 createComponent 方法建立一個元件 VNode。
scr/core/vnode/create-elements.js:
if (typeof tag === 'string') { let Ctor ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag) if (config.isReservedTag(tag)) { // platform built-in elements vnode = new VNode( config.parsePlatformTagName(tag), data, children, undefined, undefined, context ) } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) { // component vnode = createComponent(Ctor, data, context, children, tag) } else { // unknown or unlisted namespaced elements // check at runtime because it may get assigned a namespace when its // parent normalizes children vnode = new VNode( tag, data, children, undefined, undefined, context ) } } else { // direct component options / constructor vnode = createComponent(tag, data, context, children) }
在我們這一章傳入的是一個 App 物件,它本質上是一個 Component
型別,那麼它會走到上述程式碼的 else 邏輯,直接通過 createComponent
方法來建立 vnode
。所以接下來我們來看一下 createComponent
方法的實現,它定義在 src/core/vdom/create-component.js
檔案中
3. createComponent
在src/core/vdom/create-component.js中:
export function createComponent ( //可以是元件型別的類,函式或者物件 Ctor: Class<Component> | Function | Object | void, // VNode(虛擬DOM)的相關Data data: ?VNodeData, // 上下文,也就是我們的當前的Vue例項---vm context: Component, // 元件的子VNode children: ?Array<VNode>, tag?: string ): VNode | Array<VNode> | void { if (isUndef(Ctor)) { return } const baseCtor = context.$options._base // plain options object: turn it into a constructor if (isObject(Ctor)) { Ctor = baseCtor.extend(Ctor) } // if at this stage it's not a constructor or an async component factory, // reject. if (typeof Ctor !== 'function') { if (process.env.NODE_ENV !== 'production') { warn(`Invalid Component definition: ${String(Ctor)}`, context) } return } // async component let asyncFactory if (isUndef(Ctor.cid)) { asyncFactory = Ctor Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context) if (Ctor === undefined) { // return a placeholder node for async component, which is rendered // as a comment node but preserves all the raw information for the node. // the information will be used for async server-rendering and hydration. return createAsyncPlaceholder( asyncFactory, data, context, children, tag ) } } data = data || {} // resolve constructor options in case global mixins are applied after // component constructor creation resolveConstructorOptions(Ctor) // transform component v-model data into props & events if (isDef(data.model)) { transformModel(Ctor.options, data) } // extract props const propsData = extractPropsFromVNodeData(data, Ctor, tag) // functional component if (isTrue(Ctor.options.functional)) { return createFunctionalComponent(Ctor, propsData, data, context, children) } // extract listeners, since these needs to be treated as // child component listeners instead of DOM listeners const listeners = data.on // replace with listeners with .native modifier // so it gets processed during parent component patch. data.on = data.nativeOn if (isTrue(Ctor.options.abstract)) { // abstract components do not keep anything // other than props & listeners & slot // work around flow const slot = data.slot data = {} if (slot) { data.slot = slot } } // install component management hooks onto the placeholder node installComponentHooks(data) // return a placeholder vnode const name = Ctor.options.name || tag const vnode = new VNode( `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`, data, undefined, undefined, undefined, context, { Ctor, propsData, listeners, tag, children }, asyncFactory ) // Weex specific: invoke recycle-list optimized @render function for // extracting cell-slot template. // https://github.com/Hanks10100/weex-native-directive/tree/master/component /* istanbul ignore if */ if (__WEEX__ && isRecyclableComponent(vnode)) { return renderRecyclableComponentTemplate(vnode) } return vnode }
可以看到,createComponent
的邏輯也會有一些複雜,但是分析原始碼比較推薦的是隻分析核心流程,分支流程可以之後針對性的看,所以這裡針對元件渲染這個 case 主要就 3 個關鍵步驟:構造子類建構函式,安裝元件鉤子函式和例項化 vnode
。
3.1 構造子類建構函式
const baseCtor = context.$options._base
// plain options object: turn it into a constructor
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
這裡 export 的是一個物件,所以 createComponent
裡的程式碼邏輯會執行到 baseCtor.extend(Ctor)
import HelloWorld from './components/HelloWorld'
export default {
name: 'app',
components: {
HelloWorld
}
}
3.1.1 baseCtor就是Vue
在這裡 baseCtor 實際上就是 Vue,這個的定義是在最開始初始化 Vue 的階段,在 src/core/global-api/index.js 中的 initGlobalAPI 函式有這麼一段邏輯:
Vue.options._base = Vue
這裡定義的是 Vue.option
,而我們的 createComponent
取的是 context.$options
,實際上在 src/core/instance/init.js
裡 Vue 原型上的 _init
函式中有這麼一段邏輯:
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
這樣就把 Vue 上的一些 option
擴充套件到了 vm.$option 上,所以我們也就能通過 vm.$options._base
拿到 Vue 這個構造函數了。mergeOptions
的實現我們會在後續章節中具體分析,現在只需要理解它的功能是把 Vue 建構函式的 options
和使用者傳入的 options
做一層合併,到 vm.$options
上。
3.1.2 Vue.extend
src/core/global-api/extend.js
:
Vue.extend = function (extendOptions: Object): Function {
extendOptions = extendOptions || {}
**步驟1:**
// vue呼叫該方法,所以this就是vue
const Super = this
const SuperId = Super.cid
// 給當前的元件新增_Ctor做快取使用,在Vue.extend這個函式的最後,會將子構造器快取
const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]
}
**步驟2:**
// 對元件的名字做校驗,不能是h5的保留標籤,如header
const name = extendOptions.name || Super.options.name
if (process.env.NODE_ENV !== 'production' && name) {
validateComponentName(name)
}
**步驟3:**
// 建立子建構函式
const Sub = function VueComponent (options) {
//和Vue本身一樣,需要_init(繼承自Vue)
this._init(options)
}
//子建構函式繼承Vue
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.cid = cid++
// 子建構函式自己的配置和Vue的配置做合併
Sub.options = mergeOptions(
Super.options,
extendOptions
)
Sub['super'] = Super
// For props and computed properties, we define the proxy getters on
// the Vue instances at extension time, on the extended prototype. This
**步驟4:**
// 對子構造器的props和computed做初始化
if (Sub.options.props) {
initProps(Sub)
}
if (Sub.options.computed) {
initComputed(Sub)
}
// allow further extension/mixin/plugin usage
// 將Vue的全域性靜態方法賦值給子構造器
Sub.extend = Super.extend
Sub.mixin = Super.mixin
Sub.use = Super.use
// create asset registers, so extended classes
// can have their private assets too.
ASSET_TYPES.forEach(function (type) {
Sub[type] = Super[type]
})
// enable recursive self-lookup
if (name) {
Sub.options.components[name] = Sub
}
// keep a reference to the super options at extension time.
// later at instantiation we can check if Super's options have
// been updated.
Sub.superOptions = Super.options
Sub.extendOptions = extendOptions
Sub.sealedOptions = extend({}, Sub.options)
**步驟5:**
// cache constructor
// 將子構造器快取下來
cachedCtors[SuperId] = Sub
return Sub
}
步驟1:
super就是Vue,
cachedCtors作為當前的元件新增_Ctor屬性的屬性值做快取使用,在Vue.extend這個函式的最後,會將子構造器快取
步驟2:
對元件的名字做校驗,不能是h5的保留標籤,如header,
利用的是validateComponentName方法,來自於、sc/core/utils/options
步驟3:
建立子建構函式
子建構函式繼承Vue
子建構函式自己的配置和Vue的配置做合併
步驟4:
sub 這個物件本身擴充套件了一些屬性,如擴充套件 options、新增全域性 API
對sub 這個物件配置中的 props 和 computed 做了初始化工作
步驟5:
將子構造器快取下來,並存儲給cachedCtors,每一個cachedCtors[SuperId]都對應一個元件,這樣當不同的頁面引用相同的元件的時候,只用建立一次該元件的子建構函式
總結:
Vue.extend 的作用就是構造一個 Vue 的子類,它使用一種非常經典的原型繼承的方式把一個純物件轉換一個繼承於 Vue 的構造器 Sub 並返回,然後對 Sub 這個物件本身擴充套件了一些屬性,如擴充套件 options、新增全域性 API 等;並且對配置中的 props 和 computed 做了初始化工作;最後對於這個 Sub 建構函式做了快取,避免多次執行 Vue.extend 的時候對同一個子元件重複構造。
3.2
安裝元件鉤子函式
installComponentHooks(data)
我們來看installComponentHooks函式,就在這個頁面
function installComponentHooks (data: VNodeData) {
const hooks = data.hook || (data.hook = {})
for (let i = 0; i < hooksToMerge.length; i++) {
const key = hooksToMerge[i]
const existing = hooks[key]
const toMerge = componentVNodeHooks[key]
if (existing !== toMerge && !(existing && existing._merged)) {
hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge
}
}
}
是將我們的 data.hook (元件的data的鉤子) 和 componentVNodeHooks 合併。
我們來看componentVNodeHooks是啥,也在該 js
const componentVNodeHooks = {
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
}
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
}
insert (vnode: MountedComponentVNode) {
}
destroy (vnode: MountedComponentVNode) {
}
}
componentVNodeHooks中存放的也是鉤子,共有四個值init、prepatch、insert、destroy。具體實現後面講解,從函式名上我們也可以猜到,它們分別是正在VNode物件初始化、patch之前、插入到dom中、VNode物件銷燬時呼叫。
installComponentHooks函式將我們的 data.hook (元件的data的鉤子) 和 componentVNodeHooks 合併的時候採用的是mergeHook函式方法
mergeHook:
function mergeHook (f1: any, f2: any): Function {
const merged = (a, b) => {
// flow complains about extra args which is why we use any
f1(a, b)
f2(a, b)
}
merged._merged = true
return merged
}
其實就是如果 data.hook中有這個鉤子,componentVNodeHooks也有這個鉤子,那麼就按照雙方的邏輯各執行一遍。
3.3 例項化 VNode
const name = Ctor.options.name || tag
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
return vnode
最後一步非常簡單,通過 new VNode 例項化一個 vnode 並返回。需要注意的是和普通元素節點的 vnode 不同,元件的 vnode 是沒有 children 的,並且會有元件的相關配置這個引數。
4. 總結
createComponent
在渲染一個元件的時候的 3 個關鍵邏輯:
- 構造子類建構函式,
- 安裝元件鉤子函式和例項化
vnode
。 createComponent
後返回的是元件vnode。
它也一樣走到 vm._update
方法,進而執行了 patch
函式,