2024年7月

在我的记忆中布谷鸟过滤器一直是说比bloom好,那么我博客便以一个diss布谷鸟过滤器的角度来探究

学前须知:本篇立足于读者了解bloomfilter底层实现上

布谷鸟相较于bloom的优点

支持删除操作

如何支持呢?因为bloom的话是不能支持的,他的一个bit可能代表了多个key存在的情况,所以这个bit位是不能随便乱动的!

那么布谷鸟呢?下面这段便是布谷鸟过滤器的原理啦

布谷鸟交配后,雌性
布谷鸟
就准备产蛋了,但它却不会自己筑巢。它会来到像知更

、刺嘴莺等那些比它小的

类的巢中,移走原来的那窝蛋中的一个,用自己的蛋来取而代之。相对于它的体形来说,它的蛋是偏小的,而且蛋上的斑纹同它混入的其他

的蛋也非常相似,所以不易被分辨出来。如果不是这样,它的蛋肯定会被扔出去。

更具体地说是

最原始的布谷鸟
哈希方法是使用两个哈希函数对一个
key
进行哈希,得到桶中的两个位置,此时

  • 如果两个位置都为为空则将
    key
    随机存入其中一个位置
  • 如果只有一个位置为空则存入为空的位置
  • 如果都不为空,则随机踢出一个元素,踢出的元素再重新计算哈希找到相应的位置

当然假如存在绝对的空间不足,那老是踢出也不是办法,所以一般会设置一个
踢出阈值
,如果在某次插入行为过程中连续踢出超过阈值,则进行扩容。

更现代化的布谷鸟过滤器:

由两个或者多个哈希函数构成,布谷鸟过滤器的布谷鸟哈希表的基本单位称为
条目(entry,说起条目感觉很陌生,其实就是java中map的一个单位)
。 每个条目存储一个
指纹(fingerprint)(指纹这个概念大家应该会比较陌生,下文会给出更具体的描述)
,指纹指的是使用一个哈希函数生成的n位比特位,n的具体大小由所能接受的误判率来设置,例如使用8bits的指纹大小。

插入

以最简单的两个hash函数为例来进行说明

首先插入值为x,

  1. 我们需要先计算出指纹

$$
f=fingerprint(x)
$$

  1. 计算出两个候选位置

使用两个不同的哈希函数
h1

h2
计算两个候选存储位置
i1

i2
。注意这里计算
i2
时会用到指纹
f

$$
i1=h1(x)
$$

$$
i2=h2(f)=i1⊕h(f)
$$
其中,
h(f)
是对指纹
f
进行哈希运算得到的值。

尝试插入
:首先尝试将指纹
f
插入到位置
i1
对应的桶中。如果桶中有空位,则直接插入。如果
i1
的桶满了,再尝试将
f
插入到位置
i2
的桶中。如果
i2
的桶有空位,则直接插入。

踢出和重插入
:如果两个候选位置的桶都满了,就需要“踢出”其中一个桶中的已有指纹,并将被踢出的指纹重新插入。具体步骤如下:


  • i1

    i2
    中随机选择一个桶,将该桶中的一个现有指纹
    f'
    踢出。

  • 将原始指纹
    f
    插入到踢出的指纹
    f'
    的位置。

  • 对于被踢出的指纹
    f'
    ,计算它的另一个候选位置
    i2'
    ,即:
    $$
    i2′=i1′⊕h(f′)
    $$

  • 将被踢出的指纹
    f'
    重新插入到
    i2'
    的桶中。

  • 如果
    i2'
    的桶也满了,重复踢出和重插入的过程,直到成功插入或者达到最大重插入次数。

相信大家看下来可能有点模糊模糊,下面便一一解答
具体的指纹是怎么取的?这个指纹算出来是什么样子的?

指纹是通过hash函数来获得,如果说我们hash函数算出的值过长那么就截断

例如:
$$
f=h(x)&0xFF
$$
通常会选用8位和16位

为什么第二个hash函数需要是i1⊕h(f)?以及为什么他的传参要是f?

先给出结论主要是为了实现
搬家

首先回到上面的重插入!当位置冲突后我们最终会算出里面那个value最终放的是指纹对吧?然后对应的哈希函数我们都知道对吧?假如说原本这个位置添加的选择的是i1,那么他接下来要选择的是i2对吧?那么i2=i1⊕h(f),这里面什么我们都有没错吧?那么我们自然可以实现让它去搬家的操作,(
同时这里也有印证了那么一句名言,软件上有问题那么再加一层就没有问题!伟大无需多言
)

查找

