Vue 3.0 diff 新特性 – 静态节点提升

thbcm阅读(197)

文章来源于公众号:Code center ,作者五柳

前言

「静态节点提升」是「Vue3」针对 VNode 更新过程性能问题而提出的一个优化点。众所周知,在大型应用场景下,「Vue2.x」patchVNode 过程,即 diff 过程是非常缓慢的,这是一个十分令人头疼的问题。

虽然,对于面试常问的 diff 过程在一定程度上是减少了对 DOM 的直接操作。但是,「这个减少是有一定成本的」。因为,如果是复杂应用,那么就会存在父子关系非常复杂的 VNode,而这也就是 diff 的痛点,它会不断地递归调用 patchVNode,不断堆叠而成的几毫秒,最终就会造成 VNode 更新缓慢。

也因此,这也是为什么我们所看到的大型应用诸如阿里云之类的采用的是基于「React」的技术栈的原因之一。所以,「Vue3」也是痛改前非,重写了整个 Compiler 过程,提出了静态提升、靶向更新等优化点,来提高 patchVNode 过程。

那么,回到今天的正题,我们从源码角度看看在整个编译过程「Vue3」静态节点提升究竟是「何许人也」

什么是 patchFlag

由于,在 compile 过程的 transfrom 阶段会提及 AST Element 上的 patchFlag 属性。所以,在正式认识 complie 之前,我们先搞清楚一个概念,什么是 patchFlag

patchFlagcomplier 时的 transform 阶段解析 AST Element 打上的「优化标识」。并且,顾名思义 patchFlagpatch 一词表示着它会为 runtime时的 patchVNode 提供依据,从而实现靶向更新 VNode 的效果。因此,这样一来一往,也就是耳熟能详的 Vue3 巧妙结合 runtimecompiler 实现靶向更新和静态提升。

而在源码中 patchFlag 被定义为一个「数字枚举类型」,每一个枚举值对应的标识意义会是这样:

并且,值得一提的是整体上 patchFlag 的分为两大类:

  • patchFlag 的值「大于」 0 时,代表所对应的元素在 patchVNode 时或 render 时是可以被优化生成或更新的。
  • patchFlag 的值「小于」 0 时,代表所对应的元素在 patchVNode 时,是需要被 full diff,即进行递归遍历 VNode tree 的比较更新过程。

其实,还有两类特殊的 flagshapeFlagslogFlag,这里我就不对此展开,有兴趣的同学可以自行去了解。

Compile 编译过程

对比 Vue2.x 编译过程

了解过「Vue2.x」源码的同学,我想应该都知道在「Vue2.x」中的 Compile 过程会是这样:

  • parse 编译模板生成原始 AST。
  • optimize 优化原始 AST,标记 AST Element 为静态根节点或静态节点。
  • generate 根据优化后的 AST,生成可执行代码,例如 _c_l 之类的。

而在「Vue3」中,整体的 Compile 过程仍然是三个阶段,但是不同于「Vue2.x」的是,第二个阶段换成了正常编译器都会存在的阶段 transform。所以,它看起来会是这样:

在源码中,它对应的伪代码会是这样:

export function baseCompile(
  template: string | RootNode,
  options: CompilerOptions = {}
): CodegenResult {
  ...
  const ast = isString(template) ? baseParse(template, options) : template
  ...
  transform(
    ast,
    extend({}, options, {....})
  )


  return generate(
    ast,
    extend({}, options, {
      prefixIdentifiers
    })
  )
}

那么,我想这个时候大家可能会问为什么会是 transform?它的职责是什么?

通过简单的对比「Vue2.x」编译过程的第二阶段的 optimize,很明显,transform并不是「无米之炊」,它仍然有着「优化」原始 AST 的作用,而具体职责会表现在:

  • 对所有 AST Element 新增 codegen 属性来帮助 generate 更准确地生成「最优」的可执行代码。
  • 对静态 AST Element 新增 hoists 属性来实现静态节点的「单独创建」

此外,transform 还标识了诸如 isBlockhelpers 等属性,来生成最优的可执行代码,这里我们就不细谈,有兴趣的同学可以自行了解。

baseParse 构建原始抽象语法树(AST)

baseParse 顾名思义起着「解析」的作用,它的表现和「Vue2.x」parse 相同,都是解析模板 tempalte 生成「原始 AST」

假设,此时我们有一个这样的模板 template

<div><div>hi vue3</div><div>{{msg}}</div></div>

那么,它在经过 baseParse 处理后生成的 AST 看起来会是这样:

{
  cached: 0,
  children: [{…}],
  codegenNode: undefined,
  components: [],
  directives: [],
  helpers: [],
  hoists: [],
  imports: [],
  loc: {start: {…}, end: {…}, source: "<div><div>hi vue3</div><div>{{msg}}</div></div>"},
  temps: 0,
  type: 0
}

如果,了解过「Vue2.x」编译过程的同学应该对于上面这颗 AST 的大部分属性不会陌生。AST 的本质是通过用对象来描述「DSL」(特殊领域语言),例如:

  • children 中存放的就是最外层 div 的后代。
  • loc 则用来描述这个 AST Element 在整个字符串(template)中的位置信息。
  • type 则是用于描述这个元素的类型(例如 5 为插值、2 为文本)等等。

并且,可以看到的是不同于「Vue2.x」的 AST,这里我们多了诸如 helperscodegenNodehoists 等属性。而,这些属性会在 transform 阶段进行相应地赋值,进而帮助 generate 阶段生成「更优的」可执行代码。

transfrom 优化原始抽象语法树(AST)

对于 transform 阶段,如果了解过「编译器」的工作流程的同学应该知道,一个完整的编译器的工作流程会是这样:

  • 首先,parse 解析原始代码字符串,生成抽象语法树 AST。
  • 其次,transform 转化抽象语法树,让它变成更贴近目标「DSL」的结构。
  • 最后,codegen 根据转化后的抽象语法树生成目标「DSL」的可执行代码。

而在「Vue3」采用 Monorepo 的方式管理项目后,compile 对应的能力就是一个编译器。所以,transform 也是整个编译过程的重中之重。换句话说,如果没有 transformAST 做诸多层面的转化,「Vue」仍然会挂在 diff 这个「饱受诟病」的过程。

相比之下,「Vue2.x」的编译阶段没有完整的 transform,只是 optimize 优化了一下 AST,可以想象在「Vue」设计之初尤大也没想到它以后会「这么地流行」

那么,我们来看看 transform 函数源码中的定义:

function transform(root: RootNode, options: TransformOptions) {
  const context = createTransformContext(root, options)
  traverseNode(root, context)
  if (options.hoistStatic) {
    hoistStatic(root, context)
  }
  if (!options.ssr) {
    createRootCodegen(root, context)
  }
  // finalize meta information
  root.helpers = [...context.helpers]
  root.components = [...context.components]
  root.directives = [...context.directives]
  root.imports = [...context.imports]
  root.hoists = context.hoists
  root.temps = context.temps
  root.cached = context.cached
}

可以说,transform 函数做了什么,在它的定义中是「一览无余」。这里我们提一下它对静态提升其决定性作用的两件事:

  • 将原始 AST 中的静态节点对应的 AST Element 赋值给根 AST 的 hoists 属性。
  • 获取原始 AST 需要的 helpers 对应的键名,用于 generate 阶段的生成可执行代码的获取对应函数,例如 createTextVNodecreateStaticVNoderenderList 等等。

并且,在 traverseNode 函数中会对 AST Element 应用具体的 transform 函数,大致可以分为两类:

  • 静态节点 transform 应用,即节点不含有插值、指令、props、动态样式的绑定等。
  • 动态节点 transform 应用,即节点含有插值、指令、props、动态样式的绑定等。

那么,我们就来看看对于静态节点 transform 是如何应用的?

静态节点 transform 应用

这里,对于上面我们说到的这个栗子,静态节点就是这个部分:

<div>hi vue3</div>

而它在没有进行 transform 应用之前,它对应的 AST 会是这样:

{
  children: [{
    content: "hi vue3"
    loc: {start: {…}, end: {…}, source: "hi vue3"}
    type: 2
  }],
  codegenNode: undefined,
  isSelfClosing: false,
  loc: {start: {…}, end: {…}, source: "<div>hi vue3</div>"},
  ns: 0,
  props: [],
  tag: "div",
  tagType: 0,
  type: 1
}

可以看出,此时它的 codegenNodeundefined。而在源码中各类 transform函数被定义为 plugin,它会根据 baseParse 生成的 AST 「递归应用」对应的 plugin。然后,创建对应 AST Element 的 codegen 对象。

所以,此时我们会命中 transformElementtransformText 两个 plugin的逻辑。

「transformText」

transformText 顾名思义,它和「文本」相关。很显然,此时的 AST Element 所属的类型就是 Text。那么,我们先来看一下 transformText 函数对应的伪代码:

export const transformText: NodeTransform = (node, context) => {
  if (
    node.type === NodeTypes.ROOT ||
    node.type === NodeTypes.ELEMENT ||
    node.type === NodeTypes.FOR ||
    node.type === NodeTypes.IF_BRANCH
  ) {
    return () => {
      const children = node.children
      let currentContainer: CompoundExpressionNode | undefined = undefined
      let hasText = false


      for (let i = 0; i < children.length; i++) { // {1}
        const child = children[i]
        if (isText(child)) {
          hasText = true
          ...
        }
      }
      if (
        !hasText ||
        (children.length === 1 &&
          (node.type === NodeTypes.ROOT ||
            (node.type === NodeTypes.ELEMENT &&
              node.tagType === ElementTypes.ELEMENT)))
      ) { // {2}
        return
      }
      ...
    }
  }
}

可以看到,这里我们会命中 「{2}」 的逻辑,即如果对于「节点含有单一文本」 transformText 并不需要进行额外的处理,即该节点仍然在这里仍然保留和「Vue2.x」版本一样的处理方式。

transfromText 真正发挥作用的场景是当模板中存在这样的情况:

<div>ab {a} {b}</div>

此时 transformText 需要将两者放在一个「单独的」 AST Element 下,在源码中它被称为「Compound Expression」,即「组合的表达式」。这种组合的目的是为了 patchVNode 这类 VNode 时做到「更好地定位和实现 DOM 的更新」。反之,如果是一个文本节点和插值动态节点的话,在 patchVNode 阶段同样的操作需要进行两次,例如对于同一个 DOM 节点操作两次。

「transformElement」

transformElement 是一个所有 AST Element 都会被执行的一个 plugin,它的核心是为 AST Element 生成最基础的 codegen 属性。例如标识出对应 patchFlag,从而为生成 VNode 提供依据,例如 dynamicChildren

而对于静态节点,同样是起到一个初始化它的 codegenNode 属性的作用。并且,从上面介绍的 patchFlag 的类型,我们可以知道它的 patchFlag 为默认值 0。所以,它的 codegenNode 属性值看起来会是这样:

{
  children: {
    content: "hi vue3"
    loc: {start: {…}, end: {…}, source: "hi vue3"}
    type: 2
  },
  directives: undefined,
  disableTracking: false,
  dynamicProps: undefined,
  isBlock: false,
  loc: {start: {…}, end: {…}, source: "<div>hi vue3</div>"},
  patchFlag: undefined,
  props: undefined,
  tag: ""div"",
  type: 13
}

generate 生成可执行代码

generatecompile 阶段的最后一步,它的作用是将 transform 转换后的 AST 生成对应的「可执行代码」,从而在之后 Runtime 的 Render 阶段时,就可以通过可执行代码生成对应的 VNode Tree,然后最终映射为真实的 DOM Tree 在页面上。

同样地,这一阶段在「Vue2.x」也是由 generate 函数完成,它会生成是诸如 _l_c 之类的函数,这本质上是对 _createElement 函数的封装。而相比较「Vue2.x」版本的 generate,「Vue3」改变了很多,其 generate 函数对应的伪代码会是这样:

export function generate(
  ast: RootNode,
  options: CodegenOptions & {
    onContextCreated?: (context: CodegenContext) => void
  } = {}
): CodegenResult {
  const context = createCodegenContext(ast, options)
  if (options.onContextCreated) options.onContextCreated(context)
  const {
    mode,
    push,
    prefixIdentifiers,
    indent,
    deindent,
    newline,
    scopeId,
    ssr
  } = context
  ...
  genFunctionPreamble(ast, context)
  ...


  if (!ssr) {
    ...
    push(`function render(_ctx, _cache${optimizeSources}) {`)
  }
  ....


  return {
    ast,
    code: context.code,
    // SourceMapGenerator does have toJSON() method but it's not in the types
    map: context.map ? (context.map as any).toJSON() : undefined
  }
}

所以,接下来,我们就来「一睹」带有静态节点对应的 AST 生成的可执行代码的过程会是怎样。

CodegenContext 代码生成上下文

从上面 generate 函数的伪代码可以看到,在函数的开始调用了 createCodegenContext 为当前 AST 生成了一个 context。在整个 generate 函数的执行过程「都依托」于一个 CodegenContext 「生成代码上下文」(对象)的能力,它是通过 createCodegenContext 函数生成。而 CodegenContext 的接口定义会是这样:

interface CodegenContext
  extends Omit {
  source: string
  code: string
  line: number
  column: number
  offset: number
  indentLevel: number
  pure: boolean
  map?: SourceMapGenerator
  helper(key: symbol): string
  push(code: string, node?: CodegenNode): void
  indent(): void
  deindent(withoutNewLine?: boolean): void
  newline(): void
}

可以看到 CodegenContext 对象中有诸如 pushindentnewline 之类的方法。而它们的作用是在根据 AST 来生成代码时用来「实现换行」「添加代码」「缩进」等功能。从而,最终形成一个个可执行代码,即我们所认知的 render 函数,并且,它会作为 CodegenContextcode 属性的值返回。

下面,我们就来看下静态节点的可执行代码生成的核心,它被称为 Preamble 前导。

genFunctionPreamble 生成前准备

整个静态提升的可执行代码生成就是在 genFunctionPreamble 函数部分完成的。并且,大家仔细「斟酌」一番静态提升的字眼,静态二字我们可以不看,但是「提升二字」,直抒本意地表达出它(静态节点)被「提高了」

为什么说是提高了?因为在源码中的体现,确实是被提高了。在前面的 generate 函数,我们可以看到 genFunctionPreamble 是先于 render 函数加入context.code 中,所以,在 Runtime 时的 Render 阶段,它会先于 render 函数执行。

geneFunctionPreamble 函数(伪代码):

function genFunctionPreamble(ast: RootNode, context: CodegenContext) {
  const {
    ssr,
    prefixIdentifiers,
    push,
    newline,
    runtimeModuleName,
    runtimeGlobalName
  } = context
  ...
  const aliasHelper = (s: symbol) => `${helperNameMap[s]}: _${helperNameMap[s]}`
  if (ast.helpers.length > 0) {
    ...
    if (ast.hoists.length) {
      const staticHelpers = [
        CREATE_VNODE,
        CREATE_COMMENT,
        CREATE_TEXT,
        CREATE_STATIC
       ]
        .filter(helper => ast.helpers.includes(helper))
        .map(aliasHelper)
        .join(', ')
      push(`const { ${staticHelpers} } = _Vue\n`)
    }
  }
  ...
  genHoists(ast.hoists, context)
  newline()
  push(`return `)
}

可以看到,这里会对前面我们在 transform 函数提及的 hoists 属性的长度进行判断。显然,对于前面说的这个栗子,它的 ast.hoists.length 长度是大于 0 的。所以,这里就会根据 hoists 中的 AST 生成对应的可执行代码。因此,到这里,生成的可执行代码会是这样:

const _Vue = Vue
const { createVNode: _createVNode } = _Vue
// 静态提升部分
const _hoisted_1 = _createVNode("div", null, "hi vue3", -1 /* HOISTED */)
// render 函数会在这下面

小结

静态节点提升在整个 compile 编译阶段体现,从最初的 baseCompiletransform 转化原始 AST、再到 generate 的优先 render 函数处理生成可执行代码,最后交给 Runtime 时的 Render 执行,这种设计可以说是非常精妙!所以,这样一来,就完成了我们经常看到在一些文章提及的「Vue3」对于静态节点在整个生命周期中它只会执行「一次创建」的源码实现,这在一定程度上降低了性能上的开销。

以上就是W3Cschool编程狮关于Vue 3.0 diff 新特性 – 静态节点提升的相关介绍了,希望对大家有所帮助。

解决Java异常的10个实践经验

thbcm阅读(222)

文章来源于公众号:Java之道 ,作者Hollis

异常处理对于编写健康茁壮的Java应用起着极其重要的作用。异常处理并不是功能性需求,在实践中,异常处理不单单是知道语法这么简单。下面给大家展示10个最佳实践。

线上代码不要使用printStackTrace()

写完代码后请一定要检查下,代码中千万不要有printStackTrace()。因为printStackTrace()只会在控制台上输出错误的堆栈信息,他只适合于用来代码调试。

真正需要记录异常,请使用日志记录。

永远不要在catch块中吞掉异常

catch (NoSuchMethodException e) {
     return null;
  }

永远不要不处理异常,而是返回 null,这样异常就会被吞掉,无法获取到任何失败信息,会给日后的问题排查带来巨大困难。

在需要的地方声明特定的受检异常

 public void foo() throws Exception { //错误做法
  }

一定要尽量避免上面的代码,因为他的调用者完全不知道错误的原因到底是什么。

在方法声明中,可以由方法抛出一些特定受检异常。如果有多个,那就分别抛出多个,这样这个方法的使用者才会分别针对每个异常做特定的处理,从而避免发生故障。

public void foo() throws SpecificException1, SpecificException2 { 
//正确做法
}

始终只捕获那些可以处理的异常

catch (NoSuchMethodException e) 
{
    throw e; //这代码一点用都没有
}

这是一个基本概念,当你能够处理他时,你再捕获这个异常,否则永远都不要捕获他。

如果你不能在catch块中处理它,那么就不要先捕获再直接抛出,这样没有任何意义。

尽量捕获特定的子类,而不是直接捕获Exception类

try {
      someMethod();
  }
  catch (Exception e) 
  {
      LOGGER.error("method has failed", e);
  }

以上代码,最大的问题就是,如果someMethod()的开发者在里面新增了一个特定的异常,并且预期是调用方能够特殊的对他进行处理。

