Vue页面渲染流程

在 Vue 核心中除了响应式原理外,视图渲染也是重中之重。我们都知道每次更新数据,都会走视图渲染的逻辑,而这当中牵扯的逻辑也是十分繁琐。

本文主要解析的是初始化视图渲染流程,你将会了解到从挂载组件开始,Vue 是如何构建 VNode,又是如何将 VNode 转为真实节点并挂载到页面。

Vue2 的页面渲染流程可分为初始化渲染更新渲染两大阶段,核心围绕「VNode 构建」和「真实 DOM 生成」展开。以下是详细流程解析:

一、初始化渲染流程(首次渲染)

实例化与初始化(new Vue())

  • 通过 new Vue(options) 创建组件实例,触发 _init 方法(src/core/instance/init.js)。
  • _init 方法初始化组件选项(合并配置、初始化生命周期、事件、状态等),并判断是否存在 el 选项,若存在则调用 $mount 开始挂载。

Vue 是一个构造函数,通过 new 关键字进行实例化。

代码块 1(src/core/instance/index.js)

js 复制代码
// src/core/instance/index.js
function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

在实例化时,会调用 _init 进行初始化。

代码块 2(src/core/instance/init.js)

js 复制代码
// src/core/instance/init.js
Vue.prototype._init = function (options?: Object) {
  // ... component = this
  if (vm.$options.el) {
    vm.$mount(vm.$options.el)
  }
}

挂载组件($mount)

  • $mount 方法最终调用 mountComponentsrc/core/instance/lifecycle.js),负责将组件挂载到 DOM。
js 复制代码
// src/core/instance/lifecycle.js
export function mountComponent (
  vm: Component,
  el: Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  callHook(vm, 'beforeMount')

  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    // ...
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating) // 渲染页面函数
    }
  }

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  new Watcher(vm, updateComponent, noop, { // 渲染watcher
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

mountComponent 除了调用一些生命周期的钩子函数外,最主要是 updateComponent,它就是负责渲染视图的核心方法,其只有一行核心代码:

js 复制代码
vm._update(vm._render(), hydrating)
  • vm._render 创建并返回 VNode,vm._update 接收 VNode 将其转为真实节点。
  • 其中 vm._render() 生成 VNode,vm._update() 将 VNode 转为真实 DOM。
  • 创建「渲染 Watcher」,监听数据变化,首次执行 updateComponent 触发初始化渲染。
    • updateComponent 会被传入渲染 Watcher,每当数据变化触发 Watcher 更新就会执行该函数,重新渲染视图。
    • updateComponent 传入渲染 Watcher 后会被执行一次进行初始化页面渲染。

所以我们着重分析的是 vm._rendervm._update 两个方法,这也是本文主要了解的原理 ------Vue 视图渲染流程。

构建VNode(_render)

首先是 _render 方法,它用来构建组件的 VNode。

  • vm._render() 调用组件的 render 函数(模板编译生成或用户自定义),返回根 VNode(虚拟节点)。
  • render 函数通过 vm.$createElement(或编译生成的 vm._c)创建 VNode,内部调用 createElement 方法。

代码块 1(src/core/instance/render.js)

js 复制代码
// src/core/instance/render.js
Vue.prototype._render = function () {
  const { render, _parentVnode } = vm.$options
  vnode = render.call(vm._renderProxy, vm.$createElement)
  return vnode
}

_render 内部会执行 render 方法并返回构建好的 VNode,render 一般是模板编译后生成的方法,也有可能是用户自定义。

代码块 2(src/core/instance/render.js)

js 复制代码
// src/core/instance/render.js
export function initRender (vm) {
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
}
  • initRender 在初始化就会执行为实例上绑定两个方法,分别是 vm._cvm.$createElement。它们两者都是调用 createElement 方法,它是创建 VNode 的核心方法,最后一个参数用于区别是否为用户自定义。
  • vm._c 应用场景是在编译生成的 render 函数中调用,vm.$createElement 则用于用户自定义 render 函数的场景。就像上面 render 在调用时会传入参数 vm.$createElement,我们在自定义 render 函数接收到的参数就是它。

createElement

  • createElement 封装 _createElement,处理参数并规范化子节点(children):
    • 若为编译生成的 render,调用 simpleNormalizeChildren 扁平化子节点数组。
    • 若为用户自定义 render,调用 normalizeChildren 将子节点转为 VNode 数组。
  • 根据 tag 类型创建对应 VNode:
    • 内置标签(如 div):直接创建普通 VNode。
    • 组件:调用 createComponent 创建组件类型 VNode。

代码块 3(src/core/vdom/create-element.js)

js 复制代码
// src/core/vdom/create-element.js
export function createElement (
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,
  alwaysNormalize: boolean
): VNode | Array<VNode> {
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE
  }
  return _createElement(context, tag, data, children, normalizationType)
}

createElement 方法实际上是对 _createElement 方法的封装,它允许传入的参数更加灵活。

js 复制代码
export function _createElement (
  context: Component,
  tag: string | Class<Component> | Function | Object,
  data: VNodeData,
  children: any,
  normalizationType: number
): VNode | Array<VNode> {
  if (isDef(data) && isDef((data: any).is)) {
    tag = data.is
  }
  if (!tag) {
    // in case of component :is set to falsy value
    return createEmptyVNode()
  }
  // support single function children as default scoped slot
  if (Array.isArray(children) &&
    typeof children[0] === 'function'
  ) {
    data = data || {}
    data.scopedSlots = { default: children[0] }
    data.children = []
  }
  if (normalizationType === ALWAYS_NORMALIZE) {
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children)
  }
  let vnode, ns
  // 根据tag类型创建VNode
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    if (config.isReservedTag(tag)) {
      // platform built-in elements
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // component
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      // unknown or unlisted namespaced elements
      // check at runtime because it may get assigned a namespace when its
      // parent normalizes children
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  } else {
    // direct component options / constructor
    vnode = createComponent(tag, data, context, children)
  }
  if (Array.isArray(vnode)) {
    return vnode
  } else if (isDef(vnode)) {
    if (isDef(ns)) applyNS(vnode, ns)
    if (isDef(data)) registerDeepBindings(data)
    return vnode
  } else {
    return createEmptyVNode()
  }
}

