Why `!b` Becomes `_a_b`: uni-app Compiler Bug with Variable `b`
The uni-app mini-program compiler (alpha) incorrectly replaces `!b` with undeclared `_a_b` when a local variable is named `b`, causing a runtime ReferenceError only when specific data triggers the condition; the bug evades build, TypeScript, and ESLint checks, and is fixed by renaming the variable.
TL;DR
The Vue .vue source code cannot run directly in a mini-program; it must be translated into JavaScript by the compiler (the compilation output , which is what actually executes). The bug occurs in this translation step: if a component has a local variable named b, the uni-app mini-program compiler (alpha version) may silently rewrite one occurrence of !b into _a_b, a name that is never declared.
// Source code we write
if (!a || !b || a.x === b.x)
// Compilation output (actual running code)
if (!a || _a_b || a.x === b.x) // ← _a_b never declaredWhen execution reaches this line, a ReferenceError is thrown, the component fails to render, and the screen goes blank. Worse, this line only executes with specific data — in this case, only when the family tree contains a sibling edge without common parents. Changing test data makes the bug disappear, a classic ghost bug.
Fix: rename the variables ( a / b → fromCard / toCard), logic unchanged. Defense: search build output for _a_b; avoid single-letter local variable b (tested a, c, e are safe, only b triggers).
1. Problem and Investigation Path
Symptom: the family tree page has three views — Ancestor, Descendant, and Bidirectional. Ancestor view works; Descendant and Bidirectional views show blank , yet all three APIs return 200 with valid data.
Investigation followed standard order, first two steps found nothing:
Backend: verified responses for all three views — nodes and edges correct — ruled out.
Frontend display code: fed the Descendant API response into the layout algorithm, ran it in Node outside the mini-program; coordinates for 4 nodes were correct — ruled out.
Source and data are fine, leaving only one possibility: the mini-program is not running the source we wrote . It runs the compilation output ( .vue translated to JS, located at dist/build/mp-weixin). Searching there revealed the culprit.
The broken segment is the sibling-line drawing. When two family members have no common parent record, the backend uses a sibling direct edge; the frontend draws a polyline between the two cards. Source code:
// Find card position for member; return null if not found
const cardOf = (id: string) => {
for (const it of items) {
if (it.node.id === id)
return { x: it.x, cy: it.y + CARD_H / 2 }
}
return null
}
for (const e of props.edges) {
if (e.type !== 'sibling') continue
const a = cardOf(e.from) // one sibling's card
const b = cardOf(e.to) // other sibling's card
if (!a || !b || a.x === b.x) // either off-canvas or same column, skip
continue
// …generate polyline from a, b coordinates
}Compilation output ( uni build -p mp-weixin --mode development, no minification, preserves source shape):
for (const e of props.edges) {
if (e.type !== "sibling") continue;
const a = cardOf(e.from);
const b = cardOf(e.to);
if (!a || _a_b || a.x === b.x) // ← !b rewritten to _a_b
continue;
} _a_bis not declared anywhere in the file; !a, a.x === b.x, and the declaration const b = cardOf(...) all remain intact — only the bare reference !b is replaced . Across the entire project output, this is the only corruption.
Because the corruption appears in the non-minified build, the minifier is excluded first (verified with four esbuild compression configs — all clean). Other build plugins were checked one by one; none touch this code. The culprit is locked to @dcloudio/uni-mp-compiler (uni-app's mini-program compiler).
2. Why Ancestor Works but Descendant Blank: A Short-Circuit Coincidence
JavaScript's || has short-circuit evaluation : if the left side already determines the whole condition as true, the right side is never evaluated. Applied to the broken line:
if (!a || _a_b || a.x === b.x)
// ↑ a is null → !a true → whole condition true → _a_b never evaluated, no error
// a not null → evaluate _a_b → crash (undefined variable)The three views differ exactly as follows:
Ancestor view: edge list empty, the sibling-line loop never runs → never hits _a_b → renders normally.
Descendant/Bidirectional views: contain that sibling edge, both ends on canvas → !a false → evaluates _a_b → throws → component render aborts → blank screen.
Mini-program modules run in strict mode by default; reading an undeclared variable throws immediately. The error occurs during rendering, swallowed by the framework's error handler, so the user sees only a blank area with no alert.
3. Minimal Reproduction: Two Minutes to Reproduce
In any uni-app (Vue 3 + Vite + WeChat mini-program) project, create a component with the following content, then reference it in any page template (ensure it participates in the build):
<!-- src/components/CompileRepro/CompileRepro.vue -->
<script setup lang="ts">
import { computed } from 'vue'
const props = defineProps<{ cards: Array<{ id: string, x: number }>, pairs: Array<{ from: string, to: string }> }>()
function find(id: string) {
return props.cards.find(c => c.id === id) ?? null
}
const lines = computed(() => {
const out: number[] = []
for (const p of props.pairs) {
const a = find(p.from)
const b = find(p.to) // ← variable name b is the trigger
if (!a || !b || a.x === b.x) // ← !b will be rewritten
continue
const [l, r] = a.x < b.x ? [a, b] : [b, a]
out.push(r.x - l.x)
}
return out
})
</script>
<template>
<view>
<view v-for="(line, i) in lines" :key="`l${i}`">{{ line }}</view>
<view v-for="card in cards" :key="card.id">{{ card.x }}</view>
</view>
</template>Then run:
npx uni build -p mp-weixin --mode development # no minification, easy to analyze
grep -n "_a_b" dist/build/mp-weixin/components/CompileRepro/CompileRepro-vendor.js
# 20: if (!a || _a_b || a.x === b.x)Compilation output excerpt (the var _a inside find is a TypeScript helper for the ?? pattern, normal and unrelated — Experiment A proves this):
function find(id) {
var _a;
return (_a = props.cards.find(c => c.id === id)) != null ? _a : null;
}
const lines = common_vendor.computed(() => {
const out = [];
for (const p of props.pairs) {
const a = find(p.from);
const b = find(p.to);
if (!a || _a_b || a.x === b.x) // ← corruption
continue;
const [l, r] = a.x < b.x ? [a, b] : [b, a];
out.push(r.x - l.x);
}
return out;
});4. Trigger Conditions: 7 Controlled Experiments
To pinpoint exactly what triggers the bug, each experiment changed only one factor, did a full clean rebuild, and inspected the output. Seven experiments total:
Experiment Matrix
#0 Baseline: variables a + b, TS helper present, 2 v-for → ❌ !b → _a_b (real component & minimal repro both confirmed)
#A: variables a + b, no TS helper, 2 v-for → ❌ Corrupted — TS helper not required
#B: variables left + right, TS helper present, 2 v-for → ✅ Clean — renaming fixes it
#C: variables first + b, TS helper present, 2 v-for → ❌ Corrupted — a need not exist, b is the trigger
#E: variables first + b, TS helper present, 1 v-for → ❌ Corrupted — unrelated to template render key naming
#F: variables first + c, TS helper present, 1 v-for → ✅ Clean — problematic name precisely locked to b
#G: variables fromCard + toCard (real component, keep !x || !y style), TS helper present, 2 v-for → ✅ Clean — syntax irrelevant, purely naming
Additional evidence: variables named a, e, l, r, x, y in the real component are all intact; sort((a, b) => ...) comparator parameters in multiple files also test safe (likely because they are function parameters, not const declarations).
4.1 Source vs Output per Experiment
Key diff lines for each experiment (loop body same as minimal repro; output from non-minified build, trailing semicolons added by compiler):
#0 Baseline: a + b (TS ?? helper present, double v-for)
// ── Source ──
const a = find(p.from)
const b = find(p.to)
if (!a || !b || a.x === b.x)
// ── Output ──
const a = find(p.from);
const b = find(p.to);
if (!a || _a_b || a.x === b.x) // ← !b rewritten#A Remove TS ?? helper ( find uses || null , eliminates var _a )
// ── Source (find return) ──
return props.cards.find(c => c.id === id) || null
// ── Output ──
function find(id) {
return props.cards.find(c => c.id === id) || null; // ✓ clean, no var _a
}
// Main loop output:
if (!a || _a_b || a.x === b.x) // ← still corrupted, helper not trigger#B Rename: left + right (keep ?? null )
// ── Source ──
const left = find(p.from)
const right = find(p.to)
if (!left || !right || left.x === right.x)
// ── Output ──
if (!left || !right || left.x === right.x) // ✓ preserved#C Only rename a to first , keep b
// ── Source ──
const first = find(p.from)
const b = find(p.to)
if (!first || !b || first.x === b.x)
// ── Output ──
if (!first || _a_b || first.x === b.x) // ← !b still rewritten, trigger is b#E Remove second v-for (script identical to #C)
<!-- Template only one list, render key only a -->
<view>
<view v-for="(line, i) in lines" :key="`l${i}`">{{ line }}</view>
</view> // ── Output ──
// WXML only has wx:for="{{a}}", no {{b}}
if (!first || _a_b || first.x === b.x) // ← still corrupted, unrelated to render key collision#F Rename b to c
// ── Source ──
const first = find(p.from)
const c = find(p.to)
if (!first || !c || first.x === c.x)
// ── Output ──
if (!first || !c || first.x === c.x) // ✓ preserved, corruption precise to name b#G Real component fix: fromCard + toCard (keep !x || !y )
// ── Source ──
const fromCard = cardOf(e.from)
const toCard = cardOf(e.to)
if (!fromCard || !toCard || fromCard.x === toCard.x)
// ── Output (dev build) ──
if (!fromCard || !toCard || fromCard.x === toCard.x) // ✓ preserved
// ── Output (production minified) ──
if(!o||!t||o.x===t.x)continue // ✓ normal minificationAll seven together show: no matter how you rewrite syntax, template, or TS, as long as a local variable named b exists in <script setup>, one of its bare references gets corrupted; rename it and the problem vanishes instantly.
4.2 Confirmed vs. Unconfirmed
Confirmed (see matrix above):
Corruption happens in compiler phase (before minification; dev build also affected).
Trigger is a local variable named b in <script setup> — one bare reference rewritten; same expression's b.x and declaration stay intact, making the rewrite partial and inconsistent, hence dangerous.
Unrelated to TS helper variables, template render key names, or minifier.
Unconfirmed :
The exact compiler-internal logic that generates the name _a_b (not found in compiler output strings, trial version lacks source maps, deep dive ROI low). Fortunately the empirical boundary is clear enough to work around.
5. Why This Bug Escaped Every Defense
Build passes silently — _a_b is a valid identifier, syntactically legal.
TypeScript & ESLint pass — they check source, which is correct; the error is in the translation output.
Data-dependent — only triggers when that special sibling edge exists; switch account, family, or backend adds a record, bug "self-heals".
Blank screen, no hint — error swallowed by framework internals; user sees empty area, no popup, no error log.
Mini-program only — H5 web uses completely different compilation path; browser debugging never reproduces.
Dev and prod identical — not a classic "minification-only" issue; it's in the compiler core.
6. Fix and Defense
6.1 Fix (Used in This Project)
- const a = cardOf(e.from)
- const b = cardOf(e.to)
- if (!a || !b || a.x === b.x)
+ const fromCard = cardOf(e.from)
+ const toCard = cardOf(e.to)
+ if (!fromCard || !toCard || fromCard.x === toCard.x)
continue
- const [l, r] = a.x < b.x ? [a, b] : [b, a]
+ const [l, r] = fromCard.x < toCard.x ? [fromCard, toCard] : [toCard, fromCard]Zero logic change, only renaming. Production build verified: zero _a_b matches in entire output; minified guard correctly becomes if(!o||!t||o.x===t.x)continue.
6.2 Defense Checklist (Copy-Paste Ready)
# 1. Exact scan: search for corruption identifier in build output
grep -rn "_a_b" dist/build/mp-weixin --include="*.js"
# 2. Broad heuristic: search all underscore-joined suspected rename leftovers (exclude Vue internal __v_ flags)
grep -rnE "\b_[a-z]+_[a-z0-9]+\b" dist/build/mp-weixin --include="*.js" | grep -v "__v_"Add these to CI (run after build, fail if matches). Deeper remedies:
Upgrade off alpha channel : the faulty version 3.0.0-5020420260813003 is a nightly alpha; stable release may not have this bug (re-run scans after upgrade to confirm).
File upstream issue : attach minimal repro from Section 3. Search of dcloudio/uni-app issues shows no identical report; only a similar "variable rewritten causing not defined" issue (#4944).
Team coding standard : forbid single-letter local variables in .vue components, especially b. This avoids this bug and makes output inspection far easier when issues arise.
7. Version Information
vue 3.5.35
vite 5.2.8
esbuild 0.20.2 (verified innocent)
@dcloudio/uni-app 3.0.0-5020420260813003 (alpha)
@dcloudio/uni-mp-weixin 3.0.0-5020420260813003
@dcloudio/vite-plugin-uni 3.0.0-5020420260813003
@dcloudio/uni-cli-shared 3.0.0-5020420260813003
@uni-helper/vite-plugin-uni-components 0.2.3 (verified innocent)Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
