Vue技巧大全

学习Vue你必须知道的

Vue2.0基础知识

1.说说你对vue的了解

(1)vue的发展历程

①文字版:【VueConf 2022】尤雨溪:Vue的进化历程
②视频版:VueConf 2022 所有演讲视频

(2)vue的优缺点

优点:渐进式,组件化,轻量级,虚拟dom,响应式,单页面路由,数据与视图分开
缺点:单页面不利于seo,不支持IE8以下,首屏加载时间长

渐进式框架理解?

渐进式: 通俗点讲就是,你想用啥你就用啥,咱也不强求你。你想用component就用,不用也行,你想用vuex就用,不用也可以

(3)Vue和JQuery的区别在哪?为什么放弃JQuery用Vue?

  1. jQuery是直接操作DOM,Vue不直接操作DOM,Vue的数据与视图是分开的,Vue只需要操作数据即可
  2. jQuery的操作DOM行为是频繁的,而Vue利用虚拟DOM的技术,大大提高了更新DOM时的性能
  3. Vue中不倡导直接操作DOM,开发者只需要把大部分精力放在数据层面上
  4. Vue集成的一些库,大大提高开发效率,比如Vuex,Router等

(4)Vue跟React的异同点?

相同点:
1.都有组件化思想
2.都有Virtual DOM(虚拟dom)
3.数据驱动视图
4.都支持服务器端渲染
5.都是单向数据流(父子组件之间,不建议子修改父传下来的数据)
6.都有支持native的方案:Vue的weex、React的React native
7.都有自己的构建工具:Vue的vue-cli、React的Create React App

不同点:
1.React的JSX,Vue的template。
2.数据变化的实现原理不同。React手动(setState),Vue自动(初始化已响应式处理,Object.defineProperty)
3.React单向绑定,Vue双向绑定。
4.diff算法不同。react主要使用diff队列保存需要更新哪些DOM,得到patch树,再统一操作批量更新DOM。Vue 使用双向指针,边对比,边更新DOM。
5.组件化通信的不同。react中我们通过使用回调函数来进行通信的,而Vue中子组件向父组件传递消息有两种方式:事件和回调函数。
6.React的Redux,Vue的Vuex。

参考链接
1.面试官:说说你对vue的理解?
2.vue与react的个人体会
3.React 与 Vue 框架的设计思路大 PK

2.单页应用SPA和多页应用MPA

单页应用(SPA)

(一)概念和原理

概念:第一次进入页面的时候会请求一个html文件,刷新清除一下。切换到其他组件,此时路径也相应变化,但是并没有新的html文件请求,页面内容也变化了。
原理:JS会感知到url的变化,通过这一点,可以用js动态的将当前页面的内容清除掉,然后将下一个页面的内容挂载到当前页面上,这个时候的路由不是后端来做了,而是前端来做,判断页面到底是显示哪个组件,清除不需要的,显示需要的组件。这种过程就是单页应用,每次跳转的时候不需要再请求html文件了。页面跳转->JS渲染

(二)优缺点

(1)优点

①页面切换快,用户体验好
页面每次切换跳转时,并不需要做html文件的请求,这样就节约了很多http发送时延,我们在切换页面的时候速度很快。

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

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

(2)缺点

①首屏时间慢,SEO差

单页应用的首屏时间慢,首屏时需要请求一次html,同时还要发送一次js请求,两次请求回来了,首屏才会展示出来。相对于多页应用,首屏时间慢。
SEO效果差,因为搜索引擎只认识html里的内容,不认识js的内容,而单页应用的内容都是靠js渲染生成出来的,搜索引擎不识别这部分内容,也就不会给一个好的排名,会导致单页应用做出来的网页在百度和谷歌上的排名差。

②前进后退路由管理

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

③有这些缺点,为什么还要使用Vue呢?

Vue还提供了一些其它的技术来解决这些缺点,比如说服务器端渲染技术(我是SSR),通过这些技术可以完美解决这些缺点,解决完这些问题,实际上单页面应用对于前端来说是非常完美的页面开发解决方案。

多页应用(MPA)

(一)原理

每一次页面跳转的时候,后台服务器都会给返回一个新的html文档,这种类型的网站也就是多页网站,也叫做多页应用。页面跳转->返回html

(二)优点和缺点

(1)优点

①多页应用的首屏时间快
首屏时间叫做页面首个屏幕的内容展现的时间,当我们访问页面的时候,服务器返回一个html,页面就会展示出来,这个过程只经历了一个HTTP请求,所以页面展示的速度非常快。

②搜索引擎优化效果好(SEO
搜索引擎在做网页排名的时候,要根据网页内容才能给网页权重,来进行网页的排名。搜索引擎是可以识别html内容的,而我们每个页面所有的内容都放在Html中,所以这种多页应用,seo排名效果好。

(2)缺点:

②页面切换慢

因为每次跳转都需要发出一个http请求,如果网络比较慢,在页面之间来回跳转时,就会发现明显的卡顿。

多页应用模式MPA单页应用模式SPA
应用构成由多个完整页面构成一个外壳页面和多个页面片段构成
跳转方式页面之间的跳转是从一个页面跳转到另一个页面页面片段之间的跳转是把一个页面片段删除或隐藏,加载另一个页面片段并显示出来。这是片段之间的模拟跳转,并没有开壳页面
跳转后公共资源是否重新加载
URL模式http://xxx/page1.html 和 http://xxx/page2.htmlhttp://xxx/shell.html#page1 和 http://xxx/shell.html#page2
用户体验页面间切换加载慢,不流畅,用户体验差,特别是在移动设备上页面片段间的切换快,用户体验好,包括在移动设备上
能否实现转场动画无法实现容易实现(手机app动效)
页面间传递数据依赖URLcookie或者localstorage,实现麻烦因为在一个页面内,页面间传递数据很容易实现(这里是我补充,父子之间传值,或vuexstorage之类)
搜索引擎优化(SEO)可以直接做需要单独方案做,有点麻烦
特别适用的范围需要对搜索引擎友好的网站对体验要求高的应用,特别是移动应用
开发难度低一些,框架选择容易高一些,需要专门的框架来降低这种模式的开发难度

参考阅读:
(1)说说你对SPA(单页应用)的理解?
(2)SPA(单页应用)首屏加载速度慢怎么解决?
(3)面试官:SSR解决了什么问题?有做过SSR吗?你是怎么做的?
(4)大势所趋:流式服务端渲染
(5)Vue SPA 首屏优化实战

3.使用过 Vue SSR 吗?说说 SSR?

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

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

(1)服务端渲染的优点

  • 更好的 SEO
    优势在于同步。搜索引擎爬虫是不会等待异步请求数据结束后再抓取信息的,如果 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 ) 下使用,请准备相应的服务器负载,并明智地采用缓存策略。

如果没有 SSR 开发经验的同学,可以参考本文作者的另一篇 SSR 的实践文章《Vue SSR 踩坑之旅》,里面 SSR 项目搭建以及附有项目源码。

1.服务端渲染SSR及实现原理
2.《Vue SSR 踩坑之旅

4.MVVM与MVC以及MVVM的优缺点 ★★★

(1)MVVM与MVC区别与联系

MVC:最早的架构模型就是MVC, 当时从前端到后台统一称之为MVC。前端的叫视图层,后台也有自己的数据库,用户操作界面获取数据会向后端发起请求,请求会被路由拦截到,这时它会转发给对应的控制器来处理,控制器会获取数据,最终将结果返回给前端,页面重新渲染,这种方向是单向的,而且针对于整个应用的架构。

但是我们发现前端越来越复杂,不再是以前的只是渲染页面,只是通过后端渲染的了,而是有了前端的单应用,我们就把视图层由前端这一层又进行了抽离,抽离出了MVVM,View就是我们的DOM层,数据就是我们的前端的静态数据,或者AJAX请请求回来的数据,以前是我们直接手动操作数据,将数据放到页面上,我们需要手动操作DOM,很麻烦,现在就有了Vue的框架,它是典型的MVVM

主要成员:

  • M - Model:模型,是应用程序中用于处理应用程序数据逻辑的部分,通常模型对象负责在数据库中存取数据
  • V - View: 视图,是应用程序中处理数据显示的部分,通常视图是依据模型数据创建的。
  • C - Controller: 控制器, 是应用程序中处理用户交互的部分,通常控制器负责从视图读取数据,控制用户输入,并向模型发送数据。

简要流程

  • View 接受用户交互请求
  • View 将请求转交给Controller处理
  • Controller 操作Model进行数据更新保存
  • 数据更新保存之后,Model会通知View更新
  • View 更新变化数据使用户得到反馈

MVVM :

传统的前端会将数据手动渲染到页面上,比如jQueryMVVM 模式不需要用户收到操作dom 元素,将数据绑定到 viewModel 层上,会自动将数据渲染到页面中。并且在Vue中,数据都是响应式的,数据变化会通过viewModel层驱动视图进行更新,视图更改会通知 viewModel层进行更新数据,形成一个数据和视图双向绑定的模式,ViewModel 就是我们 MVVM 模式中的桥梁。

主要成员

  • M - Model,Model 代表数据模型,也可以在 Model 中定义数据修改和操作的业务逻辑
  • V - View,View 代表 UI 组件,它负责将数据模型转化为 UI 展现出来
  • VM - ViewModel,ViewModel 监听模型数据的改变和控制视图行为、处理用户交互,简单理解就是一个同步 View 和 Model 的对象,连接 Model 和 View

简略流程

  • View 接收用户交互请求
  • View 将请求转交给ViewModel
  • ViewModel 操作Model数据更新
  • Model 更新完数据,通知ViewModel数据发生变化
  • ViewModel 更新View数据

两者区别和联系

  • ViewModel 替换了Controller在UI层之下
  • ViewModel 向View暴露了它所需要的数据和指令
  • ViewModel 接收来自Model的数据

概括起来就是,MVVM由MVC发展而来,通过在Model之上而在View之下增加一个非视觉的组件将来自Model的数据映射到View中。

(2) MVVM的优缺点?


优点:

①分离视图(View)和模型(Model),降低代码耦合,提高视图或者逻辑的重用性: 比如视图(View)可以独立于 Model变化和修改,⼀个ViewModel可以绑定不同的"View"上,当View变化的时候Model不可以不变,当Model变化 的时候View也可以不变。你可以把⼀些视图逻辑放在⼀个ViewModel里面,让很多view重用这段视图逻辑 。

②提高可测试性: ViewModel的存在可以帮助开发者更好地编写测试代码 。

③自动更新dom: 利用双向绑定,数据更新后视图⾃动更新,让开发者从繁琐的手动dom中解放。

缺点:

①Bug很难被调试: 因为使用双向绑定的模式,当你看到界面异常了,有可能是你View的代码有Bug,也可能是Model 的代码有问题。数据绑定使得⼀个位置的Bug被快速传递到别的位置,要定位原始出问题的地⽅就变得不那么容易 了。另外,数据绑定的声明是指令式地写在View的模版当中的,这些内容是没办法去打断点debug的 。

②一个大的模块中model也会很大,虽然使⽤⽅便了也很容易保证了数据的⼀致性,当时长期持有不释放内存就造成了花费更多的内存

③对于大型的图形应用程序,视图状态较多,ViewModel的构建和维护的成本都会比较高

(3)为什么官方要说 Vue 没有完全遵循 MVVM 思想呢?

首先,Vue 并没有完全遵循 MVVM 的思想 这一点官网自己也有说明。
在这里插入图片描述
严格的 MVVM 要求 View 不能和 Model 直接通信,而 Vue 提供了$refs 这个属性,让 Model 可以直接操作 View,违反了这一规定,所以说 Vue 没有完全遵循 MVVM。

(4)实现MVVM步骤:

1、实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者。

2、实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数。

3、实现一个Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图。

4、mvvm入口函数,整合以上三者。

参考阅读】:说说你对双向绑定的理解?

5.谈谈你对 keep-alive 的了解?

1.vue路由缓存的几种实现方式小结
①博客1
②博客2
2.Vue源码解析,keep-alive是如何实现缓存的?
3.Vue / keep-alive
理解:

  • keep-alive其实是一个抽象组件,可以实现组件的缓存,当组件切换时不会对当前组件进行卸载,常用的2个属性include / exclude ,2个生命周期 activated , deactivated
  • LRU算法
  • 字节二面:让写一个LFU缓存策略算法,懵了
    特点:最近最久未使用法,超过最大限度会从前面删除
    在这里插入图片描述

原理:

core/components/keep-alive.js

export default {
name: 'keep-alive',
abstract: true, // 抽象组件
props: {
  include: patternTypes,//对哪些进行缓存
  exclude: patternTypes,//不想对哪些缓存
  max: [String, Number]//最多缓存多少个
},
created () {
  this.cache = Object.create(null) // 创建缓存列表
  this.keys = [] // 创建缓存组件的key列表
},
destroyed () { // keep-alive销毁时 会清空所有的缓存和key
  for (const key in this.cache) { // 循环销毁
     pruneCacheEntry(this.cache, key, this.keys)
}
},
mounted () { // 会监控include 和 include属性有没有变  变了的话进行动态添加和删除组件的缓存处理
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
},
render () {
const slot = this.$slots.default // 会默认拿当前组件的插槽
const vnode: VNode = getFirstComponentChild(slot) // (如果放多个组件)只缓存第一个组件
const componentOptions: ?VNodeComponentOptions = vnode &&
vnode.componentOptions
if (componentOptions) {
// check pattern
const name: ?string = getComponentName(componentOptions) // 取出组件的名字
const { include, exclude } = this
if ( // 判断是否缓存
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}
const { cache, keys } = this
const key: ?string = vnode.key == null
    // same constructor may get registered as different local components
// so cid alone is not enough (#3269)
? componentOptions.Ctor.cid + (componentOptions.tag ?
`::${componentOptions.tag}` : '')
: vnode.key // 如果组件没key 就自己通过 组件的标签和key和cid 拼接一个key【自己算出来的】
if (cache[key]) {//看看这个东西有没有缓存过
vnode.componentInstance = cache[key].componentInstance // 如果缓存,直接拿到组件实例
// make current key freshest
remove(keys, key) // 删除当前的 [b,c,d,e,a] // LRU 最近最久未使用法
keys.push(key) // 并将key放到后面[b,a]
} else {
cache[key] = vnode // 如果没有缓存过,就缓存vnode
keys.push(key) // 将key 存入
// prune oldest entry
if (this.max && keys.length > parseInt(this.max)) { // 缓存的太多超过了max
就需要删除掉
pruneCacheEntry(cache, keys[0], keys, this._vnode) // 要删除第0个 但是现
在渲染的就是第0}
}
vnode.data.keepAlive = true // 并且标准keep-alive下的组件是一个缓存组件
}
return vnode || (slot && slot[0]) // 返回当前的虚拟节点
}
}
app-shell app壳

6.插槽

(1). 什么是作用域插槽?(插槽和作用域插槽的区别)

1.官网
2. 面试官:说说你对slot的理解?slot使用场景有哪些?
3. Vue3+TS系统学习七 - 组件的插槽使用

理解:

1.插槽:
在创建父组件的过程中,会把xxx先渲染出来,先留好了,会把a和b渲染成虚拟节点先存起来,先明确这个渲染过程在执行app组件外渲染的时候已经渲染好了,并不是在app组件里面渲染的。这样渲染完后,会自动分类,等会调用组件的时候,可能会写这样的逻辑,这时候就把刚才渲染好的虚拟节点替换掉,a替换a,b替换b。这里先明确,普通插槽的渲染位置在父组件里边,而不是在app组件里,app在父组件里,div渲染在app的父组件里。作用域插槽唯一的区别在于它的渲染过程在app组件里面,区别就是作用域的问题。