_createElement 参数中会接收 children,它表示当前 VNode 的子节点,因为它是任意类型的,所以接下来需要将其规范为标准的 VNode 数组;

js 复制代码
// 这里规范化 children
if (normalizationType === ALWAYS_NORMALIZE) {
  children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
  children = simpleNormalizeChildren(children)
}

simpleNormalizeChildrennormalizeChildren 均用于规范化 children。由 normalizationType 判断 render 函数是编译生成的还是用户自定义的。

js 复制代码
// 1. when the children contains components - because a functional component
// may return an Array instead of a single root. In this case, just a simple
// normalization is needed. If the child is an Array, we flatten the whole
// thing with Array.prototype.concat. It is guaranteed to be only 1-level deep
// because functional components already normalize their own children.
export function simpleNormalizeChildren (children: any) {
  for (let i = 0; i < children.length; i++) {
    if (Array.isArray(children[i])) {
      return Array.prototype.concat.apply([], children)
    }
  }
  return children
}

// 2. when the children contains constructs that always generated nested Arrays,
// e.g. <template>, <slot>, v-for, or when the children is provided by user
// with hand-written render functions / JSX. In such cases a full normalization
// is needed to cater to all possible types of children values.
export function normalizeChildren (children: any): Array<VNode> {
  return isPrimitive(children)
    ? [createTextVNode(children)]
    : Array.isArray(children)
      ? normalizeArrayChildren(children)
      : undefined
}

simpleNormalizeChildren 方法调用场景是 render 函数当函数是编译生成的。normalizeChildren 方法的调用场景主要是 render 函数是用户手写的。

经过对 children 的规范化,children 变成了一个类型为 VNode 的数组。之后就是创建 VNode 的逻辑。

js 复制代码
// src/core/vdom/patch.js
let vnode, ns
if (typeof tag === 'string') {
  let Ctor
  ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
  if (config.isReservedTag(tag)) {
    // platform built-in elements
    vnode = new VNode(
      config.parsePlatformTagName(tag), data, children,
      undefined, undefined, context
    )
  } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
    // component
    vnode = createComponent(Ctor, data, context, children, tag)
  } else {
    // unknown or unlisted namespaced elements
    // check at runtime because it may get assigned a namespace when its
    // parent normalizes children
    vnode = new VNode(
      tag, data, children,
      undefined, undefined, context
    )
  }
} else {
  // direct component options / constructor
  vnode = createComponent(tag, data, context, children)
}

