当前位置: 首页 > news >正文

从认识 VNode VDOM 到实现 mini-vue

前言

现有框架几乎都引入了虚拟 DOM 来对真实 DOM 进行抽象,也就是现在大家所熟知的 VNode 和 VDOM,那么为什么需要引入虚拟 DOM 呢?下面就一起来了解下吧!!!

VNode & VDOM

VNode 和 VDOM 是什么?

直接看 vue3 中关于 VNode 部分的源码,文件位置:packages\runtime-core\src\vnode.ts

通过源码部分,可以很明显的看到 VNode 本身就是一个 JavaScript 对象,只不过它是通过不同的属性去描述一个真实 dom.

VDOM 其实就是多个 VNode 组成的树结构,这就好比 HTML 元素和 DOM 树之间的关系:多个 HTML 元素能够组成树形结构就称之为 DOM 树.

function _createVNode(type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,props: (Data & VNodeProps) | null = null,children: unknown = null,patchFlag: number = 0,dynamicProps: string[] | null = null,isBlockNode = false
): VNode {...return createBaseVNode(type,props,children,patchFlag,dynamicProps,shapeFlag,isBlockNode,true)
}function createBaseVNode(type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,props: (Data & VNodeProps) | null = null,children: unknown = null,patchFlag = 0,dynamicProps: string[] | null = null,shapeFlag = type === Fragment ? 0 : ShapeFlags.ELEMENT,isBlockNode = false,needFullChildrenNormalization = false
) {const vnode = {__v_isVNode: true,__v_skip: true,type,props,key: props && normalizeKey(props),ref: props && normalizeRef(props),scopeId: currentScopeId,slotScopeIds: null,children,component: null,suspense: null,ssContent: null,ssFallback: null,dirs: null,transition: null,el: null,anchor: null,target: null,targetAnchor: null,staticCount: 0,shapeFlag,patchFlag,dynamicProps,dynamicChildren: null,appContext: null} as VNode...return vnode
}

为什么要使用 VDOM ?

既然要使用肯定是因为 虚拟 DOM 拥有一些 真实 DOM 没有的优势:

  • 对真实元素节点抽象成 VNode,减少直接操作 dom 时的性能问题
    • 直接操作 dom 是有限制的,比如:diff、clone 等操作,一个真实元素上有许多的内容,如果直接对其进行 diff 操作,会去额外 diff 一些没有必要的内容;同样的,如果需要进行 clone 那么需要将其全部内容进行复制,这也是没必要的。但是,如果将这些操作转移到 JavaScript 对象上,那么就会变得简单了。
    • 直接操作 dom 容易引起页面的重绘和回流,但是通过 VNode 进行中间处理,可以避免一些不要的重绘和回流
  • 方便实现跨平台
    • 同一 VNode 节点可以渲染成不同平台上的对应的内容,比如:渲染在浏览器是 dom 元素节点,渲染在 Native( iOS、Android) 变为对应的控件、可以实现 SSR 、渲染到 WebGL 中等等
    • 而且 Vue 允许开发者基于 VNode 实现自定义渲染器(renderer),以便于针对不同平台上的渲染

虚拟 DOM 的渲染过程

在这里插入图片描述

Vue 三大核心系统

Vue 中的三大核心系统如下:

  • Compiler 模块:涉及 AST 抽象语法树的内容,再通过 generate 将 AST 生成渲染函数,这里暂不实现
  • Runtime 模块:也可称为 Renderer 模块,将虚拟 dom 生成真实 dom 元素,并渲染到浏览器上
  • Reactivity 模块:响应式系统

三大系统的关系

在这里插入图片描述

实现 Runtime 模块

下面的实现部分只实现最简单、最核心的内容,不涉及各种复杂的边界条件.

createVNode & h

VNode 主要作用就是将外部传入的各种参数组合成一个 JavaScript 对象.