布谷鸟过滤器的查找过程很简单,给定一个项x,算法首先根据上述插入公式,计算x的指纹和两个候选桶。然后读取这两个桶:如果两个桶中的任何现有指纹匹配,则布谷鸟过滤器返回true,否则过滤器返回false。此时,只要不发生桶溢出,就可以确保没有假阴性。(这句话的意思是:我们在插入的时候可能会有踢出的操作嘛,那么在桶不够的情况下,再踢出后你的指纹就被踢出了呀,然后我们在判断的时候会出现我们原本添加了但是现在结果是找不到)

仍然存在假阳性哦,基本上跟hash有关都有一定的假阳性,我们可能出现hash映射到同样两个桶对吧,那么在他的指纹计算也相同时,不就是假阳性了吗?

删除

很容易啦,我们关键就是在对应x的槽上放了个x对应的指纹,指纹删掉就ok了

支持扩容(普通布谷鸟是不行的,普通bloom也是不行的)

这里基本上要涉及论文了,我懒得看了..,讲述我个人的猜测,不过别人的应该不会这么简单

我的猜测就是把原先能得出hash函数的得出的指纹来当做新的要设置的值,因为我们通过得出指纹的hash得到的值应该是与原本的空间是无关的,那么也就是说这里可以当做不变量,且上文的查询操作也指出其实关键是验证指纹是否相同在这两个坑中,所以这个指纹也本身具有一定的特性,有点类似责任链的思想,弄了个hash链,不过碰撞的概率应该也不会提升太大

缺点

  • 删除不完美,存在误删的概率。
    删除的时候知识删除了一份指纹副本,并不能确定此指纹副本是要删除的key的指纹。同时这个问题也导致了假阳性的情况。
  • 插入次数可能会有多次

参考

https://www.cnblogs.com/zhaodongge/p/15067657.html
https://github.com/CGCL-codes/DCF

在 MES 开发领域,想要从 PLC 获取数据就必须要和 PLC 有信号交互。高效准确的获取 PLC 数据一直是优秀 MES 系统开发的目标之一。初涉相关系统开发的工程师往往不能很好的理解 PLC 和 MES 之间编程逻辑的本质差别,在设计交互逻辑是难免顾此失彼。因此本文结合本人这些年来和 PLC 交互的经验总结出一下几种交互方式和各自特点。

一、周期读取

周期读取就是按固定时间间隔来读取 PLC 数据。这种方式多用于读取设备心跳数据,设备工作状态,设备一般运行信息等,MES 只需要显示状态不需要进行逻辑的数据。

二、单向触发

单向触发是在 PLC 中定义一个触发点,在特定条件满足时把点位置为1,MES 读取到出发点跳变为1时,执行预定义的处理。 这种方式 PLC 和 MES 之间没有形成交互,无法实现相互动作的锁定。需要处理好触发时长和触发间隔,不然有丢失数据的风险。

以托盘进站交互作为例子,托盘进入工位后,工位 PLC 读取 RFID 内的托盘信息。之后置位托盘进站请求。此时 MES 以 1s 为周期的来读取托盘进站点位,当该点位由0变为1时读取工位信息并保存。如此循环。

交互时序图:

具体交互过程:

  1. PLC 准备工位信息
  2. PLC 置位托盘进站点位并保持 3s
  3. MES 读取到托盘进站点位上升沿
  4. MES 执行预定义的动作

三、一次交互(自复位)

单次交互虽然解决了从 PLC 获取数据的问题,但是非常依赖时间序列来实现交互关系。这样一个可靠的交互过程往往需要花费数秒的时间。一个工位通常又需要数次交互才能完成一个操作循环。这在分秒必争的工位节拍里是完全不可接受的。

以托盘进站交互为例子,设置了 PLC 和 MES 双方的交互点位,让双方可以知道对方状态并执行相应的动作。

交互时序图:

具体交互过程:

  1. PLC 准备工位信息
  2. PLC 置位托盘进站点位
  3. MES 反馈信息
  4. MES 反馈动作结果PLC 收到 MES 反馈结果后复位托盘进站点位和工位信息
  5. MES 收到 PLC 复位进站点位后复位反馈信息和进站结果

四、二次交互(手动复位)

二次交互在一次交互的基础上增加了手动复位功能。拥有了更加完善且完全可控的交互过程。可以通过组合的方式,实现各种复杂的交互逻辑。

交互时序图:

具体交互过程:

  1. PLC 准备工位信息
  2. PLC 置位托盘进站点位
  3. MES 反馈信息
  4. MES 反馈动作结果 PLC 收到 MES 反馈结果后复位托盘进站点位
  5. PLC 置位复位托盘进站点位,MES 收到 PLC 复位进站点位后复位反馈信息和进站结果
  6. PLC 收到 MES 进站结果复位后,复位复位托盘进站点位
