Vue全套知识点

Vue全套知识点

Vue

官网介绍它是一个构建用户界面的渐进式框架 ;
渐进式框架 : 主张最少 , 每个框架都不可避免会有自己的一些特点 , 从而对使用者有一定的要求 , 这些要求就是主张 , 主张有强有弱,它的强势程度会影响在业务开发中的使用方式 ; 而 Vue 虽然有全家桶套餐 , 但是你可以只用它的一部分 , 而不是用了它的 核心库 就必须用它的全部 .

声明式渲染

Vue.js 提供了简洁的模板语法声明式的将数据渲染至 DOM 中

<div id="app">
	{{ message }}
</div>

const vm = new Vue({
	el: '#app',
	data: {
		message: 'hello vue'
	}
})
  • el : 元素挂载点;只有在 new 创建实例的时候生效 ; 实例挂载之后可以使用 vm.$el 访问
  • data : Vue 实例的数据对象 , Vue 会递归的将 data 的 property 转换为 getter 和 setter , 从而让 data 的 property 能够响应数据变化 ; 对象必须是纯粹的对象 (含有 0 个或者 多个 键值对) 浏览器 API 创建的对象 , 原型上的 property 会被忽略 , 大概来说 data 只能存在数据 , 不推荐观察拥有状态行为的对象 ;
  • {{}}: 插值表达式 ; 官网也称为 Mustache 语法

为什么组件中 data 是方法

当一个组件被定义时 (非根组件) data 必须声明为一个返回对象的函数 , 因为组件可能被用来创建多个实例, 如果 data 仍然是一个对象 , 这样所有实例讲共享引用同一个数据对象 , 通过提供 data 函数 , 每次创建一个实例的时候 , 我们能够调用 data 函数 , 从而返回初始数据的一个全新数据对象

// 错误 示例
let options = {
  data: {
    uname: 'zs'
  }
}
function Component(options) {
  this.data = options.data
}

let user1 = new Component(options)
let user2 = new Component(options)

user1.data.uname = 'ls' // 修改 user1  触发了所有
console.log(user2.data.uname) // ls
// 正确示例
let options = {
  data() {
    return {
      uname: 'zs'
    }
  }
}
function Component(options) {
  this.data = options.data()
}

let user1 = new Component(options)
let user2 = new Component(options)

user1.data.uname = 'ls' 
console.log(user2.data.uname) // zs
console.log(user1.data.uname) // ls

由于组件是可以多次复用的 , 如果不使用 function return 每个组件的 data 在内存中都是指向同一个地址的 , 由于 JavaScript 复杂数据类型的特性 , 那一个数据改变其他的也改变了 , 但是如果用了 function return 其实就相当于申明了新变量 , 相互独立 , 自然就不存在以上例子中存在的问题 ; JavaScript 在赋值 Object 时 , 是直接一个相同的内存地址 , 所以为了每个组件的独立 , 采用了这种方式 ; 但由于根组件只有一个 , 不存在数据污染的情况 , 所以就可以是一个对象 ;

指令

v-cloak

这个指令可以配合着 CSS 隐藏未编译的 Mustache 标签 , 直到实例准备完毕

问题展示 :

Vue全套知识点_第1张图片

/* css */
[v-cloak] { display: none; }
<div v-cloak>{{ root }}</div>

v-text

更新某个元素节点下的值 ; 注意 : 会更新全部内容 , 如果想要局部更新 , 可以使用 Mustache 语法

<div v-text="root"></div>

v-html

更新元素的 innerHTML 注意 : 普通 html 内容
在网站上使用 HTML 是非常危险的 , 容易导致 XSS 工具 , 用户提交内容时 切记勿要使用

<div v-html="html"></div>
new Vue({
  el: '#app',
  data: {
    html: '

hello vue

'
} })

v-pre

原文输出 , 不会参与编译 , 输入什么内容就展示什么内容

<div v-pre>{{ will not compile }}</div>

v-once

被定义了 v-once 指令的元素或者组件 (包括元素或组件内的子孙节点) 只能被渲染一次 , 首次渲染收 , 即时数据发生变化 , 也不会被重新渲染 , 一般用于静态内容展示 ;

<div v-once>{{ content }}</div>
const vm = new Vue({
  el: '#app',
  data: {
    content: 'this is init data'
  }
})
vm.content = 'update data'

