从执行结果看 Vue 的生命周期

159 阅读4分钟

只有同步代码的情况

本文将给不同的生命周期执行的打印操作,来观察生命周期中代码执行的顺序,同时加深对数据的产生和变化的理解。 首先我们来看一下,给前4个钩子都添加控制台打印的代码,

<script>
export default {
  name: "App",
  beforeCreate() {
    console.log("beforeCreate运行");
  },
  created() {
    console.log("created开始 ");
  },
  beforeMount() {
    console.log("beforeMount运行");
  },
  mounted() {
    console.log("mounted开始 ");
  },
};
</script>

其执行结果是 接下来我们添加数据,并进行修改操作来查看 beforeUpdated 和 updated 这两个钩子,我们先不将数据渲染进视图,观察改变数据会不会触发这两个钩子

<template>
  <div id="app">生命周期测试</div>
</template>

<script>
export default {
  name: "App",
  data() {
    return {
      test: 0,
    };
  },
  beforeCreate() {
    this.test++
    console.log("beforeCreate运行"+this.test);
  },
  created() {
    this.test++
    console.log("created开始 "+this.test);
  },
  beforeMount() {
    this.test++
    console.log("beforeMount运行"+this.test);
  },
  mounted() {
    this.test++
    console.log("mounted开始 "+this.test);
  },
  beforeUpdate() {
    this.test++
    console.log("beforeUpdate运行"+this.test);
  },
  updated() {
    this.test++
    console.log("updated开始"+this.test);
  }
};
</script>

打印结果 我们可以看到 beforeUpdate 和 updated 这两个钩子没有被触发,很显然,这两个钩子是监视视图的。同时,我们看到,在 beforeCreate 中获取不到数据,顾名思义。 接下来,我们在视图中渲染数据

<template>
  <div id="app">生命周期测试{{test}}</div>
</template>

<script>
export default {
  name: "App",
  data() {
    return {
      test: 0,
    };
  },
  beforeCreate() {
    this.test++
    console.log("beforeCreate运行"+this.test);
  },
  created() {
    this.test++
    console.log("created开始 "+this.test);
  },
  beforeMount() {
    this.test++
    console.log("beforeMount运行"+this.test);
  },
  mounted() {
    this.test++
    console.log("mounted开始 "+this.test);
  },
  beforeUpdate() {
    this.test++
    console.log("beforeUpdate运行"+this.test);
  },
  updated() {
    this.test++
    console.log("updated开始"+this.test);
  }
};
</script>

哈哈,不幸的是,如果你运行了上面的代码,你会发现进入了死循环,因为你在视图更新前又要去改数据,视图更新后也要改数据,改了数据又要触发这两个钩子,触发了钩子又要改,这可不就是进入循环了吗。所以,这里我们得到一个教训就是,千万不要在 beforeUpdate 和 updated 中修改影响视图的数据! 现在我们去掉这两条修改数据的代码 代码链接

<template>
  <div id="app">生命周期测试{{test}}</div>
</template>

<script>
export default {
  name: "App",
  data() {
    return {
      test: 0,
    };
  },
  beforeCreate() {
    this.test++
    console.log("beforeCreate运行"+this.test);
  },
  created() {
    this.test++
    console.log("created开始 "+this.test);
  },
  beforeMount() {
    this.test++
    console.log("beforeMount运行"+this.test);
  },
  mounted() {
    this.test++
    console.log("mounted开始 "+this.test);
  },
  beforeUpdate() {
    console.log("beforeUpdate运行"+this.test);
  },
  updated() {
    console.log("updated开始"+this.test);
  }
};
</script>

打印结果 我们看到 beforeUpdate 和 updated 都只被触发了一次,明明我们前面的4个钩子至少修改了3次 test 啊,怎么只更新了一次呢?视图的更新肯定是在挂载之后的,所以在本例中 mounted 中的修改才会触发视图更新,因为挂载前,是虚拟dom,还没渲染进视图。

异步:模拟有请求的情况

我们都知道,Vue 的 created 和 mounted 这两个钩子比较适合进行 AJAX 请求。接下来我们来看一下,在 created 和 mounted 中各自用 Pormise 来异步修改一个数据,看看结果是怎样的。 代码链接

<template>
  <div id="app">生命周期测试{{test}}</div>
</template>

<script>
export default {
  name: "App",
  data() {
    return {
      test: 0
    };
  },
  beforeCreate() {
    this.test++;
    console.log("beforeCreate运行" + this.test);
  },
  created() {
    this.test++;
    console.log("created开始 " + this.test);
    new Promise((resolve, reject) => {
      resolve();
    }).then(() => {
      this.test++;
      console.log("异步的created开始 " + this.test);
    });
  },
  beforeMount() {
    this.test++;
    console.log("beforeMount运行" + this.test);
  },
  mounted() {
    this.test++;
    console.log("mounted开始 " + this.test);
    new Promise((resolve, reject) => {
      resolve();
    }).then(() => {
      this.test++;
      console.log("异步的mounted开始 " + this.test);
    });
  },
  beforeUpdate() {
    console.log("beforeUpdate运行" + this.test);
  },
  updated() {
    console.log("updated开始" + this.test);
  }
};
</script>

