深度剖析?Vue3?在瀏覽器的運(yùn)行原理
前言
上一講深度解析 Vue3 的響應(yīng)式機(jī)制我們學(xué)習(xí)了 Vue 響應(yīng)式的大致原理,響應(yīng)式就是可以把普通的 JavaScript 對(duì)象包裹成響應(yīng)式對(duì)象,這樣,我們對(duì)對(duì)象做的修改,響應(yīng)式都能夠監(jiān)聽到,并且執(zhí)行 effect 內(nèi)部注冊(cè)的函數(shù)來執(zhí)行數(shù)據(jù)修改之后的效果
那今天我就跟你聊一下 Vue 在瀏覽器里是如何運(yùn)行的,照例我們還是對(duì)著 Vue 3 的源碼來學(xué)習(xí),不過源碼復(fù)雜,為了幫助你理解主要邏輯,我會(huì)直接把源碼簡(jiǎn)化再演示
好了廢話不多說,我們馬上開始;前端框架需要處理的最核心的兩個(gè)流程,就是首次渲染和數(shù)據(jù)更新后的渲染
首次渲染
我們知道,想要啟動(dòng)一個(gè) Vue 項(xiàng)目,只需要從 Vue 中引入 createApp,傳入 App 組件,并且調(diào)用 createApp 返回的 App 實(shí)例的 mount 方法,就實(shí)現(xiàn)了項(xiàng)目的啟動(dòng);這個(gè)時(shí)候 Vue 也完成了首次渲染,代碼邏輯如下:

所以 createApp 就是項(xiàng)目的初始化渲染入口
這里就有一個(gè)看代碼的小技巧,分享給你,我們首次查看源碼的時(shí)候,可以先把一些無用的信息刪除,方便自己梳理主體的邏輯???Vue 代碼,和今天主題無關(guān)的無用信息有哪些,COMPAT 代碼是用來兼容 Vue 2 的,DEV 代碼是用來調(diào)試的,我們可以把這些代碼刪除之后,得到下面的簡(jiǎn)化版 createApp 源碼
再看思路就比較清晰了
我們使用 ensureRenderer 返回的對(duì)象去創(chuàng)建 app,并且重寫了app.mount 方法;在 mount 方法內(nèi)部,我們查找 mount 傳遞的 DOM 元素,并且調(diào)用 ensureRenderer 返回的 mount 方法,進(jìn)行初始化渲染
如下圖所示:

