前端面试题(Vue)

一、vue

说说vue动态权限绑定渲染列表(权限列表渲染)

  1. 首先请求服务器,获取当前用户的权限数据,比如请求 this.$http.get(“rights/list”);

  2. 获取到权限数据之后,在列表中使用v-if v-if-else的组合来展示不同的内容

    <template>
      <div>
        <!-- 面包屑导航区 -->
        <el-breadcrumb separator-class="el-icon-arrow-right">
          <el-breadcrumb-item :to="{ path: '/home' }">首页</el-breadcrumb-item>
          <el-breadcrumb-item>权限管理</el-breadcrumb-item>
          <el-breadcrumb-item>权限列表</el-breadcrumb-item>
        </el-breadcrumb>
        <!-- 卡片视图 -->
        <el-card>
          <el-table :data="rightsList" border stripe>
            <el-table-column type="index" label="#"></el-table-column>
            <el-table-column label="权限名称" prop="authName"></el-table-column>
            <el-table-column label="路径" prop="path"></el-table-column>
            <el-table-column label="权限等级" prop="level">
              <template slot-scope="scope">
                <el-tag v-if="scope.row.level === '0'">一级</el-tag>
                <el-tag type="success" v-else-if="scope.row.level === '1'">二级</el-tag>
                <el-tag type="danger" v-else>三级</el-tag>
              </template>
            </el-table-column>
          </el-table>
        </el-card>
      </div>
    </template>
    
    <script>
    export default {
      data() {
        return {
          // 权限列表
          rightsList: []
        };
      },
      created() {
        this.getRightsList();
      },
      methods: {
        async getRightsList() {
          //获取权限列表数据
          const { data: res } = await this.$http.get("rights/list");
          if (res.meta.status !== 200) {
            return this.$message.error("获取权限列表失败!");
          }
          this.rightsList = res.data;
        }
      }
    };
    </script>
    
    <style lang='less' scoped>
    </style>
    
    

Vue用的哪种设计模式

​ 属于发布订阅模式,在vue中使用observer和definereactive两个方法的结合对数据进行递归劫持,然后通过watch这个类来对属性进行订阅,Dep类用于解耦合,当数据变更的时候先触发数据的set方法,然后调用Dep.notiify通知视图更新

说说vue操作真实dom性能瓶颈

vue性能瓶颈的几种情况

  1. 一次渲染大量的数据的时候,存在大量数据并且都是复杂类型的时候,会导致vue对数据的劫持时间和渲染时间变长, js 连续执行时间过长,会导致页面长时间无法交互,而且渲染时间太慢,用户一次交互反馈的时间过长。

    优化方案:可以使用requestAnimation这个方法,将数据进行分割,分批次渲染,减少了 js 的连续运行时间,并且加快了渲染时间,利用加长总运行时间换取了渲染时间,用户既能快速得到反馈,而且不会因为过长时间的 js 运行而无法与页面交互。

  2. 当页面中存在大量数据,只是修改了一小部分导致页面也会导致页面卡顿,因为vue的更新以组件为粒度进行更新的,只要修改了当前组件中所使用的数据,组件就会整个去进行更新,造成大量的时间浪费

    优化方案:将不同的模块划分成不同的组件,这样有效降低虚拟dom的diff运算时间过长的问题,比如将大量数据的模块单独放一个组件,其它放一个组件,由于vue是以组件为粒度更新,修改其它组件的情况下不会导致table的重新diff,提升页面响应速度高达几百倍

  3. 动态插槽作用域或者静态插槽的更新

    使用插槽作用域来替换这两种操作方式,一样能提升性能,因为使用插槽作用域之后,插槽内容会被封装到一个函数中,被子组件渲染,而不是在父组件

Vue中如何获取dom、操作dom、更新dom

如何获取dom?在Vue中提供了一种特别的方式来获取dom,即给dom加上个ref属性,那么就可以通过this.$refs.名字来获取到该dom元素。

如何操作dom、更新dom?通过refs.名字就可以拿到对应的真实dom,然后就可以用原生JS进行操作和更新。当然vue框架本身就是不需要dom操作的,通过修改相应的数据并再配合指令、模板语法就可以轻松的操作和更新dom。

Vue 的双向数据绑定原理是什么

在Vue2.x中,双向数据绑定是通过 数据劫持 结合 发布订阅模式的方式来实现的,也就是说数据和视图同步,数据发生变化,视图跟着变化,视图变化,数据也随之发生改变。核心:关于VUE双向数据绑定,其核心是 Object.defineProperty()方法。

Vue3.x则是用ES6的语法Proxy对象来实现的。

Object.defineProperty()的缺点:

  1. 只能监听对象(Object),不能监听数组的变化,无法触发push, pop, shift, unshift,splice, sort, reverse。
  2. 必须遍历对象的每个属性
  3. 只能劫持当前对象属性,如果想深度劫持,必须深层遍历嵌套的对象。

Proxy的优点:

  1. Proxy 可以直接监听对象而非属性。
  2. Proxy 可以直接监听数组的变化。
  3. Proxy 有多达 13 种拦截方法,不限于 apply、ownKeys、deleteProperty、has 等等是 Object.defineProperty 不具备的。
  4. Proxy 返回的是一个新对象,我们可以只操作新的对象达到目的,而 Object.defineProperty 只能遍历对象属性直接修改。
  5. Proxy 作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利。
  let arr = [];
  let proxy = new Proxy(arr, {
    get: function(obj, prop){
      return obj[prop];
    },
    set: function(obj, prop, value){
      obj[prop] = value;   //可以被监听到变化
      return true;
    }
  });
  setTimeout(()=>{
    proxy.push(1);
  }, 2000)

mvvm框架是什么

MVVM是Model-View-ViewModel的简写。它本质上就是MVC(Model-View-Controller)的改进版。在开发过程中,由于需求的变更或添加,项目的复杂度越来越高,代码量越来越大,此时我们会发现MVC维护起来有些吃力,尤其Controller控制层非常的厚重,非常的庞大,难以维护。

所以有人想到把Controller的数据和逻辑处理部分从中抽离出来,用一个专门的对象去管理,这个对象就是ViewModel。ViewModel 是由前端开发人员组织生成和维护的视图数据层。在这一层,前端开发者对从后端获取的 Model 数据进行转换处理,做二次封装,以生成符合 View 层使用预期的视图数据模型。

由于实现了双向绑定,ViewModel 的内容会实时展现在 View 层,这是激动人心的,因为前端开发者再也不必低效又麻烦地通过操纵 DOM 去更新视图,MVVM 框架已经把最脏最累的一块做好了,我们开发者只需要处理和维护 ViewModel,更新数据视图就会自动得到相应更新,真正实现数据驱动开发。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wUo4GvnJ-1655384400029)(./pic/MVVM.png)]

谈谈Vue的token存储

在前后端完全分离的情况下,Vue项目中实现token验证大致思路如下:

1、第一次登录的时候,前端调后端的登陆接口,发送用户名和密码

2、后端收到请求,验证用户名和密码,验证成功,就给前端返回一个token

3、前端拿到token,将token存储到localStorage和vuex中,并跳转路由页面

4、前端每次跳转路由,就判断 localStroage 中有无 token ,没有就跳转到登录页面,有则跳转到对应路由页面

5、每次调后端接口,都要在请求头中加token

6、后端判断请求头中有无token,有token,就拿到token并验证token,验证成功就返回数据,验证失败(例如:token过期)就返回401,请求头中没有token也返回401

7、如果前端拿到状态码为401,就清除token信息并跳转到登录页面

知道nextTick的作用吗,谈谈对它的理解,是什么,怎么用

当你设置 vm.message = ‘new message’,该组件不会立即重新渲染。当刷新队列时,组件会在下一个事件循环“tick”中更新。多数情况我们不需要关心这个过程,但是如果你想基于更新后的 DOM 状态来做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员使用“数据驱动”的方式思考,避免直接接触 DOM,但是有时我们必须要这么做。为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用 Vue.nextTick(callback)。这样回调函数将在 DOM 更新完成后被调用。例如:

<div id="example">{{message}}</div>
var vm = new Vue({
  el: '#example',
  data: {
    message: 'old message'
  }
})
vm.message = 'new message' // 更改数据
vm.$el.textContent  // 'old message'
Vue.nextTick(function () {
  vm.$el.textContent  // 'new message' 
})

nextTick和setTimeout区别

首先Vue 在更新 DOM 时是异步执行的,也就是说数据变了,DOM不会立即改变,那么我们是如何知道DOM什么时候会改变呢?也就是说如何知道异步后的触发时机呢?

可以通过nextTick方法,这个方法在源码内,先监听是否具备Promise.then,利用promise来监听,如果当前环境不支持promise,那么就降级采用MutationObserver,如果MutationObserver不支持的话,那么就降级采用setImmediate,如果setImmediate不支持的话,那么就使用setTimeout(fn, 0)。

所以说nextTick和setTimeout区别总结就是:nextTick会先尝试使用promise、MutationObserver、setImmediate这些技术去监听,如果都不支持才会采用setTimeout

vue中为什么用虚拟dom而不操作真实dom

起初我们在使用JS/JQuery时,不可避免的会大量操作DOM,而DOM的变化又会引发回流或重绘,从而降低页面渲染性能。那么怎样来减少对DOM的操作呢?此时虚拟DOM应用而生,所以虚拟DOM出现的主要目的就是为了减少频繁操作DOM而引起回流重绘所引发的性能问题的!

虚拟DOM(Virtual Dom),起始本质上就是一个JS对象,当数据发生变化时,我们不直接操作真实DOM,因为很昂贵,我们去操作这个JS对象,就不会触发大量回流重绘操作,再加上diff算法,可以找到两次虚拟DOM之间改变的部分,从而最小量的去一次性更新真实DOM,而不是频繁操作DOM,性能得到了大大的提升。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ePCNHx2T-1655384400030)(./pic/虚拟DOM.png)]

虚拟DOM还有一个好处,可以渲染到 DOM 以外的平台,实现 SSR、同构渲染这些高级特性,Weex 等框架应用的就是这一特性。

Vue如何进行组件传值

父向子组件传值,可以利用prop方式。

子向父组件传值,可以利用自定义事件$emit方式。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z2ZAIwBs-1655384400032)(./pic/vue父子通信.png)]

多层级组件传值,可以使用provide/inject

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Szc8wWMR-1655384400033)(./pic/provide_inject.png)]

无关系的组件传值,利用vuex状态管理

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u7BHKToo-1655384400034)(./pic/vuex.png)]

说说vue里面的父子通信

父 -> 子: 通过 Prop 向子组件传递数据,子组件通过props属性来接收。

<blog-post title="My journey with Vue"></blog-post>
Vue.component('blog-post', {
  props: ['title'],
  template: '<h3>{{ title }}</h3>'  //获取父组件的值
})

子 -> 父: 父组件自定义事件,子组件利用$emit来完成。

<!--拿到子组件传递的数据$event即0.1-->
<blog-post v-on:enlarge-text="postFontSize += $event"></blog-post>
Vue.component('blog-post', {
  props: ['title'],
  template: '<h3 v-on:click="$emit('enlarge-text', 0.1)">{{ title }}</h3>' 
})

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wCkDuiyq-1655384400036)(./pic/vue父子通信.png)]

谈谈如何实现vue组件通信和传值方式 (两个问题为同一个答案问法不一样)

这类问题 首先分类 表明了解的比较多   具体就没说完 或者漏了  面试官也不会计较很多
	组件通信的四大类   父与子    子与父    子与子     跨层级   
	在细说各种方式 加入自己的理解
1、props和$emit
父组件向子组件传递数据是通过prop传递的,子组件传递数据给父组件是通过$emit触发事件

2、$attrs和$listeners

3、中央事件总线 bus

上面两种方式处理的都是父子组件之间的数据传递,而如果两个组件不是父子关系呢?这种情况下可以使用中央事件总线的方式。新建一个Vue事件bus对象,然后通过bus.$emit触发事件,bus.$on监听触发的事件。

4、provide和inject

父组件中通过provider来提供变量,然后在子组件中通过inject来注入变量。不论子组件有多深,只要调用了inject那么就可以注入provider中的数据。而不是局限于只能从当前父组件的prop属性来获取数据,只要在父组件的生命周期内,子组件都可以调用。

5、v-model

父组件通过v-model传递值给子组件时,会自动传递一个value的prop属性,在子组件中通过this.$emit(‘input’,val)自动修改v-model绑定的值

6、$parent和$children

7、boradcast和dispatch

8、vuex处理组件之间的数据交互 如果业务逻辑复杂,很多组件之间需要同时处理一些公共的数据,这个时候才有上面这一些方法可能不利于项目的维护,vuex的做法就是将这一些公共的数据抽离出来,然后其他组件就可以对这个公共数据进行读写操作,这样达到了解耦的目的。

说说vue中Key值的作用

关于这个可以的key的作用 首先表明 key 不是一定要有的  不写可以代码也可以跑  但是建议加上
					然后指出可以用的地方  key在v-for循环可以用用   在表单元素中也可以用key 减少缓存
					一般说key  只要说配合v-for的使用
					
key是为Vue中的vnode标记的唯一id,通过这个key,我们的diff操作可以更准确、更快速
diff算法的过程中,先会进行新旧节点的首尾交叉对比,当无法匹配的时候会用新节点的key与旧节点进行比对,然后超出差异能讲清楚diff算法就继续讲
diff程可以概括为:oldCh和newCh各有两个头尾的变量StartIdx和EndIdx,它们的2个变量相互比较,一共有4种比较方式。如果4种比较都没匹配,如果设置了key,就会用key进行比较,在比较的过程中,变量会往中间靠,一旦StartIdx>EndIdx表明oldCh和newCh至少有一个已经遍历完了,就会结束比较,这四种比较方式就是首、尾、旧尾新头、旧头新尾.

准确: 如果不加key,那么vue会选择复用节点(Vue的就地更新策略),导致之前节点的状态被保留下来,会产生一系列的bug. 快速: key的唯一性可以被Map数据结构充分利用,相比于遍历查找的时间复杂度O(n),Map的时间复杂度仅仅为O(1)
讲完以后 还要补充一点自己的看法
建议使用主键比如id  

					
					

说说vue中的虚拟dom和diff算法

Virtual DOM 其实就是一棵以 JavaScript 对象( VNode 节点)作为基础的树,用对象属性来描述节点,实际上它只是一层对真实 DOM 的抽象。最终可以通过一系列操作使这棵树映射到真实的DOM上

下面就是一个真实DOM映射到虚拟DOM的例子:
 <ul id='list'>
          <li class='item'>Item 1</li>
          <li class='item'>Item 2</li>
          <li class='item'>Item 3</li>
        </ul>
        
 var element = {
        tagName: 'ul', // 节点标签名
        props: { // DOM的属性,用一个对象存储键值对
            id: 'list'
        },
        children: [ // 该节点的子节点
          {tagName: 'li', props: {class: 'item'}, children: ["Item 1"]},
          {tagName: 'li', props: {class: 'item'}, children: ["Item 2"]},
          {tagName: 'li', props: {class: 'item'}, children: ["Item 3"]},
        ]
    }
    
  在补充点虚拟DOM的好处
具备跨平台的优势
由于 Virtual DOM 是以 JavaScript 对象为基础而不依赖真实平台环境,所以使它具有了跨平台的能力,比如说浏览器平台、Weex、Node 等。

操作原生DOM慢,js运行效率高。我们可以将DOM对比操作放在JS层,提高效率。
因为DOM操作的执行速度远不如Javascript的运算速度快,因此,把大量的DOM操作搬运到Javascript中,运用patching算法来计算出真正需要更新的节点,最大限度地减少DOM操作,从而显著提高性能。

Virtual DOM 本质上就是在 JS 和 DOM 之间做了一个缓存。可以类比 CPU 和硬盘,既然硬盘这么慢,我们就在它们之间加个缓存:既然 DOM 这么慢,我们就在它们 JS 和 DOM 之间加个缓存。CPU(JS)只操作内存(Virtual DOM),最后的时候再把变更写入硬盘(DOM)

提升渲染性能
Virtual DOM的优势不在于单次的操作,而是在大量、频繁的数据更新下,能够对视图进行合理、高效的更新。

diff算法
vdom因为是纯粹的JS对象,所以操作它会很高效,但是vdom的变更最终会转换成DOM操作,为了实现高效的DOM操作,一套高效的虚拟DOM diff算法显得很有必要

diff算法包括一下几个步骤:

用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树,插到文
档当中

当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较(diff),记录两棵树差异
把2所记录的差异应用到步骤1所构建的真正的DOM树上(patch),视图就更新了
diff算法是通过同层的树节点进行比较而非对树进行逐层搜索遍历的方式,所以时间复杂度只有O(n),是一种相当高效的算法

实现虚拟DOM的过程

vue3.0有了解过吗,你觉得vue3.0好吗,好在哪

这种问题 是开放的 多说就是都是对的 可以讲差异 也可以讲新增 的知识点

比如说 常用的api特别好用

  1. ref、toRefs、toRef、isRef
  • ref 用于定义响应式变量、快捷DOM访问。
  • 基本语法:const a = ref(1) // {value:1}
  • 基础使用: 一般用于定义 String、Number、Boolean 这种基于数据类型,外在表现统一使用 .value 访问。
  • 补充:ref 还可以访问 DOM对象或者组件实例对象,可做DOM操作。
  • toRef、toRefs 用于把一个object的变量,变成响应式的变量。
  • 基本语法:const msg = toRef(obj, key) // 把obj[key]变成响应式的
  • 基本语法:const { msg } = toRefs(obj) // 把整个obj都变成响应式的
  • unref 返回一个变量的值
  • 基本语法:const x = unref(x) // 如果x是ref变量则返回x.value,如果x不是ref变量则直接返回x。
  • isRef 用于判断一个变量是不是ref响应式变量
  • 基本语法:const bol = isRef(x)
