vue组件为什么只能有一个根元素

1.vue2中组件只能有一个根节点

Vue2中 组件只能有一个根元素,是因为Vue的模板编译器在编译模板时,会将模板转换成一个render函数,而这个render函数只能返回一个单一的根节点。

如果组件有多个根节点,则无法满足这个要求。例如:

<template>
  <div class="container">
    <h1>{{ title }}</h1>
    <p>{{ content }}</p>
  </div>
  <button @click="submit">Submit</button>
</template>

在编译时,会报错:

Template compilation error: Component template should contain exactly one root element.

因此,在Vue中,每个组件必须有且只能有一个根元素。如果需要多个元素并列显示,可以使用一个父元素将它们包裹起来。例如:

<template>
  <div class="container">
    <h1>{{ title }}</h1>
    <p>{{ content }}</p>
    <button @click="submit">Submit</button>
  </div>
</template>

这样就可以满足单一根节点的要求,并且不影响页面布局和样式。

2.vue3可以有多个根节点

Vue 3.x 中支持多个根元素,这是因为 Vue 3.x 中使用了 Fragment(片段)的概念,即可以使用 <template> 标签或 Fragment 组件来包裹多个根元素。

在 Vue 2.x 中,由于模板编译器的限制,每个组件必须有且只能有一个根元素。这是因为 Vue 2.x 的模板编译器会将模板转换成一个 render 函数,并且这个函数需要返回一个单一的根节点。

而在 Vue 3.x 中,采用了基于函数式编程的渲染方式,不再需要将模板转换成 render 函数。相反,Vue 3.x 使用了基于模板的编译方式,并且支持了 Fragment 的概念。Fragment 是一种虚拟节点,它可以包含多个子节点,并且不会在 DOM 树中创建任何实际的元素。

例如,在 Vue 3.x 中可以这样写:

<template>
  <div>
    <h1>Header</h1>
    <p>Content</p>
  </div>
  <div>
    <h1>Header</h1>
    <p>Content</p>
  </div>
</template>

或者使用 Fragment:

<template>
  <>
    <div>
      <h1>Header</h1>
      <p>Content</p>
    </div>
    <div>
      <h1>Header</h1>
      <p>Content</p>
    </div>
  </>
</template>

<script setup>

import { defineComponent } from 'vue'

export default defineComponent({
  
})

</script>

这样就可以实现多个根元素了。

3.render函数封装有什么特别的,或者用到比较巧妙的东西吗?

使用render函数封装组件可以让我们更加灵活地控制组件的渲染,同时也可以提高组件的性能(因为vue中会先把template的内容转化成render函数)。在render函数的封装中,有一些比较巧妙的技巧和特点。

  1. 使用JSX语法

JSX是一种JavaScript语法扩展,可以在JavaScript中编写类似HTML的代码。使用JSX语法可以使render函数更加直观和易读。

例如,在Vue中使用JSX语法封装一个HelloWorld组件:

const HelloWorld = {
  render() {
    return (
      <div>
        <h1>Hello World</h1>
        <p>This is a JSX component</p>
      </div>
    )
  }
}
  1. 使用函数式组件

函数式组件是指没有状态(data)和实例(this)的组件,只接受props作为参数并返回一个虚拟节点。使用函数式组件可以提高渲染性能,并且更加容易进行单元测试。

例如,在Vue中使用函数式组件封装一个HelloWorld组件:

const HelloWorld = {
  functional: true,
  props: {
    message: String
  },
  render(h, { props }) {
    return h('div', [
      h('h1', 'Hello World'),
      h('p', props.message)
    ])
  }
}
  1. 使用插槽(slot)

插槽是一种特殊的虚拟节点,用于在父组件中传递子组件内容。使用插槽可以使render函数更加灵活和可复用。

例如,在Vue中使用插槽封装一个ButtonGroup组件:

const ButtonGroup = {
  functional: true,
  render(h, { slots }) {
    return h('div', { class: 'button-group' }, slots.default())
  }
}

const App = {
  render() {
    return (
      <ButtonGroup>
        <button>Button A</button>
        <button>Button B</button>
        <button>Button C</button>
      </ButtonGroup>
    )
  }
}
  1. 使用动态属性