<app><div slot="a">xxxx</div><div slot="b">xxxx</div></app>

   slot name="a"
   slot name="b"
  • 创建组件虚拟节点时,会将组件的儿子的虚拟节点保存起来。当初始化组件时,通过插槽属性将儿 子进行分类 {a:[vnode],b[vnode]}

  • 渲染组件时会拿对应的slot属性的节点进行替换操作。(插槽的作用域为父组件

2.作用域插槽:

  • 作用域插槽在解析的时候,不会作为组件的孩子节点。会解析成函数,当子组件渲染时,会调用此函数进行渲染。(插槽的作用域为子组件

原理:

在这里插入图片描述

如果写了如下所示组件,编译后的结果是注释里所示,_c=>_createElement,它会看当前组件有三个儿子,第一个儿子是div插槽,而且把里面的内容存起来了,_v("node")创建一个文本节点.由此可知,在编译调render方法的时候就已经把这些节点渲染好了,等一会我们要渲染的时候,有一个下划线_t=>renderSlot,就会把虚拟节点给换过来,普通插槽就是一个替换的过程,将父组件渲染好的替换到自己身上,创建的过程是在父组件进行渲染的。

1.插槽:

const VueTemplateCompiler = require('vue-template-compiler');
let ele = VueTemplateCompiler.compile(`
    <my-component>
        <div slot="header">node</div>
        <div>react</div>
        <div slot="footer">vue</div>
   </my-component>
`)
/**
with(this) {
     return _c('my-component',
         [_c('div', {
              attrs: {
         "slot": "header"
         },
       slot: "header"
     }, [_v("node")] //createTextVNode _创建一个文本节点src=>core=>instance=>render-helpers=>index.js
  ), _v(" "), _c('div', [_v("react")]), _v(" "), _c('div', {
    attrs: {
   "slot": "footer"
    },
    slot: "footer"
}, [_v("vue")])])
}
*/
const VueTemplateCompiler = require('vue-template-compiler');
let ele = VueTemplateCompiler.compile(`
<div>
<slot name="header"></slot>
<slot name="footer"></slot>
<slot></slot>
</div>
`);
/**
with(this) {
//_t("header")=>_v("node")  _t('footer')=>_t(_v("vue")])]
return _c('div', [_t("header"), _v(" "), _t('footer'), _v(" "),
_t("default")], 2)
}
**/
// _t定义在 core/instance/render-helpers/index.js

作用域插槽:
什么叫作用域插槽,它渲染出来的结果和普通插槽不一样?
作用域插槽编译出来不是childen,而是一个属性叫scopedSlots,而且会把孩子变成一个函数,这个函数不调就不会渲染这个插槽,所以在初始化的时候并不会渲染这个子节点或者孩子节点,那什么时候渲染的?当我们写div让slot执行的时候,“a”=“1”,“b”=“1”.找footer调用的是_t方法(核心逻辑不一样),找到footer之后会调用这个函数,并且把"a"=“1”,“b”="1"传过去,这时候才会这个节点渲染完成之后替换掉这个东西, 作用域插槽渲染的过程在组件的内部。 渲染的作用域不同,普通插槽是父组件,作用域插槽在父组件。

let ele = VueTemplateCompiler.compile(`
<app>
<div slot-scope="msg" slot="footer">{{msg.a}}</div>
</app>
`);
/**
with(this) {
   return _c('app', {
       scopedSlots: _u([{ // 作用域插槽的内容会被渲染成一个函数
           key: "footer",
           fn: function (msg) {
                return _c('div', {}, [_v(_s(msg.a))])
            }
      }])
   })
  }
}
*/
const VueTemplateCompiler = require('vue-template-compiler');
VueTemplateCompiler.compile(`
<div>
<slot name="footer" a="1" b="2"></slot>
</div>
`);
/**
with(this) {
return _c('div', [_t("footer", null, {
"a": "1",
"b": "2"
})], 2)
}
**/

(2).作用域插槽实现 UI 和业务逻辑的分离

【场景】:很多时候,我们想复用一个组件的业务逻辑,但是不想使用该组件的 UI,那么可以使用作用域插槽实现 UI 和业务逻辑的分离。
实现思路】作用域插槽大致的思路是将 DOM 结构交给调用方去决定,组件内部只关注业务逻辑,最后将数据和事件等通过 :item =“item” 的方式传递给父组件去处理和调用,实现 UI 和业务逻辑的分离。再结合渲染函数,就可以实现无渲染组件的效果。具体可以看我的另一篇文章 【Vue 进阶】从 slot 到无渲染组件
其中父组件调用的时候可以类似这样,其中 #row 是 v-slot:row 的缩写

<!-- 父组件 -->
<template>
  ...
  <my-table>
    <template #row="{ item }">
      /* some content here. You can freely use 'item' here */
    </template>
  </my-table>
  ...
</template>
<!-- 子组件 -->
<span>
  <!-- 使用类似 v-bind:item="item",将子组件中的事件或者data传递给父组件-->
  <slot v-bind:item="item">
    {{ item.lastName }}
  </slot>
</span>

需要注意,Vue 2.6 之前使用的是 slot 和 slot-scope,后面使用的是 v-slot

7.修饰符与指令

(一).修饰符

(1)Vue常用的修饰符有哪些?有什么应用场景?
(2).stop阻止事件冒泡的3种场景
(3)【建议收藏】 Vue 32+ 修饰符,你掌握几种啦?
(4)v-model与.sync修饰符的区别

(二).模板语法常用内置指令使用场景及其原理

指令是指Vue提供的以“v-”前缀的特性,如v-text/v-html/v-for/v-show/v-if/v-else/v-cloak/v-bind/v-on/v-model/v-slot…
在Vue的模板语法中我们学习过各种各样的指令:v-show、v-for、v-model等等,除了使用这些指令之外,Vue也允许我们来自定义自己的指令。

  • 注意:在Vue中,代码的复用和抽象主要还是通过组件;
  • 通常在某些情况下,你需要对DOM元素进行底层操作,这个时候就会用到自定义指令;

自定义指令分为两种:

  • 自定义局部指令:组件中通过 directives 选项,只能在当前组件中使用;
  • 自定义全局指令:app的 directive 方法,可以在任意组件中被使用

当指令中表达式的内容发生变化时,会连带影响DOM内容发生变化。Vue.directive全局API可以创建自定义指令,并获取全局指令,除了自定义指令,Vue还内置了一些开发过程中常用的指令,如v-if、v-for等。在Vue模板解析时,会将指令解析到AST,使用AST生成字符串的过程中实现指令的功能。
在这里插入图片描述
在解析模板时,会将节点上的指令解析出来并添加到AST的directives属性中,directives将数据发送到VNode中,在虚拟DOM进行页面渲染时,会触发某些钩子函数,当钩子函数被触发后,就说明指令已生效。

(1)Vue中v-if和v-show的区别★★★

参考阅读:Vue中的v-show和v-if怎么理解?

理解:

  • v-if 如果条件不成立不会渲染当前指令所在节点的 dom 元素,是完整的重新创建和销毁过程。 v-if 是 真正 的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。
  • v-show 就简单得多, 不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 的 display 属性进行切换。
  • 两个本质区别是一个在于一个会编译成一个指令[v-show],另一个在编译阶段就变成了三元运算符了。
  • 所以,v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景;v-show 则适用于需要非常频繁切换条件的场景。

原理:

v-if

// vue-template-compiler是原生的包,会把我们的,起到编译作用,会把我们当前的模板编译成render方法
const VueTemplateCompiler = require('vue-template-compiler');

let r1 = VueTemplateCompiler.compile(`<div v-if="true"><span v-for="i in 3">hello</span></div>`);

/** with(this) {    
        return (true) ? _c('div', _l((3), 
        function (i) {       
            return _c('span', [_v("hello")])  
       }), 0) :
       _e()
       } */
①_e是vue源码里特殊的方法,src->core->instace->index.js  _e就是创建空的虚拟节点
②如果条件不满足就走_e(),所以内部指定不会执行,DOM也就不会渲染.

v-show

const VueTemplateCompiler = require('vue-template-compiler'); 
let r2 = VueTemplateCompiler.compile(`<div v-show="true"></div>`);
/** with(this) {    
return _c('div', {   
    directives: [{          
    name: "show",        
    rawName: "v-show",     
    value: (true),        
    expression: "true"   
    }] 
    })
    } */
<div v-show="true"></div>编译出来没有任何东西,它里面只有一个指令directives叫v-show,不会编译成特殊的语法,只会编译出来一个属性叫指令,在运行的时候会匹配到这个属性进行处理,[platforms=>web=>runtime=>directives=>show.js]
// v-show 操作的是样式  定义在platforms/web/runtime/directives/show.js 
bind (el: any, { value }: VNodeDirective, vnode: VNodeWithData) {  //指令的编写
    vnode = locateNode(vnode)  
    const transition = vnode.data && vnode.data.transition    
    const originalDisplay = el.__vOriginalDisplay =    
          el.style.display === 'none' ? '' : el.style.display  
    if (value && transition) {     
        vnode.data.show = true    
        enter(vnode, () => {      
            el.style.display = originalDisplay    
        })   
    } else {  
        el.style.display = value ? originalDisplay : 'none' //如果是false就none没了,如果不是false,原来是什么就是什么originalDisplay
    } 
(2)为什么v-for和v-if不能连用?★★★

问题分析
当 v-for 和 v-if 处于同一个节点时,v-for 的优先级比 v-if 更高,这意味着 v-if 将分别重复运行于每个 v-for 循环中。如果要遍历的数组很大,而真正要展示的数据很少时,这将造成很大的性能浪费。

const VueTemplateCompiler = require('vue-template-compiler');//vue-loader
let r1 = VueTemplateCompiler.compile(`<div v-if="false" v-for="i in 3">hello</div>`); /** with(this) {   
return _l((3), function (i) {   
return (false) ? _c('div',
[_v("hello")]) : _e()  
}) } */
console.log(r1.render);
//里边是先循环,然后把里边的每个div都加上一个条件判断,要是有100个,会把判断编译到每个对象上,所以性能会很低.

解决办法

方法一: 如果避免出现这种情况,则在外层嵌套template(页面渲染不生成dom节点),在这一层进行v-if判断,然后在内部进行v-for循环。

<template v-if="isShow">
    <p v-for="item in items">
</template>

方法二:如果条件出现在循环内部,可通过计算属性computed提前过滤掉那些不需要显示的项

computed: {
    items: function() {
      return this.list.filter(function (item) {
        return item.isShow
      })
    }
}

前端实现分页
情景一:调后端接口实现分页
在这里插入图片描述

//结构部分
<div class="doublebtn">
   <button class="topYe" @click="beforePage">上一页</button>
   <button class="downYe" @click="nextPage">下一页</button>
   <span>{{ currentPage }},{{ totalPage }}</span>
</div>
//数据部分
data() {
    return {
      currentPage: "",  //当前页
      totalPage: "",   //总页数  
      pageNum:1 ,
      totalCount: "",
    };
  },
//请求部分
getChargeRecord(){
       let params = {
        pageNum: this.pageNum,
        cardType:'01',
        cardNum:JSON.parse(getKey("cardMessage")).jzkh
    };
      chargeRecord(params).then(
        (res) => {
        console.log(res);
        this.tableData=res.content.list;
        this.totalCount = res.content.total;
        this.currentPage = res.content.pageNum;
        this.totalPage = res.content.pages;
       
      }
      )
    },
 //上一页和下一页
 //上一页
    beforePage() {
       this.pageNum -= 1;
       if (this.pageNum < 1) {
        this.pageNum = 1;
      }
       this.getChargeRecord();
      
    },
    //下一页
    nextPage() {
       if (this.pageNum * 7 <= this.totalCount) {
        this.pageNum += 1;

      }
       this.getChargeRecord();
    },

接口
在这里插入图片描述
前端自行实现分页
slice方法在这里插入图片描述

//allData为全部数据,tableData是目前表格绑定的数据
     this.tableData = this.allData.slice(
        (this.page - 1) * this.size,
        this.page * this.size
      );
      this.total=this.allData.length

下面是详细代码:在这里插入图片描述

在这里插入图片描述
方法二:splice方法
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
列表搜索

在正式的项目开发中,前端一般负责实现一些没有分页的列表的搜索功能。
搜索一般分为精确搜索和模糊搜索,搜索也叫过滤。
一种是模糊搜索,一般用过滤器来实现:

const a = [1, 2, 3, 4, 5]
const result = a.filter((item) => {
  return item === 3
})
console.log('result', result)

但是,如果是精确搜索,则需要使用ES6中的find

const a = [1,2,3,4,5];
const result = a.find( 
  item =>{
    return item === 3
  }
)

参考阅读】:
1.为什么Vue中的v-if和v-for不建议一起用?
2.前端实现分页功能的做法
3.用好 Vue 中 v-for 循环的 7 种方法

(3)v-for中为什么要用key 并且避免使用index作为标识(图解)★★★

在这里插入图片描述
key来给每一个节点作为唯一标识,key的作用主要是为了高效的更新虚拟DOM。 (key 是给每一个 vnode的唯一 id,依靠 key,我们的 diff 操作可以更准确、更快速)。

更准确 : 因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确,如果不加 key,会导致之前节点的状态被保留下来,会产生一系列的 bug。

更快速 : key 的唯一性可以被 Map 数据结构充分利用,相比于遍历查找的时间复杂度 O(n),Map 的时间复杂度仅仅为 O(1),源码如下:

function createKeyToOldIdx(children, beginIdx, endIdx) {
  let i, key;
  const map = {};
  for (i = beginIdx; i <= endIdx; ++i) {
    key = children[i].key;
    if (isDef(key)) map[key] = i;
  }
  return map;
} 

【注意】:

  • v-for key避免使用index作为标识,因为这样并不会带来任何性能优化,尽量不要用index作为标识,而去采用数据中的唯一值,如id 等字段。

  • 没有key的情况下,会更快。感谢评论区老哥fengyangyang的提醒:引用官网的话:key 的特殊 attribute 主要用在 Vue 的虚拟 DOM 算法,在新旧 nodes 对比时辨识 VNodes。如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。而使用 key 时,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素。

(4)v-on可以监听多个事件吗?★★★

可以

<button v-on="{mouseenter: onEnter, mouseleave: onLeave}">鼠标进来1</button>`
(5) ①v-bind和v-model的区别,②v-model中的实现原理及如何自定义v-model ③v-model与.sync修饰符的区别★★★
  • v-bind用来绑定数据和属性以及表达式
  • v-model使用在表单中,实现双向数据绑定的,在表单元素外不起使用。

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

  • text和 textarea元素使用value属性和input事件
  • checkbox和 radio使用checked属性和change事件
  • select字段将 value作为prop并将change作用事件
<input v-model="something">
本质上相当于这样
<input v-bind:value="something" v-on:input="something = $event.target.value">
其实就是通过绑定一个something属性,通过监听input事件,当用户改变输入框数据的时候,
通过事件传递过来的事件对象中的target找到事件源,value属性表示事件源的值,从而实现双向数据绑定的效果

理解: 组件的 v-model 是 value+input方法 的语法糖

<el-checkbox :value="" @input=""></el-checkbox>
<el-checkbox v-model="check"></el-checkbox>

如何实现自定义组件的 v-model?

自定义组件的v-model使用prop值为value和input事件。若是radio/checkbox类型,需要使用model来解决原生 DOM 使用的是 checked 属性 和 change 事件,如下所示。

// 父组件
<template>
  <base-checkbox v-model="baseCheck" />
</template>

// 子组件
<template>
  <input type="checkbox" :checked="checked" @change="$emit('change', $event.target.checked)" />
</template>
<script>
export default {
  model: {
    prop: 'checked',
    event: 'change'
  },
  prop: {
    checked: Boolean
  }
}
</script>

原理:

  • 会将组件的 v-model 默认转化成value+input
const VueTemplateCompiler = require('vue-template-compiler');
const ele = VueTemplateCompiler.compile('<el-checkbox v-model="check"></elcheckbox>');
// with(this) {
// return _c('el-checkbox', {
// model: {
// value: (check),
// callback: function ($$v) {
// check = $$v
// },
// expression: "check"
// }
// })
// }

core/vdom/create-component.js line:155

function transformModel (options, data: any) {
const prop = (options.model && options.model.prop) || 'value'
const event = (options.model && options.model.event) || 'input'
;(data.attrs || (data.attrs = {}))[prop] = data.model.value
const on = data.on || (data.on = {})
const existing = on[event]
const callback = data.model.callback
if (isDef(existing)) {
if (
Array.isArray(existing)
? existing.indexOf(callback) === -1
: existing !== callback
) {
on[event] = [callback].concat(existing)
}
} else {
on[event] = callback
}
}

  • 原生的 v-model ,会根据标签的不同生成不同的事件和属性
const VueTemplateCompiler = require('vue-template-compiler');
const ele = VueTemplateCompiler.compile('<input v-model="value"/>');
/**
with(this) {
return _c('input', {
directives: [{
name: "model",
rawName: "v-model",
value: (value),
expression: "value"
}],
domProps: {
"value": (value)
},
on: {
"input": function ($event) {
if ($event.target.composing) return;
value = $event.target.value
}
}
})
}
*/

编译时:不同的标签解析出的内容不一样 platforms/web/compiler/directives/model.js

if (el.component) {
genComponentModel(el, value, modifiers)
// component v-model doesn't need extra runtime
return false
} else if (tag === 'select') {
genSelect(el, value, modifiers)
} else if (tag === 'input' && type === 'checkbox') {
genCheckboxModel(el, value, modifiers)
} else if (tag === 'input' && type === 'radio') {
genRadioModel(el, value, modifiers)
} else if (tag === 'input' || tag === 'textarea') {
genDefaultModel(el, value, modifiers)
} else if (!config.isReservedTag(tag)) {
genComponentModel(el, value, modifiers)
// component v-model doesn't need extra runtime
return false
}

运行时:会对元素处理一些关于输入法的问题 platforms/web/runtime/directives/model.js

inserted (el, binding, vnode, oldVnode) {
if (vnode.tag === 'select') {
// #6903
if (oldVnode.elm && !oldVnode.elm._vOptions) {
mergeVNodeHook(vnode, 'postpatch', () => {
directive.componentUpdated(el, binding, vnode)
})
} else {
setSelected(el, binding, vnode.context)
}
el._vOptions = [].map.call(el.options, getValue)
} else if (vnode.tag === 'textarea' || isTextInputType(el.type)) {
el._vModifiers = binding.modifiers
if (!binding.modifiers.lazy) {
el.addEventListener('compositionstart', onCompositionStart)
el.addEventListener('compositionend', onCompositionEnd)
// Safari < 10.2 & UIWebView doesn't fire compositionend when
// switching focus before confirming composition choice
// this also fixes the issue where some browsers e.g. iOS Chrome
// fires "change" instead of "input" on autocomplete.
el.addEventListener('change', onCompositionEnd)
/* istanbul ignore if */
if (isIE9) {
el.vmodel = true
}
    }
}
}

③v-model与.sync修饰符的区别
v-model与.sync修饰符的区别

(6)Vuev-html会导致哪些问题?

理解:

  • 可能会导致 xss 攻击 v-html

  • 会替换掉标签内部的子元素

原理:

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])
}
}
(7)v-cloak 解决页面闪烁问题★

很多时候,我们页面模板中的数据是异步获取的,在网络不好的情况下,渲染页面的时候会出现页面闪烁的效果,影响用户体验,v-cloak 指令保持在元素上直到关联实例结束编译,利用它的特性,结合 CSS 的规则 [v-cloak] { display: none } 一起使用就可以隐藏掉未编译好的 Mustache 标签,直到实例准备完毕。

// template 中
<div class="#app" v-cloak>
    <p>{{value.name}}</p>
</div>

// css 中
[v-cloak] {
    display: none;
}

【注意】

需要注意,虽然解决了闪烁的问题,但这段时间内如果什么都不处理的话,会直接白屏,这并不是我们想要的效果,我们应该加一个 loading 或者骨架屏的效果,提升用户体验

(8)v-once 和 v-pre 提升性能

v-once
用于指定元素或者组件只渲染一次:

  • 当数据发生变化时,元素或者组件以及其所有的子节点单将视为静态内容并且跳过;
  • 该指令可以用于性能优化;
<!-- 单个元素 -->
<span v-once>This will never change: {{msg}}</span>
<!-- 有子元素 -->
<div v-once>
  <h1>comment</h1>
  <p>{{msg}}</p>
</div>
<!-- 组件 -->
<my-component v-once :comment="msg"></my-component>
<!-- `v-for` 指令-->
<ul>
  <li v-for="i in list" v-once>{{i}}</li>
</ul>

v-pre
我们知道 Vue 的性能优化很大部分在编译这一块,Vue 源码就有类似标记静态节点的操作,以在 patch 的过程中跳过编译,从而提升性能。另外,Vue 提供了 v-pre 给我们去决定要不要跳过这个元素和它的子元素的编译过程。可以用来显示原始 Mustache 标签。跳过大量没有指令的节点会加快编译。

<span v-pre>{{ this will not be compiled }}</span>   显示的是{{ this will not be compiled }}
<span v-pre>{{msg}}</span>     即使data里面定义了msg这里仍然是显示的{{msg}}
(9)v-if、v-else-if/v-else
  • 在render函数里就是三元表达式
  • 可以配合temlate来使用
(10) 如何自定义指令?自定义指令有哪些钩子?★★★

实现原理
在应用程序中,指令的处理逻辑分别监听了create函数、update函数以及destory函数,具体实现如下:

export default {
 create: updateDirectives,
 update: updateDirectives,
 destory: function unbindDirectives (vnode){
  updateDirectives(vnode, emptyNode)
 }
}

钩子函数被触发后,会执行updateDirectives函数,代码如下:

function updateDirectives(oldVnode, vnode){
 if (oldVnode.data.directives || vnode.data.directives) {
  _update(oldVnode, vnode)
 }
}

在该函数中,不论是否存在旧虚拟节点,只要其中存在directives,就会执行_update函数,_update函数代码如下:

function _update(oldVnode, vnode) {
 const isCreate = oldVnode === emptyNode
 const isDestory = vnode === emptyNode
 const oldDirs = normalizeDirectives(oldVnode.data.directives, oldVnode.context)
 const newDirs = normalizeDirectives(vnode.data.directives, vnode.context)
 const dirsWithInsert = []
 const dirsWithPostpatch = []

 let key, oldDir, dir
 for (key in newDirs) {
  oldDir = oldDirs[key]
  dir = newDirs[key]
  if (!oldDir) { //新指令触发bind
   callHook(dir, 'bind', vnode, oldVnode)
   if (dir.def && dir.def.inserted) {
    dirsWithInsert.push(dir)
   }
  } else { //指令已存在触发update
   dir.oldValue = oldDir.value
   callHook(dir, 'update', vnode, oldVnode)
   if (dir.def && dir.def.componentUpdated) {
    dirsWithPostpatch.push(dir)
   }
  }
 }

 if (dirsWithInsert.length) {
  const callInsert = () => {
   for (let i = 0; i < dirsWithInsert.length; i++) {
    callHook(dirsWithInsert[i], 'inserted', vnode, oldVnode)
   }
  }
  if (isCreate) {
   mergeVNodeHook(vnode, 'insert', callInsert)
  } else {
   callInsert()
  }
 }

 if (dirsWithPostpatch.length) {
  mergeVNodeHook(vnode, 'postpatch', () => {
   for(let i = 0; i < dirsWithPostpatch.length; i++) {
    callHook(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode)
   }
  })
 }

 if (!isCreate) {
  for(key in oldDirs) {
   if (!newDirs[key]) {
    callHook(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestory)
   }
  }
 }
}

isCreate:判断该虚拟节点是否是一个新建的节点。
isDistory:判断是否删除一个旧虚拟节点。
oldDirs:旧的指令集合,oldVnode中保存的指令。
newDirs:新的指令集合,vnode中保存的指令。
dirsWithInsert:触发inserted指令钩子函数的指令列表。
dirsWithPostpatch:触发componentUpdated钩子函数的指令列表。
通过normalizeDirectives函数将模板中使用的指令从用户注册的自定义指令集合中取出来的结果如下:

{
 v-customize: {
  def: {inserted: f},
  modifiers: {},
  name: "customize",
  rawName: "v-customize"
 }
}

自定义指令的代码为:

Vue.directives('customize', {
 inserted: function (el) {
  el.customize()
 }
})

虚拟DOM在对比和渲染时,会根据不同情景触发不同的钩子函数,当使用虚拟节点创建一个新的实际节点时,会触发create钩子函数,当一个DOM节点插入到父节点时,会触发insert钩子函数。
callHook函数执行钩子函数的方式如下:

function callHook(dir, hook, vnode, oldVnode, isDestory) {
 const fn = dir.def && dir.def[hook]
 if (fn) {
  try {
   fn(vnode.elm, dir, vnode, oldVnode, isDestory)
  } catch (e) {
   handleError(e, vnode.context, `directive ${dir.name} ${hook} hook`)
  }
 }
}

callHook函数的参数意义分别为:
dir:指令对象。
hook:将要触发的钩子函数名。
vnode:新的虚拟节点。
oldVnode:旧的虚拟节点。
isDestory:判断是否删除一个旧虚拟节点
虚拟DOM在渲染时会触发的所有钩子函数及其触发机制如下:
在这里插入图片描述
需要注意的是,remove函数是只有一个元素从其父元素中移除时才会触发,如果该元素是被移除元素的子元素,则不会触发remove函数。

1.这15个Vue自定义指令,让你的项目开发爽到爆
2.分享8个非常实用的Vue自定义指令
3.超实用:Vue 自定义指令合集
4. 3k字,从0开始了解16个Vue指令到手动封装自定义指令
5.移动端双击操作

移动端双击事件
vue移动端项目中有一个需求是图片双击放大,在网上查了好多,都是说用@dblclick="Ondblclick",结果发现在pc端,安卓微信端确实有效,但是ios和chrome device中事件不触发。今天突然发现了vue的手势插件:vue-touch,可以自定义双击事件,在ios和chrome亲测有效。
注意: vue1.0可以直接使用vue-touch,但是vue2.0要安装:vue-touch@next。具体步骤如下:
1.下载vue-touch:npm install vue-touch@next
2.在mian.js中导入:import VueTouch from 'vue-touch'
3.自定义双击事件:
VueTouch.registerCustomEvent('doubletap', {
  type: 'tap',
  taps: 2
})
4.在vue中引用:Vue.use(VueTouch, {name: 'v-touch'})
5.在组件(页面)中使用:<v-touch v-on:doubletap="Ondouble"></v-touch>
6.js中:methods: {
Ondouble(){
console.log("双击")
}
}

8.为何Vue采用异步渲染

(1)理解vue是组件级更新

因为如果不采用异步更新,那么每次更新数据都会对当前组件进行重新渲染.所以为了性能考虑。 Vue 会在本轮数据更新后,再去异步更新视图!

每个数据都会收集依赖,我们写一个页面,页面里可能有几百个数据,这几百数据改了都会渲染这一个组件, vue是组件级更新, 当前组件的数据变了就会更新这个组件。问题:组件的数据非常多,这样就可能可能频繁更新this.a =1 this.b = 2,这样一更改就重新渲染,这样我改几次它就要重新渲染几次,性能肯定不高,这时候就要采用异步更新。就是多个数据同时被更改了,比如先改this.a =1 this.b = 2,他们对应渲染的watcher(同一个watcher),把相同的watcher过滤掉,之后等他们都改完数据之后再去更改数据,所以这也是性能的考虑,为了防止一改数据就去更新视图,所以做了一个异步渲染。

(2)源码分析

update () {    /* istanbul ignore else */    
    if (this.lazy) {     
        this.dirty = true  
    } else if (this.sync) {   
        this.run()   
    } else {      
        queueWatcher(this); // 当数据发生变化时会将watcher放到一个队列中批量更新  
    } 
} export function queueWatcher (watcher: Watcher) {  
    const id = watcher.id // 会对相同的watcher进行过滤 
    if (has[id] == null) { 
        has[id] = true    
        if (!flushing) {   
            queue.push(watcher)   
        } else {     
            let i = queue.length - 1     
            while (i > index && queue[i].id > watcher.id) {    
                i-    
               }   
                queue.splice(i + 1, 0, watcher) 
            }    // queue the flush  
            if (!waiting) {    
                waiting = true
      if (process.env.NODE_ENV !== 'production' && !config.async) {  
          flushSchedulerQueue()      
          return    
           }      
            nextTick(flushSchedulerQueue) // 调用nextTick方法 批量的进行更新  
            }
        }
    }

在这里插入图片描述

看源码,其实核心的方法是nextTick()。当数据变化了,就会调用dep.notify方法通知watcher进行更新,watcher会调用它的update()方法更新,更新的时候不是立即让watcher执行,而是放到一个队列里,这个方法叫queueWatcher,它会把watcher进行过滤,如果相同的watcher它只存一个,然后最后再异步刷新这个Watcher。

dep中的notify,dep里有一个通知的方法notify,它会通知存储的依赖去更新。先明确dep里存储的wacher,每个属性都有一个depdep会对应自己的watcher,当然也可能是多个watcher,先说一个,我们一通知wacher,它会去循环,【这里是一个发布订阅模式】它会调用watcher里的update方法,通过queueWatcher把当前watcher放到队列里去,到这个方法里后先过滤watcher,每个watcher都有自己的id,相同的watcher就不再放到队列里去了,防止多次更新。如果没有,告诉它有了,把它放到queue里,如果已经有了就进不来了。最后会调用nextTick做一个清空队列的操作。nextTick可以认为是一个异步操作,刷新队列是非常简单的,它里面默认会先去触发before()方法,即更新之前的方法,在这里真正执行watcher,同样watcher执行完后,页面就渲染完成了,渲染完成了会调用一些钩子。会把多个watcher放到这个queue,然后异步刷新这个queue,核心方法是queue。

(3)问题小结

①怎么判断是不是相同的watcher

会有一个uid,每次new的时候都会让uid++,所以每次相同的watcher id都是一样的。

②当前的组件的数据如果变化了,它会更新当前的组件,更新组件的时候子组件更新,它会传。

③可以认为是渲染节流,当某个时刻不触发,等本轮循环结束再触发。

④什么时候走nextTick?只要watcher没放进来,没有的话,就会走。如果相同的watcher就不会走了,走完后会异步的刷新,这个方法只会走1次。

9.nextTick实现原理

1.vue.nextTick()方法的使用详解(简单明了)
2.Vue中的$nextTick怎么理解?
3.你必须知道的 webpack 插件原理分析

(1)理解:(宏任务和微任务) 异步方法

nextTick 方法主要是使用了宏任务微任务,定义了一个异步方法,多次调用 nextTick 会将方法存入 队列中,通过这个异步方法清空当前队列。 所以这个 nextTick 方法就是异步方法 。在下次DOM更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的DOM。

(2)源码分析

源码实现:Promise > MutationObserver > setImmediate > setTimeout

let timerFunc  // 会定义一个异步方法 
if (typeof Promise !== 'undefined' && isNative(Promise)) {  // promise 
    const p = Promise.resolve()  
    timerFunc = () => {   
        p.then(flushCallbacks)   
        if (isIOS) setTimeout(noop)  
    }  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (// MutationObserver
    isNative(MutationObserver) ||  MutationObserver.toString() === '[object MutationObserverConstructor]' )) {  
    let counter = 1  
    const observer = new MutationObserver(flushCallbacks) 
    const textNode = document.createTextNode(String(counter))          observer.observe(textNode, {   
        characterData: true  
    })  
    timerFunc = () => {    
        counter = (counter + 1) % 2   
       textNode.data = String(counter) 
    }  isUsingMicroTask = true } 
    else if (typeof setImmediate !== 'undefined' ) { // setImmediate 
        timerFunc = () => {   
            setImmediate(flushCallbacks) 
        } 
    } else {  timerFunc = () => {   // setTimeout   
        setTimeout(flushCallbacks, 0) 
    } 
   } // nextTick实现
export function nextTick (cb?: Function, ctx?: Object) { 
    let _resolve  
    callbacks.push(() => {    
        if (cb) {
             try {       
                 cb.call(ctx)  
             } catch (e) {    
                 handleError(e, ctx, 'nextTick')   
             }    
        } else if (_resolve) {    
            _resolve(ctx)    
        }  
    })  
    if (!pending) {  
        pending = true  
        timerFunc()
   }
}

在这里插入图片描述

主要用了事件,事件循环的机制,我们要先掌握两个概念,宏任务和微任务,解释一下,再我们执行的过程中,微任务高于我们宏任务先执行,比如说promise会优先setTimeout,promise其实就是浏览器帮我们实行的一个微任务,(宏任务和微任务)都叫异步方法

当我们调用nextTick方法的时候,我们会放一个flushSchedulerQueue,同样用户也会调用nextTick方法,我们经常会使用nextTick保证当前视图完成,为什么可以保证呢?我们会把我的这个nextTick回调放到flushSchedulerQueue后面,所以等待到视图更完新后再去取值,这时候获取的是最新Dom,默认内部会调nextTick,会把flushSchedulerQueue传过来,传过来之后把它存到数组里,并且去执行,这里面还有错误捕获。同样用户也会调nextTick,这时候会把用户的cb,也存到同一个数组里callbacks。当前没有pending的时候就会调用timeFunc(),也就是说多次调用nextTick()它只会执行一次,因为执行完一次pending就改为true了,等到代码都OK了,会调用timeFunc()这个函数,timeFunc()其实就是一个异步方法,它会先判断当前浏览器是否支持promise,如果支持,它就会把timeFunc()里面包了promise,把flushcallback方法放到then中,相当于异步执行flushCallbacks

flushCallbacks方法里就让我们当前传的方法拷贝一份依次去执行,用promise里标识的是微任务。如果不支持promise,判断如果不是IE且支持MutationObserver并且是原生的,就使用MutationObserver这个方法,这个方法也是微任务,先定义一个变量counter=1,之后创建一个文本节点textNode,还new了一个内置类,把刚才的flushcallbacks()方法放进去了,这时候会调用Observe.observe方法,意思是它的原生API可以帮我们观测一个节点,后面给的参数表示,如果当前给的数据变化了,它就会异步执行flushcallbacks(),同时这里面定义了timerFunc,过一会就把counter加+1模2,这时候当前文本的数据就更新了,数据一改就执行flushcallbacks(),它也是异步执行的,如果MutationObserve也不认,就会再换一种方法setImmediate,也是原生的方法,默认ie下用,而且高版本的谷歌也是支持的,可以直接来用,性能高于setTimeout,setTImeout也是异步方法,最后再不行的话就直接上去了。先明确,nextTick里面多次调用它会维持一个数组,之后会把数组里的方法依次执行,这样用户就可以在视图更新之后获取到真实的Dom节点。

10.Class 与 Style 如何动态绑定?

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

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

data: {
  isActive: true,
  hasError: false
}
  • 数组语法:

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

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

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

  • 对象语法:

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

data: {
  activeColor: 'red',
  fontSize: 30
}
  • 数组语法:

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

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

12.Vue中的scoped的实现原理以及scoped穿透的用法(字节)★★★

(1)PostCSS又是什么东西?

官网说:“PostCSS,一个使用 JavaScript 来处理CSS的框架”。这句话高度概括了 PostCSS 的作用,但是太抽象了。按我理解,PostCSS 主要做了三件事:

parse:把 CSS 文件的字符串解析成抽象语法树(抽象语法树)的框架,解析过程中会检查 CSS 语法是否正确,不正确会给出错误提示。
runPlugin: 执行插件函数。PostCSS 本身不处理任何具体任务,它提供了以特定属性或者规则命名的事件。有特定功能的插件(如 autoprefixer、CSS Modules)会注册事件监听器。PostCSS 会在这个阶段,重新扫描 AST,执行注册的监听器函数。
generate: 插件对 AST 处理后,PostCSS 把处理过的 AST 对象转成 CSS string。
在这里插入图片描述
参考阅读
零基础理解 PostCSS 的主流程

(2)Vue中的scoped的实现原理

1.在Vue文件中的style标签上有一个特殊的属性,scoped。当一个style标签拥有scoped属性时候,它的css样式只能用于当前的Vue组件,可以使组件的样式不相互污染。如果一个项目的所有style标签都加上了scoped属性,相当于实现了样式的模块化。

<style scoped>
    .example{
        color:red;
    }
</style>
<template>
    <div>scoped测试案例</div>
</template>

转译后

.example[data-v-5558831a] {
   color: red;
}
<template>
    <div class="example" data-v-5558831a>scoped测试案例</div>
</template>

PostCSS给一个组件中的所有dom添加了一个独一无二的动态属性,给css选择器额外添加一个对应的属性选择器,来选择组件中的dom,这种做法使得样式只作用于含有该属性的dom元素(组件内部的dom)。
Vue中的scoped的实现原理以及scoped穿透的用法

(3)scoped穿透的用法

scoped看起来很好用,当时在Vue项目中,当我们引入第三方组件库时(如使用vue-awesome-swiper实现移动端轮播),需要在局部组件中修改第三方组件库的样式,而又不想去除scoped属性造成组件之间的样式覆盖。这时我们可以通过特殊的方式穿透scoped。
stylus的样式穿透 使用>>>

外层 >>> 第三方组件
     样式
.wrapper >>> .swiper-pagination-bullet-active
 background: #fff

sass和less的样式穿透 使用/deep/

外层 /deep/ 第三方组件 {
    样式
}
.wrapper /deep/ .swiper-pagination-bullet-active{
  background: #fff;
}

如何优雅地覆盖组件库样式?

13.虚拟 DOM优缺点、实现原理、diff算法?

(1)真实DOM和虚拟DOM渲染过程

真实DOM渲染过程
在这里插入图片描述

虚拟DOM的渲染过程
在这里插入图片描述

(2)为什么使用虚拟 dom?(优缺点)

优点:

  • 无需手动操作 DOM: 我们不再需要手动去操作 DOM,只需要写好 View-Model 的代码逻辑,框架会根据虚拟 DOM 和 数据双向绑定,帮我们以可预期的方式更新视图,极大提高我们的开发效率;
  • 保证性能下限: 框架的虚拟 DOM 需要适配任何上层 API 可能产生的操作,它的一些 DOM 操作的实现必须是普适的,所以它的性能并不是最优的;但是比起粗暴的 DOM 操作性能要好很多,因此框架的虚拟 DOM 至少可以保证在你不需要手动优化的情况下,依然可以提供还不错的性能,即保证性能的下限;
  • 跨平台: 虚拟 DOM 本质上是 JavaScript 对象,而 DOM 与平台强相关,相比之下虚拟 DOM 可以进行更方便地跨平台操作,包括你可以将VNode节点渲染成任意你想要的节点,例如服务器渲染(SSR)、weex 开发、渲染在canvas、WebGL、Native(iOS、Android)上,并且Vue3允许你开发属于自己的渲染器(renderer),在其他的平台上渲染。

缺点:

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

(3)实现原理及其提高性能原因?

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

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

提高性能原因:虚拟dom相当于在js和真实dom中间加了一个缓存,利用dom diff算法避免了没有必要的dom操作,从而提高性能。

下面用vnode来描述一个DOM结构

  • 虚拟节点就是用一个对象来描述真实的 dom 元素.

    function $createElement(tag,data,...children){  
        let key = data.key;    
        delete data.key;   
        children = children.map(child=>{   
            if(typeof child === 'object'){       
                return child     
            }else{        
                    return vnode(undefined,undefined,undefined,undefined,child)        } 
        }) 
        return vnode(tag,data,key,children); 
        } 
        function vnode(tag,data,key,children,text){   
            return {     
                tag, // 表示的是当前的标签名   
                data, // 表示的是当前标签上的属性      
                key, // 唯一表示用户可能传递       
                children,       
                text    
            } 
        }
    //对象就能描述DOM结构
    let r = _C('div',{id:'container},_c('p',{},'hello),'zf')
    console.log(r)
    代码里会先判断是不是对象,如果是对象,它又会递归再次调用这个方法,如果不是对象,会再通过一个方法vnode把它转换成对象.再本例中,r转会转化成对象,这个对象就可以描述真实的DOM元素,就叫虚拟dom.它可以描述真实的DOM
    

    例子:和ast语法树很像,但是那个是从语法的角度分析的,我不是直接把Dom元素转换成对象,而是通过方法.

    <div id = "container"><p></p></div>
        
    let obj = {
        tag:'div',
        data:{
            id:'container'
        }
        children:[
        {
        tag:'p',
        data:{},
        chiledren:[]
    }
        ]
    }
    //方法
    render(){
       return _c(div,{id:'container'},_c('p',{}))
    }
    

[问题小结]

①不让放外层还想让v-for和v-if连用,如何处理? 用计算属性.

    computed:{
    arr(){
    return xxx.fulter()
      }
    }

②会将template转化成AST树=>codegen=>转化成render函数 =>内部调用的就是_c方法 =>虚拟Dom

结论: template=>虚拟dom[编译时]

ast是统一的库吗?可以自己写,像vue里借鉴的是JQuery之父写出出自己的AST语法树增加一些功能,比如说编译指令,遍历修饰符,遍历v-bind\v-for.

④数据的变化是虚拟dom的变化,到时候虚拟dom一变会触发对应的watcher进行重新渲染,这时候会做一些diff.

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

①.diff算法的时间复杂度

vue中的diff`做了哪些优化?

两个树的完全的 diff 算法是一个时间复杂度默认为 O(n3) , Vue 进行了优化·O(n3) 复杂度的问题转换成 O(n) 复杂度的问题(只比较同级不考虑跨级问题) , 在前端当中, 你很少会跨越层级地移动Dom元素。 所 以 Virtual Dom只会对同一个层级的元素进行对比。

和React中的diff算法相比有什么优势?

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

Vue3.x的diff算法比Vue2.0相比有什么发展?

Vue3.x借鉴了 ivi算法和 inferno算法,在创建VNode时就确定其类型,以及在mount/patch的过程中采用位运算来判断一个VNode的类型,在这个基础之上再配合核心的Diff算法,使得性能上较Vue2.x有了提升(实际的实现可以结合Vue3.x源码看),该算法中还运用了动态规划的思想求解最长递归子序列。
(看到这你还会发现,框架内无处不蕴藏着数据结构和算法的魅力)

②.diff算法过程原理及其源码分析

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

  • 先同级比较,再比较子节点

  • 先判断一方有子节点一方没子节点的情况

如果新的一方有,就把新的一方全部插到老的节点里,如果老的里面有,新的里面没有,直接把老的节点删掉就可以了 。

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

  • 递归比较子节点
    在这里插入图片描述
    源码分析

core/vdom/patch.js

const oldCh = oldVnode.children // 老的儿子
const ch = vnode.children // 新的儿子
if (isUndef(vnode.text)) {
    if (isDef(oldCh) && isDef(ch)) {
      // 比较孩子
    if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue,
removeOnly)
} else if (isDef(ch)) { // 新的儿子有 老的没有
    if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
    addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) { // 如果老的有新的没有 就删除
    removeVnodes(oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) { // 老的有文本 新的没文本
    nodeOps.setTextContent(elm, '') // 将老的清空
   }
} else if (oldVnode.text !== vnode.text) { // 文本不相同替换
nodeOps.setTextContent(elm, vnode.text)
}

function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue,
removeOnly) {
    let oldStartIdx = 0
    let 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, idxInOld, vnodeToMove, refElm
    
// removeOnly is a special flag used only by <transition-group>
// to ensure removed elements stay in correct relative positions
// during leaving transitions
const canMove = !removeOnly

if (process.env.NODE_ENV !== 'production') {
 checkDuplicateKeys(newCh)
}
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if (isUndef(oldStartVnode)) {
     oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
    } else if (isUndef(oldEndVnode)) {
     oldEndVnode = oldCh[--oldEndIdx]
   } else if (sameVnode(oldStartVnode, newStartVnode)) {
    patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh,
newStartIdx)
    oldStartVnode = oldCh[++oldStartIdx]
    newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
    patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh,
newEndIdx)
    oldEndVnode = oldCh[--oldEndIdx]
    newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
     patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh,
newEndIdx)
    canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm,
nodeOps.nextSibling(oldEndVnode.elm))
    oldStartVnode = oldCh[++oldStartIdx]
    newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
   patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh,
