前端八股文之vue

2,742 阅读23分钟

目录

  1. 说一下Vue的$nextTick原理
  2. 说一下 Vue nextTick 实现原理,以及是如何知道 dom 渲染结束的?
  3. nextTick 是在本次循环执行,还是在下次循环执行呢?setTimeout(()=>{},1000)是怎样执行的呢?
  4. 介绍 Vue template 到 render 的过程
  5. 既然 vue 通过数据劫持可以精准探测数据在具体的 dom 上变化,为什么还需要虚拟 DOM diff 呢?
  6. 对虚拟DOM的理解?虚拟DOM主要做了什么?虚拟DOM本身是什么?
  7. 你知道 Vue 的模板语法用的是哪个 web 模板引擎么?说说你对模板引擎的理解?
  8. Vue 中的 key 有什么作用?
  9. 简述 Vue 的基本原理
  10. vue2.x 为什么要求组件模板只能由一个根元素?
  11. Vue 切换路由的时候,需要保存当时状态的功能,怎么实现呢?
  12. 如何解决 vue 打包 vendor 过大的问题?webpack 打包 vue 速度慢怎么办?
  13. vue Hooks 有哪些?
  14. Vuex 的 action 和 mutation 的特性是什么?有什么区别?
  15. 说一下路由钩子在 vue 生命周期的体现?
  16. 页面刷新后的vuex的state数据丢失怎么办?
  17. 完整的路由导航解析流程(不包括其他生命周期)
  18. Vuex 怎么知道 state 是通过 mutation 修改还是外部直接修改的?
  19. 为什么要用 vuex 或者 Redux?
  20. Redux和Vuex有什么区别,说一下它们的共同思想?
  21. Vue-router history 模式部署的时候要注意什么?server 端用 nginx 和 node 时候分别怎么处理?
  22. 子组件可以直接改变父组件的数据么?说说你的理由?(vue部分)
  23. 说一下你对 Vue 中 keep-alive 的理解,以及在使用过程中需要注意的地方?
  24. vue 如何做权限校验?
  25. 使用过 Vue SSR 吗?说说 SSR?
  26. 说说Vue开发如何针对搜索引擎做SEO优化?
  27. 描述下自定义指令(vue部分)
  28. v-model是如何实现的,语法糖实际是什么?
  29. 怎样理解 Vue 的单向数据流?
  30. 说一下Vue单页与多页的区别?
  31. Vue-cli默认是单页面的,如果要开发多页面应该怎么办?
  32. 说一下Vue 的父组件和子组件生命周期钩子函数执行顺序?
  33. 说一下Vue的生命周期以及每个阶段做的事情
  34. 在哪个生命周期内调用异步请求
  35. 在 Vue 中父组件可以监听到子组件的生命周期么?
  36. Vue 组件间通信有哪几种方式?
  37. 直接给一个数组项赋值,Vue 能检测到变化吗
  38. 为什么组件中的 data 必须是一个函数,然后 return 一个对象,而 new Vue 实例里,data 可以直接是一个对象?
  39. 关于对 Vue 项目进行优化,你有哪些方法?
  40. 说一下Vue的keep-alive是如何实现的,具体缓存的是什么?
  41. 计算属性和普通属性的区别是什么?
  42. 说一下 vm.$set 原理

待输出

  1. 说一下 Vue3 的 Composition API
  2. Vuex和localStorage的区别是什么?
  3. 说一下vue-router的原理是什么?
  4. vue的双向绑定的原理是什么?
  5. Vue是如何收集依赖的?
  6. 介绍下 vue-router 中的导航钩子函数
  7. 能说下 vue-router 中常用的 hash 和 history 路由模式实现原理吗?

说一下Vue的$nextTick原理

js运行机制(Event Loop)

js执行是单线程的,它是基于事件循环的

  • 所有同步任务都在主线程上执行的,形成一个执行栈
  • 主线程外,会存在一个任务队列,只要异步任务有结果,就在任务队列中放置一个事件
  • 当执行栈中的所有同步任务执行完后,就会读取去任务队列。那么对应的异步任务,会结束等待状态,进入执行栈
  • 主线程不断重复第三步

这里主线程的执行过程是一个tick,而所有的异步结果都是通过任务队列来调度。Event Loop分为宏任务和微任务,无论是执行宏任务还是微任务,完成后都会进入到下一个tick,并且在两个tick之间进程UI渲染

由于Vue DOM 更新是异步执行的,即修改数据时,视图不会立即更新,而是监听数据变化,并缓存在同一事件循环中,等同一数据循环中的所有数据变化完成之后,在同一进行视图更新。为了确保得到更新后的DOM,所以设置了Vue.nextTick()方法

什么是$nextTick

Vue的核心方法之一,官方文档解释如下:在下次DOM更新循环之后执行的延迟回调。

在修改数据之后立即使用这个方法,获取到更新后的DOM

1. MutationObserver

MutationObserver是HTML5中的API,是一个用于监听DOM变动的接口,它可以监听一个DOM对象上发生的子节点删除、属性修改、文本内容修改等。调用过程是首先给它绑定回调,得到MO实例对象,这个回调在MO实例对象上监听到变动时触发。这里的MO回调是放在microtask中执行的

//创建MO实例
const Observer=new MutationObserver(callback);
const textNode="你好,世界!"
Observer.observe(textNode,{
    characterData:true//监听文本内容的修改
})

2. 源码分析

nextTick的实现单独有一个JS文件来维护它。nextTick源码主要分为两块:能力检测和根据能力检测以不同方式执行回调队列 能力检测 由于宏任务耗费的时间是大于微任务的,所以在浏览器支持的情况下,优先使用微任务。如果浏览器不支持微任务,再使用宏任务

//空函数,可用作函数占位符
import {noop} from './util';

//错误处理函数
import {handleError} from './error';

//是否是IE、ios、内置函数
import {isIE,isIOS,isNative} from './env';

//用来存储所有需要执行的回调函数
const callbacks=[];

//标志下是否正在执行回调函数
const flag=false;

//对callback进行遍历,然后执行相应的回调函数
function shift(){
    flag=false
    //这里拷贝的原因是:有的回调函数执行过程中又往callbacks中加入内容
    //比如$nextTick的回调函数里还有$nextTick,后者应该放到下一轮的nextTick中执行,所以拷贝一份当前的,遍历执行完当前的即可,避免无休止的执行下去
    const cb=callbacks.slice(0)
    callbacks.length=0
    for(var i=0;i<cb.length;i++){
        cb[i]()
    }
}

const timeFunc;//异步执行函数,用于异步延迟调用flushCallbacks函数

if(typeof Promise !== "undefined" && isNative(Promise)){
    const p=Promise.resolve()
    timeFunc=()=>{
        p.then(flushCallbacks);
        //IOS的UIView、Peomise.then 回调被推入microTask队列,但是队列可能不会如期执行
        //因此,添加一个空计时器强制microTask
        if (isIOS) setTimeout(noop)
    }
    isUsingMicroTask=true;
}else if(!isIE && typeof MutationObserver !=='undefined' && (isNative(MutationObserver) || MutationObserver.toString === "[Object MutationObserverConstructor]")){
    //当原生Promise不可用时,使用原生MutationObserver
    let counter=1
    //创建MO实例,监听到DOM变动后会执行回调flushCallbacks
    const obs=new MutationObserver(flushCallbacks);
    const textNode=document.createTextNode(String(conter))
    obs.observe(textNode,{
        characterData:true,//监听目标的变化
    })
    //每次执行timeFunc都hi让文本节点的内容在0和1之间切换
    //切换之后将新值赋值到MO观察的文本节点上,节点内容变化会触发回调
    timeFunc=()=>{
        counter=(counter+1)%2
        textNode.data=String(counter)//触发回调函数
    }
    isUsingMicroTask=true;
}else if(typeof setImmediate !== 'undefined' && isNative(setImmediate)){
    timeFunc=()=>{
        setImmediate(flushCallbacks)
    }
}else{
    timeFunc=()=>{
        setTimeout(flushCallbacks,0)
    }
}

延迟调用优先级如下:Promise > MutationObserver > setImmediate > setTimeout

export function nextTick(cd? Function,ctx:Object){
    let _resolve
    //cb回调函数会统一处理压入callbacks数组
    callbacks.push(()=>{
        if(cb){
            try{
                cb.call(ctx)
            }catch(e){
                handleError(e,ctx,'nextTick')
            }
        }else if(_resolve){
            _resolve(ctx)
        }
    })
    //flag为false  说明本轮事件循环中没有执行过timeFunc函数
    if(!flag){
        flag=true;
        timeFunc()
    }
    //当不传入cb参数时,提供一个promise化的调用
    //比如nextTick().then(()=>{}),当_resolve执行时,就会跳转到then逻辑中
    if(!cd && typeof Promise !== 'undefined'){
        return new Promise(resolve=>{
            _resolve=resolve
        })
    }
}

nextTick.js对外暴露了nextTick这一个参数,所以每次调用Vue.nextTick时会执行:

  • 把传入的回调函数cb压入callbacks数组
  • 执行timeFunc函数,延迟调用flushCallbacks函数
  • 遍历执行callbacks数组中的都有函数

这里的callbacks没有直接在nextTick中执行回调函数的原因是:保证在同一个tick内执行多次nextTick,不会开启多个异步任务,而是把这些异步任务都压成一个同步任务,在下一个tick执行完毕。

使用方式

  • 语法:Vue.nextTick([callback,context])

  • 参数:

    • {Function}[callback]:回调函数,不传参数时提供promise调用
    • {Object}[context]:回调函数执行的上下文环境,不传默认是自动绑定到调用它的实例上
//改变数据
vm.message="changed";
//想要立即使用更新后的DOM,这样不行,因为设置message后DOM还没有更新
console.log(vm.$el.textContent)//并不会得到"changed"
//这样可以,nextTick里面的代码在DOM更新后执行
Vue.nextTick(function(){
    //DOM更新,可以得到"changed"
    console.log(vm.$el.textContent)
})
//作为一个promise使用,不传参数时回调
Vue.nextTick().then(function(){
    //DOM更新时的执行代码
})

Vue实例方法vm.$nextTick做了进一步封装,把context参数设置成当前的Vue实例 使用Vue.nextTick()是为了可以获取到更新后的DOM。触发时机:在同一事件循环中的数据变化后,DOM完成更新,立即执行Vue.nextTick()的回调

同一事件循环中的代码执行完毕 -> DOM更新 -> nextTick callback触发

应用场景

在Vue生命周期的created()钩子函数进行的DOM操作一定要放在Vue.nextTick()的回调函数中

原因: created()钩子函数执行时DOM其实并未进行渲染。

在数据变化后要执行的某个操作,而这个操作需要使用随数据改变而改变的DOM结构的时候,这个操作应该放在Vue.nextTick()的回调函数中

原因:Vue异步执行DOM更新,只要观察到数据变化,Vue将开启一个队列,并缓冲在同一个事件循环中发生的所有数据改变,如果同一个watcher被多次触发,只会被推入到队列中一次

