關於vue中image控制元件,onload事件裡,event.target 為null的奇怪問題探討

火星大能猫發表於2024-06-19

廢話不多說(主要文筆比較差),直接上程式碼

一個簡單的demo,如下

<img :src="orginalImgSrc" style="display: none;" crossOrigin="Anonymous" @load="imgLoaded">

vue程式碼

  imgLoaded(e) {
                debugger
                console.log('event',e);
                console.log('target',e.target);               
                return;            

            }

這時候,會發現

console.log裡的event物件,target始終為null,但是如果debugger進去看,又是可以看到taget物件的

下圖為debugger模式下:

下面為控制檯

可以清晰的看到,控制檯的輸出裡,target物件始終為null,由於我一開始只輸出了event物件,看到target物件為null後百思不得其解,認定從這裡取target物件,必然也是null

實際上此處是可以直接獲取到target物件的.

那麼為什麼呢?

參考相關資料後得知

這個問題是事件迴圈機制導致的,在JavaScript中,事件處理程式是在事件迴圈任務佇列中非同步執行的.

但@load事件觸發的時候,event事件物件會被正確傳遞給事件處理程式,demo中為imgLoaded(e),在控制檯輸出之前,JavaScript引擎已經執行了後續的程式碼.

但是在這個過程中,某些瀏覽器最佳化或者垃圾回收機制導致e.target引用被移除或者清除了,導致第一次列印e.target的時候,顯示為null

第二次列印的時候.e.target是一條新的一句,JavaScript的引擎會重新計算e.target的值,所以又獲取到了正確的元素引用.

因此通常建議在程式開頭,將e.target儲存到一個區域性變數中.然後再後續的程式碼中使用該變數,防止JavaScript的引擎執行機制丟失對目標元素的引用

再舉個例子,會被最佳化掉target的例子

   const tempImg = new Image();
tempImg.setAttribute('crossOrigin', 'Anonymous');
tempImg.src = this.imgResize;

console.log('*****************');
tempImg.onload = (ev) => {
console.log('onload', ev);
console.log('ev.target',ev.target);
setTimeout(() => {
console.log('settimeout---target:',ev.target) // <- HTMLElement
}, 0)
}

輸出結果如圖

是不是有點奇怪,上面明明說.e.target應該會重新計算的,為什麼在settimeout裡,並沒有重新計算呢?

當 onload 事件觸發時,ev 引數就是一個指向事件物件的引用。但是,一旦事件處理程式執行完畢,這個引用就會被清除,以節省記憶體。
在你的程式碼中,setTimeout 函式會在當前執行上下文結束後,將其回撥函式加入事件迴圈佇列。當事件迴圈再次執行時,它會從事件佇列中取出回撥函式並執行。
問題在於,當 setTimeout 的回撥函式執行時,原始的事件物件 ev 已經不存在了,因此 ev.target 自然就變成了 null。
這個問題在基於 Blink 核心的瀏覽器中尤其常見,如 Chrome、Edge 等。它們會在事件處理程式執行完畢後,主動清除對事件物件的引用,以最佳化記憶體使用。

但是,如果我在setTimeOut裡輸出ev也就是event物件呢?event物件依然有,只不過target還是null,為什麼呢?

這是因為事件物件的生命週期與目標元素的引用生命週期不同。
在 Web 瀏覽器中,事件物件 ev 本身是一個物件,它包含了許多屬性,如 type、currentTarget 等,用於描述事件的細節。但其中的 target 屬性指向觸發該事件的 DOM 元素。
當事件處理程式執行完畢後,瀏覽器會清除對目標元素 (ev.target) 的引用,以釋放記憶體。但事件物件 ev 本身並不會被立即銷燬,它可能還會在其他地方被使用或引用。
所以在程式碼中,當 setTimeout 回撥函式執行時,雖然 ev.target 已經變成了 null,但 ev 物件本身仍然存在,只是它的 target 屬性已經被清除了。這就解釋了為什麼 console.log('settimeout---ev:', ev) 能夠列印出事件物件,但 ev.target 卻是 null。
這種行為是瀏覽器的最佳化機制,它只清除了對目標元素的引用,而保留了事件物件本身,以防止意外地破壞了其他依賴於該事件物件的程式碼。
總的來說,在非同步操作中訪問事件物件的屬性時,最安全的做法是將需要使用的屬性值提前儲存到其他變數中,而不是直接引用事件物件的屬性,因為這些屬性的生命週期可能會比事件物件本身更短。

所以如果用區域性變數接收,就可以正確輸出,程式碼如下

 const tempImg = new Image();
                tempImg.setAttribute('crossOrigin', 'Anonymous');
                tempImg.src = this.imgResize;

                console.log('*****************');
                tempImg.onload = (ev) => {
                    console.log('onload', ev);                 
                    console.log('ev.target',ev.target);
                    const t_target=ev.target;
                    setTimeout(() => {
                        console.log('settimeout---target:',t_target) // <- HTMLElement
                    }, 0)

參考相關資料:

1 https://github.com/vuejs/vue/issues/7027

2 https://claude.ai/

相關文章