动态属性是指根据props或其他变量动态生成属性值。使用动态属性可以使render函数更加灵活和可配置。

例如,在Vue中使用动态属性封装一个Link组件:

const Link = {
  functional: true,
  props: {
    to: String
  },
  render(h, { props, children }) {
    return h('a', { href: props.to }, children)
  }
}

const App = {
  data() {
    return { url: 'https://www.example.com' }
  },
  
  render() {
    return (
      <Link to={this.url}>Click me!</Link>
    )
  }
}

总之,render函数封装有很多特别的技巧和特点,通过灵活运用这些技巧和特点,我们可以更好地掌控Vue的渲染过程,并提高应用程序的性能和可维护性。

3.1. Vue2 render函数创建组件

在Vue2中,可以使用渲染函数来代替模板来定义组件的视图。渲染函数是一个JavaScript函数,它返回一个虚拟DOM节点,用于描述组件的结构和内容。

以下是一个简单的示例,展示了如何使用渲染函数来创建一个简单的HelloWorld组件:

Vue.component('HelloWorld', {
  render: function (createElement) {
    return createElement('div', {class: 'container'}, 'Hello World');
  }
});

在上面的例子中,我们定义了一个名为HelloWorld的组件,并在其render方法中返回了一个div元素节点,内容为"Hello World"

需要注意的是,在渲染函数中,我们使用createElement方法来创建虚拟DOM节点。createElement接受三个参数:标签名、属性对象、子节点(或当前节点内容)。

除了直接返回虚拟DOM节点外,还可以使用JSX语法或者通过调用其他组件来构建更复杂的视图结构。具体写法会根据需求和个人喜好而有所不同。

3.2. Vue3 render函数创建一个按钮

创建MyButton.ts 【render函数中的h函数或createElement函数是用来创建虚拟DOM节点的】

import { emit } from "process"
import { defineComponent, h } from "vue"

export default defineComponent({
  name: 'MyButton',
  props:{
    disabled:{
      default:'false',
      type:Boolean
    }
  },
  render(context) {
    return h('button', {
      onclick: () => context.$emit('custom-click')
    }, context.$slots)
  }
})

像组件一样正常使用MyButton

<script setup lang="ts">
import MyButton from "./MyButton.ts"
const onClick = () => {
  console.log('onClick')
}
</script>
<template>
  <MyButton :disabled="false" @custom-click="onClick">
    my button
  </MyButton>
</template>

4. template模板的内容是如何渲染成真实的DOM的

在Vue中,template模板的内容是通过Vue的编译器将其转换为渲染函数,然后再由渲染函数生成虚拟DOM,并最终渲染到页面上。

具体的渲染过程如下:

  1. 编译阶段:Vue的编译器会将template模板编译成一个渲染函数。这个渲染函数接受一个createElement函数作为参数,用于创建虚拟DOM节点。
  2. 创建虚拟DOM:在组件实例化或更新时,Vue会调用渲染函数,并传入createElement函数。渲染函数根据模板定义的结构和内容,调用createElement来创建对应的虚拟DOM节点。
  3. 虚拟DOM更新:一旦创建了虚拟DOM节点,Vue会将其与之前的虚拟DOM进行比较,找出需要更新的部分。然后根据差异进行局部更新,而不是重新渲染整个组件。
  4. 真实DOM挂载:最后,经过虚拟DOM更新后得到最新的虚拟DOM树。Vue会将这个虚拟DOM树转换为真实的DOM元素,并挂载到页面上指定的位置。

总结起来,template模板通过编译器转换为渲染函数,在组件实例化或更新时调用该渲染函数生成虚拟DOM,并通过对比差异进行局部更新,最终将最新的虚拟DOM转换为真实的DOM元素并挂载到页面上。

谈谈你对MVC、MVP和MVVM的理解?

MVC、MVP 和 MVVM 是三种常见的软件架构设计模式,主要通过分离关注点的方式来组织代码结构,优化开发效率。

MVC

1)MVC 分为 View、Model、Controller:

View :用户界面。