总结

  • Vue的nextTick其本质上是对JS执行原理EventLoop的一种应用
  • nextTick的核心应用:Promise、MutationObserver、setImmediate、setTimeout的原生JS的方法来模拟对应的微/宏任务的实现,本质时是为了利用JS的这些异步回调任务队列来实现Vue框架中自己的异步回调队列
  • nextTick不仅是Vue内部的异步队列的调用方法,同时也允许开发者在实际项目中使用这个方法来满足实际应用中对DOM更新数据时的后续逻辑处理
  • nextTick是典型的将底层JS执行原理应用到具体案例中的示例
  • 引入异步更新队列机制的原因
如果是同步更新,则多次对一个或多个属性赋值,会频繁触发UI/DOM的渲染,可以减少一些无用渲染

同时由于VirtualDOM的引入,每一次状态发生改变后,状态变化的信号会发送给组件,组件内部使用VirtualDOM进行计算得出需要更新的具体的DOM节点,然后对DOM进行更新操作,每次更新状态后的渲染过程需要更多的计算,而这种无用功也将浪费更多的性能,所以异步渲染变得更加至关重要。

说一下 Vue nextTick 实现原理,以及是如何知道 dom 渲染结束的?

nextTick 原理分析

官方介绍

Vue 异步执行 DOM 更新。只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。

两种写法:

Vue.$nextTick(() => {
  console.log("new Value");
});

Vue.$nextTick().then(() => {
  console.log("new new Value");
});
结合使用场景分析 nextTick
  1. 在 beforeCreate 或者 Create 生命周期要对 DOM 进行操作,在这两个生命周期可以知道,DOM 还没有被挂载数据甚至还没有初始化 DOM,所以在这里进行 DOM 操作是不太好的,这时需要将 DOM 放到 this.$nextTick
  2. 修改数据之后,及时获取 DOM 数据
this.newValue = "this is a new Value";
this.$nextTick(() => {
  console.log(xxx.getElementById("#id").innerHTML);
});

可以看一下当 Data 数据改变的时候执行情况,Vue 是怎么处理的

class Watcher {
  addDep(dep: Dep) {
    // 添加进dep,同时进行去重
    const id = dep.id;
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id);
      this.newDeps.push(dep);
      if (!this.depIds.has(id)) {
        dep.addSub(this);
      }
    }
  }
  update() {
    queueWatcher(this); // 异步更新策略
  }
  run() {
    //  dom执行真正的更新
  }
}

// 1. 使用setter,通知订阅了newValue属性的所有的watcher
// 2. watcher收到通知,接着把自己放入到待更新的数组
// 3. 紧接着执行dep.notify(),接着执行watch的update,然后queueWatcher
// 4. 执行nextTick(flushSchedulerQueue),将flushSchedulerQueue存到nextTick中的callbacks

这个地方为什么要将更新放入数组,就是因为用户很有可能多次修改数据,如果发生修改就直接修改了 DOM,那就会导致大量的渲染。所以他在 queueWatcher 中判断是否出现相同的 watcher,保证将数据操作更好的聚集起来。

分析源码

nextTick 用于延迟执行回调函数。可以看到,最多接收两个参数,当 callbacks 不存在的时候,会返回一个 Promise 实例,让 nextTick 可以使用 then 方法

export let isUsingMicroTask = false;
// 存放所有的回调函数
const callbacks = [];
// 是否在执行回调函数的标志
let pending = false;
// 处理回调函数
function flushCallbacks() {
  pending = false;
  const copies = callbacks.slice(0);
  callbacks.length = 0;
  for (let i = 0; i < copies.length; i++) {
    // 执行回调函数
    copies[i]();
  }
}
// 触发执行回调函数
let timerFunc;
if (typeof Promise !== "undefined" && isNative(Promise)) {
  // 优先使用promise
  const p = Promise.resolve();
  timerFunc = () => {
    p.then(flushCallbacks);
    if (isIOS) setTimeout(noop);
  };
  isUsingMicroTask = true;
} else if (
  !isIE &&
  typeof MutationObserver !== "undefined" &&
  (isNative(MutationObserver) ||
    // PhantomJS and iOS 7.x
    MutationObserver.toString() === "[object MutationObserverConstructor]")
) {
  // 然后考虑MutationObserver
  let counter = 1;
  const observer = new MutationObserver(flushCallbacks);
  const textNode = document.createTextNode(String(counter));
  observer.observe(textNode, {
    characterData: true,
  });
  timerFunc = () => {
    counter = (counter + 1) % 2;
    textNode.data = String(counter);
  };
  isUsingMicroTask = true;
} else if (typeof setImmediate !== "undefined" && isNative(setImmediate)) {
  // 在考虑setImmediate
  timerFunc = () => {
    setImmediate(flushCallbacks);
  };
} else {
  // 最后使用setTimeout兜底
  timerFunc = () => {
    setTimeout(flushCallbacks, 0);
  };
}

export function nextTick(cb?: Function, ctx?: Object) {
  let _resolve;
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx);
      } catch (e) {
        handleError(e, ctx, "nextTick");
      }
    } else if (_resolve) {
      _resolve(ctx);
    }
  });
  if (!pending) {
    pending = true;
    timerFunc();
  }
  if (!cb && typeof Promise !== "undefined") {
    return new Promise((resolve) => {
      _resolve = resolve;
    });
  }
}
  1. 主函数和变量
  • nextTick 入口函数
  • callbacks 用来存储回调函数
  • timerFunc 利用延迟方法来执行回调函数
  • pending 执行状态
  1. 调用 timerFunc
  • 优先使用原生 Promise
  • 在判断 MutationObserver
  • 然后判断 setImmdiate
  • 最后使用 setTimeout

首先知道在这里面,Promise 和 MutationObserver 属于 micotask,而 setImmdiate 和 setTimeout 是属于 macrotask 的范畴

  1. 接着看看 nextTick 是怎样做到 dom 更新和回调执行顺序
  • 使用数组,保存要执行的操作
  • 每次修改数据,只是往数组推入回调函数,而不是立即执行这些方法
  • 在下次数据循环中,在执行这个数组所有的方法,避免阻塞主线程(下次事件循环可以是微任务,当然也有可能是宏任务)
  • 每次执行完后,将所有的任务队列清空

优先使用了 Promise 等一些微任务,保证在同一次事件循环里面执行,这样页面只需要渲染一次,如果还是不行的话就考虑 setTimeout 等一些宏任务,但是这会第二次渲染。vie 用异步队列的方式来控制 DOM 更新和 nextTick 回调先后执行,保证能在 dom 更新后在执行回调。

这样我们可以推出 nextTick 的触发时机

1. 一次事件循环的代码执行完毕
2. DOM 更新
3. 触发 nextTick 的回调
4. 重复 1,2,3 的步骤
  1. MutationObserver

MutationObserver是 html5 中的新 api,是用来监视 DOM 变动的接口,能监听 DOM 对象发生的子节点删除、属性修改、文本内容修改等等。

let mo = new MutationObserver(callback);

通过给 MutationObserver 的构造函数传入一个回调函数,能得到一个 MutationObserver 实例,这个回调函数能在 MutationObserver 监听到 DOM 变化时触发。

而这个时候仅仅是给 MutationObserver 实例绑定好回调,具体监听哪个 DOM、监听节点删除还是其他操作,这个时候还没有设置,这个时候需要调用 observer 方法

mo.observe(domTarget, {
  characterData: true,
});
//在监听DOM更新后,调用回调函数

nextTick 实现原理简要概括

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

  • 判断逻辑:Promise >> MutationObserver >> setImmediate >> setTimeout
  • 每次 event loop 的最后,会有一个 UI render,也就是更新 DOM
  • 只要让 nextTick 里的代码放在 UI render 步骤后面执行,就能访问更新后的 DOM 了
  • microtask 有:Promise、MutationObserver 以及 nodejs 中的 process.nextTick
  • macrotask 有:setTimeout、setInterval、setImmediate、I/O、UI rendering
  • 每一次事件循环都包含一个 microtask 队列,在循环结束后会依次执行队列中的 microtask 并移除,然后再开始下一次事件循环

vue的nextTick方法的实现原理:

  • vue 用异步队列的方式来控制 DOM 更新和 nextTick 回调先后执行
  • microtask 因为其优先级特性,能确保队列中的微任务在一次事件循环前被执行完毕
  • 因为浏览器和移动端兼容问题,vue 不得不做了 microtask 向 macrotask 的兼容降级方案

nextTick 执行大致分为以下步骤:

  • 把回调函数放入 callbacks 数组等待执行
  • 将执行函数放到微任务或者宏任务中
  • 事件循环到了微任务或宏任务,执行函数依次执行 callbacks 中的回调

如何知道 DOM 渲染结束

通过异步队列的方式来控制 DOM 更新和 nextTick 回调先后执行,保证了 DOM 更新后在执行回调


nextTick 是在本次循环执行,还是在下次循环执行呢?setTimeout(()=>{},1000)是怎样执行的呢?

nextTick 在本次循环执行并且全部执行,setTimeout 在下次循环执行

分析

由于 JS 是单线程,js 设计者把任务分为同步任务和异步任务,同步任务都在主线程上排队执行,前面任务没有执行完成,后面的任务会一直等待;异步任务则是挂在在一个任务队列里,等待主线程所有任务执行完成后,通知任务队列可以把可执行的任务放到主线程执行。异步任务放到主线程执行完后,又通知任务队列把下一个异步任务放到主线程中执行。这个过程一直持续,直到异步任务执行完成,这个持续重复的过程就叫 Event loop。而一次循环就是一次 tick。

在任务队列中的异步任务又可以分为两种 microtask(微任务)和 macrotask(宏任务):

  • microtask(微任务): Promise, process.nextTick, Object.observe, MutationObserver
  • macrotask(宏任务) : script 整体代码、setTimeout、setInterval 等

在执行优先级方面,先执行宏任务macrotask,再执行微任务microtask

执行过程中需要注意的几点是:

  • 在一次event loop中, microtask(微任务)在这一次循环中是一直取一直取,直到清空microtask(微任务)队列,而macrotask则是一次循环取一次。
  • 如果执行事件循环的过程中又加入了异步任务,如果是macrotask(宏任务),则放到macrotask(宏任务)末尾,等待下一轮循环再执行。如果是microtask(微任务),则放到本次event loop中的microtask(微任务)任务末尾继续执行。直到microtask(微任务)队列清空。

介绍 Vue template 到 render 的过程

过程分析

vue 的编译模板过程主要如下:template >> ast >> render 函数

vue 在模板编译版本的源码中会执行compileToFunctions将 template 转化成 render 函数

