程序员技术选型:写Go还是Java?

thbcm阅读(204)

文章来源于公众号:架构头条 作者:John Griffin 译者 |:无明

本文作者根据自己的使用体验,详细对比了 Go 和 Java 的使用差异,给了开发者们一个中肯的选用参考。

老实说,我很喜欢 Java。我在 Spiral Scout 工作的那几年,使用了 EJB2、DB2 和 Oracle 等后端技术,积累了很多软件开发方面的专业知识。过去几年,我转向基于自然语言处理的机器人项目,涉及的技术包括 Spring BootRedis、RabbitMQ、Open NLP、IBM Watson 和 UIMA。一直以来,我选择的语言是 Java,它一直很高效,有时还会觉得它很有趣。

1. 初遇 Go

2017 年初,我接手了一个非常有趣的项目,一个用于监控和种植水培植物的自动化编程系统。项目的原始代码里包含了一个使用 CGo 开发的支持三个不同系统(Windows、MacOS 和 ARM)的网关。

因为对 Go 不熟悉,我一边学习,一边用它来实现功能。因为已有代码库的结构非常复杂,对我来说是难上加难。用 Go 开发的支持三种不同操作系统的程序意味着需要针对三种不同的系统进行部署、测试和运行维护。此外,代码采用了单例设计模式,导致系统严重相互依赖,难以预测会出现什么问题,而且难以理解。最后,我选择使用 Java 来实现新版本,但最终也变得非常丑陋和令人困惑。

在加入 Spiral Scout 后,我尝试停止使用 Java。我决定拥抱 Go,并尽可能多地使用 Go 来开发。我发现它是一种创新且全面的语言,我们的团队现在仍然每天在各种项目中使用它。

但是,与任何一门编程语言一样,Go 也有它的缺点,而且我不想撒谎——有时候我真的很想念 Java

如果说我的编程经验教会了我什么,那一定是——软件开发没有银弹。我将在这篇文章里详细分享我使用一门传统语言和一门新语言的经历。

2. 简洁性

GoJava 都是 C 家族语言,所以它们具有相似的语法。因此,Java 开发人员可以很容易读懂 Go 代码,反之亦然。Go 不需要在语句末尾使用分号(’;’),只有少数情况例外。对我来说,Go 的行分隔方式更清晰,更易读。

GoJava 都使用了我最喜欢的功能之一,即垃圾收集器(GC),用来帮助防止内存泄漏。与 C++ 不同,C 家族的程序员需要处理内存泄漏问题。垃圾回收器是自动化内存管理的一个特性,减轻了程序员的负担。

Go 的 GC 并未使用“弱世代假设”,但它的表现仍然非常出色,并且 STW(Stop-the-World)的时间非常短。在 1.5 版中,STW 降得更多,并且很稳定,而在 1.8 版中,它降到了 1 毫秒以下。

Go 的 GC 只有少量的一些选项,即用于设置初始垃圾回收目标百分比的 GOGC 变量。而 Java 有 4 个不同的垃圾回收器,每个垃圾回收器都有大量的选项。

尽管 Java 和 Go 都被认为是跨平台的,但 Java 需要 Java 虚拟机(JVM)来解释编译后的代码,而 Go 是将代码编译成目标平台的二进制文件。但我认为,与 Go 相比,Java 对平台的依赖程度更低,因为 Go 每次都需要为新平台编译二进制文件。从测试和 DevOps 的角度来看,分别为不同的平台编译二进制文件非常耗时,并且跨平台的 Go 编译在某些情况下不起作用,尤其是在使用 CGo 时。而对于 Java,你可以在安装了 JVM 的任何地方使用相同的 jar。Go 需要的 RAM 更小一些,并且不需要安装和管理虚拟机。

反射。Java 的反射更方便、更流行也更常用,而 Go 的反射似乎更复杂。Java 是一种面向对象的编程语言,因此除原始类型之外的所有东西都被视为对象。如果要使用反射,可以创建一个类,并从类中获取所需的信息,如下所示:

Class cls = obj.getClass();
Constructor constructor = cls.getConstructor();
Method[] methods = cls.getDeclaredFields();

这样就可以访问构造函数、方法和属性,然后调用或对它们赋值。

Go 没有类的概念,并且结构体只包含了已声明的字段。因此,我们需要借助“reflection”包来获得所需的信息:

type Foo struct {
  A int `tag1:"First Tag"
  tag2:"Second Tag"`
  B string
}


f := Foo{A: 10, B: "Salutations"}
fType := reflect.TypeOf(f)
switch t.Kind(fType)
    case reflect.Struct:
        for i := 0; i < t.NumField(); i++ {
          f := t.Field(i)
          // ...
        }
}

我觉得这不是一个大问题,但由于 Go 中没有结构体的构造函数,所以很多原始类型必须单独处理,并且需要考虑到指针。在 Go 中,我们可以进行指针传递或值传递。Go 的结构体可以将函数作为字段。所有这些都让 Go 的反射变得更加复杂。

可访问性。Java 有 private、protected 和 public 修饰符,为数据、方法和对象提供了不同的访问作用域。Go 有与 Java 的 public 和 private 相似的 exported/unexported,但没有修饰符。以大写字母开头的所有内容都将被导出,对其他包可见,未导出(小写)的变量或函数仅在当前包中可见。

3. Go 与 Java 的大不同

Go 不是面向对象编程语言。Go 没有类似 Java 的继承机制,因为它没有通过继承实现传统的多态性。实际上,它没有对象,只有结构体。它可以通过接口和让结构体实现接口来模拟一些面向对象特性。此外,你可以在结构体中嵌入结构体,但内部结构体无法访问外部结构体的数据和方法。Go 使用组合而不是继承将一些行为和数据组合在一起。

Go 是一种命令式语言,Java 是一种声明式语言。Go 没有依赖注入,我们需要显式地将所有东西包装在一起。因此,在使用 Go 时尽量少用“魔法”之类的东西。一切代码对于代码评审人员来说都应该是显而易见的。Go 程序员应该了解 Go 代码如何使用内存、文件系统和其他资源。

Java 要求开发人员更多地地关注程序的业务逻辑,知道如何创建、过滤、修改和存储数据。系统底层和数据库方面的东西都是通过配置和注解来完成的(比如通过 Spring Boot 等通用框架)。我们尽可能把枯燥乏味的东西留给框架去做。这样做很方便,但控制也反转了,限制了我们优化整个过程的能力。

变量定义的顺序。在 Java 中,你可以这样定义变量:

String name;

而在 Go 中,你得这么写:

name string

在我刚开始使用 Go 时,这也是令我感到困惑的一个地方。

4. Go 好的方面

简单优雅的并发。Go 具有强大的并发模型,叫作“通信顺序进程”或 CSP。Go 使用 n-to-m 分析器,允许在 n 个系统线程中执行 m 个并发。启动并发例程非常简单,只需使用 Go 的一个关键字即可,例如:

go doMyWork()

这样就可以并发执行 doMyWork()。

进程之间的通信可以通过共享内存(不推荐)和通道来完成。我们可以使用与环境变量 GOMAXPROCS 定义的进程数一样多的核心,并带来非常健壮和流畅的并行性。默认情况下,进程数等于核心数。

Go 提供了一种特殊模式来运行二进制文件,并可以检测执行竟态条件。我们可以通过这种方式测试并证明自己的程序是不是并发安全的。

go run -race myapp.go

应用程序将在竟态检测模式下运行。

