迅雷、微众银行面试

一面

1.自我介绍和实习经历

2.闭包作用域

3.vue向绑定原理

4.api设计

5.读过的书

6.对后端的了解,着重问了node的一些特性

7.http的理解

8.react单向数据流的理解

9.nginx异步非阻塞特性

10.对象的访问器与数据属性

11.前端安全的理解

12.冻结对象、不可拓展对象、密封对象

13.看过那些书,对现代前端怎么看,你认为那种人是很强的前端和很强的工程师
一面还是很基础的,我基本全部答上了,面试官当场就说进入二面了,明天面试。

二面

面试官说自己以前是我实习公司深信服的前端。

1.实现一个场景,其中有短视频、允许平移的弹幕、实时聊天,兼容ie9以上。 我提出的解决方案是:css动画完成弹幕;短视频用html5的vedio比如flv.js这种开源方案;实时通信有几种解决方案:websocket、短轮询、长轮询。 里边问的比较深,具体到Http协议和api的使用,自己很多地方没有实现过所以经验不足但这块总体答的还行。弹幕的一个匀速动画问题,被搞的很难受,我真是不擅长写动画。

2.es6的一堆语法:生成器,迭代器,元编程。嗯,都是es6里比较晦涩的部分。

3.for…in…、for()循环、forEach循环的区别,为什么有了前两个还要加forEach。

微众银行面试

微众银行很坑爹,全场谈人生一个技术问题都没问。 话说我还是很喜欢腾讯系公司的,结果腾讯捞了我简历没面我,富途证券终面挂,微众银行谈人生挂

360校招面试

做完接到电话告诉我笔试通过了,今天早上10点开始面试,最近最难一场面试,一面发挥良好,顺利过;二面中途问题太难太紧张,后期基本思路很乱,主要还是知识储备不够。

一面(45分钟)

1.介绍项目

2.svg与canvas的不同,热力图应该用哪个

3.介绍下vue开发时候如何规划项目,讲了讲组建、通信状态管理、路由、通信方式、打包发布。

4.游览器渲染过程。详细讲了渲染引擎和JS引擎。也讲了些编译原理的东西。

5.webpack本地开发怎么解决跨域的

6.webpack的原理,哪里是词法分析还是语法分析,具体什么?

7.loader和plugin区别,分别做什么

8.vue的v-dom原理,为什么高效,和模板引擎什么区别

9.diff算法

10.深拷贝,写代码。各种数据类型哪些在堆栈上?jquery的extend是浅拷贝还是深拷贝。Json的方法实现有什么缺陷

11.原型继承,写代码不能用ES6的class…extend…

12.service worker

13.websocket

14.flex布局实现栅格,实现水平垂直居中 一面问题比较和我口味,都是实现和研究过的,就全部回答出来包括追问,估计面试官也是给了个比较高的评价,当初给通过让我准备二面。刚结束立即就通知二面,然后二面很难很难,直接血崩了。

二面(1小时)

1.前端优化

2.get和post区别,get和post性能差距大不大

3.http基于udp还是tcp?tcp和udp什么区别?几次握手几次断开?为什么要这样设计?如果不这样可能会发生什么?

4.resuful的API设计

5.游览器缓存机制

6.跨域,追问正向代理与反向代理,追问websocket跨域

7.vue平级组件通信

8.200万条数据插入vue的data,不添加watcher怎么实现

9.react平级组件通信

10.rudex的设计思想

11.flux架构的单向数据流有哪些部分组成,和vuex不同点

12.jsx怎么被解析?我说AST,我知道肯定要用AST,具体说说过程。那解析jsx用了babel和webpack的什么插件或者loader

13.组件热加载方案

14.你框架掌握的不是很好。问你js基础吧。写一个观察者模式。我写不出来,讲了下概念。写一个单例模式。

15.遍历一个多叉树,我写了个递归被吐槽复杂度高。后边问我用什么数据结构优化,我说数组。然后没能写出来算法。

10分钟后收到电话告诉我未通过二面。1-6答上了,第7题到第13题基本全部不会或者讲的不清楚。感谢把我虐的很惨的二面面试官。

JavaScript垃圾回收机制

kdown>

操作系统的三种内存分配机制

静态分配: 从静态存储区域分配内存。程序编译的时候内存已经分配好了,并且在程序的整个运行期间都存在,如静态变量和全局变量。 自动分配: 在栈中为局部变量分配内存的方法,栈中的内存可以随着代码块退出时的出栈操作被自动释放。例如在执行函数时,函数内局部变量的存储单元可以在栈上创建,函数结束时这些存储单元自动被释放。 动态分配: 从静态存储区域分配内存。程序编译的时候内存已经分配好了,并且在程序的整个运行期间都存在,如静态变量和全局变量。 栈在底堆在上,function的执行都是通过入栈出栈,执行的时候去堆取到数据推入执行栈,成员变量在堆,局部变量在栈,全局变量也在堆;引用类型的引用变量存储在栈中,指向于实际存储在堆中的实际对象。

标记清楚

JavaScript最常用的垃圾回收方式是标记清楚,标记清楚是最早ie游览器采用的方式,现代游览器基本全部使用标记清楚的垃圾回收策略。 标记清楚:当变量进入执行环境(在函数里声明,赋值等),变量被标记为进入环境,进入环境的变量的内存永远不会释放,当变量离开环境(执行完函数)时,标记为“离开环境”,将其内存回收。

引用计数

