美文网首页
学习笔记(十六)Vue.js源码剖析 - 虚拟DOM

学习笔记(十六)Vue.js源码剖析 - 虚拟DOM

作者: 彪悍de文艺青年 | 来源:发表于2020-12-30 20:27 被阅读0次

    Vue.js源码剖析 - 虚拟DOM

    虚拟DOM概念回顾

    什么是虚拟DOM?

    • 虚拟DOM(Virtual DOM)是使用JavaScript对象来描述真实的DOM,本质是JavaScript对象
    • Vue.js中的虚拟DOM借鉴了Snabddom,并在此基础上添加了Vue.js的相关特性,例如:指令和组件机制

    为什么使用虚拟DOM?

    • 避免直接操作DOM,提高开发效率
    • 作为中间层可以实现跨平台
      • SSR服务端渲染
      • Weex移动端原生渲染等
    • 虚拟DOM不一定可以提高性能
      • 首次渲染时会增加开销
      • 复杂视图情况下能提升渲染性能

    Vue.js虚拟DOM的核心概念

    h函数

    在Snabddom中,h函数用来创建虚拟DOM

    Vue中也存在h函数,并且支持传入组件作为参数

    通过查看源码我们可以知道,Vue中的h函数其实就是vm.$createElement()

    • vm.$createElement(tag, data, children, normalizeChildren)
      • tag
        • 标签名称或组件对象
      • data
        • 描述tag,可以设置DOM属性或者标签属性
      • children
        • tag中的文本内容或子节点
      • normalizeChildren

    VNode

    VNode是虚拟DOM对象

    在Snabddom中,VNode只是一个简单的对象,包含6个最基本的属性

    Vue.js中的VNode通过类来定义,包含更多复杂的属性

    VNode的核心属性

    • tag
    • data
    • children
    • text
    • elm
    • key

    VNode的创建过程

    updateComponent() -> vnode = vm._render() -> vnode = render.call(vm._renderProxy, vm.$createElement) -> vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true) -> return _createElement(context, tag, data, children, normalizationType)

    • vnode在updateComponent()中通过调用vm._render()创建
    • 实例方法_render()在Vue实例初始化时在renderMixin(Vue)注册,并尝试调用用户传入的render函数vnode = render.call(vm._renderProxy, vm.$createElement)
    • vm.$createElement 在Vue构造函数调用_init()时通过initRender()注册,函数内部调用createElement(vm, a, b, c, d, true)
    • createElement()最终调用_createElement()并返回其调用结果
    • _createElement()返回的结果即为vnode

    createElement

    创建vnode

    export function _createElement (
      context: Component,
      tag?: string | Class<Component> | Function | Object,
      data?: VNodeData,
      children?: any,
      normalizationType?: number
    ): VNode | Array<VNode> {
      if (isDef(data) && isDef((data: any).__ob__)) {
        // 如果是响应式对象,创建并返回一个空的VNode
        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()
      }
      // object syntax in v-bind
      if (isDef(data) && isDef(data.is)) {
        // tag 记录 is 属性中的组件名
        tag = data.is
      }
      if (!tag) {
        // in case of component :is set to falsy value
        // 如果 is 属性被设置成 false 值则返回空VNode
        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
      // 处理作用域插槽 scoped slot
      if (Array.isArray(children) &&
        typeof children[0] === 'function'
      ) {
        data = data || {}
        data.scopedSlots = { default: children[0] }
        children.length = 0
      }
     
      if (normalizationType === ALWAYS_NORMALIZE) {
        // children 可能是文本内容,也可能是子节点数组
        // 如果是文本内容,创建一个text是该文本内容的VNode,并放入数组中返回
        // 如果是子节点数组,则递归(成员也可能是节点数组)将成员创建成VNode,存入一维数组
        // 最终统一返回一个一维数组以备后续处理
        children = normalizeChildren(children)
      } else if (normalizationType === SIMPLE_NORMALIZE) {
        // 如果是函数式组件,尝试把二维数组转换成一维数组 Array.prototype.concat.apply([], children)
        children = simpleNormalizeChildren(children)
      }
      let vnode, ns
      if (typeof tag === 'string') {
        // tag 是字符串
        let Ctor
        ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
        if (config.isReservedTag(tag)) {
          // platform built-in elements
          // 且 tag 是平台内置元素的保留标签 (浏览器环境是 html 标签)
          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
          vnode = new VNode(
            config.parsePlatformTagName(tag), data, children,
            undefined, undefined, context
          )
        } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {     
          // component
          // 判断是自定义组件
          // 查找自定义组件构造函数声明
          // 根据 Ctor 创建组件的 VNode
          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
          // 自定义标签 以 tag 创建 VNode
          vnode = new VNode(
            tag, data, children,
            undefined, undefined, context
          )
        }
      } else {
        // direct component options / constructor
        // tag 是组件,尝试创建组件的 VNode
        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 {
        // 否则返回空的 VNode
        return createEmptyVNode()
      }
    }
    

    update

    首次渲染及数据更新时,调用vm.__patch__()比对新老vnode,并更新DOM

    // 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.
        // Vue.prototype.__patch__ 方法是在入口被注入的
        // 调用 __patch__ 比对新旧 vnode 并更新视图
        if (!prevVnode) {
          // initial render
          // 首次渲染
          // __patch__() 第一个参数传入Vue挂载的DOM元素
          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的实现跟运行Vue的平台环境相关,因此patch的初始化被定义在平台相关的入口文件中

    // web环境下patch的初始化定义在 src/platforms/web/runtime/index.js
    // install platform patch function
    Vue.prototype.__patch__ = inBrowser ? patch : noop
    

    patch的创建

    通过 createPatchFunction 方法为不同平台环境创建相应的patch方法

    这里的createPatchFunction 相当于Snabddom的init方法

    export function createPatchFunction (backend) {
        ...
        return function patch (oldVnode, vnode, hydrating, removeOnly) {
        // 新 vnode 未定义
        if (isUndef(vnode)) {
          // 如果老 vnode 存在,触发老 vnode 的 destory 钩子函数并返回
          if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
          return
        }
    
        // 标记是否是根结点
        let isInitialPatch = false
        // 保存新插入的 vnode,用来在这些节点完成挂载时触发 inserted 钩子函数
        const insertedVnodeQueue = []
    
        if (isUndef(oldVnode)) {
          // 老的 vnode 不存在,创建新的根结点
          // empty mount (likely as component), create new root element
          isInitialPatch = true
          // 转换新的 vnode 为 DOM元素(但没有进行挂载)
          createElm(vnode, insertedVnodeQueue)
        } else {
          // 新老 vnode 都存在,更新
          const isRealElement = isDef(oldVnode.nodeType)
          // 判断老的 vnode 是否是真实 DOM
          if (!isRealElement && sameVnode(oldVnode, vnode)) {
            // 老 vnode 不是真实 DOM 且新老 vnode 是相同节点
            // diff算法更新节点
            // patch existing root node
            patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
          } else {
            if (isRealElement) {
              // 老的 vnode 是真实 DOM,创建 VNode
    
              // mounting to a real element
              // check if this is server-rendered content and if we can perform
              // a successful hydration.
    
              // SSR 服务端渲染相关处理
              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
                } else if (process.env.NODE_ENV !== 'production') {
                  warn(
                    'The client-side rendered virtual DOM tree is not matching ' +
                    'server-rendered content. This is likely caused by incorrect ' +
                    'HTML markup, for example nesting block-level elements inside ' +
                    '<p>, or missing <tbody>. Bailing hydration and performing ' +
                    'full client-side render.'
                  )
                }
              }
              // either not server-rendered, or hydration failed.
              // create an empty node and replace it
              // 转换老的 vnode 为空 VNode 并将其保存在 VNode.elm 中
              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,
              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)) {
              // 移除老的 vnode 对应的节点,并触发相应的钩子函数
              removeVnodes([oldVnode], 0, 0)
            } else if (isDef(oldVnode.tag)) {
              // 触发 destory 相应的钩子函数
              invokeDestroyHook(oldVnode)
            }
          }
        }
    
        // 触发 insertedVnodeQueue 中 vnode 的 inserted 钩子函数
        // 对于根结点,延迟触发(还没有真实挂载)
        invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
        // 返回 vnode 绑定的 DOM
        return vnode.elm
      }
    }
    

    createElm

    用于将VNode转换成真实DOM然后挂载到DOM树上

    // createPatchFunction 中定义
    
    function createElm (
        vnode,
        insertedVnodeQueue,
        parentElm,
        refElm,
        nested,
        ownerArray,
        index
      ) {
        // vnode.elm有值代表该将节点曾经被渲染过
        // ownerArray代表有子节点
        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曾经渲染过且有子节点
          // 直接将其作为一个新的节点并覆盖起元素可能造成潜在的风险
          // 进行备份
          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
              )
            }
          }
    
          // 创建DOM元素
          // 设置Scope
          vnode.elm = vnode.ns
            ? nodeOps.createElementNS(vnode.ns, tag)
            : nodeOps.createElement(tag, vnode)
          setScope(vnode)
    
          /* istanbul ignore if */
          // weex环境逻辑处理
          if (__WEEX__) {
            // in Weex, the default insertion order is parent-first.
            // List items can be optimized to use children-first insertion
            // with append="tree".
            const appendAsTree = isDef(data) && isTrue(data.appendAsTree)
            if (!appendAsTree) {
              if (isDef(data)) {
                invokeCreateHooks(vnode, insertedVnodeQueue)
              }
              insert(parentElm, vnode.elm, refElm)
            }
            createChildren(vnode, children, insertedVnodeQueue)
            if (appendAsTree) {
              if (isDef(data)) {
                invokeCreateHooks(vnode, insertedVnodeQueue)
              }
              insert(parentElm, vnode.elm, refElm)
            }
          } else {
            // 创建子节点DOM并挂载到当前节点下
            createChildren(vnode, children, insertedVnodeQueue)
            if (isDef(data)) {
              // 触发Create钩子函数
              invokeCreateHooks(vnode, insertedVnodeQueue)
            }
            // 将vnode.elm的DOM元素挂载到parentElm对应的节点下,refElm节点之前
            insert(parentElm, vnode.elm, refElm)
          }
    
          if (process.env.NODE_ENV !== 'production' && data && data.pre) {
            creatingElmInVPre--
          }
        } else if (isTrue(vnode.isComment)) {
          // 是注释节点
          // 创建注释节点DOM元素
          // 将vnode.elm的DOM元素挂载到parentElm对应的节点下,refElm节点之前
          vnode.elm = nodeOps.createComment(vnode.text)
          insert(parentElm, vnode.elm, refElm)
        } else {
          // 是文本节点
          // 创建文本节点DOM元素
          // 将vnode.elm的DOM元素挂载到parentElm对应的节点下,refElm节点之前
          vnode.elm = nodeOps.createTextNode(vnode.text)
          insert(parentElm, vnode.elm, refElm)
        }
      }
    

    patchVnode

    比较新老VNode,更新DOM,如果新老都存在子节点,调用updateChildren比较并更新子节点内容

    // createPatchFunction 中定义
    
    function patchVnode (
        oldVnode,
        vnode,
        insertedVnodeQueue,
        ownerArray,
        index,
        removeOnly
      ) {
        // 新老 vnode 相同,直接返回
        if (oldVnode === vnode) {
          return
        }
        // 处理同 createElm(),vnode渲染过且有子节点,进行备份,规避潜在风险
        if (isDef(vnode.elm) && isDef(ownerArray)) {
          // clone reused vnode
          vnode = ownerArray[index] = cloneVNode(vnode)
        }
    
    
        const elm = vnode.elm = oldVnode.elm
    
        if (isTrue(oldVnode.isAsyncPlaceholder)) {
          if (isDef(vnode.asyncFactory.resolved)) {
            hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
          } else {
            vnode.isAsyncPlaceholder = true
          }
          return
        }
    
        // reuse element for static trees.
        // note we only do this if the vnode is cloned -
        // if the new node is not cloned it means the render functions have been
        // reset by the hot-reload-api and we need to do a proper re-render.
        if (isTrue(vnode.isStatic) &&
          isTrue(oldVnode.isStatic) &&
          vnode.key === oldVnode.key &&
          (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
        ) {
          vnode.componentInstance = oldVnode.componentInstance
          return
        }
    
    
        let i
        const data = vnode.data
        // 执行用户定义的 prepatch 钩子函数 (如果有定义)
        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)) {
          // 调用cbs中的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)
        }
        // 如果新节点没有文本
        if (isUndef(vnode.text)) {
          if (isDef(oldCh) && isDef(ch)) {
            // 新老节点的子节点都存在,且不相等
            // updateChildren 对比更新子节点内容
            if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
          } else if (isDef(ch)) {
            // 有新节点的子节点
            if (process.env.NODE_ENV !== 'production') {
              checkDuplicateKeys(ch)
            }
            // 如果老节点有文本内容则清空
            if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
            // 调用addVnodes遍历新节点的子节点
            // 调用createElm创建DOM元素并挂载到elm
            addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
          } else if (isDef(oldCh)) {
            // 只有老节点有子节点
            // 调用removeVnodes遍历老节点的子节点
            // 移除所有子节点对应的元素及绑定的事件
            // 同时触发remove及destroy钩子函数
            removeVnodes(oldCh, 0, oldCh.length - 1)
          } else if (isDef(oldVnode.text)) {
            // 老节点有文本内容而新的没有,则清空
            nodeOps.setTextContent(elm, '')
          }
        } else if (oldVnode.text !== vnode.text) {
          // 有text文本内容,且新老不一致
          // 修改新节点文本内容为将新节点的text值
          nodeOps.setTextContent(elm, vnode.text)
        }
        if (isDef(data)) {
          // 执行用户定义的 postpatch 钩子函数(如果有定义)
          if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
        }
      }
    

    updateChildren

    diff算法比较并更新新老子节点的内容

      // createPatchFunction 中定义
    
      // diff算法
      // 比较并更新新老节点的子节点
      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) {
          if (isUndef(oldStartVnode)) {
            // 判断老开始节点是否有值并移动索引
            oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
          } else if (isUndef(oldEndVnode)) {
            // 判断老结束节点是否有值并移动索引
            oldEndVnode = oldCh[--oldEndIdx]
          } else if (sameVnode(oldStartVnode, newStartVnode)) {
            // 新老开始节点相同
            // patchVnode新老开始节点
            patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            // 移动新老开始节点索引
            oldStartVnode = oldCh[++oldStartIdx]
            newStartVnode = newCh[++newStartIdx]
          } else if (sameVnode(oldEndVnode, newEndVnode)) {
            // 新老结束节点相同
            // patchVnode新老结束节点
            patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
            // 移动新老结束节点索引
            oldEndVnode = oldCh[--oldEndIdx]
            newEndVnode = newCh[--newEndIdx]
          } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
            // 老开始节点与新结束节点相同
            // patchVnode老开始节点与新结束节点
            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老结束节点与新开始节点
            patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            // 将老结束节点移动到老开始节点之前(左移)
            canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
            // 移动老结束节点与新开始节点的索引
            oldEndVnode = oldCh[--oldEndIdx]
            newStartVnode = newCh[++newStartIdx]
          } else {
            // 如果没找到
            // 遍历老的子节点并生成一个node.key与数组索引idx的map映射对象oldKeyToIdx
            if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
    
            // 使用新开始节点的key查找其在老节点中是否存在相同key的节点
            idxInOld = isDef(newStartVnode.key)
              ? oldKeyToIdx[newStartVnode.key]
              : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
            if (isUndef(idxInOld)) { // New element
              // 如果没找到,认为是新节点
              // 调用createElm创建一个新的节点插入老开始节点之前
              createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
            } else {
              // 在老节点中找到相同key的节点
              vnodeToMove = oldCh[idxInOld]
              if (sameVnode(vnodeToMove, newStartVnode)) {
                // 判断是相同节点
                // 调用patchVnode比较更新新老节点
                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
                // 新老节点key相同,但不是相同节点
                // 调用createElm创建一个新的节点插入老开始节点之前
                createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
              }
            }
            // 移动新开始节点索引
            newStartVnode = newCh[++newStartIdx]
          }
        }
        // 遍历完成收尾工作
        if (oldStartIdx > oldEndIdx) {
          // 老节点遍历完成,新节点可能有剩余
          // 调用addVnodes将剩余的新节点批量创建并插入到老结束节点索引之后
          refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
          addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
        } else if (newStartIdx > newEndIdx) {
          // 新节点遍历完成,老节点可能有剩余
          // 调用removeVnodes批量移除剩余的老节点
          removeVnodes(oldCh, oldStartIdx, oldEndIdx)
        }
      }
    

    key在diff算法中的重要性

    通常建议在使用v-for循环创建元素时候设置key,使用key可以在数据更新触发新老VNode子元素比较的diff算法中重用元素,避免不必要的DOM创建与销毁,提升渲染执行性能

            // 设置key主要影响这部分逻辑
            
            // 遍历老的子节点并生成一个node.key与数组索引idx的map映射对象oldKeyToIdx
            if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
    
            // 使用新开始节点的key查找其在老节点中是否存在相同key的节点
            idxInOld = isDef(newStartVnode.key)
              ? oldKeyToIdx[newStartVnode.key]
              : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
            if (isUndef(idxInOld)) { // New element
              // 如果没找到,认为是新节点
              // 调用createElm创建一个新的节点插入老开始节点之前
              createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
            } else {
              // 在老节点中找到相同key的节点
              vnodeToMove = oldCh[idxInOld]
              if (sameVnode(vnodeToMove, newStartVnode)) {
                // 判断是相同节点
                // 调用patchVnode比较更新新老节点
                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
                // 新老节点key相同,但不是相同节点
                // 调用createElm创建一个新的节点插入老开始节点之前
                createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
              }
            }
    

    总结

    • vm._render()

      image-20201230201323483
    • vm._update()

      image-20201230201353539
    • vm.__patch__()

      image-20201230201454571
    • patch()

      image-20201230201625864
    • createElm()

      image-20201230201646964
    • patchVnode()

      image-20201230201706068
    • updateChildren()

      image-20201230202117876

    相关文章

      网友评论

          本文标题:学习笔记(十六)Vue.js源码剖析 - 虚拟DOM

          本文链接:https://www.haomeiwen.com/subject/alckoktx.html