vue2 源码-虚拟dom学习

168 阅读4分钟

1.patchVnode 节点比较算法

1.1执行逻辑

  1. 依据新老vnode的关系,以老得vnode为基础,渲染成新的vnode效果。把老的dom做操作变成新的dom
  2. 两个节点的子列表数据比较,规则:同层比较 ,深度优先
  3. 新老各有 头游标 和 尾游标 ,头对象 和 尾对象,每次移动都实时更新
    let oldStartIdx = 0 //旧游标开始 往右递增
    let newStartIdx = 0 //新游标开始 往右递增
    let oldEndIdx = oldCh.length - 1 //旧游标结束  往左递减
    let newEndIdx = newCh.length - 1 //新游标结束 往左递减
    
    let oldStartVnode = oldCh[0]
    let oldEndVnode = oldCh[oldEndIdx]
  1. 通过 左右游标往中间执行的方式判断 新旧首尾是否相同节点,

  2. 都没找到才用新节点循环所有老节点比较

  3. 最后比较结束的条件是 新的或者老得 头游标 和 尾游标 已经大于或者小于为结束

//由于是往中间靠拢,所以游标最终移动后,要么是左边移动多点,要么右边移动多点。
//当新老都存在 游标先超过另一边,则结束循环
//可以理解 oldStartIdx开始为0 ,一直递增, oldEndIdx开始为10,一直递减。
//那么迟早会发生oldStartIdx > oldEndIdx的情况,newStartIdx 和 newEndIdx 同理
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {

}
  1. 把比较剩下的做 新增或者删除操作
    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)
    }

1.2节点比较逻辑

核心比较顺序

新老各有 头游标 和 尾游标 ,头对象 和 尾对象,往中心移动,当新的和老的数组:开始游标大于结束游标结束移动。 image.png

1.2.1两头比较

image.png

1.2.2两尾比较

image.png

1.2.3头尾比较

image.png

1.2.4尾头比较

image.png

1.2.5剩下节点处理

当老的数组先结束,证明需要做新增

image.png

当新的数组先结束,证明需要做删除

image.png

1.2更新流程

注意这里做的是具体的dom操作,只是利用新旧的vnode找出更新内容,直接做dom操作。每次循环都会直接做dom,性能消化大,所以需要加上key做唯一判断提高性能。

  1. 属性更新
  2. 文本更新
  3. 子节点更新

1.3源码分析

//核心比较方法 单个节点
  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
    //调用对应的钩子函数
    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
    //先更新属性
    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)
    }
    
    //判断新节点是否 没有文本(相当于处理有子节点的情况)
    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(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)
    }
    if (isDef(data)) {
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
  }

//比较两个虚拟节点是否一样
function sameVnode (a, b) {
  return (
    a.key === b.key &&
    a.asyncFactory === b.asyncFactory && (
      (
        a.tag === b.tag &&
        a.isComment === b.isComment &&
        isDef(a.data) === isDef(b.data) &&
        sameInputType(a, b)
      ) || (
        isTrue(a.isAsyncPlaceholder) &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}

//是否是相同的输入类型
function sameInputType (a, b) {
  if (a.tag !== 'input') return true
  let i
  const typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type
  const typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type
  return typeA === typeB || isTextInputType(typeA) && isTextInputType(typeB)
}

  //最终比较的意义是:依据新老vnode的关系,以老得vnode为基础,渲染成新的vnode效果。把老的dom做操作变成新的dom
  //1.两个节点的子列表数据比较,规则:同层比较 ,深度优先
  //2.新老各有 头游标 和 尾游标 ,头对象 和 尾对象,每次移动都实时更新
  //3.通过 左右游标往中间执行的方式判断 新旧首尾是否相同节点
  //4.都没找到才用新节点循环所有老节点比较
  //5.最后比较结束的条件是 新的或者老得 头游标 和 尾游标 已经大于或者小于为结束
  //6.把比较剩下的做 新增或者删除操作
  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)) {
        //如果两个新老节点 头相同直接patch,同时游标一起右移
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        //如果两个新老节点 尾巴相同直接patch,同时游标一起左移
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        //交叉处理,老开始和新结束patch,老的移动到最尾巴
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))//这里把老的开始dom移动到老的结束dom后边位置,实时执行的dom操作
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
         //交叉处理,老结束和新开始patch,老的移动到最开始
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        //这里把老的结束dom移动到老的开始dom后边位置,实时执行的dom操作
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {
        //如果首尾都没找到,则拿新的给每一个老的从左到右比较,找到就patch,同时按新的位置移动老节点
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
          //idxInOld 老节点不为空
        if (isUndef(idxInOld)) { // New element
          //在老的里面没有找到,直接新增节点
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        } else {
          // 找到了
          vnodeToMove = oldCh[idxInOld]
          //如果是相同的节点,进行patch,由于是在新虚拟dom列表从左往右比较,所以插入到排头的位置
          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)
    }
  }

2.如何实现跨平台 patch

//编译入口
//src/platforms/web/runtime/index.js
import { patch } from './patch'
// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop

//通过工厂返回需要的patch方法
//nodeOps和modules 是平台特有的方法和函数
//src/platforms/web/runtime/patch.js
//dom操作 ,各种节点增删改
import * as nodeOps from 'web/runtime/node-ops'
//这里使用core上面的createPatchFunction方法做工厂
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'

const modules = platformModules.concat(baseModules)
export const patch: Function = createPatchFunction({ nodeOps, modules })

//src/platforms/web/runtime/modules/index.js
//当前方法导出二维数组
import attrs from './attrs'
import klass from './class'
import events from './events'
import domProps from './dom-props'
import style from './style'
import transition from './transition'

export default [
  attrs,
  klass,
  events,
  domProps,
  style,
  transition
]

//src/platforms/web/runtime/modules/attrs.js
export default {
  create: updateAttrs,
  update: updateAttrs
}

//src/platforms/web/runtime/modules/class.js

export default {
  create: updateClass,
  update: updateClass
}

//src/core/vdom/patch.js
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
 //这里接受平台特性的方法做dom操作
export function createPatchFunction (backend) {
  let i, j
  const cbs = {}
   
  const { modules, nodeOps } = backend
  
  //modules定义了上面定义的二维数组

  for (i = 0; i < hooks.length; ++i) {
    cbs[hooks[i]] = []
    //cbs['create'] = [] // 保留多个方法
    for (j = 0; j < modules.length; ++j) {
      if (isDef(modules[j][hooks[i]])) {
      //modules[j][hooks[i] 相当于 attrs['create'] 或者 events['create']
      //把所有相关的方法都保留到cbs数组里,供下次使用
        cbs[hooks[i]].push(modules[j][hooks[i]])
      }
    }
  }
...
}

  function patchVnode (
    oldVnode,
    vnode,
    insertedVnodeQueue,
    ownerArray,
    index,
    removeOnly
  ) {
    //这里判断如果需要打补丁 遍历上面存好的所有更新方法 所有方法都执行
   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)
    }
  }