分类 其它 下的文章

在鸿蒙开发中,系统提供了PhotoViewPicker选择图片,cameraPicker拍照和录视频,此方式能够达到无需向用户申请权限就能选图片、拍照和录视频。除了用户操作的选图、拍照和录视频,其它时间APP无权获取相册图片,无权调用相机,对于用户来说,使用相册和相机都是很明确的时机,安全性非常高。以下来看看具体的使用方式。

使用PhotoViewPicker选择图片

我们new一个PhotoViewPicker并调用select方法,就可以打开系统图片选择器进行图片选择了,支持的参数非常多,基本满足了日常的使用场景。

  • MIMEType:我们可以通过MIMEType来设置是选择图片或者选择视频,甚至是同时选择图片和视频。
  • isEditSupported:通过isEditSupported参数来设置选择好图片后,是否进入图片编辑模式。
  • isOriginalSupported:通过isOriginalSupported参数来设置选择的图片是否为源图,该参数设置为true,选择页面有一个按钮,可以控制是否使用源图。
  • isPhotoTakingSupported:在图片选择页面,可以通过isPhotoTakingSupported参数设置第一个位置是否为拍照选项,设置后点击第一个位置,可以打开相机拍摄一张图片。
  • isPreviewForSingleSelectionSupported:通过isPreviewForSingleSelectionSupported参数可以在图片选择后,进入大图预览页面,使我们能够查看图片的更多细节。
  • isSearchSupported:当相册的图片太多时,我们可以通过isSearchSupported参数显示搜索框,在搜索框中输入图片相关内容,以便快速查找相关图片
  • maxSelectNumber:通过maxSelectNumber来设置选择的数量,默认选择50张图片,最多选择500张。
  • preselectedUris:当为多选模式时,通过preselectedUris参数可以预选中一些图片,以便避免每次打开图片选择器,都是重新选择,从而导致之前选择的图片丢失了。
    一个完整的示例如下
new photoAccessHelper.PhotoViewPicker().select({
  MIMEType: photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE,
  maxSelectNumber: 9,
  isEditSupported: false,
  isOriginalSupported: rue,
  preselectedUris: [],
  isPhotoTakingSupported: false,
  isPreviewForSingleSelectionSupported: false,
  isSearchSupported: false
}, (err, data) => {
  console.log(data)
})

使用cameraPicker拍照和录视频

我们可以通过cameraPicker的select方法来进行拍照或录视频,也可以同时设置拍照和录视频,用户在拍照页面自行选择是拍照还是录视频。具体使用参数如下

  • mediaTypes:是一个数组,设置为拍照片,还是录视频,也可以同时设置
  • cameraPosition:设置是使用前置摄像头还是后置摄像头进行拍照或录视频
  • saveUri:设置拍照或录制的视频保存的位置,通常设置成我们的沙箱目录
  • videoDuration:当模式为录视频时,此参数为设置视频的最大时长。
    一个完整的示例如下
cameraPicker.pick(context,[cameraPicker.PickerMediaType.PHOTO, cameraPicker.PickerMediaType.VIDEO],
  { cameraPosition: camera.CameraPosition.CAMERA_POSITION_BACK,saveUri:'',videoDuration:30 })
  .then((value) => {
    console.log(value.resultUri)
  })

用户在系统拍照录视频页面,可以自己选择很多参数,例如是否打开闪光灯,图片的宽高比,视频的帧率,是否撕开参考线等等,可选择的参数很多,灵活度也很高,基本可以满足日常的使用场景,最主要的是不用向用户申请权限,还是非常方便的。

相关:

NVIDIA机器人仿真项目 —— Isaac Gym - Preview Release


本文说下NVIDIA公司的机器人仿真项目的一些历史发展。

NVIDIA公司的产品最初只有显卡,但是卖着卖着发现有很多人用CPU搞图形化设计,比如OpenGL,于是老黄大受启发,从此NVIDIA的显卡开发计算接口支持图形化计算,慢慢的NVIDIA的显卡被美国、中国等国家的超算机构所采用,于是NVIDIA公司就出现了家用游戏显卡和企业用计算显卡两种硬件产品以及图形化计算平台这个软件产品;随着AI时代的火热,Deep learning下的时代来临,NVIDIA公司在之前的三个产品(家用游戏显卡和企业用计算显卡两种硬件产品以及图形化计算平台)的基础上建立了AI模型库集成AI软件集成平台,然后随着AI平台在监督学习和无监督学习上集成的AI模型初具规模后,NVIDIA公司开始着眼于强化学习以及元宇宙概念,于是就有了NVIDIA公司的机器人仿真平台,这也是本文所要介绍的内容。


NVIDIA公司最初推出的机器人仿真平台是NVIDIA issac gym,这个项目一共出了四个版本,preview 1,preview 2,preview 3,preview 4,可以说这个issac gym是NVIDIA公司在机器人仿真方向上的大始祖,下面给出其中的第一、二、四版本的下载资源:

