关于vue的所有面试题

# 1 请说一下响应式数据的理解?
响应式数据分为两种,对象和数组。

  1. 对象

对象内部通过defineReactive方法,该方法是使用Object.defineProperty将属性进行劫持(只会劫持已经存在的属性)。
比如多层对象是通过递归来实现劫持。(Vue3 是用proxy来实现的)

  1. 数组

数组则是通过重写数组方法来实现。创建一个新的原型对象,该对象继承自Array.prototype,新对象在对数组的方法进行改写。

  • 引申

(1)对象层级过深,性能就会差 (2)不需要响应数据的内容不要放到data中 (3) Object.freeze() 可以冻结数据

2 Vue如何检测数组变化?

Vue在observer数据阶段会判断如果是数组的话,则修改数组的原型,这样的话,后面对数组的任何操作都可以在劫持的过程中控制。

const arrayProto = Array.prototype//原生Array的原型
export const arrayMethods = Object.create(arrayProto);
[
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
].forEach(function (method) {
  const original = arrayProto[method]//缓存元素数组原型
  //这里重写了数组的几个原型方法
  def(arrayMethods, method, function mutator () {
    //这里备份一份参数应该是从性能方面的考虑
    let i = arguments.length
    const args = new Array(i)
    while (i--) {
      args[i] = arguments[i]
    }
    const result = original.apply(this, args)//原始方法求值
    const ob = this.__ob__//这里this.__ob__指向的是数据的Observer
    let inserted
    switch (method) {
      case 'push':
        inserted = args
        break
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    return result
  })
})

//定义属性
function def (obj, key, val, enumerable) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  });
}

*引申
在Vue中修改数组的索引和长度是无法监控到的。需要通过以上7种变异方法修改数组才会触发数组对应的watcher进行更新。数组中如果是对象数据类型也会进行递归劫持。那如果想更改索引更新数据怎么办?可以通过Vue.$set()来进行处理 =》 核心内部用的是splice方法

3 Vue中模板编译原理?

1 先查找是否有render函数,没有的话看是否传入了template,如果传入了将template转化为render函数,如果仍没有,会回去el的outerHtml模板内容,将其转化成render函数。总之是转化成render函数。
2 字符串模板转化成为render函数的过程:用正则解析模板字符串生成AST语法树。将生成的ast语法树拼接成字符串,其中包含了 _c,_v,_s等方法描述元素的节点,文本节点以及变量。然后把拼接好的字符串通过new Function(with(this){return ${code}})转化成render函数

4 生命周期钩子是如何实现的?

1 通过Vue.mixin定义全局生命钩子,Vue.mixin可调用多次,会定义不同的合并策略,生命周期钩子的合并策略就是为每个钩子函数创建一个数组,每次定义的钩子函数保存在对应的数组中,并最终挂载到Vue.$options 上
2 Vue实例初始化会将Vue.$options上的全局钩子函数和实例的钩子函数合并,挂载到options上,最后在实例的不同阶段 通过callHook函数从$options中取出对应的钩子函数数组,遍历数组,依次执行生命周期函数。

  • Vue的生命周期钩子就是回调函数而已,核心是一个发布订阅模式

5 Vue.mixin的使用场景和原理

Vue.mixin的作用就是抽离公共的业务逻辑,原理类似“对象的继承”,当组件初始化时会调用mergeOptions方法进行合并,采用策略模式针对不同的属性进行合并。如果混入的数据和本身组件中的数据冲突,会采用“就近原则”以组件的数据为准。

6 nextTick在哪里使用?原理是?

nextTick中的回调是在下次 DOM 更新循环结束之后执行的延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。原理就是异步方法(promise,mutationObserver,setImmediate,setTimeout)经常与事件环一起来问(宏任务和微任务)

7 Vue为什么需要虚拟DOM?

Virtual DOM就是用js对象来描述真实DOM,是对真实DOM的抽象,由于直接操作DOM性能低但是js层的操作效率高,可以将DOM操作转化成对象操作,最终通过diff算法比对差异进行更新DOM(减少了对真实DOM的操作)。虚拟DOM不依赖真实平台环境从而也可以实现跨平台。本质上就是在JS和DOM之间的一个缓存。

