场景说明

最近使用Vue全家桶做后台系统的时候,遇到了一个很奇葩的问题:有一个输入框只允许输入数字,当输入其它类型的数据时,输入的内容会被重置为null。为了实现这一功能,使用了一个父组件和子组件。为了方便陈述,这里将业务场景简化,具体代码如下:

// 父组件
<template><Input v-model="form.a" @on-change="onChange"></Input>
</template>
<script type="javascript">
export default {data() {return {form: {a: null}}},methods: {async onChange(value) {if (typeof value !== 'number') {// await this.$nextTick()this.form.a = null}   }}
}
</script>// 子组件
<template><input v-model="currentValue" @input="onInput" />
</template>
<script type="javascript">
export default {name: 'Input',props: {value: {type: [Number, Null],default: null }},data() {return {currentValue: null}},methods: {onInput(event) {const value = event.target.valuethis.$emit('input', value)const oldValue = this.valueif (oldValue === value) returnthis.$emit('on-change', value)}},watch: {value(value, oldValue) {this.currentValue = value}}
}
</script>

将以上代码放到项目中去运行,你会很神奇地发现,在输入框输入字符串’abc’之后,输入框的值并没有被重置为空,而是保持为’abc’没有变化。在将注释的nextTick取消注释以后,输入框的值被重置为空。真的非常神奇。

其实之前有好几次同事也碰到了类似的场景:数据层发生了变化,dom并没有随之响应。在数据层发生变化以后,执行一次nextTick,dom就按照预期地更新了。这样几次以后,我们甚至都调侃:遇事不决nextTick。

代码执行顺序

那么,到底nextTick做了什么呢?这里以上面的代码为例,我们先来理一理我们代码是怎么执行的。具体来说,以上代码执行顺序如下:

  1. form.a初始值为null
  2. 用户输入字符串abc
  3. 触发input事件,form.a的值改为abc
  4. 触发on-change事件,form.a的值改为null
  5. 由于form.a的值到这里还是为null
  6. 主线程任务执行完毕,检查watch的回调函数是否需要执行。

这个顺序一理,我们就发现了输入框展示abc不置空的原因:原来form.a的值在主线程中间虽然发生了变化,但是最开始到最后始终为null。也就是说,子组件的props的value没有发生变化。自然,watch的回调函数也就不会执行。

但是这样一来,我们就有另外一个问题了:为什么触发input事件,form.a的值改为null的时候,没有触发watch的回调呢?为了说明这一点,我们需要深入Vue源码,看看$emit和watch的回调函数分别是在什么时候执行的。

$emit做了什么?

我们首先看看$emit对应的源码。由于Vue 2.X版本源码是使用flow写的,无形中增加了理解成本。考虑到这一点,我们直接找到Vue的dist包中的vue.js文件,并搜索emit函数

Vue.prototype.$emit = function (event) {var vm = this;{var 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 templates. " +"You should probably use \"" + (hyphenate(event)) + "\" instead of \"" + event + "\".");}}var cbs = vm._events[event];if (cbs) {cbs = cbs.length > 1 ? toArray(cbs) : cbs;var args = toArray(arguments, 1);var info = "event handler for \"" + event + "\"";for (var i = 0, l = cbs.length; i < l; i++) {invokeWithErrorHandling(cbs[i], vm, args, vm, info);}}return vm
};function invokeWithErrorHandling (handler,context,args,vm,info
) {var res;try {res = args ? handler.apply(context, args) : handler.call(context);if (res && !res._isVue && isPromise(res) && !res._handled) {res.catch(function (e) { return handleError(e, vm, info + " (Promise/async)"); });// issue #9511// avoid catch triggering multiple times when nested callsres._handled = true;}} catch (e) {handleError(e, vm, info);}return res
}

源码的内容其实很简单,就是把提前注册(或者说订阅)的函数放到一个数组中,执行$emit函数时就把数组中的函数一一取出并执行。可以看出,这是一个发布-订阅模式的使用。

也就是说,emit的执行是同步的。那么,watch是怎么执行的呢?相比之下,watch的执行会比较繁琐。理解了watch的流程,也就理解了Vue的核心。

首先,在初始化Vue组件时,有一个initWatch函数,我们来看看这个函数做了什么。

function initWatch (vm, watch) {for (var key in watch) {var handler = watch[key];if (Array.isArray(handler)) {for (var i = 0; i < handler.length; i++) {createWatcher(vm, key, handler[i]);}} else {createWatcher(vm, key, handler);}}
}function createWatcher (vm,expOrFn,handler,options
) {if (isPlainObject(handler)) {options = handler;handler = handler.handler;}if (typeof handler === 'string') {handler = vm[handler];}return vm.$watch(expOrFn, handler, options)
}Vue.prototype.$watch = function (expOrFn,cb,options
) {var vm = this;if (isPlainObject(cb)) {return createWatcher(vm, expOrFn, cb, options)}options = options || {};options.user = true;var watcher = new Watcher(vm, expOrFn, cb, options);if (options.immediate) {try {cb.call(vm, watcher.value);} catch (error) {handleError(error, vm, ("callback for immediate watcher \"" + (watcher.expression) + "\""));}}return function unwatchFn () {watcher.teardown();}
}var Watcher = function Watcher (vm,expOrFn,cb,options,isRenderWatcher
) {this.vm = vm;if (isRenderWatcher) {vm._watcher = this;}vm._watchers.push(this);// optionsif (options) {this.deep = !!options.deep;this.user = !!options.user;this.lazy = !!options.lazy;this.sync = !!options.sync;this.before = options.before;} else {this.deep = this.user = this.lazy = this.sync = false;}this.cb = cb;this.id = ++uid$2; // uid for batchingthis.active = true;this.dirty = this.lazy; // for lazy watchersthis.deps = [];this.newDeps = [];this.depIds = new _Set();this.newDepIds = new _Set();this.expression = expOrFn.toString();// parse expression for getterif (typeof expOrFn === 'function') {this.getter = expOrFn;} else {this.getter = parsePath(expOrFn);if (!this.getter) {this.getter = noop;warn("Failed watching path: \"" + expOrFn + "\" " +'Watcher only accepts simple dot-delimited paths. ' +'For full control, use a function instead.',vm);}}this.value = this.lazy? undefined: this.get();
}function parsePath (path) {if (bailRE.test(path)) {return}var segments = path.split('.');return function (obj) {for (var i = 0; i < segments.length; i++) {if (!obj) { return }obj = obj[segments[i]];}return obj}
}Watcher.prototype.get = function get () {pushTarget(this);var value;var 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 {// "touch" every property so they are all tracked as// dependencies for deep watchingif (this.deep) {traverse(value);}popTarget();this.cleanupDeps();}return value
}function defineReactive$$1 (obj,key,val,customSetter,shallow
) {var dep = new Dep();var property = Object.getOwnPropertyDescriptor(obj, key);if (property && property.configurable === false) {return}// cater for pre-defined getter/settersvar getter = property && property.get;var setter = property && property.set;if ((!getter || setter) && arguments.length === 2) {val = obj[key];}var childOb = !shallow && observe(val);Object.defineProperty(obj, key, {enumerable: true,configurable: true,get: function reactiveGetter () {var value = getter ? getter.call(obj) : val;if (Dep.target) {dep.depend();if (childOb) {childOb.dep.depend();if (Array.isArray(value)) {dependArray(value);}}}return value},set: function reactiveSetter (newVal) {var value = getter ? getter.call(obj) : val;/* eslint-disable no-self-compare */if (newVal === value || (newVal !== newVal && value !== value)) {return}/* eslint-enable no-self-compare */if (customSetter) {customSetter();}// #7981: for accessor properties without setterif (getter && !setter) { return }if (setter) {setter.call(obj, newVal);} else {val = newVal;}childOb = !shallow && observe(newVal);dep.notify();}});
}var Dep = function Dep () {this.id = uid++;this.subs = [];
}Dep.prototype.addSub = function addSub (sub) {this.subs.push(sub);
};Dep.prototype.removeSub = function removeSub (sub) {remove(this.subs, sub);
};Dep.prototype.depend = function depend () {if (Dep.target) {Dep.target.addDep(this);}
};Dep.prototype.notify = function notify () {// stabilize the subscriber list firstvar subs = this.subs.slice();if (!config.async) {// subs aren't sorted in scheduler if not running async// we need to sort them now to make sure they fire in correct// ordersubs.sort(function (a, b) { return a.id - b.id; });}for (var i = 0, l = subs.length; i < l; i++) {subs[i].update();}
}Watcher.prototype.update = function update () {/* istanbul ignore else */if (this.lazy) {this.dirty = true;} else if (this.sync) {this.run();} else {queueWatcher(this);}
}Dep.target = null;
var targetStack = [];function pushTarget (target) {targetStack.push(target);Dep.target = target;
}function popTarget () {targetStack.pop();Dep.target = targetStack[targetStack.length - 1];
}

我们看到,watch相关联的函数接近20个。这么多函数在来回跳的时候,很容易把逻辑弄丢了。这里我们来讲一讲整个流程。

在初始化Vue实例时,执行initWatch,initWatch函数往下走,创建了一个watcher实例。watcher实例执行了getter函数,getter函数读取了data某个属性的值,因此触发了Object.defineProperty中的get函数。get函数执行了dep.depend函数,这个函数用于收集依赖。所谓的依赖其实就是回调函数。在我们说的这个例子中,就是value的watch回调函数。

讲到这里,我们发现watch的回调函数只是在这里进行了注册,还没有执行。那么,watch真正的执行是在哪里呢?我们回到最开始代码的执行顺序来看。在第3步的时候,form.a=abc,这里有一个设置的操作。这个操作触发了Object.defineProperty的set函数,set函数执行了dep.notify函数。执行了update函数,update函数的核心就是queueWatcher函数。为了更好地说明,我们把queueWatcher函数单独列出来看看。

function queueWatcher (watcher) {var id = watcher.id;if (has[id] == null) {has[id] = true;if (!flushing) {queue.push(watcher);} else {// if already flushing, splice the watcher based on its id// if already past its id, it will be run next immediately.var i = queue.length - 1;while (i > index && queue[i].id > watcher.id) {i--;}queue.splice(i + 1, 0, watcher);}// queue the flushif (!waiting) {waiting = true;if (!config.async) {flushSchedulerQueue();return}nextTick(flushSchedulerQueue);}}
}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 component's 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 component's 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 watchersfor (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 statevar activatedQueue = activatedChildren.slice();var updatedQueue = queue.slice();resetSchedulerState();// call component updated and activated hookscallActivatedHooks(activatedQueue);callUpdatedHooks(updatedQueue);// devtool hook/* istanbul ignore if */if (devtools && config.devtools) {devtools.emit('flush');}
}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 valuevar 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);}}}
}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-lineif (!cb && typeof Promise !== 'undefined') {return new Promise(function (resolve) {_resolve = resolve;})}
}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 doesn't completely break, but// it can get stuck in a weird state where callbacks are pushed into the// microtask queue but the queue isn't 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.xMutationObserver.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);};
}