之前我們講過要會(huì) TypeScript,這時(shí)你就能感受到 TypeScript 的好處了,現(xiàn)在即使我們不知道 app.mount 是什么邏輯,也能知道這個(gè)函數(shù)的參數(shù)只能是 Element、ShadowRoot 或者 string 三者之一,也就很好理解內(nèi)部的 normalizeContainer 就是把你傳遞的參數(shù)統(tǒng)一變?yōu)闉g覽器的 DOM 元素,Typescript 類型帶來的好處,我們?cè)谧x源碼的時(shí)候會(huì)一直感受得到
export const createApp = (...args) => {
const app = ensureRenderer().createApp(...args);
const { mount } = app;
// 重寫mount
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
const container = normalizeContainer(containerOrSelector);
if (!container) return;
const component = app._component;
if (!isFunction(component) && !component.render && !component.template) {
component.template = container.innerHTML;
}
container.innerHTML = "";
const proxy = mount(container, false, container instanceof SVGElement);
if (container instanceof Element) {
container.removeAttribute("v-cloak");
container.setAttribute("data-v-app", "");
}
return proxy;
};
return app;
};我們繼續(xù)深入了解 ensureRenderer 方法,以及 ensureRenderer 方法返回的 createApp方法
這里 ensureRenderer 函數(shù),內(nèi)部通過 createRenderer 函數(shù),創(chuàng)建了一個(gè)瀏覽器的渲染器,并且緩存了渲染器 renderer,這種使用閉包做緩存的方式,你在日常開發(fā)中也可以借鑒這種思路
createRenderer 函數(shù),我們?cè)谧远x渲染器那一講里學(xué)到過,傳遞的 rendererOptions 就是瀏覽器里面標(biāo)簽的增刪改查 API:
// 瀏覽器dom操作
import { nodeOps } from "./nodeOps"; // 瀏覽器dom屬性更新
import { patchProp } from "./patchProp";
import { createRenderer } from "@vue/runtime-core";
const rendererOptions = extend({ patchProp }, nodeOps);
let renderer: Renderer<Element | ShadowRoot> | HydrationRenderer;
function ensureRenderer() {
return (
renderer ||
((renderer = createRenderer < Node),
Element | (ShadowRoot > rendererOptions))
);
}可以看到,createRenderer 函數(shù)傳遞的參數(shù)是 nodeOps 和 patchProp 的合并對(duì)象
我們繼續(xù)進(jìn)入 nodeOps 和 pathProp 也可以看到下面的代碼,寫了很多方法;通過 ensureRenderer 存儲(chǔ)這些操作方法后,createApp 內(nèi)部就可以脫離具體的渲染平臺(tái)了,這也是 Vue 3 實(shí)現(xiàn)跨端的核心邏輯:
export const nodeOps: Omit<RendererOptions<Node, Element>, 'patchProp'> = {
insert: (child, parent, anchor) => { parent.insertBefore(child, anchor || null)
},
remove: child => {
const parent = child.parentNode
if (parent) {
parent.removeChild(child)
}
},
createElement: (tag, isSVG, is, props): Element => {
const el = isSVG ? doc.createElementNS(svgNS, tag) : doc.createElement(tag, is ? { is } : undefined)
if (tag === 'select' && props && props.multiple != null) {
;(el as HTMLSelectElement).setAttribute('multiple', props.multiple)
}
return el
},
createText: text => doc.createTextNode(text),
createComment: text => doc.createComment(text),
setText: (node, text) => { node.nodeValue = text },
setElementText: (el, text) => { el.textContent = text },
parentNode: node => node.parentNode as Element | null,
nextSibling: node => node.nextSibling,
querySelector: selector => doc.querySelector(selector),
...
}然后我們就需要進(jìn)入到 rumtime-core 模塊去看下 createRenderer 是如何工作的
createRenderer 是調(diào)用 baseCreateRenderer 創(chuàng)建的,baseCreateRenderer 函數(shù)內(nèi)部有十幾個(gè)函數(shù),代碼行數(shù)合計(jì) 2000 行左右,這也是我們學(xué)習(xí) Vue 源碼最復(fù)雜的一個(gè)函數(shù)了
按前面簡(jiǎn)化源碼的思路,先把工具函數(shù)的實(shí)現(xiàn)折疊起來,精簡(jiǎn)之后代碼主要邏輯其實(shí)很簡(jiǎn)單
我們一起來看
首先獲取了平臺(tái)上所有的 insert、remove 函數(shù),這些函數(shù)都是 nodeOps 傳遞進(jìn)來的,然后定義了一些列 patch、mount、unmount 函數(shù),通過名字我們不難猜出,這就是 Vue 中更新、渲染組件的工具函數(shù),比如mountElement 就是渲染 DOM 元素、mountComponent 就是渲染組件 updateComponent 就是更新組件
export function createRenderer<
HostNode = RendererNode,
HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
return baseCreateRenderer < HostNode, HostElement > (options)
}
function baseCreateRenderer() {
const { insert: hostInsert,
remove: hostRemove,
patchProp: hostPatchProp,
createElement: hostCreateElement,
createText: hostCreateText,
createComment: hostCreateComment,
setText: hostSetText,
setElementText: hostSetElementText,
parentNode: hostParentNode,
nextSibling: hostNextSibling,
setScopeId: hostSetScopeId = NOOP,
cloneNode: hostCloneNode,
insertStaticContent: hostInsertStaticContent
} = options
const patch = () =>... //一個(gè)函數(shù)
const processText = () =>...
const processCommentNode = () =>...
const processElement = () =>...
const mountElement = () =>...
const mountChildren = () =>...
const patchElement = () =>...
const patchBlockChildren = () =>...
const patchProps = () =>...
const processComponent = () =>...
const mountComponent = () =>...
const updateComponent = () =>...
const setupRenderEffect = () =>...
const patchChildren = () =>...
const patchKeyedChildren = () =>...
const unmount = () =>...
const unmountComponent = () =>...
const unmountComponent = () =>...
const unmountComponent = () =>...
const unmountComponent = () =>...
const render: RootRenderFunction = (vnode, container, isSVG) => {
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
} else {
patch(container._vnode || null, vnode, container, null, null, null, isSV }
flushPostFlushCbs() container._vnode = vnode
} return {
render, hydrate, createApp: createAppAPI(render, hydrate)
}
}整個(gè) createApp 函數(shù)的執(zhí)行邏輯如下圖所示:

最后返回的 createApp 方法,實(shí)際上是 createAPI 的返回值,并且給 createAPI 傳遞了render 方法;render 方法內(nèi)部很簡(jiǎn)單,就是判斷 container 容器上有沒有 _vnode 屬性,如果有的話就執(zhí)行 unmout 方法,沒有的話就執(zhí)行 patch 方法,最后把 vnode 信息存儲(chǔ)在 container._vnode 上
那 createAppAPI 又做了什么呢?我們繼續(xù)進(jìn)入 createAppAPI 源碼,看下面的代碼;內(nèi)部創(chuàng)建了一個(gè) app 對(duì)象,app 上注冊(cè)了我們熟悉的 use、component 和 mount 等方法:
export function createAppAPI<HostElement>(render: RootRenderFunction, hydrate?: RootHydrateFunction): CreateAppFunction<HostElement> {
return function createApp(rootComponent, rootProps = null) {
const context = createAppContext()
let isMounted = false const app: App = (context.app = {
_context: context, _instance: null,
use(plugin: Plugin, ...options: any[]) ,
component(name: string, component?: Component): any {
if (!component) {
return context.components[name]
}
context.components[name] = component return app
},
directive(name: string, directive?: Directive)
mount(rootContainer: HostElement, isHydrate?: boolean, isSVG?: boolean): any {
if (!isMounted) {
const vnode = createVNode(rootComponent as ConcreteComponent, rootProps)
vnode.appContext = context // 核心的邏輯
if (isHydrate && hydrate) {
hydrate(vnode as VNode<Node, Element>, rootContainer as any)
} else {
render(vnode, rootContainer, isSVG)
}
return getExposeProxy(vnode.component!) || vnode.component!.proxy
}
},
provide(key, value) { context.provides[key as string] = value return app }
}) return app
}
}可以看到 mount 內(nèi)部執(zhí)行的是傳遞進(jìn)來的 render 方法,也就是上面的 render 方法
container 就是我們 app.mount 中傳遞的 DOM 元素,對(duì) DOM 元素進(jìn)行處理之后,執(zhí)行 patch 函數(shù)實(shí)現(xiàn)整個(gè)應(yīng)用的加載
所以我們的下一個(gè)任務(wù)就是需要搞清楚 patch 函數(shù)的執(zhí)行邏輯
patch 函數(shù)
patch 傳遞的是 container._vnode,也就是上一次渲染緩存的 vnode、本次渲染組件的vnode,以及容器 container;下面就是 patch 函數(shù)的代碼,核心代碼我添加了注釋;其中 n1 是上次渲染的虛擬 DOM,n2 是下次要渲染的虛擬 DOM
首先可以把 n1 和 n2 做一次判斷,如果虛擬 DOM 的節(jié)點(diǎn)類型不同,就直接 unmount 之前的節(jié)點(diǎn);因?yàn)楸热缰笆?Button 組件,現(xiàn)在要渲染 Container 組件,就沒有計(jì)算 diff的必要,直接把 Button 組件銷毀再渲染 Container 即可
如果 n1 和 n2 類型相同,比如都是 Button 組件或者都是 div 標(biāo)簽,我們需要判斷具體的類型再去執(zhí)行不同的函數(shù),比如 processText、processFragment、processElement 以及 processComponent 等函數(shù);
看第 55 行,這里的 ShapeFlags 用到了位運(yùn)算的知識(shí),我們后面會(huì)通過刷算法題的方式介紹,暫時(shí)我們只需要知道,ShapeFlags 可以幫助我們快速判斷需要操作的類型就可以了
const patch: PatchFn = (
n1,
n2,
container,
anchor = null,
parentComponent = null,
parentSuspense = null,
isSVG = false,
slotScopeIds = null,
optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
) => { // 兩次虛擬dom完全一樣 啥也不用干
if (n1 === n2) { return }// 虛擬dom節(jié)點(diǎn)類型不一樣, unmount老的虛擬dom,并且n1賦值null if (n1 && !isSameVNodeType(n1, n2)) { anchor = getNextHostNode(n1) unmount(n1, parentComponent, parentSuspense, true)n1 = null }// n2是要渲染的虛擬dom,我們獲取type,ref和shapeFlag const { type, ref, shapeFlag } = n2 switch (type) { case Text: // 文本
processText(n1, n2, container, anchor) break case Comment: // 注釋
processCommentNode(n1, n2, container, anchor) break case Static: // 靜態(tài)節(jié)點(diǎn)
if (n1 == null) {
mountStaticNode(n2, container, anchor, isSVG)
} else if (__DEV__) {
patchStaticNode(n1, n2, container, isSVG)
}
break
case Fragment: processFragment(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized)
break default: // 運(yùn)運(yùn)算判斷操作類型
if (shapeFlag & ShapeFlags.ELEMENT) { // html標(biāo)簽
processElement(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized)
} else if (shapeFlag & ShapeFlags.COMPONENT) { // 組件
processComponent(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized)
} else if (shapeFlag & ShapeFlags.TELEPORT) {
(type as typeof TeleportImpl).process(
n1 as TeleportVNode, n2 as TeleportVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds, optimized, internals
)
} else if (
__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE
) {
;
(type as typeof SuspenseImpl).process(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
internals
)
}
else if (__DEV__) { warn('Invalid VNode type:', type, `(${typeof type})`) }
}// set ref if (ref != null && parentComponent) { setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2) }代碼的整體執(zhí)行邏輯如下圖所示:

我們首次渲染的 App 是一個(gè)組件,所以要執(zhí)行的就是 processComponent 方法
processComponent 方法
那我們繼續(xù)進(jìn)入到 processComponent 代碼內(nèi)部,看下面的代碼。首次渲染的時(shí)候,n1就是 null,所以會(huì)執(zhí)行 mountComponent;如果是更新組件的時(shí)候,n1 就是上次渲染的 vdom,需要執(zhí)行 updateComponent
const processComponent = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean) => {
n2.slotScopeIds = slotScopeIds
if (n1 == null) {
if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
; (parentComponent!.ctx as KeepAliveContext).activate(n2, container, anchor, isSVG, optimized)
}
else {
mountComponent(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
}
} else {
updateComponent(n1, n2, optimized)
}
}updateComponent 是虛擬 DOM 的邏輯,我們會(huì)在下一講詳細(xì)剖析,這一講主要講首次渲染的過程
所以我們進(jìn)入 mountComponent 函數(shù)中,可以看到 mountComponent 函數(shù)內(nèi)部會(huì)對(duì)組件的類型進(jìn)行一系列的判斷,還有一些對(duì) Vue 2 的兼容代碼,核心的渲染邏輯就是 setupComponent 函數(shù)和 setupRenderEffect 函數(shù)
import { setupComponent } from './component'
const mountComponent: MountComponentFn = () => {
// 2.x compat may pre-creaate the component instance before actually
// mounting
const compatMountInstance = __COMPAT__ && initialVNode.isCompatRoot && initialVNode.component
const instance: ComponentInternalInstance = compatMountInstance || (
initialVNode.component = createComponentInstance(initialVNode, parentComponent, parentSuspense)
) // resolve props and slots for setup context
if (!(__COMPAT__ && compatMountInstance)) {
setupComponent(instance)
} (instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG, o
ptimized
)
if (__DEV__) { popWarningContext() endMeasure(instance, `mount`) }
}setupComponent 和 setupRenderEffect,它倆又做了點(diǎn)什么呢?可以參考下面的示意圖這兩個(gè)實(shí)現(xiàn)組件首次渲染的函數(shù):

