Vue 进阶系列丨虚拟DOM和VNode

本文详细介绍了Vue.js中虚拟DOM的概念及其工作原理。包括虚拟DOM如何提高渲染效率、VNode的作用及类型,以及Vue.js从1.0到2.0版本在虚拟DOM处理方式上的演变。

Vue 进阶系列教程将在本号持续发布,一起查漏补缺学个痛快!若您有遇到其它相关问题,非常欢迎在评论中留言讨论,达到帮助更多人的目的。若感本文对您有所帮助请点个赞吧!


2013年7月28日,尤雨溪第一次在 GItHub 上为 Vue.js 提交代码;2015年10月26日,Vue.js 1.0.0版本发布;2016年10月1日,Vue.js 2.0发布。

最早的 Vue.js 只做视图层,没有路由, 没有状态管理,也没有官方的构建工具,只有一个库,放到网页里就可以直接用了。

后来,Vue.js 慢慢开始加入了一些官方的辅助工具,比如路由(Router)、状态管理方案(Vuex)和构建工具(Vue-cli)等。此时,Vue.js 的定位是:The Progressive Framework。翻译成中文,就是渐进式框架。

Vue.js2.0 引入了很多特性,比如虚拟 DOM,支持 JSX 和 TypeScript,支持流式服务端渲染,提供了跨平台的能力等。Vue.js 在国内的用户有阿里巴巴、百度、腾讯、新浪、网易、滴滴出行、360、美团等等。

Vue 已是一名前端工程师必备的技能,现在就让我们开始深入学习 Vue.js 内部的核心技术原理吧!


什么是虚拟DOM

作为一个前端,想必或多或少都听说过虚拟 DOM 这个名词,那么虚拟 DOM到底是什么呢?

在 web 早期,页面的交互效果还是比较简单的,不需要频繁的去操作DOM,使用 Jquery 完全可以满足我们的需求。但是随着时代的发展,页面上的东西越来越多,需求也越来越复杂,相应的程序中需要维护的状态也越来越多,我们就要不断的去操作 DOM。此时命令式编程的 Jquery 就显得很臃肿,后期维护困难。

三大框架的诞生就是为了解决这一问题,声明式操作 DOM,我们只需要描述状态和DOM之间的映射关系,就可以将状态渲染成视图。从状态到视图的过程,框架会帮我们做,不需要我们手动去操作DOM。

当状态发生变化时,需要重新渲染页面,也就是框架要去帮助我们操作DOM,那么框架怎么去确定状态发生了什么变化以及框架要去哪里更新DOM 呢?最简单的做法就是只要有一个状态发生了变化,页面里全部的DOM 元素都重新渲染一遍,但是访问 DOM 的操作是比较费时的,如果按照这么做的话,会造成相当大的浪费。那么 Vue.js 是怎么做的呢?

Vue.js 在渲染页面之前会先根据状态生成一份虚拟 DOM 树,然后使用虚拟DOM 树进行渲染,在渲染之前,会使用新生成的虚拟 DOM 树和上一次生成的虚拟 DOM 树进行对比,只渲染不同的部分。


Vue的虚拟DOM

在 Vue.js1.0 的时候,Vue 并没有引用虚拟 DOM,当时的做法是当状态发生变化是,Vue 已经知道了是哪个节点使用了这个状态,然后将这个节点进行更新操作,根本不需要对比操作。

但是这样的缺点是,对于每一个状态都有一个依赖跟踪器,粒度太细,这样就会有一些内存开销和依赖追踪的开销,当状态被越来越多的节点使用的时候,开销就会非常大。

Vue2.0 的时候选择了一个中等粒度,当状态发生变化的时候,只在组件级别有一个依赖跟踪器,当组件内的状态改变的时候,只能通知到组件层次,组件内部通过虚拟 DOM 进行比对与渲染。Vue 之所以能随意的切换绑定的粒度,本质上还要归功于变化侦测,关于变化侦测的相关知识请移步Vue 进阶系列丨Object 的变化侦测

Vue 使用模板来描述状态与 DOM 之间的映射关系,通过编译将模板转换成渲染函数,执行渲染函数生成虚拟节点树(vnode),使用虚拟节点数就可以渲染页面了。当状态发生变化的时候,直接进行 DOM 操作会比较慢,所以首先会先生成虚拟节点树,将新的节点树和旧的节点树进行对比,然后再操作 DOM,更新页面。

所以,虚拟 DOM 主要做了两件事:

  • 提供与真实 DOM 节点所对应的虚拟节点 vnode

  • 将新的虚拟节点和旧的虚拟节点进行对比,然后更新页面