// 将模板编译为render函数
const { render, staticRenderFns } = compileToFunctions(template,optinos//省略}, this)

compileToFuntions 中的主要逻辑如下:

  • 调用 parse 方法将 template 转化为 ast 语法树
const ast = parse(template.trim(), options);

parse的目标:是把整个 template 转换为 AST 语法树,它是一种用 JS 对象的形式来描述整个模板。

解析过程:利用正则表达式解析模板,当解析到开始标签、闭合标签、文本的时候都会分别执行对应的回调函数,来达到构造 AST 树的目的

AST 元素节点总共三种类型:type 为 1 表示普通元素,type 为 3 表示纯文本。

  • 对静态节点做优化
optimize(ast, options);

这个过程主要分析出哪些是静态节点,给其打一个标记,为后续更新渲染可以直接跳过静态节点做优化

深度遍历AST,查看每个子树的节点元素是否为静态节点或者静态节点根。如果为静态节点,他们生成的 DOM 永远不会改变,这对运行时模板更新起到了极大的优化作用

  • 生成代码
const code = generate(ast, options);

generate 将 ast 抽象语法树编译成 render 字符串,并将静态部分放到 staticRenderFns 中,最后通过 new Funtion(render)生成 render 函数


既然 vue 通过数据劫持可以精准探测数据在具体的 dom 上变化,为什么还需要虚拟 DOM diff 呢?

前置知识: 依赖收集、虚拟DOM、响应式系统

现代前端框架有两种方式侦测变化,一种是pull一种是push

pull: 其代表为 React,我们可以回忆一下React是如何侦测到变化的,我们通常会用 setState API显式更新,然后React会进行一层层的 Virtual Dom Diff操作找出差异,然后Patch到DOM上,React从一开始就不知道到底是哪发生了变化,只是知道「有变化了」,然后再进行比较暴力的Diff操作查找「哪发生变化了」,另外一个代表就是Angular的脏检查操作。

push: Vue的响应式系统则是push的代表,当Vue程序初始化的时候就会对数据data进行依赖的收集,一但数据发生变化,响应式系统就会立刻得知,因此Vue是一开始就知道是「在哪发生变化了」,但是这又会产生一个问题,如果你熟悉Vue的响应式系统就知道,通常一个绑定一个数据就需要一个Watcher,一但我们的绑定细粒度过高就会产生大量的Watcher,这会带来内存以及依赖追踪的开销,而细粒度过低会无法精准侦测变化,因此Vue的设计是选择中等细粒度的方案,在组件级别进行push侦测的方式,也就是那套响应式系统,通常我们会第一时间侦测到发生变化的组件,然后在组件内部进行Virtual Dom Diff获取更加具体的差异,而Virtual Dom Diff则是pull操作,Vue是push+pull结合的方式进行变化侦测的.


对虚拟DOM的理解?虚拟DOM主要做了什么?虚拟DOM本身是什么?

什么是虚拟DOM?

从本质上来说,Virtual Dom是一个JS对象,通过对象的方式来表示DOM结构。将页面的状态抽象为JS对象的形式,配合不同的渲染工具,使跨平台渲染成为可能。通过事务处理机制,将多次DOM修改的结果一次性的更新到页面上,从而有效的减少页面渲染的次数,减少修改DOM的重绘重排次数,提高渲染性能。

虚拟dom是对DOM的抽象,这个对象是更加轻量级的对DOM的描述。它设计的最初目的,就是更好的跨平台,比如node.js就没有DOM,如果想实现SSR,那么一个方式就是借助虚拟dom,因为虚拟dom本身就是js对象。在代码渲染到页面之前,vue或者react会把代码转换成一个对象(虚拟DOM)。以对象的形式来描述真实dom结构,最终渲染到页面。在每次数据发生变化前,虚拟dom都会缓存一份,变化之时,现在的虚拟dom会与缓存的虚拟dom进行比较。

在vue或者react内部封装了diff算法,通过这个算法来进行比较,渲染时修改改变的变化,原先没有发生改变的通过原先的数据进行渲染。

另外现代前端框架的一个基本要求就是无需手动操作DOM,一方面是因为手动操作DOM无法保证程序性能,多人协作的项目中如果review不严格,可能会有开发正写出性能较低的代码,另一方面更重要的是省略手动DOM操作可以大大提升开发效率

为什么要用Virtual Dom?

1.保证性能下限,再不进行手动优化的情况下,提供能过得去的性能

看一下页面渲染的一个过程

解析HTML --- 生成DOM --- 生成cssDom -- Layout -- Paint -- Compiler

下面对比一下修改DOM时真实DOM操作和Virtual Dom的过程,来看一下它们重排重绘的性能消耗

  • 真实DOM:生成HTML字符串+重建所有的DOM元素
  • Virtual Dom:生成VNode+DOMDiff+必要的dom更新 Virtual Dom的更新DOM的准备工作耗费更多的时间,也就是js层面,相对于更多的DOM操作它的消费是极其便宜的。尤大大曾说到:框架给你的保证是,你不需要手动优化的情况下,我依然可以给你提供过得去的性能
2.跨平台

Virtual Dom本质上时JS对象,可以很方便的跨平台操作,比如服务端渲染、uniapp等

Virtual Dom真的比真实DOM性能好么?

  • 1.首次渲染大量DOM时,由于多了一层虚拟DOM的计算,比innerHTML插入慢
  • 2.正如它能保证性能下限,在真实DOM操作的时候进行针对性的优化时,还是更快的。

你知道 Vue 的模板语法用的是哪个 web 模板引擎么?说说你对模板引擎的理解?

Mustache

Vue 使用了 Mustache 模板引擎,即双大括号的语法。

mustache 是一个轻逻辑的模板引擎, 它可以在 HTML、配置文件、源代码等等中使用。它被称为"轻逻辑"是因为没有 if else 表达式或 for 循环,只有标签。它的工作原理是通过标签来展示提供的值。

mustache 使用场景

你可以在 web浏览器、服务端环境使用它,比如 Nodejs、CouchDB。 mustachejs 还支持 Commonjs、AMD 和 ECMAScript 模块。

模板引擎是什么?

模板就个人理解而言其产生的目的是为了解决展示与数据的耦合,简单来说模板还是一段字符,只不过其中有一些片段跟数据相关,实际开发中根据数据模型与模板来动态生成最终的HTML(或其它类型片段)。

而模板引擎就是可以简化该拼接过程,通过一些声明与语法或格式的工具,尽可能让最终HTML的生成简单且直观。

概念性的解释:模板引擎(这里特指用于Web开发的模板引擎)是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,用于网站的模板引擎就会生成一个标准的文档。

模板引擎的核心原理就是两个字:替换。将预先定义的标签字符替换为指定的业务数据,或者根据某种定义好的流程进行输出。


Vue 中的 key 有什么作用?

key 是为 Vue 中 vnode 的唯一标记,通过这个 key,我们的 diff 操作可以更准确、更快速。

Vue 的 diff 过程可以概括为:oldCh 和 newCh 各有两个头尾的变量 oldStartIndex、oldEndIndex 和 newStartIndex、newEndIndex,它们会新节点和旧节点会进行两两对比,即一共有4种比较方式:newStartIndex 和oldStartIndex 、newEndIndex 和 oldEndIndex 、newStartIndex 和 oldEndIndex 、newEndIndex 和 oldStartIndex,如果以上 4 种比较都没匹配,如果设置了key,就会用 key 再进行比较,在比较的过程中,遍历会往中间靠,一旦 StartIdx > EndIdx 表明 oldCh 和 newCh 至少有一个已经遍历完了,就会结束比较。 

所以 Vue 中 key 的作用是:key 是为 Vue 中 vnode 的唯一标记,通过这个 key,我们的 diff 操作可以更准确、更快速

  • 更准确:因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确。
  • 更快速:利用 key 的唯一性生成 map 对象来获取对应节点,比遍历方式更快,源码如下:
function createKeyToOldIdx (children, beginIdx, endIdx) {
  let i, key
  const map = {}
  for (i = beginIdx; i <= endIdx; ++i) {
    key = children[i].key
    if (isDef(key)) map[key] = i
  }
  return map
}

用index作为key可能会引发的问题

  (1)若对数据进行:逆序添加、逆序删除等破坏顺序操作:

      会产生没有必要的真实DOM更新 ==> 界面效果没问题,但效率低。

  (2)如果结构中还包含输入类的DOM

      会产生错误DOM更新 ==> 界面有问题。

开发中如何选择key?:

  (1)最好使用每条数据唯一表示作为key,比如id、手机号、身份证号、学号等唯一值。

  (2)如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,仅用于渲染列表用于展示,

      使用index作为key是没有问题的


简述 Vue 的基本原理

  • 当一个 Vue 实例创建的时候

  • vue 会遍历 data 选项的属性

  • Object.defineProperty(vue3.0 使用 proxy)将它们转为getter/setter

  • 并且在内部追踪相关依赖,在属性被访问和修改时通知变化

  • 每个组件实例都有相应的 watcher 程序实例

  • 它会在组件渲染的过程中把属性记录为依赖

  • 之后当依赖项的 setter 被调用的时候,会通知 watcher 重新计算,从而致使它关联的组件得以更新


vue2.x 为什么要求组件模板只能由一个根元素?

简单来说,"树状"数据结构,肯定要有"根",也就是一个遍历的起使点

通过这个根节点来递归遍历整个 vue 树下的所有节点并处理为 VDOM,最后渲染成真正的 HTML,插入在正确的位置,那么这个入口,就是这个树的"根",各个子元素,子组件,就是这个树的枝叶

从查找和遍历的角度来说,如果有很多根,那么我们的查找和遍历的效率会很低
其内只能有一个节点是因为当前构建和 diff VirtualDOM 的算法还未支撑这样的结构,也很难保证性能的情况下支撑

Vue 切换路由的时候,需要保存当时状态的功能,怎么实现呢?

一、beforeRouteLeave

beforeRouteLeave(to,from,next){
    if('用户已经输入了信息'){
           // 出现弹窗提醒保存草稿,或者自动后台为其保存
    // do something
    }else{
        next(true);// 用户离开
    }
}

这里可以把数据存储到vuex,缺点就是页面刷新数据会丢失

  • 存储到localStorage并且和vuex关联

  • 存储到数据库中

二、keep-alive

用keep-alive缓存路由


如何解决 vue 打包 vendor 过大的问题?webpack 打包 vue 速度慢怎么办?

解决 vue 打包 vendor 过大的问题

  • 使用 vue-router 路由懒加载
  • 使用 gzip 压缩
  • 使用 CDN 引入 js 和 css 文件
  • 配置 webpack 的 external,不打包第三方库
  • 配置 DllPlugin 和 DllReferencePlugin 将引用的依赖提取

webpack 打包 vue 速度慢

可以通过 webpack-bundle-analyzer 进行可视化分析,主要看依赖和 chunks 打包的时间。

  • 减少文件依赖嵌套的深度
  • 使用尽可能少的处理(loader、plugin)
  • DLL 处理第三方包
  • 多线程打包(HappyPack)
  • 关闭 sourcemap
  • 减少代码体积、压缩代码
  • 优化 resolve.extensions 配置
  • 优化 resolve.modules 配置
  • 优化 resolve.alias 配置
  • 使用 include 和 exclude
  • 设置 babel-loader 缓存

另外打包慢,是一个综合因素,和 vue 本身关系不大

  • 确保 webpack、npm、node 以及主要库版本更新,更新后的版本比更新前的版本要提升很多
  • loader 范围缩小到 src 项目文件,一些不必要的 loader 能关就关了
  • eslint 代码校验是一个很费时间的步骤
  • 可以把 eslint 范围缩小到 src,且只检查*.js、*.vue文件
  • 生产环境不开启 lint,使用 per-commit 或者 husky 在提交前校验

vue Hooks 有哪些?

什么是 Hooks

hooks 字面意思就是钩子函数,那么钩子函数的定义是什么呢?

钩子函数:在一个事件触发的时候,在系统级捕获到了它,然后做一些操作。一段用以处理系统消息的程序。钩子就是在某个阶段给你一个做某些处理操作的机会-----类似回调函数

钩子函数:

一个函数/方法,在系统消息触发时被系统调用,例如click等事件调用

不是用户自己触发的,例如发布订阅者模式的方法的实现


钩子函数的名称是确定的,当系统消息触发后,自动会调用

例如Vue的watch()函数,用户只需要编写watch()的函数体里面的函数,当页面元素发生变化的时候,系统就会先调用watch()

例如react的componentWillUpdate函数,用户只需要编写componentWillUpdate的函数体,当组件状态发生改变更新的时候,系统就会调用componentWillComponent

Vue Hooks 就是一些 vue 提供的内置函数,这些函数可以让 Function Component 和 Class Component 一样能够拥有组件状态(state)以及进行副作用(side effect)

为什么使用 Vue Hooks?

首先从 Class-component/Vue-options 开始说起

  • 跨组件代码难以复用
  • 大组件,维护困难,颗粒度不好控制,细粒度划分时,组件嵌套层次太深会影响性能
  • 类组件,this 不可控,逻辑分散,不容易理解
  • mixins 具有副作用,逻辑互相嵌套,数据来源不明,且不能互相消费

当一个模板依赖很多 mixin 的时候,很容易出现数据来源不清或者命名冲突的问题,而且开发 mixins 的时候,逻辑以及逻辑依赖的属性互相分散且 mixins 之间不可互相消费。这些都是开发中令人痛苦的点,因此 vue3.0 中引入 hooks 相关的特性非常明智

常用的 hooks 讲解

withHooks
const Foo = withHooks((h) => {
  // state
  const [count, setCount] = useState(0);

  // effect
  useEffect(() => {
    document.title = "count is " + count;
  });

  // 返回一个vnode
  return h("div", [
    h("span", `count is: ${count}`),
    h(
      "button",
      {
        on: {
          click: () => setCount(count + 1),
        },
      },
      "+"
    ),
  ]);
});

withHooks 是一个高阶函数,传入一个函数,这个函数内部返回一个 vnode,withHooks 方法返回的是一个 vue 的选项对象

Foo = {
  created() {},
  data() {},
  render() {},
};

这个选项对象可以直接调用 Vue.component 方法生成全局组件,或者在 render 方法中生成 vnode

useState

useState 理解起来很简单,和 Class Component 的 vuex 中 state 是一样的,都是用来管理组件状态的。因为 Function Component 每次执行的时候都会生成新的函数作用域所以统一组件的不同渲染(render)之间是不能够共用状态的,因此开发者一旦需要在组件中引入状态就需要将原来的 Funtion Component 改为 Class Component,这使得开发者的体验十分不好。useState 就是用来解决这个问题的,它允许 Function Component 将自己的状态持久化到 vue 运行时的某个地方,这样在组件每次渲染的时候都可以从这个地方拿到该状态,而且当该状态被更新的时候,组件也会重渲染

//声明
const [count, setcount] = useState(0)
const [state, setState] = useState({
    status: 'pending',
    data: null,
    error: null
})
const handleTextChange(value) => {
    setText({
        status: 'changed',
        data: value,
        error: null
    })
}
//引用
<div>{count}</div>
< ... onClick= setcount(count + 1) ... >
<div>{state}</div>
onChange=handleTextChange(count)

useState 接收一个 initial 变量作为状态的初始值,返回值是一个数组。返回数组的第一个元素代表当前 state 的最新值,第二个元素是一个用来更新 state 的函数。这里要注意的是 state 和 setState 这两个变量的命名不是固定的,应该根据你业务的实际情况选择不同的名字,可以是 setA 或 setB,需要注意的是 setState 这个是全量替代

我们在实际开发中,一个组件可能不止一个 state,如果组件有多个 state,则在组件内部多次调用 useState,这些使用类似 Vuex 里面的 state 的使用方式

useEffect

useEffect 用于添加组件状态更新后,需要执行的副作用逻辑

useEffect 指定的副作用逻辑,会在组件挂载后执行一次、在每次组件渲染后根据指定的依赖有选择地执行、并在组件卸载时执行清理事件的逻辑

import { withHooks, useState, useEffect } from "vue-hooks";

const Foo = withHooks((h) => {
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = "count is " + count;
  });
  return h("div", [
    h("span", `count is: ${count}`),
    h("button", { on: { click: () => setCount(count + 1) } }, "+"),
  ]);
});

