IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> Vue源码(一) 虚拟DOM和Diff算法 -> 正文阅读

[JavaScript知识库]Vue源码(一) 虚拟DOM和Diff算法

一 真实DOM及其解析流程

浏览器渲染引擎工作大致分为五步: 创建 DOM 树——创建 Style Rules ——创建Render树——布局 Layout ——绘制 Painting

  • 第一步,创建DOM树:使用HTML解析器,解析HTML元素,构建DOM树
  • 第二步,生成样式表:用 CSS 分析器,分析 CSS 文件和元素上的 inline 样式,生成页面的样式表;
  • 第三步,创建Render树:DOM树与样式表关联,构建出Render树,每个 DOM 节点都有 attach 方法,接受样式信息,返回一个 render 对象;
  • 第四步,确定节点坐标,实现布局:根据Render树的结构,为每个Render树上的节点确定一个在显示屏上出现的精确坐标;
  • 第五步,绘制页面:根据Render树和节点的左边,调用节点的paint方法,将其绘制出来

二 虚拟DOM

虚拟DOM,是一个用来表示真实DOM的对象
真实DOM:

<ul id="list">
    <li class="item">a</li>
    <li class="item">b</li>
    <li class="item">c</li>
</ul>

虚拟DOM:

let oldDOM ={ 
        tagName: 'ul', // 标签名
        props: { // 标签属性
            id: 'list'
        },
        children: [ // 标签子节点
            {
                tagName: 'li', props: { class: 'item' }, children: ['a']
            },
            {
                tagName: 'li', props: { class: 'item' }, children: ['b']
            },
            {
                tagName: 'li', props: { class: 'item' }, children: ['c']
            },
        ]
    }

真实DOM中数据被修改时会生成新的一个虚拟DOM,然后Vue会对新旧的虚拟DOM进行比较

<ul id="list">
    <li class="item">a</li>
    <li class="item">b</li>
    <li class="item">d</li>
</ul>
let newDOM ={ 
        tagName: 'ul', // 标签名
        props: { // 标签属性
            id: 'list'
        },
        children: [ // 标签子节点
            {
                tagName: 'li', props: { class: 'item' }, children: ['a']
            },
            {
                tagName: 'li', props: { class: 'item' }, children: ['b']
            },
            {
                tagName: 'li', props: { class: 'item' }, children: ['d']
            },
        ]
    }

使用虚拟DOM的好处

  • 频繁操作真实DOM会可能会导致浏览器回流和重,导致页面前端性能下降

  • 虚拟DOM可以实现跨平台渲染,服务端渲染、小程序、原生应用都使用虚拟DOM

  • 使用虚拟DOM时,改变了当前的状态不需要立即的去更新DOM,对需要更新的内容进行更新,对于没有改变的内容不做任何操作,通过前后两次差异进行比较

  • 虚拟 DOM 可以维护程序的状态,跟踪上一次的状态

  • 真实DOM的属性很多,创建DOM节点开销很大,虚拟DOM只是普通JavaScript对象,描述属性并不需要很多,创建开销很小

三 Diff算法

——比较两颗虚拟DOM树差异

  • 上述Vue经过对比新旧DOM,发现一个li标签的文本发生了改变,则会去操作修改真实DOM的对应节点,而不是所有的节点都更新一遍。而查找出哪一个标签发生改变使用的算法就是Diff算法,对比旧DOM和新DOM区别。

  • Diff算法在进行对比的时候,Diff算法指在同级比较,不会跨级比较。

  • 首先Diff算法会进行进行深度遍历,记录有差异的节点

Diff 对比流程:

页面数据发生改变时会触发 setter,并且通过 Dep.notify 去通知所有的订阅者 watcher,订阅者会调用 patch方法,给真实DOM进行打补丁,更新相应的视图

patch()方法

先判断同层的虚拟节点是否为同一种类型的标签:

  • 如果是,需要调用 patchVnode 方法进行深层次对比
  • 若不是同一类型标签,直接替换为新的虚拟节点
function patch(oldVnode, newVnode) {
  if (sameVnode(oldVnode, newVnode)) {
    patchVnode(oldVnode, newVnode)  //若为同一类型节点,进行深度比较
  } else {
    // 否
    const oldEl = oldVnode.el 
    const parentEle = api.parentNode(oldEl) //获取旧元素的父节点
    createEle(newVnode)  //根据新的虚拟节点创建真实的节点
    if (parentEle !== null) {
      api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) // 将新节点添加进父节点
      api.removeChild(parentEle, oldVnode.el)  // 移除以前的旧元素节点
      // 设置null,释放内存
      oldVnode = null
    }
  }
  return newVnode
}

sameVnode()方法

用于判断是否为同一类型的标签节点

function sameVnode(oldVnode, newVnode) {
  return (
    oldVnode.key === newVnode.key && // key值是否一样
    oldVnode.tagName === newVnode.tagName && // 标签名是否一样
    oldVnode.isComment === newVnode.isComment && // 是否都为注释节点
    isDef(oldVnode.data) === isDef(newVnode.data) && // 是否都定义了data
    sameInputType(oldVnode, newVnode) // 当标签为input时,type必须是否相同
  )
}

patchVnode()方法

若为同一节点,继续比较下去:

第一层判断:是否为同一个对象

? 第二层:都是文本节点且文本不一样