v-showv-if

这里的 v-if 不单单是这一个指令 , 它包含 v-else-if v-else 功能差不多 , 这里就统一解释了
v-show : 根据表达式的真假值 , 判断元素是否隐藏 ( 切换元素的 display : block/none )
v-if : 根据表达式的值来有条件的渲染数据 , 在切换时元素以及它的数据绑定 / 组件被销毁并重建

差异 :
Vue全套知识点_第2张图片

<div v-if="isShow"> v-if </div>
<div v-show="isShow"> v-show </div>

分支判断代码演示

<!-- 最终一会展示一个 p 标签中的内容  -->
<div>
  <p v-if="score > 90"> 
  	<span>成绩优异 : {{ score }}</span>
  </p>
  <p v-else-if="score > 70"> 
  	<span>成绩及格 : {{ score }}</span>
  </p>
  <p v-else> 
  	<span>不及格 : {{ score }}</span>
  </p>
</div>

v-for

v-for 中被循环的对象 , 必须是一个可迭代对象iterable ( Array | Number | Object | String … )
语法格式为 alias in expression 其中的 in 也可以使用 of 替代
可以为数组或者对象增加索引值

<!-- 数组循环 -->
<div v-for="(item, index) in items">
  {{ item.text }}
</div>
<!-- 对象循环 -->
<div v-for="(val, key, index) in object">
  {{ val }} {{ key }} {{ index }}
</div>

为什么 v-for 必须添加唯一 key

当 Vue 正在更新使用 v-for 渲染的数据列表时 , 它默认使用 就地更新 策略 , 如果数据项的顺序被改变 , Vue 将不会移动 DOM 来匹配数据项的数据 , 而是就地更新每个元素 , 保证它们在每个索引位置的正确渲染 ;

为什么要加 key

  • 为了给 Vue 一个提示 , 以便它跟踪每个节点的身份 , 从而重用 和 重新排序现有元素 需要为每一项提供一个唯一 key
  • key 主要用在 Vue 的 虚拟 DOM 算法 , 在新旧节点对比时 , 辨识虚拟DOM , 如果不使用 key 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法 , 而如果使用了key 它会基于 key 的变化重新排列元素顺序 , 并且会移除 key 不存在的元素

为什么不能用 indexkey

// 组件数据定义
const vm = new Vue({
  el: '#app',
  data: {
    users: [
      { id: 1, uname: 'zs', age: 23 },
	     { id: 2, uname: 'ls', age: 24 },
	     { id: 3, uname: 'we', age: 25 },
	     { id: 4, uname: 'mz', age: 26 },
	  ]
  }
})

index 错误示例

重点在于上面我们所说的会基于 Key 的变化重新排列元素顺序 , 可以看出如果我们用 index 作为 key 数组翻转的时候 , 其实 Key 的顺序是没有变的 , 但是传入的值完全变了 , 这时候原本不一样的数据 , 被误以为一样了 , 所以就造成以下问题 ;

Vue全套知识点_第3张图片

<!-- 具体语法稍后介绍; 意思为点击翻转数组 -->
<button @click="users.reverse()">年龄排序</button>
<ul>
  <!-- 循环这个 users 生成一个数据列表 并且里面带有 多选框 以供我们测试 -->
  <li v-for="(user, index) of users" :key="index">
    <input type="checkbox" />
    <span>{{ user.uname }}</span>
  </li>
</ul>

唯一 Id 正确示例

此时的 key 和数据做绑定 , 当你翻转数组的时候 , 绑定的其实是这一条数据 , 而不是索引 , 就不会造成以上问题了

Vue全套知识点_第4张图片

<li v-for="(user, index) of users" :key="user.id">
  ......
</li>

v-bind

// 绑定 attrbute
// 绑定 class
// 绑定 style

v-on

事件绑定 ; 可缩写 @ 监听DOM事件 , 并在触发时运行一些 js 代码


{{ count }}

可以接收一个方法名称 ;
注意 : 当只是一个方法名称时, 默认第一个参数为事件对象 e
当需要传入参数时 , 那么事件对象就需要手动的传入, 最后一个 并且强制写成 $event



methods: {
  handle(e) {
    console.log(e.target)
  },
  handle1(ct, e) {
  	console.log(ct)
    console.log(e.target)
  }
}

