深度剖析?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è)組件的初始化
頁面首次更新結(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)鍵部分,盡管它一開始看起來令人生畏,但在你的武庫中擁有它是必須的,下面這篇文章主要給大家介紹了關(guān)于第一次在Vue中完整使用AJAX請(qǐng)求和axios.js的相關(guān)資料,需要的朋友可以參考下2022-11-11vue-cli創(chuàng)建的項(xiàng)目,配置多頁面的實(shí)現(xiàn)方法
下面小編就為大家分享一篇vue-cli創(chuàng)建的項(xiàng)目,配置多頁面的實(shí)現(xiàn)方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2018-03-03vue如何使用router.meta.keepAlive對(duì)頁面進(jìn)行緩存
這篇文章主要介紹了vue如何使用router.meta.keepAlive對(duì)頁面進(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-12vue動(dòng)態(tài)修改頁面title的兩種方法
本文主要介紹了vue動(dòng)態(tài)修改頁面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