总结
以上四种方式从简单到复杂,也是笔者这十多年做交互逻辑时边实践边思考的总结。这一套交互逻辑的目标就是,最大程度的减少交互过程的复杂度的同时,又能够方便 PLC 工程师和 MES 工程师理解并实现,还能满足复杂功能的需要。典型的既要又要。希望这里的一点经验能,给新人提供一个方向,也给高手提供一个新的角度来共同探讨。

从最简单的例子入手分析 PixiJS 源码

image

我一般是以使用角度作为切入点查看分析源码,例子中用到什么类,什么方法,再入源码。

高屋建瓴的角度咱也做不到啊,毕竟水平有限

pixijs 的源码之前折腾了半天都运行不起来,文档也没有明确说明如何调式
我在 github 上看到过也有歪果仁在问如何本地调式最后他放弃了转用了别的库...
还有就是 npm 在我们迷之大陆确实不太友好

源码 pixijs 7.3.2 版下载地址
https://github.com/pixijs/pixijs/tree/v7.3.2

本地调式环境说明

npm 8.19.2

Node.js v16.18.0

安装命令

npm install

运行命令

npm start

serve 静态服务器全局安装

https://www.npmjs.com/package/serve

源码目录结构

  • 根目录
    • bundles 打包后源码
    • examples 例子
    • packages 源码
    • scripts 工程脚本
    • test 测试目录 (我们用不到)
    • tools 服务于测试的工具目录 (我们用不到)

项目源码根目录下有个主包的 package.json name 是 pixi.js-monorepo

从名字可以看出来,这个项目是用 monorepo 方式来组织管理代码的

在 rollup.config.mjs 配置文件内配置有一个方法:

await workspacesRun.default({ cwd: process.cwd(), orderByDeps: true }, async (pkg) =>
{
    if (!pkg.config.private)
    {
        packages.push(pkg);
    }
});

主要作用就是遍历所有子项目,将非私有项目加入到 'packages' 数组变量中,然后分析依赖关系再打包输出

PixiJS 源码在 packages 目录

/packages 目录下每一个 "大类" 模块都是单独的项目

每一个 "大类" 都有自己单独 package.json 文件, 在 package.json 文件内指定自己的依赖

比如 app 模块的 package.json 文件内指定了依赖:

"peerDependencies": {
    "@pixi/core": "file:../core",
    "@pixi/display": "file:../display"
  }

其中的 src 就是此"大类"源码目录,与 src 同级的 test 是此"大类"的测试用例

调式过程中我发现编译真的挺慢的 ...

调式步骤

为了调式大致需要以下几步

  1. npm install 安装依赖包
  2. npm start 将源码运行起来
  3. 我就将调式用的 html 网页放到 example 文件夹下
  4. 在 html 文件中引用
    <script src="/bundles/pixi.js/dist/pixi.js"></script>
  5. terminal 在根目录起一个 serve 静态服务
    serve .
  6. 浏览器访问静态服务跳转到 example 目录下的具体 html 例子中

完成以上步骤后,你就可以在 /packages 目录下的任意源码内添加 console.log 或 debugger 进行源码调式了

相信上面步骤最大的挑战是
npm install
T_T!

尝试第一个源码调式

源码中添加一个 console.log 看看能不能成功输出先

测试的 example/simple.html 文件如下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title> 最简单的例子 </title>
  <style type="text/css">
    *{
      margin: 0;
      padding: 0;
    }
  </style>
</head>
<body>
  <script src="/bundles/pixi.js/dist/pixi.js"></script>
  <script type="text/javascript">
    const app = new PIXI.Application({ width: 800, height: 600 });  
    document.body.appendChild(app.view);  
  
    const rectangle = PIXI.Sprite.from('logo.png');  
    rectangle.x = 100;  
    rectangle.y = 100;  
    rectangle.anchor.set(0.5);  
    rectangle.rotation = Math.PI / 4;  
    app.stage.addChild(rectangle);  
  
    app.ticker.add(() => {  
      rectangle.rotation += 0.01;  
    });  
  </script>

</body>

</html>

以上例子中实现的功能:

  1. simple.html 首先中引入 pixi.js 文件
  2. 通过 new PIXI.Application 建一个 800*800 的画布实例 app
  3. 利用 PIXI.Sprite.from 方法引入 logo.png 图片实例 rectangle
  4. 为 rectangle 设置坐标、anchor、旋转角度
  5. 通过 app.stage.addChild 将 rectangle添加到舞台上
  6. 在 app.ticker 定时器内添加一个回调用定时更新旋转

如果你在本地服务器环境下打开 simple.html 文件,你将会看到一个旋转的 logo.png

这里用到了二个类 Application、Sprite

Application 类是 PixiJS 的入口类在
/packages/app/src/Application.ts