Go 提供了很多开箱即用且非常有用的基本功能(https://golang.org/dl/),例如用于并发的“sync”包(https://golang.org/pkg/sync/)。“Once”类型的单例可以这么写

package singleton 
import ("sync")
type singleton struct { }
var instance *singleton 
var once sync.Once 
func GetInstance() *singleton {    
  once.Do(func() {        
    instance = &singleton{}    
  })    
  return instance 
}

sync 包还为并发 map 实现、互斥锁、条件变量和 WaitGroup 提供了一种结构体。atomic 包(https://golang.org/pkg/sync/atomic/) 支持并发安全转换和数学运算——它们基本上是编写并发代码所需的一切。

指针。借助指针,Go 可以更好地控制如何分配内存、垃圾回收器负载以及其他在 Java 中无法实现的性能调优。与 Java 相比,Go 更像是一种低级的语言,并且支持更容易、更快的性能优化。

鸭子类型(Duck Typing)。“如果它走路像鸭子,并且像鸭子一样嘎嘎叫,那它一定就是鸭子”。在 Go 中就是这样的:无需定义某种结构体是否实现了给定的接口,只要这个结构体具有与给定接口相同的方法签名,那它就是实现了这个接口。这非常有用,作为代码库的调用端,你可以定义外部库结构体所需的任意接口。而在 Java 中,对象必须显式声明实现了哪些接口。

性能分析器。Go 的性能分析工具让性能问题分析变得便捷和轻松。Go 的分析器可以揭示程序的内存分配和 CPU 使用情况,并在可视化图形中展示出来,让性能优化变得非常容易。Java 也有很性能分析器,比如 Java VisualVM,但它们都比 Go 的复杂,而且依赖 JVM 的运行情况,因此它们提供的统计信息与垃圾回收器的运行相关。

CGO。Go 可以与 C 语言集成,因此你可以在 Go 项目中开发带有 C 代码片段的应用程序。开发人员可以使用 CGo 创建调用 C 代码的 Go 程序包。Go 为 exclude/include 给定平台的 C 代码片段提供了各种构建选项。

将函数作为参数。Go 函数可以作为变量传递给另一个函数或作为结构体的字段。这种多功能性令人耳目一新。Java 8 引入了 lambda,但它们不是真正的函数,只是单函数对象。

清晰的代码风格准则。Go 社区提供了很多示例和说明:

https://golang.org/doc/effective_go.html

函数可以返回多个参数,这个也非常有用。

package main 
import "fmt" 
func returnMany() (int, string, error) {     
  return 1, "example", nil 
}
func main() {     
  i, s, err := returnMany()     
  fmt.Printf("Returned %s %s %v", i, s, err) 
}

5. Go 不好的方面

没有多态性(除非通过接口来实现)。在 Go 中,如果在同一个包中有两个函数具有不同的参数但含义相同,必须给它们指定不同的名字。例如这段代码:

func makeWorkInt(number int) {   
  fmt.Printf(“Work done number %d”, number) 
}
func makeWorkStr(title string) {   
  fmt.Printf(“Work done title %s”, title) 
}

这样一来,你就会得到很多方法,它们做的事情差不多,但名字都不一样,而且看起来很“丑”。

另外,Go 也没有继承多态性。被嵌入到结构体里的结构体只知道其自己的方法,对“宿主”结构体的方法一无所知。对于像我这样的开发人员来说,这尤其具有挑战性,因为我们是从其他 OOP 语言(最基本的概念之一就是继承)过渡到 Go 的。

不过,随着时间的推移,我开始意识到这种处理多态性的方法只是另一种思维方式,而且是有道理的,因为组合比继承更加可靠,并且运行时间是可变的。

错误处理。在 Go 中,完全由你来决定返回什么错误以及如何返回错误,因此作为开发人员,你需要负责返回和传递错误。毫无疑问的是,错误可能会被隐藏掉,这是一个痛点。时刻要记得检查错误并把它们传递出去,这有点烦人,而且不安全。

当然,你可以使用 linter 来检查隐藏的错误,但这只是一种辅助手段,不是真正的解决方案。在 Java 中,处理异常要方便得多。如果是 RuntimeException,甚至不必将其添加到函数的签名中。

public void causeNullPointerException() {
  throw new NullPointerException("demo");
}
/*
...
*/
try {
  causeNullPointerException();
} catch(NullPointerException e) {
  System.out.println("Caught inside fun().");
  throw e; // rethrowing the exception
}

没有泛型。虽然泛型很方便,但它会增加复杂性,而且从类型系统和运行时方面来看,泛型的成本很高。在构建 Go 代码时,你需要处理各种不同的类型或使用代码生成。

没有注解。尽管可以用代码生成替换一部分编译时注解,但运行时注解是不能替换的。这是有道理的,因为 Go 不是声明式的,并且代码里不应该包含任何“魔法”。我喜欢在 Java 中使用注解,因为它们让代码更优雅、简单和简约。

在为 HTTP 服务器端点生成 swagger 文件时,注解会非常有用。目前在 Go 中需要手动编写 swagger 文件,或者为端点提供特别的注释。每次 API 发生改动时,这都是一件很痛苦的事情。但是,Java 中的注解就像是一种魔法一样,人们通常都不用去关心它们是怎么实现的。

Go 的依赖管理。我之前曾写过一篇关于如何使用 vgo 和 dep 在 Go 中进行依赖管理的文章。Go 的依赖管理的演变之路充满了坎坷。最初,除了“ Gopgk”之外没有其他依赖管理工具,后来发布了实验性的“Vendor”,后被“vgo”取代,然后又被 1.10 版“go mod”取代。如今,我们可以手动或者使用各种 Go 命令(例如“go get”)来修改 go.mod 文件描述符,但这也让依赖关系变得不稳定。

Java 有 Maven 和 Gradle 之类的声明式工具,用来进行依赖关系管理,也用于构建、部署和处理其他 CD/CI 任务。但是,在 Go 中,我们必须使用 Makefile、docker-composes 和 bash 脚本自定义构建所需的依赖管理,这只会使 CD/CI 的过程和稳定性变得更加复杂。

包的名称里包括了托管域名。例如:

import "github.com/pkg/errors"

这真的很奇怪,而且很不方便,因为你不能在不修改项目代码库导入的情况下用自己的实现替换别人的实现。

Java 中,导入通常以公司名称开头,例如:

import by.spirascout.public.examples.simple.Helper;

区别在于,在 Go 中,go get 会向 by.spirascout.public 获取资源。在 Java 中,包名和域名不一定是相关联的。

我希望所有与依赖管理有关的问题都是暂时的,将来会得到妥善的解决。

6. 写在最后

Go 最有趣的一个地方是它所遵循的代码命名规则——基于代码可读性心理学:

https://medium.com/@egonelbre/psychology-of-code-readability-d23b1ff1258a

你可以用各种方法写出清晰且可维护的代码,尽管 Go 是多单词的编程语言,但写出来的代码仍然很清晰。

Go Web 开发经验让我看到了 Go 的快速、强大和易于理解,它非常适用于小型服务和并发处理。对于大型复杂的系统、功能复杂的服务以及单服务器系统,Java 目前仍然是王者。

英文原文

https://dzone.com/articles/when-to-use-go-vs-java-one-programmers-take-on-two

以上就是W3Cschool编程狮关于程序员技术选型:写Go还是Java?的相关介绍了,希望对大家有所帮助。

JavaScript面试经,offer拿到手软

thbcm阅读(201)

文章来源于公众号:猴哥说前端 作者:monkeysoft

本文给大家分享一些 JavaScript 面试经验,在这金九银十的招聘季,希望大家都能找到满意的工作。

JavaScript的数据类型都有什么?

基本数据类型:String,Boolean,Number,Undefined,Null

引用数据类型:Object(Array,Date,RegExp,Function)

javascript中=====的区别是什么?

==会自动进行类型转换,===不会

例举3种强制类型转换和2种隐式类型转换?

强制(parseInt,parseFloat,Number())

隐式(==) 1==”1”//true

null==undefined//true

原生 JS 中 call()、apply()、bind() 方法有什么区别?

三个方法都可以改变函数运行时的 this 指向。

三个方法第一个参数都是函数调用执行时this 指向的对象。

call() 方法第二个参数是个可变参数,是函数调用执行时本身所需要的参数。

apply() 方法第二个参数是数组或arguments。call()与apply()都是立即调用函数执行,在运行时修改this指向。

bind()是返回一个新的函数,新函数的函数主体与原函数的函数主体一致,当新函数被调用时,函数体中 this 指向的是 bind() 方法第一个参数传递的对象,而bind() 方法不会影响原函数本身的 this 指向。

什么是闭包?特点是?

闭包,官方对闭包的解释是:一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。

闭包的特点:

(1)作为一个函数变量的一个引用,当函数返回时,其处于激活状态。

(2) 一个闭包就是当一个函数返回时,一个没有释放资源的栈区。

简单的说,JavaScript 允许使用内部函数—即函数定义和函数表达式位于另一个函数的函数体内。而且,这些内部函数可以访问它们所在的外部函数中声明的所有局部变量、参数和声明的其他内部函数。当其中一个这样的内部函数在包含它们的外部函数之外被调用时,就会形成闭包。

事件委托是什么?

符合W3C标准的事件绑定 addEventLisntener /attachEvent

让利用事件冒泡的原理,让自己的所触发的事件,让他的父元素代替执行!

如何阻止事件冒泡和默认事件

e. stopPropagation();//标准浏览器

event.canceBubble=true;//ie9之前

阻止默认事件:

为了不让a点击之后跳转,我们就要给他的点击事件进行阻止

return false

e.preventDefault();

document load 和document ready的区别?

Document.onload 是在结构和样式加载完才执行js

window.onload:不仅仅要在结构和样式加载完,还要执行完所有的样式、图片这些资源文件,全部加载完才会触发window.onload事件

Document.ready原生种没有这个方法,jquery中有 $().ready(function)

为了保证页面输出安全,我们经常需要对一些特殊的字符进行转义,请写一个函数escapeHtml,将, &, “进行转义

return str.replace(/[”&]/g, function(match) {
    switch (match) {
      case “”:
          return “>”;
      case “&”:
          return “&”;
      case “\””:
         return “"”;
     }
  });
}

简述创建函数的几种方式

第一种(函数声明):function sum1(num1,num2){ return num1+num2; }

第二种(函数表达式):var sum2 = function(num1,num2){ return num1+num2; }

第三种(函数对象方式):var sum3 = new Function(“num1″,”num2″,”return num1+num2”);

把 Script 标签 放在页面的最底部的body封闭之前 和封闭之后有什么区别?浏览器会如何解析它们?

如果说放在body的封闭之前,将会阻塞其他资源的加载

如果放在body封闭之后,不会影响body内元素的加载

iframe的优缺点?

优点:

  1. 解决加载缓慢的第三方内容如图标和广告等的加载问题
  2. Security sandbox
  3. 并行加载脚本

缺点:

  1. iframe会阻塞主页面的Onload事件
  2. 即时内容为空,加载也需要时间
  3. 没有语意

Javascript如何实现继承?

原型链继承,借用构造函数继承,组合继承,寄生式继承,寄生组合继承

请你谈谈Cookie的弊端?

缺点:

1.Cookie 数量和长度的限制。部分浏览器每个 domain 最多只能有50条 cookie,基本所有浏览器中每个 cookie 长度不能超过4KB,否则会被截掉。

2.安全性问题。如果 cookie 被人拦截了,那人就可以取得所有的 session 信息。即使加密也与事无补,因为拦截者并不需要知道 cookie 的意义,他只要原样转发 cookie 就可以达到目的了。

3.有些状态不可能保存在客户端。例如,为了防止重复提交表单,我们需要在服务器端保存一个计数器。如果我们把这个计数器保存在客户端,那么它起不到任何作用。

4.占用网络上传带宽。每次请求服务器资源时,都会携带 cookie 信息向服务器传递。

DOM操作——怎样添加、移除、移动、复制、创建和查找节点?

创建新节点

  • createDocumentFragment() // 创建一个DOM片段
  • createElement() // 创建一个具体的元素
  • createTextNode() // 创建一个文本节点

添加、移除、替换、插入

  • appendChild()
  • removeChild()
  • replaceChild()
  • insertBefore() // 在已有的子节点前插入一个新的子节点

查找

  • getElementsByTagName() // 通过标签名称
  • getElementsByName() // 通过元素的Name属性的值(IE容错能力较强,会得到一个数组,其中包括id等于name值的)
  • getElementById() // 通过元素Id,唯一性

js延迟加载的方式有哪些?

  1. defer和async
  2. 动态创建DOM方式(创建script,插入到DOM中,加载完毕后callBack)
  3. 按需异步载入js

documen.write和 innerHTML 的区别?

document.write 只能重绘整个页面

innerHTML 可以重绘页面的一部分

哪些操作会造成内存泄漏?

内存泄漏指任何对象在您不再拥有或需要它之后仍然存在。垃圾回收器定期扫描对象,并计算引用了每个对象的其他对象的数量。如果一个对象的引用数量为 0(没有其他对象引用过该对象),或对该对象的惟一引用是循环的,那么该对象的内存即可回收。

  1. setTimeout 的第一个参数使用字符串而非函数的话,会引发内存泄漏。
  2. 闭包
  3. 控制台日志
  4. 循环(在两个对象彼此引用且彼此保留时,就会产生一个循环)

判断一个字符串中出现次数最多的字符,统计这个次数?

var str = 'asdfssaaasasasasaa';
var json = {};
for (var i = 0; i < str.length; i++) {
        if(!json[str.charAt(i)]){
                json[str.charAt(i)] = 1;
        }else{
                json[str.charAt(i)]++;
        }
};
var iMax = 0;
var iIndex = '';
for(var i in json){
        if(json[i]>iMax){
                iMax = json[i];
                iIndex = i;
        }
}
alert('出现次数最多的是:'+iIndex+'出现'+iMax+'次');

数组扁平化

数组扁平化是指将一个多维数组变为一个一维数组

const arr = [1, [2, [3, [4, 5]]], 6];
// => [1, 2, 3, 4, 5, 6]

方法一:使用flat()

const res1 = arr.flat(Infinity);

方法二:利用正则

const res2 = JSON.stringify(arr).replace(/\[|\]/g, '').split(',');

但数据类型都会变为字符串

方法三:正则改良版本

const res3 = JSON.parse('[' + JSON.stringify(arr).replace(/\[|\]/g, '') + ']');

方法四:使用reduce

const flatten = arr => {
  return arr.reduce((pre, cur) => {
    return pre.concat(Array.isArray(cur) ? flatten(cur) : cur);
  }, [])
}
const res4 = flatten(arr);

方法五:函数递归

const res5 = [];
const fn = arr => {
  for (let i = 0; i < arr.length; i++) {
    if (Array.isArray(arr[i])) {
      fn(arr[i]);
    } else {
      res5.push(arr[i]);
    }
  }
}
fn(arr);

数组去重

const arr = [1, 1, '1', 17, true, true, false, false, 'true', 'a', {}, {}];
// => [1, '1', 17, true, false, 'true', 'a', {}, {}]

方法一:利用Set

const res1 = Array.from(new Set(arr));

方法二:两层for循环+splice

const unique1 = arr => {
  let len = arr.length;
  for (let i = 0; i < len; i++) {
    for (let j = i + 1; j < len; j++) {
      if (arr[i] === arr[j]) {
        arr.splice(j, 1);
        // 每删除一个树,j--保证j的值经过自加后不变。同时,len--,减少循环次数提升性能
        len--;
        j--;
      }
    }
  }
  return arr;
}

方法三:利用indexOf

const unique2 = arr => {
  const res = [];
  for (let i = 0; i < arr.length; i++) {
    if (res.indexOf(arr[i]) === -1) res.push(arr[i]);
  }
  return res;
}

方法四:利用include

const unique3 = arr => {
  const res = [];
  for (let i = 0; i < arr.length; i++) {
    if (!res.includes(arr[i])) res.push(arr[i]);
  }
  return res;
}

方法五:利用filter

const unique4 = arr => {
  return arr.filter((item, index) => {
    return arr.indexOf(item) === index;
  });
}

方法六:利用Map

const unique5 = arr => {
  const map = new Map();
  const res = [];
  for (let i = 0; i < arr.length; i++) {
    if (!map.has(arr[i])) {
      map.set(arr[i], true)
      res.push(arr[i]);
    }
  }
  return res;
}

类数组转化为数组

类数组是具有length属性,但不具有数组原型上的方法。常见的类数组有arguments、DOM操作方法返回的结果。

方法一:Array.from

Array.from(document.querySelectorAll('div'))

方法二:Array.prototype.slice.call()

Array.prototype.slice.call(document.querySelectorAll('div'))

方法三:扩展运算符

[...document.querySelectorAll('div')]

方法四:利用concat

Array.prototype.concat.apply([], document.querySelectorAll('div'));

debounce(防抖)

触发高频时间后n秒内函数只会执行一次,如果n秒内高频时间再次触发,则重新计算时间。

const debounce = (fn, time) => {
  let timeout = null;
  return function() {
    clearTimeout(timeout)
    timeout = setTimeout(() => {
      fn.apply(this, arguments);
    }, time);
  }
};

防抖常应用于用户进行搜索输入节约请求资源,window触发resize事件时进行防抖只触发一次。

throttle(节流)

高频时间触发,但n秒内只会执行一次,所以节流会稀释函数的执行频率。

const throttle = (fn, time) => {
  let flag = true;
  return function() {
    if (!flag) return;
    flag = false;
    setTimeout(() => {
      fn.apply(this, arguments);
      flag = true;
    }, time);
  }
}

节流常应用于鼠标不断点击触发、监听滚动事件。

函数珂里化

指的是将一个接受多个参数的函数 变为 接受一个参数返回一个函数的固定形式,这样便于再次调用,例如f(1)(2)

经典面试题:实现add(1)(2)(3)(4)=10; 、 add(1)(1,2,3)(2)=9;

function add() {
  const _args = [...arguments];
  function fn() {
    _args.push(...arguments);
    return fn;
  }
  fn.toString = function() {
    return _args.reduce((sum, cur) => sum + cur);
  }
  return fn;
}

深拷贝

递归的完整版本(考虑到了Symbol属性):

const cloneDeep1 = (target, hash = new WeakMap()) => {
  // 对于传入参数处理
  if (typeof target !== 'object' || target === null) {
    return target;
  }
  // 哈希表中存在直接返回
  if (hash.has(target)) return hash.get(target);


  const cloneTarget = Array.isArray(target) ? [] : {};
  hash.set(target, cloneTarget);


  // 针对Symbol属性
  const symKeys = Object.getOwnPropertySymbols(target);
  if (symKeys.length) {
    symKeys.forEach(symKey => {
      if (typeof target[symKey] === 'object' && target[symKey] !== null) {
        cloneTarget[symKey] = cloneDeep1(target[symKey]);
      } else {
        cloneTarget[symKey] = target[symKey];
      }
    })
  }


  for (const i in target) {
    if (Object.prototype.hasOwnProperty.call(target, i)) {
      cloneTarget[i] =
        typeof target[i] === 'object' && target[i] !== null
        ? cloneDeep1(target[i], hash)
        : target[i];
    }
  }
  return cloneTarget;
}

以上就是W3Cschool编程狮关于JavaScript面试经,offer拿到手软的相关介绍了,希望对大家有所帮助。

React 17.0.0-rc.2 版本正式发布,引入了全新的 JSX 转换

thbcm阅读(235)

北京时间 9 月 23 日凌晨,React 团队发布了关于全新 JSX 转换的博客,同时发布了 React 17.0.0-rc.2 版本,新的 JSX 转换不再依赖 React 环境,在转换时会自动引入新的 runtime。

原文链接:reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html

作者:Luna Ruan

译者:QC-L

译文首发于 React 中文文档博客,欢迎关注 https://zh-hans.reactjs.org 或者关注印记中文公众号。

虽然 React 17 并未包含新特性[1],但它将提供一个全新版本的 JSX 转换。本文中,我们将为你描述它是什么以及如何使用。

何为 JSX 转换?

在浏览器中无法直接使用 JSX,所以大多数 React 开发者需依靠 Babel 或 TypeScript将 JSX 代码转换为 JavaScript。许多包含预配置的工具,例如 Create React App 或 Next.js,在其内部也引入了 JSX 转换。

React 17 发布在即,尽管我们想对 JSX 的转换进行改进,但我们不想打破现有的配置。于是我们选择与Babel[2] 合作,为想要升级的开发者提供了一个全新版本的,重构过的 JSX 转换

升级至全新的转换完全是可选的,但升级它会为你带来一些好处:

  • 使用全新的转换,你可以单独使用 JSX 而无需引入 React
  • 根据你的配置,JSX 的编译输出可能会略微改善 bundle 的大小
  • 它将减少你需要学习 React 概念的数量,以备未来之需。

此次升级不会改变 JSX 语法,也并非必须。旧的 JSX 转换将继续工作,没有计划取消对它的支持。

React 17 的 RC 版本[3] 已经引入了对全新 transform 的支持,所以你可以尝试一下!为了让大家更容易使用,在 React 17 正式发布后,我们还计划将其支持 React 16.x,React 15.x 以及 React 0.14x。你可以在下方[4]找到不同环境的升级说明。

接下来,我们来仔细对比新旧转换的区别。

新的转换有何不同?

当你使用 JSX 时,编译器会将其转换为浏览器可以理解的 React 函数调用。旧的 JSX 转换会把 JSX 转换为 React.createElement(...) 调用。

例如,假设源代码如下:

import React from 'react';


function App() {
  return <h1>Hello World</h1>;
}

旧的 JSX 转换会将上述代码变成普通的 JavaScript 代码:

import React from 'react';


function App() {
  return React.createElement('h1', null, 'Hello world');
}

注意

无需改变源码。我们将介绍 JSX 转换如何将你的 JSX 源码变成浏览器可以理解的 JavaScript 代码。

然而,这并不完美:

  • 如果使用 JSX,则需在 React 的环境下,因为 JSX 将被编译成 React.createElement
  • 有一些 React.createElement 无法做到的 性能优化和简化[5]。

为了解决这些问题,React 17 在 React 的 package 中引入了两个新入口,这些入口只会被 Babel 和 TypeScript 等编译器使用。新的 JSX 转换不会将 JSX 转换为 React.createElement,而是自动从 React 的 package 中引入新的入口函数并调用。例如:

function App() {
  return <h1>Hello World</h1>;
}

现在将转换为:

// 由编译器引入(禁止自己引入!)
import {jsx as _jsx} from 'react/jsx-runtime';


function App() {
  return _jsx('h1', { children: 'Hello world' });
}

注意,此时源代码无需引入 React 即可使用 JSX 了!(但仍需引入 React,以便使用 React 提供的 Hook 或其他导出。)

此变化与所有现有 JSX 代码兼容,所以你无需修改组件。如果你对此感兴趣,你可以查看 RFC[6] 了解全新转换工作的具体细节。

注意

react/jsx-runtimereact/jsx-dev-runtime 中的函数只能由编译器转换使用。如果你需要在代码中手动创建元素,你可以继续使用 React.createElement。它将继续工作,不会消失。

如何升级至新的转换

如果你还没准备好升级为全新的 JSX 转换,或者你正在为其他库使用 JSX,请不要担心,旧的转换不会被移除,并将继续支持。

如果你想升级,你需要准备两件事:

  • 支持新转换的 React 版本(目前,只有 React 17 的 RC 版本[7] 支持它,但是 React 17.0 发布后,我们计划针对 0.14.x、15.x 以及 16.x 做兼容。)
  • 一个兼容新转换的编译器(请看下面关于不同工具的说明)。

由于新的 JSX 转换不依赖 React 环境,我们准备了一个自动脚本[8],用于移除你代码中不必要的引入。

Create React App

Create React App 已对其做兼容支持[9],并将在 即将发布的 v4.0 版本[10]中提供,该版本处于测试阶段。

Next.js

Next.js 的 v9.5.3[11]+ 会使用新的转换来兼容 React 版本。

Gatsby

Gatsby 的 v2.24.5[12]+ 会使用新的转换来兼容 React 版本。

注意

如果你在 Gatsby 中遇到 error[13],请升级至 17.0.0-rc.2,运行 npm update 解决此问题。

手动配置 Babel

Babel 的 v7.9.0[14] 及以上版本可支持全新的 JSX 转换。

首先,你需要更新至最新版本的 Babel 和 transform 插件。

如果你使用的是 @babel/plugin-transform-react-jsx

# npm 用户
npm update @babel/core @babel/plugin-transform-react-jsx
# yarn 用户
yarn upgrade @babel/core @babel/plugin-transform-react-jsx

如果你使用的是 @babel/preset-react

# npm 用户
npm update @babel/core @babel/preset-react
# yarn 用户
yarn upgrade @babel/core @babel/preset-react

目前,旧的转换的默认选项为("runtime": "classic")。如需启用新的转换,你可以使用 {"runtime": "automatic"} 作为 @babel/plugin-transform-react-jsx@babel/preset-react 的选项:

// 如果你使用的是 @babel/preset-react
{
  "presets": [
    ["@babel/preset-react", {
      "runtime": "automatic"
    }]
  ]
}
// 如果你使用的是 @babel/plugin-transform-react-jsx
{
  "plugins": [
    ["@babel/plugin-transform-react-jsx", {
      "runtime": "automatic"
    }]
  ]
}

从 Babel 8 开始,"automatic" 会将两个插件默认集成在 rumtime 中。欲了解更多信息,请查阅 Babel 文档中的 @babel/plugin-transform-react-jsx[15] 以及 @babel/preset-react[16]。

注意

如果你在使用 JSX 时,使用 React 以外的库,你可以使用 importSource 选项[17]从该库中引入 — 前提是它提供了必要的入口。或者你可以继续使用经典的转换,它会继续被支持。

ESLint

如果你正在使用 eslint-plugin-react[18],其中的 react/jsx-uses-reactreact/react-in-jsx-scope 规则将不再需要,可以关闭它们或者删除。

{
  // ...
  "rules": {
    // ...
    "react/jsx-uses-react": "off",
    "react/react-in-jsx-scope": "off"
  }
}

TypeScript

TypeScript 将在 v4.1 beta[19] 版本中支持新的 JSX 转换。

Flow

Flow 将在 v0.126.0[20] 中支持新的 JSX 转换。

移除未使用的 React 引入

因为新的 JSX 转换会自动引入必要的 react/jsx-runtime 函数,因此当你使用 JSX 时,将无需再引入 React。将可能会导致你代码中有未使用到的 React 引入。保留它们也无伤大雅,但如果你想删除它们,我们建议运行 “codemod”[21] 脚本来自动删除它们:

cd your_project
npx react-codemod update-react-imports

注意:

如果你在运行 codemod 时出现错误,请尝试使用 npx react-codemod update-react-imports选择不同的 JavaScript 环境。尤其是选择 “JavaScript with Flow” 时,即使你未使用 Flow,也可以选择它,因为它比 JavaScript 支持更新的语法。如果遇到问题,请告知我们[22]。

请注意,codemod 的输出可能与你的代码风格并不匹配,因此你可能需要在 codemod 完成后执行 Prettier[23] 以保证格式一致。

运行 codemod 会执行如下操作:

  • 升级到新的 JSX 转换,删除所有未使用的 React 引入。
  • 改变所有 React 的默认引入将被改为解构命名引入(例如,import React from "react" 会变成 import { useState } from "react"),这将成为未来开发的首选风格。codemod 不会 影响现有的命名空间引入方式(即 import * as React from "react"),这也是一种有效的风格,默认的引入将在 React 17 中继续工作,但从长远来看,我们建议尽量不使用它们。

示例:

import React from 'react';


function App() {
  return <h1>Hello World</h1>;
}

将被替换为

function App() {
  return <h1>Hello World</h1>;
}

如果你使用了 React 的其他导出 — 比如 Hook,那么 codemod 将把它们转换为具名导入。

示例:

import React from 'react';


function App() {
  const [text, useText] = React.useState('Hello World');
  return <h1>{text}</h1>;
}

会被替换为

import { useState } from 'react';


function App() {
  const [text, useText] = useState('Hello World');
  return <h1>{text}</h1>;
}

除了清理未使用的引入外,此工具还可帮你为未来 React 主要版本(不是 React 17 版本)做铺垫,该版本将支持 ES 模块,并且没有默认导出。

鸣谢

我们要感谢 Babel,TypeScript,Create React App,Next.js,Gatsby,ESLint 以及 Flow 的主要维护者为新 JSX 转换提供的实现和整合。我们还要感谢 React 社区对相关 RFC[24] 提供的反馈和讨论。

参考资料

[1]并未包含新特性: /blog/2020/08/10/react-v17-rc.html

[2]Babel: https://babeljs.io/blog/2020/03/16/7.9.0#a-new-jsx-transform-11154httpsgithubcombabelbabelpull11154

[3]React 17 的 RC 版本: /blog/2020/08/10/react-v17-rc.html

[4]下方: #how-to-upgrade-to-the-new-jsx-transform

[5]性能优化和简化: https://github.com/reactjs/rfcs/blob/createlement-rfc/text/0000-create-element-changes.md#motivation

[6]RFC: https://github.com/reactjs/rfcs/blob/createlement-rfc/text/0000-create-element-changes.md#detailed-design

[7]React 17 的 RC 版本: /blog/2020/08/10/react-v17-rc.html

[8]我们准备了一个自动脚本: #removing-unused-react-imports

[9]对其做兼容支持: https://github.com/facebook/create-react-app/pull/9645

[10]即将发布的 v4.0 版本: https://gist.github.com/iansu/282dbe3d722bd7231fa3224c0f403fa1

[11]v9.5.3: https://github.com/vercel/next.js/releases/tag/v9.5.3

[12]v2.24.5: https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby/CHANGELOG.md#22452-2020-08-28

[13]Gatsby 中遇到 error: https://github.com/gatsbyjs/gatsby/issues/26979

[14]v7.9.0: https://babeljs.io/blog/2020/03/16/7.9.0

[15]@babel/plugin-transform-react-jsx: https://babeljs.io/docs/en/babel-plugin-transform-react-jsx

[16]@babel/preset-react: https://babeljs.io/docs/en/babel-preset-react

[17]importSource 选项: https://babeljs.io/docs/en/babel-preset-react#importsource

[18]eslint-plugin-react: https://github.com/yannickcr/eslint-plugin-react

[19]v4.1 beta: https://devblogs.microsoft.com/typescript/announcing-typescript-4-1-beta/#jsx-factories

[20]v0.126.0: https://github.com/facebook/flow/releases/tag/v0.126.0

[21]“codemod”: medium.com/@cpojer/effective-javascript-codemods-5a6686bb46fb

[22]告知我们: https://github.com/reactjs/react-codemod/issues

[23]Prettier: https://prettier.io/

[24]RFC: https://github.com/reactjs/rfcs/pull/107

以上就是W3Cschool编程狮关于React 17.0.0-rc.2 版本正式发布,引入了全新的 JSX 转换的相关介绍了,希望对大家有所帮助。

安利几个JS开发小技巧,让你的代码更简洁

thbcm阅读(225)

文章来源于公众号:前端开发社区 ,作者:炮哥

本文给大家安利几个JS开发小技巧,可能有些人已经知道了,但是对于新手来说还是蛮有用的,它可以让你的代码更简洁。

1 转换布尔值

除了常规的布尔值truefalse之外,JavaScript 还将所有其他值视为 ‘truthy’ 或‘falsy’

除非另有定义,否则 JavaScript 中的所有值都是’truthy’,除了0“”nullundefinedNaN,当然还有false,这些都是‘falsy’

我们可以通过使用负算运算符轻松地在truefalse之间切换。它也会将类型转换为“boolean”。

const isTrue  = !0;
const isFalse = !1;
const alsoFalse = !!0;
console.log(isTrue); // Result: true
console.log(typeof true); // Result: "boolean"

2 转换数字

使用加法运算符+可以快速实现相反的效果。

let int = "15";
int = +int;
console.log(int); // Result: 15
console.log(typeof int); Result: "number"

这也可以用于将布尔值转换为数字,如下所示

 console.log(+true);  // Return: 1 
 console.log(+false); // Return: 0

在某些上下文中,+将被解释为连接操作符,而不是加法操作符。当这种情况发生时(你希望返回一个整数,而不是浮点数),您可以使用两个波浪号:~~

连续使用两个波浪有效地否定了操作,因为— ( — n — 1) — 1 = n + 1 — 1 = n。换句话说,~—16 等于15

const int = ~~"15"
console.log(int); // Result: 15
console.log(typeof int); Result: "number"

虽然我想不出很多用例,但是按位NOT运算符也可以用在布尔值上:~true = \-2~false = \-1

3转换字符串

要快速地将数字转换为字符串,我们可以使用连接运算符+后跟一组空引号""

const val = 1 + "";
console.log(val); // Result: "1"
console.log(typeof val); // Result: "string"

4浮点数转整数

如果希望将浮点数转换为整数,可以使用Math.floor()Math.ceil()Math.round()。但是还有一种更快的方法可以使用|(位或运算符)将浮点数截断为整数。

console.log(23.9 | 0);  // Result: 23
console.log(-23.9 | 0); // Result: -23

|的行为取决于处理的是正数还是负数,所以最好只在确定的情况下使用这个快捷方式。

如果n为正,则n | 0有效地向下舍入。如果n为负数,则有效地向上舍入。更准确地说,此操作将删除小数点后面的任何内容,将浮点数截断为整数。

你可以使用~~来获得相同的舍入效果,如上所述,实际上任何位操作符都会强制浮点数为整数。这些特殊操作之所以有效,是因为一旦强制为整数,值就保持不变。

删除最后一个数字

按位或运算符还可以用于从整数的末尾删除任意数量的数字。这意味着我们不需要使用这样的代码来在类型之间进行转换。

let str = "1553";
Number(str.substring(0, str.length - 1));

相反,按位或运算符可以这样写:

console.log(1553 / 10   | 0)  // Result: 155
console.log(1553 / 100  | 0)  // Result: 15
console.log(1553 / 1000 | 0)  // Result: 1

5格式化JSON

最后,你之前可能已经使用过JSON.stringify,但是您是否意识到它还可以帮助你缩进JSON?

stringify()方法有两个可选参数:一个replacer函数,可用于过滤显示的JSON和一个空格值。

console.log(JSON.stringify({ alpha: 'A', beta: 'B' }, null, '\t'));
// Result:
// '{
//     "alpha": A,
//     "beta": B
// }'

6取数组最后一项

数组方法slice()可以接受负整数,如果提供它,它将接受数组末尾的值,而不是数组开头的值。

let array = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
console.log(array.slice(-1)); // Result: [9]
console.log(array.slice(-2)); // Result: [8, 9]
console.log(array.slice(-3)); // Result: [7, 8, 9]

7es6数组去重

Set对象类型是在ES6中引入的,配合展开操作…一起,我们可以使用它来创建一个新数组,该数组只有唯一的值。

const array = [1, 1, 2, 3, 5, 5, 1]
const uniqueArray = [...new Set(array)];
console.log(uniqueArray); // Result: [1, 2, 3, 5]

在ES6之前,隔离惟一值将涉及比这多得多的代码。

此技巧适用于包含基本类型的数组:undefinednullbooleanstringnumber。(如果你有一个包含对象,函数或其他数组的数组,你需要一个不同的方法!)

8更优雅的运算

从ES7开始,可以使用指数运算符**作为幂的简写,这比编写Math.pow(2, 3) 更快。这是很简单的东西,但它之所以出现在列表中,是因为没有多少教程更新过这个操作符。

console.log(2 ** 3); // Result: 8

这不应该与通常用于表示指数的^符号相混淆,但在JavaScript中它是按位异或运算符。

在ES7之前,只有以2为基数的幂才存在简写,使用按位左移操作符

JDK 竟然是这样实现栈的?

thbcm阅读(242)

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

前面的文章《动图演示:手撸堆栈的两种实现方法!》我们用数组和链表来实现了自定义的栈结构,那在 JDK 中官方是如何实现栈的呢?接下来我们一起来看。

这正式开始之前,先给大家再解释一下「堆栈」一词的含义,因为之前有读者对这个词有一定的疑惑。

Stack 翻译为中文是堆栈的意思,但为了能和 Heap(堆)区分开,因此我们一般将 Stack 简称为栈。因此当“堆栈”连在一起时有可能表示的是 Stack,而当“堆、栈”中间有分号时,则表示 Heap(堆)和 Stack(栈),如下图所示:

JDK 栈的实现

聊会正题,接下来我们来看 JDK 中是如何实现栈的?

在 JDK 中,栈的实现类是 Stack,它的继承关系如下图所示:

Stack 包含的方法如下图所示:

其中最重要的方法有:

  • push:入栈方法(添加数据);
  • pop:出栈并返回当前元素(移除数据);
  • peek:查询栈顶元素。

Stack 实现源码如下:

public class Stack extends Vector {
    /**
     * 创建一个空栈
     */
    public Stack() {
    }


    /**
     * 入栈方法,调用的是 Vector#addElement 的添加方法
     */
    public E push(E item) {
        addElement(item);
        return item;
    }


    /**
     * 出栈并返回当前元素,调用的是 Vector#removeElementAt 的移除元素方法
     */
    public synchronized E pop() {
        E       obj; // 返回当前要移除的栈顶元素信息
        int     len = size();
        obj = peek(); // 查询当前栈顶元素
        removeElementAt(len - 1); // 移除栈顶元素
        return obj;
    }


    /**
     * 查询栈顶元素,调用 Vector#elementAt 的查询方法
     */
    public synchronized E peek() {
        int     len = size(); // 查询当前栈的长度
        if (len == 0) // 如果为空栈,直接抛出异常
            throw new EmptyStackException();
        return elementAt(len - 1); // 查询栈顶元素的信息
    }


    /**
     * 判断栈是否为空
     */
    public boolean empty() {
        return size() == 0;
    }
    // 忽略其他方法...
}

从上述源码可以看出, Stack 中的核心方法中都调用了父类 Vector 类中的方法,Vector 类的核心源码:

public class Vector
    extends AbstractList
    implements List, RandomAccess, Cloneable, java.io.Serializable
{
 protected Object[] elementData; // 存储数据的容器
    protected int elementCount; // 存储数据的容量值

    
    /**
     * 添加数据
     */
    public synchronized void addElement(E obj) {
        modCount++; // 统计容器被更改的参数
        ensureCapacityHelper(elementCount + 1); // 确认容器大小,如果容量超出则进行扩容
        elementData[elementCount++] = obj; // 将数据存储到数组
    }

    
    /**
     * 移除元素(根据下标移除)
     */
    public synchronized void removeElementAt(int index) {
        modCount++; // 统计容器被更改的参数
        // 数据正确性效验
        if (index >= elementCount) {
            throw new ArrayIndexOutOfBoundsException(index + " >= " +
                                                     elementCount);
        }
        else if (index < 0) {
            throw new ArrayIndexOutOfBoundsException(index);
        }
        int j = elementCount - index - 1;
        if (j > 0) { // 删除的不是最后一个元素
         // 把删除元素之后的所有元素往前移动
            System.arraycopy(elementData, index + 1, elementData, index, j);
        }
        elementCount--; // 数组容量 -1
        elementData[elementCount] = null; // 将末尾的元素赋值为 null(删除尾部元素)
    }

    
    /**
     * 查询元素(根据下标)
     */
 public synchronized E elementAt(int index) {
     // 安全性验证
        if (index >= elementCount) {
            throw new ArrayIndexOutOfBoundsException(index + " >= " + elementCount);
        }
        // 根据下标返回数组中的元素
        return elementData(index);
    }
    // 忽略其他方法...
}

对于上述源码中,可以最不好理解的就是 System#arraycopy 这个方法,它的作用其实就是将删除的元素(非末尾元素)的后续元素依次往前移动的,比如以下代码:

Object[] elementData = {"Java", "Hello", "world", "JDK", "JRE"};
int index = 3;
int j = elementData.length - index - 1;
System.arraycopy(elementData, index + 1, elementData, index, j);
//  System.arraycopy(elementData, 4, elementData, 3, 1);
System.out.println(Arrays.toString(elementData));

它的运行结果是:

[Java, Hello, world, JRE, JRE]

也就是说当我们要删除下标为 3 的元素时,需要把 3 以后的元素往前移动,所以数组的值就从 {"Java", "Hello", "world", "JDK", "JRE"} 变为了 [Java, Hello, world, JRE, JRE],最后我们只需要把尾部元素删除掉,就可以实现数组中删除非末尾元素的功能了。

小结

通过以上源码可以得知,JDK 中的栈(Stack)也是通过物理结构数组实现的,我们通过操作物理数组来实现逻辑结构栈的功能,关于物理结构和逻辑结构详见《动图演示:手撸堆栈的两种实现方法!》

栈的应用

经过前面的学习我们对栈已经有了一定的了解了,那栈在我们的平常工作中有哪些应用呢?接下里我们一起来看。

浏览器回退

栈的特性为 LIFO(Last In First Out,LIFO)后进先出,因此借助此特性就可以实现浏览器的回退功能,如下图所示:

函数调用栈

栈在程序中最经典的一个应用就是函数调用栈了(或叫方法调用栈),比如操作系统给每个线程分配了一块独立的内存空间,这块内存被组织成“栈”这种结构, 用来存储函数调用时的临时变量。每进入一个函数,就会将临时变量作为一个栈帧入栈,当被调用函数执行完成,返回之后,将这个函数对应的栈帧出栈。为了让你更好地理解,我们一块来看下这段代码的执行过程。

int main() {
   int a = 1; 
   int ret = 0;
   int res = 0;
   ret = add(3, 5);
   res = a + ret;
   System.out.println(res);
   reuturn 0;
}
int add(int x, int y) {
   int sum = 0;
   sum = x + y;
   return sum;
}

从代码中我们可以看出, main() 函数调用了 add() 函数,获取计算结果,并且与临时变量 a 相加,最后打印 res 的值。为了让你清晰地看到这个过程对应的函数栈里出栈、入栈的操作,我画了一张图。图中显示的是,在执行到 add() 函数时,函数调用栈的情况。

栈的复杂度

复杂度分为两个维度:

  • 时间维度:是指执行当前算法所消耗的时间,我们通常用「时间复杂度」来描述;
  • 空间维度:是指执行当前算法需要占用多少内存空间,我们通常用「空间复杂度」来描述。

这两种复杂度都是用大 O 表示法来表示的,比如以下代码:

int[] arr = {1, 2, 3, 4};
for (int i = 0; i < arr.length; i++) {
    System.out.println(i);
}

用大 O 表示法来表示的话,它的时间复杂度就是 O(n),而如下代码的时间复杂度却为 O(1):

int[] arr = {1, 2, 3, 4};
System.out.println(arr[0]); // 通过下标获取元素

因此如果使用大 O 表示法来表示栈的复杂度的话,结果如下所示:

以上就是W3Cschool编程狮关于JDK 竟然是这样实现栈的?的相关介绍了,希望对大家有所帮助。

第一次部署 Kubernetes 应用,容易忽略的细节

thbcm阅读(220)

文章来源于公众号:架构头条 作者:Julian Gindi

根据笔者的个人经验,大部分人好像喜欢通过 Helm 或者手动方式将应用程序甩给 Kubernetes,然后就可以每天坐等轻松调用的美好生活。但在 GumGum 公司的实践当中,我们体会到 Kubernetes 应用的一系列“陷阱”,也希望把这些陷阱与大家分享,给您的 Kubernetes 探索之旅带来一点启发。

1. 配置 Pod 请求与限制

我们从配置一套可以运行 Pod 的简单环境开始。Kubernetes 在处理 Pod 调度与故障状态方面确实表现出色,但我们也意识到,如果 Kubernetes 调度程序无法衡量 Pod 的成功运行究竟需要多少资源,那么有时候部署工作可能面临挑战。而这一挑战,也正是资源请求与限制机制的设计根源。目前,设置应用程序请求与限制方面的最佳实践仍然存在不少争议。实际上,这项工作更像是一门艺术,而非单纯的科学。下面,我们聊聊 GumGum 公司内部对这个问题的看法:

Pod 请求: 这是调度程序用于衡量 Pod 最佳部署方法的主要指标。

下面来看 Kubernetes 说明文档中的相关描述:

过滤步骤会在可行的情况下找到一组 Pod。例如,PodFitsResources 过滤器会检查候选节点是否具备充足的可用资源,以满足 Pod 提出的特定资源请求。

在内部,我们通过这样一种方式使用应用程序请求:通过设置,我们对应用程序正常运行实际工作负载时的资源需求做出估计。以此为基础,调度程序即可更合理地放置节点。最初,我们希望将请求设置得更高一些,保证各个 Pod 都拥有充足的资源。但我们很快发现,这种方式会大大增加调度时间,并导致部分 Pod 无法完全调度。这样的结果实际上与我们完全不指定资源请求时看到的情况类似:在后一种情况下,由于控制平面并不清楚应用程序需要多少资源,因此调度程序经常会“逐出”Pod 且不再重新加以调度。正是这一调度算法中的关键组成部分,导致我们无法得到符合预期的调度效果。

Pod 限制: 即对于 Pod 的直接限制,代表着集群允许各容器所使用的最大资源量。

同样来看官方说明文档中的描述:

如果您为容器设置了 4GiB 的内存限制,则 kubelet(与容器运行时)将强制执行此限制。运行时将防止容器使用超出所配置上限的资源容量。例如,当容器中的进程所消耗的内存量超过获准数量时,系统内核将终止该资源分配尝试,并提示内存不足(OOM)错误。

容器所使用的实际资源量可以高于其请求,但永远不能高于配置上限。很明显,对限制指标的正确设置相当困难,但也非常重要。在理想情况下,我们希望让 Pod 的资源需求在整个流程生命周期内发生变化,而又不致干扰到系统上的其他流程——这也正是限制机制的意义所在。遗憾的是,我们无法明确给出最合适的设置值,只能遵循以下过程进行调整:

  1. 使用负载测试工具,我们可以模拟基准流量水平,并观察 Pod 的资源使用情况(包括内存与 CPU)。
  2. 我们将 Pod 请求设置在极低水平,同时将 Pod 资源限制保持在请求值的约 5 倍,而后观察其行为。当请求过低时,进程将无法启动,并时常引发神秘的 Go 运行时错误。

这里需要强调的一点在于,资源限制越严格,Pod 的调度难度也就越大。这是因为 Pod 调度要求目标节点拥有充足的资源。例如,如果您的资源非常有限(内存只有 4GB),那么即使是运行轻量级 Web 服务器进程都很可能非常困难。在这种情况下,大家需要进行横向扩展,而且各个新容器也应运行在同样拥有至少 4GB 可用内存的节点之上。如果不存在这样的节点,您需要在集群中引入新节点以处理该 Pod,这无疑会令启动时间有所增加。总之,请务必在资源请求与限制之间找到最小“边界”,保证快速、平衡实现扩展。

2. 配置 Liveness 与 Readiness 探针

Kubernetes 社区中经常讨论的另一个有趣话题,就是如何配置 Linvess 与 Readiness 探针。合理使用这两种探针,能够为我们带来一种运行容错软件、并最大程度减少停机时间的机制。但如果配置不正确,它们也可能对应用程序造成严重的性能影响。下面来看这两种探针的基本情况,以及如何进行使用判断:

Liveness 探针:“用于指示容器是否正在运行。如果 Liveness 探针失败,则 kubelet 将关闭容器,且容器将开始执行重新启动策略。如果容器并不提供 Liveness 探针,则其默认状态被视为成功。”—Kubernetes说明文档

Liveness 探针的资源需求必须很低,因为它们需要频繁运行,并需要在应用程序运行时向 Kubernetes 发出通知。请注意,如果将其设置为每秒运行一次,则系统将需要承担每秒 1 次的额外请求处理量。因此,请务必认真考虑如何处理这些额外请求及相应资源。在 GumGum,我们将 Liveness 探针设置为在应用程序主组件运行时进行响应,且不考虑数据是否已经完全可用(例如来自远程数据库或缓存的数据)。举例来说,我们会在应用当中设置一个特定的“health”端点,单纯负责返回 200 响应代码。只要仍在返回响应,就表明该进程已经启动并可以处理请求(但尚未正式产生流量)。

Readiness 探针:“指示容器是否准备好处理请求。如果 Readiness 探针失败,则端点控制器将从与该 Pod 相匹配的所有服务端点中,删除该 Pod 的 IP 地址。”

Readiness 探针的运行成本要高得多,因为其作用在于持续告知后端,整个应用程序正处于运行状态且准备好接收请求。关于此探针是否应该访问数据库,社区中存在诸多争论。考虑到 Readiness 探针造成的开销(需要经常运行,但频繁可以灵活调整),我们决定在某些应用程序中只在从数据库返回记录后,才开始“提供流量”。通过对 Readiness 探针的精心设计,我们已经能够实现更高的可用性水平以及零停机时间部署。

但如果大家确实有必要通过应用程序的 Readiness 探针随时检查数据库请求的就绪状态,请尽可能控制查询操作的资源用量,例如……

SELECT small_item FROM table LIMIT 1

以下,是我们在 Kubernetes 中为这两种探针指定的配置值:

livenessProbe:
httpGet:
path: /api/liveness
port: http
readinessProbe:
httpGet:
path: /api/readiness
port: http  periodSeconds: 2

您还可以添加其他一些配置选项:

  • initialDelaySeconds- 容器启动的多少秒后,探针开始实际运行
  • periodSeconds- 两次探测之间的等待间隔
  • timeoutSeconds- 需要经过多少秒,才能判定某一 Pod 处于故障状态。相当于传统意义上的超时指标
  • failureThreshold- 探针失败多少次后,才向 Pod 发出重启信号
  • successThreshold- 探针成功多少次后,才能判定 Pod 进入就绪状态(通常使用在 Pod 启动或者故障恢复之后)

3. 设置默认 Pod 网络策略

Kubernetes 使用一种“扁平”网络拓扑;在默认情况下,所有 Pod 之间都可以直接相互通信。但结合实际用例,这种通信能力往往不必要甚至不可接受。由此带来的一大潜在安全隐患在于,如果某一易受攻击的应用程序遭到利用,则攻击者即可由此获取完全访问权限,进而将流量发送至网络上的所有 Pod 当中。因此我们也有必要在 Pod 网络中应用最低访问原则,在理想情况下通过网络策略明确指定哪些容器之间允许建立相互连接。

以下列简单策略为例,可以看到其将拒绝特定命名空间中的所有入口流量:

---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
spec:
podSelector: {}
policyTypes:
- Ingress

4. 通过 Hooks 与 Init 容器执行自定义行为

我们希望在 Kubernetes 系统中实现的核心目标之一,在于尝试为现有开发人员提供近乎零停机时间的部署支持。但不同应用程序往往拥有不同的关闭方式与资源清理过程,因此整体零停机目标很难实现。首先横亘在我们面前的,就是 Nginx 这道难关。我们注意到在启动 Pod 的滚动部署时,活动连接在成功终止之前就会被丢弃。经过广泛的在线研究,事实证明 Kubernetes 在终止 Pod 之前,并不会等待 Nginx 用尽其连接资源。使用预停止 hook,我们得以注入此项功能,并由此实现了零停机时间。

lifecycle:
preStop:
exec:
command: ["/usr/local/bin/nginx-killer.sh"]

isnginx-killer.sh:

#!/bin/bashsleep 3
PID=$(cat /run/nginx.pid)
nginx -s quitwhile [ -d /proc/$PID ]; do
echo "Waiting while shutting down nginx..."
sleep 10
done

另一个实用范例,是通过 Init 容器处理特定应用程序的启动任务。部分高人气 Kubernetes 项目还会使用 Istio 等 init-containers 将 Envoy 处理代码注入 Pod 当中。如果您在应用程序启动之前,需要首先完成繁重的数据库迁移过程,那么 Init 容器特别适用。您也可以为此过程设定更高的资源上限,保证其不受主应用程序的限制设定影响。

另一种常见模式是向 init-conatiner 提供 secrets 访问权,并由该容器将这些凭证公布给主 Pod,从而防止通过主应用 Pod 本体对 secret 发出示授权访问。同样来看说明文档中的表述:

Init 容器能够安全运行实用程序或自定义代码,避免其破坏应用程序容器镜像的安全性。通过剥离这些不必要的工具,您可以限制应用程序容器镜像的攻击面。

5. 内核调优

最后,我们来聊聊一项最先进的技术。Kubernetes 本身是一套高度灵活的平台,可帮助您以最适合的方式运行工作负载。在 GumGum,我们拥有多种高性能应用程序,其对运行资源有着极为苛刻的要求。在进行了广泛的负载测试之后,我们发现有某一款应用程序难以在使用 Kubernetes 默认设置的前提下处理必要的流量负载。但 Kubernetes 允许我们运行一个高权限容器,通过修改为其配置适用于特定 Pod 的内核运行参数。通过以下示例代码,我们修改了 Pod 中的最大开启连接数量:

initContainers:
- name: sysctl
image: alpine:3.10
securityContext:
privileged: true
command: ['sh', '-c', "sysctl -w net.core.somaxconn=32768"]

这是一种使用频率较低的高级技术。如果您的应用程序难以在高负载场景下健康运行,大家可能需要调整其中的部分参数。这里建议各位在官方说明文档中参阅参数调优与可选值的相关细节信息。

6. 总结

虽然 Kubernetes 已经算是一种几乎“开箱即用”的解决方案,但大家仍然需要采取一系列关键步骤以保证应用程序的平衡运行。在将应用程序迁移至 Kubernetes 之上的整个过程中,请务必重视负载测试“循环”——运行应用程序,对其进行负载测试,观察指标与扩展行为,基于结果调整您的配置,而后重复。请尽量客观地设定预期流量,并尝试将流量增加至超限水平,借此查看哪些组件会最先陷入瘫痪。通过这种迭代方法,大家也许只需要采取本文中介绍的部分步骤即可获得理想的应用程序运行效果。总之,请永远关注以下几个核心问题:

  • 我的应用程序的资源占用量是多少?占用量会如何变化?
  • 服务的实际扩展要求是什么?预计需要处理怎样的平均流量?峰值流量处于怎样的水平?
  • 服务可能多久需要进行一次横向扩展?新的 Pod 要过多久才能正式开始接收流量?
  • 我们的 Pod 终止过程优雅可控吗?是否需要这种优雅性与可控性?我们能否实现零停机时间部署?
  • 该如何尽可能降低安全风险,并限制 Pod 入侵状况的“爆炸半径”(影响范围)?服务中是否存在某些不必要的权限或访问能力?

Kubernetes 是一套令人印象深刻的强大平台,您可以在这里运用最佳实践为整个集群部署数千项服务。但不同的软件之间总是有所差别,有时候您的应用程序可能需要进一步调整,好在 Kubernetes 为我们提供不少调整“旋钮”,尽可能让用户轻松达成与预期相符的技术目标。将资源请求与限制、Livenss 与 Readiness 检查、init-containers、网络策略以及自定义内核调优等方法相结合,相信大家能够在 Kubernetes 平台之上实现更出色的基准性能、弹性与快速规模扩展能力。

以上就是W3Cschool编程狮关于第一次部署 Kubernetes 应用,容易忽略的细节的相关介绍了,希望对大家有所帮助。

总感觉自己不会的很多,又不知道从何下手,资深前端带你破局

thbcm阅读(233)

文章来源于公众号:前端真好玩 作者:你们的恺哥

时不时有小伙伴问我这个问题,说前端的需要学习的知识太多了,然后给我列举了一大堆技术栈:什么三大框架、各种全家桶、小程序、umi、flutter、SSR、Node 等等,反正是把前端技术栈列举了一遍~

前端东西确实蛮多,但也没必要什么都想学。一旦你有这个想法,多半会像个无头苍蝇乱飞。这个看看,那个学点,到头来啥东西都没学好。

这样的例子其实我在读者里看到好些了,学习确实看起来是在学习,啥资料都收藏了,今天看会这个技术的视频,明天拿上另一个技术的书读起来,但是这种学习方式相当低效,另外啥资料都收集还会造成一个时间完全不够用的假象。如果没有一个学习的目标规划,只能事倍功半(可能连半都没有)。因为编程这个事情其中一部分就是靠大量的编码,如果你今天学这明天看那,没有大量的练习让你去训练自己到最后就是啥都不学不好。

先了解自己到底要什么

知道自己要什么是学习之前必须搞定的,否则就是无头苍蝇四处乱来了。

如果你真的没有什么思路的话,我这里推荐三个路子:

  1. 基础,反正无论什么场景下我都会推荐先学好基础,基础不好谈别的就是耍流氓
  2. 公司中用的或者将来要用的技术栈,觉得哪个还学的不好就先学哪个
  3. 看大公司的招聘要求(切记要看大公司的,因为大公司的要求不会是需要你学一大堆,只有小公司才会需要你这也会那也会),然后挑出要求中你还不熟练的开学

深度还是广度?

其实这个问题个人觉得没有绝对答案,两者各有好处。

挖掘深度有助于你成为一个领域中的专家,虽然绝大部分人是没有这个机会的啦,但是比一部分人我们肯定是做得到的,所以挖掘深度归结到底能帮助你成为行业内不那么容易淘汰的人。

挖掘广度有助于帮助你触类旁通,了解更多的概念等等,另外个人体感也会有学的越多就越快的感觉。当然这个挖掘广度不是前文说的那种啥都要去学的做法,而是在学习一个方向的时候顺带把有联系的内容也学上一点。

举个例子今天你打算开始学 ReduxReact 的状态管理库),那么在学习 Redux 的过程中,你可以考虑顺带学习一下它的竞品对比 Redux 的优势缺陷是什么等等。这里需要注意的是没有让你把它的竞品也全部学一遍,而是了解竞品的优势及缺陷(这是广度),挖掘深度是好好学 Redux 直到能造出一样的轮子(这是挖掘深度到很后面了)。

