最近開了一個讀者回饋表單郵箱,無論是對文章的感想或是對部落格的感想,有什麼想回饋的都可以發郵箱跟我說:i_kkkp@163.com

vue-事件渲染的处理

引言

本节我们将讨论如何处理事件,包括如何在虚拟节点中描述事件,如何把事件添加到 DOM 元素上,以及如何更新事件。我们先来解决第一个问题,即如何在虚拟节点中描述事件。事件可以视作一种特殊的属性,因此我们可以约定,在 vnode.props 对象中,凡是以字符串 on 开头的属性都视作事件。例如:

const vnode = {
  type: 'p',
  props: {
    // 使用 onXxx 描述事件
    onClick: () => {
      alert('clicked');
    }
  },
  children: 'text'
};

解决了事件在虚拟节点层面的描述问题后,我们再来看看如何将事件添加到 DOM 元素上。这非常简单,只需要在 patchProps 中调用 addEventListener 函数来绑定事件即可,如下面的代码所示:

function patchProps(el, key, prevValue, nextValue) {
  // 匹配以 on 开头的属性,视其为事件
  if (/^on/.test(key)) {
    // 根据属性名称得到对应的事件名称,例如 onClick ---> click
    const name = key.slice(2).toLowerCase();
    
    // 移除上一次绑定的事件处理函数
    prevValue && el.removeEventListener(name, prevValue);
    // 绑定新的事件处理函数
    el.addEventListener(name, nextValue);
  } else if (key === 'class') {
    // 省略部分代码(处理 class 属性的逻辑)
  } else if (shouldSetAsProps(el, key, nextValue)) {
    // 省略部分代码(处理其他属性的逻辑)
  } else {
    // 省略部分代码(处理其他属性的逻辑)
  }
}

事实上可以更为优化的事件更新机制,避免多次调用 removeEventListeneraddEventListener

function patchProps(el, key, prevValue, nextValue) {
  if (/^on/.test(key)) {
    const name = key.slice(2).toLowerCase();
    let invoker = el.__vei || (el.__vei = {});

    if (nextValue) {
      if (!invoker[name]) {
        // 如果没有 invoker,则创建一个伪造的 invoker 函数
        invoker[name] = (e) => {
          invoker[name].value(e);
        };
      }
      
      // 将真正的事件处理函数赋值给 invoker 函数的 value 属性
      invoker[name].value = nextValue;

      // 绑定 invoker 函数作为事件处理函数
      el.addEventListener(name, invoker[name]);
    } else if (invoker[name]) {
      // 如果新的事件处理函数不存在,且之前绑定的 invoker 存在,则移除绑定
      el.removeEventListener(name, invoker[name]);
      invoker[name] = null;
    }
  } else if (key === 'class') {
    // 省略部分代码(处理 class 属性的逻辑)
  } else if (shouldSetAsProps(el, key, nextValue)) {
    // 省略部分代码(处理其他属性的逻辑)
  } else {
    // 省略部分代码(处理其他属性的逻辑)
  }
}

观察上面的代码,事件绑定主要分为两个步骤。先从 el._vei 中读取对应的 invoker,如果 invoker 不存在,则将伪造的 invoker 作为事件处理函数,并将它缓存到el._vei 属性中。

把真正的事件处理函数赋值给 invoker.value 属性,然后把伪造的 invoker 函数作为事件处理函数绑定到元素上。可以看到,当事件触发时,实际上执行的是伪造的事件处理函数,在其内部间接执行了真正的事件处理函数 invoker.value(e)。

当更新事件时,由于 el._vei 已经存在了,所以我们只需要将invoker.value 的值修改为新的事件处理函数即可。

这样,在更新事件时可以避免一次 removeEventListener 函数的调用,从而提升了性能。实际上,伪造的事件处理函数的作用不止于此,它还能解决事件冒泡与事件更新之间相互影响的问题。但目前的实现仍然存在问题。现在我们将事件处理函数缓存在el._vei 属性中,问题是,在同一时刻只能缓存一个事件处理函数。这意味着,如果一个元素同时绑定了多种事件,将会出现事件覆盖的现象。

const vnode = {
  type: 'p',
  props: {
    // 使用 onXxx 描述事件
    onClick: () => {
      alert('clicked');
    },
    onContextmenu: () => {
      alert('contextmenu');
    }
  },
  children: 'text'
};

// 假设 renderer 是你的渲染器对象
renderer.render(vnode, document.querySelector('#app'));

当渲染器尝试渲染这上面代码中给出的 vnode 时,会先绑定click 事件,然后再绑定 contextmenu 事件。后绑定的contextmenu 事件的处理函数将覆盖先绑定的 click 事件的处理函
数。为了解决事件覆盖的问题,我们需要重新设计 el._vei 的数据结构。我们应该将 el._vei 设计为一个对象,它的键是事件名称,它的值则是对应的事件处理函数,这样就不会发生事件覆盖的现象了.

根据你提供的代码片段,这段代码主要是用于处理 DOM 元素的属性更新,其中包括事件的绑定和解绑逻辑。在这个代码中,它使用了一个 el._vei 的对象来缓存事件处理函数。下面是你提供的代码的一些修正:

function patchProps(el, key, prevValue, nextValue) {
  if (/^on/.test(key)) {
    const invokers = el._vei || (el._vei = {});
    const name = key.slice(2).toLowerCase();
    let invoker = invokers[name];

    if (nextValue) {
      if (!invoker) {
        invoker = el._vei[name] = (e) => {
          if (Array.isArray(invoker.value)) {
            invoker.value.forEach(fn => fn(e));
          } else {
            invoker.value(e);
          }
        };
      }

      invoker.value = nextValue;
      el.addEventListener(name, invoker);
    } else if (invoker) {
      el.removeEventListener(name, invoker);
      el._vei[name] = null;
    }
  } else if (key === 'class') {
    // 处理 class 属性的逻辑
  } else if (shouldSetAsProps(el, key, nextValue)) {
    // 处理其他属性的逻辑
  } else {
    // 处理其他属性的逻辑
  }
}

在这段代码中,我们修改了 invoker 函数的实现。当 invoker函数执行时,在调用真正的事件处理函数之前,要先检查invoker.value 的数据结构是否是数组,如果是数组则遍历它,并逐个调用定义在数组中的事件处理函数。

云计算及应用 vue-render挂载与更新

評論