源码中已说明这个类是创建 PixiJS 应用的便捷类,这个类会自动创建 renderer, ticker 和 root container

Application.ts 源码的 constructor 构造方法内添加个 console.log 试试能不能成功输出

Application.ts 71-85 行

  constructor(options)
  {
      // The default options
      options = Object.assign({
          forceCanvas: false,
      }, options);

      this.renderer = autoDetectRenderer(options);
      console.log('hello', 88888);
      // install plugins here
      Application._plugins.forEach((plugin) =>
      {
          plugin.init.call(this, options);
      });
  }

移除掉 typescript 类型的源码果然看起来眉清目秀一些

成功的关键要注意两点

  1. 先 npm start 项目, 作用是 watch 源码变化自动化编译到 bundles 目录

  2. 确保你是在本地服务器环境下打开网页就像这样访问
    http://localhost:3000/examples/simple

打开网页调式器如果输出
hello 88888
就说明成功可以调式源码了

Amazing!

Application 的构造方法就做了两件事,创建渲染器 (renderer) 和 初始化插件 (plugin)

renderer 是 PixiJS 的渲染器,渲染器会根据浏览器环境自动选择渲染方式,如 WebGL、Canvas

_plugins 静态属性是一个用于存放插件数组

Application 类本身的其它主要属性:

  • stage 主要用于添加子显示对象
  • renderer 渲染器
  • view canvas dom 渲染 元素引用
  • screen 屏幕信息,更准确的说应该是画布信息,x,y,width,height

在例子代码中
app.ticker
ticker 对象即是
/packages/ticker/TickerPlugin.ts
"定时器" 插件, 后面会深入其源码细节

autoDetectRenderer

autoDetectRenderer 用于自动判断使用哪种方式渲染,如 WebGL、Canvas

/packages/core/src/autoDetectRenderer.ts
第 41-52 行

export function autoDetectRenderer<VIEW extends ICanvas = ICanvas>(options?: Partial<IRendererOptionsAuto>): IRenderer<VIEW>
{
    for (const RendererType of renderers)
    {
        if (RendererType.test(options))
        {
            return new RendererType(options) as IRenderer<VIEW>;
        }
    }

    throw new Error('Unable to auto-detect a suitable renderer.');
}

显然, 通过循环检测所有的 renderers 渲染器类型 与构造函数传递过来的 options 参数进行检测返回符合条件的渲染器

RendererType.test 就是渲染器的一个检测方法

而 renderers 数组就定义在了第 29 -32 行

const renderers: IRendererConstructor<ICanvas>[] = [];

extensions.handleByList(ExtensionType.Renderer, renderers);

这里又用到了一个叫
extensions
的全局对象,这个全局对象顾名思议,就是用来管理所有扩展插件的,嗯,所以渲染器也是一个 extension

extensions 扩展插件简介

扩展插件源码文件
/packages/extensions/src/index.ts

官方的插件的类型有这些:

'renderer'
'application'
'renderer-webgl-system'
'renderer-webgl-plugin'
'renderer-canvas-system'
'renderer-canvas-plugin'
'asset'
'load-parser'
'resolve-parser'
'cache-parser'
'detection-parser

具体插件类或对象都是注册到对应的类型下的

类先通过 extensions 全局对象的
handleByList

handleByMap
方法注册插件类型

当真正添加插件时,调用的是 extensions 全局对象的
add
方法插件就会添加到对应的插件类型下

比如 TikerPlugin.ts ResizePlugin.ts 就是注册到了 'application' 类型下

又比如 load 相关的插件就注册到了 'load-parser' 类型下

最后具体的插件是注册到具体类的 _plugins 属性上比如: Application._plugins


/packages/extensions/src/index.ts
文件中第 240-265 行,找到
handleByList
方法

在 extensions/index.ts 244 行加个 console.log 打印一下:

handleByList(type: ExtensionType, list: any[], defaultPriority = -1)
  {
      return this.handle(
          type,
          (extension) =>
          {
              if (list.includes(extension.ref))
              {
                  return;
              }
              console.log(extension.ref);
              list.push(extension.ref);
              list.sort((a, b) => normalizePriority(b, defaultPriority) - normalizePriority(a, defaultPriority));
          },
          (extension) =>
          {
              const index = list.indexOf(extension.ref);

              if (index !== -1)
              {
                  list.splice(index, 1);
              }
          }
      );
  },

输出:

image

图 1-1

可以看到输出了一堆 class 和 对象 (实现了 ExtensionFormat "接口" 的对象), 只知道有这些,现在还不知道具体干啥

把 handleByList 方法的 type 和 list 也打印出来看看

image

图 2-2

可以看到每个插件类型都可以拥有多个 extention 数组

再看看它的 add 方法