https://openi.pcl.ac.cn/devilmaycry812839668/issac_gym_store

由于是初代产品,因此这个Isaac gym平台只能够导入3D模型(urdf、mjdf文件),而不能提供3D设计的操作平台,并且将仿真平台和RL算法等AI算法集成在了一起,随着时间发展,这个产品逐渐成熟,于是在preview 4版本时将AI算法的接口和算法库单独拿了出来形成了IsaacGymEnvs项目:

https://github.com/NVIDIA-Omniverse/IsaacGymEnvs


由于NVIDIA公司认为机器人的AI平台是未来走向通用人工智能(AGI)的重要途径,于是不断加码这个领域的投入,于是就出现了多个部门同时在issac gym的基础上进行扩展开发,因此在issac gym项目抽取出issac gym envs项目后又将issac gym项目和issac gym envs项目升级为OmniIsaacGymEnvs项目,此时的重点是加强3D设计的操作界面做准备,也就是加入元宇宙概念,Omniverse:

https://github.com/NVIDIA-Omniverse/OmniIsaacGymEnvs

其实这里的元宇宙概念与之前的issac gym项目和issac gym envs项目的区别就是其内部的C++实现接口进行了升级,这样可以在下一个升级版本中加入3D设计的操作接口,这样就可以在一个项目中进行3D设计从而生成urdf和mjdf文件,而不需要从第三方仿真设计平台导入了,因此虽然说OmniIsaacGymEnvs是issac gym和issac gym envs的升级,但是对于使用者来说其实并没有太实质的变化,因为这个升级主要是为了服务其内部开发人员的。


在nvidia公司将issac gym和issac gym envs升级为OmniIsaacGymEnvs的同时也进行了fork操作,也就是在issac gym和issac gym envs的基础上又开了一个独立分支,这个分支也是为了元宇宙概念服务的,在这个新分支中考虑对更多的输入数据类型进行处理并且加入更多的AI模型的接入接口,于是有了orbit项目:

https://github.com/NVIDIA-Omniverse/orbit


因为NVIDIA公司的对机器人项目的投入是一个点开始,然后从一条线分出多条线独立发展,然后再将多条线合并到一起的发展路子,因此OmniIsaacGymEnvs和orbit都可以认为是issac gym项目基础上结合不同的元宇宙发展需要的后期接口进行fork后独立发展的,在issac gym项目分叉为OmniIsaacGymEnvs和orbit项目后,NVIDIA公司又决定将这几个项目合并成一个项目,这就是issac sim项目,地址:

https://developer.nvidia.cn/isaac/sim

可以说issac sim是NVIDIA公司的元宇宙技术平台的一个阶段性的汇总,在NVIDIA公司将issac gym项目升级为IsaacGymEnvs,然后再升级为OmniIsaacGymEnvs和orbit的同时,NVIDIA公司也在issac gym的最原始版本开出了其他路线的项目,如:Isaac ROS和Isaac Manipulator,其中Isaac ROS可以看做是issac gym和ROS软件平台的集成,而Isaac Manipulator可以看做是issac gym和视觉识别系统和动作规划系统的集成,于是issac gym发展成了今天的NVIDIA的issac家族,具体如下:

1、Isaac ROS:

https://developer.nvidia.cn/isaac/ros

体验参考指南标准:

https://img-bss.csdnimg.cn/bss/NVIDIA/IsaacROS.docx

2、Isaac Manipulator:

https://developer.nvidia.cn/isaac/manipulator

体验参考指南标准:

https://img-bss.csdnimg.cn/bss/NVIDIA/IsaacManipulator.docx

3、NVIDIA Isaac Sim:

https://developer.nvidia.cn/isaac/sim

体验参考指南标准:

https://img-bss.csdnimg.cn/bss/NVIDIA/IsaacSim.docx


issac sim可以看做是在issac gymenvs基础上加入了3D设计的图形化操作界面等支持其他功能接口的一次升级。


在NVIDIA推出NVIDIA issac家族后又进行了升级,推出了一个新产品,那就是issac Lab;issac Lab在issac sim基础上进行的,于是就从issac家族再次升级为issac Lab家族,其中issac Lab家族可以看做是在issac家族基础上加入了输入数据类型扩增的一次升级,因此在windows平台下issac sim平台就代表了issac lab平台,这二者是同义词,但是在Linux平台下二者有着不同的下载地址:

Isaac lab的主页地址:

https://developer.nvidia.com/isaac/lab?ncid=ref-inor-146211

Isaac sim的主页地址:

https://developer.nvidia.com/isaac/sim/


可以说issac lab是在issac sim的基础上增加一些功能后的产品,可以看作是一次升级,因此现在NVIDIA的维护项目为issac sim和issac lab项目,而之前的issac gym/issac gym envs/OmniIsaacGymEnvs/orbit项目都已经不再维护了。

