組件
- 組件是一個(gè)抽象的概念,它是對一棵
DOM
樹的抽象 - 可以描述組件信息的
JavaScript
對象 - 從表現(xiàn)上來看
- 組件的模板決定了組件生成的
DOM
標(biāo)簽 - 在
Vue.js
內(nèi)部塞椎,一個(gè)組件想要真正的渲染生成DOM
- 組件的模板決定了組件生成的
應(yīng)用程序初始化
- 整個(gè)組件樹是由根組件開始渲染的
- 為了找到根組件的渲染入口桨仿,從應(yīng)用程序的初始化過程開始分析
- 對比
vue2.0
、vue3.0
入口
// 在 Vue.js 2.x 中案狠,初始化一個(gè)應(yīng)用的方式如下
import Vue from 'vue'
import App from './App'
const app = new Vue({
render: h => h(App)
})
app.$mount('#app')
// 在 Vue.js 3.0 中服傍,初始化一個(gè)應(yīng)用的方式如下
import { createApp } from 'vue'
import App from './app'
const app = createApp(App)
app.mount('#app')
- 在
Vue.js 3.0
中導(dǎo)入了一個(gè)createApp
钱雷,這是個(gè)入口函數(shù),它是Vue.js
對外暴露的一個(gè)函數(shù)
createApp內(nèi)部實(shí)現(xiàn)
const createApp = ((...args) => {
// 創(chuàng)建 app 對象
const app = ensureRenderer().createApp(...args)
const { mount } = app
// 重寫 mount 方法
app.mount = (containerOrSelector) => {
// ...
}
return app
})
-
createApp
主要做了兩件事情- 1)創(chuàng)建
app
對象 - 2)重寫
app.mount
方法
- 1)創(chuàng)建
創(chuàng)建app對象
-
ensureRenderer().createApp()
來創(chuàng)建app
對象 - 實(shí)現(xiàn)了跨平臺(tái)渲染
const app = ensureRenderer().createApp(...args)
-
ensureRenderer()
用來創(chuàng)建一個(gè)渲染器對象
ensureRenderer 內(nèi)部實(shí)現(xiàn)
// 渲染相關(guān)的一些配置,比如更新屬性的方法灿椅,操作 DOM 的方法
const rendererOptions = {
patchProp,
...nodeOps
}
let renderer
// 延時(shí)創(chuàng)建渲染器套蒂,當(dāng)用戶只依賴響應(yīng)式包的時(shí)候,可以通過 tree-shaking 移除核心渲染邏輯相關(guān)的代碼
function ensureRenderer() {
return renderer || (renderer = createRenderer(rendererOptions))
}
function createRenderer(options) {
return baseCreateRenderer(options)
}
function baseCreateRenderer(options) {
function render(vnode, container) {
// 組件渲染的核心邏輯
}
return {
render,
createApp: createAppAPI(render)
}
}
function createAppAPI(render) {
// createApp createApp 方法接受的兩個(gè)參數(shù):根組件的對象和 prop
return function createApp(rootComponent, rootProps = null) {
const app = {
_component: rootComponent,
_props: rootProps,
mount(rootContainer) {
// 創(chuàng)建根組件的 vnode
const vnode = createVNode(rootComponent, rootProps)
// 利用渲染器渲染 vnode
render(vnode, rootContainer)
app._container = rootContainer
return vnode.component.proxy
}
}
return app
}
}
- 首先用
ensureRenderer()
來延時(shí)創(chuàng)建渲染器- 好處是當(dāng)用戶只依賴響應(yīng)式包的時(shí)候婴洼,就不會(huì)創(chuàng)建渲染器
- 可以通過
tree-shaking
的方式移除核心渲染邏輯相關(guān)的代碼
- 通過
createRenderer
創(chuàng)建一個(gè)渲染器- 這個(gè)渲染器內(nèi)部會(huì)有一個(gè)
createApp
方法- 它是執(zhí)行
createAppAPI
方法返回的函數(shù) - 接受了
rootComponent
和rootProps
兩個(gè)參數(shù)
- 它是執(zhí)行
- 我們在應(yīng)用層面執(zhí)行
createApp(App)
方法時(shí):- 會(huì)把
App
組件對象作為根組件傳遞給rootComponent
骨坑。 - 這樣,
createApp
內(nèi)部就創(chuàng)建了一個(gè)app
對象 - 它會(huì)提供
mount
方法柬采,這個(gè)方法是用來掛載組件的卡啰。
- 會(huì)把
- 這個(gè)渲染器內(nèi)部會(huì)有一個(gè)
值得注意的是
-
app
對象創(chuàng)建過程中,Vue.js
利用閉包和函數(shù)柯里化的技巧警没,很好地實(shí)現(xiàn)了參數(shù)保留
重寫app.mount方法
為什么重寫匈辱?
-
createApp
返回的app
對象已經(jīng)擁有了mount
方法了,為什么還有在入口重寫杀迹?- 為了支持跨平臺(tái)渲染
-
createApp
函數(shù)內(nèi)部的app.mount
方法是一個(gè)標(biāo)準(zhǔn)的可跨平臺(tái)的組件渲染流程:
mount(rootContainer) {
// 創(chuàng)建根組件的 vnode
const vnode = createVNode(rootComponent, rootProps)
// 利用渲染器渲染 vnode
render(vnode, rootContainer)
app._container = rootContainer
return vnode.component.proxy
}
- 主要流程是亡脸,先創(chuàng)建
vnode
,再渲染vnode
- 參數(shù)
rootContainer
根據(jù)平臺(tái)不同而不同树酪, - 這里面的代碼不應(yīng)該包含任何特定平臺(tái)相關(guān)的邏輯浅碾,因此我們需要在外部重寫這個(gè)方法
app.mount 重寫都做了哪些事情?
app.mount = (containerOrSelector) => {
// 標(biāo)準(zhǔn)化容器
const container = normalizeContainer(containerOrSelector)
if (!container)
return
const component = app._component
// 如組件對象沒有定義 render 函數(shù)和 template 模板续语,則取容器的 innerHTML 作為組件模板內(nèi)容
if (!isFunction(component) && !component.render && !component.template) {
component.template = container.innerHTML
}
// 掛載前清空容器內(nèi)容
container.innerHTML = ''
// 真正的掛載
return mount(container)
}
- 首先是通過
normalizeContainer
標(biāo)準(zhǔn)化容器(這里可以傳字符串選擇器或者DOM
對象垂谢,但如果是字符串選擇器,就需要把它轉(zhuǎn)成DOM
對象疮茄,作為最終掛載的容器) - 然后做一個(gè)
if
判斷滥朱,如果組件對象沒有定義render
函數(shù)和template
模板,則取容器的innerHTML
作為組件模板內(nèi)容 - 接著在掛載前清空容器內(nèi)容力试,最終再調(diào)用
app.mount
的方法走標(biāo)準(zhǔn)的組件渲染流程
優(yōu)勢
- 跨平臺(tái)實(shí)現(xiàn)
- 兼容
vue2.0
寫法 -
app.mount
既可以傳dom
徙邻,又可以傳字符串選擇器
核心渲染流程:創(chuàng)建 vnode 和渲染 vnode
創(chuàng)建 vnode
- 1、
vnode
本質(zhì)上是用來描述DOM
的JavaScript
對象
它在
Vue.js
中可以描述不同類型的節(jié)點(diǎn)畸裳,比如普通元素節(jié)點(diǎn)缰犁、組件節(jié)點(diǎn)等
vnode如何描述
// vnode 這樣表示<button>標(biāo)簽
const vnode = {
type: 'button',
props: {
'class': 'btn',
style: {
width: '100px',
height: '50px'
}
},
children: 'click me'
}
type
屬性表示DOM
的標(biāo)簽類型props
屬性表示DOM
的一些附加信息,比如style
、class
等children
屬性表示DOM
的子節(jié)點(diǎn)帅容,它也可以是一個(gè)vnode
數(shù)組颇象,只不過vnode
可以用字符串表示簡單的文本2、
vnode
除了用于描述一個(gè)真實(shí)的DOM
并徘,也可以用來描述組件
vnode
其實(shí)是對抽象事物的描述
// vnode 這樣表示 <custom-component>
const CustomComponent = {
// 在這里定義組件對象
}
const vnode = {
type: CustomComponent,
props: {
msg: 'test'
}
}
- 3遣钳、其他的,還有純文本
vnode
饮亏,注釋vnode
- 4、
Vue.js 3.0
中阅爽,vnode
的type
路幸,做了更詳盡的分類,包括Suspense
付翁、Teleport
等简肴,且把vnode
的類型信息做了編碼,以便在后面的patch
階段百侧,可以根據(jù)不同的類型執(zhí)行相應(yīng)的處理邏輯
vode優(yōu)勢
- 抽象
- 跨平臺(tái)
- 但是砰识,和手動(dòng)修改
dom
對比,并不一定有優(yōu)勢
如何創(chuàng)建vnode
-
app.mount
函數(shù)的實(shí)現(xiàn)佣渴,內(nèi)部是通過createVNode
函數(shù)創(chuàng)建了根組件的vnode
const vnode = createVNode(rootComponent, rootProps)
createVNode 函數(shù)的大致實(shí)現(xiàn)
function createVNode(type, props = null,children = null) {
if (props) {
// 處理 props 相關(guān)邏輯辫狼,標(biāo)準(zhǔn)化 class 和 style
}
// 對 vnode 類型信息編碼
const shapeFlag = isString(type)
? 1 /* ELEMENT */
: isSuspense(type)
? 128 /* SUSPENSE */
: isTeleport(type)
? 64 /* TELEPORT */
: isObject(type)
? 4 /* STATEFUL_COMPONENT */
: isFunction(type)
? 2 /* FUNCTIONAL_COMPONENT */
: 0
const vnode = {
type,
props,
shapeFlag,
// 一些其他屬性
}
// 標(biāo)準(zhǔn)化子節(jié)點(diǎn),把不同數(shù)據(jù)類型的 children 轉(zhuǎn)成數(shù)組或者文本類型
normalizeChildren(vnode, children)
return vnode
}
- 對
props
做標(biāo)準(zhǔn)化處理 - 對
vnode
的類型信息編碼 - 創(chuàng)建
vnode
對象 - 標(biāo)準(zhǔn)化子節(jié)點(diǎn)
children
渲染 vnode
render(vnode, rootContainer)
const render = (vnode, container) => {
if (vnode == null) {
// 銷毀組件
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
} else {
// 創(chuàng)建或者更新組件
patch(container._vnode || null, vnode, container)
}
// 緩存 vnode 節(jié)點(diǎn)辛润,表示已經(jīng)渲染
container._vnode = vnode
}
- 如果它的第一個(gè)參數(shù)
vnode
為空膨处,則執(zhí)行銷毀組件的邏輯 - 否則執(zhí)行創(chuàng)建或者更新組件的邏輯
patch函數(shù)
const patch = (n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, optimized = false) => {
// 如果存在新舊節(jié)點(diǎn), 且新舊節(jié)點(diǎn)類型不同,則銷毀舊節(jié)點(diǎn)
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
unmount(n1, parentComponent, parentSuspense, true)
n1 = null
}
const { type, shapeFlag } = n2
switch (type) {
case Text:
// 處理文本節(jié)點(diǎn)
break
case Comment:
// 處理注釋節(jié)點(diǎn)
break
case Static:
// 處理靜態(tài)節(jié)點(diǎn)
break
case Fragment:
// 處理 Fragment 元素
break
default:
if (shapeFlag & 1 /* ELEMENT */) {
// 處理普通 DOM 元素
processElement(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
}
else if (shapeFlag & 6 /* COMPONENT */) {
// 處理組件
processComponent(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
}
else if (shapeFlag & 64 /* TELEPORT */) {
// 處理 TELEPORT
}
else if (shapeFlag & 128 /* SUSPENSE */) {
// 處理 SUSPENSE
}
}
}
- 這個(gè)函數(shù)有兩個(gè)功能:
- 一個(gè)是根據(jù)
vnode
掛載DOM
- 一個(gè)是根據(jù)新舊
vnode
更新DOM
砂竖。對于初次渲染
- 一個(gè)是根據(jù)
-
patch
函數(shù)入?yún)?- 第一個(gè)參數(shù)
n1
表示舊的vnode
真椿,當(dāng)n1
為null
的時(shí)候,表示是一次掛載的過程乎澄; - 第二個(gè)參數(shù)
n2
表示新的vnode
節(jié)點(diǎn)突硝,后續(xù)會(huì)根據(jù)這個(gè)vnode
類型執(zhí)行不同的處理邏輯; - 第三個(gè)參數(shù)
container
表示DOM
容器置济,也就是vnode
渲染生成DOM
后解恰,會(huì)掛載到container
下面。
- 第一個(gè)參數(shù)
渲染節(jié)點(diǎn)
- 對組件的處理
- 對普通
DOM
元素的處理
對組件的處理
processComponent函數(shù)實(shí)現(xiàn)
- 用來處理組件
const processComponent = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
if (n1 == null) {
// 掛載組件
mountComponent(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
}
else {
// 更新組件
updateComponent(n1, n2, parentComponent, optimized)
}
}
- 如果
n1
為null
浙于,則執(zhí)行掛載組件的邏輯 - 否則執(zhí)行更新組件的邏輯
mountComponent掛載組件的實(shí)現(xiàn)
const mountComponent = (initialVNode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
// 創(chuàng)建組件實(shí)例
const instance = (initialVNode.component = createComponentInstance(initialVNode, parentComponent, parentSuspense))
// 設(shè)置組件實(shí)例
setupComponent(instance)
// 設(shè)置并運(yùn)行帶副作用的渲染函數(shù)
setupRenderEffect(instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized)
}
主要做三件事情
- 1修噪、 創(chuàng)建組件實(shí)例
-
Vue.js 3.0
雖然不像Vue.js 2.x
那樣通過類的方式去實(shí)例化組件,但內(nèi)部也通過對象的方式去創(chuàng)建了當(dāng)前渲染的組件實(shí)例
-
- 2路媚、 設(shè)置組件實(shí)例
-
instance
保留了很多組件相關(guān)的數(shù)據(jù)黄琼,維護(hù)了組件的上下文,包括對props
、插槽脏款,以及其他實(shí)例的屬性的初始化處理
-
- 3围苫、 設(shè)置并運(yùn)行帶副作用的渲染函數(shù)(
setupRenderEffect
)
const setupRenderEffect = (instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized) => {
// 創(chuàng)建響應(yīng)式的副作用渲染函數(shù)
instance.update = effect(function componentEffect() {
if (!instance.isMounted) {
// 渲染組件生成子樹 vnode
const subTree = (instance.subTree = renderComponentRoot(instance))
// 把子樹 vnode 掛載到 container 中
patch(null, subTree, container, anchor, instance, parentSuspense, isSVG)
// 保留渲染生成的子樹根 DOM 節(jié)點(diǎn)
initialVNode.el = subTree.el
instance.isMounted = true
}
else {
// 更新組件
}
}, prodEffectOptions)
}
- 該函數(shù)利用響應(yīng)式庫的
effect
函數(shù)創(chuàng)建了一個(gè)副作用渲染函數(shù)componentEffect
副作用
當(dāng)組件的數(shù)據(jù)發(fā)生變化時(shí),effect
函數(shù)包裹的內(nèi)部渲染函數(shù)componentEffect
會(huì)重新執(zhí)行一遍撤师,從而達(dá)到重新渲染組件的目的
- 渲染函數(shù)內(nèi)部也會(huì)判斷這是一次初始渲染還是組件更新剂府,在初始渲染流程中
初始渲染主要做兩件事情:
- 1、 渲染組件生成
subTree
注意剃盾,不要弄混subTree
(執(zhí)行renderComponentRoot
生成的子樹vnode
)和initialVNode
(組件vnode
)- 每個(gè)組件都有
render
函數(shù)腺占,template
也會(huì)編譯成render
函數(shù) -
renderComponentRoot
函數(shù)就是去執(zhí)行render
函數(shù)創(chuàng)建整個(gè)組件樹內(nèi)部的vnode
, - 把這個(gè)
vnode
再經(jīng)過內(nèi)部一層標(biāo)準(zhǔn)化痒谴,就得到了該函數(shù)的返回結(jié)果:subTree
(子樹vnode
)
- 每個(gè)組件都有
- 2衰伯、把
subTree
掛載到container
中- 繼續(xù)調(diào)用
patch
函數(shù)把子樹vnode
掛載到container
中 - 繼續(xù)對這個(gè)子樹
vnode
類型進(jìn)行判斷,此時(shí)子樹vnode
為普通元素vnode
- 繼續(xù)調(diào)用
對普通 DOM 元素的處理
processElement函數(shù)
- 用來處理普通
DOM
元素
const processElement = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
isSVG = isSVG || n2.type === 'svg'
if (n1 == null) {
//掛載元素節(jié)點(diǎn)
mountElement(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
}
else {
//更新元素節(jié)點(diǎn)
patchElement(n1, n2, parentComponent, parentSuspense, isSVG, optimized)
}
}
- 如果
n1
為null
积蔚,走掛載元素節(jié)點(diǎn)的邏輯 - 否則走更新元素節(jié)點(diǎn)邏輯
mountElement 函數(shù)
const mountElement = (vnode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
let el
const { type, props, shapeFlag } = vnode
// 創(chuàng)建 DOM 元素節(jié)點(diǎn)
el = vnode.el = hostCreateElement(vnode.type, isSVG, props && props.is)
if (props) {
// 處理 props意鲸,比如 class、style尽爆、event 等屬性
for (const key in props) {
if (!isReservedProp(key)) {
hostPatchProp(el, key, null, props[key], isSVG)
}
}
}
if (shapeFlag & 8 /* TEXT_CHILDREN */) {
// 處理子節(jié)點(diǎn)是純文本的情況
hostSetElementText(el, vnode.children)
}
else if (shapeFlag & 16 /* ARRAY_CHILDREN */) {
// 處理子節(jié)點(diǎn)是數(shù)組的情況
mountChildren(vnode.children, el, null, parentComponent, parentSuspense, isSVG && type !== 'foreignObject', optimized || !!vnode.dynamicChildren)
}
// 把創(chuàng)建的 DOM 元素節(jié)點(diǎn)掛載到 container 上
hostInsert(el, container, anchor)
}
- 主要做四件事
- 1怎顾、 創(chuàng)建
DOM
元素節(jié)點(diǎn)
通過hostCreateElement
方法創(chuàng)建,這是一個(gè)平臺(tái)相關(guān)的方法漱贱,在web
端實(shí)現(xiàn):
// 調(diào)用了底層的 DOM API document.createElement 創(chuàng)建元素
function createElement(tag, isSVG, is) {
isSVG ? document.createElementNS(svgNS, tag)
: document.createElement(tag, is ? { is } : undefined)
}
- 2槐雾、 處理
props
給這個(gè)DOM
節(jié)點(diǎn)添加相關(guān)的class
、style
幅狮、event
等屬性蚜退,并做相關(guān)的處理 - 3、 處理
children
- 子節(jié)點(diǎn)是純文本彪笼,則執(zhí)行
hostSetElementText
方法钻注,它在Web
環(huán)境下通過設(shè)置DOM
元素的textContent
屬性設(shè)置文本:
- 子節(jié)點(diǎn)是純文本彪笼,則執(zhí)行
function setElementText(el, text) {
el.textContent = text
}
* 如果子節(jié)點(diǎn)是數(shù)組,則執(zhí)行`mountChildren`方法
const mountChildren = (children, container, anchor, parentComponent, parentSuspense, isSVG, optimized, start = 0) => {
for (let i = start; i < children.length; i++) {
// 預(yù)處理 child
const child = (children[i] = optimized
? cloneIfMounted(children[i])
: normalizeVNode(children[i]))
// 遞歸 patch 掛載 child
patch(null, child, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
}
}
遍歷
children
獲取到每一個(gè)child vnode
mountChildren
函數(shù)的第二個(gè)參數(shù)是container
配猫,傳入的是mountElement
時(shí)創(chuàng)建的DOM
節(jié)點(diǎn)幅恋,很好的建立了父子關(guān)系通過遞歸
patch
這種深度優(yōu)先遍歷樹的方式,我們就可以構(gòu)造完整的DOM
樹泵肄,完成組件的渲染捆交。4、 掛載
DOM
元素到container
上
調(diào)用hostInsert
方法
function insert(child, parent, anchor) {
if (anchor) {
parent.insertBefore(child, anchor)
}
else {
parent.appendChild(child)
}
}
嵌套組件的處理
-
mountChildren
的時(shí)候遞歸執(zhí)行的是patch
函數(shù)腐巢,而不是mountElement
函數(shù)品追,這是因?yàn)樽庸?jié)點(diǎn)可能有其他類型的vnode
,比如組件vnode