在queueWatcher函数中,我们看到了熟悉的面孔:nextTick。我们发现,nextTick就是一个微任务的平稳降级:它将根据所在环境,依次使用Promise、MutationObserver、setImmediate以及setTimeout执行任务。我们看到,执行form.a=abc时,实际上是先注册了一个微任务,在这里我们可以理解为watch回调函数的包裹函数。这个微任务将在主线程任务走完以后执行,因此它将被先挂起。

随后主线程执行了form.a=null,再次触发了setter。由于都是form.a注册的,在推入微任务队列前会去重,避免watch的回调多次执行。到这里,主线程任务执行完成,微任务队列中watcher回调函数的包裹函数被推出执行,由于form.a的值始终都为null,因此不会执行回调函数。

在加入$nextTick函数以后,在form.a=null之前先执行了nextTick函数,nextTick函数执行了watcher的回调函数的包裹函数,此时form.a的值为abc,旧的值和新的值不一样,因此执行了watch回调函数。至此,整个逻辑就理顺了。

后话

没想到,一个简简单单nextTick的使用居然关系到了Vue的核心原理!

本文转自 https://juejin.cn/post/6976246978850062367,如有侵权,请联系删除。

nextTick的理解和作用相关推荐

  1. Vue中$nextTick的理解

    Vue中$nextTick的理解 Vue中$nextTick方法将回调延迟到下次DOM更新循环之后执行,也就是在下次DOM更新循环结束之后执行延迟回调,在修改数据之后立即使用这个方法,能够获取更新后的 ...

  2. C this指针的理解和作用

    C  程序到 C 程序的翻译  要想理解 C 的 this 指针,我们可以先把下面的 C 代码转换成 C 代码: C 语言是没有类定义 class 关键词,但是有跟 class 类似的定义,那就是 s ...

  3. springboot 对象 空指针_C++ this指针的理解和作用

    C++ 程序到 C 程序的翻译  要想理解 C++ 的 this 指针,我们可以先把下面的 C++ 代码转换成 C 代码:C 语言是没有类定义 class 关键词,但是有跟 class 类似的定义,那 ...

  4. vue openlayer单击地图事件循环多次执行_Vue中$nextTick的理解

    Vue中$nextTick方法将回调延迟到下次DOM更新循环之后执行,也就是在下次DOM更新循环结束之后执行延迟回调,在修改数据之后立即使用这个方法,能够获取更新后的DOM.简单来说就是当数据更新时, ...

  5. vue nextTick深入理解-vue性能优化、DOM更新时机、事件循环机制

    一.定义[nextTick.事件循环] nextTick的由来: 由于VUE的数据驱动视图更新,是异步的,即修改数据的当下,视图不会立刻更新,而是等同一事件循环中的所有数据变化完成之后,再统一进行视图 ...

  6. C++ this指针的理解和作用

    微信公众号:「小林coding」 用简洁的方式,分享编程小知识. 01 C++ 程序到 C 程序的翻译 要想理解 C++ 的 this 指针,我们先把下面的 C++ 代码转换成 C 的代码 class ...

  7. 理解激活函数作用,看这篇文章就够了!

    转自微信公众号: 机器学习算法与自然语言处理 查阅资料和学习,大家对神经网络中激活函数的作用主要集中下面这个观点: 激活函数作用 激活函数是用来加入非线性因素的,解决线性模型所不能解决的问题. 下面我 ...

  8. Vue使用nextTick的原因和作用

    在下次 DOM 更新循环结束之后执行延迟回调.在修改数据之后立即使用这个方法,获取更新后的 DOM. 疑问: DOM 更新循环是指什么? 下次更新循环是什么时候? 修改数据之后使用,是加快了数据更新进 ...

  9. js闭包的理解和作用

    一.为什么引入闭包 JS为每个变量对象定了作用域,在ES5 中只有全局作用域和函数作用域,没有块级作用域,由内向外形成作用域链,函数外部不能访问函数内部作用域的局部变量.在实际开发中会带来很多不便. ...

