响应 Pointer 交互事件(上篇)

上一章我们分析了 sprite 在 canvasRenderer 上的渲染,那么接下来得看看交互上最重要的事件系统了

最简单的 demo

还是用一个最简单的 demo 演示 example/sprite-pointerdown.html

为 sprite 添加一个 pointerdown 事件,即点击事件,移动设备上就是 touch 事件, desktop 设备上即 click 事件

const app = new PIXI.Application({ width: 800, height: 600, autoStart: false });
document.body.appendChild(app.view); const sprite = PIXI.Sprite.from('logo.png'); sprite.on('pointerdown', ()=> {
console.log('clicked')
}) app.stage.addChild(sprite);
app.start()

试着用鼠标点击 sprite ,会发现控制台并未输出期望的 'clicked'

奇奇怪怪... 看下官网的例子,需要为 sprite 添加 sprite.eventMode = 'static';;

再运行,就可以看到控制台正常输出 'clicked' 了

显示对象没有自己的事件

Canvas 本身不像 DOM 那样每个元素都有自带的事件的系统用于响应事件

需要自己实现事件系统,可交互的元素都应该是 DisplayObject 及继承自它的子类元素

/packages/display/src/DisplayObject.ts 第 210 行

export abstract class DisplayObject extends utils.EventEmitter<DisplayObjectEvents>

说明,DisplayObject 继承了 EventEmitter 类,因此就有了自定义的事件系统,所有对应的 API

eventemitter3: https://github.com/primus/eventemitter3

eventemitter3 的 REAMDME 过于简单

得看它的 测试用例 https://github.com/primus/eventemitter3/blob/master/test/test.js

可以发现 监听事件可以用 on, 触发事件可以用 emit

所以 PixiJS 中的 DisplayObject 类实例对象就可以用 on 监听事件,用 emit 触发事件,即有了自定义事件的能力

当显示对象有了自定义事件能力后,需要一个事件管理系统来管理显示对象的事件触发、监听、移除

来看看 EventSystem 类

/packages/events/src/EventSystem.ts 204 -238 行

constructor(renderer: IRenderer)
{
this.renderer = renderer;
this.rootBoundary = new EventBoundary(null);
EventsTicker.init(this); this.autoPreventDefault = true;
this.eventsAdded = false; this.rootPointerEvent = new FederatedPointerEvent(null);
this.rootWheelEvent = new FederatedWheelEvent(null); this.cursorStyles = {
default: 'inherit',
pointer: 'pointer',
}; this.features = new Proxy({ ...EventSystem.defaultEventFeatures }, {
set: (target, key, value) =>
{
if (key === 'globalMove')
{
this.rootBoundary.enableGlobalMoveEvents = value;
}
target[key as keyof EventSystemFeatures] = value; return true;
}
});
this.onPointerDown = this.onPointerDown.bind(this);
this.onPointerMove = this.onPointerMove.bind(this);
this.onPointerUp = this.onPointerUp.bind(this);
this.onPointerOverOut = this.onPointerOverOut.bind(this);
this.onWheel = this.onWheel.bind(this);
}

EventSystem.ts 的最后一行 extensions.add(EventSystem); 会将它以扩展插件的方式集成到 pixiJS 内

可以看到构造函数内很简单,

  1. 传入了渲染器实例

  2. rootBoudary “根边界” 这个对象很重要,后面会具体介绍

  3. 单独创建一个 ticker 用于管理事件,确保运行状态下显示对象的碰撞检测事件

  4. 实例化了两个事件对象,用于触发时传递,事件对象内的数据结构

  5. onPointerDown/onPointerMove/onPointerUp/onPointerOverOut/onWheel 等绑定到当前 this 上

当 EventSystem 加入 PixiJS 管理后,会被触发 'init' 这个 Runner , 可理解这个 init 生命周期函数被触发

在 EventSystem.ts 第 245 - 254 行:

init(options: EventSystemOptions): void
{
const { view, resolution } = this.renderer; this.setTargetElement(view as HTMLCanvasElement);
this.resolution = resolution;
EventSystem._defaultEventMode = options.eventMode ?? 'auto';
Object.assign(this.features, options.eventFeatures ?? {});
this.rootBoundary.enableGlobalMoveEvents = this.features.globalMove;
}

