vue3复习-源码-运行时

195 阅读14分钟

1.环境搭建

安装&构建

git clone git@github.com:vuejs/core.git
pnpm i
pnpm build

构建后,在packages/vue/dist 会多出vue.global.js和vue.global.js.map 。我们可以基于这两个文件去调试源码。

image.png

官方例子

image.png

我们可以看到很多经典的demo:如todolist (这里 classic文件夹用的是传统的option模式,composition文件夹则是用compositionAPI实现。可以对比两种的差异。) image.png

我们可以看到所有demo使用的vue路径就是刚才编译好的packages/vue/distvue.global.js image.png

新增测试页面

1.初次渲染

在packages/vue/examples/composition 新增一个test.html

<script src="../../dist/vue.global.js"></script>  

<div id="demo"> 
  --{{val}}-- 
</div>

<script>
debugger
const app = Vue.createApp({ 
  setup() {
   const val = ref("oooo")
    return {
      val, 
    }
  } 
}) 
app.mount('#demo') 
</script>

<style> 
</style>

  1. 这里我们定义一个响应式对象val,
  2. 绑定到页面显示
调试
  1. 使用live server插件,右键运行这个test.html 页面 image.png

  2. 打开控制台,刷新开发debugger

在源码,我们通过单步执行,慢慢断点进入具体的逻辑

image.png

我们可以全局搜索processText(n1, n2, container, anchor) 找到最终实例化挂载替换文本的地方的代码,加上断点。

image.png

通过堆栈,我们可以看出第一次运行的结果

image.png

  1. app.mount (index.ts)
  2. mount 挂载 (apiCreateApp.ts)
  3. render 渲染 (renderer.ts)
  4. patch 对比更新 (renderer.ts)
  5. processComponent 对比更新 (renderer.ts)
  6. mountComponent 对比更新 (renderer.ts)
  7. setupRenderEffect (renderer.ts)
  8. instance.update (renderer.ts)
  9. ReactiveEffect.run (effect.ts)
  10. componentUpdateFn (renderer.ts)
  11. patch (renderer.ts) 这里执行processText挂载text内容

由此我们可以看出,需要学习的代码包括 apiCreateApp.ts和 renderer.ts

核心逻辑
  1. mountComponent最终只是执行 自定义setup方法,和组件状态初始化
  2. 通过effect 触发 update方法
  3. update方法里面执行patch
  4. patch的时候才真正渲染dom ,执行processText渲染text内容。

2.初次渲染 + 子组件渲染

在packages/vue/examples/composition 新增一个test2.html

<script src="../../dist/vue.global.js"></script>
<script type="text/x-template" id="app-template">
  <p>子组件:{{message}} - {{state.a}}</p>
</script> 
<script>
const { reactive, computed,ref,h } = Vue

const App = {
  template: '#app-template',
  props: {
    message: String, 
  },
  setup(props) {
    const state = reactive({
      a:"ggg"
    })
    return {
      state, 
    } 
  }
}
</script>


<div id="demo"> 
  --{{val}}--
  <app :message="val" >
  </app>
</div>

<script>
debugger
const app = Vue.createApp({
  components: {
    App
  },
  setup() {
   const val = ref("oooo")
    return {
      val, 
    }
  }
}) 
app.mount('#demo') 
</script>

<style> 
</style>

这里我们多加了一个子组件 App

使用上面方法开始看到

image.png

第二次,父节点+子节点

  1. app.mount (index.ts)
  2. mount (apiCreateApp.ts)
  3. render (renderer.ts)
  4. patch (renderer.ts)
  5. processComponent (renderer.ts)
  6. mountComponent (renderer.ts)
  7. setupRenderEffect (renderer.ts)
  8. instance.update (renderer.ts)
  9. ReactiveEffect.run (effect.ts)
  10. componentUpdateFn (renderer.ts)
  11. patch (renderer.ts)

加上

  1. processFragment (renderer.ts)
  2. mountChildren (renderer.ts) 这里循环patch2次,第一次:修改--{{val}}-- , 第二次:触发创建新的组件
  3. patch (第二次patch)
  4. processComponent
  5. mountComponent
  6. setupRenderEffect
  7. instance.update
  8. ReactiveEffect.run
  9. componentUpdateFn
  10. patch (这里处理最终挂载app组件对应的html到页面)

从代码中可以看出,多了processFragment + mountChildren的逻辑,当组件有子组件 会包裹一个Fragment

核心逻辑
  1. mountComponent最终只是执行 自定义setup方法,和组件状态初始化
  2. 通过effect 触发 update方法
  3. update方法里面执行patch
  4. patch的时候发现下面是app组件
  5. 执行render渲染组件
  6. 渲染一个Fragment
  7. 根据所有子元素,执行mountChildren,循环patch
  8. 循环第一次 执行processText 渲染修改--{{val}}--
  9. 循环第二次 触发创建新子组件,
  10. 新子组件又关联一个新的effect 触发 update方法
  11. update方法里面执行patch 渲染子组件创建

2.运行逻辑

例子1:完整源码逻辑

代码