其中 createVNode 就是用于创建 VNode ,而 h 函数(render function)负责将创建好的 VNode 进行返回.

function createVNode(type, props, children) {// vnode ——> js 对象return {type,props,children}
}function h(type, props, children) {return createVNode(type, props, children)
}

mount

得到 VNode 之后,接下来就需要将 VNode变成真实的 dom元素,并渲染到浏览器上.

  • 通过 document.createElement方法将 VNode变成 dom元素

  • 处理传入的 props对象

    • on 开头的默认为事件,通过 addEventListenerdom元素注册事件
    • 其他属性默认为 dom上的属性,通过 setAttributedom元素设置属性
  • 处理 children,只考虑 childrenStringArray的情况

    • childrenString 默认为是文本节点,通过 textContent属性进行设置
    • childrenArray 默认为是多个 VNode集合,通过递归调用 mount方法进行挂载
function mount(vnode, container) {// 1. 获取容器 elementif (container.nodeType !== 1) {container = document.querySelector(container)}// 2. vnode ——> elementconst { type, props, children } = vnodeconst el = document.createElement(type)vnode.el = el// 3. 处理 propsif (props) {for (const key in props) {// 事件if (key.startsWith('on')) {el.addEventListener(key.slice(2).toLowerCase(), props[key]);} else {// 属性el.setAttribute(key, props[key])}}}// 4. 处理 childrenif (typeof children === 'string') {el.textContent = children} else {children.forEach(v => {mount(v, el)});}// 5. 挂载到容器中container.appendChild(el)
}

patch

实现了能够将 VNode 渲染为真实 DOM 之后,就需要考虑更新时 VNode 间的 diff 比较了,这就属于 patch 的过程.

  • 新旧 VNode 类型不一致,先删除旧节点,用新的替换旧的

  • 新旧 VNode 类型一致

    • 更新 props:更新 dom 属性 & 更新 dom 事件

      • 新旧属性或事件存在且不一致,直接更新
      • 新属性存在 & 旧属性不存在,直接添加
      • 新属性都存在 & 新旧值不一致,直接删除
    • 更新 children

      • 新 children 是字符串,只要和旧的 children 不相等,直接使用 innerHTML 替换旧的内容
      • 新 children 是数组 & 旧 children 是字符串,先清空旧节点的内容,循环调用 mount 新增元素
      • 新旧 children 都是数组,取新旧 children 中最小长度,用于减少循环 patch 次数,若 oldLength < newLength 需要通过 mount 新增元素, 若 oldLength > newLength 需要通过 el.removeChild 删除多余旧元素
/**
* 
* @param {oldVnode} n1 
* @param {newVnode} n2 
*/
function patch(n1, n2) {// 1. 类型不一致if (n1.type !== n2.type) {const parent = n1.el.parentElement// 删除 oldVnode.elparent.removeChild(n1.el)// 渲染 newVnode.elmount(n2, parent)} else {// 2. 类型一致// 2.1 统一 el 对象,因为最终修改的是 oldVnode.el,因此,使用 n1.el 作为最终值const el = n2.el = n1.el// 2.2 处理 propsconst oldProps = n1.propsconst newProps = n2.props// 处理 props 不一致for (const key in newProps) {const newValue = newProps[key]const oldValue = oldProps[key]// 旧的有值,新的没值,移除该属性if (newValue !== oldValue) {// 事件不一致if (key.startsWith('on')) {el.removeEventListener(key.slice(2).toLowerCase(), oldProps[key])el.addEventListener(key.slice(2).toLowerCase(), newProps[key])} else {// props 值不一致el.setAttribute(key, newValue)}}}// 删除旧的 propsfor (const key in oldProps) {if (!(key in newProps)) {// 旧事件不存在 newProps 中if (key.startsWith('on')) {el.removeEventListener(key.slice(2).toLowerCase(), oldProps[key])} else {// oldProps 中的属性不存在 newProps 中el.removeAttribute(key)}}}// 2.3 处理 childrenconst oldChildren = n1.childrenconst newChildren = n2.children// 新的子节点是字符串if (typeof newChildren === 'string') {// 新旧子节点不一致,直接使用新节点进行替换旧节点if (newChildren !== oldChildren) el.innerHTML = newChildren} else {// 新的子节点为数组// 旧的子节点为字符串if (typeof oldChildren === 'string') {el.innerHTML = ''newChildren.forEach(v => {mount(v, el)})} else {// 旧的子节点也为数组// 取最小的长度进行最少的循环let commonLength = Math.min(newChildren.length, oldChildren.length)for (let i = 0; i < commonLength; i++) {// 递归调用 patch 新老节点patch(oldChildren[i], newChildren[i])}// 循环结束:oldLength < newLength || oldLength > newLength// oldLength < newLength,需要添加新节点if(oldChildren.length < newChildren.length){newChildren.slice(oldChildren.length).forEach(v => {mount(v, el)})}// oldLength > newLength,需要删除旧节点if(oldChildren.length > newChildren.length){oldChildren.slice(newChildren.length).forEach(v => {el.removeChild(v.el)})}}}}
}

