組件化
組件化是vue的另一個核心思想,所謂的組件化就,就是說把頁面拆分成多個組件(component),每個組件依賴的css、js、圖片等資源放在一起開發和維護,組件是資源獨立的,在內部系統中是可以多次復用的,組間之間也是可以互相嵌套的,
接下來我們用vue-cli為例,來分析一下Vue組件是如何作業的,還是它的創建及其作業原理,
import Vue from 'vue'
import App from './App.vue'
var app = new Vue({
el: '#app',
// 這里的 h 是 createElement 方法
render: h => h(App)
})
創建組件 - createComponent
在分析createComponent函式前,我們得先知道vue的原始碼執行程序中是怎么呼叫到createComponent的,其實我們在上一章就有所提及,具體流程如下:
①:Vue.prototype.$mount;(src\platforms\web\entry-runtime-with-compiler.js和src\platforms\web\runtime\index.js)
②:mountComponent;(src\core\instance\lifecycle.js)
③:vm._update(vm._render());(src\core\instance\lifecycle.js)
④:render.call(vm._renderProxy, vm.$createElement);(src\core\instance\render.js)
⑤:vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true);(src\core\instance\render.js)
⑥:createElement;(src\core\vdom\create-element.js)
⑦:_createElement;(src\core\vdom\create-element.js)
⑧:createComponent;
//src\core\vdom\create-element.js
// part 3
export function _createElement (
context: Component, //背景關系環境,一般就是vm
tag?: string | Class<Component> | Function | Object, //標簽(element)
data?: VNodeData, //VNode資料,VnodeData型別,詳見flow\vnode.js
children?: any, //Vnode子節點
normalizationType?: number //子節點規范型別
): VNode | Array<VNode> {
...
//這次這個tag就是Class<Component>了
if (typeof tag === 'string') {
...
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}
...
}
這次我們進入到_createElement函式走的就是createComponent的流程了,
// src\core\vdom\create-component.js
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component, //當前vm實體
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
if (isUndef(Ctor)) {
return
}
// 標注①
const baseCtor = context.$options._base //實際上就是Vue
if (isObject(Ctor)) {
// 標注②
Ctor = baseCtor.extend(Ctor) //即Vue.extend(src\core\global-api\extend.js)
}
...
// 鉤子函式掛載到data物件,詳情查閱原始碼
installComponentHooks(data)
}
①:baseCtor其實就是Vue,流程如下
// src\core\global-api\index.js
// 初始化時候定義了Vue.options._base = Vue
Vue.options._base = Vue
// src\core\instance\init.js
Vue.prototype._init = function (options?: Object) {
// 在這里吧Vue.options合并到vm.$options上
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
而context其實就是vm,所以baseCtor =context.$options._base = vm.$options._base = Vue.options._base = Vue;
②:分析完baseCtor的由來,那么baseCtor.extend顯然就是Vue.extend了,把Ctor物件轉換成新的構造器,我們下面來詳細看看Vue.extend,
Vue.extend = function (extendOptions: Object): Function {
extendOptions = extendOptions || {}
const Super = this //vue
const SuperId = Super.cid
//添加了一個_Ctor空物件屬性
const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
//后面會快取cachedCtors[SuperId],防止多次生成相同構造器
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]
}
const name = extendOptions.name || Super.options.name
if (process.env.NODE_ENV !== 'production' && name) {
//名稱校驗,防止你們整些花里胡哨的關鍵欄位,
validateComponentName(name)
}
const Sub = function VueComponent (options) {
this._init(options) //vue._init
}
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
// avoids Object.defineProperty calls for each instance created.
if (Sub.options.props) {
initProps(Sub)
}
if (Sub.options.computed) {
initComputed(Sub)
}
// allow further extension/mixin/plugin usage
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)
// cache constructor 快取起來
cachedCtors[SuperId] = Sub
return Sub
}
Vue.extend的作用其實就是構建一個Vue的子類,把物件轉換成繼承于Vue的構造器Sub并回傳,然后對Sub本身擴展了option、全域API等,并對配置中的props和computed做了初始化作業,最后對Sub做了快取,防止多次生成相同構造器,
// src\core\vdom\create-component.js
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component, //當前vm實體
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
...
// 鉤子函式掛載到data物件
installComponentHooks(data)
...
}
我們繼續看createComponent函式,中間忽略了一些代碼塊,后續涉及到的時候再分析,現在我們先看看installComponentHooks函式,
// src\core\vdom\create-component.js
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
}
}
}
const hooksToMerge = Object.keys(componentVNodeHooks)
//默認鉤子
const componentVNodeHooks = {
init(){},
prepatch(){},
insert(){},
destroy(){},
}
//合并鉤子
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
}
installComponentHooks其實就遍歷了hooksToMerge,其實就是遍歷了componentVNodeHooks的鉤子然后和data.hook合并,
接下來我們繼續往下看createComponent函式
// src\core\vdom\create-component.js
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component, //當前vm實體
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
...
// 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
)
...
return vnode
}
組件的new VNode和之前入參不太一樣,我們先回顧一下Vnode的入參分別是什么?
// src\core\vdom\vnode.js
export default class VNode {
constructor (
tag?: string,
data?: VNodeData,
children?: ?Array<VNode>,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions,
asyncFactory?: Function
){},
}
主要有三個需要關注的點:
tag:會有一個'vue-component-'標識這個是個組件;
children:入參是undefined,記住組件的children是空的,這個到時候再patch遍歷時候會用到;
componentOptions:組件的很多資料都存放在這里雖然chilrend入參為空,但是這里有傳入;
new Vnode之后把vnode return到createComponent,到此我們createComponent的流程就跑完了,接下來我們又回到了vm._update(vm._render(), hydrating)中,開始了vm._update之旅了,其實也就是回到了把vnode轉換成真實dom的patch函式,
patch函式 - 組件處理
又回到最初的起點,呆呆的站在patch前,
// src\core\vdom\patch.js
return function patch (oldVnode, vnode, hydrating, removeOnly) {
...
// create new node
createElm(
vnode,
insertedVnodeQueue,
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
}
patch的流程和配置el或者template一樣,會走到createElm
// src\core\vdom\patch.js
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
...
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
...
}
與之前最大的不同一就是跑到了createComponent函式時候的處理了,這邊需要注意的是這個createComponent函式是patch.js中的,而不是我們上文提及到的create-component.js中的,這里要區分開來,不要混淆了,
下面我們來看看我們呼叫到的patch.js中的pcreateComponent函式
// src\core\vdom\patch.js
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
//keepalive邏輯,先不解讀
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
// i.hook = data.hook,再判斷是否有init方法,都成立是運行init,
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)
}
// after calling the init hook, if the vnode is a child component
// it should've created a child instance and mounted it. the child
// component also has set the placeholder vnode's elm.
// in that case we can just return the element and be done.
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
createComponent一次賦值且判斷vnode.data、vnode.data.hook、vnode.data.init是否為空,都成立是則運行init方法,那么這個init方法又是哪個呢?我們還記得上文生成vnode時候在create-component.js中的呼叫createComponent函式的時候有個installComponentHooks方法嗎?在那里我們插入了init鉤子,忘了的可以回顧一下前文,下面我們看看init的代碼,
// src\core\vdom\create-component.js
const componentVNodeHooks = {
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
// componentInstance是undefined,進入else邏輯
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// kept-alive components, treat as a patch
const mountedNode: any = vnode // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
// 運行的代碼塊
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
},
}
在vnode.componentInstance和vnode.data.keepAlive都是undefined的情況下我們進入了else邏輯,
其中執行了createComponentInstanceForVnode函式,它回傳的其實就是一個vm實體,下面我們具體看看createComponentInstanceForVnode函式,
// src\core\vdom\create-component.js
export function createComponentInstanceForVnode (
// we know it's MountedComponentVNode but flow doesn't
vnode: any,
// activeInstance in lifecycle state
parent: any //vm實體
): Component {
const options: InternalComponentOptions = {
_isComponent: true, //重新進入vue._init時候做判斷用到
_parentVnode: vnode,
parent
}
// check inline-template render functions
const inlineTemplate = vnode.data.inlineTemplate //undefined
if (isDef(inlineTemplate)) {
options.render = inlineTemplate.render
options.staticRenderFns = inlineTemplate.staticRenderFns
}
return new vnode.componentOptions.Ctor(options)
}
上述函式一開始配置了options,inlineTemplate是undefined,直接忽略,然后到了return new vnode.componentOptions.Ctor(options);
vnode.componentOptions.Ctor究竟是什么?這個我們得回到上訴的componentOptions,還記得那里new vnode傳入的引數嗎?其實倒數第二個就是componentOptions,入參是{ Ctor, propsData, listeners, tag, children},
所以知道這個怎么來了吧,vnode.componentOptions.Ctor其實就是入參的Ctor,而Ctor忘記了的,自己回顧一下Ctor,
所以其實它運行的就是extend中的Sub構造器:
// src\core\global-api\extend.js
const Sub = function VueComponent (options) {
this._init(options)
}
因為Sub構造器繼承的是Vue,因此this._init又回來Vue._init這個初始化操作,
那么組件進入_init和普通節點有什么不一樣呢?我們來重新進入vue._init,下面只選擇性展示不一樣的地方,
// src\core\instance\init.js
Vue.prototype._init = function (options?: Object) {
...
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)
}
...
}
_isComponent在createComponentInstanceForVnode函式是已經配置好是true了,所以會進入到initInternalComponent方法,下面看看它的定義:
// src\core\instance\init.js
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
const opts = vm.$options = Object.create(vm.constructor.options)
// doing this because it's faster than dynamic enumeration.
const parentVnode = options._parentVnode
opts.parent = options.parent
opts._parentVnode = parentVnode
const vnodeComponentOptions = parentVnode.componentOptions
opts.propsData = https://www.cnblogs.com/YmmY/archive/2022/02/19/vnodeComponentOptions.propsData
opts._parentListeners = vnodeComponentOptions.listeners
opts._renderChildren = vnodeComponentOptions.children
opts._componentTag = vnodeComponentOptions.tag
if (options.render) {
opts.render = options.render
opts.staticRenderFns = options.staticRenderFns
}
}
主要就是把options的配置給到vm.$options上,
然后接下來看其他的差異:
// src\core\instance\init.js
Vue.prototype._init = function (options?: Object) {
...
initLifecycle(vm)
...
}
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是什么?看字面意思也知道這是父級的東西,沒錯就是父級vm實體,他是在哪里定義的呢,這時候我們的回顧一下createComponentInstanceForVnode的方法,其中有配置options.parent = parent,而這個入參parent再往上追溯,其實就在componentVNodeHooks中的init方法中傳遞過來的,它傳遞的activeInstance其實是一個全域變數,那么這個又是在什么時候定義的?其實它在src\core\instance\lifecycle.js中做了定義,運行Vue._update的時候已經做了賦值,下面我們看一下代碼,
// src\core\instance\lifecycle.js
//全域定義
export let activeInstance: any = null
// activeInstance的賦值
export function setActiveInstance(vm: Component) {
const prevActiveInstance = activeInstance
activeInstance = vm
return () => {
activeInstance = prevActiveInstance
}
}
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
// 呼叫activeInstance賦值方法
const restoreActiveInstance = setActiveInstance(vm)
}
因此在每次運行到_update的時候,在進入下個階段patch函式之前,它都會快取住生成真實dom之前的vm,因此在patch做遞回進入一下個階段的時候,activeInstance就是它的父vm實體,知道了parent的來源,那么這父子關聯的代碼塊的邏輯也就明了了,
這時候patch也進入了遞回階段,遞回方法還是和之前相似,在回顧一下流程:
①:Vue.prototype._init;(src\core\instance\init.js)
②:Vue.prototype.$mount;(src\platforms\web\entry-runtime-with-compiler.js)
②:Vue.prototype.$mount;(src\platforms\web\runtime\index.js)
③:mountComponent;(src\core\instance\lifecycle.js)
④:Vue.prototype._render;(src\core\instance\render.js)
⑤:Vue.prototype._update;(src\core\instance\lifecycle.js)
⑥:vm.__patch__;(src\platforms\web\runtime\index.js)
⑦:createPatchFunction;(src\core\vdom\patch.js)
⑧:patch;(src\core\vdom\patch.js)
⑨:createElm;(src\core\vdom\patch.js)
⑩:createComponent;(src\core\vdom\patch.js)
在createComponent中的i.init開始了新一輪的初始化,當遞回結束后我們還有接下來的流程,我們繼續看createComponent方法,
// src\core\vdom\patch.js
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
//這里會完成整個patch的遞回流程
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)
}
// after calling the init hook, if the vnode is a child component
// it should've created a child instance and mounted it.
// 在初始化hook之后,如果vnode是一個子組件,那么它應該創建一個子實體并掛載它,
// the child component also has set the placeholder vnode's elm.
// 子組件還設定了占位符vnode的elm
// in that case we can just return the element and be done.
// 在這種情況下,我們只需回傳element就可以了
// 只有在patch結束后才進入了這里
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
遞回呼叫i.init結束后,我們進入下一個邏輯,vnode.componentInstance,在i.int中得到了賦值,就是一個vm實體,詳情回顧componentVNodeHooks,因此我們進入了initComponent函式,下面看看initComponent是做什么的,
// src\core\vdom\patch.js
function initComponent (vnode, insertedVnodeQueue) {
...
vnode.elm = vnode.componentInstance.$el
if (isPatchable(vnode)) {
// 創建一些鉤子,后續再分析
invokeCreateHooks(vnode, insertedVnodeQueue)
setScope(vnode)
} else {
...
}
}
initComponent主要是給vnode.elm賦值,vnode.componentInstance.$el即vm.$el,__patch__方法有做回傳,我們繼續回到initComponent的下一步insert,insert其作用就是根據判斷插入dom(insertBefore/appendChild),之前有講述過,至此,子組件的真實dom就生成了,由于這是遞回插入的模式,因此dom的插入順序是先子后父,
到此組件的patch的程序就到此結束了,建議配合代碼運行除錯,反復幾次理解運行邏輯,及引數傳遞與快取,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/428535.html
標籤:其他
上一篇:如何除錯Vue3原始碼?