但是调用者直接catchException类,就会导致永远无法知道someMethod的具体变化细节。这久可能导致在运行的过程中在某一个时间点程序崩溃。

永远不要捕获Throwable类

这是一个严重的问题,因为 Java 中的Error也可以是Throwable的子类。但是ErrorJava 虚拟机本身无法控制的。Java 虚拟机甚至可能不会在出现任何错误时请求用户的catch子句。

始终在自定义异常中覆盖原有的异常,这样堆栈跟踪就不会丢失

 catch (NoSuchMethodException e) 
  {
  throw new MyServiceException("Some information: " + e.getMessage());  //错误做法
  }

上面的命令可能会丢失掉主异常的堆栈跟踪。正确的方法是:

catch (NoSuchMethodException e) {
     throw new MyServiceException("Some information: " , e);  //正确做法
}

可以记录异常或抛出异常,但不要同时做

catch (NoSuchMethodException e) {
     LOGGER.error("Some information", e);
     throw e;
  }

如上面的代码所示,抛出和日志记录可能会在日志文件中产生多个日志消息。

这就会导致同一个问题,却在日志中有很多不同的错误信息,使得开发人员陷入混乱。

永远不要在finally中抛出异常

try {
  someMethod();  //抛出 exceptionOne
}
 finally
{
  cleanUp();    //如果在这里再抛出一个异常,那么try中的exceptionOne将会丢失forever
}

在上面的例子中,如果someMethod()抛出一个异常,并且在finally块中,cleanUp()也抛出一个异常,那么初始的exceptionOne(正确的错误异常)将永远丢失。

如果您不打算处理异常,请使用finally块而不是catch块

try {
  someMethod();
}
finally
{
  cleanUp();
}

这也是一个很好的实践。如果在你的方法中访问其他方法,而该方法抛出了异常你不想处理,但是仍然需要做一些清理工作,那么在finally块中进行清理。不要使用catch块。

以上就是W3Cschool编程狮关于解决Java异常的10个实践经验的相关介绍了,希望对大家有所帮助。

用 Python 诠释啥叫硬核老爸

thbcm阅读(212)

文章来源于公众号:Python技术 作者:派森酱

前几天,给儿子买了个飞行棋,甚是喜欢,每天都要和我来两盘,昨天准备大战一场时,发现骰子弄丢了,没有骰子就没法玩了,正想要用橡皮做一个,突然想到了个更好的办法,经过一顿折腾,终于搞定了,结果……

构思

骰子是个立方体,有六个面,每个面上,标有不同地点,从 1 个 到 6 个,代表 1 到 6 六个数字,玩的时候,将骰子一掷,等它停下,朝上的面是几点,就表示摇到了几。

不同的游戏中,对摇到的点数有不同的玩法,例如飞行棋中,摇到 5 或者 6,可以起飞一架飞机。

现在我需要用程序来模拟这个过程,实际上就是产生 1 到 6 直接的随机数,直接用 random.randint(1, 6) 就可以搞定,不过我不想就这样简单完成,一是对于小孩子来说,直接给出数字不够直观,二是,能有机会给儿子炫技一把,何乐不为?

于是构思如下:

  • 找一些骰子的素材,需要有每个数字向上的图片
  • 为了制造骰子的转动效果,还需要一些处于转动中间状态的图片
  • 随机产生 0 到 5 之间的数字,0 代表点数 1,1 代表点数 2,依次类推,5 代表点数 6,为什么不直接生成 1 到 6 呢?后面有解答
  • 掷骰子过程有两种状态,即 显示点数 和 转动,那么就需要有触发机制,考虑到小孩子对鼠标操作不灵活,选用空格键来控制,按一下就相当于掷一次

实现

构思好后,赶紧实现!

素材

先从网上找了些骰子的素材,最终选择了以微信掷骰子表情图为元素的一系列 gif 图片,通过图片解析工具,从 gif 图片中提取出每个帧,其中包括了点数朝上的图片,和转动中间的图片,这样图片素材就搞定了。

实践时如果不方便获得图片素材,可从本文示例代码中获得

接下来,就是编程部分了,使用 pygame 这个 python 游戏引擎库。

骰子

首先,写一个 骰子类,用来定义骰子的各种资源,以及管理骰子的状态,代码如下:

import random
import pygame


class Dice:
    def __init__(self):
        self.diceRect = pygame.Rect(200, 225, 100, 100)
        self.diceSpin = [
            pygame.image.load("asset/rolling/4.png"),
            pygame.image.load("asset/rolling/3.png"),
            pygame.image.load("asset/rolling/2.png"),
            pygame.image.load("asset/rolling/1.png")
        ]
        self.diceStop = [
            pygame.image.load("asset/dice/1.png"),
            pygame.image.load("asset/dice/2.png"),
            pygame.image.load("asset/dice/3.png"),
            pygame.image.load("asset/dice/4.png"),
            pygame.image.load("asset/dice/5.png"),
            pygame.image.load("asset/dice/6.png")
        ]


        self.StopStatus = random.randint(0, 5)
        self.SpinStatus = 0


    def move(self):
        self.SpinStatus += 1
        if self.SpinStatus == len(self.diceSpin):
            self.SpinStatus = 0

  • 初始化方法中,用 pygame.Rect 方法设定了一个矩形区域,即游戏窗口坐标为(200, 225),高度和宽度都为 100,这个矩形区域是为了在游戏窗口中绘制骰子用的
  • diceSpin 存储了骰子转动过程中的图片素材,注意需要用 pygame.image.load 方法加载图片资源
  • diceStop 存储了骰子点数的图片素材
  • StopStatus 记录骰子停止状态的点数值,在 0 ~ 5 之间,初始化为一个随机数
  • SpinStatus 记录转动过程中当前帧的图片索引,默认为 0
  • move 方法相当于一个转动控制器,每调用一次会改变一次转动中图片的索引,骰子转动过程中会反复被调用

引擎

接下来,编写一个游戏引擎类,用于驱动游戏过程,代码如下:

import random
import sys
import pygame


class Game:
    def __init__(self, width=500, height=600):
        pygame.init()
        size = width, height
        self.screen = pygame.display.set_mode(size)
        self.clock = pygame.time.Clock()
        self.screen.fill((255, 255, 255))


        self.rollTimes = 0  # 掷骰子过程的帧数记录
        self.Dice = Dice()
        self.start = False  # 状态标识
        self.rollCount = random.randint(3, 10)  # 初始投掷帧数


    def roll(self):
        self.screen.blit(self.Dice.diceSpin[self.Dice.SpinStatus], self.Dice.diceRect)
        self.Dice.move()
        self.rollTimes += 1
        if self.rollTimes > self.rollCount:
            self.start = False
            self.rollCount = random.randint(3, 10)
            self.Dice.StopStatus = random.randint(0, 5)
            self.rollTimes = 0


    def stop(self):
        self.screen.blit(self.Dice.diceStop[self.Dice.StopStatus], self.Dice.diceRect)


    def run(self):
        while True:
            self.clock.tick(10)
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    sys.exit()
                if ((event.type == pygame.KEYDOWN and event.key==pygame.K_SPACE) \
                or event.type == pygame.MOUSEBUTTONDOWN) \
                and self.start == False:
                    self.start = True


            if self.start:
                self.roll()
            else:
                self.stop()
            pygame.display.update()

  • 初始化方法中,做了游戏窗口的初始化,并设定了窗口大小,然后对过程中的控制类变量做了初始化
  • roll 方法为抛掷,抛掷过程中会被反复调用,先设置一个转动中图片,然后,调用骰子的 move 方法,得到一个新的转动状态
  • roll 方法中,接下来是一个控制器,如果达到了设定的转动次数,就停止,并得到随机的点数
  • stop 方法,在停止转动时调用,将转动停止时的点数图片绘制到窗口上,这里StopStatus 范围是 0 ~ 5,刚好对应 diceStop 列表的索引,这就是随机数范围是 0~5 的原因
  • run 方法是引擎的启动入口,它启动了一个事件循环
  • 循环中,检查一遍事件记录,如果接收到了退出事件,则结束循环
  • 如果接收到了按下空格键或者鼠标键,且投掷状态为停止时,将投掷状态设置为开始
  • 检查完事件记录,判断投掷状态,如果是开始状态,调用 roll 方法,否则调用 stop 方法
  • 最后每次循环都需要调用 pygame.display.update() 刷新一次窗口

这里需要说明下 clock.tick,它的作用是让循环每秒执行多少次,抽象来说可以理解为动画的帧,即每秒多少帧。

相对于 clock.tick,我们更熟悉 time.sleep,后者表示等待多久再执行,那么 clock.tick(10) 的效果就相当于 time.sleep(0.1),即每秒执行 10 次,就等于每次等待十分之一秒。

运行

if __name__ == '__main__':
    Game().run()

注意:将目录切换到代码目录下运行,否则可能提示找不到图片文件。

运行效果如下,像那么回事吧:

折腾完后,我迫不及待地去儿子跟前炫耀,结果,他已经睡着了,身旁散落着一些飞行棋子儿……

总结