Model :数据保存。负责存储页面的业务数据,以及对数据的处理方法。并且 View 和 Model 应用了观察者模式,当 Model 层发生改变的时候它会通知有关 View 层更新页面。

Controller :业务逻辑。是 View 层和 Model 层的纽带,用于控制应用程序的流程,及页面的业务逻辑。当用户与页面产生交互的时候,Controller 通过调用 Model 层,来完成对 Model 的修改,然后 Model 层再去通知 View 层更新。

2)MVC的流程:

View 传送指令到 Controller ;

Controller 完成业务逻辑后,要求 Model 改变状态 ;

Model 将新的数据发送到 View,用户得到反馈。

MVP

MVP(Model-View-Presenter)是MVC的改良模式。

1)MVP 分为 Model、View、Presenter:

View :用户界面。

Model :数据保存。负责存储页面的业务数据,以及对数据的处理方法。

Presenter:负责完成View与Model间的交互。model获取数据,并填充到View中,使得View和Model不直接联系。

2)MVP特点:

MVC中是允许Model和View进行交互的,而MVP中Model与View之间的交互由Presenter完成。(与MVC的区别)

各部分之间的通信,都是双向的。除了Model和View之间。(与MVC的区别)

Presenter与View之间的交互是通过接口的。(与MVC的区别)

View 非常薄,不部署任何业务逻辑,称为"被动视图"(Passive View),即没有任何主动性。

MVVM

MVVM 模式将 Presenter 改名为 ViewModel,基本上与 MVP 模式完全一致。

唯一的区别是,它采用双向绑定(data-binding):View的变动,自动反映在 ViewModel,反之亦然。

1)MVVM 分为 Model、View、ViewModel:


View:代表UI视图,负责数据的展示;

Model:代表数据模型,请求的原始数据;就是对于纯数据的处理,比如增删改查,与后台做交互;

ViewModel(视图模型层):负责监听Model中数据的改变并且控制视图View的更新,处理用户交互操作。



2)对ViewModel的进一步理解


和MVC一样,View和Model是不可以直接进行通信的,ViewModel是连接view和model的桥梁。

当用户操作View,ViewModel感知到变化,然后通知Model发生相应改变,反之亦然。

ViewModel向上与视图层View进行双向数据绑定,向下与Model通过接口请求进行数据交互,起到承上启下的作用。

mvvm实现了双向绑定,开发者只需要处理和维护 ViewModel,而不需要自己操作DOM去更新视图

3)双向绑定: 当View 层(表单)值发生变化,View 层绑定的 ViewModel 中的数据会得到自动更新;当 ViewModel 中数据变化,View 层会得到更新;


(View和Model进行双向绑定,两者之间有一方发生变化则会反映到另一方上。ViewModle要做的只是业务逻辑处理,以及修改View或者Modle的状态。)


4)优点:


低耦合和可重用性。分离视图(View)和模型(Model),降低代码耦合,提高视图或者逻辑的重用性。视图(View)可以独立于Model变化和修改,一个ViewModel可以绑定不同的"View"上,当View变化的时候Model不可以不变,当Model变化的时候View也可以不变。你可以把一些视图逻辑放在一个ViewModel里面,让很多view重用这段视图逻辑

自动更新dom: 实现了 Model和View的数据自动同步,因此开发者只需要专注于数据的维护操作即可,而不需要自己操作DOM。

独立开发。开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计。

可测试。界面素来是比较难于测试的,测试可以针对ViewModel来写。

5)缺点:


Bug很难被调试: 因为使用双向绑定的模式,当你看到界面异常了,有可能是你View的代码有Bug,也可能是Model的代码有问题。数据绑定使得一个位置的Bug被快速传递到别的位置,要定位原始出问题的地方就变得不那么容易了。另外,数据绑定的声明是指令式地写在View的模版当中的,这些内容是没办法去打断点debug的

一个大的模块中model也会很大,虽然使用方便了也很容易保证了数据的一致性,当长期持有,不释放内存就造成了花费更多的内存

对于大型的图形应用程序,视图状态较多,ViewModel的构建和维护的成本都会比较高