kerin

kerin 查看完整档案

南昌编辑东华理工大学  |  软件工程 编辑  |  填写所在公司/组织 kerinlin.github.io/ 编辑
编辑

前端菜鸟

个人动态

kerin 发布了文章 · 12月3日

CSS函数那些事(三)背景图片函数

DcV9V1.png

url()

url函数表示对某个资源的引用,可传入链接以及相对地址,如

background-image: url('./背景图片函数.png');
background-image: url('https://s3.ax1x.com/2020/11/29/DcV9V1.png');

image()

image功能类似于url,但是与url不同的是,image提供了一种优雅降级的能力。比如

background-image: image('a.webP','a.png','a.jpg');

这段代码意思就是,假如浏览器支持webP格式图片就应用a.webP,如果不支持,就再测试a.png,直到适配到当前浏览器。遗憾的是,这个函数目前还处于草案阶段。

DcujxK.png

所以这个函数其他的功能暂时先不关注,有兴趣的同学,可自行去 MDN 了解更多相关的信息,也可了解 最新进展

image-set()

image-set可以保证图片在不同分辨率设备上的适配,能根据不同的设备类型展示不同的图片,看下面的例子

background-image: -webkit-image-set(url(./bg1x.png) 1x , url(./bg2x.png) 2x);

这段例子意思就是在1倍屏上显示bg1x.png,在2倍屏上显示 bg2x.png 。兼容性上,目前最新主流的浏览器都已支持,对于不支持的设备可以在使用这个函数前使用background:url()来进行兼容。

Dc6Kjf.png

cross-fade()

cross-fade用于在两张叠加的背景图片上施加透明度。用法如下

background-image: -webkit-cross-fade(url('./bg1x.png'),url('./bg2x.png'),70%);

DcIRFf.png

前面两个参数为图片的资源位置,后面一个需要传入百分比,表示透明度,这个透明度是相对于最后那张图片的,比如,当百分比为0%时,此时应该只显示第一张图片

DcHhEn.png

当百分比为100%时,只显示第二张图片。

DcHoCV.png

这个属性,在firefox中完全不兼容,在chrome和safari中兼容性要好太多

Dcq5lT.png

element()

element函数可以将网站上的某部分元素当做图片使用,适用于图片的属性同时也适用于应用element的对象,使用方法

element(id)

必须传入的是id,看下面的例子,用element实现了一种类似双向绑定的功能效果

      <div class="element-wrapper">
        <span id="ele" contenteditable>hello world</span>
      </div>

      <div id="element-test"></div>

//style
      .element-wrapper{
        width: 200px;
        height: 200px;
      }
      #element-test {
        width: 200px;
        height: 200px;
        background: -moz-element(#ele);
        background-size: contain;
        background-repeat: no-repeat;
      }

效果图

DcvzeU.gif

这个属性还能做到更多有趣的效果,更多有趣的效果可去这里查看,在兼容性上,遗憾的是目前只有firefox支持这个属性

DcOLz6.png

最后

我最近在总结css函数相关的东西,系列的大纲

DQs4IO.png

这篇是系列文章的第三篇,目前已产出

项目中会包含文章中的测试代码,都做好了相应的分类,欢迎各位持续关注,有帮助话可以点个赞哦!项目地址戳这里

查看原文

赞 0 收藏 0 评论 0

kerin 发布了文章 · 11月27日

Electron+Vue从零开始打造一个本地文件翻译器

又一个愉快的周末快到了,当我脚步轻快的回到家,准备拥抱女朋友的时候,却发现,女朋友愁眉苦脸,望着电脑上一堆英文文件夹,不断的唉声叹气并嘟喃道:"好累啊"。于是,我凑近一看,只见她电脑上一堆英文文件夹,不断的重复着复制文件名,然后放百度翻译里翻译成中文,然后又把翻译后的结果给文件重命名,这也太难受了吧。想到女朋友有难,我作为她的程序猿男朋友,怎么能袖手旁观,我陷入了沉思,突然想到用Node不就能做到她手上的那些操作吗,于是说做就做,我立马把女朋友抛在身后,打开电脑开始行动,毕竟女人只会影响我拔剑的速度。

项目搭建

项目搭建依旧使用的是老组合,Electron + Vue + Node,这次就不讲怎么整合electron和vue了,具体可看 Electron+vue从零开始打造一个本地音乐播放器这篇文章。懒人可通过克隆我的模板文件直接开发,戳这里戳这里.

项目功能

明确要解决的几个痛点:

  • 要能自动翻译
  • 要能将翻译好的名字自动重命名
  • 要能批量翻译
  • 希望能操作简单,能拖拽一个目录,或拖拽文件

需求明确了,下面咱们一步一步来逐个解决。实现效果

D3CNdJ.gif

项目实现

项目实现并不复杂,逐一解决,有的放矢。

自动翻译

测试过有道翻译,百度翻译,谷歌翻译。经过对比,最终选择了百度翻译,百度翻译的通用翻译每月200万字符的免费,(QPS=10)还是很符合我的需求的。
DMVOc6.png

注册申请翻译服务

要使用翻译服务需要去百度翻译开放平台申请使用权限,选择通用翻译服务就可以了,注册成为开发者后,新建项目获取appid,这个appid很重要,决定了是否能正确发起翻译请求。

D1LVAK.png

拼接翻译API

通过查看文档可知,通用翻译API HTTP地址为

https://api.fanyi.baidu.com/api/trans/vip/translate

可携带下面这些参数

D1jFns.png

由于安全限制,需要生成签名,签名需要 按照 appid+q+salt+密钥 的顺序拼接得到字符串,然后对字符串进行md5加密

  const salt = parseInt(Math.random() * 1000000000);
  const sign = md5(globalData.appid + q + salt + globalData.key);

生成签名后拼接请求的URL

  const params = encodeURI(
    `q=${q}&from=auto&to=${globalData.to}&appid=${globalData.appid}&salt=${salt}&sign=${sign}`
  );

翻译功能代码

const md5 = require("md5");
var rp = require("request-promise");
const { globalData } = require("./config.js");

function translate(msg) {
  const q = msg;
  const salt = parseInt(Math.random() * 1000000000); //加盐
  const sign = md5(globalData.appid + q + salt + globalData.key); //生成签名
  const params = encodeURI(
    `q=${q}&from=auto&to=${globalData.to}&appid=${globalData.appid}&salt=${salt}&sign=${sign}`
  );
  const options = {
    uri: `https://fanyi-api.baidu.com/api/trans/vip/translate?${params}`,
  };
  return rp(options).then((res) => JSON.parse(res).trans_result);
}

module.exports = {
  translate,
};

主体功能实现

主体功能分为:

  • 批量翻译,支持向下递归翻译
  • 拖拽不定量文件,或者拖拽文件夹翻译
  • 重命名

批量翻译

