@lark.js/mvc 0.0.1 → 0.0.3
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 +945 -1
- package/dist/{chunk-DZUOIUWX.js → chunk-Y72BUONO.js} +38 -39
- package/dist/index.cjs +236 -125
- package/dist/index.d.cts +89 -29
- package/dist/index.d.ts +89 -29
- package/dist/index.js +231 -119
- package/dist/vite.cjs +47 -48
- package/dist/vite.d.cts +5 -5
- package/dist/vite.d.ts +5 -5
- package/dist/vite.js +9 -9
- package/dist/webpack.cjs +42 -43
- package/dist/webpack.d.cts +2 -2
- package/dist/webpack.d.ts +2 -2
- package/dist/webpack.js +4 -4
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -1 +1,945 @@
|
|
|
1
|
-
# Lark
|
|
1
|
+
# Lark Framework
|
|
2
|
+
|
|
3
|
+
Lark 是基于 TypeScript 的 MVC 前端框架, 核心特点包括: 虚拟 DOM 差量更新、hash 路由、事件委托、模板编译、Proxy 响应式状态管理, 提供声明式数据绑定的 SPA 开发体验
|
|
4
|
+
|
|
5
|
+
架构总览
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Framework (主入口)
|
|
9
|
+
├── Router (hash 路由, 两阶段变更确认)
|
|
10
|
+
├── State (跨视图 observable 数据, 键引用计数自动清理)
|
|
11
|
+
├── Store (Proxy 深度响应式状态管理, 多平台适配)
|
|
12
|
+
├── Frame (视图生命周期树, 对象池复用)
|
|
13
|
+
│ └── View (视图实例, 事件方法绑定)
|
|
14
|
+
│ └── Updater (数据绑定 + VDOM diff)
|
|
15
|
+
├── EventDelegator (DOM 事件委托至 document.body)
|
|
16
|
+
├── Service + Bag (接口请求管理, 缓存/去重/队列)
|
|
17
|
+
├── Compiler (模板编译, 构建时将 HTML 模板编译为 JS 函数)
|
|
18
|
+
├── Mark/Unmark (异步回调有效性追踪)
|
|
19
|
+
├── Safeguard (Proxy 调试保护)
|
|
20
|
+
├── Cache (LFU 风格缓存, 频率+时间双重淘汰)
|
|
21
|
+
├── EventEmitter (多播事件发射器)
|
|
22
|
+
└── ApplyStyle (CSS 动态注入)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Lark 框架启动时, Framework.boot() 初始化路由器、状态管理器、事件委托器, 创建根 Frame 节点, 绑定 hashchange 事件; 路由改变与状态改变通过统一的派发器 (Dispatcher) 沿 Frame 树向下通知各个 View, 触发条件匹配的视图重新渲染
|
|
26
|
+
|
|
27
|
+
安装
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
pnpm add @lark.js/mvc
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
快速上手
|
|
34
|
+
|
|
35
|
+
1. 配置构建工具
|
|
36
|
+
|
|
37
|
+
Vite 项目在 vite.config.ts 中导入 larkMvcPlugin 插件, 该插件会自动将 .html 模板文件编译为 JS 函数
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import { larkMvcPlugin } from "@lark/framework/vite";
|
|
41
|
+
|
|
42
|
+
export default defineConfig({
|
|
43
|
+
plugins: [larkMvcPlugin()],
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Webpack 项目在 webpack.config.mjs 中配置 loader
|
|
48
|
+
|
|
49
|
+
```javascript
|
|
50
|
+
import { resolve } from "path";
|
|
51
|
+
import { larkMvcLoader } from "@lark/framework/webpack";
|
|
52
|
+
|
|
53
|
+
export default {
|
|
54
|
+
module: {
|
|
55
|
+
rules: [
|
|
56
|
+
{
|
|
57
|
+
test: /\.html$/,
|
|
58
|
+
loader: larkMvcLoader,
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
2. 定义 Store
|
|
66
|
+
|
|
67
|
+
通过 defineStore 创建响应式状态管理, creator 函数返回的对象中, 非函数属性成为响应式 state, 函数属性成为 handler
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
import { defineStore } from "@lark.js/mvc";
|
|
71
|
+
|
|
72
|
+
export interface CountStore {
|
|
73
|
+
count: number;
|
|
74
|
+
step: number;
|
|
75
|
+
history: string[];
|
|
76
|
+
increment: () => void;
|
|
77
|
+
decrement: () => void;
|
|
78
|
+
reset: () => void;
|
|
79
|
+
setStep: (val: number) => void;
|
|
80
|
+
clearHistory: () => void;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const useCountStore = defineStore<CountStore>("count", (store) => {
|
|
84
|
+
return {
|
|
85
|
+
count: 0,
|
|
86
|
+
step: 1,
|
|
87
|
+
history: [] as string[],
|
|
88
|
+
|
|
89
|
+
increment() {
|
|
90
|
+
store.count = store.count + store.step;
|
|
91
|
+
store.history = [...store.history, `+${store.step} -> ${store.count}`];
|
|
92
|
+
},
|
|
93
|
+
decrement() {
|
|
94
|
+
store.count = store.count - store.step;
|
|
95
|
+
store.history = [...store.history, `-${store.step} -> ${store.count}`];
|
|
96
|
+
},
|
|
97
|
+
reset() {
|
|
98
|
+
store.count = 0;
|
|
99
|
+
store.history = [...store.history, "Reset -> 0"];
|
|
100
|
+
},
|
|
101
|
+
setStep(val: number) {
|
|
102
|
+
store.step = val;
|
|
103
|
+
},
|
|
104
|
+
clearHistory() {
|
|
105
|
+
store.history = [];
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
export default useCountStore;
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
creator 函数接收两个参数: store 是 Proxy 代理对象, 可以读写 state, 也可以调用 observe 注册 state 改变时的 callback; apis 可以解构得到 lazySet 和 shallowSet 两个工具方法
|
|
114
|
+
|
|
115
|
+
3. 创建 View
|
|
116
|
+
|
|
117
|
+
View.extend(props) 创建视图子类, 模板属性 template 引用编译后的 HTML 模板函数, init 方法中通过 useStore(this) 获取 store 实例并绑定视图生命周期, store.observe(this, keys) 声明观察的状态键
|
|
118
|
+
|
|
119
|
+
事件方法的方法名格式为 name<eventType>, 例如 increment<click> 对应模板中的 @click="increment()", 编译时自动编码 View ID, 运行时由 EventDelegator 解析分发
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
import { Router } from "@lark.js/mvc";
|
|
123
|
+
import View from "../view";
|
|
124
|
+
import template from "./counter.html";
|
|
125
|
+
import useCountStore from "../store/count";
|
|
126
|
+
|
|
127
|
+
export default View.extend({
|
|
128
|
+
// const template: (data: unknown, selfId: string, refData: unknown) => string;
|
|
129
|
+
template,
|
|
130
|
+
|
|
131
|
+
init() {
|
|
132
|
+
const store = useCountStore(this);
|
|
133
|
+
store.observe(this, ["count", "step", "history"]);
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
"increment<click>"() {
|
|
137
|
+
useCountStore().increment();
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
"decrement<click>"() {
|
|
141
|
+
useCountStore().decrement();
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
"reset<click>"() {
|
|
145
|
+
useCountStore().reset();
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
"stepChange<change>"(e: Event) {
|
|
149
|
+
const target = e.target as HTMLInputElement;
|
|
150
|
+
const newStep = parseInt(target.value) || 1;
|
|
151
|
+
useCountStore().setStep(newStep);
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
4. 创建模板
|
|
157
|
+
|
|
158
|
+
模板使用 {{}} 语法, = 表示 HTML 转义输出, @click 等属性绑定事件, 编译时自动将 View ID 和参数编码到事件属性值中
|
|
159
|
+
|
|
160
|
+
```html
|
|
161
|
+
<div>
|
|
162
|
+
<div>{{=count}}</div>
|
|
163
|
+
<input type="number" value="{{=step}}" @change="stepChange()" />
|
|
164
|
+
<button @click="decrement()">-{{=step}}</button>
|
|
165
|
+
<button @click="reset()">Reset</button>
|
|
166
|
+
<button @click="increment()">+{{=step}}</button>
|
|
167
|
+
|
|
168
|
+
{{if history.length > 0}}
|
|
169
|
+
<ul>
|
|
170
|
+
{{each history as record}}
|
|
171
|
+
<li>{{=record}}</li>
|
|
172
|
+
{{/each}}
|
|
173
|
+
</ul>
|
|
174
|
+
{{else}}
|
|
175
|
+
<p>No history</p>
|
|
176
|
+
{{/if}}
|
|
177
|
+
</div>
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
5. 启动应用
|
|
181
|
+
|
|
182
|
+
通过 registerViewClass 注册 View 类, 配置 routes 路由映射, 调用 Framework.boot(config) 启动框架
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
import { Framework, View, registerViewClass } from "@lark.js/mvc";
|
|
186
|
+
import HomeView from "./src/views/home";
|
|
187
|
+
import CounterView from "./src/views/counter";
|
|
188
|
+
|
|
189
|
+
registerViewClass("home", HomeView as typeof View);
|
|
190
|
+
registerViewClass("counter", CounterView as typeof View);
|
|
191
|
+
|
|
192
|
+
Framework.boot({
|
|
193
|
+
rootId: "app",
|
|
194
|
+
defaultPath: "/home",
|
|
195
|
+
defaultView: "home",
|
|
196
|
+
routes: {
|
|
197
|
+
"/home": "home",
|
|
198
|
+
"/counter": "counter",
|
|
199
|
+
},
|
|
200
|
+
unmatchedView: "404",
|
|
201
|
+
error(e: Error) {
|
|
202
|
+
console.error("Lark application error:", e);
|
|
203
|
+
},
|
|
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
|
+
}
|
|
272
|
+
|
|
273
|
+
Framework.task = callFunction;
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
- 入队: callFunction(fn, args, ctx) 不是立刻执行, 而是将 fn、ctx、args 扁平 push 到 callList
|
|
277
|
+
- 异步启动: 通过 setTimeout(startCall) 推迟到下一个事件循环开始执行
|
|
278
|
+
- 分片执行: startCall 在 while 循环中, 依次取出函数执行, 每执行一个函数都会检查一次耗时
|
|
279
|
+
|
|
280
|
+
这是早期前端的长任务分片实践,用于
|
|
281
|
+
|
|
282
|
+
- 避免一次性执行大量回调, 例如大量 View 渲染完成后的通知导致页面卡顿
|
|
283
|
+
- 保持 60fps (每帧约 16ms, 48ms 约占 3 帧) 的用户交互响应
|
|
284
|
+
- 与 requestIdleCallback 的思路类似, 但 callFunction 设计时浏览器可能还没有该 API
|
|
285
|
+
|
|
286
|
+
优化
|
|
287
|
+
|
|
288
|
+
任务调度: `Framework.task(fn, args, context)` 提供分片执行能力, 调度优先级为
|
|
289
|
+
|
|
290
|
+
- `scheduler.postTask('background')` Chrome 94+
|
|
291
|
+
- `requestIdleCallback` Chrome 47+
|
|
292
|
+
- `setTimeout(0)` 通用 fallback 方案
|
|
293
|
+
|
|
294
|
+
使用 `requestIdleCallback` 时依据 `deadline.timeRemaining()` 自适应确定每片执行数量, 回退到 setTimeout 时使用固定 48ms 时间片
|
|
295
|
+
|
|
296
|
+
| | callFunction | task |
|
|
297
|
+
| -------------- | -------------------------------------- | ----------------------------------------------------- |
|
|
298
|
+
| 调度 API | setTimeout | scheduler.postTask → requestIdleCallback → setTimeout |
|
|
299
|
+
| 时间切片 | 固定 48ms | 自适应 deadline.timeRemaining() + 48ms fallback |
|
|
300
|
+
| 接口签名 | 扁平数组 [fn, ctx, args, ...] 逐个消费 | 相同保持兼容性 |
|
|
301
|
+
| 让出主线程时机 | 仅超时 | |
|
|
302
|
+
|
|
303
|
+
Router — hash 路由器
|
|
304
|
+
|
|
305
|
+
Router hash 路由器基于 location.hash, 支持 #! 前缀的 hashbang 模式
|
|
306
|
+
|
|
307
|
+
路由变更使用两阶段确认
|
|
308
|
+
|
|
309
|
+
1. change 阶段: 用户可以调用 `prevent()` 取消路由跳转, 或调用 `reject()` 回退到旧 URL, 类似 beforeunload
|
|
310
|
+
2. changed 阶段: 路由改变完成后触发, 通知 Dispatcher 派发器更新视图
|
|
311
|
+
|
|
312
|
+
核心 API:
|
|
313
|
+
|
|
314
|
+
- `Router.parse(href?)`: 将 href 解析为 Location 对象, 包含 path, params, query, hash 等字段; 解析结果被 hrefCache 缓存, 相同 href 不重复解析
|
|
315
|
+
- `Router.diff()`: 计算新旧 Location 的差异 LocationDiff 对象, 包括参数键值对变化和路径/视图变化, 计算结果被 changedCache 缓存; 如果检测到变更且并且不是 silent 静默模式, 则触发 changed 事件
|
|
316
|
+
- `Router.to(pathOrParams, params?, replace?, silent?)`: 编程式导航, 支持字符串路径或参数对象形式; 支持历史记录替换与 silent 静默更新
|
|
317
|
+
- `Router.join(...paths)`: 拼接路径, 处理 ./ , ../ , // 等路径规范
|
|
318
|
+
|
|
319
|
+
Location 对象结构
|
|
320
|
+
|
|
321
|
+
```ts
|
|
322
|
+
export interface Location {
|
|
323
|
+
/** 完整 URL */
|
|
324
|
+
href: string;
|
|
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
|
+
```
|
|
343
|
+
|
|
344
|
+
LocationDiff 对象结构
|
|
345
|
+
|
|
346
|
+
```ts
|
|
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
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
beforeunload 事件支持: 监听 window 的 beforeunload 事件, 若监听器设置了 msg 字段, 浏览器弹出离开确认对话框
|
|
362
|
+
|
|
363
|
+
调试模式下, Location.params 和 LocationDiff 会被 safeguard() Proxy 包装, 拦截非法写入
|
|
364
|
+
|
|
365
|
+
State — 跨视图 observable 数据
|
|
366
|
+
|
|
367
|
+
State 是全局单例的数据对象, 用于跨视图共享状态; View 通过 `observeState(keys)` 声明观察的键, State 改变时仅通知观察了相关键的视图
|
|
368
|
+
|
|
369
|
+
核心流程:
|
|
370
|
+
|
|
371
|
+
1. `State.get(key)` 读取数据; State.set(data) 写入数据并标记变更键
|
|
372
|
+
2. `State.digest()` 触发变更通知: 将变更键传递给 Dispatcher, Dispatcher 沿 Frame 树查找观察了这些键的 View 并触发重新渲染
|
|
373
|
+
3. `State.diff()` 返回上次 digest 的变更键映射
|
|
374
|
+
|
|
375
|
+
键引用计数: 当 View 调用 observeState(keys) 时, 通过 setupKeysRef 对这些键建立引用计数; 当 View 销毁时, 通过 teardownKeysRef 递减引用计数; 当某个键的引用计数降为 0 时, 自动从 State 中删除该键的数据, 避免内存泄漏
|
|
376
|
+
|
|
377
|
+
调试模式: 当 `window.__lark_Debug` 为 true 时, State 数据会使用 safeguard() Proxy 包装, 拦截非法写入并追踪访问来源
|
|
378
|
+
|
|
379
|
+
Store — 响应式状态管理
|
|
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()`
|
|
392
|
+
|
|
393
|
+
```typescript
|
|
394
|
+
function defineStore<S>(
|
|
395
|
+
name: string,
|
|
396
|
+
creator: (store: InnerStore<S>, apis: { lazySet; shallowSet }) => S,
|
|
397
|
+
// config.platform 指定平台: Platform.Lark (默认), Platform.React, Platform.Node
|
|
398
|
+
config?: StoreConfig,
|
|
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
|
+
}
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
ReactStore
|
|
435
|
+
|
|
436
|
+
- 使用 `freezeData()` 冻结数据, 生成不可变快照, 适配 React 的不可变数据模式
|
|
437
|
+
- 每次状态变更后生成新的快照, 确保引用变化以触发 React 组件重渲染
|
|
438
|
+
|
|
439
|
+
NodeStore: observe 直接执行回调, 不绑定视图生命周期, 适用于 Node.js 环境下的状态管理
|
|
440
|
+
|
|
441
|
+
_lazySet — 批量懒更新_
|
|
442
|
+
|
|
443
|
+
`lazySet(target, data)` 批量设置多个属性值, 不触发任何响应式通知. 内部通过 curLazySetKey 标记正在 lazySet 的属性 key, Proxy set 拦截器检测到匹配时跳过 trigger
|
|
444
|
+
|
|
445
|
+
_shallowSet — 浅层响应式_
|
|
446
|
+
|
|
447
|
+
`shallowSet(target, key, data)` 创建浅层响应式状态: 只有顶层的 key 变化才触发 trigger, 内部嵌套对象的变化不触发
|
|
448
|
+
|
|
449
|
+
_cell / observeCell — 独立的响应式数据_
|
|
450
|
+
|
|
451
|
+
- cell(data) 创建独立的响应式数据
|
|
452
|
+
- observeCell(state, cb, immediate?) 监听 cell 变更
|
|
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", // 对应 v-lark="components/counter-store"
|
|
495
|
+
frame_1: "frame_1", // 对应 v-lark="components/counter-updater"
|
|
496
|
+
},
|
|
497
|
+
|
|
498
|
+
// readyMap 已 ready 的子 frame 集合
|
|
499
|
+
// hasAltered 子 frame 是否被修改
|
|
500
|
+
// signature 异步操作签名
|
|
501
|
+
// invokeList 延迟方法调用列表
|
|
502
|
+
// originalTemplate 挂载前的原始 html
|
|
503
|
+
};
|
|
504
|
+
```
|
|
505
|
+
|
|
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?)`: 管理区域内的 v-lark 子视图, 区域内的子 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 视图基类负责用户交互, 提供数据绑定、事件处理、生命周期与视图继承能力
|
|
528
|
+
|
|
529
|
+
_视图继承_
|
|
530
|
+
|
|
531
|
+
- `View.extend(props)`: 创建视图子类, 内部使用 ES6 class extends 实现原型链继承. extend props 作为实例属性在 super() 之后调用, 避免 ES6 class 字段遮蔽 (shadow) 问题. 子类构造函数签名: `(nodeId, ownerFrame, initParams, node, mixinCtors)`
|
|
532
|
+
- `View.merge(mixin)`: 混入功能到 View 原型, 事件方法以 MixinEventHandler 形式合并, 保留原始 handler 列表
|
|
533
|
+
|
|
534
|
+
解析事件方法 (viewPrepare): 扫描 View 原型上的方法名, 匹配 `$?name<eventType>(<modifiers>)` 模式, 例如 `clickHandler<click>` 自动绑定 click 事件到 `clickHandler` 方法, `$menu<hover>` 绑定带选择器的事件. 解析结果构建 eventObjectMap (事件类型到位掩码的映射) 与 eventSelectorMap (事件类型到处理器名称列表的映射). 修饰符支持如 `<click,mousedown>` 绑定多个事件类型
|
|
535
|
+
|
|
536
|
+
```js
|
|
537
|
+
// window 上监听 resize
|
|
538
|
+
"$window<resize>"(e) {}
|
|
539
|
+
|
|
540
|
+
// document 上监听 keydown
|
|
541
|
+
"$document<keydown>"(e) {}
|
|
542
|
+
|
|
543
|
+
// 点击 .btn 元素时触发
|
|
544
|
+
"$btn<click>"(e) {}
|
|
545
|
+
|
|
546
|
+
// hover .tooltip 元素时触发
|
|
547
|
+
"$tooltip<mouseenter>"(e) {}
|
|
548
|
+
|
|
549
|
+
// $ 后面的 CSS 选择器为空时, 默认选择 frame 根节点
|
|
550
|
+
// 点击 frame 根节点时触发
|
|
551
|
+
"$<click>"(e) {}
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
```html
|
|
555
|
+
<button class="btn">只需要写 CSS 类名</button>
|
|
556
|
+
<div class="tooltip">不需要写 @event, 适用于批量绑定</div>
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
事件委托 (viewDelegateEvents): 将 View 的 eventObjectMap 和 eventSelectorMap 注册到 EventDelegator, 全局事件类型 (如 click, change) 绑定到 document.body, 选择器事件通过 matches() API 匹配; 同时处理 window/document 前缀的全局事件 (例如 resize, scroll), 绑定到 window/document
|
|
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 处理新增的 v-lark 子视图 (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 操作. 对于有 v-lark 属性的元素, 如果新旧 view 路径相同则跳过子节点更新, 保护已挂载的子视图
|
|
622
|
+
- `vdomSetChildNodes(oldParent, newParent, ref, frame, keys?)`: 对比子节点列表, 支持 keyed diff. 算法: 先从旧子节点构建 keyedNodes 映射 (键由 vdomGetCompareKey 获取, 优先级 id > ldk > v-lark 路径), 同时统计新子节点的 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 属性 > ldk 属性 > v-lark 路径. 有 autoId 标记的元素不使用 id 作为比较键. 比较键结果缓存到元素的 compareKeyCached / cachedCompareKey 属性
|
|
627
|
+
|
|
628
|
+
静态跳过: 有 ldk 属性且值相同的元素跳过 diff; 有 lak 属性且值相同的元素跳过属性更新
|
|
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 的 @event 属性在编译时编码为 viewId\x1ehandlerName(params) 格式, 运行时由 EventDelegator 解析并分发
|
|
646
|
+
|
|
647
|
+
核心机制:
|
|
648
|
+
|
|
649
|
+
- `bind(eventType, selector?)`: 注册事件类型到 document.body, 选择器事件使用事件委托匹配; 同一事件类型只注册一次全局监听器
|
|
650
|
+
- `domEventProcessor(event)`: 事件处理入口, 从目标元素向上遍历 DOM 树查找 @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 事件: 通过 lvk 属性标记 View 边界, 事件处理时检查 rangeFrameId, 确保只在对应 View 的 DOM 范围内查找事件处理器
|
|
659
|
+
|
|
660
|
+
Service + Bag — 接口请求管理
|
|
661
|
+
|
|
662
|
+
Service 提供接口请求的统一管理, 支持缓存、去重与队列
|
|
663
|
+
|
|
664
|
+
核心 API:
|
|
665
|
+
|
|
666
|
+
- Service.extend(syncFn, cacheMax?, cacheBuffer?): 创建 Service 子类, 指定同步函数 (执行实际请求, 接收 bag 和 callback 参数); cacheMax 控制缓存容量上限, cacheBuffer 控制淘汰缓冲区大小
|
|
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(): 取消所有进行中的请求, 销毁服务实例
|
|
673
|
+
|
|
674
|
+
Bag: 响应数据的包装器, 提供 get(key) 与 set(keyOrData, value?) 方法. set 支持单键设置和对象合并. 缓存信息附加在 Bag 的 cacheInfo 属性上, 包含 name (接口名), after (后置钩子), cleans (清理键), key (缓存键), time (缓存时间戳)
|
|
675
|
+
|
|
676
|
+
请求去重: 相同 cacheKey 的并发请求共享同一个 pendingCacheKeys 条目, 首次请求完成后统一通知所有等待中的回调, 避免重复请求. 缓存键由 JSON.stringify(attrs) + SPLITTER + JSON.stringify(meta) 生成
|
|
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(): 处理 @event 属性, 添加 \x1f (VIEW_ID_PLACEHOLDER) 前缀 + \x1e (SPLITTER) 分隔符, 将 JS 对象字面量参数转换为 URL 查询参数格式. 例如: @click="handlerName({key: 'value'})" 转换为 @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 处理 v-lark 子视图 → mountZone
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
对比 React 框架
|
|
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, ldk, v-lark 属性), 类似于 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, 通过 @event 属性编码 View ID 和 handler 名称, EventDelegator 解析分发. Lark 的方案更轻量, 不需要额外的事件对象池和兼容层
|
|
829
|
+
|
|
830
|
+
对比 Vue3 框架
|
|
831
|
+
|
|
832
|
+
模板编译
|
|
833
|
+
|
|
834
|
+
Vue3 的模板编译器将单文件组件的 <template> 编译为渲染函数, 编译过程分为三步: parse (模板 → AST), transform (AST → JavaScript AST), generate (JavaScript AST → 渲染函数代码). Vue3 的编译器实现了静态提升 (Static Hoisting): 将静态节点提升到渲染函数外部, 避免每次渲染重新创建; PatchFlag 标记动态节点, diff 时只比较标记为动态的部分; Block Tree 优化, 将模板按结构化指令 (v-if, v-for) 拆分为 Block, diff 时扁平化遍历
|
|
835
|
+
|
|
836
|
+
Lark 的模板编译器将 {{}} 语法编译为 JS 函数, 编译过程分为三步: 预处理 (protectComments + processViewEvents), 语法转换 (convertArtSyntax: {{}} → <% %>), 函数生成 (compileToFunction: <% %> → 箭头函数). Lark 的编译器没有 AST 中间表示, 直接通过正则匹配和字符串替换完成转换. Lark 不实现静态提升, 每次渲染完整执行模板函数; 也没有 PatchFlag, 每次渲染产生的 HTML 需要完整 diff. Lark 通过 ldk 和 lak 属性提供手动静态跳过能力, 类似于 Vue3 PatchFlag 的手动版本
|
|
837
|
+
|
|
838
|
+
Vue3 的编译器输出 render() 函数, 返回虚拟 DOM 节点树 (VNode), 再由运行时进行 diff; Lark 的编译器输出模板函数, 返回 HTML 字符串, 由 Updater 解析为真实 DOM 后再 diff. Vue3 的编译时优化更丰富, Lark 的编译器更简洁, 依赖运行时 diff 的 keyed 匹配来保证性能
|
|
839
|
+
|
|
840
|
+
Vue3 的编译器支持 v-model (双向绑定), v-slot (插槽), v-once (只渲染一次), v-memo (记忆化) 等指令; Lark 支持 = (转义输出), ! (原始输出), @ (引用查找), : (绑定) 运算符, 以及 each, parse, for, if/else, set 控制流
|
|
841
|
+
|
|
842
|
+
Proxy 响应式
|
|
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
|
+
组件模型
|
|
857
|
+
|
|
858
|
+
Vue3 使用单文件组件 (SFC) 组织视图, <template>, <script>, <style> 放在同一个 .vue 文件中; Lark 将模板 (.html) 和逻辑 (.ts) 分离, 通过 import 关联. Vue3 组件通过 props 接收父组件数据, 通过 emit 向父组件发送事件; Lark View 通过 v-lark 属性传递路径参数, 通过 {{@}} 引用查找传递对象引用, 通过 Frame.invoke 跨层级通信
|
|
859
|
+
|
|
860
|
+
Vue3 的组合式 API (Composition API) 通过 setup() 函数组织逻辑, 使用 ref, reactive, computed, watch 等 API; Lark 的 defineStore creator 函数类似于 setup(), 在其中定义 state 和 handler. Vue3 使用 provide/inject 跨层级传递数据; Lark 使用 State (全局单例) 和 Store (按需创建) 实现跨视图数据共享
|
|
861
|
+
|
|
862
|
+
API 速查
|
|
863
|
+
|
|
864
|
+
Store API
|
|
865
|
+
|
|
866
|
+
| API | 说明 |
|
|
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?) | 创建响应式状态 |
|
|
882
|
+
|
|
883
|
+
View 事件绑定
|
|
884
|
+
|
|
885
|
+
模板中通过 @事件类型 属性绑定事件, 格式为 @click="handlerName()". 支持传参: @click="handlerName({key: value})", JS 对象字面量参数会自动转换为 URL 查询参数格式. 编译时自动编码 View ID 与参数, 运行时由 EventDelegator 解析分发
|
|
886
|
+
|
|
887
|
+
模板语法
|
|
888
|
+
|
|
889
|
+
| 语法 | 说明 |
|
|
890
|
+
| ------------------------------------------- | ----------------------------- |
|
|
891
|
+
| {{=variable}} | HTML 转义输出 |
|
|
892
|
+
| {{!variable}} | 原始输出 (不转义) |
|
|
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}} | 变量声明 |
|
|
905
|
+
|
|
906
|
+
Framework 全局方法
|
|
907
|
+
|
|
908
|
+
| 方法 | 说明 |
|
|
909
|
+
| ------------------------------------------ | ------------------- |
|
|
910
|
+
| Framework.boot(config) | 启动框架 |
|
|
911
|
+
| Framework.config(cfg?) | 获取/设置配置 |
|
|
912
|
+
| Framework.mark / unmark | 异步回调追踪 |
|
|
913
|
+
| Framework.dispatch(target, type, init?) | 触发自定义 DOM 事件 |
|
|
914
|
+
| Framework.task(fn, args?, ctx?) | 分片任务调度 |
|
|
915
|
+
| Framework.delay(time) | Promise 延时 |
|
|
916
|
+
| Framework.use(names, callback?) | 模块加载 |
|
|
917
|
+
| Framework.toMap / toTry / toUrl / parseUrl | 工具函数 |
|
|
918
|
+
| Framework.has / keys / inside / node | 工具函数 |
|
|
919
|
+
| Framework.guid | 生成唯一 ID |
|
|
920
|
+
| Framework.guard | Proxy 调试保护 |
|
|
921
|
+
| Framework.applyStyle | CSS 动态注入 |
|
|
922
|
+
| Framework.Cache | LFU 缓存类 |
|
|
923
|
+
| Framework.Base | EventEmitter 基类 |
|
|
924
|
+
|
|
925
|
+
Router API
|
|
926
|
+
|
|
927
|
+
| API | 说明 |
|
|
928
|
+
| ------------------------------------------- | -------------------------- |
|
|
929
|
+
| Router.parse(href?) | 解析 href 为 Location 对象 |
|
|
930
|
+
| Router.diff() | 计算新旧路由差异 |
|
|
931
|
+
| Router.to(path, params?, replace?, silent?) | 编程式导航 |
|
|
932
|
+
| Router.join(...paths) | 路径拼接 |
|
|
933
|
+
| Router.on(event, handler) | 监听路由事件 |
|
|
934
|
+
| Router.off(event, handler?) | 移除路由事件监听 |
|
|
935
|
+
|
|
936
|
+
State API
|
|
937
|
+
|
|
938
|
+
| API | 说明 |
|
|
939
|
+
| -------------------------- | -------------------- |
|
|
940
|
+
| State.get(key) | 读取数据 |
|
|
941
|
+
| State.set(data) | 写入数据并标记变更键 |
|
|
942
|
+
| State.digest() | 触发变更通知 |
|
|
943
|
+
| State.diff() | 获取上次变更键映射 |
|
|
944
|
+
| State.on(event, handler) | 监听状态事件 |
|
|
945
|
+
| State.off(event, handler?) | 移除状态事件监听 |
|