Vue2基础知识

299 阅读10分钟

vue原理:

vue是什么?
它是基于MVVM模型设计的数据驱动视图的响应式框架。

vue响应式原理?

如果只是写普通的html页面向下面这样当我点击按钮时改变对象的属性值,页面其实是不会发生变化的。但是对象的属性值其实已经变了。 这并没有像我们希望的那样页面上从old变为new,这是因为这时还不具有响应式这种能力。

<body>
  <div>
    <span class="text"></span>
    <button class="button" onclick="buttonHandle()">按钮</button>
  </div>
  <script>
    const obj = document.querySelector('.text')
    const showObj = {name: 'old'}
    obj.innerHTML = showObj.name
    function buttonHandle () {
      showObj.name = 'new'
    }
    console.log(showObj.name);  // new,但页面上仍是old
  </script>
</body>

核心api:Object.difineProperty,让我们拥有了响应式的能力。(也就是从old变为new的能力)

1.生命周期:

一个对象从生成(new)到被销毁(destory)的过程,称为生命周期。而生命周期函数,就是在某个时刻会自动执行的函数。

生命周期描述
beforeCreate(初始化界面前)vue 实例刚刚在内存中创建,data 和 methods不可使用
created(初始化界面后)vue 实例在内存中已经创建好了,data 和 methods 可用,模板还未编译
beforeMount(渲染dom前)这个阶段完成了模板的编译,已挂载dom,dom未渲染
mounted(渲染dom后)这个阶段,模板编译完成,已挂载dom,dom渲染完成
beforeUpdate(更新数据前)数据更新之前执行此函数,此时 data 中数据的状态值已经更新为最新的,但是页面上显示的数据还是最原始的,还没有重新开始渲染 DOM 树。
updated(更新数据后)这个阶段是转态更新完成后执行此函数,此时 data 中数据的状态值是最新的,而且页面上显示的数据也是最新的,DOM 节点已经被重新渲染了。
beforeDestroy(卸载组件前)beforeDestroy 阶段处于 vue 实例被销毁之前,当然,这个阶段 vue 实例还能用
destroyed(卸载组件前)这个阶段在 vue 实例销毁后调用,此时所有实例指示的所有东西都会解除绑定,事件监听器也都移除,子实例也被销毁。

推荐在 created 钩子函数中调用异步请求:

  • 能更快获取到服务端数据,减少页面 loading 时间;
  • ssr 不支持 beforeMount 、mounted 钩子函数,所以放在 created 中有助于一致性;

2.封装组件

graph TD
定义子组件名称 --> 定义子组件props接收数据 --> 父组件引入子组件 --> 注册子组件 --> 模板调用子组件

3.组件之间传值

传值类型方法
父向子父组件通过动态属性绑定的方式传值到子组件,子组件通过props接收
子向父子组件通过$emit自定义事件,父组件监听子组件定义的事件来传值
同级传值通过刀了emit传值,eventBus('add',this.name),通过刀了on接收 eventBus.$on('add',(message)=>{ })

4.Vuex

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
Vuex是实现组件全局状态(数据)管理的一种机制,可以方便的实现组件之间数据的共享。

  • State:定义了应用状态的数据结构,可以在这里设置默认的初始状态。

  • Getter:允许组件从 Store 中获取数据,mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性。

  • Mutation:是唯一更改 store 中状态的方法,且必须是同步函数。

  • Action:用于提交 mutation,而不是直接变更状态,可以包含任意异步操作。

  • Module:允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中。

state
//组件访问State 中数据的第一种方式:
创建store数据源,提供唯一公共数据
const store = new vuex.store ({
    state: { count: 0 }
})

组件访问State 中数据的第一种方式:
this.$store.state.全局数据名称

组件访问State 中数据的第二种方式:
//1.从vuex中按需导入mapstate函数
    import { mapState } from 'vuex'
通过刚才导入的mapState函数,将当前组件需要的全局数据,映射为当前组件的computed计算属性:
//2.将全局数据,映射为当前组件的计算属性
    computed: {
        ...mapState ([ 'count' ])
    }
mutation
Mutation用于变更Store中的数据。
只能通过mutation变更Store数据,不可以直接操作Store 中的数据。
通过这种方式虽然操作起来稍微繁琐一些,但是可以集中监控所有数据的变化。
//定义Mutation
const store = new vuex.store ( {
    state: {
    count: 0
    },
    mutations: {
        add(state,step) {//变更状态
           state.count += step
        }
    }
})


//触发mutations的第一种方式
methods: {
    handle1() {           
       this.$store.commit ( 'add' , 3 )
    }
}

