用代码认识Vue生命周期

1,426 阅读7分钟

Vue生命周期官方图示

文章对应的 github

直接整理总结

  1. 生命周期日志 - 正常结构和slot结构都是一样的
    • 生命周期打印
    • 父组件是在 beforeMount 创建子组件的
    • mounted 是在最里面的子组件依次往上返回的
  2. 从props、data、看生命周期
    • beforeCreate 是访问不到 propsdata
    • beforeCreate访问props上的属性会导致报错
    • 子组件在beforeMounted 访问不到父组件的DOM节点
    • mounted 就可以访问 DOM了,不管是自身的还是子组件的。
  3. 组件更新
    • 父组件数据更新,子组件使用了父组件数据,父子都触发hook了
    • 父组件数据更新,子组件没有依赖父组件数据,并不会触发hook
    • beforeUpdate拿到的是更新前的DOM
    • updated拿到的是更新后的DOM
    • 祖先与超两层的组件传数据这种组合,祖先更新,会使整个链条都触发hook
    • 只传抵方法且不依赖祖先数据,底层组件触发方法,只有祖先会触发hook
    • 上面的触发hook就算是全部触发,实际Repaint只会在对应的节点上。
  4. 销毁组件
    • beforeDestroydestroyed在Chrome和Firefox下并无区别,两者的DOM节点都从页面移除了,但节点引用依然可以访问
    • 以防万一,使用beforeDestroy较好

1. 初始化项目

1.1. 使用 @vue/cli 快速生成项目

1.2. 文件结构和代码

文件结构

代码如下 Root.vue One.vueTwo.vue 只是把所有生命周期都在控制台打印一下三个文件,大致相同所以不展示了。

具体可在 相关分支 查看

import One from './One.vue'

export default {
  name: 'root',
  components: {
    One
  },
  data() {
    console.log('root data') // One Two 就是对应修改了 root这个单词而已
    return {
      root: 'root'
    }
  },
  beforeCreate() {
    console.log('root beforeCreate')
  },
  created() {
    console.log('root created')
  },
  beforeMount() {
    console.log('root beforeMount')
  },
  mounted() {
    console.log('root mounted')
  },
  beforeUpdate() {
    console.log('root beforeUpdate')
  },
  updated() {
    console.log('root update')
  },
  beforeDestroy() {
    console.log('root beforeDestroy')
  },
  destroyed() {
    console.log('root destroyed')
  }
}

2. 生命周期调用顺序

2.1. 正常父子组件

引用关系

<!--Root-->
<div id="app">
    Root: <input type="text" v-model="root" />
    <One :root="root" />
</div>
<!-- One -->
<div>
    <h1>One: {{root}}</h1>
    <input type="text" v-model="one">
    <Two :root="root" :one="one" />
</div>
<!-- Two -->
<div>
    <h2>Two: {{root}} {{one}}</h2>
    <input type="text" v-model="two" />
</div>

2.1.1 运行结果

生命周期打印

2.1.2. 结论

  • 父组件是在 beforeMount 创建子组件的
  • mounted 是在最里面的子组件依次往上返回的

2.2. slot 父子组件

<!-- Root -->
<div id="app">
    Root:
    <input type="text" v-model="root" />
    <One :root="root">
        <!-- 作用域插槽 传递One的数据-->
        <template v-slot="scope">
            <Two :root="root" :one="scope.one" />
        </template>
    </One>
</div>

<!--One-->
<div>
    <h1>One: {{root}}</h1>
    <input type="text" v-model="one">
    <slot></slot>
</div>

<!--Two -->
<div>
    <h2>Two: {{root}} {{one}}</h2>
    <input type="text" v-model="two" />
</div>

作用域插槽 - 官方文档

2.2.1. 运行结果

slot

2.2.2. 结论

  • 虽然写法不同了,但是层级关系还是一样的。输出也和之前相同

3. 从props、data、看生命周期

template直接使用slot的代码

