Vue知识零散学习(面试篇)

此文章仅记录学习Vue中一些平常自己没有去学习到知识,很多东西都是基于自我的认知去写的。文中可能会有理解错误的地方。但大多数都是参考Vue官网学习记录,有的部分也只是仅作了解,后续有需要会更深入学习。
一般情况下会持续更新。

目录:

动态组件
异步组件
路由懒加载
nextTick
混入
组件缓存(keep-alive)
插槽(slot)
过滤器(filter)
自定义指令
插件
几个API
风格指南
Vue CLI
Vuex
Vue响应式原理
Vue中的虚拟DOM以及Diff算法理解
Vue-Router

动态组件

让多个组件使用同一个挂载点,并动态切换,这就是动态组件。
通过使用保留的<component>元素,动态地绑定到它的is特性,可以实现动态组件。

<component :is="componentA"></component>

异步组件

异步组件就是在定义的时候什么都不做,只在组件渲染的时候进行加载渲染并缓存,缓存是以备下次访问。

路由懒加载

vue-router配置路由,实现按需加载方式

  1. webpack的代码分割(异步组件技术)
    特点:一个组件生成一个js文件
{
  path:"/",
  name:"home",
  component:resolve => require(["@/components/home"],resolve)
}
  1. webpack2 + ES6(推荐使用)
    特点:
    没有指定webpackChunkName,每个组件打包成一个js文件。
    指定了相同的webpackChunkName(使用注释的方式),会合并打包成一个js文件。
const home = ()=>import("@/components/home");
const mine = ()=>import(/* webpackChunkName:"mine" */ "@/components/mine");
  1. webpack提供的require.ensure()
    特点:多个路由指定相同的chunkName,会合并打包成一个js文件
{
  path:"/",
  name:"home",
  component: r => require.ensure([],()=> r(require(''/components/home)),"home")
}

nextTick

Vue实现响应式并不是在数据改变后就立即更新DOM,而是在一次事件循环的所有数据变化后再异步执行DOM更新.
而Vue.nextTick()就是在DOM更新之后触发的方法。
nextTick的触发时机就是
一次事件循环中的代码执行完毕=>DOM更新=>触发nextTick的回调=>进入下一次循环。
例子:

<template>
  <div class="home">
    <div class="app">
      <div ref="contentDiv">{{content}}</div>
      <div>在nextTick执行前获取内容:{{content1}}</div>
      <div>在nextTick执行之后获取内容:{{content2}}</div>
      <div>在nextTick执行前获取内容:{{content3}}</div>
    </div>
    <el-button @click="changeContent()" type="primary">主要按钮</el-button>
  </div>
</template>

<script>
export default {
  name: "App",
  data: () => {
    return {
      content: "Before NextTick",
      content1: "",
      content2: "",
      content3: ""
    };
  },
  methods: {
    changeContent() {
      this.content = "After NextTick"; // 在此处更新content的数据
      this.content1 = this.$refs.contentDiv.innerHTML; //获取DOM中的数据
      this.$nextTick()
      .then(()=>{
        this.content2 = this.$refs.contentDiv.innerHTML
      });
      this.content3 = this.$refs.contentDiv.innerHTML;
    }
  },
  mounted() {
    // this.;
  }
};
</script>

默认显示

Before NextTick
在nextTick执行前获取内容:
在nextTick执行之后获取内容:
在nextTick执行前获取内容:

点击按钮修改content的值的时候显示

After NextTick
在nextTick执行前获取内容:Before NextTick
在nextTick执行之后获取内容:After NextTick
在nextTick执行前获取内容:Before NextTick

虽然content1content3获得内容的语句是写在content数据改变语句之后的,但他们属于同一个事件循环中,所以content1content3获取的还是“Before NextTick”,而content2获得内容的语句写在nextTick的回调中,在DOM更新之后再执行,所以获取的是更新后的“After NextTick”。

官网有这么一段话:
Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。

mixin 混入

混入是一种方式,来分发Vue组件中的可复用功能,一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混入”该组件本身的选项。
mixin.js

export default {
    data() {
        return {
            name:"田雷雷"
        }
    },
    methods: {
        changeName(){
            this.name = "啦啦啦德玛西亚"
        }   
    },
    mounted() {
        console.log("mixin mounted")
    }
}

组件user

<template>
  <div class="user">
      <div>
        <div class="name">{{name}}</div>
        <el-button @click="changeName()" type="primary">主要按钮</el-button>
      </div>
  </div>
</template>

<script>
import mixin from '../mixin/mixin';
export default {
  mixins:[mixin],
  components: {},
  data() {
    return {};
  },
  methods: {
    changeName(){
      this.name="tianleilei"
    }
  },
  mounted() {
    console.log("组件mounted")
  }
};
</script>

