背景
最近接手了别人写的前端代码,填了两个星期的坑了。周四填的这个坑比较有意思,是关于 created 钩子和异步请求的问题,反复验证了一个小时,终于弄清楚了同步请求和异步请求对钩子执行顺序的影响。
常规思路写出的代码,还真看不出问题,但是一运行就有问题,还是值得细究的。
功能描述
有一个管理功能的主页面,它被拆解成四个子组件:
- ChartsComp:顶部一些统计图
- SearchComp: 中间条件区域
- BtnComp:中间操作按钮区域
- DataTableComp:底部数据列表,有一列会根据国家名称显示国旗图片
主界面 XXXHome 的 created 钩子里,会初始化其他组件需要的数据:
子组件中 watch 不到数据变化
代码的前任想把所有的数据都在 Home 页面获取到,然后由子组件使用 watch 监听并使用。第一个问题发生在 getChartData
上,子组件的引用和数据使用如下:
因为数据是在父组件中通过异步请求获取的,子组件渲染时可能没有数据,就把绘图方法放在 watch 中,监听数据变化后再触发。
父子组件的 created 执行顺序
父子组件的 created 中添加打印信息,执行顺序如下:
从测试表现来看,父组件的所有异步请求返回后,子组件的 created 才执行。即从父到子,顺次执行。此时子组件的那几个 watch 大多数时候根本监控不到数据变化,因为传入的时候,异步响应已经返回了,数据非空,所以监听不到变化。
但是,如果直接在子组件的 created 调用绘图方法,又会出现数据不完整的问题,且具有偶发性,这一点由于时间有限、没有细探。
最保险的解决方法:在 Home 中添加一个 flag ,所有依赖异步请求数据的子组件都加一个 v-if 标识,等待数据回来后修正为真,再渲染子组件 charts v-if="isDataOk"
。
组件拆解,如果仅仅是为了减少主页面的代码量,而无其他复用时,比如本文这个 ChartsCmop,就可以将数据请求放在自己的 created 中,不需要依赖父组件传入,也就不会有这个问题了。
beforeCreated 和 created 的顺序
昨天碰到一个问题,测试后发现:如果把 beforeCreated 定义为 async
,它的 await 后的代码会在 created 执行完成后才执行。感觉有些颠覆官方的介绍,所以着重研究了一下。
官方说的是先执行 beforeCreate 再执行 created ,那么 beforeCreate 中的 await 会对这个顺序产生什么影响呢?
首先,还是这个功能,它的底部数据列表,需要根据国家名称显示国旗图片。前任是在 beforeCreate 中使用 await 操作先获取到国家和图片的 base64 信息,再在 created 中获取 table 数据,代码是这样写的:
其次,这段代码看起来没问题,笔者也感觉 beforeCreate 里面用了 await 同步,应该会先执行完成,再继续执行 created 方法。结果,运行后的日志信息却是这样:
第三,从执行结果来看, beforeCreated 中的 “beforeCreate finished” 是在 created 的代码执行完成后才打印的。这说明,在它等待请求响应结果期间,created 方法已经被执行了。
这种情况下 this.areas
还是默认的空数组,此时 created 的 getDataTable 方法会获取 DataTableComp 需要的数据,并完成子组件的渲染:
<span :title="scope.row.countryName">
<el-image :src="getCountryImg(scope.row.countryName)" class="flagImg"/>
{{ scope.row.countryName}}
</span>
第四,getCountryImg
使用 countries 时,会引发错误。因为它渲染时,会获取国旗图片,查找 countryName 对应的信息,如果不存在,就展示第一个元素 contries[0]
的图片:
getCountryImg(country) {
for (let i = 0; i < this.contries.length; i += 1) {
const item = this.contries[i];
if (item.nameZh === country) {
return `data:image/jpeg;base64,${item.img64}`;
}
}
return `data:image/jpeg;base64,${this.contries[0].img64}`;
},
最后,由于 DataTableComp 绘制的时候 beforeCreate 方法还没有结束,父组件传递来的 countries 是空数组,this.contries[0]
为 undefined 而引发异常:
解决办法:添加 isAreaOk 标识。初始为否,当异步请求回来后设置为真,列表组件依赖该标识 data-table-comp v-if="isAreasOk"
,子组件推迟到依赖解决后再渲染。
启示录
本文介绍 Vue 的 created 和 beforeCreate 钩子执行的两个知识点:
- 父子组件的 created 是顺次执行的,先父后子,即使父组件的 created 方法中有异步请求,也会等待所有的异步请求结束,才会执行子组件的 created 方法;
- 相同组件的 beforeCreate 方法先于 created 方法执行,如果 beforeCreate 中使用await 返回一个 Promise 对象,主流程不会处理,而是继续执行 created 方法。
关于第二点,就是说,虽然将 beforeCreate 定义为 async ,但是 Vue 调用时应该没有 await beforeCreate()
,本质上还是异步逻辑。所以不会等待它真正执行完成,后面的 created 就会继续执行,它的 await 进入回调函数等待队列中。
关于 Vue 开发的一些知识,笔者有一篇万字长文中有详细介绍,感兴趣的朋友可以看看这篇《五条Vue 开发锦集》。