function useUpdBoxStyle() {
  const el = ref(null)
  const updateStyle = color => {
    el.value.style.color = color
  }
  return [el, updateStyle]
}
  1. shallowRef、triggerRef
  • shallowRef 用于性能优化,只对对象的第一层进行proxy
  • 基本语法:const obj = shallowRef({a:1,b:{c:{d:{e:2}}}})
  • triggerRef 用于手动触发那些shallowRef的变量进行更新视图
  • 基本语法:triggerRef(obj) // 当obj.value.b.c.d发生变化,triggerRef(obj) 强制更新视图。
  • customRef 自定义ref,把ref变量拆成get/set的写法
  • 基本语法:customRef((track, trigger) =>({get,set})
function useObj() {
  const obj = { a: 1, b: { c: { d: 2 }}}
  const obj1 = ref(obj)
  const obj2 = shallowRef(obj)
  // console.log('obj1', obj1)
  // console.log('obj2', obj2)
  const changeObj = (obj, newD) => {
    // obj1.value.b.c.d = 100
    obj.value.b.c.d = newD
    triggerRef(obj)
  }
  return [[obj1, obj2], changeObj]
}
  1. reactive、readonly
  • reactive 用于定义响应式变量(引用数据类型)
  • 基本语法:const arr = reactive([]) // {value: []}
  • ref和reactive是什么关系呢?ref背后是使用reactive来实现的。
  • shallowReactive 用于性能优化,只对对象的第一层进行proxy
  • 基本语法:const c = shallowReactive({a:{b:{c:100}}}) // 只对这个对象的第一层进行proxy
  • readonly 把响应式变量变成“只读的”,如果修改就报警告。
  • 基本语法:const user = readonly({name:1,age:2})
  • isReadonly 用于判断一个变量是否是readonly的,返回布尔值
  • 基本语法:const bol = isReadonly(x)
  • isProxy 用于判断一个变量是否是响应式的,返回布尔值
  • 基本语法:const bol = isProxy(x)
  • isReactive 用于判断一个变量是否是reactive响应式变量,返回布尔值
  • 基本语法:const bol = isReactive(x)
function useUser() {
  const user = readonly(reactive({name:'list',age:30}))
  console.log('user', user)
  // setTimeout(()=>user.age=40, 2000)
  const x = 1
  const y = readonly({a:1,b:{c:3}})
  console.log('是否被proxy拦截过', isProxy(user), isProxy(x), isProxy(y.b))
  return user
}
  1. toRaw、markRaw
  • toRaw 用于返回一个响应式变量的原始值
  • 基本语法:const a3 = toRow(reactive(a1)) // a1===a3是true
  • markRaw 用于把一个普通变量标记成“不可proxy”的
  • 基本语法:const b2 = markRaw(b1) // b2是无法被reactive的
function useRaw() {
  const a1 = { title: 100 }
  const a2 = reactive(a1)
  const a3 = toRaw(a2)
  console.log('toRow(a2)===a1', a3===a1)
  console.log('a2===a1', a2===a1)
  return [a1,a2,a3]
}
  1. computed、watch、watchEffect
  • computed 用于对响应式变量进行二次计算,当它依赖的响应式变量发生变化时会重新计算
  • 基本语法:const c = computed(()=>c1.value*c2.value) // 只读
  • 基本语法:const c = computed({get:()=>c1.value*c2.value,set:(newVal)=>c1.value=newVal}) // 可写可读
  • watch 用于监听响应式变量的变化,组件初始化它不执行
  • 基本语法:const stop = watch(x, (new,old)=>{}) // 调用stop()可以停止监听
  • 基本语法:const stop = watch([x,y], ([newX,newY],[oldX,oldY])=>{})
  • watchEffect 用于监听响应式变量的变化,组件初始化会执行
  • 基本语法:const stop = watchEffect(()=>ajax({cate,page,size}))
export default function useWatchComputed() {
  const c1 = ref(10)
  const c2 = ref(20)
  const c3 = computed(()=>c1.value*c2.value)  // 只读
  // 可读也可写
  const c4 = computed({
    get: ()=>c1.value*c2.value,
    set: (newVal)=>{
      c1.value = parseInt(newVal) / c2.value
    }
  })
  const stop1 = watch(c4, (newC4, oldC4)=>console.log('c4变了', newC4, oldC4))
  const stop2 = watch([c1,c2], ([newC1,newC2],[oldC1,oldC2])=>{
    console.log('[c1,c2] 新值:', [newC1, newC2])
    console.log('[c1,c2] 旧值:', [oldC1, oldC2])
  })
  const stop3 = watchEffect(()=>{console.log('watch effect', c1.value, c2.value)})
  const update = (c,v) => c.value = v
  return [[c1,c2,c3,c4],[stop1,stop2,stop3,update]]
}

2:也可以说亮点

  1. 性能比vue2.x快1.2~2倍
  2. 支持tree-shaking,按需编译,体积比vue2.x更小
  3. 支持组合API
  4. 更好的支持TS
  5. 更先进的组

3.更可以说性能

1.diff算法更快

vue2.0是需要全局去比较每个节点的,若发现有节点发生变化后,就去更新该节点

vue3.0是在创建虚拟dom中,会根据DOM的的内容会不会发生内容变化,添加静态标记, 谁有flag!比较谁。

2、静态提升

vue2中无论元素是否参与更新,每次都会重新创建,然后再渲染 vue3中对于不参与更新的元素,会做静态提升,只被创建一次,在渲染时直接复用即可

3、事件侦听缓存

默认情况下,onclick为动态绑定,所以每次都会追踪它的变化,但是因为是同一函数,没有必要追踪变化,直接缓存复用即可

在之前会添加静态标记8 会把点击事件当做动态属性 会进行diff算法比较, 但是在事件监听缓存之后就没有静态标记了,就会进行缓存复用

v-model有了解过吗,原理是什么

这种原理性问题 不要直接说不清楚 不了解

先讲下使用

v-model本质上是一个语法糖,可以看成是value + input 方法的语法糖。可以通过model的prop属性和event事件来进行自定义。

2、v-model是vue的双向绑定的指令,能将页面上控件输入的值同步更新到相关绑定的data属性, 也会在更新data绑定属性时候,更新页面上输入控件的值。

然后再来讲细节

vue的双向绑定是由数据劫持结合发布者-订阅者模式实现的,那么什么是数据劫持?vue是如何进行数据劫持的?说白了就是通过Object.defineProperty()来劫持对象属性的setter和getter操作,在数据变动时做你想要做的事情

我们已经知道实现数据的双向绑定,首先要对数据进行劫持监听,所以我们需要设置一个监听器Observer,用来监听所有属性。如果属性发生变化了,就需要告诉订阅者Watcher看是否需要更新。因为订阅者是有很多个,所以我们需要有一个消息订阅器Dep来专门收集这些订阅者,然后在监听器Observer和订阅者Watcher之间进行统一管理的。接着,我们还需要有一个指令解析器Compile,对每个节点元素进行扫描和解析,将相关指令(如v-model,v-on)对应初始化成一个订阅者Watcher,并替换模板数据或者绑定相应的函数,此时当订阅者Watcher接收到相应属性的变化,就会执行对应的更新函数,从而更新视图。因此接下去我们执行以下3个步骤,实现数据的双向绑定:

1.实现一个监听器Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者。

2.实现一个订阅者Watcher,每一个Watcher都绑定一个更新函数,watcher可以收到属性的变化通知并执行相应的函数,从而更新视图。

3.实现一个解析器Compile,可以扫描和解析每个节点的相关指令(v-model,v-on等指令),如果节点存在v-model,v-on等指令,则解析器Compile初始化这类节点的模板数据,使之可以显示在视图上,然后初始化相应的订阅者(Watcher)

3:最后补一下 vue2.0里面用Object.defineProperty 3.0里面用new Proxy 一个监听每个属性 一个监听整个对象

VUE组件如何与iframe通信问题

像这种问题其实问的不是特别详情 面试者可能不懂题目的意思 但是我们要学会揣摩 面试官的问题

如果不知道 你就直说vue的组件通信 在讲iframe的页面获取v

vue组件内嵌一个iframe,现在想要在iframe内获取父组件内信息,采用的是H5新特性PostMessage来解决跨域问题

采用postMessage内涵两个API:

onMessage:消息监听

postMessage:消息发送

代码和例子

 <div class="mapbox">
      <iframe name="map" src="http://localhost:8083/setposition.html?add='add'"></iframe>
 </div>
clearMap(){
      let map = document.getElementsByName("map")[0].contentWindow
      map.postMessage("clearMap","*")
    }
iframe内:

window.addEventListener('message', function (evt) {
  
    if (evt.data == 'clearMap'){
        clearMap()
    }
    //event.data获取传过来的数据
});

用过VUE 的自定义指令吗?自定义指令的方法有哪些

这种问题一样的 先回答经常用的一些指定 比如 v-for v-if v-model v-show等等之类的 指令分为全局和局部的

然后在回答自定义指令

通过directive来自定义指令,自定义指令分为全局指令和局部指令,自定义指令也有几个的钩子函数,常用的有bind和update,当 bind 和 update 时触发相同行为,而不关心其它的钩子时可以简写。一个表达式可以使用多个过滤器。过滤器之间需要用管道符“|”隔开。其执行顺序从左往右。  

当修改data时Vue的组件重渲染是异步还是同步

这个问题很有意思 因为平时我们一般问题异步和同步指的是 数据请求 同步和异步问题

这里加上了组件 还有修改data 这里给大家写个例子

<body>
    <div id="app">
        <div id="main">{{num}}</div>
        <button @click="add">更新</button>
    </div>
</body>
<script>
    new  Vue({
        el:"#app",
        data:{
            num:10
        },
        methods:{
            add(){
                this.num++;
                console.log(this.num)//11
                console.log(document.getElementById("main").innerHTML);//10
          
            }
        }
    })
</script>

以此可以说明

数据更新是同步的 但是视图更新是异步的

解决这个问题需要使用 $nextTick 解决视图异步更新的问题

.sync修饰器的作用是

首先看到 .sync 我们需要知道这是个修饰器 类似修饰器还有 .stop .prevent 之类

其实这个修饰符就是vue封装了 子组件要修改父组件传过来的动态值的语法糖,省去了父组件需要写的方法,但是子组件emit时要加上update

在有些情况下,我们可能需要对一个 prop 进行“双向绑定”。不幸的是,真正的双向绑定会带来维护上的问题,因为子组件可以修改父组件,且在父组件和子组件都没有明显的改动来源

代码解释

// 这里父组件,要给子组件传一个title的值
<template>
    <div>  
        <t-title :title.sync="fatherTitle"></t-title>
    </div>
</template>
<script>
import tTitle from './blocks/list';
export default {
    name: 'test1',
    components: {  tTitle },
    data() {
        return {
            fatherTitle: '父组件给的标题'
        };
    },
}
</script>
// 子组件
<template>
    <div>
        <h3>{{ title }}</h3>
        <button @click="changeTitle">改变</button>
    </div>
</template>
<script>
export default {
    props:{
        title: {type: String, default: '默认值11'}
    },
    methods: {
        changeTitle() {
            this.$emit("update:title", "子组件要改自己的标题");
        }
    }
};
</script>

这里关键就是emit里的参数要写成’update’+ ‘:’ +‘要修改的props’

以前是用的this.$emit(“自定义方法”)

vue多组件嵌套通信方式

这个问题其实也是属于组件通信 常见的组件通信有 父传子 子传父 子传子 以及跨层级

这个多组件嵌套通信其实就是跨层级的另一种问法

多组件通信

方法一:props 一层 一层的传递

方法二:依赖注入 provide 声明 inject接收

方法三:利用公共的bus = new Vue() bus. o n 声 明 b u s . on 声明 bus. onbus.emit() 调用

方法四:使用vuex 全局的状态管理

vue如何让css只在当前组件生效

当前组件<style>写成<style  scoped>   加上scoped就可以了

这个style中一般还有lang   lang可以是less  scss  stylus 等等
不加scoped就是全局的样式

Vue 的 keep-live 用过吗?作用是什么?

  1. 没有用过
  2. 用过,它的作用是可以在组件切换时,保存其包裹的组件的状态,使其不被销毁,防止多次渲染。

keepalive,添加这个会比平常多生命周期吗? keepalive 已经缓存了,但是想跳回去的时候添加新的属性在哪个生命周期里实现

会比平常的组件多两个生命周期钩子函数,分别是:activated 和 deactivated。使用keep-alive包裹的组件在切换时不会被销毁,而是缓存到内存中并执行 deactivated 钩子函数,再次渲染后会执行 activated 钩子函数。如果再一次跳回显示组件的时候可以在 activated 中做处理

说一下 keep-alive 的关联生命周期

会比平常的组件多两个生命周期钩子函数,分别是:activated 和 deactivated。使用keep-alive包裹的组件在切换时不会被销毁,而是缓存到内存中并执行 deactivated 钩子函数,再次渲染后会执行 activated 钩子函数。

Vue 创建项目的指令是什么

  1. 使用的官方 cli 脚手架,如果是低于 3.0 的版本,使用npm init
  2. cli 的版本大于 3.0 的使用vue create
  3. 可以使用 vite 直接搭建项目,命令为npm init vite@latest,根据提示,一步一步操作就好

参考链接

Getting Started | Vite

Vue CLI

vue 中如何使用 ref 绑定

通过为组件或者标签添加 ref 属性,可以在 js 代码中使用全局 api$refs获取 dom 元素或者组件,其上的方法或者属性可以直接进行操作。

vue 导航守卫与 jq 导航拦截器的介绍

vue 的导航守卫一般指的是路由导航守卫,作用是在页面跳转的时候可以执行一个钩子函数。

导航守卫使用最多的是全局守卫 router.beforeEach 主要是用来验证用户的登陆状态。它接收三个参数 to, from, next

  • to: 即将要进入的路由对象

  • from: 当前导航要离开的路由

  • next: 一个回调函数, 一定要调用这个方法,不然路由不会继续往下

    jq 导航拦截器没有听过,一般在 jQuery 的作用就是对 dom 元素做操作,jQuery 的核心功能是元素选择器。至于提到的导航器可能是一类第三方 jQuery 插件?或者网络请求拦截,如果是网络请求拦截,那么 jQuery 发起请求的话,可以封装一个全局 ajax 请求插件,通过设置 ajaxSetup 实现

  // 参考这个 https://www.runoob.com/jquery/ajax-ajaxsetup.html
  $.ajaxSetup({
    // url: 'demo_ajax_load.txt',
    beforeSend() {
      // 发起请求之前执行
    },
    complete() {
      // 所有的请求成功之后执行
    },
  });

vue 常用哪些命令

  1. v-model 指令,用于表单输入。
  2. v-on 指令,用于事件绑定。
  3. v-bind 指令,用于动态绑定一个值和传入变量。
  4. v-once 指令,事件只能用一次,无论点击几次,执行一次之后都不会再执行。
  5. v-html 指令,会将 span 的内容替换成 rawHtml 的属性值,直接作为 HTML 代码解析。
  6. v-for 指令,与 HTML 标签结合使用,用于数据的遍历摆放。
  7. v-if 指令,用来进行条件判断的,直接操作 dom。
  8. v-else 指令,用来进行条件判断的,与 v-if 指令连用,意义为条件不成立时执行。
  9. v-show 指令,根据真假切换元素的显示状态。

vue 中插槽共有几种,及插槽的作用

三种:默认插槽、具名插槽、作用域插槽

  • 默认插槽

    默认插槽就是指,的作用类似于占符。

    //定义一个全局子组件
    Vue.component('child', {
      template: '<div><slot></slot></div>',
    });
    
    var vm = new Vue({
      el: '#root',
    });
    
    <!--引用child组件-->
    <div id="root">
      <child>
        <span>我是占位符</span>
      </child>
    </div>
    

    上述的子组件 child 里定义的 slot 被 span 标签给代替了,如果子组件里没有定义 slot,则 span 标签会被直接忽略,且一个子组件里只能定义一个单个插槽。

  • 具名插槽

    可以通过设置 name 属性,指定显示的位置

    定义一个 组件:

    <div class="container">
      <header>
        <slot name="header"></slot>
      </header>
      <main>
        <slot></slot>
      </main>
      <footer>
        <slot name="footer"></slot>
      </footer>
    </div>
    
    <base-layout>
      <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's some contact info</p>
      </template>
    </base-layout>
    
  • 作用域插槽

    父组件替换插槽标签,但是内容由子组件来提供

    <body>
      <div id="root">
        <child>
          <!--定义一个插槽,该插槽必须放在template标签内-->
          <template slot-scope="props">
            <li>{{props.value}}</li>
          </template>
        </child>
        <!--!--定义不同的渲染方式-->
        <child>
          <!--slot-scope="props"作用是使props指向子组件中定义的<slot>-->
          <template slot-scope="props">
            <h1>{{props.value}}</h1>
          </template>
        </child>
      </div>
    </body>
    <script>
      //定义一个全局子组件
      Vue.component('child', {
        data: function () {
          return {
            list: [1, 2, 3, 4],
          };
        },
        template:
          '<div><ul><slot v-for="value in list" :value=value></slot></ul></div>',
      });
    
      var vm = new Vue({
        el: '#root',
      });
    </script>
    

vue 如何使用插件

直接安装,引入就能使用。vue 还是 js,只要是 js 那么所有的插件使用都是相同的方式,引入绑定到对应的节点或者操作对应的节点就好。

Vue 组件懒加载,图片懒加载

组件懒加载
  1. 结合路由插件使用的时候使用 import 方式实现
// 第一步注释import导入的文件
// import  About from '../components/About.vue';
// 第二步将引入组件的方式以箭头函数的方式异步引入
const routes = [
  {
    path: '/about'component: () => import( /* webpackChunkName: 'about' */ '../components/About.vue' )
  }
]
````
2. 引入组件的时候使用回调函数的方式引入,比如

```js
// 组件懒加载
const IconList = () => import('components/base/icon-list');

export default {
  components: {
    IconList,
  },
};
````
图片懒加载
就是在加载页面的时候,如果页面中的图片过多,可以使用占位符的方式替换没有在可是区域内的图片,只加载当前需要现实的图片。监听滚动条的位置,当图片标签出现在可视区域的时候,重置图片的路径为真是路径,然后展示图片地址。一般在实际开发的时候都直接使用图片懒加载插件实现。还有一种解决方案就是使用页面骨架屏效果,也是类似占位显示,当数据加载完成之后替换掉占位显示的内容

使用Vue封装过组件吗?有哪些?讲一下他们是怎么实现的

比如做后台管理中,很多模块经常会复用,比如侧边导航组件、项目中常用的 echarts图表的封装(比如折线图、柱状图等)

封装组件需要考虑复用性:

  • 预留插槽slot, 多次调用如果 子组件视图结构不一样那么就要 在 子组件template预留好 插槽(单个插槽、具名插槽,作用域插槽)
  • 考虑到数据传递,定义props 组件接收父组件传递的数据,同时需要注意单向数据流,props不能直接修改,$emit自定义事件,父组件修改
  • 业务逻辑不要在子组件中处理,子组件在不同父组件中调用时,业务处理代码不同,切记不要直接在子组件中处理业务,应该子组件 $emit自定义事件,将数据传递给父组件,父组件处理业务。

说说vuex的管理操作或理解

vuex是 vue的一个状态管理插件,采用集中式管理方式,来管理项目中多个组件的公共状态。

vuex有一个仓库概念,将组件公共的state存储在仓库的state属性中,state是只读的,组件只能使用,不能直接修改,修改需要通过 仓库中的mutations模块来修改,这样的好处是 当数据修改便于溯源,且不会因为 多个组件 直接修改数据,导致 组件间数据的互相影响, 同时 当我们仓库中有一个state 数据需要请求 数据接口才能获取时,vuex 设计了一个action模块,在action模块中发送异步请求,得到数据后,提交mutation来修改state。当state发生改变后组件自动刷新,在组件中可以commit mutation或者dispatch action来修改state。

具体工作流程如下图

说说Vuex的工作流程

vuex的仓库有5个模块,分别是 state,mutations, actions, getters, modules

我们将组件的公共状态定义在 vuex仓库的state中,state是只读的,无法直接修改,必须调动仓库中的某个mutation才能修改状态,getters可以理解为vuex中的计算属性,当我们在某个组件中使用vuex中的某个state时,不是直接使用原值,而是需要派生出一个新的值,就可以定义getters,可以在组件中获取。当依赖的state发生改变,此时getters会重新计算得到新值,同时 action中可以发送异步请求,得到数据后,commit mutation来给state赋值

具体代码如下:

仓库代码

const store = new Vuex.Store({
    state: {
        items: [] // 定义一个公共的购物车数据
    },
    getters: {
        // 可以基于已有的state 派生新的状态
        selectedItems (state) {
            // 过滤购物车中未选中的商品
            return state.items.filter(item => item.selected)
        }
    },
    mutations: {
        // 定义mutation来修改state
        INIT_ITEMS(state, items){
            state.items = items
        }
    },
    actions: {
        // action可以发送异步请求,得到数据后commit mutation将请求结果传入
        FETCH_ITEMS({commit}, params = {}){
            // 调用封装好的 接口函数
            fetchItem(params).then(res => {
                if(res.data.code === 200) {
                    commit('INIT_ITEMS', res.data.data)
                }
            })
        }
    }
})

组件中使用 使用vuex

// 获取state
this.$store.state.items // 直接获取
{
    computed: {
		...mapState(['items']) // 助手函数获取
    }
}
// 获取getters
this.$store.getters.selectedItems // 直接获取
{
    computed: {
        ...mapGetters(['selectedItems']) // 助手函数获取
    }
}
// 组件中提交action
this.$store.dispatch('FETCH_ITEMS', {token: 'xxx'})
{
    methods: {
        ...mapActions(['FETCH_ITEMS']) // 助手函数 直接调用this.FETCH_ITEMS(params)触发
    }
}
// 组件中也可以直接commit  mutation
this.$store.commit('INIT_ITEMS'[,参数])
{
    methods:{
        ...mapMutations(['INIT_ITEMS']) // 助手函数 直接调用this.INIT_ITEMS(参数)
    }
}

vuex项目中怎么使用?工作原理是什么?

原则:

​ 中小型项目中,如果组件的公共状态不多的情况下,不建议使用vuex,反而会增加代码复杂度,想要组件通信,直接通过event bus即可,中大型项目中,多个组件公共状态较多情况下,建议使用vuex

vuex的具体工作流程如下:

​ 在仓库state中定义公共状态,action中发送异步请求,得到数据后调用mutation 赋值给state,组件中使用state,也可以在组件中 dispatch action和触发mutation来修改state,视图刷新

具体代码如下:

仓库代码

const store = new Vuex.Store({
    state: {
        items: [] // 定义一个公共的购物车数据
    },
    getters: {
        // 可以基于已有的state 派生新的状态
        selectedItems (state) {
            // 过滤购物车中未选中的商品
            return state.items.filter(item => item.selected)
        }
    },
    mutations: {
        // 定义mutation来修改state
        INIT_ITEMS(state, items){
            state.items = items
        }
    },
    actions: {
        // action可以发送异步请求,得到数据后commit mutation将请求结果传入
        FETCH_ITEMS({commit}, params = {}){
            // 调用封装好的 接口函数
            fetchItem(params).then(res => {
                if(res.data.code === 200) {
                    commit('INIT_ITEMS', res.data.data)
                }
            })
        }
    }
})

组件中使用 使用vuex

// 获取state
this.$store.state.items // 直接获取
{
    computed: {
		...mapState(['items']) // 助手函数获取
    }
}
// 获取getters
this.$store.getters.selectedItems // 直接获取
{
    computed: {
        ...mapGetters(['selectedItems']) // 助手函数获取
    }
}
// 组件中提交action
this.$store.dispatch('FETCH_ITEMS', {token: 'xxx'})
{
    methods: {
        ...mapActions(['FETCH_ITEMS']) // 助手函数 直接调用this.FETCH_ITEMS(params)触发
    }
}
// 组件中也可以直接commit  mutation
this.$store.commit('INIT_ITEMS'[,参数])
{
    methods:{
        ...mapMutations(['INIT_ITEMS']) // 助手函数 直接调用this.INIT_ITEMS(参数)
    }
}

Vuex中处理异步需要在什么地方写

异步处理需要在 仓库的actions中定义

Action 类似于 mutation,不同在于:

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

我们可以在action中发送异步请求,成功后触发mutation 将结果传入,在mutation赋值给state

const store = new Vuex.Store({
    state: {
        items: [] // 定义一个公共的购物车数据
    },
    mutations: {
        // 定义mutation来修改state
        INIT_ITEMS(state, items){
            state.items = items
        }
    },
    actions: {
        // action可以发送异步请求,得到数据后commit mutation将请求结果传入
        FETCH_ITEMS({commit}, params = {}){
            // 调用封装好的 接口函数
            fetchItem(params).then(res => {
                if(res.data.code === 200) {
                    commit('INIT_ITEMS', res.data.data)
                }
            })
        }
    }
})

请你谈谈你对vuex的理解

vuex是专为vue设计的状态管理工具,可用于父子组件和非父子组件的全局组件通信。应用的状态集中放在store中,改变状态必须要经过commit,同步改变状态是提交mutations,异步是先通过actions再通过mutations。

一共有5大模块
- state
存放状态
- getters
就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
可以对state中的数据做一些处理
- mutations
更改 Vuex 的 store 中的状态的唯一方法是提交 mutation,通过store.commit提交到mutations模块
- actions
actions是异步的改变state中状态的方法,通过store.dispatch来提交到mutations模块,再通过提交commit来更改state中的状态
- modules
Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割

vuex—个模块中改变state中数据,其他模块如何获取

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。

为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割:

vuex模块化后,需要另一个模块的state变化,可以在这个模块中定义getters 获取,具体代码如下

// 模块a
const modulea = {
    namespaced: true,
    state: {
        num: 10
    },
    mutations: {
        ADD_NUM(state, n) {
            state.num += n
        }
    }
}
// 模块b
const moduleb = {
    namespaced: true,
    state: {
        num: 10
    },
    getters: {
        // 在这里拿到 模块a numstate
      moduleaNum (state, getters, rootState, rootGetters) {
          // 模块下的getter有四个参数分别是当前模块的state,当前模块的getters,以及根state个根getters可以通过rootState获取其他模块的state
          return rootState.modulea.num
          
      }  
    },
    mutations: {
        ADD_NUM(state, n) {
            state.num += n
        }
    }
}

vuex的状态是怎样的,怎么改变状态

vuex的状态储存在仓库的state属性中,state是只读的,无法直接修改必须调用mutation才能修改

const store = new Vuex.Store({
    state: {
        num: 10
    },
    mutations: {
        ADD_NUM (state, n) {
            state.num += n
        }
    }
})
// 在组件中直接出发mutaion
this.$store.commit('ADD_NUM', 10)
// 或者助手函数 提交 mutation
{
    methods: {
        ...mapMutations(['ADD_NUM'])
    }
}
// 直接调用即可
this.ADD_NUM(10)

你在项目中哪里使用vuex,vuex的应用场景

原则:

​ 中小型项目中,如果组件的公共状态不多的情况下,不建议使用vuex,反而会增加代码复杂度,想要组件通信,直接通过event bus即可,中大型项目中,多个组件公共状态较多情况下,建议使用vuex

在项目中,多个组件的公共状态可以存储的vuex中,比如电商网站的购物车数据,可以存储在vuex中。后台管理角色鉴权中的 不同角色的侧边栏数据,以及 不同角色可以访问的路由数据可以存储的vuex中,拿到数据储存。

vuex的数据丢失知道吗 怎么解决

原理:可以利用缓存,将vuex中的state,在缓存中备份一下,当状态发生改变时,同步缓存的的备份。同时当刷新时,去缓存中的备份,给state赋值

实际开发中我们一般利用vuex一个插件来实现 vuex-persistedstate

具体代码如下

安装

npm i vuex-persistedstate -S

使用

import Vuex from "vuex";
import createPersistedState from "vuex-persistedstate";

const store = new Vuex.Store({
  // ...
  plugins: [createPersistedState()],
});

vuex怎么拿数据

  • 获取vuex state中的count数据方法有

    方法1: this.$store.state.count 直接使用;

    	方法2:`import { mapState } from vuex `
    
    	然后把`...mapState('count')放入computed中`,然后直接使用count变量。
    
  • 获取vuex getters中getCount数据的方法

    方法1: this.$store.getters.getCount直接使用。

    	方法2:`import { mapGetters } from vuex `
    
    	然后把`...mapGetters ('getCount')放入computed中`,然后直接使用getCount变量。
    

说说Vuex原理

   vuex 是一个专门为 vue 构建的状态管理工具,主要是为了解决 多组间之间状态共享问题。强调的是集中式管理,(组件与组件之间的关系变成了组件与仓库之间的关系)

vuex 的核心包括:state(存放状态)、mutations(同步的更改状态)、actions(发送异步请求,拿到数据)、getters(根据之前的状态派发新的状态)、modules(模块划分)
  state 发布一条新的数据,在 getters 里面根据状态派发新的状态,actions 发送异步请求获取数据,然后在 mutations 里面同步的更改数据
  应用场合:购物车的数据共享、登入注册

vuex仓库数据很多,怎么管理

使用moduls模块划分和文件拆分来管理数据很多的问题。
例如:我们可以在modules中进行模块划分,比如用户相关模块放入user中,
文章信息相关模块放入article中。

代码如下:

modules:{
         user:{ //跟用户相关的数据放这
            state:{

            },
            geeters:{

            },
            mutations:{

            },
            actions:{

            },
        },
         article:{ //跟文章相关的保存在这里
            state:{

            },
            geeters:{

            },
            mutations:{

            },
            actions:{

            },
        }   
}        

vuex做数据集中管理,mutations和actions分别是做什么的,为什么不能用mutations处理异步数据

? mutations和actions分别是做什么的?

mutations和action都是用来改变Vuex store的状态的;mutations提供的回调函数是同步的;而actions提供的方法是异步的,此外,actions的方法最终还是通过调用mutations的方法来实现修改vuex的状态的。

? 为什么不能用mutations处理异步数据?

官方文档说明:“在 mutation 中混合异步调用会导致你的程序很难调试。例如,当你能调用了两个包含异步回调的 mutation 来改变状态,你怎么知道什么时候回调和哪个先回调呢?这就是为什么我们要区分这两个概念。在 Vuex 中,我们将全部的改变都用同步方式实现。我们将全部的异步操作都放在Actions中。”

actions 和 mutations 并不是为了解决竞态问题,而是为了能用 devtools 追踪状态变化。事实上在 vuex 里面 actions 只是一个架构性的概念,并不是必须的,说到底只是一个函数,你在里面想干嘛都可以,只要最后触发 mutation 就行。异步竞态怎么处理那是用户自己的事情。vuex 真正限制你的只有 mutation 必须是同步的这一点(在 redux 里面就好像 reducer 必须同步返回下一个状态一样)。同步的意义在于这样每一个 mutation 执行完成后都可以对应到一个新的状态(和 reducer 一样),这样 devtools 就可以打个 snapshot 存下来,然后就可以随便 time-travel 了。如果你开着 devtool 调用一个异步的 action,你可以清楚地看到它所调用的 mutation 是何时被记录下来的,并且可以立刻查看它们对应的状态。其实我有个点子一直没时间做,那就是把记录下来的 mutations 做成类似 rx-marble 那样的时间线图,对于理解应用的异步状态变化很有帮助。

vuex中actions与mutations的区别

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

Action Action 类似于 mutation,不同在于:
Action 提交的是 mutation,而不是直接变更状态。
Action 可以包含任意异步操作。
总体来说:
actions
1、用于通过提交mutation改变数据
2、会默认将自身封装为一个Promise
3、可以包含任意的异步操作
mutations
1、通过提交commit改变数据
2、只是一个单纯的函数
3、不要使用异步操作,异步操作会导致变量不能追踪

说说vue如何进行路由配置

一、安装

本地环境安装路由插件vue-router: cnpm install vue-router --save-dev

二、配置
两种配置方法:在main.js中 || 在src/router文件夹下的index.js中
这里只说在src/router/index.js中的
1.引入 import Vue from ‘vue’
import Router from 'vue-router'
注意这个Router是自定义的名字,这里叫这个名字后,下边都要用到的

  1. 使用/注册:
    Vue.use(Router)

  2. 配置
    配置路由:

    export default new Router({
      routes: [
    {
         path : ‘/’,  //到时候地址栏会显示的路径
         name : ‘Home’,
         component :  Home   // Home是组件的名字,这个路由对应跳转到的组件。。注意component没有加“s”.
     },
     {
         path : ‘/content’,
         name : ‘Content’,
         component :  Content
     }
    ],
     mode: "history"
    })
    
  3. 引入路由对应的组件地址:
    import Home from ‘@/components/Home’
    import Home from '@/components/Content’

  4. 在main.js中调用index.js的配置:
    import router from ‘./router’

  5. App.vue页面使用(展示)路由:

    把这个标签放到对应位置: <router-view></router-view>

  6. 路由切换(原来的等地方):把切换标签和链接改成:
    <router-link to="/">切换到Home组件</router-link><router-link to="/content">切换到Content组件</router-link>
    //这里,to里边的参数和配置时,path的路径一样即可

谈谈Vue路由守卫

1、路由守卫 是什么

简单来说,导航守卫就是路由跳转前、中、后过程中的一些钩子函数,这个函数能让你操作一些其他的事儿,这就是导航守卫。
官方解释,vue-router提供的导航守卫主要用来通过跳转或取消的方式守卫导航。

2、路由守卫分类
导航守卫分为:全局的、组件内的、单个路由独享三种
2.1 全局的
指路由实例上直接操作的钩子函数,他的特点是所有配置路由的组件都会触发
const router = new VueRouter({ … })
router.beforeEach((to, from, next) => {
// …
})
全局路由的钩子函数包括
beforeEach
在路由跳转前触发,参数包括to,from,next(参数会单独介绍)三个,这个钩子作用主要是用于登录验证
beforeResolve(2.5+)
这个钩子和beforeEach类似,也是路由跳转前触发,参数也是to,from,next三个,与beforeEach的区别参考官网。
afterEach
是在路由跳转完成后触发,参数包括to,from,它发生在beforeEach和beforeResolve之后,beforeRouteEnter(组件内守卫)之前。
2.2 路由独享的
指在单个路由配置的时候也可以设置的钩子函数
const router = new VueRouter({
routes: [
{
path: ‘/foo’,
component: Foo,
beforeEnter: (to, from, next) => {
// …
}
}
]
})
路由独享的钩子函数包括
beforeEnter
与全局的beforeEach完全相同,如果都设置则在beforeEach之后紧随执行,参数to、from、next
2.3 组件内的
指在组件内执行的钩子函数,类似于组件内的生命周期

... export default{ data(){ //... }, beforeRouteEnter (to, from, next) { // 在渲染该组件的对应路由被 confirm 前调用 // 不!能!获取组件实例 `this` // 因为当守卫执行前,组件实例还没被创建 }, beforeRouteUpdate (to, from, next) { // 在当前路由改变,但是该组件被复用时调用 // 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候, // 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。 // 可以访问组件实例 `this` }, beforeRouteLeave (to, from, next) { // 导航离开该组件的对应路由时调用 // 可以访问组件实例 `this` } } 组件内的路由钩子函数包括 beforeRouteEnter 路由进入组件之前调用,参数包括to,from,next。该钩子在全局守卫beforeEach和独享守卫beforeEnter之后,全局beforeResolve和全局afterEach之前调用,要注意的是该守卫内访问不到组件的实例,也就是this为undefined,也就是他在beforeCreate生命周期前触发。

beforeRouteUpdate
在当前路由改变时,并且该组件被复用时调用,可以通过this访问实例。参数包括to,from,next。

beforeRouteLeave
导航离开该组件的对应路由时调用,可以访问组件实例this,参数包括to,from,next。
3、路由守卫回调参数
to:目标路由对象;
from:即将要离开的路由对象;
next:他是最重要的一个参数,他相当于佛珠的线,把一个一个珠子逐个串起来。

路由守卫中页面跳转运用了哪些钩子函数

应用场景1:可进行一些页面跳转前处理,例如判断需要登录的页面进行拦截,做登录跳转!!

router.beforeEach((to, from, next) => {
    if (to.meta.requireAuth) {
        //判断该路由是否需要登录权限
        if (cookies('token')) {
            //通过封装好的cookies读取token,如果存在,name接下一步如果不存在,那跳转回登录页
            next()//不要在next里面加"path:/",会陷入死循环
        }
        else {
            next({
                path: '/login',
                query: {redirect: to.fullPath}//将跳转的路由path作为参数,登录成功后跳转到该路由
            })
        }
    }
    else {
        next()
    }
})

应用场景2,进入页面登录判断、管理员权限判断、浏览器判断

//使用钩子函数对路由进行权限跳转
router.beforeEach((to, from, next) => {
    const role = localStorage.getItem('ms_username');
    if(!role && to.path !== '/login'){
        next('/login');
    }else if(to.meta.permission){
        // 如果是管理员权限则可进入,这里只是简单的模拟管理员权限而已
        role === 'admin' ? next() : next('/403');
    }else{
        // 简单的判断IE10及以下不进入富文本编辑器,该组件不兼容
        if(navigator.userAgent.indexOf('MSIE') > -1 && to.path === '/editor'){
            Vue.prototype.$alert('vue-quill-editor组件不兼容IE10及以下浏览器,请使用更高版本的浏览器查看', '浏览器不兼容通知', {
                confirmButtonText: '确定'
            });
        }else{
            next();
        }
    }
})

应用场景3:当页面中有未关闭的窗口, 或未保存的内容时, 阻止页面跳转

beforeRouteLeave (to, from, next) {
 //判断是否弹出框的状态和保存信息与否
 if (this.dialogVisibility === true) {
    this.dialogVisibility = false //关闭弹出框
    next(false) //回到当前页面, 阻止页面跳转
  }else if(this.saveMessage === false) {
    alert('请保存信息后退出!') //弹出警告
    next(false) //回到当前页面, 阻止页面跳转
  }else {
    next() //否则允许跳转
  }

谈谈Vue路由模式,路由有哪些模式

在vue-router路由对象中,路由有两种模式:hash和history,而默认的是hash模式.
前端路由目前主要有两种方法:
1、利用url的hash,就是常用的锚点(#)操作,类似页面中点击某小图标,返回页面顶部,JS通过hashChange事件来监听url的改变,IE7及以下需要轮询进行实现。一般常用框架的路由机制都是用的这种方法,例如Angualrjs自带的ngRoute和二次开发模块ui-router,react的react-route,vue-route…
2、利用HTML5的History模式,使url看起来类似普通网站,以”/”分割,没有”#”,但页面并没有跳转,不过使用这种模式需要服务器端的支持,服务器在接收到所有的请求后,都指向同一个html文件,通过historyAPI,监听popState事件,用pushState和replaceState来实现。

路由守卫的作用,全局和局部使用的差异是什么

全局路由守卫:就是在整个网页中,只要发生了路由的变化,都会触发。全局导航守卫主要包括两个函数,分别为:beforeEach、afterEach。
局部路由守卫:(组件内的守卫)只有当前路由使用。
路由加载之前触发:beforeRouteEnter (to, from, next)
更新路由之前触发:beforeRouteUpdate (to, from, next)
离开当前路由之前触发:beforeRouteLeave (to, from, next)

写出路由传参的具体实现

方式一:params 传参(显示参数)
params 传参(显示参数)又可分为 声明式 和 编程式 两种方式
1、声明式 router-link
该方式是通过 router-link 组件的 to 属性实现,该方法的参数可以是一个字符串路径,或者一个描述地址的对象。使用该方式传值的时候,需要子路由提前配置好参数,例如:
//子路由配置
{
path: ‘/child/:id’,
component: Child
}
//父路由组件
进入Child路由
2、编程式 this. r o u t e r . p u s h 使 用 该 方 式 传 值 的 时 候 , 同 样 需 要 子 路 由 提 前 配 置 好 参 数 , 例 如 : / / 子 路 由 配 置 p a t h : ′ / c h i l d / : i d ′ , c o m p o n e n t : C h i l d / / 父 路 由 编 程 式 传 参 ( 一 般 通 过 事 件 触 发 ) t h i s . router.push 使用该方式传值的时候,同样需要子路由提前配置好参数,例如: //子路由配置 { path: '/child/:id', component: Child } //父路由编程式传参(一般通过事件触发) this. router.push使//path:/child/:id,component:Child//()this.router.push({
path:'/child/KaTeX parse error: Expected 'EOF', got '}' at position 8: {id}', }̲) 在子路由中可以通过下面代码…route.params.id
方式二:params 传参(不显示参数)
params 传参(不显示参数)也可分为 声明式 和 编程式 两种方式,与方式一不同的是,这里是通过路由的别名 name 进行传值的

1、声明式 router-link
该方式也是通过 router-link 组件的 to 属性实现,例如:
进入Child路由
2、编程式 this. r o u t e r . p u s h 使 用 该 方 式 传 值 的 时 候 , 同 样 需 要 子 路 由 提 前 配 置 好 参 数 , 不 过 不 能 再 使 用 : / i d 来 传 递 参 数 了 , 因 为 父 路 由 中 , 已 经 使 用 p a r a m s 来 携 带 参 数 了 , 例 如 : / / 子 路 由 配 置 p a t h : ′ / c h i l d , n a m e : ′ C h i l d ′ , c o m p o n e n t : C h i l d / / 父 路 由 编 程 式 传 参 ( 一 般 通 过 事 件 触 发 ) t h i s . router.push 使用该方式传值的时候,同样需要子路由提前配置好参数,不过不能再使用 :/id 来传递参数了,因为父路由中,已经使用 params 来携带参数了,例如: //子路由配置 { path: '/child, name: 'Child', component: Child } //父路由编程式传参(一般通过事件触发) this. router.push使使:/id使params//path:/child,name:Child,component:Child//()this.router.push({
name:‘Child’,
params:{
id:123
}
})
在子路由中可以通过下面代码来获取传递的参数值
this. r o u t e . p a r a m s . i d 注 意 : 上 述 这 种 利 用 p a r a m s 不 显 示 u r l 传 参 的 方 式 会 导 致 在 刷 新 页 面 的 时 候 , 传 递 的 值 会 丢 失 方 式 三 : q u e r y 传 参 ( 显 示 参 数 ) q u e r y 传 参 ( 显 示 参 数 ) 也 可 分 为 声 明 式 和 编 程 式 两 种 方 式 1 、 声 明 式 r o u t e r − l i n k 该 方 式 也 是 通 过 r o u t e r − l i n k 组 件 的 t o 属 性 实 现 , 不 过 使 用 该 方 式 传 值 的 时 候 , 需 要 子 路 由 提 前 配 置 好 路 由 别 名 ( n a m e 属 性 ) , 例 如 : / / 子 路 由 配 置 p a t h : ′ / c h i l d , n a m e : ′ C h i l d ′ , c o m p o n e n t : C h i l d / / 父 路 由 组 件 < r o u t e r − l i n k : t o = " n a m e : ′ C h i l d ′ , q u e r y : i d : 123 " > 进 入 C h i l d 路 由 < / r o u t e r − l i n k > 2 、 编 程 式 t h i s . route.params.id 注意:上述这种利用 params 不显示 url 传参的方式会导致在刷新页面的时候,传递的值会丢失 方式三:query 传参(显示参数) query 传参(显示参数)也可分为 声明式 和 编程式 两种方式 1、声明式 router-link 该方式也是通过 router-link 组件的 to 属性实现,不过使用该方式传值的时候,需要子路由提前配置好路由别名(name 属性),例如: //子路由配置 { path: '/child, name: 'Child', component: Child } //父路由组件 <router-link :to="{name:'Child',query:{id:123}}">进入Child路由</router-link> 2、编程式 this. route.params.idparamsurlqueryquery1routerlinkrouterlinkto使name//path:/child,name:Child,component:Child//<routerlink:to="name:Child,query:id:123">Child</routerlink>2this.router.push
使用该方式传值的时候,同样需要子路由提前配置好路由别名(name 属性),例如:
//子路由配置
{
path: '/child,
name: ‘Child’,
component: Child
}
//父路由编程式传参(一般通过事件触发)
this. r o u t e r . p u s h ( n a m e : ′ C h i l d ′ , p a r a m s : i d : 123 ) 在 子 路 由 中 可 以 通 过 下 面 代 码 来 获 取 传 递 的 参 数 值 t h i s . router.push({ name:'Child', params:{ id:123 } }) 在子路由中可以通过下面代码来获取传递的参数值 this. router.push(name:Child,params:id:123)this.route.query.id

vue路由有哪几种

this. r o u t e r . p u s h ( o b j ) 跳 转 到 指 定 u r l 路 径 , 并 想 h i s t o r y 栈 中 添 加 一 个 记 录 , 点 击 后 退 会 返 回 到 上 一 个 页 面 t h i s . router.push(obj) 跳转到指定url路径,并想history栈中添加一个记录,点击后退会返回到上一个页面 this. router.push(obj)urlhistory退this.router.replace(obj) 跳转到指定url路径,但是history栈中不会有记录
this.$router.go(n) 向前或者向后跳转n个页面,n可为正整数或负整数

说一下vue路由跳转方式

1、router-link 【实现跳转最简单的方法】
<router-link to='需要跳转到的页面的路径>
浏览器在解析时,将它解析成一个类似于<a>的标签。
div和css样式略

 <li >
 <router-link to="keyframes">点击验证动画效果 </router-link>  
 </li>

2、this.$router.push({ path:’/user’})
3、this.$router.replace{path:‘/’ }类似,不再赘述

vue路由的数据传参,params,query的区别。使用params什么时候会生效,什么时候不会生效,params有时会失效,为什么

query和params区别
query类似 get, 跳转之后页面 url后面会拼接参数,类似?id=1, 非重要性的可以这样传,刷新页面id还在
params类似 post, 跳转之后页面 url后面不会拼接参数 , 但是刷新页面id 会消失
注意:如果提供了path,params会被忽略而导致失效。
通过官网我们知道路由中的name是不能重复的,而path是可以的。所以在函数式编程中,我们可以用变量来控制路径。

路由守卫路由拦截如何配置

通常在项目里,我们需要用户进行登录,才能让用户查看项目。在后台管理系统中,会根据不同的用户权限展示不同的内容。
在用户访问页面之前,我们通过全局前置守卫对路由进行拦截,看看你是不是可以通过。通过的标准是否登录,如果登录就通过放行,没有通过就打回。

// 不需要路由验证页面 
const whiteList = ['login', 'index']

router.beforeEach((to, from, next) => {
    // 确定用户是否已登录  
    const hasToken = false  // 这里就是路由是否通过标准,一般都是通过token来验证
if (hasToken) {   // 登录
  if (to.path === '/login') {  
    // 如果已登录,请重定向到主页
    next({ path: '/index' })
    return
  } 
  next()
} else {
  if (whiteList.indexOf(to.name) !== -1) {
    // 在免费登录白名单中,直接进入
    next()
  } else {
    // 没有访问权限的其他页将重定向到登录页。
    next(`/login`)
  }
}
})

需要注意的一点是,用户没有登录,是需要跳转到登录页面,如果在白名单里面没有登录页或者没有next(),页面一直跳转直到内存溢出。

每个项目的验证是否拥有权限不一样,权限判断那一块可以根据自己的实项目需求来进行操作。

写公用组件的时候,怎么提高可配置性

  1. 带着开放封闭原则的视角思考

    开放原则,是说我们需要将有可能变动的属性,通过props接口的方式暴露给组件的调用者。

    封闭原则,意思是我们需要将该组件的通用能力及逻辑封装再组件内部,让调用者使用更加方便

  2. 组件的可配置性需要结合实际的业务需求具体分析

    假设我们要封装一个Tab选项卡组件,实际功能交互可参考组件 | Element

  3. 组件的开放原则实践

    参数说明类型可选值默认值
    value / v-model绑定值,选中选项卡的 namestring第一个选项卡的 name
    type风格类型stringcard/border-card
    closable标签是否可关闭booleanfalse
    addable标签是否可增加booleanfalse
    editable标签是否同时可增加和关闭booleanfalse
    tab-position选项卡所在位置stringtop/right/bottom/lefttop
    stretch标签的宽度是否自撑开boolean-false
    before-leave切换标签之前的钩子,若返回 false 或者返回 Promise 且被 reject,则阻止切换。Function(activeName, oldActiveName)

上面的表格为Tab组件提供的props配置接口,它们需要遵循如下特点,可以极大提高可配置性:

  • 配置项要尽可能多的,覆盖到业务中可能出现的每一种情况。
  • 保证组件在每一项配置缺省状态下都能够正常表现
  • 每一项配置都应该具备合理的默认值。
  1. 组件的封闭原则实践

    事件名称说明回调参数
    tab-clicktab 被选中时触发被选中的标签 tab 实例
    tab-remove点击 tab 移除按钮后触发被删除的标签的 name
    tab-add点击 tabs 的新增按钮后触发
    edit点击 tabs 的新增按钮或 tab 被关闭后触发(targetName, action)

上面的表格为Tab组件所提供的自定义事件,但是事件相关的逻辑实现,已经完全在组件内部封装好了,组件使用者,只需要按需绑定事件即可对组件的行为做出监听,这些事件也需要遵循如下特点,才能保证该组件的可配置性:

  • 事件函数相关逻辑,必须在组件内部封装完善

  • 自定义事件函数在触发时,必须能够返回相关的参数

    例如 @tab-click 事件会给函数返回,被点击菜单的index下标等信息

  • 事件函数本身也需要能够在缺省状态下,保持组件的正常运行。

vue-router怎么生成动态地址

  1. 动态路由配置的应用场景

    一般我们在使用vue-router进行路由管理的时候,是通过如下方式配置路由组件之间的映射关系:

    // router/index.js配置文件
    const router = new VueRouter({
        routes:[
            {
                path:'/login',
                component:()=>import ('../views/Login') //登录路由
            },
            {
                path:'/reg',
                component:()=>import ('../views/Reg') //注册路由
            },
            {
                path:'/admin',
                component:()=>import ('../views/Admin') //这是一个管理员才能访问的路由
            },
            {
                path:'/vip',
                component:()=>import ('../views/Vip') //假设,这是要给vip用户才能访问的路由
            },
        ]
    })
    

    但是在后台管理平台这种类型的项目中,我们需要让拥有不同角色权限的用户,访问不同的菜单路由,如上述代码所示,部分路由只有管理员才能访问,而另外一部分路由只能vip用户才能访问,所以需要用到vue-router提供的addRoute方法来动态管理这一部分路由配置。

  2. 本地只配置通用路由

    我们为了实现路由的动态配置,需要将上述路由配置进行拆分,本地配置文件中,只保留通用的路由映射。

    const router = new VueRouter({
        routes:[
            {
                path:'/login',
                component:()=>import ('../views/Login') //登录路由
            },
            {
                path:'/reg',
                component:()=>import ('../views/Reg') //注册路由
            }
        ]
    })
    
  3. 后端为每个用户分配一个角色,随登录接口下发给前端
    app.get('/login',(req,res)=>{
    	//此处需要实现登录相关逻辑
    	res.send({
    		username:'张三丰',
    		role:'admin',  //标志当前用户角色
    		routerList:[  //此处的路由配置,也可以通过独立接口向前端提供
    			{
                    path:'/admin',
                    component:()=>import ('../views/Admin') //这是一个管理员才能访问的路由
                },
                ...此处可能会有很多其他路由,这些路由数据应该由专门的数据表来存储
    		]
    	})
    })
    
  4. 前端登录并动态获取路由配置

    前端登录成功后,会得到后端动态下发的,跟自己账号角色相匹配的路由数据,此时可以通过addRoute方法,将这些动态获取的路由配置数据包,设置给router对象

    // views/Login.vue 登录面板
    
    axios.get('/login',(result)=>{
    	let {routerList} = result
    	routerList.forEach((item) => {
            this.$router.addRoute(item)
        })
    })
    

vue-router有哪些方法

  1. router.beforeEach 路由守卫

    我们可以使用这个方法,按需拦截用户访问某些敏感路由,例如:

    router.beforeEach((to,from,next)=>{ //路由的全局前置守卫
        if(to.path.indexOf('/account')==-1){ //判断用户访问的是不是个人中心
            next()  //不是个人中心,直接放行
        }else{
            if(store.state.my.userInfo){ //判断登录状态
                next() //如果已经登录,直接放行
            }else{
                next('/login')  //如果没有登录,则跳至登录页
            }
        }
    })
    
  2. router.push 编程式导航

    通过编程式导航,我们可以通过事件的方式触发路由跳转

    // 字符串
    router.push('home')
     
    // 对象
    router.push({ path: 'home' })
     
    // 命名的路由
    router.push({ name: 'user', params: { userId: 123 }})
     
    // 带查询参数,变成 /register?plan=private
    router.push({ path: 'register', query: { plan: 'private' }})
    
  3. router.go、router.back、router.forward 路由的进入与返回

    router.go 作用等同于window.history.go

    // 在浏览器记录中前进一步,等同于 history.forward()
    router.go(1)
     
    // 后退一步记录,等同于 history.back()
    router.go(-1)
     
    // 前进 3 步记录
    router.go(3)
    
    
  4. router.addRoute 动态设置路由映射

    添加一条新路由规则。如果该路由规则有 name,并且已经存在一个与之相同的名字,则会覆盖它。

    axios.get('/login',(result)=>{  //通过异步接口获取对应用户的特有路由配置
    	let {routerList} = result
    	routerList.forEach((item) => {
            this.$router.addRoute(item)  //通过addRoute方法依次将路由配置设置给router对象           
        })
    })
    

谈谈对MVVM的理解

  1. 什么是MVVM

    不管是MVC,MVP,或者MVVM,都是常见的软件架构设计模式(Architectural Pattern),它通过分离关注点来改进代码的组织方式。不同于设计模式(Design Pattern),只是为了解决一类问题而总结出的抽象方法,一种架构模式往往使用了多种设计模式。MVVM,可以拆分为Model-View-ViewModel来理解:

    • Model - 数据模型,可以对应到真实开发过程中的数据包
    • View - 视图层,布局和外观,可以对应到真实开发中的DOM结构
    • ViewModel - 扮演“View”和“Model”之间的使者,帮忙处理 View 视图层的全部业务逻辑
  2. 为什么使用MVVM框架

    要回答这个问题,我们需要对比一下,在使用MVVM框架之前,我们是如何完成前端交互的。

    • 使用前

    为了修改某个视图节点中的内容信息,我们需要频繁人为操作DOM,效率低下

    var dom = document.querySelector('div');
    dom.innerHTML = '张三丰';
    dom.style.color = 'red';
    
    • 使用后

    当name数据发生变化的时候,视图区域的name自定触发更新,极大提高开发效率

    <div>{{name}}</div>
    
    data:{
    	name:'张三丰'
    }
    
  3. 通过上述案例进一步理解MVVM
    • name数据包可以认为就是那个Model
    • div 节点可以认为就是那个View
    • 而Vue提供的语法环境所支持的数据驱动能力,就可以认为是那个ViewModel

vue-router有什么组件

  1. router-link 组件

    <router-link> 组件支持用户在具有路由功能的应用中 (点击) 导航。 通过 to 属性指定目标地址,默认渲染成带有正确链接的 <a> 标签,可以通过配置 tag 属性生成别的标签.。另外,当目标路由成功激活时,链接元素自动设置一个表示激活的 CSS 类名。

    <router-link> 比起写死的 <a href="..."> 会好一些,理由如下:

    • 无论是 HTML5 history 模式还是 hash 模式,它的表现行为一致,所以,当你要切换路由模式,或者在 IE9 降级使用 hash 模式,无须作任何变动。
    • 在 HTML5 history 模式下,router-link 会守卫点击事件,让浏览器不再重新加载页面。
    • 当你在 HTML5 history 模式下使用 base 选项之后,所有的 to 属性都不需要写 (基路径) 了。
  2. router-view组件

    <router-view> 组件是一个 functional 组件,渲染路径匹配到的视图组件。<router-view> 渲染的组件还可以内嵌自己的 <router-view>,根据嵌套路径,渲染嵌套组件。

    其他属性 (非 router-view 使用的属性) 都直接传给渲染的组件, 很多时候,每个路由的数据都是包含在路由参数中。

    因为它也是个组件,所以可以配合 <transition><keep-alive> 使用。如果两个结合一起用,要确保在内层使用 <keep-alive>

    <transition>
      <keep-alive>
        <router-view></router-view>
      </keep-alive>
    </transition>
    

    vue-router和location.href的用法区别

  3. vue-router使用pushState进行路由更新,静态跳转,页面不会重新加载;

  4. location.href会触发浏览器,页面重新加载一次

  5. vue-router使用diff算法,实现按需加载,减少dom操作

  6. vue-router是路由跳转或同一个页面跳转;location.href是不同页面间跳转;

  7. vue-router是异步加载this.$nextTick(()=>{获取url});location.href是同步加载

vue-cli怎么自定义组件

  1. 组件封装
// HelloWorld.vue组件
<template>
  <div>
    自定义组件
  </div>
</template>

<script>
export default {
  data() {
    return {
      key: 'value'
    }
  },
  // 组件交互
}
</script>

<style scoped lang="less">
// 组件样式
</style>

  1. 局部注册调用组件
// Test.vue
<template>
    <div>
        <HelloWorld/>
    </div>
</template>

<script>
    import HelloWorld from './HelloWorld.vue'
    export default {
        components:{
            HelloWorld
        }
    }
</script>

<style lang="less" scoped>
</style>
  1. 全局注册使用
  • 先在main.js中全局注册该组件
import Vue from 'vue'
import App from './App.vue'

//全局注册
import HelloWorld from './components/HelloWorld.vue'
Vue.component('hello-world',HelloWorld)

new Vue({
  render: h => h(App),
}).$mount('#app')

  • 然后在需要使用公共组件的业务组件中,调用该组件
// Test.vue
<template>
    <div>
        <hello-world></hello-world>
    </div>
</template>

<script>
    export default {
        
    }
</script>

<style lang="less" scoped>
</style>

谈谈对vue-loader的理解,实现原理是什么

一. vue-loader的作用是什么
  1. 首先我们需要达成共识的是,目前浏览器,只能识别普通的html、css、javascript。

  2. 但是为了能够方便使用vue的组件化开发,需要我们将代码写在.vue单文件组件中。

  3. .vue文件,以及其内部的template、style、script区域代码,不能直接交给浏览器去解析,因为它解析不了。

  4. 所以我们需要一个vue-loader进行.vue单文件组件代码的转换,也就是

    .vue方便开发 ------> vue-laoder协助翻译 -----> 浏览器才能展示

二. vue-loader 工作原理

vue-loader 的工作流程, 简单来说,分为以下几个步骤:

  1. 将一个 .vue 文件 切割成 templatescriptstyles 三个部分。
  2. template 部分 通过 compile 生成 renderstaticRenderFns
  3. 获取 script 部分 返回的配置项对象 scriptExports
  4. styles 部分,会通过 css-loadervue-style-loader, 添加到 head 中, 或者通过 css-loaderMiniCssExtractPlugin 提取到一个 公共的css文件 中。
  5. 使用 vue-loader 提供的 normalizeComponent 方法, 合并 scriptExports、render、staticRenderFns, 返回 构建vue组件需要的配置项对象 - options, 即 {data, props, methods, render, staticRenderFns…}

vue实例中的data,在生命周期哪里能找到

如果想要了解哪个生命周期中可以找到vue实例的data,那我们必须了解,vue实例初始化的基本流程。

一、vue实例的初始化过程
1. new Vue

new Vue(options) 开始作为入口,Vue 只是一个简单的构造函数,内部是这样的:

function Vue (options) {
  this._init(options)
}
复制代码

进入了 _init 函数之后,先初始化了一些属性。

  1. initLifecycle:初始化一些属性如$parent$children。根实例没有 $parent$children 开始是空数组,直到它的 子组件 实例进入到 initLifecycle 时,才会往父组件的 $children 里把自身放进去。所以 $children 里的一定是组件的实例。
  2. initEvents:初始化事件相关的属性,如 _events 等。
  3. initRender:初始化渲染相关如 $createElement,并且定义了 $attrs$listeners浅层响应式属性。具体可以查看细节章节。并且还定义了$slots$scopedSlots,其中 $slots 是立刻赋值的,但是 $scopedSlots 初始化的时候是一个 emptyObject,直到组件的 vm._render 过程中才会通过 normalizeScopedSlots 去把真正的 $scopedSlots 整合后挂到 vm 上。

然后开始第一个生命周期:

callHook(vm, 'beforeCreate')
复制代码
2. beforeCreate被调用完成

beforeCreate 之后

  1. 初始化 inject

  2. 初始化

    state
    
    • 初始化 props
    • 初始化 methods
    • 初始化 data
    • 初始化 computed
    • 初始化 watch
  3. 初始化 provide

所以在 data 中可以使用 props 上的值,反过来则不行。

然后进入 created 阶段:

callHook(vm, 'created')
复制代码
3. created被调用完成

调用 $mount 方法,开始挂载组件到 dom 上。

如果使用了 runtime-with-compile 版本,则会把你传入的 template 选项,或者 html 文本,通过一系列的编译生成 render 函数。

  • 编译这个 template,生成 ast 抽象语法树。
  • 优化这个 ast,标记静态节点。(渲染过程中不会变的那些节点,优化性能)。
  • 根据 ast,生成 render 函数。

对应具体的代码就是:

const ast = parse(template.trim(), options)
if (options.optimize !== false) {
  optimize(ast, options)
}
const code = generate(ast, options)
复制代码

如果是脚手架搭建的项目的话,这一步 vue-cli 已经帮你做好了,所以就直接进入 mountComponent 函数。

那么,确保有了 render 函数后,我们就可以往渲染的步骤继续进行了

二、结论

通过上面的vue实例化的前面几步我们可以知道,在created生命周期中,我们就可以对data做操作,因为此时根实例相关的属性都已准备完毕。那在created后面执行的生命周期,自然也都可以获取并操作data,所以能够操作data的常用生命周期就有如下几个:

  • created
  • beforeMount
  • mounted
  • beforeUpdate
  • updated

vue的路由有哪些钩子函数,可以用来做什么

一、全局守卫

顾名思义,是要定义在全局的,也就是我们 index.js 中的 router 对象。

1. beforeEach

全局前置守卫,在路由跳转前触发,它在 每次导航 时都会触发。

通过 router.beforeEach 注册一个全局前置守卫。

router.beforeEach((to, from, next) => {
  console.log('??~ to:', to);
  console.log('??~ from:', from);
  next();
})
复制代码

参数

beforeEach 全局前置守卫接收三个参数

  • to: Route: 即将要进入的目标路由对象
  • from: Route: 当前导航正要离开的路由对象
  • next: Function: 一定要调用该方法不然会阻塞路由。

注意: next 参数可以不添加,但是一旦添加,则必须调用一次,否则路由跳转等会停止。

next()方法的几种情况

  • next(): 进行管道中的下一个钩子。
  • next(false): 中断当前的导航。回到 from 路由对应的地址。
  • next(‘/’) 或者 next({ path: ‘/’ }): 跳转到一个不同的地址,可传递的参数与 router.push 中选项一致。
  • next(error): 导航终止,且该错误会被传递给 router.onError() 注册过的回调。、
2. beforeResolve

全局解析守卫,在路由跳转前,所有 组件内守卫异步路由组件 被解析之后触发,它同样在 每次导航 时都会触发。

通过 router.beforeResolve 注册一个全局解析守卫。

router.beforeResolve((to, from, next) => {
  next();
})
复制代码

回调参数,返回值和 beforeEach 一样。也可以定义多个全局解析守卫。

3. afterEach

全局后置钩子,它发生在路由跳转完成后,beforeEachbeforeResolve 之后,beforeRouteEnter(组件内守卫)之前。它同样在 每次导航 时都会触发。

通过 router.afterEach 注册一个全局后置钩子。

router.afterEach((to, from) => {
  console.log('??~ afterEach:');
})
复制代码

这个钩子的两个参数和 beforeEach 中的 tofrom 一样。然而和其它全局钩子不同的是,这些钩子不会接受 next 函数,也不会改变导航本身。

二、路由守卫

顾名思义,就是跟路由相关的钩子,我们的路由守卫只有一个,就是 beforeEnter

1. beforeEnter

需要在路由配置上定义 beforeEnter 守卫,此守卫只在进入路由时触发,在 beforeEach 之后紧随执行,不会在 paramsqueryhash 改变时触发。

//index.js
{
  path: '/a',
  component: () => import('../components/A.vue'),
  beforeEnter: (to, from) => {
   console.log('??~ beforeEnter ');
  },
},
复制代码

beforeEnter 路由守卫的参数是 tofromnext ,同 beforeEach 一样。

三、组件守卫

顾名思义,是定义在路由组件内部的守卫。

1. beforeRouteEnter
  //A.vue
  beforeRouteEnter(to, from,next) {
    console.log('??~ beforeRouteEnter');
  },
复制代码

路由进入组件之前调用,该钩子在全局守卫 beforeEach 和路由守卫 beforeEnter 之后,全局 beforeResolve 和全局 afterEach 之前调用。

参数包括 tofromnext

该守卫内访问不到组件的实例,也就是 thisundefined,也就是他在 beforeCreate 生命周期前触发。

2. beforeRouteUpdate
  //A.vue
  beforeRouteUpdate(to, from) {
    console.log('??~ beforeRouteUpdate');
  },
复制代码

对于 beforeRouteUpdate 来说,this 已经可用了,所以给 next 传递回调就没有必要了。

3. beforeRouteLeave
  //A.vue
  beforeRouteLeave(to, from) {
    console.log('??~ beforeRouteLeave');
  },
复制代码

对于 beforeRouteLeave 来说,this 已经可用了,所以给 next 传递回调就没有必要了。

四、总结

完整的导航解析流程

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

计算属性通常用来干什么

一、理解计算属性的概念

计算属性的英文是computed,其实很多时候,一些概念从英文对照为中文后,会导致其中一些含义发生错位与丢失,我们要想更好的理解computed计算属性,可以将这个概念分为两部分来看:

  • 计算

    这里的计算,是有种宏观的概念,指的是对于数据包的一种操作,比如:筛选、过滤、新增、删除等。

    说明computed本身是具有处理数据的能力。

  • 属性

    属性的意思是,一种可以读取、渲染的数据,性质跟data的作用相似

    说明computed最终会给出一个数据供页面渲染使用。

由此,我们可以得出一个结论:

computed计算属性,负责将一些数据在其内部按照指定逻辑处理后,最终给出处理后的结果数据,给到组件、页面进行渲染使用,可以让开发者更加便捷的处理一些动态变化的数据需求。

二、computed计算属性的特点
  1. computed内部对data做出处理,其处理结果可以像data一样,可以直接在页面中渲染。
  2. computed内部逻辑会自动识别到data的变化,从而做出新的操作,得出新的数据,从而自动更新视图。
  3. computed处理的数据具有缓存的能力,如果data不变,则页面中调用的都是computed第一次执行时的运算结果,提高渲染性能。
三、computed应用场景

当某个前端已经指定的data数据包,如果我们渲染前,对其有过滤、筛选等操作需求,就可以使用computed

  1. 字符串翻转的官方案例,就是将一个普通字符串翻转后再渲染到视图。
new Vue({
        el:'#app',   //占地盘
        data:{
            'msg':'Hello Vue'  //自定义数据包
        },
        computed:{
            reMsg(){ //专门用于反转msg的计算属性
                return this.msg.split('').reverse().join('')
            }
        }
    })
  1. 如果一个班级中有很多学生,每个学生通过一个对象来表达,现在我们需要根据学员成绩来进行动态切换显示:全部学员及格学员不及格学员,这种在本地进行筛选的需求可以快速通过computed实现,代码逻辑大致如下:
new Vue({
        el:'#app',   //占地盘
        data:{
            stu:[
                {name:'张三丰',score:100},
                {name:'DDK',score:50},
                {name:'张翠山',score:60},
                {name:'张无忌',score:90},
                {name:'PDD',score:45}
            ],
            status:0  //0全部 1及格  2不及格
        },
        computed:{
            filterStu(){
                let {stu,status} = this
                // let stu = this.stu
                // let status = this.status
                switch (status) {
                    case 1:  //及格
                        return stu.filter(item=>{
                            return item.score>=60
                        })
                    case 2: //不及格
                        let arr = stu.filter(item=>{
                            return item.score<60
                        })
                        return arr
                    default:  //全部
                        return stu
                }

            }
        }
    })

computed和watch的区别

上一个问题我们已经详细探讨了computed的相关特征,在这里我们可以逐一对比一下:

  1. 两者都跟data有关,区别在于
    • computed 在处理完data后,会提供一个新的数据包以供使用
    • watch 只会监听某个指定data的变化,执行相关的逻辑,不会提供新的数据
  2. 两者的使用倾向不同
    • computed 内部虽然有逻辑,但是使用时更加多的关心其提供的新数据包
    • watch 更加关注data数据变化所引发的行为逻辑

状态管理器的数据走向是什么

一、什么是状态管理?

状态管理就是,把组件之间需要共享的状态抽取出来,遵循特定的约定,统一来管理,让状态的变化可以预测

二、为什么需要状态管理?
状态共享

组件之间通常会有一些共享的状态,在 Vue 或者 React 中我们一般会将这部分状态提升至公共父组件的 props 中,由父组件来统一管理共享的状态,状态的改变也是由父组件执行并向下传递。这样会导致两个问题:

  • 需要将共享的状态提升至公共的父组件,若无公共的父组件,往往需要自行构造
  • 状态由父组件自上而下逐层传递,若组件层级过多,数据传递会变得很冗杂
变化跟踪

在应用调试过程中,可能会有跟踪状态变化过程的需求,方便对某些应用场景的复现和回溯。这时候就需要统一对状态进行管理,并遵循特定的约定去变更状态,从而让状态的变化可预测。

三、单项数据流

因为在真实项目开发过程中,Store状态管理器中的数据会在很多组件中用到,如果不设定一个统一的规范去管理数据,最终将会导致数据混乱、使得项目变得难以维护。所以vuex状态管理器设计了如下几个核心api,与视图之间进行交互配合:

  • state

    vuex提供的,用以集中存储共享的数据。

  • mutations

    vuex提供的,专门用以触发state数据变化的方法集,并且要求mutations的方法执行结果必须时可预测的,在其内部不能出现异步请求等不可预测的逻辑。

  • actions

    vuex提供的,专门用于让vuex进行异步请求处理的方法集,可选择使用。

  • view

    视图层,整个项目组件的代称,我们在此处消费状态管理器提供的数据、方法。

数据走向必须遵循单向数据流的规范:

  1. 当我们初始化使用状态机数据时的流程是

    store---->state----> view

  2. 当组件内部想要本地更新状态管理器的数据,其流程是

    view触发---->mutations---->state---->store---->view更新

  3. 当组件内部想要在异步请求后,再更新本地状态管理器的数据,其流程是

    view触发---->actions---->mutations---->state---->store---->view更新

vuex数据丢失怎么解决

vuex的 store 中的数据是保存在运行内存中的,当页面刷新时,页面会重新加载 vue 实例,vuex 里面的数据就会被重新赋值,这样就会出现页面刷新vuex中的数据丢失的问题。 如何解决浏览器刷新数据丢失问题呢?

方法一:手动操作本地存储

全局监听,页面刷新的时候将 store 里 state 的值存到 sessionStorage 中,然后从sessionStorage 中获取,再赋值给 store ,并移除 sessionStorage 中的数据。在 app.vue 中添加以下代码:

 created() {
    window.addEventListener('beforeunload',()=>{  
       sessionStorage.setItem('list', JSON.stringify(this.$store.state))
    })
    
    try{
      sessionStorage.getItem('list') && this.$store.replaceState(Object.assign({},this.$store.state,JSON.parse(sessionStorage.getItem('list'))))
    }catch(err) {
      console.log(err);
    }
  
    sessionStorage.removeItem("list");
  }
方法二:安装 vuex-persistedstate 插件
1. npm install vuex-persistedstate -S //安装插件
2. 在 store/index.js 文件中添加以下代码:
import persistedState from 'vuex-persistedstate'
const store = new Vuex.Store({
 state:{},
 getters:{},
 ...
 plugins: [persistedState()] //添加插件
})

这时候就需要使用 sessionStorage 进行存储,修改 plugins 中的代码

plugins: [
    persistedState({ storage: window.sessionStorage })
]

怎么理解v-for的key值

key的值一般为string或则number的类型,它用于解决在进行虚拟dom更新时的更新对象快速定位,虚拟dom更新需要逐个对比虚拟dom对象绑定的数据,这个过程称为diff算法,所以key也是提升diff算法的一个标识符,因为数组可能会进行排序以及增删等动作,所以使用数组的下标来定义key是没有任何作用的

能不能自己实现v-model的功能

可以的,自定义一个指令,给节点绑定上添加input的功能,数据使用value绑定,在用户输入数据的时候,采用oninput事件来进行数据获取,并实时更新value数据,即可实现v-model的功能

Vue双向数据绑定,怎么知道数据变了

在Vue2.x中采用ES5的对象属性定义方法(Object.defineProperty)来给每一个数据添加额外属性(getter和setter属性)进行数据拦截,在vue内部实现对拦截数据的setter方法数据更新消息发布机制来获取数据更新消息,对getter方法实现消息订阅机制来获取数据更新

退出登录头像还在是什么原因怎么办

多方面的原因引起的:
	1、头像信息是否采用了应用缓存机制,如果有需要清除H5应用缓存
	2、头像缓存在webStorage中,退出没有清除,直接清除
	3、缓存在移动设备的数据,也需要清除
	4、缓存在vuex或redux中的数据,直接清除即可
	5、浏览器缓存,清除cookie或则强制清除浏览器缓存

vue数据双向绑定如何实现,如果不用vue,用原生js代码怎么实现

1、获取传递进来的数据,然后对所有数据添加getter和setter方法,并在setter方法中添加消息订阅回调方法,在getter方法中实现数据更新发布回调方法
2、对作用域内的所有dom节点进行数据以来处理,根据不同指令来实现不同订阅或发布回调方法绑定:如v-model绑定的数据对象,在oninput事件中添加消息发布回调方法绑定,在v-text中添加value数据更新消息订阅回调方法绑定
当在输入框输入值的时候,出发oninput事件,出发消息发布回调方法更新数据
当数据发生变化,触发订阅方法,执行dom数据更新方法,把最新数据更新到视图上

vue如何实现响应式,说一下原理,它有什么缺点?

它使用ES5的Object.defineProperty方法把所有的属性全部改为setter和getter属性,在每一个组件中都有一个watcher对象,当数据被赋值或变更的时候会通知页面的render方法对数据进行重新渲染,达到数据和视图的响应更新
因为js的固有特性,不能动态观察对象动态添加、删除属性和数组的长度添加,所以vue2.x不能够动态进行数据双向绑定,需要调用$set、$delete这些方法来实现动态添加双向绑定属性

include,exclude的区别

这个是webpack中常常用于指定加载,对哪些文件进行加载的排除或包含的一个属性,include配置的文件路径中的所有文件都将采用配置的loader进行文件加载处理,exclude是配置的路径都不要进行这个加载器的处理

你说你使用懒加载优化页面,用的哪个版本的vue,看过源码吗, vue2.0不能实现懒加载

这个与vue没有太大关系,采用的是ES6的动态加载机制来实现页面的懒加载,主要使用的webpack语法库为:@babel/plugin-syntax-dynamic-import,在对页面引入的时候,需要把引入方式从:import MyComponent from 'path' 修改为:const MyComponent = () => import('path')

运用多个组件库,提取公共代码进行压缩,发现js代码过大,怎么处理

对组件不要做全局引入,可以采用动态引入和页面局部引入机制,减少文件首次加载的文件大小和文件进行打包时把所有依赖进行一次性打包造成的文件过大问题

vue中爷孙通信怎么实现

可以采用:eventBus事件机制来进行数据传递;也可以采用逐层props和$emit事件传递来实现传值;vuex数据传递;使用v-model逐层数据传递等

vue怎么配置多个代理

在vue.config.js中使用devServer配置选项的proxy属性来进行多个代理配置
devServer: {
	proxy: {
		'/apis': {
			target: 'http://www.baidu.com',
			pathRewrite: {'/apis': ''}
		},
		'/info': {
			target: 'http://www.sina.com',
			pathRewrite: {'/info': ''}
		}
	}
}

为什么vue3.0双向数据绑定换成了proxy

因为Object.defineProperty方法的历史原因,如果要实现数据的劫持,需要遍历所有属性,如果是深层数据需要递归来进行属性绑定;而proxy是直接代理此数据对象,不需要遍历绑定属性
如果对象有数据属性添加或则属性更新,需要重新给定数据劫持方法绑定,而proxy直接代理对象,不需要对属性做另外的处理
因此可以在进行数据监听和劫持上节省很多数据遍历性能

你封装一个组件会有哪些考虑,用户用你的组件要怎么用?如果用户在使用你组件的同时又想自己加一些按钮进去那么此时你的组件要怎么写?

回答:

**1、**封装组件会有哪些考虑,用户用你的组件要怎么用?

1)、封装组件和封装函数是同样的道理,需要考虑到组件的通用性。

2)、那么,组件的属性,组件事件,组件的内容都是需要考虑到的。

  • 组件的属性:需要考虑到属性的类型限制,属性的默认值

  • 组件的事件:组件事件对应的函数,需要考虑到函数的参数和返回值

  • 组件的内容:要充分考虑到组件的通用性,灵活性。组件的内容给予使用者以更大的灵活空间。

**2、**如果用户在使用你组件的同时又想自己加一些按钮进去那么此时你的组件要怎么写?

1)、要么使用内容的方式(vue中是插槽)

2)、要么提供一个render函数,由用户提供需要渲染的内容(如:按钮)。同时,根据自定义组件的情况,可以考虑让用户把按钮渲染在指定的区域。即:render函数里有:渲染的区域和渲染的内容。

vue图片懒加载:①不考虑兼容性做法(你认为最好处理最效率的)②考虑兼容性做法

回答:

1、场景:

一个网页如果包含了很多的图片,那么,服务器压力就会很大。不仅影响渲染速度还会浪费带宽。

通俗的说:你不看的图片我先不加载,也许你不看呢(哈哈),我何苦要做无效的事情呢
你想看时,我再加载(哈哈)

2、原理:

1)、先将img标签的src链接设为同一张图片(默认图片:可以是loading),把图片的实际地址赋给一个自定义属性。这时候所有的图片只发送一次请求。

2)、然后,当js监听到某张图片进入可视窗口时(说明你想看了),再将实际地址赋给src属性。src属性的值发生变化时,浏览器才会发送请求加载当前图片。如果图片没有进入可视区域,就不会加载图片(说明你还没想看呢),这样就大大的提高了效率。

3、示例代码(兼容性在代码中有注释):

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <style>
    *{
        margin: 0;
        padding: 0;
    }
    ul{
        list-style: none;
    }
    li{
        width: 100%;    
        height: 200px;
        text-align: center;
        line-height: 200px
    }
    img{
        display: block;
        height: 200px;
    }
    </style>
</head>
<body>
    <div id="box">
        
    </div>
</body>
</html>
<script>

let imgDoms = document.getElementsByTagName("img");
// 当前显示图片的最大下标:
let maxIndex =  -1;    

window.onload = function(){
    // 1、从后端获取到所有的图片地址,先赋值给图片标签的自定义属性(data-src),给图片的src赋值为loading
    getImgs();
    // 2、加载可视区域的图片
    loadingImg();
}

window.onscroll = function(){
    loadingImg();
}

// 从后端获取到所有的图片地址,先赋值给图片标签的自定义属性(data-src),给图片的src赋值为loading
function getImgs(){
    //这个数组中的图片,可以是从后端获取到的,也可以写死。
    let imgs = ["img/1.jpg","img/2.jpg","img/3.jpg","img/4.jpg","img/5.jpg","img/6.jpg","img/7.jpg","img/8.jpg","img/9.jpg","img/10.jpg","img/11.jpg","img/12.jpg","img/13.jpg","img/14.jpg","img/15.jpg","img/16.jpg","img/17.jpg","img/18.jpg","img/19.jpg","img/20.jpg","img/21.jpg","img/22.jpg","img/23.jpg","img/24.jpg","img/25.jpg","img/26.jpg","img/27.jpg","img/28.jpg","img/29.jpg","img/30.jpg","img/31.jpg","img/32.jpg","img/33.jpg"];
    let htmlStr = "";
    for(let i=0;i<imgs.length;i++){
        htmlStr+=`<img src="img/loading02.gif" data-src="${imgs[i]}" />`;
    }
    document.getElementById("box").innerHTML = htmlStr;
}

function loadingImg(){    
    // 1、计算当前滚动的高度+可视区域的高度(此处考虑的兼容性)
    let scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
    let clientHeight = document.documentElement.clientHeight || document.body.clientHeight;
    let height = scrollTop+clientHeight;

    // 2、得到应该显示图片的序号(可视区域最下面的图片序号);
    let index = Math.ceil(height/200)-1;
    
    // 3、如果应该显示的图片的下标大于最大的下标,那就应该做图片的加载了。
    if(index>maxIndex){
        for(let i=maxIndex+1;i<=index;i++){
            if(imgDoms[i].getAttribute("data-src")){
                imgDoms[i].src = imgDoms[i].getAttribute("data-src");
                imgDoms[i].removeAttribute("data-src");
            }
        }
    }
    maxIndex = index;
}
</script>

vue组件中的data为什么必须是函数,为什么不可以是对象,数组这些

回答:

1、如果vue组件的data是一个对象,那么在复用vue组件时,由于对象是个引用类型。那么,每个组件的data会指向同一块内存空间,组件之间的data就会互相影响。所以,组件中的data不能是对象。

2、vue框架中把data定义成函数,函数里返回真正的数据(引用类型)。每次复用组件时,vue框架都会调用该函数。当调用该函数时,函数返回新的对象(申请新的空间)。那么不同的组件的内存空间就是独立的,不会互相影响。

说说vue生命周期,发送请求在生命周期的哪个阶段,为什么不可以是beforeMount,mounted中

回答:

1、vue的生命周期

1)、生命周期是什么?
Vue 实例有一个完整的生命周期,也就是从开始创建、初始化数据、编译模版、挂载 Dom -> 渲染、更新 -> 渲染、卸载等一系列过程,我们称这是 Vue 的生命周期。

2)、各个生命周期阶段及其钩子函数

vue的生命周期核心经历了四个阶段,在四个阶段的前后分别有两个钩子函数。

第一阶段:数据挂载阶段:把配置项data中的属性,赋给了vue对象本身,并做了数据劫持。

该阶段前后的两个钩子函数:beforeCreate和created

第二阶段:模板渲染阶段:把vue对象的数据渲染到模板上。

该阶段前后的两个钩子函数:beforeMount和mounted

第三阶段:组件更新阶段:当数据发送变化时,组件会进行重新渲染,所以,准确的说是,组件重新渲染阶段。

该阶段前后的两个钩子函数:beforeUpdate和updated

第四阶段:组件销毁阶段:组件销毁。

该阶段前后的两个钩子函数:beforeDestroy和destroyed

视情况可以补充:

当使用keep-alive包裹组件时,会有组件激活和停用之说,这两个钩子函数分别是:activited和deactivated

2、发送请求在生命周期的哪个阶段,为什么不可以是beforeMount,mounted中。

(如果组件的初始数据来自后端)发送请求建议在钩子函数created里,这个钩子函数里是最快,也能有助于一致性。

为什么?

1)、为什么不能放在beforeCreate里?

因为:

一般来说,数据从后端返回回来后,都会赋给vue中的属性(data挂载的),在beforeCreate钩子函数里,data的数据还没有挂载到vue对象本身上,所以,不能在这个钩子函数里用。而created钩子函数里,数据已经挂载到vue对象了。

2)、为什么不可以是beforeMount,mounted中

因为:

第一,在这两个钩子函数里,发送请求有点晚,会增加页面loading的时间;

第二,vue的SSR不支持beforeMount 、mounted 钩子函数,所以,放在 created 中有助于一致性

兄弟和父子组件,在不能使用vuex的情况下有多少种方案,请列举出来?

回答:

1、父子之间传值:

1)、父到子:props,子到父:$emit

2)、 r e f 、 ref、 refparent

3)、事件总线(event-bus)

4)、集中管理($root)

2、兄弟之间传值:

1)、事件总线(event-bus)

2)、集中管理($root)

3)、先 子到父(用事件),再 父到子(用props)

v-if和v-for可以同时使用吗

回答:

可以使用,但是,在循环时,通过v-if只能拿到少部分数据时,建议不要使用。

原因: v-for比v-if优先级高,如果遍历的数组元素个数比较多,但是满足v-if条件比较少的情况下。会浪费性能。而且,每次刷新页面时,都会执行这样性能不高的代码。

**解决:**可以在computed里循环数组,通过filter的方式,过滤出需要的数据。v-for直接循环计算属性的结果(不用v-if)。而computed是有缓存的,所以,在原始数据没有变化时,不会多次过滤数据,这样,就提高了效率。

vue-cli怎么配置跨域

回答:

使用反向代理,vue-cli3+的项目里,新建(编辑)vue.config.js文件,(增加)配置代码如下:

module.exports = {
    
  devServer:{
    //设置代理
    proxy: { //代理是从指定的target后面开始匹配的,不是任意位置;配置pathRewrite可以做替换
      '/api': { //这个是自定义的,当axios访问以/api开头的请求时,会代理到 target + /api
             target: 'http://localhost:3001', //最终要访问的服务器地址
       		 changeOrigin: true, //创建虚拟服务器 
             pathRewrite: {
                '^/api': ''    //重写接口,去掉/api, 在代理过程中是否替换掉/api/路径 
     		 }
      	}
      }
   }    
}

v-bind是用来干什么的

回答:

v-bind指令是把标签的属性处理成动态的。分别可以把属性名属性值处理成vue里的属性,常间的是属性值处理成动态的。

格式如下:

1、属性值动态绑定:v-bind:html属性="数据" 简写 :html属性=“数据”`

示例:

<img v-bind:src="imgstr"></div> 

new Vue({
    data:{
        imgstr:'./imgs/1.jpg'
    }
})

2、 属性名动态绑定: v-bind:[属性名]="数据"

此时,属性值也是动态的

示例:

<div v-bind:[attr]="idname"  >我是div</div>

  new Vue({
        el: "#app",
        data:{
            attr:"class",
            idname:"div01"  
        }
    })

说说对插槽的理解

回答:

1、插槽的作用:

插槽是用来处理组件的内容的。插槽决定了组件的内容放在组件模板的何处。插槽使用的是vue官方提供的组件<slot>来完成的。

2、vue中的插槽分为:

1)、单个插槽