<script src="../../dist/vue.global.js"></script>  
<div id="demo"> 
  --{{val}}-- 
</div>
<script>
debugger
const app = Vue.createApp({ 
  setup() {
   const val = ref("oooo")
    return {
      val, 
    }
  } 
}) 
app.mount('#demo') 
</script>

<style> 
</style>

流程图

image.png

关键的文件

  1. packages/runtime-dom/src/index.ts 入口 全局render对象 ,所有实际dom操作接口
  2. packages/runtime-core/src/renderer.ts 渲染 patch
  3. packages/reactivity/src/effect.ts 依赖收集与触发update方法
  4. packages/runtime-core/src/component.ts 组件setup实例化

关键方法

createApp

调用Vue.createApp实例化一个app , 使用app.mount('#demo') 进行挂载

先看createApp的实现,

  • ensureRenderer().createApp(...args) 获取app
  • 使用重写mount方法,让mount前先获取dom容器 packages/runtime-dom/src/index.ts

export const createApp = ((...args) => {
  const app = ensureRenderer().createApp(...args) 

  const { mount } = app // 这里重写了原来mount方法,让mount 延迟执行
  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => { // 重写先执行的mount 主要做dom获取容器,做跨平台区分
    const container = normalizeContainer(containerOrSelector) // normalizeContainer针对dom层 做元素访问
    if (!container) return

    const component = app._component 

    // clear content before mounting
    container.innerHTML = ''
    const proxy = mount(container, false, resolveRootNamespace(container)) // 这里是延迟mount后,在这里再被执行
    if (container instanceof Element) {
      container.removeAttribute('v-cloak')
      container.setAttribute('data-v-app', '')
    }
    return proxy
  }

  return app
}) as CreateAppFunction<Element>


//获取容器
function normalizeContainer(
  container: Element | ShadowRoot | string,
): Element | null {
  if (isString(container)) {
    const res = document.querySelector(container) //主要做dom获取
    return res
  } 
  return container as any
}

  • createApp 调用的是ensureRenderer().createApp(...args)
  • ensureRenderer() 负责确认跨平台的渲染器,里面又调用了createRenderer 返回renderer,并且通过外部变量保存,通过闭包方式缓存起来。

packages/runtime-dom/src/index.ts

import { nodeOps } from './nodeOps'
import { patchProp } from './patchProp'
const rendererOptions = extend({ patchProp }, nodeOps) //这里的 extend = Object.assign