页面显示“田雷雷”,此时“田雷雷”是mixin.js中的data数据,
Console面板此时依次输出“mixin mounted”,“组件mounted”,
点击按钮触发changeName事件修改name,页面显示“tianleilei”,
此时的调用的是组件内的methodschangeName方法。

当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”。比如数据对象在内部会进行递归合并,发生冲突时以组件数据优先。

同名钩子函数将合并为一个数组,都将被调用,但是混入的钩子将在组件自身钩子 之前 调用。

值为对象的选项,例如methodscomponents、将被合并为同一个对象,两个对象键名冲突时,取组件对象的键值对。

全局混入:
混入也可以进行全局注册,一旦全局注册它将影响每一个之后创建的Vue实例。

Vue官网的例子

// 为自定义的选项 'myOption' 注入一个处理器。
Vue.mixin({
  created: function () {
    var myOption = this.$options.myOption
    if (myOption) {
      console.log(myOption)
    }
  }
})

new Vue({
  myOption: 'hello!'
})
// => "hello!"

keep-alive

keep-alive是一个抽象组件:它自身不会渲染一个DOM元素,也不会出现在父组件链中;使用keep-alive包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。

<keep-alive>包裹动态组件时会缓存不活动的组件实例,而不是销毁它们。当组件在<keep-alive>内被切换,它的 activateddeactivated 这两个生命周期钩子函数将会被对应执行。

属性:

include:只有名称匹配的组件会被缓存
exclude:任何名称匹配的组件都不会被缓存
max:数字,最多可以缓存多少组件实例。

includeexclude属性允许组件有条件地缓存,二者都可以用逗号分隔字符串、正则表达式或一个数组来表示。

<!-- 逗号分隔字符串 -->
<keep-alive include="home,user">
  <router-view></router-view>
</keep-alive>

<!-- 正则表达式(v-bind) -->
<keep-alive :include="/home | user/">
  <router-view></router-view>
</keep-alive>

<!-- 数组(v-bind) -->
<keep-alive :include="['home','user']">
  <router-view></router-view>
</keep-alive>

匹配首先检测组件自身的name选项,如果name选项不可用,则匹配它的局部注册名称(父组件components选项的键值)。匿名组件不能被匹配。

max最多可以缓存多少组件实例,一旦这个数字达到了,在新实例被创建之前,已缓存组件中最久没有被访问到的实例会被销毁掉。

activated:进入组件被执行,deactivated:离开组件被执行。

slot(插槽)

Slot可以理解为“占坑”,在组件模板中占好了位置,当使用该组件标签的时候,组件标签里面的内容就会自动“填坑”。

  1. 内容插槽:
    定义两个组件:Parent.vueChild.vue
    Parent.vue组件中引用Child.vue.
//Parent.vue
<Child>
  Hello World
</Child>
//Child.vue
<div>
  <slot></slot>
</div>

Child组件中<slot>的所占的位置会被Parent组件中的“Hello World”所替代。

  1. 后备内容插槽(默认内容)
//Parent
<Child></Child>
//Child
<div>
  <slot>啦啦啦</slot>
</div>

Child组件中<slot>的所占的位置会被默认内容“啦啦啦”所替代。

  1. 具名插槽
    有时候一个组价需要多个插槽
    可以使用<slot>元素的特殊属性:name,这个属性可以用来定义额外的插槽。
//Parent
<Child>
  <template v-slot:header>
    <div>header</div>
  </template>
  <template v-slot:footer>
    <div>footer</div>
  </template>
  <div>没有name默认为default</div>
</Child>
//Child
<div>
  <div class="header">
    <slot name="header"></slot>
  </div>
  <div class="main">
    <slot></slot>
  </div>
  <div class="footer">
    <slot name="footer"></slot>
  </div>
</div>

页面中显示为
header
没有name默认为default
footer

如果一个<slot>不带name属性的话,那么它的name默认为default
在向具名插槽提供内容的时候,我们可以在<template>元素上使用v-slot指令,并以参数的形式提供其名称。
<template>元素中的内容将会被传入对应的插槽。任何没有被包裹在带有v-slottemplate中的内容都会被视为默认插槽的内容。

总结:
插槽内可以包含任何模板代码,包括HTML。
插槽内可以添加其他组件。
如果子组件中没有包含<slot>元素,则父组件中该组件起始标签到结束标签之间的任何内容都会被抛弃。
插槽跟模板其他地方一样都可以访问相同的实例属性,(也就是相同的作用域)。父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。

  1. 作用域插槽
    插槽跟模板其他地方一样,都可以访问相同的实例属性(也就是相同的作用域),而不能访问访问子组件的作用域。也就是不能访问<Child>的作用域。

如何访问?
需要将传递的内容绑定到子组件<slot>上,然后在父组件中用v-slot设置一个值来定义我们提供插槽的名字:

//Parent
<div>
  <Child>
    <template v-slot:default="slotProps">
      <div>{{slotProps.userInfo.lastName}}</div>
    </template>
  </Child>