因为我是从issac gym项目就关注NVIDIA公司的产品,因为我是从事RL方向的,于是就发现了NVIDIA公司在同一个产品上不对的升级,而且是多部门从事搞,于是就fork出了多个项目,然后项目开发到一定时候就merge到一起,然后再开发遇到问题,然后再fork出分支,于是就出现了今天的这种要人感觉乱七八糟的感觉,如果是新人入手来学机器人的AI方向,那么之前的版本就可以不用太在意了,直接从issac sim和issac lab开始即可。



扩展:

issac-gym下载地址:

https://developer.nvidia.com/isaac-gym/download

image-20241118120100625


Isaac Sim主页地址:

https://developer.nvidia.com/isaac/sim#isaac-lab

docker安装方式地址:

https://catalog.ngc.nvidia.com/orgs/nvidia/containers/isaac-sim

image-20241118120435405



个人github博客地址:
https://devilmaycry812839668.github.io/

大家好,我是 V 哥,很好奇,在ArkTS中实现异步编程是怎样的,今天的内容来聊聊这个问题,总结了一些学习笔记,分享给大家,在 ArkTS中实现异步编程主要可以通过以下几种方式:

1. 使用
async

await
关键字

async
函数是一种特殊的函数,它能以同步代码的方式编写异步代码。在
async
函数内部,可以使用
await
表达式来等待一个异步操作返回一个Promise对象。这种方式可以让异步代码的编写和阅读更接近同步代码的风格,提高代码的可读性和可维护性。

// 定义一个async函数
async function myAsyncFunction() {
  // 使用await等待异步操作完成
  const result = await someAsyncOperation();
  console.log(result);
}

async函数和await关键字有什么区别?

async

await
是 JavaScript 中用于异步编程的两个关键字,它们共同工作以简化异步代码的编写和理解,但它们各自有不同的用途和含义:

async 关键字

  • async
    是一个用于声明异步函数的关键字。
  • 任何使用
    async
    声明的函数都会自动返回一个
    Promise
    对象。
  • 如果函数正常执行结束,
    Promise
    将被解决(resolve)并返回函数的返回值。
  • 如果函数中抛出错误,
    Promise
    将被拒绝(reject)。
  • async
    函数内部可以包含一个或多个
    await
    表达式。

await 关键字

  • await
    是一个用于等待一个
    Promise
    对象解决(resolve)的关键字。
  • await
    只能在
    async
    函数内部使用。
  • 当执行到
    await
    表达式时,JavaScript 引擎会暂停该
    async
    函数的执行,直到等待的
    Promise
    被解决。
  • 如果
    Promise
    被解决,
    await
    表达式返回
    Promise
    的值。
  • 如果
    Promise
    被拒绝,
    await
    表达式会抛出拒绝的原因,这可以通过
    try...catch
    语句捕获。
  • await
    后面通常跟的是一个异步操作,比如一个返回
    Promise
    的函数调用。

它们的区别

  • async
    是用来声明函数的,而
    await
    是用来在函数内部等待异步操作的。
  • async
    本身不暂停代码执行,它只是让函数返回一个
    Promise
    ;而
    await
    是用来暂停代码执行,直到
    Promise
    被解决。
  • async
    可以不与
    await
    一起使用,这时函数仍然返回一个
    Promise
    ,但不会暂停执行。
  • await
    必须用在
    async
    函数内部,否则会抛出语法错误。

示例

// async 函数声明
async function fetchData() {
  // 等待异步操作完成
  const data = await fetchSomeData(); // 这里使用了 await
  return data;
}

// 单独使用 async
async function justAsync() {
  console.log('This is an async function, but without await.');
}

// 使用 await
async function useAwait() {
  console.log('Before await.');
  await justAsync(); // 等待 justAsync 完成
  console.log('After await.');
}

所以啊, V哥提醒一下,
async

await
是异步编程中两个互补的概念,
async
用于声明异步函数,而
await
用于在异步函数内部等待异步操作的完成,初学者需要注意不要混淆了。

2. 使用Promise对象

上面我们已经提到Promise对象了,Promise是处理异步操作的对象,它提供了一个状态机制来管理异步操作的不同阶段,代表一个异步操作的最终完成(或失败)及其结果值,并提供了一些方法来注册回调函数以处理异步操作的成功或失败的结果。Promise有三种状态:pending(进行中)、fulfilled(已完成)和rejected(已拒绝)。

// 创建一个新的Promise
const myPromise = new Promise((resolve, reject) => {
  // 异步操作
  if (/* 异步操作成功 */) {
    resolve("Success");
  } else {
    reject("Error");
  }
});

// 使用then和catch处理Promise的结果
myPromise.then((value) => {
  console.log(value); // "Success"
}).catch((error) => {
  console.error(error); // "Error"
});

3. 异常处理

在异步函数中,如果出现错误,可以通过
try...catch
语句来捕获异常。