在 extensions/index.ts 152 - 175 行

add(...extensions: Array<ExtensionFormatLoose | any>)
{
    extensions.map(normalizeExtension).forEach((ext) =>
    {
        ext.type.forEach((type) =>
        {
            const handlers = this._addHandlers;
            const queue = this._queue;
            // 如果添加的插件还没有插件类型,就放到 _queue 内存起来
            if (!handlers[type])
            {
                queue[type] = queue[type] || [];
                queue[type].push(ext);
            }
            else
            {
                // 如果已经有相应的插件类型了,就添加到对应插件类型下
                handlers[type](ext);
            }
        });
    });

    return this;
},

可以看到它接收一个插件数组对象 'extensions' 将传进来的对象进行 '插件对象标准化'后,该对象拥有 type, name, priority, ref 这些属性

interface ExtensionFormatLoose
{
    type: ExtensionType | ExtensionType[];
    
    name?: string;
    
    priority?: number;
    
    ref: any;
}

解耦与注入插件

PixiJS 这种插件方式的设计就是为了解耦,方便管理和扩展更多插件

逻辑如下:

  1. Application.ts 在全局 extensions 对象中注册插件类型并传入用于存储插件的数组
    extensions.handleByList(ExtensionType.Application, Application._plugins);

  2. TickerPlugin.ts 在 extensions 注入至对应的 Application 类型插件数组
    extensions.add(TickerPlugin);

  3. Application.ts 在实例化时会它所有插件的 init 方法,将插件也“实例化”

  4. 其它插件或自定义插件实现注册与调用同样适用,不需要再进入 Application.ts 修改逻辑实现解耦

我们以
/packages/ticker/TickerPlugin.ts
时钟插件举例

在 tickerPlugin.ts 文件的最后一行有一句
extensions.add(TickerPlugin);

这一句就是将 TickerPlugin 对象添加到了Application 类的 _plugins 数组

TickerPlugin.ts 35 行标明了这个扩展属于 Application 类

static extension: ExtensionMetadata = ExtensionType.Application;

仔细观察 TickerPlugin.ts 文件,发现它并没有 constructor 构造函数

而是有一个公开的
init
函数,这个函数就是插件的入口函数,它会被 Application 构造函数调用并将 this 指向了 Application 对象本身

所以在
init
函数内访问的 this 就是 Application 对象本身

Ticker

我们都知道与浏览器的自动更新渲染方式不同,在 canvas 更新渲染画面都是通过手动擦掉旧的像素重新绘制新像素实现的

时钟插件很大一部分工作就是用于管理渲染更新的,它属于 Application 类的扩展插件.

在 TickerPlugin.ts 的 init 方法内,文件第 115 行

this.ticker = options.sharedTicker ? Ticker.shared : new Ticker();

即说明实例化 Application 后自动创建了一个 Ticker 实例, sharedTicker 看名字就知道是个共享的时钟

共有三种 ticker: sharedTicker, systemTicker, 普通 ticker

只要 this.ticker 被赋值,旧的 Application render 方法会删除并添加一个新的 render 回调进入 ticker 队列, 还有个 UPDATE_PRIORITY.LOW 用来管理回调队列的优先级

TickerPlugin.ts 的 init 方法内,文件第 57 - 75 行:

Object.defineProperty(this, 'ticker',
    {
        set(ticker)
        {
            if (this._ticker)
            {
                this._ticker.remove(this.render, this);
            }
            this._ticker = ticker;
            if (ticker)
            {
                ticker.add(this.render, this, UPDATE_PRIORITY.LOW);
            }
        },
        get()
        {
            return this._ticker;
        },
    });

让我们进入 Ticker.ts 类看看

渲染相关的回调通过 Ticker.add 和 Ticker.addOnce 添加加到 Ticker 类中

顾名思义 addOnce 就是一次性的回调,我们只要理解 add 方法就可以了

Ticker.ts 198 - 201 行:

add<T = any>(fn: TickerCallback<T>, context?: T, priority = UPDATE_PRIORITY.NORMAL): this
{
    return this._addListener(new TickerListener(fn, context, priority));
}

渲染回调还用 TickerListener.ts 类,包装了一下,包装的主要目的是将相应的渲染回调函数根据 priority 权重组成一个回调 “链表队列”

priority 权重在
/packages/ticker/const.ts
定义

TickerListener.ts 类主要的两个方法: emit 触发函数和 connect 连接函数

/packages/ticker/TickerListener.ts
97 - 106 行 connect 函数:

connect(previous: TickerListener): void
{
    this.previous = previous;
    if (previous.next)
    {
        previous.next.previous = this;
    }
    this.next = previous.next;
    previous.next = this;
}

得结合 Ticker 类的 _addListener 一起看:

/packages/ticker/Ticker.ts
223 - 258 行:

private _addListener(listener: TickerListener): this
{
    // For attaching to head
    let current = this._head.next;
    let previous = this._head;

    // 如果还没有添过,就添加到 _head 后面
    if (!current)
    {
        listener.connect(previous);
    }
    else
    {
        // priority 优先级从最高到最低
        while (current)
        {
            if (listener.priority > current.priority)
            {
                listener.connect(previous);
                break;
            }
            previous = current;
            current = current.next;
        }

        // 如果还没有加入到链表中,则加入到链表尾部
        if (!listener.previous)
        {
            listener.connect(previous);
        }
    }

    this._startIfPossible();

    return this;
}

可以看到通过 while 循环整个 this._head 存储的链表,根据 priority 权重找到需要插入的位置,然后插入到链表中。

如果没找到位置,则添加到链表最后

_addListener 函数最后还调用了 _startIfPossible 既而调用了 _requestIfNeeded 方法

_requestIfNeeded 即刻发起 this._tick “请求”

private _requestIfNeeded(): void
{
    if (this._requestId === null && this._head.next)
    {
        // ensure callbacks get correct delta
        this.lastTime = performance.now();
        this._lastFrame = this.lastTime;
        this._requestId = requestAnimationFrame(this._tick);
    }
}

this._tick
函数定义在 Ticker.ts 的构造函数内

/packages/ticker/Ticker.ts
116 - 137 行

constructor()
{
    this._head = new TickerListener(null, null, Infinity);
    this.deltaMS = 1 / Ticker.targetFPMS;
    this.elapsedMS = 1 / Ticker.targetFPMS;

    this._tick = (time: number): void =>
    {
        this._requestId = null;

        if (this.started)
        {
            // 此处触发回调函数,并传入 delta time
            this.update(time);
            // 回调函数执行后可能会影响 ticker状态,所以需要再次检查
            if (this.started && this._requestId === null && this._head.next)
            {
                // 继续执行下一帧
                this._requestId = requestAnimationFrame(this._tick);
            }
        }
    };
}

_tick 函数就是每一帧都会执行

this._head 链表头部,为方便处理统一加一个虚拟头部节点

this.deltaMS 默认为 1/0.06 = 16.66666 刷新率

this.elaspedMS 帧间隔时间

即使你没有往画布中绘制任何图形,也会执行。不信你可以在 _tick 内添加一个 console.log 看看

当 _tick 触发时调用的就是 update 函数

/packages/ticker/Ticker.ts
369 - 442 行

update(currentTime = performance.now()): void
{
    let elapsedMS;

    // update 也可由用户主动触发
    // 如果间隔时间是0或是负数不不需要触发通知回调
    // currentTime 
    if (currentTime > this.lastTime)
    {
        // Save uncapped elapsedMS for measurement
        elapsedMS = this.elapsedMS = currentTime - this.lastTime;

        // cap the milliseconds elapsed used for deltaTime
        if (elapsedMS > this._maxElapsedMS)
        {
            elapsedMS = this._maxElapsedMS;
        }

        elapsedMS *= this.speed;

        // If not enough time has passed, exit the function.
        // Get ready for next frame by setting _lastFrame, but based on _minElapsedMS
        // adjustment to ensure a relatively stable interval.
        if (this._minElapsedMS)
        {
            const delta = currentTime - this._lastFrame | 0;

            if (delta < this._minElapsedMS)
            {
                return;
            }

            this._lastFrame = currentTime - (delta % this._minElapsedMS);
        }

        this.deltaMS = elapsedMS;
        this.deltaTime = this.deltaMS * Ticker.targetFPMS;

        // Cache a local reference, in-case ticker is destroyed
        // during the emit, we can still check for head.next
        const head = this._head;

        // Invoke listeners added to internal emitter
        let listener = head.next;

        while (listener)
        {
            listener = listener.emit(this.deltaTime);
        }

        if (!head.next)
        {
            this._cancelIfNeeded();
        }
    }
    else
    {
        this.deltaTime = this.deltaMS = this.elapsedMS = 0;
    }

    this.lastTime = currentTime;
}

额外小知识

对于需要高精度时间戳的动画或输入处理,performance.now() 可以提供比 Date.now() 更高的精度。

与 requestAnimationFrame 结合使用:

requestAnimationFrame 的回调函数接收一个高精度的时间戳作为参数,这个时间戳与 performance.now() 返回的时间戳是同步的。

因此,你可以使用 performance.now() 来与 requestAnimationFrame 回调中的时间戳进行比较或计算。

需要注意的是,performance.now() 返回的时间戳是相对于某个特定时间点的,而不是绝对的时间(如日期和时间)。因此,它主要用于测量时间间隔,而不是获取当前的日期和时间。

