zp1996

zp1996 查看完整档案

天津编辑  |  填写毕业院校今日头条  |  打杂 编辑 blog.csdn.net/zp1996323 编辑
编辑

coder

个人动态

zp1996 评论了文章 · 2018-10-02

百度阿里网易大疆等大小厂前端校招面筋

自我介绍下:某985硕士,程序媛,接触前端一年时间。从八月份开始校招面试笔试,前前后后大厂小厂也都面了挺多,不过大厂基本都被我挂完了,哭晕我,还是太菜啊。面过的公司:ThoughtWorks,大疆,阿里,网易,百度,电信it研发中心,深信服,华为,小米,搜狗,腾讯。拿了offer的公司目前是:大疆、电信、深信服、华为。下面总结了这段时间的面筋和挂筋~

腾讯

10月16日更新,面了腾讯一面,然后就挂了,结束的太快就像龙卷风。

1. 怎么从十万个节点中找到想要的节点,怎么快速在某个节点前插入一个节点?

这个题目我已经在segmentfault上提问了,目前还没有人给出回答,求大神前去解答~

题目链接: 怎么在有十万个节点的html文档中找到特定的某个节点

2. 负载均衡方式和容错机制

3. 描述一个文件从硬盘读取到进程内存中的过程

4. TCP三次握手和挥手的过程,除了这些书本上的还有哪些?

5. 画一下OSI七层模型

6. 写一个你最熟悉的算法

7. 有哪些伪类和伪元素

8. IE盒模型和标准盒模型,用哪个属性改变

9. 怎么清除浮动,原理是什么

10. em,rem,px的区别

大疆

大疆是我校招面的第一家公司,从六月份投简历,然后笔试面试到拿到录用意向书,前后用了近四个月,来之不易啊。

一面二面

因为时间太久,就直接放在一起了,问的都是基础吧,讲真,大疆前端面试不难,都是很基础的,就是时间长,等的捉急。一面是电话面,两个面试官轮流问;二面是视频面,是三个面试官一起微信视频,视频面还是蛮累的,上下左右都得顾上;终面是去的现场面,就跟一个面试官聊了十几分钟人生。

1. meta标签

meta标签:提供给页面的一些元信息(名称/值对), 比如针对搜索引擎和更新频度的描述和关键词。

  • name:名称/值对中的名称。常用的有author、description、keywords、generator、revised、others。 把 content 属性关联到一个名称。
  • http-equiv:没有name时,会采用这个属性的值。常用的有content-type、expires、refresh、set-cookie。把content属性关联到http头部。
  • content: 名称/值对中的值, 可以是任何有效的字符串。 始终要和 name 属性或 http-equiv 属性一起使用。
  • scheme: 用于指定要用来翻译属性值的方案。

2. css哪些属性可以继承

  • 字体相关:line-height, font-family, font-size, font-style, font-variant, font-weight, font
  • 文本相关: letter-spacing, text-align, text-indent, text-transform, word-spacing
  • 列表相关:list-style-image, list-style-position, list-style-type, list-style
  • 颜色:color

3. css3有哪些新属性

(1)边框:

  • border-radius:圆角边框,border-radius:25px;
  • box-shadow:边框阴影,box-shadow: 10px 10px 5px #888888;
  • border-image:边框图片,border-image:url(border.png) 30 30 round;

(2)背景:

  • background-size:规定背景图片的尺寸,background-size:63px 100px;
  • background-origin:规定背景图片的定位区域,背景图片可以放置于 content-box、padding-box 或 border-box 区域。background-origin:content-box;
  • CSS3 允许您为元素使用多个背景图像。background-image:url(bg_flower.gif),url(bg_flower_2.gif);

(3)文本效果:

  • text-shadow:向文本应用阴影,可以规定水平阴影、垂直阴影、模糊距离,以及阴影的颜色。text-shadow: 5px 5px 5px #FF0000;
  • word-wrap:允许文本进行换行。word-wrap:break-word;

(4)字体:CSS3 @font-face 规则可以自定义字体。