要实现批量需要获取到目标文件夹路径,然后通过 fs.readdir 读取到目录下文件信息,遍历文件信息,如果是文件,对文件名和后缀进行分割,然后再进行翻译操作,如果是目录,就执行递归操作。

      // 读取目录中的所有文件/目录
      fs.readdir(dirPath, (err, files) => {
        if (err) {
          // throw err;
            dialog.showMessageBox({
            type: 'info',
            title: '确认',
            message: '请确认是否选择了目录',
          });
          this.loading = false;
          throw err;
        }
        files.forEach((fileItem) => {
          //遍历文件
          fs.stat(fullPath, (err, stat) => {
            if (err) {
              throw err;
            }
            // 判断是否为文件
            if (stat.isFile()) {
                //处理文件名
            } else if (stat.isDirectory()) {
              // 递归翻译
              this.startTrans(fullPath);
            }
          });
        });
      });
获取文件夹路径

获取文件夹路径有两种方式获取:

  1. 设置input webkitdirectory directory属性,然后监听change事件获取到所选择文件夹的路径

<input @change="getFile" id="attach-project-file" type="file" webkitdirectory  directory />

getFile(e) {
   this.path = e.target.files[0].path;
},
  1. 通过H5的拖拽API监听drop事件,获取到 DataTransfer 对象,DataTransfer 对象用于保存拖动并且放下的数据。
  <div class="home" @drop.prevent="addFile" @dragover.prevent>
  </div>

    addFile(e) {
      // 将伪数组转换成数组
      this.droppedFiles = [...e.dataTransfer.files];
      // 处理多个文件一起拖拽的情况
      if (this.droppedFiles.length > 1) {
        // 只有在同一个目录下才能多选,所以获取到第一个的父级目录就可以了
        this.path = path.dirname(this.droppedFiles[0].path);
        // 标记是否是多选
        this.isDropMulti = true;
      } else {
        this.path = this.droppedFiles[0].path;
      }
    },
分割文件名和后缀

由于是翻译文件名,所以需要通过 path.extname 将文件名与后缀分割开来,翻译后再将文件名重新组织,需要注意的是这里需要处理下文件名中的特殊字符,特殊字符会影响翻译结果,可能会导致翻译失败。

// 获取文件的后缀格式
const suffixName = path.extname(fileItem);

 // 获取前缀
