release: v1.0.14 By: AICoder

This commit is contained in:
AI Engineer 2026-06-05 19:16:52 +08:00
parent b04be8d437
commit a777f315b4
8 changed files with 31 additions and 9 deletions

View File

@ -1,5 +1,11 @@
# CHANGELOG # CHANGELOG
## v1.0.14 (2026-06-05)
### 修复
- **微任务扫描死循环**: 引入 `_stScanned` 标记对新挂载节点进行幂等拦截,防止在复杂嵌套(如 `$if` / `$each`)及大数据量场景下,新老节点重组重排循环触发 MutationObserver 造成浏览器卡死的微任务无限扫描死循环。
- **状态更新防御**: 在 `_updateBinding` 逻辑中加入同值校验,避免当目标值与原值相等时重复赋值,杜绝不必要的 Observer 触发与重绘。
## v1.0.13 (2026-05-26) ## v1.0.13 (2026-05-26)
### 新特性 ### 新特性

10
dist/state.js vendored
View File

@ -244,7 +244,9 @@
if (typeof result === "object" && result != null && !Array.isArray(result) && o[lk] == null) o[lk] = {}; if (typeof result === "object" && result != null && !Array.isArray(result) && o[lk] == null) o[lk] = {};
const lo = o[lk]; const lo = o[lk];
if (typeof lo === "object" && lo != null && lo.__watch) Object.assign(lo, result); if (typeof lo === "object" && lo != null && lo.__watch) Object.assign(lo, result);
else o[lk] = result; else {
if (o[lk] !== result) o[lk] = result;
}
} else if (typeof result === "object" && result != null && !Array.isArray(result)) { } else if (typeof result === "object" && result != null && !Array.isArray(result)) {
Object.assign(o, result); Object.assign(o, result);
} }
@ -475,6 +477,7 @@
return; return;
} }
if (node.nodeType !== 1) return; if (node.nodeType !== 1) return;
node._stScanned = true;
if (!node._stTranslated) { if (!node._stTranslated) {
Array.from(node.attributes).forEach((attr) => { Array.from(node.attributes).forEach((attr) => {
if (!attr.name.startsWith("$") && !attr.name.startsWith("st-") && !attr.name.startsWith(".")) { if (!attr.name.startsWith("$") && !attr.name.startsWith("st-") && !attr.name.startsWith(".")) {
@ -693,7 +696,10 @@
new MutationObserver((mutations) => { new MutationObserver((mutations) => {
mutations.forEach((mutation) => { mutations.forEach((mutation) => {
mutation.addedNodes.forEach((newNode) => { mutation.addedNodes.forEach((newNode) => {
if (newNode.isConnected) _scanTree(newNode); if (newNode.isConnected && newNode.nodeType === 1 && !newNode._stScanned) {
newNode._stScanned = true;
_scanTree(newNode);
}
}); });
mutation.removedNodes.forEach((oldNode) => _unbindTree(oldNode)); mutation.removedNodes.forEach((oldNode) => _unbindTree(oldNode));
}); });

2
dist/state.min.js vendored

File diff suppressed because one or more lines are too long

2
dist/state.min.mjs vendored

File diff suppressed because one or more lines are too long

10
dist/state.mjs vendored
View File

@ -240,7 +240,9 @@ function _updateBinding(binding) {
if (typeof result === "object" && result != null && !Array.isArray(result) && o[lk] == null) o[lk] = {}; if (typeof result === "object" && result != null && !Array.isArray(result) && o[lk] == null) o[lk] = {};
const lo = o[lk]; const lo = o[lk];
if (typeof lo === "object" && lo != null && lo.__watch) Object.assign(lo, result); if (typeof lo === "object" && lo != null && lo.__watch) Object.assign(lo, result);
else o[lk] = result; else {
if (o[lk] !== result) o[lk] = result;
}
} else if (typeof result === "object" && result != null && !Array.isArray(result)) { } else if (typeof result === "object" && result != null && !Array.isArray(result)) {
Object.assign(o, result); Object.assign(o, result);
} }
@ -471,6 +473,7 @@ const _scanTree = (node, scanObj = {}) => {
return; return;
} }
if (node.nodeType !== 1) return; if (node.nodeType !== 1) return;
node._stScanned = true;
if (!node._stTranslated) { if (!node._stTranslated) {
Array.from(node.attributes).forEach((attr) => { Array.from(node.attributes).forEach((attr) => {
if (!attr.name.startsWith("$") && !attr.name.startsWith("st-") && !attr.name.startsWith(".")) { if (!attr.name.startsWith("$") && !attr.name.startsWith("st-") && !attr.name.startsWith(".")) {
@ -689,7 +692,10 @@ if (typeof document !== "undefined") {
new MutationObserver((mutations) => { new MutationObserver((mutations) => {
mutations.forEach((mutation) => { mutations.forEach((mutation) => {
mutation.addedNodes.forEach((newNode) => { mutation.addedNodes.forEach((newNode) => {
if (newNode.isConnected) _scanTree(newNode); if (newNode.isConnected && newNode.nodeType === 1 && !newNode._stScanned) {
newNode._stScanned = true;
_scanTree(newNode);
}
}); });
mutation.removedNodes.forEach((oldNode) => _unbindTree(oldNode)); mutation.removedNodes.forEach((oldNode) => _unbindTree(oldNode));
}); });

View File

@ -1,6 +1,6 @@
{ {
"name": "@apigo.cc/state", "name": "@apigo.cc/state",
"version": "1.0.13", "version": "1.0.14",
"type": "module", "type": "module",
"main": "dist/state.js", "main": "dist/state.js",
"module": "dist/state.js", "module": "dist/state.js",

View File

@ -76,7 +76,7 @@ export function _updateBinding(binding) {
if (typeof result === 'object' && result != null && !Array.isArray(result) && o[lk] == null) o[lk] = {}; if (typeof result === 'object' && result != null && !Array.isArray(result) && o[lk] == null) o[lk] = {};
const lo = o[lk]; const lo = o[lk];
if (typeof lo === 'object' && lo != null && lo.__watch) Object.assign(lo, result); if (typeof lo === 'object' && lo != null && lo.__watch) Object.assign(lo, result);
else o[lk] = result; else { if (o[lk] !== result) o[lk] = result; }
} else if (typeof result === 'object' && result != null && !Array.isArray(result)) { } else if (typeof result === 'object' && result != null && !Array.isArray(result)) {
Object.assign(o, result); Object.assign(o, result);
} }
@ -308,6 +308,7 @@ export const _scanTree = (node, scanObj = {}) => {
node._stTranslated = true; return; node._stTranslated = true; return;
} }
if (node.nodeType !== 1) return; if (node.nodeType !== 1) return;
node._stScanned = true;
if (!node._stTranslated) { if (!node._stTranslated) {
Array.from(node.attributes).forEach(attr => { Array.from(node.attributes).forEach(attr => {

View File

@ -28,7 +28,10 @@ if (typeof document !== 'undefined') {
new MutationObserver(mutations => { new MutationObserver(mutations => {
mutations.forEach(mutation => { mutations.forEach(mutation => {
mutation.addedNodes.forEach(newNode => { mutation.addedNodes.forEach(newNode => {
if (newNode.isConnected) _scanTree(newNode); if (newNode.isConnected && newNode.nodeType === 1 && !newNode._stScanned) {
newNode._stScanned = true;
_scanTree(newNode);
}
}); });
mutation.removedNodes.forEach(oldNode => _unbindTree(oldNode)); mutation.removedNodes.forEach(oldNode => _unbindTree(oldNode));
}); });