如果 tagstring 类型,则接着判断:如果是内置的一些节点,创建一个普通 VNode;如果是已注册的组件名,则通过 createComponent 创建一个组件类型的 VNode;否则创建一个未知的标签的 VNode。

如果 tag 不是 string 类型,那就是 Component 类型,则直接调用 createComponent 创建一个组件类型的 VNode 节点。

最后 _createElement 会返回一个 VNode,也就是调用 vm._render 时创建得到的 VNode。之后 VNode 会传递给 vm._update 函数,用于生成真实 DOM。

生成真实dom(_update 与 patch)

  • vm._update(vnode) 调用 vm.__patch__ 方法(平台相关实现,Web 端对应 patch 函数)。
  • patch 函数核心逻辑:
    1. 首次渲染时,oldVnode 为真实 DOM(如 #app),先通过 emptyNodeAt 转为空 VNode。
    2. 调用 createElm 将 VNode 递归转为真实 DOM 并插入父节点。
    3. 移除旧节点,触发 insert 钩子,完成渲染。
js 复制代码
// 核心代码简化
Vue.prototype._update = function(vnode) {
  const vm = this;
  const prevVnode = vm._vnode;
  vm._vnode = vnode;
  if (!prevVnode) {
    // 首次渲染:将 VNode 转为真实 DOM 并挂载
    vm.$el = vm.__patch__(vm.$el, vnode);
  }
};

// patch 核心逻辑
function patch(oldVnode, vnode) {
  if (isRealElement(oldVnode)) {
    oldVnode = emptyNodeAt(oldVnode); // 真实 DOM 转空 VNode
  }
  createElm(vnode, parentElm); // 创建真实 DOM 并插入
  removeVnodes([oldVnode]); // 移除旧节点
}

代码块 1(src/core/instance/lifecycle.js)

js 复制代码
// src/core/instance/lifecycle.js
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  const vm: Component = this
  const prevEl = vm.$el
  const prevVnode = vm._vnode
  const prevActiveInstance = activeInstance
  activeInstance = vm
  vm._vnode = vnode
  // Vue.prototype.__patch__ is injected in entry points
  // based on the rendering backend used.
  if (!prevVnode) {
    // initial render
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
  } else {
    // updates
    vm.$el = vm.__patch__(prevVnode, vnode)
  }
  activeInstance = prevActiveInstance
  // update __vue__ reference
  if (prevEl) {
    prevEl.__vue__ = null
  }
  if (vm.$el) {
    vm.$el.__vue__ = vm
  }
  // if parent is an HOC, update its $el as well
  if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
    vm.$parent.$el = vm.$el
  }
  // updated hook is called by the scheduler to ensure that children are
  // updated in a parent's updated hook.
}

_update 里最核心的方法就是 vm.__patch__ 方法,不同平台的 patch 方法的定义稍有不同,在 web 平台中它是这样定义的:

代码块 2(src/platforms/web/runtime/index.js)

js 复制代码
// src/platforms/web/runtime/index.js
import { patch } from './patch'
// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop

可以看到 __patch__ 实际调用的是 patch 方法。

代码块 3(src/platforms/web/runtime/patch.js)

js 复制代码
// src/platforms/web/runtime/patch.js
import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'

// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules)

export const patch: Function = createPatchFunction({ nodeOps, modules })

而patch方法是由createPatchFunction方法创建返回出来的函数。

js 复制代码
// src/core/vdom/patch.js
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']

export function createPatchFunction (backend) {
  let i, j
  const cbs = {}
  const { modules, nodeOps } = backend

  for (i = 0; i < hooks.length; i++) {
    cbs[hooks[i]] = []
    for (j = 0; j < modules.length; j++) {
      if (isDef(modules[j][hooks[i]])) {
        cbs[hooks[i]].push(modules[j][hooks[i]])
      }
    }
  }

  // ...

  return function patch (oldVnode, vnode, hydrating, removeOnly) {
    // ...
  }
}

