Skip to content

`Vue 中 v-html 会导致哪些问题 ​

  • 可能会导致 xss 攻击
  • v-html 会替换掉标签内部的子元素
js
let template = require('vue-template-compiler');
let r = template.compile(`<div v-html="'<span>hello</span>'"></div>`)

// with(this){return _c('div',{domProps: {"innerHTML":_s('<span>hello</span>')}})}
console.log(r.render);

// _c 定义在core/instance/render.js
// _s 定义在core/instance/render-helpers/index,js
if (key === 'textContent' || key === 'innerHTML') {
    if (vnode.children) vnode.children.length = 0
    if (cur === oldProps[key]) continue // #6601 work around Chrome version <= 55 bug where single textNode // replaced by innerHTML/textContent retains its parentNode property
    if (elm.childNodes.length === 1) {
        elm.removeChild(elm.childNodes[0])
    }
}
let template = require('vue-template-compiler');
let r = template.compile(`<div v-html="'<span>hello</span>'"></div>`)

// with(this){return _c('div',{domProps: {"innerHTML":_s('<span>hello</span>')}})}
console.log(r.render);

// _c 定义在core/instance/render.js
// _s 定义在core/instance/render-helpers/index,js
if (key === 'textContent' || key === 'innerHTML') {
    if (vnode.children) vnode.children.length = 0
    if (cur === oldProps[key]) continue // #6601 work around Chrome version <= 55 bug where single textNode // replaced by innerHTML/textContent retains its parentNode property
    if (elm.childNodes.length === 1) {
        elm.removeChild(elm.childNodes[0])
    }
}

说下$attrs和$listeners 的使用场景 ​

API 考察,但$attrs和$listeners是比较少用的边界知识,而且vue3有变化,$listeners已经移除,还是有细节可说的

体验

一个包含组件透传属性的对象

html
<template>
  <child-component v-bind="$attrs">将非属性特性透传给内部的子组件</child-component>
</template>
<template>
  <child-component v-bind="$attrs">将非属性特性透传给内部的子组件</child-component>
</template>

回答范例

  • 我们可能会有一些属性和事件没有在props中定义,这类称为非属性特性,结合v-bind指令可以直接透传给内部的子组件。
  • 这类“属性透传”常常用于包装高阶组件时往内部传递属性,常用于爷孙组件之间传参。比如我在扩展 A 组件时创建了组件 B 组件,然后在 C 组件中使用 B,此时传递给 C 的属性中只有props里面声明的属性是给 B 使用的,其他的都是 A 需要的,此时就可以利用v-bind="$attrs"透传下去。
  • 最常见用法是结合v-bind做展开;$attrs本身不是响应式的,除非访问的属性本身是响应式对象。
  • vue2中使用listeners获取事件,vue3中已移除,均合并到attrs中,使用起来更简单了

原理

查看透传属性foo和普通属性bar,发现vnode结构完全相同,这说明vue3中将分辨两者工作由框架完成而非用户指定:

html
<template>
  <h1>{{ msg }}</h1>
  <comp foo="foo" bar="bar" />
</template>
<template>
  <div>{{$attrs.foo}} {{bar}}</div>
</template>

<script setup>
  defineProps({
    bar: String,
  })
</script>

_createVNode(Comp, { foo: "foo", bar: "bar" })
<template>
  <h1>{{ msg }}</h1>
  <comp foo="foo" bar="bar" />
</template>
<template>
  <div>{{$attrs.foo}} {{bar}}</div>
</template>

<script setup>
  defineProps({
    bar: String,
  })
</script>

_createVNode(Comp, { foo: "foo", bar: "bar" })

在 Vue 中使用插件的步骤 ​

  • 采用ES6的import ... from ...语法或CommonJS的require()方法引入插件
  • 使用全局方法Vue.use( plugin )使用插件,可以传入一个选项对象Vue.use(MyPlugin, { someOption: true })

vue-cli 工程技术集合介绍 ​

构建的 vue-cli 工程都到了哪些技术,它们的作用分别是什么 ​

  • vue.js:vue-cli工程的核心,主要特点是 双向数据绑定 和 组件系统。
  • vue-router:vue官方推荐使用的路由框架。
  • vuex:专为 Vue.js 应用项目开发的状态管理器,主要用于维护vue组件间共用的一些 变量 和 方法。
  • axios( 或者 fetch 、ajax ):用于发起 GET 、或 POST 等 http请求,基于 Promise 设计。
  • vuex等:一个专为vue设计的移动端 UI 组件库。
  • 创建一个emit.js文件,用于vue事件机制的管理。
  • webpack:模块加载和vue-cli工程打包器。

vue-cli 工程常用的 npm 命令有哪些 ​

  • 下载 node_modules 资源包的命令:
text
npm install
npm install
  • 启动 vue-cli 开发环境的 npm 命令:
text
npm run dev
npm run dev
  • vue-cli 生成 生产环境部署资源 的 npm命令:
text
npm run build
npm run build
  • 用于查看 vue-cli 生产环境部署资源文件大小的 npm命令:
text
npm run build --report
npm run build --report

在浏览器上自动弹出一个 展示 vue-cli 工程打包后 app.js、manifest.js、vendor.js 文件里面所包含代码的页面。可以具此优化 vue-cli 生产环境部署的静态资源,提升 页面 的加载速度

请说出 vue cli 项目中 src 目录每个文件夹和文件的用法 ​

  • assets文件夹是放静态资源;
  • components是放组件;
  • router是定义路由相关的配置;
  • view视图;
  • app.vue是一个应用主组件;
  • main.js是入口文件

delete 和 Vue.delete 删除数组的区别? ​

  • delete只是被删除的元素变成了 empty/undefined 其他的元素的键值还是不变。
  • Vue.delete直接删除了数组 改变了数组的键值。
js
var a = [1, 2, 3, 4]
var b = [1, 2, 3, 4]
delete a[0]
console.log(a) //[empty,2,3,4]
this.$delete(b, 0)
console.log(b) //[2,3,4]
var a = [1, 2, 3, 4]
var b = [1, 2, 3, 4]
delete a[0]
console.log(a) //[empty,2,3,4]
this.$delete(b, 0)
console.log(b) //[2,3,4]

v-on 可以监听多个方法吗? ​

可以监听多个方法

html
<input type="text" :value="name" @input="onInput" @focus="onFocus" @blur="onBlur" />
<input type="text" :value="name" @input="onInput" @focus="onFocus" @blur="onBlur" />

v-on 常用修饰符

  • .stop 该修饰符将阻止事件向上冒泡。同理于调用 event.stopPropagation() 方法
  • .prevent 该修饰符会阻止当前事件的默认行为。同理于调用 event.preventDefault() 方法
  • .self 该指令只当事件是从事件绑定的元素本身触发时才触发回调
  • .once 该修饰符表示绑定的事件只会被触发一次

v-once 的使用场景有哪些 ​

分析

v-once是Vue中内置指令,很有用的API,在优化方面经常会用到

体验

仅渲染元素和组件一次,并且跳过未来更新

html
<!-- single element -->
<span v-once>This will never change: {{msg}}</span>
<!-- the element have children -->
<div v-once>
  <h1>comment</h1>
  <p>{{msg}}</p>
</div>
<!-- component -->
<my-component v-once :comment="msg"></my-component>
<!-- `v-for` directive -->
<ul>
  <li v-for="i in list" v-once>{{i}}</li>
</ul>
<!-- single element -->
<span v-once>This will never change: {{msg}}</span>
<!-- the element have children -->
<div v-once>
  <h1>comment</h1>
  <p>{{msg}}</p>
</div>
<!-- component -->
<my-component v-once :comment="msg"></my-component>
<!-- `v-for` directive -->
<ul>
  <li v-for="i in list" v-once>{{i}}</li>
</ul>

回答范例

  • v-once是vue的内置指令,作用是仅渲染指定组件或元素一次,并跳过未来对其更新
  • 如果我们有一些元素或者组件在初始化渲染之后不再需要变化,这种情况下适合使用v-once,这样哪怕这些数据变化,vue也会跳过更新,是一种代码优化手段
  • 我们只需要作用的组件或元素上加上v-once即可
  • vue3.2之后,又增加了v-memo指令,可以有条件缓存部分模板并控制它们的更新,可以说控制力更强了
  • 编译器发现元素上面有v-once时,会将首次计算结果存入缓存对象,组件再次渲染时就会从缓存获取,从而避免再次计算

原理

下面例子使用了v-once:

html
<script setup>
  import { ref } from 'vue'

  const msg = ref('Hello World!')
</script>

<template>
  <h1 v-once>{{ msg }}</h1>
  <input v-model="msg" />
</template>
<script setup>
  import { ref } from 'vue'

  const msg = ref('Hello World!')
</script>

<template>
  <h1 v-once>{{ msg }}</h1>
  <input v-model="msg" />
</template>

我们发现v-once出现后,编译器会缓存作用元素或组件,从而避免以后更新时重新计算这一部分:

js
// ...
return (_ctx, _cache) => {
  return (_openBlock(), _createElementBlock(_Fragment, null, [
    // 从缓存获取vnode
    _cache[0] || (
      _setBlockTracking(-1),
      _cache[0] = _createElementVNode("h1", null, [
        _createTextVNode(_toDisplayString(msg.value), 1 /* TEXT */)
      ]),
      _setBlockTracking(1),
      _cache[0]
    ),
// ...
// ...
return (_ctx, _cache) => {
  return (_openBlock(), _createElementBlock(_Fragment, null, [
    // 从缓存获取vnode
    _cache[0] || (
      _setBlockTracking(-1),
      _cache[0] = _createElementVNode("h1", null, [
        _createTextVNode(_toDisplayString(msg.value), 1 /* TEXT */)
      ]),
      _setBlockTracking(1),
      _cache[0]
    ),
// ...

scoped 样式穿透 ​

scoped虽然避免了组件间样式污染,但是很多时候我们需要修改组件中的某个样式,但是又不想去除scoped属性

  1. 使用/deep/
html
<!-- Parent -->
<template>
  <div class="wrap">
    <Child />
  </div>
</template>

<style lang="scss" scoped>
  .wrap /deep/ .box {
    background: red;
  }
</style>

<!-- Child -->
<template>
  <div class="box"></div>
</template>
<!-- Parent -->
<template>
  <div class="wrap">
    <Child />
  </div>
</template>

<style lang="scss" scoped>
  .wrap /deep/ .box {
    background: red;
  }
</style>

<!-- Child -->
<template>
  <div class="box"></div>
</template>
  1. 使用两个style标签
html
<!-- Parent -->
<template>
  <div class="wrap">
    <Child />
  </div>
</template>

<style lang="scss" scoped>
  /* 其他样式 */
</style>
<style lang="scss">
  .wrap .box {
    background: red;
  }
</style>

<!-- Child -->
<template>
  <div class="box"></div>
</template>
<!-- Parent -->
<template>
  <div class="wrap">
    <Child />
  </div>
</template>

<style lang="scss" scoped>
  /* 其他样式 */
</style>
<style lang="scss">
  .wrap .box {
    background: red;
  }
</style>

<!-- Child -->
<template>
  <div class="box"></div>
</template>

Class 与 Style 如何动态绑定 ​

Class 可以通过对象语法和数组语法进行动态绑定

对象语法:

text
<div v-bind:class="{ active: isActive, 'text-danger': hasError }"></div>

data: {
  isActive: true,
  hasError: false
}
<div v-bind:class="{ active: isActive, 'text-danger': hasError }"></div>

data: {
  isActive: true,
  hasError: false
}

数组语法:

text
<div v-bind:class="[isActive ? activeClass : '', errorClass]"></div>

data: {
  activeClass: 'active',
  errorClass: 'text-danger'
}
<div v-bind:class="[isActive ? activeClass : '', errorClass]"></div>

data: {
  activeClass: 'active',
  errorClass: 'text-danger'
}

Style 也可以通过对象语法和数组语法进行动态绑定

对象语法:

text
<div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>

data: {
  activeColor: 'red',
  fontSize: 30
}
<div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>

data: {
  activeColor: 'red',
  fontSize: 30
}

数组语法:

text
<div v-bind:style="[styleColor, styleSize]"></div>

data: {
  styleColor: {
     color: 'red'
   },
  styleSize:{
     fontSize:'23px'
  }
}
<div v-bind:style="[styleColor, styleSize]"></div>

data: {
  styleColor: {
     color: 'red'
   },
  styleSize:{
     fontSize:'23px'
  }
}

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

  • 考点: Vue的变化侦测原理
  • 前置知识: 依赖收集、虚拟DOM、响应式系统

根本原因是Vue与React的变化侦测方式有所不同

  • 当 React 知道发生变化后,会使用Virtual Dom Diff进行差异检测,但是很多组件实际上是肯定不会发生变化的,这个时候需要 shouldComponentUpdate 进行手动操作来减少diff,从而提高程序整体的性能
  • Vue在一开始就知道那个组件发生了变化,不需要手动控制diff,而组件内部采用的diff方式实际上是可以引入类似于shouldComponentUpdate相关生命周期的,但是通常合理大小的组件不会有过量的 diff,手动优化的价值有限,因此目前Vue并没有考虑引入shouldComponentUpdate这种手动优化的生命周期

SPA、SSR 的区别是什么 ​

我们现在编写的Vue、React和Angular应用大多数情况下都会在一个页面中,点击链接跳转页面通常是内容切换而非页面跳转,由于良好的用户体验逐渐成为主流的开发模式。但同时也会有首屏加载时间长,SEO不友好的问题,因此有了SSR,这也是为什么面试中会问到两者的区别

  1. SPA(Single Page Application)即单页面应用。一般也称为 客户端渲染(Client Side Render), 简称 CSR。SSR(Server Side Render)即 服务端渲染。一般也称为 多页面应用(Mulpile Page Application),简称 MPA
  2. SPA应用只会首次请求html文件,后续只需要请求JSON数据即可,因此用户体验更好,节约流量,服务端压力也较小。但是首屏加载的时间会变长,而且SEO不友好。为了解决以上缺点,就有了SSR方案,由于HTML内容在服务器一次性生成出来,首屏加载快,搜索引擎也可以很方便的抓取页面信息。但同时 SSR 方案也会有性能,开发受限等问题
  3. 在选择上,如果我们的应用存在首屏加载优化需求,SEO需求时,就可以考虑SSR
  4. 但并不是只有这一种替代方案,比如对一些不常变化的静态网站,SSR 反而浪费资源,我们可以考虑预渲染(prerender)方案。另外nuxt.js/next.js中给我们提供了SSG(Static Site Generate)静态网站生成方案也是很好的静态站点解决方案,结合一些CI手段,可以起到很好的优化效果,且能节约服务器资源

内容生成上的区别:

SSR

img

SPA

img

部署上的区别

img

vue-loader 是什么?它有什么作用? ​

回答范例

  1. vue-loader是用于处理单文件组件(SFC,Single-File Component)的webpack loader
  2. 因为有了vue-loader,我们就可以在项目中编写SFC格式的Vue组件,我们可以把代码分割为<template>、<script>和<style>,代码会异常清晰。结合其他loader我们还可以用Pug编写<template>,用SASS编写<style>,用TS编写<script>。我们的<style>还可以单独作用当前组件
  3. webpack打包时,会以loader的方式调用vue-loader
  4. vue-loader被执行时,它会对SFC中的每个语言块用单独的loader链处理。最后将这些单独的块装配成最终的组件模块

原理

vue-loader会调用@vue/compiler-sfc模块解析SFC源码为一个描述符(Descriptor),然后为每个语言块生成import代码,返回的代码类似下面

js
// source.vue被vue-loader处理之后返回的代码

// import the <template> block
import render from 'source.vue?vue&type=template'
// import the <script> block
import script from 'source.vue?vue&type=script'
export * from 'source.vue?vue&type=script'
// import <style> blocks
import 'source.vue?vue&type=style&index=1'

script.render = render
export default script
// source.vue被vue-loader处理之后返回的代码

// import the <template> block
import render from 'source.vue?vue&type=template'
// import the <script> block
import script from 'source.vue?vue&type=script'
export * from 'source.vue?vue&type=script'
// import <style> blocks
import 'source.vue?vue&type=style&index=1'

script.render = render
export default script

我们想要script块中的内容被作为js处理(当然如果是<script lang="ts">被作为ts理),这样我们想要webpack把配置中跟.js匹配的规则都应用到形如source.vue?vue&type=script的这个请求上。例如我们对所有*.js配置了babel-loader,这个规则将被克隆并应用到所在Vue SFC

js
import script from 'source.vue?vue&type=script
import script from 'source.vue?vue&type=script

将被展开为:

js
import script from 'babel-loader!vue-loader!source.vue?vue&type=script'
import script from 'babel-loader!vue-loader!source.vue?vue&type=script'

类似的,如果我们对.sass文件配置了style-loader + css-loader + sass-loader,对下面的代码

text
<style scoped lang="scss">
<style scoped lang="scss">

vue-loader将会返回给我们下面结果:

js
import 'source.vue?vue&type=style&index=1&scoped&lang=scss'
import 'source.vue?vue&type=style&index=1&scoped&lang=scss'

然后webpack会展开如下:

js
import 'style-loader!css-loader!sass-loader!vue-loader!source.vue?vue&type=style&index=1&scoped&lang=scss'
import 'style-loader!css-loader!sass-loader!vue-loader!source.vue?vue&type=style&index=1&scoped&lang=scss'
  • 当处理展开请求时,vue-loader将被再次调用。这次,loader将会关注那些有查询串的请求,且仅针对特定块,它会选中特定块内部的内容并传递给后面匹配的loader

  • 对于<script>块,处理到这就可以了,但是<template>和<style>

    还有一些额外任务要做,比如

    • 需要用 Vue 模板编译器编译template,从而得到render函数
  • 需要对 <style scoped>中的CSS做后处理(post-process),该操作在css-loader之后但在style-loader之前

实现上这些附加的loader需要被注入到已经展开的loader链上,最终的请求会像下面这样:

js
// <template lang="pug">
import 'vue-loader/template-loader!pug-loader!source.vue?vue&type=template'

// <style scoped lang="scss">
import 'style-loader!vue-loader/style-post-loader!css-loader!sass-loader!vue-loader!source.vue?vue&type=style&index=1&scoped&lang=scss'
// <template lang="pug">
import 'vue-loader/template-loader!pug-loader!source.vue?vue&type=template'

// <style scoped lang="scss">
import 'style-loader!vue-loader/style-post-loader!css-loader!sass-loader!vue-loader!source.vue?vue&type=style&index=1&scoped&lang=scss'

说说你对 slot 的理解?slot 使用场景有哪些 ​

slot 是什么 ​

在 HTML 中 slot 元素 ,作为 Web Components 技术套件的一部分,是 Web 组件内的一个占位符

该占位符可以在后期使用自己的标记语言填充

举个栗子

html
<template id="element-details-template">
  <slot name="element-name">Slot template</slot>
</template>
<element-details>
  <span slot="element-name">1</span>
</element-details>
<element-details>
  <span slot="element-name">2</span>
</element-details>
<template id="element-details-template">
  <slot name="element-name">Slot template</slot>
</template>
<element-details>
  <span slot="element-name">1</span>
</element-details>
<element-details>
  <span slot="element-name">2</span>
</element-details>

template不会展示到页面中,需要用先获取它的引用,然后添加到DOM中,

js
customElements.define(
  'element-details',
  class extends HTMLElement {
    constructor() {
      super()
      const template = document.getElementById('element-details-template').content
      const shadowRoot = this.attachShadow({ mode: 'open' }).appendChild(template.cloneNode(true))
    }
  },
)
customElements.define(
  'element-details',
  class extends HTMLElement {
    constructor() {
      super()
      const template = document.getElementById('element-details-template').content
      const shadowRoot = this.attachShadow({ mode: 'open' }).appendChild(template.cloneNode(true))
    }
  },
)

在Vue中的概念也是如此

Slot 艺名插槽,花名“占坑”,我们可以理解为solt在组件模板中占好了位置,当使用该组件标签时候,组件标签里面的内容就会自动填坑(替换组件模板中slot位置),作为承载分发内容的出口

slot 使用场景 ​

通过插槽可以让用户可以拓展组件,去更好地复用组件和对其做定制化处理

如果父组件在使用到一个复用组件的时候,获取这个组件在不同的地方有少量的更改,如果去重写组件是一件不明智的事情

通过slot插槽向组件内部指定位置传递内容,完成这个复用组件在不同场景的应用

比如布局组件、表格列、下拉选、弹框显示内容等

slot 分类 ​

slot可以分来以下三种:

  • 默认插槽
  • 具名插槽
  • 作用域插槽

1. 默认插槽

子组件用<slot>标签来确定渲染的位置,标签里面可以放DOM结构,当父组件使用的时候没有往插槽传入内容,标签内DOM结构就会显示在页面

父组件在使用的时候,直接在子组件的标签内写入内容即可

子组件Child.vue

html
<template>
  <slot>
    <p>插槽后备的内容</p>
  </slot>
</template>
<template>
  <slot>
    <p>插槽后备的内容</p>
  </slot>
</template>

父组件

html
<Child>
  <div>默认插槽</div>
</Child>
<Child>
  <div>默认插槽</div>
</Child>

2. 具名插槽

子组件用name属性来表示插槽的名字,不传为默认插槽

父组件中在使用时在默认插槽的基础上加上slot属性,值为子组件插槽name属性值

子组件Child.vue

html
<template>
  <slot>插槽后备的内容</slot>
  <slot name="content">插槽后备的内容</slot>
</template>
<template>
  <slot>插槽后备的内容</slot>
  <slot name="content">插槽后备的内容</slot>
</template>

父组件

html
<child>
  <template v-slot:default>具名插槽</template>
  <!-- 具名插槽⽤插槽名做参数 -->
  <template v-slot:content>内容...</template>
</child>
<child>
  <template v-slot:default>具名插槽</template>
  <!-- 具名插槽⽤插槽名做参数 -->
  <template v-slot:content>内容...</template>
</child>

3. 作用域插槽

子组件在作用域上绑定属性来将子组件的信息传给父组件使用,这些属性会被挂在父组件v-slot接受的对象上

父组件中在使用时通过v-slot:(简写:#)获取子组件的信息,在内容中使用

子组件Child.vue

html
<template>
  <slot name="footer" testProps="子组件的值">
    <h3>没传footer插槽</h3>
  </slot>
</template>
<template>
  <slot name="footer" testProps="子组件的值">
    <h3>没传footer插槽</h3>
  </slot>
</template>

父组件

html
<child>
  <!-- 把v-slot的值指定为作⽤域上下⽂对象 -->
  <template v-slot:default="slotProps">来⾃⼦组件数据:{{slotProps.testProps}}</template>
  <template #default="slotProps">来⾃⼦组件数据:{{slotProps.testProps}}</template>
</child>
<child>
  <!-- 把v-slot的值指定为作⽤域上下⽂对象 -->
  <template v-slot:default="slotProps">来⾃⼦组件数据:{{slotProps.testProps}}</template>
  <template #default="slotProps">来⾃⼦组件数据:{{slotProps.testProps}}</template>
</child>

小结:

  • v-slot属性只能在<template>上使用,但在只有默认插槽时可以在组件标签上使用
  • 默认插槽名为default,可以省略 default 直接写v-slot
  • 缩写为#时不能不写参数,写成#default
  • 可以通过解构获取v-slot={user},还可以重命名v-slot="{user: newName}"和定义默认值v-slot="{user = '默认值'}"

slot 原理分析 ​

slot本质上是返回VNode的函数,一般情况下,Vue中的组件要渲染到页面上需要经过template -> render function -> VNode -> DOM 过程,这里看看slot如何实现:

编写一个buttonCounter组件,使用匿名插槽

js
Vue.component('button-counter', {
  template: '<div> <slot>我是默认内容</slot></div>',
})
Vue.component('button-counter', {
  template: '<div> <slot>我是默认内容</slot></div>',
})

使用该组件

js
new Vue({
  el: '#app',
  template: '<button-counter><span>我是slot传入内容</span></button-counter>',
  components: { buttonCounter },
})
new Vue({
  el: '#app',
  template: '<button-counter><span>我是slot传入内容</span></button-counter>',
  components: { buttonCounter },
})

获取buttonCounter组件渲染函数

js
;(function anonymous() {
  with (this) {
    return _c('div', [_t('default', [_v('我是默认内容')])], 2)
  }
})
;(function anonymous() {
  with (this) {
    return _c('div', [_t('default', [_v('我是默认内容')])], 2)
  }
})

_v表示穿件普通文本节点,_t表示渲染插槽的函数

渲染插槽函数renderSlot(做了简化)

js
function renderSlot(name, fallback, props, bindObject) {
  // 得到渲染插槽内容的函数
  var scopedSlotFn = this.$scopedSlots[name]
  var nodes
  // 如果存在插槽渲染函数,则执行插槽渲染函数,生成nodes节点返回
  // 否则使用默认值
  nodes = scopedSlotFn(props) || fallback
  return nodes
}
function renderSlot(name, fallback, props, bindObject) {
  // 得到渲染插槽内容的函数
  var scopedSlotFn = this.$scopedSlots[name]
  var nodes
  // 如果存在插槽渲染函数,则执行插槽渲染函数,生成nodes节点返回
  // 否则使用默认值
  nodes = scopedSlotFn(props) || fallback
  return nodes
}

name属性表示定义插槽的名字,默认值为default,fallback表示子组件中的slot节点的默认值

关于this.$scopredSlots是什么,我们可以先看看vm.slot

js
function initRender (vm) {
  ...
  vm.$slots = resolveSlots(options._renderChildren, renderContext);
  ...
}



resolveSlots`函数会对`children`节点做归类和过滤处理,返回`slots
function resolveSlots (
    children,
    context
  ) {
    if (!children || !children.length) {
      return {}
    }
    var slots = {};
    for (var i = 0, l = children.length; i < l; i++) {
      var child = children[i];
      var data = child.data;
      // remove slot attribute if the node is resolved as a Vue slot node
      if (data && data.attrs && data.attrs.slot) {
        delete data.attrs.slot;
      }
      // named slots should only be respected if the vnode was rendered in the
      // same context.
      if ((child.context === context || child.fnContext === context) &&
        data && data.slot != null
      ) {
        // 如果slot存在(slot="header") 则拿对应的值作为key
        var name = data.slot;
        var slot = (slots[name] || (slots[name] = []));
        // 如果是tempalte元素 则把template的children添加进数组中,这也就是为什么你写的template标签并不会渲染成另一个标签到页面
        if (child.tag === 'template') {
          slot.push.apply(slot, child.children || []);
        } else {
          slot.push(child);
        }
      } else {
        // 如果没有就默认是default
        (slots.default || (slots.default = [])).push(child);
      }
    }
    // ignore slots that contains only whitespace
    for (var name$1 in slots) {
      if (slots[name$1].every(isWhitespace)) {
        delete slots[name$1];
      }
    }
    return slots
}



_render`渲染函数通过`normalizeScopedSlots`得到`vm.$scopedSlots
vm.$scopedSlots = normalizeScopedSlots(
  _parentVnode.data.scopedSlots,
  vm.$slots,
  vm.$scopedSlots
);
function initRender (vm) {
  ...
  vm.$slots = resolveSlots(options._renderChildren, renderContext);
  ...
}



resolveSlots`函数会对`children`节点做归类和过滤处理,返回`slots
function resolveSlots (
    children,
    context
  ) {
    if (!children || !children.length) {
      return {}
    }
    var slots = {};
    for (var i = 0, l = children.length; i < l; i++) {
      var child = children[i];
      var data = child.data;
      // remove slot attribute if the node is resolved as a Vue slot node
      if (data && data.attrs && data.attrs.slot) {
        delete data.attrs.slot;
      }
      // named slots should only be respected if the vnode was rendered in the
      // same context.
      if ((child.context === context || child.fnContext === context) &&
        data && data.slot != null
      ) {
        // 如果slot存在(slot="header") 则拿对应的值作为key
        var name = data.slot;
        var slot = (slots[name] || (slots[name] = []));
        // 如果是tempalte元素 则把template的children添加进数组中,这也就是为什么你写的template标签并不会渲染成另一个标签到页面
        if (child.tag === 'template') {
          slot.push.apply(slot, child.children || []);
        } else {
          slot.push(child);
        }
      } else {
        // 如果没有就默认是default
        (slots.default || (slots.default = [])).push(child);
      }
    }
    // ignore slots that contains only whitespace
    for (var name$1 in slots) {
      if (slots[name$1].every(isWhitespace)) {
        delete slots[name$1];
      }
    }
    return slots
}



_render`渲染函数通过`normalizeScopedSlots`得到`vm.$scopedSlots
vm.$scopedSlots = normalizeScopedSlots(
  _parentVnode.data.scopedSlots,
  vm.$slots,
  vm.$scopedSlots
);

作用域插槽中父组件能够得到子组件的值是因为在renderSlot的时候执行会传入props,也就是上述_t第三个参数,父组件则能够得到子组件传递过来的值

Vue.observable 你有了解过吗?说说看 ​

Observable 是什么 ​

Observable 翻译过来我们可以理解成可观察的

我们先来看一下其在Vue中的定义

Vue.observable,让一个对象变成响应式数据。Vue 内部会用它来处理 data 函数返回的对象

返回的对象可以直接用于渲染函数和计算属性内,并且会在发生变更时触发相应的更新。也可以作为最小化的跨组件状态存储器

js
Vue.observable({ count: 1 })
Vue.observable({ count: 1 })

其作用等同于

js
new vue({ count: 1 })
new vue({ count: 1 })

在 Vue 2.x 中,被传入的对象会直接被 Vue.observable 变更,它和被返回的对象是同一个对象

在 Vue 3.x 中,则会返回一个可响应的代理,而对源对象直接进行变更仍然是不可响应的

使用场景 ​

在非父子组件通信时,可以使用通常的bus或者使用vuex,但是实现的功能不是太复杂,而使用上面两个又有点繁琐。这时,observable就是一个很好的选择

创建一个js文件

js
// 引入vue
import Vue from 'vue
// 创建state对象,使用observable让state对象可响应
export let state = Vue.observable({
  name: '张三',
  'age': 38
})
// 创建对应的方法
export let mutations = {
  changeName(name) {
    state.name = name
  },
  setAge(age) {
    state.age = age
  }
}
// 引入vue
import Vue from 'vue
// 创建state对象,使用observable让state对象可响应
export let state = Vue.observable({
  name: '张三',
  'age': 38
})
// 创建对应的方法
export let mutations = {
  changeName(name) {
    state.name = name
  },
  setAge(age) {
    state.age = age
  }
}

在.vue文件中直接使用即可

js
<template>
  <div>
    姓名:{{ name }}
    年龄:{{ age }}
    <button @click="changeName('李四')">改变姓名</button>
    <button @click="setAge(18)">改变年龄</button>
  </div>
</template>
import { state, mutations } from '@/store
export default {
  // 在计算属性中拿到值
  computed: {
    name() {
      return state.name
    },
    age() {
      return state.age
    }
  },
  // 调用mutations里面的方法,更新数据
  methods: {
    changeName: mutations.changeName,
    setAge: mutations.setAge
  }
}
<template>
  <div>
    姓名:{{ name }}
    年龄:{{ age }}
    <button @click="changeName('李四')">改变姓名</button>
    <button @click="setAge(18)">改变年龄</button>
  </div>
</template>
import { state, mutations } from '@/store
export default {
  // 在计算属性中拿到值
  computed: {
    name() {
      return state.name
    },
    age() {
      return state.age
    }
  },
  // 调用mutations里面的方法,更新数据
  methods: {
    changeName: mutations.changeName,
    setAge: mutations.setAge
  }
}

原理分析 ​

源码位置:src\core\observer\index.js

js
export function observe(value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  // 判断是否存在__ob__响应式属性
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    // 实例化Observer响应式对象
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}
export function observe(value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  // 判断是否存在__ob__响应式属性
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    // 实例化Observer响应式对象
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

Observer类

js
export class Observer {
    value: any;
    dep: Dep;
    vmCount: number; // number of vms that have this object as root $data

    constructor (value: any) {
        this.value = value
        this.dep = new Dep()
        this.vmCount = 0
        def(value, '__ob__', this)
        if (Array.isArray(value)) {
            if (hasProto) {
                protoAugment(value, arrayMethods)
            } else {
                copyAugment(value, arrayMethods, arrayKeys)
            }
            this.observeArray(value)
        } else {
            // 实例化对象是一个对象,进入walk方法
            this.walk(value)
        }
}
export class Observer {
    value: any;
    dep: Dep;
    vmCount: number; // number of vms that have this object as root $data

    constructor (value: any) {
        this.value = value
        this.dep = new Dep()
        this.vmCount = 0
        def(value, '__ob__', this)
        if (Array.isArray(value)) {
            if (hasProto) {
                protoAugment(value, arrayMethods)
            } else {
                copyAugment(value, arrayMethods, arrayKeys)
            }
            this.observeArray(value)
        } else {
            // 实例化对象是一个对象,进入walk方法
            this.walk(value)
        }
}

walk函数

js
walk (obj: Object) {
    const keys = Object.keys(obj)
    // 遍历key,通过defineReactive创建响应式对象
    for (let i = 0; i < keys.length; i++) {
        defineReactive(obj, keys[i])
    }
}
walk (obj: Object) {
    const keys = Object.keys(obj)
    // 遍历key,通过defineReactive创建响应式对象
    for (let i = 0; i < keys.length; i++) {
        defineReactive(obj, keys[i])
    }
}

defineReactive方法

js
export function defineReactive(
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean,
) {
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }

  let childOb = !shallow && observe(val)
  // 接下来调用Object.defineProperty()给对象定义响应式属性
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter(newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      // 对观察者watchers进行通知,state就成了全局响应式对象
      dep.notify()
    },
  })
}
export function defineReactive(
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean,
) {
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }

  let childOb = !shallow && observe(val)
  // 接下来调用Object.defineProperty()给对象定义响应式属性
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter(newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      // 对观察者watchers进行通知,state就成了全局响应式对象
      dep.notify()
    },
  })
}

Vue 中的过滤器了解吗?过滤器的应用场景有哪些? ​

过滤器实质不改变原始数据,只是对数据进行加工处理后返回过滤后的数据再进行调用处理,我们也可以理解其为一个纯函数

Vue 允许你自定义过滤器,可被用于一些常见的文本格式化

ps: Vue3中已废弃filter

如何用 ​

vue 中的过滤器可以用在两个地方:双花括号插值和 v-bind 表达式,过滤器应该被添加在 JavaScript 表达式的尾部,由“管道”符号指示:

html
<!-- 在双花括号中 -->
{ message | capitalize }

<!-- 在 `v-bind` 中 -->
<div v-bind:id="rawId | formatId"></div>
<!-- 在双花括号中 -->
{ message | capitalize }

<!-- 在 `v-bind` 中 -->
<div v-bind:id="rawId | formatId"></div>

定义 filter ​

在组件的选项中定义本地的过滤器

js
filters: {
  capitalize: function (value) {
    if (!value) return ''
    value = value.toString()
    return value.charAt(0).toUpperCase() + value.slice(1)
  }
}
filters: {
  capitalize: function (value) {
    if (!value) return ''
    value = value.toString()
    return value.charAt(0).toUpperCase() + value.slice(1)
  }
}

定义全局过滤器:

js
Vue.filter('capitalize', function (value) {
  if (!value) return ''
  value = value.toString()
  return value.charAt(0).toUpperCase() + value.slice(1)
})

new Vue({
  // ...
})
Vue.filter('capitalize', function (value) {
  if (!value) return ''
  value = value.toString()
  return value.charAt(0).toUpperCase() + value.slice(1)
})

new Vue({
  // ...
})

注意:当全局过滤器和局部过滤器重名时,会采用局部过滤器

过滤器函数总接收表达式的值 (之前的操作链的结果) 作为第一个参数。在上述例子中,capitalize 过滤器函数将会收到 message 的值作为第一个参数

过滤器可以串联:

text
{ message | filterA | filterB }
{ message | filterA | filterB }

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

过滤器是 JavaScript函数,因此可以接收参数:

text
{{ message | filterA('arg1', arg2) }}
{{ message | filterA('arg1', arg2) }}

这里,filterA 被定义为接收三个参数的过滤器函数。

其中 message 的值作为第一个参数,普通字符串 'arg1' 作为第二个参数,表达式 arg2 的值作为第三个参数

举个例子:

html
<div id="app">
  <p>{{ msg | msgFormat('疯狂','--')}}</p>
</div>

<script>
  // 定义一个 Vue 全局的过滤器,名字叫做  msgFormat
  Vue.filter('msgFormat', function (msg, arg, arg2) {
    // 字符串的  replace 方法,第一个参数,除了可写一个 字符串之外,还可以定义一个正则
    return msg.replace(/单纯/g, arg + arg2)
  })
</script>
<div id="app">
  <p>{{ msg | msgFormat('疯狂','--')}}</p>
</div>

<script>
  // 定义一个 Vue 全局的过滤器,名字叫做  msgFormat
  Vue.filter('msgFormat', function (msg, arg, arg2) {
    // 字符串的  replace 方法,第一个参数,除了可写一个 字符串之外,还可以定义一个正则
    return msg.replace(/单纯/g, arg + arg2)
  })
</script>

小结:

  • 部过滤器优先于全局过滤器被调用
  • 一个表达式可以使用多个过滤器。过滤器之间需要用管道符“|”隔开。其执行顺序从左往右

[应用场景 ​

平时开发中,需要用到过滤器的地方有很多,比如单位转换、数字打点、文本格式化、时间格式化之类的等

比如我们要实现将30000 => 30,000,这时候我们就需要使用过滤器

js
Vue.filter('toThousandFilter', function (value) {
  if (!value) return ''
  value = value.toString()
  return .replace(str.indexOf('.') > -1 ? /(\d)(?=(\d{3})+\.)/g : /(\d)(?=(?:\d{3})+$)/g, '$1,')
})
Vue.filter('toThousandFilter', function (value) {
  if (!value) return ''
  value = value.toString()
  return .replace(str.indexOf('.') > -1 ? /(\d)(?=(\d{3})+\.)/g : /(\d)(?=(?:\d{3})+$)/g, '$1,')
})

原理分析 ​

使用过滤器

js
{
  {
    message | capitalize
  }
}
{
  {
    message | capitalize
  }
}

在模板编译阶段过滤器表达式将会被编译为过滤器函数,主要是用过parseFilters,我们放到最后讲

js
_s(_f('filterFormat')(message))
_s(_f('filterFormat')(message))

首先分析一下_f:

_f 函数全名是:resolveFilter,这个函数的作用是从this.$options.filters中找出注册的过滤器并返回

js
// 变为
this.$options.filters['filterFormat'](message) // message为参数
// 变为
this.$options.filters['filterFormat'](message) // message为参数

关于resolveFilter

js
import { indentity, resolveAsset } from 'core/util/index'

export function resolveFilter(id) {
  return resolveAsset(this.$options, 'filters', id, true) || identity
}
import { indentity, resolveAsset } from 'core/util/index'

export function resolveFilter(id) {
  return resolveAsset(this.$options, 'filters', id, true) || identity
}

内部直接调用resolveAsset,将option对象,类型,过滤器id,以及一个触发警告的标志作为参数传递,如果找到,则返回过滤器;

resolveAsset的代码如下:

js
export function resolveAsset(options, type, id, warnMissing) {
  // 因为我们找的是过滤器,所以在 resolveFilter函数中调用时 type 的值直接给的 'filters',实际这个函数还可以拿到其他很多东西
  if (typeof id !== 'string') {
    // 判断传递的过滤器id 是不是字符串,不是则直接返回
    return
  }
  const assets = options[type] // 将我们注册的所有过滤器保存在变量中
  // 接下来的逻辑便是判断id是否在assets中存在,即进行匹配
  if (hasOwn(assets, id)) return assets[id] // 如找到,直接返回过滤器
  // 没有找到,代码继续执行
  const camelizedId = camelize(id) // 万一你是驼峰的呢
  if (hasOwn(assets, camelizedId)) return assets[camelizedId]
  // 没找到,继续执行
  const PascalCaseId = capitalize(camelizedId) // 万一你是首字母大写的驼峰呢
  if (hasOwn(assets, PascalCaseId)) return assets[PascalCaseId]
  // 如果还是没找到,则检查原型链(即访问属性)
  const result = assets[id] || assets[camelizedId] || assets[PascalCaseId]
  // 如果依然没找到,则在非生产环境的控制台打印警告
  if (process.env.NODE_ENV !== 'production' && warnMissing && !result) {
    warn('Failed to resolve ' + type.slice(0, -1) + ': ' + id, options)
  }
  // 无论是否找到,都返回查找结果
  return result
}
export function resolveAsset(options, type, id, warnMissing) {
  // 因为我们找的是过滤器,所以在 resolveFilter函数中调用时 type 的值直接给的 'filters',实际这个函数还可以拿到其他很多东西
  if (typeof id !== 'string') {
    // 判断传递的过滤器id 是不是字符串,不是则直接返回
    return
  }
  const assets = options[type] // 将我们注册的所有过滤器保存在变量中
  // 接下来的逻辑便是判断id是否在assets中存在,即进行匹配
  if (hasOwn(assets, id)) return assets[id] // 如找到,直接返回过滤器
  // 没有找到,代码继续执行
  const camelizedId = camelize(id) // 万一你是驼峰的呢
  if (hasOwn(assets, camelizedId)) return assets[camelizedId]
  // 没找到,继续执行
  const PascalCaseId = capitalize(camelizedId) // 万一你是首字母大写的驼峰呢
  if (hasOwn(assets, PascalCaseId)) return assets[PascalCaseId]
  // 如果还是没找到,则检查原型链(即访问属性)
  const result = assets[id] || assets[camelizedId] || assets[PascalCaseId]
  // 如果依然没找到,则在非生产环境的控制台打印警告
  if (process.env.NODE_ENV !== 'production' && warnMissing && !result) {
    warn('Failed to resolve ' + type.slice(0, -1) + ': ' + id, options)
  }
  // 无论是否找到,都返回查找结果
  return result
}

下面再来分析一下_s:

_s 函数的全称是 toString,过滤器处理后的结果会当作参数传递给 toString函数,最终 toString函数执行后的结果会保存到Vnode中的 text 属性中,渲染到视图中

js
function toString(value) {
  return value == null
    ? ''
    : typeof value === 'object'
    ? JSON.stringify(value, null, 2) // JSON.stringify()第三个参数可用来控制字符串里面的间距
    : String(value)
}
function toString(value) {
  return value == null
    ? ''
    : typeof value === 'object'
    ? JSON.stringify(value, null, 2) // JSON.stringify()第三个参数可用来控制字符串里面的间距
    : String(value)
}

最后,在分析下parseFilters,在模板编译阶段使用该函数阶段将模板过滤器解析为过滤器函数调用表达式

js
function parseFilters(filter) {
  let filters = filter.split('|')
  let expression = filters.shift().trim() // shift()删除数组第一个元素并将其返回,该方法会更改原数组
  let i
  if (filters) {
    for (i = 0; i < filters.length; i++) {
      experssion = warpFilter(expression, filters[i].trim()) // 这里传进去的expression实际上是管道符号前面的字符串,即过滤器的第一个参数
    }
  }
  return expression
}
// warpFilter函数实现
function warpFilter(exp, filter) {
  // 首先判断过滤器是否有其他参数
  const i = filter.indexof('(')
  if (i < 0) {
    // 不含其他参数,直接进行过滤器表达式字符串的拼接
    return `_f("${filter}")(${exp})`
  } else {
    const name = filter.slice(0, i) // 过滤器名称
    const args = filter.slice(i + 1) // 参数,但还多了 ‘)’
    return `_f('${name}')(${exp},${args}` // 注意这一步少给了一个 ')'
  }
}
function parseFilters(filter) {
  let filters = filter.split('|')
  let expression = filters.shift().trim() // shift()删除数组第一个元素并将其返回,该方法会更改原数组
  let i
  if (filters) {
    for (i = 0; i < filters.length; i++) {
      experssion = warpFilter(expression, filters[i].trim()) // 这里传进去的expression实际上是管道符号前面的字符串,即过滤器的第一个参数
    }
  }
  return expression
}
// warpFilter函数实现
function warpFilter(exp, filter) {
  // 首先判断过滤器是否有其他参数
  const i = filter.indexof('(')
  if (i < 0) {
    // 不含其他参数,直接进行过滤器表达式字符串的拼接
    return `_f("${filter}")(${exp})`
  } else {
    const name = filter.slice(0, i) // 过滤器名称
    const args = filter.slice(i + 1) // 参数,但还多了 ‘)’
    return `_f('${name}')(${exp},${args}` // 注意这一步少给了一个 ')'
  }
}

小结:

  • 在编译阶段通过parseFilters将过滤器编译成函数调用(串联过滤器则是一个嵌套的函数调用,前一个过滤器执行的结果是后一个过滤器函数的参数)
  • 编译后通过调用resolveFilter函数找到对应过滤器并返回结果
  • 执行结果作为参数传递给toString函数,而toString执行后,其结果会保存在Vnode的text属性中,渲染到视图

50 Vue 项目中有封装过 axios 吗?主要是封装哪方面的? ​

axios 是什么 ​

axios 是一个轻量的 HTTP客户端

基于 XMLHttpRequest 服务来执行 HTTP 请求,支持丰富的配置,支持 Promise,支持浏览器端和 Node.js 端。自Vue2.0 起,尤大宣布取消对 vue-resource 的官方推荐,转而推荐 axios。现在 axios 已经成为大部分 Vue 开发者的首选

特性

  • 从浏览器中创建 XMLHttpRequests
  • 从 node.js 创建 http请求
  • 支持 Promise API
  • 拦截请求和响应
  • 转换请求数据和响应数据
  • 取消请求
  • 自动转换JSON 数据
  • 客户端支持防御XSRF

基本使用

安装

js
// 项目中安装
npm install axios --S
// cdn 引入
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
// 项目中安装
npm install axios --S
// cdn 引入
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>

导入

js
import axios from 'axios'
import axios from 'axios'

发送请求

js
axios({
  url: 'xxx', // 设置请求的地址
  method: 'GET', // 设置请求方法
  params: {
    // get请求使用params进行参数凭借,如果是post请求用data
    type: '',
    page: 1,
  },
}).then((res) => {
  // res为后端返回的数据
  console.log(res)
})
axios({
  url: 'xxx', // 设置请求的地址
  method: 'GET', // 设置请求方法
  params: {
    // get请求使用params进行参数凭借,如果是post请求用data
    type: '',
    page: 1,
  },
}).then((res) => {
  // res为后端返回的数据
  console.log(res)
})

并发请求axios.all([])

js
function getUserAccount() {
  return axios.get('/user/12345')
}

function getUserPermissions() {
  return axios.get('/user/12345/permissions')
}

axios.all([getUserAccount(), getUserPermissions()]).then(
  axios.spread(function (res1, res2) {
    // res1第一个请求的返回的内容,res2第二个请求返回的内容
    // 两个请求都执行完成才会执行
  }),
)
function getUserAccount() {
  return axios.get('/user/12345')
}

function getUserPermissions() {
  return axios.get('/user/12345/permissions')
}

axios.all([getUserAccount(), getUserPermissions()]).then(
  axios.spread(function (res1, res2) {
    // res1第一个请求的返回的内容,res2第二个请求返回的内容
    // 两个请求都执行完成才会执行
  }),
)

为什么要封装 ​

axios 的 API 很友好,你完全可以很轻松地在项目中直接使用。

不过随着项目规模增大,如果每发起一次HTTP请求,就要把这些比如设置超时时间、设置请求头、根据项目环境判断使用哪个请求地址、错误处理等等操作,都需要写一遍

这种重复劳动不仅浪费时间,而且让代码变得冗余不堪,难以维护。为了提高我们的代码质量,我们应该在项目中二次封装一下 axios 再使用

举个例子:

js
axios('http://localhost:3000/data', {
  // 配置代码
  method: 'GET',
  timeout: 1000,
  withCredentials: true,
  headers: {
    'Content-Type': 'application/json',
    Authorization: 'xxx',
  },
  transformRequest: [
    function (data, headers) {
      return data
    },
  ],
  // 其他请求配置...
}).then(
  (data) => {
    // todo: 真正业务逻辑代码
    console.log(data)
  },
  (err) => {
    // 错误处理代码
    if (err.response.status === 401) {
      // handle authorization error
    }
    if (err.response.status === 403) {
      // handle server forbidden error
    }
    // 其他错误处理.....
    console.log(err)
  },
)
axios('http://localhost:3000/data', {
  // 配置代码
  method: 'GET',
  timeout: 1000,
  withCredentials: true,
  headers: {
    'Content-Type': 'application/json',
    Authorization: 'xxx',
  },
  transformRequest: [
    function (data, headers) {
      return data
    },
  ],
  // 其他请求配置...
}).then(
  (data) => {
    // todo: 真正业务逻辑代码
    console.log(data)
  },
  (err) => {
    // 错误处理代码
    if (err.response.status === 401) {
      // handle authorization error
    }
    if (err.response.status === 403) {
      // handle server forbidden error
    }
    // 其他错误处理.....
    console.log(err)
  },
)

如果每个页面都发送类似的请求,都要写一堆的配置与错误处理,就显得过于繁琐了

这时候我们就需要对axios进行二次封装,让使用更为便利

如何封装 ​

  • 封装的同时,你需要和 后端协商好一些约定,请求头,状态码,请求超时时间.......
  • 设置接口请求前缀:根据开发、测试、生产环境的不同,前缀需要加以区分
  • 请求头 : 来实现一些具体的业务,必须携带一些参数才可以请求(例如:会员业务)
  • 状态码: 根据接口返回的不同status , 来执行不同的业务,这块需要和后端约定好
  • 请求方法:根据get、post等方法进行一个再次封装,使用起来更为方便
  • 请求拦截器: 根据请求的请求头设定,来决定哪些请求可以访问
  • 响应拦截器: 这块就是根据 后端`返回来的状态码判定执行不同业务

设置接口请求前缀

利用node环境变量来作判断,用来区分开发、测试、生产环境

js
if (process.env.NODE_ENV === 'development') {
  axios.defaults.baseURL = 'http://dev.xxx.com'
} else if (process.env.NODE_ENV === 'production') {
  axios.defaults.baseURL = 'http://prod.xxx.com'
}
if (process.env.NODE_ENV === 'development') {
  axios.defaults.baseURL = 'http://dev.xxx.com'
} else if (process.env.NODE_ENV === 'production') {
  axios.defaults.baseURL = 'http://prod.xxx.com'
}

在本地调试的时候,还需要在vue.config.js文件中配置devServer实现代理转发,从而实现跨域

js
devServer: {
    proxy: {
      '/proxyApi': {
        target: 'http://dev.xxx.com',
        changeOrigin: true,
        pathRewrite: {
          '/proxyApi': ''
        }
      }
    }
  }
devServer: {
    proxy: {
      '/proxyApi': {
        target: 'http://dev.xxx.com',
        changeOrigin: true,
        pathRewrite: {
          '/proxyApi': ''
        }
      }
    }
  }

设置请求头与超时时间

大部分情况下,请求头都是固定的,只有少部分情况下,会需要一些特殊的请求头,这里将普适性的请求头作为基础配置。当需要特殊请求头时,将特殊请求头作为参数传入,覆盖基础配置

js
const service = axios.create({
    ...
    timeout: 30000,  // 请求 30s 超时
	  headers: {
        get: {
          'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8'
          // 在开发中,一般还需要单点登录或者其他功能的通用请求头,可以一并配置进来
        },
        post: {
          'Content-Type': 'application/json;charset=utf-8'
          // 在开发中,一般还需要单点登录或者其他功能的通用请求头,可以一并配置进来
        }
  },
})
const service = axios.create({
    ...
    timeout: 30000,  // 请求 30s 超时
	  headers: {
        get: {
          'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8'
          // 在开发中,一般还需要单点登录或者其他功能的通用请求头,可以一并配置进来
        },
        post: {
          'Content-Type': 'application/json;charset=utf-8'
          // 在开发中,一般还需要单点登录或者其他功能的通用请求头,可以一并配置进来
        }
  },
})

封装请求方法

先引入封装好的方法,在要调用的接口重新封装成一个方法暴露出去

js
// get 请求
export function httpGet({ url, params = {} }) {
  return new Promise((resolve, reject) => {
    axios
      .get(url, {
        params,
      })
      .then((res) => {
        resolve(res.data)
      })
      .catch((err) => {
        reject(err)
      })
  })
}

// post
// post请求
export function httpPost({ url, data = {}, params = {} }) {
  return new Promise((resolve, reject) => {
    axios({
      url,
      method: 'post',
      transformRequest: [
        function (data) {
          let ret = ''
          for (let it in data) {
            ret += encodeURIComponent(it) + '=' + encodeURIComponent(data[it]) + '&'
          }
          return ret
        },
      ],
      // 发送的数据
      data,
      // url参数
      params,
    }).then((res) => {
      resolve(res.data)
    })
  })
}
// get 请求
export function httpGet({ url, params = {} }) {
  return new Promise((resolve, reject) => {
    axios
      .get(url, {
        params,
      })
      .then((res) => {
        resolve(res.data)
      })
      .catch((err) => {
        reject(err)
      })
  })
}

// post
// post请求
export function httpPost({ url, data = {}, params = {} }) {
  return new Promise((resolve, reject) => {
    axios({
      url,
      method: 'post',
      transformRequest: [
        function (data) {
          let ret = ''
          for (let it in data) {
            ret += encodeURIComponent(it) + '=' + encodeURIComponent(data[it]) + '&'
          }
          return ret
        },
      ],
      // 发送的数据
      data,
      // url参数
      params,
    }).then((res) => {
      resolve(res.data)
    })
  })
}

把封装的方法放在一个api.js文件中

js
import { httpGet, httpPost } from './http'
export const getorglist = (params = {}) => httpGet({ url: 'apps/api/org/list', params })
import { httpGet, httpPost } from './http'
export const getorglist = (params = {}) => httpGet({ url: 'apps/api/org/list', params })

页面中就能直接调用

js
// .vue
import { getorglist } from '@/assets/js/api'

getorglist({ id: 200 }).then((res) => {
  console.log(res)
})
// .vue
import { getorglist } from '@/assets/js/api'

getorglist({ id: 200 }).then((res) => {
  console.log(res)
})

这样可以把api统一管理起来,以后维护修改只需要在api.js文件操作即可

请求拦截器

请求拦截器可以在每个请求里加上 token,做了统一处理后维护起来也方便

js
// 请求拦截器
axios.interceptors.request.use(
  (config) => {
    // 每次发送请求之前判断是否存在token
    // 如果存在,则统一在http请求的header都加上token,这样后台根据token判断你的登录情况,此处token一般是用户完成登录后储存到localstorage里的
    token && (config.headers.Authorization = token)
    return config
  },
  (error) => {
    return Promise.error(error)
  },
)
// 请求拦截器
axios.interceptors.request.use(
  (config) => {
    // 每次发送请求之前判断是否存在token
    // 如果存在,则统一在http请求的header都加上token,这样后台根据token判断你的登录情况,此处token一般是用户完成登录后储存到localstorage里的
    token && (config.headers.Authorization = token)
    return config
  },
  (error) => {
    return Promise.error(error)
  },
)

响应拦截器

响应拦截器可以在接收到响应后先做一层操作,如根据状态码判断登录状态、授权

js
// 响应拦截器
axios.interceptors.response.use(
  (response) => {
    // 如果返回的状态码为200,说明接口请求成功,可以正常拿到数据
    // 否则的话抛出错误
    if (response.status === 200) {
      if (response.data.code === 511) {
        // 未授权调取授权接口
      } else if (response.data.code === 510) {
        // 未登录跳转登录页
      } else {
        return Promise.resolve(response)
      }
    } else {
      return Promise.reject(response)
    }
  },
  (error) => {
    // 我们可以在这里对异常状态作统一处理
    if (error.response.status) {
      // 处理请求失败的情况
      // 对不同返回码对相应处理
      return Promise.reject(error.response)
    }
  },
)
// 响应拦截器
axios.interceptors.response.use(
  (response) => {
    // 如果返回的状态码为200,说明接口请求成功,可以正常拿到数据
    // 否则的话抛出错误
    if (response.status === 200) {
      if (response.data.code === 511) {
        // 未授权调取授权接口
      } else if (response.data.code === 510) {
        // 未登录跳转登录页
      } else {
        return Promise.resolve(response)
      }
    } else {
      return Promise.reject(response)
    }
  },
  (error) => {
    // 我们可以在这里对异常状态作统一处理
    if (error.response.status) {
      // 处理请求失败的情况
      // 对不同返回码对相应处理
      return Promise.reject(error.response)
    }
  },
)

小结

  • 封装是编程中很有意义的手段,简单的axios封装,就可以让我们可以领略到它的魅力
  • 封装 axios 没有一个绝对的标准,只要你的封装可以满足你的项目需求,并且用起来方便,那就是一个好的封装方案

说下你的 vue 项目的目录结构,如果是大型项目你该怎么划分结构和划分组件呢 ​

为什么要划分 ​

使用vue构建项目,项目结构清晰会提高开发效率,熟悉项目的各种配置同样会让开发效率更高

在划分项目结构的时候,需要遵循一些基本的原则:

  • 文件夹和文件夹内部文件的语义一致性
  • 单一入口/出口
  • 就近原则,紧耦合的文件应该放到一起,且应以相对路径引用
  • 公共的文件应该以绝对路径的方式从根目录引用
  • /src 外的文件不应该被引入

文件夹和文件夹内部文件的语义一致性

我们的目录结构都会有一个文件夹是按照路由模块来划分的,如pages文件夹,这个文件夹里面应该包含我们项目所有的路由模块,并且仅应该包含路由模块,而不应该有别的其他的非路由模块的文件夹

这样做的好处在于一眼就从 pages文件夹看出这个项目的路由有哪些

单一入口/出口

举个例子,在pages文件夹里面存在一个seller文件夹,这时候seller 文件夹应该作为一个独立的模块由外部引入,并且 seller/index.js 应该作为外部引入 seller 模块的唯一入口

js
// 错误用法
import sellerReducer from 'src/pages/seller/reducer'

// 正确用法
import { reducer as sellerReducer } from 'src/pages/seller'
// 错误用法
import sellerReducer from 'src/pages/seller/reducer'

// 正确用法
import { reducer as sellerReducer } from 'src/pages/seller'

这样做的好处在于,无论你的模块文件夹内部有多乱,外部引用的时候,都是从一个入口文件引入,这样就很好的实现了隔离,如果后续有重构需求,你就会发现这种方式的优点

就近原则,紧耦合的文件应该放到一起,且应以相对路径引用

使用相对路径可以保证模块内部的独立性

js
// 正确用法
import styles from './index.module.scss'
// 错误用法
import styles from 'src/pages/seller/index.module.scss'
// 正确用法
import styles from './index.module.scss'
// 错误用法
import styles from 'src/pages/seller/index.module.scss'

举个例子

假设我们现在的 seller 目录是在 src/pages/seller,如果我们后续发生了路由变更,需要加一个层级,变成 src/pages/user/seller。

如果我们采用第一种相对路径的方式,那就可以直接将整个文件夹拖过去就好,seller 文件夹内部不需要做任何变更。

但是如果我们采用第二种绝对路径的方式,移动文件夹的同时,还需要对每个 import 的路径做修改

公共的文件应该以绝对路径的方式从根目录引用

公共指的是多个路由模块共用,如一些公共的组件,我们可以放在src/components下

在使用到的页面中,采用绝对路径的形式引用

js
// 错误用法
import Input from '../../components/input'
// 正确用法
import Input from 'src/components/input'
// 错误用法
import Input from '../../components/input'
// 正确用法
import Input from 'src/components/input'

同样的,如果我们需要对文件夹结构进行调整。将 /src/components/input 变成 /src/components/new/input,如果使用绝对路径,只需要全局搜索替换

再加上绝对路径有全局的语义,相对路径有独立模块的语义

src 外的文件不应该被引入

vue-cli脚手架已经帮我们做了相关的约束了,正常我们的前端项目都会有个src文件夹,里面放着所有的项目需要的资源,js,css, png, svg 等等。src 外会放一些项目配置,依赖,环境等文件

这样的好处是方便划分项目代码文件和配置文件

目录结构 ​

单页面目录结构

js
project
│  .browserslistrc
│  .env.production
│  .eslintrc.js
│  .gitignore
│  babel.config.js
│  package-lock.json
│  package.json
│  README.md
│  vue.config.js
│  yarn-error.log
│  yarn.lock
│
├─public
│      favicon.ico
│      index.html
│
|-- src
    |-- components
        |-- input
            |-- index.js
            |-- index.module.scss
    |-- pages
        |-- seller
            |-- components
                |-- input
                    |-- index.js
                    |-- index.module.scss
            |-- reducer.js
            |-- saga.js
            |-- index.js
            |-- index.module.scss
        |-- buyer
            |-- index.js
        |-- index.js
project
│  .browserslistrc
│  .env.production
│  .eslintrc.js
│  .gitignore
│  babel.config.js
│  package-lock.json
│  package.json
│  README.md
│  vue.config.js
│  yarn-error.log
│  yarn.lock
│
├─public
│      favicon.ico
│      index.html
│
|-- src
    |-- components
        |-- input
            |-- index.js
            |-- index.module.scss
    |-- pages
        |-- seller
            |-- components
                |-- input
                    |-- index.js
                    |-- index.module.scss
            |-- reducer.js
            |-- saga.js
            |-- index.js
            |-- index.module.scss
        |-- buyer
            |-- index.js
        |-- index.js

多页面目录结构

js
my-vue-test:.
│  .browserslistrc
│  .env.production
│  .eslintrc.js
│  .gitignore
│  babel.config.js
│  package-lock.json
│  package.json
│  README.md
│  vue.config.js
│  yarn-error.log
│  yarn.lock
│
├─public
│      favicon.ico
│      index.html
│
└─src
    ├─apis //接口文件根据页面或实例模块化
    │      index.js
    │      login.js
    │
    ├─components //全局公共组件
    │  └─header
    │          index.less
    │          index.vue
    │
    ├─config //配置(环境变量配置不同passid等)
    │      env.js
    │      index.js
    │
    ├─contant //常量
    │      index.js
    │
    ├─images //图片
    │      logo.png
    │
    ├─pages //多页面vue项目,不同的实例
    │  ├─index //主实例
    │  │  │  index.js
    │  │  │  index.vue
    │  │  │  main.js
    │  │  │  router.js
    │  │  │  store.js
    │  │  │
    │  │  ├─components //业务组件
    │  │  └─pages //此实例中的各个路由
    │  │      ├─amenu
    │  │      │      index.vue
    │  │      │
    │  │      └─bmenu
    │  │              index.vue
    │  │
    │  └─login //另一个实例
    │          index.js
    │          index.vue
    │          main.js
    │
    ├─scripts //包含各种常用配置,工具函数
    │  │  map.js
    │  │
    │  └─utils
    │          helper.js
    │
    ├─store //vuex仓库
    │  │  index.js
    │  │
    │  ├─index
    │  │      actions.js
    │  │      getters.js
    │  │      index.js
    │  │      mutation-types.js
    │  │      mutations.js
    │  │      state.js
    │  │
    │  └─user
    │          actions.js
    │          getters.js
    │          index.js
    │          mutation-types.js
    │          mutations.js
    │          state.js
    │
    └─styles //样式统一配置
        │  components.less
        │
        ├─animation
        │      index.less
        │      slide.less
        │
        ├─base
        │      index.less
        │      style.less
        │      var.less
        │      widget.less
        │
        └─common
                index.less
                reset.less
                style.less
                transition.less
my-vue-test:.
│  .browserslistrc
│  .env.production
│  .eslintrc.js
│  .gitignore
│  babel.config.js
│  package-lock.json
│  package.json
│  README.md
│  vue.config.js
│  yarn-error.log
│  yarn.lock
│
├─public
│      favicon.ico
│      index.html
│
└─src
    ├─apis //接口文件根据页面或实例模块化
    │      index.js
    │      login.js
    │
    ├─components //全局公共组件
    │  └─header
    │          index.less
    │          index.vue
    │
    ├─config //配置(环境变量配置不同passid等)
    │      env.js
    │      index.js
    │
    ├─contant //常量
    │      index.js
    │
    ├─images //图片
    │      logo.png
    │
    ├─pages //多页面vue项目,不同的实例
    │  ├─index //主实例
    │  │  │  index.js
    │  │  │  index.vue
    │  │  │  main.js
    │  │  │  router.js
    │  │  │  store.js
    │  │  │
    │  │  ├─components //业务组件
    │  │  └─pages //此实例中的各个路由
    │  │      ├─amenu
    │  │      │      index.vue
    │  │      │
    │  │      └─bmenu
    │  │              index.vue
    │  │
    │  └─login //另一个实例
    │          index.js
    │          index.vue
    │          main.js
    │
    ├─scripts //包含各种常用配置,工具函数
    │  │  map.js
    │  │
    │  └─utils
    │          helper.js
    │
    ├─store //vuex仓库
    │  │  index.js
    │  │
    │  ├─index
    │  │      actions.js
    │  │      getters.js
    │  │      index.js
    │  │      mutation-types.js
    │  │      mutations.js
    │  │      state.js
    │  │
    │  └─user
    │          actions.js
    │          getters.js
    │          index.js
    │          mutation-types.js
    │          mutations.js
    │          state.js
    │
    └─styles //样式统一配置
        │  components.less
        │
        ├─animation
        │      index.less
        │      slide.less
        │
        ├─base
        │      index.less
        │      style.less
        │      var.less
        │      widget.less
        │
        └─common
                index.less
                reset.less
                style.less
                transition.less

小结

项目的目录结构很重要,因为目录结构能体现很多东西,怎么规划目录结构可能每个人有自己的理解,但是按照一定的规范去进行目录的设计,能让项目整个架构看起来更为简洁,更加易用

从 0 到 1 自己构架一个 vue 项目,说说有哪些步骤、哪些重要插件、目录结构你会怎么组织 ​

综合实践类题目,考查实战能力。没有什么绝对的正确答案,把平时工作的重点有条理的描述一下即可

思路

  • 构建项目,创建项目基本结构
  • 引入必要的插件:
  • 代码规范:prettier,eslint
  • 提交规范:husky,lint-staged`
  • 其他常用:svg-loader,vueuse,nprogress
  • 常见目录结构

回答范例

  1. 从0创建一个项目我大致会做以下事情:项目构建、引入必要插件、代码规范、提交规范、常用库和组件
  2. 目前vue3项目我会用vite或者create-vue创建项目
  3. 接下来引入必要插件:路由插件vue-router、状态管理vuex/pinia、ui库我比较喜欢element-plus 和antd-vue、http工具我会选axios
  4. 其他比较常用的库有vueuse,nprogress,图标可以使用vite-svg-loader
  5. 下面是代码规范:结合prettier和eslint即可
  6. 最后是提交规范,可以使用husky,lint-staged,commitlint
  7. 目录结构我有如下习惯: .vscode:用来放项目中的 vscode 配置
  • plugins:用来放 vite 插件的 plugin 配置
  • public:用来放一些诸如 页头icon 之类的公共文件,会被打包到dist根目录下
  • src:用来放项目代码文件
  • api:用来放http的一些接口配置
  • assets:用来放一些 CSS 之类的静态资源
  • components:用来放项目通用组件
  • layout:用来放项目的布局
  • router:用来放项目的路由配置
  • store:用来放状态管理Pinia的配置
  • utils:用来放项目中的工具方法类
  • views:用来放项目的页面文件

vue 要做权限管理该怎么做?如果控制到按钮级别的权限怎么做 ​

是什么 ​

权限是对特定资源的访问许可,所谓权限控制,也就是确保用户只能访问到被分配的资源

而前端权限归根结底是请求的发起权,请求的发起可能有下面两种形式触发

  • 页面加载触发
  • 页面上的按钮点击触发

总的来说,所有的请求发起都触发自前端路由或视图

所以我们可以从这两方面入手,对触发权限的源头进行控制,最终要实现的目标是:

  • 路由方面,用户登录后只能看到自己有权访问的导航菜单,也只能访问自己有权访问的路由地址,否则将跳转 4xx 提示页
  • 视图方面,用户只能看到自己有权浏览的内容和有权操作的控件
  • 最后再加上请求控制作为最后一道防线,路由可能配置失误,按钮可能忘了加权限,这种时候请求控制可以用来兜底,越权请求将在前端被拦截

如何做 ​

前端权限控制可以分为四个方面:

  • 接口权限
  • 按钮权限
  • 菜单权限
  • 路由权限

接口权限

接口权限目前一般采用jwt的形式来验证,没有通过的话一般返回401,跳转到登录页面重新进行登录

登录完拿到token,将token存起来,通过axios请求拦截器进行拦截,每次请求的时候头部携带token

js
axios.interceptors.request.use(config => {
    config.headers['token'] = cookie.get('token')
    return config
})
axios.interceptors.response.use(res=>{},{response}=>{
    if (response.data.code === 40099 || response.data.code === 40098) { //token过期或者错误
        router.push('/login')
    }
})
axios.interceptors.request.use(config => {
    config.headers['token'] = cookie.get('token')
    return config
})
axios.interceptors.response.use(res=>{},{response}=>{
    if (response.data.code === 40099 || response.data.code === 40098) { //token过期或者错误
        router.push('/login')
    }
})

路由权限控制

方案一

初始化即挂载全部路由,并且在路由上标记相应的权限信息,每次路由跳转前做校验

js
const routerMap = [
  {
    path: '/permission',
    component: Layout,
    redirect: '/permission/index',
    alwaysShow: true, // will always show the root menu
    meta: {
      title: 'permission',
      icon: 'lock',
      roles: ['admin', 'editor'], // you can set roles in root nav
    },
    children: [
      {
        path: 'page',
        component: () => import('@/views/permission/page'),
        name: 'pagePermission',
        meta: {
          title: 'pagePermission',
          roles: ['admin'], // or you can only set roles in sub nav
        },
      },
      {
        path: 'directive',
        component: () => import('@/views/permission/directive'),
        name: 'directivePermission',
        meta: {
          title: 'directivePermission',
          // if do not set roles, means: this page does not require permission
        },
      },
    ],
  },
]
const routerMap = [
  {
    path: '/permission',
    component: Layout,
    redirect: '/permission/index',
    alwaysShow: true, // will always show the root menu
    meta: {
      title: 'permission',
      icon: 'lock',
      roles: ['admin', 'editor'], // you can set roles in root nav
    },
    children: [
      {
        path: 'page',
        component: () => import('@/views/permission/page'),
        name: 'pagePermission',
        meta: {
          title: 'pagePermission',
          roles: ['admin'], // or you can only set roles in sub nav
        },
      },
      {
        path: 'directive',
        component: () => import('@/views/permission/directive'),
        name: 'directivePermission',
        meta: {
          title: 'directivePermission',
          // if do not set roles, means: this page does not require permission
        },
      },
    ],
  },
]

这种方式存在以下四种缺点:

  • 加载所有的路由,如果路由很多,而用户并不是所有的路由都有权限访问,对性能会有影响。
  • 全局路由守卫里,每次路由跳转都要做权限判断。
  • 菜单信息写死在前端,要改个显示文字或权限信息,需要重新编译
  • 菜单跟路由耦合在一起,定义路由的时候还有添加菜单显示标题,图标之类的信息,而且路由不一定作为菜单显示,还要多加字段进行标识

方案二

初始化的时候先挂载不需要权限控制的路由,比如登录页,404 等错误页。如果用户通过 URL 进行强制访问,则会直接进入 404,相当于从源头上做了控制

登录后,获取用户的权限信息,然后筛选有权限访问的路由,在全局路由守卫里进行调用addRoutes添加路由

js
import router from './router'
import store from './store'
import { Message } from 'element-ui'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
import { getToken } from '@/utils/auth' // getToken from cookie

NProgress.configure({ showSpinner: false }) // NProgress Configuration

// permission judge function
function hasPermission(roles, permissionRoles) {
  if (roles.indexOf('admin') >= 0) return true // admin permission passed directly
  if (!permissionRoles) return true
  return roles.some((role) => permissionRoles.indexOf(role) >= 0)
}

const whiteList = ['/login', '/authredirect'] // no redirect whitelist

router.beforeEach((to, from, next) => {
  NProgress.start() // start progress bar
  if (getToken()) {
    // determine if there has token
    /* has token*/
    if (to.path === '/login') {
      next({ path: '/' })
      NProgress.done() // if current page is dashboard will not trigger	afterEach hook, so manually handle it
    } else {
      if (store.getters.roles.length === 0) {
        // 判断当前用户是否已拉取完user_info信息
        store
          .dispatch('GetUserInfo')
          .then((res) => {
            // 拉取user_info
            const roles = res.data.roles // note: roles must be a array! such as: ['editor','develop']
            store.dispatch('GenerateRoutes', { roles }).then(() => {
              // 根据roles权限生成可访问的路由表
              router.addRoutes(store.getters.addRouters) // 动态添加可访问路由表
              next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record
            })
          })
          .catch((err) => {
            store.dispatch('FedLogOut').then(() => {
              Message.error(err || 'Verification failed, please login again')
              next({ path: '/' })
            })
          })
      } else {
        // 没有动态改变权限的需求可直接next() 删除下方权限判断 ↓
        if (hasPermission(store.getters.roles, to.meta.roles)) {
          next() //
        } else {
          next({ path: '/401', replace: true, query: { noGoBack: true } })
        }
        // 可删 ↑
      }
    }
  } else {
    /* has no token*/
    if (whiteList.indexOf(to.path) !== -1) {
      // 在免登录白名单,直接进入
      next()
    } else {
      next('/login') // 否则全部重定向到登录页
      NProgress.done() // if current page is login will not trigger afterEach hook, so manually handle it
    }
  }
})
router.afterEach(() => {
  NProgress.done() // finish progress bar
})
import router from './router'
import store from './store'
import { Message } from 'element-ui'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
import { getToken } from '@/utils/auth' // getToken from cookie

NProgress.configure({ showSpinner: false }) // NProgress Configuration

// permission judge function
function hasPermission(roles, permissionRoles) {
  if (roles.indexOf('admin') >= 0) return true // admin permission passed directly
  if (!permissionRoles) return true
  return roles.some((role) => permissionRoles.indexOf(role) >= 0)
}

const whiteList = ['/login', '/authredirect'] // no redirect whitelist

router.beforeEach((to, from, next) => {
  NProgress.start() // start progress bar
  if (getToken()) {
    // determine if there has token
    /* has token*/
    if (to.path === '/login') {
      next({ path: '/' })
      NProgress.done() // if current page is dashboard will not trigger	afterEach hook, so manually handle it
    } else {
      if (store.getters.roles.length === 0) {
        // 判断当前用户是否已拉取完user_info信息
        store
          .dispatch('GetUserInfo')
          .then((res) => {
            // 拉取user_info
            const roles = res.data.roles // note: roles must be a array! such as: ['editor','develop']
            store.dispatch('GenerateRoutes', { roles }).then(() => {
              // 根据roles权限生成可访问的路由表
              router.addRoutes(store.getters.addRouters) // 动态添加可访问路由表
              next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record
            })
          })
          .catch((err) => {
            store.dispatch('FedLogOut').then(() => {
              Message.error(err || 'Verification failed, please login again')
              next({ path: '/' })
            })
          })
      } else {
        // 没有动态改变权限的需求可直接next() 删除下方权限判断 ↓
        if (hasPermission(store.getters.roles, to.meta.roles)) {
          next() //
        } else {
          next({ path: '/401', replace: true, query: { noGoBack: true } })
        }
        // 可删 ↑
      }
    }
  } else {
    /* has no token*/
    if (whiteList.indexOf(to.path) !== -1) {
      // 在免登录白名单,直接进入
      next()
    } else {
      next('/login') // 否则全部重定向到登录页
      NProgress.done() // if current page is login will not trigger afterEach hook, so manually handle it
    }
  }
})
router.afterEach(() => {
  NProgress.done() // finish progress bar
})

按需挂载,路由就需要知道用户的路由权限,也就是在用户登录进来的时候就要知道当前用户拥有哪些路由权限

这种方式也存在了以下的缺点:

  • 全局路由守卫里,每次路由跳转都要做判断
  • 菜单信息写死在前端,要改个显示文字或权限信息,需要重新编译
  • 菜单跟路由耦合在一起,定义路由的时候还有添加菜单显示标题,图标之类的信息,而且路由不一定作为菜单显示,还要多加字段进行标识

菜单权限

菜单权限可以理解成将页面与理由进行解耦

方案一

菜单与路由分离,菜单由后端返回

前端定义路由信息

js
{
    name: "login",
    path: "/login",
    component: () => import("@/pages/Login.vue")
}
{
    name: "login",
    path: "/login",
    component: () => import("@/pages/Login.vue")
}

name字段都不为空,需要根据此字段与后端返回菜单做关联,后端返回的菜单信息中必须要有name对应的字段,并且做唯一性校验

全局路由守卫里做判断

js
function hasPermission(router, accessMenu) {
  if (whiteList.indexOf(router.path) !== -1) {
    return true
  }
  let menu = Util.getMenuByName(router.name, accessMenu)
  if (menu.name) {
    return true
  }
  return false
}

Router.beforeEach(async (to, from, next) => {
  if (getToken()) {
    let userInfo = store.state.user.userInfo
    if (!userInfo.name) {
      try {
        await store.dispatch('GetUserInfo')
        await store.dispatch('updateAccessMenu')
        if (to.path === '/login') {
          next({ name: 'home_index' })
        } else {
          //Util.toDefaultPage([...routers], to.name, router, next);
          next({ ...to, replace: true }) //菜单权限更新完成,重新进一次当前路由
        }
      } catch (e) {
        if (whiteList.indexOf(to.path) !== -1) {
          // 在免登录白名单,直接进入
          next()
        } else {
          next('/login')
        }
      }
    } else {
      if (to.path === '/login') {
        next({ name: 'home_index' })
      } else {
        if (hasPermission(to, store.getters.accessMenu)) {
          Util.toDefaultPage(store.getters.accessMenu, to, routes, next)
        } else {
          next({ path: '/403', replace: true })
        }
      }
    }
  } else {
    if (whiteList.indexOf(to.path) !== -1) {
      // 在免登录白名单,直接进入
      next()
    } else {
      next('/login')
    }
  }
  let menu = Util.getMenuByName(to.name, store.getters.accessMenu)
  Util.title(menu.title)
})

Router.afterEach((to) => {
  window.scrollTo(0, 0)
})
function hasPermission(router, accessMenu) {
  if (whiteList.indexOf(router.path) !== -1) {
    return true
  }
  let menu = Util.getMenuByName(router.name, accessMenu)
  if (menu.name) {
    return true
  }
  return false
}

Router.beforeEach(async (to, from, next) => {
  if (getToken()) {
    let userInfo = store.state.user.userInfo
    if (!userInfo.name) {
      try {
        await store.dispatch('GetUserInfo')
        await store.dispatch('updateAccessMenu')
        if (to.path === '/login') {
          next({ name: 'home_index' })
        } else {
          //Util.toDefaultPage([...routers], to.name, router, next);
          next({ ...to, replace: true }) //菜单权限更新完成,重新进一次当前路由
        }
      } catch (e) {
        if (whiteList.indexOf(to.path) !== -1) {
          // 在免登录白名单,直接进入
          next()
        } else {
          next('/login')
        }
      }
    } else {
      if (to.path === '/login') {
        next({ name: 'home_index' })
      } else {
        if (hasPermission(to, store.getters.accessMenu)) {
          Util.toDefaultPage(store.getters.accessMenu, to, routes, next)
        } else {
          next({ path: '/403', replace: true })
        }
      }
    }
  } else {
    if (whiteList.indexOf(to.path) !== -1) {
      // 在免登录白名单,直接进入
      next()
    } else {
      next('/login')
    }
  }
  let menu = Util.getMenuByName(to.name, store.getters.accessMenu)
  Util.title(menu.title)
})

Router.afterEach((to) => {
  window.scrollTo(0, 0)
})

每次路由跳转的时候都要判断权限,这里的判断也很简单,因为菜单的name与路由的name是一一对应的,而后端返回的菜单就已经是经过权限过滤的

如果根据路由name找不到对应的菜单,就表示用户有没权限访问

如果路由很多,可以在应用初始化的时候,只挂载不需要权限控制的路由。取得后端返回的菜单后,根据菜单与路由的对应关系,筛选出可访问的路由,通过addRoutes动态挂载

这种方式的缺点:

  • 菜单需要与路由做一一对应,前端添加了新功能,需要通过菜单管理功能添加新的菜单,如果菜单配置的不对会导致应用不能正常使用
  • 全局路由守卫里,每次路由跳转都要做判断

方案二

菜单和路由都由后端返回

前端统一定义路由组件

js
const Home = () => import('../pages/Home.vue')
const UserInfo = () => import('../pages/UserInfo.vue')
export default {
  home: Home,
  userInfo: UserInfo,
}
const Home = () => import('../pages/Home.vue')
const UserInfo = () => import('../pages/UserInfo.vue')
export default {
  home: Home,
  userInfo: UserInfo,
}

后端路由组件返回以下格式

js
;[
  {
    name: 'home',
    path: '/',
    component: 'home',
  },
  {
    name: 'home',
    path: '/userinfo',
    component: 'userInfo',
  },
]
;[
  {
    name: 'home',
    path: '/',
    component: 'home',
  },
  {
    name: 'home',
    path: '/userinfo',
    component: 'userInfo',
  },
]

在将后端返回路由通过addRoutes动态挂载之间,需要将数据处理一下,将component字段换为真正的组件

如果有嵌套路由,后端功能设计的时候,要注意添加相应的字段,前端拿到数据也要做相应的处理

这种方法也会存在缺点:

  • 全局路由守卫里,每次路由跳转都要做判断
  • 前后端的配合要求更高

按钮权限

方案一

按钮权限也可以用v-if判断

但是如果页面过多,每个页面页面都要获取用户权限role和路由表里的meta.btnPermissions,然后再做判断

这种方式就不展开举例了

方案二

通过自定义指令进行按钮权限的判断

首先配置路由

js
{
    path: '/permission',
    component: Layout,
    name: '权限测试',
    meta: {
        btnPermissions: ['admin', 'supper', 'normal']
    },
    //页面需要的权限
    children: [{
        path: 'supper',
        component: _import('system/supper'),
        name: '权限测试页',
        meta: {
            btnPermissions: ['admin', 'supper']
        } //页面需要的权限
    },
    {
        path: 'normal',
        component: _import('system/normal'),
        name: '权限测试页',
        meta: {
            btnPermissions: ['admin']
        } //页面需要的权限
    }]
}
{
    path: '/permission',
    component: Layout,
    name: '权限测试',
    meta: {
        btnPermissions: ['admin', 'supper', 'normal']
    },
    //页面需要的权限
    children: [{
        path: 'supper',
        component: _import('system/supper'),
        name: '权限测试页',
        meta: {
            btnPermissions: ['admin', 'supper']
        } //页面需要的权限
    },
    {
        path: 'normal',
        component: _import('system/normal'),
        name: '权限测试页',
        meta: {
            btnPermissions: ['admin']
        } //页面需要的权限
    }]
}

自定义权限鉴定指令

js
import Vue from 'vue'
/**权限指令**/
const has = Vue.directive('has', {
  bind: function (el, binding, vnode) {
    // 获取页面按钮权限
    let btnPermissionsArr = []
    if (binding.value) {
      // 如果指令传值,获取指令参数,根据指令参数和当前登录人按钮权限做比较。
      btnPermissionsArr = Array.of(binding.value)
    } else {
      // 否则获取路由中的参数,根据路由的btnPermissionsArr和当前登录人按钮权限做比较。
      btnPermissionsArr = vnode.context.$route.meta.btnPermissions
    }
    if (!Vue.prototype.$_has(btnPermissionsArr)) {
      el.parentNode.removeChild(el)
    }
  },
})
// 权限检查方法
Vue.prototype.$_has = function (value) {
  let isExist = false
  // 获取用户按钮权限
  let btnPermissionsStr = sessionStorage.getItem('btnPermissions')
  if (btnPermissionsStr == undefined || btnPermissionsStr == null) {
    return false
  }
  if (value.indexOf(btnPermissionsStr) > -1) {
    isExist = true
  }
  return isExist
}
export { has }
import Vue from 'vue'
/**权限指令**/
const has = Vue.directive('has', {
  bind: function (el, binding, vnode) {
    // 获取页面按钮权限
    let btnPermissionsArr = []
    if (binding.value) {
      // 如果指令传值,获取指令参数,根据指令参数和当前登录人按钮权限做比较。
      btnPermissionsArr = Array.of(binding.value)
    } else {
      // 否则获取路由中的参数,根据路由的btnPermissionsArr和当前登录人按钮权限做比较。
      btnPermissionsArr = vnode.context.$route.meta.btnPermissions
    }
    if (!Vue.prototype.$_has(btnPermissionsArr)) {
      el.parentNode.removeChild(el)
    }
  },
})
// 权限检查方法
Vue.prototype.$_has = function (value) {
  let isExist = false
  // 获取用户按钮权限
  let btnPermissionsStr = sessionStorage.getItem('btnPermissions')
  if (btnPermissionsStr == undefined || btnPermissionsStr == null) {
    return false
  }
  if (value.indexOf(btnPermissionsStr) > -1) {
    isExist = true
  }
  return isExist
}
export { has }

在使用的按钮中只需要引用v-has指令

js
<el-button @click='editClick' type="primary" v-has>编辑</el-button>
<el-button @click='editClick' type="primary" v-has>编辑</el-button>

小结

关于权限如何选择哪种合适的方案,可以根据自己项目的方案项目,如考虑路由与菜单是否分离

权限需要前后端结合,前端尽可能的去控制,更多的需要后台判断

Vue 项目中你是如何解决跨域的呢 ​

跨域是什么 ​

跨域本质是浏览器基于同源策略的一种安全手段

同源策略(Sameoriginpolicy),是一种约定,它是浏览器最核心也最基本的安全功能

所谓同源(即指在同一个域)具有以下三个相同点

  • 协议相同(protocol)
  • 主机相同(host)
  • 端口相同(port)

反之非同源请求,也就是协议、端口、主机其中一项不相同的时候,这时候就会产生跨域

一定要注意跨域是浏览器的限制,你用抓包工具抓取接口数据,是可以看到接口已经把数据返回回来了,只是浏览器的限制,你获取不到数据。用 postman 请求接口能够请求到数据。这些再次印证了跨域是浏览器的限制。

如何解决 ​

解决跨域的方法有很多,下面列举了三种:

  • JSONP
  • CORS
  • Proxy

而在vue项目中,我们主要针对CORS或Proxy这两种方案进行展开

CORS

CORS (Cross-Origin Resource Sharing,跨域资源共享)是一个系统,它由一系列传输的 HTTP 头组成,这些 HTTP 头决定浏览器是否阻止前端 JavaScript 代码获取跨域请求的响应

CORS 实现起来非常方便,只需要增加一些 HTTP 头,让服务器能声明允许的访问来源

只要后端实现了 CORS,就实现了跨域

!img

以koa框架举例

添加中间件,直接设置Access-Control-Allow-Origin响应头

js
app.use(async (ctx, next) => {
  ctx.set('Access-Control-Allow-Origin', '*')
  ctx.set(
    'Access-Control-Allow-Headers',
    'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild',
  )
  ctx.set('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS')
  if (ctx.method == 'OPTIONS') {
    ctx.body = 200
  } else {
    await next()
  }
})
app.use(async (ctx, next) => {
  ctx.set('Access-Control-Allow-Origin', '*')
  ctx.set(
    'Access-Control-Allow-Headers',
    'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild',
  )
  ctx.set('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS')
  if (ctx.method == 'OPTIONS') {
    ctx.body = 200
  } else {
    await next()
  }
})

ps: Access-Control-Allow-Origin 设置为*其实意义不大,可以说是形同虚设,实际应用中,上线前我们会将Access-Control-Allow-Origin 值设为我们目标host

Proxy

代理(Proxy)也称网络代理,是一种特殊的网络服务,允许一个(一般为客户端)通过这个服务与另一个网络终端(一般为服务器)进行非直接的连接。一些网关、路由器等网络设备具备网络代理功能。一般认为代理服务有利于保障网络终端的隐私或安全,防止攻击

方案一

如果是通过vue-cli脚手架工具搭建项目,我们可以通过webpack为我们起一个本地服务器作为请求的代理对象

通过该服务器转发请求至目标服务器,得到结果再转发给前端,但是最终发布上线时如果 web 应用和接口服务器不在一起仍会跨域

在vue.config.js文件,新增以下代码

js
amodule.exports = {
  devServer: {
    host: '127.0.0.1',
    port: 8084,
    open: true, // vue项目启动时自动打开浏览器
    proxy: {
      '/api': {
        // '/api'是代理标识,用于告诉node,url前面是/api的就是使用代理的
        target: 'http://xxx.xxx.xx.xx:8080', //目标地址,一般是指后台服务器地址
        changeOrigin: true, //是否跨域
        pathRewrite: {
          // pathRewrite 的作用是把实际Request Url中的'/api'用""代替
          '^/api': '',
        },
      },
    },
  },
}
amodule.exports = {
  devServer: {
    host: '127.0.0.1',
    port: 8084,
    open: true, // vue项目启动时自动打开浏览器
    proxy: {
      '/api': {
        // '/api'是代理标识,用于告诉node,url前面是/api的就是使用代理的
        target: 'http://xxx.xxx.xx.xx:8080', //目标地址,一般是指后台服务器地址
        changeOrigin: true, //是否跨域
        pathRewrite: {
          // pathRewrite 的作用是把实际Request Url中的'/api'用""代替
          '^/api': '',
        },
      },
    },
  },
}

通过axios发送请求中,配置请求的根路径

js
axios.defaults.baseURL = '/api'
axios.defaults.baseURL = '/api'

方案二

此外,还可通过服务端实现代理请求转发

以express框架为例

js
var express = require('express')
const proxy = require('http-proxy-middleware')
const app = express()
app.use(express.static(__dirname + '/'))
app.use('/api', proxy({ target: 'http://localhost:4000', changeOrigin: false }))
module.exports = app
var express = require('express')
const proxy = require('http-proxy-middleware')
const app = express()
app.use(express.static(__dirname + '/'))
app.use('/api', proxy({ target: 'http://localhost:4000', changeOrigin: false }))
module.exports = app

方案三

通过配置nginx实现代理

js
server {
    listen    80;
    # server_name www.josephxia.com;
    location / {
        root  /var/www/html;
        index  index.html index.htm;
        try_files $uri $uri/ /index.html;
    }
    location /api {
        proxy_pass  http://127.0.0.1:3000;
        proxy_redirect   off;
        proxy_set_header  Host       $host;
        proxy_set_header  X-Real-IP     $remote_addr;
        proxy_set_header  X-Forwarded-For  $proxy_add_x_forwarded_for;
    }
}
server {
    listen    80;
    # server_name www.josephxia.com;
    location / {
        root  /var/www/html;
        index  index.html index.htm;
        try_files $uri $uri/ /index.html;
    }
    location /api {
        proxy_pass  http://127.0.0.1:3000;
        proxy_redirect   off;
        proxy_set_header  Host       $host;
        proxy_set_header  X-Real-IP     $remote_addr;
        proxy_set_header  X-Forwarded-For  $proxy_add_x_forwarded_for;
    }
}

Vue 项目本地开发完成后部署到服务器后报 404 是什么原因呢 ​

如何部署 ​

前后端分离开发模式下,前后端是独立布署的,前端只需要将最后的构建物上传至目标服务器的web容器指定的静态目录下即可

我们知道vue项目在构建后,是生成一系列的静态文件

常规布署我们只需要将这个目录上传至目标服务器即可

bash
# scp 上传 user为主机登录用户,host为主机外网ip, xx为web容器静态资源路径
scp dist.zip user@host:/xx/xx/xx
# scp 上传 user为主机登录用户,host为主机外网ip, xx为web容器静态资源路径
scp dist.zip user@host:/xx/xx/xx

让web容器跑起来,以nginx为例

nginx
server {
  listen  80;
  server_name  www.xxx.com;

  location / {
    index  /data/dist/index.html;
  }
}
server {
  listen  80;
  server_name  www.xxx.com;

  location / {
    index  /data/dist/index.html;
  }
}

配置完成记得重启nginx

js
// 检查配置是否正确
nginx -t

// 平滑重启
nginx -s reload
// 检查配置是否正确
nginx -t

// 平滑重启
nginx -s reload

操作完后就可以在浏览器输入域名进行访问了

当然上面只是提到最简单也是最直接的一种布署方式

什么自动化,镜像,容器,流水线布署,本质也是将这套逻辑抽象,隔离,用程序来代替重复性的劳动,本文不展开

404 问题 ​

这是一个经典的问题,相信很多同学都有遇到过,那么你知道其真正的原因吗?

我们先还原一下场景:

  • vue项目在本地时运行正常,但部署到服务器中,刷新页面,出现了 404 错误

先定位一下,HTTP 404 错误意味着链接指向的资源不存在

问题在于为什么不存在?且为什么只有history模式下会出现这个问题?

为什么 history 模式下有问题

Vue是属于单页应用(single-page application)

而SPA是一种网络应用程序或网站的模型,所有用户交互是通过动态重写当前页面,前面我们也看到了,不管我们应用有多少页面,构建物都只会产出一个index.html

现在,我们回头来看一下我们的nginx配置

js
server {
  listen  80;
  server_name  www.xxx.com;

  location / {
    index  /data/dist/index.html;
  }
}
server {
  listen  80;
  server_name  www.xxx.com;

  location / {
    index  /data/dist/index.html;
  }
}

可以根据 nginx 配置得出,当我们在地址栏输入 www.xxx.com 时,这时会打开我们 dist 目录下的 index.html 文件,然后我们在跳转路由进入到 www.xxx.com/login

关键在这里,当我们在 website.com/login 页执行刷新操作,nginx location 是没有相关配置的,所以就会出现 404 的情况

为什么 hash 模式下没有问题

router hash` 模式我们都知道是用符号#表示的,如 `website.com/#/login`, `hash` 的值为 `#/login
router hash` 模式我们都知道是用符号#表示的,如 `website.com/#/login`, `hash` 的值为 `#/login

它的特点在于:hash 虽然出现在 URL 中,但不会被包括在 HTTP 请求中,对服务端完全没有影响,因此改变 hash 不会重新加载页面

hash 模式下,仅 hash 符号之前的内容会被包含在请求中,如 website.com/#/login 只有 website.com 会被包含在请求中 ,因此对于服务端来说,即使没有配置location,也不会返回404错误

解决方案 ​

看到这里我相信大部分同学都能想到怎么解决问题了,

产生问题的本质是因为我们的路由是通过 JS 来执行视图切换的,

当我们进入到子路由时刷新页面,web容器没有相对应的页面此时会出现404

所以我们只需要配置将任意页面都重定向到 index.html,把路由交由前端处理

对nginx配置文件.conf修改,添加try_files $uri $uri/ /index.html;

nginx
server {
  listen  80;
  server_name  www.xxx.com;

  location / {
    index  /data/dist/index.html;
    try_files $uri $uri/ /index.html;
  }
}
server {
  listen  80;
  server_name  www.xxx.com;

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

修改完配置文件后记得配置的更新

bash
nginx -s reload
nginx -s reload

这么做以后,你的服务器就不再返回 404 错误页面,因为对于所有路径都会返回 index.html 文件

为了避免这种情况,你应该在 Vue 应用里面覆盖所有的路由情况,然后在给出一个 404 页面

js
const router = new VueRouter({
  mode: 'history',
  routes: [{ path: '*', component: NotFoundComponent }],
})
const router = new VueRouter({
  mode: 'history',
  routes: [{ path: '*', component: NotFoundComponent }],
})

实际工作中,你总结的 vue 最佳实践有哪些 ​

从编码风格、性能、安全等方面说几条:

编码风格方面:

  • 命名组件时使用“多词”风格避免和HTML元素冲突
  • 使用“细节化”方式定义属性而不是只有一个属性名
  • 属性名声明时使用“驼峰命名”,模板或jsx中使用“肉串命名”
  • 使用v-for时务必加上key,且不要跟v-if写在一起

性能方面:

  • 路由懒加载减少应用尺寸
  • 利用SSR减少首屏加载时间
  • 利用v-once渲染那些不需要更新的内容
  • 一些长列表可以利用虚拟滚动技术避免内存过度占用
  • 对于深层嵌套对象的大数组可以使用shallowRef或shallowReactive降低开销
  • 避免不必要的组件抽象

安全:

  • 不使用不可信模板,例如使用用户输入拼接模板:template: <div> + userProvidedString + </div>
  • 避免使用v-html,:url,:style等,避免html、url、样式等注入

vue 中使用了哪些设计模式 ​

  • 工厂模式 传入参数即可创建实例:虚拟 DOM 根据参数的不同返回基础标签的 Vnode 和组件 Vnode
  • 单例模式 整个程序有且仅有一个实例:vuex 和 vue-router 的插件注册方法 install 判断如果系统存在实例就直接返回掉
  • 发布-订阅模式 (vue 事件机制)
  • 观察者模式 (响应式数据原理)
  • 装饰模式: (@装饰器的用法)
  • 策略模式 策略模式指对象有某个行为,但是在不同的场景中,该行为有不同的实现方案-比如选项的合并策略

如果让你从零开始写一个 vuex,说说你的思路 ​

思路分析

这个题目很有难度,首先思考vuex解决的问题:存储用户全局状态并提供管理状态 API。

  • vuex需求分析
  • 如何实现这些需求

回答范例

  1. 官方说vuex是一个状态管理模式和库,并确保这些状态以可预期的方式变更。可见要实现一个vuex
  • 要实现一个Store存储全局状态
  • 要提供修改状态所需 API:commit(type, payload), dispatch(type, payload)
  1. 实现Store时,可以定义Store类,构造函数接收选项options,设置属性state对外暴露状态,提供commit和dispatch修改属性state。这里需要设置state为响应式对象,同时将Store定义为一个Vue插件
  2. commit(type, payload)方法中可以获取用户传入mutations并执行它,这样可以按用户提供的方法修改状态。 dispatch(type, payload)类似,但需要注意它可能是异步的,需要返回一个Promise给用户以处理异步结果

实践

Store的实现:

js
class Store {
  constructor(options) {
    this.state = reactive(options.state)
    this.options = options
  }
  commit(type, payload) {
    this.options.mutations[type].call(this, this.state, payload)
  }
}
class Store {
  constructor(options) {
    this.state = reactive(options.state)
    this.options = options
  }
  commit(type, payload) {
    this.options.mutations[type].call(this, this.state, payload)
  }
}

vuex 简易版

js
/**
 * 1 实现插件,挂载$store
 * 2 实现store
 */

let Vue

class Store {
  constructor(options) {
    // state响应式处理
    // 外部访问: this.$store.state.***
    // 第一种写法
    // this.state = new Vue({
    //   data: options.state
    // })

    // 第二种写法:防止外界直接接触内部vue实例,防止外部强行变更
    this._vm = new Vue({
      data: {
        $$state: options.state,
      },
    })

    this._mutations = options.mutations
    this._actions = options.actions
    this.getters = {}
    options.getters && this.handleGetters(options.getters)

    this.commit = this.commit.bind(this)
    this.dispatch = this.dispatch.bind(this)
  }

  get state() {
    return this._vm._data.$$state
  }

  set state(val) {
    return new Error('Please use replaceState to reset state')
  }

  handleGetters(getters) {
    Object.keys(getters).map((key) => {
      Object.defineProperty(this.getters, key, {
        get: () => getters[key](this.state),
      })
    })
  }

  commit(type, payload) {
    let entry = this._mutations[type]
    if (!entry) {
      return new Error(`${type} is not defined`)
    }

    entry(this.state, payload)
  }

  dispatch(type, payload) {
    let entry = this._actions[type]
    if (!entry) {
      return new Error(`${type} is not defined`)
    }

    entry(this, payload)
  }
}

const install = (_Vue) => {
  Vue = _Vue

  Vue.mixin({
    beforeCreate() {
      if (this.$options.store) {
        Vue.prototype.$store = this.$options.store
      }
    },
  })
}

export default { Store, install }
/**
 * 1 实现插件,挂载$store
 * 2 实现store
 */

let Vue

class Store {
  constructor(options) {
    // state响应式处理
    // 外部访问: this.$store.state.***
    // 第一种写法
    // this.state = new Vue({
    //   data: options.state
    // })

    // 第二种写法:防止外界直接接触内部vue实例,防止外部强行变更
    this._vm = new Vue({
      data: {
        $$state: options.state,
      },
    })

    this._mutations = options.mutations
    this._actions = options.actions
    this.getters = {}
    options.getters && this.handleGetters(options.getters)

    this.commit = this.commit.bind(this)
    this.dispatch = this.dispatch.bind(this)
  }

  get state() {
    return this._vm._data.$$state
  }

  set state(val) {
    return new Error('Please use replaceState to reset state')
  }

  handleGetters(getters) {
    Object.keys(getters).map((key) => {
      Object.defineProperty(this.getters, key, {
        get: () => getters[key](this.state),
      })
    })
  }

  commit(type, payload) {
    let entry = this._mutations[type]
    if (!entry) {
      return new Error(`${type} is not defined`)
    }

    entry(this.state, payload)
  }

  dispatch(type, payload) {
    let entry = this._actions[type]
    if (!entry) {
      return new Error(`${type} is not defined`)
    }

    entry(this, payload)
  }
}

const install = (_Vue) => {
  Vue = _Vue

  Vue.mixin({
    beforeCreate() {
      if (this.$options.store) {
        Vue.prototype.$store = this.$options.store
      }
    },
  })
}

export default { Store, install }

验证方式

js
import Vue from 'vue'
import Vuex from './vuex'
// this.$store
Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    counter: 0,
  },
  mutations: {
    // state从哪里来的
    add(state) {
      state.counter++
    },
  },
  getters: {
    doubleCounter(state) {
      return state.counter * 2
    },
  },
  actions: {
    add({ commit }) {
      setTimeout(() => {
        commit('add')
      }, 1000)
    },
  },
  modules: {},
})
import Vue from 'vue'
import Vuex from './vuex'
// this.$store
Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    counter: 0,
  },
  mutations: {
    // state从哪里来的
    add(state) {
      state.counter++
    },
  },
  getters: {
    doubleCounter(state) {
      return state.counter * 2
    },
  },
  actions: {
    add({ commit }) {
      setTimeout(() => {
        commit('add')
      }, 1000)
    },
  },
  modules: {},
})

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