let renderer: Renderer<Element | ShadowRoot> | HydrationRenderer 
function ensureRenderer() {
  return (
    renderer ||
    (renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
  )
}
  • 传入重写nodeOps节点操作
  • 传入重写pathProp属性操作 这两个属性用于跨平台实现节点的操作。

参考juejin.cn/post/741629…

packages/runtime-dom/src/nodeOps.ts

import type { RendererOptions } from '@vue/runtime-core'

export const svgNS = 'http://www.w3.org/2000/svg'
export const mathmlNS = 'http://www.w3.org/1998/Math/MathML'

const doc = (typeof document !== 'undefined' ? document : null) as Document

const templateContainer = doc && /*#__PURE__*/ doc.createElement('template')

export const nodeOps: Omit<RendererOptions<Node, Element>, 'patchProp'> = {
  insert: (child, parent, anchor) => {
    parent.insertBefore(child, anchor || null)
  },

  remove: child => {
    const parent = child.parentNode
    if (parent) {
      parent.removeChild(child)
    }
  },

  createElement: (tag, namespace, is, props): Element => {
    const el =
      namespace === 'svg'
        ? doc.createElementNS(svgNS, tag)
        : namespace === 'mathml'
          ? doc.createElementNS(mathmlNS, tag)
          : is
            ? doc.createElement(tag, { is })
            : doc.createElement(tag)

    if (tag === 'select' && props && props.multiple != null) {
      ;(el as HTMLSelectElement).setAttribute('multiple', props.multiple)
    }

    return el
  },

  createText: text => doc.createTextNode(text),

  createComment: text => doc.createComment(text),

  setText: (node, text) => {
    node.nodeValue = text
  },

  setElementText: (el, text) => {
    el.textContent = text
  },

  parentNode: node => node.parentNode as Element | null,

  nextSibling: node => node.nextSibling,

  querySelector: selector => doc.querySelector(selector), 
}

packages/runtime-dom/src/patchProp.ts

import type { RendererOptions } from '@vue/runtime-core'
 

type DOMRendererOptions = RendererOptions<Node, Element>

export const patchProp: DOMRendererOptions['patchProp'] = (
  el,
  key,
  prevValue,
  nextValue,
  namespace,
  prevChildren,
  parentComponent,
  parentSuspense,
  unmountChildren,
) => {
  const isSVG = namespace === 'svg'
  if (key === 'class') {
    patchClass(el, nextValue, isSVG)
  } else if (key === 'style') {
    patchStyle(el, prevValue, nextValue)
  } else if (isOn(key)) {
    // ignore v-model listeners
    if (!isModelListener(key)) {
      patchEvent(el, key, prevValue, nextValue, parentComponent)
    }
  } else if (
    key[0] === '.'
      ? ((key = key.slice(1)), true)
      : key[0] === '^'
        ? ((key = key.slice(1)), false)
        : shouldSetAsProp(el, key, nextValue, isSVG)
  ) {
    patchDOMProp(
      el,
      key,
      nextValue,
      prevChildren,
      parentComponent,
      parentSuspense,
      unmountChildren,
    ) 
    if (
      !el.tagName.includes('-') &&
      (key === 'value' || key === 'checked' || key === 'selected')
    ) {
      patchAttr(el, key, nextValue, isSVG, parentComponent, key !== 'value')
    }
  } else { 
    if (key === 'true-value') {
      ;(el as any)._trueValue = nextValue
    } else if (key === 'false-value') {
      ;(el as any)._falseValue = nextValue
    }
    patchAttr(el, key, nextValue, isSVG, parentComponent)
  }
}
 

createRenderer

createRenderer内部又调用baseCreateRenderer方法(两者都在一个文件),内部传入的RendererOptions就是nodeOps和patchProp实现的内容。

packages/runtime-core/src/renderer.ts

export function createRenderer<
  HostNode = RendererNode,
  HostElement = RendererElement,
>(options: RendererOptions<HostNode, HostElement>) {
  return baseCreateRenderer<HostNode, HostElement>(options)
}

export interface RendererOptions<
  HostNode = RendererNode,
  HostElement = RendererElement,
> {
  patchProp(
    el: HostElement,
    key: string,
    prevValue: any,
    nextValue: any,
    namespace?: ElementNamespace,
    prevChildren?: VNode<HostNode, HostElement>[],
    parentComponent?: ComponentInternalInstance | null,
    parentSuspense?: SuspenseBoundary | null,
    unmountChildren?: UnmountChildrenFn,
  ): void
  insert(el: HostNode, parent: HostElement, anchor?: HostNode | null): void
  remove(el: HostNode): void
  createElement(
    type: string,
    namespace?: ElementNamespace,
    isCustomizedBuiltIn?: string,
    vnodeProps?: (VNodeProps & { [key: string]: any }) | null,
  ): HostElement
  createText(text: string): HostNode
  createComment(text: string): HostNode
  setText(node: HostNode, text: string): void
  setElementText(node: HostElement, text: string): void
  parentNode(node: HostNode): HostElement | null
  nextSibling(node: HostNode): HostNode | null
  querySelector?(selector: string): HostElement | null
  setScopeId?(el: HostElement, id: string): void
  cloneNode?(node: HostNode): HostNode
  insertStaticContent?(
    content: string,
    parent: HostElement,
    anchor: HostNode | null,
    namespace: ElementNamespace,
    start?: HostNode | null,
    end?: HostNode | null,
  ): [HostNode, HostNode]
}


baseCreateRenderer

我们可以看到baseCreateRenderer的代码真的长的吓死人 2k多行代码


function baseCreateRenderer(
    options: RendererOptions,
    createHydrationFns?: typeof createHydrationFunctions,
  ): any { 
    const {
      insert: hostInsert,
      remove: hostRemove,
      patchProp: hostPatchProp,
      createElement: hostCreateElement,
      createText: hostCreateText,
      createComment: hostCreateComment,
      setText: hostSetText,
      setElementText: hostSetElementText,
      parentNode: hostParentNode,
      nextSibling: hostNextSibling,
      setScopeId: hostSetScopeId = NOOP,
      insertStaticContent: hostInsertStaticContent,
    } = options
  
    // Note: functions inside this closure should use `const xxx = () => {}`
    // style in order to prevent being inlined by minifiers.
    const patch: PatchFn = (
      n1,
      n2,
      container,
      anchor = null,
      parentComponent = null,
      parentSuspense = null,
      namespace = undefined,
      slotScopeIds = null,
      optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren,
    ) => { 
    }
  
    const processText: ProcessTextOrCommentFn = (n1, n2, container, anchor) => {
 
    }
  
    const processCommentNode: ProcessTextOrCommentFn = (
      n1,
      n2,
      container,
      anchor,
    ) => {
     
    }
  
    const mountStaticNode = (
      n2: VNode,
      container: RendererElement,
      anchor: RendererNode | null,
      namespace: ElementNamespace,
    ) => {
      // static nodes are only present when used with compiler-dom/runtime-dom
      // which guarantees presence of hostInsertStaticContent.
      ;[n2.el, n2.anchor] = hostInsertStaticContent!(
        n2.children as string,
        container,
        anchor,
        namespace,
        n2.el,
        n2.anchor,
      )
    }
  
    /**
     * Dev / HMR only
     */
    const patchStaticNode = (
      n1: VNode,
      n2: VNode,
      container: RendererElement,
      namespace: ElementNamespace,
    ) => {
      // static nodes are only patched during dev for HMR
      if (n2.children !== n1.children) {
        const anchor = hostNextSibling(n1.anchor!)
        // remove existing
        removeStaticNode(n1)
        // insert new
        ;[n2.el, n2.anchor] = hostInsertStaticContent!(
          n2.children as string,
          container,
          anchor,
          namespace,
        )
      } else {
        n2.el = n1.el
        n2.anchor = n1.anchor
      }
    }
  
    const moveStaticNode = (
      { el, anchor }: VNode,
      container: RendererElement,
      nextSibling: RendererNode | null,
    ) => { 
    }
  
    const removeStaticNode = ({ el, anchor }: VNode) => { 
    }
  
    const processElement = (
      n1: VNode | null,
      n2: VNode,
      container: RendererElement,
      anchor: RendererNode | null,
      parentComponent: ComponentInternalInstance | null,
      parentSuspense: SuspenseBoundary | null,
      namespace: ElementNamespace,
      slotScopeIds: string[] | null,
      optimized: boolean,
    ) => { 
    }
  
    const mountElement = (
      vnode: VNode,
      container: RendererElement,
      anchor: RendererNode | null,
      parentComponent: ComponentInternalInstance | null,
      parentSuspense: SuspenseBoundary | null,
      namespace: ElementNamespace,
      slotScopeIds: string[] | null,
      optimized: boolean,
    ) => {
       
    }
  
    const setScopeId = (
      el: RendererElement,
      vnode: VNode,
      scopeId: string | null,
      slotScopeIds: string[] | null,
      parentComponent: ComponentInternalInstance | null,
    ) => {
       
    }
  
    const mountChildren: MountChildrenFn = (
      children,
      container,
      anchor,
      parentComponent,
      parentSuspense,
      namespace: ElementNamespace,
      slotScopeIds,
      optimized,
      start = 0,
    ) => {
      for (let i = start; i < children.length; i++) {
        const child = (children[i] = optimized
          ? cloneIfMounted(children[i] as VNode)
          : normalizeVNode(children[i]))
        patch(
          null,
          child,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          namespace,
          slotScopeIds,
          optimized,
        )
      }
    }
  
    const patchElement = (
      n1: VNode,
      n2: VNode,
      parentComponent: ComponentInternalInstance | null,
      parentSuspense: SuspenseBoundary | null,
      namespace: ElementNamespace,
      slotScopeIds: string[] | null,
      optimized: boolean,
    ) => {
      
    }
  
    // The fast path for blocks.
    const patchBlockChildren: PatchBlockChildrenFn = (
      oldChildren,
      newChildren,
      fallbackContainer,
      parentComponent,
      parentSuspense,
      namespace: ElementNamespace,
      slotScopeIds,
    ) => {
       
    }
  
    const processFragment = (
      n1: VNode | null,
      n2: VNode,
      container: RendererElement,
      anchor: RendererNode | null,
      parentComponent: ComponentInternalInstance | null,
      parentSuspense: SuspenseBoundary | null,
      namespace: ElementNamespace,
      slotScopeIds: string[] | null,
      optimized: boolean,
    ) => {
      
    }
  
    const processComponent = (
      n1: VNode | null,
      n2: VNode,
      container: RendererElement,
      anchor: RendererNode | null,
      parentComponent: ComponentInternalInstance | null,
      parentSuspense: SuspenseBoundary | null,
      namespace: ElementNamespace,
      slotScopeIds: string[] | null,
      optimized: boolean,
    ) => {
      
    }
  
    const mountComponent: MountComponentFn = (
      initialVNode,
      container,
      anchor,
      parentComponent,
      parentSuspense,
      namespace: ElementNamespace,
      optimized,
    ) => {
      
    }
  
    const updateComponent = (n1: VNode, n2: VNode, optimized: boolean) => {
      const instance = (n2.component = n1.component)!
       
    }
  
    const setupRenderEffect: SetupRenderEffectFn = (
      instance,
      initialVNode,
      container,
      anchor,
      parentSuspense,
      namespace: ElementNamespace,
      optimized,
    ) => {
     
    }
  
    const updateComponentPreRender = (
      instance: ComponentInternalInstance,
      nextVNode: VNode,
      optimized: boolean,
    ) => {
       
    }
  
    const patchChildren: PatchChildrenFn = (
      n1,
      n2,
      container,
      anchor,
      parentComponent,
      parentSuspense,
      namespace: ElementNamespace,
      slotScopeIds,
      optimized = false,
    ) => {
     
    }
  
    const patchUnkeyedChildren = (
      c1: VNode[],
      c2: VNodeArrayChildren,
      container: RendererElement,
      anchor: RendererNode | null,
      parentComponent: ComponentInternalInstance | null,
      parentSuspense: SuspenseBoundary | null,
      namespace: ElementNamespace,
      slotScopeIds: string[] | null,
      optimized: boolean,
    ) => {
      
    }
  
    // can be all-keyed or mixed
    const patchKeyedChildren = (
      c1: VNode[],
      c2: VNodeArrayChildren,
      container: RendererElement,
      parentAnchor: RendererNode | null,
      parentComponent: ComponentInternalInstance | null,
      parentSuspense: SuspenseBoundary | null,
      namespace: ElementNamespace,
      slotScopeIds: string[] | null,
      optimized: boolean,
    ) => {
      
    }
  
    const move: MoveFn = (
      vnode,
      container,
      anchor,
      moveType,
      parentSuspense = null,
    ) => {
      const { el, type, transition, children, shapeFlag } = vnode
      
    }
  
    const unmount: UnmountFn = (
      vnode,
      parentComponent,
      parentSuspense,
      doRemove = false,
      optimized = false,
    ) => {
      
    }
  
    const remove: RemoveFn = vnode => {
     
    }
  
    const removeFragment = (cur: RendererNode, end: RendererNode) => {
    
    }
  
    const unmountComponent = (
      instance: ComponentInternalInstance,
      parentSuspense: SuspenseBoundary | null,
      doRemove?: boolean,
    ) => { 
    }
  
    const unmountChildren: UnmountChildrenFn = (
      children,
      parentComponent,
      parentSuspense,
      doRemove = false,
      optimized = false,
      start = 0,
    ) => {
     
    }
  
    const getNextHostNode: NextFn = vnode => {
     
    }
  
    let isFlushing = false
    const render: RootRenderFunction = (vnode, container, namespace) => {
     
    } 
  
    return {
      render,
      hydrate,
      createApp: createAppAPI(render, hydrate),
    }
  }

可以看到 该方法返回createApp由createAppAPI(render, hydrate)产生

  return {
    render,
    hydrate,
    createApp: createAppAPI(render, hydrate),
  }
createAppAPI

createAppAPI在

packages/runtime-core/src/apiCreateApp.ts


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

    if (rootProps != null && !isObject(rootProps)) {
      rootProps = null
    }

    const context = createAppContext()
    const installedPlugins = new WeakSet()

    let isMounted = false

    const app: App = (context.app = {
      _uid: uid++,
      _component: rootComponent as ConcreteComponent,
      _props: rootProps,
      _container: null,
      _context: context,
      _instance: null, 
      version,

      get config() {
        return context.config
      },

      set config(v) {
      
      },

      use(plugin: Plugin, ...options: any[]) {
        if (installedPlugins.has(plugin)) {
          __DEV__ && warn(`Plugin has already been applied to target app.`)
        } else if (plugin && isFunction(plugin.install)) {
          installedPlugins.add(plugin)
          plugin.install(app, ...options)
        } else if (isFunction(plugin)) {
          installedPlugins.add(plugin)
          plugin(app, ...options)
        }
        return app
      },

      mixin(mixin: ComponentOptions) {
        
        return app
      },

      component(name: string, component?: Component): any {
        
        if (!component) {
          return context.components[name]
        }
       
        context.components[name] = component
        return app
      },

      directive(name: string, directive?: Directive) { 
        if (!directive) {
          return context.directives[name] as any
        } 
        context.directives[name] = directive
        return app
      },

      mount(
        rootContainer: HostElement,
        isHydrate?: boolean,
        namespace?: boolean | ElementNamespace,
      ): any {
        if (!isMounted) { 
          const vnode = createVNode(rootComponent, rootProps) // 这里创建了一个根root的vnode
          vnode.appContext = context

          if (namespace === true) {
            namespace = 'svg'
          } else if (namespace === false) {
            namespace = undefined
          }

          if (isHydrate && hydrate) {
            hydrate(vnode as VNode<Node, Element>, rootContainer as any)
          } else {
            render(vnode, rootContainer, namespace) // 关键渲染的地方
          }
          isMounted = true
          app._container = rootContainer
          // for devtools and telemetry
          ;(rootContainer as any).__vue_app__ = app
          
          return getComponentPublicInstance(vnode.component!)
        } 
      },

      unmount() {
        if (isMounted) {
          render(null, app._container)
      },

      provide(key, value) { 
        context.provides[key as string | symbol] = value
        return app
      },

      runWithContext(fn) {
        const lastApp = currentApp
        currentApp = app
        try {
          return fn()
        } finally {
          currentApp = lastApp
        }
      },
    }) 

    return app
  }
}

