2
\$\begingroup\$

I use this function in browser automation. It pings the document in the browser to check if an element we are looking for is available (useful when there is JavaScript generated async content). If it's not available, it will retry after a given interval or times out eventually.

Example usage:

function clickContinue() {
  return $(":contains('Continue')");
}

elementFinder(clickContinue).then(el => el.click());

Function:

// accepts both document.querySelector and jQuery() selector
function elementFinder(selectorFn) {
  const interval = 500;
  const timeout = 8000;
  const maxTries = timeout / interval;
  function checkerFn(resolve, reject, tryCount=0) {
    if(tryCount > maxTries) {
      reject(`Timeout ${timeout}ms reached when searching for element`);
      return;
    }
    setTimeout(() => {
      const el = selectorFn();
      // null check is for document.querySelector, zero check is for jQuery()
      if(el !== null && el.length !== 0) {
        resolve(el);
        return;
      }
      tryCount += 1;
      checkerFn(resolve, reject, tryCount)
    }, tryCount === 0 ? 0 : interval);
  }
  return new Promise(function(resolve, reject) {
    checkerFn(resolve, reject)
  });
}

It seems weird to me that I define a function within a function, wonder if this can be maybe done with one function or if there are some other issues.

\$\endgroup\$
3
\$\begingroup\$

This function will certainly do the job. Couple of nitpicks:

  • You might consider replacing el !== null && el.length !== 0 with el && "nodeType" in el

  • Every time the elementFinder is called, you are defining a new function in memory called checkerFn, instead you can reuse it, but probably you'll have to pass interval and maxTries as a parameter to it:

const finder = function(){
   function elementFinder (..){
      ...
   }
   function checkerFn (..) {
      ...
   }
   return elementFinder;
}();
  • Consider switching to requestAnimationFrame (rAF); for tasks/animations that does not have to be executed with sub-millisecond precision can be handled by rAF. The good thing with rAF is that if the tab is out of focus, it won't run.

  • If you are simultaneously searching for several elements at the same time, multiple setTimeout/rAF calls will have to be executed. For a couple of elements/tasks this is not an overhead, but if you have many, it makes sense to execute 1 setTimeout/rAF per 'tick' and process the tasks in there. For this you will need to modify your code to store tasks in some sort of ledger.

  • I see that you are using Promises, which is a great idea, perhaps to return a thenable from your function which you can execute other tasks once the element is available. The problem with Promises is that you cannot 'break' out of it, you'll have to add some sort of breaking mechanism to your code. Also if you want to seamlessly support evergreen and older browsers, you'll need a Promise polyfill.

  • Although Promises bring a lot of convenience, they certainly bring an overhead (see perf differences between Native Promises and Bluebird's version). If the sole purpose is to return a thenable for constructing chains, this can certainly be implemented using only setTimeout/rAF. This alleviates the need for polyfill for olderbrowsers.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.