Vue.js設(shè)計與實現(xiàn)之十三-渲染器的核心功能:掛載與更新02
1、寫在前面
在上篇文章中介紹了虛擬節(jié)點的掛載與更新,以及虛擬DOM節(jié)點上的屬性設(shè)置,封裝了新的卸載函數(shù)unmount。那么,虛擬節(jié)點上的事件又是如何處理的呢,同一個事件設(shè)置多個處理函數(shù),同一個元素綁定多個事件,觸發(fā)事件和綁定事件的時機問題應(yīng)該如何處理?
2、事件的處理
在Vue.js的事件處理先要解決的問題,就是如何在虛擬節(jié)點中描述事件,事件是一種特殊的屬性,在vnode.props對象中以字符串on開頭的屬性都被視作事件。
const vnode = {
type:"p",
props:{
// 同一個事件多個事件處理函數(shù)
onClick:[
()=>{
//...
},
()=>{
//...
}
],
// 同一個元素綁定多個事件
onContextMenu(){
//...
}
},
children:"text"
}
renderer.render(vnode, document.querySelector("#app"));
在上面代碼中,我們看到同一的DOM元素上可以綁定多個事件,同一個事件上又可以有多個事件處理函數(shù)。多次我們修改patchProps函數(shù)中事件處理相關(guān)代碼得到:
patchProps(el, key, prevValue, nextValue){
if(/^on/.test(key)){
const invokers = el._vei || (el._vei = {});
let invoker = invokers[key];
const name = key.slice(2).toLowerCase();
if(nextValue){
if(!invoker){
invoker = el._vei[key] => {
//invoker.value是數(shù)組時,遍歷逐個調(diào)用事件處理函數(shù)
if(Array.isArray(invoker.value)){
invoker.value.forEach(fn=>fn(e));
}else{
invoker.value(e);
}
}
invoker.value = nextValue;
el.addEventListener(name, invoker);
}else{
invoker.value = nextValue;
}
}else if(key === "class"){
//...
}else if(shouleSetAsProps(el, key, nextValue)){
//...
}else{
//...
}
}
}
在上面代碼中,先通過/^on/.test(key)檢測元素上以on開頭的屬性,在綁定事件時偽造事件處理函數(shù)invoker
- 如果invoker不存在時,將invoker作為事件處理函數(shù),緩存到el._vei屬性中
- 將真正的事件處理函數(shù)設(shè)置為invoker.value屬性的值,偽造的事件處理函數(shù)invoker綁定到元素上
將el._vei的數(shù)據(jù)結(jié)構(gòu)設(shè)計為一個對象,鍵即為事件名稱,值為對應(yīng)的事件處理函數(shù),這樣就不會出現(xiàn)事件覆蓋的現(xiàn)象。當(dāng)上面invoker.value的類型是數(shù)組時,數(shù)組中的每個元素都是一個獨立的事件處理函數(shù),且這些事件處理函數(shù)都能夠正確綁定到對應(yīng)元素上。
3、事件冒泡與更新時機問題
在事件處理中,需要注意處理事件冒泡和更新時機結(jié)合導(dǎo)致的問題,事件觸發(fā)的時間會早于事件處理函數(shù)被綁定的時間。
const {effect, ref} = VueReactivity;
const bol = ref(false);
effect(()=>{
//創(chuàng)建vnode
const vnode = {
type:"div",
props:bol.value ? {
onClick(){
//...
}
}:{},
children:[{
type:"p",
props:{
onClick(){
bol.value = true;
}
},
children:"pingping"
}]
}
//渲染vnode
renderer.render(vnode, document.querySelector("#app"));
})
在上面代碼中進行理論分析,首次渲染后由于bol.value的初始值為false,對此渲染器并不會給div元素綁定點擊事件。在鼠標(biāo)點擊p元素后,bol.value的值變更為true,看到點擊事件會從子元素p冒泡到父元素div上,但是div元素又沒有綁定事件,因此啥也不發(fā)生。
但是,事實上在點擊p元素時,父元素div的click事件觸發(fā)了執(zhí)行函數(shù)的執(zhí)行。這是因為bol是個響應(yīng)式數(shù)據(jù),在點擊p元素后,bol.value的值發(fā)生改變,會觸發(fā)副作用函數(shù)的重新執(zhí)行。而在更新階段,渲染器會給div元素綁定click事件,在更新完后點擊事件才從p元素冒泡到div元素。
觸發(fā)事件的時機與事件綁定的時機的聯(lián)系
在一個事件觸發(fā)時,目標(biāo)元素上還沒有綁定相關(guān)的事件處理函數(shù),因此屏蔽所有綁定事件時機要晚于觸發(fā)時間的事件處理函數(shù)的執(zhí)行。
patchProps(el, key, prevValue, nextValue){
if(/^on/.test(key)){
const invokers = el._vei || (el._vei = {});
let invoker = invokers[key];
const name = key.slice(2).toLowerCase();
if(nextValue){
if(!invoker){
invoker = el._vei[key] => {
//e.timeStamp是事件發(fā)生的時間,如果事件觸發(fā)的時機早于事件綁定的時間,則不執(zhí)行事件處理函數(shù)
if(e.timeStamp < invoker.attached) return;
//invoker.value是數(shù)組時,遍歷逐個調(diào)用事件處理函數(shù)
if(Array.isArray(invoker.value)){
invoker.value.forEach(fn=>fn(e));
}else{
invoker.value(e);
}
}
invoker.value = nextValue;
// 添加invoker.attached屬性,存儲事件處理函數(shù)被綁定的時間
invoker.attached = performance.now();
el.addEventListener(name, invoker);
}else{
invoker.value = nextValue;
}
}else if(key === "class"){
//...
}else if(shouleSetAsProps(el, key, nextValue)){
//...
}else{
//...
}
}
}
在上面代碼中,給偽造的事件處理函數(shù)添加了invoker.attached屬性,用于存儲事件處理函數(shù)被綁定的時間。在invoker執(zhí)行的時候,通過事件對象e.timeStamp獲取事件發(fā)生的時間,比較兩者的時間,如果事件觸發(fā)的時機早于事件綁定的時間,則不執(zhí)行事件處理函數(shù)。
4、寫在最后
在本文中主要討論了事件的處理,介紹了在虛擬節(jié)點上綁定事件,如何綁定和更新事件。同時,還介紹了如何處理觸發(fā)事件與更新時機的問題,屏蔽所有綁定事件時機要晚于觸發(fā)時間的事件處理函數(shù)的執(zhí)行。