一文速览 TypeScript 装饰器 与 IoC 机制

发表于 3年以前  | 总阅读数:365 次

前言

本篇文章适用于对 TypeScript 装饰器缺少使用经验或只是浅尝辄止过的同学,我将从 TypeScript 装饰器的诞生背景开始,介绍不同种类装饰器的使用场景和功能,再到 元数据反射 与 IoC 机制。相信读完本文后,在以后使用 TypeScript 装饰器时,你会多一份踏实:现在你清清楚楚得知道它们的运作流程了!

TypeScript装饰器简介

首先,装饰器是什么?简单的说,装饰器是一种应用在类及其内部成员的语法,它的本质其实就是函数。我对这一语法抱有额外的热情,则是因为它能很好地隐藏掉许多功能的实现细节。如:

@InternalChanges()
class Foo { }

@InternalChanges() 这个装饰器中,我们甚至能够完全修改掉这个类的功能行为,而只需要这唯一的一行代码。你可能会觉得这使得内部实现过于黑盒,但仔细想想,可复用的装饰器实际就相当于 utils 方法,在提供给外部使用时,我们本就不希望使用者需要关心内部的逻辑。

而装饰器的另外一个功能要使用的更加广泛,也更加符合我上面所说的“我就希望它是黑盒的”,那就是元数据(元编程)相关,这一点我们会在后面详细展开。

其次我们需要知道,JavaScript 与 TypeScript 中的装饰器完全不是一回事,JS中的装饰器目前依然停留在 stage 2 阶段,并且目前版本的草案与TS中的实现差异相当之大(TS是基于第一版,JS目前已经第三版了),所以二者最终的装饰器实现必然有非常大的差异。

如果你曾使用过 TypeScript 装饰器,不妨看看下面这张图展示的当前 JavaScript 装饰器使用方式,就能感觉出现在二者的实现差异之大了:

js-decorator

严格的来说,装饰器不是 TypeScript 所提供的特性(如类型别名与接口等),而是其实现的 ECMAScript提案(就像类的私有成员一样)。

TS实际上只会对 stage-3 以上的提案提供支持,比如 TS3.7版本 引入了可选链(Optional chaining)与空值合并(Nullish-Coalescing),我想这两个语法目前应该非常多的同学已经重度使用了。而当 TypeScript 开始引入装饰器支持 时(大约在15年左右,最先引入的 TypeScript 版本是 1.5 版本),ECMAScript 中的装饰器依然处于 stage-1 阶段。其原因是 TypeScript 与 Angular 团队达成了合作,Ng 团队不再维护 AtScript,而 TypeScript 引入了注解语法(Annotation)及相关特性。

AtScript 最初构建于 TypeScript 之上,但是又引入了一部分来自于 Dart 的语言特性。同时 Angular 2.0 也是基于 AtScript 而开发的。同样是在 TypeScript 1.5 版本,TypeScript 团队宣布许多 AtScript 的特性将被实现在 1.5 版本中,而 Angular 2.0 也将直接基于 TypeScript。

为什么叫 AtScript ?因为 Angular 中重度使用了装饰器,at即代表了 @ 这一语法。

但是并不需要担心,即使装饰器永远到达不了stage-3/4 阶段,它也不会消失的(更何况现在提案中的装饰器和 TypeScript 装饰器也不是一个东西了)。有相当多的框架都是装饰器的重度用户,如AngularNestMidway等。对于装饰器的内部实现与编译结果会始终保留(但不能确定的是,在 JavaScript 装饰器成功进入最终阶段后是否会发生变化),就像JSX一样。

如果你对它的历史与发展方向有兴趣,可以读一读 是否应该在production里使用typescript的decorator?(贺师俊贺老的回答)

为什么我们需要装饰器?在后面的例子中我们会体会到装饰器的强大与魅力,基于装饰器我们能够快速优雅的复用逻辑,对业务代码进行能力增强。同时我们本文的重点:依赖注入也将使用装饰器的元数据反射能力来实现。

装饰器与注解

由于我本身并没学习过 Java 以及Spring IoC,因此我的理解可能存在一些偏差,还请在评论区指出错误之处~