什么是 VNode

VNode 是 Vue.js 的一个类,使用它可以实例化不同的 vnode 实例,我们只需要控制传入的参数的不同就可以了。

class VNode {
  constructor (
    tag,
    data,
    children,
    text,
    elm,
    context,
    componentOptions,
    asyncFactory
  ) {
    this.tag = tag;
    this.data = data;
    this.children = children;
    this.text = text;
    this.elm = elm;
    this.ns = undefined;
    this.context = context;
    this.fnContext = undefined;
    this.fnOptions = undefined;
    this.fnScopeId = undefined;
    this.key = data && data.key;
    this.componentOptions = componentOptions;
    this.componentInstance = undefined;
    this.parent = undefined;
    this.raw = false;
    this.isStatic = false;
    this.isRootInsert = true;
    this.isComment = false;
    this.isCloned = false;
    this.isOnce = false;
    this.asyncFactory = asyncFactory;
    this.asyncMeta = undefined;
    this.isAsyncPlaceholder = false;
  }
  get child () {
    return this.componentInstance
  }
}

我们可以看到 vnode 仅仅只是 VNODE 类的一个实例而已,通过传入的参数不同,就可以生成不同类型的 vnode 实例对象了。vnode 可以理解成节点描述对象,它描述了应该怎样去创建真实的 DOM 节点。


VNode 的种类

  • 注释节点

const createEmptyVNode = (text = '') => {
  const node = new VNode();
  node.text = text;
  node.isComment = true;
  return node
};

我们可以看到注释节点只有两个有效属性,text 和 isComment,其余属性都是默认的 undefined 或者 false。

例如,一个真实的注释节点:

<!-- 注释节点 -->

所对应的 vnode 是这样的:

{
  text:'注释节点',
  isComment:true
}
  • 文本节点

function createTextVNode (val) {
  return new VNode(undefined, undefined, undefined, String(val))
}

文本节点只有一个 text 属性,它的 vnode 是这样的:

{
  text:'我是文本内容',
}
  • 克隆节点

function cloneVNode (vnode) {
  const cloned = new VNode(
    vnode.tag,
    vnode.data,
    vnode.children && vnode.children.slice(),
    vnode.text,
    vnode.elm,
    vnode.context,
    vnode.componentOptions,
    vnode.asyncFactory
  );
  cloned.ns = vnode.ns;
  cloned.isStatic = vnode.isStatic;
  cloned.key = vnode.key;
  cloned.isComment = vnode.isComment;
  cloned.fnContext = vnode.fnContext;
  cloned.fnOptions = vnode.fnOptions;
  cloned.fnScopeId = vnode.fnScopeId;
  cloned.asyncMeta = vnode.asyncMeta;
  cloned.isCloned = true;
  return cloned
}

克隆节点就是将一个节点的内容全部复制到新的节点中。主要用来优化静态节点和插槽节点。当静态节点需要重新渲染的时候,并不需要重新再次走一遍渲染函数,然后重新生成 vnode,只需要使用克隆节点的方法将之前的 vnode 克隆一份即可,一定程度上优化了程序的性能。

克隆节点和被克隆节点的唯一区别是 isCloned 属性,克隆节点的 isCloned为 true,被克隆的原始节点的 isCloned 为 false。

  • 元素节点

元素节点一般包含 4 个有效属性:

  1. tag:就是节点的名称,例如 div、p、img

  2. data:该属性包含一些节点上的数据,例如 class、style

  3. children:该节点的子节点列表

  4. context:当前组件的 vue.js 实例

通常,函数式组件的 vnode 是这样子的:

{
  children:[VNode,VNode],
  context:{...},
  data:{...},
  tag:{...}
}
  • 组件节点

和元素节点类似,有两个独有的属性 componentInstance 和 componentOptions。

通常,函数式组件的 vnode 是这样子的:

{
  componentInstance:{...},
  componentOptions:{...},
  context:{...},
  data:{...},
  tag:{...}
}
  • 函数式组件

和组件节点类似,有两个独有的属性 functionalContext 和 functionalOptions。

通常,函数式组件的 vnode 是这样子的:

{
  functionalContext :{...},
  functionalOptions:{...},
  context:{...},
  data:{...},
  tag:{...}
}

Vue 进阶系列教程将在本号持续发布,一起查漏补缺学个痛快!若您有遇到其它相关问题,非常欢迎在评论中留言讨论,达到帮助更多人的目的。若感本文对您有所帮助请点个赞吧!

叶阳辉

HFun 前端攻城狮

往期精彩:

<think>嗯,用户遇到了Vue3中vnode为null的报错问题,需要帮忙解决。首先,我得回忆一下Vue3的虚拟DOM机制,以及常见的导致vnode为null的情况。根据引用[2]提到的,Vue在处理子节点时,有三种情况:新子节点存在旧子节点不存在,旧子节点存在新子节点不存在,或者两者都存在。这时候可能会触发updateChildren或者addVnodes、removeVnodes等方法。如果在这个过程中vnode为null,可能是在某个环节没有正确创建或更新vnode。 接下来,我需要考虑可能的原因。首先,组件渲染时返回了null,这会导致父组件在尝试处理子vnode时遇到问题。比如在setup函数中返回了错误的数据,或者条件渲染的逻辑有问题。例如,v-if的条件判断不当,可能导致组件未被正确挂载,从而vnode为null。 其次,异步操作可能引起问题。比如在数据加载完成前组件已经渲染,此时如果数据未准备好,可能导致vnode未被正确生成。这时候需要检查异步数据的处理,是否使用了v-if或v-show来控制渲染时机,或者是否在数据未准备好时渲染了组件。 另外,动态组件或路由切换时,如果组件未正确注册或加载失败,也可能导致vnode为null。需要检查动态组件的is属性是否正确,或者路由配置是否有误,导致组件无法正确加载。 还有可能是第三方库的兼容性问题,尤其是在Vue3中使用为Vue2设计的库,可能导致vnode生成异常。这时候需要确认所有使用的库是否支持Vue3,或者是否有必要的适配层。 此外,模板中的语法错误,比如错误使用v-for的key,或者未正确处理插槽内容,也可能导致vnode为null。比如引用[2]提到的checkDuplicateKeys,如果子节点的key重复,在开发环境下会发出警告,可能影响vnode的生成。 解决方案方面,首先应该检查组件渲染逻辑,确保返回有效的vnode。使用模板中的条件渲染时,确保条件状态正确,避免在数据未准备好时渲染组件。对于异步操作,使用v-if或Suspense组件来处理加载状态。动态组件路由部分需要确认组件是否正确注册加载。检查第三方库的兼容性,必要时更新或替换。同时,使用Vue Devtools检查组件树,确认哪个环节出现了null的vnode。 在代码层面,可以添加空值检查,比如在可能返回null的地方使用默认内容或加载状态。例如,在setup函数中确保返回的对象有效,或者在父组件中使用v-if控制子组件的渲染条件。此外,确保模板中的语法正确,特别是v-for的key条件渲染的逻辑。 如果用户仍然无法解决,建议提供一个可复现的示例,方便进一步排查问题。比如检查是否有在子组件未挂载时访问其vnode,或者是否在生命周期钩子中错误地操作了DOM。</think>### Vue3 vnode null 报错解决方案 #### 一、问题原因分析 在Vue3中触发`vnode is null`错误通常与以下场景相关: $$ \text{报错触发条件} \subseteq \{\text{组件渲染异常}, \text{异步操作时序问题}, \text{动态组件加载失败}\} $$ 根据引用[2]的虚拟DOM更新逻辑,当新旧子节点对比出现空值时会触发该错误[^2]。 #### 二、常见场景与修复方案 1. **组件未正确渲染** ```javascript // 错误示例:setup返回无效值 setup() { return null; // 必须返回有效对象或h()创建的vnode } // 正确方案:返回有效渲染内容 import { h } from 'vue'; setup() { return () => h('div', 'Valid Content'); } ``` 2. **异步数据未加载完成** ```html <!-- 错误用法:未处理加载状态 --> <ChildComponent v-if="asyncData" :data="asyncData" /> <!-- 正确方案:添加加载状态保护 --> <template v-if="isLoaded"> <ChildComponent :data="asyncData" /> </template> <SkeletonLoader v-else /> ``` 3. **动态组件异常** ```javascript // 错误示例:未注册组件 const dynamicComponent = computed(() => unregisteredComponent); // 正确方案:确保组件已注册 components: { ValidComponent }, setup() { const dynamicComponent = shallowRef(ValidComponent); } ``` #### 三、调试建议 1. **使用Vue Devtools检查组件树** 2. **添加错误边界捕获** ```javascript // errorCaptured生命周期钩子 onErrorCaptured((err) => { console.error('VNode error:', err); return false; // 阻止错误继续传播 }); ``` #### 四、进阶处理 对于引用[1]中的事件监听器更新逻辑[^1],若遇到vnode为空时: ```javascript function safeUpdateListeners(target) { if (!target || !target.elm) { console.warn('Invalid vnode during event update'); return; } updateDOMListeners(oldVnode, newVnode); } ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值