分析

企业级项目中渲染大量数据的情况比较常见,因此这是一道非常好的综合实践题目。

回答

  1. 在大型企业级项目中经常需要渲染大量数据,此时很容易出现卡顿的情况。比如大数据量的表格、树
  2. 处理时要根据情况做不同处理:
  • 可以采取分页的方式获取,避免渲染大量数据
  • vue-virtual-scroller (opens new window)等虚拟滚动方案,只渲染视口范围内的数据
  • 如果不需要更新,可以使用 v-once 方式只渲染一次
  • 通过v-memo (opens new window)可以缓存结果,结合v-for使用,避免数据变化时不必要的VNode创建
  • 可以采用懒加载方式,在用户需要的时候再加载数据,比如tree组件子树的懒加载
  1. 还是要看具体需求,首先从设计上避免大数据获取和渲染;实在需要这样做可以采用虚表的方式优化渲染;最后优化更新,如果不需要更新可以v-once处理,需要更新可以v-memo进一步优化大数据更新性能。其他可以采用的是交互方式优化,无线滚动、懒加载等方案

动态给 vue 的 data 添加一个新的属性时会发生什么?怎样解决? ​

Vue 不允许在已经创建的实例上动态添加新的响应式属性

若想实现数据与视图同步更新,可采取下面三种解决方案:

  • Vue.set()
  • Object.assign()
  • $forcecUpdated()

