日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 前端技术 > vue >内容正文

vue

千层套路 - Vue 3.0 初始化源码探秘

發布時間:2023/12/9 vue 40 豆豆
生活随笔 收集整理的這篇文章主要介紹了 千层套路 - Vue 3.0 初始化源码探秘 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

關注若川視野, 回復"pdf" 領取資料,回復"1",可加群長期交流學習

劉崇楨,微醫云服務團隊前端工程師,左手抱娃、右手持家的非典型碼農。

9 月初 Vue.js 3.0 正式發布,代號 "One Piece"。大秘寶都擺到眼巴前了,再不扒拉扒拉就說不過去了。那我們就從初始化開始。

目標:

  • 弄清楚 createApp(App).mount("#app") 到底做了什么

  • 弄清楚 Vue3.0 的初始化渲染是怎么樣的過程

能收獲到什么:

  • 了解 Vue3.0 的初始化過程

  • 介紹一個閱讀 Vue3.0 源碼的入口和方向

先跑起來

將 vue-next 代碼克隆到本地,打開 package.json 將 scripts dev 末尾加上 --sourcemap。

然后 ?yarn dev,vue 目錄下的 ?dist ?打包出了一份 ?vue.global.js 和相應的 sourcemap 文件。這樣方便我們一步一步調試代碼,查看程序在 call Stack 中的每一步調用。

查看 vue 官方給出的 demo,發現 vue 的使用分為 classic 和 composition,我們先用 classic 方式,實現一個最簡單的 demo。

const app = {data () {return {counter: 1}} } Vue.createApp(app).mount("#app")

ok,頁面跑起來了。我們就在這段代碼打個斷點,然后一步一步的調試,觀察createApp(App).mount("#app")到底做了什么,了解Vue3.0的初始化過程。

在這之前,簡單了解一下整體的背景,我們這次主要涉及到 runtime 運行時的代碼。

runtime-dom

我們先跟著代碼進入:createApp(App).mount("#app");

這個 createApp() 來自 runtime-dom,我們通過這個圖可以看到他大致做的事情:return 了一個注冊了 mount 方法 app。這樣我們的 demo 至少能跑起來不報錯。

createApp 調用了 ensureRenderer 方法,他確保你能得到一個 renderer 渲染器。renderer 是通過調用創建渲染器的 createRenderer 來生成的,這個 createRenderer 來自于 runtime-core,后面我們會看到。

而這個 rendererOptions 是什么呢?