该代码包含了app里面常见的component,mount,directive,use,的初始化。 关键代码 render(vnode, rootContainer, namespace) ,rootContainer就是 #demo。 这里,当执行mount方法就会执行render开始渲染。也就是2k多行代码里面的render

render

代码如下


  const render: RootRenderFunction = (vnode, container, namespace) => {
    if (vnode == null) {//这里不为空 用的是上面创建的root vnode
      if (container._vnode) {
        unmount(container._vnode, null, null, true)
      }
    } else {
        //关键逻辑 执行了patch
      patch(
        container._vnode || null,
        vnode,
        container,
        null,
        null,
        null,
        namespace,
      )
    }
    if (!isFlushing) {
      isFlushing = true
      flushPreFlushCbs()
      flushPostFlushCbs()
      isFlushing = false
    }
    container._vnode = vnode
  }

  const internals: RendererInternals = {
    p: patch,
    um: unmount,
    m: move,
    r: remove,
    mt: mountComponent,
    mc: mountChildren,
    pc: patchChildren,
    pbc: patchBlockChildren,
    n: getNextHostNode,
    o: options,
  }

  let hydrate: ReturnType<typeof createHydrationFunctions>[0] | undefined
  let hydrateNode: ReturnType<typeof createHydrationFunctions>[1] | undefined
  if (createHydrationFns) {
    ;[hydrate, hydrateNode] = createHydrationFns(
      internals as RendererInternals<Node, Element>,
    )
  }

  return {
    render,
    hydrate,
    createApp: createAppAPI(render, hydrate),
  }
}
patch