无论在生活或者工作中,编程技能越来越重要了,编程已然成为了思考和创造的工具了,习得一项编程技能,不仅能帮助自己,也许可以省一笔少儿编程的花费,在提高孩子逻辑思维能力的同时,还能增进与孩子的感情,不得不说,当儿子使用我编写的骰子玩飞行棋时,更开心了。

做硬核家长,我用 Python

以上就是W3Cschool编程狮关于用 Python 诠释啥叫硬核老爸的相关介绍了,希望对大家有所帮助。

动图演示:手撸堆栈的两种实现方法!

thbcm阅读(195)

文章来源于公众号:Java中文社群 作者:磊哥

随着软件开发行业竞争的日益激烈,面试的难度也在逐渐增加,因为企业要从众多的面试人中选出最优秀的人,只能提高面试的难度,而算法和数据结构比较烧脑的硬核技能之一,自然也就成了面试的首选科目。并且随着时间的推移,算法和数据结构出现的频率和占比也会不断增加,因此为了顺应时代发展的潮流,我们也要做一些调整,所以在后面的一些文章中,我会陆续更新一些关于算法和数据结构的文章,希望大家能够喜欢。

PS:当然随着智能系统的普及(如今日头条和抖音),算法和数据结构在企业中应用也越来越多,因此学习算法和数据结构也是迫在眉睫的事了。

栈定义

栈(Stack)又叫堆栈(简称栈),它是在同一端进行插入和删除数据的线性表。

栈是最基础也是最常见的数据结构之一,它的数据结构和操作流程如下图所示:

其中,允许进行插入和删除的一端叫作栈顶(Top),另一端叫作栈底(Bottom),栈底固定,栈顶浮动。

当栈中的元素为零时,该栈叫作空栈。添加数据时一般叫作入栈或进栈(Push),删除数据叫作出栈或退栈(Pop)。栈是后进先出(Last In First Out,LIFO)的线性表

物理结构 & 逻辑结构

在手撸算法之前,我们先来认识一下数据结构中的两个重要概念:物理结构和逻辑结构

当谈到“物理”和“逻辑”一词时,我们可以会想到数据库中的逻辑删除和物理删除。

所谓的物理删除是指通过删除命令真实的将数据从物理结构中删除的过程;而逻辑删除是指通过修改命令将数据更改为“已删除”的状态,并非真实的删除数据。

这里的逻辑结构和物理结构和上面的概念类似,所谓的物理结构是指可以将数据存储在物理空间中,比如数组和链表都属于物理数据结构;而逻辑结构则是用于描述数据间的逻辑关系的,比如本文要讲的栈就属于逻辑结构。

可能有些人看到这里就蒙了,没关系,我这里举一个例子你就明白了。

如果用人来表示物理结构和逻辑结构的话,那么真实存在的有血有肉的人就属于物理结构,而人的思想和信念就属于逻辑结构了

自定义栈I:数组实现

通过上面的内容,我们知道了栈属于逻辑结构,因此它的实现方式就可以有很多种了,比如数组的实现方式或者是链表的实现方式。那么我们就先用数组实现一下,栈的主要方法有:

① 定义结构

那么我们先来定义它的结构:

public class MyStack {
    private Object[] value = null; // 栈存储容器
    private int top = -1; // 栈顶(的指针)
    private int maxSize = 0; // 栈容量


    // 构造函数(初始化默认容量)
    MyStack() {
        this.maxSize = 10;
    }