Vue.set() ​

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

参数

  • {Object | Array} target
  • {string | number} propertyName/index
  • {any} value

返回值:设置的值

通过Vue.set向响应式对象中添加一个property,并确保这个新 property同样是响应式的,且触发视图更新

关于Vue.set源码(省略了很多与本节不相关的代码)

源码位置:src\core\observer\index.js

js
function set (target: Array<any> | Object, key: any, val: any): any {
  ...
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}
function set (target: Array<any> | Object, key: any, val: any): any {
  ...
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

这里无非再次调用defineReactive方法,实现新增属性的响应式

关于defineReactive方法,内部还是通过Object.defineProperty实现属性拦截

大致代码如下:

js
function defineReactive(obj, key, val) {
  Object.defineProperty(obj, key, {
    get() {
      console.log(`get ${key}:${val}`)
      return val
    },
    set(newVal) {
      if (newVal !== val) {
        console.log(`set ${key}:${newVal}`)
        val = newVal
      }
    },
  })
}
function defineReactive(obj, key, val) {
  Object.defineProperty(obj, key, {
    get() {
      console.log(`get ${key}:${val}`)
      return val
    },
    set(newVal) {
      if (newVal !== val) {
        console.log(`set ${key}:${newVal}`)
        val = newVal
      }
    },
  })
}

Object.assign() ​

直接使用Object.assign()添加到对象的新属性不会触发更新

应创建一个新的对象,合并原对象和混入对象的属性

js
this.someObject = Object.assign({},this.someObject,{newProperty1:1,newProperty2:2 ...})
this.someObject = Object.assign({},this.someObject,{newProperty1:1,newProperty2:2 ...})

$forceUpdate ​

如果你发现你自己需要在 Vue中做一次强制更新,99.9% 的情况,是你在某个地方做错了事

$forceUpdate迫使Vue 实例重新渲染

PS:仅仅影响实例本身和插入插槽内容的子组件,而不是所有子组件。

小结 ​

  • 如果为对象添加少量的新属性,可以直接采用Vue.set()
  • 如果需要为新对象添加大量的新属性,则通过Object.assign()创建新对象
  • 如果你实在不知道怎么操作时,可采取$forceUpdate()进行强制刷新 (不建议)

PS:vue3是用过proxy实现数据响应式的,直接动态添加新属性仍可以实现数据响应式

Vue 项目本地开发完成后部署到服务器后报 404 是什么原因呢 ​

如何部署 ​

前后端分离开发模式下,前后端是独立布署的,前端只需要将最后的构建物上传至目标服务器的web容器指定的静态目录下即可

我们知道vue项目在构建后,是生成一系列的静态文件

常规布署我们只需要将这个目录上传至目标服务器即可

bash
# scp 上传 user为主机登录用户,host为主机外网ip, xx为web容器静态资源路径
scp dist.zip user@host:/xx/xx/xx
# scp 上传 user为主机登录用户,host为主机外网ip, xx为web容器静态资源路径
scp dist.zip user@host:/xx/xx/xx

让web容器跑起来,以nginx为例

nginx
server {
  listen  80;
  server_name  www.xxx.com;

  location / {
    index  /data/dist/index.html;
  }
}
server {
  listen  80;
  server_name  www.xxx.com;

  location / {
    index  /data/dist/index.html;
  }
}

配置完成记得重启nginx

js
// 检查配置是否正确
nginx -t

// 平滑重启
nginx -s reload
// 检查配置是否正确
nginx -t

// 平滑重启
nginx -s reload

操作完后就可以在浏览器输入域名进行访问了

当然上面只是提到最简单也是最直接的一种布署方式

什么自动化,镜像,容器,流水线布署,本质也是将这套逻辑抽象,隔离,用程序来代替重复性的劳动,本文不展开

404 问题 ​

这是一个经典的问题,相信很多同学都有遇到过,那么你知道其真正的原因吗?

我们先还原一下场景:

  • vue项目在本地时运行正常,但部署到服务器中,刷新页面,出现了 404 错误

先定位一下,HTTP 404 错误意味着链接指向的资源不存在

问题在于为什么不存在?且为什么只有history模式下会出现这个问题?

为什么 history 模式下有问题

Vue是属于单页应用(single-page application)

而SPA是一种网络应用程序或网站的模型,所有用户交互是通过动态重写当前页面,前面我们也看到了,不管我们应用有多少页面,构建物都只会产出一个index.html

现在,我们回头来看一下我们的nginx配置

js
server {
  listen  80;
  server_name  www.xxx.com;

  location / {
    index  /data/dist/index.html;
  }
}
server {
  listen  80;
  server_name  www.xxx.com;

  location / {
    index  /data/dist/index.html;
  }
}

可以根据 nginx 配置得出,当我们在地址栏输入 www.xxx.com 时,这时会打开我们 dist 目录下的 index.html 文件,然后我们在跳转路由进入到 www.xxx.com/login

关键在这里,当我们在 website.com/login 页执行刷新操作,nginx location 是没有相关配置的,所以就会出现 404 的情况

为什么 hash 模式下没有问题

router hash` 模式我们都知道是用符号#表示的,如 `website.com/#/login`, `hash` 的值为 `#/login
router hash` 模式我们都知道是用符号#表示的,如 `website.com/#/login`, `hash` 的值为 `#/login

它的特点在于:hash 虽然出现在 URL 中,但不会被包括在 HTTP 请求中,对服务端完全没有影响,因此改变 hash 不会重新加载页面

hash 模式下,仅 hash 符号之前的内容会被包含在请求中,如 website.com/#/login 只有 website.com 会被包含在请求中 ,因此对于服务端来说,即使没有配置location,也不会返回404错误

解决方案 ​

看到这里我相信大部分同学都能想到怎么解决问题了,

产生问题的本质是因为我们的路由是通过 JS 来执行视图切换的,

当我们进入到子路由时刷新页面,web容器没有相对应的页面此时会出现404

所以我们只需要配置将任意页面都重定向到 index.html,把路由交由前端处理

对nginx配置文件.conf修改,添加try_files $uri $uri/ /index.html;

nginx
server {
  listen  80;
  server_name  www.xxx.com;

  location / {
    index  /data/dist/index.html;
    try_files $uri $uri/ /index.html;
  }
}
server {
  listen  80;
  server_name  www.xxx.com;

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

修改完配置文件后记得配置的更新

bash
nginx -s reload
nginx -s reload

这么做以后,你的服务器就不再返回 404 错误页面,因为对于所有路径都会返回 index.html 文件

为了避免这种情况,你应该在 Vue 应用里面覆盖所有的路由情况,然后在给出一个 404 页面

js
const router = new VueRouter({
  mode: 'history',
  routes: [{ path: '*', component: NotFoundComponent }],
})
const router = new VueRouter({
  mode: 'history',
  routes: [{ path: '*', component: NotFoundComponent }],
})

实际工作中,你总结的 vue 最佳实践有哪些 ​

从编码风格、性能、安全等方面说几条:

编码风格方面:

  • 命名组件时使用“多词”风格避免和HTML元素冲突
  • 使用“细节化”方式定义属性而不是只有一个属性名
  • 属性名声明时使用“驼峰命名”,模板或jsx中使用“肉串命名”
  • 使用v-for时务必加上key,且不要跟v-if写在一起

性能方面:

  • 路由懒加载减少应用尺寸
  • 利用SSR减少首屏加载时间
  • 利用v-once渲染那些不需要更新的内容
  • 一些长列表可以利用虚拟滚动技术避免内存过度占用
  • 对于深层嵌套对象的大数组可以使用shallowRef或shallowReactive降低开销
  • 避免不必要的组件抽象

安全:

  • 不使用不可信模板,例如使用用户输入拼接模板:template: <div> + userProvidedString + </div>
  • 避免使用v-html,:url,:style等,避免html、url、样式等注入

vue 中使用了哪些设计模式 ​

  • 工厂模式 传入参数即可创建实例:虚拟 DOM 根据参数的不同返回基础标签的 Vnode 和组件 Vnode
  • 单例模式 整个程序有且仅有一个实例:vuex 和 vue-router 的插件注册方法 install 判断如果系统存在实例就直接返回掉
  • 发布-订阅模式 (vue 事件机制)
  • 观察者模式 (响应式数据原理)
  • 装饰模式: (@装饰器的用法)
  • 策略模式 策略模式指对象有某个行为,但是在不同的场景中,该行为有不同的实现方案-比如选项的合并策略

如果让你从零开始写一个 vuex,说说你的思路 ​

思路分析

这个题目很有难度,首先思考vuex解决的问题:存储用户全局状态并提供管理状态 API。

  • vuex需求分析
  • 如何实现这些需求

回答范例

  1. 官方说vuex是一个状态管理模式和库,并确保这些状态以可预期的方式变更。可见要实现一个vuex
  • 要实现一个Store存储全局状态
  • 要提供修改状态所需 API:commit(type, payload), dispatch(type, payload)
  1. 实现Store时,可以定义Store类,构造函数接收选项options,设置属性state对外暴露状态,提供commit和dispatch修改属性state。这里需要设置state为响应式对象,同时将Store定义为一个Vue插件
  2. commit(type, payload)方法中可以获取用户传入mutations并执行它,这样可以按用户提供的方法修改状态。 dispatch(type, payload)类似,但需要注意它可能是异步的,需要返回一个Promise给用户以处理异步结果

实践

Store的实现:

js
class Store {
  constructor(options) {
    this.state = reactive(options.state)
    this.options = options
  }
  commit(type, payload) {
    this.options.mutations[type].call(this, this.state, payload)
  }
}
class Store {
  constructor(options) {
    this.state = reactive(options.state)
    this.options = options
  }
  commit(type, payload) {
    this.options.mutations[type].call(this, this.state, payload)
  }
}

vuex 简易版

js
/**
 * 1 实现插件,挂载$store
 * 2 实现store
 */

let Vue

class Store {
  constructor(options) {
    // state响应式处理
    // 外部访问: this.$store.state.***
    // 第一种写法
    // this.state = new Vue({
    //   data: options.state
    // })

    // 第二种写法:防止外界直接接触内部vue实例,防止外部强行变更
    this._vm = new Vue({
      data: {
        $$state: options.state,
      },
    })

    this._mutations = options.mutations
    this._actions = options.actions
    this.getters = {}
    options.getters && this.handleGetters(options.getters)

    this.commit = this.commit.bind(this)
    this.dispatch = this.dispatch.bind(this)
  }

  get state() {
    return this._vm._data.$$state
  }

  set state(val) {
    return new Error('Please use replaceState to reset state')
  }

  handleGetters(getters) {
    Object.keys(getters).map((key) => {
      Object.defineProperty(this.getters, key, {
        get: () => getters[key](this.state),
      })
    })
  }

  commit(type, payload) {
    let entry = this._mutations[type]
    if (!entry) {
      return new Error(`${type} is not defined`)
    }

    entry(this.state, payload)
  }

  dispatch(type, payload) {
    let entry = this._actions[type]
    if (!entry) {
      return new Error(`${type} is not defined`)
    }

    entry(this, payload)
  }
}

const install = (_Vue) => {
  Vue = _Vue

  Vue.mixin({
    beforeCreate() {
      if (this.$options.store) {
        Vue.prototype.$store = this.$options.store
      }
    },
  })
}

export default { Store, install }
/**
 * 1 实现插件,挂载$store
 * 2 实现store
 */

let Vue

class Store {
  constructor(options) {
    // state响应式处理
    // 外部访问: this.$store.state.***
    // 第一种写法
    // this.state = new Vue({
    //   data: options.state
    // })

    // 第二种写法:防止外界直接接触内部vue实例,防止外部强行变更
    this._vm = new Vue({
      data: {
        $$state: options.state,
      },
    })

    this._mutations = options.mutations
    this._actions = options.actions
    this.getters = {}
    options.getters && this.handleGetters(options.getters)

    this.commit = this.commit.bind(this)
    this.dispatch = this.dispatch.bind(this)
  }

  get state() {
    return this._vm._data.$$state
  }

  set state(val) {
    return new Error('Please use replaceState to reset state')
  }

  handleGetters(getters) {
    Object.keys(getters).map((key) => {
      Object.defineProperty(this.getters, key, {
        get: () => getters[key](this.state),
      })
    })
  }

  commit(type, payload) {
    let entry = this._mutations[type]
    if (!entry) {
      return new Error(`${type} is not defined`)
    }

    entry(this.state, payload)
  }

  dispatch(type, payload) {
    let entry = this._actions[type]
    if (!entry) {
      return new Error(`${type} is not defined`)
    }

    entry(this, payload)
  }
}

const install = (_Vue) => {
  Vue = _Vue

  Vue.mixin({
    beforeCreate() {
      if (this.$options.store) {
        Vue.prototype.$store = this.$options.store
      }
    },
  })
}

export default { Store, install }

验证方式

js
import Vue from 'vue'
import Vuex from './vuex'
// this.$store
Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    counter: 0,
  },
  mutations: {
    // state从哪里来的
    add(state) {
      state.counter++
    },
  },
  getters: {
    doubleCounter(state) {
      return state.counter * 2
    },
  },
  actions: {
    add({ commit }) {
      setTimeout(() => {
        commit('add')
      }, 1000)
    },
  },
  modules: {},
})
import Vue from 'vue'
import Vuex from './vuex'
// this.$store
Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    counter: 0,
  },
  mutations: {
    // state从哪里来的
    add(state) {
      state.counter++
    },
  },
  getters: {
    doubleCounter(state) {
      return state.counter * 2
    },
  },
  actions: {
    add({ commit }) {
      setTimeout(() => {
        commit('add')
      }, 1000)
    },
  },
  modules: {},
})

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