引用计数在JavaScript内存回收中用的较少,现在只有KDE下的KJS引擎的游览器在采用,而最早是网景3游览器采用了这种方式。 引用计数的含义是跟踪记录每个值被引用的次数,当声明一个变量并将一个引用类型赋值给变量时,引用次数是1.如果值又赋给另外一个变量,引用次数加1,如果包含这个值引用的变量取得了其他值,引用次数减1,当引用次数为0,表示不能再访问到,即可回收。 值得注意的是,虽然基本没有游览器上JavaScript使用引用计数了,但ie9一下游览器很多对象不是原生JavaScript对象,比如DOM与BOM中的对象就是c++以COM(组件对象模型)实现的,而COM的垃圾回收采用的是引用计数策略,因此即使游览器采用标记清楚实现垃圾回收,但只要在游览器设计COM对象,就使用了引用计数。这会引起循环引用的问题。如;

var ele = document.getElementById('ele');
var myObj = new Object();
myObj.ele = ele;  //新对象一个属性引用了com对象
ele.someObj = myObj;  ///com对象一个属性引用了JavaScript对象

在上述情况下,即使将DOM从页面移除,其内存也不会被回收。为了避免出现,要在使用完毕后手工回收,赋值null来手工解除引用。 ie9以上的游览器上,BOM与DOM变成了JavaScript对象,不会再出现这个问题,但做兼容性处理的时候这种内存泄漏是要考虑的。

网易面试

一面(1小时)

1.自我介绍。 2.问项目,问实习。 3.indexDB,cookie,localStorage,sessionStorage区别。 4.游览器缓存机制。就是HTTP相关字段的解释。 6.js合并后与合并前哪个快,也就是一个<script>标签与两个情况下,哪个更快。第一反应合并后,说合并后减少http请求,后边反应过来说不一定,看script位置,如果在head里优先于css与dom加载了的话,会阻塞影响的,同时还说了游览器引擎渲染过程这些分析了,然后提到了defer异步,被问除了defer还有什么,说了async,被问区别。然后面试官说只考虑理想情况,都是正常些都在Body底部,那个快?我坚持的认为合并后快,但面试官说http是同步多线程的,所以多个请求。 7.HTTP请求报文结构,当年腾讯实习生招聘问我这个没答好,回去看了HTTP权威指南,所以说了一部分后追问真实HTTP是怎么区分各个字段的,我说换行,那冒号呢?这个不懂了。 8.事件绑定的原理,冒泡与捕获概念以及API。 9.手写代码,封装一个事件处理函数。 10.点击input事件传播的全过程,除了focus,blur,click想不起来了。 11.简单定位问题,修改后的定位,思考几秒后没想出来(其实很简单),然后面试官问你是不是CSS不好,我说是的不擅长CSS。 12.CSS画三角形。 13.box-sizing。 14.z-index,追问了何时生效,我说我用的时候position是absolute的,只知道肯定有定位条件具体什么不清楚。后来查了是被定位了的元素才会生效。 15.看过那些前端书籍。听完面试官说你一本css都没看过,怪不得css不好。 16.面试结束,有没有问题问。我问网易智能与感知中心做什么工作,期间聊到了对人工智能,VR、AR的认知。面试官建议学好JS基础同时也适当注重下CSS,找本书看看。

二面(30分钟)

1.自我介绍。 2.项目介绍。 3.SEO。第一次遇上问SEO,大概说了一些SEO基础后,聊到SPA的SEO怎么优化,结果自己带了个坑说了SSR,然后被问有没有做过SSR,没做过。 4.前端跨域,从同源机制聊到六种跨域方法基本全了。提了CSRF与XSS没讲细节。 5.又问了一次游览器缓存机制。比第一次说的全了一些。 6.谈谈前端工程化理解,答得挺全的,遗憾忘记提ES6,其实我倒是希望被问ES6的问题,前段时间一直写ES6除了个别不常见api外,ES6很熟悉了。 7.又问了一次box-sizing。 8.看了那些书。 9.啥时候学前端的,怎么学的。 10.解释下原型链,两句话说完我感觉说的有点少,然后重说了一次还是两句话。 11.有没有问题要问。我表示惊讶的说这么快。然后问了部门用技术栈,用的工具,人工智能团队前端的业务。后来面试官还给出建议,说我知道的已经很全面,但表述上存在问题,首先声音太小虽然我听的懂,此外说的太快中间不停顿,无法get重点,建议增加下表述条理性。然后面试官介绍说他们用自己开发的regular框架.

Zepto源码分析——事件模块

Zepto的事件模块有些简化,因为Zepto作为一个针对移动端游览器的框架,所以游览器的事件系统本身相对完善,不需要做过多事件机制中最复杂的兼容处理。所以在分析后打算总结下针对PC端框架中兼容处理的。

游览器事件API

dom提供了三种层级的事件api:html事件、dom0级事件、dom2级事件(可以绑定多个回调)。整个事件机制主要简历在dom0级与dom2级两个标准上:http://bugzhang.com/2017/08/13/chang-yong-de-javascriptdai-ma-duan-2-dom-shi-jian-ajax-cookie/ ,改博文里第一段代码就是相关的实现,此处不再阐述。

与dom 0级事件的缺陷

  • 对于DOM3新增事件不支持,如:FocusIn,FocsuOut,MouseRemoved,MouseScrill等,但这些事件用的很少
  • 每次只可以绑定一个回调,重复绑定就会取消掉上次的绑定
  • 在ie下回调没有参数,在其他游览器回调第一个参数是事件对象
  • 只能再冒泡阶段可用