update 方法主要功能就是判断当前时间与上一次调用的时间差,如果大于最大间隔时间(需要更新一帧时)就执行回调链表

listener.emit(this.deltaTime);

注意 listener.emit() 执行后返回的是下一个回调函数,即 listener.next 以完成 while 循环

PixiJS Ticker 与 EaselJS Ticker 的区别

  1. PixiJS Ticker 默认是开启的,EaselJS Ticker 直到有添加 Ticker 回调才开启

  2. PixiJS Ticker 可被实例化,有构造函数,而 EaselJS Ticker 更像是一个全局对象

  3. PixiJS Ticker 回调使用函数采用链表方式存储拥有可调节的权重, EaselJS Ticker 直接使用了 EventDispatcher “标准事件” 方式实现回调,回调使用数组存储没有权重可调节

  4. PixiJS Ticker 使用 requestAnimationFrame 实现 tick,EaselJS Ticker 库较早,所以还支持 setTimeout 方式

本章小节

这一章先介绍源码如何下载并搭建本地调式环境,然后用一个简单的例子来打印出调式信息

以 Appllication 类为入口进入源码, 了解了 PixiJS 的基本扩展插件机制

最后分析最重要的 Ticker 实现

说实话我在现实前端项目中从未用到过链表,很意外在分析PixiJS源码的时候居然发现 Ticker 回调是用链表实现的,look! 没用的知识又增加了!

上面 simple.html 例子中的 PIXI.Sprite 和 app.stage 还没有进入源码, 下一章先尝试进入 stage 这一部分,如果可以的话 Sprite 也过一遍


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

本文使用MySQL原生支持的主从同步机制,详细记录了配置步骤及运维操作方法,可供大家直接参考、使用。
本文假设已经部署了两台主机的MySQL软件,且数据库服务正常,详细部署步骤可本站搜索:"mysql二进制安装包部署"

■■ 主从配置

■ master 授权同步账户

CREATE USER 'repl'@'10.19.238.241' IDENTIFIED WITH MYSQL_NATIVE_PASSWORD by 'passwd';
GRANT REPLICATION SLAVE,REPLICATION CLIENT ON *.* TO 'repl'@'10.19.238.241';
FLUSH PRIVILEGES;

■ master 修改配置文件

vim /etc/my.cnf

# 配置server id,建议采用ip地址最后一段以便于区分
server_id = 240
log_bin = master-bin
binlog_format = ROW

# 作为从库时使用
relay_log = master-relay-bin

# 作为从库时,来自主库的更新操作是否写入日志,默认值为OFF,配置 on 时,其他节点以此节点做主库时才能进行同步
log_slave_updates = on

# 以下两个为可选选项,见字可知意
expire_logs_days = 7
max_binlog_size = 1G

# 设置系统库的日志不计入binlog
# 注:Binlog_Do_DB/Binlog_Ignore_DB 两个参数互斥,一般只选择其一设置,只能在启动命令行中或配置文件中设置
binlog_ignore_db = mysql
binlog_ignore_db = sys
binlog_ignore_db = information_schema
binlog_ignore_db = performance_schema

# 同步时忽略系统库,但 create user 时数据还是会同步的
replicate_ignore_db = mysql
replicate_ignore_db = sys
replicate_ignore_db = information_schema
replicate_ignore_db = performance_schema

# 同步时忽略系统库的表,这样在 create user 时也不会进行同步了
replicate_wild_ignore_table = mysql.%
replicate_wild_ignore_table = sys.%
replicate_wild_ignore_table = information_schema.%
replicate_wild_ignore_table = performance_schema.%

■ master 服务重启

参见前文:"mysql二进制安装包部署"

■ slave 修改配置文件

vim /etc/my.cnf
同 master,只修改如下3项:

server_id = 25
log_bin = slave-bin
relay_log = slave-relay-bin

■ slave 服务重启

参见前文:"mysql二进制安装包部署"

■ master 查看 binlog 信息

show master status;
执行之后,可获取目前主库 binlog 使用的 File 及 Position 点,类似如下:

+-------------------+----------+--------------+-------------------------------------------------+-------------------+
| File              | Position | Binlog_Do_DB | Binlog_Ignore_DB                                | Executed_Gtid_Set |
+-------------------+----------+--------------+-------------------------------------------------+-------------------+
| master-bin.000007 |      157 |              | mysql,sys,information_schema,performance_schema |                   |
+-------------------+----------+--------------+-------------------------------------------------+-------------------+

■ slave 设置复制信息

根据上一步获取到的信息(File 及 Position)配置从库的复制信息,在从库上执行如下命令

