一个removeEventListener引发的思考

7次阅读

共计 3228 个字符,预计需要花费 9 分钟才能阅读完成。

起因

最近在看以前的代码时,发现年初在熟悉 react hooks 新特性时写下了这样一段代码:

let i = 0;
function Test(props) {const { loading, error, startFetch} = props;
  useEffect(() => {const $btn = document.querySelector('.btn'); // .info-con 存在于外层的 dom 中
    $btn.addEventListener('click', () => {const action =`action data${i++}`;
      console.log('resbtn', action);
    }, false);
    return $btn.removeEventListener('click', () => {});
  });
  if (error) {return <div>{error.msg}</div>;
  }
  return (
    <div>
      {loading && <h3>loading</h3>}
      <h3 className="mt-10 mb-10">finished</h3>
      <button onClick={startFetch}> 获取数据 </button>
      <h4 className="mt-10"><span className="btn">Saga 模拟测试 </span></h4>
    </div>
  );
}

我用了 addEventListener 和 removeEventListener 来尝试 useEffect 的挂载和清除功能,细心的你,发现这段代码有几个错误呢?
自我观察,自认为是有如下几个的:

  1. 清除函数使用方式错误,应该返回的是一个函数,like:() => { $btn.removeEventListener(‘click’, () => {}); },而我这里是一个语句;
  2. removeEventListener 使用语法错误;
  3. useEffect 未使用第二个参数,导致 addEventListener 挂载会多次执行(可以优化)

第 1 个和第 3 个错误,是可以原谅的,当时自己对 hooks 还不熟悉。但第二个错误,是不可原谅的,这是需要检讨的。本文后面不会对 useEffect 做深入讲解,官方文档已经足够清楚,后面围绕 removeEventListener 来剖析。

认清 removeEventListener

第二个错误,拆开看,又可分为两个方面:removeEventListener 使用方式多余与语法错误。

  • 使用方式多余:虽然 $btn 添加了 click 监听事件回调,但由于这个节点属于当前 Test 组件,所以组件销毁时,其相关节点的监听事件也会一并销毁,这个在自己刚接触前端时就做过这一方面的解析,所以这里的 removeEventListener 使用是多余的。但如果换成一个组件外的节点,比如我后面替换的.info-con 节点,这是一直存在于 Layout 组件中的,使用 removeEventListener 是必要的。
  • 语法错误:指使用 removeEventListener,前面两个参数是必传的,事件类型(type:click),事件回调函数(listener: callback),由于使用 addEventListener 是为事件添加的一个队列(即同一个事件,可添加多个监听回调),所以事件回调函数(listener)是必传,且其引用与添加的事件监听回调函数指向相同。详细描述见官方文档

关于语法错误,官方文档中有这样一段描述:

由于我在添加监听时,使用的是箭头函数,所以删除时无法找到相同引用的监听事件,所以第一件事就是改变监听函数的写法。完善后,写法是下面这样的:

let i = 0;
function Test(props) {const { loading, error, startFetch} = props;
  useEffect(() => {const $btn = document.querySelector('.info-con');
    const eventAction = () => {const action =`action data${i++}`;
      console.log('resbtn:', action);
    };
    $btn.addEventListener('click', eventAction);
    return () => {$btn.removeEventListener('click', eventAction);
    };
  }, []);
  if (error) {return <div>{error.msg}</div>;
  }
  return (
    <div>
      {loading && <h3>loading</h3>}
      <h3 className="mt-10 mb-10">finished</h3>
      <button onClick={startFetch}> 获取数据 </button>
      <h4 className="mt-10"><span className="btn">Saga 模拟测试 </span></h4>
    </div>
  );
}

到此,看似已经结束了。但既然已经打开了,就深入的学习一下这个 api 吧,常用的前两个参数,我们都很熟悉,第三个参数写成布尔值也偶尔会有,但是否已足够了解呢?

第三个参数

addEventListener 和 removeEventListener 传参是一样的,第三个可选参数都是一个对象或者一个布尔值:

target.addEventListener(type, listener[, options]);
target.addEventListener(type, listener[, useCapture]);
target.removeEventListener(type, listener[, options]);
target.removeEventListener(type, listener[, useCapture]);

当参数为布尔值时,意指 useCapture,是否在捕获阶段触发监听函数。而为对象时,可用选项如下:

clipboard.png
之所以第三个参数有两种形态,是在旧版本中只存在一个布尔值,即 useCapture 属性;但随着时间推移以及发展的需要,需要支持设置更多的特性设置,所以有了 options 选项这个对象传参,又为了兼容以前的老程序,所以对两者进行了兼容。once 和 passive 属性非常有趣,但我还没想到合适的使用它们的场景。查看官方文档,发现里面确实有好多以前没有关注到的东西,值得细细品味。
其实在 js 很多 api 中,我们都只用了一些常用的用法,而忽略了一些存在且也很适用的不常用传参,比如下面这些:

  • setTimout(callback, time, …params): 这个有多有用呢,有一道网红面试题,关于闭包的,就可以利用这个参数来解决,在详解网红前端经典面试题:setTimeout 与循环闭包提到过
    function test(){for (var i=0; i<5; i) {setTimeout( function timer() {console.log(new Date(),i);
            }, 1000);
        }
        console.log('end',new Date(),i);
    }
  • bind(thisArg, …params): 为函数预先传参,在 react 中用的比较多;
  • replace(reg, str | function): replace 第二参数也可以为一个函数,用于自定义替换
  • split(reg, length): split 其实也是有第二个参数的,用以指定返回数组的长度不超过指定大小;
  • 暂时想不起来了 …

思考

这一次关于对 removeEventListener 的使用错误,让我不得不意识到,对于自己下一步技术提升的着重点还是该多关注基础,不要学的太宽泛,而成为泛而不精的人,最后一无是处。最近这一年确实太痴迷(yilai)于框架(React),基础关注的太少。框架与基础,框架层出不穷,但基础只是持续在演进。

正文完
 0