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

目录
  • 0. 写在开头
  • 1. vdom
  • 2. 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内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Vue的transition-group与Virtual Dom Diff算法的使用

    开始 这次的题目看上去好像有点奇怪:把两个没有什么关联的名词放在了一起,正如大家所知道的,transition-group就是Vue的内置组件之一主要用在列表的动画上,但是会跟Virtual Dom Diff算法有什么特别的联系吗?答案明显是有的,所以接下来就是代码分解. 缘起 主要是最近对Vue的Virtual Dom Diff算法有点模糊了,然后顺手就打开了电脑准备温故知新:但是很快就留意到代码: // removeOnly is a special flag used only by <t

  • vue.js diff算法原理详细解析

    目录 diff算法的概念 虚拟Dom h函数 diff对比规则 patch patchVnode updateChildren 总结 diff算法的概念 diff算法可以看作是一种对比算法,对比的对象是新旧虚拟Dom.顾名思义,diff算法可以找到新旧虚拟Dom之间的差异,但diff算法中其实并不是只有对比虚拟Dom,还有根据对比后的结果更新真实Dom. 虚拟Dom 上面的概念我们提到了虚拟Dom,相信大家对这个名词并不陌生,下面为大家解释一下虚拟Dom的概念,以及diff算法中为什么要对比虚拟

  • Vue实现virtual-dom的原理简析

    virtual-dom(后文简称vdom)的概念大规模的推广还是得益于react出现,virtual-dom也是react这个框架的非常重要的特性之一.相比于频繁的手动去操作dom而带来性能问题,vdom很好的将dom做了一层映射关系,进而将在我们本需要直接进行dom的一系列操作,映射到了操作vdom,而vdom上定义了关于真实dom的一些关键的信息,vdom完全是用js去实现,和宿主浏览器没有任何联系,此外得益于js的执行速度,将原本需要在真实dom进行的创建节点,删除节点,添加节点等一系列复

  • React DOM diff 对比Vue DOM diff 区别详解

    目录 React DOM diff 和 Vue DOM diff 的区别 React DOM diff 代码查看流程 总结 React DOM diff 和 Vue DOM diff 的区别 React 是从左向右遍历对比,Vue 是双端交叉对比. React 需要维护三个变量(我看源码发现是五个变量),Vue 则需要维护四个变量. Vue 整体效率比 React 更高,举例说明:假设有 N 个子节点,我们只是把最后子节点移到第一个,那么 React 需要进行借助 Map 进行 key 搜索找到

  • vue 中Virtual Dom被创建的方法

    本文将通过解读render函数的源码,来分析vue中的vNode是如何创建的.在vue2.x的版本中,无论是直接书写render函数,还是使用template或el属性,或是使用.vue单文件的形式,最终都需要编译成render函数进行vnode的创建,最终再渲染成真实的DOM. 如果对vue源码的目录还不是很了解,推荐先阅读下 深入vue -- 源码目录和编译过程. 01  render函数 render方法定义在文件 src/core/instance/render.js 中 Vue.pro

  • vue的Virtual Dom实现snabbdom解密

    vue在官方文档中提到与react的渲染性能对比中,因为其使用了snabbdom而有更优异的性能. JavaScript 开销直接与求算必要 DOM 操作的机制相关.尽管 Vue 和 React 都使用了 Virtual Dom 实现这一点,但 Vue 的 Virtual Dom 实现(复刻自 snabbdom)是更加轻量化的,因此也就比 React 的实现更高效. 看到火到不行的国产前端框架vue也在用别人的 Virtual Dom开源方案,是不是很好奇snabbdom有何强大之处呢?不过正式

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

    目录 0. 写在开头 1. vdom 2. 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: '

  • vue中虚拟DOM与Diff算法知识精讲

    目录 前言 知识点: 虚拟DOM(Virtual DOM): 虚拟dom库 diff算法 snabbdom的核心 init函数 h函数 patch函数(核心) 题外话:diff算法简介 传统diff算法 snabbdom的diff算法优化 updateChildren(核中核:判断子节点的差异) 新结束节点和旧结束节点(情况2) 旧结束节点/新开始节点(情况4) 前言 面试官:"你了解虚拟DOM(Virtual DOM)跟Diff算法吗,请描述一下它们"; 我:"额,...鹅

  • Vue中的 DOM与Diff详情

    目录 DOM Diff 整体思路 处理简单情况 比对优化 尾部新增元素 头部新增元素 开始元素移动到末尾 末尾元素移动到开头 乱序比对 写在最后 DOM Diff Vue创建视图分为俩种情况: 首次渲染,会用组件template转换成的真实DOM来替换应用中的根元素 当数据更新后,视图重新渲染,此时并不会重新通过组件template对应的虚拟节点来创建真实DOM,而是会用老的虚拟节点和新的虚拟节点进行比对,根据比对结果来更新DOM 第二种情况就是Vue中经常谈到的DOM Diff,接下来我们将详

  • 深入浅析Vue 中 ref 的使用

    官网上的说明 一.前言# 在之前的前端开发中,为了实现我们的需求,通常采用的方案是通过 JS/Jquery 直接操纵页面的 DOM 元素,得益于 Jquery 对于 DOM 元素优异的操作能力,我们可以很轻易的对获取到的 DOM 元素进行操作.但是,当我们开始在前端项目中使用 Vue 这类的 MVVM 框架之后,对于 DOM 的操作我们就应当完全的交给框架,而我们只需要关注于数据.难道,在 Vue 中就不能手动获取到页面上的 DOM 元素了吗,答案当然是可以手动获取到 DOM 元素的,在 Vue

  • 解决vue中虚拟dom,无法实时更新的问题

    碰到的问题:使用jq获取元素节点的个数时一直为0 解决方法:使用vue的nextTick()函数即可解决 原理:nextTick可以在下一次更新dom之后进行回调,我的问题在于,在页面加载完成时无法获取虚拟dom,而使用回调函数后就可以获取到正确的dom数量,所以只需要在nextTick函数中执行jq函数就可以正确获取了. self.$nextTick(function () { // DOM 更新了 $("#myCarousel").carousel(0); }) 以上这篇解决vue

  • 浅析Vue 中的 render 函数

    render函数是什么 简单的说,在vue中我们使用模板HTML语法组建页面的,使用render函数我们可以用js语言来构建DOM 因为vue是虚拟DOM,所以在拿到template模板时也要转译成VNode的函数,而用render函数构建DOM,vue就免去了转译的过程. 当使用render函数描述虚拟DOM时,vue提供一个函数,这个函数是就构建虚拟DOM所需要的工具.官网上给他起了个名字叫createElement.还有约定的简写叫h, vm中有一个方法_c,也是这个函数的别名 先看官网对

  • 浅析vue中的nextTick

    背景 vue是异步渲染的,当data改变之后,DOM不会立刻被渲染,页面渲染时会将data的修改做整合,多次data修改只会做整合最后一次性渲染出来,这也是异步渲染的原因.只有异步渲染才可以实现整合操作. 例子 methods: { update() { for (let i = 0; i < 10; i++) { this.testNum = this.testNum + i; } }, }, 在你的 Vue 视图中, testNum 会发生变化.不过需要注意的是这个变化的过程,虽然我们把 f

  • vue中view-model双向绑定基础原理解析

    利用Object.defineProperty进行数据劫持 代码如下 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Vue

随机推荐