共计 13327 个字符,预计需要花费 34 分钟才能阅读完成。
当咱们通过 effect
将副函数向响应上下文注册后,副作用函数内拜访响应式对象时即会主动收集依赖,并在相应的响应式属性发生变化后,主动触发副作用函数的执行。
// ./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
函数的代码非常少,次要流程是
- 将基于副作用函数构建
ReactiveEffect
对象 - 若为默认模式则马上调用
ReactiveEffect
对象的run
办法执行副作用函数。
不过这里咱们有几个疑难
ReactiveEffectRunner
是什么?ReactiveEffect
生成的对象到底是什么?显然ReactiveEffect
的run
办法才是梦开始的中央,到底它做了些什么?- 针对配置项
scope
,recordEffectScope
的作用?
ReactiveEffectRunner
是什么?
// ./effect.ts
// ReactiveEffectRunner 是一个函数,而且有一个名为 effect 的属性且其类型为 RectiveEffect
export interface ReactiveEffectRunner<T = any> {(): T
effect: ReactiveEffect
}
ReactiveEffect
生成的对象到底是什么?
// 用于记录位于响应上下文中的 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
}
}
}
}
为应答嵌套 effect
外部将以后位于响应上下文的 ReactiveEffect 对象压入栈构造 effectStack: ReactiveEffect[]
,当以后副作用函数执行后再弹出栈。另外,尽管咱们通过effect
函数将副作用函数注册到响应上下文中,但咱们仍能通过调用 stop
办法让其脱离响应上下文。
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
}
}
在执行副作用函数前和执行后咱们会看到别离调用了 enableTracking()
和resetTracking()
函数,它们别离示意 enableTracking()
执行后的代码将启用依赖收集,resetTracking()
则示意前面的代码将在复原之前是否收集依赖的开关执行上来。要了解它们必须联合 pauseTracking()
和理论场景阐明:
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
}
假如咱们如下场景
const values = reactive([1,2,3])
effect(() => {values.push(1)
})
因为在执行 push
时外部会拜访代理对象的 length
属性,并批改 length
值,因而会导致一直执行该副作用函数直到抛出异样 Uncaught RangeError: Maximum call stack size exceeded
,就是和(function error(){error() })()
一直调用本身导致栈空间有余一样的。而 @vue/reactivity 是采纳如下形式解决
;(['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
}
})
即通过 pauseTracking()
暂停 push
外部的发生意外的依赖收集,即 push
仅仅会触发以其余模式依赖 length
属性的副作用函数执行。而后通过 resetTracking()
复原到之前的跟踪状态。
最初在执行副作用函数 return this.fn()
前,竟然有几句难以了解的语句
try {
trackOpBit = 1 << ++effectTrackDepth
if (effectTrackDepth <= maxMarkerBits) {initDepMarkers(this)
}
else {cleanupEffect(this)
}
return this.fn()}
finally {if (effectTrackDepth <= maxMarkerBits) {finalizeDepMarkers(this)
}
trackOpBit = 1 << --effectTrackDepth
}
咱们能够将其简化为
try {cleanupEffect(this)
return this.fn()}
finally {}
为什么在执行副作用函数前须要清理所有依赖呢?咱们能够考虑一下如下的状况:
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)
一开始的时候副作用函数将同时依赖 show
和values
,5 秒后向 values
追加新值副作用函数马上被触发从新执行,再过 10 秒后 show
转变为 false
,那么if(state.show)
无论如何运算都不成立,此时再对 values
追加新值若副作用函数再次被触发显然除了占用系统资源外,别无用处。
因而,在副作用函数执行前都会先清理所有依赖 (cleanupEffect
的作用),而后在执行时从新收集。
面对上述情况,先清理所有依赖再从新收集是必须的,但如下状况,这种清理工作反而减少无谓的性能耗费
const state = reactive({show: true, values: [1,2,3] })
effect(() => {console.log(state.values)
})
@vue/reactivity 给咱们展现了一个十分优良的解决形式,那么就是通过标识每个依赖汇合的状态(新依赖和曾经被收集过),并对新依赖和曾经被收集过两个标识进行比照筛选出已被删除的依赖项。
优化无用依赖清理算法
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)
}
}
单单从代码实现角度能难了解这个优化形式,不如咱们从理论的例子登程吧!
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})
- 首次执行副作用函数
a.effectTrackDepth
为 0,因而1 << ++effectTrackDepth
失去的effectTrackDepth
和trackOpBit
均为 1,但因为此时副作用函数还没有收集依赖,因而initDepMarkers
函数没有任何成果;
b. 拜访state.show
时因为之前没有收集过响应式对象state
的show
属性,因而会调用createDep
创立w
和n
均为 0 的依赖汇合,并调用trackEffects
发现newTracked(dep)
为未跟踪过,则将n
设置为 1,而后开始收集依赖;
c. 拜访state.values
会反复第 2 步的操作;
d. 因为state.show
和state.values
都是新跟踪的 (n
为 1),因而在finalizeDepMarkers
解决后依然将副作用函数保留在这两个属性对应的依赖汇合中。 - 执行
state.values.push(4)
触发副作用函数变动
a.effectTrackDepth
为 0,因而1 << ++effectTrackDepth
失去的effectTrackDepth
和trackOpBit
均为 1,此时副作用函数曾经收集过依赖,因而initDepMarkers
将该副作用函数所在的依赖汇合都都标记为已收集过 (w
为 1);
b. 拜访state.show
时会调用trackEffects
发现newTracked(dep)
为未跟踪过 (在finalizeDepMarkers
中已被置零),则将n
设置为 1,而后开始收集依赖;
c. 拜访state.values
会反复第 2 步的操作;
d. 因为state.show
和state.values
都是新跟踪的 (n
为 1),因而在finalizeDepMarkers
解决后依然将副作用函数保留在这两个属性对应的依赖汇合中。 - 执行
state.show = false
触发副作用函数变动
a.effectTrackDepth
为 0,因而1 << ++effectTrackDepth
失去的effectTrackDepth
和trackOpBit
均为 1,此时副作用函数曾经收集过依赖,因而initDepMarkers
将该副作用函数所在的依赖汇合都都标记为已收集过 (w
为 1);
b. 拜访state.show
时会调用trackEffects
发现newTracked(dep)
为未跟踪过 (在finalizeDepMarkers
中已被置零),则将n
设置为 1,而后开始收集依赖;
c. 因为state.values
没有标记为新跟踪的 (n
为 0),因而在finalizeDepMarkers
解决后会将副作用函数从state.values
对应的依赖汇合中移除,仅保留在state.values
对应的依赖汇合中。
到这里,我想大家曾经对这个优化有更深的了解了。那么接下来的问题自然而然就是为什么要硬编码将优化算法启动的嵌套层级设置为maxMarkerBits = 30
?
SMI 优化原理
首先 maxMarkerBits = 30
示意仅反对 effect 嵌套 31 层,正文中形容该值是因为想让 JavaScript 影响应用 SMI。那么什么是 SMI 呢?
因为 ECMAScript 规范约定 number
数字须要转换为 64 位双精度浮点数解决,但所有数字都用 64 位存储和解决是非常低效的,所以 V8 外部采纳其它内存示意形式(如 32 位)而后向外提供 64 位体现的个性即可。其中数组非法索引范畴是 [0, 2^32 - 2]
,V8 引擎就是采纳 32 位的形式来存储这些非法的下标数字。另外,所有在[0, 2^32 - 2]
内的数字都会优先应用 32 位二进制补码的形式存储。
针对 32 位有符号位范畴内的整型数字 V8 为其定义了一种非凡的表示法 SMI
(非SMI
的数字则被定义为 HeapNumber
),而 V8 引擎针对 SMI 启用非凡的优化: 当应用 SMI 内的数字时,引擎不须要为其调配专门的内存实体,并会启用疾速整型操作。
对于非 SMI
的数字
let o = {
x: 42, // SMI
y: 4.2 // HeapNumber
}
内存构造为 HeapNumber{value: 4.2, address: 1}
和JSObject{x: 42, y: 1}
,因为 x 值类型为 SMI
因而间接存储在对象上,而 y 为 HeapNumber
则须要调配一个独立的内存空间寄存,并通过指针让对象的 y 属性指向 HeapNumber
实例的内存空间。
然而在批改值时,而后 x 为 SMI
所以能够原地批改内存中的值,而 HeapNumber
为不可变,因而必须再调配一个新的内存空间寄存新值,并批改 o.y
中的内存地址。那么在没有启用 Mutable HeapNumber
时,如下代码将产生 1.1
、1.2
和1.3
3 个长期实例。
let o = {x: 1.1}
for (let i = 0; i < 4; ++i) {o.x += 1;}
有 SMI
是带符号位的,那么理论存储数字是 31 位,因而设置 maxMarkerBits = 30
且通过 if (effectTrackDepth <= maxMarkerBits)
判断层级,即当 effec 嵌套到 31 层时不再应用无用依赖清理优化算法。而优化算法中采纳的是二进制位对上一轮已收集和本轮收集的依赖进行比拟,从而清理无用依赖。若 n
和w
值所占位数超过 31 位则外部会采纳 HeapNumber
存储,那么在位运算上性能将有所降落。
其实咱们还看到若 effectTrackDepth
等于 31 时还会执行 trackOpBit = 1 << ++effectTrackDepth
,这会导致trackOpBit
从SMI
的存储形式转换为HeapNumber
,那是不是能够加个判断批改成上面这样呢!
const maxMarkerBit = 1 << 30
if (trackOpBit & maxMarkerBit !== 1) {trackOpBit = 1 << ++effectTrackDepth}
副作用函数触发器 -trigger
因为在解说 ” 优化无用依赖清理算法 ” 时曾经对 track
进行了分析,因而当初咱们间接剖析 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()}
}
}
}
调度器
在上一节的 triggerEffects
中咱们看到默认采纳同步形式执行副作用函数,若要同步执行数十个副作用函数那么势必会影响以后事件循环主逻辑的执行,这时就是调度器闪亮退场的时候了。咱们回顾以下 petite-vue 中提供的调度器吧!
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
}
副作用函数压入队列中,并将遍历队列执行其中的副作用函数后清空队列的 flushJobs
压入 micro queue。那么以后事件循环主逻辑执行完后,JavaScript 引擎将会执行 micro queue 中的所有工作。
什么是EffectScope
?
Vue 3.2 引入新的 Effect scope API,可主动收集setup
函数中创立的 effect
、watch
和computed
等,当组件被销毁时主动销毁作用域 (scope) 和作用域下的这些实例 (effect
、watch
和computed
等)。这个 API 次要是提供给插件或库开发者们应用的,日常开发不须要用到它。
还记得 petite-vue 中的 context 吗?当遇到 v-if
和v-for
就会为每个子分支创立新的 block 实例和新的 context 实例,而子分支下的所有 ReactiveEffect
实例都将对立被对应的 context 实例治理,当 block 实例被销毁则会对对应的 context 实例下的 ReactiveEffect
实例通通销毁。
block 实例对应是 DOM 树中动静的局部,能够大略对应上 Vue 组件,而 context 实例就是这里的 EffectScope
对象了。
应用示例:
cosnt scope = effectScope()
scope.run(() => {const state = reactive({ value: 1})
effect(() => {console.log(state.value)
})
})
scope.stop()
那么 effect
生成的 ReactiveEffect
实例是如何和 scope 关联呢?
那就是 ReactiveEffect
的构造函数中调用的recordEffectScope(this, scope)
export function recordEffectScope(
effect: ReactiveEffect,
scope?: EffectScope | null
) {
// 默认将 activeEffectScope 和以后副作用函数绑定
scope = scope || activeEffectScope
if (scope && scope.active) {scope.effects.push(effect)
}
}
总结
petite-vue 中应用 @vue/reactivity 的局部算是分析实现了,兴许你会说 @vue/reactivity 可不止这些内容啊,这些内容我将会在后续的《vue-lit 源码分析》中更详尽的梳理剖析,敬请期待。
下一篇咱们将看看 eval
中是如何应用 new Function
和with
来结构 JavaScript 解析执行环境的。
尊重原创,转载请注明来自:https://www.cnblogs.com/fsjoh… 肥仔 John