下面是patch的内容


  const patch: PatchFn = (
    n1, //上一次vnode
    n2, //这次vnode
    container,
    anchor = null,
    parentComponent = null,
    parentSuspense = null,
    namespace = undefined,
    slotScopeIds = null,
    optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren,
  ) => {
    if (n1 === n2) {
      return
    }

    // patching & not same type, unmount old tree
    if (n1 && !isSameVNodeType(n1, n2)) {
      anchor = getNextHostNode(n1)
      unmount(n1, parentComponent, parentSuspense, true)
      n1 = null
    }

    if (n2.patchFlag === PatchFlags.BAIL) {
      optimized = false
      n2.dynamicChildren = null
    }

    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, namespace)
        } else if (__DEV__) {
          patchStaticNode(n1, n2, container, namespace)
        }
        break
      case Fragment:
        processFragment(
          n1,
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          namespace,
          slotScopeIds,
          optimized,
        )
        break
      default:
        if (shapeFlag & ShapeFlags.ELEMENT) {
          processElement(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            namespace,
            slotScopeIds,
            optimized,
          )
        } else if (shapeFlag & ShapeFlags.COMPONENT) {
        //首次会进入渲染组件方法
          processComponent(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            namespace,
            slotScopeIds,
            optimized,
          )
        } else if (shapeFlag & ShapeFlags.TELEPORT) {
          ;(type as typeof TeleportImpl).process(
            n1 as TeleportVNode,
            n2 as TeleportVNode,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            namespace,
            slotScopeIds,
            optimized,
            internals,
          )
        } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
          ;(type as typeof SuspenseImpl).process(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            namespace,
            slotScopeIds,
            optimized,
            internals,
          )
        } else if (__DEV__) {
          warn('Invalid VNode type:', type, `(${typeof type})`)
        }
    }

    // set ref
    if (ref != null && parentComponent) {
      setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2)
    }
  }
