文章内容输出来源:拉勾教育大前端高薪训练营
虚拟DOM
使用虚拟DOM的好处 不需要直接操作DOM,提高开发效率
注意:虚拟DOM不一定会提高性能 首次渲染的时候会增加开销
h函数
h函数就是vm.$createElement(tag,data,children,normalizeChildren) vm.$createElement(tag,data,children,normalizeChildren)
tag 标签名称或组件对象
const vm = new Vue({
el: '#app',
data: {
msg: 'Hello Word'
},
render(h) {
let vnode = h('h1', {
attrs: {
id: 'title'
}
}, this.msg)
return vnode
}
})
VNode的核心属性
- tag
- data
- children
- text
- elm 对应真实DOM
- key
creteElement
在执行mountComponent过程中会实例化Watcher,new Watcher时会执行updateComponent,_update执行时会将_render()当做参数传递进来
// core/instance/lifecycle.js
...
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
...
_render会调用用户传入的render函数或模板编译生成的render函数,将vm.$createElement传递进来,所以上面代码中的h函数就是vm.$createElement
// core/instance/render.js
export function initRender (vm: Component) {
...
// 编译生成的render函数
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.
// 用户传入的render函数
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
...
}
...
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
if (_parentVnode) {
vm.$scopedSlots = normalizeScopedSlots(
_parentVnode.data.scopedSlots,
vm.$slots,
vm.$scopedSlots
)
}
// set parent vnode. this allows render functions to have access
// to the data on the placeholder node.
vm.$vnode = _parentVnode
// render self
let vnode
try {
// There's no need to maintain a stack because all render fns are called
// separately from one another. Nested component's render fns are called
// when parent component is patched.
currentRenderingInstance = vm
// 调用传入的render
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
handleError(e, vm, `render`)
// return error render result,
// or previous vnode to prevent render error causing blank component
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {
try {
vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
} catch (e) {
handleError(e, vm, `renderError`)
vnode = vm._vnode
}
} else {
vnode = vm._vnode
}
} finally {
currentRenderingInstance = null
}
// if the returned array contains only a single node, allow it
if (Array.isArray(vnode) && vnode.length === 1) {
vnode = vnode[0]
}
// return empty vnode in case the render function errored out
if (!(vnode instanceof VNode)) {
if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
warn(
'Multiple root nodes returned from render function. Render function ' +
'should return a single root node.',
vm
)
}
vnode = createEmptyVNode()
}
// set parent
vnode.parent = _parentVnode
return vnode
}
createElement就是对参数的处理,使$createElement既可以传递两个参数,也可以传入三个参数。然后调用_createElement
// core/vdom/create-element.js
const SIMPLE_NORMALIZE = 1
const ALWAYS_NORMALIZE = 2
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主要做了
- data是响应式对象返回空vnode处理
- 动态组件处理
- 开发环境key不是原始值处理
- children处理 将children转化为一维数组
- 创建vnode
- 如果tag是string
- tag是html保留标签
- 自定义组件调用createComponent
- 自定义标签
- 组件调用createComponent
- vnode处理
// core/vdom/create-element.js
export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
// 响应式对象返回空vnode
if (isDef(data) && isDef((data: any).__ob__)) {
process.env.NODE_ENV !== 'production' && warn(
`Avoid using observed data object as vnode data: ${JSON.stringify(data)}n` +
'Always create fresh vnode data objects in each render!',
context
)
return createEmptyVNode()
}
// 动态组件
// <component v-bind:is="currentTabComponent"></component>
// 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()
}
// warn against non-primitive key
if (process.env.NODE_ENV !== 'production' &&
isDef(data) && isDef(data.key) && !isPrimitive(data.key)
) {
if (!__WEEX__ || !('@binding' in data.key)) {
warn(
'Avoid using non-primitive value as key, ' +
'use string/number value instead.',
context
)
}
}
// 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处理
// 用户传入的render
if (normalizationType === ALWAYS_NORMALIZE) {
// children转化为一维数组
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)
// html保留标签
if (config.isReservedTag(tag)) {
// platform built-in elements
if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn)) {
warn(
`The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
context
)
}
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()
}
}
update
// 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 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.
}
patch
patch函数是跟平台相关的
// platforms/web/runtime/index.js
import { patch } from './patch'
Vue.prototype.__patch__ = inBrowser ? patch : noop
patch函数调用了createPatchFunction,传入了跟平台相关的模块和平台无关的模块
// platforms/web/runtime/patch.js
import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
// 平台无关模块 ref directives
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 })
createPatchFunction首先初始化模块的钩子函数
// 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){
...
}
createPatchFunction返回了patch方法
patch方法主要执行过程
- 判断vnode是否存在,如果不存在,而oldVnode存在 ,就执行oldVnode的destory钩子函数,返回,
- oldVnode不存在,创建vnode对应的真实dom(没有插入到页面DOM树上)
- oldVnode存在
- 判断oldVnode是否是真实DOM
- oldVnode如果不存在真实DOM,且oldVnode具有相同vnode,调用patchVnode
- oldVnode是真实DOM, 将oldVnode转化为对应的Vnode对象,然后创建vnode对应的真实DOM,移除oldVnode
- 最后调用invokeInsertHook,并返回vnode对应的真实DOM
// core/vdom/patch.js
export function createPatchFunction (backend) {
...
return function patch (oldVnode, vnode, hydrating, removeOnly){
// 新vnode不存在
if (isUndef(vnode)) {
// 旧vnode存在,执行destory钩子函数
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
//记录vnode创建的DOM元素是否添加到DOM树上 true是表示没有
let isInitialPatch = false
const insertedVnodeQueue = []
// 旧vnode不存在
// 创建vnode对应的真实dom(不插入到页面上)
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
//判断旧节点是否存在真实DOM,存在说明oldVnode是真实的DOM元素
const isRealElement = isDef(oldVnode.nodeType)
// oldVnode不是真实DOM并且oldVnode和vnode是相同vnode
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.
...
// 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
// 创建vnode对应的DOM元素
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,//null并不会挂载带DOM树上
nodeOps.nextSibling(oldElm)
)
// update parent placeholder node element, recursively
...
// destroy old node
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
}
invokeInsertHook是对vnode的insert钩子函数的处理
// core/vdom/patch.js
function invokeInsertHook (vnode, queue, initial) {
// delay insert hooks for component root nodes, invoke them after the
// element is really inserted
//initial为true并且vnode.pareent存在,将insertedVnodeQueue保存在pendingInsert上
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])
}
}
}
createElm
createElm执行过程
- 如果是组件,直接返回创建组件
- 如果vnode的tag存在
- 创建vnode标签对应的DOM
- 创建vnode对应的子节点的DOM,如果data存在,调用invokeCreateHooks
- 将vnode插入到DOM树上
- tag不存在
- 如果是注释节点,创建注释节点插入DOM树上
// core/vdom/patch.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)) {
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标签对应的DOM
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)
}
// 将vnode插入到dom树上
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)
}
}
createChildren
// 创建vnode对应 的子节点并添加到vnode对应的DOM上
function createChildren (vnode, children, insertedVnodeQueue) {
// 如果children是数组
if (Array.isArray(children)) {
if (process.env.NODE_ENV !== 'production') {
// 检查是否存在相同key
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)) {
// 如果vnode.是原始值text
nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
}
}
invokeCreateHooks
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)//触发hook中的create钩子函数
if (isDef(i.insert)) insertedVnodeQueue.push(vnode)//将hook中的insert钩子函数添加到insertedVnodeQueue中
}
}
patchVnode
在前面patch方法中,当oldVnode和vnode是sameVnode(key,tag等相同)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
}
patchVnode的主要执行过程
- 触发用户传入的prepatch钩子函数
- 触发模块(更新节点的属性样式事件等)和用户传入的钩子函数
- 判断vnode有没有text属性
- 没有text属性
- 如果新旧节点都存在子节点,调用updateChildren
- 新节点有子节点,旧节点没有
- 判断oldVnode有没有text属性,有清空text属性
- 调用addVnodes把创建子节点对应的DOM并插入进来
- 老节点有子节点,新节点没有子节点,调用removeVnodes删除老节点的子节点
- 老节点有text属性,清空text属性
- 有text属性
- vnode和oldVnode的text属性不相等,直接修改文本内容
- 触发用户传入的postpatch钩子函数
// core/vdom/patch.js
function patchVnode (
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
if (oldVnode === vnode) {
return
}
...
// 触发用户传入的prepatch钩子函数
let i
const data = vnode.data
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
const oldCh = oldVnode.children
const ch = vnode.children
if (isDef(data) && isPatchable(vnode)) {
// 执行模块update钩子,更新节点的属性样式事件等
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
// 用户传入的update钩子函数
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)
}
// oldVnode有text属性,清空oldVnode的DOM的text
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
// 把子节点转化为真实DOM添加到vnode对应的DOM上
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
// oldVnode存在子节点,删除子节点对应的DOM元素
removeVnodes(oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
// oldVnode有text属性 新节点没有文本也没有子节点
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
// vnode有text属性
// 修改文本
nodeOps.setTextContent(elm, vnode.text)
}
// 执行postpatch钩子函数
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}
updateChildren整体分析
vue.js中的updateChildren的方法和snabbdom中的updateChildren方法相似
主要比较过程
oldStartVnode/newStartVnode
- 新旧节点从开始位置开始比较
- 旧节点第一个元素与新节点第一个元素进行使用sameVnode比较,如果相同调用pathVnode对比和更新节点
- 比较完成后索引后移,往后进行比较
- oldEndVnode/newEndVnode(效果与上面相似,只不过是从结束位置开始比较,如果相同,比较完成后索引左移)
- 新旧节点从结束位置开始比较
- 如果相同调用pathVnode对比和更新节点
- 比较完成后索引前移,往前移动进行比较
- oldStartVnode/newEndVnode
- 旧节点第一个节点、新节点最后一个节点开始比较
- 如果相同就把如果相同调用pathVnode对比和更新节点,再把旧节点的第一个节点移动到最后
- 旧节点索引后移,新节点索引前移,进行比较
- oldEndVnode/newStartVnode
- 旧节点第一个节点、新节点的第一个节点开始比较
- 如果相同就把如果相同调用pathVnode对比和更新节点,再把旧节点的最后一个节点移动到最前面
- 更新索引,旧节点索引前移,新节点索引后移
- 如果上面四种情况都不满足
- 在老节点中查找子节点newStartVnode对应的vnode
- 没有就创建newStartVnode对应DOM并插入到oldStartVnode对应的DOM的前面
- 存在,比较newStartVnode在老节点中对应的vnode elmToMove的sel属性是否相同
- 不相同,直接创建(同上)
- 相同,调用patchVnode,再将elmToMove对应的DOM插入到oldStartVnode对应的DOM前面
- 上面执行完毕后,如果新老节点至少有一个没有遍历完成
- 如果老节点先遍历完成,说明新节点没有遍历完,创建新节点还没有遍历的节点(从newStartIdx,newEndIdx)
- 否则说明老节点有多余的节点,删除老节点还没有遍历的节点(oldStartIdx, oldEndIdx)
源码
function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx, idxInOld, vnodeToMove, refElm
// removeOnly is a special flag used only by <transition-group>
// to ensure removed elements stay in correct relative positions
// during leaving transitions
const canMove = !removeOnly
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(newCh)
}
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 当oldStartVnode不存在, ++oldStartIdx索引右移
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
// 当oldEndVnode不存在, --oldStartIdx索引左移
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// 新旧开始节点是sameVnode,调用patchVnode,新旧节点的索引都右移
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// 新旧结束节点是sameVnode
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
// 旧开始节点和新结束节点是sameVnode
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
// 移动旧结束节点对应的DOM到最后
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
// 旧开始索引右移,新结束索引左移
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
// 旧结束和新开始节点是sameVnode
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
// 移动旧结束节点对应的DON到最前面
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
// 旧结束节点索引左移,新开始节点右移
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
//包含所有旧节点的对象从 oldStartIdx到oldEndIdx
// key为oldVnode的子节点的key, 值为索引
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
// 查找newStartVnode在oldVnode的位置
// 如果新开始节点存在key,直接通过key从newStartVnode获取在老节点中对应的索引
// 不存在key ,遍历老节点所有节点查找是否存在于newStartVnode是sameVnode,存在就返回索引
idxInOld = isDef(newStartVnode.key) ?
oldKeyToIdx[newStartVnode.key] :
findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
// 索引不存在,直接创建newStartVnode对应的DOM元素
if (isUndef(idxInOld)) { // New element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
// 存在,获取oldVnode对应的vnode
vnodeToMove = oldCh[idxInOld]
// 判断是否是sameVnode 是,调用patchVnode 再将找到的vnode放到最前面
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
// 将oldCh设置为undefined,防止vnodeToMove对应的DOM被removeVnodes被删掉
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// same key but different element. treat as new element
// 不是sameVnode,创建newStartVnode对应的DOM
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
// 索引右移
newStartVnode = newCh[++newStartIdx]
}
}
// 旧节点先遍历完成,创建没有遍历完newCh对应的DOM元素
if (oldStartIdx > oldEndIdx) {
// 获取参考节点
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
// 新节点先遍历完成,将未遍历完的老节点删除掉
removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}
}
带key的好处
使用key可以让它能够跟踪每个节点的身份,在进行比较的时候,key值不同是就不会进行子节点的比较,会提高diff比较效率,可以避免频繁更新dom元素
比如下面例子,*如果不带key,sameVnode返回的值一直就是true,就会调用patchVnode对比vnode的差异,导致频繁更新DOM
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<ul>
<li v-for="item in arr" :key="item">{{item}}</li>
</ul>
<button @click="add">button</button>
</div>
</body>
<script src="../dist/vue.js"></script>
<script>
let id = 4
const vm = new Vue({
el: '#app',
data: {
msg: 'Hello Word',
arr: [1, 2, 3]
},
methods: {
add() {
this.arr.splice(0, 0, id++)
}
}
})
</script>
</html>
- 不设置key总共会更新3次dom,创建一次dom
- 设置key值创建了一次dom