newStartIdx)
    canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm,
oldStartVnode.elm)
     oldEndVnode = oldCh[--oldEndIdx]
     newStartVnode = newCh[++newStartIdx]
} else {
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh,
oldStartIdx, oldEndIdx)
    idxInOld = isDef(newStartVnode.key)
    ? oldKeyToIdx[newStartVnode.key]
    : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
    if (isUndef(idxInOld)) { // New element
       createElm(newStartVnode, insertedVnodeQueue, parentElm,
oldStartVnode.elm, false, newCh, newStartIdx)
} else {
      vnodeToMove = oldCh[idxInOld]
      if (sameVnode(vnodeToMove, newStartVnode)) {
          patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh,
newStartIdx)
       oldCh[idxInOld] = undefined
       canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm,
       oldStartVnode.elm)
} else {
// same key but different element. treat as new element
       createElm(newStartVnode, insertedVnodeQueue, parentElm,
   oldStartVnode.elm, false, newCh, newStartIdx)
  }
}
   newStartVnode = newCh[++newStartIdx]
  }
}
if (oldStartIdx > oldEndIdx) {
   refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
   addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx,
 insertedVnodeQueue)
  } else if (newStartIdx > newEndIdx) {
  removeVnodes(oldCh, oldStartIdx, oldEndIdx)
 }
}

​ 方法patchVnode比较的是虚拟节点不是真实节点,就是两个树都是对象,他会比较对象.如果两个节点一样,

会复用老的真实元素,会把老的元素赋给新的,说明他们是同一个元素.复用之后该比较儿子了,比较儿子的话,会拿出老的儿子和新的儿子做比较,最简单的就是两个儿子都有的情况下,它会比较他们的孩子,如果只用新的里面有儿子,他会把新增的儿子插到老的dom中.如果老的儿子,新的没有.直接把老的儿子删除即可.diff算法核心是比较两个儿子都有的情况下,它是最复杂的.vue中实现比较的方式是双指针,即代码中会拿到老的开始的索引,结束的索引.新的开始的索引,结束的索引.四个指针.vue中会对这个比较过程做一些优化.它会比较新的和老的第一个儿子一不一样,如果一样会移动前面的指针,会依次往后移动,知道移动到E,新的里多了一个E,直接把E插进去就好了,做了一个我们DOM元素经常往列列表里新增元素的操作,这样不必改变原有dom,只需要新增即可.还有一种可能,我们用户不是后面插入的,而是在前面插入的,那这样开始就从后面比.这时候动的就是尾指针,最后比到,同样插进入就好了,直接把新增的节点塞进去.还有一种是以前是ABCD,现在是DABC,把这个尾移到前面去,这时候,头和头尾和尾都不相等.这时候会把当前的头比尾.直接把D移动到前面去.这个指针完成后,会把当前的指针向前移动一位.D的指针移动到A的位置,同理,如果发现你只是做了个移动操作,vue同样会只做移动操作.同样可以把头移动到尾上.它会采用新的开头和老的开头比,新的结尾和老的结尾比.新的结尾和老的开头比,新的开头和老的结尾比这四种优化策略.如果比对得到,直接把元素移过去.最后一种情况,老的叫ABCD,新的叫CDME,这时候我们就知道了写元素的时候给每一个元素加key,拿C往老的里面找,发现有C,就把C移动到当前老指针的最前面,这时候变成了CABD,C完事就把D往过移.发现D也有就把D也插到老元素当前指针的前面.比较完D接着往后比,比到M,发现里面没有,直接把M插到开头前面去,比到E发现也没有,也插到开头前面去.最后把老的多余的删除掉。

参考阅读
1.深入理解 Vue 与 React 中的 diff 算法原理
2.一文搞定Diff算法
3.分析diff算法与虚拟dom(理解现代前端框架)

14.调试 template及template模板编译原理

HTML内容模板()元素是一种用于保存客户端内容机制,该内容在加载页面时不会呈现,但随后可以(原文为 may be)在运行时使用JavaScript实例化。

将模板视为一个可存储在文档中以便后续使用的内容片段。虽然解析器在加载页面时确实会处理元素的内容,但这样做只是为了确保这些内容有效;但元素内容不会被渲染。
MDN template

(1)调试 template

很多时候,我们会遇到 template 模板中变量报错的问题,这个时候,我们很想通过 console.log 打印到控制台,看它的值是什么。

// 这里最好是判断一下,只有在测试环境中才使用
// main.js
Vue.prototype.$log = window.console.log;

// 组件内部
<div>{{$log(info)}}</div>

在这里插入图片描述
在线Demo:Demo

(2)template模板编译

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

①生成AST树

首先解析模版,生成AST语法树(一种用JavaScript对象的形式来描述整个模板)。使用大量的正则表达式对模板进行解析,遇到标签、文本的时候都会执行对应的钩子进行相关处理。
②优化
Vue的数据是响应式的,但其实模板中并不是所有的数据都是响应式的。有一些数据首次渲染后就不会再变化,对应的DOM也不会变化。那么优化过程就是深度遍历AST树,按照相关条件对树节点进行标记。这些被标记的节点(静态节点)我们就可以跳过对它们的比对,对运行时的模板起到很大的优化作用。
③codegen
编译的最后一步是将优化后的AST树转换为可执行的代码。

/*怎么将模板转换成render函数*/
function baseCompile (  
   template: string,  
   options: CompilerOptions ) {  
   const ast = parse(template.trim(), options) // 1.将模板转化成ast抽象语法树 
   if (options.optimize !== false) {           // 2.优化树:标记哪些树是静态节点        
       optimize(ast, options)  
   }  
   const code = generate(ast, options)         // 3.将ast生成代码 
   return {    
   ast,   
   render: code.render,   
   staticRenderFns: code.staticRenderFns
   } 
   })
/*怎么编译把div变成一个render函数*/
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`; 
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;
const startTagOpen = new RegExp(`^<${qnameCapture}`); // 标签开头的正则 捕获的内容是 标签名 
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); // 匹配标签结尾的  </div> const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+| ([^\s"'=<>`]+)))?/; // 匹配属性的 
const startTagClose = /^\s*(\/?)>/; // 匹配标签结束的  > 
let root;
let currentParent;
let stack = [] 
function createASTElement(tagName,attrs){  
    return {       
        tag:tagName,     
        type:1,        
        children:[],    
        attrs,     
        parent:null 
    }
} function start(tagName,attrs){   
    let element = createASTElement(tagName,attrs); 
    if(!root){       
        root = element;   
    }    currentParent = element;    
    stack.push(element);
} function chars(text){   
    currentParent.children.push({       
        type:3,      
        text   
    }) 
} function end(tagName){   
    const element = stack[stack.length-1];   
    stack.length --;  
    currentParent = stack[stack.length-1];   
    if(currentParent){        
        element.parent = currentParent;   
        currentParent.children.push(element)  
    }
}
  function parseHTML(html){ 
      while(html){      
          let textEnd = html.indexOf('<');    
          if(textEnd == 0){            
              const startTagMatch = parseStartTag();  
              if(startTagMatch){          
                  start(startTagMatch.tagName,startTagMatch.attrs);  
                  continue;           
              }         
              const endTagMatch = html.match(endTag); 
              if(endTagMatch){             
                  advance(endTagMatch[0].length);       
                  end(endTagMatch[1])         
              }      
          }    
          let text;        
          if(textEnd >=0 ){           
              text = html.substring(0,textEnd)  
          }     
          if(text){          
              advance(text.length);          
              chars(text);       
          }   
      }   
      function advance(n) { 
          html = html.substring(n);  
      }   
      function parseStartTag(){    
          const start = html.match(startTagOpen);     
          if(start){        
              const match = {             
                  tagName:start[1],           
                  attrs:[]          
              }
} 
          function start(tagName,attrs){  
              let element = createASTElement(tagName,attrs);   
              if(!root){       
                  root = element;  
              }  
              currentParent = element;  
              stack.push(element); 
          } 
          function chars(text){   
              currentParent.children.push({  
                  type:3,       
                  text   
              }) 
          }
          function end(tagName){   
              const element = stack[stack.length-1]; 
              stack.length --;    
              currentParent = stack[stack.length-1];  
              if(currentParent){      
                  element.parent = currentParent;  
                  currentParent.children.push(element)  
              } 
          } function parseHTML(html){   
              while(html){       
                  let textEnd = html.indexOf('<'); 
                  if(textEnd == 0){         
                      const startTagMatch = parseStartTag();   
                      if(startTagMatch){               
                          start(startTagMatch.tagName,startTagMatch.attrs); 
                          continue;           
                      }          
                      const endTagMatch = html.match(endTag);   
                      if(endTagMatch){           
                          advance(endTagMatch[0].length);   
                          end(endTagMatch[1])      
                      }
                  }      
                  let text;      
                  if(textEnd >=0 ){        
                      text = html.substring(0,textEnd)      
                  }       
                  if(text){         
                      advance(text.length);    
                      chars(text);     
                  }    }  
              function advance(n) {   
                  html = html.substring(n);    
              }    function parseStartTag(){     
                  const start = html.match(startTagOpen); 
                  if(start){         
                      const match = {               
                          tagName:start[1],           
                          attrs:[]          
                      }
                      advance(start[0].length);  
                      let attr,end     
   while(!(end = html.match(startTagClose)) && (attr=html.match(attribute))){       
                          advance(attr[0].length);  
                          match.attrs.push({name:attr[1],value:attr[3]
                                           })           
   }            
                      if(end){           
                          advance(end[0].length);   
                          return match           
                      }     
                  }  
              } 
          } 
          // 生成语法树
          parseHTML(`<div id="container"><p>hello<span>zf</span></p></div>`);
          function gen(node){   
              if(node.type == 1){   
                  return generate(node);   
              }else{        
                  return `_v(${JSON.stringify(node.text)})` 
              } } function genChildren(el){    
                  const children = el.children;  
                  if(el.children){        
                      return `[${children.map(c=>gen(c)).join(',')}]`    
                  }else{   
                      return false; 
                  } } 
          function genProps(attrs){  
              let str = ''; 
              for(let i = 0; i < attrs.length;i++){     
                  let attr = attrs[i];     
                  str+= `${attr.name}:${attr.value},`;   
              }    return `{attrs:{${str.slice(0,-1)}}}`
          } function generate(el){ 
              let children = genChildren(el); 
              let code = `_c('${el.tag}'${   
              el.attrs.length? `,${genProps(el.attrs)}`:''  
              }${        children? `,${children}`:''   
              })`;    return code; }
          // 根据语法树生成新的代码 
          let code = generate(root);
          let render = `with(this){return ${code}}`;
          
            // 包装成函数
          let renderFn = new Function(render); //模板引擎实现原理
          console.log(renderFn.toString());

[虚拟DOM]:用一个对象来描述DOM元素,
[ast]:把js语法用对象来描述出来
[模板引擎实现原理]:用with包起来,然后new,with会不安全,但可以解决作用域的问题

15.在 Vue 中使用 JSX和render函数

(1).来聊聊Vue中使用Render函数和JSX
(2).【Vue 进阶】手把手教你在 Vue 中使用 JSX
(3).一起玩转 Vue 中的 JSX:让你一次性掌握它的特性!
JSX 是一种 Javascript 的语法扩展,JSX = Javascript + XML,即在 Javascript 里面写 XML,因为 JSX 的这个特性,所以他即具备了 Javascript 的灵活性,同时又兼具 html 的语义化和直观性。
有时候,我们使用渲染函数(render function)来抽象组件,而渲染函数有时候写起来是非常痛苦的,这个时候我们可以在渲染函数中使用 JSX 简化我们的代码。在 Vue 中使用 JSX,需要使用 Babel 插件,它可以让我们回到更接近于模板的语法上,详情可以看我之前总结的一篇文章【Vue 进阶】手把手教你在 Vue 中使用 JSX

render() {
  {/* 指令 */}
  {/* v-model */}
  <div><input vModel={this.newTodoText} /></div>
  {/* v-model 以及修饰符 */}
  <div><input vModel_trim={this.tirmData} /></div>
  {/* v-on 监听事件 */}
  <div><input vOn:input={this.inputText} /></div>
  {/* v-on 监听事件以及修饰符 */}
  <div><input vOn:click_stop_prevent={this.inputText} /></div>
  {/* v-html */}
  <p domPropsInnerHTML={html} />
}

【推荐阅读】
1.聊聊Vue中使用Render函数和JSX

16.请说一下响应式数据的原理(双向数据绑定)★★★

Vue 响应式原理模拟
面试官:你可以手写 Vue2 的响应式原理吗?
高级前端开发者必会的34道Vue面试题系列(二)
美团面试官:你可以手写 Vue3 的响应式原理吗?

1.简述:

采用数据劫持+发布订阅模式
vue 初始化时,通过递归遍历整个data,用Object.defineProperty()来劫持各个属性的setter和getter方法,给每个属性创建一个订阅中心,在get方法中添加订阅者到订阅中心,在set方法中通知订阅中心,继而通知每个订阅者。
(vue 初始化时,会用 Object.defineProperty 给data中每一个属性添加getter和setter,同时创建dep和watcher进行依赖收集与派发更新,最后通过diff算法对比新老vnode差异,通过patch即时更新DOM。)

简易图解:
在这里插入图片描述

2.详述Vue 在初始化数据时,会获取一个data,data内部会默认把对象进行遍历,使用Object.defineProperty 重新定义所有属 性,并且使数据的获取和设置都增加一个拦截的功能,同时增加一些自己的逻辑,这个逻辑就是依赖收集。当页面取到对应属性时,会进行依赖收集(收集当前组件的watcher) 如果属性发生变化会通知相关依赖进行更新操作。

比如说我们取数据的时候,我们可以收集一些功能,等一会儿,数据变化了,告诉收集的这些依赖去更新,我收集的东西就叫watcher,watcher有很多种,渲染的时候对数据进行取值,取值的时候就把当前渲染的过程存起来,对应到这个数据上,等会更新数据的时候告诉watcher去更新,这样就实现了响应式数据原理。

详细版本:
在这里插入图片描述

源码图解

源码分析Vue初始化的时候,会调用一个方法,这个方法叫initData,它会拿到当前用户传入的数据,对数据进行观测 。通过new Observer,创建一个观测点,如果我们的数据是一个对象类型非数组的话,就会调用this.walk(value),内部会把我们的数据进行遍历,同样当前对象的值还是一个对象的话,会递归观测,同样会调用之前Observer方法。用defineReactive定义响应式。采用Object.defineProperty重新定义,用户取值的时候,会调用get方法,调get方法的时候会对当前的依赖收集,等会数据变了,会通知watcher更新数据,这时候在我们set方法里数据一更新进行判断,如果当前的值和新的不一样的话,就会调用核心方法的notify(),notify()的过程就会通知我们的视图更新。最后,如果是基础类型是不观测的。
(3)源码要点分析
在这里插入图片描述
①任何⼀个 Vue Component 都有⼀个与之对应的 Watcher 实例。
②Vue 的 data 上的属性会被添加 getter 和 setter 属性。
③当 Vue Component render 函数被执行的时候, data 上会被 触碰(touch), 即被读, getter 方法会被调用, 此时 Vue 会去记录此 Vue component 所依赖的所有 data。(这⼀过程被称为依赖收集)。
④data 被改动时(主要是用户操作), 即被写, setter 方法会被调用, 此时 Vue 会去通知所有依赖于此 data 的组件去调用他们的 render 函数进行更新。

(4)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 重写。

参考阅读
面试官:说说对observable的理解
响应式数据的原理(双向数据绑定)[proxy和Object.defineProperty]
Vue Options

17.Vue中源码中的数组

1.Vue中是如何检测数组变化

(1)要点分析

  • 使用函数劫持的方式,重写了数组的方法

  • Vue 将 data 中的数组,进行了原型链重写。指向了自己定义的数组原型方法,这样当调用数组 api 时,可以通知依赖更新.如果数组中包含着引用类型。会对数组中的引用类型再次进行监控。

(2)大致流程分析

​ 数组并没有使用defineReactive去重新定义数组的每一项,数组常用的方法有push pop shift unshift slice sort reverse等7种方法,这7种方法都可以更改数组的内容,只要把改了数组内容更新就好了,所以Vue把数组的原型方法进行了重写,重写出了一个新的类型,让数组类型通过原型链找到了新写的原型功能,这样用户在调用数组上的方法的时候,就是自己的方法。这样就可以设置,它一更新数据就通知视图更新。同时这个数组比较特殊,数组里面可能还是一个对象,我们就把数组遍历一下,如果数组里面有对象的话,会继续深度遍历,看看它里面有没有对象,如果有对象,就对对象深度观测。

(3)源码分析

const arrayProto = Array.prototype 
export const arrayMethods = Object.create(arrayProto) 
const methodsToPatch = [ 
   'push',  
   'pop',  
   'shift', 
   'unshift', 
   'splice',
   'sort',
   'reverse' 
 ] methodsToPatch.forEach(function (method) { // 重写原型方法 
     const original = arrayProto[method] // 调用原数组的方法 
     def(arrayMethods, method, function mutator (...args) {  
        const result = original.apply(this, args)   
        const ob = this.__ob__   
        let inserted   
            switch (method) {      
            case 'push':     
            case 'unshift':       
                  inserted = args      
                   break      
            case 'splice':       
                  inserted = args.slice(2)       
                   break    
                   }   
             if (inserted) ob.observeArray(inserted)    
             // notify change   
            ob.dep.notify() // 当调用数组方法后,手动通知视图更新 
            return result 
     })
 })

this.observeArray(value) // 进行深度监控

在这里插入图片描述

如果是数组的话先判断支不支持原型链,【里面有个protoAugment,让目标的原型链指向src。】

当前如果是数组的话,让数据的原型链指向 arrayMethods,arrayMethods就是我们改写的数组原型上的方法,一共有7个push pop shift unshift slice sort reverse,为什么只拦截7个,只有这7个方法才能改变我们数组,内部同样会采用数据劫持的方式,就是当用户调用这些方法之后,还会调用源数组的方法进行更细数组。重新定义方法,等一会儿,用户调用方法的时候,就会执行这个函数,这个函数里我可以手动通知视图更新,还用一个特点,如果有新增的数据比如push,unshift,splice,这些方法可以帮我们更新数组的。新增一项,如果有新增的话,我也要重新观测,因为新增的数据也可能是对象类型,把新增的内容拿到,然后observeArray,把它遍历一遍看内部需不需要做响应式。protoAugment是更改原型链,除了这个数组里面可能还有引用类型,继续调用observeArray去观测它,循环当中的每一项,继续进行观测,但是前提必须是对象类型才会被观测,observeArray方法会判断,如果非对象类型,就会return,所以只有数组里的对象才能进行响应式的数据变换。所以就两点,一是更改了数组的原型,更改了我自己的,当他调用方法的时候我会手动去更新,通知视图更新。再就是对数组里的对象进行观测,如果它里面是对象的话也会导致视图更新。

(4)问题小结

/*数组里的对象?*/
arr = [{a:1}2]//数组里的对象和普通值
arr[0].a = 100 //对象会触发数组的更新
arr[1] =100//这样是不行的
/*更改原型*/
data:{
   arr:[]
}
data.arr__proto__=arrayMethods//arrayMethods里面的属性和方法都是我们重写后的【Observer=>array.js=>24-46】重写的方法 原生的方法
/*Observer=>index=>118*/
如果已经检测过了,就返回。不会重复监听

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

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

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

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

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

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

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

问:为何Object.defineProperty明明能监听到数组值的变化,而它却没有实现呢?

对于b问题,则需要去看看Vue的源码里,为何Object.defineProperty明明能监听到数组值的变化,而它却没有实现呢?

这里分享一下我看源码的技巧,如果直接打开github一行一行看看源码是很懵逼的,我这里是直接用Vue-cli在本地生成一个Vue项目,然后在安装的node_modules下的Vue包里进行断点查看的,大家可以尝试下。

测试代码很简单,如下;

import Vue from './node_modules/_vue@2.6.11@vue/dist/vue.runtime.common.dev'
// 实例化Vue,启动起来后直接
new Vue({
  data () {
    return {
      list: [1, 3]
    }
  },
})

在这里插入图片描述

解释一下这一块儿的源码,下面的hasProto的源码是看是否有原型存在,arrayMethods是被重写的数组方法,代码流程是如果有原型,直接修改原型上的push,pop,shift,unshift,splice, sort,reverse七个方法,如果没有原型的情况下,走copyAugment去新增这七个属性后赋值这七个方法,并没有监听。

/**
   * Observe a list of Array items.
   */
observeArray (items: Array<any>) {
  for (let i = 0, l = items.length; i < l; i++) {
    // 监听数组元素
    observe(items[i])
  }
}

最后就是this.observeArray函数了,它的内部实现非常简单,它对数组元素进行了监听,什么意思呢,就是改变数组里的元素不能监听到,但是数组内的值是对象类型的,修改它依旧能得到监听响应,如改变list[0].val可以得到监听,但是改变list[0]不能,但是依旧没有对数组本身的变化进行监听。

再看看arrayMethods是如何重写数组的操作方法的。

// 记录原始Array未重写之前的API原型方法
const arrayProto = Array.prototype
// 拷贝一份上面的原型出来
const arrayMethods = Object.create(arrayProto)
// 将要重写的方法
const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ]
/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function (method) {
  def(arrayMethods, method, function mutator (...args) {
    // 原有的数组方法调用执行
    const result = arrayProto[method].apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    // 如果是插入的数据,将其再次监听起来
    if (inserted) ob.observeArray(inserted)
    // 触发订阅,像页面更新响应就在这里触发
    ob.dep.notify()
    return result
  })
})