代码中,通过 useEffect 时,每当 count 的状态值发生变化时,都会重置 document.title。这里没有指定 useEffect 的第二个参数 deps,表示只要组件重新渲染都会执行 useEffect 指定的逻辑,不限制必须是 count 变化时

useRef

useRef 是用来在组件不同渲染之间共用的一些数据的,它的作用和我们在 Vue Class Component 里面为$refs.xxx 赋值是一样的。那么它的一些特性就跟 refs 是类似:

  • 组件更新之后,可以获取最新的状态、值
  • 值不需要响应式处理
  • 独立于其他作用域之外,不污染其他作用域
  • useRef 返回的是对象
const [count, setcount] = useState(0);
const num = useRef(count);
const addCount = () => {
  let sum = count++;
  setcount(sum);
  num.current = sum;
  console.log(count, num.current);
};
//得到的结果是
// 0 1
// 1 2
// 2 3
// ...
useData

useData 可以理解为 Vue Class Funtion 里面的$data,也可以认为与 useState 类似。不同的是:useState 不提供更新器。只是作为数据变量的声明、修改、调用

//声明
const data = useData({
  count: 0,
});
//调用
console.log(data.count);
useMounted

useMounted 需要在 mounted 事件中执行的逻辑

useMounted(() => {
  console.log("mounted!");
});
useDestroyed

useDestroyed 需要在 destroyed 事件中执行的逻辑

useDestroyed(() => {
  console.log("destroyed!");
});

说一下你对 Vue 中 keep-alive 的理解,以及在使用过程中需要注意的地方?

keep-alive 是 vue 中内置的组件,用 keep-alive 包裹的组件,用来缓存和保留当时状态的组件实例,而不是销毁它们。主要是保存当时的组件状态和避免重复创建,避免重复渲染导致的性能问题。

特点

  • 它是一个抽象组件,本身不会渲染一个 dom 元素,也不会出现在组件中的父组件链上
  • 当组件在 keep-alive 内被切换的时候,组件内的 activated 和 deactivated 这两个生命周期钩子函数会被执行。组件一旦被缓存,再次渲染就不会执行 created、mounted 生命周期钩子函数
  • 要求同时只有一个子组件被渲染
  • 不会在函数式组件中正常工作,因为它们没有缓存实例

使用场景

<!-- 1 动态组件(所谓动态组件就是让多个组件使用同一个挂载点,并动态切换。) -->
<keep-alive>
  <component is="currentComponent"></component>
</keep-alive>
<!-- 2 多个条件判断的子组件 -->
<keep-alive>
  <comp-a v-if="true"></comp-a>
  <comp-b v-else></comp-b>
</keep-alive>
<!-- 3 transition -->
<transition>
  <keep-alive>
    <component is="currentComponent"></component>
  </keep-alive>
</transition>
<!-- 4 结合vue-router -->
<keep-alive>
  <router-view></router-view>
</keep-alive>

props

  • include 白名单 只有名称匹配的组件会被缓存
  • exclude 黑名单 任何匹配的组件都不会被缓存
  • max 最多缓存多少实例,一旦达到这个数字,新实例被创建之前,会销毁已缓存组件中最久没有被访问到的实例--- LRU 算法

include 和 exclude 首先检查组件的 name 属性,如果 name 不可用,则匹配局部注册名称,匿名组件不能被匹配

需要注意的地方

当引入 keep-alive 的时候,页面第一次进入,生命周期钩子的触发顺序:created --- mounted----activated;

退出时触发 deactivated;当再次进入的时候,只触发 activated

返回 dom 不让其重新刷新,只执行一次的放在 mounted 中;组件每次进去执行的方法放在 activated 中;

在 keep-alive 中直接添加 include,cachedViews----Array 类型:包含 vue 文件的组件 name 都将被缓存起来;反之 exclude 则是不会被缓存;exclude 的优先级大于 include,也就是说:当 include 和 exclude 同时存在的时候,exclude 会生效,include 不会生效的


Vuex 的 action 和 mutation 的特性是什么?有什么区别?

Action
  • 一些对 State 的异步操作可放在 Action 中,并通过在 Action 中 commit Mutation 变更状态
  • Action 可通过 store.dispatch() 方法触发,或者通过 mapActions 辅助函数将 vue 组件的 methods 映射成 store.dispatch()调用
Mutation
  • 在 vuex 的严格模式下,Mutaion 是 vuex 中改变 State 的唯一途径
  • Mutation 中只能是同步操作
  • 通过 store.commit () 调用 Mutation
总结

mutations 可以直接修改 state,但只能包含同步操作,同时,只能通过提交 commit 调用。actions 是用来触发 mutations 的,它无法直接改变 state,它可以包含异步操作,它只能通过 store.dispatch 触发


说一下路由钩子在 vue 生命周期的体现?

Vue-Router 导航守卫

有的时候,我们需要通过路由来进行一些操作,比如最常见的登录权限验证,当用户满足条件时,才让其进入导航,否则就取消跳转,并跳到登录页面让其登录。

为此我们有很多种方法可以植入路由的导航过程: 全局的,单个路由独享的,组件级的

全局路由钩子

vue router 全局有三个路由钩子:

  • router.beforeEach 全局前置守卫 进入路由之前
  • router.beforeResolve 全局解析守卫(2.5.0+) 在 beforeRouteEnter 调用之后调用
  • router.afterEach 全局后置钩子 进入路由之后

具体使用:

  • beforeEach (判断是否登录, 没登录就跳转到登录页)
router.beforeEach((to, from, next) => {
  let ifInfo = Vue.prototype.$common.getSession("userData"); // 判断是否登录的存储信息
  if (!ifInfo) {
    // sessionStorage里没有储存user信息
    if (to.path == "/") {
      //如果是登录页面路径,就直接next()
      next();
    } else {
      //不然就跳转到登录
      Message.warning("请重新登录!");
      window.location.href = Vue.prototype.$loginUrl;
    }
  } else {
    return next();
  }
});
  • afterEach(跳转之后滚动条返回顶部)
