vue高阶组件
高阶组件的定义
- 高阶组件是一个函数,它将一个组件作为参数并返回新创建的组件。返回的组件通常使用HOC(高阶组件)提供的功能进行扩充。
- 高阶组件不是可以抓取和安装的软件,而是一种可以帮助编写可重用和可维护代码的技术方案。
示例
- BlogPost(博文),CommentsList(评论列表)为例代码如下:
<template>
<div id="app">
<blog-post/>
<comments-list/>
</div>
</template>
<script>
import CommentsList from './components/CommentsList'
import BlogPost from './components/BlogPost'
export default {
name: 'app',
components: {
'blog-post': BlogPost,
'comments-list': CommentsList
}
}
</script>
<template>
<ul>
<li
v-for="(comment, index) in comments"
:key="index"
>{{comment}}</li>
</ul>
</template>
<script>
import DataSource from '../store/source.js'
export default {
name: 'comments-list',
data() {
return {
comments: DataSource.getComments()
}
},
methods: {
handleChange() {
this.comments = DataSource.getComments()
}
},
mounted() {
DataSource.addChangeListener(this.handleChange)
},
beforeDestroy() {
DataSource.removeChangeListener(this.handleChange)
}
}
</script>
<template>
<div>
{{blogPost}}
</div>
</template>
<script>
import DataSource from '../store/source.js'
export default {
data() {
return {
blogPost: DataSource.getBlogPost()
}
},
methods: {
handleChange() {
this.blogPost = DataSource.getBlogPost()
}
},
mounted() {
DataSource.addChangeListener(this.handleChange)
},
beforeDestroy() {
DataSource.removeChangeListener(this.handleChange)
}
}
</script>
const listeners = {};
const comments = ['comment one', 'comment two', 'comment three', 'comment four', 'comment five'];
const blogPosts = {
1: `Lorem ipsum dolor sit amet, utinam scripta splendide ei cum.
Mediocrem dissentiet est ut, nec tale ullum no, has putent scaevola mediocrem an.
Ex quot latine denique vim, ne quot quaeque sea.
In pri habeo diceret, an ius tale voluptatum, ad liber facilis minimum vis.
Eos iriure concludaturque id, sed inani nulla interesset in, labores adipiscing dissentiet vel ut.`,
2: 'Peek-A-Boo!'
}
setInterval(() => {
comments.push(`fresh comment ${comments.length + 1}`)
Object.keys(blogPosts).forEach(id => {
blogPosts[id] = `${blogPosts[id]} ${comments.length}`
})
}, 5000)
export default {
getComments() {
return comments;
},
getBlogPost(id) {
return blogPosts[id];
},
addChangeListener(listener) {
const intervalId = setInterval(() => {
listener()
}, 1000)
listeners[listener] = intervalId
},
removeChangeListener(listener) {
clearInterval(listeners[listener])
}
}
复制代码
- 从BlogPost、CommentsList组件可以看出它们结构非常相似,现作如下说明:
- 从DataSource中获取数据(如:DataSource.getComments()、DataSource.getBlogPost())
- 更新外部数据源中每次更新的数据(handleChange方法)
- 将更改侦听器添加到数据源(mounted方法)
- 从数据源中删除更改侦听器(beforeDestroy方法)
为了避免代码重复,可以将BlogPost和CommentsList之间的共享逻辑提取到高阶组件中,下面是实现步骤。
复制代码
- 高阶组件一步一步演化
- 此时,高阶组件不会做太多。它只需要一个组件并创建一个呈现传递组件的新组件。代码如下:
import Vue from 'vue'
import CommentsList from '~/components/CommentsList.vue'
const withSubscription = (component) => {
return Vue.component('withSubscription', {
render(createElement) {
return createElement(component)
}
}
}
const CommentsListWithSubscription = withSubscription(CommentsList)
复制代码
- 实现共享逻辑,添加mount,beforeDestroy钩子和handleChange方法。代码如下:
import DataSource from '../store/source'
import Vue from 'vue'
const withSubscription = (component) => {
return Vue.component('withSubscription', {
render(createElement) {
return createElement(component)
},
methods: {
handleChange() {
}
},
mounted() {
DataSource.addChangeListener(this.handleChange)
},
beforeDestroy() {
DataSource.removeChangeListener(this.handleChange)
}
})
}
export default withSubscription
复制代码
- 现在,高阶组件返回的新组件需要生命周期钩子。 handleChange方法保留为空。两个组件都具有handleChange方法,但是,此方法在每个组件中的实现略有不同
- 高阶组件可以接受多个参数。目前,withSubscription仅接受组件作为参数。为了在handleChange中调用自定义逻辑,需要第二个参数。第二个参数是应该在每次数据源更改时调用的方法。代码如下
复制代码
import DataSource from '../store/source'
import Vue from 'vue'
const withSubscription = (component, selectData) => {
return Vue.component('withSubscription', {
render(createElement, context) {
return createElement(component, {
props: {
content: this.fetchedData
}
})
},
data() {
return {
fetchedData: null
}
},
methods: {
handleChange() {
this.fetchedData = selectData(DataSource)
}
},
mounted() {
DataSource.addChangeListener(this.handleChange)
},
beforeDestroy() {
DataSource.removeChangeListener(this.handleChange)
}
})
}
export default withSubscription
复制代码
<template>
<div id="app">
<blog-post/>
<comments-list/>
</div>
</template>
<script>
import CommentsList from './components/CommentsList'
import BlogPost from './components/BlogPost'
import withSubscription from './hocs/withSubscription'
const BlogPostWithSubscription = withSubscription(BlogPost, (DataSource) => {
return DataSource.getBlogPost()
})
const CommentsListWithSubscription = withSubscription(CommentsList, (DataSource) => DataSource.getComments())
export default {
name: 'app',
components: {
'blog-post': BlogPostWithSubscription,
'comments-list': CommentsListWithSubscription
}
}
</script>
复制代码
- BlogPost and CommentsList代码如下
<template>
<div>
{{content}}
</div>
</template>
<script>
export default {
props: ['content']
}
</script>
----
<template>
<ul>
<li v-for="(comment, index) in content" :key="index">{{comment}}</li>
</ul>
</template>
<script>
export default {
name: 'comments-list',
props: ['content']
}
</script>
复制代码
- 到目前阶段,组件渲染是正常的,可以为自己打个call,毕竟取得了一定的成果,但是并不完美,需进一步完善。假设我需要将博客文章ID传递给BlogPost怎么办?或者如果我需要从BlogPost发布一个事件到App组件又该怎么办?
复制代码
<template>
<div id="app">
<blog-post :id="1"/>
</div>
</template>
<script>
import BlogPost from './components/BlogPost'
import withSubscription from './hocs/withSubscription'
const BlogPostWithSubscription = withSubscription(BlogPost, (DataSource, props) => {
return DataSource.getBlogPost(props.id)
})
export default {
name: 'app',
components: {
'blog-post': BlogPostWithSubscription
}
}
</script>
---
<template>
<div>
{{content}}
</div>
</template>
<script>
export default {
props: ['content', 'id']
}
</script>
复制代码
import DataSource from '../store/source'
import Vue from 'vue'
const withSubscription = (component, selectData) => {
const originalProps = component.props || [];
return Vue.component('withSubscription', {
render(createElement) {
return createElement(component, {
props: {
...originalProps,
content: this.fetchedData
}
})
},
props: [...originalProps],
data() {
return {
fetchedData: null
}
},
methods: {
handleChange() {
this.fetchedData = selectData(DataSource, this.$props)
}
},
mounted() {
DataSource.addChangeListener(this.handleChange)
},
beforeDestroy() {
DataSource.removeChangeListener(this.handleChange)
}
})
}
export default withSubscription
复制代码
- 从原组件BlogPost获取props保存到originalProps中,withSubscription高阶组件props接收originalProps的值,以便以后能够将它们传递给BlogPost组件
复制代码
<template>
<div id="app">
<blog-post :id="1" @click="onClick"/>
</div>
</template>
---
<template>
<div>
<button @click="$emit('click', 'aloha')">CLICK ME!</button>
{{data}}
</div>
</template>
<script>
export default {
props: ['data', 'id']
}
</script>
复制代码
- 要记住重要的一点,我们不直接在App中渲染BlogPost,有一个中间组件 - withSubscription HOC。 为了将事件监听器传递给渲染组件,我需要在高阶组件中添加一行代码。withSubscription代码作如下调整
return Vue.component('withSubscription', {
...
on: {...this.$listeners}
})
复制代码
参考链接