# 2.0虚拟DOM

# 虚拟dom的概念

& 虚拟dom图

// 打补丁

// 基于snabdom库

# 优点

  • 轻量、快速:介于js操作和真实dom之间,当数据发生变化的时候,通过新旧虚拟dom的比对得到最小dom操作量,然后进行数据更新

  • 跨平台:将虚拟dom更新转换为不同运行时特殊操作实现跨平台

  • 兼容性:还可以加入兼容性代码增强操作的兼容性

#
# ** 必要性**

// watcher粒度上的需求性

// vue1.0 2.0 30.比较,改进优化的必要性

vue 1.0中有细粒度的数据变化侦测,它是不需要虚拟DOM的,但是细粒度造成了大量开销,这对于大型项目来说是不可接受的。

因此,vue 2.0选择了中等粒度的解决方案,每一个组件一个watcher实例, 这样只有组件数据状态变化时才会通知到组件,接着再通过引入虚拟DOM去对新旧DOM进行比对并对页面进行渲染。

3.0

# 实现

# 渲染更新组件

core/instance/lifecycle.js 方法 mountComponent()

// 定义组件更新函数 
updateComponent = () => { 
// 实际调用是在lifeCycleMixin中定义的_update和renderMixin中定义的_render 
vm._update(vm._render(), hydrating)
}
  • _render 执行获取虚拟dom, VNode

  • _update 将虚拟dom转成真createPatchFunction实dom

    _update转换分为两步:

    • 1)初始化直接转换成真实dom
    • 2)更新流程 使用diff算法进行新dom 旧dom 的替换

查看以下demo的console结果

<div id="demo">
      <h1>虚拟DOM</h1>
      <p>{{foo}}</p>
</div>
<script src="../../dist/vue.js"></script>
<script>
    const app = new Vue({
        el: "#demo",
        data: { foo: "foo" },
    });
	console.log(app.$options.render);
</script>

渲染函数最后的生成结果

// 生成一个匿名函数
// _c() 就是render(h){}中的h函数,也就是createElemet()
// _v 文本节点
// _s 格式化
ƒ anonymous(
) {
    with(this){
        return _c('div',{
            attrs:{"id":"demo"}
        },[
            _c('h1',[_v("虚拟DOM")]),_v(" "),
           	_c('p',[_v(_s(foo))])
        ])
    }
}

# _render函数的定义

src/core/instance/index.js 调用了

//* _update 所在函数
lifecycleMixin(Vue) 
// * $nextTick _render所在的函数
renderMixin(Vue) 

src/core/instance/render.js 定义了renderMixin

// render函数 实现了$nextTick和_render
export function renderMixin (Vue: Class<Component>) {

  Vue.prototype.$nextTick = function (fn: Function) {
    return nextTick(fn, this)
  }

  Vue.prototype._render = function (): VNode { //返回的是VNode
    const vm: Component = this
    const { render, _parentVnode } = vm.$options
    vm.$vnode = _parentVnode
    // render self
    let vnode = render.call(vm._renderProxy, vm.$createElement)
    // if the returned array contains only a single node, allow it
    if (Array.isArray(vnode) && vnode.length === 1) {
      vnode = vnode[0]
    }
    // return empty vnode in case the render function errored out
    if (!(vnode instanceof VNode)) {
      vnode = createEmptyVNode()
    }
    // set parent
    vnode.parent = _parentVnode
    return vnode
  }
}

src/core/instance/render.js 中的initRender()

// *render中的h()函数,虚拟dom的生成函数
//编译器render函数生成
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// 用户render函数生成
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

src/core/vdom/create-element.js

 _createElement(context, tag, data, children, normalizationType)

_createElement()

创建虚拟DOM

/** 创建标签的时候,先判断是否是原生标签**/
// * 根据标签执行相应操作,标签为string类型时
  if (typeof tag === 'string') {
    if (config.isReservedTag(tag)) {
    //   原生标签直接创建虚拟DOM
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    //   自定义组件
    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // component
      // * 根据组件的构造函数自定义组件
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }

    else {
        // direct component options / constructor
        // 传进来的标签不是字符串
        vnde = createComponent(tag, data, context, children)
    }
  }else{
    // 传进来的标签不是字符串的情况下直接创建
    vnode = createComponent(tag, data, context, children)

  }

# _update函数的执行

src/core/instance/lifecycle.js lifecycleMixin()

Vue.prototype._update

 // * 获取上次执行结果,旧的虚拟DOM
    const prevVnode = vm._vnode
    // 	上次执行结果没有,就是初始化过程
     if (!prevVnode) {
      // * 初始化时,传入的 vm.$el 是真实DOM
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } 
    // 否则就是更新过程
    else {
      vm.$el = vm.__patch__(prevVnode, vnode)
    }