分析

企业级项目中渲染大量数据的情况比较常见,因此这是一道非常好的综合实践题目。

回答

  1. 在大型企业级项目中经常需要渲染大量数据,此时很容易出现卡顿的情况。比如大数据量的表格、树
  2. 处理时要根据情况做不同处理:
  • 可以采取分页的方式获取,避免渲染大量数据
  • vue-virtual-scroller (opens new window)等虚拟滚动方案,只渲染视口范围内的数据
  • 如果不需要更新,可以使用 v-once 方式只渲染一次
  • 通过v-memo (opens new window)可以缓存结果,结合v-for使用,避免数据变化时不必要的VNode创建
  • 可以采用懒加载方式,在用户需要的时候再加载数据,比如tree组件子树的懒加载
  1. 还是要看具体需求,首先从设计上避免大数据获取和渲染;实在需要这样做可以采用虚表的方式优化渲染;最后优化更新,如果不需要更新可以v-once处理,需要更新可以v-memo进一步优化大数据更新性能。其他可以采用的是交互方式优化,无线滚动、懒加载等方案

动态给 vue 的 data 添加一个新的属性时会发生什么?怎样解决? ​

Vue 不允许在已经创建的实例上动态添加新的响应式属性

若想实现数据与视图同步更新,可采取下面三种解决方案:

  • Vue.set()
  • Object.assign()
  • $forcecUpdated()

