前端vue知识点梳理

前端vue知识点梳理

Vue的优点

  1. 渐进式框架:可以在任何项目中轻易的引入;
  2. 轻量级框架:只关注视图层,是一个构建数据的视图集合,大小只有几十 kb
  3. 简单易学:国人开发,中文文档,不存在语言障碍 ,易于理解和学习;
  4. 双向数据绑定:在数据操作方面更为简单;
  5. 组件化:很大程度上实现了逻辑的封装和重用,在构建单页面应用方面有着独特的优势;
  6. 视图,数据,结构分离:使数据的更改更为简单,不需要进行逻辑代码的修改,只需要操作数据就能完成相关操作;

说说你对 SPA 单页面的理解,它的优缺点分别是什么?

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

优点:用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染;

基于上面一点,SPA 相对对服务器压力小;

前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理;

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

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

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

template预编译是什么?

对于 Vue 组件来说,模板编译只会在组件实例化的时候编译一次,生成渲染函数之后在也不会进行编译。因此,编译对组件的 runtime 是一种性能损耗。

而模板编译的目的仅仅是将template转化为render function,这个过程,正好可以在项目构建的过程中完成,这样可以让实际组件在 runtime 时直接跳过模板渲染,进而提升性能,这个在项目构建的编译template的过程,就是预编译。

Vue模板渲染的原理是什么?

vue中的模板template无法被浏览器解析并渲染,因为这不属于浏览器的标准,不是正确的HTML语法,所有需要将template转化成一个JavaScript函数,这样浏览器就可以执行这一个函数并渲染出对应的HTML元素,就可以让视图跑起来了,这一个转化的过程,就成为模板编译。

模板编译又分三个阶段,解析parse,优化optimize,生成generate,最终生成可执行函数render。

parse阶段:使用大量的正则表达式对template字符串进行解析,将标签、指令、属性等转化为抽象语法树AST。

optimize阶段:遍历AST,找到其中的一些静态节点并进行标记,方便在页面重渲染的时候进行diff比较时,直接跳过这一些静态节点,优化runtime的性能。

generate阶段:将最终的AST转化为render函数字符串。

Vue 模板编译原理

大致分为3步:

一、解析器:模板字符 --> 抽象语法树(AST)

<div>
    <p>{{name}}</p>
</div>

抽象语法树(AST)

{
    tag: 'div',
    type: 1,
    staticRoot: false,
    static: false,
    plain: true, // 标记节点有没有属性
    parent: undefined,
    attrList: [],
    attrMap: {},
    children: [
        {
            tag: 'p',
            type: 1,
            staticRoot: false,
            static: false,
            plain: true,
            parent: { tag: "div", ...},
            attrList: [],
            attrMap: {},
            children: [
                    {
                        type: 2,
                        text: '{{name}}',
                        static: false,
                        expression: "_s(name)"
                    }
            ]
        }
    ]
}

二、优化器:对抽象语法树(AST)进行静态节点标记(用来做虚拟DOM的渲染优化)

优化器的目标是找出那些静态节点并打上标记,而静态节点指的是 DOM 不需要发生变化的节点,例如:

<p>我是静态节点,我不需要发生变化</p>

标记静态节点有两个好处:

每次重新渲染的时候不需要为静态节点创建新节点

在 Virtual DOM 中 patching 的过程可以被跳过

优化器的实现原理主要分两步:

第一步:用递归的方式将所有节点添加 static 属性,标识是不是静态节点

第二步:标记所有静态根节点

三、代码生成器:抽象语法树(AST)--> render 函数代码字符串

使用本文开头举的例子中的模板生成后的 AST 来生成 render 后是这样的:

with(this){
  return _c(
    'div',
    [
      _c(
        'p',
        [
          _v(_s(name))
        ]
      )
    ]
  )
}

生成后的代码字符串中看到了有几个函数调用 _c,_v,_s。

_c 对应的是 createElement,它的作用是创建一个元素。

第一个参数是一个HTML标签名

第二个参数是元素上使用的属性所对应的数据对象,可选项

第三个参数是 children

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

1. Vue 的单向数据流:指数据一般从父组件传到子组件,子组件没有权利直接修改父组件传来的数据,即子组件从 props 中直接获取的数据,只能请求父组件修改数据再传给子组件。父级属性值的更新会下行流动到子组件中。

2. 为什么不能子组件直接修改父组件传来的值呢?父组件的值可能会不断发生变化,那么如果我们子组件对父组件传来的值比如说 props 有一个 number,子组件收到了 number=1,在收到后,子组件直接改变number 的值为 5,去做些事情,但还未做时父组件数据更新了,传过来一个值 3,也就是说子组件刚将其变为 5,父组件又把它变成了 3,可能影响子组件的使用。说的官方一些,就是父组件的值更新时,子组件中 props 的值也会发生更新。

3. 在子组件中直接用 v-model 绑定父组件传过来的数据是不合理的,如果希望修改父组件传给子组件的值:(1)在子组件 data 中创建一个变量获取 props 中的值,再改变这个 data 中的值。

(2)子组件使用 $emit 发出一个事件,让父组件接收去修改这个值。

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

因为组件是用来复用的,且 JS 里对象是引用关系,如果组件中 data 是一个对象,那么这样作用域没有隔离,子组件中的 data 属性值会相互影响,如果组件中 data 选项是一个函数,那么每个实例可以维护一份被返回对象的独立的拷贝,组件实例之间的 data 属性值不会互相影响;而 new Vue 的实例,是不会被复用的,因此不存在引用对象的问题。

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

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

当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue

当你修改数组的长度时,例如:vm.items.length = newLength

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

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

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

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

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

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

更准确:因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确。

谈谈你对 Vue 生命周期的理解?

Vue 的生命周期就是实例从初始化到销毁的过程

beforeCreate (创建前):实例在完全创建出来之前,访问不到 data 的数据和 methods 的方法。

场景:可以在这里做一个 loading 效果,等页面加载完成(也就是 mounted() 之后再销毁这个 loading)

created (创建完成):实例创建完成,可以访问data中的数据和调用methods中的方法,在这里会对data 的属性进行遍历,给每个属性加上 getter 和 setter,实现数据的响应式,但 DOM 还没生成。

场景:可以发一些 ajax 请求

beforeMount (挂载前):实例挂载的 DOM 元素 $el 已经初始化,模板已经在内存中编译完成,只是还没将模板渲染到页面中

mounted (挂载完成):页面已渲染完成,初始化阶段完成,可访问dom结构

场景:数据交互,发送 ajax 请求

beforeUpdate (更新前):当响应式数据发生变化时触发,此时数据更新了但页面还没更新

updated (更新完成):当响应式数据发生变化时触发,此时数据已经和新数据保持同步

beforeDestroy (销毁前):实例销毁前调用,此时任然可以访问到实例,this 任指向实例对象

场景:清除定时器、解绑全局事件

destroyed (销毁完成):Vue 实例销毁后调用。调用后,Vue 实例的所有东西都会解除绑定,所有的事件监听器也会被移除,所有的子实例也会被销毁。

Activited:keep-alive 专属,组件被激活时调用

Deactivated:keep-alive 专属,组件被销毁时调用

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

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

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

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

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

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

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

可以在钩子函数 created、beforeMount、mounted 中进行调用,因为在这三个钩子函数中,data 已经创建,可以将服务端返回的数据进行赋值。但是推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:能更快获取到服务端数据,减少页面 loading 时间;

生命周期钩子原理?

