vue 问题笔记 ref获取不到指定的DOM节点问题解决
在vue中不建议操作DOM,但是如果我们在没办法的情况下,可以通过ref来进行DOM操作,而在使用DOM时,可能会有获取不到ref对应DOM节点的问题像下面这种情况,这是执行了console.log(this.$refs)的情况在用console打印出来的时候明明看到可以按下箭头显示我们要的组件内容,但是在花括号里面却写的是undefined,此时如果要获取这个节点的属性值,会报无法从...
在vue中不建议操作DOM,但是如果我们在没办法的情况下,可以通过ref来进行DOM操作,而在使用DOM时,可能会有获取不到ref对应DOM节点的问题
像下面这种情况,这是执行了console.log(this.$refs)的情况
在用console打印出来的时候明明看到可以按下箭头显示我们要的组件内容,但是在花括号里面却写的是undefined,此时如果要获取这个节点的属性值,会报无法从undefined取值的错误。
之所以会这样,是因为vue在更新DOM是异步的,只要侦听到数据发生变化,Vue将开启一个队列,并缓存在同一事件循环中发生的所有数据变更。即是说,在我们更新数据时,组件不会立即重新渲染,而是在刷新队列时才进行渲染。
在上面代码中,DOM节点还没创建出来的时候,就执行了这个方法,导致无法正确地获取DOM节点,所以我们应该在DOM节点创建后才执行这个方法。
对于这个问题,其实只要做个异步处理就可以了,最简单的做法就是让这个方法延迟一点执行,使用一个setTimeout来将这个方法延迟执行。
setTimeout(function(){
console.log(this.$refs);
},time)
虽然setTimeout可以解决这个问题,但是这个time我们无法确定,因为影响解析的因素很多,如果延迟的时间太长又可能造成其他问题,所以我们采用另一个方法:Vue.nextTick(callback)。
Vue.nextTick(callback)可以让里面的方法在DOM节点全部解析后才执行。在组件内使用时可以直接使用this来代替Vue,this会自动绑定到当前的Vue实例上。
this.$nextTick(function(){
console.log(this.$refs);
})
同时,因为$nextTick()返回一个Promise对象,所以可以使用async/await语法完成同样的事情。
await this.$nextTick();
console.log(this.$refs);
至此,问题解决
问题解决了,但是原理其实说的很浅,之前只是个小笔记,既然那么多人看,我就把相关的原理也写出来吧
$nextTick原理
上文说到了,我们使用ref获取DOM节点时,因为节点可能还没渲染好,所以我们无法正常获取到,那么vue内部的$nextTick是怎么做到在DOM节点渲染好后才去获取的呢
我们先稍微看一下源码
export const nextTick = (function () {
const callbacks = []
let pending = false
let timerFunc
function nextTickHandler () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
if (typeof Promise !== 'undefined' && isNative(Promise)) { // 使用promise
var p = Promise.resolve()
var logError = err => { console.error(err) }
timerFunc = () => {
p.then(nextTickHandler).catch(logError)
if (isIOS) setTimeout(noop)
}
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) { // 使用MutationObserver
var counter = 1
var observer = new MutationObserver(nextTickHandler)
var textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
} else { // 使用setTimeout
timerFunc = () => {
setTimeout(nextTickHandler, 0)
}
}
return function queueNextTick (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()
}
if (!cb && typeof Promise !== 'undefined') {
return new Promise((resolve, reject) => {
_resolve = resolve
})
}
}
})()
初始化变量
接下来,开始解析源码,从上到下,我们首先看到,其定义了三个变量
const callbacks = [] // 缓存要执行的函数
let pending = false // 是否正在执行
let timerFunc // 保存要执行的函数
然后,在这里采用了闭包,在函数内创建了$nextTick真正调用的函数
function nextTickHandler () {
pending = false // 改为执行结束
const copies = callbacks.slice(0) // 拷贝函数数组
callbacks.length = 0 // 清空函数数组
for (let i = 0; i < copies.length; i++) { // 执行函数数组中的函数
copies[i]()
}
}
可以注意到,在上面源码中我在三处加了注释,正是$nextTick根据兼容性的不同,采取不同的措施,分别使用promise,MutationObserver和setTimeout来实现
异步执行
promise
if (typeof Promise !== 'undefined' && isNative(Promise)) { // 使用promise
var p = Promise.resolve()
var logError = err => { console.error(err) }
timerFunc = () => {
p.then(nextTickHandler).catch(logError)
if (isIOS) setTimeout(noop)
}
}
这里采用了promise.then,将函数的执行延迟到函数调用栈的最末端,这里可以看成是将该函数放到微任务队列里,关于JavaScript的执行顺序,可见我的另一篇博客详解JavaScript异步执行顺序
MutationObserver
else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) { // 使用MutationObserver
var counter = 1
var observer = new MutationObserver(nextTickHandler)
var textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
}
MutationObserver接口提供了监视对DOM树所做更改的能力,当其监听的DOM发生改变,且所有DOM变动完成后,就会触发其回调函数,所以可以用在这里
当使用MutationObserver时,会创建一个文本节点,使用MutationObserver监听这个文本节点的变化。而在这段源码中,通过在内部写一个方法timerFunc来触发这个文本节点的变化,在调用$nextTick时,通过去调用timerFunc方法,来触发这个MutationObserver对应的回调函数,而因为DOM的改变是在同步代码执行完后执行,所以这样可以达到异步执行回调方法的目的
而这个MutationObserver的回调同样是微任务,所以可以替代promise.then,只是需要额外地去创建一个文本节点
setTimeout
else { // 使用setTimeout
timerFunc = () => {
setTimeout(nextTickHandler, 0)
}
}
setTimeout其实很简单,只是单纯地将一个任务放到的任务队列的尾部
返回函数
return function queueNextTick (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()
}
if (!cb && typeof Promise !== 'undefined') {
return new Promise((resolve, reject) => {
_resolve = resolve
})
}
}
在这里,将函数push进callbacks中,如果当前不是等待状态,就直接执行函数,最后一个promise是将函数做了一个promise化
相关的打印顺序
上面说到了源码中,如果promise可以使用,就会使用到promise.then,而MutationObserver可以使用时,也会用到相应的方法,而在这两种情况中,都是使用的微任务,所以就会出现下面的情况
click:function() {
console.log('start')
setTimeout(()=>{
console.log('setTimeout1')
},0)
this.$nextTick(()=>{
console.log('$nextTick1')
setTimeout(()=>{
console.log('setTimeout2')
},0)
this.$nextTick(()=>{
console.log('$nextTick2')
})
})
console.log('end')
}
当有这样的代码时,打印的结果是
start
end
nextTick1
nextTick2
setTimeout1
setTimeout2
相应的打印顺序理解,其实也很简单,只要我们将$nextTick看成微任务就可以了,这里就不展开说了
更多推荐
所有评论(0)