Vue.set() ​

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

参数

  • {Object | Array} target
  • {string | number} propertyName/index
  • {any} value

返回值:设置的值

通过Vue.set向响应式对象中添加一个property,并确保这个新 property同样是响应式的,且触发视图更新

关于Vue.set源码(省略了很多与本节不相关的代码)

源码位置:src\core\observer\index.js

js
function set (target: Array<any> | Object, key: any, val: any): any {
  ...
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}
function set (target: Array<any> | Object, key: any, val: any): any {
  ...
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

这里无非再次调用defineReactive方法,实现新增属性的响应式

关于defineReactive方法,内部还是通过Object.defineProperty实现属性拦截

大致代码如下:

js
function defineReactive(obj, key, val) {
  Object.defineProperty(obj, key, {
    get() {
      console.log(`get ${key}:${val}`)
      return val
    },
    set(newVal) {
      if (newVal !== val) {
        console.log(`set ${key}:${newVal}`)
        val = newVal
      }
    },
  })
}
function defineReactive(obj, key, val) {
  Object.defineProperty(obj, key, {
    get() {
      console.log(`get ${key}:${val}`)
      return val
    },
    set(newVal) {
      if (newVal !== val) {
        console.log(`set ${key}:${newVal}`)
        val = newVal
      }
    },
  })
}

Object.assign() ​

直接使用Object.assign()添加到对象的新属性不会触发更新

应创建一个新的对象,合并原对象和混入对象的属性

js
this.someObject = Object.assign({},this.someObject,{newProperty1:1,newProperty2:2 ...})
this.someObject = Object.assign({},this.someObject,{newProperty1:1,newProperty2:2 ...})

$forceUpdate ​

如果你发现你自己需要在 Vue中做一次强制更新,99.9% 的情况,是你在某个地方做错了事

$forceUpdate迫使Vue 实例重新渲染

PS:仅仅影响实例本身和插入插槽内容的子组件,而不是所有子组件。

小结 ​

  • 如果为对象添加少量的新属性,可以直接采用Vue.set()
  • 如果需要为新对象添加大量的新属性,则通过Object.assign()创建新对象
  • 如果你实在不知道怎么操作时,可采取$forceUpdate()进行强制刷新 (不建议)

PS:vue3是用过proxy实现数据响应式的,直接动态添加新属性仍可以实现数据响应式

你是怎么处理 vue 项目中的错误的? ​

分析

  • 这是一个综合应用题目,在项目中我们常常需要将 App 的异常上报,此时错误处理就很重要了。
  • 这里要区分错误的类型,针对性做收集。
  • 然后是将收集的的错误信息上报服务器。

思路

  • 首先区分错误类型
  • 根据错误不同类型做相应收集
  • 收集的错误是如何上报服务器的

回答范例

  1. 应用中的错误类型分为"接口异常"和“代码逻辑异常”
  2. 我们需要根据不同错误类型做相应处理:接口异常是我们请求后端接口过程中发生的异常,可能是请求失败,也可能是请求获得了服务器响应,但是返回的是错误状态。以Axios为例,这类异常我们可以通过封装Axios,在拦截器中统一处理整个应用中请求的错误。代码逻辑异常是我们编写的前端代码中存在逻辑上的错误造成的异常,vue应用中最常见的方式是使用全局错误处理函数app.config.errorHandler收集错误
  3. 收集到错误之后,需要统一处理这些异常:分析错误,获取需要错误信息和数据。这里应该有效区分错误类型,如果是请求错误,需要上报接口信息,参数,状态码等;对于前端逻辑异常,获取错误名称和详情即可。另外还可以收集应用名称、环境、版本、用户信息,所在页面等。这些信息可以通过vuex存储的全局状态和路由信息获取

实践

axios拦截器中处理捕获异常:

js
// 响应拦截器
instance.interceptors.response.use(
  (response) => {
    return response.data
  },
  (error) => {
    // 存在response说明服务器有响应
    if (error.response) {
      let response = error.response
      if (response.status >= 400) {
        handleError(response)
      }
    } else {
      handleError(null)
    }
    return Promise.reject(error)
  },
)
// 响应拦截器
instance.interceptors.response.use(
  (response) => {
    return response.data
  },
  (error) => {
    // 存在response说明服务器有响应
    if (error.response) {
      let response = error.response
      if (response.status >= 400) {
        handleError(response)
      }
    } else {
      handleError(null)
    }
    return Promise.reject(error)
  },
)

vue中全局捕获异常:

js
import { createApp } from 'vue'

const app = createApp(...)

app.config.errorHandler = (err, instance, info) => {
  // report error to tracking services
}
import { createApp } from 'vue'

const app = createApp(...)

app.config.errorHandler = (err, instance, info) => {
  // report error to tracking services
}

处理接口请求错误:

js
function handleError(error, type) {
  if(type == 1) {
    // 接口错误,从config字段中获取请求信息
    let { url, method, params, data } = error.config
    let err_data = {
       url, method,
       params: { query: params, body: data },
       error: error.data?.message || JSON.stringify(error.data),
    })
  }
}
function handleError(error, type) {
  if(type == 1) {
    // 接口错误,从config字段中获取请求信息
    let { url, method, params, data } = error.config
    let err_data = {
       url, method,
       params: { query: params, body: data },
       error: error.data?.message || JSON.stringify(error.data),
    })
  }
}

处理前端逻辑错误:

js
function handleError(error, type) {
  if (type == 2) {
    let errData = null
    // 逻辑错误
    if (error instanceof Error) {
      let { name, message } = error
      errData = {
        type: name,
        error: message,
      }
    } else {
      errData = {
        type: 'other',
        error: JSON.strigify(error),
      }
    }
  }
}
function handleError(error, type) {
  if (type == 2) {
    let errData = null
    // 逻辑错误
    if (error instanceof Error) {
      let { name, message } = error
      errData = {
        type: name,
        error: message,
      }
    } else {
      errData = {
        type: 'other',
        error: JSON.strigify(error),
      }
    }
  }
}

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

什么是首屏加载 ​

首屏时间(First Contentful Paint),指的是浏览器从响应用户输入网址地址,到首屏内容渲染完成的时间,此时整个网页不一定要全部渲染完成,但需要展示当前视窗需要的内容

首屏加载可以说是用户体验中最重要的环节

关于计算首屏时间

利用performance.timing提供的数据:

image-20241106140505585

通过DOMContentLoad或者performance来计算出首屏时间

js
// 方案一:
document.addEventListener('DOMContentLoaded', (event) => {
    console.log('first contentful painting');
});
// 方案二:
performance.getEntriesByName("first-contentful-paint")[0].startTime

// performance.getEntriesByName("first-contentful-paint")[0]
// 会返回一个 PerformancePaintTiming的实例,结构如下:
{
  name: "first-contentful-paint",
  entryType: "paint",
  startTime: 507.80000002123415,
  duration: 0,
};
// 方案一:
document.addEventListener('DOMContentLoaded', (event) => {
    console.log('first contentful painting');
});
// 方案二:
performance.getEntriesByName("first-contentful-paint")[0].startTime

// performance.getEntriesByName("first-contentful-paint")[0]
// 会返回一个 PerformancePaintTiming的实例,结构如下:
{
  name: "first-contentful-paint",
  entryType: "paint",
  startTime: 507.80000002123415,
  duration: 0,
};

加载慢的原因 ​

在页面渲染的过程,导致加载速度慢的因素可能如下:

  • 网络延时问题
  • 资源文件体积是否过大
  • 资源是否重复发送请求去加载了
  • 加载脚本的时候,渲染内容堵塞了

解决方案 ​

常见的几种 SPA 首屏优化方式

  • 减小入口文件积
  • 静态资源本地缓存
  • UI 框架按需加载
  • 图片资源的压缩
  • 组件重复打包
  • 开启 GZip 压缩
  • 使用 SSR

1. 减小入口文件体积

常用的手段是路由懒加载,把不同路由对应的组件分割成不同的代码块,待路由被请求的时候会单独打包路由,使得入口文件变小,加载速度大大增加

img

在vue-router配置路由的时候,采用动态加载路由的形式

js
routes:[
    path: 'Blogs',
    name: 'ShowBlogs',
    component: () => import('./components/ShowBlogs.vue')
]
routes:[
    path: 'Blogs',
    name: 'ShowBlogs',
    component: () => import('./components/ShowBlogs.vue')
]

以函数的形式加载路由,这样就可以把各自的路由文件分别打包,只有在解析给定的路由时,才会加载路由组件

2. 静态资源本地缓存

后端返回资源问题:

  • 采用HTTP缓存,设置Cache-Control,Last-Modified,Etag等响应头
  • 采用Service Worker离线缓存

前端合理利用localStorage

3. UI 框架按需加载

在日常使用UI框架,例如element-UI、或者antd,我们经常性直接引用整个UI库

js
import ElementUI from 'element-ui'
Vue.use(ElementUI)
import ElementUI from 'element-ui'
Vue.use(ElementUI)

但实际上我用到的组件只有按钮,分页,表格,输入与警告 所以我们要按需引用

js
import { Button, Input, Pagination, Table, TableColumn, MessageBox } from 'element-ui'
Vue.use(Button)
Vue.use(Input)
Vue.use(Pagination)
import { Button, Input, Pagination, Table, TableColumn, MessageBox } from 'element-ui'
Vue.use(Button)
Vue.use(Input)
Vue.use(Pagination)

4. 组件重复打包

假设A.js文件是一个常用的库,现在有多个路由使用了A.js文件,这就造成了重复下载

解决方案:在webpack的config文件中,修改CommonsChunkPlugin的配置

js
minChunks: 3
minChunks: 3

minChunks为 3 表示会把使用 3 次及以上的包抽离出来,放进公共依赖文件,避免了重复加载组件

5. 图片资源的压缩

图片资源虽然不在编码过程中,但它却是对页面性能影响最大的因素

对于所有的图片资源,我们可以进行适当的压缩

对页面上使用到的icon,可以使用在线字体图标,或者雪碧图,将众多小图标合并到同一张图上,用以减轻http请求压力。

6. 开启 GZip 压缩

拆完包之后,我们再用gzip做一下压缩 安装compression-webpack-plugin

js
cnmp i compression-webpack-plugin -D
cnmp i compression-webpack-plugin -D

在vue.congig.js中引入并修改webpack配置

js
const CompressionPlugin = require('compression-webpack-plugin')