装饰器与注解实际上也有一定区别,由于并没有学过Java,这里就不与Java中的注解进行比较了。而只是说我所认为的二者差异:

  • 注解 应该如同字面意义一样, 只是为某个被注解的对象提供元数据(metadata)的注入,本质上不能起到任何修改行为的操作,需要额外的scanner去进行扫描获得元数据并基于其去执行操作,注解的元数据才有实际意义。
  • 单纯的装饰器 没法添加元数据,只能基于已经由注解注入的元数据来执行操作,来对类以及内部成员如方法、属性、方法参数进行某种特定的操作。

但实际上,TypeScript中的装饰器通常是同时包含了这两种效能的,它在消费元数据的同时,也能够提供元数据供别的装饰器消费(通过装饰器的先后执行顺序)。

不同类型的装饰器及使用

如果要在本地运行示例代码,你需要确保在tsconfig.json中启用了experimentalDecoratorsemitDecoratorMetadata

类装饰器

function addProp(constructor: Function) {
  constructor.prototype.job = 'fe';
}

@addProp
class P {
  job: string;
  constructor(public name: string) {}
}

let p = new P('林不渡');

console.log(p.job); // fe
复制代码

我们发现,在以单纯装饰器方式 @addProp 调用时,不管用它来装饰哪个类,起到的作用都是相同的,即修改类上的属性。因为这里装饰器的逻辑是固定的。这样肯定不是我们为想要的,起码得支持调用时传入不同的参数来将属性修改为不同的值吧?

试试以 @addProp() 的方式来调用:

function addProp(param: string): ClassDecorator {
  return (constructor: Function) => {
    constructor.prototype.job = param;
  };
}

@addProp('fe+be')
class P {
  job: string;
  constructor(public name: string) {}
}

let p = new P('林不渡');

console.log(p.job); // fe+be

首先要明确地是,TS中的装饰器实现本质是一个语法糖,它的本质是一个函数,如果调用形式为@deco()(即上面的例子),那么这个函数应该再返回一个函数来实现调用,所以 addProp 方法再次返回了一个 ClassDecorator。应用在不同位置的装饰器将接受的参数是不同的,如这里的类装饰器接受的参数将是类的构造函数

其次,你应该明白ES6中class的实质,如果现在暂时不明白,推荐先阅读我的这篇一点都没技术含量的技术文章: 从 Babel 编译结果看 ES6 的 Class 实质。

现在我们想要添加的属性值就可以由我们决定了, 实际上由于我们拿到了原型对象,还可以进行更多操作,解锁更多神秘姿势。

方法装饰器

方法装饰器的入参为 类的原型对象 属性名 以及属性描述符(descriptor),其属性描述符包含writable``enumerable``configurable ,我们可以在这里去配置其相关信息,如禁止这个方法再次被修改。

注意,对于静态成员来说,首个参数会是类的构造函数。而对于实例成员(比如下面的例子),则是类的原型对象。

function addProps(): MethodDecorator {
  return (target, propertyKey, descriptor) => {
    descriptor.writable = false;
  };
}

class A {
  @addProps()
  originMethod() {
    console.log("I'm Original!");
  }
}

const a = new A();

a.originMethod = () => {
  console.log("I'm Changed!");
};

// 仍然是原来的方法
a.originMethod(); // I'm Original! 

你是否有点想起来Object.defineProperty()?的确方法装饰器也是借助它来修改类和方法的属性的,你可以在 TypeScript Playground 中看看 TypeScript 对上面代码的编译结果。

属性装饰器

类似于方法装饰器,但它的入参少了属性描述符。原因则是目前没有方法在定义原型对象成员的同时,去描述一个实例的属性(创建描述符)。

function addProps(): PropertyDecorator {
  return (target, propertyKey) => {
    console.log(target);
    console.log(propertyKey);
  };
}

class A {
  @addProps()
  originProps: unknown;
}

属性与方法装饰器有一个重要作用是注入与提取元数据,这点我们在后面会体现到。

参数装饰器

参数装饰器的入参首要两位与属性装饰器相同,第三个参数则是参数在当前函数参数中的索引

function paramDeco(params?: any): ParameterDecorator {
  return (target, propertyKey, index) => {
    target.constructor.prototype.fromParamDeco = 'Foo';
  };
}