</div>
//Child
<template>
  <div>
    <slot v-bind:userInfo="user">{{user.firstName}}</slot>
  </div>
</template>
data(){
  return{
    user:{
      firstName:"tian",
      lastName:"leilei"
    }
  }
}

绑定在<slot>元素上的特性被称为插槽prop。在父组件中,我们可以用v-slot设置一个值来定义我们提供的插槽prop的名字,然后直接使用就好了。

  1. 独占默认插槽缩写:
//Parent
<div>
  <Child>
    <!-- 可以去掉:default -->
    <template v-slot="slotprops">
      <div>{{slotprops.userInfo.lastName}}</div>
    </template>
  </Child>
</div>

注:默认插槽的缩写语法不能和具名插槽混用,因为它会导致作用域不明确。

//Parent.vue
<div>
  <!-- 会报错 -->
  <Child v-slot="slotprops">
    <div>{{slotprops.userInfo.firstName}}</div>
    <template v-slot:other="otherProps">
      <div>{{otherProps.userInfo.lastName}}</div>
    </template>
  </Child>
</div>

一般,只要出现多个插槽,始终要为所有插槽使用完整的基于<template>的语法:

//Parent.vue
<Child>
  <template v-slot:default="slotProps">
    {{slotProps.userInfo.firstName}}
  </template>
  <template v-slot:other="otherProps">
    {{otherProps.userInfo.lastName}}
  </template>
</Child>
  1. 解构插槽属性Prop
    原来:
//Parent.vue
<Child v-slot="slotProps">
  {{slotProps.userInfo.firstName}}
</Child>

解构后:

//Parent.vue
<Child v-slot={userInfo}>
  {{userInfo.firstName}}
</Child>
  1. 具名插槽的缩写
    v-onv-bind一样,v-slot也有缩写,缩写形式为:把v-slot:替换为字符#
    例如:v-slot:header可以重写为#header
    缩写前:
//Parent.vue
<div>
  <template v-slot:header>
    <div>header</div>
  </template>
</div>

缩写后:

//Parent.vue
<div>
  <template #header>
    <div>header</div>
  </template>
</div>

注:使用缩写必须有明确的插槽名,如果没有插槽名则使用#default来使用。

<!-- 报错 -->
<template #>
  <div>我是header呀</div>
</template>
<!-- 没有插槽名使用default -->
<template #default>
  <div>我是header呀</div>
</template>

过滤器 (filter)

过滤器是对即将显示的数据进行进一步筛选加工处理,然后进行显示,过滤器并不会改变原有数据,而是在原数据基础上返回新的数据。

  1. 全局过滤器:
Vue.filter(filterName,function(value){
  //逻辑
})

参数:filterName:过滤器名称,value:需要过滤的数据。

  1. 局部过滤器:
data(){
    return {
    }
},
methods: {},
filters:{
    "filterName":function(value){
        //逻辑
    }
}
  1. 使用过滤器
    在花括号中使用
<div>{{ name | capitalize }}</div>

v-bind中使用

<div v-bind:id="id | formatId"></div>

例子

<template>
  <div class="study">
      <div>{{name | capitalize}}</div>
  </div>
</template>

<script>
export default {
  components: {},
  data() {
    return {
      name:"lisa"
    };
  },
  computed: {},
  filters:{
    "capitalize":function(value){
      return value.toUpperCase();
    }
  },
};
</script>

过滤器capitalizename由“lisa”修改为“LISA”。

  1. 传递多个参数的过滤器使用
    {{ 参数1 | 过滤器名称(参数2,参数3) }}
<div>{{ value1 | capitalize(value2,value3) }}</div>
  1. 在一个参数上使用多个过滤器
<div>{{message | filterA | filterB}}</div>

filterA被定义为接收单个参数的过滤器函数,表达式message的值将作为参数传入到函数中,然后继续调用同样被定义为接收单个参数的过滤器函数filterB,将filterA的结果传递到filterB中。
例子:

<template>
  <div class="study">
      <div>{{name | capitalize | addIs | isGirl}}</div>
  </div>
</template>

<script>
export default {
  components: {},
  data() {
    return {
      name:"lisa"
    };
  },
  computed: {},

  filters:{
    "capitalize":function(value){
      return value.toUpperCase();
    },
    "addIs":function(value){
      return value + " is"
    },
    "isGirl":function(value){
      return value + "girl "
    }
  },
};
</script>

过滤的过程是,过滤器函数capitalize将数据name:“lisa”改为“LISA”,然后将“LISA”作为参数传递给过滤器addIs,经过过滤为LISA is,再将此字符串作为参数传递给过滤函数isGirl,经过过滤后变为“LISA is girl”。最终输出“LISA is girl”。

自定义指令