(5)2D 转换(transform

  • translate():元素从其当前位置移动,根据给定的 left(x 坐标) 和 top(y 坐标) 位置参数。 transform: translate(50px,100px);
  • rotate():元素顺时针旋转给定的角度。允许负值,元素将逆时针旋转。transform: rotate(30deg);
  • scale():元素的尺寸会增加或减少,根据给定的宽度(X 轴)和高度(Y 轴)参数。transform: scale(2,4);
  • skew():元素翻转给定的角度,根据给定的水平线(X 轴)和垂直线(Y 轴)参数。transform: skew(30deg,20deg);
  • matrix(): 把所有 2D 转换方法组合在一起,需要六个参数,包含数学函数,允许您:旋转、缩放、移动以及倾斜元素。transform:matrix(0.866,0.5,-0.5,0.866,0,0);

(6)3D 转换

  • rotateX():元素围绕其 X 轴以给定的度数进行旋转。transform: rotateX(120deg);
  • rotateY():元素围绕其 Y 轴以给定的度数进行旋转。transform: rotateY(130deg);

(7)transition:过渡效果,使页面变化更平滑

  • transition-property :执行动画对应的属性,例如 color,background 等,可以使用 all 来指定所有的属性。
  • transition-duration:过渡动画的一个持续时间。
  • transition-timing-function:在延续时间段,动画变化的速率,常见的有:ease | linear | ease-in | ease-out | ease-in-out | cubic-bezier 。
  • transition-delay:延迟多久后开始动画。

简写为:transition: [<transition-property> || <transition-duration> || <transition-timing-function> || <transition-delay>];

(8)animation:动画

使用CSS3 @keyframes 规则。

  • animation-name: 定义动画名称
  • animation-duration: 指定元素播放动画所持续的时间长
  • animation-timing-function:ease | linear | ease-in | ease-out | ease-in-out | cubic-bezier(<number>, <number>, <number>, <number>): 指元素根据时间的推进来改变属性值的变换速率,说得简单点就是动画的播放方式。
  • animation-delay: 指定元素动画开始时间
  • animation-iteration-count:infinite | <number>:指定元素播放动画的循环次
  • animation-direction: normal | alternate: 指定元素动画播放的方向,其只有两个值,默认值为normal,如果设置为normal时,动画的每次循环都是向前播放;另一个值是alternate,他的作用是,动画播放在第偶数次向前播放,第奇数次向反方向播放。
  • animation-play-state:running | paused :控制元素动画的播放状态。

简写为: animation:[<animation-name> || <animation-duration> || <animation-timing-function> || <animation-delay> || <animation-iteration-count> || <animation-direction>]

这里只列出了一部分,详情可以去看w3school的CSS3 教程

4. 闭包是什么,什么时候闭包会消除?

因为作用域链,外部不能访问内部的变量和方法,这时我们就需要通过闭包,返回内部的方法和变量给外部,从而就形成了一个闭包。

JavaScript是一门具有自动垃圾回收机制的编程语言,主要有两种方式:

  • 标记清除(最常用)

垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记(可以使用任何标记方式)。然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后,垃圾收集器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。

  • 引用计数

引用计数(reference counting)的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是1。如果同一个值又被赋给另一个变量,则该值的引用次数加1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减1。当这个值的引用次数变成0 时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。这样,当垃圾收集器下次再运行时,它就会释放那些引用次数为零的值所占用的内存。

导致问题:会导致循环引用的变量和函数无法回收。

解决:将用完的函数或者变量置为null。

5. 怎么理解js是单线程的

主要说一下异步以及事件循环机制,还有事件队列中的宏任务、微任务。

  • macrotask:主代码块,setTimeout,setInterval、setImmediate等。
  • microtask:process.nextTick(相当于node.js版的setTimeout),Promise 。process.nextTick的优先级高于Promise。

更详细可以看这篇博客:这一次,彻底弄懂 JavaScript 执行机制,讲的非常清晰。

6. 有哪些排序算法,时间复杂度是多少?什么时候快排的效率最低?

排序算法最坏事件复杂度平均时间复杂度稳定度空间复杂度
冒泡排序O(n^2)O(n^2)稳定O(1)
插入排序O(n^2)O(n^2)稳定O(1)
选择排序O(n^2)O(n^2)稳定O(1)
快速排序O(n^2)O(n*log2n)不稳定O(log2n)~O(n)
二叉树排序O(n^2)O(n*log2n)不一定O(n)
堆排序O(n*log2n)O(n*log2n)不稳定O(1)

整个序列已经有序或完全倒序时,快排的效率最低。

7. 原生ajax的请求过程

创建全平台兼容的XMLHttpRequest对象:

function getXHR(){
  var xhr = null;
  if(window.XMLHttpRequest) {// 兼容 IE7+, Firefox, Chrome, Opera, Safari
    xhr = new XMLHttpRequest();
  } else if (window.ActiveXObject) {
    try {
      xhr = new ActiveXObject("Msxml2.XMLHTTP");// 即MSXML3
    } catch (e) {
      try {
        xhr = new ActiveXObject("Microsoft.XMLHTTP");// // 兼容 IE6, IE5,很老的api,虽然浏览器支持,功能可能不完善,故不建议使用
      } catch (e) {
        alert("您的浏览器暂不支持Ajax!");
      }
    }
  }
  return xhr;
}

Ajax请求数据的过程:

var xhr = getXHR();
xhr.open('GET', url/file,true);  //设置请求方式,url,以及是否异步
xhr.onreadystatechange = function() {   //设置回调监听函数
   if(xhr.readyState==4){
        if(xhr.status==200){
            var data=xhr.responseText;
             console.log(data);
   }
};
xhr.onerror = function() {
  console.log("Oh, error");
};
xhr.send();  //发送请求

8. http状态码,cookie字段,cookie一般存的是什么,session怎么存在的?

这部分可以参考我的博客:HTTP协议知识点总结

9. http请求方式有哪些?

  • HTTP1.0定义了三种请求方法: GET, POST 和 HEAD方法。
  • HTTP1.1新增了五种请求方法:OPTIONS, PUT, DELETE, TRACE 和 CONNECT 方法。
  • 更多请看:HTTP请求方法

10. 怎么用原生js实现一个轮播图,以及滚动滑动

之前我使用轮播图都是用的antd的组件,所以我大致说了一下思路,用定时器去实现,以及如何实现平滑的滚动效果。详情请看: 原生js实现轮播图

11. 用过哪些开源的组件

说了antd和element-ui。

12. 怎么实现上传下载的功能

主要说了下form表单和input标签。

13. react生命周期,以及diff算法,diff算法是对树的深度优先遍历还是广度优先遍历?

14. 强缓存和协商缓存

参考:HTTP协议知识点总结

15. react-router的原理

react-router就是控制不同的url渲染不同的组件。react-router在history库的基础上,实现了URL与UI的同步。

原理:DOM渲染完成之后,给window添加onhashchange事件监听页面hash的变化,并且在state属性中添加了route属性,代表当前页面的路由。

具体步骤

  • 当点击链接,页面hash改变时,触发绑定在 window 上的 onhashchange 事件;
  • 在 onhashchange 事件中改变组件的 state中的 route 属性,react组件的state属性改变时,自动重新渲染页面;
  • 页面随着 state 中的route属性改变,自动根据不同的hash给Child变量赋值不同的组件,进行渲染。

参考:react-router的实现原理

16. 怎么用无人机捕获天空上的鸟

这个题目我也不造啊,毕竟我没用过无人机,有知道的大神可以在评论中回答一下~

终面

终面是去的现场,在深圳总部那边,基本就是闲聊了二十来分钟吧,面完还有hr小姐姐给我们介绍和参观了无人机,酷炫~

  1. 做的项目中,哪个做的最深入最久
  2. 为什么要做前端,喜欢做前端么
  3. 未来的职业规划
  4. 了解大疆么,大疆的文化是什么
  5. 除了实习,还做过哪些项目
  6. 如果生活富足,衣食无忧,你会选择干什么

阿里巴巴

阿里是提前批,找人内推了菜鸟网络,面了六轮,面的我怀疑人生了,中途四面本来已经挂了,后面三面面试官又捞起来给我加面了一轮,不过最后还是挂在了hr。

一面

1. css选择器,怎么选择相同的类

id、class、标签、伪类、通配符等。可以用getElementsByClassName()选择相同的类。

2. css3有哪些伪类,伪类选择器有哪些

这里要区分一下伪类和伪元素的概念。根本区别在于它们是否创造了新的元素(抽象)。

  • 伪类:用于向某些选择器添加特殊的效果。例如,a标签的:link, :visited, :hover, :active; 以及 :first-child, :last-child
  • 伪元素:是html中不存在的元素,用于将特殊的效果添加到某些选择器。例如:before,:after, :first-letter, :first-line。css3只新增了一个伪元素 ::selection(改变用户所选取部分的样式)。

参考: CSS3 选择器——伪类选择器

3. OSI七层网络模型

OSI七层模型作用对应协议对应设备
应用层它是计算机用户,以及各种应用程序和网络之间的接口HTTP, FTP, SMTP, POP3计算机设备
表示层信息的语法语义以及它们的关系,如加密解密、转换翻译、压缩解压缩IPX, LPP, XDP
会话层建立、维护、管理应用程序之间的会话SSL, TLS, DAP, LDAP
传输层服务点编址,分段与重组、连接控制、流量控制、差错控制TCP, UDP防火墙
网络层为网络设备提供逻辑地址,进行路由选择、分组转发IP ARP RARP ICMP IGMP路由器
数据链路层物理寻址,同时将原始比特流转变为逻辑传输路线PPTP, ARP, RARP交换机
物理层机械、电子、定时接口通道信道上的原始比特流传输IEEE 802.2, Ethernet v.2, Internetwork网卡

参考: 一张非常强大的OSI七层模型图解

4. MVC和MVVM的区别

  • Model用于封装和应用程序的业务逻辑相关的数据以及对数据的处理方法;
  • View作为视图层,主要负责数据的展示;
  • Controller定义用户界面对用户输入的响应方式,它连接模型和视图,用于控制应用程序的流程,处理用户的行为和数据上的改变。

MVC将响应机制封装在controller对象中,当用户和你的应用产生交互时,控制器中的事件触发器就开始工作了。

MVVM把View和Model的同步逻辑自动化了。以前Controller负责的View和Model同步不再手动地进行操作,而是交给框架所提供的数据绑定功能进行负责,只需要告诉它View显示的数据对应的是Model哪一部分即可。也就是双向数据绑定,就是View的变化能实时让Model发生变化,而Model的变化也能实时更新到View。

参考: 浅析前端开发中的 MVC/MVP/MVVM 模式

5. 用过哪些设计模式

(1)单例模式

定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

实现方法:先判断实例存在与否,如果存在则直接返回,如果不存在就创建了再返回,这就确保了一个类只有一个实例对象。

适用场景:一个单一对象。比如:弹窗,无论点击多少次,弹窗只应该被创建一次。

(2)发布/订阅模式
定义:又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。

场景:订阅感兴趣的专栏和公众号。

(3)策略模式
定义:将一个个算法(解决方案)封装在一个个策略类中。

优点:

  • 策略模式可以避免代码中的多重判断条件。
  • 策略模式很好的体现了开放-封闭原则,将一个个算法(解决方案)封装在一个个策略类中。便于切换,理解,扩展。
  • 策略中的各种算法可以重复利用在系统的各个地方,避免复制粘贴。
  • 策略模式在程序中或多或少的增加了策略类。但比堆砌在业务逻辑中要清晰明了。
  • 违反最少知识原则,必须要了解各种策略类,才能更好的在业务中应用。

应用场景:根据不同的员工绩效计算不同的奖金;表单验证中的多种校验规则。

(4)代理模式

定义:为一个对象提供一个代用品或占位符,以便控制对它的访问。

应用场景:图片懒加载(先通过一张loading图占位,然后通过异步的方式加载图片,等图片加载好了再把完成的图片加载到img标签里面。)

(5)中介者模式

定义:通过一个中介者对象,其他所有相关对象都通过该中介者对象来通信,而不是互相引用,当其中的一个对象发生改变时,只要通知中介者对象就可以。可以解除对象与对象之间的紧耦合关系。

应用场景: 例如购物车需求,存在商品选择表单、颜色选择表单、购买数量表单等等,都会触发change事件,那么可以通过中介者来转发处理这些事件,实现各个事件间的解耦,仅仅维护中介者对象即可。

(6)装饰者模式

定义:在不改变对象自身的基础上,在程序运行期间给对象动态的添加方法。

应用场景: 有方法维持不变,在原有方法上再挂载其他方法来满足现有需求;函数的解耦,将函数拆分成多个可复用的函数,再将拆分出来的函数挂载到某个函数上,实现相同的效果但增强了复用性。

参考: JavaScript设计模式

6. Http状态码

7. https怎么加密

参考: HTTP协议知识点总结

8. es6相比es5有哪些优点

大概说一下:let、const,模板字符串,箭头函数,做异步处理的promise、generator、async await,es6模块等。

参考: 阮一峰 —— ECMAScript 6 入门

9. ajax请求过程

不多说,上面有。

10. 有哪些性能优化

参考:

11. 懒加载怎么实现

场景:一个页面中很多图片,但是首屏只出现几张,这时如果一次性把图片都加载出来会影响性能。这时可以使用懒加载,页面滚动到可视区在加载。优化首屏加载。

实现:img标签src属性为空,给一个data-xx属性,里面存放图片真实地址,当页面滚动直至此图片出现在可视区域时,用js取到该图片的data-xx的值赋给src。

优点:页面加载速度快,减轻服务器压力、节约流量,用户体验好。

12. 项目中写过什么组件,组件有哪些功能

主要介绍了下实习项目写过的组件,说了下如何实现的。

二面

1. react框架有哪些设计的好的地方

主要介绍了以下几个部分:

  • JSX语法
  • 组件化
  • react单项数据流
  • 虚拟DOM
  • react生命周期

2. react是怎么工作的,怎么提高性能

主要还是说了下react的生命周期,还有shouldComponentUpdate这个函数,以及diff算法。

3. redux有哪些需要改进,你觉得你用的不怎么舒服的地方?

我当时说的是redux的subscribe方法有点麻烦,每次更新数据都要手动的subscribe一下,所以觉得react-redux的api封装的更好,用起来比较简单。

参考:

4. 怎么设计一个类似于antd 的 react 组件库

这个问题把我给问懵了额,我是按照软件工程的生命周期流程来答的。

5. 你做的最自豪的一个项目

这个略过...言之有理即可

6. mysql 的左关联和右关联

左关联:保留左表中所有的元组,右表中没有的属性填充NULL。

右关联:保留右表中所有的元组,左表中没有的属性填充NULL。

7. 有没有折腾过后端

直接说了没有,之前学了点PHP,不过都快忘得差不多了额。

8. 学习方法和未来的学习路线

言之有理即可。

9. 浏览器页面渲染机制

  • 解析html建立dom树
  • 解析css构建render树(将CSS代码解析成树形的数据结构,然后结合DOM合并成render树)
  • 布局render树(Layout/reflow),负责各元素尺寸、位置的计算
  • 绘制render树(paint),绘制页面像素信息
  • 浏览器会将各层的信息发送给GPU,GPU会将各层合成(composite),显示在屏幕上。

参考: 从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理

10. XSS和CSRF防范措施

(1)XSS:跨站脚本攻击

攻击方式:在URL或者页面输入框中插入JavaScript代码。

防范:

  • 设置httpOnly,禁止用document.cookie操作;
  • 输入检查:在用户输入的时候进行格式检查;
  • 对输出转义。

(2)CSRF:跨站点伪造请求

攻击方式:攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并执行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。主要是拿到了用户的登录态。

防范:

  • 检查 Referer 字段:这个字段用以标明请求来源于哪个地址。在处理敏感数据请求时,通常来说,Referer 字段应和请求的地址位于同一域名下。
  • 添加校验 Token:这种数据通常是表单中的一个数据项。服务器生成token并附加在表单中,其内容是一个伪乱数。当客户端通过表单提交请求时,这个伪乱数也一并提交上去以供校验。正常的访问时,客户端浏览器能够正确得到并传回这个伪乱数,而通过 CSRF 传来的欺骗性攻击中,攻击者无从事先得知这个伪乱数的值,服务器端就会因为校验 Token 的值为空或者错误,拒绝这个可疑请求。
  • 通过输入验证码来校验合法请求

三面

这一面基本问的是个人知识沉淀了,如实回答就可以了。

  1. 在项目中的难点,怎么解决的
  2. 你的优势是什么
  3. redux 源码学到了什么,怎么看源码的
  4. 了解哪些前端的前沿技术
  5. 平时看什么书,兴趣爱好是什么
  6. 异步有哪些方法
  7. 博客写了什么
  8. 除了实习经历,还做过哪些项目

四面

这一面是在杭州菜鸟现场面的,尴尬的是通知我的小姐姐一直强调是hr面,我天真的以为是hr面了,然鹅问了很多技术,当时候想的是阿里的hr都这么厉害了,都能直接问技术了。临走之前,特意问了面试官是hr面么,他说是技术,然后我......大概就知道自己凉了。

1. mysql的索引用的什么,介绍一下b树,b+树,红黑树这些

mysql的索引用的是B+树。

参考: 数据结构中常见的树(BST二叉搜索树、AVL平衡二叉树、RBT红黑树、B-树、B+树、B*树)

2. Mysql的基本写法

参考: 一千行 MySQL 学习笔记

3. 估算下杭州上空现在有多少架飞机

这个题目,也真的是为难我了额。在网上搜到了个答案,可以参考下:高盛的面试题

4. 两组数据,都存储五亿条url,内存有4G,如何找出相同的两条url

参考: 面试- 阿里-. 大数据题目- 给定a、b两个文件,各存放50亿个url,每个url各占64字节,内存限制是4G,让你找出a、b文件共同的url?

5. 如何找到一个字符串中最长的两个字符串

解法:后缀数组。首先生成字符串的所有后缀数组,在进行排序,找出相邻两个最长的公共子串(从第一位开始相同的)

例如:abcdeabc

生成后缀数组:【abcdeabc,bcdeabc,cdeabc,deabc,eabc,abc,bc,c】

再排序:【abcdeabc,abc,bcdeabc,bc,cdeabc,c,deabc,eabc】

找出相邻的最长公共子串:【abc,bc,c】

因此,最长的串是abc。

6. 在白板上画出这个项目的整个架构

画了下项目的功能架构什么的。

7. XSS, CSRF,token 怎么来的,sql 注入知道么

sql注入:

攻击方式:服务器上的数据库运行非法的 SQL 语句,主要通过拼接字符串的形式来完成,改变sql语句本身的语义。通过sql语句实现无账号登陆,甚至篡改数据库。

防御

  • 使用参数化查询:使用预编译语句,预先编译的 SQL 语句,并且传入适当参数多次执行。由于没有拼接的过程,因此可以防止 SQL 注入的发生。 使用preparedStatement的参数化sql,通过先确定语义,再传入参数,就不会因为传入的参数改变sql的语义。(通过setInt,setString,setBoolean传入参数)
  • 单引号转换:将传入的参数中的单引号转换为连续两个单引号,PHP 中的 Magic quote 可以完成这个功能。
  • 检查变量数据类型和格式。
  • 使用正则表达式过滤传入的参数,对特殊符号过滤或者转义处理。

8. 怎么设计一个ant的组件

9. 你觉得你实习做的项目有什么改进的地方

10. 你做过印象最深刻的项目

11. 算法了解过吗

就知道一些基本的排序额...

12. Setstate 会发生什么

setState会引发一次组件的更新过程,从而引发页面的重新绘制。主要会涉及以下几个生命周期函数:

  • shouldComponentUpdate(被调用时this.state没有更新;如果返回了false,生命周期被中断,虽然不调用之后的函数了,但是state仍然会被更新)
  • componentWillUpdate(被调用时this.state没有更新)
  • render(被调用时this.state得到更新)
  • componentDidUpdate

13. 平时处理过什么兼容性

参考: web前端兼容性问题总结

14. 了解分布式和负载均衡么

然鹅我并不了解呃。

参考: 服务器负载均衡的基本功能和实现原理

五面

第四面确实是挂了,没面hr就让我走了,后面过了两天之后,三面面试官又把我捞起来了,说我计算机基础还有数据库基础不怎么好,然后说问我几个简单的,之后给了我机会面了hr,感谢三面面试官让我体验了阿里的整个面试流程,很满足了。

1. 进程和线程的区别

  • 根本区别:进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位。
  • 在开销方面:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
  • 所处环境:在操作系统中能同时运行多个进程(程序);而在同一个进程(程序)中有多个线程同时执行(通过CPU调度,在每个时间片中只有一个线程执行)。
  • 内存分配方面:系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了CPU外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源。
  • 包含关系:没有线程的进程可以看做是单线程的,如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。

2. 冒泡排序和快速排序的区别

简述了下冒泡和快排的思想,以及冒泡和快排的时间复杂度。

3. OSI七层模型以及作用

上面有写噢,不知道的往上翻。

4. 你有哪些优势,或者打动他的

呃,最怕这种自夸的问题额,然后就是夸了一顿,手动捂脸。

5. 面向对象和非面向对象有什么区别

面向对象三大特性:封装,继承,多态。

面向对象的好处:

  • 将对象进行分类,分别封装它们的数据和可以调用的方法,方便了函数、变量、数据的管理,方便方法的调用(减少重复参数等),尤其是在编写大型程序时更有帮助。
  • 用面向对象的编程可以把变量当成对象进行操作,让编程思路更加清晰简洁,而且减少了很多冗余变量的出现

参考: 面向对象(一)|面向对象概念及优点

6. 设计模式有哪些,说下装饰者模式和代理模式

前面有总结,往前翻。

7. 重载和重写有什么区别

方法重写(overriding)

  • 也叫子类的方法覆盖父类的方法,要求返回值、方法名和参数都相同。
  • 子类抛出的异常不能超过父类相应方法抛出的异常。(子类异常不能超出父类异常)
  • 子类方法的的访问级别不能低于父类相应方法的访问级别(子类访问级别不能低于父类访问级别)。

方法重载(overloading):

重载是在同一个类中的两个或两个以上的方法,拥有相同的方法名,但是参数却不相同,方法体也不相同,最常见的重载的例子就是类的构造函数。

参考: 方法重载和重写的区别

hr面

  1. 为什么选择前端开发
  2. 什么事情让你最有成就感
  3. 什么让你最有挫败感
  4. 为什么选择阿里
  5. 平时是怎么学习的
  6. 职业发展

百度

二面三面都有手写代码的环节,对于我这种动手能力弱的人来说还是挺吃力。当时提前批投递的是深圳百度,总共只招五个前端,没过也很正常。后面正式批笔试过了,但是要去北京面试,也就直接放弃了。

1. 为什么要用flex布局,align-items和justify-content的区别

传统布局基于盒模型,非常依赖 display属性 、position属性 、float属性。而flex布局更灵活,可以简便、完整、响应式地实现各种页面布局,比如水平垂直居中。

align-items:定义在垂直方向上的对齐方式;

justify-content:定义在水平方向上的对齐方式。

2. webpack是怎么打包的,babel又是什么?

把项目当做一个整体,通过一个给定的主文件(如:index.js),Webpack将从这个文件开始找到项目的所有依赖文件,使用loaders处理它们,最后打包为一个(或多个)浏览器可识别的JavaScript文件。

babel将es6、es7、es8等语法转换成浏览器可识别的es5或es3语法。

3. saas和less不同于普通css的地方

  • 定义变量,可以把反复使用的css属性值定义成变量,然后通过变量名来引用它们,而无需重复书写这一属性值;
  • 嵌套写法,父子关系一目了然;
  • 使用运算符,可以进行样式的计算;
  • 内置一些颜色处理函数用来对颜色值进行处理,例如加亮、变暗、颜色梯度等;
  • 继承:为多个元素定义相同样式的时候,我们可以考虑使用继承的做法;
  • Mixins (混入):有点像是函数或者是宏,当某段 CSS经常需要在多个元素中使用时,可以为这些共用的 CSS 定义一个Mixin,然后只需要在需要引用这些 CSS 地方调用该 Mixin 即可。

4. es 6模块和其他模块不同的地方

对比了一下es6模块和CommonJS模块:

区别CommonJSes6
加载原理第一次加载模块就会执行整个模块,再次用到时,不会执行该模块,而是到缓存中取值。不会缓存运行结果,动态的去被加载的模块中取值,并且变量总是绑定其所在模块。
输出值的拷贝(模块中值的改变不会影响已经加载的值)值的引用(静态分析,动态引用,原来模块值改变会改变加载的值)
加载方式运行时加载(加载整个模块,即模块中的所有接口)编译时加载(只加载需要的接口)
this指向指向当前模块指向undefined
循环加载只输出已经执行的部分,还未执行的部分不会输出遇到模块加载命令import时不会去执行模块,而是生成一个动态的只读引用,等到真正用到时再去模块中取值。只要引用存在,代码就能执行。

5. 有没有用过es6的一些异步处理函数

Promise,generator,async await

6. redux怎么处理异步操作

可以引入Redux-thunk或者redux-promise这种中间件,可以延迟事件的派发。

其中的原理:是因为他们用了applymiddleware()包装了store的dispatch方法,拥有可以处理异步的能力。

7. 为什么reducer要是个纯函数,纯函数是什么?

纯函数:对于相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用,也不依赖外部环境的状态。

原因:Redux只通过比较新旧两个对象的存储位置来比较新旧两个对象是否相同(浅比较)。如果你在reducer内部直接修改旧的state对象的属性值,那么新的state和旧的state将都指向同一个对象。因此Redux认为没有任何改变,返回的state将为旧的state。两个state相同的话,页面就不会重新渲染了。

因为比较两个Javascript对象所有的属性是否相同的的唯一方法是对它们进行深比较。但是深比较在真实的应用当中代价昂贵,因为通常js的对象都很大,同时需要比较的次数很多。因此一个有效的解决方法是作出一个规定:无论何时发生变化时,开发者都要创建一个新的对象,然后将新对象传递出去。同时,当没有任何变化发生时,开发者发送回旧的对象。也就是说,新的对象代表新的state。

8. 高阶函数是什么,怎么去写一个高阶函数

高阶函数:参数值为函数或者返回值为函数。例如map,reduce,filter,sort方法就是高阶函数。

编写高阶函数,就是让函数的参数能够接收别的函数。

9. vue跟react的区别是什么

没有用过vue,所以就只说了vue具有双向绑定,react是单向数据流。

参考: Vue.js与React的全面对比

10. nodejs处理了什么问题

可以处理高并发的I/O,也能与websocket配合,开发长连接的实时交互应用程序。

11. 响应式布局,怎么做移动端适配

使用媒体查询可以实现响应式布局。

移动端适配方案:

(1)meta viewport:让当前viewport的宽度等于设备的宽度,同时不允许用户手动缩放。

<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">

width=device-width: 让当前viewport宽度等于设备的宽度

user-scalable=no: 禁止用户缩放

initial-scale=1.0: 设置页面的初始缩放值为不缩放

maximum-scale=1.0: 允许用户的最大缩放值为1.0

minimum-scale=1.0: 允许用户的最小缩放值为1.0

(2)媒体查询(响应式)

(3)动态 rem 方案

参考: 移动端是怎么做适配的?

二面

1. 怎么做一个实时的聊天系统

使用WebSocket和nodejs,《nodejs实战》这本书详细介绍了这个项目。

2. 当消息有延迟的时候,怎么保证消息的正确顺序

每个消息在被创建时,都将被赋予一个全局唯一的、单调递增的、连续的序列号(SerialNumber,SN)。可以通过一个全局计数器来实现这一点。通过比较两个消息的SN,确定其先后顺序。

3. 怎么做一个登陆窗口,input有哪些兼容性

使用form表单。

4. input按钮如何校验

使用正则表达式。

5. 如何实现水平垂直居中,relative、absolute、fixed

我说了三种方式:

(1)使用表格

.container{
  width: 600px;
  height: 600px;
  background: #eee;
  display: table-cell;
  text-align: center;
  vertical-align: middle;
}
.center{
  background: blue;
}

(2)css3的transform属性

.container{
  width: 100%;
  height: 400px;
  background: #eee;
  position: relative;
}
.center{
  background: blue;
  position:absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

(3)flex布局

.container{
  width: 100%;
  height: 400px;
  background: #eee;
  display: flex;
  justify-content: center;
  align-items: center;
}
.center{
  width: 100px;
  height: 100px;
  background: blue;
  text-align: center;
}
  • relative:相对于自己的定位;
  • absolute:相对于最近一级定位元素的定位;
  • fixed:相对于窗口的定位。

6. 写一个函数,1s之后依次输出1,2,3,4,5

直接使用了let和定时器。

for(let i = 1 ; i < 6; i++){
    setTimeout(() => {
        conosle.log(i)
    }, 1000)
}

7. 事件队列(宏任务、微任务)

参考::这一次,彻底弄懂 JavaScript 执行机制

8. 如何每隔三个数加一个逗号,还要考虑小数点的情况

这道题就是大疆的笔试题,当时候笔试题也是瞎写的,后面也没仔细看,没想到又成了一道面试题。

function transform(number){
    var num = number.toString() 
    var numArr = num.split('.')
    var [num, dotNum] = numArr


    var operateNum = num.split('').reverse()
    var result = [], len = operateNum.length
    for(var i = 0; i< len; i++){
         result.push(operateNum[i])
         if(((i+1) % 3 === 0) && (i !== len-1)){
              result.push(',')
        }
    }

    if(dotNum){
         result.reverse().push('.', ...dotNum)
         return result.join('')
    }else{
         return result.reverse().join('')
    }

}

9. webpack有配置过吗?原理知道吗

参考前面。

10. 父子组件如何通信,子组件怎么跟父组件通信?

父组件把state作为props传递给子组件进行通信。

父组件写好state和处理该state的函数,同时将函数名通过props属性值的形式传入子组件,子组件调用父组件的函数,同时引起state变化。

11. 简单说一下pwa

面试的这个部门就是做pwa的,所以说了下自己对pwa的理解。

三面

1. 手写indexOf

function indexOf(str, val){
    var strLen = str.length, valLen = val.length
    for(var i = 0; i < strLen; i++){
        var matchLen = i + valLen
        var matchStr = str.slice(i, matchLen)
        if(matchLen > strLen){
            return -1
        }
        if(matchStr === val){
            return i
        }
    }
    return -1
}

2. 实现 JS 的继承

function A () {
   this.name = 'a';    
}

A.prototype.getName = function () {
    return this.name;
}

function B () {
}

// B 如何继承 A

参考: JS实现继承的几种方式

3. 从url输入到页面显示会有哪些步骤

(1)DNS服务器解析域名,找到对应服务器的IP地址;

(2)和服务器建立TCP三次握手连接;

(3)发送HTTP请求,服务器会根据HTTP请求到数据服务器取出相应的资源,并返回给浏览器;

(4)浏览器处理响应

  • 加载:浏览器对一个html页面的加载顺序是从上而下的。

当加载到外部css文件、图片等资源,浏览器会再发起一次http请求,来获取外部资源。
当加载到js文件,html文档会挂起渲染(加载解析渲染同步)的线程,等待js文件加载、解析完毕才可以恢复html文档的渲染线程。

  • 解析:解析DOM树和CSSDOM树。
  • 渲染:构建渲染树,将DOM树进行可视化表示,将页面呈现给用户。

4. method有哪些方法,分别是什么意思?post和put的区别

post:上传资源

put:修改资源

5. https有几次握手

6. http2比http1好的地方

主要是考察http2的几个特性。

参考:HTTP协议知识点总结

7. 页面刷新不出来,是有哪些问题

可以从第三题的每个步骤进行分析,大概是:

  • 域名不存在,或者ip地址错误
  • 网络问题,不能建立正常的tcp连接
  • 服务器找不到正确的资源

8. 上一次系统性的学习是什么时候,怎么学习的

学习react的时候,看文档、博客,照着网上写了点小项目。

9. 你觉得项目中最自豪的是什么

10. 上家公司有哪些不好的地方

网易

网易是在杭州网易大厦面的,一天面完三轮,然后录用排序,择优录取的吧。我投的是网易考拉,哭唧唧,后面被拒了之后还伤心的卸载了考拉。之后正式批投了杭研,过了笔试,要去武汉面,本来海康也是在武汉面的,考虑到还要住一晚上,有点怕怕,就没去了。

1.css3动画

2. flex布局

3. 实现call

Function.prototype.call2 = function (context) {
    var context = Object(context) || window
    context.fn = this
    var args = []
    for (var i = 1; i < arguments.length; i++) {
        args.push('arguments[' + i +']')
    }

    var res = eval('context.fn(' + args + ')')

    delete context.fn
    return res
}

4. 图片懒加载data-src

5. Promise异步

6. 水平垂直居中

7. 数组有哪些方法,哪些会改变原数组

改变原数组的方法:pop、push、reverse、shift、sort、splice、unshift,以及两个ES6新增的方法copyWithin 和 fill;

不改变原数组(复制):concat、join、slice、toString、toLocaleString、indexOf、lastIndexOf、未标准的toSource以及ES7新增的方法includes;

循环遍历:forEach、every、some、filter、map、reduce、reduceRight 以及ES6新增的方法entries、find、findIndex、keys、values。

8. 操作dom有哪些方法

创建:

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

添加:appendChild()

移出:removeChild()

替换:replaceChild()

插入:insertBefore()

复制:cloneNode(true)

查找:

  getElementsByTagName()    //通过标签名称<br>
  getElementsByClassName()    //通过标签名称<br>
  getElementsByName()    //通过元素的Name属性的值<br>
  getElementById()    //通过元素Id,唯一性

9. 左边定宽右边自适应

(1)左盒子左浮动,右盒子width=100%

(2)左盒子左浮动,右盒子margin-left=左盒子宽度

(3)左盒子左浮动,右盒子右浮动,设置calc(100vw-盒子宽度)

(4)父容器设置display=flex,右盒子flex:1

10. 事件代理

利用事件冒泡的原理,让自己的所触发的事件,让他的父元素代替执行。打个比方:一个button对象,本来自己需要监控自身的点击事件,但是自己不来监控这个点击事件,让自己的父节点来监控自己的点击事件。

11. 后端了解么

直接说了不了解,笑哭。

二面

1. 节流和防抖,手写一下代码

(1)防抖:

定义: 合并事件且不会去触发事件,当一定时间内没有触发这个事件时,才真正去触发事件。

原理:对处理函数进行延时操作,若设定的延时到来之前,再次触发事件,则清除上一次的延时操作定时器,重新定时。

场景: keydown事件上验证用户名,输入法的联想。

实现:

function debounce(fn, delay) {
    var timer

    return function () {
        var that = this
        var args = arguments

        clearTimeout(timer)
            timer = setTimeout(function () {
            fn.apply(that, args)
        }, delay)
    }
}

(2)节流:

定义: 持续触发事件时,合并一定时间内的事件,在间隔一定时间之后再真正触发事件。每间隔一段时间触发一次。

原理:对处理函数进行延时操作,若设定的延时到来之前,再次触发事件,则清除上一次的延时操作定时器,重新定时。

场景: resize改变布局时,onscroll滚动加载下面的图片时。

实现:

  • 方法一:使用时间戳。

当触发事件的时候,我们取出当前的时间戳,然后减去之前的时间戳(最一开始值设为0),如果大于设置的时间周期,就执行函数,然后更新时间戳为当前的时间戳,如果小于,就不执行。

缺陷:第一次事件会立即执行,停止触发后没办法再激活事件。

function throttle(fn, interval) {
    var previousTime = +new Date()

    return function () {
        var that = this
        var args = arguments
        var now = +new Date()
        if (now - previousTime >= interval) {
            previousTime = now
            fn.apply(that, args)
        }
   }
}
  • 方法二:使用定时器

当触发事件的时候,我们设置一个定时器,再触发事件的时候,如果定时器存在,就不执行,直到定时器执行,然后执行函数,清空定时器,这样就可以设置下个定时器。

缺陷:第一次事件会在n秒后执行,停止触发后依然会再执行一次事件。

function throttle(fn, interval) {
    var timer
    return function (){
        var that = this
        var args = arguments

   if(!timer){
        timer = setTimeout(function () {
            fn.apply(that, args)
            timer = null
         }, interval)
        }
    }
}
  • 方法三:优化

鼠标移入能立刻执行,停止触发的时候还能再执行一次。

var throttle = function(func,delay){
    var timer = null;
    var startTime = Date.now();

    return function(){
        var curTime = Date.now();
        var remaining = delay-(curTime-startTime);
        var context = this;
        var args = arguments;

        clearTimeout(timer);
        if(remaining<=0){
            func.apply(context,args);
            startTime = Date.now();
        }else{
            timer = setTimeout(func,remaining);
        }
    }
}

2. 知道哪些性能优化

3. react为什么比其他要快,虚拟dom知道吗

4. 写过什么组件

5. 平时怎么学习的

6. node,webpack了解么

7. 模块化,commonjs,es6模块

8. redux怎么实现的

hr面

  1. 项目上有哪些难点,项目中学到了什么
  2. 不喜欢跟什么样的人共事
  3. 平时怎么学习
  4. 为什么来杭州
  5. 职业发展

搜狗

搜狗是内推的,面试也很迷,第一面到第二面中间隔了二十几天,然后二面完了也毫无反馈。

一面

1. 说一下项目,整个网络过程,从前端到后台

2. Ajax 底层实现,readystate 有哪些

0-(未初始化)还没有调用send()方法

1-(载入)已调用send()方法,正在发送请求

2-(载入完成)send()方法执行完成,已经接收到全部响应内容

3-(交互)正在解析响应内容

4-(完成)响应内容解析完成,可以在客户端调用了

3. 状态码有哪些,100,307

4. OSI七层模型

5. TCP三次握手

6. SSL握手过程

7. jQuery 有哪些方法

8. display 有哪些属性,说一下flex的属性

9. Es6的async awiat ,generator

10. Map有哪些方法

Map的方法:set, get, has, delete, clear

遍历方法:

keys():返回键名的遍历器。

values():返回键值的遍历器。

entries():返回所有成员的遍历器。

forEach():遍历 Map 的所有成员。

参考: Set 和 Map 数据结构

11. 正则用过吗?exec, 匹配一个手机号

12. css3动画了解吗,怎么写一个loading动画

13. 怎么实现跨域,cors涉及哪些请求字段

14. 编程: 判断两个网络地址是否属于同一个子网掩码

用与运算符就可以了。当时是在牛客网的面试系统上写的,一直AC不出,也是很迷了额。

15. 怎么上传文件

二面

1. 怎么计算在一个页面上的停留时间

方案1:websocket,前端开个长连接,后台统计长连接时间。

方案2:ajax轮询,隔几秒发一个查询,后台记录第一与最后一个查询间隔时间。

方案3: 关闭窗口或者跳转的时候会触发window.onbeforeunload函数,可以在该函数中做处理(有兼容性问题);统计完数据记录到本地cookies中,一段时间后统一发送。

2. 给你一亿个数,是连续的,怎么找出两个不存在的数

用bitmap就能搞定了,存在为1,不存在为0。

3. 一个搜索框的输入联想,会遇到什么问题?如果第一个请求延迟,第二个请求先到,请问怎么处理?

键盘输入太快,每次输入都去联想,需要多次发送请求,会导致用户体验太差,可以使用防抖优化。

在前端做判断,判断此时的值是否与返回的值相同,不同就丢弃,相同就显示在页面。

4. Http的缓存

5. 二维码怎么工作的,扫描pc端的二维码,怎么让pc端登录?

  • pc端随机生成一个含有唯一uid的二维码,并与服务器建立一个长连接;
  • 手机扫描二维码,解析出二维码中的uid,并把这个uid和手机端的用户密码进行绑定,上传给服务器;
  • 服务器获得客户端信息之后,pc端的长连接轮询操作会获得该消息,显示该账号的信息;
  • pc端会再开一个长连接与手机端保持通信,等待手机端确认登陆后,获得服务器授权的token,就可以在pc端登陆进行正常通信了。

6. Promise 做什么的,有哪几种状态

异步处理的,有三个状态:resolve,pending,reject。

7. 项目有哪些难点,怎么处理的

8. 遇到过哪些性能优化

电信IT研发中心

当时听说电信对学历要求很高,本科基本都是211起的,想着自己本科太渣,就直接放弃了网上的笔试。之后电信来了学校宣讲会,跟朋友吃完饭看到了,就去说凑凑热闹,刚好有笔试也就做了。做完之后笔试居然考了最高分,比第二名高出二十分,手动捂脸额。一面完分数也挺高的,有95分,运气爆棚。重点是今年电信开的薪资实在太高了,目前还在纠结选哪个。

1. Xhtml和html的区别

  • XHTML 元素必须被正确地嵌套。
  • XHTML 元素必须被关闭。
  • 标签名必须用小写字母。
  • XHTML 文档必须拥有根元素。

2. 遇到过哪些兼容性问题

3. 浏览器内核有哪些,移动端用的是哪个

  • Trident内核:IE,MaxThon,TT,The Word,360,搜狗浏览器等。[又称为MSHTML]
  • Gecko内核:Netscape6及以上版本,FF,MozillaSuite/SeaMonkey等;
  • Presto内核:Opera7及以上。[Opera内核原为:Presto,现为:Blink]
  • Webkit内核:Safari,Chrome等。[Chrome的:Blink(Webkit的分支)]

对于Android手机而言,使用率最高的就是Webkit内核。

4. 怎么实现标签页的通信

5. Cookie、session,localstorage,sessionstorage

6. React 和jquery 之间的区别,哪个好用

7. 怎么实现继承

8. Es6,es7有哪些特性

9. 怎么跨域

10. Commonjs用的js哪个特性?

因为js之前只能在浏览器运行,为了能让js能在服务器上运行,所以设计了commonjs规范,而且js之前没有模块化的概念。

11. 选择器优先级

12. 伪类知道吗,有哪些

13. 块级元素有哪些,怎么转成行内元素

14. 一个完整的http请求,页面渲染过程,js和css文件怎么渲染

二面

一面问的都很常规的,不知道为啥给了这么高的分。二面的时候三个面试官,总共就问了三个问题,然后就说面试结束了,不超过五分钟。

1. TCP怎么工作的

三次握手

2. OSI七层模型,路由器工作在哪一层?

网络层

3. 平时用什么语言,用过哪些框架

深信服

深信服给的薪资居然比电信还低,而且加班还严重,就直接拒了。

一面

1. 跨域,同源策略,webpack里面有个跨域的方式知道么

2. 怎么把es6转成es5,babel怎么工作的

  • 解析:将代码字符串解析成抽象语法树
  • 变换:对抽象语法树进行变换操作
  • 再建:根据变换后的抽象语法树再生成代码字符串

3. 反向代理知道么,Nginx

4. 继承有哪些方式

5. 怎么实现一个sleep ,手写一个promise

6. 能写一个二叉树么,怎么去遍历

7. 深拷贝怎么写

(1)var new_arr = JSON.parse( JSON.stringify(arr) );

(2)for in 加递归

function isObj(obj) {
//判断是否为对象或者函数,但不是null
    return (typeof obj === 'object' || typeof obj === 'function') && obj !== null
}

function deepCopy(obj) {
    let newObj = Array.isArray(obj) ? [] : {}
    for(let key in obj) {
        newObj[key] = isObj(obj[key]) ? deepCopy(obj[key]) : obj[key]
    }
    return newObj
}

(3)$.extend()

(4)函数库lodash,提供_.cloneDeep()

8. 在公司除了完成上级交待的任务,还做了什么

9. 怎么实现垂直中间布局

10. Call和apply,哪个性能开销大

在思否上提问了,已有大神回答。

参考: call和apply的哪个性能更好

11. 正则写一个手机号,全局匹配是什么

12. 删除一个数组中的某个数

splice方法

13. 模块化介绍一下,什么是编译时优化

14. 有哪些网络安全名词,怎么防范

15. 平时怎么学习

二面

二面小哥哥问了几个问题之后,就一直跟我介绍深信服内部的一些管理、技术氛围、晋升机制什么的,全程都是笑脸额。

1. git push -u 是什么意思

绑定默认提交的远程版本库,加了参数-u后,以后即可直接用git push 代替git push origin master

2. git rebase解释下

有test和dev两个分支,分别有两个commit,此时执行下列命令:

git checkout test
git rebase dev

以dev为基准将test的提交进行回放,挨个的应用到dev上去,然后test的那些提交就会废弃。 等价于git merge dev。

git merge 和git rebase区别:

  • merge不会修改提交历史,rebase会修改提交历史

  • rebase只应用于本地没有提交的代码,如果应用到已经提交到远程的分支不要应用,不然会非常的麻烦,git merge 可以应用于远程分支。

3. linux命令,怎么打开一个文件

cat abc.txt

4. 你的上级给你review 代码时会提什么建议

5. 怎么看待加班和工作效率

6. get和post分别进行几次数据交互

get请求过程:(2次交互)

  • 浏览器请求tcp连接(第一次握手)   
  • 服务器答应进行tcp连接(第二次握手)   
  • 浏览器确认,并发送get请求头和数据(第三次握手,这个报文比较小,所以http会在此时进行第一次数据发送)   
  • 服务器返回200 ok响应。

post请求过程:(3次交互)

  • 浏览器请求tcp连接(第一次握手)   
  • 服务器答应进行tcp连接(第二次握手)   
  • 浏览器确认,并发送post请求头(第三次握手,这个报文比较小,所以http会在此时进行第一次数据发送)   
  • 服务器返回100 continue响应   
  • 浏览器开始发送数据   
  • 服务器返回200 ok响应

7. 怎么打断点,如何确定一个结果来自于哪个函数

ThoughtWorks

TW是内推的,做了内推作业后,就面了技术和文化面。技术面是在作业的基础上加两个功能,只写出来一个,后面一个没时间写了,然后就只讲了下思路。

文化面面了快一个小时,听说TW不加班,对女程序员还很友好,挺中意的公司,不过最后还是挂了额。

华为

华为的面试就不多说了,基本不问前端的,进去是随机分岗的。华为的面试阵仗是我见过的最大的,听说要招一万人,在万达那里面的,全是人啊,阔怕。现在正泡在offer池里,估计国庆后发正式offer吧。

二面碰到的是个女面试官,太恐怖了,一直在怼我,最怕碰到女面试官了,惨。

小米

小米是内推的,电话面了一面,国庆后要我去武汉现场面,那会学校刚好有事应该也不会去了。

1. redux主要做什么的,用过redux的一些中间件吗,简单说一下

2. react生命周期说一下,diff算法说一下

3. setstate时会合并修改,是在哪个函数里修改的?宏事件和微事件

setstate是异步更新的,通过一个队列机制实现state的更新,当执行setState时,会将需要更新的state合并后放入状态队列,而不会立即更新,队列可以高效的批量更新state。

4. let、const、var的区别;如果const定义的是个对象,能够修改对象的属性吗?

const实际上保证的并不是变量的值不得改动,而是变量指向的那个指针不得改动,可以给对象添加属性。如果真的想将对象冻结,应该使用Object.freeze方法。

5. Object.freeze和Object.seal的区别

Object.preventExtension:禁止对象添加新属性并保留已有属性;

Object.seal:在一个现有对象上调用 Object.preventExtensions(..) 并把所有现有属性标记为 configurable:false;

Object.freeze:在一个现有对象上调用 Object.seal(..) 并把所有“数据访问”属性标记为 writable:false。

6. 说一下防抖,应用场景是什么

7. 快速排序算法说下,基点怎么选?如果一个数组是已经排序好的怎么选基点?

  • 数组元素随机,取固定基准;
  • 数组元素已排序或逆序,取随机基准;
  • 更好的方法:三数取中,选取数组开头,中间和结尾的元素,通过比较,选择中间的值作为快排的基准。

8. 算法的稳定性,冒泡、快排

9. lodash,underscore的库了解么?有哪些方法

10. 整个项目的架构,包括前端、后台、运营

11. sort的底层实现机制,看过源码么?

数组长度<=22时采用插入排序,大于22用快排。

12. 怎么调试bug?打过断点么?如果前端代码被压缩,如何去找到相应元素?

chromre控制台下,在 Scripts 面板下面有个 Pretty print 按钮(这种符号 {}),点击会将压缩 js 文件格式化缩进规整的文件,这时候在设定断点可读性就大大提高了。

写在最后

感谢这一年的学习。另外,公司已签,感谢各位的关注。

其他资料

这些都是我的学习笔记,也可以参考:

查看原文

zp1996 发布了文章 · 2018-09-08

nodejs源码—初始化

概述

相信很多的人,每天在终端不止一遍的执行着node这条命令,对于很多人来说,它就像一个黑盒,并不知道背后到底发生了什么,本文将会为大家揭开这个神秘的面纱,由于本人水平有限,所以只是讲一个大概其,主要关注的过程就是node模块的初始化,event loopv8的部分基本没有深入,这些部分可以关注一下我以后的文章。(提示本文非常的长,希望大家不要看烦~)

node是什么?

这个问题很多人都会回答就是v8 + libuv,但是除了这个两个库以外node还依赖许多优秀的开源库,可以通过process.versions来看一下:
clipboard.png

  • http_parser主要用于解析http数据包的模块,在这个库的作者也是ry,一个纯c的库,无任何依赖
  • v8这个大家就非常熟悉了,一个优秀的js引擎
  • uv这个就是ry实现的libuv,其封装了libevIOCP,实现了跨平台,node中的i/o就是它,尽管js是单线程的,但是libuv并不是,其有一个线程池来处理这些i/o操作。
  • zlib主要来处理压缩操作,诸如熟悉的gzip操作
  • aresc-ares,这个库主要用于解析dns,其也是异步的
  • modules就是node的模块系统,其遵循的规范为commonjs,不过node也支持了ES模块,不过需要加上参数并且文件名后缀需要为mjs,通过源码看,nodeES模块的名称作为了一种url来看待,具体可以参见这里
  • nghttp2如其名字一样,是一个http2的库
  • napi是在node8出现,node10稳定下来的,可以给编写node原生模块更好的体验(终于不用在依赖于nan,每次更换node版本还要重新编译一次了)
  • openssl非常著名的库,tls模块依赖于这个库,当然还包括https
  • icu就是small-icu,主要用于解决跨平台的编码问题,versions对象中的unicodecldrtz也源自icu,这个的定义可以参见这里

从这里可以看出的是process对象在node中非常的重要,个人的理解,其实node与浏览器端最主要的区别,就在于这个process对象

注:node只是用v8来进行js的解析,所以不一定非要依赖v8,也可以用其他的引擎来代替,比如利用微软的ChakraCore,对应的node仓库

node初始化

经过上面的一通分析,对node的所有依赖有了一定的了解,下面来进入正题,看一下node的初始化过程:

挖坑

node_main.cc为入口文件,可以看到的是除了调用了node::Start之外,还做了两件事情:

NODE_SHARED_MODE忽略SIGPIPE信号

SIGPIPE信号出现的情况一般在socket收到RST packet之后,扔向这个socket写数据时产生,简单来说就是clientserver发请求,但是这时候client已经挂掉,这时候就会产生SIGPIPE信号,产生这个信号会使server端挂掉,其实node::PlatformInit中也做了这种操作,不过只是针对non-shared lib build

改变缓冲行为

stdout的默认缓冲行为为_IOLBF(行缓冲),但是对于这种来说交互性会非常的差,所以将其改为_IONBF(不缓冲)

探索

node.cc文件中总共有三个Start函数,先从node_main.cc中掉的这个Start函数开始看:

int Start(int argc, char** argv) {
  // 退出之前终止libuv的终端行为,为正常退出的情况
  atexit([] () { uv_tty_reset_mode(); });
  // 针对平台进行初始化
  PlatformInit();
  // ...
  Init(&argc, const_cast<const char**>(argv), &exec_argc, &exec_argv);
  // ...
  v8_platform.Initialize(v8_thread_pool_size);
  // 熟悉的v8初始化函数
  V8::Initialize();
  // ..
  const int exit_code =
    Start(uv_default_loop(), argc, argv, exec_argc, exec_argv);
}

上面函数只保留了一些关键不走,先来看看PlatformInit

PlatfromInit

unix中将一切都看作文件,进程启动时会默认打开三个i/o设备文件,也就是stdin stdout stderr,默认会分配0 1 2三个描述符出去,对应的文件描述符常量为STDIN_FILENO STDOUT_FILENO STDERR_FILENO,而windows中没有文件描述符的这个概念,对应的是句柄,PlatformInit首先是检查是否将这个三个文件描述符已经分配出去,若没有,则利用open("/dev/null", O_RDWR)分配出去,对于windows做了同样的操作,分配句柄出去,而且windows只做了这一个操作;对于unix来说还会针对SIGINT(用户调用Ctrl-C时发出)和SIGTERMSIGTERMSIGKILL类似,但是不同的是该信号可以被阻塞和处理,要求程序自己退出)信号来做一些特殊处理,这个处理与正常退出时一样;另一个重要的事情就是下面这段代码:

  struct rlimit lim;
  // soft limit 不等于 hard limit, 意味着可以增加
  if (getrlimit(RLIMIT_NOFILE, &lim) == 0 && lim.rlim_cur != lim.rlim_max) {
    // Do a binary search for the limit.
    rlim_t min = lim.rlim_cur;
    rlim_t max = 1 << 20;
    // But if there's a defined upper bound, don't search, just set it.
    if (lim.rlim_max != RLIM_INFINITY) {
      min = lim.rlim_max;
      max = lim.rlim_max;
    }
    do {
      lim.rlim_cur = min + (max - min) / 2;
      // 对于mac来说 hard limit 为unlimited
      // 但是内核有限制最大的文件描述符,超过这个限制则设置失败
      if (setrlimit(RLIMIT_NOFILE, &lim)) {
        max = lim.rlim_cur;
      } else {
        min = lim.rlim_cur;
      }
    } while (min + 1 < max);
  }

这个件事情也就是提高一个进程允许打开的最大文件描述符,但是在mac上非常的奇怪,执行ulimit -H -n得到hard limitunlimited,所以我认为mac上的最大文件描述符会被设置为1 << 20,但是最后经过实验发现最大只能为24576,非常的诡异,最后经过一顿搜索,查到了原来mac的内核对能打开的文件描述符也有限制,可以用sysctl -A | grep kern.maxfiles进行查看,果然这个数字就是24576
clipboard.png

Init

Init函数调用了RegisterBuiltinModules

// node.cc
void RegisterBuiltinModules() {
#define V(modname) _register_##modname();
  NODE_BUILTIN_MODULES(V)
#undef V
}

// node_internals.h
#define NODE_BUILTIN_MODULES(V)                                               \
  NODE_BUILTIN_STANDARD_MODULES(V)                                            \
  NODE_BUILTIN_OPENSSL_MODULES(V)                                             \
  NODE_BUILTIN_ICU_MODULES(V)

从名字也可以看出上面的过程是进行c++模块的初始化,node利用了一些宏定义的方式,主要关注NODE_BUILTIN_STANDARD_MODULES这个宏:

#define NODE_BUILTIN_STANDARD_MODULES(V)                                      \
    V(async_wrap)                                                             \
    V(buffer)
    ...

结合上面的定义,可以得出编译后的代码大概为:

void RegisterBuiltinModules() {
  _register_async_wrap();
  _register_buffer();
}

而这些_register又是从哪里来的呢?以buffer来说,对应c++文件为src/node_buffer.cc,来看这个文件的最后一行,第二个参数是模块的初始化函数:

NODE_BUILTIN_MODULE_CONTEXT_AWARE(buffer, node::Buffer::Initialize)

这个宏存在于node_internals.h中:

#define NODE_MODULE_CONTEXT_AWARE_CPP(modname, regfunc, priv, flags)
  static node::node_module _module = {
    NODE_MODULE_VERSION,                                                      
    flags,                                                                    
    nullptr,                                                                  
    __FILE__,                                                                  
    nullptr,                                                                   
    (node::addon_context_register_func) (regfunc),// 暴露给js使用的模块的初始化函数
    NODE_STRINGIFY(modname),                                                 
    priv,                                                                     
    nullptr                                                                   
  };                                                                          
  void _register_ ## modname() {                                              
    node_module_register(&_module);                                           
  }


#define NODE_BUILTIN_MODULE_CONTEXT_AWARE(modname, regfunc)                   
  NODE_MODULE_CONTEXT_AWARE_CPP(modname, regfunc, nullptr, NM_F_BUILTIN)

发现调用的_register_buffer实质上调用的是node_module_register(&_module),每一个c++模块对应的为一个node_module结构体,再来看看node_module_register发生了什么:

extern "C" void node_module_register(void* m) {
  struct node_module* mp = reinterpret_cast<struct node_module*>(m);

  if (mp->nm_flags & NM_F_BUILTIN) {
    mp->nm_link = modlist_builtin;
    modlist_builtin = mp;
  }
  ...
}

由此可以见,c++模块被存储在了一个链表中,后面process.binding()本质上就是在这个链表中查找对应c++模块,node_module是链表中的一个节点,除此之外Init还初始化了一些变量,这些变量基本上都是取决于环境变量用getenv获得即可

v8初始化

到执行完Init为止,还没有涉及的jsc++的交互,在将一些环境初始化之后,就要开始用v8这个大杀器了,v8_platform是一个结构体,可以理解为是node对于v8v8::platform一个封装,紧接着的就是对v8进行初始化,自此开始具备了与js进行交互的能力,初始化v8之后,创建了一个libuv事件循环就进入了下一个Start函数

第二个Start函数

inline int Start(uv_loop_t* event_loop,
                 int argc, const char* const* argv,
                 int exec_argc, const char* const* exec_argv) {
  std::unique_ptr<ArrayBufferAllocator, decltype(&FreeArrayBufferAllocator)>
      allocator(CreateArrayBufferAllocator(), &FreeArrayBufferAllocator);
  Isolate* const isolate = NewIsolate(allocator.get());
  // ...
  {
    Locker locker(isolate);
    Isolate::Scope isolate_scope(isolate);
    HandleScope handle_scope(isolate);
  }
}

首先创建了一个v8Isolate(隔离),隔离在v8中非常常见,仿佛和进程一样,不同隔离不共享资源,有着自己得堆栈,但是正是因为这个原因在多线程的情况下,要是对每一个线程都创建一个隔离的话,那么开销会非常的大(可喜可贺的是node有了worker_threads),这时候可以借助Locker来进行同步,同时也保证了一个Isolate同一时刻只能被一个线程使用;下面两行就是v8的常规套路,下一步一般就是创建一个Context(最简化的一个流程可以参见v8hello world),HandleScope叫做句柄作用域,一般都是放在函数的开头,来管理函数创建的一些句柄(水平有限,暂时不深究,先挖个坑);第二个Start的主要流程就是这个,下面就会进入最后一个Start函数,这个函数可以说是非常的关键,会揭开所有的谜题

解开谜题

inline int Start(Isolate* isolate, IsolateData* isolate_data,
                 int argc, const char* const* argv,
                 int exec_argc, const char* const* exec_argv) {
  HandleScope handle_scope(isolate);
  // 常规套路
  Local<Context> context = NewContext(isolate);
  Context::Scope context_scope(context);
  Environment env(isolate_data, context, v8_platform.GetTracingAgentWriter());
  env.Start(argc, argv, exec_argc, exec_argv, v8_is_profiling);
  // ...

可以见到v8的常见套路,创建了一个上下文,这个上下文就是js的执行环境,Context::Scope是用来管理这个ContextEnvironment可以理解为一个node的运行环境,记录了isolate,event loop等,Start的过程主要是做了一些libuv的初始化以及process对象的定义:

  auto process_template = FunctionTemplate::New(isolate());
  process_template->SetClassName(FIXED_ONE_BYTE_STRING(isolate(), "process"));

  auto process_object =
      process_template->GetFunction()->NewInstance(context()).ToLocalChecked();
  set_process_object(process_object);

  SetupProcessObject(this, argc, argv, exec_argc, exec_argv);

SetupProcessObject生成了一个c++层面上的process对象,这个已经基本上和平时node中的process对象一致,但是还会有一些出入,比如没有binding等,完成了这个过程之后就开始了LoadEnvironment

LoadEnvironment

Local<String> loaders_name =
    FIXED_ONE_BYTE_STRING(env->isolate(), "internal/bootstrap/loaders.js");
MaybeLocal<Function> loaders_bootstrapper =
    GetBootstrapper(env, LoadersBootstrapperSource(env), loaders_name);
Local<String> node_name =
    FIXED_ONE_BYTE_STRING(env->isolate(), "internal/bootstrap/node.js");
MaybeLocal<Function> node_bootstrapper =
    GetBootstrapper(env, NodeBootstrapperSource(env), node_name);

先将lib/internal/bootstrap文件夹下的两个文件读进来,然后利用GetBootstrapper来执行js代码分别得到了一个函数,一步步来看,先看看GetBootstrapper为什么可以执行js代码,查看这个函数可以发现主要是因为ExecuteString

MaybeLocal<v8::Script> script =
    v8::Script::Compile(env->context(), source, &origin);
...
MaybeLocal<Value> result = script.ToLocalChecked()->Run(env->context());

这个主要利用了v8的能力,对js文件进行了解析和执行,打开loaders.js看看其参数,需要五个,捡两个最重要的来说,分别是processgetBinding,这里面往后继续看LoadEnvironment发现process对象就是刚刚生成的,而getBinding是函数GetBinding

node_module* mod = get_builtin_module(*module_v);
Local<Object> exports;
if (mod != nullptr) {
  exports = InitModule(env, mod, module);
} else if (!strcmp(*module_v, "constants")) {
  exports = Object::New(env->isolate());
  CHECK(exports->SetPrototype(env->context(),
                              Null(env->isolate())).FromJust());
  DefineConstants(env->isolate(), exports);
} else if (!strcmp(*module_v, "natives")) { // NativeModule _source
  exports = Object::New(env->isolate());
  DefineJavaScript(env, exports);
} else {
  return ThrowIfNoSuchModule(env, *module_v);
}

args.GetReturnValue().Set(exports);

其作用就是根据传参来初始化指定的模块,当然也有比较特殊的两个分别是constantsnatives(后面再看),get_builtin_module调用的就是FindModule,还记得之前在Init过程中将模块都注册到的链表吗?FindModule就是遍历这个链表找到相应的模块:

struct node_module* mp;
for (mp = list; mp != nullptr; mp = mp->nm_link) {
  if (strcmp(mp->nm_modname, name) == 0)
    break;
}

InitModule就是调用之前注册模块定义的初始化函数,还以buffer看的话,就是执行node::Buffer::Initialize函数,打开着函数来看和平时写addon的方式一样,也会暴露一个对象出来供js调用;LoadEnvironment下面就是将process, GetBinding等作为传入传给上面生成好的函数并且利用v8来执行,来到了大家熟悉的领域,来看看loaders.js

const moduleLoadList = [];
ObjectDefineProperty(process, 'moduleLoadList', {
  value: moduleLoadList,
  configurable: true,
  enumerable: true,
  writable: false
});

定义了一个已经加载的Module的数组,也可以在node通过process.moduleLoadList来看看加载了多少的原生模块进来

process.binding

process.binding = function binding(module) {
  module = String(module);
  let mod = bindingObj[module];
  if (typeof mod !== 'object') {
    mod = bindingObj[module] = getBinding(module);
    moduleLoadList.push(`Binding ${module}`);
  }
  return mod;
};

终于到了这个方法,翻看lib中的js文件,有着非常多的这种调用,这个函数就是对GetBinding做了一个js层面的封装,做的无非是查看一下这个模块是否已经加载完成了,是的话直接返回回去,不需要再次初始化了,所以利用prcoess.binding加载了对应的c++模块(可以执行一下process.binding('buffer'),然后再去node_buffer.cc中看看)继续向下看,会发现定义了一个class就是NativeModule,发现其有一个静态属性:

加载js

NativeModule._source = getBinding('natives');

返回到GetBinding函数,看到的是一个if分支就是这种情况:

exports = Object::New(env->isolate());
DefineJavaScript(env, exports);

来看看DefineJavaScript发生了什么样的事情,这个函数发现只能在头文件(node_javascript.h)里面找到,但是根本找不到具体的实现,这是个什么鬼???去翻一下node.gyp文件发现这个文件是用js2c.py这个文件生成的,去看一下这个python文件,可以发现许多的代码模板,每一个模板都是用Render返回的,data参数就是js文件的内容,最终会被转换为c++中的byte数组,同时定义了一个将其转换为字符串的方法,那么问题来了,这些文件都是那些呢?答案还是在node.gyp中,就是library_files数组,发现包含了lib下的所有的文件和一些dep下的js文件,DefineJavaScript这个文件做的就是将待执行的js代码注册下,所以NativeModule._source中存储的是一些待执行的js代码,来看一下NativeModule.require

NativeModule

const cached = NativeModule.getCached(id);
if (cached && (cached.loaded || cached.loading)) {
  return cached.exports;
}
moduleLoadList.push(`NativeModule ${id}`);

const nativeModule = new NativeModule(id);

nativeModule.cache();
nativeModule.compile();

return nativeModule.exports;

可以发现NativeModule也有着缓存的策略,require先把其放到_cache中再次require就不会像第一次那样执行这个模块,而是直接用缓存中执行好的,后面说的Module与其同理,看一下compile的实现:

let source = NativeModule.getSource(this.id);
source = NativeModule.wrap(source);

NativeModule.wrap = function(script) {
  return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
};
NativeModule.wrapper = [
  '(function (exports, require, module, process) {',
  '\n});'
];

首先从_source中取出相应的模块,然后对这个模块进行包裹成一个函数,执行函数用的是什么呢?

const script = new ContextifyScript(
  source, this.filename, 0, 0,
  codeCache[this.id], false, undefined
);

this.script = script;
const fn = script.runInThisContext(-1, true, false);
const requireFn = this.id.startsWith('internal/deps/') ?
  NativeModule.requireForDeps :
  NativeModule.require;
fn(this.exports, requireFn, this, process);

本质上就是调用了vm编译自妇产得到函数,然后给其传入了一些参数并执行,this.exports就是一个对象,require区分了一下是否加载node依赖的js文件,this也就是参数module,这也说明了两者的关系,exports就是module的一个属性,也解释了为什么exports.xx之后再指定module.exports = yy会将xx忽略掉,还记得LoadEnvironment吗?bootstrap/loaders.js执行完之后执行了bootstrap/node.js,可以说这个文件是node真正的入口,比如定义了global对象上的属性,比如console setTimeout等,由于篇幅有限,来挑一个最常用的场景,来看看这个是什么一回事:

else if (process.argv[1] && process.argv[1] !== '-') {
  const path = NativeModule.require('path');
  process.argv[1] = path.resolve(process.argv[1]);

  const CJSModule = NativeModule.require('internal/modules/cjs/loader');
  ...
  CJSModule.runMain();
}

这个过程就是熟悉的node index.js这个过程,可以看到的对于开发者自己的js来说,在node中对应的classModule,相信这个文件大家很多人都了解,与NativeModule相类似,不同的是,需要进行路径的解析和模块的查找等,来大致的看一下这个文件,先从上面调用的runMain来看:

if (experimentalModules) {
  // ...
} else {
  Module._load(process.argv[1], null, true);
}

Module

node中开启--experimental-modules可以加载es模块,也就是可以不用babel转义就可以使用import/export啦,这个不是重点,重点来看普通的commonnjs模块,process.argv[1]一般就是要执行的入口文件,下面看看Module._load

Module._load = function(request, parent, isMain) {
  if (parent) {
    debug('Module._load REQUEST %s parent: %s', request, parent.id);
  }
  // 查找文件具体位置
  var filename = Module._resolveFilename(request, parent, isMain);

  // 存在缓存,则不需要再次执行
  var cachedModule = Module._cache[filename];
  if (cachedModule) {
    updateChildren(parent, cachedModule, true);
    return cachedModule.exports;
  }

  // 加载node原生模块,原生模块不需要缓存,因为NativeModule中也存在缓存
  if (NativeModule.nonInternalExists(filename)) {
    debug('load native module %s', request);
    return NativeModule.require(filename);
  }

  // 加载并执行一个模块
  var module = new Module(filename, parent);

  if (isMain) {
    process.mainModule = module;
    module.id = '.';
  }

  Module._cache[filename] = module;

  // 调用load方法进行加载
  tryModuleLoad(module, filename);

  return module.exports;
};

这里看每一个Module有一个parent的属性,假如a.js中引入了b.js,那么Module bparent就是Module a,利用resolveFilename可以得到文件具体的位置,这个过程而后调用load函数来加载文件,可以看到的是区分了几种类型,分别是.js .json .node,对应的.js是读文件然后执行,.json是直接读文件后JSON.parse一下,.node是调用dlopenModule.compileNativeModule.compile相类似都是想包裹一层成为函数,然后调用了vm编译得到这个函数,最后传入参数来执行,对于Module来说,包裹的代码如下:

Module.wrapper = [
  '(function (exports, require, module, __filename, __dirname) { ',
  '\n});'
];

执行完上述过程后,前期工作就已经做得比较充分了,再次回到最后一个Start函数来看,从代码中可以看到开始了nodeevent loop,这就是node的初始化过程,关于event loop需要对libuv有一定的了解,可以说node真正离不开的是libuv,具体这方面的东西,可以继续关注我后面的文章

总结

总结一下这个过程,以首次加载没有任何缓存的情况开看:require('fs'),先是调用了Module.require,而后发现为原生模块,于是调用NativeModule.require,从NativeModule._sourcelib/fs的内容拿出来包裹一下然后执行,这个文件第一行就可以看到process.binding,这个本质上是加载原生的c++模块,这个模块在初始化的时候将其注册到了一个链表中,加载的过程就是将其拿出来然后执行

以上内容如果有错误的地方,还请大佬指出,万分感激,另外一件重要的事情就是:我所在团队也在招人,如果有兴趣可以将简历发至zhoupeng.1996@bytedance.com

查看原文

赞 17 收藏 11 评论 0

zp1996 回答了问题 · 2017-11-27

为什么一个数字左移或者右移 32位 64位 128位的结果都是数字本身?

java的实现类似,可以理解为要先进行取模

关注 2 回答 2

zp1996 发布了文章 · 2017-11-20

记一次由BOM引起的bug

bug

今天团队小伙伴给了我一个json配置文件,可以用如下替代(毕竟内容不是重点):

{
    "text": "this is a example"
}

考虑到这个json并不需要常驻,就没有用require来引用,因为node模块的缓存机制,势必会导致内存泄漏问题的发生,就采取了以下方式:

fs.readFile(`${__dirname}/y.json`, 'utf8', function(err, str) {
  if (err) {
    throw err;
  }
  try {
    const data = JSON.parse(str);
    // ...
  } catch(err) {
    throw err;
  }
});

但是诡异的事情发生了,JSON.parse竟然报错了???

Unexpected token  in JSON at position 0

此时一脸懵逼,就用了require的方式试了一下发现一点问题都没有,考虑到了团队小伙伴使用的windows,就去问了下他,得知这个jsonnotepad++写的,加上之前写php经常遇到的BOM问题,就猜测这个bug由BOM引起,将读出来的str转成Buffer来看果然开头是ef bb bf。下面先来看下今天说的这个BOM到底是个什么东西:

BOM

字节顺序标记(英语:byte-order mark,BOM)是位于码点U+FEFF的统一码字符的名称。当以UTF-16或UTF-32来将UCS/统一码字符所组成的字符串编码时,这个字符被用来标示其字节序。它常被用来当做标示文件是以UTF-8、UTF-16或UTF-32编码的记号。

说白了就是存在于文本文件的开头,标记出文件是依靠那种格式进行编码的,mac上应该不存在,但是windowsnotepad++一般会带有。大家也可以用python写一个带有BOM标记的文件,来验证这个问题:

import codecs

code = '''{
    "x": 20
}
'''

f = codecs.open('y.json', 'w', 'utf_8_sig')
f.write(code)
f.close()

了解了产生原因以及BOM到底是什么,还有一个疑惑就是为什么用require引入可以?

require json做了啥

记得require是用的fs.readFileSync同步读取的,为什么这个可以呢?猜测都是无用的,来看下node的源码,找到了这段:

Module._extensions['.json'] = function(module, filename) {
  var content = fs.readFileSync(filename, 'utf8');
  try {
    module.exports = JSON.parse(internalModule.stripBOM(content));
  } catch (err) {
    err.message = filename + ': ' + err.message;
    throw err;
  }
};

看了上面的代码可以非常明了,require在读取之后,对字符串进行了去除BOM的操作,来看下internalModule.stripBOM的实现:

function stripBOM(content) {
  // 检测第一个字符是否为BOM
  if (content.charCodeAt(0) === 0xFEFF) {
    content = content.slice(1);
  }
  return content;
}

至此问题已经解决了,但是我还有一点不明白的是ef bb bfutf8的标记,为什么会转换为feff,这个不是utf16大端序的表示吗?下面就来解决这个疑惑:

Unicode与utf8

先来讲一下编码的历史,首先出现的表示字符编码为ASCII,八位二进制,可以表示出256种状态,英文用128个符号编码就可以了,但是其他的语言却无法表示,于是在一些欧洲国家,开始各自规定其表示,比如130在法语代表一个字符,俄语代表一个字符,这样造成了0-127一致,而128-255可能会千差万别;为了解决这种问题,国际组织设计提出了Unicode,一个可以容纳全世界所有语言文字的编码方案,Unicode只规定了符号的二进制代码,但是没有规定该如何存储,比如中文可能至少需要2个字节,而英文只需要一个字节即可。utf8作为一种Unicode的实现方式被广泛颚用于互联网应用中utf8明确了编码规则:

  • 对于单字节的符号,将其第一位置为0,使用后面7位进行表示,所以说英文utf8编码与ASCII码一致

  • 对于n(n > 2)个字节的符号,第一个字节的前n为都设置为1,第n+1为设为0,后面字节的前两位一律设为10,剩下的二进制位,为这个符号的Unicode

可以参见以下对照:

字符字节Unicode符号范围utf8编码方式
10000 0000 - 0000 007F0xxxxxxx
20000 0080 - 0000 07FF110xxxxx 10xxxxxx
30000 0800 - 0000 FFFF1110xxxx 10xxxxxx 10xxxxxx
40001 0000 - 0010 FFFF11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
50020 0000 - 03FF FFFF111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
60400 0000 - 7FFF FFFF1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx

来看下feff转化为ef bb bffs.readFileSync进行了buffer -> string的转换,buffer的编码为utf8,而stringUnicode,根据上表计算下:

FEFF
1111111011111111

根据其范围,得出其utf8编码:

111011111011101110111111
EFBBBF

用代码来实现下Unicodeutf8的过程:

def UnicodeToUtf8(unic):
    res = list()
    if unic < 0x7F:
        res.append(hex(unic & 0x7F))
    elif unic >= 0x80 and unic <= 0x7FF:
        # 110xxxxx
        res.append(((unic >> 6) & 0x1F) | 0xC0)
        # 10xxxxxx
        res.append((unic & 0x3F) | 0x80)
    elif unic >= 0x800 and unic <= 0xFFFF:
        # 1110xxxx
        res.append(((unic >> 12) & 0x0F) | 0xE0)
        # all is 10xxxxxx
        res.append(((unic >>  6) & 0x3F) | 0x80)
        res.append((unic & 0x3F) | 0x80)
    elif unic >= 0x10000 and unic <= 0x1FFFFF:
        # 11110xxx
        res.append(((unic >> 18) & 0x07) | 0xF0)
        # all is 10xxxxxx
        res.append(((unic >> 12) & 0x3F) | 0x80)
        res.append(((unic >>  6) & 0x3F) | 0x80)
        res.append((unic & 0x3F) | 0x80)
    elif unic >= 0x200000 and unic <= 0x3FFFFFF:
        # 111110xx
        res.append(((unic >> 24) & 0x03) | 0xF8)
        # all is 10xxxxxx
        res.append(((unic >> 18) & 0x3F) | 0x80)
        res.append(((unic >> 12) & 0x3F) | 0x80)
        res.append(((unic >>  6) & 0x3F) | 0x80)
        res.append((unic & 0x3F) | 0x80)
    elif unic >= 0x4000000 and unic <= 0x7FFFFFFF:
        # 1111110x
        res.append(((unic >> 30) & 0x01) | 0xFC)
        # all is 10xxxxxx
        res.append(((unic >> 24) & 0x3F) | 0x80)
        res.append(((unic >> 18) & 0x3F) | 0x80)
        res.append(((unic >> 12) & 0x3F) | 0x80)
        res.append(((unic >>  6) & 0x3F) | 0x80)
        res.append((unic & 0x3F) | 0x80)
    return map(lambda r:hex(r), res)
# test
print UnicodeToUtf8(0xFEFF)

utf8Unicode只需要去除标志位即可,这里就不在实现。

到此,终于清楚的可以和团队小伙伴说出bug的解决方法就利用上面的stripBOM

致谢

如有错误,还请指出!

Unicode与utf8 部分内容参考自阮老师文章

查看原文

赞 4 收藏 3 评论 0

zp1996 赞了文章 · 2017-10-17

PostCSS 是个什么鬼东西?

前言

最近大漠前辈在群里发关于PostCSS的系列文章,但是耗子姐姐又说看了有点云里雾里的感觉,所以这篇文章将按一个思考的角度来理解一下 PostCSS 到底是一个什么东西。

图片描述

一、提出不懂的地方

很多时候第一次在网上查询 PostCSS 概念的时候,大家都解释成一个后处理器的概念,其实个人觉得这些概念不重要,更为重要的有以下几点:

  1. 它本质上是一个什么东西?

  2. 它能解决我们什么问题?

  3. 它是通过什么方式来解决我们的问题?

  4. 它解决我们的问题是为什么?

  5. 怎么实现与 SASSLESSStylus 相同的功能(因为它们被经常拿来比较)

  6. 它由哪些东西组成?

  7. 既然是程序可以用的,那么它的API呢?

Q: 这个时候,你应该会问:为什么要将组成和API放到最后呢?

A: 那是因为我们在认识一个不太清楚的东西的时候,第一次肯定是一个直观的认识:它到底有什么用?而不会说,一来就去深入的研究它。不过这里本质还是要先说一下的,先留个印象。

二、个个击破

1. 它本质上是一个什么东西?

  • PostCSS 可以直观的理解为:它就是一个平台、平台、平台,重要的事情来三遍比较爽,哈哈!

为什么说它是一个平台呢?因为我们直接用它,感觉不能干什么事情,但是如果让一些插件在它上面跑,那么将会很强大。

  • PostCSS 提供了一个解析器,它能够将 CSS 解析成抽象语法树(AST)。

上面两条看完后,我们可以理解为下面这个模型。

图片描述

所以说,PostCSS 它需要一个插件系统才能够发挥作用。我们可以通过“插件”来传递AST,然后再把AST转换成一个串,最后再输出到目标文件中去。当然,这里是有API可以用,这里先不讲,免得晕了。

2. 它能解决我们什么问题?它是通过什么方式来解决我们的问题?

上面的图很清晰,但是我还是不知道是个什么东西!所以接下来温和点,直接从代码层面来感官的认识一下。

  • 它能够为 CSS 提供额外的功能;

  • 通过在 PostCSS 这个平台上,我们能够开发一些插件,来处理我们的CSS,比如热门的:autoprefixer

  • 我们能够使用JavaScript来开发插件(这点对前端来说很重要)

好吧,看到一个熟悉的单词了:autoprefixer,这里我们就让它来当栗子吧,可能更容易理解一点。

首先,我们需要做一些准备,安装好需要的东西。

// postcss 的命令行工具
sudo npm install  -g postcss-cli
// autoprefixer 插件
sudo npm install -g autoprefixer

第一次用命令行能让你更直观去理解它哈,所以请要有一颗折腾的心。

// 1. 先看下这个命令有哪些参数可以用
postcss --help

Usage: /usr/local/bin/postcss -use plugin [--config|-c config.json] [--output|-o
output.css] [input.css]

选项:
  -c, --config       JSON file with plugin configuration
  -u, --use          postcss plugin name (can be used multiple times)
  -o, --output       Output file (stdout if not provided)
  -d, --dir          Output directory
  -r, --replace      Replace input file(s) with generated output       [boolean]
  -s, --syntax       Alternative input syntax parser
  -p, --parser       Alternative CSS parser
  -t, --stringifier  Alternative output stringifier
  -w, --watch        auto-recompile when detecting source changes
  -v, --version      显示版本号                                        [boolean]
  -h, --help         显示帮助信息                                      [boolean]

示例:
  postcss --use autoprefixer -c             Use autoprefixer as a postcss plugin
  options.json -o screen.css screen.css
  postcss --use autoprefixer                Pass plugin parameters in
  --autoprefixer.browsers "> 5%" -o         plugin.option notation
  screen.css screen.css
  postcss -u postcss-cachify -u             Use multiple plugins and multiple
  autoprefixer -d build *.css               input files

Please specify at least one plugin name.

PS: 我贴出来是方便大家在看的时候不用电脑……^_^

好吧,先看一下文件目录,这里我只说一下比较好写的方式,就是将一些参数配置到配置文件中去。

图片描述

// config.json: 所有的配置
// p.json: 仅有 autoprefixer 插件的配置

// config.json 的内容
{
    "use": ["autoprefixer"],
    "input": "src/index.css",
    "output": "index.css",
    "autoprefixer": {
        "browsers": "> 5%"
    }
}

// p.json 的内容
{
    "autoprefixer": {
        "browsers": "> 5%"
    }
}

接下来我们在终端里面输入:

// 最简洁的方式
postcss -c config.json
// 稍微复杂一点的方式,这里要用 -i 参数,help里面没有,我是从config.json里面的配置猜出来的,官方的那个写法出不来
postcss -u autoprefixer -c p.json -i src/index.css -o index.css
// 最复杂的方式
// 还是不写比较好。。。

跟平时想到的效果一样:


// src/index.css 中的源码
`* {
    transition: all .1s;
}`
// 转换过后的代码 index.css
`* {
    -webkit-transition: all .1s;
        transition: all .1s;
}`

好吧,现在肯定就对 PostCSS 有一个感官的认识了,接下来就是需要自己动手去用一下 cssnext 这个插件了~看会发生什么,这里就不写了,也挺好用的,不过应该还是草案状态。

我们开发不可能用命令行吧,所以这里再接着介绍代码编写,然后用 node 去执行文件的方式。直接上代码吧。

// 1. 先安装一下需要的库
npm install postcss --save-dev
npm install autoprefixer --save-dev

// 2. 其实应该先看看 postcss 的 package.json 文件,来看看包含了些什么,留个印象

// 3. p.js 中的代码
var postcss = require('postcss');
var autoprefixer = require('autoprefixer');
var fs = require('fs');

var css = '* { transition: all .1s; }';

postcss([autoprefixer])
    .process(css)
    .then(function(result) {
        // 这一行是学习的时候需要的,看一下到底对象里面包含什么
        console.log(result);
        if (result.css) {
            fs.writeFileSync('index.css', result.css);
        }
        if (result.map) {
            fs.writeFileSync('index.css.map', result.map);
        }
    });

// 4. 执行 p.js
node p

好吧,最后的结果和之前用命令行的方式一样,只不过过程不同。这样下来应该对 PostCSS 有了更多的感觉了吧。还没完,不用慌~我们还需要提出一个问题,我都有 SASS 等预处理器了,还拿它来不是又给前端届添乱么?因为这2年东西确实太多了~

记住一句话:存在即合理

既然合理,那么我们就看看它有什么优势呗~

3. 它解决我们的问题是为什么?优势何在?

比如,我们用 SASS 来处理 box-shadow 的前缀,我们需要这样写:

/* CSS3 box-shadow */
@mixin box-shadow($top, $left, $blur, $size, $color, $inset: false) {
    @if $inset {
        -webkit-box-shadow: inset $top $left $blur $size $color;
        box-shadow: inset $top $left $blur $size $color;
    } @else {
        -webkit-box-shadow: $top $left $blur $size $color;
        box-shadow: $top $left $blur $size $color;
    }
}

使用 PostCSS 我们只需要按标准的 CSS 来写就行了,因为最后 autoprefixer 会帮我们做添加这个事情~

box-shadow: 0 0 3px 5px rgba(222, 222, 222, .3);

所以,这里就出现了一个经常大家说的未来编码的问题。实际上,PostCSS 改变的是一种开发模式。

  • SASS等工具:源代码 -> 生产环境 CSS

  • PostCSS:源代码 -> 标准 CSS -> 生产环境 CSS

这样能体会出优势吧,但是目前大家都是 SASS + PostCSS 这样的开发模式,其实我认为是不错的,取长补短嘛,当然,在 PostCSS 平台上都是可以做到的,只是目前这个过渡期,这样更好,更工程化。接下来我就介绍一些方法来纯粹是用 PostCSS。

4. 怎么实现与 SASS、LESS、Stylus 相同的功能

其实这一节我都不需要写了~列一下插件就行了,因为插件才是实现,PostCSS 只是提供了一个平台。

其实可以去官方看看:插件系统
这里列几个便于理解的插件

  • postcss-each

  • postcss-for

  • postcss-mixins

  • postcss-extend

从名字就能看出来了吧~应该很好理解。

5. 它由哪些东西组成?

其实从官方介绍来看,只包含以下内容:

  1. CSS Parser

  2. CSS 节点树 API

  3. source map 生成器

  4. 生成节点树串

英文不太好 == ,就这 4 部分吧,从第一个图其实也能够看出来。

其中的 I/O 体现在什么地方呢?好吧,很容易想到,主要体现在:

  • Input: 插件程式CSS Parser

  • Output: 生成节点树串

CSS Parser 可以理解为一个内部过程,而插件程式主要体现在:

postcss([ autoprefixer ])

最后生成的节点树串体现在:

postcss().process().then(function (result) {
    // 就是这里了
    console.log(result.css);
});

// 现在我贴一下上面 result 对象的一个输出结果
// 这里我多引入了一个 cssnano 插件
// 改变的代码就这点,为了更全的看 result
var opts = {
    from: 'src/index.css',
    to: 'index.css',
    // 配置 map
    map: { inline: false }
};
postcss([ autoprefixer, cssnano() ]).process(css, opts)

Result {
    processor: Processor {
        // 处理器的版本号
        version: '5.0.10',
        // 加载的一堆插件
        plugins: [
            [Object], [Object], [Object], [Object], [Object],
            [Object], [Object], [Object], [Object], [Object],
            [Object], [Object], [Object], [Object], [Object],
            [Object], [Object], [Object], [Object], [Object],
            [Object], [Object], [Object], [Object], [Object],
            [Object], [Object]
        ]
    },
    messages: [],
    root: Root {
        raws: {
            semicolon: false,
            after: ''
        },
        type: 'root',
        nodes: [
            [Object]
        ],
        source: {
            input: [Object],
            start: [Object]
        },
        _autoprefixerDisabled: false,
        _autoprefixerPrefix: false,
        rawCache: {
            colon: ':',
            indent: '',
            beforeDecl: '',
            beforeRule: '',
            beforeOpen: '',
            beforeClose: '',
            beforeComment: '',
            after: '',
            emptyBody: '',
            commentLeft: '',
            commentRight: ''
        }
    },
    // 我们代码中配置 opts 变量
    opts: {
        from: 'src/index.css',
        to: 'index.css'
    },
    // 这就是重新生成的 节点树串
    // 这里有自动补全和高效压缩的效果
    css: '*{-webkit-transition:all .1s;transition:all .1s}',
    // map的文件的配置
    map: 
        SourceMapGenerator {
            _file: 'index.css',
            _sourceRoot: null,
            _skipValidation: false,
            _sources: ArraySet { _array: [Object], _set: [Object] },
            _names: ArraySet { _array: [], _set: {} },
            _mappings: MappingList { _array: [Object], _sorted: true, _last: [Object] },
            _sourcesContents: { '$src/index.css': '* { transition: all .1s; }' } },
    // 这里应该是链式要用的吧,暂时不深究
    lastPlugin: {
        [Function]
        postcssPlugin: 'cssnano-reset-stylecache',
            postcssVersion: '5.0.10'
    }
}

其实吧,这样有点抽象的,还是来看熟悉的 API 吧。

这里出现了 sourcemap,说明 PostCSS 中的转换功能是它必备的,但是必备并不等于:源代码与目标代码不能完全一致

这里吐槽一下 Chrome 的 sourcemap 功能,一坨屎!下面看看 firefox 里面的效果吧。

图片描述

这里 firefox 里面就自动映射了源文件,非常不错!

6. 既然是程序可以用的,那么它的API呢?

其实官方有 API 的详细解释,我看了一下,一看就明白了,就不再花时间介绍了,大家可以去看看,这样会知道,原来如此~

PS: 大家可以先看看 Node Common 和 Node相关的,然后再看 plugin

官方API

这里看一个 DEMO,主要做 rem 和 px 单位之间的互换,加入 processors 就可以用了,很方便:

var custom = function(css, opts){
    css.eachDecl(function(decl){
        decl.value = decl.value.replace(/\d+rem/, function(str){
            return 16 * parseFloat(str) + "px";
        });
    });
};

开发插件可以看一下 官方插件指南

更细致的地方,之后有时间的时候再写写 ^_^ 一说技术就停不下来了~

大家在问?我怎么在工程上应用它呢?好吧,使用 gulp, grunt, webpack 都是可以的,我觉得都理解了 PostCSS ,使用这些就很简单了,一查资料,拷贝一份配置就可以开始用了~就这样吧,下次再结合 react 来介绍一下一个叫: postcss-js 的插件,看上去还不错,还没深入用,用到的时候再分享吧。

其实我也是初学者,只是用了自己的学习方法来梳理成文章,下面都是我看过的文章,部分是引用的。这里就不全部举例了,看的文章有点多。。。

参考的文章

大家也可以直接阅读我的博客:http://www.60sky.com

查看原文

赞 95 收藏 286 评论 37

zp1996 赞了文章 · 2017-10-17

Node.js 中的循环依赖

我们在写node的时候有可能会遇到循环依赖的情况,什么是循环依赖,怎么避免或解决循环依赖问题?

先看一段官网给出的循环依赖的代码:

a.js:

console.log('a starting'); 
exports.done = false;
var b = require('./b.js'); // ---> 1
console.log('in a, b.done = %j', b.done);
exports.done = true;
console.log('a done') // ---> 4

b.js:

console.log('b starting'); 
exports.done = false;
var a = require('./a.js');  // ---> 2
// console.log(a);  ---> {done:false}
console.log('in b, a.done = %j', a.done); // ---> 3
exports.done = true;
console.log('b done');

main.js:

console.log('main starting'); 
var a = require('./a.js'); // --> 0
var b = require('./b.js');
console.log('in main, a.done=%j, b.done=%j', a.done, b.done);

如果我们启动 main.js 会出现什么情况? 在 a.js 中加载 b.js,然后在b.js中加载 a.js,然后再在 a.js中加载 b.js 吗?这样就会造成循环依赖死循环。

让我们执行看看:

$ node main.js

main starting
a starting
b starting
in b, a.done = false
b done
in a, b.done = true
a done
in main, a.done=true, b.done=true

可以看到程序并没有陷入死循环,从上面的执行结果可以看到 main.js 中先requirea.jsa.js 中执行完了consoleexport.done=fasle之后,转而去加载b.js,待b.js被load完之后,再返回a.js中执行完剩下的代码。

我在官网的代码基础上增加了一些注释,基本 load 顺序就是按照这个0-->1-->2-->3-->4的顺序去执行的,然后在第二步下面我打印出了require('./a')的结果,可以看到是{done:false},可以猜测在b.jsrequire('./a')的结果是a.js中已经执行到的exports出的值。

上面所说的还只是基于结果基础上的猜测,没有什么说服力,为了验证我的猜测是正确的,我把 Node 的源码稍微翻看了一些,C++ 的代码看不懂没关系,能看懂 JS 的部分就可以了,下面就是 Node 源码的分析(主要是 module 的分析, Node 源码在此):

将会分析的主要源码:

  1. node/src/node.js

  2. node/lib/module.js

启动 $ node main.js

C++ 的代码我看不懂,总而言之,在我查了资料之后知道当我们在shell中输入node main.js之后,会先执行 node/src/node.cc,然后会执行 node/src/node.js, 所以C++代码不分析,从分析 node/src/node.js 开始(只会分析和主题相关的代码)。

node.js 源码分析

node.js文件主要结构为


(function(process) {

    this.global = this
    
    function startup() {
      ...
    }
    
    startup()

})

这种闭包代码很常见,从名字可以看出,此处为启动文件。接下来看看 startup 函数中有一大块条件语句,我删除大多数无关代码,如下:

if (process.argv[1]) {
     // ...
    
    var Module = NativeModule.require('module');
    Module.runMain();
}

我把无关的代码基本都删除了。可以看到这段代码主要做的事是先通过 Native 引入module模块,执行 Module.runMain()

很多人都知道 require 核心代码,如 require('path'),不需要写全路径,Node 是怎样做到的呢?

Node 采用了 V8 附带的 js2c.py 工具,将所有内置的 JavasSript 代码( src/node.js 和 lib/*.js) 转成 c++ 里面的数组生成 node_navtives.h 头文件。
在这个过程中, JavasSript 以字符串的形式存储在 node 命名空间中, 是不可直接执行的。
在启动 Node 进程时, JavaScript 代码直接加载进内存中。

Node 在启动时,会生成一个全局变量 process, 并提供 binding() 方法来协助加载内建模块。

上面大段介绍基本引自朴老师的「深入浅出 Node.js」。大概理解就是在启动命令的时候,Node 会把 node.jslib/*.js 的内容都放到 process 中传入当前闭包中,我们在当前函数就可以通过process.binding('natives')取出来放到 _source 中,如下代码所示:

  function NativeModule(id) {
    this.filename = id + '.js';
    this.id = id;
    this.exports = {};
    this.loaded = false;
  }

  NativeModule._source = process.binding('natives');
  NativeModule._cache = {};

接下来看看NativeModule.require做了哪些事情:

  NativeModule.require = function(id) {
    if (id == 'native_module') {
      return NativeModule;
    }

    var cached = NativeModule.getCached(id);
    if (cached) {
      return cached.exports;
    }

    var nativeModule = new NativeModule(id);

    nativeModule.cache();
    nativeModule.compile();

    return nativeModule.exports;
  };

这上面的代码表明内建模块被缓存,就直接返回内建模块的exports,如果没有的话,就生成一个核心模块的实例,然后先把模块根据id来cache,然后调用nativeModule.compile接口编译源文件:

  NativeModule.getSource = function(id) {
    return NativeModule._source[id];
  };

  NativeModule.wrap = function(script) {
    return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
  };

  NativeModule.wrapper = [
    '(function (exports, require, module, __filename, __dirname) {\n',
    '\n});'
  ];

  NativeModule.prototype.compile = function() {
    var source = NativeModule.getSource(this.id);
    source = NativeModule.wrap(source);

    var fn = runInThisContext(source, {
      filename: this.filename,
      lineOffset: -1
    });
    fn(this.exports, NativeModule.require, this, this.filename);

    this.loaded = true;
  };

  NativeModule.prototype.cache = function() {
    NativeModule._cache[this.id] = this;
  };

cache 是把实例根据 id 放到 _cache 对象中。先从 _source 中取出对应id的源文件字符串,包上一层
(function (exports, require, module, __filename, __dirname) {\n','\n});。比如main.js最终变成如下JS代码的字符串:

(function (exports, require, module, __filename, __dirname) {
 // 如果是main.js
     console.log('main starting'); 
    var a = require('./a.js'); // --> 0
    var b = require('./b.js');
    console.log('in main, a.done=%j, b.done=%j', a.done, b.done);
})

runInThisContext是将被包装后的源字符串转成可执行函数,(runInThisContext来自contextify 模块),runInThisContext的作用,类似eval,再执行这个被eval后的函数,就算被 load 完成了,最后把 load 设为 true。

可以看到fn的实参为 this.exports; NativeModule.require; this; this.filename;

所以require('module')的作用是加载/lib/module.js文件。让我们再回到 startup 函数,加载完 module.js,紧接着运行 Module.runMain()方法。(估计有人忘了前面的startup函数是干嘛的,我再放一次,省得再拉回去了)

if (process.argv[1]) {
     // ...
    
    var Module = NativeModule.require('module');
    Module.runMain();
}

module.js源码分析

上面走完了NatvieModule的加载代码。再看看module.js是怎样加载用户使用的文件的。

function Module(id, parent) {
  this.id = id;
  this.exports = {};
  this.parent = parent;
  if (parent && parent.children) {
    parent.children.push(this);
  }

  this.filename = null;
  this.loaded = false;
  this.children = [];
}
module.exports = Module;

Module._cache = {};
Module._pathCache = {};
Module._extensions = {};
var modulePaths = [];
Module.globalPaths = [];

Module.wrapper = NativeModule.wrapper;
Module.wrap = NativeModule.wrap;

这是Module的构造函数,Module.wrapperModule.wrap,是由NativeModule赋值来的,Module._cache是个空对象,存放所有被 load 后的模块 id。

node.js文件的 startup 函数中,最后一步走到Module.runMain():

Module.runMain = function() {
  // Load the main module--the command line argument.
  Module._load(process.argv[1], null, true);
  // Handle any nextTicks added in the first tick of the program
  process._tickCallback();
};

runMain方法中调用了_load方法:


Module._load = function(request, parent, isMain) {
  var filename = Module._resolveFilename(request, parent);
  var cachedModule = Module._cache[filename];
  
  if (cachedModule) {
    return cachedModule.exports;
  }

  var module = new Module(filename, parent);
  Module._cache[filename] = module;

  module.load(filename);
  
  return module.exports;
};

上述代码照例我删除了一些不是很相关的代码,从剩下的代码可以看出_load函数的主要干了两件事(还有一件加载NativeModule的代码被我删掉了):

  1. 先判断当前的源文件有没有被加载过,如果 _cache 对象中存在,直接返回 _cache 中的exports对象

  2. 如果没有被加载过,新建这个源文件的 module 的实例,并存放到 _cache 中,然后调用 load 方法。


Module.prototype.load = function(filename) {
  this.filename = filename;
  this.paths = Module._nodeModulePaths(path.dirname(filename));

  var extension = path.extname(filename) || '.js';
  if (!Module._extensions[extension]) extension = '.js';
  Module._extensions[extension](this, filename);
  this.loaded = true;
};

load方法中判断源文件的扩展名是什么,默认是'.js',(我这里也只分析后缀是 .js 的情况),然后调用 Module._extensions[extension]() 方法,并传入 this 和 filename;当extension'.js'的时候, 调用Module._extensions['.js']() 方法。

// Native extension for .js
Module._extensions['.js'] = function(module, filename) {
  var content = fs.readFileSync(filename, 'utf8');
  module._compile(internalModule.stripBOM(content), filename);
};

这个方法是读到源文件的字符串后,调用module._compile方法。

Module.prototype._compile = function(content, filename) {

  var self = this;

  function require(path) {
    return self.require(path);
  }

  var dirname = path.dirname(filename);
  // create wrapper function
  var wrapper = Module.wrap(content);

  var compiledWrapper = runInThisContext(wrapper,
                                      { filename: filename, lineOffset: -1 });

  var args = [self.exports, require, self, filename, dirname];
  return compiledWrapper.apply(self.exports, args);
};

其实跟NativeModule_complie做的事情差不多。先把源文件content包装一层(function (exports, require, module, __filename, __dirname) {\n','\n});, 然后通过 runInThisContext 把字符串转成可执行的函数,最后把
self.exports, require, self, filename, dirname 这几个实参传入可执行函数中。

require 方法为:

Module.prototype.require = function(path) {
  return Module._load(path, this);
};

循环依赖的时候为什么不会无限循环引用

所谓的循环依赖就是在两个不同的文件中互相应用了对方。假设按照最上面官网给出的例子中,

main.js 中:

  1. require('./a.js');此时会调用 self.require(),
    然后会走到module._load,在_load中会判断./a.js是否被load过,当然运行到这里,./a.js还没被 load 过,所以会走完整个load流程,直到_compile

  2. 运行./a.js,运行到 exports.done = false 的时候,给 esports 增加了一个属性。此时的 exports={done: false}

  3. 运行require('./b.js'),同 第 1 步。

  4. 运行./b.js,到require('./a.js')。此时走到_load函数的时候发现./a.js已经被load过了,所以会直接从_cache中返回。所以此时./a.js还没有运行完,exports = {done.false},那么返回的结果就是 in b, a.done = false;

  5. ./b.js全部运行完毕,回到./a.js中,继续向下运行,此时的./b.jsexports={done:true}, 结果自然是in main, a.done=true, b.done=true

Flag

虽然Node.js通过 cache 解决无限循环引用的问题,但是没有解决循环引用时已加载了模块,而exports没有输出想要的值得问题,听说ES6import已经完美解决这类问题,所以立个死亡 Flag,等我研究完 import ,再写篇文章分析 import 是怎么解决这个问题的。 为什么是死亡 Flag 呢?每个等我 XXX 的时候,我就 OOO 的事情,最后一定不会做。^_^。

原文地址: GitHub

喜欢的点个推荐吧

查看原文

赞 16 收藏 33 评论 2

zp1996 发布了文章 · 2017-09-30

小而美的框架—hyperapp

写在前面

没错,又是一个新的前端框架,hyperapp非常的小,仅仅1kb,当然学习起来也是非常的简单,可以说是1分钟入门。声明式:HyperApp 的设计基于Elm Architecture(这也意味着组件更多的是纯函数),支持自定义标签以及虚拟DOM。下面先来看下怎么使用:

hello world

import { h, app } from 'hyperapp';

app({
    state: {
        count: 0
    },
    view: (state, actions) => (
        <main>
            <h2>{state.count}</h2>
            <button onclick={actions.down}>-</button>
            <button onclick={actions.up}>+</button>
        </main>
    ),
    actions: {
        down: state => ({
            count: state.count - 1
        }),
        up: state => ({
            count: state.count + 1
        })
    }
});

(完整demo可以参见这里
这样就完成了一个Counter,基本由stateviewactions构成:

  • state: 与react中的如出一辙,state的改变会引起重新渲染
  • view: 相当于react中的render
  • actions: 对state进行改变

h相当于reactcreateElement,来看下h接收的参数:

  • tag: 标签名,或者一个函数,传入函数也就意味着无状态组件
  • data: 相当于react中的props
  • children: 子节点

需要注意的一点是,hyperapp并不支持boolean类型,对于boolean类型会忽略,使用时注意将其转化为string类型,如:

<h3>Test {true}</h3>             // Test
<h3>Test {String(true)}</h3>     // Test true

至于为什么?可以参见源码

生命周期

下面来看一下其生命周期,对于hyperapp的整个运行过程,可以参见下图:

图片描述

  • load:相当于reactcomponentWillMount
  • update:相当于reactcomponentWillUpdate
  • render:调用view函数之前调用
  • action:调用actions之前,一般用来进行log
  • resolve:调用actions之后,对于一个异步操作来说,actions返回一个promise,生命周期resolve来处理,返回一个函数update => result.then(update),即框架内部调用update来更新state,重新渲染
    具体代码可以参考:
// 生命周期: action  ->  actions[key]  ->  resolve
// 异步请求需要利用resolve
emit('action', { name: name, data: data });

var result = emit('resolve', action(appState, appActions, data));

return typeof result === 'function' ? result(update) : update(result);

对于每一个节点来说,有着三个特殊的属性:

  • oncreate:相当于componentDidMount
  • onupdate:相当于componentDidUpdate
  • onremove:与componentWillUnMount类似,需要注意的是,加入有了这个属性,那么当节点需要被移除时,也不会被移除,需要自己来从dom中移除,这样设计是为了便于做一些淡入淡出等效果,具体源码可以参见这里,更多的使用方式以及讨论可以参见这里

三个属性均为函数,接收一个参数,就是这个节点

自定义组件

通过上面,基本上可以了解hyperapp的基本写法,下面来看一下如何自定义组件:

“木偶”组件

const Header = ({ title, caption }) => (
    <header>
        <h1>
            {title}
            <small>{caption}</small>
        </h1>
    </header>
);
// 使用
<Header title="hyperapp-example" caption="demo" />

无状态组件的写法与react基本一致,hyperapp官方给出的自定义组件的方式仅仅有这种,但是所有的组件都要是无状态的???答案当然是否定的,如何实现“智能组件”是一个问题:

“智能”组件

利用app方法实现

我们通常的期望业务组件具有一些基本的功能,比如数据获取展现这种:

const Header = app({
    state: {
        caption: 'loading'
    },
    view(state, actions) {
        return (
            <header>{state.caption}</header>
        );
    },
    actions: {
        fetchData(state) {
            return new Promise((resolve) => {
                // 模拟fetch数据
                setTimeout(() => {
                    state.caption = 'ok';
                    resolve(state);
                }, 1000);
            });
        }
    },
    events: {
        load(state, actions) {
            actions.fetchData(state);
        },
        resolve(state, actions, result) {
            if (result && typeof result.then === 'function') {
                return update => result.then(update);
            }
        }
    }
});

export default Header;

按照如下方式使用:

import Header from './Header';
...
state: {
    count: 0
},
view: (state, actions) => (
    <main>
        <Header />
        <h2>{state.count}</h2>
    </main>
),
...

打开页面,从ui来看已经实现组件封装,但是这种是一种”曲线“的实现方式,为什么说它是不正规,可以观察其dom层级,可能与我们理解和期望的并不相同。我们期望得到的层级是:

body
    main
        header
        h2

但是事实上得到的层级为:

body
    header
    main
        h2

至于为什么会产生这种情况,需要看一下源码:

app做了什么?
// app接收一个对象
function app(props) {
    ...
    // appRoot 就是需要挂载到的根节点
    var appRoot = props.root || document.body
    ...
    // 注意此处,下文会用到
    return emit;

    ...
    // 利用raf调用render渲染ui
    function render(cb) {
         element = patch(
            appRoot,
            ...
        );
    }
    ...
    function patch(parent, ...) {
        if (oldNode == null) {
            // 第一次渲染,将节点插入到appRoot中
            // 只要是第一次挂载,element为null
            element = parent.insertBefore(createElement(node, isSVG), element);
        }
        ...
    }
}

所以说将Header组件挂载的原因并不是我们通过jsx写出了这层结构,而是在import的时候,就已经将其挂载到了document.body下,main在挂载到document.body时,被插入到子节点的末尾。

<Header />去哪儿了?

<Header />就这样消失了,先来看下h,就像在reactjsx翻译为createElementhyperappjsx会被翻译为如下形式:

h(tagName, props, children)

来简单的看下h的实现:

function h(tag, data) {
    // 根据后续参数,生成children
    while (stack.length) {
        if (Array.isArray((node = stack.pop()))) {
            // 处理传入的child为数组    
            for (i = node.length; i--; ) {
                stack.push(node[i]);
            }
        }
        ...
    }
    ...
    return typeof tag === 'string' ? {
        tag: tag,
        data: data || {},
        children: children
    } : tag(data, children);
}

可以得出的是,tag接收函数传入,比如木偶组件,tag就是一个函数,但是对于<Header />来说,tagapp函数返回的emit

function emit(name, data) {
    // 一个不常见的写法,这个写法会返回data
    return (
        (appEvents[name] || []).map(function(cb) {
            var result = cb(appState, appActions, data);
            if (result != null) {
                data = result;
            }
        }),
        data
    );
}

基于目前这两点,可以得出:

  • <Header />被转为了,h(emit, null)
  • h返回的就是children,也就是一个[]
  • 由于<Header />作为子节点,会再次被h整理一次,参照h对数组的处理,可以得出[]直接就被忽略掉了
  • 需要render的节点的子节点中根本就没有<Header/>的出现

这种实现方式可以说是非常的不好,局限性也很大,想想可不可以利用其他方法实现:

利用oncreate实现
// 改进Header组件
const Header = (root) => app({
    root,
    ...同上
});

// 改进引入方式
view: (state, actions) => (
    <main>
        <div oncreate={(e) => Header(e)}></div>
        <h2>{state.count}</h2>
    </main>
),

这种方式,利用了oncreate方法,挂载后,载入组件(可以考虑通过代码分割将组件异步加载)

“木偶”组件+mixins

hyperapp支持传入mixins,既然天然的支持这个,那么将一个组件进行两方面分割:

  • view,利用“木偶组件”实现
  • feature,利用mixins实现
    组件定义:
export const HeaderView = ({ text }) => (
    <header>{text}</header>
);

export const HeaderMixins = () => ({
    state:   // 同上
    actions: // 同上
    events:  // 同上
});

使用方式:

import { HeaderView, HeaderMixins } from './HeaderView';
...
state: {
    count: 0
},
view: (state, actions) => (
    <main>
        <HeaderView text={state.caption} />
        <h2>{state.count}</h2>
    </main>
),
mixins: [
    HeaderMixins()
]
...

mixins会将其属性与本身进行一个并操作,可以理解为Object.assign(key, mixins[key]),对于events来说,为一个典型的发布/订阅模式,events的某一种类型对应一个数组,emit时会将其全部执行。本人认为利用这种方式可以实现出一个比较符合框架本意的”智能“组件,但是仍然有些问题,就是state,在使用这个组件时不得不去看一下组件内部的state叫什么名字,而且容易造成同名state冲突的情况。

写在最后

总体来说,hyperapp是一个小而美的框架,值得我们来折腾一下,以上均为本人理解,如有错误还请指出,不胜感激~

一个硬广

我所在团队(工作地点在北京)求大量前端(社招 or 实习),有意者可发简历至:zp139505@alibaba-inc.com

查看原文

赞 7 收藏 15 评论 4

zp1996 赞了文章 · 2017-08-24

Python源码理解: '+=' 和 'xx = xx + xx'的区别

前菜

在我们使用Python的过程, 很多时候会用到+运算, 例如:

a = 1 + 2
print a 

# 输出
3

不光在加法中使用, 在字符串的拼接也同样发挥这重要的作用, 例如:

a = 'abc' + 'efg'
print a

# 输出
abcefg

同样的, 在列表中也能使用, 例如:

a = [1, 2, 3] + [4, 5, 6]
print a

# 输出
[1, 2, 3, 4, 5, 6]

为什么上面不同的对象执行同一个+会有不同的效果呢? 这就涉及到+的重载, 然而这不是本文要讨论的重点, 上面的只是前菜而已~~~

正文

先看一个例子:

num = 123
num = num + 4
print num

# 输出
127

这段代码的用途很明确, 就是一个简单的数字相加, 但是这样似乎很繁琐, 一点都Pythonic, 于是就有了下面的代码:

num = 123
num += 4
print num

# 输出
127

哈, 这样就很Pythonic了! 但是这种用法真的就是这么好么? 不一定. 看例子:

# coding: utf8
l = [1, 2]
l = l + [3, 4]
print l

# 输出
[1, 2, 3, 4]

# ------------------------------------------

l = [1, 2]
l += [3, 4]  # 列表的+被重载了, 左右操作数必须都是iterable对象, 否则会报错
print l

# 输出
[1, 2, 3, 4]

看起来结果都一样嘛~, 但是真的一样吗? 我们改下代码再看下:

# coding: utf8
l = [1, 2]
print 'l之前的id: ', id(l)
l = l + [3, 4]
print 'l之后的id: ', id(l)

# 输出
l之前的id:  40270024
l之后的id:  40389000

# ------------------------------------------

l = [1, 2]
print 'l之前的id: ', id(l)
l += [3, 4]  # 列表的+被重载了, 左右操作数必须都是iterable对象, 否则会报错
print 'l之后的id: ', id(l)

# 输出
l之前的id:  40270024
l之后的id:  40270024

看到结果了吗? 虽然结果一样, 但是通过id的值表示, 运算前后, 第一种方法对象是不同的了, 而第二种还是同一个对象! 为什么会这样?

结果分析

先来看看字节码:

[root@test1 ~]# cat 2.py 
# coding: utf8
l = [1, 2]
l = l + [3, 4]
print l


l = [1, 2]
l += [3, 4]  
print l
[root@test1 ~]# python -m dis 2.py 
  2           0 LOAD_CONST               0 (1)
              3 LOAD_CONST               1 (2)
              6 BUILD_LIST               2
              9 STORE_NAME               0 (l)

  3          12 LOAD_NAME                0 (l)
             15 LOAD_CONST               2 (3)
             18 LOAD_CONST               3 (4)
             21 BUILD_LIST               2
             24 BINARY_ADD          
             25 STORE_NAME               0 (l)

  4          28 LOAD_NAME                0 (l)
             31 PRINT_ITEM          
             32 PRINT_NEWLINE       

  7          33 LOAD_CONST               0 (1)
             36 LOAD_CONST               1 (2)
             39 BUILD_LIST               2
             42 STORE_NAME               0 (l)

  8          45 LOAD_NAME                0 (l)
             48 LOAD_CONST               2 (3)
             51 LOAD_CONST               3 (4)
             54 BUILD_LIST               2
             57 INPLACE_ADD         
             58 STORE_NAME               0 (l)

  9          61 LOAD_NAME                0 (l)
             64 PRINT_ITEM          
             65 PRINT_NEWLINE       
             66 LOAD_CONST               4 (None)
             69 RETURN_VALUE    

在上诉的字节码, 我们着重需要看的是两个: BINARY_ADDINPLACE_ADD! 很明显:
l = l + [3, 4, 5]    这种背后就是BINARY_ADD
l += [3, 4, 5]     这种背后就是INPLACE_ADD

深入理解

虽然两个单词差很远, 但其实两个的作用是很类似的, 最起码前面一部分是, 为什么这样说, 请看源码:

# 取自ceva.c
# BINARY_ADD
TARGET_NOARG(BINARY_ADD)
        {
            w = POP();
            v = TOP();
            if (PyInt_CheckExact(v) && PyInt_CheckExact(w)) {    // 检查左右操作数是否 int 类型
                /* INLINE: int + int */
                register long a, b, i;
                a = PyInt_AS_LONG(v);
                b = PyInt_AS_LONG(w);
                /* cast to avoid undefined behaviour
                   on overflow */
                i = (long)((unsigned long)a + b);
                if ((i^a) < 0 && (i^b) < 0)
                    goto slow_add;
                x = PyInt_FromLong(i);
            }
            else if (PyString_CheckExact(v) &&
                     PyString_CheckExact(w)) {                   // 检查左右操作数是否 string 类型
                x = string_concatenate(v, w, f, next_instr);
                /* string_concatenate consumed the ref to v */
                goto skip_decref_vx;
            }
            else {
              slow_add:                                          // 两者都不是, 请走这里~
                x = PyNumber_Add(v, w);
            }
           ...(省略)


# INPLACE_ADD
TARGET_NOARG(INPLACE_ADD)
        {
            w = POP();
            v = TOP();
            if (PyInt_CheckExact(v) && PyInt_CheckExact(w)) {   // 检查左右操作数是否 int 类型
                /* INLINE: int + int */
                register long a, b, i;
                a = PyInt_AS_LONG(v);
                b = PyInt_AS_LONG(w);
                i = a + b;
                if ((i^a) < 0 && (i^b) < 0)
                    goto slow_iadd;
                x = PyInt_FromLong(i);
            }
            else if (PyString_CheckExact(v) &&
                     PyString_CheckExact(w)) {                 // 检查左右操作数是否 string 类型
                x = string_concatenate(v, w, f, next_instr);
                /* string_concatenate consumed the ref to v */
                goto skip_decref_v;
            }
            else {
              slow_iadd:                           
                x = PyNumber_InPlaceAdd(v, w);                 // 两者都不是, 请走这里~
            }
           ... (省略)

从上面可以看出, 不管是BINARY_ADD 还是 INPLACE_ADD, 他们都会有如下相同的操作:

检查是不是都是`int`类型, 如果是, 直接返回两个数值相加的结果
检查是不是都是`string`类型, 如果是, 直接返回字符串拼接的结果

因为两者的行为真的很类似, 所以在这着重讲INPLACE_ADD, 对BINARY_ADD感兴趣的童鞋可以在源码文件: abstract.c, 搜索: PyNumber_Add.实际上也就少了对列表之类对象的操作而已.

那我们接着继续, 先贴个源码:

PyObject *
PyNumber_InPlaceAdd(PyObject *v, PyObject *w)
{
    PyObject *result = binary_iop1(v, w, NB_SLOT(nb_inplace_add),     
                                   NB_SLOT(nb_add));
    if (result == Py_NotImplemented) {
        PySequenceMethods *m = v->ob_type->tp_as_sequence;
        Py_DECREF(result);
        if (m != NULL) {
            binaryfunc f = NULL;
            if (HASINPLACE(v))
                f = m->sq_inplace_concat;
            if (f == NULL)
                f = m->sq_concat;
            if (f != NULL)
                return (*f)(v, w);
        }
        result = binop_type_error(v, w, "+=");
    }
    return result;

INPLACE_ADD本质上是对应着abstract.c文件里面的PyNumber_InPlaceAdd函数, 在这个函数中, 首先调用binary_iop1函数, 然后进而又调用了里面的binary_op1函数, 这两个函数很大一个篇幅, 都是针对ob_type->tp_as_number, 而我们目前是list, 所以他们的大部分操作, 都和我们的无关. 正因为无关, 所以这两函数调用最后, 直接返回Py_NotImplemented, 而这个是用来干嘛, 这个有大作用, 是列表相加的核心所在!

因为binary_iop1的调用结果是Py_NotImplemented, 所以下面的判断成立, 开始寻找对象(也就是演示代码中l对象)的ob_type->tp_as_sequence属性.

因为我们的对象是l(列表), 所以我们需要去PyList_type需找真相:

# 取自: listobject.c
PyTypeObject PyList_Type = {
    ... (省略)
    &list_as_sequence,                          /* tp_as_sequence */
    ... (省略)
}

可以看出, 其实也就是直接取list_as_sequence, 而这个是什么呢? 其实是一个结构体, 里面存放了列表的部分功能函数.

static PySequenceMethods list_as_sequence = {
    (lenfunc)list_length,                       /* sq_length */
    (binaryfunc)list_concat,                    /* sq_concat */
    (ssizeargfunc)list_repeat,                  /* sq_repeat */
    (ssizeargfunc)list_item,                    /* sq_item */
    (ssizessizeargfunc)list_slice,              /* sq_slice */
    (ssizeobjargproc)list_ass_item,             /* sq_ass_item */
    (ssizessizeobjargproc)list_ass_slice,       /* sq_ass_slice */
    (objobjproc)list_contains,                  /* sq_contains */
    (binaryfunc)list_inplace_concat,            /* sq_inplace_concat */
    (ssizeargfunc)list_inplace_repeat,          /* sq_inplace_repeat */
};

接下来就是一个判断, 判断咱们这个l对象是否有Py_TPFLAGS_HAVE_INPLACEOPS这个特性, 很明显是有的, 所以就调用上步取到的结构体中的sq_inplace_concat函数, 那接下来呢? 肯定就是看看这个函数是干嘛的:

list_inplace_concat(PyListObject *self, PyObject *other)
{
    PyObject *result;

    result = listextend(self, other);    # 关键所在
    if (result == NULL)
        return result;
    Py_DECREF(result);
    Py_INCREF(self);
    return (PyObject *)self;
}

终于找到关键了, 原来最后就是调用这个listextend函数, 这个和我们python层面的列表的extend方法很类似, 在这不细讲了!

PyNumber_InPlaceAdd的执行调用过程, 简单整理下来就是:

INPLACE_ADD(字节码)
    -> PyNumber_InPlaceAdd
        -> 判断是否数字: 如果是, 直接返回两数相加
        -> 判断是否字符串: 如果是, 直接返回`string_concatenate`的结果
        -> 都不是:
            -> binary_iop1 (判断是否数字, 如果是则按照数字处理, 否则返回Py_NotImplemented)
                -> binary_iop (判断是否数字, 如果是则按照数字处理, 否则返回Py_NotImplemented)
            -> 返回的结果是否 Py_NotImplemented:
                -> 是: 
                    -> 对象是否有Py_TPFLAGS_HAVE_INPLACEOPS:
                        -> 是: 调用对象的: sq_inplace_concat
                        -> 否: 调用对象的: sq_concat
                -> 否: 报错

所以在上面的结果, 第二种代码: l += [3,4,5], 我们看到的id值并没有改变, 就是因为+=通过sq_inplace_concat调用了列表的listextend函数, 然后导致新列表以追加的方式去处理.

结论

现在我们大概明白了+=实际上是干嘛了: 它应该能算是一个加强版的+, 因为它比+多了一个写回本身的功能.不过是否能够写回本身, 还是得看对象自身是否支持, 也就是说是否具备Py_NotImplemented标识, 是否支持sq_inplace_concat, 如果具备, 才能实现, 否则, 也就是和 + 效果一样而已.

欢迎各位大神指点交流,转载请注明来源: https://segmentfault.com/a/11...

查看原文

赞 6 收藏 2 评论 2

zp1996 赞了文章 · 2017-08-21

腾讯祭出大招VasSonic,让你的H5页面首屏秒开

VasSonic成长历程


前言

2017.8.8 14时,SNG增值产品部Vas团队研发的轻量级高性能Hybrid框架VasSonic通过了公司最终审核,作为腾讯开源组件分享给大家。从当初立项优化页面加载速度,到不断摸索、优化,再到整理代码、文档,最终在Github上开源,并且在24小时内获取star数超过1600。我们非常高兴看到我们的成果收到这么多的关注,趁此机会,正好回顾一下VasSonic的成长历程,也希望能够让大家更了解VasSonic。

项目背景

Web相信大家再熟悉不过了,它具有快速迭代发布的天然优势,但也存在中一些让人诟病的问题,比如加载速度慢,体验差等。在此之前,手Q上很多页面首屏打开速度居高不下,甚至有些耗时达到3s以上,这意味着用户打开页面必须经过3秒之后才能进行交互操作,体验相当差,很多用户忍受不了这个漫长的时间直接流失掉了。

为了提升用户体验和业务用户留存率,我们很多业务一开始通过Web开发,等页面模型验证符合预期后,再将H5页面转化成原生界面。我们很快意识到这不是一种健康的可持续的开发模式,一方面存在重复人力浪费,另外一方面原生商城除了速度快一点,要运营活动改版都很难。

所以后来团队改了切入方向,安排人力专心研究如何加快页面打开速度,经过了一系列的摸爬滚打和优化探索,最终我们研发出了VasSonic框架,让H5页面首屏达到秒开,给用户一个更好的H5体验。下面就和大家分享VasSonic框架的发展历程。

业务形态

任何一个技术框架都是结合具体的业务形态来进行发展优化的,技术是为了更好地服务业务,业务也会驱动技术的发展。在此首先介绍一下业务形态,我们是来自手Q增值产品部门的VAS团队,负责手机QQ上很多深受年轻人喜欢的个性化增值服务,比如气泡、挂件、主题等等。手Q上大部分的业务还是基于H5开发的,大家对手Q的业务形态可能有简单的了解。比如下图的游戏分发中心、会员特权中心、个性化装扮商城等。这部分商城的特点比较明显,页面的很多数据都是动态的,是由我们的产品经理在后台配置的。
业务

这些都是很常见页面,我们通常将html/js/css等静态资源放到CDN上,然后页面加载后,再通过CGI去拉取最新的数据,进行拼接展示, 这样子可以利用到CDN的多地部署和就近接入等优势,同时提高了服务器的并发能力。这种传统模式的加载流程如下所示:
加载流程

  1. 用户点击后,经过终端一系列初始化流程,比如进程启动、Runtime初始化、创建WebView等等。
  2. 完成初始化后,WebView开始去CDN上面请求Html加载页面。
  3. 页面发起CGI请求对应的数据或者通过localStorage获取数据,数据回来后再对DOM进行操作更新

可以看出上述流程存在着几个问题:

  1. 从外网统计数据来看,用户的终端耗时在1s以上,这意味着在这1s多的时间里,网络完全是空闲在等待的,非常浪费;
  2. 页面的资源和数据完全依赖于网络,特别是用户在弱网络场景下,页面会出现很长时间的白屏,体验非常差;
  3. 因为页面的数据依赖于动态拉取,加载完页面后,往往是看到一些模块先转菊花,再展示,体验也是不好的。同时这里涉及到较多数据更新,经常要更新DOM,性能上也有不少开销。

所以针对以上几个问题,我们也对应做了很多优化和探索。

问题

VasSonic的前世

优化终端

针对终端耗时1s以上的情况,我们对手Q WebView框架进行了重构:

  1. 启动流程彻底拆分,设计为一个状态机按序按需执行
  2. View相关拆分模块化设计,尽可能懒加载,IO异步化
  3. X5内核在手Q中的独立进程中提前预加载
  4. 创建WebView对象复用池

关于第四点,我们想分享一些Android平台上的细节,由于Android系统的生态原因,导致用户的系统版本和系统Webkit内核处于极其分裂状态,所以我们公司在手Q和微信统一使用X5内核。相对系统WebView来说,首次启动X5内核时,创建WebView比较耗时,因此我们尽量想复用WebView,但是WebView却是与Activity Context绑定。销毁复用的时候,需要释放Activity的Context,否则会内存泄露。针对这种情况,有没有一种两全其美的办法呢?

计算机有一句经典的名言:计算机领域任何一个问题都可以通过引入中间层来解决。于是我们通过包装的方式,实现了一个Context的壳,真正的实现体包装在里面,逻辑调用真正调用到对应的实现体的函数。 经过实验发现,Android系统本身提供了这么一个MutableContextWrapper,作为Context的一个中间层。

我们会将Activity context包在MutableContextWrapper里面,destory的时候,会将WebView的Context设置为Application的Context,从而释放Activity Context。
类似如下:


//precreate WebView
MutableContextWrapper contextWrapper = new MutableContextWrapper(BaseApplicationImpl.sApplication);
mPool[0] = new WebView(contextWrapper);

//reset WebView 
ct =(MutableContextWrapper)webview.getContext();
ct.setBaseContext(getApplication());

//reuse WebView
((MutableContextWrapper)webview.getContext()).setBaseContext(activityContext);

静态直出

“直出”这个概念对前端同学来说,并不陌生。为了优化首屏体验,大部分主流的页面都会在服务器端拉取首屏数据后通过NodeJs进行渲染,然后生成一个包含了首屏数据的Html文件,这样子展示首屏的时候,就可以解决内容转菊花的问题了。
当然这种页面“直出”的方式也会带来一个问题,服务器需要拉取首屏数据,意味着服务端处理耗时增加。
不过因为现在Html都会发布到CDN上,WebView直接从CDN上面获取,这块耗时没有对用户造成影响。
手Q里面有一套自动化的构建系统Vnues,当产品经理修改数据发布后,可以一键启动构建任务,Vnues系统就会自动同步最新的代码和数据,然后生成新的含首屏Html,并发布到CDN上面去。

直出

离线预推

页面发布到CDN上面去后,那么WebView需要发起网络请求去拉取。当用户在弱网络或者网速比较差的环境下,这个加载时间会很长。于是我们通过离线预推的方式,把页面的资源提前拉取到本地,当用户加载资源的时候,相当于从本地加载,即使没有网络,也能展示首屏页面。这个也就是大家熟悉的离线包。
手Q使用7Z生成离线包, 同时离线包服务器将新的离线包跟业务对应的历史离线包进行BsDiff做二进制差分,生成增量包,进一步降低下载离线包时的带宽成本,下载所消耗的流量从一个完整的离线包(253KB)降低为一个增量包(3KB)。
带宽优化

经过一系列优化后,在Android平台上,点击到页面首屏展示的耗时从平均3s多降低为1.8s,优化40% 以上

数据对比

VasSonic的诞生

虽然通过静态直出和离线预推等方式优化后,速度已经达到1.8s,但还存在很大的优化空间,当我们准备持续深入优化时,我们的业务形态发生了新的变化。

之前我们页面内容的数据主要是由产品经理要配置的,用户看到的内容基本都是一样的。而现在页面为了更好地为用户推荐喜欢的内容,我们后台引入机器学习和随机算法来做智能个性化推荐。比如左边新用户推荐的是新货精选,而右边活跃用户展示的是潮品推荐。另外还有部分的内容是随机算法推荐的。这意味着不同用户看到的内容是不同的,同一个用户不同时间看到的内容也有可能不同。

新业务

所以为了满足业务的需求,我们只能实时拉取用户数据并在服务端渲染后返回给客户端,也就是动态直出的方案。

但是动态直出方案存在几个比较明显的问题:

  1. 服务端实时拉取数据渲染导致白屏时间长,因为服务器要先实时拉取个人数据,然后进行渲染直出,这个耗时不可控;
  2. 首屏无法使用离线预推等缓存策略,因为每个用户看到的内容不一样,我们无法通过静态直出的方式那样把Html全部发布到CDN;

虽然动态直出方案下,页面首屏无法通过离线预推等方式进行加载优化,但前面优化积累的经验给我们提供了思路:要优化白屏问题,核心还是得从提升资源加载速度方向入手。所以我们重点在资源加载方面进行了深度优化。

并行加载

首先在加载流程方面,我们发现这里WebView访问依然是串行的, WebView要等终端初始化完成之后,才发起请求。虽然终端耗时优化了不少,但是从外网的统计数据来看,终端初始化还是存在几百毫秒的耗时,而这段时间内网络是在空等的。

串行

因此性能上不够极致,我们优化代码,这两个操作并行处理,流程改为:

并行

并行处理后速度有所改善,但我们发现在某些场景下,终端初始化比较快,但数据没有完成返回,这意味着内核在空等,而内核是支持边加载边渲染的,我们在并行的同时,能否也利用内核的这个特性呢?

于是我们加入了一个中间层来桥接内核和数据,内部称为流式拦截:

桥接流

  1. 启动子线程请求页面主资源,子线程中不断讲网络数据读取到内存中,也就是网络流(NetStream)和内存流(MemStream)之间的转换;
  2. 当WebView初始化完成的时候,提供一个中间层BridgeStream来连接WebView和数据流;
  3. 当WebView读取数据的时候,中间层BridgeStream会先把内存的数据读取返回后,再继续读取网络的数据。

通过这种桥接流的方式,整个内核无需等待,继续做到边加载边解析。这种并行的方式让首屏的速度优化15%以上,进一步提升了页面加载速度。

动态缓存

通过并行加载,我们极大地提升了WebView请求的速度,但是在弱网络场景下白屏时间还是非常长,用户体验非常糟糕。于是我们在思考,是否能够将用户的已经加载的页面内容缓存下来,等用户下此点击页面的时候,我们先加载展示页面缓存,第一时间让用户看到内容,然后同时去请求新的页面数据,等新的页面数据拉取下来之后,我们再重新加载一遍即可。

动态缓存

保存页面内容这个工作很简单,因为现在我们资源读取都是通过中间层BridgeStream来管理的,只需要将整个读取的内容缓存下来即可。
于是我们就按动态缓存这种方案去实现了,但很快就发现了问题。用户打开页面之后,先是看到历史页面,等用户准备去操作的时候,突然页面白闪一下,重新加载了一遍,这种体验非常差,特别在一些低端机器上,这个白闪的过程太明显,非常影响体验,这是用户和产品经理都不能接受的。于是我们在思考,能否只做局部的刷新,仅刷新变化的元素呢?

通过分析,我们发现同一个用户的页面,大部分数据都是不变的,经常变化的只有少量数据,于是我们提出了模板(template)和数据块(data)的概念:页面中经常变化的数据我们称为数据块,除了数据块之外的数据称为模板。

页面分离

我们将整个页面html通过VasSonic标签进行划分,包裹在标签中的内容为data,标签外的内容为模版。

页面规范

首先我们对Html内容进行了扩展,通过代码注释的方式,增加了“sonicdiff-xxx”来标注一个数据块的开始与结束。
而模板就是将数据块抠掉之后的Html,然后通过{albums}来表示这个是一个数据块占位。
数据就是JSON格式,直接Key-Value。
当然,为了完美地兼容Html,我们对协议头部进行了扩展,比如增加accept-diff来标注是否支持增量更新、template-tag来标注模板的md5是多少等。OK,有了上面这个规则或者公式后,我们就可以实现增量更新了。

请求规范约定

VasSonic为了支持区分客户端是否支持增量更新等能力,对头部字段进行了扩展

字段说明请求头(Y/N)响应头(Y/N)
accept-diff表示终端是否支持VasSonic模式,true为支持,否则不支持YN
If-none-match本地缓存的etag,给服务端判断是否命中304YN
etag页面内容的唯一标识(哈希值)NY
template-tag模版唯一标识(哈希值),客户端使用本地校验 或 服务端使用判断是模板有变更YY
template-change标记模版是否变更,客户端使用NY
cache-offline客户端端使用,根据不同类型进行不同行为NY

cache-offline字段说明

字段说明
true缓存到磁盘并展示返回内容
false展示返回内容,无需缓存到磁盘
store缓存到磁盘,如果已经加载缓存,则下次加载,否则展示返回内容
http容灾字段,如果http表示终端六个小时之内不会采用sonic请求该URL

模式介绍

VasSonic根据本地是否有缓存以及本地缓存数据跟服务器数据的差异情况分为以下四种模式。

模式说明条件
首次加载本地没有缓存,即第一次加载页面etag为空值或template_tag为空值
完全缓存本地有缓存,且缓存内容跟服务器内容完全一样etag一致
数据更新本地有缓存,本地模版内容跟服务器模版内容一样,但数据块有变化etag不一致 且 template_tag一致
模版更新本地有缓存,缓存的模版内容跟服务器的模版内容不一样etag不一致 且 template_tag不一致

首次加载

我们会在请求头部带上支持accept-diff为true和sdk版本号等标识着首次加载的信息。当请求返回后,VasSonic会在延迟几秒后(避免激烈IO竞争)将页面抽离成模板和数据并保存到本地。此时终端缓存目录下,该页面将对应三个缓存文件xxx.html、xxx.template、xxx.data,其中xxx是该页面的唯一标识(即sonicSessionId)。

对于页面非首次加载场景,VasSonic优先加载本地缓存, 同时我们会在请求头部带上当前缓存和模板的md5,后台进行模板md5对比之后,分为以下几种情况:

非首次加载之完全缓存

本地有缓存,且缓存内容跟服务器内容完全一样.

非首次加载之增量数据

增量数据

如果模板发现没有变化,那么会在响应头部返回template-change=false,同时响应包体返回的数据不再是完整的html,而是一段JSON数据,及全部的数据块。我们现在需要跟本地数据进行差分,找出真正的增量数据,如上图中,后台返回了N个数据,实际上仅有一个数据是有变化的,那么我们仅需要将这个变化的数据提交到页面即可。一般场景下,这个差异的数据比全部数据要小很多。如果页面拆分数据得更细,那么页面的变动就更小,这个取决于前端同学对数据块的细化程度。

获得变化数据块(diff_data)后,客户端只需要通知页面页面设置的回调接口(getDiffDataCallback)进行界面元素更新即可。这里javascript的通信方式也可以自由定义(可以使用webview标准的javascript通信方式,也可以使用伪协议的方式),只要页面跟终端协商一致就可以。
提交增量

对于数据更新这种场景,终端还会将新的数据和模板拼接成为新的页面,保持缓存最新。当终端初始化比较慢的时候,WebView去加载缓存的时候,这个页面可能已经是最新的了,连数据刷新都不需要。

非首次加载之模板更新

与数据更新模式不一样,由于业务需求,页面的模板会发生更改。当终端在获取到新的模板和数据后,本地在子线程中进行合并,生成一个新的缓存,然后回调通知终端,刷新WebView来加载新的缓存。

我们来看一下最终的流程图,跟动态缓存对比,有不少细节优化:

整体流程

我们从第2步开始,SonicSession首先会去读取缓存。会抛个消息通知WebView读取缓存,如果Webview已经准备好,则直接加载缓存,如果没有,则缓存先放在内存里面。同时SonicSession也会带上模板等信息到后台拉取新的内容,后台经过Sonic-Diff之后,会返回新的数据。SonicSession拿到新的数据后,首先会跟本地数据进行Diff,如果发现WebView已经加载缓存,则直接提交增量数据给页面。否则继续拼接最新的页面,替换掉内存里面的缓存,同时保存到本地。这个时候WebView如果Ready,则直接进行第5步load最新的内容即可。

效果统计

效果统计

这个是我们外网的统计数据。在数据更新模式下,首屏的耗时在1s左右,相比普通的动态直出,优化了50%以上。模板更新这个会比首次高,是因为加载了两次页面,不过从模式占比上来看,我们大部分页面都是数据更新。针对模板更新这种耗时比较高的情况,前面优化积累的经验给我们提供了思路,核心还是从提前获取资源方向入手,因此我们优先考虑如何预加载模板更新。

预加载

实际上整个SonicSession在没有WebView的情况下,也是可以独立完成所有逻辑的,当用户点击页面的时候,我们在将WebView和SonicSession绑定起来即可。于是我们支持了两种预加载的模式,一种是通过后台push的方式,来提前获取数据。还有一种就是JSAPI,页面可以调用JSAPI来预加载用户可能操作的下一个页面。通过这两种方式,我们可以把需要的增量更新数据提前拉取回来
预加载

效果对比

Pic 1: 没有使用VasSonicPic 2: 使用VasSonic
default modeVasSonic mode

展望未来

开源只是故事的开始,我们仍会持续对 VasSonic 做改进,包括更易用的接口、更好的性能、更高的可靠性,同时快速响应解决开源后的issue和PR。这些改进最终也会原封不动地在手Q内使用,这一切都是为了更快的WebView加载速度。

Talk is cheap,read the fucking code. If you are interested in VasSonic, don't forget to STAR VasSonic.
Thank you for reading ~

查看原文

赞 96 收藏 346 评论 22

zp1996 关注了用户 · 2017-06-23

小_秦 @xqin

再见sf

关注 577

认证与成就

  • 获得 251 次点赞
  • 获得 78 枚徽章 获得 1 枚金徽章, 获得 16 枚银徽章, 获得 61 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2015-09-19
个人主页被 2.9k 人浏览