Skip to main content
Tillbaka till bloggen
JavaScriptWebThird-Party ScriptsFrontendPatterns

Fånga ett tredjepartsvärde i samma sekund det landar på window

Ett skript du inte kontrollerar skriver sitt resultat till en global variabel på window vid en tidpunkt du inte kan förutsäga. Det mesta av koden tävlar mot det med en timeout eller belastar main thread med onödig polling. Här är accessor-property-tricket som förvandlar skrivningen till en händelse — plus gettern du aldrig får glömma, och hur du helt kan ersätta värdet.

Publicerad 27 juli 20269 min läsning

Förr eller senare integrerar du ett skript du inte äger — en attribution-SDK, ett consent-verktyg, ett A/B-ramverk, en deep-link-resolver som släpps in via en tag manager — och det kommunicerar tillbaka till din sida genom att tilldela ett värde till en global variabel på window. Något i stil med window.SMART_LINK_RESULT = { url: … }. Din uppgift är att läsa det värdet och agera på det så snart det existerar.

Haken är att du inte har någon kontroll över när tilldelningen sker. Den kan ske innan din kod körs, några hundra millisekunder senare, eller — om nätverket är långsamt eller skriptet aldrig laddas — inte alls. Den här artikeln går igenom de två metoder alla griper efter först, varför båda är kompromisser, och en tredje som är strikt bättre: att fånga upp själva tilldelningen med en accessor-property.

Scenariot: ett värde som anländer när det själv vill

Konkret: föreställ dig en lättviktig omdirigeringssida. En tag manager injicerar ett skript som beräknar den korrekta, fullt attribuerade destinations-URL:en och skriver den, när det är klart, till window.SMART_LINK_RESULT. Du vill omdirigera till den URL:en i samma stund den blir tillgänglig, och falla tillbaka till en enkel standard-URL om den aldrig dyker upp.

Värdet är ett litet objekt, och vi äger inte skriptet som producerar det — vi känner bara till formen vi förväntar oss:

global.d.ts
// A third-party script assigns this global once it has computed a result.
// We don't own the script, only the type we expect on the window.
interface SmartLinkResult {
  url?: string;
  [key: string]: unknown;
}

declare global {
  interface Window {
    SMART_LINK_RESULT?: SmartLinkResult;
  }
}

export {};

Försök 1: en fast timeout

Den första instinkten är att vänta en "säker" mängd tid på att skriptet ska bli klart, sedan läsa den globala variabeln en gång och köra:

attempt-1-timeout.ts
const FALLBACK_URL = "https://example.com/download";

// Wait a "safe" amount of time, then read the global once and redirect.
setTimeout(() => {
  const url = window.SMART_LINK_RESULT?.url;
  window.location.href = url?.trim() ? url : FALLBACK_URL;
}, 3000);

Det fungerar tillräckligt ofta för att skeppas, och kostar dig sedan i tysthet. En enda läsning bakom en fast fördröjning är en kapplöpning du förlorar åt båda hållen:

  • För tidigt: om skriptet inte har skrivit värdet vid 3-sekundersmarkeringen läser du undefined och omdirigerar till fallback — och kastar bort den korrekta URL:en som skulle ha anlänt en stund senare.
  • För sent: om skriptet var klart på 200 ms tvingar du fortfarande användaren att stirra på en tom omdirigeringssida i hela 3 sekunder innan något händer.
  • Den "rätta" fördröjningen går inte att veta, eftersom den beror på nätverket och tredjeparten — så du slutar med att välja ett tal som samtidigt är för långt för snabba klienter och för kort för långsamma.

Försök 2: polling

Den uppenbara lösningen på "för sent"-halvan är att sluta vänta på en fast tidpunkt och istället kontrollera upprepade gånger tills värdet dyker upp, med en deadline för att ge upp:

attempt-2-polling.ts
const FALLBACK_URL = "https://example.com/download";
const DEADLINE = 3000;
let elapsed = 0;

const timer = setInterval(() => {
  const url = window.SMART_LINK_RESULT?.url;

  if (url?.trim()) {
    clearInterval(timer);
    window.location.href = url;           // finally showed up
  } else if ((elapsed += 50) >= DEADLINE) {
    clearInterval(timer);
    window.location.href = FALLBACK_URL;  // gave up waiting
  }
}, 50);