router.afterEach((to, from) => {
  // 跳转之后滚动条回到顶部
  window.scrollTo(0, 0);
});
单个路由独享钩子
  • beforeEnter

如果不想全局配置守卫的话,可以为某些路由单独配置守卫

export default [
  {
    path: "/",
    name: "login",
    component: login,
    beforeEnter: (to, from, next) => {
      console.log("即将进入登录页面");
      next();
    },
  },
];
组件内的钩子
  • beforeRouteEnter: 进入组件前触发
  • beforeRouteUpdate: 当前地址改变并且该组件被复用时触发,举例来说,带有动态参数的路径 list/:id,在/list/1 和/list/2 之间跳转的时候,由于会渲染同样的 list 组件,这个钩子在这种情况下就会被调用
  • beforeRouteLeave: 离开组件被调用

注意:beforeRouteEnter 组件内还访问不到 this,因为该守卫执行前组件实例还没有被创建,需要传一个回调给 next 来访问,比如:

beforeRouteEnter(to, from, next) {
    next(target => {
        if (from.path == '/classProcess') {
            target.isFromProcess = true
        }
    })
}

beforeRouteUpdate 和 beforeRouteLeave 可以访问组件实例 this


页面刷新后的vuex的state数据丢失怎么办?

原因分析

因为store里的数据是保存在运行内存中的,当页面刷新时,页面会重新加载vue实例,store里面的数据就会被重新赋值初始化

解决办法

  • 可以使用浏览器存储如localStorage、sessionStorage等,刷新后读取赋值。具体使用哪个可根据业务需要选择。
  • 对一些没有必要存储的请求接口数据,直接通过接口重新获取
  • 也可以使用一些插件,如vuex-persistedstate,可以自动存储数据
// 在点击页面刷新时先将state数据保存到sessionStorage
// beforeunload这个事件在页面刷新时先触发的
// 选择放在app.vue这个入口组件中,这样就可以保证每次刷新页面都可以触发
export default {
  name: 'App',
  created () {
    //在页面加载时读取sessionStorage里的状态信息
    if (sessionStorage.getItem("store") ) {
        this.$store.replaceState(Object.assign({}, this.$store.state,JSON.parse(sessionStorage.getItem("store"))))
    } 

    //在页面刷新时将vuex里的信息保存到sessionStorage里
    window.addEventListener("beforeunload",()=>{
        sessionStorage.setItem("store",JSON.stringify(this.$store.state))
    })
  }
}

完整的路由导航解析流程(不包括其他生命周期)

完整的路由导航解析流程(不包括其他生命周期)

  • 触发进入其他路由。
  • 调用要离开路由的组件守卫 beforeRouteLeave
  • 调用局前置守卫: beforeEach
  • 在重用的组件里调用 beforeRouteUpdate
  • 调用路由独享守卫 beforeEnter
  • 解析异步路由组件
  • 在将要进入的路由组件中调用 beforeRouteEnter
  • 调用全局解析守卫 beforeResolve
  • 导航被确认
  • 调用全局后置钩子的 afterEach 钩子
  • 触发 DOM 更新(mounted)
  • 执行 beforeRouteEnter 守卫中传给 next 的回调函数

触发钩子的完成顺序

路由导航、keep-alive、组件生命周期钩子结合起来的,假设是从 a 组件离开,第一次进入 b 组件,触发顺序:

  • beforeRouteLeave:路由组件的组件离开路由前钩子,可取消路由离开。
  • beforeEach:路由全局前置守卫,可用于登录验证、全局路由 Loading 等。
  • beforeEnter: 路由独享守卫
  • beforeRouteEnter: 路由组件的组件进入路由前钩子
  • beforeResolve:路由全局解析守卫
  • afterEach:路由全局后置钩子
  • beforeCreate 组件生命周期,不能访问 this
  • created:组件生命周期,可以访问 this, 不能访问dom
  • beforeMount:组件生命周期
  • deactivated: 离开缓存组件 a,或者触发 a 的beforeDestroy 和 destroyed 组件销毁钩子
  • mounted:访问或者操作 dom
  • activated:进入缓存组件,进入a的嵌套子组件(如果有的话)
  • 执行beforeRouteEnter回调函数next。

Vuex 怎么知道 state 是通过 mutation 修改还是外部直接修改的?

通过 $watch 监听 mutation 的 commit 函数中的 _committing 是否为 true

Vuex 中修改 state 的唯一渠道就是执行 commit('xx',payload) 方法, 其底层通过执行 this._ withCommit(fn) 设置_committing 标志变量为 true,然后才能修改 state,修改完毕还需要还原_committing 变量。外部修改虽然能够直接修改 state,但是并没有修改_committing 标志位,所以只要 watch 一下 state,state change 时判断_committing 值是否为 true,即可判断修改的合法性


vue 如何做权限校验?

接口权限控制 jwt

在用户登录成功之后,后台将返回一个token,之后前端每次进行接口请求的时候,都要带上这个token。后台拿到这个token后进行判断,如果此token确实存在并且没有过期,则可以通过访问。如果token不存在或后台判断已经过期,则会跳转到登录页面,要求用户重新登录获取token

页面控制权限

  • 实现页面访问权限又可分为以下两种方案:

    • 方案一:初始化即挂载全部路由,每次路由跳转前做校验
    • 方案二:只挂载当前用户拥有的路由,如果用户通过URL进行强制访问,则会直接进入404,相当于从源头上做了控制
  • 页面中的按钮(增、删、改、查)的权限控制是否显示

    • vue提供的自定义指令,实现按钮权限控制。

为什么要用 vuex 或者 Redux?

由于传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力.我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝.以上的这些模式非常脆弱,通常会导致代码无法维护

所以我们需要把组件的共享状态抽取出来,以一个全局单例模式管理.在这种模式下,我们的组件树构成了一个巨大的"视图",不管在树的哪个位置,任何组件都能获取状态或者触发行为

另外,通过定义和隔离状态管理中的各种概念并强制遵守一定的规则,我们的代码将会变得更结构化且易维护.


Redux和Vuex有什么区别,说一下它们的共同思想?

Redux和Vuex区别

  • Vuex改进了Redux中的Action和Reducer函数,以mutations变化函数取代Reducer,无需switch,只需在对应的mutation函数里改变state值就可以
  • Vuex由于Vue自动重新渲染的特性,无需订阅重新渲染函数,只要生成新的state就可以
  • Vuex数据流的顺序是:View调用store.commit提交对应的请求到Store中对应的mutation函数 -- store改变(vue检测到数据变化自动渲染)

通俗理解就是:Vuex弱化dispatch,通过commit进行store状态的一次变更;取消了action概念,不必传入特定的action形式进行指定变更;弱化reducer,基于commit参数直接对数据进行转变,使得框架更加建议

共同思想

  • 单一的数据源
  • 变化可以预测
  • 本质上:Redux和Vuex都是对MVVM思想的服务,将数据从视图中抽离的一种方案
  • 形式上:Vuex借鉴了Redux,将store作为全局的数据中心,进行数据管理

Vue-router history 模式部署的时候要注意什么?server 端用 nginx 和 node 时候分别怎么处理?

Vue Router

Vue-router 默认是 hash 模式,使用 url 的 hash 来模拟一个完整的 url,当 url 改变的时候,页面不会重新加载。比如:使用 hash 模式的时候,那么访问变成 http://localhost:8080/#page 这样的访问。但是如果路由使用 history 的话,那么访问的路径变成如下 http://localhost:8080/page/

需要注意的问题

如果使用的是 history 这种模式,在非首页情况下刷新页面或直接访问的时候就会返回 404,导致页面丢失。

这是因为利用 H5 history API 来实现的。通过 history.pushState 方法实现 URL 的跳转而无需重新加载页面。但是它的问题在于当刷新页面的时候会走后端路由,相当于直接在浏览器里输入这个地址,要对服务器发起 http 请求,但是这个目标在服务器上又不存在这个路由,所以会返回 404

解决方式:需要服务端的辅助来兜底,避免 URL 无法匹配到资源时能返回页面

Nginx 或者 node 作为服务端解决方式

Nginx配置

location / {
  try_files $uri $uri/ /index.html;
 }

node中间件配置

// 可以使用 connect-history-api-fallback 这个中间件
// npm install --save connect-history-api-fallback

// app.js
var history = require("connect-history-api-fallback");
var connect = require("connect");
var app = connect().use(history()).listen(3000);

// 或者使用 express
var express = require("express");
var app = express();
app.use(history());

使用过 Vue SSR 吗?说说 SSR?

Vue.js 是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出 Vue 组件,进行生成 DOM 和操作 DOM。然而,也可以将同一个组件渲染为服务端的 HTML 字符串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上完全可交互的应用程序。 即:SSR大致的意思就是vue在客户端将标签渲染成的整个 html 片段的工作在服务端完成,服务端形成的html 片段直接返回给客户端这个过程就叫做服务端渲染。

服务端渲染的优点:

  • 更好的 SEO: 因为 SPA 页面的内容是通过 Ajax 获取,而搜索引擎爬取工具并不会等待 Ajax 异步完成后再抓取页面内容,所以在 SPA 中是抓取不到页面通过 Ajax 获取到的内容;而 SSR 是直接由服务端返回已经渲染好的页面(数据已经包含在页面中),所以搜索引擎爬取工具可以抓取渲染好的页面

  • 更快的内容到达时间(首屏加载更快): SPA 会等待所有 Vue 编译后的 js 文件都下载完成后,才开始进行页面的渲染,文件下载等需要一定的时间等,所以首屏渲染需要一定的时间;SSR 直接由服务端渲染好页面直接返回显示,无需等待下载 js 文件及再去渲染等,所以 SSR 有更快的内容到达时间

服务端渲染的缺点:

  • 更多的开发条件限制:例如服务端渲染只支持 beforCreate 和 created 两个钩子函数,这会导致一些外部扩展库需要特殊处理,才能在服务端渲染应用程序中运行;并且与可以部署在任何静态文件服务器上的完全静态单页面应用程序 SPA 不同,服务端渲染应用程序,需要处于 Node.js server 运行环境;

  • 更多的服务器负载:在 Node.js 中渲染完整的应用程序,显然会比仅仅提供静态文件的 server 更加大量占用CPU 资源 (CPU-intensive - CPU 密集),因此如果你预料在高流量环境 ( high traffic ) 下使用,请准备相应的服务器负载,并明智地采用缓存策略。


说说Vue开发如何针对搜索引擎做SEO优化?

总所周知,Vue SPA单页面应用对SEO不友好,但是也有相应的解决方案

1. SSR服务器渲染

Vue.js是构建客户端应用程序的框架。默认的情况下,可以在浏览器中输出Vue组件,进行生成DOM和操作DOM。然而,也可以将同一个组件渲染为服务器端的html字符串,将它们直接发送到浏览器,最后将这些静态标记为"激活"为客户端上完全可交互的应用程序

服务器渲染的 Vue.js 应用程序也可以被认为是"同构"或"通用",因为应用程序的大部分代码都可以在服务器和客户端上运行。

