整體流程

完整的創建與渲染流程可以分成這些階段:

  1. 創建 App 實例
  2. 創建根組件實例
  3. 設置響應式狀態
  4. 創建渲染器(Renderer)
  5. 掛載 Mount
  6. vnode -> DOM 渲染
  7. 數據變更觸發更新
  8. 重新渲染 / diff / patch

流程圖大致如下:

createApp() ───> app.mount('#app')
         │                 │
         ▼                 ▼
   createRootComponent    createRenderer
         │                 │
         ▼                 ▼
 setup() / render()   render(vnode) -> patch
         │                 │
         ▼                 ▼
   effect(fn) ────> scheduler -> patch updates

1、createApp 初始化

Vue 應用的入口通常是:

createApp(App).mount('#app')

從源碼看 createApp:

// packages/runtime-core/src/apiCreateApp.ts
export function createAppAPI(render) {
  return function createApp(rootComponent, rootProps = null) {
    const app = {
      _component: rootComponent,
      _props: rootProps,
      _container: null,
      _context: createAppContext()
    }
    const proxy = (app._instance = {
      app
    })
    // register global APIs
    // ...
    return {
      mount(container) {
        const vnode = createVNode(rootComponent, rootProps)
        app._container = container
        render(vnode, container)
      },
      unmount() { /* ... */ }
    }
  }
}

關鍵點:

  • createAppAPI(render) 生成 createApp 函數
  • app 內保存 _component、上下文 _context
  • app.mount 調用 render(vnode, container)

render平台渲染器 注入(在 web 下是 DOM 渲染器)。

2、createVNode 創建虛擬節點(VNode)

在 mount 前會創建一個虛擬節點:

function createVNode(type, props, children) {
  const vnode = {
    type,
    props,
    children,
    shapeFlag: getShapeFlag(type),
    el: null,
    key: props && props.key
  }
  return vnode
}

vnode 是渲染的基礎單元:

shapeFlag 用來快速判斷 vnode 類型,是內部性能優化。

3、渲染器 Renderer 初始化

Vue3 是平台無關的(runtime-core),真正依賴 DOM 的是在 runtime-dom 中。

創建 Renderer:

export const renderer = createRenderer({
  createElement: hostCreateElement,
  patchProp: hostPatchProp,
  insert: hostInsert,
  remove: hostRemove,
  setElementText: hostSetElementText
})

createRenderer 返回了我們前面在 createApp 中使用的 render(vnode, container) 函數。

4、render & patch

核心渲染入口:

function render(vnode, container) {
  patch(null, vnode, container, null, null)
}

patch 是渲染補丁函數:

function patch(n1, n2, container, parentComponent, anchor) {
  const { type, shapeFlag } = n2
  if (shapeFlag & ShapeFlags.ELEMENT) {
    processElement()
  } else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
    processComponent(...)
  }
}

簡化為:

  • 如果是 DOM 元素 vnode → 掛載/更新
  • 如果是 組件 vnode → 創建組件實例、掛載、渲染子樹

5、組件實例創建

當渲染組件時:

function processComponent(n1, n2, container, parentComponent, anchor) {
  mountComponent(n2, container, parentComponent, anchor)
}
function mountComponent(vnode, container, parentComponent, anchor) {
  const instance = createComponentInstance(vnode, parentComponent)
  setupComponent(instance)
  setupRenderEffect(instance, container, anchor)
}
  • processComponent 處理組件
  • mountComponent 掛載組件
    • createComponentInstance 創建組件實例
    • setupComponent 創建組件對象

createComponentInstance:

function createComponentInstance(vnode, parent) {
  const instance = {
    vnode,
    parent,
    proxy: null,
    ctx: {},
    props: {},
    attrs: {},
    slots: {},
    setupState: {},
    isMounted: false,
    subTree: null
  }
  return instance
}

實例保存基礎信息,還沒運行 setup。

6、 setupComponent(初始化組件)

function setupComponent(instance) {
  initProps(instance, vnode.props)
  initSlots(instance, vnode.children)
  setupStatefulComponent(instance)
}

內部會執行:

const { setup } = Component
if (setup) {
  const setupResult = setup(props, ctx)
  handleSetupResult(instance, setupResult)
}

setup 返回值​:

  • 返回對象 → 作為響應式狀態 state
  • 返回函數 → render 函數

最終讓組件擁有 instance.render

7、創建響應式狀態

Vue3 的響應式來自 reactivity 包:

const state = reactive({ count: 0 })

底層是 Proxy 攔截 getter/setter:

  • getter:收集依賴
  • setter:觸發依賴更新

依賴管理核心是 ​effect / track / trigger​。

8、 setupRenderEffect 與首次渲染

創建渲染器副作用,並調度組件掛載和異步更新:

function setupRenderEffect(instance, container, anchor) {
  instance.update = effect(function componentEffect() {
    if (!instance.isMounted) {
      const subTree = (instance.subTree = instance.render.call(proxy))
      patch(null, subTree, container, instance, anchor)
      instance.isMounted = true
    } else {
      // 更新更新邏輯
    }
  }, {
    scheduler: queueJob
  })
}

這裏:

  • 創建一個 響應式 effect
  • 第一次執行 render 得到 subTree
  • patch 子樹到 DOM

effect + scheduler 實現異步更新。

9、vnode-> 真實 DOM(DOM mount)

當 patch 到真正的 DOM 時,走的是 element 分支:

function processElement(...) {
  if (!n1) {
    mountElement(vnode, container)
  } else {
    patchElement(n1, n2)
  }
}

mountElement

function mountElement(vnode, container) {
  const el = (vnode.el = hostCreateElement(vnode.type))
  // props
  for (key in props) {
    hostPatchProp(el, key, null, props[key])
  }
  // children
  if (typeof children === 'string') {
    hostSetElementText(el, children)
  } else {
    children.forEach(c => patch(null, c, el))
  }
  hostInsert(el, container)
}

10、更新 & Diff 算法

當響應式狀態改變:

state.count++

觸發 setter → trigger

  • 將 effect 放入更新隊列
  • 異步執行 scheduler
  • 調用 instance.update 再次 patch

更新階段:

patchElement(n1, n2)

核心邏輯:

  1. props diff
  2. children diff
  3. unkeyed/keyed diff 算法(最小化移動)

具體見 patchChildrenpatchKeyedChildren

整體核心對象關係架構

App
 └─ vnode(root)
     └─ ComponentInstance
         ├─ props / slots
         ├─ setupState
         └─ render() -> subTree
             └─ vnode tree
                 └─ DOM nodes

響應式依賴結構:

reactive state
 ├─ effects[]
 └─ track -> effect
              └─ scheduler -> patch