前端进阶之认识与手写compose方法

前言:为什么要学习这个方法
遇到这个方法主要是最近在阅读redux,koa 原理 等多次遇到这个方法,为了更好地理解框架原理,于是深入学习了一下compose的实现。
然后也发现这属于函数式编程的东西,发现函数式编程是进击前端进阶的必经之路,因为像其中的纯函数的概念在redux的reducer中也展示得淋漓尽致,而保留函数计算结果的思想无论是在vue,还是react等其他框架也多处见到。
所以建议有时间可以去看下函数试编程。
接下来,就让我们学习下其中的compose函数吧!
compose简介
compose就是执行一系列的任务(函数),比如有以下任务队列
let tasks = [step1, step2, step3, step4]
每一个step都是一个步骤,按照步骤一步一步的执行到结尾,这就是一个compose
compose在函数式编程中是一个很重要的工具函数,在这里实现的compose有三点说明
- 第一个函数是多元的(接受多个参数),后面的函数都是单元的(接受一个参数)
- 执行顺序的自右向左的
- 所有函数的执行都是同步的
还是用一个例子来说,比如有以下几个函数
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
这几个函数组成一个任务队列
steps = [step4, step3, step2, init]
使用compose组合这个队列并执行
let composeFunc = compose(...steps)
console.log(composeFunc(1, 2, 3))
执行过程
6 -> 6 + 2 = 8 -> 8 + 3 = 11 -> 11 + 4 = 15
所以流程就是从init自右到左依次执行,下一个任务的参数是上一个任务的返回结果,并且任务都是同步的,这样就能保证任务可以按照有序的方向和有序的时间执行。
compose的实现
好了,我们现在已经知道compose是什么东西了,现在就来实现它吧!
最容易理解的实现方式
思路就是使用递归的过程思想,不断的检测队列中是否还有任务,如果有任务就执行,并把执行结果往后传递,这里是一个局部的思维,无法预知任务何时结束。直观上最容易理解。
const compose = function(...funcs) {
let length = funcs.length
let count = length - 1
let result
return function f1 (...arg1) {
result = funcs[count].apply(this, arg1)
if (count <= 0) {
count = length - 1
return result
}
count--
return f1.call(null, result)
}
}
删繁就简来看下,去掉args1参数
const compose = function(...funcs) {
let length = funcs.length
let count = length - 1
let result
return function f1 () {
result = funcs[count]()
if (count <= 0) {
count = length - 1
return result
}
count--
return f1(result)
}
}
这就好看很多,我们假设有三个方法,aa,bb,cc
function aa() {
console.log(11);
}
function bb() {
console.log(22);
}
function cc() {
console.log(33);
return 33
}
然后传入compose
compose(aa,bb,cc)
此时count = 2,则下面其实是执行cc
result = funcs[count]()
然后count--。再递归执行f1,则下面其实就是执行bb
result = funcs[count]()
这样,就实现了 从funcs数组里从右往左依次拿方法出来调用,再把返回值传递给下一个。
后面的步骤同理。
这其实是一种面向过程的思想
手写javascript中reduce方法
为什么要手写?其实你要是能够很熟练的使用reduce,我觉得不必手写reduce,只是我觉得熟悉一下reduce内部的实现可以更好地理解后面的内容,况且 也不会太难呀!
function reduce(arr, cb, initialValue){
var num = initValue == undefined? num = arr[0]: initValue;
var i = initValue == undefined? 1: 0
for (i; i< arr.length; i++){
num = cb(num,arr[i],i)
}'
return num
}
如代码所示,就是先判断有没有传入初始值,有的话,下面的循环直接 从i = 0开始,否则i=1开始。
如果没有传入初始值,num就取 数组的第一个元素。这也是说明了为什么传入初始值,i就=1,因为第一个都被取出来了,就不能再取一次啦啦啦!
下面使用我们写的reduce方法
function fn(result, currentValue, index){
return result + currentValue
}
var arr = [2,3,4,5]
var b = reduce(arr, fn,10)
var c = reduce(arr, fn)
console.log(b) // 24
好了 ,没毛病,既然我们了解了reduce原理,就看看下面的redux中compose的实现吧
redux中compose的实现
function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
}
if (funcs.length === 1) {
return funcs[0]
}
debugger
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
很简短,非常的巧妙,但是不是很不好理解。不过没关系。
依旧通过例子来讲解。
function aa() {
console.log(11);
}
function bb() {
console.log(22);
}
function cc() {
console.log(33);
}
假设只有这三个方法,我们怎样才能先执行cc再执行bb,再执行aa呢?
没错,可以直接写
aa(bb(cc()))
就是这样,非常巧妙,不仅完成了执行顺序,还实现了前一个方法执行返回的结果传递给了下一个即将要执行的方法。
而下面这段代码所做的就是将funcs数组[aa,bb,cc],转化成aa(bb(cc()))
funcs.reduce((a, b) => (...args) => a(b(...args)))
怎么办到的?
看看下面的解释:
reduce内部第一次执行返回的结果是 一个方法
(...args) => aa(bb(...args))
我们现在把这个方法简化成dd,即
dd = (...args) => aa(bb(...args))
reduce内部第二次执行的时候,此时的a 是 上一次返回的dd方法,b是cc
所以执行结果是
(...args) => dd(cc(...args))
而dd(cc(...args))不就是先执行cc再执行dd吗?而dd就是执行bb再执行aa。
我的天,!这不是俄罗斯套娃吗!没错 redux中的compose的实现原理就是套娃哈哈哈!
参考文章
https://segmentfault.com/a/1190000011447164
最后
文章首发于公众号《前端阳光》,欢迎加入技术交流群。
前端进阶之认识与手写compose方法的更多相关文章
- 2019前端面试系列——JS高频手写代码题
实现 new 方法 /* * 1.创建一个空对象 * 2.链接到原型 * 3.绑定this值 * 4.返回新对象 */ // 第一种实现 function createNew() { let obj ...
- 手写redux方法以及数组reduce方法
reduce能做什么? 1)求和 2)计算价格 3)合并数据 4)redux的compose方法 这篇文章主要内容是什么? 1)介绍reduce的主要作用 2)手写实现reduce方法 0)了解red ...
- UI进阶之--网易彩票手写plist文件,动态创建控制器与tableViewcell
点击右上角设置按钮 点击按钮后发生的事件:1. 控制器的跳转,进入新的控制器.view, 2. 跳转的时候对将要跳转的目标控制的子控件进行了布局.---通过手写plist文件的方式加载 为按钮注册单击 ...
- JavaScript手写new方法
1.看一下正常使用的new方法 function father(name){ this.name=name; this.sayname=function(){ console.log(this.nam ...
- 推荐使用并手写实现redux-actions原理
@ 目录 一.前言 二.介绍 2.1 创建action 2.2 reducer 2.3 触发action 三. 认识与手写createAction() 3.1 用法 3.2 原理实现 四.认识hand ...
- 手写系列-实现一个铂金段位的 React
一.前言 本文基于 https://pomb.us/build-your-own-react/ 实现简单版 React. 本文学习思路来自 卡颂-b站-React源码,你在第几层. 模拟的版本为 Re ...
- 如何手写一个js工具库?同时发布到npm上
自从工作以来,写项目的时候经常需要手写一些方法和引入一些js库 JS基础又十分重要,于是就萌生出自己创建一个JS工具库并发布到npm上的想法 于是就创建了一个名为learnjts的项目,在空余时间也写 ...
- 前端面试手写代码——call、apply、bind
1 call.apply.bind 用法及对比 1.1 Function.prototype 三者都是Function原型上的方法,所有函数都能调用它们 Function.prototype.call ...
- 前端面试手写代码——模拟实现new运算符
目录 1 new 运算符简介 2 new 究竟干了什么事 3 模拟实现 new 运算符 4 补充 预备知识: 了解原型和原型链 了解this绑定 1 new 运算符简介 MDN文档:new 运算符创建 ...
随机推荐
- HotSpot的启动过程(配视频进行源码分析)
本文将详细介绍HotSpot的启动过程,启动过程涉及到的逻辑比较复杂,细节也比较多,为了让大家更快的了解这部分知识,我录制了对应的视频放到了B站上,大家可以参考. 第4节-HotSpot的启动过程 下 ...
- C#高级编程之反射
反射的定义 MSDN定义:反射提供描述程序集.模块和类型的对象(Type类型). 可以使用反射动态地创建类型的实例,将类型绑定到现有对象,或从现有对象中获取类型,然后调用其方法或访问器字段和属性. 如 ...
- 一些 git 常用的命令
1.本地命令 查看状态 -git status 添加文件 -git add . 提交文件 -git commit -m "(comment)" 查看历史key -git reflo ...
- 精尽MyBatis源码分析 - MyBatis初始化(二)之加载Mapper接口与XML映射文件
该系列文档是本人在学习 Mybatis 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释(Mybatis源码分析 GitHub 地址.Mybatis-Spring 源码分析 GitHub ...
- NLP之统计句法分析(PCFG+CYK算法)
一.认识句法分析 首先,了解一下句法分析到底是什么意思?是做什么事情呢?顾名思义,感觉是学习英语时候讲的各种句法语法.没错!这里就是把句法分析过程交给计算机处理,让它分析一个句子的句法组成,然后更好理 ...
- Wasp XT合成器功能介绍
本章节将采用图文结合的方式给大家讲解电音编曲软件FL Studio中的Wasp XT合成器的相关功能,感兴趣的朋友可以一起来交流哦. 下面我们一起来看看吧 Wasp XT是一个3振荡器合成器,它包含一 ...
- 左右声道音频怎么制作,用Vegas就对啦
一款优秀的视频剪辑软件,不仅有高水平的视频制作功能,它的音频编辑功能也是必不可少的.Vegas就是这么一款软件,同时具备视频制作特效制作的同时,还能帮助制作轨道音频效果. 下面,就让小编带大家去学习, ...
- web自动化 下拉框、切换到新窗口
一.下拉框 相信大家在手动测试web页面时,遇到过下拉框吧,那进行web自动化测试时,如何操作下拉框,且看下文 1.selenium中提供了方法,先导入Select方法 from selenium.w ...
- python+selenium通过加载用户配置实现免登陆
1查看profile路径 在Chrome地址栏访问chrome://version,可以查看个人资料存储位置: 2 python代码如下: from selenium import webdriver ...
- 使用Eclipse创建Maven的JSP项目
使用Eclipse创建Maven的JSP项目 MyEclipse2015根本不行,试过各种解决方案都无济于事. 创建Maven项目 此时项目上有错,pom.xml有错. 修改Java版本 生成web. ...