//组件访问State 中数据的第二种方式:
//1.从vuex中按需导入mapmutations函数
    import { mapMutations } from 'vuex'
//通过刚才导入的mapmutations函数,将当前组件需要的全局数据,映射为当前组件的methods方法:
//2.将全局数据,映射为当前组件的计算属性
    methods: {
        ...mapmutations ([ 'add' ])
    }
action
Action用于处理异步任务。
如果通过异步操作变更数据,必须通过Action,而不能使用Mutation,但是在Action中还是要通过触发Mutation的方式间接变更数据。
//定义Action
const store = new vuex.store ({
    //...省略其他代码
    mutations: {
        add (state,step) {
            state.count += step
        }
    },
    actions: {
        addAsync(context) {   //接受context形参调用mutation里面的方法
            setTimeout ( () => {
                context.commit ( ' add ' )}, 1000)
            }        
    }
})

//触发action的第一种方式:
test(){
    this.$store.dispatch('addAsync',3)
}

//触发action的第二种方式:
//1.从vuex中按需导入mapaction函数
    import { mapAction } from 'vuex'
//通过刚才导入的mapaction函数,将当前组件需要的全局数据,映射为当前组件的methods方法:
//2.将全局数据,映射为当前组件的计算属性
    methods: {
        ...mapaction ([ 'add' ])
    }
getter
Getter 用于对 Store 中的数据进行加工处理形成新的数据。
Getter可以对Store 中已有的数据加工处理之后形成新的数据,类似Vue的计算属性。Store中数据发生变化,Getter的数据也会跟着变化。
//定义Getter
const store = new vuex.store ({
    state: {
        count: 0
    },
    getters:{
        showNum: state =>{
            return '当前最新的数量是【'+ state.count +'】'
        }
    }
})



使用getters的第一种方式:
this.$store.getters.名称

使用getters的第二种方式:
import { mapGetters } from 'vuex'
computed: {
    ...mapgetters ( [ ' showNum' ] )
}
Vuex持久化:

更改store文件下index文件state的定义

const store = new Vuex.Store({
    state:sessionStorage.getItem('state') ? JSON.parse(sessionStorage.getItem('state')): {name:""}
})

在APP.vue中添加监听unload方法,如果重载页面就把state存入sessionStorage,然后在需要state的时候从sessionStorage中取值。

mounted() {
            window.addEventListener('unload', this.saveState)
        },
        methods: {
            saveState() {
                sessionStorage.setItem('state', JSON.stringify(this.$store.state))
            }
        }

5.Router

路由:路由的本质就是建立url和组件之间的映射关系

hash模式是vue-router的默认模式:
hash指的是url锚点,当锚点发生变化的时候,浏览器只会修改访问历史记录,不会访问服务器重新获取页面。window.onhashchange这个事件监听到了hash的变化,从而触发了组件的更新,也就绘制出了相应的页面。

history模式:
pushState方法、replaceState方法,只能导致history对象发生变化修改活动历史记录条目,从而改变当前地址栏的 URL,但浏览器不会向后端发送请求。 当活动历史记录条目更改时,将触发popstate事件载入对应页面组件,完成跳转。

import Vue from 'vue'
// 引入路由插件
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
// 初始化路由
Vue.use(VueRouter)
// 路由具体配置
const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  }
]
//  创建路由实例
const router = new VueRouter({
  routes
})

// 导出路由插件
export default router
动态路由匹配:

就是将不确定的参数进行路由映射到同一个组件上去;同一个组件不同的参数,渲染不同的数据。

url="/user/:username"
编程式导航:
// query使用path和name传参跳转都可以,而params只能使用name传参跳转。
// params传参若是路由上面不写参数,也是能够传过去的,但不会在url上面显示出你的参数,而且当你跳到别的页面或者刷新页面的时候参数会丢失

goUser(id) {
    this.$router.push({
        name : 'User ' ,
        params : {id: this.id}
    })
},
goUser2(id) {
    this.$router.push({
        path : '/user ',
        query : {id: this.id}
    })
},
嵌套路由
{
path: '/ user/ :id' ,       //父路由
name: 'User' ,
component : User,
    children : [
        {
            path : 'profile ',       //子路由,路径不需加/
            component : UserProfile},
        {
            path : 'posts ' ,
            component : UserPosts},
    ]
}

router-view

不同的组件在不同的区域渲染,渲染多个组件,

<router-view name="sidebar">< / router-view>
<router-view/>
<router-view name="footer"></router-view>
{
    path:'/',
    name:'Home',
    components:{
        default:Home,
        sudebar:Sidebar,
        footer:Footer
    }
}
路由重定向
{
    path: '/a ' ,

    //redirect : '/list'

    redirect : to => {
    if (true) i
    return {
        name : 'List'
        }
    }
}
路由导航守卫