src/core/vdom/patch.js patchVnode所在位置

Web/runtime/index.js 实现了patch方法

Vue.prototype.__patch__ = inBrowser ? patch : noop

src/platforms/web/runtime/patch.js patch 工厂函数,实现跨平台

// 根据传进来的平台类型,进行不同的patch操作
// nodeOps:节点操作, modules:节点属性操作
export const patch: Function = createPatchFunction({ nodeOps, modules })

src/core/vdom/patch.js 定义createPatchFunction()方法

通过同层的树节点进行比较而非对树进行逐层搜索遍历的方式,所以时间复杂度只有O(n),降低了时间复杂度,是一种相当高效的算法。

同层级只做三件事:增删改。

具体规则是:

  • new VNode不存在就删;

  • old VNode不存在就增;

  • 都存在就比较类型,类型不同直接替换、类型相同执行更新;

# patchVnode

两个VNode类型相同,就执行更新操作,包括三种类型操作:属性更新PROPS文本更新TEXT子节点更新

# patchVnode具体规则如下:
  1. 如果新旧VNode都是静态的,那么只需要替换elm以及componentInstance即可。

  2. 新老节点均有子节点,则对子节点进行diff操作,调用updateChildren

  3. 如果老节点没有子节点而新节点存在子节点,先清空老节点DOM的文本内容,然后为当前DOM节 点加入子节点。

  4. 新节点没有子节点而老节点有子节点的时候,则移除该DOM节点的所有子节点。

  5. 新老节点都无子节点的时候,只是文本的替换。

v2-f9d6f677e54a61da09aa8d1ceba1f4bc_w
# 静态节点判断
if (isTrue(oldVnode.isAsyncPlaceholder)) {
    if (isDef(vnode.asyncFactory.resolved)) {
        hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
    } else {
        vnode.isAsyncPlaceholder = true
    }
    return
}
# 属性更新
let i
const data = vnode.data
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
    i(oldVnode, vnode)
}
// oldCh:旧节点 ch:新节点
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)
}
function patchVnode (
 oldVnode,
 vnode,
 insertedVnodeQueue,
 ownerArray,
 index,
 removeOnly
) {
    if (oldVnode === vnode) {
        return
    }

    if (isDef(vnode.elm) && isDef(ownerArray)) {
        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
    }

    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
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
        i(oldVnode, vnode)
    }
    // 1.oldCh:旧节点 ch:新节点
    const oldCh = oldVnode.children
    const ch = vnode.children
    // 2.属性更新
    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)
    }

 	// 3.新的节点里没文本
    if (isUndef(vnode.text)) {     
        // 新旧节点里都存在子节点,直接对比更新新旧子节点
        if (isDef(oldCh) && isDef(ch)) {
            if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)

        } 
        // 新节点存在子节点、老节点没有子节点,清空老节点中的text文本并增加新节点中的children
        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, '')
        }
    } 
     // 4.有文本,内容不一样,直接替换
     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)
    }
}

updateComponent

   /** 假设头尾有可能相同
     * 新节点开始:nS 新节点结束nE 旧节点开始oS 旧节点结束oE
     * 
     * */   
  function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    // 设置首尾4个index以及相对应的节点
    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)
    }
    //开始循环:结束条件开始的index不能超过结束index
    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)
        // 开始的index分别加1
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } 
        // 新旧节点结束位置节点相同
        else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        // // 结束的index分别减1
        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)
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
        // 旧节点没有,新节点增加
         if (isUndef(idxInOld)) {
          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)
    }
  }

整体流程

# Vue3.0的虚拟DOM

使用proxy代替deficreatePatchFunctionneReactive

  1. Vue2.0中defineReactive只能对object的一个key进行监听,所以当object有很多可,或者有深度嵌套的时候,我想要对所有的key进行监听,就需要做深层遍历。

  2. 在每一个key中有大量的数据去保存数据的变化,会形成很多闭包;而且为了建立数据和界面的依赖关系,会创建许多watcher和dep来保存依赖关系。所以这就造成了初始化速度慢,占用内存高。

  3. 对于数组要做特殊的处理,修改数据时不能使用索引方法。动态添加或删除对象属性时,需要使用额外的API vue3.0中使用到了proxy,proxy是懒处理,发现嵌套对象的时候才会进行递归。

vue2.0 视图更新patch过程:https://segmentfault.com/a/1190000021057420

vue3.0

  • https://zhuanlan.zhihu.com/p/86067078
  • https://www.cnblogs.com/fs0196/p/12691407.html
  • https://juejin.cn/post/6844904134303301645#heading-14