学习Vue你必须知道的
- Vue2.0基础知识
-
- 1.说说你对vue的了解
- 2.单页应用SPA和多页应用MPA
- 3.使用过 Vue SSR 吗?说说 SSR?
- 4.MVVM与MVC以及MVVM的优缺点 ★★★
- 5.谈谈你对 keep-alive 的了解?
- 6.插槽
- 7.修饰符与指令
-
- (一).修饰符
- (二).模板语法常用内置指令使用场景及其原理
-
- (1)`Vue`中v-if和v-show的区别★★★
- (2)为什么v-for和v-if不能连用?★★★
- (3)`v-for`中为什么要用key 并且避免使用index作为标识(图解)★★★
- (4)v-on可以监听多个事件吗?★★★
- (5) ①v-bind和v-model的区别,②`v-model`中的实现原理及如何自定义v-model ③v-model与.sync修饰符的区别★★★
- (6)`Vue`中`v-html`会导致哪些问题?
- (7)v-cloak 解决页面闪烁问题★
- (8)v-once 和 v-pre 提升性能
- (9)v-if、v-else-if/v-else
- (10) 如何自定义指令?自定义指令有哪些钩子?★★★
- 8.为何`Vue`采用异步渲染
- 9.`nextTick`实现原理
- 10.Class 与 Style 如何动态绑定?
- 12.Vue中的scoped的实现原理以及scoped穿透的用法(字节)★★★
- 13.虚拟 DOM优缺点、实现原理、diff算法?
- 14.调试 template及template模板编译原理
- 15.在 Vue 中使用 JSX和render函数
- 16.请说一下响应式数据的原理(双向数据绑定)★★★
- 17.Vue中源码中的数组
- 18.Vue 怎么用 vm.$set() 解决对象新增属性不能响应的问题 ?
- 19.组件中的 data相关问题及优化?
- 20.Computed与Watch
- 21.`Vue`组件的生命周期五问
- 22.methods属性
- 23.深入理解组件原理及组件的封装技巧
- 24.`Vue`中事件绑定的原理
- 25.程序化的事件侦听器
- 26.`Vue`中相同逻辑如何抽离?
- 27.`Vue`中常见性能优化
- Vue2.0生态
- Vue-Router
- VueX
- Vue项目
- 推荐使用的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?
- jQuery是直接操作DOM,Vue不直接操作DOM,Vue的数据与视图是分开的,Vue只需要操作数据即可
- jQuery的操作DOM行为是频繁的,而Vue利用虚拟DOM的技术,大大提高了更新DOM时的性能
- Vue中不倡导直接操作DOM,开发者只需要把大部分精力放在数据层面上
- 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.html | http://xxx/shell.html#page1 和 http://xxx/shell.html#page2 |
用户体验 | 页面间切换加载慢,不流畅,用户体验差,特别是在移动设备上 | 页面片段间的切换快,用户体验好,包括在移动设备上 |
能否实现转场动画 | 无法实现 | 容易实现(手机app动效) |
页面间传递数据 | 依赖URL 、cookie 或者localstorage ,实现麻烦 |
因为在一个页面内,页面间传递数据很容易实现(这里是我补充,父子之间传值,或vuex 或storage 之类) |
搜索引擎优化(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
:
传统的前端会将数据手动渲染到页面上,比如jQuery
。MVVM
模式不需要用户收到操作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的区别★★★
理解:
- 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)Vue
中v-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
,每个属性都有一个dep
,dep
会对应自己的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 插件原理分析
4. vue输入框自动获取焦点事件
(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
这个方法,这个方法也是微任