可以看到,setTargetElement 用于设置事件目标元素,就是 渲染器对应的 view, 可以认为这个 view 就是 canvas 本身,它是可以响应浏览器的 DOM 事件的, 当然包括,鼠标的点击,移动 等。

setTargetElement 函数 最终会调到 addEvents()

在 EventSystem.ts 第 483 - 546 行:

private addEvents(): void
{
... 省略部分源码
if (this.supportsPointerEvents)
{
globalThis.document.addEventListener('pointermove', this.onPointerMove, true);
this.domElement.addEventListener('pointerdown', this.onPointerDown, true);
... 省略部分源码
globalThis.addEventListener('pointerup', this.onPointerUp, true);
}
else
{
globalThis.document.addEventListener('mousemove', this.onPointerMove, true);
this.domElement.addEventListener('mousedown', this.onPointerDown, true);
... 省略部分源码 if (this.supportsTouchEvents)
{
this.domElement.addEventListener('touchstart', this.onPointerDown, true);
... 省略部分源码
}
} this.domElement.addEventListener('wheel', this.onWheel, {
passive: true,
capture: true,
}); this.eventsAdded = true;
}

此函数是真正为根元素(或者说是整个 canvas 内自定义事件发起事件的元素)添加事件监听器的地方。

如果支持 pointer 事件则使用 pointer 事件,

如果不支持 pointer 事件则使用 mouse 事件。

如果支持 touch 事件则也要添加上 touch 事件

注意 move 相关的事件是添加在 document 元素上的

至此,当用户点击 canvas 元素时,就相关的回调函数就会执行,如注册的 this.onPointerDown、this.onPointerUp 等

触发的回调函数内会去触发 eventemitter3 的自定义事件。

还是以我们在 sprite-pointer.html 中的例子为例,我们注册了 sprite-pointer.html 中 sprite.on('pointerdown', function() {}),那么当用户点击 canvas 元素时,就会触发这个回调函数。

在 onPointerDown

在 EventSystem.ts 第 343 - 377 行:

private onPointerDown(nativeEvent: MouseEvent | PointerEvent | TouchEvent): void

{

... 省略部分源码

this.rootBoundary.rootTarget = this.renderer.lastObjectRendered as DisplayObject;

const events = this.normalizeToPointerData(nativeEvent);
... 省略部分源码 for (let i = 0, j = events.length; i < j; i++)
{
const nativeEvent = events[i];
const federatedEvent = this.bootstrapEvent(this.rootPointerEvent, nativeEvent); this.rootBoundary.mapEvent(federatedEvent);
} ... 省略部分

}

  1. 先指定当前 rootBoundary.rootTarget = this.renderer.lastObjectRendered 即响应事件的目标对象"为渲染器最上层的一个显示对象"

  2. 适配浏览器原生事件 nativeEvent 后,调用 this.rootBoundary.mapEvent(federatedEvent), federatedEvent 即标准化为 PixiJS 自定义事件

事件边界 EventBoundary

canvas 内绘制的元素要准确的响应用户点击的操作,必须先确定用户点击的范围在哪里,然后将范围内的 DisplayObject 显示元素触发对应用户绑定的点击事件回调

/packages/events/src/EventBoundary.ts

EventBoundary.ts 构造函数 149 - 172 行 :

constructor(rootTarget?: DisplayObject)
{
this.rootTarget = rootTarget; this.hitPruneFn = this.hitPruneFn.bind(this);
this.hitTestFn = this.hitTestFn.bind(this);
this.mapPointerDown = this.mapPointerDown.bind(this);
this.mapPointerMove = this.mapPointerMove.bind(this);
this.mapPointerOut = this.mapPointerOut.bind(this);
this.mapPointerOver = this.mapPointerOver.bind(this);
this.mapPointerUp = this.mapPointerUp.bind(this);
this.mapPointerUpOutside = this.mapPointerUpOutside.bind(this);
this.mapWheel = this.mapWheel.bind(this); this.mappingTable = {};
this.addEventMapping('pointerdown', this.mapPointerDown);
this.addEventMapping('pointermove', this.mapPointerMove);
this.addEventMapping('pointerout', this.mapPointerOut);
this.addEventMapping('pointerleave', this.mapPointerOut);
this.addEventMapping('pointerover', this.mapPointerOver);
this.addEventMapping('pointerup', this.mapPointerUp);
this.addEventMapping('pointerupoutside', this.mapPointerUpOutside);
this.addEventMapping('wheel', this.mapWheel);
}