    // 有参构造函数
    MyStack(int initSize) throws Exception {
        if (initSize  Hello


从上述代码可以看出,我们添加栈的顺序是 `Hello`、`Java` 而输出的顺序是 `Java`、 `Hello` 符合栈的定义(后进先出)。


## 自定义栈II:链表实现


除了数组之外,我们可以还可使用链表来实现栈结构,它的实现稍微复杂一些,我们先来看链表本身的数据结构:


![链表的数据结构](https://atts.w3cschool.cn/attachments/image/20200923/1600831705876009.png "链表的数据结构")


使用链表实现栈的流程如下:


![链表实现栈的流程](https://atts.w3cschool.cn/attachments/image/20200923/1600831725907957.gif "链表实现栈的流程")


也就是说,入栈时我们将数据存储在链表的头部,出栈时我们从头部进行移除,并将栈顶指针指向原头部元素的下一个元素,实现代码如下。


我们先来定义一个链表节点:

public class Node { Object value; // 每个节点的数据 Node next; // 下一个节点

public Node(Object value) { this(value, null); }

/**

  • 创建新节点
  • @param value 当前节点数据
  • @param next 指向下一个节点(头插法) */ public Node(Object value, Node next) { this.value = value; this.next = next; } }

接下来我们使用链表来实现一个完整的栈:


public class StackByLinked {


    private Node top = null; // 栈顶数据
    private int maxSize = 0; // 栈最大容量
    private int leng = 0; // 栈实际容量


    public StackByLinked(int initSize) throws Exception {
        if (initSize = maxSize;
    }


    /**
     * 是否为空
     * @return
     */
    public boolean isEmpty() {
        return leng  Java
>
> Hello


## 总结


本文我们使用了数组和链表等物理结构来实现了栈,当然我们也可以使用其他容器来实现,比如 [Java](https://www.w3cschool.cn/java/) 中的 `List`,我们只需要保证在操作栈时是后进先出的执行顺序,并且至少包含 3 个重要方法:入栈、出栈和查询栈顶元素就可以了。


## 最后


**算法和数据结构的学习是 3 分学 7 分练**,只看不练是没办法学好算法的,而且**学习算法和数据结构是一个循序渐进的过程,短时间内不会有明显的收效**。因为这些算法经过了几百年的发展和积累才得以流传下来的,所以想要“玩得转”还需要一点耐心。


这里给你讲一个学习算法的“秘诀”:**看不懂的知识要反复看,如果反复看还是看不懂,那么别着急,休息一下再继续看!**相信我,对于学习算法这件事,所有人的过程都是一样的。


以上就是`W3Cschool编程狮`关于**动图演示:手撸堆栈的两种实现方法!**的相关介绍了,希望对大家有所帮助。

TypeScript 中的class和interface

thbcm阅读(203)

文章来源于公众号:小丑的小屋

前言

刚刚的vue3.0一发布,各大网址和社区以及公众号已经被 Vue3.0 的One Piece版本所霸屏,出现不同的标题有着同样内容的现象,借此热度我们不如好好回顾一下ts基础知识,备战vue3.0的正式使用。

TypeScript 这个东西说实在的,真的是容易忘记,一段时间不用就感觉特别陌生,但是回过头来看看,又有一种熟悉的感觉,有句话是这么说的:ts越用越香,它确实能够规范我们的书写的格式,语法校验和类型校验等。之前写过react+ts的一个demo,但是时间久了就忘记了,现在也是趁着热度再回顾一下ts的内容,以及一些高阶语法,现在我们回顾一下ts中常见的类和接口。

class

首页我们要清楚的一点是 TypeScript 中类和 JavaScript 中ES6语法类的区别,千万不要混淆。ts 中相比于 js 添加了声明属性的类型和参数的类型以及返回结果类型。这个地方一看就会一写就不对,如果不声明 ts 会报错。

class Person{
    name:string;
    constructor(name:string){
        this.name = name;
    }
    getName():void{
        console.log(this.name);
    }
}
class Person{
    constructor(name){
        this.name = name;
    }
    getName(){
        console.log(this.name);
    }
}

ES5编辑后的结果

var Person = /** @class */ (function () {
    function Person(name) {
        this.name = name;
    }
    Person.prototype.getName = function () {
        console.log(this.name);
    };
    return Person;
}());

类的get和set

ts 在编译 get 和 set 的时候默认是 es3 编译,vscode 编辑器会报错error TS1056: Accessors are only available when targeting ECMAScript 5 and higher需要编译到版本 ES5 或以上,解决办法:$ tsc xxx.ts -t es5

class User{
    myname:string;
    constructor(myname:string){
        this.myname = myname
    }
    get name(){
        return this.myname
    }
    set name(newName:string){
        this.myname = newName
    }
}

ES5编译后的结果

var User = /** @class */ (function () {
    function User(myname) {
        this.myname = myname;
    }
    Object.defineProperty(User.prototype, "name", {
        get: function () {
            return this.myname;
        },
        set: function (newName) {
            this.myname = newName;
        },
        enumerable: false,
        configurable: true
    });
    return User;
}());

这里有几个思考问题,答案见文末:

var u = new User("a");
console.log(u);
console.log(u.myname);
u.myname = 'b';
console.log(u.myname);
console.log(u.hasOwnProperty("name"));
console.log(Object.getPrototypeOf(u));
console.log(Object.getPrototypeOf(u).hasOwnProperty("name"));

抽象类

abstract关键字表示抽象类,抽象类是不能被实例化即new,只能被继承,抽象类一般是用来封装一些公共的,提供给子类使用的方法和属性的

abstract class Animal{
    public readonly name:string;
    protected age:number = 38;
    private money:number = 10;
    constructor(name:string){
        this.name = name
    }
}
class Dog extends Animal{
    static className = 'Dog'
    static getClassName(){
        console.log(this.className)
    }
    getName(){
        console.log(this.name)
    }
    getAge(){
        console.log(this.age)
    }
}
let a = new Animal("ss");

这里打印看一下继承的结果:

console.log(a); //Dog { age: 38, money: 10, name: 'ss' }

这里顺便说一下访问修饰符 public protected private

  • public 里里外外都能访问,包括自己、自己的子类、其他类都能
  • protected 自己和子类能访问但是其他地方不能访问
  • private 私有的(只有自己能访问,子类的其他都不能访问)

interface

接口表示对象的属性

interface Rectangle {
    width: number;
    height: number
}


let r: Rectangle = {
    width: 100, height: 10
}


interface Speakable {
    speak(): void;
    name?: string
}


let speaker: Speakable = {
    //name:"bdt",
    speak() { }
}

接口用来描述抽象的行为

interface AnimalLink {
    eat(): void;
    move(): void
}

接口可以实现继承

interface PersonLike extends AnimalLink {
    speak(): void
}
class Person2 implements PersonLike {
    speak() { };
    eat() { };
    move() { }
}

通过接口约束变量类型

interface Person3 {
    readonly id: number;
    name: string;
    [PropName: string]: any
}
let p1: Person3 = {
    id: 1,
    name: "sss"
}

通过接口约束(规范)函数

interface DiscountInterface{
    (price:number):number
}
let discount:DiscountInterface = function (price: number): number {
    return price * .8
}

通过索引约束数组和对象

interface UserInterface{
    [index:number]:string
}


let arr:UserInterface = ['aa','bb']


interface UserInterface2{
    [index:string]:string
}
let obj:UserInterface2  = {name:"sss"}

通过接口约束构造函数

class Animal3{
    constructor(public name:string){}
}
interface WithClassName{
    new (name:string):Animal3
}
function createClass(clazz:WithClassName,name:string){
    return new clazz(name)
}
let a3 = createClass(Animal3,"别抖腿");
console.log(a3)

class和interface的区别

  • class 类声明并实现方法
  • interface 接口声明,但是不能实现方法
abstract class Animal{
    name:string="111";
    abstract speak():void;  //抽象类和方法不包含具体实现  必须在子类中实现
}
//接口里的方法都是抽象的
interface Flying{
    fly():void
}
interface Eating{
    eat():void
}
class Dog extends Animal{
    speak(){
        console.log("汪汪汪")   //重写:子类重写继承自父类中的方法
    }
}
class Cat extends Animal implements Flying,Eating{
    speak(){   //继承抽象类的方法必须实现
        console.log("喵喵喵")
    }
    fly(){
        console.log("我是一只飞货")
    }
    eat(){
        console.log("我是一只吃货")
    }
}

写在最后

文中答案

User { myname: 'a' }
a
b 
false
User { name: [Getter/Setter] }
true

以上就是W3Cschool编程狮关于TypeScript 中的class和interface的相关介绍了,希望对大家有所帮助。

SpringBoot 项目接入 Redis 集群

thbcm阅读(221)

文章来源于公众号:Java极客技术 作者:鸭血粉丝

想必大家一定对 Redis 不会陌生,平常工作中或多或少都会用到,不管是用来存储登录信息还是用来缓存热点数据,对我们来说都是很有帮助的。但是 Redis 的集群估计并不是每个人都会用到,因为很多业务场景或者系统都是一些简单的管理系统,并不会需要用到 Redis 的集群环境。

阿粉之前也是这样,项目中用的的 Redis 是个单机环境,但是最近随着终端量的上升,慢慢的发现单机已经快支撑不住的,所以思考再三决定将 Redis 的环境升级成集群。下面阿粉给大家介绍一下在升级的过程中项目中需要调整的地方,这篇文章不涉及集群的搭建和配置,感兴趣的同学自行搜索。

配置参数

因为这篇文章不介绍 Redis 集群的搭建,这里我们假设已经有了一个 Redis 的集群环境,我们项目中需要调整以下几个部分

  1. 修改配置参数,集群的节点和密码配置;
  2. 确保引入的 Jedis 版本支持设置密码,spring-data-redis 1.8 以上,SpringBoot 1.5 以上才支持设置密码;
  3. 注入 RedisTemplate
  4. 编写工具类;

修改配置参数

############### Redis 集群配置 #########################
spring.custome.redis.cluster.nodes=172.20.0.1:7001,172.20.0.2:7002,172.20.0.3:7003
spring.custome.redis.cluster.max-redirects=3
spring.custome.redis.cluster.max-active=500
spring.custome.redis.cluster.max-wait=-1
spring.custome.redis.cluster.max-idle=500
spring.custome.redis.cluster.min-idle=20
spring.custome.redis.cluster.timeout=3000
spring.custome.redis.cluster.password=redis.cluster.password

引入依赖(如果需要)

确保 SpringBoot 的版本大于 1.4.x 如果不是的话,采用如下配置,先排除 SpringBoot 中旧版本 Jedisspring-data-redis,再依赖高版本的 Jedisspring-data-redis


       
            org.springframework.boot
            spring-boot-starter-data-redis

            

            

                
                    redis.clients
                    jedis

                

                
                    org.springframework.data
                    spring-data-redis

                

            

        

        

        
            redis.clients
            jedis
            2.9.0

        

        
            org.springframework.data
            spring-data-redis
            1.8.0.RELEASE

        

注入 RedisTemplate

注入 RedisTemplate 我们需要三个组件,分别是JedisConnectionFactoryRedisClusterConfigurationJedisPoolConfig,下面是注入RedisTempalte 的代码。先根据配置创建 JedisConnectFactory 同时需要配置 RedisClusterConfigurationJedisPoolConfig,最后将JedisConnectionFactory 返回用于创建RedisTemplate

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.RedisNode;
import org.springframework.data.redis.connection.jedis.JedisClientConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;


import java.time.Duration;
import java.util.ArrayList;
import java.util.List;


public class RedisClusterConfig {


    @Bean(name = "redisTemplate")
    @Primary
    public RedisTemplate redisClusterTemplate(@Value("${spring.custome.redis.cluster.nodes}") String host,
                                     @Value("${spring.custome.redis.cluster.password}") String password,
                                     @Value("${spring.custome.redis.cluster.timeout}") long timeout,
                                     @Value("${spring.custome.redis.cluster.max-redirects}") int maxRedirect,
                                     @Value("${spring.custome.redis.cluster.max-active}") int maxActive,
                                     @Value("${spring.custome.redis.cluster.max-wait}") int maxWait,
                                     @Value("${spring.custome.redis.cluster.max-idle}") int maxIdle,
                                     @Value("${spring.custome.redis.cluster.min-idle}") int minIdle) {


        JedisConnectionFactory connectionFactory =  jedisClusterConnectionFactory(host, password,
                timeout, maxRedirect, maxActive, maxWait, maxIdle, minIdle);
        return createRedisClusterTemplate(connectionFactory);
    }


    private JedisConnectionFactory jedisClusterConnectionFactory(String host, String password,
                                                                   long timeout, int maxRedirect, int maxActive, int maxWait, int maxIdle, int minIdle) {
        RedisClusterConfiguration redisClusterConfiguration = new RedisClusterConfiguration();
        List nodeList = new ArrayList();
        String[] cNodes = host.split(",");
        //分割出集群节点
        for (String node : cNodes) {
            String[] hp = node.split(":");
            nodeList.add(new RedisNode(hp[0], Integer.parseInt(hp[1])));
        }
        redisClusterConfiguration.setClusterNodes(nodeList);
        redisClusterConfiguration.setPassword(password);
        redisClusterConfiguration.setMaxRedirects(maxRedirect);


        // 连接池通用配置
        GenericObjectPoolConfig genericObjectPoolConfig = new GenericObjectPoolConfig();
        genericObjectPoolConfig.setMaxIdle(maxIdle);
        genericObjectPoolConfig.setMaxTotal(maxActive);
        genericObjectPoolConfig.setMinIdle(minIdle);
        genericObjectPoolConfig.setMaxWaitMillis(maxWait);
        genericObjectPoolConfig.setTestWhileIdle(true);
        genericObjectPoolConfig.setTimeBetweenEvictionRunsMillis(300000);


        JedisClientConfiguration.DefaultJedisClientConfigurationBuilder builder = (JedisClientConfiguration.DefaultJedisClientConfigurationBuilder) JedisClientConfiguration
                .builder();
        builder.connectTimeout(Duration.ofSeconds(timeout));
        builder.usePooling();
        builder.poolConfig(genericObjectPoolConfig);
        JedisConnectionFactory connectionFactory = new JedisConnectionFactory(redisClusterConfiguration, builder.build());
        // 连接池初始化
        connectionFactory.afterPropertiesSet();


        return connectionFactory;
    }


    private RedisTemplate createRedisClusterTemplate(JedisConnectionFactory redisConnectionFactory) {
        RedisTemplate redisTemplate = new RedisTemplate();
        redisTemplate.setConnectionFactory(redisConnectionFactory);


        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);


        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        // key采用String的序列化方式
        redisTemplate.setKeySerializer(stringRedisSerializer);
        // hash的key也采用String的序列化方式
        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        // value序列化方式采用jackson
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        // hash的value序列化方式采用jackson
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.afterPropertiesSet();


        return redisTemplate;
    }
}

相关代码已经提交到GitHub,公众号回复【源码仓库】即可获取。这里一定要注意 Jedis 的 Spring-data-redis 的版本支持设置密码,毕竟生产环境是一定要配置密码的。

编写工具类

其实到这里基本上已经完成了,我们可以看到 SpringBoot 项目接入 Redis 集群还是比较简单的,而且如果之前单机环境就是采用RedisTemplate 的话,现在也就不需要编写工具类,之前的操作依旧有效。不过作为贴心的阿粉,我还是给大家准备了一个工具类,代码太长,我只贴部分,需要完成代码的可以到公众号回复【源码仓库】获取。

/**
     *  删除KEY
     * @param key
     * @return
     */
    public boolean delete(String key) {
        try {
            return getTemplate().delete(key);
        } catch (Exception e) {
            log.error("redis hasKey() is error");
            return false;
        }
    }


    /**
     * 普通缓存获取
     *
     * @param key 键
     * @return 值
     */
    public Object get(String key) {


        return key == null ? null : getTemplate().opsForValue().get(key);
    }


    /**
     * 普通缓存放入
     *
     * @param key   键
     * @param value 值
     * @return true成功 false失败
     */
    public boolean set(String key, Object value) {


        try {
            getTemplate().opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
            log.error("redis set() is error");
            return false;
        }


    }


    /**
     * 普通缓存放入并设置时间
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒) time要大于0 如果time小于等于0 将设置无限期
     * @return true成功 false 失败
     */
    public boolean set(String key, Object value, long time) {
        try {
            if (time > 0) {
                getTemplate().opsForValue().set(key, value, time, TimeUnit.SECONDS);
            } else {
                set(key, value);
            }
            return true;
        } catch (Exception e) {
            log.error("redis set() is error");
            return false;
        }
    }


    /**
     * 计数器
     *
     * @param key 键
     * @return 值
     */
    public Long incr(String key) {


        return getTemplate().opsForValue().increment(key);
    }


    public Long incrBy(String key, long step) {


        return getTemplate().opsForValue().increment(key, step);
    }


    /**
     * HashGet
     *
     * @param key  键 不能为null
     * @param item 项 不能为null
     * @return 值
     */
    public Object hget(String key, String item) {


        return getTemplate().opsForHash().get(key, item);
    }


    /**
     * 获取hashKey对应的所有键值
     *
     * @param key 键
     * @return 对应的多个键值
     */
    public Map hmget(String key) {


        return getTemplate().opsForHash().entries(key);
    }


    /**
     * 获取hashKey对应的批量键值
     * @param key
     * @param values
     * @return
     */
    public List<Object> hmget(String key, List values) {


        return getTemplate().opsForHash().multiGet(key, values);
    }

上面随机列了几个方法,更多方案等待你的探索。

总结

今天阿粉给大家介绍了一下 SpringBoot 项目如何接入 Redis 集群,需要的朋友可以参考一下,不过阿粉还是要说一下,系统的设计不能过于冗余,如果短期内还能支撑业务的发展,那就暂时不要考虑太复杂,毕竟系统的架构是需要不断的完善的,不可能刚开始的时候就设计出一套很完善的系统框架。随着业务的不断发展,当真正发现单机 Redis 已经无法满足业务需求的时候再接入也不迟!

以上就是W3Cschool编程狮关于SpringBoot 项目接入 Redis 集群的相关介绍了,希望对大家有所帮助。

小程序云开发支持公众号网页开发了

thbcm阅读(199)

文章来源于公众号:Vue中文社区 作者:性感超人

小程序云开发,这波更新有料!

云开发(Tencent Cloud Base)

做前端的同学基本上或多或少都接触过 小程序 ,但是,接触过小程序的同学不一定接触过云开发

普及一下

云开发是一种典型的 Serverless 架构的实现方式,这里可能还有同学对 Serverless 不熟悉

我们再把这个词拆开,Server(服务器) less(不重要),可以理解为“无服务器”

试想一下,大多数公司和开发者在开发应用时和部署服务时,无论是选择公有云还是自建数据中心,都需要提前考虑服务器、存储和数据库等需求,并且需要花费时间精力在部署应用、依赖。那么是否有一种架构可以帮我们节省这部分的成本呢?有,就是我们的 Serverless(无服务器)架构

具体来说,Serverless 架构是指由第三方服务商负责后端基础结构的维护,以服务的方式为开发者提供如数据库、消息、身份验证等功能,架构的目的就是要让开发人员只需要专注业务本身

现在我们在回到小程序云开发本身,云开发官方封装了云函数、存储、云数据库的能力,一个函数调用,直接就实现了鉴权、文件上传、更新数据等最最常用的功能

两个字来说,就是真香

我们以一个更具体的功能来说

传统模式下我们想要调用微信登录,需要将 code 传给服务端,服务端将 code 拿去微信服务换取用户信息,自做存储,更新

而使用云开发时,我们只要调用一个api,没看错,就是一个api,一行代码完事

我们看下图对比一下

跨账号环境共享

小程序 的云开发一直都是一个appid对应一个独立云环境,这个怎么理解呢?

还是以实际场景举例,比如你有A,B,C三个小程序产品,你的某些数据可能需要共享的,如用户信息共享,商品信息共享

这个在发文前小程序云开发是没有这个能力处理的,所以想要实现要么就是自建共享数据服务,要么就是直接弃用云服务

好在今天它来了

云开发支持跨账号环境(资源)共享,也就是一个小程序 的云开发资源可以授权给其他小程序 / 公众号使用

开通方式也很简单,只需要在云开发的后台进行给相应的小程序 / 公众号授权即可,如下图

公众号环境打通

细心的同学可能已经发现了,上面有提到公众号

What? 公众号也有云服务了吗?

是滴,这次更新云开发打通了公众号体系,公众号只要小程序给开通授权,再引入云开发的Web sdk 就可以直接使用小程序的云服务

对我们来说有什么好处

  • 资源复用

云开发小程序所有资源都可以在web端复用

  • 能力复用

云开发小程序所有能力都可以在web端复用(如你花两个星期写好的一个全是bug的云函数)

更直观的感受,比如我们之前 jssdk 的签名方式

wx.config({
  appId: '公众号 AppID', // 必填,公众号的唯一标识
  timestamp: res.timestamp + '', // 必填,生成签名的时间戳
  nonceStr: res.nonceStr, // 必填,生成签名的随机串
  signature: res.signature,// 必填,签名
  jsApiList: ['JS API 名'] // 必填,需要使用的JS接口列表
})

现在我们只需要这样

const res = await cloud.getJSSDKSignature({
  url: '要签名的网页 URL'
})

静态网站托管

这个词相信大家已经很熟了

当我们想布署一个静态的网页应用,博客,手头上暂时又没有域名,服务器的时候,我们可以将静态网站托管到第三方平台,比如同性交友github

那么小程序云开发的静态网站托管跟其它的有什么不同吗?

原则上没什么不同,但作为微信生态的一部分,在微信中有着天然优势

比如:

  • 小程序 webview

小程序不用配置业务域名即可在打开云开发静态网站托管的域名(仅支持能够使用标签的小程序)

  • 免鉴权直接打开小程序

非个人主体的认证的小程序,使用静态网站托管的网页,可以免鉴权跳转任意合法合规的小程序

写到这里,帅编突然邪魅一笑,是不是可以用静态网站托管做一个微信小程序的应用分发,然后躺着挣流量钱

最后

虽然吹了一波云开发,但还是得给个中肯建议

个人项目、功能简单的小程序可以尝试使用,那是真香

规模稍大以及企业级项目现阶段不建议完全使用,可按需混合使用

以上就是W3Cschool编程狮关于小程序云开发支持公众号网页开发了的相关介绍了,希望对大家有所帮助。

Moment.js官方推荐使用其它时间处理库代替

thbcm阅读(210)

文章来源于公众号:印记中文 译者:Phobal

近期,Moment.js 在官方文档中发布了项目状态,文中写道:Momentjs 正式进入维护期,不会再提供大版本更新,推荐使用其他时间处理库代替或使用 JavaScript 处于实验阶段的提案 Temporal。

以下是针对这篇项目状态的中文翻译。

Moment.js 已广泛应用于数百万个项目中,能帮助你处理网站中日期和时间的问题,我们感到万分荣幸。截至 2020 年 9 月,Moment 每周下载量超 1200 万次!然而 Moment 是为 JavaScript 生态系统的上一个时代而构建的。这些年 Web 发生翻天覆地的变化,Moment 紧随其后,但其设计与 2011 年创建时基本相同。鉴于有很多项目依赖它,所以我们优先考虑稳定性而非新功能。

Moment 对象是可变对象(mutable),这点经常被用户所诟病。尽管我们在 使用指南 中写明了如何解决此问题,但还是会有很多新用户犯错。而如果将 Moment 变为不可变对象(immutable)这会对已使用 Moment 的项目产生破坏性的影响。让 Moment 支持 immutable 本身就是件艰巨的任务,这将使 Moment 变为另外一个库,现阶段已有很多类似的库实现这个特性,所以让 Moment 保持 mutable 也没什么不好。

而大家所诟病的另一点就是 Moment 包的体积大小,而 tree shakingMoment 无效,导致应用的包体积剧增。如果你的应用中需要用到国际化和时区,那么 Moment 可能会大到超乎想象。现代 Web 浏览器(和 Node.js)通过Intl 对象(其编号为 ECMA-402)实现了对国际化和时区支持。而像 Luxon 之类的库就利用了这一优势来降低库文件的大小。

最近,Chrome 开发工具会建议用户更换 Moment。我们也赞成此举动。

社区针对这个问题发表过很多文章:

  • 你可能不再需要 Moment.js
  • 你(可能)不需要 Moment.js
  • 为什么你不需要使用 Moment.js…
  • 4 个可替代 moment.js 的库,用于日期国际化

Moment 团队也针对这些问题做过详细的讨论。我们意识到可能已有项目会继续使用 Moment。但我们并不推荐你在新项目中使用它,相反,我们向大家 推荐 一些能更好应用于现代 web 的库。同时还推荐大家试用 JavaScript 处理时间的新提案 – Temporal,该提案需要大家的支持和贡献。

我们正式宣布 Moment 进入维护期,但并非消亡,只是完成了使命

事实上,这意味着:

  • 不再添加新功能。
  • 不会将 API 变为 immutable。
  • 不会解决 tree shaking 及包体积的问题。
  • 不会进行任何重大更改(不会有 v3)。
  • 可能选择不对 bug 进行修复,特别是长期存在的已知 bug。

关于 Moment 的国际化语言环境文件:

  • 我们可能选择不接受对语言环境字符串或本地化日期格式的更正,特别是它们的现有格式已被论证时。
  • 语言环境发生重大改变时,必须提出令人信服的依据来支持你的立场。
  • 如果你要更改字符串或格式,那么你必须先在 CLDR 上提交更改申请并被接受后才可更改。

但是,由于 Moment 使用者还很多,当遇到以下问题时我们会及时进行处理:

  • 当出现严重的安全问题时,我们将予以解决。
  • 我们将在 IANA 时区数据库 发布更新后更新 Moment-Timezone 的数据。

需要继续使用 Moment 的场景

在大多数情况下,新项目请不要选择 Moment。但是,在一些特殊的场景下你可能还是需要使用它。

浏览器支持

Moment 能在 IE8 下完美运行。相比之下,Luxon 只能在 IE 10 及更高版本上运行,并且还需要搭配 polyfill 使用。你可以在 Luxon 的文档中了解更多相关内容。

其他库在 Safari 上也有相同的问题,尤其是在移动设备上。如果你必须要支持旧版浏览器,那可能还要继续使用 Moment

但是,Day.js 支持 IE8 及更高版本,如果有兼容性相关的需求,你可以考虑使用它。

其他库的依赖

有些库,尤其是日期选择器和图形库,都将 Moment 作为依赖项,如果你正在使用类似的组件,且找不到替代方案,那么你的项目已经依赖了 Moment。你可以在项目中继续使用 Moment,而不需要再引入日期时间库。

忠实粉丝

如果你是 Moment 的忠实粉丝,那么你肯定非常了解它的 API 和局限性。如果是这样,并且不 care 上述问题,那可以继续使用它。

推荐一些替代库

有很多不错的库可以代替 Moment。

在做选择时,请考虑下面几点:

  • 有些库会被分割为模块,插件及配套库。
  • 有些库将 ECMAScript 的 Intl API 用于语言环境、时区或两者皆有。
  • 有些库仍像 Moment 和 Moment-Timezone 一样提供自己的语言环境和时区文件。

以下是我们推荐的替代方案:

Luxon

Luxon 可以认为是 Moment 的演变,它由 Moment 的长期撰稿人 Isaac Cambron 撰写。请阅读为什么会存在Luxon?以及 Luxon 文档中的 For Moment用户 相关文档。

  • 语言环境:Intl 实现
  • 时区:Intl 实现

Day.js

使用类似的 API,Day.js 被设计为 Moment.js 的极简替代品。它不是临时替代品,如果你习惯使用 Moment 的 API 并希望快速入门,请考虑使用 Day.js。

  • 语言环境:可以单独导入的自定义数据文件
  • 时区:Intl 通过插件实现

date-fns

Date-fns 提供了一系列用于处理 JavaScript Date 对象的函数。欲了解更多详细信息,请到date-fns 主页中阅读 “为什么使用 date-fns?” 一节。

  • 语言环境:可以单独导入的自定义数据文件
  • 时区:Intl 通过单独的库实现

js-Joda

js-JodaJavaThree-Ten BackportJavaScript 版本,该 backport 是根据 Java SE 8 java.time 包中 JSR-310 实现的基础。如果你熟悉java.time,Joda-Time 或 Noda Time,你会发现 js-Joda 具有可比性。

  • 语言环境:通过附加模块的自定义数据文件
  • 时区:通过附加模块的自定义数据文件

不使用任何第三方库

JavaScript 一直有一个 Date 对象,遵循了 ECMAScript(ECMA-262)的规范。

使用 Date 对象时,请注意以下几点:

  • Date 对象内部具有毫秒精度的 Unix 时间戳。它提供了可以在系统本地时区之间来回转换的功能,但是内部始终是 UTC。与 Moment 对象不同,不能将其设置为使用其他时区。它并不存在“模式”的概念。
  • 使用 Date.parsenew Date() 在过去一直存在 bug,且实现不一。当前的规范 支持定义解析 ISO 8601 的字符串,其中只有日期的形式会(如 "2020-09-14")被解析为 UTC,而非 ISO 8601 中的当地时间。即便如此,也不是所有的现代浏览器都会按照这个标准来实现(例如 Safari)。其他类型的字符串也可以使用,但是解析它们是额外实现的,并且可能会有很大的不同,特别是对于旧版本的浏览器来说。实现方式以及传入字符串的不同,可能会产生不同的结果。由于这些原因,我们同意 MDN 的声明,即 强烈反对使用 Date 对象对字符串进行解析

现代 JavaScript 环境也实现了 ECMA-402 的规范,提供了 Intl 对象,并在 Date 对象上定义了toLocaleStringtoLocaleDateString 以及 toLocaleTimeString 等方法。

使用 Intl 对象时,请注意以下几点:

  • 并非每个环境都会实现完整的规范。特别是,Node.js 环境依赖 ICU 提供的国际化支持。有关更多详细信息,请参见 Node.js 文档。
  • ECMAScript Intl 兼容性列表 (由 kangax 提供) ,方便查询支持情况。
  • 多数较新的环境提供了通过 IANA 时区支持 timeZoneIntl.DateTimeFormat 构造函数(以及 Date.toLocaleStringDate.toLocaleDateStringDate.toLocaleTimeString)选项。该选项可用于获取对象的内部基于 UTC 的时间戳和获取已转换为命名时区的字符串。但是,它不能将 Date 对象转换为其他时区。

如果 DateIntl 对象能满足你的需求,并且你完全了解它们的局限性,则可以考虑直接使用它们。

未来

Temporal – 使用 JavaScript 自带时间和日期处理方式会更好

将来,我们希望不再需要使用 JavaScript 相关的日期和时间处理库。而是直接使用 JavaScript 语言本身的特性。尽管今天有 Date 和提供了一些特性的 Intl,但我们从以往的经验和数据来看,它们仍有很大的改进空间。

从 ECMA TC39 的临时提案可以看出,组织正在努力为 JavaScript 语言编写更好的日期和时间 API。目前已处于 TC39 流程的第二阶段。

Temporal 将是一个充当顶级名称空间(如 Math)的新全局对象。它暴露了许多不同的类型的对象,包括 Temporal.AbsoluteTemporal.DateTimeTemporal.DateTemporal.TimeTemporal.TimeZone等。Temporal Cookbook 中包含了许多案例,并举例说明了如何在不同情况下使用这些对象。

你可以通过这个 实验性的 polyfill 来体验它,但还是不要在生产环境中使用它。

如果你有使用 Moment 或其他日期时间处理库的经验,并且对 temporal 提案感兴趣,欢迎参与讨论和开发。

以上就是W3Cschool编程狮关于Moment.js官方推荐使用其它时间处理库代替的相关介绍了,希望对大家有所帮助。

前端算法入门之「数据结构」

thbcm阅读(213)

前端算法入门 — 数据结构

1)什么叫算法?

算法就是计算或解决问题的步骤。

2)算法和程序有什么区别?

区别在于,程序是以计算机能够理解的编程语言编写的,可以在计算机上运行,而算法是以人类能够理解的数学方式来描述的,用于编程之前。但,算法和编程没有具体边界。

3)如何选择算法?

同样的问题,不同的开发者解法不同,不同的编程语言,写法不同,为算法设立评判标准的目的在于选择最优标准的算法。评判算法的优劣有两个标准:一是从运行到计算出结果需要耗费空间的大小,另一个是从运行到计算出结果需要花费多少时间。分别称为, 空间复杂度时间复杂度 。通常,复杂度越低,耗费内存越少,执行越快,也更容易被人理解。一般, 最为重视的是算法的运行时间

4)如何反映算法的运行时间?

算法不同、设备不同、数据量不同都会导致算法时间有差异,通过理论计算出的运行时间是一个多项式,而我们需要能最直观的了解到时间随数据量变化的关系,常常会忽略掉非重要项,得到一个最简单的并且最能反映运行时间趋势的表达式,我们把:忽略掉不重要项、能最简表示运行时间随数据量变化关系写成O(n)的形式,其中O是大写,表示忽略重要项以外的内容,读音同order;n表示参与算法的数据量。

数据结构

为什么要有多种数据结构?

根据使用目的的不同,使用不同的数据类型,可以提供内存空间利用率。

1)链表

链表是数据结构之一,其数据呈线性排列;在内存空间中,数据是分散存储于内存中的,每个数据都由两部分组成,一部分是数据本身,另一部分是一个指针,它指向下一块存储空间。当对数据进行访问时,只能顺着指针指向一一往下访问,直到找到或者访问到末尾,如果链表中的数据量是n,那么,查找到一个数据,最快需要一次,最多需要查找n次;当需要在链表中添加或者删除一个数据时,只需要改变其中某一个或两个的数据指针即可,与链表的数据量无关,是常量级的。

小结:

链表数据是线性的,存储空间是不连续的,访问的时间复杂度为O(n),增删的时间复杂度为O(1)。

拓展:

循环链表:链表尾部的数据是没有指针的,当为尾部数据添加一个指向链表头部数据的指针时,链表指针之间就形成了环形结构,称为循环链表或环形链表。

双向链表:当链表内部指针既可以从前一个数据指向后一个数据同时也可以从后一个元素指向前一个数据时,就形成了双向链表。

这里要注意:在给出定义时就说了,数据由数据体本身和指针共同组成,当指针增加时,数据需要的存储空间也会增加,就会占用更多的内存。同时,当指针越多,增删数据时,需要改变的指针也就越复杂,需要改变指向的指针也越多。

2)数组

数组也是线性排列的数据结构之一,它与链表不同的地方在于,数组在内存空间的存储是连续的。当访问数组时,只需要根据数组索引找到对应位置即可,查找复杂度是常量级的,表示为O(1);而当对数组进行增加时,如果在数组头部增加,则需要先将数组扩容。然后将每一个元素都依次向后移动,这个过程复杂度是O(n),而如果在数组尾部增加一个元素,复杂度变成了O(1),同理,删除一个元素,尾部删除时为O(1),头部删除时为O(n)。可以看出,相比于链表,数组虽然查询方便了,但是操作复杂度却高了。

3)栈

栈是一种线性数据结构,当为栈添加一个元素时,这个元素被添加到了栈的最顶端,当取出元素时,只能单向的从最前面的位置读取,然后才能读取后面的元素,也就是说,最后被添加的,反而是最先被读取的,因此,栈被称为是后进先出(LIFO)模式,添加和删除数据的方式也被称为是入栈和出栈。由于栈具有的LIFO的特点,它常常被用来保存最新的数据。

4)队列