比如新增一个自定义指令v-focus,当页面默认加载完成,绑定v-focusinput元素获得焦点。

  1. 全局自定义指令
//注册一个全局指令
Vue.directive("focus",{
  inserted:function(el){
    //聚焦元素
    el.focus()
  }
})
  1. 局部自定义指令
    局部注册自定义指令,组件接受一个directives选项
directives:{
  focus:{
    inserted:function(el){
      el.focus()
    }
  }
}

使用:

<input v-focus type="text">

详见Vue官网自定义指令

插件

插件一般都是为了避免重复写同样的代码而提取出来的实现特定功能的代码。
Vue中插件有很多种比如vue-router,vuex
官网对插件的解释:

插件通常用来为Vue添加全局功能,插件的功能范围没有严格限制,一般有下面几种:

  1. 添加全局方法或属性

  2. 添加全局资源

  3. 通过全局混入来添加一些组件选项

  4. 添加Vue实例方法,通过把它们添加到Vue.prototype上实现

  5. 一个库,提供自己的API,同时提供上面提到的一个或多个功能。如vue-router,element-ui

  6. 插件使用:
    通过全局方法Vue.use()使用插件,它需要在你调用new Vue()启动应用之前完成:

// `pluginName` 插件
Vue.use(pluginName);

new Vue({
  //...组件选项
})

也可以传入一个可选的选项对象:

Vue.use(pluginName,{size:medium})

看看ElementUI组件也是使用插件方式引入。
https://element.eleme.cn/#/zh-CN/component/quickstart
Vue.use()会自动阻止多次注册相同插件,届时即使使用多次也只会注册一次该插件。

Vue.js官方提供的插件例如vue-router在检测到Vue是可访问的全局变量时会自动调用Vue.use().然而在像CommonJs这样的模块环境中,你应该始终显式地调用Vue.use():

//用browserify 或 webpack 提供的 CommonJs模块环境时
var Vue = require('vue');
var VueRouter = require(vue-router);

Vue.use(VueRouter)//调用此方法
  1. 开发插件
    Vue.js的插件开发需要符合一定的规范,官方说,应该暴露一个install方法。这个方法的第一个参数是Vue构造器,第二个参数是一个可选的选项对象。
//1.添加全局方法或属性  这块不是很清楚如何去使用,调用Vue.MyGlobalMethod()报错。。。求解惑、
Vue.MyGlobalMethod = function(){}

//2.添加全局资源
Vue.directive("my-directive",{
  bind(el,binding,vnode,oldVnode){
    //逻辑
  }
})
//3.注入组件选项
Vue.mixin({
  created:function(){
    //逻辑。。。
  }
  ...
})
//4.添加实例方法
Vue.prototype.$myMethod = function(){
  //逻辑...
}

开发一个插件并发布到npm可以参考我写的另一篇文章vue-cli3自定义插件并发布到npm

API

1. Vue.extend(options)

使用Vue构造器,创建一个“子类”,参数是一个包含组件选项的对象。
其中data选项是特例,在Vue.extend()中它必须是函数。
组件选项的对象如下:

obj {
  template:'<div>template</div>',
  data:function(){
    return {
      name:"lucy"
    }
  },
  methods: {},
  filters: {},
  directives: {},
  components: {}
}

使用:

<div id="mount-point"></div>
var Profile = Vue.extend({
  template:"<p>{{name}}</p>",
  data:function(){
    return {
      name:"lucy"
    }
  }
})
new Profile().$mount("#mount-point");

结果如下:

<p>lucy</p>

经过查阅一些资料得知。在Vue内部,所有的组件都是通过Vue.extend()来构建的。
比如:
开发中常用的编写组件方法

Vue.component("MyComponent",{
  template:'<div>这是组件</div>'
})

其实这是一个语法糖,真正的内部是这样运行的。

let component = Vue.extend({
  template:'<div>这是组件</div>'
})

Vue.component("MyComponent",component)

既然使用Vue.extend()会返回一个组件的构造函数(也就是Vue的子类)。那么就可以使用它的实例去挂载到dom上。就像 new Vue().$mount("#app")

总结:

在Vue内部,所有的组件都是通过Vue.extend()来构建的,这个函数接收一个满足组件定义的object对象,最终返回一个构建此组件的构造函数(类,也就是Vue的子类)。可以实例化这个构造函数,然后去使用Vue原型上的属性和方法。

参考:
Vue - 组件和Vue.extend
Vue-extend

2. Vue.set(target,propertyName/index,value)

参数:
target:数组或对象
propertyName/index:对象的key或数组的下标
value:要设置的值
Vue官方文档在深入响应式原理这一章是这样使用的。
前提:由于Javascript的限制,Vue不能检测数组和对象的变化,

对于对象:

Vue无法检测property的添加或移除,由于Vue会在初始化实例时对property执行getter/setter转化,所以property必须在data对象上存在才能让Vue将它转换为响应式的。比如:

var vm = new Vue({
  data:{
    a:1
  }
})

// `vm.a`是响应式的,
vm.b = 2
// `vm.b`不是响应式的

对于已经创建的实例Vue不允许添加根级别的响应式property。但是可以使用Vue.set(object,propertyName,value)方法向嵌套对象添加响应式property。例如,对于:

Vue.set(vm.someObject,'b',2)

还可以使用vm.$set实例方法,是全局Vue.set的别名:

this.$set(this.someObject,'b',2)

有时你可能需要为已有对象赋值多个新property,在这种情况下可以使用Object.assign(),将已有对象与含有新property的对象进行合并,创建一个新的对象。

this.someObject = Object.assign({},this,someObject,{a:1,b:2})

对于数组:
Vue不能检测一下数组的变动:

  1. 当利用索引直接设置一个数组项时,例如vm.items[0] = newVal.
  2. 当你修改数组长度时,例如:vm.items.length = newLength
    举个例子:
var vm = new Vue({
  data:{
    items:["a","b","c"]
  }
})
vm.items[1] = "x" // 不是响应式的
vm.items.length = 2 //不是响应式的

使用set实现响应式

Vue.set(vm.items,1,'x')

3. Vue.delete(target,propertyName/index)

Vue不能检测到property被删除,比如

data(){
  name:{
    first:"firstName"
  }
},
methods:{
  deleteName(){
    delete this.name.first
  }
}

当执行deleteName方法时,name属性会被删除掉,但是此时模板中的name还依然存在,因为Vue检测不到property被删除。
官网如是说:deleteAPIs是删除对象的 property。如果对象是响应式的,确保删除能触发更新视图。这个方法主要用于避开 Vue 不能检测到 property 被删除的限制,但是你应该很少会使用它。
使用:

  deleteName(){
    this.$delete(this.name,"first")
  }

可以实现响应

风格指南

  1. 使用 v-for 时记得加 key,可以快速定位到需要更新的DOM的节点,提高效率。
  2. 永远不要把 v-if 和 v-for 用在同一个元素上,提高渲染效率。
  3. 优先通过 Vuex 管理全局状态,而不是通过 this.$root 或全局事件总线。
  4. 为组件样式设置 scope 作用域。

Vue CLI

Vue CLI 3

旧版本处理
Vue CLI 的包名称由vue-cli改成了@vue/cli。如果你已经全局安装了旧版本的vue-cli(1.x或2.x),需要先通过npm uninstall vue-cli-g卸载。

Node版本要求
Vue CLI需要Node.js8.9或更高版本。

安装:

npm install -g @vue/cli

查看

vue --version

创建项目:

vue create app app为你的项目名称

接下来需要选择一些配置。

启动项目:

npm run serve

打包项目:

npm run build

Vuex

Vuex是专门为Vue.js应用程序开发的状态管理模式。

Vuex应用核心是store(仓库),类似于在vue.js项目里的一个数据库。

  1. Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
  2. 你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。

基础结构:

const store = new Vuex.Store({
  state:{
  
  },
  mutations:{
   
  },
  getters:{
    
  },
  actions:{
    
  },
  modules:{

  }
})

State

state类似于组件中的data,存储在Vuex中的数据和Vue实例中的data遵循相同的规则。
定义:

state:{
  count:0
}

在组件中使用:

<div>{{$store.state.count}}</div>

由于在根组件中进行注册过store:

import store from './store'
new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

所以在子组件中可以通过this.$store访问到

computed:{
  count(){
    return this.$store.state.count
  }
}

还有一种官方提供的方法mapState辅助函数:
不使用辅助函数同样可以使用store中的state,使用mapState辅助函数有个好处就是访问方便。
当没有使用mapState辅助函数的时候这样访问:this.$store.state.count
当使用了mapState辅助函数的时候这样访问:this.count;
是不是干净清爽。

mapState用法:

  1. 在组件中引入mapState辅助函数;
  2. mapState辅助函数映射到组件计算属性中;就可以愉快的使用了。
import { mapState } from 'Vuex';

export default{
  //...
  computed:mapState({
    //使用箭头函数
    count: state => state.count,
    //直接使用字符串
    countAlias:"count"
  })
}

也可以直接传给mapState一个字符串数组

computed:mapState(["count"])

当有多个需要映射的state时可以直接往数组里面接着放。

computed:mapState(["count","num","name","age"])

这样访问的时候就可以使用this.count,this.num,this.name,this.age直接访问了。

有个问题就是组件内部的计算属性被mapState函数占用,如果组件内需要定义使用计算属性该如何操作?

使用对象展开运算符:

mapState函数返回的是一个对象,可以使用对象展开运算符将多个对象合并为一个,然后将最终的对象传给computed属性。

computed:{
  localComputed(){/*...*/},
  // 使用对象展开运算符将此对象混入到外部对象中
  ...mapState({
    //...
  })
}

