浅析Vue中Virtual DOM和Diff原理及实现

 更新时间:2023年03月21日 15:42:56   作者:NOxONE  
这篇文章主要为大家详细介绍了Vue中Virtual DOM和Diff原理及实现的相关知识,文中的示例代码讲解详细,感兴趣的小伙伴可以了解一下

0. 写在开头

本文将秉承Talk is cheap, show me the code原则,做到文字最精简,一切交由代码说明!

1. vdom

vdom即虚拟DOM,将DOM映射为JS对象,结合diff算法更新DOM

以下为DOM

<div id="app">
  <div class="home">home</div>
</div>

映射成VDOM

{
  tag: 'div',
  attrs: {
    id: 'app'
  },
  children: [
    {
      tag: 'div',
      attrs: {
        class: 'home'
      },
      children: [
        {
          tag: undefined,
          attrs: undefined,
          text: 'home',
          children: undefined
        }
      ]
    }
  ]
}

通过这个vdom实现简单的render函数,可以通过js操作修改dom

<template>
  <div id="app">
    <div v-for="item in arr">{{ item.name }} : {{ item.id }}</div>
  </div>
  <button id="btn">reRender</button>
</template>
let app = document.getElementById('app')
let data = {
  arr: [   
    { name: 'a', id: 1 },
    { name: 'b', id: 2 },
    { name: 'c', id: 3 },
  ]
}

function render(data) {
  app.innerHtml = ''
  let children = []
  data.forEach(item => {
    let el = document.createElement("div")
    el.innerHtml = `${ item.name } : ${item.id}`
    app.appendChild(el)
  })  
}

// test
render(data.arr) // 首次渲染
let btn = document.getElementById('btn')
btn.onClick = () => {
  data.arr[2].id++ // 修改关联数据
  render(data.arr) // 重新渲染:暴力刷新DOM,没有diff,实际上只用更新最后一个div就行
}

使用snabbdom实现VDOM

snabbldom是简易实现vdom功能的库,有两个核心api:h函数和patch函数

h(tag, attrs, children) // 创建vnode
patch(vnode, newVnode) // 对vnode进行diff后挂载到真实dom上

结合hpatch实现render渲染函数

let app = document.getElementById('app')
let vnode;

function render(data) {
  let newVnode = h('div', { class: 'wrap' }, data.forEach(item => {
      return h('div', {}, `${item.name} : ${item.id}`)
    })
  )
  patch(vnode, newVnode)
  vnode = newVnode
}

render(data.arr) // 首次渲染

let btn = document.getElementById('btn')
btn.onClick = () => {
  data.arr[2].id++ // 修改关联数据
  render(data.arr) // 重新渲染:在patch函数里经过vdom的diff后再挂载到真实dom,这里只更新最后一个div
}

2. Diff

为了尽量减少DOM操作,需要通过diff对比新旧vnode,针对更改的地方进行更新DOM,而非替换整个DOM

大体思路为:

  • 对新旧两个节点调用patch函数
  • 进来先判断两个节点是否为同一类型,具体是对比keytagdata等属性
  • 若不为同一类型,那么基于新节点创建dom之后作替换
  • 若为同一类型,那么调用patchVnode函数
  • 进来先判断两个节点是文本节点的话,那么就作文本内容替换
  • 否则判断是否都有子节点,都有的话调用updateChildren函数,通过首尾四个指针对子节点数组进行diff更新;若旧节点有子节点,新节点没有,这时就删除子节点;若旧节点无子节点,新节点有,这时基于新节点创建dom作替换即可

通过createElment函数,将VDOM转为真实DOM

function createElement(vnode) {
  if(vnode.text) return document.createTextNode(vnode) // 文本节点
    
  let { tag, attrs, children } = vnode
  
  let el = document.createElement(tag) // tag
  
  for(let key of attrs){ // attrs
    el.setAttribute(key, attrs[key])
  }
  
  children.forEach(childVnode => { // children
    el.appendChild(createElement(childVnode)) 
  })
  vnode.el = el
  return el
}

通过patch函数,执行diff更新操作

判断vnodenewVnode是否为同一类型节点,是则继续递归对比子节点,否则直接替换

function patch(vnode, newVnode) {
  if (isSameNode(vnode, newVnode)) patchVnode(vnode, newVnode)
  else replaceVnode(vnode, newVnode)
}

function replaceVnode(vnode, newVnode) {
  let el = vnode.el // 旧节点
  let parentEl = api.getParentNode(el) // 获取父节点
  api.insertBefore(parentEl, createElement(newVnode), api.getNextSibling(el)) // 插入新节点
  api.removeChild(parentEl, el) // 删除旧节点
}

function isSameNode(vnode, newVnode) {
  return (
    vnode.key == newVnode.key && // key是否相同
    vnode.tag == newVnode.tag && // tag是否相同
    isDef(vnode.data) == isDef(newVnode.data) // 是否都定义了data
    // &&... 其他条件
  )
}

function patchVnode(vnode, newVnode) {
  let el = newVnode.el = vnode.el // 获取当前旧节点对应的dom,并赋值给新节点的el

  // 1.都为文本节点,且文本不一样
  if (vnode.text && newVnode.text && vnode.text != newVnode.text)
    return api.setElText(el, newVnode.text) // 替换文本
  
  let ch = vnode.children
  let newCh = newVnode.children
  if (ch && newCh) return updateChildren(el, ch, newCh) // 2.都有子节点,递归对比
  if (ch) return api.removeChild(el) // 3.vnode有子节点,newVnode无,删除子节点
  return replaceVnode(vnode, newVnode) // 4. newNode有子节点,vnode无,替换即可
}