processComponent

首次渲染,进入processComponent,


  const processComponent = (
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    namespace: ElementNamespace,
    slotScopeIds: string[] | null,
    optimized: boolean,
  ) => {
    n2.slotScopeIds = slotScopeIds
    if (n1 == null) {//n1为空 
      if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) { // 没有keepalive
        ;(parentComponent!.ctx as KeepAliveContext).activate(
          n2,
          container,
          anchor,
          namespace,
          optimized,
        )
      } else {
        // 关键代码 执行首次挂载
        mountComponent(
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          namespace,
          optimized,
        )
      }
    } else {
      updateComponent(n1, n2, optimized)
    }
  }

mountComponent代码,关键是执行setupRenderEffect代码,针对单个组件进行设置依赖收集

  const mountComponent: MountComponentFn = (
    initialVNode,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    namespace: ElementNamespace,
    optimized,
  ) => { 

      // resolve props and slots for setup context
    if (!(__COMPAT__ && compatMountInstance)) {
      if (__DEV__) {
        startMeasure(instance, `init`)
      }
      setupComponent(instance) //关键代码 执行自定义setup方法
      if (__DEV__) {
        endMeasure(instance, `init`)
      }
    }
  
    if (__FEATURE_SUSPENSE__ && instance.asyncDep) {
      parentSuspense &&
        parentSuspense.registerDep(instance, setupRenderEffect, optimized) 
      if (!initialVNode.el) {
        const placeholder = (instance.subTree = createVNode(Comment))
        processCommentNode(null, placeholder, container!, anchor)
      }
    } else {
       //关键代码,进行设置依赖收集
      setupRenderEffect(
        instance,
        initialVNode,
        container,
        anchor,
        parentSuspense,
        namespace,
        optimized,
      )
    }

    if (__DEV__) {
      popWarningContext()
      endMeasure(instance, `mount`)
    }
  }

packages/runtime-core/src/component.ts

setupComponent方法 初始化 prop 和插槽等,并调用 setupStatefulComponent

setupStatefulComponent 主要执行自定义setup方法


export function setupComponent(
  instance: ComponentInternalInstance,
  isSSR = false,
) {
  isSSR && setInSSRSetupState(isSSR)

  const { props, children } = instance.vnode
  const isStateful = isStatefulComponent(instance)
  initProps(instance, props, isStateful, isSSR) // 初始化prop
  initSlots(instance, children) // 初始化插槽

  const setupResult = isStateful
    ? setupStatefulComponent(instance, isSSR) // 关键代码
    : undefined

  isSSR && setInSSRSetupState(false)
  return setupResult
}