队列也是线性结构的数据结构,它与栈很像,都是单向的有序操作,但是,后进先出,而队列就像排队,先来的排在前面,后来的排在后面,属于先进先出(FIFO),要访问后面的元素,只能把前面的元素都访问完了,才能访问到目标元素。添加和删除队列的操作也被称为入队和出队。

5)哈希表

哈希表存储的是以键值对组合的数据,一般,把键当做数据的标识,而把值当做数据的内容。哈希表通常与哈希函数组合使用,在建立哈希表的过程中,需要使用哈希函数计算数据的哈希值,将其存在数组中,这样在访问时就可以快速使用数组的特性访问到;如果在建立数组的时候存在多个位于同一个数组位置的值,则再次使用链表存储相同的值。

哈希表的使用,加快了数组查询的速度,在灵活性和高效性上有很大的优势。在编程中,关联数组时常常会用到哈希表。

6)堆

堆是图的一种,是二维的数据结构,其示意可以用二位的树状图表示,子节点的数据值总比父节点大。在堆中,顶端的数据始终是最小的,所以无论多少数据量,取出最小值的复杂度始终都是O(1)。另外,由于取出数据后需要将最后的数据移动到最顶端,然后一边比较它与子节点数据的大小,一边往下移动,所以,取出数据需要的运行时间和树的高度成正比,假设数据量为n,根据堆的形状特点可知,树的高度为log2n,那么重构树的复杂度就是O(logn).添加数据也一样,在堆的最后添加数据,数据一边比较它与父节点的大小,一边往上移动,知道满足堆的条件为止。

