前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版

Vue 组件从注册到通信完整梳理(九)

首页2018-08-27 11:20:32Front-End
Vue组件组件通信

组件是 Vue 里绕不过去的一关。语法本身不难,Vue.component 一注册,模板里当标签用就行。真正卡人的是那些「为什么必须这样写」的规则:为什么组件的 data 一定要写成函数,为什么模板只能有一个根元素,为什么在子组件里改 props 会挨骂,为什么 table 里放自定义组件会渲染错位。这些规则不是 Vue 团队随手定的,每一条背后都有具体的原因。这篇把组件的注册、传值、事件、插槽、通信这几块串起来讲一遍,重点放在规则背后的机制上,顺带把原文里几处写错的代码改对。

在本篇文章中,我们将从浅入深,和大家一起学习以下知识:

  • 全局注册和局部注册的差别,各自什么时候用
  • 组件的 data 为什么必须是函数,不是函数会发生什么
  • DOM 模板的解析限制,以及 is 属性在解决什么问题
  • props 的传值规则,为什么静态传值拿到的永远是字符串
  • 单向数据流,子组件想改父组件数据的三种正确姿势
  • prop 验证的完整写法和自定义构造器校验
  • 自定义事件、.native 修饰符,以及 v-model 在组件上的原理
  • 动态组件 component :is 和 keep-alive 的缓存
  • 具名插槽的内容分发规则
  • 父子、子父、兄弟三个方向的组件通信
  • 这些写法在 Vue 3 里哪些变了

# 一、组件的基本使用

# 1.1 注册组件

注册组件就是利用 Vue.component() 方法,先传入一个自定义组件的名字,然后传入这个组件的配置:

Vue.component('mycomponent',{
    template: `<div>这是一个自定义组件</div>`,
    data () {
      return {
        message: 'hello world'
      }
    }
  })
@前端进阶之旅: 代码已经复制到剪贴板

这样就创建了一个全局组件,之后在任何 Vue 实例挂载的 DOM 元素中都可以使用它。

还可以在某个 Vue 实例中注册只有自己能用的局部组件,写在 components 选项里:

var app = new Vue({
    el: '#app',
    data: {
    },
    components: {
      'my-component': {
        template: `<div>这是一个局部的自定义组件,只能在当前Vue实例中使用</div>`,
      }
    }
  })
@前端进阶之旅: 代码已经复制到剪贴板

两种组件在模板里的用法没差别:

<div id="app">
    <mycomponent></mycomponent>
    <my-component></my-component>
</div>
@前端进阶之旅: 代码已经复制到剪贴板

那该选哪种?我的判断标准是看复用范围。全局注册胜在方便,注册一次到处能用,但它有个实实在在的代价:无论项目里有没有真的用到,这个组件的代码都会被打进最终产物,而且打包工具没法通过静态分析摇掉它。所以只有确实全站在用的基础组件(按钮、图标、弹窗)才值得全局注册,业务组件一律局部注册。

组件名的写法也有讲究。用 kebab-case(短横线)注册的组件,模板里只能用 kebab-case;用 PascalCase(首字母大写)注册的,两种写法都能用。在字符串模板和 DOM 模板里表现还不一样,稳妥的做法是注册时用 PascalCase,在单文件组件的模板里也用 PascalCase,在 DOM 模板里用 kebab-case。

# 1.2 模板的要求

组件的模板只能有一个根元素,下面的写法是不允许的:

template: `<div>这是一个局部的自定义组件,只能在当前Vue实例中使用</div>
            <button>hello</button>`,
@前端进阶之旅: 代码已经复制到剪贴板

为什么有这条限制?因为 Vue 2 的虚拟 DOM 实现里,一个组件实例对应一棵虚拟节点树,而树必须有唯一的根。有了唯一根节点,diff 的时候才能从一个确定的起点开始比对,组件的 $el 也才有一个明确的指向。