const?rendererOptions?=?extend({?patchProp,?forcePatchProp?},?nodeOps);export?const?nodeOps:?Omit<RendererOptions<Node,?Element>,?"patchProp">?=?{insert:?(child,?parent,?anchor)?=>?{parent.insertBefore(child,?anchor?||?null);},remove,createElement,createText,//?... };

是不是就是一些 DOM API 的高階封裝,這個在 vue 的生態中,叫平臺特性。vue 源碼中的平臺特性就是針對 web 平臺的。如果開發者想要在別的平臺上運行 vue,比如 mpvue、weex,就不需要 fork 源碼庫改源碼了,直接把 nodeOps 中的方法按著平臺的特性逐一實現就可以了。這也是 createRenderer 等跨平臺的代碼放到 runtime-core 中的原因。

當然 runtime-dom 遠遠不只圖中這些東西,我們先大致過一下初始化過程,以對 vue3.0 有一個大致的了解。

runtime-core

緊接著,進入 runtime-core,創建渲染器

我們注意 baseCreateRenderer 這個 fn,2000 多行的代碼量,里面的東西都是渲染的核心代碼,從平臺特性 options 取出相關 API,實現了 patch、處理節點、處理組件、更新組件、安裝組件實例等等方法,最終返回了一個對象。這里我們看到了【2】中渲染器調用的 createApp 方法,他是通過 createAppAPI 創建的。代碼進入 createAppAPI。

這里我們又看見了熟悉的 Vue2.x 中的 API,掛載在 app 上面。

至此,Vue.createApp(app).mount("#app"),創建 app 實例的流程,終于在【7】中 return app 告一段落,我們拿到了【2】中的 app 實例。

大致瞄一眼 app ,我們可以在 apiCreateApp.ts 中找到其實現

初次渲染 .mount("#app")

上面的介紹中,其實有兩處 .mount 的實現,一處是在 runtime-dom【2】中的 mount,我們叫他 dom-mount。一處是【7】中的 mount,我們叫他 core-mount。

dom-mount的實現:

const?{?mount?}?=?app;?//?先暫存'core-mount' app.mount?=?(containerOrSelector:?Element?|?string):?any?=>?{const?container?=?normalizeContainer(containerOrSelector);?//?#app?dom?節點if?(!container)?return;const?component?=?app._component;if?(!isFunction(component)?&&?!component.render?&&?!component.template)?{component.template?=?container.innerHTML;?//?平臺特性的邏輯}//?clear?content?before?mountingcontainer.innerHTML?=?"";const?proxy?=?mount(container);?//?執行'core-mount'container.removeAttribute("v-cloak");return?proxy; };

dom-mount 并不是重寫 core-mount,而是提取了平臺特性的邏輯。比如上面如果 component 不是 function,又沒有 render、template,就讀取 dom 節點內部的 html 作為渲染模板。

然后再執行 core-mount,mount(container)。

代碼很簡單,就兩步:

  • 創建根組件的 vnode

  • 渲染這個 vnode

創建根組件的vnode

創建 vnode,是一個初始化 vnode 的過程,這個階段中,下面的這些屬性被初始化為具體的值(還有很多屬性沒有羅列,都是初始值)。

當 vnode 描述不同的事物時,他的屬性值也各不相同,這些在 vnode 初始化階段確定的屬性在渲染組件時,能帶來非常重要的效率提升。

  • type,標識 VNode 的種類

  • html 標簽的描述,type 屬性就是一個字符串,即標簽的名字

  • 組件的描述,type 屬性就是引用組件類(或函數)本身

  • 文本節點的描述,type 屬性就是 null

    • patchFlag,標識組件變化的地方

    • shapeFlag,VNode 的標識,標明 VNode 屬于哪一類,demo 中的shapeFlag 是 4:STATEFUL_COMPONENT,有狀態的組件。

    在packages/shared/src/shapeFlags.ts中,定義了這些通過將十進制數字 1 左移不同的位數得來的枚舉值。

    export?const?enum?ShapeFlags?{ELEMENT?=?1,?//?1?-?html/svg?標簽FUNCTIONAL_COMPONENT?=?1?<<?1,?//?2?-?函數式組件STATEFUL_COMPONENT?=?1?<<?2,?//?4?-?有狀態組件TEXT_CHILDREN?=?1?<<?3,?//?8ARRAY_CHILDREN?=?1?<<?4,?//?16SLOTS_CHILDREN?=?1?<<?5,?//?32TELEPORT?=?1?<<?6,?//?64SUSPENSE?=?1?<<?7,?//?128COMPONENT_SHOULD_KEEP_ALIVE?=?1?<<?8,?//?256?-?需要被?keepAlive?的有狀態組件COMPONENT_KEPT_ALIVE?=?1?<<?9,?//?512?-?已經被?keepAlive?的有狀態組件COMPONENT?=?ShapeFlags.STATEFUL_COMPONENT?|?ShapeFlags.FUNCTIONAL_COMPONENT?//?組件 }

    為什么為 VNode 標識這些枚舉值呢?在 Vue2.x 的 patch 過程中,代碼通過 createElm 區分 VNode 是 html 還是組件或者 text 文本。

    所以 Vue2.x 的 patch 是一個試錯過程,在這個階段是有很大的性能損耗的。Vue3.0 把對 VNode 的判斷放到了創建的時候,這樣在 patch 的時候就能避免消耗性能的判斷。

    最終,我們看一下 vnode 的結構

    export?interface?VNode<HostNode?=?RendererNode,HostElement?=?RendererElement,ExtraProps?=?{?[key:?string]:?any?} >?{/***?@internal*/__v_isVNode:?true?//?一個始終為?true?的值,有了它,我們就可以判斷一個對象是否是?VNode?對象/***?@internal?內部屬性*/[ReactiveFlags.SKIP]:?truetype:?VNodeTypesprops:?(VNodeProps?&?ExtraProps)?|?nullkey:?string?|?number?|?nullref:?VNodeNormalizedRef?|?nullscopeId:?string?|?null?//?SFC?onlychildren:?VNodeNormalizedChildrencomponent:?ComponentInternalInstance?|?nulldirs:?DirectiveBinding[]?|?nulltransition:?TransitionHooks<HostElement>?|?null//?DOM?相關el:?HostNode?|?nullanchor:?HostNode?|?null?//?fragment?anchortarget:?HostElement?|?null?//?teleport?targettargetAnchor:?HostNode?|?null?//?teleport?target?anchorstaticCount:?number?//?number?of?elements?contained?in?a?static?vnode//?suspense?支持?suspense?的屬性suspense:?SuspenseBoundary?|?nullssContent:?VNode?|?nullssFallback:?VNode?|?null//?optimization?only?優化模式中使用的屬性shapeFlag:?numberpatchFlag:?numberdynamicProps:?string[]?|?nulldynamicChildren:?VNode[]?|?null//?application?root?node?onlyappContext:?AppContext?|?null }

    渲染這個vnode

    ok,書接上回,我們拿到 根組件的 VNode,接下來執行到 render 函數。

    render 的核心邏輯就是 patch 函數。

    patch 函數

    patch 有兩種含義: 1)整個虛擬 dom 映射到真實 dom 的過程;2)patch 函數。我們這里講的是函數。

    patch 就是 render 渲染組件的關鍵邏輯,【5】中 baseCreateRenderer 2000 行左右的代碼,主要是為了 patch 服務的。

    //?patching?&?not?same?type,?unmount?old?tree if?(n1?&&?!isSameVNodeType(n1,?n2))?{anchor?=?getNextHostNode(n1)unmount(n1,?parentComponent,?parentSuspense,?true)n1?=?null } //?對于前后節點類型不同的,vue 是直接卸載之前的然后重新渲染新的,不會考慮可能的子節點復用。 ...const?{?type,?ref,?shapeFlag?}?=?n2 switch?(type)?{?//?根據節點類型?type?分發到不同的?processcase?Text:processText(n1,?n2,?container,?anchor)breakcase?Comment:processCommentNode(n1,?n2,?container,?anchor)breakcase?Static:...case?Fragment:?...default:?//?根據不同的節點標識?shapeFlag?分發到不同的?processif?(shapeFlag?&?ShapeFlags.ELEMENT)?{?processElement(...)?}?else?if?(shapeFlag?&?ShapeFlags.COMPONENT)?{processComponent(...)...

    patch 根據節點 VNode(4.1 創建的根組件的 vnode) 的 type 和 shapeFlags 執行不同的 process。

  • type:Text 文本

  • type:Comment 注釋

  • type:Static 靜態標簽

  • type:Fragment 片段:VNode 的類型是 Fragment,就只需要把該 VNode 的子節點渲染到頁面。有了他,就沒有只能有一個根節點的限制,也可以做到組件平級遞歸

  • shapeFlags:ShapeFlags.ELEMENT 原生節點,html/svg 標簽

  • shapeFlags:ShapeFlags.COMPONENT 組件節點

  • shapeFlags:ShapeFlags.TELEPORT 傳送節點,將組件渲染的內容傳送到制定的 dom 節點中

  • shapeFlags:ShapeFlags.SUSPENSE 掛起節點(異步渲染)

  • Vue3 新增組件 - Fragment、Teleport、Suspense,可見此鏈接 (https://www.yuque.com/hugsun/vue3/component)

    我們的 demo 中的根組件 VNode 的 shapeFlag 是 4(0100),ShapeFlags.COMPONENT(0110),按位與后結果為非零,代碼會進入 processCompoent。

    processXXX

    processXXX 是對掛載(mount)和更新(update)補丁的統一操作入口。

    processXXX 會根據節點是否是初次渲染,進行不同的操作。

    • 如果沒有老的 VNode,就掛載組件(mount)。首次掛載,遞歸創建真實節點。

    • 如果有老的 VNode,就更新組件(update)。更新補丁的的渲染系統的介紹放到下下篇來介紹。

    掛載

    創建組件內部實例

    內部實例也會暴露一些實例屬性給其他更高級的庫或工具使用。組件實例屬性很多很重要也能幫助理解,可以在 packages/runtime-core/src/component.ts 查看實例的接口聲明 ComponentInternalInstance。很壯觀啊,啪的一下 100 多行屬性的定義,主要包括基本屬性、響應式 state 相關、suspense 相關、生命周期鉤子等等

    安裝組件實例
  • 初始化 props 和 slots

  • 安裝有狀態的組件,這里會初始化組件的響應式

  • 【15】setupStatefulComponent,調用了 setup(props, setupContext)。

    如果沒有 setup 時會調用 applyOptions,應用 vue2.x 的 options API,最終對 data() 的響應式處理也是使用 vue3.0 的 reactive。

    上面講過,安裝組件實例觸發響應式初始化就發生在這里,具體怎么觸發的,這塊又是一個千層套路,放到下一篇中。

    【16】主要是根據 template 拿到組件的 render 渲染函數和應用 vue2.x 的 options API。

    我們看一下 template 模板編譯后生成的 render 函數。

    我們大致看下生成的 render 函數,有幾點需要注意

  • 這里的 render 函數執行后的返回是組件的 VNode

  • _createVNode 函數,用于創建 VNode

  • _createVNode函數的入參,type、patchFlags、dynamicProps等

  • function?_createVNode(type:?VNodeTypes?|?ClassComponent?|?typeof?NULL_DYNAMIC_COMPONENT,?//?type,標識?VNode?的種類props:?(Data?&?VNodeProps)?|?null?=?null,children:?unknown?=?null,patchFlag:?number?=?0,?//?標記節點動態變化的地方dynamicProps:?string[]?|?null?=?null,?//?動態?propsisBlockNode?=?false ):?VNode?{?...?}

    createVNode 在創建根節點的時候就出現過,用于創建虛擬 DOM。這個是內部使用的 API,面向用戶的 API 還是h函數。

    export?function?h(type:?any,?propsOrChildren?:?any,?children?:?any):?VNode?{?...?}

    h 的實現也是調用 createVNode,但是沒有 patchFlag、dynamicProps、isBlockNode 這三個參數。也就是 h 是沒有 optimization 的,應該是因為這三個參數,讓用戶自己算容易出錯。

    看來這個 patchFlags 有點意思,標識組件變化的地方,用于 patch 的 diff 算法優化。

    export?const?enum?PatchFlags?{TEXT?=?1,?//?動態文字內容CLASS?=?1?<<?1,?//?[2]動態?class?綁定STYLE?=?1?<<?2,?//?[4]動態樣式PROPS?=?1?<<?3,?//?[8]動態?props,不是?class?和?style?的動態?propsFULL_PROPS?=?1?<<?4,?//?[16]有動態的 key,也就是說 props 對象的 key 不是確定的。key 變化時,進行一次 full diffHYDRATE_EVENTS?=?1?<<?5,?//?[32]STABLE_FRAGMENT?=?1?<<?6,?//?[64]children?順序確定的?fragmentKEYED_FRAGMENT?=?1?<<?7,?//?[128]children?中有帶有?key?的節點的?fragmentUNKEYED_FRAGMENT?=?1?<<?8,?//?[256]沒有?key?的?children?的?fragmentNEED_PATCH?=?1?<<?9,?//?[512]DYNAMIC_SLOTS?=?1?<<?10,?//?[1024]動態的插槽//?SPECIAL?FLAGS?-------------------------------------------------------------//?以下是特殊的?flag,負值HOISTED?=?-1,?//?表示他是靜態節點,他的內容永遠不會改變BAIL?=?-2,?//?用來表示一個節點的?diff?應該結束 }

    之所以使用位運算,是因為

    • 用 | 來進行復合,TEXT | PROPS得到0000 1001,即十進制 9。標識他既有動態文字內容,也有動態 props。

    • 用 & 進行 check,patchFlag & TEXT,0000 1001 & 0000 0001,得到0000 0001,只要結果大于 0,就說明屬性命中。

    • 方便擴展、計算更快...

    patchFlag 被賦值到 VNode 的屬性中,他在后面更新節點時會被用到。為了配合代碼的正常流轉,先放一放,代碼繼續 F10。如果你去調試代碼,會發現這真的是千層套路啊,一直 shift + F11 跳出代碼到懷疑人生,才終于回到 mountComponent...

    總結一下 setupComponent 安裝組件實例,主要做了什么事情:initProps、initSlots、響應式初始化、得到模板的 render 函數等等。

    回顧前文,跳出到【13】,setup 安裝組件實例后,下一步是 setupRenderEffect 激活渲染函數的副作用

    激活渲染函數的副作用 setupRenderEffect

    實現基于【21】,effect 副作用,意味著響應式數據變化后引起的變更。effect 源自 reactive,傳入一個 fn 得到一個 reactiveEffect。

    effect 的入參 componentEffect 是一個命名函數,會立即執行。componentEffect 執行過程中,觸發響應式數據的 getter 攔截,會在全局數據響應關系倉庫記錄當前componentEffect。在響應式對象發生改變時,派發更新,執行componentEffect。

    回到componentEffect

    function?componentEffect()?{if?(!instance.isMounted)?{let?vnodeHook:?VNodeHook?|?null?|?undefinedconst?{?el,?props?}?=?initialVNodeconst?{?bm,?m,?parent?}?=?instance//?beforeMount?hook?生命周期鉤子函數if?(bm)?{invokeArrayFns(bm)}...//?subTree?根節點的?subTree,通過?renderComponentRoot?根據?render?生成的?vnode//大家回憶一下 render 是什么?是不是根組件的 template 編譯后得到的好多_createVNode 的渲染器函數?const?subTree?=?(instance.subTree?=?renderComponentRoot(instance))...//?更新patch(null,?subTree,?container,?...)...if?(m)?{?//?parent?的?mounted?執行之前,先執行?subTree?的?patchqueuePostRenderEffect(m,?parentSuspense)}...instance.isMounted?=?true?//?標志實例已掛載}?else?{?...?} }

    執行前面編譯后得到的渲染函數 render,生成subTree: vnode

    最后執行 patch,上文中渲染根節點的 vnode 時執行過 patch,這里就進入了一個大循環,根據組件的 children 的 type 和 shapeFlag,baseCreateRenderer 會繼續進行各種 processXXX 處理,直至基于 平臺特性 的 DOM 操作 掛載到各自的父節點中。

    這個順序是深度遍歷的過程,子節點的 patch 完成之后再進行父節點的 mounted。

    patch 循環 && subTree 一覽

    //?subTree?的?模板?template <div?id="app"><h1>composition-api</h1><p?@click="add"?:attr-key="counter">{{counter}}</p><p?:class="{'counter':?counter?%?2}">{{doubleCounter}}</p> </div>//?patchFlag:?64? // STABLE_FRAGMENT = 1 << 6, // 64 表示:children 順序確定的 fragment //?shapeFlag:?16 //?ARRAY_CHILDREN?=?1?<<?4,?//?16?
  • 觀察上面這個模板,Vue2.x 中的模板只能有一個根元素,Vue3.0 的這個 demo 中有三個根元素,這得益于新增的 fragment 組件。

  • vnode 標識出來 patchFlag:64,表示 children 順序確定的 fragment;

  • vnode 標識出來 shapeFlag:16,表示當前節點是一個孩子數組。

  • vnode 標識出來 dynamicChildren,標識動態變化的孩子節點。顯然是兩個 p 標簽,可以想象這個數組的元素也是當前呈現的 vnode,只不過具體屬性值不同罷了

  • 等等,還有 4 嗎,我不知道...

    當然還有,processxxx 中一般都會判斷是掛載還是更新,更新的時候就會用到 patchFlag,比如 patchElement... 下次一定

    等等,還有 5 嗎,我不知道...

    當然還有,第五層我就已經裂開了啊...

    あ:あげない??????あ:不給你哦~????????????? い:いらない,????い:不要了啦~????????????? う:うごけない????う:動不了了~????????????? え:えらべない????え:不會選嘛~????????????? お:おせない??????お:按不到耶~?[裂開][裂開][裂開]

    剛看源碼不久,只能靠 F11 、參考其他文檔,憑自己的理解寫出這樣的文章,肯定有很多理解不對的地方,希望得到批判指正。

    附錄

    • Vue3初始化.drawio (https://www.yuque.com/office/yuque/0/2020/drawio/441847/1605880555730-4e18923f-c087-4082-af06-ec51986ba658.drawio?from=https%3A%2F%2Fwww.yuque.com%2Fdocs%2Fshare%2F64bd5cdc-3086-4154-a447-04032d161830%3F%23)

    推薦閱讀

    我在阿里招前端,我該怎么幫你?(現在還可以加模擬面試群)
    如何拿下阿里巴巴 P6 的前端 Offer
    如何準備阿里P6/P7前端面試--項目經歷準備篇
    大廠面試官常問的亮點,該如何做出?
    如何從初級到專家(P4-P7)打破成長瓶頸和有效突破
    若川知乎問答:2年前端經驗,做的項目沒什么技術含量,怎么辦?
    若川知乎高贊:有哪些必看的 JS庫?

    末尾

    你好,我是若川,江湖人稱菜如若川,歷時一年只寫了一個學習源碼整體架構系列~(點擊藍字了解我)

  • 關注若川視野,回復"pdf" 領取優質前端書籍pdf,回復"1",可加群長期交流學習

  • 我的博客地址:https://lxchuan12.gitee.io?歡迎收藏

  • 覺得文章不錯,可以點個在看呀^_^另外歡迎留言交流~

  • 小提醒:若川視野公眾號面試、源碼等文章合集在菜單欄中間【源碼精選】按鈕,歡迎點擊閱讀,也可以星標我的公眾號,便于查找

    總結

    以上是生活随笔為你收集整理的千层套路 - Vue 3.0 初始化源码探秘的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。