When we register the side function with the response context through effect
, the dependencies will be automatically collected when the responsive object is accessed in the side effect function, and the execution of the side effect function will be automatically triggered after the corresponding responsive property changes. .
// ./effect.ts
export funciton effect<T = any>(
fn: () => T,
options?: ReactiveEffectOptions
): ReactiveEffectRunner {
if ((fn as ReactiveEffectRunner).effect) {
fn = (fn as ReactiveEffectRunner).effect.fn
}
const _effect = new ReactiveEffect(fn)
if (options) {
extend(_effect, options)
if (options.scope) recordEffectScope(_effect, options.scope)
}
// 默认是马上执行副作用函数收集依赖,但可通过lazy属性延迟副作用函数的执行,延迟依赖收集。
if (!options || !options.lazy) {
_effect.run()
}
// 类型为ReactiveEffectRunner的runner是一个绑定this的函数
const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
runner.effect = _effect
return runner
}
effect
The code of the function is very small, the main process is
- Will build
ReactiveEffect
object based on side effect function - If it is the default mode, the
ReactiveEffect
object'srun
method will be called immediately to execute the side effect function.
But here we have a few questions
-
ReactiveEffectRunner
what is it? -
ReactiveEffect
What exactly is the generated object? Obviously theReactiveEffect
run
method of ---7f0b8670866f825cc2cd59f6e6406120--- is where the dream begins. What does it do? - What is the function of configuration items
scope
andrecordEffectScope
?
ReactiveEffectRunner
what is it?
// ./effect.ts
// ReactiveEffectRunner是一个函数,而且有一个名为effect的属性且其类型为RectiveEffect
export interface ReactiveEffectRunner<T = any> {
(): T
effect: ReactiveEffect
}
ReactiveEffect
What exactly is the generated object?
// 用于记录位于响应上下文中的effect嵌套层次数
let effectTrackDepth = 0
// 二进制位,每一位用于标识当前effect嵌套层级的依赖收集的启用状态
export left trackOpBit = 1
// 表示最大标记的位数
const maxMarkerBits = 30
const effectStack: ReactiveEffect[] = []
let activeEffect: ReactiveEffect | undefined
export class ReactiveEffect<T = any> {
// 用于标识副作用函数是否位于响应式上下文中被执行
active = true
// 副作用函数持有它所在的所有依赖集合的引用,用于从这些依赖集合删除自身
deps: Dep[] = []
// 默认为false,而true表示若副作用函数体内遇到`foo.bar += 1`则无限递归执行自身,直到爆栈
allowRecurse?: boolean
constructor(
public fn: () => T,
public scheduler: EffectScheduler | null = null,
scope?: EffectScope | null
) {
recordEffectScope(this, scope)
}
run() {
/**
* 若当前ReactiveEffect对象脱离响应式上下文,那么其对应的副作用函数被执行时不会再收集依赖,并且其内部访问的响应式对象发生变化时,也会自动触发该副作用函数的执行
*/
if (!this.active) {
return this.fn()
}
// 若参与响应式上下文则需要先压栈
if (!effectStack.includes(this)) {
try {
// 压栈的同时必须将当前ReactiveEffect对象设置为活跃,即程序栈中当前栈帧的意义。
effectStack.push(activeEffect = this)
enableTracking()
trackOpBit = 1 << ++effectTrackDepth
if (effectTrackDepth <= maxMarkerBits) {
// 标记已跟踪过的依赖
initDepMarkers(this)
}
else {
cleanupEffect(this)
}
return this.fn()
}
finally {
if (effectTrackDepth <= maxMarkerBits) {
/**
* 用于对曾经跟踪过,但本次副作用函数执行时没有跟踪的依赖,采取删除操作。
* 即,新跟踪的 和 本轮跟踪过的都会被保留。
*/
finalizeDepMarkers(this)
}
trackOpBit = 1 << --effectTrackDepth
resetTracking()
// 最后当然弹栈,把控制权交还给上一个栈帧咯
effectStack.pop()
const n = effectStack.length
activeEffect = n > 0 ? effectStack[n - 1] : undefined
}
}
/**
* 让当前ReactiveEffect对象脱离响应式上下文,请记住这是一去不回头的操作哦!
*/
stop() {
if (this.active) {
cleanupEffect(this)
this.active = false
}
}
}
}
In order to cope with the nesting effect
the ReactiveEffect object currently in the response context is pushed into the stack structure effectStack: ReactiveEffect[]
, and the stack is popped after the current side effect function is executed. In addition, although we registered the side-effect function into the response context through the effect
function, we can still get it out of the response context by calling the stop
method.
function cleanupEffect(effect: ReactiveEffect) {
const { deps } = effect
if (deps.length) {
// 将当前ReactiveEffect对象从它依赖的响应式属性的所有Deps中删除自己,那么当这些响应式属性发生变化时则不会遍历到当前的ReactiveEffect对象
for (let i = 0; i < deps.length; ++i) {
deps[i].delete(effect)
}
// 当前ReactiveEffect对象不再参与任何响应了
deps.length = 0
}
}
Before and after the side effect function is executed, we will see that the functions enableTracking()
and resetTracking()
are called respectively, which means enableTracking()
The code after execution will enable dependency collection , resetTracking()
means that the following code will execute the switch of whether to collect dependencies before recovery. To understand them one has to combine pauseTracking()
with the actual scenario description:
let shouldTrack = true
const trackStack: boolean[] = []
export function enableTracking() {
trackStack.push(shouldTrack)
shouldTrack = true
}
export function resetTracking() {
const last = trackStack.pop()
shouldTrack = last === undefined ? true : last
}
export function pauseTracking() {
trackStack.push(shouldTrack)
shouldTrack = false
}
Suppose we have the following scenario
const values = reactive([1,2,3])
effect(() => {
values.push(1)
})
Since the length
attribute of the proxy object is accessed internally when push
is executed, and the length
value is modified, the side effect function will be continuously executed until an exception is Uncaught RangeError: Maximum call stack size exceeded
is the same as (function error(){ error() })()
Continuously calling itself leads to insufficient stack space. And @vue/reactivity is handled in the following way
;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
pauseTracking()
const res = (toRaw(this) as any)[key].apply(this, args)
resetTracking()
return res
}
})
That is, through pauseTracking()
pause push
internal unexpected dependency collection, ie push
will only trigger side effects that depend on other forms of length
function execution. Then go back to the previous tracking state by resetTracking()
.
Finally, before executing the side effect function return this.fn()
, there are actually a few incomprehensible statements
try {
trackOpBit = 1 << ++effectTrackDepth
if (effectTrackDepth <= maxMarkerBits) {
initDepMarkers(this)
}
else {
cleanupEffect(this)
}
return this.fn()
}
finally {
if (effectTrackDepth <= maxMarkerBits) {
finalizeDepMarkers(this)
}
trackOpBit = 1 << --effectTrackDepth
}
We can simplify it to
try {
cleanupEffect(this)
return this.fn()
}
finally {}
Why do you need to clean up all dependencies before executing side-effecting functions? We can consider the following situation:
const state = reactive({ show: true, values: [1,2,3] })
effect(() => {
if (state.show) {
console.log(state.values)
}
})
setTimeout(() => {
state.values.push(4)
}, 5000)
setTimeout(() => {
state.show = false
}, 10000)
setTimeout(() => {
state.values.push(5)
}, 15000)
At the beginning, the side effect function will depend on both show
and values
, and after 5 seconds, the side effect function will be triggered to re-execute with a new value added to values
, and after 10 Seconds later show
changed to false
, then if(state.show)
if the side effect of operation does not hold, then add the new function values
Being triggered again obviously does nothing but take up system resources.
Therefore, all dependencies (the role of cleanupEffect
) will be cleaned up before the side effect function is executed, and then recollected during execution.
In the face of the above situation, it is necessary to first clean up all dependencies and then re-collect, but in the following cases, this cleanup will increase unnecessary performance consumption
const state = reactive({ show: true, values: [1,2,3] })
effect(() => {
console.log(state.values)
})
@vue/reactivity shows us a very good way to deal with it, that is, by identifying the status of each dependency set (new dependency and already collected), and compare and filter the new dependency and the two markers that have been collected out dependencies that have been removed.
Optimize useless dependency cleaning algorithm
export type Dep = Set<ReactiveEffect> & Trackedmarkers
type TrackedMarkers = {
/**
* wasTracked的缩写,采用二进制格式,每一位表示不同effect嵌套层级中,该依赖是否已被跟踪过(即在上一轮副作用函数执行时已经被访问过)
*/
w: number
/**
* newTracked的缩写,采用二进制格式,每一位表示不同effect嵌套层级中,该依赖是否为新增(即在本轮副作用函数执行中被访问过)
*/
n: number
}
export const createDep = (effects) => {
const dep = new Set<ReactiveEffect>(effects) as Dep
// 虽然TrackedMarkers标识是位于响应式对象属性的依赖集合上,但它每一位仅用于表示当前执行的副作用函数是否曾经访问和正在访问该响应式对象属性
dep.w = 0
dep.n = 0
return dep
}
export const wasTracked = (dep: Dep): boolean => (dep.w & trackOpBit) > 0
export const newTracked = (dep: Dep): boolean => (dep.n & trackOpBit) > 0
/**
* 将当前副作用函数的依赖标记为 `已经被收集`
*/
export const initDepMarkers = ({ deps }: ReactiveEffect) => {
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].w |= trackOpBit
}
}
}
/**
* 用于对曾经跟踪过,但本次副作用函数执行时没有跟踪的依赖,采取删除操作。
* 即,新跟踪的 和 本轮跟踪过的都会被保留。
*/
export const finalizeDepMarkers = (effect: ReactiveEffect) => {
const { deps } = effect
if (deps.length) {
let ptr = 0
for (let i = 0; i < deps.length; i++) {
const dep = deps[i]
if (wasTracked(dep) && !newTracked(dep)) {
// 对于曾经跟踪过,但本次副作用函数执行时没有跟踪的依赖,采取删除操作。
dep.delete(effect)
}
else {
// 缩小依赖集合的大小
deps[ptr++] = dep
}
// 将w和n中对应的嵌套层级的二进制位置零,如果缺少这步后续副作用函数重新执行时则无法重新收集依赖。
dep.w &= ~trackOpBit
dep.n &= ~trackOpBit
}
// 缩小依赖集合的大小
deps.length = ptr
}
}
// 在位于响应式上下文执行的副作用函数内,访问响应式对象属性,将通过track收集依赖
export function track(target: object, type: TrackOpTypes, key: unknown) {
if (!isTracking()) {
return
}
// targetMap用于存储响应式对象-对象属性的键值对
// depsMap用于存储对象属性-副作用函数集合的键值对
let depsMap = targetMap.get(target)
if (!depsMap) {
target.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = createDep()))
}
trackEffects(dep)
}
// 收集依赖
export function trackEffects(
dep: Dep
) {
let shouldTrack = false
if (effectTrackDepth <= maxMarkerBits) {
// 如果本轮副作用函数执行过程中已经访问并收集过,则不用再收集该依赖
if (!newTracked(dep)) {
dep.n |= trackOpBit
shouldTrack = !wasTracked(dep)
}
}
else {
// 对于全面清理的情况,如果当前副作用函数对应的ReactiveEffect对象不在依赖集合中,则标记为true
shouldTrack = !dep.has(activeEffect!)
}
if (shouldTrack) {
dep.add(activeEffect!)
activeEffect!.deps.push(dep)
}
}
It is difficult to understand this optimization method from the perspective of code implementation alone. Let's start with an actual example!
const runAync = fn => setTimeout(fn, 1000)
const state = reactive({ show: true, values: [1,2,3] })
// 1
effect(() => {
if (state.show) {
console.log(state.values)
}
})
// 2
runAync(() => {
state.values.push(4)
})
// 3
runAync(() => {
state.show = false
})
- First execution of side effect function
A.effectTrackDepth
is 0, so1 << ++effectTrackDepth
obtainedeffectTrackDepth
andtrackOpBit
are 1, but this time because the side effects are not a function of collecting depends, so theinitDepMarkers
function has no effect;
b. Accessstate.show
due not previously been collected responsive objectsstate
ofshow
property, and therefore callscreateDep
createw
andn
are dependent on the set to 0, and calltrackEffects
foundnewTracked(dep)
is not tracking too, thenn
Set to 1, then start collecting dependencies;
c. Accessstate.values
will repeat the operation of step 2;
d. Sincestate.show
andstate.values
are both newly traced (n
is 1), the side effect is still processed afterfinalizeDepMarkers
Remain in the set of dependencies corresponding to these two properties. - Execute
state.values.push(4)
trigger side effect function change
a.effectTrackDepth
is 0, so1 << ++effectTrackDepth
obtained byeffectTrackDepth
andtrackOpBit
are the side effects that have been collected by ea5, cc269---a function has been collected at this time. ThereforeinitDepMarkers
the dependency collections where the side effect function is located are marked as collected (w
is 1);
b. Accessstate.show
will be calledtrackEffects
foundnewTracked(dep)
for the untracked too (infinalizeDepMarkers
has been zeroed), thenn
Set to 1, and then start to collect dependencies;
c. Accessstate.values
will repeat the operation of step 2;
d. Sincestate.show
andstate.values
are both newly traced (n
is 1), the side effect will still be processed afterfinalizeDepMarkers
Remain in the set of dependencies corresponding to these two properties. - Execute
state.show = false
trigger side effect function change
a.effectTrackDepth
is 0, so1 << ++effectTrackDepth
obtainedeffectTrackDepth
andtrackOpBit
have been collected by side effects, both of which depend on the side effect function. ThereforeinitDepMarkers
the dependency collections where the side effect function is located are marked as collected (w
is 1);
b. When accessingstate.show
, it will calltrackEffects
and find thatnewTracked(dep)
is not tracked (it has been zeroed infinalizeDepMarkers
has been set to zero)n
Set it to 1, and then start collecting dependencies;
c. Sincestate.values
not marked as a new track (n
0), sofinalizeDepMarkers
treatment will be a function of the sidestate.values
from the corresponding dependency set, and only keep it in the corresponding dependency set ofstate.values
.
At this point, I think everyone has a deeper understanding of this optimization. So the next question is naturally why to hardcode the optimization algorithm startup nesting level to maxMarkerBits = 30
?
SMI optimization principle
First of all maxMarkerBits = 30
indicates that only 31 layers of effect nesting are supported. The value described in the comments is because you want JavaScript to affect the use of SMI. So what is SMI?
Due to the ECMAScript standard convention number
numbers need to be converted to 64-bit double-precision floating-point numbers for processing, but it is very inefficient to store and process all numbers in 64 bits, so V8 uses other internal memory representations (such as 32 bit) and then provide the characteristics of 64-bit performance to the outside world. The legal index range of the array is [0, 2^32 - 2]
, and the V8 engine uses 32 bits to store these legal subscript numbers. In addition, all numbers in [0, 2^32 - 2]
will be stored preferentially in 32-bit two's complement.
V8 defines a special representation for integer numbers in the 32-bit signed bit range SMI
(numbers that are not SMI
are defined as HeapNumber
), while the V8 engine enables special optimizations for SMI: when using numbers within SMI, the engine does not need to allocate a dedicated memory entity for it, and fast integer operations are enabled .
For numbers other than SMI
let o = {
x: 42, // SMI
y: 4.2 // HeapNumber
}
Memory structure HeapNumber{ value: 4.2, address: 1 }
and JSObject{ x: 42, y: 1 }
, since the type of the value x SMI
thus stored directly on the object, and y is HeapNumber
is required Allocate a separate memory space for storage, and let the y attribute of the object point to the memory space of the instance HeapNumber
through the pointer.
However, when modifying the value, then x is SMI
so the value in memory can be modified in place, while HeapNumber
is immutable, so a new memory space must be allocated to store the new value , and modify the memory address in o.y
. Then when Mutable HeapNumber
is not enabled, the following code will generate 1.1
, 1.2
and 1.3
temporary instances of 1f89412.
let o = { x: 1.1 }
for (let i = 0; i < 4; ++i) {
o.x += 1;
}
If SMI
is signed, then the actual storage number is 31 bits, so set maxMarkerBits = 30
and pass if (effectTrackDepth <= maxMarkerBits)
to judge the level of nesting, that is, when effec is nested to 31 The useless dependency cleaning optimization algorithm is no longer used when layering. In the optimization algorithm, the binary bits are used to compare the dependencies collected in the previous round with those collected in the current round, so as to clean up useless dependencies. If n
and w
values occupy more than 31 bits, the internal storage will be HeapNumber
, then the performance of the bit operation will decrease.
In fact, we also see that if effectTrackDepth
is equal to 31, it will also execute trackOpBit = 1 << ++effectTrackDepth
, which will cause the storage method of ---8089aea37c9b1e81efcd42a12125c1eb trackOpBit
to convert from SMI
to 8e5098 HeapNumber
, is it possible to add a judgment and modify it to the following!
const maxMarkerBit = 1 << 30
if (trackOpBit & maxMarkerBit !== 1) {
trackOpBit = 1 << ++effectTrackDepth
}
Side Effect Function Trigger trigger
Since we have already analyzed --- track
in the explanation of "Optimizing the Useless Dependency Cleaning Algorithm", now we can directly analyze trigger
.
export function trigger(
target: object,
// set, add, delete, clear
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
const depsMap = targetMap.get(target)
if (!depsMap) {
// 该属性没有被任何副作用函数跟踪过,所以直接返回就好了
return
}
/**
* 用于存储将要被触发的副作用函数。
* 为什么不直接通过类似depsMap.values().forEach(fn => fn())执行副作用函数呢?
* 那是因为副作用函数执行时可能会删除或增加depsMap.values()的元素,导致其中的副作用函数执行异常。
* 因此用另一个变量存储将要执行的副作用函数集合,那么执行过程中修改的是depsMap.values()的元素,而正在遍历执行的副作用函数集合结构是稳定的。
*/
let deps: (Dep | undefined)[] = []
if (type === TriggerOpTypes.CLEAR) {
// 对象的所有属性值清空,所有依赖该响应式对象的副作用函数都将被触发
deps = [...depsMap.values()]
}
else if (key === 'length' && isArray(target)) {
// 若设置length属性,那么依赖length属性和索引值大于等于新的length属性值的元素的副作用函数都会被触发
depsMap.forEach((dep, key) => {
if (key === 'length' || key >= (newValue as number)) {
deps.push(dep)
}
})
}
else {
// 将依赖该属性的
if (key !== void 0) {
// 即使插入的是undefined也没有关系
deps.push(depsMap.get(key))
}
/**
* 添加间接依赖的副作用函数
* 1. 新增数组新值索引大于数组长度时,会导致数组容量被扩充,length属性也会发生变化
* 2. 新增或删除Set/WeakSet/Map/WeakMap元素时,需要触发依赖迭代器的副作用函数
* 3. 新增或删除Map/WeakMap元素时,需要触发依赖键迭代器的副作用函数
* 4. 设置Map/WeakMap元素的值时,需要触发依赖迭代器的副作用函数
*/
switch(type) {
case TriggerOpTypes.ADD:
if (!isArray(target)) {
// 对于非数组,则触发通过迭代器遍历的副作用函数
deps.push(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
else if (isIntegerKey(key)) {
// 对数组插入新元素,则需要触发依赖length的副作用函数
deps.push(depsMap.get('length'))
}
break
case TriggerOpTypes.DELETE:
if (!isArray(target)) {
// 对于非数组,则触发通过迭代器遍历的副作用函数
deps.push(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
break
case TriggerOpTypes.SET:
// 对于Map/WeakMap需要触发依赖迭代器的副作用函数
if (isMap(target)) {
deps.push(depsMap.get(ITERATE_KEY))
}
}
if (deps.length === 1) {
// 过滤掉undefined
if (deps[0]) {
triggerEffects(deps[0])
}
}
else {
const effects: ReactiveEffect[] = []
// 过滤掉undefined
for (const dep of deps) {
if (dep) {
effects.push(...dep)
}
}
triggerEffects(createDep(effects))
}
}
}
export function triggerEffects(
dep: Dep | ReactiveEffect[]
) {
for (const effect of isArray(dep) ? dep : [...dep]) {
/**
* 必须保证将要触发的副作用函数(effect)不是当前运行的副作用函数(activeEffect),否则将嵌入无限递归。
* 假设存在如下情况
* let foo = reactive({ bar: 1 })
* effect(() => {
* foo.bar = foo.bar + 1
* })
* 若没有上述的保障,则将会不断递归下去直接爆栈。
*
* 假如ReactiveEffect对象的allowRecurse设置为true,那么表示不对上述问题作防御。
*/
if (effect !== activeEffect || effect.allowRecurse) {
if (effect.scheduler) {
// 若设置有调度器则调用调用器
effect.scheduler()
}
else {
// 立即执行副作用函数
effect.run()
}
}
}
}
scheduler
In the previous section triggerEffects
we saw that the side effect function is executed synchronously by default. If you want to execute dozens of side effect functions synchronously, it will inevitably affect the execution of the main logic of the current event loop. At this time, it is the scheduler Time to shine. Let's review the schedulers provided in petite-vue below!
import { effect as rawEffect } from '@vue/reactivity'
const effect = (fn) => {
const e: ReactiveEffectRunner = rawEffect(fn, {
scheduler: () => queueJob(e)
})
return e
}
// ./scheduler.ts
let queued = false
const queue: Function[] = []
const p = Promise.resolve()
export const nextTick = (fn: () => void) => p.then(fn)
export const queueJob = (job: Function) => {
if (!queue.includes(job)) queue.push(job)
if (!queued) {
queued = true
nextTick(flushJobs)
}
}
const flushJobs = () => {
for (const job of queue) {
job()
}
queue.length = 0
queued = false
}
The side effect function is pushed into the queue, and the flushJobs
that empties the queue after traversing the queue and executing the side effect function is pushed into the micro queue. Then after the main logic of the current event loop is executed, the JavaScript engine will execute all tasks in the micro queue.
What is EffectScope
?
Vue 3.2 introduce a new Effect scope API , may be collected automatically setup
function created effect
, watch
and computed
the like, when the assembly Automatically destroy the scope and these instances under the scope ( effect
, watch
and computed
etc.) when destroyed. This API is mainly provided for use by plugin or library developers, and it is not needed for daily development.
Remember the context in petite-vue? When encountering v-if
and v-for
, a new block instance and a new context instance will be created for each sub-branch, and all ReactiveEffect
instances under the sub-branch are It will be uniformly managed by the corresponding context instance. When the block instance is destroyed, all the ReactiveEffect
instances under the corresponding context instance will be destroyed.
The block instance corresponds to the dynamic part of the DOM tree, which can roughly correspond to the Vue component, and the context instance is the EffectScope
object here.
Example of use:
cosnt scope = effectScope()
scope.run(() => {
const state = reactive({ value: 1 })
effect(() => {
console.log(state.value)
})
})
scope.stop()
So how is the effect
generated ReactiveEffect
instance associated with the scope?
That is ReactiveEffect
called in the constructor of recordEffectScope(this, scope)
export function recordEffectScope(
effect: ReactiveEffect,
scope?: EffectScope | null
) {
// 默认将activeEffectScope和当前副作用函数绑定
scope = scope || activeEffectScope
if (scope && scope.active) {
scope.effects.push(effect)
}
}
Summarize
The part of using @vue/reactivity in petite-vue is considered to have been analyzed. Maybe you will say that @vue/reactivity is more than this content. I will sort out these content in more detail in the follow-up "vue-lit Source Code Analysis". Analysis, stay tuned.
In the next article, we will see how eval
is used to construct a JavaScript parsing execution environment using new Function
and with
.
Respect the original, please indicate the source for reprint: https://www.cnblogs.com/fsjohnhuang/p/16163888.html Fat Boy John
"Anatomy of Petite-Vue Source Code" booklet
"Petite-Vue Source Code Analysis" combines examples to interpret the source code line by line from online rendering, responsive system and sandbox model, and also analyzes the SMI optimization dependency cleaning algorithm using the JS engine in the responsive system in detail. It is definitely an excellent stepping stone before getting started with Vue3 source code. If you like it, remember to forward it and appreciate it!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。