Det här är bättre — det reagerar inom ett intervall efter att värdet anländer istället för att vänta hela fördröjningen. Men det är fortfarande en kompromiss: du kör en timer som inte gör något användbart på de flesta av sina tick, du har bytt latens mot ett justerbart intervall (för grovt känns laggigt, för fint belastar main thread), och du busy-läser en property i en het loop för något som händer exakt en gång.

Tricket: fånga upp själva skrivningen

Båda metoderna behandlar tilldelningen som något att upptäcka i efterhand — genom att gissa när den skedde, eller genom att fråga om och om igen om den redan har skett. Men en tilldelning till en property är något JavaScript gärna berättar för dig, om du definierar propertyn som en accessor innan skriptet körs. Ersätt den vanliga dataplatsen på window.SMART_LINK_RESULT med ett getter/setter-par:

interceptor.ts
// Store the current value in a closure so we control what the property holds.
let value: SmartLinkResult | undefined = window.SMART_LINK_RESULT;

Object.defineProperty(window, "SMART_LINK_RESULT", {
  configurable: true,
  get() {
    return value;
  },
  set(next: SmartLinkResult | undefined) {
    value = next; // keep it readable for whoever assigned it
    if (next?.url?.trim()) {
      window.location.href = next.url; // fires the instant the script writes
    }
  },
});

Nu, i samma stund tredjepartsskriptet exekverar window.SMART_LINK_RESULT = …, skriver det inte till en vanlig plats — det anropar din set-funktion, synkront, med värdet som argument. Ingen fördröjning, ingen polling, inga gissningar. Du reagerar i exakt den stund värdet existerar, inte en tick tidigare eller senare. Timeouten slutar vara din primära mekanism och blir det den borde ha varit hela tiden: en fallback för fallet där värdet aldrig kommer.

En fast timeout gissar när värdet anländer. Polling frågar upprepade gånger om det redan har gjort det. En accessor-property blir helt enkelt informerad — tilldelningen blir en synkron callback, med noll latens och noll bortkastat arbete.

Varför gettern inte är valfri

Det är lockande att bara definiera en set — trots allt är det ju hela poängen att reagera på skrivningen. Gör det inte. En accessor-property med en setter men utan getter returnerar undefined på varje läsning. Tredjepartsskriptet (och allt annat på sidan) läser ofta tillbaka sin egen globala variabel — för att kontrollera den, uppdatera ett fält på den, skicka den till en annan modul. En set-only accessor bryter tyst allt detta:

set-only.ts
// A set-only accessor silently swallows the value on read.
Object.defineProperty(window, "SMART_LINK_RESULT", {
  configurable: true,
  set(next: SmartLinkResult | undefined) {
    if (next?.url) window.location.href = next.url;
  },
});

// The third-party script does its normal thing:
window.SMART_LINK_RESULT = { url: "/go" };

// ...but now anyone reading it back — including the script itself — sees nothing:
console.log(window.SMART_LINK_RESULT); // undefined

Regeln är enkel: om du fångar upp skrivningar måste du också servera läsningar. Behåll det senaste värdet i en closure-variabel, returnera det från get, och uppdatera det i set. För alla andra beter sig propertyn exakt som den vanliga dataproperty den ersatte — du har bara lagt till en sidoeffekt vid tilldelning.

Stäng kapplöpningen från andra hållet: den tidiga kontrollen

En interceptor fångar bara skrivningar som sker efter att du installerar den. Om tredjepartsskriptet är snabbt — eller inlinades före din kod — kan värdet redan ligga på window när din kod körs. Kontrollera därför först, och installera bara accessorn om det inte redan finns där:

early-check.ts
// The value may already be on the window before our code runs.
// Check first, and only install the interceptor if it isn't there yet.
const existing = window.SMART_LINK_RESULT?.url;

if (existing?.trim()) {
  window.location.href = existing;
} else {
  installInterceptor(); // the Object.defineProperty from above
}

Med den tidiga kontrollen och settern tillsammans har du täckt hela tidslinjen: värdet fanns redan där (läs det nu), eller det anländer senare (settern triggas), eller det anländer aldrig (fallback-timern). Det finns inget fönster där ett verkligt värde kan smita förbi dig.