async function myAsyncFunction() {
  try {
    const result = await someAsyncOperation();
    console.log(result);
  } catch (error) {
    console.error(error);
  }
}

4. 并发执行异步任务

可以使用
Promise.all
来并发执行多个异步任务,并等待它们全部完成。

async function concurrentTasks() {
  const [result1, result2] = await Promise.all([
    asyncOperation1(),
    asyncOperation2()
  ]);
  console.log(result1, result2);
}

5. 使用Actor模型

ArkTS语言采用的Actor模型是一种并发编程模型,其中每个Actor都是一个独立的计算单元,拥有自己的状态和行为,并通过消息传递与其他Actor进行通信和协作。

1. Actor模型概述

在Actor模型中,每个Actor都可以接收异步消息,并根据消息内容和当前状态做出响应。Actor之间的消息传递是异步的,发送消息的Actor不需要等待接收消息的Actor的响应,从而实现并发执行。由于每个Actor都是独立的,它们之间不存在共享状态,因此不需要进行锁机制和同步操作,避免了一些常见的并发编程问题,如死锁和竞争条件。

2. 数据传输对象

ArkTS语言支持传输的数据对象可以分为四种:普通对象、可转移对象、可共享对象和Native绑定对象。普通对象的传输是通过结构化克隆算法进行序列化的,支持多种类型,包括基础类型、Date、String、RegExp、Array、Map、Set等。

3. 实现例子

以下是一个使用ArkTS的Actor模型实现的生产者消费者问题的示例:

生产者(Producer)和消费者(Consumer)的Actor模型实现:

import taskpool from '@ohos.taskpool';

// 跨线程并发任务
@Concurrent
async function produce(): Promise<number>{
  // 添加生产相关逻辑
  console.log("test producing...");
  return Math.random();
}

class Consumer {
  public consume(value: number) {
    // 添加消费相关逻辑
    console.log("test consuming value: " + value);
  }
}

@Entry
@Component
struct Index {
  @State message: string = 'Hello I am VG';
  build() {
    Row() {
      Column() {
        Text(this.message)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
        Button() {
          Text("start")
        }.onClick(() => {
          let produceTask: taskpool.Task = new taskpool.Task(produce);
          let consumer: Consumer = new Consumer();
          for (let index: number = 0; index < 10; index++) {
            // 执行生产异步并发任务
            taskpool.execute(produceTask).then((res: number) => {
              consumer.consume(res);
            }).catch((e: Error) => {
              console.error(e.message);
            })
          }
        })
        .width('20%')
        .height('20%')
      }
      .width('100%')
    }
    .height('100%')
  }
}

在这个例子中,
produce
函数是一个异步函数,模拟生产者的生产过程,并返回一个随机数作为产品。
Consumer
类包含
consume
方法,用于消费产品。在UI界面上,当用户点击“start”按钮时,会启动10次生产任务,每次生产任务完成后,都会调用
Consumer

consume
方法来消费产品。

这个例子展示了如何在ArkTS中使用Actor模型来实现并发执行,其中生产者和消费者之间通过异步消息传递来通信,无需等待对方的响应,从而实现高效的并发处理。

最后

ArkTS提供了强大的异步编程能力,使得开发者可以有效地处理异步任务,提高程序的性能和响应能力。对于有 TypeScript 经验的兄弟是不是有种找到真爱的感觉呢。关注威哥爱编程,鸿蒙开天辟地,你我皆是同路人。

前言

watch这个API大家应该都不陌生,在Vue3版本中给watch增加不少有用的功能,比如
deep选项支持传入数字

pause、resume、stop方法

once选项

onCleanup函数
。这些功能大家平时都不怎么用得上,但是在一些特定的场景中,他们能够起大作用,这篇文章欧阳就来带你盘点一下这些功能。

关注公众号:【前端欧阳】,给自己一个进阶vue的机会

deep
支持传入数字

deep
选项大家应该比较熟悉,常见的值为
true
或者
false
,表示是否深度监听
watch
传入的对象。

在Vue3.5版本中对
deep
选项进行了增强,不光支持布尔值,而且还支持传入数字,数字表示需要监听的层数。

比如下面这个例子:

const obj1 = ref({
  a: {
    b: 1,
    c: {
      d: 2,
      e: {
        f: 3,
      },
    },
  },
});

watch(
  obj1,
  () => {
    console.log("监听到obj1变化");
  },
  {
    deep: 3,
  }
);

function changeDeep3Obj() {
  obj1.value.a.c.d = 20;	// 能够触发watch回调
}

function changeDeep4Obj() {
  obj1.value.a.c.e.f = 30;	// 不能触发watch回调
}

在上面的例子
watch

deep
选项值是3,表明监听到对象的第3层。

changeDeep3Obj
函数中就是修改对象的第3层的
d
属性,所以能够触发
watch
的回调。


changeDeep4Obj
函数是修改对象的第4层的
f
属性,所以不能触发
watch
的回调。