构造函数内表明实例化后, 由 addEventMapping 方法 将pointerdown,pointermove, pointerout .... 等 8 类事件的回调映射函数保存在了 mappingTable 对象内

后续使用过程中用户添加到显示对象上的交互事件,都会被存储到对应的这 8 类事件列表中

当鼠标点击例子中的 sprite 显示对象时,这个 mapPointerDown 会被触发

EventBoundary.ts 构造函数 672 - 701 行 :

protected mapPointerDown(from: FederatedEvent): void
{
if (!(from instanceof FederatedPointerEvent))
{
console.warn('EventBoundary cannot map a non-pointer event as a pointer event'); return;
} const e = this.createPointerEvent(from);
console.log(e.target)
this.dispatchEvent(e, 'pointerdown'); if (e.pointerType === 'touch')
{
this.dispatchEvent(e, 'touchstart');
}
else if (e.pointerType === 'mouse' || e.pointerType === 'pen')
{
const isRightButton = e.button === 2; this.dispatchEvent(e, isRightButton ? 'rightdown' : 'mousedown');
} const trackingData = this.trackingData(from.pointerId); trackingData.pressTargetsByButton[from.button] = e.composedPath(); this.freeEvent(e);
}

当 view 被点击后,先创建事件对象,然后向目标对象发送事件,接下来就是找到那个目标对象了

找到目标对象即点击的对象

把 mapPointerDown 函数的 const e = this.createPointerEvent(from); 的事件对象打出来看看

图 4-1

果然 e.target 把当前点击的就是 sprite,显然在这一步确定了 当前点击的对象 this.createPointerEvent(from); 方法调用非常重要

createPointerEvent 该当在 EventBoundary.ts 文件的 1181 - 1205 行 :

protected createPointerEvent(
from: FederatedPointerEvent,
type?: string,
target?: FederatedEventTarget
): FederatedPointerEvent
{
const event = this.allocateEvent(FederatedPointerEvent); this.copyPointerData(from, event);
this.copyMouseData(from, event);
this.copyData(from, event); event.nativeEvent = from.nativeEvent;
event.originalEvent = from;
event.target = target
?? this.hitTest(event.global.x, event.global.y) as FederatedEventTarget
?? this._hitElements[0]; if (typeof type === 'string')
{
event.type = type;
} return event;
}

可以看到正是在这个 createPointerEvent 方法内调用 hitTest 或 _hitElements

注意 mapPointerDown 方法内调用 const e = this.createPointerEvent(from); 时只传了一个参数 from

所以此处 target 的确定就是由 this.hitTest(event.global.x, event.global.y) as FederatedEventTarget 来决定的

向 hitTest 方法 传入了当前事件的全局的 x, y 坐标

在 EventBoundary.ts 文件的 247 - 265 行 :

public hitTest(
x: number,
y: number,
): DisplayObject
{
EventsTicker.pauseUpdate = true;
// if we are using global move events, we need to hit test the whole scene graph
const useMove = this._isPointerMoveEvent && this.enableGlobalMoveEvents;
const fn = useMove ? 'hitTestMoveRecursive' : 'hitTestRecursive';
console.log(this.rootTarget)
const invertedPath = this[fn](
this.rootTarget,
this.rootTarget.eventMode,
tempHitLocation.set(x, y),
this.hitTestFn,
this.hitPruneFn,
); return invertedPath && invertedPath[0];
}

由于对 move 事件需要特殊处理,所以需要判断 在 hitTest 函数内调用了 hitTestMoveRecursive || hitTestRecursive

我们当前 demo 中用的是点击事件,所以调用的会是 hitTestRecursive

把 this.rootTarget 打印出来看看

图 4-2

可以看到图 4-2 当前 rootTarget 是一个 container 对象

