Vue 中的事件循环

893 阅读4分钟

Vue 中的事件循环

父子组件钩子执行顺序

父beforeCreate -> 父created -> 父beforeMount -> 子beforeCreate -> 子created -> 子beforeMount -> 子mounted -> 父mounted

父组件包含多个子组件时,父组件和兄弟组件钩子执行顺序如下

父beforeCreate -> 父created -> 父beforeMount -> A组件beforeCreate -> A组件created -> A组件beforeMount-> B组件beforeCreate -> B组件created -> B组件beforeMount -> A组件mounted -> B组件mounted -> 父mounted

我们一般会在 父created / 父mounted 中去发送请求,那么执行顺序会变成

父beforeCreate -> 父created[request1] -> 父beforeMount -> 子beforeCreate -> 子created -> 子beforeMount -> 子mounted -> 父mounted[request2] -> request1_callback -> request2_callback

我们还可以在 Vue 中使用 setTimeout / setInterval / nextTick,那么执行顺序就会变成

父beforeCreate -> 父created -> 父beforeMount -> 子beforeCreate -> 子created[setTimeout -> nextTick] -> 子beforeMount -> 子mounted -> 父mounted[setInterval] -> nextTick_callback -> setTimeout_callback -> setInterval_callback

这其中涉及了 Javascript Event Loop 的知识,script(整体代码) / setTimeout / setInterval / request 都是宏任务(macro-tasks / tasks),nextTick 是微任务(micro-tasks / jobs)。事件循环从宏任务 script(整体代码) 开始第一次循环,之后全局上下文进入函数调用栈,直到调用栈清空(只剩全局),然后执行所有的 micro-tasks,当所有可执行的 micro-tasks 执行完毕之后,第一次事件循环结束。循环再次从 macro-tasks 开始,找到其中一个任务队列执行完毕,然后再执行所有的 micro-tasks,这样一直循环下去。 更加具体的解释可参考 详解事件循环

了解以上内容后,我们来看看下面这个例子打印的是什么?

<html>
  <head>
    <title>
      Event Loop in Vue
    </title>
    <meta name="description" content="Shopee Seller Data Center" />
  </head>
  <body>
    <div id="app">
      <div>I am Parent Compoent : name is {{ name }}</div>
      <div>-----------------------------------------</div>
      <comp :metric-data="metricData" />
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script>
      
      const comp = {
        template: `<div>I am Child Component : name is Child<div><ul v-for="item in metricData">{{ item.name }}</ul></div></div>`,
        props: {
          metricData: {
            type: Array,
            required: true,
          },
        },
        data() { 
          return {
            name: 'child',
          }
        },
        beforeCreate() {
          console.log('子beforeCreate')
        },
        created() {
          console.log('子created')
          setTimeout(() => {
            console.log('子created: setTimeout')
          }, 1000)
          this.$nextTick(() => {
            console.log('子created: nextTick')
          })
        },
        beforeMount() {
          console.log('子beforeMount')
        },
        mounted() {
          console.log('子mounted')
          setInterval(() => {
            console.log('子mounted: setInterval')
          }, 1000)
        }
      }
      const vm = new Vue({
        el: '#app',
        data: {
          name: 'Parent',
          metricData: []
        },
        components: {
          comp
        },
        methods: {
          getDataInCreated () {
            // 模拟请求
            const metricData = [
              { name: 'jack' },
              { name: 'lucy' }
            ]
            setTimeout(() => {
              console.log('父created: getDataInCreated')
              this.metricData = metricData
            }, 1000)
          },
          getDataInMounted () {
            // 模拟请求
            setTimeout(() => {
              console.log('父mounted: getDataInMounted')
            }, 1000)
          }
        },
        beforeCreate() {
          console.log('父beforeCreate')
        },
        created() {
          console.log('父created')
          this.getDataInCreated()
        },
        beforeMount() {
          console.log('父beforeMount')
        },
        mounted() {
          console.log('父mounted')
          this.getDataInMounted()
        }
      })
    </script>
  </body>
</html>

打印结果:

父beforeCreate        ---第一次事件循环
父created
父beforeMount
子beforeCreate
子created
子beforeMount
子mounted
父mounted
子created: nextTick

父created: getDataInCreated   ---第二次事件循环
子created: setTimeout         ---第三次事件循环
子mounted: setInterval        ---第四次事件循环
父mounted: getDataInMounted   ---第五次事件循环