8 Vue中的diff原理

  • 1.先比较是否是相同节点
  • 2.相同节点比较属性,并复用老节点
  • 3.比较儿子节点,考虑老节点和新节点儿子的情况
  • 4.优化比较:头头、尾尾、头尾、尾头
  • 5.比对查找进行复用

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

响应式数据变化,Vue确实可以在数据发生变化时,响应式系统可以立刻得知。但是如果给每个属性都添加watcher用于更新的话,会产生大量的watcher从而降低性能。而且粒度过细也会导致更新不精准的问题,所以vue采用了组件级的watcher配合diff来检测差异。这里可以在讲一下diff的原理

10 Vue中computed和watch的区别

computed和watch都是基于Watcher来实现的,分别是计算属性watcher和用户watcher。computed属性是具备缓存的,依赖的值不发生变化,对其取值时计算属性方法不会重新执行(可以用模板渲染,取值的过程中不支持异步方法)watch则是监控值的变化,当值发生变化时调用对应的回调函数。computed不会立即执行,内部通过defineProperty进行定义。并且通过dirty属性来检测依赖的数据是否发生变化。watch则是立即执行将老值保存在watcher上,当数据更新时重新计算新值,将新值和老值传递到回调函数中。

11 Vue.set方法是如何实现的?

我们给对象和数组本身都增加了dep属性。当给对象新增不存在的属性则触发对象依赖的watcher去更新,当修改数组索引时我们调用数组本身的splice方法去更新数组

12 Vue的生命周期方法有哪些?一般在哪一步发起请求及原因

  • beforeCreate 在实例初始化之后,数据观测(data observer) 和 event/watcher 事件配置之前被调用。
  • created 实例已经创建完成之后被调用。在这一步,实例已完成以下的配置:数据观测(data observer),属性和方法的运算, watch/event 事件回调。这里没有$el
  • beforeMount 在挂载开始之前被调用:相关的 render 函数首次被调用。
  • mounted el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子。
  • beforeUpdate 数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。
  • updated 由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。
  • beforeDestroy 实例销毁之前调用。在这一步,实例仍然完全可用。
  • destroyed Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。 该钩子在服务器端渲染期间不被调用。

    ♥钩子函数的作用

  • created 实例已经创建完成,因为它是最早触发的原因可以进行一些数据,资源的请求。(服务端渲染支持created方法)
  • mounted 实例已经挂载完成,可以进行一些DOM操作
  • beforeUpdate 可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程。
  • updated 可以执行依赖于 DOM 的操作。然而在大多数情况下,你应该避免在此期间更改状态,因为这可能会导致更新无限循环。 该钩子在服务器端渲染期间不被调用。
  • destroyed 可以执行一些优化操作,清空定时器,解除绑定事件

♥在哪发送请求都可以,主要看具体你要做什么事

13 vue-router有几种钩子函数?分别用在什么地方