建立知识体系

构建知识体系相当重要,否则不管你学到什么都是单独的一块知识,和其他内容不存在联系的话很容易忘记。

大家应该之前有在网上看到过前端知识脑图这类的东西,这个其实就算是一种次点(因为这种只是一个细分领域下的划分,没有和更多的细分领域产生联系)的知识体系,当然能先掌握它也是很棒的。

更好的方式是你学到的知识尽可能的要与别的知识连接起来,能与越多的知识联系起来越好。

举个例子今天面试官问了你一个理论知识,这时候如果你能先讲出理论知识,又能讲出有关联的理论知识,最后用工作中的实例去描述这个知识,这种就算是一个不错的知识体系实践。你既将这个理论知识与别的理论知识连接了起来,又能与实战中的例子产生关联。

那么我们该如何建立自己的知识体系呢?方法很简单:

  1. 把自己学到的知识用自己的话写成笔记
  2. 画脑图,把笔记浓缩到脑图中
  3. 学到新的知识重复一和二步骤,然后思考新学习到的内容是否可以与别的知识产生联系,能产生联系就用箭头双向连接起来

不要想着啥都学

文章开头列举的很多技术栈比如:flutter、SSR、umi 这些其实很多笔者也并不熟悉,但我不会老是想着我啥时候去学一下它们。

