Write By CS逍遥剑仙
我的主页: www.csxiaoyao.com
GitHub: github.com/csxiaoyaojianxian
Email: sunjianfeng@csxiaoyao.com
QQ: 1724338257
Vue是数据驱动的MVVM框架,视图是由数据驱动生成的,因此对视图的修改不是通过操作 DOM,而是通过修改数据,相比传统使用jQuery的前端开发,能够大大简化代码量,尤其在交互逻辑复杂的情况下,减少DOM操作,直接操作数据会让代码的逻辑变的非常清晰、利于维护。
真实DOM存储的节点信息非常多,频繁的DOM操作会带来明显的性能问题,虚拟DOM能有效解决性能问题。在Vue中,虚拟DOM由Vue中$mount
实例方法调用mountComponent
函数生成,vm._render
负责创建虚拟DOM,vm._update
负责渲染虚拟DOM。
虚拟DOM的渲染是按照下面的流程运行的,后面会详细介绍。
(1) new Vue ==> (2) init ==> (3) $mount ==> (4) compile ==> (5) render ==> (6) vnode ==> (7) patch ==> (8) DOM
Vue通过 $mount
实例方法挂载 vm
,$mount
方法的实现和平台、构建方式都相关,因此在项目中有多处实现,其中,带 compiler
版本的 $mount
可以在浏览器中使用,有利于对源码进行调试分析,作为学习,应该从带 compiler
的版本入手,具体的实现在 src/platform/web/entry-runtime-with-compiler.js 中。
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
/* istanbul ignore if */
if (el === document.body || el === document.documentElement) {
process.env.NODE_ENV !== 'production' && warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead.`
)
return this
}
const options = this.$options
// resolve template/el and convert to render function
if (!options.render) {
...
const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
...
}
return mount.call(this, el, hydrating)
}
首先对原型上的 $mount
方法进行缓存,目的是为了重新定义该方法,以便在 $mount
方法执行前执行平台差异的代码。以web平台为例,传入两个参数:el
、hydrating
(服务端渲染相关,此处无需传入),重新定义的$mount
执行了一些平台相关的额外操作,首先限制 el
不能为 body
、html
这类根节点,接着,检查是否有 render
方法,如果没有则会把 el
或者 template
字符串转换成 render
方法,最后调用 compileToFunctions
方法实现render在线编译。
原型上的 $mount
方法在 src/platform/web/runtime/index.js 中定义,$mount
方法实际会调用定义在 src/core/instance/lifecycle.js 中的 mountComponent
方法进行挂载。
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
...
}
callHook(vm, 'beforeMount')
let updateComponent
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
const name = vm._name
const id = vm._uid
const startTag = `vue-perf-start:${id}`
const endTag = `vue-perf-end:${id}`
mark(startTag)
const vnode = vm._render()
mark(endTag)
measure(`vue ${name} render`, startTag, endTag)
mark(startTag)
vm._update(vnode, hydrating)
mark(endTag)
measure(`vue ${name} patch`, startTag, endTag)
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
mountComponent
先调用 vm._render
方法先生成虚拟 Node,再实例化一个Watcher
,由此看出,渲染最核心的 2 个方法:vm._render
和 vm._update
。
Vue 的 _render
方法是实例的一个私有方法,可以把实例渲染成一个虚拟 Node,定义在 src/core/instance/render.js 中。平时开发工作中很少手写 render
,大多是写 template
模板,在上面的 mounted
方法中会把 template
编译成 render
方法。VDOM是由VNODE组成的树形结构,_render
函数中创建VNODE的实现是通过调用 createElement
方法,定义在 src/core/vdom/create-elemenet.js 中
Virtual DOM 的节点定义的描述在 src/core/vdom/vnode.js 中,vnode.js 详细描述了VNODE的结构,比真实DOM结构简化了很多,Vue 的 Virtual DOM 是借鉴了开源库 snabbdom 的实现。除自身的数据结构的定义,映射到真实 DOM 要经历 VNode 的 create、diff、patch 等过程。
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 方法的最后调用了 _createElement 私有方法。
exporte function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
...
}
_createElement 方法有 5 个参数,context
表示 VNode 的上下文环境;tag
表示标签;data
表示 VNode 的数据,它是一个 VNodeData
类型,定义在 flow/vnode.js 中;children
表示当前 VNode 的子节点,将会被规范为标准的 VNode 数组;normalizationType
表示子节点规范的类型,类型不同规范的方法不同,由 render
函数是编译生成的还是用户手写决定。createElement 中最关键的两个流程是 normalizeChildren 和 VNODE 创建。
Virtual DOM 是树状结构,每一个 VNode 可能会有若干个子节点,并且这些子节点也为 VNode 类型,因此需要在 createElement 过程中将传入的 any 类型的 children 参数规范化为 VNODE。
_createElement 会根据传入的 normalizationType
参数的不同,分别调用 normalizeChildren(children)
和 simpleNormalizeChildren(children)
方法,二者都定义在 src/core/vdom/helpers/normalzie-children.js 中。simpleNormalizeChildren
是当render
由函数是编译生成时调用,大部分编译生成的 children
已是 VNode 类型的,除了 functional component
函数式组件返回的是一个数组而不是一个根节点,所以需要通过 Array.prototype.concat
方法把 children
数组变成深度只有一层的一维数组。normalizeChildren
方法存在两种调用场景,一是 render
函数由用户手写,当 children
只有一个节点时,Vue调用 createTextVNode
创建一个文本节点的 VNode;另一场景是当编译 slot
、v-for
的时候会产生嵌套数组的情况,会调用 normalizeArrayChildren
方法进行处理。
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
}
export function normalizeChildren (children: any): ?Array<VNode> {
return isPrimitive(children)
? [createTextVNode(children)]
: Array.isArray(children)
? normalizeArrayChildren(children)
: undefined
}
规范化 children
后便可以创建 VNode 的实例。如果是内置节点,则直接创建普通 VNode,如果是为已注册的组件名,则通过 createComponent
创建一个组件类型的 VNode,否则创建一个未知的标签的 VNode。
let vnode, ns
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
vnode = createComponent(Ctor, data, context, children, tag)
} else {
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
vnode = createComponent(tag, data, context, children)
}
Vue 的 _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
const prevActiveInstance = activeInstance
activeInstance = vm
vm._vnode = vnode
if (!prevVnode) {
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
vm.$el = vm.__patch__(prevVnode, vnode)
}
activeInstance = prevActiveInstance
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
}
_update
的核心是调用 vm.__patch__
方法,定义在 src/platforms/web/runtime/index.js
中,不同平台的定义不同,浏览器端渲染的 patch
方法定义在 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'
const modules = platformModules.concat(baseModules)
export const patch: Function = createPatchFunction({ nodeOps, modules })
patch
方法的定义是调用 createPatchFunction
方法的返回值,传入 nodeOps
参数和 modules
参数。其中,nodeOps
封装了一系列 DOM 操作方法,modules
定义了一些模块的钩子函数的实现。
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
首次渲染执行 patch
函数的时候,传入的 vm.$el
是例子中形如<div id="app">
的 DOM 对象, vm.$el
的赋值在之前 mountComponent
函数中完成,vnode
是调用 render
函数的返回值,hydrating
在非服务端渲染时为 false,removeOnly
为 false。patch的关键操作如下:
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
} else {
if (isRealElement) {
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
...
oldVnode = emptyNodeAt(oldVnode)
}
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
createElm(
vnode,
insertedVnodeQueue,
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
}
emptyNodeAt
方法把 oldVnode
转换成 VNode
对象,然后再调用 createElm
方法。createElm
的作用是通过虚拟节点创建真实的 DOM 并插入到它的父节点中。 对于创建真实DOM子元素,调用了createChildren
方法。
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)))
}
}
createChildren
遍历子虚拟节点,递归调用 createElm
实现深度优先遍历。接着再调用 invokeCreateHooks
方法执行所有的 create 的钩子并把 vnode
push 到 insertedVnodeQueue
队列中。
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
方法把 DOM
插入到父节点中,因为是递归调用,子元素会优先调用 insert
。insert
方法定义在 src/core/vdom/patch.js
上,最终使用原生DOM操作进行了渲染,实际上整个过程就是递归创建了一个完整的 DOM 树并插入到 Body 上。在 createElm
过程中,如果 vnode
节点不包含 tag
,可能是注释或者纯文本节点,可以直接插入到父元素中。
function insert (parent, elm, ref) {
if (isDef(parent)) {
if (isDef(ref)) {
if (ref.parentNode === parent) {
nodeOps.insertBefore(parent, elm, ref)
}
} else {
nodeOps.appendChild(parent, elm)
}
}
}
export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) {
parentNode.insertBefore(newNode, referenceNode)
}
export function appendChild (node: Node, child: Node) {
node.appendChild(child)
}
至此,虚拟DOM渲染为真实DOM。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。