这里有两个比较重要的对象 nodeOpsmodules

  • nodeOps 是封装的原生 dom 操作方法,在生成真实节点树的过程中,dom 相关操作都是调用 nodeOps 内的方法。
  • modules 是待执行的钩子函数,在进入函数时,会将不同模块的钩子函数分类放置到 cbs 中,其中包括自定义指令钩子函数、ref 钩子函数。在 patch 阶段,会根据操作节点的行为取出对应类型进行调用。

Patch

js 复制代码
 // initial render
2 vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)

在首次渲染时,vm.$el 对应的是根节点 dom 对象,也就是我们熟知的 id 为 app 的 div。它作为 oldVnode 参数传入 patch

js 复制代码
return function patch(oldVnode, vnode, hydrating, removeOnly) {
  if (isUndef(vnode)) {
    if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
    return
  }

  let isInitialPatch = false
  const insertedVnodeQueue = []

  if (isUndef(oldVnode)) {
    // empty mount (likely as component), create new root element
    isInitialPatch = true
    createElm(vnode, insertedVnodeQueue)
  } else {
    const isRealElement = isDef(oldVnode.nodeType)
    if (!isRealElement && sameVnode(oldVnode, vnode)) {
      // patch existing root node
      patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
    } else {
      if (isRealElement) {
        // mounting to a real element
        // check if this is server-rendered content and if we can perform
        // a successful hydration.
        if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
          oldVnode.removeAttribute(SSR_ATTR)
          hydrating = true
        }
        if (isTrue(hydrating)) {
          if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
            invokeInsertHook(vnode, insertedVnodeQueue, true)
            return oldVnode
          } else if (process.env.NODE_ENV !== 'production') {
            warn(
              'The client-side rendered virtual DOM tree is not matching ' +
              'server-rendered content. This is likely caused by incorrect ' +
              'HTML markup, for example nesting block-level elements inside ' +
              '<p>, or missing <tbody>. Bailing hydration and performing ' +
              'full client-side render.'
            )
          }
        }
        // either not server-rendered, or hydration failed.
        // create an empty node and replace it
        oldVnode = emptyNodeAt(oldVnode)
      }

      // replacing existing element
      const oldElm = oldVnode.elm
      const parentElm = nodeOps.parentNode(oldElm)

      // create new node
      createElm(
        vnode,
        insertedVnodeQueue,
        // extremely rare edge case: do not insert if old element is in a
        // leaving transition. Only happens when combining transition +
        // keep-alive + HOCs. (#4590)
        oldElm._leaveCb ? null : parentElm,
        nodeOps.nextSibling(oldElm)
      )

      // update parent placeholder node element, recursively
      if (isDef(vnode.parent)) {
        let ancestor = vnode.parent
        const patchable = isPatchable(vnode)
        while (ancestor) {
          for (let i = 0; i < cbs.destroy.length; ++i) {
            cbs.destroy[i](ancestor)
          }
          ancestor.elm = vnode.elm
          if (patchable) {
            for (let i = 0; i < cbs.create.length; ++i) {
              cbs.create[i](emptyNode, ancestor)
            }
            // #6513
            // invoke insert hooks that may have been merged by create hooks.
            // e.g. for directives that uses the "inserted" hook.
            const insert = ancestor.data.hook.insert
            if (insert.merged) {
              // start at index 1 to avoid re-invoking component mounted hook
              for (let i = 1; i < insert.fns.length; i++) {
                insert.fns[i]()
              }
            }
          } else {
            registerRef(ancestor)
          }
          ancestor = ancestor.parent
        }
      }

      // destroy old node
      if (isDef(parentElm)) {
        removeVnodes([oldVnode], 0, 0)
      } else if (isDef(oldVnode.tag)) {
        invokeDestroyHook(oldVnode)
      }
    }
  }

  invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
  return vnode.elm
}

通过检查属性 nodeType(真实节点才有的属性),判断 oldVnode 是否为真实节点。

js 复制代码
const isRealElement = isDef(oldVnode.nodeType)
if (isRealElement) {
  // ...
  oldVnode = emptyNodeAt(oldVnode)
}

很明显第一次的 isRealElement 是为 true,因此会调用 emptyNodeAt 将其转为 VNode:

js 复制代码
function emptyNodeAt (elm) {
  return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)
}

接着会调用 createElm 方法,它就是将 VNode 转为真实 dom 的核心方法:

递归创建DOM节点(createElm)

