首页 > 编程语言 >【vue源码】虚拟DOM和diff算法

【vue源码】虚拟DOM和diff算法

时间:2024-06-02 12:21:23浏览次数:22  
标签:vue oldVnode DOM elm vnode isDef 源码 && 节点

虚拟DOM 与 AST抽象语法树的区别

虚拟DOM是表示页面状态的JavaScript对象,(虚拟DOM只有实现diff算法)
而AST是表示代码语法结构的树形数据结构。
虚拟DOM通常用于优化页面渲染性能,
而AST通常用于进行代码静态分析、代码转换等操作。(AST主要执行compiler编译)

什么是虚拟DOM?

用JavaScript对象描述DOM的层次结构。DOM中的一切属性都在虚拟DOM中有对应的属性。

本质:真实dom的一个js对象
image

vue源码借鉴了snabbdom
render() 实现虚拟dom,h函数渲染虚拟dom的函数,返回的是一个 vnode对象

diff算法

一、 什么是diff算法?

diff算法本质:就是比较2个JS对象的差异。找到2个js对象的差异最小化更新视图

image

二、 diff算法比较:

1.只比较同层级的节点。
2.同层比较时,如果类型不同,会把该节点和该节点的所有子节点全部销毁。
3.类型相同时,使用key合理性能优化

三、 diff算法主要通过patch(oldVnode,newVnode)方法实现

image

patch函数

  • patch函数比较新旧虚拟节点,进行diff算法,更新。
  • patch判断是不是首次渲染?
    是首次渲染直接createElement。否则sameVnode判断元素类型是否相同,不同则直接替换, 类型相同则执行核心函数patchVnode.
点击查看代码
// 用于 比较 新老节点的不同,然后更新的 函数
function patch (oldVnode, vnode, hydrating, removeOnly) {
    // 当新节点不存在的时候,销毁旧节点
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }
 
    let isInitialPatch = false
    // 用来存储 insert 钩子函数,在 插入节点之后调用
    const insertedVnodeQueue = []
    // 如果旧节点 是未定义的,直接创建新节点
    if (isUndef(oldVnode)) {
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {
      const isRealElement = isDef(oldVnode.nodeType)
      // 当老节点不是真实的 dom 节点, 当两个节点是相同节点的时候,进入 patctVnode 的过程
      // 而 patchVnode 也是 传说中 diff updateChildren 的调用者
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } else {
        // 当老节点是真实存在的 dom 节点的时候
        if (isRealElement) {
          // 当 老节点是 真实节点,而是在 ssr 环境的时候,修改 SSR_ATTR 属性
          if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
            oldVnode.removeAttribute(SSR_ATTR)
            hydrating = true
          }
          ....
          // 设置 oldVnode 为一个包含 oldVnode 的无属性节点
          oldVnode = emptyNodeAt(oldVnode)
        }
 
        // replacing existing element
        const oldElm = oldVnode.elm
        // 获取父亲节点,这样方便 删除或者增加节点
        const parentElm = nodeOps.parentNode(oldElm)
 
        // 在 dom 中插入新节点
        createElm(
          vnode,
          insertedVnodeQueue,
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        )
 
        // 递归 更新父占位符元素
        // 就是执行一遍 父节点的 destory 和 create 、insert 的 钩子函数
        // 类似于 style 组件,事件组件,这些 钩子函数
        if (isDef(vnode.parent)) {
          let ancestor = vnode.parent
          const patchable = isPatchable(vnode)
          while (ancestor) {
            for (let i = 0; i < cbs.destroy.length; ++i) {
              cbs.destroy[i](ancestor)
            }
            ancestor.elm = vnode.elm
            if (patchable) {
              for (let i = 0; i < cbs.create.length; ++i) {
                cbs.create[i](emptyNode, ancestor)
              }
              const insert = ancestor.data.hook.insert
              if (insert.merged) {
                for (let i = 1; i < insert.fns.length; i++) {
                  insert.fns[i]()
                }
              }
            } else {
              registerRef(ancestor)
            }
            ancestor = ancestor.parent
          }
        }
 
        // 销毁老节点
        if (isDef(parentElm)) {
          removeVnodes([oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
          // 触发老节点 的 destory 钩子
          invokeDestroyHook(oldVnode)
        }
      }
    }
    // 执行 虚拟 dom 的 insert 钩子函数
    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    // 返回最新 vnode 的 elm ,也就是真实的 dom节点
    return vnode.elm
  }

