Vue 常见 API 及问题

组件通信


父子组件通信通过 props$emit 相信小伙伴们都清楚,那么毫无关联的两个组件如果需要实现通信,又有哪些方法呢?相信小伙伴们可能第一时间想到了 vuex,但是如果只是简单的业务逻辑,vuex 的引入和维护就没有那么必要~~~我百度了一下,大部分有说通过 使用 Vue事件总线(EventBus) 来进行处理无关联组件之间的通信,当然也有说 vue 自身就有自定义事件 event.$on、event.$emit、event.$off 来进行相关逻辑处理,其实两者殊途同归,用法差异不大,来看栗子:

  • 初始化 event
// event.js
import Vue from 'vue'
export default new Vue()
  • 组件 A,通过 event.$emit 发送事件
<template>
  <div class="index">
    <button @click="sendMsg()">按钮</button>
  </div>
</template>

<script>
import event from './event'
export default {
  methods: {
    sendMsg() {
      event.$emit('aMsg', '来自A页面的消息')
    }
  }
}
</script>
  • 组件 B,通过 event.$on 接收事件
<template>
  <div class="list">{{ msg }}</div>
</template>

<script>
import event from './event'
export default {
  data() {
    return {
      msg: ""
    }
  },
  methods: {
    addMsgFromA(msg) {
      this.msg = msg
      console.log(msg) // 来自A页面的消息
    }
  },
  mounted() {
    // event.$on('aMsg', (msg) => {
    //   this.msg = msg
    //   console.log(msg)
    // })
    event.$on('aMsg', this.addMsgFromA)
  },
  beforeDestroy() {
    // 及时销毁,否则可能造成内存泄漏
    event.$off('aMsg', this.addMsgFromA)
  }
}
</script>

上述代码中,特意将组件 B 中的 mounted 函数内的方法进行了封装,而不是直接写,是为了保证在页面销毁时对该方法进行及时移除,我们也可以使用 event.$off('aMsg', this.addMsgFromA) 来移除应用内所有对此某个事件的监听。或者直接调用 event.$off() 来移除所有事件频道,不需要添加任何参数 。

生命周期


Vue 的声明周期基本可以分为三个阶段:

  • 挂载阶段
    beforeCreatecreatedbeforeMountmounted
  • 更新阶段
    beforeUpdateupdated
  • 销毁阶段
    beforeDestroydestroyed

createdmounted 的区别?

  • created 页面还没有开始渲染,但是页面的实例已经初始化完成(此时可以获取到 datamethods 中的数据,无法获取 DOM 节点);mounted 页面完成渲染,此时组件在网页上绘制完成。此时可获取到 DOM 节点。

父子组件生命周期调用顺序


写一个简略版的 todoList ,查看父子组件的生命周期调用顺序。

  • 父组件
<template>
  <div class="index">
    <Input @addTitle="addTitleHandler" />
    <List :list="list" @delete="deleteHandler" />
  </div>
</template>

<script>
import Input from './Input'
import List from './List'
export default {
  components: { Input, List },
  data() {
    return {
      list: [
        {
          id: 'id-1',
          title: '标题1'
        },
        {
          id: 'id-2',
          title: '标题2'
        }
      ]
    }
  },
  methods: {
    addTitleHandler(title) {
      this.list.push({
        id: `id-${Date.now()}`,
        title
      })
    },
    deleteHandler(id) {
      this.list = this.list.filter(item => item.id !== id)
    }
  },
  created() {
    console.log('index created')
  },
  mounted() {
    console.log('index mounted')
  },
  beforeUpdate() {
    console.log('index beforeUpdate')
  },
  updated() {
    console.log('index updated')
  },
  beforeDestroy() {
    console.log('index beforeDestroy')
  },
  destroyed() {
    console.log('index destroyed')
  }
}
</script>
  • 子组件 List
<template>
  <div class="list">
    <ul>
      <li v-for="item in list" :key="item.id">
        {{ item }}
        <button @click.stop="deleteItem(item.id)">删除</button>
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  props: {
    list: {
      type: Array,
      default() {
        return []
      }
    }
  },
  methods: {
    deleteItem(id) {
      this.$emit('delete', id)
    }
  },
  created() {
    console.log('list created')
  },
  mounted() {
    console.log('list mounted')
  },
  beforeUpdate() {
    console.log('list beforeUpdate')
  },
  updated() {
    console.log('list updated')
  },
  beforeDestroy() {
    console.log('list beforeDestroy')
  },
  destroyed() {
    console.log('list destroyed')
  }
}
</script>
  • 子组件 Input
<template>
  <div>
    <input type="text" v-model="title" />
    <button @click="addTitle">add</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      title: ''
    }
  },
  methods: {
    addTitle() {
      this.$emit('addTitle', this.title)
      this.title = ''
    }
  }
}
</script>

挂载阶段的执行渲染结果为:index created => list created => list mounted => index mounted,基本可以说明:父子组件渲染的挂载阶段是由外向内在向外进行执行的。而更新阶段渲染结果为:index beforeUpdate => list beforeUpdate => list updated => index updated,基本逻辑流程和挂载阶段差不多。

$nextTick


  • Vue 是异步渲染

  • data 改变之后,DOM 不会立刻渲染

  • $nextTick 会在 DOM 渲染之后被触发,以获取最新 DOM 节点

其实很好理解,因为 Vue 中所有 DOM 的渲染都是异步的,所以我们在 DOM 渲染之后直接获取 DOM 元素可能会有偏差,使用 $nextTick 可以保证所有异步渲染完成之后再来执行,如下栗子:

<template>
  <div>
    <ul ref="ul1">
      <li v-for="(item, index) in list" :key="index">{{ item }}</li>
    </ul>
    <button @click="addItem">添加一项</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      list: ['a', 'b', 'c']
    }
  },
  methods: {
    addItem() {
      this.list.push(`${Date.now()}`)
      this.list.push(`${Date.now()}`)
      this.list.push(`${Date.now()}`)

      // 获取 DOM 元素
      const ulElem = this.$refs.ul1
      console.log(ulElem.childNodes.length) // 3
      // 使用 $nextTick
      this.$nextTick(() => {
        const ulElem = this.$refs.ul1
        console.log(ulElem.childNodes.length) // 6
      })
    }
  }
}
</script>

slot


  • 基本使用 (父组件往子组件中插入一段内容)
// 父组件
<template>
  <div id="app">
    <slot-demo :url="website.url">
      {{ website.title }}
    </slot-demo>
  </div>
</template>

<script>
import SlotDemo from './components/SlotDemo'
export default {
  name: "App",
  components: { SlotDemo },
  data() {
    return {
      name: '张三',
      website: {
        url: 'https://www.baidu.com',
        title: '百度',
        subTitle: '百度一下,你就知道'
      }
    }
  },
};
</script>

// 子组件
<template>
  <div>
    <a :href="url">
      <slot>默认内容,父组件没设置内容,我就会被显示出来</slot>
    </a>
  </div>
</template>
<script>
export default {
  props: {
    url: {
      type: String,
      default() {
        return ''
      }
    }
  }
}
</script>
  • 作用域插槽 (子组件 data 中的数据传递给父组件)。子组件通过 :slotData 绑定要传递的 data 中的数据,父组件通过 template 包裹定义 v-slot 接收,然后直接插值调用。如下栗子:
// 父组件
<div id="app">
  <slot-demo :url="website.url">
    <template v-slot="slotProps">
      {{ slotProps.slotData.title }}
    </template>
  </slot-demo>
</div>
<script>
data() {
  return {
    name: '张三',
    website: {
      url: 'https://www.baidu.com',
      title: '百度',
      subTitle: '百度一下,你就知道'
    }
  }
}
</script>
// 子组件
<div>
  <a :href="url">
    <slot :slotData="website">{{ website.subTitle }}</slot>
  </a>
</div>
<script>
data() {
  return {
    website: {
      url: 'https://www.qq.com',
      title: 'QQ',
      subTitle: '每一天,乐在沟通'
    }
  }
}
</script>
  • 具名插槽 (父组件通过 v-slot 与子组件 slot 中的 name 值进行绑定)
// 父组件
<slot-demo>
  <template v-slot:header>
    <h1>这里的内容会被插入到子组件的 header 中</h1>
  </template>
  <p>这里的内容会被插入到子组件的 main 中</p>
  <template v-slot:footer>
    <p>这里的内容会被插入到 footer slot 中</p>
  </template>
</slot-demo>
// 子组件
<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

mixin


  • 多个组件有相同的逻辑,抽离出来

  • mixin 并不是完美的解决方案,会有一些问题

  • 变量来源不明确,不利于阅读

  • mixin 可能会造成命名冲突

  • mixin 和组件可能会出现多对多的关系,复杂度较高

  • Vue 3 提出的 Composition API 旨在解决这些问题

当然缺点固然很多,但是日常开发中使用 mixin 还是非常符合 真香定律 的。基础用法:

<template>
  <div>
    <p>{{ name }} {{ major }} {{ city }}</p>
    <button @click="showName">显示姓名</button>
  </div>
</template>

<script>
import myMixin from './mixin'
export default {
  mixins: [myMixin], // 可添加多个,如[myMixin, myMixin1, myMixin2]
  data() {
    return {
      name: '张三',
      major: 'web 前端'
    }
  },
  methods: {},
  mounted() {
    console.log('mixin mounted', this.name)
  }
}
</script>
//mixin.js
export default {
  data() {
    return {
      city: '上海'
    }
  },
  methods: {
    showName() {
      console.log(this.name)
    }
  },
  mounted() {
    console.log('mixin.js mounted', this.name) // 先于 vue 模板中的 mounted 执行
  }
}

cityshowName 我们都没有在模板中直接定义,而是定义在 mixin.js 中,但是我们却可以直接使用,这是因为 mixins 会将 mixin.js 中的内容和我们 vue 模板中的内容进行融合,从而导致多个地方都可以直接使用 mixin.js 中的内容。

动态组件


  • :is = "component-name" 用法

  • 需要根据数据,动态渲染的场景。即组件类型不确定。

光说可能有点混,但是开发中还真的遇到过~~~举个栗子:

<template>
  <div id="app">
    <!-- 使用 :is 和 data 中的值进行绑定 -->
    <component :is="nextTickName"></component>
  </div>
</template>
<script>
import NextTick from './components/NextTick'
export default {
  name: "App",
  components: { NextTick }, // 此处还是要引入和注册组件
  data() {
    return {
      nextTickName: NextTick // 将组件赋值到 data 中
    }
  },
};
</script>

有没有感觉多此一举,但是 vue 将其作为 API 独立出来还是有一定意义的,开发中很多位置还是会用到的,像组件切换,Tab 等确实会用到,具体实用场景小伙伴可自行斟酌哦~~~

异步组件


  • import() 函数

  • 按需加载,异步加载大组件

感觉这个用法和 vue-router 差不多,具体看栗子一目了然:

<template>
  <div id="app">
    <next-tick></next-tick>
  </div>
</template>

<script>
export default {
  name: "App",
  components: {
    NextTick: () => import('./components/NextTick') // import 按需加载组件
  },
  data() {
    return {
    }
  },
};
</script>
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335