在组件中只有一个插槽时,插槽不用起名字。默认的名字是:default

示例:

//1、定义组件
let book = {
        template: `
            <div>     
               <p>我是上p</p>
  			   <!--这是插槽,该组件的内容将会渲染在此处 -->
               <slot></slot> 
               <p>我是下p</p>
            </div>
        `,
    }

//2、父组件的模板
<div id="box">        
    <book>
       <!--放在组件book标签的内容,最终会渲染在book标签的<slot>处。-->
       <img src="imgs/2.jpg" />
    </book>
</div>

2)、具名插槽

但组件中的插槽多于一个时,就需要给组件起名字,用名字来区分不同的插槽。用官方组件slot的name属性给插槽起名字

格式:

<slot name="插槽的名字"></slot>

示例:

//1、组件:
    let book = {
        template: `
            <div>     
               <p>我是上p</p>
				<!--具名插槽,该插槽起名为s1-->
               <slot name="s1"></slot> 
               <p>我是中p</p> 
				<!--具名插槽,该插槽起名为s2-->
               <slot name="s2"></slot>
               <p>我是下p</p>
            </div>
        `,
    }

//2、父组件模板里:

<div id="box">
    <book>
    	<!--以下内容插在name为s1的插槽处-->
        <template v-slot:s1>
            <img src="imgs/2.jpg" />
        </template>
    	<!--以下内容插在name为s2的插槽处-->
        <template v-slot:s2>
            <div>我是div</div>
        </template>
	</book>