如果需要频繁的从数据中取出最小值,那么,堆,是一种很好的选择。

7)二叉查找树

二叉查找树也叫作二叉搜索树,或二叉排序树。是二维的图结构的一种。它的特点是:每个节点最多有两个节点,分别称为左子树和右子树,每一个节点上的值均大于其左子树上的值,每个节点上的值均小于其右子树上的值。

根据这两个特点可知: 二叉查找树查找最小值要往左下末端找,查找最大值要往右下末端找 。

数据结构到底选哪种要根据使用目的来确定,前端常用的为以上7种。

以上就是W3Cschool编程狮关于前端算法入门之「数据结构」的相关介绍了,希望对大家有所帮助。

文章来源:www.toutiao.com/i6858491700608762380/

再也不用为多张Excel表汇总发愁了,Python 秒处理真香!

thbcm阅读(279)

文章来源于公众号:Python技术 作者:派森酱

为何非程序员学 Python 的越来越多了?他们可能并不是想去爬取一些网站的数据,而是在工作中碰到很多数据分析处理的问题,用 Python 就可以简单高效地解决。本文就通过一个实际的例子来给大家展示一下 Python 是如何应用于实际工作中高效解决复杂问题的。

背景

小明就职于一家户外运动专营公司,他们公司旗下有好多个品牌,并且涉及到很多细分的行业。小明在这家公司任数据分析师,平时都是通过 Excel 来做数据分析的。今天老板丢给他一个任务:下班前筛选出集团公司旗下最近一年销售额前五名的品牌以及销售额。