事件修饰符

  • .prevent : 阻止默认事件
  • .stop : 阻止冒泡
  • .self : 只有当前元素触发事件
  • .once : 只触发一次该事件
  • .native : 监听组件根元素的原生事件
// 定义子组件
Vue.component('my-component', {
  template: `
    
  `,
  methods: {
    handle() {
      console.log('///')
    }
  }
})

加了 native 相当于把自定义组件看成了 html 可以直接在上面监听原生事件, 否则自定义组件上面绑定的就是自定义事件 , 而你在自定义事件上没有定义这个事件 , 所以不加 native 不会执行

// 父组件中引用

  • .capture : 添加事件监听时 , 使用 捕获模式
// 此时会优先捕获 box1
// 允许只有修饰符 prevent 阻止默认事件
百度

// 多个事件修饰符可以连用触发时机也是相同的
baidu

按键修饰符

//Vue 中允许为 v-on 监听键盘事件时添加键盘修饰符

当然提供了大多数的按键码别名 按键码
还可以通过全局 Vue.config.keyCodes 自定义修饰符别名
Vue.config.keyCodes.f1 = 112

v-model

在表单元素上创建数据双向绑定 , 它会根据控件类型自动选取正确的值来更新元素

// 文本

{{ message }}

// 多行文本

{{ message }}

// 单选框

{{ sex }}

// 单个复选框

{{ checked }}

// 多个复选框
打篮球
打皮球
打气球
打棒球

{{ hobby }}

// 选择框 -> 单选

{{ selected }}

// 选择框 -> 多选

{{ selectList }}

// 实例对象
new Vue({
  el: '#app',
  data: {
    message: '', // 多行, 单行文本
    sex: '', // 单选框
    checked: false, // 复选框单个
    hobby: [], // 复选框多个
    selected: '', // 选择框 -> 单个
    selectList: [] // 选择框 -> 多个
  }
})

修饰符

  • .lazy : 默认情况下 v-model在每次的 input 事件触发后将输入框内容进行同步 , 添加 lazy 修饰符后 , 会变成 change 事件后同步数据

  • .number : 用户输入的值转为数值类型

  • .trim : 过滤输入框中的左右空白

Vue.set

如果在实例创建之后添加新的属性到实例上 , 它不会触发更新视图 怎么理解呢 ?

data() {
  return {
    info: {
      uname: 'zs'
    }
  }
}
mounted() {
  // 此时是不会生效的 , 如果再模块化的开发中还会报错
  this.info.age = 23
}

ES5 的限制 , Vue 不能检测到对象属性的添加或者删除 , 因为 Vue 在初始化的时候将属性转换为getter setter 所以属性必须要在 data 对象上才能让 Vue 转换 , 只有在 data 对象上 才是响应式的

mounted() {
  // 正确写法
  this.$set(this.info, 'age', 23)
}

Vue.set() : 与 this.$set没有区别, 一个全局 一个局部 官网说 this.$setVue.set的一个别名

methods

methods 将会被混入到Vue 实例中 , 可以直接通过 vm 实例访问这些方法 , 或者在指令表达式中使用 , 方法中的this自动绑定 Vue 实例
注意 : methods 中的 方法 不要使用 箭头函数 , 箭头函数中的 this指向父级作用域的上下文 , 所以this 将不会指向 Vue 实例

new Vue({
  methods: {
    handle() {
      console.log(this)
    }
  }
})

计算属性 computed

模板内写表达式固然是很方便的 , 但是你应该明白 , 表达式的初衷是用来计算的 , 比如处理一些字符串 , 时间格式等等 , 如果我们写成方法吧 ! 每次都要去调用 , 那就太麻烦了 , 为此 Vue 提供了 计算属性 computed

可以看出每次我们都去调用这个参数 , 感觉很不方便

 + 
 =
{{ getSum() }}
data() {
  return {
    sum: '',
    input1: '',
    input2: ''
  }
},
methods: {
  getSum() {
    return this.sum = this.input1 + this.input2
  }
}

下面我们使用计算属性解决 ; 可以看出我们去除了 data 中的 sum 属性 在 computed 中新增了 getSum 函数

 + 
 =
{{ getSum }}
data() {
  return {
    input1: '',
    input2: ''
  }
},
computed: {
  getSum() {
    return this.input1 + this.input2
  }
},