</div>

$nextTick理解 和定时器有什么区别 都是延时执行

回答:

1、$nextTick理解:

vue更新DOM时,使用的是异步更新队列。目的是提高性能,避免无效的重复的DOM更新。即:vue中更新数据后,并不会立即更新DOM,而是把数据引起的DOM更新放入到异步更新队列里。等待下次事件循环(tick),并在两个tick之间进行UI渲染。

按照这个思路,程序员就不能在更改数据后,立即获取更新后的DOM,也不知道什么时候DOM能够更新。基于此,vue提供了 n e x t T i c k 函 数 。 程 序 员 只 需 要 把 ∗ ∗ ” ∗ ∗ 操 作 更 新 后 D O M 的 代 码 ∗ ∗ “ ∗ ∗ 放 入 到 nextTick函数。程序员只需要把 **”**操作更新后DOM的代码**“** 放入到 nextTickDOMnextTick的回调函数里。由$nextTick内部,在更新完DOM后,调用回调函数。

示例:


this.msg = "hello"

this.$nextTick(()=>{
    
     操作“this.msg影响的DOM”的代码。
     
})

2、$nextTick理解和定时器有什么区别

**相同:**都是延迟加载,都使用事件队列

不同:

1)、定时器是下一个队列的队首。

2)、$nextTick()是放在当前队列的最后一个。$nextTick()的回调函数执行要先于定时器。