function setupStatefulComponent(
  instance: ComponentInternalInstance,
  isSSR: boolean,
) {
  const Component = instance.type as ComponentOptions
 
  // 0. create render proxy property access cache
  instance.accessCache = Object.create(null)
  // 1. create public instance / render proxy
  instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)
  if (__DEV__) {
    exposePropsOnRenderContext(instance)
  }
  // 2. call setup()
  const { setup } = Component
  if (setup) {
    const setupContext = (instance.setupContext =
      setup.length > 1 ? createSetupContext(instance) : null)

    const reset = setCurrentInstance(instance)
    pauseTracking()
    const setupResult = callWithErrorHandling( //关键代码 执行自定义setup fn(),并保存返回值
      setup, // 自定义setup方法
      instance,
      ErrorCodes.SETUP_FUNCTION,
      [
        __DEV__ ? shallowReadonly(instance.props) : instance.props,
        setupContext,
      ],
    )
    resetTracking()
    reset()
    //处理异步情况
    if (isPromise(setupResult)) {
      ...
    } else {
      handleSetupResult(instance, setupResult, isSSR)
    }
  } else {
    finishComponentSetup(instance, isSSR)
  }
}
setupRenderEffect

packages/runtime-core/src/renderer.ts

setupRenderEffect代码也很长,关键是new了一个ReactiveEffect方法,用于组件的依赖收集,每单响应数据变化时,触发调用定义的componentUpdateFn,也同时等价于 调用update() , effect.run() 的执行


const setupRenderEffect: SetupRenderEffectFn = (
    instance,
    initialVNode,
    container,
    anchor,
    parentSuspense,
    namespace: ElementNamespace,
    optimized,
  ) => {
    const componentUpdateFn = () => {
      if (!instance.isMounted) {
        let vnodeHook: VNodeHook | null | undefined
        const { el, props } = initialVNode
        const { bm, m, parent } = instance
        const isAsyncWrapperVNode = isAsyncWrapper(initialVNode)
 
        instance.isMounted = true 
        // #2458: deference mount-only object parameters to prevent memleaks
        initialVNode = container = anchor = null as any 
        let { next, bu, u, parent, vnode } = instance

        if (__FEATURE_SUSPENSE__) {
          const nonHydratedAsyncRoot = locateNonHydratedAsyncRoot(instance)
          // we are trying to update some async comp before hydration
          // this will cause crash because we don't know the root node yet
          if (nonHydratedAsyncRoot) {
            // only sync the properties and abort the rest of operations
            if (next) {
              next.el = vnode.el
              updateComponentPreRender(instance, next, optimized)
            }
            // and continue the rest of operations once the deps are resolved
            nonHydratedAsyncRoot.asyncDep!.then(() => {
              // the instance may be destroyed during the time period
              if (!instance.isUnmounted) {
                componentUpdateFn() //关键代码
              }
            })
            return
          }
        }

        // updateComponent
        // This is triggered by mutation of component's own state (next: null)
        // OR parent calling processComponent (next: VNode) 
        patch( // 关键代码,这里有执行了一次patch,即每次effect触发时候都会执行一次patch
          prevTree,
          nextTree,
          // parent may have changed if it's in a teleport
          hostParentNode(prevTree.el!)!,
          // anchor may have changed if it's in a fragment
          getNextHostNode(prevTree),
          instance,
          parentSuspense,
          namespace,
        ) 
        next.el = nextTree.el 
    }

    // create reactive effect for rendering 关键代码 
    const effect = (instance.effect = new ReactiveEffect(
      componentUpdateFn,
      NOOP,
      () => queueJob(update),
      instance.scope, // track it in component's effect scope
    ))

    const update: SchedulerJob = (instance.update = () => {
      if (effect.dirty) {
        effect.run() // effect(fn)传入的fn
      }
    })
    update.id = instance.uid
    // allowRecurse
    // #1801, #2043 component render effects should allow recursive updates
    toggleRecurse(instance, true) 
    update()
 }
}

流程:

  1. 第一次根组件调用mountComponent实例化,会触发一次setupRenderEffect,把根组件对应一个effect的update。并且会立即执行update方法
  2. update方法执行时候触发patch
  3. patch的时候查找当前组件下对应新所有子节点和 新属性
  4. 如果有子节点,并且是html直接渲染html,如果是组件继续递归
  5. 判断新属性 进行更新

例子2逻辑

代码

<script src="../../dist/vue.global.js"></script>
<script type="text/x-template" id="app-template">
  <p>子组件:{{message}} - {{state.a}}</p>
</script> 
<script>
const { reactive, computed,ref,h } = Vue

const App = {
  template: '#app-template',
  props: {
    message: String, 
  },
  setup(props) {
    const state = reactive({
      a:"ggg"
    })
    return {
      state, 
    } 
  }
}
</script>


<div id="demo"> 
  --{{val}}--
  <app :message="val" >
  </app>
</div>

<script>
debugger
const app = Vue.createApp({
  components: {
    App
  },
  setup() {
   const val = ref("oooo")
    return {
      val, 
    }
  }
}) 
app.mount('#demo') 
</script>

