前端知识体系(2)-vue篇

43,296

1. 简单说下vue

  • vue是渐变式框架,根据自己的需求添加功能
  • vue数据驱动采用mvvm模式,m是数据层,v是视图层,vm是调度者
  • SPA单页面应用,只有一个页面,加载速率快
  • 组件化,复用性强

那么,它有什么缺点?

  1. vue2底层基于Object.defineProperty实现响应式,这个api本身不支持IE8及以下浏览器
  2. csr的先天不足,首屏性能问题(白屏)
  3. 由于百度等搜索引擎爬虫无法爬取js中的内容,故spa先天就对seo优化心有余力不足(谷歌的puppeteer就挺牛逼的,实现预渲染底层也是用到了这个工具)

2. vuehereact的区别

首先你得说说相同点,两个都是MVVM框架,数据驱动视图,无争议。如果说不同,那可能分为以下这么几点:

  1. vue是完整一套由官方维护的框架,核心库主要有由尤雨溪大神独自维护,而react是不要脸的书维护(很多库由社区维护),曾经一段时间很多人质疑vue的后续维护性,似乎这并不是问题。

  2. vue上手简单,进阶式框架,白话说你可以学一点,就可以在你项目中去用一点,你不一定需要一次性学习整个vue才能去使用它,而react,恐怕如果你这样会面对项目束手无策。

  3. 语法上vue并不限制你必须es6+完全js形式编写页面,可以视图和js逻辑尽可能分离,减少很多人看不惯react-jsx的恶心嵌套,毕竟都是作为前端开发者,还是更习惯于html干净。

  4. 很多人说react适合大型项目,适合什么什么,vue轻量级,适合移动端中小型项目,其实我想说,说这话的人是心里根本没点逼数,vue完全可以应对复杂的大型应用,甚至于说如果你react学的不是很好,写出来的东西或根本不如vue写的,毕竟vue跟着官方文档撸就行,自有人帮你规范,而react比较懒散自由,可以自由发挥

  5. vue在国内人气明显胜过react,这很大程度上得益于它的很多语法包括编程思维更符合国人思想

1. 什么是mvvm

MVVM的核心是数据驱动即ViewModel,ViewModel是View和Model的关系映射

MVVM本质就是基于操作数据来操作视图进而操作DOM,借助于MVVM无需直接操作DOM,开发者只需编写ViewModel中有业务,使得View完全实现自动化

2. 什么是 SPA 单页面,它的优缺点分别是什么