ie事件addachEvent的缺陷

  • this指向的是window,存在内存泄漏
  • 多钟时间绑定回调后,执行顺序不是按照绑定时的顺序触发
  • 与W3C有一些事件有区别
  • 只支持冒泡阶段

addEventListenner的缺陷

  • 部分游览器与标准的事件定义不一致
  • 第四个参数是ff跨文档监听事件,第五个参数是flash下制定监听函数的引用强弱
  • 事件对象不稳定,各个游览器有区别
  • input事件不如ie的propertychange事件好用

Zepto的Event模块核心方法

add:内部用来添加事件的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
function add(element, events, fn, data, selector, delegator, capture) {
//拓展标识属性zid
var id = zid(element),
set = (handlers[id] || (handlers[id] = []))
events
//匹配空格,获取多个event
.split(/\s/)
.forEach(function (event) {
if (event == 'ready')
return $(document).ready(fn)
var handler = parse(event)
handler.fn = fn
handler.sel = selector
// 如果事件是emulate mouseenter, mouseleave
if (handler.e in hover)
fn = function (e) {
var related = e.relatedTarget
if (!related || (related !== this && !$.contains(this, related)))
return handler.fn.apply(this, arguments)
}
//事件代理
handler.del = delegator
var callback = delegator || fn
handler.proxy = function (e) {
e = compatible(e)
if (e.isImmediatePropagationStopped())
return
e.data = data
var result = callback.apply(element, e._args == undefined
? [e]
: [e].concat(e._args))
if (result === false)
e.preventDefault(),
e.stopPropagation()
return result
}
handler.i = set.length
set.push(handler)
//如果支持dom2级事件
if ('addEventListener' in element)
element.addEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))
})
}

remove:内部删除事件的方法

1
2
3
4
5
6
7
8
9
function remove(element, events, fn, selector, capture) {
var id = zid(element)
eachEvent(events || '', fn, function(event, fn) {
findHandlers(element, event, fn, selector).forEach(function(handler) {
delete handlers[id][handler.i]
element.removeEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))
})
})
}

就是调用removeEventListener

on:添加事件

on是zepto中最核心的事件方法,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
$.fn.on = function(event, selector, data, callback, one){
var autoRemove, delegator, $this = this
if (event && !isString(event)) {
$.each(event, function(type, fn){
$this.on(type, selector, data, fn, one)
})
return $this
}

if (!isString(selector) && !isFunction(callback) && callback !== false)
callback = data, data = selector, selector = undefined
if (callback === undefined || data === false)
callback = data, data = undefined

if (callback === false) callback = returnFalse

return $this.each(function(_, element){
//如果是有one=true,先删掉事件,再执行事件
if (one) autoRemove = function(e){
remove(element, e.type, callback)
return callback.apply(this, arguments)
}
//按照选择器找到元素
if (selector) delegator = function(e){
var evt, match = $(e.target).closest(selector, element).get(0)
if (match && match !== element) {
evt = $.extend(createProxy(e), {currentTarget: match, liveFired: element})
return (autoRemove || callback).apply(match, [evt].concat(slice.call(arguments, 1)))
}
}

add(element, event, callback, data, selector, delegator || autoRemove)
})
}

参数说明:

  • event:事件类型,可以通过空格的字符串方式添加(“click mousedown”),或者事件类型为键,函数为值的方式({click:function,mousedown:function})。
  • selector:可选参数,事件委托的节点选择器
  • data:事件处理程序中的event.data属性
  • callback:事件处理程序的回调函数
  • one:绑定事件后,只触发一次回调

使用方式:

1
2
3
4
5
6
7
8
9
var elem = $('#content')
// observe all clicks inside #content:
elem.on('click', function(e){ ... })
// observe clicks inside navigation links in #content
elem.on('click', 'nav a', function(e){ ... })
// all clicks inside links in the document
$(document).on('click', 'a', function(e){ ... })
// disable following any navigation link on the page
$(document).on('click', 'nav a', false)

很多其他api是内部通过调用on方法实现的,如:

1
2
3
4
5
6
7
8
9
10
11
12
//基于一组特定的根元素为所有选择器匹配的元素附加一个处理事件,匹配的元素可能现在或将来才创建。
$.fn.delegate = function(selector, event, callback){
return this.on(event, selector, callback)
}
//添加一个处理事件到元素,当第一次执行事件以后,该事件将自动解除绑定,保证处理函数在每个元素上最多执行一次。
$.fn.one = function(event, selector, data, callback){
return this.on(event, selector, data, callback, 1)
}
//为一个元素绑定一个处理事件。
$.fn.bind = function(event, data, callback){
return this.on(event, data, callback)
}

off:移除事件

off模块也是主要的核心模块,主要做移除事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$.fn.off = function(event, selector, callback){
var $this = this
if (event && !isString(event)) {
$.each(event, function(type, fn){
$this.off(type, selector, fn)
})
return $this
}

if (!isString(selector) && !isFunction(callback) && callback !== false)
callback = selector, selector = undefined

if (callback === false) callback = returnFalse

return $this.each(function(){
remove(this, event, callback, selector)
})
}

一些api是内部通过调用off方法实现的,如:

1
2
3
4
5
6
7
8
//移除Bind绑定的事件
$.fn.unbind = function (event, callback) {
return this.off(event, callback)
}
//移除通过delegate 注册的事件。
$.fn.undelegate = function (selector, event, callback) {
return this.off(event, selector, callback)
}

trigge与triggerHandler

在对象集合的元素上触发指定的事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
$.fn.trigger = function (event, args) {
event = (isString(event) || $.isPlainObject(event))
? $.Event(event)
: compatible(event)
event._args = args
return this.each(function () {
// handle focus(), blur() by calling them directly
if (event.type in focus && typeof this[event.type] == "function")
this[event.type]()
// items in the collection might not be DOM elements
else if ('dispatchEvent' in this)
this.dispatchEvent(event)
else
$(this).triggerHandler(event, args)
})
}
$.fn.triggerHandler = function (event, args) {
var e,
result
this.each(function (i, element) {
e = createProxy(isString(event)
? $.Event(event)
: event)
e._args = args
e.target = element
$.each(findHandlers(element, event.type || event), function (i, handler) {
result = handler.proxy(e)
if (e.isImmediatePropagationStopped())
return false
})
})
return result
}

triggle模拟整个冒泡过程,除了自身,还触发祖先节点与window的同类型的回调,在游览器底层上,使用的方法是dispatchEvent,如果是在ie上是fireEvent。

$.Event:自定义事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$.Event = function (type, props) {
if (!isString(type))
props = type,
type = props.type
var event = document.createEvent(specialEvents[type] || 'Events'),
bubbles = true
if (props)
for (var name in props)
(name == 'bubbles')
? (bubbles = !!props[name])
: (event[name] = props[name])
event.initEvent(type, bubbles, true)
return compatible(event)
}

底层采用的是游览器的createEvent和initEvent方法来模拟事件,从而实现自定义事件的功能。

一些补充

1.很多框架对于事件代理不采用捕获而采用冒泡的原因,是考虑兼容性

2.zepto因为自身主要做移动端的原因,所以对于低版本ie浏览器是不考虑的,我在上述中或多或少做了补充 3.zepto的很大一部分事件实际是在touch模块,这块主要是触摸事件,内容也比较多,打算留到下篇再写

模板引擎原理及部分实现

模板引擎,是前端MV*架构中view的重要组成部分,是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,用于网站的模板引擎就会生成一个标准的HTML文档。多数前端框架都用到了前端模板引擎。

前端模板引擎类型

前端模板引擎主要分三类:

  • string-based模板,基于字符串的parse和compile,如ejs、hbs;
  • dom-based模板,基于dom的link或compile,如angular、vue的模板;
  • 虚拟dom模板,基于v-dom和ast,如React的模板jsx。

字符串模板引擎

1.String-based模板原理

字符串模板引擎主要依赖一下这几个dom API:createElement,appendChild,innerHTML。 在这些api中,innerHTML有最佳的可读性与实用性,成为事实上的主要标准,虽然其他API可能在性能上更胜一筹,但原生js的字符串生成方案中,最常用的还是innerHTML。 构建过程如下: 1.把整个文档作为字符串输入。 2.通过一个带正则的函数,将模板按照标记分为js表达式、模板语法、正常HTML语法。 3.合并成一个js表达式,这个可以接受数据作为输入。 4.输入数据后,输出字符串。 5.该字符串即可拼接为html代码。

2.String-based模板实现demo

大概实现下,约定一个语法,以经典的双大括号{ {}}作为模板插值。模板如下:

<div id="app"></div>
<script type="text/tpl" id="template">
    <p>name: { {name}}</p>
    <p>age: { {age}}</p>
</script>

插入的数据:

var info = [
    {
        name: 'bugzhang',
        age: 22
    }, {
        name: 'justzht',
        age: 20
    }, {
        name: 'zp',
        age: 20
    }
];

最重要的模板解析:

//解析模板
function template(tpl, data) {
    //定义解析模式
    var re = /{ {(.+?)}}/g,
        cursor = 0
    reExp = /(^( )?(var|if|for|else|switch|case|break|{|}|;))(.*)?/g,
        code = 'var r=[];\n';

    // 解析html
    function parsehtml(line) {
        // 单双引号转义,换行符替换为空格,去掉前后的空格
        line = line.replace(/('|")/g, '\\$1').replace(/\n/g, ' ').replace(/(^\s+)|(\s+$)/g, "");
        code += 'r.push("' + line + '");\n';
    }

// 解析js代码
    function parsejs(line) {
        // 去掉前后的空格
        line = line.replace(/(^\s+)|(\s+$)/g, "");
        code += line.match(reExp) ? line + '\n' : 'r.push(' + line + ');\n';
    }

    while ((match = re.exec(tpl)) !== null) {
        // 开始标签  { { 前的内容和结束标签 }} 后的内容
        parsehtml(tpl.slice(cursor, match.index))
        // 开始标签  { { 和 结束标签 }} 之间的内容
        parsejs(match[1])
        // 每一次匹配完成移动指针
        cursor = match.index + match[0].length;
    }
    // 最后一次匹配完的内容
    parsehtml(tpl.substr(cursor, tpl.length - cursor));
    code += 'return r.join("");';
    return new Function(code.replace(/[\r\t\n]/g, '')).apply(data);
}
//把生成的字符串插入app节点
var tpl = document.getElementById("app").innerHTML.toString();
document.getElementById("content").innerHTML = template(tpl, info);

3.String-based模板优点与缺点

优点主要有:

  • 快速的初始化时间
  • 同时适用于服务器端与客户端,对SSR有最好的支持度
  • 语法支持好

缺点也很明显:

  • 存在安全隐患
  • 性能较低下
  • 渲染后与数据断开联系

dom模板引擎

1.原理概述

dom-based模板引擎,输出的直接是dom,很多著名框架的模板都是采用了dom-based模板,如angular.js,vue.js,avalon.js,regular.js。 构建过程如下: 1.从字符串中生成不带数据的无状态模板; 2.从无状态模板编译成动态模板; 3.动态模板与model进行绑定,完成插值的功能。

2.优缺点

优点主要有:

  • 与数据绑定,可以不需要操作dom更改view
  • 运行高效
  • 指令带来的声明式开发

缺点:

  • 安全问题
  • 信息冗余度高
  • 初次进入dom的内容不是最终想要的内容

V-dom模板

目前了解不多,而且v-dom内容比较多,以后再单独写这块。 参考:https://segmentfault.com/a/1190000004420078,http://blog.csdn.net/yczz/article/details/49585381

Zepto源码分析——Zepto选择器

选择器引擎是框架中实现操作的主要方式,可以快速选取到所需元素,通过我们更加熟知的css选择器,zepto的选择器分两大块,一块在zepto.js中的核心选择器qsa方法,另外是selector.js文件,里边封装有一些扩充选择器的实现。

选择器引擎概述

CSS选择符是一条CSS样式中最左边的部分,选择符分为了五大类:元素、关系、伪类、并联、伪元素。其中只有伪元素选择器不能直接被js所选取到。 一般认为,框架的选择器引擎需要包括以下几个基本方法:

  • contain(a,b):判断a中是否包含b,主要用作优化
  • visible()与hidden():判断是否可见
  • selected():选中了元素
  • sortNode():节点的排序与去重,主要为了使更类似于原生方法的排序
  • filter():过滤器,对于不支持querySelectorAll的游览器,需要对用户的api进行过滤,这个步骤类似词法分析,可以拆分出有用的选择符,对其使用应该的API

不过zepto比较奇葩的一点是,zepto主要面向移动端,移动端游览器坑少,所以zepto就直接拿querySelectorAll来匹配非规定外的元素了,好处自不必说,代码少实现简单。坏处就是兼容性不好,也学不到真正选择器引擎的核心,但zepto的优化做的还是很好,也是值得看。而且querySelectorAll这也是未来的趋势。

Zepto核心选择器

首先是选择器的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
zepto.qsa = function (element, selector) {
var found,
maybeID = selector[0] == '#',
maybeClass = !maybeID && selector[0] == '.',
nameOnly = maybeID || maybeClass ? selector.slice(1) : selector, // Ensure that a 1 char tag name still gets checked
isSimple = simpleSelectorRE.test(nameOnly)
return (element.getElementById && isSimple && maybeID) ? // Safari DocumentFragment doesn't have getElementById
((found = element.getElementById(nameOnly)) ? [found] : []) :
(element.nodeType !== 1 && element.nodeType !== 9 && element.nodeType !== 11) ? [] :
slice.call(
isSimple && !maybeID && element.getElementsByClassName ? // DocumentFragment doesn't have getElementsByClassName/TagName
maybeClass ? element.getElementsByClassName(nameOnly) : // If it's simple, it could be a class
element.getElementsByTagName(selector) : // Or a tag
element.querySelectorAll(selector) // Or it's not simple, and we need to query all
)
}

判断了选择器类型,是id、class、标签还是复杂选择器,然后分别调用getElementById()、getElementsByClassName()、getElementsByTagName()、querySelectorAll()。 其次是否匹配选择器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
zepto.matches = function (element, selector) {
if (!selector || !element || element.nodeType !== 1) return false
var matchesSelector = element.matches || element.webkitMatchesSelector ||
element.mozMatchesSelector || element.oMatchesSelector ||
element.matchesSelector
if (matchesSelector) return matchesSelector.call(element, selector)
// fall back to performing a selector:
var match, parent = element.parentNode,
temp = !parent
if (temp)(parent = tempParent).appendChild(element)
match = ~zepto.qsa(parent, selector).indexOf(element)
temp &amp;&amp; tempParent.removeChild(element)
return match
}

主要是用来判断当前DOM节点否能完全匹配对应的CSS选择器规则。这个matches方法可以在事件委托等地方被用得上,用来判定匹配到当前标签的元素,当匹配到后添加事件。但原生的兼容性很差,因此框架在这里做了兼容性处理。

Zepto拓展选择器

把全部文件站过来,将源码解读写到注释中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
(function ($) {
var zepto = $.zepto,
oldQsa = zepto.qsa,
oldMatches = zepto.matches
function visible(elem) {
elem = $(elem)
return !!(elem.width() || elem.height()) &amp;&amp; elem.css(&quot;display&quot;) !== &quot;none&quot;
}
//这是一套过滤器系统
var filters = $.expr[':'] = {
visible: function () {
if (visible(this))
return this
},
hidden: function () {
if (!visible(this))
return this
},
selected: function () {
if (this.selected)
return this
},
checked: function () {
if (this.checked)
return this
},
parent: function () {
return this.parentNode
},
first: function (idx) {
if (idx === 0)
return this
},
last: function (idx, nodes) {
if (idx === nodes.length - 1)
return this
},
eq: function (idx, _, value) {
if (idx === value)
return this
},
contains: function (idx, _, text) {
if ($(this).text().indexOf(text) &gt; -1)
return this
},
has: function (idx, _, sel) {
if (zepto.qsa(this, sel).length)
return this
}
}

var filterRe = new RegExp('(.*):(\\w+)(?:\\(([^)]+)\\))?$\\s*'),
childRe = /^\s*&gt;/,
classTag = 'Zepto' + (+ new Date())
//分解选择器为三部分,选择器、选择器的过滤器方法、参数
function process(sel, fn) {
// quote the hash in `a[href^=#]` expression
sel = sel.replace(/=#\]/g, '=&quot;#&quot;]')
var filter,
arg,
match = filterRe.exec(sel)
if (match &amp;&amp; match[2] in filters) {
filter = filters[match[2]],
arg = match[3]
sel = match[1]
if (arg) {
var num = Number(arg)
if (isNaN(num))
arg = arg.replace(/^[&quot;']|[&quot;']$/g, '')
else
arg = num
}
}
return fn(sel, filter, arg)
}

zepto.qsa = function (node, selector) {
return process(selector, function (sel, filter, arg) {
try {
var taggedParent
if (!sel &amp;&amp; filter)
sel = '*'
else if (childRe.test(sel))
// support &quot;&gt; *&quot; child queries by tagging the parent node with a unique class
// and prepending that classname onto the selector
taggedParent = $(node).addClass(classTag),
sel = '.' + classTag + ' ' + sel

var nodes = oldQsa(node, sel)
} catch (e) {
console.error('error performing selector: %o', selector)
throw e
} finally {
if (taggedParent)
taggedParent.removeClass(classTag)
}
return !filter
? nodes
: zepto.uniq($.map(nodes, function (n, i) {
return filter.call(n, i, nodes, arg)
}))
})
}

zepto.matches = function (node, selector) {
return process(selector, function (sel, filter, arg) {
return (!sel || oldMatches(node, sel)) &amp;&amp; (!filter || filter.call(node, null, arg) === node)
})
}
})(Zepto)

从源码看出,虽然zepto主要采用的是getElementById()、getElementsByClassName()、getElementsByTagName()、querySelectorAll()这些内置的api完成,思路是分解->匹配->调用原生api->组装zepto对象。 根据不同的情况,给出了不同的提速方案,getElementById是最优先的,因为该api内部做了缓存而且只返回一个节点;getElementsByClassName()、getElementsByTagName()也是比较快的,返回多个节点并且又缓存;只有无法完成时候才进行querySelectorAll()。此外注意的是,getElementsByClassName()、getElementsByTagName()返回的是一个NodeList对象,而querySelectorAll返回的是一个StaticNodeList对象,前者动态后者静态,前者每次匹配到的都是同是缓存引用,后者返回的是不同的Object对象,数据表明前者的速度要快百分之90%以上,这就是为什么尽量用getElementsByClassName()、getElementsByTagName()的原因。

Zepto源码分析——Zepto核心

前言

一直计划想写一写某个框架的源码分析,之前读过jQuery的,但感觉jQuery太大,同时网上研究也已经很多而且很全了,所以选取迷你版jQuery之称的Zepto作为分析对象,边读边写,同时司徒正美大神的《JavaScript框架设计》这本书的对js框架的讲解,来写写自己理解。

目录结构

源码下载地址:https://github.com/madrobby/zepto ,源码在src目录下,目录结构如下:

├── src
   ├── ajax.js
   ├── amd_layout.js
   ├── assets.js
   ├── callbacks.js
   ├── data.js
   ├── deferred.js
   ├── detect.js
   ├── event.js
   ├── form.js
   ├── fx.js
   ├── fx_methods.js
   ├── gesture.js
   ├── ie.js
   ├── ios3.js
   ├── selector.js
   ├── stack.js
   ├── touch.js
   └── zepto.js

命名空间

1
2
3
4
5
6
var Zepto = (function() {
return $
})()

window.Zepto = Zepto
window.$ === undefined && (window.$ = Zepto)

Zepto借鉴了类似jQuery的挂载全局变量的方式实现,核心是返回$传递给Zepto。然后把Zepto和$作为window的属性。这样不会发现变量冲突,即使冲突只需要修改这两个变量名。 此外补充下,因为很多框架都采用了$作为标配,因此经常会出现$这个变量名冲突的问题,不过一般情况下Zepto是不会和jQuery冲突(这两个只选取一个就足够了),jQuery中的解决方案是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//先把可能存在的变量名存放
var _jQuery = window.jQuery,
_$ = window.$;

//没冲突时候放进去
jQuery.extend({
noConflict:function(deep){
window.$ = _$;
if(deep){
window.jQuery = _jQuery;
}
return jQuery;
}
});

类型判定

这段代码是相对比较简单的,就不再复习一遍JavaScript的类型了。直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function type(obj) {
return obj == null ? String(obj) :
class2type[toString.call(obj)] || "object"
}

function isFunction(value) {
return type(value) == "function"
}

function isWindow(obj) {
return obj != null && obj == obj.window
}

function isDocument(obj) {
return obj != null && obj.nodeType == obj.DOCUMENT_NODE
}

function isObject(obj) {
return type(obj) == "object"
}

function isPlainObject(obj) {
return isObject(obj) && !isWindow(obj) && Object.getPrototypeOf(obj) == Object.prototype
}

function likeArray(obj) {
var length = !!obj && 'length' in obj && obj.length,
type = $.type(obj)

return 'function' != type && !isWindow(obj) && (
'array' == type || length === 0 ||
(typeof length == 'number' && length > 0 && (length - 1) in obj)
)
}

JavaScript的类型判断有一些坑,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//type的坑
typeof null // 'object'
typeof document.childNodes //safari 'function'
typeof document.createElement('embed') //firefox3-10 'function'
typeof document.createElement('object') //firefox3-10 'function'
typeof document.createElement('applet') //firefox3-10 'function'
typeof /\d/i // 'function'
typeof window.alert //ie6-8 'object'
//跨文档比较原型,会不一致
var iframe = document.createElement('iframe');
document.body.appendChild(iframe);
xArray = window.frames[window.frames.length - 1].Array;
var arr = new xArray[1,2,3];
arr instanceof Array; //false
arr.constructor === Array; //false
//旧版本AE中DOM和BOM对象的constructor不存在
window.onload = function(){
alert(window.constructor); //ie67 undefined
alert(document.constructor); //ie67 undefined
alert(document.body.constructor); //ie67 undefined
alert((new ActiveXObject('Microsoft.XMLHTTP')).constructor); //ie6-9 undefined
}
//isNaN会把字符串、数组放回去返回true
isNaN('aaa') //true

框架入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
zepto.init = function(selector, context) {
var dom
// 如果没有选择器,返回一个空Zepto对象
if (!selector) return zepto.Z()
// 如果是字符串类型的选择器
else if (typeof selector == 'string') {
// 去除收尾空白符
selector = selector.trim()
// 如果传入的字符串是以<开头且符合HTML代码规则,创建元素
if (selector[0] == '<' && fragmentRE.test(selector))
dom = zepto.fragment(selector, RegExp.$1, context), selector = null
// 如果不是规范但又有内容,在指定位置查找
else if (context !== undefined) return $(context).find(selector)
// 如果是CSS选择器,调用CSS选择器查找
else dom = zepto.qsa(document, selector)
}
// 如果是函数,当dom创建时调用
else if (isFunction(selector)) return $(document).ready(selector)
// 如果是一个Zepto对象,直接返回这个对象
else if (zepto.isZ(selector)) return selector
else {
// 如果是数组,转成类数组对象
if (isArray(selector)) dom = compact(selector)
// 如果是一个对象,将其包括到一个数组中
else if (isObject(selector))
dom = [selector], selector = null
// 如果是HTML片段,以此创建节点
else if (fragmentRE.test(selector))
dom = zepto.fragment(selector.trim(), RegExp.$1, context), selector = null
// 如果有context参数,在context上创建
else if (context !== undefined) return $(context).find(selector)
// 如果是CSS选择器,调用CSS选择器查找
else dom = zepto.qsa(document, selector)
}
// 从查找到的节点创建一个Zepto对象
return zepto.Z(dom, selector)
}

一个基本概念就是Zepto对象,Zepto是一个类数组对象,它具有链式方法来操作它指向的DOM节点,除了$(Zepto)对象上的直接方法外(如$.extend),文档对象中的所有方法都是集合方法。 调用Zepto框架的方法,都是作用于Zepto()对象的,init函数所做的工作就是讲正常的DOM对象转换成具有Zepto方法的Zepto对象。

对象拓展

对象拓展是一种机制,用来把新功能添加到命名空间上。实质就是深浅拷贝。 javascript function extend(target, source, deep) { for (key in source) if (deep && (isPlainObject(source[key]) || isArray(source[key]))) { if (isPlainObject(source[key]) && !isPlainObject(target[key])) target[key] = {} if (isArray(source[key]) && !isArray(target[key])) target[key] = [] extend(target[key], source[key], deep) } else if (source[key] !== undefined) target[key] = source[key] }

深入理解Vitual DOM

Vitual DOM是被诸如React,Vue,Preact等框架采用的一种内部黑盒技术,用来作为到最终dom操作的一个中介。 v-dom在前端的工作图

v-dom是什么

v-dom是一个真实dom的模拟表示,它像是原始dom的一个轻量级的副本,不同的是v-dom减少了冗余,采用了高性能的diff算法比较更新dom,同时只通过最终的正式dom操作来渲染入游览器,这最终特性保证了v-dom的高效。

为什么要用v-dom

DOM操作是把js实现到游览器的核心,一切的交互都需要用过dom。不幸的是,don操作是一件性能非常底下的工作:因为dom对象中有很多和操作无关的冗余字段,而任何dom操作不得不遍历整颗冗余度非常高的dom树。 比如,目前有一个列表,包含多个项目,当检查第一项,dom将重建整个列表,这是十倍以上的非必须工作,在早起前端,因为业务的不复杂性,dom操作尚能工作顺利,但目前越来越复杂的前端场景,现代网站可以使用大量dom操作,低效更新已经是一个严重的问题。 真实的游览器渲染过程,元素的更新会涉及到重绘和回流,进一步降低性能。 这种缓慢的现实,使得很多js框架采用自己的方式去更新dom,v-dom就是其中一个被推广的佼佼者。

v-dom如何被建立

Preact的dom工作流程。

虚拟元素

我们可以用如下的虚拟元素替代真实dom:

/**
 * 一个类似的dom结构元素的v-dom设计
 * <div id="container">
 *  <h1>Hello v-dom</h1>
 * </div>
 */

var element = {
    tagName: 'div',
    attr: {
        props: {
            id: 'container'
        },
        style: {
            color: 'black'
        }
    },
    children: {
        tagName: 'h1',
        value: 'Hello v-dom'
    }
}

//用构造函数模拟一下
function Element(tagName, attr, children, value) {
    this.tagName = tagName;
    this.attr = attr;
    this.children = children;
    this.value = value;
};
var headline = new Element('h1', null, null, 'Hello world');
var container = new Element('div', {
    props: {
        id: 'container'
    },
    style: {
        color: 'black'
    }
}, headline, null);

上述代码就是一个对v-dom的描述以及简单实现,实际上React的JSX转换后真正调用的API也是类似的API,这个API是React.createElement()。 v-dom有一些通用的特点:轻量级、无状态、不可改变。

render方法

有了上述的v-dom对象后,再通过类似render()的方法,就可以创建真实dom,render函数的简单实现类似这样:

function render(element, root) {
    var realDOM = document.createElement(element.tagName);
    //循环设置属性和样式
    var props = element.attr.props;
    var styles = element.attr.style;
    for (var i in props) {
        realDOM.setAttrbute(i, props[i]);
    }
    for (var j in styles) {
        realDOM.style[j] = styles[j];
    }
    //循环子节点,如果是对象递归该方法,否则创建文本节点
    element.children.forEach(function (child) {
        if (child instanceof Element) {
            render(child, realDOM);
} else {
            raedlDOM.appendChild(document.createTextNode(child));
        }
    });
    //插入真实dom
    root.appendChild(realDOM);
    return realDOM;
}

diff算法比较差异

当Virtual DOM发生更新时候,会进行变化生成一颗新的dom树,为了比较两棵树的异同,引入了一种Diff算法完成比较,diff算法非常高效,当对v-dom完成差异比较后,这个差异会作用到真实dom,过程如下:

  • 1.构建Virtual Dom树
  • 2.将Virtual Dom插入真实dom
  • 3.构建变化后的Virtual Dom树
  • 4.通过diff算法比较差异
  • 5.仅将差异在真实DOM中更新

给定任意两颗树,进行转换的差异算法,一般复杂度是O(n^3),react的diff算法复杂度是O(n),它基于v-dom的两个基本事实:

  • 两个相同组件产生类似的DOM结构,不同的组件产生不同的DOM结构;
  • 对于同一层次的一组子节点,它们可以通过唯一的id进行区分。

组件的树是自带层级的,diff算法按照层级比较,如图: 如果节点类型不同,直接删除Before中的节点,插入新节点;如果节点类型相同,继续层序遍历属性,属性不同则替换属性;直到遍历到最底层。通过一次遍历,即可比较出不同,更新了整个dom。

//一个参考算法,链接:https://www.zhihu.com/question/29504639/answer/73607810
// diff 函数,对比两棵树
function diff (oldTree, newTree) {
  var index = 0 // 当前节点的标志
  var patches = {} // 用来记录每个节点差异的对象
  dfsWalk(oldTree, newTree, index, patches)
  return patches
}

// 对两棵树进行深度优先遍历
function dfsWalk (oldNode, newNode, index, patches) {
  // 对比oldNode和newNode的不同,记录下来
  patches[index] = [...]

  diffChildren(oldNode.children, newNode.children, index, patches)
}

// 遍历子节点
function diffChildren (oldChildren, newChildren, index, patches) {
  var leftNode = null
  var currentNodeIndex = index
  oldChildren.forEach(function (child, i) {
    var newChild = newChildren[i]
    currentNodeIndex = (leftNode && leftNode.count) // 计算节点的标识
      ? currentNodeIndex + leftNode.count + 1
      : currentNodeIndex + 1
    dfsWalk(child, newChild, currentNodeIndex, patches) // 深度遍历子节点
    leftNode = child
  })
}

一篇很好的diff算法原理的解析:http://www.infoq.com/cn/articles/react-dom-diff/

生命周期问题

v-dom的整个过程,即产生了生命周期,一切和v-dom相关的组件的生命周期也与之相关。如: vue组件的生命周期 react组件的生命周期 一般分为初始化、运行中、销毁三个状态,三个状态有关的生命周期钩子函数有: 初始化阶段:   getDefaultProps:获取实例的默认属性(即使没有生成实例,组件的第一个实例被初始化CreateClass的时候调用,只调用一次,)   getInitialState:获取每个实例的初始化状态(每个实例自己维护)   componentWillMount:组件即将被装载、渲染到页面上(render之前最好一次修改状态的机会)   render:组件在这里生成虚拟的DOM节点(只能访问this.props和this.state;只有一个顶层组件,也就是说render返回值值职能是一个组件;不允许修改状态和DOM输出)   componentDidMount:组件真正在被装载之后,可以修改DOM 运行中状态:   componentWillReceiveProps:组件将要接收到属性的时候调用(赶在父组件修改真正发生之前,可以修改属性和状态)   shouldComponentUpdate:组件接受到新属性或者新状态的时候(可以返回false,接收数据后不更新,阻止render调用,后面的函数不会被继续执行了)   componentWillUpdate:不能修改属性和状态   render:只能访问this.props和this.state;只有一个顶层组件,也就是说render返回值只能是一个组件;不允许修改状态和DOM输出   componentDidUpdate:可以修改DOM 销毁阶段:   componentWillUnmount:开发者需要来销毁(组件真正删除之前调用,比如计时器和事件监听器)