项目中遇到的坑描述:
在vue中,created的时候,发送请求,并使用返回的数据进行渲染DOM节点,然后在mounted中操作DOM节点的时候,发现得到的DOM节点是undefined
原因:
在created里面开始渲染的DOM节点,但执行到mounted的时候并没有渲染结束,所以找不到DOM元素。因为赋值操作只完成了数据模型的改变并没有完成视图更新。
解决:
使用vm.$nextTick()
官方文档介绍:
将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,然后等待 DOM 更新。它跟全局方法 Vue.nextTick 一样,不同的是回调的 this 自动绑定到调用它的实例上。
示例:
new Vue({
// ...
methods: {
// ...
example: function () {
// 修改数据
this.message = 'changed'
// DOM 还没有更新
this.$nextTick(function () {
// DOM 现在更新了
// `this` 绑定到当前实例
this.doSomethingElse()
})
}
}
})
源码解读
定义一些变量
var callbacks = []; // 缓存函数的数组
var pending = false; // 是否正在执行
var timerFunc; // 保存着要执行的函数
function nextTickHandler () {
pending = false;
// 拷贝出函数数组副本
var copies = callbacks.slice(0);
// 把函数数组清空
callbacks.length = 0;
// 依次执行函数
for (var i = 0; i < copies.length; i++) {
copies[i]();
}
}
这个函数就是$nextTick内部实际调用的函数
接下来的目标就是延迟调用这个函数了,目标就是把传进来的函数延迟到DOM更新完之后再使用
接下来它用了三种方法,依次优雅降级的使用JavaScript方法做到这一点。
promise.then延迟调用
Promise.then方法将函数延迟到当前函数调用栈最末端,也就是函数调用栈最后调用该函数。从而做到延迟。
if (typeof Promise !== 'undefined' && isNative(Promise)) {
var p = Promise.resolve();
var logError = function (err) { console.error(err); };
timerFunc = function () {
p.then(nextTickHandler).catch(logError);
if (isIOS) { setTimeout(noop); }
};
}
MutationObserver监听变化
else if (typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
var counter = 1;
var observer = new MutationObserver(nextTickHandler);
var textNode = document.createTextNode(String(counter));
observer.observe(textNode, {
characterData: true
});
timerFunc = function () {
counter = (counter + 1) % 2;
textNode.data = String(counter);
};
}
MutationObserver是h5新加的一个功能,其功能是监听dom节点的变动,在所有dom变动完成后,执行回调函数。
具体有一下几点变动的监听
- childList:子元素的变动
- attributes:属性的变动
- characterData:节点内容或节点文本的变动
- subtree:所有下属节点(包括子节点和子节点的子节点)的变动
可以看出,以上代码是创建了一个文本节点,来改变文本节点的内容来触发的变动,因为我们在数据模型更新后,将会引起dom节点重新渲染,所以,我们加了这样一个变动监听,用一个文本节点的变动触发监听,等所有dom渲染完后,执行函数,达到我们延迟的效果。
setTimeout延迟器
利用了setTimeout的延迟原理,setTimeout(func,0)将func函数延迟到下一次调用栈的开始,也就是当前函数执行完毕后再接着执行该函数
else {
timerFunc = function () {
setTimeout(nextTickHandler, 0);
};
}
闭包函数
return function queueNextTick (cb, ctx) {
var _resolve;
callbacks.push(function () {
if (cb) { cb.call(ctx); }
if (_resolve) { _resolve(ctx); }
});
// 如果没有函数队列在执行才执行
if (!pending) {
pending = true;
timerFunc();
}
// promise化
if (!cb && typeof Promise !== 'undefined') {
console.log('进来了')
return new Promise(function (resolve) {
_resolve = resolve;
})
}
}
每次添加函数的时候,都会向callbacks这个函数数组中入栈,然后监听当前是否在执行,如果没有,执行函数。if是为了promise化
promise化
this.$nextTick(function () {
})
// promise化
this.$nextTick().then(function () {
}.bind(this))
直接调用$nextTick函数,然后使用promise格式去书写代码,不过这个then里面需要手动绑定this,vue内部没有做处理