当点击事件发生时,需要判断的不止是当前对象,而是当前 container 下的所有子对象,所以才需要用到 hitTestRecursive 即是递归判断

递归遍历

hitTestRecursive 函数的最生两个参数分别是用于具体碰撞检测的 hitTestFn 函数 和 用于判断是否可剔除用于碰撞判断的 hitPruneFn 函数

在 EventBoundary.ts 文件的 407 - 539 行 :

protected hitTestRecursive(
currentTarget: DisplayObject,
eventMode: EventMode,
location: Point,
testFn: (object: DisplayObject, pt: Point) => boolean,
pruneFn?: (object: DisplayObject, pt: Point) => boolean
): DisplayObject[]
{
// Attempt to prune this DisplayObject and its subtree as an optimization.
if (this._interactivePrune(currentTarget) || pruneFn(currentTarget, location))
{
return null;
} if (currentTarget.eventMode === 'dynamic' || eventMode === 'dynamic')
{
EventsTicker.pauseUpdate = false;
} // Find a child that passes the hit testing and return one, if any.
if (currentTarget.interactiveChildren && currentTarget.children)
{
const children = currentTarget.children; for (let i = children.length - 1; i >= 0; i--)
{
const child = children[i] as DisplayObject; const nestedHit = this.hitTestRecursive(
child,
this._isInteractive(eventMode) ? eventMode : child.eventMode,
location,
testFn,
pruneFn
); if (nestedHit)
{
// Its a good idea to check if a child has lost its parent.
// this means it has been removed whilst looping so its best
if (nestedHit.length > 0 && !nestedHit[nestedHit.length - 1].parent)
{
continue;
} // Only add the current hit-test target to the hit-test chain if the chain
// has already started (i.e. the event target has been found) or if the current
// target is interactive (i.e. it becomes the event target).
const isInteractive = currentTarget.isInteractive(); if (nestedHit.length > 0 || isInteractive) nestedHit.push(currentTarget); return nestedHit;
}
}
} const isInteractiveMode = this._isInteractive(eventMode);
const isInteractiveTarget = currentTarget.isInteractive(); // Finally, hit test this DisplayObject itself.
if (isInteractiveMode && testFn(currentTarget, location))
{
// The current hit-test target is the event's target only if it is interactive. Otherwise,
// the first interactive ancestor will be the event's target.
return isInteractiveTarget ? [currentTarget] : [];
} return null;
}

函数大致流程

  1. 进来先判断是否需要进行碰撞检测 if (this._interactivePrune(currentTarget) || pruneFn(currentTarget, location))

    剔除需要进行碰撞检测的对象,比如遮罩、不可见、不可交互、无需渲染等对象

  2. 如果有子显示对象,则需要循环所有子显示对象,并递归检测子显示对象

  3. 如果碰撞检测成功(点击位置有子显示对象)则在返回的 nextedHit 数组内把 currentTarget 添加进队尾, 并返回 nestedHit 数组

    if (nestedHit.length > 0 || isInteractive) nestedHit.push(currentTarget);
    return nestedHit;
  4. 最后 如果是 isInteractiveMode 并且 testFn 碰撞检测成功,则把当前碰撞对象放到数组内返回

注意在这个函数内的这一行 const isInteractiveMode = this._isInteractive(eventMode);

在 EventBoundary.ts 文件的 541 -544 行 :

private _isInteractive(int: EventMode): int is 'static' | 'dynamic'
{
return int === 'static' || int === 'dynamic';
}

到这里终于知道我们 demo 中当没有指定 eventMode 为 static 或 dynamic 时,没有响应点击事件的原因了

这是检测 rootTarget 父级对象的, 如果父级对象比如 container 都不支持交互了,就不必再对其子显示对象进行碰撞检测了

还有 const isInteractiveTarget = currentTarget.isInteractive(); 这一行,判断元素本身是否可交互 也是判断 eventMode 这是检测当前 target 的

/packages/events/src/FederatedEventTarget.ts 事件定义内 657 - 660 行:

isInteractive()
{
return this.eventMode === 'static' || this.eventMode === 'dynamic';
},

接下来就要用碰撞检测来检测是否是点击对象了