答案:Vue的生命周期钩子就是回调函数而已,当创建组件实例的过程中会调用对应的钩子方法。内部主要是使用callHook方法来调用对应的方法。核心是一个发布订阅模式,将钩子订阅好(内部采用数组的方式存储),在对应的阶段进行发布!

查看vue源码,会发现在vue实例初始化(_init)、挂载($mount )等过程中,都调用了一个叫callHook的方法,如下图:

再看看callHook函数作用:调用生命周期钩子函数

export function callHook (vm: ***ponent, hook: string) {
  // #7573 disable dep collection when invoking lifecycle hooks
  pushTarget() //为了避免在某些生命周期钩子中使用 props 数据导致收集冗余的依赖
  const handlers = vm.$options[hook] //获取生命周期钩子 vue选项合并会把生命周期钩子选项合并成一个数组
  if (handlers) {
    for (let i = 0, j = handlers.length; i < j; i++) {
      try {
        handlers[i].call(vm)//为了保证生命周期钩子函数内可以通过 this 访问实例对象,所以使用 .call(vm) 执行这些函数
      } catch (e) { //为了捕捉可能出现的错误
        handleError(e, vm, `${hook} hook`)
      }
    }
  }
  if (vm._hasHookEvent) {
    vm.$emit('hook:' + hook)
  }
  popTarget() //为了避免在某些生命周期钩子中使用 props 数据导致收集冗余的依赖
}

AST 抽象语法树 生成过程?