CHANGE MASTER TO MASTER_HOST='10.19.238.240',MASTER_PORT=3306,MASTER_USER='repl',MASTER_PASSWORD='passwd',MASTER_LOG_FILE='master-bin.000006',MASTER_LOG_POS=1195;

■ slave 开启复制

start slave;

■■ 常用操作

■ 查看主从复制状态

show slave status \G
主要查看下面两个参数状态,只要都是yes,表示主从通信正常:
Slave_IO_Running=Yes
Slave_SQL_Running=Yes

■ 启动/停止复制

start slave;
stop slave;

■ reset master;

在master上执行,作用:
删除binlog索引文件中列出的所有binlog文件
清空binlog索引文件
创建一个新的binlog文件
清空系统变量gtid_purged和gtid_executed
在MySQL 5.7.5 及后续版本中, RESET MASTER还会会清空 mysql.gtid_executed 数据表

■ reset slave;

在slave上执行,作用:
清除slave 复制时的master binlog的位置
清空master info, relay log info
删除所有的relay log文件,并创建一个新的relay log文件。
重置复制延迟(CHANGE MASTER TO 的 MASTER_DELAY参数指定的)为0

注:
RESET SLAVE 不会改变 gtid_executed or gtid_purged.
RESET SLAVE 不会改变复制连接使用的参数,例如 master host, master port, master user, or master password
如果要重置这些连接参数,需要使用命令:
RESET SLAVE ALL
重置操作之后,就需要使用 CHANGE MASTER TO 重新指定复制连接参数。

■ 注意事项

如果出现同步失败,可以根据提示处理错误,处理完成后,需要刷新同步配置:
先停止同步
stop slave sql_thread;
stop slave;
清理掉之前的配置,防止同步已经同步了的数据
reset slave all;
然后重新连接主库,进行同步。

■ 主从节点的重启步骤

停应用 ->停数据库(先备后主)-> 启数据库(先主后备)-> 启应用

1、关闭从库,在从库操作
a.先查看当前的主从同步状态 show slave status\G; 看是否双yes
b.执行stop slave
c.停止从库服务
d.查看是否还有mysql的进程ps -ef | grep mysql

2、关闭主库,在主库操作
a.停止主库服务
b.查看是否还有mysql的进程ps -ef | grep mysql

3、启动主库,在主库操作
a.启动主库服务
b.查看mysql的进程ps -ef | grep mysql

4、启动从库,在从库操作
a.启动从库服务
b.启动复制start slave;
c.检查同步状态 show slave status\G; 是否双yes
d.查看mysql的进程ps -ef | grep mysql

萝卜快跑是百度旗下的自动驾驶出行服务平台,提供
全无人、半无人、有人驾驶三种模式的出行选择
,目前已经正式投放到武汉市场开始运营了。

我们可以通过百度地图 APP、“萝卜快跑”小程序等打到“萝卜快跑”无人驾驶网约车。不过目前“萝卜快跑”只开放了汽车的后排座位,所以一辆车最多能乘坐三人。

也有很多人会担忧无人网约车不安全,或者会不会抢占网约车司机的“铁饭碗”?前一个问题好回答,如果无人网约车不安全,那么也不会正式投向市场,而且 AI 的进化速度是以秒计算的,它进化速度很快,一定是越来越安全,越来越智能的。

至于会不会淘汰掉更多网约车司机?我想大家都已经有答案了,历史的车轮也不会因为个人的意志,或某个群体的利益而停止,我们能做的就是紧跟历史的潮流,拥抱变化,和未来一起变的越来越好。

所以,当萝卜快跑刷屏的时候,作为技术男的我,考虑的是无人网约车背后需要什么样的岗位?

带着这样的疑问,我打开了萝卜快跑的官网:
https://www.apollo.auto/
image.png
纳尼?还有 Apollo 开发平台:
image.png
意味着,我们可以在萝卜的基础上做点自己应用上去?这可能就是普通开发者可以去尝试的一个方向,大的技术凭借个人的力量是很难完成的,但跟随趋势,顺势而为,做点实用的好用程序和产品出来,也许也是一个不错的方向。

书回正传,咱们需要看的是萝卜快跑到底要找哪些岗位的哪些人?

拉到网页最后,点击“加入我们”:
image.png
来到了百度招聘页,如下图所示:
image.png
很开心,在萝卜团队第一页看到了 Java 研发工程师,yyds,兄弟们招聘的链接发你们了:
https://talent.baidu.com/jobs/social-list?search=智能驾驶事业群组

感兴趣的抓紧时间投起来。

本文已收录到我的面试小站
www.javacn.site
,其中包含的内容有:Redis、JVM、并发、并发、MySQL、Spring、Spring MVC、Spring Boot、Spring Cloud、MyBatis、设计模式、消息队列等模块。