高级前端开发者必会的34道Vue面试题解析(三)
sinye56 2024-10-26 14:43 5 浏览 0 评论
前言
通过前面的文章,我们认识了页面的响应是由Vue实例里的data函数所返回的数据变化而驱动,也重点学习了页面的响应与数据变化之间是是如何来联系起来的,并且分别在Vue2.x与3.x中,从零到一实现了两个版本下的数据变化驱动页面响应原理。
接下来在本文里一起看看当数据变化时,从源码层面逐步分析一下触发页面的响应动作之后,如何做渲染到页面上,展示到用户层面的。
同时也会了解在Vue中的异步方法NextTick的源码实现,看一看NextTick方法与浏览器的异步API有何联系。
注意,本文涉及的Vue源码版本为2.6.11。
什么是异步渲染?
这个问题应该先要做一个前提补充,当数据在同步变化的时候,页面订阅的响应操作为什么不会与数据变化完全对应,而是在所有的数据变化操作做完之后,页面才会得到响应,完成页面渲染。
从一个例子体验一下异步渲染机制。
import Vue from 'Vue'
new Vue({
el: '#app',
template: '<div>{{val}}</div>',
data () {
return {
val: 'init'
}
},
mounted () {
this.val = '我是第一次页面渲染'
// debugger this.val = '我是第二次页面渲染' const st = Date.now() while(Date.now() - st < 3000) {} }})
上面这一段代码中,在mounted里给val属性进行了两次赋值,如果页面渲染与数据的变化完全同步的话,页面应该是在mounted里有两次渲染。
而由于Vue内部的渲染机制,实际上页面只会渲染一次,把第一次的赋值所带来的的响应与第二次的赋值所带来的的响应进行一次合并,将最终的val只做一次页面渲染。
而且页面是在执行所有的同步代码执行完后才能得到渲染,在上述例子里的while阻塞代码之后,页面才会得到渲染,就像在熟悉的setTimeout里的回调函数的执行一样,这就是的异步渲染。
熟悉React的同学,应该很快能想到多次执行setState函数时,页面render的渲染触发,实际上与上面所说的Vue的异步渲染有异曲同工之妙。
Vue为什么要异步渲染?
我们可以从用户和性能两个角度来探讨这个问题。
从用户体验角度,从上面例子里便也可以看出,实际上我们的页面只需要展示第二次的值变化,第一次只是一个中间值,如果渲染后给用户展示,页面会有闪烁效果,反而会造成不好的用户体验。
从性能角度,例子里最终的需要展示的数据其实就是第二次给val赋的值,如果第一次赋值也需要页面渲染则意味着在第二次最终的结果渲染之前页面还需要渲染一次无用的渲染,无疑增加了性能的消耗。
对于浏览器来说,在数据变化下,无论是引起的重绘渲染还是重排渲染,都有可能会在性能消耗之下造成低效的页面性能,甚至造成加载卡顿问题。
异步渲染和熟悉的节流函数最终目的是一致的,将多次数据变化所引起的响应变化收集后合并成一次页面渲染,从而更合理的利用机器资源,提升性能与用户体验。
Vue中如何实现异步渲染?
先总结一下原理,在Vue中异步渲染实际在数据每次变化时,将其所要引起页面变化的部分都放到一个异步API的回调函数里,直到同步代码执行完之后,异步回调开始执行,最终将同步代码里所有的需要渲染变化的部分合并起来,最终执行一次渲染操作。
拿上面例子来说,当val第一次赋值时,页面会渲染出对应的文字,但是实际这个渲染变化会暂存,val第二次赋值时,再次暂存将要引起的变化,这些变化操作会被丢到异步API,Promise.then的回调函数中,等到所有同步代码执行完后,then函数的回调函数得到执行,然后将遍历存储着数据变化的全局数组,将所有数组里数据确定先后优先级,最终合并成一套需要展示到页面上的数据,执行页面渲染操作操作。
异步队列执行后,存储页面变化的全局数组得到遍历执行,执行的时候会进行一些筛查操作,将重复操作过的数据进行处理,实际就是先赋值的丢弃不渲染,最终按照优先级最终组合成一套数据渲染。
这里触发渲染的异步API优先考虑Promise,其次MutationObserver,如果没有MutationObserver的话,会考虑setImmediate,没有setImmediate的话最后考虑是setTimeout。
接下来在源码层面梳理一下的Vue的异步渲染过程。
接下来从源码角度一步一分析一下。
1、当我们使用this.val='343'赋值的时候,val属性所绑定的Object.defineProperty的setter函数触发,setter函数将所订阅的notify函数触发执行。
defineReactive() {
...
set: function reactiveSetter (newVal) {
...
dep.notify();
...
}
...
}
2、notify函数中,将所有的订阅组件watcher中的update方法执行一遍。
Dep.prototype.notify = function notify () {
// 拷贝所有组件的watcher
var subs = this.subs.slice();
...
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
};
3、update函数得到执行后,默认情况下lazy是false,sync也是false,直接进入把所有响应变化存储进全局数组queueWatcher函数下。
Watcher.prototype.update = function update () {
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
};
4、queueWatcher函数里,会先将组件的watcher存进全局数组变量queue里。默认情况下config.async是true,直接进入nextTick的函数执行,nextTick是一个浏览器异步API实现的方法,它的回调函数是flushSchedulerQueue函数。
function queueWatcher (watcher) {
...
// 在全局队列里存储将要响应的变化update函数
queue.push(watcher);
...
// 当async配置是false的时候,页面更新是同步的
if (!config.async) {
flushSchedulerQueue();
return
}
// 将页面更新函数放进异步API里执行,同步代码执行完开始执行更新页面函数
nextTick(flushSchedulerQueue);
}
5、nextTick函数的执行后,传入的flushSchedulerQueue函数又一次push进callbacks全局数组里,pending在初始情况下是false,这时候将触发timerFunc。
function nextTick (cb, ctx) {
var _resolve;
callbacks.push(function () {
if (cb) {
try {
cb.call(ctx);
} catch (e) {
handleError(e, ctx, 'nextTick');
}
} else if (_resolve) {
_resolve(ctx);
}
});
if (!pending) {
pending = true;
timerFunc();
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(function (resolve) {
_resolve = resolve;
})
}
}
6、timerFunc函数是由浏览器的Promise、MutationObserver、setImmediate、setTimeout这些异步API实现的,异步API的回调函数是flushCallbacks函数。
var timerFunc;
// 这里Vue内部对于异步API的选用,由Promise、MutationObserver、setImmediate、setTimeout里取一个// 取用的规则是 Promise存在取由Promise,不存在取MutationObserver,MutationObserver不存在setImmediate,// setImmediate不存在setTimeout。
if (typeof Promise !== 'undefined' && isNative(Promise)) {
var p = Promise.resolve();
timerFunc = function () {
p.then(flushCallbacks);
if (isIOS) {
setTimeout(noop);
}
};
isUsingMicroTask = true;
} else if (!isIE && typeof MutationObserver !== 'undefined' && ( isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]')) {
var counter = 1;
var observer = new MutationObserver(flushCallbacks);
var textNode = document.createTextNode(String(counter)); observer.observe(textNode, { characterData: true }); timerFunc = function () { counter = (counter + 1) % 2; textNode.data = String(counter); }; isUsingMicroTask = true;} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { timerFunc = function () { setImmediate(flushCallbacks); };} else { timerFunc = function () { setTimeout(flushCallbacks, 0); };}
7、flushCallbacks函数中将遍历执行nextTick里push的callback全局数组,全局callback数组中实际是第5步的push的flushSchedulerQueue的执行函数。
// 将nextTick里push进去的flushSchedulerQueue函数进行for循环依次调用
function flushCallbacks () {
pending = false;
var copies = callbacks.slice(0);
callbacks.length = 0;
for (var i = 0; i < copies.length; i++) {
copies[i]();
}
}
8、callback遍历执行的flushSchedulerQueue函数中,flushSchedulerQueue里先按照id进行了优先级排序,接下来将第4步中的存储watcher对象全局queue遍历执行,触发渲染函数watcher.run。
function flushSchedulerQueue () {
var watcher, id;
// 安装id从小到大开始排序,越小的越前触发的update
queue.sort(function (a, b) { return a.id - b.id; });
// queue是全局数组,它在queueWatcher函数里,每次update触发的时候将当时的watcher,push进去
for (index = 0; index < queue.length; index++) {
...
watcher.run(); // 渲染
...
}
}
9、watcher.run的实现在构造函数Watcher原型链上,初始状态下active属性为true,直接执行Watcher原型链的set方法。
Watcher.prototype.run = function run () {
if (this.active) {
var value = this.get();
...
}
};
10、get函数中,将实例watcher对象push到全局数组中,开始调用实例的getter方法,执行完毕后,将watcher对象从全局数组弹出,并且清除已经渲染过的依赖实例。
Watcher.prototype.get = function get () {
pushTarget(this);
// 将实例push到全局数组targetStack
var vm = this.vm;
value = this.getter.call(vm, vm);
...
}
11、实例的getter方法实际是在实例化的时候传入的函数,也就是下面vm的真正更新函数_update。
function () {
vm._update(vm._render(), hydrating);
};
12、实例的_update函数执行后,将会把两次的虚拟节点传入传入vm的patch方法执行渲染操作。
Vue.prototype._update = function (vnode, hydrating) {
var vm = this;
...
var prevVnode = vm._vnode;
vm._vnode = vnode;
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode);
}
...
};
nextTick的实现原理
首先nextTick并不是浏览器本身提供的一个异步API,而是Vue中,用过由浏览器本身提供的原生异步API封装而成的一个异步封装方法,上面第5第6段是它的实现源码。
它对于浏览器异步API的选用规则如下,Promise存在取由Promise.then,不存在Promise则取MutationObserver,MutationObserver不存在setImmediate,setImmediate不存在最后取setTimeout来实现。
从上面的取用规则也可以看出来,nextTick即有可能是微任务,也有可能是宏任务,从优先去Promise和MutationObserver可以看出nextTick优先微任务,其次是setImmediate和setTimeout宏任务。
对于微任务与宏任务的区别这里不深入,只要记得同步代码执行完毕之后,优先执行微任务,其次才会执行宏任务。
Vue能不能同步渲染?
1、 Vue.config.async = false
当然是可以的,在第四段源码里,我们能看到如下一段,当config里的async的值为为false的情况下,并没有将flushSchedulerQueue加到nextTick里,而是直接执行了flushSchedulerQueue,就相当于把本次data里的值变化时,页面做了同步渲染。
function queueWatcher (watcher) {
...
// 在全局队列里存储将要响应的变化update函数
queue.push(watcher);
...
// 当async配置是false的时候,页面更新是同步的
if (!config.async) {
flushSchedulerQueue();
return
}
// 将页面更新函数放进异步API里执行,同步代码执行完开始执行更新页面函数
nextTick(flushSchedulerQueue);
}
在我们的开发代码里,只需要加入下一句即可让你的页面渲染同步进行。
import Vue from 'Vue'
Vue.config.async = false
2、this._watcher.sync = true
在Watch的update方法执行源码里,可以看到当this.sync为true时,这时候的渲染也是同步的。
Watcher.prototype.update = function update () {
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
};
在开发代码中,需要将本次watcher的sync属性修改为true,对于watcher的sync属性变化只需要在需要同步渲染的数据变化操作前执行this._watcher.sync=true,这时候则会同步执行页面渲染动作。
像下面的写法中,页面会渲染出val为1,而不会渲染出2,最终渲染的结果是3,但是官网未推荐该用法,请慎用。
new Vue({
el: '#app',
sync: true,
template: '<div>{{val}}</div>',
data () {
return { val: 0 }
},
mounted () {
this._watcher.sync = true
this.val = 1
debugger
this._watcher.sync = false
this.val = 2
this.val = 3
}
})
总结
本文中介绍了Vue中为什么采用异步渲染页面的原因,并且从源码的角度深入剖析了整个渲染前的操作链路,同时剖析出Vue中的异步方法nextTick的实现与原生的异步API直接的联系。最后也从源码角度下了解到,Vue并非不能同步渲染,当我们的页面中需要同步渲染时,做适当的配置即可满足。
References
[1] https://github.com/vuejs/vue
[2] https://cn.vuejs.org/
后记
如果你喜欢探讨技术,或者对本文有任何的意见或建议,你可以扫描下方二维码,关注微信公众号“ 全栈者 ”,也欢迎加作者微信,与作者随时互动。欢迎!衷心希望可以遇见你。
?
相关推荐
- Linux两种光驱自动挂载的方法
-
环境:CentOS6.4西昆云服务器方式一修改fstab文件/etc/fstab是系统保存文件系统信息?静态文件,每一行描述一个文件系统;系统每次启动会读取此文件信息以确定需要挂载哪些文件系统。参...
- linux系统运维,挂载和分区概念太难?在虚机下操作一次全掌握
-
虚拟机的好处就是可以模拟和学习生产环境的一切操作,假如我们还不熟悉磁盘操作,那先在虚机环境下多操作几次。这次来练习下硬盘扩容操作。虚拟机环境:centos8vm11linux设备命名规则在linux中...
- Linux 挂载 NFS 外部存储 (mount 和 /etc/fstab)
-
mount:手工挂载,下次重启需再重新挂载,操作命令:mount-tnfs-ooptionsserver:/remote/export/local/directory上面命令中,本地目录...
- 在Linux中如何设置自动挂载特定文件系统(示例)
-
Linux...
- Linux环境中的绑定挂载(bind mount)
-
简介:Linux中的mount命令是一个特殊的指令,主要用于挂载文件目录。而绑定挂载(bindmount)命令更为特别。mount的bind选项将第一个目录克隆到第二个。一个目录中的改变将会在...
- Linux挂载CIFS共享 临时挂载 1. 首先
-
如何解决服务器存储空间不足的问题?大家好,欢迎回来。在上一期视频中,我为大家介绍了如何利用Linux挂载来扩容服务器存储空间。这一期视频,我将以Linux为例,教大家如何进行扩容。群辉使用的是Linu...
- Linux 硬盘挂载(服务器重启自动挂载)
-
1、先查看目前机器上有几块硬盘,及已挂载磁盘:fdisk-l能够查看到当前主机上已连接上的磁盘,以及已经分割的磁盘分区。(下面以/dev/vdb磁盘进行分区、挂载为例,挂载点设置为/data)df...
- linux 挂载磁盘
-
在Linux中挂载硬盘的步骤如下:...
- 笨小猪教您Linux磁盘挂载
-
本教程针对Linux系统比较熟悉或者想学习Linux基础的用户朋友,本教程操作起来比较傻瓜式,跟着步骤就会操作,本文使用的工具是XShell同时多多注意空格(文中会有提示)。【问答】什么是磁盘挂载?答...
- Linux 磁盘挂载和docker安装命令
-
本篇给大家介绍Linux磁盘挂载和docker安装的相关内容,Linux服务器的操作是一个手熟的过程,一些不常用的命令隔断时间就忘记了,熟话说好记性不如烂笔头,还需在平时的工作中多练习记录。...
- Linux设置开机自动挂载分区
-
有时候,我们在安装完Linux系统之后,可能在使用过程中添加硬盘或者分区进行使用,这时候就需要手动把磁盘分区挂载到某个路径,但是开机之后就会消失,需要重新挂载,非常麻烦,那么我们应该如何设置开机自动挂...
- 在linux挂载一个新硬盘的完整步骤
-
以下是在Linux中挂载新原始磁盘的完整步骤,包括分区、创建文件系统以及使用UUID在/etc/fstab中启动时挂载磁盘:将新的原始磁盘连接到Linux系统并打开电源。运行以下命令,...
- Linux系统如何挂载exFAT分区
-
简介:Linux系统中不能像Windows系统那样自动识别加载新设备,需要手动识别,手动加载。Linux中一切皆文件。文件通过一个很大的文件树来组织,文件树的根目录是:/,从根目开始录逐级展开。这些文...
- Linux系统挂载硬盘
-
fdisk-l查看可挂载的磁盘都有哪些df-h查看已经挂载的磁盘...
- WSL2发布,如何在Win10中挂载Linux文件系统
-
WSL2是最新版本的架构,它为Windows子系统提供支持,使其能够在Windows上运行ELF64Linux二进制文件。通过最近的更新,它允许使用Linux文件系统访问存储在硬盘中的文件。如果你...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- oracle忘记用户名密码 (59)
- oracle11gr2安装教程 (55)
- mybatis调用oracle存储过程 (67)
- oracle spool的用法 (57)
- oracle asm 磁盘管理 (67)
- 前端 设计模式 (64)
- 前端面试vue (56)
- linux格式化 (55)
- linux图形界面 (62)
- linux文件压缩 (75)
- Linux设置权限 (53)
- linux服务器配置 (62)
- mysql安装linux (71)
- linux启动命令 (59)
- 查看linux磁盘 (72)
- linux用户组 (74)
- linux多线程 (70)
- linux设备驱动 (53)
- linux自启动 (59)
- linux网络命令 (55)
- linux传文件 (60)
- linux打包文件 (58)
- linux查看数据库 (61)
- linux获取ip (64)
- 关闭防火墙linux (53)