小结点

小结点 查看完整档案

东莞编辑广东海洋大学  |  计算机科学与技术 编辑  |  填写所在公司/组织 www.jianshu.com/u/9d05f45e0304 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

小结点 关注了专栏 · 8月29日

阿里妈妈前端技术周刊

阿里妈妈为阿里巴巴的广告部门,本周刊主要面向前端,包括新闻和专题两个板块。

关注 1384

小结点 赞了文章 · 7月26日

Vue+Express全栈购物商城

一、前言提纲

基于Vue和Express框架写的一个全栈购物商城,记录项目过程中遇到的一些问题以及经验和技巧。

二、历史版本

  1. 基于Vue-CLI2.0:点我查看

    这个分支版本是一两年前的,基于Vue-CLI2.0写的,数据请求是Mock,纯前端的项目。

  2. 基于 Vue-CLI3.0:点我查看

    这个分支版本是基于Vue-CLI3.0的,将脚手架从2.0迁移升级到了3.0,遇到的一些问题和坑也都填完了~也是纯Web端Mock模拟数据的项目。

  3. 当前版本:点我查看

    基于Vue-CLI3.0,前端用Vue全家桶,后端用Express+MongoDB+Redis,后台管理系统CMS是用的Vue-Element-Admin

三、详情

1.前端

在线预览:https://www.fancystore.cn

手机直接扫描二维码真机体验:

1.1 技术栈:

  • Vue全家桶(Vue-CLI3,Vue2.x)
  • Vue-Router(页面KeepAlive的处理)
  • Vuex(状态管理库,刷新保存状态)
  • Axios(二次封装配置的数据请求)
  • Less(CSS预处理)
  • I18n(国际化处理)
  • Vant(UI库,按需加载+rem)
  • SEO(预渲染)
  • Sentry(线上错误日志监控)
  • Travic(自动构建,持续部署)

1.2适配

项目代码px自动转换为rem,需要在main.js中引入amfe-flexible

Vant UI库也有REM单位,需要在vue.config.js中配置:

1.3 SEO

单页(SPA)SEO是一个痛点,目前有两种方式,一种是SSR,一种是预渲染(PrerenderSPAPlugin)

这个项目是用预渲染(PrerenderSPAPlugin)+vue-meta-info这两个库来做SEO优化的。

  1. rouer.js模式改为mode:history
  2. 下载安装PrerenderSPAPlugin

    PrerenderSPAPlugin是Google的一个库,基于Chromium是获取数据,安装PrerenderSPAPlugin的时候会自动下载Chromium浏览器,国内npm安装Chromium会经常安装失败,建议用淘宝的cnpm安装

    npm install -g cnpm --registry=https://registry.npm.taobao.org
    cnpm install PrerenderSPAPlugin --save
  1. vue.config.js中引入PrerenderSPAPlugin,配置需要预渲染的路由。
  2. 下载安装 vue-meta-info

    在main.js中引入vue-meta-info,在每个页面配置meta信息,这样每个单页路由都有不同的title,理由爬虫引擎抓取重要内容,利于SEO。

预渲染前只有一个index.html,预渲染后最后打包出来的预渲染目录文件如下:

1.4 路由懒加载以及缓存keep-alive动的态判断

项目中会使用keep-alive会提高用户体验和网站的性能,如果想实现部分页面缓存,部分页面不需要缓存,可以在router.js里面的路由添加meta.keepalive在跟router-vier加入判断:

1.5 Vuex状态管理页面刷新失效问题

用Vuex管理全局的状态,会遇到刷新页面的时候所有的状态丢失或者重置,可以在App.vue的钩子函数添加代码,会在页面刷新的时候将Vuex存储到Storage中,刷新完成后又再从Storage取出来存到Vuex里面:

1.6 封装数据请求

封装Axios,添加Axios请求(request)和相应(response),统一处理错误信息或者登录认证的消息,所有的数据请求都存放到api目录下,对应的页面方便后续的维护和管理。