从上面的源码里可以完整的看到了Vue2.x中重写数组方法的思路,重写之后的数组会在每次在执行数组的原始方法之后手动触发响应页面的效果。

看完源码后,问题a也水落石出了,Vue2.x中并没有实现将已存在的数组元素做监听,而是去监听造成数组变化的方法,触发这个方法的同时去调用挂载好的响应页面方法,达到页面响应式的效果。

但是也请注意并非所有的数组方法都重新写了一遍,只有push,pop,shift,unshift,splice, sort,reverse这七个。至于为什么不用Object.defineProperty去监听数组中已存在的元素变化。
在这里插入图片描述
作者尤雨溪的考虑是因为性能原因,给每一个数组元素绑定上监听,实际消耗很大,而受益并不大。
issue地址:https://github.com/vuejs/vue/issues/8562。
参考阅读:高级前端开发者必会的34道Vue面试题系列(二)

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

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

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

我们查看对应的 Vue 源码:vue/src/core/instance/index.js

export function set (target: Array<any> | Object, key: any, val: any): any {
  // target 为数组  
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    // 修改数组的长度, 避免索引>数组长度导致splcie()执行有误
    target.length = Math.max(target.length, key)
    // 利用数组的splice变异方法触发响应式  
    target.splice(key, 1, val)
    return val
  }
  // key 已经存在,直接修改属性值  
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  const ob = (target: any).__ob__
  // target 本身就不是响应式数据, 直接赋值
  if (!ob) {
    target[key] = val
    return val
  }
  // 对属性进行响应式处理
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

我们阅读以上源码可知,vm.$set 的实现原理是:
如果目标是数组,直接使用数组的 splice 方法触发响应式;
如果目标是对象,会先判读属性是否存在、对象是否是响应式,最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理( defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 getter 和 setter 的功能所调用的方法)
参考阅读
面试官:Vue中给对象添加新属性界面不刷新?

19.组件中的 data相关问题及优化?

(1)为什么是一个函数?为什么new Vue({})可以放一个对象?★★

面试官:为什么data属性是一个函数而不是一个对象?

【1问】:同一个组件被复用多次,会创建多个实例。这些实例用的是同一个构造函数,js里的对象是引用关系,如果 data 是一个对象的话,那么所有组件都共享了同一个对象,子组件data属性值会互相污染,产生不必要的麻烦。为了保证组件的数据独立性要求每个组件必须通过 data 函数 返回一个对象作为组件的状态。【 一个组件被使用多次,用的都是同一个构造函数。为了保证组件的不同的实例data不冲突,要求 data必须是一个函数,这样组件间不会相互影响.】
2问】:因为这个类创建的实例不会被共用,new vue只创建这一次,不会考虑复用的问题,根只有1个。
举例分析
Vue每次会通过一个组件创造一个构造函数来,当每个实例都是通过这个构造函数new出来的【如下的vc1和vc2】,假如data是对象,把data放到原型上,当第一次new出来一个实例vc1是第一个组件,这个组件里就可以拿到data进行改写,第二个组件vc2也会new这个构造函数,同样会用这个data,这时候相当于第1个组件把数据改了,第二个组件拿到的是当前第1个改后的数据,这就导致两个组件并不独立啦,而且可以相互引用到data。如果换一种思路把data变成一个函数,这时候拿到这个组件后调用这个函数,它会返回一个对象,用这个对象作为它的属性、数据或状态。第二个人用的时候同样也需要修改状态,这样就能保证每个组件调用data都能返回一个全新的对象,而且和全面没有关系,所以说不能把data声明成对象,否则会有这样一系列问题,主要避免组件间的数据互相影响。

function VueComponent(){}
VueComponent.prototype.$options = {
   data:{name:'zf'}
}
let vc1 = new VueComponent();
vc1.$options.data = 'jw';
let vc2 = new VueComponent();
console.log(vc2.$options.data);
//jw
function VueComponent(){}
VueComponent.prototype.$options = {
   data()=>({name:'zf'})
}
let vc1 = new VueComponent();
vc1.$options.data();
let vc2 = new VueComponent();
console.log(vc2.$options.data());

原理:
mergeOptions就是mixin的原理,也是vue中各种合并的原理,包括声明周期的合并、data的合并。创建子类就是通过vue.extend方法【13】,创建子类之后,会把父类的选项和自己的做一个合并【39-43】,自己的选项就包含data。mergeOptions=>util/options.js,组件里可能会有mixins属性和extends属性【util/options.js409-416】,它会循环父亲也会循环儿子,最后会调一个方法mergeField,strats方法里有自己对应的值,stats.data【127-133】就是合并data,怎么合并data的?在合并之前,这个孩子是不是一个函数,如果不是函数,就会告诉你这个数据应该是个函数,每个组件都应该返回一个实例,这样才能保证组件间是互不干扰的,这样的话子组件如果传的是对象的话,在任何环境都会报错。
core/global-api/extend.js line:33

Sub.options = mergeOptions(
    Super.options,
    extendOptions
)
function mergeOptions(){
    function mergeField (key) {
         const strat = strats[key] || defaultStrat
         options[key] = strat(parent[key], child[key], vm, key)
   }
}
strats.data = function (
   parentVal: any,
   childVal: any,  
   vm?: Component
    ): ?Function {
if (!vm) { // 合并是会判断子类的data必须是一个函数
  if (childVal && typeof childVal !== 'function') {
    process.env.NODE_ENV !== 'production' && warn(
   'The "data" option should be a function ' +
   'that returns a per-instance value in component ' +
    'definitions.',
     vm
   )
    return parentVal
    }
    return mergeDataOrFn(parentVal, childVal)
  }
return mergeDataOrFn(parentVal, childVal, vm)
}

【问题小结】
①为什么要合并?
core/global-api/extend.js[33][56-58]
因为子组件也要有父组件的属性,Vue.extend通过一个对象创建了一个构造函数,但这个构造函数并没有父类的东西,它是一个新函数,和我们之前vue的构造函数是没有关系的通过extend就要产生一个子函数,这个子函数需要有vue上的所有东西,所以需要合并。相当于每个子类都有自己的父类,这个父类是通过父类构建出来的【33】。

(2)不需要响应式的数据应该怎么处理?(长列表性能优化)

在我们的Vue开发中,会有一些数据,从始至终都未曾改变过,这种死数据,既然不改变,那也就不需要对他做响应式处理了,不然只会做一些无用功消耗性能,比如一些写死的下拉框,写死的表格数据,这些数据量大的死数据,如果都进行响应式处理,那会消耗大量性能。

方法一:将数据定义在data之外

data () {
    this.list1 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
    this.list2 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
    this.list3 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
    this.list4 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
    this.list5 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
    return {}
 }
    

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

//例1
data () {
    return {
        list1: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
        list2: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
        list3: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
        list4: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
        list5: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
    }
 }
// 例2
export default {
  data: () => ({
    users: {}
  }),

  async created() {
    const users = await axios.get("/api/users");
    this.users = Object.freeze(users);
  }
};

(3)使用局部变量

这里主要是优化前后的组件的计算属性 result 的实现差异,优化前的组件多次在计算过程中访问 this.base,而优化后的组件会在计算前先用局部变量 base,缓存 this.base,后面直接访问 base。

那么为啥这个差异会造成性能上的差异呢,原因是你每次访问 this.base 的时候,由于 this.base 是一个响应式对象,所以每次都会触发它的 getter,进而会执行依赖收集相关逻辑代码,类似的逻辑执行多了,性能自然就越差。像示例这样,几百次循环更新几百个组件,每个组件触发 computed 重新计算,然后又多次执行依赖收集相关逻辑,性能自然就下降了。

从需求上来说,this.base 执行一次依赖收集就够了,把它的 getter 求值结果返回给局部变量 base,后续再次访问 base 的时候就不会触发 getter,也不会走依赖收集的逻辑了,性能自然就得到了提升。

从需求上说在一个函数里一个变量执行一次依赖收集就够了,可是很多人习惯性的在项目中大量写 this.xx,而忽略了 this.xx 背后做的事,就会导致性能问题了

比如下面例子

优化前

<template>
  <div :style="{ opacity: start / 300 }">{{ result }}</div>
</template>

<script>
import { heavy } from '@/utils'

export default {
  props: ['start'],
  computed: {
    base () { return 42 },
    result () {
      let result = this.start
      for (let i = 0; i < 1000; i++) {
        result += heavy(this.base)
      }
      return result
    }
  }
}
</script>

优化后

<template>
  <div :style="{ opacity: start / 300 }">
    {{ result }}</div>
</template>

<script>
import { heavy } from '@/utils'

export default {
  props: ['start'],
  computed: {
    base () { return 42 },
    result () {
      const base = this.base
      let result = this.start
      for (let i = 0; i < 1000; i++) {
        result += heavy(base)
      }
      return result
    }
  }
}
</script>

参考阅读:
1.『前端优化』—— Vue中避免滥用this去读取data中数据
2.揭秘 Vue.js 九个性能优化技巧

(4)Vue里做响应式更新

问:如果在Vue里做响应式更新,数据在哪定义?
答:数据肯定在data中定义,不在data中定义的数据它是非响应式的。
问:哪如何变成响应式?
答:可以通过vue语法糖this.$set强制变成响应式,或者手写get、set方法也可以。
问:如何手写get、set方法?
答:利用js原生的Object.defineProperty对它变量做修改以及对变量获取做监听,监听到了去给它绑定相应的事件。

(5)如何获取初始状态的变量值

在开发中,有时候需要拿初始状态去计算。例如

