Vue渲染系统模块

news2024/11/26 16:40:15

我们想要实现一个简洁版的Mini-Vue框架,应该包含三个模块:分别是:渲染系统模块、可响应式系统模块、应用程序入库模块。

这篇就来写一下渲染系统模块。剩下两个模块后面有时间再更新。

vue渲染系统实现,应该包含三个功能,分别是:① h函数,用于返回一个VNode对象;② mount函数,用于将VNode转为真实dom,并挂载到DOM上;③ patch函数,类似于diff算法,用于对比两个VNode,决定如何处理新的VNode

虚拟dom本身其实就是个javascript对象。这里会写一些核心逻辑,大家如果看vue源码的话,会发现里面有非常多的边界情况的处理判断。我们只需要知道核心逻辑就可以了,也就是下面所写的。

下面写的会有用到个判断类型的方法,getObjType如下: 

也是最准确的判断数据类型的方法 

function getObjType(obj) {
  let toString = Object.prototype.toString
  let map = {
    '[object Boolean]': 'boolean',
    '[object Number]': 'number',
    '[object String]': 'string',
    '[object Function]': 'function',
    '[object Array]': 'array',
    '[object Date]': 'date',
    '[object RegExp]': 'regExp',
    '[object Undefined]': 'undefined',
    '[object Null]': 'null',
    '[object Object]': 'object'
  }
  if (obj instanceof Element) {
    return 'element'
  }
  return map[toString.call(obj)]
}

h函数 

h函数其实非常简单,就是返回一个js对象 

/**
 * 虚拟dom本身就是个javascript对象
 * @param {String} tag 标签名称
 * @param {Object} props 属性配置
 * @param {String | Array} children 子元素
 * @returns 
 */
const h = (tag, props, children) => ({ tag, props, children })

 

可以看到,这就是个虚拟的dom树 

mount函数 

mount函数负责将vnode转为真实的dom,并挂载到指定容器下。 

/**
 * 将虚拟dom转为真实dom并挂载到指定容器
 * @param {Object} vnode 虚拟dom
 * @param {Element} container 容器
 */
const mount = (vnode, container) => {
  const el = vnode.el = document.createElement(vnode.tag)

  if(vnode.props) {
    for(let key in vnode.props) {
      const value = vnode.props[key]
      if(key.startsWith('on')) {
        el.addEventListener(key.slice(2).toLowerCase(), value)
      } else {
        el.setAttribute(key, value)
      }
    }
  }

  if(vnode.children) {
    if(getObjType(vnode.children) === 'string') {
      el.textContent = vnode.children
    } else if(getObjType(vnode.children) === 'array') {
      vnode.children.forEach(item => {
        mount(item, el)
      })
    }
  }

  container.appendChild(el)
}

这里第三个参数只考虑了,string和array的情况,还有对象的情况相对少一些,就是我们平常的使用的插槽 

patch函数 

patch函数类似于diff算法,用于两个VNode进行对比,决定如何处理新的VNode

/**
 * 类似diff对比两个vnode
 * @param {Object} n1 旧vnode
 * @param {Object} n2 新vnode
 */
const patch = (n1, n2) => {
  if(n1.tag !== n2.tag) {
    const n1parentEl = n1.el.parentElement
    n1parentEl.removeChild(n1.el)
    mount(n2, n1parentEl)
  } else {
    const el = n2.el = n1.el

    // 处理props
    const oldProps = n1.props || {}
    const newProps = n2.props || {}

    for(let key in newProps) {
      const oldValue = oldProps[key]
      const newValue = newProps[key]
      if(newValue !== oldValue) {
        if (key.startsWith('on')) {
          el.addEventListener(key.slice(2).toLowerCase(), newValue)
        } else {
          el.setAttribute(key, newValue)
        }
      }
    }

    for(let key in oldProps) {
      if(!(key in newProps)) {
        if (key.startsWith('on')) {
          const oldValue = oldProps[key]
          el.removeEventListener(key.slice(2).toLowerCase(), oldValue)
        } else {
          el.removeAttribute(key)
        }
      }
    }

    
    const oldChildren = n1.children || []
    const newChildren = n2.children || []

    if(getObjType(newChildren) === 'string') {
      if(getObjType(oldChildren) === 'string') {
        if(newChildren !== oldChildren) {
          el.textContent = newChildren
        }
      } else {
        el.innerHTML = newChildren
      }
    } else if(getObjType(newChildren) === 'array') {
      if(getObjType(oldChildren) === 'string') {
        el.innerHTML = ''
        newChildren.forEach(item => {
          mount(item, el)
        })
      } else if(getObjType(oldChildren) === 'array') {
        // oldChildren -> [vnode1, vnode2, vnode3]
        // newChildren -> [vnode1, vnode5, vnode6, vnode7, vnode8]

        // 前面有相同节点的元素进行patch操作
        const commonLength = Math.min(newChildren.length, oldChildren.length)
        for(let i = 0; i < commonLength; i++) {
          patch(oldChildren[i], newChildren[i])
        }

        // 2. newChildren.length > oldChildren.length  多余的做挂载操作
        if(newChildren.length > oldChildren.length) {
          newChildren.slice(commonLength).forEach(item => {
            mount(item, el)
          })
        }

        // 3. newChildren.length < oldChildren.length  多余的做移除操作
        if(newChildren.length < oldChildren.length) {
          oldChildren.slice(commonLength).forEach(item => {
            el.removeChild(item.el)
          })
        }
      }
    }
  }
}

