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 挂载组件,最终执行 mountComponent:
// src/core/instance/init.js
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// ...
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
mountComponent 的核心是 updateComponent 方法:
vm._update(vm._render(), hydrating)
这行代码包含两个关键步骤:
vm._render()创建并返回 VNodevm._update()将 VNode 转为真实 DOM
updateComponent 会被传入渲染 Watcher,数据变化时触发重新渲染。初始化时会执行一次完成首次渲染。
构建 VNode(_render)
_render 方法负责构建组件的 VNode:
// src/core/instance/render.js
Vue.prototype._render = function () {
const { render, _parentVnode } = vm.$options
vnode = render.call(vm._renderProxy, vm.$createElement)
return vnode
}
在初始化时,Vue 会为实例绑定两个创建 VNode 的方法:
// 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)
}
vm._c:模板编译生成的 render 函数使用vm.$createElement:用户手写 render 函数使用
createElement
createElement 是创建 VNode 的核心方法,它会对参数进行处理后调用 _createElement:
// 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 首先会规范化 children,将其转为标准的 VNode 数组:
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
simpleNormalizeChildren:编译生成的 render 函数使用,只需简单扁平化normalizeChildren:用户手写的 render 函数使用,需要完整规范化
然后根据 tag 类型创建不同的 VNode:
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 是 Component 类型,直接创建组件 VNode
vnode = createComponent(tag, data, context, children)
}
生成真实 DOM(_update)
_update 方法接收 VNode,通过 __patch__ 方法生成真实 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)
}
}
__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]])
}
}
}
return function patch (oldVnode, vnode, hydrating, removeOnly) {
// ...
}
}
这里有两个重要对象:
nodeOps:封装的原生 DOM 操作方法modules:不同阶段的钩子函数(指令、ref 等)
patch
首次渲染时,oldVnode 是根节点的真实 DOM(如 id="app" 的 div):
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)
}
// 创建真实节点
createElm(vnode, insertedVnodeQueue, oldVnode.elm, ...)
// 移除旧节点
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
}
}
}
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)
}
}
}
整个过程是递归的,最深的子节点会先插入,所以插入顺序是”先子后父”。
最后移除旧节点,触发 insert 钩子函数,完成渲染。
关键点
- 渲染流程分为三步:
$mount挂载组件 →_render构建 VNode →_update生成真实 DOM createElement根据 tag 类型创建不同的 VNode,遇到组件会递归执行$mountchildren需要规范化为 VNode 数组,编译生成和手写 render 函数的处理方式不同createElm通过递归创建节点树,插入顺序是”先子后父”- Vue 初始渲染时不是替换根节点,而是在其后插入新节点再移除旧节点
目录