Getter

Getter相当于组件中的计算属性,当需要对state派生一些状态的时候,比如计算,过滤操作,可以使用Getter。

const store = new Vuex.Store({
  state: {
    todos: [
      { id: 1, text: '...', done: true },
      { id: 2, text: '...', done: false }
    ]
  },
  getters: {
    doneTodos: state => {
      return state.todos.filter(todo => todo.done)
    }
  }
})

Getter接受state作为其第一个参数,接受getter作为第二个参数

getters: {
  // ...
  doneTodosCount: (state, getters) => {
    return getters.doneTodos.length
  }
}

在组件中访问:

$store.getters.doneTodos //// -> [{ id: 1, text: '...', done: true }]
computed: {
  doneTodosCount () {
    return this.$store.getters.doneTodosCount
  }
}

同样可以使用mapGetter辅助函数

import { mapGetters } from 'vuex'

export default {
  // ...
  computed: {
  // 使用对象展开运算符将 getter 混入 computed 对象中
    ...mapGetters([
      'doneTodosCount',
      'anotherGetter',
      // ...
    ])
  }
}

如果想要给getter换一个名字可以使用对象形式

mapGetters({
  // 把 `this.doneCount` 映射为 `this.$store.getters.doneTodosCount`
  doneCount: 'doneTodosCount'
})

Mutation

官方说更改Vuex的store中的状态的唯一方法是提交mutation。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数:

const store = new Vuex.Store({
  state: {
    count: 1
  },
  mutations: {
    increment (state) {
      // 变更状态
      state.count++
    }
  }
})

使用:

this.$store.commit("increment");

mutation的载荷(payload)
有时候在调用mutation的同时传递参数,该参数即为mutation的载荷(payload):

//...
mutations: {
  increment (state, n) {
    state.count += n
  }
}

在组件中调用:

this.$store.commit("increment",10);

在大多数情况下,载荷应该是一个对象,这样可以包含多个字段并且记录的 mutation 会更易读:

// ...
mutations: {
  increment (state, payload) {
    state.count += payload.amount
  }
}
this.$store.commit('increment', {
  amount: 10
})

使用对象风格的方式提交

store.commit({
  type: 'increment',
  amount: 10
})

需要注意的几点:

  1. 提前在store中初始化好所有所需属性,
  2. Mutation必须是同步函数
  3. 异步操作一般都交给Action来处理

使用mapMutations辅助函数将组件中的methods映射为store.commit调用

import { mapMutations } from 'vuex'

export default {
  // ...
  methods: {
    ...mapMutations([
      'increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')`

      // `mapMutations` 也支持载荷:
      'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`
    ]),
    ...mapMutations({
      add: 'increment' // 将 `this.add()` 映射为 `this.$store.commit('increment')`
    })
  }
}

需要传递参数的时候直接在调用的时候传递即可

this.incrementBy(amount)

Action

Action类似于mutation,不同点在于:

  1. Action提交的是mutation,而不是直接变更状态,
  2. Action可以包含任意异步操作。
const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    increment (context) {
      context.commit('increment')
    }
  }
})

Action函数接受一个与store实例具有相同方法和属性的context对象,可以调用context.commit提交一个mutation,或者通过context.state和context.getters来获取state和getters。

可以使用参数解构来简化代码:

actions: {
  increment ({ commit }) {
    commit('increment')
  }
}

在组件中分发Action:

this.$store.dispatch("increment");

Actons支持同样的载荷方式和对象方式进行分发:

// 以载荷形式分发
this.$store.dispatch('incrementAsync', {
  amount: 10
})

// 以对象形式分发
this.$store.dispatch({
  type: 'incrementAsync',
  amount: 10
})

使用mapActions辅助函数将组件的methods映射为store.dispatch调用。

import { mapActions } from 'vuex'

export default {
  // ...
  methods: {
    ...mapActions([
      'increment', // 将 `this.increment()` 映射为 `this.$store.dispatch('increment')`

      // `mapActions` 也支持载荷:
      'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)`
    ]),
    ...mapActions({
      add: 'increment' // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
    })
  }
}

同样也是在调用的时候直接传参数:

this.incrementBy(amount)

组合Action

如果action之间需要相互组合处理更加复杂的异步流程,可以利用Promise

actions: {
  actionA ({ commit }) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        commit('someMutation')
        resolve()
      }, 1000)
    })
  },
  actionB ({ dispatch, commit }) {
    return dispatch('actionA').then(() => {
      commit('someOtherMutation')
    })
  }
}

不过现在使用async/await更加方便直观:

// 假设 getData() 和 getOtherData() 返回的是 Promise

actions: {
  async actionA ({ commit }) {
    commit('gotData', await getData())
  },
  async actionB ({ dispatch, commit }) {
    await dispatch('actionA') // 等待 actionA 完成
    commit('gotOtherData', await getOtherData())
  }
}