1.7 打包构建优化vue.config.js

  1. 区分开发环境和生产环境
  2. alias的方式直接指定目录。
  3. CDN

    生产环境中将一些共有库Vue,vuex,vue-router等库不打包到项目中,而是通过CDN的方式引入这些共有库,这样可以减少项目的大小,也可以借助CDN的优势,让网站加载更快。推荐一个强大的cdn库:[https://www.bootcdn.cn/](https://www.bootcdn.cn/)

  4. 生产环境压缩和出去console打印日志
  5. 生产环境开启gzip压缩
  6. 生产环境启用预渲染
  7. 生产环境分离css,外链CSS可以缓存

1.7 错误日志监控Sentry

集成Sentry开源日志监控系统,在官网注册获取key,在main.js中引入RavenVue并配置即可

1.8 自动构建和持续集成

Github自动构建和持续集成基于Travis

  1. 登录Travis选择需要持续集成的项目。
  2. 在.travis.yml写上相应的config,服务器配置ssh_key,
  3. 每次代码push到指定分支(比如master)的时候,Travis会自动执行项目上的.travis.yml文件,开始自动构建,构建成功通过scp密令传送到服务器,完成自动部署的功能。

    每次需要发版,只需要push代码,然后去喝杯咖啡,回来就已经构建发布完成,解放劳动力

1.9 代码自动格式化优化

团队合作的时候,每个成员用的编辑器不同,缩进格式也不同,这样合并代码的时候会出现各种意外的情况,团队统一编辑器和编辑器不太现实,因为每个人的写代码习惯和风格不一致。可以借助husky 和 link-stage,每次commit的时候都会安装配置的规则格式化代码,参考文章:https://segmentfault.com/a/1190000009546913

1.10代码优化

  1. 设计模式

表单验证需要写很多判断条件,if-else 或者swith,当条件越多时或者后面需要修改需求条件的时候,会变得不是很好维护,可以用策略模式来重构业务代码:

  1. 善用Mixin,提取共用的组件,将项目组件化

Vue的Mixin复用代码,可以更好的提高开发效率和可维护性
除了将一些共用的页面做成组件引入的方式之外,大文件项目也分好几个模块,将文件才成模块的方式会更好维护和更好的阅读。

2.服务端

2.1 技术栈:

  • Node
  • Express
  • Mongo
  • Mongoose
  • Redis
  • Qiniu
  • PM2

2.2 登录授权

用Session认证机制,来实现登录登出。
配置Session的加密解密,将Session存储到Redis,提高性能,如果有多台服务器,Redis可以共享Session。

2.3 中间件判断用户是否登录:

有些API请求是需要用户登录才可以访问的,可以写中间件来判断:

2.4 中间件判断用户的权限:

有些API的请求是需要判断用户是否有权限,比如添加、删除和更新,会在中间件判断是否有权限

2.5 PM2多进程启动项目

2.6 Mongodb优化设置索引

2.7 Redis做缓存

2.8 七牛云对象存储配置Key还有域名的绑定以及HTTPS证书的申请

3.后台管理系统CMS

在线预览:https://www.fancystore.cn/admin

3.1技术栈

vue-element-admin

配合后端做了权限系统,根据用户的权限来展示和隐藏菜单和按钮。

4.服务器

  1. Nginx配置gzip和缓存策略,根据不同的 URL来代理。
  2. 申请HTTPS证书,全站升级到HTTPS,配置HTTP2.0的协议。

Github:

前端: https://github.com/czero1995/fancy-store

服务端: https://github.com/czero1995/fancy-store-server.git

后台管理CMS: https://github.com/czero1995/fancy-store-admin.git

查看原文

赞 58 收藏 45 评论 2

小结点 赞了文章 · 7月21日

git cz Commitizen 使用方法

Commit Message

(Commitizen是一个格式化commit message的工具。它的安装需要NPM的支持,NPM是Node.js的包管理工具,所以首先安装node.js)

  1. Commitizen安装:

    npm install -g commitizen
  2. 安装changelog,生成changelog的工具:

    npm install -g conventional-changelog conventional-changelog-cli
  3. 检验是否安装成功:

    npm ls -g -depth=0
  4. 项目根目录下创建空的package.json,然后进入到项目目录,执行以下命令会生成对应的项目信息:

    npm init --yes
  5. 运行下面命令,使其支持Angular的Commit message格式:

    commitizen init cz-conventional-changelog --save --save-exact
  6. 进入到项目目录,执行以下命令生成CHANGELOG.md文件:

    conventional-changelog -p angular -i CHANGELOG.md -s
  7. 到这步就成功了,以后,凡是用到git commit命令的时候统一改为git cz,然后就会出现选项,生成符合格式的Commit Message。
  8. 生成CHANGELOG:

    • conventional-changelog -p angular -i CHANGELOG.md -s (该命令不会覆盖以前的 Change log,只会 在CHANGELOG.md的头部加上自从上次发布以来的变动)
    • conventional-changelog -p angular -i CHANGELOG.md -s -r 0 (生成所有发布的 Change log
    • 或者方便使用直接写入package.json的scripts字段:
     {
         "scripts": {
            "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0"
          }
     }

直接运行 npm run changelog 命令即可。。

注意事项

  1. 把node_modules加入.gitignore忽略

    1. commit的几种类型选项,如下:
      feat 新功能
      fix Bug 修复
      docs 文档更新
      style 代码的格式,标点符号的更新
      refactor 代码重构
      perf 性能优化
      test 测试更新
      build 构建系统或者包依赖更新
      ci CI 配置,脚本文件等更新
      chore 非 src 或者 测试文件的更新
      revert commit 回退
  2. 每次打包生成changelog之后在最后的提交纪录上打tag,tag命名格式为v1.0.0(超过三位changelog不识别)。这样下次生成changelog的时候会在这个tag的基础上增量更新。
  3. changelog中的版本号根据package.json中的version生成,注意不能和你的tag同名否则不生成日志。
查看原文

赞 4 收藏 3 评论 0

小结点 赞了文章 · 6月25日

老码农教你学英语

英语真是硬伤,收藏一下前辈的经验学习学习

对于咱们这些高端大气、时刻需要和国际接轨的码农,英语的重要性自然是毋庸置疑的。尤其是那些胸怀大志的潜在大牛们,想在码农行业闯出一片天地,秒杀身边的小弟们,熟练掌握英语更是实现其目标最关键的因素之一。否则,试想在你捧着某出版社刚刚翻译出来的《JSP 高效编程》苦苦学习JSP模板的时候,你旁边的小弟却是拿着原版的《AngularJS in Action》学习开发单页面应用,虽然你们都同样认真地学习了一个月,可做出来东西的效果能一样吗?

所以,英语好才能学到最新最炫的技术,否则只能拿着国内出的翻译版学习两三年前的老古董还把它当个宝。更何况国内的翻译书水平如何你不会不知道吧?多少坑爹的翻译啊!不提了!其实我十多年前还参加过一本Java开发指南的翻译,而当时我一直在IBM主机上做开发,压根就不会Java,所以误人子弟是肯定的了。回首往事,惭愧啊!请上帝宽恕我的罪恶,阿门……

好了,现在言归正传,说说码农应该如何学习英语,达到熟练掌握英语的水平。首先,我要明确一个概念:英语学习是不可能速成的。一心想速成的同学们可以不用往下看了,不然浪费了你们的时间我可担不起责任啊。

作为码农的习惯,自然第一个重点是要准确定义”熟练掌握英语“的概念。

我的定义如下:

  • 阅读:能够直接阅读英文文档,比如《MongoDB: The Definitive Guide》,并且阅读速度和理解程度都能与母语相当;
  • 写作:能够直接编写英文文档、邮件,达到英语母语人士能够无歧义理解的程度,学有余力的同学可以追求逻辑严密和用词严谨;
  • 听说:能够顺畅地与英语母语人士进行有关技术方面的交流,达到双方沟通无歧义的程度,学有余力的同学还可以争取在对话中表现幽默感和个人品位。

肯定有人要不服了:为啥学习的起点给俺整这么高呢?因为,只有达到这个标准,你才能充分发现学好英语的好处,也才能自觉地尽可能使用英语,形成良性循环。

那么,要达到这个标准需要多少时间呢?对于CET-4或者6级水平的同学(没错,4和6都是一个水平,几乎没啥差别),我的估计是需要1万小时以上。这也是我前面说英语不可能速成的原因。可能有人会抱怨说,老码农你不靠谱,1万小时也太多了,就算一天花5小时学英语,也需要6年时间,这不是太坑爹了么?对此我想说的是,每天5小时其实一点也不难,如果你一直在学新技术而不是重复做熟练工的话,平均每天看技术文档就能看3小时,另外写文档邮件注释再花1小时,上下班路上也别坐着发呆,至少可以练听力1小时,这样5小时不就出来了么?

最关键的是,这5小时并不是另外挤出来的,而只需要把以前的一些习惯改一改就好。遇到问题百度查中文文档改成Google查英文文档,把写中文注释的习惯改成写英文注释,拼音变量名改成英文短语变量名,尽量和国际接轨,做一个高端大气国际化的码农。除了这5小时之外,平时没事也可以听点外语歌,感受一下英语的韵律;有机会就经常去听一些国际性的行业大会;周末再去看看好莱坞原声版大片,不亦乐乎?这些都是你本来工作和生活中就有的内容,并不需要刻意地去挤很多时间苦学。只要有心,处处时时都是学习的机会。

至于6年时间有人觉得太长,如果是你喜欢做的事,谁会嫌时间长? 更何况你往后看就会发现,这1万个小时里在学英语的同时也是在学技术,而且是最基础或者最先进的技术,是不学好英语就很难及时掌握的技术。通过这个过程,你不仅英语水平提高了,技术上也能大有长进。这些都是潜移默化的,每天都在进步,而不是一定要突击到满1万小时产生一个突然的飞跃,这才是真正提高水平的真谛所在。所以我认为,学英语首先要去掉功利心理,通过一个努力的过程把英语变成你的一项爱好:看书就喜欢看英文版的书,看电影就喜欢看原声不带字幕的。如果能做到这一点,别说6年,就是60年你也能甘之如饴;反之如果你不喜欢英语,那么每天5个小时的煎熬即使是6个月恐怕都很难坚持下来吧?

写到这里,对于学习英语的心态,我想小结一下。有些同学会狠狠地下一个决心:我要开始学英语了!苦读一年练成英语神功!然后开始拿一本词典开始背单词,花了一个月时间把A打头的单词背得滚瓜烂熟,然后,就放弃了。。。我想说的是,不要把学英语看作一次磨练意志的马拉松赛跑,把它当做一次旅行,享受这个过程,享受途中的风景,不用急着赶路,这样你才能走得更远。

言归正传,1万个小时这个数据的估算方法是:阅读5000小时、听2000小时、 写2000小时、 说1000小时,下面具体说明:

1. 大量阅读是提高英语水平的基础、核心、重中之重,要在大量阅读的基础上再开始练习听力和写作。

为什么这么说?最有效的学习方式就是模仿,而听说读写四个要素里只有听和读有模仿条件。

那为什么不是先多听呢?有两个原因。一是口语比较随意,俚语方言比较多,在表达上不如书面内容严谨;二是听一次没能理解的内容不容易查找和重复。所以大量的阅读是提高英语水平的关键性基础性的工作。

有人又要问了,难道不要先背单词吗?这个嘛,我自己是最讨厌背单词的,又怎么会把你们往火坑里推呢?而且单纯地背单词用处不大,在阅读中记住的单词才是有用的。

比如在GRE单词表开头随便找个单词avalanche,释义是n.雪崩 vi.崩塌,你可以使劲地背,把它和其他几千个单词都记得滚瓜烂熟,然后当你某天在文档中看到一句话说:”… to handle an avalanche of client requests …“,你可能都想不起来背过avalanche这个单词,即使想起来也未必能准确理解它在这段话里的含义。

这是为什么呢?因为背单词是孤立地去记一个个单词,应试也许有用,但是事倍功半。最好的方法就是在阅读中去理解它,比如还是那一句,我压根不知道avalanche这个单词,但是从前后文去蒙,an avalanche of大概是说非常多的意思,如果你不确定理解得对不对就去查一下字典,就明白了这意思是”像雪崩一样滚滚而来的大量的什么东西“。

查词最好是查英-英辞典,比如Merriam-Webster的韦氏英英辞典。还有,查一个单词的时候,不要去记那几个中文释义。某些人背单词就喜欢像念经似的念叨:“capability,才能,能力,容量,性能,生产率,capability,才能,能力,容量,性能,生产率……”,旁观者都替他觉得累得慌对吧?其实你只要模糊地知道它的意思就行了,学英语尽量不要掺进来中文。老外不知道啥叫“才能,能力,容量,性能,生产率”,也没影响人家用capability这个词不是?

另外我觉得吧,单词和人一样,也是有眼缘的,记得住记不住都不要去强求。即使这一次看完又忘了也没关系,等你阅读量上去之后,遇到次数多了自然就记住了。有人说,万一有的词遇到次数很少,还是记不住咋办?对这种问题我就无语了。遇到次数很少的单词你记它干啥?有个著名单词中文意思是什么早期银版照相术的,我就偏不记它,它能把我怎么着啊?

通过这个过程,你不但记住了单词,还掌握了它的常见用法,这对于以后的写和说都是非常好的基础。现在很多人学英语的弊病在于应试思维,就仿佛孔乙己说的茴香豆的茴有四种写法,少记住一种就可能在考试里做错题被扣分,但是现实生活中你也许只需要会一种就行了,关键是知道它的意思而且能够准确地使用它。

矮马,一下子扯到背单词跑偏了。还是接着说阅读,怎么阅读呢?我推荐一种暴力方法,不管你基础如何,先找一两本和技术相关的白话书,而且是你最感兴趣的领域,比如经典的《Man Month Myth》(人月神话) 和《Joel on Software》(大神Joel谈软件开发)原版书,这样读起来好歹不那么苦闷,有助于你坚持下来。毕竟在这个起步阶段,坚持是第一位的。不然给你找本《advanced econometrics》试试,据说95%的码农读了三天之后都疯了。

就算是你感兴趣的书,也保不齐一开始读起来觉得很费劲。也许有的人翻开第一页一看,妈呀,一半单词不认识!这咋办?没事,像我前面说的,有把握蒙的就蒙,没把握的就查,查过的可以把中文意思写在单词旁边,能写英文理解更好,懒得写也没关系,总之原则就是把全部内容看懂吃透就行。

还有,句子太长里边有好多that….. which….. who….什么的从句?那也没关系,别人理解不了,可咱是码农啊,懂得嵌套结构的原理,不就是递归嘛!甭管他连了多少个,从最后面的那个往前一个一个处理,把从句用彩笔一个一个标出来,最后看清楚嵌套关系以后,句子也就容易看懂了。实际上,我觉得英语最妙的地方就在这里,它能在一个句子里用一个无限延展的树形结构来描述一个概念,直到把它定义得非常严谨无歧义为止。中文做不到这一点,必须拆成好多个句子才能做到通顺,但理解起来就困难多了。

在这个阶段一定不要求快,一天吭哧吭哧地只看了半页都没关系,也很正常,但一定要确保准确理解。在此我想特别提醒的是,如果你一开始不适应,一定要坚持下来。其实学习英语过程中最难的不是听说读写,而是英语思维。汉语是讲朦胧美的,所谓“道可道,非常道”,越深刻的东西往往越是“只可意会,不可言传”,文人写文章喜欢下结论而很少论证,结论的经验性主观性较大,例如“肉食者鄙,未能远谋”;而欧美文章则大多务求精确严谨,定义精确,论证充分,避免逻辑上的漏洞,下结论则往往比较谨慎,一般都是客观数据,尽量避免主观看法,例如“根据卫星云图,明天的降水概率为70%”。我年轻的时候买过《孙子兵法》和克劳塞维茨的《战争论》一起看,两者的风格差别真是泾渭分明。

此外对于人文方面也有很大差别。老外经过文艺复兴,对于人文关怀有了很好的基础,更强调个人自由和权利,所谓”风能进,雨能进,国王不能进”;我们从历史文化传承的角度看则是集体大于个人,更强调个人适应环境。再比如老外对于弱势群体的歧视嘲笑是非常忌讳的,但中国人往往习惯了小品里瘸子瞎子胖子出场摔一大跟头这样的笑料。

所以,这些思维上的差异才是学英语的最大障碍,而大量阅读有助于理解和养成这种思维习惯。特别是逻辑上的严密性和表达的客观性方面,由于文化传统和教师本身的原因,在中国现在的教育环境很难训练出来。但是通过大量阅读英语材料,就可以达到很好的洗脑效果,实为居家旅行、和平演变必备良药。

读完两三本白话书就算是热好身了,下面可以开始读一些更枯燥的东西,主要是专业教课书。如果上大学的时候学的《数据结构》、《离散数学》、《操作系统》这些都还给老师了,正好拿英文版的复习一下。找你专业相关的基础课原版教材,5本左右,开练。照着前面的要求,一样细细地看,不求快但求精,练习题也好好做一部分,不然你怎么知道自己确实理解透了?

等你看完这几本,阅读基本上就算入门了,可以开始练习写作和听力。具体做法后面再细说。

与此同时,要开始看一些行业里最新的技术文档。这些可能还没出书,也可能有一些免费的pdf,不管怎样,也找5本左右来看,什么MongoDB,Neo4j,Node.js,AngularJS之类的,细细地看,边看边做个系统练手,因为这些新技术文档还比较少,社区里的东西也不多,所以免不了要去StackOverflow问,或者去GitHub找些例子来看,别偷懒,这些都是很好的学习方式。

等你把这几本书也啃下来以后,按A4纸算,你的阅读量肯定会达到7000页以上,在SO, gitHub, Google上查看过的英文资料也不会少于3000页,阅读总量肯定超过1万页了。现在恭喜你,你的阅读能力肯定没问题了。如果我没猜错的话,你这时候碰到翻译版的书都懒得瞟一眼,更别说花钱买和花时间读了。

另外,阅读能力达到这个水平以后,写作也基本有个基础了。所谓“熟读唐诗三百首,不会作诗也会吟”,看多了以后你会发现英语写作也有一些套路,和中文是一个道理,经过大量的阅读,很多句式其实已经在你的脑子里了,只要在写作过程中经常练习这些句式,自然就熟能生巧了。

2. 写作要创造环境,每天都要写1000字以上。

在互联网时代,这一点也不难。最简单直接的办法就是注册一个StackOverflow账号,起初是去问问题,把你搞不定的代码贴上去,会有大牛们帮你搞定,顺带着阅读也练了;等你技术水平涨了以后,想想这么多人帮了你你才成了大牛,总不能只进不出吧,有余力了就应该报复社会,所以就经常去StackOverflow找一些你拿手的主题,帮其他菜鸟们解答一些问题,这样也就练了写作了,一举两得多好!

等你在SO上混出了点名堂,有点江湖地位的时候,就会有人来找你,比如帮忙干点活啊咨询点问题啊之类的,自然邮件联系就少不了了,这都是练习写作的机会,就算你不想帮他干活,也可以和他扯一扯。

另外,弄个FQ软件比如goagent,注册个Google Plus账号,上去看看热点文章,写点自己的体会之类的。实在闲得无聊也可以找Linus之类的大牛混,他每发一贴你就上去评论一番或者请教一下,先混个脸熟嘛!顺带也练习了写作。时间长了,说不定大牛还喜欢上你了,没准随便给你个肉身FQ的机会,这就是意外之喜了。

总之,写作是大量阅读之后水到渠成的产物,不过要注意一点,写出来的每个句子甚至每个单词都要务求精准地道,不知道的不要瞎写,要么改换自己熟悉的写法,要么查清楚了再写。要是养成随意乱写的习惯,写出来的句子都是中国人能看懂,外国人都看不懂,那前面的努力就付诸东流了。

3. 听力要在阅读能力达到一定水平后再开始练习,和前面说的阅读给写作打基础的关系相似,练听力的同时其实就在为口语打基础。

我首先要说的是,千万不要跟着美剧什么的练听力,你又不是打算偷渡去纽约皇后区卖毒品跟黑人大哥混,就算你听一耳朵就能熟练分辨出说WTF are you doing的是意大利人、爱尔兰人还是黑人,又有啥用呢?咱们做码农的一定不要忘了自己的本分,得跟着码农的大哥混才对。所以练习听力一定要多找IT圈的大牛的访谈来听。

最经典的自然是天妒英才英年早逝的乔帮主,多听听他的访谈你就知道他的成功绝非偶然。像这样一位逻辑严密,思维活跃,表达能力强,善于调动听众情绪,还很有幽默感,而且做事又非常有韧性的人,实在是百年难遇的天才人物。

比尔盖茨的访谈就差多了,他的表达能力真的是不敢恭维,有时候东拉西扯的听不明白重点,依稀有点韩乔生老师的风韵。政客系列也不推荐,像奥巴马的讲演听起来总有一种似曾相识的赶脚,细细一想,这不就是美国版的传销讲座嘛!

所以,多听乔大神的访谈绝对是没错的。为啥是访谈呢?因为都是对话,形式上和咱们的需求匹配,咱们工作中需要的英语对话就是类似于访谈式的,你想想是不是?而且他的访谈聊的都是咱们挨踢的事儿,背景知识都比较熟悉,你专心听他的表达和逻辑就好了。你要是不服,俺给你弄个生物系教授讲分子生物学课程的录音让你听,你听完还能找得到北吗?

这些访谈在网上都有,到iTunes里的podcast里大把大把的,不过别找带字幕的听,千万!

现在材料有了,怎么听呢?我先告诉你,每个访谈都要听100遍以上,头20遍就是稀里糊涂地听,能听懂多少听懂多少,听不懂的先蒙。但我敢保证,第20遍肯定比第1遍听懂的东西要多多了。到了20遍还在蒙的,基本你听到第100遍也还是在蒙,所以再蒙下去就没意义了,这时候找到文字版好好看一遍,就看一遍,然后收起来,再听20遍,这时候有些原来靠蒙的就听出来了,听完20遍再看一次,再听,如此反复,100遍之后基本应该都不用蒙了,然后再听下一个。听过100遍的,以后也要经常复习,反复听,再背几遍,很多句型就会自动进到你的脑子里,给你的口语打下一个很好的基础。

IT界的访谈每个大概都在1-2小时之间,就算1.5小时吧。一个听100遍就是150小时,精听10个就是1500小时。10个就够了,不用追求数量,关键是重复。当然平时还要有一些泛听,比如英文广播、看CNN新闻什么的,越多越好,这些就不求全听懂,听懂多少是多少,一遍就过去,权当是个消遣。

在这之后,再故意去找一些录音不是那么清晰的访谈来听,比如乔大神96年回到Apple时,在当年的WWDC上的访谈。这个访谈很有意思,因为他一回来就砍掉了好多正在做的产品,有很多利益受损的听众对他不服不忿的,带讽刺挖苦甚至攻击性的问题也不少,可乔大神应对自如,潇洒极了。这个访谈大概是因为年代久远,杂音比较多,音质也不太好,但仔细听也能听出来。很好,就是它了!就照着这个标准找那么三、四个略模糊的访谈,比如通过电话进行的一些访谈,再如法炮制,按100遍的方法听它500小时,这样听力基本也就过关了。

有人大概心里会嘀咕:故意找这种模糊的录音听有什么意义呢?这主要是让你适应不那么理想环境下的听力,比如在喧闹的餐厅里对话,或者是老外通过skype对你进行电话面试,而你只能听清专业录音设备录下来的访谈,那咋行?所以也需要在前面理想条件下1500小时听力练习完成的基础上,训练一下恶劣条件下的听力。

4. 练习口语是最麻烦的,因为说的条件最难创造,这必须得有个大活人认真地跟你聊才行啊!

当然了,要是你能找个英语母语的老外谈对象,那就又省事又高效了。不过,等口语练好了一定要一脚将其踹掉。要记住,你是一个中国人,肥水不流外国田!

好了,大晚上的不扯淡了。练习英语口语不外乎这么几个途径:交外国朋友,进外国公司,或者花钱雇外国人跟你聊。各人工作、经济情况不同,这就自己选择吧。反正现在改革开放了,一部分人也富起来了,跟谁聊也就丰俭由人了。

不过这里要提醒一点,别去什么英语角练口语,这种地方要么是一百多人围着一个老外,跟看猴似的,要么就是一堆中国人互相来几句好肚油肚,听着就想摸摸对方的肚子看看有多好有多油,纯粹浪费时间!我认为,练口语一定要找英语是母语的人士练习,这样才能事半功倍。

另外,有机会就多去美国英国澳大利亚加拿大什么的地方转转,现场体验一下自己的学习成果。不过我这里讲的主要是码农工作相关的东西,基本没涉及到生活类的英语,比如去麦当劳买个milk shake什么的,这些主要要靠现场体验现场学习,但有了前面阳春白雪的码农高端上流社会英语作为基础,这些下里巴人的生活英语上手很快,对你肯定不在话下。

【补充】有同学想让我推荐一些学习材料,我后来补充了一篇补充一些英语学习素材,供大家参考。

关于码农如何学好英语,我就先说这么多吧。最后再总结一下,英语学习是一个终身的事业,只有通过痛苦的起步阶段,慢慢把它变成你的一个爱好,才能持久下去。等到它真正成为你喜爱的东西了,你就会发现眼中的世界从此变得不同。它变大了,变得多样化了,也变得更美好了。这时,所有的努力和坚持都有了回报。

作者Aaron
作者微博:@老码农的自留地

查看原文

赞 18 收藏 55 评论 7

小结点 发布了文章 · 6月24日

chrome浏览器插件开发实践

前言

参考:
https://www.cnblogs.com/liuxi...
https://developer.chrome.com/...
使用Vue.js开发一个简单的文档阅读辅助插件,效果如下:
折叠api菜单
api_collapse.gif
笔记
notes.gif
嵌入代码
css.gif

脚手架搭建

安装依赖

npm i @babel/core @babel/plugin-transform-runtime @babel/preset-env @babel/runtime babel-loader autoprefixer postcss postcss-loader babel-plugin-component copy-webpack-plugin css-loader file-loader html-webpack-plugin node-sass sass-loader style-loader url-loader vue-loader vue-template-compiler webpack webpack-cli -D
npm i element-ui mavon-editor vue -S

新建webpack.config.js文件:

const path = require('path')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const HTMLWebpackPlugin = require('html-webpack-plugin')
const VueLoaderPlugin = require('vue-loader/lib/plugin')

const mode = process.env.NODE_ENV.trim()

module.exports = {
  mode,
  entry: {
    content: './src/content/index.js',
    popup: './src/popup/index.js',
    options: './src/options/index.js',
    background: './src/background/index.js'
  },
  output: {
    filename: '[name]/[name].js',
    path: path.resolve('dist'),
    publicPath: './'
  },
  resolve: {
    modules: [
      'node_modules'
    ],
    extensions: ['.vue', '.js', '.json'],
    alias: {
      'vue$': 'vue/dist/vue.js'
    }
  },
  module: {
    rules: [
      {
        test: /\.scss$/,
        use: [
          'style-loader',
          'css-loader',
          'sass-loader',
          'postcss-loader'
        ]
      },
      {
        test: /\.css$/,
        use: [
          {
            loader: 'style-loader',
            options: {
              insert: 'html',
            }
          },
          'css-loader'
        ]
      },
      {
        test: /\.js$/,
        loader: 'babel-loader'
      },
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      },
      {
        test: /\.(eot|svg|ttf|woff|woff2)(\?\S*)?$/,
        loader: 'url-loader'
      }
    ]
  },
  plugins: [
    new HTMLWebpackPlugin({
      template: './src/popup/index.html',
      filename: 'popup.html',
      chunks: ['popup']
    }),
    new HTMLWebpackPlugin({
      template: './src/options/index.html',
      filename: 'options.html',
      chunks: ['options']
    }),
    new CopyWebpackPlugin({
      // root is output(dist)
      patterns: [
        {
          from: 'manifest.json',
          to: '.'
        },
        {
          from: 'src/static',
          to: 'static'
        }
      ]
    }),
    new VueLoaderPlugin()
  ]
}

新建.babelrc文件:

{
  "presets": [
    [
      "@babel/preset-env",
      {
          "targets": {
            "chrome": "58",
            "ie": "8"
          },
          "modules": false
      }
    ]
  ],
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "helpers": false,
        "useESModules": true,
        "absoluteRuntime": true
      }
    ],
    [
      "component",
      {
        "libraryName": "element-ui",
        "styleLibraryName": "theme-chalk"
      }
    ]
  ]
}