那么问题来了 为什么定义了一个函数, 却当成属性执行 ? 其实这个只是简写而已 , 算是一个语法糖 , 每一个计算属性包含 get 和 set 当只有 get 时可以简写为 函数的格式

export default {
  computed: {
    getSum: {
      get() {
       // 获取数据
      },
      set(val) {
        // val 是这个计算属性被修改之后的数据  设置数据
      }
    }
  }
}

示例 : 看完这个例子就明白了为什么叫 计算属性了吧

 + 
 =
{{ getSum }}


computed: {
  getSum: {
    get() {
      return this.input1  + this.input2
    },
    // 接收 getSum 这个属性改变后的值
    set(val) {
      console.log(val)
      this.input1 = 20
      this.input2 = 30
    }
  }
},

侦听属性 watch

虽然计算属性在大多数情况下都适用 , 但有时也需要一个自定义的侦听器 , 这个时候就需要 侦听属性 watch

仍然是计算两数之和 ; 在 watch 监听了 input1 的属性 input1 触发时 求出 sum ; 仔细看已经出现了问题, 修改 input2 的时候就不会再触发了 ;

总结 : 它监听 data 某一个属性的变化 , 并不会创造新的属性

 + 
 =
{{ getSum }}
data() {
  return {
    input1: '',
    input2: '',
    getSum: ''
  }
},
watch: {
  // 这样写看着是一个函数, 和属性理解不一致, 当然还可以写成这样
  input1(newVal, oldVal) {
    console.log(newVal, oldVal)
    this.getSum = this.input1 + this.input2
  }
  input2: {
    // 回调函数监听 input2 的变化  函数名必须是 handler
    handler(newVal, oldVal) {
      console.log(newVal, oldVal)
      this.getSum = this.input1 + this.input2
    }
  }
}

如果我们需要侦听对象属性, 可以在选项参数中使用 deep: true 注意监听数据的变更不需要这么做

watch: {
  obj: {
    handler() {
      // ....
    },
    deep: true 
  }
}

watch 使用时有一个特点 , 就是当值第一次绑定的时候 , 不会执行监听函数 , 只有值发生改变时才会执行 , 如果我们需要在最初绑定值的时候也执行函数 , 则需要用到immediate: true

methods computed watch 区别

  • watch 就是单纯的监听某个数据的变化 , 支持深度监听 , 接收两个参数一个最新值, 一个变化前的旧值 , 结果不会被缓存 , 并且 watch 可以处理异步任务

  • computed 是计算属性, 依赖于某个或者某些属性值 , 计算出来的结果会出现缓存 , 只有当数据的依赖项变化时才会发生变化 , 会创建一个新的属性

  • methods 是函数调用 , 没有缓存 , 主要处理一些业务逻辑, 而不是监听或者计算一些属性

过滤器 filter

可以被用于一些常见的文本格式化 , 允许被应用在两个地方 {{}} v-bind

{{ msg | formatMsg }}
  • 可以在组件的选项中定义组件内私有的过滤器
Vue.component('son-component', {
  template: `
    
{{ msg | formatMsg }}
`, data() { return { msg: 'this is message' } }, filters: { formatMsg(msg) { return msg.toString().toUpperCase() } } })
  • 可以在创建 Vue 实例之前定义全局过滤器
Vue.filter('formatMsg', function(msg) {
  return msg.toString().toUpperCase()
})
  • 过滤器默认是以 |前面的的内容作为过滤器的第一个参数 , 还可以再次传入传输
{{ msg | formatMsg('lower') }}
Vue.filter('formatMsg', function(msg, args) {
  console.log(msg) // lower
  if (args === 'lower') {
    return msg.toString().toLowerCase()
  }
})

自定义指令 directive

与上面提到的指令一致 , 如果那些指令不能满足使用要求 , 可以自己进行定制

自定获取焦点案例


// 全局指令 定义时不需要 v-  调用时要加上 v- 前缀
Vue.directive('focus', {
  inserted(el) {
    el.focus()
  }
})
// 或者可以定义为局部
directives: {
  'focus': {
    inserted(el) {
      el.focus()
    }
  }
}
钩子函数
  • bind :只调用一次 , 指令第一次绑定元素时调用 , 在这里可以进行一次性的初始化设置 ;
  • inserted : 被绑定元素插入父节点时调用 , 不一定渲染完成 , html 已经创建好了
  • update :所在组件的 VNode 更新时调用
  • componentUpdated : 指令所在的组件的 VNode 全部更新完成后
  • unbind : 指令与元素解绑时调用
