Vue 页面渲染流程
Vue 从挂载组件到生成真实 DOM 的完整渲染过程
问题
Vue 是如何将模板渲染成页面的?从 new Vue() 开始,到页面显示内容,中间经历了什么?
解答
挂载组件($mount)
Vue 通过 new 关键字实例化时,会调用 _init 方法进行初始化:
// 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 内部会调用 $mount 挂载组件,$mount 实际调用的是 mountComponent:
// src/core/instance/lifecycle.js
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
callHook(vm, 'beforeMount')
let updateComponent
// 核心渲染方法
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
// 创建渲染 Watcher,数据变化时重新渲染
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
callHook(vm, 'mounted')
return vm
}
updateComponent 是渲染视图的核心方法,包含两个关键步骤:
vm._render()创建并返回 VNodevm._update()将 VNode 转为真实 DOM
构建 VNode(_render)
_render 方法用于构建组件的 VNode:
// src/core/instance/render.js
Vue.prototype._render = function () {
const vm = this
const { render, _parentVnode } = vm.$options
// 执行 render 函数生成 VNode
vnode = render.call(vm._renderProxy, vm.$createElement)
return vnode
}
在初始化时,Vue 会为实例绑定两个创建 VNode 的方法:
// src/core/instance/render.js
export function initRender (vm) {
// 模板编译生成的 render 函数使用
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// 用户手写 render 函数使用
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
}
createElement
createElement 是创建 VNode 的核心方法:
// src/core/vdom/create-elemenet.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 是实际创建 VNode 的方法:
export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
// 规范化 children 为 VNode 数组
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
let vnode, ns
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {
// 平台内置元素,创建普通 VNode
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// 已注册组件,创建组件类型 VNode
vnode = createComponent(Ctor, data, context, children, tag)
} else {
// 未知标签
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
// tag 是组件类型,直接创建组件 VNode
vnode = createComponent(tag, data, context, children)
}
return vnode
}
children 规范化有两种方式:
// 编译生成的 render 函数使用,只需简单扁平化
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
}
// 用户手写 render 函数使用,需要完整规范化
export function normalizeChildren (children: any): ?Array<VNode> {
return isPrimitive(children)
? [createTextVNode(children)]
: Array.isArray(children)
? normalizeArrayChildren(children)
: undefined
}
生成真实 DOM(_update)
_update 方法将 VNode 转换为真实 DOM:
// 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
vm._vnode = vnode
if (!prevVnode) {
// 初始渲染
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false)
} else {
// 更新渲染
vm.$el = vm.__patch__(prevVnode, vnode)
}
// 更新引用
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
}
__patch__ 方法在 web 平台的定义:
// src/platforms/web/runtime/index.js
import { patch } from './patch'
Vue.prototype.__patch__ = inBrowser ? patch : noop
patch 由 createPatchFunction 创建:
// 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]])
}
}
}
// 返回 patch 函数
return function patch (oldVnode, vnode, hydrating, removeOnly) {
// ...
}
}
这里有两个重要对象:
nodeOps:封装的原生 DOM 操作方法modules:各模块的钩子函数(指令、ref 等)
patch 过程
初始渲染时,oldVnode 是根节点的真实 DOM:
return function patch (oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
if (isRealElement) {
// 将真实 DOM 转为 VNode
oldVnode = emptyNodeAt(oldVnode)
}
// 获取父节点
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// 创建新节点
createElm(
vnode,
insertedVnodeQueue,
parentElm,
nodeOps.nextSibling(oldElm)
)
// 移除旧节点
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
}
}
}
// 触发 insert 钩子
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
createElm
createElm 是将 VNode 转为真实 DOM 的核心方法:
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
// 尝试创建组件节点
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)
// 创建子节点
createChildren(vnode, children, insertedVnodeQueue)
// 调用 create 钩子
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
// 插入父节点
insert(parentElm, vnode.elm, refElm)
} 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)
}
}
创建子节点的过程:
function createChildren (vnode, children, insertedVnodeQueue) {
if (Array.isArray(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)))
}
}
插入节点:
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 插入,所以整个节点树的插入顺序是”先子后父”。
最后触发 insert 钩子:
function invokeInsertHook (vnode, queue, initial) {
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])
}
}
}
关键点
- 渲染流程分为三个阶段:挂载组件(
$mount)、构建 VNode(_render)、生成真实 DOM(_update) createElement根据 tag 类型创建不同的 VNode,包括普通元素、组件、未知标签三种- children 规范化有两种方式:编译生成的 render 使用
simpleNormalizeChildren,手写 render 使用normalizeChildren createElm通过递归创建节点树,插入顺序是”先子后父”,最深的子节点最先插入- 初始渲染时,Vue 不是替换根节点,而是在其后插入新节点,再移除旧节点
目录