vue 虚拟DOM源码解析

1、什么是虚拟DOM

  • 虚拟DOM(Virtual DOM)是使用javaScript对象描述真实DOM
  • vue.js中的虚拟DOM借鉴snabbdom,并添加了vue.js的特性,例如:指令和组件机制

2、为什么使用虚拟DOM

  • 可以避免直接操作DOM,提高开发效率
  • 作为一个中间层,可以跨平台
  • 虚拟DOM不一定可以提高效率
    1. 首次渲染的时候会增加开销,在第一次渲染的时候,需要增加一个虚拟DOM
    2. 复杂视图情况下提升渲染性能,例如:diff算法进行新旧值比较;使key减少DOM更新次数

3、 vue初始化,渲染过程

vm._init()->vm.$mount()->mountComponent()->创建watcher->updateComponent()

  • vm._update()(vm._render()),调用_render(),_undate()方法

4、 vm._render()结束,返回vnode

1. 地址:src/core/instance/render.js
  • 调用用户传来render或者编译生成的render,如果是用户传入的render,调用vm.$createElement,模板编译的render使用vm._c
  • call改变指向,vm._renderProxy是vue实例,vm.$createElement是h函数
vnode = render.call(vm._renderProxy,vm.$createElement)
2. 地址:src/core/instance/render.js
  • vm.$createElement()——用户传入的render,调用createElement(vm,a,b,c,d,true)
// 用户传入的render会调用$createElement
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
3. 地址:src/core/instance/render.js
  • vm._c()——编译生成的render,调用createElement(vm,a,b,c,d,false)
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
4. 地址: src/core/vdom/create-element.js
  • createElement调用_createElement()
  • _createElement()创建vnode对象,并返回vnode
// 创建VNode实例,传入,tag,data,children,context是vue实例
vnode = new VNode(config.parsePlatformTagName(tag), data, children, undefined, undefined, context)

5、_undate(vnode),返回的vnode传入——负责把虚拟dom渲染成真实dom

1. 地址:src/core/instance/lifecycle.js
  • _update()主要通过preVnode来判断,没有就是初次渲染,有就是修改;首次执行,将vm.$el真实dom与vnode虚拟dom进行比较,并把结果放到$el中
if (!prevVnode) {
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
}
  • 数据更新:将旧的vnode,之前渲染保存下来的preVnode与新的vnode进行比较,并存放到$el中
else {
    // updates
    // 数据更新
    // 旧的vnode-prevVnode与最新的vnode比较,并存放到$el
    vm.$el = vm.__patch__(prevVnode, vnode)
}

6、vm.__patch__()——__patch__()初始化

1. 地址:src/platforms/web/runtime/index.js
  • 初始化__patch()__,相当于patch()函数
Vue.prototype.__patch__ = inBrowser ? patch : noop
2. 地址:src/platforms/web/runtime/patch.js
  • patch函数主要传入两个参数:modules存储的是与web平台相关的模块;nodeOps操作dom的api
const modules = platformModules.concat(baseModules)
// nodeOps操作dom的api,platformModules是关于生命周期钩子函数,baseModules处理指令和ref
// modules存储的是与web平台相关的模块
export const patch: Function = createPatchFunction({ nodeOps, modules })
3. 地址:src/core/dom/patch.js
  • createPatchFunction()返回patch函数

7、createPatchFunction()返回patch()函数——初始加载调用createElm,数据更新调用patchVnode()函数进行diff算法

1. 地址:src/core/dom/patch.js
  • 挂载cbs节点的属性/事件/样式操作的钩子函数
  • 判断第一个参数是真实dom还是vnode,首次加载第一个是真实dom,将真实dom转为vnode元素,并存储到oldVnode中,调用createElm,将vnode转为真实dom
  • 如果是数据更新的时候,新旧节点如果key,tag相同即sameVnode相同,调用patchVnode进行diff算法
  • 删除旧节点
export function createPatchFunction (backend) {
  ......
  // 存储的是模块中的钩子函数
  const cbs = {}
  ......
  return function patch  (oldVnode, vnode, hydrating, removeOnly) {
    ......
    else {
      // 获取oldVnode.nodeType,nodeType存在的话,就是真实dom,说明是第一次渲染
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // !!!patchVnode diff算法
        // 比较新旧节点的差异
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } else {
        // 不是真实dom,oldVnode,node不是相同节点
        // 判断是否是真实dom节点,是的话,说明第一次渲染
        if (isRealElement) {
         ......
          // emptyNodeAt把真实dom转为vnode元素,存储到oldVnode中
          oldVnode = emptyNodeAt(oldVnode)
        }
        // elm是为了找parentElm,parentElm是为了将vnode转为真实dom,挂载到父元素上
        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)

        // createElm是为了将vnode转为真实dom,挂载到parentElm
        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)
        )
        ......
        // 删除旧节点
        if (isDef(parentElm)) {
          removeVnodes([oldVnode], 0, 0)
        }
        ......
      }
    }
  }
}