他的实现也很简单,我们来看一下deep相关的源码:

function watch(source, cb, options) {
  // ...省略
  if (cb && deep) {
    const depth = deep === true ? Infinity : deep
    getter = () => traverse(baseGetter(), depth)
  }
  // ...省略
}

这里的
depth
就表示watch监听一个对象的深度。

如果
deep
选项的值为true,那么就将
depth
设置为正无穷
Infinity
,说明需要监听到对象的最深处。

如果
deep
选项的值为false,或者没有传入
deep
,那么就表明只需要监听对象的最外层。

如果
deep
选项的值为number类型数字,那么就把这个数字赋给
depth
,表明需要监听到对象的具体某一层。

pause、resume、stop方法

这三个方法也是Vue3.5版本中引入的,通过解构
watch
函数的返回值就可以直接拿到
pause

resume

stop
这三个方法。

我们来看一下源码,其实很简单:

function watch(source, cb, options) {
  // ...省略
  watchHandle.pause = effect.pause.bind(effect)
  watchHandle.resume = effect.resume.bind(effect)
  watchHandle.stop = watchHandle
  return watchHandle
}

watch返回了一个名为
watchHandle
的对象,对象上面有
pause、resume、stop
这三个方法,所以我们可以通过解构
watch
函数的返回值拿到这三个方法。

pause
方法的作用是“暂停”watch回调的触发,也就是说在暂停期间不管watch监听的响应式变量如何改变,他的回调函数都不会触发。

有“暂停”,那么肯定就有“恢复”。

resume
方法的作用是恢复watch回调的触发,此时会主动执行一次watch的回调。后面watch监听的响应式变量改变时,他的回调函数也会触发。

来看个demo,代码如下:

<template>
  <button @click="count++">count++</button>
  <button @click="runner.pause()">暂停</button>
  <button @click="runner.resume()">恢复</button>
  <button @click="runner.stop()">停止</button>
</template>

<script setup lang="ts">
import { watch, ref } from "vue";

const count = ref(0);
const runner = watch(count, () => {
  console.log(count.value);
});
</script>

点击“count++”按钮会导致
watch
回调中的console执行。

但是当我们点击了“暂停”按钮后,此时我们再怎么点击“count++”按钮都不会触发
watch
的回调。

点击
恢复
按钮后会立即触发一次
watch
回调的执行,后面点击“count++”按钮也同样会触发
watch
的回调。

我们来看看
pause

resume
方法的源码,很简单,代码如下:

class ReactiveEffect {
  pause(): void {
    this.flags |= EffectFlags.PAUSED
  }

  resume(): void {
    if (this.flags & EffectFlags.PAUSED) {
      this.flags &= ~EffectFlags.PAUSED
      if (pausedQueueEffects.has(this)) {
        pausedQueueEffects.delete(this)
        this.trigger()
      }
    }
  }

  trigger(): void {
    if (this.flags & EffectFlags.PAUSED) {
      pausedQueueEffects.add(this)
    } else if (this.scheduler) {
      this.scheduler()
    } else {
      this.runIfDirty()
    }
  }
}


pause

resume
方法中通过修改
flags
属性的值,来切换是不是“暂停状态”。

在执行
trigger
方法依赖触发时,就会先去读取
flags
属性判断当前是不是“暂停状态”,如果是那么就不去执行watch的回调。

从上面的代码可以看到这三个方法是在
ReactiveEffect
类上面的,这个
ReactiveEffect
类是Vue的一个底层类,
watch

watchEffect

watchPosEffect

watchSyncEffect
都是基于这个类实现的,所以他们自然也支持
pause

resume

stop
这三个方法。

最后就是
stop
方法了,当你确定后面都不再想要触发watch的回调了,那么就调用这个
stop
方法。代码如下:

const watchHandle: WatchHandle = () => {
  effect.stop()
  if (scope && scope.active) {
    remove(scope.effects, effect)
  }
}

watchHandle.stop = watchHandle

响应式变量
count
收集的订阅者集合中有这个watch回调,所以当
count
的值改变后会触发watch回调。这里的
stop
方法中主要是依靠双向链表将这个watch回调从响应式变量
count
的订阅者集合中给remove掉,所以执行stop方法后无论
count
变量的值如何改变,watch回调也不会再执行了。(PS:如果你看不懂这段话,建议你去看看我的上一篇
Vue3.5双向链表
文章,看完后你就懂了)

once选项

如果你只想让你的watch回调只执行一次,那么可以试试这个
once
选项,这个是在Vue3.4版本中新加的。

看个demo:

<template>
  <button @click="count++">count++</button>
</template>

<script setup lang="ts">
import { watch, ref } from "vue";

const count = ref(0);
watch(
  count,
  () => {
    console.log("once", count.value);
  },
  {
    once: true,
  }
);
</script>

由于使用了
once
选项,所以只有第一次点击“count++”按钮才会触发watch的回调。后面再怎么点击按钮都不会触发watch回调。