SPA( single-page application )即一个web项目只有一个页面(即一个HTML文件,HTML 内容的变换是利用路由机制实现的。

仅在 Web 页面初始化加载相应的 HTML、JavaScript 和 CSS。一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现 HTML 内容的变换,UI 与用户的交互,避免页面的重新加载。

优点:

  • 用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染;
  • 基于上面一点,SPA 相对对服务器压力小
  • 前后端职责分离架构清晰,前端进行交互逻辑,后端负责数据处理;

缺点

  • 初次加载耗时多:为实现单页 Web 应用功能及显示效果,需要在加载页面的时候将 JavaScript、CSS 统一加载,部分页面按需加载;

前进后退路由管理:由于单页应用在一个页面中显示所有的内容,所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理;

SEO 难度较大:由于所有的内容都在一个页面中动态替换显示,所以在 SEO 上其有着天然的弱势。

3. 生命周期

3-1 基本概念

什么是vue生命周期? Vue 实例创建销毁过程,就是生命周期。

注意:浏览器有8个钩子,但是node中做服务端渲染的时候只有beforeCreatecreated

  • beforeCreatenew Vue()之后触发的第一个钩子,在当前阶段data、methods、computed以及watch上的数据和方法都不能被访问。 可以做页面拦截。当进一个路由的时候我们可以判断是否有权限进去,是否安全进去,携带参数是否完整,参数是否安全。使用这个钩子好函数的时候就避免了让页面去判断,省掉了创建一个组建Vue实例。

  • created 发生在实例创建完成后,当前阶段已经完成了数据观测,也就是可以使用数据,更改数据,在这里更改数据不会触发updated函数。可以做一些初始数据的获取,在当前阶段无法Dom进行交互(因为Dom还没有创建),如果非要想,可以通过vm.$nextTick来访问Dom。

  • beforeMount发生在挂载之前,在这之前template模板已导入渲染函数编译。而当前阶段虚拟Dom已经创建完成,即将开始渲染。在此时也可以对数据进行更改,不会触发updated。

  • mounted发生在挂载完成后,在当前阶段,真实Dom挂载完毕,数据完成双向绑定,可以访问到Dom节点,使用$refs属性对Dom进行操作。

  • beforeUpdate发生在更新之前,也就是响应式数据发生更新,虚拟dom重新渲染之前被触发,你可以在当前阶段进行更改数据,不会造成重渲染。

  • updated发生在更新完成之后,当前阶段组件Dom已完成更新。要注意的是避免在此期间更改数据,因为这可能会导致无限循环的更新。

  • beforeDestroy发生在实例销毁之前,在当前阶段实例完全可以被使用,我们可以在这时进行善后收尾工作,比如清除计时器,销毁父组件对子组件的重复监听。beforeDestroy(){Bus.$off("saveTheme")}

  • destroyed发生在实例销毁之后,这个时候只剩下了dom空壳。组件已被拆解,数据绑定被卸除,监听被移出,子实例也统统被销毁。

3-2 生命周期调用顺序

  • 组件的调用顺序都是先父后子
  • 渲染完成的顺序是先子后父
  • 组件的销毁操作是先父后子
  • 销毁完成的顺序是先子后父

加载渲染过程 父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount- >子mounted->父mounted

子组件更新过程 父beforeUpdate->子beforeUpdate->子updated->父updated

父组件更新过程 父 beforeUpdate -> 父 updated

销毁过程 父beforeDestroy->子beforeDestroy->子destroyed->父destroyed

3-3 vue生命周期的作用是什么

它的生命周期中有多个事件钩子,让我们控制Vue实例过程更加清晰

3-4 第一次页面加载会触发哪几个钩子

第一次页面加载时会触发 beforeCreate, created, beforeMount, mounted 这几个钩子

3-5 每个周期具体适合哪些场景

  • beforecreate : 可以在这加个loading事件,在加载实例时触发
  • created : 初始化完成时的事件写在这里,如在这结束loading事件,异步请求也适宜在这里调用
  • mounted : 挂载元素,获取到DOM节点
  • updated : 如果对数据统一处理,在这里写上相应函数
  • beforeDestroy : 可以清除定时器
  • nextTick : 更新数据后立即操作dom

4.v-show 与 v-if 的区别

v-if

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

v-show

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

所以:

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

5. Vue 的单向数据流

背景:

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

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

有两种常见的试图改变一个 prop 的情形 :

  • 这个 prop 用来传递一个初始值;
  • 这个子组件接下来希望将其作为一个本地的 prop 数据来使用。

在第2情况下,最好定义一个本地的 data属性并将这个 prop 用作其初始值:

props: ['initialCounter'],
data: function () {
  return {
    counter: this.initialCounter//定义本地的data属性接收prop初始值
  }
}

这个 prop 以一种原始的值传入且需要进行转换。

在这种情况下,最好使用这个 prop 的值来定义一个计算属性

props: ['size'],
computed: {
  normalizedSize: function () {
    return this.size.trim().toLowerCase()
  }
}

6.异步请求适合在哪个生命周期调用?

官方实例的异步请求是在mounted生命周期中调用的,而实际上也可以在created生命周期中调用。

本人推荐在 created 钩子函数中调用异步请求,有以下优点:

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

7.Vue2.x组件通信有哪些方式?

7-1 父子组件通信

  • 父->子props;子(emit)->父(on)
  • 获取父子组件实例 parent/parent / children 如:直接在子组件的methods方法里面写:this.$parent.show()//show为父组件中定义的方法
  • Ref (如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例),如在引入的子组件的标签上挂载一个: ref="comA",然后在方法中或者子组件的数据,this.$refs.comA.titles
  • Provide、inject 官方不推荐使用,但是写组件库时很常用,祖先组件中通过provider来提供变量,然后在子孙组件中通过inject来注入变量

7-2 兄弟组件通信

  • Event Bus 实现跨组件通信: Vue.prototype.$bus = new Vue
  • Vuex

7-3 跨级组件通信

  • Vuex
  • attrs,listeners
  • Provide、inject

7-4 使用

1. 父子props,on

// 子组件

<template>
  <header>
    <h1 @click="changeTitle">{{title}}</h1>//绑定一个点击事件
  </header>
</template>
<script>
export default {
  data() {
    return {
      title:"Vue.js Demo"
    }
  },
  methods:{
    changeTitle() {
      this.$emit("titleChanged","子向父组件传值");//自定义事件  传递值“子向父组件传值”
    }
  }
}
</script>

// 父组件

<template>
  <div id="app">
    <Header @titleChanged="updateTitle" ></Header>//与子组件titleChanged自定义事件保持一致
    <h2>{{title}}</h2>
  </div>
</template>
<script>
import Header from "./Header"
export default {
  data(){
    return{
      title:"传递的是一个值"
    }
  },
  methods:{
    updateTitle(e){   //声明这个函数
      this.title = e;
    }
  },
  components:{
   Header
  }
}
</script>

2. parent / $children与 ref

// A 子组件

export default {
  data () {
    return {
      title: 'a组件'
    }
  },
  methods: {
    sayHello () {
      alert('Hello');
    }
  }
}

// 父组件

<template>
  <A ref="comA"></A>
</template>
<script>
  export default {
    mounted () {
      const comA = this.$refs.comA;
      console.log(comA.title);  // a组件
      comA.sayHello();  // 弹窗
    }
  }
</script>

3.attrs,listeners

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

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

// index.vue

<template>
  <div>
    <h2>浪里行舟</h2>
    <child-com1 :foo="foo" :boo="boo" :coo="coo" :doo="doo" title="前端工匠"></child-com1>
  </div>
</template>
<script>
const childCom1 = () => import("./childCom1.vue");
export default {
  components: { childCom1 },
  data() {
    return {
      foo: "Javascript",
      boo: "Html",
      coo: "CSS",
      doo: "Vue"
    };
  }
};
</script>

// childCom1.vue

<template class="border">
  <div>
    <p>foo: {{ foo }}</p>
    <p>childCom1的$attrs: {{ $attrs }}</p>
    <child-com2 v-bind="$attrs"></child-com2>
  </div>
</template>
<script>
const childCom2 = () => import("./childCom2.vue");
export default {
  components: {
    childCom2
  },
  inheritAttrs: false, // 可以关闭自动挂载到组件根元素上的没有在props声明的属性
  props: {
    foo: String // foo作为props属性绑定
  },
  created() {
    console.log(this.$attrs); // 父组件中的属性,且不在当前组件props中的属性。{ "boo": "Html", "coo": "CSS", "doo": "Vue", "title": "前端工匠" }
  }
};
</script>

// childCom2.vue

<template>
  <div class="border">
    <p>boo: {{ boo }}</p>
    <p>childCom2: {{ $attrs }}</p>
    <child-com3 v-bind="$attrs"></child-com3>
  </div>
</template>
<script>
const childCom3 = () => import("./childCom3.vue");
export default {
  components: {
    childCom3
  },
  inheritAttrs: false,
  props: {
    boo: String
  },
  created() {
    console.log(this.$attrs); // / 父组件中的属性,且不在当前组件props中的属性。{"coo": "CSS", "doo": "Vue", "title": "前端工匠" }
  }
};
</script>

// childCom3.vue

<template>
  <div class="border">
    <p>childCom3: {{ $attrs }}</p>
  </div>
</template>
<script>
export default {
  props: {
    coo: String,
    title: String
  }
};
</script>

4. Provide、inject的使用:

父组件

<template>
    <div id="app">
    </div>
</template>
    <script>
        export default {
            data () {
                    return {
                        datas: [
                            {
                                id: 1,
                                label: '产品一'
                            }
                        ]
                    }
            },
            provide {
                return {
                    datas: this.datas
                }
            }
        }
    </script>

子组件

<template>
    <div>
        <ul>
        <li v-for="(item, index) in datas" :key="index">
            {{ item.label }}
        </li>
        </ul>
    </div>
</template>
    <script>
        export default {
            inject: ['datas']
        }
    </script>

8. 什么是SSR

SSR也就是服务端渲染,也就是将Vue在客户端把标签渲染成HTML的工作放在服务端完成,然后再把html直接返回给客户端。

服务端渲染 SSR 的优缺点如下:

(1)服务端渲染的优点:

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

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

(2) 服务端渲染的缺点:

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

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

9.Vue路由

9-1 vue-router 路由模式有几种?

vue-router 有 3 种路由模式:hashhistoryabstract,对应的源码如下所示:

switch (mode) {
  case 'history':
	this.history = new HTML5History(this, options.base)
	break
  case 'hash':
	this.history = new HashHistory(this, options.base, this.fallback)
	break
  case 'abstract':
	this.history = new AbstractHistory(this, options.base)
	break
  default:
	if (process.env.NODE_ENV !== 'production') {
	  assert(false, `invalid mode: ${mode}`)
	}
}

路由模式的说明如下:

  • hash: 使用 URL hash 值来作路由。支持所有浏览器,包括不支持 HTML5 History Api 的浏览器;

  • history : 依赖 HTML5 History API 和服务器配置。具体可以查看 HTML5 History 模式;

  • abstract : 支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式.

9-2 hash路由和history路由实现原理

(1)hash 模式的实现原理

早期的前端路由的实现就是基于 location.hash 来实现的。其实现原理很简单,location.hash 的值就是 URL 中 # 后面的内容。

比如下面这个网站,它的 location.hash 的值为 '#search'

https://www.word.com#search

hash 路由模式的实现主要是基于下面几个特性:

  • URL 中 hash 值只是客户端的一种状态,也就是说当向服务器端发出请求时,hash 部分不会被发送
  • hash 值的改变,都会在浏览器的访问历史增加一个记录。因此我们能通过浏览器的回退、前进按钮控制hash 的切换;
  • 可以通过 a 标签,并设置 href 属性,当用户点击这个标签后,URL 的 hash 值会发生改变;或者使用JavaScript 来对 loaction.hash 进行赋值,改变 URL 的 hash 值;
  • 我们可以使用 hashchange 事件来监听 hash 值的变化,从而对页面进行跳转(渲染)。

(2)history 模式的实现原理

HTML5 提供了 History API 来实现 URL 的变化,其中做最主要的 API 有以下两个:

  • history.pushState() //新曾历史记录
  • history.repalceState()。 //替换历史记录

这两个 API 可以在不进行刷新的情况下,操作浏览器的历史纪录。唯一不同的是,前者是新增一个历史记录,后者是直接替换当前的历史记录,如下所示:

window.history.pushState(null, null, path);
window.history.replaceState(null, null, path);

history 路由模式的实现主要基于存在下面几个特性:

  • pushState 和 repalceState 两个 API 来操作实现 URL 的变化;
  • 我们可以使用 popstate 事件来监听 url 的变化,从而对页面进行跳转(渲染);
  • history.pushState() 或 history.replaceState() 不会触发 popstate 事件,这时我们需要手动触发页面跳转(渲染)。

10.Vue 中的 key 有什么作用?

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

Vue 的 diff 过程可以概括为:

oldChnewCh 各有两个头尾的变量 oldStartIndex、oldEndIndexnewStartIndex、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
}

参考1:Vue2.0 v-for 中 :key 到底有什么用?

11.虚拟 DOM 实现原理

虚拟 DOM 的实现原理主要包括以下 3 部分:

  • 用 JavaScript 对象模拟真实 DOM 树,对真实 DOM 进行抽象;
  • diff 算法 — 比较两棵虚拟 DOM 树的差异;
  • pach 算法 — 将两个虚拟 DOM 对象的差异应用到真正的 DOM 树。

详情点击这里

13.虚拟 DOM 的优缺点

优点:

  • 保证性能下限: 框架的虚拟 DOM 需要适配任何上层 API 可能产生的操作,它的一些 DOM 操作的实现必须是普适的,所以它的性能并不是最优的;但是比起粗暴的 DOM 操作性能要好很多,因此框架的虚拟 DOM 至少可以保证在你不需要手动优化的情况下,依然可以提供还不错性能,即保证性能的下限;

  • 无需手动操作 DOM: 我们不再需要手动去操作 DOM,只需要写好 View-Model 的代码逻辑,框架会根据虚拟 DOM 和 数据双向绑定,帮我们以可预期的方式更新视图,极大提高我们的开发效率;

  • 跨平台: 虚拟 DOM 本质上是 JavaScript 对象,而 DOM 与平台强相关,相比之下虚拟 DOM 可以进行更方便地跨平台操作,例如服务器渲染、weex 开发等等。

缺点:

  • 无法进行极致优化: 虽然虚拟 DOM + 合理的优化,足以应对绝大部分应用的性能需求,但在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化。