patchVnode

patchVnode函数对比 两个虚拟dom不同的地方, 同时 也是 updateChildren 调用递归 的 函数

点击查看代码
function patchVnode (
    oldVnode,  // 旧节点
    vnode,     // 新节点
    insertedVnodeQueue,  // 插入节点的队列
    ownerArray,      // 节点 数组
    index,           // 当前 节点的
    removeOnly       // 只有在 patch 函数中被传入,当老节点不是真实的 dom 节点,当新老节点是相同节点的时候
  ) {
    // 如果新节点和旧节点 相等(使用了 同一个地址,直接返回不进行修改)
    // 这里就是 当 props 没有改变的时候,子组件不会做渲染,而是直接复用
    if (oldVnode === vnode) {
      return
    }
 
    if (isDef(vnode.elm) && isDef(ownerArray)) {
      // clone reused vnode
      vnode = ownerArray[index] = cloneVNode(vnode)
    }
 
    const elm = vnode.elm = oldVnode.elm
    // 当 当前节点 是 注释节点(被 v-if )了,或者是一个 异步函数节点,那不执行
    if (isTrue(oldVnode.isAsyncPlaceholder)) {
      if (isDef(vnode.asyncFactory.resolved)) {
        hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
      } else {
        vnode.isAsyncPlaceholder = true
      }
      return
    }
 
    // 当前节点 是一个静态节点的时候,或者 标记了 once 的时候,那不执行
    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
    // 调用 prepatch 的钩子函数
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
      i(oldVnode, vnode)
    }
 
    const oldCh = oldVnode.children
    const ch = vnode.children
    // 调用 update 钩子函数
    if (isDef(data) && isPatchable(vnode)) {
      // 这里 的 update 钩子函数式 vnode 本身的钩子函数
      for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
      // 这里的 update 钩子函数  是 用户传过来的 钩子函数
      if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
    }
    // 新节点 没有 text 属性
    if (isUndef(vnode.text)) {
      // 如果都有子节点,对比更新子节点
      if (isDef(oldCh) && isDef(ch)) {
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      } else if (isDef(ch)) { // 新节点存在,但是老节点不存在
        // 如果老节点是  text, 清空
        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)) { // 如果老节点是  text, 清空
        nodeOps.setTextContent(elm, '')
      }
       // 新旧节点 text 属性不一样
    } else if (oldVnode.text !== vnode.text) {
      // 将 text 设置为 新节点的 text
      nodeOps.setTextContent(elm, vnode.text)
    }
    if (isDef(data)) {
      // 执行 postpatch 钩子函数
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
  }

updateChildren

采用双端比较,4个指针来实现 。旧节点的2个头尾指针, 新节点的2个头尾指针。
头头 、 尾尾 、头尾 、尾头
image

点击查看代码
function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
	// 旧节点头指针
    let oldStartIdx = 0 
	 // 新节点头指针
    let newStartIdx = 0 
	 // 旧节点尾指针
    let oldEndIdx = oldCh.length - 1 
	// 新节点尾指针
	let newEndIdx = newCh.length - 1
	
    let oldStartVnode = oldCh[0]
    let oldEndVnode = oldCh[oldEndIdx]

    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)
    }

    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)) {
        // 继续深度patch
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
        // 【尾尾】新后和旧后是相同节点,指针前移 
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        // 继续深度patch
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        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 {
        // 首先将旧数组转换成 节点 key-下标 的map (key值是唯一的)
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        // 判断当前新前指针指向节点是否存在key,若存在直接在旧数组对应的map中找到该key对应的下标,若不存在遍历旧数组中剩余的节点,找到新节点对应匹配的下标
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
        // 如果下标为空,代表该新节点是一个新节点,需要新增节点
        if (isUndef(idxInOld)) { // New element
          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)
    }
  }