我们来看看
once
选项的源码,很简单,代码如下:

function watch(source, cb, options) {
  const watchHandle: WatchHandle = () => {
    effect.stop()
    if (scope && scope.active) {
      remove(scope.effects, effect)
    }
  }

  if (once && cb) {
    const _cb = cb
    cb = (...args) => {
      _cb(...args)
      watchHandle()
    }
  }

  // ...省略
  watchHandle.pause = effect.pause.bind(effect)
  watchHandle.resume = effect.resume.bind(effect)
  watchHandle.stop = watchHandle
  return watchHandle
}

先看中间的代码
if (once && cb)
,这句话的意思是如果
once
选项的值为true,并且也传入了watch回调。那么就封装一层新的
cb
回调函数,在新的回调函数中还是会执行用户传入的watch回调。然后再去执行一个
watchHandle
函数,这个
watchHandle
是不是觉得有点眼熟?

前面讲的
stop
方法其实就是在执行这个
watchHandle
,执行完这个
watchHandle
函数后watch就不再监听
count
变量了,所以后续不管
count
变量怎么修改,watch的回调也不会再触发。

onCleanup函数

有的情况我们需要watch监听一个变量,然后去发起http请求。如果变量改变的很快就会出现第一个请求还没回来,第二个请求就已经发起了。在一些极端情况下还会出现第一个请求的响应比第二个请求的响应还要慢,此时第一个请求的返回值就会覆盖第二个请求的返回值。实际上我们期待最终拿到的是第二个请求的返回值。

这种情况我们就可以使用
onCleanup函数
,他是作为watch回调的第三个参数暴露给我们的。看个例子:

watch(id, async (newId, oldId, onCleanup) => {
  const { response, cancel } = myFetch(newId)
  // 当 `id` 变化时,`cancel` 将被调用,
  // 取消之前的未完成的请求
  onCleanup(cancel)
  data.value = await response
})

watch回调的前两个参数大家都很熟悉:新的id值和旧的id值。第三个参数就是
onCleanup
函数,在watch回调触发之前调用,所以我们可以使用他来cancel掉上一次的请求。

onCleanup
函数的注册也很简单,代码如下:

let boundCleanup

boundCleanup = fn => onWatcherCleanup(fn, false, effect)

function watch(source, cb, options) {
  // ...省略
  const job = (immediateFirstRun?: boolean) => {
    const args = [
      newValue,
      oldValue,
      boundCleanup,
    ]
    cb(...args)
    oldValue = newValue
  }
  // ...省略
}

执行watch回调实际就是在执行这个
job
函数,在
job
函数中执行watch回调时传入了三个参数。分别是
newValue

oldValue

boundCleanup
。前两个参数大家都很熟悉,第三个参数
boundCleanup
是一个函数:
fn => onWatcherCleanup(fn, false, effect)

这个
onWatcherCleanup
大家熟悉不?这也是Vue暴露出来的一个API,注册一个清理函数,在当前侦听器即将重新运行时执行。关于
onWatcherCleanup
之前欧阳写过一篇文章专门讲了如何使用:
使用Vue3.5的onWatcherCleanup封装自动cancel的fetch函数

总结

这篇文章盘点了Vue3 watch新增的一些新功能:
deep选项支持传入数字

pause、resume、stop方法

once选项

onCleanup函数
。这些功能大家平时可能用不上,但是还是要知道有这些功能,因为有的情况下这些功能能够派上大用场。

关注公众号:【前端欧阳】,给自己一个进阶vue的机会

另外欧阳写了一本开源电子书
vue3编译原理揭秘
,看完这本书可以让你对vue编译的认知有质的提升。这本书初、中级前端能看懂,完全免费,只求一个star。

技术背景

在Jax的JIT即时编译中,会追踪每一个Tensor的Shape变化。如果在计算的过程中出现了一些动态Shape的Tensor(Shape大小跟输入的数据有关),那么就无法使用Jax的JIT进行编译优化。最常见的就是
numpy.where
这种操作,因为这个操作返回的是符合判定条件的Index序号,而不同输入对应的输出Index长度一般是不一致的,因此在Jax的JIT中无法对该操作进行编译。当然,需要特别说明的是,
numpy.where
这个操作有两种用法,一种是
numpy.where(condition, 1, 0)
直接给输入打上Mask。另一种用法是
numpy.where(condition)
,这种用法返回的就是一个Index序列,也就是我们需要讨论的应用场景。

普通模式

我们考虑一个比较简单的Toy Model用于测试:

\[E=\sum_{|r_i-r_j|\leq \epsilon}q_iq_j
\]

在不采用即时编译的场景下,Jax的代码可以这么写:

import os
os.environ['XLA_PYTHON_CLIENT_PREALLOCATE']='false'

import numpy as np
np.random.seed(0)
import jax
from jax import numpy as jnp

