溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

vue3中渲染系統(tǒng)的示例分析

發(fā)布時間:2021-12-29 11:05:49 來源:億速云 閱讀:187 作者:小新 欄目:編程語言

這篇文章給大家分享的是有關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é)點。

vue3中渲染系統(tǒng)的示例分析

了解完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)vue3vue2的掛載方式是不同的,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、mixincomponentdirective方法都返回了app應用實例,開發(fā)者可以鏈式調(diào)用。

// 一直use一直爽
createApp(App).use(Router).use(Vuex).component('component',{}).mount("#app")

到此app應用實例已經(jīng)創(chuàng)建好了~,打印查看下創(chuàng)建的app應用:

vue3中渲染系統(tǒng)的示例分析

總結一下創(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并返回

vue3中渲染系統(tǒng)的示例分析

掛載階段

接下來,當我們執(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,相反如果vnodepatch過程過長,同樣會導致頁面的卡頓。 而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結構:

vue3中渲染系統(tǒng)的示例分析

渲染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)
    }
  }
}
  • 當傳入的vnodenull&存在老的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ù)會通過判斷typeshapeFlag的不同來走不同的處理邏輯,今天我們主要分析組件類型普通DOM元素的處理。

processComponent

初始化渲染時,typeobject并且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);
  }
};

如果n1null,則執(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)容:

vue3中渲染系統(tǒng)的示例分析

  • 設置組件實例

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的邏輯也很簡單,首先初始化組件propsslots掛載到組件實例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ā)生什么變化:

vue3中渲染系統(tǒng)的示例分析

這個時候組件實例instancedata、proxy、rendersetupState已經(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)容:

vue3中渲染系統(tǒng)的示例分析

此時,可能有些同學開始疑惑了,為什么會有兩顆vnode樹呢?這兩顆vnode樹又有什么區(qū)別呢?

  • initialVNode

    initialVNode就是組件的vnode,即描述整個組件對象的,組件vnode會定義一些和組件相關的屬性:dataprops、生命周期等。通過渲染組件vnode,生成子樹vnode

  • sub tree

    子樹vnode是通過組件vnoderender方法生成的,其實也就是對組件模板template的描述,即真正要渲染到瀏覽器的DOM vnode。

生成subTree后,接下來就繼續(xù)通過patch方法,把subTree節(jié)點掛載到container上。 接下來,我們繼續(xù)往下分析,大家可以看下上面subTree的截圖:subTreetype值為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遞歸處理。接下來,我們主要看下typeELEMENT類型的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中渲染系統(tǒng)的示例分析

更新流程

分析完vue3首次渲染的整個流程后,那么在數(shù)據(jù)更新后,vue3又是怎么更新渲染呢?接下來分析更新流程階段就要涉及到vue3響應式系統(tǒng)的知識了(由于篇幅有限,我們不會展開更多響應式的知識,期待后續(xù)篇章的更加詳細的分析)。

依賴收集

回憶下在首次渲染時的設置組件實例setupComponent階段會創(chuàng)建渲染上下文代理,而在生成subTree階段,會通過renderComponentRoot函數(shù),執(zhí)行組件vnoderender方法,同時會觸發(fā)渲染上下文代理PublicInstanceProxyHandlersget,從而實現(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;
}

我們可以查看下此時組件vnoderender方法的內(nèi)容:

vue3中渲染系統(tǒng)的示例分析

或者打印查看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ā)PublicInstanceProxyHandlersget方法,那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() {}
};

接下來我們以keytitle的例子簡單介紹下get的邏輯:

  • 首先判斷key值是否已$開頭,明顯title走否的邏輯

  • 再看accessCache緩存中是否存在

    性能優(yōu)化:緩存屬性應該根據(jù)哪種類型獲取,避免每次都觸發(fā)**hasOwn**的開銷

  • 最后再按照順序獲?。?code>setupState => data => props => ctxPublicInstanceProxyHandlerssethas的處理邏輯,同樣以這個順序處理

  • 若存在時,先設置緩存accessCache,再從setupState中獲取title對應的值

重點來了,當訪問setupState.title時,觸發(fā)proxyget的流程會有兩個階段:

  • 首先觸發(fā)setupState對應的proxyget,然后獲取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)容,驗證一下我們的分析:

vue3中渲染系統(tǒng)的示例分析

通過截圖,我們可以看到此時title收集的副作用就是渲染effect,細心的同學就發(fā)現(xiàn)了截圖中的fn方法就是componentUpdateFn函數(shù),執(zhí)行fn()繼續(xù)掛載children。

vue3中渲染系統(tǒng)的示例分析

派發(fā)更新

分析完依賴收集階段,我們再看下,vue3又是如何進行派發(fā)更新呢?

當我們點擊按鈕執(zhí)行this.title += 1時,同樣會觸發(fā)PublicInstanceProxyHandlersset方法,而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ā)setupStateset方法:

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ā)refset方法,判斷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中渲染系統(tǒng)的示例分析

總結

綜上,我們分析完了vue3的整個渲染過程更新流程,當然我們只是從主要的渲染流程分析,完整的渲染過程的復雜度不止于此,比如基于block tree的優(yōu)化實現(xiàn),patch階段的diff優(yōu)化以及在更新流程中的響應式階段的優(yōu)化又是怎樣的等細節(jié)。

本文的初衷便是給大家提供分析vue3整個渲染過程的輪廓,有了整體的印象,再去分析了解更加細節(jié)的點的時候,也會更有思路和方向。

最后,附一張完整的渲染流程圖,與君共享。

vue3中渲染系統(tǒng)的示例分析

感謝各位的閱讀!關于“vue3中渲染系統(tǒng)的示例分析”這篇文章就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,讓大家可以學到更多知識,如果覺得文章不錯,可以把它分享出去讓更多的人看到吧!

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內(nèi)容。

AI