前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
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核心梳理 组件通信 生命周期 Vuex 与路由原理

首页2019-10-06 18:10:32Front-End
VueVuexVue Router面试

Vue 用了两年,业务需求都能做,但被问到「组件为什么会更新」「computed 和 watch 到底该选哪个」「Vuex 的 state 是怎么变成响应式的」,脑子里能浮现的只有 API 文档的样子,讲不出个所以然。我自己有一阵就卡在这个状态,会用但不成体系。这篇是我把 Vue 2 的核心概念从头串一遍的笔记,从组件的属性、事件、插槽三要素起步,一路走到 Vuex 的简化实现和路由的底层原理。每一节都给了可以直接跑的代码和它的使用边界,不是 API 罗列。

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

  • 组件的三个核心概念,属性 props 的四种声明写法、事件的触发与冒泡、插槽的新旧两套语法
  • 「大属性」这种把插槽当属性传的写法,什么时候值得用
  • 双向绑定和单向数据流为什么不冲突,v-model 展开之后是什么
  • 触发组件更新的条件是什么,为什么有时候改了数据视图不动
  • computed 和 watch 各自的适用面,以及为什么优先用 computed
  • 生命周期每个钩子的实际用途,函数式组件省掉了什么
  • 内置指令和自定义指令的五个钩子
  • template 和 JSX 的取舍,同一组组件用 JSX 重写一遍是什么样
  • Vuex 的运行机制、核心概念、最佳实践,以及一个三十行的简化实现
  • vue-router 的使用方式、路由类型和底层原理

# 一、组件的核心概念-属性props几种写法

我们的开发都是围绕着 options 来的。Vue 2 的组件说到底就是一个配置对象,props、data、computed、methods、生命周期钩子,全是这个对象上的字段。先把这张全景图放在这里:

Vue 组件 options 配置项全景,包含数据、DOM、生命周期、资源等分类

组件之间要协作,靠的是三个东西:属性往下传数据,事件往上报变化,插槽往里塞内容。属性是其中最基础的一环。

props 的几种声明写法对比,数组写法与对象写法

下面这个组件把 props 的几种写法都用上了,类型约束、自定义校验、默认值、函数型 prop 各一个:

<template>
  <div>
    name: {{ name }}
    <br />
    type: {{ type }}
    <br />
    list: {{ list }}
    <br />
    isVisible: {{ isVisible }}
    <br />
    <button @click="handleClick">change type</button>
  </div>
</template>

<script>
export default {
  name: "PropsDemo",
  // inheritAttrs: false,
  // 这种写法不利于后期维护
  // props: ['name', 'type', 'list', 'isVisible'],
  props: {
    name: String,
    type: {
      validator: function(value) {
        // 这个值必须匹配下列字符串中的一个
        return ["success", "warning", "danger"].includes(value);
      }
    },
    list: {
      type: Array,
      // 对象或数组默认值必须从一个工厂函数获取
      default: () => []
    },
    isVisible: {
      type: Boolean,
      default: false
    },
    onChange: {
      type: Function,
      default: () => {}
    }
  },
  methods: {
    handleClick() {
      // 不要这么做、不要这么做、不要这么做
      // this.type = "warning";

      // 可以,还可以更好
      this.onChange(this.type === "success" ? "warning" : "success");
    }
  }
};
</script>
@前端进阶之旅: 代码已经复制到剪贴板

这段代码里有四个点值得停一下。

props: ['name', 'type', 'list', 'isVisible'] 这种数组写法被我注释掉了,因为它不利于后期维护。数组写法只声明了名字,没有类型、没有默认值、没有校验,半年后接手的人得去读调用方的代码才知道该传什么。

type 用的是 validator 自定义校验,只允许 success、warning、danger 三个值。传错了 Vue 会在控制台打警告,但组件照常渲染,它是开发期的提示不是运行时的拦截。

list 的默认值写成了 default: () => [],这里必须是工厂函数。对象或数组默认值必须从一个工厂函数获取,因为组件可能被复用多次,写成 default: [] 的话所有实例共享同一个数组引用,一个实例往里 push,别的实例全跟着变。

onChange 是一个函数型的 prop。handleClick 里注释掉的那行 this.type = "warning" 是明确不该做的事,props 是单向的,子组件直接改父组件传下来的值,父组件下次重新渲染就会把你的修改覆盖掉,而且 Vue 会警告。正确做法是调用父组件传下来的 onChange,让父组件自己去改。

用法这边混着写了 props 和原生属性:

// 用法
<Props
  name="Hello Vue!" // 原生属性
  :type="type"
  :is-visible="false"
  :on-change="handlePropChange"
  title="属性Demo" // 原生属性
  class="test1" // 原生属性
  :class="['test2']"
  :style="{ marginTop: '20px' }"
  style="margin-top: 10px" // 原生属性
/>
@前端进阶之旅: 代码已经复制到剪贴板

没有在 props 里声明的属性,比如这里的 title,会落到组件根元素上,同时能通过 $attrs 拿到。不想让它自动落到根元素上,就打开被注释掉的那行 inheritAttrs: false。

class 和 style 是两个特例,它们不走 $attrs,而是和组件内部根元素上写的 class、style 做合并,不是覆盖。所以上面同时写 class="test1" 和 :class="['test2']" 两个都会生效,style 同理。

还有个细节,模板里写 :is-visible 是短横线命名,声明的时候是 isVisible 驼峰。HTML 属性名大小写不敏感,所以模板里推荐用短横线,Vue 会自动转换。用 JSX 或者 render 函数的时候没有这个限制,直接写驼峰。