Att ersätta värdet, inte bara bevaka det

Eftersom propertyns lagring nu lever i din closure är du inte begränsad till att observera värdet — du bestämmer vad det faktiskt innehåller. Det är skillnaden mellan en listener och en interceptor. Du kan normalisera en felformad payload, ta bort ett fält, eller ersätta tredjepartens standardvärde med ditt eget innan någon längre fram i kedjan ens läser det:

substitute.ts
let value: SmartLinkResult | undefined = window.SMART_LINK_RESULT;

Object.defineProperty(window, "SMART_LINK_RESULT", {
  configurable: true,
  get() {
    return value;
  },
  set(next: SmartLinkResult | undefined) {
    // Don't store what they sent verbatim — normalize it, or replace it entirely.
    value = normalize(next) ?? { url: FALLBACK_URL };
  },
});

Vad skriptet än tilldelar flödar genom din set, och vad din get returnerar är det resten av sidan ser. Standardvärdet skriptet tänkte installera behöver aldrig träda i kraft — bara det värde du väljer att behålla.

Städa upp: sätt tillbaka propertyn

En accessor installerad på en global variabel lever längre än komponenten som lade till den, så riv den ner när du är klar — särskilt i single-page-appar, React Strict Mode, eller var som helst samma kod kan köras två gånger. Ta bort accessorn och, om det fanns ett värde, återställ det som en vanlig dataproperty:

teardown.ts
function uninstall() {
  const current = value;          // whatever the accessor last held
  delete window.SMART_LINK_RESULT; // remove the getter/setter pair
  if (current !== undefined) {
    // Put it back as an ordinary data property so nothing downstream breaks.
    window.SMART_LINK_RESULT = current;
  }
}

Att återställa en vanlig property (istället för att lämna en hängande getter/setter, eller radera värdet helt) betyder att allt som läser den globala variabeln efteråt får normalt beteende tillbaka. Att sätta configurable: true när du definierade propertyn är vad som gör den här delete-operationen möjlig från början — utan det är accessorn permanent.

Att sätta ihop allt

Här är hela mönstret som en enda React-hook: tidig kontroll, interceptor med en matchande getter, en fallback-timer, och uppstädning som återställer en vanlig property. En done-spärr garanterar att omdirigeringen triggas exakt en gång oavsett vilken väg som vinner.

useSmartLinkRedirect.ts
import { useEffect } from "react";

const FALLBACK_URL = "https://example.com/download";
const FALLBACK_DELAY = 3000;

export function useSmartLinkRedirect() {
  useEffect(() => {
    let done = false;
    let fallbackTimer: ReturnType<typeof setTimeout> | null = null;
    let value: SmartLinkResult | undefined = window.SMART_LINK_RESULT;

    const go = (url: string) => {
      if (done) return;
      done = true;
      if (fallbackTimer) clearTimeout(fallbackTimer);
      window.location.href = url;
    };

    // 1. Already resolved before we mounted? Go now.
    if (value?.url?.trim()) {
      go(value.url);
      return;
    }

    // 2. Otherwise, catch the assignment the moment it happens.
    Object.defineProperty(window, "SMART_LINK_RESULT", {
      configurable: true,
      get() {
        return value;
      },
      set(next: SmartLinkResult | undefined) {
        value = next;
        if (next?.url?.trim()) go(next.url);
      },
    });

    // 3. Never trust the third party to always deliver.
    fallbackTimer = setTimeout(() => go(FALLBACK_URL), FALLBACK_DELAY);

    // 4. Restore a plain property on unmount.
    return () => {
      if (fallbackTimer) clearTimeout(fallbackTimer);
      const current = value;
      delete window.SMART_LINK_RESULT;
      if (current !== undefined) window.SMART_LINK_RESULT = current;
    };
  }, []);
}

Samma form fungerar utanför React — de rörliga delarna är en closure-variabel, ett Object.defineProperty med get/set, en tidig kontroll, en fallback och en nedmontering. I en vanlig modul skulle du wrappa det i en funktion och anropa nedmonteringen på pagehide eller när din feature avslutas.

Timeout mot polling mot interceptor