8、createPatchFunction()中createElm(vnode,insertedVnodeQueue)——虚拟dom转为真实dom,并插入到父节点上(dom树)触发钩子函数

1. 地址:src/core/dom/patch.js
  • 通过对应的tag判断是组件/标签/注释/文本,来转为对应的真实dom,createComponent()/createElement()/createComment()/createTextNode()
  • 把虚拟dom的children转为真实dom,插入到dom树

9、createPatchFunction()中 patchVnode(oldVnode,vnode...)——比较新旧vnode及子节点的差异

1. 地址:src/core/dom/patch.js
  • 比较新旧vnode,及新旧vnode的子节点的更新差异
  • 如果新旧vnode都有子节点,并且子节点不同调用updateChildren(),对比子节点的差异
// 新老节点的子节点存在
if (isDef(oldCh) && isDef(ch)) {
  // 对子节点进行diff操作,调用updateChildren
  if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
}

10、createPatchFunction()中 updateChildren()——4中方法比较子节点的patchVnode

1. 地址:src/core/dom/patch.js
  • 新老节点的子节点传过来都是数组的形式,对比两个数组中的vnode,比较两者的差异
  • 设置8个属性值:新老节点开始index,结束index,新老节点开始节点的值,结束节点的值

diff算法:

1. 两个数组没有遍历完时:oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx

sameVnode仅仅判断key和tag相同,子节点跟文本是否相同不知道

  • 开始开始:判断老节点和新节点的开始值是否相同,相同的话,调用patchVnode来判断子节点是否相同,判断完成比较下一个
      else if (sameVnode(oldStartVnode, newStartVnode)) {
        // 判断老节点和新节点开始值是否相同,相同的话
        // sameVnode仅仅判断key和tag相同,子节点跟文本是否相同不知道
        // 调用patchVnode来判断子节点是否相同,判断完成比较下一个
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } 
  • 结束结束:判断老节点和新节点结束值是否相同,相同的话,调用patchVnode来判断子节点是否相同,判断完成比较下一个
      else if (sameVnode(oldEndVnode, newEndVnode)) {
        // 判断老节点和新节点结束值是否相同,相同的话
        // 调用patchVnode来判断子节点是否相同,判断完成比较下一个
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      }
  • 开始结束:将新的列表翻转,比较老的开始节点值与新的结束节点值,相同的话,调用patchVnode来判断子节点是否相同,判断完成比较下一个
      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]
      }
  • 结束开始:将老的列表翻转,比较老的结束节点值与新的开始节点值,相同的话,调用patchVnode来判断子节点是否相同,判断完成比较下一个
      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]
      } 
2. 以上4中情况都不满足:以新节点为基础,从老节点中查找新节点
  • 先找新节点的key和老节点的相同索引,如果没有找到再通过sameVnode找
else {
  // 从新节点的开始获取一个,去老节点中查找相同节点
  // 先找新开始节点的key和老节点的相同索引,如果没有找到再通过sameVnode找
  // 把老节点的Key和索引存储到oldKeyToIdx中,然后如果新开始节点有key属性,查找老的节点中的索引
  // 如果没有key,去老节点中findIdxInOld依次遍历找到老节点的索引
  // 在这体现使用Key会快一点
  if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
  idxInOld = isDef(newStartVnode.key)
    ? oldKeyToIdx[newStartVnode.key]
    : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
  if (isUndef(idxInOld)) { // New element
    // 如果没有找到老节点对应的索引,重新创建createElm新节点对应的dom对象
    // 并插入到老的开始节点对应的dom元素前边
    createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
  } else {
    // 如果找到新节点开始对应的老节点的索引,把对应老节点取出来存储到vnodeToMove
    vnodeToMove = oldCh[idxInOld]
    // 然后比较老节点的vnodeToMove与新节点的key,tag是否相同,如果相同的话
    if (sameVnode(vnodeToMove, newStartVnode)) {
      // 比较两个的子节点是否相同
      patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
      // 将对应的老节点设为undefined
      oldCh[idxInOld] = undefined
      // 把对应的老的节点移动到老的开始节点之前
      canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
    } else {
      // same key but different element. treat as new element
      // 如果不相同,也就是key相同,tag不相同,创建createElm新的dom元素
      createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
    }
  }
  // 把下一个新的节点作为开始节点接着处理
  newStartVnode = newCh[++newStartIdx]
}
3. 当遍历结束时,老的开始大于老的结束时,老节点遍历完,新节点还未遍历完
  • 新几点比老节点多,把剩下的新节点批量插入到老节点后
