Vue2源码解读(七)-mount

7,780 阅读9分钟

前言

上面六篇文章对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里面还有很多小函数的实现,此处就不一一说明了。

读源码是很枯燥的,不过对于个人的提升是有帮助的,俗话说“书读百遍其义自见”,先对源码读几遍,最后按照自己的理解对其进行解读并写出来,更能加深印象,如果不对之处,欢迎指出。