您好,登錄后才能下訂單哦!
這篇文章給大家分享的是有關vue3中渲染系統的示例分析的內容。小編覺得挺實用的,因此分享給大家做個參考,一起跟隨小編過來看看吧。
思考
在開始今天的文章之前,大家可以想一下:
vue
文件是如何轉換成DOM
節點,并渲染到瀏覽器上的?
數據更新時,整個的更新流程又是怎么樣的?
vuejs
有兩個階段:編譯時和運行時。
編譯時
我們平常開發時寫的.vue
文件是無法直接運行在瀏覽器中的,所以在webpack
編譯階段,需要通過vue-loader
將.vue
文件編譯生成對應的js
代碼,vue
組件對應的template
模板會被編譯器轉化為render函數
。
運行時
接下來,當編譯后的代碼真正運行在瀏覽器時,便會執行render函數
并返回VNode
,也就是所謂的虛擬DOM
,最后將VNode
渲染成真實的DOM節點
。
了解完vue
組件渲染的思路后,接下來讓我們從Vue.js 3.0(后續簡稱vue3
)的源碼出發,深入了解vue
組件的整個渲染流程是怎么樣的?
準備
本文主要是分析
vue3
的渲染系統,為了方便調試,我們直接通過引入vue.js
文件的方式進行源碼調試分析。
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
啟動開發環境
npm run serve
測試代碼
<!-- 調試代碼目錄:/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>
創建應用
從上面的測試代碼,我們會發現vue3
和vue2
的掛載方式是不同的,vue3
是通過createApp
這個入口函數進行應用的創建。接下來我們來看下createApp
的具體實現:
// 入口文件: /vue-next/packages/runtime-dom/src/index.ts const createApp = ((...args) => { console.log('createApp入參:', ...args); // 創建應用 const app = ensureRenderer().createApp(...args); const { mount } = app; // 重寫mount app.mount = (containerOrSelector) => { // ... }; return app; });
ensureRenderer
首先通過ensureRenderer
創建web端的渲染器,我們來看下具體實現:
// 更新屬性的方法 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端的渲染器所需的參數設置 const rendererOptions = extend({ patchProp }, nodeOps); let renderer; // 延遲創建renderer function ensureRenderer() { return (renderer || (renderer = createRenderer(rendererOptions))); }
在這里可以看出,通過延遲創建渲染器,當我們只依賴響應式包的情況下,可以通過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 // ======================= // 渲染的核心流程 // 通過閉包緩存內斂函數 // ======================= const patch = () => {} // 核心diff過程 const processElement = () => {} // 處理element const mountElement = () => {} // 掛載element const mountChildren = () => {} // 掛載子節點 const processFragment = () => {} // 處理fragment節點 const processComponent = () => {} // 處理組件 const mountComponent = () => {} // 掛載組件 const setupRenderEffect = () => {} // 運行帶副作用的render函數 const render = () => {} // 渲染掛載流程 // ... // ======================= // 2000+行的內斂函數 // ======================= return { render, hydrate, // 服務端渲染相關 createApp: createAppAPI(render, hydrate) } }
接下來我們先跳過這些內斂函數的實現(后面的渲染流程用到時,我們再具體分析),來看下createAppAPI
的具體實現:
createAppAPI
function createAppAPI(render, hydrate) { // 真正創建app的入口 return function createApp(rootComponent, rootProps = null) { // 創建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
函數才是真正創建應用的入口。在createApp
里會創建vue
應用的上下文,同時初始化app
,并綁定應用上下文到app
實例上,最后返回app
。
這里有個值得注意的點:
app
對象上的use
、mixin
、component
和directive
方法都返回了app
應用實例,開發者可以鏈式調用。
// 一直use一直爽 createApp(App).use(Router).use(Vuex).component('component',{}).mount("#app")
到此app應用實例已經創建好了~,打印查看下創建的app
應用:
總結一下創建app
應用實例的過程:
創建web端
對應的渲染器(延遲創建,tree-shaking)
執行baseCreateRenderer
方法(通過閉包緩存內斂函數,后續掛載階段的主流程)
執行createAppAPI
方法(1. 創建應用上下文;2. 創建app并返回)
掛載階段
接下來,當我們執行app.mount
時,便會開始掛載組件。而我們調用的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; // 判斷如果傳入的根組件不是函數&根組件沒有render函數&沒有template,就把容器的內容設置為根組件的template if (!isFunction(component) && !component.render && !component.template) { component.template = container.innerHTML; } // 清空容器內容 container.innerHTML = ''; // 執行緩存的mount方法 const proxy = mount(container, false, container); return proxy; }; return app; });
執行完web端
重寫的mount
方法后,才是真正掛載組件的開始,即調用createAppAPI
返回的app應用
上的mount
方法:
function createAppAPI(render, hydrate) { // 真正創建app的入口 return function createApp(rootComponent, rootProps = null) { // ... const app = (context.app = { // 掛載根組件 mount(rootContainer, isHydrate, isSVG) { if (!isMounted) { // 創建根組件對應的vnode const vnode = createVNode(rootComponent, rootProps); // 根級vnode存在應用上下文 vnode.appContext = context; // 將虛擬vnode節點渲染成真實節點,并掛載 render(vnode, rootContainer, isSVG); isMounted = true; // 記錄應用的根組件容器 app._container = rootContainer; rootContainer.__vue_app__ = app; app._instance = vnode.component; return vnode.component.proxy; } } }); return app; }; }
總結一下,mount
方法主要做了什么呢?
創建根組件對應的vnode
根組件vnode
綁定應用上下文context
渲染vnode
成真實節點,并掛載
記錄掛載狀態
細心的同學可能已經發現了,這里的
mount
方法是一個標準的跨平臺渲染流程,抽象vnode
,然后通過rootContainer
實現特定平臺的渲染,例如在瀏覽器環境下,它就是一個DOM
對象,在其他平臺就是其他特定的值。這也就是為什么我們在調用runtime-dom
包的creataApp
方法時,重寫mount
方法,完善不同平臺的渲染邏輯。
創建vnode
提到
vnode
,可能更多人會和高性能聯想到一起,誤以為vnode
的性能就一定比手動操作DOM
的高,其實不然。vnode
的底層同樣是要操作DOM
,相反如果vnode
的patch
過程過長,同樣會導致頁面的卡頓。 而vnode
的提出則是對原生DOM
的抽象,在跨平臺設計的處理上會起到一定的抽象化。例如:服務端渲染、小程序端渲染、weex平臺...
接下來,我們來看下創建vnode
的過程:
function _createVNode( type, props, children, patchFlag, ... ): VNode { // 規范化class & style // 例如:class=[]、class={}、style=[]等格式,需規范化 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, // 跳過響應式數據化 type, // 創建vnode的第一個參數 props, // DOM參數 children, component: null, // 組件實例(instance),通過createComponentInstance創建 shapeFlag, // 類型標記,在patch階段,通過匹配shapeFlag進行相應的渲染過程 ... }; // 標準化子節點 if (needFullChildrenNormalization) { normalizeChildren(vnode, children); } // 收集動態子代節點或子代block到父級block tree if (isBlockTreeEnabled > 0 && !isBlockNode && currentBlock && (vnode.patchFlag > 0 || shapeFlag & 6 /* COMPONENT */) && vnode.patchFlag !== 32 /* HYDRATE_EVENTS */) { currentBlock.push(vnode); } return vnode; }
通過上面的代碼,我們可以總結一下,創建vnode
階段都做了什么:
規范化class & style(例如:class=[]、class={}、style=[]等格式)
標記vnode
的類型shapeFlag
,即根組件對應的vnode
類型(type即為根組件rootComponent
,此時根組件為對象格式,所以shapeFlag
即為4)
標準化子節點(初始化時,children為空)
收集動態子代節點或子代block
到父級block tree
(這里便是vue3
引入的新概念:block tree
,篇幅有限,本文就不展開陳述了)
這里,我們可以打印查看一下此時根組件對應的vnode
結構:
渲染vnode
通過createVNode
獲取到根組件對應的vnode
,然后執行render
方法,而這里的render
函數便是baseCreateRenderer
通過閉包緩存的render
函數:
// 實際調用的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
,則進行卸載組件
否則,正常掛載
掛載完成后,批量執行組件生命周期
綁定vnode到容器上,以便后續更新階段通過新舊vnode
進行patch
??:接下來,整個渲染過程將會在
baseCreateRenderer
這個核心函數的內斂函數中執行~
patch
接下來,我們來看下render
過程中的patch
函數的實現:
const patch = ( n1, // 舊的vnode n2, // 新的vnode container, // 掛載的容器 ... ) => { // ... 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: // 靜態節點 if (n1 == null) { mountStaticNode(n2, container, anchor, isSVG) } break case Fragment: // fragment節點 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
函數,我們會發現patch
函數會通過判斷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
,則執行掛載組件;否則更新組件。
mountComponent
接下來我們繼續看掛載組件的mountComponent
函數的實現:
const mountComponent = (initialVNode, container, ...) => { // 1. 創建組件實例 const instance = ( // 這個時候就把組件實例掛載到了組件vnode的component屬性上了 initialVNode.component = createComponentInstance(initialVNode, parentComponent, parentSuspense) ); // 2. 設置組件實例 setupComponent(instance); // 3. 設置并運行帶有副作用的渲染函數 setupRenderEffect(instance, initialVNode, container,...); };
省略掉無關主流程的代碼后,可以看到,mountComponent
函數主要做了三件事:
創建組件實例
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節點類型 parent, // 父組件的實例instance appContext, // 應用上下文 root: null, // 根實例 next: null, // 當前組件mounted時,為null,將設置為instance.vnode,下次update時,將執行updateComponentPreRender subTree: null, // 組件的渲染vnode,由組件的render函數生成,創建后同步 update: null, // 組件內容掛載或更新到視圖的執行回調,創建后同步 scope: new EffectScope(true /* detached */), render: null, // 組件的render函數,在setupStatefulComponent階段賦值 proxy: null, // 是一個proxy代理ctx字段,內部使用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
函數主要是初始化組件實例并返回,打印查看下根組件對應的instance
內容:
設置組件實例
function setupComponent(instance, isSSR = false) { const { props, children } = instance.vnode; // 判斷是否為狀態組件 const isStateful = isStatefulComponent(instance); // 初始化組件屬性、slots initProps(instance, props, isStateful, isSSR); initSlots(instance, children); // 當狀態組件時,掛載setup信息 const setupResult = isStateful ? setupStatefulComponent(instance, isSSR) : undefined; return setupResult; }
setupComponent
的邏輯也很簡單,首先初始化組件props
和slots
掛載到組件實例instance
上,然后根據組件類型vnode.shapeFlag===4
,判斷是否掛載setup
信息(也就是vue3的composition api)。
function setupStatefulComponent(instance, isSSR) { const Component = instance.type; // 創建渲染上下文的屬性訪問緩存 instance.accessCache = Object.create(null); // 創建渲染上下文代理 instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers)); const { setup } = Component; // 判斷組件是否存在setup if (setup) { // 判斷setup是否有參數,有的話,創建setup上下文并掛載組件實例 // 例如:setup(props) => {} const setupContext = (instance.setupContext = setup.length > 1 ? createSetupContext(instance) : null); // 執行setup函數 const setupResult = callWithErrorHandling(setup, instance, 0 /* SETUP_FUNCTION */, [shallowReadonly(instance.props) , setupContext]); handleSetupResult(instance, setupResult, isSSR); } else { finishComponentSetup(instance, isSSR); } }
判斷組件是否設置了setup
函數:
若設置了setup
函數,則執行setup
函數,并判斷其返回值的類型。若返回值類型為函數時,則設置組件實例render
的值為setupResult
,否則作為組件實例setupState
的值
function handleSetupResult(instance, setupResult, isSSR) { // 判斷setup返回值類型 if (isFunction(setupResult)) { // 返回值為函數時,則當作組件實例的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
函數,render
函數有三種設置方式:
若setup
返回值為函數類型,則instance.render = setupResult
若組件存在render
方法,則instance.render = component.render
若組件存在template
模板,則instance.render = compile(template)
組件實例的
render
優化級: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函數 Component.render = compile(template, ...); } } instance.render = (Component.render || NOOP); } // support for 2.x options ... }
設置完組件后,我們可以再查看下instance
的內容有發生什么變化:
這個時候組件實例instance
的data
、proxy
、render
、setupState
已經綁定上了初始值。
設置并運行帶有副作用的渲染函數
const setupRenderEffect = (instance, initialVNode, container, ...) => { // 創建響應式的副作用函數 const componentUpdateFn = () => { // 首次渲染 if (!instance.isMounted) { // 渲染組件生成子樹vnode const subTree = (instance.subTree = renderComponentRoot(instance)); patch(null, subTree, container, ...); initialVNode.el = subTree.el; instance.isMounted = true; } else { // 更新 } }; // 創建渲染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(); };
接下來繼續執行setupRenderEffect
函數,首先會創建渲染effect
(響應式系統還包括其他副作用:computed effect、watch effect),并綁定副作用執行函數到組件實例的update
屬性上(更新流程會再次觸發update
函數),并立即執行update
函數,觸發首次更新。
function renderComponentRoot(instance) { const { proxy, withProxy, render, ... } = instance; let result; try { const proxyToUse = withProxy || proxy; // 執行實例的render方法,返回vnode,然后再標準化vnode // 執行render方法時,會調用proxyToUse,即會觸發PublicInstanceProxyHandlers的get result = normalizeVNode(render.call(proxyToUse, proxyToUse, ...)); } return result; }
此時,renderComponentRoot
函數會執行實例的render
方法,即setupComponent
階段綁定在實例render
方法上的函數,同時標準化render
返回的vnode
并返回,作為子樹vnode
。
同樣我們可以打印查看一下子樹vnode
的內容:
此時,可能有些同學開始疑惑了,為什么會有兩顆vnode
樹呢?這兩顆vnode
樹又有什么區別呢?
initialVNode
initialVNode
就是組件的vnode
,即描述整個組件對象的,組件vnode
會定義一些和組件相關的屬性:data
、props
、生命周期
等。通過渲染組件vnode
,生成子樹vnode
。
sub tree
子樹vnode
是通過組件vnode
的render
方法生成的,其實也就是對組件模板template
的描述,即真正要渲染到瀏覽器的DOM vnode
。
生成subTree
后,接下來就繼續通過patch
方法,把subTree
節點掛載到container
上。
接下來,我們繼續往下分析,大家可以看下上面subTree
的截圖:subTree
的type
值為Fragment
,回憶下patch
方法的實現:
const patch = ( n1, // 舊的vnode n2, // 新的vnode container, // 掛載的容器 ... ) => { const { type, ref, shapeFlag } = n2 switch (type) { case Fragment: // fragment節點 processFragment(n1, n2, container, ...) break default: // ... } }
Fragment
也就是vue3
提到的新特性之一,在vue2
中,是不支持多根節點組件,而vue3
則是正式支持的。細想一下,其實還是單個根節點組件,只是vue3
的底層用Fragment
包裹了一層。我們再看下processFragment
的實現:
const processFragment = (n1, n2, container, ...) => { // 創建碎片開始、結束的文本節點 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); // 掛載子節點數組 mountChildren(n2.children, container, ...); } else { // 更新 } };
接下來繼續掛載子節點數組:
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, ...); } };
遍歷子節點,patch
每個子節點,根據child
節點的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; { // 創建DOM節點,并綁定到當前vnode的el上 el = vnode.el = hostCreateElement(vnode.type, ...); } // 插入父級節點 hostInsert(el, container, anchor); };
創建DOM
節點,并掛載到vnode.el
上,然后把DOM
節點掛載到container
中,繼續遞歸其他vnode
的處理,最后掛載整個vnode
到瀏覽器視圖中,至此完成vue3
的首次渲染整個流程。mountElement
方法中提到到hostCreateElement
、hostInsert
也就是在最開始創建渲染器時傳入的參數對應的處理方法,也就完成整個跨平臺的初次渲染流程。
更新流程
分析完vue3
首次渲染的整個流程后,那么在數據更新后,vue3
又是怎么更新渲染呢?接下來分析更新流程階段就要涉及到vue3
的響應式系統的知識了(由于篇幅有限,我們不會展開更多響應式的知識,期待后續篇章的更加詳細的分析)。
依賴收集
回憶下在首次渲染時的設置組件實例setupComponent
階段會創建渲染上下文代理,而在生成subTree
階段,會通過renderComponentRoot
函數,執行組件vnode
的render
方法,同時會觸發渲染上下文代理的PublicInstanceProxyHandlers
的get
,從而實現依賴收集。
function setupStatefulComponent(instance, isSSR) { ... // 創建渲染上下文代理 instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers)); }
function renderComponentRoot(instance) { const proxyToUse = withProxy || proxy; // 執行render方法時,會調用proxyToUse,即會觸發PublicInstanceProxyHandlers的get result = normalizeVNode( render.call(proxyToUse, proxyToUse, ...) ); return result; }
我們可以查看下此時組件vnode
的render
方法的內容:
或者打印查看render
方法內容:
(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
的第一個參數_ctx
,即傳入的渲染上下文代理proxy
,當訪問title
字段時,就會觸發PublicInstanceProxyHandlers
的get
方法,那PublicInstanceProxyHandlers
的邏輯又是怎么呢?
// 代理渲染上下文的handler實現 const PublicInstanceProxyHandlers = { get({ _: instance }, key) { const { ctx, setupState, data, props, accessCache, type, appContext } = instance; let normalizedProps; // key值不以$開頭的屬性 if (key[0] !== '$') { // 優先從緩存中判斷當前屬性需要從哪里獲取 // 性能優化:緩存屬性應該根據哪種類型獲取,避免每次都觸發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
緩存中是否存在
性能優化:緩存屬性應該根據哪種類型獲取,避免每次都觸發**hasOwn**
的開銷
最后再按照順序獲取:setupState => data => props => ctx
PublicInstanceProxyHandlers
的set
和has
的處理邏輯,同樣以這個順序處理
若存在時,先設置緩存accessCache
,再從setupState
中獲取title
對應的值
重點來了,當訪問setupState.title
時,觸發proxy
的get
的流程會有兩個階段:
首先觸發setupState
對應的proxy
的get
,然后獲取title
的值,判斷其是否為Ref
?
是:繼續獲取ref.value
,即觸發ref
類型的依賴收集流程
否:直接返回,即為普通數據類型,不進行依賴收集
// 設置組件實例時會設置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
時,觸發ref
的依賴收集。那我們先來分析Vue.ref()
的實現邏輯又是什么呢?
// 調用Vue.ref(0),從而觸發createRef的流程 // 省略其他無關代碼 function ref(value) { return createRef(value, false) } function createRef(rawValue) { return new RefImpl(rawValue, false) } // ref的實現 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
的實現,會發現當訪問ref.value
時,會觸發RefImpl
實例的value
方法,從而觸發trackRefValue
,進行依賴收集dep.add(activeEffect)
。那這時的activeEffect
又是誰呢?
回憶下setupRenderEffect
階段的實現:
const setupRenderEffect = (instance, initialVNode, container, ...) => { // 創建響應式的副作用函數 const componentUpdateFn = () => {}; // 創建渲染effcet const effect = new ReactiveEffect( componentUpdateFn, () => queueJob(instance.update), instance.scope ); const update = (instance.update = effect.run.bind(effect)); update(); }; // 創建effect類的實現 class ReactiveEffect { run() { try { effectStack.push((activeEffect = this)) // ... return this.fn() } finally {} } }
當執行update
函數時(即渲染effect
實例的run
方法),從而設置全局activeEffect
為當前渲染effect
,也就是說此時dep.add(activeEffect)
收集的activeEffect
就是這個渲染effect
,從而實現了依賴收集。
我們可以打印一下setupState
的內容,驗證一下我們的分析:
通過截圖,我們可以看到此時title
收集的副作用就是渲染effect
,細心的同學就發現了截圖中的fn
方法就是componentUpdateFn
函數,執行fn()
繼續掛載children
。
派發更新
分析完依賴收集階段,我們再看下,vue3
又是如何進行派發更新呢?
當我們點擊按鈕執行this.title += 1
時,同樣會觸發PublicInstanceProxyHandlers
的set
方法,而set
的觸發順序同樣和get
一致:setupState
=>data
=>其他不允許修改的判斷(例如:props
、$開頭的保留字段
)
// 代理渲染上下文的handler實現 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]
從而繼續觸發setupState
的set
方法:
const shallowUnwrapHandlers: ProxyHandler<any> = { set: (target, key, value, receiver) => { const oldValue = target[key] // oldValue為ref類型&value不是ref時執行 if (isRef(oldValue) && !isRef(value)) { oldValue.value = value return true } else { // 否則,直接返回 return Reflect.set(target, key, value, receiver) } } }
當設置oldValue.value
的值時繼續觸發ref
的set
方法,判斷ref
是否存在dep
,執行副作用effect.run()
,從而派發更新,完成更新流程。
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是否存在依賴,從而派發更新 function triggerRefValue(ref) { ref = toRaw(ref) if (ref.dep) { triggerEffects(ref.dep) } } // 派發更新 function triggerEffects(dep) { for (const effect of isArray(dep) ? dep : [...dep]) { if (effect !== activeEffect || effect.allowRecurse) { // 執行副作用 effect.run() } } }
總結
綜上,我們分析完了vue3
的整個渲染過程和更新流程,當然我們只是從主要的渲染流程分析,完整的渲染過程的復雜度不止于此,比如基于block tree
的優化實現,patch
階段的diff
優化以及在更新流程中的響應式階段的優化又是怎樣的等細節。
本文的初衷便是給大家提供分析vue3整個渲染過程的輪廓,有了整體的印象,再去分析了解更加細節的點的時候,也會更有思路和方向。
最后,附一張完整的渲染流程圖,與君共享。
感謝各位的閱讀!關于“vue3中渲染系統的示例分析”這篇文章就分享到這里了,希望以上內容可以對大家有一定的幫助,讓大家可以學到更多知識,如果覺得文章不錯,可以把它分享出去讓更多的人看到吧!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。