@lark.js/mvc 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +554 -786
- package/dist/{chunk-5OEHRF3U.js → chunk-ANWA22AX.js} +100 -89
- package/dist/index.cjs +942 -691
- package/dist/index.d.cts +1475 -726
- package/dist/index.d.ts +1475 -726
- package/dist/index.js +939 -686
- package/dist/vite.cjs +100 -89
- package/dist/vite.d.cts +1 -1
- package/dist/vite.d.ts +1 -1
- package/dist/vite.js +1 -1
- package/dist/webpack.cjs +100 -89
- package/dist/webpack.js +1 -1
- package/package.json +9 -6
- package/lark.d.ts +0 -1176
package/README.md
CHANGED
|
@@ -1,73 +1,264 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
name: lark-mvc
|
|
3
|
+
description: >
|
|
4
|
+
Lark MVC Framework (@lark.js/mvc) skill for building TypeScript frontend applications.
|
|
5
|
+
Use this skill whenever the user is working with the Lark framework, including:
|
|
6
|
+
creating Views, defining Stores, configuring Routes, writing templates, using Updater,
|
|
7
|
+
integrating with Vite or Webpack, or any question about Lark's API, architecture,
|
|
8
|
+
or conventions. Also trigger when the user mentions hash-based routing, Proxy-based
|
|
9
|
+
reactive state, VDOM diff rendering, event delegation, or the v-lark attribute pattern,
|
|
10
|
+
even if they do not explicitly name "Lark".
|
|
11
|
+
---
|
|
2
12
|
|
|
3
|
-
Lark
|
|
13
|
+
# Lark MVC Framework
|
|
4
14
|
|
|
5
|
-
|
|
15
|
+
Lark is a TypeScript MVC framework for building single-page applications with hash-based routing, Proxy-based reactive state management, and real DOM diff rendering.
|
|
6
16
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
## Architecture Overview
|
|
18
|
+
|
|
19
|
+
Lark follows a strict MVC separation:
|
|
20
|
+
|
|
21
|
+
- **Model**: `defineStore` for reactive state (recommended), `cell`/`observeCell` for standalone reactive cells, `State` for global singleton data (default when no store is specified), `Service` for API request management
|
|
22
|
+
- **View**: `View.extend()` creates view subclasses with template rendering, event delegation, and lifecycle hooks
|
|
23
|
+
- **Controller**: `Router` for hash-based navigation, `Updater` for per-view data binding and VDOM diff, `Frame` for view lifecycle management
|
|
24
|
+
|
|
25
|
+
Data flows through three pipelines (use Store when possible, State is the default when no store is specified):
|
|
26
|
+
|
|
27
|
+
1. **Updater Pipeline** (view-local): `updater.set(data)` -> `updater.digest()` -> template function -> HTML string -> VDOM diff -> DOM patch -> `endUpdate()`
|
|
28
|
+
2. **State Pipeline** (default cross-view): `State.set(data)` -> `State.digest()` -> `changed` event -> views read `State.get()` in `assign()` -> `updater.digest()` -> VDOM diff -> DOM patch. State uses key reference counting: when no view observes a key, the data is automatically deleted on view destroy via `State.clean()`
|
|
29
|
+
3. **Store Pipeline** (cross-view, recommended): `store.key = value` -> Proxy set trap -> `trigger()` -> `GlobalDeps` lookup -> Queue microtask batch -> `store.observe` callback -> `updater.digest()` / `updater.set()` -> VDOM diff -> DOM patch
|
|
30
|
+
|
|
31
|
+
### Boot Sequence (Critical Order)
|
|
32
|
+
|
|
33
|
+
`Framework.boot()` executes in a specific order that must not be changed:
|
|
34
|
+
|
|
35
|
+
1. Merge config -> 2. Set Router config -> 3. Set EventDelegator frame getter -> 4. Bind Router/State events -> 5. **Create rootFrame** -> 6. **Router.\_bind()** -> 7. Mount default view
|
|
36
|
+
|
|
37
|
+
The rootFrame is created BEFORE `Router._bind()` so that the root element has a proper ID. If the order were reversed, rootId would default to `"root"` and views could render into `document.body` unexpectedly.
|
|
24
38
|
|
|
25
|
-
|
|
39
|
+
If a view was already mounted by the router (e.g. after a page reload with `#!/counter`), the default view is NOT mounted to avoid URL/display mismatch (`if (defaultView && !rootFrame.view)`).
|
|
26
40
|
|
|
27
|
-
|
|
41
|
+
### Window Globals
|
|
42
|
+
|
|
43
|
+
After boot, the framework sets these globals for debugging and extension:
|
|
44
|
+
|
|
45
|
+
| Global | Value | Purpose |
|
|
46
|
+
| ------------------------- | ---------------- | --------------------------------- |
|
|
47
|
+
| `window.__lark_Framework` | Framework object | Direct framework access |
|
|
48
|
+
| `window.__lark_State` | State object | Direct state access |
|
|
49
|
+
| `window.__lark_Router` | Router object | Direct router access |
|
|
50
|
+
| `window.__lark_Frame` | Frame class | Direct Frame class access |
|
|
51
|
+
| `window.__lark_View` | View class | Direct View class access |
|
|
52
|
+
| `window.__lark_Debug` | boolean | Enable safeguard Proxy debug mode |
|
|
53
|
+
|
|
54
|
+
Setting `window.__lark_Debug = true` before boot enables:
|
|
55
|
+
|
|
56
|
+
- Safeguard Proxy wrapping on State/Updater data reads (warns on cross-page reads, delays direct mutation warnings by 500ms)
|
|
57
|
+
- Safeguard Proxy on Router `diff()` results and Location params
|
|
58
|
+
|
|
59
|
+
## Project Structure
|
|
28
60
|
|
|
29
61
|
```
|
|
30
|
-
|
|
62
|
+
project/
|
|
63
|
+
index.html # Entry HTML with <script type="module" src="/src/boot.ts">
|
|
64
|
+
vite.config.ts # Vite config with larkMvcPlugin()
|
|
65
|
+
webpack.config.mjs # Webpack config with larkMvcLoader (alternative bundler)
|
|
66
|
+
src/
|
|
67
|
+
boot.ts # Bootstrap: registerViewClass + Framework.boot(config)
|
|
68
|
+
view.ts # Project-level View base class (View.extend)
|
|
69
|
+
styles.css # Global styles
|
|
70
|
+
store/
|
|
71
|
+
count.ts # defineStore declarations
|
|
72
|
+
views/
|
|
73
|
+
home.ts # View.extend({ template, init, render, event methods })
|
|
74
|
+
home.html # Template file (compiled by larkMvcPlugin or larkMvcLoader)
|
|
75
|
+
about.ts
|
|
76
|
+
about.html
|
|
77
|
+
components/
|
|
78
|
+
counter-store.ts # Sub-component views
|
|
79
|
+
counter-store.html
|
|
31
80
|
```
|
|
32
81
|
|
|
33
|
-
|
|
82
|
+
**Important**: `boot.ts` must be placed inside `src/` (not project root). The `index.html` entry references it as `/src/boot.ts`.
|
|
83
|
+
|
|
84
|
+
## Step-by-Step Guide
|
|
85
|
+
|
|
86
|
+
### 1. Install and Configure
|
|
34
87
|
|
|
35
|
-
|
|
88
|
+
```bash
|
|
89
|
+
pnpm add @lark.js/mvc
|
|
90
|
+
```
|
|
36
91
|
|
|
37
|
-
|
|
92
|
+
### 2. Configure Vite Integration
|
|
38
93
|
|
|
39
94
|
```typescript
|
|
40
|
-
|
|
95
|
+
// vite.config.ts
|
|
96
|
+
import { defineConfig } from "vite";
|
|
97
|
+
import { resolve } from "path";
|
|
98
|
+
import { larkMvcPlugin } from "@lark.js/mvc/vite";
|
|
41
99
|
|
|
42
100
|
export default defineConfig({
|
|
43
101
|
plugins: [larkMvcPlugin()],
|
|
102
|
+
resolve: {
|
|
103
|
+
alias: {
|
|
104
|
+
"@": resolve(__dirname, "./src"),
|
|
105
|
+
},
|
|
106
|
+
},
|
|
44
107
|
});
|
|
45
108
|
```
|
|
46
109
|
|
|
47
|
-
|
|
110
|
+
The `larkMvcPlugin` automatically compiles `.html` template imports into JS function modules. Zero configuration required -- just add the plugin and import `.html` files in your views.
|
|
111
|
+
|
|
112
|
+
How it works internally:
|
|
113
|
+
|
|
114
|
+
1. **resolveId hook**: Intercepts `.html` import source strings. When a module imports a `.html` file, the plugin resolves the full path and appends the `?lark-template` suffix (internal constant `LARK_TEMPLATE_SUFFIX`). This prevents Vite from treating the file as a static asset.
|
|
115
|
+
2. **load hook**: When Vite requests a module whose ID ends with `?lark-template`, the plugin reads the raw HTML file from disk, auto-extracts global variables via `extractGlobalVars()` AST analysis, and compiles the template through `compileTemplate()`. The compiled output is an ES module exporting a function with signature `(data, selfId, refData) => string`.
|
|
116
|
+
3. **enforce: "pre"**: The plugin is registered as a pre-phase plugin to ensure it runs before other Vite plugins that might also handle `.html` files.
|
|
117
|
+
|
|
118
|
+
For debug mode with line tracking and detailed error messages:
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
plugins: [larkMvcPlugin({ debug: true })];
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### 3. Configure Webpack Integration
|
|
48
125
|
|
|
49
126
|
```javascript
|
|
50
|
-
|
|
51
|
-
import
|
|
127
|
+
// webpack.config.mjs
|
|
128
|
+
import path from "path";
|
|
129
|
+
import { fileURLToPath } from "url";
|
|
130
|
+
import HtmlWebpackPlugin from "html-webpack-plugin";
|
|
131
|
+
import { larkMvcLoader } from "@lark.js/mvc/webpack";
|
|
132
|
+
|
|
133
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
52
134
|
|
|
53
135
|
export default {
|
|
136
|
+
entry: "./boot.ts",
|
|
137
|
+
output: {
|
|
138
|
+
path: path.resolve(__dirname, "dist"),
|
|
139
|
+
filename: "js/[name].[contenthash:8].js",
|
|
140
|
+
clean: true,
|
|
141
|
+
},
|
|
142
|
+
resolve: {
|
|
143
|
+
extensions: [".ts", ".js"],
|
|
144
|
+
alias: {
|
|
145
|
+
"@": path.resolve(__dirname, "src"),
|
|
146
|
+
},
|
|
147
|
+
},
|
|
54
148
|
module: {
|
|
55
149
|
rules: [
|
|
150
|
+
{
|
|
151
|
+
test: /\.ts$/,
|
|
152
|
+
use: "ts-loader",
|
|
153
|
+
exclude: /node_modules/,
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
test: /\.css$/,
|
|
157
|
+
use: ["style-loader", "css-loader", "postcss-loader"],
|
|
158
|
+
},
|
|
159
|
+
// Lark template processing - larkMvcLoader compiles .html to JS functions
|
|
56
160
|
{
|
|
57
161
|
test: /\.html$/,
|
|
58
|
-
loader: larkMvcLoader,
|
|
162
|
+
use: [{ loader: larkMvcLoader }],
|
|
163
|
+
exclude: /index\.html$/, // Exclude the entry HTML page
|
|
59
164
|
},
|
|
60
165
|
],
|
|
61
166
|
},
|
|
167
|
+
plugins: [
|
|
168
|
+
new HtmlWebpackPlugin({
|
|
169
|
+
template: "./index.html",
|
|
170
|
+
inject: "body",
|
|
171
|
+
minify: false,
|
|
172
|
+
}),
|
|
173
|
+
],
|
|
174
|
+
devServer: {
|
|
175
|
+
port: 3001,
|
|
176
|
+
open: true,
|
|
177
|
+
hot: true,
|
|
178
|
+
},
|
|
179
|
+
devtool: "source-map",
|
|
62
180
|
};
|
|
63
181
|
```
|
|
64
182
|
|
|
65
|
-
|
|
183
|
+
The `larkMvcLoader` is a standard Webpack loader that compiles `.html` template files into JS function modules at build time. It works by:
|
|
184
|
+
|
|
185
|
+
1. Receiving the raw HTML source string as input from Webpack.
|
|
186
|
+
2. Auto-extracting global variables via `extractGlobalVars()` AST analysis (the same function used by the Vite plugin).
|
|
187
|
+
3. Compiling the template through `compileTemplate()` to produce an ES module string.
|
|
188
|
+
4. Returning the compiled result to Webpack via `this.callback()`.
|
|
189
|
+
|
|
190
|
+
Key configuration points:
|
|
191
|
+
|
|
192
|
+
- Import `larkMvcLoader` from `@lark.js/mvc/webpack` (not from a file path).
|
|
193
|
+
- Use the loader object directly as `loader: larkMvcLoader` (it is a function, not a string name).
|
|
194
|
+
- Exclude `index.html` from the loader rule -- the entry HTML page should be processed by HtmlWebpackPlugin, not by larkMvcLoader.
|
|
195
|
+
- Use `HtmlWebpackPlugin` to inject scripts into the entry HTML page.
|
|
196
|
+
|
|
197
|
+
For debug mode, pass loader options:
|
|
198
|
+
|
|
199
|
+
```javascript
|
|
200
|
+
{
|
|
201
|
+
test: /\.html$/,
|
|
202
|
+
use: [
|
|
203
|
+
{
|
|
204
|
+
loader: larkMvcLoader,
|
|
205
|
+
options: { debug: true },
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
exclude: /index\.html$/,
|
|
209
|
+
},
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
The `debug` option enables line tracking and detailed compile-time/runtime error messages with source mapping.
|
|
213
|
+
|
|
214
|
+
### 4. Create Entry HTML
|
|
215
|
+
|
|
216
|
+
```html
|
|
217
|
+
<!-- index.html -->
|
|
218
|
+
<!doctype html>
|
|
219
|
+
<html lang="en">
|
|
220
|
+
<head>
|
|
221
|
+
<meta charset="UTF-8" />
|
|
222
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
223
|
+
<title>My Lark App</title>
|
|
224
|
+
</head>
|
|
225
|
+
<body>
|
|
226
|
+
<div id="app"></div>
|
|
227
|
+
<script type="module" src="/src/boot.ts"></script>
|
|
228
|
+
</body>
|
|
229
|
+
</html>
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
The `<div id="app">` corresponds to `rootId: "app"` in the boot config.
|
|
233
|
+
|
|
234
|
+
For Webpack, the entry HTML uses HtmlWebpackPlugin instead of a `<script>` tag. HtmlWebpackPlugin automatically injects the bundled script.
|
|
235
|
+
|
|
236
|
+
### 5. Create a Project Base View
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
// src/view.ts
|
|
240
|
+
import { View, Router } from "@lark.js/mvc";
|
|
241
|
+
|
|
242
|
+
export default View.extend({
|
|
243
|
+
make() {
|
|
244
|
+
this.updater.set({ appName: "My App" });
|
|
245
|
+
this.on("destroy", () => {
|
|
246
|
+
console.log(`View destroyed: ${this.id}`);
|
|
247
|
+
});
|
|
248
|
+
},
|
|
249
|
+
navigate(path: string, params?: Record<string, unknown>) {
|
|
250
|
+
Router.to(path, params);
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
**How View.extend() works internally**: It creates an ES6 class that extends the parent View. The `make` function is collected into a `ctors` array (along with mixin `make` functions). In the constructor, extend props (like `template`) are applied as **instance properties** after `super()`, because ES6 class field declarations (e.g., `template;` in View) would shadow prototype properties. **CRITICAL**: `render` is explicitly skipped as an instance property — it is wrapped on the prototype by `View.wrapMethod()` to manage signature checking and resource cleanup. Setting `render` on the instance would bypass signature checking, the `"render"` event, and `destroyAllResources()`.
|
|
66
256
|
|
|
67
|
-
|
|
257
|
+
### 6. Define Stores
|
|
68
258
|
|
|
69
259
|
```typescript
|
|
70
|
-
|
|
260
|
+
// src/store/count.ts
|
|
261
|
+
import { defineStore, cell, observeCell, multi } from "@lark.js/mvc";
|
|
71
262
|
|
|
72
263
|
export interface CountStore {
|
|
73
264
|
count: number;
|
|
@@ -78,868 +269,445 @@ export interface CountStore {
|
|
|
78
269
|
reset: () => void;
|
|
79
270
|
setStep: (val: number) => void;
|
|
80
271
|
clearHistory: () => void;
|
|
272
|
+
registerObservers: () => void;
|
|
81
273
|
}
|
|
82
274
|
|
|
83
|
-
const useCountStore = defineStore<CountStore>(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
275
|
+
const useCountStore = defineStore<CountStore>(
|
|
276
|
+
"count",
|
|
277
|
+
(store, { lazySet, shallowSet }) => {
|
|
278
|
+
return {
|
|
279
|
+
count: 0,
|
|
280
|
+
step: 1,
|
|
281
|
+
history: [] as string[],
|
|
282
|
+
increment() {
|
|
283
|
+
store.count = store.count + store.step;
|
|
284
|
+
store.history = [...store.history, `+${store.step} -> ${store.count}`];
|
|
285
|
+
},
|
|
286
|
+
decrement() {
|
|
287
|
+
store.count = store.count - store.step;
|
|
288
|
+
store.history = [...store.history, `-${store.step} -> ${store.count}`];
|
|
289
|
+
},
|
|
290
|
+
reset() {
|
|
291
|
+
store.count = 0;
|
|
292
|
+
store.history = [...store.history, "Reset -> 0"];
|
|
293
|
+
},
|
|
294
|
+
setStep(val: number) {
|
|
295
|
+
store.step = val;
|
|
296
|
+
},
|
|
297
|
+
clearHistory() {
|
|
298
|
+
store.history = [];
|
|
299
|
+
},
|
|
300
|
+
registerObservers() {
|
|
301
|
+
// Inner observe (no view binding) for store-internal reactions
|
|
302
|
+
store.observe(undefined, ["step"], () => {
|
|
303
|
+
store.count = 0; // Reset count when step changes
|
|
304
|
+
});
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
},
|
|
308
|
+
);
|
|
88
309
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
310
|
+
export default useCountStore;
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
**Creator function signature**: `(store, apis) => initialState`
|
|
314
|
+
|
|
315
|
+
- `store` — InnerStore proxy for reading/writing reactive state. Reads return the raw reactive Proxy; writes through `store.key = value` trigger dependency tracking
|
|
316
|
+
- `apis` — `{ lazySet, shallowSet }` utility functions:
|
|
317
|
+
- `lazySet(target, data)` — Batch-set properties without triggering observers (used during `createState` initialization)
|
|
318
|
+
- `shallowSet(target, key, data)` — Create a shallow reactive sub-state where only top-level property changes trigger observers
|
|
319
|
+
|
|
320
|
+
### Standalone Reactive Cells
|
|
321
|
+
|
|
322
|
+
For simple reactive data that doesn't need the full store pattern, use `cell` and `observeCell`:
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
import { cell, observeCell } from "@lark.js/mvc";
|
|
326
|
+
|
|
327
|
+
// Create a standalone reactive cell
|
|
328
|
+
const count = cell(0);
|
|
329
|
+
|
|
330
|
+
// Observe changes
|
|
331
|
+
const unobserve = observeCell(count, () => {
|
|
332
|
+
console.log("count changed:", count.count);
|
|
108
333
|
});
|
|
109
334
|
|
|
110
|
-
|
|
335
|
+
// Mutate (triggers observer)
|
|
336
|
+
count.count = 1;
|
|
337
|
+
|
|
338
|
+
// Clean up
|
|
339
|
+
unobserve();
|
|
111
340
|
```
|
|
112
341
|
|
|
113
|
-
|
|
342
|
+
`cell(data)` creates a Proxy-based reactive state with `belong: "lark-global"` and a unique `linkKeys`. `observeCell(state, cb, immediate?)` tracks changes and calls `cb` when any property changes. By default `immediate = true` (fires callback immediately).
|
|
343
|
+
|
|
344
|
+
### Multi-Instance Stores
|
|
114
345
|
|
|
115
|
-
|
|
346
|
+
When a component is used multiple times on the same page and each instance needs independent state, use `multi`:
|
|
347
|
+
|
|
348
|
+
```typescript
|
|
349
|
+
import { defineStore, multi } from "@lark.js/mvc";
|
|
116
350
|
|
|
117
|
-
|
|
351
|
+
const useCounterStore = defineStore("counter", (store) => ({
|
|
352
|
+
count: 0,
|
|
353
|
+
increment() {
|
|
354
|
+
store.count++;
|
|
355
|
+
},
|
|
356
|
+
}));
|
|
118
357
|
|
|
119
|
-
|
|
358
|
+
// multi() returns [useFn, mixinObj]
|
|
359
|
+
const [useMultiCounter, counterMixin] = multi(useCounterStore);
|
|
360
|
+
|
|
361
|
+
// In the view:
|
|
362
|
+
export default View.extend({
|
|
363
|
+
mixins: [counterMixin], // make() generates per-instance flag
|
|
364
|
+
template,
|
|
365
|
+
init() {
|
|
366
|
+
const store = useMultiCounter(this); // Each view instance gets its own store clone
|
|
367
|
+
store.observe(this, ["count"]);
|
|
368
|
+
},
|
|
369
|
+
});
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
`multi()` works by intercepting `mountFrame` calls to propagate a per-instance flag (`lark-comp-{storeName}`) down the Frame tree. When `useFn(view)` is called, it checks the flag and either returns an existing store clone or creates one via `cloneStore()`.
|
|
373
|
+
|
|
374
|
+
### 7. Create a View with Template
|
|
120
375
|
|
|
121
376
|
```typescript
|
|
377
|
+
// src/views/home.ts
|
|
122
378
|
import { Router } from "@lark.js/mvc";
|
|
123
379
|
import View from "../view";
|
|
124
|
-
import template from "./
|
|
380
|
+
import template from "./home.html";
|
|
125
381
|
import useCountStore from "../store/count";
|
|
126
382
|
|
|
127
383
|
export default View.extend({
|
|
128
|
-
// const template: (data: unknown, selfId: string, refData: unknown) => string;
|
|
129
384
|
template,
|
|
130
385
|
|
|
131
386
|
init() {
|
|
387
|
+
this.assign();
|
|
388
|
+
|
|
389
|
+
// Observe count store -- changes trigger automatic view re-render
|
|
132
390
|
const store = useCountStore(this);
|
|
133
|
-
store.observe(this, ["count", "step"
|
|
391
|
+
store.observe(this, ["count", "step"]);
|
|
134
392
|
},
|
|
135
393
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
394
|
+
assign() {
|
|
395
|
+
this.updater.snapshot();
|
|
396
|
+
|
|
397
|
+
const store = useCountStore();
|
|
398
|
+
this.updater.set({
|
|
399
|
+
title: "Home",
|
|
400
|
+
count: store.count,
|
|
401
|
+
step: store.step,
|
|
402
|
+
});
|
|
139
403
|
|
|
140
|
-
|
|
141
|
-
useCountStore().decrement();
|
|
404
|
+
return this.updater.altered();
|
|
142
405
|
},
|
|
143
406
|
|
|
144
|
-
|
|
145
|
-
|
|
407
|
+
render() {
|
|
408
|
+
this.updater.digest();
|
|
146
409
|
},
|
|
147
410
|
|
|
148
|
-
"
|
|
149
|
-
const
|
|
150
|
-
const
|
|
151
|
-
|
|
411
|
+
"navigateTo<click>"(e: Record<string, unknown>) {
|
|
412
|
+
const params = e["params"] as Record<string, string> | undefined;
|
|
413
|
+
const path = params?.path;
|
|
414
|
+
if (path) Router.to(path);
|
|
152
415
|
},
|
|
153
416
|
});
|
|
154
417
|
```
|
|
155
418
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
模板使用 {{}} 语法, = 表示 HTML 转义输出, v-click 等属性绑定事件, 编译时自动将 View ID 和参数编码到事件属性值中
|
|
419
|
+
### 8. Write Templates
|
|
159
420
|
|
|
160
421
|
```html
|
|
422
|
+
<!-- src/views/home.html -->
|
|
161
423
|
<div>
|
|
162
|
-
<
|
|
163
|
-
<
|
|
164
|
-
<button
|
|
165
|
-
<button v-click="reset()">Reset</button>
|
|
166
|
-
<button v-click="increment()">+{{=step}}</button>
|
|
424
|
+
<h1>{{=title}}</h1>
|
|
425
|
+
<div>Count: {{=count}}</div>
|
|
426
|
+
<button @click="navigateTo({path: '/about'})">About</button>
|
|
167
427
|
|
|
168
|
-
{{if
|
|
169
|
-
<
|
|
170
|
-
{{each history as record}}
|
|
171
|
-
<li>{{=record}}</li>
|
|
172
|
-
{{/each}}
|
|
173
|
-
</ul>
|
|
428
|
+
{{if count > 0}}
|
|
429
|
+
<p>Positive</p>
|
|
174
430
|
{{else}}
|
|
175
|
-
<p>
|
|
431
|
+
<p>Zero or negative</p>
|
|
176
432
|
{{/if}}
|
|
433
|
+
|
|
434
|
+
<ul>
|
|
435
|
+
{{forOf items as item idx}}
|
|
436
|
+
<li>{{=idx}}: {{=item}}</li>
|
|
437
|
+
{{/forOf}}
|
|
438
|
+
</ul>
|
|
439
|
+
|
|
440
|
+
<!-- Sub-view embedding -->
|
|
441
|
+
<div v-lark="components/child"></div>
|
|
177
442
|
</div>
|
|
178
443
|
```
|
|
179
444
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
通过 registerViewClass 注册 View 类, 配置 routes 路由映射, 调用 Framework.boot(config) 启动框架
|
|
445
|
+
### 9. Bootstrap the Application
|
|
183
446
|
|
|
184
447
|
```typescript
|
|
185
|
-
|
|
186
|
-
import
|
|
187
|
-
import
|
|
188
|
-
|
|
448
|
+
// src/boot.ts
|
|
449
|
+
import { Framework, Router, View, registerViewClass } from "@lark.js/mvc";
|
|
450
|
+
import type { FrameworkConfig } from "@lark.js/mvc";
|
|
451
|
+
import HomeView from "./views/home";
|
|
452
|
+
import AboutView from "./views/about";
|
|
453
|
+
import CounterView from "./views/counter";
|
|
454
|
+
import NotFoundView from "./views/404";
|
|
455
|
+
import CounterStoreComponent from "./components/counter-store";
|
|
456
|
+
import CounterUpdaterComponent from "./components/counter-updater";
|
|
457
|
+
|
|
458
|
+
// Register View classes to Frame
|
|
189
459
|
registerViewClass("home", HomeView as typeof View);
|
|
460
|
+
registerViewClass("about", AboutView as typeof View);
|
|
190
461
|
registerViewClass("counter", CounterView as typeof View);
|
|
191
|
-
|
|
192
|
-
|
|
462
|
+
registerViewClass("404", NotFoundView as typeof View);
|
|
463
|
+
registerViewClass(
|
|
464
|
+
"components/counter-store",
|
|
465
|
+
CounterStoreComponent as typeof View,
|
|
466
|
+
);
|
|
467
|
+
registerViewClass(
|
|
468
|
+
"components/counter-updater",
|
|
469
|
+
CounterUpdaterComponent as typeof View,
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
const config: FrameworkConfig = {
|
|
193
473
|
rootId: "app",
|
|
194
474
|
defaultPath: "/home",
|
|
195
475
|
defaultView: "home",
|
|
196
476
|
routes: {
|
|
197
477
|
"/home": "home",
|
|
478
|
+
"/about": "about",
|
|
198
479
|
"/counter": "counter",
|
|
199
480
|
},
|
|
200
481
|
unmatchedView: "404",
|
|
201
482
|
error(e: Error) {
|
|
202
|
-
console.error("Lark
|
|
483
|
+
console.error("Lark error:", e);
|
|
203
484
|
},
|
|
204
|
-
}
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
核心模块
|
|
208
|
-
|
|
209
|
-
Framework — 框架主入口
|
|
210
|
-
|
|
211
|
-
Framework 是 Lark 框架的主入口对象, 提供 boot() 启动方法与全局工具方法
|
|
212
|
-
|
|
213
|
-
boot() 启动流程如下:
|
|
214
|
-
|
|
215
|
-
1. 合并配置 (rootId, defaultView, routes, hashbang 等)
|
|
216
|
-
2. `Router._setConfig()` 注入路由配置
|
|
217
|
-
3. `EventDelegator.setFrameGetter()` 注入 Frame 查找器
|
|
218
|
-
4. 绑定 Router 的 changed 事件到 `dispatcherNotifyChange()`
|
|
219
|
-
5. 绑定 State 的 changed 事件到 `dispatcherNotifyChange()`
|
|
220
|
-
6. `Frame.root(rootId)` 创建根 Frame 节点
|
|
221
|
-
7. `Router._bind()` 绑定 hashchange 事件, 执行首次 diff
|
|
222
|
-
8. 如果路由没有已挂载的视图, 则 `rootFrame.mountView(defaultView)` 挂载默认视图
|
|
223
|
-
|
|
224
|
-
Framework 还提供以下工具方法
|
|
225
|
-
|
|
226
|
-
- mark/unmark 异步回调追踪
|
|
227
|
-
- dispatch 自定义 DOM 事件
|
|
228
|
-
- task 分片任务调度
|
|
229
|
-
- delay Promise 延时
|
|
230
|
-
- use 模块加载
|
|
231
|
-
- toMap/toTry/toUrl/parseUrl/has/keys/inside/node/guard/applyStyle 等工具函数
|
|
232
|
-
- Cache, Base, Router, State, View, Frame 等模块的直接访问
|
|
233
|
-
|
|
234
|
-
callFunction 分片执行
|
|
235
|
-
|
|
236
|
-
```ts
|
|
237
|
-
let callIndex = 0;
|
|
238
|
-
// 扁平存储: [fn, ctx, args, fn2, ctx2, args2, ...]
|
|
239
|
-
const callList = [];
|
|
240
|
-
// 每片最大执行时间 48ms
|
|
241
|
-
const callBreakTime = 48;
|
|
242
|
-
|
|
243
|
-
function startCall() {
|
|
244
|
-
const last = Date.now(),
|
|
245
|
-
next;
|
|
246
|
-
while (true) {
|
|
247
|
-
next = callList[callIndex - 1];
|
|
248
|
-
// 依次取出函数执行
|
|
249
|
-
if (next) {
|
|
250
|
-
next.apply(callList[callIndex], callList[callIndex + 1]);
|
|
251
|
-
callIndex += 3; // 每次消费 3 个元素: fn/ctx/args
|
|
252
|
-
// 每执行一个函数都会检查一次耗时
|
|
253
|
-
if (Date.now() - last > callBreakTime && callList.length > callIndex) {
|
|
254
|
-
setTimeout(startCall); // 超时则让出主线程
|
|
255
|
-
break;
|
|
256
|
-
}
|
|
257
|
-
} else {
|
|
258
|
-
callList.length = callIndex = 0; // 全部执行, 清空
|
|
259
|
-
break;
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
function callFunction(fn, args, ctx) {
|
|
265
|
-
callList.push(fn, ctx, args); // 入队
|
|
266
|
-
if (!callIndex) {
|
|
267
|
-
// 如果没有在执行中
|
|
268
|
-
callIndex = 1;
|
|
269
|
-
setTimeout(startCall); // 异步启动
|
|
270
|
-
}
|
|
271
|
-
}
|
|
485
|
+
};
|
|
272
486
|
|
|
273
|
-
Framework.
|
|
487
|
+
Framework.boot(config);
|
|
274
488
|
```
|
|
275
489
|
|
|
276
|
-
|
|
277
|
-
- 异步启动: 通过 setTimeout(startCall) 推迟到下一个事件循环开始执行
|
|
278
|
-
- 分片执行: startCall 在 while 循环中, 依次取出函数执行, 每执行一个函数都会检查一次耗时
|
|
279
|
-
|
|
280
|
-
这是早期前端的长任务分片实践,用于
|
|
281
|
-
|
|
282
|
-
- 避免一次性执行大量回调, 例如大量 View 渲染完成后的通知导致页面卡顿
|
|
283
|
-
- 保持 60fps (每帧约 16ms, 48ms 约占 3 帧) 的用户交互响应
|
|
284
|
-
- 与 requestIdleCallback 的思路类似, 但 callFunction 设计时浏览器可能还没有该 API
|
|
490
|
+
## Key Patterns
|
|
285
491
|
|
|
286
|
-
|
|
492
|
+
### Event Method Naming
|
|
287
493
|
|
|
288
|
-
|
|
494
|
+
Event handlers use the `name<eventType>` pattern. The framework scans the View prototype at class preparation time (in `View.prepare()`) and builds three event maps on the prototype: `$evtObjMap`, `$selMap`, `$globalEvtList`.
|
|
289
495
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
496
|
+
| Pattern | Meaning |
|
|
497
|
+
| -------------------------- | ------------------------------------------------- |
|
|
498
|
+
| `handler<click>` | Root event on the view element |
|
|
499
|
+
| `$selector<click>` | Delegated event matching CSS selector `.selector` |
|
|
500
|
+
| `$window<resize>` | Global event on `window` |
|
|
501
|
+
| `$document<keydown>` | Global event on `document` |
|
|
502
|
+
| `handler<click,mousedown>` | Multi-event binding |
|
|
293
503
|
|
|
294
|
-
|
|
504
|
+
All DOM events are delegated to `document.body` using **capture-phase** listeners (`addEventListener(type, handler, true)`). The EventDelegator uses reference counting: the first binding adds the listener, the last unbinding removes it. On event dispatch, the processor walks up the DOM from the target to `document.body`, checking `@<eventType>` attributes and selector matches at each level.
|
|
295
505
|
|
|
296
|
-
|
|
297
|
-
| -------------- | -------------------------------------- | ----------------------------------------------------- |
|
|
298
|
-
| 调度 API | setTimeout | scheduler.postTask → requestIdleCallback → setTimeout |
|
|
299
|
-
| 时间切片 | 固定 48ms | 自适应 deadline.timeRemaining() + 48ms fallback |
|
|
300
|
-
| 接口签名 | 扁平数组 [fn, ctx, args, ...] 逐个消费 | 相同保持兼容性 |
|
|
301
|
-
| 让出主线程时机 | 仅超时 | |
|
|
506
|
+
The event object passed to handlers contains:
|
|
302
507
|
|
|
303
|
-
|
|
508
|
+
- `e.eventTarget` -- the actual DOM element that was clicked
|
|
509
|
+
- `e.params` -- parsed parameters from `@event` attributes (URL query string format)
|
|
510
|
+
- Standard DOM Event properties (`type`, `target`, etc.)
|
|
304
511
|
|
|
305
|
-
|
|
512
|
+
When two mixins define the same event method, they are merged into a single function that calls both in sequence via a `handlerList` array.
|
|
306
513
|
|
|
307
|
-
|
|
514
|
+
### Store Observe Variations
|
|
308
515
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
核心 API:
|
|
516
|
+
```typescript
|
|
517
|
+
// Simple: auto-digest on key change
|
|
518
|
+
store.observe(this, ["count", "step"]);
|
|
313
519
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
520
|
+
// With custom callback
|
|
521
|
+
store.observe(this, ["count"], (changedMap) => {
|
|
522
|
+
console.log("count changed", changedMap);
|
|
523
|
+
});
|
|
318
524
|
|
|
319
|
-
|
|
525
|
+
// With ObservePayload for fine-grained control
|
|
526
|
+
store.observe(this, [
|
|
527
|
+
{ key: "count", alias: "currentCount", lazy: false },
|
|
528
|
+
{ key: "items", transform: (val) => ({ itemCount: val.length }) },
|
|
529
|
+
]);
|
|
320
530
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
/** 查询字符串 */
|
|
326
|
-
srcQuery: string;
|
|
327
|
-
/** hash 字符串 */
|
|
328
|
-
srcHash: string;
|
|
329
|
-
/** 查询参数 */
|
|
330
|
-
query: ParsedUri;
|
|
331
|
-
/** 哈希参数 */
|
|
332
|
-
hash: ParsedUri;
|
|
333
|
-
/** 合并参数 */
|
|
334
|
-
params: Record<string, string>;
|
|
335
|
-
/** 视图路径 */
|
|
336
|
-
view?: string;
|
|
337
|
-
/** 路由路径 */
|
|
338
|
-
path?: string;
|
|
339
|
-
/** 参数读取函数 */
|
|
340
|
-
get: (key: string, defaultValue?: string) => string;
|
|
341
|
-
}
|
|
342
|
-
```
|
|
531
|
+
// Inner observe (no view binding, for store-internal reactions)
|
|
532
|
+
store.observe(undefined, ["step"], () => {
|
|
533
|
+
store.count = 0; // Reset count when step changes
|
|
534
|
+
});
|
|
343
535
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
export interface LocationDiff {
|
|
348
|
-
/** 变更参数映射 */
|
|
349
|
-
params: Record<string, ParamDiff>;
|
|
350
|
-
/** 路径变更 */
|
|
351
|
-
path?: ParamDiff;
|
|
352
|
-
/** 视图变更 */
|
|
353
|
-
view?: ParamDiff;
|
|
354
|
-
/** 是否首次强制变更 */
|
|
355
|
-
force: boolean;
|
|
356
|
-
/** 是否有变更 */
|
|
357
|
-
changed: boolean;
|
|
358
|
-
}
|
|
536
|
+
// Lazy observe (default: true) -- triggers updater.digest() on change
|
|
537
|
+
// Non-lazy observe (lazy: false) -- triggers updater.set() then digest()
|
|
538
|
+
// This means lazy: true merges data via digest(), lazy: false sets data first
|
|
359
539
|
```
|
|
360
540
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
调试模式下, Location.params 和 LocationDiff 会被 safeguard() Proxy 包装, 拦截非法写入
|
|
364
|
-
|
|
365
|
-
State — 跨视图 observable 数据
|
|
541
|
+
### Store Read/Write Behavior
|
|
366
542
|
|
|
367
|
-
|
|
543
|
+
The `useStore()` function returns a Proxy that separates state reads from handler access:
|
|
368
544
|
|
|
369
|
-
|
|
545
|
+
- **Reading state keys** (e.g., `store.count`) — returns a deep-cloned copy of the value (frozen for React adapter). This prevents external mutation of reactive state
|
|
546
|
+
- **Writing state keys** (e.g., `store.count = 5`) — sets the value on the internal reactive state, triggering dependency tracking
|
|
547
|
+
- **Accessing handlers** (e.g., `store.increment()`) — calls the function defined in the creator
|
|
548
|
+
- **Inside the creator**, `store.count` reads the raw reactive Proxy (no cloning), enabling direct mutation and reactivity
|
|
370
549
|
|
|
371
|
-
|
|
372
|
-
2. `State.digest()` 触发变更通知: 将变更键传递给 Dispatcher, Dispatcher 沿 Frame 树查找观察了这些键的 View 并触发重新渲染
|
|
373
|
-
3. `State.diff()` 返回上次 digest 的变更键映射
|
|
550
|
+
The store uses a microtask-based `Queue` scheduler (Promise.resolve().then()) for batching observer callbacks. Multiple state changes in the same synchronous block are batched and processed in a single microtask.
|
|
374
551
|
|
|
375
|
-
|
|
552
|
+
### Router Two-Phase Change
|
|
376
553
|
|
|
377
|
-
|
|
554
|
+
Route changes go through two phases:
|
|
378
555
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
Store 模块基于 Proxy 实现深度响应式状态管理, 支持多平台适配 (Lark / React / Node), 提供 defineStore 定义 store 与自动依赖追踪
|
|
382
|
-
|
|
383
|
-
createState 响应式状态底层实现
|
|
384
|
-
|
|
385
|
-
`createState(data, config?)` 接收一个普通 JS 对象, 返回 Proxy 包装的代理对象 (响应式状态). Proxy 的 get 拦截器执行 track(depKey, effect) 收集依赖, set 拦截器执行 trigger(depKey) 触发更新. 依赖追踪通过全局 `GlobalDeps` Map 实现: key 为 SPLITTER + depKey (例如如 \x1ecount), value 为 effect 函数的 Set 集合
|
|
386
|
-
|
|
387
|
-
当读取到对象或数组类型的属性值时, 递归调用 createState 包装, 实现深层响应式. 对于数组, 代理了 indexOf, includes, push, pop, splice, shift, unshift, sort, reverse 等方法, 使得数组操作也能触发响应式更新
|
|
388
|
-
|
|
389
|
-
defineStore — 定义 store
|
|
390
|
-
|
|
391
|
-
Store 模块使用 defineStore 定义 store 实例,通过 observe 建立 store 与 view 的响应式绑定, 状态改变时自动触发回调, 不需要手动调用 `updater.digest()`
|
|
556
|
+
1. **`change` event** -- listeners can call `prevent()` or `reject()` to cancel navigation
|
|
557
|
+
2. **`changed` event** -- URL has been updated, views are re-rendered
|
|
392
558
|
|
|
393
559
|
```typescript
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
): LarkUseStore<S> | ReactUseStore<S> | NodeUseStore<S>;
|
|
400
|
-
```
|
|
401
|
-
|
|
402
|
-
### store 生命周期
|
|
403
|
-
|
|
404
|
-
defineStore 返回 useStore 函数, 内部维护三个状态: IDLE 未使用、ACTIVE 已激活、DESTROYED 已销毁
|
|
405
|
-
|
|
406
|
-
- 首次调用 `useStore(view)` 时激活 store, store 进入 ACTIVE 状态并执行 creator
|
|
407
|
-
- 所有绑定的 View 销毁后, store 自动进入 DESTROYED 状态
|
|
408
|
-
- 重新调用 useStore(view) 时会重新执行 creator 创建新的 store 实例
|
|
409
|
-
|
|
410
|
-
LarkStore: observe(view, keys, defCallback?) 绑定视图生命周期
|
|
411
|
-
|
|
412
|
-
```ts
|
|
413
|
-
store.observe(
|
|
414
|
-
// 传递 view 实例时, 状态变更时自动调用 updater.set()、updater.digest()
|
|
415
|
-
// 传递 undefined 时, 不绑定 view, 仅注册内部监听
|
|
416
|
-
view: View | undefined,
|
|
417
|
-
// 监听的键列表, 支持字符串和 ObservePayload 对象
|
|
418
|
-
keys: (string | ObservePayload)[],
|
|
419
|
-
// 可选, 回调函数, 默认为 updater.digest
|
|
420
|
-
defCallback?: (changedMap) => void,
|
|
421
|
-
): () => void
|
|
422
|
-
```
|
|
423
|
-
|
|
424
|
-
```ts
|
|
425
|
-
interface ObservePayload {
|
|
426
|
-
key: string; // 监听的键, 支持 a.b 路径
|
|
427
|
-
alias?: string; // 别名
|
|
428
|
-
cb: (changedMap) => void; // 自定义回调函数
|
|
429
|
-
lazy?: boolean; // 是否延迟执行, 默认 true
|
|
430
|
-
transform?: (val) => object; // 状态派生函数
|
|
431
|
-
}
|
|
560
|
+
Router.on("change", (e) => {
|
|
561
|
+
if (hasUnsavedChanges) {
|
|
562
|
+
e.prevent(); // Cancel navigation
|
|
563
|
+
}
|
|
564
|
+
});
|
|
432
565
|
```
|
|
433
566
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
- 使用 `freezeData()` 冻结数据, 生成不可变快照, 适配 React 的不可变数据模式
|
|
437
|
-
- 每次状态变更后生成新的快照, 确保引用变化以触发 React 组件重渲染
|
|
438
|
-
|
|
439
|
-
NodeStore: observe 直接执行回调, 不绑定视图生命周期, 适用于 Node.js 环境下的状态管理
|
|
567
|
+
### Resource Management
|
|
440
568
|
|
|
441
|
-
|
|
569
|
+
Use `capture/release` for automatic cleanup:
|
|
442
570
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
cell 本质上是 createState 的轻量封装, 适用于不依赖 View 生命周期的独立的响应式数据
|
|
455
|
-
|
|
456
|
-
_multi — 多实例 store_
|
|
457
|
-
|
|
458
|
-
multi(useStore) 创建支持多实例的 store, 内部通过 flag 标识不同的 View, 首次调用时为每个 flag 克隆一个新的 store 实例 (cloneStore), 每个 store 拥有独立的 storeName 和状态. flag 通过拦截 mountFrame 沿 Frame 树向下传播, 确保同一个 Frame 树共享同一个 store 实例, 不同 Frame 树使用独立的 store 实例
|
|
459
|
-
|
|
460
|
-
- `getStore(name)` / `delStore(name)` / `cloneStore(name, useStore, config?)` 按 store 名称获取、删除、克隆 store 实例
|
|
461
|
-
- `isStoreActive(name)` 检查 store 是否处于 ACTIVE 状态
|
|
462
|
-
- `isState(target)` 检查对象是否为 Proxy 响应式状态
|
|
463
|
-
|
|
464
|
-
## Frame — 视图生命周期树
|
|
465
|
-
|
|
466
|
-
Frame 是管理 View 生命周期的容器, 与 DOM 结构对应组成 Frame 树; 每个 Frame 拥有一个 View 实例, 负责挂载、卸载、子区域管理和方法调用
|
|
467
|
-
|
|
468
|
-
Frame 树的设计思路
|
|
469
|
-
|
|
470
|
-
- Frame 树与 DOM 树结构相同, 每个 Frame 节点对应一个 DOM 元素 (通过 ID 关联)
|
|
471
|
-
- Frame 对象挂载到 DOM 元素的 frame 属性上 [frame.ts](./src/frame.ts#L130)
|
|
472
|
-
|
|
473
|
-
这种设计使得 DOM 操作 (例如 VDOM diff) 可以直接通过 Frame 找到对应的 View, 也使得 View 事件处理可以沿 Frame 树向上冒泡
|
|
474
|
-
|
|
475
|
-
```js
|
|
476
|
-
const app = document.getElementById("app");
|
|
477
|
-
// app.frame
|
|
478
|
-
const frame = {
|
|
479
|
-
id: "app", // frameId, 与 DOM 元素 ID 相同
|
|
480
|
-
|
|
481
|
-
parentId: undefined, // rootFrame 没有父节点
|
|
482
|
-
|
|
483
|
-
// childrenCount: 子 frame 数量
|
|
484
|
-
childrenCount: 2, // 有 2 个子 frame
|
|
485
|
-
|
|
486
|
-
// 已触发 created 事件的子 frame 数量
|
|
487
|
-
readyCount: 2, // 2 个子 frame 已触发 created 事件
|
|
488
|
-
|
|
489
|
-
// 所有子 frame 是否已触发 created 事件
|
|
490
|
-
childrenCreated: 1,
|
|
491
|
-
|
|
492
|
-
// 子 frameId 映射
|
|
493
|
-
childrenMap: {
|
|
494
|
-
frame_0: "frame_0", // 对应 lark-view="components/counter-store"
|
|
495
|
-
frame_1: "frame_1", // 对应 lark-view="components/counter-updater"
|
|
571
|
+
```typescript
|
|
572
|
+
const timer = setInterval(() => {
|
|
573
|
+
/* ... */
|
|
574
|
+
}, 1000);
|
|
575
|
+
this.capture(
|
|
576
|
+
"myTimer",
|
|
577
|
+
{
|
|
578
|
+
destroy() {
|
|
579
|
+
clearInterval(timer);
|
|
580
|
+
},
|
|
496
581
|
},
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
// invokeList 延迟方法调用列表
|
|
502
|
-
// originalTemplate 挂载前的原始 html
|
|
503
|
-
};
|
|
582
|
+
true,
|
|
583
|
+
);
|
|
584
|
+
// destroyOnRender=true: destroyed on next render call
|
|
585
|
+
// destroyOnRender=false: destroyed only on view destroy
|
|
504
586
|
```
|
|
505
587
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
- `mountView(viewPath, viewInitParams?)`: 查找 View 类、实例化、执行 `init()` 和 `render()`, 将 HTML 注入 DOM
|
|
509
|
-
- `unmountView()`: 销毁 View 以及所有子 Frame, 清理事件绑定与资源
|
|
510
|
-
- `mountFrame(frameId, viewPath, viewInitParams?)`: 挂载子 Frame, 复用对象池中的 Frame 或创建新 Frame
|
|
511
|
-
- `unmountFrame(id?)`: 卸载子 Frame, 归还 Frame 对象到对象池
|
|
512
|
-
- `mountZone(zoneId?)` / `unmountZone(zoneId?)`: 管理区域内的 lark-view 子视图, 区域内的子 Frame 在 mountZone 时批量创建
|
|
513
|
-
- `invoke(name, args?)`: 沿 Frame 树广播方法调用; 如果 View 已渲染则直接调用, 否则加入 invokeList 延迟执行
|
|
514
|
-
- `children()`: 返回所有子 Frame ID 列表
|
|
515
|
-
- `parent(level?)`: 获取指定层级的父 Frame
|
|
516
|
-
|
|
517
|
-
对象池: Frame 对象使用 frameCache 数组作为对象池, unmountFrame 时将 Frame 归还池中, mountFrame 时优先从池中取出复用, 避免频繁 GC
|
|
518
|
-
|
|
519
|
-
事件通知: notifyCreated 与 notifyAlter 事件沿 Frame 树向上冒泡, 当所有子 Frame 就绪时触发 created 事件, 子 Frame 发生变化时触发 alter 事件. holdFireCreated 标志用于批量挂载时延迟触发 created 事件. 这些事件用于 waitZoneViewsRendered() 等待子视图就绪
|
|
520
|
-
|
|
521
|
-
translateQuery: 当 viewPath 包含 SPLITTER 引用时, 通过父视图的 refData 解析引用参数, 实现父视图向子视图传递对象引用
|
|
522
|
-
|
|
523
|
-
registerViewClass(viewPath, ViewClass): 注册 View 类到 viewClassRegistry, 提供给 mountView 查找
|
|
524
|
-
|
|
525
|
-
View — 视图基类
|
|
526
|
-
|
|
527
|
-
View 视图基类负责用户交互, 提供数据绑定、事件处理、生命周期与视图继承能力
|
|
588
|
+
### Async Safety with wrapAsync
|
|
528
589
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
"$window<resize>"(e) {}
|
|
539
|
-
|
|
540
|
-
// document 上监听 keydown
|
|
541
|
-
"$document<keydown>"(e) {}
|
|
542
|
-
|
|
543
|
-
// 点击 .btn 元素时触发
|
|
544
|
-
"$btn<click>"(e) {}
|
|
590
|
+
```typescript
|
|
591
|
+
async loadData() {
|
|
592
|
+
const safeCallback = this.wrapAsync((data) => {
|
|
593
|
+
this.updater.set({ items: data }).digest();
|
|
594
|
+
});
|
|
595
|
+
const data = await fetch("/api/items").then(r => r.json());
|
|
596
|
+
safeCallback(data); // Only executes if view has not been re-rendered/destroyed
|
|
597
|
+
}
|
|
598
|
+
```
|
|
545
599
|
|
|
546
|
-
|
|
547
|
-
"$tooltip<mouseenter>"(e) {}
|
|
600
|
+
### Sub-View Embedding
|
|
548
601
|
|
|
549
|
-
|
|
550
|
-
// 点击 frame 根节点时触发
|
|
551
|
-
"$<click>"(e) {}
|
|
552
|
-
```
|
|
602
|
+
Use the `v-lark` attribute to embed child views:
|
|
553
603
|
|
|
554
604
|
```html
|
|
555
|
-
<
|
|
556
|
-
<div class="tooltip">不需要写 v-event, 适用于批量绑定</div>
|
|
605
|
+
<div v-lark="components/child-view"></div>
|
|
557
606
|
```
|
|
558
607
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
_生命周期方法_
|
|
562
|
-
|
|
563
|
-
- `make()`: 构造函数, View 实例创建时调用
|
|
564
|
-
- `init(options)`: 初始化, 接收路由参数与传入数据; 返回 Promise 对象时, 框架会等待该 Promise 对象 resolve 后再执行 render
|
|
565
|
-
- `render()`: 渲染方法, 内部调用 `updater.digest()` 生成 HTML 并执行 VDOM diff 更新 DOM; render 被 `viewWrapMethod` 包装, 包装函数会: 检查 signature 有效性, 递增 signature, 执行资源清理 (destroyOnRender), 调用 unmark 使旧的异步回调失效, 调用 `assign(])` 判断是否可以跳过完整渲染
|
|
566
|
-
- `assign(options)`: 响应式更新模式, 仅当 assign() 返回 true 时跳过完整渲染, 执行增量更新
|
|
567
|
-
|
|
568
|
-
> Lark 渲染管线对比 react 渲染管线
|
|
569
|
-
|
|
570
|
-
- react 渲染管线: state 变化 → 生成 VDOM 树 (JS 对象树) → 与旧 VDOM 树 diff → 计算最小 DOM 操作 → 批量提交; react 始终操作 JS 对象, 没有生成 HTML 字符串
|
|
571
|
-
- lark 渲染官宣: state 变化 → 调用模板函数生成 HTML 字符串 → 解析为真实 DOM → 与页面上真实 DOM 做 diff → 收集 DOM 操作 → 批量提交
|
|
572
|
-
|
|
573
|
-
Lark 框架的 diff 算法是真实 DOM diff, 只是真实 DOM 不在页面上
|
|
574
|
-
|
|
575
|
-
_数据观察_
|
|
576
|
-
|
|
577
|
-
- `observeLocation(keys, observePath?):` 声明关注的路由参数键, 路由变更时如果匹配的键发生变化, 则自动触发 View 重新渲染
|
|
578
|
-
- `observeState(keys)`: 声明关注的 State 键, State 变更时如果匹配的键发生变化, 则自动触发 View 重新渲染; keys 是逗号分隔的字符串
|
|
579
|
-
|
|
580
|
-
_资源管理_
|
|
581
|
-
|
|
582
|
-
- `capture(key, resource?, destroyOnRender?)`: 注册资源到 View, 当 View 重新渲染时自动销毁有 destroyOnRender 标记的旧资源
|
|
583
|
-
- `release(key, destroy?)`: 手动释放资源
|
|
584
|
-
|
|
585
|
-
_异步安全_
|
|
586
|
-
|
|
587
|
-
- `wrapAsync(fn, context?)`: 包装异步回调, 通过 `mark()` 生成签名校验函数, View 销毁或重新渲染后 (unmark) 回调不会执行; wrapAsync 检查当前 signature 是否与调用时一致
|
|
588
|
-
- `leaveTip(message, condition):` 页面离开提示, 条件不满足时阻止跳转; 监听 Router 的 change 事件和 window 的 beforeunload 事件
|
|
589
|
-
|
|
590
|
-
Updater — 数据绑定与 VDOM diff
|
|
591
|
-
|
|
592
|
-
每个 View 持有一个 Updater 实例, 管理视图数据并触发渲染
|
|
593
|
-
|
|
594
|
-
核心流程
|
|
595
|
-
|
|
596
|
-
1. `updater.set(data, excludes?)`: 写入数据到内部 refData ($a), 记录变更键; excludes 排除变更检测的键集合
|
|
597
|
-
2. `updater.digest(data?, excludes?)`: 触发渲染管线:
|
|
598
|
-
- 如果传入了 data, 则先调用 `set(data, excludes)`
|
|
599
|
-
- 调用模板函数 `template(refData, viewId, refData)` 生成 HTML 字符串
|
|
600
|
-
- 通过虚拟文档 (vdomGetNode) 解析 HTML 为 DOM 节点
|
|
601
|
-
- 执行 VDOM diff (vdomSetChildNodes) 实际上是真实 DOM diff, 计算最小 DOM 操作集
|
|
602
|
-
- 调用 `applyIdUpdates()` 批量更新元素 ID
|
|
603
|
-
- 调用 `applyVdomOps()` 批量执行 DOM 操作
|
|
604
|
-
- 通知 Frame 处理新增的 lark-view 子视图 (mountZone)
|
|
605
|
-
3. `updater.snapshot()`: 保存当前数据的 JSON 快照
|
|
606
|
-
4. `updater.altered()`: 检测数据是否与上次快照不同 (通过 JSON.stringify 比较)
|
|
607
|
-
5. `updater.get(key?)`: 获取数据, 不传递 key 则返回完整数据对象
|
|
608
|
-
|
|
609
|
-
refData ($a): 模板函数中的引用对象 (例如循环中的列表项) 通过 refData 存储, 使用 SPLITTER 前缀的数字键索引 (例如 \x1e0, \x1e1). `updaterRef()` 负责去重查找与分配: 遍历 refData 中的 SPLITTER 前缀键, 查找值等于目标对象的已有键, 如果找到则复用, 否则分配新键
|
|
610
|
-
|
|
611
|
-
digestingQueue: 支持 digest 执行过程中再次调用 digest (重新渲染), 通过 digestingQueue 队列管理, 使用 null 哨兵分隔每次 digest 的数据, 当外层 digest 结束后处理队列中的待执行 digest
|
|
612
|
-
|
|
613
|
-
模板内置编码函数: $e (HTML 实体编码), $n (null 安全的 toString), $eu (URI 编码), $eq (引号编码), $i (引用查找, 用于 {{@}} 运算符)
|
|
614
|
-
|
|
615
|
-
VDOM Diff Engine — 虚拟 DOM (实际上是真实 DOM) 差量比较引擎
|
|
616
|
-
|
|
617
|
-
虚拟 DOM 差量比较引擎, 将新旧 DOM 树对比, 输出最小操作集
|
|
618
|
-
|
|
619
|
-
核心算法:
|
|
620
|
-
|
|
621
|
-
- `vdomSetNode(oldNode, newNode, parent, ref, frame, keys?)`: 对比单个节点. 先执行 vdomSpecialDiff 处理表单元素, 再通过 isEqualNode 判断节点是否相同. 如果节点类型和名称相同, 则分别对比属性 (vdomSetAttributes) 和子节点 (vdomSetChildNodes); 如果节点类型不同, 则生成 replaceChild 操作. 对于有 lark-view 属性的元素, 如果新旧 view 路径相同则跳过子节点更新, 保护已挂载的子视图
|
|
622
|
-
- `vdomSetChildNodes(oldParent, newParent, ref, frame, keys?)`: 对比子节点列表, 支持 keyed diff. 算法: 先从旧子节点构建 keyedNodes 映射 (键由 vdomGetCompareKey 获取, 优先级 id > lark-key > lark-view 路径), 同时统计新子节点的 keyed 计数; 再遍历新子节点, 通过 keyed 匹配查找旧节点, 匹配成功则将旧节点移动到正确位置并递归 diff, 匹配失败则生成 appendChild 操作; 最后移除多余的旧节点
|
|
623
|
-
- `vdomSetAttributes(oldNode, newNode, ref, keepId?)`: 对比元素属性. 删除新节点中不存在的旧属性, 添加/更新新节点中的属性. id 属性变更记录到 idUpdates 数组而不是直接修改, 因为 id 变更可能影响事件委托. 每次对比前清除 compareKeyCached 缓存
|
|
624
|
-
- `vdomSpecialDiff(oldNode, newNode)`: 处理 input (value, checked), textarea (value), option (selected) 等特殊元素, 直接同步 DOM 属性值, 绕过 setAttribute
|
|
625
|
-
|
|
626
|
-
比较键 (vdomGetCompareKey): 用于 keyed diff 的节点标识, 优先级: id 属性 > lark-key 属性 > lark-view 路径. 有 autoId 标记的元素不使用 id 作为比较键. 比较键结果缓存到元素的 compareKeyCached / cachedCompareKey 属性
|
|
627
|
-
|
|
628
|
-
静态跳过: 有 lark-key 属性且值相同的元素跳过 diff; 有 lark-attr-key 属性且值相同的元素跳过属性更新
|
|
629
|
-
|
|
630
|
-
DOM 操作编码为数字操作码:
|
|
631
|
-
|
|
632
|
-
- 1 = `appendChild(parent, newChild)`
|
|
633
|
-
- 2 = `removeChild(parent, oldChild)`
|
|
634
|
-
- 4 = `replaceChild(parent, newChild, oldChild)`
|
|
635
|
-
- 8 = `insertBefore(parent, newChild, refChild)`
|
|
636
|
-
|
|
637
|
-
`applyVdomOps(ops)` 批量执行操作码, `applyIdUpdates(updates)` 批量更新元素 ID (用于事件委托选择器匹配)
|
|
638
|
-
|
|
639
|
-
HTML 解析: `vdomGetNode(html, refNode)` 使用 `document.implementation.createHTMLDocument()` 创建虚拟文档, 通过 WrapMeta 映射处理 table, select, SVG, MathML 等需要特定父元素的标签 (例如 thead 需要包在 `<table>` 中, td 需要 `<table>, <tbody>, <tr>`). 自动检测 refNode 的 namespaceURI, SVG 命名空间使用 `<g>` 包裹, MathML 使用 `<math>` 包裹
|
|
640
|
-
|
|
641
|
-
编码工具函数: encodeHTML (HTML 实体编码), encodeSafe (null 安全的字符串转换), encodeURIExtra (URI 编码), encodeQ (引号编码)
|
|
642
|
-
|
|
643
|
-
EventDelegator — DOM 事件委托
|
|
644
|
-
|
|
645
|
-
所有 DOM 事件委托到 document.body, 通过事件冒泡机制捕获. 每个 View 的 v-event 属性在编译时编码为 viewId\x1ehandlerName(params) 格式, 运行时由 EventDelegator 解析并分发
|
|
646
|
-
|
|
647
|
-
核心机制:
|
|
648
|
-
|
|
649
|
-
- `bind(eventType, selector?)`: 注册事件类型到 document.body, 选择器事件使用事件委托匹配; 同一事件类型只注册一次全局监听器
|
|
650
|
-
- `domEventProcessor(event)`: 事件处理入口, 从目标元素向上遍历 DOM 树查找 v-event 属性, 解析得到 View ID 与处理器名称; 解析使用 EVENT_METHOD_REGEX 正则, 捕获 Frame ID (可选), handler 名称, 参数字符串
|
|
651
|
-
- `findFrameInfo(element)`: View 边界检测, 从目标元素向上查找, 当遇到已绑定的 Frame 元素时停止, 确保事件不会跨 View 传播. 通过 rangeFrameId 属性标记 View 边界, 事件冒泡到边界时停止搜索
|
|
652
|
-
- `clearRangeEvents(frameId)`: 清除指定 View 范围内的事件标记
|
|
653
|
-
|
|
654
|
-
事件信息缓存: 解析后的事件信息存入 Cache 缓存, 避免重复解析属性值; Cache 使用 SPLITTER 前缀键实现命名空间隔离
|
|
655
|
-
|
|
656
|
-
选择器事件: 方法名中 $ 前缀标识选择器事件 (例如 `$menu<hover>`), 编译时将选择器名称编码到 eventSelectorMap, 运行时通过 `element.matches(selector)` API 匹配, 支持 CSS 选择器语法
|
|
657
|
-
|
|
658
|
-
Range 事件: 通过 lark-view-key 属性标记 View 边界, 事件处理时检查 rangeFrameId, 确保只在对应 View 的 DOM 范围内查找事件处理器
|
|
659
|
-
|
|
660
|
-
Service + Bag — 接口请求管理
|
|
661
|
-
|
|
662
|
-
Service 提供接口请求的统一管理, 支持缓存、去重与队列
|
|
608
|
+
The framework automatically creates a child Frame, mounts the registered View class, and manages its lifecycle. The `v-lark` attribute value must match a path registered via `registerViewClass`.
|
|
663
609
|
|
|
664
|
-
|
|
610
|
+
### Updater vs State vs Store Data Flow
|
|
665
611
|
|
|
666
|
-
|
|
667
|
-
- Service.add(attrs): 注册接口元数据, 包含 name (接口名), url (请求地址), cache (缓存时间, 毫秒, 0 表示不缓存), before (前置钩子), after (后置钩子), cleans (销毁时需要清理的缓存键)
|
|
668
|
-
- service.all(attrs, done): 批量请求, 优先使用缓存, 全部完成后调用 done(errors, bag1, bag2, ...)
|
|
669
|
-
- service.one(attrs, done): 批量请求, 每个完成时立即回调 done(error, bag, isLast, index)
|
|
670
|
-
- service.save(attrs, done): 强制请求, 跳过缓存
|
|
671
|
-
- service.enqueue(task) / service.dequeue(): 任务队列, 串行执行异步操作; enqueue 将任务加入队列, dequeue 通过 setTimeout(0) 异步消费队列
|
|
672
|
-
- service.destroy(): 取消所有进行中的请求, 销毁服务实例
|
|
612
|
+
Three patterns for managing view data. When no store is specified, State is the default cross-view state management:
|
|
673
613
|
|
|
674
|
-
|
|
614
|
+
**Updater pattern** (view-local, manual):
|
|
675
615
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
缓存策略: 基于 Cache 模块实现, 每个 Service 子类拥有独立的 bagCache 实例. 缓存命中时检查是否过期 (当前时间 - cacheInfo.time > cache), 过期则删除缓存并重新请求. Service.clear(names) 按接口名批量清理缓存
|
|
679
|
-
|
|
680
|
-
事件: Service 支持静态事件 (begin, done, fail, end) 和实例事件 (通过 on/off/fire), 使用 EventEmitter 实现
|
|
681
|
-
|
|
682
|
-
Compiler — 模板编译器
|
|
683
|
-
|
|
684
|
-
模板编译器将 {{}} 模板语法编译为 JS 函数, 在构建时通过 Vite 插件或 Webpack loader 执行
|
|
685
|
-
|
|
686
|
-
三阶段编译流水线:
|
|
687
|
-
|
|
688
|
-
第一阶段 — 预处理:
|
|
689
|
-
|
|
690
|
-
- protectComments(): 保护 HTML 注释内的模板语法不被误处理; 将注释替换为 **lark_comment_N** 占位符, 编译完成后恢复
|
|
691
|
-
- processViewEvents(): 处理 v-event 属性, 添加 \x1f (VIEW_ID_PLACEHOLDER) 前缀 + \x1e (SPLITTER) 分隔符, 将 JS 对象字面量参数转换为 URL 查询参数格式. 例如: v-click="handlerName({key: 'value'})" 转换为 v-click="\x1f\x1ehandlerName(key=value)"
|
|
692
|
-
|
|
693
|
-
第二阶段 — 语法转换:
|
|
694
|
-
|
|
695
|
-
- convertArtSyntax(): 将 {{}} 模板语法转换为 <% %> 内部格式. 支持的运算符:
|
|
696
|
-
- = (HTML 转义输出): {{=variable}} 转换为 <%=variable%>
|
|
697
|
-
- ! (原始输出): {{!variable}} 转换为 <%!variable%>
|
|
698
|
-
- @ (引用查找): {{@variable}} 转换为 <%@variable%>, 运行时通过 $i() 查找 refData 中的键名
|
|
699
|
-
- : (双向绑定): {{:variable}} 转换为 <%:variable%>, 渲染时与 = 相同
|
|
700
|
-
- each (数组循环): {{each list as item [index]}} 转换为 for 循环, 支持 first/last 辅助变量, 支持解构赋值 {a,b}
|
|
701
|
-
- parse (对象遍历): {{parse obj as val [key]}}
|
|
702
|
-
- for (通用 for 循环): {{for(init;test;update)}}
|
|
703
|
-
- if/else/else if (条件判断)
|
|
704
|
-
- set (变量声明): {{set a = b}}
|
|
705
|
-
- 块级验证: 使用 blockStack 追踪开放的块, 编译结束时若存在未关闭的块则抛出错误
|
|
706
|
-
- addLineMarkers(): 调试模式下, 在每个 {{ 标签前插入 \x1e + lineNo 行号标记
|
|
707
|
-
- extractArtInfo(): 从带行号标记的 {{ }} 块中提取行号与代码
|
|
708
|
-
|
|
709
|
-
第三阶段 — 函数生成:
|
|
710
|
-
|
|
711
|
-
- compileToFunction(): 将 <% %> 内部格式编译为 JS 模板函数. 输出为箭头函数: ($$,$viewId,$$ref,$e,$n,$eu,$i,$eq)=>{...}, 返回渲染后的 HTML 字符串. 函数体使用 $p 作为输出缓冲区, 通过字符串拼接构建 HTML
|
|
712
|
-
- 自动变量提取: extractGlobalVars() 基于 @babel/parser AST 分析, 自动从模板中提取全局变量名. 两遍扫描: 第一遍收集变量声明和函数作用域, 第二遍识别未声明的标识符为全局变量. 排除内置全局 (JS 内置对象, 模板运行时变量, Lark 框架变量). AST 解析失败时回退到正则提取
|
|
713
|
-
- View ID 注入: \x1f (VIEW_ID_PLACEHOLDER) 在函数生成阶段替换为 '+$viewId+', 实现运行时注入 View ID
|
|
714
|
-
- 调试模式: 支持 $expr/$art/$line 行号追踪与 try-catch 错误包装, 错误信息包含原始模板语法和行号
|
|
715
|
-
- 后处理: 清理空字符串拼接 ($p+='';), 优化 $p=''+ 为 $p+=
|
|
716
|
-
|
|
717
|
-
最终输出为 ES 模块, 导出一个函数: function(data, selfId, refData) 返回渲染后的 HTML 字符串
|
|
718
|
-
|
|
719
|
-
Mark/Unmark — 异步回调有效性追踪
|
|
720
|
-
|
|
721
|
-
基于签名的异步回调保护机制
|
|
722
|
-
|
|
723
|
-
mark(host, key) 在 host 对象上维护一个签名计数器 (SPLITTER + $b 键下的 key 属性), 每次调用 mark 递增计数器并返回一个校验函数. 校验函数闭包捕获当前签名值, 调用时检查 host 上的签名是否与闭包捕获值一致, 一致返回 true (仍有效), 不一致返回 false (已失效)
|
|
724
|
-
|
|
725
|
-
unmark(host) 将签名对象设为 0, 并设置 DELETED_KEY ($a) 为 1, 使所有已有校验函数返回 false. 调用时机: View 销毁 (unmountView) 或重新渲染 (viewWrapMethod)
|
|
726
|
-
|
|
727
|
-
典型场景: View 发起异步请求, 请求返回时 View 可能已被销毁或重新渲染. 通过 mark() 在请求发起时创建校验函数, 回调执行前检查有效性, 避免操作已失效的 View
|
|
728
|
-
|
|
729
|
-
Safeguard — 调试保护
|
|
730
|
-
|
|
731
|
-
基于 Proxy 的数据保护工具, 仅在 window.\_\_lark_debug 为 true 且 Proxy 可用时激活
|
|
732
|
-
|
|
733
|
-
功能:
|
|
734
|
-
|
|
735
|
-
- 拦截非法写入: 根级别直接修改数据时抛出错误 (throw new Error), 强制使用 State.set() / updater.set()
|
|
736
|
-
- 追踪数据访问: getter 回调记录属性读取操作, setter 回调记录属性写入路径和值, 辅助定位数据流向问题
|
|
737
|
-
- 递归代理: 嵌套对象与数组自动递归包装为 Proxy, 深层读写同样被拦截; isRoot 参数控制是否为根级对象 (根级对象不递归)
|
|
738
|
-
- Proxy 池缓存: proxiesPool (Map<object, {cacheKey, entity}>) 缓存已创建的 Proxy 实例, 相同对象不重复包装; SAFEGUARD_SENTINEL 属性防止对已代理对象重复代理
|
|
739
|
-
- clearSafeguardCache(): 清空 Proxy 缓存, 用于测试场景
|
|
740
|
-
|
|
741
|
-
Cache — LFU 风格缓存
|
|
742
|
-
|
|
743
|
-
频率与时间双重维度的缓存淘汰策略
|
|
744
|
-
|
|
745
|
-
每个 CacheEntry 记录: originalKey (原始键, 不含 SPLITTER 前缀), value (缓存值), frequency (访问频率), lastTimestamp (最后访问时间)
|
|
746
|
-
|
|
747
|
-
核心方法:
|
|
748
|
-
|
|
749
|
-
- set(key, value): 写入缓存, 内部键为 SPLITTER + key 实现命名空间隔离; 容量超限时触发淘汰
|
|
750
|
-
- get(key): 读取缓存, 命中时递增 frequency 并更新 lastTimestamp
|
|
751
|
-
- del(key): 删除缓存项
|
|
752
|
-
- forEach(callback): 遍历所有缓存项
|
|
753
|
-
|
|
754
|
-
淘汰策略: 当缓存数量超过 maxSize + bufferSize 时, 对所有条目按 (frequency, lastTimestamp) 排序, 淘汰频率最低且时间最旧的条目, 直到数量不超过 maxSize. 支持 onRemove 回调在条目被移除时通知调用方. sortComparator 允许自定义排序比较器
|
|
755
|
-
|
|
756
|
-
EventEmitter — 多播事件发射器
|
|
757
|
-
|
|
758
|
-
支持 on/off/fire 的事件系统
|
|
759
|
-
|
|
760
|
-
核心机制:
|
|
761
|
-
|
|
762
|
-
- on(event, handler): 注册事件监听器, 内部键为 SPLITTER + event, 监听器包含 handler 和 executing 状态
|
|
763
|
-
- off(event, handler?): 移除监听器. 传入 handler 时, 将其替换为 noop 而非从数组中移除 (避免在 fire 遍历过程中修改数组); 不传 handler 时, 删除整个事件的所有监听器
|
|
764
|
-
- fire(event, data?, remove?, lastToFirst?): 触发事件, 执行所有监听器. executing 标志防止在 fire 过程中移除的监听器在下次 fire 时被清理. remove 参数触发后自动移除所有监听器. lastToFirst 控制执行顺序 (默认从先到后)
|
|
765
|
-
- onEventName 约定: 若对象上存在 on + EventName 方法, fire 时自动调用
|
|
766
|
-
|
|
767
|
-
ApplyStyle — CSS 注入
|
|
768
|
-
|
|
769
|
-
动态向文档头部注入 <style> 标签
|
|
770
|
-
|
|
771
|
-
支持两种调用方式: 单条注入 applyStyle(styleId, css) 和批量注入 applyStyle([id1, css1, id2, css2, ...]). 通过 injectedStyleIds Set 去重, 相同 ID 的样式不会重复注入. 每条注入返回一个清理函数, 调用后从 DOM 移除 <style> 标签并从 Set 中删除 ID
|
|
772
|
-
|
|
773
|
-
变更分发机制
|
|
774
|
-
|
|
775
|
-
Lark 的变更分发由 Dispatcher 模块 (内嵌在 framework.ts) 实现, 当路由或 State 发生变更时, 沿 Frame 树向下查找并通知匹配的 View 重新渲染
|
|
776
|
-
|
|
777
|
-
```
|
|
778
|
-
路由变更 / State 变更
|
|
779
|
-
└── dispatcherNotifyChange(data)
|
|
780
|
-
├── 路由 view 变更 → rootFrame.mountView(newViewPath)
|
|
781
|
-
└── 参数/状态变更 → dispatcherUpdate(rootFrame, changedKeys)
|
|
782
|
-
├── 检查 View 是否观察了变更键
|
|
783
|
-
│ ├── observeLocation → viewIsObserveChanged()
|
|
784
|
-
│ └── observeState → stateIsObserveChanged()
|
|
785
|
-
├── 匹配则触发 View.render()
|
|
786
|
-
└── 递归子 Frame → dispatcherUpdate(childFrame, changedKeys)
|
|
787
|
-
```
|
|
788
|
-
|
|
789
|
-
dispatcherUpdate 通过 dispatcherUpdateTag 防止同一更新周期内重复处理同一 Frame; View 的 signature <= 1 时跳过更新 (尚未完成首次渲染); 支持 render() 返回 Promise 的异步场景, 异步渲染完成后递归处理子 Frame
|
|
790
|
-
|
|
791
|
-
渲染管线
|
|
792
|
-
|
|
793
|
-
```
|
|
794
|
-
updater.digest()
|
|
795
|
-
├── 调用模板函数(template) → 生成 HTML 字符串
|
|
796
|
-
├── 虚拟文档解析 HTML → 新 DOM 节点
|
|
797
|
-
├── VDOM diff (新旧节点对比)
|
|
798
|
-
│ ├── 属性差异 → setAttribute / 直接属性赋值
|
|
799
|
-
│ ├── 子节点差异 → appendChild / removeChild / replaceChild / insertBefore
|
|
800
|
-
│ └── 特殊元素 → 直接同步 value / checked / selected
|
|
801
|
-
├── applyIdUpdates() — 批量更新元素 ID
|
|
802
|
-
├── applyVdomOps() — 批量执行 DOM 操作
|
|
803
|
-
└── Frame 处理 lark-view 子视图 → mountZone
|
|
616
|
+
```typescript
|
|
617
|
+
this.updater.set({ count: newCount }).digest();
|
|
804
618
|
```
|
|
805
619
|
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
虚拟 DOM 与 diff 算法
|
|
809
|
-
|
|
810
|
-
React 使用完整的虚拟 DOM 树表示 UI 结构, 通过 JSX 创建 ReactElement 对象树, diff 算法比较新旧两棵虚拟 DOM 树, 生成最小变更集. React 的 diff 算法基于三个假设: 1) 跨层级的节点移动极少, 2) 不同类型的元素产生不同的树, 3) key 属性标识节点的稳定性
|
|
811
|
-
|
|
812
|
-
Lark 不使用完整的虚拟 DOM 树表示, 而是采用增量式 diff: 新模板渲染生成 HTML 字符串, 通过 document.implementation.createHTMLDocument() 解析为真实 DOM 节点, 然后直接与现有 DOM 树进行 diff. 这意味着 Lark 不需要维护一棵独立的虚拟 DOM 树, 内存占用更低. diff 过程中, Lark 的 vdomSetChildNodes 实现了基于 key 的节点匹配 (通过 id, lark-key, lark-view 属性), 类似于 React 的 key 机制, 但匹配策略更简单: 通过哈希表 (keyedNodes) 一次性构建旧节点的 key 映射, 遍历新节点时查找匹配
|
|
813
|
-
|
|
814
|
-
Lark 的 diff 产生四种操作码 (appendChild=1, removeChild=2, replaceChild=4, insertBefore=8), 批量执行; React 的 diff 产生 patch 列表, 通过 React Reconciler 调度执行. Lark 的 diff 是同步的、全量的 (针对单个 View), React 的 diff 可以通过 shouldComponentUpdate / React.memo 等机制跳过子树
|
|
815
|
-
|
|
816
|
-
Fiber 架构
|
|
817
|
-
|
|
818
|
-
React Fiber 是 React 16 引入的协调引擎, 将渲染工作拆分为小单元 (Fiber 节点), 支持可中断的异步渲染. 每个 Fiber 节点包含 type, key, child, sibling, return 等指针, 构成链表结构, 替代了之前的递归式栈帧调用. Fiber 架构的核心优势: 1) 时间切片, 可暂停渲染以响应用户交互; 2) 优先级调度, 高优先级更新 (如用户输入) 可以中断低优先级更新 (如数据请求); 3) 可恢复的渲染, 暂停后可从断点继续
|
|
819
|
-
|
|
820
|
-
Lark 的 Frame 树与 Fiber 树有相似之处: 两者都与 DOM 树同构, 都管理组件/视图的生命周期. 但 Lark 的 Frame 不实现时间切片和可中断渲染, diff 和 DOM 更新是同步执行的. Lark 的 Frame 更侧重于生命周期管理 (mountView/unmountView) 和事件边界检测 (findFrameInfo), 而非渲染调度. Lark 的任务调度能力来自 Framework.task(), 它使用 scheduler.postTask / requestIdleCallback / setTimeout 实现时间切片, 但这只用于通用任务队列, 不用于渲染过程本身
|
|
821
|
-
|
|
822
|
-
渲染模型
|
|
823
|
-
|
|
824
|
-
React 使用不可变数据模型: setState 生成新的状态对象, 触发组件重渲染, 组件的 render 函数返回新的 ReactElement 树. Lark 使用可变数据模型: updater.set() 直接修改内部数据, updater.digest() 触发模板函数重新执行, 生成新的 HTML 字符串. React 组件通过 props 和 state 驱动渲染, Lark View 通过 updater.refData 和 Store.observe 驱动渲染
|
|
825
|
-
|
|
826
|
-
事件系统
|
|
827
|
-
|
|
828
|
-
React 使用合成事件 (SyntheticEvent), 在 React 17+ 将事件委托到根容器节点, 事件对象是原生事件的跨浏览器包装. Lark 使用原生事件委托到 document.body, 通过 v-event 属性编码 View ID 和 handler 名称, EventDelegator 解析分发. Lark 的方案更轻量, 不需要额外的事件对象池和兼容层
|
|
829
|
-
|
|
830
|
-
对比 Vue3 框架
|
|
831
|
-
|
|
832
|
-
模板编译
|
|
620
|
+
**State pattern** (default cross-view, key-ref counting):
|
|
833
621
|
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
Vue3 使用 Proxy 实现响应式系统, 核心是 reactive() 和 ref() 两个 API. reactive() 接收对象并返回 Proxy 包装, get 拦截器通过 track(target, key) 收集依赖, set 拦截器通过 trigger(target, key) 触发更新. ref() 包装原始值, 通过对象访问器 (get/set) 实现响应式. Vue3 的依赖追踪使用全局 WeakMap(target -> Map(key -> Set(effect))) 结构, effect 函数执行时自动收集依赖. Vue3 还提供 computed (计算属性, 惰性求值+缓存), watch/watchEffect (副作用), shallowReactive/shallowRef (浅层响应式) 等衍生 API
|
|
845
|
-
|
|
846
|
-
Lark 的 Store 模块同样基于 Proxy 实现响应式, 核心是 createState() 和 defineStore(). createState() 接收普通对象, 返回 Proxy 包装的响应式状态, get 拦截器执行 track(depKey, effect), set 拦截器执行 trigger(depKey). 依赖追踪使用全局 GlobalDeps Map (key 为 SPLITTER + depKey, value 为 effect Set). 两者的依赖追踪机制本质相同, 区别在于:
|
|
847
|
-
|
|
848
|
-
1. 依赖粒度: Vue3 以 target + key 为粒度 (每个对象的每个属性独立追踪), Lark 以 depKey 为粒度 (扁平化的全局 Map). Vue3 的 WeakMap 结构允许对象被 GC 时自动清理依赖映射, Lark 的全局 Map 需要手动清理 (通过 Store 生命周期管理)
|
|
849
|
-
2. 效果调度: Vue3 的 effect 通过 scheduler 调度执行, 支持异步批量更新 (nextTick); Lark 的 Store 的 observe 直接绑定 View 的 updater.digest, 状态变更后同步触发渲染, 或通过 ObservePayload.lazy 延迟到下一帧
|
|
850
|
-
3. 计算属性: Vue3 提供 computed() 实现惰性求值和缓存; Lark 通过 ObservePayload.transform 实现状态派生, 但不内置缓存
|
|
851
|
-
4. 浅层响应式: Vue3 提供 shallowReactive/shallowRef; Lark 提供 shallowSet(), 只有顶层 key 变化触发通知
|
|
852
|
-
5. 批量更新: Vue3 通过 nextTick 批量处理同一事件循环内的多次状态变更; Lark 通过 lazySet() 批量设置多个属性但只触发一次通知
|
|
853
|
-
6. 原始值响应式: Vue3 使用 ref() 包装原始值; Lark 使用 cell() 创建独立响应式数据单元, 本质是 createState 的轻量封装
|
|
854
|
-
7. 多平台: Lark 的 Store 设计了 LarkStore / ReactStore / NodeStore 三种适配器, ReactStore 使用 freezeData 生成不可变快照适配 React, NodeStore 适用于 Node.js 环境; Vue3 没有类似的多平台设计, 其响应式系统与 Vue 组件生命周期绑定
|
|
855
|
-
|
|
856
|
-
组件模型
|
|
622
|
+
```typescript
|
|
623
|
+
// Write: set data and digest to notify
|
|
624
|
+
State.set({ count: newCount }).digest();
|
|
625
|
+
|
|
626
|
+
// Read: in view's assign(), pull from State
|
|
627
|
+
assign() {
|
|
628
|
+
this.updater.snapshot();
|
|
629
|
+
this.updater.set({ count: State.get("count") });
|
|
630
|
+
return this.updater.altered();
|
|
631
|
+
}
|
|
857
632
|
|
|
858
|
-
|
|
633
|
+
// Observe: listen to State "changed" event to trigger re-render
|
|
634
|
+
State.on("changed", (e) => {
|
|
635
|
+
if (e.keys.count) this.assign(); // Re-assign if count changed
|
|
636
|
+
});
|
|
859
637
|
|
|
860
|
-
|
|
638
|
+
// Cleanup: use State.clean() in mixins to auto-delete keys on view destroy
|
|
639
|
+
export default View.extend({
|
|
640
|
+
mixins: [State.clean("count")],
|
|
641
|
+
});
|
|
642
|
+
```
|
|
861
643
|
|
|
862
|
-
|
|
644
|
+
Key differences from Store:
|
|
863
645
|
|
|
864
|
-
Store
|
|
646
|
+
- State uses `State.set()` + `State.digest()` (manual notification), Store uses direct property assignment (automatic reactivity via Proxy)
|
|
647
|
+
- State keys are auto-deleted when reference count reaches 0 (via `State.clean()` mixin), Store persists until `store.$destroyFn()` is called
|
|
648
|
+
- State requires manual event listening (`State.on("changed", ...)`), Store provides `store.observe()` with auto-cleanup
|
|
865
649
|
|
|
866
|
-
|
|
867
|
-
| --------------------------------------- | ----------------------------------------- |
|
|
868
|
-
| defineStore(name, creator, config?) | 声明式定义 store |
|
|
869
|
-
| useStore(view?) | 获取 store 代理, 传入 view 时绑定生命周期 |
|
|
870
|
-
| store.observe(view, keys, defCallback?) | 监听状态变更 |
|
|
871
|
-
| lazySet(target, data) | 批量更新, 只触发一次通知 |
|
|
872
|
-
| shallowSet(target, key, data) | 创建浅层响应式状态 |
|
|
873
|
-
| cell(data) | 创建独立响应式数据单元 |
|
|
874
|
-
| observeCell(state, cb, immediate?) | 监听 cell 变更 |
|
|
875
|
-
| multi(useStore) | 创建多实例 store |
|
|
876
|
-
| getStore(name) | 按名称获取 store 实例 |
|
|
877
|
-
| delStore(name) | 删除 store |
|
|
878
|
-
| cloneStore(name, useStore, config?) | 克隆 store |
|
|
879
|
-
| isStoreActive(name) | 检查 store 是否处于 ACTIVE 状态 |
|
|
880
|
-
| isState(target) | 检查对象是否为响应式状态 |
|
|
881
|
-
| createState(data, config?) | 创建响应式状态 |
|
|
650
|
+
**Store pattern** (cross-view, reactive, recommended):
|
|
882
651
|
|
|
883
|
-
|
|
652
|
+
```typescript
|
|
653
|
+
const store = useCountStore(this);
|
|
654
|
+
store.observe(this, ["count"]);
|
|
655
|
+
// Later, from any view:
|
|
656
|
+
useCountStore().increment(); // Automatically triggers re-render in all observing views
|
|
657
|
+
```
|
|
884
658
|
|
|
885
|
-
|
|
659
|
+
### Frame Tree Events
|
|
886
660
|
|
|
887
|
-
|
|
661
|
+
Frame fires lifecycle events:
|
|
888
662
|
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
| {{@variable}} | 引用查找 (用于对象/数组) |
|
|
894
|
-
| {{:variable}} | 双向绑定 |
|
|
895
|
-
| {{each list as item}} | 数组循环 |
|
|
896
|
-
| {{each list as item idx}} | 数组循环 (带索引) |
|
|
897
|
-
| {{each list as item idx last}} | 数组循环 (带索引和 last 变量) |
|
|
898
|
-
| {{parse obj as val key}} | 对象遍历 |
|
|
899
|
-
| {{for(init;test;update)}} | 通用 for 循环 |
|
|
900
|
-
| {{if condition}} | 条件判断 |
|
|
901
|
-
| {{else if condition}} | else-if 条件 |
|
|
902
|
-
| {{else}} | else 分支 |
|
|
903
|
-
| {{/if}} / {{/each}} / {{/parse}} / {{/for}} | 关闭块 |
|
|
904
|
-
| {{set var = expr}} | 变量声明 |
|
|
663
|
+
- `add` -- new Frame created
|
|
664
|
+
- `remove` -- Frame removed
|
|
665
|
+
- `created` -- all child Frames rendered
|
|
666
|
+
- `alter` -- child Frame content changed
|
|
905
667
|
|
|
906
|
-
|
|
668
|
+
```typescript
|
|
669
|
+
Frame.on("add", ({ frame }) => {
|
|
670
|
+
/* ... */
|
|
671
|
+
});
|
|
672
|
+
```
|
|
907
673
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
|
911
|
-
|
|
|
912
|
-
|
|
|
913
|
-
|
|
|
914
|
-
|
|
|
915
|
-
|
|
|
916
|
-
|
|
|
917
|
-
|
|
|
918
|
-
|
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
674
|
+
### Vite vs Webpack Comparison
|
|
675
|
+
|
|
676
|
+
| Feature | Vite (larkMvcPlugin) | Webpack (larkMvcLoader) |
|
|
677
|
+
| -------------------- | -------------------------------------------------- | -------------------------------------------------- |
|
|
678
|
+
| Import path | `@lark.js/mvc/vite` | `@lark.js/mvc/webpack` |
|
|
679
|
+
| Type | Vite Plugin (resolveId + load hooks) | Webpack Loader (standard loader interface) |
|
|
680
|
+
| Configuration | `plugins: [larkMvcPlugin()]` | `module.rules` with loader rule |
|
|
681
|
+
| Debug mode | `larkMvcPlugin({ debug: true })` | `options: { debug: true }` |
|
|
682
|
+
| HTML entry exclusion | Not needed (Vite handles index.html separately) | `exclude: /index\.html$/` |
|
|
683
|
+
| Dev server | Vite dev server (fast HMR) | webpack-dev-server (standard HMR) |
|
|
684
|
+
| Template compilation | Same pipeline: extractGlobalVars + compileTemplate | Same pipeline: extractGlobalVars + compileTemplate |
|
|
685
|
+
|
|
686
|
+
## Common Pitfalls
|
|
687
|
+
|
|
688
|
+
1. **boot.ts must be in `src/`** -- The index.html entry point references `/src/boot.ts`, not `/boot.ts`
|
|
689
|
+
2. **registerViewClass before boot** -- All view classes must be registered before calling `Framework.boot()`
|
|
690
|
+
3. **Template imports require larkMvcPlugin or larkMvcLoader** -- `.html` imports only work because the Vite plugin or Webpack loader compiles them at build time
|
|
691
|
+
4. **Use State.set/digest, not direct mutation** -- Directly modifying State data bypasses change detection. In debug mode (`window.__lark_Debug = true`), safeguard Proxy warns about this with a 500ms delay
|
|
692
|
+
5. **observe requires view binding for auto-cleanup** -- Always pass `this` to `store.observe(this, ...)` so the observation is cleaned up when the view is destroyed. Without view binding, you must manually call the returned unobserve function
|
|
693
|
+
6. **Event method names use `<>` not `()`** -- The pattern is `name<click>`, not `name(click)`
|
|
694
|
+
7. **assign must return altered()** -- The `assign` method should call `this.updater.snapshot()` at the start and return `this.updater.altered()` at the end to enable incremental updates
|
|
695
|
+
8. **Do not modify view.signature** -- It is managed internally; setting it to 0 destroys the view. The wrapped render() increments signature automatically
|
|
696
|
+
9. **v-lark containers are replaced** -- The content inside a `v-lark` element is replaced by the child view's rendered output
|
|
697
|
+
10. **Webpack must exclude index.html** -- The larkMvcLoader rule must exclude `index.html` so HtmlWebpackPlugin can process it instead
|
|
698
|
+
11. **Webpack uses loader object, not string name** -- Import `larkMvcLoader` from `@lark.js/mvc/webpack` and use it directly as `loader: larkMvcLoader`, not as a string like `"larkMvcLoader"`
|
|
699
|
+
12. **Store reads return cloned data** — `useStore(view).count` returns a deep-cloned copy, not the reactive Proxy. Mutating the returned value does NOT trigger reactivity. Only writes through `store.key = value` are reactive
|
|
700
|
+
13. **forOf requires "as" keyword** -- `{{forOf list item}}` is invalid. Must use `{{forOf list as item}}`
|
|
701
|
+
14. **Inner observe deduplication** -- `store.observe(undefined, keys, callback)` uses a deduplication flag based on `key + observeKeys.join("-") + cb.toString()`. The same inner observe with identical key/callback won't register twice
|
|
702
|
+
15. **View.wrapAsync is signature-based** -- The callback only executes if `view.signature` hasn't changed since `wrapAsync` was called. Re-rendering or destroying the view increments signature, invalidating the callback
|
|
703
|
+
16. **Frame object pooling** -- Destroyed Frame objects are cached and reused. Don't hold references to Frame instances after unmountView() as they may be reinitialized for a different view
|
|
704
|
+
17. **Updater re-digest support** -- Calling `updater.digest()` during an ongoing digest is supported via an internal queue. The sentinel pattern (`null` entry) marks the digest boundary, and callbacks are executed after all digest cycles complete
|
|
705
|
+
18. **Store creator runs once** -- The creator function passed to `defineStore` runs once at definition time. Store state persists across view mounts/unmounts unless `store.$destroyFn()` is explicitly called
|
|
706
|
+
19. **State is the default when no store is specified** -- Without `defineStore`, cross-view data flows through `State.set()` / `State.get()` / `State.digest()`. Always use `State.set()` + `State.digest()` to update data (never mutate `State.get()` directly). Use `State.clean()` in mixins to auto-delete keys on view destroy; otherwise, State data persists globally and is never garbage collected
|
|
707
|
+
|
|
708
|
+
## References
|
|
709
|
+
|
|
710
|
+
For detailed API signatures and template syntax, consult the reference files:
|
|
711
|
+
|
|
712
|
+
- `references/api-reference.md` -- Complete API reference for all Lark modules
|
|
713
|
+
- `references/template-syntax.md` -- Template syntax, operators, control flow, and event binding
|