data() {
    return {
      num: 10
  },
mounted() {
    this.num = 1000
  },
methods: {
    howMuch() {
        // 计算出num增加了多少,那就是1000 - 初始值
        // 可以通过this.$options.data().xxx来获取初始值
        console.log(1000 - this.$options.data().num)
    }
  }

20.Computed与Watch

(一)Vue中Computed的特点

官网计算属性的缘由与方法、侦听属性的区别
Vue 的计算属性如何实现缓存?
computed使用详解(附demo,不定期更新)

(1)理解

计算属性的创作初衷(场景):computed 一般用于简化模板中变量的调用。computed适用于计算比较消耗性能的计算场景。当表达式过于复杂时,在模板中放入过多逻辑会让模板难以维护,可以将复杂的逻辑放入计算属性中处理。
原理:computed 本质是一个惰性求值的观察者computed watcher,也可以理解为是一个是具备缓存的watcher 。其内部通过 this.dirty 属性标记计算属性是否需要重新求值。

  • 当 computed 的依赖状态发生改变时,就会通知这个惰性的 watcher,computed watcher 通过
    this.dep.subs.length 判断有没有订阅者。

  • 有的话,会重新计算,然后对比新旧值,如果变化了,会重新渲染。(Vue想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化时才会触发渲染 watcher 重新渲染,本质上是一种优化。)

  • 没有的话,仅仅把 this.dirty = true
    (当计算属性依赖于其他数据时,属性并不会立即重新计算,只有之后其他地方需要读取属性的时候,它才会真正计算,即具备
    lazy(懒计算)特性。)

(2)computed 和 methods区别

最好理解的是方法method和计算属性的computed的区别,method方法的特点:只要你把方法用到模板上了,那每次一变化就会重新视图渲染,性能开销比较大,每次一更新,方法就要重新执行,但是computed是具备缓存的,这就区别开了,如果有个数据需要计算,我们不要直接{{fn()}},可以写个计算属性,计算属性有缓存的意思是如果它依赖的属性没有发生变化,就不会导致方法重新执行,相当于减少重新计算的过程。

实例

问题: 计算变量时,methods和computed哪个好?

computed会好一些,因为computed会有缓存。例如index由0变成1,那么会触发视图更新,这时候methods会重新执行一次,而computed不会,因为computed依赖的两个变量num和price都没变。

<div>
    <div>{{howMuch1()}}</div>
    <div>{{howMuch2()}}</div>
    <div>{{index}}</div>
</div>

data: () {
    return {
         index: 0
       }
     }
methods: {
    howMuch1() {
        return this.num + this.price
    }
  }
computed() {
    howMuch2() {
        return this.num + this.price
    }
  }

(3)computed 和watch

简单理解

  • computed 一般用于简化模板中变量的调用。是依赖已有的变量来计算一个目标变量,大多数情况都是多个变量凑在一起计算出一个变量,并且computed具有缓存机制,依赖值不变的情况下其会直接读取缓存进行复用,computed不能进行异步操作
  • watch是一般用于监听数据的变化并执行相应的回调函数,做一些逻辑处理或者异步处理,可以深度监听、立即执行。通常是一个变量的变化决定多个变量的变化,watch可以进行异步操作
  • 简单记就是:一般情况下computed是多对一,watch是一对多
  • 初始化时,先创建 computed 再创建 watch 。数据改变时,先执行 computed 再执行 watch
  • computed 和 watch 在源码里都是通过 Watcher 类创建出来的

参考链接

先明确computed和watch内部的原理都是watcher实现的。

详细理解

Watch没有缓存性,更多的是观察的作用,每当监听的数据变化时都会执行回调进行后续操作。
虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器。这就是为什么 Vue 通过 watch 选项提供了一个更通用的方法,来响应数据的变化。当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。当我们需要深度监听对象中的属性时,可以打开deep:true选项,这样便会对对象中的每一项进行监听。这样会带来性能问题,优化的话可以使用字符串形式监听,如果没有写到组件中,不要忘记使用unWatch手动注销。

(4)实际项目中的小技巧

1.组件模板应该只包含简单的表达式,复杂的表达式则应该重构为计算属性或方法。

复杂表达式会让你的模板变得不那么声明式。我们应该尽量描述应该出现的是什么,而非如何计算那个值。而且计算属性和方法使得代码可以重用。

// bad
{{
  fullName.split(' ').map((word) => {
    return word[0].toUpperCase() + word.slice(1)
  }).join(' ')
}}

更好的做法:

<!-- 在模板中 -->
{{ normalizedFullName }}
// 复杂表达式已经移入一个计算属性
computed: {
  normalizedFullName: function () {
    return this.fullName.split(' ').map(function (word) {
      return word[0].toUpperCase() + word.slice(1)
    }).join(' ')
  }
}

2.应该把复杂计算属性分割为尽可能多的更简单的属性。 小的、专注的计算属性减少了信息使用时的假设性限制,所以需求变更时也用不着那么多重构了。

// bad
computed: { 
  price: function () { 
    var basePrice = this.manufactureCost / (1 - this.profitMargin) 
    return ( 
      basePrice - 
      basePrice * (this.discountPercent || 0) 
    ) 
  } 
}

// good
computed: {
  basePrice: function () {
    return this.manufactureCost / (1 - this.profitMargin)
  },
  discount: function () {
    return this.basePrice * (this.discountPercent || 0)
  },
  finalPrice: function () {
    return this.basePrice - this.discount
  }
}

3.计算属性的 setter
计算属性默认只有 getter,不过在需要时你也可以提供一个 setter:

// ...
computed: {
  fullName: {
    // getter
    get: function () {
      return this.firstName + ' ' + this.lastName
    },
    // setter
    set: function (newValue) {
      var names = newValue.split(' ')
      this.firstName = names[0]
      this.lastName = names[names.length - 1]
    }
  }
}
// ...

现在再运行 vm.fullName = ‘John Doe’ 时,setter 会被调用,vm.firstName 和 vm.lastName 也会相应地被更新。

4.使用闭包为computed计算属性传参

     <li class="recommend-list-item border-bottom"
          v-for="item of recommendList"
          :key="item.id"
      >
         <div class="recommend-list-item-comments">
            <span>{{stars(item.star)}}</span>
          </div>
     </li>
......
export default {
  name: 'HomeRecommend',
  data () {
    return {
      recommendList: [
        {
          id: '0001',
          imgUrl: '/static/imgs/list/adca619faaab0898245dc4ec482b5722.jpg_250x250_0fc722c0.jpg',
          title: '故宫',
          star: [1, 1, 1, 1, 1],
          comments: '32879',
          price: '¥60',
          location: '东城区',
          desc: '世界五大宫之首,穿越与您近在咫尺'
        }
      ]
   }
  },
  computed: {
    stars: function () {
      return function (star) {
        var count = 0
        star.forEach((item, index) => {
          if (item) {
            count++
          }
        })
        return count
      }
    }
  }
}

(5)源码分析

默认初始化计算属性的时候,会创建一个watcher,而且这个watcher会有一个标识,叫lazy:true,默认true不执行,它会定义一个defineComputed属性,当用户取值的时候就会调用watcher求值,求完值之后,会把watcher变成false,要多次取值,当再取值时,dirty为false,直接返回上次计算的结果。什么时候再变为true?它计算属性依赖的数据发生变化,会让计算属性的watcher的dirty变成true,它会重新的计算。

function initComputed (vm: Component, computed: Object) {  
    const watchers = vm._computedWatchers = Object.create(null)  
    const isSSR = isServerRendering()  
     for (const key in computed) {  
        const userDef = computed[key]   
        const getter = typeof userDef === 'function' ? userDef : userDef.get  
        if (!isSSR) {   
            // create internal watcher for the computed property.
         watchers[key] = new Watcher(   
             vm,       
             getter || noop,      
             noop,       
             computedWatcherOptions    
         ) 
        }
    // component-defined computed properties are already defined on the   
    // component prototype. We only need to define computed properties defined  
   // at instantiation here.   
      if (!(key in vm)) {    
          defineComputed(vm, key, userDef)  
      } else if (process.env.NODE_ENV !== 'production') {
      if (key in vm.$data) {       
          warn(`The computed property "${key}" is already defined in data.`, vm)     
      } else if (vm.$options.props && key in vm.$options.props) {        warn(`The computed property "${key}" is already defined as a prop.`, vm)     
           }  
      }  
     }
} function createComputedGetter (key) {  
    return function computedGetter () {  
        const watcher = this._computedWatchers && this._computedWatchers[key]  
        if (watcher) {      
            if (watcher.dirty) { // 如果依赖的值没发生变化,就不会重新求值        watcher.evaluate()  
            }      if (Dep.target) {      
                watcher.depend()    
            }      return watcher.value 
        } 
    } 
}

在这里插入图片描述

initComputed里面初始化的时候,它会先获取用户计算属性的定义,内部会创建一个watcher,把用户的定义传进去,而且会标识一下这个watcher是懒watcher,默认不会执行,这里有computedWatcherOptions,getter是用户方法的定义,expOrFn,用户定义是个函数,如果是个函数,就把它存到getter上,存好以后,如果它是lazy的话,就什么也不做,所以创建一个wacher的计算属性的话,默认是不会执行计算属性中的函数的,只用在页面取值的时候,里面有一个dedineComputed,为什么计算属性可以通过vm实例调用?其实它底层也是用Object.defineProperty,只要是响应式变的都用的是Object.defineProperty,target就是vm,key就是计算属性的key值,同时它对应的对象并不是用户的那个,而是自己写了一个,叫createComputedGetter,computedGetter就是刚才方法的返回值,用户真正取值会执行computedGetter函数,它会看看当前计算属性的值是否为true,如果为true就会求值,watcher.evaluate计算属性的核心就是做了一个dirty实现了缓存的机制。

(4)问题小结

①计算属性支持异步吗?

先明确它为什么有异步?在里面写异步的时候,拿到的返回值就是undefined,把undefined作成一个值返回驱动到页面上没有什么实际意义,只是说watch的功能可以异步拿到的功能。计算属性和watch的实现原理都是基于watcher的。

②什么时候再变为true?

当这个属性依赖的值变化,比如说计算属性依赖了firstNamelastName,只要firstNamelastName一变,它就会调用firstName对应watcher的update(),因为这个firstName会记住当前的watcher是谁?这个watcher有计算属性,还有渲染,它会更新一下,把当前计算属性watcher的值,计算属性依赖的数据发生变化,会让计算属性的watcher的dirty变成true,再变回true。

③计算属性的watcher在普通的watcher之前,所以不是异步更新,所以计算属性会优先于渲染,这样的话,在渲染的时候才会取到最新的值。

(二).Watch介绍及其高阶使用和原理

(1)基本概念、分类、及使用场景

基本概念
watch没有缓存性,更多的是观察的作用,每当监听的数据变化时都会执行回调进行后续操作。

分类
Watcher : 观察者对象 , 实例分为渲染 watcher (render watcher)计算属性 watcher (computed watcher)侦听器 watcher(user watcher) 三种.

运用场景
当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。

(2)高阶使用[5]

立即执行
情景一:当被监控的prop发生突变时,watch handler就会触发,也就是说watch 是在监听属性改变时才会触发。
情景二:有些时候,我们希望在组件创建后 watch 能够立即执行。可能想到的的方法就是在 create 生命周期中调用一次,但这样的写法不优雅,或许我们可以使用这样的方法,使用immediate完全可以这么写,当它为true时,会初始执行一次

//不优雅
created(){
  this.getList()
},
watch: {
  searchInputValue(){
    this.getList()
  }
}
//优雅
export default {
    data() {
        return {
            name: 'Joe'
        }
    },
    watch: {
        name: {
            handler: 'sayName',
            immediate: true
        }
    },
    methods: {
        sayName() {
            console.log(this.name)
        }
    }
}

深度监听
有时,watch 属性是一个对象,但是其属性突变无法触发 wacher 处理程序。在这种情况下,为观察者添加 deep:true 可以使其属性的突变可检测到。
【注意】:当对象具有多个层时,深层可能会导致一些严重的性能问题。最好考虑使用更扁平的数据结构。

export default {
    data: {
        studen: {
            name: 'Joe',
            skill: {
                run: {
                    speed: 'fast'
                }
            }
        }
    },
    watch: {
        studen: {
            handler: 'sayName',
            deep: true
        }
    },
    methods: {
        sayName() {
            console.log(this.studen)
        }
    }
}

触发监听执行多个方法
使用数组可以设置多项,形式包括字符串、函数、对象

export default {
    data: {
        name: 'Joe'
    },
    watch: {
        name: [
            'sayName1',
            function(newVal, oldVal) {
                this.sayName2()
            },
            {
                handler: 'sayName3',
                immaediate: true
            }
        ]
    },
    methods: {
        sayName1() {
            console.log('sayName1==>', this.name)
        },
        sayName2() {
            console.log('sayName2==>', this.name)
        },
        sayName3() {
            console.log('sayName3==>', this.name)
        }
    }
}

watch订阅多个变量突变
watch本身无法监听多个变量。但我们可以将需要监听的多个变量通过计算属性返回对象,再监听这个对象来实现“监听多个变量”。

export default {
    data() {
        return {
            msg1: 'apple',
            msg2: 'banana'
        }
    },
    compouted: {
        msgObj() {
            const { msg1, msg2 } = this
            return {
                msg1,
                msg2
            }
        }
    },
    watch: {
        msgObj: {
            handler(newVal, oldVal) {
                if (newVal.msg1 != oldVal.msg1) {
                    console.log('msg1 is change')
                }
                if (newVal.msg2 != oldVal.msg2) {
                    console.log('msg2 is change')
                }
            },
            deep: true
        }
    }
}

watch监听一个对象时,如何排除某些属性的监听?

下面代码是,params发生改变就重新请求数据,无论是a,b,c,d属性改变

data() {
    return {
      params: {
        a: 1,
        b: 2,
        c: 3,
        d: 4
      },
    };
  },
watch: {
    params: {
      deep: true,
      handler() {
        this.getList;
      },
    },
  }

但是如果我只想要a,b改变时重新请求,c,d改变时不重新请求呢?

mounted() {
    Object.keys(this.params)
      .filter((_) => ![c, d].includes(_)) // 排除对c,d属性的监听
      .forEach((_) => {
        this.$watch((vm) => vm.params[_], handler, {
          deep: true,
        });
      });
  },
data() {
    return {
      params: {
        a: 1,
        b: 2,
        c: 3,
        d: 4
      },
    };
  },
watch: {
    params: {
      deep: true,
      handler() {
        this.getList;
      },
    },
  }

(3)Watch中的deep:true 是如何实现的(原理)

①理解:
当用户指定了 watch 中的deep属性为 true 时,如果当前监控的值是数组类型。会对对象中的每 一项进行求值,此时会将当前 watcher 存入到对应属性的依赖中,这样数组中对象发生变化时也 会通知数据更新 。

第一步首先知道什么是watch,watch里面怎么实现的,才知道deep怎么实现的。watch就是用户写的watch对象,里面放着key和值。

②原理:

get () {   
     pushTarget(this) // 先将当前依赖放到 Dep.target上  
     let value    
     const vm = this.vm  
     try {     
     value = this.getter.call(vm, vm)  
     } catch (e) {     
     if (this.user) {     
     handleError(e, vm, `getter for watcher"${this.expression}"`) 
     } else {      
     throw e   
     }   
     } finally {     
     if (this.deep) { // 如果需要深度监控 
     traverse(value) // 会对对象中的每一项取值,取值时会执行对应的get方法      }    
     popTarget()   
     }  
     return value
     } 
     function _traverse (val: any, seen: SimpleSet) {  
     let i, keys
     const isA = Array.isArray(val)
     if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {   
     return  
     }  if (val.__ob__) {   
     const depId = val.__ob__.dep.id 
     if (seen.has(depId)) {    
     return   
     }   
     seen.add(depId)  
     }  if (isA) {   
     i = val.length   
     while (i--) _traverse(val[i], seen) 
     } else {  
     keys = Object.keys(val) 
     i = keys.length    
     while (i--) _traverse(val[keys[i]], seen) 
     } 
     }

initWatch初始化watch内部会调用createWatcher,里面会有vm.$watcher

vm.$watch('msg',()=>{})里可以放一个值,也可以放一个函数,如果放一个值,要监听它的变化,如果变的话就执行函数。'msg'就会传到exporFn,用户定义的方法就会传到cb,options是用户自己添加的。exporFn就是字符串类型,cb就是一个函数。如果exporFn是字符串,会帮你包装成函数,msg function(){return vm.msg},lazy此时是false,走得是this.get(),一调用get就会调用getter,默认将msg进行取值,在取值之前会把watcher放到全局上,取msg值的时候会watcher收集起来,msg一变,watcher值就会更新,但是有个缺陷,如果msg值是个对象的话{a:{a:1}},它只会对最外层进行依赖收集,不会对里面的a进行收集,怎么能让里边的也收集,非常简单,就是把这个对象也循环一遍,循环取值的过程就会把watcher存起来。为什么computed它没有deep:true。computed内部是用在模板里,{{xxx}},在模板里的数据会调用一个方法JSON.Stringnify(),会默认对这个对象里的属性都进行求值,所以没有这个过程,如果单独监控这个对象{a:{a:1}},只是监听最外层,如果放到模板里,会对所有属性进行取值,这里面会判断,如果当前是deep = true的话,会做一个方法叫traverse,在traverse里如果是数组的话,会根据数组的索引,把数组里的每一项进行取值,而且是递归,为什么deep=true会消耗性能?因为它会把这个对象从头遍历到尾,同样如果是对象的话,拿到key,接着去遍历,只要一取值,就会把我们当前看到的watcher存起来,这样它内部的属性发生变化,也会触发watcher进行更新。

问题小结

①watcher的种类:计算属性watcher 、渲染render watcher、 还有一个用户自定义属性user watcher。user watcher执行顺序应该在render watcher之前。watcher是一个监听器,只要数据一变,就让watcher去执行。

deep:true的实现就是递归,耗性能,会把对象的每个属性都进行取值,性能不高,尽量不要采用。

21.Vue组件的生命周期五问

(1)Vue`组件的生命周期都有哪些?

参考阅读:说说你对Vue生命周期的理解?
Vue的生命周期就是组件或者实例,从开始创建到销毁(初始化数据、编译模版、挂载Dom 、渲染=>更新=>渲染、卸载)等⼀系列过程,总共分为4个阶段,8个钩子,我们称这是Vue的生命周期。

第一阶段(创建阶段):beforeCreate,created
第二阶段(挂载阶段):beforeMount(render),mounted
第三阶段(更新阶段):beforeUpdate,updated
第四阶段(销毁阶段):beforeDestroy,destroyed

理解要掌握每个生命周期什么时候被调用

  • beforeCreate :vue实例的挂载元素el和数据对象data都还没有进行初始化,还是一个 undefined状态;在实例初始化之后,数据观测(data observer) 之前被调用。 特点是拿不到实例中的数据

  • created:(此时vue实例的数据对象data已经有了,可以访问里面的数据和方法, el还没有,也没有挂载dom。)实例已经创建完成之后被调用。在这一步,实例已完成以下的配置:数据观测(data observer),属性和方法的运算, watch/event 事件回调。这里没有$el可以拿到数据,拿不到$el。这时候实例创建完成后,还没有渲染到页面上

  • beforeMount :在这里vue实例的元素el和数据对象都有了,只不过在挂载之前还是虚拟的dom节点。在挂载开始之前被调用,相关的 render 函数首次被调用。

    会调用用户写的render方法,在渲染之前把template页面进行求值,重新渲染,用到很少,几乎看不到

  • mountedvue实例已经挂在到真实的dom上,可以通过对 dom操作来获取dom节点
    el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子。
    mounted是当前页面已经挂载好了,挂载方式是先创建一个新的el,再把以前老的el删掉,换成新的,mounted已经可以拿到vm实例对应的真实的Dom元素了。

  • beforeUpdate :响应式数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。适合在更新之前访问现有的 dom,比如手动移除已添加的事件监听器。
    当watcher进行更新的时候会调beforeUpdate方法,当然里面还有比较核心的逻辑DOM diff

  • updated虚拟dom重新渲染和打补丁之后调用,组成新的 dom已经更新,避免在这个钩子函数中操作数据,防止死循环。
    由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。 表示当前的生命周期更新完成之后会走这样一个逻辑,它也在watcher里执行的

  • beforeDestroy :vue实例在销毁前调用,在这里还可以使用,通过this也能访问到实例,可以在这里对一些不用的定时器进行清除,解绑事件。
    用户也可以手动调用这样beforeDestroy,叫销毁之前。destroyed叫销毁之后。

  • destroyed: Vue 实例销毁后调用。调用后, Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。 该钩子在服务器端渲染期间不被调用。

(2)要掌握每个生命周期内部可以做什么事

  • created 实例已经创建完成,因为它是早触发的原因可以进行一些数据,资源的请求,比如ajax请求。

  • mounted 实例已经挂载完成可以进行一些DOM操作

  • beforeUpdate 可以在这个钩子中进一步地更改状态这里不会触发附加的重渲染过程。

  • updated 可以执行依赖于 DOM 的操作。然而在大多数情况下,你应该避免在此期间更改状态因为这可能会导致更新无限循环。 该钩子在服务器端渲染期间不被调用。
    目前用的在tansition里,其他用的都不多,基本看不到。

  • beforedestroyed、destroyed里 可以执行一些优化操作,清空定时器,解除绑定事件前端常见内存泄漏及解决方案

在这里插入图片描述

(2).Vue父子组件生命周期调用顺序

加载渲染过程

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

子组件更新过程

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

父组件更新过程

  • beforeUpdate->父updated

销毁过程

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

理解:

组件的调用顺序都是先父后子,渲染完成的顺序肯定是先子后父

组件的销毁操作是先父后子,销毁完成的顺序是先子后父

原理:

在这里插入图片描述

function patch (oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = [] // 定义收集所有组件的insert hook方法的数组
// somthing ...
createElm(
vnode,
insertedVnodeQueue,
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
// somthing...
// 最终会依次调用收集的insert hook
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
     return vnode.elm
}
function createElm (
    vnode,
    insertedVnodeQueue,
    parentElm,
    refElm,
    nested,
    ownerArray,
    index
 ) {
        // createChildren会递归创建儿子组件
        createChildren(vnode, children, insertedVnodeQueue)
        // something...
 }
// 将组件的vnode插入到数组中
function invokeCreateHooks (vnode, insertedVnodeQueue) {
    for (let i = 0; i < cbs.create.length; ++i) {
      cbs.create[i](emptyNode, vnode)
   }
    i = vnode.data.hook // Reuse variable
    if (isDef(i)) {
      if (isDef(i.create)) i.create(emptyNode, vnode)
      if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
   }
 }
// insert方法中会依次调用mounted方法
insert (vnode: MountedComponentVNode) {
    const { context, componentInstance } = vnode
    if (!componentInstance._isMounted) {
      componentInstance._isMounted = true
      callHook(componentInstance, 'mounted')
   }
}
function invokeInsertHook (vnode, queue, initial) {
     // delay insert hooks for component root nodes, invoke them after the
     // element is really inserted
     if (isTrue(initial) && isDef(vnode.parent)) {
         vnode.parent.data.pendingInsert = queue
     } else {
         for (let i = 0; i < queue.length; ++i) {
             queue[i].data.hook.insert(queue[i]); // 调用insert方法
         }
     }
}

Vue.prototype.$destroy = function () {
    callHook(vm, 'beforeDestroy') //
    // invoke destroy hooks on current rendered tree
    vm.__patch__(vm._vnode, null) // 先销毁儿子
    // fire destroyed hook
    callHook(vm, 'destroyed')
}

(3)父组件如何监听到子组件的生命周期?

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

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

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

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

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

当然 @hook 方法不仅仅是可以监听 mounted,其它的生命周期事件,例如:created,updated 等都可以监听。

(4).ajax请求放在哪个生命周期中

理解】

  • 在created的时候,视图中的 dom 并没有渲染出来,所以此时如果直接去操 dom 节点,无法找到相 关的元素 缺陷:写前端代码中无法拿到真实DOM元素

  • 在mounted中,由于此时 dom 已经渲染出来了,所以可以直接操作 dom 节点,官方实例的异步请求是在mounted生命周期中调用的 。

    注意】

    ①一般情况下都放到 mounted 中,保证逻辑的统一性,因为生命周期是同步执行的, ajax 是异步执行的。

    ②服务端渲染不支持mounted方法,所以在服务端渲染的情况下统一放到created中 。

    问题小结】

    ①同一个域名下http可以并发6个请求,并发没有顺序,如果写到了链式调用就有顺序了。

(5).何时需要使用beforeDestroy

理解:

  • 可能在当前页面中使用了 $on 方法,那需要在组件销毁前解绑。

    如果不解绑,等会再触发这些事情的时候,可能还会操作实例,实例已经销毁了,再操作就不行了

  • 清除自己定义的定时器

  • 解除原生事件的绑定 scroll mousemove .

22.methods属性

methods属性是一个对象,通常我们会在这个对象中定义很多的方法:

  • 这些方法可以被绑定到 template 模板中;
  • 在该方法中,我们可以使用this关键字来直接访问到data中返回的对象的属性;

对于有经验的同学,在这里我提一个问题,官方文档有这么一段描述:
在这里插入图片描述

问题一:为什么不能使用箭头函数(官方文档有给出解释)?
我们在methods中要使用data返回对象中的数据,那么这个this是必须有值的,并且应该可以通过this获取到data返回对象中的数据。

那么我们这个this能不能是window呢?

  • 不可以是window,因为window中我们无法获取到data返回对象中的数据;
  • 但是如果我们使用箭头函数,那么这个this就会是window了;
    我们来看下面的代码:
  • 我将increment换成了箭头函数,那么它其中的this进行打印时就是window;
const App = {
  template: "#my-app",
  data() {
    return {
      counter: 0
    }
  },
  methods: {
    increment: () => {
      // this.counter++;
      console.log(this);
    },
    decrement() {
      this.counter--;
    }
  }
}

为什么是window呢?

  • 这里涉及到箭头函数使用this的查找规则,它会在自己的上层作用域中来查找this;
  • 最终刚好找到的是script作用域中的this,所以就是window;

this到底是如何查找和绑定的呢?

  • 在我的公众号有另外一篇文章,专门详细的讲解了this的绑定规则;
  • https://mp.weixin.qq.com/s/hYm0JgBI25grNG_2sCRlTA;
  • 认真学习之后你绝对对this的绑定一清二楚;

问题二:不使用箭头函数的情况下,this到底指向的是什么?(可以作为一道面试题)
事实上Vue的源码当中就是对methods中的所有函数进行了遍历,并且通过bind绑定了this:
在这里插入图片描述

参考链接

1.原创作者:coderwhy Vue3+TS系统学习二 - 邂逅Vue3开发
2.原创作者:coderwhy Vue3+TS系统学习三 - Vue3开发基础语法(一)

23.深入理解组件原理及组件的封装技巧

(1).组件的渲染和更新:描述组件渲染和更新过程

理解: 渲染组件时,会通过 Vue.extend 方法构建子组件的构造函数,并进行实例化。最终手动调用 $mount() 进行挂载。更新组件时会进行 patchVnode 流程.核心就是diff算法

在这里插入图片描述

(2).怎样理解 Vue 的单向数据流?

我们经常说 Vue 的双向绑定,其实是在单向绑定的基础上给元素添加 input/change 事件,来动态修改视图。所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。

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

有两种常见的试图改变一个 prop 的情形 :
这个 prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用。 在这种情况下,最好定义一个本地的data 属性并将这个 prop 用作其初始值:

props: ['initialCounter'],
data: function () {
  return {
    counter: this.initialCounter
  }
}

这个 prop 以一种原始的值传入且需要进行转换。 在这种情况下,最好使用这个 prop 的值来定义一个计算属性。

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

问题一:如果子组件改变props里的数据会发生什么?

  • 改变的props数据是基本类型

如果修改的是基本类型,则会报错

props: {
    num: Number,
  }
created() {
    this.num = 999
  }

在这里插入图片描述

  • 改变的props数据是引用类型
props: {
    item: {
      default: () => {},
    }
  }
created() {
    // 不报错,并且父级数据会跟着变
    this.item.name = 'sanxin';
    
    // 会报错,跟基础类型报错一样
    this.item = 'sss'
  },

问题二:组件进阶props校验

props: {
    // 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)
    propA: Number,
    // 多个可能的类型
    propB: [String, Number],
    // 必填的字符串
    propC: {
      type: String,
      required: true
    },
    // 带有默认值的数字
    propD: {
      type: Number,
      default: 100
    },
    // 带有默认值的对象
    propE: {
      type: Object,
      // 对象或数组默认值必须从一个工厂函数获取
      default: function () {
        return { message: 'hello' }
      }
    },
    // 自定义验证函数
    propF: {
      validator: function (value) {
        // 这个值必须匹配下列字符串中的一个
        return ['success', 'warning', 'danger'].indexOf(value) !== -1
      }
    }
  }

(3).Vue组件如何通信? 单向数据流

2 种定义依赖 props 中的值
1.组件间通信
2.组件8大通信方式用法
3.Vue组件间通信方式都有哪些?
4.Vue 组件通信方式及其应用场景总结

  • 父子间通信 父->子通过 props 、子-> 父 $on$emit (发布订阅)

通信非常简单最常见就是父子间通信,父子间通信可以通过props进行传递,vue有个特点单向数据流,儿子不能去改父亲,所以父亲提供数据可以把数据传给儿子,通过属性传。如果儿子想改父亲,非常简单,可以让父亲传给他一个方法,让儿子去调用,通过属性来控制。第二种呢?我可以给儿子帮一个事件,绑的这个事件是父亲的事件,这时候再通过$emit触发自己的事件,这个逻辑就是简易的发布订阅。
[发布订阅源码]:src=>instance文件夹=>index.js
=>eventsMixin(vue)=>events.js
这个文件里主要做了几件事,比如说它提供了$on方法,这个方法和我们node种的on一样,里面主要做的事情就是发布订阅,你绑了多个事件,比如a事件,它可能有很多函数,它会把这个东西维护成这个样子:{a:[fn,fn,fn]},源码:(vm._ events[event] = [ ])) . push(fn)对象,key,key就是a,值就是一个个函数,等一会儿触发的时候,你可以把名字传过来,我就可以找到函数,让它依次执行,其实就是做个循环,找到这个事,去依次执行。

/*index.js*/
eventsMixin(Vue) // 3. 初始化vue中的$on $emit事件
/*events.js*/
export function eventsMixin (Vue: Class<Component>) {
   const hookRE = /^hook:/
   Vue.prototype.$on = function ( event: stringArray<string>, fn: Function): Component{
        const vm: Component = this
        if (Array . isArray( event)) {
             for(leti=0,1=event.length;i<l;i++){
            vm. $on(event[i], fn)
       }
     } else
      (vm._ events [event]|| **(vm._ events[event] = [ ])) . push(fn)**// optimize hook:event cost by using a boolean flag marked at registration
      // instead of a hash lookup
     if (hookRE . test(event)) {
        Vm. hasHookEvent = true
        }
        }
      return Vm
   }
vue.prototype. $once = function (event: string, fn: Function): Component {
     const vm: Component = this
     function on () {
        Vm.$off(event, on)
         fn.apply(vm, arguments)
         }
        on.fn = fn
        vm. $on(event, on)
       return vm 
      }
      ②触发传名字
       vue. prototype.$off = function (event?: string I Array<string>, fn?: Function): Component {
             const vm: Component = this
                   //all
              if (larguments. length) {
                   vm._ events = object .create(null)
                   return vm
                }
            // array of events
            if (Array. isArray( event)) {
                 ③循环
                    for(let i=0,1=event.length;i<1;i++){
                   vm. $off (event[i], fn)
                return vm
}
//specific event
const cbs = vm._ events [ event]
........
vue. prototype. $emit = function (event: string): Component {
    const vm: Component = this
    if ( process. env.NODE_ ENV !== ”production') { 
         const lowercaseEvent = event . toLowerCase() 
         if (lowerCaseEvent !== event && vm._ events [lowerCaseEvent]) {
         ④找到这个事
            tip(
                Event "${ lowerCaseEvent}" is emitted in component+
                ${ formatComponentName(vm)} but the handler is registered for”$ event}".
                 Note that HTML attributes are case - insensitive and you cannot use
                 v-on to listen to camelcase events when using in-DOM temp lates.
                 You should probably use "$ {hyphenate(event)}" instead of "${event}"."
           )
     }
}
let cbs = vm. events[ event]
⑤依次执行
if (cbs) {
      cbs = cbs.length > 1 ? toArray(cbs) : cbs
      const args = toArray(arguments, 1)
      const info =event handler for“${ event }for(let i=0,1=cbs.length;i<1;it+){
           invokewithErrorHandling(cbs[i], vim, args, Vm, info)
    }
}

:props怎么自定义验证

props: {
    num: {
      default: 1,
      validator: function (value) {
          // 返回值为true则验证不通过,报错
          return [
            1, 2, 3, 4, 5
          ].indexOf(value) !== -1
    }
    }
  }
  • 获取父子组件实例的方式 $parent``$children
    除了上面一种,我们也可以通过父子关系来调用,这里可以用 p a r e n t 、 parent、 parentchildren
    我可以找到它的爸爸和它的儿子,这个说明组件在初始化的时候会初始一个这样的父子关系。
    [源码]:core=>instance=>init.js=>lifeLifecyle()=>lifeLifecyle.js
    这里面会初始化一个父亲和儿子,这样就会知道我的父亲是谁,我的孩子是谁,这样我就可以拿到父实例和儿子实例,放的都是实例,所以可以拿到父子关系。
export function initl ifecycle (vm: Component) {
const options = vm. $options
// locate first non- abstract parent
let parent = options . parent
if (parent && !options . abstract) {
while (parent . $options. abstract && parent . $parent) {
parent = parent . $parent
}
parent . $chi ldren. push(vm)//放的都是实例
}
vm. $parent = parent
VM. $root = parent ? parent. $root : Vm
vm. $children-= [] 
vm.$refs = {]
Vm. watcher = null
vm. inactive = nul 1
vm._ directInactive = false
vm. isMounted = false

  • 在父组件中提供数据子组件进行消费 Provide、inject 插件
    同样会提供Provide、inject,这两个东西是写插件必备,开发时用的少。功能非常简单,我可以在父组件提供好数据,子组件可以把数据注入进来。
    【源】:core=>instance=>init.js=>initProvide(vm)andinitInjections(vm)``initProvide(vm)就是把当前的数据放到vm.$options,把当前提供的属性放到 当前实例的vm. provided,等会去调inject.js时候,这个方法很简单,就是不停的找父亲,有一个resolveInject,它会找父亲有没有vm. provided = typeof provide === ' function '这个属性,如果有这个属性,就把这个属性定义到自己的身上,而且是响应式的defineReactive(Vm, key, result[key]),这样父组件的数据一变就会更新子组件。【resolveInject方法里会做一个循环,不停的找爸爸看有没有这个属性,如果一旦找到了就return,做了一个查找的过程】
export function initProvide (vm: Component) {
       const provide = vm.$options.provide
       if (provide) {
                vm. provided = typeof provide === ' function '
               ? provide .call(vm)
               :provide
   }
}
export function initinjections (vm: Component) {
        const result = resolveInject( vm. $options. inject, vm)
        if (result) {
           toggleobserving(false)
           object. keys (result). forEach(key => {
             /* istanbul ignore else */
           if (process .env.NODE ENV !== ' production') {
                 defineReactive(vm, key, result[key], () => {
                        warn(
                 `Avoid mutating an injected value directly since the changes will be~ `+
                       `overwritten whenever the provided component re - renders.` +                                                                `injection being mutated: "${key}" `,
                                  vm
                           )
                  })
} else {
   defineReactive(Vm, key, result[key])
}
}export function resolveInject (inject: any, vm: Component): ?object {
         if (inject) {
           // inject is :any because flow is not smart enough to figure out cached
              const result = object.create(nu1l)
              const keys = hassymbol
                       ? Reflect . ownKeys(inject)
                       : object. keys(inject)
             for (let i = 0; i < keys.length; i++) {
               const key
             keys[i]
           // #6574 in case the inject object is observed...
             if (key === 'ob_') continue
           const provideKey = inject[key]. from
           let source = vm
         while (source) {
        if (source.. priovided 8&& hasown( source. provided, providekey)) {
      result[key] = source. provided[ provideKey ]
         break
         }
    source = source. $parent
       }
if (!source) {
      if ('default' in inject[key]) {
           const provideDefault = iniect「key] .default

  • Ref 获取实例的方式调用组件的属性或者方法
    Ref它也可以获取组件的实例,这种方式用的比较少,在组件用的比较多。如果给dom写的话,获取的是dom元素,如果给组件写的话,获取的是组件的实例。vnode . componentInstance II vnode .elm
ref.js
 export function registerRef (vnode : VNodewi thData, isRemoval:?boolean) {
       const key = vnode. data.ref
       if (!isDef(key)) return
      const Vm = vnode . context
      const ref = **vnode.componentInstance II vnode .elm**
      const refs = vm.$refs
      if (isRemoval) { 
          if (Array. isArray(refs[key])) {
             remove(refs[key], ref)
         } else if (refs[key] === ref) {
       refs[key] = undefined
        } else {
     if (vnode . data. refInFor) {
         if (!Array . isArray(refs[key])) {
          refs[key] = [ref ]
     } else if (refs[key]. index0f(ref) < 0) {
    // $flow-disable- line
   refs[key]. push(ref)
  }
   } else {
 refs[key] = ref
  • Event Bus 实现跨组件通信 Vue.prototype.$bus = new Vue
    Event Bus其实还是基于$on$emit,因为说了,每个实例都有$on$emit ,我们可以专门找一个实例去进行通信,比如说我创建个实例Vue.protoype.$busVue.protoype.$bus = newVue,那每个实例都可以拿到$bus属性,因为这个vue上有$on$emit,那我就可以绑定全局的事件,并触发事件,核心原理就是这句话,相当于我通过公共的实例通信,先明确我们绑定事件和触发事件必须在同一个实例上,所以我专门做了一个全局的来进行通信。
  • Vue.observable(废弃)
    用法:让一个对象可响应。
    (1)Vue 内部会用它来处理 data 函数返回的对象;
    (2)返回的对象可以直接用于渲染函数和计算属性内,并且会在发生改变时触发相应的更新;
    (3)也可以作为最小化的跨组件状态存储器,用于简单的场景。通讯原理实质上是利用Vue.observable实现一个简易的 vuex
// 文件路径 - /store/store.js
import Vue from 'vue'

export const store = Vue.observable({ count: 0 })
export const mutations = {
  setCount (count) {
    store.count = count
  }
}

//使用
<template>
    <div>
        <label for="bookNum">数 量</label>
            <button @click="setCount(count+1)">+</button>
            <span>{{count}}</span>
            <button @click="setCount(count-1)">-</button>
    </div>
</template>

<script>
import { store, mutations } from '../store/store' // Vue2.6新增API Observable

export default {
  name: 'Add',
  computed: {
    count () {
      return store.count
    }
  },
  methods: {
    setCount: mutations.setCount
  }
}
</script
  • Vuex 状态管理实现通信
    最靠谱的就是Vuex,我把数据全都存到一个容器里,大家组件一起去共享数据。
  • v-slot
    2.6.0 新增
    (1)slot,slot-cope,scope 在 2.6.0 中都被废弃,但未被移除
    (2)作用就是将父组件的 template 传入子组件
    (3)插槽分类:

A.匿名插槽(也叫默认插槽): 没有命名,有且只有一个;

// 父组件
<todo-list> 
    <template v-slot:default>
       任意内容
       <p>我是匿名插槽 </p>
    </template>
</todo-list> 

// 子组件
<slot>我是默认值</slot>
//v-slot:default写上感觉和具名写法比较统一,容易理解,也可以不用写

B.具名插槽: 相对匿名插槽组件slot标签带name命名的;

// 父组件
<todo-list> 
    <template v-slot:todo>
       任意内容
       <p>我是匿名插槽 </p>
    </template>
</todo-list> 

//子组件
<slot name="todo">我是默认值</slot>

C.作用域插槽: 子组件内数据可以被父页面拿到(解决了数据只能从父页面传递给子组件)

// 父组件
<todo-list>
 <template v-slot:todo="slotProps" >
   {{slotProps.user.firstName}}
 </template> 
</todo-list> 
//slotProps 可以随意命名
//slotProps 接取的是子组件标签slot上属性数据的集合所有v-bind:user="user"

// 子组件
<slot name="todo" :user="user" :test="test">
    {{ user.lastName }}
 </slot> 
data() {
    return {
      user:{
        lastName:"Zhang",
        firstName:"yue"
      },
      test:[1,2,3,4]
    }
  },
// {{ user.lastName }}是默认数据  v-slot:todo 当父页面没有(="slotProps")
  • 路由传参
    方案二参数不会拼接在路由后面,页面刷新参数会丢失
    方案一和三参数拼接在后面,丑,而且暴露了信息
    (1)方案一
// 路由定义
{
  path: '/describe/:id',
  name: 'Describe',
  component: Describe
}
// 页面传参
this.$router.push({
  path: `/describe/${id}`,
})
// 页面获取
this.$route.params.id

(2)方案二

// 路由定义
{
  path: '/describe',
  name: 'Describe',
  omponent: Describe
}
// 页面传参
this.$router.push({
  name: 'Describe',
  params: {
    id: id
  }
})
// 页面获取
this.$route.params.id

(3)方案三

// 路由定义
{
  path: '/describe',
  name: 'Describe',
  component: Describe
}
// 页面传参
this.$router.push({
  path: '/describe',
    query: {
      id: id
  `}
)
// 页面获取
this.$route.query.id
  • $attrs $listeners