14.Proxy 与 Object.defineProperty 优劣对比

Proxy 的优势如下:

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

Object.defineProperty 的优势如下:

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

14-1 那么,Proxy 可以实现什么功能?

Proxy 是 ES6 中新增的功能,它可以用来自定义对象中的操作。

let p = new Proxy(target, handler)
  • target 代表需要添加代理的对象
  • handler 用来自定义对象中操作,比如可以用来自定义 set 或者 get 函数。

下面来通过 Proxy 来实现一个数据响应式:

let onWatch = (obj, setBind, getLogger) => {
  let handler = {
    get(target, property, receiver) {
      getLogger(target, property)
      return Reflect.get(target, property, receiver)
    },
    set(target, property, value, receiver) {
      setBind(value, property)
      return Reflect.set(target, property, value)
    }
  }
  return new Proxy(obj, handler)
}
let obj = { a: 1 }
let p = onWatch(
  obj,
  (v, property) => {
    console.log(`监听到属性${property}改变为${v}`)
  },
  (target, property) => {
    console.log(`'${property}' = ${target[property]}`)
  }
)
p.a = 2 // 监听到属性a改变为2
p.a // 'a' = 2

在上述代码中,通过自定义 set 和 get 函数的方式,在原本的逻辑中插入了我们的函数逻辑,实现了在对对象任何属性进行读写时发出通知。

当然这是简单版的响应式实现,如果需要实现一个 Vue 中的响应式,需要在 get 中收集依赖,在 set 派发更新,之所以 Vue3.0 要使用 Proxy 替换原本的 API 原因在于 Proxy 无需一层层递归为每个属性添加代理,一次即可完成以上操作,性能上更好,并且原本的实现有一些数据更新不能监听到,但是 Proxy 可以完美监听到任何方式的数据改变,唯一缺陷就是浏览器的兼容性不好。

15.Vue 框架怎么实现对象和数组的监听?

Vue 框架是通过遍历数组 和递归遍历对象,从而达到利用 Object.defineProperty() 也能对对象和数组(部分方法的操作)进行监听。

vue2:

数组就是使用 object.defineProperty 重新定义数组的每一项,能引起数组变化的方法为 pop 、 push 、 shift 、 unshift 、 splice 、 sort 、 reverse 这七种,只要这些方法执行改了数组内容,就更新内容

  • 是用来函数劫持的方式,重写了数组方法,具体就是更改了数组的原型,更改成自己的,用户调数组的一些方法的时候,走的就是自己的方法,然后通知视图去更新(本质就是在原有的方法上又调用了更新数据的方法)。

  • 数组项可能是对象,那么就对数组的每一项进行观测

vue3:

改用 proxy ,可直接监听对象数组的变化。

参考

16.Vue 是如何实现数据双向绑定的

Vue 数据双向绑定主要是指:数据变化更新视图,视图变化更新数据

输入框内容变化时,Data 中的数据同步变化。即 View => Data 的变化。 Data 中的数据变化时,文本节点的内容同步变化。即 Data => View 的变化。

其中,View 变化更新 Data ,可以通过事件监听的方式来实现,所以 Vue 的数据双向绑定的工作主要是如何根据 Data 变化更新 View。

Vue 主要通过以下 4 个步骤来实现数据双向绑定的

  • 实现一个监听器 Observer 对数据对象进行遍历,包括子属性对象的属性,利用 Object.defineProperty() 对属性都加上 settergetter。这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化。

  • 实现一个解析器 Compile 解析 Vue 模板指令,将模板中的变量都替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,调用更新函数进行数据更新。

  • 实现一个订阅者 Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁 ,主要的任务是订阅 Observer 中的属性值变化的消息,当收到属性值变化的消息时,触发解析器 Compile 中对应的更新函数。

  • 实现一个订阅器 Dep 订阅器采用 发布-订阅 设计模式,用来收集订阅者 Watcher,对监听器 Observer 和 订阅者 Watcher 进行统一管理。

17.v-model 的原理

v-model 指令在表单 input、textarea、select 等元素上创建双向数据绑定,v-model 本质上是语法糖,会在内部为不同的输入元素使用不同的属性并抛出不同的事件:

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

以 input 表单元素为例

<input v-model='something'>

相当于

<input :value="something" @input="something = $event.target.value">

18.组件中 data 为什么是一个函数?

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

// data

data() {
  return {
	message: "子组件",
	childName:this.name
  }
}

// new Vue

new Vue({
el: '#app',
router,
template: '<App/>',
components: {App}
})

一个组件被复用多次的话,也就会创建多个实例,本质上,这些实例用的都是同一个构造函数。

如果data是对象的话,对象属于引用类型,会影响到所有的实例,所以为了保证组件不同的实例之间data不冲突,data必须是一个函数。

而 new Vue 的实例,是不会被复用的,因此不存在引用对象的问题。

19.谈谈你对 keep-alive

keep-alive 是 Vue 内置的一个组件,可以使被包含的组件保留状态避免重新渲染 ,其有以下特性:

  • 一般结合路由和动态组件一起使用,用于缓存组件;

  • 提供 includeexclude 属性,两者都支持字符串或正则表达式, include 表示只有名称匹配的组件会被缓存,exclude 表示任何名称匹配的组件都不会被缓存 ,其中 exclude 的优先级比 include

  • 对应两个钩子函数 activateddeactivated ,当组件被激活时,触发钩子函数 activated,当组件被移除时,触发钩子函数 deactivated。

keep-alive的生命周期

  • activated: 页面第一次进入的时候,钩子触发的顺序是created->mounted->activated
  • deactivated: 页面退出的时候会触发deactivated,当再次前进或者后退的时候只触发activated

20.父组件可以监听到子组件的生命周期吗?

比如有父组件 Parent 和子组件 Child,如果父组件监听到子组件挂载 mounted 就做一些逻辑处理,可以通过以下写法实现:

// Parent.vue

<Child @mounted="doSomething"/>

// Child.vue

mounted() {
  this.$emit("mounted");
}

以上需要手动通过 $emit 触发父组件的事件,更简单的方式可以在父组件引用子组件时通过 @hook 来监听即可,如下所示:

// Parent.vue

<Child @hook:mounted="doSomething" ></Child>
doSomething() {
   console.log('父组件监听到 mounted 钩子函数 ...');
},

// Child.vue

mounted(){
   console.log('子组件触发 mounted 钩子函数 ...');
}, 

// 以上输出顺序为: // 子组件触发 mounted 钩子函数 ... // 父组件监听到 mounted 钩子函数 ...
当然 @hook 方法不仅仅是可以监听 mounted,其它的生命周期事件,例如:created,updated 等都可以监听。

21.直接给一个数组项赋值,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)

22.vue2.x中如何监测数组变化

使用了函数劫持的方式,重写了数组的方法,Vue将data中的数组进行了原型链重写,指向了自己定义数组原型方法。这样当调用数组api时,可以通知依赖更新。如果数组中包含着引用类型,会对数组中的引用类型再次递归遍历进行监控。这样就实现了监测数组变化。

22.Vue2.x和Vue3.x渲染器的diff算法分别说一下

简单来说,diff算法有以下过程

同级比较, 再比较子节点,先判断一方有子节点一方没有子节点的情况(如果新的children没有子节点,将旧的子节点移除)

比较都有子节点的情况(核心diff)递归比较子节点

正常Diff两个树的时间复杂度是O(n^3),但实际情况下我们很少会进行跨层级的移动DOM,所以Vue将Diff进行了优化,从O(n^3) -> O(n),只有当新旧children都为多个子节点时才需要用核心的Diff算法进行同层级比较。

Vue2的核心Diff算法采用了双端比较的算法,同时从新旧children的两端开始进行比较,借助key值找到可复用的节点,再进行相关操作。相比React的Diff算法,同样情况下可以减少移动节点次数,减少不必要的性能损耗,更加的优雅。

Vue3.x借鉴了 ivi算法和 inferno算法 在创建VNode时就确定其类型,以及在mount/patch的过程中采用位运算来判断一个VNode的类型,在这个基础之上再配合核心的Diff算法,使得性能上较Vue2.x有了提升。 该算法中还运用了动态规划的思想求解最长递归子序列。

23.Vue模版编译原理

简单说,Vue的编译过程就是将template转化为render函数的过程。会经历以下阶段:

  • 生成AST
  • 树优化
  • codegen

首先解析模版,生成AST语法树(一种用JavaScript对象的形式来描述整个模板)。