找到碰撞检测函数

注意看最后的 testFn 即传入来的 hitTestFn 碰撞检测函数

在 EventBoundary.ts 文件的 615 - 637 行 :

protected hitTestFn(displayObject: DisplayObject, location: Point): boolean
{
// If the displayObject is passive then it cannot be hit directly.
if (displayObject.eventMode === 'passive')
{
return false;
} // If the display object failed pruning with a hitArea, then it must pass it.
if (displayObject.hitArea)
{
return true;
} if ((displayObject as any).containsPoint)
{
return (displayObject as any).containsPoint(location) as boolean;
} // TODO: Should we hit test based on bounds? return false;
}

主要进行了三个判断

  1. eventMode === 'passive' 直接不进行碰撞检测,用于优化性能,比如在滚动区域内滚动时可以设置内部的元素为 passive

  2. displayObject.hitArea 判断 主要作用

    • 自定义交互区域:你可以定义一个特定的区域来响应用户交互,而不是使用显示对象的整个边界框。这在某些情况下非常有用,例如当你有一个复杂形状的对象,但只希望某个部分响应交互。
    • 提高性能:通过定义较小的交互区域,可以减少不必要的命中测试,从而提高性能。
    • 精确控制:你可以精确控制哪些区域应该响应用户交互,这在游戏开发和复杂的用户界面中非常有用。
  3. displayObject.containsPoint 检测,可以看到,containsPoint 是由显示对象各自自己实现的方法

注意: GraphicsGeometry.containsPoint 方法,内可知,如果你绘制的是直线、贝塞尔曲线等线条添加鼠标事件是不会起作用的,因为这些只是路径,并不是形状,你需要为这些添加 hitArea 后才交互事件才会起作用

以 sprite 类实现的 containsPoint 举例

/packages/sprite/src/Sprite.ts 第 439 - 459 行:

public containsPoint(point: IPointData): boolean
{
this.worldTransform.applyInverse(point, tempPoint); const width = this._texture.orig.width;
const height = this._texture.orig.height;
const x1 = -width * this.anchor.x;
let y1 = 0; if (tempPoint.x >= x1 && tempPoint.x < x1 + width)
{
y1 = -height * this.anchor.y; if (tempPoint.y >= y1 && tempPoint.y < y1 + height)
{
return true;
}
} return false;
}

sprite 的 containsPoint 判断坐标点是否在显示对象的矩形内比较简单,就是将全局坐标点转换为 sprite 的本地坐标点,然后判断是否在矩形内

this.worldTransform.applyInverse 方法,传入一个坐标点,返回一个由世界坐标转换成本地坐标的新坐标点,这个新坐标点就是 sprite 本地坐标点

如何处理 sprite 叠加时的碰撞检测

如果只是简单的点与形状的碰撞检测,那么如果两个显示对象叠加在一起时,点击上层的显示对像,如果不加处理,叠在下面的对象也会响应点击事件

新建个 demo 演示 example/two-sprite-pointerdown.html

const app = new PIXI.Application({ width: 800, height: 600, autoStart: false });
document.body.appendChild(app.view); const sprite = PIXI.Sprite.from('logo.png');
sprite.eventMode = 'static';
sprite._Name = 'sprite1';
sprite.on('pointerdown', ()=> {
console.log('clicked')
}) const sprite2 = PIXI.Sprite.from('logo.png');
sprite2.tint = 'red';
sprite2.eventMode = 'static';
sprite2._Name = 'sprite2';
sprite2.x = 100
sprite2.on('pointerdown', ()=> {
console.log('clicked2')
}) app.stage.addChild(sprite);
app.stage.addChild(sprite2);
app.start()
  1. 其它代码与 sprite-pointerdown.html 几乎一样,就是添加了两个 sprite 且有一部分重叠在一起

  2. 分别给这两个 sprite 分别添加了 _Name 属性,方便调试 sprite1 和 sprite2

  3. sprite2 的 tint 属性设置为红色

  4. 修改了 sprite2 的x 值,使得 sprite2 只覆盖一部分 sprite1

如图:4-3

图 4-3

在 hitTestRecursive 函数内把 for 循环内的 nestedHit 打印出来