event-bus是怎么用?

event-bus是事件总线,是借助一个全局的vue对象,来完成事件的绑定和事件的触发。

**当:**我们需要把A组件的数据传给B组件时,在A、B两个组件里都引入全局的vue对象。然后,在B组件里绑定事件,在A组件里触发事件,就可以把A组件的数据传给B组件了。

示例:

//1、全局的vue对象: bus.js
export default new Vue();//vue 对象具有 $on  和 $emit  方法

//2、B组件的代码
import bus from "./bus.js"

export default {
	………………
    data(){
        return {
            bookmsg:""
        }
    },
    created(){
        // 绑定事件(用全局变量bus绑定一个事件)
        bus.$on("eclick",str=>{
           this.bookmsg = str;
        })
    }    
}

//3、A组件的代码

import bus from "./bus.js"

export default {
	………………
    data(){
        return {
             msg:"要传给B组件的数据"
        }
    },
    methods:{
        chuan(){
            // 触发事件(用全部变量bus触发事件)
            bus.$emit("eclick",this.msg);
        }
    }     
}

mounted与created的区别

回答:

mounted和created都是vue对象生命周期的钩子函数,执行时机不同。

1、created 是在 data配置项的数据挂载到vue对象本身,会调用的钩子函数。

此时,

1)、用 this. 的方式可以拿到data里的数据。

