同期异步系列文章推荐谈一谈 javascript 异步 javascript 异步与 promisejavascript 异步之 Promise.all()、Promise.race()、Promise.finally()javascript 异步之 Promise.resolve()、Promise.reject()javascript 异步之 Promise then 和 catchjavascript 异步之 async(一)javascript 异步之 async(二)javascript 异步实战 javascript 异步总结归档
我们之前介绍了 javascript 异步的相关内容,我们知道 javascript 以同步,单线程的方式执行主线程代码,将异步内容放入事件队列中,当主线程内容执行完毕就会立即循环事件队列,直到事件队列为空,当用产生用户交互事件(鼠标点击,点击键盘,滚动屏幕等待),会将事件插入事件队列中,然后继续执行。处理异步逻辑最常用的方式是什么?没错这就是我们今天要说的 — 回调
js 回调函数
如你所知,函数是对象,所以可以存储在变量中,所以函数还有以下身份:
可以作为函数的参数
可以在函数中创建
可以在函数中返回
当一个函数 a 以一个函数作为参数或者以一个函数作为返回值时,那么函数 a 就是高阶函数回调函数百度百科
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
维基百科
在计算机程序设计中,回调函数,或简称回调(Callback 即 call then back 被主函数调用运算后会返回主函数),是指通过函数参数传递到其它代码的,某一块可执行代码的引用。这一设计允许了底层代码调用在高层定义的子程序。
回调函数,几乎每天我们都在用
setTimeout(() => {
console.log(“ 这是回调函数 ”);
}, 1000);
const hero=[‘ 郭靖 ’,’ 黄蓉 ’]
hero.forEach(item=>{
console.log(item);
})
回调函数解决了哪些问题
举一个简单的:
let girlName = “ 裘千尺 ”
function hr() {
girlName = “ 黄蓉 ”
console.log(` 我是 ${girlName}`);
}
function gj() {
console.log(`${girlName} 你好,我是郭靖,认识一下吧 `);
}
hr()
gj()
输出, 重点看输出顺序
//=> 我是黄蓉
//=> 黄蓉你好,我是郭靖,认识一下吧
上面的代码输出是没什么悬念的,不存在异步,都单线程同步执行,最后郭靖和黄蓉相识如果这时候黄蓉很忙,出现了异步,会怎么样?
let girlName = “ 裘千尺 ”
function hr() {
setTimeout(() => {
girlName = “ 黄蓉 ”
console.log(‘ 我是黄蓉 ’);
}, 0);
}
function gj() {
console.log(`${girlName} 你好,我是郭靖,认识一下吧 `);
}
hr()
gj()
输出, 重点看输出顺序
//=> 裘千尺你好,我是郭靖,认识一下吧
//=> 我是黄蓉
虽然定时器是 0ms,但是也导致了郭靖和黄蓉的擦肩而过,这不是我们期望的结果,hr 函数存在异步,只有等主线程的内容走完,才能走异步函数所以最简单的办法就是使用回调函数解决这种问题,gj 函数依赖于 hr 函数的执行结果,所以我们把 gj 作为 hr 的一个回调函数
let girlName = “ 裘千尺 ”
function hr(callBack) {
setTimeout(() => {
girlName = “ 黄蓉 ”
console.log(‘ 我是黄蓉 ’);
callBack()
}, 0);
}
function gj() {
console.log(`${girlName} 你好,我是郭靖,认识一下吧 `);
}
hr(gj)
输出, 重点看输出顺序
//=> 我是黄蓉
//=> 黄蓉你好,我是郭靖,认识一下吧
⚠️:当回调函数作为参数时,不要带后面的括号!我们只是传递函数的名称,不是传递函数的执行结果上面小栗子貌似的很简单,我们继续
嵌套回调和链式回调
我们把昨天的 demo 做一下升级引入了 lodash:处理按钮点击防抖 axios,集成了 promis,但 promise 不是我们今天讨论的内容,我们只使用 axios 的 ajax 请求接口功能 easy-mock: 接口数据,用来实现 ajax 请求(数据是假的,但是请求是真的)
嵌套回调
<!DOCTYPE html>
<html lang=”en”>
<head>
<meta charset=”UTF-8″>
<meta name=”viewport” content=”width=device-width, initial-scale=1.0″>
<meta http-equiv=”X-UA-Compatible” content=”ie=edge”>
<title>javascript 回调 </title>
<script src=”https://unpkg.com/axios/dist/axios.min.js”></script>
<script src=”https://cdn.bootcss.com/lodash.js/4.17.11/lodash.min.js”></script>
</head>
<body>
<button> 点击 </button>
<script>
{
const btn = document.querySelector(‘button’)
btn.onclick = () => {
_.debounce(() => {
axios.get(‘https://easy-mock.com/mock/5b0525349ae34e7a89352191/example/mock’)
.then(data => {
console.log(“ajax 返回成功 ”);
myData = data.data
console.log(myData);
})
.catch(error => {
console.log(“ajax 返回失败 ”);
})
}, 500)()
}
}
</script>
</body>
</html>
仔细看代码,不难发现,这是一个典型的嵌套回调,我们分析一下第一层异步,用户交互,来自按钮的点击事件第二层异步,按钮去抖,来自 lodash 下 debounce 的 500ms 延时第三次异步,ajax 请求,处理后台接口数据拿到数据后我们没有继续做处理,在实际工作中可能还存在异步,还会继续嵌套,会形成一个三角形的缩进区域再继续嵌套,就会形成所说的“回调地狱”,就是回调的层级太多了,代码维护成本会高很多上面的栗子最多算是入门毁掉地狱,我们看一下这个
function funA(callBack) {
console.log(“A”);
setTimeout(() => {
callBack()
}, 10);
}
function funB() {
console.log(“B”);
}
function funC(callBack) {
console.log(“C”);
setTimeout(() => {
callBack()
}, 100);
}
function funD() {
console.log(“D”);
}
function funE() {
console.log(“E”);
}
function funF() {
console.log(“F”);
}
// 从这里开始执行
funA(() => {
funB()
funC(() => {
funD()
})
funE()
})
funF()
(这段代码,带回调的都是异步逻辑)你能很快的看出这段代码的执行顺序吗?顺序如下:A、F、B、C、E、D 一般正常人不会这么嵌套多层,层级一多,就会考虑拆分
链式回调
const btn = document.querySelector(‘button’)
// 监听按钮点击事件
btn.onclick = () => {
debounceFun()
}
// 去抖动
const debounceFun = _.debounce(() => {
ajax()
}, 500)
//ajax 请求
const ajax = function () {
axios.get(‘https://easy-mock.com/mock/5b0525349ae34e7a89352191/example/mock’)
.then(data => {
console.log(“ajax 返回成功 ”);
myData = data.data
console.log(myData);
})
.catch(error => {
console.log(“ajax 返回失败 ”);
})
}
我相信很多人都会通过这种链式回调的方式处理异步回调,因为可读性比嵌套回调要搞,但是维护的成本可能要高很多上面的栗子,三个异步函数之间只有执行顺序上的关联,并没有数据上的关联,但是实际开发中的情况要比这个复杂,
回调函数参数校验
我们举一个简单的栗子
let girlName = “ 裘千尺 ”
function hr(callBack) {
setTimeout(() => {
girlName = “ 黄蓉 ”
console.log(‘ 我是黄蓉 ’);
callBack(girlName)
}, 0);
}
function gj(love) {
console.log(`${girlName} 你好,我是郭靖,认识一下吧,我喜欢 ${love}`);
}
hr(gj)
gj 作为 hr 的回调函数,并且 hr 将自己的一个变量传递给 gj,gj 在 hr 的回调中执行,仔细看这种写法并不严谨,如果 gj 并不只是一个 function 类型会怎么样?如果 love 的实参并不存在会怎么样?况且这只是一个简单的栗子所以回调函数中,参数的校验是很有必要的,回调函数链拉的越长,校验的条件就会越多,代码量就会越多,随之而来的问题就是可读性和可维护性就会降低。
还是回调函数的校验
但我们引用了第三方的插件或库的时候,有时候难免要出现异步回调的情况,一个栗子:xx 支付,当用户发起支付后,我们将自己的一个回调函数,传递给 xx 支付,xx 支付比较耗时,执行完之后,理论上它会去执行我们传递给他的回调函数,是的理论上是这样的,我们把回调的执行权交给了第三方,隐患随之而来第三方支付,多次调用我们的回调函数怎么办?第三方支付,不调用我们的回调函数怎么办?当我们把回调函数的执行权交给别人时,我们也要考虑各种场景可能会发生的问题
总结一下:回调函数简单方便,但是坑也不少,用的时候需要多注意校验
原文链接