configureWebpack: (config) => {
        if (process.env.NODE_ENV === 'production') {
            // 为生产环境修改配置...
            config.mode = 'production'
            return {
                plugins: [new CompressionPlugin({
                    test: /\.js$|\.html$|\.css/, //匹配文件名
                    threshold: 10240, //对超过10k的数据进行压缩
                    deleteOriginalAssets: false //是否删除原文件
                })]
            }
        }
const CompressionPlugin = require('compression-webpack-plugin')

configureWebpack: (config) => {
        if (process.env.NODE_ENV === 'production') {
            // 为生产环境修改配置...
            config.mode = 'production'
            return {
                plugins: [new CompressionPlugin({
                    test: /\.js$|\.html$|\.css/, //匹配文件名
                    threshold: 10240, //对超过10k的数据进行压缩
                    deleteOriginalAssets: false //是否删除原文件
                })]
            }
        }

在服务器我们也要做相应的配置 如果发送请求的浏览器支持gzip,就发送给它gzip格式的文件 我的服务器是用express框架搭建的 只要安装一下compression就能使用

js
const compression = require('compression')
app.use(compression()) // 在其他中间件使用之前调用
const compression = require('compression')
app.use(compression()) // 在其他中间件使用之前调用

7. 使用 SSR

SSR(Server side ),也就是服务端渲染,组件或页面通过服务器生成 html 字符串,再发送到浏览器

从头搭建一个服务端渲染是很复杂的,vue应用建议使用Nuxt.js实现服务端渲染

小结 ​

减少首屏渲染时间的方法有很多,总的来讲可以分成两大部分 :资源加载优化 和 页面渲染优化

下图是更为全面的首屏优化的方案

img

大家可以根据自己项目的情况选择各种方式进行首屏渲染的优化

Vue 中常见性能优化 ​

Vue 性能优化方式

  • 合理使用v-show和v-if
  • 合理使用computed
  • v-for时添加key,以及避免和v-if同时使用
  • 自定义事件、DOM事件及时销毁
  • 合理使用异步组件,加载大组件
  • 合理使用keep-alive避免频繁重复渲染
  • data层级不要太深,避免递归性能不好
  • 使用vue-loader在开发环境做模板编译(预编译)
  • webpack层级优化
  • 前端通用性能优化,如图片懒加载
  • 使用SSR

编码优化:

  1. 使用v-show复用DOM:避免重复创建组件
html
<template>
  <div class="cell">
    <!-- 这种情况用v-show复用DOM,比v-if效果好 -->
    <div v-show="value" class="on">
      <Heavy :n="10000" />
    </div>
    <section v-show="!value" class="off">
      <Heavy :n="10000" />
    </section>
  </div>
</template>
<template>
  <div class="cell">
    <!-- 这种情况用v-show复用DOM,比v-if效果好 -->
    <div v-show="value" class="on">
      <Heavy :n="10000" />
    </div>
    <section v-show="!value" class="off">
      <Heavy :n="10000" />
    </section>
  </div>
</template>
  1. 合理使用路由懒加载、异步组件,有效拆分App尺寸,访问时才异步加载
js
const router = createRouter({
  routes: [
    // 借助webpack的import()实现异步组件
    { path: '/foo', component: () => import('./Foo.vue') },
  ],
})
const router = createRouter({
  routes: [
    // 借助webpack的import()实现异步组件
    { path: '/foo', component: () => import('./Foo.vue') },
  ],
})
  1. keep-alive缓存页面:避免重复创建组件实例,且能保留缓存组件状态
html
<router-view v-slot="{ Component }">
  <keep-alive>
    <component :is="Component"></component>
  </keep-alive>
</router-view>
<router-view v-slot="{ Component }">
  <keep-alive>
    <component :is="Component"></component>
  </keep-alive>
</router-view>
  1. v-once和v-memo:不再变化的数据使用v-once
html
<!-- single element -->
<span v-once>This will never change: {{msg}}</span>
<!-- the element have children -->
<div v-once>
  <h1>comment</h1>
  <p>{{msg}}</p>
</div>
<!-- component -->
<my-component v-once :comment="msg"></my-component>
<!-- `v-for` directive -->
<ul>
  <li v-for="i in list" v-once>{{i}}</li>
</ul>
<!-- single element -->
<span v-once>This will never change: {{msg}}</span>
<!-- the element have children -->
<div v-once>
  <h1>comment</h1>
  <p>{{msg}}</p>
</div>
<!-- component -->
<my-component v-once :comment="msg"></my-component>
<!-- `v-for` directive -->
<ul>
  <li v-for="i in list" v-once>{{i}}</li>
</ul>

按条件跳过更新时使用v-momo:下面这个列表只会更新选中状态变化项

html
<div v-for="item in list" :key="item.id" v-memo="[item.id === selected]">
  <p>ID: {{ item.id }} - selected: {{ item.id === selected }}</p>
  <p>...more child nodes</p>
</div>
<div v-for="item in list" :key="item.id" v-memo="[item.id === selected]">
  <p>ID: {{ item.id }} - selected: {{ item.id === selected }}</p>
  <p>...more child nodes</p>
</div>
  1. 长列表性能优化:如果是大数据长列表,可采用虚拟滚动,只渲染少部分区域的内容
html
<recycle-scroller class="items" :items="items" :item-size="24">
  <template v-slot="{ item }">
    <FetchItemView :item="item" @vote="voteItem(item)" />
  </template>
</recycle-scroller>
<recycle-scroller class="items" :items="items" :item-size="24">
  <template v-slot="{ item }">
    <FetchItemView :item="item" @vote="voteItem(item)" />
  </template>
</recycle-scroller>
  1. 防止内部泄漏,组件销毁后把全局变量和事件销毁:Vue 组件销毁时,会自动解绑它的全部指令及事件监听器,但是仅限于组件本身的事件
js
export default {
  created() {
    this.timer = setInterval(this.refresh, 2000)
  },
  beforeUnmount() {
    clearInterval(this.timer)
  },
}
export default {
  created() {
    this.timer = setInterval(this.refresh, 2000)
  },
  beforeUnmount() {
    clearInterval(this.timer)
  },
}
  1. 图片懒加载

对于图片过多的页面,为了加速页面加载速度,所以很多时候我们需要将页面内未出现在可视区域内的图片先不做加载,等到滚动到可视区域后再去加载

html
<!-- 参考 https://github.com/hilongjw/vue-lazyload -->
<img v-lazy="/static/img/1.png" />
<!-- 参考 https://github.com/hilongjw/vue-lazyload -->
<img v-lazy="/static/img/1.png" />
  1. 滚动到可视区域动态加载

https://tangbc.github.io/vue-virtual-scroll-list(opens new window)

  1. 第三方插件按需引入:(babel-plugin-component)

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

js
import { createApp } from 'vue'
import { Button, Select } from 'element-plus'

const app = createApp()
app.use(Button)
app.use(Select)
import { createApp } from 'vue'
import { Button, Select } from 'element-plus'

const app = createApp()
app.use(Button)
app.use(Select)
  1. 服务端渲染:SSR

如果SPA应用有首屏渲染慢的问题,可以考虑SSR

以及下面的其他方法

  • 不要将所有的数据都放在data中,data中的数据都会增加getter和setter,会收集对应的watcher
  • v-for 遍历为 item 添加 key
  • v-for 遍历避免同时使用 v-if
  • 区分 computed 和 watch 的使用
  • 拆分组件(提高复用性、增加代码的可维护性,减少不必要的渲染 )
  • 防抖、节流

用户体验

  • app-skeleton 骨架屏
  • pwa serviceworker

SEO 优化

  • 预渲染插件 prerender-spa-plugin
  • 服务端渲染 ssr

打包优化

  • Webpack 对图片进行压缩
  • 使用 cdn 的方式加载第三方模块
  • 多线程打包 happypack
  • splitChunks 抽离公共文件
  • 优化 SourceMap
  • 构建结果输出分析,利用 webpack-bundle-analyzer 可视化分析工具

基础的 Web 技术的优化

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

Vue 项目性能优化-详细 ​

Vue 框架通过数据双向绑定和虚拟 DOM 技术,帮我们处理了前端开发中最脏最累的 DOM 操作部分, 我们不再需要去考虑如何操作 DOM 以及如何最高效地操作 DOM;但 Vue 项目中仍然存在项目首屏优化、Webpack 编译配置优化等问题,所以我们仍然需要去关注 Vue 项目性能方面的优化,使项目具有更高效的性能、更好的用户体验

代码层面的优化 ​

1. v-if 和 v-show 区分使用场景

  • v-if 是 真正 的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块
  • v-show 就简单得多, 不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS display的none/block属性进行切换。
  • 所以,v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景;v-show 则适用于需要非常频繁切换条件的场景

2. computed 和 watch 区分使用场景

  • computed: 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;
  • watch: 更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作

运用场景:

  • 当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算;
  • 当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的

3. v-for 遍历必须为 item 添加 key,且避免同时使用 v-if

  • v-for
    v-for

    遍历必须为

    item
    item

    添加

    key
    key
    • 在列表数据进行遍历渲染时,需要为每一项 item 设置唯一 key 值,方便 Vue.js 内部机制精准找到该条列表数据。当 state 更新时,新的状态值和旧的状态值对比,较快地定位到 diff
  • v-for
    v-for

    遍历避免同时使用

    v-if
    v-if
    • vue2.x中v-for 比 v-if 优先级高,如果每一次都需要遍历整个数组,将会影响速度,尤其是当之需要渲染很小一部分的时候,必要情况下应该替换成 computed 属性

推荐:

text
<ul>
  <li
    v-for="user in activeUsers"
    :key="user.id">
    {{ user.name }}
  </li>
</ul>
computed: {
  activeUsers: function () {
    return this.users.filter(function (user) {
	 return user.isActive
    })
  }
}
<ul>
  <li
    v-for="user in activeUsers"
    :key="user.id">
    {{ user.name }}
  </li>
</ul>
computed: {
  activeUsers: function () {
    return this.users.filter(function (user) {
	 return user.isActive
    })
  }
}

不推荐:

html
<ul>
  <li v-for="user in users" v-if="user.isActive" :key="user.id">{{ user.name }}</li>
</ul>
<ul>
  <li v-for="user in users" v-if="user.isActive" :key="user.id">{{ user.name }}</li>
</ul>

4. 长列表性能优化

Vue 会通过 Object.defineProperty 对数据进行劫持,来实现视图响应数据的变化,然而有些时候我们的组件就是纯粹的数据展示,不会有任何改变,我们就不需要 Vue 来劫持我们的数据,在大量数据展示的情况下,这能够很明显的减少组件初始化的时间,那如何禁止 Vue 劫持我们的数据呢?可以通过 Object.freeze 方法来冻结一个对象,一旦被冻结的对象就再也不能被修改了

js
export default {
  data: () => ({
    users: {},
  }),
  async created() {
    const users = await axios.get('/api/users')
    this.users = Object.freeze(users)
  },
}
export default {
  data: () => ({
    users: {},
  }),
  async created() {
    const users = await axios.get('/api/users')
    this.users = Object.freeze(users)
  },
}

5. 事件的销毁

Vue 组件销毁时,会自动清理它与其它实例的连接,解绑它的全部指令及事件监听器,但是仅限于组件本身的事件。 如果在 js 内使用 addEventListener 等方式是不会自动销毁的,我们需要在组件销毁时手动移除这些事件的监听,以免造成内存泄露,如:

js
created() {
  addEventListener('click', this.click, false)
},
beforeDestroy() {
  removeEventListener('click', this.click, false)
}
created() {
  addEventListener('click', this.click, false)
},
beforeDestroy() {
  removeEventListener('click', this.click, false)
}

6. 图片资源懒加载

对于图片过多的页面,为了加速页面加载速度,所以很多时候我们需要将页面内未出现在可视区域内的图片先不做加载, 等到滚动到可视区域后再去加载。这样对于页面加载性能上会有很大的提升,也提高了用户体验。我们在项目中使用 Vue 的 vue-lazyload 插件

text
npm install vue-lazyload --save-dev
npm install vue-lazyload --save-dev

在入口文件 man.js 中引入并使用

js
import VueLazyload from 'vue-lazyload'

Vue.use(VueLazyload)

// 或者添加自定义选项
Vue.use(VueLazyload, {
  preLoad: 1.3,
  error: 'dist/error.png',
  loading: 'dist/loading.gif',
  attempt: 1,
})
import VueLazyload from 'vue-lazyload'

Vue.use(VueLazyload)

// 或者添加自定义选项
Vue.use(VueLazyload, {
  preLoad: 1.3,
  error: 'dist/error.png',
  loading: 'dist/loading.gif',
  attempt: 1,
})

在 vue 文件中将 img 标签的 src 属性直接改为 v-lazy ,从而将图片显示方式更改为懒加载显示

html
<img v-lazy="/static/img/1.png" />
<img v-lazy="/static/img/1.png" />

以上为 vue-lazyload 插件的简单使用,如果要看插件的更多参数选项,可以查看 vue-lazyload 的 github 地址(opens new window)

7. 路由懒加载

Vue 是单页面应用,可能会有很多的路由引入 ,这样使用 webpcak 打包后的文件很大,当进入首页时,加载的资源过多,页面会出现白屏的情况,不利于用户体验。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应的组件,这样就更加高效了。这样会大大提高首屏显示的速度,但是可能其他的页面的速度就会降下来

路由懒加载:

js
const Foo = () => import('./Foo.vue')
const router = new VueRouter({
  routes: [{ path: '/foo', component: Foo }],
})
const Foo = () => import('./Foo.vue')
const router = new VueRouter({
  routes: [{ path: '/foo', component: Foo }],
})

8. 第三方插件的按需引入

我们在项目中经常会需要引入第三方插件,如果我们直接引入整个插件,会导致项目的体积太大,我们可以借助 babel-plugin-component ,然后可以只引入需要的组件,以达到减小项目体积的目的。以下为项目中引入 element-ui 组件库为例

text
npm install babel-plugin-component -D
npm install babel-plugin-component -D

将 .babelrc 修改为:

json
{
  "presets": [["es2015", { "modules": false }]],
  "plugins": [
    [
      "component",
      {
        "libraryName": "element-ui",
        "styleLibraryName": "theme-chalk"
      }
    ]
  ]
}
{
  "presets": [["es2015", { "modules": false }]],
  "plugins": [
    [
      "component",
      {
        "libraryName": "element-ui",
        "styleLibraryName": "theme-chalk"
      }
    ]
  ]
}

在 main.js 中引入部分组件:

js
import Vue from 'vue'
import { Button, Select } from 'element-ui'

Vue.use(Button)
Vue.use(Select)
import Vue from 'vue'
import { Button, Select } from 'element-ui'

Vue.use(Button)
Vue.use(Select)

9. 优化无限列表性能

如果你的应用存在非常长或者无限滚动的列表,那么需要采用虚拟列表的技术来优化性能,只需要渲染少部分区域的内容,减少重新渲染组件和创建 dom 节点的时间。 你可以参考以下开源项目 vue-virtual-scroll-list (opens new window) 和 vue-virtual-scroller (opens new window)来优化这种无限列表的场景的

10. 服务端渲染 SSR or 预渲染

服务端渲染是指 Vue 在客户端将标签渲染成的整个 html 片段的工作在服务端完成,服务端形成的 html 片段直接返回给客户端这个过程就叫做服务端渲染。

  • 如果你的项目的 SEO 和 首屏渲染是评价项目的关键指标,那么你的项目就需要服务端渲染来帮助你实现最佳的初始加载性能和 SEO
  • 如果你的 Vue 项目只需改善少数营销页面(例如 /, /about, /contact 等)的 SEO,那么你可能需要预渲染,在构建时简单地生成针对特定路由的静态 HTML 文件。优点是设置预渲染更简单,并可以将你的前端作为一个完全静态的站点,具体你可以使用 prerender-spa-plugin (opens new window) 就可以轻松地添加预渲染

Webpack 层面的优化 ​

1. Webpack 对图片进行压缩

对小于 limit 的图片转化为 base64 格式,其余的不做操作。所以对有些较大的图片资源,在请求资源的时候,加载会很慢,我们可以用 image-webpack-loader来压缩图片

text
npm install image-webpack-loader --save-dev



{
  test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
  use:[
    {
    loader: 'url-loader',
    options: {
      limit: 10000,
      name: utils.assetsPath('img/[name].[hash:7].[ext]')
      }
    },
    {
      loader: 'image-webpack-loader',
      options: {
        bypassOnDebug: true,
      }
    }
  ]
}
npm install image-webpack-loader --save-dev



{
  test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
  use:[
    {
    loader: 'url-loader',
    options: {
      limit: 10000,
      name: utils.assetsPath('img/[name].[hash:7].[ext]')
      }
    },
    {
      loader: 'image-webpack-loader',
      options: {
        bypassOnDebug: true,
      }
    }
  ]
}

2. 减少 ES6 转为 ES5 的冗余代码

Babel 插件会在将 ES6 代码转换成 ES5 代码时会注入一些辅助函数,例如下面的 ES6 代码

js
class HelloWebpack extends Component{...}
class HelloWebpack extends Component{...}

这段代码再被转换成能正常运行的 ES5 代码时需要以下两个辅助函数:

js
babel - runtime / helpers / createClass // 用于实现 class 语法
babel - runtime / helpers / inherits // 用于实现 extends 语法
babel - runtime / helpers / createClass // 用于实现 class 语法
babel - runtime / helpers / inherits // 用于实现 extends 语法

在默认情况下, Babel 会在每个输出文件中内嵌这些依赖的辅助函数代码,如果多个源代码文件都依赖这些辅助函数,那么这些辅助函数的代码将会出现很多次,造成代码冗余。为了不让这些辅助函数的代码重复出现,可以在依赖它们时通过 require('babel-runtime/helpers/createClass') 的方式导入,这样就能做到只让它们出现一次。babel-plugin-transform-runtime 插件就是用来实现这个作用的,将相关辅助函数进行替换成导入语句,从而减小 babel 编译出来的代码的文件大小

js
npm install babel-plugin-transform-runtime --save-dev
npm install babel-plugin-transform-runtime --save-dev

修改 .babelrc 配置文件为:

json
"plugins": [
    "transform-runtime"
]
"plugins": [
    "transform-runtime"
]

3. 提取公共代码

如果项目中没有去将每个页面的第三方库和公共模块提取出来,则项目会存在以下问题:

  • 相同的资源被重复加载,浪费用户的流量和服务器的成本。
  • 每个页面需要加载的资源太大,导致网页首屏加载缓慢,影响用户体验。

所以我们需要将多个页面的公共代码抽离成单独的文件,来优化以上问题 。Webpack 内置了专门用于提取多个Chunk 中的公共部分的插件 CommonsChunkPlugin,我们在项目中 CommonsChunkPlugin 的配置如下:

js
// 所有在 package.json 里面依赖的包,都会被打包进 vendor.js 这个文件中。
new webpack.optimize.CommonsChunkPlugin({
  name: 'vendor',
  minChunks: function (module, count) {
    return (
      module.resource &&
      /\.js$/.test(module.resource) &&
      module.resource.indexOf(path.join(__dirname, '../node_modules')) === 0
    )
  },
}),
  // 抽取出代码模块的映射关系
  new webpack.optimize.CommonsChunkPlugin({
    name: 'manifest',
    chunks: ['vendor'],
  })
// 所有在 package.json 里面依赖的包,都会被打包进 vendor.js 这个文件中。
new webpack.optimize.CommonsChunkPlugin({
  name: 'vendor',
  minChunks: function (module, count) {
    return (
      module.resource &&
      /\.js$/.test(module.resource) &&
      module.resource.indexOf(path.join(__dirname, '../node_modules')) === 0
    )
  },
}),
  // 抽取出代码模块的映射关系
  new webpack.optimize.CommonsChunkPlugin({
    name: 'manifest',
    chunks: ['vendor'],
  })

4. 模板预编译

  • 当使用 DOM 内模板或 JavaScript 内的字符串模板时,模板会在运行时被编译为渲染函数。通常情况下这个过程已经足够快了,但对性能敏感的应用还是最好避免这种用法。
  • 预编译模板最简单的方式就是使用单文件组件——相关的构建设置会自动把预编译处理好,所以构建好的代码已经包含了编译出来的渲染函数而不是原始的模板字符串。
  • 如果你使用 webpack,并且喜欢分离 JavaScript 和模板文件,你可以使用 vue-template-loader (opens new window),它也可以在构建过程中把模板文件转换成为 JavaScript 渲染函数

5. 提取组件的 CSS

当使用单文件组件时,组件内的 CSS 会以 style 标签的方式通过 JavaScript 动态注入。这有一些小小的运行时开销,如果你使用服务端渲染,这会导致一段 “无样式内容闪烁 (fouc) ” 。将所有组件的 CSS 提取到同一个文件可以避免这个问题,也会让 CSS 更好地进行压缩和缓存

6. 优化 SourceMap

我们在项目进行打包后,会将开发中的多个文件代码打包到一个文件中,并且经过压缩、去掉多余的空格、babel 编译化后,最终将编译得到的代码会用于线上环境,那么这样处理后的代码和源代码会有很大的差别,当有 bug 的时候,我们只能定位到压缩处理后的代码位置,无法定位到开发环境中的代码,对于开发来说不好调式定位问题,因此 sourceMap 出现了,它就是为了解决不好调式代码问题的

SourceMap 的可选值如下(+ 号越多,代表速度越快,- 号越多,代表速度越慢, o 代表中等速度)

img

  • 开发环境推荐: cheap-module-eval-source-map
  • 生产环境推荐: cheap-module-source-map

原因如下:

  • cheap: 源代码中的列信息是没有任何作用,因此我们打包后的文件不希望包含列相关信息,只有行信息能建立打包前后的依赖关系。因此不管是开发环境或生产环境,我们都希望添加 cheap 的基本类型来忽略打包前后的列信息;
  • module :不管是开发环境还是正式环境,我们都希望能定位到bug的源代码具体的位置,比如说某个 Vue 文件报错了,我们希望能定位到具体的 Vue 文件,因此我们也需要 module配置;
  • soure-map :source-map 会为每一个打包后的模块生成独立的 soucemap 文件 ,因此我们需要增加source-map 属性;
  • eval-source-map:eval 打包代码的速度非常快,因为它不生成 map 文件,但是可以对 eval 组合使用 eval-source-map 使用会将 map 文件以 DataURL 的形式存在打包后的 js 文件中。在正式环境中不要使用 eval-source-map, 因为它会增加文件的大小,但是在开发环境中,可以试用下,因为他们打包的速度很快。

7. 构建结果输出分析

Webpack 输出的代码可读性非常差而且文件非常大,让我们非常头疼。为了更简单、直观地分析输出结果,社区中出现了许多可视化分析工具。这些工具以图形的方式将结果更直观地展示出来,让我们快速了解问题所在。接下来讲解我们在 Vue 项目中用到的分析工具:webpack-bundle-analyzer

js
if (config.build.bundleAnalyzerReport) {
  var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
  webpackConfig.plugins.push(new BundleAnalyzerPlugin())
}
if (config.build.bundleAnalyzerReport) {
  var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
  webpackConfig.plugins.push(new BundleAnalyzerPlugin())
}

执行 $ npm run build --report 后生成分析报告如下

img

基础的 Web 技术优化 ​

1. 开启 gzip 压缩

gzip 是 GNUzip 的缩写,最早用于 UNIX 系统的文件压缩。HTTP 协议上的 gzip 编码是一种用来改进 web 应用程序性能的技术,web 服务器和客户端(浏览器)必须共同支持 gzip。目前主流的浏览器,Chrome,firefox,IE 等都支持该协议。常见的服务器如 Apache,Nginx,IIS 同样支持,zip 压缩效率非常高,通常可以达到 70% 的压缩率,也就是说,如果你的网页有 30K,压缩之后就变成了 9K 左右

以下我们以服务端使用我们熟悉的 express 为例,开启 gzip 非常简单,相关步骤如下:

text
npm install compression --save



var compression = require('compression');
var app = express();
app.use(compression())
npm install compression --save



var compression = require('compression');
var app = express();
app.use(compression())

重启服务,观察网络面板里面的 response header,如果看到如下红圈里的字段则表明 gzip 开启成功

img

Nginx 开启 gzip 压缩

bash
#是否启动gzip压缩,on代表启动,off代表开启
gzip  on;

#需要压缩的常见静态资源
gzip_types text/plain application/javascript   application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;

#由于nginx的压缩发生在浏览器端而微软的ie6很坑爹,会导致压缩后图片看不见所以该选
项是禁止ie6发生压缩
gzip_disable "MSIE [1-6]\.";

#如果文件大于1k就启动压缩
gzip_min_length 1k;

#以16k为单位,按照原始数据的大小以4倍的方式申请内存空间,一般此项不要修改
gzip_buffers 4 16k;

#压缩的等级,数字选择范围是1-9,数字越小压缩的速度越快,消耗cpu就越大
gzip_comp_level 2;
#是否启动gzip压缩,on代表启动,off代表开启
gzip  on;

#需要压缩的常见静态资源
gzip_types text/plain application/javascript   application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;

#由于nginx的压缩发生在浏览器端而微软的ie6很坑爹,会导致压缩后图片看不见所以该选
项是禁止ie6发生压缩
gzip_disable "MSIE [1-6]\.";

#如果文件大于1k就启动压缩
gzip_min_length 1k;

#以16k为单位,按照原始数据的大小以4倍的方式申请内存空间,一般此项不要修改
gzip_buffers 4 16k;

#压缩的等级,数字选择范围是1-9,数字越小压缩的速度越快,消耗cpu就越大
gzip_comp_level 2;

要想配置生效,记得重启nginx服务

text
nginx -t
nginx -s reload
nginx -t
nginx -s reload

2. 浏览器缓存

为了提高用户加载页面的速度,对静态资源进行缓存是非常必要的,根据是否需要重新向服务器发起请求来分类,将 HTTP 缓存规则分为两大类(强制缓存,对比缓存)

3. CDN 的使用

浏览器从服务器上下载 CSS、js 和图片等文件时都要和服务器连接,而大部分服务器的带宽有限,如果超过限制,网页就半天反应不过来。而 CDN 可以通过不同的域名来加载文件,从而使下载文件的并发连接数大大增加,且 CDN 具有更好的可用性,更低的网络延迟和丢包率

4. 使用 Chrome Performance 查找性能瓶颈

Chrome 的 Performance 面板可以录制一段时间内的 js 执行细节及时间。使用 Chrome 开发者工具分析页面性能的步骤如下。

  • 打开 Chrome 开发者工具,切换到 Performance 面板
  • 点击 Record 开始录制
  • 刷新页面或展开某个节点
  • 点击 Stop 停止录制

img

Vue 与 Angular 以及 React 的区别? ​

Vue 与 AngularJS 的区别 ​

  • Angular采用TypeScript开发, 而Vue可以使用javascript也可以使用TypeScript
  • AngularJS依赖对数据做脏检查,所以Watcher越多越慢;Vue.js使用基于依赖追踪的观察并且使用异步队列更新,所有的数据都是独立触发的。
  • AngularJS社区完善, Vue的学习成本较小

Vue 与 React 的区别 ​

相同点:

  1. Virtual DOM。其中最大的一个相似之处就是都使用了Virtual DOM。(当然Vue是在Vue2.x才引用的)也就是能让我们通过操作数据的方式来改变真实的DOM状态。因为其实Virtual DOM的本质就是一个JS对象,它保存了对真实DOM的所有描述,是真实DOM的一个映射,所以当我们在进行频繁更新元素的时候,改变这个JS对象的开销远比直接改变真实DOM要小得多。
  2. 组件化的开发思想。第二点来说就是它们都提倡这种组件化的开发思想,也就是建议将应用分拆成一个个功能明确的模块,再将这些模块整合在一起以满足我们的业务需求。
  3. Props。Vue和React中都有props的概念,允许父组件向子组件传递数据。
  4. 构建工具、Chrome 插件、配套框架。还有就是它们的构建工具以及 Chrome 插件、配套框架都很完善。比如构建工具,React中可以使用CRA,Vue中可以使用对应的脚手架vue-cli。对于配套框架Vue中有vuex、vue-router,React中有react-router、redux。

不同点

  1. 模版的编写。最大的不同就是模版的编写,Vue鼓励你去写近似常规HTML的模板,React推荐你使用JSX去书写。
  2. 状态管理与对象属性。在React中,应用的状态是比较关键的概念,也就是state对象,它允许你使用setState去更新状态。但是在Vue中,state对象并不是必须的,数据是由data属性在Vue对象中进行管理。
  3. 虚拟DOM的处理方式不同。Vue中的虚拟DOM控制了颗粒度,组件层面走watcher通知,而组件内部走vdom做diff,这样,既不会有太多watcher,也不会让vdom的规模过大。而React走了类似于CPU调度的逻辑,把vdom这棵树,微观上变成了链表,然后利用浏览器的空闲时间来做diff

Vue2 高级用法 ​

自定义组件 model ​

vue
<!-- 自定义 v-model -->
<CustomVModel v-model="name" />

<!-- CustomVModel -->
<template>
  <!-- 例如:vue 颜色选择 -->
  <input type="text" :value="text" @input="$emit('change', $event.target.value)" />
  <!--
        1. 上面的 input 使用了 :value 而不是 v-model
        2. 上面的 change 和 model.event 要对应起来
        3. text 属性对应起来
    -->
</template>

<script>
export default {
  model: {
    prop: 'text', // 对应 props text
    event: 'change',
  },
  props: {
    text1: String,
    default() {
      return ''
    },
  },
}
</script>
<!-- 自定义 v-model -->
<CustomVModel v-model="name" />

<!-- CustomVModel -->
<template>
  <!-- 例如:vue 颜色选择 -->
  <input type="text" :value="text" @input="$emit('change', $event.target.value)" />
  <!--
        1. 上面的 input 使用了 :value 而不是 v-model
        2. 上面的 change 和 model.event 要对应起来
        3. text 属性对应起来
    -->
</template>

<script>
export default {
  model: {
    prop: 'text', // 对应 props text
    event: 'change',
  },
  props: {
    text1: String,
    default() {
      return ''
    },
  },
}
</script>

vue3 自定义组件 model ​

vue
<!-- index.vue -->
<CustomVModel v-model="name" />

<!-- CustomVModel.vue -->
<template>
  <input type="text" :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" />
</template>

<script>
export default {
  props: {
    modelValue: String,
  },
}
</script>
<!-- index.vue -->
<CustomVModel v-model="name" />

<!-- CustomVModel.vue -->
<template>
  <input type="text" :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" />
</template>

<script>
export default {
  props: {
    modelValue: String,
  },
}
</script>

vue2 slot ​

vue
<!-- slotDemo.vue 默认插槽 -->
<template>
  <a :href="url">
    <slot>默认内容,即父组件没设置内容时,这里显示</slot>
  </a>
</template>

<script>
export default {
  props: ['url'],
  data() {
    return {}
  },
}
</script>

<!-- 用法 -->
<SlotDemo :url="website.url">
    {{website.title}}
</SlotDemo>

<!-- ScopedSlotDemo.vue 作用域插槽demo -->
<template>
  <a :href="url">
    <slot :slotData="website">
      {{ website.subTitle }}
      <!-- 默认值显示 subTitle ,即父组件不传内容时 -->
    </slot>
  </a>
</template>

<script>
export default {
  props: ['url'],
  data() {
    return {
      website: {
        url: 'http://test.com/',
        title: 'test',
        subTitle: 'subtitle',
      },
    }
  },
}
</script>

<!-- 用法 -->
<ScopedSlotDemo :url="website.url">
    <template v-slot="slotProps">
        {{slotProps.slotData.title}}
    </template>
</ScopedSlotDemo>

<!-- 具名插槽 -->
<div>
    <header>
        <slot name="header" />
    </header>
    <main>
        <slot  />
    </main>
    <footer>
        <slot name="footer" />
    </footer>
</div>

<!-- 用法 -->
<template>
  <template #header></template>
  <p>默认显示到main slot中</p>
  <template #footer></template>
</template>
<!-- slotDemo.vue 默认插槽 -->
<template>
  <a :href="url">
    <slot>默认内容,即父组件没设置内容时,这里显示</slot>
  </a>
</template>

<script>
export default {
  props: ['url'],
  data() {
    return {}
  },
}
</script>

<!-- 用法 -->
<SlotDemo :url="website.url">
    {{website.title}}
</SlotDemo>

<!-- ScopedSlotDemo.vue 作用域插槽demo -->
<template>
  <a :href="url">
    <slot :slotData="website">
      {{ website.subTitle }}
      <!-- 默认值显示 subTitle ,即父组件不传内容时 -->
    </slot>
  </a>
</template>

<script>
export default {
  props: ['url'],
  data() {
    return {
      website: {
        url: 'http://test.com/',
        title: 'test',
        subTitle: 'subtitle',
      },
    }
  },
}
</script>

<!-- 用法 -->
<ScopedSlotDemo :url="website.url">
    <template v-slot="slotProps">
        {{slotProps.slotData.title}}
    </template>
</ScopedSlotDemo>

<!-- 具名插槽 -->
<div>
    <header>
        <slot name="header" />
    </header>
    <main>
        <slot  />
    </main>
    <footer>
        <slot name="footer" />
    </footer>
</div>

<!-- 用法 -->
<template>
  <template #header></template>
  <p>默认显示到main slot中</p>
  <template #footer></template>
</template>

vue2 动态组件 ​

动态组件

  • :is="component-name
  • 需要根据数据,动态渲染的场景。即组件类型不确定
vue
<component :is="compoentName" />
<component :is="compoentName" />

vue2 异步组件 ​

  • import()函数
  • 按需加载异步大组件
vue
<!-- 异步组件 -->
<template>
  <FormDemo v-if="showFormDemo" />
  <button @click="showFormDemo = true">show form demo</button>
</template>
<script>
export default {
  components: {
    FormDemo: () => import('./FormDemo'), // 异步加载组件,初始化不加载
  },
}
</script>
<!-- 异步组件 -->
<template>
  <FormDemo v-if="showFormDemo" />
  <button @click="showFormDemo = true">show form demo</button>
</template>
<script>
export default {
  components: {
    FormDemo: () => import('./FormDemo'), // 异步加载组件,初始化不加载
  },
}
</script>

Vue 面试考察的高频原理 ​

响应式原理 ​

响应式

  • 组件data数据一旦变化,立刻触发视图的更新

  • 实现数据驱动视图的第一步

  • 核心 API:

    Object.defineProperty
    Object.defineProperty
    • 缺点
      • 深度监听,需要递归到底,一次计算量大
      • 无法监听新增属性、删除属性(使用Vue.set、Vue.delete可以)
      • 无法监听原生数组,需要重写数组原型
js
// 触发更新视图
function updateView() {
  console.log('视图更新')
}

// 重新定义数组原型
const oldArrayProperty = Array.prototype
// 创建新对象,原型指向 oldArrayProperty ,再扩展新的方法不会影响原型
const arrProto = Object.create(oldArrayProperty)
;['push', 'pop', 'shift', 'unshift', 'splice'].forEach((methodName) => {
  arrProto[methodName] = function () {
    updateView() // 触发视图更新
    oldArrayProperty[methodName].call(this, ...arguments)
    // Array.prototype.push.call(this, ...arguments)
  }
})

// 重新定义属性,监听起来
function defineReactive(target, key, value) {
  // 深度监听
  observer(value)

  // 核心 API
  Object.defineProperty(target, key, {
    get() {
      return value
    },
    set(newValue) {
      if (newValue !== value) {
        // 深度监听
        observer(newValue)

        // 设置新值
        // 注意,value 一直在闭包中,此处设置完之后,再 get 时也是会获取最新的值
        value = newValue

        // 触发更新视图
        updateView()
      }
    },
  })
}

// 监听对象属性
function observer(target) {
  if (typeof target !== 'object' || target === null) {
    // 不是对象或数组
    return target
  }

  // 污染全局的 Array 原型
  // Array.prototype.push = function () {
  //     updateView()
  //     ...
  // }

  if (Array.isArray(target)) {
    target.__proto__ = arrProto
  }

  // 重新定义各个属性(for in 也可以遍历数组)
  for (let key in target) {
    defineReactive(target, key, target[key])
  }
}

// 准备数据
const data = {
  name: 'zhangsan',
  age: 20,
  info: {
    address: 'shenzhen', // 需要深度监听
  },
  nums: [10, 20, 30],
}

// 监听数据
observer(data)

// 测试
// data.name = 'lisi'
// data.age = 21
// // console.log('age', data.age)
// data.x = '100' // 新增属性,监听不到 —— 所以有 Vue.set
// delete data.name // 删除属性,监听不到 —— 所有已 Vue.delete
// data.info.address = '上海' // 深度监听
data.nums.push(4) // 监听数组

// proxy-demo

// const data = {
//     name: 'zhangsan',
//     age: 20,
// }
const data = ['a', 'b', 'c']

const proxyData = new Proxy(data, {
  get(target, key, receiver) {
    // 只处理本身(非原型的)属性
    const ownKeys = Reflect.ownKeys(target)
    if (ownKeys.includes(key)) {
      console.log('get', key) // 监听
    }

    const result = Reflect.get(target, key, receiver)
    return result // 返回结果
  },
  set(target, key, val, receiver) {
    // 重复的数据,不处理
    if (val === target[key]) {
      return true
    }

    const result = Reflect.set(target, key, val, receiver)
    console.log('set', key, val)
    // console.log('result', result) // true
    return result // 是否设置成功
  },
  deleteProperty(target, key) {
    const result = Reflect.deleteProperty(target, key)
    console.log('delete property', key)
    // console.log('result', result) // true
    return result // 是否删除成功
  },
})
// 触发更新视图
function updateView() {
  console.log('视图更新')
}

// 重新定义数组原型
const oldArrayProperty = Array.prototype
// 创建新对象,原型指向 oldArrayProperty ,再扩展新的方法不会影响原型
const arrProto = Object.create(oldArrayProperty)
;['push', 'pop', 'shift', 'unshift', 'splice'].forEach((methodName) => {
  arrProto[methodName] = function () {
    updateView() // 触发视图更新
    oldArrayProperty[methodName].call(this, ...arguments)
    // Array.prototype.push.call(this, ...arguments)
  }
})

// 重新定义属性,监听起来
function defineReactive(target, key, value) {
  // 深度监听
  observer(value)

  // 核心 API
  Object.defineProperty(target, key, {
    get() {
      return value
    },
    set(newValue) {
      if (newValue !== value) {
        // 深度监听
        observer(newValue)

        // 设置新值
        // 注意,value 一直在闭包中,此处设置完之后,再 get 时也是会获取最新的值
        value = newValue

        // 触发更新视图
        updateView()
      }
    },
  })
}

// 监听对象属性
function observer(target) {
  if (typeof target !== 'object' || target === null) {
    // 不是对象或数组
    return target
  }

  // 污染全局的 Array 原型
  // Array.prototype.push = function () {
  //     updateView()
  //     ...
  // }

  if (Array.isArray(target)) {
    target.__proto__ = arrProto
  }

  // 重新定义各个属性(for in 也可以遍历数组)
  for (let key in target) {
    defineReactive(target, key, target[key])
  }
}

// 准备数据
const data = {
  name: 'zhangsan',
  age: 20,
  info: {
    address: 'shenzhen', // 需要深度监听
  },
  nums: [10, 20, 30],
}

// 监听数据
observer(data)

// 测试
// data.name = 'lisi'
// data.age = 21
// // console.log('age', data.age)
// data.x = '100' // 新增属性,监听不到 —— 所以有 Vue.set
// delete data.name // 删除属性,监听不到 —— 所有已 Vue.delete
// data.info.address = '上海' // 深度监听
data.nums.push(4) // 监听数组

// proxy-demo

// const data = {
//     name: 'zhangsan',
//     age: 20,
// }
const data = ['a', 'b', 'c']

const proxyData = new Proxy(data, {
  get(target, key, receiver) {
    // 只处理本身(非原型的)属性
    const ownKeys = Reflect.ownKeys(target)
    if (ownKeys.includes(key)) {
      console.log('get', key) // 监听
    }

    const result = Reflect.get(target, key, receiver)
    return result // 返回结果
  },
  set(target, key, val, receiver) {
    // 重复的数据,不处理
    if (val === target[key]) {
      return true
    }

    const result = Reflect.set(target, key, val, receiver)
    console.log('set', key, val)
    // console.log('result', result) // true
    return result // 是否设置成功
  },
  deleteProperty(target, key) {
    const result = Reflect.deleteProperty(target, key)
    console.log('delete property', key)
    // console.log('result', result) // true
    return result // 是否删除成功
  },
})

vdom 和 diff 算法 ​

1. vdom

  • 背景
    • DOM操作非常耗时
    • 以前用jQuery,可以自行控制DOM操作时机,手动调整
    • Vue和React是数据驱动视图,如何有效控制DOM操作
  • 解决方案 VDOM
    • 有了一定的复杂度,想减少计算次数比较难
    • 能不能把计算,更多的转移为 JS 计算?因为JS执行速度很快
    • vdom 用JS模拟DOM结构,计算出最小的变更,操作DOM
  • 用 JS 模拟 DOM 结构
  • img
  • 通过 snabbdom 学习 vdom
    • 简洁强大的vdom库
    • vue2参考它实现的vdom和diff
    • snabbdom
      • h函数
      • vnode数据结构
      • patch函数
  • vdom 总结
    • 用JS模拟DOM结构(vnode)
    • 新旧vnode对比,得出最小的更新范围,有效控制DOM操作
    • 数据驱动视图模式下,有效控制DOM操作
html
<!-- snabbdom-demo -->

<div id="container"></div>
<button id="btn-change">change</button>

<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-class.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-props.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-style.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-eventlisteners.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/h.js"></script>
<script>
  const snabbdom = window.snabbdom

  // 定义 patch
  const patch = snabbdom.init([
    snabbdom_class,
    snabbdom_props,
    snabbdom_style,
    snabbdom_eventlisteners,
  ])

  // 定义 h
  const h = snabbdom.h

  const container = document.getElementById('container')

  // 生成 vnode
  const vnode = h('ul#list', {}, [h('li.item', {}, 'Item 1'), h('li.item', {}, 'Item 2')])
  patch(container, vnode)

  document.getElementById('btn-change').addEventListener('click', () => {
    // 生成 newVnode
    const newVnode = h('ul#list', {}, [
      h('li.item', {}, 'Item 1'),
      h('li.item', {}, 'Item B'),
      h('li.item', {}, 'Item 3'),
    ])
    patch(vnode, newVnode)

    // vnode = newVnode // patch 之后,应该用新的覆盖现有的 vnode ,否则每次 change 都是新旧对比
  })
</script>

<!-- table-without-vdom.html -->
<div id="container"></div>
<button id="btn-change">change</button>

<script type="text/javascript" src="https://cdn.bootcss.com/jquery/3.2.0/jquery.js"></script>
<script type="text/javascript">
  const data = [
    {
      name: '张三',
      age: '20',
      address: '北京',
    },
    {
      name: '李四',
      age: '21',
      address: '上海',
    },
    {
      name: '王五',
      age: '22',
      address: '广州',
    },
  ]

  // 渲染函数
  function render(data) {
    const $container = $('#container')

    // 清空容器,重要!!!
    $container.html('')

    // 拼接 table
    const $table = $('<table>')

    $table.append($('<tr><td>name</td><td>age</td><td>address</td>/tr>'))
    data.forEach((item) => {
      $table.append(
        $(
          '<tr><td>' +
            item.name +
            '</td><td>' +
            item.age +
            '</td><td>' +
            item.address +
            '</td>/tr>',
        ),
      )
    })

    // 渲染到页面
    $container.append($table)
  }

  $('#btn-change').click(() => {
    data[1].age = 30
    data[2].address = '深圳'
    // re-render  再次渲染
    render(data)
  })

  // 页面加载完立刻执行(初次渲染)
  render(data)
</script>

<!-- table-with-vdom -->

<div id="container"></div>
<button id="btn-change">change</button>

<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-class.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-props.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-style.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-eventlisteners.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/h.js"></script>
<script type="text/javascript">
  const snabbdom = window.snabbdom
  // 定义关键函数 patch
  const patch = snabbdom.init([
    snabbdom_class,
    snabbdom_props,
    snabbdom_style,
    snabbdom_eventlisteners,
  ])

  // 定义关键函数 h
  const h = snabbdom.h

  // 原始数据
  const data = [
    {
      name: '张三',
      age: '20',
      address: '北京',
    },
    {
      name: '李四',
      age: '21',
      address: '上海',
    },
    {
      name: '王五',
      age: '22',
      address: '广州',
    },
  ]
  // 把表头也放在 data 中
  data.unshift({
    name: '姓名',
    age: '年龄',
    address: '地址',
  })

  const container = document.getElementById('container')

  // 渲染函数
  let vnode
  function render(data) {
    const newVnode = h(
      'table',
      {},
      data.map((item) => {
        const tds = []
        for (let i in item) {
          if (item.hasOwnProperty(i)) {
            tds.push(h('td', {}, item[i] + ''))
          }
        }
        return h('tr', {}, tds)
      }),
    )

    if (vnode) {
      // re-render
      patch(vnode, newVnode)
    } else {
      // 初次渲染
      patch(container, newVnode)
    }

    // 存储当前的 vnode 结果
    vnode = newVnode
  }

  // 初次渲染
  render(data)

  const btnChange = document.getElementById('btn-change')
  btnChange.addEventListener('click', () => {
    data[1].age = 30
    data[2].address = '深圳'
    // re-render
    render(data)
  })
</script>
<!-- snabbdom-demo -->

<div id="container"></div>
<button id="btn-change">change</button>

<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-class.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-props.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-style.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-eventlisteners.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/h.js"></script>
<script>
  const snabbdom = window.snabbdom

  // 定义 patch
  const patch = snabbdom.init([
    snabbdom_class,
    snabbdom_props,
    snabbdom_style,
    snabbdom_eventlisteners,
  ])

  // 定义 h
  const h = snabbdom.h

  const container = document.getElementById('container')

  // 生成 vnode
  const vnode = h('ul#list', {}, [h('li.item', {}, 'Item 1'), h('li.item', {}, 'Item 2')])
  patch(container, vnode)

  document.getElementById('btn-change').addEventListener('click', () => {
    // 生成 newVnode
    const newVnode = h('ul#list', {}, [
      h('li.item', {}, 'Item 1'),
      h('li.item', {}, 'Item B'),
      h('li.item', {}, 'Item 3'),
    ])
    patch(vnode, newVnode)

    // vnode = newVnode // patch 之后,应该用新的覆盖现有的 vnode ,否则每次 change 都是新旧对比
  })
</script>

<!-- table-without-vdom.html -->
<div id="container"></div>
<button id="btn-change">change</button>

<script type="text/javascript" src="https://cdn.bootcss.com/jquery/3.2.0/jquery.js"></script>
<script type="text/javascript">
  const data = [
    {
      name: '张三',
      age: '20',
      address: '北京',
    },
    {
      name: '李四',
      age: '21',
      address: '上海',
    },
    {
      name: '王五',
      age: '22',
      address: '广州',
    },
  ]

  // 渲染函数
  function render(data) {
    const $container = $('#container')

    // 清空容器,重要!!!
    $container.html('')

    // 拼接 table
    const $table = $('<table>')

    $table.append($('<tr><td>name</td><td>age</td><td>address</td>/tr>'))
    data.forEach((item) => {
      $table.append(
        $(
          '<tr><td>' +
            item.name +
            '</td><td>' +
            item.age +
            '</td><td>' +
            item.address +
            '</td>/tr>',
        ),
      )
    })

    // 渲染到页面
    $container.append($table)
  }

  $('#btn-change').click(() => {
    data[1].age = 30
    data[2].address = '深圳'
    // re-render  再次渲染
    render(data)
  })

  // 页面加载完立刻执行(初次渲染)
  render(data)
</script>

<!-- table-with-vdom -->

<div id="container"></div>
<button id="btn-change">change</button>

<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-class.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-props.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-style.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-eventlisteners.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/h.js"></script>
<script type="text/javascript">
  const snabbdom = window.snabbdom
  // 定义关键函数 patch
  const patch = snabbdom.init([
    snabbdom_class,
    snabbdom_props,
    snabbdom_style,
    snabbdom_eventlisteners,
  ])

  // 定义关键函数 h
  const h = snabbdom.h

  // 原始数据
  const data = [
    {
      name: '张三',
      age: '20',
      address: '北京',
    },
    {
      name: '李四',
      age: '21',
      address: '上海',
    },
    {
      name: '王五',
      age: '22',
      address: '广州',
    },
  ]
  // 把表头也放在 data 中
  data.unshift({
    name: '姓名',
    age: '年龄',
    address: '地址',
  })

  const container = document.getElementById('container')

  // 渲染函数
  let vnode
  function render(data) {
    const newVnode = h(
      'table',
      {},
      data.map((item) => {
        const tds = []
        for (let i in item) {
          if (item.hasOwnProperty(i)) {
            tds.push(h('td', {}, item[i] + ''))
          }
        }
        return h('tr', {}, tds)
      }),
    )

    if (vnode) {
      // re-render
      patch(vnode, newVnode)
    } else {
      // 初次渲染
      patch(container, newVnode)
    }

    // 存储当前的 vnode 结果
    vnode = newVnode
  }

  // 初次渲染
  render(data)

  const btnChange = document.getElementById('btn-change')
  btnChange.addEventListener('click', () => {
    data[1].age = 30
    data[2].address = '深圳'
    // re-render
    render(data)
  })
</script>

2. diff 算法

  • diff算法是vdom中最核心、最关键的部分
  • diff算法能在日常使用vue react中提现出来(如key)

树的 diff 的时间复杂度 O(n^3)

  • 第一,遍历tree1
  • 第二,遍历tree2
  • 第三,排序
  • 1000个节点,要计算10亿次,算法不可用

优化时间复杂度到 O(n)

  • 只比较同一层级,不跨级比较
  • tag不想同,则直接删掉重建,不再深度比较
  • tag和key相同,则认为是相同节点,不再深度比较

diff 过程细节

  • 新旧节点都有children执行updateChildren

    diff 对比

    • 开始和开始对比--头头
    • 结束和结束对比--尾尾
    • 开始和结束对比--头尾
    • 结束和开始对比--尾头
    • 以上四个都未命中:拿新节点 key ,能否对应上 oldCh 中的某个节点的 key
  • 新children有,旧children无:清空旧text节点,新增新children节点

  • 旧children有,新children无:移除旧children

  • 否则旧text有,设置text为空

vdom 和 diff 算法总结

  • 细节不重要,updateChildren的过程也不重要,不要深究
  • vdom的核心概念很重要:h、vnode、patch、diff、key
  • vdom存在的价值更重要,数据驱动视图,控制dom操作
js
// snabbdom源码位于 src/snabbdom.ts
/* global module, document, Node */
import { Module } from './modules/module';
import vnode, { VNode } from './vnode';
import * as is from './is';
import htmlDomApi, { DOMAPI } from './htmldomapi';

type NonUndefined<T> = T extends undefined ? never : T;

function isUndef (s: any): boolean { return s === undefined; }
function isDef<A> (s: A): s is NonUndefined<A> { return s !== undefined; }

type VNodeQueue = VNode[];

const emptyNode = vnode('', {}, [], undefined, undefined);

function sameVnode (vnode1: VNode, vnode2: VNode): boolean {
  // key 和 sel 都相等
  // undefined === undefined // true
  return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel;
}

function isVnode (vnode: any): vnode is VNode {
  return vnode.sel !== undefined;
}

type KeyToIndexMap = {[key: string]: number};

type ArraysOf<T> = {
  [K in keyof T]: Array<T[K]>;
}

type ModuleHooks = ArraysOf<Module>;

function createKeyToOldIdx (children: VNode[], beginIdx: number, endIdx: number): KeyToIndexMap {
  const map: KeyToIndexMap = {};
  for (let i = beginIdx; i <= endIdx; ++i) {
    const key = children[i]?.key;
    if (key !== undefined) {
      map[key] = i;
    }
  }
  return map;
}

const hooks: Array<keyof Module> = ['create', 'update', 'remove', 'destroy', 'pre', 'post'];

export { h } from './h';
export { thunk } from './thunk';

export function init (modules: Array<Partial<Module>>, domApi?: DOMAPI) {
  let i: number, j: number, cbs = ({} as ModuleHooks);

  const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi;

  for (i = 0; i < hooks.length; ++i) {
    cbs[hooks[i]] = [];
    for (j = 0; j < modules.length; ++j) {
      const hook = modules[j][hooks[i]];
      if (hook !== undefined) {
        (cbs[hooks[i]] as any[]).push(hook);
      }
    }
  }

  function emptyNodeAt (elm: Element) {
    const id = elm.id ? '#' + elm.id : '';
    const c = elm.className ? '.' + elm.className.split(' ').join('.') : '';
    return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm);
  }

  function createRmCb (childElm: Node, listeners: number) {
    return function rmCb () {
      if (--listeners === 0) {
        const parent = api.parentNode(childElm);
        api.removeChild(parent, childElm);
      }
    };
  }

  function createElm (vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
    let i: any, data = vnode.data;
    if (data !== undefined) {
      const init = data.hook?.init;
      if (isDef(init)) {
        init(vnode);
        data = vnode.data;
      }
    }
    let children = vnode.children, sel = vnode.sel;
    if (sel === '!') {
      if (isUndef(vnode.text)) {
        vnode.text = '';
      }
      vnode.elm = api.createComment(vnode.text!);
    } else if (sel !== undefined) {
      // Parse selector
      const hashIdx = sel.indexOf('#');
      const dotIdx = sel.indexOf('.', hashIdx);
      const hash = hashIdx > 0 ? hashIdx : sel.length;
      const dot = dotIdx > 0 ? dotIdx : sel.length;
      const tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel;
      const elm = vnode.elm = isDef(data) && isDef(i = data.ns)
        ? api.createElementNS(i, tag)
        : api.createElement(tag);
      if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot));
      if (dotIdx > 0) elm.setAttribute('class', sel.slice(dot + 1).replace(/\./g, ' '));
      for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode);
      if (is.array(children)) {
        for (i = 0; i < children.length; ++i) {
          const ch = children[i];
          if (ch != null) {
            api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue));
          }
        }
      } else if (is.primitive(vnode.text)) {
        api.appendChild(elm, api.createTextNode(vnode.text));
      }
      const hook = vnode.data!.hook;
      if (isDef(hook)) {
        hook.create?.(emptyNode, vnode);
        if (hook.insert) {
          insertedVnodeQueue.push(vnode);
        }
      }
    } else {
      vnode.elm = api.createTextNode(vnode.text!);
    }
    return vnode.elm;
  }

  function addVnodes (
    parentElm: Node,
    before: Node | null,
    vnodes: VNode[],
    startIdx: number,
    endIdx: number,
    insertedVnodeQueue: VNodeQueue
  ) {
    for (; startIdx <= endIdx; ++startIdx) {
      const ch = vnodes[startIdx];
      if (ch != null) {
        api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before);
      }
    }
  }

  function invokeDestroyHook (vnode: VNode) {
    const data = vnode.data;
    if (data !== undefined) {
      data?.hook?.destroy?.(vnode);
      for (let i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode);
      if (vnode.children !== undefined) {
        for (let j = 0; j < vnode.children.length; ++j) {
          const child = vnode.children[j];
          if (child != null && typeof child !== "string") {
            invokeDestroyHook(child);
          }
        }
      }
    }
  }

  function removeVnodes (parentElm: Node,
    vnodes: VNode[],
    startIdx: number,
    endIdx: number): void {
    for (; startIdx <= endIdx; ++startIdx) {
      let listeners: number, rm: () => void, ch = vnodes[startIdx];
      if (ch != null) {
        if (isDef(ch.sel)) {
          invokeDestroyHook(ch); // hook 操作

          // 移除 DOM 元素
          listeners = cbs.remove.length + 1;
          rm = createRmCb(ch.elm!, listeners);
          for (let i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm);
          const removeHook = ch?.data?.hook?.remove;
          if (isDef(removeHook)) {
            removeHook(ch, rm);
          } else {
            rm();
          }
        } else { // Text node
          api.removeChild(parentElm, ch.elm!);
        }
      }
    }
  }

  // diff算法核心
  function updateChildren (parentElm: Node,
    oldCh: VNode[],
    newCh: VNode[],
    insertedVnodeQueue: VNodeQueue) {
    let oldStartIdx = 0, newStartIdx = 0;
    let oldEndIdx = oldCh.length - 1;
    let oldStartVnode = oldCh[0];
    let oldEndVnode = oldCh[oldEndIdx];
    let newEndIdx = newCh.length - 1;
    let newStartVnode = newCh[0];
    let newEndVnode = newCh[newEndIdx];
    let oldKeyToIdx: KeyToIndexMap | undefined;
    let idxInOld: number;
    let elmToMove: VNode;
    let before: any;

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (oldStartVnode == null) {
        oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left
      } else if (oldEndVnode == null) {
        oldEndVnode = oldCh[--oldEndIdx];
      } else if (newStartVnode == null) {
        newStartVnode = newCh[++newStartIdx];
      } else if (newEndVnode == null) {
        newEndVnode = newCh[--newEndIdx];

      // 开始和开始对比--头头
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
        oldStartVnode = oldCh[++oldStartIdx];
        newStartVnode = newCh[++newStartIdx];

      // 结束和结束对比--尾尾
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
        oldEndVnode = oldCh[--oldEndIdx];
        newEndVnode = newCh[--newEndIdx];

      // 开始和结束对比--头尾
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
        api.insertBefore(parentElm, oldStartVnode.elm!, api.nextSibling(oldEndVnode.elm!));
        oldStartVnode = oldCh[++oldStartIdx];
        newEndVnode = newCh[--newEndIdx];

      // 结束和开始对比--尾头
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
        api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!);
        oldEndVnode = oldCh[--oldEndIdx];
        newStartVnode = newCh[++newStartIdx];

      // 以上四个都未命中
      } else {
        if (oldKeyToIdx === undefined) {
          oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
        }
        // 拿新节点 key ,能否对应上 oldCh 中的某个节点的 key
        idxInOld = oldKeyToIdx[newStartVnode.key as string];

        // 没对应上
        if (isUndef(idxInOld)) { // New element
          api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!);
          newStartVnode = newCh[++newStartIdx];

        // 对应上了
        } else {
          // 对应上 key 的节点
          elmToMove = oldCh[idxInOld];

          // sel 是否相等(sameVnode 的条件)
          if (elmToMove.sel !== newStartVnode.sel) {
            // New element
            api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!);

          // sel 相等,key 相等
          } else {
            patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
            oldCh[idxInOld] = undefined as any;
            api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!);
          }
          newStartVnode = newCh[++newStartIdx];
        }
      }
    }
    if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
      if (oldStartIdx > oldEndIdx) {
        before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
        addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
      } else {
        removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
      }
    }
  }

  function patchVnode (oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
    // 执行 prepatch hook
    const hook = vnode.data?.hook;
    hook?.prepatch?.(oldVnode, vnode);

    // 设置 vnode.elem
    const elm = vnode.elm = oldVnode.elm!;

    // 旧 children
    let oldCh = oldVnode.children as VNode[];
    // 新 children
    let ch = vnode.children as VNode[];

    if (oldVnode === vnode) return;

    // hook 相关
    if (vnode.data !== undefined) {
      for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);
      vnode.data.hook?.update?.(oldVnode, vnode);
    }

    // vnode.text === undefined (vnode.children 一般有值)
    if (isUndef(vnode.text)) {
      // 新旧都有 children
      if (isDef(oldCh) && isDef(ch)) {
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue);
      // 新 children 有,旧 children 无 (旧 text 有)
      } else if (isDef(ch)) {
        // 清空 text
        if (isDef(oldVnode.text)) api.setTextContent(elm, '');
        // 添加 children
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
      // 旧 child 有,新 child 无
      } else if (isDef(oldCh)) {
        // 移除 children
        removeVnodes(elm, oldCh, 0, oldCh.length - 1);
      // 旧 text 有
      } else if (isDef(oldVnode.text)) {
        api.setTextContent(elm, '');
      }

    // else : vnode.text !== undefined (vnode.children 无值)
    } else if (oldVnode.text !== vnode.text) {
      // 移除旧 children
      if (isDef(oldCh)) {
        removeVnodes(elm, oldCh, 0, oldCh.length - 1);
      }
      // 设置新 text
      api.setTextContent(elm, vnode.text!);
    }
    hook?.postpatch?.(oldVnode, vnode);
  }

  return function patch (oldVnode: VNode | Element, vnode: VNode): VNode {
    let i: number, elm: Node, parent: Node;
    const insertedVnodeQueue: VNodeQueue = [];
    // 执行 pre hook
    for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();

    // 第一个参数不是 vnode
    if (!isVnode(oldVnode)) {
      // 创建一个空的 vnode ,关联到这个 DOM 元素
      oldVnode = emptyNodeAt(oldVnode);
    }

    // 相同的 vnode(key 和 sel 都相等)
    if (sameVnode(oldVnode, vnode)) {
      // vnode 对比
      patchVnode(oldVnode, vnode, insertedVnodeQueue);

    // 不同的 vnode ,直接删掉重建
    } else {
      elm = oldVnode.elm!;
      parent = api.parentNode(elm);

      // 重建
      createElm(vnode, insertedVnodeQueue);

      if (parent !== null) {
        api.insertBefore(parent, vnode.elm!, api.nextSibling(elm));
        removeVnodes(parent, [oldVnode], 0, 0);
      }
    }

    for (i = 0; i < insertedVnodeQueue.length; ++i) {
      insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i]);
    }
    for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
    return vnode;
  };
}
// snabbdom源码位于 src/snabbdom.ts
/* global module, document, Node */
import { Module } from './modules/module';
import vnode, { VNode } from './vnode';
import * as is from './is';
import htmlDomApi, { DOMAPI } from './htmldomapi';