2)、但是数据还没有渲染到模板上。所以,访问dom时,内容还是原始的模板内容。

2、mounted是在组件的模板初次渲染完毕后会调用的钩子函数。

此时,

data里的数据已经渲染到模板上了。所以,访问dom时,已经和页面上看到的效果一样了。

v-model 原理 是什么

回答:

1、v-model指令的作用

vue中的v-model指令是完成双向绑定的,用在表单元素上。双向绑定就是 M会影响V。V也会影响M。即:能将页面上输入的值同步更新到相关绑定的data属性,也会在更新data绑定属性时候,更新页面上输入控件的值。

2、v-model的原理

v-model指令是一个语法糖,是属性绑定和事件的语法糖。vue会根据不同的表单元素使用不同的属性和事件。

如下:

  • text 和 textarea 元素使用 value property 和 input 事件;
  • checkbox 和 radio 使用 checked property 和 change 事件;
  • select 字段将 value 作为 prop 并将 change 作为事件。

以文本框为例剖析原理,以下是代码:

 <!-- V -->
  <div id="app">
      <!--文本框使用value属性和input事件-->
      <input type="text" v-bind:value="msg" @input="changeMsg"  >
  </div>

 //M:

  let vm = new Vue({
        el: "#app",
        data: {
         	 msg:"hi"
        },
        methods: {
            changeMsg(e){
                this.msg = e.target.value; 
            }      
        }
  })

而,使用v-model来完成以上功能的代码如下:

  <!-- V-->
  <div id="app">
	  <!-- 此处不需要使用value属性和input事件,直接使用v-mdoel指令即可 -->
      <input type="text" v-model="msg"  >
  </div>

  // M:model

  let vm = new Vue({
      el: "#app",
      data: {
          msg:"hi"
      }      
  })

vue 的组件中的 data 配置为什么必须是函数(每个组件都是 vue 的实例)

vue 为了保证每个实例上的 data 数据的独立性,规定了必须使用函数,而不是对象。 因为使用对象的话,每个实例(组件)上使用的 data 数据是相互影响的,这当然就不是我们想要的了。 对象是对于内存地址的引用,直接定义个对象的话组件之间都会使用这个对象,这样会造成组件之间数据相互影响。而函数具有内部作用域,可以解决这个问题。

vue 中 computed 和 watch 的区别

  1. watch 和 computed 都是以 Vue 的依赖追踪机制为基础的,当某一个依赖型数据(依赖型数据:简单理解即放在 data 等对象下的实例数据)发生变化的时候,所有依赖这个数据的相关数据会自动发生变化,即自动调用相关的函数,来实现数据的变动。
    当依赖的值变化时,在 watch 中,是可以做一些复杂的操作的,而 computed 中的依赖,仅仅是一个值依赖于另一个值,是值上的依赖。
  2. 应用场景:
    computed:用于处理复杂的逻辑运算;一个数据受一个或多个数据影响;用来处理 watch 和 methods 无法处理的,或处理起来不方便的情况。例如处理模板中的复杂表达式、购物车里面的商品数量和总金额之间的变化关系等。
       watch:用来处理当一个属性发生变化时,需要执行某些具体的业务逻辑操作,或要在数据变化时执行异步或开销较大的操作;一个数据改变影响多个数据。例如用来监控路由、inpurt 输入框值的特殊处理等。
  3. 区别:
  • computed
    • 初始化显示或者相关的 data、props 等属性数据发生变化的时候调用;
    • 计算属性不在 data 中,它是基于 data 或 props 中的数据通过计算得到的一个新值,这个新值根据已知值的变化而变化;
    • 在 computed 属性对象中定义计算属性的方法,和取 data 对象里的数据属性一样,以属性访问的形式调用;
    • 如果 computed 属性值是函数,那么默认会走 get 方法,必须要有一个返回值,函数的返回值就是属性的属性值;
    • computed 属性值默认会缓存计算结果,在重复的调用中,只要依赖数据不变,直接取缓存中的计算结果,只有依赖型数据发生改变,computed 才会重新计算;
    • 在 computed 中的,属性都有一个 get 和一个 set 方法,当数据变化时,调用 set 方法。
  • watch
    • 主要用来监听某些特定数据的变化,从而进行某些具体的业务逻辑操作,可以看作是 computed 和 methods 的结合体;
    • 可以监听的数据来源:data,props,computed 内的数据;
    • watch 支持异步;
    • 不支持缓存,监听的数据改变,直接会触发相应的操作;
    • 监听函数有两个参数,第一个参数是最新的值,第二个参数是输入之前的值,顺序一定是新值,旧值。

vue 的生命周期?网络请求为什么要挂载在 mounted 中?

  1. vue2 的生命周期
  • 初始化阶段:
    • beforeCreate
    • created
  • 挂载阶段
    • beforeMount
    • mounted
  • 更新阶段
    • beforeUpdate
    • updated
  • 卸载阶段
    • beforeDestroy
    • destroyed
  • 缓存组件相关
    • activated
    • deactivated
  • 处理错误相关
    • errorCaptured
  1. vue3 的生命周期
    在 vue2 的基础上新增了:
  • renderTracked: 跟踪虚拟 DOM 重新渲染时调用。钩子接收 debugger event 作为参数。此事件告诉你哪个操作跟踪了组件以及该操作的目标对象和键。
  • renderTriggered:当虚拟 DOM 重新渲染被触发时调用。和 renderTracked 类似,接收 debugger event 作为参数。此事件告诉你是什么操作触发了重新渲染,以及该操作的目标对象和键。
    一共 13 个
  1. vue3 的组合 api 的生命周期
    移除了 beforeCreate 和 created,因为创建时的事件可以在 setup 里面直接调用。
    其他的 11 个生命周期前面全部加上 on
    比如:mounted -> onMounted, beforeDestroy -> onDeforeDestroy
  2. 网络请求为什么要挂载在 mounted 中?
    在 Created 生命周期里 Data 才生成,而请求返回的数据需要挂载在 data 上,所以 Created 里是可以初始化请求的,但是 Created 的这时候 DOM 还没有初始化;Mounted 生命周期里 DOM 才初始化渲染完成。
    然而,请求是异步的,所以不会堵塞页面渲染的主线程。
    所以请求放在 created 和 mounted 里面都是可行的。
    如果我们的请求不需要获取/借助/依赖/改变 DOM,这时请求可以放在 Created。反之则可以放在 Mounted 里。这样做会更加的安全,也能保证页面不会闪烁。

vue 的指令,在项目中封装了那些常用指令?

在 vue 中我们可以使用 Vue.directive()方法注册全局指令。也可以只用 directives 选项注册局部指令。

  • 输入框防抖指令 v-debounce
const debounce = {
  inserted: function (el, binding) {
    let timer;
    el.addEventListener("keyup", () => {
      if (timer) {
        clearTimeout(timer);
      }
      timer = setTimeout(() => {
        binding.value();
      }, 1000);
    });
  },
};

export default debounce;
  • 复制粘贴指令 v-copy
const copy = {
  bind(el, { value }) {
    el.$value = value;
    el.handler = () => {
      if (!el.$value) {
        // 值为空的时候,给出提示。可根据项目UI仔细设计
        console.log("无复制内容");
        return;
      }
      // 动态创建 textarea 标签
      const textarea = document.createElement("textarea");
      // 将该 textarea 设为 readonly 防止 iOS 下自动唤起键盘,同时将 textarea 移出可视区域
      textarea.readOnly = "readonly";
      textarea.style.position = "absolute";
      textarea.style.left = "-9999px";
      // 将要 copy 的值赋给 textarea 标签的 value 属性
      textarea.value = el.$value;
      // 将 textarea 插入到 body 中
      document.body.appendChild(textarea);
      // 选中值并复制
      textarea.select();
      const result = document.execCommand("Copy");
      if (result) {
        console.log("复制成功"); // 可根据项目UI仔细设计
      }
      document.body.removeChild(textarea);
    };
    // 绑定点击事件,就是所谓的一键 copy 啦
    el.addEventListener("click", el.handler);
  },
  // 当传进来的值更新的时候触发
  componentUpdated(el, { value }) {
    el.$value = value;
  },
  // 指令与元素解绑的时候,移除事件绑定
  unbind(el) {
    el.removeEventListener("click", el.handler);
  },
};

export default copy;
  • 长按指令 v-longpress
const longpress = {
  bind: function (el, binding, vNode) {
    if (typeof binding.value !== "function") {
      throw "callback must be a function";
    }
    // 定义变量
    let pressTimer = null;
    // 创建计时器( 2秒后执行函数 )
    let start = (e) => {
      if (e.type === "click" && e.button !== 0) {
        return;
      }
      if (pressTimer === null) {
        pressTimer = setTimeout(() => {
          handler();
        }, 2000);
      }
    };
    // 取消计时器
    let cancel = (e) => {
      if (pressTimer !== null) {
        clearTimeout(pressTimer);
        pressTimer = null;
      }
    };
    // 运行函数
    const handler = (e) => {
      binding.value(e);
    };
    // 添加事件监听器
    el.addEventListener("mousedown", start);
    el.addEventListener("touchstart", start);
    // 取消计时器
    el.addEventListener("click", cancel);
    el.addEventListener("mouseout", cancel);
    el.addEventListener("touchend", cancel);
    el.addEventListener("touchcancel", cancel);
  },
  // 当传进来的值更新的时候触发
  componentUpdated(el, { value }) {
    el.$value = value;
  },
  // 指令与元素解绑的时候,移除事件绑定
  unbind(el) {
    el.removeEventListener("click", el.handler);
  },
};

export default longpress;
  • 禁止表情及特殊字符 v-emoji
let findEle = (parent, type) => {
  return parent.tagName.toLowerCase() === type
    ? parent
    : parent.querySelector(type);
};

const trigger = (el, type) => {
  const e = document.createEvent("HTMLEvents");
  e.initEvent(type, true, true);
  el.dispatchEvent(e);
};