AspektFast timeoutPollingAccessor-interceptor
Reagerar i samma stund värdet sättsNej — väntar hela fördröjningenNästan — inom ett intervallJa — synkront
Bortkastad tid när värdet kommer tidigtHela fördröjningenUpp till ett intervallIngen
Belastning på main thread under väntanIngenUpprepade läsningar via en timerIngen
Fångar ett värde satt innan du startarJa (läser en gång i slutet)JaEndast med en explicit tidig kontroll
Kan ersätta eller normalisera värdetNejNejJa
Hanterar att värdet aldrig anländerJa (läser, faller tillbaka)Ja (deadline-grenen)Ja (med en fallback-timer)

Interceptorn vinner varje rad som handlar om att reagera på skrivningen, och ligger jämnt i resten så snart du lägger till den tidiga kontrollen och fallback-timern den ändå behöver. Det enda att komma ihåg är att den måste installeras innan tredjepartsskriptet körs — vilket, för ett värde levererat av en tag manager eller en asynkron SDK, nästan alltid är fallet om du installerar den tidigt i sidans livscykel.

Fallgropar värda att känna till

  • Installationsordningen spelar roll. Interceptorn fångar bara skrivningar som kommer efter den. Definiera accessorn så tidigt som möjligt, och kombinera den alltid med den tidiga kontrollen för värdet som hann före dig.
  • Sätt alltid configurable: true. Utan det kan du inte göra delete på propertyn för att städa upp, och ett andra försök att omdefiniera den kastar ett fel.
  • Skydda för SSR. Det finns inget window på servern. I ett ramverk, kör det här i en client-only-effekt; i en vanlig modul, skydda med "undefined" !== "undefined".
  • En interceptor per property. Om två delar av din app båda omdefinierar samma globala variabel skriver den andra över den första. Centralisera det, eller kontrollera Object.getOwnPropertyDescriptor(window, name) innan du installerar.
  • Vissa globala variabler är redan accessorer. En property definierad av miljön som non-configurable (eller med sin egen getter/setter) kan inte ersättas — kontrollera deskriptorn först och falla tillbaka på polling om du verkligen inte kan omdefiniera den.
  • Håll get och set billiga. De körs på varje läsning och skrivning av den globala variabeln. Gör det tunga arbetet en gång, bakom din done-spärr — inte vid varje åtkomst.

När du bör ta till detta

Det här är ett precisionsverktyg, inte ett standardval. Det motiverar sig när allt följande är sant:

  • Ett värde du behöver skrivs till en global variabel (eller vilken objektproperty som helst) av kod du inte kontrollerar.
  • Tidpunkten är oförutsägbar och du vill reagera utan latens — eller du behöver ersätta värdet innan något annat läser det.
  • Du kan köra din kod innan skrivaren gör det, eller du är villig att kombinera det med en tidig kontroll för fallet när du inte kan.

Slutsatser

  • En tredje part ger dig ofta ett värde genom att tilldela en global variabel vid en tidpunkt du inte kan förutsäga; en fast timeout tävlar mot det och polling belastar main thread.
  • Object.defineProperty(window, name, { get, set }), installerad innan skrivaren körs, förvandlar tilldelningen till en synkron callback — noll latens, noll bortkastat arbete.
  • Skeppa aldrig en set-only accessor: kombinera varje set med en get som returnerar det lagrade värdet, annars bryter du tyst skrivarens egen återläsning.
  • Lägg till en tidig kontroll för ett värde som sattes innan du installerade accessorn, och en fallback för ett värde som aldrig anländer.
  • Eftersom du äger lagringen kan du normalisera eller ersätta värdet — en interceptor, inte bara en listener.
  • Sätt configurable: true och återställ en vanlig dataproperty vid uppstädning så att inget längre fram i kedjan lämnas med en överraskning.

Inget av det här är exotiskt — det är bara Object.defineProperty riktat mot en global variabel istället för dina egna objekt. Men att omformulera "vänta på ett värde" till "bli informerad när värdet skrivs" tar bort en hel klass av timing-buggar, och ger dig värdet tidigt nog för att kunna ändra det. Nästa gång du hittar dig själv i färd med att välja ett magiskt timeout-tal för att vänta ut ett tredjepartsskript, ta till settern istället.