前言
最近学习react 过程中,接触到在 React + Redux 结合作为前端框架的时候,提出了一个将组件分为智能组件
和木偶组件
两种。由于平时开发使用vue频率比较高,发现这个思想可以借鉴到vue中。
智能组件 & 木偶组件
智能组件
:它是数据的所有者,它拥有数据、且拥有操作数据的action,但是它不实现任何具体功能。它会将数据和操作action传递给子组件,让子组件来完成UI或者功能。这就是智能组件,也就是项目中的各个页面。"
木偶组件
:它就是一个工具,不拥有任何数据、及操作数据的action,给它什么数据它就显示什么数据,给它什么方法,它就调用什么方法,比较傻,就像一个被线操控的木偶。
一般来说,它们的结构关系是这样的:
<智能组件>
<木偶组件 />
</智能组件>
或
<容器组件>
<ui组件 />
</容器组件>
它们还有另一个别名,就是 容器组件
和 ui组件
,是不是很形象。
什么是高阶组件?
在react中
在 React 里,组件是 Class
,所以高阶组件有时候会用 装饰器
语法来实现,因为 装饰器
的本质也是接受一个 Class
返回一个新的 Class
。
在 React 的世界里,高阶组件就是 f(Class) -> 新的Class
。
我们开发一个评论列表的react组件大概是这样:
// CommentList.js
class CommentList extends React.Component {
constructor() {
super();
this.state = { comments: [] };
}
componentDidMount() {
$.ajax({
url: "/my-comments.json",
dataType: "json",
success: function (comments) {
this.setState({ comments: comments });
}.bind(this),
});
}
render() {
return <ul> {this.state.comments.map(renderComment)} </ul>;
}
renderComment({ body, author }) {
return (
<li>
{body}—{author}
</li>
);
}
}
拆分过后:
// CommentListContainer.js
class CommentListContainer extends React.Component {
constructor() {
super();
this.state = { comments: [] };
}
componentDidMount() {
$.ajax({
url: "/my-comments.json",
dataType: "json",
success: function (comments) {
this.setState({ comments: comments });
}.bind(this),
});
}
render() {
return <CommentList comments={this.state.comments} />;
}
}
// CommentList.js
class CommentList extends React.Component {
constructor(props) {
super(props);
}
render() {
return <ul> {this.props.comments.map(renderComment)} </ul>;
}
renderComment({ body, author }) {
return (
<li>
{body}—{author}
</li>
);
}
}
这样就做到了数据提取和渲染分离,CommentList可以复用,CommentList可以设置props判断数据的可用性。
优势:
- 展示组件和容器组件更好的分离,更好的理解应用程序和UI重用性高,
- 展示组件可以用于多个不同的state数据源,也可以变更展示组件。
在vue中
在 Vue 的世界里,组件是一个对象,所以高阶组件就是一个函数接受一个对象,返回一个新的包装好的对象。
类比到 Vue 的世界里,高阶组件就是 f(object) -> 新的object
Vue高阶组件 1.0(原始版)
具体到上面这个例子中,我们加入一些请求状态帮助理解,我们的思路是这样的,
-
- 高阶组件接受
木偶组件
和请求的方法
作为参数
- 高阶组件接受
-
- 在
mounted
生命周期中请求到数据
- 在
-
- 把请求的数据通过
props
传递给木偶组件
。
- 把请求的数据通过
const withPromise = (wrapped, promiseFn) => {
return {
name: "with-promise",
data() {
return {
loading: false,
error: false,
result: null,
};
},
async mounted() {
this.loading = true;
const result = await promiseFn().finally(() => {
this.loading = false;
});
this.result = result;
},
};
};
在参数中:
wrapped
也就是需要被包裹的组件对象。promiseFn
也就是请求对应的函数,需要返回一个 Promise
再加入 render
函数中,我们把传入的 wrapped
也就是木偶组件给包裹起来。这样就形成了 智能组件获取数据
-> 木偶组件消费数据
,这样的数据流动了。
// 智能组件 1.0
const withPromise = (wrapped, promiseFn) => {
return {
name: 'with-promise',
data() {
return {
loading: false,
error: false,
result: null
}
},
async mounted() {
this.loading = true
const result = await promiseFn().finally(() => {
this.loading = false
})
this.result = result
},
render(h) {
const args = {
props: {
result: this.result,
loading: this.loading,
error: this.error
}
}
const wrapper = h('div', [
h(wrapped, args),
this.loading ? h('span', ['加载中……']) : null,
this.error ? h('span', ['加载错误']) : null
])
return wrapper
}
}
}
// 木偶组件1.0
const view = {
props: ["result","loading","error"],
template: `
<ul v-if="result">
<li v-for="item in result">
{{ item }}
</li>
</ul>
`
};
具体使用生成一个HOC
组件
// HOC 组件 1.0
// 这里先用promise 模拟一个请求数据过程
const getListData = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(['foo','bar','Too']);
}, 1000);
});
};
export default withPromise(view, getListData)
1.0 示例,来个demo 验证一下:
import Vue from 'vue'
const withPromise = (wrapped, promiseFn) => {
return {
name: 'with-promise',
data() {
return {
loading: false,
error: false,
result: null
}
},
async mounted() {
this.loading = true
const result = await promiseFn().finally(() => {
this.loading = false
})
this.result = result
},
render(h) {
const args = {
props: {
result: this.result,
loading: this.loading,
error: this.error
}
}
const wrapper = h('div', [
h(wrapped, args),
this.loading ? h('span', ['加载中……']) : null,
this.error ? h('span', ['加载错误']) : null
])
return wrapper
}
}
}
const view = {
props: ['result', 'loading', 'error'],
template: `
<ul v-if="result">
<li v-for="item in result">
{{ item }}
</li>
</ul>
`
}
// 这里先用promise 模拟一个请求数据过程
const getListData = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(['foo', 'bar', 'baz'])
}, 1000)
})
}
var hoc = withPromise(view, getListData)
new Vue({
el: '#app',
template: `<hoc></hoc>`,
components: {
hoc
}
})
Vue高阶组件 2.0(进阶版)
1.0 只是初步雏形,实际开发过程中,接口是参数频繁变动的,我们希望得到改进几个地方
- 由于
逻辑
与视图
是分离的,我们需要考虑如何解决从视图层通知逻辑层更新数据 - 额外的
props
或者attrs
listener
甚至是 插槽slot
给最内层的木偶组件
第一个问题,我们只需要在智能组件
上使用ref访问到 木偶组件
监听木偶组件参数变化,回调中更新数据即可。
第二个问题,我们只要在渲染子组件的时候把 $attrs
、$listeners
、$scopedSlots
传递下去即可
// 智能组件2.0
const withPromise = (wrapped, promiseFn) => {
return {
name: 'with-promise',
data() {
return {
loading: false,
error: false,
result: null
}
},
async mounted() {
this.loading = true
const result = await promiseFn().finally(() => {
this.loading = false
})
this.result = result
},
render(h) {
const args = {
props: {
// 混入 $attrs
...this.$attrs,
result: this.result,
loading: this.loading
},
// 传递事件
on: this.$listeners,
// 传递 $scopedSlots
scopedSlots: this.$scopedSlots,
ref: 'wrapped'
}
const wrapper = h('div', [
h(wrapped, args),
this.loading ? h('span', ['加载中……']) : null,
this.error ? h('span', ['加载错误']) : null
])
return wrapper
}
}
}
2.0 示例
import Vue from 'vue'
// 这里先用promise 模拟一个请求数据过程
const api = {
getCommenList: (params) => {
// 打印请求参数
console.log(params)
return new Promise((resolve) => {
setTimeout(() => {
resolve(['foo', 'bar', 'baz'])
}, 1000)
})
},
getUserList: (params) => {
// 打印请求参数
console.log(params)
return new Promise((resolve) => {
setTimeout(() => {
resolve(['Tim', 'Alia', 'Jessia'])
}, 1000)
})
}
}
const axios = (url, params) => {
return api[url](params)
}
const withPromise = (wrapped, promiseFn) => {
return {
name: 'with-promise',
data() {
return {
loading: false,
error: false,
result: null
}
},
methods: {
async request() {
this.loading = true
const { requestApi, requestParams } = this.$refs.wrapped
const result = await promiseFn(requestApi, requestParams).finally(
() => {
this.loading = false
}
)
this.result = result
}
},
mounted() {
// 立刻发送请求,并且监听参数变化重新请求
this.$refs.wrapped.$watch('requestParams', this.request.bind(this), {
immediate: true
})
},
render(h) {
const args = {
props: {
// 混入 $attrs
...this.$attrs,
result: this.result,
loading: this.loading
},
// 传递事件
on: this.$listeners,
// 传递 $scopedSlots
scopedSlots: this.$scopedSlots,
ref: 'wrapped'
}
const wrapper = h('div', [
this.loading ? h('span', ['加载中……']) : null,
this.error ? h('span', ['加载错误']) : null,
h(wrapped, args)
])
return wrapper
}
}
}
const view1 = {
props: ['result', 'loading', 'error'],
template: `
<div>
<ul v-if="result && !loading">
<li v-for="item in result">
{{ item }}
</li>
</ul>
<button @click="addComment">新增一条评论</button>
<slot></slot>
<slot name="named"></slot>
</div>
`,
data() {
return {
requestApi: 'getCommenList',
requestParams: {
page: 1,
pageSize: 10
}
}
},
methods: {
addComment() {
this.$emit('change', '新增了一条评论')
}
}
}
const view2 = {
props: ['result', 'loading', 'error'],
template: `
<div>
<ul v-if="result && !loading">
<li v-for="item in result">
{{ item }}
</li>
</ul>
<button @click="reload">重新加载数据</button>
</div>
`,
methods: {
reload() {
this.requestParams = {
page: this.requestParams.page++
}
}
},
data() {
return {
requestApi: 'getUserList',
requestParams: {
page: 1,
pageSize: 10
}
}
}
}
var hoc1 = withPromise(view1, axios)
var hoc2 = withPromise(view2, axios)
new Vue({
el: '#app',
template: `
<div>
评论列表:
<hoc1 @change="onchange">
<template>
<div>这是一个插槽</div>
</template>
<template v-slot:named>
<div>这是一个具名插槽</div>
</template>
</hoc1>
用户列表:
<hoc2></hoc2>
</div>
`,
components: {
hoc1,
hoc2
},
methods: {
onchange(msg) {
alert(msg)
}
}
})
Vue高阶组件 3.0(最终版)
突然有一天我想改造这个组件(天气冷,多穿几件衣服)。组件嵌套
<容器组件A>
<容器组件B>
<容器组件C>
<UI组件></UI组件>
</容器组件C>
</容器组件B>
</容器组件A>
于是,再包在刚刚的 hoc
之外,这里我们不考虑使用jsx语法:
function normalizeProps(vm) {
return {
on: vm.$listeners,
attr: vm.$attrs,
// 传递 $scopedSlots
scopedSlots: vm.$scopedSlots,
}
}
var withA = (wrapped) => {
return {
mounted() {
console.log('I am withA!')
},
render(h) {
return h(wrapped, normalizeProps(this))
}
}
}
var withB = (wrapped) => {
return {
mounted() {
console.log('I am withB!')
},
render(h) {
return h(wrapped, normalizeProps(this))
}
}
}
// 这里 withC = withPromise(view, axios)
var hoc = withA(withB(withPromise(view, axios)));
或者
var hoc = withB(withA(withPromise(view, axios)));
这样的循环嵌套确实让人头疼,不由让我们想到函数式编程的composed,我们如何改造下?
什么是组合(composed)?
compose
在函数式编程中是一个很重要的工具函数,在这里实现的compose
有三点说明
特点:
- 第一个函数是多元的(接受多个参数),后面的函数都是单元的(接受一个参数)
- 执行顺序的自右向左的
- 所有函数的执行都是同步的,当然异步也能处理,这里暂时不考虑
// ====== compose ======
// 借助 compose 函数对连续的异步过程进行组装,不同的组合方式实现不同的业务流程
// 我们希望使用:compose(f1,f2,f3,init)(...args) ==> 实际执行:init(f1(f2(f3.apply(this,args))))
// ===== 组合同步操作 ==== start
const _pipe = (f, g) => {
return (...arg) => {
return g.call(this, f.apply(this, arg))
}
}
const compose = (...fns) => fns.reverse().reduce(_pipe, fns.shift())
var f1 = function(){
console.log(1)
}
var f2 = function(){
console.log(2)
}
var f3 = function(){
console.log(3)
}
// 随机组合
console.log('f1,f2,f3:')
compose(...[f3, f2, f1])()
console.log('f3,f2,f1:')
compose(...[f1, f2, f3])()
// ===== 组合同步操作 ==== end
再来个例子加深理解
let init = (...args) => args.reduce((ele1, ele2) => ele1 + ele2, 0)
let step2 = (val) => val + 2
let step3 = (val) => val + 3
let step4 = (val) => val + 4
let composeFunc = compose(...steps)
console.log(composeFunc(1, 2, 3))
改造后 3.0
// 大部分代不变 只是hoc 组件做了调整
const withPromise = (promiseFn) => {
// 返回的这一层函数,就符合我们的要求,只接受一个参数
return function (wrapped) {
return {
mounted() {
...
},
render() {
...
},
}
}
}
// 以后我们使用的时候,就能用过composed更优雅的实现了
const composed = compose(withA,
withB,
withPromise(request)
)
const hoc = composed(view1)
结语
这是目前我对组件编程一些思考,以前编写Vue业务组件的缺乏这方面思考(Mark)。希望有一些别的观点相互碰撞。