type NonUndefined<T> = T extends undefined ? never : T;

function isUndef (s: any): boolean { return s === undefined; }
function isDef<A> (s: A): s is NonUndefined<A> { return s !== undefined; }

type VNodeQueue = VNode[];

const emptyNode = vnode('', {}, [], undefined, undefined);

function sameVnode (vnode1: VNode, vnode2: VNode): boolean {
  // key 和 sel 都相等
  // undefined === undefined // true
  return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel;
}

function isVnode (vnode: any): vnode is VNode {
  return vnode.sel !== undefined;
}

type KeyToIndexMap = {[key: string]: number};

type ArraysOf<T> = {
  [K in keyof T]: Array<T[K]>;
}

type ModuleHooks = ArraysOf<Module>;

function createKeyToOldIdx (children: VNode[], beginIdx: number, endIdx: number): KeyToIndexMap {
  const map: KeyToIndexMap = {};
  for (let i = beginIdx; i <= endIdx; ++i) {
    const key = children[i]?.key;
    if (key !== undefined) {
      map[key] = i;
    }
  }
  return map;
}

const hooks: Array<keyof Module> = ['create', 'update', 'remove', 'destroy', 'pre', 'post'];

export { h } from './h';
export { thunk } from './thunk';

export function init (modules: Array<Partial<Module>>, domApi?: DOMAPI) {
  let i: number, j: number, cbs = ({} as ModuleHooks);

  const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi;

  for (i = 0; i < hooks.length; ++i) {
    cbs[hooks[i]] = [];
    for (j = 0; j < modules.length; ++j) {
      const hook = modules[j][hooks[i]];
      if (hook !== undefined) {
        (cbs[hooks[i]] as any[]).push(hook);
      }
    }
  }

  function emptyNodeAt (elm: Element) {
    const id = elm.id ? '#' + elm.id : '';
    const c = elm.className ? '.' + elm.className.split(' ').join('.') : '';
    return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm);
  }

  function createRmCb (childElm: Node, listeners: number) {
    return function rmCb () {
      if (--listeners === 0) {
        const parent = api.parentNode(childElm);
        api.removeChild(parent, childElm);
      }
    };
  }

  function createElm (vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
    let i: any, data = vnode.data;
    if (data !== undefined) {
      const init = data.hook?.init;
      if (isDef(init)) {
        init(vnode);
        data = vnode.data;
      }
    }
    let children = vnode.children, sel = vnode.sel;
    if (sel === '!') {
      if (isUndef(vnode.text)) {
        vnode.text = '';
      }
      vnode.elm = api.createComment(vnode.text!);
    } else if (sel !== undefined) {
      // Parse selector
      const hashIdx = sel.indexOf('#');
      const dotIdx = sel.indexOf('.', hashIdx);
      const hash = hashIdx > 0 ? hashIdx : sel.length;
      const dot = dotIdx > 0 ? dotIdx : sel.length;
      const tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel;
      const elm = vnode.elm = isDef(data) && isDef(i = data.ns)
        ? api.createElementNS(i, tag)
        : api.createElement(tag);
      if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot));
      if (dotIdx > 0) elm.setAttribute('class', sel.slice(dot + 1).replace(/\./g, ' '));
      for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode);
      if (is.array(children)) {
        for (i = 0; i < children.length; ++i) {
          const ch = children[i];
          if (ch != null) {
            api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue));
          }
        }
      } else if (is.primitive(vnode.text)) {
        api.appendChild(elm, api.createTextNode(vnode.text));
      }
      const hook = vnode.data!.hook;
      if (isDef(hook)) {
        hook.create?.(emptyNode, vnode);
        if (hook.insert) {
          insertedVnodeQueue.push(vnode);
        }
      }
    } else {
      vnode.elm = api.createTextNode(vnode.text!);
    }
    return vnode.elm;
  }

  function addVnodes (
    parentElm: Node,
    before: Node | null,
    vnodes: VNode[],
    startIdx: number,
    endIdx: number,
    insertedVnodeQueue: VNodeQueue
  ) {
    for (; startIdx <= endIdx; ++startIdx) {
      const ch = vnodes[startIdx];
      if (ch != null) {
        api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before);
      }
    }
  }

  function invokeDestroyHook (vnode: VNode) {
    const data = vnode.data;
    if (data !== undefined) {
      data?.hook?.destroy?.(vnode);
      for (let i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode);
      if (vnode.children !== undefined) {
        for (let j = 0; j < vnode.children.length; ++j) {
          const child = vnode.children[j];
          if (child != null && typeof child !== "string") {
            invokeDestroyHook(child);
          }
        }
      }
    }
  }

  function removeVnodes (parentElm: Node,
    vnodes: VNode[],
    startIdx: number,
    endIdx: number): void {
    for (; startIdx <= endIdx; ++startIdx) {
      let listeners: number, rm: () => void, ch = vnodes[startIdx];
      if (ch != null) {
        if (isDef(ch.sel)) {
          invokeDestroyHook(ch); // hook 操作

          // 移除 DOM 元素
          listeners = cbs.remove.length + 1;
          rm = createRmCb(ch.elm!, listeners);
          for (let i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm);
          const removeHook = ch?.data?.hook?.remove;
          if (isDef(removeHook)) {
            removeHook(ch, rm);
          } else {
            rm();
          }
        } else { // Text node
          api.removeChild(parentElm, ch.elm!);
        }
      }
    }
  }

  // diff算法核心
  function updateChildren (parentElm: Node,
    oldCh: VNode[],
    newCh: VNode[],
    insertedVnodeQueue: VNodeQueue) {
    let oldStartIdx = 0, newStartIdx = 0;
    let oldEndIdx = oldCh.length - 1;
    let oldStartVnode = oldCh[0];
    let oldEndVnode = oldCh[oldEndIdx];
    let newEndIdx = newCh.length - 1;
    let newStartVnode = newCh[0];
    let newEndVnode = newCh[newEndIdx];
    let oldKeyToIdx: KeyToIndexMap | undefined;
    let idxInOld: number;
    let elmToMove: VNode;
    let before: any;

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (oldStartVnode == null) {
        oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left
      } else if (oldEndVnode == null) {
        oldEndVnode = oldCh[--oldEndIdx];
      } else if (newStartVnode == null) {
        newStartVnode = newCh[++newStartIdx];
      } else if (newEndVnode == null) {
        newEndVnode = newCh[--newEndIdx];

      // 开始和开始对比--头头
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
        oldStartVnode = oldCh[++oldStartIdx];
        newStartVnode = newCh[++newStartIdx];

      // 结束和结束对比--尾尾
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
        oldEndVnode = oldCh[--oldEndIdx];
        newEndVnode = newCh[--newEndIdx];

      // 开始和结束对比--头尾
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
        api.insertBefore(parentElm, oldStartVnode.elm!, api.nextSibling(oldEndVnode.elm!));
        oldStartVnode = oldCh[++oldStartIdx];
        newEndVnode = newCh[--newEndIdx];

      // 结束和开始对比--尾头
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
        api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!);
        oldEndVnode = oldCh[--oldEndIdx];
        newStartVnode = newCh[++newStartIdx];

      // 以上四个都未命中
      } else {
        if (oldKeyToIdx === undefined) {
          oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
        }
        // 拿新节点 key ,能否对应上 oldCh 中的某个节点的 key
        idxInOld = oldKeyToIdx[newStartVnode.key as string];

        // 没对应上
        if (isUndef(idxInOld)) { // New element
          api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!);
          newStartVnode = newCh[++newStartIdx];

        // 对应上了
        } else {
          // 对应上 key 的节点
          elmToMove = oldCh[idxInOld];

          // sel 是否相等(sameVnode 的条件)
          if (elmToMove.sel !== newStartVnode.sel) {
            // New element
            api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!);

          // sel 相等,key 相等
          } else {
            patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
            oldCh[idxInOld] = undefined as any;
            api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!);
          }
          newStartVnode = newCh[++newStartIdx];
        }
      }
    }
    if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
      if (oldStartIdx > oldEndIdx) {
        before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
        addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
      } else {
        removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
      }
    }
  }

  function patchVnode (oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
    // 执行 prepatch hook
    const hook = vnode.data?.hook;
    hook?.prepatch?.(oldVnode, vnode);

    // 设置 vnode.elem
    const elm = vnode.elm = oldVnode.elm!;

    // 旧 children
    let oldCh = oldVnode.children as VNode[];
    // 新 children
    let ch = vnode.children as VNode[];

    if (oldVnode === vnode) return;

    // hook 相关
    if (vnode.data !== undefined) {
      for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);
      vnode.data.hook?.update?.(oldVnode, vnode);
    }

    // vnode.text === undefined (vnode.children 一般有值)
    if (isUndef(vnode.text)) {
      // 新旧都有 children
      if (isDef(oldCh) && isDef(ch)) {
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue);
      // 新 children 有,旧 children 无 (旧 text 有)
      } else if (isDef(ch)) {
        // 清空 text
        if (isDef(oldVnode.text)) api.setTextContent(elm, '');
        // 添加 children
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
      // 旧 child 有,新 child 无
      } else if (isDef(oldCh)) {
        // 移除 children
        removeVnodes(elm, oldCh, 0, oldCh.length - 1);
      // 旧 text 有
      } else if (isDef(oldVnode.text)) {
        api.setTextContent(elm, '');
      }

    // else : vnode.text !== undefined (vnode.children 无值)
    } else if (oldVnode.text !== vnode.text) {
      // 移除旧 children
      if (isDef(oldCh)) {
        removeVnodes(elm, oldCh, 0, oldCh.length - 1);
      }
      // 设置新 text
      api.setTextContent(elm, vnode.text!);
    }
    hook?.postpatch?.(oldVnode, vnode);
  }

  return function patch (oldVnode: VNode | Element, vnode: VNode): VNode {
    let i: number, elm: Node, parent: Node;
    const insertedVnodeQueue: VNodeQueue = [];
    // 执行 pre hook
    for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();

    // 第一个参数不是 vnode
    if (!isVnode(oldVnode)) {
      // 创建一个空的 vnode ,关联到这个 DOM 元素
      oldVnode = emptyNodeAt(oldVnode);
    }

    // 相同的 vnode(key 和 sel 都相等)
    if (sameVnode(oldVnode, vnode)) {
      // vnode 对比
      patchVnode(oldVnode, vnode, insertedVnodeQueue);

    // 不同的 vnode ,直接删掉重建
    } else {
      elm = oldVnode.elm!;
      parent = api.parentNode(elm);

      // 重建
      createElm(vnode, insertedVnodeQueue);

      if (parent !== null) {
        api.insertBefore(parent, vnode.elm!, api.nextSibling(elm));
        removeVnodes(parent, [oldVnode], 0, 0);
      }
    }

    for (i = 0; i < insertedVnodeQueue.length; ++i) {
      insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i]);
    }
    for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
    return vnode;
  };
}