AST 是什么
抽象语法树 (Abstract Syntax Tree),简称 AST,它是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。
AST 有什么用
AST 运用广泛,比如:
编辑器的错误提示、代码格式化、代码高亮、代码自动补全;
elint、pretiier 对代码错误或风格的检查;
webpack 通过 babel 转译 javascript 语法;
AST 如何生成
js 执行的第一步是读取 js 文件中的字符流,然后通过词法分析生成 token,之后再通过语法分析( Parser )生成 AST,最后生成机器码执行。
整个解析过程主要分为以下两个步骤:
1、分词:将整个代码字符串分割成最小语法单元数组
2、语法分析:在分词基础上建立分析语法单元之间的关
JS Parser 是 js 语法解析器,它可以将 js 源码转成 AST,常见的 Parser 有 esprima、traceur、acorn、shift 等。
词法分析
词法分析,也称之为扫描(scanner),简单来说就是调用 next() 方法,一个一个字母的来读取字符,然后与定义好的 JavaScript 关键字符做比较,生成对应的Token。Token 是一个不可分割的最小单元:
例如 var 这三个字符,它只能作为一个整体,语义上不能再被分解,因此它是一个 Token
词法分析器里,每个关键字是一个 Token ,每个标识符是一个 Token,每个操作符是一个 Token,每个标点符号也都是一个 Token。除此之外,还会过滤掉源程序中的注释和空白字符(换行符、空格、制表符等。
最终,整个代码将被分割进一个tokens列表(或者说一维数组)。
语法分析
语法分析会将词法分析出来的 Token 转化成有语法含义的抽象语法树结构。同时,验证语法,语法如果有错的话,抛出语法错误。

在什么阶段才能访问操作DOM

在钩子函数 mounted 被调用前,Vue 已经将编译好的模板挂载到页面上,所以在 mounted 中可以访问操作 DOM。

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

方法一:使用$emit,当子组件调了钩子函数的时候,触发父组件绑定的方法

// 父组件代码:<child @created="doSomething" />

// 子组件代码:created() { this.$emit('created') }

方法二:使用@hook:原理跟方法一是一样的,只是框架已经帮我们处理好了

// 父组件代码: <child @hook:created="doSomething"/>

谈谈你对 keep-alive 的了解?

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

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

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

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

keep-alive 使用场景和原理

<keep-alive> 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。<keep-alive> 是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在组件的父组件链中。

当组件在 <keep-alive> 内被切换时,它的 mounted 和 unmounted 生命周期钩子不会被调用,取而代之的是 activated 和 deactivated。(这会运用在 <keep-alive> 的直接子节点及其所有子孙节点。)

主要用于保留组件状态或避免重新渲染。

<!-- 基本 -->
<keep-alive>
  <***ponent :is="view"></***ponent>
</keep-alive>

<!-- 多个条件判断的子组件 -->
<keep-alive>
  <***p-a v-if="a > 1"></***p-a>
  <***p-b v-else></***p-b>
</keep-alive>

注意,<keep-alive> 是用在其一个直属的子组件被切换的情形。如果你在其中有 v-for 则不会工作。如果有上述的多个条件性的子元素,<keep-alive> 要求同时只有一个子元素被渲染。

include 和 exclude

include 和 exclude prop 允许组件有条件地缓存。二者都可以用逗号分隔字符串、正则表达式或一个数组来表示:

<!-- 逗号分隔字符串 -->
<keep-alive include="a,b">
  <***ponent :is="view"></***ponent>
</keep-alive>

<!-- regex (使用 `v-bind`) -->
<keep-alive :include="/a|b/">
  <***ponent :is="view"></***ponent>
</keep-alive>

<!-- Array (使用 `v-bind`) -->
<keep-alive :include="['a', 'b']">
  <***ponent :is="view"></***ponent>
</keep-alive>

匹配首先检查组件自身的 name 选项,如果 name 选项不可用,则匹配它的局部注册名称 (父组件 ***ponents 选项的键值)。匿名组件不能被匹配。

max

最多可以缓存多少组件实例。一旦这个数字达到了,在新实例被创建之前,已缓存组件中最久没有被访问的实例会被销毁掉。

<keep-alive :max="10">
  <***ponent :is="view"></***ponent>
</keep-alive
<keep-alive> 不会在函数式组件中正常工作,因为它们没有缓存实例。

keep-alive 运用了 LRU 算法,选择最近最久未使用的组件予以淘汰。

扩展补充:LRU 算法是什么?请仔细看下图

vue 双向绑定(v-model)的原理?

Vue是采用数据劫持结合发布/订阅模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。

v-model 实际上是做了两步动作:1、绑定数据value

2、触发输入事件input

首先用Object.defineProperty()为每个属性添加访问器属性,实现数据劫持,然后为每个属性分配一个订阅者集合的管理数组dep;编译的时候在数组dep中添加订阅者,凡是用到该属性的地方都会被放进dep数组里,接着为input会添加监听事件,修改值就等于为该属性赋值,则会触发该属性的set方法,在set方法内通知订阅者数组dep,订阅者数组循环调用各订阅者的update方法更新视图。

v-model原理及应用

v-model在input元素上时

第一行的代码其实只是第二行的语法糖

<input v-model="sth" />
<input v-bind:value="sth" v-on:input="sth = $event.target.value" />

然后第二行代码还能简写成这样:

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

要理解这行代码,首先你要知道 input 元素本身有个 oninput 事件,这是 HTML5 新增加的,类似 onchange ,每当输入框内容发生变化时,就会触发oninput,把最新的value传递给 sth。

我们仔细观察语法糖和原始语法那两行代码,可以得出一个结论:

在给<input />元素添加v-model属性时,默认会把value作为元素的属性,然后把'input'事件作为实时传递value的触发事件

在 3.x 中,自定义组件上的 v-model 相当于传递了 modelValue prop 并接收抛出的 update:modelValue 事件:

<Child***ponent v-model="pageTitle" />
<!-- 是以下的简写: -->
<Child***ponent
  :modelValue="pageTitle"
  @update:modelValue="pageTitle = $event"
/>

v-model 参数

默认情况下,组件上的 v-model 使用 modelValue 作为 prop 和 update:modelValue 作为事件。我们可以通过向 v-model 传递参数来修改这些名称:

<my-***ponent v-model:title="bookTitle"></my-***ponent>

在本例中,子组件将需要一个 title prop 并发出 update:title 事件来进行同步:

app.***ponent('my-***ponent', {
  props: {
    title: String
  },
  emits: ['update:title'],
  template: `
    <input
      type="text"
      :value="title"
      @input="$emit('update:title', $event.target.value)">
  `
})

Vue事件绑定原理、

Vue中通过v-on或其语法糖@指令来给元素绑定事件并且提供了事件修饰符,基本流程是进行模板编译生成AST,生成render函数后并执行得到VNode,VNode生成真实DOM节点或者组件时候使用addEventListener方法进行事件绑定。

$on、$emit是基于发布订阅模式的,维护一个事件中心,on的时候将事件按名称存在事件中心里,称之为订阅者,然后emit将对应的事件进行发布,去执行事件中心里的对应的监听器。
编译阶段、代码生成
Vue在挂载实例前,有相当多的工作是进行模板的编译,将template模板进行编译,解析成AST树,再转换成render函数,而在编译阶段,就是对事件的指令做收集处理。
在template模板中,定义事件的部分是属于XML的Attribute,所以收集指令时需要匹配Attributes以确定哪个Attribute是属于事件
事件绑定
前面介绍了如何编译模板提取事件收集指令以及生成render字符串和render函数,但是事件真正的绑定到DOM上还是离不开事件注册,此阶段就发生在patchVnode过程中,在生成完成VNode后,进行patchVnode过程中创建真实DOM时会进行事件注册的相关钩子处理。

v-for为什么要加key原因的正确表达

如果不使用key,Vue会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。key 是为Vue中Vnode的唯一标识,通过这个key,我们的diff操作可以更准确、更快速。
更准确:因为带key就不是就地复用了,在sameNode函数 a.key === b.key 对比中可以避免就地复用的情况。所以更加准确。
更快速:利用key的唯一性生成map对象来获取对应节点,比遍历方式块。

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

Vue 组件间通信是面试常考的知识点之一,这题有点类似于开放题,你回答出越多方法当然越加分,表明你对 Vue 掌握的越熟练。Vue 组件间通信只要指以下 3 类通信:父子组件通信、隔代组件通信、兄弟组件通信,下面我们分别介绍每种通信方式且会说明此种方法可适用于哪类组件间通信。

(1)props / $emit 适用 父子组件通信

这种方法是 Vue 组件的基础,相信大部分同学耳闻能详,所以此处就不举例展开介绍。

(2)ref 与 children 适用 父子组件通信

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

children:访问父 / 子实例

(3)EventBus (on) 适用于 父子、隔代、兄弟组件通信

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

(4)listeners 适用于 隔代组件通信

attrs" 传入内部组件。通常配合 inheritAttrs 选项一起使用。

listeners" 传入内部组件

(5)provide / inject 适用于 隔代组件通信

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

(6)Vuex 适用于 父子、隔代、兄弟组件通信

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )。

Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。

改变 store 中的状态的唯一途径就是显式地提交 (***mit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。

你使用过 Vuex 吗?

Vuex 是一个状态管理模式。核心就是 store(仓库)。

(1)Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。

(2)改变 store 中的状态的唯一途径就是显式地提交 (***mit) mutation。方便跟踪一个状态的变化。

主要包括以下几个模块:

State:定义了应用状态的数据结构,可以在这里设置默认的初始状态。

Getter:允许组件从 Store 中获取数据,mapGetters 辅助函数可以将 store 中的 getter 映射为计算属性。

Mutation:是唯一更改 store 中状态的方法,且必须是同步函数。

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

Module:允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中。

简单介绍一下 vuex 实现原理?

Vue.use(vuex)会调用vuex的install方法,该方法主要作用就是在所有组件的beforeCreate生命周期注入了设置this.$store这样一个对象。保证每个组件通过$store可以取到全局store对象;

业务代码中我们会这样使用如下操作:

const store = new Vuex.Store({state, mutations, actions, modules});

Vuex.Store 这个类构造方法中会做一些初始化操作,最关键的是执行一个 resetStoreVM(this, state)方法,重置VM。其本质就是将我们传入的state作为一个隐藏的vue组件的data,也就是说,我们的***mit操作,本质上其实是修改这个组件的data值。核心代码如下:

// src/store.js
function resetStoreVM (store, state, hot) {
  // 省略无关代码
  Vue.config.silent = true
  store._vm = new Vue({
    data: {
      $$state: state
    },
    ***puted
  })
}

Vuex的state状态是响应式,是借助vue的data是响应式,将state存入vue实例组件的data中;

Vuex的getters则是借助vue的计算属性***puted实现数据实时监听。

Vuex 为什么要分模块并且加命名空间?

模块:由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能会变得相当臃肿。为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块。

命名空间:默认情况下,模块内部的 action、mutation、getter是注册在全局命名空间的 --- 这样使得多个模块能够对同一 mutation 或 action 做出响应。如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced:true 的方式使其成为带命名的模块。当模块被注册后,他所有 getter、action、及 mutation 都会自动根据模块注册的路径调整命名。

【扩展】Vuex 页面刷新数据丢失怎么解决?

需要做 vuex 数据持久化,一般使用本地储存的方案来保存数据,可以自己设计存储方案,也可以使用第三方插件。

推荐使用 vuex-persist (脯肉赛斯特)插件,它是为 Vuex 持久化储存而生的一个插件。不需要你手动存取 storage,而是直接将状态保存至 cookie 或者 localStorage中。

介绍一下 nextTick

vue进行DOM更新内部也是调用nextTick来做异步队列控制。只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个 watcher 被多次触发,只会被推入到队列中一次。

DOM至少会在当前事件循环里面的所有数据变化完成之后,再统一更新视图。而当我们自己调用nextTick的时候,它就在更新DOM的microtask(微任务队列)后追加了我们自己的回调函数。

大致说一下整个过程:

1、nextTick接受一个回调函数时(当不传参数的时候,提供一个Promise化的调用),传入的回调函数会在callbacks中存起来,根据一个状态标记 pending 来判断当前是否要执行 timerFunc();

2、timerFunc() 是根据当前环境判断使用哪种方式实现,按照 Promise.then和 MutationObserver以及setImmediate的优先级来判断,支持哪个就用哪个,如果执行环境不支持,会采用setTimeout(fn, 0)代替;

3、timerFunc()函数中会执行 flushCallbacks函数,flushCallbacks函数的作用就是对所有callback进行遍历,然后指向响应的回调函数。

总结:Vue是异步更新DOM的,在平常的开发过程中,我们可能会需要基于更新后的 DOM 状态来做点什么,比如后端接口数据发生了变化,某些方法是依赖于更新后的DOM变化,这时我们就可以使用 Vue.nextTick(callback)方法。

源码:

/* @flow */
/* globals MutationObserver */

import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'

export let isUsingMicroTask = false

const callbacks = []
let pending = false

/**
 * 对所有callback进行遍历,然后指向响应的回调函数
 * 使用 callbacks 保证了可以在同一个tick内执行多次 nextTick,不会开启多个异步任务,而把这些异步任务都压成一个同步任务,在下一个 tick 执行完毕。
*/

function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]( "i")
  }
}
let timerFunc
/**
* timerFunc 实现的就是根据当前环境判断使用哪种方式实现
* 就是按照 Promise.then和 MutationObserver以及setImmediate的优先级来判断,支持哪个就用哪个,如果执行环境不支持,会采用setTimeout(fn, 0)代替;
*/