createElm,将 vnode 转换为 真实的 dom 节点

  • 判断当前的 vnode tag 标签是否是存在的
  • 如果存在,创建对应的节点,然后 设置 样式的 作用域
  • 遍历子元素,并插入节点之中
  • 触发 create 钩子函数
  • 如果tag 标签不存在,判断是否是 注释节点,然后创建
  • 如果tag 标签不存在,且不是 注释节点,直接创建文本节点
点击查看代码
// 把 vnode 转换为 真实的 dom,挂载到 节点上
  function createElm (
    vnode,               // vnode
    insertedVnodeQueue,  // inserted 钩子函数 
    parentElm,
    refElm,              // 如果这个存在的话,就插到这个节点之前
    nested,
    ownerArray,
    index
  ) {
    // 如果存在子节点的话,就会克隆一遍
    if (isDef(vnode.elm) && isDef(ownerArray)) {
      // This vnode was used in a previous render!
      // now it's used as a new node, overwriting its elm would cause
      // potential patch errors down the road when it's used as an insertion
      // reference node. Instead, we clone the node on-demand before creating
      // associated DOM element for it.
      vnode = ownerArray[index] = cloneVNode(vnode)
    }
 
    vnode.isRootInsert = !nested // for transition enter check
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
      return
    }
 
    const data = vnode.data
    const children = vnode.children
    const tag = vnode.tag
    // 是否是标签
    if (isDef(tag)) {
      if (process.env.NODE_ENV !== 'production') {
        if (data && data.pre) {
          creatingElmInVPre++
        }
        // 如果是一个未定义标签
        if (isUnknownElement(vnode, creatingElmInVPre)) {
          warn(
            'Unknown custom element: <' + tag + '> - did you ' +
            'register the component correctly? For recursive components, ' +
            'make sure to provide the "name" option.',
            vnode.context
          )
        }
      }
      // 是否有 命名空间,主要是 svg
      vnode.elm = vnode.ns
        ? nodeOps.createElementNS(vnode.ns, tag)
        : nodeOps.createElement(tag, vnode)
      setScope(vnode)  // 设置样式的作用域 
 
      /* istanbul ignore if */
      if (__WEEX__) {
        。。。
      } else {
        // 把子元素设置为  vnode 的对象
        createChildren(vnode, children, insertedVnodeQueue)
        if (isDef(data)) {
          // 触发 create 钩子函数
          invokeCreateHooks(vnode, insertedVnodeQueue)
        }
        // 将 创建好的 vnode 插入到 parent 中,如果 refElm 存在的话,就插入到 refElm 元素之前
        insert(parentElm, vnode.elm, refElm)
      }
 
      if (process.env.NODE_ENV !== 'production' && data && data.pre) {
        creatingElmInVPre--
      }
    } else if (isTrue(vnode.isComment)) {  // 是否是注释节点
      vnode.elm = nodeOps.createComment(vnode.text)  // 创建 注释的文本 节点
      insert(parentElm, vnode.elm, refElm)
    } else {  // Text 标签
      vnode.elm = nodeOps.createTextNode(vnode.text)  // 创建 文本节点
      insert(parentElm, vnode.elm, refElm)
    }
  }

标签:vue,oldVnode,DOM,elm,vnode,isDef,源码,&&,节点
From: https://www.cnblogs.com/lingXie/p/18158663