class B {
  someMethod(@paramDeco() param1: unknown, @paramDeco() param2: unknown) {
    console.log(`${param1}  ${param2}`);
  }
}

// "A B"
new B().someMethod('A', 'B');
// Foo
// @ts-ignore
console.log(B.prototype.fromParamDeco);

参数装饰器与属性装饰器都有个特别之处,他们都不能获取到描述符descriptor,因此也就不能去修改其参数/属性的行为。但是我们可以这么做:给类原型添加某个属性,携带上与参数/属性/装饰器相关的元数据,并由下一个执行的装饰器来读取。(装饰器的执行顺序请参见下一节)。

当然像例子中这样直接在原型上添加属性的方式是十分不推荐的,后面我们会使用 ES7 中的 Reflect Metadata 来进行元数据的读/写。

装饰器工厂

假设现在我们同时需要四种功能相近的装饰器,你会怎么做?定义四种装饰器然后分别使用吗?也行,但后续你看着这一堆装饰器可能会感觉有点头疼...,因此我们可以考虑接入工厂模式,使用一个装饰器工厂来为我们根据条件生成不同的装饰器。

首先我们准备好各个装饰器函数:

function classDeco(): ClassDecorator {
    return (target: Object) => {
        console.log('Class Decorator Invoked');
        console.log(target);
    };
}

function propDeco(): PropertyDecorator {
    return (target: Object, propertyKey: string | symbol) => {
        console.log('Property Decorator Invoked');
        console.log(propertyKey);
    };
}

function methodDeco(): MethodDecorator {
    return (
        target: Object,
        propertyKey: string | symbol,
        descriptor: PropertyDescriptor
    ) => {
        console.log('Method Decorator Invoked');
        console.log(propertyKey);
    };
}

function paramDeco(): ParameterDecorator {
    return (target: Object, propertyKey: string | symbol, index: number) => {
        console.log('Param Decorator Invoked');
        console.log(propertyKey);
        console.log(index);
    };
}

接着,我们实现一个工厂函数来根据不同条件返回不同的装饰器:

enum DecoratorType {
  CLASS = 'CLASS',
  METHOD = 'METHOD',
  PROPERTY = 'PROPERTY',
  PARAM = 'PARAM',
}

type FactoryReturnType =
  | ClassDecorator
  | MethodDecorator
  | PropertyDecorator
  | ParameterDecorator;

function decoFactory(
  this: any,
  type: DecoratorType.CLASS,
  ...args: any[]
): ClassDecorator;

function decoFactory(
  this: any,
  type: DecoratorType.METHOD,
  ...args: any[]
): MethodDecorator;

function decoFactory(
  this: any,
  type: DecoratorType.PROPERTY,
  ...args: any[]
): PropertyDecorator;

function decoFactory(
  this: any,
  type: DecoratorType.PARAM,
  ...args: any[]
): ParameterDecorator;

function decoFactory(
  this: any,
  type: DecoratorType,
  ...args: any[]
): FactoryReturnType {
  switch (type) {
    case DecoratorType.CLASS:
      return classDeco.apply(this, args);

    case DecoratorType.METHOD:
      return methodDeco.apply(this, args);

    case DecoratorType.PROPERTY:
      return propDeco.apply(this, args);

    case DecoratorType.PARAM:
      return paramDeco.apply(this, args);

    default:
      throw new Error('Invalid DecoratorType');
  }
}

@decoFactory(DecoratorType.CLASS)
class C {
  @decoFactory(DecoratorType.PROPERTY)
  prop: unknown;

  @decoFactory(DecoratorType.METHOD)
  method(@decoFactory(DecoratorType.PARAM) param: string) {}
}

new C().method('foobar');

以上是一种方式,你也可以通过判断传入的参数,来判断当前的装饰器被应用在哪个位置。

多个装饰器声明的执行顺序

类中不同声明上的装饰器将按以下规定的顺序应用:

  1. 参数装饰器,然后依次是方法装饰器访问符装饰器,或属性装饰器应用到每个实例成员。
  2. 参数装饰器,然后依次是方法装饰器访问符装饰器,或属性装饰器应用到每个静态成员。
  3. 参数装饰器应用到构造函数。
  4. 类装饰器应用到类。