? 第三层判断:都有子节点且子节点不一样

? 新节点有子节点,旧没有

? 旧节点有子节点,新没有

function patchVnode(oldVnode, newVnode) {
  const el = newVnode.el = oldVnode.el // 获取新旧虚拟节点共同对应的真实DOM对象
  const oldCh = oldVnode.children, newCh = newVnode.children// 获取新旧虚拟节点的子节点数组
  if (oldVnode === newVnode) return  //如果新旧虚拟节点是同一个对象,则终止
  
  if (oldVnode.text !== null && newVnode.text !== null && oldVnode.text !== newVnode.text) {   
    api.setTextContent(el, newVnode.text) // 如果新旧虚拟节点是文本节点,且文本不一样:则直接将真实DOM中文本更新为新虚拟节点的文本
  } else {
      
    if (oldCh && newCh && oldCh !== newCh) {
      updateChildren(el, oldCh, newCh)// 新旧虚拟节点都有子节点:且子节点不一样,调用updateChildren()对比子节点并更新
    } else if (newCh) {
        
      createEle(newVnode)   //新虚拟节点有子节点,旧虚拟节点没有:创建新虚拟节点的子节点,并更新到真实DOM上去
    } else if (oldCh) {
     
      api.removeChild(el) // 旧虚拟节点有子节点,新虚拟节点没有:直接删除真实DOM里对应的子节点
    }
  }
}

updateChildren()方法

真实的DOM节点布置变化需要以新节点为基准

patchVnode() 中重要的方法。vue2 中使用了首尾指针的方法,oldS 旧DOM首元素,oldE 旧DOM尾元素;newS 新DOM首元素,newE 新DOM尾元素

  • 比较方法,匹配相同的节点:

    • 1、oldS 和 newS使用sameVnode方法进行比较,sameVnode(oldS, newS)

    • 2、oldS 和 newE使用sameVnode方法进行比较,sameVnode(oldS, newE)

    • 3、oldE 和 newS使用sameVnode方法进行比较,sameVnode(oldE, newS)

    • 4、oldE 和 newE使用sameVnode方法进行比较,sameVnode(oldE, newE)

    • 如果以上逻辑都匹配不到,再把所有旧子节点的 key 做一个映射到旧节点下标的 key -> index 表,然后用新 vnodekey 去找出在旧节点中可以复用的位置。

  • 找到匹配的节点后将真实DOM调换到以新DOM节点为基准的位置上。提示用于匹配的两个指针向中间缩进

  • 若新DOM比旧DOM长,则真实DOM插入元素与对应的位置,若旧DOM比新DOM长,则真实DOM会根据其删除元素

核心代码:

function updateChildren(parentElm, oldCh, newCh) {
  let oldStartIdx = 0, 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
  let idxInOld
  let elmToMove
  let before
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if (oldStartVnode == null) {
      oldStartVnode = oldCh[++oldStartIdx]
    } else if (oldEndVnode == null) {
      oldEndVnode = oldCh[--oldEndIdx]
    } else if (newStartVnode == null) {
      newStartVnode = newCh[++newStartIdx]
    } else if (newEndVnode == null) {
      newEndVnode = newCh[--newEndIdx]
    } else if (sameVnode(oldStartVnode, newStartVnode)) {
      patchVnode(oldStartVnode, newStartVnode)
      oldStartVnode = oldCh[++oldStartIdx]
      newStartVnode = newCh[++newStartIdx]
    } else if (sameVnode(oldEndVnode, newEndVnode)) {
      patchVnode(oldEndVnode, newEndVnode)
      oldEndVnode = oldCh[--oldEndIdx]
      newEndVnode = newCh[--newEndIdx]
    } else if (sameVnode(oldStartVnode, newEndVnode)) {
      patchVnode(oldStartVnode, newEndVnode)
      api.insertBefore(parentElm, oldStartVnode.el, api.nextSibling(oldEndVnode.el))
      oldStartVnode = oldCh[++oldStartIdx]
      newEndVnode = newCh[--newEndIdx]
    } else if (sameVnode(oldEndVnode, newStartVnode)) {
      patchVnode(oldEndVnode, newStartVnode)
      api.insertBefore(parentElm, oldEndVnode.el, oldStartVnode.el)
      oldEndVnode = oldCh[--oldEndIdx]
      newStartVnode = newCh[++newStartIdx]
    } else {
      // 使用key时的比较
      if (oldKeyToIdx === undefined) {
        oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // 有key生成index表
      }
      idxInOld = oldKeyToIdx[newStartVnode.key]
      if (!idxInOld) {
        api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
        newStartVnode = newCh[++newStartIdx]
      }
      else {
        elmToMove = oldCh[idxInOld]
        if (elmToMove.sel !== newStartVnode.sel) {
          api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
        } else {
          patchVnode(elmToMove, newStartVnode)
          oldCh[idxInOld] = null
          api.insertBefore(parentElm, elmToMove.el, oldStartVnode.el)
        }
        newStartVnode = newCh[++newStartIdx]
      }
    }
  }
  if (oldStartIdx > oldEndIdx) {
    before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].el
    addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx)
  } else if (newStartIdx > newEndIdx) {
    removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
  }
}

Diff 掘金

  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2021-09-05 10:43:24  更:2021-09-05 10:45:36 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 15:33:17-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码