实际写业务时,绕过它的办法就是外面包一层 div。代价是多一层无意义的 DOM 嵌套,在做 flex 布局或者写 CSS 选择器的时候偶尔会碍事。

Vue 3 已经支持多根节点了(官方叫 Fragment),这条限制不存在了。但相应地,多根节点组件上的非 prop 属性没法自动继承(Vue 不知道该往哪个根上放),需要用 v-bind="$attrs" 手动指定落到哪个元素上。

# 1.3 组件中的 data 必须是函数

注册组件时传入的配置和创建 Vue 实例差不多,但有一个关键差别:data 属性必须是一个函数。

原因在于 JavaScript 里对象类型的变量保存的是引用。如果像 Vue 实例那样直接传一个对象,当页面上存在多个这样的组件时,它们会共享同一份数据,导致一个组件中数据的改变会引起其他组件数据的改变。

而使用一个返回对象的函数,每次创建组件实例时都会调用一次这个函数,拿到一个全新的对象,各实例之间互不影响。

这个坑很有画面感:一个列表里渲染了十个计数器组件,点第一个的加号,十个数字一起跳。写业务的时候不太容易碰到,因为 .vue 单文件组件里 data 天然就是函数写法,写错了 ESLint 也会拦。但面试问到「为什么」的时候,能不能说到「引用类型共享」这一层,差别就出来了。

顺带说一句,根实例(new Vue({}))的 data 可以是对象,因为根实例全局只有一份,不存在复用问题。

# 1.4 关于 DOM 模板的解析

当使用 DOM 作为模板时(例如把 el 选项挂载到一个已存在的元素上),会受到 HTML 的一些限制,因为 Vue 只有在浏览器解析和标准化 HTML 之后才能获取模板内容。

像 ul、ol、table、select 这些元素限制了能被它包裹的元素,而 option 这样的元素只能出现在某些特定元素内部。在自定义组件中使用这些受限制的元素时会出问题,例如:

<table>
  <my-row>...</my-row>
</table>
@前端进阶之旅: 代码已经复制到剪贴板

自定义组件 my-row 被认为是无效的内容,浏览器在解析阶段就会把它从 table 里挪出去,等 Vue 接手时结构已经乱了。这时应使用特殊的 is 属性:

<table>
  <tr is="my-row"></tr>
</table>
@前端进阶之旅: 代码已经复制到剪贴板

也就是说,标准 HTML 中一些元素里只能放置特定的子元素,另一些元素只能存在于特定的父元素中。比如 table 中不能直接放 div,tr 的父元素不能是 div。所以当使用自定义组件时,标签名还是用那些合法的标签名,把自定义组件的名字填在 is 属性里。

有一点要强调,这是浏览器的 HTML 解析规则在作怪,跟 Vue 没关系。所以如果模板不是从页面 DOM 里读出来的,这些限制就不适用。以下三种来源不受影响:

  • type="text/x-template" 的模板标签
  • JavaScript 内联模板字符串
  • .vue 单文件组件

一般情况下,只要用了 vue-cli 或者 Vite 走单文件组件,就完全不用操心这一节的内容。这也是我建议新手直接上脚手架、别从 CDN 引入开始学的原因之一,能少踩一整类坑。

# 二、组件的属性和事件

在 HTML 中使用元素会有一些属性,如 class、id,还可以绑定事件,自定义组件也是一样的。当在一个组件中使用了其他自定义组件时,就会利用子组件的属性和事件来和父组件进行数据交流。

父子组件通过 props 向下传值、通过 events 向上通信

父子组件之间的通信可以概括成 props down,events up。父组件通过属性 props 向下传递数据给子组件,子组件通过事件 events 给父组件发送消息。

比如子组件需要某个数据,就在内部定义一个 prop 属性,然后父组件像给 HTML 元素指定特性值一样,把自己的 data 属性传递给子组件的这个属性。而当子组件内部发生了什么事情,就通过自定义事件把涉及到的数据暴露出来,供父组件处理。