小结

  • 父子关系的组件数据传递选择 props 与 $emit进行传递,也可选择ref
  • 兄弟关系的组件数据传递可选择EventBus ($emit / $on),其次可以选择$parent进行传递
  • 祖先与后代组件数据传递可选择attrs与listeners或者 Provide与 Inject(插件)
  • 复杂关系的组件数据传递可以通过vuex存放共享的变量

(4)provide/Inject 和 prop 的区别?provide能替换掉prop吗?★★★

  1. props一般是绑定到元素上面的;而provide/inject是成对使用的

  2. props是响应式的,provide/inject不是响应式的。

  3. props可以通过.sync指令进行双向绑定

  4. props主要是数值绑定,provide/inject可以绑定方法
    provide 选项应该是一个对象或返回一个对象的函数。一般用于父子组件传值。提供了一些数据校验的参数。Inject是子组件注入父组件的provid;

在这里插入图片描述
在这里插入图片描述

参考阅读

  1. provide/Inject 和 prop的区别
  2. 官网provide / inject

(5).详解vue组件三大核心概念

详解vue组件三大核心概念

(6).如何优雅地监听子组件生命周期钩子

参考阅读:VUE @hook浅析(监听子组件的生命周期钩子)
通常我们监听组件生命周期会使用 $emit ,父组件接收事件来进行通知

子组件

export default {
    mounted() {
        this.$emit('listenMounted')
    }
}

父组件

<template>
    <div>
        <List @listenMounted="listenMounted" />
    </div>
</template>

其实还有一种简洁的方法,使用 @hook 即可监听组件生命周期,组件内无需做任何改变。同样的, created 、 updated 等也可以使用此方法。

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

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

(7).组件的种类

①为什么要使用异步组件?

官网地址
理解:

如果组件功能多打包出的结果会变大,我可以采用异步的方式来加载组件。主要依赖 import()【es7】 这 个语法,可以实现文件的分割加载,是核心优化之一
可以减少打包的结果,它会把我们的异步组件分开打包,而且会采用jcp的方式进行加载,可以有效解决一个文件过大的问题,比如说页面里有个非常大的组件,组件里有个非常大的图表eachars这样一个组件其实刚开始不用加载出来, 而是异步加载出来,这时候就可以使用异步组件,异步组件的核心就是,我们可以给组件的定义变成一个函数,函数里可以用
import()语法,这个语法主要是Webpack提供的。

components:{
AddCustomerSchedule:(resolve)=>import("../components/AddCustomer") // require([])
}

原理:
刚开始组件如果是一个函数的话, 会调用
resolveAsyncComponent方法,异步组件一定是一 个函数(新版,提供了返回对象写法),把这个函数传进去之后,它会让asyncFactory马上执行,即impor(".components/AddCustomer"),但不会马上返回结果,因为它是异步的,返回的是promise,不会马上执行就没有返回值,没有返回值这个值就是undefined,就会先渲染异步组件的占位符createAsyncPlaceholder,说白了就是一个注释<!-->,如果加载完之后,如果这个东西是promise,factory执行,会传一个成功的回调和失败的回调,promise成功就会调resolve,resolve就干了一件事情,非常简单叫强制更新 forceRender(true),内部是$forceUpdate方法。同时会把刚才的结果放到factory.resolved,强制刷新的时候会再走到刚才的resolveAsyncComponent,这时候有个判断如果当前已经成功了,就把成功的结果返回回去,这样采取执行代码的时候返回的结果就不是undefined了。接着是创建组件、初始化组件、去渲染组件。如果失败,一样强制刷新。所以主要核心是更新完之后强制刷新一下。在返回期间还可以配置loading。

export function (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
// async component
   let asyncFactory
   if (isUndef(Ctor.cid)) {
   asyncFactory = Ctor//异步组件一定是一个函数,新版本提供了返回对象那个的方法
   Ctor = resolveAsyncComponent(asyncFactory, baseCtor) // 默认调用此函数时返回
undefiend
// 第二次渲染时Ctor不为undefined
if (Ctor === undefined) {
return createAsyncPlaceholder( // 渲染占位符 空虚拟节点<!-->
        asyncFactory,
        data,
        context,
        children,
        tag
        )
     }
   }
}
function resolveAsyncComponent (
    factory: Function,
    baseCtor: Class<Component>
): Class<Component> | void {
 if (isDef(factory.resolved)) { // 3.在次渲染时可以拿到获取的最新组件
    return factory.resolved
}
const resolve = once((res: Object | Class<Component>) => {
     factory.resolved = ensureCtor(res, baseCtor)
if (!sync) {
     forceRender(true) //2. 强制更新视图重新渲染 内部$forceUpdate
} else {
   owners.length = 0
}
})
const reject = once(reason => {
   if (isDef(factory.errorComp)) {
     factory.error = true
    forceRender(true)
}
})
const res = factory(resolve, reject)// 1.将resolve方法和reject方法传入,用户调用
resolve方法后
sync = false
return factory.resolved
}
②函数式组件

优点: 函数式组件是无状态,它无法实例化,没有任何的生命周期和方法。创建函数式组件也很简单,只需要在模板添加 functional 声明即可。一般适合只依赖于外部数据的变化而变化的组件,因其轻量,渲染性能也会有所提高。
组件需要的一切都是通过 context 参数传递。它是一个上下文对象,具体属性查看文档。这里 props 是一个包含所有绑定属性的对象。

函数式组件

<template functional>
    <div class="list">
        <div class="item" v-for="item in props.list" :key="item.id" @click="props.itemClick(item)">
            <p>{{item.title}}</p>
            <p>{{item.content}}</p>
        </div>
    </div>
</template>

父组件使用

<template>
    <div>
        <List :list="list" :itemClick="item => (currentItem = item)" />
    </div>
</template>
import List from  @/components/List.vue
export default {
    components: {
        List
    },
    data() {
        return {
            list: [{
                title:  title ,
                content:  content
            }],
            currentItem:
        }
    }
}

10个略骚的 Vue 开发技巧

③.动态组件

场景:做一个 tab 切换时就会涉及到组件动态加载

<component v-bind:is="currentTabComponent"></component>

但是这样每次组件都会重新加载,会消耗大量性能,所以 就起到了作用

<keep-alive>
  <component v-bind:is="currentTabComponent"></component>
</keep-alive>

这样切换效果没有动画效果,这个也不用着急,可以利用内置的

<transition>
  <keep-alive>
    <component v-bind:is="currentTabComponent"></component>
  </keep-alive>
</transition>
④递归组件

场景:如果开发一个 tree 组件,里面层级是根据后台数据决定的,这个时候就需要用到递归组件

// 递归组件: 组件在它的模板内可以递归的调用自己,只要给组件设置name组件就可以了。
// 设置那么House在组件模板内就可以递归使用了,不过需要注意的是,
// 必须给一个条件来限制数量,否则会抛出错误: max stack size exceeded
// 组件递归用来开发一些具体有未知层级关系的独立组件。比如:
// 联级选择器和树形控件 
<template>
  <div v-for="(item,index) in treeArr">
      子组件,当前层级值: {{index}} <br/>
      <!-- 递归调用自身, 后台判断是否不存在改值 -->
      <tree :item="item.arr" v-if="item.flag"></tree>
  </div>
</template>
<script>
export default {
  // 必须定义name,组件内部才能递归调用
  name: 'tree',
  data(){
    return {}
  },
  // 接收外部传入的值
  props: {
     item: {
      type:Array,
      default: ()=>[]
    }
  }
}
</script>

递归组件必须设置name 和结束的阀值

⑤抽象组件

keep-alive

(10).封装组件你必须知道的

封装组件小技巧一:样式里使用js变量法

【场景说明】

在使用vue开发时,经常会封装很多的组件方便复用,那么难免就有写样式相关组件,比如需要使用时传入颜色、高度等样式参数。
那么问题来了:我们要怎么在样式中使用组件中接收的参数呢?或者说,我们要怎么在样式中使用data中的变量呢?

【代码演示】

这个用法真的简单,没什么其他的知识点,直接上代码:

<template>
  <div class="box" :style="styleVar">
  </div>
</template>
<script>
export default {
  props: {
    height: {
      type: Number,
      default: 54,
    },
  },
  computed: {
    styleVar() {
      return {
        '--box-height': this.height + 'px'
      }
    }
  },
}
</script>
<style scoped>
.box {
  height: var(--box-height);
}
</style>

这样我们就在vue中实现了 在样式里使用js变量的方法: 即通过css定义变量的方式,将变量在行内注入,然后在style中使用var()获取我们在行内设置的数据即可。以后,在封装一些需要动态传入样式参数的ui组件是不是简便了不少。

原文地址:https://juejin.cn/post/6911662617178144776

②封装组件小技巧二:自定义组件双向绑定

【场景说明】: 组件 model 选项

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

input 默认作为双向绑定的更新事件,通过 $emit 可以更新绑定的值

<my-switch v-model="val"></my-switch>
export default {
    props: {
        value: {
            type: Boolean,
            default: false
        }
    },
    methods: {
        switchChange(val) {
            this.$emit( input , val)
        }
    }
}

修改组件的 model 选项,自定义绑定的变量和事件

<my-switch v-model="num" value="some value"></my-switch>
export default {
    model: {
        prop:  num ,
        event:  update
    },
    props: {
        value: {
            type: String,
            default:
        },
        num: {
            type: Number,
            default: 0
        }
    },
    methods: {
        numChange() {
            this.$emit( update , this.num++)
        }
    }
}

③自动注册组件
我们平时可能这样引入注册组件。每次都得需要在头部引入,然后注册,最后在模板上使用。

<template>
  <div id="app">
    <HelloWorld msg="Welcome to Your Vue.js App"/>
  </div>
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'

export default {
  name: 'App',
  components: {
    HelloWorld
  }
}
</script>

那么,有没有更加方便快捷的方法呢?我们不妨这样。

创建一个名为globalRC.js文件,假设我们这里与组件平级,即存放在组件文件夹中。

目录结构如:

-src
--components
---component1.vue
---globalRC.js

globalRC.js:

import Vue from 'vue'

function changeStr (str){
    return str.charAt(0).toUpperCase() + str.slice(1);
}

const requireComponent = require.context('./',false,/\.vue$/); // './'操作对象为当前目录

requireComponent.keys().forEach(element => {
    const config = requireComponent(element);

    const componentName = changeStr(
        element.replace(/^\.\//,'').replace(/\.\w+$/,'')
    )
    
    Vue.component(componentName, config.default || config)
});

然后,我们需要在main.js文件中引入。

import './components/globalRC.js'

最后,我们只需要在模板直接使用就可以了。

<template>
  <div id="app">
    <Component1 />
  </div>
</template>

<script>
export default {
  name: 'App'
}
</script>

注意,require.context是webpack的一个API,所以,需要基于webpack环境才可以使用。

④封装Vue第三方组件的三板斧
组件的拆分粒度是越细越好吗?
封装Vue第三方组件的三板斧
一个透传技巧,治好了我的重度代码洁癖
如何修改组件库源码来封装符合自己需求的组件?
vue3优雅实现移动端登录注册模块
如何搭建一个完美的组件库?
做好这三个关键点就可以更好的实现前端业务组件库
手把手教你写一个Vue组件发布到npm且可外链引入使用
怎样设计一个可扩展、通用的、健壮性组件?

(11).项目中组件的自动加载和使用方式

自动加载
在我们的项目中,往往会使用的许多组件,一般使用频率比较高的组件为了避免重复导入的繁琐一般是作为全局组件在项目中使用的。而注册全局组件我们首先需要引入组件,然后使用Vue.component进行注册;这是一个重复的工作,我们每次创建组件都会进行,如果我们的项目是使用webpack构建(vue-cli也是使用webpack),我们就可以通过require.context自动将组件注册到全局。创建components/index.js文件:

export default function registerComponent (Vue) {
  /**
   * 参数说明:
   * 1. 其组件目录的相对路径
   * 2. 是否查询其子目录
   * 3. 匹配基础组件文件名的正则表达式
   **/
  const modules = require.context('./', false, /\w+.vue$/)
  modules.keys().forEach(fileName => {
    // 获取组件配置
    const component = modules(fileName)
    // 获取组件名称,去除文件名开头的 `./` 和结尾的扩展名
    const name = fileName.replace(/^\.\/(.*)\.\w+$/, '$1')
    // 注册全局组件
    // 如果这个组件选项是通过 `export default` 导出的,
    // 那么就会优先使用 `.default`,
    // 否则回退到使用模块的根。
    Vue.component(name, component.default || component)
  })
}

之后在main.js中导入注册模块进行注册,使用require.context我们也可以实现vue插件和全局filter的导入。

import registerComponent from './components'
registerComponent(Vue)

通过插件的方式来使用组件

在很多第三方组件库中,我们经常看到直接使用插件的方式调用组件的方式,比如VantUI的Dialog弹出框组件,我们不但可以使用组件的方式进行使用,也可以通过插件的形式进行调用。

this.$dialog.alert({
  message: '弹窗内容'
});

将组件作为插件使用的原理其实并不复杂,就是使用手动挂载Vue组件实例。

import Vue from 'vue';
export default function create(Component, props) {
    // 先创建实例
    const vm = new Vue({
        render(h) {
            // h就是createElement,它返回VNode
            return h(Component, {props})
        }
    }).$mount();
    // 手动挂载
    document.body.appendChild(vm.$el);
    // 销毁方法
    const comp = vm.$children[0];
    comp.remove = function() {
        document.body.removeChild(vm.$el);
        vm.$destroy();
    }
    return comp;
}

调用create传入组件和props参数就可以获取组件的实例,通过组件实例我们就可以调用组件的各种功能了。

<template>
  <div class="loading-wrapper" v-show="visible">
    加载中
  </div>
</template>
<script>
export default {
  name: 'Loading',
  data () {
    return {
      visible: false
    }
  },
  methods: {
    show () {
      this.visible = true
    },
    hide () {
      this.visible = false
    }
  }
}
</script>
<style lang="css" scoped>
.loading-wrapper {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 100%;
  background-color: rgba(0, 0, 0, .4);
  z-index: 999;
}
</style>
<!--使用-->
const loading = create(Loading, {})
loading.show() // 显示
loading.hide() // 关闭

第三方组件

移动端各种组件、插件已经相对完善,在项目开发中重复造轮子是一件很不明智的事情;开发项目时我们可以借助第三方组件、插件提高我们的开发效率。

常用组件库
VantUI是有赞开源的一套轻量、可靠的移动端Vue组件库;支持按需引入、主题定制、SSR,除了常用组件外,针对电商场景还有专门的业务组件,如果是开发电商项目的话,推荐使用。官方文档关于主题定制是在webpack.config.js中进行设置的:

// webpack.config.js
module.exports = {
  rules: [
    {
      test: /\.less$/,
      use: [
        // ...其他 loader 配置
        {
          loader: 'less-loader',
          options: {
            modifyVars: {
              // 直接覆盖变量
              'text-color': '#111',
              'border-color': '#eee'
              // 或者可以通过 less 文件覆盖(文件路径为绝对路径)
              'hack': `true; @import "your-less-file-path.less";`
            }
          }
        }
      ]
    }
  ]
};

但我们的项目可能是使用vue-cli构建,这时我们需要在vue.config.js中进行设置:

module.exports = {
  css: {
    loaderOptions: {
      less: {
        modifyVars: {
          'hack': `true; @import "~@/assets/less/vars.less";`
        }
      }
    }
  }
}

另外vux、mint-ui也是很好的选择。

常用插件

better-scroll是一个为移动端各种滚动场景提供丝滑的滚动效果的插件,如果在vue中使用可以参考作者的文章当 better-scroll 遇见 Vue。

swiper是一个轮播图插件,如果是在vue中使用可以直接使用vue-awesome-swiper,vue-awesome-swiper基于Swiper4,并且支持SSR。

24.Vue中事件绑定的原理

Vue 的事件绑定分为两种一种是原生的事件绑定,还有一种是组件的事件绑定
原生事件绑定是通过 addEventListener 绑定给真实元素的,组件事件绑定是通过 Vue 自定义的$on 实现的。如果要在组件上使用原生事件,需要加.native 修饰符,这样就相当于在父组件中把子组件当做普通 html 标签,然后加上原生事件。 $on$emit
是基于发布订阅模式的,维护一个事件中心,on 的时候将事件按名称存在事件中心里,称之为订阅者,然后 emit 将对应的事件进行发布,去执行事件中心里的对应的监听器

理解:

  • 原生 dom 事件的绑定,采用的是 addEventListener 实现
  • 组件绑定事件采用的是 $on 方法

(1)原生 dom 的绑定

事件参数:$event 是事件对象的一个特殊变量。它在某些场景下为复杂的功能提供了更多的可选参数。
在原生事件中,该值与默认事件(DOM事件或窗口事件)相同。

<template>
  <input type="text" @input="handleInput('hello', $event)" />
</template>

<script>
export default {
  methods: {
    handleInput (val, e) {
      console.log(e.target.value) // hello
    }
  }
}
</script>

(2)自定义事件(组件中绑定事件)
在自定义事件中,该值是从其子组件中捕获的值。

<!-- Child -->
<template>
  <input type="text" @input="$emit('custom-event', 'hello')" />
</template>

<!-- Parent -->
<template>
  <Child @custom-event="handleCustomevent" />
</template>

<script>
export default {
  methods: {
    handleCustomevent (value) {
      console.log(value) // hello
    }
  }
}
</script>

原理:
一般绑事件有两种关系,第一种给普通标签绑定事件还有一种给组件绑定事件,而且事件有两种绑法,一种是click.native,这种绑的就是原生事件,还有一种是@click="fn1",这种事件叫组件的自定义事件,这两种结果是不一样的,如代码打印所示,如果是原生的div,编译结果是一个 {on:{click}},如果是组件的话,是nativeOn:{click},完了on是一个click。为什么组件要加nativeOn属性?先明确,最终组件会把nativeOn属性放到On里去,完了这个on会单独处理。相当于组件里的nativeOn等价于普通元素里的On,组件里的on会单独处理。

  • 事件的编译:
let compiler = require('vue-template-compiler');//vue-loader
let r1 = compiler.compile('<div @click="fn()"></div>');
let r2 = compiler.compile('<my-component @click.native="fn" @click="fn1"></mycomponent>');
console.log(r1.render); // {on:{click}}
console.log(r2.render); // {nativeOn:{click},on:{click}}

创建组件的时候会找data里有没有nativeOn,它会把on赋到listeners上,这就是组件自己绑定的事件,把data里的nativeOn赋给on,nativeOn等价于on,on会单独处理。
vdom/create-components.js

// extract listeners, since these needs to be treated as
// child component listeners instead of DOM listeners
const listeners = data.on
// replace with listeners with . native modifier
// So it gets processed during parent C omponent patch .
data.on = data.nativeOn
if (isTrue(Ctor.opt ions . abstract)) {
// abstract components do not keep anything
// other than props & listeners & slot

如何绑定事件的?
我给div绑定一个原生事件叫say,给test帮了一个native,又绑了一个click。来看一看绑定的原则,第一次它绑定的事件的是div元素,div元素会把on取出来,在这里会调一个方法 叫updatelisteners,这个主要是加一个监听事件的,监听事件里主要有一个add$1方法,add方法就是怎么给这个div绑事件的,接着看最核心addEventListener方法,也就是说它给普通dom元素绑事件的话是直接把这个事件帮给这个div,它和react不一样,react有事件代理,vue直接绑给这个元素,这时候看看组件,组件的方法叫updateComponentListeners它里面传的也是add,方法都是一个,只是最后绑定方法采用的策略不一样,组件绑定采用的add用的是它自己定义的发布订阅模式,用的是它内部的$on方法。同样内部解析的是on方法,同样还有个nativeOn,listenners,同样还有一个事件同样会给组件的最外层元素nativeOn用的策略也是addEventListeners,所以绑定事件的方法就两种一种是addEventListeners,还有一种就是$on方法,所以组件里的方法就分析清楚了,所以说普通元素的事件绑定方法<div @click = "say">和组件的原生事件绑定<test @click.native="()=>{}">是同一个方法。所以组件中的<test @click="()=>{}"></test>会变成test.$on('click',()=>{}),所以在组件内部可以通过test.$emit('click')触发它的事件.所以这样的写法<div v-for="i in 100"><div @click></div></div>性能一定不是很好,相当于给这个div循环100次,每个人都用addEventListeners去绑定一下,所以遇到这样的问题。所以把@click放到外层,<div @click=""><div v-for = "i in 100"><div></div></div></div>通过事件代理的方式。在这里插入图片描述

<div>
      <div @click="say">说话</div>//等价于
      <test @click.native="()=>{}"></test>
</div>
//性能不好
<div v-for="i in 100">
     <div @click></div>
</div>
//事件代理提高性能
<div @click="">
      <div v-for = "i in 100">
      <div></div>
      </div>
</div>

在这里插入图片描述

  • Vue 在创建真是 dom 时会调用 createElm ,默认会调用 invokeCreateHooks

会遍历当前平台下相对的属性处理代码,其中就有 updateDOMListeners 方法,内部会传入 add 方法

function updateDOMListeners (oldVnode: VNodeWithData, vnode: VNodeWithData) {
 if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {
  return
 }
const on = vnode.data.on || {}
const oldOn = oldVnode.data.on || {}
 target = vnode.elm
 normalizeEvents(on)
 updateListeners(on, oldOn, add, remove, createOnceHandler, vnode.context)
 target = undefined
}
function add (
 name: string,
 handler: Function,
 capture: boolean,
 passive: boolean
) {
target.addEventListener( // 给当前的dom添加事件
 name,
 handler,
 supportsPassive
? { capture, passive }
: capture
)
}

vue 中绑定事件是直接绑定给真实dom元素的

export function updateComponentListeners (
  vm: Component,
  listeners: Object,
  oldListeners: ?Object
) {
  target = vm
   updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler,
vm)
  target = undefined
}
function add (event, fn) {
   target.$on(event, fn)
}

组件绑定事件是通过 vue 中自定义的 $on 方法来实现的

25.程序化的事件侦听器

比如,在页面挂载时定义计时器,需要在页面销毁时清除定时器。这看起来没什么问题。但仔细一看 this.timer 唯一的作用只是为了能够在 beforeDestroy 内取到计时器序号,除此之外没有任何用处。

export default {
    mounted() {
        this.timer = setInterval(() => {
            console.log(Date.now())
        }, 1000)
    },
    beforeDestroy() {
        clearInterval(this.timer)
    }
}

如果可以的话最好只有生命周期钩子可以访问到它。这并不算严重的问题,但是它可以被视为杂物。

我们可以通过 $on 或 $once 监听页面生命周期销毁来解决这个问题:

export default {
    mounted() {
        this.creatInterval('hello')
        this.creatInterval('world')
    },
    creatInterval(msg) {
        let timer = setInterval(() => {
            console.log(msg)
        }, 1000)
        this.$once('hook:beforeDestroy', function() {
            clearInterval(timer)
        })
    }
}

使用这个方法后,即使我们同时创建多个计时器,也不影响效果。因为它们会在页面销毁后程序化的自主清除。

26.Vue中相同逻辑如何抽离?

1.官网

- Vue.mixin 用法给组件每个生命周期,函数等都混入一些公共逻辑。
Vue.mixin可以给每个生命周期混入公共逻辑,源代码非常简单,mergeOptions可以帮我们合并数据、合并方法、合并生命周期,这样的话我们可以在每个组件里都增加一些公共方法,或者公共的生命周期或公共的一些事。有很多合并策略,比如说data的合并策略,比如说,生命周期的合并策略。mixin可以全局使用也可以放到组件种使用。
1.1. Mixin混入
1.1.1. 认识Mixin
目前我们是使用组件化的方式在开发整个Vue的应用程序,但是组件和组件之间有时候会存在相同的代码逻辑,我们希望对相同的代码逻辑进行抽取。

在Vue2和Vue3中都支持的一种方式就是使用Mixin来完成。

Mixin提供了一种非常灵活的方式,来分发Vue组件中的可复用功能;
一个Mixin对象可以包含任何组件选项;
当组件使用Mixin对象时,所有Mixin对象的选项将被 混合 进入该组件本身的选项中;
比如我们封装一个mixin的对象在sayHelloMixin.js文件中:

const sayHelloMixin = {
  created() {
    this.sayHello();
  },
  methods: {
    sayHello() {
      console.log("Hello Page Component");
    }
  }
}

export default sayHelloMixin;

之后,在Home.vue中通过mixins的选项进行混入:

<template>
  <div>

  </div>
</template>

<script>
  import sayHelloMixin from '../mixins/sayHello';

  export default {
    mixins: [sayHelloMixin]
  }
</script>

<style scoped>

</style>

1.1.2. Mixin合并
如果Mixin对象中的选项和组件对象中的选项发生了冲突,那么Vue会如何操作呢?

这里分成不同的情况来进行处理;
情况一:如果是data函数的返回值对象

返回值对象默认情况下会进行合并;
如果data返回值对象的属性发生了冲突,那么会保留组件自身的数据;
mixin中的代码:

const sayHelloMixin = {
  data() {
    return {
      name: "mixin",
      age: 18
    }
  }
}

export default sayHelloMixin;

Home.vue中的代码:

<script>
  import sayHelloMixin from '../mixins/sayHello';

  export default {
    mixins: [sayHelloMixin],
    data() {
      return {
        message: "Hello World",
        // 冲突时会保留组件中的name
        name: "home"
      }
    }
  }
</script>

情况二:如果是生命周期钩子函数

生命周期的钩子函数会被合并到数组中,都会被调用;
mixin中的代码:

const sayHelloMixin = {
  created() {
    console.log("mixin created")
  }
}


export default sayHelloMixin;

Home.vue中的代码:

<script>
  import sayHelloMixin from '../mixins/sayHello';

  export default {
    mixins: [sayHelloMixin],
    created() {
      console.log("home created");
    }
  }
</script>

情况三:值为对象的选项,例如 methods、components 和 directives,将被合并为同一个对象。

比如都有methods选项,并且都定义了方法,那么它们都会生效;
但是如果对象的key相同,那么会取组件对象的键值对;
比如下面的代码中,最终methods对象会被合并成一个对象;
mixin中的代码:

const sayHelloMixin = {
  methods: {
    sayHello() {
      console.log("Hello Page Component");
    },
    foo() {
      console.log("mixin foo function");
    }
  }
}

export default sayHelloMixin;

Home.vue中的代码:

<script>
  import sayHelloMixin from '../mixins/sayHello';

  export default {
    mixins: [sayHelloMixin],
    methods: {
      foo() {
        console.log("mixin foo function");
      },
      bar() {
        console.log("bar function");
      }
    }
  }
</script>

1.1.3. 全局Mixin
如果组件中的某些选项,是所有的组件都需要拥有的,那么这个时候我们可以使用全局的mixin:

全局的Mixin可以使用 应用app的方法 mixin 来完成注册;
一旦注册,那么全局混入的选项将会影响每一个组件;

import { createApp } from "vue";
import App from "./14_Mixin混入/App.vue";

const app = createApp(App);
app.mixin({
  created() {
    console.log("global mixin created");
  }
})
app.mount("#app");

1.2. extends
另外一个类似于Mixin的方式是通过extends属性:

允许声明扩展另外一个组件,类似于Mixins;
我们开发一个HomePage.vue的组件对象:

<script>
  export default {
    data() {
      return {
        message: "Hello Page"
      }
    }
  }
</script>

在Home.vue中我们可以继承自HomePage.vue:

注意:只可以继承自对象中的属性,不可以继承模板和样式等;

<script>
  import BasePage from './BasePage.vue';

  export default {
    extends: BasePage
  }
</script>

在开发中extends用的非常少,在Vue2中比较推荐大家使用Mixin,而在Vue3中推荐使用Composition API。

所以有两种区别【源码分析】:如果放到组件中,可以找组件里的mixin,看有没有,如果组件里有mixin的话,会递归合并。如果写了extends,也会对extends进行merge,但这个用的不是很多。核心还是在mergeOptions,它会合并一些字段,比如:我下面这样写,它会给每个组件都增加一个beforeCreate(),怎么做到的非常简单?它弄个数组,先把mixin放到里边,再把当前组件放到后边去,这样执行callhook
callhook时候会循环这个数组,让数组依次执行。所以它最终合并的是一个数组。它会去循环里面每一项去调mergehooks,mergehook作用:就是看当前你这孩子有没有,如果要是有的话,看看爸爸有没有,如果都有话,把这两个合并起来,而且是concat。如果没有爸爸,就看看是不是数组,是数组直接返回,不是数组包装成数组,这样在合并的时候,把每个生命周期都做成一个数组,执行的时候会依次执行。合并策略的还用很多,包括data的合并、props、watch、methods【util=>options.js241】,都不一样。
【证明】搜LIFESYLE_HOOKS=>util=>options.js[172152]

Vue.mixin({
   beforeCreate(){// [beforeCrete]
}})

【源码】:core=>global-api=>mimin.js

Vue.mixin = function (mixin: Object) {
    this.options = mergeOptions(this.options, mixin); // 将当前定义的属性合并到每个
组件中
    return this
}
export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  if (!child._base) {
    if (child.extends) { // 递归合并extends
      parent = mergeOptions(parent, child.extends, vm)
   }
    if (child.mixins) { // 递归合并mixin
      for (let i = 0, l = child.mixins.length; i < l; i++) {
        parent = mergeOptions(parent, child.mixins[i], vm)
     }
   }
 }
  const options = {} // 属性及生命周期的合并
  let key
  for (key in parent) {
    mergeField(key)
 }
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
   }
 }
  function mergeField (key) {
    const strat = strats[key] || defaultStrat
    // 调用不同属性合并策略进行合并
    options[key] = strat(parent[key], child[key], vm, key)
 }
  return options
}