在 EventBoundary.ts 文件的 407 - 539 行 :

protected hitTestRecursive(
currentTarget: DisplayObject,
eventMode: EventMode,
location: Point,
testFn: (object: DisplayObject, pt: Point) => boolean,
pruneFn?: (object: DisplayObject, pt: Point) => boolean
): DisplayObject[]
{
...省略部分代码
if (currentTarget.interactiveChildren && currentTarget.children)
{
const children = currentTarget.children; for (let i = children.length - 1; i >= 0; i--)
{
const child = children[i] as DisplayObject; const nestedHit = this.hitTestRecursive(
child,
this._isInteractive(eventMode) ? eventMode : child.eventMode,
location,
testFn,
pruneFn
);
console.log(nestedHit)
...省略部分代码
}
}
...省略部分代码
}

测试,点击与左侧logo重叠的位置右侧红色的 pixijs logo

图 4-4

输出结果确实是正确的,并没有把 sprite1 输出,仔细看 for 循环可以发现它是倒序遍历的,也就是添加在最后的显示对象,先响应碰撞

因为后面添加的对象理论上是覆盖在上层,所以应该先响应碰撞

如果把 for 循环的遍历顺序改成正序,那么就会输出 sprite1

...省略部分代码
for (let i = 0; i < children.length; i++)
{
const child = children[i] as DisplayObject; const nestedHit = this.hitTestRecursive(
child,
this._isInteractive(eventMode) ? eventMode : child.eventMode,
location,
testFn,
pruneFn
);
console.log(nestedHit)
...省略部分代码
}

图 4-5

可以看到,通过 for 循环的倒序遍历就实现了盖在上层的显示对象优先响应的功能

本章小节

碰撞检测的粗略流程:

EventSystem.ts 的 init -> pointerdown -> onPointerDown -> mapPointerDown -> createPointerEvent -> hitTest -> hitTestRecursive -> hitTestFn -> containsPoint

事件的检测为什么不是像素级:

与 EaselJS 库的像素级碰撞检测不同, PixiJS 采用的是点与形状的碰撞检测

https://github.com/pixijs/pixijs/wiki/v5-Hacks#pixel-perfect-interaction

绕了一圈函数调用,才成功实现碰撞检测,下一篇再关注一下,碰撞检测成功后,派发事件


注:转载请注明出处博客园:王二狗Sheldon池中物 (willian12345@126.com)