新建postcss.config.js文件:

module.exports = {
  plugins: [
    require('autoprefixer')()
  ]
}

新建Manifest.json文件:

{
  "manifest_version": 2,
  "name": "web_docs_helper",
  "version": "1.0.0",
  "description": "",
  "permissions": [
    "storage",
    "tabs"
  ],
  "content_scripts": [
    {
      "matches": [
        "<all_urls>"
      ],
      "js": [
        "content/content.js"
      ],
      "run_at": "document_start"
    }
  ],
  "browser_action": {
    "default_title": "这是一个示例Chrome插件",
    "default_popup": "popup.html"
  },
  "options_ui": {
    "page": "options.html",
    "chrome_style": true
  },
  "background": {
    "script": "background/background.js"
  },
  "content_security_policy": "style-src 'self' 'unsafe-inline';script-src 'self' 'unsafe-eval' https://cdn.bootcss.com~~; object-src 'self' ;"
}

项目目录

新建src文件夹,对应页面看webpack.config.js的entry,现在用content来举例。
在src文件夹中新建:
image.png

在index.js中引入Vue:

import Vue from 'vue'
import VMenu from './menu'
import {Popover, Button, Input, Dialog, Card} from 'element-ui'
import mavonEditor from 'mavon-editor'
import 'mavon-editor/dist/css/index.css'