createElm 是 VNode 转真实 DOM 的核心方法:

  1. 若为组件 VNode,调用 createComponent 实例化组件并递归挂载。
  2. 若为普通元素,通过 nodeOps.createElement 创建真实节点,赋值给 vnode.elm
  3. 递归调用 createChildren 处理子节点,将子节点插入当前节点(先子后父的插入顺序)。
  4. 调用 insert 方法将节点插入父容器(使用 appendChildinsertBefore)。
js 复制代码
function createElm (
  vnode,
  insertedVnodeQueue,
  parentElm,
  refElm,
  nested,
  ownerArray,
  index
) {
  if (isDef(vnode.elm) && isDef(ownerArray)) {
    // This vnode was used in a previous render!
    // now it's used as a new node, overwriting its elm would cause
    // potential patch errors down the road when it's used as an insertion
    // reference node. Instead, we clone the node on-demand before creating
    // associated DOM element for it.
    vnode = ownerArray[index] = cloneVNode(vnode)
  }

  vnode.isRootInsert = !nested // for transition enter check
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return
  }

  const data = vnode.data
  const children = vnode.children
  const tag = vnode.tag
  if (isDef(tag)) {
    vnode.elm = vnode.ns
      ? nodeOps.createElementNS(vnode.ns, tag)
      : nodeOps.createElement(tag, vnode)
    setScope(vnode)

    /* istanbul ignore if */
    if (__WEEX__) {
      // ...
    } else {
      createChildren(vnode, children, insertedVnodeQueue) // 递归创建子节点
      if (isDef(data)) {
        invokeCreateHooks(vnode, insertedVnodeQueue)
      }
      insert(parentElm, vnode.elm, refElm)
    }
  } else if (process.env.NODE_ENV !== 'production' && data && data.pre) {
    creatingElmInPre--
  } else if (isTrue(vnode.isComment)) {
    vnode.elm = nodeOps.createComment(vnode.text)
    insert(parentElm, vnode.elm, refElm) // 插入父节点
  } else {
    vnode.elm = nodeOps.createTextNode(vnode.text)
    insert(parentElm, vnode.elm, refElm)
  }
}

一开始会调用 createComponent 尝试创建组件类型的节点,如果成功会返回 true。在创建过程中也会调用 $mount 进行组件范围内的挂载,所以走的还是 patch 这套流程。

js 复制代码
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
  return
}

如果没有完成创建,代表该 VNode 对应的是真实节点,往下继续创建真实节点的逻辑。

js 复制代码
vnode.elm = vnode.ns
  ? nodeOps.createElementNS(vnode.ns, tag)
  : nodeOps.createElement(tag, vnode)

根据 tag 创建对应类型真实节点,赋值给 vnode.elm,它作为父节点容器,创建的子节点会被放到里面。

然后调用 createChildren 创建子节点:

js 复制代码
function createChildren (vnode, children, insertedVnodeQueue) {
  if (Array.isArray(children)) {
    if (process.env.NODE_ENV !== 'production') {
      checkDuplicateKeys(children)
    }
    for (let i = 0; i < children.length; ++i) {
      createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i)
    }
  } else if (isPrimitive(vnode.text)) {
    nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
  }
}

内部进行遍历子节点数组,再次调用 createElm 创建节点,而上面创建的 vnode.elm 作为父节点传入。如此循环,直到没有子节点,就会创建文本节点插入到 vnode.elm 中。

执行完成后出来,会调用 invokeCreateHooks,它负责执行 dom 操作时的 create 钩子函数,同时将 VNode 加入到 insertedVnodeQueue 中:

js 复制代码
function invokeCreateHooks (vnode, insertedVnodeQueue) {
  for (let i = 0; i < cbs.create.length; ++i) {
    cbs.create[i](emptyNode, vnode)
  }
  i = vnode.data.hook // Reuse variable
  if (isDef(i)) {
    if (isDef(i.create)) i.create(emptyNode, vnode)
    if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
  }
}

插入到父节点

最后一步就是调用 insert 方法将节点插入到父节点:

js 复制代码
function insert (parent, elm, ref) {
  if (isDef(parent)) {
    if (isDef(ref)) {
      if (nodeOps.parentNode(ref) === parent) {
        nodeOps.insertBefore(parent, elm, ref)
      }
    } else {
      nodeOps.appendChild(parent, elm)
    }
  }
}