实现 Reactivity 模块

在这里插入图片描述

基于 Object.defineProperty 实现响应式

Object.defineProperty 的优点:

  • 兼容性好,可以兼容到 IE9
    Object.defineProperty 的不足:
  • 不能劫持对象 property添加移除
  • 不能劫持数组变化
    • 通过数组下标修改数组项
    • 修改数组长度
class Dep {constructor() {this.subscribers = new Set()}depend() {if (activeEffect) {this.subscribers.add(activeEffect)}}notify() {this.subscribers.forEach(effect => {effect()})}
}// 当前正在执行 effect 函数
let activeEffect = nullfunction watchEffect(effect) {activeEffect = effecteffect() // 目的:初始化调用 + 依赖收集activeEffect = null
}// 存储依赖副作用
const targetMap = new WeakMap()// 获取当前的对象的依赖 dep 对象
function getDep(target, key) {// 1. 根据传入的 target 获取对应的 Map 对象let depsMap = targetMap.get(target)// 2. 若 depsMap 不存在,则初始化一个 Map 对象if (!depsMap) {depsMap = new Map()targetMap.set(target, depsMap)}// 3. 获取具体的 dep 对象let dep = depsMap.get(key)// 4. 若 dep 不存在,则实例化一个 Dep 对象if (!dep) {dep = new Dep()depsMap.set(key, dep)}// 5. 返回 dep 实例return dep
}// 数据劫持
function reactive(raw) {Object.keys(raw).forEach(key => {const dep = getDep(raw, key)let value = raw[key]Object.defineProperty(raw, key, {enumerable: true,configurable: true,get() {// 依赖收集dep.depend()return value},set(newValue) {if (value !== newValue) {value = newValue// 通知依赖更新dep.notify()}return true}})})return raw
}

基于 Proxy 实现响应式

Proxy 的优点:

  • Proxy 能监测的类型比 Object.defineProperty 更丰富的类型
    • 能监测对象和数组的变化
    • hasin 操作符的捕获器
    • deletePropertydelete 操作符的捕获器
  • Proxy 作为新标准,将受到浏览器厂商重点持续性的优化

Proxy 的缺点:

  • 不兼容 IE,没有对应的 polyfill
class Dep {constructor() {this.subscribers = new Set()}depend() {if (activeEffect) {this.subscribers.add(activeEffect)}}notify() {this.subscribers.forEach(effect => {effect()})}
}// 当前正在执行 effect 函数
let activeEffect = nullfunction watchEffect(effect) {activeEffect = effecteffect() // 目的:初始化调用 + 依赖收集activeEffect = null
}// 存储依赖副作用
const targetMap = new WeakMap()// 获取当前的对象的依赖 dep 对象
function getDep(target, key) {// 1. 根据传入的 target 获取对应的 Map 对象let depsMap = targetMap.get(target)// 2. 若 depsMap 不存在,则初始化一个 Map 对象if (!depsMap) {depsMap = new Map()targetMap.set(target, depsMap)}// 3. 获取具体的 dep 对象let dep = depsMap.get(key)// 4. 若 dep 不存在,则实例化一个 Dep 对象if (!dep) {dep = new Dep()depsMap.set(key, dep)}// 5. 返回 dep 实例return dep
}// 数据劫持
function reactive(raw) {return new Proxy(raw, {get(target, key) {const dep = getDep(target, key)dep.depend()return Reflect.get(target, key)},set(target, key, newValue) {const dep = getDep(target, key)const result = Reflect.set(target, key, newValue)dep.notify()return result},})
}

createApp() —— Runtime 模块 + Reactivity 模块

如果你对 Vue3 中 createApp 的使用比较熟练或者阅读过相关源码,其实不难发现 createApp 其实会返回一个带有 mount 方法的 JavaScript 对象.

在下面的 mount 方法中针对 VNode 的 mount(挂载) 和 patch(更新) 进行了判断,以便于在响应式数据发生变更时渲染不同的内容.

function createApp(rootComponent) {return {mount(selector) {let isMounted = falselet oldVnode = nulllet newVnode = nullwatchEffect(() => {if (!isMounted) {isMounted = trueoldVnode = rootComponent.render()mount(oldVnode, selector)} else {newVnode = rootComponent.render()patch(oldVnode, newVnode)oldVnode = newVnode}})}}
}

下面是一个简单的计数器案例的实现:

测试代码如下

  <div id="app"></div><script src="./js/renderer.js"></script><script src="./js/reactive.js"></script><script src="./js/createApp.js"></script><script>const App = {data: reactive({count: 0}),render() {return h('div', null, [h('h1', null, `当前计数:${this.data.count}`),h('button', {onClick: () => this.data.count++}, '+1')])}}const app = createApp(App)app.mount('#app')</script>

最后

通过对 VNode 、VDOM 以及 Vue 三大核心系统的认识和实现,最终又通过 createApp 将这些内容串联在一起,可以说是实现了一个小版本的 vue,当然很多场景还是没有进行一一处理,现在只能实现最简单的测试案例.


http://www.mrgr.cn/news/69563.html

相关文章:

  • 【Linux】进程的概念
  • 使用python-Spark使用的场景案例具体代码分析
  • Hbase Shell
  • 探索美赛:从准备到挑战的详细指南
  • Redhat7.9 安装 KingbaseES 金仓数据库 V9单机版(静默安装)
  • 论文阅读《机器人状态估计中的李群》
  • 【含文档】基于ssm+jsp的流浪动物收养系统(含源码+数据库+lw)
  • 关于我的编程语言——C/C++——第八篇
  • 大华Android面试题及参考答案
  • C#实现:电脑系统信息的全面获取与监控
  • cell队列监控
  • Redis相关技术内容
  • 花指令例子
  • Java期末复习暨学校第二次上机课作业
  • Python | Leetcode Python题解之第554题砖墙
  • 系统安全第七次作业题目及答案
  • 高并发内存池介绍
  • 【JAVA项目】基于jspm的【医院病历管理系统】
  • 基于java+SpringBoot+Vue的课程答疑系统设计与实现
  • openpyxl处理Excel模板,带格式拷贝行和数据填入
  • fpga开发原理图设计仿真分析
  • JavaWeb——Web入门(7/9)-Tomcat-介绍(Tomcat 的简介:轻量级Web服务器,支持Servlet/JSP少量JavaEE规范)
  • 互联网及其应用大作业要求-计算机实践课程题目要求
  • Python软体中使用Seaborn绘制热力图的实用指南
  • .Net相关知识
  • C++ | Leetcode C++题解之第554题砖墙