Vue.use(mavonEditor)
Vue.use(Popover)
Vue.use(Button)
Vue.use(Input)
Vue.use(Dialog)
Vue.use(Card)

let VApp

window.addEventListener('DOMContentLoaded', async () => {
  // 如果是禁用页面
  const isMatched = await isMatchedPage(window.location.href)
  if (!isMatched) return

  const el = document.createElement('div')
  el.id = 'v-app'
  document.body.appendChild(el)
  try {
    const items = await getChromeStorage({[isShowMenuKey]: true})
    VApp = new Vue({
      el: '#v-app',
      components: {
        VMenu
      },
      template: '<v-menu ref="menu" :isShowMenu="isShowMenu" @update:isShowMenu="setIsShowMenu" />',
      data: {
        isShowMenu: items[isShowMenuKey]
      },
      methods: {
        setIsShowMenu (value) {
          // some code here
        }
      }
    })
  } catch (err) {}
})

完整代码:
https://github.com/maoyonglon...

查看原文

赞 0 收藏 0 评论 0

小结点 发布了文章 · 6月1日

贝塞尔曲线运动

首先,贝塞尔曲线的算法如下:

export const linear = (p1, p2, t) => {
  const [x1, y1] = p1
  const [x2, y2] = p2
  const x = x1 + (x2 - x1) * t
  const y = y1 + (y2 - y1) * t
  return {x, y}
}
export const quadratic = (p1, p2, cp, t) => {
  const [x1, y1] = p1
  const [x2, y2] = p2
  const [cx, cy] = cp
  let x = (1 - t) * (1 - t) * x1 + 2 * t * (1 - t) * cx + t * t * x2
  let y = (1 - t) * (1 - t) * y1 + 2 * t * (1 - t) * cy + t * t * y2
  return {x, y}
}
export const cubic = (p1, p2, cp1, cp2, t) => {
  const [x1, y1] = p1
  const [x2, y2] = p2
  const [cx1, cy1] = cp1
  const [cx2, cy2] = cp2
  let x =
    x1 * (1 - t) * (1 - t) * (1 - t) +
    3 * cx1 * t * (1 - t) * (1 - t) +
    3 * cx2 * t * t * (1 - t) +
    x2 * t * t * t
  let y =
    y1 * (1 - t) * (1 - t) * (1 - t) +
    3 * cy1 * t * (1 - t) * (1 - t) +
    3 * cy2 * t * t * (1 - t) +
    y2 * t * t * t
  return {x, y}
}

参考文章:https://segmentfault.com/a/11...
然后,要让元素进行移动,使用requestAnimationFrame很方便。就是每次执行这个函数时,通过算法算出现在的坐标,将它赋予运动的元素的left和top就可以了。部分代码:

_move (target, count) {
    // count是指requestAnimationFrame的执行次数,t就是贝塞尔算法中的t
    const t = this._calcT(count, t)
    // 计算当前坐标
    const points = this._calcBezierPoint(t)
    target.style.left = points.x + 'px'
    target.style.top = points.y + 'px'
    // 如果t>=1,就表示运行完成了
    if (t < 1) {
      count++
      requestAnimationFrame(() => {
        this._move(target, count)
      })
    } else {
      if (isDef(this.onEnd)) {
        this.onEnd()
      }
    }
  }

  play () {
    // target是运动的元素
    // start是初始坐标
    // end是终点坐标
    const {target, start, end} = this
    if ([target, start, end].every(isDef)) {
      let count = 0
      target.style.position = 'absolute'
      requestAnimationFrame(() => {
        this._move(target, count)
      })
    } else {
      throw new Error('[error]: the target, start and end option must be defined.')
    }
    return this
  }

codepen
npm

查看原文

赞 0 收藏 0 评论 0

小结点 收藏了文章 · 5月28日

一篇文章说清浏览器解析和CSS(GPU)动画优化

相信不少人在做移动端动画的时候遇到了卡顿的问题,这篇文章尝试从浏览器渲染的角度;一点一点告诉你动画优化的原理及其技巧,作为你工作中优化动画的参考。文末有优化技巧的总结。

因为GPU合成没有官方规范,每个浏览器的问题和解决方式也不同;所以文章内容仅供参考。

浏览器渲染

提高动画的优化不得不提及浏览器是如何渲染一个页面。在从服务器中拿到数据后,浏览器会先做解析三类东西:

  • 解析html,xhtml,svg这三类文档,形成dom树。

  • 解析css,产生css rule tree。

  • 解析js,js会通过api来操作dom tree和css rule tree。

解析完成之后,浏览器引擎会通过dom tree和css rule tree来构建rendering tree:

  • rendering tree和dom tree并不完全相同,例如:<head></head>或display:none的东西就不会放在渲染树中。

  • css rule tree主要是完成匹配,并把css rule附加给rendering tree的每个element。

在渲染树构建完成后,

  • 浏览器会对这些元素进行定位和布局,这一步也叫做reflow或者layout。

  • 浏览器绘制这些元素的样式,颜色,背景,大小及边框等,这一步也叫做repaint。

  • 然后浏览器会将各层的信息发送给GPU,GPU会将各层合成;显示在屏幕上。

渲染优化原理

如上所说,渲染树构建完成后;浏览器要做的步骤:

reflow——》repaint——》composite

reflow和repaint

reflow和repaint都是耗费浏览器性能的操作,这两者尤以reflow为甚;因为每次reflow,浏览器都要重新计算每个元素的形状和位置。

由于reflow和repaint都是非常消耗性能的,我们的浏览器为此做了一些优化。浏览器会将reflow和repaint的操作积攒一批,然后做一次reflow。但是有些时候,你的代码会强制浏览器做多次reflow。例如:

var content = document.getElementById('content');
content.style.width = 700px;
var contentWidth = content.offsetWidth;
content.style.backgound = 'red';

以上第三行代码,需要浏览器reflow后;再获取值,所以会导致浏览器多做一次reflow。

下面是一些针对reflow和repaint的最佳实践:

  • 不要一条一条地修改dom的样式,尽量使用className一次修改。

  • 将dom离线后修改

    • 使用documentFragment对象在内存里操作dom。

    • 先把dom节点display:none;(会触发一次reflow)。然后做大量的修改后,再把它显示出来。

    • clone一个dom节点在内存里,修改之后;与在线的节点相替换。

  • 不要使用table布局,一个小改动会造成整个table的重新布局。

  • transform和opacity只会引起合成,不会引起布局和重绘。

从上述的最佳实践中你可能发现,动画优化一般都是尽可能地减少reflow、repaint的发生。关于哪些属性会引起reflow、repaint及composite,你可以在这个网站找到https://csstriggers.com/

composite

在reflow和repaint之后,浏览器会将多个复合层传入GPU;进行合成工作,那么合成是如何工作的呢?

假设我们的页面中有A和B两个元素,它们有absolute和z-index属性;浏览器会重绘它们,然后将图像发送给GPU;然后GPU将会把多个图像合成展示在屏幕上。

<style>
#a, #b {
 position: absolute;
}

#a {
 left: 30px;
 top: 30px;
 z-index: 2;
}

#b {
 z-index: 1;
}
</style>
<div id="#a">A</div>
<div id="#b">B</div>

clipboard.png

我们将A元素使用left属性,做一个移动动画:

<style>
#a, #b {
 position: absolute;
}

#a {
 left: 10px;
 top: 10px;
 z-index: 2;
 animation: move 1s linear;
}

#b {
 left: 50px;
 top: 50px;
 z-index: 1;
}

@keyframes move {
 from { left: 30px; }
 to { left: 100px; }
}
</style>
<div id="#a">A</div>
<div id="#b">B</div>

在这个例子中,对于动画的每一帧;浏览器会计算元素的几何形状,渲染新状态的图像;并把它们发送给GPU。(你没看错,position也会引起浏览器重排的)尽管浏览器做了优化,在repaint时,只会repaint部分区域;但是我们的动画仍然不够流畅。

因为重排和重绘发生在动画的每一帧,一个有效避免reflow和repaint的方式是我们仅仅画两个图像;一个是a元素,一个是b元素及整个页面;我们将这两张图片发送给GPU,然后动画发生的时候;只做两张图片相对对方的平移。也就是说,仅仅合成缓存的图片将会很快;这也是GPU的优势——它能非常快地以亚像素精度地合成图片,并给动画带来平滑的曲线。

为了仅发生composite,我们做动画的css property必须满足以下三个条件:

  • 不影响文档流。

  • 不依赖文档流。

  • 不会造成重绘。

满足以上以上条件的css property只有transform和opacity。你可能以为position也满足以上条件,但事实不是这样,举个例子left属性可以使用百分比的值,依赖于它的offset parent。还有em、vh等其他单位也依赖于他们的环境。

我们使用translate来代替left

<style>
#a, #b {
 position: absolute;
}

#a {
 left: 10px;
 top: 10px;
 z-index: 2;
 animation: move 1s linear;
}

#b {
 left: 50px;
 top: 50px;
 z-index: 1;
}

@keyframes move {
 from { transform: translateX(0); }
 to { transform: translateX(70px); }
}
</style>
<div id="#a">A</div>
<div id="#b">B</div>

浏览器在动画执行之前就知道动画如何开始和结束,因为浏览器没有看到需要reflow和repaint的操作;浏览器就会画两张图像作为复合层,并将它们传入GPU。

这样做有两个优势:

  • 动画将会非常流畅

  • 动画不在绑定到CPU,即使js执行大量的工作;动画依然流畅。

看起来性能问题好像已经解决了?在下文你会看到GPU动画的一些问题。

GPU是如何合成图像的

GPU实际上可以看作一个独立的计算机,它有自己的处理器和存储器及数据处理模型。当浏览器向GPU发送消息的时候,就像向一个外部设备发送消息。

你可以把浏览器向GPU发送数据的过程,与使用ajax向服务器发送消息非常类似。想一下,你用ajax向服务器发送数据,服务器是不会直接接受浏览器的存储的信息的。你需要收集页面上的数据,把它们放进一个载体里面(例如JSON),然后发送数据到远程服务器。

同样的,浏览器向GPU发送数据也需要先创建一个载体;只不过GPU距离CPU很近,不会像远程服务器那样可能几千里那么远。但是对于远程服务器,2秒的延迟是可以接受的;但是对于GPU,几毫秒的延迟都会造成动画的卡顿。

浏览器向GPU发送的数据载体是什么样?这里给出一个简单的制作载体,并把它们发送到GPU的过程。

  • 画每个复合层的图像

  • 准备图层的数据

  • 准备动画的着色器(如果需要)

  • 向GPU发送数据