模板编译 ​

前置知识

  • 模板是vue开发中最常用的,即与使用相关联的原理
  • 它不是HTML,有指令、插值、JS 表达式,能实现循环、判断,因此模板一定转为JS代码,即模板编译
  • 面试不会直接问,但会通过组件渲染和更新过程考察

模板编译

  • vue template compiler将模板编译为render函数
  • 执行render函数,生成vnode
  • 基于vnode在执行patch和diff
  • 使用webpack vue loader,会在开发环境下编译模板

with 语法

  • 改变{}内自由变量的查找规则,当做obj属性来查找
  • 如果找不到匹配的obj属性,就会报错
  • with要慎用,它打破了作用域规则,易读性变差

vue 组件中使用 render 代替 template

js
// 执行 node index.js

const compiler = require('vue-template-compiler')

// 插值
const template = `<p>{{message}}</p>`
with (this) {
  return _c('p', [_v(_s(message))])
}
// this就是vm的实例, message等变量会从vm上读取,触发getter
// _c => createElement 也就是h函数 => 返回vnode
// _v => createTextVNode
// _s => toString
// 也就是这样 with(this){return createElement('p',[createTextVNode(toString(message))])}

// h -> vnode
// createElement -> vnode

// 表达式
const template = `<p>{{flag ? message : 'no message found'}}</p>`
// with(this){return _c('p',[_v(_s(flag ? message : 'no message found'))])}

// 属性和动态属性
const template = `
    <div id="div1" class="container">
        <img :src="imgUrl"/>
    </div>
`
with (this) {
  return _c('div', { staticClass: 'container', attrs: { id: 'div1' } }, [
    _c('img', { attrs: { src: imgUrl } }),
  ])
}

// 条件
const template = `
    <div>
        <p v-if="flag === 'a'">A</p>
        <p v-else>B</p>
    </div>
`
with (this) {
  return _c('div', [flag === 'a' ? _c('p', [_v('A')]) : _c('p', [_v('B')])])
}

// 循环
const template = `
    <ul>
        <li v-for="item in list" :key="item.id">{{item.title}}</li>
    </ul>
`
with (this) {
  return _c(
    'ul',
    _l(list, function (item) {
      return _c('li', { key: item.id }, [_v(_s(item.title))])
    }),
    0,
  )
}

// 事件
const template = `
    <button @click="clickHandler">submit</button>
`
with (this) {
  return _c('button', { on: { click: clickHandler } }, [_v('submit')])
}

// v-model
const template = `<input type="text" v-model="name">`
// 主要看 input 事件
with (this) {
  return _c('input', {
    directives: [{ name: 'model', rawName: 'v-model', value: name, expression: 'name' }],
    attrs: { type: 'text' },
    domProps: { value: name },
    on: {
      input: function ($event) {
        if ($event.target.composing) return
        name = $event.target.value
      },
    },
  })
}

// render 函数
// 返回 vnode
// patch

// 编译
const res = compiler.compile(template)
console.log(res.render)

// ---------------分割线--------------

// 从 vue 源码中找到缩写函数的含义
function installRenderHelpers(target) {
  target._o = markOnce
  target._n = toNumber
  target._s = toString
  target._l = renderList
  target._t = renderSlot
  target._q = looseEqual
  target._i = looseIndexOf
  target._m = renderStatic
  target._f = resolveFilter
  target._k = checkKeyCodes
  target._b = bindObjectProps
  target._v = createTextVNode
  target._e = createEmptyVNode
  target._u = resolveScopedSlots
  target._g = bindObjectListeners
  target._d = bindDynamicKeys
  target._p = prependModifier
}
// 执行 node index.js

const compiler = require('vue-template-compiler')

// 插值
const template = `<p>{{message}}</p>`
with (this) {
  return _c('p', [_v(_s(message))])
}
// this就是vm的实例, message等变量会从vm上读取,触发getter
// _c => createElement 也就是h函数 => 返回vnode
// _v => createTextVNode
// _s => toString
// 也就是这样 with(this){return createElement('p',[createTextVNode(toString(message))])}

// h -> vnode
// createElement -> vnode

// 表达式
const template = `<p>{{flag ? message : 'no message found'}}</p>`
// with(this){return _c('p',[_v(_s(flag ? message : 'no message found'))])}

// 属性和动态属性
const template = `
    <div id="div1" class="container">
        <img :src="imgUrl"/>
    </div>
`
with (this) {
  return _c('div', { staticClass: 'container', attrs: { id: 'div1' } }, [
    _c('img', { attrs: { src: imgUrl } }),
  ])
}

// 条件
const template = `
    <div>
        <p v-if="flag === 'a'">A</p>
        <p v-else>B</p>
    </div>
`
with (this) {
  return _c('div', [flag === 'a' ? _c('p', [_v('A')]) : _c('p', [_v('B')])])
}

// 循环
const template = `
    <ul>
        <li v-for="item in list" :key="item.id">{{item.title}}</li>
    </ul>
`
with (this) {
  return _c(
    'ul',
    _l(list, function (item) {
      return _c('li', { key: item.id }, [_v(_s(item.title))])
    }),
    0,
  )
}

// 事件
const template = `
    <button @click="clickHandler">submit</button>
`
with (this) {
  return _c('button', { on: { click: clickHandler } }, [_v('submit')])
}

// v-model
const template = `<input type="text" v-model="name">`
// 主要看 input 事件
with (this) {
  return _c('input', {
    directives: [{ name: 'model', rawName: 'v-model', value: name, expression: 'name' }],
    attrs: { type: 'text' },
    domProps: { value: name },
    on: {
      input: function ($event) {
        if ($event.target.composing) return
        name = $event.target.value
      },
    },
  })
}

// render 函数
// 返回 vnode
// patch

// 编译
const res = compiler.compile(template)
console.log(res.render)

// ---------------分割线--------------

// 从 vue 源码中找到缩写函数的含义
function installRenderHelpers(target) {
  target._o = markOnce
  target._n = toNumber
  target._s = toString
  target._l = renderList
  target._t = renderSlot
  target._q = looseEqual
  target._i = looseIndexOf
  target._m = renderStatic
  target._f = resolveFilter
  target._k = checkKeyCodes
  target._b = bindObjectProps
  target._v = createTextVNode
  target._e = createEmptyVNode
  target._u = resolveScopedSlots
  target._g = bindObjectListeners
  target._d = bindDynamicKeys
  target._p = prependModifier
}

组件渲染更新过程(重点掌握) ​

前言

  • 一个组件渲染到页面,修改data触发更新(数据驱动视图)
  • 其背后原理是什么,需要掌握哪些点
  • 考察对流程了解的全面程度

回顾三大核心知识点

  • 响应式:监听data属性getter、setter(包括数组)

  • 模板编译:模板到render函数,再到vnode

  • vdom

    :两种用法

    • patch(elem,vnode) 首次渲染vnode到container上
    • patch(vnode、newVnode) 新的vnode取更新旧的vnode
  • 搞定这三点核心原理,vue原理不是问题

组件渲染更新过程

  • \1. 初次渲染过程

    • 解析模板为render函数(或在开发环境已经完成vue-loader)

    • 触发响应式,监听data属性getter、setter

    • 执行

      render
      render

      函数(执行

      render
      render

      函数过程中,会获取

      data
      data

      的属性触发

      getter
      getter

      ),生成

      vnode
      vnode

      ,在执行

      patch(elem,vnode)
      patch(elem,vnode)
      elem
      elem

      组件对应的

      dom
      dom

      节点

      • const template = <p></p>
      • 编译为render函数 with(this){return _c('p', [_v(_s(message))])}
      • this就是vm的实例, message等变量会从vm上读取,触发getter进行依赖收集
      js
      export default {
        data() {
          return {
            message: 'hello', // render函数执行过程中会获取message变量值,触发getter
          }
        },
      }
      export default {
        data() {
          return {
            message: 'hello', // render函数执行过程中会获取message变量值,触发getter
          }
        },
      }
  • \2. 更新过程

    • 修改data,触发setter(此前在getter中已被监听)
    • 重新执行render函数,生成newVnode
    • 在调用patch(oldVnode, newVnode)算出最小差异,进行更新
  • 3. 完成流程图 img

异步渲染

  • 汇总data的修改,一次更新视图
  • 减少DOM操作次数,提高性能

img

js
methods: {
    addItem() {
        this.list.push(`${Date.now()}`)
        this.list.push(`${Date.now()}`)
        this.list.push(`${Date.now()}`)

        // 1.页面渲染是异步的,$nextTick待渲染完在回调
        // 2.页面渲染时会将data的修改做整合,多次data修改也只会渲染一次
        this.$nextTick(()=>{
            const ulElem = this.$refs.ul
            console.log(ulElem.childNotes.length)
        })
    }
}
methods: {
    addItem() {
        this.list.push(`${Date.now()}`)
        this.list.push(`${Date.now()}`)
        this.list.push(`${Date.now()}`)

        // 1.页面渲染是异步的,$nextTick待渲染完在回调
        // 2.页面渲染时会将data的修改做整合,多次data修改也只会渲染一次
        this.$nextTick(()=>{
            const ulElem = this.$refs.ul
            console.log(ulElem.childNotes.length)
        })
    }
}

总结

  • 渲染和响应式的关系
  • 渲染和模板编译的关系
  • 渲染和vdom的关系

前端路由原理 ​

hash 的特点

  • hash变化会触发网页跳转,即浏览器的前进和后退
  • hash变化不会刷新页面,SPA必须的特点
  • hash永远不会提交到server端
  • 通过onhashchange监听

H5 History

  • 用url规范的路由,但跳转时不刷新页面

  • 通过history.pushState和history.onpopstate监听

  • H5 History
    H5 History

    需要后端支持

    • 当我们进入到子路由时刷新页面,web容器没有相对应的页面此时会出现404
    • 所以我们只需要配置将任意页面都重定向到 index.html,把路由交由前端处理
    • 对nginx配置文件.conf修改,添加try_files $uri $uri/ /index.html;
    nginx
    server {
      listen  80;
      server_name  www.xxx.com;
    
      location / {
        index  /data/dist/index.html;
        try_files $uri $uri/ /index.html;
      }
    }
    server {
      listen  80;
      server_name  www.xxx.com;
    
      location / {
        index  /data/dist/index.html;
        try_files $uri $uri/ /index.html;
      }
    }

两者选择

  • to B系统推荐使用 hash,简单易用,对url规范不敏感
  • to C系统,可以考虑使用H5 History,但需要服务端支持
  • 能选择简单的,就别用复杂的,要考虑成本和收益
js
// hash 变化,包括:
// a. JS 修改 url
// b. 手动修改 url 的 hash
// c. 浏览器前进、后退
window.onhashchange = (event) => {
  console.log('old url', event.oldURL)
  console.log('new url', event.newURL)

  console.log('hash:', location.hash)
}

// 页面初次加载,获取 hash
document.addEventListener('DOMContentLoaded', () => {
  console.log('hash:', location.hash)
})

// JS 修改 url
document.getElementById('btn1').addEventListener('click', () => {
  location.href = '#/user'
})

// history API

// 页面初次加载,获取 path
document.addEventListener('DOMContentLoaded', () => {
  console.log('load', location.pathname)
})

// 打开一个新的路由
// 【注意】用 pushState 方式,浏览器不会刷新页面
document.getElementById('btn1').addEventListener('click', () => {
  const state = { name: 'page1' }
  console.log('切换路由到', 'page1')
  history.pushState(state, '', 'page1') // 重要!!
})

// 监听浏览器前进、后退
window.onpopstate = (event) => {
  // 重要!!
  console.log('onpopstate', event.state, location.pathname)
}

// 需要 server 端配合,可参考
// https://router.vuejs.org/zh/guide/essentials/history-mode.html#%E5%90%8E%E7%AB%AF%E9%85%8D%E7%BD%AE%E4%BE%8B%E5%AD%90
// hash 变化,包括:
// a. JS 修改 url
// b. 手动修改 url 的 hash
// c. 浏览器前进、后退
window.onhashchange = (event) => {
  console.log('old url', event.oldURL)
  console.log('new url', event.newURL)

  console.log('hash:', location.hash)
}

// 页面初次加载,获取 hash
document.addEventListener('DOMContentLoaded', () => {
  console.log('hash:', location.hash)
})

// JS 修改 url
document.getElementById('btn1').addEventListener('click', () => {
  location.href = '#/user'
})

// history API

// 页面初次加载,获取 path
document.addEventListener('DOMContentLoaded', () => {
  console.log('load', location.pathname)
})

// 打开一个新的路由
// 【注意】用 pushState 方式,浏览器不会刷新页面
document.getElementById('btn1').addEventListener('click', () => {
  const state = { name: 'page1' }
  console.log('切换路由到', 'page1')
  history.pushState(state, '', 'page1') // 重要!!
})

// 监听浏览器前进、后退
window.onpopstate = (event) => {
  // 重要!!
  console.log('onpopstate', event.state, location.pathname)
}

// 需要 server 端配合,可参考
// https://router.vuejs.org/zh/guide/essentials/history-mode.html#%E5%90%8E%E7%AB%AF%E9%85%8D%E7%BD%AE%E4%BE%8B%E5%AD%90

Vue 面试考点答题分析 ​

请说一下响应式数据的理解 ​

参考答案

  • 核心点考察的是:数组和对象类型当值变化时如何劫持到。对象内部通过defineReactive方法,使用Object.defineProperty将属性进行劫持(只会劫持已经存在的属性),数组则是通过重写数组方法来实现。 这里在回答时可以带出一些相关知识点(比如多层对象是通过递归来实现劫持,顺带提出Vue3中是使用proxy来实现响应式数据)
  • 核心点答出来了也可以在进行补充回答,内部依赖收集是怎样做到的,每个属性都拥有自己的dep属性,存放他所依赖的watcher,当属性变化后会通知自己对应的watcher去更新
  • 这里可以引出性能优化相关的内容
    • 对象层级过深,性能就会差
    • 不需要响应数据的内容不要放到data中
    • Object.freeze() 可以冻结数据

Vue 如何检测数组变化 ​

  • 核心点考察的是:数组考虑性能原因没有用defineProperty对数组的每一项进行拦截,而是选择重写数组(push,shift,pop,splice,unshift,sort,reverse)方法进行重写
  • 核心点答出来了也可以在进行补充回答,在Vue中修改数组的索引和长度是无法监控到的。需要通过以上7种变异方法修改数组才会触发数组对应的watcher进行更新。数组中如果是对象数据类型也会进行递归劫持
  • 引发出的问题,那如果想更改索引更新数据怎么办?可以通过Vue.$set()来进行处理 => 核心内部用的是splice方法

Vue 中模板编译原理 ​

`

  • 核心点考察的是:如何将template转换成render函数(这里要注意的是我们在开发时尽量不要使用template,因为将template转化成render方法需要在运行时进行编译操作会有性能损耗,同时引用带有compiler包的vue体积也会变大。默认.vue文件中的template处理是通过vue-loader来进行处理的并不是通过运行时的编译 - 后面我们会说到默认vue项目中引入的vue.js是不带有compiler模块的)

  • 模板编译过程

    • 将template模板转换成ast语法树 - parserHTML
    • 对静态语法做静态标记 - markUp
    • 重新生成代码 - codeGen
    • 核心点答出来了也可以在进行补充回答 (模板引擎的实现原理就是new Function + with来进行实现的)
    js
    function compileToFunctions(template) {
      // 我们需要把html字符串变成render函数
      // 1.把html代码转成ast语法树  ast用来描述代码本身形成树结构 不仅可以描述html 也能描述css以及js语法
      // 很多库都运用到了ast 比如 webpack babel eslint等等
      let ast = parse(template)
      // 2.优化静态节点:对ast树进行标记,标记静态节点
      if (options.optimize !== false) {
        optimize(ast, options)
      }
    
      // 3.通过ast 重新生成代码
      // 我们最后生成的代码需要和render函数一样
      // 类似_c('div',{id:"app"},_c('div',undefined,_v("hello"+_s(name)),_c('span',undefined,_v("world"))))
      // _c代表创建元素 _v代表创建文本 _s代表文Json.stringify--把对象解析成文本
      let code = generate(ast)
      //   使用with语法改变作用域为this  之后调用render函数可以使用call改变this 方便code里面的变量取值
      let renderFn = new Function(`with(this){return ${code}}`)
      return renderFn
    }
    function compileToFunctions(template) {
      // 我们需要把html字符串变成render函数
      // 1.把html代码转成ast语法树  ast用来描述代码本身形成树结构 不仅可以描述html 也能描述css以及js语法
      // 很多库都运用到了ast 比如 webpack babel eslint等等
      let ast = parse(template)
      // 2.优化静态节点:对ast树进行标记,标记静态节点
      if (options.optimize !== false) {
        optimize(ast, options)
      }
    
      // 3.通过ast 重新生成代码
      // 我们最后生成的代码需要和render函数一样
      // 类似_c('div',{id:"app"},_c('div',undefined,_v("hello"+_s(name)),_c('span',undefined,_v("world"))))
      // _c代表创建元素 _v代表创建文本 _s代表文Json.stringify--把对象解析成文本
      let code = generate(ast)
      //   使用with语法改变作用域为this  之后调用render函数可以使用call改变this 方便code里面的变量取值
      let renderFn = new Function(`with(this){return ${code}}`)
      return renderFn
    }
    • vue-loader中处理template属性主要靠的是vue-template-compiler模块
    js
    const VueTemplateCompiler = require('vue-template-compiler');
    const {render} = VueTemplateCompiler.compile("<div id="hello">{{msg}}</div>");
    console.log(render.toString())
    const VueTemplateCompiler = require('vue-template-compiler');
    const {render} = VueTemplateCompiler.compile("<div id="hello">{{msg}}</div>");
    console.log(render.toString())

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

  • 核心点考察的是:Vue的生命周期钩子就是回调函数而已,当创建组件实例的过程中会调用对应的钩子方法
  • 核心点答出来了也可以在进行补充回答:内部主要是使用callHook方法来调用对应的方法。核心是一个发布订阅模式,将钩子订阅好(内部采用数组的方式存储),在对应的阶段进行发布
js
export function callHook(vm, hook) {
  // 依次执行生命周期对应的方法
  const handlers = vm.$options[hook]
  if (handlers) {
    for (let i = 0; i < handlers.length; i++) {
      handlers[i].call(vm) //生命周期里面的this指向当前实例
    }
  }
}

// 调用的时候
Vue.prototype._init = function (options) {
  const vm = this
  vm.$options = mergeOptions(vm.constructor.options, options)
  callHook(vm, 'beforeCreate') //初始化数据之前
  // 初始化状态
  initState(vm)
  callHook(vm, 'created') //初始化数据之后
  if (vm.$options.el) {
    vm.$mount(vm.$options.el)
  }
}

// 销毁实例实现
Vue.prototype.$destory = function () {
  // 触发钩子
  callHook(vm, 'beforeDestory')
  // 自身及子节点
  remove()
  // 删除依赖
  watcher.teardown()
  // 删除监听
  vm.$off()
  // 触发钩子
  callHook(vm, 'destoryed')
}
export function callHook(vm, hook) {
  // 依次执行生命周期对应的方法
  const handlers = vm.$options[hook]
  if (handlers) {
    for (let i = 0; i < handlers.length; i++) {
      handlers[i].call(vm) //生命周期里面的this指向当前实例
    }
  }
}

// 调用的时候
Vue.prototype._init = function (options) {
  const vm = this
  vm.$options = mergeOptions(vm.constructor.options, options)
  callHook(vm, 'beforeCreate') //初始化数据之前
  // 初始化状态
  initState(vm)
  callHook(vm, 'created') //初始化数据之后
  if (vm.$options.el) {
    vm.$mount(vm.$options.el)
  }
}

// 销毁实例实现
Vue.prototype.$destory = function () {
  // 触发钩子
  callHook(vm, 'beforeDestory')
  // 自身及子节点
  remove()
  // 删除依赖
  watcher.teardown()
  // 删除监听
  vm.$off()
  // 触发钩子
  callHook(vm, 'destoryed')
}

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

  • 核心点考察的是:Vue.mixin的作用就是抽离公共的业务逻辑,原理类似“对象的继承”,当组件初始化时会调用mergeOptions方法进行合并,采用策略模式针对不同的属性进行合并。如果混入的数据和本身组件中的数据冲突,会采用“就近原则”以组件的数据为准
  • 核心点答出来了也可以在进行补充回答:mixin中有很多缺陷 "命名冲突问题"、"依赖问题"、"数据来源问题",这里强调一下mixin的数据是不会被共享的

nextTick 在哪里使用?原理是 ​

  • 核心点考察的是:nextTick中的回调是在下次 DOM 更新循环结束之后执行的延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。原理就是异步方法(promise,mutationObserver,setImmediate,setTimeout)经常与事件环一起来问(宏任务和微任务)
  • 核心点答出来了也可以在进行补充回答:vue多次更新数据,最终会进行批处理更新。内部调用的就是nextTick实现了延迟更新,用户自定义的nextTick中的回调会被延迟到更新完成后调用,从而可以获取更新后的DOM

Vue 为什么需要虚拟 DOM ​

  • 核心点考察的是:Virtual DOM就是用js对象来描述真实DOM,是对真实DOM的抽象,由于直接操作DOM性能低但是js层的操作效率高,可以将DOM操作转化成对象操作,最终通过diff算法比对差异进行更新DOM(减少了对真实DOM的操作)。虚拟DOM不依赖真实平台环境从而也可以实现跨平台。
  • 核心点答出来了也可以在进行补充回答:虚拟DOM的实现就是普通对象包含tag、attrs、children等属性对真实节点的描述。(本质上就是在JS和DOM之间的一个缓存)

Vue 中的 diff 原理 ​

核心点考察的是:Vue的diff算法是平级比较,不考虑跨级比较的情况。内部采用深度递归的方式 + 双指针的方式进行比较

比较过程

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

Vue3中采用最长递增子序列实现diff算法

Vue 中 computed 和 watch 的区别 ​

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

Vue.set 方法是如何实现的 ​

核心点考察的是:为什么$set可以触发更新,我们给对象和数组本身都增加了dep属性。当给对象新增不存在的属性则触发对象依赖的watcher去更新,当修改数组索引时我们调用数组本身的splice方法去更新数组

js
export function set(target: Array | Object, key: any, val: any): any {
  // 1.是开发环境 target 没定义或者是基础类型则报错
  if (process.env.NODE_ENV !== 'production' && (isUndef(target) || isPrimitive(target))) {
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  // 2.如果是数组 Vue.set(array,1,100); 调用我们重写的splice方法 (这样可以更新视图)
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }
  // 3.如果是对象本身的属性,则直接添加即可
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  const ob = (target: any).__ob__
  // 4.如果是Vue实例 或 根数据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.如果不是响应式的也不需要将其定义成响应式属性
  if (!ob) {
    target[key] = val
    return val
  }
  // 6.将属性定义成响应式的
  defineReactive(ob.value, key, val)
  // 7.通知视图更新
  ob.dep.notify()
  return val
}
export function set(target: Array | Object, key: any, val: any): any {
  // 1.是开发环境 target 没定义或者是基础类型则报错
  if (process.env.NODE_ENV !== 'production' && (isUndef(target) || isPrimitive(target))) {
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  // 2.如果是数组 Vue.set(array,1,100); 调用我们重写的splice方法 (这样可以更新视图)
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }
  // 3.如果是对象本身的属性,则直接添加即可
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  const ob = (target: any).__ob__
  // 4.如果是Vue实例 或 根数据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.如果不是响应式的也不需要将其定义成响应式属性
  if (!ob) {
    target[key] = val
    return val
  }
  // 6.将属性定义成响应式的
  defineReactive(ob.value, key, val)
  // 7.通知视图更新
  ob.dep.notify()
  return val
}

Vue.use 是干什么的?原理是什么 ​

核心点考察的是:Vue.use是用来使用插件的,我们可以在插件中扩展全局组件、指令、原型方法等

js
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)  // install方法的第一个参数是Vue的构造函数,其他参数是Vue.use中除了第一个参数的其他参数
  if (typeof plugin.install === 'function') { // 调用插件的install方法
      plugin.install.apply(plugin, args)  Vue.install = function(Vue,args){}
  } else if (typeof plugin === 'function') { // 插件本身是一个函数,直接让函数执行
      plugin.apply(null, args)
  }
  installedPlugins.push(plugin) // 缓存插件
  return this
}
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)  // install方法的第一个参数是Vue的构造函数,其他参数是Vue.use中除了第一个参数的其他参数
  if (typeof plugin.install === 'function') { // 调用插件的install方法
      plugin.install.apply(plugin, args)  Vue.install = function(Vue,args){}
  } else if (typeof plugin === 'function') { // 插件本身是一个函数,直接让函数执行
      plugin.apply(null, args)
  }
  installedPlugins.push(plugin) // 缓存插件
  return this
}

vue-router 有几种钩子函数?具体是什么及执行流程是怎样的 ​

核心点考察的是:路由钩子的执行流程, 钩子函数种类有: 全局守卫、路由守卫、组件守卫

完整的导航解析流程

  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 的回调函数,创建好的组件实例会作为回调函数的参数传入