1 全局钩子函数

  • router.beforeEach
  • router.beforeResolve (这和 router.beforeEach 类似,区别是在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用。
  • router.afterEach

2 路由独享的守卫

beforeEnter

const router = new VueRouter({
   routes: [
     {
       path: '/foo',
       component: Foo,
       beforeEnter: (to, from, next) => {
         // ...
       }
     }
   ]
 })
3 组件内的守卫
  • beforeRouteEnter
  • beforeRouteUpdate (2.2 新增)
  • beforeRouteLeave
const Foo = {
  template: `...`,
  beforeRouteEnter (to, from, next) {
    // 在渲染该组件的对应路由被 confirm 前调用
    // 不!能!获取组件实例 `this`
    // 因为当守卫执行前,组件实例还没被创建
  },
  beforeRouteUpdate (to, from, next) {
    // 在当前路由改变,但是该组件被复用时调用
    // 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
    // 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
    // 可以访问组件实例 `this`
  },
  beforeRouteLeave (to, from, next) {
    // 导航离开该组件的对应路由时调用
    // 可以访问组件实例 `this`
  }
}

完整的导航解析流程

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

14 vue-router的两种模式的区别

hash模式

url地址后面 hash 值的变化,并不会导致浏览器向服务器发出请求,浏览器不发出请求,也就不会刷新页面。每次 hash 值的变化,会触发hashchange 这个事件,通过这个事件我们就可以知道 hash 值发生了哪些变化。然后我们便可以监听hashchange来实现更新页面部分内容的操作

hash模式背后的原理是onhashchange事件,可以在window对象上监听这个事件

history模式

因为HTML5标准发布,多了两个 API,pushState()replaceState()。通过这两个 API (1)可以改变 url 地址且不会发送请求,(2)不仅可以读取历史记录栈,还可以对浏览器历史记录栈进行修改。

除此之外,还有popState().当浏览器跳转到新的状态时,将触发popState事件.
当刷新时,如果服务器中没有相应的响应或者资源,会出现404

15 函数式组件的优势及原理

函数式组件的组件类型,用来定义那些没有响应数据,也不需要有任何生命周期的场景,它只接受一些props 来显示组件。正常自定义组件的区别?

  1. 不维护响应数据
  2. 无钩子函数
  3. 没有instance实例

正是因为函数式组件精简了很多例如响应式和钩子函数的处理,因此渲染性能会有一定的提高,所以如果你的业务组件是一个纯展示且不需要有响应式数据状态的处理的,那函数式组件会是一个非常好的选择。
原理:创建组件时,判断是否是函数式组件,如果是return 自定义render函数返回的Vnode,跳过正常组件初始化的流程。正常的组件是在进行初始化需要设置钩子函数和响应式数据以及其他操作。

16 v-for和v-if优先级

v-for的优先级高于v-if,在实际开发中尽量不要让v-if与v-for同时使用,需要遍历一些不需要渲染的元素。

17 组件中写name选项有哪些好处及作用?

  • 允许组件递归调用自身
  • 使用keep-alive时,可以搭配组件name进行缓存过滤
  • 使用dev-tool时有name更加方便调试
  • 拥有更好的警告信息

18 Vue事件修饰符有哪些?其实现原理是什么?

  • .stop:等同于JavaScript中的event.stopPropagation(),防止事件冒泡
  • .prevent:等同于JavaScript中的event.preventDefault(),防止执行预设的行为(如果事件可取消,则取消该事件,而不停止事件的进一步传播)
  • .capture:与事件冒泡的方向相反,事件捕获由外到内
  • .self:只会触发自己范围内的事件,不包含子元素
  • .once:只会触发一次

原理就是在生成AST树的时候,会解析这些修饰符,通过getHandler方法,对不同的修饰符做响应的处理。

19 keep-alive 实现原理

1 keep-alive是一个组件,有include和exclude两个参数,created时初始化cache(默认空对象)和keys(默认空数组)两个私有变量
2 mouted时会调用watch来实时监控Include、exclude变化。实时地更新(删除)this.cache对象数据。
3 render函数分为以下几个步骤
第一步:获取keep-alive包裹着的第一个子组件对象及其组件名;
第二步:根据设定的黑白名单(如果有)进行条件匹配,决定是否缓存。不匹配,直接返回组件实例(VNode),否则执行第三步;
第三步:根据组件ID和tag生成缓存Key,并在缓存对象中查找是否已缓存过该组件实例。如果存在,直接取出缓存值并更新该key在this.keys中的位置(更新key的位置是实现LRU置换策略的关键),否则执行第四步;
第四步:在this.cache对象中存储该组件实例并保存key值,之后检查缓存的实例数量是否超过max设置值,超过则根据LRU置换策略删除最近最久未使用的实例(即是下标为0的那个key);
第五步:最后并且很重要,将该组件实例的keepAlive属性值设置为true。

20 v-if v-model v-for原理

v-if 实现原理:

为例,该div会有个ast结构,
含有v-if的div会被额外增加三个属性:if:show,ifConfitions:Array(1),ifProcessed:true然后根据这个ast会生成如下的render函数:render:"with(this){return _c('div',{attrs:{"id":"app"}},[(show)?_c('div',[_v("v-if")]):_e()])}"
可以看到这句代码:(show)?_c('div',[_v("v-if")]):_e(),就是根据show的值决定是否生成该节点。

v-model

v-model只是一个语法糖,真正的实现靠的还是 v-bind:绑定响应式数据
和触发oninput 事件并传递数据

// 等同于

v-for

1 Vue有一个函数parseFor()用于解析v-for的值,返回一个对象,如下:

function parseFor (exp) {               
//第9383行 解析v-for属性 exp:v-for的值 ;例如:"(item,key,index) in infos"
  var inMatch = exp.match(forAliasRE);                      //用正则匹配  forAliasRE定义在9403行等于:/([^]*?)\s+(?:in|of)\s+([^]*)/; 
  if (!inMatch) { return }                                  //如果不能匹配,则返回false
  var res = {};
  res.for = inMatch[2].trim();                              //for的值,这里等于:infos
  var alias = inMatch[1].trim().replace(stripParensRE, ''); //去除两边的括号,此时alias等于:item,key,index
  var iteratorMatch = alias.match(forIteratorRE);           //匹配别名和索引
  if (iteratorMatch) {                                      //如果匹配到了,即是这个格式:v-for="(item,index) in data"
    res.alias = alias.replace(forIteratorRE, '');               //获取别名
    res.iterator1 = iteratorMatch[1].trim();                    //获取索引
    if (iteratorMatch[2]) {
      res.iterator2 = iteratorMatch[2].trim();
    }
  } else {
    res.alias = alias;
  }
  return res                                                 //返回对象,比如:{alias: "item",for: "infos",iterator1: "key",iterator2: "index"}
}

接下来在generate生成rendre函数的时候会调用genFor()生成对应的_l函数,如下:

function genFor (         //渲染v-for指令
  el,
  state,
  altGen,
  altHelper
) {
  var exp = el.for;                                                 //获取for的值
  var alias = el.alias;                                             //获取别名
  var iterator1 = el.iterator1 ? ("," + (el.iterator1)) : '';       //获取索引(v-for的值为对象时则为key)
  var iterator2 = el.iterator2 ? ("," + (el.iterator2)) : '';       ///获取索引(v-for的值为对象时))

  if ("development" !== 'production' &&
    state.maybeComponent(el) &&
    el.tag !== 'slot' &&
    el.tag !== 'template' &&
    !el.key
  ) {
    state.warn(
      "<" + (el.tag) + " v-for=\"" + alias + " in " + exp + "\">: component lists rendered with " +
      "v-for should have explicit keys. " +
      "See https://vuejs.org/guide/list.html#key for more info.",
      true /* tip */
    );
  }

  el.forProcessed = true; // avoid recursion
  return (altHelper || '_l') + "((" + exp + ")," +                //拼凑_l函数
    "function(" + alias + iterator1 + iterator2 + "){" +
      "return " + ((altGen || genElement)(el, state)) +
    '})'
}

生成的render函数其中与v-for相关的如下:

_l((infos),function(item,key,index){return _c('p',[_v(_s(index)+":"+_s(key)+":"+_s(item))

渲染生成VNode时就会执行Vue内部的_l函数,也就是全局的renderList,如下:

function renderList (     //第3691行   渲染v-for指令
  val,  
  render
) {
  var ret, i, l, keys, key;
  if (Array.isArray(val) || typeof val === 'string') {    //如果val是个数组
    ret = new Array(val.length);                            //将ret定义成val一样大小的数组
    for (i = 0, l = val.length; i < l; i++) {                 //遍历val数组
      ret[i] = render(val[i], i);                                 //依次调用render函数,参数1为值 参数2为索引 返回VNode,并把结果VNode保存到ret里面
    }
  } else if (typeof val === 'number') {
    ret = new Array(val);
    for (i = 0; i < val; i++) {
      ret[i] = render(i + 1, i);
    }
  } else if (isObject(val)) {
    keys = Object.keys(val);
    ret = new Array(keys.length);
    for (i = 0, l = keys.length; i < l; i++) {
      key = keys[i];
      ret[i] = render(val[key], key, i);
    }
  }
  if (isDef(ret)) {                                       //如果ret存在(成功调用了)
    (ret)._isVList = true;                                  //则给该数组添加一个_isVList标记,值为true
  }
  return ret                                                //最后返回ret
}

21 组件间通信

  • 父子之间通信
    1 父向子传递数据 props 子向父传递信息用 $emit方法
    2 父链 / 子链也可以通过($parent / $children)通信
    3 ref 可以获取到子组件
    4 provide / inject 可以跨层级,向所有子孙组件传递数据
    5 $attrs/$listeners 可以跨级别传入所有属性 和方法
  • 任意组件通信
    1 Vuex
    2 $emit, $on 这种方法通过一个空的Vue实例作为中央事件总线(事件中心),用它来触发事件和监听事件,巧妙而轻量地实现了任何组件间的通信,包括父子、兄弟、跨级。

    22 $attrs是为了解决什么问题出现的?应用场景有哪些?provide/inject 不能解决它能解决的问题吗?

    $attrs主要的作用就是实现批量传递数据。provide/inject更适合应用在插件中,主要是实现跨级数据传递

23 vue.use 实现了什么

可以在项目中使用vue.use()全局注入一个插件,从而不需要在每个组件文件中import插件。
源码:

import { toArray } from '../util/index'

export function initUse (Vue: GlobalAPI) {
  Vue.use = function (plugin: Function | Object) {
    const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
    if (installedPlugins.indexOf(plugin) > -1) {
      return this
    }

    // additional parameters
    const args = toArray(arguments, 1)
    args.unshift(this)
    if (typeof plugin.install === 'function') {
      plugin.install.apply(plugin, args)
    } else if (typeof plugin === 'function') {
      plugin.apply(null, args)
    }
    installedPlugins.push(plugin)
    return this
  }
}

1、首先先判断插件plugin是否是对象或者函数:
Vue.use = function (plugin: Function | Object)

2、判断vue是否已经注册过这个插件
installedPlugins.indexOf(plugin) > -1
如果已经注册过,跳出方法

3、取vue.use参数。
const args = toArray(arguments, 1)

4、toArray()取参数

24 Vue组件的渲染流程 ,父子组件的渲染顺序

1 在 new Vue() 之后。 Vue 会调用 _init 函数进行初始化,也就是这里的 init 过程,它会初始化生命周期、事件、 props、 methods、 data、 computed 与 watch 等。
上面就是使用vue template complier(compile编译可以分成 parse、optimize 与 generate 三个阶段),将模板编译成render函数,执行render函数后,变成vnode。

parse、optimize 与 generate 三个阶段

parse

parse 会用正则等方式解析 template 模板中的指令、class、style等数据,形成AST,就是with语法的过程。

optimize

optimize 的主要作用是标记 static 静态节点,这是 Vue 在编译过程中的一处优化,后面当 update更新界面时,会有一个 patch 的过程, diff 算法会直接跳过静态节点,从而减少了比较的过程,优化了 patch 的性能。

generate

generate 是将 AST 转化成 render function 字符串的过程,得到结果是 render 的字符串以及 staticRenderFns 字符串。

在经历过 parse、optimize 与 generate 这三个阶段以后,组件中就会存在渲染 VNode 所需的 render function 了。
直接执行render function

25 谈一下你对vuex的理解

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
Vuex核心

  • state 存放状态
  • mutations state成员操作
  • getters 加工state成员给外界
  • actions 异步操作
  • modules 模块化状态管理

26 slot的原理

插槽(Slot)是Vue提出来的一个概念,正如名字一样,插槽用于决定将所携带的内容,插入到指定的某个位置,从而使模板分块,具有模块化的特质和更大的重用性。

  • 普通插槽

1 父组件先解析,把子组件当做子元素处理,把插槽当做 子组件 的子元素处理
2 子组件解析,slot 作为一个占位符,会被解析成一个函_t('default')
3 这个 _t 函数,传入 'default ' 参数并执行。因为这里不给名字,默认插槽,所以是default,如果给了名字,就传入你的名字这个函数的作用,是把第一步解析得到的插槽节点拿到,然后返回

  • 作用域插槽

1 父组件先解析,把 test 当做子元素处理,把 插槽包装成一个函数,保存给节点
2 子组件解析,slot 作为一个占位符,会被解析成一个函数
这个_t 函数,和普通插槽 的一样,但是多传了一个参数,就是子组件向插槽传的参数
3 _t函数根据传入的名字('default'),拿到第一步解析插槽得到的函数(代号为A),执行A,传入参数

你可能感兴趣的:(javascript,vue.js,前端)