钩子函数参数
  • el :指令所绑定的元素 , 可以直接操作 DOM
  • binding : 指令相关的配置对象
    • modifiers : 一个包含修饰符的对象 示例v-drag.limit
    • name :指令名 , 不包含前缀
    • value :指令绑定的值 v-drag="true"
// 拖拽方块案例
Vue.directive('drag', {
  // 初始化样式
  bind(el) {
    el.style.position = 'absolute'
    el.style.top = 0
    el.style.left = 0
    el.style.width = '100px'
    el.style.height = '100px'
    el.style.background = 'skyblue'
    el.style.cursor = 'pointer'
  },
  // 元素对象存在后, 开始写拖动逻辑
  inserted(el, binding) {
    let draging = false
    let elLeft = 0
    let elRight = 0
    
    document.addEventListener('mousedown', function (e) {
      draging = true
      let move = el.getBoundingClientRect()
      elLeft = e.clientX - move.left
      elRight = e.clientY - move.top  
    })
    document.addEventListener('mousemove', function (e) {
      let moveX = e.clientX - elLeft
      let moveY = e.clientY - elRight
      
      if (draging) {
        el.style.left = moveX + 'px'
        el.style.top = moveY + 'px'
      }
    })
    document.addEventListener('mouseup', function () {
      draging = false
    })
  }
})
自定义指令修饰符

相信大家仔细看上面的代码可能会发现这个方格拖拽还存在一些问题 ; 它还是可以拖拽到可视区域之外的 , 那么可不可以传递一个修饰符 , 来告诉他呢 ? 这时候就需要用到 binding 这个指令配置相关的对象了

// 我们先传入修饰符 limit 为自己定义的修饰符
// 既然不想让他拖拽出视口, 那么就应该在鼠标移动的时候加入一些逻辑
document.addEventListener('mousemove', function (e) {
  let moveX = e.clientX - elLeft
  let moveY = e.clientY - elRight
  
  // 是否传入了修饰符 limit 为什么这样可以获取 下面就上截图
  if (binding.modifiers.limit) {
    moveX = moveX <= 0 ? moveX = 0 : moveX   
    moveY = moveY <= 0 ? moveY = 0 : moveY
  }
  if (draging) {
    el.style.left = moveX + 'px'
    el.style.top = moveY + 'px'
  }
  console.log(binding) // binding 对象
})

自定义指令传参

上面我们已经解决了拖出视口的问题 , 只要传递一个修饰符就解决了 , 那么现在我们希望可以手动的暂停拖拽 , 当然也是可行的 ;

document.addEventListener('mousemove', function (e) {
  let moveX = e.clientX - elLeft
  let moveY = e.clientY - elRight
  
  // 是否传入了修饰符 limit
  if (binding.modifiers.limit) {
    moveX = moveX <= 0 ? moveX = 0 : moveX   
    moveY = moveY <= 0 ? moveY = 0 : moveY
  }
  // 是否传入 isDrag 判断是否可滑动
  if (!binding.value.isDrag) return
  if (draging) {
    el.style.left = moveX + 'px'
    el.style.top = moveY + 'px'
  }
})

组件

通常一个组件会以一棵嵌套的组件数的形式来组织 ; 为了能在模板中使用 , 这些组件必须先注册以便 vue 能够识别 ;

  • 全局组件
Vue.component('GlobalComponent', {
  template: `
hello component
` })
// 命名时推荐驼峰 , 调用时推荐 - 链接, html 不识别大小写

  • 局部组件
new Vue({
  el: '#app',
  components: {
    SonComponent: {
      template: `
hello private component
` } } })
// 组件可以被复用多次



  • 模块化开发中的组件
import SonComponent from '@/components/SonComponent.vue'

export default {
  components: {
    SonComponent
  }
}

通过 props 向子组件传递数据

prop 是组件上一些自定义的 attribute , 当一个值传递给一个 prop attribute 的时候 , 它就变成那个组件实例的 property ;