相关文章

  • 【手撕面试题】Vue(高频知识点三)
            每天10道题,100天后,搞定所有前端面试的高频知识点,加油!!!在看文章的同时,希望不要直接看答案,先思考一下自己会不会,如果会,自己的答案是什么?想过之后再与答案比对,是不是会更好一点,当然如果你有比我更好的答案,欢迎评论区留言,一起探讨技术之美。目录面试官:请简述一下v......
  • openeuler源码安装Postgresql 16
    准备条件OpenEuler(虚拟机):版本:22.03-LTS-SP3下载地址:https://www.openeuler.org/zh/download/PostgreSQL:版本:16.3源码包下载地址:https://www.postgresql.org/ftp/source/操作系统安装安装过程与centos基本一致,此处就省略了,安装的时候可以把需要的网络工具和开发工具包勾......
  • 开源多企业AI智能名片小程序源码中的市场细分策略分析
    摘要:在数字化营销的新时代,开源多企业AI智能名片小程序源码为众多企业提供了快速构建智能名片系统的能力。其中,市场细分作为营销策略的重要组成部分,对于提高营销效果、满足消费者需求具有重要意义。本文将以开源多企业AI智能名片小程序源码为背景,探讨市场细分中的行为细分和心......
  • gcc源码分析 GIMPLIFY相关
    gcc源码分析GIMPLIFY相关四、GIMPLIFY相关4.1gcc全局符号表与符号分析4.1.1全局符号表4.1.2函数节点的gimplify4.2gimple高端化4.2.1gimplify_body函数4.2.2gimplify_stmt4.3gimple低端化4.3.1pass_lower_cf4.3.2pass_build_cfg4.3.3p......
  • vue3 vite 项目tsx写法尝试
    vite配置上面jsx插件搞好就能在vue项目中使用jsx写法了代码尝试ChildWorld.vue<scriptlang="tsx">import{defineComponent,defineProps}from"vue"constchildAbc=()=>{return(<div>childAbc</div>)}constchildCbd=(props,......
  • 【Vue】深入理解MVVM模式的魔力
    目录前言一、MVVM模式是什么?二、具体示例总结前言    Vue.js是一种基于JavaScript的前端框架,它采用了MVVM(Model-View-ViewModel)模式来实现数据的双向绑定。在本篇博客中,我将介绍MVVM模式的基本概念,并演示如何使用Vue.js来实现这种模式。一、MVVM模式是什么? ......
  • 【Vue】中v-if和v-show的区别到底在哪里?
    概要   Vue.js是一种流行的JavaScript框架,用于构建交互式的Web应用程序。在Vue.js中,v-if和v-show是两个常用的指令,用于控制DOM元素的显示与隐藏。本文将介绍它们之间的区别。整体架构流程   Vue.js的整体架构基于虚拟DOM和响应式数据,当数据发生变化时,Vue会重新......
  • Vue插槽与作用域插槽
    title:Vue插槽与作用域插槽date:2024/6/1下午9:07:52updated:2024/6/1下午9:07:52categories:前端开发tags:VueSlotScopeSlot组件通信Vue2/3插槽作用域API动态插槽插槽优化第1章:插槽的概念与原理插槽的定义在Vue.js中,插槽(Slots)是一种强大的功能,它允许你......
  • Vue-router之页面跳转
    目录1.Vue Router1.1VueRouter的简介1.2安装1.3创建路由器实例2.router-link页面跳转2.1.router-link简介2.2使用路由对象的query属性进行传参1.Vue Router1.1VueRouter的简介官方文档见:https://router.vuejs.org/zh/introduction.htmlhttps://router.v......
  • 基于uniapp+vue+nodejs高校食堂餐厅点餐系统2x2v4 小程序hbuilderx
    近年来,我国餐饮业发展的质量和内涵发生了重大的变化。行业的经营领域和市场空间不断变化,经营档次和企业管理水平不断提高,经营业态日趋丰富,投资主体和消费需求多元化特点更加突出,网点数量和人员队伍继续扩大;餐饮市场更加繁荣,消费的个性化和特色化的趋势明显,追求健康营养和连锁规......