// One 变动的代码
<template>
    <!--给div 加上id-->
    <div id="one">
        <h1>One: {{root}}</h1>
        <input type="text" v-model="one" />
        <slot></slot>
    </div>
</template>
beforeCreate() {
    console.log('One beforeCreate')
    console.log('One beforeCreate data', this.one) 
    // console.log('One props', this.root) // 如果访问 props上的属性会报错
},
created() {
    console.log('One created')
    console.log('One created data', this.one)
    console.log('One created props', this.root)
},
beforeMount() {
    console.log('One beforeMount')
    console.log('One beforeMount DOM', window.one) // 直接通过DOM元素的id访问
},
mounted() {
    console.log('One mounted')
    console.log('One mounted DOM', window.one)
},

3.1. 运行结果

props、data、DOM

访问props内属性的报错信息

报错

3.2. 结论

  • beforeCreate 是访问不到 propsdata
  • beforeCreate访问props上的属性导致报错,从报错信息看到已经代理到了 undefined上,应该 this.root被劫持了,然后 访问其实是 this.props['root']就报错了
  • 子组件在beforeMounted 访问不到父组件的DOM节点
  • mounted 就可以访问 DOM了,不管是自身的还是子组件的。

这里有一个反模式,通过 this.$children 访问自身的子组件,效果是跟 访问data一样的, beforeCreate的时候返回 空数组

3.3. 需要解决的疑惑

  • 为什么 beforeCreate 之后才能访问到data
  • 为什么 组件 One的 this.root 访问会报错
  • mounted 的触发顺序, 直觉是依次出栈然后 触发hook的,尝试揪出源码

3.3.1. beforeCreate 之后才能访问到data

源码debugger

// One
beforeCreate() {
    debugger // 添加断点
    console.log('One beforeCreate')
    console.log('One beforeCreate data', this.one)
    // console.log('One props', this.root) // 如果访问 props上的属性会报错
},

devtool

beforeCreate

可以看到调用完 beforeCreate 这个钩子,才在 initState(vm)dataprops绑定在对应的组件上的

3.3.2. 子组件props代理时机(即直接访问this.key获取的是this.props.key

One为例。 在 render One组件的时候(Root beforeMonut hook调用 之后)

  1. 创建 One的 virtualDOM
  2. 根据 virtualDOM 创建Vue组件
function createComponent (Ctor,data,context,children,tag) {
  if (isUndef(Ctor)) {
    return
  }
  var baseCtor = context.$options._base; // 这里指的就是 Vue的这个构造函数 

  // 普通选项对象:将其转换为构造函数 (plain options object: turn it into a constructor)
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor);
  }
  // ... 其他代码
}

Vue.extend = function (extendOptions) {
  // ... 其他代码
  // For props and computed properties, we define the proxy getters on
  // the Vue instances at extension time, on the extended prototype. This
  // avoids Object.defineProperty calls for each instance created.
  if (Sub.options.props) {
    initProps$1(Sub);
  }
  // ... 其他代码
}

function initProps$1 (Comp) {
  var props = Comp.options.props;
  for (var key in props) {
    proxy(Comp.prototype, "_props", key);
  }
}

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);
}

所以在组件初始化之前(_init方法调用前),root已经挂到 One的原型上了

4. 组件更新

4.1. 父子组件 - 子组件渲染了父组件的数据

把data、props的打印清除掉,并且先不用 Two组件,现在我们只有Root,One

<!--Root-->
<div id="app">
    Root:
    <input type="text" v-model="root" />
    <One :root="root">
    <!-- <template v-slot="scope">
        <Two :root="root" :one="scope.one" />
    </template> -->
    </One>
</div>
<!--One-->
<div id="one">
    <h1>One: {{root}}</h1>
    <input type="text" v-model="one" />
</div>

在 hook 中打印组件的HTML

beforeUpdate() {
    console.log('root beforeUpdate', window.one.innerHTML)
},
updated() {
    console.log('root update', window.one.innerHTML)
},

One

我们在One中使用了Root的数据