// 父组件
// 子组件
Vue.component('SonComponent', {
   // 多个单词可以是驼峰式, 但是父组件传递时多个单词必须是 - 连接
   // props 中的值, 可以像 data 中的数据一样访问  this.content / {{ content }} 
   // props 是只读的 切记不要修改 会报错
   props: ['content'],
   template: `
{{ content }}
` })

props 可以是数组也可以是一个对象 , 用来接收来自父组件的数据 ;
对象允许配置高级选项 , 如类型检测等

  • type : 可以是 Number String Boolean Array Object Date Function 任何自定义构造函数 , 或上述内容组成的数组 , 会检查一个 prop 是否是给定的类型 , 否则抛出异常

  • default : 默认值 , 对象或者数组的默认值必须从一个工厂函数中返回

  • required : boolean 是否为必填项

  • validator : Function 自定义验证函数会将 prop 的值作为唯一的参数传入

props: {
  content: {
    type: String,
    // default: 0, 普通值可直接默认返还
    default: () => [1, 2, 3],
    required: true,
  	 // 如果传进来的 content 长度大于 20 就会报错
    validator: (value) => value.length >= 20
  }
}

监听子组件事件 $emit

有些时候 , 父组件需要用的子组件中特定的值时 , 可以使用 $emit 把这个值传递出去

  • 行内模式传值
// 子组件

// 父组件
// 监听子组件定义的自定义事件 , 通过 $event 访问第一个传递的参数

  • 事件处理函数传值
// 子组件


// 父组件


  • 事件处理函数传递多个值
// 子组件


// 父组件


组件上使用v-model

在使用这个功能之前我们需要先了解一个东西 , v-model究竟是什么 ; 其实它从某种程度来说就是一个语法糖



应用到组件中就是下面这样 为了不引起歧义, 我把自定义的事件以及属性加了test 前缀详情看VUE官方文档

// 父组件

 
  • 子组件的 result 必须绑定到 value 上面
  • 在这个 input 触发的时候 通过 $emit 将自定义的 test-input 在暴露出去
// 子组件

// script
props: ['testValue'] // 自定义属性传递过来的值

此时我们再优化一下 , 使用 v-model

// 父组件
 

由于我们组件中使用了 v-model 而前面我们也提到了 v-model 其实是 v-bind 和 v-on 的语法糖 , 所以只能用 value 属性和 input 事件

// 子组件

// script
props: ['value'] 

那么问题来了 , 上面我们提过 v-model 默认是 value属性 和 input事件 , 但是像单选框 , 复选框等类型怎么处理 ? 对此 Vue 提供了model 选项来避免这样的冲突

单个复选框组件绑定

// 父组件


// script
data() {
  return {
    isChecked: false
  }
},

选中 和 未选中 返回 true / false

// 子组件

// script

export default {
  name: 'ModelInput',
  // v-model 拆分
  model: {
    prop: 'checked', // 将传进来的 isChecked 变成 checked 供后面的 props 使用
    event: 'change' //  定义 emit 自定义的事件名字
  },
  props: {
    checked: {
      type: Boolean
    }
  }
}

组件插槽

在 2.6.0 中 为具名插槽和作用域插槽提供了新的统一语法 v-slot 它取代了 slotslot-scope 这两个目前已被废弃 , 但是还没有移除 (仍然可以使用)
插槽 : 简单理解就是 占坑 在组件模板中占好位置 , 当使用该组件的标签时 , 组件标签的内容就会自动填坑 , ( 替换组件模板中的 slot位置 ) , 并且可以作为承载分发内容的出口

内容插槽
// 子组件

// 父组件
<

规则 : 父级模板里的所有内容都是在父级作用域中编译的 ; 子模板的所有内容都在子作用域中编译

默认内容插槽

标签内可以加入 组件, 文本, 标签等默认内容 , 如果父组件调用时, 没有传入内容, 那么就会展示默认的内容

// 子组件

具名插槽

有些时候一个插槽是不能满足需求的 , 我们可能需要多个 ; 对于这种情况 , 元素中有一个特殊的 attribute name这个 attribute 用来定义额外的插槽

// 子组件

在向具名插槽提供内容的时候 , 可以在一个 template 元素中上 使用v-slot指令 并以参数的形式提供名称

// 父组件