// 判断是否支持原生 Promise
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
  // 不支持 Promise的话,再判断是否原生支持 MutationObserver
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // 新建一个 textNode的DOM对象,使用 MutationObserver 绑定该DOM并传入回调函数,在DOM发生变化的时候会触发回调,该回调会进入主线程(比任务队列优先执行)
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    // 此时便会触发回调
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
  // 不支持的 MutationObserver 的话,再去判断是否原生支持 setImmediate
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // Promise,MutationObserver, setImmediate 都不支持的话,最后使用 setTimeout(fun, 0)
  // Fallback to setTimeout.
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

// 该函数的作用就是延迟 cb 到当前调用栈执行完成之后执行
export function nextTick (cb?: Function, ctx?: Object) {
  // 传入的回调函数会在callbacks中存起来
  let _resolve
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  // pending是一个状态标记,保证timerFunc在下一个tick之前只执行一次
  if (!pending) {
    pending = true
    /**
    * timerFunc 实现的就是根据当前环境判断使用哪种方式实现
    * 就是按照 Promise.then和 MutationObserver以及setImmediate的优先级来判断,支持哪个就用哪个,如果执行环境不支持,会采用setTimeout(fn, 0)代替;
    */
    timerFunc()
  }
  // 当nextTick不传参数的时候,提供一个Promise化的调用
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

nextTick 使用场景和原理?

nextTick用于将回调推迟到下一个 DOM 更新周期之后执行。在更改了一些数据以等待 DOM 更新后立即使用它。
import { createApp, nextTick } from 'vue'

const app = createApp({
  setup() {
    const message = ref('Hello!')
    const changeMessage = async newMessage => {
      message.value = newMessage
      await nextTick()
      console.log('Now DOM is updated')
    }
  }
})
原理分析
简单来说,Vue在修改数据后,视图不会立刻更新,而是等同一事件循环中的所有数据变化完成之后,再统一进行视图更新
这里主线程的执行过程就是一个tick,而所有的异步结果都是通过任务队列来调度。Event Loop 分为宏任务和微任务,无论是执行宏任务还是微任务,完成后都会进入到一下tick,并在两个tick之间进行UI渲染。

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

理解 - Vue.***ponent()Vue.use()this.$xxx()

Vue.***ponent()方法注册全局组件。
第一个参数是自定义元素名称,也就是将来在别的组件中使用这个组件的标签名称。
第二个参数是将要注册的Vue组件。
import Vue from 'vue';
// 引入loading组件 
import Loading from './loading.vue';
// 将loading注册为全局组件,在别的组件中通过<loading>标签使用Loading组件
Vue.***ponent('loading', Loading);
Vue.use注册插件,这接收一个参数。这个参数必须具有install方法。Vue.use函数内部会调用参数的install方法。
如果插件没有被注册过,那么注册成功之后会给插件添加一个installed的属性值为true。Vue.use方法内部会检测插件的installed属性,从而避免重复注册插件。
插件的install方法将接收两个参数,第一个是参数是Vue,第二个参数是配置项options。
在install方法内部可以添加全局方法或者属性
import Vue from 'vue';

// 这个插件必须具有install方法
const plugin = {
  install (Vue, options) {
    // 添加全局方法或者属性
    Vue.myGlobMethod = function () {};
    // 添加全局指令
    Vue.directive();
    // 添加混入
    Vue.mixin();
    // 添加实例方法
    Vue.prototype.$xxx = function () {};
    // 注册全局组件
    Vue.***ponent()
  }
}

// Vue.use内部会调用plugin的install方法
Vue.use(plugin);
将Hello方法挂载到Vue的prototype上.

import Vue from 'vue';
import Hello from './hello.js';
Vue.prototype.$hello = Hello;
vue组件中就可以this.$hello('hello world')

使用过 Vue SSR 吗?说说 SSRServer Side Render服务端渲染)?

SSR:服务端直接返回html文本给浏览器。服务端对vue页面进行渲染(获取数据,填充组件都在服务端完成)生成HTML文件,将HTML页面传给浏览器

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

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

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

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

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

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

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

vue-router 路由模式有几种?

vue-router 有 2 种路由模式:hash、history,

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

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

能说下 vue-router 中常用的 hash history 路由模式实现原理吗?

(1)hash 模式的实现原理

早期的前端路由的实现就是基于 location.hash 来实现的。其实现原理很简单,location.hash 的值就是 URL 中 # 后面的内容。比如下面这个网站,它的 location.hash 的值为 '#search':

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

URL 中 hash 值只是客户端的一种状态,也就是说当向服务器端发出请求时,hash 部分不会被发送;

hash 值的改变,都会在浏览器的访问历史中增加一个记录。因此我们能通过浏览器的回退、前进按钮控制hash 的切换;

可以通过 a 标签,并设置 href 属性,当用户点击这个标签后,URL 的 hash 值会发生改变;或者使用 JavaScript 来对 loaction.hash 进行赋值,改变 URL 的 hash 值;

我们可以使用 hashchange 事件来监听 hash 值的变化,从而对页面进行跳转(渲染)。

(2)history 模式的实现原理

HTML5 提供了 History API 来实现 URL 的变化。其中做最主要的 API 有以下两个:history.pushState() 和 history.repalceState()。这两个 API 可以在不进行刷新的情况下,操作浏览器的历史纪录。唯一不同的是,前者是新增一个历史记录,后者是直接替换当前的历史记录,如下所示:

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

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

pushState 和 repalceState 两个 API 来操作实现 URL 的变化 ;

我们可以使用 popstate 事件来监听 url 的变化,从而对页面进行跳转(渲染);

history.pushState() 或 history.replaceState() 不会触发 popstate 事件,这时我们需要手动触发页面跳转(渲染)。

vue-router实现原理?

端路由简介以及vue-router实现原理原理核心就是 更新视图但不重新请求页面。路径之间的切换,也就是组件的切换。vue-router实现单页面路由跳转模式:hash模式、history模式。根据设置mode参数

// router.js 
export default new Router({
    mode:"hash",
    routes:[
        {
            path:"/hello",
            ***ponent:HelloWorld
        },
    ]
})

hash模式:通过锚点值的改变,根据不同的值,渲染指定DOM位置的不同数据。每一次改变#后的部分,都会在浏览器的访问历史中增加一个记录,使用”后退”按钮,就可以回到上一个位置。history模式:利用 window.history.pushState API 来完成 URL 跳转而无须重新加载页面。

<template>
  <div id="app">
    <router-link to="/hello">HelloWorld</router-link>
    <router-view></router-view>
  </div>
</template>

我们点击了router-link时导致路由变了,vue-router内部必然是在监听路由变化,根据路由规则找到匹配的组件,然后在router-view中渲染。路由切换最终是页面的不同组件的展示,而没有真正去刷新页面。

// main.js
import router from "./config/router";
new Vue({
  router,
  render: h => h(App),
}).$mount('#app')

vue-router核心实现原理:

实现一个静态install方法,因为作为插件都必须有这个方法,给Vue.use()去调用;

可以监听路由变化;

解析配置的路由,即解析router的配置项routes,能根据路由匹配到对应组件;

实现两个全局组件router-link和router-view;(最终落地点)

核心代码实现简版:

let Vue;
class KVueRouter {
    constructor(options){
        this.$options=options;
        this.$routerMap={};//{"/":{***ponent:...}}
        // url 响应式,当值变化时引用的地方都会刷新
        this.app = new Vue({
            data:{
                current:"/"
            }
        });
    }
    // 初始化
    init(){
        // 监听事件
        this.bindEvent();
        // 解析路由
        this.createRouteMap();
        // 声明组件
        this.init***ponent();
    }
    bindEvent(){
        window.addEventListener('hashchange',this.onHashchange.bind(this));
    }
    onHashchange(){
        this.app.current = window.location.hash.slice(1) || "/";
    }
    createRouteMap(){
        this.$options.routes.forEach(route=>{
            this.$routerMap[route.path]=route;
        })
    }
    init***ponent(){
        Vue.***ponent('router-link',{
            props:{
                to:String,
            },
            render(h){
                return h('a',{attrs:{href:'#'+this.to}},[this.$slots.default])
            }
        });
        Vue.***ponent('router-view',{
            render:(h)=>{
                const ***ponent = this.$routerMap[this.app.current].***ponent;
                return h(***ponent)
            }
        });
    }
}
// 参数是vue构造函数,Vue.use(router)时,执行router的install方法并把Vue作为参数传入
KVueRouter.install = function(_vue){
    Vue = _vue;
    //全局混入
    Vue.mixin({
        beforeCreate(){//拿到router的示例,挂载到vue的原型上
            if (this.$options.router) {
                Vue.prototype.$router=this.$options.router;
                this.$options.router.init();
            }
        }
    })
}
export default KVueRouter;

解读如下:

Vue.use(Router)时,会调用router的install方法并把Vue类传入,混入beforeCreate方法,即在Vue实例化后挂载前在vue原型上挂个$router方法(因为这样后面才能用this.$router.push()...但此处没有实现哦),然后调用router实例的init方法;

在init中把三件事情都干了,监听路由,解析路由(路由mapping匹配),定义组件;

需要注意的是,存储当前路由的变量this.app.current非一般的变量,而是借用Vue的响应式定义的,所以当路由变化时只需要给这个this.app.current赋值,而router-view组件刚好引用到这个值,当其改变时所有的引用到的地方都会改变,则得到的要展示的组件也就响应式的变化了。

vue-router 导航守卫分类 & 解析流程

何为路由守卫?路由守卫有点类似于ajax的请求拦截器,就是请求发送之前先给你拦截住做一些事情之后再去发送请求,同样这里的路由守卫意思差不多;简单理解为就是你在进路由之前,首先把你拦住,对你进行检查;这是不是有点中学门口的保安?进来之前拦住,有学生证就进,没有学生证就不让进;当然,路由守卫不仅仅只是在你进入之前拦住你,还有其他的钩子函数进行其他操作;

vue-router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航。这里有很多方式植入路由导航中:全局的,单个路由独享的,或者组件级的。

1、全局钩子函数(beforeEach、afterEach)

2、路由独享的钩子函数(beforeEnter)

3、组件内钩子函数(beforeRouterEnter、beforeRouterUpdate、beforeRouterLeave)

完整的导航解析流程

  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 的回调函数,创建好的组件实例会作为回调函数的参数传入。

什么是 MVVM

M: Model 模型

V:View 视图

VM:ViewModel 视图模型

VM(视图模型) 是 MVVM 设计模式的核心,它是连接模型(数据) 和 视图(界面)的桥梁,实现了 View 和 Model 的同步,当 model 属性发生变化不需要手动去操作 dom 来改变视图,当 view 发生变化也不需要手动修改 model 属性,都是 VM 自动同步的,这就称之为 数据的双向绑定。

将数据绑定到 viewModel 层上,会自动将数据渲染到页面中,视图变化会通知 viewModel 层更新数据。

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

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

Proxy Object.defineProperty 优劣对比

Proxy 的优势如下:

Proxy 可以直接监听对象而非属性;

Proxy 可以直接监听数组的变化;

Proxy 有多达 13 种拦截方法,不限于 apply、ownKeys、deleteProperty、has 等等Object.defineProperty 不具备的;

Proxy 返回的是一个新对象,我们可以只操作新的对象达到目的,而 Object.defineProperty 只能遍历对象属性直接修改;

Proxy 作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利;

Object.defineProperty 的优势如下:

兼容性好,支持 IE9,而 Proxy 的存在浏览器兼容性问题,而且无法用 polyfill 磨平,因此 Vue 的作者才声明需要等到下个大版本( 3.0 )才能用 Proxy 重写。

Vue 怎么用 vm.$set() 解决对象新增属性不能响应的问题

受现代 JavaScript 的限制 ,Vue 无法检测到对象属性的添加或删除。由于 Vue 会在初始化实例时对属性执行 getter/setter 转化,所以属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的。

但是 Vue 提供了 Vue.set (object, propertyName, value) / vm.$set (object, propertyName, value) 来实现为对象添加响应式属性,那框架本身是如何实现的呢?

vm.$set 的实现原理是:

如果目标是数组,直接使用数组的 splice 方法触发相应式;

如果目标是对象,会先判读属性是否存在、对象是否是响应式,最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理( defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 getter 和 setter 的功能所调用的方法)

Vue.set 方法原理

由于JS的限制,有些Vue无法检测的更改类型。但是,有一些方法可以规避它们以维持响应性。

对于对象

Vue 无法检测到 property 的添加或删除。由于 Vue 在实例初始化期间执行 getter/setter 转换过程,因此必须在 data对象中存在一个 property,以便 Vue 对其进行转换并使其具有响应式。例如:

var vm = new Vue({
  data: {
    a: 1
  }
})
// `vm.a` 现在是响应式的

vm.b = 2
// `vm.b` 不是响应式的

对于已经创建的实例,Vue 不允许动态添加根级别的响应式 property。但是,可以使用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式 property:

Vue.set(vm.someObject, 'b', 2)

你还可以使用 vm.$set 实例方法,这也是全局 Vue.set 方法的别名:

this.$set(this.someObject, 'b', 2)

有时你可能需要为已有对象赋值多个新 property,比如使用 Object.assign() 或 _.extend()。但是,这样添加到对象上的新 property 不会触发更新。在这种情况下,你应该用原对象与要混合进去的对象的 property 一起创建一个新的对象。

// 而不是 `Object.assign(this.someObject, { a: 1, b: 2 })`
this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })

对于数组

Vue 不能检测以下数组的变动:

当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue

当你修改数组的长度时,例如:vm.items.length = newLength

例如:

var vm = new Vue({
  data: {
    items: ['a', 'b', 'c']
  }
})
vm.items[1] = 'x' // 不是响应式的
vm.items.length = 2 // 不是响应式的

为了解决第一种问题,以下两种方式都可以实现和 vm.items[indexOfItem] = newValue 相同的效果,同时也会触发响应性系统的状态更新:

// Vue.set
Vue.set(vm.items, indexOfItem, newValue)
// Array.prototype.splice
vm.items.splice(indexOfItem, 1, newValue)

你也可以使用 vm.$set实例方法,该方法是全局方法 Vue.set 的一个别名:

vm.$set(vm.items, indexOfItem, newValue)

为了解决第二种问题(修改数组的长度时非响应式),你可以使用 splice:


vm.items.splice(newLength)

声明响应式 property

由于 Vue 不允许动态添加根级响应式 property,所以你必须在初始化实例前声明所有根级响应式 property,哪怕只是一个空值:

var vm = new Vue({
  data: {
    // 声明 message 为一个空值字符串
    message: ''
  },
  template: '<div>{{ message }}</div>'
})
// 之后设置 `message`
vm.message = 'Hello!'

如果你未在 data 选项中声明 message,Vue 将警告你渲染函数正在试图访问不存在的 property。

这样的限制在背后是有其技术原因的,它消除了在依赖追踪系统中的一类边界情况,也使组件实例能更好地配合类型检查系统工作。但与此同时在代码可维护性方面也有一点重要的考虑:data 对象就像组件的状态结构 (schema)。提前声明所有的响应式 property,可以让组件代码在未来修改或给其他开发人员阅读时更易于理解。

Vue.set 或者说是 $set 原理

由响应式数据 我们给对象和数组本身新增了__ob__属性,代表的是 Observer 实例。当给对象新增不存在的属性,首先会把新的属性进行响应式跟踪 然后会触发对象 __ob__ 的dep收集到的 watcher 去更新,当修改数组索引时我们调用数组本身的 splice 方法去更新数组。

VUE 自定义指令及原理分析

除了核心功能默认内置的指令 (例如 v-model 和 v-show),Vue 也允许注册自定义指令。注意,在 Vue 中,代码复用和抽象的主要形式是组件。然而,有的情况下,你仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。举个聚焦输入框的例子,如下:

当页面加载时,该元素将获得焦点 (注意:autofocus 在移动版 Safari 上不工作)。事实上,如果你在打开这个页面后还没有点击过任何内容,那么此时这个输入框就应当处于聚焦状态。此外,你可以单击 Rerun 按钮,输入框将被聚焦。

现在让我们用指令来实现这个功能:

const app = Vue.createApp({})
// 注册一个全局自定义指令 `v-focus`
app.directive('focus', {
  // 当被绑定的元素挂载到 DOM 中时……
  mounted(el) {
    // 聚焦元素
    el.focus()
  }
})

如果想注册局部指令,组件中也接受一个 directives 的选项:

directives: {
  focus: {
    // 指令的定义
    mounted(el) {
      el.focus()
    }
  }
}

然后你可以在模板中任何元素上使用新的 v-focus attribute,如下:

<input v-focus />

原理:

指令本质上是装饰器,是vue对HTML元素的扩展,给HTML元素增加自定义功能,语义化HTML标签。vue编译DOM时,会执行与指令关联的JS代码,即找到指令对象,执行指令对象的相关方法。

1、在生成 ast 语法树时,遇到指令会给当前元素添加 directives 属性

2、通过 genDirectives 生成指令代码

3、在 patch 前将指令的钩子提取到 cbs 中,在 patch 过程中调用对应的钩子。

4、当执行指令对应钩子函数时,调用对应指令定义方法。

【扩展】directive 注册或检索全局指令

参数:{string} name

{Function | Object} [definition]

返回值:如果传入 definition 参数,则返回应用实例。

如果不传入 definition 参数,则返回指令定义。

用法:注册或检索全局指令。

示例:

import { createApp } from 'vue'
const app = createApp({})
// 注册
app.directive('my-directive', {
  // 指令具有一组生命周期钩子:
  // 在绑定元素的 attribute 或事件监听器被应用之前调用
  created() {},
  // 在绑定元素的父组件挂载之前调用
  beforeMount() {},
  // 在绑定元素的父组件挂载之后调用
  mounted() {},
  // 在包含组件的 VNode 更新之前调用
  beforeUpdate() {},
  // 在包含组件的 VNode 及其子组件的 VNode 更新之后调用
  updated() {},
  // 在绑定元素的父组件卸载之前调用
  beforeUnmount() {},
  // 在绑定元素的父组件卸载之后调用
  unmounted() {}
})

// 注册 (函数指令)
app.directive('my-directive', () => {
  // 这将被作为 `mounted` 和 `updated` 调用
})

// getter, 如果已注册,则返回指令定义
const myDirective = app.directive('my-directive')

Vue 修饰符分类?

1、事件修饰符

在事件处理程序中调用 event.preventDefault() 或 event.stopPropagation() 是非常常见的需求。尽管我们可以在方法中轻松实现这点,但更好的方式是:方法只有纯粹的数据逻辑,而不是去处理 DOM 事件细节。

为了解决这个问题,Vue.js 为 v-on 提供了事件修饰符。之前提过,修饰符是由点开头的指令后缀来表示的。

.stop

.prevent

.capture

.self

.once

.passive

<!-- 阻止单击事件继续冒泡 -->
<a @click.stop="doThis"></a>

<!-- 提交事件不再重载页面 -->
<form @submit.prevent="onSubmit"></form>

<!-- 修饰符可以串联 -->
<a @click.stop.prevent="doThat"></a>

<!-- 只有修饰符 -->
<form @submit.prevent></form>

<!-- 添加事件监听器时使用事件捕获模式 -->
<!-- 即内部元素触发的事件先在此处理,然后才交由内部元素进行处理 -->
<div @click.capture="doThis">...</div>

<!-- 只当在 event.target 是当前元素自身时触发处理函数 -->
<!-- 即事件不是从内部元素触发的 -->
<div @click.self="doThat">...</div>

<!-- 点击事件将只会触发一次 -->
<a @click.once="doThis"></a>

<!-- 滚动事件的默认行为 (即滚动行为) 将会立即触发,   -->
<!-- 而不会等待 `onScroll` 完成,                    -->
<!-- 以防止其中包含 `event.preventDefault()` 的情况  -->
<div @scroll.passive="onScroll">...</div>

2、按键修饰符

在监听键盘事件时,我们经常需要检查特定的按键。Vue 允许为 v-on 或者 @ 在监听键盘事件时添加按键修饰符:

<!-- 只有在 `key` 是 `Enter` 时调用 `vm.submit()` -->
<input @keyup.enter="submit" />
你可以直接将 KeyboardEvent.key暴露的任意有效按键名转换为 kebab-case 来作为修饰符。
<input @keyup.page-down="onPageDown" />
在上述示例中,处理函数只会在 $event.key 等于 'PageDown' 时被调用。
按键别名
Vue 为最常用的键提供了别名:
.enter
.tab
.delete (捕获“删除”和“退格”键)
.esc
.space
.up
.down
.left
.right
3、系统修饰键
可以用如下修饰符来实现仅在按下相应按键时才触发鼠标或键盘事件的监听器。
.ctrl
.alt
.shift
.meta
<!-- Alt + Enter -->
<input @keyup.alt.enter="clear" />

<!-- Ctrl + Click -->
<div @click.ctrl="doSomething">Do something</div>
请注意修饰键与常规按键不同,在和 keyup 事件一起用时,事件触发时修饰键必须处于按下状态。换句话说,只有在按住 ctrl 的情况下释放其它按键,才能触发 keyup.ctrl。而单单释放 ctrl 也不会触发事件
4、.exact 修饰符
.exact 修饰符允许你控制由精确的系统修饰符组合触发的事件。

<!-- 即使 Alt 或 Shift 被一同按下时也会触发 -->
<button @click.ctrl="onClick">A</button>

<!-- 有且只有 Ctrl 被按下的时候才触发 -->
<button @click.ctrl.exact="onCtrlClick">A</button>

<!-- 没有任何系统修饰符被按下的时候才触发 -->
<button @click.exact="onClick">A</button>
5、鼠标按钮修饰符
.left
.right
.middle
这些修饰符会限制处理函数仅响应特定的鼠标按钮。
6、 v-model 修饰符
 v-model 有内置修饰符——.trim、.number 和 .lazy。