setupComponent
首先看 setupComponent,要完成的就是執(zhí)行我們寫的 setup 函數(shù)
可以看到,內(nèi)部先初始化了 props 和 slots,并且執(zhí)行 setupStatefulComponent 創(chuàng)建組件,而這個(gè)函數(shù)內(nèi)部從 component 中獲取 setup 屬性,也就是 script setup 內(nèi)部實(shí)現(xiàn)的函數(shù),就進(jìn)入到我們組件內(nèi)部的reactive、ref 等函數(shù)實(shí)現(xiàn)的邏輯了
export function setupComponent(
instance: ComponentInternalInstance,
isSSR = false) { isInSSRComponentSetup = isSSR const { props, children } = instance.vnode const isStateful = isStatefulComponent(instance) initProps(instance, props, isStateful, isSSR) initSlots(instance, children) const setupResult = isStateful ? setupStatefulComponent(instance, isSSR) : undefined isInSSRComponentSetup = false return setupResult } function setupStatefulComponent(instance: ComponentInternalInstance, isSSR: boolean) {
const Component = instance.type as ComponentOptions // 執(zhí)行setup
const { setup } = Component if (setup) {
const setupContext = (instance.setupContext = setup.length > 1 ? createSetupContext(instance) : null)
setCurrentInstance(instance) pauseTracking()
const setupResult = callWithErrorHandling(
setup,
instance,
ErrorCodes.SETUP_FUNCTION,
[instance.props, setupContext])
if (isPromise(setupResult)) {
setupResult.then(
unsetCurrentInstance, unsetCurrentInstance
)
} else {
handleSetupResult(instance, setupResult, isSSR)
}
} else {
finishComponentSetup(instance, isSSR)
}
}
export function callWithErrorHandling(
fn: Function,
instance: ComponentInternalInstance | null,
type: ErrorTypes, args?: unknown[]
) {
let res
try { res = args ? fn(...args) : fn() } catch (err) { handleError(err, instance, type) } return res
}setupRenderEffect
另一個(gè) setupRenderEffect 函數(shù),就是為了后續(xù)數(shù)據(jù)修改注冊(cè)的函數(shù),我們先梳理一下核心的實(shí)現(xiàn)邏輯
組件首次加載會(huì)調(diào)用 patch 函數(shù)去初始化子組件,注意 setupRenderEffect 本身就是在 patch 函數(shù)內(nèi)部執(zhí)行的,所以這里就會(huì)遞歸整個(gè)虛擬 DOM 樹,然后觸發(fā)生命周期 mounted,完成這個(gè)組件的初始化
頁(yè)面首次更新結(jié)束后,setupRenderEffect 不僅實(shí)現(xiàn)了組件的遞歸渲染,還注冊(cè)了組件的更新機(jī)制
在下面的核心代碼中,我們通過 ReactiveEffect 創(chuàng)建了 effect 函數(shù),這個(gè)概念上一講我們手寫過,然后執(zhí)行 instance.update 賦值為 effect.run 方法,這樣結(jié)合 setup 內(nèi)部的 ref 和 reactive 綁定的數(shù)據(jù),數(shù)據(jù)修改之后,就會(huì)觸發(fā) update 方法的執(zhí)行,內(nèi)部就會(huì) componentUpdateFn,內(nèi)部進(jìn)行遞歸的 patch 調(diào)用執(zhí)行每個(gè)組件內(nèi)部的 update 方法實(shí)現(xiàn)組件的更新
if (!instance.isMounted) {
patch(null, subTree, container, anchor, instance, parentSuspense, isSVG)
} else {
// updateComponent
}
// create reactive effect for rendering
const effect = new ReactiveEffect(componentUpdateFn, () => queueJob(instance.update),
instance.scope // track it in component's effect scope
)
const update = (instance.update = effect.run.bind(effect) as SchedulerJob)
update.id = instance.uid
update()這樣我們就實(shí)現(xiàn)了整個(gè) Vue 的渲染和更新流程
到此這篇關(guān)于深度剖析 Vue3 在瀏覽器的運(yùn)行原理的文章就介紹到這了,更多相關(guān)Vue3 運(yùn)行原理內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
第一次在Vue中完整使用AJAX請(qǐng)求和axios.js的實(shí)戰(zhàn)記錄
AJAX是現(xiàn)代Web開發(fā)的一個(gè)關(guān)鍵部分,盡管它一開始看起來令人生畏,但在你的武庫(kù)中擁有它是必須的,下面這篇文章主要給大家介紹了關(guān)于第一次在Vue中完整使用AJAX請(qǐng)求和axios.js的相關(guān)資料,需要的朋友可以參考下2022-11-11
vue-cli創(chuàng)建的項(xiàng)目,配置多頁(yè)面的實(shí)現(xiàn)方法
下面小編就為大家分享一篇vue-cli創(chuàng)建的項(xiàng)目,配置多頁(yè)面的實(shí)現(xiàn)方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2018-03-03
vue如何使用router.meta.keepAlive對(duì)頁(yè)面進(jìn)行緩存
這篇文章主要介紹了vue如何使用router.meta.keepAlive對(duì)頁(yè)面進(jìn)行緩存問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-05-05
一次用vue3簡(jiǎn)單封裝table組件的實(shí)戰(zhàn)過程
之所以封裝全局組件是為了省事,所有的目的,全都是為了偷懶,下面這篇文章主要給大家介紹了關(guān)于用vue3簡(jiǎn)單封裝table組件的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-12-12
vue動(dòng)態(tài)修改頁(yè)面title的兩種方法
本文主要介紹了vue動(dòng)態(tài)修改頁(yè)面title的兩種方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-06-06
關(guān)于Element-UI可編輯表格的實(shí)現(xiàn)過程
這篇文章主要介紹了關(guān)于Element-UI可編輯表格的實(shí)現(xiàn)過程,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-07-07
基于Vue el-autocomplete 實(shí)現(xiàn)類似百度搜索框功能
本文通過代碼給大家介紹了Vue el-autocomplete 實(shí)現(xiàn)類似百度搜索框功能,代碼簡(jiǎn)單易懂,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-10-10