因为人的精力肯定是有限的,对于在工作中大概率用不到的东西我向来的策略是了解这个技术栈,读一下它的 Readme,知道它到底解决了什么问题就行,除此以外就不会再继续学习了,只有当我真的有需要这些技术栈的时候我才会去学习它们。

这个策略我也推荐大家可以用起来,因为真的没有必要超前很多去学习一门不知道什么时候才能用得到技术。前文笔者也说过编程是需要大量练习的,没有练习的话过段时间可能你就有点忘记了(反正笔者会这样),然后再过段时间这个技术可能更新迭代大版本了,那你学的东西可能还没用上就得重学了,有那时间打游戏不好嘛~

以上就是W3Cschool编程狮关于总感觉自己不会的很多,又不知道从何下手,资深前端带你破局的相关介绍了,希望对大家有所帮助。

为什么我停止使用Redux?

thbcm阅读(188)

ReduxReact 生态系统中的革命性技术。它使我们能够在全局范围内存储不可变数据,并解决了在组件树中 prop-drilling 的问题。需要在应用程序之间共享不可变数据时,它现在依旧是一种可以方便扩展的优秀工具。

但是,为什么我们非得需要一个全局存储呢?我们的前端应用程序真的那么复杂吗,还是说我们试图用 Redux 做的事情太多了?