<style> 
</style>

流程图

image.png

执行逻辑

在处理第一次patch时候,由于该html没有根节点,会自动包裹一个fragment。

  --{{val}}--
  <app :message="val" >
  </app>

所有第一次patch多处理了 processFragment的逻辑处理,代码如下


  const processFragment = (
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    namespace: ElementNamespace,
    slotScopeIds: string[] | null,
    optimized: boolean,
  ) => {
    const fragmentStartAnchor = (n2.el = n1 ? n1.el : hostCreateText(''))!
    const fragmentEndAnchor = (n2.anchor = n1 ? n1.anchor : hostCreateText(''))!

    let { patchFlag, dynamicChildren, slotScopeIds: fragmentSlotScopeIds } = n2

    if (
      __DEV__ &&
      // #5523 dev root fragment may inherit directives
      (isHmrUpdating || patchFlag & PatchFlags.DEV_ROOT_FRAGMENT)
    ) {
      // HMR updated / Dev root fragment (w/ comments), force full diff
      patchFlag = 0
      optimized = false
      dynamicChildren = null
    }

    // check if this is a slot fragment with :slotted scope ids
    if (fragmentSlotScopeIds) {
      slotScopeIds = slotScopeIds
        ? slotScopeIds.concat(fragmentSlotScopeIds)
        : fragmentSlotScopeIds
    }

    if (n1 == null) {
      hostInsert(fragmentStartAnchor, container, anchor)
      hostInsert(fragmentEndAnchor, container, anchor)
      // a fragment can only have array children
      // since they are either generated by the compiler, or implicitly created
      // from arrays.
      mountChildren(
        // #10007
        // such fragment like `<></>` will be compiled into
        // a fragment which doesn't have a children.
        // In this case fallback to an empty array
        (n2.children || []) as VNodeArrayChildren,
        container,
        fragmentEndAnchor,
        parentComponent,
        parentSuspense,
        namespace,
        slotScopeIds,
        optimized,
      )
    } else {
      if (
        patchFlag > 0 &&
        patchFlag & PatchFlags.STABLE_FRAGMENT &&
        dynamicChildren &&
        // #2715 the previous fragment could've been a BAILed one as a result
        // of renderSlot() with no valid children
        n1.dynamicChildren
      ) {
        // a stable fragment (template root or <template v-for>) doesn't need to
        // patch children order, but it may contain dynamicChildren.
        patchBlockChildren(
          n1.dynamicChildren,
          dynamicChildren,
          container,
          parentComponent,
          parentSuspense,
          namespace,
          slotScopeIds,
        )
        if (__DEV__) {
          // necessary for HMR
          traverseStaticChildren(n1, n2)
        } else if (
          // #2080 if the stable fragment has a key, it's a <template v-for> that may
          //  get moved around. Make sure all root level vnodes inherit el.
          // #2134 or if it's a component root, it may also get moved around
          // as the component is being moved.
          n2.key != null ||
          (parentComponent && n2 === parentComponent.subTree)
        ) {
          traverseStaticChildren(n1, n2, true /* shallow */)
        }
      } else {
        // keyed / unkeyed, or manual fragments.
        // for keyed & unkeyed, since they are compiler generated from v-for,
        // each child is guaranteed to be a block so the fragment will never
        // have dynamicChildren.
        patchChildren(
          n1,
          n2,
          container,
          fragmentEndAnchor,
          parentComponent,
          parentSuspense,
          namespace,
          slotScopeIds,
          optimized,
        )
      }
    }
  }

根据n1是否为空 走不同逻辑

  • 第一次都是刚创建,n1为空,走hostInsert + mountChildren
     hostInsert(fragmentStartAnchor, container, anchor)
      hostInsert(fragmentEndAnchor, container, anchor)
      // a fragment can only have array children
      // since they are either generated by the compiler, or implicitly created
      // from arrays.
      mountChildren(
        // #10007
        // such fragment like `<></>` will be compiled into
        // a fragment which doesn't have a children.
        // In this case fallback to an empty array
        (n2.children || []) as VNodeArrayChildren,
        container,
        fragmentEndAnchor,
        parentComponent,
        parentSuspense,
        namespace,
        slotScopeIds,
        optimized,
      )

其中 mountChildren会递归遍历所有子组件并批量执行patch


  const mountChildren: MountChildrenFn = (
    children,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    namespace: ElementNamespace,
    slotScopeIds,
    optimized,
    start = 0,
  ) => {
    for (let i = start; i < children.length; i++) {
      const child = (children[i] = optimized
        ? cloneIfMounted(children[i] as VNode)
        : normalizeVNode(children[i]))
      patch(
        null,
        child,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        namespace,
        slotScopeIds,
        optimized,
      )
    }
  }

这时候

  • 第一次patch执行的是processText,把--{{val}}--插入到<div id="demo">
  • 第二次patch执行processCompoment,并调用mountComponent 最后执行processElement输出p标签到<div id="demo">

源码

github.com/mjsong07/vu…

参考

[玩转vue3](time.geekbang.org/column/intr…