注意这个顺序,后面我们能够实现元数据读写,也正是因为这个顺序。

当存在多个装饰器来装饰同一个声明时,则会有以下的顺序:

  • 首先,由上至下依次对装饰器表达式求值,得到返回的真实函数(如果有的话)。
  • 而后,求值的结果会由下至上依次调用。

这个执行顺序有点像洋葱模型对吧?

function foo() {
    console.log("foo in");
    return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
        console.log("foo out");
    }
}

function bar() {
    console.log("bar in");
    return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
        console.log("bar out");
    }
}

class A {
    @foo()
    @bar()
    method() {}
}

// foo in
// bar in
// bar out
// foo out

Reflect Metadata

基本元数据读写

Reflect Metadata是属于 ES7 的一个提案,其主要作用是在声明时去读写元数据。TS早在1.5+版本就已经支持反射元数据的使用,目前想要使用,我们还需要安装 reflect-metadata ,且在 tsconfig.json中启用 emitDecoratorMetadata 选项。

你可以将元数据理解为用于描述数据的数据,如某个对象的键、键值、类型等等就可称之为该对象的元数据。做一个简单的阐述:

为类或类属性添加了元数据后,构造函数的原型(或是构造函数,根据静态成员还是实例成员决定)会具有[[Metadata]]属性,该属性内部包含一个Map结构,键为属性键,值为元数据键值对

reflect-metadata提供了对 Reflect 对象的扩展,在引入后,我们可以直接从 Reflect对象上获取扩展方法,并将其作为装饰器使用:

文档见 reflect-metadata,但不用急着看,其API命令还是很语义化的。

import 'reflect-metadata';

@Reflect.metadata('className', 'D')
class D {
  @Reflect.metadata('methodName', 'hello')
  public hello(): string {
    return 'hello world';
  }
}

const d = new D();
console.log(Reflect.getMetadata('className', D));
console.log(Reflect.getMetadata('methodName', d));

可以看到,我们给类 D 与 D 内部的方法hello都注入了元数据,并通过getMetadata(metadataKey, target)这个方式取出了存放的元数据。

Reflect-metadata支持 命令式(Reflect.defineMetadata) 与声明式(上面的装饰器方式)的元数据定义

我们注意到,注入在类上的元数据在取出时 target 为这个类D,而注入在方法上的元数据在取出时 target 则为实例d。原因其实我们实际上在上面的装饰器执行顺序提到了,这是由于注入在方法、属性、参数上的元数据实际上是被添加在了实例对应的位置上,因此需要实例化才能取出。

内置元数据

Reflect允许程序去检视自身,基于这个效果,我们可以在装饰器运行时去检查其类型相关信息,如目标类型、目标参数的类型以及方法返回值的类型,这需要借助 TypeScript 内置的元数据metadataKey来实现,以一个检查入参的例子为例:

访问符装饰器的属性描述符参数将会额外拥有getset方法,其他与属性装饰器相同

import 'reflect-metadata';

class Point {
  x: number;
  y: number;
}

class Line {
  private _p0: Point;
  private _p1: Point;

  @validate
  set p0(value: Point) {
    this._p0 = value;
  }
  get p0() {
    return this._p0;
  }

  @validate
  set p1(value: Point) {
    this._p1 = value;
  }
  get p1() {
    return this._p1;
  }
}

function validate<T>(
  target: any,
  propertyKey: string,
  descriptor: TypedPropertyDescriptor<T>
) {
  let set = descriptor.set!;
  descriptor.set = function (value: T) {
    let type = Reflect.getMetadata('design:type', target, propertyKey);
    if (!(value instanceof type)) {
      throw new TypeError('Invalid type.');
    }
    set(value);
  };
}

const line = new Line();
// Error!
// @ts-ignore
line.p0 = {
  x: 1,
};

在这个例子中,我们基于 Reflect.getMetadata('design:type', target, propertyKey) 获取到了装饰器对应声明的属性类型,并确保在 setter被调用时检查值类型。

这里的 design:type 即是 TS 的内置元数据key,也即是说 TS 在编译前还手动执行了@Reflect.metadata("design:type", Point)。除了 design:key 以外,TS还内置了**design:paramtypes(获取目标参数类型)design:returntype(获取方法返回值类型)**这两种元数据字段来提供帮助。但有一点需要注意,即使对于基本类型,这些元数据也返回对应的包装类型,如number -> [Function: Number]