权衡的地方:
    开发条件所限,浏览器特定的代码,只能在某些生命周期钩子函数中使用;一些外部扩展库可能需要特殊处理,才能在服务器渲染应用程序中运行
    环境和部署要求更高,需要node.js server 运行环境
    高流量的情况下,需要准备相应的服务器负载,并明智地采取缓存策略

优势:
    更好的seo,因为搜索引擎爬虫抓取工具可以直接查看完全渲染的页面
    更快的内容到达时间,特别是对于缓慢的网络情况或运行缓慢的设备

不足:
    一套代码两套执行环境,会引发各种问题,比如服务端没有window、document对象,处理方式是增加判断,如果是客户端才可以执行
    涉及构建设置和部署的更多要求,需要处于node server的运行环境
    更多的服务端负载

2. Nuxt 静态化

Nuxt是一个基于Vue生态的更高层的框架,为开发服务端渲染的Vue应用提供了极其便利的开发体验。更酷的是:可以用它来作为静态站生成器。

静态化是Nuxt.js打包的另一种方式,算是Nuxt.js的一个创新点,页面加载速度很快,需要注意的是:在Nuxt.js执行generate静态化打包时,动态路由会被忽略

优势:
    纯静态文件,访问速度超快
    对比SSR,不涉及到服务器负载方面问题
    静态网页不宜遭到黑客攻击,安全性更高
不足:
    如果动态路由参数多的话不适用

3. 预渲染 prerender-spa-plugin

如果你只是用来改善少数营销页面(例如/,/about/,/contact等)的seo,那么你可能需要预渲染。无需使用web服务器实时动态编译html,而是使用预渲染的方式,在构建时简单地生成针对特定路由地静态HTML文件。优点就是设置预渲染更简单,并可以将你的前端作为一个完全静态的站点

优势:
    改动小,引入插件配置即可
不足: 
    无法使用动态路由
    只适用少量页面的项目,页面多达几百个的情况下打包会非常慢

4. 使用Phantomjs针对爬虫做处理

Phantomjs 是基于一个 webkit内核的无头浏览器,即没有UI界面,即它就是一个浏览器,只是其内的点击、翻页等为人相关操作需要程序设计实现。虽然"Phantomjs 宣布终止开发",但是已经满足对Vue的SEO处理

这种解决方案其实是一种旁路机制

原理就是通过 nginx配置,判断访问的来源UA是否爬虫访问,如果是则就将搜索引擎的爬虫请求转发到一个 node server,在通过Phantomjs 来解析完整的html,返回给爬虫.

优势:
    完全不用改动项目代码,按照原本的spa开发即可,对比开发SSR成本小不要太多
    对已用SPA开发完成的项目,这是最好的选择
不足:
    部署需要node服务器支持
    爬虫访问比网页访问要慢一些,因为定时要定时资源加载完成才返回给爬虫
    如果被恶意模拟百度爬虫大量循环爬取,会造成服务器负载方面问题,解决方法是判断访问的ip,是否是百度官方爬虫的ip

总结

如果构建大型网站,比如商城类,直接上SSR服务器渲染;如果是个人博客、公司官网这类,其余三种都可以;如果对已用SPA开发完成的项目进行SEO优化,而且支持node服务器,请使用Phantomjs。

描述下自定义指令(vue部分)

自定义指令

在vue2.x中,代码复用和抽象的主要形式是组件。然而,有的情况下,你仍然需要对普通DOM元素进行底层操作,这时候就会用到自定义指令。

一般需要对DOM元素进行底层操作时使用,尽量只用来操作DOM展示,不修改内部的值。当使用自定义指令直接修改value值时绑定v-model的值也不会同步更新;

如必须修改可以在自定义指令中使用keydown事件,在vue组件中使用change事件,回调中修改vue数据;

自定义指令基本内容

  • 全局定义:Vue.directive("focus",{})
  • 局部定义:directives:{focus:{}}
  • 钩子函数:指令定义对象提供钩子函数
bind: 只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
inserted: 被绑定元素插入父节点时调用(仅保证父节点存在,但不一定已被插入文档中)。
update:所在组件的VNode更新时调用,但是可能发生在其子VNode更新之前调用。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新。
componentUpdate:指令所在组件的VNode及其子VNode全部更新后调用。
unbind:只调用一次,指令与元素解绑时调用。
  • 钩子函数参数
el:绑定元素
bing:指令核心对象,描述指令全部信息属性
  name  value  oldValue  expression  arg  modifers
vnode:虚拟节点
oldValue: 上一个虚拟节点(更新钩子函数中才有用)

使用场景

  • 普通DOM元素进行底层操作的时候,可以使用自定义指令。
  • 自定义指令是用来操作DOM的。尽管Vue推崇数据驱动视图的理念,但并非所有情况都适合数据驱动。自定义指令就是一种有效的补充和扩展,不仅可用于定义任何的DOM操作,并且是可复用的。

使用案例

初级使用:鼠标聚焦 下拉菜单 相对时间转换 滚动动画

高级应用:自定义指令实现图片懒加载 自定义指令集成第三方插件

v-model是如何实现的,语法糖实际是什么?

语法糖

指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。通常来说使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会。糖在不改变其所在位置的语法结构的前提下,实现了运行时的等价。可以简单理解为:加糖前后的代码编译后一样,但是代码更简洁流畅、代码更语义自然

实现原理

  1. 作用在普通表单元素上

动态绑定了inputvalue指向了message变量,并且在触发input事件的时候去动态把message设置为目标值

<input v-model="something"/>
//等同于
<input v-bind:value="message" v-on:input="message=$event.target.value"/>
//$event:当前触发的事件对象
//$event.target:当前触发的事件对象的dom
//$event.target.value:当前dom的value值
//在@input方法中,value-->something
//在:value中,something-->value
  1. 作用在组件上

在自定义组件中,v-model默认会利用名为value的prop和名为input的事件

本质是一个父子组件通信的语法糖,通过prop和$.emit来实现 因此父组件v-model语法糖本质上可以修改为<child :value="text" @input="function(e){text=e}"></child> 在组件的实现中,我们是可以通过v-model属性来配置子组件接收的prop属性以及派发的事件名称

//父组件
<parent-input v-model="parent"></parent-input>

//等价于
<parent-input v-bind:value="parent" v-on:input="parent=$event.target.value"></parent-input>

//子组件
<input v-bind:vaule="message" v-on:input="onmessage"/>
props:{value:message}
methods:{
    onmessage(e){
        $emit('input',e.target.value)
    }
}

默认情况下,一个组件上的v-model会把value用作prop并且把input用作event 但是一些输入类型比如单选框和复选框按钮可能想使用value prop来达到不同的目的。 使用model选项可以回避这个情况产生的冲突

js监听输入框输入数据的变化,用oninput事件,数据改变以后就会立刻触发这个事件 通过input事件把数据emit出去,在父组件接收父组件设置vmodel的值为inputemit出去,在父组件接收 父组件设置v-model的值为input emit获取过来的值

v-model 的缺点和解决办法

在创建类似复选框或者单选框的常见组件时,v-model 就不是很好玩啦

<input type="checkbox" v-model="something" />

v-model 给我们提供好 value 属性和 onInput 事件,但是,我们需要的不是 value 属性,而是 checked 属性,并且当你点击这个单选框的时候就不会触发 onInput 事件,它只会触发 onchange 事件。

因为 v-model 只是用到 input 元素上,所以这种情况很好解决:

<input type="checkbox" :checked="value" @change="change(value, $event)"

当 v-model 用到组件上时:

<checkbox v-model="value"></checkbox>

