阅读 108

编程思想: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(原始版)

具体到上面这个例子中,我们加入一些请求状态帮助理解,我们的思路是这样的,


    1. 高阶组件接受 木偶组件 和 请求的方法 作为参数


    1. 在 mounted 生命周期中请求到数据


    1. 把请求的数据通过 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;     },   }; }; 复制代码

在参数中:

  1. wrapped 也就是需要被包裹的组件对象。

  2. 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


文章分类
后端
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