const emoji = {
  bind: function (el, binding, vnode) {
    // 正则规则可根据需求自定义
    var regRule = /[^u4E00-u9FA5|d|a-zA-Z|rns,.?!,。?!…—&$=()-+/*{}[]]|s/g;
    let $inp = findEle(el, "input");
    el.$inp = $inp;
    $inp.handle = function () {
      let val = $inp.value;
      $inp.value = val.replace(regRule, "");

      trigger($inp, "input");
    };
    $inp.addEventListener("keyup", $inp.handle);
  },
  unbind: function (el) {
    el.$inp.removeEventListener("keyup", el.$inp.handle);
  },
};

export default emoji;
  • 图片懒加载 v-LazyLoad
const LazyLoad = {
  // install方法
  install(Vue, options) {
    const defaultSrc = options.default;
    Vue.directive("lazy", {
      bind(el, binding) {
        LazyLoad.init(el, binding.value, defaultSrc);
      },
      inserted(el) {
        if (IntersectionObserver) {
          LazyLoad.observe(el);
        } else {
          LazyLoad.listenerScroll(el);
        }
      },
    });
  },
  // 初始化
  init(el, val, def) {
    el.setAttribute("data-src", val);
    el.setAttribute("src", def);
  },
  // 利用IntersectionObserver监听el
  observe(el) {
    var io = new IntersectionObserver((entries) => {
      const realSrc = el.dataset.src;
      if (entries[0].isIntersecting) {
        if (realSrc) {
          el.src = realSrc;
          el.removeAttribute("data-src");
        }
      }
    });
    io.observe(el);
  },
  // 监听scroll事件
  listenerScroll(el) {
    const handler = LazyLoad.throttle(LazyLoad.load, 300);
    LazyLoad.load(el);
    window.addEventListener("scroll", () => {
      handler(el);
    });
  },
  // 加载真实图片
  load(el) {
    const windowHeight = document.documentElement.clientHeight;
    const elTop = el.getBoundingClientRect().top;
    const elBtm = el.getBoundingClientRect().bottom;
    const realSrc = el.dataset.src;
    if (elTop - windowHeight < 0 && elBtm > 0) {
      if (realSrc) {
        el.src = realSrc;
        el.removeAttribute("data-src");
      }
    }
  },
  // 节流
  throttle(fn, delay) {
    let timer;
    let prevTime;
    return function (...args) {
      const currTime = Date.now();
      const context = this;
      if (!prevTime) prevTime = currTime;
      clearTimeout(timer);

      if (currTime - prevTime > delay) {
        prevTime = currTime;
        fn.apply(context, args);
        clearTimeout(timer);
        return;
      }

      timer = setTimeout(function () {
        prevTime = Date.now();
        timer = null;
        fn.apply(context, args);
      }, delay);
    };
  },
};

export default LazyLoad;
  • 权限校验指令 v-premission
function checkArray(key) {
  let arr = ["1", "2", "3", "4"];
  let index = arr.indexOf(key);
  if (index > -1) {
    return true; // 有权限
  } else {
    return false; // 无权限
  }
}

const permission = {
  inserted: function (el, binding) {
    let permission = binding.value; // 获取到 v-permission的值
    if (permission) {
      let hasPermission = checkArray(permission);
      if (!hasPermission) {
        // 没有权限 移除Dom元素
        el.parentNode && el.parentNode.removeChild(el);
      }
    }
  },
};

export default permission;
  • 实现页面水印 v-waterMarker
function addWaterMarker(str, parentNode, font, textColor) {
  // 水印文字,父元素,字体,文字颜色
  var can = document.createElement("canvas");
  parentNode.appendChild(can);
  can.width = 200;
  can.height = 150;
  can.style.display = "none";
  var cans = can.getContext("2d");
  cans.rotate((-20 * Math.PI) / 180);
  cans.font = font || "16px Microsoft JhengHei";
  cans.fillStyle = textColor || "rgba(180, 180, 180, 0.3)";
  cans.textAlign = "left";
  cans.textBaseline = "Middle";
  cans.fillText(str, can.width / 10, can.height / 2);
  parentNode.style.backgroundImage = "url(" + can.toDataURL("image/png") + ")";
}

const waterMarker = {
  bind: function (el, binding) {
    addWaterMarker(
      binding.value.text,
      el,
      binding.value.font,
      binding.value.textColor
    );
  },
};

export default waterMarker;
  • 拖拽指令 v-draggable
const draggable = {
  inserted: function (el) {
    el.style.cursor = "move";
    el.onmousedown = function (e) {
      let disx = e.pageX - el.offsetLeft;
      let disy = e.pageY - el.offsetTop;
      document.onmousemove = function (e) {
        let x = e.pageX - disx;
        let y = e.pageY - disy;
        let maxX =
          document.body.clientWidth -
          parseInt(window.getComputedStyle(el).width);
        let maxY =
          document.body.clientHeight -
          parseInt(window.getComputedStyle(el).height);
        if (x < 0) {
          x = 0;
        } else if (x > maxX) {
          x = maxX;
        }

        if (y < 0) {
          y = 0;
        } else if (y > maxY) {
          y = maxY;
        }

        el.style.left = x + "px";
        el.style.top = y + "px";
      };
      document.onmouseup = function () {
        document.onmousemove = document.onmouseup = null;
      };
    };
  },
};
export default draggable;

vue 的移动端适配怎么做的,rem 怎么用的

vue 的移动端适配我们可以参考 vant-ui 组件库给我们提供的方案。
使用 amfe-flexible(用于自动定义跟字体大小)插件和 postcss-pxtorem(用于将 px 自动转成 rem)插件
在 main.ts 里面 import “amfe-flexible”
在根目录新建 .postcssrc.js 文件

module.exports = {
  plugins: {
    "postcss-pxtorem": {
      rootValue: 37.5,
      propList: ["*"],
    },
  },
};

rem 是相对于跟字体的倍数,如果我们整个项目都是用 rem 作为单位,那么当我们做移动端的响应式的时候只需要去改变跟字体的大小就能做到适配。

后台管理系统用户验证权限

  1. 登录
    用户填写完账号和密码后向服务端验证是否正确,登录成功后,服务端会返回一个 token(该 token 的是一个能唯一标示用户身份的一个 key),之后我们将 token 存储在本地 localstorage 之中,这样下次打开页面或者刷新页面的时候能记住用户的登录状态,不用再去登录页面重新登录了。
    为了保证安全性,后台所有 token 有效期(Expires/Max-Age)都是 Session,就是当浏览器关闭了就丢失了。重新打开浏览器都需要重新登录验证,后端也会在每周固定一个时间点重新刷新 token,让后台用户全部重新登录一次,确保后台用户不会因为电脑遗失或者其它原因被人随意使用账号。
  2. 拦截路由进行判断
    页面会先从 localstorage 中查看是否存有 token,没有,就走一遍上一部分的流程重新登录,如果有 token,就会把这个 token 返给后端去拉取 user_info,保证用户信息是最新的。 当然如果是做了单点登录得的的话,用户信息存储在本地也是可以的。当你一台电脑登录时,另一台会被提下线,所以总会重新登录获取最新的内容。
  3. 权限控制
    前端会有一份路由表,它表示了每一个路由可访问的权限。当用户登录之后,通过 token 获取用户的 role ,动态根据用户的 role 算出其对应有权限的路由,再通过 router.addRoutes 动态挂载路由。但这些控制都只是页面级的,说白了前端再怎么做权限控制都不是绝对安全的,后端的权限验证是逃不掉的。
    前端控制页面级的权限,不同权限的用户显示不同的侧边栏和限制其所能进入的页面(也做了少许按钮级别的权限控制),后端则会验证每一个涉及请求的操作,验证其是否有该操作的权限,每一个后台的请求不管是 get 还是 post 都会让前端在请求 header 里面携带用户的 token,后端会根据该 token 来验证用户是否有权限执行该操作。若没有权限则抛出一个对应的状态码,前端检测到该状态码,做出相对应的操作。
  4. 利用 vuex 管理路由表,根据 vuex 中可访问的路由渲染侧边栏组件。
  • 创建 vue 实例的时候将 vue-router 挂载,但这个时候 vue-router 挂载一些登录或者不用权限的公用的页面。
  • 当用户登录后,获取用 role,将 role 和路由表每个页面的需要的权限作比较,生成最终用户可访问的路由表。
  • 调用 router.addRoutes(store.getters.addRouters)添加用户可访问的路由。
  • 使用 vuex 管理路由表,根据 vuex 中可访问的路由渲染侧边栏组件。

vuex 做数据请求刷新页面,数据可能会发生丢失这个问题怎么解决

因为 store 里的数据是保存在运行内存中的,当页面刷新时,页面会重新加载 vue 实例,store 里面的数据就会被重新赋值初始化。
所以我们可以在修改 store 的数据同时将数据再存一份到本地存储(localStorage 或者 sessionStorage),本地存储的内容是存在浏览器里面的,不会因为刷新而丢失。
我们也可以用过比如 vuex-persistedstate 这样的第三方包来帮助我们做 vuex 的持久化数据。

vue2 和 vue3 两者的具体的区别有哪些,请一一例举出来

  1. 源码使用 ts 重写
    现如今 typescript 异常火爆,它的崛起是有原因的,因为对于规模很大的项目,没有类型声明,后期维护和代码的阅读都是头疼的事情,所以广大码农迫切的需要 vue 能完美支持 ts。
    vue2 用的是 Facebook 的 Flow 做类型检查,但是因为某些情况下推断有问题,所以 vue3 使用 typescript 进行了源码的重写。一个是为了更好的类型检查,另一个是拥抱 ts。
  2. 使用 proxy 代替 defineProperty
    我们知道 vue2.x 双向绑定的核心是 Object.defineProperty(),所以导致 vue 对数组对象的深层监听无法实现。
    所以 vue3 使用 proxy 对双向绑定进行了重写。Proxy 可以对整体进行监听,不需要关心里面有什么属性,而且 Proxy 的配置项有 13 种,可以做更细致的事情,这是之前的 defineProperty 无法达到的。
  3. Diff 算法的提升
    vue3 在 vue2 的 diff 算法的基础上增加了静态标记,元素提升和事件缓存等优化。使得速度更快。
  4. 打包体积变化
    vue2 官方说的运行时打包师 23k,但这只是没安装依赖的时候,随着依赖包和框架特性的增多,有时候不必要的,未使用的代码文件都被打包了进去,所以后期项目大了,打包文件会特别多还很大。
    在 Vue 3 中,我们通过将大多数全局 API 和内部帮助程序移动到 Javascript 的 module.exports 属性上实现这一点。这允许现代模式下的 module bundler 能够静态地分析模块依赖关系,并删除与未使用的 module.exports 属性相关的代码。模板编译器还生成了对树抖动友好的代码,只有在模板中实际使用某个特性时,该代码才导入该特性的帮助程序。
    尽管增加了许多新特性,但 Vue 3 被压缩后的基线大小约为 10 KB,不到 Vue 2 的一半。
  5. 其他 Api 和功能的改动
  • Global API
  • 模板指令
  • 组件
  • 渲染函数
  • vue-cli 从 v4.5.0 开始提供 Vue 3 预设
  • Vue Router 4.0 提供了 Vue 3 支持,并有许多突破性的变化
  • Vuex 4.0 提供了 Vue 3 支持,其 API 与 2.x 基本相同
  1. 组件基本结构

在创建组件的时候我们不必在写唯一的根元素。移除了 vue2 中的 filters。

  1. 生命周期的区别

新增了 renderTracked 和 renderTriggered 两个生命周期。

  1. 增加了组合 api

我们可以使用 setup 函数来使用类似 react 的自定义 hooks 的功能,主要解决逻辑关注点分离的问题。

vue 操作虚拟 DOM 有什么优异的地方?他不是还多做了一层虚拟 DOM,为什么比原生操作 DOM 还快

我们有必要先了解下模板转换成视图的过程整个过程:

  • Vue.js 通过编译将 template 模板转换成渲染函数(render ) ,执行渲染函数就可以得到一个虚拟节点树。
  • 在对 Model 进行操作的时候,会触发对应 Dep 中的 Watcher 对象。Watcher 对象会调用对应的 update 来修改视图。这个过程主要是将新旧虚拟节点进行差异对比,然后根据对比结果进行 DOM 操作来更新视图。

简单点讲,在 Vue 的底层实现上,Vue 将模板编译成虚拟 DOM 渲染函数。结合 Vue 自带的响应系统,在状态改变时,Vue 能够智能地计算出重新渲染组件的最小代价并应到 DOM 操作上。
avatar

那么 vue 操作虚拟 DOM 有什么优异的地方呢?

  • 具备跨平台的优势
    由于 Virtual DOM 是以 JavaScript 对象为基础而不依赖真实平台环境,所以使它具有了跨平台的能力,比如说浏览器平台、Weex、Node 等。
  • 操作 DOM 慢,js 运行效率高。我们可以将 DOM 对比操作放在 JS 层,提高效率。
    因为 DOM 操作的执行速度远不如 Javascript 的运算速度快,因此,把大量的 DOM 操作搬运到 Javascript 中,运用 patching 算法来计算出真正需要更新的节点,最大限度地减少 DOM 操作,从而显著提高性能。
    Virtual DOM 本质上就是在 JS 和 DOM 之间做了一个缓存。可以类比 CPU 和硬盘,既然硬盘这么慢,我们就在它们之间加个缓存:既然 DOM 这么慢,我们就在它们 JS 和 DOM 之间加个缓存。CPU(JS)只操作内存(Virtual DOM),最后的时候再把变更写入硬盘(DOM)
  • 提升渲染性能
    Virtual DOM 的优势不在于单次的操作,而是在大量、频繁的数据更新下,能够对视图进行合理、高效的更新。
    为了实现高效的 DOM 操作,一套高效的虚拟 DOM diff 算法显得很有必要。我们通过 patch 的核心—-diff 算法,找出本次 DOM 需要更新的节点来更新,其他的不更新。比如修改某个 model 100 次,从 1 加到 100,那么有了 Virtual DOM 的缓存之后,只会把最后一次修改 patch 到 view 上。那 diff 算法的实现过程是怎样的?

那么为什么比原生操作 DOM 还快呢?
首先我们每次操作 dom 的时候,都会去执行浏览器的那 5 个步骤,尤其是当大量循环的时候,每次循环完都不知道后面还要不要修改,所以每次都要去重复这个过程,引发不必要的渲染。
但是在实际开发过程中,我们会发现虚拟 dom 并没有比真实 dom 更快。这个问题尤雨溪在知乎上面有过回答:
这是一个性能 vs. 可维护性的取舍。框架的意义在于为你掩盖底层的 DOM 操作,让你用更声明式的方式来描述你的目的,从而让你的代码更容易维护。没有任何框架可以比纯手动的优化 DOM 操作更快,因为框架的 DOM 操作层需要应对任何上层 API 可能产生的操作,它的实现必须是普适的。针对任何一个 benchmark,我都可以写出比任何框架更快的手动优化,但是那有什么意义呢?在构建一个实际应用的时候,你难道为每一个地方都去做手动优化吗?出于可维护性的考虑,这显然不可能。框架给你的保证是,你在不需要手动优化的情况下,我依然可以给你提供过得去的性能。

token 过期你是如何来进行处理,有没有弄过 token 续期

在开发中,我们经常会遇到使用 token,token 的作用是要验证用户是否处于登录状态,所以要请求一些只有登录状态才能查看的资源的时候,我们需要携带 token。

一般的后端接口设置的 token 是有时效的,超时后就会失效,失效之后的处理策略一般会做两种处理:

  • 一种是直接跳转到登录页面,重新登录。
  • 另外一种如果返回 token 失效的信息,自动去刷新 token,然后继续完成未完成的请求操作。

vue底层实现原理

  • 使用 Object.defineProperty 劫持 data上的数据。
  • Vue2.0通过设定对象属性的 setter/getter 方法来监听数据的变化,通过getter进行依赖收集,而每个setter方法就是一个观察者,在数据变更的时候通知订阅者更新视图。

Vue的生命周期,created与mounted的区别

1、created

表示组件实例已经完全创建,data数据已经被 Object.defineProperty 劫持完成,属性也绑定成功,但真实dom还没有生成,$el还不可用。

2、mounted

el选项所对应的视图节点已经被新创建的 vm.$el 替换,并挂载到实例上去了。此时响应式数据都已经完成了渲染。

用vue写了商城,从列表页点进详情页,从详情页退出来的时候,怎么保持进入详情页之前的页面卷动值。

使用 对列表页面进行包裹,被包裹的列表页面就有了activated、deactivated这两个生命周期。
在离开列表页面时,在deactivated中记录页面的滚动条位置。
再次进入列表页面时,在activated中使用 this.$el.scrollTop 把页面滚动到离开时所记录的位置。

说说你对vue的理解

  • vue是数据驱动的MVVM框架,相比传统的DOM库,vue有一层虚拟DOM。每当数据发生更新时,会触发虚拟DOM进行diff运算,找出最小的变化节点,大大地节省了DOM操作性能。
  • vue是组件化的,在单页面应用程序中,每一个组件相当于是一块积木,搭建起庞大的应用系统。组件,可以理解成更大粒度的“HTML元素”,有利于快速开发、组件的高效复用。
  • vue有一整套指令,大大地降低了开发者的开发难度,提升了开发效率。
  • 虽然vue有诸多优点,但仍然有一些缺陷,比如复杂的业务页面通常过长,data、methods、computed、watch对应的数据和逻辑交织在一起,难以维护。

说说对虚拟DOM的理解

  • 在vue中,虚拟DOM本质上就是一个固定格式的JSON数据,它用于描述真实的DOM结构,并使用各种不同flag标记出动态的DOM节点。
  • 虚拟DOM数据保存在应用程序对应的内存结构中,拥有更快的数据交换速度。
  • 每当有数据发生变化时,就会生成新的虚拟DOM,进一步发生diff运算,找出最小脏节点,减少不必要的DOM开销,这也是vue拥有更好的性能的根本原因。

说说provide的用法

  • 在父级组件中,使用provide选项向vue组件树中“提供数据”,其语法是:provide:{a: 1}
  • 在后代子级组件中,使用 inject选项从vue组件中“取出数据”,其语法是:inject: [‘a’]

说一下element ui遇到过的坑

  1. 表单设置触发事件为blur,但是ctrl+A全选以后再删除时又触发了change事件,并提示一个原始报错

    • 解决方案:trigger设置成trigger: ['blur', 'change']
  2. 使用el-dialog 遮罩层把显示内容遮住了

    • 原因: Dialog 的外层布局的 position 值为 fixed, absolute, relative 三者之一时,就会出现被蒙板遮住的情况。
    • 解决方法:v-bind:modal-append-to-body="false"
  3. 使用el-select 不能继承父元素的宽度

    • 原因:el-select 本身是 inline-block
    • 解决办法:手动设置el-select的宽度
        <el-select style="width:100%"></el-select>
    

怎么修改element ui动态组件的样式

要修改elementUI组件的样式,可以采用以下两种方式

  1. 全局样式

    通过选择权重覆盖elementUI组件的样式,如修改复选框为圆角:

        <style>
            .edit-item .el-checkbox__inner {
              border-radius: 50%;
            }
        </style>
    

    但这种方式为全局样式,会影响页面中所有复选框,如果不希望影响其它页面的样式,可以采用第二中方式

  2. 局部样式

        <style scoped>
            .edit-item .el-checkbox__inner {
              border-radius: 50%;
            }
        </style>
    

    但如果仅仅是设置了scoped属性,样式无法生效,原因是以上样式会被编译成属性选择器,而elementUI组件内部的结构却无法添加该html属性,以上样式被编译成如下代码:

        .edit-item[data-v-6558bc58] .el-checkbox__inner[data-v-6558bc58] {
              border-radius: 50%;
            }
    

    解决方案也很简单,只需在选择器中要添加>>>即可

        <style scoped>
            .edit-item >>> .el-checkbox__inner {
              border-radius: 50%;
            }
        </style>
    

    如果是sass或less编写的样式,还可以使用/deep/

        <style scoped lang="scss">
            .edit-item /deep/ .el-checkbox__inner {
              border-radius: 50%;
            }
        </style>
    

    以上写法样式都会编译成以下样式:

        .edit-item[data-v-6558bc58] .el-checkbox__inner{} 
    

    所以elementUI中的样式就能成功覆盖

vue和react中的key值主要用来干什么

key是虚拟DOM对象的标识,在更新显示时key起着极其重要的作用,vue和react都是采用diff算法来对比新旧虚拟节点,而key的作用是为了在执行 diff算法 的时候,更快更准确地找到对应的虚拟节点,从而提高diff速度。

route和router区别

routerouter 是vue-router中经常会操作的两个对象, route表示当前的路由信息对象,包含了当前 URL 解析得到的信息,包含当前的路径、参数、query对象等,一般用于获取跳转时传入的参数。 router对象是全局路由的实例,是router构造方法的实例,一般用户路由跳转,如router.push()router.replace() 等方法

vue和react相对于传统的有什么好处,性能优点

  1. 组件化开发,开发效率更高

    React与Vue都鼓励使用组件化开发。这本质上是建议你将你的应用分拆成一个个功能明确的模块,每个模块之间可以通过特定的方式进行关联。这样可以更好的管理功能模块与复用,在团队项目中也能更好的分工协作

  2. VirtualDOM,性能更高

    对真实DOM的操作很慢,所以Vue和React都实现了虚拟DOM,用户对数据的修改首先反映到虚拟DOM上,而不是直接操作真实DOM,然后在虚拟DOM环节进行优化,比如只修改差异项,对虚拟 DOM 进行频繁修改时进行合并,然后一次性修改真实 DOM 中需要改的部分,最后在真实 DOM 中进行排版与重绘,减少过多DOM节点排版与重绘损耗等

  3. 数据和结构的分离
  4. 双向数据绑定

    Vue可以通过v-model指令实现,react可通过单向绑定+事件来实现

  5. 强大的周边生态

    Vue和React都有着强大的生态,比如路由、状态管理、脚手架等,用户可以根据需求自行选择,不需要重复造轮子

虚拟DOM实现原理

我们先来看看浏览器渲染一个页面的过和,大概需要以下5个步骤

  1. 解析HTML元素,构建DOM树
  2. 解析CSS,生成页面CSS规则树(Style Rules)
  3. 将DOM树和CSS规则树进行关联,生成render树
  4. 布局(layout/reflow):浏览器设定Render树中的每个节点在屏幕上的位置与尺寸
  5. 绘制Render树:绘制页面像素信息到屏幕上

众所周知,一个页面在浏览器中最大的开销就是DOM节点操作,页面的性能问题大多数是DOM操作引起的,当我们用原生js 或jquery这样的库去操作DOM时,浏览器会从构建DOM树开始执行完以上整个流程,所以频繁操作DOM会引起不需要的计算、重排与重绘,从而导致页面卡顿,影响用户体验

所以减少DOM的操作能达到性能优化的目的,事实上,虚拟DOM就是这么做的,虚拟DOM(VirtualDOM) 的实现原理主要包括以下 3 部分:

  1. 用 JavaScript 对象模拟真实 DOM 树,对真实 DOM 进行抽象
  2. diff 算法 — 比较新旧两个虚拟DOM,得到差异对象
  3. pach 算法 — 将差异对象应用到真实 DOM 树

Virtual DOM 本质上就是一个javascript对象,数据的修改会生成一个新的虚拟DOM(一个新的javascript对象),然后与旧的虚拟DOM进行对比,得到两个对象的差异项,最后只更新差异对象中的内容到真实DOM,这样能做到最少限度地修改真实DOM,从而实现性能优化

如何实现角色权限分配

在开发中后台应用过程中或多或少都会涉及到一个问题:权限,简单地说就是让不同的用户在系统中拥有不同的操作能力。

但在实际应用中我们一般不直接将权限赋予在用户身上,因为这样操作对有大量用户的系统来说过于繁琐,所以我们一般基于RBAC(Role-Based Access Control)权限模型,引入角色的概念,通过角色的媒介过渡,先将权限赋予在角色上,再关联相应的用户,对应的用户就继承了角色的权限

用户与角色,角色与权限都是多对多的关系

引入角色媒介的优点:

  1. 实现了用户与权限的解耦
  2. 提高了权限配置的效率
  3. 降低了后期维护的成本

双向数据绑定和单向数据流的优缺点

所谓数据绑定,就是指View层和Model层之间的映射关系。

  • 单向数据绑定:Model的更新会触发View的更新,而View的更新不会触发Model的更新,它们的作用是单向的。

    优点:所有状态变化都可以被记录、跟踪,状态变化通过手动调用触发,源头易追溯。

    缺点:会有很多类似的样板代码,代码量会相应的上升。

  • 双向数据绑定:Model的更新会触发View的更新,View的更新也会触发Model的更新,它们的作用是相互的。

    优点:在操作表单时使用v-model方便简单,可以省略繁琐或重复的onChange事件去处理每个表单数据的变化(减少代码量)。

    缺点:属于暗箱操作,无法很好的追踪双向绑定的数据的变化。

Vue是如何实现双向绑定的?

Vue的双向数据绑定是通过数据劫持结合发布者订阅者模式来实现的 要实现这种双向数据绑定,必要的条件有:

  1. 实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者
  2. 实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数
  3. 实现一个Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图
  4. MVVM入口函数,整合以上三者

补充回答

在new Vue的时候,在 Observer 中通过 Object.defineProperty() 达到数据劫持,代理所有数据的 getter 和 setter 属性,在每次触发 setter 的时候,都会通过 Dep 来通知 Watcher,Watcher 作为Observer数据监听器与Compile模板解析器之间的桥梁,当 Observer 监听到数据发生改变的时候,通过 Updater 来通知 Compile 更新视图,而 Compile 通过 Watcher 订阅对应数据,绑定更新函数,通过 Dep 来添加订阅者,达到双向绑定。

Proxy与Object.defineProperty的优劣对比?

Proxy的优势如下

  • Proxy可以直接监听整个对象而非属性。
  • Proxy可以直接监听数组的变化。
  • Proxy有13中拦截方法,如ownKeys、deleteProperty、has 等是 Object.defineProperty 不具备的。
  • Proxy返回的是一个新对象,我们可以只操作新的对象达到目的,而Object.defineProperty只能遍历对象属性直接修改;
  • Proxy做为新标准将受到浏览器产商重点持续的性能优化,也就是传说中的新标准的性能红利。

Object.defineProperty 的优势如下

  • 兼容性好,支持 IE9,而 Proxy 的存在浏览器兼容性问题,而且无法用 polyfill 磨平。

Object.defineProperty 不足在于

  • Object.defineProperty 只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。
  • Object.defineProperty不能监听数组。是通过重写数据的那7个可以改变数据的方法来对数组进行监听的。
  • Object.defineProperty 也不能对 es6 新产生的 Map,Set 这些数据结构做出监听。
  • Object.defineProperty也不能监听新增和删除操作,通过 Vue.set()Vue.delete来实现响应式的。

你是如何理解Vue的响应式系统的?

就是能够自动追踪数据的变化,而不必手动触发视图更新。Vue2.X通过Object.defineProperty()做数据劫持,而Vue3通过Proxy做数据代理,从而捕捉到对数据的get和set。

  • 什么叫数据响应式?

简单说就是用户更改数据(Data)时,视图可以自动刷新,页面UI能够响应数据变化。

  • 为什么要了解Vue数据响应式?

因为这是Vue的立身之本呀,连尤雨溪都在给放文档上这样说,“Vue 最独特的特性之一,是其非侵入性的响应式系统。”

Vue就是通过getter 和setter来对数据进行操作,通过get()和set()函数可以生成一个虚拟属性,来直接调用函数操作数据。这一步是被封装起来的,我们是看不到的。

此外,还需要一个重要的媒介API,那就是Object.defineProperty,因为对象被定义完成后,想要更改或添加属性(像是get和set这样的属性),只能通过这个Object.defineProperty来添加。接着需要实现对数据属性的读写进行监控。能够监控就意味着能让vm(一般生成的实例)能够知道数据有变化,从而触发render(data)函数,页面UI就会做出响应。

既然Vue通过数据劫持可以精准探测数据变化,为什么还需要虚拟DOM进行diff检测差异?

1.减少对真实DOM的操作

Diff 算法探讨的就是虚拟 DOM 树发生变化后,生成 DOM 树更新补丁的方式。对比新旧两株虚拟 DOM 树的变更差异,将更新补丁作用于真实 DOM,以最小成本完成视图更新。

具体流程:

  1. 真实 DOM 与虚拟 DOM 之间存在一个映射关系。这个映射关系依靠初始化时的 JSX 建立完成;
  2. 当虚拟 DOM 发生变化后,就会根据差距计算生成 patch,这个 patch 是一个结构化的数据,内容包含了增加、更新、移除等;
  3. 最后再根据 patch 去更新真实的 DOM,反馈到用户的界面上。

这样一个生成补丁、更新差异的过程统称为 diff 算法。

这里涉及3个要点:

  1. 更新时机:更新发生在setState、Hooks 调用等操作以后
  2. 遍历算法:采用深度优先遍历,从根节点出发,沿着左子树方向进行纵向遍历,直到找到叶子节点为止。然后回溯到前一个节点,进行右子树节点的遍历,直到遍历完所有可达节点
  3. 优化策略:树、组件及元素三个层面进行复杂度的优化
    1. 忽略节点跨层级操作场景,提升比对效率。
      1. 这一策略需要进行树比对,即对树进行分层比较。树比对的处理手法是非常“暴力”的,即两棵树只对同一层次的节点进行比较,如果发现节点已经不存在了,则该节点及其子节点会被完全删除掉,不会用于进一步的比较,这就提升了比对效率
    2. 如果组件的 class 一致,则默认为相似的树结构,否则默认为不同的树结构
      1. 在组件比对的过程中:如果组件是同一类型则进行树比对;如果不是则直接放入补丁中。只要父组件类型不同,就会被重新渲染。这也就是为什么shouldComponentUpdate、PureComponent 及 React.memo 可以提高性能的原因
    3. 同一层级的子节点,可以通过标记 key 的方式进行列表对比
      1. 元素比对主要发生在同层级中,通过标记节点操作生成补丁。节点操作包含了插入、移动、删除等。其中节点重新排序同时涉及插入、移动、删除三个操作,所以效率消耗最大,此时策略三起到了至关重要的作用。通过标记 key 的方式,React 可以直接移动 DOM 节点,降低内耗

通过diff算法能准确的获取到具体的节点 进行针对操作!!!

Vue为什么没有类似于React中shouldComponentUpdate的生命周期?

因为 Vue 的响应式系统已经在初次渲染时收集了渲染依赖的数据项,通过自动的方式就能够得到相当不错的性能。不过,在一些场景下,手动控制刷新检查还是能够进一步提升渲染性能的

vue 的机制,其实就是个依赖关系管理机制。不管是计算属性,watcher,以及 renderer,站在外部看,模型都是一样的,即初始化的时候通过一些方法分析出来这个模块依赖谁,等被依赖的数据更新了,这些模块就重新运算一遍产出物(实际上不尽然,比如计算属性有惰性机制)。

具体到 renderer,哪些数据的变更会引起 dom 变更,在模板编译的时候已经确定了,并且写死在生成的代码里了。

而 react 是没有这种自动机制的,它去执行 render 唯一的原因就是你主动让他 render。那你什么时候让它 render 呢?工程上一般是使用一个数据流工具,数据有变化的时候发出一个事件,一股脑把数据推过来,不区分哪个字段有变更(你区分了也没用,renderer 根本不认)。如果这个数据流模型是多个组件共用的,那必然是在 render 之前有个 hook 给我们个机会告诉组件“这次没你的事儿”,有利于性能优化。

那么,我们有没有可能不增加代码静态分析环节,搞清楚 react renderer 到底依赖哪些数据,继而把这个判断自动做掉呢?依我看不太可能,因为我们不能保证运行期跑一遍 render,它就会一次性访问它所有可能访问的数据。

Vue中的key到底有什么用?

1、key的作用主要是为了搞笑的更新虚拟dom,其原理是vue在patch过程中通过key可以精准判断两个节点是否是同一个,从而避免频繁更新不同元素,使得整个patch过程更加高效,减少dom操作量,提高性能。
2、另外,若不设置key还可能在列表更新时候引发一些隐藏的bug。
3、vue中在使用相同标签名元素的过渡切换时,也会使用到key属性,其目的也是为了让vue可以区分它们,否则vue只会替换其内部属性而不会触发过渡效果。

不能用index做key

看了上面的介绍其实也很容易明白为什么不能用index作为key。

1、影响性能:当用index作为key的时候,删除节点后面的所有节点都会导致重新渲染,因为index变化了,可以也就变化了

有人说,当你的列表数据没有变化的时候可以用index作为key。也就是说列表不会触发更新元素,只有静态展示。这种说法你怎么看呢?
之所以说到这个问题,是在vue官方群里面一群人应为这个问题讨论半天。我弱弱回复一句,任何情况下都不要用index作为key。结果遭到炮轰,哎!(除非前端写死的list,且无操作不会引起key变化,只要是后端数据,前端怎么能保证数据不变呢)。
关于这个问题,我有这样三点想法:

1、代码的规范性
2、类比typescript,为什么变量要加类型,除了规范,也方便定位错误
3、列表的顺序稳定其实是难以保证的

vue项目中用jsx语法

JSX就是Javascript和XML结合的一种格式。React发明了JSX,利用HTML语法来创建虚拟DOM。当遇到<,JSX就当HTML解析,遇到{就当JavaScript解析.

我为什么要在vue中用JSX?

是使用的的模板语法,Vue的模板实际上就是编译成了 render 函数,同样支持 JSX 语法。在 Vue 官网中,提供 createElement 函数中使用模板中的功能。

<script>
   export default {
       name: "item",
       props:{
         id:{
           type:Number,
           default:1
         }
       },
     render(){
         const hText=`
                       <h${this.id}>${this.$slots.default[0].text}</h${this.id}>
                     `
       return <div domPropsInnerHTML={hText}></div>
     }
   }
</script>

但是极少数的 VUE项目用JSX语法 JSX语法 根植在react上面 在vue上还是 template舒服!

vue的$set是什么?

我们使用vue进行开发的过程中,可能会遇到一种情况:当生成vue实例后,当再次给数据赋值时,有时候并不会自动更新到视图上去;

当我们去看vue文档的时候,会发现有这么一句话:如果在实例创建之后添加新的属性到实例上,它不会触发视图更新。例如这个代码:

<div id="app">
    姓名:{{ name }}<br>
    年龄:{{age}}<br>
    性别:{{sex}}<br>
    说明:{{info.content}}
</div>
<script>
    var data = {
        name: "张三",
        age: '3',
        info: {
            content: 'my name is test'
        }
    }    
    var vm = new Vue({
        el:'#app',
        data: data
    });
    data.sex = '男';
</script>

运行结果:

姓名:张三
年龄:3
性别:
说明:my name is test

为什么会这样呢?当去查对应文档时,你会发现响应系统 ,把一个普通 Javascript 对象传给 Vue 实例来作为它的 data 选项,Vue 将遍历它的属性,用 Object.defineProperty 将它们转为 getter/setter,如上示例,将data在控制台打印出来会发现:

https://images2017.cnblogs.com/blog/1055667/201710/1055667-20171017175710537-1569151545.png

在age及name都有get和set方法,但是在sex里面并没有这两个方法,因此,设置了sex值后vue并不会自动更新视图;

解决方法:

new Vue({
    el:'#app',
    data: data,
    created: function(){
        Vue.set(data,'sex', '男'); // 等同于this.$set(data,'sex','男')
    }
});

总结:

由于 Vue 会在初始化实例时进行双向数据绑定,使用Object.defineProperty()对属性遍历添加 getter/setter 方法,所以属性必须在 data 对象上存在时才能进行上述过程 ,这样才能让它是响应的。如果要给对象添加新的属性,此时新属性没有进行过上述过程,不是响应式的,所以会出现数据变化,页面不变的情况。此时需要用到$set。

语法: this.$set(Object, key, value)

vue里绑定了一个空对象,往空对象里面添加属性,在哪个函数周期里面可以实现

需要在created里面才可以,如下代码:

<div>{{user}}</div>
 data () {
    return {
      user: { },
    }
  },
 created () {
    this.user.a = 1  // 这样界面就会显示 {a:1}
  },

Vue的通信方式有什么

vue中8种常规的通信方案

  1. 通过 props 传递
  2. 通过 $emit 触发自定义事件
  3. 使用 ref
  4. EventBus
  5. parent或root
  6. attrs 与 listeners
  7. Provide 与 Inject
  8. Vuex
props传递数据

img

  • 适用场景:父组件传递数据给子组件
  • 子组件设置props属性,定义接收父组件传递过来的参数
  • 父组件在使用子组件标签中通过字面量来传递值

Children.vue

props:{
    // 字符串形式
 name:String // 接收的类型参数
    // 对象形式
    age:{  
        type:Number, // 接收的类型为数值
        defaule:18,  // 默认值为18
       require:true // age属性必须传递
    }
}

Father.vue

<Children name:"jack" age=18 />
$emit 触发自定义事件
  • 适用场景:子组件传递数据给父组件
  • 子组件通过$emit触发自定义事件,$emit第二个参数为传递的数值
  • 父组件绑定监听器获取到子组件传递过来的参数

Children.vue

this.$emit('add', good)

Father.vue

<Children @add="cartAdd($event)" />
ref
  • 父组件在使用子组件的时候设置ref
  • 父组件通过设置子组件ref来获取数据

父组件

<Children ref="foo" />

this.$refs.foo  // 获取子组件实例,通过子组件实例我们就能拿到对应的数据
EventBus
  • 使用场景:兄弟组件传值
  • 创建一个中央时间总线EventBus
  • 兄弟组件通过$emit触发自定义事件,$emit第二个参数为传递的数值
  • 另一个兄弟组件通过$on监听自定义事件

Bus.js

// 创建一个中央时间总线类
class Bus {
  constructor() {
    this.callbacks = {};   // 存放事件的名字
  }
  $on(name, fn) {
    this.callbacks[name] = this.callbacks[name] || [];
    this.callbacks[name].push(fn);
  }
  $emit(name, args) {
    if (this.callbacks[name]) {
      this.callbacks[name].forEach((cb) => cb(args));
    }
  }
}
// main.js
Vue.prototype.$bus = new Bus() // 将$bus挂载到vue实例的原型上
// 另一种方式
Vue.prototype.$bus = new Vue() // Vue已经实现了Bus的功能

Children1.vue

this.$bus.$emit('foo')

Children2.vue

this.$bus.$on('foo', this.handle)
Parent或 root
  • 通过共同祖辈$parent或者$root搭建通信侨联

兄弟组件

this.$parent.on('add',this.add)

另一个兄弟组件

this.$parent.emit('add')
attrs与 listeners
  • 适用场景:祖先传递数据给子孙
  • 设置批量向下传属性$attrs$listeners
  • 包含了父级作用域中不作为 prop 被识别 (且获取) 的特性绑定 ( class 和 style 除外)。
  • 可以通过 v-bind="$attrs" 传⼊内部组件
// child:并未在props中声明foo
<p>{{$attrs.foo}}</p>

// parent
<HelloWorld foo="foo"/>
// 给Grandson隔代传值,communication/index.vue
<Child2 msg="lalala" @some-event="onSomeEvent"></Child2>

// Child2做展开
<Grandson v-bind="$attrs" v-on="$listeners"></Grandson>

// Grandson使⽤
<div @click="$emit('some-event', 'msg from grandson')">
{{msg}}
</div>
provide 与 inject
  • 在祖先组件定义provide属性,返回传递的值
  • 在后代组件通过inject接收组件传递过来的值

祖先组件

provide(){
    return {
        foo:'foo'
    }
}

后代组件

inject:['foo'] // 获取到祖先组件传递过来的值
vuex
  • 适用场景: 复杂关系的组件数据传递
  • Vuex作用相当于一个用来存储共享变量的容器

img

  • state用来存放共享变量的地方
  • getter,可以增加一个getter派生状态,(相当于store中的计算属性),用来获得共享变量的值
  • mutations用来存放修改state的方法。
  • actions也是用来存放修改state的方法,不过action是在mutations的基础上进行。常用来做一些异步操作
小结
  • 父子关系的组件数据传递选择 props$emit进行传递,也可选择ref
  • 兄弟关系的组件数据传递可选择$bus,其次可以选择$parent进行传递
  • 祖先与后代组件数据传递可选择attrslisteners或者 ProvideInject
  • 复杂关系的组件数据传递可以通过vuex存放共享的变量

Vue的生命周期里什么时候拿DOM结构比较好

Vue实例从创建到销毁的过程就是Vue的生命周期。

也就是说:开始创建->初始化实例->编译模板->挂载dom->数据更新和重新渲染虚拟dom->最后销毁。这一系列的过程就是Vue的生命周期。

beforeCreate:vue实例的挂载元素el和数据对象data还没有初始化,还是一个undefined的一个状态。

created:这个时候Vue实例的数据对象data已经有了,可以访问里面的数据和方法,el还没有,也没有挂载dom

beforeMount:在这里Vue实例的元素el和数据对象都有了,只不过在挂载之前还是虚拟的dom节点。

mounted:Vue实例已经挂载在真实的dom节点上了,可以对dom进行操作来获取dom节点。

beforeUpdate:响应式数据更新时调用,发生在虚拟dom打补丁之前,适合在更新之前访问现有的dom。

updated:虚拟dom重新渲染和打补丁之后调用,组成新的dom已经更新,避免在这个钩子函数种操作数据,防止死循环。

beforeDestory:Vue实例在销毁之前调用,在这里可以使用,通过this也能访问到实例,可以在这里对一些不用的定时器进行清除,解绑事件。

destoryed:vue销毁之后调用,调用之后所有的事件监听都会被移除,所有的实例都会被销毁。

beforeMount与mounted的区别

beforeMount:在这里Vue实例的元素el和数据对象都有了,只不过在挂载之前还是虚拟的dom节点。

挂载前 :完成模板编译,虚拟Dom已经创建完成,即将开始渲染。在此时也可以对数据进行更改,不会触发updated。数据还没有更新到页面上去。当编译完成之后,只是在内存中已经有了编译好的页面,但并未渲染。

mounted:Vue实例已经挂载在真实的dom节点上了,可以对dom进行操作来获取dom节点。

挂载完成 : 将编译好的模板挂载到页面 (虚拟DOM挂载) ,可以在这进行异步请求以及DOM节点的访问,在vue用$ref操作

跳转下一个页面,在返回,怎么保持页面的状态不变

在vue项目中,难免会有列表页面或者搜索结果列表页面,点击某个结果之后,返回回来时,如果不对结果页面进行缓存,那么返回列表页面的时候会回到初始状态,但是我们想要的结果是返回时这个页面还是之前搜索的结果列表,这时候就需要用到vue的keep-alive技术了.

keep-alive 简介

keep-alive 是 Vue 内置的一个组件,可以使被包含的组件保留状态,或避免重新渲染。
实际项目中,需要配合vue-router共同使用

router-view 也是一个组件,如果直接被包在 keep-alive 里面,所有路径匹配到的视图组件都会被缓存:

<keep-alive>
    <!-- 所有路径匹配到的视图组件都会被缓存! -->
    <router-view>
    </router-view>
</keep-alive>

如果只想 router-view 里面某个组件被缓存,怎么办?

增加 router.meta 属性

// routes 配置
export default [
  {
    path: '/',
    name: 'home',
    component: Home,
    meta: {
      keepAlive: true // 需要被缓存
    }
  }, {
    path: '/:id',
    name: 'edit',
    component: Edit,
    meta: {
      keepAlive: false // 不需要被缓存
    }
  }
]
<!-- 这里是会被缓存的视图组件,比如 Home! -->
<keep-alive>
    <router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>

 <!-- 这里是不被缓存的视图组件,比如 Edit! -->
<router-view v-if="!$route.meta.keepAlive"></router-view>

Vue的性能优化有哪些方法

1、路由懒加载
2、keep-alive缓存页面
3、使用v-show复用DOM
4、v-for遍历避免同时使用v-if (使用computed属性过滤需要显示的数据项 )
5、长列表性能优化

(1)如果列表是纯粹的数据展示,不会有任何改变,就不需要做响应话, 使用object.freeze(data)
(2)如果是大数据长列表,可采用虚拟滚动,只渲染少部分区域的内容,参考vue-virtual-scroller、vue-virtual-scroll-list

6、事件的销毁

vue组件销毁时,会自动解绑它的全部指令及事件监听,但是仅限于组件本身的限制

7、图片懒加载
8、第三方插件按需引入

像element-ui这样的第三方组件库可以按需引入避免体积太大。

9、无状态的组件标记为函数式组件
<template functional>
  	<div class='cell'>
		<div v-if="props.value" class="on"></div>
		<section v-else class="off"></section>
	</div>
</template>

<script>
	export defalut {
		props: ['value']
	}
</script>


10、SSR

vue项目怎么进行seo的优化

Vue的单页面模式进行开发的项目,网站信息搜索引擎无法做索引,导入收录只有是首页!

搜索引擎无法进行索引的核心原因就是,其在爬取网站数据的时候,是不会执行其中包含的JS过程的;

而采用Vue的方式开发的应用,其数据都是来源于axios或者其它的ajax方法获取的数据!也就是说,想要友好的支持搜索引擎,就必须采用服务器端渲染的相关技术,比如JSP,就是一个典型的服务器端渲染技术,用户请求一个地址然后展示到浏览器中的数据都是服务器端处理好的,浏览器只管展示;又比如静态页面,所有页面都是预先编写或生成好的,浏览器将请求拿到的数据直接展现即可。
所以为了实现seo的优化,我们可以采用比较成熟的服务端SSR的框架Nuxt实现。

element-ui要改变默认样式有几种方法

当我们在vue中引入第三方组件库的时候,vue组件中样式的scoped就会成为我们修改样式的阻碍,有以下三种方法修改样式,并且不影响全局样式.

  • 在样式外新增一个样式标签不添加scoped

    <style>
    	.my{
    		margin: 20px;
    	}
    	.my .el-input__inner{
    		border-radius: 15px;/* 这个样式起效果 */
    	}
    </style>
    <style scoped>
    	.my .el-input__inner{
    		border-radius: 30px; /* 这个样式不起效果 */
    	}
    </style>
    
  • 使用deep样式穿透

    <style scoped>
    	.my .el-input__inner{
    		border-radius: 30px;/* 这个不起作用 */
    	}
    	.my /deep/ .el-input__inner{
    		border-radius: 30px;/* 这个起作用 */
    	}
    </style>
    
  • 使用>>>穿透

    <style scoped>
    	.my .el-input__inner{
    		border-radius: 30px;/* 这个不起作用 */
    	}
    	.my >>> .el-input__inner{
    		border-radius: 30px;/* 这些起作用 */
    		border: 1px solid #eceef2;
    		outline: 0;
    	}
    </style>
    

用vue开发项目和原生开发项目相比有什么好处

? 首先vue和原生js都可以用来开发web项目,但越来越多的项目在立项及技术选型时选择使用vue,主要是有以下优势.

  1. 控件跟数据自动绑定,可以直接使用data里面的数据值来提交表单,而不需要再使用$(“#myid”).val()那一套方法来获取控件的值,对控件赋值也方便很多,只需要改变data的值,控件就会自动改变值。将复杂的界面操作,转化为对数据进行操作.

  2. 页面参数传递和页面状态管理.

    页面传值对于vue来说,可供选择的方法非常多。比如使用子组件实现,通过对props属性传值;也可以使用页面url参数的方法传值;或使用vuex全局状态管理的方法页面传值等等。而原生开发的时候,在页面有多个参数的时候,页面传值和初始化,要复杂很多。而vue直接将参数保存在对象里面,直接给子组件的属性或vuex存储一个对象就行了,比如 , 这样就可以将userinfo传到自定义组件。

  3. 模块化开发、无刷新保留场景参数更新

    比如一个列表页面里面有添加功能,有修改功能,这时候我们可以通过引用子组件的形式,当子组件内容更新的时候,修改主组件的数据,比如修改了一条数据后,我们需要列表页同时刷新,但我们不希望改变原来列表页的页码和搜索条件。假如你用原生开发来实现这个,需要写很多业务逻辑保存上一个页面的搜索条件和页码这些参数,但假如你用vue开发,将变得非常简单.

  4. 代码的可阅读性

    vue天生具有组件化开发的能力,因此不同的功能基本都是写在不同的模块里面,因此代码的可阅读性非常高。当一个新手接手一个旧项目的时候,基本上可以做到一天就能定位到要修改的代码,进行修改,快速接手项目.

  5. 丰富的生态圈

    基于强大的nodejs,添加新的组件库,基本一句npm命令就能安装,比如当我需要使用axios组件的时候,直接npm install axios安装一下,就可以使用axios这个组件。

  6. 开发单页面应用非常友好

    主路由、子路由、主页面、子组件的方式,可以让我们彻底抛弃iframe。写过前端页面的都知道,因为iframe的滚动条、和子页面跟其他页面的交互性这些原因、用户体验还是远远没有单页面架构友好。而且使用vue非常简单方便的实现系统菜单、导航等固定布局.

  7. 各子组件样式不冲突

    各个组件之间,可以使用相同的样式名,但有不同的样式属性。比如组件A和组件B的button都绑定了class=“btn”, 但在两个组件里,我们可以实现两个不同的btn样式属性,互不影响

    当然,vue也有不足,不足的地方如下:

    1. vue是单页面页面,对于搜索引擎不友好,影响seo.因此不适合做公司官网。比如两个vue路由(页面),它的路径是这样的:index.html#aaa 和 index.html#bbb,但对于搜索引擎来说,都是同一个页面,就是index.html。这样搜索引擎就无法收录你的页面。

    2. vue门槛较高,使用vue,需要先学习和摸索vue大概3天左右的时候,建议使用vue的时候,不需要看node.js自动帮你生成的js文件。你只需要编写你自己页面的代码就行了。

element-UI的table组件有哪些属性,为什么table组件要用key?

  • table表格组件的属性有:

    • data , 显示的数据
    • size , 表格的尺寸, 可选的值有medium / small / mini
    • height , Table 的高度,默认为自动高度。如果 height 为 number 类型,单位 px.
    • empty-text , 空数据时显示的文本内容
    • stripe , 是否为斑马纹 table , boolean类型.
    • border , 是否带有纵向边框 , boolean类型.
    • row-key , 行数据的 Key,用来优化 Table 的渲染;在使用 reserve-selection 功能与显示树形数据时,该属性是必填的.
  • 为什么table组件要用key?

    row-key属性用来优化 Table 的渲染, 如果不添加row-key,当重新请求渲染表格数据,会默认触发@current-change等等方法,因为重新渲染列表,current-change发生了改变,原本选择的row,变成不再选择任何一行,导致问题很难排查.

    参考官网: table组件

  • 4
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值