完整代码 

render.js 

/**
 * Vue渲染系统实现
 * 1. h函数,用于返回一个VNode对象;
 * 2. mount函数,用于将VNode挂载到DOM上;
 * 3. patch函数,用于对两个VNode进行对比,决定如何处理新的VNode;
 */


/**
 * 虚拟dom本身就是个javascript对象
 * @param {String} tag 标签名称
 * @param {Object} props 属性配置
 * @param {String | Array} children 子元素
 * @returns 
 */
const h = (tag, props, children) => ({ tag, props, children })

/**
 * 将虚拟dom转为真实dom并挂载到指定容器
 * @param {Object} vnode 虚拟dom
 * @param {Element} container 容器
 */
const mount = (vnode, container) => {
  const el = vnode.el = document.createElement(vnode.tag)

  if(vnode.props) {
    for(let key in vnode.props) {
      const value = vnode.props[key]
      if(key.startsWith('on')) {
        el.addEventListener(key.slice(2).toLowerCase(), value)
      } else {
        el.setAttribute(key, value)
      }
    }
  }

  if(vnode.children) {
    if(getObjType(vnode.children) === 'string') {
      el.textContent = vnode.children
    } else if(getObjType(vnode.children) === 'array') {
      vnode.children.forEach(item => {
        mount(item, el)
      })
    }
  }

  container.appendChild(el)
}

/**
 * 类似diff对比两个vnode
 * @param {Object} n1 旧vnode
 * @param {Object} n2 新vnode
 */
const patch = (n1, n2) => {
  if(n1.tag !== n2.tag) {
    const n1parentEl = n1.el.parentElement
    n1parentEl.removeChild(n1.el)
    mount(n2, n1parentEl)
  } else {
    const el = n2.el = n1.el

    // 处理props
    const oldProps = n1.props || {}
    const newProps = n2.props || {}

    for(let key in newProps) {
      const oldValue = oldProps[key]
      const newValue = newProps[key]
      if(newValue !== oldValue) {
        if (key.startsWith('on')) {
          el.addEventListener(key.slice(2).toLowerCase(), newValue)
        } else {
          el.setAttribute(key, newValue)
        }
      }
    }

    for(let key in oldProps) {
      if(!(key in newProps)) {
        if (key.startsWith('on')) {
          const oldValue = oldProps[key]
          el.removeEventListener(key.slice(2).toLowerCase(), oldValue)
        } else {
          el.removeAttribute(key)
        }
      }
    }

    
    const oldChildren = n1.children || []
    const newChildren = n2.children || []

    if(getObjType(newChildren) === 'string') {
      if(getObjType(oldChildren) === 'string') {
        if(newChildren !== oldChildren) {
          el.textContent = newChildren
        }
      } else {
        el.innerHTML = newChildren
      }
    } else if(getObjType(newChildren) === 'array') {
      if(getObjType(oldChildren) === 'string') {
        el.innerHTML = ''
        newChildren.forEach(item => {
          mount(item, el)
        })
      } else if(getObjType(oldChildren) === 'array') {
        // oldChildren -> [vnode1, vnode2, vnode3]
        // newChildren -> [vnode1, vnode5, vnode6, vnode7, vnode8]

        // 前面有相同节点的元素进行patch操作
        const commonLength = Math.min(newChildren.length, oldChildren.length)
        for(let i = 0; i < commonLength; i++) {
          patch(oldChildren[i], newChildren[i])
        }

        // 2. newChildren.length > oldChildren.length  多余的做挂载操作
        if(newChildren.length > oldChildren.length) {
          newChildren.slice(commonLength).forEach(item => {
            mount(item, el)
          })
        }

        // 3. newChildren.length < oldChildren.length  多余的做移除操作
        if(newChildren.length < oldChildren.length) {
          oldChildren.slice(commonLength).forEach(item => {
            el.removeChild(item.el)
          })
        }
      }
    }
  }
}