[问题小结]
①创建一个实例就会自动走mixin方法。
global-api=>extend.js[39~44]每次一做都会自动创建生命周期,这是子类默认有的。如果合并我就自己调mixin
②如果使用mixin会很乱,缺点是数据的来源不知道去哪里找,会导致我们项目的问题。所以Vue3才把它变成compositionAPI,react就是高阶组件。

Vue.mixin({
data(){
},
methods:{
},

})

③组件里mixin

let component ={
    template,
    mixin:[xxx]
}

mixin和生命周期哪个先执行?
先走mixin的,mixin放到前面,组件的放到后边去。区别是一个是全局的一个是局部的。建议项目中少用但是写插件必备,比如vue-routervuex.

2.说说你对vue的mixin的理解,有哪些应用场景?
3.深入浅出 Vue Mixin

27.Vue中常见性能优化

(一)首屏加载速度慢怎么解决?(缩短白屏时间)

在这里插入图片描述

面试官:SPA(单页应用)首屏加载速度慢怎么解决?
Vue 项目性能优化 — 实践指南(网上最全 / 详细)

(一)编码优化

(1)组件方面

  • SPA 页面采用keep-alive缓存组件
  • 合理拆分组件( 提高复用性、增加代码的可维护性,减少不必要的渲染 )
  • 合理使用异步组件/函数式组件
    组件的拆分粒度是越细越好吗

(2)指令相关

  • 区分v-if 和 v-show 使用场景。v-if 当值为false时内部指令不会执行,具有阻断功能,很多情况下使用v-if替代v-show
  • vue 在 v-for 时给每项元素绑定事件需要用事件代理
  • v-for 遍历必须为 item 添加 key,且避免同时使用 v-if,key 保证唯一性 ( 默认 vue 会采用就地复用策略 )

(3)data相关

  • 区分computed 和 watch 使用场景 ,computed 适合在计算和依赖场景使用,watch 适合数据变化的异步操作

  • 不要将所有的数据都放在data中,data中的数据都会增加getter和setter,会收集对应的 watcher

  • Vue中避免滥用this去读取data中数据

  • 长列表性能优化

    非响应式数据Object.freeze 冻结数据 [es5]
    data有个特点,所有的数据都可能被劫持,所以尽可能将数据扁平化,当数据非常长非常大,而且数据只是用来渲染的话,可用使用非常重要的语法Object.freeze,它可以把数据冻结起来,这样就不能增加gettersetter了。
    超长列表性能优化
    前端虚拟列表的实现原理
    vue-virtual-scroll-list:默认最多只渲染三屏,当前看到的和上一页还有下一页。看不到的用空div撑起来,这样可以控制DOM的数量。

(4)加载方面

  • 第三方模块按需导入 ( babel-plugin-component )
    很多三方插件包很大,所以它们大部分也都给了按需加载的使用说明

  • 合理使用路由懒加载,图片懒加载
    路由懒加载 :Vue 是单页面应用,可能会有很多的路由引入 ,这样使用 webpcak 打包后的文件很大,当进入首页时,加载的资源过多,页面会出现白屏的情况,不利于用户体验。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应的组件,这样就更加高效了。这样会大大提高首屏显示的速度,但是可能其他的页面的速度就会降下来。component: () => import("./views/Login.vue")
    图片懒加载图片资源懒加载可以使用 vue-lazyload 插件,保证只加可视区域内的图片。 用的是image,unload事件,先做预加载,再去加载图片,也是看可视区域的,都是判断offset,clientset。

  • event 在页面上使用了事件之后,离开的时候最好把事件给销毁(全局除外),可以防止内存泄漏(比如图表的加载和重绘)

  • 数据持久化的问题 (防抖、节流)
    写任何代码尽可能的少执行,尽可能的不执行
    防抖:不停的执行最终执行一次
    节流:不停的执行按特点的时间来执行

  • 尽量采用runtime运行时版本

(二)Webpack 层面的优化

(1)Webpack 对图片进行压缩
在 vue 项目中除了可以在 webpack.base.conf.js 中 url-loader 中设置 limit 大小来对图片处理,对小于 limit 的图片转化为 base64 格式,其余的不做操作。所以对有些较大的图片资源,在请求资源的时候,加载会很慢,我们可以用 image-webpack-loader来压缩图片:

  • 首先,安装 image-webpack-loader :
npm install image-webpack-loader --save-dev
  • 然后,在 webpack.base.conf.js 中进行配置:
{
  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 代码:

class HelloWebpack extends Component{...}

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

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

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

  • 首先,安装 babel-plugin-transform-runtime
npm install babel-plugin-transform-runtime --save-dev
  • 然后,修改 .babelrc 配置文件为:
"plugins": [
    "transform-runtime"
]

如果要看插件的更多详细内容,可以查看babel-plugin-transform-runtime 的 详细介绍。

(3)提取公共代码

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

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

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

// 所有在 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']
})

如果要看插件的更多详细内容,可以查看 CommonsChunkPlugin 的 详细介绍。

(4)模板预编译

当使用 DOM 内模板或 JavaScript 内的字符串模板时,模板会在运行时被编译为渲染函数。通常情况下这个过程已经足够快了,但对性能敏感的应用还是最好避免这种用法。

预编译模板最简单的方式就是使用单文件组件——相关的构建设置会自动把预编译处理好,所以构建好的代码已经包含了编译出来的渲染函数而不是原始的模板字符串。

如果你使用 webpack,并且喜欢分离 JavaScript 和模板文件,你可以使用 vue-template-loader,它也可以在构建过程中把模板文件转换成为 JavaScript 渲染函数。

(5)提取组件的 CSS

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

查阅这个构建工具各自的文档来了解更多:

  • webpack + vue-loader ( vue-cli 的 webpack 模板已经预先配置好)
  • Browserify + vueify
  • Rollup + rollup-plugin-vue

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

SourceMap 的可选值如下(+ 号越多,代表速度越快,- 号越多,代表速度越慢, o 代表中等速度 )
在这里插入图片描述
开发环境推荐: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 。
我们在项目中 webpack.prod.conf.js 进行配置:

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

执行 $ npm run build --report 后生成分析报告如下:
在这里插入图片描述
(8).Vue 项目的编译优化
如果你的 Vue 项目使用 Webpack 编译,需要你喝一杯咖啡的时间,那么也许你需要对项目的 Webpack 配置进行优化,提高 Webpack 的构建效率。具体如何进行 Vue 项目的 Webpack 构建优化,可以参考作者的另一篇文章《 Vue 项目 Webpack 优化实践》

(三)基础的 Web 技术优化

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

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

  • 安装:
npm install compression --save
  • 添加代码逻辑:
var compression = require('compression');
var app = express();
app.use(compression())
  • 重启服务,观察网络面板里面的 response header,如果看到如下红圈里的字段则表明 gzip 开启成功
    复制代码
    在这里插入图片描述

(2)浏览器缓存
为了提高用户加载页面的速度,对静态资源进行缓存是非常必要的,根据是否需要重新向服务器发起请求来分类,将 HTTP 缓存规则分为两大类(强制缓存,对比缓存),如果对缓存机制还不是了解很清楚的,可以参考作者写的关于 HTTP 缓存的文章《深入理解HTTP缓存机制及原理》,这里不再赘述。

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

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

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

在这里插入图片描述

(5)Vue 加载性能优化:

  • 第三方模块按需导入 ( babel-plugin-component )

  • 滚动到可视区域动态加载
    默认最多只渲染三屏,当前看到的和上一页还有下一页。看不到的用空div撑起来,这样可以控制DOM的数量。

  • 图片懒加载
    图片的懒加载用的是image,unload事件,先做预加载,再去加载图片,也是看可视区域的,都是判断offset,clientset。

(6)用户体验:

  • app-skeleton 骨架屏
    就是个插件,loading。

  • app-shell app
    -默认先渲染app的导航和app的头部。用户感觉已经加载出来了,单页面有个缺陷就是加载太慢,不能让用户看到白页,让用户预先体验的感觉。

  • pwa serviceworker(用的不多了)
    可以实现是H5的离线缓存,用的到的技术是serviceworker,因为国内兼容性太差了。

基于饿了么骨架屏方案,使用Chrome扩展程序生成网页骨架屏

前端骨架屏自动生成方案

(7)SEO 优化:

  • 预渲染插件 prerender-spa-plugin
    针对于纯静态页,可以用这个插件,作用是把这个代码运行起来,通过浏览器跑起来,完了保存起来,缺陷是不实时。如果数据实时性很高,就不能使用这个方式,只能用ssr做优化。
  • 服务端渲染 ssr
    通过服务端渲染的,减少白屏时间。缺陷是以前都在前端做,把压力都转给客户端,现在把压力都传给了服务端,会有一些性能问题。

(8)打包优化:

  • 使用 cdn 的方式加载第三方模块
  • 多线程打包 happypack
  • splitChunks 抽离公共文件
  • sourceMap 生成

(9)缓存,压缩

  • 客户端缓存、服务端缓存
  • 服务端 gzip 压缩

(10)项目实战案例

本题中的个别性能优化点详见:
1.项目实战之项目实战之vue掘金小册WebApp
2.揭秘 Vue.js 九个性能优化技巧

Vue2.0生态

Vue-Router

1.了解URL及HTML5History

URL(Universal Resource Locator)即统一资源定位符,又称网页地址,用于定位浏览器中所需显示的网页资源。在H5之前切换URL都会导致浏览器强制刷新,耗费时间和资源。
HTML5History
HTML5History允许在不刷新浏览器的情况下更新页面局部布局。 HTML5History接口允许操作浏览器的曾经在标签页或者框架里的历史记录,通俗说就是,可以通过window可以对history对象进行属性的获取和方法的操作。
在这里插入图片描述

2.前端路由原理

1.vue路由vue-router的安装及使用方式
2.vue-router安装和配置方法
3.一文带你看透Vue前端路由
4.vue刷新当前页面来重新获取一些数据
5.2020前端技术面试必备Vue:(二)Router篇
6.Vue权限路由思考

3. vue-router 路由模式

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

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

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

其中,3 种路由模式的说明如下:

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

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

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

(2)能说下 vue-router 中常用的 hash 和 history 路由模式区别及其各自的实现原理吗?

onhashchange

[展示特点]:在浏览器中符号 “#”,#以及#后面的字符称之为hash,用window.location.hash读取;hash虽然在URL中,但不被包括在HTTP请求中;用来指导浏览器动作,对服务端安全无副作用,hash不会重加载页面,除了感觉有点丑,没什么问题

[实现原理]

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

https://www.word.com#search

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

  • URL 中 hash 值只是客户端的一种状态,也就是说当向服务器端发出请求时,hash 部分不会被发送;
  • hash 值的改变,都会在浏览器的访问历史中增加一个记录。因此我们能通过浏览器的回退、前进按钮控制hash 的切换;
  • 可以通过 a 标签,并设置 href 属性,当用户点击这个标签后,URL 的 hash 值会发生改变;或者使用 JavaScript 来对 loaction.hash 进行赋值,改变 URL 的 hash 值;
  • 我们可以使用 hashchange 事件来监听 hash 值的变化,从而对页面进行跳转(渲染)
  • hash值存在 URL 中,携带#,hash值改变不会重载页面
  • hash改变会触发onhashchange事件,可被浏览器记录,从而实现浏览器的前进后退。
  • hash传参基于url,传递复杂参数会有体积限制
  • 兼容性好,支持低版本浏览器和 IE 浏览器。
history 模式

①实现原理

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

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

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

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

②存在问题

history.pushState会导致页面不存在的问题,但是可以通过服务端来解决。

在history模式下,前端的URL必须和实际向后端发起请求的URL一致,如http://www.xxx.com/items/id。后端如果缺少对 /items/id 的路由处理,将返回 404 错误。

1.面试官:vue项目如何部署?有遇到布署服务器后刷新404问题吗?

4.Vue-Router中导航守卫有哪些?

Vue-Router官网
Vue 路由守卫完整流程实战+解析
vue判断页面是否需要鉴权

1、全局前置守卫: router.beforeEach

使用场景:路由跳转前触发
作用: 常用于登录验证

快速上手Token登录认证
网站一般只要登陆过一次后,接下来该网站的其他页面都是可以直接访问的,不用再次登陆。我们可以通过 token 或 cookie 来实现,下面用代码来展示一下如何用 token 控制登陆验证。

router.beforeEach((to, from, next) => {
    // 如果有token 说明该用户已登陆
    if (localStorage.getItem('token')) {
        // 在已登陆的情况下访问登陆页会重定向到首页
        if (to.path === '/login') {
            next({path: '/'})
        } else {
            next({path: to.path || '/'})
        }
    } else {
        // 没有登陆则访问任何页面都重定向到登陆页
        if (to.path === '/login') {
            next()
        } else {
            next(`/login?redirect=${to.path}`)
        }
    }
})

2、全局解析守卫: router.beforeResolve

解析守卫刚好会在导航被确认之前、所有组件内守卫和异步路由组件被解析之后调用。router.beforeResolve 是获取数据或执行任何其他操作(如果用户无法进入页面时你希望避免执行的操作)的理想位置。

3、全局后置钩子: router.afterEach
你也可以注册全局后置钩子,然而和守卫不同的是,这些钩子不会接受 next 函数也不会改变导航本身:

4、路由独享的守卫: beforeEnter
beforeEnter 守卫 只在进入路由时触发,不会在 params、query 或 hash 改变时触发。例如,从 /users/2 进入到 /users/3 或者从 /users/2#info 进入到 /users/2#projects。它们只有在 从一个不同的 路由导航时,才会被触发。你也可以将一个函数数组传递给 beforeEnter,这在为不同的路由重用守卫时很有用。

5、组件内的守卫: beforeRouteEnterbeforeRouteUpdate (2.2 新增)、beforeRouteLeave

(1)beforeRouteEnter
执行顺序: 全局前置守卫beforeEach 和全局独享守卫beforeEnter之后,全局beforeResolve和全局afterEach之前调用。

使用场景: 路由进入之前调用。
不能获取组件 this 实例 ,因为路由在进入组件之前,组件实例还没有被创建。

(2) beforeRouteUpdate
使用场景:
在当前路由改变时,并且该组件被复用时调用,可以通过this访问实例。
当前路由query变更时,该守卫会被调用。

(3) 离开守卫beforeRouteLeave
导航离开该组件的对应路由时调用,可以访问组件实例this。
通常用来禁止用户在还未保存修改前突然离开。该导航可以通过 next(false) 来取消:

beforeRouteLeave (to, from , next) {
  const answer = window.confirm('Do you really want to leave? you have unsaved changes!')
  if (answer) {
    next()
  } else {
    next(false)
  }
}

完整的导航解析流程 (runQueue)

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

5.监听路由有几种方式?

1.vue 路由监听的方式——监听$route变化& beforeRouteEnter和beforeRouteLeave的使用

6.router和route的区别

(1) $router 对象
$router 对象是全局路由的实例,是 router 构造方法的实例。
$router 对象上的方法有: push()、go()、back()、replace()。

(2)$route 对象
$route 对象表示当前的路由信息,包含了当前 URL解析得到的信息。包含当前的路径,参数,query对象等
$route 对象上的属性有: path、params、query、hash等等

7. 开发小技巧

(1)路由参数解耦

①一般在组件内使用路由参数,大多数人会这样做:

export default {
    methods: {
        getParamsId() {
            return this.$route.params.id
        }
    }
}

在组件中使用 $route 会使之与其对应路由形成高度耦合,从而使组件只能在某些特定的 URL 上使用,限制了其灵活性。
正确的做法是通过 props 解耦

const router = new VueRouter({
    routes: [{
        path: '/user/:id',
        component: User,
        props: true
    }]
})

将路由的 props 属性设置为 true 后,组件内可通过 props 接收到 params 参数

export default {
    props: ['id'],
    methods: {
        getParamsId() {
            return this.id
        }
    }
}

另外你还可以通过函数模式来返回 props

const router = new VueRouter({
    routes: [{
        path: '/user/:id',
        component: User,
        props: (route) => ({
            id: route.query.id
        })
    }]
})

(2)vue路由传参query和params的区别(详解!)

用法上
query使用path和name传参跳转都可以,而params只能使用name传参跳转。

query传参:

//页面带参数跳转:
this.$router.push({ path:'/city',name:'City', query: { cityid: this.Cityid,cityname:this.Cityname }})

//路由配置:
{path:'/city',name:'City',component:City},
 
//接收参数:
this.cityid = this.$route.query.cityid;

params传参:

//页面带参数跳转:
this.$router.push({ name:'City', params: { cityid: this.Cityid,cityname:this.Cityname }})
 
//路由配置
{path:'/city/:cityid/:cityname',name:'City',component:City},
 
//接收参数:
this.cityid = this.$route.params.cityid;

展示上
query更加类似于我们ajax中get传参,params则类似于post,说的再简单一点,前者在浏览器地址栏中显示参数,后者则不显示。
query:
在这里插入图片描述
params:

在这里插入图片描述

params出现的参数丢失问题
传参跳转页面时,query不需要再路由上配参数就能在新的页面获取到参数,params也可以不用配,但是params不在路由配参数的话,当用户刷新当前页面的时候,参数就会消失。

也就是说使用params不在路由配参数跳转,只有第一次进入页面参数有效,刷新页面参数就会消失。

8.路由角色权限管理

  1. vue实现角色权限一般有两种方式

VueX

1.简述Vuex工作原理

参考阅读
从头开始学习Vuex
Vuex:Github
Vuex面试题汇总
用最简的方式学Vuex

// index.js文件
import {createStore} from "vuex";

import {moduleA} from "./module/moduleA";

export const store = createStore({
    // Vuex允许将store分割成模块(module),每个模块拥有自己的state、mutation、action、getter、甚至是嵌套子模块
    // 访问moduleA的状态:store.state.moduleA
    modules: {
        moduleA
    }
});

// module/moduleA.js文件
// 对于模块内部的mutation和getter,接收的第一个参数是模块的局部状态对象
// 对于模块内部的action,局部状态通过context.state暴露出来,根节点状态则为context.rootState
// 对于模块内部的getter,根节点状态会作为第三个参数暴露出来

// 在带命名空间的模块内访问全局内容
// 如果希望使用全局state和getter,rootState和rootGetters会作为第三和第四个参数传入getter,也会通过context对象的属性传入action
// 若需要在全局命名空间内分发action或提交mutation,将{root: true}作为第三个参数传给dispatch或commit即可。

export const moduleA = {
    // 默认情况下,模块内部的action、mutation和getter是注册在全局命名空间的,如果希望模块具有更高的封装度和复用性,可以通过添加namespaced:true的方式使其成为带命名空间的模块
    namespaced: true,
    state: {
        testState1: 'xxxx',
        testState2: {
            a: 0,
            b: 1
        },
        testState3: 0
    },
    // 有的时候需要从store中的state中派生出一些状态,此时可以将该部分抽象出一个函数供多处使用。
    // Vuex允许在store中定义getter,像计算属性一样,getter的返回值会根据它的依赖被缓存起来,且只有当他的依赖值发生了改变才会被重新计算
    getters: {
        // getter接收state作为其第一个参数
        testGetter1: state => {
            return state.testState1 + state.testState3;
        },
        // getter可以接受其他getter作为第二个参数
        testGetter2: (state, getters) => {
            return getters.testGetter1.length;
        }
    },
    // 更改Vuex的store中的状态的唯一方法是提交mutation,每个mutation都有一个字符串的事件类型和一个回调函数,该回调函数接收state作为第一个参数,提交的载荷作为第二个参数
    // 以相应的type调用store.commit方法来触发相应的回调函数
    // Mutation必须是同步函数
    mutations: {
        testMutation1(state) {
            // 变更状态
            state.testState3++;
        },
        // 第二个参数是载荷
        testMutation2(state, payload) {
            state.testState1 += payload.content;
        }
    },
    // Action提交的是mutation,而不是直接变更状态
    // Action可以包含任意异步操作
    // Action函数接受一个与store实例具有相同方法和属性的context对象,因此可以调用context.commit提交一个mutation,或者通过context.state和context.getters来获取state和getters。
    // Action通过store.dispatch方法触发
    actions: {
        testAction1(context) {
            setTimeout(() => {
                context.commit('testMutation1');
            }, 1000);
        },
        testAction2({commit}, payload) {
            setTimeout(() => {
                commit({
                    type: 'testMutation2',
                    content: payload.content
                });
            }, 1000);
        }
    }
};

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

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

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

主要包括以下几个模块:

State:定义了应用状态的数据结构,可以在这里设置默认的初始状态。
Getter允许组件从 Store 中获取数据,mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性。
有的时候需要从store中的state中派生出一些状态,此时可以将该部分抽象出一个函数供多处使用。Vuex允许在store中定义getter,像计算属性一样,getter的返回值会根据它的依赖被缓存起来,且只有当他的依赖值发生了改变才会被重新计算。

Mutation:是唯一更改 store 中状态的方法,且必须是同步函数。
Action:用于提交 mutation,而不是直接变更状态,可以包含任意异步操作。
Module允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中。
由于使用单一状态树,应用的所有状态都会集中到一个较大的对象,当应用变的复杂时store会变的很难维护,为了解决该问题,Vuex允许将store分割成模块(module),每个模块拥有自己的state、mutation、action、getter、甚至是嵌套子模块,具体使用可见第二节中开头部分的代码,其中包含了其常见的使用方式
理解:
单向数据流,状态统一管理
在这里插入图片描述

2.actionmutation区别

(一)联系

actionmutation他们两个的原理是一样的。

(二)区别

  • mutation 是同步更新数据,内部会进行是否为异步方式更新数据的检测【异步校验】
    内部会进行异步校验,如果当前搞状态不是通过同步更改的就会报异常。核心逻辑是基于$watch ,然后进行监控,当前状态是同步变化的还是异步变化的。如果是异步的变化,在严格模式下会报错。
  • action 异步操作,可以获取数据后调用 mutation 提交最终数据。
    action可以做异步操作,可以远程请求数据,把数据处理好之后提交给mutationmutaion再去更改状态state,重新渲染组件,同样组件可以异步调用或直接调同步方法去更新状态。

3.插件

3.1 插件基础

Vuex在实例化store的时候可以接受plugins选项,该选项可以添加一系列的插件,插件就可以帮助我们完成一系列的工作,节省人力和物力,下面我们自定义一个简单的插件并调用该插件。

// plugins/myPlugin.js
// 插件接收唯一的参数store
const myPlugin = store => {
    // store上有一系列的方法,可以用在插件中https://next.vuex.vuejs.org/zh/api/#commit
    // 注册一个动态模块用registerModule
    // 替换store的根状态用replaceState
    // 监听mutation的变化,该处理函数会在每个mutation完成后调用,接收mutation和经过mutation后的状态作为参数
    store.subscribe((mutation, state) => {
        console.log(mutation);
    });
};

export default myPlugin;

// index.js
import {createStore} from "vuex";
import myPlugin from "./plugins/myPlugin";

export const store = createStore({
    // ……
    // 一个数组,包含应用在store上的插件方法,这些插件直接接收store作为唯一参数,可以监听mutation或者提交mutation
    plugins: [myPlugin]
});

3.2数据持久化插件

Vuex 页面刷新数据丢失怎么解决?

Vuex的状态存储并不能持久化,只要一刷新页面数据就丢失了。需要做 vuex 数据持久化 一般使用本地存储的方案来保存数据 可以自己设计存储方案 也可以使用第三方插件。
推荐使用 vuex-persist 插件,它就是为 Vuex 持久化存储而生的一个插件。不需要你手动存取 storage ,而是直接将状态保存至 cookie 或者 localStorage 中。