最新文章

  1. SpringBoot-web开发(四): SpringMVC的拓展、接管(源码分析)
  2. 这项技术厉害了!让旅行者 2 号从星际空间发首批数据!
  3. 市面上不成熟的系统Java_回顾java基础知识
  4. 用Broadcast广播在activity之间、fragment之间、activity和fragment之间相互传数据
  5. 双显示器设置:如何设置一台电脑两个显示器
  6. 如何提升大规模Transformer的训练效果?Primer给出答案
  7. mysql数据存储和函数_MySQL数据库——存储和函数
  8. Java高级语法笔记-内部类
  9. window 快捷键
  10. python---之round
  11. FRR BGP协议分析11 -- ZEBRA初始化
  12. 大规模均衡分割与层次聚类
  13. oracle数据库双机热备原理,oracle数据库双机热备方案
  14. 图形验证码php点击刷新,ThinkPHP实现点击图片刷新验证码
  15. 好看又实用的英文字体
  16. java模拟器电脑版怎么安装教程_电脑Java模拟器安装使用教程
  17. Codeforces Young Physicist
  18. 通过Nginx搭建直播带货平台的直播服务器
  19. 泰拉瑞亚服务器一直显示什么,《泰拉瑞亚》Steam联机总是掉?教你一个稳定联机方法...
  20. Python WindowsError

热门文章

  1. 网易邮箱大师上使用icloud邮箱
  2. Java web实时进度条整个系统共用(如java上传、下载进度条、导入、导出excel进度条等)...
  3. C盘爆满,电脑卡顿,笔记本电脑一键重置,让电脑运行重新变快!
  4. 借道IIS搭建企业内部Web方式文件共享平台
  5. 从荣耀V20看技术人怎么销售自己
  6. 简单分享,阿里巴巴测试岗4轮面经(已拿34K+ offer)
  7. 【go】mac下brew升级golang
  8. 菜鸟浅谈自己学习Java过程的经历
  9. android 菜单一行两列,Android RadioGroup 横向显示(两行两列)
  10. win10系统 没有wifi图标 WiFi列表没有显示