所以你可以看到,每次当你添加transform:translateZ(0)will-change:transform给一个元素,你都会做同样的工作。重绘是非常消耗性能的,在这里它尤其缓慢。在大多数情况,浏览器不能增量重绘。它不得不重绘先前被复合层覆盖的区域。

隐式合成

还记得刚才a元素和b元素动画的例子吗?现在我们将b元素做动画,a元素静止不动。

clipboard.png

和刚才的例子不同,现在b元素将拥有一个独立复合层;然后它们将被GPU合成。但是因为a元素要在b元素的上面(因为a元素的z-index比b元素高),那么浏览器会做什么?浏览器会将a元素也单独做一个复合层!

所以我们现在有三个复合层a元素所在的复合层、b元素所在的复合层、其他内容及背景层。

一个或多个没有自己复合层的元素要出现在有复合层元素的上方,它就会拥有自己的复合层;这种情况被称为隐式合成。

浏览器将a元素提升为一个复合层有很多种原因,下面列举了一些:

  • 3d或透视变换css属性,例如translate3d,translateZ等等(js一般通过这种方式,使元素获得复合层)

  • <video><iframe><canvas><webgl>等元素。

  • 混合插件(如flash)。

  • 元素自身的 opacity和transform 做 CSS 动画。

  • 拥有css过滤器的元素。

  • 使用will-change属性。

  • position:fixed。

  • 元素有一个 z-index 较低且包含一个复合层的兄弟元素(换句话说就是该元素在复合层上面渲染)

这看起来css动画的性能瓶颈是在重绘上,但是真实的问题是在内存上:

内存占用

使用GPU动画需要发送多张渲染层的图像给GPU,GPU也需要缓存它们以便于后续动画的使用。

一个渲染层,需要多少内存占用?为了便于理解,举一个简单的例子;一个宽、高都是300px的纯色图像需要多少内存?

300 300 4 = 360000字节,即360kb。这里乘以4是因为,每个像素需要四个字节计算机内存来描述。

假设我们做一个轮播图组件,轮播图有10张图片;为了实现图片间平滑过渡的交互;为每个图像添加了will-change:transform。这将提升图像为复合层,它将多需要19mb的空间。800 600 4 * 10 = 1920000。

仅仅是一个轮播图组件就需要19m的额外空间!

在chrome的开发者工具中打开setting——》Experiments——》layers可以看到每个层的内存占用。如图所示:

clipboard.png

clipboard.png

GPU动画的优点和缺点

现在我们可以总结一下GPU动画的优点和缺点:

  • 每秒60帧,动画平滑、流畅。

  • 一个合适的动画工作在一个单独的线程,它不会被大量的js计算阻塞。

  • 3D“变换”是便宜的。

缺点:

  • 提升一个元素到复合层需要额外的重绘,有时这是慢的。(即我们得到的是一个全层重绘,而不是一个增量)

  • 绘图层必须传输到GPU。取决于层的数量和传输可能会非常缓慢。这可能让一个元素在中低档设备上闪烁。

  • 每个复合层都需要消耗额外的内存,过多的内存可能导致浏览器的崩溃。

  • 如果你不考虑隐式合成,而使用重绘;会导致额外的内存占用,并且浏览器崩溃的概率是非常高的。

  • 我们会有视觉假象,例如在Safari中的文本渲染,在某些情况下页面内容将消失或变形。

优化技巧

避免隐式合成

  • 保持动画的对象的z-index尽可能的高。理想的,这些元素应该是body元素的直接子元素。当然,这不是总可能的。所以你可以克隆一个元素,把它放在body元素下仅仅是为了做动画。

  • 将元素上设置will-change CSS属性,元素上有了这个属性,浏览器会提升这个元素成为一个复合层(不是总是)。这样动画就可以平滑的开始和结束。但是不要滥用这个属性,否则会大大增加内存消耗。

动画中只使用transform和opacity

如上所说,transform和opacity保证了元素属性的变化不影响文档流、也不受文档流影响;并且不会造成repaint。
有些时候你可能想要改变其他的css属性,作为动画。例如:你可能想使用background属性改变背景:

<div class="bg-change"></div>
.bg-change {
  width: 100px;
  height: 100px;
  background: red;
  transition: opacity 2s;
}
.bg-change:hover {
  background: blue;
}

在这个例子中,在动画的每一步;浏览器都会进行一次重绘。我们可以使用一个复层在这个元素上面,并且仅仅变换opacity属性:

<div class="bg-change"></div>
<style>
.bg-change {
  width: 100px;
  height: 100px;
  background: red;
}
.bg-change::before {
  content: '';
  display: block;
  width: 100%;
  height: 100%;
  background: blue;
  opacity: 0;
  transition: opacity 20s;
}
.bg-change:hover::before {
  opacity: 1;
}
</style>

减小复合层的尺寸

看一下两张图片,有什么不同吗?

clipboard.png

这两张图片视觉上是一样的,但是它们的尺寸一个是39kb;另外一个是400b。不同之处在于,第二个纯色层是通过scale放大10倍做到的。

<div id="a"></div>
<div id="b"></div>

<style>
#a, #b {
 will-change: transform;
}

#a {
 width: 100px;
 height: 100px;
}

#b {
 width: 10px;
 height: 10px;
 transform: scale(10);
}
</style>

对于图片,你要怎么做呢?你可以将图片的尺寸减少5%——10%,然后使用scale将它们放大;用户不会看到什么区别,但是你可以减少大量的存储空间。

用css动画而不是js动画

css动画有一个重要的特性,它是完全工作在GPU上。因为你声明了一个动画如何开始和如何结束,浏览器会在动画开始前准备好所有需要的指令;并把它们发送给GPU。而如果使用js动画,浏览器必须计算每一帧的状态;为了保证平滑的动画,我们必须在浏览器主线程计算新状态;把它们发送给GPU至少60次每秒。除了计算和发送数据比css动画要慢,主线程的负载也会影响动画; 当主线程的计算任务过多时,会造成动画的延迟、卡顿。

所以尽可能地使用基于css的动画,不仅仅更快;也不会被大量的js计算所阻塞。

优化技巧总结

  • 减少浏览器的重排和重绘的发生。

  • 不要使用table布局。

  • css动画中尽量只使用transform和opacity,这不会发生重排和重绘。

  • 尽可能地只使用css做动画。

  • 避免浏览器的隐式合成。

  • 改变复合层的尺寸。

参考

GPU合成主要参考:

https://www.smashingmagazine....

哪些属性会引起reflow、repaint及composite,你可以在这个网站找到:

https://csstriggers.com/

查看原文

小结点 赞了文章 · 5月28日

flip你的动画

在vue官方文档上看到vue使用flip做动画,就去研究了一下。这是一个准则,协调js和css对动画的操作。如果你看到我的前一篇文章一篇文章说清浏览器解析和CSS(GPU)动画优化,理解本篇文章还是很简单的。

flip概念

首先我们说说flip这几个字母的含义:

F:first,参加过渡元素的初始状态。
L:last,元素的终止状态。
I:invert,这是flip的核心。你通过这个元素的初始状态和终止状态计算出元素改变了什么,比如它的宽、高及透明度,然后你翻转这个改变;举个例子,如果一个元素的初始状态和终止状态之间偏移90px,你应该设置这个元素transform: translateY(-90px)。这个元素好像是在它的初始位置,其实正好相反。
P:play,为你要改变的任何css属性启用tansition,移除你invert的改变。这时你的元素会做动画从起始点到终止点。

以下是代码示例:

//js
var app = document.getElementById('app');
var first = app.getBoundingClientRect();
app.classList.add('app-to-end');
var last = app.getBoundingClientRect();
var invert = first.top - last.top;
//使元素看起来好像在起始点
app.style.transform = `translateY(${invert}px)`;
requestAnimationFrame(function() {
   //启用tansition,并移除翻转的改变
  app.classList.add('animate-on-transforms');
  app.style.transform = '';
});
app.addEventListener('transitionend', () => {
  app.classList.remove('animate-on-transforms');
})
<div id="app"></div>
<style>
  #app{
    position: absolute;
    width:20px;
    height:20px;
    background: red;
  }
  .app-to-end{
    top: 100px;
  }
  .animate-on-transforms{
    transition: all 2s;
  }
</style>

使用flip的好处

看到这里,如果你看过我的上一篇文章一篇文章说清浏览器解析和CSS(GPU)动画优化,你知道getBoundingClientRect()是一个耗费昂贵的操作,它会迫使浏览器发生一次重排。那么为什么我们可以做这消耗不菲的操作呢?
图片描述

如上图所示,在用户与网站交互后有100ms的空闲时间,如果我们利用这100ms做预计算操作,然后使用css3的transform和opacity执行动画,用户会觉得你的网站响应非常快。

注意事项

1、别超过100ms的空闲期:一旦超过这个空闲期,就会造成卡顿的状况出现;使用开发者工具注意这一点。
2、仔细安排动画:想象一下你正在执行你动画中的一个,然后你执行另外一个;这个需要大量的预计算。这会打断正在运行的动画,这是糟糕的。关键是确保你的预计算在用户响应的空闲时间执行,这样两个动画就不会冲突了。
3、使用transform和scale时,元素会被扭曲;虽然可以重构标签避免扭曲,但最终他们会相互影响。

总结

flip是一个如何做动画的思考方式,它是使css和js非常好的配合。使用js做计算,使用css做动画。使用css做动画不是一定的,你也可以使用Web Animations API或者单单JavaScript。关键是你要减少每一帧的复杂度(推荐使用transform和opacity)。

参考

https://aerotwist.com/blog/fl...

查看原文

赞 9 收藏 12 评论 0

小结点 赞了文章 · 5月28日

一篇文章说清浏览器解析和CSS(GPU)动画优化

相信不少人在做移动端动画的时候遇到了卡顿的问题,这篇文章尝试从浏览器渲染的角度;一点一点告诉你动画优化的原理及其技巧,作为你工作中优化动画的参考。文末有优化技巧的总结。

因为GPU合成没有官方规范,每个浏览器的问题和解决方式也不同;所以文章内容仅供参考。

浏览器渲染

提高动画的优化不得不提及浏览器是如何渲染一个页面。在从服务器中拿到数据后,浏览器会先做解析三类东西:

  • 解析html,xhtml,svg这三类文档,形成dom树。

  • 解析css,产生css rule tree。

  • 解析js,js会通过api来操作dom tree和css rule tree。

解析完成之后,浏览器引擎会通过dom tree和css rule tree来构建rendering tree:

  • rendering tree和dom tree并不完全相同,例如:<head></head>或display:none的东西就不会放在渲染树中。

  • css rule tree主要是完成匹配,并把css rule附加给rendering tree的每个element。

在渲染树构建完成后,

  • 浏览器会对这些元素进行定位和布局,这一步也叫做reflow或者layout。

  • 浏览器绘制这些元素的样式,颜色,背景,大小及边框等,这一步也叫做repaint。

  • 然后浏览器会将各层的信息发送给GPU,GPU会将各层合成;显示在屏幕上。

渲染优化原理

如上所说,渲染树构建完成后;浏览器要做的步骤:

reflow——》repaint——》composite

reflow和repaint

