前言
上面六篇文章对Vue的基本内容进行了讲解,包含了声明和调用,以及Vue的observe的实现,本章将开始进行mount的讲解,包含编译和update、patch等;篇幅较多,耐心看完。
mount
mount函数之前在声明相关简单提到过,在这里咱们进行详细的讲解,正文从下面开始。
$mount
来看下完整的$mount的代码:
// @file src/platforms/web/entry-runtime-with-compiler.js
// 保存mount
const mount = Vue.prototype.$mount
// 重写$mount函数
Vue.prototype.$mount = function (el?: string | Element, hydrating?: boolean): Component {
el = el && query(el)
// 对el进行判断,不允许在body和根元素进行实例化Vue
if (el === document.body || el === document.documentElement) {
return this
}
const options = this.$options
if (!options.render) {
let template = options.template
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
template = idToTemplate(template)
}
} else if (template.nodeType) {
template = template.innerHTML
} else {
return this
}
} else if (el) {
template = getOuterHTML(el)
}
if (template) {
const {render, staticRenderFns} = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
}
}
return mount.call(this, el, hydrating)
}
上面代码先是对el进行了判断,不允许在body和根元素进行实例化Vue,然后开始生成render和staticRenderFns并挂到options对象上面,先来看下render和staticRenderFns是怎么生成的。
- 首先获取到的是template(用到了nodeType);
- 然后调用的是compileToFunctions获取render和staticRenderFns;
- 最后调用了保存的mount,保存的mount在这:
// @file src/platforms/web/runtime/index.js
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
最终调用的是mountComponent函数。
compileToFunctions
compileToFunctions这个函数嵌套的比较深,也就是比较绕;其实就是用函数作为参数调用函数,最后返回函数的过程。初步的获取是在
// @file src/platforms/web/compiler/index.js
import { baseOptions } from './options'
import { createCompiler } from 'compiler/index'
const { compile, compileToFunctions } = createCompiler(baseOptions)
export { compile, compileToFunctions }
此处调用createCompiler生成了compile和compileToFunctions;关于baseOptions的内容如下图所示:
然后咱们来看下createCompiler:
// @file src/compiler/index.js
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
// 根据template生成抽象语法树
const ast = parse(template.trim(), options)
if (options.optimize !== false) {
// 优化抽象语法树
optimize(ast, options)
}
// 根据抽象语法树生成render和staticRenderFns函数
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
createCompiler的获取调用了createCompilerCreator,createCompilerCreator接收了一个函数作为参数,此函数就是Vue最基础的编译函数baseCompile,该函数的返回结果,其实也就是咱们上面$mount函数里面需要使用的render和staticRenderFns。
// @file src/compiler/create-compiler.js
export function createCompilerCreator (baseCompile: Function): Function {
return function createCompiler (baseOptions: CompilerOptions) {
function compile (
template: string,
options?: CompilerOptions
): CompiledResult {
const finalOptions = Object.create(baseOptions)
const errors = []
const tips = []
let warn = (msg, range, tip) => {
(tip ? tips : errors).push(msg)
}
if (options) {
// merge custom modules
if (options.modules) {
finalOptions.modules =
(baseOptions.modules || []).concat(options.modules)
}
// merge custom directives
if (options.directives) {
finalOptions.directives = extend(
Object.create(baseOptions.directives || null),
options.directives
)
}
// copy other options
for (const key in options) {
if (key !== 'modules' && key !== 'directives') {
finalOptions[key] = options[key]
}
}
}
finalOptions.warn = warn
const compiled = baseCompile(template.trim(), finalOptions)
// compiled = {
// ast,
// render: code.render,
// staticRenderFns: code.staticRenderFns
// }
compiled.errors = errors
compiled.tips = tips
return compiled
}
return {
compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
}
}
上面代码就是createCompilerCreator函数的源代码,先解读下compile函数:
- 定义finalOptions;
- 对options.modules、options.directives进行合并,对options的其他内容进行复制;
- 调用基础编译函数进行生成前面代码块部分讲到的render和staticRenderFns;
- 最终把compiled对象返回;
createCompilerCreator其实就两个部分:
- 对compile函数的定义;
- 返回compile和compileToFunctions;
compileToFunctions的定义是在createCompileToFunctionFn里面进行的,接收编译函数作为参数;
// @file src/compiler/to-function.js
export function createCompileToFunctionFn (compile: Function): Function {
// 闭包来做缓存
const cache = Object.create(null)
return function compileToFunctions (
template: string,
options?: CompilerOptions,
vm?: Component
): CompiledFunctionResult {
options = extend({}, options)
delete options.warn
// 获取缓存所使用的key
const key = options.delimiters ? String(options.delimiters) + template : template
// 如有缓存则直接返回
if (cache[key]) {
return cache[key]
}
// compile,也就是上一部分代码compile的返回结果
const compiled = compile(template, options)
// 把字符串函数转为函数
const res = {}
const fnGenErrors = []
res.render = createFunction(compiled.render, fnGenErrors)
res.staticRenderFns = compiled.staticRenderFns.map(code => {
return createFunction(code, fnGenErrors)
})
// 缓存,且返回
return (cache[key] = res)
}
}
上面的代码,是最后一步进行的函数,把最初咱们要使用的render和staticRenderFns,进行赋值给Vue的options;简单画了下流程图:
mountComponent
上面讲完了render函数和staticRenderFns函数的获取过程,接下来咱们进行讲解mountComponent部分的代码。
// @file src/core/instance/lifeCycle.js
export function mountComponent (vm: Component, el: ?Element, hydrating?: boolean): Component {
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
}
// 调用钩子函数beforeMount
callHook(vm, 'beforeMount')
let updateComponent
updateComponent = () => {
const vnode = vm._render();
vm._update(vnode, hydrating)
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
// 调用钩子函数beforeUpdate
callHook(vm, 'beforeUpdate')
}
}
}, true)
hydrating = false
if (vm.$vnode == null) {
vm._isMounted = true
// 调用钩子函数mounted
callHook(vm, 'mounted')
}
return vm
}
看上面的代码:
- 首先判断了render,咱们经过上面的分析可知,咱们是有render的;
- 调用钩子函数beforeMount;
- 定义了updateComponent函数,也就是watcher里面的getter;
- 新建一个Watcher,数据发生变化,触发updateComponent,触发之前会调用before函数,before里面调用钩子函数beforeUpdate,最后一个参数是为true,即vm._watcher为当前Watcher;
- 调用钩子函数mounted;
updateComponent函数会在new Watcher的时候进行调用;先是通过调用_render获取到vnode;然后哦调用_update函数,把vnode渲染到页面上面。
_update
先来看下调用_update之前,调用_render获取vnode的函数:
_render
// @file src/core/instance/render.js
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
)
}
vm.$vnode = _parentVnode
let vnode
try {
currentRenderingInstance = vm
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
handleError(e, vm, `render`)
vnode = vm._vnode
} finally {
currentRenderingInstance = null
}
if (Array.isArray(vnode) && vnode.length === 1) {
vnode = vnode[0]
}
if (!(vnode instanceof VNode)) {
vnode = createEmptyVNode()
}
vnode.parent = _parentVnode
return vnode
}
函数_render的定义是在render文件里面进行定义的,此前在Vue声明里面提到过这部分的定义;下面咱们来看看执行:
- 先从$options获取render和_parentVnode,render的定义就在文章开头的mount函数里面;
- 判断插槽;
- 调用render,参数vm._renderProxy之前也在Vue解读二讲过,其实就是vm本身,vm.$createElement是Vue的底层函数;
- 最后返回vnode;
接下来也就是调用_update了,来看下update函数的定义:
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
if (!prevVnode) {
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false)
} else {
vm.$el = vm.__patch__(prevVnode, vnode)
}
restoreActiveInstance()
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
// $vnode = parentVnode
// $parent = parent
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
}
上面就是_update的源码,核心函数:patch,先分析下_update的源码;
- 变量vm定义,指向this;
- 变量定义prevEl,指向当前$el;
- 变量定义prevVNode,指向之前的vnode,也就是_vnode;
- 设置当前活动的实例为当前vm,获取设置函数;
- 保存最新的vnode为之前的vnode,方便下次获取之前的vnode;
- 如果是第一次渲染,则调用vm.patch(vm.$el, vnode, hydrating, false);如果是diff渲染,则调用vm.patch(prevVnode, vnode);
- 调用设置函数;
- 把之前的元素的__vue__置为空,释放内存;
- 重新为当前元素定义__vue__为当前实例;
- 判断parentVnode和parent的之前的vnode是否相同,相同则把当前el直接赋值给parent的el;
patch
patch是与平台相关的函数,先看下暴露的入口:
// @file src/platforms/web/runtime/patch.js
export const patch: Function = createPatchFunction({ nodeOps, modules })
调用的是createPatchFunction函数,根据名字就可以知道,是创建patch函数的一个函数,参数是个对象,包含两个值:
- nodeOps:与平台相关的一些方法,如appendChild、insertBefore、removeChild、createElement等一些原生浏览器操作dom的方法封装; modules:是平台相关模块和基础模块的合并,包含attrs、class、events、style、domProps、transtion、directives、ref等;主要是这些指令的安装、更新和销毁的实现;
createPatchFunction函数的篇幅太长,最终是返回了patch函数,来看下patch的源码,点滴开始,慢慢啃:
return function patch (oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
// 第一次,无oldVnode,直接新建
if (isUndef(oldVnode)) {
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// dom diff
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
if (isRealElement) {
//
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
if (isTrue(hydrating)) {
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
invokeInsertHook(vnode, insertedVnodeQueue, true)
return oldVnode
}
}
//
oldVnode = emptyNodeAt(oldVnode)
}
//
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// create new node
createElm(
vnode,
insertedVnodeQueue,
//
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
//
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)
}
//
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
}
}
//
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
参数解读:
- oldVnode:上一次更新的vnode或者是第一次生成时传递进来的el(dom)节点;
- vnode:本次要更新的vnode;
- hydrating:是否有副作用;
- removeOnly:是否只能进行删除操作
源码解读:
- 如果无vnode,有oldVnode,则证明是在销毁当前实例,调用destroy钩子函数,直接返回;
- 定义变量isInitialPatch,是否是第一次比较;
- 定义insertedVnodeQueue数组,存储已经插入到vnode的对象,后面会调用这些插入进去的钩子函数;
- 判断,如果oldVnode未定义,则把isInitialPatch置为true,同时调用createElm新建一个根元素;
- 如果oldVnode已经定义,则判断oldVnode的nodeType,vnode类型的是无nodeType的,nodeType有值的话,则说明是原生的node,否则就是vnode =》isRealElement;
- 是vnode的话,则进行sameVnode进行判断是否是相同的vnode;是,则进行patch操作,调用patchVnode;
- 如果不是vnode,也就是是真实的node,则会check服务端渲染等,最终调用emptyNodeAt根据节点创建虚拟节点;
- 调用createElm创建新的节点;
- 树行遍历祖先,调用destroy、create、insert等hook;
- 删除老的节点,
- 调用insertHook;
这部分代码其实并不是重点,重点在patchVnode和updateChildren部分,咱们先来看下patchVnode的源码:
// @file src/core/vdom/patch.js
function patchVnode(oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly) {
// 如果强等,则是同一个对象,并无发生变化,不做任何操作,直接返回
if (oldVnode === vnode) {
return
}
// 如果vnode的元素不是null或undefined,并且ownerArray也不是null或undefined
if (isDef(vnode.elm) && isDef(ownerArray)) {
// 对vnode进行克隆
vnode = ownerArray[index] = cloneVNode(vnode)
}
// 使最新的vnode的根节点引用到现在的vnode的根节点
const elm = vnode.elm = oldVnode.elm
let i
// 获取将要更新的vnode的虚拟dom数据
const data = vnode.data
// 调用prepatch hook
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
// 现在的子节点
const oldCh = oldVnode.children
// 将要更新的vnode的子节点
const ch = vnode.children
// 虚拟dom数据有值,且vnode有_vnode;
if (isDef(data) && isPatchable(vnode)) {
// 遍历调用上文提到的modules的update钩子函数
for (i = 0; i < cbs.update.length; ++i)
cbs.update[i](oldVnode, vnode)
// 调用当前dom的update钩子函数
if (isDef(i = data.hook) && isDef(i = i.update))
i(oldVnode, vnode)
}
// 如果新vnode不是文本节点
if (isUndef(vnode.text)) {
// 对比的新老node的子节点都存在,且不相等,则调用updateChildren;
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) { // 如果只定义了新子节点
if (isDef(oldVnode.text)) // 如果是文本节点,则更新文本节点
nodeOps.setTextContent(elm, '')
// 添加到insertedVnode队列
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) { // 如果只定义了老节点,直接进行删除操作
removeVnodes(oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {// 如果老节点是个文本节点,则更新文本节点
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {// 如果是文本节点,则进行比较,判断是否相同,不相同则进行更新
nodeOps.setTextContent(elm, vnode.text)
}
// 调用postpatch钩子函数
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}
上面是patchVnode的部分源码,都打好了注释,可以一步步进行阅读,这不是代码,在实际项目的开发中,其实也不会真正的涉及到,但是也是核心之一,最核心的还是updateChildren的内容.
updateChildren
// @file src/core/vdom/patch.js
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
const canMove = !removeOnly
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) { // 无旧开头
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) { // 无旧结尾
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) { // 比较两个开头
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) { // 比较两个结尾
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // 比较最远的;Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) { // 比较最近的;Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
if (isUndef(oldKeyToIdx)) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
}
if (isDef(newStartVnode.key)) {
idxInOld = oldKeyToIdx[newStartVnode.key]
} else {
idxInOld = findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
}
if (isUndef(idxInOld)) { // New element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
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)
}
}
来看看上面的代码:
- 定义了几个变量,然后while循环,就这么简单;but这几个变量尤为重要;
-
- 先从old来看,oldStartIdx为老树开始的索引;oldEndIdx为老树最后的索引;相应的,就有索引所对应的节点,oldStartVnode和oldEndVnode;
-
- 再来看new,newStartIdx为新树开始的索引;newEndIdx为新树最后的索引;相应的,就有索引对应的新的节点,newStartVnode和newEndVnode;
-
- oldKeyToIdx:老树,索引下的Vnode对应的key,是个map;
-
- idxInOld:新树的index在老树里面的索引;
-
- vnodeToMove:需要移动位置或者插入的vnode;
-
- refElm:存储需要插入到vnode中的elm;
- 变量定义完后,就开始遍历了,
- 先对oldStartVnode进行判断,无oldStartVnode则oldStartIdx右移一个单位,重新循环;
- 再对oldEndVnode进行判断,无oldEndVnode,则oldEndIdx左移一个单位,重新循环;
- 比较新老两个树的开头,oldStartVnode和newStartVnode,相同则调用上面的patchVnode,两个startIdx右移一个单位,重新循环;
- 比较新老两个树的结尾,oldEndVnode和newEndVnode,相同则调用上面的patchVnode,两个endIdx左移一个单位,重新循环;
- 比较老开和新尾,oldStartVnode和newEndVnode,相同则调用上面的patchVnode,同时把oldStartVnode插入到老树的下一个节点的前面;
- 比较老尾和新开,oldEndVnode和newStartVnode,相同则调用上面的patchVnode,同时把oldEndVnode插入到老树的当前开始节点的前面;
- 剩下的则是进行其他情况的判断了,不会拿idx来进行操作了;
- 先是获取老树的keys,获取一个map,存储到oldKeyToIdx;
- 如果新树的当前节点newStartVnode定义了key,则从上面的map中获取到idxInOld位置信息;如果未定义key,则调用findIdxInOld进行查找idxInOld信息,此处说明,对元素定义key是最便捷快速的查找;
- 如果idxInOld为undefined,也就是未找到,newStartVnode在老树里面是不存在的,则调用createElm创建;
- 如果找到了idxInOld,也就是说newStartVnode在老树里面是存在的,位置为idxInOld,则根据位置获取到了vnodeToMove,则对vnodeToMove和newStartVnode进行比较,如果相同则调用上面的patchVnode,同时把老树的idxInOld置为undefined,把vnodeToMove插入到老树的oldStartVnode的前面;
- 如果找到了,但是不相同,则还是得调用createElm创建;
- 此时其他情况的查找结束,newStartIdx右移一个单位,重新循环;
- 当oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx的时候跳出循环;
综上分析,跳出循环只有三种情况:
- 都循环完了,结束;
- oldStartIdx > oldEndIdx,老树循环结束,新树未循环完,此时,就需要把新树剩余部分,加上去,调用addVnodes;
- newStartIdx > newEndIdx,新树循环结束,老树未循环完,此时,就需要把老树剩余部分去掉,调用removeVnodes;
至此,updateChildren讲解完成。
dom diff 示例
假设咱们有一个dom是这样的:[a, b, c, d]
咱们要更新后的树是这样的:[a, d,e, b]
咱们来看看dom diff的过程;
- 第一步:此时oldStartIdx为0,newStartIdx为0,oldEndIdx为3,newEndIdx为3;走到了比较新老两个树的开头,老a和新a是一样的,保持不变,此时还是为**[a, b, c, d]**继续循环;
- 第二步:此时oldStartIdx为1,newStartIdx为1,oldEndIdx为3,newEndIdx为3;走到了比较老开和新尾,也就是老b和新b是一样的,此时会把老b移动到最后面结尾处,变为:[a, c, d, b],此时++oldStartIdx,--newEndIdx;
- 第三步:此时oldStartIdx为2,newStartIdx为1,oldEndIdx为3,newEndIdx为2;走到了比较老尾和新开,也就是老d和新d是一样的,此时会把老d移动到oldStartIdx的前面,也就是插入到第2个位置,变为**[a, d, c, b]**,然后--oldEndIdx,++newStartIdx;
- 第四步:此时oldStartIdx为2,newStartIdx为2,oldEndIdx为2,newEndIdx为2;走到了其他情况,此时oldKeyToIdx={c: 2},newStartVnode.key为e,idxInOld在oldKeyToIdx里面是不存在的,也就是idxInOld为undefined,此时调用createElm创建e,并把e插入到oldStartVnode的前面,变为:[a, d, e, c, b],此时++newStartIdx;
- 第五步:此时oldStartIdx为2,newStartIdx为3,oldEndIdx为2,newEndIdx为2;条件[oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx]为false,跳出循环;
- 第六步:根据条件判断newStartIdx > newEndIdx,走到了removeVnodes函数,也就是把老树剩余的部分删除掉,调用removeVnodes(oldCh, oldStartIdx, oldEndIdx);把老树,从第二个位置,删除到第二个位置,也就是把c去掉;
- 最终执行完成,结果为:[a, d, e, b]
结言
至此,mount函数的讲解基本完成,在Vue的dom diff中,尤为对key进行判断哪,key一样才会判断为一样,否则就判断为不一样,所以咱们在开发中,对于子元素切记要定义key,提升性能。
Vue2的源码解析算是初步完成,从vue的声明到Vue的初始化,也就是new Vue的过程;同时也详细讲解了初始化过程中initState的调用过程;对Vue的核心之一Observer进行了详细的解读,其中的Watcher && Scheduler也单独进行了讲解和分析;在Vue中对nextTick的使用和宏任务与微任务的对比也进行了比较详细的说明;最终在本篇文章对最后的mount部分进行了粗略的讲解,patch里面还有很多小函数的实现,此处就不一一说明了。
读源码是很枯燥的,不过对于个人的提升是有帮助的,俗话说“书读百遍其义自见”,先对源码读几遍,最后按照自己的理解对其进行解读并写出来,更能加深印象,如果不对之处,欢迎指出。