系统学习大前端(1)---函数式编程、异步编程

    技术2022-07-10  110

    文章内容输出来源:拉勾教育大前端高薪训练营

    1、函数式编程

    学习函数式编程的目的 函数式编程随react流行受到更多关注vue3 开始拥抱函数式编程函数式编程可以抛弃this打包过程中可以更好的利用tree shaking过滤无用代码方便测试、方便并行处理很多库可以帮助我们进行函数式开发,如 lodash、 underscore 、ramda

    1.1 函数式编程概念

    函数式编程(Function Programming,FP)是编程范式之一。常说的编程范式还有面向过程编程、面向对象编程。

    面向对象的编程:把现实世界中的事物抽象成程序世界中的类和对象,通过封装、继承和多态来演示事物之间的联系。函数式编程:把现实世界的事物和事物之间的联系抽象到程序世界(对运算过程进行抽象) 程序的本质:根据输入通过某种运算获得相应的输出,程序开发过程中会涉及很多有输入和输出的函数。x–>f(联系、映射)–>y 即可表示为 y=f(x)函数式编程中的函数指的不是程序中的函数(方法),而是数学中的函数即映射关系,例如 : y=sin(x) x和y 的关系相同的输入始终要得到相同的输出(纯函数的概念)函数式编程用来描述数据(函数)之间的映射 // 非函数式 let a = 1; let b = 2; let sum = a+b; console.log(sum); // 函数式 function add(a,b){ return a+b } let sum1 = add(1,2) console.log(sum1);

    1.2 函数是一等公民

    体现在:mdn 头等函数

    函数可以存储在变量中函数可以作为参数函数可以作为返回值 在JS中函数就是一个普通的对象(new Function()) 。我们可以把函数存在变量、数组中,还可以作为另一个函数的参数和返回值。可以在程序运行时通过new Function()来构造一个新的函数。 let fn = function(){ console.log('First-class Function'); } fn(); // 一个示例 const BlogController = { index(posts){return Views.index(posts)}, show(post){return Views.show(post)}, create(attrs){return Db.create(attrs)}, update(post,attrs){return Db.update(post,attrs)}, destroy(post){return Db.destroy(post)}, } // 优化 const BlogController = { index:Views.index, show: Views.show, create: Db.create, update: Db.update, destroy:Db.destroy }

    1.3 高阶函数

    高阶函数 可以把函数作为参数传递给另一个函数可以把函数作为另一个函数的返回结果 // 高阶函数 函数作为参数 function forEach(arr,fn) { for (let i = 0; i < arr.length; i++) { fn(arr[i]) } } // 函数作为返回值 function makeFn(){ let msg = 'hello' return function(){ console.log(msg); } } 高阶函数的意义 抽象可以帮我们屏蔽细节,只需关注我们的目标高阶函数用来抽象通用的问题 常用的高阶函数 代码 forEachmapfiltereverysomefind/findIndexreducesort… // 模拟常用高阶函数 // map const map = (arr,fn)=>{ let res =[]; for (let i = 0; i < arr.length; i++) { res.push(fn(arr[i])) } return res; } // let arr = [1,2,3,4] // let res = map(arr,v=>v*v) // console.log(res); //every const every = (array,fn)=>{ let res = true; for (let i = 0; i < array.length; i++) { res = fn(array[i]) if(!res) break } return res } // let arr = [3,4,5] // let res = every(arr,v=>v>2) // console.log(res); // some const some = (array,fn)=>{ let res = false; for (const value of array) { res = fn(value) if(res) break } return res; } let arr = [1,4,5,7,9] let res = some(arr,v=>v%2===0) console.log(res);

    1.4 闭包

    Closure:函数和其周围的状态(词法环境)的引用捆绑到一起形成闭包。 可以在另一个作用域中调用一个函数的内部函数并访问到该函数的作用域中的成员 function once(fn) { let done = false; return function(){ if(!done){ done = true; return fn.apply(this,arguments) } } } let pay = once(function(money){ console.log(`支付${money}RMB`); }) pay(18) pay(18) pay(18) pay(18) 闭包的本质:函数在执行的时候会放到一个执行栈上,当函数执行完后从执行栈上移除,但是堆上的作用域成员因为被外部引用而不能释放,因此内部函数依然可以访问外部函数的成员。 function makePower(power) { return function(number){ return Math.pow(number,power) } } let power2 = makePower(2);//二次方 let power3 = makePower(3); // console.log(power2(4)); // console.log(power2(5)); // console.log(power3(4)); function makeSalary(base) { return function(performance){ return base + performance; } } let salaryLevel1 = makeSalary(12000); let salaryLevel2 = makeSalary(15000); console.log(salaryLevel1(2000)); console.log(salaryLevel2(3000));

    1.5 纯函数

    纯函数:相同的输入永远得到相同的输出,而且没有任何可观察的副作用。 纯函数就类似数学中的函数(用来描述输入和输出之间的关系) y=f(x)

    lodash 是一个一致性、模块化、高性能的JS实用工具库(lodash的fp模块提供了对函数式编程友好的方法),提供了对数组、数字、对象、函数等操作的一些方法。 // 演示lodash // first / last / toUpper / reverse / each / includes / find / findIndex const _ = require('lodash') const array = ['jack','tom','kate','lucy'] // console.log(_.first(array)); // console.log(_.last(array)); // console.log(_.toUpper(_.first(array))); // console.log(_.reverse(array)); const r = _.each(array,(item,index)=>{ console.log(item,index); }) console.log(r); 数组的slice和splice分别是纯函数和不纯的函数。 slice返会数组中指定部分,不改变原数组splice对数组进行操作返回该数组,改变原数组 // 纯函数和不纯的函数 // slice splice let arr = [1,2,3,4,5,6,7] // 纯函数 console.log(arr.slice(0,3));//[ 1, 2, 3 ] console.log(arr.slice(0,3));//[ 1, 2, 3 ] console.log(arr.slice(0,3));//[ 1, 2, 3 ] // 不纯的函数 console.log(arr.splice(0,3));//[ 1, 2, 3 ] console.log(arr.splice(0,3));//[ 4, 5, 6 ] console.log(arr.splice(0,3));//[ 7 ] // 纯函数 function getSum(a,b){ return a+b } console.log(getSum(1,2)); console.log(getSum(1,2)); console.log(getSum(1,2)); 函数式编程不会保留计算中间的结果,所以变量是不可变的(无状态)我们可以把一个函数的执行结果交给另一个函数去处理

    纯函数的好处

    可缓存 因为纯函数对相同的输入失踪有相同的输出,所以可以把纯函数的结果保存起来模拟实现memoize函数 const _ = require('lodash') function getArea(r){ console.log(r); return Math.PI*r*r } // let getAreaWithMemory = _.memoize(getArea); // console.log(getAreaWithMemory(4)); // console.log(getAreaWithMemory(4)); // console.log(getAreaWithMemory(4)); // 模拟实现memoize function memoize(f){ const cache = {} return function(){ let key = JSON.stringify(arguments); cache[key] = cache[key] || f.apply(f,arguments) return cache[key] } } let getAreaWithMemory = memoize(getArea); console.log(getAreaWithMemory(4)); console.log(getAreaWithMemory(4)); console.log(getAreaWithMemory(4)); 可测试 纯函数让测试更方便 并行处理 在多线程环境下并行操作共享的内存数据很可能会出现意外的情况纯函数不需要访问共享的内存数据,所以在并行环境下可以任意运行纯函数(Web Worker)

    1.6 副作用

    纯函数:对于相同的输入永远会得到相同的输出,而且没有任何可观察的副作用。 副作用让一个函数变得不纯,纯函数的根据相同的输入返回相同的输出,如果函数依赖于外部的状态就无法保证输出相同,就会带来副作用。 // 不纯的 let min = 18; function checkAge(age){ return age>= min } // 纯的 (有硬编码) function checkAge2(age){ let min = 18; return age>=min } 副作用来源: 配置文件数据库获取用户的输入等

    所有的外部交互都有可能带来副作用,副作用也使得方法通用性下降不适合扩展和可重用性,同时副作用会给程序中带来安全隐患,但是副作用不可能完全禁止,尽可能控制他们在可控范围内发生。

    1.7 柯里化(Haskell Brooks Curry)

    使用柯里化解决上一个案例中硬编码的问题 // function checkAge(age){ // let min = 18; // return age >= min; // } // 普通的纯函数 // function checkAge(min,age){ // return age >= min // } // console.log( // checkAge(18,24), // checkAge(18,20), // checkAge(20,30) // ); // function checkAge(min) { // return function (age) { // return age >= min // } // } const checkAge = min => (age => age >= min) let checkAge18 = checkAge(18) let checkAge20 = checkAge(20) console.log(checkAge18(20)); console.log(checkAge20(24)); 柯里化(currying) 当一个函数有多个参数的时候先传递一部分参数调用它(这部分参数以后永远不变)然后返回一个新的函数接受剩余的参数,返回结果。

    1.8 lodash中的柯里化函数

    _.curry(func) 功能 :创建一个函数,该函数接受一个或多个func的参数,如果func所需要的参数都被提供则执行func并返回执行的结果,否则继续返回该函数并等待接受剩余的参数。参数: 需要柯里化的函数返回值: 柯里化后的函数 // lodash中curry的使用 const _ = require('lodash') function getSum(a,b,c){ return a+b+c; } const curried = _.curry(getSum) console.log(curried(1,2,3)); console.log(curried(1)(2,3)); console.log(curried(1,2)(3)); // 柯里化案例 const match = _.curry(function(reg,str){ return str.match(reg) }) const haveSpace = match(/\s+/g) const haveNumber = match(/\d+/g) const filter = _.curry((func,array)=>{ return array.filter(func) }) const findSpace = filter(haveSpace) // console.log(haveSpace('hello world')); // console.log(haveNumber('abc123')); console.log(filter(haveSpace,['John Connor','John_Donne'])); console.log(findSpace(['John Connor','John_Donne'])); 模拟_.curry()的实现 function getSum(a,b,c){ return a+b+c; } const curried = curry(getSum) console.log(curried(1,2,3)); console.log(curried(1)(2,3)); console.log(curried(1,2)(3)); function curry(func){ return function curriedFn(...args){ if(args.length<func.length){ return function(){ return curriedFn(...args.concat(Array.from(arguments))) } } return func(...args) } }

    总结

    柯里化可以让我们给一个函数传递较少的参数得到一个已经记住了某些固定参数的新函数。这是一种对函数参数的’缓存‘让函数变得更灵活,让函数的粒度更小可以把多元函数转换成一元函数,可以组合使用函数产生强大的功能。

    1.9 函数组合

    纯函数和柯里化很容易写出洋葱代码 获取数组的最后一个元素再转换成大写字母_.toUpper(_.first(_.reverse(arr))) 函数组合可以让我们把细粒度的函数重新组合生成一个新的函数

    管道

    下图便是程序中使用函数处理数据的过程,给fn函数输入参数a,返回结果b。可以想象a数据通过一个管道得到了b数据。 当fn函数比较复杂的时候,可以把fn拆分成多个小函数,此时多了中间运算过程中产生的m和n。 下图可以想象把fn这个管道拆分成3个管道f1,f2,f3,数据a通过管道f3得到结果m,m再通过管道f2得到结果n,n通过管道f1得到最终结果b. fn = compose(f1,f2,f3) b = fn(a)

    函数组合

    compose : 如果一个函数要经过多个函数处理才能得到最终值,这个时候可以把中间过程的函数合并成一个函数。 函数就像是数据的管道,函数组合就是把这些管道连接起来,让数据穿过多个管道形成最终结果。函数组合默认从右到左执行 // 函数组合 function compose(f,g){ return function(value){ return f(g(value)) } } function reverse(arr){ return arr.reverse(); } function first(arr){ return arr[0] } let last = compose(first,reverse) console.log(last([1,2,3,4])); lodash中的组合函数 flow()或flowRight() // lodash中函数组合 _.flowRight const _ = require('lodash') const reverse = arr => arr.reverse(); const first = arr => arr[0] const toUpper = s => s.toUpperCase(); const f = _.flowRight(toUpper,first,reverse) console.log(f(['one','two','three'])); 模拟实现lodash的flowRight方法 // 模拟实现lodash的flowRight方法 const reverse = arr=>arr.reverse(); const first = arr=>arr[0] const toUpper = s=>s.toUpperCase(); // function compose(...args){ // return function(value){ // return args.reverse().reduce((acc,fn)=>{ // return fn(acc) // },value) // } // } const compose = (...args)=>value=>args.reverse().reduce((acc,fn)=>fn(acc),value) const f = compose(toUpper,first,reverse) console.log(f(['one','two','three'])); 函数的组合要满足结合律 既可以把g和h组合,还可以把f和g组合,结果都是一样的 const _ = require('lodash') const f = _.flowRight(_.toUpper,_.first,_.reverse) const f1 = _.flowRight(_.flowRight(_.toUpper,_.first),_.reverse) const f2 = _.flowRight(_.toUpper,_.flowRight(_.first,_.reverse)) console.log(f(['one','two','three'])); console.log(f1(['one','two','three'])); console.log(f2(['one','two','three']));

    组合调试

    组合调试 const _ = require('lodash') const trace = _.curry((tag,v)=>{ console.log(tag,v); return v }) const split = _.curry((sep,str)=>_.split(str,sep)) const jion = _.curry((sep,arr)=>_.join(arr,sep)) const map = _.curry((fn,arr)=>_.map(arr,fn)) const f = _.flowRight(jion('-'),trace('map之后'),map(_.toLower),trace('split之后'),split(' ')) console.log(f('never say die')); lodash/fp lodash的fp模块提供了实用的对函数式编程友好的方法 const fp = require('lodash/fp') const _ = require('lodash') const f = fp.flowRight(fp.join('-'),fp.map(fp.toLower),fp.split(' ')) console.log(f('NEVER SAY DIE')); console.log(_.map(['23','8','10'],parseInt)); console.log(fp.map(parseInt,['23','8','10']));

    1.10 Point Free

    我们可以把数据处理的过程定义成与数据无关的合成运算,不需要用到代表数据的那个参数,只是把简单的运算步骤合成到一起,在使用这种模式之前我们需要定义一些辅助的基本运算函数。

    不需要指明处理的函数只需要合成运算过程需要定义一些辅助的基本运算函数 const f = fp.flowRight(fp.join('-'),fp.map(_.toLower),fp.split(' ')) // 非 Point Free模式 // function f(word){ // return word.toLowerCase().replace(/\s+/g,'_') // } // console.log(f('Hello World')); // Point Free const fp = require('lodash/fp') const f = fp.flowRight(fp.replace(/\s+/g,'_'),fp.toLower) console.log(f('Hello World')); // 实用Point Free,模式 将单词中的首字母提取出来,并转换为大些 const firstLetterToUpper = fp.flowRight(fp.join('. '), fp.map(fp.flowRight(fp.first, fp.toUpper)), fp.split(' ')) console.log(firstLetterToUpper('world wild web'));

    1.11 函子(Functor)

    在函数式编程中如何把副作用控制在可控的范围内、异常处理、异步操作等。

    什么是函子?

    容器:包含值和值的变形关系(这个变形关系就是函数)函子:是一个特殊的容器,通过一个普通的对象来实现,该对象具有map方法,map方法可以运行一个函数对值进行处理(变形关系) 总结 函数式编程的运算不直接操作值,而是由函子完成函子就是一个实现了map锲约的对象可以把函子想象成一个盒子,这个盒子里封装了一个值想要处理盒子中的值,我们需要给盒子的map方法传递一个处理值的函数(纯函数),由这个函数来对值进行处理最终map方法返回一个包含新值的盒子(函子) 在Functor中如果我们传入null或undefined // class Contains{ // constructor(value){ // this._value = value // } // map(fn){ // return new Contains(fn(this._value)) // } // } // let r = new Contains(5).map(x=>x+1).map(x=>x*x) // console.log(r); // 36 class Contains{ static of(val){ return new Contains(val) } constructor(value){ this._value = value } map(fn){ return Contains.of(fn(this._value)) } } let r = new Contains(5).map(x=>x+2).map(x=>x*x) console.log(r); // 49 // 演示 null undefined的问题 如果不小心传入了空值 Contains.of(null).map(x=>x.toUpperCase())

    MayBe函子

    对错误做响应的处理。 MayBe的作用就是对外部的空值情况做处理(控制副作用的允许的范围) 在MayBe函子中,很难确认是哪一步产生的空值问题。

    class MayBe{ static of(val){ return new MayBe(val) } constructor(val){ this._val = val } map(fn){ return this.isNothing()?MayBe.of(null):MayBe.of(fn(this._val)) } isNothing(){ return this._val === null || this._val === undefined } } let r = MayBe.of('hello lala').map(x=>x.toUpperCase()) // console.log(r); let r1 = MayBe.of(null).map(x=>x.toUpperCase()) // console.log(r1); let r2 = MayBe.of('hello lala') .map(x=>x.toUpperCase()) .map(x=>null) .map(x=>x.split(' ')) console.log(r2);

    Either函子

    两者中的任何一个,类似if…else…的处理 异常会让函数变得不纯,Either函子可以用来做异常处理

    class Left{ static of(val){ return new Left(val) } constructor(val){ this._val = val } map(fn){ return this } } class Right{ static of(val){ return new Right(val) } constructor(val){ this._val = val } map(fn){ return Right.of(fn(this._val)) } } let r1 = Right.of(12).map(x=>x+2) let r2 = Left.of(12).map(x=>x+2) console.log(r1); console.log(r2); function parseJson(str) { try { return Right.of(JSON.parse(str)) } catch (e) { return Left.of({err:e.message}) } } let r = parseJson('{"name":"zs"}').map(x=>x.name.toUpperCase()) console.log(r);

    IO函子

    IO函子中的_val是一个函数,这里是把函数作为值来处理。 IO函子可以把不纯的动作存储到_val中,延迟执行这个不纯的操作(惰性执行),把不纯的操作交给调用者来处理。

    const fp = require('lodash/fp') class IO{ static of(val){ return new IO(function(){ return val }) } constructor(val){ this._val = val } map(fn){ return new IO(fp.flowRight(fn,this._val)) } } let r = IO.of(process).map(p=>p.execPath) console.log(r); console.log(r._val());

    Task异步执行

    使用folktale中的Task来演示。 folktale是一个标准的函数式编程库。 只提供了一些函数式处理操作,如compose、curry等 一些函子Task、Either、MayBe等

    const {compose,curry} = require('folktale/core/lambda') const {toUpper,first} = require('lodash/fp') let f = curry(2,(x,y)=>x+y) console.log(f(1,2)); console.log(f(1)(2)); let f1 = compose(toUpper,first) console.log(f1(['one','two']));

    task异步执行

    const fs = require('fs') const { task } = require('folktale/concurrency/task') const {split,find} = require('lodash/fp') function readFile(fileName){ return task(resolver=>{ fs.readFile(fileName,'utf-8',(err,data)=>{ if(err) resolver.reject(err) resolver.resolve(data) }) }) } readFile('../../package.json') .map(split('\n')) .map(find(x=>x.includes('version'))) .run() .listen({ onRejected:err=>{ console.log(err); }, onResolved:value=>{ console.log(value); } })

    Pointed函子

    Pointed函子是实现了of静态方法的函子 of方法是为了避免使用new来创建对象,更深层的含义是of方法来把值放到上下文Context(把值放到容器中,使用map来处理)

    class Contains{ static of(val){ return new Contains(val) } ... } Contains.of(2).map(x=>x+1)

    Monad(单子)

    一个函子如果具有join和of两个方法并遵守一些定律就是一个Monad

    const fs = require('fs') const fp = require('lodash/fp') class IO{ static of(val){ return new IO(()=>val) } constructor(val){ this._val = val } map(fn){ return new IO(fp.flowRight(fn,this._val)) } } let readFile = (filename)=>{ return new IO(()=>fs.readFileSync(filename,'utf-8')) } let print = (x)=>{ return new IO(()=>{ console.log(x); return x }) } let cat = fp.flowRight(print,readFile) let r = cat('../../package.json')._val()._val() console.log(r); const fs = require('fs') const fp = require('lodash/fp') class IO{ static of(val){ return new IO(()=>val) } constructor(fn){ this._val = fn } map(fn){ return new IO(fp.flowRight(fn,this._val)) } join(){ return this._val() } flatMap(fn){ return this.map(fn).join() } } let readFile = (filename)=>{ return new IO(()=>fs.readFileSync(filename,'utf-8')) } let print = (x)=>{ return new IO(()=>{ console.log(x); return x }) } let r = readFile('../../package.json') .map(fp.toUpper) .flatMap(print) .join() console.log(r);

    参考资料: 函数式编程指北 函数式编程入门 Pointfree 编程风格指南 图解 Monad Functors

    2、异步编程

    2.1 同步&异步

    同步

    console.log('start'); function bar(){ console.log('bar'); } function foo(){ console.log('foo'); bar() } foo(); console.log('end');

    异步

    console.log('start'); setTimeout(()=>{ console.log('timer1'); },1800) setTimeout(()=>{ console.log('timer2'); setTimeout(()=>{ console.log('inner'); },1000) },1000) console.log('end'); // start // end // timer2 // timer1 // inner

    2.2 回调

    function foo(cb) { setTimeout(()=>{ cb() },1000) } foo(()=>{ console.log('回调'); console.log('调用者定义,执行者执行'); console.log('就是调用者告诉执行者异步任务结束后应该做什么'); }) // 回调地狱

    2.3 promise

    promise基本示例

    const promise = new Promise((resolve,reject)=>{ resolve(100) }) promise.then(value=>{ console.log(value); },err=>{ console.log(err); }) console.log('end');

    promise-ajax

    function ajax(url) { return new Promise((resolve,reject)=>{ var xhr = new XMLHttpRequest(); xhr.open('GET',url); xhr.responseType = 'json' xhr.onload = function(){ if(this.status === 200){ resolve(this.response) }else{ reject(new Error(this.statusText)) } } xhr.send() }) } ajax('./api/users.json').then(res=>{ console.log(res); },err=>{ console.log(err); }) // 嵌套使用 Promise 是最常见的误区 ajax('/api/urls.json').then(function (urls) { ajax(urls.users).then(function (users) { ajax(urls.users).then(function (users) { ajax(urls.users).then(function (users) { ajax(urls.users).then(function (users) { }) }) }) }) })

    promise链式调用

    function ajax(url) { return new Promise((resolve,reject)=>{ var xhr = new XMLHttpRequest(); xhr.open('GET',url); xhr.responseType = 'json' xhr.onload = function(){ if(this.status === 200){ resolve(this.response) }else{ reject(new Error(this.statusText)) } } xhr.send() }) } ajax('./api/users.json') .then(res=>{ console.log(res); return ajax('./api/users.json') }) .then(res=>{ console.log(res); return ajax('./api/users.json') }) .then(res=>{ console.log(res); return ajax('./api/users.json') }) .then(res=>{ console.log(res); return 'foo' }) .then(res=>{ console.log(res); })

    promise异常处理

    function ajax(url) { return new Promise((resolve,reject)=>{ var xhr = new XMLHttpRequest(); xhr.open('GET',url); xhr.responseType = 'json' xhr.onload = function(){ if(this.status === 200){ resolve(this.response) }else{ reject(new Error(this.statusText)) } } xhr.send() }) } // 使用catch注册失败回调 ajax('./api/users.json') .then(res=>{ console.log(res); return ajax('./api/users.json') }) .catch(err=>{ console.log(err); }) // then(onRejected) === then(undefined,onRejected) ajax('./api/users.json') .then(res=>{ console.log(res); return ajax('./api/users.json') }).then(undefined,(err)=>{ console.log(err); }) // 全局捕获 promise异常 window.addEventListener('unhandlerejection',event=>{ const {reason,promise} = event console.log(reason,promise); event.preventDefault(); },false) // node中的方式 process.on('unhandledRejection',(reason,promise)=>{ console.log(reason,promise); })

    微任务

    console.log('start'); setTimeout(()=>{ console.log('setTimeout'); },0) Promise.resolve().then(()=>{ console.log('promise'); }).then(()=>{ console.log('promise1'); }) console.log('end');

    2.4 generator

    function * foo(){ console.log('start'); try { const res = yield 'foo' console.log(res); } catch (error) { console.log(error); } } const generator = foo(); const res = generator.next(); console.log(res); // generator.throw(new Error('xxx error')) function * main () { try { const users = yield ajax('/api/users.json') console.log(users) const posts = yield ajax('/api/posts.json') console.log(posts) const urls = yield ajax('/api/urls11.json') console.log(urls) } catch (e) { console.log(e) } } function co (generator) { const g = generator() function handleResult (result) { if (result.done) return // 生成器函数结束 result.value.then(data => { handleResult(g.next(data)) }, error => { g.throw(error) }) } handleResult(g.next()) } co(main)

    2.5 async/await

    async function main () { try { const users = await ajax('/api/users.json') console.log(users) const posts = await ajax('/api/posts.json') console.log(posts) const urls = await ajax('/api/urls.json') console.log(urls) } catch (e) { console.log(e) } } // co(main) const promise = main()
    Processed: 0.010, SQL: 9