<my-component v-bind:foo="baz" v-on:event-a="doThis(arg1, arg2)"></my-component>
@前端进阶之旅: 代码已经复制到剪贴板

这一行里,foo 是 my-component 组件内部定义的一个 prop 属性,baz 是父组件的一个 data 属性;event-a 是子组件定义的一个事件,doThis 是父组件的一个方法。

整个过程是这样的:

  • 父组件把 baz 数据通过 prop 传递给子组件的 foo
  • 子组件内部拿到 foo 的值,进行相应的操作
  • 当子组件内部发生了一些变化、希望父组件知道时,触发 event-a 事件,把数据发送出去
  • 父组件把这个事件的处理器绑定为 doThis 方法,子组件发送的数据作为参数传进来
  • 父组件根据这些数据进行相应的操作

这套约定看着简单,但它定义了整个组件树的数据流向:数据只能从上往下流,变更的意图只能从下往上报。所有组件通信的复杂方案(Vuex、provide/inject、event bus)都是在这条基本规则不够用的时候才出场的。

# 三、属性 Props

Vue 组件通过 props 属性来声明自己接收哪些属性,然后父组件就可以往里面传递数据:

Vue.component('mycomponent',{
    template: '<div>这是一个自定义组件,父组件传给我的内容是:{{myMessage}}</div>',
    props: ['myMessage'],
    data () {
      return {
        message: 'hello world'
      }
    }
  })
@前端进阶之旅: 代码已经复制到剪贴板

调用该组件:

<div id="app">
    <mycomponent my-message="hello"></mycomponent>
</div>
@前端进阶之旅: 代码已经复制到剪贴板

注意,由于 HTML 特性不区分大小写,所以在 DOM 模板里传递属性时,myMessage 应该转换成 kebab-case 形式写作 my-message="hello"。声明的时候用驼峰,模板里用短横线,Vue 会自动做这层转换。

这条规则只在 DOM 模板里成立。单文件组件的模板是由 Vue 自己编译的,不经过浏览器的大小写标准化,所以写 :myMessage 也能工作。但为了统一,社区惯例还是模板里一律 kebab-case。

# 3.1 v-bind 绑定属性值

一般情况下,使用 v-bind 给元素特性传递值时,Vue 会把引号里的内容当作一个表达式来求值。

class 和 style 是两个例外,用 v-bind:class 和静态 class 同时传入类名,效果会叠加,因为对这两个特性 Vue 采用的是合并而不是替换的原则。

# 3.2 动态绑定特性值

想要把父组件的属性绑定到子组件,应该使用 v-bind,这样父组件中数据改变时能反映到子组件。

根据传递的属性类型不同,在子组件中更改这个属性会出现两种情况。

第一种,当父组件传递的属性是引用类型时,在子组件中更改属性内部的内容会直接影响父组件:

fe
  • 一、组件的基本使用
    • 1.1 注册组件
    • 1.2 模板的要求
    • 1.3 组件中的 data 必须是函数
    • 1.4 关于 DOM 模板的解析
  • 二、组件的属性和事件
  • 三、属性 Props
    • 3.1 v-bind 绑定属性值
    • 3.2 动态绑定特性值
    • 3.3 子组件希望对传入的 prop 进行操作
    • 3.4 给子组件传递正确类型的值
  • 四、Prop 验证
    • 4.1 非 prop 类型的属性
  • 五、自定义事件
    • 5.1 绑定原生事件
    • 5.2 探究 v-model
    • 5.3 用自定义事件创建自定义的表单输入组件
  • 六、动态组件
  • 七、使用 slot 分发内容
    • 7.1 单个 slot
    • 7.2 带名称的 slot
  • 八、组件通信
    • 8.1 父传子
    • 8.2 子传父
    • 8.3 兄弟组件之间的通信
  • 九、Vue 3 里组件这块变了什么
  • 总结
  • 参考

← Vue 计算属性与 watch 数据监听详解(十)Vue 过滤器 filters 全局与局部注册用法(八) →