您好,登錄后才能下訂單哦!
這篇文章給大家分享的是有關vue3中渲染系統(tǒng)的示例分析的內(nèi)容。小編覺得挺實用的,因此分享給大家做個參考,一起跟隨小編過來看看吧。
思考
在開始今天的文章之前,大家可以想一下:
vue
文件是如何轉換成DOM
節(jié)點,并渲染到瀏覽器上的?
數(shù)據(jù)更新時,整個的更新流程又是怎么樣的?
vuejs
有兩個階段:編譯時和運行時。
編譯時
我們平常開發(fā)時寫的.vue
文件是無法直接運行在瀏覽器中的,所以在webpack
編譯階段,需要通過vue-loader
將.vue
文件編譯生成對應的js
代碼,vue
組件對應的template
模板會被編譯器轉化為render函數(shù)
。
運行時
接下來,當編譯后的代碼真正運行在瀏覽器時,便會執(zhí)行render函數(shù)
并返回VNode
,也就是所謂的虛擬DOM
,最后將VNode
渲染成真實的DOM節(jié)點
。
了解完vue
組件渲染的思路后,接下來讓我們從Vue.js 3.0(后續(xù)簡稱vue3
)的源碼出發(fā),深入了解vue
組件的整個渲染流程是怎么樣的?
準備
本文主要是分析
vue3
的渲染系統(tǒng),為了方便調(diào)試,我們直接通過引入vue.js
文件的方式進行源碼調(diào)試分析。
vue3
源碼下載
# 源碼地址(推薦ssh方式下載) https://github.com/vuejs/vue-next # 或者下載筆者做筆記用的版本 https://github.com/AsyncGuo/vue-next/tree/vue3_notes
生成vue.global.js
文件
npm run dev # bundles .../vue-next/packages/vue/src/index.ts → packages/vue/dist/vue.global.js... # created packages/vue/dist/vue.global.js in 2.8s
啟動開發(fā)環(huán)境
npm run serve
測試代碼
<!-- 調(diào)試代碼目錄:/packages/vue/examples/test.html --> <script src="./../dist/vue.global.js"></script> <div id="app"> <div>static node</div> <div>{{title}}</div> <button @click="add">click</button> <Item :msg="title"/> </div> <script> const Item = { props: ['msg'], template: `<div>{{ msg }}</div>` } const app = Vue.createApp({ components: { Item }, setup() { return { title: Vue.ref(0) } }, methods: { add() { this.title += 1 } }, }) app.mount('#app') </script>
創(chuàng)建應用
從上面的測試代碼,我們會發(fā)現(xiàn)vue3
和vue2
的掛載方式是不同的,vue3
是通過createApp
這個入口函數(shù)進行應用的創(chuàng)建。接下來我們來看下createApp
的具體實現(xiàn):
// 入口文件: /vue-next/packages/runtime-dom/src/index.ts const createApp = ((...args) => { console.log('createApp入?yún)?', ...args); // 創(chuàng)建應用 const app = ensureRenderer().createApp(...args); const { mount } = app; // 重寫mount app.mount = (containerOrSelector) => { // ... }; return app; });
ensureRenderer
首先通過ensureRenderer
創(chuàng)建web端的渲染器,我們來看下具體實現(xiàn):
// 更新屬性的方法 const patchProp = () => { // ... } // 操作DOM的方法 const nodeOps = { insert: (child, parent, anchor) => { parent.insertBefore(child, anchor || null) }, remove: child => { const parent = child.parentNode if (parent) { parent.removeChild(child) } }, ... } // web端的渲染器所需的參數(shù)設置 const rendererOptions = extend({ patchProp }, nodeOps); let renderer; // 延遲創(chuàng)建renderer function ensureRenderer() { return (renderer || (renderer = createRenderer(rendererOptions))); }
在這里可以看出,通過延遲創(chuàng)建渲染器,當我們只依賴響應式包的情況下,可以通過tree-shaking移除渲染相關的代碼,大大減少包的體積。
createRenderer
通過ensureRenderer
可以看出,真正的入口是這個createRenderer
方法:
// /vue-next/packages/runtime-core/src/renderer.ts export function createRenderer(options) { return baseCreateRenderer(options) } function baseCreateRenderer(options, createHydrationFns) { // 通用的DOM操作方法 const { insert: hostInsert, remove: hostRemove, ... } = options // ======================= // 渲染的核心流程 // 通過閉包緩存內(nèi)斂函數(shù) // ======================= const patch = () => {} // 核心diff過程 const processElement = () => {} // 處理element const mountElement = () => {} // 掛載element const mountChildren = () => {} // 掛載子節(jié)點 const processFragment = () => {} // 處理fragment節(jié)點 const processComponent = () => {} // 處理組件 const mountComponent = () => {} // 掛載組件 const setupRenderEffect = () => {} // 運行帶副作用的render函數(shù) const render = () => {} // 渲染掛載流程 // ... // ======================= // 2000+行的內(nèi)斂函數(shù) // ======================= return { render, hydrate, // 服務端渲染相關 createApp: createAppAPI(render, hydrate) } }
接下來我們先跳過這些內(nèi)斂函數(shù)的實現(xiàn)(后面的渲染流程用到時,我們再具體分析),來看下createAppAPI
的具體實現(xiàn):
createAppAPI
function createAppAPI(render, hydrate) { // 真正創(chuàng)建app的入口 return function createApp(rootComponent, rootProps = null) { // 創(chuàng)建vue應用上下文 const context = createAppContext(); // 已安裝的vue插件 const installedPlugins = new Set(); let isMounted = false; const app = (context.app = { _uid: uid++, _component: rootComponent, // 根組件 use(plugin, ...options) { // ... return app }, mixin(mixin) {}, component(name, component) {}, directive(name, directive) {}, mount(rootContainer) {}, unmount() {}, provide(key, value) {} }); return app; }; }
可以看出,createAppAPI
返回的createApp
函數(shù)才是真正創(chuàng)建應用的入口。在createApp
里會創(chuàng)建vue
應用的上下文,同時初始化app
,并綁定應用上下文到app
實例上,最后返回app
。
這里有個值得注意的點:
app
對象上的use
、mixin
、component
和directive
方法都返回了app
應用實例,開發(fā)者可以鏈式調(diào)用。
// 一直use一直爽 createApp(App).use(Router).use(Vuex).component('component',{}).mount("#app")
到此app應用實例已經(jīng)創(chuàng)建好了~,打印查看下創(chuàng)建的app
應用:
總結一下創(chuàng)建app
應用實例的過程:
創(chuàng)建web端
對應的渲染器(延遲創(chuàng)建,tree-shaking)
執(zhí)行baseCreateRenderer
方法(通過閉包緩存內(nèi)斂函數(shù),后續(xù)掛載階段的主流程)
執(zhí)行createAppAPI
方法(1. 創(chuàng)建應用上下文;2. 創(chuàng)建app并返回)
掛載階段
接下來,當我們執(zhí)行app.mount
時,便會開始掛載組件。而我們調(diào)用的app.mount
則是重寫后的mount
方法:
const createApp = ((...args) => { // ... const { mount } = app; // 緩存原始的mount方法 // 重寫mount app.mount = (containerOrSelector) => { // 獲取容器 const container = normalizeContainer(containerOrSelector); if (!container) return; const component = app._component; // 判斷如果傳入的根組件不是函數(shù)&根組件沒有render函數(shù)&沒有template,就把容器的內(nèi)容設置為根組件的template if (!isFunction(component) && !component.render && !component.template) { component.template = container.innerHTML; } // 清空容器內(nèi)容 container.innerHTML = ''; // 執(zhí)行緩存的mount方法 const proxy = mount(container, false, container); return proxy; }; return app; });
執(zhí)行完web端
重寫的mount
方法后,才是真正掛載組件的開始,即調(diào)用createAppAPI
返回的app應用
上的mount
方法:
function createAppAPI(render, hydrate) { // 真正創(chuàng)建app的入口 return function createApp(rootComponent, rootProps = null) { // ... const app = (context.app = { // 掛載根組件 mount(rootContainer, isHydrate, isSVG) { if (!isMounted) { // 創(chuàng)建根組件對應的vnode const vnode = createVNode(rootComponent, rootProps); // 根級vnode存在應用上下文 vnode.appContext = context; // 將虛擬vnode節(jié)點渲染成真實節(jié)點,并掛載 render(vnode, rootContainer, isSVG); isMounted = true; // 記錄應用的根組件容器 app._container = rootContainer; rootContainer.__vue_app__ = app; app._instance = vnode.component; return vnode.component.proxy; } } }); return app; }; }
總結一下,mount
方法主要做了什么呢?
創(chuàng)建根組件對應的vnode
根組件vnode
綁定應用上下文context
渲染vnode
成真實節(jié)點,并掛載
記錄掛載狀態(tài)
細心的同學可能已經(jīng)發(fā)現(xiàn)了,這里的
mount
方法是一個標準的跨平臺渲染流程,抽象vnode
,然后通過rootContainer
實現(xiàn)特定平臺的渲染,例如在瀏覽器環(huán)境下,它就是一個DOM
對象,在其他平臺就是其他特定的值。這也就是為什么我們在調(diào)用runtime-dom
包的creataApp
方法時,重寫mount
方法,完善不同平臺的渲染邏輯。
創(chuàng)建vnode
提到
vnode
,可能更多人會和高性能聯(lián)想到一起,誤以為vnode
的性能就一定比手動操作DOM
的高,其實不然。vnode
的底層同樣是要操作DOM
,相反如果vnode
的patch
過程過長,同樣會導致頁面的卡頓。 而vnode
的提出則是對原生DOM
的抽象,在跨平臺設計的處理上會起到一定的抽象化。例如:服務端渲染、小程序端渲染、weex平臺...
接下來,我們來看下創(chuàng)建vnode
的過程:
function _createVNode( type, props, children, patchFlag, ... ): VNode { // 規(guī)范化class & style // 例如:class=[]、class={}、style=[]等格式,需規(guī)范化 if (props) { // ... } // 獲取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; return createBaseVNode() }
function createBaseVNode( type, props = null, children = null, ... ) { // vnode的默認結構 const vnode = { __v_isVNode: true, // 是否為vnode __v_skip: true, // 跳過響應式數(shù)據(jù)化 type, // 創(chuàng)建vnode的第一個參數(shù) props, // DOM參數(shù) children, component: null, // 組件實例(instance),通過createComponentInstance創(chuàng)建 shapeFlag, // 類型標記,在patch階段,通過匹配shapeFlag進行相應的渲染過程 ... }; // 標準化子節(jié)點 if (needFullChildrenNormalization) { normalizeChildren(vnode, children); } // 收集動態(tài)子代節(jié)點或子代block到父級block tree if (isBlockTreeEnabled > 0 && !isBlockNode && currentBlock && (vnode.patchFlag > 0 || shapeFlag & 6 /* COMPONENT */) && vnode.patchFlag !== 32 /* HYDRATE_EVENTS */) { currentBlock.push(vnode); } return vnode; }
通過上面的代碼,我們可以總結一下,創(chuàng)建vnode
階段都做了什么:
規(guī)范化class & style(例如:class=[]、class={}、style=[]等格式)
標記vnode
的類型shapeFlag
,即根組件對應的vnode
類型(type即為根組件rootComponent
,此時根組件為對象格式,所以shapeFlag
即為4)
標準化子節(jié)點(初始化時,children為空)
收集動態(tài)子代節(jié)點或子代block
到父級block tree
(這里便是vue3
引入的新概念:block tree
,篇幅有限,本文就不展開陳述了)
這里,我們可以打印查看一下此時根組件對應的vnode
結構:
渲染vnode
通過createVNode
獲取到根組件對應的vnode
,然后執(zhí)行render
方法,而這里的render
函數(shù)便是baseCreateRenderer
通過閉包緩存的render
函數(shù):
// 實際調(diào)用的render方法即為baseCreateRenderer方法中緩存的render方法 function baseCreateRenderer() { const render = (vnode, container) => { if (vnode == null) { if (container._vnode) { // 卸載組件 unmount() } } else { // 正常掛載 patch(container._vnode || null, vnode, container) } } }
當傳入的vnode
為null
&存在老的vnode
,則進行卸載組件
否則,正常掛載
掛載完成后,批量執(zhí)行組件生命周期
綁定vnode到容器上,以便后續(xù)更新階段通過新舊vnode
進行patch
??:接下來,整個渲染過程將會在
baseCreateRenderer
這個核心函數(shù)的內(nèi)斂函數(shù)中執(zhí)行~
patch
接下來,我們來看下render
過程中的patch
函數(shù)的實現(xiàn):
const patch = ( n1, // 舊的vnode n2, // 新的vnode container, // 掛載的容器 ... ) => { // ... const { type, ref, shapeFlag } = n2 switch (type) { case Text: // 處理文本 processText(n1, n2, container, anchor) break case Comment: // 注釋節(jié)點 processCommentNode(n1, n2, container, anchor) break case Static: // 靜態(tài)節(jié)點 if (n1 == null) { mountStaticNode(n2, container, anchor, isSVG) } break case Fragment: // fragment節(jié)點 processFragment(n1, n2, container, ...) break default: if (shapeFlag & 1 /* ELEMENT */) { // 處理DOM元素 processElement(n1, n2, container, ...); } else if (shapeFlag & 6 /* COMPONENT */) { // 處理組件 processComponent(n1, n2, container, ...); } else if (shapeFlag & 64 /* TELEPORT */) { type.process(n1, n2, container, ...); } else if (shapeFlag & 128 /* SUSPENSE */) { type.process(n1, n2, container, ...); } } }
分析patch
函數(shù),我們會發(fā)現(xiàn)patch
函數(shù)會通過判斷type
和shapeFlag
的不同來走不同的處理邏輯,今天我們主要分析組件類型和普通DOM元素的處理。
processComponent
初始化渲染時,type
為object
并且shapeFlag
對應的值為4
(位運算4 & 6),即對應processComponent
組件的處理方法:
const processComponent = (n1, n2, container, ...) => { if (n1 == null) { if (n2.shapeFlag & 512 /* COMPONENT_KEPT_ALIVE */) { // 激活組件(已緩存的組件) parentComponent.ctx.activate(n2, container, ...); } else { // 掛載組件 mountComponent(n2, container, ...); } } else { // 更新組件 updateComponent(n1, n2, optimized); } };
如果n1
為null
,則執(zhí)行掛載組件;否則更新組件。
mountComponent
接下來我們繼續(xù)看掛載組件的mountComponent
函數(shù)的實現(xiàn):
const mountComponent = (initialVNode, container, ...) => { // 1. 創(chuàng)建組件實例 const instance = ( // 這個時候就把組件實例掛載到了組件vnode的component屬性上了 initialVNode.component = createComponentInstance(initialVNode, parentComponent, parentSuspense) ); // 2. 設置組件實例 setupComponent(instance); // 3. 設置并運行帶有副作用的渲染函數(shù) setupRenderEffect(instance, initialVNode, container,...); };
省略掉無關主流程的代碼后,可以看到,mountComponent
函數(shù)主要做了三件事:
創(chuàng)建組件實例
function createComponentInstance(vnode, parent, suspense) { const type = vnode.type; // 綁定應用上下文 const appContext = (parent ? parent.appContext : vnode.appContext) || emptyAppContext; // 組件實例的默認值 const instance = { uid: uid$1++, //組件唯一id vnode, // 當前組件的vnode type, // vnode節(jié)點類型 parent, // 父組件的實例instance appContext, // 應用上下文 root: null, // 根實例 next: null, // 當前組件mounted時,為null,將設置為instance.vnode,下次update時,將執(zhí)行updateComponentPreRender subTree: null, // 組件的渲染vnode,由組件的render函數(shù)生成,創(chuàng)建后同步 update: null, // 組件內(nèi)容掛載或更新到視圖的執(zhí)行回調(diào),創(chuàng)建后同步 scope: new EffectScope(true /* detached */), render: null, // 組件的render函數(shù),在setupStatefulComponent階段賦值 proxy: null, // 是一個proxy代理ctx字段,內(nèi)部使用this時,指向它 // local resovled assets // resolved props and emits options // emit // props default value // inheritAttrs // state // suspense related // lifecycle hooks }; { instance.ctx = createDevRenderContext(instance); } instance.root = parent ? parent.root : instance; instance.emit = emit.bind(null, instance); return instance; }
createComponentInstance
函數(shù)主要是初始化組件實例并返回,打印查看下根組件對應的instance
內(nèi)容:
設置組件實例
function setupComponent(instance, isSSR = false) { const { props, children } = instance.vnode; // 判斷是否為狀態(tài)組件 const isStateful = isStatefulComponent(instance); // 初始化組件屬性、slots initProps(instance, props, isStateful, isSSR); initSlots(instance, children); // 當狀態(tài)組件時,掛載setup信息 const setupResult = isStateful ? setupStatefulComponent(instance, isSSR) : undefined; return setupResult; }
setupComponent
的邏輯也很簡單,首先初始化組件props
和slots
掛載到組件實例instance
上,然后根據(jù)組件類型vnode.shapeFlag===4
,判斷是否掛載setup
信息(也就是vue3的composition api)。
function setupStatefulComponent(instance, isSSR) { const Component = instance.type; // 創(chuàng)建渲染上下文的屬性訪問緩存 instance.accessCache = Object.create(null); // 創(chuàng)建渲染上下文代理 instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers)); const { setup } = Component; // 判斷組件是否存在setup if (setup) { // 判斷setup是否有參數(shù),有的話,創(chuàng)建setup上下文并掛載組件實例 // 例如:setup(props) => {} const setupContext = (instance.setupContext = setup.length > 1 ? createSetupContext(instance) : null); // 執(zhí)行setup函數(shù) const setupResult = callWithErrorHandling(setup, instance, 0 /* SETUP_FUNCTION */, [shallowReadonly(instance.props) , setupContext]); handleSetupResult(instance, setupResult, isSSR); } else { finishComponentSetup(instance, isSSR); } }
判斷組件是否設置了setup
函數(shù):
若設置了setup
函數(shù),則執(zhí)行setup
函數(shù),并判斷其返回值的類型。若返回值類型為函數(shù)時,則設置組件實例render
的值為setupResult
,否則作為組件實例setupState
的值
function handleSetupResult(instance, setupResult, isSSR) { // 判斷setup返回值類型 if (isFunction(setupResult)) { // 返回值為函數(shù)時,則當作組件實例的render方法 instance.render = setupResult; } else if (isObject(setupResult)) { // 返回值為對象時,則當作組件實例的setupState instance.setupState = proxyRefs(setupResult); } else if (setupResult !== undefined) { warn$1(`setup() should return an object. Received: ${setupResult === null ? 'null' : typeof setupResult}`); } finishComponentSetup(instance, isSSR); }
設置組件實例的render
方法,分析finishComponentSetup
函數(shù),render
函數(shù)有三種設置方式:
若setup
返回值為函數(shù)類型,則instance.render = setupResult
若組件存在render
方法,則instance.render = component.render
若組件存在template
模板,則instance.render = compile(template)
組件實例的
render
優(yōu)化級:instance.render = setup() || component.render || compile(template)
function finishComponentSetup(instance, ...) { const Component = instance.type; // 綁定render方法到組件實例上 if (!instance.render) { if (compile && !Component.render) { const template = Component.template; if (template) { // 通過編譯器編譯template,生成render函數(shù) Component.render = compile(template, ...); } } instance.render = (Component.render || NOOP); } // support for 2.x options ... }
設置完組件后,我們可以再查看下instance
的內(nèi)容有發(fā)生什么變化:
這個時候組件實例instance
的data
、proxy
、render
、setupState
已經(jīng)綁定上了初始值。
設置并運行帶有副作用的渲染函數(shù)
const setupRenderEffect = (instance, initialVNode, container, ...) => { // 創(chuàng)建響應式的副作用函數(shù) const componentUpdateFn = () => { // 首次渲染 if (!instance.isMounted) { // 渲染組件生成子樹vnode const subTree = (instance.subTree = renderComponentRoot(instance)); patch(null, subTree, container, ...); initialVNode.el = subTree.el; instance.isMounted = true; } else { // 更新 } }; // 創(chuàng)建渲染effcet 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)); update.id = instance.uid; update(); };
接下來繼續(xù)執(zhí)行setupRenderEffect
函數(shù),首先會創(chuàng)建渲染effect
(響應式系統(tǒng)還包括其他副作用:computed effect、watch effect),并綁定副作用執(zhí)行函數(shù)到組件實例的update
屬性上(更新流程會再次觸發(fā)update
函數(shù)),并立即執(zhí)行update
函數(shù),觸發(fā)首次更新。
function renderComponentRoot(instance) { const { proxy, withProxy, render, ... } = instance; let result; try { const proxyToUse = withProxy || proxy; // 執(zhí)行實例的render方法,返回vnode,然后再標準化vnode // 執(zhí)行render方法時,會調(diào)用proxyToUse,即會觸發(fā)PublicInstanceProxyHandlers的get result = normalizeVNode(render.call(proxyToUse, proxyToUse, ...)); } return result; }
此時,renderComponentRoot
函數(shù)會執(zhí)行實例的render
方法,即setupComponent
階段綁定在實例render
方法上的函數(shù),同時標準化render
返回的vnode
并返回,作為子樹vnode
。
同樣我們可以打印查看一下子樹vnode
的內(nèi)容:
此時,可能有些同學開始疑惑了,為什么會有兩顆vnode
樹呢?這兩顆vnode
樹又有什么區(qū)別呢?
initialVNode
initialVNode
就是組件的vnode
,即描述整個組件對象的,組件vnode
會定義一些和組件相關的屬性:data
、props
、生命周期
等。通過渲染組件vnode
,生成子樹vnode
。
sub tree
子樹vnode
是通過組件vnode
的render
方法生成的,其實也就是對組件模板template
的描述,即真正要渲染到瀏覽器的DOM vnode
。
生成subTree
后,接下來就繼續(xù)通過patch
方法,把subTree
節(jié)點掛載到container
上。
接下來,我們繼續(xù)往下分析,大家可以看下上面subTree
的截圖:subTree
的type
值為Fragment
,回憶下patch
方法的實現(xiàn):
const patch = ( n1, // 舊的vnode n2, // 新的vnode container, // 掛載的容器 ... ) => { const { type, ref, shapeFlag } = n2 switch (type) { case Fragment: // fragment節(jié)點 processFragment(n1, n2, container, ...) break default: // ... } }
Fragment
也就是vue3
提到的新特性之一,在vue2
中,是不支持多根節(jié)點組件,而vue3
則是正式支持的。細想一下,其實還是單個根節(jié)點組件,只是vue3
的底層用Fragment
包裹了一層。我們再看下processFragment
的實現(xiàn):
const processFragment = (n1, n2, container, ...) => { // 創(chuàng)建碎片開始、結束的文本節(jié)點 const fragmentStartAnchor = (n2.el = n1 ? n1.el : hostCreateText('')); const fragmentEndAnchor = (n2.anchor = n1 ? n1.anchor : hostCreateText('')); if (n1 == null) { hostInsert(fragmentStartAnchor, container, anchor); hostInsert(fragmentEndAnchor, container, anchor); // 掛載子節(jié)點數(shù)組 mountChildren(n2.children, container, ...); } else { // 更新 } };
接下來繼續(xù)掛載子節(jié)點數(shù)組:
const mountChildren = (children, container, ...) => { for (let i = start; i < children.length; i++) { const child = (children[i] = optimized ? cloneIfMounted(children[i]) : normalizeVNode(children[i])); patch(null, child, container, ...); } };
遍歷子節(jié)點,patch
每個子節(jié)點,根據(jù)child
節(jié)點的type
遞歸處理。接下來,我們主要看下type
為ELEMENT
類型的DOM
元素,即processElement
:
const processElement = (n1, n2, container, ...) => { if (n1 == null) { // 掛載DOM元素 mountElement(n2, container,...) } else { // 更新 } }
const mountElement = (vnode, container, ...) => { let el; let vnodeHook; const { type, props, shapeFlag, ... } = vnode; { // 創(chuàng)建DOM節(jié)點,并綁定到當前vnode的el上 el = vnode.el = hostCreateElement(vnode.type, ...); } // 插入父級節(jié)點 hostInsert(el, container, anchor); };
創(chuàng)建DOM
節(jié)點,并掛載到vnode.el
上,然后把DOM
節(jié)點掛載到container
中,繼續(xù)遞歸其他vnode
的處理,最后掛載整個vnode
到瀏覽器視圖中,至此完成vue3
的首次渲染整個流程。mountElement
方法中提到到hostCreateElement
、hostInsert
也就是在最開始創(chuàng)建渲染器時傳入的參數(shù)對應的處理方法,也就完成整個跨平臺的初次渲染流程。
更新流程
分析完vue3
首次渲染的整個流程后,那么在數(shù)據(jù)更新后,vue3
又是怎么更新渲染呢?接下來分析更新流程階段就要涉及到vue3
的響應式系統(tǒng)的知識了(由于篇幅有限,我們不會展開更多響應式的知識,期待后續(xù)篇章的更加詳細的分析)。
依賴收集
回憶下在首次渲染時的設置組件實例setupComponent
階段會創(chuàng)建渲染上下文代理,而在生成subTree
階段,會通過renderComponentRoot
函數(shù),執(zhí)行組件vnode
的render
方法,同時會觸發(fā)渲染上下文代理的PublicInstanceProxyHandlers
的get
,從而實現(xiàn)依賴收集。
function setupStatefulComponent(instance, isSSR) { ... // 創(chuàng)建渲染上下文代理 instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers)); }
function renderComponentRoot(instance) { const proxyToUse = withProxy || proxy; // 執(zhí)行render方法時,會調(diào)用proxyToUse,即會觸發(fā)PublicInstanceProxyHandlers的get result = normalizeVNode( render.call(proxyToUse, proxyToUse, ...) ); return result; }
我們可以查看下此時組件vnode
的render
方法的內(nèi)容:
或者打印查看render
方法內(nèi)容:
(function anonymous( ) { const _Vue = Vue const { createVNode: _createVNode, createElementVNode: _createElementVNode } = _Vue const _hoisted_1 = /*#__PURE__*/_createElementVNode("div", null, "static node", -1 /* HOISTED */) const _hoisted_2 = ["onClick"] return function render(_ctx, _cache) { with (_ctx) { const { createElementVNode: _createElementVNode, toDisplayString: _toDisplayString, resolveComponent: _resolveComponent, createVNode: _createVNode, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue const _component_item = _resolveComponent("item") return (_openBlock(), _createElementBlock(_Fragment, null, [ _hoisted_1, _createElementVNode("div", null, _toDisplayString(title), 1 /* TEXT */), _createElementVNode("button", { onClick: add }, "click", 8 /* PROPS */, _hoisted_2), _createVNode(_component_item, { msg: title }, null, 8 /* PROPS */, ["msg"]) ], 64 /* STABLE_FRAGMENT */)) } } })
仔細觀察render
的第一個參數(shù)_ctx
,即傳入的渲染上下文代理proxy
,當訪問title
字段時,就會觸發(fā)PublicInstanceProxyHandlers
的get
方法,那PublicInstanceProxyHandlers
的邏輯又是怎么呢?
// 代理渲染上下文的handler實現(xiàn) const PublicInstanceProxyHandlers = { get({ _: instance }, key) { const { ctx, setupState, data, props, accessCache, type, appContext } = instance; let normalizedProps; // key值不以$開頭的屬性 if (key[0] !== '$') { // 優(yōu)先從緩存中判斷當前屬性需要從哪里獲取 // 性能優(yōu)化:緩存屬性應該根據(jù)哪種類型獲取,避免每次都觸發(fā)hasOwn的開銷 const n = accessCache[key]; if (n !== undefined) { switch (n) { case 0 /* SETUP */: return setupState[key]; case 1 /* DATA */: return data[key]; case 3 /* CONTEXT */: return ctx[key]; case 2 /* PROPS */: return props[key]; // default: just fallthrough } } // 獲取屬性值的順序:setupState => data => props => ctx => 取值失敗 else if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) { accessCache[key] = 0 /* SETUP */; return setupState[key]; } else if (data !== EMPTY_OBJ && hasOwn(data, key)) { accessCache[key] = 1 /* DATA */; return data[key]; } else if ( (normalizedProps = instance.propsOptions[0]) && hasOwn(normalizedProps, key)) { accessCache[key] = 2 /* PROPS */; return props[key]; } else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) { accessCache[key] = 3 /* CONTEXT */; return ctx[key]; } else if (shouldCacheAccess) { accessCache[key] = 4 /* OTHER */; } } }, set() {}, has() {} };
接下來我們以key
為title
的例子簡單介紹下get
的邏輯:
首先判斷key
值是否已$
開頭,明顯title
走否的邏輯
再看accessCache
緩存中是否存在
性能優(yōu)化:緩存屬性應該根據(jù)哪種類型獲取,避免每次都觸發(fā)**hasOwn**
的開銷
最后再按照順序獲?。?code>setupState => data => props => ctxPublicInstanceProxyHandlers
的set
和has
的處理邏輯,同樣以這個順序處理
若存在時,先設置緩存accessCache
,再從setupState
中獲取title
對應的值
重點來了,當訪問setupState.title
時,觸發(fā)proxy
的get
的流程會有兩個階段:
首先觸發(fā)setupState
對應的proxy
的get
,然后獲取title
的值,判斷其是否為Ref
?
是:繼續(xù)獲取ref.value
,即觸發(fā)ref
類型的依賴收集流程
否:直接返回,即為普通數(shù)據(jù)類型,不進行依賴收集
// 設置組件實例時會設置setupState的代理prxoy // 設置流程:setupComponent=>setupStatefulComponent=>handleSetupResult instance.setupState = proxyRefs(setupResult) export function proxyRefs(objectWithRefs) { return isReactive(objectWithRefs) ? objectWithRefs : new Proxy(objectWithRefs, { get: (target, key, receiver) => { return unref(Reflect.get(target, key, receiver)) }, set: (target, key, value, receiver) => {} }) } export function unref(ref) { return isRef(ref) ? ref.value : ref }
訪問ref.value
時,觸發(fā)ref
的依賴收集。那我們先來分析Vue.ref()
的實現(xiàn)邏輯又是什么呢?
// 調(diào)用Vue.ref(0),從而觸發(fā)createRef的流程 // 省略其他無關代碼 function ref(value) { return createRef(value, false) } function createRef(rawValue) { return new RefImpl(rawValue, false) } // ref的實現(xiàn) class RefImpl { constructor(value) { this._rawValue = toRaw(value) this._value = toReactive(value) } get value() { trackRefValue(this) return this._value } } function trackRefValue(ref) { if (isTracking()) { if (!ref.dep) { ref.dep = new Set() } // 添加副作用,進行依賴收集 dep.add(activeEffect) activeEffect.deps.push(dep) } }
分析ref
的實現(xiàn),會發(fā)現(xiàn)當訪問ref.value
時,會觸發(fā)RefImpl
實例的value
方法,從而觸發(fā)trackRefValue
,進行依賴收集dep.add(activeEffect)
。那這時的activeEffect
又是誰呢?
回憶下setupRenderEffect
階段的實現(xiàn):
const setupRenderEffect = (instance, initialVNode, container, ...) => { // 創(chuàng)建響應式的副作用函數(shù) const componentUpdateFn = () => {}; // 創(chuàng)建渲染effcet const effect = new ReactiveEffect( componentUpdateFn, () => queueJob(instance.update), instance.scope ); const update = (instance.update = effect.run.bind(effect)); update(); }; // 創(chuàng)建effect類的實現(xiàn) class ReactiveEffect { run() { try { effectStack.push((activeEffect = this)) // ... return this.fn() } finally {} } }
當執(zhí)行update
函數(shù)時(即渲染effect
實例的run
方法),從而設置全局activeEffect
為當前渲染effect
,也就是說此時dep.add(activeEffect)
收集的activeEffect
就是這個渲染effect
,從而實現(xiàn)了依賴收集。
我們可以打印一下setupState
的內(nèi)容,驗證一下我們的分析:
通過截圖,我們可以看到此時title
收集的副作用就是渲染effect
,細心的同學就發(fā)現(xiàn)了截圖中的fn
方法就是componentUpdateFn
函數(shù),執(zhí)行fn()
繼續(xù)掛載children
。
派發(fā)更新
分析完依賴收集階段,我們再看下,vue3
又是如何進行派發(fā)更新呢?
當我們點擊按鈕執(zhí)行this.title += 1
時,同樣會觸發(fā)PublicInstanceProxyHandlers
的set
方法,而set
的觸發(fā)順序同樣和get
一致:setupState
=>data
=>其他不允許修改的判斷(例如:props
、$開頭的保留字段
)
// 代理渲染上下文的handler實現(xiàn) const PublicInstanceProxyHandlers = { set({ _: instance }, key, value) { const { data, setupState, ctx } = instance; // 1. 更新setupState的屬性值 if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) { setupState[key] = value; } // 2. 更新data的屬性值 else if (data !== EMPTY_OBJ && hasOwn(data, key)) { data[key] = value; } // ... return true; } };
設置setupState[key]
從而繼續(xù)觸發(fā)setupState
的set
方法:
const shallowUnwrapHandlers: ProxyHandler<any> = { set: (target, key, value, receiver) => { const oldValue = target[key] // oldValue為ref類型&value不是ref時執(zhí)行 if (isRef(oldValue) && !isRef(value)) { oldValue.value = value return true } else { // 否則,直接返回 return Reflect.set(target, key, value, receiver) } } }
當設置oldValue.value
的值時繼續(xù)觸發(fā)ref
的set
方法,判斷ref
是否存在dep
,執(zhí)行副作用effect.run()
,從而派發(fā)更新,完成更新流程。
class RefImpl{ set value(newVal) { newVal = this._shallow ? newVal : toRaw(newVal) if (hasChanged(newVal, this._rawValue)) { this._rawValue = newVal this._value = this._shallow ? newVal : toReactive(newVal) triggerRefValue(this, newVal) } } } // 判斷ref是否存在依賴,從而派發(fā)更新 function triggerRefValue(ref) { ref = toRaw(ref) if (ref.dep) { triggerEffects(ref.dep) } } // 派發(fā)更新 function triggerEffects(dep) { for (const effect of isArray(dep) ? dep : [...dep]) { if (effect !== activeEffect || effect.allowRecurse) { // 執(zhí)行副作用 effect.run() } } }
總結
綜上,我們分析完了vue3
的整個渲染過程和更新流程,當然我們只是從主要的渲染流程分析,完整的渲染過程的復雜度不止于此,比如基于block tree
的優(yōu)化實現(xiàn),patch
階段的diff
優(yōu)化以及在更新流程中的響應式階段的優(yōu)化又是怎樣的等細節(jié)。
本文的初衷便是給大家提供分析vue3整個渲染過程的輪廓,有了整體的印象,再去分析了解更加細節(jié)的點的時候,也會更有思路和方向。
最后,附一張完整的渲染流程圖,與君共享。
感謝各位的閱讀!關于“vue3中渲染系統(tǒng)的示例分析”這篇文章就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,讓大家可以學到更多知識,如果覺得文章不錯,可以把它分享出去讓更多的人看到吧!
免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內(nèi)容。