Vue的父子组件钩子函数执行顺序
//【加载渲染过程】
父beforeCreate -> 父created -> 父beforeMount -> 子beforeCreate -> 子created -> 子created -> 子beforeMount -> 子mounted -> 父mounted
//【子组件更新过程】
父beforeUpdate -> 子beforeUpdate -> 子updated -> 父updated
//【父组件更新过程】
父beforeUpdate -> 父updated
//【销毁过程】
父beforeDestory -> 子beforeDestory -> 子destoryed -> 父destoryed
【总结】
父组件先开始,子组件先结束
Vue中的data为什么是一个函数
【问题】
vue中的data为什么是个函数,而不是个对象?
【回答】
-
如果data是一个函数,数据以
函数返回值的形式定义,这样每复用一次组件,就会返回新的data- 类似给每个组件实例创建一个私有的数据空间,让各个组件实例维护自己的数据
-
而Object是
引用数据类型,里面保存的是内存地址,单纯写成对象形式,就使得所有组件实例共用了一份data(访问同一个内存地址),就会造成一个改变全部都会变的结果1. 组件是可以复用的vue实例,一个组件被创建好之后,就可能被用在各个地方 2. 组件不管被复用多少次,其中的data应该是相互隔离、互不影响的 3. 基于这个理念,组件每复用一次,data数据就应该被复制一次(new一个新的内存地址进行存储) 4. 这样无论组件被复用多少次,当一处修改data中的数据时,其他复用的data不受影响
【如果】
-
将data写成对象的形式
data() { return { count:0 } } 变成 data: { count: 0 } /* 那么无论在哪个复用组件中改变 count值,都会影响到其他复用组件里的 count 这是因为当 data定义后,所有复用组件实例都公用了一份 data数据。 因此,无论在哪个组件实例中修改data,都会影响到所有的组件实例 */
【延伸】
- 从
JS原型链角度解析
JavaScript只有函数构成作用域(函数作用域)。当data是一个函数时,每个组件实例都有自己的作用域。
这些都是JS本身的特性带来的,跟vue本身设计无关。
/*
只有函数的{}构成作用域,对象的{}以及if的{}都不构成作用域
因为 JS 中只有 【全局作用域】 和 【函数作用域】
*/
/*
【作用域】- 一个变量的作用范围。
限定一个变量的可用性的代码范围就是这个变量的作用域。
*/
【总结】
- 根实例对象的data可以是函数也可以对象,(根实例是单例,不存在复用的情况)
- 组件实例的data必须是函数
- 采用函数的形式,
initData时会将其作为工厂函数都会返回新的data对象
Vue的单向数据流
单向数据流 - 数据总是从父组件传递到子组件,子组件没有权限改变父组件传递过来的数据(只能请求父组件对原始数据进行更改)。
解决的问题 - 防止从子组件意外改变父组件的状态,从而导致应用的数据流向难以理解
- 如果非要改变父组件传递过来的数据
- 在子组件的data中定义一个变量,props的值初始化它,之后用$emit通知父组件更新这个值
Vue的生命周期函数
生命周期函数 - 相当于一种特殊事件,当vm实例在整个运行的过程中,会在不同的时期去执行特定的函数,这样的函数就是vue的生命周期函数。
-
beforeCreate- DOM元素、data数据和methods方法都没有初始化完成
-
created- DOM元素未初始化
- data数据和methods方法初始化完成
- 要操作data中的数据,调用methods中的方法,最早只能在这个函数中进行
-
beforeMount- DOM初始化完成,但是还未挂载到页面,保存在内存中。虚拟DOM
-
mounted- 完成虚拟DOM替换到真实DOM的过程,数据完成双向绑定
- 要操作页面上的DOM节点,最早要在这个钩子函数中进行
vue实例初始化过程(创建阶段)到这里结束,进入运行阶段
-
beforeUpdate- 页面显示的数据还是旧的,此时data的数据是最新的
- 页面数据尚未跟最新的data数据保持同步
-
updated- 页面和data数据已经保持同步
-
beforeDestoryvue实例运行阶段到这里结束,进入销毁阶段- 实例上所有data,methods以及过滤器... 都处于可用状态
-
destoryed- 组件已被完全销毁
- 实例上所有data,methods以及过滤器... 都不可用
beforeCreate created beforeMount mounted destory 这些钩子函数只执行一次
beforeUpdate updated 第一次构建不会被调用,以后每次data被更新就会调用
组件创建阶段的4个生命钩子:beforeCreate created beforeMount mounted
组件运行阶段的2个生命钩子:beforeUpdate updated
组件销毁阶段的2个生命钩子:beforeDestory destoryed
另外 组合式API中的 setup() 钩子会在所有钩子之前调用,beforeCreate()也不例外
如果非要在created中操作DOM元素,可以通过vm.$nextTick来访问DOM
created 与 beforeMount 有一个模板编译的过程。执行的是:
1. 把vue代码中的执行进行编译,在内存中生成一个编译好的模板字符串
2. 将模板字符串渲染成 内存中的DOM树
beforeUpdate 与 updated 中有一个【re-render and patch】过程。执行的是:
1. 先根据data中的最新数据,在内存中,【重新】一个最新的内存DOM树
2. DOM树渲染完成后,把最新的内存DOM树重新渲染到页面
3. 此时,完成了数据从 data(Model层) -> view(视图层)的更新
异步请求在哪一个函数中进行
【首先】 可以在created/beforeMount/mounted中进行,因为此时data已经创建,可以将服务器端返回的数据进行赋值操作。
【再者】 看是否依赖DOM元素
- 否,推荐在created中进行异步请求。
- 是,需要在mounted中进行异步请求
v-if 和 v-show的区别
- v-if 在编译过程会被转化为三元表达式,条件不满足时不渲染此节点。元素销毁和重建来控制显示和隐藏
- v-show 会被编译成指令,条件不满足时控制样式隐藏节点(
display:none)
【使用场景】
- v-if 适用于在运行时很少改变的条件,不需要频繁切换条件的场景
- v-show 适用于需要频繁切换条件的场景
【拓展】
display:none visibility:hidden opacity:0 之间的区别
【共同点】都是隐藏节点
【不同点】
1.是否占据空间
display:none 不占据空间,其余的都占据空间
2.子元素是否继承隐藏
display:none 不会被子元素继承(父元素都没了,哪里还会有子元素)
visibility:hidden 会被子元素继承。子元素可通过visibility:visible来显示
opacity:0 会被子元素继承。但是子元素设置opacity:1也不会显示
3.事件绑定
display:none 的元素已不存在了,因此无法触发它绑定的事件
visibility:hidden 的元素不会触发其绑定的事件
opacity:0 的元素的事件可以触发
4.过渡动画 - transition
display 无效
visibility 无效
opacity 有效
【性能比较】
display切换时会产生DOM的回流(回流:当页面中的一部分元素需要改变规模尺寸、布局、显示隐藏等,页面重新构建,此时就是回流。所有页面第一次加载时需要产生一次回流)。其他两个不会产生回流。
v-if 和 v-for 为什么不建议同时在一个标签中使用
带来的问题
因为在解析时,先解析v-for,再解析v-if。这样会带来性能上的浪费。(每次渲染都会先循环再执行条件判断)
解决的方式
-
嵌套一个 template,在这里做
v-if判断<template v-if="isShow"> <p v-for="item in items"></p> </template> -
可以通过计算属性提前过滤掉数据中不需要显示的项
computed 和 watch的区别
-
功能上
- computed是计算属性
- watch是监听一个值的变化,然后执行对应的回调
-
是否调用缓存
- computed中函数所依赖的属性没有发生变化,那么值从缓存中读取
- watch在每次监听的值发生变化都会执行回调
-
第一次调用
- computed默认第一次加载时就开始监听
- watch默认第一次加载不做监听
- 如果需要第一次加载就进行监听,则需要加
immediate属性,值为true(immediate:true)
- 如果需要第一次加载就进行监听,则需要加
-
是否有return
- computed必须有return
- watch没有要求
-
使用场景
- computed - 当一个属性受多个属性影响的时候
- watch - 当一条数据影响多条数据的时候
【语法】
==【computed】==
// 简单写法
computed: {
计算属性名() {
return 值
}
}
// 完整写法
computed: {
计算属性名() {
set(val) {},
get() {return 值}
}
}
// 当计算属性被修改时,会触发set方法,并将修改后的值传递过来
==【watch】==
// 简单写法
watch: {
要监听的数据名(newVal, oldVal) {
// 当数据变化时,函数调用
}
}
// 完整写法
watch: {
要监听的数据名: {
deep: true, // 深度监听
handle(newVal, oldVal) {
// 当数据变化时,函数调用
}
}
}
【总结】
- computed支持数据缓存,相关依赖的数据发生改变才会重新计算。watch不支持缓存,只要监听的数据变化就会触发相关操作。
- computed属性的属性值是函数,必须有返回值。watch监听的数据必须是data中声明过或父组件传递过来的props中的数据,当数据发生变化时,触发监听器。
v-model双向绑定的原理
- 本质
- v-bind绑定一个value(text)/checked(radio/checkbox)/value(select)属性
- v-on指令给当前元素绑定input/change/change事件
<input type="text" :value="msg" @input="msg=$event.target.value"/>
<input type="text" v-model="msg"/>
Vue图片懒加载
依赖安装
npm i vue-lazyload -D
用法
- 入口文件
main.js做引入
import Vue from 'vue'
import App from './App.vue'
import VueLazyload from 'vue-lazyload' // 引入懒加载依赖
Vue.use(VueLazyload, {
preload: 1, // 预加载高度比例
error: require(''),// 图片路径错误时加载图片url
loading: require(''), // 预加载图片url
attempt: 2, // 尝试加载图片数量
})
- 其他使用img的组件
<template>
<!-- img中使用图片懒加载。v-lazy代替v-bind:src -->
<img v-lazy="图片地址" alt=""/>
<!-- 背景图中使用图片懒加载。v-lazy:background-image="" -->
<div v-lazy:background-image=""></div>
</template>
Vue路由懒加载
为什么进行路由懒加载
路由懒加载将页面进行划分,进行按需加载,可以有效减少首页加载时长
实现路由懒加载的三种方式
- Vue异步组件
- ES6的import() -- 推荐
- webpack的require.ensure()
Vue异步组件
1. vue-router配置路由,使用vue的异步组件技术,一个组件会生成一个js文件
2. component:resolve => require(['需要加载的路由地址'], resolve)
{
name: 'home',
path: '/home',
component: resolve => require(['../pages/home], resolve)
}
ES6的import()
1. ES6语法,需要兼容浏览器,则需要转化为es5
2. webpack版本 > 2.4
const Home = () => import('../pages/home')
{
name: 'home',
path: '/home',
component: Home
}
webpack的require.ensure()
1. 多个路由指定相同名称,会合并打包成一个.js文件
2. require.ensure(
dependencies: String[],
callback: function(require),
errorCallback: function(error),
chunkName: String
)
- 第一个参数是字符串数组。声明需要的模块
- 第二个参数是回调函数。
- 第三个参数是错误回调
- 第四个参数是chunk名称
const Home = resolve => {
require.ensure([],()=>r(require('@/pages/home')),'home'
)
}
=> import实现
const Home = () => import(/*webpackChunkName: 'home'*/ '@/pages/home')
{
name: 'home',
path: '/home',
component: Home
}
nextTick的理解
为什么需要nextTick
因为Vue对DOM的更新采用【异步更新】策略。
// 异步更新
当监听到数据发生变化时不会立即更新DOM
而是开启一个任务队列,缓存同一事件循环中发生所有数据变更
// 好处
将多次数据更新合并成一次,减少DOM的更新次数,提高性能
使用场景
想要操作最新数据生成的DOM时,就将这个操作放在nextTick的回调函数中
<template>
<div ref="msgRef">{{ msg }}<div>
<div>{{ msg1 }}</div>
<button @click="updData">更新数据</button>
<template>
<script>
export default {
data() {
return {
msg: 'hello world'
msg1: ''
}
},
methods: {
updData() {
// 此时页面渲染的msg数据还是 hello world
this.msg = 'Hello vue'
this.nextTick(() => {
// 此时页面渲染的msg1数据变成 hello vue
this.msg1 = this.$ref.msgRef.innerHTML
})
}
}
}
</script>
//
nextTick实现原理
next接收一个回调函数作为参数,并将这个回调延迟到DOM更新后才执行。
将回调函数包装成异步任务。为了尽快执行所以优先选择了微任务。
nextTick提供了四个异步方法:Promise.then / MutationObserver / setImmediate / setTimeout(fn, 0)
以此按照这个顺序进行异步方法的选择
Vuex的理解
vuex是什么
vuex是vue的状态管理器。采用集中式存储管理应用的所有组件的状态。 以此来实现多组件的数据通信
【疑问】
关于多组件通信,有人就要问了。
vue不是也推出了`事件中心`Bus来解决跨/兄弟组件之间的通信吗?
【解答】
在大型项目中,组件很多,组件间的通信也免不了,使用事件中心容易导致代码繁琐,代码阅读和维护都变得不容易
语法
inport Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
// 存储数据(跟data类似)- 常用
state: {},
// 用来个修改state和getter里面的数据 - 常用
mutation: {},
// 相当于计算属性
getters: {},
// vuex中用于发起异步请求
actions: {},
// 拆分模块
modules: {}
})
export default store
项目运用
- 新建store文件夹,index.js文件
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex({
state:{
count: 0
},
mutations: {
setCount(state, newVal) {
state.count = newVal
}
}
})
- 在vue实例中注入store
// 省略其他
import store from './store'
new Vue({
// 省略其他
store // 注入Vuex实例
})
- 组件中使用
// 获取Vuex中state的值
this.$store.state.count
// 修改Vuex中的值
this.$store.commit('setCount', 1) // 通过commit触发mutations中事件
Vuex页面刷新数据丢失怎么解决
vuex数据持久化,一般使用本地存储的方案 进行数据的保存-
保存到浏览器缓存中(sessionStorage/localStorage/cookie)
-
可以借助第三方插件 vuex-persist
npm i vuex-persist// store -> index.js import VuexPersistence const VuexLocal = new VuexPersistence({ storage: window.localStorage }) const store = new Vuex.Store({ // 省略其他 plugins: [VuexLocal.plugin] })
-
keep-alive的理解
作用
作为一种vue的内置组件,keep-alive主要作用是缓存组件状态。当需要组件的切换,不用重新渲染组件,避免多次渲染,就可以使用keep-alive包裹组件。
props
include:字符串或正则,只有名称匹配的组件会被缓存exclude:字符串或正则,任何匹配的名称都不会被缓存max:数字,最大的缓存组件数量
使用
App.vue
<template>
<div id="id">
<keep-alive>
// 缓存name为 home 的组件
// <router-view include="home"/>
// 缓存name为 a 或 b 的组件
// <router-view include="a,b"/>
// 正则,需要使用 v-bind
// <router-view :include="/a|b"/>
// 条件判断
// <router-view v-if="$route.meta.keepAlive"/>
</keep-alive>
</div>
</template>
原理
文件位置:src/core/components/keep-alive.js
export default {
name: 'keep-alive',
...
created () {
this.cache = Object.create(null) // 创建缓存列表
this.keys = [] // 创建缓存组件的key列表
},
destroyed () {
for (const key in this.cache) {
// keep-alive销毁时,循环清空所有的缓存和key
pruneCacheEntry(this.cache, key, this.keys)
}
},
...
}
在created中创建缓存列表和组件的key列表(如果没有指定,则会自动生成一个唯一的key值)。销毁时执行一个循环清零所有的缓存和key。
基本逻辑是
- 判断当前渲染的vnode是否有对应的缓存
- 有,读取缓存中对应的组件实例
- 无,将其缓存 如果缓存超过max,则移除key数组中第一个元素(因为其中运用到一个算法-LRU。LRU删除最久未使用的缓存,将当前使用的组件缓存后移,最前面就是最久未使用的)
Vue.set方法
使用场景
- 在实例创建之后添加新的属性到实例上(给响应式对象新增属性)
- 通过更改数组下标来修改数组的值
项目运用
export default {
data() {
return {
lists:[
{id:1, name:'1'},
{id:2, name:'2'}
]
}
},
methods: {
addList() {
/*
操作的数据 下标 新对象
*/
this.$set(this.lists, 0, {id:3, name:'3'})
}
}
}