import {createStore} from "vuex";
import VuexPersistence from "vuex-persist";
import myPlugin from "./plugins/myPlugin";

// 利用该插件可实现对store数据的持久化
const vuexLocal = new VuexPersistence({
    storage: window.localStorage
});

export const store = createStore({
    // ……
    // 一个数组,包含应用在store上的插件方法,这些插件直接接收store作为唯一参数,可以监听mutation或者提交mutation
    plugins: [myPlugin, vuexLocal.plugin]
});

1.Vuex持久化插件(解决页面刷新数据消失问题)
2.vuex 数据持久化插件
3.vuex-persistedstate

36.Vue3.0你知道有哪些改进?

1.Vue3.0笔记大全

Vue项目

一.项目开发中常用的组件库问题解决方案

1.vantUI组件库

  1. vue vant组件库修改样式

2.ElementUI

  1. 一份 ElementUI 问题清单
  2. el-dialog 内容不刷新及组件封装
    el-dialog 关闭再打开后窗口内容不刷新问题
    Vue 点击按钮显示弹窗的几种不同组件封装
  3. 表单组件如何校验
    vue 父组件校验子组件的form表单
    el-form表单组件校验
  4. element-ui 表格打印及错位问题
    打印需要用到的组件为 print-js

【普通表格打印】

printJS({
    printable: id, // DOM id
    type: 'html',
    scanStyles: false,
})

element-ui 表格打印】(其他组件库的表格同理)
lement-ui 的表格,表面上看起来是一个表格,实际上是由两个表格组成的。

表头为一个表格,表体又是个表格,这就导致了一个问题:打印的时候表体和表头错位。
在这里插入图片描述
另外,在表格出现滚动条的时候,也会造成错位。
在这里插入图片描述
解决方案
我的思路是将两个表格合成一个表格,print-js 组件打印的时候,实际上是把 id 对应的 DOM 里的内容提取出来打印。所以,在传入 id 之前,可以先把表头所在的表格内容提取出来,插入到第二个表格里,从而将两个表格合并,这时候打印就不会有错位的问题了。

function printHTML(id) {
    const html = document.querySelector('#' + id).innerHTML
    // 新建一个 DOM
    const div = document.createElement('div')
    const printDOMID = 'printDOMElement'
    div.id = printDOMID
    div.innerHTML = html

    // 提取第一个表格的内容 即表头
    const ths = div.querySelectorAll('.el-table__header-wrapper th')
    const ThsTextArry = []
    for (let i = 0, len = ths.length; i < len; i++) {
        if (ths[i].innerText !== '') ThsTextArry.push(ths[i].innerText)
    }

    // 删除多余的表头
    div.querySelector('.hidden-columns').remove()
    // 第一个表格的内容提取出来后已经没用了 删掉
    div.querySelector('.el-table__header-wrapper').remove()

    // 将第一个表格的内容插入到第二个表格
    let newHTML = '<tr>'
    for (let i = 0, len = ThsTextArry.length; i < len; i++) {
        newHTML += '<td style="text-align: center; font-weight: bold">' + ThsTextArry[i] + '</td>'
    }

    newHTML += '</tr>'
    div.querySelector('.el-table__body-wrapper table').insertAdjacentHTML('afterbegin', newHTML)
    // 将新的 DIV 添加到页面 打印后再删掉
    document.querySelector('body').appendChild(div)
    
    printJS({
        printable: printDOMID,
        type: 'html',
        scanStyles: false,
        style: 'table { border-collapse: collapse }' // 表格样式
    })

    div.remove()
}

二.一些前端项目常见问题的解决方案(Vue)

有一些问题不限于 Vue,还适应于其他类型的 SPA 项目。

项目架构逻辑和流程

基础架构设计:
①代码版本管理,Git or SVN?
②技术选型:
基础框架选择(vue,react,团队成员能力?)
HTTP请求库(axios,fetch)
CSS预处理器(less,scss)
UI库选择(iview,element ui,ant design,vant等等)
静态资源(图标字体,图片格式?)
安全处理(csrf,xss,https?)
数据mock(eolinker,还是自己搭建yapi?)
③自动编译发布jenkins
④灰度发布(金丝雀发布)
⑤监控埋点:百度统计,友盟
⑥报警与错误处理:sentry,BetterJS,Logan等。
⑦开发工具配置
eslint配置,vscode快捷插件等。
⑧开发文档撰写
业务架构设计:
①业务如何分层?模块如何划分?
②路由如何设计?父子嵌套?
③数据如何管理?
④组件如何划分?划分原则是什么?
⑤浏览器兼容性如何处理?
⑥代码规范?如何命名?
⑦性能优化如何做?

如何编写高质量前端设计文档?

如何编写高质量前端设计文档?

Vue 项目里戳中你痛点的问题及解决办法(更新

Vue 项目里戳中你痛点的问题及解决办法(更新)

为什么大厂前端监控都在用GIF做埋点?

为什么大厂前端监控都在用GIF做埋点?

1.权限控制和登陆验证

vue项目权限控制

页面权限控制
页面权限控制是什么意思呢?

就是一个网站有不同的角色,比如管理员和普通用户,要求不同的角色能访问的页面是不一样的。如果一个页面,有角色越权访问,这时就得做出限制了。

一种方法是通过动态添加路由和菜单来做控制,不能访问的页面不添加到路由表里,这是其中一种办法。具体细节请看下一节的《动态菜单》。

另一种办法就是所有的页面都在路由表里,只是在访问的时候要判断一下角色权限。如果有权限就允许访问,没有权限就拒绝,跳转到 404 页面。

思路

在每一个路由的 meta 属性里,将能访问该路由的角色添加到 roles 里。用户每次登陆后,将用户的角色返回。然后在访问页面时,把路由的 meta 属性和用户的角色进行对比,如果用户的角色在路由的 roles 里,那就是能访问,如果不在就拒绝访问。

代码示例

路由信息

routes: [
    {
        path: '/login',
        name: 'login',
        meta: {
            roles: ['admin', 'user']
        },
        component: () => import('../components/Login.vue')
    },
    {
        path: 'home',
        name: 'home',
        meta: {
            roles: ['admin']
        },
        component: () => import('../views/Home.vue')
    },
]

页面控制

// 假设角色有两种:admin 和 user
// 这里是从后台获取的用户角色
const role = 'user'
// 在进入一个页面前会触发 router.beforeEach 事件
router.beforeEach((to, from, next) => {
    if (to.meta.roles.includes(role)) {
        next()
    } else {
        next({path: '/404'})
    }
})

登陆验证
快速上手Token登录认证
网站一般只要登陆过一次后,接下来该网站的其他页面都是可以直接访问的,不用再次登陆。我们可以通过 token 或 cookie 来实现,下面用代码来展示一下如何用 token 控制登陆验证。

router.beforeEach((to, from, next) => {
    // 如果有token 说明该用户已登陆
    if (localStorage.getItem('token')) {
        // 在已登陆的情况下访问登陆页会重定向到首页
        if (to.path === '/login') {
            next({path: '/'})
        } else {
            next({path: to.path || '/'})
        }
    } else {
        // 没有登陆则访问任何页面都重定向到登陆页
        if (to.path === '/login') {
            next()
        } else {
            next(`/login?redirect=${to.path}`)
        }
    }
})

2.动态菜单

写后台管理系统,估计有不少人遇过这样的需求:根据后台数据动态添加路由和菜单。为什么这么做呢?因为不同的用户有不同的权限,能访问的页面是不一样的。

动态添加路由
利用 vue-router 的 addRoutes 方法可以动态添加路由。
先看一下官方介绍:
router.addRoutes

router.addRoutes(routes: Array<RouteConfig>)

动态添加更多的路由规则。参数必须是一个符合 routes 选项要求的数组。
举个例子:

const router = new Router({
    routes: [
        {
            path: '/login',
            name: 'login',
            component: () => import('../components/Login.vue')
        },
        {path: '/', redirect: '/home'},
    ]   
})

上面的代码和下面的代码效果是一样的

const router = new Router({
    routes: [
        {path: '/', redirect: '/home'},
    ]   
})

router.addRoutes([
    {
        path: '/login',
        name: 'login',
        component: () => import('../components/Login.vue')
    }
])

在动态添加路由的过程中,如果有 404 页面,一定要放在最后添加,否则在登陆的时候添加完页面会重定向到 404 页面。
类似于这样,这种规则一定要最后添加。

{path: '*', redirect: '/404'}

动态生成菜单
假设后台返回来的数据长这样:

// 左侧菜单栏数据
menuItems: [
    {
        name: 'home', // 要跳转的路由名称 不是路径
        size: 18, // icon大小
        type: 'md-home', // icon类型
        text: '主页' // 文本内容
    },
    {
        text: '二级菜单',
        type: 'ios-paper',
        children: [
            {
                type: 'ios-grid',
                name: 't1',
                text: '表格'
            },
            {
                text: '三级菜单',
                type: 'ios-paper',
                children: [
                    {
                        type: 'ios-notifications-outline',
                        name: 'msg',
                        text: '查看消息'
                    },
                ]
            }
        ]
    }
]

来看看怎么将它转化为菜单栏,我在这里使用了 iview 的组件,不用重复造轮子。

<!-- 菜单栏 -->
<Menu ref="asideMenu" theme="dark" width="100%" @on-select="gotoPage" 
accordion :open-names="openMenus" :active-name="currentPage" @on-open-change="menuChange">
    <!-- 动态菜单 -->
    <div v-for="(item, index) in menuItems" :key="index">
        <Submenu v-if="item.children" :name="index">
            <template slot="title">
                <Icon :size="item.size" :type="item.type"/>
                <span v-show="isShowAsideTitle">{{item.text}}</span>
            </template>
            <div v-for="(subItem, i) in item.children" :key="index + i">
                <Submenu v-if="subItem.children" :name="index + '-' + i">
                    <template slot="title">
                        <Icon :size="subItem.size" :type="subItem.type"/>
                        <span v-show="isShowAsideTitle">{{subItem.text}}</span>
                    </template>
                    <MenuItem class="menu-level-3" v-for="(threeItem, k) in subItem.children" :name="threeItem.name" :key="index + i + k">
                        <Icon :size="threeItem.size" :type="threeItem.type"/>
                        <span v-show="isShowAsideTitle">{{threeItem.text}}</span>
                    </MenuItem>
                </Submenu>
                <MenuItem v-else v-show="isShowAsideTitle" :name="subItem.name">
                    <Icon :size="subItem.size" :type="subItem.type"/>
                    <span v-show="isShowAsideTitle">{{subItem.text}}</span>
                </MenuItem>
            </div>
        </Submenu>
        <MenuItem v-else :name="item.name">
            <Icon :size="item.size" :type="item.type" />
            <span v-show="isShowAsideTitle">{{item.text}}</span>
        </MenuItem>
    </div>
</Menu>

代码不用看得太仔细,理解原理即可,其实就是通过三次 v-for 不停的对子数组进行循环,生成三级菜单。

不过这个动态菜单有缺陷,就是只支持三级菜单。一个更好的做法是把生成菜单的过程封装成组件,然后递归调用,这样就能支持无限级的菜单。在生菜菜单时,需要判断一下是否还有子菜单,如果有就递归调用组件。

动态路由因为上面已经说过了用 addRoutes 来实现,现在看看具体怎么做。

首先,要把项目所有的页面路由都列出来,再用后台返回来的数据动态匹配,能匹配上的就把路由加上,不能匹配上的就不加。最后把这个新生成的路由数据用 addRoutes 添加到路由表里。

const asyncRoutes = {
    'home': {
        path: 'home',
        name: 'home',
        component: () => import('../views/Home.vue')
    },
    't1': {
        path: 't1',
        name: 't1',
        component: () => import('../views/T1.vue')
    },
    'password': {
        path: 'password',
        name: 'password',
        component: () => import('../views/Password.vue')
    },
    'msg': {
        path: 'msg',
        name: 'msg',
        component: () => import('../views/Msg.vue')
    },
    'userinfo': {
        path: 'userinfo',
        name: 'userinfo',
        component: () => import('../views/UserInfo.vue')
    }
}

// 传入后台数据 生成路由表
menusToRoutes(menusData)

// 将菜单信息转成对应的路由信息 动态添加
function menusToRoutes(data) {
    const result = []
    const children = []

    result.push({
        path: '/',
        component: () => import('../components/Index.vue'),
        children,
    })

    data.forEach(item => {
        generateRoutes(children, item)
    })

    children.push({
        path: 'error',
        name: 'error',
        component: () => import('../components/Error.vue')
    })

    // 最后添加404页面 否则会在登陆成功后跳到404页面
    result.push(
        {path: '*', redirect: '/error'},
    )

    return result
}

function generateRoutes(children, item) {
    if (item.name) {
        children.push(asyncRoutes[item.name])
    } else if (item.children) {
        item.children.forEach(e => {
            generateRoutes(children, e)
        })
    }
}

动态菜单的代码实现放在 github 上,分别放在这个项目的 src/components/Index.vue、src/permission.js 和 src/utils/index.js 文件里。

3. 前进刷新后退不刷新

需求一:
在一个列表页中,第一次进入的时候,请求获取数据。

点击某个列表项,跳到详情页,再从详情页后退回到列表页时,不刷新。

也就是说从其他页面进到列表页,需要刷新获取数据,从详情页返回到列表页时不要刷新。

解决方案
在 App.vue设置:

 <keep-alive include="list">
            <router-view/>
 </keep-alive>

假设列表页为 list.vue,详情页为 detail.vue,这两个都是子组件。

我们在 keep-alive 添加列表页的名字,缓存列表页。

然后在列表页的 created 函数里添加 ajax 请求,这样只有第一次进入到列表页的时候才会请求数据,当从列表页跳到详情页,再从详情页回来的时候,列表页就不会刷新。这样就可以解决问题了。

需求二:

在需求一的基础上,再加一个要求:可以在详情页中删除对应的列表项,这时返回到列表页时需要刷新重新获取数据。

我们可以在路由配置文件上对 detail.vue 增加一个 meta 属性。

  {
           path: '/detail',
           name: 'detail',
           component: () => import('../view/detail.vue'),
           meta: {isRefresh: true}
       },

这个 meta 属性,可以在详情页中通过 this.$route.meta.isRefresh 来读取和设置。

设置完这个属性,还要在 App.vue 文件里设置 watch 一下 $route 属性。

    watch: {
       $route(to, from) {
           const fname = from.name
           const tname = to.name
           if (from.meta.isRefresh || (fname != 'detail' && tname == 'list')) {
               from.meta.isRefresh = false
       // 在这里重新请求数据
           }
       }
   },

这样就不需要在列表页的 created 函数里用 ajax 来请求数据了,统一放在 App.vue 里来处理。

触发请求数据有两个条件:

(1).从其他页面(除了详情页)进来列表时,需要请求数据。
(2).从详情页返回到列表页时,如果详情页 meta 属性中的 isRefresh 为 true,也需要重新请求数据。

当我们在详情页中删除了对应的列表项时,就可以将详情页 meta 属性中的 isRefresh 设为 true。这时再返回到列表页,页面会重新刷新。

解决方案二
对于需求二其实还有一个更简洁的方案,那就是使用 router-view 的 key 属性。

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

vue fullpath 和 path的区别
首先 keep-alive 让所有页面都缓存,当你不想缓存某个路由页面,要重新加载它时,可以在跳转时传一个随机字符串,这样它就能重新加载了。例如从列表页进入了详情页,然后在详情页中删除了列表页中的某个选项,此时从详情页退回列表页时就要刷新,我们可以这样跳转:

this.$router.push({
    path: '/list',
    query: { 'randomID': 'id' + Math.random() },
})

这样的方案相对来说还是更简洁的。

4. 多个请求下 loading 的展示与关闭

一般情况下,在 vue 中结合 axios 的拦截器控制 loading 展示和关闭,是这样的:

在 App.vue 配置一个全局 loading。

    <div class="app">
        <keep-alive :include="keepAliveData">
            <router-view/>
        </keep-alive>
        <div class="loading" v-show="isShowLoading">
            <Spin size="large"></Spin>
        </div>
    </div>

同时设置 axios 拦截器。

 // 添加请求拦截器
 this.$axios.interceptors.request.use(config => {
     this.isShowLoading = true
     return config
 }, error => {
     this.isShowLoading = false
     return Promise.reject(error)
 })

 // 添加响应拦截器
 this.$axios.interceptors.response.use(response => {
     this.isShowLoading = false
     return response
 }, error => {
     this.isShowLoading = false
     return Promise.reject(error)
 })

这个拦截器的功能是在请求前打开 loading,请求结束或出错时关闭 loading。

如果每次只有一个请求,这样运行是没问题的。但同时有多个请求并发,就会有问题了。

举例:

假如现在同时发起两个请求,在请求前,拦截器 this.isShowLoading = true 将 loading 打开。

现在有一个请求结束了。this.isShowLoading = false 拦截器关闭 loading,但是另一个请求由于某些原因并没有结束。

造成的后果就是页面请求还没完成,loading 却关闭了,用户会以为页面加载完成了,结果页面不能正常运行,导致用户体验不好。

解决方案

增加一个 loadingCount 变量,用来计算请求的次数。

loadingCount: 0

再增加两个方法,来对 loadingCount 进行增减操作。

 methods: {
        addLoading() {
            this.isShowLoading = true
            this.loadingCount++
        },

        isCloseLoading() {
            this.loadingCount--
            if (this.loadingCount == 0) {
                this.isShowLoading = false
            }
        }
    }

现在拦截器变成这样:

  // 添加请求拦截器
        this.$axios.interceptors.request.use(config => {
            this.addLoading()
            return config
        }, error => {
            this.isShowLoading = false
            this.loadingCount = 0
            this.$Message.error('网络异常,请稍后再试')
            return Promise.reject(error)
        })

        // 添加响应拦截器
        this.$axios.interceptors.response.use(response => {
            this.isCloseLoading()
            return response
        }, error => {
            this.isShowLoading = false
            this.loadingCount = 0
            this.$Message.error('网络异常,请稍后再试')
            return Promise.reject(error)
        })

这个拦截器的功能是:

每当发起一个请求,打开 loading,同时 loadingCount 加1。

每当一个请求结束, loadingCount 减1,并判断 loadingCount 是否为 0,如果为 0,则关闭 loading。

这样即可解决,多个请求下有某个请求提前结束,导致 loading 关闭的问题。

5.下载二进制文件

平时在前端下载文件有两种方式,一种是后台提供一个 URL,然后用 window.open(URL) 下载,另一种就是后台直接返回文件的二进制内容,然后前端转化一下再下载。

由于第一种方式比较简单,在此不做探讨。本文主要讲解一下第二种方式怎么实现。

第二种方式需要用到 Blob 对象, mdn 文档上是这样介绍的:

Blob 对象表示一个不可变、原始数据的类文件对象。Blob 表示的不一定是JavaScript原生格式的数据

具体使用方法

axios({
  method: 'post',
  url: '/export',
})
.then(res => {
  // 假设 data 是返回来的二进制数据
  const data = res.data
  const url = window.URL.createObjectURL(new Blob([data], {type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}))
  const link = document.createElement('a')
  link.style.display = 'none'
  link.href = url
  link.setAttribute('download', 'excel.xlsx')
  document.body.appendChild(link)
  link.click()
  document.body.removeChild(link)
})

打开下载的文件,看看结果是否正确。
在这里插入图片描述
一堆乱码…

一定有哪里不对。

最后发现是参数 responseType 的问题,responseType 它表示服务器响应的数据类型。由于后台返回来的是二进制数据,所以我们要把它设为 arraybuffer, 接下来再看看结果是否正确。

axios({
  method: 'post',
  url: '/export',
  responseType: 'arraybuffer',
})
.then(res => {
  // 假设 data 是返回来的二进制数据
  const data = res.data
  const url = window.URL.createObjectURL(new Blob([data], {type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}))
  const link = document.createElement('a')
  link.style.display = 'none'
  link.href = url
  link.setAttribute('download', 'excel.xlsx')
  document.body.appendChild(link)
  link.click()
  document.body.removeChild(link)
})

在这里插入图片描述
这次没有问题,文件能正常打开,内容也是正常的,不再是乱码。
根据后台接口内容决定是否下载文件
作者的项目有大量的页面都有下载文件的需求,而且这个需求还有点变态。

具体需求如下

  1. 如果下载文件的数据量条数符合要求,正常下载(每个页面限制下载数据量是不一样的,所以不能在前端写死)。
  2. 如果文件过大,后台返回 { code: 199999, msg: '文件过大,请重新设置查询项', data: null },然后前端再进行报错提示。

先来分析一下,首先根据上文,我们都知道下载文件的接口响应数据类型为 arraybuffer。返回的数据无论是二进制文件,还是 JSON 字符串,前端接收到的其实都是 arraybuffer。所以我们要对 arraybuffer 的内容作个判断,在接收到数据时将它转换为字符串,判断是否有 code: 199999。如果有,则报错提示,如果没有,则是正常文件,下载即可。具体实现如下:

axios.interceptors.response.use(response => {
    const res = response.data
    // 判断响应数据类型是否 ArrayBuffer,true 则是下载文件接口,false 则是正常接口
    if (res instanceof ArrayBuffer) {
        const utf8decoder = new TextDecoder()
        const u8arr = new Uint8Array(res)
        // 将二进制数据转为字符串
        const temp = utf8decoder.decode(u8arr)
        if (temp.includes('{code:199999')) {
            Message({
             // 字符串转为 JSON 对象
                message: JSON.parse(temp).msg,
                type: 'error',
                duration: 5000,
            })

            return Promise.reject()
        }
    }
    // 正常类型接口,省略代码...
    return res
}, (error) => {
    // 省略代码...
    return Promise.reject(error)
})

7. 自动忽略 console.log 语句

export function rewirteLog() {
    console.log = (function (log) {
        return process.env.NODE_ENV == 'development'? log : function() {}
    }(console.log))
}

在 main.js 引入这个函数并执行一次,就可以实现忽略 console.log 语句的效果。

8.vue2.0风格指南中的关键规则

1.官网风格指南
2.看到赚到!重读vue2.0风格指南,我整理了这些关键规则
3.史上最全 Vue 前端代码风格指南

9.对Vue项目团队开发的一些基本配置封装分享

对Vue项目团队开发的一些基本配置封装分享

10.Vue 项目打包部署总结

Vue 项目打包部署总结

11.Vue 项目下载文件最佳解决方案

Vue 项目下载文件最佳解决方案

原文链接:https://mp.weixin.qq.com/s/FZfWSip-8CeLB6usOoaWmw

1.项目实战之项目实战之vue掘金小册WebApp
2.基于Vue+Canvas实现图片的裁切
3.Vue 项目棘手问题的解决方案
4.Vue中的this.$options.data()this.$data用法说明
5.可视化拖拽组件库一些技术要点原理分析
6.可视化拖拽组件库一些技术要点原理分析(二)
7.可视化拖拽组件库一些技术要点原理分析(三)
8.Vue.js开发移动端经验总结
9.基于 Vue 的两层吸顶踩坑总结
10.字节跳动面试官:请你实现一个大文件上传和断点续传
11.可视化拖拽页面编辑器 | 项目复盘
12.12 个非常适合做私活或外包项目的开源后台管理系统(附源码和文档)
13.GitHub上 10 个超好看可视化面板

11.vue调试工具vue-devtools安装及使用

  1. 极速安装Vue.js devtools

推荐使用的vue库

vue3新拖拽组件库推荐,web开发拖拽必备

进阶

1.深入解析 Vue 的热更新原理,尤大是如何巧用源码中的细节?

1.深入解析 Vue 的热更新原理,尤大是如何巧用源码中的细节?

2.Vue CLI 是如何实现的 – 终端命令行工具篇

1.Vue CLI 是如何实现的 – 终端命令行工具篇
2.如何打造企业通用脚手架?
作业:

1.双向绑定和 vuex 是否冲突?
2. Vue 中内置组件transition、transition-group 的源码实现原理?
3. 说说patch函数里做了啥?
4. 知道vue 生命周期内部怎么实现的么 ?
主要靠callhook,会把生命周期变成数组,发布订阅
5. ssr 项目如果并发很大服务器性能怎么优化?
6. 说下项目中怎么实现权限校验?
7. 讲 vue-lazyloader 的原理,手写伪代码?
8. Vue.set 的原理?
9. vue compile 过程详细说一下,指令、插值表达式等 vue 语法如何生效的?

Vue参考阅读

1.化身面试官出 30+ Vue 面试题,超级干货
2.Vue 开发必须会的 36 个技巧
3.「ssh 回忆」面了几个说自己精通 Vue 的同学
4.10个Vue开发技巧
5.Vue 强烈推介的实用技巧
6.35道常见的前端vue面试题
7.面试官问 Vue 性能优化,我该怎么回答
8.Vue实现原理+前端性能优化
9.阿里大佬浅谈大型项目前端架构设计
11.实战技巧,Vue原来还可以这样写
12.绝对干货~!学会这些Vue小技巧,可以早点下班和女神约会了
13.前方高能,这是最新的一波Vue实战技巧,不用则已,一用惊人
14.看到赚到!重读vue2.0风格指南,我整理了这些关键规则
15.我在项目中是这样配置Vue的
16.学会使用Vue JSX,一车老干妈都是你的
17.30 道 Vue 面试题,内含详细讲解(涵盖入门到精通,自测 Vue 掌握程度)
18.化身面试官出30+Vue面试题,超级干货(附答案)|牛气冲天新年征文
19.10 个 GitHub 上超火的前端面试项目,打造自己的加薪宝库!
20.史上最强vue总结—面试开发全靠它了
21.一个合格(优秀)的前端都应该阅读这些文章
22.霖呆呆的近期面试128题汇总(含超详细答案) | 掘金技术征文
23.上海莉莉丝、米哈游、B站、小红书、得物等互联网公司前端面试总结
面试总结文章

24.845- Vuejs开发移动端经验总结
25.一个合格的中级前端工程师应该掌握的 20 个 Vue 技巧
26.30 道 Vue 面试题,内含详细讲解(涵盖入门到精通,自测 Vue 掌握程度)
27.熬夜总结50个Vue知识点,全都会你就是神!!!
28.1125- 非常实用的 5 个 Vue 高级实战技巧
29.Vue 项目里戳中你痛点的问题及解决办法(更新)
30.Vue 的这些技巧你真的都掌握了吗?
31.Vue 开发必须知道的 36 个技巧【近1W字】
32.12 个 Vue 开发中的性能优化小技巧,文末送10本vue书
33.『前端优化』—— Vue项目性能优化
34.送你22个实用的Vue3技巧,请收下
35.最全的 Vue 面试题+详解答案

  • 6
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值