reflow和repaint都是耗费浏览器性能的操作,这两者尤以reflow为甚;因为每次reflow,浏览器都要重新计算每个元素的形状和位置。

由于reflow和repaint都是非常消耗性能的,我们的浏览器为此做了一些优化。浏览器会将reflow和repaint的操作积攒一批,然后做一次reflow。但是有些时候,你的代码会强制浏览器做多次reflow。例如:

var content = document.getElementById('content');
content.style.width = 700px;
var contentWidth = content.offsetWidth;
content.style.backgound = 'red';

以上第三行代码,需要浏览器reflow后;再获取值,所以会导致浏览器多做一次reflow。

下面是一些针对reflow和repaint的最佳实践:

  • 不要一条一条地修改dom的样式,尽量使用className一次修改。

  • 将dom离线后修改

    • 使用documentFragment对象在内存里操作dom。

    • 先把dom节点display:none;(会触发一次reflow)。然后做大量的修改后,再把它显示出来。

    • clone一个dom节点在内存里,修改之后;与在线的节点相替换。

  • 不要使用table布局,一个小改动会造成整个table的重新布局。

  • transform和opacity只会引起合成,不会引起布局和重绘。

从上述的最佳实践中你可能发现,动画优化一般都是尽可能地减少reflow、repaint的发生。关于哪些属性会引起reflow、repaint及composite,你可以在这个网站找到https://csstriggers.com/

composite

在reflow和repaint之后,浏览器会将多个复合层传入GPU;进行合成工作,那么合成是如何工作的呢?

假设我们的页面中有A和B两个元素,它们有absolute和z-index属性;浏览器会重绘它们,然后将图像发送给GPU;然后GPU将会把多个图像合成展示在屏幕上。

<style>
#a, #b {
 position: absolute;
}

#a {
 left: 30px;
 top: 30px;
 z-index: 2;
}

#b {
 z-index: 1;
}
</style>
<div id="#a">A</div>
<div id="#b">B</div>

clipboard.png

我们将A元素使用left属性,做一个移动动画:

<style>
#a, #b {
 position: absolute;
}

#a {
 left: 10px;
 top: 10px;
 z-index: 2;
 animation: move 1s linear;
}

#b {
 left: 50px;
 top: 50px;
 z-index: 1;
}

@keyframes move {
 from { left: 30px; }
 to { left: 100px; }
}
</style>
<div id="#a">A</div>
<div id="#b">B</div>

在这个例子中,对于动画的每一帧;浏览器会计算元素的几何形状,渲染新状态的图像;并把它们发送给GPU。(你没看错,position也会引起浏览器重排的)尽管浏览器做了优化,在repaint时,只会repaint部分区域;但是我们的动画仍然不够流畅。

因为重排和重绘发生在动画的每一帧,一个有效避免reflow和repaint的方式是我们仅仅画两个图像;一个是a元素,一个是b元素及整个页面;我们将这两张图片发送给GPU,然后动画发生的时候;只做两张图片相对对方的平移。也就是说,仅仅合成缓存的图片将会很快;这也是GPU的优势——它能非常快地以亚像素精度地合成图片,并给动画带来平滑的曲线。

为了仅发生composite,我们做动画的css property必须满足以下三个条件:

  • 不影响文档流。

  • 不依赖文档流。

  • 不会造成重绘。

满足以上以上条件的css property只有transform和opacity。你可能以为position也满足以上条件,但事实不是这样,举个例子left属性可以使用百分比的值,依赖于它的offset parent。还有em、vh等其他单位也依赖于他们的环境。

我们使用translate来代替left

<style>
#a, #b {
 position: absolute;
}

#a {
 left: 10px;
 top: 10px;
 z-index: 2;
 animation: move 1s linear;
}

#b {
 left: 50px;
 top: 50px;
 z-index: 1;
}

@keyframes move {
 from { transform: translateX(0); }
 to { transform: translateX(70px); }
}
</style>
<div id="#a">A</div>
<div id="#b">B</div>

浏览器在动画执行之前就知道动画如何开始和结束,因为浏览器没有看到需要reflow和repaint的操作;浏览器就会画两张图像作为复合层,并将它们传入GPU。

这样做有两个优势:

  • 动画将会非常流畅

  • 动画不在绑定到CPU,即使js执行大量的工作;动画依然流畅。

看起来性能问题好像已经解决了?在下文你会看到GPU动画的一些问题。

GPU是如何合成图像的

GPU实际上可以看作一个独立的计算机,它有自己的处理器和存储器及数据处理模型。当浏览器向GPU发送消息的时候,就像向一个外部设备发送消息。

你可以把浏览器向GPU发送数据的过程,与使用ajax向服务器发送消息非常类似。想一下,你用ajax向服务器发送数据,服务器是不会直接接受浏览器的存储的信息的。你需要收集页面上的数据,把它们放进一个载体里面(例如JSON),然后发送数据到远程服务器。

同样的,浏览器向GPU发送数据也需要先创建一个载体;只不过GPU距离CPU很近,不会像远程服务器那样可能几千里那么远。但是对于远程服务器,2秒的延迟是可以接受的;但是对于GPU,几毫秒的延迟都会造成动画的卡顿。

浏览器向GPU发送的数据载体是什么样?这里给出一个简单的制作载体,并把它们发送到GPU的过程。

  • 画每个复合层的图像

  • 准备图层的数据

  • 准备动画的着色器(如果需要)

  • 向GPU发送数据

所以你可以看到,每次当你添加transform:translateZ(0)will-change:transform给一个元素,你都会做同样的工作。重绘是非常消耗性能的,在这里它尤其缓慢。在大多数情况,浏览器不能增量重绘。它不得不重绘先前被复合层覆盖的区域。

隐式合成

还记得刚才a元素和b元素动画的例子吗?现在我们将b元素做动画,a元素静止不动。

clipboard.png

和刚才的例子不同,现在b元素将拥有一个独立复合层;然后它们将被GPU合成。但是因为a元素要在b元素的上面(因为a元素的z-index比b元素高),那么浏览器会做什么?浏览器会将a元素也单独做一个复合层!

所以我们现在有三个复合层a元素所在的复合层、b元素所在的复合层、其他内容及背景层。

一个或多个没有自己复合层的元素要出现在有复合层元素的上方,它就会拥有自己的复合层;这种情况被称为隐式合成。

浏览器将a元素提升为一个复合层有很多种原因,下面列举了一些:

  • 3d或透视变换css属性,例如translate3d,translateZ等等(js一般通过这种方式,使元素获得复合层)

  • <video><iframe><canvas><webgl>等元素。

  • 混合插件(如flash)。

  • 元素自身的 opacity和transform 做 CSS 动画。

  • 拥有css过滤器的元素。

  • 使用will-change属性。

  • position:fixed。

  • 元素有一个 z-index 较低且包含一个复合层的兄弟元素(换句话说就是该元素在复合层上面渲染)

这看起来css动画的性能瓶颈是在重绘上,但是真实的问题是在内存上:

内存占用

使用GPU动画需要发送多张渲染层的图像给GPU,GPU也需要缓存它们以便于后续动画的使用。

一个渲染层,需要多少内存占用?为了便于理解,举一个简单的例子;一个宽、高都是300px的纯色图像需要多少内存?

300 300 4 = 360000字节,即360kb。这里乘以4是因为,每个像素需要四个字节计算机内存来描述。

假设我们做一个轮播图组件,轮播图有10张图片;为了实现图片间平滑过渡的交互;为每个图像添加了will-change:transform。这将提升图像为复合层,它将多需要19mb的空间。800 600 4 * 10 = 1920000。

仅仅是一个轮播图组件就需要19m的额外空间!

在chrome的开发者工具中打开setting——》Experiments——》layers可以看到每个层的内存占用。如图所示:

clipboard.png

clipboard.png

GPU动画的优点和缺点

现在我们可以总结一下GPU动画的优点和缺点:

  • 每秒60帧,动画平滑、流畅。

  • 一个合适的动画工作在一个单独的线程,它不会被大量的js计算阻塞。

  • 3D“变换”是便宜的。

缺点:

  • 提升一个元素到复合层需要额外的重绘,有时这是慢的。(即我们得到的是一个全层重绘,而不是一个增量)

  • 绘图层必须传输到GPU。取决于层的数量和传输可能会非常缓慢。这可能让一个元素在中低档设备上闪烁。

  • 每个复合层都需要消耗额外的内存,过多的内存可能导致浏览器的崩溃。

  • 如果你不考虑隐式合成,而使用重绘;会导致额外的内存占用,并且浏览器崩溃的概率是非常高的。

  • 我们会有视觉假象,例如在Safari中的文本渲染,在某些情况下页面内容将消失或变形。

优化技巧

避免隐式合成

  • 保持动画的对象的z-index尽可能的高。理想的,这些元素应该是body元素的直接子元素。当然,这不是总可能的。所以你可以克隆一个元素,把它放在body元素下仅仅是为了做动画。

  • 将元素上设置will-change CSS属性,元素上有了这个属性,浏览器会提升这个元素成为一个复合层(不是总是)。这样动画就可以平滑的开始和结束。但是不要滥用这个属性,否则会大大增加内存消耗。

动画中只使用transform和opacity

如上所说,transform和opacity保证了元素属性的变化不影响文档流、也不受文档流影响;并且不会造成repaint。
有些时候你可能想要改变其他的css属性,作为动画。例如:你可能想使用background属性改变背景:

<div class="bg-change"></div>
.bg-change {
  width: 100px;
  height: 100px;
  background: red;
  transition: opacity 2s;
}
.bg-change:hover {
  background: blue;
}

在这个例子中,在动画的每一步;浏览器都会进行一次重绘。我们可以使用一个复层在这个元素上面,并且仅仅变换opacity属性:

<div class="bg-change"></div>
<style>
.bg-change {
  width: 100px;
  height: 100px;
  background: red;
}
.bg-change::before {
  content: '';
  display: block;
  width: 100%;
  height: 100%;
  background: blue;
  opacity: 0;
  transition: opacity 20s;
}
.bg-change:hover::before {
  opacity: 1;
}
</style>

减小复合层的尺寸

看一下两张图片,有什么不同吗?

clipboard.png

这两张图片视觉上是一样的,但是它们的尺寸一个是39kb;另外一个是400b。不同之处在于,第二个纯色层是通过scale放大10倍做到的。

<div id="a"></div>
<div id="b"></div>

<style>
#a, #b {
 will-change: transform;
}

#a {
 width: 100px;
 height: 100px;
}

#b {
 width: 10px;
 height: 10px;
 transform: scale(10);
}
</style>

对于图片,你要怎么做呢?你可以将图片的尺寸减少5%——10%,然后使用scale将它们放大;用户不会看到什么区别,但是你可以减少大量的存储空间。

用css动画而不是js动画

css动画有一个重要的特性,它是完全工作在GPU上。因为你声明了一个动画如何开始和如何结束,浏览器会在动画开始前准备好所有需要的指令;并把它们发送给GPU。而如果使用js动画,浏览器必须计算每一帧的状态;为了保证平滑的动画,我们必须在浏览器主线程计算新状态;把它们发送给GPU至少60次每秒。除了计算和发送数据比css动画要慢,主线程的负载也会影响动画; 当主线程的计算任务过多时,会造成动画的延迟、卡顿。