function getObjType(obj) {
  let toString = Object.prototype.toString
  let map = {
    '[object Boolean]': 'boolean',
    '[object Number]': 'number',
    '[object String]': 'string',
    '[object Function]': 'function',
    '[object Array]': 'array',
    '[object Date]': 'date',
    '[object RegExp]': 'regExp',
    '[object Undefined]': 'undefined',
    '[object Null]': 'null',
    '[object Object]': 'object'
  }
  if (obj instanceof Element) {
    return 'element'
  }
  return map[toString.call(obj)]
}

测试: 

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script src="./render.js"></script>
</head>
<body>

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

  <script>
    const vnode = h('div', { class: 'wft' }, [
      h('h1', null, '标题'),
      h('span', null, '哈哈哈')
    ])

    setTimeout(() => {
      mount(vnode, document.getElementById('app'))
    }, 1500)

    const newVNode = h('div', { class: 'new-wft', id: 'wft' }, [
      h('h3', null, '我是h3')
    ])

    setTimeout(() => {
      patch(vnode, newVNode)
    }, 4000)
  </script>
</body>
</html>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/764596.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

linux之Ubuntu系列(二)远程管理指令

shutdown shutdown 选项 时间 关机或重启 选项&#xff1a; -r 重新启动 提示 不指定选项和参数&#xff0c;默认表示1分钟之后 关闭电脑用SSH远程维护服务器时&#xff0c;最好不要关闭系统&#xff0c;而应该重新启动系统 -r shutdown常用示例 # 1分钟后关机 shutdown …

【C语言初阶(17)】初阶指针

文章目录 Ⅰ指针的概念Ⅱ 指针和指针类型⒈指针 - 整数⒉指针的解引用 Ⅲ 野指针⒈野指针成因⒉规避野指针 Ⅳ 指针运算⒈指针 - 整数⒉指针 - 指针⒊指针的关系运算 Ⅴ 指针和数组Ⅵ 二级指针Ⅶ 指针数组 Ⅰ指针的概念 指针的两个要点 指针是内存中一个最小单元的编号&#…

使用 torch.stft 进行短时傅里叶变换

python 常规的 stft 都是在 cpu 上进行计算&#xff0c;如果网络训练是在 GPU 上进行&#xff0c;那么就涉及到数据传输的问题&#xff0c;降低计算效率&#xff1b;而 torch 自带的 stft 可以直接在 GPU 上进行计算&#xff0c;因此可以节省计算时间。 import torch import t…

简单版本视频播放服务器V3-前端优化-播放器在左,列表在右侧【推荐】【完成】

做个家用版本的家庭影院&#xff0c;通过这个服务器可以给电脑&#xff0c;平板&#xff0c;手机等设备提供直接播放电影的作用&#xff0c;通过离线下载电影放入目录里就是就可以给全家提供电影播放了&#xff0c;通过浏览器就是可以访问电脑里面的视频&#xff0c;实现简单的…

Html基础知识学习——圣杯布局、margin负值、等高布局

文章目录 圣杯布局margin负值等高布局 圣杯布局 两边页面固定中间页面宽度随着浏览器大小自适应 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-widt…

代码随想录算法训练营day6 | 242. 有效的字母异位词,349. 两个数组的交集,202. 快乐数,1. 两数之和

目录 242. 有效的字母异位词 349. 两个数组的交集 202. 快乐数 1. 两数之和 242. 有效的字母异位词 242. 有效的字母异位词 难度&#xff1a;easy 类型&#xff1a;哈希表 思路&#xff1a; 代码&#xff1a; class Solution {public boolean isAnagram(String s, St…

计算机网络 day9 DNAT实验

目录 DNAT DNAT策略的典型应用环境 DNAT策略的原理 在网关中使用DNAT策略发布内网服务器 DNAT实验&#xff1a; 实验环境&#xff1a; DNAT网络规划拓扑图&#xff1a; 步骤&#xff1a; 1、创建linux客户端Web网站&#xff08;go语言&#xff09;&#xff0c;实现Web服…

二、DDL-1.数据库操作

一、查询 1、查询所有数据库 show databases; MySQL自带的默认的数据库有四个&#xff1a; 二、创建 1、创建一个新的数据库itcast&#xff08;不区分大小写&#xff09;&#xff1a; create database itcast; 查询所有数据库&#xff1a;多了itcast 2、再创建同名的数据库…

Latex合并多个公式且居中

要求&#xff1a;1&#xff1a;多个公式居中对齐 2&#xff1a;多个公式组合只有一个编号。 结果类似于这一种&#xff1a; 代码&#xff1a;使用gathered可以。 \begin{equation}\begin{gathered}\vspace{0.6em}{E} {A(I)}\\\vspace{0.6em}{F} Conv(\sum_{i1}^3{M_i}) \\{…