Module

当项目业务越来越多的时候,尽早将模块独立出来,否则会很头痛,不过一般小型项目也可以不使用模块。

const moduleA = {
  state: { ... },
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const moduleB = {
  state: { ... },
  mutations: { ... },
  actions: { ... }
}

const store = new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态

组见中模块化的store访问:

this.$store.state.a.count//表示访问a模块下的state中的count

在访问的时候一定要注意需要携带模块名,如果使用辅助函数的时候

computed:{
  ...mapState({
      countA:state => state.a.counta,
      countB:state => state.a.countb,
  }),
}

参考:https://vuex.vuejs.org/zh/

Vue响应式原理

几个概念:
Observer数据监听器,把一个普通的JavaScript对象传给Vue实例的data选项,Vue将遍历此对象所有的属性,并使用Object.defineProperty()方法把这些属性全部转为setter、getter方法。当data中的某个属性被访问,则会调用getter()方法,当data中的属性被改变时,则会调用setter方法。

Compile指令解析器,它的作用对每个元素节点的指令进行解析,替换模板数据,并绑定对应的更新函数,初始化相应的订阅。

Watcher订阅者,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数。

Dep消息订阅器也叫依赖收集,内部维护了一个数组,用来收集订阅者(Watcher),数据变动触发notify函数,再调用订阅者的update方法。主要是收集template中被绑定的数据。

Vue响应式原理

Vue在初始化阶段会分为两方面,一方面将data中的属性进行遍历,并使用Object.defineProperty()方法进行数据劫持,转换为getter,setter方法,实现数据监听功能。
另一方面Vue的指令编译器Compile进行指令解析,初始化视图,将调用了getter方法的属性使用依赖收集器(Dep)进行收集,并订阅Watcher来更新视图,初始化完毕。
当数据发生变化的时候,Observer中的setter方法被触发,setter会立即调用Dep.notify(),Dep开始遍历所有订阅者(Watcher)并调用Watcher中的update方法,订阅者收到通知后进行视图更新。

Vue中的虚拟DOM以及Diff算法理解

Virtual DOM是一棵以JavaScript对象作为基础的树,用JS对象来描述节点,实际上它只是一层对真实DOM的抽象,最终通过一系列操作使这棵树映射到真实环境上。

VDOM好处是解决频繁操作真实DOM问题,当需要频繁操作真实DOM时,可以使用虚拟DOM对所有操作进行合并生成最终的虚拟DOM,并将最终结果映射成真实DOM,交由浏览器渲染,节省渲染性能。

Diff算法是为了实现高效DOM操作而产生的一套算法。

虚拟DOM渲染流程

参考其他文章对上图概念加以解释:

  • 渲染函数:渲染函数是用来生成Virtual DOM的,Vue推荐使用模板来构建应用界面,在底层实现中Vue会将模板编译成渲染函数。
  • VNode虚拟节点:它可以代表一个真实dom节点,通过createElement方法能将VNode渲染成dom节点,简单地说,VNode可以理解成节点描述对象,它描述了真实dom的各种特性。
  • patch(也叫作patching算法):虚拟DOM最核心部分,它可以将VNode渲染成真实的DOM,这个过程是对比新旧节点之间的不同,然后根据对比结果找到需要更新的节点进行更新。其实际作用就是在现有DOM上进行修改来达到更新视图的目的。

Diff算法的几个步骤:

  • 用JavaScript对象结构表示DOM树的结构,然后用这个树构建一个真正的DOM树,查到文档当中。
  • 当状态变更的时候,重新构造一棵新的对象树,然后进行新树与老树的比较,记录两棵树的差异。
  • 把所有记录的差异应用到所构建的真正的DOM树上,视图就更新了。

Diff算法核心:
Vue的Diff算法仅在同级VNode间做Diff,递归进行同级VNode的Diff,最终实现整个DOM树的更新。因为跨层级的操作比较少,因此忽略不计,这样时间复杂度就从O(n3)变成O(n)。可以理解成Vue为了速度而损失了可以忽略的精确。

用大白话说Diff就是在patch函数中有两个操作

  • patch(container,vnode) :初次渲染的时候,将VDOM渲染成真正的DOM然后插入到容器里面。
  • patch(vnode,newVnode):再次渲染的时候,将新的vnode和旧的vnode相对比,然后之间差异应用到所构建的真正的DOM树上。
    第一个操作没啥说的,主要是第二个新旧VNode在对比的时候才是Diff核心。
Diff算法核心图解

根据图流程来理解一下整个过程:
VueDiff算法采用深度优先遍历,把树形结构按照层级分解,只比较同级元素,当数据发生改变时,setter方法会调用Dep.notify通知所有订阅者Watcher,订阅者就会调用patch给真实DOM打补丁(两个重要的函数patchVnodeupdateChildren

  • 先判断根节点及变化后的节点是否是sameVnode,如果不是的话,就会创建新的根节点并进行替换
  • 如果是sameVnode,则进入patchVnode函数,其内部工作如下:
      1. 如果两个节点相等 oldVNode === Vnode,则直接return。
      1. 如果新节点是文本节点,则判断新旧文本节点是否一致,不一致(oldVnode.text !== Vnode.text)则替换
      1. 如果新节点不是文本节点,则开始比较新旧节点的子节点oldChch
      1. 如果子节点都存在,则进行updateChildren计算
      1. 如果只有新节点存在,则如果旧节点有文本节点,则移除文本节点,然后将新子节点插入
      1. 如果只有旧子节点存在,则移除所有旧子节点
      1. 如果均无子节点且旧节点是文本节点,则移除文本节点(此时新节点一定不是文本节点)
  • updateChildren函数细致对比(提高效率)
      1. start&&oldStart对比
      1. end&&oldEnd对比
      1. start&&oldEnd对比
      1. end && oldStart对比
      1. 生成map映射,(key:旧子节点上的key),根据key记录下老节点的位置(idxInOld)
        1. 如果找到了idxInOld,如果是相同节点,则移动旧节点到新的对应的地方,否则虽然key相同但元素不同,当做新元素节点去创建
        1. 如果没有找到idxInOld,则创建节点
      1. 如果老节点先遍历完,则新节点比老节点多,将新节点多余部分直接插入进去
      1. 如果新节点先遍历完,则旧节点比新节点多,将旧节点多余的直接删除

参考:
详解Vue中的虚拟DOM
详解vue的diff算法原理

Vue-Router

路由传参:

  1. 在路由中配置:(适合比较固定的传参)

router.js

{
  path:'/user/:id',
  component:User
}

调用:

this.$router.push({path:"/user/"+id})

User.vue

this.$route.params.id // 通过this.$route.params.id获取路由中配置的参数

这里可以使用props去解耦$route来获取参数,具体可以参考官网。
注意:页面刷新路由参数不会丢失

  1. params+name
{
  path: '/home',
  name: 'home',
  component: Home
}

传参:

this.$router.push({
  name:'home',
  params:{
    id:007
  }
})

获取:

this.$route.params.id

注意:页面刷新,路由参数丢失

  1. path+query
{
  path:'/home',
  name:'home',
  component:Home
}

传参:

this.$router.push({
  path:'/home',
  query:{
    id:008
  }
})

获取:

this.$route.query.id

注意:刷新页面参数会丢失。此方法传递参数会将参数暴露在地址栏,如果是私密信息不建议这么处理,可以采用本次存储等方式。

路由导航
全局守卫:

路由独享守卫:

  • beforeEnter
{
  path:'home',
  name:'home',
  component:Home,
  beforeEnter:(to, from, next)=>{
    // ...
  }
}

组件内守卫:

  • beforeRouteEnter
  • beforeRouteUpdate
  • beforeRouteLeave
    注意:beforeRouteEnter 守卫 不能 访问 this,因为守卫在导航确认前被调用,因此即将登场的新组件还没被创建。但是可以通过传一个回调给 next来访问组件实例。在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数。
beforeRouteEnter (to, from, next) {
  next(vm => {
    // 通过 `vm` 访问组件实例
  })
}

完整的导航流程:

  1. 导航被触发。
  2. 在失活的组件里调用 beforeRouteLeave 守卫。
  3. 调用全局的 beforeEach 守卫。
  4. 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
  5. 在路由配置里调用 beforeEnter。
  6. 解析异步路由组件。
  7. 在被激活的组件里调用 beforeRouteEnter。
  8. 调用全局的 beforeResolve 守卫 (2.5+)。
  9. 导航被确认。
  10. 调用全局的 afterEach 钩子。
  11. 触发 DOM 更新。
  12. 用创建好的实例调用 beforeRouteEnter 守卫中传给 next 的回调函数。

面试问:Vue-router原理
回答:Vue-router有两种模式一种是Hash模式,一种是History模式,两种模式的实现方式不同。
Hash模式是通过监听window对象提供的 onhashchange事件,根据url中的hash值发生改变,从而实现页面切换。
History模式是通过Html5的History API为浏览器的全局history提供api扩展。window对象提供了onpopstate事件来监听历史栈的改变,一旦浏览器发出动作,导致历史栈信息发生改变,那么就会触发该事件。然后调用操作历史栈的api实现压入和替换功能。
history提供了两个操作历史栈的api,history.pushState,history.replaceState

参考:
https://router.vuejs.org/zh/
深入Vue-router最佳实践
[实践系列] 前端路由


未完待续。。。

写在最后:文中内容大多为自己平时从各种途径学习总结,文中参考文章大多收录在我的个人博客里,欢迎阅览http://www.tianleilei.cn

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容