beforeCreate & created 时选项的初始化

各个选项的初始化顺序:

beforeCreate -> initState[initProps -> initMethods -> initData -> initCompouted -> initWatch] -> created

beforeCreate:

  • props:属性(e.g. metricData)已经代理到 vm 上,但是访问会报错,这是因为 vm._props === undefined,访问 metricData 会执行 vm._props.metricData,所以会报 Exception: TypeError: Cannot read property 'metricData' of undefined at VueComponent.proxyGetter 的错误。相关源码如下:

    // 源码
    function initProps$1 (Comp) {
      var props = Comp.options.props;
      for (var key in props) {
        proxy(Comp.prototype, "_props", key);
      }
    }
    
    var sharedPropertyDefinition = {
      enumerable: true,
      configurable: true,
      get: noop,
      set: noop
    };
    function proxy (target, sourceKey, key) {
      sharedPropertyDefinition.get = function proxyGetter () {
        return this[sourceKey][key]
      };
      sharedPropertyDefinition.set = function proxySetter (val) {
        this[sourceKey][key] = val;
      };
      Object.defineProperty(target, key, sharedPropertyDefinition);
    }
    
  • methods:属性还未代理到 vm 上,即 vm 上没有这个属性。

  • data:属性还未代理到 vm 上。

  • computed:属性已经代理到 vm 上,即 vm 上有这个属性,但是值全是 undefined。

  • watch:vm 上没有 _watchers 属性。

created:

  • props:如果父组件中有使用这个prop:即传值 !== undefined,那么它的值就是父组件传递的值;如果父组件中没有使用这个 prop,定义时有默认值就等于默认值,没有默认值就是 undefined。

    if (父组件有使用:即传值 !== undefined) {
        value = 父组件传值
    } else {
        if (定义时有默认值) {
            value = 默认值
        } else {
            value = undefined
        }
    }
    
  • methods:在 initMethods 初始化并挂载到 vm 上。

  • data:属性在 initData 中被代理到 vm 上,值等于定义的初始值。

  • computed:每一个 computed 属性,都会生成一个对应的 watcher 对象(computed-watcher),这类 watcher 有个特点,我们拿下面的 b 举例:属性 b 依赖 a,当 a 改变的时候,b 并不会立即重新计算,而是之后其他地方读取 b 的时候,它才会真正计算,即具备 lazy(懒计算)特性。computed-watcher 会保存在 _watchers 中。

    {
        data () {
            return {
                a: 12
            }
        },
        computed: {
            b () {
                return this.a * 2
            }
        }
    }
    
  • watch:每一个 watch 属性,都会生成一个对应的 watcher 对象(normal-watcher / user-watcher), 只要监听的属性改变了,就会触发定义好的回调函数。normal-watcher 也会保存在 _watchers 中。

watch: { immediate: true }

现在有个需求,两个功能点:展示列表;有一个输入框可以筛选列表。代码有两种实现:

  1. 定义一个searchValue,searchValue 绑定输入框,在 created 中发送请求,watch searchValue,当它发生变化时,再去发送请求,代码如下:

    {
      data () {
        return {
          searchValue: ''
        }
      }
      watch: {
        searchValue(newVal, oldVal) {
          if (newVal !== oldVal) {
            this.getList()
          }
        }
      },
      methods: {
        getList() {}
      },
      created () {
        this.getList()
      }
    }
    
  2. 无需在 created 中发送请求,使用 watch immediate: true

    {
      data () {
        return {
          searchValue: ''
        }
      }
      watch: {
        searchValue: {
          handler: this.getList,
          immediate: true
        }
      },
      methods: {
        getList() {}
      },
    }
    

写法一是在初始化 watch 的时候(initWatch),不会执行 handler(默认 immediate: false),接下来执行钩子 created,在其中发送请求,之后 searchValue 有变化再去发送请求。

写法二设置了 immediate: true,所以在初始化 watch 时就会立即执行 handler,接下来再去调用钩子函数(如果有的话),之后 searchValue 有变化再去发送请求。

两种的差别在于第一次触发请求的地方,写法一是在 created 中,写法二是在 watch 中,写法二第一次请求触发的时间早于写法一。

参考

浅谈 vue 中的 watcher

探讨父组件和兄弟组件的生命周期

详解事件循环