# 二、组件的核心概念-事件

属性把数据从父传到子,事件负责把变化从子报回父。

Vue 组件自定义事件的触发与监听机制示意

下面这个例子既演示了 $emit 上报,也演示了一个很容易踩的坑,原生事件的冒泡阻止:

<template>
  <div>
    name: {{ name || "--" }}
    <br />
    <input :value="name" @change="handleChange" />
    <br />
    <br />
    <div @click="handleDivClick">
      <button @click="handleClick">重置成功</button>&nbsp;&nbsp;&nbsp;
      <button @click.stop="handleClick">重置失败</button>
    </div>
  </div>
</template>

<script>
export default {
  name: "EventDemo",
  props: {
    name: String
  },
  methods: {
    handleChange(e) {
      this.$emit("change", e.target.value);
    },
    handleDivClick() {
      this.$emit("change", "");
    },
    handleClick(e) {
      // 都会失败
      //e.stopPropagation();
    }
  }
};
</script>
@前端进阶之旅: 代码已经复制到剪贴板

handleChange 里的 this.$emit("change", e.target.value) 是标准写法,事件名用小写短横线风格更安全,因为 HTML 里事件名同样大小写不敏感。

真正有意思的是那两个按钮。外层 div 上绑了 handleDivClick,里层按钮上绑了 handleClick。点「重置成功」这个按钮,事件冒泡到外层 div,handleDivClick 会执行,$emit("change", "") 把名字清空,所以它「成功」了。点「重置失败」那个按钮,模板上写了 @click.stop,冒泡被 Vue 的修饰符拦住了,外层的处理函数不执行。

handleClick 里被注释掉的 e.stopPropagation() 标着「都会失败」,这句注释指的是:你想在这个方法里手动阻止冒泡,效果和用 .stop 修饰符不一样。修饰符生成的代码是在事件处理函数最外层先调 stopPropagation,而你写在函数体里的时机没那么靠前,遇到多个处理函数或者更复杂的绑定顺序时行为不可控。

结论就是能用修饰符就用修饰符。.stop、.prevent、.capture、.self、.once、.passive 这一套是 Vue 给你的声明式出口,逻辑意图写在模板上一眼可见,比藏在方法体里清楚得多。

# 三、组件的核心概念-插槽

属性传数据,事件传变化,插槽传的是「内容」,也就是一段 DOM 结构。

Vue 插槽的默认插槽、具名插槽与作用域插槽结构示意

Vue 2.6 换过一次插槽语法,新旧两套都能用,下面这段把它们放在一起对比:

 <a-tab-pane key="slot" tab="插槽">
    <h2>2.6 新语法</h2>
    <SlotDemo>
      <p>default slot</p>
      <template v-slot:title>
        <p>title slot1</p>
        <p>title slot2</p>
      </template>
      <template v-slot:item="props">
        <p>item slot-scope {{ props }}</p>
      </template>
    </SlotDemo>
    <br />
    <h2>老语法</h2>
    <SlotDemo>
      <p>default slot</p>
      <p slot="title">title slot1</p>
      <p slot="title">title slot2</p>
      <p slot="item" slot-scope="props">item slot-scope {{ props }}</p>
    </SlotDemo>
</a-tab-pane>
<script>
import Slot from "./Slot";
export default {
  components: {
    SlotDemo: Slot
  },
  data: () => {
    return {
      name: "",
      type: "success",
      bigPropsName: "Hello world!"
    };
  },
};
</script>
@前端进阶之旅: 代码已经复制到剪贴板

新语法统一用 v-slot,具名插槽写 v-slot:title,作用域插槽写 v-slot:item="props",一个指令覆盖两种场景。老语法是 slot="title" 和 slot-scope="props" 两个不同的特性,而且 slot 是写在普通元素上的,看不出它和插槽的关系。

新语法还有个硬性限制:v-slot 只能用在 <template> 标签或者组件标签上,不能像老的 slot 那样随便挂在一个 <p> 上。这个限制不是找麻烦,它让插槽的边界在模板里变得明确。

对应的子组件是这么定义插槽出口的:

fe
  • 一、组件的核心概念-属性props几种写法
  • 二、组件的核心概念-事件
  • 三、组件的核心概念-插槽
  • 四、双向绑定和单向数据流并不冲突
  • 五、如何触发组件的更新
  • 六、合理应用计算属性和监听器
    • 6.1 计算属性Computed
    • 6.2 监听watcher
    • 6.3 computed vs watcher
  • 七、生命周期的应用场景和函数式组件
    • 7.1 生命周期
    • 7.2 函数式组件
  • 八、Vue指令
    • 8.1 内置指令
    • 8.2 自定义指令
  • 九、template和jsx
    • 9.1 JSX VS template
    • 9.2 以下是jsx写法
  • 十、为什么需要vuex
  • 十一、vuex核心概念和底层原理
    • 11.1 核心概念
    • 11.2 底层原理
  • 十二、vuex最佳实践
    • 12.1 核心概念
    • 12.2 使用常量代替Mutation事件类型
    • 12.3 命名空间
    • 12.4 实践例子
    • 12.5 Vuex 在今天
  • 十三、vue-router使用场景
    • 13.1 解决的问题
    • 13.2 使用方式
    • 13.3 例子
  • 十四、路由的类型及底层原理
  • 总结
  • 参考

← Ionic 项目 iOS 打包真机调试与上架 App Store 全流程React Native Icon 与启动图设置 iOS 和 Android 双端配置实战 →