本文为拉勾网大前端高薪训练营第一期笔记
虚拟 DOM(Virtual DOM) 是使用 JavaScript 对象来描述 DOM,虚拟 DOM 的本质就是 JavaScript 对
象,使用 JavaScript 对象来描述 DOM 的结构。应用的各种状态变化首先作用于虚拟 DOM,最终映射
到 DOM。Vue.js 中的虚拟 DOM 借鉴了 Snabbdom,并添加了一些 Vue.js 中的特性,例如:指令和组
件机制。
Vue 1.x 中细粒度监测数据的变化,每一个属性对应一个 watcher,开销太大Vue 2.x 中每个组件对应一个 watcher,状态变化通知到组件,再引入虚拟 DOM 进行比对和渲染
使用虚拟 DOM,可以避免用户直接操作 DOM,开发过程关注在业务代码的实现,不需要关注如
何操作 DOM,从而提高开发效率
作为一个中间层可以跨平台,除了 Web 平台外,还支持 SSR、Weex。
关于性能方面,在首次渲染的时候肯定不如直接操作 DOM,因为要维护一层额外的虚拟 DOM,
如果后续有频繁操作 DOM 的操作,这个时候可能会有性能的提升,虚拟 DOM 在更新真实 DOM
之前会通过 Diff 算法对比新旧两个虚拟 DOM 树的差异,最终把差异更新到真实 DOM
演示 render 中的 h 函数
h 函数就是 createElement()
vm.$createElement(tag, data, children, normalizeChildren)
tag: 标签名称或者组件对象
data: 描述tag,可以设置DOM的属性或者标签的属性
children: tag中的文本内容或者子节点
核心属性
const vm = new Vue({
el: '#app',
render(h) {
// h(tag, data, children)
// return h('h1', this.msg)
// return h('h1', { domProps: { innerHTML: this.msg } })
// return h('h1', { attrs: { id: 'title' } }, this.msg)
const vnode = h(
'h1', {
attrs: { id: 'title' }
},
this.msg)
console.log(vnode)
return vnode
},
data: {
msg: 'Hello Vue'
}
})
功能
createElement() 函数,用来创建虚拟节点 (VNode),我们的 render 函数中的参数 h,就是
createElement()
render(h) {
// 此处的 h 就是 vm.$createElement
return h('h1', this.msg)
}
定义
在 vm._render() 中调用了,用户传递的或者编译生成的 render 函数,这个时候传递了 createElement
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// normalization is always applied for the public version, used in
// user-written render functions.
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
vm._c和vm.$createElement都调用createElement,区别只有最后一个参数
执行完 createElement 之后创建好了 VNode,把创建好的 VNode 传递给 vm._update() 继续处理
export function createElement(
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array {
// 判断第三个参数
// 如果 data 是数组或者原始值的话就是 children,实现类似函数重载的机制
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)
}
export function _createElement(
context: Component,
tag?: string | Class | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array {
if (isDef(data) && isDef((data: any).__ob__)) {
......
return createEmptyVNode()
}
// object syntax in v-bind
if (isDef(data) && isDef(data.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] } children.length = 0
}
// 去处理 children
if (normalizationType === ALWAYS_NORMALIZE) {
// 当手写 render 函数的时候调用
// 判断 children 的类型,如果是原始值的话转换成 VNode 的数组
// 如果是数组的话,继续处理数组中的元素
// 如果数组中的子元素又是数组(slot template),递归处理
// 如果连续两个节点都是字符串会合并文本节点
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
// 把二维数组转换为一维数组
// 如果 children 中有函数组件的话,函数组件会返回数组形式
// 这时候 children 就是一个二维数组,只需要把二维数组转换为一维数组
children = simpleNormalizeChildren(children)
}
let vnode, ns
// 判断 tag 是字符串还是组件
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
// 如果是浏览器的保留标签,创建对应的 VNode
if (config.isReservedTag(tag)) {
// platform built-in elements
vnode = new VNode(config.parsePlatformTagName(tag), data, children, undefined, undefined, context
)
} else if ((!data || !data.pre) && 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()
}
}
功能
内部调用 vm.patch() 把虚拟 DOM 转换成真实 DOM
定义
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
const restoreActiveInstance = setActiveInstance(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)
}
restoreActiveInstance()
// 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.
}
import { patch } from './patch'
Vue.prototype.__patch__ = inBrowser ? patch : noop
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 })
nodeOps是dom操作,modules包括attrs, kclass, events, domProps, style, transition,里面导出的是钩子函数
export function createPatchFunction(backend) {
let i, j
const cbs = {}
const { modules, nodeOps } = backend
// 把模块中的钩子函数全部设置到 cbs 中,将来统一触发 // cbs --> { 'create': [fn1, fn2], ... }
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) {
}
}
patch 函数执行过程
function patch(oldVnode, vnode, hydrating, removeOnly) {
// 如果没有 vnode 但是有 oldVnode,执行销毁的钩子函数
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
// 如果没有 oldVnode,创建 vnode 对应的真实 DOM
// empty mount (likely as component), create new root element
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
// 判断当前 oldVnode 是否是 DOM 元素(首次渲染)
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// 如果不是真实 DOM,并且两个 VNode 是 sameVnode,这个时候开始执行 Diff
// 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
}
// ......
// 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(parentElm, [oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
把 VNode 转换成真实 DOM,插入到 DOM 树上
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)) {
if (process.env.NODE_ENV !== 'production') {
if (data && data.pre) {
creatingElmInVPre++
}
if (isUnknownElement(vnode, creatingElmInVPre)) {
warn(
'Unknown custom element: <' + tag + '> - did you ' +
'register the component correctly? For recursive components, ' +
'make sure to provide the "name" option.',
vnode.context
)
}
}
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)
}
if (process.env.NODE_ENV !== 'production' && data && data.pre) {
creatingElmInVPre--
}
} 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 patchVnode(
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
// 如果新旧节点是完全相同的节点,直接返回
if (oldVnode === vnode) {
return
}
if (isDef(vnode.elm) && isDef(ownerArray)) {
// clone reused vnode
vnode = ownerArray[index] = cloneVNode(vnode)
}
const elm = vnode.elm = oldVnode.elm
// ......
// 触发 prepatch 钩子函数
let i
const data = vnode.data
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
// 获取新旧 VNode 的子节点
const oldCh = oldVnode.children
const ch = vnode.children
// 触发 update 钩子函数
if (isDef(data) && isPatchable(vnode)) {
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}
// 如果 vnode 没有 text 属性(说明有可能有子元素)
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
// 如果新旧节点都有子节点并且不相同,这时候对比和更新子节点
if (oldCh !== ch) updateChildren(elm, oldCh, ch,
insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(ch)
}
// 如果新节点有子节点,并且旧节点有 text
// 清空旧节点对应的真实 DOM 的文本内容
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
// 把新节点的子节点添转换成真实 DOM,添加到 elm
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
// 如果旧节点有子节点,新节点没有子节点
// 移除所有旧节点对应的真实 DOM
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
// 如果旧节点有 text,新节点没有子节点和 text
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
// 如果新节点有 text,并且和旧节点的 text 不同
// 直接把新节点的 text 更新到 DOM 上
nodeOps.setTextContent(elm, vnode.text)
}
// 触发 postpatch 钩子函数
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}
updateChildren 和 Snabbdom 中的 updateChildren 整体算法一致,这里就不再展开了。我们再来看
下它处理过程中 key 的作用,再 patch 函数中,调用 patchVnode 之前,会首先调用 sameVnode()判
断当前的新老 VNode 是否是相同节点,sameVnode() 中会首先判断 key 是否相同。
- {{value}}
在 updateChildren 中比较子节点的时候,会做三次更新 DOM 操作和一次插入 DOM 的操作
在 updateChildren 中比较子节点的时候,因为 oldVnode 的子节点的 b,c,d 和 newVnode 的 x,b,c 的key 相同,所以只做比较,没有更新 DOM 的操作,当遍历完毕后,会再把 x 插入到 DOM 上DOM 操作只有一次插入操作。