现在修改root的数据

修改root的数据

打印结果

修改root的数据打印

符合预期的两个都触发hook了

beforeUpdate拿到的是更新前的DOM

updated拿到的是更新后的DOM

只修改One 当然只会更新 One组件;

4.2. 父子组件 - 子组件的数据和父组件无关

代码就不展示了

即使One 的 props接收了Root的数据,没有依赖props,更新root也不会使 One 触发hook

4.3. 祖先和孙子元素 - solt组合

One依然不使用Root的数据

<!--Root-->
<div id="app">
    Root:
    <input type="text" v-model="root" />
    <One :root="root">
        <template v-slot="scope">
            <Two :root="root" :one="scope.one" />
        </template>
    </One>
</div>

我们修改 Root的数据

TwoUpdated

One 也跟着触发hook了

4.3. 祖先和孙子元素 - 正常组合

代码,One不使用slot直接自身引入Two,中转Root的数据。One自身不渲染Root的数据

正常组合

结论

  • 祖先与超两层的组件传数据这种组合,祖先更新,会使整个链条都触发hook

4.4 父子组件 - 不传数据只传方法

<!--Root-->
<div id="app">
    Root:
    <input type="text" v-model="root" />
    <One @root="handleRoot"></One>
</div>
<!--
handleRoot() {
  this.root = 'one'
}
-->

<!--One-->
<div id="one">
    <h1 @click="$emit('root')">One:</h1>
    <input type="text" v-model="one" />
</div>

点击One的 h1.

点击h1

只有root触发hook

4.5 祖孙组件 - 不传数据只传方法

<!--One-->
<div id="one">
    <h1>One:</h1>
    <input type="text" v-model="one" />
    <Two @root="$emit('root')" />
</div>

<!--Two-->
<div id="two">
    <h2 @click="$emit('root')">Two: </h2>
    <input type="text" v-model="two" />
</div

点击 Two的h2

clickTwo

这里,组件没有数据变动,就没有触发hook,数据驱动更新。。。

4.6 Updated 的总结

这里的更新是指 hook的触发

而DOM的Repaint 只有在数据变动的节点出现

所以多层组件传值得消耗只有在js层面(创建组件,触发hook等)

destroyed

直接移除One,Two v-if 版

<!--Root-->
<div id="app">
    Root:
    <input type="text" v-model="root" />
    <One v-if="root" :root="root"></One>
</div>
// One
data() {
    console.log('One data')
    return {
      one: null
    }
},
mounted() {
    console.log('One mounted')
    this.one = window.one
},
beforeDestroy() {
    console.log('One beforeDestroy this.one', this.one.innerHTML)
    console.log('One beforeDestroy window.one', window.one)
},
destroyed() {
    console.log('One destroyed this.one', this.one.innerHTML)
    console.log('One destroyed window.one', window.one)
}

// Two
props: {
    root: String,
    one: HTMLDivElement
},
data() {
    console.log('Two data')
    return {
        two: 'two'
    }
},
beforeDestroy() {
    console.log('Two beforeDestroy this.one', this.one.innerHTML)
    console.log('Two beforeDestroy this.two', this.two)
    console.log('Two beforeDestroy window.two', window.two)
},
destroyed() {
    console.log('Two destroyed this.one', this.one.innerHTML)
    console.log('Two destroyed this.two', this.two)
    console.log('Two destroyed window.two', window.two)
}

清空root

des

root是更新,子组件销毁完最后才触发 updated

在beforeDestroy DOM已经从页面移除了,但是还在组件的引用还在

这里分不出区别。。。

两个hook依然可以访问this,Two的 destroyed 也还能访问One

官网- destroyed 的说法是

Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。

测试一下 One能不能在 destroyed 访问到Two

。。。

children

万脸懵逼,可能是浏览器(Chrome)的锅,经测试Firefox也是一样。

以防万一还是在beforeDestroy 做该做的吧

v-show 并不会触发 Destroyed,触发的是Updated,行为只是在节点上添加display:none