我们发现,updated 这个钩子只被触发了2次,我们看到,两次使用 Promise 修改数据都是在 mounted 之后执行的,所以,mounted之后至少修改了 3 次数据,显然有 2 次修改的视图更新被合并了,因为 Vue 是异步渲染的,像是有个周期一样,每个周期内收集需要修改的内容,然后时间到了就渲染,并清空渲染队列,再进入下一个周期。在本例中,渲染 mounted 中的修改和 created 中的异步修改被合并了。所以可以认为,在 created 中请求数据,也不会立即得到,至少需要执行到 mounted 之后中才能获取到。

watch 的 immediate 属性与生命周期的关系

接下来我们来聊一下 watch 中的 immediate 这个属性。假如我们的项目中的有一个 tabs 切换不同的显示,每个 tab 对应一个 id 以及对应的数据,数据需要根据 id 向后台去请求,我们需要 watch 来监听这个 id,当它一变化,也就是我们切换了 tab,需要重新请求数据。比如:

watch: {
  id: {
    async handler() {
      const res = await this.$http.get('/data/' + id)
      this.data = res.data
    }
  }
},

但是,这种方案,会有一个问题,就是 watch 的immediate 为 false 的时候,id 从 undefined 到初始化完成的时候,是不会监听的,也就是我们初次打开页面的时候,没法获取到 tab 的数据。时候可以通过将 immediate 设为 true 来解决。下面举例来说明这个属性的影响。

首先,我们来看一下 watch 设置 immediate 为 false 时候,在各钩子中变化数据的反应。 代码链接

<template>
  <div id="app">生命周期测试{{test}}</div>
</template>

<script>
export default {
  name: "App",
  data() {
    return {
      test: 0
    };
  },
  beforeCreate() {
    this.test++;
    console.log("beforeCreate运行" + this.test);
  },
  created() {
    this.test++;
    console.log("created开始 " + this.test);
    new Promise((resolve, reject) => {
      resolve();
    }).then(() => {
      this.test++;
      console.log("异步的created开始 " + this.test);
    });
  },
  beforeMount() {
    this.test++;
    console.log("beforeMount运行" + this.test);
  },
  mounted() {
    this.test++;
    console.log("mounted开始 " + this.test);
    new Promise((resolve, reject) => {
      resolve();
    }).then(() => {
      this.test++;
      console.log("异步的mounted开始 " + this.test);
    });
  },
  beforeUpdate() {
    console.log("beforeUpdate运行" + this.test);
  },
  updated() {
    console.log("updated开始" + this.test);
  },

  watch: {
    test: {
      handler(oldVal,newVal) {
        console.log('watch 到 test 变化了');
      }
    },
  },
};
</script>

结果 我们可以看到,在 mounted 之前修改的数据 根本 watch不到,mounted以及之后的数据修改的 watch 情况与 update 是同步的。其实渲染中也是有一个 watch 的,如果数据修改了,watch就会被压入队列中,如果同一个 watch 在一个 nextTick 的状态为等待时间时多次被触发,也会去重,不会多次压入队列,所以,watch 也是有一个异步调用的机制在里面的。

接下来,我们将 watch 设置 immediate 为 true 代码链接

<template>
  <div id="app">生命周期测试{{test}}</div>
</template>

<script>
export default {
  name: "App",
  data() {
    return {
      test: 0
    };
  },
  beforeCreate() {
    this.test++;
    console.log("beforeCreate运行" + this.test);
  },
  created() {
    this.test++;
    console.log("created开始 " + this.test);
    new Promise((resolve, reject) => {
      resolve();
    }).then(() => {
      this.test++;
      console.log("异步的created开始 " + this.test);
    });
  },
  beforeMount() {
    this.test++;
    console.log("beforeMount运行" + this.test);
  },
  mounted() {
    this.test++;
    console.log("mounted开始 " + this.test);
    new Promise((resolve, reject) => {
      resolve();
    }).then(() => {
      this.test++;
      console.log("异步的mounted开始 " + this.test);
    });
  },
  beforeUpdate() {
    console.log("beforeUpdate运行" + this.test);
  },
  updated() {
    console.log("updated开始" + this.test);
  },

  watch: {
    test: {
      handler(oldVal, newVal) {
        console.log("watch 到 test 变化了");
      },
      immediate: true
    }
  }
};
</script>

结果 我们发现,在 mounted 之前,test 被初始化的时候,触发了一次 watch。所以,如果需要根据一个变化进行请求的情况,可以考虑把 immediate 设为true。