updateChildren实现比较复杂,使用首尾四指针进行vnodenewVnode的对比

function updateChildren(el, ch, newCh) {
  // 子节点下标
  let l = 0
  let r = ch.length - 1
  let newL = 0
  let newR = newCh.length - 1

  // 子节点
  let lNode = ch[l]
  let rNode = ch[r]
  let newLNode = newCh[newL]
  let newRNode = newCh[newR]

  while (l <= r && newL <= newR) {
    if (!lNode || !rNode || !newLNode || !newRNode) { // 边界处理
      if (!lNode) lNode = ch[++l]
      if (!rNode) rNode = ch[--r]
      if (!newLNode) newLNode = newCh[++newL]
      if (!newRNode) newRNode = newCh[--newR]
      continue
    }
    
    // 新旧子节点首尾指针对比 l*newL、r*newR、l*newR、r*newL
    if (isSameNode(lNode, newLNode)) {
      patchVnode(lNode, newLNode)
      lNode = ch[++l]
      newLNode = newCh[++newL]
      continue
    }
    if (isSameNode(rNode, newRNode)) {
      patchVnode(rNode, newRNode)
      rNode = ch[--r]
      newRNode = newCh[--newR]
      continue
    }
    if (isSameNode(lNode, newRNode)) {
      patchVnode(lNode, newRNode)
      api.insertBefore(el, lNode.el, api.nextSibling(rNode.el))
      lNode = ch[++l]
      newRNode = newCh[--newR]
      continue
    }
    if (isSameNode(rNode, newLNode)) {
      patchVnode(rNode, newLNode)
      api.insertBefore(el, rNode.el, lNode.el)
      rNode = ch[--r]
      newLNode = newCh[++newL]
      continue
    }

    // 在vnode未知序列区间[l,r]生成key-idx的map表,用newLNode的key在未知序列中找到可复用的位置
    if (!keyIdxMap) keyIdxMap = getKeyIdxMap(ch, l, r) // map

    keyIdx = keyIdxMap.get(newLNode.key)
    if (!keyIdx) {
      api.insertBefore(el, createElement(newLNode), lNode.el)
    }
    else {
      let nodeToMove = ch[keyIdx]
      patchVnode(nodeToMove, newLNode)
      api.insertBefore(el, nodeToMove.el, lNode.el)
    }
    newLNode = newCh[++newL]
  }
}

function getKeyIdxMap(ch, l, r) {
  let map = new Map()
  while (l <= r) map.set(ch[l].key, l++)
  return map
}

到此这篇关于浅析Vue中Virtual DOM和Diff原理及实现的文章就介绍到这了,更多相关Vue Virtual DOM Diff内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 详解vue-cli脚手架中webpack配置方法

    详解vue-cli脚手架中webpack配置方法

    这篇文章主要介绍了详解vue-cli脚手架中webpack配置方法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-08-08
  • Vue格式化数据后切换页面出现NaN问题及解决

    Vue格式化数据后切换页面出现NaN问题及解决

    这篇文章主要介绍了Vue格式化数据后切换页面出现NaN问题及解决,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-06-06
  • 解决Antd 里面的select 选择框联动触发的问题

    解决Antd 里面的select 选择框联动触发的问题

    这篇文章主要介绍了解决Antd 里面的select 选择框联动触发的问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-10-10
  • vue+vant-UI框架实现购物车的复选框全选和反选功能

    vue+vant-UI框架实现购物车的复选框全选和反选功能

    这篇文章主要介绍了vue+vant-UI框架实现购物车的复选框全选和反选功能,本文通过实例代码给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下
    2019-11-11
  • vue如何在线预览各类型文件

    vue如何在线预览各类型文件

    这篇文章主要介绍了vue如何在线预览各类型文件问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-11-11
  • Vue3中注册全局的组件,并在TS中添加全局组件提示方式

    Vue3中注册全局的组件,并在TS中添加全局组件提示方式

    这篇文章主要介绍了Vue3中注册全局的组件,并在TS中添加全局组件提示方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-07-07
  • Vue高性能列表GridList组件及实现思路详解

    Vue高性能列表GridList组件及实现思路详解

    这篇文章主要为大家介绍了Vue高性能列表GridList组件及实现思路详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-11-11
  • 深入探讨Vue3实现多数据变化监听的方式

    深入探讨Vue3实现多数据变化监听的方式

    随着 Vue 3 的发布,框架带来了更多的新特性和增强,其中之一就是 watch 函数的升级,这一改进使得多个数据的变化侦听更加优雅和灵活,本文将深入探讨 Vue 3 中的 watch 函数,以及如何以更加优雅的方式实现对多个数据变化的监听
    2023-08-08
  • 利用vite创建vue3项目的全过程及一个小BUG详解

    利用vite创建vue3项目的全过程及一个小BUG详解

    Vite作为一个构建工具,提供了一种快速的方法来构建Vue应用,而Vue3 则是一个前端框架,提供了强大的功能来构建和管理前端项目,下面这篇文章主要给大家介绍了关于利用vite创建vue3项目的全过程及一个小BUG的相关资料,需要的朋友可以参考下
    2023-04-04
  • 详解VueJS应用中管理用户权限

    详解VueJS应用中管理用户权限

    本篇文章主要给大家讲述了VueJS应用中管理用户权限的详细过程和方法,以及相关的代码展示,需要的朋友参考下吧。
    2018-02-02

最新评论