IoC

概念介绍:IoC、依赖注入、容器

IoC的全称为 Inversion of Control,意为控制反转,它是OOP中的一种设计原则,常用于解耦代码。

直接说概念多没意思,让我们来想象这样一个场景:

有这么一个类 C,它的代码内部使用到了另外两个类 A、B,需要去分别实例化它们。在不使用 IoC 的情况下,我们很容易写出来这样的代码:

import { A } from './modA';
import { B } from './modB';

class C {
  constructor() {
    this.a = new A();
    this.b = new B();
  }
}

乍一看可能没什么,但实际上类 C 会强依赖于A、B,造成模块之间的耦合。如果后续 A、B 的实例化参数变化,或者是 A、B 内部又依赖了别的类,那么维护起来简直是一团乱麻。

要解决这个问题,我们可以这么做:

  • C 的内部代码只需要定义一个 类型为 A、B的成员。
  • 用一个第三方容器来管理这些作为依赖的类
  • 当实例化 C 时,由容器负责实例化 A、B,并注入到对应的属性上

以 Injection 为例:

import { Container } from 'injection';
import { A } from './A';
import { B } from './B';
const container = new Container();
container.bind(A);
container.bind(B);

class C {
  constructor() {
    this.a = container.get('a');
    this.b = container.get('b');
  }
}

现在A、B、C之间没有了耦合,甚至当某个类 D 需要使用 C 的实例时,我们也可以把 C 交给IoC容器,它会帮我们照看好的。

我们现在能够知道 IoC 容器大概的作用了:容器内部维护着一个对象池,管理着各个对象实例,当用户需要使用实例时,容器会自动将对象实例化交给用户。

再举个栗子,当我们想要处对象时,会上Soul、Summer、陌陌...等等去一个个找,找哪种的与怎么找是由我自己决定的,这叫 控制正转。现在我觉得有点麻烦,直接把自己的介绍上传到世纪佳缘,如果有人对我感兴趣了,就会主动向我发起聊天,这叫 控制反转

DI的全称为Dependency Injection,即依赖注入。依赖注入是控制反转最常见的一种应用方式,就如它的名字一样,它的思路就是在对象创建时自动注入依赖对象。再以 Injection 的使用为例,这次我们用上装饰器:

// provide意为当前对象需要被绑定到容器中
// inject意为去容器中取出对应的实例注入到当前属性中
@provide()
export class UserService {

  @inject()
  userModel;

  async getUser(userId) {
    return await this.userModel.get(userId);
  }
}

我们不需要在构造函数中去手动 this.userModel = xxx 并且考虑需要的传参了,容器会自动帮我们做这一步。

基于 IoC 机制的路由简易实现

如果你用 NestJS、MidwayJS 写过应用,那么你肯定熟悉下面这样的代码:

@provide()
@controller('/user')
export class UserController {

  @get('/all')
  async getUser(): Promise<void> {
    // ...
  }

  @get('/uid/:uid')
  async findUserByUid(): Promise<void> {
    // ...
  }

  @post('/uid/:uid')
  async updateUser(): Promise<void> {
    // ...
  }
}

这种基于装饰器声明路由的方式一直是我的心头好,你可以通过装饰器非常容易的定义路由层面的拦截器与中间件等操作,在 NestJS 中,还存在着 @Pipe``@Guard``@Catch``@UseInterceptors 等非常多细粒度的装饰器用于在控制器或者路由层面进行操作。

可是你想过它们是如何实现的吗?假设我们要解析的路由如下:

@controller('/user')
export class UserController {
  @get('/all')
  async getAllUser(): Promise<void> {
    // ...
  }

  @post('/update')
  async updateUser(): Promise<void> {
    // ...
  }
}

首先思考 controllerget / post装饰器,我们需要使用这几个装饰器注入哪些信息:

  • 路径
  • 方法(方法装饰器)

首先是对于整个类,我们需要将path: "/user"这个数据注入:

// 工具常量枚举
export enum METADATA_MAP {
  METHOD = 'method',
  PATH = 'path',
  GET = 'get',
  POST = 'post',
  MIDDLEWARE = 'middleware',
}

const { METHOD, PATH, GET, POST } = METADATA_MAP;

export const controller = (path: string): ClassDecorator => {
  return (target) => {
    Reflect.defineMetadata(PATH, path, target);
  };
};

而后是方法装饰器,我们选择一个高阶函数去吐出各个方法的装饰器,而不是为每种方法定义一个。

// 方法装饰器 保存方法与路径
export const methodDecoCreator = (method: string) => {
  return (path: string): MethodDecorator => {
    return (_target, _key, descriptor) => {
      Reflect.defineMetadata(METHOD, method, descriptor.value!);
      Reflect.defineMetadata(PATH, path, descriptor.value!);
    };
  };
};

// 首先确定方法,而后在使用时才去确定路径
const get = methodDecoCreator(GET);
const post = methodDecoCreator(POST);

接下来我们要做的事情就很简单了:

  • 拿到注入在类上元数据的根路径
  • 拿到每个方法上元数据的方法、路径
  • 拼接,生成路由表
const routeGenerator = (ins: Record<string, unknown>) => {
  const prototype = Object.getPrototypeOf(ins);

  const rootPath = Reflect.getMetadata(PATH, prototype['constructor']);

  const methods = Object.getOwnPropertyNames(prototype).filter(
    (item) => item !== 'constructor'
  );

  const routeGroup = methods.map((methodName) => {
    const methodBody = prototype[methodName];

    const path = Reflect.getMetadata(PATH, methodBody);
    const method = Reflect.getMetadata(METHOD, methodBody);
    return {
      path: `${rootPath}${path}`,
      method,
      methodName,
      methodBody,
    };
  });
  console.log(routeGroup);
  return routeGroup;
};

生成的结果大概是这样:

[
  {
    path: '/user/all',
    method: 'post',
    methodName: 'getAllUser',
    methodBody: [Function (anonymous)]
  },
  {
    path: '/user/update',
    method: 'get',
    methodName: 'updateUser',
    methodBody: [Function (anonymous)]
  }
]

依赖注入工具库

我个人了解并使用过的TS依赖注入工具库包括:

  • TypeDI,TypeStack出品
  • TSYringe,微软出品
  • Inversify,目前 JS/TS 中 star数最多的一个 依赖注入工具库
  • Injection,MidwayJS团队出品,是 MidwayJS 底层 IoC 的能力支持

我们再看看上面呈现过的Injection的例子:

@provide()
export class UserService {

  @inject()
  userModel;

  async getUser(userId) {
    return await this.userModel.get(userId);
  }
}

实际上,一个依赖注入工具库必定会提供的就是 从容器中获取实例注入对象到容器中的两个方法,如上面的 provideinject,TypeDI的 ServiceInject,以及 Inversify 的 injectableinject

总结

读完这篇文章,我想你应该对 TypeScript中 的装饰器与 IoC 机制有了大概的了解,如果你意犹未尽,不妨去看一下 TypeScript 对装饰器、反射元数据的编译结果(原本想作为本文的收尾部分,但我个人觉得没有特别具有技术含量的地方,所以还请自行根据需要扩展~),如果不想自己本地再起一个项目,你也可以直接使用TypeScript Playground。

最后,强烈推荐尝试一次全程重度使用装饰器来开发项目,这里给一个可行的技术方案:

  • Midway Serverless,使用装饰器声明你的 Serverless 函数,如果不想使用 Serverless ,你也可以使用 MidwayJS 来开发 Node 服务。
  • TypeORM,使用装饰器语法声明你的数据库表以及字段,结合 MidwayJS 官方提供的 ORM组件 来获得丝滑体验。
  • TypeGraphQL,使用装饰器语法声明你的 GraphQL 对象类型,与 TypeORM 可以一体使用,这样你就能够同时修改数据库表字段与 GraphQL Schema了。如果要与 Midway(Serverless)一同使用,需要 Apollo-Server-Midway。
  • Util-Decorators,提供基于装饰器的公用方法,如节流防抖、错误处理等。

本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/jDKdBXl-uKdOxwVwbl24aA

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237229次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8063次阅读
 目录