【Vue不全记录】- Vue底层知识点汇集

233 阅读9分钟

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数据已经保持同步
  • beforeDestory

    • vue实例运行阶段到这里结束,进入销毁阶段
    • 实例上所有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

项目运用

  1. 新建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
        }
    }
})
  1. 在vue实例中注入store
// 省略其他
import store from './store'

new Vue({
    // 省略其他
    store // 注入Vuex实例
})
  1. 组件中使用
// 获取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'})
        }
    }
}