单页应用程序的问题

React 这样的单页应用程序(SPA)的出现为我们开发 Web 应用程序的方式带来了许多变化。它将我们的后端与前端代码分离开来,使我们能够专心一致并分离出关注点。围绕状态,它还引入了很多复杂性。

现在,异步获取数据意味着数据必须位于两个位置:前端和后端。我们必须考虑如何在全局范围内以最佳方式存储这些数据,以便它们能对我们的所有组件都可用,同时保持数据缓存以减少网络延迟。现在,前端开发中的很大一部分负担来自于我们的全局存储的维护工作,我们还要确保这些存储不会遭受状态错误、数据非规范化和陈旧数据的困扰。

Redux 不是缓存

使用 Redux 和类似的状态管理库时,大多数人都会遇到的一大问题是,我们会将其视为后端状态的缓存。我们获取数据,通过 reducer/action 将其添加到存储中,并定期重新获取以确保它是最新的。

我们用 Redux 做的事情太多了,甚至把它看成是解决问题的全面解决方案。

关键在于,我们的前端和后端状态永远不会真正同步,我们最多可以营造一种它们同步的错觉。这是客户端 – 服务器模型的缺点之一,也是为什么我们需要缓存的原因所在。但是,同步缓存和保持状态是非常复杂的,因此我们不应该像 Redux 鼓励的那样,从头开始重新创建这个后端状态。