Vue.mixin 的使用场景和原理

在日常开发中,我们经常会遇到在不同组件中经常用到一些相同或者相似的代码,这些代码的功能相对独立,可以通过vue 的 mixin 功能抽离公共的业务逻辑,原理类似“对象的继承”,当组件初始化时会调用 mergeOptions 方法进行合并,采用策略模式针对不同的属性进行合并。当组件和混入对象含有相同名选项时,这些选项将以恰当的方式进行“合并”

基本用法

// 定义一个 mixin 对象
const myMixin = {
  created() {
    this.hello()
  },
  methods: {
    hello() {
      console.log('hello from mixin!')
    }
  }
}
// 定义一个使用此 mixin 对象的应用
const app = Vue.createApp({
  mixins: [myMixin]
})
app.mount('#mixins-basic') // => "hello from mixin!"

不足

在 Vue 2 中,mixin 是将部分组件逻辑抽象成可重用块的主要工具。但是,他们有几个问题:

Mixin 很容易发生冲突:因为每个 mixin 的 property 都被合并到同一个组件中,所以为了避免 property 名冲突,你仍然需要了解其他每个特性。

可重用性是有限的:我们不能向 mixin 传递任何参数来改变它的逻辑,这降低了它们在抽象逻辑方面的灵活性。

虚拟 DOM 的优缺点?

优点:

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

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

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

缺点:

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

虚拟 DOM 实现原理?

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

用 JavaScript 对象模拟真实 DOM 树,对真实 DOM 进行抽象;

diff 算法 — 比较两棵虚拟 DOM 树的差异;

pach 算法 — 将两个虚拟 DOM 对象的差异应用到真正的 DOM 树。

SEO相关

SEO汉译为搜索引擎优化。是一种方式:利用搜索引擎的规则提高网站在有关搜索引擎内的自然排名。目的是让其在行业内占据领先地位,获得品牌收益。很大程度上是网站经营者的一种商业行为,将自己或自己公司的排名前移。

说白了就是你的SEO做的越好,当别人搜索某个关键词时,你的网站在搜索结果中就排的越靠前

SEO优化就是人们在各浏览器通过搜索结果获得网站流量,提升网站排名的一种技术实现过程。普通的一个前端网站可以通过哪些地方来做优化呢?

  1. 合理的titledescriptionkeywords: title, description,keywords他们的权重逐渐减小。

1. title就是我们看到的网页标题

2. description为对该网页的简要描述

3.keywords的作用就是告诉搜索引擎,本网页中主要围绕着哪些方面的关键词展开

2、语义化的HTML代码,符合W3C规范

多使用语义化的HTML标签,什么叫语义化标签,说白了就是对号入座,不要什么地方都永远是div,span。HTML 5中提供了很多语义化的标签,比如<header></header>,<footer></footer>, <nav></nav> ,<aside></aside>,<section></section>

3、非装饰性图片必须加alt

<img>标签的alt 属性指定了替代文本,用于在图像无法显示时,代替图像显示在浏览器中的内容。对于非装饰性图片必须添加alt,非装饰性图片是指除了那些作为元素背景图的图片。alt可以增强内容相关性,提高关键词密度

4、友情链接

友链就是在你的网站和别人的网站上相互放上对方的网站超链接,通过点击链接可以跳到对方的网站上。友情链接是网站流量来源的根本,比如-种可以自动交换链接的友情链接网站(每来访一个IP,就会自动排到第一),这是一 -种创新的自助式友情链接互联网模式。

5、外链:外链就是指在别的网站导入自己网站的链接。导入链接对于网站优化来说是非常重要的-个过程。导入链接的质量(即

导入链接所在页面的权重)间接影响了我们的网站在搜索引擎中的权重。

6、向各大搜索引擎提交收录自己的站点:搜索引擎收录了你的网站后,会很大程度上提升网站的排名。

7、重要的内容放在前面搜索引擎抓取是自上而下进行的,把主要的关键性的内容放在前面,可以保证所抓取的内容更符合或代表网站的特征

8、其他: 1.少用iframe: iframe中的内容是不会被抓取到的

2.提高网站速度:这也是搜索引擎排序的一一个重要指标

3.流量:访问你的网站的人越多,排名也会越靠前

vue搭建的网站前后端分离不利于SEO,原因如下:

1、搜索引擎的基础爬虫原理就是抓取url,然后获取htm|源码并进行解析,而vue的页面是通过

数据绑定机制来渲染页面的,所以当爬虫的时候获取到的最先的并不是我们的数据,而是一个html的模型页面,所以说,用js来渲染数据对于seo而言并不友好

2、SEO的本质是一个服务器向另-个服务器请求数据,解析请求的内容。但一般来说搜索引擎是不会去执行请求到的js的,也就是说,如果一个vue的单页应用,html在服务端还没有渲染数据,在浏览器才把数据渲染出来,这样的话,搜索引擎请求到的htmI就是那个在服务端还没有渲染到数据的html,也就是空空如也的html,这样子很不利于我们的内容被搜索引擎给捕捉到。so, 服务端的渲染需要尽量在服务器发送到浏览器前,页面就需要是有数据的。首先,搜索引擎的实现是基于爬虫(Python) 原理,先抓取并解析你网站的URL地址,然后获取该网址的HTML源代码并解析。

而Vue js的页面数据渲染依赖于其数据绑定机制,井且Vue js语法是基于JavaScript。

可以理解为,Python获取解析的是网站的HTML模型页面(相当于静态的一个框架),而VUE的的页面数据展示是其工作过程的最终数据渲染页面(也就是VUE生命周期的Mounted阶段),所以说用JS来渲染数据对SEO不友好。

如何对VUE进行SEO优化?

现在主要采用的有以下四种方式:

1、页面预渲染prerender-spa-plugin:

2、SSR服务器渲染:服务端渲染就是尽量在服务器发送到浏览器前就将页面渲染到页面上。

3、静态化:需要借助Nuxt框架

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

各个方法优劣分析:

1、SSR服务器渲染

优势:1. 更好的SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面;

2.更快的内容到达时间(time-to-content),特别是对于缓慢的网络情况或运行缓慢的设备。

不足:1.对vue版本有要求,对服务器也有一定要求,需要支持nodejs环境。

2.一套代码两套执行环境,会引起各种问题,比如服务端没有window、document对象,处理方式是增加判断,如果是客户端才执行:

2、静态化:静态化是Nuxt.js打包的另一种方式,算是Nuxt.js的一个创新点,页面加载速度很快。

优势:1.纯静态文件,访问速度超快;

2.对比SSR,不涉及到服务器负载方面问题;

3.静态网页不宜遭到黑客攻击,安全性更高。

不足:如果动态路由参数多的话不适用。

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

注意: router中必须设置mode:"history"。

优势:改动小,引入个插件就完事;

不足:无法使用动态路由;

只适用少量页面的项目,页面多达几百个的情况下,打包会很很很慢;

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

这种解决方案其实是一种旁路机制,原理就是通过Nginx配置,判断访问的来源UA是否是爬虫访问,如果是则将搜索引擎的爬虫请求转发到一个nodeserver,再通过PhantomJS来解析完整的HTML,返回给爬虫。

优势:完全不用改动项目代码,按原本的SPA开发即可,对比开发SSR成本小不要太多;

不足:部署需要node服务器支持;

爬虫访问比网页访问要慢一些,因为定时要定时资源加载完成才返回给爬虫;

