vue批量更新,vue3数组更新
本文将用示例代码介绍Vue批量更新dom的实现步骤。通过示例代码对其进行了详细介绍。和有需要的朋友一起学习吧。
目录
场景介绍深度响应式触发器getter寻找Dep.targetgettersetter摘要
场景介绍
在SFC(单文件组件)中,我们经常编写这样的逻辑:
模板
差异
span{{ a }}/span
span{{ b }}/span
/div
/模板
脚本类型=javascript
导出默认值{
data() {
返回{
答:0,
乙:0
}
},
已创建(){
//一些逻辑代码
this.a=1
this.b=2
}
}
/脚本
大家可能知道,this.a和this.b赋值后,Vue会把this.a和this.b的dom更新函数放到一个微任务里。等待主线程的同步任务完成后,微任务将出队执行。让我们看看Vue官方文件的“深入响应原则-声明响应属性”部分,它是如何描述的:
你可能没有注意到,Vue在更新DOM的时候是异步执行的。只要检测到数据更改,Vue就会打开一个队列,缓冲在同一个事件周期中发生的所有数据更改。
那么,Vue是如何实现这种能力的呢?要回答这个问题,我们需要深入到Vue源代码的核心部分,——响应式原理。
深入响应式
我们先来看看给this.a和this.b赋值后会发生什么,如果使用Vue CLI进行开发,main.js文件中会有new Vue()的实例化操作。因为Vue的源代码是用flow写的,所以增加了理解的成本。为了方便,我们直接看npm vue包中dist文件夹下vue.js的源代码。搜索“function Vue”并找到以下源代码:
功能Vue(选项){
如果(!(这是Vue的例子)
) {
warn(Vue是一个构造函数,应该用 new 关键字调用);
}
这个。_init(选项);
}
非常简单的源码,源码真的没有我们想象的那么难!带着这样一个意想不到的惊喜,我们找到了_init函数来看看这个函数做了什么:
vue . prototype . _ init=function(options){
var vm=this
//一个uid
vm。_ uid=uid $ 3;
var startTag,endTag
/*伊斯坦布尔忽略if */
if(配置性能标志){
startTag=vue-perf-start: (vm。_ uid);
endTag=vue-perf-end: (vm。_ uid);
mark(startTag);
}
//避免被观察到的标志
vm。_ isVue=true
//合并选项
if(选项选项。_isComponent) {
//优化内部组件实例化
//因为动态选项合并非常慢,而且
//内部组件选项需要特殊处理。
initInternalComponent(vm,options);
}否则{
vm。$options=mergeOptions(
resolveConstructorOptions(VM . constructor),
选项 {},
伏特计
);
}
/*伊斯坦布尔忽略else */
{
init proxy(VM);
}
//暴露真实的自己
vm。_ self=vm
初始化生命周期(虚拟机);
初始化事件(虚拟机);
init render(VM);
callHook(vm, before create );
初始注入(VM);//在数据/属性之前解析注入
initState(VM);
init provide(VM);//解析后提供数据/属性
callHook(vm, created );
/*伊斯坦布尔忽略if */
if(配置性能标志){
vm。_name=formatComponentName(vm,false);
mark(endTag);
测量(( vue (vm。_name) init )、startTag、end tag);
}
如果(vm。$options.el) {
vm。$mount(vm。$ options . El);
}
}
先忽略上面一堆判断,直接拉到下面的主要逻辑。可以看到,_init函数已经连续执行了initLifeCycle、initEvents、initRender、callHook、initInjections、initState、initProvide和第二个callHook函数。从函数的命名可以知道具体的含义。一般来说,这段代码分为以下两部分
在完成初始化生命周期、事件钩子以及渲染函数后,进入创建前生命周期(执行创建前函数)
在完成初始化注入值、状态以及提供值之后,进入创造生命周期(执行创造函数)
其中,我们关心的数据响应式原理部分在初始状态函数中,我们看看这个函数做了什么:
函数初始化状态(虚拟机){
虚拟机._ watchers=[];
var opts=vm .$选项
if (opts.props) { initProps(vm,opts。道具);}
如果(opts。方法){初始化方法(VM,opts。方法);}
if (opts.data) {
初始化数据(虚拟机);
}否则{
观察(虚拟机._data={},true/* as root data */);
}
如果(opts。计算){ init计算(VM,opts。已计算);}
if (opts.watch opts.watch!==nativeWatch) {
initWatch(vm,opts。观看);
}
}
这里我们看到了在书写表面(同表面)文件时常常见到的几个配置项:道具、方法、数据、计算和看着。我们将注意力集中到opts.data部分,这一部分执行了initData函数:
函数初始数据(虚拟机){
定义变量数据=vm .$ options.data
数据=虚拟机. data=数据类型===函数
?获取数据(数据,虚拟机)
:data { };
如果(!isPlainObject(data)) {
data={ };
警告(
数据函数应该返回对象:\n
https://vuejs。org/v2/guide/组件。 html #数据必须是函数,
伏特计
);
}
//实例上的代理数据
var keys=object。密钥(数据);
var props=vm .$选项.道具
定义变量方法=vm .$ options.methods
var i=keys.length
while (i - ) {
var key=keys[I];
{
if (methods hasOwn(methods,key)) {
警告(
("方法"键"\"已被定义为数据属性。),
伏特计
);
}
}
if (props hasOwn(props,key)) {
警告(
数据属性"键"已被声明为属性。
请改用合适默认值。
伏特计
);
} else if(!isReserved(key)) {
proxy(vm, _data ,key);
}
}
//观察数据
observe(data,true/* as root data */);
}
我们在写数据配置项时,会将其定义为函数,因此这里执行了得到一条数据函数:
函数获取数据(数据,虚拟机){
//#7573调用数据获取器时禁用资料执行防止集合
push target();
尝试{
返回数据。调用(虚拟机,虚拟机)
} catch (e) {
handleError(e,vm, data());
返回{}
}最后{
pop target();
}
}
得到一条数据函数做的事情非常简单,就是在组件实例上下文中执行数据函数。注意,在执行数据函数前后,分别执行了推送目标函数和popTarget函数,这两个函数我们后面再讲。
执行得到一条数据函数后,我们回到initData函数,后面有一个循环的错误判断,暂时不用管。于是我们来到了观察函数:
函数观察(值,asRootData) {
如果(!是对象(值) VNode的值实例){
返回
}
var ob
if (hasOwn(value, __ob__ )值观察员的实例){
ob=值. ob _ _
} else if(
应该观察
!isServerRendering()
(数组。is array(value) isPlainObject(value))
Object.isExtensible(值)
!价值. isVue
) {
ob=新观察者(值);
}
if (asRootData ob) {
卒于虚拟机计数;
}
返回鄂毕河(Ob)
}
观察函数为数据对象创建了一个观察者(ob),也就是实例化观察者,实例化观察者具体做了什么呢?我们继续看源码:
定义变量观察者=函数观察者(值){
这个值=值
这个。Dep=new Dep();
这个。虚拟机计数=0;
定义(值, __ob__ ,这个);
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(值、数组方法);
}否则{
copyAugment(值、数组方法、数组键);
}
this.observeArray(值);
}否则{
散步(值);
}
}
正常情况下,因为我们定义的数据函数返回的都是一个对象,所以这里我们先不管对数组的处理。那么就是继续执行步行函数:
观察者.原型.行走=函数步行
var keys=object。keys(obj);
for(var I=0;I键.长度i ) {
defineReactive$$1(obj,keys[I]);
}
}
对于数据函数返回的对象,即组件实例的数据对象中的每个可枚举属性,执行定义有效$$1函数:
函数定义活动$$1(
obj,
钥匙,
瓦尔,
customSetter,
浅的
) {
var Dep=new Dep();
var属性=对象。getownpropertydescriptor(obj,key);
if(property属性。可配置===假){
返回
}
//满足预定义的getter/setter
var getter=property属性。get
var setter=property属性。设置;
如果((!getter setter)参数。长度===2){
val=obj[key];
}
var childOb=!浅层观察(val);
Object.defineProperty(obj,key,{
可枚举:真,
可配置:真,
获取:函数reactiveGetter () {
定义变量值=getter?吸气剂。call(obj):val;
if (Dep.target) {
离开依赖();
if (childOb) {
childob。离开依赖();
if (Array.isArray(value)) {
受抚养人(值);
}
}
}
返回值
},
设置:函数反应设置器(新值){
定义变量值=getter?吸气剂。call(obj):val;
/* eslint-禁用无自比较*/
if (newVal===value (newVal!==newVal值!==value)) {
返回
}
/* eslint-启用无自比较*/
if (customSetter) {
自定义setter();
}
//#7981:对于不带作曲者的访问器属性
如果(getter!setter) { return }
if (setter) {
setter.call(obj,new val);
}否则{
val=newVal
}
childOb=!浅层观察(新val);
离开notify();
}
});
}
在定义有效$$1函数中,首先实例化一个依赖收集器。然后使用对象。定义属性重新定义对象属性的吸气剂(即上面的得到函数)和二传手(即上面的设置函数)。
触发getter
吸气剂和作曲者某种意义上可以理解为回调函数,当读取对象某个属性的值时,会触发得到函数(即getter);当设置对象某个属性的值时,会触发设置函数(即二传手).我们回到最开始的例子:
模板
差异
span{{ a }}/span
span{{ b }}/span
/div
/模板
脚本类型=javascript
导出默认值{
data() {
返回{
答:0,
乙:0
}
},
已创建(){
//一些逻辑代码
this.a=1
this.b=2
}
}
/脚本
这里有设置这对象的属性a和属性b的值,因此会触发二传手。我们把上面设置函数代码单独拿出来:
函数反应设置器(新值){
定义变量值=getter?吸气剂。call(obj):val;
/* eslint-禁用无自比较*/
if (newVal===value (newVal!==newVal值!==value)) {
返回
}
/* eslint-启用无自比较*/
if (customSetter) {
自定义setter();
}
//#7981:对于不带作曲者的访问器属性
如果(getter!setter) { return }
if (setter) {
setter.call(obj,new val);
}否则{
val=newVal
}
childOb=!浅层观察(新val);
离开notify();
}
作曲者先执行了吸气器:
函数reactiveGetter () {
定义变量值=getter?吸气剂。call(obj):val;
if (Dep.target) {
离开依赖();
if (childOb) {
childob。离开依赖();
if (Array.isArray(value)) {
受抚养人(值);
}
}
}
返回值
}
吸气剂先检测副目标是否存在。在前面执行得到一条数据函数的时候,部门目标的初始值为空,它在什么时候被赋值了呢?我们前面讲得到一条数据函数的时候,有看到一个推送目标函数和popTarget函数,这两个函数的源码如下:
Dep.target=null
var目标堆栈=[];
函数推送目标(目标){
targetStack.push(目标);
部门目标=目标
}
函数popTarget () {
目标堆栈。pop();
离开target=目标堆栈[目标堆栈。长度-1];
}
想要正常执行吸气剂,就需要先执行推送目标函数。我们找找推送目标函数在哪里执行的。在vue。j中搜索推送目标,我们找到了5个地方,除去定义的地方,执行的地方有四个。
第一个执行推送目标函数的地方。这是一个处理错误的函数,正常逻辑不会触发:
函数句柄错误(错误,虚拟机,信息){
//在处理错误处理程序时停用依赖的列表跟踪,以避免可能的无限呈现。
//参见:https://github . com/vue js/vuex/issues/1505
push target();
尝试{
如果(虚拟机){
var cur=vm
while ((cur=cur .$parent)) {
变化挂钩=曲线. options.errorCaptured已捕获
如果(挂钩){
for(var I=0;我挂钩。长度;i ) {
尝试{
var capture=hooks[i].call(cur,err,vm,info)==false;
if (capture) { return }
} catch (e) {
globalHandleError(e,cur,错误捕获挂钩);
}
}
}
}
}
globalHandleError(err,vm,info);
}最后{
pop target();
}
}
第二个执行推送目标的地方。这是调用对应的钩子函数。在执行到对应的钩子函数时会触发。不过,我们现在的操作介于创建前钩子和创造钩子之间,还没有触发:
函数调用钩子(虚拟机,钩子){
//#7573调用生命周期挂钩时禁用资料执行防止收集
push target();
定义变量处理程序=vm .$ options[hook];
var info=hook hook
如果(处理程序){
for (var i=0,j=handlers.lengthI ji ) {
invokeWithErrorHandling(handlers[I],vm,null,vm,info);
}
}
如果(虚拟机._hasHookEvent) {
虚拟机.$emit(hook:钩子);
}
pop target();
}
第三个执行推送目标的地方。这是实例化看守人时执行的函数。检查前面的代码,我们似乎也没有看到新观察者的操作:
观察者。原型。get=function get(){
推送目标(这个);
定义变量值;
var vm=this.vm
尝试{
value=this.getter.call(vm,VM);
} catch (e) {
if (this.user) {
handleError(e,vm,( watcher的getter )(这。表达式) \ ));
}否则{
扔e
}
}最后{
//"触摸"每个属性,以便它们都被跟踪为
//深度观察的依赖性
如果(this.deep) {
遍历(值);
}
pop target();
这个。clean up deps();
}
返回值
}
第四个执行推送目标的地方,这就是前面的得到一条数据函数。但是得到一条数据函数的执行位于定义有效$$1函数之前。在执行完得到一条数据函数以后,部门目标已经被重置为空了。
函数获取数据(数据,虚拟机){
//#7573调用数据获取器时禁用资料执行防止集合
push target();
尝试{
返回数据。调用(虚拟机,虚拟机)
} catch (e) {
handleError(e,vm, data());
返回{}
}最后{
pop target();
}
}
看起来,直接触发作曲者并不能让吸气剂中的逻辑正常执行。并且,我们还发现,由于作曲者中也有副目标的判断,所以如果我们找不到副目标的来源,setter的逻辑也无法继续往下走。
寻找Dep.target
那么,到底副目标的值是从哪里来的呢?不用着急,我们回到_init函数的操作继续往下看:
vue。原型。_ init=函数(选项){
var vm=this
//一个用户界面设计(User Interface Design的缩写)
虚拟机._ uid=uid $ 3;
变量开始标记,结束标记
/*伊斯坦布尔忽略if */
如果(配置性能标志){
startTag=vue-perf-start: (vm ._ uid);
endTag=vue-perf-end: (vm ._ uid);
mark(startTag);
}
//避免被观察到的标志
虚拟机._ isVue=true
//合并选项
如果(选项选项. isComponent) {
//优化内部组件实例化
//因为动态选项合并非常慢,而且
//内部组件选项需要特殊处理。
initInternalComponent(vm,options);
}否则{
虚拟机.$options=mergeOptions(
resolveConstructorOptions(VM。建造师),
选项 {},
伏特计
);
}
/*伊斯坦布尔忽略else */
{
初始化代理(虚拟机);
}
//暴露真实的自己
虚拟机._ self=vm
初始化生命周期(虚拟机);
初始化事件(虚拟机);
初始化渲染(虚拟机);
callHook(vm,“创建前”);
初始注入(VM);//在数据/属性之前解析注入
initState(VM);
初始化提供(虚拟机);//解析后提供数据/属性
callHook(vm, created );
/*伊斯坦布尔忽略if */
如果(配置性能标志){
虚拟机._name=formatComponentName(vm,false);
mark(endTag);
测量((‘vue’(VM ._name) init )、startTag、end tag);
}
如果(虚拟机.$options.el) {
虚拟机.$mount(虚拟机.$ options。El);
}
}
我们发现,在_init函数的最后,执行了虚拟机.$mount函数,这个函数做了什么呢?
Vue.prototype.$mount=function(
埃尔,
(使)水合
) {
el=el inBrowser?查询(el):未定义;
返回安装组件(此组件,el,补水)
}
我们继续进入安装组件函数看看:
功能安装组件(
vm,
埃尔,
(使)水合
) {
虚拟机.$ el=el
如果(!虚拟机.$options.render) {
虚拟机.$ options。render=createEmptyVNode
{
/*伊斯坦布尔忽略if */
如果((vm .$options.template vm .$options.template.charAt(0)!==#)
虚拟机.$options.el el) {
警告(
您正在使用某视频剪辑软件的仅运行时版本,其中模板
编译器不可用。要么将模板预编译成
呈现函数,或者使用编译器自带的版本。
伏特计
);
}否则{
警告(
未能安装组件:未定义模板或呈现函数。
伏特计
);
}
}
}
callHook(vm,“挂载前”);
var updateComponent
/*伊斯坦布尔忽略if */
如果(配置性能标志){
updateComponent=function () {
var name=vm ._ name
var id=vm ._ uid
var startTag= vue-perf-start: id;
var end tag= vue-perf-end: id;
mark(startTag);
var vnode=vm ._ render();
mark(endTag);
measure((vue name render ),startTag,end tag);
mark(startTag);
虚拟机._更新(vnode,补水);
mark(endTag);
measure((vue name patch ),startTag,end tag);
};
}否则{
updateComponent=function () {
虚拟机._更新(虚拟机. render(),补水);
};
}
//我们把这个设置为虚拟机.观察器构造函数中的观察器(_ w)
//由于观察器的初始补丁可能调用$forceUpdate(例如在儿童内部
//组件的挂载钩子),它依赖于虚拟机.已经定义了_观察者
新观察器(虚拟机、更新组件、noop 、{
之前:函数之前(){
如果(虚拟机._isMounted!虚拟机._isDestroyed) {
callHook(vm,“更新前”);
}
}
},true/* isRenderWatcher */);
补水=假;
//手动装入实例,调用装入自身
//在其插入的挂钩中为呈现创建的子组件调用安装好的
如果(虚拟机.$vnode==null) {
虚拟机._ isMounted=true
呼叫挂钩(虚拟机,"已安装");
}
返回虚拟机
}
我们惊喜地发现,这里有一个新观察者的操作!真是山重水复疑无路,柳暗花明又一村!这里实例化的看守人是一个用来更新数字正射影像图的观察者。他会依次读取表面(同表面)文件中的模板部分中的所有值。这也就意味着会触发对应的吸气剂。
由于新观察者会执行watcher.get函数,该函数执行推送目标函数,于是副目标被赋值吸气剂。内部的逻辑顺利执行。
getter
至此,我们终于到了某视频剪辑软件的响应式原理的核心。我们再次回到吸气剂,看一看有了副目标以后,getter做了什么:
函数reactiveGetter () {
定义变量值=getter?吸气剂。call(obj):val;
if (Dep.target) {
离开依赖();
if (childOb) {
childob。离开依赖();
if (Array.isArray(value)) {
受抚养人(值);
}
}
}
返回值
}
同样地,我们先不关注提高代码健壮性的细节处理,直接看主线。可以看到,当副目标存在时,执行了依赖于函数。这个函数做了什么呢?我们看看代码:
离开原型。depend=function depend(){
if (Dep.target) {
离开目标。添加dep(这个);
}
}
做的事情也非常简单。就是执行了目标增加函数。但是副目标其实是一个观察者,所以我们要回到看守人的代码:
Watcher.prototype.addDep=函数addDep (dep) {
var id=dep。id;
如果(!this.newDepIds.has(id)) {
这个。新的。添加(id);
这个。新部门。push(dep);
如果(!this.depIds.has(id)) {
离开addsub(this);
}
}
}
同样地,我们先忽略一些次要的逻辑处理,把注意力集中到dep.addSub函数上:
Dep.prototype.addSub=函数addSub (sub) {
这个。潜艇。推(分);
}
也是非常简单的逻辑,把看守人作为一个订阅者推入数组中缓存。至此,getter的整个逻辑走完。此后执行popTarget函数,部门目标被重置为空
setter
我们再次回到业务代码:
模板
差异
span{{ a }}/span
span{{ b }}/span
/div
/模板
脚本类型=javascript
导出默认值{
data() {
返回{
答:0,
乙:0
}
},
已创建(){
//一些逻辑代码
this.a=1
this.b=2
}
}
/脚本
在创造生命周期中,我们触发了两次二传手,二传手执行的逻辑如下:
函数反应设置器(新值){
定义变量值=getter?吸气剂。call(obj):val;
/* eslint-禁用无自比较*/
if (newVal===value (newVal!==newVal值!==value)) {
返回
}
/* eslint-启用无自比较*/
if (customSetter) {
自定义setter();
}
//#7981:对于不带作曲者的访问器属性
如果(getter!setter) { return }
if (setter) {
setter.call(obj,new val);
}否则{
val=newVal
}
childOb=!浅层观察(新val);
离开notify();
}
这里,我们只需要关注作曲者最后执行的函数:dep.notify()。我们看看这个函数做了什么:
离开原型。notify=函数notify(){
//首先稳定订户列表
var subs=this。潜艇。slice();
如果(!config.async) {
//如果没有运行异步,subs在调度程序中不排序
//我们现在需要对它们进行排序,以确保它们能够正确启动
//订单
subs.sort函数(a,b){ return a . id-b . id;});
}
for (var i=0,l=subs.lengthI li ) {
分句[我].update();
}
}
This.subs的每一项元素均为一个观察者。在上面吸气剂章节中,我们只收集到了一个观察者。因为触发了两次二传手,所以subs[0].更新(),即观察器。更新()函数会执行两次。我们看看这个函数做了什么:
Watcher.prototype .更新=函数更新(){
/*伊斯坦布尔忽略else */
如果(this.lazy) {
this.dirty=true
} else if (this.sync) {
这个。run();
}否则{
队列观察器(这个);
}
}
按照惯例,我们直接跳入队列观察者函数:
函数队列观察器(watcher) {
var id=watcher.id
如果(有[id]==null) {
has[id]=true;
如果(!冲洗){
排队。推(守望者);
}否则{
//如果已经刷新,则根据其编号拼接观察器
//如果已经超过它的id,它将立即运行下一个。
var I=队列。长度-1;
而(我索引队列[我].id watcher.id) {
I-;
}
queue.splice(i 1,0,watcher);
}
//将刷新排队
如果(!等待){
等待=真;
如果(!config.async) {
flushSchedulerQueue();
返回
}
next tick(flushSchedulerQueue);
}
}
}
由于编号相同,所以看守人的回调函数只会被推入到长队一次。这里我们再次看到了一个熟悉的面孔:下一个刻度。
函数nextTick (cb,ctx) {
变量_解析
复试
.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;
})
}
}
nextTick函数将回调函数再次包裹一层后,执行timerFunc()
var timerFunc;
// The nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== undefined && isNative(Promise)) {
var p = Promise.resolve();
timerFunc = function () {
p.then(flushCallbacks);
// In problematic UIWebViews, Promise.then doesnt completely break, but
// it can get stuck in a weird state where callbacks are pushed into the
// microtask queue but the queue isnt being flushed, until the browser
// needs to do some other work, e.g. handle a timer. Therefore we can
// "force" the microtask queue to be flushed by adding an empty timer.
if (isIOS) { setTimeout(noop); }
};
isUsingMicroTask = true;
} else if (!isIE && typeof MutationObserver !== undefined && (
isNative(MutationObserver)
// PhantomJS and iOS 7.x
MutationObserver.toString() === [object MutationObserverConstructor]
)) {
// Use MutationObserver where native Promise is not available,
// e.g. PhantomJS, iOS7, Android 4.4
// (#6466 MutationObserver is unreliable in IE11)
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)) {
// Fallback to setImmediate.
// Technically it leverages the (macro) task queue,
// but it is still a better choice than setTimeout.
timerFunc = function () {
setImmediate(flushCallbacks);
};
} else {
// Fallback to setTimeout.
timerFunc = function () {
setTimeout(flushCallbacks, 0);
};
}
timerFunc函数是微任务的平稳降级。他将根据所在环境的支持程度,依次调用Promise、MutationObserver、setImmediate和setTimeout。并在对应的微任务或者模拟微任务队列中执行回调函数。
function flushSchedulerQueue () {
currentFlushTimestamp = getNow();
flushing = true;
var watcher, id;
// Sort queue before flush.
// This ensures that:
// 1. Components are updated from parent to child. (because parent is always
// created before the child)
// 2. A components user watchers are run before its render watcher (because
// user watchers are created before the render watcher)
// 3. If a component is destroyed during a parent components watcher run,
// its watchers can be skipped.
queue.sort(function (a, b) { return a.id - b.id; });
// do not cache length because more watchers might be pushed
// as we run existing watchers
for (index = 0; index < queue.length; index++) {
watcher = queue[index];
if (watcher.before) {
watcher.before();
}
id = watcher.id;
has[id] = null;
watcher.run();
// in dev build, check and stop circular updates.
if (has[id] != null) {
circular[id] = (circular[id] 0) + 1;
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
You may have an infinite update loop + (
watcher.user
? ("in watcher with expression \"" + (watcher.expression) + "\"")
: "in a component render function."
),
watcher.vm
);
break
}
}
}
// keep copies of post queues before resetting state
var activatedQueue = activatedChildren.slice();
var updatedQueue = queue.slice();
resetSchedulerState();
// call component updated and activated hooks
callActivatedHooks(activatedQueue);
callUpdatedHooks(updatedQueue);
// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit(flush);
}
}
回调函数的核心逻辑是执行watcher.run函数:
Watcher.prototype.run = function run () {
if (this.active) {
var value = this.get();
if (
value !== this.value
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value)
this.deep
) {
// set new value
var oldValue = this.value;
this.value = value;
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue);
} catch (e) {
handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\""));
}
} else {
this.cb.call(this.vm, value, oldValue);
}
}
}
}
执行this.cb函数,即watcher的回调函数。至此,所有的逻辑走完。
总结
我们再次回到业务场景:
<template>
<div>
<span>{{ a }}</span>
<span>{{ b }}</span>
</div>
</template>
<script type="javascript">
export default {
data() {
return {
a: 0,
b: 0
}
},
created() {
// some logic code
this.a = 1
this.b = 2
}
}
</script>
虽然我们触发了两次setter,但是对应的渲染函数在微任务中却只执行了一次。也就是说,在dep.notify函数发出通知以后,Vue将对应的watcher进行了去重、排队操作并最终执行回调。
可以看出,两次赋值操作实际上触发的是同一个渲染函数,这个渲染函数更新了多个dom。这就是所谓的批量更新dom。
到此这篇关于Vue批量更新dom的实现步骤的文章就介绍到这了,更多相关Vue批量更新dom 内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。