if (oldStartIdx > oldEndIdx) {
  // 说明新节点比老节点多,把剩下的新节点批量插入到新节点后面
  refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
  addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
}
4. 当遍历结束时,新节点遍历完,老节点还未
  • 老节点比新节点多,把老节点剩余的批量删除
else if (newStartIdx > newEndIdx) {
  // 新节点遍历完,老节点还未,说明新节点少于老节点,将老节点剩余的批量删除
  removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}

11、使用Key的优点:减少dom更新

例如:在a后加入x

data() {
  return {
    vnodeArr: ['a','b','c']
  }
},
methods: {
  handler() {
    this.vnodeArr.splice(1,0,'x')
  }
}
1. 没有使用key时
  • {{ item }}

操作步骤:在updateChildren中开始断点

1. 第一轮循环:a——a
  • oldStartIdx = 0,oldEndIdx = 2,newStartIdx = 0,newEndIdx = 3
  • 老开始节点与新的开始节点比较,进入sameVnode,没有key,key都是undefined,相同;tag都是li,相同;sameVnode为true
  • patchVnode比较,判断li中的内容(刚开始vnode.text为undefined),进入if (isUndef(vnode.text)) ,oldCh是数组vnode,ch也是数组vnode,if (oldCh !== ch)为true,再次进入updateChildren
  • sameVnode(oldStartVnode, newStartVnode),true
  • patchVnode比较,else if (oldVnode.text !== vnode.text),两个text都是'a',不执行,所以不更新dom(当节点的内容没有发送变化时,不操作dom)
2. 第二轮循环:b——x
  • oldStartIdx = 1,oldEndIdx = 2,newStartIdx = 1,newEndIdx = 3
  • 下个节点比较,老节点b,新节点x,sameVnode(oldStartVnode, newStartVnode),key = undefined,tag = li 相同,
  • patchVnode比较,再找到text,else if (oldVnode.text !== vnode.text),文本不同会更新dom,此时页面中视图发生变化
3. 第三轮循环:c——b
  • oldStartIdx = 2,oldEndIdx = 2,newStartIdx = 2,newEndIdx = 3
  • 与第二轮循环一样,更新dom
4. 第四轮已遍历结束:——c
  • oldStartIdx = 3,oldEndIdx = 2,newStartIdx = 3,newEndIdx = 3
  • if (oldStartIdx > oldEndIdx),新节点比老节点多,把剩下的新节点批量插入到老节点中addVnodes,更新dom

更新2次dom,一次插入dom,总共3次dom操作

2. 使用key
  • {{ item }}
1. 第一轮循环:a——a
  • oldStartIdx = 0,oldEndIdx = 2,newStartIdx = 0,newEndIdx = 3
  • sameVnode(oldStartVnode, newStartVnode),key: a,a,tag: li,li,相同,为true
  • patchVnode比较,text文本一样为a,不更新dom
2. 第二轮循环:b——x->c——c
  • oldStartIdx = 1,oldEndIdx = 2,newStartIdx = 1,newEndIdx = 3
  • sameVnode(oldStartVnode, newStartVnode),key: b,x,tag: li,li,不相同,为false
  • sameVnode(oldEndVnode, newEndVnode),key: c,c,tag: li,li,相同,为true
  • patchVnode比较,text文本一样为c,不更新dom
  • oldStartIdx = 1,oldEndIdx = 1,newStartIdx = 1,newEndIdx = 2
3. 第三轮循环:b——b
  • oldStartIdx = 1,oldEndIdx = 1,newStartIdx = 1,newEndIdx = 2
  • sameVnode(oldEndVnode, newEndVnode),key: b,b,tag: li,li,相同,为true
  • patchVnode比较,text文本一样为b,不更新dom
  • oldStartIdx = 1,oldEndIdx = 0,newStartIdx = 1,newEndIdx = 1
4. 第四轮已遍历结束:——x
  • if (oldStartIdx > oldEndIdx),新节点比老节点多,把剩下的新节点批量插入到老节点中addVnodes,更新dom

一次插入dom,总共1次dom操作

你可能感兴趣的:(vue 虚拟DOM源码解析)