使用大量的正则表达式对模板进行解析,遇到标签文本的时候都会执行对应的钩子进行相关处理。

Vue的数据是响应式的,但其实模板中并不是所有的数据都是响应式的。有一些数据首次渲染后就不会变化,对应的DOM也不会变化。那么优化过程就是深度遍历AST树,按照相关条件对树节点进行标记。这些被标记的节点(静态节点)我们就可以跳过对它们的比对,对运行时的模板起到很大的优化作用。

编译的最后一步是将优化后的AST树转换可执行代码

24.Computed和Watch

computed:

  • computed是计算属性,也就是计算值,它更多用于计算值的场景
  • computed具有缓存性,computed的值在getter执行后是会缓存的,只有在它依赖的属性值改变之后,下一次获取computed的值时才会重新调用对应的getter来计算
  • computed适用于计算比较消耗性能的计算场景

watch:

  • 更多的是「观察」的作用,类似于某些数据的监听回调,用于观察props $emit或者本组件的值,当数据变化时来执行回调进行后续操作
  • 无缓存性,页面重新渲染时值不变化也会执行

小结:

  • 当我们要进行数值计算,而且依赖于其他数据,那么把这个数据设计为computed
  • 如果你需要在某个数据变化时做一些事情,使用watch来观察这个数据变化

25.nextTick

下次 DOM 更新循环结束之后执行延迟回调。在这里面的代码会等到dom更新以后再执行。

<template>
  <section>
    <div ref="hello">
      <h1>Hello World ~</h1>
    </div>
    <el-button type="danger" @click="get">点击</el-button>
  </section>
</template>
<script>
  export default {
    methods: {
      get() {
      }
    },
    mounted() {
      console.log(333);
      console.log(this.$refs['hello']);
      this.$nextTick(() => {
        console.log(444);
        console.log(this.$refs['hello']);
      });
    },
    created() {
      console.log(111);
      console.log(this.$refs['hello']);
      this.$nextTick(() => {
        console.log(222);
        console.log(this.$refs['hello']);
      });
    }
  }
</script>

image.png

详细点击这路

26.vue响应式原理

26-1 Vue2.x响应式数据原理

Vue在初始化数据时,会使用Object.defineProperty重新定义data中的所有属性,当页面使用对应属性时,首先会进行依赖收集(收集当前组件的watcher),如果属性发生变化通知相关依赖进行更新操作(发布订阅)

具体的过程

  • 首先Vue使用 initData 初始化用户传入的参数
  • 然后使用 new Observer 对数据进行观测
  • 如果数据是一个对象类型就会调用 this.walk(value) 对对象进行处理,内部使用 defineeReactive 循环对象属性定义响应式变化,核心就是使用 Object.defineProperty 重新定义数据。

26-2 Vue3.x响应式数据原理

Vue3.x改用Proxy替代Object.defineProperty。因为Proxy可以直接监听对象和数组的变化,并且有多达13种拦截方法。并且作为新标准将受到浏览器厂商重点持续的性能优化。

Proxy只会代理对象的第一层,那么Vue3又是怎样处理这个问题的呢?

判断当前Reflect.get的返回值是否为Object,如果是则再通过reactive方法做代理, 这样就实现了深度观测。

监测数组的时候可能触发多次get/set,那么如何防止触发多次呢?

我们可以判断key是否为当前被代理对象target自身属性,也可以判断旧值新值是否相等,只有满足以上两个条件之一时,才有可能执行trigger

27.在使用计算属性的时,函数名和data数据源中的数据可以同名吗?

不能同名 因为不管是计算属性还是data还是props 都会被挂载在vm实例上,因此 这三个都不能同名

28.怎么解决vue打包后静态资源图片失效的问题

找到config/index.js 配置文件,找build打包对象里的assetsPublicPath属性 默认值为/,更改为./就好了

29. 怎么解决vue动态设置img的src不生效的问题?

因为动态添加src被当做静态资源处理了,没有进行编译,所以要加上require。

<img :src="require('../../../assets/images/xxx.png')" />

30. 使用vue渲染大量数据时应该怎么优化?说下你的思路!

  1. 思路
  • 按需加载局部数据, 虚拟列表,无限下拉刷新
  • js运行异步处理: 分割任务,实现时间切片处理, 类似react fiber, 每次执行记录时间, 超过一定执行时间则settimeout或requestAnimation推迟到下一个时间片,一般一个时间片为16ms
  • 大量纯展示的数据,不需要追踪变化的 用object.freeze冻结(可以使用虚拟列表,Object.freeze冻结对象,Object.preventExtentsion阻止对象扩展来阻止vue给每个对象加上get,set,但是缺点是不能响应了)
  • 数据量大的时候,可以做分页处理。翻页一次请求10-20条数据
  1. Object.freez

适合一些 big data的业务场景。尤其是做管理后台的时候,经常会有一些超大数据量table,或者一个含有 n 多数据的图表,这种数据量很大的东西使用起来最明显的感受就是卡。但其实很多时候其实这些数据其实并不需要响应式变化,这时候你就可以使用 Object.freeze 方法了,它可以冻结一个对象(注意它不并是 vue 特有的 api)。

当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter,它们让 Vue 能进行追踪依赖,在属性被访问和修改时通知变化。

使用了 Object.freeze 之后,不仅可以减少 observer 的开销,还能减少不少内存开销

使用方式:

this.item = Object.freeze(Object.assign({}, this.item))

30. vue 自定义指令

先了解一下,在 vue 中,有很多内置的指令.

比如:

  • v-bind: 属性绑定,把数据绑定在HTML元素的属性上.
  • v-html & v-text 把数据绑定在HTML元素的属性上,作用同 innerHTML & innerText
  • v-on: 绑定HTML元素事件

所以,关于指令,我们可以总结下面几点:

  • 指令是写在 HTML 属性地方的,<input v-model='name' type='text' />
  • 指令都是以 v- 开头的.
  • 指令表达式的右边一般也可以跟值 v-if = false

Vue自定义指令案例1

例如:我们需要一个指令,写在某个HTML表单元素上,然后让它在被加载到DOM中时,自动获取焦点.

// 和自定义过滤器一样,我们这里定义的是全局指令
Vue.directive('focus',{
    inserted(el) {
      el.focus()
    }
})

<div id='app'>
    <input type="text">
    <input type="text" v-focus placeholder="我有v-focus,所以,我获取了焦点">
  </div>

image.png

先总结几个点:

  • 使用 Vue.directive() 来新建一个全局指令,(指令使用在HTML元素属性上的)
  • Vue.directive('focus') 第一个参数focus是指令名,指令名在声明的时候,不需要加 v-
  • 在使用指令的HTML元素上,<input type="text" v-focus placeholder="我有v-focus,所以,我获取了焦点"/> 我们需要加上 v-.
  • Vue.directive('focus',{}) 第二个参数是一个对象,对象内部有个 inserted() 的函数,函数有 el 这个参数.
  • el 这个参数表示了绑定这个指令的 DOM元素,在这里就是后面那个有 placeholderinput,el 就等价于 document.getElementById('el.id')
  • 可以利用 $(el) 无缝连接 jQuery

指令的生命周期

用指令我们需要:

  • 新增一个指令
  • 定义指令的第二个参数里的 inserted 函数
  • 在需要获取焦点的元素上,使用这个指令.

