当前位置: 首页 > Web前端 > HTML5

你想看看这些有趣的函数方法吗?

时间:2023-04-05 10:56:16 HTML5

前言这是underscore.js源码解析的第六篇。如果您对本系列感兴趣,请点击underscore-analysis/watch,随时查看动态更新。underscore中有很多有趣的方法,可以用更巧妙的方式解决我们日常生活中遇到的问题,比如_.after、_.before、_.defer……等等。也许你用过它们,今天我们就深入源码,看看它们是如何实现的。指定调用次数(after,before)将这两个方法放在前面是因为它们至少可以解决我们工作中的以下两个问题如果要等待多个异步请求完成再去执行某个操作fn,那么就可以用_.after代替写多层异步回调地狱来满足要求。有些应用程序可能需要执行初始化操作,并且只需要一次初始化。判断一个变量,如果为true则认为已经初始化,直接返回,如果为false则进行参数初始化工作,初始化完成后将变量置为true,则下次进入时无需重复初始化。对于问题1letasync1=(cb)=>{setTimeout(()=>{console.log('异步任务1结束')cb()},1000)}letasync2=(cb)=>{setTimeout(()=>{console.log('异步任务2结束')cb()},2000)}letfn=()=>{console.log('两个任务都完成后我才开始任务')}如果要在task1和task2都结束后执行fn任务,我们一般的写法是什么?你可能会这样写async1(()=>{async2(fn)}),这确实可以保证前面两个异步任务完成后,任务fn才会执行,但我相信你不喜欢这样的方式写回调,这里只说了两个异步任务,如果再多了,恐怕会吃亏。不要受伤,带下划线的after函数会来救你。fn=_.after(2,fn)async1(fn)async2(fn)运行截图很爽,不用写回调地狱的形式了。那我们看看源码是如何实现的。源码实现后_.after=function(times,func){returnfunction(){//只有在返回函数被调用times次后才执行func操作if(--times<1){returnfunc.apply(this,参数);}};};源码简单得要命,但是神奇的是它妥善解决了我们的问题1。对于问题2letapp={init(name,sex){if(this.initialized){return}//初始化参数this.name=namethis.sex=sex//初始化完成,设置flagthis.initialized=true},showInfo(){console.log(this.name,this.sex)}}//传参初始化app.init('qianlonog','boy')app.init('xiaohuihui','girl')app.showInfo()//qianlonogboy注意这里打印出来的是为第一次一般都需要,只需要一个参数初始化,我们可以按上面的方法来做。但实际上,如果我们在下划线中使用before方法,还是可以做到这一点的。letapp={init:_.before(2,function(name,sex){//初始化参数this.name=namethis.sex=sex}),showInfo(){console.log(this.name,this.sex)}}//传参初始化应用app.init('qianlonog','boy')app.init('xiaohuihui','girl')app.showInfo()//qianlonogboy注意这里是什么打印出来的是第一次传入的参数。让我们看看_.before是如何实现的。//创建一个函数,这个函数的调用次数不超过次数//如果次数>=次,会记住上次调用函数的返回值,一直返回这个值_.before=function(次,func){var备忘录;returnfunction(){//返回的函数每次调用都会将times减1if(--times>0){//调用func并传入从外部传入的参数//需要注意的是后一次调用的返回值会覆盖前一次memo=func.apply(this,arguments);}//当调用次数足够时,func会被销毁并设置为nullif(times<=1)func=null;返回备忘录;};};让函数具有记忆的功能在程序中,我们经常需要进行一些计算操作。当遇到耗时操作时,如果有一种机制,对于相同的输入,一定要得到相同的输出,而对于相同的输入,后续的计算直接从缓存中读取,不再需要运行计算程序,很棒。例子letcalculate=(num,num2)=>{letresult=0letstart=Date.now()for(leti=0;i<10000000;i++){//这里只是模拟一个耗时操作result+=num}for(leti=0;i<10000000;i++){//这里只是模拟一个耗时操作result+=num2}letend=Date.now()console.log(end-start)returnresult}calculate(1,2)//30000000//log得到235calculate(1,2)//30000000//log得到249对于上面的calculate函数,同样输入1、2,输出两次调用的是一样的,而且两次耗时循环都走了两次,看一下underscore中的memoize函数,如何省去我们第二次的耗时操作,直接给出返回值300000letcalculate=_.memoize((num,num2)=>{letstart=Date.now()letresult=0for(leti=0;i<10000000;i++){//这里只是模拟耗时运算结果+=num}for(leti=0;i<10000000;i++){//这里只是模拟一个耗时操作result+=num2}letend=Date.now()console.log(end-start)returnresult},function(){return[].join.call(arguments,'@')//这里是为同一个输入指定唯一的缓存键})calculate(1,2)//30000000//log得到238calculate(1,2)//30000000//log什么都没有打印出来,因为源码实现_.memoize是直接从缓存中读取的=function(func,hasher){varmemoize=function(key){varcache=memoize.cache;//注意hasher,如果传递了hasher,会使用hasher()的执行结果来缓存func()执行的结果keyvaraddress=''+(hasher?hasher.apply(this,arguments):钥匙);//如果在缓存中没有找到对应的key,则计算一次并缓存起来if(!_.has(cache,address))cache[address]=func.apply(this,arguments);//返回结果returncache[address];};memoize.cache={};返回记忆;//返回一个带有缓存静态属性的函数};源码实现相信你已经看懂了,是不是很简单,也很实用有趣在原生延迟函数setTimeout的基础上,我们再来看下划线的延迟(_.delay和_.defer)。上面两个函数_.delay(function,wait,*arguments)是延迟函数执行的等待时间,函数需要的参数由*arguments提供使用示例varlog=_.bind(console.log,console)_.delay(log,1000,'helloqianlongo')//1秒后打印出helloqianlongo源码实现_.delay=function(func,wait){//从第三个参数开始读取其他参数varargs=slice.call(参数,2);returnsetTimeout(function(){//执行func并传入参数,注意当apply的第一个参数为null且undefined时,func里面的this指的是全局窗口或者全局returnfunc.apply(null,args);},等待);};但是有一些需要注意的是_.delay(function,wait,*arguments)`thisin`function指的是windoworglobal`_.defer(function,*arguments)延迟调用函数直到当前调用堆栈被清空,类似于使用延迟为0的setTimeout方法。对于在不阻塞UI线程的情况下执行昂贵的计算和HTML渲染很有用。如果传入arguments参数,函数执行时会将arguments作为参数传递给源码,实现_.defer=_.partial(_.delay,_,1);所以它主要取决于_.partial是否可以预先指定参数函数_.partial将一个函数部分地应用于任意数量的参数,而不改变它的动态this值。它与绑定方法非常相似。你可以在你的参数列表中传递_来指定一个不应该被预填充的参数(下划线中文网站翻译)使用示例letfn=(num1,num2,num3,num4)=>{letstr=`num1=${num1}`str+=`num2=${num2}`str+=`num3=${num3}`str+=`num4=${num4}`返回str}fn=_.partial(fn,1,_,3,_)fn(2,4)//num1=1num2=2num3=3num4=4可以看到,我们传入了_(这里指的是下划线本身)来占位,后面会讲到2和4后面填充到相应的位置。源码是如何实现的?_.partial=function(func){//获取回调函数以外的前置参数varboundArgs=slice.call(arguments,1);varbound=function(){varposition=0,length=boundArgs.length;//首先创建一个与boundArgs长度相同的空数组varargs=Array(length);//处理占位符元素_for(vari=0;i{returnstr+='-A'}letfuncB=(str)=>{returnstr+='-B'}letfuncC=(str)=>{returnstr+='-C'}funcC(funcB(funcA('hello')))//"hello-A-B-C"如何使用下划线中的compose方法?letfn=_.compose(funcC,funcB,funcA)fn('hello')//"hello-A-B-C"看起来不正常这样,层被卷入,但以非常平坦的方式使用。同样,我们看看源码是如何实现的。_.compose源代码_.compose=function(){varargs=arguments;//从最后一个参数开始处理varstart=args.length-1;返回函数(){vari=开始;//执行最后一个函数,并得到结果resultvarresult=args[start].apply(this,arguments);//从后往前一个一个调用传递过来的函数,将上次执行的结果作为参数传递给下一个函数while(i--)result=args[i].call(this,result);//最后导出结果返回结果;};};Bindmultiplefunctionstosamecontext(_.bindAll(object,*methodNames))BindmultiplefunctionmethodNamestothecontextofobject???,好困,写一篇文章真的很费时间和精力,一直写到这里将近3个小时,深夜,我好像躺下睡觉了!!!啊啊啊,稍等一下,我说的差不多了(希望不要误导弟子们)。varbuttonView={label:'underscore',onClick:function(){alert('clicked:'+this.label);},onHover:function(){console.log('hovering:'+this.label);}};_.bindAll(buttonView,'onClick','onHover');$('#underscore_button').bind('click',buttonView.onClick);我们用官网给出的例子来说,在jQuery默认的$(selector).on(eventName,callback)回调中的this指的是当前元素本身。经过上面的处理,会弹出下划线。_.bindAll源码实现_.bindAll=function(obj){vari,length=arguments.length,key;//必须指定需要绑定到obj的函数参数if(length<=1)thrownewError('bindAllmustbepassedfunctionnames');//从第一个实际参数开始处理,这些是需要将此作用域绑定到obj的函数for(i=1;i{returntrue}_.negate(fn)()//false好像没什么用,但是。...letarr=[1,2,3,4,5,6]letfindEven=(num)=>{returnnum%2===0}arr.filter(findEven)//[2,4,6]如果找奇数呢?letarr=[1,2,3,4,5,6]letfindEven=(num)=>{returnnum%2===0}arr.filter(_.negate(findEven))//[1,3,5]源代码实现_.negate=function(predicate){returnfunction(){return!predicate.apply(this,arguments);};};源码很简单,就是执行你传入的predicate函数的结果Negateit,但是应用还是挺多的。最后就是underscore库中函数相关的API了。大部分已经完成了,如果对你有帮助的话。点个小星星???点个小星星???点个小星星???晚安?