所以尽可能地使用基于css的动画,不仅仅更快;也不会被大量的js计算所阻塞。

优化技巧总结

  • 减少浏览器的重排和重绘的发生。

  • 不要使用table布局。

  • css动画中尽量只使用transform和opacity,这不会发生重排和重绘。

  • 尽可能地只使用css做动画。

  • 避免浏览器的隐式合成。

  • 改变复合层的尺寸。

参考

GPU合成主要参考:

https://www.smashingmagazine....

哪些属性会引起reflow、repaint及composite,你可以在这个网站找到:

https://csstriggers.com/

查看原文

赞 102 收藏 155 评论 9

小结点 赞了文章 · 5月28日

带你彻底弄懂Event Loop

前言

我在学习浏览器和NodeJS的Event Loop时看了大量的文章,那些文章都写的很好,但是往往是每篇文章有那么几个关键的点,很多篇文章凑在一起综合来看,才可以对这些概念有较为深入的理解。

于是,我在看了大量文章之后,想要写这么一篇博客,不采用官方的描述,结合自己的理解以及示例代码,用最通俗的语言表达出来。希望大家可以通过这篇文章,了解到Event Loop到底是一种什么机制,浏览器和NodeJS的Event Loop又有什么区别。如果在文中出现书写错误的地方,欢迎大家留言一起探讨。

(PS:说到Event Loop肯定会提到Promise,我根据Promise A+规范自己实现了一个简易Promise库,源码放到Github上,大家有需要的可以当做参考,后续我也会也写一篇博客来讲Promise,如果对你有用,就请给个Star吧~)

正文

Event Loop是什么

event loop是一个执行模型,在不同的地方有不同的实现。浏览器和NodeJS基于不同的技术实现了各自的Event Loop。

  • 浏览器的Event Loop是在html5的规范中明确定义。
  • NodeJS的Event Loop是基于libuv实现的。可以参考Node的官方文档以及libuv的官方文档
  • libuv已经对Event Loop做出了实现,而HTML5规范中只是定义了浏览器中Event Loop的模型,具体的实现留给了浏览器厂商。

宏队列和微队列

宏队列,macrotask,也叫tasks。 一些异步任务的回调会依次进入macro task queue,等待后续被调用,这些异步任务包括:

  • setTimeout
  • setInterval
  • setImmediate (Node独有)
  • requestAnimationFrame (浏览器独有)
  • I/O
  • UI rendering (浏览器独有)

微队列,microtask,也叫jobs。 另一些异步任务的回调会依次进入micro task queue,等待后续被调用,这些异步任务包括:

  • process.nextTick (Node独有)
  • Promise
  • Object.observe
  • MutationObserver

(注:这里只针对浏览器和NodeJS)

浏览器的Event Loop

我们先来看一张图,再看完这篇文章后,请返回来再仔细看一下这张图,相信你会有更深的理解。

browser-eventloop

这张图将浏览器的Event Loop完整的描述了出来,我来讲执行一个JavaScript代码的具体流程:

  1. 执行全局Script同步代码,这些同步代码有一些是同步语句,有一些是异步语句(比如setTimeout等);
  2. 全局Script代码执行完毕后,调用栈Stack会清空;
  3. 从微队列microtask queue中取出位于队首的回调任务,放入调用栈Stack中执行,执行完后microtask queue长度减1;
  4. 继续取出位于队首的任务,放入调用栈Stack中执行,以此类推,直到直到把microtask queue中的所有任务都执行完毕。注意,如果在执行microtask的过程中,又产生了microtask,那么会加入到队列的末尾,也会在这个周期被调用执行
  5. microtask queue中的所有任务都执行完毕,此时microtask queue为空队列,调用栈Stack也为空;
  6. 取出宏队列macrotask queue中位于队首的任务,放入Stack中执行;
  7. 执行完毕后,调用栈Stack为空;
  8. 重复第3-7个步骤;
  9. 重复第3-7个步骤;
  10. ......

可以看到,这就是浏览器的事件循环Event Loop

这里归纳3个重点:

  1. 宏队列macrotask一次只从队列中取一个任务执行,执行完后就去执行微任务队列中的任务;
  2. 微任务队列中所有的任务都会被依次取出来执行,知道microtask queue为空;
  3. 图中没有画UI rendering的节点,因为这个是由浏览器自行判断决定的,但是只要执行UI rendering,它的节点是在执行完所有的microtask之后,下一个macrotask之前,紧跟着执行UI render。

好了,概念性的东西就这么多,来看几个示例代码,测试一下你是否掌握了:

console.log(1);

setTimeout(() => {
  console.log(2);
  Promise.resolve().then(() => {
    console.log(3)
  });
});

new Promise((resolve, reject) => {
  console.log(4)
  resolve(5)
}).then((data) => {
  console.log(data);
})

setTimeout(() => {
  console.log(6);
})

console.log(7);

这里结果会是什么呢?运用上面了解到的知识,先自己做一下试试看。

// 正确答案
1
4
7
5
2
3
6

你答对了吗?

我们来分析一下整个流程:


  • 执行全局Script代码

Step 1

console.log(1)

Stack Queue: [console]

Macrotask Queue: []

Microtask Queue: []

打印结果:
1

Step 2

setTimeout(() => {
  // 这个回调函数叫做callback1,setTimeout属于macrotask,所以放到macrotask queue中
  console.log(2);
  Promise.resolve().then(() => {
    console.log(3)
  });
});

Stack Queue: [setTimeout]

Macrotask Queue: [callback1]

Microtask Queue: []

打印结果:
1

Step 3

new Promise((resolve, reject) => {
  // 注意,这里是同步执行的,如果不太清楚,可以去看一下我开头自己实现的promise啦~~
  console.log(4)
  resolve(5)
}).then((data) => {
  // 这个回调函数叫做callback2,promise属于microtask,所以放到microtask queue中
  console.log(data);
})

Stack Queue: [promise]

Macrotask Queue: [callback1]

Microtask Queue: [callback2]

打印结果:
1
4

Step 5

setTimeout(() => {
  // 这个回调函数叫做callback3,setTimeout属于macrotask,所以放到macrotask queue中
  console.log(6);
})

Stack Queue: [setTimeout]

Macrotask Queue: [callback1, callback3]

Microtask Queue: [callback2]

打印结果:
1
4

Step 6

console.log(7)

Stack Queue: [console]

Macrotask Queue: [callback1, callback3]

Microtask Queue: [callback2]

打印结果:
1
4
7

  • 好啦,全局Script代码执行完了,进入下一个步骤,从microtask queue中依次取出任务执行,直到microtask queue队列为空。

Step 7

console.log(data)       // 这里data是Promise的决议值5

Stack Queue: [callback2]

Macrotask Queue: [callback1, callback3]

Microtask Queue: []

打印结果:
1
4
7
5

  • 这里microtask queue中只有一个任务,执行完后开始从宏任务队列macrotask queue中取位于队首的任务执行

Step 8

console.log(2)

Stack Queue: [callback1]

Macrotask Queue: [callback3]

Microtask Queue: []

打印结果:
1
4
7
5
2

但是,执行callback1的时候又遇到了另一个Promise,Promise异步执行完后在microtask queue中又注册了一个callback4回调函数

Step 9

Promise.resolve().then(() => {
  // 这个回调函数叫做callback4,promise属于microtask,所以放到microtask queue中
  console.log(3)
});

Stack Queue: [promise]

Macrotask v: [callback3]

Microtask Queue: [callback4]

打印结果:
1
4
7
5
2

  • 取出一个宏任务macrotask执行完毕,然后再去微任务队列microtask queue中依次取出执行

Step 10

console.log(3)

Stack Queue: [callback4]

Macrotask Queue: [callback3]

Microtask Queue: []

打印结果:
1
4
7
5
2
3

  • 微任务队列全部执行完,再去宏任务队列中取第一个任务执行

Step 11

console.log(6)

Stack Queue: [callback3]

Macrotask Queue: []

Microtask Queue: []

打印结果:
1
4
7
5
2
3
6

  • 以上,全部执行完后,Stack Queue为空,Macrotask Queue为空,Micro Queue为空

Stack Queue: []

Macrotask Queue: []

Microtask Queue: []

最终打印结果:
1
4
7
5
2
3
6

因为是第一个例子,所以这里分析的比较详细,大家仔细看一下,接下来我们再来一个例子:

console.log(1);

setTimeout(() => {
  console.log(2);
  Promise.resolve().then(() => {
    console.log(3)
  });
});

new Promise((resolve, reject) => {
  console.log(4)
  resolve(5)
}).then((data) => {
  console.log(data);
  
  Promise.resolve().then(() => {
    console.log(6)
  }).then(() => {
    console.log(7)
    
    setTimeout(() => {
      console.log(8)
    }, 0);
  });
})

setTimeout(() => {
  console.log(9);
})

console.log(10);

最终输出结果是什么呢?参考前面的例子,好好想一想......

// 正确答案
1
4
10
5
6
7
2
3
9
8

相信大家都答对了,这里的关键在前面已经提过:

在执行微队列microtask queue中任务的时候,如果又产生了microtask,那么会继续添加到队列的末尾,也会在这个周期执行,直到microtask queue为空停止。

注:当然如果你在microtask中不断的产生microtask,那么其他宏任务macrotask就无法执行了,但是这个操作也不是无限的,拿NodeJS中的微任务process.nextTick()来说,它的上限是1000个,后面我们会讲到。

浏览器的Event Loop就说到这里,下面我们看一下NodeJS中的Event Loop,它更复杂一些,机制也不太一样。

NodeJS中的Event Loop

libuv

先来看一张libuv的结构图:

node-libuv

NodeJS中的宏队列和微队列

NodeJS的Event Loop中,执行宏队列的回调任务有6个阶段,如下图:

node-eventloop-6phase

各个阶段执行的任务如下:

  • timers阶段:这个阶段执行setTimeout和setInterval预定的callback
  • I/O callback阶段:执行除了close事件的callbacks、被timers设定的callbacks、setImmediate()设定的callbacks这些之外的callbacks
  • idle, prepare阶段:仅node内部使用
  • poll阶段:获取新的I/O事件,适当的条件下node将阻塞在这里
  • check阶段:执行setImmediate()设定的callbacks
  • close callbacks阶段:执行socket.on('close', ....)这些callbacks

NodeJS中宏队列主要有4个

由上面的介绍可以看到,回调事件主要位于4个macrotask queue中:

  1. Timers Queue
  2. IO Callbacks Queue
  3. Check Queue
  4. Close Callbacks Queue

这4个都属于宏队列,但是在浏览器中,可以认为只有一个宏队列,所有的macrotask都会被加到这一个宏队列中,但是在NodeJS中,不同的macrotask会被放置在不同的宏队列中。

NodeJS中微队列主要有2个

  1. Next Tick Queue:是放置process.nextTick(callback)的回调任务的
  2. Other Micro Queue:放置其他microtask,比如Promise等

在浏览器中,也可以认为只有一个微队列,所有的microtask都会被加到这一个微队列中,但是在NodeJS中,不同的microtask会被放置在不同的微队列中。

具体可以通过下图加深一下理解:

node-eventloop

大体解释一下NodeJS的Event Loop过程:

  1. 执行全局Script的同步代码
  2. 执行microtask微任务,先执行所有Next Tick Queue中的所有任务,再执行Other Microtask Queue中的所有任务
  3. 开始执行macrotask宏任务,共6个阶段,从第1个阶段开始执行相应每一个阶段macrotask中的所有任务,注意,这里是所有每个阶段宏任务队列的所有任务,在浏览器的Event Loop中是只取宏队列的第一个任务出来执行,每一个阶段的macrotask任务执行完毕后,开始执行微任务,也就是步骤2
  4. Timers Queue -> 步骤2 -> I/O Queue -> 步骤2 -> Check Queue -> 步骤2 -> Close Callback Queue -> 步骤2 -> Timers Queue ......
  5. 这就是Node的Event Loop

关于NodeJS的macrotask queue和microtask queue,我画了两张图,大家作为参考:

node-microtaskqueue

node-macrotaskqueue

好啦,概念理解了我们通过几个例子来实战一下:

第一个例子

console.log('start');

setTimeout(() => {          // callback1
  console.log(111);
  setTimeout(() => {        // callback2
    console.log(222);
  }, 0);
  setImmediate(() => {      // callback3
    console.log(333);
  })
  process.nextTick(() => {  // callback4
    console.log(444);  
  })
}, 0);

setImmediate(() => {        // callback5
  console.log(555);
  process.nextTick(() => {  // callback6
    console.log(666);  
  })
})

setTimeout(() => {          // callback7              
  console.log(777);
  process.nextTick(() => {  // callback8
    console.log(888);   
  })
}, 0);

process.nextTick(() => {    // callback9
  console.log(999);  
})

console.log('end');

请运用前面学到的知识,仔细分析一下......

// 正确答案
start
end
999
111
777
444
888
555
333
666
222


更新 2018.9.20

上面这段代码你执行的结果可能会有多种情况,原因解释如下。

  • setTimeout(fn, 0)不是严格的0,一般是setTimeout(fn, 3)或什么,会有一定的延迟时间,当setTimeout(fn, 0)和setImmediate(fn)出现在同一段同步代码中时,就会存在两种情况。
  • 第1种情况:同步代码执行完了,Timer还没到期,setImmediate回调先注册到Check Queue中,开始执行微队列,然后是宏队列,先从Timers Queue中开始,发现没回调,往下走直到Check Queue中有回调,执行,然后timer到期(只要在执行完Timer Queue后到期效果就都一样),timer回调注册到Timers Queue中,下一轮循环执行到Timers Queue中才能执行那个timer 回调;所以,这种情况下,setImmediate(fn)回调先于setTimeout(fn, 0)回调执行
  • 第2种情况:同步代码还没执行完,timer先到期,timer回调先注册到Timers Queue中,执行到setImmediate了,它的回调再注册到Check Queue中。 然后,同步代码执行完了,执行微队列,然后开始先执行Timers Queue,先执行Timer 回调,再到Check Queue,执行setImmediate回调;所以,这种情况下,setTimeout(fn, 0)回调先于setImmediate(fn)回调执行
  • 所以,在同步代码中同时调setTimeout(fn, 0)和setImmediate情况是不确定的,但是如果把他们放在一个IO的回调,比如readFile('xx', function () {// ....})回调中,那么IO回调是在IO Queue中,setTimeout到期回调注册到Timers Queue,setImmediate回调注册到Check Queue,IO Queue执行完到Check Queue,timer Queue得到下个周期,所以setImmediate回调这种情况下肯定比setTimeout(fn, 0)回调先执行。

综上,这个例子是不太好的,setTimeout(fn, 0)和setImmediate(fn)如果想要保证结果唯一,就放在一个IO Callback中吧,上面那段代码可以把所有它俩同步执行的代码都放在一个IO Callback中,结果就唯一了。

更新结束



你答对了吗?我们来一起分析一下:

  • 执行全局Script代码,先打印start,向下执行,将setTimeout的回调callback1注册到Timers Queue中,再向下执行,将setImmediate的回调callback5注册到Check Queue中,接着向下执行,将setTimeout的回调callback7注册到Timers Queue中,继续向下,将process.nextTick的回调callback9注册到微队列Next Tick Queue中,最后一步打印end。此时,各个队列的回调情况如下:

宏队列

Timers Queue: [callback1, callback7]

Check Queue: [callback5]

IO Callback Queue: []

Close Callback Queue: []

微队列

Next Tick Queue: [callback9]

Other Microtask Queue: []

打印结果
start
end
  • 全局Script执行完了,开始依次执行微任务Next Tick Queue中的全部回调任务。此时Next Tick Queue中只有一个callback9,将其取出放入调用栈中执行,打印999

宏队列

Timers Queue: [callback1, callback7]

Check Queue: [callback5]

IO Callback Queue: []

Close Callback Queue: []

微队列

Next Tick Queue: []

Other Microtask Queue: []

打印结果
start
end
999
  • 开始依次执行6个阶段各自宏队列中的所有任务,先执行第1个阶段Timers Queue中的所有任务,先取出callback1执行,打印111,callback1函数继续向下,依次把callback2放入Timers Queue中,把callback3放入Check Queue中,把callback4放入Next Tick Queue中,然后callback1执行完毕。再取出Timers Queue中此时排在首位的callback7执行,打印777,把callback8放入Next Tick Queue中,执行完毕。此时,各队列情况如下:

宏队列

Timers Queue: [callback2]

Check Queue: [callback5, callback3]

IO Callback Queue: []

Close Callback Queue: []

微队列

Next Tick Queue: [callback4, callback8]

Other Microtask Queue: []

打印结果
start
end
999
111
777
  • 6个阶段每阶段的宏任务队列执行完毕后,都会开始执行微任务,此时,先取出Next Tick Queue中的所有任务执行,callback4开始执行,打印444,然后callback8开始执行,打印888,Next Tick Queue执行完毕,开始执行Other Microtask Queue中的任务,因为里面为空,所以继续向下。

宏队列

Timers Queue: [callback2]

Check Queue: [callback5, callback3]

IO Callback Queue: []

Close Callback Queue: []

微队列

Next Tick Queue: []

Other Microtask Queue: []

打印结果
start
end
999
111
777
444
888
  • 第2个阶段IO Callback Queue队列为空,跳过,第3和第4个阶段一般是Node内部使用,跳过,进入第5个阶段Check Queue。取出callback5执行,打印555,把callback6放入Next Tick Queue中,执行callback3,打印333

宏队列

Timers Queue: [callback2]

Check Queue: []

IO Callback Queue: []

Close Callback Queue: []

微队列

Next Tick Queue: [callback6]

Other Microtask Queue: []

打印结果
start
end
999
111
777
444
888
555
333
  • 执行微任务队列,先执行Next Tick Queue,取出callback6执行,打印666,执行完毕,因为Other Microtask Queue为空,跳过。

宏队列

Timers Queue: [callback2]

Check Queue: []

IO Callback Queue: []

Close Callback Queue: []

微队列

Next Tick Queue: [callback6]

Other Microtask Queue: []

打印结果
start
end
999
111
777
444
888
555
333
  • 执行第6个阶段Close Callback Queue中的任务,为空,跳过,好了,此时一个循环已经结束。进入下一个循环,执行第1个阶段Timers Queue中的所有任务,取出callback2执行,打印222,完毕。此时,所有队列包括宏任务队列和微任务队列都为空,不再打印任何东西。

宏队列

Timers Queue: []

Check Queue: []

IO Callback Queue: []

Close Callback Queue: []

微队列

Next Tick Queue: [callback6]

Other Microtask Queue: []

最终结果
start
end
999
111
777
444
888
555
333
666
222

以上就是这道题目的详细分析,如果没有明白,一定要多看几次。


下面引入Promise再来看一个例子:

console.log('1');

setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})

new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})
process.nextTick(function() {
  console.log('6');
})

setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})

大家仔细分析,相比于上一个例子,这里由于存在Promise,所以Other Microtask Queue中也会有回调任务的存在,执行到微任务阶段时,先执行Next Tick Queue中的所有任务,再执行Other Microtask Queue中的所有任务,然后才会进入下一个阶段的宏任务。明白了这一点,相信大家都可以分析出来,下面直接给出正确答案,如有疑问,欢迎留言和我讨论。

// 正确答案
1
7
6
8
2
4
9
11
3
10
5
12

setTimeout 对比 setImmediate

  • setTimeout(fn, 0)在Timers阶段执行,并且是在poll阶段进行判断是否达到指定的timer时间才会执行
  • setImmediate(fn)在Check阶段执行

两者的执行顺序要根据当前的执行环境才能确定:

  • 如果两者都在主模块(main module)调用,那么执行先后取决于进程性能,顺序随机
  • 如果两者都不在主模块调用,即在一个I/O Circle中调用,那么setImmediate的回调永远先执行,因为会先到Check阶段

setImmediate 对比 process.nextTick

  • setImmediate(fn)的回调任务会插入到宏队列Check Queue中
  • process.nextTick(fn)的回调任务会插入到微队列Next Tick Queue中
  • process.nextTick(fn)调用深度有限制,上限是1000,而setImmedaite则没有

总结

  1. 浏览器的Event Loop和NodeJS的Event Loop是不同的,实现机制也不一样,不要混为一谈。
  2. 浏览器可以理解成只有1个宏任务队列和1个微任务队列,先执行全局Script代码,执行完同步代码调用栈清空后,从微任务队列中依次取出所有的任务放入调用栈执行,微任务队列清空后,从宏任务队列中只取位于队首的任务放入调用栈执行,注意这里和Node的区别,只取一个,然后继续执行微队列中的所有任务,再去宏队列取一个,以此构成事件循环。
  3. NodeJS可以理解成有4个宏任务队列和2个微任务队列,但是执行宏任务时有6个阶段。先执行全局Script代码,执行完同步代码调用栈清空后,先从微任务队列Next Tick Queue中依次取出所有的任务放入调用栈中执行,再从微任务队列Other Microtask Queue中依次取出所有的任务放入调用栈中执行。然后开始宏任务的6个阶段,每个阶段都将该宏任务队列中的所有任务都取出来执行(注意,这里和浏览器不一样,浏览器只取一个),每个宏任务阶段执行完毕后,开始执行微任务,再开始执行下一阶段宏任务,以此构成事件循环。
  4. MacroTask包括: setTimeout、setInterval、 setImmediate(Node)、requestAnimation(浏览器)、IO、UI rendering
  5. Microtask包括: process.nextTick(Node)、Promise、Object.observe、MutationObserver

第3点修改:
Node 在新版本中,也是每个 Macrotask 执行完后,就去执行 Microtask 了,和浏览器的模型一致。
node队列

欢迎关注我的公众号

微信公众号

参考链接

不要混淆nodejs和浏览器中的event loop
node中的Event模块
Promises, process.nextTick And setImmediate
浏览器和Node不同的事件循环
Tasks, microtasks, queues and schedules
理解事件循环浅析

查看原文

赞 220 收藏 160 评论 22

认证与成就

  • 获得 12 次点赞
  • 获得 8 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 8 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2018-09-01
个人主页被 477 人浏览