当我们开始在前端重新创建数据库时,后端和前端之间的职责界限很快就变得模糊不清。作为前端开发人员,我们不需要完全了解表及其关系即可创建简单的 UI。

我们也不必知道如何高水平地标准化我们的数据。这种责任应该落在设计表的那些人(后端开发人员)身上。然后,后端开发人员可以用文档化的 API 形式为前端开发人员提供抽象。

现在,人们围绕 Redux 构建了无数的库(redux-observable、redux-saga 和 redux-thunk 等),以帮助我们管理后端数据,每个库都为已经繁琐不已的库又增加了一层复杂性。我相信其中大多数都没有达成目标。有时为了前进。我们需要先退后一步。

如果我们不再在前端代码中管理后端状态,而只是将其视为需要定期更新的缓存会怎么样呢?将前端视为从缓存读取内容的简单显示层后,我们的代码就会变得更加易用,并且更适合纯前端开发人员阅读。我们获得了分离关注点的所有好处,同时避开了构建 SPA 的大部分缺点。

后端状态的更简单方法

我认为有两个库比使用 Redux(或类似的状态管理库)存储后端状态要好用很多。

React Query

我已经在自己的多数个人和工作项目中使用 React Query 几个月了。这个库有一个非常简单的 API 和几个 hooks,用于管理查询(获取数据)和突变(更改数据)。