导航守卫的作用就是跳转或取消跳转的导航功能,比登录跳转方案;

全局:

to: 即将要进入的目标
from: 当前导航正要离开的路由
next: Function: 一定要调用该方法来 resolve这个钩子。执行效果依赖 `next` 方法的调用参数。

router.beforeEach((to, from, next) => {
  // ...
})

router.afterEach((to, from) => {
  // ...
})

单个路由独享的:

const router = new VueRouter({
  routes: [
    {
      path: '/foo',
      component: Foo,
      beforeEnter: (to, from, next) => {
        // ...
      }
    }
  ]
})

组件内的守卫:

const Foo = {
  template: `...`,
  beforeRouteEnter(to, from, next) {
     next(vm => {
        // 通过 `vm` 访问组件实例
      })
    // 在渲染该组件的对应路由被 confirm 前调用
    // 不!能!获取组件实例 `this`
    // 因为当守卫执行前,组件实例还没被创建
  },
  beforeRouteUpdate(to, from, next) {
    // 在当前路由改变,但是该组件被复用时调用
    // 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
    // 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
    // 可以访问组件实例 `this`
  },
  beforeRouteLeave(to, from, next) {
    // 导航离开该组件的对应路由时调用
    // 可以访问组件实例 `this`
  }
}
路由滚动记忆位置
const router = new VueRouter( options: {
    mode: 'history ',
    routes,
    scrollBehavior(to, from) {
    return {
    x : 10,
    y : 1p
    }

}



PS:当我们进入新路由组件时,会自动top;当然,还支持前进/后退模式以及返回异步;
scrollBehavior (tofrom,savedPosition) {
    if (savedPosition) {
        return savedPosition
    /保存之前的位置
    } else{
    return { x: 0,y: 0 }
        }
}
路由懒加载

路由懒加载:即当加载这个路由的时候再加载这个组件,提高运行的效率。

{
    path : ' /about ' ,
    name: 'About ' ,
    component: () => import( ' ../views /About.vue ' )
}

{
    path : ' /about ' ,
    name: 'About ' ,
    component: () => import( ' @/views /About.vue ' )
}

6.Axios 

ajax原理:
1.先实例化一个XMLHttpRequest对象
new XMLHttpRequest
2.通过该对象的open()方法与服务器建立连接
open(method:'post', url:'')
3.通过该对象的send()方法将入参发送给服务器
send(body:{})
4.通过该对象的onreadystatechange监听来监听服务器端的通信状态获得数据渲染页面
readyState:
0:open()未调用
1:send()未调用
2:send()已调用响应头和响应体已经返回
3:响应体正在下载responseText获取到部分数据
4:整个请求过程已经完毕,获取到全部数据

基本使用
axios({
    method: 'GET',
    url: 'http://localhost:3000/posts/2',
}).then(response =>{console.log(response)});


axios({
    method: 'POST',
    url: 'http://localhost:3000/posts',
    data:{
        tital:"价格价格改革"author:"挂号费"
    }
}).then(response =>{console.log(response);});


axios({
    method: 'PUT',
    url: 'http://localhost:3000/posts/5',
    data:{
        tital:"价格价格改革"author:"规范和工程"    //修改后
    }
}).then(response =>{console.log(response)});


axios({
    method: 'DELETE',
    url: 'http://localhost:3000/posts/2',
}).then(response =>{console.log(response)});

axios响应数据分析

响应报文:响应行,响应头,相应空行,响应体

config:配置对象,包括请求类型、请求体、请求头信息

data:响应体

headers:响应头信息

status:响应状态码

status Text:相应状态字符串信息

默认配置

axios.defaults.baseUrl='  '

axios.defaults.timeout=3000

创建实例对象
//创建实例对象
const http1 = axios.create({
    baseURL:'https: /lapi.apiopen.top',
    timeout: 2000
});
const http2 = axios.create(i
    baseURL: 'https: //b.com' ,
    timeout: 2000
});
拦截器

在请求或响应被 then 或 catch 处理前拦截它们。

请求拦截器:发送请求之前做的逻辑处理和检测。

响应拦截器:在处理得到的数据之前,做的预处理。

axios.interceptors.request.use(function (config) {
    console.log('请求拦截器成功-2号');
    //修改config中的参数
    config.timeout = 2000;
    return config;
    , function (error) i
    console.log('请求拦截器失败-2号');
    return Promise.reject(error);
});


axios.interceptors.response.use(function (response) {
    console.log('响应拦截器成功1号');
    return response.data;
    }, function (error) i
        console.log('响应拦截器失败1号')
        return Promise.reject(error);
});