可移植性测试包括哪些

可移植性测试 可移植性是指应用程序能够安装到不同的环境中&#xff0c;在不同的环境中使用&#xff0c;甚至可以移动到不同的环境中。当然&#xff0c;前两者对所有系统都很重要。就PC软件而言&#xff0c;鉴于操作系统、共存和互操作应用程序、硬件、带宽可用性等方面的快速…

AutoSAR EM执行管理模块01

1执行管理的作用&#xff1f; 管理应用AA的生命周期&#xff0c;啥时候启动&#xff0c;啥时候shut down。管理板子的启动和关闭&#xff0c;这里的板子跑的程序是最基本的程序&#xff0c;不包含业务逻辑的AA&#xff0c;可以理解为最小系统的程序。根据定义的执行依赖关系加载…

Appium Android ——利用 TestNG 并行执行用例

目录 前言&#xff1a; 一、测试类 二、连接两个 Android 设备或启动两个虚拟机 三、项目路径下新建两个 testng.xml 四、开启两个 appium server 五、导出依赖 六、执行测试 七、查看报告 前言&#xff1a; Appium是一个流行的移动应用自动化测试工具&#xff0c;…

PHP与Golang对战:两种语言的比较与应用场景探讨

引言 在软件开发领域&#xff0c;选择一种合适的编程语言对于项目的成功至关重要。而在今天的文中&#xff0c;我们将探讨两个备受争议的编程语言——PHP与Golang之间的对战。通过比较它们的优势和应用场景&#xff0c;帮助开发者更好地了解如何选择适合自己项目的语言。 PHP的…

vue3和vue2主要的一些区别?

一、Vue3介绍 关于vue3的重构背景&#xff0c;尤大是这样说的&#xff1a; 「Vue 新版本的理念成型于 2018 年末&#xff0c;当时 Vue 2 的代码库已经有两岁半了。比起通用软件的生命周期来这好像也没那么久&#xff0c;但在这段时期&#xff0c;前端世界已经今昔非比了 在我…

Spring 框架——事件驱动模型

目录 1.概述2.三种角色2.1.事件角色2.2.事件监听者角色2.3.事件发布者角色 3.示例 1.概述 &#xff08;1&#xff09;Spring 事件驱动模型是 Spring 框架中的一种编程模型&#xff0c;也被称为发布/订阅模型&#xff0c;通过使用观察者模式和事件机制&#xff0c;实现了组件之…

Flask_自定义flask的cmd命令

创建自定义命令 from flask import Flaskapp Flask(__name__)app.cli.command() def hello():"""命令说明写这里"""print("hello python")if __name__ __main__:app.run() 执行flask --help 可以在命令查看定义的命令 注意事项&a…

网络工程基础框架3层次模型 ,1接入层2 汇聚层 3核心层

网络工程基础框架3层次模型 ,1接入层2 汇聚层 3核心层_「已注销」的博客-CSDN博客 CISCO有自己的3层层次模型 1&#xff0c;接入层 2&#xff0c;汇聚层 3&#xff0c;核心层 区别&#xff1a;通常将网络中直接面向用户连接或访问网络的部分称为接入层&#xff0c;将位于接入层…

盛元广通科研院所实验室安全管理系统LIMS

实验室的管理与安全直接影响着教学与科研质量&#xff0c;从科研角度出发&#xff0c;实验室安全风险特点与生产现场安全风险特点存在较大差异&#xff0c;危险源种类复杂实验内容变更频繁&#xff0c;缺乏有效监管&#xff0c;实验室安全运行及管理长期游离于重点监管领域外&a…

抖音seo矩阵源码SaaS搭建代码分享-可二开

场景&#xff1a;适用于抖音seo源码&#xff0c;抖音矩阵源码&#xff0c;短视频seo源码&#xff0c;短视频矩阵源码&#xff0c;抖音短视频seo矩阵系统源码开发搭建等。 抖音seo源码优化逻辑 抖音SEO是通过一系列的技术手段和优化策略来提升视频内容在抖音平台内的曝光率和排名…

机器学习实战系列:工业蒸汽量预测

背景介绍 火力发电的基本原理是&#xff1a;燃料在燃烧时加热水生成蒸汽&#xff0c;蒸汽压力推动汽轮机旋转&#xff0c;然后汽轮机带动发电机旋转&#xff0c;产生电能。在这一系列的能量转化中&#xff0c;影响发电效率的核心是锅炉的燃烧效率&#xff0c;即燃料燃烧加热水…