def func(r, q, cutoff=0.2):
    dis = jnp.abs(r[:, None] - r[None])
    maski, maskj = jnp.where(dis<=cutoff)
    qi = q[maski]
    qj = q[maskj]
    return jnp.sum(qi*qj)

N = 100
r = jnp.array(np.random.random(N), jnp.float32)
q = jnp.array(np.random.random(N), jnp.float32)

print (func(r, q))
# 1035.7422

那么我们先记住这个输出的结果,因为采用的随机种子是一致的,一会儿可以直接跟JIT的输出结果进行对比。

JIT模式

Jax的JIT模式的使用方法常见的就是三种,一种是在函数头顶加一个
装饰器
,一种是在函数引用的时候使用
jax.jit(function)
来调用,最后一种是配合
partial
偏函数
来使用,都不是很复杂。那么这里先用装饰器的形式演示一下Jax中即时编译的用法:

import os
os.environ['XLA_PYTHON_CLIENT_PREALLOCATE']='false'

import numpy as np
np.random.seed(0)
import jax
from jax import numpy as jnp

@jax.jit
def func(r, q, cutoff=0.2):
    dis = jnp.abs(r[:, None] - r[None])
    maski, maskj = jnp.where(dis<=cutoff)
    qi = q[maski]
    qj = q[maskj]
    return jnp.sum(qi*qj)

N = 100
r = jnp.array(np.random.random(N), jnp.float32)
q = jnp.array(np.random.random(N), jnp.float32)

print (func(r, q))

正如前面所说,因为
numpy.where
对应的输出是一个动态的Shape,那么在编译阶段就会报错。报错信息如下:

Traceback (most recent call last):
  File "/home/dechin/projects/gitee/dechin/tests/jax_mask.py", line 21, in <module>
    print (func(r, q))
  File "/home/dechin/anaconda3/envs/jax/lib/python3.10/site-packages/jax/_src/traceback_util.py", line 162, in reraise_with_filtered_traceback
    return fun(*args, **kwargs)
  File "/home/dechin/anaconda3/envs/jax/lib/python3.10/site-packages/jax/_src/api.py", line 622, in cache_miss
    execute = dispatch._xla_call_impl_lazy(fun_, *tracers, **params)
  File "/home/dechin/anaconda3/envs/jax/lib/python3.10/site-packages/jax/_src/dispatch.py", line 236, in _xla_call_impl_lazy
    return xla_callable(fun, device, backend, name, donated_invars, keep_unused,
  File "/home/dechin/anaconda3/envs/jax/lib/python3.10/site-packages/jax/linear_util.py", line 303, in memoized_fun
    ans = call(fun, *args)
  File "/home/dechin/anaconda3/envs/jax/lib/python3.10/site-packages/jax/_src/dispatch.py", line 359, in _xla_callable_uncached
    return lower_xla_callable(fun, device, backend, name, donated_invars, False,
  File "/home/dechin/anaconda3/envs/jax/lib/python3.10/site-packages/jax/_src/profiler.py", line 314, in wrapper
    return func(*args, **kwargs)
  File "/home/dechin/anaconda3/envs/jax/lib/python3.10/site-packages/jax/_src/dispatch.py", line 445, in lower_xla_callable
    jaxpr, out_type, consts = pe.trace_to_jaxpr_final2(
  File "/home/dechin/anaconda3/envs/jax/lib/python3.10/site-packages/jax/_src/profiler.py", line 314, in wrapper
    return func(*args, **kwargs)
  File "/home/dechin/anaconda3/envs/jax/lib/python3.10/site-packages/jax/interpreters/partial_eval.py", line 2077, in trace_to_jaxpr_final2
    jaxpr, out_type, consts = trace_to_subjaxpr_dynamic2(fun, main, debug_info)
  File "/home/dechin/anaconda3/envs/jax/lib/python3.10/site-packages/jax/interpreters/partial_eval.py", line 2027, in trace_to_subjaxpr_dynamic2
    ans = fun.call_wrapped(*in_tracers_)
  File "/home/dechin/anaconda3/envs/jax/lib/python3.10/site-packages/jax/linear_util.py", line 167, in call_wrapped
    ans = self.f(*args, **dict(self.params, **kwargs))
  File "/home/dechin/projects/gitee/dechin/tests/jax_mask.py", line 12, in func
    maski, maskj = jnp.where(dis<=cutoff)
  File "/home/dechin/anaconda3/envs/jax/lib/python3.10/site-packages/jax/_src/numpy/lax_numpy.py", line 1077, in where
    return nonzero(condition, size=size, fill_value=fill_value)
  File "/home/dechin/anaconda3/envs/jax/lib/python3.10/site-packages/jax/_src/numpy/lax_numpy.py", line 1332, in nonzero
    size = core.concrete_or_error(operator.index, size,
  File "/home/dechin/anaconda3/envs/jax/lib/python3.10/site-packages/jax/core.py", line 1278, in concrete_or_error
    raise ConcretizationTypeError(val, context)
jax._src.traceback_util.UnfilteredStackTrace: jax._src.errors.ConcretizationTypeError: Abstract tracer value encountered where concrete value is expected: Traced<ShapedArray(int32[])>with<DynamicJaxprTrace(level=0/1)>
The size argument of jnp.nonzero must be statically specified to use jnp.nonzero within JAX transformations.
The error occurred while tracing the function func at /home/dechin/projects/gitee/dechin/tests/jax_mask.py:9 for jit. This concrete value was not available in Python because it depends on the value of the argument 'r'.

See https://jax.readthedocs.io/en/latest/errors.html#jax.errors.ConcretizationTypeError

The stack trace below excludes JAX-internal frames.
The preceding is the original exception that occurred, unmodified.

--------------------

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/dechin/projects/gitee/dechin/tests/jax_mask.py", line 21, in <module>
    print (func(r, q))
  File "/home/dechin/projects/gitee/dechin/tests/jax_mask.py", line 12, in func
    maski, maskj = jnp.where(dis<=cutoff)
  File "/home/dechin/anaconda3/envs/jax/lib/python3.10/site-packages/jax/_src/numpy/lax_numpy.py", line 1077, in where
    return nonzero(condition, size=size, fill_value=fill_value)
  File "/home/dechin/anaconda3/envs/jax/lib/python3.10/site-packages/jax/_src/numpy/lax_numpy.py", line 1332, in nonzero
    size = core.concrete_or_error(operator.index, size,
jax._src.errors.ConcretizationTypeError: Abstract tracer value encountered where concrete value is expected: Traced<ShapedArray(int32[])>with<DynamicJaxprTrace(level=0/1)>
The size argument of jnp.nonzero must be statically specified to use jnp.nonzero within JAX transformations.
The error occurred while tracing the function func at /home/dechin/projects/gitee/dechin/tests/jax_mask.py:9 for jit. This concrete value was not available in Python because it depends on the value of the argument 'r'.

See https://jax.readthedocs.io/en/latest/errors.html#jax.errors.ConcretizationTypeError

想避免这个报错,要么就是对该函数不做编译(牺牲性能),要么就是自己写一个CUDA算子(增加工作量),再就是我们这里用到的NonZero定长输出的方法(预置条件)。

NonZero的使用

使用Jax的NonZero函数时,也有一点需要注意:虽然NonZero可以做到固定长度的输出,但是这个固定的长度本身也是一个名为
size
的传入参数。也就是说,NonZero的输出Shape也是要取决于输入参数的。Jax开发时也考虑到了这一点,所以在编译时提供了一个功能可以设置静态参量:
static_argnames
,例如我们的案例中,将
size
这个名称的传参设置为静态参量,这样就可以使用Jax的即时编译了:

import os
os.environ['XLA_PYTHON_CLIENT_PREALLOCATE']='false'

import numpy as np
np.random.seed(0)
import jax
from jax import numpy as jnp
from functools import partial

@partial(jax.jit, static_argnames='size')
def func(r, q, cutoff=0.2, size=5000):
    if q.shape[0] != r.shape[0]+1:
        raise ValueError("The q.shape[0] should be equal to r.shape[0]+1")

    dis = jnp.abs(r[:, None] - r[None])
    maski, maskj = jnp.nonzero(jnp.where(dis<=cutoff, 1, 0), size=size, fill_value=-1)
    qi = q[maski]
    qj = q[maskj]
    return jnp.sum(qi*qj)

N = 100
r = jnp.array(np.random.random(N), jnp.float32)
q = jnp.array(np.random.random(N), jnp.float32)
pader = jnp.array([0.], jnp.float32)
q = jnp.append(q, pader)

print (func(r, q))
# 1035.7422

可以看到,函数用Jax的JIT成功编译,并且输出结果跟前面未编译时候是一致的。当然,这里还用到了一个小技巧,就是NonZero函数输出结果时,不到长度的输出结果会被自动Pad到给定的长度,这里Pad的值使用的是我们给出的
fill_value
。因为NonZero输出的也是索引,这样我们可以把Pad的这些索引设置为
-1
,然后在构建参数
\(q\)
的时候事先在末尾
append
一个0,这样就可以确保计算的输出结果直接就是正确的。

总结概要

在使用Jax的过程中,有时候会遇到函数输出是一个动态的Shape,这种情况下我们很难利用到Jax的即时编译的功能,不能使得性能最大化。这也是使用Tensor数据结构来计算的一个特点,有好有坏。本文介绍了Jax的另外一个函数NonZero,可以使得我们能够编译那些动态Shape输出的函数。

版权声明

本文首发链接为:
https://www.cnblogs.com/dechinphy/p/nonzero.html

作者ID:DechinPhy

更多原著文章:
https://www.cnblogs.com/dechinphy/

请博主喝咖啡:
https://www.cnblogs.com/dechinphy/gallery/image/379634.html