const initSubFileName = this.removeSymbol(
    fileItem.split(suffixName)[0]
);
// 移除文件名中的特殊字符
removeSymbol(fileName) {
    const reg = /[`~_!@#$^&*%()=|{}':;',.<>\\/?~!@#¥……&*()——|{}';:""'。,、?\s]/g;
    const newFile = fileName.replace(reg, " ");
    return newFile;
},
翻译文件名

对分割好的文件名进行翻译,然后重新新的文件名称,这里要注意的是,由于百度翻译限制了(QPS=10),所以需要添加对翻译请求节流,限制其每秒不能超过10次。

节流函数

const { globalData } = require("./config.js");

const throttle = (function(delay = 1500) {
  const wait = [];
  let canCall = true;
  return function throttle(callback) {
    if (!canCall) {
      if (callback) wait.push(callback);
      return;
    }

    callback();
    canCall = false;
    setTimeout(() => {
      canCall = true;
      if (wait.length) {
        throttle(wait.shift());
      }
    }, delay);
  };
})(globalData.delay);

module.exports = {
  throttle
};

翻译后重组文件名

      throttle(() => {
        translate(initSubFileName).then(res => {
          if (res) {
            // 如果有【】保留文件名,如果没有就加上【】
            const target = this.checkName(res[0].dst);

            // 拼接带后缀的文件名
            const fullSuffixName = `${target}${suffixName}`;

            // 翻译后的文件路径
            const newPath = path.resolve(dirPath, fullSuffixName);

          } else {
            // 翻译失败
            console.log("翻译接口服务出错");
              dialog.showMessageBox({
              type: "error",
              title: "错误",
              message: "翻译接口服务出错"
            });
          }
        });
      });

重命名

重命名使用node自带的 fs.rename

            fs.rename(oldPath, newPath, err => {
              if (err) {
                dialog.showErrorBox("错误", "翻译失败,请关闭软件重试");
                this.loading = false;
                throw err;
              }
              console.log(`${initSubFileName} 已翻译成 ${fullSuffixName}`);
              this.path = `${initSubFileName} 已翻译成 ${fullSuffixName}`;
            });

最后

终于完成了,我伸了伸懒腰,我兴高采烈的准备去女朋友那邀功,不小心摔了一跤,我猛地起来,啊,还好,原来是一场梦!不对!按这么说,我的女朋友。。。。我突然伤感起来,悲伤的打开了网抑云:

生不出人,我很抱歉!

原来这一切都是假的!!!那晚,泪浸湿了我的枕头!!!源码在这

查看原文

赞 0 收藏 0 评论 0

kerin 赞了文章 · 11月26日

Github个人主页已被疯狂玩坏了,您的呢?

image.png

我们玩过Github。
也在Github上获取到不错的资源。
一些大项目也在上面可以看到最新动态。
也有些不错的工具库。
更是有很多很好的面试、文章资料。

干一杯

也很多人知道它的高级搜索功能:

1、明确搜索仓库标题、仓库描述、README
2、明确搜索 star、fork 数大于多少的
3、明确搜索仓库大小的
4、明确仓库是否还在更新维护
...
6、明确搜索仓库的语言
7、明确搜索某个人或组织的仓库

image.png

非常酷🆒o( ̄▽ ̄)d

来一杯

在GitHub个人主页可以看到一个记录每天贡献次数的面板。

image.png

玩出花样来了。

再来一杯

更有些人花样git commit message

http://whatthecommit.com/

先介绍一下:www.whatthecommit.com,一个 Git 提交日志生成网站。每次刷新的内容随机,不怕挨揍的壮士可以使用如下命令进行日常代码的提交:

git commit -m"`curl -s http://whatthecommit.com/index.txt`"

网站总结的提交记录都比较欢乐,不少内容会让码农们深有同感的会心一笑。

image.png
image.png

喝完这杯,还有...

今天讲一个更加好玩的:
在 GitHub 个人页中添加 README 介绍,用于展示更加详细的个人信息。

image.png
image.png

非常有意思。

来,动手吧,你也搞一个。
创建与 GitHub 用户名同名的代码仓库。

tips: 因为我已经创建了。
image.png

马上在你个人主页就有了:

image.png

banner
这里有个你可以参考模板:
A collection of awesome readme templates to display on your profile.
https://github.com/kautukkund...

在今年年底,估计Github会出现人传人现象。

喝继续喝

喜欢就点赞收藏,也可以去我主页star支持一下。
非常感谢Thanks♪(・ω・)ノ
大家多多相互学习。

查看原文

赞 9 收藏 4 评论 0

kerin 发布了文章 · 11月25日

CSS 函数那些事(二)你不知道的 attr()

属性函数 attr() 用于获取HTML元素里面的属性值,并用于样式中,但目前暂时只能应用于CSS元素中的伪元素。

例子

实现一个Tooltip

DU9XAf.png

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>css attr函数</title>
    <style>
      .tooltip {
        width: 100px;
        position: relative;
        margin: 0 auto;
      }
      .tooltip:hover::after {
        padding: 5px;
        position: absolute;

        /* 在伪元素中作为字符串中使用 */
        content: attr(data-tooltip);
        color: #fff;
        background-color: #000;
        border-radius: 10px;
        top: 25px;
        left: 0;
      }

      /* 箭头 */
      .tooltip:hover .arrow::after {
        content: "";
        position: absolute;
        bottom: -5px;
        left: 20%;
        margin-left: -5px;
        border-width: 5px;
        border-style: solid;
        border-color: transparent transparent black transparent;
      }

    </style>
  </head>
  <body>
    <div class="tooltip" data-tooltip="一段提示">
      Hover me
      <span class="arrow"></span>
    </div>
  </body>
</html>

语法中的实验属性(目前所有浏览器都不支持)

在新的语法中支持各种类型的CSS属性,具体支持的可查看MDN文档#Specifications "MDN文档"),举个例子,假如需要设置一个margin-top,正常是需要去找到类名然后设置,稍微图省事一点可能会集中书写css类名,然后全局引入再调用.这种写法一定程度上能方便一点,但是不够个性化,假如我要设置成上边距15px,又得重新加一个类名,还是很麻烦。

<div class="mt10"></div>

//style
.mt10{
    margin-top: 10px;
}

但是如果实验属性支持的话,可以写成这样。

<div mt="10px"></div>

//style

[mt] {
    margin-top: attr(mt,0);
}

这种写法就很类似组件开发,不需要指定特定大小的px值,在HTML元素上直接能指定任意大小的PX值,而且基于CSS,没有JS的参与,会更加轻巧。但是,很遗憾的是目前所有浏览器都不支持,估计很长一段时间内也是不支持的,这里做一下了解,提供一种组件开发的思路。幸运的是,在找资料的过程发现张鑫旭大佬已经探索过这种可能性,然后对这种特性做了 Polyfill,查看Polyfill

Polyfill attr()实验属性原理

利用CSS自定义属性传递attr的属性值

      .test-attr {
        --mbNum: attr(mb px);
        margin-bottom: var(--mbNum);
        --mlNum: attr(ml px);
        margin-left: var(--mlNum);
      }

然后获取所有包含attr()函数的自定义的属性名

    // 获取页面中所有的CSS自定义属性
    var isSameDomain = function (styleSheet) {
        if (!styleSheet.href) {
            return true;
        }

        return styleSheet.href.indexOf(window.location.origin) === 0;
    };

    var isStyleRule = function (rule) {
        return rule.type === 1;
    };

    var arrCSSCustomProps = (function () {
        return [].slice.call(document.styleSheets).filter(isSameDomain).reduce(function (finalArr, sheet) {
            return finalArr.concat([].slice.call(sheet.cssRules).filter(isStyleRule).reduce(function (propValArr, rule) {
                var props = [].slice.call(rule.style).map(function (propName) {
                    return [
                        propName.trim(),
                        rule.style.getPropertyValue(propName).trim()
                    ];
                }).filter(function ([propName]) {
                    return propName.indexOf('--') === 0;
                });

                return [].concat(propValArr, props);
            }, []));
        }, []);
    })();

打印下 arrCSSCustomProps ,得到

DUnnnU.png

最后一步是遍历Dom,如果设置了对应的自定义属性,就将通过attr定义属性值,转换成css能够解析的自定义属性值 var

    // attr()语法转换成目前CSS变量可识别的语法
    var funAttrVar2NormalVar = function (objParseAttr, valueAttr) {
        // attr()语法 attr( <attr-name> <type-or-unit>? [, <attr-fallback> ]? )
        // valueVar示意:attr(bgcolor color, deeppink)
        // valueAttr示意: 'deepskyblue'或者null

        var attrName = objParseAttr.attrName;
        var typeOrUnit = objParseAttr.typeOrUnit;

        // typeOrUnit值包括:
        // string | color | url | integer | number | length | angle | time | frequency | cap | ch | em | ex | ic | lh | rlh | rem | vb | vi | vw | vh | vmin | vmax | mm | Q | cm | in | pt | pc | px | deg | grad | rad | turn | ms | s | Hz | kHz | %

        var arrUnits = ['ch', 'em', 'ex', 'ic', 'lh', 'rlh', 'rem', 'vb', 'vi', 'vw', 'vh', 'vmin', 'vmax', 'mm', 'cm', 'in', 'pt', 'pc', 'px', 'deg', 'grad', 'rad', 'turn', 'ms', 's', 'Hz', 'kHz', '%'];

        var valueVarNormal = valueAttr;
        // 如果是string类型
        switch (typeOrUnit) {
            case 'string': {
                valueVarNormal = '"' + valueAttr + '"';
                break;
            }
            case 'url': {
                if (/^url\(/i.test(valueAttr) == false) {
                    valueVarNormal = 'url(' + valueAttr + ')';
                }
                break;
            }
        }

        // 数值变单位的处理
        if (arrUnits.includes(typeOrUnit) && valueAttr.indexOf(typeOrUnit) == -1 && parseFloat(valueAttr) == valueAttr) {
            valueVarNormal = parseFloat(valueAttr) + typeOrUnit;
        }

        return valueVarNormal;
    };

        var valueVarNormal = funAttrVar2NormalVar(objParseAttr, strHtmlAttr);

        console.log(valueVarNormal); //100px
        // 设置
        node.style.setProperty(cssProp, valueVarNormal);  // margin-bottom : 100px

objParseAttr就是 attr(mb px)解析后的对象,valueAttr就是 自定义属性的值,也就是例子中的 100

DUucGR.png

效果图

DUKjpR.png

详细的Polyfill代码请 戳这

最后

attr()加上做兼容后的实验功能很强大,非常的灵活,后面我打算整合一些常用的需要这种写法的属性,封装成npm包,方便日常应用的开发。最近在整理CSS函数的相关知识,欢迎大家持续关注,本文相关代码,戳这

查看原文

赞 0 收藏 0 评论 0

kerin 发布了文章 · 11月23日

CSS函数那些事(一)比较函数

D8vrLQ.png

CSS比较函数有三个:

  • max()
  • min()
  • clamp()

min与max

CSS min,max函数作用类似于js函数中的min,max,用于取多个属性中的最小值或者最大值,属性之间用逗号分隔。例子如下

      width: min(100px,200px,300px); //取值100px
      height: max(100px,200px,300px); //取值300px

D8mqOA.png

如图,宽度取了最小值100px,高度取了最大值300px.

clamp

clamp函数需要传入3个参数,一个最小值,一个默认值,一个最大值,用于处理边界值,当默认值大于最大值时,取最大值,小于最小值时,取最小值,介于最小与最大之间时,取默认值。

使用方法

clamp(MIN,DEFAULT,MAX)

clamp就相当于max(MIN,min(DEFAULT,MAX))

案例

font-size: clamp(20px,10vw,40px);

分析下,当10vw小于20px,也就是页面宽度小于等于200px时,字体最小为20px,当10vw大于40px,也就是页面宽度大于等于400px时,字体最大为40px.处于200px-400px之间的,则按照 width/10的计算公式进行计算,下面验证一下

小于200px

D83xzV.png

大于400px

D882OU.png

200px到400px之间

D8GSfI.png

兼容性

D8JVKK.png

可以看出这3个函数都是最近不久才出来的,所以兼容性不太好,国产浏览器全挂,主流浏览器最新的版本基本能够支持,这是个好事,因为这三个数学在响应式开发中的作用还是很明显的,未来或许这3个函数在响应式开发中的比重会慢慢的得到提升。

常用的使用场景

下面会列举几个常用的使用场景

侧边栏响应

对于侧边栏布局,需要侧边栏固定宽度,做响应式时可以考虑超过最大宽度时通过vw来固定侧边栏的占比

      aside {
        background: #ccc;
        flex-basis: max(30vw, 150px);
      }
      main {
        background: #09acdd;
        flex-grow: 1;
      }

D8om1s.gif

字体响应

通过clamp限制最大最小值,然后中间的默认值根据视窗改变

font-size: clamp(20px, 10vw, 40px);

渐变平滑过渡

渐变指定渐变的梯度线,按照一般操作会出现过渡不够平滑的情况,在移动端会有一条明显的过渡线

background: linear-gradient(135deg, #2c3e50, #2c3e50, #3498db);

D8XXA1.png

利用min修改一下,过渡会更加平滑一点

background: linear-gradient(135deg, #2c3e50, #2c3e50 min(20vw, 60%), #3498db);

D8jiBd.png

动态容器宽度

在实际运用中,比如如果我们想在桌面端限定宽度,在移动端显示100%,需要这样写

    .container{
      width: 1440px;
      max-width: 100%;
    }

现在只需要

    .container{
      width: min(1440px,100%);
    }

非常简洁明了。

总结

这3个函数适用于响应式布局的开发,在不需要考虑兼容性问题的情况下可以酌情使用,但如果要考虑兼容性,还是最好不要使用。我最近在总结css函数相关的东西,欢迎各位持续关注,源码在这,戳这里戳这里

查看原文

赞 0 收藏 0 评论 0

kerin 发布了文章 · 11月15日

一起来看Javascript的垃圾回收机制

JS的垃圾回收机制

JS会在创建变量时自动分配内存,在不使用的时候会自动周期性的释放内存,释放的过程就叫 "垃圾回收"。这个机制有好的一面,当然也也有不好的一面。一方面自动分配内存减轻了开发者的负担,开发者不用过多的去关注内存使用,但是另一方面,正是因为因为是自动回收,所以如果不清楚回收的机制,会很容易造成混乱,而混乱就很容易造成"内存泄漏".由于是自动回收,所以就存在一个 "内存是否需要被回收的" 的问题,但是这个问题的判定在程序中意味着无法通过某个算法去准确完整的解决,后面探讨的回收机制只能有限的去解决一般的问题。

回收算法

垃圾回收对是否需要回收的问题主要依赖于对变量的判定是否可访问,由此衍生出两种主要的回收算法:

  • 标记清理
  • 引用计数

标记清理

标记清理是js最常用的回收策略,2012年后所有浏览器都使用了这种策略,此后的对回收策略的改进也是基于这个策略的改进。其策略是:

  1. 变量进入上下文,也可理解为作用域,会加上标记,证明其存在于该上下文;
  2. 将所有在上下文中的变量以及上下文中被访问引用的变量标记去掉,表明这些变量活跃有用;
  3. 在此之后再被加上标记的变量标记为准备删除的变量,因为上下文中的变量已经无法访问它们;
  4. 执行内存清理,销毁带标记的所有非活跃值并回收之前被占用的内存;

过程

局限

  • 由于是从根对象(全局对象)开始查找,对于那些无法从根对象查询到的对象都将被清除
  • 回收后会形成内存碎片,影响后面申请大的连续内存空间

引用计数

引用计数策略相对而言不常用,因为弊端较多。其思路是对每个值记录它被引用的次数,通过最后对次数的判断(引用数为0)来决定是否保留,具体的规则有

  • 声明一个变量,赋予它一个引用值时,计数+1;
  • 同一个值被赋予另外一个变量时,引用+1;
  • 保存对该值引用的变量被其他值覆盖,引用-1;
  • 引用为0,回收内存;

局限

最重要的问题就是,循环引用 的问题

function refProblem () {
    let a = new Object();
    let b = new Object();
    a.c = b;
    b.c = a;  //互相引用
}

根据之前提到的规则,两个都互相引用了,引用计数不为0,所以两个变量都无法回收。如果频繁的调用改函数,则会造成很严重的内存泄漏。

Nodejs V8回收机制

V8的回收机制基于 分代回收机制 ,将内存分为新生代(young generation)和老生代(tenured generation),新生代为存活时间较短的对象,老生代为存活时间较长或者常驻内存的变量。

新生代老生代

V8堆的构成

V8将堆分成了几个不同的区域

堆

  • 新生代(New Space/Young Generation): 大多数新生对象被分配到这,分为两块空间,整体占据小块空间,垃圾回收的频率较高,采用的回收算法为 Scavenge 算法
  • 老生代(Old Space/Old Generation):大多数在新生区存活一段时间后的对象会转移至此,采用的回收算法为 标记清除 & 整理(Mark-Sweep & Mark-Compact,Major GC) 算法,内部再细分为两个空间

    • 指针空间(Old pointer space): 存储的对象含有指向其他对象的指针
    • 数据空间(Old data space):存储的对象仅包含数据,无指向其他对象的指针
  • 大对象空间(Large Object Space):存放超过其他空间(Space)限制的大对象,垃圾回收器从不移动此空间中的对象
  • 代码空间(Code Space): 代码对象,用于存放代码段,是唯一拥有执行权限的内存空间,需要注意的是如果代码对象太大而被移入大对象空间,这个代码对象在大对象空间内也是拥有执行权限的,但不能因此说大对象空间也有执行权限
  • Cell空间、属性空间、Map空间 (Cell ,Property,Map Space): 这些区域存放Cell、属性Cell和Map,每个空间因为都是存放相同大小的元素,因此内存结构很简单。

Scavenge 算法

Scavenge 算法是新生代空间中的主要算法,该算法由 C.J. Cheney 在 1970 年在论文 A nonrecursive list compacting algorithm 提出。
Scavenge 主要采用了 Cheney算法,Cheney算法新生代空间的堆内存分为2块同样大小的空间,称为 Semi space,处于使用状态的成为 From空间 ,闲置的称为 To 空间。垃圾回收过程如下:

  • 检查From空间,如果From空间被分配满了,则执行Scavenge算法进行垃圾回收
  • 如果未分配满,则检查From空间的是否有存活对象,如果无存活对象,则直接释放未存活对象的空间
  • 如果存活,将检查对象是否符合晋升条件,如果符合晋升条件,则移入老生代空间,否则将对象复制进To空间
  • 完成复制后将From和To空间角色互换,然后再从第一步开始执行

晋升条件

  1. 经历过一次Scavenge 算法筛选;
  2. To空间内存使用超过25%;

晋升

标记清除 & 整理(Mark-Sweep & Mark-Compact,Major GC)算法

之前说过,标记清除策略会产生内存碎片,从而影响内存的使用,这里 标记整理算法(Mark-Compact)的出现就能很好的解决这个问题。标记整理算法是在 标记清除(Mark-Sweep )的基础上演变而来的,整理算法会将活跃的对象往边界移动,完成移动后,再清除不活跃的对象。

整理过程

由于需要移动移动对象,所以在处理速度上,会慢于Mark-Sweep。

全停顿(Stop The World )

为了避免应用逻辑与垃圾回收器看到的逻辑不一样,垃圾回收器在执行回收时会停止应用逻辑,执行完回收任务后,再继续执行应用逻辑。这种行为就是 全停顿,停顿的时间取决与不同引擎执行一次垃圾回收的时间。这种停顿对新生代空间的影响较小,但对老生代空间可能会造成停顿的现象。

增量标记(Incremental Marking)

为了解决全停顿的现象,2011年V8推出了增量标记。V8将标记过程分为一个个的子标记过程,同时让垃圾回收标记和JS应用逻辑交替进行,直至标记完成。

增量标记

内存泄漏

内存泄漏的问题难以察觉,在函数被调用很多次的情况下,内存泄漏可能是个大问题。常见的内存泄漏主要有下面几个场景。

意外声明全局变量

function hello (){
    name = 'tom'
}
hello();

未声明的对象会被绑定在全局对象上,就算不被使用了,也不会被回收,所以写代码的时候,一定要记得声明变量。

定时器

let name = 'Tom';
setInterval(() => {
  console.log(name);
}, 100);

定时器的回调通过闭包引用了外部变量,如果定时器不清除,name会一直占用着内存,所以用定时器的时候最好明白自己需要哪些变量,检查定时器内部的变量,另外如果不用定时器了,记得及时清除定时器。

闭包

let out = function() {
  let name = 'Tom';
  return function () {
    console.log(name);
  }
}

由于闭包会常驻内存,在这个例子中,如果out一直存在,name就一直不会被清理,如果name值很大的时候,就会造成比较严重的内存泄漏。所以一定要慎重使用闭包。

事件监听

mounted() {
window.addEventListener("resize",  () => {
    //do something
});
}

在页面初始化时绑定了事件监听,但是在页面离开的时候未清除事监听,就会导致内存泄漏。

最后

文章为参考资料总结的笔记文章,我最近在重学js,会将复习总结的文章记录在Github,戳这, 有想一起复习的小伙伴可一起参与复习总结!

参考资料

  1. 有意思的 Node.js 内存泄漏问题
  2. js 垃圾回收机制
  3. A tour of V8: Garbage Collection
  4. JS探索-GC垃圾回收
  5. JavaScript内存管理
  6. JavaScript高级程序设计(第4版)
查看原文

赞 7 收藏 4 评论 0

kerin 赞了文章 · 11月13日

ES6-ES10知识整合合集

目录

  • ECMAScript
  • ES2015
  • 新特性的分类
  • ES6-ES10学习版图
  • 基本语法链接整合

历经两个月,终于把所有的ES6-ES10的知识点都发布完成了,这里进行一个小的整合,以后方便查阅资料用。
这些东西打印出来A4纸也有120多页,一本小书的样子( ̄▽ ̄)/

有些东西遇到了网上查和自己整理一遍感觉还是不一样的,也希望自己可以慢慢有一种写作整理的习惯。语法是基础,还是要整体过一遍的,遇到了之后再查,心里没数都不知道从哪里查起。所以将每个部分做了个分类展示,这样查起来也好查✧(^_-✿

还是要对ECMAScript进行一下知识整理的

ECMAScript

ECMAScript通常看做JavaScript的标准化规范,实际上JavaScriptECMAScript的扩展语言,ECMAScript只是提供了最基本的语法。

每个前端人烂熟于心的公式:

JavaScript = ECMAScript + BOM + DOM

ES2015

  • 2015年开始保持每年一个版本的迭代,并且开始按照年份命名。
  • 相比于ES5.1的变化比较大
  • 自此,标准命名规则发生变化
  • ES6泛指是2015年之后的所有新标准,特指2015年的ES版本,以后要看清楚是特指还是泛指

新特性的分类

  • 解决原有语法上的一些问题或者不足
  • 对原有语法进行增强
  • 全新的对象、全新的方法、全新的功能
  • 全新的数据类型和数据结构

ES6-ES10学习版图

ES6-ES10学习版图

基本语法链接整合

ES6

ES7

ES8

ES9

ES10

查看原文

赞 65 收藏 56 评论 2

kerin 发布了文章 · 11月5日

Vue + Amap咸鱼翻身记之今天吃什么

为什么要做

人的一生中三个终极的哲学问题:

  • 早饭吃什么
  • 中饭吃什么
  • 晚饭吃什么

这个问题不知道困扰了多少像我一样的哲学家(不要脸),每天工作到一半,就在思考这哲学问题。鲁迅说过:

不知道每天吃什么的人跟咸鱼有什么区别 !

Bg2V7q.jpg

于是,为了不当咸鱼,秉着要当就要当最最香的烤鱼,呸,要当就当最靓的鲤鱼的信念下,就有了让程序帮我选今天吃什么的想法。下面说下具体的实现过程及原理。

咸鱼翻身记-实现

实现的过程不复杂,主要是利用高德地图的api获取商家数据,然后再对数据做进一步的处理。

实现原理

利用高德地图的定位功能找到自己所处位置,然后通过搜索附近商家功能,搜集到附近一定范围内的商家的信息,然后随机挑选一家餐饮店,完成咸鱼翻身把歌唱。

实现准备

  1. 需要去高德开放平台,申请使用权限,然后去控制台新建应用,申请使用的key,目前我使用的是免费版,每天有3万次的调用限制,不过对于我这个玩具应用来说,已经足够了,具体的申请过程就不再赘述了。
  2. 一颗宁可饿死都不做咸鱼的心

实现过程

通过查阅高德地图的资料,找到需要配置的参数,了解相关参数的含义以及可配置范围。

实现当前定位

需要调用高德地图的api,在页面中引入,一定要在head标签中引入,不然会报错

    <script
      type="text/javascript"
      data-original="https://webapi.amap.com/maps?v=1.4.15&key=你自己申请的key"
    ></script>

然后在vue.config.js中添加配置

module.exports = {
  configureWebpack: {
    externals: {
      AMap: 'AMap'
    }
  }
};

这样就完成插件的引入了,开始定位,获取到自己当前所处位置的经度纬度。

// 获取当前位置
getLocation() {
  const that = this;
  AMap.plugin('AMap.Geolocation', () => {
    var geolocation = new AMap.Geolocation({
      // 是否使用高精度定位,默认:true
      enableHighAccuracy: true,
      timeout: 10000,
    });

    geolocation.getCurrentPosition();
    AMap.event.addListener(geolocation, 'complete', (data) => {
      // console.log('纬度:', data.position.lat);
      // console.log('经度:', data.position.lng);
      that.lat = data.position.lat;
      that.lng = data.position.lng;

      // 查询店铺信息
      this.getData();
    });
    AMap.event.addListener(geolocation, 'error', (err) => {
      console.log(err);
      console.log('获取定位失败');
    });
  });
},

高德地图的定位在默认情况下,PC 端优先使用精确 IP 定位,IP定位失败后使用浏览器定位;手机端优先使用浏览器定位,失败后使用IP定位。这里要注意的是,由于Chrome、IOS10等已不再支持非安全域的浏览器定位请求,需要将站点升级到https才能保证定位的精确度。

获取店铺信息

完成定位后需要拼接请求的api来获取到商家数据。

    // 获取附近店铺信息
    getData() {
      const url = `https://restapi.amap.com/v3/place/around?这里跟具体的参数信息,参数配置可查高德文档`;
      axios(url)
        .then((res) => {
          // console.log(res.data.pois);
          this.fullShopData = res.data.pois;
        })
        .catch((err) => {
          console.log(err);
          console.log('获取店铺信息失败');
        });
    },

然后获取随机数据。

随机抽取店铺

通过获取到店铺信息的数组的长度,然后在 [0-数组长度)之间生成一个随机数,这个随机数就是当前所有店铺信息中被选中的天之骄子,也就是我们今天要去吃饭的店铺。

    // 随机抽取一家店铺
    randomShop() {
      if (!this.fullShopData || this.fullShopData.length === 0) {
        // console.log('附近无店铺或者获取店铺数据失败');
        this.$Notice.info({
          title: '附近无店铺信息,吃点正常的东西吧!',
        });
        return false;
      }
      const total = this.fullShopData.length;  //数据长度
      const randomIndex = this.random(0, total); // 随机数组下标
      this.shopName = this.fullShopData[randomIndex].name; //店名
      this.distance = this.fullShopData[randomIndex].distance; //距离
      this.address = this.fullShopData[randomIndex].address; //地址
      this.isChose = true;
    },

这样,一个最小可用的版本已经做好了。但是有时候自己又有点想法,不想做程序的奴隶,于是就需要对随机的范围进行更加精确的配置。

配置项开发

配置项主要有对类别,搜索半径,关键词的配置,具体没什么好说的,主要是对接口参数的重新赋值,这里拿一个数据联动的例子说一下,在选择想要吃的餐馆类别中,需要选中一个大类,再对他的子类进行挑选。这里的分类是基于高德地图的 POI分类编码,进行分类的,根据以上的需求信息,构造一个符合需求的数据结构。

    {
      name: '咖啡厅',
      value: '050500',
      types: [
        {
          name: '星巴克咖啡',
          value: '050501'
        },
        {
          name: '上岛咖啡',
          value: '050502'
        },
        {
          name: 'Pacific Coffee Company',
          value: '050503'
        },
        {
          name: '巴黎咖啡店',
          value: '050504'
        }
      ]
    },

然后通过监测第一个大类的变动,获取到当前的选中项的value,然后再跟所有的大类数据进行筛选甄别,找到当前项的types,再把当前项的types作为子类的数据来源。

<Select @on-select="selectFirstType" v-model="formData.firstType" placeholder="请选择一个大类">
    <Option v-for="(item, index) in typeList" :value="item.value" :key="index">{{ item.name }}</Option>
</Select>
    // 筛选大类
    selectFirstType(val) {
      // 筛选出当前选中项的数据,方便取子项目
      const result = this.typeList.filter((item) => item.value === val.value);
      this.restaurantList = result[0].types;
      this.apiOption.types = val.value;
      this.disableChose = false;
    },
项目部署

项目是通过netlify部署的,netlify非常适合快速部署一些静态网站,只需要把自己build好的dist包上传至netlify上的个人主页,再配置下域名,就能完成发布部署,非常轻量,可配置项也特别多。这是这个项目的线上版本 https://createforyou.netlify.app/#/

咸鱼翻身记-最后

突然又想到一个场景,当可爱的程序猿小张跟女生小莉出去约会的时候。

程序猿小张: 你想吃什么?
女生小莉:随便。

如果小张有选择困难症,听到随便两个字,心中是啥状态?所以这个时候你就可以发挥你的程序猿本色,现场给他撸一个随机选择程序,然后吃饭的时候跟她讲你撸程序的过程,我相信,你们一定会度过一个快乐的时光的。

光棍节快乐,赶紧跑,小命要紧(狗头)!

源码在这 Gayhub , 最后附上效果图

GIF.gif

查看原文

赞 1 收藏 0 评论 0

kerin 发布了文章 · 10月27日

Electron+vue从零开始打造一个本地播放器

BCM9HI.png

为什么要做?

女朋友工作是音频后期,平常会收集一些音频音乐,需要看音频的频谱波形,每次用au这种大型软件播放音乐看波形,很不方便,看到她这么辛苦,身为程序猿的我痛心疾首,于是,就有了这么一个小软件,软件涉及到的技术主要为electron,vue,node,波形的展示主要通过wavesurfer生成。

从零开始-搭建项目

项目通过vue脚手架搭建的,所以需要安装cli工具,如果已经装了,可以跳过这一步.

npm install -g @vue/cli
# OR
yarn global add @vue/cli

装好后,通过脚手架搭建项目

vue create music

vue需要与electron集成,这里社区已经有比较成熟的vue插件了,Vue CLI Plugin Electron Builder

vue add electron-builder

懒人可以直接去clone我的搭建好得架子直接开发, 戳这里

从零开始-项目开发

首先先明确下这个播放器的功能需求,主要有这几个

  • 不添加文件目录,加载任意的本地文件系统内的音频文件,直接调用播放器播放
  • 前一首后一首功能
  • 声音音量控制
  • 自定义软件窗口

如何关联播放

如何实现关联播放?因为对electron不是很熟,查了很久electron的资料,终于找到了配置项,需要配置fileAssociations

        fileAssociations: [
          {
            ext: ["mp3", "wav", "flac", "ogg", "m4a"],
            name: "music",
            role: "Editor"
          }
        ],

配置好后,通过electron的open-file事件,获取打开的音频文件的本地路径。对于windows,需要通过process.argv,来获取文件路径。

const filePath = process.argv[1];

如何加载本地音频文件

上一步通过配置拿到文件的本地路径后,下一步就是通过路径读取音频文件的信息。由于音频的插件无法解析绝对路径,所以需要通过node的文件系统,通过fs.readFileSync读取到文件的buffer信息。

let buffer = fs.readFileSync(diskPath); //读取文件,并将缓存区进行转换

读取后需要将buffer转换成node可读流

const stream = this.bufferToStream(buffer);//将buffer数据转换成node 可读流

转换方法 bufferToStream

    bufferToStream(binary) {
      const readableInstanceStream = new Readable({
        read() {
          this.push(binary);
          this.push(null);
        }
      });
      return readableInstanceStream;
    }

转换成流后需要将音频流转换成blob对象来加载,实现方法

module.exports = streamToBlob

function streamToBlob (stream, mimeType) {
  if (mimeType != null && typeof mimeType !== 'string') {
    throw new Error('Invalid mimetype, expected string.')
  }
  return new Promise((resolve, reject) => {
    const chunks = []
    stream
      .on('data', chunk => chunks.push(chunk))
      .once('end', () => {
        const blob = mimeType != null
          ? new Blob(chunks, { type: mimeType })
          : new Blob(chunks)
        resolve(blob)
      })
      .once('error', reject)
  })
}

转blob

  let fileUrl; // blob对象
  streamToBlob(stream)
    .then(res => {
      fileUrl = res;
      // console.log(fileUrl);

      //将blob对象转成blob链接
      let filePath = window.URL.createObjectURL(fileUrl);
      // console.log(filePath);
      this.wavesurfer.load(filePath);

      // 自动播放
      this.wavesurfer.play();
      this.playing = true;
    })
    .catch(err => {
      console.log(err);
    });

这样就实现了加载本地文件播放了

上一首下一首功能

这里的上一首下一首的功能是基于上面获取到的文件的绝对路径,通过node的path模块,path.dirname获取到文件的父级目录。

const dirPath = path.dirname(diskPath);

然后通过fs.readdir读取目录下所有文件,会返回一个文件名数组,找到该目录下正在播放的文件的下标,通过数组下标判断前一首和后一首歌曲的名称,然后再组装成绝对路径,读取资源播放

    playFileList(diskPath, pos) {
      let isInFiles;
      let fileIndex;
      let preIndex;
      let nextIndex;
      let fullPath;
      let dirPath = path.dirname(diskPath);
      let basename = path.basename(diskPath);
      fs.readdir(dirPath, (err, files) => {
        isInFiles = files.includes(basename);

        if (isInFiles && pos === "pre") {
          fileIndex = files.indexOf(basename);
          preIndex = fileIndex - 1;
          fullPath = path.resolve(dirPath, files[preIndex]);

          this.loadMusic(fullPath);
        }
        if (isInFiles && pos === "next") {
          fileIndex = files.indexOf(basename);
          nextIndex = fileIndex + 1;
          fullPath = path.resolve(dirPath, files[nextIndex]);
          this.loadMusic(fullPath);
        }
      });
    },

声音音量控制

音量控制需要通过监听input的键入事件,获取到range的值,然后通过设置样式background-image,动态计算百分比,然后调用wavesurfer的setVolume方法调节音量

:style="`background-image:linear-gradient( to right, ${fillColor}, ${fillColor} ${percent}, ${emptyColor} ${percent})`"

改变音量changeVol事件

    changeVol(e) {
      let val = e.target.value;
      let min = e.target.min;
      let max = e.target.max;
      let rate = (val - min) / (max - min);
      this.percent = 100 * rate + "%";
      console.log(this.percent, rate);
      this.wavesurfer.setVolume(Number(rate));
    },

自定义标题栏

个人觉得系统自带的菜单栏太丑了,就给设置了无边框再自己加上最小化,关闭的功能。最小化,关闭是通过ipc通信,渲染进程监听到有点击操作后,通知主进程进行相应的操作。

渲染进程

    close() {
      ipcRenderer.send("close");
    },
    minimize() {
      ipcRenderer.send("minimize");
    }

主进程

ipcMain.on("close", () => {
  win.close();
  app.quit();
});

ipcMain.on("minimize", () => {
  win.minimize();
});

打开多个实例的问题

在实际测试的过程中发现会出现,打开一首新的音乐播放,就会出现重新开一个实例的现象,不能实现覆盖播放,后面查阅资料发现electron有一个second-instance事件,可以监听是否打开了第二个实例。当第二个实例被执行并且调用 app.requestSingleInstanceLock()") 时,这个事件将在应用程序的首个实例中触发,并且会返回第二个实例的相关信息,然后通过主进程通知渲染进程,告知渲染进程第二个实例的本地绝对路径,渲染进程接收到信息后,立马加载第二个实例的资源。app.requestSingleInstanceLock(),表示应用程序实例是否成功取得了锁。 如果它取得锁失败,可以假设另一个应用实例已经取得了锁并且仍旧在运行,所以可以直接关闭掉,这样就避免了打开多个实例的问题

主进程

const gotTheLock = app.requestSingleInstanceLock();
if (gotTheLock) {
  app.on("second-instance", (event, commandLine, workingDirectory) => {
    // 监听是否有第二个实例,向渲染进程发送第二个实例的本地路径
    win.webContents.send("path", `${commandLine[commandLine.length - 1]}`);
    if (win) {
      if (win.isMinimized()) win.restore();
      win.focus();
    }
  });

  app.on("ready", async () => {
    createWindow();
  });
} else {
  app.quit();
}

渲染进程

  ipcRenderer.on("path", (event, arg) => {
    const newOriginPath = arg;

    // console.log(newOriginPath);
    this.loadMusic(newOriginPath);
  });

自动更新

需求的起因是,在很兴奋的给女朋友成品的时候,尴尬的被女朋友试出很多bug(捂脸ing),然后频繁的修改打包,然后通过私发传给她。特别麻烦,所以这个需求很急迫。最后查了资料,通过electron-updater实现了这个需求.

安装electron-updater

yarn add electron-updater

发布设置

    electronBuilder: {
      builderOptions: {
        publish: ['github']
      }
    }

主进程监听

autoUpdater.on("checking-for-update", () => {});
autoUpdater.on("update-available", info => {
  dialog.showMessageBox({
    title: "新版本发布",
    message: "有新内容更新,稍后将重新为您安装",
    buttons: ["确定"],
    type: "info",
    noLink: true
  });
});

autoUpdater.on("update-downloaded", info => {
  autoUpdater.quitAndInstall();
});

生成Github Access Token
因为是用github作为更新站,所以本地需要相应的操作权限,去这里生成token,戳这,生成后,在powershell中设置

[Environment]::SetEnvironmentVariable("GH_TOKEN","<YOUR_TOKEN_HERE>","User")
# 例如 [Environment]::SetEnvironmentVariable("GH_TOKEN","sdfdsfgsdg14463232","User")

打包上传Github

yarn electron:build -p always

完成上面步骤后软件会自动上传打包后的文件到release,然后编辑下release就可以直接发布了,软件是基于版本号更新的,所以记得一定要改版本号

从零开始-结束

作为程序猿最开心的事莫过于得到女朋友的夸奖,虽然这是一个小程序,实现难度也不高,但是最后做出最小可用的版本呈现在女朋友面前的时候,看到女盆友感动的眼神,我想,这应该是我作为程序猿唯一感到欣慰的时候。软件还有很多可以改进的地方,源码在此,戳这里Github

查看原文

赞 20 收藏 13 评论 19

kerin 赞了文章 · 7月22日

我入职腾讯啦

蚂蚁金服上市的消息我想已经席卷了大家的朋友圈了,我也第一时间慰问了我所有蚂蚁的朋友,有期权的小伙伴都在估算自己变现后数字了,确实有很多老阿里人有财富自由的机会了,也有P7以下没期权苦恼的小伙伴。

我身边其实有很多偏高层的朋友,但是他们最多也就是年薪百万,还没到财富自由的地步,那我去哪里找财富自由的大佬呢?

我第一时间想到的就是广东这个魔幻的城市,我身边的,三歪、鸡蛋都是广东人,还有帅地等我认识的很多财富自由的自媒体大佬都在广东,他们都有一种特殊的商业气息。

认识帅地也很久了,在他没入职腾讯前就认识了,就是跟认识的时候两个人也可以聊很久,聊很多有意思的事情,特别是写文章相关的,我、三歪、帅地、JavaGuide都是同龄人,所以我们的共同话题还算多,我和他打算叫广州几个大佬聚一下从他们汲取一点经验,所以这个周末我就出发去找他了。

这个需要提一下的就是去机场一件很魔幻的事情,我居然遇到了高中的同学,我带着口罩她还是认出了我,两个人互相寒暄,仿佛又回到了曾经那个懵懂的高中时代,回到了高中那种前后桌说悄悄话的时刻,一路上她载着我笑着聊过去,为了多聊会还多带着我兜了一圈,可惜美好的时光总是短暂的,我还是忍住眼角的泪水跟她说了声再见。

更魔幻的是,上了飞机后,因为我坐在空姐那种凳子的旁边,一路上就跟空乘小姐姐和小哥哥聊了一路,她们知道我是程序员的时候很惊讶,说:程序员原来不都是穿格子衬衣秃头的呀,我通过跟她们的聊天也知道了,原来他们也不是一天都在天上飞的,飞个来回就回去休息了。

职业的偏见就因为一次美好的偶遇被化解了,回去他们肯定会告诉别人程序员不秃头不穿格子衬衣,我也会告诉大家,他们也不是整天都在飞,休息时间也还是挺多的。

在愉快的交谈中时间又过得很快,尽管十分不舍,但是还是得说再见,我也只能通过一张照片去记录下这段短暂的时光,她说:下次去深圳还坐这一班吧,我说:好。

一落地就被帅地带去吃潮汕火锅了,还挺好吃的哈哈,两个人像网友见面一样,无话不聊,从大学到毕业后的种种,从自媒体到他腾讯的趣事,我和他其实很像,我们都像是被命运眷顾的人,他在csdn有两篇顶流的博文,我也在有些博客有还算不错的数据,都是靠某些机遇才走到现在的,他作为程序员的顶流博主也是我一直学习的榜样。

我两都是96年,两个人也都是来自很远的农村,他父母跟我爸妈一样在种地,他也出钱修了家里的房子,命运如此相似的两个逗比就这样在深圳相遇了。

吃完我们去酒店,他选的酒店就在他公司附近,窗外能看到腾讯的大楼,放了东西就出发去广州了,因为几个财富自由的大佬才是我们此行的目的。

不知道大家都认出来其他三个财富自由的大佬没,我看我周末推文还是有很多眼见的朋友全部认出来的,从左到右分别是:良许、敖丙、帅地、吴师兄、uzi(发哥,讲真的发哥是不是很像UZI,我看照片的时候我都惊呆了,真的超级像有木有)。

和大佬们相处的时间聊了很多,其实做自媒体能做到自由职业是需要很多努力和机遇的,在他们身上我也发现了大家各自的闪光点,有很用心对待每一个内容的良许,有很会利用资源的发哥,程序员里面微博粉丝最多的可能就是他,95年已经是几百号员工的老板了,还有很有想法的吴师兄,跟他聊天发现很多观点总是不谋而合。

大家都是流量时代的宠儿,也在这个时代因为热爱做着自己喜欢的事情,他们无论是学历还是阅历都很优秀,哈工大的研究生不想写代码,因为喜欢摄影和视频选择了自由职业全职拍视频你敢信?希望大家都越来越好,带着敖丙也财富自由一把哟哈哈。

我和帅地是聚餐里面刚入社会的仔,我们更多的是倾听大家给我们带来的经验分享,大家也很聊得来,可能因为做着一样的事情,有一样的交友圈,有特别多的共同话题,最难能可贵的是大家也毫无保留的分享自己总结了很久的经验踩过的坑,大家在聊的时候我和帅地的笔记本一直在记录,手现在还很酸。

又到了离别的时刻了,很难想象这样的一群人跟视频和文章里面一样的平易近人,那样年轻,那样的有趣,那样热血。

饭后我和帅地又乘车回深圳了,一天的舟车劳顿,在列车上帅地睡着了,看着他的脸庞,我也开始了自己的思考,我们应该怎么做,才能有属于我们的未来,才能在这个时代不被人潮淹没,不随波逐流。

第二天,我去了腾讯,因为腾讯邀请我做入职体验,内部的照片我就不分享了,总之腾讯的工作环境还是很nice的,师兄们也很热心,如果有一天我考虑去深圳我想腾讯应该是最好的选择(至少是个双休嘛哈哈)。

逛完腾讯就是跟学姐汇合了,学姐大我两届,和学姐认识也还算魔幻,大一我参加了学校的学生会,她是学生会副主席,帮了我和很多忙,说实话大学时候我觉得很多社团的东西根本一点用都没,但是那些没用的东西背后,可能也有你看不见的有用。

学姐现在也是公司HR Leader了,她说见过很多人,她说:大学经历过社团锻炼和没经历过的人还是有一些区别的,同等条件的同学参加过社团的表达会好很多,组织过活动的同学,策划和组织能力也会强很多,我一想确实,很多我们觉得没意义的东西,或许无形中改变了我们。

不过我得吐槽一下行程安排,一直都在吃吃、喝喝、玩玩,不带一秒停歇的,就连我去坐车上飞机的时间,她和另外一个妹妹都算好了(其实内心窃喜)

第一次玩射箭,我表现得不太行,也就5、6个超级正中的十环吧哈哈,其实还挺难的,手抖一下就偏很远了,所以拿起来到射出去基本上要在3秒内。

走之前我们去酒吧小酌了一杯,聊起了过往,几年不见大家都被社会磨去了学校的棱角,在职场的我们就像是一个个被抛了光的鹅卵石,不过让我佩服的是学姐的毅力,学姐每天11点准时睡觉,6点起床,工作再忙也坚持去练舞,也让我看到了那个大学时候学姐学习时的坚持。

大家会发现我一次旅行不看什么景点,就匆匆结束了,是不是想问我累么?值得么?

我觉得值得,其实旅行的意义更多的是那些人,那些故事,不是嘛?景点就在那我有时间随时就可以去看,但是很多人就不知道要什么时候再见了。

时光不问赶路人,离别只是为了下次更好的相逢,愿大家永远前程似锦,永远热泪盈眶。

絮叨

我是敖丙,你知道的越多,你不知道的越多,我们下期见!

人才们的 【三连】 就是敖丙创作的最大动力,如果本篇博客有任何错误和建议,欢迎人才们留言!


文章持续更新,可以微信搜索「 三太子敖丙 」第一时间阅读,回复【资料】有我准备的一线大厂面试资料和简历模板,本文 GitHubhttps://github.com/JavaFamily 已经收录,有大厂面试完整考点,欢迎Star。
查看原文

赞 6 收藏 0 评论 2

认证与成就

  • 获得 102 次点赞
  • 获得 4 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 4 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-11-29
个人主页被 1.4k 人浏览