citrine 0.2.0
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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +249 -0
- data/bin/citrine +39 -0
- data/desktop/main.swift +44 -0
- data/lib/citrine/browser.rb +11 -0
- data/lib/citrine/canvas.rb +503 -0
- data/lib/citrine/component.rb +692 -0
- data/lib/citrine/debug.rb +109 -0
- data/lib/citrine/dev_server.rb +305 -0
- data/lib/citrine/dom.rb +292 -0
- data/lib/citrine/event.rb +31 -0
- data/lib/citrine/key_event.rb +39 -0
- data/lib/citrine/list_signal.rb +354 -0
- data/lib/citrine/node.rb +36 -0
- data/lib/citrine/num.rb +75 -0
- data/lib/citrine/packager.rb +129 -0
- data/lib/citrine/reactive.rb +40 -0
- data/lib/citrine/renderer.rb +828 -0
- data/lib/citrine/signal.rb +236 -0
- data/lib/citrine/sourcemap.rb +143 -0
- data/lib/citrine/string_renderer.rb +116 -0
- data/lib/citrine/style.rb +79 -0
- data/lib/citrine/theme.rb +99 -0
- data/lib/citrine/version.rb +5 -0
- data/lib/citrine.rb +92 -0
- data/lib/rubocop/cop/citrine/no_raw_ivar_assignment.rb +75 -0
- metadata +157 -0
|
@@ -0,0 +1,828 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "signal"
|
|
4
|
+
require_relative "node"
|
|
5
|
+
require_relative "style"
|
|
6
|
+
|
|
7
|
+
module Citrine
|
|
8
|
+
# 可调用体(Symbol / Proc)分发的统一入口:Symbol 在 receiver 上按 arity
|
|
9
|
+
# 决定是否接收实参;Proc 默认保持闭包 self(事件回调的语义——嵌套组件的
|
|
10
|
+
# 父级回调不能被重绑),bind: true 时重绑到 receiver(生命周期钩子 /
|
|
11
|
+
# error_fallback 需要在 owner 上下文里调 DSL)。
|
|
12
|
+
# 事件处理器(Component#handle_event)与错误兜底(Renderer#render_error_fallback)
|
|
13
|
+
# 同此一份 arity 约定,不再各处手写分支。
|
|
14
|
+
def self.dispatch_callable(handler, receiver, arg = nil, bind: false)
|
|
15
|
+
case handler
|
|
16
|
+
when Symbol
|
|
17
|
+
receiver.method(handler).arity.zero? ? receiver.send(handler) : receiver.send(handler, arg)
|
|
18
|
+
when Proc
|
|
19
|
+
if handler.arity.zero?
|
|
20
|
+
bind ? receiver.instance_exec(&handler) : handler.call
|
|
21
|
+
else
|
|
22
|
+
bind ? receiver.instance_exec(arg, &handler) : handler.call(arg)
|
|
23
|
+
end
|
|
24
|
+
else
|
|
25
|
+
raise ArgumentError, "无法分发的处理器: #{handler.inspect}"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# 渲染器基类:节点树管理 + Effect 装配 + 块级重建/销毁(平台无关)。
|
|
30
|
+
#
|
|
31
|
+
# 平台子类实现钩子:setup_root / create_dom / attach / detach /
|
|
32
|
+
# apply_props / bind_events / set_text / setup_widget / finalize,
|
|
33
|
+
# 并以 reactive? 声明是否建立更新订阅(一次性渲染如 StringRenderer 返回 false)。
|
|
34
|
+
class Renderer
|
|
35
|
+
TAGS = { box: "div", label: "p", button: "button",
|
|
36
|
+
text_input: "input", check_box: "input" }.freeze
|
|
37
|
+
VOID = %i[text_input check_box img].freeze
|
|
38
|
+
|
|
39
|
+
# 响应式属性白名单:这些 prop 的值可以是 Proc,在**该节点自己的 Effect** 内求值。
|
|
40
|
+
#
|
|
41
|
+
# G-2:props 的求值位置决定订阅范围——写成 `box(css_class: cell_class(row, col))`
|
|
42
|
+
# 时实参在**外层块**执行期间求值,于是外层块订阅了这一格的信号(改一格 → 整块重建)。
|
|
43
|
+
# 写成 `box(css_class: -> { cell_class(row, col) })` 则订阅落在本节点的属性 Effect 上,
|
|
44
|
+
# 且重跑只重设属性、不重建子树。
|
|
45
|
+
#
|
|
46
|
+
# 事件处理器(on_*)不在此列:它们的 Proc 是回调,不是待求值的值。
|
|
47
|
+
REACTIVE_PROPS = %i[css_class placeholder style direction gap].freeze
|
|
48
|
+
|
|
49
|
+
# 透明容器类型(S1-4 fragment / S1-5 portal / S1-10 suspense):无自身 DOM 语义,
|
|
50
|
+
# dispose 时不对它们做 detach(子根挂在真实容器上,逐个摘除)
|
|
51
|
+
TRANSPARENT_TYPES = %i[fragment portal suspense].freeze
|
|
52
|
+
|
|
53
|
+
# S2-2:框架未消费的属性原样透传到 DOM / SSR(id / disabled / aria_* / data_* / title …)。
|
|
54
|
+
# 命名规则:snake_case → kebab-case(aria_label → aria-label);
|
|
55
|
+
# 布尔规则:true → 空值属性(<button disabled>)、false / nil → 不输出。
|
|
56
|
+
# 消费面(不透传):布局快捷键、样式、复用/引用标记、事件回调(on_*),
|
|
57
|
+
# 以及值类 prop(见 WIDGET_VALUE_PROPS,节点感知)。
|
|
58
|
+
ALWAYS_CONSUMED = %i[key ref style css_class direction gap portal_target
|
|
59
|
+
suspense_ready suspense_loading].freeze
|
|
60
|
+
|
|
61
|
+
# 值类 prop 只在对应控件上被框架消费;其他元素(textarea / select / 自定义标签)
|
|
62
|
+
# 照常透传——否则 value 又会被静默吞掉(F11 的老路)
|
|
63
|
+
WIDGET_VALUE_PROPS = {
|
|
64
|
+
placeholder: [:text_input], type: %i[text_input check_box],
|
|
65
|
+
value: [:text_input], checked: [:check_box]
|
|
66
|
+
}.freeze
|
|
67
|
+
|
|
68
|
+
def initialize
|
|
69
|
+
@parents = []
|
|
70
|
+
@reuse_pools = []
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def mount_component(component, element)
|
|
74
|
+
# DSL(Component#emit)经全局 Citrine.renderer 分发到当前渲染器;
|
|
75
|
+
# 响应式 Effect 重跑发生在 mount 之后,也依赖此设置,
|
|
76
|
+
# 因此不恢复旧值——"活动渲染器 = 最近挂载的那个"。
|
|
77
|
+
Citrine.renderer = self
|
|
78
|
+
@undirected_boxes = 0
|
|
79
|
+
root = Node.new(:root, {}, nil, owner: component)
|
|
80
|
+
attach_root_ref(component, root)
|
|
81
|
+
setup_root(root, element)
|
|
82
|
+
run_view(root, component)
|
|
83
|
+
finalize(root)
|
|
84
|
+
warn_undirected_boxes
|
|
85
|
+
register_window_keys(component)
|
|
86
|
+
component.run_mount_hooks if component.respond_to?(:run_mount_hooks)
|
|
87
|
+
component.run_watch_effects if reactive? && component.respond_to?(:run_watch_effects)
|
|
88
|
+
component.run_effects if reactive? && component.respond_to?(:run_effects)
|
|
89
|
+
root
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# 卸载:销毁整棵子树(含每个节点的 Effect 订阅)、解绑全局键盘、跑 on_unmount。
|
|
93
|
+
# root 自身留着(root.dom 就是页面上那个挂载容器,不属于组件),只清空内容。
|
|
94
|
+
def unmount_component(root)
|
|
95
|
+
root.children.dup.each { |child| dispose(child) }
|
|
96
|
+
root.children.clear
|
|
97
|
+
root.owned_effects.each(&:dispose)
|
|
98
|
+
root.owned_effects.clear
|
|
99
|
+
|
|
100
|
+
component = root.owner
|
|
101
|
+
if component.respond_to?(:run_unmount_hooks)
|
|
102
|
+
unregister_window_keys(component)
|
|
103
|
+
component.run_unmount_hooks
|
|
104
|
+
end
|
|
105
|
+
root
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# DSL 挂载入口(Component#emit 调用):把 node 挂到当前父节点下
|
|
109
|
+
def mount(node)
|
|
110
|
+
parent = @parents.last
|
|
111
|
+
unless parent
|
|
112
|
+
raise "Citrine: 元素挂载时没有父节点。组件 view 只能在渲染器挂载过程中执行;" \
|
|
113
|
+
"一页多根请分别用各渲染器实例(DomRenderer.new + mount_component),不要多次 mount_at"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
node.reuse_key = node.props[:key]
|
|
117
|
+
node.identity ||= [:element, node.type]
|
|
118
|
+
|
|
119
|
+
return mount_portal(node, parent) if node.type == :portal
|
|
120
|
+
return mount_suspense(node, parent) if node.type == :suspense
|
|
121
|
+
return mount_fragment(node, parent) if node.type == :fragment
|
|
122
|
+
|
|
123
|
+
node.dom = create_dom(node)
|
|
124
|
+
warn_unreactive_proc(node)
|
|
125
|
+
register_ref(node)
|
|
126
|
+
parent.children << node
|
|
127
|
+
attach(node, parent)
|
|
128
|
+
bind_events(node)
|
|
129
|
+
setup_widget(node)
|
|
130
|
+
|
|
131
|
+
# G-2:响应式属性必须在**本节点自己的 Effect** 内求值——挂载路径上直接求值会让
|
|
132
|
+
# 订阅落进外层块(正是要消除的隐性外扩)。重跑只重设属性,不重建子树。
|
|
133
|
+
if reactive? && reactive_props?(node)
|
|
134
|
+
node.props_effect = Effect.create { apply_props(node) }
|
|
135
|
+
node.owned_effects << node.props_effect
|
|
136
|
+
else
|
|
137
|
+
apply_props(node)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
if node.block
|
|
141
|
+
if reactive?
|
|
142
|
+
node.block_effect = Effect.create do
|
|
143
|
+
run_block(node) { node.owner.instance_exec(&node.block) }
|
|
144
|
+
end
|
|
145
|
+
node.owned_effects << node.block_effect
|
|
146
|
+
else
|
|
147
|
+
run_block(node) { node.owner.instance_exec(&node.block) }
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
# 子节点此时才建好:多子容器才会真的"塌",所以放在 block 之后统计(G-8)
|
|
151
|
+
note_undirected_box(node)
|
|
152
|
+
finalize(node)
|
|
153
|
+
node
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# 透明容器(S1-4):children 插槽的落位节点。无自身 DOM——借父容器的,
|
|
157
|
+
# 子孙 attach 经它落到真实父容器;不参与属性/事件(也没有 props 可言)。
|
|
158
|
+
def mount_fragment(node, parent)
|
|
159
|
+
mount_transparent(node, parent, parent.dom)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Portal(S1-5):子树挂到渲染器指定的宿主节点(DOM 下默认 body)——
|
|
163
|
+
# 逃出父容器的 overflow / 层叠上下文。树上仍是逻辑父的孩子(复用、Effect、
|
|
164
|
+
# 卸载级联与原地渲染一致),DOM 上子孙落到宿主;卸载时随 dispose 逐个摘除,
|
|
165
|
+
# 宿主里 portal 之外的内容不受影响。
|
|
166
|
+
def mount_portal(node, parent)
|
|
167
|
+
mount_transparent(node, parent, resolve_portal_host(node.props[:portal_target]))
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def mount_transparent(node, parent, host_dom)
|
|
171
|
+
node.dom = host_dom
|
|
172
|
+
parent.children << node
|
|
173
|
+
if reactive?
|
|
174
|
+
node.block_effect = Effect.create do
|
|
175
|
+
run_block(node) { node.owner.instance_exec(&node.block) }
|
|
176
|
+
end
|
|
177
|
+
node.owned_effects << node.block_effect
|
|
178
|
+
else
|
|
179
|
+
run_block(node) { node.owner.instance_exec(&node.block) }
|
|
180
|
+
end
|
|
181
|
+
finalize(node)
|
|
182
|
+
node
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Suspense(S1-10):依赖未就绪时渲染占位,就绪后原地切换到真实内容。
|
|
186
|
+
# ready Proc 的信号读取订阅在本节点的 Effect 上——就绪状态翻转驱动切换;
|
|
187
|
+
# 占位/真实内容走通用块调和(旧分支节点被替换,其余组件实例与 state 不动)。
|
|
188
|
+
# 只做"渲染期等待",不含数据请求实现。
|
|
189
|
+
def mount_suspense(node, parent)
|
|
190
|
+
node.dom = parent.dom
|
|
191
|
+
parent.children << node
|
|
192
|
+
if reactive?
|
|
193
|
+
node.block_effect = Effect.create { run_suspense(node) }
|
|
194
|
+
node.owned_effects << node.block_effect
|
|
195
|
+
else
|
|
196
|
+
run_suspense(node) # SSR / 一次性渲染:只输出当前分支
|
|
197
|
+
end
|
|
198
|
+
finalize(node)
|
|
199
|
+
node
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def run_suspense(node)
|
|
203
|
+
owner = node.owner
|
|
204
|
+
ready = node.props[:suspense_ready]
|
|
205
|
+
loading = node.props[:suspense_loading]
|
|
206
|
+
# ready 的读取(Proc 在 owner 上下文求值)订阅本节点的 Effect——
|
|
207
|
+
# 就绪状态翻转是切换的触发源
|
|
208
|
+
ready_value = ready.nil? ? true : prop_value(node, ready)
|
|
209
|
+
is_ready = !ready_value.nil? && ready_value != false
|
|
210
|
+
|
|
211
|
+
previous = node.children.dup
|
|
212
|
+
node.children.clear
|
|
213
|
+
@parents.push(node)
|
|
214
|
+
pool = ReusePool.new(previous)
|
|
215
|
+
@reuse_pools.push(pool)
|
|
216
|
+
|
|
217
|
+
if is_ready
|
|
218
|
+
owner.instance_exec(&node.block) if node.block
|
|
219
|
+
elsif loading
|
|
220
|
+
owner.instance_exec(&loading)
|
|
221
|
+
end
|
|
222
|
+
ensure
|
|
223
|
+
@parents.pop
|
|
224
|
+
@reuse_pools&.pop
|
|
225
|
+
pool&.unused_nodes&.each { |old| dispose(old) }
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# 嵌套组件入口(Component#render 调用)。
|
|
229
|
+
#
|
|
230
|
+
# 复用单位 = 子组件实例(+它的根节点)。同类组件落到同一槽位即复用(S1-2):
|
|
231
|
+
# props 差异经 prop 信号原地传播,实例与 state 保留;子组件 view 在**自己的
|
|
232
|
+
# Effect** 里执行(首次挂载创建),prop/state 的 view 体读取都订阅它——
|
|
233
|
+
# 父块不再因子组件重渲染而重跑,prop 写入也不会再入正在运行的父块。
|
|
234
|
+
# 带 block 调用即插槽(S1-4):块由子组件经 children 落位,块内 self 是父组件。
|
|
235
|
+
def render_component(owner, component, props, &children_block)
|
|
236
|
+
raise "Citrine.render:没有父节点(只能在组件的 view 里渲染子组件)" unless @parents.last
|
|
237
|
+
|
|
238
|
+
klass = component.is_a?(Class) ? component : component.class
|
|
239
|
+
key = props[:key]
|
|
240
|
+
ref_name = props[:ref]
|
|
241
|
+
child_props = props.reject { |name, _| name == :key || name == :ref }
|
|
242
|
+
|
|
243
|
+
if (existing = reusable_node(key, [:component, klass], child_props, owner))
|
|
244
|
+
child = existing.rendered_component
|
|
245
|
+
existing.component_props = child_props
|
|
246
|
+
child.update_props(child_props) if component.is_a?(Class) && child.respond_to?(:update_props)
|
|
247
|
+
child.set_children_block(children_block, owner) if child.respond_to?(:set_children_block)
|
|
248
|
+
register_component_ref(owner, ref_name, child)
|
|
249
|
+
return child.respond_to?(:root) && child.root ? child.root : existing
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
child = component.is_a?(Class) ? klass.new(child_props) : component
|
|
253
|
+
child.set_children_block(children_block, owner) if child.respond_to?(:set_children_block)
|
|
254
|
+
parent = @parents.last
|
|
255
|
+
if reactive? && child.respond_to?(:view_effect=)
|
|
256
|
+
child.view_effect = Effect.create do
|
|
257
|
+
child.root ? rerun_component_view(child, parent) : first_component_render(child, parent, klass, key, child_props)
|
|
258
|
+
end
|
|
259
|
+
node = child.root
|
|
260
|
+
else
|
|
261
|
+
node = first_component_render(child, parent, klass, key, child_props)
|
|
262
|
+
end
|
|
263
|
+
register_component_ref(owner, ref_name, child)
|
|
264
|
+
register_window_keys(child)
|
|
265
|
+
child.run_mount_hooks if child.respond_to?(:run_mount_hooks)
|
|
266
|
+
child.run_watch_effects if reactive? && child.respond_to?(:run_watch_effects)
|
|
267
|
+
child.run_effects if reactive? && child.respond_to?(:run_effects)
|
|
268
|
+
node
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# 首次渲染子组件 view,收编输出为组件根:恰好一个真实节点 → 该节点就是根;
|
|
272
|
+
# 多个 → 包进透明 fragment(复用/销毁单位仍是组件实例)。
|
|
273
|
+
def first_component_render(child, parent, klass, key, child_props)
|
|
274
|
+
before = parent.children.size
|
|
275
|
+
run_view_with_fallback(child, parent)
|
|
276
|
+
added = parent.children[before..] || []
|
|
277
|
+
if added.empty?
|
|
278
|
+
raise "Citrine.render:子组件的 view 必须渲染**至少一个**根节点,实际 0 个;" \
|
|
279
|
+
"纯文本请包一层 label { },多根会按 fragment 处理"
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
node = if added.size == 1
|
|
283
|
+
added.first
|
|
284
|
+
else
|
|
285
|
+
wrap_fragment(added, parent, before, child)
|
|
286
|
+
end
|
|
287
|
+
adopt_root(node, child, [:component, klass], key, child_props)
|
|
288
|
+
node
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# 子组件 view 的后续重跑(prop/state 信号触发):原地调和回既有根。
|
|
292
|
+
# mount/reusable 的 attach 一律追加到末尾,信号驱动的重跑发生在父块渲染之外,
|
|
293
|
+
# 因此 reconcile 后要把根挪回原渲染位(attach_before),保持兄弟次序。
|
|
294
|
+
def rerun_component_view(child, parent)
|
|
295
|
+
node = child.root
|
|
296
|
+
return rerun_fragment_view(child, node, parent) if node.type == :fragment
|
|
297
|
+
|
|
298
|
+
index = parent.children.index(node)
|
|
299
|
+
raise "Citrine.render:子组件 #{child.class} 的根不在宿主容器里(正常流程不可达)" unless index
|
|
300
|
+
|
|
301
|
+
anchor = parent.children[index + 1]
|
|
302
|
+
parent.children.delete(node)
|
|
303
|
+
size_before = parent.children.size
|
|
304
|
+
pool = ReusePool.new([node])
|
|
305
|
+
@parents.push(parent)
|
|
306
|
+
@reuse_pools.push(pool)
|
|
307
|
+
run_view_with_fallback(child, parent)
|
|
308
|
+
if parent.children.size != size_before + 1
|
|
309
|
+
raise "Citrine.render:子组件的 view 必须渲染**恰好一个**根节点," \
|
|
310
|
+
"实际 #{parent.children.size - size_before} 个;多根或纯文本请自行包一层 stack { }"
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
new_root = parent.children.pop
|
|
314
|
+
parent.children.insert(index, new_root)
|
|
315
|
+
unless new_root.equal?(node)
|
|
316
|
+
# 根元素换了类型:旧根整棵卸载,但**组件实例仍然活着**(S3-2 的边界摘除舞步):
|
|
317
|
+
# 先摘组件边界再 dispose,dispose 就不会误跑 on_unmount / 解绑全局键盘。
|
|
318
|
+
node.rendered_component = nil
|
|
319
|
+
adopt_root(new_root, child, node.component_identity, node.reuse_key, node.component_props)
|
|
320
|
+
end
|
|
321
|
+
reposition(new_root, parent, anchor)
|
|
322
|
+
ensure
|
|
323
|
+
@parents.pop
|
|
324
|
+
@reuse_pools&.pop
|
|
325
|
+
pool&.unused_nodes&.each { |old| dispose(old) }
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# 多根子组件的重跑:把 fragment 当块容器重跑(子根经 fragment.dom 落到真实父容器)
|
|
329
|
+
def rerun_fragment_view(child, frag, parent)
|
|
330
|
+
index = parent.children.index(frag)
|
|
331
|
+
return unless index
|
|
332
|
+
|
|
333
|
+
anchor = parent.children[index + 1]
|
|
334
|
+
parent.children.delete(frag)
|
|
335
|
+
pool = ReusePool.new(frag.children.dup)
|
|
336
|
+
frag.children.clear
|
|
337
|
+
@parents.push(frag)
|
|
338
|
+
@reuse_pools.push(pool)
|
|
339
|
+
run_view_with_fallback(child, frag)
|
|
340
|
+
ensure
|
|
341
|
+
@parents.pop
|
|
342
|
+
@reuse_pools&.pop
|
|
343
|
+
pool&.unused_nodes&.each { |old| dispose(old) }
|
|
344
|
+
if index && !parent.children[index].equal?(frag)
|
|
345
|
+
parent.children.insert(index, frag)
|
|
346
|
+
end
|
|
347
|
+
frag.children.each { |n| reposition(n, parent, anchor) if index }
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# 多根输出 → 透明 fragment:子根已在 parent.dom 里(view 挂载时 attach 的),
|
|
351
|
+
# 树上收拢到 fragment 名下、fragment 占据首个子根的原渲染位。
|
|
352
|
+
# dom 借父容器的(与 mount_fragment 同一口径):子根 attach 经它落到真实父容器。
|
|
353
|
+
def wrap_fragment(added, parent, index, child)
|
|
354
|
+
fragment = Node.new(:fragment, {}, nil, owner: child)
|
|
355
|
+
fragment.identity = [:element, :fragment]
|
|
356
|
+
fragment.dom = parent.dom
|
|
357
|
+
added.each { |n| parent.children.delete(n) }
|
|
358
|
+
fragment.children.concat(added)
|
|
359
|
+
parent.children.insert(index, fragment)
|
|
360
|
+
finalize(fragment)
|
|
361
|
+
fragment
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# 把一个真实节点标记为"某子组件的根"(复用 / 卸载 / 生命周期都以它为单位)
|
|
365
|
+
def adopt_root(node, child, identity, key, child_props)
|
|
366
|
+
node.rendered_component = child
|
|
367
|
+
node.component_identity = identity # 注意:不覆盖 identity(它仍是该元素的元素身份)
|
|
368
|
+
node.reuse_key = key
|
|
369
|
+
node.component_props = child_props
|
|
370
|
+
child.root = node if child.respond_to?(:root=)
|
|
371
|
+
child.renderer = self if child.respond_to?(:renderer=)
|
|
372
|
+
node
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# S1-9:ref: :name 登记的是**子组件实例**(元素 ref 登记平台句柄),
|
|
376
|
+
# 父经 refs 取到实例即可调用其公开方法。复用/重建两条路径都重新登记,
|
|
377
|
+
# 保证指向当前活着的实例。
|
|
378
|
+
def register_component_ref(owner, name, child)
|
|
379
|
+
return unless name && owner.respond_to?(:refs)
|
|
380
|
+
|
|
381
|
+
owner.refs[name] = child
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
# 复用匹配:key 优先,没有 key 时按"本次第几个子节点"对位(React 的隐含位置 key)。
|
|
385
|
+
# 命中后节点按新顺序重新排位。返回 nil 表示"不匹配,应新建"。
|
|
386
|
+
def reusable_node(key, identity, props, requester)
|
|
387
|
+
pool = @reuse_pools.last
|
|
388
|
+
return nil unless pool
|
|
389
|
+
|
|
390
|
+
node = pool.take(key, identity, props, requester)
|
|
391
|
+
return nil unless node
|
|
392
|
+
|
|
393
|
+
# 复用的节点要重新计入当前父的子节点表(本轮开始时已清空),顺序由追加次序决定
|
|
394
|
+
@parents.last.children << node
|
|
395
|
+
# 透明容器(fragment / portal)无自身 DOM(借宿主容器的),没有可挂的东西
|
|
396
|
+
attach(node, @parents.last) unless TRANSPARENT_TYPES.include?(node.type)
|
|
397
|
+
node
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# 复用既有节点:就地换上新 props/block,重应用属性,并重跑两个 Effect。
|
|
401
|
+
# 事件监听不需要重绑:DOM 监听器在事件发生时从 node.props 现取处理器(见 DomRenderer#ensure_events)。
|
|
402
|
+
def refresh_node(node, props, block)
|
|
403
|
+
node.props = props
|
|
404
|
+
node.block = block
|
|
405
|
+
# 有属性 Effect 时由它求值(Effect 体内就是 apply_props);再直接调一次会让每次复用
|
|
406
|
+
# 都写两遍属性与样式(幂等但白做),因此这里二选一。
|
|
407
|
+
if node.props_effect
|
|
408
|
+
node.props_effect.run
|
|
409
|
+
else
|
|
410
|
+
apply_props(node)
|
|
411
|
+
end
|
|
412
|
+
node.block_effect&.run
|
|
413
|
+
node
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
# Context 解析(S1-3):沿当前渲染遍历栈向上找提供 name 的**最近**祖先组件
|
|
417
|
+
# (跳过读者自己——组件读自己的 context 时应取到祖先的)。
|
|
418
|
+
# 只在渲染遍历内可用;消费端首次绑定发生在挂载路径上,之后走缓存绑定。
|
|
419
|
+
# 供 Component#use_context 跨对象调用,保持 public。
|
|
420
|
+
def find_context_provider(name, consumer)
|
|
421
|
+
@parents.reverse_each do |node|
|
|
422
|
+
owner = node.owner
|
|
423
|
+
next if owner.nil? || owner.equal?(consumer)
|
|
424
|
+
|
|
425
|
+
return owner if owner.provides_context?(name)
|
|
426
|
+
end
|
|
427
|
+
nil
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
private
|
|
431
|
+
|
|
432
|
+
def run_view(root, component)
|
|
433
|
+
if reactive?
|
|
434
|
+
root.block_effect = Effect.create do
|
|
435
|
+
run_block(root) { component.instance_exec { view } }
|
|
436
|
+
end
|
|
437
|
+
root.owned_effects << root.block_effect
|
|
438
|
+
else
|
|
439
|
+
run_block(root) { component.instance_exec { view } }
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
# 重建 node 的子树:带 key 的子节点**复用**(同一批 DOM/Effect/组件实例),
|
|
444
|
+
# 其余按今天的语义整体重建;本轮未被复用的旧节点在此完整卸载。
|
|
445
|
+
def run_block(node)
|
|
446
|
+
previous = node.children.dup
|
|
447
|
+
node.children.clear
|
|
448
|
+
@parents.push(node)
|
|
449
|
+
pool = ReusePool.new(previous)
|
|
450
|
+
@reuse_pools.push(pool)
|
|
451
|
+
|
|
452
|
+
result = begin
|
|
453
|
+
yield
|
|
454
|
+
rescue StandardError => e
|
|
455
|
+
raise unless (fallback_owner = fallback_owner_for(node))
|
|
456
|
+
|
|
457
|
+
# S1-6:失败那一轮不留半更新——块内本轮产出的节点全部拆掉
|
|
458
|
+
# (抛错的子组件从未被收编,不会伪装成卸载、不跑 on_unmount),
|
|
459
|
+
# 然后以异常对象渲染兜底内容
|
|
460
|
+
node.children.dup.each { |child| dispose(child) }
|
|
461
|
+
node.children.clear
|
|
462
|
+
render_error_fallback(fallback_owner, fallback_owner.class.error_fallback_def, e)
|
|
463
|
+
nil
|
|
464
|
+
end
|
|
465
|
+
if !result.nil? && node.children.empty?
|
|
466
|
+
warn_nonstring(node, result) unless result.is_a?(String)
|
|
467
|
+
set_text(node, result)
|
|
468
|
+
elsif node.text && !node.text.to_s.empty?
|
|
469
|
+
# C3:上一轮写过文本、本轮没有(内容切成子节点或 nil)——显式清空,
|
|
470
|
+
# 否则旧 textContent 与新内容叠加(label { cond ? "text" : nil } 的残留)
|
|
471
|
+
set_text(node, "")
|
|
472
|
+
end
|
|
473
|
+
result
|
|
474
|
+
ensure
|
|
475
|
+
@parents.pop
|
|
476
|
+
@reuse_pools.pop
|
|
477
|
+
pool.unused_nodes.each { |old| dispose(old) }
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
# S1-6:本块的 owner 声明了 error_fallback 时,它就是边界组件
|
|
481
|
+
def fallback_owner_for(node)
|
|
482
|
+
owner = node.owner
|
|
483
|
+
owner && owner.class.error_fallback_def ? owner : nil
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
# C1:子组件 view 的统一执行入口(首次挂载与信号驱动重跑同口径)。
|
|
487
|
+
# 与 run_block 的错误边界同思路,但兜底归**子组件自己**声明的 error_fallback——
|
|
488
|
+
# 重跑路径上不存在外层块,异常原来直接穿出 Effect → Scheduler → 事件处理器,
|
|
489
|
+
# 子组件自己的 fallback 永远接不住。未声明兜底的组件照常上抛
|
|
490
|
+
# (由外层块的边界或调用方处理,与挂载路径一致)。
|
|
491
|
+
def run_view_with_fallback(child, parent)
|
|
492
|
+
before = parent.children.size
|
|
493
|
+
child.instance_exec { view }
|
|
494
|
+
rescue StandardError => e
|
|
495
|
+
raise unless (fallback = child.class.error_fallback_def)
|
|
496
|
+
|
|
497
|
+
# 失败那一轮不留半更新:本轮已产出(未收编)的节点全部拆掉,
|
|
498
|
+
# 然后以异常对象渲染兜底内容(其输出占据组件根的位置)
|
|
499
|
+
added = parent.children[before..] || []
|
|
500
|
+
added.each { |n| dispose(n) }
|
|
501
|
+
parent.children.pop(added.size)
|
|
502
|
+
render_error_fallback(child, fallback, e)
|
|
503
|
+
fallback_added = parent.children.size - before
|
|
504
|
+
if fallback_added != 1
|
|
505
|
+
raise "Citrine.render:error_fallback 的输出必须是**恰好一个**根节点," \
|
|
506
|
+
"实际 #{fallback_added} 个;多根请自行包一层 stack { }"
|
|
507
|
+
end
|
|
508
|
+
nil
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
# 异常对象交给兜底分支(经 Citrine.dispatch_callable,与事件处理器同口径;
|
|
512
|
+
# Proc 需要 owner 上下文里的 DSL,故 bind: true 重绑 self)
|
|
513
|
+
def render_error_fallback(owner, fallback, error)
|
|
514
|
+
Citrine.dispatch_callable(fallback, owner, error, bind: true)
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
# keyed 复用的匹配池:一次块执行内,按 key + 身份标签取用旧节点。
|
|
518
|
+
class ReusePool
|
|
519
|
+
def initialize(nodes)
|
|
520
|
+
@all = nodes.dup
|
|
521
|
+
@keyed = {}
|
|
522
|
+
nodes.each { |node| @keyed[node.reuse_key] = node if node.reuse_key }
|
|
523
|
+
@taken = {}
|
|
524
|
+
@seen = {}
|
|
525
|
+
@cursor = 0
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
# key 优先;没有 key 时按"本次第几个子节点"对位匹配(React 的隐含位置 key)。
|
|
529
|
+
# 同一个节点有两种身份:元素身份(自身)与组件根身份(它是某子组件的根)——
|
|
530
|
+
# 父组件按组件身份匹配、子组件 view 重跑时按元素身份匹配,两者指向同一节点。
|
|
531
|
+
#
|
|
532
|
+
# requester = 本次正在渲染的组件(emit 的 owner)。**元素槽位只认这个组件自己的节点**:
|
|
533
|
+
# 跨组件复用会让 refresh_node 沿用旧节点的 Effect,而 Effect 里是
|
|
534
|
+
# `node.owner.instance_exec(&node.block)`——owner 不跟着换,新块就在老 self 上跑。
|
|
535
|
+
# (同一位置换组件类型、或新组件的根按位置抢到旧组件的根,都会踩到。)
|
|
536
|
+
def take(key, identity, props, requester)
|
|
537
|
+
@cursor += 1
|
|
538
|
+
candidate = if key
|
|
539
|
+
register!(key)
|
|
540
|
+
@keyed[key]
|
|
541
|
+
else
|
|
542
|
+
@all[@cursor - 1]
|
|
543
|
+
end
|
|
544
|
+
return nil unless candidate
|
|
545
|
+
return nil if @taken.key?(candidate.object_id)
|
|
546
|
+
|
|
547
|
+
if identity.first == :component
|
|
548
|
+
# 组件槽位(S1-2):同类组件落到同一槽位即复用——props 差异经 prop 信号
|
|
549
|
+
# 原地传播(读它的块才重跑),不再作为"变了就重建"的依据;
|
|
550
|
+
# 按 props 比较会让父每次重传都重建子组件、丢光子组件 state。
|
|
551
|
+
return nil unless candidate.component_identity == identity
|
|
552
|
+
else
|
|
553
|
+
# 元素槽位:keyed 与位置匹配都要求"由同一个组件渲染出来的节点"
|
|
554
|
+
return nil unless candidate.owner.equal?(requester)
|
|
555
|
+
return nil unless candidate.identity == identity && same_props?(candidate.props, props)
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
@taken[candidate.object_id] = candidate
|
|
559
|
+
candidate
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
# 登记本层出现过的 key(新建路径也要走,用于重复检测)
|
|
563
|
+
def register!(key)
|
|
564
|
+
raise "Citrine: 同一层出现重复 key #{key.inspect}(key 只需在兄弟间唯一)" if @seen.key?(key)
|
|
565
|
+
|
|
566
|
+
@seen[key] = true
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
# 复用之外的旧节点(含所有没 key 的)=本轮被替换掉的,交给渲染器卸载。
|
|
570
|
+
# 以 @taken(object_id 为键)判定"是否被取走":不构造临时数组/哈希——每轮块重跑
|
|
571
|
+
# 都会走这里,而 `Array#-` 会为右侧集合建一份临时哈希(Opal 侧同样如此)。
|
|
572
|
+
def unused_nodes
|
|
573
|
+
@all.reject { |node| @taken.key?(node.object_id) }
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
private
|
|
577
|
+
|
|
578
|
+
# 比较 props 时忽略 key 与 Proc:Proc(响应式属性 / 回调)每次都是新对象,
|
|
579
|
+
# 不能作为"变了"的依据(仅元素槽位还在用它;组件槽位走信号传播,S1-2)
|
|
580
|
+
def same_props?(old_props, new_props)
|
|
581
|
+
comparable(old_props) == comparable(new_props)
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
def comparable(props)
|
|
585
|
+
props.reject { |name, value| name == :key || value.is_a?(Proc) }
|
|
586
|
+
end
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
# F16:block 返回非字符串时按 to_s 渲染而非静默置空;每类只提醒一次
|
|
590
|
+
def warn_nonstring(node, result)
|
|
591
|
+
@warned_types ||= {}
|
|
592
|
+
type = result.class
|
|
593
|
+
return if @warned_types[type]
|
|
594
|
+
return unless respond_to?(:warn, true)
|
|
595
|
+
|
|
596
|
+
@warned_types[type] = true
|
|
597
|
+
warn "[citrine] #{node.type} 的内容 block 返回了 #{type}(#{result.inspect})," \
|
|
598
|
+
"已按 to_s 渲染。建议写成插值:label { \"#{'{...}'}\" }"
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
def dispose(node)
|
|
602
|
+
node.owned_effects.each(&:dispose)
|
|
603
|
+
node.owned_effects.clear
|
|
604
|
+
node.children.dup.each { |child| dispose(child) }
|
|
605
|
+
node.children.clear
|
|
606
|
+
|
|
607
|
+
# 组件边界:子组件随之卸载(解绑全局键盘 + 跑 on_unmount),顺序是"先子后父"
|
|
608
|
+
if (child = node.rendered_component)
|
|
609
|
+
node.rendered_component = nil
|
|
610
|
+
unregister_window_keys(child)
|
|
611
|
+
child.run_unmount_hooks if child.respond_to?(:run_unmount_hooks)
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
# 透明容器(fragment / portal)无自身 DOM:子根挂在真实父容器上,不能对它做 detach
|
|
615
|
+
detach(node) unless TRANSPARENT_TYPES.include?(node.type)
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
# box 支持布局快捷参数:direction(stack/flow 的抽象)、gap
|
|
619
|
+
# 样式键在此后均为 snake_case(决策 #10,经 Style.normalize 归一)
|
|
620
|
+
# 值可以是 Proc(响应式属性,见 REACTIVE_PROPS)——由本节点的属性 Effect 负责重求值
|
|
621
|
+
def resolve_style(node)
|
|
622
|
+
style = (prop_value(node, node.props[:style]) || {}).dup
|
|
623
|
+
if node.type == :box
|
|
624
|
+
style[:display] ||= "flex"
|
|
625
|
+
# A7:显式 display 不是 flex(如 grid)时 direction 与它无关——flex_direction
|
|
626
|
+
# 写给 grid 容器是无效声明,还会让阅读者误以为方向控制生效了
|
|
627
|
+
if style[:display].to_s == "flex" && (direction = prop_value(node, node.props[:direction]))
|
|
628
|
+
style[:flex_direction] = direction == :column ? "column" : "row"
|
|
629
|
+
end
|
|
630
|
+
gap = prop_value(node, node.props[:gap])
|
|
631
|
+
style[:gap] = gap.is_a?(Numeric) ? "#{gap}px" : gap.to_s if gap
|
|
632
|
+
end
|
|
633
|
+
# Proc 求值出来的样式没经过 Component#emit,这里统一归一(幂等,静态样式重复归一无副作用)
|
|
634
|
+
Style.normalize(style)
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
# ── 响应式属性(G-2)──────────────────────────────────
|
|
638
|
+
|
|
639
|
+
# 求值一个 prop 值:Proc → 在该节点 owner 的上下文里求值(可调用组件方法、读信号)
|
|
640
|
+
def prop_value(node, value)
|
|
641
|
+
value.is_a?(Proc) ? node.owner.instance_exec(&value) : value
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
def reactive_props?(node)
|
|
645
|
+
node.props.any? { |key, value| value.is_a?(Proc) && REACTIVE_PROPS.include?(key) }
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
# ── S2-2:属性透传 ─────────────────────────────────────
|
|
649
|
+
|
|
650
|
+
def passthrough_prop?(name, node)
|
|
651
|
+
return false if ALWAYS_CONSUMED.include?(name)
|
|
652
|
+
return false if name.to_s.start_with?("on_")
|
|
653
|
+
|
|
654
|
+
consumed_on = WIDGET_VALUE_PROPS[name]
|
|
655
|
+
consumed_on ? !consumed_on.include?(node&.type) : true
|
|
656
|
+
end
|
|
657
|
+
|
|
658
|
+
# 未消费属性 → [[kebab-case 名, 字符串值]]。true → ""(空值属性);false / nil 不输出。
|
|
659
|
+
# DOM 与 SSR 两侧共用同一口径,保证输出一致。
|
|
660
|
+
def passthrough_props(node)
|
|
661
|
+
node.props.map do |name, value|
|
|
662
|
+
next unless passthrough_prop?(name, node)
|
|
663
|
+
next if value.nil? || value == false
|
|
664
|
+
|
|
665
|
+
[Style.kebab(name), value == true ? "" : value.to_s]
|
|
666
|
+
end.compact
|
|
667
|
+
end
|
|
668
|
+
|
|
669
|
+
# 响应式 style 会换掉整份内联样式:记录本次的键,返回需要显式清空的旧键
|
|
670
|
+
# (否则 `style: -> { { background: ok ? "green" : nil } }` 里的绿色会残留)
|
|
671
|
+
def track_style_keys(node, keys)
|
|
672
|
+
stale = (node.applied_style_keys || []) - keys
|
|
673
|
+
node.applied_style_keys = keys
|
|
674
|
+
stale
|
|
675
|
+
end
|
|
676
|
+
|
|
677
|
+
# 值类 prop / 透传属性收到 Proc 或 Signal 时提醒一次:它们不会被求值、
|
|
678
|
+
# 也不会订阅(on_* 的 Proc 是回调,不在此列;value:/checked: 传 Signal 是
|
|
679
|
+
# 受控值语义,也不在 warn 之列)。Signal 落进透传属性会被 to_s 成
|
|
680
|
+
# "#<Citrine::Signal…>" 输出——静默坏页面,必须提醒。
|
|
681
|
+
def warn_unreactive_proc(node)
|
|
682
|
+
bad = node.props.select do |key, value|
|
|
683
|
+
(value.is_a?(Proc) && !REACTIVE_PROPS.include?(key) &&
|
|
684
|
+
(WIDGET_VALUE_PROPS.key?(key) || passthrough_prop?(key, node))) ||
|
|
685
|
+
(value.is_a?(Signal) && passthrough_prop?(key, node))
|
|
686
|
+
end
|
|
687
|
+
return if bad.empty? || !respond_to?(:warn, true)
|
|
688
|
+
|
|
689
|
+
@warned_bad_props ||= {}
|
|
690
|
+
bad.each do |key, value|
|
|
691
|
+
next if @warned_bad_props[key]
|
|
692
|
+
|
|
693
|
+
@warned_bad_props[key] = true
|
|
694
|
+
message = if value.is_a?(Signal)
|
|
695
|
+
"[citrine] #{node.type} 的 prop :#{key} 收到 Signal,但它是透传属性:" \
|
|
696
|
+
"Signal 不会被解包,会渲染成 #<Citrine::Signal…>。请传 .get 后的值;" \
|
|
697
|
+
"要双向绑定请用受控值属性(value: / checked:)"
|
|
698
|
+
else
|
|
699
|
+
hint = key == :value ? "受控输入请传 Signal:value: signal(:draft)" : "请直接传值或 Signal"
|
|
700
|
+
"[citrine] #{node.type} 的 prop :#{key} 收到 Proc,但它不是响应式属性:" \
|
|
701
|
+
"Proc 不会被求值。支持 Proc 的只有 #{REACTIVE_PROPS.map { |k| ":#{k}" }.join(' / ')};#{hint}"
|
|
702
|
+
end
|
|
703
|
+
warn message
|
|
704
|
+
end
|
|
705
|
+
end
|
|
706
|
+
|
|
707
|
+
# ── 布局提醒(G-8)────────────────────────────────────
|
|
708
|
+
|
|
709
|
+
# 未显式声明方向、且**有多个子节点**的 box 计数:默认 row 是"面板塌成一条"那类
|
|
710
|
+
# 真机事故的根源(桩里没有布局引擎,测不出来),开发模式下按页汇报一次。
|
|
711
|
+
# 空容器/单子容器不会塌,不提醒——避免用噪音换信任。
|
|
712
|
+
def note_undirected_box(node)
|
|
713
|
+
return unless node.type == :box && !node.props.key?(:direction) && node.children.size > 1
|
|
714
|
+
|
|
715
|
+
@undirected_boxes = (@undirected_boxes || 0) + 1
|
|
716
|
+
end
|
|
717
|
+
|
|
718
|
+
def warn_undirected_boxes
|
|
719
|
+
count = @undirected_boxes.to_i
|
|
720
|
+
@undirected_boxes = 0
|
|
721
|
+
return if count.zero? || !Citrine.dev_mode? || !respond_to?(:warn, true)
|
|
722
|
+
|
|
723
|
+
warn "[citrine] 本次挂载有 #{count} 处 box 未声明方向(默认横排 row;内容一多,真机上会塌成一条):" \
|
|
724
|
+
"竖排请写 stack { },横排请写 row { }(等价于 box(direction: :column|:row))。" \
|
|
725
|
+
"本提示只在开发模式出现。"
|
|
726
|
+
end
|
|
727
|
+
|
|
728
|
+
# ── 生命周期与全局键盘(G-9 / G-10)────────────────────
|
|
729
|
+
|
|
730
|
+
# 组件与它的根节点/渲染器互相记住:卸载时才知道该拆哪棵树
|
|
731
|
+
def attach_root_ref(component, root)
|
|
732
|
+
return unless component.respond_to?(:root=)
|
|
733
|
+
|
|
734
|
+
component.root = root
|
|
735
|
+
component.renderer = self if component.respond_to?(:renderer=)
|
|
736
|
+
end
|
|
737
|
+
|
|
738
|
+
# ref: :name → component.refs[:name] = 平台句柄(DOM 下是元素本身)
|
|
739
|
+
def register_ref(node)
|
|
740
|
+
name = node.props[:ref]
|
|
741
|
+
return unless name && node.owner.respond_to?(:refs)
|
|
742
|
+
|
|
743
|
+
node.owner.refs[name] = node.dom
|
|
744
|
+
end
|
|
745
|
+
|
|
746
|
+
def register_window_keys(component)
|
|
747
|
+
return unless component.respond_to?(:run_unmount_hooks)
|
|
748
|
+
|
|
749
|
+
component.class.window_key_handlers.each { |handler| register_window_key(component, handler) }
|
|
750
|
+
end
|
|
751
|
+
|
|
752
|
+
# ── 信号驱动重跑的落位(S1-2)────────────────────────────
|
|
753
|
+
# mount / reusable 的 attach 一律追加到父容器末尾;子组件 view 的信号重跑发生在
|
|
754
|
+
# 父块渲染之外,reconcile 完必须把根挪回原渲染位,否则它会排到后续兄弟后面。
|
|
755
|
+
|
|
756
|
+
# anchor = 原位后面的兄弟 Node;nil 表示本来就该排最后(attach 已就位,无需动)
|
|
757
|
+
def reposition(node, parent, anchor)
|
|
758
|
+
attach_before(node, parent, anchor) if anchor
|
|
759
|
+
end
|
|
760
|
+
|
|
761
|
+
# 平台钩子:把 node 挪到 parent 内 anchor 之前。默认无操作——
|
|
762
|
+
# 线性追加式渲染器按渲染序落位(如 Canvas 全量重绘按树遍历),无需移动。
|
|
763
|
+
def attach_before(_node, _parent, _anchor)
|
|
764
|
+
nil
|
|
765
|
+
end
|
|
766
|
+
|
|
767
|
+
# Portal 宿主解析(S1-5):target 为 nil → 平台默认宿主(DOM 下是 body)。
|
|
768
|
+
# 显式给了 target 却解析不到时由平台实现处理(DOM 侧抛错,不静默回退)。
|
|
769
|
+
def resolve_portal_host(_target)
|
|
770
|
+
raise NotImplementedError, "#{self.class} 不支持 portal"
|
|
771
|
+
end
|
|
772
|
+
|
|
773
|
+
# ── 平台钩子 ───────────────────────────────────────────
|
|
774
|
+
|
|
775
|
+
def reactive?
|
|
776
|
+
true
|
|
777
|
+
end
|
|
778
|
+
|
|
779
|
+
def setup_root(_root, _element)
|
|
780
|
+
raise NotImplementedError
|
|
781
|
+
end
|
|
782
|
+
|
|
783
|
+
def create_dom(_node)
|
|
784
|
+
raise NotImplementedError
|
|
785
|
+
end
|
|
786
|
+
|
|
787
|
+
def attach(_node, _parent)
|
|
788
|
+
raise NotImplementedError
|
|
789
|
+
end
|
|
790
|
+
|
|
791
|
+
def detach(_node)
|
|
792
|
+
raise NotImplementedError
|
|
793
|
+
end
|
|
794
|
+
|
|
795
|
+
# 应用属性。必须是**幂等**的:响应式属性重跑时会再次调用,
|
|
796
|
+
# 但只重设属性、不重建子树。事件监听请放 bind_events。
|
|
797
|
+
def apply_props(_node)
|
|
798
|
+
raise NotImplementedError
|
|
799
|
+
end
|
|
800
|
+
|
|
801
|
+
# 绑定事件监听(挂载时调用一次)。DOM 渲染器按需绑定,并在响应式属性重跑时
|
|
802
|
+
# 补齐新出现的处理器(见 DomRenderer#ensure_events)
|
|
803
|
+
def bind_events(_node)
|
|
804
|
+
nil
|
|
805
|
+
end
|
|
806
|
+
|
|
807
|
+
# 全局键盘(window 级):DOM 渲染器实现;其它平台默认无操作
|
|
808
|
+
def register_window_key(_component, _handler)
|
|
809
|
+
nil
|
|
810
|
+
end
|
|
811
|
+
|
|
812
|
+
def unregister_window_keys(_component)
|
|
813
|
+
nil
|
|
814
|
+
end
|
|
815
|
+
|
|
816
|
+
def set_text(_node, _text)
|
|
817
|
+
raise NotImplementedError
|
|
818
|
+
end
|
|
819
|
+
|
|
820
|
+
def setup_widget(_node)
|
|
821
|
+
nil
|
|
822
|
+
end
|
|
823
|
+
|
|
824
|
+
def finalize(_node)
|
|
825
|
+
nil
|
|
826
|
+
end
|
|
827
|
+
end
|
|
828
|
+
end
|