Vue.component('checkbox', {
  tempalte: '<input type="checkbox" @change="change" :checked="currentValue"/>'
  props: ['value'],
  data: function () {
        return {
            //这里为什么要定义一个局部变量,并用 prop 的值初始化它。
            currentValue: this.value
        };
    },
  methods: {
    change: function ($event) {
      this.currentValue = $event.target.checked;
      this.$emit('input', this.currentValue);
    }
})

在 vue2.2 版本,你可以在定义组件时通过 model 选项的方式来定义 prop/event


子组件可以直接改变父组件的数据么?说说你的理由?(vue部分)

结果

不可以

理由

主要是为了维护父子组件的单向数据流

每次父组件发生更新时,子组件中所有的prop都将会刷新为最新的值

如果这样做的话,Vue会在浏览器控制台中发出警告

Vue提倡单向数据流,即父级props的更新会流向子组件,但是反过来则不行。这是为了防止意外的改变父组件的状态,使得应用的数据流变得难以理解,导致数据流混乱。如果破环了单向数据流,当应用复杂情况时,debug的成本会非常高

只有通过$emit派发一个自定义事件,父组件接收后,由父组件修改


怎样理解 Vue 的单向数据流?

所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。

额外的,每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告。子组件想修改时,只能通过 $emit 派发一个自定义事件,父组件接收到后,由父组件修改。

有两种常见的试图改变一个 prop 的情形:
  • 这个 prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用。 在这种情况下,最好定义一个本地的 data 属性并将这个 prop 用作其初始值:
props: ['initialCounter'],
data: function () {
  return {
    counter: this.initialCounter
  }
}
  • 这个 prop 以一种原始的值传入且需要进行转换。 在这种情况下,最好使用这个 prop 的值来定义一个计算属性
props: ['size'],
computed: {
  normalizedSize: function () {
    return this.size.trim().toLowerCase()
  }
}

说一下Vue单页与多页的区别?

定义

SPA单页面应用(SinglePage Web Application),指只有一个主页面的应用,一开始只需要加载一次js、css等相关资源。所有的内容都包含在主页面,对每一个功能模块组件化。单页应用跳转,就是切换相关组件,仅仅刷新局部资源。

MPA多页面应用(MultiPage Application),指有多个独立页面的应用,每个页面必须重复加载js、css等相关资源。多页应用跳转,需要整页资源刷新。

区别

1. 刷新方式
  • SPA: 相关组件切换,页面局部刷新或更改
  • MPA: 整页刷新
2. 路由模式
  • SPA:可以使用hash也可以使用history
  • MPA:普通链接跳转
3. 用户体验
  • SPA:页面片段切换的时间短,用户体验良好,第一次加载文件过多需要相关优化
  • MPA:页面切换加载缓慢,流畅度不够,用户体验比较差,尤其网速慢的时候
4. 转场动画
  • SPA: 容易实现转场动画
  • MPA: 无法实现转场动画
5. 数据传递
  • SPA: 容易实现数据的传递,方法有很多(通过路由带参数传值,Vuex传值等等)
  • MPA: 依赖url传参,cookie,本地存储等
6. 搜索引擎优化(SEO)
  • SPA: 需要单独方案,实现较为困难,不利于SEO检索,可利用服务端渲染(SSR)优化
  • MPA: 适用于追求高度支持搜索引擎的应用
7. 使用范围
  • SPA: 高要求的体验度,追求界面流畅的应用
  • MPA: 适用于追求高度支持搜索引擎的应用
8. 开发成本
  • SPA: 较高,需要借助专业的框架
  • MPA: 较低,页面代码重复的比较多
9. 维护成本
  • SPA: 相对容易
  • MPA: 相对复杂
10.结构
  • SPA: 一个主页面+许多模块的组件
  • MPA: 许多完整的页面
11.资源文件
  • SPA: 组件公用的资源只需要加载一次
  • MPA: 每个页面都需要自己加载公用的资源

Vue-cli默认是单页面的,如果要开发多页面应该怎么办?

单页应用(SPA)往往只含有一个主入口文件与index.html,页面间切换通过局部刷新资源来完成。而在多页应用中,我们会为每个html文档文件都指定好一个JS入口,这样一来当页面跳转时用户会获得一个新的html文档,整个页面会重新加载。

vue-cli可以配置vue.config.jspages选项,实现多页面应用开发


说一下Vue 的父组件和子组件生命周期钩子函数执行顺序?

Vue 的父组件和子组件生命周期钩子函数执行顺序可以归类为以下 4 部分:

加载、更新、销毁

加载渲染过程

父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted

子组件更新过程

父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated

父组件更新过程

父 beforeUpdate -> 父 updated

销毁过程

父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed


说一下Vue的生命周期以及每个阶段做的事情

beforeCreate(创建前)

在数据观测和初始化事件还没有开始

created(创建后)

完成数据观测,属性和方法的运算,初始化事件,$el属性还没有显示出来

beforeMounted(挂载前)

在挂载开始之前被调用,相关的render函数首次被调用。实例已完成下面的配置:编译模板,把data里面的数据和模板生成html。此时还没有挂载html到页面上

mounted(挂载后)

在el被新创建的vm.$el替换,并挂载到实例上去之后调用。实例已经完成下面的配置:用上面编译好的html内容替换el属性指向的DOM对象。完成模板中的html渲染到html页面中,此过程中可进行ajax交互

beforeUpdate(更新前)

在数据更新之前调用,发生在虚拟dom重新渲染和打补丁之前。可以在该钩子中进一步更改状态,不会触发重复渲染过程

update(更新后)

在由于数据更改导致的虚拟DOM重新渲染和打补丁之后调用。调用时,组件dom已经更新,所以可以执行依赖于dom操作。但是在大多数情况下,在此期间避免更改状态,因为这可能会导致更新无法循环。此钩子在服务端渲染期间不被调用

beforeDestroy(销毁前)

在实例销毁之前调用。实例仍然完全可以用。

destroyed(销毁后)

在实例销毁之后调用。调用后,所有的事件监听器会被移除,所有的子实例也会被销毁。此钩子在服务端渲染期间不被调用


在哪个生命周期内调用异步请求

可以在钩子函数 created、beforeMount、mounted 中进行调用,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。

但是建议在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:

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

在 Vue 中父组件可以监听到子组件的生命周期么?

可以监听

  • on 和 emit
  • 使用hook钩子函数

使用 on 和 emit

// Parent.vue
<Child @mounted="doSomething"/>
// Child.vue
mounted() {
  this.$emit("mounted");
}

使用 hook 钩子函数

//  Parent.vue
<Child @hook:mounted="doSomething" ></Child>

doSomething() {
   console.log('父组件监听到 mounted 钩子函数 ...');
},
//  Child.vue
mounted(){
   console.log('子组件触发 mounted 钩子函数 ...');
},
// 以上输出顺序为:
// 子组件触发 mounted 钩子函数 ...
// 父组件监听到 mounted 钩子函数 ...

Vue 组件间通信有哪几种方式?

Vue 组件间通信主要指以下 3 类通信:父子组件通信隔代组件通信兄弟组件通信,下面我们分别介绍每种通信方式:

  1. props / $emit 适用 父子组件通信 这种方法是 Vue 组件的基础,相信大部分同学耳闻能详,所以此处就不举例展开介绍。

  2. ref 与 $parent / $children 适用 父子组件通信

    • ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例

    • $parent/$children:访问父 / 子实例

  3. EventBus($emit/$on)适用于 父子、隔代、兄弟组件通信 这种方法通过一个空的 Vue 实例作为中央事件总线(事件中心),用它来触发事件和监听事件,从而实现任何组件间的通信,包括父子、隔代、兄弟组件。

  4. $attrs/$listeners 适用于 隔代组件通信

    • $attrs: 包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 ( class 和 style 除外 )。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 ( class 和 style 除外 ),并且可以通过v-bind="$attrs"传入内部组件。通常配合inheritAttrs选项一起使用。

    • $listeners: 包含了父作用域中的 (不含 .native 修饰器的) v-on事件监听器。它可以通过v-on="$listeners"传入内部组件

  5. provide / inject 适用于 隔代组件通信 祖先组件中通过 provider 来提供变量,然后在子孙组件中通过 inject 来注入变量。 provide / inject API 主要解决了跨级组件间的通信问题,不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系。

  6. Vuex 适用于 父子、隔代、兄弟组件通信。Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )。

    • Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
    • 改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。

直接给一个数组项赋值,Vue 能检测到变化吗

由于 JavaScript 的限制,Vue 不能检测到以下数组的变动:

  • 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
  • 当你修改数组的长度时,例如:vm.items.length = newLength

为了解决第一个问题,Vue 提供了以下操作方法:

// Vue.set
Vue.set(vm.items, indexOfItem, newValue)
// vm.$set,Vue.set的一个别名
vm.$set(vm.items, indexOfItem, newValue)
// Array.prototype.splice
vm.items.splice(indexOfItem, 1, newValue)

为了解决第二个问题,Vue 提供了以下操作方法:

// Array.prototype.splice
vm.items.splice(newLength)

为什么组件中的 data 必须是一个函数,然后 return 一个对象,而 new Vue 实例里,data 可以直接是一个对象?

为什么根实例的data是一个对象?

new Vue()中只有一个data属性,共用该data。

为什么组件中的data必须是一个函数?

  • 因为如果data是一个对象,对象是引用类型,那复用的所有组件实例都会共享这些数据,就会导致修改一个组件实例上的数据,其他复用该组件的实例上对应的数据也会被修改

  • 如果data是一个函数,函数虽然也是引用类型,但是函数是有作用域的,函数内的变量不能被外部访问到,这样每个组件实例都会有个独立的拷贝同时又因为函数作用域的限制修改自己的数据时其他组件实例的数据是不会受到影响的

总结:

对象是引用类型,且没有作用域,会导致一改全改;

函数是引用类型,但它有作用域,不会彼此受牵连。


关于对 Vue 项目进行优化,你有哪些方法?

1.代码层面的优化

  • v-if 和 v-show 区分使用场景
  • computed 和 watch 区分使用场景
  • v-for 遍历必须为 item 添加 key,且避免同时使用 v-if
  • 长列表性能优化
  • 事件的销毁
  • 图片资源懒加载
  • 路由懒加载
  • 第三方插件的按需引入
  • 优化无限列表性能
  • 服务端渲染 SSR or 预渲染

2.Webpack 层面的优化

  • Webpack 对图片进行压缩
  • 减少 ES6 转为 ES5 的冗余代码
  • 提取公共代码
  • 模板预编译
  • 提取组件的 CSS
  • 优化 SourceMap
  • 构建结果输出分析
  • Vue 项目的编译优化

3.基础的 Web 技术的优化

  • 开启 gzip 压缩
  • 浏览器缓存
  • CDN 的使用
  • 使用 Chrome Performance 查找性能瓶颈

说一下Vue的keep-alive是如何实现的,具体缓存的是什么?

keep-alive 用法

  • include字符串或正则表达式,只有名称匹配的组件会被匹配
  • exclude字符串或正则表达式。任何名称匹配的组件都不会被缓存
  • max数字。最多可以缓存多少组件实例
  • keep-alive 包裹动态组件时,会缓存不活动的组件实例

主要流程

  • 1.判断组件name,不在include或者在exclude中,直接返回vnode,说明该组件不被缓存
  • 2.获取组件实例key,如果由获取实例的key,否则重新生成。
  • 3.key生成规则,cid+"::"+tag,仅靠cid是不够的,因为相同的构造函数可以注册为不同的本地组件
  • 4.如果缓存对象内存在,则直接从缓存对象中获取组件实例给vnode,不存在则添加到缓存对象中
  • 5.最大缓存数量,当缓存数量超过max值时,清楚keys数组内的第一个组件

keep-alive 的实现

const patternTypes: Array<Function> = [String,RegExp,Array]//接收:字符串、正则、数组
export default{
    name: 'keep-alive',
    abstract: true, // 一个抽象组件,自身不会渲染一个DOM元素,也不会出现在父组件中
    props:{
        include: patternTypes,//匹配的组件,缓存
        exclude: patternTypes,//不去匹配的组件,你缓存
        max: [String,Number],//缓存组件的最大实例数量,由于缓存的是组件实例(vnode),数量过多的时候,会占用过多的内存,可以用max指定上限
    },
    create(){
        //用于初始化缓存虚拟DOM数组和vnode的key
        this.cache=Object.create(null)
        this.keys=[]
    },
    destroyed(){
        //销毁缓存cache的组件实例
        for(const key in this.cache){
            pruneCacheEntry(this.cache,key,this.keys)
        }
    },
    mounted(){
        //监控include和exclude的改变,根据最新的include和exclude的内容,来实时削减缓存的组建的内容
        this.$watch('include',(val)=>{
            pruneCache(this,(name=>matches(val,name)))
        })
        this.$watch('exclude',(val)=>{
            pruneCache(this,(name)=>!matches(val,name))
        })
    },
}
  • render函数
  1. 会在keep-alive组件内部去写自己的内容,所以可以去获取默认slot的内容,然后根据这个去获取组件
  2. keep-alive只对第一个组件有效,所以获取第一个子组件
  3. 和keep-alive搭配使用的一般由动态组件route-view