7.Vue指令

v-if与v-show

v-if 是真正的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。

v-show 就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 的 “display” 属性进行切换。

所以,v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景;v-show 则适用于需要非常频繁切换条件的场景。

class 与 style 动态绑定
<div v-bind:class="{ active: isActive, 'text-danger': hasError }"></div>

data: {
  isActive: true,
  hasError: false
}

<div v-bind:class="[isActive ? activeClass : '', errorClass]"></div>

data: {
  activeClass: 'active',
  errorClass: 'text-danger'
}


<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>

data: {
  activeColor: 'red',
  fontSize: 30
}

<div :style="[styleColor, styleSize]"></div>

data: {
  styleColor: {
     color: 'red'
   },
  styleSize:{
     fontSize:'23px'
  }
}

v-model

我们在 vue 项目中主要使用 v-model 指令在表单 input、textarea、select 等元素上创建双向数据绑定,我们知道 v-model 本质上不过是语法糖,v-model 在内部为不同的输入元素使用不同的属性并抛出不同的事件

//语法糖写法 
<input type="text" v-model="name" > 

//还原为以下实例 
<input type="text" v-bind:value="name" v-on:input="name=$event.target.value">
v-once

定义它的元素或组件只渲染一次,包括元素或组件的所有子节点,首次渲染后不再随数据的变化而重新渲染将被视为静态内容。

v-if 与v-for

v-for 比 v-if 优先级高 v-for 和 v-if 不要在同一个标签中使用,因为解析时先解析 v-for 再解析 v-if,所以是达不到先判断再循环的作用。如果遇到需要同时使用时可以考虑将列表数组写成计算属性的方式,直接渲染过滤后的数据。

8.computed与watch

computed: 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;
当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算;

watch: 更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作;
当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。

computed:{
    //可以利用闭包的方式拿到for循环的当前数据,模板中一切复杂逻辑都应该用计算属性。
    stepFaBoxH() {
      return function (item) {
        if (item.actFlag) {
          return "stepFaBoxRetractClass";
        } else {
          return "stepFaBoxShowClass";
        }
      };
    },
}

9. 为什么 data 是一个函数

因为组件是用来复用的,且 JS 里对象是引用关系,如果组件中 data 是一个对象,那么这样作用域没有隔离,子组件中的 data 属性值会相互影响,如果组件中 data 选项是一个函数,那么每个实例可以维护一份被返回对象的独立的拷贝,组件实例之间的 data 属性值不会互相影响;而 new Vue 的实例,是不会被复用的,因此不存在引用对象的问题。
组件中的 data 写成一个函数,数据以函数返回值形式定义,这样每复用一次组件,就会返回一份新的 data,类似于给每个组件实例创建一个私有的数据空间,让各个组件实例维护各自的数据。而单纯的写成对象形式,就使得所有组件实例共用了一份 data,就会造成一个变了全都会变的结果.(简单来说就是一句话:避免组件之间数据相互污染)

10.MVC 和 MVVM 区别

MVC
全名是 Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范

  • Model(模型):是应用程序中用于处理应用程序数据逻辑的部分。通常模型对象负责在数据库中存取数据
  • View(视图):是应用程序中处理数据显示的部分。通常视图是依据模型数据创建的
  • Controller(控制器):是应用程序中处理用户交互的部分。通常控制器负责从视图读取数据,控制用户输入,并向模型发送数据

MVC 的思想:一句话描述就是 Controller 负责将 Model 的数据用 View 显示出来,换句话说就是在 Controller 里面把 Model 的数据赋值给 View。

MVVM

MVVM是Model-View-ViewModel缩写,也就是把MVC中的Controller演变成ViewModel。Model层代表数据模型,View代表UI组件,ViewModel是View和Model层的桥梁,数据会绑定到viewModel层并自动将数据渲染到页面中,视图变化的时候会通知viewModel层更新数据。

  • ViewModel 层:做了两件事达到了数据的双向绑定 一是将【模型】转化成【视图】,即将后端传递的数据转化成所看到的页面。实现的方式是:数据绑定。二是将【视图】转化成【模型】,即将所看到的页面转化成后端的数据。实现的方式是:DOM 事件监听。

MVVM 与 MVC 最大的区别就是:它实现了 View 和 Model 的自动同步,也就是当 Model 的属性改变时,我们不用再自己手动操作 Dom 元素,来改变 View 的显示,而是改变属性后该属性对应 View 层显示会自动改变(对应Vue数据驱动的思想)

