注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

如何写 CSS 重置(RESET)样式?

很长一段时间,我都使用Eric Meyer著名的CSS Reset。这是CSS的一个坚实的块,但是在这一点上它有点长。它已经十多年没有更新了,从那时起发生了很多变化! 最近,我一直在使用我自己的自定义CSS重置。它包括我发现的所有小技巧,以改善用户体验和CSS...
继续阅读 »

很长一段时间,我都使用Eric Meyer著名的CSS Reset。这是CSS的一个坚实的块,但是在这一点上它有点长。它已经十多年没有更新了,从那时起发生了很多变化!


最近,我一直在使用我自己的自定义CSS重置。它包括我发现的所有小技巧,以改善用户体验和CSS创作体验。


像其他CSS重置一样,在设计/化妆品方面,它是不赞成的。您可以将此重置用于任何项目,无论您想要哪种美学。


在本教程中,我们将介绍我的自定义 CSS 重置。我们将深入研究每个规则,您将了解它的作用以及您可能想要使用它的原因!


CSS 重置


事不宜迟,这里是:


/*
1. Use a more-intuitive box-sizing model.
*/
*, *::before, *::after {
box-sizing: border-box;
}
/*
2. Remove default margin
*/
* {
margin: 0;
}
/*
3. Allow percentage-based heights in the application
*/
html, body {
height: 100%;
}
/*
Typographic tweaks!
4. Add accessible line-height
5. Improve text rendering
*/
body {
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
/*
6. Improve media defaults
*/
img, picture, video, canvas, svg {
display: block;
max-width: 100%;
}
/*
7. Remove built-in form typography styles
*/
input, button, textarea, select {
font: inherit;
}
/*
8. Avoid text overflows
*/
p, h1, h2, h3, h4, h5, h6 {
overflow-wrap: break-word;
}
/*
9. Create a root stacking context
*/
#root, #__next {
isolation: isolate;
}


它相对较短,但是这个小样式表中包含了很多东西。让我们开始吧!



从历史上看,CSS重置的主要目标是确保浏览器之间的一致性,并撤消所有默认样式,从而创建一个空白的石板。我的CSS重置并没有真正做这些事情。




如今,浏览器在布局或间距方面没有巨大的差异。总的来说,浏览器忠实地实现了CSS规范,并且事情的行为符合您的预期。因此,它不再是必要的了。




我也不认为有必要剥离所有浏览器默认值。例如,我可能确实想要设置标签!我总是可以在各个项目风格中做出不同的设计决策,但我认为剥离常识性默认值是没有意义的。<em>``font-style: italic




我的CSS重置可能不符合"CSS重置"的经典定义,但我正在采取这种创造性的自由。



CSS盒子模型


测验!通过可见的粉红色边框进行测量,假设未应用其他 CSS,则在以下方案中元素的宽度是多少?.box


<style>
.parent {
width: 200px;
}
.box {
width: 100%;
border: 2px solid hotpink;
padding: 20px;
}
</style>
<div>
<div></div>
</div>

我们的元素有.因为它的父级是200px宽,所以100%将解析为200px。.box``width: 100%


但是它在哪里应用200px宽度? 默认情况下,它会将该大小应用于内容框


如果您不熟悉,"内容框"是框模型中实际保存内容的矩形,位于边框和填充内:


一个粉红色的盒子,里面有一个绿色的盒子。粉红色代表边框,绿色代表填充。在内部,一个黑色矩形被标记为"内容框"


该声明会将 的内容框设置为 200px。填充将添加额外的40px(每侧20px)。边框添加最后一个 4px(每侧 2px)。当我们进行数学计算时,可见的粉红色矩形将是244px宽。width: 100%``.box


当我们尝试将一个 244px 的框塞入一个 200px 宽的父级中时,它会溢出:


image.png


这种行为很奇怪,对吧?幸运的是,我们可以通过设置以下规则来更改它:


*, *::before, *::after {
box-sizing: border-box;
}

应用此规则后,百分比将基于边框进行解析。在上面的示例中,我们的粉红色框将为 200px,内部内容框将缩小到 156px(200px - 40px - 4px)。


在我看来,这是一个必须的规则。 它使CSS更适合使用。


我们使用通配符选择器 () 将其应用于所有元素和伪元素。与普遍的看法相反,这对性能来说并不坏*


我在网上看到了一些建议,可以代替这样做:


html {
box-sizing: border-box;
}
*, *:before, *:after {
box-sizing: inherit;
}


 删除默认间距


* {
margin: 0;
}

浏览器围绕保证金做出常识性的假设。例如,默认情况下,将包含比段落更多的边距。h1


这些假设在文字处理文档的上下文中是合理的,但对于现代 Web 应用程序而言,它们可能不准确。


Margin是一个棘手的魔鬼,而且我经常发现自己希望元素默认情况下没有任何元素。所以我决定全部删除它。🔥


如果/当我确实想为特定标签添加一些边距时,我可以在我的自定义项目样式中执行此操作。通配符选择器 () 具有极低的特异性,因此很容易覆盖此规则。*


基于百分比的高度


html, body {
height: 100%;
}

你有没有试过在CSS中使用基于百分比的高度,却发现它似乎没有效果?


下面是一个示例:


image.png


元素有,但元素根本不会增长!main``height: 100%


这不起作用,因为在 Flow 布局(CSS 中的主要布局模式)中,并且操作的原则根本不同。元素的宽度是根据其父级计算的,但元素的高度是根据其子元素计算的。height``width


这是一个复杂的主题,远远超出了本文的范围。我计划写一篇关于它的博客文章,但与此同时,你可以在我的CSS课程中了解它,CSS for JavaScript Developers。


作为一个快速演示,在这里我们看到,当我们应用此规则时,我们的元素可以增长:main


image.png


如果你使用的是像 React 这样的 JS 框架,你可能还希望向这个规则添加第三个选择器:框架使用的根级元素。


例如,在我的 Next.js 项目中,我按如下方式更新规则:


html, body, #__next {
height: 100%;
}


为什么不使用vh?


您可能想知道:为什么要在基于百分比的高度上大惊小怪?为什么不改用该装置呢?vh


问题是该单元在移动设备上无法正常工作; 将占用超过100%的屏幕空间,因为移动浏览器在浏览器UI来来去去的地方做那件事。vh``100vh


将来,新的CSS单元将解决这个问题。在此之前,我继续使用基于百分比的高度。



调整行高


body {
line-height: 1.5;
}

line-height控制段落中每行文本之间的垂直间距。默认值因浏览器而异,但往往在 1.2 左右。


此无单位数字是基于字体大小的比率。它的功能就像设备一样。如果为 1.2,则每行将比元素的字体大小大 20%。em``line-height


问题是:对于那些有阅读障碍的人来说,这些行挤得太紧,使得阅读起来更加困难。WCAG标准规定行高应至少为1.5


现在,这个数字确实倾向于在标题和其他具有大类型的元素上产生相当大的行:


image.png


您可能希望在标题上覆盖此值。我的理解是,WCAG标准适用于"正文"文本,而不是标题。



使用"计算"实现更智能的线高


我一直在尝试一种管理行高的替代方法。在这里:


* {
line-height: calc(1em + 0.5rem);
}

这是一个非常高级的小片段,它超出了这篇博客文章的范围,但这里有一个快速的解释。



字体平滑,抗锯齿


body {
-webkit-font-smoothing: antialiased;
}

好吧,所以这个有点争议。


在 MacOS 电脑上,浏览器将默认使用"子像素抗锯齿"。这是一种旨在通过利用每个像素内的 R/G/B 灯使文本更易于阅读的技术。


过去,这被视为可访问性的胜利,因为它提高了文本对比度。您可能已经读过一篇流行的博客文章停止"修复"字体平滑,该帖子主张反对切换到"抗锯齿"。


问题是:那篇文章写于2012年,在高DPI"视网膜"显示时代之前。今天的像素要小得多,肉眼看不见。


像素 LED 的物理排列也发生了变化。如果你在显微镜下看一台现代显示器,你不会再看到R/G/B线的有序网格了。


在2018年发布的MacOS Mojave中 ,Apple在整个操作系统中禁用了子像素抗锯齿。我猜他们意识到它在现代硬件上弊大于利。


令人困惑的是,像Chrome和Safari这样的MacOS浏览器仍然默认使用子像素抗锯齿。我们需要通过设置为 来显式关闭它。-webkit-font-smoothing``antialiased


区别如下:


image.png


MacOS 是唯一使用子像素抗锯齿的操作系统,因此此规则对 Windows、Linux 或移动设备没有影响。如果您使用的是 MacOS 电脑,则可以尝试实时渲染:


合理的媒体默认值


img, picture, video, canvas, svg {
display: block;
max-width: 100%;
}

所以这里有一件奇怪的事情:图像被认为是"内联"元素。这意味着它们应该在段落的中间使用,例如 或 。<em>``<strong>


这与我大多数时候使用图像的方式不符。通常,我对待图像的方式与处理段落或标题或侧边栏的方式相同;它们是布局元素。


但是,如果我们尝试在布局中使用内联元素,则会发生奇怪的事情。如果您曾经有过一个神秘的4px间隙,不是边距,填充或边框,那么它可能是浏览器添加的"内联魔术空间"。line-height


通过默认设置所有图像,我们回避了整个类别的时髦问题。display: block


我也设置了.这样做是为了防止大图像溢出,如果它们放置在不够宽而无法容纳它们的容器中。max-width: 100%


大多数块级元素会自动增大/缩小以适应其父元素,但媒体元素是特殊的:它们被称为替换元素,并且它们不遵循相同的规则。<img>


如果图像的"本机"大小为 800×600,则该元素的宽度也将为 800px,即使我们将其放入 500px 宽的父级中也是如此。<img>


此规则将防止该图像超出其容器,这对我来说更像是更明智的默认行为。


继承窗体控件的字体


input, button, textarea, select {
font: inherit;
}

如果我们想避免这种自动缩放行为,输入的字体大小需要至少为1rem / 16px。以下是解决此问题的一种方法:


CSS
input, button, textarea, select {
font-size: 1rem;
}

这解决了自动变焦问题,但它是创可贴。让我们解决根本原因:表单输入不应该有自己的印刷风格!


CSS
input, button, textarea, select {
font: inherit;
}

font是一种很少使用的速记,它设置了一堆与字体相关的属性,如 、 和 。通过将其设置为 ,我们指示这些元素与其周围环境中的排版相匹配。font-size``font-weight``font-family``inherit


只要我们不为正文文本选择令人讨厌的小字体大小,就可以同时解决我们所有的问题。🎉


自动换行


CSS
p, h1, h2, h3, h4, h5, h6 {
overflow-wrap: break-word;
}

在 CSS 中,如果没有足够的空间来容纳一行上的所有字符,文本将自动换行。


默认情况下,该算法将寻找"软包装"机会;这些是算法可以拆分的字符。在英语中,唯一的软包装机会是空格和连字符,但这因语言而异。


如果某行没有任何软换行机会,并且它不合适,则会导致文本溢出:


image.png


这可能会导致一些令人讨厌的布局问题。在这里,它添加了一个水平滚动条。在其他情况下,它可能会导致文本与其他元素重叠,或滑到图像/视频后面。


该属性允许我们调整换行算法,并允许它在找不到软换行机会时使用硬换行:overflow-wrap


image.png


这两种解决方案都不完美,但至少硬包装不会弄乱布局!


感谢Sophie Alpert提出类似的规则!她建议将其应用于所有元素,这可能是一个好主意,但不是我个人测试过的东西。


您也可以尝试添加属性:hyphens


p {
overflow-wrap: break-word;
hyphens: auto;
}

hyphens: auto使用连字符(在支持连字符的语言中)来指示硬换行。这也使得硬包装更加普遍。


如果您有非常窄的文本列,这可能是值得的,但它也可能有点分散注意力。我选择不将其包含在重置中,但值得尝试!


根堆叠上下文


#root, #__next {
isolation: isolate;
}

最后一个是可选的。通常只有当你使用像 React 这样的 JS 框架时才需要它。


正如我们在"到底是什么,z-index??"中看到的那样,该属性允许我们创建新的堆叠上下文,而无需设置 .isolation``z-index


这是有益的,因为它允许我们保证某些高优先级元素(模式,下拉列表,工具提示)将始终显示在应用程序中的其他元素之上。没有奇怪的堆叠上下文错误,没有z指数军备竞赛。


您应该调整选择器以匹配您的框架。我们希望选择在其中呈现应用程序的顶级元素。例如,create-react-app 使用 一个 ,因此正确的选择器是 。<div id="root">``#root


最终成品


下面再次以精简的复制友好格式进行 CSS 重置:


/*
Josh's Custom CSS Reset
https://www.joshwcomeau.com/css/custom-css-reset/
*/
*, *::before, *::after {
box-sizing: border-box;
}
* {
margin: 0;
}
html, body {
height: 100%;
}
body {
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
img, picture, video, canvas, svg {
display: block;
max-width: 100%;
}
input, button, textarea, select {
font: inherit;
}
p, h1, h2, h3, h4, h5, h6 {
overflow-wrap: break-word;
}
#root, #__next {
isolation: isolate;
}
```
```
/*
Josh's Custom CSS Reset
https://www.joshwcomeau.com/css/custom-css-reset/
*/
*, *::before, *::after {
box-sizing: border-box;
}
* {
margin: 0;
}
html, body {
height: 100%;
}
body {
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
img, picture, video, canvas, svg {
display: block;
max-width: 100%;
}
input, button, textarea, select {
font: inherit;
}
p, h1, h2, h3, h4, h5, h6 {
overflow-wrap: break-word;
}
#root, #__next {
isolation: isolate;
}
```
`




















作者:非优秀程序员
链接:https://juejin.cn/post/7034308682825351176

收起阅读 »

前端面试js高频手写大全(上)

在前端面试中,手撕代码显然是不可避免的,并且占很大的一部分比重。编程题主要分为这几种类型:* 算法题* 涉及js原理的题以及ajax请求* 业务场景题: 实现一个具有某种功能的组件* 其他(进阶,对计算机综合知识的考察,考的相对较少):实现订阅发布者模式;分别...
继续阅读 »



介绍

在前端面试中,手撕代码显然是不可避免的,并且占很大的一部分比重。

一般来说,如果代码写的好,即使理论知识答得不够清楚,也能有大概率通过面试。并且其实很多手写往往背后就考察了你对相关理论的认识。

编程题主要分为这几种类型:

* 算法题
* 涉及js原理的题以及ajax请求
* 业务场景题: 实现一个具有某种功能的组件
* 其他(进阶,对计算机综合知识的考察,考的相对较少):实现订阅发布者模式;分别用面向对象编程,面向过程编程,函数式编程实现把大象放进冰箱等等

其中前两种类型所占比重最大。
算法题建议养成每天刷一道leetcode的习惯,重点刷数据结构(栈,链表,队列,树),动态规划,DFS,BFS

本文主要涵盖了第二种类型的各种重点手写。

建议优先掌握

  • instanceof (考察对原型链的理解)

  • new (对创建对象实例过程的理解)

  • call&apply&bind (对this指向的理解)

  • 手写promise (对异步的理解)

  • 手写原生ajax (对ajax原理和http请求方式的理解,重点是get和post请求的实现)

  • 事件订阅发布 (高频考点)

  • 其他:数组,字符串的api的实现,难度相对较低。只要了解数组,字符串的常用方法的用法,现场就能写出来个大概。(ps:笔者认为数组的reduce方法比较难,这块有余力可以单独看一些,即使面试没让你实现reduce,写其他题时用上它也是很加分的)


话不多说,直接开始

1. 手写instanceof

instanceof作用:

判断一个实例是否是其父类或者祖先类型的实例。

instanceof 在查找的过程中会遍历左边变量的原型链,直到找到右边变量的 prototype查找失败,返回 false

 let myInstanceof = (target,origin) => {
    while(target) {
        if(target.__proto__===origin.prototype) {
           return true
        }
        target = target.__proto__
    }
    return false
}
let a = [1,2,3]
console.log(myInstanceof(a,Array));  // true
console.log(myInstanceof(a,Object));  // true

2. 实现数组的map方法

数组的map() 方法会返回一个新的数组,这个新数组中的每个元素对应原数组中的对应位置元素调用一次提供的函数后的返回值。

用法:

const a = [1, 2, 3, 4];
const b = array1.map(x => x * 2);
console.log(b);   // Array [2, 4, 6, 8]

实现前,我们先看一下map方法的参数有哪些
image.png
map方法有两个参数,一个是操作数组元素的方法fn,一个是this指向(可选),其中使用fn时可以获取三个参数,实现时记得不要漏掉,这样才算完整实现嘛

原生实现:

    // 实现
    Array.prototype.myMap = function(fn, thisValue) {
           let res = []
           thisValue = thisValue||[]
           let arr = this
           for(let i=0; i<arr.length; i++) {
               res.push(fn.call(thisValue, arr[i],i,arr))   // 参数分别为this指向,当前数组项,当前索引,当前数组
          }
           return res
      }
       // 使用
       const a = [1,2,3];
       const b = a.myMap((a,index)=> {
               return a+1;
          }
      )
       console.log(b)   // 输出 [2, 3, 4]

3. reduce实现数组的map方法

利用数组内置的reduce方法实现map方法,考察对reduce原理的掌握

Array.prototype.myMap = function(fn,thisValue){
    var res = [];
    thisValue = thisValue||[];
    this.reduce(function(pre,cur,index,arr){
        return res.push(fn.call(thisValue,cur,index,arr));
    },[]);
    return res;
}

var arr = [2,3,1,5];
arr.myMap(function(item,index,arr){
console.log(item,index,arr);
})

4. 手写数组的reduce方法

reduce() 方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终为一个值,是ES5中新增的又一个数组逐项处理方法

参数:

  • callback(一个在数组中每一项上调用的函数,接受四个函数:)

    • previousValue(上一次调用回调函数时的返回值,或者初始值)

    • currentValue(当前正在处理的数组元素)

    • currentIndex(当前正在处理的数组元素下标)

    • array(调用reduce()方法的数组)

  • initialValue(可选的初始值。作为第一次调用回调函数时传给previousValue的值)

 function reduce(arr, cb, initialValue){
    var num = initValue == undefined? num = arr[0]: initValue;
    var i = initValue == undefined? 1: 0
    for (i; i< arr.length; i++){
       num = cb(num,arr[i],i)
    }
    return num
}

function fn(result, currentValue, index){
    return result + currentValue
}

var arr = [2,3,4,5]
var b = reduce(arr, fn,10)
var c = reduce(arr, fn)
console.log(b)   // 24

5. 数组扁平化

数组扁平化就是把多维数组转化成一维数组

1. es6提供的新方法 flat(depth)

let a = [1,[2,3]]; 
a.flat(); // [1,2,3]
a.flat(1); //[1,2,3]

其实还有一种更简单的办法,无需知道数组的维度,直接将目标数组变成1维数组。 depth的值设置为Infinity。

let a = [1,[2,3,[4,[5]]]]; 
a.flat(Infinity); // [1,2,3,4,5] a是4维数组

2. 利用cancat

function flatten(arr) {
    var res = [];
    for (let i = 0, length = arr.length; i < length; i++) {
    if (Array.isArray(arr[i])) {
    res = res.concat(flatten(arr[i])); //concat 并不会改变原数组
    //res.push(...flatten(arr[i])); //或者用扩展运算符
    } else {
        res.push(arr[i]);
      }
    }
    return res;
}
let arr1 = [1, 2,[3,1],[2,3,4,[2,3,4]]]
flatten(arr1); //[1, 2, 3, 1, 2, 3, 4, 2, 3, 4]

补充:指定deep的flat

只需每次递归时将当前deep-1,若大于0,则可以继续展开

     function flat(arr, deep) {
       let res = []
       for(let i in arr) {
           if(Array.isArray(arr[i])&&deep) {
               res = res.concat(flat(arr[i],deep-1))
          } else {
               res.push(arr[i])
          }
      }
       return res
  }
   console.log(flat([12,[1,2,3],3,[2,4,[4,[3,4],2]]],1));

6. 函数柯里化

用的这里的方法 https://juejin.im/post/684490...

柯里化的定义:接收一部分参数,返回一个函数接收剩余参数,接收足够参数后,执行原函数。

当柯里化函数接收到足够参数后,就会执行原函数,如何去确定何时达到足够的参数呢?

有两种思路:

  1. 通过函数的 length 属性,获取函数的形参个数,形参的个数就是所需的参数个数

  2. 在调用柯里化工具函数时,手动指定所需的参数个数

将这两点结合一下,实现一个简单 curry 函数:

/**
* 将函数柯里化
* @param fn   待柯里化的原函数
* @param len   所需的参数个数,默认为原函数的形参个数
*/
function curry(fn,len = fn.length) {
return _curry.call(this,fn,len)
}

/**
* 中转函数
* @param fn   待柯里化的原函数
* @param len   所需的参数个数
* @param args 已接收的参数列表
*/
function _curry(fn,len,...args) {
   return function (...params) {
        let _args = [...args,...params];
        if(_args.length >= len){
            return fn.apply(this,_args);
        }else{
         return _curry.call(this,fn,len,..._args)
        }
  }
}

我们来验证一下:

let _fn = curry(function(a,b,c,d,e){
console.log(a,b,c,d,e)
});

_fn(1,2,3,4,5);     // print: 1,2,3,4,5
_fn(1)(2)(3,4,5);   // print: 1,2,3,4,5
_fn(1,2)(3,4)(5);   // print: 1,2,3,4,5
_fn(1)(2)(3)(4)(5); // print: 1,2,3,4,5

我们常用的工具库 lodash 也提供了 curry 方法,并且增加了非常好玩的 placeholder 功能,通过占位符的方式来改变传入参数的顺序。

比如说,我们传入一个占位符,本次调用传递的参数略过占位符, 占位符所在的位置由下次调用的参数来填充,比如这样:

直接看一下官网的例子:

image.png

接下来我们来思考,如何实现占位符的功能。

对于 lodash 的 curry 函数来说,curry 函数挂载在 lodash 对象上,所以将 lodash 对象当做默认占位符来使用。

而我们的自己实现的 curry 函数,本身并没有挂载在任何对象上,所以将 curry 函数当做默认占位符

使用占位符,目的是改变参数传递的顺序,所以在 curry 函数实现中,每次需要记录是否使用了占位符,并且记录占位符所代表的参数位置。

直接上代码:

/**
* @param fn           待柯里化的函数
* @param length       需要的参数个数,默认为函数的形参个数
* @param holder       占位符,默认当前柯里化函数
* @return {Function}   柯里化后的函数
*/
function curry(fn,length = fn.length,holder = curry){
return _curry.call(this,fn,length,holder,[],[])
}
/**
* 中转函数
* @param fn           柯里化的原函数
* @param length       原函数需要的参数个数
* @param holder       接收的占位符
* @param args         已接收的参数列表
* @param holders       已接收的占位符位置列表
* @return {Function}   继续柯里化的函数 或 最终结果
*/
function _curry(fn,length,holder,args,holders){
return function(..._args){
//将参数复制一份,避免多次操作同一函数导致参数混乱
let params = args.slice();
//将占位符位置列表复制一份,新增加的占位符增加至此
let _holders = holders.slice();
//循环入参,追加参数 或 替换占位符
_args.forEach((arg,i)=>{
//真实参数 之前存在占位符 将占位符替换为真实参数
if (arg !== holder && holders.length) {
    let index = holders.shift();
    _holders.splice(_holders.indexOf(index),1);
    params[index] = arg;
}
//真实参数 之前不存在占位符 将参数追加到参数列表中
else if(arg !== holder && !holders.length){
    params.push(arg);
}
//传入的是占位符,之前不存在占位符 记录占位符的位置
else if(arg === holder && !holders.length){
    params.push(arg);
    _holders.push(params.length - 1);
}
//传入的是占位符,之前存在占位符 删除原占位符位置
else if(arg === holder && holders.length){
   holders.shift();
}
});
// params 中前 length 条记录中不包含占位符,执行函数
if(params.length >= length && params.slice(0,length).every(i=>i!==holder)){
return fn.apply(this,params);
}else{
return _curry.call(this,fn,length,holder,params,_holders)
}
}
}

验证一下:;

let fn = function(a, b, c, d, e) {
console.log([a, b, c, d, e]);
}

let _ = {}; // 定义占位符
let _fn = curry(fn,5,_); // 将函数柯里化,指定所需的参数个数,指定所需的占位符

_fn(1, 2, 3, 4, 5);                 // print: 1,2,3,4,5
_fn(_, 2, 3, 4, 5)(1);             // print: 1,2,3,4,5
_fn(1, _, 3, 4, 5)(2);             // print: 1,2,3,4,5
_fn(1, _, 3)(_, 4,_)(2)(5);         // print: 1,2,3,4,5
_fn(1, _, _, 4)(_, 3)(2)(5);       // print: 1,2,3,4,5
_fn(_, 2)(_, _, 4)(1)(3)(5);       // print: 1,2,3,4,5

至此,我们已经完整实现了一个 curry 函数~~

7. 浅拷贝和深拷贝的实现

深拷贝和浅拷贝是只针对Object和Array这样的引用数据类型的。

浅拷贝和深拷贝的区别:

浅拷贝:创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址,如果其中一个对象改变了引用类型的属性,就会影响到另一个对象。

深拷贝:将一个对象从内存中完整的复制一份出来,从堆内存中开辟一个新区域存放。这样更改拷贝值就不影响旧的对象

浅拷贝实现:

方法一:

function shallowCopy(target, origin){
   for(let item in origin) target[item] = origin[item];
   return target;
}

其他方法(内置api):

  1. Object.assign

var obj={a:1,b:[1,2,3],c:function(){console.log('i am c')}}
var tar={};
Object.assign(tar,obj);

当然这个方法只适合于对象类型,如果是数组可以使用slice和concat方法

  1. Array.prototype.slice

var arr=[1,2,[3,4]];
var newArr=arr.slice(0);
  1. Array.prototype.concat

var arr=[1,2,[3,4]];
var newArr=arr.concat();

测试同上(assign用对象测试、slice concat用数组测试),结合浅拷贝深拷贝的概念来理解效果更佳

深拷贝实现:

方法一:

转为json格式再解析
const a = JSON.parse(JSON.stringify(b))

方法二:

// 实现深拷贝  递归
function deepCopy(newObj,oldObj){
    for(var k in oldObj){
        let item=oldObj[k]
        // 判断是数组、对象、简单类型?
        if(item instanceof Array){
            newObj[k]=[]
            deepCopy(newObj[k],item)
        }else if(item instanceof Object){
            newObj[k]={}
            deepCopy(newObj[k],item)
        }else{  //简单数据类型,直接赋值
            newObj[k]=item
        }
    }
}
(未完待续……)

作者:晚起的虫儿

来源:https://segmentfault.com/a/1190000038910420







收起阅读 »

太震撼了!我把七大JS排序算法做成了可视化!!!太好玩了!

前言大家好,我是林三心。写这篇文章是有原因的,偶然我看到了一个Java的50种排序算法的可视化的视频,但是此视频却没给出具体的实现教程,于是我心里就想着,我可以用JavaScript + canvas去实现这个酷炫的效果。每种排序算法的动画效果基本都不一样哦。...
继续阅读 »

前言

大家好,我是林三心。写这篇文章是有原因的,偶然我看到了一个Java的50种排序算法的可视化的视频,但是此视频却没给出具体的实现教程,于是我心里就想着,我可以用JavaScript + canvas去实现这个酷炫的效果。每种排序算法的动画效果基本都不一样哦。例如冒泡排序是这样的


冒泡排序2.gif

实现思路

想实现的效果

从封面可以看到,无论是哪种算法,一开始都是第一张图,而最终目的是要变成第二张图的效果


截屏2021-09-05 下午6.05.45.png

截屏2021-09-05 下午6.06.03.png

极坐标

讲实现思路之前,我先给大家复习一下高中的一个知识——极坐标。哈哈,不知道还有几个人记得他呢?



  • O:极点,也就是原点
  • ρ:极径
  • θ:极径与X轴夹角
  • x = ρ * cosθ,因为x / ρ = cosθ
  • y = ρ * sinθ,因为y / ρ = sinθ

截屏2021-09-05 下午6.26.31.png

那我们想实现的结果,又跟极坐标有何关系呢?其实是有关系的,比如我现在有一个排序好的数组,他具有37个元素,那我们可以把这37个元素转化为极坐标中的37个点,怎么转呢?


const arr = [
0, 1, 2, 3, 4, 5, 6, 7, 8,
9, 10, 11, 12, 13, 14, 15, 16, 17,
18, 19, 20, 21, 22, 23, 24, 25, 26,
27, 28, 29, 30, 31, 32, 33, 34, 35, 36
]

我们可以这么转:



  • 元素对应的索引index * 10 -> 角度θ(为什么要乘10呢,因为要凑够360°嘛)
  • 元素对应的值arr[index] -> 极径ρ

按照上面的规则来转的话,那我们就可以在极坐标上得到这37个点(在canvas中Y轴是由上往下的,下面这个图也是按canvas的,但是Y轴我还是画成正常方向,所以这个图其实是反的,但是是有原因的哈):


(0 -> θ = 00°,ρ = 0) (1 -> θ = 10°,ρ = 1) (2 -> θ = 20°,ρ = 2) (3 -> θ = 30°,ρ = 3)
(4 -> θ = 40°,ρ = 4) (5 -> θ = 50°,ρ = 5) (6 -> θ = 60°,ρ = 6) (7 -> θ = 70°,ρ = 7)
(8 -> θ = 80°,ρ = 8) (9 -> θ = 90°,ρ = 9) (10 -> θ = 100°,ρ = 10) (11 -> θ = 110°,ρ = 11)
(12 -> θ = 120°,ρ = 12) (13 -> θ = 130°,ρ = 13) (14 -> θ = 140°,ρ = 14) (15 -> θ = 150°,ρ = 15)
(16 -> θ = 160°,ρ = 16) (17 -> θ = 170°,ρ = 17) (18 -> θ = 180°,ρ = 18) (19 -> θ = 190°,ρ = 19)
(20 -> θ = 200°,ρ = 20) (21 -> θ = 210°,ρ = 21) (22 -> θ = 220°,ρ = 22) (23 -> θ = 230°,ρ = 23)
(24 -> θ = 240°,ρ = 24) (25 -> θ = 250°,ρ = 25) (26 -> θ = 260°,ρ = 26) (27 -> θ = 270°,ρ = 27)
(28 -> θ = 280°,ρ = 28) (29 -> θ = 290°,ρ = 29) (30 -> θ = 300°,ρ = 30) (31 -> θ = 310°,ρ = 31)
(32 -> θ = 320°,ρ = 32) (33 -> θ = 330°,ρ = 33) (34 -> θ = 340°,ρ = 34) (35 -> θ = 350°,ρ = 35)
(36 -> θ = 360°,ρ = 36)

截屏2021-09-05 下午7.11.07.png

有没有发现,跟咱们想实现的最终效果的轨迹很像呢?


截屏2021-09-05 下午6.06.03.png

随机打散

那说完最终的效果,咱们来下想想如何一开始先把数组的各个元素打散在极坐标上呢?其实很简单,咱们可以先把生成一个乱序的数组,比如


const arr = [
25, 8, 32, 1, 19, 14, 0, 29, 17,
6, 7, 26, 3, 30, 31, 16, 28, 15,
24, 10, 21, 2, 9, 4, 35, 5, 36,
33, 11, 27, 34, 22, 13, 18, 23, 12, 20
]

然后还是用上面那个规则,去转换极坐标



  • 元素对应的索引index * 10 -> 角度θ(为什么要乘10呢,因为要凑够360°嘛)
  • 元素对应的值arr[index] -> 极径ρ

那么我们可以的到这37个点,自然就可以实现打散的效果


(25 -> θ = 00°,ρ = 25) (8 -> θ = 10°,ρ = 8) (32 -> θ = 20°,ρ = 32) (1 -> θ = 30°,ρ = 1)
(19 -> θ = 40°,ρ = 19) (14 -> θ = 50°,ρ = 14) (0 -> θ = 60°,ρ = 0) (29 -> θ = 70°,ρ = 29)
(17 -> θ = 80°,ρ = 17) (6 -> θ = 90°,ρ = 6) (7 -> θ = 100°,ρ = 7) (26 -> θ = 110°,ρ = 26)
(3 -> θ = 120°,ρ = 3) (30 -> θ = 130°,ρ = 30) (31 -> θ = 140°,ρ = 31) (16 -> θ = 150°,ρ = 16)
(28 -> θ = 160°,ρ = 28) (15 -> θ = 170°,ρ = 15) (24 -> θ = 180°,ρ = 24) (10 -> θ = 190°,ρ = 10)
(21 -> θ = 200°,ρ = 21) (2 -> θ = 210°,ρ = 2) (9 -> θ = 220°,ρ = 9) (4 -> θ = 230°,ρ = 4)
(35 -> θ = 240°,ρ = 35) (5 -> θ = 250°,ρ = 5) (36 -> θ = 260°,ρ = 36) (33 -> θ = 270°,ρ = 33)
(11 -> θ = 280°,ρ = 11) (27 -> θ = 290°,ρ = 27) (34 -> θ = 300°,ρ = 34) (22 -> θ = 310°,ρ = 22)
(13 -> θ = 320°,ρ = 13) (18 -> θ = 330°,ρ = 18) (23 -> θ = 340°,ρ = 23) (12 -> θ = 350°,ρ = 12)
(20 -> θ = 360°,ρ = 20)

截屏2021-09-05 下午7.32.17.png

实现效果

综上所述,咱们想实现效果,也就有了思路



  • 1、先生成一个乱序数组
  • 2、用canvas画布画出此乱序数组所有元素对应的极坐标对应的点
  • 3、对乱序数组进行排序
  • 4、排序过程中不断清空画布,并重画数组所有元素对应的极坐标对应的点
  • 5、直到排序完成,终止画布操作

截屏2021-09-05 下午7.41.54.png

开搞!!!

咱们,做事情一定要有条有理才行,还记得上面说的步骤吗?



  • 1、先生成一个乱序数组
  • 2、用canvas画布画出此乱序数组所有元素对应的极坐标对应的点
  • 3、对乱序数组进行排序
  • 4、排序过程中不断清空画布,并重画数组所有元素对应的极坐标对应的点
  • 5、直到排序完成,终止画布操作

咱们就按照这个步骤,来一步一步实现效果,兄弟们,冲啊!!!


生成乱序数组

咱们上面举的例子是37个元素,但是37个肯定是太少了,咱们搞多点吧,我搞了这么一个数组nums:我先生成一个0 - 179的有序数组,然后打乱,并塞进数组nums中,此操作我执行4次。为什么是0 - 179,因为0 - 179刚好有180个数字


身位一个程序员,我肯定不可能自己手打这么多元素的啦。。来。。上代码


let nums = []
for (let i = 0; i < 4; i++) {
// 生成一个 0 - 179的有序数组
const arr = [...Array(180).keys()] // Array.keys()可以学一下,很有用
const res = []
while (arr.length) {
// 打乱
const randomIndex = Math.random() * arr.length - 1
res.push(arr.splice(randomIndex, 1)[0])
}
nums = [...nums, ...res]
}

经过上面操作,也就是我的nums中拥有4 * 180 = 720个元素,nums中的元素都是0 - 179范围内的


canvas画乱序数组

画canvas之前,肯定要现在html页面上,编写一个canvas的节点,这里我宽度设置1000,高度也是1000,并且背景颜色是黑色


<canvas id="canvas" width="1000" height="1000" style="background: #000;"></canvas>

上面看到了,极点(原点)是在坐标正中间的,但是canvas的初始原点是在画布的左上角,我们需要把canvas的原点移动到画布的正中间,那正中间的坐标是多少呢?还记得咱们宽高都是1000吗?那画布中心点坐标不就是(500, 500),咱们可以使用canvas的ctx.translate(500, 500)来移动中心点位置。因为咱们画的点都是白色的,所以咱们顺便把ctx.fillStyle设置为white



有一点注意了哈,canvas里的Y轴是自上向下的,与常规的Y轴的相反的。



截屏2021-09-05 下午8.55.39.png

const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
ctx.fillStyle = 'white' // 设置画画的颜色
ctx.translate(500, 500) // 移动中心点到(500, 500)

那到底该怎么画点呢?按照之前的,其实光计算出角度θ极径ρ是不够的,因为canvas画板不认这两个东西啊。。那canvas认啥呢,他只认(x, y),所以咱们只要通过角度θ极径ρ去算出(x, y),就好了,还记得前面极坐标的公式吗



  • x = ρ * cosθ,因为x / ρ = cosθ
  • y = ρ * sinθ,因为y / ρ = sinθ

由于咱们是要铺散点是要铺出一个圆形来,那么一个圆形的角度是0° - 360°,但是我们不要360°,咱们只要0° - 359°,因为0°和360°是同一个直线。咱们一个直线上有一个度数就够了。所以咱们要求出0° - 359°每个角度所对应的cosθ和sinθ(这里咱们只算整数角度,不算小数角度)


const CosandSin = []
for (let i = 0; i < 360; i++) {
const jiaodu = i / 180 * Math.PI
CosandSin.push({ cos: Math.cos(jiaodu), sin: Math.sin(jiaodu) })
}

这时候又有新问题了,咱们一个圆上的整数角度只有0° - 359°360个整数角,但是nums中有720个元素啊,那怎么分配画布呢?很简单啊,一个角度上画2个元素,那不就刚好 2 * 360 = 720


行,咱们废话不多说,开始画初始散点吧。咱们也知道咱们需要画720个点,对于这种多个相同的东西,咱们要多多使用面向对象这种编程思想


// 单个长方形构造函数
function Rect(x, y, width, height) {
this.x = x // 坐标x
this.y = y // 坐标y
this.width = width // 长方形的宽
this.height = height // 长方形的高
}

// 单个长方形的渲染函数
Rect.prototype.draw = function () {
ctx.beginPath() // 开始画一个
ctx.fillRect(this.x, this.y, this.width, this.height) // 画一个
ctx.closePath() // 结束画一个
}

const CosandSin = []
for (let i = 0; i < 360; i++) {
const jiaodu = i / 180 * Math.PI
CosandSin.push({ cos: Math.cos(jiaodu), sin: Math.sin(jiaodu) })
}

function drawAll(arr) {
const rects = [] // 用来存储720个长方形
for (let i = 0; i < arr.length; i++) {
const num = arr[i]
const { cos, sin } = CosandSin[Math.floor(i / 2)] // 一个角画两个
const x = num * cos // x = ρ * cosθ
const y = num * sin // y = ρ * sinθ
rects.push(new Rect(x, y, 5, 3)) // 收集所有长方形
}
rects.forEach(rect => rect.draw()) // 遍历渲染
}
drawAll(nums) // 执行渲染函数

来页面中看看效果吧。此时就完成了初始的散点渲染


截屏2021-09-05 下午6.05.45.png

边排序边重画

其实很简单,就是排序一次,就清空画布,然后重新执行上面的渲染函数drawAll就行了。由于性能原因,我先把drawAll封装成一个Promise函数


function drawAll(arr) {
return new Promise((resolve) => {
setTimeout(() => {
ctx.clearRect(-500, -500, 1000, 1000) // 清空画布
const rects = [] // 用来存储720个长方形
for (let i = 0; i < arr.length; i++) {
const num = arr[i]
const { cos, sin } = CosandSin[Math.floor(i / 2)] // 一个角画两个
const x = num * cos // x = ρ * cosθ
const y = num * sin // y = ρ * sinθ
rects.push(new Rect(x, y, 5, 3)) // 收集所有长方形
}
rects.forEach(rect => rect.draw()) // 遍历渲染
resolve('draw success')
}, 10)
})
}

然后咱们拿一个排序算法例子来讲一讲,就拿个冒泡排序来讲吧


async function bubbleSort(arr) {
var len = arr.length;
for (var i = 0; i < len; i++) {
for (var j = 0; j < len - 1 - i; j++) {
if (arr[j] > arr[j + 1]) { //相邻元素两两对比
var temp = arr[j + 1]; //元素交换
arr[j + 1] = arr[j];
arr[j] = temp;
}
}
await drawAll(arr) // 一边排序一边重新画
}
return arr;
}

然后在页面里放一个按钮,用来执行开始排序


<button id="btn">开始排序</button>

document.getElementById('btn').onclick = function () {
bubbleSort(nums)
}

效果如下,是不是很开心哈哈哈!!!


冒泡排序gift.gif

完整代码

这是完整代码


<canvas id="canvas" width="1000" height="1000" style="background: #000;"></canvas>
<button id="btn">开始排序</button>
复制代码
const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
ctx.fillStyle = 'white' // 设置画画的颜色
ctx.translate(500, 500) // 移动中心点到(500, 500)

let nums = []
for (let i = 0; i < 4; i++) {
// 生成一个 0 - 180的有序数组
const arr = [...Array(180).keys()]
const res = []
while (arr.length) {
// 打乱
const randomIndex = Math.random() * arr.length - 1
res.push(arr.splice(randomIndex, 1)[0])
}
nums = [...nums, ...res]
}

// 单个长方形构造函数
function Rect(x, y, width, height) {
this.x = x // 坐标x
this.y = y // 坐标y
this.width = width // 长方形的宽
this.height = height // 长方形的高
}

// 单个长方形的渲染函数
Rect.prototype.draw = function () {
ctx.beginPath() // 开始画一个
ctx.fillRect(this.x, this.y, this.width, this.height) // 画一个
ctx.closePath() // 结束画一个
}

const CosandSin = []
for (let i = 0; i < 360; i++) {
const jiaodu = i / 180 * Math.PI
CosandSin.push({ cos: Math.cos(jiaodu), sin: Math.sin(jiaodu) })
}

function drawAll(arr) {
return new Promise((resolve) => {
setTimeout(() => {
ctx.clearRect(-500, -500, 1000, 1000) // 清空画布
const rects = [] // 用来存储720个长方形
for (let i = 0; i < arr.length; i++) {
const num = arr[i]
const { cos, sin } = CosandSin[Math.floor(i / 2)] // 一个角画两个
const x = num * cos // x = ρ * cosθ
const y = num * sin // y = ρ * sinθ
rects.push(new Rect(x, y, 5, 3)) // 收集所有长方形
}
rects.forEach(rect => rect.draw()) // 遍历渲染
resolve('draw success')
}, 10)
})
}
drawAll(nums) // 执行渲染函数

async function bubbleSort(arr) {
var len = arr.length;
for (var i = 0; i < len; i++) {
for (var j = 0; j < len - 1 - i; j++) {
if (arr[j] > arr[j + 1]) { //相邻元素两两对比
var temp = arr[j + 1]; //元素交换
arr[j + 1] = arr[j];
arr[j] = temp;
}
}
await drawAll(arr) // 一边排序一边重新画
}
return arr;
}

document.getElementById('btn').onclick = function () {
bubbleSort(nums) // 点击执行
}

正片开始!!!

首先说明,哈哈



  • 我是算法渣渣
  • 每种算法排序,动画都不一样
  • drawAll放在不同地方也可能有不同效果

冒泡排序

async function bubbleSort(arr) {
var len = arr.length;
for (var i = 0; i < len; i++) {
for (var j = 0; j < len - 1 - i; j++) {
if (arr[j] > arr[j + 1]) { //相邻元素两两对比
var temp = arr[j + 1]; //元素交换
arr[j + 1] = arr[j];
arr[j] = temp;
}
}
await drawAll(arr) // 一边排序一边重新画
}
return arr;
}

document.getElementById('btn').onclick = function () {
bubbleSort(nums) // 点击执行
}

冒泡排序gift.gif

选择排序

async function selectionSort(arr) {
var len = arr.length;
var minIndex, temp;
for (var i = 0; i < len - 1; i++) {
minIndex = i;
for (var j = i + 1; j < len; j++) {
if (arr[j] < arr[minIndex]) { //寻找最小的数
minIndex = j; //将最小数的索引保存
}
}
temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
await drawAll(arr)
}
return arr;
}
document.getElementById('btn').onclick = function () {
selectionSort(nums)
}

选择排序gif.gif

插入排序

async function insertionSort(arr) {
if (Object.prototype.toString.call(arr).slice(8, -1) === 'Array') {
for (var i = 1; i < arr.length; i++) {
var key = arr[i];
var j = i - 1;
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key;
await drawAll(arr)
}
return arr;
} else {
return 'arr is not an Array!';
}
}
document.getElementById('btn').onclick = function () {
insertionSort(nums)
}

插入排序gif.gif

堆排序

async function heapSort(array) {
if (Object.prototype.toString.call(array).slice(8, -1) === 'Array') {
//建堆
var heapSize = array.length, temp;
for (var i = Math.floor(heapSize / 2) - 1; i >= 0; i--) {
heapify(array, i, heapSize);
await drawAll(array)
}

//堆排序
for (var j = heapSize - 1; j >= 1; j--) {
temp = array[0];
array[0] = array[j];
array[j] = temp;
heapify(array, 0, --heapSize);
await drawAll(array)
}
return array;
} else {
return 'array is not an Array!';
}
}
function heapify(arr, x, len) {
if (Object.prototype.toString.call(arr).slice(8, -1) === 'Array' && typeof x === 'number') {
var l = 2 * x + 1, r = 2 * x + 2, largest = x, temp;
if (l < len && arr[l] > arr[largest]) {
largest = l;
}
if (r < len && arr[r] > arr[largest]) {
largest = r;
}
if (largest != x) {
temp = arr[x];
arr[x] = arr[largest];
arr[largest] = temp;
heapify(arr, largest, len);
}
} else {
return 'arr is not an Array or x is not a number!';
}
}
document.getElementById('btn').onclick = function () {
heapSort(nums)
}

堆排序gif.gif

快速排序

async function quickSort(array, left, right) {
drawAll(nums)
if (Object.prototype.toString.call(array).slice(8, -1) === 'Array' && typeof left === 'number' && typeof right === 'number') {
if (left < right) {
var x = array[right], i = left - 1, temp;
for (var j = left; j <= right; j++) {
if (array[j] <= x) {
i++;
temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
await drawAll(nums)
await quickSort(array, left, i - 1);
await quickSort(array, i + 1, right);
await drawAll(nums)
}
return array;
} else {
return 'array is not an Array or left or right is not a number!';
}
}
document.getElementById('btn').onclick = function () {
quickSort(nums, 0, nums.length - 1)
}

快排gif.gif

基数排序

async function radixSort(arr, maxDigit) {
var mod = 10;
var dev = 1;
var counter = [];
for (var i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
for (var j = 0; j < arr.length; j++) {
var bucket = parseInt((arr[j] % mod) / dev);
if (counter[bucket] == null) {
counter[bucket] = [];
}
counter[bucket].push(arr[j]);
}
var pos = 0;
for (var j = 0; j < counter.length; j++) {
var value = null;
if (counter[j] != null) {
while ((value = counter[j].shift()) != null) {
arr[pos++] = value;
await drawAll(arr)
}
}
}
}
return arr;
}
document.getElementById('btn').onclick = function () {
radixSort(nums, 3)
}

基数排序gif.gif

希尔排序

async function shellSort(arr) {
var len = arr.length,
temp,
gap = 1;
while (gap < len / 5) { //动态定义间隔序列
gap = gap * 5 + 1;
}
for (gap; gap > 0; gap = Math.floor(gap / 5)) {
for (var i = gap; i < len; i++) {
temp = arr[i];
for (var j = i - gap; j >= 0 && arr[j] > temp; j -= gap) {
arr[j + gap] = arr[j];
}
arr[j + gap] = temp;
await drawAll(arr)
}
}
return arr;
}
document.getElementById('btn').onclick = function () {
shellSort(nums)
}

基数排序gif.gif

参考


总结

如果你觉得此文对你有一丁点帮助,点个赞,鼓励一下林三心哈哈。或者可以加入我的摸鱼群 想进学习群,摸鱼群,请点击这里摸鱼


image.png

作者:Sunshine_Lin
来源:https://juejin.cn/post/7004454008634998821

收起阅读 »

JavaScript复制内容到剪贴板

最近一个活动页面中有一个小需求,用户点击或者长按就可以复制内容到剪贴板,记录一下实现过程和遇到的坑。 常见方法 查了一下万能的Google,现在常见的方法主要是以下两种:第三方库:clipboard.js原生方法:document.execCommand()分...
继续阅读 »

最近一个活动页面中有一个小需求,用户点击或者长按就可以复制内容到剪贴板,记录一下实现过程和遇到的坑。


常见方法


查了一下万能的Google,现在常见的方法主要是以下两种:

  • 第三方库:clipboard.js
  • 原生方法:document.execCommand()

分别来看看这两种方法是如何使用的。


clipboard.js


这是clipboard的官网:clipboardjs.com/,看起来就是这么的简单。


引用


直接引用: <script src="dist/clipboard.min.js"></script>


包: npm install clipboard --save ,然后 import Clipboard from 'clipboard';


使用


从输入框复制


现在页面上有一个 <input> 标签,我们需要复制其中的内容,我们可以这样做:


<input id="demoInput" value="hello world">
<button class="btn" data-clipboard-target="#demoInput">点我复制</button>
import Clipboard from 'clipboard';
const btnCopy = new Clipboard('btn');

注意到,在 <button> 标签中添加了一个 data-clipboard-target 属性,它的值是需要复制的 <input>id,顾名思义是从整个标签中复制内容。


直接复制


有的时候,我们并不希望从 <input> 中复制内容,仅仅是直接从变量中取值。如果在 Vue 中我们可以这样做:


<button class="btn" :data-clipboard-text="copyValue">点我复制</button>
import Clipboard from 'clipboard';
const btnCopy = new Clipboard('btn');
this.copyValue = 'hello world';

事件


有的时候我们需要在复制后做一些事情,这时候就需要回调函数的支持。


在处理函数中加入以下代码:


// 复制成功后执行的回调函数
clipboard.on('success', function(e) {
console.info('Action:', e.action); // 动作名称,比如:Action: copy
console.info('Text:', e.text); // 内容,比如:Text:hello word
console.info('Trigger:', e.trigger); // 触发元素:比如:<button :data-clipboard-text="copyValue">点我复制</button>
e.clearSelection(); // 清除选中内容
});

// 复制失败后执行的回调函数
clipboard.on('error', function(e) {
console.error('Action:', e.action);
console.error('Trigger:', e.trigger);
});

小结


文档中还提到,如果在单页面中使用 clipboard ,为了使得生命周期管理更加的优雅,在使用完之后记得 btn.destroy() 销毁一下。


clipboard 使用起来是不是很简单。但是,就为了一个 copy 功能就使用额外的第三方库是不是不够优雅,这时候该怎么办?那就用原生方法实现呗。


document.execCommand()方法


先看看这个方法在 MDN 上是怎么定义的:



which allows one to run commands to manipulate the contents of the editable region.



意思就是可以允许运行命令来操作可编辑区域的内容,注意,是可编辑区域


定义



bool = document.execCommand(aCommandName, aShowDefaultUI, aValueArgument)



方法返回一个 Boolean 值,表示操作是否成功。

  • aCommandName :表示命令名称,比如: copycut 等(更多命令见命令);
  • aShowDefaultUI:是否展示用户界面,一般情况下都是 false
  • aValueArgument:有些命令需要额外的参数,一般用不到;

兼容性


这个方法在之前的兼容性其实是不太好的,但是好在现在已经基本兼容所有主流浏览器了,在移动端也可以使用。


兼容性


使用


从输入框复制


现在页面上有一个 <input> 标签,我们想要复制其中的内容,我们可以这样做:


<input id="demoInput" value="hello world">
<button id="btn">点我复制</button>
const btn = document.querySelector('#btn');
btn.addEventListener('click', () => {
const input = document.querySelector('#demoInput');
input.select();
if (document.execCommand('copy')) {
document.execCommand('copy');
console.log('复制成功');
}
})

其它地方复制


有的时候页面上并没有 <input> 标签,我们可能需要从一个 <div> 中复制内容,或者直接复制变量。


还记得在 execCommand() 方法的定义中提到,它只能操作可编辑区域,也就是意味着除了 <input><textarea> 这样的输入域以外,是无法使用这个方法的。


这时候我们需要曲线救国。


<button id="btn">点我复制</button>
const btn = document.querySelector('#btn');
btn.addEventListener('click',() => {
const input = document.createElement('input');
document.body.appendChild(input);
input.setAttribute('value', '听说你想复制我');
input.select();
if (document.execCommand('copy')) {
document.execCommand('copy');
console.log('复制成功');
}
document.body.removeChild(input);
})

算是曲线救国成功了吧。在使用这个方法时,遇到了几个坑。


遇到的坑


在Chrome下调试的时候,这个方法时完美运行的。然后到了移动端调试的时候,坑就出来了。


对,没错,就是你,ios。。。




  1. 点击复制时屏幕下方会出现白屏抖动,仔细看是拉起键盘又瞬间收起


    知道了抖动是由于什么产生的就比较好解决了。既然是拉起键盘,那就是聚焦到了输入域,那只要让输入域不可输入就好了,在代码中添加 input.setAttribute('readonly', 'readonly'); 使这个 <input> 是只读的,就不会拉起键盘了。




  2. 无法复制


    这个问题是由于 input.select() 在ios下并没有选中全部内容,我们需要使用另一个方法来选中内容,这个方法就是 input.setSelectionRange(0, input.value.length);




完整代码如下:


const btn = document.querySelector('#btn');
btn.addEventListener('click',() => {
const input = document.createElement('input');
input.setAttribute('readonly', 'readonly');
input.setAttribute('value', 'hello world');
document.body.appendChild(input);
input.setSelectionRange(0, 9999);
if (document.execCommand('copy')) {
document.execCommand('copy');
console.log('复制成功');
}
document.body.removeChild(input);
})


作者:axuebin
链接:https://juejin.cn/post/6844903567480848391

收起阅读 »

你怎么总是能写出两三千行的controller类?

你一定经常见到一个两三千行的 controller 类,类之所以发展成如此庞大,有如下原因: 长函数太多 类里面有特别多的字段和函数 量变引起质变,可能每个函数都很短小,但数量太多 1 程序的modularity 你思考过为什么你不会把all code写到...
继续阅读 »

你一定经常见到一个两三千行的 controller 类,类之所以发展成如此庞大,有如下原因:



  • 长函数太多

  • 类里面有特别多的字段和函数


量变引起质变,可能每个函数都很短小,但数量太多


1 程序的modularity


你思考过为什么你不会把all code写到一个文件?因为你的潜意识里明白:



  • 相同的功能模块无法复用

  • 复杂度远超出个人理解极限


一个人理解的东西是有限的,在国内互联网敏捷开发环境下,更没有人能熟悉所有代码细节。


解决复杂的最有效方案就是分而治之。所以,各种程序设计语言都有自己的模块划分(modularity)方案:



  • 从最初的按文件划分

  • 到后来使用OO按类划分


开发者面对的不再是细节,而是模块,模块数量显然远比细节数量少,理解成本大大降低,开发效率也提高了,再也不用 996, 每天都能和妹纸多聊几句了。


modularity,本质就是分解问题,其背后原因,就是个人理解能力有限。



说这么多我都懂,那到底怎么把大类拆成小类?



2 大类是怎么来的?


2.1 职责不单一


最容易产生大类的原因


CR一段代码:


该类持有大类的典型特征,包含一坨字段:这些字段都缺一不可吗?



  • userId、name、nickname等应该是一个用户的基本信息

  • email、phoneNumber 也算是和用户相关联


很多应用都提供使用邮箱或手机号登录方式,所以,这些信息放在这里,也能理解



  • authorType,作者类型,表示作者是签约作者还是普通作者,签约作者可设置作品的付费信息,但普通作者无此权限

  • authorReviewStatus,作者审核状态,作者成为签约作者,需要有一个申请审核的过程,该状态字段就是审核状态

  • editorType,编辑类型,编辑可以是主编,也可以是小编,权限不同


这还不是 User 类的全部。但只看这些内容就能看出问题:



  • 普通用户既不是作者,也不是编辑


作者和编辑这些相关字段,对普通用户无意义



  • 对那些成为作者的用户,编辑的信息意义不大


因为作者不能成为编辑。编辑也不会成为作者,作者信息对成为编辑的用户无意义


总有一些信息对一部分人毫无意义,但对另一部分人又必需。出现该问题的症结在于只有“一个”用户类。


普通用户、作者、编辑,三种不同角色,来自不同业务方,关心的是不同内容。仅因为它们都是同一系统的用户,就把它们都放到一个用户类,导致任何业务方的需求变动,都会反复修改该类,严重违反单一职责原则
所以破题的关键就是职责拆分。


虽然这是一个类,但它把不同角色关心的东西都放在一起,就愈发得臃肿了。


只需将不同信息拆分即可:


public class User {
private long userId;
private String name;
private String nickname;
private String email;
private String phoneNumber;
...
}

public class Author {
private long userId;
private AuthorType authorType;
private ReviewStatus authorReviewStatus;
...
}

public class Editor {
private long userId;
private EditorType editorType;
...
}

拆出 Author、Editor 两个类,将和作者、编辑相关的字段分别移至这两个类里。
这俩类分别有个 userId 字段,用于关联该角色和具体用户。


2.2 字段未分组


有时觉得有些字段确实都属于某个类,结果就是,这个类还是很大。


之前拆分后的新 User 类:


public class User {
private long userId;
private String name;
private String nickname;
private String email;
private String phoneNumber;
...
}

这些字段应该都算用户信息的一部分。但依然也不算是个小类,因为该类里的字段并不属于同一种类型的信息。
如,userId、name、nickname算是用户的基本信息,而 email、phoneNumber 则属于用户的联系方式。


需求角度看,基本信息是那种一旦确定一般就不变的内容,而联系方式则会根据实际情况调整,如绑定各种社交账号。把这些信息都放到一个类里面,类稳定程度就差点。


据此,可将 User 类的字段分组:


public class User {
private long userId;
private String name;
private String nickname;
private Contact contact;
...
}

public class Contact {
private String email;
private String phoneNumber;
...
}

引入一个 Contact 类(联系方式),把 email 和 phoneNumber 放了进去,后面再有任何关于联系方式的调整就都可以放在这个类里面。
此次调整,把不同信息重新组合,但每个类都比原来要小。


前后两次拆分到底有何不同?



  • 前面是根据职责,拆分出不同实体

  • 后面是将字段做了分组,用类把不同的信息分别封装


大类拆解成小类,本质上是个设计工作,依据单一职责设计原则。


若把大类都拆成小类,类的数量就会增多,那人们理解的成本是不是也会增加呢?
这也是很多人不拆分大类的借口。


各种程序设计语言中,本就有如包、命名空间等机制,将各种类组合在一起。在你不需要展开细节时,面对的是一个类的集合。
再进一步,还有各种程序库把这些打包出来的东西再进一步打包,让我们只要面对简单的接口,而不必关心各种细节。


软件正这样层层封装构建出来的。


作者:JavaEdge在掘金
链接:https://juejin.cn/post/7031442685152100360
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

大红大紫的 Golang 真的是后端开发中的万能药吗?

前言 城外的人想进去,城里的人想出来。-- 钱钟书《围城》 随着容器编排(Container Orchestration)、微服务(Micro Services)、云技术(Cloud Technology)等在 IT 行业不断盛行,2009 年诞生于 Go...
继续阅读 »

前言



城外的人想进去,城里的人想出来。-- 钱钟书《围城》



随着容器编排(Container Orchestration)、微服务(Micro Services)、云技术(Cloud Technology)等在 IT 行业不断盛行,2009 年诞生于 Google 的 Golang(Go 语言,简称 Go)越来越受到软件工程师的欢迎和追捧,成为如今炙手可热的后端编程语言。在用 Golang 开发的软件项目列表中,有 Docker(容器技术)、Kubernetes(容器编排)这样的颠覆整个 IT 行业的明星级产品,也有像 Prometheus(监控系统)、Etcd(分布式存储)、InfluxDB(时序数据库)这样的强大实用的知名项目。当然,Go 语言的应用领域也绝不局限于容器和分布式系统。如今很多大型互联网企业在大量使用 Golang 构建后端 Web 应用,例如今日头条、京东、七牛云等;长期被 Python 统治的框架爬虫领域也因为简单而易用的爬虫框架 Colly 的崛起而不断受到 Golang 的挑战。Golang 已经成为了如今大多数软件工程师最想学习的编程语言。下图是 HackerRank 在 2020 年调查程序员技能的相关结果。


hackerrank-survey-2020


那么,**Go 语言真的是后端开发人员的救命良药呢?它是否能够有效提高程序员们的技术实力和开发效率,从而帮助他们在职场上更进一步呢?Go 语言真的值得我们花大量时间深入学习么?**本文将详细介绍 Golang 的语言特点以及它的优缺点和适用场景,带着上述几个疑问,为读者分析 Go 语言的各个方面,以帮助初入 IT 行业的程序员以及对 Go 感兴趣的开发者进一步了解这个热门语言。


Golang 简介


golang


Golang 诞生于互联网巨头 Google,而这并不是一个巧合。我们都知道,Google 有一个 20% 做业余项目(Side Project)的企业文化,允许工程师们能够在轻松的环境下创造一些具有颠覆性创新的产品。而 Golang 也正是在这 20% 时间中不断孵化出来。Go 语言的创始者也是 IT 界内大名鼎鼎的行业领袖,包括 Unix 核心团队成员 Rob Pike、C 语言作者 Ken Thompson、V8 引擎核心贡献者 Robert Griesemer。Go 语言被大众所熟知还是源于容器技术 Docker 在 2014 年被开源后的爆发式发展。之后,Go 语言因为其简单的语法以及迅猛的编译速度受到大量开发者的追捧,也诞生了很多优秀的项目,例如 Kubernetes。


Go 语言相对于其他传统热门编程语言来说,有很多优点,特别是其高效编译速度天然并发特性,让其成为快速开发分布式应用的首选语言。Go 语言是静态类型语言,也就是说 Go 语言跟 Java、C# 一样需要编译,而且有完备的类型系统,可以有效减少因类型不一致导致的代码质量问题。因此,Go 语言非常适合构建对稳定性灵活性均有要求的大型 IT 系统,这也是很多大型互联网公司用 Golang 重构老代码的重要原因:传统的静态 OOP 语言(例如 Java、C#)稳定性高但缺乏灵活性;而动态语言(例如 PHP、Python、Ruby、Node.js)灵活性强但缺乏稳定性。因此,“熊掌和鱼兼得” 的 Golang,受到开发者们的追捧是自然而然的事情,毕竟,“天下苦 Java/PHP/Python/Ruby 们久矣“。


不过,Go 语言并不是没有缺点。用辩证法的思维方式可以推测,Golang 的一些突出特性将成为它的双刃剑。例如,Golang 语法简单的优势特点将限制它处理复杂问题的能力。尤其是 Go 语言缺乏泛型(Generics)的问题,导致它构建通用框架的复杂度大增。虽然这个突出问题在 2.0 版本很可能会有效解决,但这也反映出来明星编程语言也会有缺点。当然,Go 的缺点还不止于此,Go 语言使用者还会吐槽其啰嗦的错误处理方式(Error Handling)、缺少严格约束的鸭子类型(Duck Typing)、日期格式问题等。下面,我们将从 Golang 语言特点开始,由浅入深多维度深入分析 Golang 的优缺点以及项目适用场景。


语言特点


简洁的语法特征


Go 语言的语法非常简单,至少在变量声明、结构体声明、函数定义等方面显得非常简洁。


变量的声明不像 Java 或 C 那样啰嗦,在 Golang 中可以用 := 这个语法来声明新变量。例如下面这个例子,当你直接使用 := 来定义变量时,Go 会自动将赋值对象的类型声明为赋值来源的类型,这节省了大量的代码。


func main() {
valInt := 1 // 自动推断 int 类型
valStr := "hello" // 自动推断为 string 类型
valBool := false // 自动推断为 bool 类型
}

Golang 还有很多帮你节省代码的地方。你可以发现 Go 中不会强制要求用 new 这个关键词来生成某个类(Class)的新实例(Instance)。而且,对于公共和私有属性(变量和方法)的约定不再使用传统的 publicprivate 关键词,而是直接用属性变量首字母的大小写来区分。下面一些例子可以帮助读者理解这些特点。


// 定义一个 struct 类
type SomeClass struct {
PublicVariable string // 公共变量
privateVariable string // 私有变量
}

// 公共方法
func (c *SomeClass) PublicMethod() (result string) {
return "This can be called by external modules"
}

// 私有方法
func (c *SomeClass) privateMethod() (result string) {
return "This can only be called in SomeClass"
}

func main() {
// 生成实例
someInstance := SomeClass{
PublicVariable: "hello",
privateVariable: "world",
}
}

如果你用 Java 来实现上述这个例子,可能会看到冗长的 .java 类文件,例如这样。


// SomeClass.java
public SomeClass {
public String PublicVariable; // 公共变量
private String privateVariable; // 私有变量

// 构造函数
public SomeClass(String val1, String val2) {
this.PublicVariable = val1;
this.privateVariable = val2;
}

// 公共方法
public String PublicMethod() {
return "This can be called by external modules";
}

// 私有方法
public String privateMethod() {
return "This can only be called in SomeClass";
}
}

...

// Application.java
public Application {
public static void main(String[] args) {
// 生成实例
SomeClass someInstance = new SomeClass("hello", "world");
}
}

可以看到,在 Java 代码中除了容易看花眼的多层花括号以外,还充斥着大量的 publicprivatestaticthis 等修饰用的关键词,显得异常啰嗦;而 Golang 代码中则靠简单的约定,例如首字母大小写,避免了很多重复性的修饰词。当然,Java 和 Go 在类型系统上还是有一些区别的,这也导致 Go 在处理复杂问题显得有些力不从心,这是后话,后面再讨论。总之,结论就是 Go 的语法在静态类型编程语言中非常简洁。


内置并发编程


Go 语言之所以成为分布式应用的首选,除了它性能强大以外,其最主要的原因就是它天然的并发编程。这个并发编程特性主要来自于 Golang 中的协程(Goroutine)和通道(Channel)。下面是使用协程的一个例子。


func asyncTask() {
fmt.Printf("This is an asynchronized task")
}

func syncTask() {
fmt.Printf("This is a synchronized task")
}

func main() {
go asyncTask() // 异步执行,不阻塞
syncTask() // 同步执行,阻塞
go asyncTask() // 等待前面 syncTask 完成之后,再异步执行,不阻塞
}

可以看到,关键词 go 加函数调用可以让其作为一个异步函数执行,不会阻塞后面的代码。而如果不加 go 关键词,则会被当成是同步代码执行。如果读者熟悉 JavaScript 中的 async/awaitPromise 语法,甚至是 Java、Python 中的多线程异步编程,你会发现它们跟 Go 异步编程的简单程度不是一个量级的!


异步函数,也就是协程之间的通信可以用 Go 语言特有的通道来实现。下面是关于通道的一个例子。


func longTask(signal chan int) {
// 不带参数的 for
// 相当于 while 循环
for {
// 接收 signal 通道传值
v := <- signal

// 如果接收值为 1,停止循环
if v == 1 {
break
}

time.Sleep(1 * Second)
}
}

func main() {
// 声明通道
sig := make(chan int)

// 异步调用 longTask
go longTask(sig)

// 等待 1 秒钟
time.Sleep(1 * time.Second)

// 向通道 sig 传值
sig <- 1

// 然后 longTask 会接收 sig 传值,终止循环
}

面向接口编程


Go 语言不是严格的面向对象编程(OOP),它采用的是面向接口编程(IOP),是相对于 OOP 更先进的编程模式。作为 OOP 体系的一部分,IOP 更加强调规则和约束,以及接口类型方法的约定,从而让开发人员尽可能的关注更抽象的程序逻辑,而不是在更细节的实现方式上浪费时间。很多大型项目采用的都是 IOP 的编程模式。如果想了解更多面向接口编程,请查看 “码之道” 个人技术博客的往期文章《为什么说 TypeScript 是开发大型前端项目的必备语言》,其中有关于面向接口编程的详细讲解。


Go 语言跟 TypeScript 一样,也是采用鸭子类型的方式来校验接口继承。下面这个例子可以描述 Go 语言的鸭子类型特性。


// 定义 Animal 接口
interface Animal {
Eat() // 声明 Eat 方法
Move() // 声明 Move 方法
}

// ==== 定义 Dog Start ====
// 定义 Dog 类
type Dog struct {
}

// 实现 Eat 方法
func (d *Dog) Eat() {
fmt.Printf("Eating bones")
}

// 实现 Move 方法
func (d *Dog) Move() {
fmt.Printf("Moving with four legs")
}
// ==== 定义 Dog End ====

// ==== 定义 Human Start ====
// 定义 Human 类
type Human struct {
}

// 实现 Eat 方法
func (h *Human) Eat() {
fmt.Printf("Eating rice")
}

// 实现 Move 方法
func (h *Human) Move() {
fmt.Printf("Moving with two legs")
}
// ==== 定义 Human End ====

可以看到,虽然 Go 语言可以定义接口,但跟 Java 不同的是,Go 语言中没有显示声明接口实现(Implementation)的关键词修饰语法。在 Go 语言中,如果要继承一个接口,你只需要在结构体中实现该接口声明的所有方法。这样,对于 Go 编译器来说你定义的类就相当于继承了该接口。在这个例子中,我们规定,只要既能吃(Eat)又能活动(Move)的东西就是动物(Animal)。而狗(Dog)和人(Human)恰巧都可以吃和动,因此它们都被算作动物。这种依靠实现方法匹配度的继承方式,就是鸭子类型:如果一个动物看起来像鸭子,叫起来也像鸭子,那它一定是鸭子。这种鸭子类型相对于传统 OOP 编程语言显得更灵活。但是,后面我们会讨论到,这种编程方式会带来一些麻烦。


错误处理


Go 语言的错误处理是臭名昭著的啰嗦。这里先给一个简单例子。


package main

import "fmt"

func isValid(text string) (valid bool, err error){
if text == "" {
return false, error("text cannot be empty")
}
return text == "valid text", nil
}

func validateForm(form map[string]string) (res bool, err error) {
for _, text := range form {
valid, err := isValid(text)
if err != nil {
return false, err
}
if !valid {
return false, nil
}
}
return true, nil
}

func submitForm(form map[string]string) (err error) {
if res, err := validateForm(form); err != nil || !res {
return error("submit error")
}
fmt.Printf("submitted")
return nil
}

func main() {
form := map[string]string{
"field1": "",
"field2": "invalid text",
"field2": "valid text",
}
if err := submitForm(form); err != nil {
panic(err)
}
}

虽然上面整个代码是虚构的,但可以从中看出,Go 代码中充斥着 if err := ...; err != nil { ... } 之类的错误判断语句。这是因为 Go 语言要求开发者自己管理错误,也就是在函数中的错误需要显式抛出来,否则 Go 程序不会做任何错误处理。因为 Go 没有传统编程语言的 try/catch 针对错误处理的语法,所以在错误管理上缺少灵活度,导致了 “err 满天飞” 的局面。


不过,辩证法则告诉我们,这种做法也是有好处的。第一,它强制要求 Go 语言开发者从代码层面来规范错误的管理方式,这驱使开发者写出更健壮的代码;第二,这种显式返回错误的方式避免了 “try/catch 一把梭”,因为这种 “一时爽” 的做法很可能导致 Bug 无法准确定位,从而产生很多不可预测的问题;第三,由于没有 try/catch 的括号或额外的代码块,Go 程序代码整体看起来更清爽,可读性较强。


其他


Go 语言肯定还有很多其他特性,但笔者认为以上的特性是 Go 语言中比较有特色的,是区分度比较强的特性。Go 语言其他一些特性还包括但不限于如下内容。



  • 编译迅速

  • 跨平台

  • defer 延迟执行

  • select/case 通道选择

  • 直接编译成可执行程序

  • 非常规依赖管理(可以直接引用 Github 仓库作为依赖,例如 import "github.com/crawlab-team/go-trace"

  • 非常规日期格式(格式为 "2006-01-02 15:04:05",你没看错,据说这就是 Golang 的创始时间!)


优缺点概述


前面介绍了 Go 的很多语言特性,想必读者已经对 Golang 有了一些基本的了解。其中的一些语言特性也暗示了它相对于其他编程语言的优缺点。Go 语言虽然现在很火,在称赞并拥抱 Golang 的同时,不得不了解它的一些缺点。


这里笔者不打算长篇大论的解析 Go 语言的优劣,而是将其中相关的一些事实列举出来,读者可以自行判断。以下是笔者总结的 Golang 语言特性的不完整优缺点对比列表。














































特性 优点 缺点
语法简单 提升开发效率,节省时间 难以处理一些复杂的工程问题
天然支持并发 极大减少异步编程的难度,提高开发效率 不熟悉通道和协程的开发者会有一些学习成本
类型系统
  • Go 语言是静态类型,相对于动态类型语言更稳定和可预测

  • IOP 鸭子类型比严格的 OOP 语言更简洁



    • 没有继承、抽象、静态、动态等特性

    • 缺少泛型,导致灵活性降低

    • 难以快速构建复杂通用的框架或工具


    错误处理 强制约束错误管理,避免 “try/catch 一把梭” 啰嗦的错误处理代码,充斥着 if err := ...
    编译迅速 这绝对是一个优点 怎么可能是缺点?
    非常规依赖管理

    • 可以直接引用发布到 Github 上的仓库作为模块依赖引用,省去了依赖托管的官方网站

    • 可以随时在 Github 上发布 Go 语言编写的第三方模块

    • 自由的依赖发布意味着 Golang 的生态发展将不受官方依赖托管网站的限制


    严重依赖 Github,在 Github 上搜索 Go 语言模块相对不精准
    非常规日期格式 按照 6-1-2-3-4-5(2006-01-02 15:04:05),相对来说比较好记 对于已经习惯了 yyyy-MM-dd HH:mm:ss 格式的开发者来说非常不习惯

    其实,每一个特性在某种情境下都有其相应的优势和劣势,不能一概而论。就像 Go 语言采用的静态类型和面向接口编程,既不缺少类型约束,也不像严格 OOP 那样冗长繁杂,是介于动态语言和传统静态类型 OOP 语言之间的现代编程语言。这个定位在提升 Golang 开发效率的同时,也阉割了不少必要 OOP 语法特性,从而缺乏快速构建通用工程框架的能力(这里不是说 Go 无法构建通用框架,而是它没有 Java、C# 这么容易)。另外,Go 语言 “奇葩” 的错误处理规范,让 Go 开发者们又爱又恨:可以开发出更健壮的应用,但同时也牺牲了一部分代码的简洁性。要知道,Go 语言的设计理念是为了 “大道至简”,因此才会在追求高性能的同时设计得尽可能简单。


    无可否认的是,Go 语言内置的并发支持是非常近年来非常创新的特性,这也是它被分布式系统广泛采用的重要原因。同时,它相对于动辄编译十几分钟的 Java 来说是非常快的。此外,Go 语言没有因为语法简单而牺牲了稳定性;相反,它从简单的约束规范了整个 Go 项目代码风格。因此,**“快”(Fast)、“简”(Concise)、“稳”(Robust)**是 Go 语言的设计目的。我们在对学习 Golang 的过程中不能无脑的接纳它的一切,而是应该根据它自身的特性判断在实际项目应用中的情况。


    适用场景


    经过前文关于 Golang 各个维度的讨论,我们可以得出结论:Go 语言并不是后端开发的万能药。在实际开发工作中,开发者应该避免在任何情况下无脑使用 Golang 作为后端开发语言。相反,工程师在决定技术选型之前应该全面了解候选技术(语言、框架或架构)的方方面面,包括候选技术与业务需求的切合度,与开发团队的融合度,以及其学习、开发、时间成本等因素。笔者在学习了包括前后端的一些编程语言之后,发现它们各自有各自的优势,也有相应的劣势。如果一门编程语言能广为人知,那它绝对不会是一门糟糕语言。因此,笔者不会断言 “XXX 是世界上最好的语言“,而是给读者分享个人关于特定应用场景下技术选型的思路。当然,本文是针对 Go 语言的技术文,接下来笔者将分享一下个人认为 Golang 最适合的应用场景。


    分布式应用


    Golang 是非常适合在分布式应用场景下开发的。分布式应用的主要目的是尽可能多的利用计算资源和网络带宽,以求最大化系统的整体性能和效率,其中重要的需求功能就是并发(Concurrency)。而 Go 是支持高并发异步编程方面的佼佼者。前面已经提到,Go 语言内置了协程(Goroutine)通道(Channel)两大并发特性,这使后端开发者进行异步编程变得非常容易。Golang 中还内置了sync,包含 Mutex(互斥锁)、WaitGroup(等待组)、Pool(临时对象池)等接口,帮助开发者在并发编程中能更安全的掌控 Go 程序的并发行为。Golang 还有很多分布式应用开发工具,例如分布式储存系统(Etcd、SeaweedFS)、RPC 库(gRPC、Thrift)、主流数据库 SDK(mongo-driver、gnorm、redigo)等。这些都可以帮助开发者有效的构建分布式应用。


    网络爬虫


    稍微了解网络爬虫的开发者应该会听说过 Scrapy,再不济也是 Python。市面上关于 Python 网络爬虫的技术书籍数不胜数,例如崔庆才的 《Python 3 网络开发实战》 和韦世东的《Python 3 网络爬虫宝典 用 Python 编写的高性能爬虫框架 Scrapy》,自发布以来一直是爬虫工程师的首选。


    不过,由于近期 Go 语言的迅速发展,越来越多的爬虫工程师注意到用 Golang 开发网路爬虫的巨大优势。其中,用 Go 语言编写的 Colly 爬虫框架,如今在 Github 上已经有 13k+ 标星。其简洁的 API 以及高效的采集速度,吸引了很多爬虫工程师,占据了爬虫界一哥 Scrapy 的部分份额。前面已经提到,Go 语言内置的并发特性让严重依赖网络带宽的爬虫程序更加高效,很大的提高了数据采集效率。另外,Go 语言作为静态语言,相对于动态语言 Python 来说有更好的约束下,因此健壮性和稳定性都更好。


    后端 API


    Golang 有很多优秀的后端框架,它们大部分都非常完备的支持了现代后端系统的各种功能需求:RESTful API、路由、中间件、配置、鉴权等模块。而且用 Golang 写的后端应用性能很高,通常有非常快的响应速度。笔者曾经在开源爬虫管理平台 Crawlab 中用 Golang 重构了 Python 的后端 API,响应速度从之前的几百毫秒优化到了几十毫秒甚至是几毫秒,用实践证明 Go 语言在后端性能方面全面碾压动态语言。Go 语言中比较知名的后端框架有 GinBeegoEchoIris


    当然,这里并不是说用 Golang 写后端就完全是一个正确的选择。笔者在工作中会用到 Java 和 C#,用了各自的主流框架(SpringBoot 和 .Net Core)之后,发现这两门传统 OOP 语言虽然语法啰嗦,但它们的语法特性很丰富,特别是泛型,能够轻松应对一些逻辑复杂、重复性高的业务需求。因此,笔者认为在考虑用 Go 来编写后端 API 时候,可以提前调研一下 Java 或 C#,它们在写后端业务功能方面做得非常棒。


    总结


    本篇文章从 Go 语言的主要语法特性入手,循序渐进分析了 Go 语言作为后端编程语言的优点和缺点,以及其在实际软件项目开发中的试用场景。笔者认为 Go 语言与其他语言的主要区别在于语法简洁天然支持并发面向接口编程错误处理等方面,并且对各个语言特性在正反两方面进行了分析。最后,笔者根据之前的分析内容,得出了 Go 语言作为后端开发编程语言的适用场景,也就是分布式应用网络爬虫以及后端API。当然,Go 语言的实际应用领域还不限于此。实际上,不少知名数据库都是用 Golang 开发的,例如时序数据库 Prometheus 和 InfluxDB、以及有 NewSQL 之称的 TiDB。此外,在机器学习方面,Go 语言也有一定的优势,只是目前来说,Google 因为 Swift 跟 TensorFlow 的意向合作,似乎还没有大力推广 Go 在机器学习方面的应用,不过一些潜在的开源项目已经涌现出来,例如 GoLearn、GoML、Gorgonia 等。


    在理解 Go 语言的优势和适用场景的同时,我们必须意识到 Go 语言并不是全能的。它相较于其他一些主流框架来说也有一些缺点。开发者在准备采用 Go 作为实际工作开发语言的时候,需要全面了解其语言特性,从而做出最合理的技术选型。就像打网球一样,不仅需要掌握正反手,还要会发球、高压球、截击球等技术动作,这样才能把网球打好。


    作者:MarvinZhang
    链接:https://juejin.cn/post/6943524790636544036
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

    写给前端工程师的 Flutter 教程

    最爱折腾的就是前端工程师了,从 jQuery 折腾到 AngularJs,再折腾到 Vue、React。 最爱跨端的也是前端工程师,从 phonegap,折腾到 React Native,这不又折腾到了 Flutter。 图啥? 低成本地为用户带来更优秀的用户...
    继续阅读 »

    最爱折腾的就是前端工程师了,从 jQuery 折腾到 AngularJs,再折腾到 Vue、React。 最爱跨端的也是前端工程师,从 phonegap,折腾到 React Native,这不又折腾到了 Flutter。


    图啥?


    低成本地为用户带来更优秀的用户体验


    目前来说Flutter可能是其中最优秀的一种方案了。


    Flutter 是什么?



    Flutter is Google’s UI toolkit for building beautiful, natively compiled applications for mobile, web, and desktop from a single codebase.



    Flutter是由原 Google Chrome 团队成员,利用 Chrome 2D 渲染引擎,然后精简 CSS 布局演变而来。


    Flutter 架构


    或者更详细的版本




    • Flutter 在各个原生的平台中,使用自己的 C++的引擎渲染界面,没有使用 webview,也不像 RN、NativeScript 一样使用系统的组件。简单来说平台只是给 Flutter 提供一个画布。

    • 界面使用 Dart 语言开发,貌似唯一支持 JIT,和 AOT 模式的强类型语言。

    • 写法非常的现代,声明式,组件化,Composition > inheritance,响应式……就是现在前端流行的这一套 😄

    • 一套代码搞定所有平台。


    Flutter 为什么快?Flutter 相比 RN 的优势在哪里?


    从架构中实际上已经能看出 Flutter 为什么快,至少相比之前的当红炸子鸡 React Native 快的原因了。



    • Skia 引擎,Chrome, Chrome OS,Android,Firefox,Firefox OS 都以此作为渲染引擎。

    • Dart 语言可以 AOT 编译成 ARM Code,让布局以及业务代码运行的最快,而且 Dart 的 GC 针对 Flutter 频繁销毁创建 Widget 做了专门的优化。

    • CSS 的的子集 Flex like 的布局方式,保留强大表现能力的同时,也保留了性能。

    • Flutter 业务书写的 Widget 在渲染之前 diff 转化成 Render Object,对,就像 React 中的 Virtual DOM,以此来确保开发体验和性能。


    而相比 React Native:



    • RN 使用 JavaScript 来运行业务代码,然后 JS Bridge 的方式调用平台相关组件,性能比有损失,甚至平台不同 js 引擎都不一样。

    • RN 使用平台组件,行为一致性会有打折,或者说,开发者需要处理更多平台相关的问题。


    而具体两者的性能测试,可以看这里,结论是 Flutter,在 CPU,FPS,内存稳定上均优于 ReactNative。


    Dart 语言


    在开始 Flutter 之前,我们需要先了解下 Dart 语言……


    Dart 是由 Google 开发,最初是想作为 JavaScript 替代语言,但是失败沉寂之后,作为 Flutter 独有开发语言又焕发了第二春 😂。


    实际上即使到了 2.0,Dart 语法和 JavaScriptFlutter非常的相像。单线程,Event Loop……


    Dart Event Loop模型


    当然作为一篇写给前端工程师的教程,我在这里只想写写 JavaScript 中暂时没有的,Dart 中更为省心,也更“甜”的东西。



    • 不会飘的this

    • 强类型,当然前端现在有了 TypeScript 😬

    • 强大方便的操作符号:

      • ?. 方便安全的foo?.bar取值,如果 foo 为null,那么取值为null

      • ?? condition ? expr1 : expr2 可以简写为expr1 ?? expr2

      • =和其他符号的组合: *=~/=&=|= ……

      • 级联操作符(Cascade notation ..)




    // 想想这样省了多少变量声明
    querySelect('#button')
    ..text ="Confirm"
    ..classes.add('important')
    ..onClick.listen((e) => window.alert('Confirmed'))

    甚至可以重写操作符


    class Vector {
    final int x, y;

    Vector(this.x, this.y);

    Vector operator +(Vector v) => Vector(x + v.x, y + v.y);
    Vector operator -(Vector v) => Vector(x - v.x, y - v.y);

    // Operator == and hashCode not shown. For details, see note below.
    // ···
    }

    void main() {
    final v = Vector(2, 3);
    final w = Vector(2, 2);

    assert(v + w == Vector(4, 5));
    assert(v - w == Vector(0, 1));
    }

    注:重写==,也需要重写 Object hashCodegetter


    class Person {
    final String firstName, lastName;

    Person(this.firstName, this.lastName);

    // Override hashCode using strategy from Effective Java,
    // Chapter 11.
    @override
    int get hashCode {
    int result = 17;
    result = 37 * result + firstName.hashCode;
    result = 37 * result + lastName.hashCode;
    return result;
    }

    // You should generally implement operator == if you
    // override hashCode.
    @override
    bool operator ==(dynamic other) {
    if (other is! Person) return false;
    Person person = other;
    return (person.firstName == firstName &&
    person.lastName == lastName);
    }
    }

    void main() {
    var p1 = Person('Bob', 'Smith');
    var p2 = Person('Bob', 'Smith');
    var p3 = 'not a person';
    assert(p1.hashCode == p2.hashCode);
    assert(p1 == p2);
    assert(p1 != p3);
    }

    这点在 diff 对象的时候尤其有用。


    lsolate


    Dart 运行在独立隔离的 iSolate 中就类似 JavaScript 一样,单线程事件驱动,但是 Dart 也开放了创建其他 isolate,充分利用 CPU 的多和能力。


    loadData() async {
       // 通过spawn新建一个isolate,并绑定静态方法
       ReceivePort receivePort =ReceivePort();
       await Isolate.spawn(dataLoader, receivePort.sendPort);
       
       // 获取新isolate的监听port
       SendPort sendPort = await receivePort.first;
       // 调用sendReceive自定义方法
    List dataList = await sendReceive(sendPort, 'https://hicc.me/posts');
       print('dataList $dataList');
    }

    // isolate的绑定方法
    static dataLoader(SendPort sendPort) async{
       // 创建监听port,并将sendPort传给外界用来调用
       ReceivePort receivePort =ReceivePort();
       sendPort.send(receivePort.sendPort);
       
       // 监听外界调用
       await for (var msg in receivePort) {
         String requestURL =msg[0];
         SendPort callbackPort =msg[1];
       
         Client client = Client();
         Response response = await client.get(requestURL);
         List dataList = json.decode(response.body);
         // 回调返回值给调用者
         callbackPort.send(dataList);
      }    
    }

    // 创建自己的监听port,并且向新isolate发送消息
    Future sendReceive(SendPort sendPort, String url) {
       ReceivePort receivePort =ReceivePort();
       sendPort.send([url, receivePort.sendPort]);
       // 接收到返回值,返回给调用者
       return receivePort.first;
    }

    当然 Flutter 中封装了compute,可以方便的使用,譬如在其它 isolate 中解析大的 json


    Dart UI as Code


    在这里单独提出来的意义在于,从 React 开始,到 Flutter,到最近的 Apple SwiftUI,Android Jetpack Compose 声明式组件写法越发流行,Web 前端使用 JSX 来让开发者更方便的书写,而 Flutter,SwiftUI 则直接从优化语言本身着手。


    函数类的命名参数


    void test({@required int age,String name}) {
    print(name);
    print(age);
    }
    // 解决函数调用时候,参数不明确的问题
    test(name:"hicc",age: 30)

    // 这样对于组件的使用尤为方便
    class MyApp extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
    return Scaffold(
    appBar: AppBar(),
    body: Container(),
    floatingActionButton:FloatingActionButton()
    );
    }
    }

    大杀器:Collection If 和 Collection For


    // collection If
    Widget build(BuildContext context) {
    return Row(
    children: [
    IconButton(icon: Icon(Icons.menu)),
    Expanded(child: title),
    if (!isAndroid)
    IconButton(icon: Icon(Icons.search)),
    ],
    );
    }
    // Collect For
    var command = [
    engineDartPath,
    frontendServer,
    for (var root in fileSystemRoots) "--filesystem-root=$root",
    for (var entryPoint in entryPoints)
    if (fileExists("lib/$entryPoint.json")) "lib/$entryPoint",
    mainPath
    ];

    更多 Dart 2.3 对此的优化看这里


    Flutter 怎么写


    到这里终于到正题了,如果熟悉 web 前端,熟悉 React 的话,你会对下面要讲的异常的熟悉。


    UI=F(state)


    Flutter App 的一切从lib/main.dart文件的 main 函数开始:


    import 'package:flutter/material.dart';

    void main() => runApp(MyApp());

    class MyApp extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
    return MaterialApp(
    title: 'Welcome to Flutter',
    home: Scaffold(
    appBar: AppBar(
    title: Text('Welcome to Flutter'),
    ),
    body: Center(
    child: Text('Hello World'),
    ),
    ),
    );
    }
    }

    Dart 类 build 方法返回的便是 Widget,在 Flutter 中一切都是 Widget,包括但不限于



    • 结构性元素,menu,button 等

    • 样式类元素,font,color 等

    • 布局类元素,padding,margin 等

    • 导航

    • 手势


    Widget 是 Dart 中特殊的类,通过实例化(Dart 中new 是可选的)相互嵌套,你的这个 App 就是形如下图的一颗组件树(Dart 入口函数的概念,main.dart -> main())。


    Flutter Widget Tree


    Widget 布局


    上说过 Flutter 布局思路来自 CSS,而 Flutter 中一切皆 Widget,因此整体布局也很简单:



    • 容器组件 Container

      • decoration 装饰属性,设置背景色,背景图,边框,圆角,阴影和渐变等

      • margin

      • padding

      • alignment

      • width

      • height



    • Padding,Center

    • Row,Column,Flex

    • Wrap, Flow 流式布局

    • stack, z 轴布局

    • ……


    更多可以看这里


    Flutter 中 Widget 可以分为三类,形如 React 中“展示组件”、“容器组件”,“context”。


    StatelessWidget


    这个就是 Flutter 中的“展示组件”,自身不保存状态,外部参数变化就销毁重新创建。Flutter 建议尽量使用无状态的组件。


    StatefulWidget


    状态组件就是类似于 React 中的“容器组件”了,Flutter 中状态组件写法会稍微不一样。


    class Counter extends StatefulWidget {
    // This class is the configuration for the state. It holds the
    // values (in this case nothing) provided by the parent and used by the build
    // method of the State. Fields in a Widget subclass are always marked "final".

    @override
    _CounterState createState() => _CounterState();
    }

    class _CounterState extends State {
    int _counter = 0;

    void _increment() {
    setState(() {
    // This call to setState tells the Flutter framework that
    // something has changed in this State, which causes it to rerun
    // the build method below so that the display can reflect the
    // updated values. If you change _counter without calling
    // setState(), then the build method won't be called again,
    // and so nothing would appear to happen.
    _counter++;
    });
    }

    @override
    Widget build(BuildContext context) {
    // This method is rerun every time setState is called, for instance
    // as done by the _increment method above.
    // The Flutter framework has been optimized to make rerunning
    // build methods fast, so that you can just rebuild anything that
    // needs updating rather than having to individually change
    // instances of widgets.
    return Row(
    children: [
    RaisedButton(
    onPressed: _increment,
    child: Text('Increment'),
    ),
    Text('Count: $_counter'),
    ],
    );
    }
    }

    可以看到 Flutter 中直接使用了和 React 中同名的setState方法,不过不会有变量合并的东西,当然也有生命周期


    Flutter StatefulWidget 声明周期


    可以看到一个有状态的组件需要两个 Class,这样写的原因在于,Flutter 中 Widget 都是 immmutable 的,状态组件的状态保存在 State 中,组件仍然每次重新创建,Widget 在这里只是一种对组件的描述,Flutter 会 diff 转换成 Element,然后转换成 RenderObject 才渲染。


    Flutter render object


    Flutter Widget 更多的渲染流程可以看这里


    实际上 Widget 只是作为组件结构一种描述,还可以带来的好处是,你可以更方便的做一些主题性的组件, Flutter 官方提供的Material Components widgetsCupertino (iOS-style) widgets质量就相当高,再配合 Flutter 亚秒级的Hot Reload,开发体验可以说挺不错的。




    State Management


    setState()可以很方便的管理组件内的数据,但是 Flutter 中状态同样是从上往下流转的,因此也会遇到和 React 中同样的问题,如果组件树太深,逐层状态创建就显得很麻烦了,更不要说代码的易读和易维护性了。


    InheritedWidget


    同样 Flutter 也有个context一样的东西,那就是InheritedWidget,使用起来也很简单。


    class GlobalData extends InheritedWidget {
    final int count;
    GlobalData({Key key, this.count,Widget child}):super(key:key,child:child);

    @override
    bool updateShouldNotify(GlobalData oldWidget) {
    return oldWidget.count != count;
    }

    static GlobalData of(BuildContext context) => context.inheritFromWidgetOfExactType(GlobalData);
    }

    class MyApp extends StatelessWidget {
    // This widget is the root of your application.
    @override
    Widget build(BuildContext context) {
    return MaterialApp(
    title: 'Flutter Demo',
    theme: ThemeData(
    primarySwatch: Colors.blue,
    ),
    home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
    }
    }

    class MyHomePage extends StatefulWidget {
    MyHomePage({Key key, this.title}) : super(key: key);

    final String title;

    @override
    _MyHomePageState createState() => _MyHomePageState();
    }

    class _MyHomePageState extends State {
    int _counter = 0;

    void _incrementCounter() {
    _counter++;
    });
    }

    @override
    Widget build(BuildContext context) {
    return Scaffold(
    appBar: AppBar(
    title: Text(widget.title),
    ),
    body: GlobalData(
    count: _counter,
    child: Center(
    child: Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: [
    Text(
    'You have pushed the button this many times:',
    ),
    Text(
    '$_counter',
    style: Theme.of(context).textTheme.display1,
    ),
    Body(),
    Body2()
    ],
    ),
    ),
    ),
    floatingActionButton: FloatingActionButton(
    onPressed: _incrementCounter,
    tooltip: 'Increment',
    child: Icon(Icons.add),
    ),
    );
    }
    }

    class Body extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
    GlobalData globalData = GlobalData.of(context);
    return Text(globalData.count.toString());
    }
    }

    class Body2 extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
    // TODO: implement build
    GlobalData globalData = GlobalData.of(context);
    return Text(globalData.count.toString());
    }

    具体实现原理可以参考这里,不过 Google 封装了一个更为上层的库provider,具体使用可以看这里


    BlOC


    BlOC是 Flutter team 提出建议的另一种更高级的数据组织方式,也是我最中意的方式。简单来说:


    Bloc = InheritedWidget + RxDart(Stream)


    Dart 语言中内置了 Steam,Stream ~= Observable,配合RxDart, 然后加上StreamBuilder会是一种异常强大和自由的模式。


    class GlobalData extends InheritedWidget {
    final int count;
    final Stream timeInterval$ = new Stream.periodic(Duration(seconds: 10)).map((time) => new DateTime.now().toString());
    GlobalData({Key key, this.count,Widget child}):super(key:key,child:child);

    @override
    bool updateShouldNotify(GlobalData oldWidget) {
    return oldWidget.count != count;
    }

    static GlobalData of(BuildContext context) => context.inheritFromWidgetOfExactType(GlobalData);

    }

    class TimerView extends StatelessWidget {

    @override
    Widget build(BuildContext context) {
    GlobalData globalData = GlobalData.of(context);
    return StreamBuilder(
    stream: globalData.timeInterval$,
    builder: (context, snapshot) {
    return Text(snapshot?.data ?? '');
    }
    );
    }
    }

    当然 Bloc 的问题在于



    • 学习成本略高,Rx 的概念要吃透,不然你会抓狂

    • 自由带来的问题是,可能代码不如 Redux 类的规整。


    顺便,今年 Apple 也拥抱了响应式,Combine(Rx like) + SwiftUI 也基本等于 Bloc 了。


    所以,Rx 还是要赶紧学起来 😬


    除去 Bloc,Flutter 中还是可以使用其他的方案,譬如:



    展开来说现在的前端开发使用强大的框架页面组装已经不是难点了。开发的难点在于如何组合富交互所需的数据,也就是上面图中的state部分。


    更具体来说,是怎么优雅,高效,易维护地处理短暂数据(ephemeral state)setState()和需要共享的 App State 的问题,这是个工程性的问题,但往往也是日常开发最难的事情了,引用 Redux 作者 Dan 的一句:



    “The rule of thumb is:Do whatever is less awkward.”



    到这里,主要的部分已经讲完了,有这些已经可以开发出一个不错的 App 了。剩下的就当成一个 bonus 吧。




    测试


    Flutter debugger,测试都是出场自带,用起来也不难。


    // 测试在/test/目录下面
    void main() {

    testWidgets('Counter increments smoke test', (WidgetTester tester) async {
    // Build our app and trigger a frame.
    await tester.pumpWidget(MyApp());

    // Verify that our counter starts at 0.
    expect(find.text('0'), findsOneWidget);
    expect(find.text('1'), findsNothing);

    // Tap the '+' icon and trigger a frame.
    await tester.tap(find.byIcon(Icons.add));
    await tester.pump();

    // Verify that our counter has incremented.
    expect(find.text('0'), findsNothing);
    expect(find.text('1'), findsOneWidget);
    });
    }

    包管理,资源管理


    类似与 JavaScript 的 npm,Flutter,也就是 Dart 也有自己的包仓库。不过项目包的依赖使用 yaml 文件来描述:


    name: app
    description: A new Flutter project.
    version: 1.0.0+1

    environment:
    sdk: ">=2.1.0 <3.0.0"

    dependencies:
    flutter:
    sdk: flutter

    cupertino_icons: ^0.1.2

    生命周期


    移动应用总归需要应用级别的生命周期,flutter 中使用生命周期钩子,也非常的简单:


    class MyApp extends StatefulWidget {
    @override
    _MyAppState createState() => new _MyAppState();
    }

    class _MyAppState extends State with WidgetsBindingObserver {
    @override
    void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    }

    @override
    void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
    }

    @override
    void didChangeAppLifecycleState(AppLifecycleState state) {
    switch (state) {
    case AppLifecycleState.inactive:
    print('AppLifecycleState.inactive');
    break;
    case AppLifecycleState.paused:
    print('AppLifecycleState.paused');
    break;
    case AppLifecycleState.resumed:
    print('AppLifecycleState.resumed');
    break;
    case AppLifecycleState.suspending:
    print('AppLifecycleState.suspending');
    break;
    }
    super.didChangeAppLifecycleState(state);
    }

    @override
    Widget build(BuildContext context) {
    return Container();
    }
    }

    使用原生能力


    和 ReactNative 类似,Flutter 也是使用类似事件的机制来使用平台相关能力。


    Flutter platform channels


    Flutter Web, Flutter Desktop


    这些还在开发当中,鉴于对 Dart 喜欢,以及对 Flutter 性能的乐观,这些倒是很值得期待。


    Flutter web 架构


    还记得平台只是给 Flutter 提供一个画布么,Flutter Desktop 未来更是可以大有可为 😄,相关可以看这里


    最后每种方案,每种技术都有优缺点,甚至技术的架构决定了,有些缺陷可能永远都没法改进,所以 🤔


    作者:小刀c
    链接:https://juejin.cn/post/6844903906800041998
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

    Flutter | 求求你们了,切换 Widget 的时候加上动画吧

    平时我们在切换 Widget 的时候是怎样的呢?有没有动画效果?是不是直接改变了一个 Widget?类似于这样的:如果是的话,那么今天所说的 Widget,绝对符合你的口味。那如何在 Flutter 当中切换 Widget 的时候加上特效?完成这样的效果?An...
    继续阅读 »

    平时我们在切换 Widget 的时候是怎样的呢?

    有没有动画效果?是不是直接改变了一个 Widget?

    类似于这样的:

    如果是的话,那么今天所说的 Widget,绝对符合你的口味。

    那如何在 Flutter 当中切换 Widget 的时候加上特效?完成这样的效果?

    AnimatedSwitcher 了解一下。

    AnimatedSwitcher

    官方介绍

    话不多说,功能我们已经了解,再来看一下官方的介绍:

    A widget that by default does a FadeTransition between a new widget and the widget previously set on the AnimatedSwitcher as a child.

    If they are swapped fast enough (i.e. before duration elapses), more than one previous child can exist and be transitioning out while the newest one is transitioning in.

    If the "new" child is the same widget type and key as the "old" child, but with different parameters, then AnimatedSwitcher will not do a transition between them, since as far as the framework is concerned, they are the same widget and the existing widget can be updated with the new parameters. To force the transition to occur, set a Key on each child widget that you wish to be considered unique (typically a ValueKey on the widget data that distinguishes this child from the others).

    大致意思就是:

    默认情况下是执行透明度的动画。

    如果交换速度足够快,则存在多个子级,但是在新子级传入的时候将它移除。

    如果新 Widget 和 旧 Widget 的类型和键相同,但是参数不同,那么也不会进行转换。如果想要进行转换,那么要添加一个 Key。

    构造函数

    再来看构造函数,来确定如何使用:

    const AnimatedSwitcher({
    Key key,
    this.child,
    @required this.duration,
    this.reverseDuration,
    this.switchInCurve = Curves.linear,
    this.switchOutCurve = Curves.linear,
    this.transitionBuilder = AnimatedSwitcher.defaultTransitionBuilder,
    this.layoutBuilder = AnimatedSwitcher.defaultLayoutBuilder,
    }) : assert(duration != null),
    assert(switchInCurve != null),
    assert(switchOutCurve != null),
    assert(transitionBuilder != null),
    assert(layoutBuilder != null),
    super(key: key);
    复制代

    来解释一下每个参数:

    1. child:不用多说
    2. duration:动画持续时间
    3. reverseDuration:从新的 Widget 到旧的 Widget 动画持续时间,如果不设置则为 duration 的值
    4. switchInCurve:动画效果
    5. switchOutCurve:同上
    6. transitionBuilder:设置一个新的转换动画
    7. layoutBuilder:包装新旧 Widget 的组件,默认是一个 Stack

    其中必要参数就是一个 duration,那既然知道如何使用了,那就开撸。

    简单例子

    前面我们看的图,就是在对 AppBar上的 actions 进行操作,

    其实这个例子在实际开发当中经常存在,肯定要删除一些东西的嘛,然后选中了以后批量删除。

    那这里也不多说,直接上代码,然后解释:

    class _AnimatedSwitcherPageState extends State<AnimatedSwitcherPage> {
    IconData _actionIcon = Icons.delete;

    @override
    void initState() {
    super.initState();
    }

    @override
    Widget build(BuildContext context) {
    return Scaffold(
    appBar: AppBar(
    title: Text('AnimatedSwitcherPage'),
    actions: <Widget>[
    AnimatedSwitcher(
    transitionBuilder: (child, anim){
    return ScaleTransition(child: child,scale: anim);
    },
    duration: Duration(milliseconds: 300),
    child: IconButton(
    key: ValueKey(_actionIcon),
    icon: Icon(_actionIcon),
    onPressed: () {
    setState(() {
    if (_actionIcon == Icons.delete)
    _actionIcon = Icons.done;
    else
    _actionIcon = Icons.delete;
    });
    }),
    )
    ],
    ),
    body: Container());
    }
    }
    复制代

    我们定义的是一个 StatefulWidget,因为在切换 Widget 的时候要调用 setState()

    下面来说一下整个流程:

    1. 首先定义好我们初始化的 Icon的数据为 Icons.delete
    2. 在 AppBar 的 actions 里面加入 AnimatedSwitcher
    3. 设置 transitionBuilder 为 缩放动画 ScaleTransition
    4. 给 AnimatedSwitcher 的 child 为 IconButton
    5. 因为前面官方文档说过,如果 Widget 类型一样,只是数据不一样,那么想要动画,就必须添加 Key。
    6. 所以我们给 IconButton 添加了一个 ValueKey,值就为定义好的 IconData
    7. 最后在点击事件中切换两个 Icon 就完成了

    最后再看一下效果:

    总结

    使用该控件最应该注意的点就是 Key 的问题,一定要记住:

    如果新 Widget 和 旧 Widget 的类型和键相同,但是参数不同,那么也不会进行转换。如果想要进行转换,那么要添加一个 Key。

    完整代码已经传至GitHub:github.com/wanglu1209/…


    作者:Flutter笔记
    链接:https://juejin.cn/post/6844903897912311821
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    收起阅读 »

    苍老师的 "码" 是怎么打上的

    --OpenCV初体验,Swift和C++混编 文档更新说明 2017年10月27日 v1.0 初稿 2017年10月28日 v1.1 添加Objective-C++编译方法 ...
    继续阅读 »

    --OpenCV初体验,Swift和C++混编


    文档更新说明



    • 2017年10月27日 v1.0 初稿

    • 2017年10月28日 v1.1 添加Objective-C++编译方法



    提到OpenCV,相信大多数人都听说过,应用领域非常广泛,使用C++开发,天生具有跨平台的优势,我们学习一次,就可以在各个平台使用,这个还是很具有诱惑力的
    本文主要记录我第一次使用OpenCV,在iOS开发平台上面搭建开发环境,并且实现一个简单的马赛克功能
    开发环境:Swift4XCode 9.0



    1、什么是OpenCV?



    • 由英特尔公司于1999年发起并参与开发,至今已有18年历史

    • OpenCV的全称是Open Source Computer Vision Library

    • 是一个跨平台开源计算机视觉库,可用于开发实时的图像处理计算机视觉以及模式识别程序。

    • 支持C/C++JavaPythonOCSwiftRuby等等语言

    • 支持WindowsAndroidMaemoFreeBSDOpenBSDiOSLinuxMac OS


    2、难点,思路



    • 由于我们使用的是Swift,由于目前还不能在Swift中使用C++的类,所以我们得想一个方法,在Swift中调用C++的类

    • 其实方法很简单,Swift天生具有跟Objective-C++混编的能力,而Objective-C++里面是可以直接使用C++的类的,上面的问题也就解决了


    swift-c++handle


    3、马赛克原理



    • 其实把图片的像素密度调低,就可以出现马赛克效果了

    • 开始做马赛克之前,需要定一个马赛克的级别,表示原图中每几个像素变成新图里面的一个像素

    • 取一小块区域左上角的一个像素,并把这个像素填充到整个小区域内

    • 如下图,左边是原图,右边是经过变换之后的图,假设马赛克级别为3,每个数字表示的区域就是处理的一个小单元,取这个最小单元左上角的颜色,填充整个小单元就OK了


    马赛克原理


    4、开动工程


    4.1、搭建c++和swift混编环境



    我们首先要搭建一个c++的环境,然后才能进行c++的开发,而c++环境可以通过iostream里面的cout函数验证




    1. 首先,我们使用xCode新建一个swiftiOS项目

    2. 在工程内,新建一个Objective-C类,继承NSObject,这里会自动提示我们是否为项目添加桥接文件,选择添加即可(桥接文件是用来向Swift暴露Objective-C方法的)


    3. 因为我们要使用Objective-C++,而把Objective-C转成Objective-C++的方法有两种



      • .m文件的后缀名改为.mm,xCode就会自动识别我们的代码为Objective-C++了(xCode会通过后缀名自动识别源文件类型)


      • 选中要修改的.m文件,在右边的Type属性修改成:Objective-C++ Source(也可以手动指定源文件类型)







    4. 在刚才的.mm文件中,添加一个测试方法,在这里测试一下C++环境是否搭建成功


      #import "MyUtil.h"
      #import <iostream> // 记得导入iostrem头文件

      using namespace std;

      @implementation MyUtil

      + (void)testCpp {
      cout << "Hello Swift and Cpp" << endl;
      }


    5. 在前面xCode自动创建的桥接文件中暴露我们的测试方法头文件




    6. Swift中调用测试方法,控制台输出 "Hello Swift and Cpp" 就正常了


      import UIKit

      class ViewController: UIViewController {
      override func viewDidLoad() {
      super.viewDidLoad()
      // 测试方法
      MyUtil.testCpp()
      }
      }


    4.3、导入OpenCV动态库



    iOS开发中导入OpenCV的库其实非常简单,直接拖拽到工程文件就行了




    1. 首先去OpenCV官网下载我们需要的framework,下载地址:opencv.org/releases.ht…,选择最新版本的iOS pack即可


    2. 下载下来之后解压,然后拖拽到我们的工程目录,设置如下图




    3. 设置我们的工程链接OpenCV动态库




    4. build一下,确认不会报错




    4.4、实现马赛克函数



    接下来就是干代码的时候了





    1. 首先要在.m文件中,导入OpenCV的头文件,导入头文件之后代码如下,这里有几个坑要注意:



      • 不要在.h文件中去导入OpenCV的相关头文件,否则会报错,错误信息: Core.hpp header must be compiled as C++,看到这个问题,赶紧把头文件移动到.m文件中去

      • 还有就是OpenCV的头文件最好放在#import <UIKit/UIKit.h>之前,否则也会报一个错误: enum { NO, FEATHER, MULTI_BAND }; Expected identifier


      //导入OpenCV框架 最好放在Foundation.h UIKit.h之前
      //核心头文件
      #import <opencv2/opencv.hpp>
      //对iOS支持
      #import <opencv2/imgcodecs/ios.h>
      //导入矩阵帮助类
      #import <opencv2/highgui.hpp>
      #import <opencv2/core/types.hpp>

      #import "MyUtil.h"
      #import <iostream>

      using namespace std;
      using namespace cv;


    2. 实现马赛克函数


      +(UIImage*)opencvImage:(UIImage*)image level:(int)level{
      //实现功能
      //第一步:将iOS图片->OpenCV图片(Mat矩阵)
      Mat mat_image_src;
      UIImageToMat(image, mat_image_src);

      //第二步:确定宽高
      int width = mat_image_src.cols;
      int height = mat_image_src.rows;

      //在OpenCV里面,必须要先把ARGB的颜色空间转换成RGB的,否则处理会失败(官方例程里面,每次处理都会有这个操作)
      //ARGB->RGB
      Mat mat_image_dst;
      cvtColor(mat_image_src, mat_image_dst, CV_RGBA2RGB, 3);

      //为了不影响原始图片,克隆一张保存
      Mat mat_image_clone = mat_image_dst.clone();

      //第三步:马赛克处理
      int xMax = width - level;
      int yMax = height - level;

      for (int y = 0; y <= yMax; y += level) {
      for (int x = 0; x <= xMax; x += level) {
      //让整个矩形区域颜色值保持一致
      //mat_image_clone.at<Vec3b>(i, j)->像素点(颜色值组成->多个)->ARGB->数组
      //mat_image_clone.at<Vec3b>(i, j)[0]->R值
      //mat_image_clone.at<Vec3b>(i, j)[1]->G值
      //mat_image_clone.at<Vec3b>(i, j)[2]->B值
      Scalar scalar = Scalar(
      mat_image_clone.at<Vec3b>(y, x)[0],
      mat_image_clone.at<Vec3b>(y, x)[1],
      mat_image_clone.at<Vec3b>(y, x)[2]);

      //取出要处理的矩形区域
      Rect2i mosaicRect = Rect2i(x, y, level, level);
      Mat roi = mat_image_dst(mosaicRect);

      //将前面处理的小区域拷贝到要处理的区域
      //CV_8UC3的含义
      //CV_:表示框架命名空间
      //8表示:32位色->ARGB->8位 = 1字节 -> 4个字节
      //U: 无符号类型
      //C分析:char类型
      //3表示:3个通道->RGB
      Mat roiCopy = Mat(mosaicRect.size(), CV_8UC3, scalar);
      roiCopy.copyTo(roi);
      }
      }

      //第四步:将OpenCV图片->iOS图片
      return MatToUIImage(mat_image_dst);
      }


    4.5、在swift中调用马赛克函数



    函数已经实现了,接下来就是在Swift中调用了





    1. 为了便于测试,我们在storyboard中搭一个简单的界面,在按钮中切换马赛克图片和原图,界面如下:
      苍井空




    2. 在按钮点击事件中调用上面的马赛克函数即可


      @IBOutlet weak var imageView: UIImageView!
      /// 显示原图按钮
      @IBAction func origImageBtnClick(_ sender: Any) {
      imageView.image = UIImage(named: "pic.jpg")
      }

      /// 显示马赛克图片
      @IBAction func mosaicImageBtnClick(_ sender: Any) {
      guard let origImage = imageView.image else {
      return
      }

      let mosaicImage = MyUtil.opencvImage(origImage, level: 20)
      imageView.image = mosaicImage
      }


    3. 效果如下,左边的是原图,右边的是马赛克之后的图片,就这样,苍老师的码就打上去啦~





    5、后记


    对于C++,很多人并不陌生,不过我想对于iOS开发者来说,用过C++的童鞋并不多吧,我一直很崇拜那些C++大神,因为通过C++,我们可以很方便的实现跨平台开发,就我们今天的马赛克代码来说,移植到安卓平台,里面的东西也只需要做很小部分的修改,就可以非常完美的适配(当然,安卓的开发环境么有iOS这么简单),所以,掌握和使用C++的性价比还是很高的。


    完整代码已经上传到github: github.com/fengqiangbo…,不过移除了OpenCV.framework,因为太多传不上去,欢迎大家给Star

    作者:加勒比海带王
    链接:https://juejin.cn/post/6844903506944458760
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

    Jetpack Compose 动画初步了解和使用

    Animatable compose 使用 Animatable 来实现动画效果,Animatable 可以理解为一个可以作为动画属性的 Value 持有者。当它持有的 Value 通过 animateTo 更新时,可以自动以动画的形式对这一过程进行演变。与传...
    继续阅读 »

    Animatable


    compose 使用 Animatable 来实现动画效果,Animatable 可以理解为一个可以作为动画属性的 Value 持有者。当它持有的 Value 通过 animateTo 更新时,可以自动以动画的形式对这一过程进行演变。与传统基于 View 实现的动画不同, 其内部使用协程计算动画的中间过程,所以触发函数 animateTo() 是用suspend 这大大保障了动画运行时的性能。基本的使用方式:


    @Composable
    fun Demo() {
    var flag by remember { mutableStateOf(false) }
    Box(
    Modifier
    .fillMaxSize()
    .background(Color.DarkGray),
    contentAlignment = Alignment.Center
    ) {
    val animate = remember { Animatable(32.dp, Dp.VectorConverter) }
    // 通过协程触发 animateTo()
    LaunchedEffect(key1 = flag) {
    animate.animateTo(if (flag) 32.dp else 144.dp)
    }
    Row(
    Modifier
    .size(animate.value) // size 在 animate 中取值
    .background(Color.Magenta)
    .clickable { flag = !flag }
    ) {}
    }
    }

    首先看 Animatable :


    androidx.compose.animation.core.Animatable

    public constructor Animatable<T, V : AnimationVector>(
    initialValue: T,
    typeConverter: TwoWayConverter<T, V>,
    visibilityThreshold: T?
    )


    • initialValue 很好理解,作为它的初始值传入,所谓的 Value 持有者持有的就是它。

    • typeConverter 是用来统一动画行为,可以做属性动画的值都通过这个converter 把不同类型的值都转化成 Float 进行动画计算,与对应的 AnimationVector 进行互相转化。

    • visibilityThreshold 判断动画逐渐变为目标值得阈值,可空,暂且按下不表。


    详细了解一下其中的 TwoWayConverter


    /**
    * [TwoWayConverter] class contains the definition on how to convert from an arbitrary type [T]
    * to a [AnimationVector], and convert the [AnimationVector] back to the type [T]. This allows
    * animations to run on any type of objects, e.g. position, rectangle, color, etc.
    */
    interface TwoWayConverter<T, V : AnimationVector> {
    /**
    * Defines how a type [T] should be converted to a Vector type (i.e. [AnimationVector1D],
    * [AnimationVector2D], [AnimationVector3D] or [AnimationVector4D], depends on the dimensions of
    * type T).
    */
    val convertToVector: (T) -> V
    /**
    * Defines how to convert a Vector type (i.e. [AnimationVector1D], [AnimationVector2D],
    * [AnimationVector3D] or [AnimationVector4D], depends on the dimensions of type T) back to type
    * [T].
    */
    val convertFromVector: (V) -> T
    }

    TwoWayConverter 用于定义如何把任意类型的值与可供动画使用的 AnimationVector 之前互相转化的方法,这样通过对它的封装就可以进行对任意属性类型做统一的动画计算。同时,根据动画所需的维度数据返回对应维度的封装 AnimationVectorXD ,这里所说的XD 是指数据维度的个数。例如:



    • androidx.compose.ui.unit.Dp 值转化为 AnimationVector 只有一个维度,也就是它的 value ,所以转化为与之对应的 AnimationVector1D

    • androidx.compose.ui.geometry.Size 中包含两个维度的数据:widthheight , 所以对转化为 AnimationVector2D

    • androidx.compose.ui.geometry.Rect 中包含四个数据维度:lefttoprightbottom,对应 AnimationVector4D


    同时,Compose 还对常用与动画的对象非常贴心的做了默认实现:



    • Float.Companion.VectorConverter: TwoWayConverter<Float, AnimationVector1D>

    • Int.Companion.VectorConverter: TwoWayConverter<Int, AnimationVector1D>

    • Rect.Companion.VectorConverter: TwoWayConverter<Rect, AnimationVector4D>

    • Dp.Companion.VectorConverter: TwoWayConverter<Dp, AnimationVector1D>

    • DpOffset.Companion.VectorConverter: TwoWayConverter<DpOffset, AnimationVector2D>

    • Size.Companion.VectorConverter: TwoWayConverter<Size, AnimationVector2D>

    • Offset.Companion.VectorConverter: TwoWayConverter<Offset, AnimationVector2D>

    • IntOffset.Companion.VectorConverter: TwoWayConverter<IntOffset, AnimationVector2D>

    • IntSize.Companion.VectorConverter: TwoWayConverter<IntSize, AnimationVector2D>


    至此,Animatable 有了初始值, 也有了值类型与对应动画数据的转换方式,那么只需要一个目标值,就满足触发动画的条件了。又因为动画数据的计算在协程中进行,那么我们此时只需在协程中触发 animateTo() 就可以了:


    // 通过协程触发 animateTo()
    LaunchedEffect(key = flag) {
    animate.animateTo(if (flag) 32.dp else 144.dp)
    }

    注意此处的协程 CoroutineScope 是通过 Composable 函数 LaunchedEffect 提供的,该函数内部实现了对于 composer 的优化,同时通过 remember 函数缓存状态,所以不会由于 recompose 的主动或被动调用而多次执行。


    AnimationSpec


    AnimationSpec 顾名思义支持对动画定义规范,以此实现自定义动画。


    查看 animateTo 函数的定义可以发现其第二个参数可以设置 animationSpec,它有一个默认的实现 defaultSpringSpec ,所以上面的例子中没有明确指定 animationSpec


    androidx.compose.animation.core.Animatable 
    public final suspend fun animateTo(
    targetValue: T,
    animationSpec: AnimationSpec<T> = defaultSpringSpec,
    initialVelocity: T = velocity,
    block: (Animatable<T, V>.() → Unit)? = null
    ): AnimationResult<T, V>

    spring


    defaultSpringSpec 是一个通过 spring 创建的基于弹簧的物理特性的动画:


    androidx.compose.animation.core AnimationSpec.kt 
    @Stable
    public fun <T> spring(
    dampingRatio: Float = Spring.DampingRatioNoBouncy,
    stiffness: Float = Spring.StiffnessMedium,
    visibilityThreshold: T? = null
    ): SpringSpec<T>

    val value by animateFloatAsState(
    targetValue = 1f,
    animationSpec = spring(
    dampingRatio = Spring.DampingRatioHighBouncy,
    stiffness = Spring.StiffnessMedium
    )
    )

    spring 接受两个参数,dampingRatiostiffness 。前者定义弹簧的弹性,默认值为 Spring.DampingRatioNoBouncy 。后者定义弹簧向 targetVaule 移动的速度。 默认值为 Spring.StiffnessMedium。基于物理特性的 spring 无法设置 duration。具体效果参考下图:


    animation-spring.gif


    tween


    androidx.compose.animation.core AnimationSpec.kt 
    @Stable
    public fun <T> tween(
    durationMillis: Int = DefaultDurationMillis,
    delayMillis: Int = 0,
    easing: Easing = FastOutSlowInEasing
    ): TweenSpec<T>

    tween 在指定的 durationMillis 内使用缓和曲线在起始值和结束值之间添加动画效果。动画曲线通过 Easing 添加。


    val value by animateFloatAsState(
    targetValue = 1f,
    animationSpec = tween(
    durationMillis = 300,
    delayMillis = 50,
    easing = LinearOutSlowInEasing
    )
    )

    keyframes


    keyframes 会根据在动画时长内的不同时间戳中指定的快照值添加动画效果。在任何给定时间,动画值都将插值到两个关键帧值之间。对于其中每个关键帧,可以指定 Easing 来确定插值曲线:


    val value by animateFloatAsState(
    targetValue = 1f,
    animationSpec = keyframes {
    durationMillis = 375
    0.0f at 0 with LinearOutSlowInEasing // for 0-15 ms
    0.2f at 15 with FastOutLinearInEasing // for 15-75 ms
    0.4f at 75 // ms
    0.4f at 225 // ms
    }
    )

    snapTo(targetValue: T)


    androidx.compose.animation.core.Animatable
    public final suspend fun snapTo(
    targetValue: T
    ): Unit

    Animatable 还提供了一个 snapTo(targetValue) 的函数,这个函数允许直接设置它内部持有的 value 值,此过程不会产生任何动画,正在进行的动画也会被取消,某些场景可能需要动画开始前有一个初始值,可以使用此函数。


    一种更方便的使用方式:animate*AsState


    设置某一个属性的目标值,当对应属性值发生变化后,自动触发动画,过度到对应值。



    This Composable function is overloaded for different parameter types such as Float, Color, Offset, etc. When the provided targetValue is changed, the animation will run automatically. If there is already an animation in-flight when targetValue changes, the on-going animation will adjust course to animate towards the new target value.



    compose 提供了这几个覆盖基本场景的函数:



    • animateFloatAsState

    • animateDpAsState

    • animateSizeAsState

    • animateOffsetAsState

    • animateRectAsState

    • animateIntAsState

    • animateIntOffsetAsState

    • animateIntSizeAsState


    @Composable
    fun Demo() {
    var flag by remember { mutableStateOf(false) }
    Box(
    Modifier.fillMaxSize().background(Color.DarkGray),
    contentAlignment = Alignment.Center
    ) {

    val size by animateDpAsState(if (flag) 32.dp else 96.dp) { valueOnAnimateEnd ->
    // 可以设置动画结束的监听函数,回调动画结束时对应属性的目标值
    Log.i(TAG, "size animate finished with $valueOnAnimateEnd")
    }
    Row(
    Modifier
    .size(size)
    .background(Color.Magenta)
    .clickable { flag = !flag }
    ) {}
    }
    }

    演示效果:


    demo_animateDpAsState.gif


    作为 compose 动画的最基本操作,与我们平时使用动画的方式不太一样,你会发现你能影响动画的核心只能是选一个属性和一个目标值。甚至连属性的初始值都不能预设,动画的时长没有办法干预。


    深入一点点


    查看 animateDpState() 函数的实现:


    /**
    * ... ...
    *
    * [animateDpAsState] returns a [State] object. The value of the state object will continuously be
    * updated by the animation until the animation finishes.
    *
    * Note, [animateDpAsState] cannot be canceled/stopped without removing this composable function
    * from the tree. See [Animatable] for cancelable animations.
    *
    * @sample androidx.compose.animation.core.samples.DpAnimationSample
    *
    * @param targetValue Target value of the animation
    * @param animationSpec The animation that will be used to change the value through time. Physics animation will be used by default.
    * @param finishedListener An optional end listener to get notified when the animation is finished.
    * @return A [State] object, the value of which is updated by animation.
    */
    @Composable
    fun animateDpAsState(
    targetValue: Dp,
    animationSpec: AnimationSpec<Dp> = dpDefaultSpring,
    finishedListener: ((Dp) -> Unit)? = null
    ): State<Dp> {
    return animateValueAsState(
    targetValue,
    Dp.VectorConverter,
    animationSpec,
    finishedListener = finishedListener
    )
    }

    查看 animateIntAsState 的实现:


    /**
    * ... ...
    *
    * [animateIntAsState] returns a [State] object. The value of the state object will continuously be
    * updated by the animation until the animation finishes.
    *
    * Note, [animateIntAsState] cannot be canceled/stopped without removing this composable function
    * from the tree. See [Animatable] for cancelable animations.
    *
    * @param targetValue Target value of the animation
    * @param animationSpec The animation that will be used to change the value through time. Physics
    * animation will be used by default.
    * @param finishedListener An optional end listener to get notified when the animation is finished.
    * @return A [State] object, the value of which is updated by animation.
    */
    @Composable
    fun animateIntAsState(
    targetValue: Int,
    animationSpec: AnimationSpec<Int> = intDefaultSpring,
    finishedListener: ((Int) -> Unit)? = null
    ): State<Int> {
    return animateValueAsState(
    targetValue, Int.VectorConverter, animationSpec, finishedListener = finishedListener
    )
    }




    • targetValue是某一个以Dp为单位的属性的目标值,顾名思义就是你希望这个属性变化为某一个具体的值;




    • animationSpec 该属性的值如何跟随时间的变化而变化,有默认实现;




    • finishedListener 动画结束函数,可空;




    • anumate*AsState 系列函数的实现都很相似,统一在内部调用了 animateValueAsState(...)




    这是一个基于 State 的实现,联系 Compose 中对于数据的封装和订阅方式,可以理解为当程序的某一个行为触发动画启动后,compose 会自主启动,并根据时间来计算对应的属性应该是什么值,再通过 State 返回,Composable 函数在一次次 recompose 行为中不断通过 State 获取到该属性的最新值,并刷新到界面上,知道这个值变化到目标值状态,更新也就结束了。也就是动画结束。


    继续深入 animateValueAsState 的实现:


    @Composable
    fun <T, V : AnimationVector> animateValueAsState(
    targetValue: T,
    typeConverter: TwoWayConverter<T, V>,
    animationSpec: AnimationSpec<T> = remember {
    spring(visibilityThreshold = visibilityThreshold)
    },
    visibilityThreshold: T? = null,
    finishedListener: ((T) -> Unit)? = null
    ): State<T> {

    val animatable = remember { Animatable(targetValue, typeConverter) }
    val listener by rememberUpdatedState(finishedListener)
    val animSpec by rememberUpdatedState(animationSpec)

    ... ...

    return animatable.asState()
    }

    你会发现其内部其实还是使用 Animatable 来实现。anumate*AsState 虽然基于 Animatable ,不但没有扩充 Animatable 的用法,反而还有了局限,怎会如此?个人认为 animate*AsState 是专门为确定性的简单使用场景进行的封装,这些场景有明确的状态变化,需要做动画的值也不会很复杂,在这些场景中如果能极为方便的快速定义动画,也会是一种非常实用的设计,即使场景变得复杂,再用 Animatable 兜底也能满足需求。


    updateTransition


    在实际的使用场景中,很多情况下的动画设计都不是单一参数可以完成的,比如大小变化的同时对颜色进行过渡、大小与圆角同时变化,形状与颜色同时变化等。这些情况需要组合多个动画同时进行:


    @Composable
    fun Demo() {
    var flag by remember { mutableStateOf(false) }
    Box(
    Modifier
    .fillMaxSize()
    .background(Color.DarkGray),
    contentAlignment = Alignment.Center
    ) {

    val size by animateDpAsState(if (flag) 32.dp else 96.dp) { valueOnAnimateEnd ->
    // 可以设置动画结束的监听函数,回调动画结束时对应属性的目标值
    Log.i(TAG, "size animate finished with $valueOnAnimateEnd")
    }
    val color by animateColorAsState(
    targetValue = if (flag) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
    )
    Row(
    Modifier
    .size(size)
    .background(color)
    .clickable { flag = !flag }
    ) {}
    }
    }

    但是上面的实现存在一个问题,就是每一个属性值的动画过程都是单独计算的,同时每个属性动画也都要考单独的状态进行管理,这显然在性能上是有浪费的,而却也很不方便。这种情况可以引入 Transition 来进行动画的统一管理:


    @Composable
    fun Demo() {
    var flag by remember { mutableStateOf(false) }
    Box(
    Modifier
    .fillMaxSize()
    .background(Color.DarkGray),
    contentAlignment = Alignment.Center
    ) {
    val transition = updateTransition(flag)
    val size = transition.animateDp { if (it) 32.dp else 96.dp }
    val color = transition.animateColor { if (it) MaterialTheme.colors.primary else MaterialTheme.colors.secondary }
    Row(
    Modifier
    .size(size.value)
    .background(color.value)
    .clickable { flag = !flag }
    ) {}
    }
    }

    这样当 flag 触发 transition 状态改变时,sizecolor 的值就可以同时在 transition 内部进行计算,性能又节省了亿点点🤏🏻


    @Composable
    fun <T> updateTransition(
    targetState: T,
    label: String? = null
    ): Transition<T> {
    val transition = remember { Transition(targetState, label = label) }
    transition.animateTo(targetState)
    DisposableEffect(transition) {
    onDispose {
    // Clean up on the way out, to ensure the observers are not stuck in an in-between
    // state.
    transition.onTransitionEnd()
    }
    }
    return transition
    }

    updateTransition 可以创建并保存状态,其内部使用 remember 实现。


    作者:User_20211124110632
    链接:https://juejin.cn/post/7034107134551785503
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    kotlin 进阶教程:核心概念

    1 空安全 // ? 操作符,?: Elvis 操作符 val length = b?.length ?: -1 // 安全类型转换 val code = res.code as? Int // StringsKt val code = res.code?.t...
    继续阅读 »

    1 空安全


    // ? 操作符,?: Elvis 操作符
    val length = b?.length ?: -1
    // 安全类型转换
    val code = res.code as? Int
    // StringsKt
    val code = res.code?.toIntOrNull()
    // CollectionsKt
    val list1: List<Int?> = listOf(1, 2, 3, null)
    val list2 = listOf(1, 2, 3)
    val a = list2.getOrNull(5)
    // 这里注意 null 不等于 false
    if(a?.hasB == false) {}

    2 内联函数



    使用 inline 操作符标记的函数,函数内代码会编译到调用处。


    // kotlin
    val list = listOf("a", "b", "c", null)
    list.getOrElse(4) { "d" }?.let {
    println(it)
    }

    // Decompile,getOrElse 方法会内联到调用处
    List list = CollectionsKt.listOf(new String[]{"a", "b", "c", (String)null});
    byte var3 = 4;
    Object var10000;
    if (var3 <= CollectionsKt.getLastIndex(list)) {
    var10000 = list.get(var3);
    } else {
    var10000 = "d";
    }

    String var9 = (String)var10000;
    if (var9 != null) {
    String var2 = var9;
    System.out.print(var2);
    }

    noline: 禁用内联,用于标记参数,被标记的参数不会参与内联。


    // kotlin
    inline fun sync(lock: Lock, block1: () -> Unit, noinline block2: () -> Unit) {}

    // Decompile,block1 会内联到调用处,但是 block2 会生成函数对象并生成调用
    Function0 block2$iv = (Function0)null.INSTANCE;
    ...
    block2.invoke()


    @kotlin.internal.InlineOnly: kotlin 内部注解,这个注解仅用于内联函数,用于防止 java 类调用(原理是编译时会把这个函数标记为 private,内联对于 java 类来说没有意义)。


    如果扩展函数的方法参数包含高阶函数,需要加上内联。


    非局部返回:
    lambda 表达式内部是禁止使用裸 return 的,因为 lambda 表达式不能使包含它的函数返回。但如果 lambda 表达式传给的函数是内联的,那么该 return 也可以内联,所以它是允许的,这种返回称为非局部返回。
    但是可以通过 crossinline 修饰符标记内联函数的表达式参数禁止非局部返回。


    public inline fun <T> observable(initialValue: T, crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Unit):
    ReadWriteProperty<Any?, T> =
    object : ObservableProperty<T>(initialValue) {
    override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) = onChange(property, oldValue, newValue)
    }

    3 泛型



    (1) 基本用法


    class A<T> {
    }
    fun <T> T.toString(): String {
    }
    // 约束上界
    class Collection<T : Number, R : CharSequence> : Iterable<T> {
    }
    fun <T : Iterable> T.toString() {
    }
    // 多重约束
    fun <T> T.eat() where T : Animal, T : Fly {
    }


    (2) 类型擦除
    为了兼容 java 1.5 以前的版本,带不带泛型编译出来的字节码都是一样的,泛型的特性是通过编译器类型检查和强制类型转换等方式实现的,所以 java 的泛型是伪泛型。
    虽然运行时会擦除泛型,但也是有办法拿到的。


    (javaClass.genericSuperclass as? ParameterizedType)
    ?.actualTypeArguments
    ?.getOrNull(0)
    ?: Any::class.java

    fastjsonTypeReferencegsonTypeToken 都是用这种方式来获取泛型的。


    // fastjson 
    HttpResult<PrivacyInfo> httpResult = JSON.parseObject(
    json,
    new TypeReference<HttpResult<PrivacyInfo>>() {
    }
    );
    // gson
    Type type = new TypeToken<ArrayList<JsonObject>>() {
    }.getType();
    ArrayList<JsonObject> srcJsonArray = new Gson().fromJson(sourceJson, type);


    Reified 关键字
    在 kotlin 里,reified 关键字可以让泛型能够在运行时被获取到。reified 关键字必须结合内联函数一起用。


    // fastjson
    inline fun <reified T : Any> parseObject(json: String) {
    JSON.parseObject(json, T::class.java)
    }
    // gson
    inline fun <reified T : Any> fromJson(json: String) {
    Gson().fromJson(json, T::class.java)
    }
    // 获取 bundle 中的 Serializable
    inline fun <reified T> Bundle?.getSerializableOrNull(key: String): T? {
    return this?.getSerializable(key) as? T
    }
    // start activity
    inline fun <reified T : Context> Context.startActivity() {
    startActivity(Intent(this, T::class.java))
    }


    (3) 协变(out)和逆变(in)


    javaList 是不变的,下面的操作不被允许。


    List<String> strList = new ArrayList<>();
    List<Object> objList = strList;


    但是 kotlinList 是协变的,可以做这个操作。


    public interface List<out E> : Collection<E> { ... }
    val strList = arrayListOf<String>()
    val anyList: List<Any> = strList

    注意这里赋值之后 anyList 的类型还是 List<Any> , 如果往里添加数据,那个获取的时候就没法用 String 接收了,这是类型不安全的,所以协变是不允许写入的,是只读的。在 kotlin 中用 out 表示协变,用 out 声明的参数类型不能作为方法的参数类型,只能作为返回类型,可以理解成“生产者”。相反的,kotlin 中用 in 表示逆变,只能写入,不能读取,用 in 声明的参数类型不能作为返回类型,只能用于方法参数类型,可以理解成 “消费者”。


    注意 kotlin 中的泛型通配符 * 也是协变的。


    4 高阶函数



    高阶函数: 将函数用作参数或返回值的函数。


    写了个 test 方法,涵盖了常见的高阶函数用法。


    val block4 = binding?.title?.test(
    block1 = { numer ->
    setText(R.string.app_name)
    println(numer)
    },
    block2 = { numer, checked ->
    "$numer : $checked"
    },
    block3 = {
    toIntOrNull() ?: 0
    }
    )
    block4?.invoke(2)

    fun <T: View, R> T.test(
    block1: T.(Int) -> Unit,
    block2: ((Int, Boolean) -> String)? = null,
    block3: String.() -> R
    ): (Int) -> Unit {
    block1(1)
    block2?.invoke(2, false)
    "5".block3()
    return { number ->
    println(number)
    }
    }

    5 作用域函数


    // with,用于共用的场景
    with(View.OnClickListener {
    it.setBackgroundColor(Color.WHITE)
    }) {
    tvTitle.setOnClickListener(this)
    tvExpireDate.setOnClickListener(this)
    }

    // apply,得到值后会修改这个值的属性
    return CodeLoginFragment().apply {
    arguments = Bundle().apply {
    putString(AppConstants.INFO_EYES_EVENT_ID_FROM, eventFrom)
    }
    }

    // also,得到值后还会继续用这个值
    tvTitle = view.findViewById<TextView?>(R.id.tvTitle).also {
    displayTag(it)
    }

    // run,用于需要拿内部的属性的场景
    tvTitle?.run {
    text = "test"
    visibility = View.VISIBLE
    }

    // let,用于使用它自己的场景
    tvTitle?.let {
    handleTitle(it)
    }

    fun <T> setListener(listenr: T.() -> Unit) {
    }

    6 集合


    list.reversed().filterNotNull()
    .filter {
    it % 2 != 0
    }
    .map {
    listOf(it, it * 2)
    }
    .flatMap {
    it.asSequence()
    }.onEach {
    println(it)
    }.sortedByDescending {
    it
    }
    .forEach {
    println(it)
    }

    7 操作符重载



    重载(overload)操作符的函数都需要使用 operator 标记,如果重载的操作符被重写(override),可以省略 operator 修饰符。
    这里列几个比较常用的。


    索引访问操作符:


    a[i, j] => a.get(i, j)
    a[i] = b => a.set(i, b)


    注意 i、j 不一定是数字,也可以是 String 等任意类型。


    public interface List<out E> : Collection<E> {
    public operator fun get(index: Int): E
    }
    public interface MutableList<E> : List<E>, MutableCollection<E> {
    public operator fun set(index: Int, element: E): E
    }


    调用操作符:
    invoke 是调用操作符函数名,调用操作符函数可以写成函数调用表达式。


    val a = {}
    a() => a.invoke()
    a(i, j) => a.invoke(i, j)


    变量 block: (Int) -> Unit 调用的时候可以写成 block.invoke(2),也可以写成 block(2),原因是重载了 invoke 函数:


    public interface Function1<in P1, out R> : Function<R> {
    /** Invokes the function with the specified argument. */
    public operator fun invoke(p1: P1): R
    }


    getValuesetValueprovideDelegate 操作符:
    用于委托属性,变量的 get() 方法会委托给委托对象的 getValue 操作符函数,相对应的变量的 set() 方法会委托给委托对象的 setValue 操作符函数。


    class A(var name: String? = null) {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String? = name
    operator fun setValue(thisRef: Any?, property: KProperty<*>, name: String?) {
    this.name = name
    }
    }
    // 翻译
    var b by A() =>
    val a = A()
    var b:String?
    get() = a.getValue(this, ::b)
    set(value) = a.setValue(this, ::b, value)


    表达式 ::b 求值为 KProperty 类型的属性对象。



    跟前面的操作符函数有所区别的是,这两个操作符函数的参数格式都是严格要求的,一个类中的函数格式符合特定要求才可以被当做委托对象。


    provideDelegate 主要用于对委托对象通用处理,比如多个变量用了同一个委托对象时需要验证变量名的场景。


    var b by ALoader()

    class A(var name: String? = null) : ReadWriteProperty<Any?, String?>{
    override fun getValue(thisRef: Any?, property: KProperty<*>): String? {
    return name
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: String?) {
    this.name = value
    }
    }

    class ALoader : PropertyDelegateProvider<Any?, A> {
    override operator fun provideDelegate(thisRef: Any?, property: KProperty<*>) : A {
    property.run {
    when {
    isConst -> {}
    isLateinit -> {}
    isFinal -> {}
    isSuspend -> {}
    !property.name.startsWith("m") -> {}
    }
    }
    return A()
    }
    }

    // 翻译
    var b by ALoader() =>
    val a = ALoader().provideDelegate(this, this::b)
    var b: String?
    get() = a.getValue(this, ::b)
    set(value) = a.setValue(this, ::b, value)

    8 委托


    8.1 委托模式




    // 单例
    companion object {
    @JvmStatic
    val instance by lazy { FeedManager() }
    }

    // 委托实现多继承
    interface BaseA {
    fun printA()
    }

    interface BaseB {
    fun printB()
    }

    class BaseAImpl(val x: Int) : BaseA {
    override fun printA() {
    print(x)
    }
    }

    class BaseBImpl() : BaseB {
    override fun printB() {
    print("printB")
    }
    }

    class Derived(a: BaseA, b: BaseB) : BaseA by a, BaseB by b {
    override fun printB() {
    print("world")
    }
    }

    fun main() {
    val a = BaseAImpl(10)
    val b = BaseBImpl()
    Derived(a, b).printB()
    }

    // 输出:world



    这里 Derived 类相当于同时继承了 BaseAImplBaseBImpl 类,并且重写了 printB() 方法。
    在实际开发中,一个接口有多个实现,如果想复用某个类的实现,可以使用委托的形式。
    还有一种场景是,一个接口有多个实现,需要动态选择某个类的实现:


    interface IWebView {
    fun load()
    }

    // SDK 内部 SystemWebView
    class SystemWebView : IWebView {
    override fun load() {
    ...
    }

    fun stopLoading() {
    ...
    }
    }

    // SDK 内部 X5WebView
    class X5WebView : IWebView {
    override fun load() {
    ...
    }

    fun stopLoading() {
    ...
    }
    }

    abstract class IWebViewAdapter(webview: IWebView) : IWebView by webview{
    abstract fun stopLoading()
    }

    class SystemWebViewAdapter(private val webview: SystemWebView) : IWebViewAdapter(webview){
    override fun stopLoading() {
    webview.stopLoading()
    }
    }

    class X5WebViewAdapter(private val webview: X5WebView) : IWebViewAdapter(webview){
    override fun stopLoading() {
    webview.stopLoading()
    }
    }


    8.2 委托属性



    格式:


    import kotlin.reflect.KProperty

    public interface ReadOnlyProperty<in R, out T> {
    public operator fun getValue(thisRef: R, property: KProperty<*>): T
    }

    public interface ReadWriteProperty<in R, T> {
    public operator fun getValue(thisRef: R, property: KProperty<*>): T

    public operator fun setValue(thisRef: R, property: KProperty<*>, value: T)
    }

    public fun interface PropertyDelegateProvider<in T, out D> {
    public operator fun provideDelegate(thisRef: T, property: KProperty<*>): D
    }

    自定义委托对象, getValue 方法的参数跟上面完全一致即可,返回值类型必须是属性类型;setValue 方法的前两个参数跟上面完全一致即可,第三个参数类型必须是属性类型;provideDelegate 方法的参数跟上卖弄完全一致即可,返回值类型必须是属性类型。
    ReadOnlyPropertyReadWritePropertyPropertyDelegateProvider 都是 kotlin 标准库里的类,需要自定义委托对象时直接继承他们会更方便。


    9 怎么写单例?



    不用 object 的写法可能是:


    // 不带参单例
    class A {
    companion object {
    @JvmStatic
    val instance by lazy { A() }
    }
    }

    // 带参的单例,不推荐
    class Helper private constructor(val context: Context) {

    companion object {
    @Volatile
    private var instance: Helper? = null

    @JvmStatic
    fun getInstance(context: Context?): Helper? {
    if (instance == null && context != null) {
    synchronized(Helper::class.java) {
    if (instance == null) {
    instance = Helper(context.applicationContext)
    }
    }
    }
    return instance
    }
    }
    }

    先说带参的单例,首先不推荐写带参数的单例,因为单例就是全局共用,初始化一次之后保持不变,需要的参数应该在第一次使用前设置好(比如通过 by lazy{ A().apply { ... } }),或者单例内部拿应用内全局的参数,然后上例中 context 作为静态变量,Android Studio 会直接报黄色警告,这是个内存泄漏。context 可以设置一个全局的 applicationContext 变量获取。


    然后上面不带参的单例可以
    直接用 object 代替或者直接不用 object 封装,写在文件顶层,可以对比下编译后的代码:


    // kotlin
    object A{
    fun testA(){}
    }

    // 编译后:
    public final class A {
    @NotNull
    public static final A INSTANCE;

    public final void testA() {
    }

    private A() {
    }

    static {
    A var0 = new A();
    INSTANCE = var0;
    }
    }


    // kotlin
    var a = "s"
    fun testB(a: String){
    print(a)
    }

    // 编译后:
    public final class TKt {
    @NotNull
    private static String a = "s";

    @NotNull
    public static final String getA() {
    return a;
    }

    public static final void setA(@NotNull String var0) {
    Intrinsics.checkNotNullParameter(var0, "<set-?>");
    a = var0;
    }

    public static final void testB(@NotNull String a) {
    Intrinsics.checkNotNullParameter(a, "a");
    boolean var1 = false;
    System.out.print(a);
    }
    }

    可以发现,直接文件顶层写,不会创建对象,都是静态方法,如果方法少且评估不需要封装(主要看调用的时候是否需要方便识别哪个对象的方法)可以直接写在文件顶层。


    同理,伴生对象也尽量非必要不创建。


    // kotlin
    class A {
    companion object {
    const val TAG = "A"

    @JvmStatic
    fun newInstance() = A()
    }
    }

    // 编译后
    public final class A {
    @NotNull
    public static final String TAG = "A";
    @NotNull
    public static final A.Companion Companion = new A.Companion((DefaultConstructorMarker)null);

    @JvmStatic
    @NotNull
    public static final A newInstance() {
    return Companion.newInstance();
    }

    public static final class Companion {
    @JvmStatic
    @NotNull
    public final A newInstance() {
    return new A();
    }

    private Companion() {
    }

    // $FF: synthetic method
    public Companion(DefaultConstructorMarker $constructor_marker) {
    this();
    }
    }
    }

    可以发现,伴生对象会创建一个对象(废话...),知道这个很重要,因为如果伴生对象里没有函数,只有常量,那还有必要创建这个对象吗?函数也只是为了 newInstance 这种方法调用的时候看起来统一一点,如果是别的方法,完全可以写在类所在文件的顶层。


    作者:流沙三七
    链接:https://juejin.cn/post/7034110955571609607
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    前端vue面霸修炼手册!!

    一、对MVVM的理解MVVM全称是Model-View-ViewModelModel 代表数据模型,数据和业务逻辑都在Model层中定义;泛指后端进行的各种业务逻辑处理和数据操控,对于前端来说就是后端提供的 api 接口。View 代表UI视图,负责数据的展示...
    继续阅读 »



    一、对MVVM的理解

    MVVM全称是Model-View-ViewModel

    Model 代表数据模型,数据和业务逻辑都在Model层中定义;泛指后端进行的各种业务逻辑处理和数据操控,对于前端来说就是后端提供的 api 接口。View 代表UI视图,负责数据的展示;视图层,也就是用户界面。前端主要由 HTML 和 CSS 来构建 。ViewModel 负责监听 Model 中数据的改变并且控制视图的更新,处理用户交互操作;

    Vue是以数据为驱动的,Vue自身将DOM和数据进行绑定,一旦创建绑定,DOM和数据将保持同步,每当数据发生变化,DOM会跟着变化。 ViewModel是Vue的核心,它是Vue的一个实例。Vue实例时作用域某个HTML元素上的这个HTML元素可以是body,也可以是某个id所指代的元素。

    二、vue常见指令

    1. v-textv-text 主要用来更新 textContent,可以等同于 JS 的 text 属性。

    <span v-text="name"></span>

    <span插值表达式{{name}}</span>
    1. v-html等同于 JS 的 innerHtml 属性

    <div v-html="content"></div>
    1. v-cloak用来保持在元素上直到关联实例结束时进行编译 解决闪烁问题

    <div id="app" v-cloak>
       <div>
          {{msg}}
       </div>
    </div>
    <script type="text/javascript">
       new Vue({
         el:'#app',
         data:{
           msg:'hello world'
        }
      })
    </script>

    正常在页面加载时会闪烁,先显示:

    <div>
      {{msg}}
    </div>

    编译后才显示:

    <div>
      hello world!
    </div>

    可以用 v-cloak 指令解决插值表达式闪烁问题,v-cloak 在 css 中用属性选择器设置为 display: none;

    1. v-oncev-once 关联的实例,只会渲染一次。之后的重新渲染,实例极其所有的子节点将被视为静态内容跳过,这可以用于优化更新性能

    <span v-once>This will never change:{{msg}}</span>  //单个元素
    <div v-once>//有子元素
       <h1>comment</h1>
       <p>{{msg}}</p>
    </div>
    <my-component v-once:comment="msg"></my-component> //组件
    <ul>
       <li v-for="i in list">{{i}}</li>
    </ul>

    上面的例子中,msg,list 即使产生改变,也不会重新渲染。

    1. v-ifv-if 可以实现条件渲染,Vue 会根据表达式的值的真假条件来渲染元素

    <a v-if="true">show</a>
    1. v-elsev-else 是搭配 v-if 使用的,它必须紧跟在 v-if 或者 v-else-if 后面,否则不起作用

    <a v-if="true">show</a>
    <a v-else>hide</a>
    1. v-else-ifv-else-if 充当 v-if 的 else-if 块, 可以链式的使用多次。可以更加方便的实现 switch 语句。

    <div v-if="type==='A'">
      A
    </div>
    <div v-else-if="type==='B'">
      B
    </div>
    <div v-else-if="type==='C'">
      C
    </div>
    <div v-else>
      Not A,B,C
    </div>
    1. v-show也是用于根据条件展示元素。和 v-if 不同的是,如果 v-if 的值是 false,则这个元素被销毁,不在 dom 中。但是 v-show 的元素会始终被渲染并保存在 dom 中,它只是简单的切换 css 的 dispaly 属性。

    <span v-show="true">hello world</span >

    注意:v-if 有更高的切换开销 v-show 有更高的初始渲染开销。因此,如果要非常频繁的切换, 则使用 v-show 较好;如果在运行时条件不太可能改变,则 v-if 较好

    1. v-for用 v-for 指令根据遍历数组来进行渲染

    <div v-for="(item,index) in items"></div>   //使用in,index是一个可选参数,表示当前项的索引
    1. v-bindv-bind 用来动态的绑定一个或者多个特性。没有参数时,可以绑定到一个包含键值对的对象。常用于动态绑定 class 和 style。以及 href 等。简写为一个冒号【 :】

    <div id="app">
       <div :class="{'is-active':isActive, 'text-danger':hasError}"></div>
    </div>
    <script>
       var app = new Vue({
           el: '#app',
           data: {
               isActive: true,  
               hasError: false    
          }
      })
    </script>

    编译后

    <div class = "is-active"></div>
    1. v-model用于在表单上创建双向数据绑定

    <div id="app">
       <input v-model="name">
       <p>hello {{name}}</p>
    </div>
    <script>
       var app = new Vue({
           el: '#app',
           data: {
               name:'小明'
          }
      })
    </script>

    model 修饰符有

    .lazy(在 change 事件再同步) > v-model.lazy .number(自动将用户的输入值转化为数值类型) > v-model.number .trim(自动过滤用户输入的首尾空格) > v-model.trim

    1. v-onv-on 主要用来监听 dom 事件,以便执行一些代码块。表达式可以是一个方法名。 简写为:【 @ 】

    <div id="app">
      <button @click="consoleLog"></button>
    </div>
    <script>
      var app = new Vue({
          el: '#app',
          methods:{
              consoleLog:function (event) {
                  console.log(1)
              }
          }
      })
    </script>

    事件修饰符

    .stop 阻止事件继续传播 .prevent 事件不再重载页面 .capture 使用事件捕获模式,即元素自身触发的事件先在此处处理,然后才交由内部元素进行处理 .self 只当在 event.target 是当前元素自身时触发处理函数 .once 事件将只会触发一次 .passive 告诉浏览器你不想阻止事件的默认行为

    三 、v-if 和 v-show 有什么区别?

    共同点:v-if 和 v-show 都能实现元素的显示隐藏

    区别:

    v-show 只是简单的控制元素的 display 属性 而 v-if 才是条件渲染(条件为真,元素将会被渲染,条件为假,元素会被销毁) 2. v-show 有更高的首次渲染开销,而 v-if 的首次渲染开销要小的多 3. v-if 有更高的切换开销,v-show 切换开销小 4. v-if 有配套的 v-else-if 和 v-else,而 v-show 没有 5. v-if 可以搭配 template 使用,而 v-show 不行

    四、如何让CSS只在当前组件中起作用?

    将组件样式加上 scoped

    <style scoped>
    ...
    </style>

    五、 keep-alive的作用是什么?

    keep-alive包裹动态组件时,会缓存不活动的组件实例, 主要用于保留组件状态或避免重新渲染。

    六、在Vue中使用插件的步骤

    采用ES6的 import … from … 语法 或 CommonJSd的 require() 方法引入插件 2、使用全局方法 Vue.use( plugin ) 使用插件,可以传入一个选项对象 Vue.use(MyPlugin, { someOption: true })

    七、Vue 生命周期

    八、Vue 组件间通信有哪几种方式

    Vue 组件间通信只要指以下 3 类通信:父子组件通信、隔代组件通信、兄弟组件通信

    九、computed 和 watch 的区别和运用的场景

    computed: 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值

    watch: 更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作

    运用场景:

    • 当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed

    • 当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch

    十、vue-router 路由模式有几种

    1. Hash: 使用 URL 的 hash 值来作为路由。支持所有浏览器。

    2. History: 以来 HTML5 History API 和服务器配置。参考官网中 HTML5 History 模式

    3. Abstract: 支持所有 javascript 运行模式。如果发现没有浏览器的 API,路由会自动强制进入这个模式。

    十一、SPA 单页面的理解,它的优缺点分别是什么

    SPA( single-page application )仅在 Web 页面初始化时加载相应的 HTML、JavaScript 和 CSS 一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转 取而代之的是利用路由机制实现 HTML 内容的变换, UI 与用户的交互,避免页面的重新加载。

    优点:

    1、用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染
    2、基于上面一点,SPA 相对对服务器压力小
    3、前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理

    缺点:

    1、初次加载耗时多:为实现单页 Web 应用功能及显示效果, 需要在加载页面的时候将 JavaScript、CSS 统一加载,部分页面按需加载
    2、前进后退路由管理:由于单页应用在一个页面中显示所有的内容, 所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理
    3、SEO 难度较大:由于所有的内容都在一个页面中动态替换显示,所以在 SEO 上其有着天然的弱势
    作者:不要搞偷袭
    来源:https://blog.51cto.com/u_15115139/2675806


    收起阅读 »

    Swift路由组件(一)使用路由的目的和实现思想

    iOS
    Swift路由组件(一)使用路由的目的和实现思想这个为本人原创,转载请注明出处:juejin.cn/post/703216…目的项目开发到一定程度,功能之间的调用会变的越来越复杂这里用一个商品购买的逻辑举例从图上看,问题就是业务之间的跳转很多,而且乱。还有就是...
    继续阅读 »

    Swift路由组件(一)使用路由的目的和实现思想

    这个为本人原创,转载请注明出处:juejin.cn/post/703216…

    目的

    项目开发到一定程度,功能之间的调用会变的越来越复杂

    这里用一个商品购买的逻辑举例

    image.png

    从图上看,问题就是业务之间的跳转很多,而且乱。还有就是当跳同一个页面时,跳转要带的参数都一致,如何保证?如果代码分散到各个业务里面去跳就难免会到处维护的问题。

    这就需要路由了。

    而且路由做好了,还能有一个好处就是后端或者前端,他们按路由协议统一处理跳转,app就可以不考虑业务之间的跳转了。

    下面是加上路由模块的跳转图。

    image.png

    这下清晰了。

    从图上来看,路由,他主要负责业务的跳转,从一个页面跳转到另一个页面等。

    实现的思想

    为了能跳,那么就需要知道路。所以可以这样理解,路由他需要知道你要跳转到哪里去,去的地方需要什么入参。

    所以得有一个key,map到一个ViewController,然后ViewController需要什么入参,就顺便带过来。

    解决这个key的问题,业界比较常见的做法是有一个路由表

    1. 比如维护一个plist文件,开发的时候把对应的key映射controller维护到plist里面,运行的时候一次性load到内存中。然后路由要跳转的时候就只需要查表来跳。
    2. 或者在运行的时候通过业务注册,每个业务把key注册到路由里面去,在内存中维护一个路由表。

    两种方法都可以。结果大概是这样。

    keyvalue
    to_home_pageHomeViewController
    to_buy_pageBuyViewController
    ......

    路由跳转他要解决三种跳转逻辑

    1. 通过后端下发,直接让App打开某个原生或者Web页面
      • 比如推送消息,点击消息就可以进入某个原生或者Web页面
      • 比如后端返回的商品卡片,点击商品进入某个原生或者Web页面
    2. 比如活动页面,点击按钮进入某个原生或者Web页面
    3. 比如原生页面的某个按钮,点击按钮进入某个原生或者Web页面

    总结起来也就两种,

    1. 一种是远程调用,
    2. 一种是app内部调用。

    所谓远程调用就是app提供的跳转能力,允许外面调用的。再者理解,可以被别的app打开调用,比如微信的分享,支付等。相对的内部调用就是app内部由A页面跳转到B页面的。

    所以针对上面的用处,从命名上可以做好区分,比如内部调用加native://做为开头,表示是内部调用。外部就加weixinapp://(用app名更容易调用者理解),或者加http/https,毕竟可以直接兼容http://www.baidu.com 这样的网页

    之所以要好明确区分,是因为可以利用路由做好统一的权限管理。比如外部调用可以加某一种校验后直接打开,内部调用就加另一种检验,特别是内部跳转要做好权限控制,确保真的是你自己的app调用的内部调用才能打开,防止别人只是用URL Schemes就打开了你的内部页面。

    想到这,那是不是可以加多种前缀呢,答案肯定是可以的,具体看不同公司的业务。这里就先加两种先。 如下:

    keyvalue
    native://to_home_pageHomeViewController
    native://to_buy_pageBuyViewController
    httpWebViewController
    httpsWebViewController
    ......

    上面是说,

    1. 当key是native://to_home_page的时候,就进入主页,打开HomeViewController这个页面。
    2. 当key是http的时候。就进入网页,打开WebViewController这个页面渲染。

    看到这,那么路由的定义也就出来了。 统一的入口和传参,如:

    YYRouter.push(jumpParams: [:])

    然后调用上面的路由表如下:

    YYRouter.push(jumpParams: ["to":"native://to_home_page"]) // 去到首页
    YYRouter.push(jumpParams: ["to":"http://www.baidu.com"]) // 打开网页
    YYRouter.push(jumpParams: ["to":"https://www.baidu.com"]) // 打开网页

    想传参数,那就这样。

    YYRouter.push(jumpParams: ["to":"native://to_home_page", "name": "名字"])
    YYRouter.push(jumpParams: ["to":"http://www.baidu.com&a=1&b=2", "name": "名字"])
    YYRouter.push(jumpParams: ["to":"https://www.baidu.com&c=3&d=4", "name": "名字"])

    终上一个路由的定义就出来了。

    下一编,再讲一个路由的具体实现。 Swift路由组件(二)路由的实现

    链接:https://juejin.cn/post/7032164814210203685/

    收起阅读 »

    Metal 框架之设置加载和存储操作

    iOS
    Metal 框架之设置加载和存储操作「这是我参与11月更文挑战的第19天,活动详情查看:2021最后一次更文挑战」 概述 通过设置 MTLLoadAction 和 MTLStoreAction 属性,可以定义渲染通道加载和存储 MTLRenderPassAtt...
    继续阅读 »

    Metal 框架之设置加载和存储操作


    「这是我参与11月更文挑战的第19天,活动详情查看:2021最后一次更文挑战


    概述


    通过设置 MTLLoadAction 和 MTLStoreAction 属性,可以定义渲染通道加载和存储 MTLRenderPassAttachmentDescriptor 对象 的方式。为渲染目标设置适当的操作,在渲染通道的开始(加载)或结束(存储)时,可以避免昂贵且不必要的工作。


    在 texture 属性上设置渲染目标的纹理,在 loadAction 和 storeAction 属性上设置它的动作:



    let renderPassDescriptor = MTLRenderPassDescriptor()


    // Color render target

    renderPassDescriptor.colorAttachments[0].texture = colorTexture

    renderPassDescriptor.colorAttachments[0].loadAction = .clear

    renderPassDescriptor.colorAttachments[0].storeAction = .store

    // Depth render target

    renderPassDescriptor.colorAttachments[0].texture = depthTexture

    renderPassDescriptor.colorAttachments[0].loadAction = .dontCare

    renderPassDescriptor.colorAttachments[0].storeAction = .dontCare


    // Stencil render target

    renderPassDescriptor.colorAttachments[0].texture = stencilTexture

    renderPassDescriptor.colorAttachments[0].loadAction = .dontCare

    renderPassDescriptor.colorAttachments[0].storeAction = .dontCare



    选择加载操作


    有多个加载操作选项可用,选择哪一个选项,取决于渲染目标的加载需求。



    • 不需要渲染目标的先前内容,而是渲染到其所有像素时,选择 MTLLoadAction.dontCare


    此操作不会产生任何成本,并且在渲染通道开始时像素值始终未定义。


    不需要考虑加载操作.png



    • 不需要渲染目标的先前内容,只需渲染其部分像素时,选择 MTLLoadAction.clear


    此操作会产生将渲染目标的清除值写入每个像素的成本。


    清楚成本.png



    • 需要渲染目标的先前内容,并且只渲染到它的一些像素时,选择 MTLLoadAction.load


    此操作会产生从内存中加载每个像素的先前值的成本,明显慢于 MTLLoadAction.dontCare 或 MTLLoadAction.clear。


    加载成本.png


    选择存储操作


    有多个存储操作选项可用,选择哪一个选项,取决渲染目标的存储需求。



    • 不需要保留渲染目标的内容,选择 MTLStoreAction.dontCare


     此操作不会产生任何成本,并且在渲染通道结束时像素值始终未定义。 在渲染通道中,为中间渲染目标选择此操作,之后不需要该中间的结果。 对于深度和模板渲染目标这是正确的选择。


    不关心存储.png



    • 确实需要保留渲染目标的内容,选择 MTLStoreAction.store


    此操作将每个像素的值存储到内存,会产生成本。 对于可绘制对象,这始终是正确的选择。


    存储成本.png



    • 渲染目标是多重采样纹理


    当执行多重采样时,可以选择存储渲染目标的多重采样或解析数据。 对于多重采样数据,其存储在渲染目标的 texture 属性中。 对于解析的数据,其存储在渲染目标的 resolveTexture 属性中。 多重采样时,请参考此表选择存储操作: 





































    多重采样数据存储解析数据存储需要解析纹理需要的存储操作
    是 是 是  MTLStoreAction.storeAndMultisampleResolve
     MTLStoreAction.store
     MTLStoreAction.multisampleResolve
      MTLStoreAction.dontCare

    要在单个渲染通道中存储和解析多采样纹理,请始终选择 MTLStoreAction.storeAndMultisampleResolve 操作并使用单个渲染命令编码器。



    • 需要推迟存储选择 


    在某些情况下,在收集更多渲染通道信息之前,可能不知道要对特定渲染目标使用哪个存储操作。 要推迟存储操作选择,请在创建 MTLRenderPassAttachmentDescriptor 对象时设置 MTLStoreAction.unknown 值。 设置未知的存储操作,可以避免消耗潜在的成本(因为设置另一个存储操作成本更高)。 但是,在完成对渲染通道的编码之前,必须指定有效的存储操作; 否则,会发生错误。


    评估渲染通道之间的操作


    可以在多个渲染过程中使用相同的渲染目标。 对于任何两个渲染通道之间的同一渲染目标,可能有多种加载和存储组合,选择哪一种组合,取决于渲染目标从一个渲染通道到另一个渲染通道的需求。



    • 下一个渲染通道中,不需要渲染目标的先前内容 


    在第一个渲染通道中,选择 MTLStoreAction.dontCare 以避免存储渲染目标的内容。 在第二个渲染通道中,选择 MTLLoadAction.dontCare 或 MTLLoadAction.clear 以避免加载渲染目标的内容。


    渲染通道的评估1.png


    渲染通道的评估2.png



    • 需要在下一个渲染通道中使用渲染目标的先前内容


    在第一个渲染通道中,选择 MTLStoreAction.store、MTLStoreAction.multisampleResolve 或 MTLStoreAction.storeAndMultisampleResolve 来存储渲染目标的内容。 在第二个渲染通道中,选择 MTLLoadAction.load 以加载渲染目标的内容。


    渲染通道间内容传递.png


    总结


    本文介绍了根据渲染的需要,来设置渲染目标的加载和存储操作,合理的设置这些操作,可以避免昂贵且不必要的工作。通过图文详细介绍了,根据不同的渲染需求,设置不同的加载和存储操作达到的渲染效果。


    作者:__sky
    链接:https://juejin.cn/post/7033731322850148366
    来源:稀土掘金

    收起阅读 »

    iOS 封装一个简易 UITableView 链式监听点击事件的功能思路与实现

    iOS
    废话开篇:RxSwift 对于其功能可以说是 swift 语言的高度封装了,但是它里面也用到了一些 OC 特性,比如交换方法实现。RxSwift 对于 UITableView 的点击事件就进行了二次封装,里面就交换了 respondsToSelector 方法...
    继续阅读 »

    废话开篇:RxSwift 对于其功能可以说是 swift 语言的高度封装了,但是它里面也用到了一些 OC 特性,比如交换方法实现。RxSwift 对于 UITableView 的点击事件就进行了二次封装,里面就交换了 respondsToSelector 方法及重写了消息转发机制下的 forwardInvocationRxSwift 源码太复杂,因此,简单用 OC 写一个 demo,来理解一下 RxSwift 对于 UITableView 的点击事件的绑定。


    1、实现原理


    1、修改一个对象的 respondsToSelector 方法,当进来判断的 sel 是要求继续进行的方法时,返回 YES,这里很明显就是判断 tableView:didSelectRowAtIndexPath: 这个方法。这里注意的是,一个对象即使没有遵循代理协议而只要你实现了代理方法,那么,它也是可以正常执行的,代理协议的遵守只是方便开发通过编译器提示去实现代理方法的。


    也就是说,让对象作为 UITableView 可执行 tableView:didSelectRowAtIndexPath: 方法的代理,但是不去实现这个代理方法。


    2、修改一个对象的 forwardInvocation 方法,当一个对象调用方法出现没有实现的时候就要进行消息转发了,那么,在转发的时候截获 tableView:didSelectRowAtIndexPath: 方法的参数,进而转到别的对象去执行后续操作。


    2、代码效果


    image.png


    当点击 cell 的时候,就通过上图中的 block 进行响应。这里其实从风格上有点类似 RX,但是,这里并没有对创建中对象的内存进行管理,下面有提到,一般 RxSwift 会有 Disposable 对象的返回,它就是来控制序列中创建的对象何时释放的类,可以用属性保存 DisposeBag,让序列与当前使用类生命周期一致,也可以在方法执行的最后面直接执行 dispose 销毁。


    3、UITableView 的 rxRegistSelected 方法的实现


    这里创建一个 UITableView 的分类:


    UITableView+KDS.h


    image.png


    UITableView+KDS.m


    image.png


    这里圈出1KDSDelegateProxy 类就是 tableView:didSelectRowAtIndexPath: 代理方法处理类,并且圈出2UITableView 提供了一个 delegate,并在此之前调用 saveNeedDelegateSel 保存了需要消息转发的代理方法,这个方法后面解释。


    到了这里,UITableView 就可以执行 rxRegistSelected 这个方法了,并且要为这个方法返回的 block1 传一个 UITableViewCell 点击事件响应的 block2block2 是真正执行的点击事件具体实现,block1 仅为仿写 RAC 而写。


    4、KDSDelegateProxy 对象的实现内容


    KDSDelegateProxy.h


    image.png


    KDSDelegateProxy.m


    image.png


    image.png


    4、KDSTableViewDelegateProxy


    KDSTableViewDelegateProxy 是遵循 UITableViewDelegate 协议的对象,并为该对象保存外界传进来的 cell 点击的代理方法


    image.png


    5、总结与思考


    RxSwift 远比上述复杂的多,换句话说个人能力有限说很难从一个百米高楼中去推断夯实地基的具体细节,因为毕竟不是参与施工人员,所以,这里也仅仅是个人思路。那么,说一下为什么没有类似 Disposable 对象,因为 demo 代码的的对象用的是 static 修饰的,如果不用全局变量,那么,作用域外对象就会销毁,代码也就无法运行了,所以,完全可以封装一个 WSLDisposable 类,在最里层的 block 里作为返回值,在最外层进行 dispose 销毁操作,来临时控制中间过程中的对象生命周期。或者封装类似 WSLDisposeBag,将它作为属性保存在例如控制器下,生命周期与当前 控制器一致。


    好了,文章本意也仅分享,代码拙劣,大神勿笑。


    作者:头疼脑胀的代码搬运工
    链接:https://juejin.cn/post/7033679440613736456
    收起阅读 »

    iOS中的事件

    iOS
    iOS中的事件「这是我参与11月更文挑战的第23天,活动详情查看:2021最后一次更文挑战」iOS中的事件在用户使用APP过程中,会产生各种各样的事件,可以分为三大类触摸事件(如点击...)加速器事件(如摇一摇...)远程控制事件(如耳机可以控制手机音量......
    继续阅读 »

    iOS中的事件

    「这是我参与11月更文挑战的第23天,活动详情查看:2021最后一次更文挑战

    iOS中的事件

    • 在用户使用APP过程中,会产生各种各样的事件,可以分为三大类
    • 触摸事件(如点击...)
    • 加速器事件(如摇一摇...)
    • 远程控制事件(如耳机可以控制手机音量...)

    响应者对象(UIResponder)

    说到触摸事件,首先需要了解一个概念:响应者对象

    • 在iOS中不是任何对象都能处理事件,只有继承了 UIResponder 的对象才能接收并处理事件,通常被称为“响应者对象”。如 UIApplicationUIViewControllerUIView 等等

    • UIResponder 内部提供了以下方法来处理事件

    • 触摸事件

      - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
      - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
      - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
      - (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;

    • 加速器事件

      - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;
      - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event;
      - (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event;

    • 远程控制方法

      - (void)remoteControlReceivedWithEvent:(UIEvent *)event;


    UIView 的触摸事件处理

    • UIView 是 UIResponder 的子类,可以覆盖以下4个方法处理不同的触摸事件

    • 一根或者多根手指开始触摸 view,系统会自动调用 view 的下面方法

      - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;

    • 一根或者多根手指在 view 上移动,系统会自动调用 view 的下面方法

      - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;

    • 一根或者多根手指离开 view,系统会自动调用 view 的下面方法

          - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;

    • 触摸结束前,某个系统事件(例如电话呼入)会打断触摸过程,系统会自动调用 view 的下面方法

      - (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;


    UITouch

    • 当用户用一根手指触摸屏幕时,会创建一个与手指关联的 UITouch 对象
    • 一根手指对应一个 UITouch 对象
    • UITouch 的作用:保存着跟手指相关的信息,比如触摸的位置、时间、阶段
    • 当手指移动时,系统会更新同一个 UITouch 对象,使之能够一直保存该手指在的触摸位置
    • 当手指离开屏幕时,系统会销毁相应的 UITouch 对象
    • UITouch 相关属性
      • window 触摸产生时所处的窗口
      • view 触摸产生时所处的视图
      • tapCount 短时间内按屏幕的次数,根据 tapCount 判断单击、双击或更多点击
      • timestamp 记录了触摸事件产生或变化的时间,单位是秒
      • phase 当前触摸事件所处的状态
    • UITouch 相关方法
      • 返回值表示触摸在 view 上的位置,这里返回的位置是针对view的坐标系的(以 view 的左上角为原点(0,0)),调用时如果传入的 view 参数是 nil 的话,返回的时触摸点在 window 的位置

        [touch locationInView:view];

      • 该方法记录了上一个点的位置

        [touch previousLocationInView:view];

    注:
    iPhone开发中,要避免使用双击事件
    如果要在一个 view 中监听多个手指,需要设置属性

    //需要view支持多个手
    view.multipleTouchEnabled = YES;

    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        NSLog(@"%ld",(long)touches.count); //2
    }


    UIEvent

    • 每产生一个事件,就会产生一个 UIEvent 对象
    • UIEvent 被称为事件对象,用于记录事件产生的时刻和类型
    • UIEvent 相关属性
      • 事件类型
        • type 枚举类型(触摸事件、加速器事件、远程控制事件)
        • subtype
      • timestamp 事件产生时间
    • UIEvent 相关方法
      • UIEvent 提供相应方法用于获取在某个 view 上面的接触对象(UITouch

    简单示例

    实现需求:一个按钮可以在屏幕任务拖拽

    Kapture 2021-11-22 at 22.49.40.gif

    1.自定义一个 UIImageView

    @implementation InputImageView

    - (instancetype)initWithFrame:(CGRect)frame{
        self = [super initWithFrame:frame];
        if (self) {        
            UIImage *image = [UIImage imageNamed:@"inputButton"];
            self.image = image;
            self.userInteractionEnabled = YES;
        }
        return self;
    }
    - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    UITouch *touch = touches.anyObject;
    //获取当前点
    CGPoint currentPoint = [touch locationInView:self];
    //获取上一个点的位置
    CGPoint previousPoint = [touch previousLocationInView:self];
    //获取x轴偏移量
        CGFloat offsetX = currentPoint.x - previousPoint.x;
        CGFloat offsetY = currentPoint.y - previousPoint.y;
        //修改view的位置
        self.transform = CGAffineTransformTranslate(self.transform, offsetX, offsetY);
    }
    @end

    2.实际调用

    #import "ViewController.h"
    #import "InputImageView.h"

    @interface ViewController ()
    @property (nonatomic,strong) InputImageView *redView;
    @end

    @implementation ViewController

    - (void)viewDidLoad {
        [super viewDidLoad];
        InputImageView *inputImageView = [[InputImageView alloc]initWithFrame:CGRectMake(150, 150, 56, 56)];
        [self.view addSubview:inputImageView];
    }
    @end


    收起阅读 »

    iOS 获取图片的主题色

    iOS
    iOS 获取图片的主题色目录1.需求背景2.代码部分3.使用效果及代码地址需求背景有时候我们会有这样的需求,用户从相册选择一张照片,返回展示的时候,除了展示照片还要让整体背景也是和照片相近颜色,最近自己写了一个图片加水印的项目,想加上此功能,然鹅谷歌搜了一圈发...
    继续阅读 »

    iOS 获取图片的主题色

    目录

    1.需求背景
    2.代码部分
    3.使用效果及代码地址

    需求背景

    • 有时候我们会有这样的需求,用户从相册选择一张照片,返回展示的时候,除了展示照片还要让整体背景也是和照片相近颜色,最近自己写了一个图片加水印的项目,想加上此功能,然鹅谷歌搜了一圈发现全是OC代码写的,直接使用好像还存在一些问题,所以本文分别用swift和OC实现相关功能。

    代码部分

    • 主要逻辑:
    1. 将图片按比例缩小,因为后续遍历图片每个像素点,循环次数是图片width x height,如果直接原图去遍历,可能一次循环就要跑几十万、百万次,需要时间非常久,所以要将图片缩小。
    2. 获取图片的所有像素的RGB值,每组RGB使用数组存储(可以根据自己的需求过滤部分颜色),然后用Set将数组装起来。
    3. 统计Set里面相同次数最多的色值,即是整个图片的主题色

    swift实现代码:

    ssss.png

    调用:

    selectedImage.subjectColor({[unowned self] color in
    guard let subjectColor = color else { return }
    self.view.backgroundColor = subjectColor
    })

    因为里面是两个for循环,时间复杂度是On^2,如果设置的width和Height比较大的话,会比较耗时,在主线程里面执行可能会卡住,所以使用了gcd开启子线程去执行,完成后回到主线程执行回调。

    OC实现代码:

    Snipaste_2021-11-24_16-19-45.png

    使用效果及代码地址

    54786116-bc1d-45ab-8eb7-2af3fe2a5520.gif

    demon地址

    收起阅读 »

    Fiddler抓取抖音视频数据

    本文仅供参考学习,禁止用于任何形式的商业用途,违者自行承担责任。准备工作:手机(安卓、ios都可以)/安卓模拟器,今天主要以安卓模拟器为主,操作过程一致。抓包工具:Fiddel 下载地址:(https://www.telerik.com/download/fi...
    继续阅读 »



    本文仅供参考学习,禁止用于任何形式的商业用途,违者自行承担责任。

    准备工作:

    1. 手机(安卓、ios都可以)/安卓模拟器,今天主要以安卓模拟器为主,操作过程一致。

    2. 抓包工具:Fiddel 下载地址:(https://www.telerik.com/download/fiddler

    3. 编程工具:pycharm

    4. 安卓模拟器上安装抖音(逍遥安装模拟器)

    一、fiddler配置

    在tools中的options中,按照图中勾选后点击Actions


    配置远程链接:

    选择允许监控远程链接,端口可以随意设置,只要别重复就行,默认8888


    然后:重启fiddler!!!这样配置才能生效。

    二、安卓模拟器/手机配置

    首先查看本机的IP:在cmd中输入ipconfig,记住这个IP


    手机确保和电脑在同一局域网下。

    手机配置:配置已连接的WiFi,代理选择手动,然后输入上图ip端口号为8888

    模拟器配置:设置中长按已连接wifi,代理选择手动,然后输入上图ip端口号为8888



    代理设置好后,在浏览器中输入你设置的ip:端口,例如10.10.16.194:8888,就会打开fiddler的页面。然后点击fiddlerRoot certificate安装证书,要不手机会认为环境不安全。

    证书名称随便设,可能还需要设置一个锁屏密码。


    接下来就可以在fiddler中抓到手机/模拟器软件的包了。

    三、抖音抓包

    打开抖音,然后观察fiddler中所有的包


    其中有个包,包类型为json(json就是网页返回的数据,具体百度),主机地址如图,包大小一般不小,这个就是视频包。

    点击这个json包,在fidder右侧,点击解码,我们将视频包的json解码

    解码后:点击aweme_list,其中每个大括号代表一个视频,这个和bilibili弹幕或者快手一样,每次加载一点出来,等你看完预加载的,再重新加载一些。


    Json是一个字典,我们的视频链接在:aweme_list中,每个视频下的video下的play_addr下的url_list中,一共有6个url,是完全一样的视频,可能是为了应付不同环境,但是一般第3或4个链接的视频不容易出问题,复制链接,浏览器中粘贴就能看到视频了。


    接下来解决几个问题

    1、视频数量,每个包中只有这么几个视频,那如何抓取更多呢?

    这时候需要借助模拟器的模拟鼠标翻页,让模拟器一直翻页,这样就不断会出现json包了。


    2、如何json保存在本地使用

    一种方法可以手动复制粘贴,但是这样很low。

    所以我们使用fidder自带的脚本,在里面添加规则,当视频json包刷出来后自动保存json包。

    自定义规则包:

    链接:https://pan.baidu.com/s/1wmtUUMChzuSDZFYGSyUhCg

    提取码:7z0l

    点击规则脚本,然后将自定义规则放在如图所示位置:


    这个脚本有两点需要修改的:

    (1)第一行的网址

    这个是从视频包的url中摘出来的,抖音会时不时更新这个url,所以不能用了也要去更新:

    比如现在的已经和昨天不同了,记着修改。

    (2)路径,那个是我设置json包保存的地址,自己一定要去修改,并创建文件夹,修改完记着点保存。


    打开设置好模拟器和脚本后,等待一会,就可以看到文件夹中保存的包了:

    四、爬虫脚本

    接下来在pycharm中写脚本获取json包里的视频链接:

    导包:

    import os,json,requests

    伪装头:

    headers = {‘User-Agent’: ‘Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36’}

    逻辑代码:

    运行代码:


    效果:

    源码:

    import os, json, requests# 伪装头
    headers = {
       'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36'
    }
    videos_list = os.listdir(
       'C:/Users/HEXU/Desktop/抖音数据爬取/抖音爬取资料/raw_data/')# 获取文件夹内所有json包名
    count = 1# 计数, 用来作为视频名字
    for videos in videos_list: #循环json列表, 对每个json包进行操作
    a = open('./抖音爬取资料/raw_data/{}'.format(videos), encoding = 'utf-8')# 打开json包
    content = json.load(a)['aweme_list']# 取出json包中所有视频
    for video in content: #循环视频列表, 选取每个视频
    video_url = video['video']['play_addr']['url_list'][4]# 获取视频url, 每个视频有6个url, 我选的第5个
    videoMp4 = requests.request('get', video_url, headers = headers).content# 获取视频二进制代码
    with open('./抖音爬取资料/VIDEO/{}.mp4'.format(count), 'wb') as f: #
       以二进制方式写入路径, 记住要先创建路径
    f.write(videoMp4)# 写入
    print('视频{}下载完成'.format(count))# 下载提示
    count += 1# 计数 + 1
    作者:冬晨夕阳
    来源:https://blog.51cto.com/lixi/3022373 收起阅读 »

    不想加班,你就背会这 10 条 JS 技巧

    为了让自己写的代码更优雅且高效,特意向大佬请教了这 10 条 JS 技巧1. 数组分割const listChunk = (list = [], chunkSize = 1) => {const result = [];const tmp = [...l...
    继续阅读 »



    为了让自己写的代码更优雅且高效,特意向大佬请教了这 10 条 JS 技巧

    1. 数组分割

    const listChunk = (list = [], chunkSize = 1) => {
    const result = [];
    const tmp = [...list];
    if (!Array.isArray(list) || !Number.isInteger(chunkSize) || chunkSize <= 0) {
    return result;
      };
    while (tmp.length) {
    result.push(tmp.splice(0, chunkSize));
      };
    return result;
    };
    listChunk(['a', 'b', 'c', 'd', 'e', 'f', 'g']);
    // [['a'], ['b'], ['c'], ['d'], ['e'], ['f'], ['g']]

    listChunk(['a', 'b', 'c', 'd', 'e', 'f', 'g'], 3);
    // [['a', 'b', 'c'], ['d', 'e', 'f'], ['g']]

    listChunk(['a', 'b', 'c', 'd', 'e', 'f', 'g'], 0);
    // []

    listChunk(['a', 'b', 'c', 'd', 'e', 'f', 'g'], -1);
    // []

    2. 求数组元素交集

    const listIntersection = (firstList, ...args) => {
    if (!Array.isArray(firstList) || !args.length) {
    return firstList;
      }
    return firstList.filter(item => args.every(list => list.includes(item)));
    };
    listIntersection([1, 2], [3, 4]);
    // []

    listIntersection([2, 2], [3, 4]);
    // []

    listIntersection([3, 2], [3, 4]);
    // [3]

    listIntersection([3, 4], [3, 4]);
    // [3, 4]

    3. 按下标重新组合数组

    const zip = (firstList, ...args) => {
    if (!Array.isArray(firstList) || !args.length) {
    return firstList
      };
    return firstList.map((value, index) => {
    const newArgs = args.map(arg => arg[index]).filter(arg => arg !== undefined);
    const newList = [value, ...newArgs];
    return newList;
      });
    };
    zip(['a', 'b'], [1, 2], [true, false]);
    // [['a', 1, true], ['b', 2, false]]

    zip(['a', 'b', 'c'], [1, 2], [true, false]);
    // [['a', 1, true], ['b', 2, false], ['c']]

    4. 按下标组合数组为对象

    const zipObject = (keys, values = {}) => {
    const emptyObject = Object.create({});
    if (!Array.isArray(keys)) {
    return emptyObject;
      };
    return keys.reduce((acc, cur, index) => {
    acc[cur] = values[index];
    return acc;
      }, emptyObject);
    };
    zipObject(['a', 'b'], [1, 2])
    // { a: 1, b: 2 }
    zipObject(['a', 'b'])
    // { a: undefined, b: undefined }

    5. 检查对象属性的值

    const checkValue = (obj = {}, objRule = {}) => {
    const isObject = obj => {
    return Object.prototype.toString.call(obj) === '[object Object]';
      };
    if (!isObject(obj) || !isObject(objRule)) {
    return false;
      }
    return Object.keys(objRule).every(key => objRule[key](obj[key]));
    };

    const object = { a: 1, b: 2 };

    checkValue(object, {
    b: n => n > 1,
    })
    // true

    checkValue(object, {
    b: n => n > 2,
    })
    // false

    6. 获取对象属性

    const get = (obj, path, defaultValue) => {
    if (!path) {
    return;
      };
    const pathGroup = Array.isArray(path) ? path : path.match(/([^[.\]])+/g);
    return pathGroup.reduce((prevObj, curKey) => prevObj && prevObj[curKey], obj) || defaultValue;
    };

    const obj1 = { a: { b: 2 } }
    const obj2 = { a: [{ bar: { c: 3 } }] }

    get(obj1, 'a.b')
    // 2
    get(obj2, 'a[0].bar.c')
    // 3
    get(obj2, ['a', '0', 'bar', 'c'])
    // 2
    get(obj1, 'a.bar.c', 'default')
    // default
    get(obj1, 'a.bar.c', 'default')
    // default

    7. 将特殊符号转成字体符号

    const escape = str => {
    const isString = str => {
    return Object.prototype.toString.call(str) === '[string Object]';
      };
    if (!isString(str)) {
    return str;
      }
    return (str.replace(/&/g, '&')
    .replace(/"/g, '"')
    .replace(/'/g, '&#x27;')
    .replace(/</g, '<')
    .replace(/>/g, '>')
    .replace(/\//g, '&#x2F;')
    .replace(/\\/g, '&#x5C;')
    .replace(/`/g, '&#96;'));
    };

    8. 利用注释创建一个事件监听器

    class EventEmitter {
    #eventTarget;
    constructor(content = '') {
    const comment = document.createComment(content);
    document.documentElement.appendChild(comment);
    this.#eventTarget = comment;
      }
    on(type, listener) {
    this.#eventTarget.addEventListener(type, listener);
      }
    off(type, listener) {
    this.#eventTarget.removeEventListener(type, listener);
      }
    once(type, listener) {
    this.#eventTarget.addEventListener(type, listener, { once: true });
      }
    emit(type, detail) {
    const dispatchEvent = new CustomEvent(type, { detail });
    this.#eventTarget.dispatchEvent(dispatchEvent);
      }
    };

    const emmiter = new EventEmitter();
    emmiter.on('biy', () => {
    console.log('hello world');
    });
    emmiter.emit('biu');
    // hello world

    9. 生成随机的字符串

    const genRandomStr = (len = 1) => {
    let result = '';
    for (let i = 0; i < len; ++i) {
    result += Math.random().toString(36).substr(2)
      }
    return result.substr(0, len);
    }
    genRandomStr(3)
    // u2d
    genRandomStr()
    // y
    genRandomStr(10)
    // qdueun65jb

    10. 判断是否是指定的哈希值

    const isHash = (type = '', str = '') => {
    const isString = str => {
    return Object.prototype.toString.call(str) === '[string Object]';
      };
    if (!isString(type) || !isString(str)) {
    return str;
      };
    const algorithms = {
    md5: 32,
    md4: 32,
    sha1: 40,
    sha256: 64,
    sha384: 96,
    sha512: 128,
    ripemd128: 32,
    ripemd160: 40,
    tiger128: 32,
    tiger160: 40,
    tiger192: 48,
    crc32: 8,
    crc32b: 8,
      };
    const hash = new RegExp(`^[a-fA-F0-9]{${algorithms[type]}}$`);
    return hash.test(str);
    };

    isHash('md5', 'd94f3f016ae679c3008de268209132f2');
    // true
    isHash('md5', 'q94375dj93458w34');
    // false

    isHash('sha1', '3ca25ae354e192b26879f651a51d92aa8a34d8d3');
    // true
    isHash('sha1', 'KYT0bf1c35032a71a14c2f719e5a14c1');
    // false

    后记

    如果你喜欢探讨技术,或者对本文有任何的意见或建议,非常欢迎加鱼头微信好友一起探讨,当然,鱼头也非常希望能跟你一起聊生活,聊爱好,谈天说地。

    全文完

    作者:酸菜鱼+黄焖鸡

    来源:https://blog.51cto.com/u_15291238/4538068

    收起阅读 »

    Three.js 随着元宇宙开启WEB3D之路

    元宇宙设想了一个由虚拟世界和3D技术广泛应用重塑的未来。 Three.js 是一个非常令人印象深刻的 JavaScript 3D 库,它也使用 WebGL(或 2d Canvas)进行渲染。随着 WebGL API 标准的改进,以及对 WebXR 的支持, T...
    继续阅读 »

    当视频游戏遇到Web 2.0时会发生什么? 当虚拟世界相遇地球的地理空间地图?当模拟变成现实,生活和商业变成虚拟?当你使用虚拟地球在物理地球上导航时,你的化身就变成了你的在线代理?这一切发生的就是元宇宙。

    元宇宙设想了一个由虚拟世界和3D技术广泛应用重塑的未来。 Three.js 是一个非常令人印象深刻的 JavaScript 3D 库,它也使用 WebGL(或 2d Canvas)进行渲染。随着 WebGL API 标准的改进,以及对 WebXR 的支持, Three.js 成为了一个可以用来营造沉浸式体验的主流工具。与此同时,浏览器对 3D 渲染和 WebXR 设备 API 的支持也得到提升,使得 web 成为一个越来越有吸引力的 3D 内容平台。

    Three.js

    Three.js Ricardo Cabello(@mrdoob) 在2010年开发的一个JavaScript库(如今它在Github上有许多贡献者)。这个令人难以置信的工具让用户可以在浏览器上处理 3D 图形,使用 WebGL 技术非常简单和直观的方式来实现。而 WebGL 技术已经非常普及和被浏览器广泛支持。


    WebGL 在许多设备和浏览器中创建丰富的交互体验, 点击查看浏览器支持程度。


    开始

    本文将以 Vue 为基础框架来构建 Three.js 示例,关于代码可以查阅 GitHub

    首先安装依赖:

    npm install three --save

    构建

    现在开始添加代码到页面,如下:

    <template>
    <div id="app"></div>
    </template>

    <script>
    import * as THREE from "three";
    export default {
    name: "App",
    data() {
    return {};
    },
    mounted() {
    this.init();
    },
    methods: {
    init() {
    const scene = new THREE.Scene();

    // 创建一个基本透视相机 camera
    const camera = new THREE.PerspectiveCamera(
    75,
    window.innerWidth / window.innerHeight,
    0.1,
    1000
    );
    camera.position.z = 4;

    // 创建一个抗锯齿渲染器
    const renderer = new THREE.WebGLRenderer({ antialias: true });

    // 配置渲染器清除颜色
    renderer.setClearColor("#000000");

    // 配置渲染器尺寸
    renderer.setSize(window.innerWidth, window.innerHeight);

    // 添加渲染器到DOM
    document.body.appendChild(renderer.domElement);

    // 创建一个立方体网格
    const geometry = new THREE.BoxGeometry(1, 1, 1);
    const material = new THREE.MeshBasicMaterial({ color: "#433F81" });
    const cube = new THREE.Mesh(geometry, material);

    // 将立方体到场景中
    scene.add(cube);

    const render = function () {
    requestAnimationFrame(render);

    cube.rotation.x += 0.01;
    cube.rotation.y += 0.01;

    renderer.render(scene, camera);
    };

    render();
    },
    },
    };
    </script>

    然后再添加简单的样式:

    body {
    margin: 0;
    }
    canvas {
    width: 100%;
    height: 100%;
    }

    执行脚本 yarn serve ,打开浏览器将看到如下效果:


    这是每个 Three.js 应用程序一种常见模式,包括 WebGlRendererSceneCamera ,如下:

    1. 创建渲染器WebGlRenderer

    2. 创建 Scene

    3. 创建Camera

    渲染器是放置场景结果的地方。在 Three.js 中,可以有多个场景,每个场景可以有不同的对象。


    在示例中创建 WebGLRenderer,再将窗口的大小作为参数传递给它,然后将它附加到 DOM 中。

    // 创建一个抗锯齿渲染器
    const renderer = new THREE.WebGLRenderer({ antialias: true });

    // 配置渲染器清除颜色
    renderer.setClearColor("#000000");

    // 配置渲染器尺寸
    renderer.setSize(window.innerWidth, window.innerHeight);

    // 添加渲染器到DOM
    document.body.appendChild(renderer.domElement);

    首先需要创建一个空场景,将在其中添加创建的立方体对象:

    const scene = new THREE.Scene();

    最后创建一个相机 camera ,将 视野 、纵横比以及近平面和远平面作为参数:

    const camera = new THREE.PerspectiveCamera(
    75,
    window.innerWidth / window.innerHeight,
    0.1,
    1000
    );

    到此 Three.js 应用程序的 3 个基本元素已经完成了。

    几何、材料和网格

    第二种常见模式是向场景添加对象:

    1. 创建几何

    2. 创建材质

    3. 创建网格

    4. 将网格添加到场景中。

    在 Three.js 中,网格是几何体与材质的组合。


    几何是对象的数学公式,在 Three.js 中有很多几何,将在以后的章节中探索它,几何体提供了要添加到场景中的对象的顶点。


    材质可以定义为对象的属性及其与场景光源的行为。如下图所示,有不同类型的材料。


    现在知道了网格、几何体和材质是什么,将把它们添加到场景中。在示例中,使用基本材质创建一个立方体:

    // 创建一个立方体网格
    const geometry = new THREE.BoxGeometry(1, 1, 1);
    const material = new THREE.MeshBasicMaterial({ color: "#1b55e3" });
    const cube = new THREE.Mesh(geometry, material);

    // 将立方体到场景中
    scene.add(cube);

    请求动画帧

    最后一段代码是为场景设置动画,使用 requestAnimationFrame,它允许有一个以每秒 60 帧(最多)运行的函数。

    const render = () => {
      requestAnimationFrame(render);
      renderer.render(scene, camera);
    };

    render();

    立方体动画

    为了在渲染循环中为立方体设置动画,需要更改它的属性。当创建一个网格时,可以访问一组在动画制作时非常有用的属性。

    // Rotation (XYZ) 弧度
    cube.rotation.x;
    cube.rotation.y;
    cube.rotation.z;

    // Position (XYZ)
    cube.position.x;
    cube.position.y;
    cube.position.z;

    // Scale (XYZ)
    cube.scale.x;
    cube.scale.y;
    cube.scale.z;

    在示例中,为立方体设置 X 和 Y 旋转动画:

    cube.rotation.x += 0.01;
    cube.rotation.y += 0.01;

    控制台

    作为前端,控制台是最好的调试工具。当使用 Three.js 时,控制台是必不可少的工具。

    效果增持

    现在理解了示例的逻辑,将向场景中添加更多片段,目的是生成更复杂的片段。

    /**
    * 创建材质的方法
    */
    const createMesh = (boxOptions, meshOptions) => {
      const geometry = new THREE.BoxGeometry(...boxOptions);
      const material = new THREE.MeshBasicMaterial(meshOptions);
      return new THREE.Mesh(geometry, material);
    };

    const cube01 = createMesh([1, 1, 1], {
      color: "#A49FEF",
      wireframe: true,
      transparent: true,
    });
    scene.add(cube01);

    const cube01_wireframe = createMesh([3, 3, 3], {
      color: "#433F81",
      wireframe: true,
      transparent: true,
    });
    scene.add(cube01_wireframe);

    const cube02 = createMesh([1, 1, 1], {
      color: "#A49FEF",
    });
    scene.add(cube02);

    const cube02_wireframe = createMesh([3, 3, 3], {
      color: "#433F81",
      wireframe: true,
      transparent: true,
    });

    scene.add(cube02_wireframe);

    const bar01 = createMesh([10, 0.05, 0.5], {
      color: "#00FFBC",
    });
    bar01.position.z = 0.5;
    scene.add(bar01);

    const bar02 = createMesh([10, 0.05, 0.5], {
      color: "#ffffff",
    });
    bar02.position.z = 0.5;
    scene.add(bar02);

    const render = () => {
      requestAnimationFrame(render);

      cube01.rotation.x += 0.01;
      cube01.rotation.y += 0.01;

      cube01_wireframe.rotation.x += 0.01;
      cube01_wireframe.rotation.y += 0.01;

      cube02.rotation.x -= 0.01;
      cube02.rotation.y -= 0.01;

      cube02_wireframe.rotation.x -= 0.01;
      cube02_wireframe.rotation.y -= 0.01;

      bar01.rotation.z -= 0.01;
      bar02.rotation.z += 0.01;

      renderer.render(scene, camera);
    };

    render();

    运行后效果如图:


    作者:天行无忌
    来源:http://blog.51cto.com/u_15088848/4670782

    收起阅读 »

    尤大亲自解释vue3源码中为什么不使用?.可选链式操作符?

    vue
    阅读本文🦀 1.什么是可选链式操作符号 2.为什么vue3源码中不使用可选链式操作符 什么是可选链式操作符号❓ 可选链操作符( ?. )允许读取位于连接对象链深处的属性的值,而不必明确验证链中的每个引用是否有效。?. 操作符的功能类似于 . 链式操作符,不同之...
    继续阅读 »

    阅读本文🦀


    1.什么是可选链式操作符号


    2.为什么vue3源码中不使用可选链式操作符


    什么是可选链式操作符号❓


    可选链操作符( ?. )允许读取位于连接对象链深处的属性的值,而不必明确验证链中的每个引用是否有效。?. 操作符的功能类似于 . 链式操作符,不同之处在于,在引用为空(nullish ) (null 或者 undefined) 的情况下不会引起错误,该表达式短路返回值是 undefined。与函数调用一起使用时,如果给定的函数不存在,则返回 undefined


    当尝试访问可能不存在的对象属性时,可选链操作符将会使表达式更短、更简明。在探索一个对象的内容时,如果不能确定哪些属性必定存在,可选链操作符也是很有帮助的。


    const adventurer = {
    name: 'Alice',
    cat: {
    name: 'Dinah'
    }
    };

    const dogName = adventurer.dog?.name;
    console.log(dogName);
    // expected output: undefined

    console.log(adventurer.someNonExistentMethod?.());
    // expected output: undefined


    短路效应


    如果 ?. 左边部分不存在,就会立即停止运算(“短路效应”)。


    所以,如果后面有任何函数调用或者副作用,它们均不会执行。


    let user = null; 
    let x = 0;
    user?.sayHi(x++);
    // 没有 "sayHi",因此代码执行没有触达 x++ alert(x); // 0,值没有增加

    Vue3源码中为什么不采用这么方便的操作符


    image-20211114120351836


    看看这样是不是代码更简洁了,但是为什么这个PR没有被合并呢


    来自尤大的亲自解释


    image-20211114120545284


    (我们有意避免在代码库中使用可选链,因为我们的目标是 ES2016,而 TS 会将其转换为更加冗长的内容)


    从尤大的话中我们可以得知由于Vu3打包后的代码是基于ES2016的,虽然我们在编写代码时看起来代码比较简洁了,实际打包之后反而更冗余了,这样会增大包的体积,影响Vu3的加载速度。由此可见一个优秀的前端框架真的要考虑的东西很多,语法也会考虑周到~✨



    作者:速冻鱼
    链接:https://juejin.cn/post/7033167068895641637

    收起阅读 »

    想知道一个20k级别前端在项目中是怎么使用LocalStorage的吗?

    前言 大家好,我是林三心,用最通俗的话,讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心,今天就给大家唠一下嗑,讲一下,怎么样使用localStorage、sessionStorage,才能更规范,更高大上,更能让人眼前一亮。 用处 在平时的开发中,lo...
    继续阅读 »

    前言


    大家好,我是林三心,用最通俗的话,讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心,今天就给大家唠一下嗑,讲一下,怎么样使用localStorage、sessionStorage,才能更规范,更高大上,更能让人眼前一亮。


    用处


    在平时的开发中,localStorage、sessionStorage的用途是非常的多的,在我们的开发中发挥着非常重要的作用:



    • 1、登录完成后token的存储

    • 2、用户部分信息的存储,比如昵称、头像、简介

    • 3、一些项目通用参数的存储,例如某个id、某个参数params

    • 4、项目状态管理的持久化,例如vuex的持久化、redux的持久化

    • 5、项目整体的切换状态存储,例如主题颜色、icon风格、语言标识

    • 6、等等、、、、、、、、、、、、、、、、、、、、、、、、、、


    普通使用


    那么,相信我们各位平时使用都是这样的(拿localStorage举例)


    1、基础变量


    // 当我们存基本变量时
    localStorage.setItem('基本变量', '这是一个基本变量')
    // 当我们取值时
    localStorage.getItem('基本变量')
    // 当我们删除时
    localStorage.removeItem('基本变量')

    2、引用变量


    // 当我们存引用变量时
    localStorage.setItem('引用变量', JSON.stringify(data))
    // 当我们取值时
    const data = JSON.parse(localStorage.getItem('引用变量'))
    // 当我们删除时
    localStorage.removeItem('引用变量')

    3、清空


    localStorage.clear()

    暴露出什么问题?


    1、命名过于简单



    • 1、比如我们存用户信息会使用user作为 key 来存储

    • 2、存储主题的时候用theme 作为 key 来存储

    • 3、存储令牌时使用token作为 key 来存储


    其实这是很有问题的,咱们都知道,同源的两个项目,它们的localStorage是互通的。


    我举个例子吧比如我现在有两个项目,它们在同源https://www.sunshine.com下,这两个项目都需要往localStorage中存储一个 key 为name的值,那么这就会造成两个项目的name互相顶替的现象,也就是互相污染现象


    截屏2021-11-10 下午10.19.09.png


    2、时效性


    咱们都知道localStorage、sessionStorage这两个的生命周期分别是



    • localStorage:除非手动清除,否则一直存在

    • sessionStorage:生命结束于当前标签页的关闭或浏览器的关闭


    其实平时普通的使用时没什么问题的,但是给某些指定缓存加上特定的时效性,是非常重要的!比如某一天:



    • 后端:”兄弟,你一登录我就把token给你“

    • 前端:”好呀,那你应该会顺便判断token过期没吧?“

    • 后端:”不行哦,放在你前端判断过期呗“

    • 前端:”行吧。。。。。“


    那这时候,因为需要在前端判断过期,所以咱们就得给token设置一个时效性,或者是1天,或者是7天


    截屏2021-11-10 下午10.48.50.png


    3、隐秘性


    其实这个好理解,你们想想,当咱们把咱们想缓存的东西,存在localStorage、sessionStorage中,在开发过程中,确实有利于咱们的开发,咱们想看的时候也是一目了然,点击Application就可以看到。


    但是,一旦产品上线了,用户也是可以看到缓存中的东西的,而咱们肯定是会想:有些东西可以让用户看到,但是有些东西我不想让你看到


    截屏2021-11-10 下午11.02.24.png


    或者咱们在做状态管理持久化时,需要把数据先存在localStorage中,这个时候就很有必要对缓存进行加密了。


    解决方案


    1、命名规范


    我个人的看法是项目名 + 当前环境 + 项目版本 + 缓存key,如果大家有其他规则的,可以评论区告诉林三心,让林三心学学


    截屏2021-11-11 下午9.12.32.png


    2、expire定时


    思路:设置缓存key时,将value包装成一个对象,对象中有相应的时效时段,当下一次想获取缓存值时,判断有无超时,不超时就获取value,超时就删除这个缓存


    截屏2021-11-11 下午9.33.00.png


    3、crypto加密


    加密很简单,直接使用crypto-js进行对数据的加密,使用这个库里的encrypt、decrypyt进行加密、解密


    截屏2021-11-11 下午9.43.16.png


    实践


    其实实践的话比较简单啦,无非就是四步



    • 1、与团队商讨一下key的格式

    • 2、与团队商讨一下expire的长短

    • 3、与团队商讨一下使用哪个库来对缓存进行加密(个人建议crypto-js

    • 4、代码实施(不难,我这里就不写了)


    结语


    有人可能觉得没必要,但是严格要求自己其实是很有必要的,平时严格要求自己,才能做到每到一个公司都能更好的做到向下兼容难度。


    如果你觉得此文对你有一丁点帮助,点个赞,鼓励一下林三心哈哈。或者可以加入我的摸鱼群 想进学习群,摸鱼群,请点击这里摸鱼,我会定时直播模拟面试,简历指导,答疑解惑


    image.png


    作者:Sunshine_Lin
    链接:https://juejin.cn/post/7033749571939336228

    收起阅读 »

    巧用渐变实现高级感拉满的背景光动画

    实现 这个效果想利用 CSS 完全复制是比较困难的。CSS 模拟出来的光效阴影相对会 Low 一点,只能说是尽量还原。 其实每组光都基本是一样的,所以我们只需要实现其中一组,就几乎能实现了整个效果。 观察这个效果: 它的核心其实就是角向渐变 -- conic...
    继续阅读 »

    141609598-e0a1e420-2967-4ce4-8086-bfef1233f5f6.gif


    实现


    这个效果想利用 CSS 完全复制是比较困难的。CSS 模拟出来的光效阴影相对会 Low 一点,只能说是尽量还原。


    其实每组光都基本是一样的,所以我们只需要实现其中一组,就几乎能实现了整个效果。


    观察这个效果:



    它的核心其实就是角向渐变 -- conic-gradient(),利用角向渐变,我们可以大致实现这样一个效果:


    <div></div>

    div {
    width: 1000px;
    height: 600px;
    background:
    conic-gradient(
    from -45deg at 400px 300px,
    hsla(170deg, 100%, 70%, .7),
    transparent 50%,
    transparent),
    linear-gradient(-45deg, #060d5e, #002268);
    }

    看看效果:



    有点那意思了。当然,仔细观察,渐变的颜色并非是由一种颜色到透明就结束了,而是颜色 A -- 透明 -- 颜色 B,这样,光源的另一半并非就不会那么生硬,改造后的 CSS 代码:


    div {
    width: 1000px;
    height: 600px;
    background:
    conic-gradient(
    from -45deg at 400px 300px,
    hsla(170deg, 100%, 70%, .7),
    transparent 50%,
    hsla(219deg, 90%, 80%, .5) 100%),
    linear-gradient(-45deg, #060d5e, #002268);
    }

    我们在角向渐变的最后多加了一种颜色,得到观感更好的一种效果:



    emm,到这里,我们会发现,仅仅是角向渐变 conic-gradient() 是不够的,它无法模拟出光源阴影的效果,所以必须再借助其他属性实现光源阴影的效果。


    这里,我们会很自然的想到 box-shadow。这里有个技巧,利用多重 box-shadow, 实现 Neon 灯的效果。


    我们再加个 div,通过它实现光源阴影:


    <div class="shadow"></div>

    .shadow {
    width: 200px;
    height: 200px;
    background: #fff;
    box-shadow:
    0px 0 .5px hsla(170deg, 95%, 80%, 1),
    0px 0 1px hsla(170deg, 91%, 80%, .95),
    0px 0 2px hsla(171deg, 91%, 80%, .95),
    0px 0 3px hsla(171deg, 91%, 80%, .95),
    0px 0 4px hsla(171deg, 91%, 82%, .9),
    0px 0 5px hsla(172deg, 91%, 82%, .9),
    0px 0 10px hsla(173deg, 91%, 84%, .9),
    0px 0 20px hsla(174deg, 91%, 86%, .85),
    0px 0 40px hsla(175deg, 91%, 86%, .85),
    0px 0 60px hsla(175deg, 91%, 86%, .85);
    }


    OK,光是有了,但问题是我们只需要一侧的光,怎么办呢?裁剪的方式很多,这里,我介绍一种利用 clip-path 进行对元素任意空间进行裁切的方法:


    .shadow {
    width: 200px;
    height: 200px;
    background: #fff;
    box-shadow: .....;
    clip-path: polygon(-100% 100%, 200% 100%, 200% 500%, -100% 500%);
    }

    原理是这样的:



    这样,我们就得到了一侧的光:



    这里,其实 CSS 也是有办法实现单侧阴影的(你所不知道的 CSS 阴影技巧与细节),但是实际效果并不好,最终采取了上述的方案。


    接下来,就是利用定位、旋转等方式,将上述单侧光和角向渐变重叠起来,我们就可以得到这样的效果:


    image


    这会,已经挺像了。接下来要做的就是让整个图案,动起来。这里技巧也挺多的,核心还是利用了 CSS @Property,实现了角向渐变的动画,并且让光动画和角向渐变重叠起来。


    我们需要利用 CSS @Property 对代码渐变进行改造,核心代码如下:


    <div class="wrap">
    <div class="shadow"></div>
    </div>

    @property --xPoint {
    syntax: '<length>';
    inherits: false;
    initial-value: 400px;
    }
    @property --yPoint {
    syntax: '<length>';
    inherits: false;
    initial-value: 300px;
    }

    .wrap {
    position: relative;
    margin: auto;
    width: 1000px;
    height: 600px;
    background:
    conic-gradient(
    from -45deg at var(--xPoint) var(--yPoint),
    hsla(170deg, 100%, 70%, .7),
    transparent 50%,
    hsla(219deg, 90%, 80%, .5) 100%),
    linear-gradient(-45deg, #060d5e, #002268);
    animation: pointMove 2.5s infinite alternate linear;
    }

    .shadow {
    position: absolute;
    top: -300px;
    left: -330px;
    width: 430px;
    height: 300px;
    background: #fff;
    transform-origin: 100% 100%;
    transform: rotate(225deg);
    clip-path: polygon(-100% 100%, 200% 100%, 200% 500%, -100% 500%);
    box-shadow: ... 此处省略大量阴影代码;
    animation: scale 2.5s infinite alternate linear;
    }

    @keyframes scale {
    50%,
    100% {
    transform: rotate(225deg) scale(0);
    }
    }

    @keyframes pointMove {
    100% {
    --xPoint: 100px;
    --yPoint: 0;
    }
    }

    这样,我们就实现了完整的一处光的动画:



    我们重新梳理一下,实现这样一个动画的步骤:



    1. 利用角向渐变 conic-gradient 搭出基本框架,并且,这里也利用了多重渐变,角向渐变的背后是深色背景色;

    2. 利用多重 box-shadow 实现光及阴影的效果(又称为 Neon 效果)

    3. 利用 clip-path 对元素进行任意区域的裁剪

    4. 利用 CSS @Property 实现渐变的动画效果


    剩下的工作,就是重复上述的步骤,补充其他渐变和光源,调试动画,最终,我们就可以得到这样一个简单的模拟效果:



    由于原效果是 .mp4,无法拿到其中的准确颜色,无法拿到阴影的参数,其中颜色是直接用的色板取色,阴影则比较随意的模拟了下,如果有源文件,准确参数,可以模拟的更逼真。


    完整的代码你可以戳这里:CodePen -- iPhone 13 Pro Gradient


    作者:chokcoco
    链接:https://juejin.cn/post/7033952765151805453

    收起阅读 »

    vite对浏览器的请求做了什么

    工作原理:type="module" 浏览器中ES Module原生native支持。 如果浏览器支持type="module" ,我i们可以使用es6模块化的方式编写。浏览器会把我们需要导入的文件再发一次http请求,再发到服务器上。 开...
    继续阅读 »

    工作原理:

    • type="module" 浏览器中ES Module原生native支持。 如果浏览器支持type="module" ,我i们可以使用es6模块化的方式编写。浏览器会把我们需要导入的文件再发一次http请求,再发到服务器上。 开发阶段不需要打包
    • 第三方依赖预打包
    • 启动一个开发服务器处理资源请求

    一图详解vite原理:


    6F2(QRN))B}6D@~KQN8CYD0.png


    浏览器做的什么事啊


    宿主文件index.html


    <script type="module" src="/src/main.js"></script>

    浏览器获取到宿主文件中的资源后,发现还要再去请求main.js文件。会再向服务端发送一次main.js的资源请求。


    image.png


    main.js


    在main中,可以发现,浏览器又再次发起对vue.js?v=d253a66cApp.vue?t=1637479953836两个文件的资源请求。


    服务器会将App.vue中的内容进行编译然后返回给浏览器,下图可以看出logo图片和文字都被编译成_hoisted_ 的静态节点。


    image.png
    从请求头中,也可以看出sfc文件已经变成浏览器可以识别的js文件(app.vue文件中要存在script内容才会编译成js)。对于浏览器来说,执行的就是一段js代码。


    image.png


    其他裸模块


    如果vue依赖中还存在其他依赖的话,浏览器依旧会再次发起资源请求,获取相应资源。


    了解一下预打包


    对于第三方依赖(裸模块)的加载,vite对其提前做好打包工作,将其放到node_modules/.vite下。当启动项目的时候,直接从该路径下下载文件。


    1637397635556.png
    通过上图,可以看到再裸模块的引入时,路径发生了改变。


    服务器做的什么事啊


    总结一句话:服务器把特殊后缀名的文件进行处理返回给前端展示


    我们可以模拟vite的devServe,使用koa中间件启动一个本地服务。


    // 引入依赖
    const Koa = require('koa')
    const app = new Koa()
    const fs = require('fs')
    const path = require('path')
    const compilerSfc = require('@vue/compiler-sfc')
    const compilerDom = require('@vue/compiler-dom')

    app.use(async (ctx) => {
    const { url, query } = ctx.request
    // 处理请求资源代码都写这
    })
    zaiz都h这z都he在
    app.listen(3001, () => {
    console.log('dyVite start!!')
    })

    请求首页index.html


     if (url === '/') {
    const p = path.join(__dirname, './index.html') // 绝对路径
    // 首页
    ctx.type = 'text/html'
    ctx.body = fs.readFileSync(p, 'utf8')
    }

    1637475203111.png


    看到上面这张图,就知道我们的宿主文件已经请求成功了。只是浏览器又给服务端发送的一个main.js文件的请求。这时,我们还需要判断处理一下main.js文件。


    请求以.js结尾的文件


    我们处理上述情况后,emmmm。。。发现main中还是存在好多其他资源请求。


    基础js文件


    main文件:


    console.log(1)

    处理main:


    else if (url.endsWith('.js')) {
       // 响应js请求
       const p = path.join(__dirname, url)
       ctx.type = 'text/javascript'
       ctx.body = rewriteImport(fs.readFileSync(p, 'utf8')) // 处理依赖函数
    }

    对main中的依赖进行处理


    你以为main里面就一个输出吗?太天真了。这样的还能处理吗?


    main文件:


    import { createApp, h } from 'vue'
    createApp({ render: () => h('div', 'helllo dyVite!') }).mount('#app')

    emmm。。。应该可以!


    我们可以将main中导入的地址变成相对地址。

    在裸模块路径添加上/@modules/。再去识别/@modules/的文件即(裸模块文件)。


    // 把能读出来的文件地址变成相对地址
    // 正则替换 重写导入 变成相对地址
    // import { createApp } from 'vue' => import { createApp } from '/@modules/vue'
    function rewriteImport(content) {
    return content.replace(/ from ['|"](.*)['|"]/g, function (s0, s1) {
    // s0匹配字符串,s1分组内容
    // 是否是相对路径
    if (s1.startsWith('./') || s1.startsWith('/') || s1.startsWith('../')) {
    // 直接返回
    return s0
    } else {
    return ` from '/@modules/${s1}'`
    }
    })
    }

    对于第三方依赖,vite内部是使用预打包请求自己服务器/node_modules/.vite/下的内部资源。
    我们可以简单化一点,将拿到的依赖名去客户端下的node_modules下拿相应的资源。


      else if (url.startsWith('/@modules/')) {
    // 裸模块的加载
    const moduleName = url.replace('/@modules/', '')
    const pre![1637477009328](imgs/1637477009328.png)![1637477009368](imgs/1637477009368.png)的地址
    const module = require(prefix + '/package.json').module
    const filePath = path.join(prefix, module) // 拿到文件加载的地址
    // 读取相关依赖
    const ret = fs.readFileSync(filePath, 'utf8')
    ctx.type = 'text/javascript'
    ctx.body = rewriteImport(ret) //依赖内部可能还存在依赖,需要递归
    }

    在main中进行render时,会报下图错误:


    1637477015346.png


    我们加载的文件都是服务端执行的库,内部可能会产生node环境的代码,需要判断一下环境变量。如果开发时,会输出一些警告信息,但是在前端是没有的。所以我们需要mock一下,告诉浏览器我们当前的环境。


    给html加上process环境变量。


      <script>
       window.process = { env: { NODE_ENV: 'dev' } }
     </script>

    此时main文件算是加载出来了。


    但是这远远打不到我们的目的啊!


    我们需要的是可以编译vue文件的服务器啊!


    处理.vue文件


    main.js文件:


    import { createApp, h } from 'vue'
    import App from './App.vue'
    createApp(App).mount('#app')

    在vue文件中,它是模块化加载的。


    1637477806326.png


    我们需要在处理vue文件的时候,对.vue后面携带的参数做处理。


    在此,我们简化只考虑template和sfc情况。


    else if (url.indexOf('.vue') > -1) {
    // 处理vue文件 App.vue?vue&type=style&index=0&lang.css
    // 读取vue内容
    const p = path.join(__dirname, url.split('?')[0])
    // compilerSfc解析sfc 获得ast
    const ret = compilerSfc.parse(fs.readFileSync(p, 'utf8'))
    // App.vue?type=template
    // 如果请求没有query.type 说明是sfc
    if (!query.type) {
    // 处理内部的script
    const scriptContent = ret.descriptor.script.content
    // 将默认导出配置对象转为常量
    const script = scriptContent.replace(
    'export default ',
    'const __script = ',
    )
    ctx.type = 'text/javascript'
    ctx.body = `
    ${rewriteImport(script)}
    // template解析转换为单独请求一个资源
    import {render as __render} from '${url}?type=template'
    __script.render = __render
    export default __script
    `
    } else if (query.type === 'template') {
    const tpl = ret.descriptor.template.content
    // 编译包含render模块
    const render = compilerDom.compile(tpl, { mode: 'module' }).code
    ctx.type = 'text/javascript'
    ctx.body = rewriteImport(render)
    }
    }

    处理图片路径


    直接从客户端读取返回。


     else if (url.endsWith('.png')) {
       ctx.body = fs.readFileSync('src' + url)
    }


    作者:ClyingDeng
    链接:https://juejin.cn/post/7033713960784248868

    收起阅读 »

    别再问我 new 字符串创建了几个对象了!我来证明给你看!

    我想所有 Java 程序员都曾被这个 new String 的问题困扰过,这是一道高频的 Java 面试题,但可惜的是网上众说纷纭,竟然找不到标准的答案。有人说创建了 1 个对象,也有人说创建了 2 个对象,还有人说可能创建了 1 个或 2 个对象,但谁都没有...
    继续阅读 »

    我想所有 Java 程序员都曾被这个 new String 的问题困扰过,这是一道高频的 Java 面试题,但可惜的是网上众说纷纭,竟然找不到标准的答案。有人说创建了 1 个对象,也有人说创建了 2 个对象,还有人说可能创建了 1 个或 2 个对象,但谁都没有拿出干掉对方的证据,这就让我们这帮吃瓜群众们陷入了两难之中,不知道到底该信谁得。


    但是今天,老王就斗胆和大家聊聊这个话题,顺便再拿出点证据


    以目前的情况来看,关于 new String("xxx") 创建对象个数的答案有 3 种:



    1. 有人说创建了 1 个对象;

    2. 有人说创建了 2 个对象;

    3. 有人说创建了 1 个或 2 个对象。


    而出现多个答案的关键争议点在「字符串常量池」上,有的说 new 字符串的方式会在常量池创建一个字符串对象,有人说 new 字符串的时候并不会去字符串常量池创建对象,而是在调用 intern() 方法时,才会去字符串常量池检测并创建字符串。


    那我们就先来说说这个「字符串常量池」。


    字符串常量池


    字符串的分配和其他的对象分配一样,需要耗费高昂的时间和空间为代价,如果需要大量频繁的创建字符串,会极大程度地影响程序的性能,因此 JVM 为了提高性能和减少内存开销引入了字符串常量池(Constant Pool Table)的概念。


    字符串常量池相当于给字符串开辟一个常量池空间类似于缓存区,对于直接赋值的字符串(String s="xxx")来说,在每次创建字符串时优先使用已经存在字符串常量池的字符串,如果字符串常量池没有相关的字符串,会先在字符串常量池中创建该字符串,然后将引用地址返回变量,如下图所示:


    字符串常量池示意图.png


    以上说法可以通过如下代码进行证明:


    public class StringExample {
    public static void main(String[] args) {
    String s1 = "Java";
    String s2 = "Java";
    System.out.println(s1 == s2);
    }
    }

    以上程序的执行结果为:true,说明变量 s1 和变量 s2 指向的是同一个地址。


    在这里我们顺便说一下字符串常量池的再不同 JDK 版本的变化。


    这里,顺便送大家一份经典学习资料,我把大学和工作中用的经典电子书库(包含数据结构、操作系统、C++/C、网络经典、前端编程经典、Java相关、程序员认知、职场发展)、面试找工作的资料汇总都打包放在这。



    戳这里直接获取:


    计算机经典必读书单(含下载方式)


    Java 入门到精通含面试最全资料包(含下载方式)


    常量池的内存布局


    JDK 1.7 之后把永生代换成的元空间,把字符串常量池从方法区移到了 Java 堆上


    JDK 1.7 内存布局如下图所示:


    JDK 1.7 内存布局.png


    JDK 1.8 内存布局如下图所示:


    JDK 1.8 内存布局.png


    JDK 1.8 与 JDK 1.7 最大的区别是 JDK 1.8 将永久代取消,并设立了元空间。官方给的说明是由于永久代内存经常不够用或发生内存泄露,会爆出 java.lang.OutOfMemoryError: PermGen 的异常,所以把将永久区废弃而改用元空间了,改为了使用本地内存空间,官网解释详情:openjdk.java.net/jeps/122


    答案解密


    认为 new 方式创建了 1 个对象的人认为,new String 只是在堆上创建了一个对象,只有在使用 intern() 时才去常量池中查找并创建字符串。


    认为 new 方式创建了 2 个对象的人认为,new String 会在堆上创建一个对象,并且在字符串常量池中也创建一个字符串。


    认为 new 方式有可能创建 1 个或 2 个对象的人认为,new String 会先去常量池中判断有没有此字符串,如果有则只在堆上创建一个字符串并且指向常量池中的字符串,如果常量池中没有此字符串,则会创建 2 个对象,先在常量池中新建此字符串,然后把此引用返回给堆上的对象,如下图所示:


    new 字符串常量池.png


    老王认为正确的答案:创建 1 个或者 2 个对象


    技术论证


    解铃还须系铃人,回到问题的那个争议点上,new String 到底会不会在常量池中创建字符呢?我们通过反编译下面这段代码就可以得出正确的结论,代码如下:


    public class StringExample {
    public static void main(String[] args) {
    String s1 = new String("javaer-wang");
    String s2 = "wang-javaer";
    String s3 = "wang-javaer";
    }
    }

    首先我们使用 javac StringExample.java 编译代码,然后我们再使用 javap -v StringExample 查看编译的结果,相关信息如下:


    Classfile /Users/admin/github/blog-example/blog-example/src/main/java/com/example/StringExample.class
    Last modified 2020年4月16日; size 401 bytes
    SHA-256 checksum 89833a7365ef2930ac1bc3d7b88dcc5162da4b98996eaac397940d8997c94d8e
    Compiled from "StringExample.java"
    public class com.example.StringExample
    minor version: 0
    major version: 58
    flags: (0x0021) ACC_PUBLIC, ACC_SUPER
    this_class: #16 // com/example/StringExample
    super_class: #2 // java/lang/Object
    interfaces: 0, fields: 0, methods: 2, attributes: 1
    Constant pool:
    #1 = Methodref #2.#3 // java/lang/Object."<init>":()V
    #2 = Class #4 // java/lang/Object
    #3 = NameAndType #5:#6 // "<init>":()V
    #4 = Utf8 java/lang/Object
    #5 = Utf8 <init>
    #6 = Utf8 ()V
    #7 = Class #8 // java/lang/String
    #8 = Utf8 java/lang/String
    #9 = String #10 // javaer-wang
    #10 = Utf8 javaer-wang
    #11 = Methodref #7.#12 // java/lang/String."<init>":(Ljava/lang/String;)V
    #12 = NameAndType #5:#13 // "<init>":(Ljava/lang/String;)V
    #13 = Utf8 (Ljava/lang/String;)V
    #14 = String #15 // wang-javaer
    #15 = Utf8 wang-javaer
    #16 = Class #17 // com/example/StringExample
    #17 = Utf8 com/example/StringExample
    #18 = Utf8 Code
    #19 = Utf8 LineNumberTable
    #20 = Utf8 main
    #21 = Utf8 ([Ljava/lang/String;)V
    #22 = Utf8 SourceFile
    #23 = Utf8 StringExample.java
    {
    public com.example.StringExample();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
    stack=1, locals=1, args_size=1
    0: aload_0
    1: invokespecial #1 // Method java/lang/Object."<init>":()V
    4: return
    LineNumberTable:
    line 3: 0

    public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
    stack=3, locals=4, args_size=1
    0: new #7 // class java/lang/String
    3: dup
    4: ldc #9 // String javaer-wang
    6: invokespecial #11 // Method java/lang/String."<init>":(Ljava/lang/String;)V
    9: astore_1
    10: ldc #14 // String wang-javaer
    12: astore_2
    13: ldc #14 // String wang-javaer
    15: astore_3
    16: return
    LineNumberTable:
    line 5: 0
    line 6: 10
    line 7: 13
    line 8: 16
    }
    SourceFile: "StringExample.java"


    备注:以上代码的运行也编译环境为 jdk1.8.0_101。



    其中 Constant pool 表示字符串常量池,我们在字符串编译期的字符串常量池中找到了我们 String s1 = new String("javaer-wang"); 定义的“javaer-wang”字符,在信息 #10 = Utf8 javaer-wang 可以看出,也就是在编译期 new 方式创建的字符串就会被放入到编译期的字符串常量池中,也就是说 new String 的方式会首先去判断字符串常量池,如果没有就会新建字符串那么就会创建 2 个对象,如果已经存在就只会在堆中创建一个对象指向字符串常量池中的字符串。


    那么问题来了,以下这段代码的执行结果为 true 还是 false?


    String s1 = new String("javaer-wang");
    String s2 = new String("javaer-wang");
    System.out.println(s1 == s2);

    既然 new String 会在常量池中创建字符串,那么执行的结果就应该是 true 了。其实并不是,这里对比的变量 s1 和 s2 堆上地址,因为堆上的地址是不同的,所以结果一定是 false,如下图所示:


    字符串引用.png


    从图中可以看出 s1 和 s2 的引用一定是相同的,而 s3 和 s4 的引用是不同的,对应的程序代码如下:


    public static void main(String[] args) {
    String s1 = "Java";
    String s2 = "Java";
    String s3 = new String("Java");
    String s4 = new String("Java");
    System.out.println(s1 == s2);
    System.out.println(s3 == s4);
    }

    程序执行的结果也符合预期:



    true false



    扩展知识


    我们知道 String 是 final 修饰的,也就是说一定被赋值就不能被修改了。但编译器除了有字符串常量池的优化之外,还会对编译期可以确认的字符串进行优化,例如以下代码:


    public static void main(String[] args) {
    String s1 = "abc";
    String s2 = "ab" + "c";
    String s3 = "a" + "b" + "c";
    System.out.println(s1 == s2);
    System.out.println(s1 == s3);
    }

    按照 String 不能被修改的思想来看,s2 应该会在字符串常量池创建两个字符串“ab”和“c”,s3 会创建三个字符串,他们的引用对比结果也一定是 false,但其实不是,他们的结果都是 true,这是编译器优化的功劳。


    同样我们使用 javac StringExample.java 先编译代码,再使用 javap -c StringExample 命令查看编译的代码如下:


    警告: 文件 ./StringExample.class 不包含类 StringExample
    Compiled from "StringExample.java"
    public class com.example.StringExample {
    public com.example.StringExample();
    Code:
    0: aload_0
    1: invokespecial #1 // Method java/lang/Object."<init>":()V
    4: return

    public static void main(java.lang.String[]);
    Code:
    0: ldc #7 // String abc
    2: astore_1
    3: ldc #7 // String abc
    5: astore_2
    6: ldc #7 // String abc
    8: astore_3
    9: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
    12: aload_1
    13: aload_2
    14: if_acmpne 21
    17: iconst_1
    18: goto 22
    21: iconst_0
    22: invokevirtual #15 // Method java/io/PrintStream.println:(Z)V
    25: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
    28: aload_1
    29: aload_3
    30: if_acmpne 37
    33: iconst_1
    34: goto 38
    37: iconst_0
    38: invokevirtual #15 // Method java/io/PrintStream.println:(Z)V
    41: return
    }

    从 Code 3、6 可以看出字符串都被编译器优化成了字符串“abc”了。




    总结


    本文我们通过 javap -v XXX 的方式查看编译的代码发现 new String 首次会在字符串常量池中创建此字符串,那也就是说,通过 new 创建字符串的方式可能会创建 1 个或 2 个对象,如果常量池中已经存在此字符串只会在堆上创建一个变量,并指向字符串常量池中的值,如果字符串常量池中没有相关的字符,会先创建字符串在返回此字符串的引用给堆空间的变量。我们还将了字符串常量池在 JDK 1.7 和 JDK 1.8 的变化以及编译器对确定字符串的优化,希望能帮你正在的理解字符串的比较。


    最后的话 原创不易,本篇近 3000 的文字描述,以及大量精美的图片,耗费了作者大概 5 个多小时的时间,写作是一件很酷,并且能帮助他人的事,作者希望一直能坚持下去。如果觉得有用,请随手点击一个赞吧,谢谢


    作者:码农出击666
    链接:https://juejin.cn/post/7033653572898127879
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

    2020-iOS最新面试题解析(原理篇)

    runtime怎么添加属性、方法等ivar表示成员变量class_addIvarclass_addMethodclass_addPropertyclass_addProtocolclass_replaceProperty是否可以把比较耗时的操作放在NSNoti...
    继续阅读 »

    runtime怎么添加属性、方法等


    • ivar表示成员变量
    • class_addIvar
    • class_addMethod
    • class_addProperty
    • class_addProtocol
    • class_replaceProperty


    是否可以把比较耗时的操作放在NSNotificationCenter中


    • 首先必须明确通知在哪个线程中发出,那么处理接受到通知的方法也在这个线程中调用
    • 如果在异步线程发的通知,那么可以执行比较耗时的操作;
    • 如果在主线程发的通知,那么就不可以执行比较耗时的操作


    runtime 如何实现 weak 属性


    首先要搞清楚weak属性的特点


    weak策略表明该属性定义了一种“非拥有关系” (nonowning relationship)。
    为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。此特质同assign类似;
    然而在属性所指的对象遭到摧毁时,属性值也会清空(nil out)


    那么runtime如何实现weak变量的自动置nil?


    runtime对注册的类,会进行布局,会将 weak 对象放入一个 hash 表中。
    用 weak 指向的对象内存地址作为 key,当此对象的引用计数为0的时候会调用对象的 dealloc 方法,
    假设 weak 指向的对象内存地址是a,那么就会以a为key,在这个 weak hash表中搜索,找到所有以a为key的 weak 对象,从而设置为 nil。


    weak属性需要在dealloc中置nil么


    • 在ARC环境无论是强指针还是弱指针都无需在 dealloc 设置为 nil , ARC 会自动帮我们处理
    • 即便是编译器不帮我们做这些,weak也不需要在dealloc中置nil
    • 在属性所指的对象遭到摧毁时,属性值也会清空


    // 模拟下weak的setter方法,大致如下
    - (void)setObject:(NSObject *)object
    {
    objc_setAssociatedObject(self, "object", object, OBJC_ASSOCIATION_ASSIGN);
    [object cyl_runAtDealloc:^{
    _object = nil;
    }];
    }


    一个Objective-C对象如何进行内存布局?(考虑有父类的情况)


    • 所有父类的成员变量和自己的成员变量都会存放在该对象所对应的存储空间中
    • 父类的方法和自己的方法都会缓存在类对象的方法缓存中,类方法是缓存在元类对象中
    • 每一个对象内部都有一个isa指针,指向他的类对象,类对象中存放着本对象的如下信息
      • 对象方法列表
      • 成员变量的列表
      • 属性列表
    • 每个 Objective-C 对象都有相同的结构,如下图所示

    Objective-C 对象的结构图

    ISA指针

    根类(NSObject)的实例变量

    倒数第二层父类的实例变量

    ...

    父类的实例变量

    类的实例变量


    • 根类对象就是NSObject,它的super class指针指向nil
    • 类对象既然称为对象,那它也是一个实例。类对象中也有一个isa指针指向它的元类(meta class),即类对象是元类的实例。元类内部存放的是类方法列表,根元类的isa指针指向自己,superclass指针指向NSObject类


    一个objc对象的isa的指针指向什么?有什么作用?


    • 每一个对象内部都有一个isa指针,这个指针是指向它的真实类型
    • 根据这个指针就能知道将来调用哪个类的方法


    下面的代码输出什么?


    @implementation Son : Father
    - (id)init
    {
    self = [super init];
    if (self) {
    NSLog(@"%@", NSStringFromClass([self class]));
    NSLog(@"%@", NSStringFromClass([super class]));
    }
    return self;
    }
    @end


    • 答案:都输出 Son
    • 这个题目主要是考察关于objc中对 self 和 super 的理解:
      • self 是类的隐藏参数,指向当前调用方法的这个类的实例。而 super 本质是一个编译器标示符,和 self 是指向的同一个消息接受者
      • 当使用 self 调用方法时,会从当前类的方法列表中开始找,如果没有,就从父类中再找;
      • 而当使用 super时,则从父类的方法列表中开始找。然后调用父类的这个方法
      • 调用[self class] 时,会转化成 objc_msgSend函数
    id objc_msgSend(id self, SEL op, ...)
      • 调用 [super class]时,会转化成 objc_msgSendSuper函数
    id objc_msgSendSuper(struct objc_super *super, SEL op, ...)
      • 第一个参数是 objc_super 这样一个结构体,其定义如下
    struct objc_super {
    __unsafe_unretained id receiver;
    __unsafe_unretained Class super_class;
    };
      • 第一个成员是 receiver, 类似于上面的 objc_msgSend函数第一个参数self
      • 第二个成员是记录当前类的父类是什么,告诉程序从父类中开始找方法,找到方法后,最后内部是使用 objc_msgSend(objc_super->receiver, @selector(class))去调用, 此时已经和[self class]调用相同了,故上述输出结果仍然返回 Son
      • objc Runtime开源代码对- (Class)class方法的实现


        -(Class)class {
    return object_getClass(self);
    }



    收起阅读 »

    iOS 面试策略之算法基础1-3节

    1. 基本数据结构数组数组是最基本的数据结构。在 Swift 中,以前 Objective-C 时代中将 NSMutableArray 和 NSArray 分开的做法,被统一到了唯一的数据结构 —— Array 。虽然看上去就一种数据结构,其实它的实现有三种:...
    继续阅读 »

    1. 基本数据结构


    数组


    数组是最基本的数据结构。在 Swift 中,以前 Objective-C 时代中将 NSMutableArray 和 NSArray 分开的做法,被统一到了唯一的数据结构 —— Array 。虽然看上去就一种数据结构,其实它的实现有三种:


    • ContiguousArray:效率最高,元素分配在连续的内存上。如果数组是值类型(栈上操作),则 Swift 会自动调用 Array 的这种实现;如果注重效率,推荐声明这种类型,尤其是在大量元素是类时,这样做效果会很好。
    • Array:会自动桥接到 Objective-C 中的 NSArray 上,如果是值类型,其性能与 ContiguousArray 无差别。
    • ArraySlice:它不是一个新的数组,只是一个片段,在内存上与原数组享用同一区域。


    下面是数组最基本的一些运用。


    // 声明一个不可修改的数组
    let nums = [1, 2, 3]
    let nums = [Int](repeating: 0, count: 5)

    // 声明一个可以修改的数组
    var nums = [3, 1, 2]
    // 增加一个元素
    nums.append(4)
    // 对原数组进行升序排序
    nums.sort()
    // 对原数组进行降序排序
    nums.sort(by: >)
    // 将原数组除了最后一个以外的所有元素赋值给另一个数组
    // 注意:nums[0..<nums.count - 1] 返回的是 ArraySlice,不是 Array
    let anotherNums = Array(nums[0 ..< nums.count - 1])


    不要小看这些简单的操作:数组可以依靠它们实现更多的数据结构。Swift 虽然不像 Java 中有现成的队列和栈,但我们完全可以用数组配合最简单的操作实现这些数据结构,下面就是用数组实现栈的示例代码。


    // 用数组实现栈
    struct Stack<Element> {
    private var stack: [Element]
    var isEmpty: Bool { return stack.isEmpty }
    var peek: AnyObject? { return stack.last }

    init() {
    stack = [Element]()
    }

    mutating func push(_ element: Element) {
    stack.append(object)
    }

    mutating func pop() -> Element? {
    return stack.popLast()
    }
    }

    // 初始化一个栈
    let stack = Stack<String>()


    最后特别强调一个操作:reserveCapacity()。它用于为原数组预留空间,防止数组在增加和删除元素时反复申请内存空间或是创建新数组,特别适用于创建和 removeAll() 时候进行调用,为整段代码起到提高性能的作用。


    字典和集合


    字典和集合(这里专指HashSet)经常被使用的原因在于,查找数据的时间复杂度为 O(1)。

    一般字典和集合要求它们的 Key 都必须遵守 Hashable 协议,Cocoa 中的基本数据类型都

    满足这一点;自定义的 class 需要实现 Hashable,而又因为 Hashable 是对 Equable 的扩展,

    所以还要重载 == 运算符。


    下面是关于字典和集合的一些实用操作:


    let primeNums: Set = [3, 5, 7, 11, 13]
    let oddNums: Set = [1, 3, 5, 7, 9]

    // 交集、并集、差集
    let primeAndOddNum = primeNums.intersection(oddNums)
    let primeOrOddNum = primeNums.union(oddNums)
    let oddNotPrimNum = oddNums.subtracting(primeNums)

    // 用字典和高阶函数计算字符串中每个字符的出现频率,结果 [“h”:1, “e”:1, “l”:2, “o”:1]
    Dictionary("hello".map { ($0, 1) }, uniquingKeysWith: +)


    集合和字典在实战中经常与数组配合使用,请看下面这道算法题:


    给一个整型数组和一个目标值,判断数组中是否有两个数字之和等于目标值


    这道题是传说中经典的 “2Sum”,我们已经有一个数组记为 nums,也有一个目标值记为 target,最后要返回一个 Bool 值。


    最粗暴的方法就是每次选中一个数,然后遍历整个数组,判断是否有另一个数使两者之和为 target。这种做法时间复杂度为 O(n^2)。


    采用集合可以优化时间复杂度。在遍历数组的过程中,用集合每次保存当前值。假如集合中已经有了目标值减去当前值,则证明在之前的遍历中一定有一个数与当前值之和等于目标值。这种做法时间复杂度为 O(n),代码如下。


    func twoSum(nums: [Int], _ target: Int) -> Bool {
    var set = Set<Int>()

    for num in nums {
    if set.contains(target - num) {
    return true
    }

    set.insert(num)
    }

    return false
    }


    如果把题目稍微修改下,变为


    给定一个整型数组中有且仅有两个数字之和等于目标值,求两个数字在数组中的序号


    思路与上题基本类似,但是为了方便拿到序列号,我们采用字典,时间复杂度依然是 O(n)。代码如下。


    func twoSum(nums: [Int], _ target: Int) -> [Int] {
    var dict = [Int: Int]()

    for (i, num) in nums.enumerated() {
    if let lastIndex = dict[target - num] {
    return [lastIndex, i]
    } else {
    dict[num] = i
    }
    }

    fatalError("No valid output!")
    }


    字符串和字符


    字符串在算法实战中极其常见。在 Swift 中,字符串不同于其他语言(包括 Objective-C),它是值类型而非引用类型,它是多个字符构成的序列(并非数组)。首先还是列举一下字符串的通常用法。


    // 字符串和数字之间的转换
    let str = "3"
    let num = Int(str)
    let number = 3
    let string = String(num)

    // 字符串长度
    let len = str.count

    // 访问字符串中的单个字符,时间复杂度为O(1)
    let char = str[str.index(str.startIndex, offsetBy: n)]

    // 修改字符串
    str.remove(at: n)
    str.append("c")
    str += "hello world"

    // 检测字符串是否是由数字构成
    func isStrNum(str: String) -> Bool {
    return Int(str) != nil
    }

    // 将字符串按字母排序(不考虑大小写)
    func sortStr(str: String) -> String {
    return String(str.sorted())
    }

    // 判断字符是否为字母
    char.isLetter

    // 判断字符是否为数字
    char.isNumber

    // 得到字符的 ASCII 数值
    char.asciiValue


    关于字符串,我们来一起看一道以前的 Google 面试题。


    给一个字符串,将其按照单词顺序进行反转。比如说 s 是 "the sky is blue",

    那么反转就是 "blue is sky the"。


    这道题目一看好简单,不就是反转字符串的翻版吗?这种方法有以下两个问题


    • 每个单词长度不一样
    • 空格需要特殊处理
      这样一来代码写起来会很繁琐而且容易出错。不如我们先实现一个字符串翻转的方法。


    fileprivate func reverse<T>(_ chars: inout [T], _ start: Int, _ end: Int) {
    var start = start, end = end

    while start < end {
    swap(&chars, start, end)
    start += 1
    end -= 1
    }
    }

    fileprivate func swap<T>(_ chars: inout [T], _ p: Int, _ q: Int) {
    (chars[p], chars[q]) = (chars[q], chars[p])
    }


    有了这个方法,我们就可以实行下面两种字符串翻转:


    • 整个字符串翻转,"the sky is blue" -> "eulb si yks eht"
    • 每个单词作为一个字符串单独翻转,"eulb si yks eht" -> "blue is sky the"
      整体思路有了,我们就可以解决这道问题了


    func reverseWords(s: String?) -> String? {
    guard let s = s else {
    return nil
    }

    var chars = Array(s), start = 0
    reverse(&chars, 0, chars.count - 1)

    for i in 0 ..< chars.count {
    if i == chars.count - 1 || chars[i + 1] == " " {
    reverse(&chars, start, i)
    start = i + 2
    }
    }

    return String(chars)
    }


    时间复杂度还是 O(n),整体思路和代码简单很多。


    总结


    在 Swift 中,数组、字符串、集合以及字典是最基本的数据结构,但是围绕这些数据结构的问题层出不穷。而在日常开发中,它们使用起来也非常高效(栈上运行)和安全(无需顾虑线程问题),因为他们都是值类型。


    2. 链表


    本节我们一起来探讨用 Swift 如何实现链表以及链表相关的技巧。


    基本概念


    对于链表的概念,实在是基本概念太多,这里不做赘述。我们直接来实现链表节点。


    class ListNode { 
    var val: Int
    var next: ListNode?

    init(_ val: Int) {
    self.val = val
    }
    }


    有了节点,就可以实现链表了。


    class LinkedList {
    var head: ListNode?
    var tail: ListNode?

    // 头插法
    func appendToHead(_ val: Int) {
    let node = ListNode(val)

    if let _ = head {
    node.next = head
    } else {
    tail = node
    }

    head = node
    }

    // 头插法
    func appendToTail(_ val: Int) {
    let node = ListNode(val)

    if let _ = tail {
    tail!.next = node
    } else {
    head = node
    }

    tail = node
    }
    }


    有了上面的基本操作,我们来看如何解决复杂的问题。


    Dummy 节点和尾插法


    话不多说,我们直接先来看下面一道题目。


    给一个链表和一个值 x,要求将链表中所有小于 x 的值放到左边,所有大于等于 x 的值放到右边。原链表的节点顺序不能变。

    例:1->5->3->2->4->2,给定x = 3。则我们要返回1->2->2->5->3->4


    直觉告诉我们,这题要先处理左边(比 x 小的节点),然后再处理右边(比 x 大的节点),最后再把左右两边拼起来。


    思路有了,再把题目抽象一下,就是要实现这样一个函数:


    func partition(_ head: ListNode?, _ x: Int) -> ListNode? {}


    即我们有给定链表的头节点,有给定的x值,要求返回新链表的头结点。接下来我们要想:怎么处理左边?怎么处理右边?处理完后怎么拼接?


    先来看怎么处理左边。我们不妨把这个题目先变简单一点:


    给一个链表和一个值 x,要求只保留链表中所有小于 x 的值,原链表的节点顺序不能变。


    例:1->5->3->2->4->2,给定x = 3。则我们要返回 1->2->2


    我们只要采用尾插法,遍历链表,将小于 x 值的节点接入新的链表即可。代码如下:


    func getLeftList(_ head: ListNode?, _ x: Int) -> ListNode? { 
    let dummy = ListNode(0)
    var pre = dummy, node = head

    while node != nil {
    if node!.val < x {
    pre.next = node
    pre = node!
    }
    node = node!.next
    }

    // 防止构成环
    pre.next = nil
    return dummy.next
    }


    注意,上面的代码我们引入了 Dummy 节点,它的作用就是作为一个虚拟的头前结点。我们引入它的原因是我们不知道要返回的新链表的头结点是哪一个,它有可能是原链表的第一个节点,可能在原链表的中间,也可能在最后,甚至可能不存在(nil)。而 Dummy 节点的引入可以巧妙的涵盖所有以上情况,我们可以用 dummy.next 方便得返回最终需要的头结点。


    现在我们解决了左边,右边也是同样处理。接着只要让左边的尾节点指向右边的头结点即可。全部代码如下:


    func partition(_ head: ListNode?, _ x: Int) -> ListNode? {
    // 引入Dummy节点
    let prevDummy = ListNode(0), postDummy = ListNode(0)
    var prev = prevDummy, post = postDummy

    var node = head

    // 用尾插法处理左边和右边
    while node != nil {
    if node!.val < x {
    prev.next = node
    prev = node!
    } else {
    post.next = node
    post = node!
    }
    node = node!.next
    }

    // 防止构成环
    post.next = nil
    // 左右拼接
    prev.next = postDummy.next

    return prevDummy.next
    }


    注意这句 post.next = nil,这是为了防止链表循环指向构成环,是必须的但是很容易忽略的一步。

    刚才我们提到了环,那么怎么检测链表中是否有环存在呢?



    快行指针


    笔者理解快行指针,就是两个指针访问链表,一个在前一个在后,或者一个移动快另一个移动慢,这就是快行指针。来看一道简单的面试题:


    如何检测一个链表中是否有环?


    答案是用两个指针同时访问链表,其中一个的速度是另一个的 2 倍,如果他们相等了,那么这个链表就有环了,这就是快行指针的实际使用。代码如下:


    func hasCycle(_ head: ListNode?) -> Bool { 
    var slow = head
    var fast = head

    while fast != nil && fast!.next != nil {
    slow = slow!.next
    fast = fast!.next!.next

    if slow === fast {
    return true
    }
    }

    return false
    }


    再举一个快行指针一前一后的例子,看下面这道题。


    删除链表中倒数第 n 个节点。例:1->2->3->4->5,n = 2。返回1->2->3->5。

    注意:给定 n 的长度小于等于链表的长度。


    解题思路依然是快行指针,这次两个指针移动速度相同。但是一开始,第一个指针(指向头结点之前)就落后第二个指针 n 个节点。接着两者同时移动,当第二个移动到尾节点时,第一个节点的下一个节点就是我们要删除的节点。代码如下:


    func removeNthFromEnd(head: ListNode?, _ n: Int) -> ListNode? {
    guard let head = head else {
    return nil
    }

    let dummy = ListNode(0)
    dummy.next = head
    var prev: ListNode? = dummy
    var post: ListNode? = dummy

    // 设置后一个节点初始位置
    for _ in 0 ..< n {
    if post == nil {
    break
    }
    post = post!.next
    }

    // 同时移动前后节点
    while post != nil && post!.next != nil {
    prev = prev!.next
    post = post!.next
    }

    // 删除节点
    prev!.next = prev!.next!.next

    return dummy.next
    }


    这里还用到了 Dummy 节点,因为有可能我们要删除的是头结点。


    总结


    这次我们用 Swift 实现了链表的基本结构,并且实战了链表的几个技巧。在结尾处,我还想强调一下 Swift 处理链表问题的两个细节问题:


    • 一定要注意头结点可能就是 nil。所以给定链表,我们要看清楚 head 是不是 optional,在判断是不是要处理这种边界条件。
    • 注意每个节点的 next 可能是 nil。如果不为 nil,请用"!"修饰变量。在赋值的时候,也请注意"!"将 optional 节点传给非 optional 节点的情况。


    3. 栈和队列


    这期我们来讨论一下栈和队列。在 Swift 中,没有内设的栈和队列,很多扩展库中使用 Generic Type 来实现栈或是队列。正规的做法使用链表来实现,这样可以保证加入和删除的时间复杂度是 O(1)。然而笔者觉得最实用的实现方法是使用数组,因为 Swift 没有现成的链表,而数组又有很多的 API 可以直接使用,非常方便。


    基本概念


    对于栈来说,我们需要了解以下几点:


    • 栈是后进先出的结构。你可以理解成有好几个盘子要垒成一叠,哪个盘子最后叠上去,下次使用的时候它就最先被抽出去。
    • 在 iOS 开发中,如果你要在你的 App 中添加撤销操作(比如删除图片,恢复删除图片),那么栈是首选数据结构
    • 无论在面试还是写 App 中,只关注栈的这几个基本操作:push, pop, isEmpty, peek, size。


    protocol Stack {
    /// 持有的元素类型
    associatedtype Element

    /// 是否为空
    var isEmpty: Bool { get }
    /// 栈的大小
    var size: Int { get }
    /// 栈顶元素
    var peek: Element? { get }

    /// 进栈
    mutating func push(_ newElement: Element)
    /// 出栈
    mutating func pop() -> Element?
    }

    struct IntegerStack: Stack {
    typealias Element = Int

    var isEmpty: Bool { return stack.isEmpty }
    var size: Int { return stack.count }
    var peek: Element? { return stack.last }

    private var stack = [Element]()

    mutating func push(_ newElement: Element) {
    stack.append(newElement)
    }

    mutating func pop() -> Element? {
    return stack.popLast()
    }
    }


    对于队列来说,我们需要了解以下几点:


    • 队列是先进先出的结构。这个正好就像现实生活中排队买票,谁先来排队,谁先买到票。
    • iOS 开发中多线程的 GCD 和 NSOperationQueue 就是基于队列实现的。
    • 关于队列我们只关注这几个操作:enqueue, dequeue, isEmpty, peek, size。


    protocol Queue {
    /// 持有的元素类型
    associatedtype Element

    /// 是否为空
    var isEmpty: Bool { get }
    /// 队列的大小
    var size: Int { get }
    /// 队首元素
    var peek: Element? { get }

    /// 入队
    mutating func enqueue(_ newElement: Element)
    /// 出队
    mutating func dequeue() -> Element?
    }

    struct IntegerQueue: Queue {
    typealias Element = Int

    var isEmpty: Bool { return left.isEmpty && right.isEmpty }
    var size: Int { return left.count + right.count }
    var peek: Element? { return left.isEmpty ? right.first : left.last }

    private var left = [Element]()
    private var right = [Element]()

    mutating func enqueue(_ newElement: Element) {
    right.append(newElement)
    }

    mutating func dequeue() -> Element? {
    if left.isEmpty {
    left = right.reversed()
    right.removeAll()
    }
    return left.popLast()
    }
    }


    栈和队列互相转化


    处理栈和队列问题,最经典的一个思路就是使用两个栈/队列来解决问题。也就是说在原栈/队列的基础上,我们用一个协助栈/队列来帮助我们简化算法,这是一种空间换时间的思路。下面是示例代码:


    // 用栈实现队列
    struct MyQueue {
    var stackA: Stack
    var stackB: Stack

    var isEmpty: Bool {
    return stackA.isEmpty
    }

    var peek: Any? {
    get {
    shift()
    return stackB.peek
    }
    }

    var size: Int {
    get {
    return stackA.size + stackB.size
    }
    }

    init() {
    stackA = Stack()
    stackB = Stack()
    }

    func enqueue(object: Any) {
    stackA.push(object);
    }

    func dequeue() -> Any? {
    shift()
    return stackB.pop();
    }

    fileprivate func shift() {
    if stackB.isEmpty {
    while !stackA.isEmpty {
    stackB.push(stackA.pop()!);
    }
    }
    }
    }

    // 用队列实现栈
    struct MyStack {
    var queueA: Queue
    var queueB: Queue

    init() {
    queueA = Queue()
    queueB = Queue()
    }

    var isEmpty: Bool {
    return queueA.isEmpty
    }

    var peek: Any? {
    get {
    if isEmpty {
    return nil
    }

    shift()
    let peekObj = queueA.peek
    queueB.enqueue(queueA.dequeue()!)
    swap()
    return peekObj
    }
    }

    var size: Int {
    return queueA.size
    }

    func push(object: Any) {
    queueA.enqueue(object)
    }

    func pop() -> Any? {
    if isEmpty {
    return nil
    }

    shift()
    let popObject = queueA.dequeue()
    swap()
    return popObject
    }

    private func shift() {
    while queueA.size > 1 {
    queueB.enqueue(queueA.dequeue()!)
    }
    }

    private func swap() {
    (queueA, queueB) = (queueB, queueA)
    }
    }


    上面两种实现方法都是使用两个相同的数据结构,然后将元素由其中一个转向另一个,从而形成一种完全不同的数据。


    面试题实战


    给一个文件的绝对路径,将其简化。举个例子,路径是 "/home/",简化后为 "/home";路径是"/a/./b/../../c/",简化后为 "/c"。


    这是一道 Facebook 的面试题。这道题目其实就是平常在终端里面敲的 cd、pwd 等基本命令所得到的路径。


    根据常识,我们知道以下规则:


    • “. ” 代表当前路径。比如 “ /a/. ” 实际上就是 “/a”,无论输入多少个 “ . ” 都返回当前目录
    • “..”代表上一级目录。比如 “/a/b/.. ” 实际上就是 “ /a”,也就是说先进入 “a” 目录,再进入其下的 “b” 目录,再返回 “b” 目录的上一层,也就是 “a” 目录。


    然后针对以上信息,我们可以得出以下思路:


    1. 首先输入是个 String,代表路径。输出要求也是 String, 同样代表路径;
    2. 我们可以把 input 根据 “/” 符号去拆分,比如 "/a/b/./../d/" 就拆成了一个String数组["a", "b", ".", "..", "d"];
    1. 创立一个栈然后遍历拆分后的 String 数组,对于一般 String ,直接加入到栈中,对于 ".." 那我们就对栈做 pop 操作,其他情况不错处理。


    思路有了,代码也就有了


    func simplifyPath(path: String) -> String {
    // 用数组来实现栈的功能
    var pathStack = [String]()
    // 拆分原路径
    let paths = path.split(separatedBy: "/")

    for path in paths {
    // 对于 "." 我们直接跳过
    guard path != "." else {
    continue
    }
    // 对于 ".." 我们使用pop操作
    if path == ".." {
    if (!pathStack.isEmpty) {
    pathStack.removeLast()
    }
    // 对于太注意空数组的特殊情况
    } else if path != "" {
    pathStack.append(path)
    }
    }
    // 将栈中的内容转化为优化后的新路径
    return "/" + pathStack.joined(separator: "/")
    }


    上面代码除了完成了基本思路,还考虑了大量的特殊情况、异常情况。这也是硅谷面试考察的一个方面:面试者思路的严谨,对边界条件的充分考虑,以及代码的风格规范。


    总结


    在 Swift 中,栈和队列是比较特殊的数据结构,笔者认为最实用的实现和运用方法是利用数组。虽然它们本身比较抽象,却是很多复杂数据结构和 iOS 开发中的功能模块的基础。这也是一个工程师进阶之路理应熟练掌握的两种数据结构。

    收起阅读 »

    iOS 面试简单准备

    1.简历的准备在面试中,我发现很多人都不能写好一份求职简历,所以我们首先谈谈如何写一份针对互联网公司的求职简历。1.简洁的艺术互联网公司和传统企业有着很大的区别,通常情况下,创新和效率是互联网公司比较追求的公司文化,所以体现在简历上,就是超过一页的简历通常会被...
    继续阅读 »

    1.简历的准备


    在面试中,我发现很多人都不能写好一份求职简历,所以我们首先谈谈如何写一份针对互联网公司的求职简历。


    1.简洁的艺术


    互联网公司和传统企业有着很大的区别,通常情况下,创新和效率是互联网公司比较追求的公司文化,所以体现在简历上,就是超过一页的简历通常会被认为不够专业。


    更麻烦的是,多数超过一页的简历很可能在 HR 手中就被过滤掉了。因为 HR 每天会收到大量的简历,一般情况下每份简历在手中的停留时间也就 10 秒钟左右。而超过一页的简历会需要更多的时间去寻找简历中的有价值部分,对于 HR 来说,她更倾向于认为这种人通常是不靠谱的,因为写个简历都不懂行规,为什么还要给他面试机会呢?


    那么我们应该如何精简简历呢? 简单说来就是一个字:删!


    删掉不必要的自我介绍信息。很多求职者会将自己在学校所学的课程罗列上去,例如:C 语言,数据结构,数学分析⋯⋯好家伙,一写就是几十门,还放在简历的最上面,就怕面试官看不见。对于这类信息,一个字:删!面试官不关心你上了哪些课程,而且在全中国,大家上的课程也都大同小异,所以没必要写出来。


    删除不必要的工作或实习、实践经历。如果你找一份程序员的工作,那么你参加了奥运会的志愿者活动,并且拿到了奖励或者你参加学校的辩论队,获得了最佳辩手这些经历通常是不相关的。诸如此类的还有你帮导师代课,讲了和工作不相关的某某专业课,或者你在学生会工作等等。删除不相关的工作、实习或实践内容可以保证你的简历干净。当然,如果你实在没得可写,比如你是应届生,一点实习经历都没有,那可以适当写一两条,保证你能写够一页的简历,但是那两条也要注意是强调你的团队合作能力或者执行力之类的技能,因为这些才是面试官感兴趣的。


    删除不必要的证书。最多写个 4、6 级的证书,什么教师资格证,中高级程序员证,还有国内的各种什么认证,都是没有人关心的。


    删除不必要的细节。作为 iOS 开发的面试官,很多求职者在介绍自己的 iOS 项目经历的时候,介绍了这个工程用的工作环境是 Mac OS,使用的机器是 Mac Mini,编译器是 Xcode,能够运行在 iOS 什么版本的环境。还有一些人,把这个项目用到的开源库都写上啦,什么 AFNetworking, CocoaPods 啥的。这些其实都不是重点,请删掉。后面我会讲,你应该如何介绍你的 iOS 项目经历。


    自我评价,这个部分是应届生最喜欢写的,各种有没有的优点都写上,例如:


    性格开朗、稳重、有活力,待人热情、真诚;工作认真负责,积极主动,能吃苦耐劳,勇于承受压力,勇于创新;有很强的组织能力和团队协作精神,具有较强的适应能力;纪律性强,工作积极配合;意志坚强,具有较强的无私奉献精神。对待工作认真负责,善于沟通、协调有较强的组织能力与团队精神;活泼开朗、乐观上进、有爱心并善于施教并行;上进心强、勤于学习能不断提高自身的能力与综合素质。


    这些内容在面试的时候不太好考查,都可以删掉。通常如果有 HR 面的话,HR 自然会考查一些你的沟通,抗压,性格等软实力。


    我相信,不管你是刚毕业的学生,还是工作十年的老手,你都可以把你的简历精简到一页 A4 纸上。记住,简洁是一种美,一种效率,也是一种艺术。


    2.重要的信息写在最前面


    将你觉得最吸引人的地方写在最前面。如果你有牛逼公司的实习,那就把实习经历写在最前面,如果你在一个牛逼的实验室里面做科研,就把研究成果和论文写出来,如果你有获得过比较牛逼的比赛名次(例如 Google code jam, ACM 比赛之类),写上绝对吸引眼球。


    所以,每个人的简历的介绍顺序应该都是不一样的,不要在网上下载一个模板,然后就一项一项地填:教育经历,实习经历,得奖经历,个人爱好,这样的简历毫无吸引力,也无法突出你的特点。


    除了你的个人特点是重要信息外,你的手机号、邮箱、毕业院校、专业以及毕业时间这些也都是非常重要的,一定要写在简历最上面。


    3.不要简单地罗列工作经历


    不要简单地说你开发了某某 iOS 客户端。这样简单的罗列你的作品集并不能让面试官很好地了解你的能力,当然,真正在面试时面试官可能会仔细询问,但是一份好的简历,应该省去一些面试官额外询问你的工作细节的时间。


    具体的做法是:详细的描述你对于某某 iOS 客户端的贡献。主要包括:你参与了多少比例功能的开发? 你解决了哪些开发中的有挑战的问题? 你是不是技术负责人?


    而且,通过你反思这些贡献,你也可以达到自我审视,如果你发现这个项目你根本什么有价值的贡献都没做,就打了打酱油,那你最好不要写在简历上,否则当面试官在面试时问起时,你会很难回答,最终让他发现你的这个项目经历根本一文不值时,肯定会给一个负面的印象。


    4.不要写任何虚假或夸大的信息


    刚刚毕业的学生都喜欢写精通 Java,精通 C/C++,其实代码可能写了不到 1 万行。我觉得你要精通某个语言,至少得写 50 万行这个语言的代码才行,而且要对语言的各种内部机制和原理有了解。那些宣称精通 Java 的同学,连 Java 如何做内存回收,如何做泛型支持,如何做自动 boxing 和 unboxing 的都不知道,真不知道为什么要写精通二字。


    任何夸大或虚假的信息,在面试时被发现,会造成极差的面试印象。所以你如果对某个知识一知半解,要么就写 “使用过” 某某,要么就干脆不写。如果你简历实在太单薄,没办法写上了一些自己打酱油的项目,被问起来怎么办? 请看看下面的故事:


    我面试过一个同学,他在面试时非常诚实。我问他一些简历上的东西,他如果不会,就会老实说,这个我只是使用了一下,确实不清楚细节。对于一些没有技术含量的项目,他也会老实说,这个项目他做的工作比较少,主要是别人在做。最后他还会补充说,“我自认为自己数据结构和算法还不错,要不你问我这方面的知识吧。”


    这倒是一个不错的办法,对于一个没有项目经验,但是聪明并且数据结构和算法基础知识扎实的应届生,其实我们是非常愿意培养的。很多人以为公司面试是看经验,希望招进来就能干活,其实不是的,至少我们现在以及我以前在网易招人,面试的是对方的潜力,潜力越大,可塑性好,未来进步得也更快;一些资质平庸,却经验稍微丰富一点的开发者,相比聪明好学的面试者,后劲是不足的。


    总之,不要写任何虚假或夸大的信息,即使你最终骗得过面试官,进了某公司,如果能力不够,在最初的试用期内,也很可能因为能力不足而被开掉。


    5.留下更多信息


    刚刚说到,简历最好写够一张 A4 纸即可,那么你如果想留下更多可供面试官参考的信息怎么办呢?其实你可以附上更多的参考链接,这样如果面试官对你感兴趣,自然会仔细去查阅这些链接。对于 iOS 面试来说,GitHub 上面的开源项目地址、博客地址都是不错的参考信息。如果你在微博上也频繁讨论技术,也可以附上微博地址。


    我特别建议大家如果有精力,可以好好维护一下自己的博客或者 GitHub 上的开源代码。因为如果你打算把这些写到简历上,让面试官去上面仔细评价你的水平,你就应该对上面的内容做到足够认真的准备。否则,本来面试完面试官还挺感兴趣的,结果一看你的博客和开源代码,评价立刻降低,就得不偿失了。


    6.不要附加任何可能带来负面印象的信息


    任何与招聘工作无关的东西,尽量不要提。有些信息提了可能有加分,也可能有减分,取决于具体的面试官。下面我罗列一下我认为是减分的信息。


    1)个人照片


    不要在简历中附加个人照片。个人长相属于与工作能力不相关的信息,也许你觉得你长得很帅,那你怎么知道你的样子不和面试官的情敌长得一样? 也许你长得很漂亮,那么你怎么知道 HR 是否被你长得一样的小三把男朋友抢了? 我说得有点极端,那人们对于长相的评价标准确实千差万别,萝卜青菜各有所爱,加上可能有一些潜在的极端情况,所以没必要附加这部分信息。这属于加了可能有加分,也可能有减分的情况。


    2)有风险的爱好


    不要写各种奇怪的爱好。喜欢打游戏、抽烟、喝酒,这类可能带来负面印象的爱好最好不要写。的确有些公司会有这种一起联机玩游戏或者喝酒的文化,不过除非你明确清楚对于目标公司,写上会是加分项,否则还是不写为妙。


    3)使用 PDF 格式


    不要使用 Word 格式的简历,要使用 PDF 的格式。我在招 iOS 程序员时,好多人的简历都是 Word 格式的,我都怀疑这些人是否有 Mac 电脑。因为 Mac 下的 office 那么难用,公司好多人机器上都没有 Mac 版 office。我真怀疑这些人真是的想投简历么? PDF 格式的简历通常能展现出简历的专业性。


    4)QQ号码邮箱


    不要使用 QQ 号开头的 QQ 邮箱,例如 12345@qq.com ,邮箱的事情我之前简单说过,有些人很在乎这个,有些人觉得无所谓,我个人对用数字开头的 QQ 邮箱的求职者不会有加分,但是对使用 Gmail 邮箱的求职者有加分。因为这涉及到个人的工作效率,使用 Gmail 的人通常会使用邮件组,过滤器,IMAP 协议,标签,这些都有助于提高工作效率。如果你非要使用 QQ 邮箱,也应该申请一个有意义的邮箱名,例如 tangqiaoboy@qq.com 。


    7.职业培训信息


    不要写参加过某某培训公司的 iOS 培训,特别是那种一、两个月的速成培训。这对于我和身边很多面试官来说,绝对是负分。


    这个道理似乎有点奇怪,因为我们从小都是由老师教授新知识的。我自己也实验过,掌握同样的高中课本上的知识,自己自学的速度通常比老师讲授的速度要慢一倍的时间。即一个知识点,如果你自己要看 2 小时的书才能理解的话,有好的老师给你讲解的话,只需要一个小时就够了。所以,我一直希望在学习各种东西的时候都能去听一些课程,因为我认为这样节省了我学习的时间。


    但是这个道理在程序员这个领域行不通,为什么这么说呢?原因有两点:


    1. 计算机编程相关的知识更新速度很快。同时,国内的 IT 类资料的翻译质量相当差,原创的优秀书籍也很少。所以,我们通常需要靠阅读英文才能掌握最新的资料。拿 iOS 来说,每年 WWDC 的资料都非常重要,而这些内容涉及版权,国内培训机构很难快速整理成教材。
    2. 计算机编程知识需要较多的专业知识积累和实践。学校的老师更多只能做入门性的教学工作。
      如果一个培训机构有一个老师,他强到能够通过自己做一些项目来积累很多专业知识和实践,并且不断地从国外资料上学习最新的技术。那么这个人在企业里面会比在国内的培训机构更有施展自己能力的空间。国内的培训机构因为受众面的原因,基本上还是培养那种初级的程序员新手,所以对老师的新技术学习速度要求不会那么高,自然老师也不会花那么时间在新技术研究上。但是企业就不一样了,企业需要不停地利用新技术来增强自己的产品竞争力,所以对于 IT 企业来说,产品的竞争就是人才的竞争,所以给优秀的人能够开出很高的薪水。
      所以,我们不能期望从 IT 类培训机构中学习到最新的技术,一切只能通过我们自学。当然,自学之后在同行之间相互交流,对于我们的技术成长也是很有用的。小结



    上图是本节讨论的总结,在简历准备上,我们需要考虑简历的简洁性等各种注意事项。


    2.寻找机会


    1.寻找内推机会

    其实,最好的面试机会都不是公开渠道的。好的机会都隐藏于各种内部推荐之中。通过内部推荐,你可以更加了解目标工作的团队和内容,另外内部推荐通常也可以跳过简历筛选环节,直接参加笔试或面试。我所在的猿辅导公司为内推设立了非常高的奖金激励,因为我们发现,综合各种渠道来看,内推的渠道的人才的简历质量最高,面试通过率最高的。


    所以,如果你是学生,找找你在各大公司的师兄师姐内推;如果你已经工作了,你可以找前同事或者通过一些社交活动认识的技术同行内推。


    大部分情况下,如果在目标公司你完全没有认识的人,你也可以找机会来认识几个。比如你可以通过微博、知乎、Twitter、GitHub 来结交新的朋友。然后双方聊天如果愉快的话,我相信内推这种举手之劳的事情对方不会拒绝的。


    如果你都工作 5 年以上,还是没有建立足够好的社交圈子帮助你内推,那可能你需要做很多的社交活动交一些朋友。


    2.其它常见的渠道

    内推之外,其它的公开招聘渠道通常都要差一些。现在也有一些专门针对互联网行业的招聘网站,例如拉勾、100offer 这类,它们也是不错的渠道,可以找到相关的招聘信息。


    但因为这类公开渠道简历投放数量巨大,通常 HR 那边就会比较严格地筛选简历,拿我们公司来说,通常在这些渠道看 20 份简历,才会有 1 份愿意约面的简历。而且 HR 会只挑比较好的学校或者公司的候选人,也不排除还有例如笔试这种更多的面试流程。但是面试经验都是慢慢积累的,建议你也可以尝试这些渠道。


    3.面试流程


    1.流程简述


    就我所知,大部分的 iOS 公司的面试流程都大同小异。我们先简述一下大体的流程,然后再详细讨论。


    在面试的刚开始,面试官通常会要求你做一个简短的自我介绍。然后面试官可能会和你聊聊你过去的实习项目或者工作内容。接着面试官可能会问你一些具体的技术问题,有经验的面试官会尽量找一些和你过去工作相关的技术问题。最后,有些公司会选择让你当场写写代码,Facebook 会让你在白板上写代码,国内的更多是让你在 A4 纸上写。有一些公司也会问一些系统设计方面的问题,考查你的整体架构能力。在面试快要结束时,通常面试官还会问你有没有别的什么问题。


    以上这些流程,不同公司可能会跳过某些环节。比如有一些公司就不会考察当场在白板或 A4 纸上写代码。有些公司可能跳过问简历的环节直接写代码,特别是校园招聘的时候,因为应届生通常项目经验较少。面试流程图如下所示:



    2.自我介绍


    自我介绍通常是面试中最简单、最好准备的环节。


    一个好的自我介绍应该结合公司的招聘职位来做定制。比如公司有硬件的背景,就应该介绍一下在硬件上的经验或者兴趣;公司如果注重算法能力,则介绍自己的算法练习;公司如果注重团队合作,那么你介绍一下自己的社会活动都是可以的。


    一个好的自我介绍应该提前写下来,并且背熟。因为候选人通常的紧张感都是来自于面试刚开始的几分钟,如果你刚开始的几分钟讲的结结巴巴,那么这种负面情绪会加剧你面试时的紧张感,从而影响你正常发挥。如果你提前把自我介绍准备得特别流利,那么开始几分钟的紧张感过去之后,你很可能就会很快进入状态,而忘记紧张这个事情了。


    即使做到了以上这些仍然是不够的,候选者常见的问题还包括:


    • 太简短
    • 没有重点
    • 太拖沓
    • 不熟练


    我们在展开讨论上面这些问题之前,我们可以站在面试官的立场考虑一下:如果你是面试官,你为什么要让候选人做自我介绍?你希望在自我介绍环节考察哪些信息?


    在我看来,自我介绍环节相当重要,因为:


    • 首先它考察了候选人的表达能力。大部分的程序员表达能力可能都一般,但是如果连自我介绍都说不清楚,通常就说明表达沟通能力稍微有点问题了。面试官通过这个环节可以初步考察到候选人的表达能力。
    • 它同样也考察了候选人对于面试的重视程度。一般情况下,表达再差的程序员,也可以通过事先拟稿的方式,把自我介绍内容背下来。如果一个人自我介绍太差,说明他不但表达差,而且不重视这次面试。
    • 最后,自我介绍对之后的面试环节起到了支撑作用。因为自我介绍中通常会涉及自己的项目经历,自己擅长的技术等。这些都很可能吸引面试官追问下去。好的候选人会通过自我介绍 “引导” 面试官问到自己擅长的领域知识。如果面试官最后对问题答案很满意,通过面试的几率就会大大增加。


    所以我如果是面试官,我希望能得到一个清晰流畅的自我介绍。下面我们来看看候选人在面试中的常见问题。


    1)太简短


    一个好的自我介绍大概是 3~5 分钟。过短的自我介绍没法让面试官了解你的大致情况,也不足以看出来你的基本表达能力。


    如果你发现自己没法说出足够时间的自我介绍。可以考虑在介绍中加入:自己的简单的求学经历,项目经历,项目中有亮点的地方,参与或研究过的一些开源项目,写过的博客,其它兴趣爱好,自己对新工作的期望和目标公司的理解。


    我相信每个人经过准备,都可以做到一个 5 分钟长度的自我介绍。


    2)没有重点


    突破了时间的问题,接下来就需要掌握介绍的重点。通常一个技术面试,技术相关的介绍才是重点。所以求学经历,兴趣爱好之类的内容可以简单提到即可。


    对于一个工作过的开发者,你过去做的项目哪个最有挑战,最能展示出你的水平其实自己应该是最清楚的。所以大家可以花时间把这个内容稍微强调一下。


    当然你也没必要介绍得太细致,面试官如果感兴趣,自然会在之后的面试过程中和你讨论。


    3)太拖沓


    有些工作了几年的人,做过的项目差不多有个 3~5 个,面试的时候就忍不住一个一个说。单不说这么多项目在自我介绍环节不够介绍。就是之后的详细讨论环节,面试官也不可能讨论完你的所有项目经历。


    所以千万不要做这种 “罗列经历” 的事情,你要做的就是挑一个或者最多两个项目,介绍一下项目大致的背景和你解决的主要问题即可。至于具体的解决过程,可以不必介绍。


    4)不熟练


    即便你的内容完全合适,时间长度完全合理,你也需要保证一个流利的陈述过程。适当在面试前多排练几次,所有人都可以做到一个流利的自我介绍。


    还有一点非常神奇,就是一个人在做一件事情的时候,通常都是开始的前以及刚开始几分钟特别紧张。比如考试,演讲或者面试,通常这几分钟之后,人们进入 “状态” 了,就会忘记紧张了。


    将自己的自我介绍背下来,可以保证一个流利顺畅的面试开局,这可以极大舒缓候选人的紧张情绪。相反,一开始自我介绍就结结巴巴,会加剧候选人的紧张情绪,而技术面试如果不能冷静的话,是很难在写代码环节保证逻辑清晰正确的。


    所以,请大家务必把这个小环节重视起来,做出一个完美的开局。


    3.项目介绍


    自我介绍之后,就轮到讨论更加具体的内容环节了,通常面试官都会根据自我介绍或者你的简历,选一个他感兴趣的项目来详细讨论。


    这个时候,大家务必需要提前整理出自己参与的项目的具体挑战,以及自己做得比较好的地方。切忌不要说:“这个项目很简单,没什么挑战,那个项目也很简单,没什么好说的”。再简单的事情,都可以做到极致的,就看你有没有一个追求完美的心。


    比如你的项目可能在你看来就是摆放几个 iOS 控件。但是,这些控件各自有什么使用上的技巧,有什么优化技巧?其实如果专心研究,还是有很多可以学习的。拿 UITableView 来说,一个人如果能够做到把它的滑动流程度优化好,是非常不容易的。这里面涉及网络资源的异步加载、图片资源的缓存、后台线程的渲染、CALayer 层的优化等等。


    这其实也要求我们做工作要精益求精,不求甚解。所以一场成功的面试最最本质上,看得还是自己平时的积累。如果自己平时只是糊弄工作,那么面试时就很容易被看穿。


    在这一点上,我奉劝大家在自己的简历上一定要老实。不要在建简历上弄虚作假,把自己没有做过的项目写在里面。


    顺便我在这里也教一下大家如何面试别人。如果你是面试官,考察简历的真假最简单的方法就是问细节。一个项目的细节如果问得很深入,候选人有没有做过很容易可以看出来。


    举个例子,如果候选人说他在某公司就职期间做了某某项目。你就可以问他:


    • 这个工作具体的产品需求是什么样的?
    • 大概做了多长时间?
    • 整体的软件架构是什么样的?
    • 涉及哪些人合作?几个开发和测试?
    • 项目的时间排期是怎么定的?
    • 你主要负责的部分和合作的人?
    • 项目进行中有没有遇到什么问题?
    • 这个项目最后最大的收获是什么?遗憾是什么?
    • 项目最困难的一个需求是什么?具体什么实现的?


    面试官如果做过类似项目,还可以问问通常这个项目常见的坑,看看候选人是什么解决的。


    4.写代码


    编程能力,说到底还是一个实践的能力,所以说大部分公司都会考察当场写代码。我面试过上百人,见到过很多候选人在自我介绍和项目讨论时都滔滔不绝,侃侃而谈,感觉非常好。但是一到写代码环节就怂了,要么写出来各种逻辑问题和细节问题没处理好,要么就是根本写不出来。


    由于人最终招进来就是干活写代码的,所以如果候选人当场写代码表现很差的话,基本上面试就挂掉了。


    程序员这个行业,说到底就是一个翻译的工作,把产品经理的产品文档和设计师的 UI 设计,翻译成计算机能够理解的形式,这个形式通常就是一行一行的源码。


    当面试官考察你写代码的时候,他其实在考察:


    • 你对语言的熟悉程度。如果候选人连常见的变量定义和系统函数都不熟悉,说明他肯定经验还是非常欠缺。
    • 你对逻辑的处理能力。产品经理关注的是用户场景和核心需求,而程序员关注的是逻辑边界和异常情况。程序的 bug 往往就是边界和特殊情况没有处理好。虽然说写出没有 bug 的程序几乎不可能,但是逻辑清晰的程序员能够把思路理清楚,减少 bug 发生的概率。
    • 设计和架构能力。好的代码需要保证易于维护和修改。这里面涉及很多知识,从简单的 “单一职责” 原则,到复杂的 “好的组合优于继承” 原则,其中设计模式相关的知识最多。写代码的时候多少还是能够看出这方面的能力。另外有些公司,例如 Facebook,会有专门的系统设计(System Design)面试环节,专注于考察设计能力。


    5.系统设计


    有一些公司喜欢考查一些系统设计的问题,简单来说,就是让你解决一个具体的业务需求,看看你是否能够将业务逻辑梳理清楚,并且拆解成各个模块,设计好模块间的关系。举几个例子,面试官可能让你:


    • 设计一个类似微博的信息流应用。
    • 设计一个本地数据缓存架构。
    • 设计一个埋点分析系统。
    • 设计一个直播答题系统。
    • 设计一个多端的数据同步系统。
    • 设计一个动态补丁的方案。


    这些系统的设计都非常考查一个人知识的全面性。通常情况下,如果一个人只知道 iOS 开发的知识,是很难做出相关的设计的。为了能够解决这些系统设计题,我们首先需要有足够的知识面,适度了解一下 Android 端、Web 端以及服务器端的各种技术方案背后的原理。你可以不写别的平台的代码,但是一定要理解它们在技术上能做到什么,不能做到什么。你也不必过于担心,面试官在考查的时候,还是会重点考查 iOS 相关的部分。


    我们将在下一小节,展开讨论如何准备和回答系统设计题。


    6.提问


    提问环节通常在面试结束前,取决于前面的部分是否按时结束,有些时候前面的环节占用了太多时间,也可能没有提问环节了。在后面的章节,我们会展开讨论一下如何提问。



    收起阅读 »

    腾讯抖音iOS岗位三面面经

    1.进程和线程的区别2.死锁的原因3.介绍虚拟内存4.常见排序算法,排序算法稳定的意思,快排的复杂度什么时候退化,基本有序用什么5.TCP可靠性6.http+https算法Z字遍历二叉树,归并排序后面说因为我不会java和安卓,会帮忙转推到iOS的组(面试的这...
    继续阅读 »

    1.进程和线程的区别


    2.死锁的原因


    3.介绍虚拟内存


    4.常见排序算法,排序算法稳定的意思,快排的复杂度什么时候退化,基本有序用什么


    5.TCP可靠性


    6.http+https


    算法


    Z字遍历二叉树,归并排序


    后面说因为我不会java和安卓,会帮忙转推到iOS的组(面试的这个组是java客户端)


    腾讯PCG iOS一面(1h)


    1.聊项目,聊了很久,一开始没有意会面试官想知道什么,最后说是想知道我这么做比起从客户端自己去实现的区别(这个项目是小米实习时候的项目,做的浏览器内核,页面翻译功能,


    基本每一个客户端应用都会有一个类似于浏览器内核的东西,对页面进行渲染,呈现,也可以叫渲染引擎,学前端的肯定知道这个东西,他主要是解释html,css,js的。


    我做的这个页面翻译功能可以不经过内核直接由客户端工程师用安卓客户端实现整套逻辑,所以这么问我了)


    2.实现string类,实现构造,析构,里面加一个kmp


    3.介绍智能指针,智能指针保存引用计数的变量存在哪里,引用计数是否线程安全


    4.算法:两个只有0和1的数字序列,只能0  1互换,每次当前位互换都会使后面的也换掉(比如,011000,换第二位,成了000111),计算从一个变到另一个需要几步操作。


    5.https,验证公钥有效的方法,为什么非对称对称并用


    腾讯PCG iOS二面 (40min)


    1.算法:


    合并排序链表


    2.static关键字的作用


    3.const关键字的作用


    4.成员初始化列表的作用


    5.指针和引用的区别


    6.又是很久的项目,怎么去学习浏览器内核(chromium内核的代码量有几千万行,而且写的很难懂,用了大量的设计模式,作为一个菜鸡真的很痛苦)


    ,怎么去调试项目中遇到的问题(这里主要是一个ipc接口没用对),你觉得人家google的是怎么去调的,你为什么和人家做法不一样?


    腾讯PCG iOS三面(2h)


    1.还是聊了很久项目(已经麻了,做过的东西一定要能说出口)


    2.浏览器呈现一个页面经过了哪几步(DOM树,layoutobject树,browser进程绘制)


    3.C++多态的实现


    4.DNS解析,递归与迭代的区别


    5.chromium用的渲染引擎是什么,这个渲染引擎对应的js解释引擎是什么(blink和v8,前几个问题表现有些差,这会在问一些1+1的问题了,哭)


    6.平时怎么学习技术的,看过哪些书,有过哪些输出(我把实习时写的项目wiki给截了个图)


    然后反问,打开牛客让我写了个代码,他不知道去哪了,我自己在这写,写了一个多钟头,你以为这是道很难的题吗?no,是我那会确实很菜,哈哈


    题目是,给一个字符串插入最少的字符,让这个字符串变成回文


    腾讯hr面 (40min)


    1.有哪些缺点


    2.投了哪些,为什么不投阿里头条(实习忙的我面你们都要面不过来了)


    3.如何选择offer


    4.家是哪的,为什么愿意去深圳


    每一个问题都不是简单的答完就完事了,他会跟着问很多


    然后反问时我问问题给我说了20分钟


    抖音 iOS一面 (1h20min)


    上来闲聊了一会


    1.算法:


    字符串大数相加


    写完问我有没有需要优化的地方(内存可以优化一下)


    2.string类赋值运算符是深拷贝还是浅拷贝


    3.算法:


    根据前序和中序输出二叉树的后续遍历


    4.C++ deque底层,deque有没有重载[]


    5.为什么要内存对齐,内存对齐的规则


    6.算法:


    上台阶,加了个条件,这次上两级,下次就只能上一级


    7.反问+闲聊


    抖音 iOS二面 (1h)


    十分钟不到的项目


    1.进程和线程的区别和联系


    2.线程共享哪些内存空间


    3.进程内存模型


    4.进程间通信方式


    5. 虚拟内存,为什么要有虚拟内存,虚拟内存如何映射到物理内存


    后面还挖了一些操作系统的问题,记不太清了


    5.TCP为什么四次挥手


    6.https客户端验证公钥的方法


    7.描述并写一下LRU


    8.说一下怎么学chromium的,怎么上手项目的


    9.C++内存分配,写了一段代码,看里面申请了哪部分内存,申请了多少,代码有什么问题


    10.代码里面的内存泄漏怎么解决,智能指针的引用计数怎么实现,那些成员函数会影响到引用计数


    11.代码里面有无线程安全问题,线程安全问题的是否会导致程序崩溃,为什么


    12.C++虚函数的实现原理,纯虚函数


    13.C++引用和指针的区别,引用能否为空


    抖音 iOS三面 (1h)


    1.lambda表达式,它应用表达式外变量的方式和区别


    2.decltype的作用,他和auto有什么不同


    3.C++的所有智能指针介绍一下


    4.C++thread里面的锁,条件变量,讲一下怎么用他们实现生产者消费者模型


    5.C++20有什么新东西(我就知道支持了协程,然后他说我就想问你协程,然后我说,其实我具体不了解,丢人了)


    6.右值引用是什么,移动构造函数有什么好处


    7.操作系统微内核宏内核(懵)


    8.进程间通信的共享内存,如何保证安全性(信号量),结合epoll讲讲共享内存


    9.TCP协议切片(懵)


    10.TCP协议的流量控制机制,滑窗为0时,怎么办


    11.算法


    合并K个排序链表


    12.合并K个排序数组,讲思路,我说归并,他说,传输参数是数组,不是vector,你如何判断数组的大小



    收起阅读 »

    iOS 整理出一份高级iOS面试题

    1、NSArray与NSSet的区别?NSArray内存中存储地址连续,而NSSet不连续NSSet效率高,内部使用hash查找;NSArray查找需要遍历NSSet通过anyObject访问元素,NSArray通过下标访问2、NSHashTable与NSMa...
    继续阅读 »

    1、NSArray与NSSet的区别?


    • NSArray内存中存储地址连续,而NSSet不连续
    • NSSet效率高,内部使用hash查找;NSArray查找需要遍历
    • NSSet通过anyObject访问元素,NSArray通过下标访问


    2、NSHashTable与NSMapTable?


    • NSHashTable是NSSet的通用版本,对元素弱引用,可变类型;可以在访问成员时copy
    • NSMapTable是NSDictionary的通用版本,对元素弱引用,可变类型;可以在访问成员时copy


    (注:NSHashTable与NSSet的区别:NSHashTable可以通过option设置元素弱引用/copyin,只有可变类型。但是添加对象的时候NSHashTable耗费时间是NSSet的两倍。

    NSMapTable与NSDictionary的区别:同上)


    3、属性关键字assign、retain、weak、copy


    • assign:用于基本数据类型和结构体。如果修饰对象的话,当销毁时,属性值不会自动置nil,可能造成野指针。
    • weak:对象引用计数为0时,属性值也会自动置nil
    • retain:强引用类型,ARC下相当于strong,但block不能用retain修饰,因为等同于assign不安全。
    • strong:强引用类型,修饰block时相当于copy。


    4、weak属性如何自动置nil的?


    • Runtime会对weak属性进行内存布局,构建hash表:以weak属性对象内存地址为key,weak属性值(weak自身地址)为value。当对象引用计数为0 dealloc时,会将weak属性值自动置nil。


    5、Block的循环引用、内部修改外部变量、三种block


    • block强引用self,self强引用block
    • 内部修改外部变量:block不允许修改外部变量的值,这里的外部变量指的是栈中指针的内存地址。__block的作用是只要观察到变量被block使用,就将外部变量在栈中的内存地址放到堆中。
    • 三种block:NSGlobalBlack(全局)、NSStackBlock(栈block)、NSMallocBlock(堆block)


    6、KVO底层实现原理?手动触发KVO?swift如何实现KVO?


    • KVO原理:当观察一个对象时,runtime会动态创建继承自该对象的类,并重写被观察对象的setter方法,重写的setter方法会负责在调用原setter方法前后通知所有观察对象值得更改,最后会把该对象的isa指针指向这个创建的子类,对象就变成子类的实例。
    • 如何手动触发KVO:在setter方法里,手动实现NSObject两个方法:willChangeValueForKey、didChangeValueForKey
    • swift的kvo:继承自NSObject的类,或者直接willset/didset实现。


    7、categroy为什么不能添加属性?怎么实现添加?与Extension的区别?category覆盖原类方法?多个category调用顺序


    • Runtime初始化时categroy的内存布局已经确定,没有ivar,所以默认不能添加属性。
    • 使用runtime的关联对象,并重写setter和getter方法。
    • Extenstion编译期创建,可以添加成员变量ivar,一般用作隐藏类的信息。必须要有类的源码才可以添加,如NSString就不能创建Extension。
    • category方法会在runtime初始化的时候copy到原来前面,调用分类方法的时候直接返回,不再调用原类。如何保持原类也调用(https://www.jianshu.com/p/40e28c9f9da5)。
    • 多个category的调用顺序按照:Build Phases ->Complie Source 中的编译顺序。


    8、load方法和initialize方法的异同。——主要说一下执行时间,各自用途,没实现子类的方法会不会调用父类的?

    load initialize 调用时机 app启动后,runtime初始化的时候 第一个方法调用前调用 调用顺序 父类->本类->分类 父类->本类(如果有分类直接调用分类,本类不会调用) 没实现子类的方法会不会调用父类的 否 是 是否沿用父类实现 否 是




    9、对 runtime 的理解。——主要是方法调用时如何查找缓存,如何找到方法,找不到方法时怎么转发,对象的内存布局


    OC中向对象发送消息时,runtime会根据对象的isa指针找到对象所属的类,然后在该类的方法列表和父类的方法列表中寻找方法执行。如果在最顶层父类中没找到方法执行,就会进行消息转发:Method resoution(实现方法)、fast forwarding(转发给其他对象)、normal forwarding(完整消息转发。可以转发给多个对象)


    10、runtime 中,SEL和IMP的区别?


    每个类对象都有一个方法列表,方法列表存储方法名、方法实现、参数类型,SEL是方法名(编号),IMP指向方法实现的首地址


    11、autoreleasepool的原理和使用场景?


    • 若干个autoreleasepoolpage组成的双向链表的栈结构,objc_autoreleasepoolpush、objc_autoreleasepoolpop、objc_autorelease
    • 使用场景:多次创建临时变量导致内存上涨时,需要延迟释放
    • autoreleasepoolpage的内存结构:4k存储大小



    12、Autorelase对象什么时候释放


    在没有手加Autorelease Pool的情况下,Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop。


    13、Runloop与线程的关系?Runloop的mode? Runloop的作用?内部机制?


    • 每一个线程都有一个runloop,主线程的runloop默认启动。
    • mode:主要用来指定事件在运行时循环的优先级
    • 作用:保持程序的持续运行、随时处理各种事件、节省cpu资源(没事件休息释放资源)、渲染屏幕UI


    14、iOS中使用的锁、死锁的发生与避免


    • @synchronized、信号量、NSLock等
    • 死锁:多个线程同时访问同一资源,造成循环等待。GCD使用异步线程、并行队列


    15、NSOperation和GCD的区别


    • GCD底层使用C语言编写高效、NSOperation是对GCD的面向对象的封装。对于特殊需求,如取消任务、设置任务优先级、任务状态监听,NSOperation使用起来更加方便。
    • NSOperation可以设置依赖关系,而GCD只能通过dispatch_barrier_async实现
    • NSOperation可以通过KVO观察当前operation执行状态(执行/取消)
    • NSOperation可以设置自身优先级(queuePriority)。GCD只能设置队列优先级(DISPATCH_QUEUE_PRIORITY_DEFAULT),无法在执行的block中设置优先级
    • NSOperation可以自定义operation如NSInvationOperation/NSBlockOperation,而GCD执行任务可以自定义封装但没有那么高的代码复用度
    • GCD高效,NSOperation开销相对高


    16、oc与js交互


    • 拦截url
    • JavaScriptCore(只适用于UIWebView)
    • WKScriptMessageHandler(只适用于WKWebView)
    • WebViewJavaScriptBridge(第三方框架)


    17、swift相比OC有什么优势?


    18、struct、Class的区别


    • class可以继承,struct不可以
    • class是引用类型,struct是值类型
    • struct在function里修改property时需要mutating关键字修饰


    19、访问控制关键字(public、open、private、filePrivate、internal)


    • public与open:public在module内部中,class和func都可以被访问/重载/继承,外部只能访问;而open都可以
    • private与filePrivate:private修饰class/func,表示只能在当前class源文件/func内部使用,外部不可以被继承和访问;而filePrivate表示只能在当前swift源文件内访问
    • internal:在整个模块或者app内都可以访问,默认访问级别,可写可不写


    20、OC与Swift混编


    • OC调用swift:import "工程名-swift.h” @objc 
    • swift调用oc:桥接文件


    21、map、filter、reduce?map与flapmap的区别?


    • map:数组中每个元素都经过某个方法转换,最后返回新的数组(xx.map({$0 * $0}))
    • flatmap:同map类似,区别在flatmap返回的数组不存在nil,并且会把optional解包;而且还可以把嵌套的数组打开变成一个([[1,2],[2,3,4],[5,6]] ->[1,2,2,3,4,5,6])
    • filter:用户筛选元素(xxx.filter({$0 > 25}),筛选出大于25的元素组成新数组)
    • reduce:把数组元素组合计算为一个值,并接收初始值()




    22、guard与defer


    • guard用于提前处理错误数据,else退出程序,提高代码可读性
    • defer延迟执行,回收资源。多个defer反序执行,嵌套defer先执行外层,后执行内层


    23、try、try?与try!


    • try:手动捕捉异常
    • try?:系统帮我们处理,出现异常返回nil;没有异常返回对应的对象
    • try!:直接告诉系统,该方法没有异常。如果出现异常程序会crash


    24、@autoclosure:把一个表达式自动封装成闭包


    25、throws与rethrows:throws另一个throws时,将前者改为rethrows


    26、App启动优化策略?main函数执行前后怎么优化


    • 启动时间 = pre-main耗时+main耗时
    • pre-main阶段优化:
    • 删除无用代码
    • 抽象重复代码
    • +load方法做的事情延迟到initialize中,或者+load的事情不宜花费太多时间
    • 减少不必要的framework,或者优化已有framework
    • Main阶段优化
    • didFinishLauchingwithOptions里代码延后执行
    • 首次启动渲染的页面优化


    27、crash防护?


    • unrecognized selector crash
    • KVO crash
    • NSNotification crash
    • NSTimer crash
    • Container crash(数组越界,插nil等)
    • NSString crash (字符串操作的crash)
    • Bad Access crash (野指针)
    • UI not on Main Thread Crash (非主线程刷UI (机制待改善))


    28、内存泄露问题?


    主要集中在循环引用问题中,如block、NSTime、perform selector引用计数问题。


    29、UI卡顿优化?


    30、架构&设计模式


    • MVC设计模式介绍
    • MVVM介绍、MVC与MVVM的区别?
    • ReactiveCocoa的热信号与冷信号
    • 缓存架构设计LRU方案
    • SDWebImage源码,如何实现解码
    • AFNetWorking源码分析
    • 组件化的实施,中间件的设计
    • 哈希表的实现原理?如何解决冲突


    31、数据结构&算法


    • 快速排序、归并排序
    • 二维数组查找(每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数)
    • 二叉树的遍历:判断二叉树的层数
    • 单链表判断环


    32、计算机基础


    1. http与https?socket编程?tcp、udp?get与post?
    2. tcp三次握手与四次握手
    1. 进程与线程的区别



    收起阅读 »

    一步一步搭建Flutter开发架子-Tabbar

    一点点搭建一个架构,架构对于开发比较重要,有固定的模式,第一不容易产生bug,并且有利于对于项目以及开发架构的理解。 对于一个app,常见的架构一般是底部有Tabbar形式,或者采用抽屉的形式,底部Tabbar大部分app都是平铺的,中间有一块凸出来的形式。普...
    继续阅读 »

    一点点搭建一个架构,架构对于开发比较重要,有固定的模式,第一不容易产生bug,并且有利于对于项目以及开发架构的理解。 对于一个app,常见的架构一般是底部有Tabbar形式,或者采用抽屉的形式,底部Tabbar大部分app都是平铺的,中间有一块凸出来的形式。

    普通Tabbar

     比较简单代码直接贴出来了

     @override
    Widget build(BuildContext context) {
    return Scaffold(
    bottomNavigationBar: normalBottomBar(),
    );
    }
    normalBottomBar() {
    return BottomNavigationBar(
    // 底部导航
    items: <BottomNavigationBarItem>[
    BottomNavigationBarItem(
    icon: Icon(Icons.home),
    label: 'Home',
    activeIcon: Icon(Icons.access_alarm)),
    BottomNavigationBarItem(icon: Icon(Icons.search), label: 'search'),
    BottomNavigationBarItem(icon: Icon(Icons.people), label: 'mine'),
    ],
    currentIndex: _selectedIndex,
    fixedColor: Colors.blue,
    elevation: 10, // default: 8.0
    type: BottomNavigationBarType.fixed,
    iconSize: 30,
    selectedFontSize: 12, // 默认是14,未选择是14
    onTap: _onItemTapped,
    );
    }
    _onItemTapped(int index) {
    setState(() {
    _selectedIndex = index;
    });
    }

    中间凸出的Tabbar

    官方的Material风格库中存在这种效果,不过个人感觉跟现在流行的风格有点不太匹配,所以封装了一个接近于现在流行风格的Tabbar。 刚开始的效果是这样的:  凸出的按钮封装了一下的代码片段:

    import 'dart:math';

    import 'package:flutter/material.dart';

    class CenterNavigationItem extends StatefulWidget {
    CenterNavigationItem({Key key, this.onTap}) : super(key: key);

    final Function onTap;

    @override
    _CenterNavigationItemState createState() => _CenterNavigationItemState();
    }

    class _CenterNavigationItemState extends State<CenterNavigationItem> {
    @override
    Widget build(BuildContext context) {
    return Stack(
    children: [
    // CustomPaint(size: Size(76, 76), painter: MyPainter()),
    Container(
    width: 76,
    height: 76,
    padding: EdgeInsets.all(8),
    decoration: BoxDecoration(
    color: Color.fromRGBO(250, 250, 250, 1),
    borderRadius: BorderRadius.circular(38)),
    child: FloatingActionButton(
    child: Icon(Icons.add),
    // child: TextField(),
    tooltip: '测试', // 长按弹出提示
    onPressed: () {
    widget.onTap();
    }),
    ),
    ],
    );
    }
    }
    // 主页面引用:
    @override
    Widget build(BuildContext context) {
    return Scaffold(
    floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
    floatingActionButton: CenterNavigationItem(
    onTap: () {
    setState(() {
    _selectedIndex = 1;
    });
    },
    ),
    bottomNavigationBar: normalBottomBar(),
    );
    }

    感觉还是差点意思, 在于底部线与线直接连接的不够平滑 

    带有动画效果的Tabbar

    继续改造一下的想法就是通过CustomPainter去画一个半圆。效果如下:  代码片段:

    class MyPainter extends CustomPainter {
    @override
    paint(Canvas canvas, Size size) {
    Paint paint = Paint()
    ..isAntiAlias = false
    ..color = Colors.green
    ..strokeCap = StrokeCap.round
    ..strokeWidth = 0
    ..style = PaintingStyle.stroke;
    print(pi);
    canvas.drawArc(
    new Rect.fromCircle(center: Offset(38, 38), radius: size.width / 2),
    pi,
    2 * pi * 0.5,
    false,
    paint);
    }

    @override
    bool shouldRepaint(CustomPainter oldDelegate) {
    return false;
    }
    }

    在凸出的按钮封装的代码中添加:

    class _CenterNavigationItemState extends State<CenterNavigationItem> {
    @override
    Widget build(BuildContext context) {
    return Stack(
    children: [
    CustomPaint(size: Size(76, 76), painter: MyPainter()),
    class _CenterNavigationItemState extends State<CenterNavigationItem> {
    @override
    Widget build(BuildContext context) {
    return Stack(
    children: [
    // 这个位置加入!!!!!!!!!!
    CustomPaint(size: Size(76, 76), painter: MyPainter()),
    Container(
    width: 76,
    height: 76,
    padding: EdgeInsets.all(8),
    decoration: BoxDecoration(
    color: Color.fromRGBO(250, 250, 250, 1),
    borderRadius: BorderRadius.circular(38)),
    child: FloatingActionButton(
    child: Icon(Icons.add),
    // child: TextField(),
    tooltip: '测试', // 长按弹出提示
    onPressed: () {
    widget.onTap();
    }),
    ),
    ],
    );
    }
    }
    ],
    );
    }
    }

    最终就是这个效果。还有就是也不能闭门造车,上网搜搜大家都是怎么去构建Tabbar的。其中在github上发现一个加入了动画的开源代码。Motion-Tab-Bar。分析了一波代码,看了一眼效果如下: 

    稍加修改了一下,留着以后项目可能用的上。over~欢迎讨论

    one more thing...

    • 1, 路由管理,
    • 2, 国际化管理,
    • 3, 数据持久化管理,
    • 4, 响应式管理方法


    作者:一天清晨
    链接:https://juejin.cn/post/6933496106827546632
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    收起阅读 »

    一步一步搭建Flutter开发架子-国际化,路由,本地化,响应式

    接上一篇文章,这篇文章主要介绍,路由管理,国际化管理,响应式管理方法,数据持久化管理。还是先看看大神么们都是怎么写的,从中学习一下。看到又一个比较好用的库getx,方便简介,基本上都包含今天要封装的内容,那就用起来吧。ps:有人可能会有想法说是应该自己写,总用...
    继续阅读 »

    接上一篇文章,这篇文章主要介绍,路由管理,国际化管理,响应式管理方法,数据持久化管理。还是先看看大神么们都是怎么写的,从中学习一下。看到又一个比较好用的库getx,方便简介,基本上都包含今天要封装的内容,那就用起来吧。ps:有人可能会有想法说是应该自己写,总用第三方的库,遇到问题不好处理,有点道理,换个想法如果自己没达到那个水平也可以先使用第三方库,好好看看大神的源码,来提升自己。总之所当面看待吧。


    引入GetX


    在pubspec.yaml文件中加入


    dependencies:
    get: ^3.24.0

    在需要使用的文件中引入


    import 'package:get/get.dart'

    在main.dart中使用GetMaterialApp替换MaterialApp


     return GetMaterialApp(
    enableLog: false,
    debugShowCheckedModeBanner: false,
    defaultTransition: Transition.rightToLeftWithFade,
    theme: ThemeData(
    primarySwatch: Colors.orange,
    visualDensity: VisualDensity.adaptivePlatformDensity,
    ),
    home: TabBarPage());

    路由管理


    比较喜欢这个的原因: 不需要获取上下文context直接跳转页面,代码很简洁,并且支持别名路由跳转
    效果


    不带参数颇通跳转


    Get.to(OtherPage())

    带参数跳转


    Get.to(OtherPage(id:''))

    无返回跳转


    比如在登录成功之后的跳转,不能够再返回到登录页面


    Get.off(OtherPage(id:''))

    跳转到Tabbar页面


    比如在商品的详情页面直接跳转到购物车页面,一般购物车页面在Tabbar上。


    Get.off(TabbarPage(currentIndex: 1));

    别名跳转


    这种情况大家可以去看下GetX的文档,这里就不介绍了。因为我不打算在项目里面坐这种跳转。ps:纯个人原因


    SnackBars,Dialogs,BottomSheets使用


    GetX中,我们也可以不获取上下文context进行跳用SnackBars,Dialogs, BottomSheets使用


    SnackBars


    效果:


     Get.snackbar(
    "Hey i'm a Get SnackBar!",
    "It's unbelievable! I'm using SnackBar without context, without boilerplate, without Scaffold, it is something truly amazing!",
    icon: Icon(Icons.alarm),
    shouldIconPulse: true,
    barBlur: 20,
    isDismissible: true,
    duration: Duration(seconds: 3),
    backgroundColor: Colors.red);

    具体的属性,大家可以点击进去看下源码配置


    Dialogs


    效果:


     Get.defaultDialog(
    onConfirm: () => print("Ok"),
    buttonColor: Colors.white,
    middleText: "Dialog made in 3 lines of code");


    也可以弹出自定义的组件


    Get.dialog(YourDialogWidget());

    BottomSheets


    效果:


    Get.bottomSheet(Container(
    decoration: BoxDecoration(color: Colors.red),
    child: Wrap(
    children: <Widget>[
    ListTile(
    leading: Icon(Icons.music_note),
    title: Text('Music'),
    onTap: () {}),
    ListTile(
    leading: Icon(Icons.videocam),
    title: Text('Video'),
    onTap: () {},
    ),
    ],
    ),
    ));

    以上是简单的用法,我们可以新建哥utils文件夹,封装成一个工具类去调用这个方法。


    国际化管理


    目前涉及到2中模式,



    • 根据系统语言设置国际化

    • 在应用内设置国际化显示


    首先创建一个Languages.dart文件 简单的写了一个hello的中英文含义


    import 'package:get/get.dart';
    class Languages extends Translations {
    @override
    Map<String, Map<String, String>> get keys => {
    'zh_CN': {
    'hello': '你好 世界',
    },
    'en_US': {
    'hello': 'Hallo Welt',
    }
    };
    }

    在main.dart中加入代码:


    return GetMaterialApp(
    translations: Languages(), // 你的翻译
    locale: Locale('zh', 'CN'), // 将会按照此处指定的语言翻译
    fallbackLocale: Locale('en', 'US'), // 添加一个回调语言选项,以备上面指定的语言翻译不存在
    );

    显示只需要加入如下就ok。很简单


    Text('hello'.tr)


    跟随系统语言


    ui.window.locale 获取当前系统语言,设置本地语言


    GetMaterialApp(
    translations: Languages(), // 你的翻译
    locale: ui.window.locale, // 将会按照此处指定的语言翻译
    fallbackLocale: Locale('en', 'US'), // 添加一个回调语言选项,以备上面指定的语言翻译 不存在
    ......
    )

    在应用内设置国际化显示


    更新文字显示为中文如下:


    var locale = Locale('zh', 'CN');
    Get.updateLocale(locale);

    多写两句用RadioListTile实现一下效果


    RadioListTile(
    value: 'chinese',
    groupValue: _selected,
    title: Text('中文'),
    subtitle: Text('中文'),
    selected: _selected == 'chinese',
    onChanged: (type) {
    var locale = Locale('zh', 'CN');
    Get.updateLocale(locale);
    setState(() {
    _selected = type;
    });
    }),
    RadioListTile(
    value: 'english',
    groupValue: _selected,
    title: Text('英文'),
    subtitle: Text('英文'),
    selected: _selected == 'english',
    onChanged: (type) {
    var locale = Locale('en', 'US');
    Get.updateLocale(locale);
    setState(() {
    _selected = type;
    });
    },
    ),

    这是本人测试使用用的代码。看一下效果


    响应式管理方法


    GetX举例是一个计数器的例子,已经很容易理解了,作用就是不用在引入过多的状态管理的库,比如provide之类的。用法差不多。更简洁。还是记录一下,方便以后查看用


    class Controller extends GetxController{
    var count = 0.obs;
    increment() => count++;
    }

    lass Home extends StatelessWidget {

    @override
    Widget build(context) {

    // 使用Get.put()实例化你的类,使其对当下的所有子路由可用。
    final Controller c = Get.put(Controller());

    return Scaffold(
    // 使用Obx(()=>每当改变计数时,就更新Text()。
    appBar: AppBar(title: Obx(() => Text("Clicks: ${c.count}"))),

    // 用一个简单的Get.to()即可代替Navigator.push那8行,无需上下文!
    body: Center(child: RaisedButton(
    child: Text("Go to Other"), onPressed: () => Get.to(Other()))),
    floatingActionButton:
    FloatingActionButton(child: Icon(Icons.add), onPressed: c.increment));
    }
    }

    class Other extends StatelessWidget {
    // 你可以让Get找到一个正在被其他页面使用的Controller,并将它返回给你。
    final Controller c = Get.find();

    @override
    Widget build(context){
    // 访问更新后的计数变量
    return Scaffold(body: Center(child: Text("${c.count}")));
    }
    }

    这块还没有在程序中使用,但是状态管理在程序中使用还是很方便的,比如更改用户信息,登录。购物车逻辑中都可以使用


    数据持久化管理


    这个地方引入了第二个第三方库


      flustars: ^0.3.3
    # https://github.com/Sky24n/sp_util
    # sp_util分拆成单独的库,可以直接引用
    sp_util: ^1.0.1

    用起来也很方便感觉不错。
    为了之后方便使用,现定义一个Global.dart文件,做初始化操作


    import 'package:flustars/flustars.dart';
    class Global {
    static Future initSqutil() async => await SpUtil.getInstance();
    }
    在main方法中调用:
    Global.initSqutil();

    接下来进行存储数据以及获取数据的方法,类型包括:字符串,布尔值,对象,数组
    举个例子:


    存数据
    SpUtil.putString( 'login', '登录了',);
    取数据
    SpUtil.getString('login',defValue: '');

    额外提的一点就是存储对象类型数组,分两种形式,getObjList, getObjectList方法


     类似泛型的结构, 可以进行转换
    List<Map> dataList = SpUtil.getObjList('cityMap', (v) => v);
    返回一个Map数组
    List<Map> dataList = SpUtil.getObjectList('cityMap');

    这个地方可以配合国际化语言切换时使用。比如每次改变语言进行存储。然后每次打开app进行,获取初始化。


    one more things...



    • 网络请求

    • 页面不同状态展示封装

    作者:一天清晨
    链接:https://juejin.cn/post/6934572753601167374
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

    Python操作Redis

    Part1前言前面我们都是使用 Redis 客户端对 Redis 进行使用的,但是实际工作中,我们大多数情况下都是通过代码来使用 Redis 的,由于小编对 Python 比较熟悉...
    继续阅读 »

    Part1前言

    前面我们都是使用 Redis 客户端对 Redis 进行使用的,但是实际工作中,我们大多数情况下都是通过代码来使用 Redis 的,由于小编对 Python 比较熟悉,所以我们今天就一起来学习下如何使用 Python 来操作 Redis

    Part2环境准备

    • Redis 首先需要安装好。
    • Python 安装好(建议使用 Python3)。
    • Redis 的 Python 库安装好(pip install redis)。

    Part3开始实践

    1小试牛刀

    例:我们计划通过 Python 连接到 Redis。然后写入一个 kv,最后将查询到的 v 打印出来。

    直接连接

    #!/usr/bin/python3

    import redis # 导入redis模块

    r = redis.Redis(host='localhost', port=6379, password="pwd@321", decode_responses=True) # host是redis主机,password为认证密码,redis默认端口是6379
    r.set('name', 'phyger-from-python-redis') # key是"name" value是"phyger-from-python-redis" 将键值对存入redis缓存
    print(r['name']) # 第一种:取出键name对应的值
    print(r.get('name')) # 第二种:取出键name对应的值
    print(type(r.get('name')))


    执行结果
    服务端查看客户端列表

    其中的 get 为连接池最后一个执行的命令。

    连接池

    通常情况下,需要连接 redis 时,会创建一个连接,基于这个连接进行 redis 操作,操作完成后去释放。正常情况下,这是没有问题的,但是并发量较高的情况下,频繁的连接创建和释放对性能会有较高的影响,于是连接池发挥作用。

    连接池的原理:预先创建多个连接,当进行 redis 操作时,直接获取已经创建好的连接进行操作。完成后,不会释放这个连接,而是让其返回连接池,用于后续 redis 操作!这样避免连续创建和释放,从而提高了性能!

    #!/usr/bin/python3

    import redis,time # 导入redis模块,通过python操作redis 也可以直接在redis主机的服务端操作缓存数据库

    pool = redis.ConnectionPool(host='localhost', port=6379, password="pwd@321", decode_responses=True) # host是redis主机,需要redis服务端和客户端都起着 redis默认端口是6379
    r = redis.Redis(connection_pool=pool)
    r.set('name', 'phyger-from-python-redis')
    print(r['name'])
    print(r.get('name')) # 取出键name对应的值
    print(type(r.get('name')))


    执行结果

    你会发现,在实际使用中直连和使用连接池的效果是一样的,只是在高并发的时候会有明显的区别。

    2基操实践

    对于众多的 Redis 命令,我们在此以 SET 命令为例进行展示。

    格式: set(name, value, ex=None, px=None, nx=False, xx=False)

    在 redis-py 中 set 命令的参数:

    参数名释义
    ex过期时间(m)
    px过期时间(ms)
    nx如果为真,则只有 name 不存在时,当前 set 操作才执行
    xx如果为真,则只有 name 存在时,当前 set 操作才执行

    ex

    我们计划创建一个 kv 并且设置其 ex 为 3,期待 3 秒后此 k 的 v 会变为 None

    #!/usr/bin/python3

    import redis,time # 导入redis模块,通过python操作redis 也可以直接在redis主机的服务端操作缓存数据库

    pool = redis.ConnectionPool(host='localhost', port=6379, password="pwd@321", decode_responses=True) # host是redis主机,需要redis服务端和客户端都起着 redis默认端口是6379
    r = redis.Redis(connection_pool=pool)
    r.set('name', 'phyger-from-python-redis',ex=3)
    print(r['name']) # 应当有v
    time.sleep(3)
    print(r.get('name')) # 应当无v
    print(type(r.get('name')))


    3秒过期

    nx

    由于 px 的单位太短,我们就不做演示,效果和 ex 相同。

    我们计划去重复 set 前面已经 set 过的 name,不出意外的话,在 nx 为真时,我们将会 set 失败。但是人如果 set 不存在的 name1,则会成功。

    #!/usr/bin/python3

    import redis,time # 导入redis模块,通过python操作redis 也可以直接在redis主机的服务端操作缓存数据库

    pool = redis.ConnectionPool(host='localhost', port=6379, password="pwd@321", decode_responses=True) # host是redis主机,需要redis服务端和客户端都起着 redis默认端口是6379
    r = redis.Redis(connection_pool=pool)
    r.set('name', 'phyger-0',nx=3) # set失败
    print(r['name']) # 应当不生效
    r.set('name1', 'phyger-1',nx=3) # set成功
    print(r.get('name1')) # 应当生效
    print(type(r.get('name')))


    只有不存在的k才会被set

    如上,你会发现 name 的 set 未生效,因为 name 已经存在于数据库中。而 name1 的 set 已经生效,因为 name1 是之前在数据库中不存在的。

    xx

    我们计划去重复 set 前面已经 set 过的 name,不出意外的话,在 nx 为真时,我们将会 set 成功。但是人如果 set 不存在的 name2,则会失败。

    #!/usr/bin/python3

    import redis,time # 导入redis模块,通过python操作redis 也可以直接在redis主机的服务端操作缓存数据库

    pool = redis.ConnectionPool(host='localhost', port=6379, password="pwd@321", decode_responses=True) # host是redis主机,需要redis服务端和客户端都起着 redis默认端口是6379
    r = redis.Redis(connection_pool=pool)
    r.set('name', 'phyger-0',xx=3) # set失败
    print(r['name']) # 应当变了
    r.set('name2', 'phyger-1',xx=3) # set成功
    print(r.get('name2')) # 应当没有set成功
    print(type(r.get('name')))


    只有存在的k才会被set

    以上,就是今天全部的内容,更多信息建议参考 redis 官方文档。


    作者:phyger
    来源:https://mp.weixin.qq.com/s/bsv57OPKubD2dz0Wskn6eQ

    收起阅读 »

    基于echarts 24种数据可视化展示,填充数据就可用,动手能力强的还可以DIY

    前言我们先跟随百度百科了解一下什么是“数据可视化 [1]”。   数据可视化,是关于数据视觉表现形式的科学技术研究。   其中,这种数据的视觉表现形式被定义为,一种以某种概要形式抽提出来的信息,包括相应信息单位的各种属性和变量。   它是一个处于不断演变之中...
    继续阅读 »

    前言

    我们先跟随百度百科了解一下什么是“数据可视化 [1]”。



      数据可视化,是关于数据视觉表现形式的科学技术研究。


      其中,这种数据的视觉表现形式被定义为,一种以某种概要形式抽提出来的信息,包括相应信息单位的各种属性和变量。


      它是一个处于不断演变之中的概念,其边界在不断地扩大。


    主要指的是技术上较为高级的技术方法,而这些技术方法允许利用图形、图像处理计算机视觉以及用户界面,通过表达、建模以及对立体、表面、属性以及动画的显示,对数据加以可视化解释。


    与立体建模之类的特殊技术方法相比,数据可视化所涵盖的技术方法要广泛得多。



      大家对展厅显示、客户导流、可视化汇报工作、对数据结果进行图形分析等这些业务场景都不陌生。


    很多后端大多都只提供接口数据,并没有去构建前端显示页面,一来是不专业(各种特效、自适应等),二来是公司有前端,用不到后端来写。


      但是暂时用到不代表我们不用,用的时候写不来怎么办?下面介绍24种数据可视化的demo,直接下载下来填充数据就可以跑起来,不满足的还可以DIY(演示地址+下载地址),yyds。


    演示地址

    注意:演示中如果有加载失败的,是环境问题,下载下来运行就好了。


    演示地址:https://www.xiongze.net/viewdata/index.html [2]


    现有的24种如下:


    大数据展示系统、物流数据概况系统、物流订单系统、物流信息系统、办税渠道监控平台、车辆综合管控平台、


    电子商务公共服务中心、各行业程序员中心、简洁大数据统计中心、警务平台大数据统计、农业监测大数据指挥舱、


    农业监控数据平台、社会治理运行分析云图、水质监测大数据中心、水质情况实时监测预警系统、


    物联网大数据统计平台、消防监控预警、销售数据报表中心、医疗大数据中心、营业数据统中心、


    智慧旅游综合服务平台、智慧社区内网比对平台、智慧物流服务中心、政务大数据共享交换平台。



    echarts图表库:Echarts提供了常规的折线图、柱状图、散点图、饼图、k线图,用于统计的盒形图,用于地理数据可视化的地图、热力图、线图,用于关系数据可视化的关系图、treemap、旭日图,多维数据可视化的平行坐标,还有用于 BI 的漏斗图,仪表盘,并且支持图与图之间的混搭。



    下面demo里面的图标颜色、样式都可以在 echarts官网-文档-配置项手册里面进行查看, 是支持通过修改里面的配置项里面的属性来达到项目需求,我们可以去进行查看修改。


    pic_a12bf32a.png

    下载地址

    Git下载链接:https://gitee.com/xiongze/viewdata.git [3]


    百度网盘下载链接:https://pan.baidu.com/s/1jgwK6BvrS2rmbkrtW2MpYA提取码:xion


    Demo示例(部分)

    1、总览

    pic_e6090ae5.png

    pic_d4d67ce3.png

    pic_f40b7776.png

    2、物流信息展示

    pic_8757023a.png

    3、车辆综合管控平台

    pic_cacec7a0.png

    4、农业监测大数据指挥舱

    pic_72002bf9.png

    5、水质情况实时监控预警中心

    pic_ab9ecf3a.png

    6、消防监控预警中心

    pic_94a47e47.png

    7、医疗大数据中心

    pic_9744b569.png

    8、物联网平台数据中心

    pic_50685f14.png

    更多……

    总共24种,这里就不一一展示了,大家下载下来就可以玩了。


    可视化应用

      数据可视化的开发和大部分项目开发一样,也是根据需求来根据数据维度或属性进行筛选,根据目的和用户群选用表现方式。同一份数据可以可视化成多种看起来截然不同的形式。



    • 有的可视化目标是为了观测、跟踪数据,所以就要强调实时性、变化、运算能力,可能就会生成一份不停变化、可读性强的图表。
    • 有的为了分析数据,所以要强调数据的呈现度、可能会生成一份可以检索、交互式的图表
    • 有的为了发现数据之间的潜在关联,可能会生成分布式的多维的图表。
    • 有的为了帮助普通用户或商业用户快速理解数据的含义或变化,会利用漂亮的颜色、动画创建生动、明了,具有吸引力的图表。
    • 还有的被用于教育、宣传或政治,被制作成海报、课件,出现在街头、广告手持、杂志和集会上。这类可视化拥有强大的说服力,使用强烈的对比、置换等手段,可以创造出极具冲击力自指人心的图像。在国外许多媒体会根据新闻主题或数据,雇用设计师来创建可视化图表对新闻主题进行辅助。

      数据可视化的应用价值,其多样性和表现力吸引了许多从业者,而其创作过程中的每一环节都有强大的专业背景支持。无论是动态还是静态的可视化图形,都为我们搭建了新的桥梁,让我们能洞察世界的究竟、发现形形色色的关系,感受每时每刻围绕在我们身边的信息变化,还能让我们理解其他形式下不易发掘的事物。


    参考文献

    [1]百度百科:数据可视化
    [2]演示地址:https://www.xiongze.net/viewdata/index.html
    [3]下载链接:https://gitee.com/xiongze/viewdata.git
    [4]数据可视化概念



    作者:熊泽-学习中的苦与乐
    来源:https://www.cnblogs.com/xiongze520/p/15588852.html


    收起阅读 »

    CommonJS和ES6 Module究竟是什么

    对于前端模块化总是稀里糊涂,今天深入学习一下前端模块化,彻底弄懂CommonJs和ES6 Module,希望本文可以给你带来帮助。 CommonJS 模块 CommonJS中规定每个文件是一个模块。将一个JS文件通过script标签插入页面与封装成Common...
    继续阅读 »

    对于前端模块化总是稀里糊涂,今天深入学习一下前端模块化,彻底弄懂CommonJs和ES6 Module,希望本文可以给你带来帮助。


    CommonJS


    模块


    CommonJS中规定每个文件是一个模块。将一个JS文件通过script标签插入页面与封装成CommonJS模块最大的不同在于,前者的顶层作用域是全局作用域,在进行变量及函数声明时会污染全局环境;而后者形成一个属于模块自身的作用域,所有的变量及函数只能自己访问,对外不可见。


    导出


    导出是一个模块向外暴露自身的唯一方式。在commonJS中,通过modul
    e.exports可以导出模块中的内容
    。下面的代码导出了一个对象,包含name和add属性。


    module.exports = {
    name: 'calculater',
    add: function(a, b){
    return a+b;
    }
    }

    为了书写方便,CommonJS也支持直接使用exports。


    exports.name = 'calculater';
    exports.add = function(a, b){
    return a+b;
    }

    exports可以理解为


    var module = {
    exports:{}
    };
    var exports = module.exports;

    注意错误的用法:



    1. 不要给exports直接赋值,否则导出会失效。如下代码,对exports赋值,使其指向新的对象。module.exports却仍然是原来的空对象,因此name属性并不会被导出。


    exports = {
    name: 'calculater'
    }


    1. 不恰当的把module.exports和exports混用。如下代码,先通过exports导出add属性,然后将module.exports重新赋值为另一个对象,将导致add属性丢失,最后导出只有name。


    exports.add = function(a,b){
    return a+b;
    }
    module.exports = {
    name: 'calculater'
    }

    导入


    在CommonJs中,使用require进行模块导入。


    const calculator = require('./calculator.js')
    let sum = calculator.add(2,3)

    注意:

    1. require的模块是第一次被加载,这时会首先执行该模块,然后导出内容
    2. require的模块曾被加载过,这时该模块的代码不会再次执行,而是直接导出上次执行后得到的结果。
    3. 对于不需要获取导出内容的模块,直接使用require即可。
    4. require函数可以接收表达式,借助这个特性可以动态地指定模块加载路径。

    const moduleName = ['a.js', 'b.js'];
    moduleNames.forEach(name => {
    require('./'+name)
    })

    ES6 Module


    模块


    ES6 Module是ES语法的一部分,它也是将每个文件作为一个模块,每个模块拥有自身的作用域。


    导出


    在ES6 Module中使用export命令来导出模块。export有两种形式:

    • 命名导出
    • 默认导出

    一个模块可以有多个命名导出,它有两种不同的写法:


    //写法1,将变量的声明和导出写在一行
    export const name = 'calculator'
    export const add = function(a, b){return a+b}

    //写法2,先进行变量的声明,然后在用同一个export语句导出。
    const name = 'calculator'
    const add = function(a, b){return a+b}
    export {name, add}

    与命名导出不同,模块的默认导出只能有一个。


    export default {
    name: 'calculator',
    add: function(a, b){
    return a+b
    }
    }

    导入


    ES6 Module中使用import语法导入模块。


    加载带有命名导出的模块

    有两种方式



    1. import后面要跟一对大括号,将导入的变量名包裹起来。并且这些变量名要与导出的变量名完全一致。


    //calculator.js
    const name = 'calculator'
    const add = function(a, b){return a+b}
    export {name, add}

    //index.js
    import {name, add} from './calculator.js'
    add(2,3)


    1. 采用整体导入的方式, 使用import * as myModule可以把所有导入的变量作为属性值添加到myModule中,从而减少对当前作用域的影响。


    import * as calculator from './calculator.js'
    console.log(calculator.add(2,3))
    console.log(calculator.name)

    加载默认导出的模块

    import后面直接跟变量名,并且这个名字可以自由指定


    //calculator.js
    export default {
    name: 'calculator',
    add: function(a, b){
    return a+b
    }
    }
    //index.js
    import calculator from './calculator.js'
    calculator.add(2,3)

    两种导入方式混合起来

    import React, {Component} from 'react'

    这里的React对应的是该模块的默认导出,Component则是其命名导出中的一个变量。


    CommonJS和ES6 Module的区别


    动态和静态

    • CommonJS是动态的模块结构,模块依赖关系的建立发生在代码的运行阶段
    • ES Module是静态的模块结构,在编译阶段就可以分析模块的依赖关系。


    相比于CommonJS,ES6 Module有如下优势:

    1. 死代码监测和排除
    2. 模块变量和类型检查
    3. 编译器优化

    值拷贝和动态映射


    在导入一个模块时,对于CommonJs来说,获取的是一份导出值的拷贝。而在ES6 Module中则是值的动态映射,并且这个映射是只读的。


    总结

  • CommonJS使用Module.exports或exports导出
  • CommonJS使用require()函数导入,该函数返回一个对象,包含导出的变量。
  • ES6 Module使用export导出,包括命名导出或者默认导出。
  • 命名导出是export后面跟一个大括号,括号里面包含导出的变量
  • 命名导出的另一种方式是export和变量声明在一行。
  • 默认导出是export default,只能有一个默认导出
  • ES6 Module导入使用import
  • 加载带有命名导出的模块,import后面要跟一对大括号,将导入的变量名包裹起来。并且这些变量名要与导出的变量名完全一致。
  • 采用整体导入的方式, 使用import * as 可以把所有导入的变量作为属性值添加到中,从而减少对当前作用域的影响。
  • 加载默认导出的模块,import后面直接跟变量名,并且这个名字可以自由指定。

  • 作者:邓惠子本尊

    链接:https://juejin.cn/post/7033651418934444063

    收起阅读 »

    通过协程简化Activity之间的通信

    假设我们有这样一个常用的场景:有两个Activity,第一个Activity展示一段文本点击“编辑”按钮启动第二个Activity,并把这段文本当做参数传递到第二个Activity在第二个Activity编辑这个字符串编辑完成后点击保存将结果返回到第一个Act...
    继续阅读 »

    假设我们有这样一个常用的场景:

    • 有两个Activity,第一个Activity展示一段文本
    • 点击“编辑”按钮启动第二个Activity,并把这段文本当做参数传递到第二个Activity
    • 在第二个Activity编辑这个字符串
    • 编辑完成后点击保存将结果返回到第一个Activity
    • 第一个Activity展示修改后的字符串

    如下图:

    这是一个非常简单和常见的场景,我们一般通过 startActivityForResult 的方式传递参数,并在 onActivityResult 接收编辑后的结果,代码也很简单,如下:


    //第一个Activity启动编辑Activity
    btnEditByTradition.setOnClickListener {
    val content = tvContent.text.toString().trim()
    val intent = Intent(this, EditActivity::class.java).apply {
    putExtra(EditActivity.ARG_TAG_CONTENT, content)
    }
    startActivityForResult(intent, REQUEST_CODE_EDIT)
    }
    //EditActivity回传编辑后的结果
    btnSave.setOnClickListener {
    val newContent = etContent.text.toString().trim()
    setResult(RESULT_OK, Intent().apply {
    putExtra(RESULT_TAG_NEW_CONTENT, newContent)
    })
    finish()
    }
    //第一个Activity中接受编辑后的结果,并展示
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    when (requestCode) {
    REQUEST_CODE_EDIT -> {
    if (resultCode == RESULT_OK && data != null) {
    val newContent = data.getStringExtra(EditActivity.RESULT_TAG_NEW_CONTENT)
    tvContent.text = newContent
    }
    }
    else -> super.onActivityResult(requestCode, resultCode, data)
    }
    }

    那这种方式有什么缺点呢?

    1. 代码分散,可读性差
    2. 封装不彻底,调用方需要到EditActivity才能知道需要传递什么参数,类型是什么,key是什么
    3. 调用方需要知道EditActivity是如何返回的参数类型和key是什么才能正确解析
    4. 约束性差,各种常量的定义(REQUEST_CODE,PARAM_KEY等),若项目管理不严谨,重复定义,导致后期重构和维护比较麻烦

    那有没有一种方式能解决上面的缺点呢?我们期望的是:

    1. 一个对外提供某些功能的Activity应该有足够的封装性,调用者像调用普通方法一样,一行代码即可完成调用
    2. 方法的参数列表就是调用本服务需要传递的参数(参数数量,参数类型,是否必须)
    3. 方法的返回参数就是本服务的返回结果
    4. 提供服务的Activity像一个组件一样,能对外提供功能都是以一个个方法的形式体现

    通过Kotlin 协程和一个不可见的Fragment来实现。


    btnEditByCoroutine.setOnClickListener {
    GlobalScope.launch {
    val content = tvContent.text.toString().trim()

    // 调用EditActivity的 editContent 方法
    // content为要编辑的内容
    // editContent 即为编辑后的结果
    val newContent = EditActivity.editContent(this@MainActivity, content)

    if (!newContent.isNullOrBlank()) {
    tvContent.text = newContent
    }
    }
    }


    通过上面的代码,我们看到,通过一个方法即可完成调用,基本实现了上文提到的期望。 那 editContent 方法内部是如何实现的呢?看如下代码:


    /**
    * 对指定的文本进行编辑
    * @param content 要编辑的文本
    *
    * @return 可空 不为null 表示编辑后的内容 为null表示用户取消了编辑
    */

    @JvmStatic
    suspend fun editContent(activity: FragmentActivity, content: String): String? =
    suspendCoroutine { continuation ->
    val editFragment = BaseSingleFragment().apply {
    intentGenerator = {
    Intent(it, EditActivity::class.java).apply {
    putExtra(ARG_TAG_CONTENT, content)
    }
    }
    resultParser = { resultCode, data ->
    if (resultCode == RESULT_OK && data != null) {
    val result = data.getStringExtra(RESULT_TAG_NEW_CONTENT)
    continuation.resume(result)
    } else {
    continuation.resume(null)
    }
    removeFromActivity(activity.supportFragmentManager)
    }
    }
    editFragment.addToActivity(activity.supportFragmentManager)
    }

    这里需要借助一个“BaseSingleFragment”来实现,这是因为我不能违背 ActivityManagerService 的规则,依然需要通过 startActivityForResult 和 onActivityResult 来实现,所以我们这里通过一个不可见(没有界面)的 Fragment ,将这个过程封装起来,代码如下:


    class BaseSingleFragment : Fragment() {


    /**
    * 生成启动对应Activity的Intent,因为指定要启动的Activity,如何启动,传递参数,所以由具体的使用位置来实现这个Intent
    *
    * 使用者必须实现这个lambda,否则直接抛出一个异常
    */

    var intentGenerator: ((context: Context) -> Intent) = {
    throw RuntimeException("you should provide a intent here to start activity")
    }

    /**
    * 解析目标Activity返回的结果,有具体实现者解析,并回传
    *
    * 使用者必须实现这个lambda,否则直接抛出一个异常
    */

    var resultParser: (resultCode: Int, data: Intent?) -> Unit = { resultCode, data ->
    throw RuntimeException("you should parse result data yourself")
    }

    companion object {
    const val REQUEST_CODE_GET_RESULT = 100
    }

    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val context = requireContext()
    startActivityForResult(intentGenerator.invoke(context), REQUEST_CODE_GET_RESULT)
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    if (requestCode == REQUEST_CODE_GET_RESULT) {
    resultParser.invoke(resultCode, data)
    } else {
    super.onActivityResult(requestCode, resultCode, data)
    }
    }


    /**
    * add current fragment to FragmentManager
    */

    fun addToActivity(fragmentManager: FragmentManager) {
    fragmentManager.beginTransaction().add(this, this::class.simpleName)
    .commitAllowingStateLoss()
    }

    /**
    * remove current fragment from FragmentManager
    */

    fun removeFromActivity(fragmentManager: FragmentManager) {
    fragmentManager.beginTransaction().remove(this).commitAllowingStateLoss()
    }
    }
    当然,这是一个 suspend 方法,java是不支持协程的,而现实情况是,很多项目都有中途集成Kotlin的,有很多遗留的java代码,对于这种情况,我们需要提供相应的java实现吗?The answer is no. Java 代码同样可以调用 suspend 方法,调用方式如下:
    btnEditByCoroutine.setOnClickListener((view) -> {
    String content = tvContent.getText().toString().trim();
    EditActivity.editContent(MainActivityJava.this, content, new Continuation<String>() {
    @NotNull
    @Override
    public CoroutineContext getContext() {
    return EmptyCoroutineContext.INSTANCE;
    }

    @Override
    public void resumeWith(@NotNull Object o) {
    String newContent = (String) o;
    if (!TextUtils.isEmpty(content)) {
    tvContent.setText(newContent);
    }
    }
    });
    });

    虽然是通过回调的方式,在resumeWith方法中来接受结果,但也是比 startActivityForResult 的方式要好的多。

    Perfect!!!

    这种实现方式的灵感是来源于 RxPermission 对权限申请流程的实现,在此对 RxPermission 表达感谢。 另外 Glide 3.X 版本对图片加载任务的启动,暂停,和取消和Activity的和生命周期绑定也是通过向FragmentManager中添加了一个隐藏的Fragment来实现的。 这个demo的代码在

    CourtineTest GitHub ,

    原文链接:https://juejin.cn/post/7033598140766912549?utm_source=gold_browser_extension

    收起阅读 »

    屏幕旋转切换机制详解

    前言 屏幕旋转的机制; 默认情况下,当用户手机的重力感应器打开后,旋转屏幕方向,会导致当前activity发生onDestroy-> onCreate,这样会重新构造当前activity和界面布局,如果在Camera界面,则表现为卡顿或者黑屏一段时间; 今天...
    继续阅读 »

    前言



    屏幕旋转的机制;


    默认情况下,当用户手机的重力感应器打开后,旋转屏幕方向,会导致当前activity发生onDestroy-> onCreate,这样会重新构造当前activity和界面布局,如果在Camera界面,则表现为卡顿或者黑屏一段时间;


    今天就介绍下平面旋转方面的知识点;



    一、screenOrientation属性说明


    android:screenOrientation属性说明:



    • unspecified,默认值,由系统决定,不同手机可能不一致

    • landscape,强制横屏显示,只有一个方向

    • portrait,强制竖屏显,只有一个方向

    • behind,与前一个activity方向相同

    • sensor,根据物理传感器方向转动,用户90度、180度、270度旋转手机方向,activity都更着变化,会重启activity(无论系统是否设置为自动转屏,activity页面都会跟随传感器而转屏)

    • sensorLandscape,横屏旋转,就是可以上下旋转,有两个方向,不会重启activity

    • sensorPortrait,竖屏旋转,就是可以上下旋转,有两个方向,不会重启activity

    • nosensor,旋转设备时候,界面不会跟着旋转。初始化界面方向由系统控制(无论系统是否设置为自动转屏,activity页面都不会转屏)

    • user,用户当前设置的方向

    • reverseLandscape,与正常的横向方向相反显示(反向横屏)

    • reversePortrait,与正常的纵向方向相反显示(反向竖屏)(我设置没用)


    二、屏幕旋转详解


    1、配置文件设置



    • AndroidManifest.xml设置;

    • 横屏或者竖屏是被直接定死,旋转方向不会变化,只有一个方向(意思是旋转180°也不会改变),当然就不会在手机旋转的时候重启activity;


      
    
                android:name=".test1"

                android:screenOrientation="landscape" />

           
                android:name=".test2"

                android:screenOrientation="portrait" />

    2、代码设置


    调用setRequestedOrientation()函数,其效果就是和在


    AndroidManifest中设置一样的,当前方向和设置的方向不一致的时候,会重启activity,一致的话不会重启;


    setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);//横屏设置

    setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);//竖屏设置

    setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);//默认设置

    注意点:


    不想activity被重启,可以在AndroidManifest中加上android:configChanges(orientation|screenSize这两个一定要加上)


     
    
                android:name=".MainActivity"

                android:screenOrientation="sensor"

                android:configChanges="keyboardHidden|orientation|screenSize">

    3、监听屏幕旋转变化


    重写onConfigurationChanged方法


        @Override

        public void onConfigurationChanged(Configuration newConfig) {

            super.onConfigurationChanged(newConfig);

            Log.d(TAG, "onConfigurationChanged");

        }

    这个方法将会在屏幕旋转变化时调用,可以在这里做出我们在屏幕变化时想要的操作,并且不会重启activity。但它只能一次旋转90度,如果一下子旋转180度,onConfigurationChanged函数不会被调用;


    4、自定义旋转监听设置


    想更加完美,更加完全的掌控监听屏幕旋转变化,就的自定义旋转监听


    (1)创建一个类继承OrientationEventListener


    (2)开启和关闭监听


    可以在 activity 中创建MyOrientationDetector 类的对象,注意,监听的开启的关闭,是由该类的父类的 enable() 和 disable() 方法实现的;


    因此,可以在activity的 onResume() 中调用MyOrientationDetector 对象的 enable方法,在 onPause() 中调用MyOrientationDetector 对象的 disable方法来完车功能;


    (3)监测指定的屏幕旋转角度


    MyOrientationDetector类的onOrientationChanged 参数orientation是一个从0~359的变量,如果只希望处理四个方向,加一个判断即可:


     OrientationEventListener mOrientationListener;

        @Override

        public void onCreate(Bundle savedInstanceState) {

            super.onCreate(savedInstanceState);

            setContentView(R.layout.main);

            mOrientationListener = new OrientationEventListener(this,

                SensorManager.SENSOR_DELAY_NORMAL) {

                @Override

                public void onOrientationChanged(int orientation) {

                if (orientation == OrientationEventListener.ORIENTATION_UNKNOWN) {

        return;  //手机平放时,检测不到有效的角度

    }

    //只检测是否有四个角度的改变

    if (orientation > 350 || orientation < 10) { //0度

        orientation = 0;

    } else if (orientation > 80 && orientation < 100) { //90度

        orientation = 90;

    } else if (orientation > 170 && orientation < 190) { //180度

        orientation = 180;

    } else if (orientation > 260 && orientation < 280) { //270度

        orientation = 270;

    } else {

        return;

    }

    Log.v(DEBUG_TAG,"Orientation changed to " + orientation);

                }

            };

           if (mOrientationListener.canDetectOrientation()) {

               Log.v(DEBUG_TAG, "Can detect orientation");

               mOrientationListener.enable();

           } else {

               Log.v(DEBUG_TAG, "Cannot detect orientation");

               mOrientationListener.disable();

           }

        }

        @Override

        protected void onDestroy() {

            super.onDestroy();

            mOrientationListener.disable();

        }

    总结


    快年底了,很多人都要找工作或者写毕业设计,有不懂就发私信给我,或许可以给你点帮助建议;


    我们一起努力进步;


    作者:Android开发编程
    链接:https://juejin.cn/post/7033665120962084901
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    如何从性能角度选择数组的遍历方式

    前言 本文讲述了JS常用的几种数组遍历方式以及性能分析对比。 如果这篇文章有帮助到你,❤️关注+点赞❤️鼓励一下作者,文章公众号首发,关注 前端南玖 第一时间获取最新的文章~ 数组的方法 JavaScript发展到现在已经提供了许多数组的方法,下面这张图涵盖...
    继续阅读 »

    前言


    本文讲述了JS常用的几种数组遍历方式以及性能分析对比。


    如果这篇文章有帮助到你,❤️关注+点赞❤️鼓励一下作者,文章公众号首发,关注 前端南玖 第一时间获取最新的文章~


    敖丙.png


    数组的方法


    JavaScript发展到现在已经提供了许多数组的方法,下面这张图涵盖了数组大部分的方法,这篇文章主要说一说数组的遍历方法,以及各自的性能,方法这么多,如何挑选性能最佳的方法对我们的开发有非常大的帮助。


    数组.png


    数组遍历的方法


    for


    标准的for循环语句,也是最传统的循环语句


    var arr = [1,2,3,4,5]
    for(var i=0;i<arr.length;i++){
    console.log(arr[i])
    }

    最简单的一种遍历方式,也是使用频率最高的,性能较好,但还能优化


    优化版for循环语句


    var arr = [1,2,3,4,5]
    for(var i=0,len=arr.length;i<len;i++){
    console.log(arr[i])
    }

    使用临时变量,将长度缓存起来,避免重复获取数组长度,尤其是当数组长度较大时优化效果才会更加明显。


    这种方法基本上是所有循环遍历方法中性能最高的一种


    forEach


    普通forEach


    对数组中的每一元素运行给定的函数,没有返回值,常用来遍历元素


    var arr5 = [10,20,30]
    var result5 = arr5.forEach((item,index,arr)=>{
    console.log(item)
    })
    console.log(result5)
    /*
    10
    20
    30
    undefined 该方法没有返回值
    */

    数组自带的foreach循环,使用频率较高,实际上性能比普通for循环弱


    原型forEach


    由于foreach是Array型自带的,对于一些非这种类型的,无法直接使用(如NodeList),所以才有了这个变种,使用这个变种可以让类似的数组拥有foreach功能。


    const nodes = document.querySelectorAll('div')
    Array.prototype.forEach.call(nodes,(item,index,arr)=>{
    console.log(item)
    })

    实际性能要比普通foreach弱


    for...in


    任意顺序遍历一个对象的除Symbol以外的可枚举属性,包括继承的可枚举属性。


    一般常用来遍历对象,包括非整数类型的名称和继承的那些原型链上面的属性也能被遍历。像 Array和 Object使用内置构造函数所创建的对象都会继承自Object.prototype和String.prototype的不可枚举属性就不能遍历了.


    var arr = [1,2,3,4,5]
    for(var i in arr){
    console.log(i,arr[i])
    } //这里的i是对象属性,也就是数组的下标
    /**
    0 1
    1 2
    2 3
    3 4
    4 5 **/

    大部分人都喜欢用这个方法,但它的性能却不怎么好


    for...of(不能遍历对象)



    在可迭代对象(具有 iterator 接口)(Array,Map,Set,String,arguments)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句,不能遍历对象



    let arr=["前端","南玖","ssss"];
    for (let item of arr){
    console.log(item)
    }
    //前端 南玖 ssss

    //遍历对象
    let person={name:"南玖",age:18,city:"上海"}
    for (let item of person){
    console.log(item)
    }
    // 我们发现它是不可以的 我们可以搭配Object.keys使用
    for(let item of Object.keys(person)){
    console.log(person[item])
    }
    // 南玖 18 上海

    这种方式是es6里面用到的,性能要好于forin,但仍然比不上普通for循环


    map



    map: 只能遍历数组,不能中断,返回值是修改后的数组。



    let arr=[1,2,3];
    const res = arr.map(item=>{
    return item+1
    })
    console.log(res) //[2,3,4]
    console.log(arr) // [1,2,3]

    every


    对数组中的每一运行给定的函数,如果该函数对每一项都返回true,则该函数返回true


    var arr = [10,30,25,64,18,3,9]
    var result = arr.every((item,index,arr)=>{
    return item>3
    })
    console.log(result) //false

    some


    对数组中的每一运行给定的函数,如果该函数有一项返回true,就返回true,所有项返回false才返回false


    var arr2 = [10,20,32,45,36,94,75]
    var result2 = arr2.some((item,index,arr)=>{
    return item<10
    })
    console.log(result2) //false

    reduce


    reduce()方法对数组中的每个元素执行一个由你提供的reducer函数(升序执行),将其结果汇总为单个返回值


    const array = [1,2,3,4]
    const reducer = (accumulator, currentValue) => accumulator + currentValue;

    // 1 + 2 + 3 + 4
    console.log(array1.reduce(reducer));

    filter


    对数组中的每一运行给定的函数,会返回满足该函数的项组成的数组


    // filter  返回满足要求的数组项组成的新数组
    var arr3 = [3,6,7,12,20,64,35]
    var result3 = arr3.filter((item,index,arr)=>{
    return item > 3
    })
    console.log(result3) //[6,7,12,20,64,35]

    性能测试


    工具测试


    使用工具测试性能分析结果如下图所示


    性能测试1.png


    手动测试


    我们也可以自己用代码测试:


    //测试函数
    function clecTime(fn,fnName){
    const start = new Date().getTime()
    if(fn) fn()
    const end = new Date().getTime()
    console.log(`${fnName}执行耗时:${end-start}ms`)
    }

    function forfn(){
    let a = []
    for(var i=0;i<arr.length;i++){
    // console.log(i)
    a.push(arr[i])
    }
    }
    clecTime(forfn, 'for') //for执行耗时:106ms

    function forlenfn(){
    let a = []
    for(var i=0,len=arr.length;i<len;i++){
    a.push(arr[i])
    }
    }
    clecTime(forlenfn, 'for len') //for len执行耗时:95ms

    function forEachfn(){
    let a = []
    arr.forEach(item=>{
    a.push[item]
    })
    }
    clecTime(forEachfn, 'forEach') //forEach执行耗时:201ms

    function forinfn(){
    let a = []
    for(var i in arr){
    a.push(arr[i])
    }
    }
    clecTime(forinfn, 'forin') //forin执行耗时:2584ms (离谱)

    function foroffn(){
    let a = []
    for(var i of arr){
    a.push(i)
    }
    }
    clecTime(foroffn, 'forof') //forof执行耗时:221ms

    // ...其余可自行测试

    结果分析


    经过工具与手动测试发现,结果基本一致,数组遍历各个方法的速度:传统的for循环最快,for-in最慢



    for-len > for > for-of > forEach > map > for-in



    javascript原生遍历方法的建议用法:



    • for循环遍历数组

    • for...in遍历对象

    • for...of遍历类数组对象(ES6)

    • Object.keys()获取对象属性名的集合


    为何for… in会慢?


    因为for … in语法是第一个能够迭代对象键的JavaScript语句,循环对象键({})与在数组([])上进行循环不同,引擎会执行一些额外的工作来跟踪已经迭代的属性。因此不建议使用for...in来遍历数组



    作者:南玖
    链接:https://juejin.cn/post/7033578966887694373

    收起阅读 »

    async/await 优雅永不过时

    引言 async/await是非常棒的语法糖,可以说他是解决异步问题的最终解决方案。从字面意思来理解。async 是异步的意思,而 await 是 等待 ,所以理解 async用于申明一个function是异步的,而 await 用于等待一个异步方法执行完成...
    继续阅读 »

    引言



    async/await是非常棒的语法糖,可以说他是解决异步问题的最终解决方案。从字面意思来理解。async 是异步的意思,而 await 是 等待 ,所以理解 async用于申明一个function是异步的,而 await 用于等待一个异步方法执行完成。



    src=http___pic.962.net_up_2019-12_15767448543514326.png&refer=http___pic.962.jpeg


    async作用



    async声明function是一个异步函数,返回一个promise对象,可以使用 then 方法添加回调函数。async函数内部return语句返回的值,会成为then方法回调函数的参数。



    async function test() {
    return 'test';
    }
    console.log(test); // [AsyncFunction: test] async函数是[`AsyncFunction`]构造函数的实例
    console.log(test()); // Promise { 'test' }

    // async返回的是一个promise对象
    test().then(res=>{
    console.log(res); // test
    })

    // 如果async函数没有返回值 async函数返回一个undefined的promise对象
    async function fn() {
    console.log('没有返回');
    }
    console.log(fn()); // Promise { undefined }

    // 可以看到async函数返回值和Promise.resolve()一样,将返回值包装成promise对象,如果没有返回值就返回undefined的promise对象

    await



    await 操作符只能在异步函数 async function 内部使用。如果一个 Promise 被传递给一个 await 操作符,await 将等待 Promise 正常处理完成并返回其处理结果,也就是说它会阻塞后面的代码,等待 Promise 对象结果。如果等待的不是 Promise 对象,则返回该值本身。



    async function test() {
    return new Promise((resolve)=>{
    setTimeout(() => {
    resolve('test 1000');
    }, 1000);
    })
    }
    function fn() {
    return 'fn';
    }

    async function next() {
    let res0 = await fn(),
    res1 = await test(),
    res2 = await fn();
    console.log(res0);
    console.log(res1);
    console.log(res2);
    }
    next(); // 1s 后才打印出结果 为什么呢 就是因为 res1在等待promise的结果 阻塞了后面代码。

    错误处理



    如果await后面的异步操作出错,那么等同于async函数返回的 Promise 对象被reject



    async function test() {
    await Promise.reject('错误了')
    };

    test().then(res=>{
    console.log('success',res);
    },err=>{
    console.log('err ',err);
    })
    // err 错误了


    防止出错的方法,也是将其放在try...catch代码块之中。



    async function test() {
    try {
    await new Promise(function (resolve, reject) {
    throw new Error('错误了');
    });
    } catch(e) {
    console.log('err', e)
    }
    return await('成功了');
    }


    多个await命令后面的异步操作,如果不存在继发关系(即互不依赖),最好让它们同时触发。



    let foo = await getFoo();
    let bar = await getBar();
    // 上面这样写法 getFoo完成以后,才会执行getBar

    // 同时触发写法 ↓

    // 写法一
    let [foo, bar] = await Promise.all([getFoo(), getBar()]);

    // 写法二
    let fooPromise = getFoo();
    let barPromise = getBar();
    let foo = await fooPromise;
    let bar = await barPromise;

    async/await优点



    async/await的优势在于处理由多个Promise组成的 then 链,在之前的Promise文章中提过用then处理回调地狱的问题,async/await相当于对promise的进一步优化。
    假设一个业务,分多个步骤,且每个步骤都是异步的,而且依赖上个步骤的执行结果。



    // 假设表单提交前要通过俩个校验接口

    async function check(ms) { // 模仿异步
    return new Promise((resolve)=>{
    setTimeout(() => {
    resolve(`check ${ms}`);
    }, ms);
    })
    }
    function check1() {
    console.log('check1');
    return check(1000);
    }
    function check2() {
    console.log('check2');
    return check(2000);
    }

    // -------------promise------------
    function submit() {
    console.log('submit');
    // 经过俩个校验 多级关联 promise传值嵌套较深
    check1().then(res1=>{
    check2(res1).then(res2=>{
    /*
    * 提交请求
    */
    })
    })
    }
    submit();

    // -------------async/await-----------
    async function asyncAwaitSubmit() {
    let res1 = await check1(),
    res2 = await check2(res1);
    console.log(res1, res2);
    /*
    * 提交请求
    */
    }



    原理



    async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。



    async function fn(args) {
    // ...
    }

    // 等同于

    function fn(args) {
    return spawn(function* () {
    // ...
    });
    }

    /*
    * Generator 函数就是一个封装的异步任务,或者说是异步任务的容器。
    * 异步操作需要暂停的地方,都用 yield 语句注明
    * 调用 Generator 函数,返回的是指针对象(这是它和普通函数的不同之处),。调用指针对象的 next 方法,会移动内部指针。
    * next 方法的作用是分阶段执行 Generator 函数。每次调用 next 方法,会返回一个对象,表示当前阶段的信息( value 属性和 done 属性)。value 属性是 yield 语句后面表达式的值,表示当前阶段的值;done 属性是一个布尔值,表示 Generator 函数是否执行完毕,即是否还有下一个阶段。
    */

    // 了解generator的用法
    function* Generator() {
    yield '1';
    yield Promise.resolve(2);
    return 'ending';
    }

    var gen = Generator(); // 返回指针对象 Object [Generator] {}

    let res1 = gen.next();
    console.log(res1); // 返回当前阶段的值 { value: '1', done: false }

    let res2 = gen.next();
    console.log(res2); // 返回当前阶段的值 { value: Promise { 2 }, done: false }

    res2.value.then(res=>{
    console.log(res); // 2
    })

    let res3 = gen.next();
    console.log(res3); // { value: 'ending', done: true }

    let res4 = gen.next();
    console.log(res4); // { value: undefined, done: true }



    Generator实现async函数



    // 接受一个Generator函数作为参数
    function spawn(genF) {
    // 返回一个函数
    return function() {
    // 生成指针对象
    const gen = genF.apply(this, arguments);
    // 返回一个promise
    return new Promise((resolve, reject) => {
    // key有next和throw两种取值,分别对应了gen的next和throw方法
    // arg参数则是用来把promise resolve出来的值交给下一个yield
    function step(key, arg) {
    let result;

    // 监控到错误 就把promise给reject掉 外部通过.catch可以获取到错误
    try {
    result = gen[key](arg)
    } catch (error) {
    return reject(error)
    }

    // gen.next() 返回 { value, done } 的结构
    const { value, done } = result;

    if (done) {
    // 如果已经完成了 就直接resolve这个promise
    return resolve(value)
    } else {
    // 除了最后结束的时候外,每次调用gen.next()
    return Promise.resolve(
    // 这个value对应的是yield后面的promise
    value
    ).then((val)=>step("next", val),(err) =>step("throw", err))
    }
    }
    step("next")
    })
    }
    }


    测试



    function fn(nums) {
    return new Promise(resolve => {
    setTimeout(() => {
    resolve(nums)
    }, 1000)
    })
    }
    // async 函数
    async function testAsync() {
    let res1 = await fn(1);
    console.log(res1); // 1
    let res2 = await fn(2);
    console.log(res2); // 2
    return res2;
    }
    let _res = testAsync();
    console.log('testAsync-res',_res); // Promise
    _res.then(v=>console.log('testAsync-res',v)) // 2

    // Generator函数
    function* gen() {
    let res1 = yield fn(3);
    console.log(res1); // 3
    let res2 = yield fn(4);
    console.log(res2); // 4
    // let res3 = yield Promise.reject(5);
    // console.log(res3);
    return res2;
    }

    let _res2 = spawn(gen)();
    console.log('gen-res',_res2); // Promise

    _res2
    .then(v=>console.log('gen-res',v)) // 4
    .catch(err=>{console.log(err)}) // res3 执行会抛出异常



    总结



    async/await语法糖可以让异步代码变得更清晰,可读性更高,所以快快卷起来吧。Generator有兴趣的可以了解一下。


    作者:小撕夜
    链接:https://juejin.cn/post/7033647059378896903

    收起阅读 »

    当老婆又让我下载一个腾讯视频时

    我们结婚了! 是的,这次不是女朋友啦,是老婆了! 时隔将近一个月,老婆又让我给她下载腾讯视频,如果按照上次探索的内容来下载的话,倒是可以一步步下载,合并,不过很麻烦,程序员不都是为了解决麻烦的吗,这么麻烦的步骤,有没有简单点呢。有!当然有,有很多简单的工具,...
    继续阅读 »

    我们结婚了!


    是的,这次不是女朋友啦,是老婆了!


    WechatIMG58.jpeg


    时隔将近一个月,老婆又让我给她下载腾讯视频,如果按照上次探索的内容来下载的话,倒是可以一步步下载,合并,不过很麻烦,程序员不都是为了解决麻烦的吗,这么麻烦的步骤,有没有简单点呢。有!当然有,有很多简单的工具,上一期很多朋友给我推荐了各种工具,这里我没有一一查看,我可以列举出来,有需要的同学可以尝试看看,不想尝试的也可以看看我下面为了偷懒准备的方法。


    心路历程


    最初,我是想着把我之前的步骤,用无头浏览器加载一遍,然后用代码去下载ts片段,然后在机器上用ffmpeg进行合并,但是仿佛还是有些许麻烦,然后我就去npm搜了一下关键词:m3u8tomp4


    image.png


    m3u8-to-mp4


    于是我点击了第一个包:m3u8-to-mp4


    image.png


    纳尼?这个包就一个版本,用了3年,而且周下载量还不少


    image.png


    于是我想着这个包要么就是很牛逼,一次性解决了m3u8转mp4的问题,一劳永逸,所以3年没更新过了,要么就是作者忘记了自己还有这个包


    于是我就用了这个3年没人维护没人更新的包。


    用法也很简单,就copy example 就好了。代码如下:



    var m3u8ToMp4 = require("m3u8-to-mp4");
    var converter = new m3u8ToMp4();
    (async function() {
    var url = "https://apd-666945ea97106754c57813479384d30c.v.smtcdns.com/omts.tc.qq.com/AofRtrergNwkAhpHs4RrxH2_9DWLWSG8xjDMZDQoFGyY/uwMROfz2r55kIaQXGdGnC2deOm68BrdPrRewQlOzrMAbixNO/svp_50001/cKAgRbCb6Re4BpHkI-IlK_KN1VJ8gQVK2sZtkHEY3vQUIlxVz7AtWmVJRifZrrPfozBS0va-SSJFhQhOFSKVNmqVi165fCQJoPl8V5QZBcGZBDpSIfrpCImJKryoZOdR5C0oGYkzIW77I4his7UkPY9Iwmf1QWjaHwNV2hpKv3aD9ysL_-YByA/szg_9276_50001_0bc3uuaa2aaafmaff4e3ijqvdjodbwsqadka.f304110.ts.m3u8?ver=4"
    await converter
    .setInputFile(url)
    .setOutputFile("dummy.mp4")
    .start();
    console.log("File converted");
    })();

    视频地址是 v.qq.com/x/page/v331…


    然后视频就转换成功了,哇哦!


    so easy ! so beautiful!


    原理


    带着好奇,我想看下这个包是如何进行转换的


    于是我点进去m3u8-to-mp4这个包文件


    包文件内容如下


    image.png


    只有一个文件?


    然后我打开了index.js ,只有64行😂


    全部代码如下


    /**
    * @description M3U8 to MP4 Converter
    * @author Furkan Inanc
    * @version 1.0.0
    */

    let ffmpeg = require("fluent-ffmpeg");

    /**
    * A class to convert M3U8 to MP4
    * @class
    */
    class m3u8ToMp4Converter {
    /**
    * Sets the input file
    * @param {String} filename M3U8 file path. You can use remote URL
    * @returns {Function}
    */
    setInputFile(filename) {
    if (!filename) throw new Error("You must specify the M3U8 file address");
    this.M3U8_FILE = filename;

    return this;
    }

    /**
    * Sets the output file
    * @param {String} filename Output file path. Has to be local :)
    * @returns {Function}
    */
    setOutputFile(filename) {
    if (!filename) throw new Error("You must specify the file path and name");
    this.OUTPUT_FILE = filename;

    return this;
    }

    /**
    * Starts the process
    */
    start() {
    return new Promise((resolve, reject) => {
    if (!this.M3U8_FILE || !this.OUTPUT_FILE) {
    reject(new Error("You must specify the input and the output files"));
    return;
    }

    ffmpeg(this.M3U8_FILE)
    .on("error", error => {
    reject(new Error(error));
    })
    .on("end", () => {
    resolve();
    })
    .outputOptions("-c copy")
    .outputOptions("-bsf:a aac_adtstoasc")
    .output(this.OUTPUT_FILE)
    .run();
    });
    }
    }

    module.exports = m3u8ToMp4Converter;


    大致看了下这个包做的内容,就是检测并设置了输入链接,和输出文件名,然后调用了fluent-ffmpeg这个库


    ???


    站在巨人的肩膀上吗,自己就包了一层😂


    接着看fluent-ffmpeg这个包,是如何实现转换的


    image.png


    然后我们在这个包文件夹下面搜索.run方法,用来定位到具体执行的地方


    image.png


    凭借多年的cv经验,感觉应该是processor.js这个文件里的,然后我们打开这个文件,定位到该方法处


    image.png


    往下看代码,我注意到了这段代码


    image.png


    因为都是基于ffmpeg这个大爹来做的工具,所以最底层也都是去调用ffmpeg的command


    image.png


    这几个if判断都是对结果进行捕获异常,那么我们在这个核心代码的地方打个端点看下


    image.png


    貌似是调用了几个命令行参数


    于是我就有了一个大胆的想法!


    image.png


    是的,我手动在终端将这个命令拼接起来,用我的本地命令去跑应该也没问题的吧,于是我尝试了一下


    image.png


    没想到还成功了,其实成功是必然的,因为都是借助来ffmpeg这个包,只不过我是手动去操作,框架是代码去拼接这个命令而已


    剩余的时间里,我看了看fluent-ffmpeg的其他代码,它做的东西比较多,比如去本查找ffmpeg的绝对路径啊,对ffmpeg的结果进行捕获异常信息等...



    作者:小松同学哦
    链接:https://juejin.cn/post/7033652317958176799

    收起阅读 »

    为什么 MySQL 不推荐使用 join?

     1. 对于 mysql,不推荐使用子查询和 join 是因为本身 join 的效率就是硬伤,一旦数据量很大效率就很难保证,强烈推荐分别根据索引 单表取数据,然后在程序里面做 join,merge 数据。   2. 子查询就更别用了,效率太差,执行子查询时,M...
    继续阅读 »

     1. 对于 mysql,不推荐使用子查询和 join 是因为本身 join 的效率就是硬伤,一旦数据量很大效率就很难保证,强烈推荐分别根据索引


    单表取数据,然后在程序里面做 join,merge 数据。


      2. 子查询就更别用了,效率太差,执行子查询时,MYSQL 需要创建临时表,查询完毕后再删除这些临时表,所以,子查询的速度会


    受到一定的影响,这里多了一个创建和销毁临时表的过程。


      3. 如果是 JOIN 的话,它是走嵌套查询的。小表驱动大表,且通过索引字段进行关联。如果表记录比较少的话,还是 OK 的。大的话


    业务逻辑中可以控制处理。


      4. 数据库是最底层的,瓶颈往往是数据库。建议数据库只是作为数据 store 的工具,而不要添加业务上去。


      让缓存的效率更高。许多应用程序可以方便地缓存单表查询对应的结果对象。如果关联中的某个表发生了变化,那么就无法使用查


    询缓存了,而拆分后,如果某个表很少改变,那么基于该表的查询就可以重复利用查询缓存结果了。


      将查询分解后,执行单个查询可以减少锁的竞争。


      在应用层做关联,可以更容易对数据库进行拆分,更容易做到高性能和可扩展。


      查询本身效率也可能会有所提升。查询 id 集的时候,使用 IN()代替关联查询,可以让 MySQL 按照 ID 顺序进行查询,这可能比随机的关联要更高效。


      可以减少冗余记录的查询。在应用层做关联查询,意味着对于某条记录应用只需要查询一次,而在数据库中做关联查询,则可能需


    要重复地访问一部分数据。从这点看,这样的重构还可能会减少网络和内存的消艳。


      更进一步,这样做相当于在应用中实现了哈希关联,而不是使用 MySQL 的嵌套循环关联。某些场景哈希关联的效率要高很多。


    当应用能够方便地缓存单个查询的结果的时候


    当可以将数据分布到不同的 MySQL 服务器上的时候


    当能够使用 IN()的方式代替关联查询的时候


    并发场景多,DB 查询频繁,需要分库分表


    1.DB 承担的业务压力大,能减少负担就减少。当表处于百万级别后,join 导致性能下降;


    2. 分布式的分库分表。这种时候是不建议跨库 join 的。目前 mysql 的分布式中间件,跨库 join 表现不良。


    3. 修改表的 schema,单表查询的修改比较容易,join 写的 sql 语句要修改,不容易发现,成本比较大,当系统比较大时,不好维护。


      在业务层,单表查询出数据后,作为条件给下一个单表查询。也就是子查询。 会担心子查询出来的结果集太多。mysql 对 in 的数量没有限制,但是


      mysql 限制整条 sql 语句的大小。通过调整参数 max_allowed_packet ,可以修改一条 sql 的最大值。建议在业务上做好处理,限制一次查询出来的结果集是能接受的。


      关联查询的好处时候可以做分页,可以用副表的字段做查询条件,在查询的时候,将副表匹配到的字段作为结果集,用主表去 in 它,但是问题来了,如果匹配到的数据量太大就不行了,也会导致返回的分页记录跟实际的不一样,解决的方法可以交给前端,一次性查询,让前端分批显示就可以了,这种解决方案的前提是数据量不太,因为 sql 本身长度有限。


    作者:Java小咖秀
    链接:https://juejin.cn/post/6948227472232022053
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    我的Android开发之旅(一):BaseActivity的浅入之简单封装 Toolbar

    为什么要写BaseAcivity 我们都知道在做Android应用开发的时候都需要创建一个Activity,但很多时候我们的程序有多个界面并且每个界面都有相似的内容(例如:Toolbar、DrawerLayout)和后台的操作有共同的方法,这个时候我们写一个B...
    继续阅读 »

    为什么要写BaseAcivity


    我们都知道在做Android应用开发的时候都需要创建一个Activity,但很多时候我们的程序有多个界面并且每个界面都有相似的内容(例如:Toolbar、DrawerLayout)和后台的操作有共同的方法,这个时候我们写一个BaseActivity作为每一个Activity的基类,统一管理程序中的每个Activity。


    一行代码实现 Toolbar 效果


    activity_main.xml 的代码


    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:gravity="center"
    android:background="@android:color/holo_blue_light">

    <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="我是MainActivity的界面"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

    </LinearLayout>

    MainAcitvity.java 的代码


    public class MainActivity extends BaseActivity {

    @Override
    protected int getContentView() {
    return R.layout.activity_main;
    }
    }

    在这里插入图片描述

    在上面的 activity_main.xml 中可以看出,父布局设置了背景颜色和里面只有一个TextView,并没图中的Toolbar。那到底是为什么呢?其实细心观察的小伙伴们会发现,怎么MainActivity中的代码和平常不一样呢?onCreate()方法呢?别急,我们重头开始!


    “少啰嗦,先看东西”




    • 创建 BaseActivity


      在项目创建后,我们可以看到AndroidStudio自动帮我们生成了MainActivity.java和activity_main.xml文件,然后我们再创建一个新的Activity,命名为BaseActivity。

      在这里插入图片描述




    • 修改 activity_base.xml 文件


      接着打开activity_base.xml文件,把父布局的ConstraintLayout换成垂直的LinearLayout(其实也可以不换的,主要是我喜欢用LinearLayout),并在里面添加两个元素Toolbar和FrameLayout。



      注意:由于Toolbar代替 ActionBar,所以先把 ActionBar 去掉,我们通过设置 Application 的 theme 来隐藏,这样项目中所有的界面的 ActionBar 就都隐藏了。





    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".BaseActivity"
    android:orientation="vertical">

    <androidx.appcompat.widget.Toolbar
    android:id="@+id/toolbar"
    android:layout_width="match_parent"
    android:layout_height="?attr/actionBarSize"
    android:background="?attr/colorPrimary"
    android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
    app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>

    <FrameLayout
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    </FrameLayout>

    </LinearLayout>


    • 修改 BaseActivity.java 文件


      接下来打开 BaseActivity.java 文件,让 BaseActivity 继承 AppCompatActivity ,修改代码如下



      注意:protected abstract int getContentView(); 是一个抽象方法,所以我们要将 BaseActivity 修改成抽象类。为什么要修改成抽象类呢?原因很简单,因为一个类里是不允许有一个抽象方法的,如要有抽象方法,那这个类就必须是抽象类。那可能你又会问,为什么要用抽象方法呢?(你是十万个为什么吗?哪来的那么多为什么)因为我们想让其他 Activity 的界面显示到 BaseActivity 中,那这个方法是必须要实现的,如果设置成普通的方法的话,我们很有可能在写代码的时候忘记了调用了这个方法,导致界面不显示。所以我们得用抽象方法,这样每个 Activity 继承这个 BaseActivity 的时候就必须覆写 getContentView() 这个方法。





    public abstract class BaseActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_base);
    initView();
    }

    private void initView() {
    // 绑定控件
    Toolbar toolbar = findViewById(R.id.toolbar);
    FrameLayout container = findViewById(R.id.container);
    // 初始化设置Toolbar
    toolbar.setTitle("我是BaseActivity的Toolbar");
    setSupportActionBar(toolbar);
    // 将继承了BaseActivity的布局文件解析到 container 中,这样 BaseActivity 就能显示 MainActivity 的布局文件了
    LayoutInflater.from(this).inflate(getContentView(), container);
    }

    /**
    * 获取要显示内容的布局文件的资源id
    *
    * @return 显示的内容界面的资源id
    */
    protected abstract int getContentView();

    }

    • 修改 activity_main.xml 文件
      打开 activity_main.xml 文件,然后我们将父布局的背景颜色修改一下,方便我们辨别到底是 MainActivity 的布局文件还是 BaseActivity 的布局文件。再添加添加一个 TextView ,原因也是和修改背景颜色是一样的。


    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:gravity="center"
    android:background="@android:color/holo_blue_light">

    <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="我是MainActivity的界面"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

    </LinearLayout>

    • 修改 MainActivity.java 文件
      让 MainActivity 继承 BaseActivity 并覆写 getContentView() 方法,然后删除onCreate()方法。通过 getContentView() 方法返回当前的布局资源ID给 BaseActivity,让 BaseActivity 加载布局文件。


    public class MainActivity extends BaseActivity {

    @Override
    protected int getContentView() {
    return R.layout.activity_main;
    }
    }


    • 运行项目


      现在你运行一下项目,我们并没有在 MainActivity 的布局中添加 ToolBar,但是运行出来的效果是 Toolbar 已经存在了。

      在这里插入图片描述

      现在就能做到用一行代码实现 Toolbar 的效果。那你现在可能就会有疑问,如果我像对 Toolbar 修改标题和添加按钮呢?其实也简单,我们继续往下看。




    • 修改标题
      我们在 BaseActivity 中再添加一个抽象方法,并在初始化 Toolbar 那一处调用我们写的这个抽象方法。




    public abstract class BaseActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_base);
    initView();
    }

    private void initView() {
    // 绑定控件
    Toolbar toolbar = findViewById(R.id.toolbar);
    FrameLayout container = findViewById(R.id.container);
    // 初始化设置Toolbar
    toolbar.setTitle(setTitle());
    setSupportActionBar(toolbar);
    // 将继承了BaseActivity的布局文件解析到 container 中,这样 BaseActivity 就能显示 MainActivity 的布局文件了
    LayoutInflater.from(this).inflate(getContentView(), container);
    }

    /**
    * 获取要显示内容的布局文件的资源id
    *
    * @return 显示的内容界面的资源id
    */
    protected abstract int getContentView();

    /**
    * 设置标题
    *
    * @return 要显示的标题名称
    */
    protected abstract String setTitle();

    }

    • 修改 MainActivity.java
      我们还是像刚才一样覆写 setTitle() 方法,并在返回值输入我们想要显示的标题


    public class MainActivity extends BaseActivity {

    @Override
    protected int getContentView() {
    return R.layout.activity_main;
    }

    @Override
    protected String setTitle() {
    return "我是MainActivity的标题";
    }
    }

    这个时候再运行以下你的程序就会出现你设置的标题了。

    在这里插入图片描述
    那么现在你可能又会问了,如果我想对 Toolbar 添加一个返回按钮,并能对他进行操作应该怎么办?(我不想写了,你也别问了!)其实很简单,在 BaseActivity 里自定义一个接口,在子类中设置这个接口的实例就行。




    • 显示返回按钮
      我们先给 Toolbar 显示返回按钮,通过 getSupportActionBar() 得到 ActionBar 的实例,再调用 ActionBar 的 setDisplayHomeAsUpEnabled(true) 方法让返回按钮显示出来。



      Toolbar 最左侧的按钮是叫做HomeAsUp,默认是隐藏的,并且它的图标是一个返回箭头。还有一种 setNavigationcon() 方法也能设置图标,具体可以查找 Toolbar 的文档说明





    private void initView() {
    // 绑定控件
    Toolbar toolbar = findViewById(R.id.toolbar);
    FrameLayout container = findViewById(R.id.container);
    // 初始化设置Toolbar
    toolbar.setTitle(setTitle());
    setSupportActionBar(toolbar);
    // 将继承了BaseActivity的布局文件解析到 container 中,这样 BaseActivity 就能显示 MainActivity 的布局文件了
    LayoutInflater.from(this).inflate(getContentView(), container);
    // 显示返回按钮
    ActionBar actionBar = getSupportActionBar();
    if (actionBar != null) {
    actionBar.setDisplayHomeAsUpEnabled(true);
    }
    // 初始化
    init();
    }


    • 自己定义一个接口并声明
      这里我就用截图显示代码片段

      在这里插入图片描述




    • 设置监听事件
      打开 MainActivity.java 文件,覆写 init() 方法,并调用父类的 setBackOnClickListener() 方法。



      这里我用了 lambda 表达式,这是 java 8 才支持的,默认项目是不支持的,你在 build.gradle 中需要声明一下。





    	@Override
    protected void init() {
    setBackOnClickListener(() ->
    Toast.makeText(this, "点击了一下返回按钮", Toast.LENGTH_SHORT).show()
    );
    }

    • 运行app
      在这里插入图片描述


    最后


    相信你对 BaseActivity 有了一些简单的了解了,具体如何使用还是得看你的项目,不是项目里就一定要写 BaseActivity 和所有 Activity 都要继承 BaseActivity ,我只是将我所理解的 BaseActivity 和大家分享一下。可能你看完了这一篇文章发现还是没能理解,在这里我想说声抱歉,可能有些地方讲的不够通俗易懂或是讲解有误,还请您多多指教,我会虚心接受并及时改正(就算是讲错了我也不会改)。



    Demo的Github地址:
    github.com/lmx0206/Bas…
    Demo的Gitee地址:
    gitee.com/Leungmx/Bas…


    作者:梁大侠
    链接:https://juejin.cn/post/6844904146659704846
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

    Android 包大小优化实践

    android减少包大小是非常必要的,在性能,转换率等等都有益处,而常用的包大小优化Google已经给出了一些方案,再加上市面上的一些美团方案,微信方案、抖音方案等等,下面就说一下我们在包大小优化做的努力。 1、使用AAB模式 google play现在强制所...
    继续阅读 »

    android减少包大小是非常必要的,在性能,转换率等等都有益处,而常用的包大小优化Google已经给出了一些方案,再加上市面上的一些美团方案,微信方案、抖音方案等等,下面就说一下我们在包大小优化做的努力。


    1、使用AAB模式


    google play现在强制所有上传的应用都使用aab,Google Play 会使用您的 AAB 针对每种设备配置生成并提供经过优化的 APK,因此只会下载特定设备所需的代码和资源来运行您的应用。假如一个AAB是90MB在google play上下载耗费的流量可能也就50MB,但是这种方案对性能上没有任何的影响只是减少了下载流量可能会增加一些转换率。具体文档可以参考官方文档。这里有必要说一下AAB还有更多又去的玩法比如使用AAB实现插件化(对模块拆分还是非常有帮助的),对不同地区实现不同的业务然后使用google play进行分发


    2、使用AGP配置来减少包大小(链接


    使用lint本地检测无用资源或者开启shrinkResources


    使用lint本地检查无用资源


    1、点击AS上的Analyze菜单按钮,选择Run Inspection by Name 如下图


    image.png
    2、会出现一个弹窗, 输入unused resources


    image.png
    3、会弹出“inspaction scope”选择窗口,选择检查的范围,一般选择整个项目或模块。“inspaction scope”窗口下面还可以设置文件过滤,选择好后点ok就开始检查了


    image.png
    4、下面的输出栏会输出没有用的资源文件。


    image.png
    5、删除无用资源


    开启shrinkResources


    android {
        ...
        buildTypes {
            release {
                shrinkResources true
                minifyEnabled true
                proguardFiles
                    getDefaultProguardFile('proguard-android-optimize.txt'),
                    'proguard-rules.pro'
            }
        }
    }
    复制代码

    此配置必须和代码压缩一起使用才有效果,如果说要保留某些资源,假如插件化里面宿主里面放了某个资源需要给很多个插件使用,这个时候就需要保留此资源那么就需要做如下配置:
    在项目的res目录下新建一个创建一个包含 <resources> 标记的 XML 文件,tools:keep 属性中指定每个要保留的资源,在 tools:discard 属性中指定每个要舍弃的资源。例如:


    <?xml version="1.0" encoding="utf-8"?>
    <resources xmlns:tools="http://schemas.android.com/tools"
        tools:keep="@layout/l_used*_c,@layout/l_used_a,@layout/l_used_b*"
        tools:discard="@layout/unused2" />
    复制代码

    缩减、混淆处理和代码优化功能


    在AGP 3.4.0以上版本R8是默认编辑器,把代码编译成android所需要的dex文件,但是代码的缩减、混淆和代码优化是默认关闭的,建议在release版本应用将此功能打开可以有效的减少包体积。上面提到的shrinkResources也是必须和此功能一起使用才有效,关于R8更多的配置可以详见官网文档
    如果要保留一些类就要在proguard-rules.pro 配置具体proguard 配置规则可以查看手册
    这里proguard特点说一下他是累加的,假如A moudle 依赖B moudle,那么到最后proguard 规则就是A moudle 的配置+B moudle的配置


    移除无用so库


    现在市面上的手机cpu指令基本上就两种了 armeabi-v7a 和 arm64-v8a,x86可以不考虑了很少有手机在用了,所以就可以通过gradle配置来只依赖v7a和v8a,例如:


    android {
    ...
    defaultConfig {
    ...
    ndk {
    // Specifies the ABI configurations of your native
    // libraries Gradle should build and package with your APK.
    abiFilters 'armeabi-v7a','arm64-v8a'
    }
    }

    }
    复制代码

    但是这样还是会很大,可以减少arm64-v8a指令集,不管你的手机cpu是v7还是v8都可以运行v7的so。
    如果说你的应用有多个变种,比如一个是上线到google play的aab一个是在国内上线的apk这样你可以使用不同变种依赖不同的指令集


    android {
    ...
    defaultConfig {
    ...
    flavorDimensions "market"
    productFlavors {

    //上google play市场
    gp {
    dimension "market"
    ndk.abiFilters "armeabi-v7a", "arm64-v8a"
    }

    //中国版本
    cn {
    dimension "market"
    ndk.abiFilters "armeabi-v7a"}
    }
    }
    复制代码

    3、美团进阶方案


    美团的这种方案主要是提到了zip压缩。dex优化,R Field的优化,我主要是用了R Field的优化。主要说下这个R field的优化这个点。这篇文章写的比较早用的是java代码插桩的方式进行处理的,现在其实AGP就可以完成一样的处理。
    先说下原理为什么R文件会导致包大小变大,假如你的项目结构如下:


    image.png


    lib1_R= lib1_R


    lib2_R= lib2_R


    lib3_R= lib3_R


    biz1_R = lib1_R + lib2_R + lib3_R + (自己的R)


    biz2_R = lib2_R + lib3_R + (自己的R)


    app_R = lib1_R + lib2_R + lib3_R + biz1_R + biz2_R + (自己的R)


    app_R因为是final的所以如果开启java优化也就是混淆会被shrink掉,但是其他moudle的R不是final而且引用也不是直接使用id的值来引用的:


    这是app moudle 下的MainActivity中setcontent对应的字节码:


    WeChata234b76e355e3899a7be1119de8a8880.png


    这是子moudle Activity中同样setContent对应的字节码:


    WeChat71e04d6bc8f6153355d695db68318246.png


    发现什么不同了吗?就是子moudle中是R的变量引用而非常量,在打包过程中aapt会将这个变量统一赋值来防止id冲突,而如果你的项目特别复杂子moudle特别多的话那么各个R类的就会特别大,但是这部分是必须的吗?好像有方法来解决


    R类内联解决方法一:


    就好像美团方案里面这种方法利用插桩在aapt分配了id后将对应的R变量的引用修改成对应的id值这样就可以把原有的R类删除掉(这个方案中的插桩插件是自己写的其实市面上有很多插桩三方库,字节的bytex,滴滴的Booster这些都可以直接使用)


    R类内联解决方法二:


    升级AGP版本到4.1以上
    image.png


    这是AGP 4.1版本的升级说明截图,他帮助咱们做了上面美团方案的插桩替换的一系列动作,R类内联是非常有必要的我们的app做了R类的内联以后apk大小减少了百分之十。这部分收益必须是在R类没有被混淆的时候keep住的前提下才可以,如果keep住了R类这部分收益就没办法了,可以在主moudle的proguard-rules.-printconfiguration "build/outputs/mapping/configuration.txt"(其实此文件的路径随便写都可以)来查看是不是添加了keep R类。如果是自己工程里面添加了keep R类就直接删了就好但是如果是三方的aar怎么办呢?


        tasks.each {
    if (it.name.startsWith("mini") && it.name.endsWith("R8")) {
    def f = it.configurationFiles.filter { File ff ->
    if (ff.exists()) {
    !ff.text.contains("-keep public class **.R\$*")
    } else {
    false
    }
    }
    it.configurationFiles.setFrom(f.files)
    }
    }
    }
    复制代码

    可以在app moudle 下的build.gradle中添加如下代码,会自动的将keep R类排除在外


    4、资源压缩


    在apk中res的资源占了很大的一部分,这部分如果可以被减少那么对减少apk size也有很大的收益,在android中主要的资源是图片,图片有几种格式jpg,png,webp同一个图片应该webp是最小的,所以可以将图片从png转成webp这样apk size不就变小了。如果是一两张图片还好可以在线转然后直接丢到工程里面但是如果所有的图片都要转呢?


    抖音团队给出了一个无入侵的解决方案,但是我并没有完全使用他这种方案因为需要hook如果说android系统版本更新hook点就要非常小心,搞过插件化的都知道这是永远的痛,但是有什么更好的方法吗?其实是有的


    需要先明确个概念就是android中所有的res资源都是会合并的,但是如果在不同的moudle包含了同样的res怎么办呢?答案是合并,合并的规则为参考官网总而言之就是主moudle会优先依赖的moudle,根据这个特点是不是可以将所有的图片都转成webp到主moudle的res目录中呢?


    在我们的项目中使用了zoom通过apk大小分析看到zoom相关的图片是最大的所以这里就拿zoom为例。


    先讲下操作思路,自定义gradle插件,添加png转webp task将png转为webp,我这里用的是webp官方的转换工具libwebp,其实还有其他工具。


    下个问题就是怎么获取当前工程中全部的资源呢?答案是通过AGP 中android 对象下的 ApplicationVariant中的 getAllRawAndroidResources()此方法可以把当前变种所有的资源都获取到,具体代码如下:


    Set<File> getAllResPath(Project project) {
    def extension = project.getExtensions().getByName("android")
    Set<File> allRes = new HashSet<>()
    if (project.getPlugins().hasPlugin("com.android.application")) {
    extension.applicationVariants.each {
    allRes.addAll(it.allRawAndroidResources)
    }
    return allRes
    }
    return null
    }
    复制代码

    获取到所有的资源以后就可以一层层的遍历找到非webp的图片然后使用libwebp转为webp


    5、插件化


    插件化是一个非常好的减少包大小的方式,将一些无关紧要不常用的moudle改成插件,然后发布的时候只发布宿主,到用户使用到对应的模块时候下载对应的插件,市面上有很多插件化方案,大家可以对比选用,这个我们也在进行中ing。


    参考链接:


    美团方案


    网易大前端团队实践


    抖音瘦身实践


    作者:Android_阿飞
    链接:https://juejin.cn/post/7031471769248595975
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    Python列表和集合的查找原理

    集合与列表查找对比关于大量数据查找,效率差距到底有多大?先看一组实例:import timeimport randomnums = [random.randint(0, 2000000) for i in range(1000)]list_test = lis...
    继续阅读 »

    集合与列表查找对比

    关于大量数据查找,效率差距到底有多大?

    先看一组实例:

    import time
    import random
    nums = [random.randint(0, 2000000) for i in range(1000)]
    list_test = list(range(1000000))
    set_test = set(list_test)
    count_list, count_set = 0, 0
    t1 = time.time() #测试在列表中进行查找
    for num in nums:
    if num in list_test:
    count_list += 1
    t2 = time.time()
    for num in nums: #测试在集合中进行查找
    if num in set_test:
    count_set += 1
    t3 = time.time() #测试在集合中进行查找
    print('找到个数,列表:{},集合:{}'.format(count_list, count_set))
    print('使用时间,列表:{:.4f}s'.format(t2 - t1))
    print('使用时间,集合:{:.4f}s'.format(t3 - t2))

    输出结果为:

    找到个数,列表:528,集合:528
    使用时间,列表:7.9329s
    使用时间,集合:0.0010s

    对于大数据集量来说,我们清晰地看到,集合的查找效率远远的高于列表,那么本文接下来会从Python底层数据结构的角度分析为何出现如此情况。

    list列表的原理

    Python中的list作为一个常用数据结构,在很多程序中被用来当做数组使用,可能很多人都觉得list无非就是一个动态数组,就像C++中的vector或者Go中的slice一样。但事实真的是这样的吗?

    我们来思考一个简单的问题,Python中的list允许我们存储不同类型的数据,既然类型不同,那内存占用空间就就不同,不同大小的数据对象又是如何存入数组中呢?

    比如下面的代码中,我们分别在数组中存储了一个字符串,一个整形,以及一个字典对象,假如是数组实现,则需要将数据存储在相邻的内存空间中,而索引访问就变成一个相当困难的事情了,毕竟我们无法猜测每个元素的大小,从而无法定位想要的元素位置。

    >>> test = ["hello world", 456, {}]
    >>> test
    ['hello world', 456, {}]

    是通过链表结构实现的吗?毕竟链表支持动态的调整,借助于指针可以引用不同类型的数据。但是这样的话使用下标索引数据的时候,需要依赖于遍历的方式查找,O(n)的时间复杂度访问效率实在是太低。

    同时使用链表的开销也较大,每个数据项除了维护本地数据指针外,还要维护一个next指针,因此还要额外分配8字节数据,同时链表分散性使其无法像数组一样利用CPU的缓存来高效的执行数据读写。

    实现的细节可以从其Python的源码中找到, 定义如下:

    typedef struct {
    PyObject_VAR_HEAD
    PyObject **ob_item;
    Py_ssize_t allocated;
    } PyListObject;

    内部list的实现的是一个C结构体,该结构体中的obitem是一个指针数组,存储了所有对象的指针数据,allocated是已分配内存的数量, PyObjectVAR_HEAD是一个宏扩展包含了更多扩展属性用于管理数组,比如引用计数以及数组大小等内容。

    所以我们可以看出,用动态数组作为第一层数据结构,动态数组里存储的是指针,指向对应的数据。

    既然是一个动态数组,则必然会面临一个问题,如何进行容量的管理,大部分的程序语言对于此类结构使用动态调整策略,也就是当存储容量达到一定阈值的时候,扩展容量,当存储容量低于一定的阈值的时候,缩减容量。

    道理很简单,但实施起来可没那么容易,什么时候扩容,扩多少,什么时候执行回收,每次又要回收多少空闲容量,这些都是在实现过程中需要明确的问题。

    假如我们使用一种最简单的策略:超出容量加倍,低于一半容量减倍。这种策略会有什么问题呢?设想一下当我们在容量已满的时候进行一次插入,随即删除该元素,交替执行多次,那数组数据岂不是会不断地被整体复制和回收,已经无性能可言了。

    对于Python list的动态调整规则程序中定义如下, 当追加数据容量已满的时候,通过下面的方式计算再次分配的空间大小,创建新的数组,并将所有数据复制到新的数组中。这是一种相对数据增速较慢的策略,回收的时候则当容量空闲一半的时候执行策略,获取新的缩减后容量大小。

    具体规则如下:

    new_allocated = (newsize >> 3) + (newsize < 9 ? 3 : 6)
    new_allocated += newsize

    动态数组扩容规则是:当出现数组存满时,扩充容量新加入的长度和额外3个,如果新加入元素大于9时,则扩6额外。

    其实对于Python列表这种数据结构的动态调整,在其他语言中也都存在,只是大家可能在日常使用中并没有意识到,了解了动态调整规则,我们可以通过比如手动分配足够的空间,来减少其动态分配带来的迁移成本,使得程序运行的更高效。

    另外如果事先知道存储在列表中的数据类型都相同,比如都是整形或者字符等类型,可以考虑使用arrays库,或者numpy库,两者都提供更直接的数组内存存储模型,而不是上面的指针引用模型,因此在访问和存储效率上面会更高效一些。

    从上面的数据结构可以得出,Python list的查找时间复杂度为O(n),因为作为一个动态数组,需要遍历每一个元素去找到目标元素,故而是一种较为低效的查找方式。

    set集合的原理

    说到集合,就不得不提到Python中的另一种数据结构,就是字典。字典和集合有异曲同工之妙。

    在Python中,字典是通过散列表或说哈希表实现的。字典也被称为关联数组,还称为哈希数组等。也就是说,字典也是一个数组,但数组的索引是键经过哈希函数处理后得到的散列值。

    哈希函数的目的是使键均匀地分布在数组中,并且可以在内存中以O(1)的时间复杂度进行寻址,从而实现快速查找和修改。哈希表中哈希函数的设计困难在于将数据均匀分布在哈希表中,从而尽量减少哈希碰撞和冲突。由于不同的键可能具有相同的哈希值,即可能出现冲突,高级的哈希函数能够使冲突数目最小化。

    Python中并不包含这样高级的哈希函数,几个重要(用于处理字符串和整数)的哈希函数是常见的几个类型。

    通常情况下建立哈希表的具体过程如下:

    • 数据添加:把key通过哈希函数转换成一个整型数字,然后就将该数字对数组长度进行取余,取余结果就当作数组的下标,将value存储在以该数字为下标的数组空间里。

    • 数据查询:再次使用哈希函数将key转换为对应的数组下标,并定位到数组的位置获取value。

    哈希函数就是一个映射,因此哈希函数的设定很灵活,只要使得任何关键字由此所得的哈希函数值都落在表长允许的范围之内即可。本质上看哈希函数不可能做成一个一对一的映射关系,其本质是一个多对一的映射,这也就引出了下面一个概念——哈希冲突或者说哈希碰撞。哈希碰撞是不可避免的,但是一个好的哈希函数的设计需要尽量避免哈希碰撞。

    Python中使用开放地址法解决冲突

    CPython使用伪随机探测(pseudo-random probing)的散列表(hash table)作为字典的底层数据结构。由于这个实现细节,只有可哈希的对象才能作为字典的键。字典的三个基本操作(添加元素,获取元素和删除元素)的平均事件复杂度为O(1)。

    Python中所有不可变的内置类型都是可哈希的。可变类型(如列表,字典和集合)就是不可哈希的,因此不能作为字典的键。

    常见的哈希碰撞解决方法:

    1. 开放寻址法(open addressing)
      开放寻址法中,所有的元素都存放在散列表里,当产生哈希冲突时,通过一个探测函数计算出下一个候选位置,如果下一个获选位置还是有冲突,那么不断通过探测函数往下找,直到找个一个空槽来存放待插入元素。开放地址的意思是除了哈希函数得出的地址可用,当出现冲突的时候其他的地址也一样可用,常见的开放地址思想的方法有线性探测再散列,二次探测再散列等,这些方法都是在第一选择被占用的情况下的解决方法。
    2. 再哈希法
      这个方法是按顺序规定多个哈希函数,每次查询的时候按顺序调用哈希函数,调用到第一个为空的时候返回不存在,调用到此键的时候返回其值。
    3. 链地址法
      将所有关键字哈希值相同的记录都存在同一线性链表中,这样不需要占用其他的哈希地址,相同的哈希值在一条链表上,按顺序遍历就可以找到。
    4. 公共溢出区
      其基本思想是:所有关键字和基本表中关键字为相同哈希值的记录,不管他们由哈希函数得到的哈希地址是什么,一旦发生冲突,都填入溢出表。
    5. 装填因子α
      一般情况下,处理冲突方法相同的哈希表,其平均查找长度依赖于哈希表的装填因子。哈希表的装填因子定义为表中填入的记录数和哈希表长度的比值,也就是标志着哈希表的装满程度。直观看来,α越小,发生冲突的可能性就越小,反之越大。一般0.75比较合适,涉及数学推导。

    在Python中一个key-value是一个entry,entry有三种状态:

    1. Unused:me_key == me_value == NULL

    Unused是entry的初始状态,key和value都为NULL。插入元素时,Unused状态转换成Active状态。这是me_key为NULL的唯一情况。

    1. Active:me_key != NULL and me_key != dummy 且 me_value != NULL

    插入元素后,entry就成了Active状态,这是me_value唯一不为NULL的情况,删除元素时Active状态可转换成Dummy状态。

    1. Dummy:me_key == dummy 且 me_value == NULL

    此处的Dummy对象实际上一个PyStringObject对象,仅作为指示标志。Dummy状态的元素可以在插入元素的时候将它变成Active状态,但它不可能再变成Unused状态。

    为什么entry有Dummy状态呢?

    这是因为采用开放寻址法中,遇到哈希冲突时会找到下一个合适的位置,例如某元素经过哈希计算应该插入到A处,但是此时A处有元素的,通过探测函数计算得到下一个位置B,仍然有元素,直到找到位置C为止,此时ABC构成了探测链,查找元素时如果hash值相同,那么也是顺着这条探测链不断往后找,当删除探测链中的某个元素时,比如B,如果直接把B从哈希表中移除,即变成Unused状态,那么C就不可能再找到了,因为AC之间出现了断裂的现象,正是如此才出现了第三种状态-Dummy,Dummy是一种类似的伪删除方式,保证探测链的连续性。

    set集合和dict一样也是基于散列表的,只是他的表元只包含键的引用,而没有对值的引用,其他的和dict基本上是一致的,所以在此就不再多说了。并且dict要求键必须是能被哈希的不可变对象,因此普通的set无法作为dict的键,必须选择被“冻结”的不可变集合类:frozenset。顾名思义,一旦初始化,集合内数据不可修改。

    一般情况下普通的顺序表数组存储结构也可以认为是简单的哈希表,虽然没有采用哈希函数(取余),但同样可以在O(1)时间内进行查找和修改。但是这种方法存在两个问题:

    • 扩展性不强
    • 浪费空间

    dict是用来存储键值对结构的数据的,set其实也是存储的键值对,只是默认键和值是相同的。Python中的dict和set都是通过散列表来实现的。下面来看与dict相关的几个比较重要的问题:

    • dict中的数据是无序存放的。操作的时间复杂度,插入、查找和删除都可以在O(1)的时间复杂度。这是因为查找相当于将查找值通过哈希函数运算之后,直接得到对应的桶位置(不考虑哈希冲突的理想情况下),故而复杂度为O(1)。

    • 由于键的限制,只有可哈希的对象才能作为字典的键和set的值。可hash的对象即Python中的不可变对象和自定义的对象。可变对象(列表、字典、集合)是不能作为字典的键和set的值的。

    与list相比:list的查找和删除的时间复杂度是O(n),添加的时间复杂度是O(1)。但是dict使用hashtable内存的开销更大。为了保证较少的冲突,hashtable的装载因子,一般要小于0.75,在Python中当装载因子达到2/3的时候就会自动进行扩容。

    参考资料:

    Python dict和set的底层原理:https://blog.csdn.net/liuweiyuxiang/article/details/98943272

    python 图解Python List数据结构:https://blog.csdn.net/u014029783/article/details/107992840

    作者:严天宇
    来源:https://mp.weixin.qq.com/s/wvgf7GpbCoeDsLOp1WAFPg
    收起阅读 »

    【前端工程化】- 结合代码实践,全面学习前端工程化

    前言前端工程化,简而言之就是软件工程 前端,以自动化的形式呈现。就个人理解而言:前端工程化,从开发阶段到代码发布生产环境,包含了以下几个内容:开发构建测试部署性能规范 下面我们根据上述几个内容,选择有代表性的几个方面进行深入学习前端工程化。脚手架脚手...
    继续阅读 »

    前言

    前端工程化,简而言之就是软件工程 前端,以自动化的形式呈现。就个人理解而言:前端工程化,从开发阶段到代码发布生产环境,包含了以下几个内容:

    • 开发
    • 构建
    • 测试
    • 部署
    • 性能
    • 规范

    image.png 下面我们根据上述几个内容,选择有代表性的几个方面进行深入学习前端工程化。


    脚手架

    脚手架是什么?(What)

    现在流行的前端脚手架基本上都是基于NodeJs编写,比如我们常用的Vue-CLI,比较火的create-react-app,还有Dva-CLI等。

    脚手架存在的意义?(Why)

    随着前端工程化的概念越来越深入人心,脚手架的出现就是为减少重复性工作而引入的命令行工具,摆脱ctrl cctrl v,此话怎讲? 现在新建一个前端项目,已经不是在html头部引入css,尾部引入js那么简单的事了,css都是采用Sass或则Less编写,在js中引入,然后动态构建注入到html中;除了学习基本的jscss语法和热门框架,还需要学习构建工具webpackbabel这些怎么配置,怎么起前端服务,怎么热更新;为了在编写过程中让编辑器帮我们查错以及更加规范,我们还需要引入ESlint;甚至,有些项目还需要引入单元测试(Jest)。对于一个更入门的人来说,这无疑会让人望而却步。而前端脚手架的出现,就让事情简单化,一键命令,新建一个工程,再执行两个npm命令,跑起一个项目。在入门时,无需关注配置什么的,只需要开心的写代码就好。

    如何实现一个新建项目脚手架(基于koa)?(How)

    先梳理下实现思路

    我们实现脚手架的核心思想就是自动化思维,将重复性的ctrl cctrl v创建项目,用程序来解决。解决步骤如下:

    1. 创建文件夹(项目名)
    2. 创建 index.js
    3. 创建 package.json
    4. 安装依赖

    1. 创建文件夹

    创建文件夹前,需要先删除清空:


    // package.json
    {
    ...
    "scripts": {
    "test": "rm -rf ./haha && node --experimental-modules index.js"
    }
    ...
    }

    创建文件夹:我们通过引入 nodejsfs 模块,使用 mkdirSync API来创建文件夹。


    // index.js
    import fs from 'fs';

    function getRootPath() {
    return "./haha";
    }

    // 生成文件夹
    fs.mkdirSync(getRootPath());

    2. 创建 index.js


    创建 index.js:使用 nodejsfs 模块的 writeFileSync API 创建 index.js 文件:


    // index.js
    fs.writeFileSync(getRootPath() + "/index.js", createIndexTemplate(inputConfig));

    接着我们来看看,动态模板如何生成?我们最理想的方式是通过配置来动态生成文件模板,那么具体来看看 createIndexTemplate 实现的逻辑吧。


    // index.js
    import fs from 'fs';
    import { createIndexTemplate } from "./indexTemplate.js";

    // input
    // process
    // output
    const inputConfig = {
    middleWare: {
    router: true,
    static: true
    }
    }
    function getRootPath() {
    return "./haha";
    }
    // 生成文件夹
    fs.mkdirSync(getRootPath());
    // 生成 index.js 文件
    fs.writeFileSync(getRootPath() + "/index.js", createIndexTemplate(inputConfig));

    // indexTemplate.js
    import ejs from "ejs";
    import fs from "fs";
    import prettier from "prettier";// 格式化代码
    // 问题驱动
    // 模板
    // 开发思想 - 小步骤的开发思想
    // 动态生成代码模板
    export function createIndexTemplate(config) {
    // 读取模板
    const template = fs.readFileSync("./template/index.ejs", "utf-8");

    // ejs渲染
    const code = ejs.render(template, {
    router: config.middleware.router,
    static: config.middleware.static,
    port: config.port,
    });

    // 返回模板
    return prettier.format(code, {
    parser: "babel",
    });
    }

    // template/index.ejs
    const Koa = require("koa");
    <% if (router) { %>
    const Router = require("koa-router");
    <% } %>


    <% if (static) { %>
    const serve = require("koa-static");
    <% } %>

    const app = new Koa();

    <% if (router) { %>
    const router = new Router();
    router.get("/", (ctx) => {
    ctx.body = "hello koa-setup-heihei";
    });
    app.use(router.routes());
    <% } %>

    <% if (static) { %>
    app.use(serve(__dirname + "/static"));
    <% } %>

    app.listen(<%= port %>, () => {
    console.log("open server localhost:<%= port %>");
    });

    3. 创建 package.json


    创建 package.json 文件,实质是和创建 index.js 类似,都是采用动态生成模板的思路来实现,我们来看下核心方法 createPackageJsonTemplate 的实现代码:


    // packageJsonTemplate.js
    function createPackageJsonTemplate(config) {
    const template = fs.readFileSync("./template/package.ejs", "utf-8");

    const code = ejs.render(template, {
    packageName: config.packageName,
    router: config.middleware.router,
    static: config.middleware.static,
    });

    return prettier.format(code, {
    parser: "json",
    });
    }

    // template/package.ejs
    {
    "name": "<%= packageName %>",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
    },
    "keywords": [],
    "author": "",
    "license": "ISC",
    "dependencies": {
    "koa": "^2.13.1"
    <% if (router) { %>
    ,"koa-router": "^10.1.1"
    <% } %>

    <% if (static) { %>
    ,"koa-static": "^5.0.0"
    }
    <% } %>
    }

    4. 安装依赖


    要自动安装依赖,我们可以使用 nodejsexeca 库执行 yarn 安装命令:


    execa("yarn", {
    cwd: getRootPath(),
    stdio: [2, 2, 2],
    });

    至此,我们已经用 nodejs 实现了新建项目的脚手架了。最后我们可以重新梳理下可优化点将其升级完善。比如将程序配置升级成 GUI 用户配置(用户通过手动选择或是输入来传入配置参数,例如项目名)。




    编译构建

    编译构建是什么?


    构建,或者叫作编译,是前端工程化体系中功能最繁琐、最复杂的模块,承担着从源代码转化为宿主浏览器可执行的代码,其核心是资源的管理。前端的产出资源包括JS、CSS、HTML等,分别对应的源代码则是:



    • 领先于浏览器实现的ECMAScript规范编写的JS代码(ES6/7/8...)。

    • LESS/SASS预编译语法编写的CSS代码。

    • Jade/EJS/Mustache等模板语法编写的HTML代码。


    以上源代码是无法在浏览器环境下运行的,构建工作的核心便是将其转化为宿主可执行代码,分别对应:



    • ECMAScript规范的转译。

    • CSS预编译语法转译。

    • HTML模板渲染。


    那么下面我们就一起学习下如今3大主流构建工具:Webpack、Rollup、Vite。


    Webpack


    image.png


    Webpack原理


    想要真正用好 Webpack 编译构建工具,我们需要先来了解下它的工作原理。Webpack 编译项目的工作机制是,递归找出所有依赖模块,转换源码为浏览器可执行代码,并构建输出bundle。具体工作流程步骤如下:



    1. 初始化参数:取配置文件和shell脚本参数并合并

    2. 开始编译:用上一步得到的参数初始化compiler对象,执行run方法开始编译

    3. 确定入口:根据配置中的entry,确定入口文件

    4. 编译模块:从入口文件出发,递归遍历找出所有依赖模块的文件

    5. 完成模块编译:使用loader转译所有模块,得到转译后的最终内容和依赖关系

    6. 输出资源:根据入口和模块依赖关系,组装成一个个chunk,加到输出列表

    7. 输出完成:根据配置中的output,确定输出路径和文件名,把文件内容写入输出目录(默认是dist


    Webpack实践


    1. 基础配置


    【entry】



    入口配置,webpack 编译构建时能找到编译的入口文件,进而构建内部依赖图。



    【output】



    输出配置,告诉 webpack 在哪里输出它所创建的 bundle,以及如何命名这些文件。



    【loader】



    模块转换器,loader 可以处理浏览器无法直接运行的文件模块,转换为有效模块。比如:css-loader和style-loader处理样式;url-loader和file-loader处理图片。



    【plugin】



    插件,解决 loader 无法实现的问题,在 webpack 整个构建生命周期都可以扩展插件。比如:打包优化,资源管理,注入环境变量等。



    下面是 webpack 基本配置的简单示例:


    const path = require("path");

    module.exports = {
    mode: "development",
    entry: "./src/index.js",
    output: {
    filename: "main.js",
    path: path.resolve(__dirname, "dist"),
    },
    devServer: {
    static: "./dist",
    },
    module: {
    rules: [
    {
    // 匹配什么样子的文件
    test: /\.css$/i,
    // 使用loader , 从后到前执行
    use: ["style-loader", "css-loader"],
    }
    ],
    },
    };


    参考webpack官网:webpack.docschina.org/concepts/

    (注意:使用不同版本的 webpack 切换对应版本的文档哦)



    2. 性能优化


    • 编译速度优化

    【检测编译速度】


    寻找检测编译速度的工具,比如 speed-measure-webpack-plugin插件 ,用该插件分析每个loader和plugin执行耗时具体情况。


    【优化编译速度该怎么做呢?】




    1. 减少搜索依赖的时间



    • 配置 loader 匹配规则 test/include/exclue,缩小搜索范围,即可减少搜索时间



    1. 减少解析转换的时间



    • noParse配置,精准过滤不用解析的模块

    • loader性能消耗大的,开启多进程



    1. 减少构建输出的时间



    • 压缩代码,开启多进程



    1. 合理使用缓存策略



    • babel-loader开启缓存

    • 中间模块启用缓存,比如使用 hard-source-webpack-plugin


    具体优化措施可参考:webpack性能优化的一段经历|项目复盘



    • 体积优化

    【检测包体积大小】


    寻找检测构建后包体积大小的工具,比如 webpack-bundle-analyzer插件 ,用该插件分析打包后生成Bundle的每个模块体积大小。


    【优化体积该怎么做呢?】




    1. bundle去除第三方依赖

    2. 擦除无用代码 Tree Shaking


    具体优化措施参考:webpack性能优化的一段经历|项目复盘



    Rollup


    Rollup概述


    Rollup 是一个 JavaScript 模块打包器,可以将小块代码编译成大块复杂的代码,例如 library 或应用程序。并且可以对代码模块使用新的标准化格式,比如CommonJSes module


    Rollup原理


    我们先来了解下 Rollup 原理,其主要工作机制是:



    1. 确定入口文件

    2. 使用 Acorn 读取解析文件,获取抽象语法树 AST

    3. 分析代码

    4. 生成代码,输出


    Rollup 相对 Webpack 而言,打包出来的包会更加轻量化,更适用于类库打包,因为内置了 Tree Shaking 机制,在分析代码阶段就知晓哪些文件引入并未调用,打包时就会自动擦除未使用的代码。



    Acorn 是一个 JavaScript 语法解析器,它将 JavaScript 字符串解析成语法抽象树 AST 如果想了解 AST 语法树可以点下这个网址astexplorer.net/



    Rollup实践


    【input】



    入口文件路径



    【output】



    输出文件、输出格式(amd/es6/iife/umd/cjs)、sourcemap启用等。



    【plugin】



    各种插件使用的配置



    【external】



    提取外部依赖



    【global】



    配置全局变量



    下面是 Rollup 基础配置的简单示例:


    import commonjs from "@rollup/plugin-commonjs";
    import resolve from "@rollup/plugin-node-resolve";
    // 解析json
    import json from '@rollup/plugin-json'
    // 压缩代码
    import { terser } from 'rollup-plugin-terser';
    export default {
    input: "src/main.js",
    output: [{
    file: "dist/esmbundle.js",
    format: "esm",
    plugins: [terser()]
    },{
    file: "dist/cjsbundle.js",
    format: "cjs",
    }],
    // commonjs 需要放到 transform 插件之前,
    // 但是又个例外, 是需要放到 babel 之后的
    plugins: [json(), resolve(), commonjs()],
    external: ["vue"]
    };

    Vite

    Vite概述


    Vite,相比 Webpack、Rollup 等工具,极大地改善了前端开发者的开发体验,编译速度极快。


    Vite原理


    为什么 Vite 开发编译速度极快?我们就先来探究下它的原理吧。
    image.png
    由上图可见,Vite 原理是利用现代主流浏览器支持原生的 ESM 规范,配合 server 做拦截,把代码编译成浏览器支持的。
    image.png


    Vite实践体验


    我们可以搭建一个Hello World版的Vite项目来感受下飞快的开发体验:



    注意:Vite 需要 Node.js 版本 >= 12.0.0。



    使用 NPM:


    $ npm init vite@latest

    使用 Yarn:


    $ yarn create vite

    image.png
    上图是Vite项目的编译时间,363ms,开发秒级编译的体验,真的是棒棒哒!


    3种构建工具综合对比





































    WebpackRollupVite
    编译速度一般较快最快
    HMR热更新支持需要额外引入插件支持
    Tree Shaking需要额外配置支持支持
    适用范围项目打包类库打包不考虑兼容性的项目



    测试

    当我们前端项目越来越庞大时,开发迭代维护成本就会越来越高,数十个模块相互调用错综复杂,为了提高代码质量和可维护性,就需要写测试了。下面就给大家具体介绍下前端工程经常做的3类测试。

    单元测试


    单元测试,是对最小可测试单元(一般为单个函数、类或组件)进行检查和验证。

    做单元测试的框架有很多,比如 Mocha断言库ChaiSinonJest等。我们可以先选择 jest 来学习,因为它集成了 Mochachaijsdomsinon 等功能。接下来,我们一起看看 jest 怎么写单元测试吧?



    1. 根据正确性写测试,即正确的输入应该有正常的结果。

    2. 根据错误性写测试,即错误的输入应该是错误的结果。


    以验证求和函数为例:


    // add函数
    module.exports = (a,b) => {
    return a+b;
    }

    // 正确性测试验证
    const add = require('./add.js');

    test('should 1+1 = 2', ()=> {
    // 准备测试数据 -> given
    const a = 1;
    const b = 1;
    // 触发测试动作 -> when
    const r = add(a,b);
    // 验证 -> then
    expect(r).toBe(2);
    })

    image.png


    // 错误性测试验证
    test('should 1+1 = 2', ()=> {
    // 准备测试数据 -> given
    const a = 1;
    const b = 2;
    // 触发测试动作 -> when
    const r = add(a,b)
    // 验证 -> then
    expect(r).toBe(2);
    })

    image.png


    组件测试

    组件测试,主要是针对某个组件功能进行测试,这就相对困难些,因为很多组件涉及了DOM操作。组件测试,我们可以借助组件测试框架来做,比如使用 Cypress(它可以做组件测试,也可以做 e2e 测试)。我们就先来看看组件测试怎么做?


    以 vue3 组件测试为例:



    1. 我们先建好 vue3 + vite 项目,编写测试组件

    2. 再安装 cypress 环境

    3. cypress/component 编写组件测试脚本文件

    4. 执行 cypress open-ct 命令,启动 cypress component testing 的服务运行 xx.spec.js 测试脚本,便能直观看到单个组件自动执行操作逻辑


    // Button.vue 组件

    <template>
    <div>Button测试</div>
    </template>
    <script>
    export default {
    }
    </script>
    <style>
    </style>

    // cypress/plugin/index.js 配置

    const { startDevServer } = require('@cypress/vite-dev-server')
    // eslint-disable-next-line no-unused-vars
    module.exports = (on, config) => {
    // `on` is used to hook into various events Cypress emits
    // `config` is the resolved Cypress config
    on('dev-server:start', (options) => {
    const viteConfig = {
    // import or inline your vite configuration from vite.config.js
    }
    return startDevServer({ options, viteConfig })
    })
    return config;
    }

    // cypress/component/Button.spec.js Button组件测试脚本

    import { mount } from "@cypress/vue";
    import Button from "../../src/components/Button.vue";

    describe("Button", () => {
    it("should show button", () => {
    // 挂载button
    mount(Button);

    cy.contains("Button");
    });
    });

    e2e测试


    e2e 测试,也叫端到端测试,主要是模拟用户对页面进行一系列操作并验证其是否符合预期。我们同样也可以使用 cypress 来做 e2e 测试,具体怎么做呢?


    以 todo list 功能验证为例:



    1. 我们先建好 vue3 + vite 项目,编写测试组件

    2. 再安装 cypress 环境

    3. cypress/integration 编写组件测试脚本文件

    4. 执行 cypress open 命令,启动 cypress 的服务,选择 xx.spec.js 测试脚本,便能直观看到模拟用户的操作流程


    // cypress/integration/todo.spec.js todo功能测试脚本

    describe('example to-do app', () => {
    beforeEach(() => {
    cy.visit('https://example.cypress.io/todo')
    })

    it('displays two todo items by default', () => {
    cy.get('.todo-list li').first().should('have.text', 'Pay electric bill')
    cy.get('.todo-list li').last().should('have.text', 'Walk the dog')
    })

    it('can add new todo items', () => {
    const newItem = 'Feed the cat'
    cy.get('[data-test=new-todo]').type(`${newItem}{enter}`)

    cy.get('.todo-list li')
    .should('have.length', 3)
    .last()
    .should('have.text', newItem)
    })

    it('can check off an item as completed', () => {
    cy.contains('Pay electric bill')
    .parent()
    .find('input[type=checkbox]')
    .check()

    cy.contains('Pay electric bill')
    .parents('li')
    .should('have.class', 'completed')
    })

    context('with a checked task', () => {
    beforeEach(() => {
    cy.contains('Pay electric bill')
    .parent()
    .find('input[type=checkbox]')
    .check()
    })

    it('can filter for uncompleted tasks', () => {
    cy.contains('Active').click()

    cy.get('.todo-list li')
    .should('have.length', 1)
    .first()
    .should('have.text', 'Walk the dog')

    cy.contains('Pay electric bill').should('not.exist')
    })

    it('can filter for completed tasks', () => {
    // We can perform similar steps as the test above to ensure
    // that only completed tasks are shown
    cy.contains('Completed').click()

    cy.get('.todo-list li')
    .should('have.length', 1)
    .first()
    .should('have.text', 'Pay electric bill')

    cy.contains('Walk the dog').should('not.exist')
    })

    it('can delete all completed tasks', () => {
    cy.contains('Clear completed').click()

    cy.get('.todo-list li')
    .should('have.length', 1)
    .should('not.have.text', 'Pay electric bill')

    cy.contains('Clear completed').should('not.exist')
    })
    })
    })

    e2e.gif




    总结

    本文前言部分通过开发、构建、性能、测试、部署、规范六个方面,较全面地梳理了前端工程化的知识点,正文则主要介绍了在实践项目中落地使用的前端工程化核心技术点。

    希望本文能够帮助到正在学前端工程化的小伙伴构建完整的知识图谱~


    作者:小铭子
    来源:https://juejin.cn/post/7033355647521554446
    收起阅读 »

    Python运算符优先级及结合性

    当多个运算符出现在一起需要进行运算时,Python 会先比较各个运算符的优先级,按照优先级从高到低的顺序依次执行;当遇到优先级相同的运算符时,再根据结合性决定先执行哪个运算符:如果是左结合性就先执行左边的运算符,如果是右结合性就先执行右边的运算符。运算符的优先...
    继续阅读 »

    当多个运算符出现在一起需要进行运算时,Python 会先比较各个运算符的优先级,按照优先级从高到低的顺序依次执行;

    当遇到优先级相同的运算符时,再根据结合性决定先执行哪个运算符:如果是左结合性就先执行左边的运算符,如果是右结合性就先执行右边的运算符。

    运算符的优先级

    在数学运算2 + 4 * 3中,要先计算乘法,再计算加法,否则结果就是错误的。所谓优先级,就是当多个运算符出现在一起时,需要先执行哪个运算符,那么这个运算符的优先级就更高。 

    Python中运算符优先级如下表所示,括号的优先级是最高的,无论任何时候优先计算括号里面的内容,逻辑运算符的优先级最低,在表中处在同一行运算符的优先级一致,上一层级的优先级高于下一层级的。算术运算符可以分为四种,幂运算最高,其次是正负号,然后是 “* / // %”,最后才是加减“+ -”。

    运算符

    描述

    ()

    括号

    **

    幂运算

    ~

    按位取反

    +、-

    正号、负号

    、/、 %、 //

    乘、除、取模、取整除

    、-

    加、减

    >> 、<<

    右移、左移

    &

    按位“与”

    、|

    按位“异或”,按位“或”

    <=  、< 、>、 >=

    比较运算符

    ==、!=

    等于、不等于

    =、%=、/=、//=、-=、+=、*=、**=

    赋值运算符

    is、is not

    身份运算符

    in、not in

    成员运算符

    and or not

    逻辑运算符

    运算符的结合性

    在多种运算符在一起进行运算时,除了要考虑优先级,有时候还需要考虑结合性。当同时出现多个优先级相同的运算符时,先执行左边的叫左结合性,先执行右边的叫右结合性。如:5 / 2 * 4,由于/*的优先级相同,所以只能参考运算符的结合性了,/*都是左结合性的,所以先计算除法,再计算乘法,结果是10.0。Python中大部分运算符都具有左结合性,其中,幂运算**、正负号、赋值运算符等具有右结合性。

    >>> 5 / 2* 4# 左结合性运算符
    10.0
    >>> 2 ** 2 ** 3# 右结合性,等同于2 ** (2 **3)
    256

    虽然Python运算符存在优先级的关系,但写程序时不建议写很长的表达式,过分依赖运算符的优先级,比如:2 ** -1 % 3 / 5 ** 3 *4,这样的表达式会大大降低程序的可读性。因此,建议写程序时,遵守以下两点原则,保证运算逻辑清晰明了。

    1. 尽量不要把一个表达式写的过长过于复杂,如果计算过程的确需要,可以尝试将它拆分几部分来写。
    2. 尽量多使用()来控制运算符的执行顺序,使用()可以让运算的先后顺序变得十分清楚。



    作者:刘文飞 

    来源:https://mp.weixin.qq.com/s/fXzg2L6emlEVCCT-t4Pk6Q





    收起阅读 »

    【vue自定义组件】实现一个污染日历

    vue
    前言 佛祖保佑, 永无bug。Hello 大家好!我是海的对岸! 实际开发中,碰到一个日历的需求,这个日历的需求中要加入定制的业务,网上没有现成的,手动实现了一下,整理记录下。 动画效果: 实现 实现背景 工作上碰到一个需求,需要有一个可以在日历上能看到每天...
    继续阅读 »

    前言


    佛祖保佑, 永无bug。Hello 大家好!我是海的对岸!


    实际开发中,碰到一个日历的需求,这个日历的需求中要加入定制的业务,网上没有现成的,手动实现了一下,整理记录下。


    动画效果:


    calendar.gif


    实现


    实现背景


    工作上碰到一个需求,需要有一个可以在日历上能看到每天的污染情况的状态,因此,我们梳理下需求:



    1. 要有一个日历组件

    2. 要在这个日历组件中追加自己的业务逻辑


    简单拎一下核心代码的功能


    实现日历模块


    大体上日历就是看某个月有多少多少天,拆分下,如下所示:
    image.png


    再对比这我们的效果图,日历上还要有上个月的末尾几天


    image.png


    实现上个月的末尾几天


    monthFisrtDay() {
    // 所指的星期中的某一天,使用本地时间。返回值是 0(周日) 到 6(周六) 之间的一个整数
    // eslint-disable-next-line radix
    const currDT = (parseInt(this.year.substr(0, 4)) + '/' + parseInt((this.month).replace('月', '')) + '/1');
    let currWeek = new Date(currDT).getDay();
    return ++currWeek || 7;
    },
    // 刷新日历 获得上个月的结尾天数 <=7
    refreshCalendar() {
    this.nunDays = [];
    const lastDays = [];
    const lastMon = (this.month).replace('月', '') * 1 - 1;
    let lastDay = new Date(new Date(this.year.substr(0, 4), lastMon).getTime() - 8.64e7).getDate();
    for (let i = 1; i < this.monthFisrtDay(); i += 1) {
    lastDays.unshift(lastDay);
    lastDay -= 1;
    }
    this.nunDays = lastDays;
    },

    实现每个月的实际天数


    // 展示 日历数据
    getDatas() {
    if (this.dealDataFinal && this.dealDataFinal.length > 0) {
    // console.log(this.dealDataFinal);
    this.list = [];
    const datas = this.dealDataFinal;
    const dataMap = {};
    if (datas.length > 0) {
    datas.forEach((item) => {
    item.level -= 1;
    item.dateStr = item.tstamp.substr(0, 10);
    item.date = item.tstamp.substr(8, 2);
    dataMap[item.date] = item;
    });
    }

    const curDay = new Date().getDate();
    for (let i = 1; i <= this.monthDays; i += 1) {
    let currColor = this.lvls[6];
    let dateStr = String(i);
    let isCurDay = false;
    if (i == curDay) {
    isCurDay = true; // 表示刚好是今天(该日期 和网络上的今天是同一天)
    }
    dateStr = '0' + dateStr;
    dateStr = dateStr.substr(dateStr.length - 2);
    const dataObj = dataMap[dateStr];
    if (dataObj) {
    if (dataObj.level >= 0 && dataObj.level <= 5) {
    currColor = this.lvls[dataObj.level].color;
    } else {
    currColor = this.lvls[6].color;
    }

    this.list.push({
    date: i,
    curDay: isCurDay,
    color: currColor,
    datas: dataObj,
    checkedColor: undefined, // 选中颜色
    });
    } else {
    this.list.push({
    date: i,
    curDay: isCurDay,
    color: this.lvls[6].color,
    datas: {},
    checkedColor: undefined, // 选中颜色
    });
    }
    }
    // console.log(this.list);
    } else {
    this.clearCalendar();
    }
    },
    // 清除上一次的记录
    clearCalendar() {
    this.list = [];
    for (let i = 1; i <= this.monthDays; i += 1) {
    this.list.push({
    date: i,
    color: this.lvls[6].color,
    datas: {},
    });
    }
    },

    实现日历之后,追加业务


    定义业务上的字段


    data() {
    return {
    ...
    lvls: [
    { title: '优', color: '#00e400' },
    { title: '良', color: '#ffff00' },
    { title: '轻度污染', color: '#ff7e00' },
    { title: '中度污染', color: '#ff0000' },
    { title: '重度污染', color: '#99004c' },
    { title: '严重污染', color: '#7e0023' },
    { title: '未知等级', color: '#cacaca' },
    ],
    list: [], // 当前月的所有天数
    dealDataFinal: [], // 处理接口数据之后获得的最终的数组
    ...
    curYearMonth: '', // 当前时间 年月
    choseYearMonth: '', // 选择的时间 年月
    };
    },

    定义业务上的方法


    // 加载等级
    loadImgType(value) {
    let imgUrl = 0;
    switch (value) {
    case '优':
    imgUrl = 1;
    break;
    case '良':
    imgUrl = 2;
    break;
    case '轻':
    imgUrl = 3;
    break;
    case '中':
    imgUrl = 4;
    break;
    case '重':
    imgUrl = 5;
    break;
    case '严':
    imgUrl = 6;
    break;
    default:
    imgUrl = 0;
    break;
    }
    return imgUrl;
    },

    因为展示效果,用到的是css,css用的比较多,这里就不一段一段的解读了,总而言之,就是日元素不同状态的样式展示,通过前面设置的等级方法,来得到不同的返回参数,进而展示出不同参数对应的不同颜色样式。


    最后会放出日历组件的完整代码。


    完整代码


    <template>
    <div class="right-content">
    <div style="height: 345px;">
    <div class="" style="padding: 0px 15px;">
    <el-select v-model="year" style="width: 119px;" popper-class="EntDate">
    <el-option v-for="item in years" :value="item" :label="item" :key="item"></el-option>
    </el-select>
    <el-select v-model="month" style="width: 119px; margin-left: 10px;" popper-class="EntDate">
    <el-option v-for="item in mons" :value="item" :label="item" :key="item"></el-option>
    </el-select>
    <div class="r-inline">
    <span class="searchBtn" @click="qEQCalendar">查询</span>
    </div>
    </div>
    <div class="calendar" element-loading-spinner="el-icon-loading"
    element-loading-background="rgba(0, 0, 0, 0.6)">
    <div class="day-title clearfix">
    <div class="day-tt" v-for="day in days" :key="day">{{day}}</div>
    </div>
    <div class="clearfix" style="padding-top: 10px;">
    <div :class="{'date-item': true, 'is-last-month': true,}" v-for="(item, index) in nunDays" :key="index + 'num'">
    <div class="day">{{item}}</div>
    </div>
    <div :class="{'date-item': true, 'is-last-month': false, 'isPointer': isPointer}"
    v-for="(item, index) in list" :key="index" @click="queryDeal(item)">
    <div v-if="item.curDay && (curYearMonth === choseYearMonth)" class="day" :style="{border:'2px dashed' + item.color}"
    :class="{'choseDateItemI': item.checkedColor === '#00e400',
    'choseDateItemII': item.checkedColor === '#ffff00', 'choseDateItemIII': item.checkedColor === '#ff7e00', 'choseDateItemIV': item.checkedColor === '#ff0000',
    'choseDateItemV': item.checkedColor === '#99004c', 'choseDateItemVI': item.checkedColor === '#7e0023', 'choseDateItemVII': item.checkedColor === '#cacaca'}"
    >

    </div>
    <div v-else class="day" :style="{border:'2px solid' + item.color}"
    :class="{'choseDateItemI': item.checkedColor === '#00e400',
    'choseDateItemII': item.checkedColor === '#ffff00', 'choseDateItemIII': item.checkedColor === '#ff7e00', 'choseDateItemIV': item.checkedColor === '#ff0000',
    'choseDateItemV': item.checkedColor === '#99004c', 'choseDateItemVI': item.checkedColor === '#7e0023', 'choseDateItemVII': item.checkedColor === '#cacaca'}"
    >
    {{item.date}}
    </div>
    </div>
    </div>
    </div>
    </div>
    </div>
    </template>

    <script>
    const today = new Date();
    const years = [];
    const year = today.getFullYear();
    for (let i = 2018; i <= year; i += 1) {
    years.push(`${i}年`);
    }
    export default {
    props: {
    rightData2: {
    type: Object,
    defaul() {
    return undefined;
    },
    },
    isPointer: {
    type: Boolean,
    default() {
    return false;
    },
    },
    },
    watch: {
    rightData2(val) {
    this.dealData(val);
    },
    calendarData(val) {
    this.dealData(val);
    },
    },
    data() {
    return {
    pointInfo: {
    title: 'xxx污染日历',
    },
    days: ['日', '一', '二', '三', '四', '五', '六'],
    year: year + '年',
    years,
    month: (today.getMonth() + 1) + '月',
    mons: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
    lvls: [
    { title: '优', color: '#00e400' },
    { title: '良', color: '#ffff00' },
    { title: '轻度污染', color: '#ff7e00' },
    { title: '中度污染', color: '#ff0000' },
    { title: '重度污染', color: '#99004c' },
    { title: '严重污染', color: '#7e0023' },
    { title: '未知等级', color: '#cacaca' },
    ],
    list: [], // 当前月的所有天数
    dealDataFinal: [], // 处理接口数据之后获得的最终的数组
    nunDays: [],
    testDays: ['日', '一', '二', '三', '四', '五', '六'],
    calendarData: null,
    curYearMonth: '', // 当前时间 年月
    choseYearMonth: '', // 选择的时间 年月
    };
    },
    computed: {
    // 获取 select框中展示的具体月份应对应的月数
    monthDays() {
    const lastyear = (this.year).replace('年', '') * 1;
    const lastMon = (this.month).replace('月', '') * 1;
    const monNum = new Date(lastyear, lastMon, 0).getDate();
    // return this.$mp.dateFun.GetMonthDays(this.year.substr(0, 4), lastMon);
    return monNum;
    },
    },
    methods: {
    monthFisrtDay() {
    // 所指的星期中的某一天,使用本地时间。返回值是 0(周日) 到 6(周六) 之间的一个整数
    // eslint-disable-next-line radix
    const currDT = (parseInt(this.year.substr(0, 4)) + '/' + parseInt((this.month).replace('月', '')) + '/1');
    let currWeek = new Date(currDT).getDay();
    return ++currWeek || 7;
    },
    // 刷新日历 获得上个月的结尾天数 <=7
    refreshCalendar() {
    this.nunDays = [];
    const lastDays = [];
    const lastMon = (this.month).replace('月', '') * 1 - 1;
    let lastDay = new Date(new Date(this.year.substr(0, 4), lastMon).getTime() - 8.64e7).getDate();
    for (let i = 1; i < this.monthFisrtDay(); i += 1) {
    lastDays.unshift(lastDay);
    lastDay -= 1;
    }
    this.nunDays = lastDays;
    },
    // 展示 日历数据
    getDatas() {
    if (this.dealDataFinal && this.dealDataFinal.length > 0) {
    // console.log(this.dealDataFinal);
    this.list = [];
    const datas = this.dealDataFinal;
    const dataMap = {};
    if (datas.length > 0) {
    datas.forEach((item) => {
    item.level -= 1;
    item.dateStr = item.tstamp.substr(0, 10);
    item.date = item.tstamp.substr(8, 2);
    dataMap[item.date] = item;
    });
    }

    const curDay = new Date().getDate();
    for (let i = 1; i <= this.monthDays; i += 1) {
    let currColor = this.lvls[6];
    let dateStr = String(i);
    let isCurDay = false;
    if (i == curDay) {
    isCurDay = true; // 表示刚好是今天(该日期 和网络上的今天是同一天)
    }
    dateStr = '0' + dateStr;
    dateStr = dateStr.substr(dateStr.length - 2);
    const dataObj = dataMap[dateStr];
    if (dataObj) {
    if (dataObj.level >= 0 && dataObj.level <= 5) {
    currColor = this.lvls[dataObj.level].color;
    } else {
    currColor = this.lvls[6].color;
    }

    this.list.push({
    date: i,
    curDay: isCurDay,
    color: currColor,
    datas: dataObj,
    checkedColor: undefined, // 选中颜色
    });
    } else {
    this.list.push({
    date: i,
    curDay: isCurDay,
    color: this.lvls[6].color,
    datas: {},
    checkedColor: undefined, // 选中颜色
    });
    }
    }
    // console.log(this.list);
    } else {
    this.clearCalendar();
    }
    },
    clearCalendar() {
    this.list = [];
    for (let i = 1; i <= this.monthDays; i += 1) {
    this.list.push({
    date: i,
    color: this.lvls[6].color,
    datas: {},
    });
    }
    },

    // 处理接口返回的日历数据
    dealData(currDS) {
    const tempData = [];
    if (('dates' in currDS) && ('level' in currDS) && ('levelName' in currDS) && ('values' in currDS)) {
    if (currDS.dates.length > 0 && currDS.level.length > 0 && currDS.levelName.length > 0 && currDS.values.length > 0) {
    for (let i = 0; i < currDS.dates.length; i++) {
    const temp = {
    tstamp: currDS.dates[i],
    level: currDS.level[i],
    levelName: currDS.levelName[i],
    value: currDS.values[i],
    grade: this.loadImgType(currDS.levelName[i]),
    week: this.testDays[new Date(currDS.dates[i]).getDay()], // currDS.dates[i]: '2020-03-31'
    };
    tempData.push(temp);
    }
    // this.dealDataFinal = tempData.filter(item => item.grade>0);
    this.dealDataFinal = tempData;
    this.refreshCalendar();
    this.getDatas();
    } else {
    this.dealDataFinal = null;
    this.getDatas();
    }
    } else {
    this.dealDataFinal = null;
    this.getDatas();
    }
    },
    // 加载等级
    loadImgType(value) {
    let imgUrl = 0;
    switch (value) {
    case '优':
    imgUrl = 1;
    break;
    case '良':
    imgUrl = 2;
    break;
    case '轻':
    imgUrl = 3;
    break;
    case '中':
    imgUrl = 4;
    break;
    case '重':
    imgUrl = 5;
    break;
    case '严':
    imgUrl = 6;
    break;
    default:
    imgUrl = 0;
    break;
    }
    return imgUrl;
    },
    // (右边)区域环境质量日历
    qEQCalendar() {
    this.curYearMonth = new Date().getFullYear() + '-' + (new Date().getMonth() + 1);
    this.choseYearMonth = this.year.substr(0, 4) + '-' + this.month.substr(0, 1);
    this.calendarData = {
    dates: [
    '2020-07-01',
    '2020-07-02',
    '2020-07-03',
    '2020-07-04',
    '2020-07-05',
    '2020-07-06',
    '2020-07-07',
    '2020-07-08',
    '2020-07-09',
    '2020-07-10',
    '2020-07-11',
    '2020-07-12',
    '2020-07-13',
    '2020-07-14',
    '2020-07-15',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    ],
    level: [
    1,
    4,
    2,
    3,
    1,
    4,
    4,
    3,
    1,
    4,
    2,
    2,
    4,
    1,
    3,
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    ],
    levelName: [
    '优',
    '中度污染',
    '良',
    '轻度污染',
    '优',
    '中度污染',
    '中度污染',
    '轻度污染',
    '优',
    '中度污染',
    '良',
    '良',
    '中度污染',
    '优',
    '轻度污染',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    ],
    values: [
    '65',
    '65',
    '65',
    '65',
    '65',
    '65',
    '65',
    '65',
    '65',
    '65',
    '65',
    '65',
    '65',
    '65',
    '65',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    ],
    };
    // this.$axios.get('api/sinoyd-airquality/airquality/gis/calendar?year=' + parseInt(this.year.substr(0, 4)) + '&month=' + parseInt((this.month).replace('月', '')))
    // .then((res) => {
    // if (res.status == 200) {
    // this.calendarData = res.data.data;
    // } else {
    // this.calendarData = null;
    // }
    // }, () => {
    // this.calendarData = null;
    // });
    },
    // 设置选中之后的逻辑
    queryDeal(item) {
    if (this.isPointer) {
    console.log(item);
    // 设置选中之后的效果
    if (this.list && this.list.length) {
    const tempList = [...this.list];
    tempList.forEach((singleObj) => {
    singleObj.checkedColor = undefined;
    if (item.date === singleObj.date) {
    singleObj.checkedColor = singleObj.color;
    }
    });
    this.list = tempList;
    }
    }
    },
    },
    mounted() {
    this.qEQCalendar();
    },
    };
    </script>

    <style>
    .EntDate{
    background-color: rgba(2, 47, 79, 0.8) !important;
    border: 1px solid rgba(2, 47, 79, 0.8) !important;
    }
    .EntDate /deep/ .popper__arrow::after{
    border-bottom-color: rgba(2, 47, 79, 0.8) !important;
    }
    .EntDate /deep/ .el-scrollbar__thumb{
    background-color: rgba(2, 47, 79, 0.8) !important;
    }
    .el-select-dropdown__item.hover, .el-select-dropdown__item:hover{
    background-color: transparent !important;
    }
    </style>

    <style lang="scss" scoped>
    .r-inline{
    display: inline-block;
    }
    .right-content{
    width: 380px;
    margin: 7px;
    border-radius: 9px;
    background-color: rgba(2, 47, 79, 0.8);
    }
    .day-title {
    border-bottom: 2px solid #03596f;
    padding: 1px 0 10px;
    height: 19px;
    .day-tt {
    float: left;
    text-align: center;
    color: #ffffff;
    width: 48px;
    }
    }
    .date-item {
    float: left;
    text-align: center;
    color: #fff;
    width: 34px;
    // padding: 2px 2px;
    padding: 4px 4px;
    margin: 0px 3px;
    &.is-last-month {
    color: #7d8c8c;
    }
    .day {
    border-radius: 17px;
    padding: 3px;
    height: 25px;
    line-height: 25px;
    text-shadow: #000 0.5px 0.5px 0.5px, #000 0 0.5px 0, #000 -0.5px 0 0, #000 0 -0.5px 0;
    background-color: #173953;
    }
    }
    .calendar{
    padding: 0px 6px;
    }
    .lvls {
    padding: 0px 6px 6px 13px;
    }
    .lvl-t-item {
    float: left;
    font-size:10px;
    padding-right: 3px;
    .lvl-t-ico {
    height: 12px;
    width: 12px;
    display: inline-block;
    margin-right: 5px;
    }
    .lvl-tt {
    color: #5b5e5f;
    }
    }
    // ================================================================================================= 日期框样式
    ::v-deep .el-input__inner {
    background-color: transparent;
    border-radius: 4px;
    border: 0px solid #DCDFE6;
    color: #Fcff00;
    font-size: 19px;
    font-weight: bolder;
    }
    ::v-deep .el-select .el-input .el-select__caret {
    color: #fcff00;
    font-weight: bolder;
    }
    // ================================================================================================= 日期框的下拉框样式
    .el-select-dropdown__item{
    background-color: rgba(2, 47, 79, 0.8);
    color: white;
    &:hover{
    background-color: rgba(2, 47, 79, 0.8);
    color: #5de6f8;
    cursor: pointer;
    }
    }
    .searchBtn {
    cursor: pointer;
    width: 60px;
    height: 28px;
    display: inline-block;
    background-color: rgba(2, 47, 79, 0.8);
    color: #a0daff;
    text-align: center;
    border: 1px solid #a0daff;
    border-radius: 5px;
    margin-left: 15px;
    line-height: 28px;
    }

    .isPointer{
    cursor: pointer;
    }
    .choseDateItemI{
    border: 2px solid #00e400 !important;
    box-shadow: #00e400 0px 0px 9px 2px;
    }
    .choseDateItemII{
    border: 2px solid #ffff00 !important;
    box-shadow: #ffff00 0px 0px 9px 2px;
    }
    .choseDateItemIII{
    border: 2px solid #ff7e00 !important;
    box-shadow: #ff7e00 0px 0px 9px 2px;
    }
    .choseDateItemIV{
    border: 2px solid #ff0000 !important;
    box-shadow: #ff0000 0px 0px 9px 2px;
    }
    .choseDateItemV{
    border: 2px solid #99004c !important;
    box-shadow: #99004c 0px 0px 9px 2px;
    }
    .choseDateItemVI{
    border: 2px solid #7e0023 !important;
    box-shadow: #7e0023 0px 0px 9px 2px;
    }
    .choseDateItemVII{
    border: 2px solid #cacaca !important;
    box-shadow: #cacaca 0px 0px 9px 2px;
    }
    </style>
    作者:海的对岸
    链接:https://juejin.cn/post/7033038877485072397

    收起阅读 »

    生成 UUID 的三种方式及测速对比!

    通用唯一识别码(英语:Universally Unique Identifier,缩写:UUID)是用于计算机体系中以识别信息的一个 128 位标识符,通常表现为一串 32 位十六进制数字。 UUID 用于解决 ID 唯一的问题! 然而,如何确保唯一,这本身...
    继续阅读 »

    通用唯一识别码(英语:Universally Unique Identifier,缩写:UUID)是用于计算机体系中以识别信息的一个 128 位标识符,通常表现为一串 32 位十六进制数字。


    image.png


    UUID 用于解决 ID 唯一的问题!


    然而,如何确保唯一,这本身就是一项挑战!


    如何保证所生成 ID 只有一个副本?如何保证两个 ID 之间没有相关性?唯一性和随机性之间怎么取舍......


    (OS:看过本瓜之前写的《理解 P/NP 问题时,我产生了一种已经触碰到人类认知天花板的错觉?!》这篇文章的朋友,应该知道:或许这个世界上没有随机这个东西?任何随机都能被量子计算算清楚,上帝到底掷骰子吗?没人知道......)


    是否有真正的随机,先按下不表,


    基于目前的算力精度,现在各种 UUID 生成器和不同版本的处理方式能最大限度的确保 ID 不重复,重复 UUID 码概率接近零,可以忽略不计。


    本篇带来 3 种 UUID 生成器! 👍👍👍


    UUID


    基于 RFC4122 标准创建的 UUID,它有很多版本:v1,v2..v5;


    uuid v1是使用主机 MAC 地址和当前日期和时间的组合生成的,这种方式意味着 uuid 是匿名的。


    uuid v4 是随机生成的,没有内在逻辑,组合方式非常多(2¹²⁸),除非每秒生成数以万亿计的 ID,否则几乎不可能产生重复,如果你的应用程序是关键型任务,仍然应该添加唯一性约束,以避免 v4 冲突。


    uuid v5与 v1 v4不同,它通过提供两条输入信息(输入字符串和命名空间)生成的,这两条信息被转换为 uuid;


    特性:

    • 完善;
    • 跨平台;
    • 安全:加密、强随机性;
    • 体积小:零依赖,占用空间小;
    • 良好的开源库支持:uuid command line


    上手:


    import { v4 as uuidv4 } from 'uuid';

    let uuid = uuidv4();

    console.log(uuid) // ⇨ '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d'

    Crypto.randomUUID


    Node.js API Crypto 提供 **randomUUID()** 方法,基于 RFC 4122 V4 生成随机数;


    上手:


    let uuid = crypto.randomUUID();

    console.log(uuid); // ⇨ "36b8f84d-df4e-4d49-b662-bcde71a8764f"

    Nano ID


    Nano ID 有 3 个 api:

    1. normal (blocking); 普通
    2. asynchronous;异步
    3. non-secure;非安全

    默认情况下,Nano ID 使用符号(A-Za-z0-9-),并返回一个包含 21 个字符的 ID(具有类似于UUID v4的重复概率)。


    特性:

    • 体积小:130 bytes (压缩后);
    • 零依赖;
    • 生成更快;
    • 安全:
    • 更短,只要 21 位;
    • 方便移植,支持 20 种编程语言.


    上手:


    import { nanoid } from 'nanoid'

    let uuid = nanoid();

    console.log(uuid) // ⇨ "V1StGXR8_Z5jdHi6B-myT"

    Nano IDnpm 下载趋势:


    image.png


    测速


    我们不妨来对比以上所提 3 种生成 UUID 的方式速度差异:


    // test-uuid-gen.js
    const { v4 as uuidv4 } = require('uuid');

    for (let i = 0; i < 10_000_000; i++) {
    uuidv4();
    }

    // test-crypto-gen.js
    const { randomUUID } = require('crypto');

    for (let i = 0; i < 10_000_000; i++) {
    randomUUID();
    }

    // test-nanoid-gen.js
    const { nanoid } = require('nanoid');

    for (let i = 0; i < 10_000_000; i++) {
    nanoid();
    }

    借助 hyperfine


    调用测试:hyperfine ‘node test-uuid-gen.js’ ‘node test-crypto-gen.js’ ‘node test-nanoid-gen.js’


    运行结果:


    img


    我们可以看到, 第二种 randomUUID() 比第三种 nanoid 快 4 倍左右,比第一种 uuid 快 12 倍左右~



    作者:掘金安东尼
    链接:https://juejin.cn/post/7033221241100042271

    收起阅读 »

    老板:你来弄一个团队代码规范!?

    一、背景 9月份换了一个新部门,部门成立时间不长,当时组内还没有统一的代码规范(部分工程用了规范,部分没有,没有统一的收口) 小组的技术栈框架有Vue,React,Taro,Nuxt,用Typescript,算是比较杂了,结合到部门后续还可能扩展其他技术栈,我...
    继续阅读 »

    一、背景


    9月份换了一个新部门,部门成立时间不长,当时组内还没有统一的代码规范(部分工程用了规范,部分没有,没有统一的收口)


    小组的技术栈框架有VueReactTaroNuxt,用Typescript,算是比较杂了,结合到部门后续还可能扩展其他技术栈,我们从0-1实现了一套通用的代码规范


    到现在小组内也用起来几个月了,整个过程还算是比较顺利且快速,最近得空分享出来~


    ⚠️本篇文章不会讲基础的具体的规范,而是从实践经验讲怎么制定规范以及落地规范


    image.png


    二、为什么要代码规范


    就不说了...大家懂的~
    image.png


    不是很了解的话,指路


    三、确定规范范围


    首先,跟主管同步,团队需要一个统一的规范,相信主管也在等着人来做起来


    第一步收集团队的技术栈情况,确定规范要包括的范围


    把规范梳理为三部分ESLintStyleLintCommitLint,结合团队实际情况分析如下



    • ESLint:团队统一用的TypeScript,框架用到了VueReactTaro、还有Nuxt

    • StyleLint:团队统一用的Less

    • CommitLint:git代码提交规范


    image.png
    当然,还需考虑团队后续可能会扩展到的技术栈,以保证实现的时候确保可扩展性


    四、调研业内实现方案


    常见以下3种方案




    1. 团队制定文档式代码规范,成员都人为遵守这份规范来编写代码



      靠人来保证代码规范存在不可靠,且需要人为review代码不规范,效率低





    2. 直接使用业内已有成熟规范,比如css使用StyleLint官方推荐规范stylelint-config-standard、stylelint-order,JavaScript使用ESLint推荐规范eslint:recommended等



      a) 开源规范往往不能满足团队需求,可拓展性差; b) 业内提供的规范都是独立的(stylint只提供css代码规范,ESLint只提供JavaScript规范),是零散的,对于规范初始化或升级存在成本高、不可靠问题(每个工程需要做人为操作多个步骤)





    3. 基于StyleLint、ESLint制定团队规范npm包,使用团队制定规范库



      a) 该方案解决可扩展性差的问题,但是第二点中的(b)问题依旧存在





    五、我们的技术方案


    整体技术思路图如下图,提供三个基础包@jd/stylelint-config-selling@jd/eslint-config-selling@jd/commitlint-config-selling分别满足StyleLintESLintCommitLint



    1. @jd/stylelint-config-selling包括css、less、saas(团队暂未使用到)

    2. @jd/eslint-config-selling包括Vue、React、Taro、Next、nuxt(团队暂未使用到)...,还包括后续可能会扩展到需要自定义的ESLint插件或者解析器

    3. @jd/commitlint-config-selling统一使用git


    向上提供一个简单的命令行工具,交互式初始化init、或者更新update规范


    image.png


    几个关键点


    1、用lerna统一管理包


    lerna是一个管理工具,用于管理包含多个软件包(package)的 JavaScript项目,业内已经广泛使用了,不了解的可以自己找资料看下

    项目结构如下图

    image.png


    2、三个基础包的依赖包都设置为生产依赖dependencies


    如下图,包@jd/eslint-config-selling的依赖包都写在了生产依赖,而不是开发依赖

    image.png
    解释下:
    开发依赖&生产依赖



    • 开发依赖:业务工程用的时候不会下载开发依赖中的包,业内常见的规范如standardairbnb都是写在开发依赖

      • 缺点:业务工程除了安装@jd/eslint-config-selling外,需要自己去安装前置依赖包,如eslint、根据自己选择的框架安装相关前置依赖包如使用的Vue需要安装eslint-plugin-vue...使用成本、维护升级成本较高

      • 优点:按需安装包,开发时不会安装多余的包(Lint相关的包在业务工程中都是开发依赖,所以只会影响开发时)



    • 生产依赖:业务工程用的时候会下载这些包

      • 优点:安装@jd/eslint-config-selling后,无需关注前置依赖包

      • 缺点:开发时会下载@jd/eslint-config-selling中所有写在生产依赖的包,即使有些用不到,比如你使用的是React,却安装了eslint-plugin-vue




    3、提供简单的命令行


    这个比较简单,提供交互式命令,支持一键初始化或者升级3种规范,就不展开说了


    不会的,指路中高级前端必备:如何设计并实现一个脚手架



    组里现在还没有项目模版脚手架,后续有的话需要把规范这部分融进去



    六、最重要的一点


    什么是一个好的规范?

    基本每个团队的规范都是不一样的,团队各成员都认同并愿意遵守的规范


    所以确定好技术方案后,涉及到的各个规范,下图,我们在小组内分工去制定,比如几个人去制定styleLint的,几个人制定Vue的...


    然后拉会评审,大家统一通过的规范才敲定
    image.png
    最后以开源的方式维护升级,使用过程中,遇到规范不合适的问题,提交issue,大家统一讨论确定是否需要更改规范


    写在结尾


    以上就是我们团队在前端规范落地方面的经验~


    作者:jjjona0215
    链接:https://juejin.cn/post/7033210664844066853
    收起阅读 »

    如何优雅的使用枚举功能——Constants

    背景 在项目中,或多或少的会遇到使用枚举/快码/映射/字典,它们一般长这个样子。(PS:我不知道怎么称呼这个玩意) 在一些需要展示的地方,会使用下面的代码来展示定义。 <div>{{ statusList[status] }}</div&g...
    继续阅读 »

    背景


    在项目中,或多或少的会遇到使用枚举/快码/映射/字典,它们一般长这个样子。(PS:我不知道怎么称呼这个玩意)



    在一些需要展示的地方,会使用下面的代码来展示定义。


    <div>{{ statusList[status] }}</div>

    而在代码中,又会使用下面的形式进行判断。这样写会让代码里充斥着许多的 'draft' 字符串,非常不利于管理。


    if (status === 'draft') {
    // do sth...
    }

    基于这种情况,在使用时会先声明一个变量。


    const DRAFT = 'draft'

    if (status === DRAFT) {
    // do sth...
    }

    为了应对整个项目都会使用到的情况,会这样处理。


    export const statusList = {
    draft: '草稿',
    pending: '待处理',
    }

    export const statusKeys = {
    draft: 'draft',
    pending: 'pending',
    }

    看了隔壁后端同事的代码,在 Java 里,枚举的定义及使用一般是如下形式。于是我就有了写这个工具类的想法。


    public enum Status {
    DRAFT('draft', '草稿');

    Status(String code, String name) {
    this.code = code;
    this.name = name;
    }

    public String getCode() {
    return code;
    }

    public String getName() {
    return name;
    }
    }

    public void aFunction() {
    const draftCode = Status.DRAFT.getCode();
    }

    Constants


    直接上代码


    const noop = () => {}

    class Constants {
    constructor(obj) {
    Object.keys(obj).forEach((key) => {
    const initValue = obj[key];
    if (initValue instanceof Object) {
    console.error(`Warnning: this util only support primitive values, current value is ${JSON.stringify(initValue)}`)
    // throw new Error(`Warnning: this util only support primitive values, current value is ${JSON.stringify(initValue)}`)
    }
    const newKey = `_${key}`;
    this[newKey] = initValue;
    Object.defineProperty(this, key, {
    configurable : true,
    enumerable : true,
    get: function() {
    const value = this[newKey];
    const constructorOfValue = value.constructor;
    const entry = [key, value];
    ['getKey', 'getValue'].forEach((item, index) => {
    constructorOfValue.prototype[item] = () => {
    constructorOfValue.prototype.getKey = noop;
    constructorOfValue.prototype.getValue = noop;
    return entry[index];
    }
    })
    return value;
    },
    set: function(newValue) {
    this[newKey] = newValue;
    }
    })
    });
    }
    }

    测试


    const testValues = {
    draft: '草稿',
    id: 1,
    money: 1.2,
    isTest: true,
    testObj: {},
    testArray: [],
    }
    const constants = new Constants(testValues)

    const test = (result, expect) => {
    const isExpected = result === expect
    if (isExpected) {
    console.log(`PASS: The result is ${result}`)
    } else {
    console.error(`FAIL: the result is ${result}, should be ${expect}`)
    }
    }

    test(constants.draft, '草稿')
    test(constants.draft.getKey(), 'draft')
    test(constants.draft.getValue(), '草稿')

    test(constants.id, 1)
    test(constants.id.getKey(), 'id')
    test(constants.id.getValue(), 1)

    test(constants.money, 1.2)
    test(constants.money.getKey(), 'money')
    test(constants.money.getValue(), 1.2)

    test(constants.isTest, true)
    test(constants.isTest.getKey(), 'isTest')
    test(constants.isTest.getValue(), true)
    a = 'test'
    test(a.getKey(), undefined)
    test(a.getValue(), undefined)


    作者:Wetoria
    链接:https://juejin.cn/post/7033220309386395679

    收起阅读 »