前言
这个标题不太好取。
本文需要下面的知识:
/p/260811233
问题描述
我最近的一个功能需求是通过axios获取我存储在COS中的json文件,同时渲染到页面上。
而问题就是,axios可以成功获取数据,组件也可以成功渲染,但是控制台会报错
[Vue warn]: Error in render: "TypeError: Cannot read property 'text' of undefined"。
问题分析
首先,我的情况比我已经查到的博主们的问题更为复杂。
我的json文件部分内容如下:
{"0": {"headline": "如何快速调整显示器组的亮度?","url": "/p/259647484","dateP": "-09-26T13:35:07.000Z","dateM": "-09-26T13:37:24.000Z","img": "/v2-8b58a57ba0923d23329abc16766da34c_r.jpg","text": "前言 Mac的情况我不清楚,但是Windows下调整显示器亮度其实是一个挺烦人的事。 我的显示器组为一个15.6寸的副屏,一个22寸的显示器,一个15.6寸的笔记本屏幕。 由于硬件原因,通过显示器本身调整亮…"},"1": {"headline": "通过Vue实现vw/vh自适应单位","url": "/p/257866252","dateP": "-09-22T10:51:08.000Z","dateM": "-09-22T10:51:08.000Z","img": "/v2-8ac0ec7ef990ca579657ae5b8bea714c_r.jpg","text": "前言 虽然现在可能响应式布局多一些,但是我还是觉得自适应单位比较好用。对于一些框架的UI组件,例如Vuetify,可能并不支持使用自适应单位,此时就会非常影响布局效果。 这篇文章提供的方法可以帮助你实现这…"},"2": {"headline": "如何DIY便携显示器?","url": "/p/257257464","dateP": "-09-21T13:43:08.000Z","dateM": "-09-21T13:52:32.000Z","img": "/v2-7f27fabd9fc334d2c7e529561658b82f_r.jpg","text": "前言 最近找工作比较压抑,写几篇文章放松一下。 本文较长,知识点较多,建议收藏或者赞同给自己留个指针(误 便携显示器 便携显示器听名字就能知道,是一种移动的显示器,所以它要满足几个大的需求: 低压供电(相对于传…"},
如果你好奇这个json文件是怎么获取到的,你可以访问:
LikeDreamwalker/zhihu-articles-api
并且给我一个star!
这个json文件的问题是它是多层的,一般的示例中都是简单的一维数组或者一层的对象。
接下来就是问题的关键,为什么axios已经成功获取到数据,并且组件也已经渲染,但是会报错呢?
undefined意味着当时index[0].text并没有被定义,而却又被调用,即在页面渲染时调用index对象,但是index[0].text还没有被定义,也就是axios还没有得到数据。
那么思路就很简单了,方案1是我们尝试提前写好index对象,即在data中指定
index: {{text=null}}
方案2则是我们让对应组件在axios获取数据之后再渲染组件。
我先解释一下1,1是一个解决方案,但是对于我这种情况,不可控有多少个对象,而每个对象又有诸多属性,这种解决方案治标不治本。但是,当你在声明时应该尽可能按照对象的真实结构声明。如果你和我一样不能解决这个问题,下面也有办法。
当然,如果你的对象很简单,那么到这里就结束了,这可以是一个解决方法。
所以,问题变简单了,核心就是axios到底是在什么时刻获取的数据,并且有没有可能在页面被渲染之前获取数据。
axios在何时获取到数据
axios的一般用法有两种,一种是放在created里,也就是在页面渲染之前,另外一种是放在mounted里,也就是页面渲染之后。
所以我们可以进行尝试,分别写好两种情况:
created() {console.log("start");axios.get("https://ldwid-1258491808.cos.ap-/json/doNotUseThisJSON.json")//用于调试本地json//.get("http://localhost:8080/doNotUseThisJSON.json").then(response => {_this.index = response.data;console.log(response.data);console.log(_this.index);console.log(_this.index[0]);});console.log("created");},mounted() {console.log("start");axios.get("https://ldwid-1258491808.cos.ap-/json/doNotUseThisJSON.json")//用于调试本地json//.get("http://localhost:8080/doNotUseThisJSON.json").then(response => {_this.index = response.data;console.log(response.data);console.log(_this.index);console.log(_this.index[0]);});console.log("mounted");}
不出意外的话,你会发现这两种情况依然会抛出undefined的错误。
但是这里与逻辑是相悖的,既然我选择在页面渲染之前获取到数据,为什么依然会报错?
猜测:网络问题
由于axios是异步的,也许在created时,真的执行了axios语句,但是由于网络问题,加载的时间大于语句的执行时间,所以导致获取到数据会在页面渲染之后。
这种猜测是符合逻辑的,但实际上是对JS异步和生命周期理解有问题。请阅读下面这篇文章:
/p/260811233
与其猜测,我们不如看一下具体的执行顺序:
beforeCreate() {console.log("beforCreate");},created() {let _this = this;console.log("start");axios// .get(// "https://ldwid-1258491808.cos.ap-/json/doNotUseThisJSON.json"// ).get("http://localhost:8080/doNotUseThisJSON.json").then(response => {_this.index = response.data;console.log(response.data);console.log(_this.index);console.log(_this.index[0]);});console.log("created");},beforeMount() {console.log("beforMount");},mounted() {console.log("mounted");}
之后我们加载页面:
答案可能会出乎你的意料,虽然axios语句被放在了created下,但是实际上的执行是在mounted之后。
如果你不相信,你还可以把axios放置到其他的生命周期,依然会是这个结果。
所以,axios会在页面完全渲染之后获取到数据。在渲染页面,获得模板字符串与替换字符串的时候,发现引用了并不存在的属性,自然会报错。
axios能不能在页面渲染之前获取到数据
这里需要生命周期和异步的知识:
/p/260811233
首先,axios是异步的,既然是异步,axios语句会在所有的宏任务执行完毕后,才会被访问到,并且按照微任务队列的顺序继续执行。所以,axios不能再页面渲染之前获取到数据。
如果,axios是同步的,当你填写了错误的API或者API失效,这就会导致整个生命周期受阻从而崩溃,最终页面渲染失败。但是很明显,在开发过程中不可能出现这种情况。
而如果你仍然有执念,认为是上面的网络环境问题,如果此时为真,那么axios实际上是另外一个线程,并行于生命周期,但是JavaScript是一个单线程语言。
到这里,整个问题的因果关系应该非常清晰了:
如果想要index[0].text正常解析,我们需要在解析前先填入正确的text属性,此时需要你直接写好index或者让axios在模板解析前将正确的text属性填入由于生命周期,我们可知axios必须在beforeMount之前完成获取由于axios的异步特性,axios不能在页面渲染之前完成获取如果想要如此,必须使axios在另外一个线程中执行,但JavaScript是一个单线程语言
问题解决
在我们知道出现问题的原因之后,解决问题的方式就变得很简单了。
我们在分析中已经知道,解决这个问题有至少两个办法。我们现在来看一下第二个办法。
在axios获取到数据之后渲染对应组件
这里我们使用Vue的条件渲染:
条件渲染 — Vue.js
既然,我们知道axios会在页面渲染之后执行,那么我们可以人为的添加一个状态,用于标识axios是否获取到数据。而对于对应的模板,仅当该状态为true时才会进行渲染。
如果你很疑惑为什么如此可以实现,你可以重新看一下报错的节点,是在beforeMount之后,在Mounted之前,也就是在模板被解析为模板字符串存储在内存中,但是还没有被渲染到页面上(或者说是即将被渲染在页面上)时,此时Vue不会执行不符合渲染条件的模板内容:
v-if
也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。
我们修改模板内容:
<v-card-text v-if="status">{{ index[0].text }}</v-card-text>
修改data:
data: () => ({status: false,index: {}}),
修改axios:
mounted() {let _this = this;axios.get("https://ldwid-1258491808.cos.ap-/json/doNotUseThisJSON.json").then(response => {_this.index = response.data;_this.status = true;});}
此时,再次刷新页面,不会再报错。
务必把status的修改放在.then下而不是mounted下,否则依然不会有效果。
这里需要提醒一下,很多人解决这个问题的办法使用的判断条件是属性本身,使用这个属性作为判断的条件的前提条件是这个属性存在,否则的话undefined依然会被转化为false属性为null也会转化为false,前者在使用时依然会报错undefined此时不会有任何改变,而之所以这个页面依然可以渲染出数据是因为双向绑定使这个属性值在页面被渲染后存在了,自然就会渲染出这个模板内容。
而如果,你可以去声明这样的内容,为什么不直接把完整的对象写好呢?如果完整的对象不便于手工声明,为什么还要使用这个对象的属性值作为判断条件呢?
至于性能,我认为影响甚微,毕竟多声明一个变量不是非常大的运算量。
最后
我不知道你有没有懂,但是我觉得在我爬出这个坑之后对Vue和JavaScript有了全新的认识。