当一个指令绑定到一个元素上时,其实指令的内部会有五个生命周期事件函数.

  • bind(){} 当指令绑定到 HTML 元素上时触发.只调用一次.
  • inserted() 当绑定了指令的这个HTML元素插入到父元素上时触发(在这里父元素是 div#app).但不保证,父元素已经插入了 DOM 文档.
  • updated() 所在组件的VNode更新时调用.
  • componentUpdate 指令所在的组件的VNode以及其子VNode 全部更新后调用.
  • unbind: 指令和元素解绑的时候调用,只调用一次

Vue 指令的声明周期函数

Vue.directive('gqs',{
    bind() {
      // 当指令绑定到 HTML 元素上时触发.**只调用一次**
      console.log('bind triggerd')
    },
    inserted() {
      // 当绑定了指令的这个HTML元素插入到父元素上时触发(在这里父元素是 `div#app`)**.但不保证,父元素已经插入了 DOM 文档.**
      console.log('inserted triggerd')
    },
    updated() {
      // 所在组件的`VNode`更新时调用.
      console.log('updated triggerd')
    },
    componentUpdated() {
      // 指令所在组件的 VNode 及其子 VNode 全部更新后调用。
      console.log('componentUpdated triggerd')
      
    },
    unbind() {
      // 只调用一次,指令与元素解绑时调用.
      console.log('unbind triggerd')
    }
  })

HTML

<div id='app' v-gqs></div>

结果:

bind triggerd
inserted triggerd

发现默认情况下只有 bind 和 inserted 声明周期函数触发了.

那么剩下的三个什么时候触发呢?

 <div id='app' >
    <p v-gqs v-if="show">v-if是删除或者新建dom元素,它会触发unbind指令声明周期吗?</p>
    <button @click="show=!show">toggle</button>
  </div>

当指令绑定的元素被销毁时,会触发指令的 unbind 事件. (新建并显示,仍然是触发 bind & inserted)

unbind触发.gif

 <p v-gqs v-show="show2">v-show设置元素的display:block|none,会触发componentUpdated事件</p>
 <button @click="show2=!show2">toggle-v-show</button>
  • 一个把元素从DOM删除触发unbind().---> 仅仅是删除.
  • 一个显示设置元素的隐藏和显示的时候触发 componentUpdated() ---> block | none 都触发.

31. vue实例挂载的过程是什么

32. 组件和插件有什么区别

  • 组件 (Component) 是用来构成你的 App 的业务模块,它的目标是 App.vue。

  • 插件 (Plugin) 是用来增强你的技术栈的功能模块,它的目标是 Vue 本身。

33.使用vue过程中可能会遇到的问题(坑)有哪些

34. 动态给vue的data添加一个新的属性时会发生什么

根据官方文档定义:

如果在实例创建之后添加新的属性到实例上,它不会触发视图更新。

Vue 不允许在已经创建实例动态添加新的根级响应式属性 (root-level reactive property)。

然而它可以使用 Vue.set(object, key, value) 方法将响应属性添加到嵌套的对象上。

35. SPA首屏加载速度慢的怎么解决

  1. 通过Gzip压缩
  2. 使用路由懒加载
  3. 利用webpack中的externals这个属性把不需要打包的库文件都分离出去,减小项目打包后的大小
  4. 使用SSR渲染

36. mixin

多个实例引用了相同或相似的方法或属性等,可将这些重复的内容抽取出来作为mixins的js,export出去,在需要引用的vue文件通过mixins属性注入,与当前实例其他内容进行merge

一个混入对象可以包含任意组件选项。同一个生命周期,混入对象会比组件先执行

//暴露两个mixins对象

export const mixinsTest1 = {
    methods: {
        hello1() {
            console.log("hello1");
        }
    },
    created() {
        this.hello1();
    },
}


export const mixinsTest2 = {
    methods:{
        hello2(){
            console.log("hello2");
        }
    },
    created() {
        this.hello2();
    },
}
<template>
<div>
    home
</div>
</template>

<script>
import {mixinsTest1,mixinsTest2} from '../util/test.js'
export default {
  name: "Home",
  data () {
    return {
    };
  },
  created(){
      console.log("1212");
  },
  mixins:[mixinsTest2,mixinsTest1] // 先调用哪个mixins对象,就先执行哪个
}
</script>

hello2
hello1
1212

37. vue的核心是什么

  1. 数据驱动 专注于View 层。它让开发者省去了操作DOM的过程,只需要改变数据。
  2. 组件响应原理 数据(model)改变驱动视图(view)自动更新
  3. 组件化 扩展HTML元素,封装可重用的代码。

38. vue常用的修饰符有哪些

  • v-model: .trim .number
  • v-on: .stop .prevent

39. v-on可以绑定多个方法吗?

<input v-model="msg" type="text" v-on="{input:a, focus:b}"/>

40. template编译的理解

链接

41.axios是什么,如何中断axios的请求

axios 官方文档示例

  var CancelToken = axios.CancelToken; 
  var cancel;  
  axios.get('/user/12345', {   
      cancelToken: new CancelToken(function (c) {     
          // executor 函数接收一个 cancel 函数作为参数     
          cancel = c;   
      }) 
});  
// 取消请求 
cancel(); 

实际项目中

1. 首先需要单独引入 axios,已配置的 axios 文件 http.js export default 没有包含 CancelToken

import axios from 'axios' 

2. 定义一个 cancelArr 用于存放多个 cancel 函数,单独中止请求时获取索引来执行: this.cancelArr[key]

data() {
    return {
        //用于存放 cancel 函数
        cancelArr:[]
    }
}

3. 单独引入 axios 后方可使用 axios.CancelToken

let CancelToken = axios.CancelToken
let cancel;
let _this = this;
this.axios.post('upload', obj, {
    onUploadProgress: progress => {
        //处理进度
    },
    cancelToken:new CancelToken(function excutor(c){
        cancel = c;
        //一个处理函数接受一个cancel函数作为参数
        _this.cancelArr.push(cancel)
        //在已定义的cancelArr数组中放入cancel函数
        console.log(_this.cancelArr);
        //上传多个文件时,每增加一个上传文件,cancelArr数组中会多一个cancel函数
    })
})

4. 根据索引取消单个请求

cancelRequest(key){
    //获取到要中止请求的索引后,执行cancel
    this.cancelArrkey
},

5. 中止多个请求

for(let i in this.cancelArr){
    //遍历 cancelArr,逐个执行
    this.cancelArri
}

42. 如何引入scss?

安装scss依赖包:

npm install sass-loader --save-dev npm install node-sass --save-dev

在build文件夹下修改 webpack.base.conf.js 文件,在 module 下的 rules 里添加配置,如下:

{ test: /\.scss$/, loaders: ['style', 'css', 'sass'] }

应用:

在vue文件中应用scss时,需要在style样式标签上添加lang="scss",即<style lang="scss">。

43. 在vue中watch和created哪个先执行

watch 中的 immediate 会让监听在初始值声明的时候去执行监听计算,否则就是 created 先执行

44. 在vue中created与activated有什么区别

created():在实例创建完成后被立即调用。在这一步,实例已完成以下的配置:数据观测 (data observer),property 和方法的运算,watch/event 事件回调。然而,挂载阶段还没开始,$el property 目前尚不可用。

activated():是在路由设置<keep-alive></keep-alive>时,才会有这个生命周期。在被 keep-alive 缓存的组件激活时调用。

45. 为什么在v-for中的key不推荐使用随机数或者index呢

因为在插入数据或者删除数据的时候,会导致后面的数据的key绑定的index变化,进而导致从新渲染,效率会降低

46. 如何批量引入组件

动态组件使用方法

<keep-alive>
    <component :is="isWhich"></component>
</keep-alive>

使用标签保存状态,即切换组件再次回来依然是原来的样子,页面不会刷新,若不需要可以去掉。

通过事件改变is绑定的isWhich值即可切换成不同的组件,isWhich的值为组件名称。

47. vue中怎么重置data

使用场景:

比如,有一个表单,表单提交成功后,希望组件恢复到初始状态,重置data数据。

使用Object.assign()vm.$data可以获取当前状态下的data,vm.$options.data可以获取到组件初始化状态下的data

初始状态下设置data数据的默认值,重置时直接object.assign(this.$data, this.$options.data())

说明:

  • this.$data获取当前状态下的data
  • this.$options.data()获取该组件初始状态下的data(即初始默认值)
  • 如果只想修改data的某个属性值,可以this[属性名] = this.$options.data()[属性名],如this.message = this.$options.data().message

48. vue渲染模板时怎么保留模板中的HTML注释呢

<template comments>
  ...
</template>

49. style加scoped属性的用途和原理

  • 用途:防止全局同名CSS污染
  • 原理:在标签加上v-data-something属性,再在选择器时加上对应[v-data-something],即CSS属性选择器,以此完成类似作用域的选择方式

50.在vue项目中如何配置favicon

  • 将 favicon 图片放到 static 文件夹下

  • 然后在 index.html 中添加:

<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico">

51. babel-polyfill模块

Babel默认只转换新的JavaScript句法(syntax),而不转换新的API,比如Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise等全局对象,以及一些定义在全局对象上的方法(比如Object.assign)都不会转码。

举例来说,ES6在Array对象上新增了Array.from方法。Babel就不会转码这个方法。如果想让这个方法运行,必须使用babel-polyfill,为当前环境提供一个垫片。

52. 在vue事件中传入$event,使用e.target和e.currentTarget有什么区别

  • currentTarget: 事件绑定的元素
  • target: 鼠标触发的元素

53. vue怎么实现强制刷新组件

强制重新渲染

this.$forceUpdate()

强制重新刷新某组件

//模版上绑定key
<SomeComponent :key="theKey"/>
//选项里绑定data
data(){
  return{
      theKey:0
  }
}

//刷新key达到刷新组件的目的
theKey++;

54. vue给组件绑定自定义事件无效怎么解决

加入.native修饰符

55. vue的属性名与method的方法名一样时会发生什么问题

报错 "Method 'xxx' has already been defined as a data property"

键名优先级:props > data > methods,这三个不能重复命名

56. vue变量名如果以_、$开头的属性会发生什么问题

实例创建之后,可以通过 vm.$data 访问原始数据对象。Vue 实例也代理了 data 对象上所有的属性,因此访问 vm.a 等价于访问 vm.$data.a

_$ 开头的属性 不会 被 Vue 实例代理,因为它们可能和 Vue 内置的属性、API 方法冲突。可以使用 vm.$data._property 的方式访问这些属性。

57. vue项目本地开发完成后部署到服务器后报404

使用了history模式,而后端又没有进行相关资源配置。

58. vue的表单修饰符.lazy

在输入框中,v-model默认是在input事件中同步输入框的数据(除了输入法中文输入的情况),使用修饰符 .lazy 会转变为 change 事件中同步(类似懒加载):

<div id="app1"> 
    <input type="text" v-model.lazy="message"> 
    <p>{{message}}</p> 
</div> 
<script> 
    var app1 = new Vue({ el: '#app1', data: { message:'测试' } }); 
</script>

此时,message并不是实时更新的,而是在失焦或按回车时才更新。

59. vue为什么要求组件模板只能有一个根元素

diff算法要求,源码中patch.js中的patchVnode也是根据树状结构进行遍历

60. 在vue中使用this应该注意哪些问题

生命周期的钩子函数不能使用箭头函数,否者this不能指向vue实例

61. <template></template>有什么用

包裹嵌套其它元素,使元素具有区域性,自身具有三个特点:

  • 隐藏性:不会显示在页面中
  • 任意性:可以写在页面的任意地方
  • 无效性: 没有一个根元素包裹,任何HTML内容都是无效的

62. 组件中写name选项有什么作用

  • 项目使用keep-alive时,可搭配组件name进行缓存过滤
  • DOM做递归组件时需要调用自身name
  • vue-devtools调试工具里显示的组见名称是由vue中组件name决定的

63. prop是怎么做验证的

  • 单个类型就用Number等基础类型
  • 多个类型用数组
  • 必填的话设置require为true
  • 默认值的话设置default
  • 对象和数组设置默认用工厂函数
  • 自定义验证函数validator。

64. vue权限管理,按钮和路由

详细地址

65. 大型项目你该怎么划分结构和划分组件

  • views目录存放一级路由的组件,即视图组件
  • Components目录存放组件
  • Store存放vuex相关文件
  • Router目录存放路由相关文件
  • Untils目录存放工具js文件
  • API目录存放封装好的与后端交互的逻辑
  • Assets存放静态文件

66. vue3.0的新特性

链接

67. 高阶组件

68. vue-loader是什么

解析和转换 .vue 文件,提取出其中的逻辑代码 script、样式代码 style、以及 HTML 模版 template,再分别把它们交给对应的 Loader 去处理。

69. 怎么捕获组件vue的错误信息

//通过这个钩子来捕获
Vue.config.errorHandler = (err, vm, info) => {
    console.log('进来啦~');
}

export default {
    created() {
        let a = null;
        if(a.length > 1) {
            // ...
        }
    }
};

70.怎么配置跨域

vue.config.js

module.exports = {
  //相当于webpack-dev-server,  对本地服务器进行配置
  devSever: {
    proxy: {
      '/api': {
        target: ' http://localhost:3000 ', //需要跨域的目标url ,我这里是自己本地起的一个服务端口
        changeOrigin: true, // 将基于名称的虚拟托管网站的选项,如果不配置,请求会报404
        ws: true,
        pathRewrite: {
          ' ^/api ': '' //若请求的路径在目标url下但不在/api 下,则将其转换成空
        }
      }
    }
  }
}

例:

this.$axios.post('/api/list').then(res =>{
   console.log(res)
})

71. vue-router怎么配置404页面

设置 path: '*' , 并且放在最后一个

72. vue-router如何响应路由参数的变化

为什么要响应参数变化?

  • 切换路由,路由参数发生了变化,但是页面数据及时更新,需要强制刷新后才会变化。
  • 不同路由渲染相同的组件时(组件复用比销毁重新创建效率要高),在切换路由后,当前组件下的生命周期函数不会再被调用。

解决方案:

使用 watch 监听

watch: {
    $route(to, from){
        if(to != from) {
            console.log("监听到路由变化,做出相应的处理");
        }
    }
}

向 router-view 组件中添加 key

<router-view :key="$route.fullPath"></router-view>

$route.fullPath 是完成后解析的URL,包含其查询参数信息和hash完整路径

73. 切换到新路由时,面要滚动到顶部或保持原先的滚动位置

在路由实例中配置
scrollBehavior(ro,form,savedPosition){
//滚动到顶部
return {x:0,y:0}
//保持原先的滚动位置
return {selector:false}
}

74. 路由懒加载

  1. require
{ path: '/home', name: 'home'component: resolve => require(['@/components/home'],resolve) }
  1. import
//打包后的js文件名为ImportFuncDemo
const Home = () => import(webpackChunkName: 'ImportFuncDemo', '@/components/home')
  1. require.ensure
//打包后的js文件名为demo
{ path: '/home', name: 'home', component: r => require.ensure([], () => r(require('@/components/home')), 'demo') },

75. MVVM

全称: Model-View-ViewModel , Model 表示数据模型层, view 表示视图层, ViewModel 是 View 和 Model 层的桥梁数据绑定viewModel 层并自动渲染到页面中,视图变化通知 viewModel 层更新数据。

76. vue中的事件绑定原理

事件绑定有几种?

  1. 原生的事件绑定,原生 dom 事件的绑定,采用的是 addEventListener 实现。

  2. 组件的事件绑定,组件绑定事件采用的是 $on 方法 。

  • 普通元素原生事件绑定在上是通过@click进行绑定的

  • 组件原生事件绑定是通过@click.native进行绑定的,组件中的nativeOn是等价于on的。

  • 组件自定义事件是通过@click绑定的,是通过 $on 方法来实现的,必须有$emit才可以触发。

解释下这2种的区别:

  • 原生:比如我们在原生便签上写一个事件<div @click="getData"></div>,直接触发的就是原生的点击事件

  • 组件: 现在我们自定义了个组件,想要组件上面写事件,<BtnGroup @click="getName" @click.native="getData"></BtnGroup>,这时候,要触发原生的点击事件getData,就需要使用修饰符.native,因为直接使用@click是接收来自子组件emit过来的事件getName,这样才不会冲突。

let compiler = require('vue-template-compiler'); // vue loader中的包
let r1 = compiler.compile('<div @click="fn()"></div>'); // 给普通标签绑定click事件
// 给组件绑定一个事件,有两种绑定方法
// 一种@click.native,这个绑定的就是原生事件
// 另一种@click,这个绑定的就是组件自定义事件
let r2 = compiler.compile('<my-component @click.native="fn" @click="fn1"></mycomponent>');
console.log(r1.render); // {on:{click}} 
console.log(r2.render); // {nativeOn:{click},on:{click}}
// 为什么组件要加native?因为组件最终会把nativeOn属性放到on的属性中去,这个on会单独处理
// 组件中的nativeOn 等价于 普通元素on,组件on会单独处理

详细连接

77. Vue为何采用异步渲染

Vue在更新DOM时是异步执行的,只要侦听到数据变化,将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更,如果同一个watcher多次触发,只会被推入队列一次,这种在缓冲时去除重复数据对于减少不必要计算和DOM操作是非常重要的.

然后,在下一个的事件循环tick中,Vue刷新队列并执行实际(已去重的)工作,Vue在内部对异步队列尝试使用原生的Promise.then、MutationObserver和setImmediate,如果执行环境不支持,则会采用setTimeout(fn, 0)代替。

描述

对于Vue为何采用异步渲染,简单来说就是为了提升性能,因为不采用异步更新,在每次更新数据都会对当前组件进行重新渲染,为了性能考虑,Vue会在本轮数据更新后,再去异步更新视图,举个例子,让我们在一个方法内重复更新一个值。

this.msg = 1;
this.msg = 2;
this.msg = 3;

事实上,我们真正想要的其实只是最后一次更新而已,也就是说前三次DOM更新都是可以省略的,我们只需要等所有状态都修改好了之后再进行渲染就可以减少一些性能损耗。

对于渲染方面的问题是很明确的,最终只渲染一次肯定比修改之后即渲染所耗费的性能少,在这里我们还需要考虑一下异步更新队列的相关问题,假设我们现在是进行了相关处理使得每次更新数据只进行一次真实DOM渲染,来让我们考虑异步更新队列的性能优化。

假设这里是同步更新队列,this.msg=1,大致会发生这些事

msg值更新 -> 触发setter -> 触发Watcher的update -> 重新调用 render -> 生成新的vdom -> dom-diff -> dom更新

这里的dom更新并不是渲染(即布局、绘制、合成等一系列步骤),而是更新内存中的DOM树结构,之后再运行this.msg=2,再重复上述步骤,之后的第3次更新同样会触发相同的流程,等开始渲染的时候,最新的DOM树中确实只会存在更新完成3,从这里来看,前2次对msg的操作以及Vue内部对它的处理都是无用的操作,可以进行优化处理。

如果是异步更新队列,会是下面的情况

运行this.msg=1,并不是立即进行上面的流程,而是将对msg有依赖的Watcher都保存在队列中,该队列可能这样[Watcher1, Watcher2...],当运行this.msg=2后,同样是将对msg有依赖的Watcher保存到队列中,Vue内部会做去重判断,这次操作后,可以认为队列数据没有发生变化,第3次更新也是上面的过程。

当然,你不可能只对msg有操作,你可能对该组件中的另一个属性也有操作,比如this.otherMsg=othermessage,同样会把对otherMsg有依赖的Watcher添加到异步更新队列中,因为有重复判断操作,这个Watcher也只会在队列中存在一次,本次异步任务执行结束后,会进入下一个任务执行流程,其实就是遍历异步更新队列中的每一个Watcher,触发其update,然后进行重新调用render -> new vdom -> dom-diff -> dom更新等流程,但是这种方式和同步更新队列相比,不管操作多少次msg,Vue在内部只会进行一次重新调用真实更新流程。

所以,对于异步更新队列不是节省了渲染成本,而是节省了Vue内部计算及DOM树操作的成本,不管采用哪种方式,渲染确实只有一次。

此外,组件内部实际使用VirtualDOM进行渲染,也就是说,组件内部其实是不关心哪个状态发生了变化,它只需要计算一次就可以得知哪些节点需要更新,也就是说,如果更改了N个状态,其实只需要发送一个信号就可以将DOM更新到最新,如果我们更新多个值。

this.msg = 1;
this.age = 2;
this.name = 3;

此处我们分三次修改了三种状态,但其实Vue只会渲染一次,因为VIrtualDOM只需要一次就可以将整个组件的DOM更新到最新,它根本不会关心这个更新的信号到底是从哪个具体的状态发出来的。

而为了达到这个目的,我们需要将渲染操作推迟到所有的状态都修改完成,为了做到这一点只需要将渲染操作推迟到本轮事件循环的最后或者下一轮事件循环,也就是说,只需要在本轮事件循环最后,等前面更新状态的语句都执行完之后执行一次渲染操作,它就可以无视前面各种更新状态的语法,无论前面写了多少条更新状态的语句,只在最后渲染一次就可以了。

将渲染推迟到本轮事件循环的最后执行渲染的时机会比推迟到下一轮快很多,所以Vue优先将渲染操作推迟到本轮事件循环的最后,如果执行环境不支持会降级到下一轮,Vue的变化侦测机制(setter)决定了它必然会在每次状态发生变化时都会发出渲染的信号,但Vue会在收到信号之后检查队列中是否已经存在这个任务,保证队列中不会有重复,如果队列中不存在则将渲染操作添加到队列中,之后通过异步的方式延迟执行队列中的所有渲染的操作并清空队列,当同一轮事件循环中反复修改状态时,并不会反复向队列中添加相同的渲染操作,所以我们在使用Vue时,修改状态后更新DOM都是异步的。

当数据变化后会调用notify方法,将watcher遍历,调用update方法通知watcher进行更新,这时候watcher并不会立即去执行,在update中会调用queueWatcher方法将watcher放到了一个队列里,在queueWatcher会根据watcher的进行去重,若多个属性依赖一个watcher,则如果队列中没有该watcher就会将该watcher添加到队列中,然后便会在$nextTick方法的执行队列中加入一个flushSchedulerQueue方法(这个方法将会触发在缓冲队列的所有回调的执行),然后将$nextTick方法的回调加入$nextTick方法中维护的执行队列,flushSchedulerQueue中开始会触发一个before的方法,其实就是beforeUpdate,然后watcher.run()才开始真正执行watcher,执行完页面就渲染完成,更新完成后会调用updated钩子。

$nextTick

在上文中谈到了对于Vue为何采用异步渲染,假如此时我们有一个需求,需要在页面渲染完成后取得页面的DOM元素,而由于渲染是异步的,我们不能直接在定义的方法中同步取得这个值的,于是就有了vm.$nextTick方法,Vue中$nextTick方法将回调延迟到下次DOM更新循环之后执行,也就是在下次DOM更新循环结束之后执行延迟回调,在修改数据之后立即使用这个方法,能够获取更新后的DOM。简单来说就是当数据更新时,在DOM中渲染完成后,执行回调函数。

通过一个简单的例子来演示$nextTick方法的作用,首先需要知道Vue在更新DOM时是异步执行的,也就是说在更新数据时其不会阻塞代码的执行,直到执行栈中代码执行结束之后,才开始执行异步任务队列的代码,所以在数据更新时,组件不会立即渲染,此时在获取到DOM结构后取得的值依然是旧的值,而在$nextTick方法中设定的回调函数会在组件渲染完成之后执行,取得DOM结构后取得的值便是新的值。

<!DOCTYPE html>
<html>
<head>
    <title>Vue</title>
</head>
<body>
    <div id="app"></div>
</body>
<script src="https://cdn.bootcss.com/vue/2.4.2/vue.js"></script>
<script type="text/javascript">
    var vm = new Vue({
        el: '#app',
        data: {
            msg: 'Vue'
        },
        template:`
            <div>
                <div ref="msgElement">{{msg}}</div>
                <button @click="updateMsg">updateMsg</button>
            </div>
        `,
        methods:{
            updateMsg: function(){
                this.msg = "Update";
                console.log("DOM未更新:", this.$refs.msgElement.innerHTML)
                this.$nextTick(() => {
                    console.log("DOM已更新:", this.$refs.msgElement.innerHTML)
                })
            }
        },
        
    })
</script>
</html>

异步机制#

Js是单线程的,其引入了同步阻塞与异步非阻塞的执行模式,在Js异步模式中维护了一个Event Loop,Event Loop是一个执行模型,在不同的地方有不同的实现,浏览器和NodeJS基于不同的技术实现了各自的Event Loop。浏览器的Event Loop是在HTML5的规范中明确定义,NodeJS的Event Loop是基于libuv实现的。

在浏览器中的Event Loop执行栈Execution Stack、后台线程Background Threads、宏队列Macrotask Queue、微队列Microtask Queue组成。

  • 执行栈就是在主线程执行同步任务数据结构,函数调用形成了一个由若干帧组成的栈。
  • 后台线程就是浏览器实现对于setTimeout、setInterval、XMLHttpRequest等等的执行线程。
  • 宏队列,一些异步任务的回调会依次进入宏队列,等待后续被调用,包括setTimeout、setInterval、setImmediate(Node)、requestAnimationFrame、UI rendering、I/O等操作。
  • 微队列,另一些异步任务的回调会依次进入微队列,等待后续调用,包括Promise、process.nextTick(Node)、Object.observe、MutationObserver等操作。

当Js执行时,进行如下流程:

  • 首先将执行栈中代码同步执行,将这些代码中异步任务加入后台线程中。
  • 执行栈中的同步代码执行完毕后,执行栈清空,并开始扫描微队列。
  • 取出微队列队首任务,放入执行栈中执行,此时微队列是进行了出队操作。
  • 当执行栈执行完成后,继续出队微队列任务并执行,直到微队列任务全部执行完毕。
  • 最后一个微队列任务出队并进入执行栈后微队列中任务为空,当执行栈任务完成后,开始扫面微队列为空,继续扫描宏队列任务,宏队列出队,放入执行栈中执行,执行完毕后继续扫描微队列为空则扫描宏队列,出队执行。不断往复...。

实例#

// Step 1
console.log(1);

// Step 2
setTimeout(() => {
  console.log(2);
  Promise.resolve().then(() => {
    console.log(3);
  });
}, 0);

// Step 3
new Promise((resolve, reject) => {
  console.log(4);
  resolve();
}).then(() => {
  console.log(5);
})

// Step 4
setTimeout(() => {
  console.log(6);
}, 0);

// Step 5
console.log(7);

// Step N
// ...

// Result
/*
  1
  4
  7
  5
  2
  3
  6
*/

分析#

在了解异步任务的执行队列后,回到中$nextTick方法,当用户数据更新时,Vue将会维护一个缓冲队列,对于所有的更新数据将要进行的组件渲染与DOM操作进行一定的策略处理后加入缓冲队列,然后便会在$nextTick方法的执行队列中加入一个flushSchedulerQueue方法(这个方法将会触发在缓冲队列的所有回调的执行),然后将$nextTick方法的回调加入$nextTick方法中维护的执行队列,在异步挂载的执行队列触发时就会首先会首先执行flushSchedulerQueue方法来处理DOM渲染的任务,然后再去执行$nextTick方法构建的任务,这样就可以实现在$nextTick方法中取得已渲染完成的DOM结构。

在测试的过程中发现了一个很有意思的现象,在上述例子中的加入两个按钮,在点击updateMsg按钮的结果是3 2 1,点击updateMsgTest按钮的运行结果是2 3 1。

<!DOCTYPE html>
<html>
<head>
    <title>Vue</title>
</head>
<body>
    <div id="app"></div>
</body>
<script src="https://cdn.bootcss.com/vue/2.4.2/vue.js"></script>
<script type="text/javascript">
    var vm = new Vue({
        el: '#app',
        data: {
            msg: 'Vue'
        },
        template:`
            <div>
                <div ref="msgElement">{{msg}}</div>
                <button @click="updateMsg">updateMsg</button>
                <button @click="updateMsgTest">updateMsgTest</button>
            </div>
        `,
        methods:{
            updateMsg: function(){
                this.msg = "Update";
                setTimeout(() => console.log(1))
                Promise.resolve().then(() => console.log(2))
                this.$nextTick(() => {
                    console.log(3)
                })
            },
            updateMsgTest: function(){
                setTimeout(() => console.log(1))
                Promise.resolve().then(() => console.log(2))
                this.$nextTick(() => {
                    console.log(3)
                })
            }
        },
        
    })
</script>
</html>

这里假设运行环境中Promise对象是完全支持的,那么使用setTimeout是宏队列在最后执行这个是没有异议的,但是使用$nextTick方法以及自行定义的Promise实例是有执行顺序的问题的,虽然都是微队列任务,但是在Vue中具体实现的原因导致了执行顺序可能会有所不同,首先直接看一下$nextTick方法的源码,关键地方添加了注释,请注意这是Vue2.4.2版本的源码,在后期$nextTick方法可能有所变更。

/**
 * Defer a task to execute it asynchronously.
 */
var nextTick = (function () {
  // 闭包 内部变量
  var callbacks = []; // 执行队列
  var pending = false; // 标识,用以判断在某个事件循环中是否为第一次加入,第一次加入的时候才触发异步执行的队列挂载
  var timerFunc; // 以何种方法执行挂载异步执行队列,这里假设Promise是完全支持的

  function nextTickHandler () { // 异步挂载的执行任务,触发时就已经正式准备开始执行异步任务了
    pending = false; // 标识置false
    var copies = callbacks.slice(0); // 创建副本
    callbacks.length = 0; // 执行队列置空
    for (var i = 0; i < copies.length; i++) {
      copies[i](); // 执行
    }
  }
   
   // 如果支持promise
  if (typeof Promise !== 'undefined' && isNative(Promise)) {
    var p = Promise.resolve();
    var logError = function (err) { console.error(err); };
    timerFunc = function () {
      p.then(nextTickHandler).catch(logError); // 挂载异步任务队列
      if (isIOS) { setTimeout(noop); }
    };
  } else if (typeof MutationObserver !== 'undefined' && (
    isNative(MutationObserver) ||
    MutationObserver.toString() === '[object MutationObserverConstructor]'
  )) {
    var counter = 1;
    var observer = new MutationObserver(nextTickHandler);
    var textNode = document.createTextNode(String(counter));
    observer.observe(textNode, {
      characterData: true
    });
    timerFunc = function () {
      counter = (counter + 1) % 2;
      textNode.data = String(counter);
    };
  } else {
    // fallback to setTimeout
    /* istanbul ignore next */
    timerFunc = function () {
      setTimeout(nextTickHandler, 0);
    };
  }

  return function queueNextTick (cb, ctx) { // nextTick方法真正导出的方法
    var _resolve;
    callbacks.push(function () { // 添加到执行队列中 并加入异常处理
      if (cb) {
        try {
          cb.call(ctx);
        } catch (e) {
          handleError(e, ctx, 'nextTick');
        }
      } else if (_resolve) {
        _resolve(ctx);
      }
    });
    //判断在当前事件循环中是否为第一次加入,若是第一次加入则置标识为true并执行timerFunc函数用以挂载执行队列到Promise
    // 这个标识在执行队列中的任务将要执行时便置为false并创建执行队列的副本去运行执行队列中的任务,参见nextTickHandler函数的实现
    // 在当前事件循环中置标识true并挂载,然后再次调用nextTick方法时只是将任务加入到执行队列中,直到挂载的异步任务触发,便置标识为false然后执行任务,再次调用nextTick方法时就是同样的执行方式然后不断如此往复
    if (!pending) { 
      pending = true;
      timerFunc();
    }
    if (!cb && typeof Promise !== 'undefined') {
      return new Promise(function (resolve, reject) {
        _resolve = resolve;
      })
    }
  }
})();

回到刚才提出的问题上,在更新DOM操作时会先触发$nextTick方法的回调,解决这个问题的关键在于谁先将异步任务挂载到Promise对象上。

首先对有数据更新的updateMsg按钮触发的方法进行debug,断点设置在Vue.js的715行,版本为2.4.2,在查看调用栈以及传入的参数时可以观察到第一次执行$nextTick方法的其实是由于数据更新而调用的nextTick(flushSchedulerQueue)语句,也就是说在执行this.msg = "Update";的时候就已经触发了第一次的$nextTick方法,此时在$nextTick方法中的任务队列会首先将flushSchedulerQueue方法加入队列并挂载$nextTick方法的执行队列到Promise对象上,然后才是自行自定义的Promise.resolve().then(() => console.log(2))语句的挂载,当执行微任务队列中的任务时,首先会执行第一个挂载到Promise的任务,此时这个任务是运行执行队列,这个队列中有两个方法,首先会运行flushSchedulerQueue方法去触发组件的DOM渲染操作,然后再执行console.log(3),然后执行第二个微队列的任务也就是() => console.log(2),此时微任务队列清空,然后再去宏任务队列执行console.log(1)。

接下来对于没有数据更新的updateMsgTest按钮触发的方法进行debug,断点设置在同样的位置,此时没有数据更新,那么第一次触发$nextTick方法的是自行定义的回调函数,那么此时$nextTick方法的执行队列才会被挂载到Promise对象上,很显然在此之前自行定义的输出2的Promise回调已经被挂载,那么对于这个按钮绑定的方法的执行流程便是首先执行console.log(2),然后执行$nextTick方法闭包的执行队列,此时执行队列中只有一个回调函数console.log(3),此时微任务队列清空,然后再去宏任务队列执行console.log(1)。

简单来说就是谁先挂载Promise对象的问题,在调用$nextTick方法时就会将其闭包内部维护的执行队列挂载到Promise对象,在数据更新时Vue内部首先就会执行$nextTick方法,之后便将执行队列挂载到了Promise对象上,其实在明白Js的Event Loop模型后,将数据更新也看做一个$nextTick方法的调用,并且明白$nextTick方法会一次性执行所有推入的回调,就可以明白其执行顺序的问题了,下面是一个关于$nextTick方法的最小化的DEMO。

var nextTick = (function(){

    var pending = false;
    const callback = [];
    var p = Promise.resolve();

    var handler = function(){
        pending = true;
        callback.forEach(fn => fn());
    }

    var timerFunc = function(){
        p.then(handler);
    }

    return function queueNextTick(fn){
        callback.push(() => fn());
        if(!pending){
            pending = true;
            timerFunc();
        }
    }

})();


(function(){
    nextTick(() => console.log("触发DOM渲染队列的方法")); // 注释 / 取消注释 来查看效果
    setTimeout(() => console.log(1))
    Promise.resolve().then(() => console.log(2))
    nextTick(() => {
        console.log(3)
    })
})();