render(){
    function getFirstComponentChild(children:? Array<VNode>) :? VNode{
        if(Array.isArray(children)){
            for(var i=0;i< children.length;i++){
                const c=children[i]
                if(isDef(c)&&isDef(c.componentOptions)||isAsyncPlaceholder(c)){
                    return c
                }
            }
        }
        const slot= this.$slots.default//获取默认插槽
        const vnode: VNode = getFirstComponentChild(slot)//获取第一个子组件
        const componentOptions:?VNodeComponentOptions=vnode && vnode.componentOptions//组件参数
        if(componentOptions){//是否有组件参数
            const name:?string=getComponentName(componentOptions)//获得组件名字
            const {include,exclude}=this
            if(
                //not include
                (include && (!name || !matches(include,name))) ||
                //excluded
                (exclude && name && matches(exclude,name))
            ){
                //如果不匹配当前组件的名字和include以及exclude
                //那么直接返回组件的实例
                return vnode
            }
            const {cache,keys}=this
            //获取这个组件的key
            const key:?string=vnode.key == null?componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}`:''):vnode.key
            if(cache[key]){
                //LRU缓存策略执行
                vnode.componentInstance=cache[key].componentInstance//组件初次渲染的时候componentInstance为undefined
                remove(keys,key)
                keys.push(key)
                //根据LRU缓存策略进行,将key从原来的位置移除,然后将这个key值放到最后
            }else{
                //在缓存列表里没有的话,则加入,同时判断当前加入之后,是否超过了max所设定的范围,是的话那么就移除
                //使用时间间隔最长的一个
                cache[key]=vnode
                keys.push(key)
                if(this.max && keys.length >parseInt(this.max)){
                    pruneCacheEntry(cache,key[0],keys,this._vnode)
                }
            }
            //将组件的keepAlive属性设置为true
            vnode.data.keepAlive=true//判断是否执行组件的created、mounted生命周期函数
        }
    }
    return vnode||(slot && slot[0])
}

keep-alive 具体是通过cache数组缓存所有组件的vnode实例。当cache内原有组件被使用时会将该组件key从keys数组中删除,然后push到keys数组最后面,方便清除最不常用组件

  • 步骤总结
  • 1.获取keep-alive下第一个子组件的实例对象,通过它去获取这个组件的名字
  • 2.通过当前组件名去匹配原来include和exclude,判断当前组件是否需要缓存,不需要缓存直接返回当前组件的实例vnode
  • 3.需要缓存,判断它当前是否在缓存数组里面,存在的话就将它原来的位置上的key给移除,同时将这个组件的key放到数组最后面
  • 4.不存在的话,将组件key放入数组,然后判断当前key数组是否超过max所设置的范围,超过的话那就削减没使用时间最长的一个组件的key值
  • 5.最后将这个组件的keepAlive设置为true

keep-alive本身的创建过程和patch过程

缓存渲染的时候,会根据vnode.componentInstance(首次渲染 vnode.componentInstance为undefined)和keepAlive属性判断不会执行组件的created、mounted等钩子函数,而是对缓存的组件执行patch过程:直接把缓存的DOM对象直接插入到目标元素中,完成了数据更新情况下的渲染过程

  • 首次渲染 组件的首次渲染:判断组件的abstract属性,才往父组件里面挂载DOM
function initLifecycle(vm:Component){
    const options=vm.$options
    let parent=options.parent
    if(parent && !options.abstract){//判断组件的abstract属性,才往父组件里面挂载DOM
        while(parent.$options.abstract && parent.$parent){
            parent=parent.$parent
        }
        parent.$children.push(vm)
    }
    vm.$parent=parent
    vm.$root=parent?parent:$root:vm
    vm.$children=[]
    vm.$refs={}
    vm._watcher=null
    vm._inactive=null
    vm._directInactive=false
    vm._isMounted=false
    vm._isDestroyed=false
    vm._isBeingDestoryed=false
}

判断当前keepAlive和componentInstance是否存在来判断是否要执行组件perpatch还是执行创建componentInstance

init(vnode:VNodeWithData,hydrating:boolean):?boolean{
    if(vnode.componentInstance && !vnode.componentInstance.__isDestroyed && vnode.data.keepAlive){//首次渲染 vnode.componentInstance为undefined
        const mounteNode:any=vnode
        componentVNodeHooks.prepatch(mountedNode,mountedNode)//prepatch函数执行的是组件更新的过程
    }else{
        const child=vnode.componentInstance=createComponentInstanceForVnode(vnode,activeInstance)
    }
    child.$mount(hydrating?vode.elm:undefined,hydrating)
}

prepatch操作就不会在执行组件的mounted和created声明周期函数,而是直接将DOM插入

LRU(least recently used)缓存策略

LRU缓存策略: 从内存找出最久未使用的数据并置换新的数据 LRU(least recently used) 算法根据数据的历史访问记录来进行淘汰数据,其核心思想是:如果数据最近被访问过,那么将来被访问的几率也更高。最常见的实现是使用一个链表保存缓存数据,详细算法实现如下

1.新数据插入到链表头部
2.每当缓存数据被访问,则将数据移到链表头部
3.链表满的时候,将链表尾部的数据丢弃

计算属性和普通属性的区别是什么?

computed属性是vue计算属性,是数据层到视图层的数据转化映射;计算属性是基于他们的依赖进行缓存的,只有在相关依赖发生改变时,他们才会重新求值,也就是说,只要他的依赖没有发生变化,那么每次访问的时候计算属性都会立即返回之前的计算结果,不再执行函数

  • computed是响应式的,methods并非响应式。
  • 调用方式不一样,computed的定义成员像属性一样访问,methods定义的成员必须以函数形式调用
  • computed是带缓存的,只有依赖数据发生改变,才会重新进行计算,而methods里的函数在每次调用时都要执行。
  • computed中的成员可以只定义一个函数作为只读属性,也可以定义get/set变成可读写属性,这点是methods中的成员做不到的
  • computed不支持异步,当computed内有异步操作时无效,无法监听数据的变化

如果声明的计算属性计算量非常大的时候,而且访问量次数非常多,改变的时机却很小,那就需要用到computed;缓存会让我们减少很多计算量。


说一下 vm.$set 原理

vm.$set()解决的问题是什么?

在 Vue 里面只有 data 中已经存在的属性才会被 Observe 为响应式数据,如果你是新增的属性是不会成为响应式数据,因此 Vue 提供了一个 api---vm.$set----来解决这个问题

<!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>Vue</title>
   <script src="https://cdn.jsdelivr.net/npm/vue@2"></script>
  </head>
  <body>
    <div id="app">
      {{user.name}} {{user.age}}
      <button @click="addUserAgeField">增加一个年纪字段</button>
    </div>
    <script>
      const app = new Vue({
        el: "#app",
        data: {
          user: {
            name: "test",
          },
        },
        mounted() {},
        methods: {
          addUserAgeField() {
            // this.user.age = 20 这样是不起作用, 不会被Observer
            this.$set(this.user, "age", 20); // 应该使用
          },
        },
      });
    </script>
  </body>
</html>

原理

vm.$set()在 new Vue()的时候被注入到 Vue 的原型上

import { initMixin } from "./init";
import { stateMixin } from "./state";
import { renderMixin } from "./render";
import { eventsMixin } from "./events";
import { lifecycleMixin } from "./lifecycle";
import { warn } from "../util/index";

function Vue(options) {
  if (process.env.NODE_ENV !== "production" && !(this instanceof Vue)) {
    warn("Vue is a constructor and should be called with the `new` keyword");
  }
  this._init(options);
}

initMixin(Vue);
// 给原型绑定代理属性$props, $data
// 给Vue原型绑定三个实例方法: vm.$watch,vm.$set,vm.$delete
stateMixin(Vue);
// 给Vue原型绑定事件相关的实例方法: vm.$on, vm.$once ,vm.$off , vm.$emit
eventsMixin(Vue);
// 给Vue原型绑定生命周期相关的实例方法: vm.$forceUpdate, vm.destroy, 以及私有方法_update
lifecycleMixin(Vue);
// 给Vue原型绑定生命周期相关的实例方法: vm.$nextTick, 以及私有方法_render, 以及一堆工具方法
renderMixin(Vue);

export default Vue;

stateMixin()应用

Vue.prototype.$set = set;
Vue.prototype.$delete = del;

set()函数

export function set(target: Array<any> | Object, key: any, val: any): any {
  // 1.类型判断
  // 如果 set 函数的第一个参数是 undefined 或 null 或者是原始类型值,那么在非生产环境下会打印警告信息
  // 这个api本来就是给对象与数组使用的
  if (
    process.env.NODE_ENV !== "production" &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(
      `Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`
    );
  }
  // 2.数组处理
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    // 类似$vm.set(vm.$data.arr, 0, 3)
    // 修改数组的长度, 避免索引大于数组长度导致splcie()执行有误
    //如果不设置length,splice时,超过原本数量的index则不会添加空白项
    target.length = Math.max(target.length, key);
    // 利用数组的splice变异方法触发响应式, 这个前面讲过
    target.splice(key, 1, val);
    return val;
  }
  //3.对象,并且key不是原型上的属性处理
  // target为对象, key在target或者target.prototype上。
  // 同时必须不能在 Object.prototype 上
  // 直接修改即可, 有兴趣可以看issue: https://github.com/vuejs/vue/issues/6845
  if (key in target && !(key in Object.prototype)) {
    target[key] = val;
    return val;
  }
  // 以上都不成立, 即开始给target创建一个全新的属性
  // 获取Observer实例
  const ob = (target: any).__ob__;
  // Vue 实例对象拥有 _isVue 属性, 即不允许给Vue 实例对象添加属性
  // 也不允许Vue.set/$set 函数为根数据对象(vm.$data)添加属性
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== "production" &&
      warn(
        "Avoid adding reactive properties to a Vue instance or its root $data " +
          "at runtime - declare it upfront in the data option."
      );
    return val;
  }
  //5.target是非响应式数据时
  // target本身就不是响应式数据, 直接赋值
  if (!ob) {
    target[key] = val;
    return val;
  }
  //6.target对象是响应式数据时
  //定义响应式对象
  defineReactive(ob.value, key, val);
  //watcher执行
  ob.dep.notify();
  return val;
}

工具函数的方法

// 判断给定变量是否是未定义,当变量值为 null时,也会认为其是未定义
export function isUndef(v: any): boolean %checks {
  return v === undefined || v === null;
}

// 判断给定变量是否是原始类型值
export function isPrimitive(value: any): boolean %checks {
  return (
    typeof value === "string" ||
    typeof value === "number" ||
    // $flow-disable-line
    typeof value === "symbol" ||
    typeof value === "boolean"
  );
}

// 判断给定变量的值是否是有效的数组索引
export function isValidArrayIndex(val: any): boolean {
  const n = parseFloat(String(val));
  return n >= 0 && Math.floor(n) === n && isFinite(val);
}

ob && ob.vmCount 的应用

export function observe(value: any, asRootData: ?boolean): Observer | void {
  // 省略...
  if (asRootData && ob) {
    // vue已经被Observer了,并且是根数据对象, vmCount才会++
    ob.vmCount++;
  }
  return ob;
}

在初始化 Vue 的过程中

export function initState(vm: Component) {
  vm._watchers = [];
  const opts = vm.$options;
  if (opts.props) initProps(vm, opts.props);
  if (opts.methods) initMethods(vm, opts.methods);
  if (opts.data) {
    //opts.data为对象属性
    initData(vm);
  } else {
    observe((vm._data = {}), true /* asRootData */);
  }
  if (opts.computed) initComputed(vm, opts.computed);
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch);
  }
}

initData(vm)

function initData(vm: Component) {
  let data = vm.$options.data;
  data = vm._data = typeof data === "function" ? getData(data, vm) : data || {};

  // 省略...

  // observe data
  observe(data, true /* asRootData */);
}

总结

vm.$set(target,key,value)

  • 开始判断类型
  • 当 target 为数组时,直接调用数组方法 splice 实现
  • 如果目标是对象,会先判断属性是否存在、对象是否为响应式
  • 最终结果要对属性进行响应式处理,则是通过调用defineRective方法进行响应式处理
  • definedReactive 方法就是 Vue 在初始化对象时,给对象属性采用Object.defineProperty动态添加 getter 和 setter 的功能所调用的方法