对于 Excel 大佬来说,这不就是分分钟的事吗?小明并没有放在眼里,直到市场部的同事将原始的数据文件发给他,他才意识到事情并没有那么简单:

这并不是想象中的排序取前五就行了。这总共有90个文件,按常规的思路来看,他要么将所有文件的内容复制到一张表中进行分类汇总,要么将每张表格进行分类汇总,然后再最最终结果进行分类汇总。

想想这工作量,再想想截止时间,小明挠了挠头,感觉到要渐渐头秃。

思路分析

这种体力活,写程序解决是最轻松的啦。小明这时候想到了他的程序员好朋友小段,于是他把这个问题抛给了小段。

小段缕了下他那所剩无几的头发,说:so easy,只需要找潘大师即可。

小明说:你搞不定吗?还要找其他人!

小段苦笑说:不不不,潘大师是 Python 里面一个处理数据的库,叫 Pandas ,俗称 潘大师。

小明说:我不管什么大师不大师,就说需要多久搞定。

小段说:给我几分钟写程序,再跑几秒钟就好了!

小明发过去了膜拜大佬的表情。

小段略微思考了下,整理了一下程序思路:

  • 计算每张表每一行的销售额,用“访客数 转化率 客单价”就行。
  • 将每张表格根据品牌汇总销售额。
  • 将所有表格的结果汇总成一张总表
  • 在总表中根据品牌汇总销售额并排序

编码

第零步,读取 Excel :

import pandas as pd


df = pd.read_excel("./tables/" + name)

第一步,计算每张表格内的销售额:

df['销售额'] = df['访客数'] * df['转化率'] * df['客单价']

第二步,将每张表格根据品牌汇总销售额:

df_sum = df.groupby('品牌')['销售额'].sum().reset_index()

第三步,将所有表格的结果汇总成一张总表:

result = pd.DataFrame()
result = pd.concat([result, df_sum])

第四步,在总表中根据品牌汇总销售额并排序:

final = result.groupby('品牌')['销售额'].sum().reset_index().sort_values('销售额', ascending=False)

最后,我们来看看完整的程序:

import pandas as pd
import os


result = pd.DataFrame()


for name in os.listdir("./tables"):
    try:
        df = pd.read_excel("./tables/" + name)
        df['销售额'] = df['访客数'] * df['转化率'] * df['客单价']
        df_sum = df.groupby('品牌')['销售额'].sum().reset_index()
        result = pd.concat([result, df_sum])
    except:
        print(name)
        pass


final = result.groupby('品牌')['销售额'].sum().reset_index().sort_values('销售额', ascending=False)
pd.set_option('display.float_format', lambda x: '%.2f' % x)
print(final.head())

最后的结果是这样的:

       品牌           销售额
15   品牌-5 1078060923.62
8   品牌-17 1064495314.96
4   品牌-13 1038560274.21
3   品牌-12 1026115153.00
13   品牌-3 1006908609.07

可以看到最终的前五已经出来了,整个程序运行起来还是很快的。

几分钟之后,小段就把结果给小明发过去了,小明感动得内牛满面,直呼改天请吃饭,拜师学艺!

总结

本文主要是想通过一个实际的案例来向大家展示潘大师(Pandas)的魅力,特别是应用于这种表格处理,可以说是太方便了。写过程序的可能都有点熟悉的感觉,这种处理方式有点类似于 SQL 查询语句。潘大师不仅能使我们的程序处理起来变得更简单高效,对于需要经常处理表格的非程序员也是非常友好的,上手起来也比较简单。

以上就是W3Cschool编程狮关于再也不用为多张Excel表汇总发愁了,Python 秒处理真香!的相关介绍了,希望对大家有所帮助。

联系我们