可以看到 Vue 是通过递归调用 createElm 来创建节点树的。同时也说明最深的子节点会先调用 insert 插入节点。所以整个节点树的插入顺序是 "先子后父"。插入节点方法就是原生 dom 的方法 insertBeforeappendChild

js 复制代码
if (isDef(parentElm)) {
  removeVnodes([oldVnode], 0, 0)
}

createElm 流程走完后,构建完成的节点树已经插入到页面上了。其实 Vue 在初始化渲染页面时,并不是把原来的根节点 app 给真正替换掉,而是在其后面插入一个新的节点,接着再把旧节点给移除掉。

所以在 createElm 之后会调用 removeVnodes 来移除旧节点,它里面同样是调用的原生 dom 方法 removeChild

js 复制代码
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
js 复制代码
function invokeInsertHook (vnode, queue, initial) {
  // delay insert hooks for component root nodes, invoke them after the
  // element is really inserted
  if (isTrue(initial) && isDef(vnode.parent)) {
    vnode.parent.data.pendingInsert = queue
  } else {
    for (let i = 0; i < queue.length; ++i) {
      queue[i].data.hook.insert(queue[i])
    }
  }
}

patch 的最后就是调用 invokeInsertHook 方法,触发节点插入的钩子函数。

至此整个页面渲染的流程完毕。

完成渲染

  • 触发 mounted 生命周期钩子(所有子组件挂载完成后),页面渲染完毕。

二、更新渲染流程(数据变化时)

当响应式数据变化时,触发渲染 Watcher 的更新,重新执行 updateComponent,流程如下:

  1. 触发 beforeUpdate 生命周期钩子。
  2. 重新调用 vm._render() 生成新 VNode。
  3. 调用 vm._update(),通过 patch 对比新旧 VNode(sameVnode 检查):
    • 若为同一节点,调用 patchVnode 对比并更新差异(属性、文本、子节点等)。
    • 若为不同节点,直接创建新节点并替换旧节点。
  4. 触发 updated 生命周期钩子,完成更新。

三、总结

  • 初始化调用$mount挂载组件。 - _render开始构建VNode,核心方法为createElement,一般会创建普通的VNode,遇到组件就创建组件类型的VNode,否则就是未知标签的VNode,构建完成传递给_update
  • patch阶段根据VNode创建真实节点树,核心方法为createElm,首选遇到组件类型的VNode,内部会执行$mount,再走一遍相同的流程。普通节点类型则创建一个真实的节点,如果它有子节点开始递归调用createElm,使用insert插入子节点,直到没有子节点就填充内容节点。
  • 最后递归完成后,同样也是使用insert将整个节点树插入到页面中,再将旧的根节点移除。
  1. 初始化渲染new Vue()_init$mountmountComponent_render(生成 VNode) → _updatepatch 生成真实 DOM)。
  2. 更新渲染 :数据变化 → 渲染 Watcher 触发 → 重新生成 VNode → patch 对比更新 DOM。
  3. 核心思想:通过 VNode 抽象 DOM,减少直接操作 DOM 的开销,通过 Diff 算法高效更新差异。
相关推荐
_codeOH13 小时前
Vue 3 vs React 19:框架还在卷,核心原理就这些
前端·vue.js
英勇无比的消炎药14 小时前
新手必看玩转TinyRobot一定要避开这些坑
前端·vue.js
英勇无比的消炎药15 小时前
别再盲目混用AI组件库和传统组件库差距原来这么大
前端·vue.js
英勇无比的消炎药17 小时前
前端提效神器全新AI组件库TinyRobot改写日常开发模式
前端·vue.js
英勇无比的消炎药17 小时前
前端提效神器TinyRobot
前端·vue.js
CDwenhuohuo17 小时前
uni 背景色渐变 全屏
前端·javascript·vue.js
爱怪笑的小杰杰17 小时前
Vue 项目交付第三方开发,如何隐藏核心 JS 源码?
前端·javascript·vue.js
小二·17 小时前
Vue 3 组合式 API 进阶实战
前端·javascript·vue.js
rising start19 小时前
九、vue3 组件通信:全场景详解
前端·vue.js·typescript
编程技术手记19 小时前
Vue Scoped CSS 与动态创建 DOM 的兼容性问题
前端·css·vue.js