<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      [vue3] vue3初始化渲染流程

      組件初次渲染流程

      組件是對DOM樹的抽象,組件的外觀由template定義,模板在編譯階段會被轉化為一個渲染函數,用于在運行時生成vnode。即組件在運行時的渲染步驟是:

      graph LR A[創建vnode] --> B[渲染vnode] --> C[生成DOM]

      vnode是一個用于描述視圖的結構和屬性的JavaScript對象。vnode是對真實DOM的一層抽象。

      使用vnode的優點:

      • 相比于直接操作DOM在需要頻繁更新視圖的場景下,可以將多次操作應用在vnode上,再一次性地生成真實DOM,可以避免頻繁重排重繪導致的性能問題;
      • vnode是抽象的視圖層,具有平臺無關性,上層代碼可移植性強。

      應用程序初始化

      對于一個vue-app來說,整個組件樹由根組件開始渲染。為了找到根組件的渲染入口,從應用程序的初始化過程開始分析。

      Vue2中,初始化應用的代碼:

      import Vue from 'vue';
      import App from './App';
      
      const app = new Vue({
          render: h=>h(App)
      });
      app.$mount('#app');
      

      Vue3中,初始化應用的代碼:

      import { createApp } from 'vue';
      import App from './App';
      
      const app = createApp(App);
      app.mount('#app');
      

      對比二者的代碼可以看出,本質都是把App組件掛載到了#appDOM節點上。

      本文主要關注Vue3

      Vue3createApp的實現大致如下:

      首先,createApp函數由createAppAPI根據對應的render對象構建得到。

      export function createAppAPI<HostElement>(
        render: RootRenderFunction<HostElement>,
        hydrate?: RootHydrateFunction,
      ): CreateAppFunction<HostElement> {
        return function createApp(rootComponent, rootProps = null) {
            //...
        }
      }
      

      源碼位置:core/packages/runtime-core/src/apiCreateApp.ts at main · vuejs/core (github.com)

      render對象由baseCreateRenderer函數創建,根據不同的環境創建不同的render對象(常見的是瀏覽器環境下用來渲染DOM)。

      并由render對象來決定createApp函數的實現:

      // baseCreateRenderer函數的返回值
      return {
          render,
          hydrate,
          createApp: createAppAPI(render, hydrate),
      }
      

      這種根據不同環境構建不同render對象的操作是為了實現跨平臺。

      接下來回到createApp內部。

      createApp應用工廠模式,在內部創建app對象,實現了mount方法,mount方法就是用來掛載組件的。

      function createApp(rootComponent, rootProps = null){
          // ...
          const app: App = {
              // ...
              mount(
              	rootContainer: HostElement,
                  isHydrate?: boolean,
                  namespace?: boolean | ElementNamespace,
              ): any{
              	// mount的具體實現,這里省略了很多代碼...
                  // 1. 創建vnode
                  const vnode = createVNode(rootComponent, rootProps)
                  // 2. 渲染vnode
                  render(vnode, rootContainer, namespace)
          	}
              // ...
          }
          return app;
      }
      

      在整個app對象創建過程中,Vue3通過閉包和函數柯里化等技巧實現了參數保留。

      例如上面的mount方法內部實際上會使用render函數將vnode掛載到container上。而rendercreateAppAPI調用時傳入。這就是閉包的應用。

      graph TB A["baseCreateRenderer"] --> B["craeteAppAPI [render]"] B --> C["createApp"] C --> D["mount"] D --> |"使用render"|B

      上面提到的app對象中對mount的實現位于packages/runtime-core,也就是說是與平臺無關的,內部都是對抽象的vnoderootContainer進行操作,不一定是DOM節點。


      Vue3將瀏覽器相關的DOM的實現移到了packages/runtime-dom中,在index.ts中可以看到ensureRenderer函數就調用了runtime-core中上述提到的createRenderer方法,傳入了DOM相關的配置,用于獲取一個專門用于瀏覽器環境的renderer

      源碼位置:core/packages/runtime-dom/src/index.ts at main · vuejs/core (github.com)

      runtime-domindex.ts中,我們從createApp函數入手,觀察到它調用了ensureRenderer來獲取一個適配瀏覽器環境的renderer,并調用其對應的createApp函數。

      export const createApp = ((...args) => {
        const app = ensureRenderer().createApp(...args)
        // ......
        const { mount } = app
        // 重寫mount方法
        app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
          // 標準化容器:將字符串選擇器轉換為DOM對象
          const container = normalizeContainer(containerOrSelector)
          if (!container) return
      
          const component = app._component
          // 如果組件對象沒有定義render函數和template模板,則取容器的innerHTML作為模板內容
          if (!isFunction(component) && !component.render && !component.template) {
            // 使用innerHTML需要注意安全性問題
            component.template = container.innerHTML
            // ......
          }
      
          // 掛載前刪除容器的內容
          container.innerHTML = ''
          // 走runtime-core中實現的標準流程進行掛載
          const proxy = mount(container, false, resolveRootNamespace(container))
          // ......
          return proxy
        }
      
        return app
      }) as CreateAppFunction<Element>
      

      階段性總結

      • 重寫mount的原因:

        • runtime-core中的mount:實現標準化掛載流程;
        • runtime-dom中的mount:實現DOM節點相關的預處理,然后調用runtime-core中的mount進行掛載;
      • runtime-dommount的流程:

        1. 標準化容器:如果傳入字符串選擇器,那么調用document.querySelector將其轉換為DOM對象;

        2. 檢查組件是否存在render函數和template對象,如果沒有則使用容器的innerHTML作為模板;

          使用innerHTML需要注意安全性問題。

        3. 刪除容器原先的innerHTML內容;

        4. 調用runtime-core中實現的mount方法走標準化流程掛載組件到DOM節點上。

      app.mount方法調用后,才真正開始組件的渲染流程。

      接下來,回到runtime-core中關注渲染流程。

      核心渲染流程

      這一流程中主要做了兩件事:創建vnode渲染vnode

      vnode是用來描述DOMJavaScript對象,在Vue中既可以描述普通DOM節點,也可以描述組件節點,除此之外還有純文本vnode和注釋vnode。

      可以在runtime-corevnode.ts文件中找到vnode的類型定義:core/packages/runtime-core/src/vnode.ts at main · vuejs/core (github.com)

      內容較多,這里不做展示,比較核心的屬性有比如:

      • type:組件的標簽類型;
      • props:附加信息;
      • children:子節點,vnode數組;

      除此之外,Vue3還為vnode打上了各種flag來做標記,在patch階段根據不同的類型執行相應的處理邏輯。

      創建vnode

      mount方法的實現中,通過調用createVNode函數創建根組件的vnode

      const vnode = createVNode(rootComponent, rootProps);
      

      vnode.ts中可以找到createVNode函數的實現:core/packages/runtime-core/src/vnode.ts at main · vuejs/core (github.com)

      大致思路如下:

      function _createVNode(
        type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
        props: (Data & VNodeProps) | null = null,
        children: unknown = null,
        patchFlag: number = 0,
        dynamicProps: string[] | null = null,
        isBlockNode = false,
      ): VNode{
        // ...
        // 標準化class和style這些樣式屬性
        if(props){
          // ...
        }
          
        // 對vnode類型信息編碼(二進制)
        const shapeFlag = isString(type)
          ? ShapeFlags.ELEMENT
          : __FEATURE_SUSPENSE__ && isSuspense(type)
            ? ShapeFlags.SUSPENSE
            : isTeleport(type)
              ? ShapeFlags.TELEPORT
              : isObject(type)
                ? ShapeFlags.STATEFUL_COMPONENT
                : isFunction(type)
                  ? ShapeFlags.FUNCTIONAL_COMPONENT
                  : 0
        // 調用工廠函數構建vnode對象
        return createBaseVNode(
          type,
          props,
          children,
          patchFlag,
          dynamicProps,
          shapeFlag,
          isBlockNode,
          true,
        )
      }
      

      接下來看一下createBaseVNode的大致實現(這個函數也位于vnode.ts文件內):

      function createBaseVNode(
      	// vnode部分屬性的值
      ){
      	const vnode = {
              type,
              props,
              // ...很多屬性
          } as VNode
          
          // 標準化children:討論數組或者文本類型
          if (needFullChildrenNormalization) {
          	normalizeChildren(vnode, children)
          }
          return vnode
      }
      

      渲染vnode

      創建好vnode之后就是渲染的過程,在mount中使用render函數渲染創建好的vnode。

      render的標準化流程的實現位于runtime-corerenderer.ts中:

      源碼位置:core/packages/runtime-core/src/renderer.ts at main · vuejs/core (github.com)

      const render: RootRenderFunction = (vnode, container, namespace) => {
          if (vnode == null) {
              // 銷毀組件
              if (container._vnode) {
                  unmount(container._vnode, null, null, true)
              }
          } else {
              // 創建或者更新組件
              patch(
                  container._vnode || null,
                  vnode,
                  container,
                  null,
                  null,
                  null,
                  namespace,
              )
          }
          if (!isFlushing) {
              isFlushing = true
              flushPreFlushCbs()
              flushPostFlushCbs()
              isFlushing = false
          }
          // 緩存vnode節點,表示已經渲染
          container._vnode = vnode
      }
      
      • 如果vnode不存在,則調用unmount銷毀組件;
      • 如果vnode存在,那么調用patch創建或者更新組件;
      • vnode緩存到容器對象上,表示已渲染。

      patch函數的前兩個參數分別是舊vnode和新vnode

      • 初次調用,則container._vnode屬性返回undefined,短路運算符傳入null,則patch內部走創建邏輯;調用過后會將創建的vnode緩存到container._vnode;
      • 后續調用的container._vnode表示上一次創建的vnode,不為null,傳入patch后走更新邏輯。
      patch的實現

      patch本意是打補丁,這個函數有兩個功能:

      1. 根據vnode掛載DOM;
      2. 比較新舊vnode更新DOM

      這里只討論初始化流程,故只記錄如何掛載DOM,更新流程這里不做介紹。

      源碼位置:core/packages/runtime-core/src/renderer.ts at main · vuejs/core (github.com)

      const patch: PatchFn = (
          n1,
          n2,
          container,
          anchor = null,
          parentComponent = null,
          parentSuspense = null,
          namespace = undefined,
          slotScopeIds = null,
          optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren,
      ) => {
          // 二者相同,不需要更新
          if (n1 === n2) {
              return
          }
      
          // vnode類型不同,直接卸載舊節點
          if (n1 && !isSameVNodeType(n1, n2)) {
              anchor = getNextHostNode(n1)
              unmount(n1, parentComponent, parentSuspense, true)
              n1 = null
          }
      	// ......
      
          const { type, ref, shapeFlag } = n2
          switch (type) {
              case Text:
                  // 處理文字節點
                  break
              case Comment:
                  // 處理注釋節點
                  break
              case Static:
                  // 靜態節點
                  break
              case Fragment:
                  // Fragment節點
                  break
              default:
                  if (shapeFlag & ShapeFlags.ELEMENT) {
                      // 處理普通DOM元素
                  } else if (shapeFlag & ShapeFlags.COMPONENT) {
                      // 處理組件
                  } else if (shapeFlag & ShapeFlags.TELEPORT) {
                      // 處理teleport
                  } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
                      // 處理suspense
                  } else if (__DEV__) {
                      // 報錯:vnode類型不在可識別范圍內
                      warn('Invalid VNode type:', type, `(${typeof type})`)
                  }
          }
      }
      

      這里只關注前三個函數參數:

      • n1:舊vnode,為null則表示初次掛載;
      • n2:新vnode
      • container:掛載的目標容器。

      patch在其內部調用了processXXX處理不同類型的vnode,這里只關注組件類型和普通DOM節點類型。

      對組件的處理

      處理組件調用的是processComponent函數:

      processComponent
      const processComponent = (
          n1: VNode | null,
          n2: VNode,
          container: RendererElement,
          // ... 其它參數
      ) => {
          if (n1 == null) {
              // 掛載組件
              mountComponent(n2, container, /*...other args*/)
          } else {
              // 更新組件
              updateComponent(n1, n2, optimized)
          }
      }
      // 這里還有很多其它參數省略了,函數體內還處理了`keep-alive`的情況,具體可以自己看源碼。
      
      • 掛載組件使用mountComponent函數;
      • 更新組件使用updateComponent函數。
      mountComponent

      源碼位置:core/packages/runtime-core/src/renderer.ts at main · vuejs/core (github.com)

      這個函數處理了較多邊界情況,這里只展示主要的步驟:

       const mountComponent: MountComponentFn = (
          initialVNode,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          namespace: ElementNamespace,
          optimized,
       ) => {
           // 創建組件實例
           const instance: ComponentInternalInstance =
                 (initialVNode.component = createComponentInstance(
                     initialVNode,
                     parentComponent,
                     parentSuspense,
                 ))
      	
           // 設置組件實例
           setupComponent(instance, false, optimized)
      	
           // 設置并運行帶副作用的渲染函數
           setupRenderEffect(
               instance,
               initialVNode,
               container,
               anchor,
               parentSuspense,
               namespace,
               optimized,
           )
       }
      
      • 創建組件實例:工廠模式創建組件實例對象;
      • 設置組件實例:instance記錄了許多組件相關的數據,setupComponent這一步主要是對propsslots等屬性進行初始化。

      接下來重點看一下setupRenderEffect函數的實現。

      setupRenderEffect

      setupRenderEffect 函數的主要工作是設置一個響應式效果 (ReactiveEffect),并創建一個調度任務 (SchedulerJob) 來管理組件的渲染和更新。首次渲染和后續更新的邏輯都封裝在 componentUpdateFn 中。

      簡化后的代碼

      const setupRenderEffect: SetupRenderEffectFn = (
        instance,
        initialVNode,
        container,
        anchor,
        parentSuspense,
        namespace: ElementNamespace,
        optimized,
      ) => {
        // 組件更新函數
        const componentUpdateFn = () => {
          if (!instance.isMounted) {
            // 首次掛載邏輯
            instance.subTree = renderComponentRoot(instance)
            patch(null, instance.subTree, container, anchor, instance, parentSuspense, namespace)
            instance.isMounted = true
          } else {
            // 后續更新邏輯
            const nextTree = renderComponentRoot(instance)
            patch(instance.subTree, nextTree, container, anchor, instance, parentSuspense, namespace)
            instance.subTree = nextTree
          }
        }
      
        // 創建響應式效果
        const effect = (instance.effect = new ReactiveEffect(componentUpdateFn, NOOP))
      
        // 創建調度任務
        const update: SchedulerJob = (instance.update = () => {
          if (effect.dirty) {
            effect.run()
          }
        })
      
        // 立即執行更新函數
        update()
      }
      

      setupRenderEffect內部主要包含了3個函數:

      • componentUpdateFn 的主要作用是在組件首次掛載和后續更新時執行相應的渲染邏輯,確保組件的虛擬 DOM 樹與實際的 DOM 樹保持同步,并執行相關的生命周期鉤子函數。
      • effect 封裝了組件的渲染邏輯,負責在響應式依賴變化時觸發重新渲染。
      • update 是調度任務,負責在適當的時機檢查和觸發 effect,確保組件的渲染邏輯能夠正確執行。

      也就是說它們依次為前者的進一步封裝。

      componentUpdateFn中的初始掛載邏輯:

      • 渲染組件生成subTree;(遞歸調用patch
      • subTree通過patch掛載到container上。

      這里的patch就是一個遞歸過程。事實上patch對于組件只有渲染過程,沒有掛載的操作,因為組件是抽象的,并不能通過DOM API插入到頁面上。

      也就是說patch只對DOM類型元素進行mount掛載,對于組件類型元素的處理只做遞歸操作。換個角度描述就是:組件樹的葉子節點一定都是DOM類型元素,只有這樣才能渲染并掛載到頁面上。

      接下來開始研究patchDOM類型元素的處理過程。(可以返回上文看一下patch的實現)。

      對DOM的處理
      processElement

      patch函數使用processElement 函數處理新舊DOM元素,當n1null時,走掛載流程;否則走更新流程。

      源碼地址:core/packages/runtime-core/src/renderer.ts at main · vuejs/core (github.com)

      const processElement = (
          n1: VNode | null,
          n2: VNode,
          container: RendererElement,
          // ...other args...
        ) => {
          if (n1 == null) {
            // 掛載
            mountElement(n2, container, /* ...other args... */)
          } else {
            // 更新
            patchElement(n1, n2, parentComponent, /* ...other args... */)
          }
        }
      
      mountElement

      源碼位置:core/packages/runtime-core/src/renderer.ts at main · vuejs/core (github.com)

      這里省略了很多代碼,只保留大致流程:

      1. 創建DOM元素;

      2. 掛載子節點;

        • 如果子節點只是文字,則設置DOM節點的textContent;

        • 如果子節點是數組,則使用for循環 + 遞歸調用patch函數渲染子元素;

          這里遞歸使用的是patch而不是mountElement是因為子元素可能不是DOM元素,而是其它類型的元素。因此還是要用到patch中的switch - case走類型判斷的邏輯。

      3. 設置DOM元素的屬性;

      4. 插入DOM元素。

      const mountElement = (
        vnode: VNode,
        container: RendererElement,
        /* ...other args... */
      ) => {
        const { props, shapeFlag, transition, dirs } = vnode
        // 創建DOM元素
        const el = vnode.el = hostCreateElement(vnode.type as string, namespace, props && props.is, props)
      
        // 掛載子節點
        if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
          hostSetElementText(el, vnode.children as string)
        } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
          mountChildren(vnode.children as VNodeArrayChildren, el, null, parentComponent, parentSuspense, resolveChildrenNamespace(vnode, namespace), slotScopeIds, optimized)
        }
      	
        // 設置屬性
        if (props) {
          for (const key in props) {
            if (key !== 'value' && !isReservedProp(key)) {
              hostPatchProp(el, key, null, props[key], namespace, parentComponent)
            }
          }
          // 特殊處理 value 屬性
          if ('value' in props) {
            hostPatchProp(el, 'value', null, props.value, namespace)
          }
        }
      
        // 插入元素
        hostInsert(el, container, anchor)
      }
      

      其中的hostCreateElementhostSetElementTexthostPatchProp、hostInsert函數都由runtime-dom中在創建renderer的時候傳入對應的實現。

      runtime-dom模塊的nodeOps.tspatchProp.ts文件可以找到這些DOM相關操作的具體實現。

      nodeOps.ts源碼位置:core/packages/runtime-dom/src/nodeOps.ts at e26fd7b1d15cb3335a4c2230cc49b1008daddca1 · vuejs/core (github.com)

      patchProp.ts源碼位置:core/packages/runtime-dom/src/patchProp.ts at e26fd7b1d15cb3335a4c2230cc49b1008daddca1 · vuejs/core (github.com)

      上述hostXXX對應的DOM方法分別是:

      • hostCreateElementdocument.createElement;
      • hostSetElementTextel.textContent = ...
      • hostPatchProp:直接修改DOM對象上的鍵值,會對特殊的key做處理;
      • hostInsert:[Node.insertBefore](Node.insertBefore() - Web API | MDN (mozilla.org))

      初次渲染流程總結

      mount

      posted @ 2024-08-05 19:18  feixianxing  閱讀(474)  評論(0)    收藏  舉報
      主站蜘蛛池模板: 国产精品美女一区二区三| 99在线精品国自产拍中文字幕| 蜜臀av日韩精品一区二区| 元朗区| 国产久爱免费精品视频| 美女午夜福利视频一区二区| 成人无码特黄特黄AV片在线| 人妻熟女一二三区夜夜爱| 亚洲av中文乱码一区二| 久久精品国产99亚洲精品| 成在线人视频免费视频| 成人国产精品中文字幕| 久久精品熟女亚洲av麻| 潮喷失禁大喷水无码| 少妇高潮毛片免费看| 国产精品国产三级国av| 我要看亚洲黄色太黄一级黄| 日韩大片一区二区三区| 国产中文字幕在线一区| 无套内谢少妇高清毛片| 婷婷色香五月综合缴缴情香蕉 | 亚洲中文字幕久久精品品| 中文字幕v亚洲日本在线电影| 青青国产揄拍视频| 无码人妻一区二区三区AV| 久久天天躁狠狠躁夜夜婷| 亚洲欧洲美洲无码精品va| 精品久久丝袜熟女一二三| 亚洲精品熟女一区二区| 麻豆国产成人av高清在线| 国产精品小仙女自拍视频| 精品一日韩美女性夜视频| 亚洲av二区伊人久久| 亚洲欧美在线综合一区二区三区| 成人午夜视频在线| 九九热在线免费视频播放| 下面一进一出好爽视频| 国产超碰人人爽人人做| 中文字幕av无码不卡| 新丰县| 久久综合激情网|