之前的文章已经说明过 Qwik 更新的 SSR 部分,相关的内容就不在这里过多描述,这里主要是探讨 Lazy 部分。依然会采用同样的 Example。
Example
下面我们以 Qwik 的 init project 代码来作为案例,通过 count 的加减来去理解 state 和 ui 的关联
import { component$, useSignal } from '@builder.io/qwik'
import styles from './counter.module.css'
export default component$(() => {
const count = useSignal(70)
return (
<div class={styles['counter-wrapper']}>
<button
class="button-dark button-small"
onClick$={() => (count.value -= 1)}
>
-
</button>
{count.value + 12}
{count.value}
<button
class="button-dark button-small"
onClick$={() => (count.value += 1)}
>
+
</button>
</div>
)
})
在 Dev 环境下打包出来的界面
当用户点击 + 之后,Qwik 会去加载三个文件:
- 切割出来的用户交互行为(click)的回调函数的 JS,简称 click.js
@builder.io/qwik/core
是 Qwik 的核心代码@builder.io/qwik/build
就是环境变量
// click.js
import { useLexicalScope } from '/node_modules/@builder.io/qwik/core.mjs?v=e747bb0b'
export const counter_component_div_button_onClick_1_LkCVrojX09Y = () => {
const [count] = useLexicalScope()
return (count.value += 1)
}
// @builder.io/qwik/build
export const isServer = false
export const isBrowser = true
export const isDev = true
click.js 以及 qwik/build 都很简单,这里重点是要去理解 useLexicalScope
是怎么做到绑定 state 以及 count.value += 1
为什么界面也能跟着 +1。
代码部分会把异常提示的 console 先去掉,如
assertDefined
、assertQrl
,因为对逻辑影响不大,去掉可以更好的理解代码.
useLexicalScope
const useLexicalScope = () => {
const context = getInvokeContext()
let qrl = context.$qrl$
if (!qrl) {
const el = context.$element$
const container = getWrappingContainer(el)
qrl = parseQRL(decodeURIComponent(String(context.$url$)), container)
resumeIfNeeded(container)
const elCtx = getContext(el, _getContainerState(container))
inflateQrl(qrl, elCtx)
} else {
// assertQrl assertDefined
}
return qrl.$captureRef$
}
我们按源码去理解它里面的所用到的每个函数,虽然很多函数通过名字都可以知道它的作用,但是也会简单介绍。
getInvokeContex
const seal = obj => {
if (qDev) {
Object.seal(obj) // 封闭一个对象,阻止添加新属性并将所有现有属性标记为不可配置。
}
}
const newInvokeContext = (locale, hostElement, element, event, url) => {
const ctx = {
$seq$: 0,
$hostElement$: hostElement,
$element$: element,
$event$: event,
$url$: url,
$locale$: locale,
$qrl$: undefined,
// ... code
}
seal(ctx)
return ctx // context 的内容
}
const newInvokeContextFromTuple = context => {
const element = context[0]
const container = element.closest(QContainerSelector)
const locale = container?.getAttribute(QLocaleAttr) || undefined
locale && setLocale(locale) // 用于设置 lang,和本质上的逻辑关系不大
return newInvokeContext(locale, undefined, element, context[1], context[2])
}
let _context
const tryGetInvokeContext = () => {
if (!_context) {
const context =
typeof document !== 'undefined' && document && document.__q_context__
if (!context) {
return undefined
}
if (isArray(context)) {
return (document.__q_context__ = newInvokeContextFromTuple(context))
}
return context
}
return _context
}
const getInvokeContext = () => {
const ctx = tryGetInvokeContext()
return ctx
}
getInvokeContext 就是只是获取一个 context,可以是缓存中的 _context
或者 document.__q_context__
。
element 首次恢复时用的是 document.q_context
,它在 qwikload 中处理,在 import 前定义,finally 后进行重置。
Flow
getWrappingContainer
// 匹配特定选择器且离当前元素最近的祖先元素(可以是当前元素本身)
const getWrappingContainer = el => el.closest(QContainerSelector)
getWrappingContainer,就只是获取 el container
parseQRL
const createQRL = (
chunk,
symbol,
symbolRef,
symbolFn,
capture,
captureRef,
refSymbol
) => {
// ... 函数、变量的定义
const invokeQRL = async function (...args) {
const fn = invokeFn.call(this, tryGetInvokeContext())
const result = await fn(...args)
return result
}
const resolvedSymbol = refSymbol ?? symbol
const hash = getSymbolHash(resolvedSymbol)
const QRL = invokeQRL
const methods = {
getSymbol: () => resolvedSymbol,
// ... code
$capture$: capture,
$captureRef$: captureRef,
dev: null,
}
const qrl = Object.assign(invokeQRL, methods)
seal(qrl)
return qrl
}
const parseQRL = (qrl, containerEl) => {
const endIdx = qrl.length
const hashIdx = indexOf(qrl, 0, '#')
const captureIdx = indexOf(qrl, hashIdx, '[')
const chunkEndIdx = Math.min(hashIdx, captureIdx)
const chunk = qrl.substring(0, chunkEndIdx)
const symbolStartIdx = hashIdx == endIdx ? hashIdx : hashIdx + 1
const symbolEndIdx = captureIdx
const symbol =
symbolStartIdx == symbolEndIdx
? 'default'
: qrl.substring(symbolStartIdx, symbolEndIdx)
const captureStartIdx = captureIdx
const captureEndIdx = endIdx
const capture =
captureStartIdx === captureEndIdx
? EMPTY_ARRAY
: qrl.substring(captureStartIdx + 1, captureEndIdx - 1).split(' ')
const iQrl = createQRL(chunk, symbol, null, null, capture, null, null)
if (containerEl) {
iQrl.$setContainer$(containerEl)
}
return iQrl
}
parseQRL,就是将 qurl,也就是 on:click="./chunk.js#onClick"
中的 ./chunk.js#onClick
进行处理,解析出来一个 object。
resumeIfNeeded
const directGetAttribute = (el, prop) => el.getAttribute(prop)
const resumeIfNeeded = containerEl => {
const isResumed = directGetAttribute(containerEl, QContainerAttr)
if (isResumed === 'paused') {
resumeContainer(containerEl)
}
}
resumeIfNeeded 就是判断 containerEl 的 q:container="paused"
,如果是 paused
则进行 resume
resumeContainer
const resumeContainer = containerEl => {
const pauseState = containerEl['_qwikjson_'] ?? getPauseState(containerEl) // qwik/json
containerEl['_qwikjson_'] = null
const doc = getDocument(containerEl)
const isDocElement = containerEl === doc.documentElement
const parentJSON = isDocElement ? doc.body : containerEl
// const getQwikInlinedFuncs 获取 q:func="qwik/json" 的 script
const inlinedFunctions = getQwikInlinedFuncs(parentJSON)
const containerState = _getContainerState(containerEl)
moveStyles(containerEl, containerState)
// Collect all elements
const elements = new Map()
const text = new Map()
let node = null
let container = 0
// Collect all virtual elements
const elementWalker = doc.createTreeWalker(containerEl, SHOW_COMMENT$1)
while ((node = elementWalker.nextNode())) {
// ... 遍历收集 elements 和 text
}
const slotPath = containerEl.getElementsByClassName('qc📦').length !== 0
// Collect all q:id dom
containerEl.querySelectorAll('[q\\:id]').forEach(el => {
if (slotPath && el.closest('[q\\:container]') !== containerEl) {
return
}
const id = directGetAttribute(el, ELEMENT_ID)
const index = strToInt(id)
elements.set(index, el)
})
const parser = createParser(containerState, doc) // 根据不同的 prefix 使用不同的 serializer
const finalized = new Map() // 缓存 obj
const revived = new Set()
const getObject = id => {
if (finalized.has(id)) {
return finalized.get(id)
}
return computeObject(id)
}
const computeObject = id => {
// Handle elements
if (id.startsWith('#')) {
// ... return elements 中 的 el
return rawElement
} else if (id.startsWith('@')) {
// ... return 执行的 function
return func
} else if (id.startsWith('*')) {
// ... return dom 的 text
return str
}
const index = strToInt(id)
const objs = pauseState.objs // qwik/json 中的 objs
let value = objs[index]
if (isString(value)) {
value = value === UNDEFINED_PREFIX ? undefined : parser.prepare(value)
}
let obj = value
for (let i = id.length - 1; i >= 0; i--) {
// ... 对 obj 进行处理
}
finalized.set(id, obj)
if (!isPrimitive(value) && !revived.has(index)) {
revived.add(index)
reviveSubscriptions(
value,
index,
pauseState.subs,
getObject,
containerState,
parser
)
reviveNestedObjects(value, getObject, parser)
}
return obj
}
containerState.$elementIndex$ = 100000
containerState.$pauseCtx$ = {
getObject,
meta: pauseState.ctx,
refs: pauseState.refs,
}
// 处理把 containerEl q:container 改为 "resumed" 以及执行 qresume 的注册事件
directSetAttribute(containerEl, QContainerAttr, 'resumed')
emitEvent$1(containerEl, 'qresume', undefined, true)
}
resumeContainer,它就跟命名一样,用来恢复当前的 Container
- 收集 element,包含属性为 q:id 和 Comment(
<!--qv q:id=4-->
) 节点 - 通过 createParser 生成 parser
- 生成并处理 containerState,包含
pauseCtx
、elementIndex
等信息 - getObject,先查询 finalized,没有缓存的 obj,再通过 computeObject 生成
- computeObject 根据 id 来做处理
- startsWith('#'), return elements 中 的 el
- startsWith('@'), return 执行的 function
- startsWith('*'), return dom 的 text
- 上述情况外则 return
parser.prepare(value)
生成的 obj,并执行reviveSubscriptions
(将 parseSubscription 添加到 serializers 的订阅) 和reviveNestedObjects
(将 obj 的 $func$、$args$ 进行处理)不同的情况会使用不同 serializers,demo 中的会使用 SignalSerializer 和 DerivedSignalSerializer
- 处理把 containerEl q:container 改为 "resumed" 以及执行 qresume 的注册事件
reviveSubscriptions
const parseSubscription = (sub, getObject) => {
const parts = sub.split(' ')
const type = parseInt(parts[0], 10)
const host = getObject(parts[1])
if (!host) {
return undefined
}
if (isSubscriberDescriptor(host) && !host.$el$) {
return undefined
}
const subscription = [type, host]
if (type === 0) {
subscription.push(
parts.length === 3 ? decodeURI(parts[parts.length - 1]) : undefined
)
} else if (type <= 2) {
subscription.push(
getObject(parts[2]),
getObject(parts[3]),
parts[4],
parts[5]
)
} else if (type <= 4) {
subscription.push(getObject(parts[2]), getObject(parts[3]), parts[4])
}
return subscription
}
const reviveSubscriptions = (
value,
i,
objsSubs,
getObject,
containerState,
parser
) => {
// objsSubs = [['3 #9 2 #9', '3 #a 4 #a']]
const subs = objsSubs[i]
if (subs) {
const converted = []
let flag = 0
// 将 parseSubscription push 到 converted,subs= ['3 #9 2 #9', '3 #a 4 #a']
for (const sub of subs) {
// ... code
const parsed = parseSubscription(sub, getObject)
if (parsed) {
converted.push(parsed)
}
}
if (flag > 0) {
setObjectFlags(value, flag)
}
// 重点在执行这个 parser.subs,将 converted 添加到订阅中
if (!parser.subs(value, converted)) {
// ... code
}
}
}
reviveSubscriptions,将 qwik/json 中的 subs 进行处理,通过 parseSubscription 生成一个带有 type 以及 getObject() 的数组(converted),并将 converted 和 value 添加到 parser 的订阅中。
reviveNestedObjects
const reviveNestedObjects = (obj, getObject, parser) => {
if (parser.fill(obj, getObject)) {
return
}
if (obj && typeof obj == 'object') {
if (isArray(obj)) {
for (let i = 0; i < obj.length; i++) {
obj[i] = getObject(obj[i])
}
} else if (isSerializableObject(obj)) {
for (const key in obj) {
obj[key] = getObject(obj[key])
}
}
}
}
reviveNestedObjects 用来对 obj 进行处理,
SignalSerializer
const SignalSerializer = {
$prefix$: '\u0012',
// ... code
$prepare$: (data, containerState) => {
return new SignalImpl(
data,
containerState?.$subsManager$?.$createManager$(),
0
)
},
$subs$: (signal, subs) => {
signal[QObjectManagerSymbol].$addSubs$(subs)
},
$fill$: (signal, getObject) => {
signal.untrackedValue = getObject(signal.untrackedValue)
},
}
class SignalBase {}
class SignalImpl extends SignalBase {
constructor(v, manager, flags) {
super()
this[_a$1] = 0
this.untrackedValue = v
this[QObjectManagerSymbol] = manager
this[QObjectSignalFlags] = flags
}
// ... valueOf toString toJSON
get value() {
if (this[QObjectSignalFlags] & SIGNAL_UNASSIGNED) {
throw SignalUnassignedException
}
const sub = tryGetInvokeContext()?.$subscriber$
if (sub) {
this[QObjectManagerSymbol].$addSub$(sub)
}
return this.untrackedValue
}
set value(v) {
// ...code
const manager = this[QObjectManagerSymbol]
const oldValue = this.untrackedValue
if (manager && oldValue !== v) {
this.untrackedValue = v
manager.$notifySubs$()
}
}
}
SignalSerializer 用来生成 SignalImpl,用在 DerivedSignalSerializer 的 this.$func$ 作为 args,并且它具有发布订阅功能,在 set 的时候执行 manager.$notifySubs$(),而 manager 是由 _getContainerState 生成的 LocalSubscriptionManager,它可以执行 notifyChange 来进行 UI 的更新。
DerivedSignalSerializer
class SignalDerived extends SignalBase {
constructor($func$, $args$, $funcStr$) {
super()
this.$func$ = $func$
this.$args$ = $args$
this.$funcStr$ = $funcStr$
}
get value() {
return this.$func$.apply(undefined, this.$args$)
}
}
const DerivedSignalSerializer = {
$prefix$: '\u0011',
// ... code
$prepare$: data => {
const ids = data.split(' ')
const args = ids.slice(0, -1)
const fn = ids[ids.length - 1]
return new SignalDerived(fn, args, fn)
},
$fill$: (fn, getObject) => {
fn.$func$ = getObject(fn.$func$)
fn.$args$ = fn.$args$.map(getObject)
},
}
DerivedSignalSerializer 用来生成 SignalDerived,SignalDerived 和 SignalImpl 有点不同,它主要是用 $func$ 和 $args$ 去 return value,它类似于 Recoil 的 Selector,SignalImpl 类似于 Atom。
Flow
getContext
const getDomListeners = (elCtx, containerEl) => {
const attributes = elCtx.$element$.attributes
const listeners = []
for (let i = 0; i < attributes.length; i++) {
const { name, value } = attributes.item(i)
if (
name.startsWith('on:') ||
name.startsWith('on-window:') ||
name.startsWith('on-document:')
) {
const urls = value.split('\n')
for (const url of urls) {
const qrl = parseQRL(url, containerEl)
if (qrl.$capture$) {
inflateQrl(qrl, elCtx)
}
listeners.push([name, qrl])
}
}
}
return listeners
}
const createContext = element => {
const ctx = {
$flags$: 0,
$id$: '',
$element$: element,
$refMap$: [],
li: [],
// ... code
}
element[Q_CTX] = ctx
return ctx
}
const getContext = (el, containerState) => {
const ctx = tryGetContext(el)
if (ctx) {
return ctx
}
const elCtx = createContext(el)
const elementID = directGetAttribute(el, 'q:id')
if (elementID) {
const pauseCtx = containerState.$pauseCtx$
elCtx.$id$ = elementID
if (pauseCtx) {
const { getObject, meta, refs } = pauseCtx
if (isElement(el)) {
const refMap = refs[elementID]
if (refMap) {
elCtx.$refMap$ = refMap.split(' ').map(getObject)
elCtx.li = getDomListeners(elCtx, containerState.$containerEl$)
}
} else {
// ... code
}
}
}
return elCtx
}
getContext 主要是获取 element 的 context,如果没有就通过 createContext 生成的 context
- 设置 $refMap$,map 执行 getObject 得到一个数组,后续 infalteQrl 会用到
- 通过 getDomListeners 来获取 dom 事件
Flow
_getContainerState
class LocalSubscriptionManager {
constructor($groupToManagers$, $containerState$, initialMap) {
this.$groupToManagers$ = $groupToManagers$;
this.$containerState$ = $containerState$;
this.$subs$ = [];
if (initialMap) {
this.$addSubs$(initialMap);
}
}
$addSub$(sub, key) {
// ... code
this.$addToGroup$(group, this);
}
$notifySubs$(key) {
const subs = this.$subs$;
for (const sub of subs) {
// ... code
notifyChange(sub, this.$containerState$);
}
}
// ... $addSubs$(subs) $addToGroup$(group, manager) $unsubGroup$(group)
}
const createSubscriptionManager = containerState => {
const groupToManagers = new Map()
const manager = {
$groupToManagers$: groupToManagers,
$createManager$: initialMap => {
return new LocalSubscriptionManager(groupToManagers, containerState, initialMap)
},
$clearSub$: group => {
const managers = groupToManagers.get(group)
if (managers) {
for (const manager of managers) {
manager.$unsubGroup$(group)
}
groupToManagers.delete(group)
managers.length = 0
}
},
}
return manager
}
const createContainerState = (containerEl, base) => {
const containerState = {
$containerEl$: containerEl,
$elementIndex$: 0,
...
}
containerState.$subsManager$ = createSubscriptionManager(containerState)
return containerState
}
const _getContainerState = containerEl => {
let set = containerEl[CONTAINER_STATE]
if (!set) {
containerEl[CONTAINER_STATE] = set = createContainerState(
containerEl,
directGetAttribute(containerEl, 'q:base') ?? '/'
)
}
return set
}
_getContainerState 在没有 state 的时候生成一个带有发布订阅功能的 state,LocalSubscriptionManager 就是发布订阅的 class,主要是它的 $notifySubs$ 会调用 notifyChange。
notifyChange
const scheduleFrame = containerState => {
if (containerState.$renderPromise$ === undefined) {
containerState.$renderPromise$ = getPlatform().nextTick(() =>
renderMarked(containerState)
)
}
return containerState.$renderPromise$
}
const notifySignalOperation = (op, containerState) => {
const activeRendering = containerState.$hostsRendering$ !== undefined
containerState.$opsNext$.add(op)
if (!activeRendering) {
scheduleFrame(containerState)
}
}
const notifyChange = (subAction, containerState) => {
if (subAction[0] === 0) {
const host = subAction[1]
if (isSubscriberDescriptor(host)) {
notifyWatch(host, containerState)
} else {
notifyRender(host, containerState)
}
} else {
notifySignalOperation(subAction, containerState)
}
}
notifyChange 主要用来调用 renderMarked,先收集了 $opsNext$(dom 的处理操作),并且在 !activeRendering 的时候使用 nextTick 来异步执行 renderMarked。
renderMarked
const renderMarked = async containerState => {
const doc = getDocument(containerState.$containerEl$)
try {
const rCtx = createRenderContext(doc, containerState)
const staticCtx = rCtx.$static$
const hostsRendering = (containerState.$hostsRendering$ = new Set(
containerState.$hostsNext$
))
containerState.$hostsNext$.clear()
await executeWatchesBefore(containerState, rCtx)
containerState.$hostsStaging$.forEach(host => {
hostsRendering.add(host)
})
containerState.$hostsStaging$.clear()
const signalOperations = Array.from(containerState.$opsNext$)
containerState.$opsNext$.clear()
const renderingQueue = Array.from(hostsRendering)
sortNodes(renderingQueue)
for (const elCtx of renderingQueue) {
const el = elCtx.$element$
if (!staticCtx.$hostElements$.has(el)) {
if (elCtx.$componentQrl$) {
staticCtx.$roots$.push(elCtx)
await renderComponent(rCtx, elCtx, getFlags(el.parentElement))
}
}
}
signalOperations.forEach(op => {
executeSignalOperation(staticCtx, op)
})
// Add post operations
staticCtx.$operations$.push(...staticCtx.$postOperations$)
// Early exist, no dom operations
if (staticCtx.$operations$.length === 0) {
printRenderStats(staticCtx)
await postRendering(containerState, rCtx)
return
}
await executeContextWithTransition(staticCtx)
printRenderStats(staticCtx)
return postRendering(containerState, rCtx)
} catch (err) {
logError(err)
}
}
renderMarked 执行需要 update 的元素,所有的 dom operation 都在这里执行。operations 就是所有 dom 操作,然后遍历 apply 执行。
- 处理 $hostsRendering$,用于在下次执行时判断当前是否 activeRendering
- 执行 executeWatchesBefore,如果在 notifyWatch 中有添加的 watch task
- 对 node 做排序处理
- $opsNext$ 循环执行 executeSignalOperation,根据不同的 type 来生成对应的 $operations$
- executeContextWithTransition,执行生成的 $operations$
- 执行 postRendering,重置 containerState 的各种信息和 executeWatchesAfter,return rendering 的状态
executeSignalOperation
const setProperty = (staticCtx, node, key, value) => {
staticCtx.$operations$.push({
$operation$: _setProperty,
$args$: [node, key, value],
})
}
const _setProperty = (node, key, value) => {
try {
node[key] = value == null ? '' : value // 将 node[key] 设置为 value
if (value == null && isNode$1(node) && isElement$1(node)) {
node.removeAttribute(key)
}
} catch (err) {
// logError
}
}
const executeSignalOperation = (staticCtx, operation) => {
try {
const type = operation[0]
switch (type) {
case 1:
case 2: {
let elm
let hostElm
if (type === 1) {
elm = operation[1]
hostElm = operation[3]
} else {
elm = operation[3]
hostElm = operation[1]
}
const elCtx = tryGetContext(elm)
if (elCtx == null) {
return
}
const prop = operation[4]
const isSVG = elm.namespaceURI === SVG_NS
staticCtx.$containerState$.$subsManager$.$clearSignal$(operation)
let value = trackSignal(operation[2], operation.slice(0, -1))
if (prop === 'class') {
value = serializeClassWithHost(value, tryGetContext(hostElm))
} else if (prop === 'style') {
value = stringifyStyle(value)
}
const vdom = getVdom(elCtx)
if (vdom.$props$[prop] === value) {
return
}
vdom.$props$[prop] = value
return smartSetProperty(staticCtx, elm, prop, value, isSVG)
}
case 3:
case 4: {
const elm = operation[3]
if (!staticCtx.$visited$.includes(elm)) {
staticCtx.$containerState$.$subsManager$.$clearSignal$(operation)
const value = trackSignal(operation[2], operation.slice(0, -1))
return setProperty(staticCtx, elm, 'data', jsxToString(value))
}
}
}
} catch (e) {
// Ignore
}
}
executeContextWithTransition
const executeDOMRender = staticCtx => {
for (const op of staticCtx.$operations$) {
op.$operation$.apply(undefined, op.$args$) // 这个就是 UI 更新的地方
}
resolveSlotProjection(staticCtx)
}
const executeContextWithTransition = async ctx => {
// try to use `document.startViewTransition`
if (isBrowser && !qTest) {
if (document.__q_view_transition__) {
document.__q_view_transition__ = undefined
if (document.startViewTransition) {
await document.startViewTransition(() => executeDOMRender(ctx)).finished
return
}
}
}
executeDOMRender(ctx) // fallback
}
executeContextWithTransition 执行之前生成的 $operations$,也就是 UI 的更新
Flow
infalteQrl
const inflateQrl = (qrl, elCtx) => {
// `./chunk#symbol[captures]
return (qrl.$captureRef$ = qrl.$capture$.map(idx => {
const int = parseInt(idx, 10)
const obj = elCtx.$refMap$[int]
return obj
}))
}
infalteQrl 就是处理 qrl,将 $refMap$ 中对应的 obj(getObject)return,click 中的 callback 就是操作这个 obj。
Lazy Flow
Conclusion
结合前面的 SSR 部分,就是整个 State 的更新机制,Lazy 的部分相对前面的更加复杂,它包含 Core 的部分。也可以通过代码看出,它相对于 React 更新的性能会更好,因为直接操作对应的 Dom。
它目前 version 是 1.1.5
还是个比较新的框架,上面内容也都是它当前 version 的代码逻辑,未来可能会调整也说不定,但是它一些做法和理念还是非常好的
- 将需要的 data(qwik/json q:func) 提前 render 到 html 中
- 编译时已经将需要用到的交互、state 等都可以做优化处理
- 尽可能的 lazy,这会使得更关注代码的分离,也更好的去 split 代码