如果被恶意模拟百度爬虫大量循环爬取,会造成服务器负载方面问题,解决方法是判断访问的IP,是否是百度官方爬虫的IP。

简述 Vue 异步组件用法

在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。为了简化,Vue 允许你以一个工厂函数的方式定义你的组件,这个工厂函数会异步解析你的组件定义。Vue 只有在这个组件需要被渲染的时候才会触发该工厂函数,且会把结果缓存起来供未来重渲染

Vue 2.0 用法

const Async***ponent = () => ({
  // 需要加载的组件 (应该是一个 `Promise` 对象)
  ***ponent: import('./My***ponent.vue'),
  // 异步组件加载时使用的组件
  loading: Loading***ponent,
  // 加载失败时使用的组件
  error: Error***ponent,
  // 展示加载时组件的延时时间。默认值是 200 (毫秒)
  delay: 200,
  // 如果提供了超时时间且组件加载也超时了,
  // 则使用加载失败时使用的组件。默认值是:`Infinity`
  timeout: 3000
})

Vue 3.0 用法

在 Vue 3 中,由于函数式组件被定义为纯函数,因此异步组件需要通过将其包裹在新的 defineAsync***ponent 助手方法中来显式地定义。以下是对变化的总体概述:

新的 defineAsync***ponent 助手方法,用于显式地定义异步组件

import { defineAsync***ponent } from 'vue'
import Error***ponent from './***ponents/Error***ponent.vue'
import Loading***ponent from './***ponents/Loading***ponent.vue'

// 不带选项的异步组件
const asyncModal = defineAsync***ponent(() => import('./Modal.vue'))

// 带选项的异步组件
const asyncModalWithOptions = defineAsync***ponent({
  loader: () => import('./Modal.vue'),
  delay: 200,
  timeout: 3000,
  error***ponent: Error***ponent,
  loading***ponent: Loading***ponent
})

***ponent 选项被重命名为 loader

Loader 函数本身不再接收 resolve 和 reject 参数,且必须返回一个 Promise

// 2.x 版本

const oldAsync***ponent = (resolve, reject) => {
  /* ... */
}

// 3.x 版本

const async***ponent = defineAsync***ponent(
  () =>
    new Promise((resolve, reject) => {
      /* ... */
    })
)

注意:Vue Router 支持一个类似的机制来异步加载路由组件,也就是俗称的懒加载。尽管类似,但是这个功能和 Vue 所支持的异步组件是不同的。当用 Vue Router 配置路由组件时,你不应该使用 defineAsync***ponent。

Vue首页白屏问题解决???

加载js太慢了,打包需要优化下,另外用cdn加快请求;图片等资源进行压缩处理;用服务端渲染(SSP)。

Vue 的性能优化方向?

这里只列举针对 Vue 的性能优化,整个项目的性能优化是一个大工程。

  1. 对象层级不要过深,否则性能就会差。
  2. 不需要响应式的数据不要放在 data 中(可以使用 Object.freeze() 冻结数据)
  3. v-if 和 v-show 区分使用场景
  4. ***puted 和 watch 区分场景使用
  5. v-for 遍历必须加 key,key最好是id值,且避免同时使用 v-if
  6. 大数据列表和表格性能优化 - 虚拟列表 / 虚拟表格
  7. 防止内部泄露,组件销毁后把全局变量和时间销毁
  8. 图片懒加载
  9. 路由懒加载
  10. 异步路由
  11. 第三方插件的按需加载
  12. 适当采用 keep-alive 缓存组件
  13. 防抖、节流的运用
  14. 服务端渲染 SSR or 预渲染

你有对 Vue 项目进行哪些优化?

(1)代码层面的优化

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

***puted 和 watch 区分使用场景

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

长列表性能优化

事件的销毁

图片资源懒加载

路由懒加载

第三方插件的按需引入

优化无限列表性能

服务端渲染 SSR or 预渲染

(2)Webpack 层面的优化

Webpack 对图片进行压缩

减少 ES6 转为 ES5 的冗余代码

提取公共代码

模板预编译

提取组件的 CSS

优化 SourceMap

构建结果输出分析

Vue 项目的编译优化

(3)基础的 Web 技术的优化

开启 gzip 压缩

浏览器缓存

CDN 的使用

使用 Chrome Performance 查找性能瓶颈

对于即将到来的 vue3.0 特性你有什么了解的吗?

Vue 3.0 正走在发布的路上,Vue 3.0 的目标是让 Vue 核心变得更小、更快、更强大,因此 Vue 3.0 增加以下这些新特性:

(1)监测机制的改变

3.0 将带来基于代理 Proxy 的 observer 实现,提供全语言覆盖的反应性跟踪。这消除了 Vue 2 当中基于 Object.defineProperty 的实现所存在的很多限制:

只能监测属性,不能监测对象

检测属性的添加和删除;

检测数组索引和长度的变更;

支持 Map、Set、WeakMap 和 WeakSet。

新的 observer 还提供了以下特性:

用于创建 observable 的公开 API。这为中小规模场景提供了简单轻量级的跨组件状态管理解决方案。

默认采用惰性观察。在 2.x 中,不管反应式数据有多大,都会在启动时被观察到。如果你的数据集很大,这可能会在应用启动时带来明显的开销。在 3.x 中,只观察用于渲染应用程序最初可见部分的数据。

更精确的变更通知。在 2.x 中,通过 Vue.set 强制添加新属性将导致依赖于该对象的 watcher 收到变更通知。在 3.x 中,只有依赖于特定属性的 watcher 才会收到通知。

不可变的 observable:我们可以创建值的“不可变”版本(即使是嵌套属性),除非系统在内部暂时将其“解禁”。这个机制可用于冻结 prop 传递或 Vuex 状态树以外的变化。

更好的调试功能:我们可以使用新的 renderTracked 和 renderTriggered 钩子精确地跟踪组件在什么时候以及为什么重新渲染。

(2)模板

模板方面没有大的变更,只改了作用域插槽,2.x 的机制导致作用域插槽变了,父组件会重新渲染,而 3.0 把作用域插槽改成了函数的方式,这样只会影响子组件的重新渲染,提升了渲染的性能。

同时,对于 render 函数的方面,vue3.0 也会进行一系列更改来方便习惯直接使用 api 来生成 vdom 。

(3)对象式的组件声明方式

vue2.x 中的组件是通过声明的方式传入一系列 option,和 TypeScript 的结合需要通过一些装饰器的方式来做,虽然能实现功能,但是比较麻烦。

3.0 修改了组件的声明方式,改成了类式的写法,这样使得和 TypeScript 的结合变得很容易。

此外,vue 的源码也改用了 TypeScript 来写。其实当代码的功能复杂之后,必须有一个静态类型系统来做一些辅助管理。

现在 vue3.0 也全面改用 TypeScript 来重写了,更是使得对外暴露的 api 更容易结合 TypeScript。静态类型系统对于复杂代码的维护确实很有必要。

(4)其它方面的更改

vue3.0 的改变是全面的,上面只涉及到主要的 3 个方面,还有一些其他的更改:

支持自定义渲染器,从而使得 weex 可以通过自定义渲染器的方式来扩展,而不是直接 fork 源码来改的方式。

支持 Fragment(多个根节点)和 Protal(在 dom 其他部分渲染组建内容)组件,针对一些特殊的场景做了处理。

基于 treeshaking 优化,提供了更多的内置功能。

转载请说明出处内容投诉
CSS教程网 » 前端vue知识点梳理

发表评论

欢迎 访客 发表评论

一个令你着迷的主题!

查看演示 官网购买