自从使用 React Query 之后,我不仅提升了效率,而且最终编写的样板代码比 Redux 少了 9 成。我发现自己更容易将注意力集中在前端应用程序的 UI/UX 上,不会再时刻操心整个后端状态了。

要对比这个库和 Redux 的话,我们来看这两种方法的一个代码示例。我使用常规 JS、React Hooks 和 axios 实现了一个从服务器获取的简单 TODO 列表。

首先是 Redux 实现:

import React, { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import axios from 'axios';
const SET_TODOS = "SET_TODOS";


export const rootReducer = (state = { todos: [] }, action) => {
  switch (action.type) {
    case SET_TODOS:
      return { ...state, todos: action.payload };
    default:
      return state;
  }
};


export const App = () => {
  const todos = useSelector((state) => state.todos);
  const dispatch = useDispatch();
  useEffect(() => {
    const fetchPosts = async () => {
      const { data } = await axios.get("/api/todos");
      dispatch({
        type: SET_TODOS,
        payload: data}
      );
    };
    fetchPosts();
  }, []);
  return (
    <ul>{
        todos.length > 0 && todos.map((todo) =>{
            <li>{todo.text}</li>
        } 
      )
    }
    </ul> 
  );
};

请注意,到这里甚至还没有开始处理重新获取、缓存和无效化,只是加载数据并在加载时将其存储在全局存储中而已。

React Query 实现的相同示例:

import React from "react";
import { useQuery } from "react-query";
import axios from "axios";
const fetchTodos = () => {
  const { data } = axios.get("/api/todos");
  return data;
};
const App = () => {
  const { data } = useQuery("todos", fetchTodos);
  return data ? (
    <ul>{data.length > 0 && data.map((todo) => <li>{todo.text}</li>)}</ul> 
  ) : null;
};

默认情况下,上面的示例包括具有合理默认值的数据重新获取、缓存和过时内容无效化。你可以在全局级别设置缓存配置,然后就可以忘掉它了——一般来说它足以完成你期望的工作

现在,无论需要什么数据,你都可以将 useQuery hook 与你设置的唯一键(在本例中为“todos”)一起使用,并使用异步调用来获取数据。

要更改接口状态时,React Query 提供了 useMutation hook。

一旦你开始使用这个库,就会发现在绝大多数项目中 Redux 都太笨重了。处理完应用程序的数据获取 / 缓存部分后,前端几乎没有全局状态可处理。可以使用 Context 或 useContext+useReducer 处理剩下的少量内容,代替 Redux 的作用。

或者更好的方法是,使用 React 的内置状态作为你的简单前端状态,这样做肯定没问题的。

// clean, beautiful, and simple
const [state, setState] = useState();

我们应该更彻底地分离后端与前端,而不是陷在这种模棱两可的中间状态里。本文提到的这些库代表了我们在单页应用程序中管理状态的方式变革,并且是朝着正确方向迈出的一大步。我期待着看到它们能对 React 社区产生怎样的影响。

文章来源:dev.to/g_abud/why-i-quit-redux-1knl

以上就是W3Cschool编程狮关于为什么我停止使用Redux?的相关介绍了,希望对大家有所帮助。

工具用得好,Python源码阅读没烦恼

thbcm阅读(211)

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

每当我们接手一个新项目时,面对庞杂的模块、繁复的代码,想必心情是非常绝望的,“这都特么啥呀?”

如果你也有这样的烦恼,那你就应该看这篇文章。

我们阅读源码的一大烦恼在于,项目代码中存在着各种各样的调用,而我们的大脑却没办法像计算机一样完好地维护一个动态的调用链;时常发生的情况就是一头扎进了源码中,然后——

“我是谁?我在哪儿?我为什么要在这儿?”

显然,作为一个地道的程序员,这个时候应该想到借助工具了。

再根据软件界第一定律“99%的工作都是别人做过的”,于是我们大可以拿来主义一把,琢磨琢磨是不是已经有人做了这样的工具。

说到这里可能很多同学已经想到了一个被局部称为“世界第一编辑器”的软件——Source Insight。(然后看了看正版价格就灰溜溜滚回了Google)

然后还有强大的Understand,付费环节还比较繁琐,暂时pass。

直到我们在 V2EX 看见有同学推荐了一款名为Sourcetrail的开源软件。

下载安装

打开软件官方网站,可以看到一个很简洁的页面,直截了当地告诉你它是哪条道上的四年级老大哥:Sourcetrail就是为你阅读陌生源码赋能的。同时还免费、开源、跨平台……háo嘛,今年几大红火要素都占齐活儿了,不捞一把都说不过去。

接下来就是download,在GitHub的release页面选择自己系统对应的发布版本下载安装:

具体的安装步骤与其他应用相比大同小异,就不再赘述了,否则这篇文章也太水了点

软件介绍

安装好后,运行程序,会出现这样的界面:

我们选择“New Project”,在随后的界面中填写好项目名称和项目路径:

填好后点击下方的Add Source Group按钮,用以添加代码。

——在Sourcetrail中允许我们在同一个项目中添加多个来源的代码,甚至允许各个代码分组的语言互不相同,这些代码分组即为各个Source Group

由于我们只需要查看一个代码库的内容,因此我们也只需要添加一个Source Group即可,如下图依次点击:

在继续输入新的信息前,打开你的命令行工具(Windows系统:Win+R,输入cmd然后回车;Linux下不赘述),输入where python(Windows)或which python3(Linux),即可看到当前环境的Python安装路径,记下这个路径,我们需要用它来解析 Python 代码。

然后在新的界面中,需要填写 Python 环境的字段填入刚刚我们查到的Python所在目录(即去除最后一个斜杠及之后的内容)。

其他的需要注意的就是“要建立索引的文件/目录”,这个字段就是添加我们真正要阅读的 Python 源码路径。字段左下角有一个“+”号,点击即可增加一个源码路径:

原本是想把 Python 之父龟叔多年前写的爬虫程序作为示例的,奈何网络不给力,迟迟拉取不下源码,于是另外找了一份开源项目“北京实时公交”替换之。

点击右下角“Create”,再点击“Start”:

解析就开始了:

再点击“OK”:

得到下图的解析结果:

可以看到,Sourcetrail将解析结果按“文件”、“模块”等大致分了类。

我们点击最关心的“函数(Function)”来体验一下:

在左边,Sourcetrail为我们生成了形象的调用图;在右边,Sourcetrail列出了当前焦点函数的代码及其相应调用。

无论在左边操作还是在右边操作,都会带来界面的同步变化。

从此我们再也不必苦哈哈地在A4纸上写下繁琐的调用关系了哈哈哈哈,翻身农奴把歌唱

总结

本文介绍了一个可以把源码调用关系可视化的工具,可以极大便利我们阅读他人代码的工作。实际上这类工具还有很多,比如Source Insight和Understand。

只要是能够提升我们学习/开发效率的,我们都应该乐于尝试。后续我们还会推荐一些这类实用的工具,希望可以帮助大家升职加薪[手动滑稽]。

以上就是W3Cschool编程狮关于工具用得好,源码阅读没烦恼的相关介绍了,希望对大家有所帮助。

普通的一个跨域问题,一不小心就带来三个大BUG

thbcm阅读(248)

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

hello 大家好,我是阿粉,最近在做一个前后分离的项目,天天除了跟前端就是跟测试撕逼。今天,阿粉带着大家来了解一下 最近在项目中遇到的一个坑。

需求

前端用了个富文本插件 Ueditor ,插件初始化的时候需要从后端获取 config 配置。

入坑经历

首先看下最开始的代码:

 @RequestMapping(value = "/getConfig")
    public Object getConfig(HttpServletRequest request){
       return readConfig();
    }


 /**
     * 读取配置文件
     * @return
     */
    private UedConfig readConfig() {
        String path = this.getClass().getResource("/").getPath();
        FileInputStream fileInputStream = new FileInputStream(path + "config/ued_config.json");
        //读出来,转成对象返回
        ...
    }

代码大概就是这样,然后启动起来之后,前端告诉我没有获取到信息。纳尼,我 postman 自测都没问题,有数据返回。然后阿粉跑去问前端,有报错吗?前端说跨域了。当时阿粉就不乐意了,马上 ctrl+c 加上ctrl+v 把后端处理跨域请求的配置发给前端。然后前端告诉我是 jsonp 请求跨域。阿粉一脸懵逼??? jsonp 跨域?没听说过啊。马上找度娘去,果然一搜全是处理方式。因为是 springboot 项目,所以找了个最简单的。看代码:

@ControllerAdvice
public class JsonpAdvice extends AbstractJsonpResponseBodyAdvice {
    public JsonpAdvice(){
        super("callback");
    }
}

看,是不是很简单。要是 AbstractJsonpResponseBodyAdvice 这个单词下面没有红线就更完美了。没有引入类吗?怎么难得倒阿粉呢。alt + enter,嗯哼,什么情况,没有这个类?不可能啊。马上问度娘一下,原来这个类是 springboot 2.0以下才有。那怎么办呢?2.0以上好像没有处理 jsonp 跨域的方式啊。阿粉又在百度搜啊搜,果然,皇天不负有心人,阿粉看到 jackson 里面有个类 JSONPObject 可以处理,然后阿粉改了一下代码:

@RequestMapping(value = "/getConfig")
public Object getConfig(String callback,HttpServletRequest request){
    return new JSONPObject(callback,readConfig());
}

然后重启,自测没问题。让前端试下,可以正常获取。OK,完美。

后面项目完成了,前后端也对接完了,发到测试环境,让测试人员测试。Duang~一个bug扔阿粉头上,获取配置失败。不可能啊,肯定是前端的问题,跑去找前端让前端看下。前端看了之后给我来了一句,测试环境不跨域。瞬间一万头草尼玛从头上飘过。阿粉辛辛苦苦花了几个小时才把跨域问题处理了,你给我说测试环境不跨域。

哎,没办法,这口锅只能自己含泪抗下了。然后阿粉又改了一次代码:

@RequestMapping(value = "/getConfig")
public Object getConfig(String callback,HttpServletRequest request){
    return StringUtils.isEmpty(callback) ? readConfig() : new JSONPObject(callback,readConfig());
}

还是做了个兼容,因为前端还需要连我本地调试。不过这次应该没有问题了吧,阿粉心里还是有些小得意。提交代码,发测试,搞定。

Duang~同样的bug再一次扔在了阿粉脸上。心都凉了,怎么回事呢?日志也没报错啊。捣鼓半天,不知道什么原因,阿粉只能加写log,因为在测试环境不好调试,也没报错。然后让运维配合一下,然后发现 this.getClass().getResource("/").getPath() 得到的路径不对,阿粉再一次一脸懵逼,这个不是获取项目根路径吗?怎么回事呢。

然后阿粉又厚着脸皮找度娘,果然一下就找到原因了,因为 springboot 集成了tomcat,项目直接是打成 jar 包运行的,不能通过 this.getClass().getResource("/").getPath() 这种获取项目根路径的方式来获取,只能通过流的方式,然后阿粉又改了下代码:

/**
     * 读取配置文件
     * @return
     */
private UedConfig readConfig() {
    InputStream resourceAsStream = this.getClass().getResourceAsStream("/"+"config/ued_config.json");
    //读出来,转成对象返回
    ...
}

好了,这次提交之后终于可以了。解决了 bug ,阿粉心里美滋滋。

总结

经常看到一些小伙伴说,本地对接没问题,怎么一到测试环境就那么多 bug 呢。阿粉总结了一下,主要还是以下几点:

  1. 系统不一样,本地一般都是window系统,而测试和线上一般都是 linux 系统。
  2. 运行方式不一样,本地 springboot 项目是通过代码工具 (idea)启动,而测试和线上是通过 jar 包启动。
  3. 比如还有一些其他 jar 引入的问题

以上就是W3Cschool编程狮关于普通的一个跨域问题,一不小心就带来三个大BUG的相关介绍了,希望对大家有所帮助。

联系我们