整体看来,MVVM 比 MVC 精简很多,不仅简化了业务与界面的依赖,还解决了数据频繁更新的问题,不用再用选择器操作 DOM 元素。因为在 MVVM 中,View 不知道 Model 的存在,Model 和 ViewModel 也观察不到 View,这种低耦合模式提高代码的可重用性

注意:Vue 并没有完全遵循 MVVM 的思想 这一点官网自己也有说明 。 严格的 MVVM 要求 View 不能和 Model 直接通信,而 Vue 提供了$refs 这个属性,让 Model 可以直接操作 View,违反了这一规定,所以说 Vue 没有完全遵循 MVVM。

11.怎样理解 Vue 的单向数据流

数据总是从父组件传到子组件,子组件没有权利修改父组件传过来的数据,只能请求父组件对原始数据进行修改。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。

注意:在子组件直接用 v-model 绑定父组件传过来的 prop 这样是不规范的写法 开发环境会报警告

如果实在要改变父组件的 prop 值 可以再 data 里面定义一个变量 并用 prop 的值初始化它 之后用$emit 通知父组件去修改

<comp :foo.sync="bar"></comp>

<comp :foo="bar" @update:foo="val => bar = val"></comp>

向子组件传数据时利用.sync语法糖,子组件方法中通过$emit('update:foo', newValue),更新父组件传过来的数据。

12.nextTick 使用场景和原理

nextTick:在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。

点击按钮显示原本以 v-show = false 隐藏起来的输入框,并获取焦点。

showsou(){
  this.showit = true //修改 v-show
  document.getElementById("keywords").focus()  //在第一个 tick 里,获取不到输入框,自然也获取不到焦点
}


showsou(){
  this.showit = true
  this.$nextTick(function () {
    // DOM 更新了
    document.getElementById("keywords").focus()
  })
}

13.slot插槽

父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。

语法糖:把参数之前的所有内容 (v-slot:) 替换为字符 #

匿名插槽
//home.vue
<test>
     Hello Word
</test>

//test.vue
<a href="#">
	 <slot></slot>
</a>
当组件渲染的时候,<slot></slot>会被替换为Hello Word
如果`<test>`中没有包含一个`<slot>`元素,则该组件起始标签和结束标签之间的任何内容都会被抛弃。
具名插槽
//子组件
<div>
  <header>
    <slot name="header"></slot>
  </header>
  
  <main>
    <slot></slot>
  </main>
  
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

//父组件
<div>
   <template v-slot:header>
    <h1>Here might be a page title</h1>
   </template>

  <p>A paragraph for the main content.</p>
  <p>And another one.</p>

  <template v-slot:footer>
    <p>Here some contact info</p>
  </template>
</div>
现在 `<template>` 元素中的所有内容都将会被传入相应的插槽。
任何没有被包裹在带有 `v-slot` 的 `<template>` 中的内容都会被视为默认插槽的内容。

我们可以通过 `v-slot:[dynamicSlotName]`*(可以在data中定义dynamicSlotName)方式动态绑定一个名称;
作用域插槽

作用域插槽作用:父组件通过v-slot访问子组件的数据(跨作用域访问数据)

//父组件
<template>
  <div>
    <show-names :names="names">
      <template v-slot:default="slotProps">
        <span>{{slotProps.item}}-{{slotProps.index}}</span>
      </template>
    </show-names>
  </div>
</template>

<script>
  import ShowNames from './ShowNames.vue';

  export default {
    components: {
      ShowNames,
    },
    data() {
      return {
        names: ["why", "kobe", "james", "curry"]
      }
    }
  }
</script>

//子组件
<template>
  <div>
    <template v-for="(item, index) in names" :key="item">
      <!-- 插槽prop -->
      <slot :item="item" :index="index"></slot>
    </template>
  </div>
</template>

<script>
  export default {
    props: {
      names: {
        type: Array,
        default: () => []
      }
    }
  }
</script>



##### Vue渲染中, 核心关键的几步是

-   `new Vue`, 执行初始化
-   挂载`$mount`, 通过自定义`render`方法, `template`, `el`等生成`render`渲染函数
-   通过`Watcher`监听数据的变化
-   当数据发生变化时, `render`函数执行生成VNode对象
-   通过`patch`方法, 对比新旧VNode对象, 通过`DOM Diff`算法, 添加/修改/删除真正的DOM元素

至此, 整个`new Vue`渲染过程完成.

##### nextTick
Vue.nextTick([callback,context])
在下次DOM更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,**获取更新之后的DOM**。