PixiJS源码分析系列:第四章 响应 Pointer 交互事件(上篇)的更多相关文章

  1. jQuery-1.9.1源码分析系列(四) 缓存系统

    先前在分析Sizzle的时候分析到Sizzle有自己的缓存机制,点击这里查看.不过Sizzle的缓存只是对内使用的(内部自己存,自己取).接下来分析jQuery可以对外使用的缓存(可存可取). 首先需 ...

  2. jQuery-1.9.1源码分析系列(十) 事件系统——主动触发事件和模拟冒泡处理

    发现一个小点,先前没有注意的 stopPropagation: function() { var e = this.originalEvent; ... if ( e.stopPropagation ...

  3. Thinkphp源码分析系列(四)–Dispatcher类

    下面我们来分析一下Thinkphp中的url解析和路由调度类.此类主要功能是 // +--------------------------------------------------------- ...

  4. jQuery-1.9.1源码分析系列完毕目录整理

    jQuery 1.9.1源码分析已经完毕.目录如下 jQuery-1.9.1源码分析系列(一)整体架构 jQuery-1.9.1源码分析系列(一)整体架构续 jQuery-1.9.1源码分析系列(二) ...

  5. Spring Ioc源码分析系列--容器实例化Bean的四种方法

    Spring Ioc源码分析系列--实例化Bean的几种方法 前言 前面的文章Spring Ioc源码分析系列--Bean实例化过程(二)在讲解到bean真正通过那些方式实例化出来的时候,并没有继续分 ...

  6. Netty 源码分析系列(二)Netty 架构设计

    前言 上一篇文章,我们对 Netty做了一个基本的概述,知道什么是Netty以及Netty的简单应用. Netty 源码分析系列(一)Netty 概述 本篇文章我们就来说说Netty的架构设计,解密高 ...

  7. Spring Ioc源码分析系列--Bean实例化过程(一)

    Spring Ioc源码分析系列--Bean实例化过程(一) 前言 上一篇文章Spring Ioc源码分析系列--Ioc容器注册BeanPostProcessor后置处理器以及事件消息处理已经完成了对 ...

  8. Spring mvc源码分析系列--Servlet的前世今生

    Spring mvc源码分析系列--Servlet的前世今生 概述 上一篇文章Spring mvc源码分析系列--前言挖了坑,但是由于最近需求繁忙,一直没有时间填坑.今天暂且来填一个小坑,这篇文章我们 ...

  9. jQuery源码分析系列

    声明:本文为原创文章,如需转载,请注明来源并保留原文链接Aaron,谢谢! 版本截止到2013.8.24 jQuery官方发布最新的的2.0.3为准 附上每一章的源码注释分析 :https://git ...

  10. [转]jQuery源码分析系列

    文章转自:jQuery源码分析系列-Aaron 版本截止到2013.8.24 jQuery官方发布最新的的2.0.3为准 附上每一章的源码注释分析 :https://github.com/JsAaro ...

随机推荐

  1. css 文本换行的问题

    今天测试再测试功能的时候无意间发现了这个css的bug,我们可以仔细看第一张图的最后两行,明明还没有到结尾就自动换到最后一行去了,按理说应该是撑满当前行再挤到下一行去,于是我就去看了下css的文本溢出 ...

  2. 新手入门html

    网页的组成:结构 表现 行为 Web标准: 结构  表现 行为 Html  css  js Html和css 是w3c制定标准  js是ECMA制定标准 HTML:指的是超文本标记语言 文件命名的规范 ...

  3. 剑指Offer-64.滑动窗口的最大值(C++/Java)

    题目: 给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值.例如,如果输入数组{2,3,4,2,6,2,5,1}及滑动窗口的大小3,那么一共存在6个滑动窗口,他们的最大值分别为{4,4,6, ...

  4. 关于朋友圈出现的小米新店广告骗局(非法获取个人消息)木马通过广东政务服务网(tyrz.gd.gov.cn)的url漏洞显示

    前两天在朋友圈突然看到有发 小米新店开业 送千台扫地机器人的 广告,出于天上不会掉馅饼到我身上的原则 我选择忽略了,但是没多久 看到他又晒了个物流订单,于是还是点开看了一下,发现微信打开的网站还蛮正规 ...

  5. 网络世界的脊柱——OSI七层模型

    简介 OSI代表开放系统互联(Open Systems Interconnection),这是国际标准化组织(ISO)提出的一个概念模型,用于描述网络通信的功能划分.简单来说,OSI模型把复杂的网络通 ...

  6. 『手撕Vue-CLI』完善提示信息

    前言 经过『手撕Vue-CLI』自动安装依赖,已经实现了自动安装依赖的功能. 然而,虽然项目已复制并安装依赖,但其提示信息并不够友好,于是我试着去运行了一下vue create,发现其提示信息是这样的 ...

  7. Arduino实现温湿度传感器以及数据上传到云(乐维互联)

    0 准备材料 0.1 ESP-01S 引脚及定义 官方定义: 序号 pin 功能 1 GND 地线 2 IO0/GPIO0 工作模式选择:①悬空:Flash Boot,工作模式 ②下拉:UART Do ...

  8. 什么是RDD

    RDD(Resilient Distributed Dataset)叫做弹性分布式数据集RDD,是Spark中最基本的数据抽象,它代表一个不可变.可分区.里面的元素可并行计算的集合.RDD具有数据流模 ...

  9. welcome to chifan-duck's blog

    博主简介 截至至 2024 博主事一名初二学生. 为什么开博客园 因为博主是一名 Oier. 博客内容 OI 知识(科技)的普及 & 题解 为什么叫 chifan-duck ? 原本博主再各大 ...

  10. MyBatis学习篇

    什么是MyBatis (1)Mybatis是一个半ORM(对象关系映射)框架,它内部封装了JDBC,开发时只需要关注SQL语句本身,不需要花费精力去处理加载驱动.创建连接.创建statement等繁杂 ...