编程思想:react高阶组件 => vue 高阶组件
前言
最近学习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)。希望有一些别的观点相互碰撞。
作者:Tim
链接:https://juejin.cn/post/7021083557284020260