漫漫技术路

  • 首页

  • 标签

  • 分类

  • 归档

  • 搜索

一步一步DIY zepto库,研究zepto源码5-- callbacks

发表于 2016-12-05 | 更新于 2018-11-30 | 分类于 前端技术

Callbacks API用来管理回调函数,也作为deferred延迟对象的基础部分,今天就一起来探寻它的源码(对应src下的callbacks.js)。

代码挂在我的github上,对应文件夹v0.5.1。
https://github.com/zrysmt/DIY-zepto

注:要在github源代码中自己编译的话,要在基础包命令:npm run dist上要进行扩展了,输入命令:

1
2
3
4
MODULES="zepto event ajax  callbacks" npm run dist
# on Windows
> SET MODULES=zepto event ajax callbacks
> npm run dist

1.示例Demo

1
2
3
4
5
6
var foo = function(value) {
console.log('foo:' + value);
};
var bar = function(value) {
console.log('bar:' + value);
};

示例1:

1
2
3
4
5
6
7
8
var callbacks = $.Callbacks();
callbacks.add(foo);
callbacks.fire(['hello', 'wo', '123']);
callbacks.add(bar);
callbacks.fire('中');
/*foo:hello,wo,123
foo:中
bar:中*/

标记:

  • once: 回调只能触发一次
  • memory 记录上一次触发回调函数列表时的参数,之后添加的函数都用这参数立即执行
  • unique 一个回调函数只能被添加一次
  • stopOnFalse 当某个回调函数返回false时中断执行

示例2:

1
2
3
4
5
6
7
8
var callbacks = $.Callbacks({
memory: true
});
callbacks.add(foo);
callbacks.fire(['hello', 'wo', '123']);
callbacks.add(bar);
/*foo:hello,wo,123
bar:hello,wo,123*/

示例3:

1
2
3
4
5
6
7
8
9
10
var callbacks = $.Callbacks({
memory: true,
once: true //只能执行一次
});
callbacks.add(foo);
callbacks.fire(['hello', 'wo', '123']);
callbacks.add(bar);
callbacks.fire(['hello', 'wo', '123']);
/*foo:hello,wo,123
bar:hello,wo,123*/

2.整体结构

1
2
3
4
5
6
7
var Callbacks = function($) {
$.Callbacks = function(options) {
Callbacks = {/*add remove has empty等方法*/};
return Callbacks;
};
};
export default Callbacks;

3.源码

当然你也可以结合下一部分的过程分析,来理解源码。

3.1 几个重要变量

1
2
3
4
5
6
7
8
9
10
options = $.extend({}, options);

var memory, // Last fire value (for non-forgettable lists)
fired, // Flag to know if list was already fired //是否回调过
firing, // Flag to know if list is currently firing //回调函数列表是否正在执行中
firingStart, // First callback to fire (used internally by add and fireWith) //第一回调函数的下标
firingLength, // End of the loop when firing //回调函数列表长度?
firingIndex, // Index of currently firing callback (modified by remove if needed)
list = [], // Actual callback list //回调数据源: 回调列表
stack = !options.once && [], // Stack of fire calls for repeatable lists//回调只能触发一次的时候,stack永远为false

3.2 fire函数– 回调底层函数

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
/**
* 触发 回调底层函数
*/
fire = function(data) {
memory = options.memory && data;
fired = true;
firingIndex = firingStart || 0;
firingStart = 0;
firingLength = list.length;
firing = true; //正在回调
//遍历回调列表,全部回调函数都执行,参数是传递过来的data
for (; list && firingIndex < firingLength; ++firingIndex) {
//如果 list[ firingIndex ] 为false,且stopOnFalse(中断)模式
//list[firingIndex].apply(data[0], data[1]) 这是执行回调
//data经过封装,[context,arg] 第一个参数为上下文
if (list[firingIndex].apply(data[0], data[1]) === false && options.stopOnFalse) {
memory = false; //中断回掉执行
break;
}
}
firing = false; //回调执行完毕
if (list) {
//stack里还缓存有未执行的回调
if (stack) { //options.once存在的时候,不执行下面的一行
stack.length && fire(stack.shift()); //执行stack里的回调
} else if (memory) {
list.length = 0; //memory 清空回调列表
} else {
Callbacks.disable(); //其他情况如 once 禁用回调
}
}
}

3.3 Callbacks对象

回调函数管理:添加add() 移除remove()、触发fire()、锁定lock()、禁用disable()回调函数。它为Deferred异步队列提供支持。

原理:通过一个数组保存回调函数,其他方法围绕此数组进行检测和操作

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
Callbacks = {
add: function() {
if (list) {
var start = list.length,
add = function(args) {
$.each(args, function(_, arg) {//$.each处理数组
if (typeof arg === "function") {
//非unique,或者是unique,但回调列表未添加过
if (!options.unique || !Callbacks.has(arg)) list.push(arg);
} else if (arg && arg.length && typeof arg !== 'string') {
//是数组/伪数组,添加,重新遍历
add(arg);
}
});
};
add(arguments); //添加进列表
if (firing) { //如果列表正在执行中,修正长度,使得新添加的回调也可以执行
firingLength = list.length;
} else if (memory) {
firingStart = start; //memory 模式下,修正开始下标
fire(memory); //立即执行所有回调
}
}
return this;
},
//从回调列表里删除一个或一组回调函数
remove: function() {
if (list) { //回调列表存在才可以删除
$.each(arguments, function(_, arg) {
var index;
while ((index = $.inArray(arg, list, index)) > -1) {
list.splice(index, 1); //执行删除
// Handle firing indexes
if (firing) {
//避免回调列表溢出
if (index <= firingLength) --firingLength; //在正执行的回调函数后,递减结尾下标
if (index <= firingIndex) --firingIndex; //在正执行的回调函数前,递减开始下标
}
}
});
}
return this;
},
//检查指定的回调函数是否在回调列表中

has: function(fn) {
return !!(list && (fn ? $.inArray(fn, list) > -1 : list.length));
},
//清空回调函数
empty: function() {
firingLength = list.length = 0;
return this;
},
//禁用回掉函数
disable: function() {
list = stack = memory = undefined;
return this;
},
//是否禁用了回调函数
disabled: function() {
return !list;
},
//锁定回调函数
lock: function() {
stack = undefined;
//非memory模式下,禁用列表
if (!memory) Callbacks.disable();
return this;
},
//是否是锁定的
locked: function() {
return !stack;
},
//用上下文、参数执行列表中的所有回调函数
fireWith: function(context, args) {
// 未回调过,非锁定、禁用时
if (list && (!fired || stack)) {
args = args || [];
args = [context, args.slice ? args.slice() : args];
if (firing) {
stack.push(args); //正在回调中,存入stack
} else {
fire(args); //否则立即回调,外层fire函数
}
}
return this;
},
fire: function() {
//执行回调
return Callbacks.fireWith(this, arguments);
},
//回调列表是否被回调过
fired: function() {
return !!fired;
}
};

4.过程分析

对于第一部分的示例:

1
2
3
var callbacks = $.Callbacks();
callbacks.add(foo);
callbacks.fire(['hello', 'wo', '123']);

  • add方法(CallBacks.add方法)
    • add函数(add(arguments))
      $.each可以处理参数是数组的形式
      • 非数组 直接存放到list中list.push(arg);
      • 数组 递归 add(arg)
    • 如果memory存在,为ture,离开执行 (fire(memory))
    • 返回this,可以链式调用
  • fire方法(CallBacks.fire方法)
    • Callbacks.fireWith(this, arguments)
      • 未回调过,非锁定、禁用时
        • 处理参数,包装成数组`args = [context, args.slice ? args.slice() : args]
        • 如果正在执行回调(firing = true) stack.push(args);//正在回调中,存入stack
        • 否则 调用外层fire函数,立即执行。
  • 外层fire函数

    • 参数处理

      1
      2
      3
      4
      5
      6
      memory = options.memory && data;
      fired = true;
      firingIndex = firingStart || 0;
      firingStart = 0;
      firingLength = list.length;
      firing = true; //正在回调
    • 遍历回调函数执行 for循环

      • 如果 list[ firingIndex ] 为false,且stopOnFalse(中断)模式
        立即中断memory = false; break;
      • 否则,立刻执行
        list[firingIndex].apply(data[0], data[1])
    • 处理结束 firing = false; //回调执行完毕
    • option.once为false,即stack为true
      stack.length && fire(stack.shift()); //执行stack里的回调
    • option.memory存在 则清空list【此时已经执行完结束了】
    • 其余情况,包括option.once为true,在遍历回调函数中已经执行过了,这里禁用回掉即可
      Callbacks.disable();

全部代码挂在我的github上,本博文对应文件夹v0.5.x。
https://github.com/zrysmt/DIY-zepto

参考阅读:

  • Zepto源码分析-callbacks模块

一步一步DIY zepto库,研究zepto源码4 -- ajax模块

发表于 2016-12-05 | 更新于 2018-11-30 | 分类于 前端技术

上面的博文介绍的都是源码src下的基础模块zepto.js文件和事件模块event.js,下面接着看另外一个独立的模块–ajax模块ajax.js

代码挂在我的github上,对应文件夹v0.4.1。
https://github.com/zrysmt/DIY-zepto

1.ajax的过程

  • 当global: true时。在Ajax请求生命周期内,以下这些事件将被触发。
  • ajaxStart (global):如果没有其他Ajax请求当前活跃将会被触发。
  • ajaxBeforeSend (data: xhr, options):再发送请求前,可以被取消。
  • ajaxSend (data: xhr, options):像 ajaxBeforeSend,但不能取消。
  • ajaxSuccess (data: xhr, options, data):当返回成功时。
  • ajaxError (data: xhr, options, error):当有错误时。
  • ajaxComplete (data: xhr, options):请求已经完成后,无论请求是成功或者失败。
  • ajaxStop (global):如果这是最后一个活跃着的Ajax请求,将会被触发。

下面我们就首先来看这些过程的源码:

  • 实现逻辑函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// trigger a custom event and return false if it was cancelled
function triggerAndReturn(context, eventName, data) {
var event = $.Event(eventName); //包装成事件
$(context).trigger(event, data); //触发
return !event.isDefaultPrevented();
}
// trigger an Ajax "global" event
function triggerGlobal(settings, context, eventName, data) {
if (settings.global) return triggerAndReturn(context || document, eventName, data);
}
// Number of active Ajax requests
// 发送中的ajax请求个数
$.active = 0;
//如果没有其他Ajax请求当前活跃将会被触发
  • ajaxStart
1
2
3
4
//如果没有其他Ajax请求当前活跃将会被触发
function ajaxStart(settings) {
if (settings.global && $.active++ === 0) triggerGlobal(settings, null, 'ajaxStart');
}
  • ajaxBeforeSend
1
2
3
4
5
6
7
8
9
10
11
// triggers an extra global event "ajaxBeforeSend" that's like "ajaxSend" but cancelable
// 触发选项中beforeSend回调函数和触发ajaxBeforeSend事件
// 上述的两步中的回调函数中返回false可以停止发送ajax请求,否则就触发ajaxSend事件
function ajaxBeforeSend(xhr, settings) {
var context = settings.context;
if (settings.beforeSend.call(context, xhr, settings) === false ||
triggerGlobal(settings, context, 'ajaxBeforeSend', [xhr, settings]) === false)
return false;

triggerGlobal(settings, context, 'ajaxSend', [xhr, settings]);
}
  • ajaxSuccess
1
2
3
4
5
6
7
8
function ajaxSuccess(data, xhr, settings, deferred) {
var context = settings.context,
status = 'success';
settings.success.call(context, data, status, xhr);
if (deferred) deferred.resolveWith(context, [data, status, xhr]);
triggerGlobal(settings, context, 'ajaxSuccess', [xhr, settings, data]);
ajaxComplete(status, xhr, settings);
}
  • ajaxError
1
2
3
4
5
6
7
8
// type: "timeout", "error", "abort", "parsererror"
function ajaxError(error, type, xhr, settings, deferred) {
var context = settings.context;
settings.error.call(context, xhr, type, error);
if (deferred) deferred.rejectWith(context, [xhr, type, error]);
triggerGlobal(settings, context, 'ajaxError', [xhr, settings, error || type]);
ajaxComplete(type, xhr, settings);
}
  • ajaxComplete
1
2
3
4
5
6
7
// status: "success", "notmodified", "error", "timeout", "abort", "parsererror"
function ajaxComplete(status, xhr, settings) {
var context = settings.context;
settings.complete.call(context, xhr, status);
triggerGlobal(settings, context, 'ajaxComplete', [xhr, settings]);
ajaxStop(settings);
}
  • ajaxStop
1
2
3
4
// 所有ajax请求都完成后才触发
function ajaxStop(settings) {
if (settings.global && !(--$.active)) triggerGlobal(settings, null, 'ajaxStop');
}

我们注意到,我们只是自定义了ajaxXXX事件,并没有实际的意义,这时候我们就需要将这些事件的逻辑实现。这部分逻辑放在了$.ajax中,在适当的时候触发事件,这些很值得我们去思考的它的巧妙。

2.$.ajax

2.1 全局变量,工具函数定义:

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
var jsonpID = +new Date(),
document = window.document,
key,
name,
rscript = /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, //用来除掉html代码中的<script>标签
scriptTypeRE = /^(?:text|application)\/javascript/i,
xmlTypeRE = /^(?:text|application)\/xml/i, //用来判断是不是js的mime
jsonType = 'application/json',
htmlType = 'text/html',
blankRE = /^\s*$/,
originAnchor = document.createElement('a');
originAnchor.href = window.location.href;

// Empty function, used as default callback
// 空函数,被用作默认的回调函数
function empty() {}
function mimeToDataType(mime) {
if (mime) mime = mime.split(';', 2)[0];
return mime && (mime == htmlType ? 'html' :
mime == jsonType ? 'json' :
scriptTypeRE.test(mime) ? 'script' :
xmlTypeRE.test(mime) && 'xml') || 'text';
}
//把参数添加到url上
function appendQuery(url, query) {
if (query == '') return url;
return (url + '&' + query).replace(/[&?]{1,2}/, '?');
//将&、&&、&?、?、?、&?&? 转化为 ?
}

2.2 $.ajaxSetting

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$.ajaxSettings = {
type: 'GET',
beforeSend: empty,
success: empty,
error: empty,
complete: empty,
context: null,
global: true,
xhr: function() {
return new window.XMLHttpRequest();
},
accepts: {
script: 'text/javascript, application/javascript, application/x-javascript',
json: jsonType,
xml: 'application/xml, text/xml',
html: htmlType,
text: 'text/plain'
},
crossDomain: false,
timeout: 0,
processData: true,
cache: true,
dataFilter: empty
};

2.3 序列化

1
2
3
4
5
6
7
8
9
10
11
// 列化data参数,并且如果是GET方法的话把参数添加到url参数上
function serializeData(options) {
//options.data是个对象
if (options.processData && options.data && $.type(options.data) != "string")
console.info(options.data);
options.data = $.param(options.data, options.traditional);

// 请求方法为GET,data参数添加到url上
if (options.data && (!options.type || options.type.toUpperCase() == 'GET' || 'jsonp' == options.dataType))
options.url = appendQuery(options.url, options.data), options.data = undefined;
}
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
var escape = encodeURIComponent;
//序列化
//在Ajax post请求中将用作提交的表单元素的值编译成 URL编码的 字符串
function serialize(params, obj, traditional, scope) {
var type, array = $.isArray(obj),
hash = $.isPlainObject(obj);
// debugger;

$.each(obj, function(key, value) {
type = $.type(value);

if (scope) {
key = traditional ? scope :
scope + '[' + (hash || type == 'object' || type == 'array' ? key : '') + ']';
}
// handle data in serializeArray() format
if (!scope && array) { //obj是个数组
params.add(value.name, value.value);
}
// obj的value是个数组/对象
else if (type == "array" || (!traditional && type == "object")) {
serialize(params, value, traditional, key);
} else {
params.add(key, value);
}
});
}
$.param = function(obj, traditional) {
var params = [];
//serialize函数使用add
params.add = function(key, value) {
if ($.isFunction(value)) value = value();
if (value == null) value = "";
this.push(escape(key) + '=' + escape(value));
};
serialize(params, obj, traditional); //处理obj
return params.join('&').replace(/%20/g, '+');
};

2.4 XMLHttpRequest解释

常用方法:

函数/属性 作用
setRequestHeader() 给指定的HTTP请求头赋值.在这之前,你必须确认已经调用 open() 方法打开了一个url
overrideMimeType() 重写由服务器返回的MIME type
onreadystatechange readyState属性改变时会调用它
open() 初始化一个请求
send() 发送请求. 如果该请求是异步模式(默认),该方法会立刻返回. 相反,如果请求是同步模式,则直到请求的响应完全接受以后,该方法才会返回

readyState的状态:

值 状态 描述
0 UNSENT(未打开) open()方法还未被调用.
1 OPENED (未发送) send()方法还未被调用.
2 HEADERS_RECEIVED (已获取响应头) send()方法已经被调用, 响应头和响应状态已经返回.
3 LOADING (正在下载响应体) 响应体下载中; responseText中已经获取了部分数据.
4 DONE (请求完成) 整个请求过程已经完毕.

2.5 $.ajax实现

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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
$.ajax = function(options) {
var settings = $.extend({}, options || {}),
deferred = $.Deferred && $.Deferred(),
urlAnchor, hashIndex;
for (key in $.ajaxSettings)
if (settings[key] === undefined) settings[key] = $.ajaxSettings[key];

ajaxStart(settings); //---------------@开始
// 如果没有传入crossDomain参数,就通过检测setting.url和网址的protocol、host是否一致判断该请求是否跨域
if (!settings.crossDomain) {
// 通过设置a元素的href就可以很方便的获取一个url的各组成部分
urlAnchor = document.createElement('a');
urlAnchor.href = settings.url;
// cleans up URL for .href (IE only), see https://github.com/madrobby/zepto/pull/1049
urlAnchor.href = urlAnchor.href;
settings.crossDomain = (originAnchor.protocol + '//' + originAnchor.host) !== (urlAnchor.protocol + '//' + urlAnchor.host);
}
// 没有传入url参数,使用网站的网址为url参数
// window.location.toString() 等于 window.location.href
if (!settings.url) settings.url = window.location.toString();
//去掉url上的hash部分
if ((hashIndex = settings.url.indexOf('#')) > -1) settings.url = settings.url.slice(0, hashIndex);
serializeData(settings); // 序列化data参数,并且如果是GET方法的话把参数添加到url参数上
var dataType = settings.dataType,
hasPlaceholder = /\?.+=\?/.test(settings.url); // 判断url参数是否包含=?
if (hasPlaceholder) dataType = 'jsonp'; //jsonp url 举例http://www.xxx.com/xx.php?callback=?

// 设置了cache参数为false,或者cache参数不为true而且请求数据的类型是script或jsonp,就在url上添加时间戳防止浏览器缓存
// (cache设置为true也不一定会缓存,具体要看缓存相关的http响应首部)
if (settings.cache === false || (
(!options || options.cache !== true) &&
('script' == dataType || 'jsonp' == dataType)
))
settings.url = appendQuery(settings.url, '_=' + Date.now());

// jsonp调用$.ajaxJSONP实现
if ('jsonp' == dataType) {
if (!hasPlaceholder)
settings.url = appendQuery(settings.url,
settings.jsonp ? (settings.jsonp + '=?') : settings.jsonp === false ? '' : 'callback=?');
return $.ajaxJSONP(settings, deferred);
}

// 下面代码用来设置请求的头部、相应的mime类型等
var mime = settings.accepts[dataType],
headers = {},
setHeader = function(name, value) { headers[name.toLowerCase()] = [name, value]; },
protocol = /^([\w-]+:)\/\//.test(settings.url) ? RegExp.$1 : window.location.protocol,
xhr = settings.xhr(), //XMLHttpRequest
nativeSetHeader = xhr.setRequestHeader,
abortTimeout;

if (deferred) deferred.promise(xhr);

//不跨域
if (!settings.crossDomain) setHeader('X-Requested-With', 'XMLHttpRequest');
setHeader('Accept', mime || '*/*');
if (mime = settings.mimeType || mime) {
if (mime.indexOf(',') > -1) mime = mime.split(',', 2)[0];
//重写由服务器返回的MIME type 注意,这个方法必须在send()之前被调用
xhr.overrideMimeType && xhr.overrideMimeType(mime);
}
//设置contentType
if (settings.contentType || (settings.contentType !== false && settings.data && settings.type.toUpperCase() != 'GET'))
setHeader('Content-Type', settings.contentType || 'application/x-www-form-urlencoded');
//如果配置中有对headers内容
if (settings.headers)
for (name in settings.headers) setHeader(name, settings.headers[name]);
xhr.setRequestHeader = setHeader; //设置头信息

xhr.onreadystatechange = function() {
if (xhr.readyState == 4) { //请求完成
xhr.onreadystatechange = empty;
clearTimeout(abortTimeout);
var result, error = false;
//请求成功
//在本地调动ajax,也就是请求url以file开头,也代表请求成功
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304 || (xhr.status == 0 && protocol == 'file:')) {
dataType = dataType || mimeToDataType(settings.mimeType || xhr.getResponseHeader('content-type'));

// 根据xhr.responseType和dataType处理返回的数据
if (xhr.responseType == 'arraybuffer' || xhr.responseType == 'blob') {
result = xhr.response;
} else {
result = xhr.responseText;

try {
// http://perfectionkills.com/global-eval-what-are-the-options/
// (1,eval)(result) 这样写还可以让result里面的代码在全局作用域里面运行
result = ajaxDataFilter(result, dataType, settings);
if (dataType == 'script') {
(1, eval)(result);
} else if (dataType == 'xml') {
result = xhr.responseXML;
} else if (dataType == 'json') {
result = blankRE.test(result) ? null : $.parseJSON(result);
}
} catch (e) { error = e; }

if (error) return ajaxError(error, 'parsererror', xhr, settings, deferred);//---------------@
}

ajaxSuccess(result, xhr, settings, deferred);//---------------@
} else {
ajaxError(xhr.statusText || null, xhr.status ? 'error' : 'abort', xhr, settings, deferred);//---------------@
}
}
};
//必须send()之前//---------------@
if (ajaxBeforeSend(xhr, settings) === false) {
xhr.abort();
ajaxError(null, 'abort', xhr, settings, deferred);
return xhr;
}

var async = 'async' in settings ? settings.async : true;
/**
* void open(
DOMString method,
DOMString url,
optional boolean async,
optional DOMString user,
optional DOMString password
);
*/
xhr.open(settings.type, settings.url, async, settings.username, settings.password);

if (settings.xhrFields)
for (name in settings.xhrFields) xhr[name] = settings.xhrFields[name];

for (name in headers) nativeSetHeader.apply(xhr, headers[name]);

// 超时丢弃请求
if (settings.timeout > 0) abortTimeout = setTimeout(function() {
xhr.onreadystatechange = empty;
xhr.abort();
ajaxError(null, 'timeout', xhr, settings, deferred);
}, settings.timeout);

// avoid sending empty string (#319)
xhr.send(settings.data ? settings.data : null);
return xhr;
};

2.6 示例Demo

ajax读取本地的json,安装个小服务器,或者使用其他服务器,如apache,tomcat等。

1
2
npm install anywhere -g      //安装
anywhere 8860 //开启端口(任意没被占用的)为服务器使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$.ajax({
type: 'GET',
url: '/projects.json',
data: {
name: 'Hello'
},
dataType: 'json',
timeout: 300,
success: function(data) {
console.log(data);
},
error: function(xhr, type) {
alert('Ajax error!');
}
})

3.jsonp

使用jsonp跨域的核心思想是:

1
2
3
script = document.createElement('script')
script.src = options.url.replace(/\?(.+)=\?/, '?$1=' + callbackName);
document.head.appendChild(script)

具体实现的代码:

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
$.ajaxJSONP = function(options, deferred) {
// 没有type选项,调用$.ajax实现
if (!('type' in options)) return $.ajax(options);

var _callbackName = options.jsonpCallback,
//options配置写了jsonpCallback,那么回调函数的名字就是options.jsonpCallback
//没有就是'Zepto' + (jsonpID++)
callbackName = ($.isFunction(_callbackName) ?
_callbackName() : _callbackName) || ('Zepto' + (jsonpID++)),
script = document.createElement('script'),
originalCallback = window[callbackName],
responseData,
abort = function(errorType) {
$(script).triggerHandler('error', errorType || 'abort');
},
xhr = { abort: abort },
abortTimeout;

if (deferred) deferred.promise(xhr);
// 加载成功或者失败触发相应的回调函数
// load error 在event.js
$(script).on('load error', function(e, errorType) {
clearTimeout(abortTimeout);
// 加载成功或者失败都会移除掉添加到页面的script标签和绑定的事件
$(script).off().remove();

if (e.type == 'error' || !responseData) { //失败
ajaxError(null, errorType || 'error', xhr, options, deferred);
} else { //成功
ajaxSuccess(responseData[0], xhr, options, deferred);
}

window[callbackName] = originalCallback;
if (responseData && $.isFunction(originalCallback))
originalCallback(responseData[0]);

originalCallback = responseData = undefined;
});
// 在beforeSend回调函数或者ajaxBeforeSend事件中返回了false,取消ajax请求
if (ajaxBeforeSend(xhr, options) === false) {
abort('abort');
return xhr;
}

window[callbackName] = function() {
responseData = arguments;
};
// 参数中添加上变量名
script.src = options.url.replace(/\?(.+)=\?/, '?$1=' + callbackName);
document.head.appendChild(script);
//超时处理
if (options.timeout > 0) abortTimeout = setTimeout(function() {
abort('timeout');
}, options.timeout);

return xhr;
};

4.get、post、getJSON、load 方法

篇幅所限,这里就不再单列出来了,他们都是处理好参数,调用$.ajax方法。详细的实现方式去我的github 下载。

全部代码挂在我的github上,本博文对应文件夹v0.4.x。
https://github.com/zrysmt/DIY-zepto

参考阅读:

  • Zepto ajax 模块 源码分析
  • XMLHttpRequest解释–MDN

一步一步DIY zepto库,研究zepto源码3 -- 事件模块

发表于 2016-12-05 | 更新于 2018-11-30 | 分类于 前端技术

上面的博文介绍的都是源码src下的zepto.js文件,接着我们来看看zepto的事件模块,对应文件是event.js

代码挂在我的github上,对应文件夹v0.3.2(只实现on),v0.3.3(完整实现)。
https://github.com/zrysmt/DIY-zepto

1.绑定事件

实例Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<div id="foo1">foo1</div>
<div id="foo2">foo2</div>
<div id="foo3">foo3
<div id="foo31">foo31</div>
</div>
<a href="demo1.html" class="my-a">demo1.html</a>
<script src="zepto.js"></script>
<script type="text/javascript">
var div1 = $('#foo1');
var div2 = $('#foo2');
$('body').on('click', '#foo1', function(event) {
console.log(event);
event.preventDefault();
alert("点击");
});
$('body').on('click', '#foo2', 'test',function(event) {
event.preventDefault();
alert("点击foo2"+event.data);//点击foo2test
});
$('#foo3').on('click', function(event) {
alert("点击");
});
$('body').on('click', '.my-a', false);//不跳转
</script>

1.1 handlers对象

handlers对象的数据格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
1: [ // handlers的值为DOM元素的_zid
{
del: function() {}, // 实现事件代理的函数
e: "click", // 事件名称
fn: function() {}, // 用户传入的回调函数
i: 0, // 该对象在数组里的下标
ns: "", // 事件的命名空间,只用使用$.fn.triggerHandler时可用,$.fn.trigger不能使用。
proxy: function(e) {}, // 真正绑定事件时的回调函数,里面判断调用del或者fn
sel: undefined // 要进行事件代理时传入的selector
}
]
}

1.2 全局变量

1
2
3
4
5
6
7
8
9
10
11
12
var _zid = 1, //用来生成标示元素和回调函数的id,每标示一个就+1
undefined,
handlers = {},
slice = Array.prototype.slice,
isFunction = $.isFunction,
isString = function(obj) {
return typeof obj == 'string';
},
specialEvents = {},
focusinSupported = 'onfocusin' in window,
focus = { focus: 'focusin', blur: 'focusout' },
hover = { mouseenter: 'mouseover', mouseleave: 'mouseout' };

1.3 添加三个方法:isDefaultPrevented、isDefaultPrevented和isPropagationStopped

  • 如果preventDefault()被该事件的实例调用,那么返回true。 这可作为跨平台的替代原生的defaultPrevented属性,如果defaultPrevented缺失或在某些浏览器下不可靠的时候
  • 如果stopImmediatePropagation()被该事件的实例调用,那么返回true。Zepto在不支持该原生方法的浏览器中实现它(例如老版本的Android)
  • 如果stopPropagation()被该事件的实例调用,那么返回true

通过改写原生的preventDefault、stopImmediatePropagation和stopPropagation方法实现新增三个方法

新增的三个方法:

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
var returnTrue = function() {
return true;
},
returnFalse = function() {
return false;
}, // 构建事件对象时所不要的几个属性:returnValue、layerX和layerY(还有以大写字母开头的属性?)
ignoreProperties = /^([A-Z]|returnValue$|layer[XY]$)/,
// 事件对象需要添加的三个方法名
eventMethods = {
preventDefault: 'isDefaultPrevented',
stopImmediatePropagation: 'isImmediatePropagationStopped',
stopPropagation: 'isPropagationStopped'
};
// 添加eventMethods里面的三个方法:isDefaultPrevented、isDefaultPrevented和isPropagationStopped
function compatible(event, source) {
if (source || !event.isDefaultPrevented) {
source || (source = event);
//遍历eventMethods对象,name是key,predicate是value
$.each(eventMethods, function(name, predicate) {
var sourceMethod = source[name];
event[name] = function() {
this[predicate] = returnTrue;
return sourceMethod && sourceMethod.apply(source, arguments);
}
event[predicate] = returnFalse;
})
try {
event.timeStamp || (event.timeStamp = Date.now())
} catch (ignored) {}
// 设置isDefaultPrevented默认指向的函数
// 如果有defaultPrevented属性,就根据defaultPrevented的值来判断
if (source.defaultPrevented !== undefined ? source.defaultPrevented :
'returnValue' in source ? source.returnValue === false :
//getPreventDefault和defaultPrevented属性类似,不过是非标准的。为了兼容没有defaultPrevented参数的浏览器
source.getPreventDefault && source.getPreventDefault())
event.isDefaultPrevented = returnTrue;
}
return event;
}

1.4 $.fn.on实现

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
/**调用形式:
*on(type, [selector], function(e){ ... })
*on(type, [selector], [data], function(e){ ... })
*on({ type: handler, type2: handler2, ... }, [selector])
*on({ type: handler, type2: handler2, ... }, [selector], [data])
*/
$.fn.on = function(event, selector, data, callback, one) {
var autoRemove, delegator, $this = this;
//event 为对象,批量绑定事件
if (event && !isString(event)) {
$.each(event, function(type, fn) {
$this.on(type, selector, data, fn, one);
})
return $this;
}
//处理参数
//没传selector参数 callback不是函数,且不为false
if (!isString(selector) && !isFunction(callback) && callback !== false)
callback = data, data = selector, selector = undefined;
//没传data
if (callback === undefined || data === false)
callback = data, data = undefined;

if (callback === false) callback = returnFalse;
// 给每一个Z对象里面的元素绑定事件
return $this.each(function(_, element) {
// 绑定一次,自动解绑
if (one) autoRemove = function(e) {
remove(element, e.type, callback);
return callback.apply(this, arguments);
}
//有selector选择符,使用代理
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);
})
}

1.5 核心函数add remove

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
/**
* 添加事件的实际方法
* @param {元素} element DOM元素
* @param {String} events 事件字符串
* @param {Function} fn 回调函数
* @param {All} data 绑定事件时传入的data,可以是各种类型
* @param {String} selector 被代理元素的css选择器
* @param {[type]} delegator 进行事件代理的函数
* @param {[type]} capture 指定捕获或者冒泡阶段
*/
function add(element, events, fn, data, selector, delegator, capture) {
var id = zid(element),
set = (handlers[id] || (handlers[id] = []));
//多个事件以空格为间隔
events.split(/\s/).forEach(function(event) {
//为ready
if (event == 'ready') return $(document).ready(fn);
//*************************构建handler*************************
var handler = parse(event);
handler.fn = fn;
handler.sel = selector;

// emulate mouseenter, mouseleave
// mouseenter、mouseleave通过mouseover、mouseout来模拟realEvent函数处理
// hover = { mouseenter: 'mouseover', mouseleave: 'mouseout' }
if (handler.e in hover) fn = function(e) {
//http://www.w3school.com.cn/jsref/event_relatedtarget.asp
// relatedTarget为相关元素,只有mouseover和mouseout事件才有
// 对mouseover事件而言,相关元素就是那个失去光标的元素;
// 对mouseout事件而言,相关元素则是获得光标的元素。
var related = e.relatedTarget;
if (!related || (related !== this && !$.contains(this, related)))
return handler.fn.apply(this, arguments);
};
handler.del = delegator;
// 需要进行事件代理时,调用的是封装了fn的delegator函数
var callback = delegator || fn;
handler.proxy = function(e) {
e = compatible(e); //无第二个参数,其实就是e = e;
if (e.isImmediatePropagationStopped()) return;
e.data = data;
var result = callback.apply(element, e._args == undefined ? [e] : [e].concat(e._args))
//当事件处理函数返回false时,阻止默认操作和冒泡
if (result === false) e.preventDefault(), e.stopPropagation();
return result;
}
handler.i = set.length; // 把handler在set中的下标赋值给handler.i
set.push(handler);
//*************************构建handler end*************************
if ('addEventListener' in element)
//addEventListener -- https://developer.mozilla.org/zh-CN/docs/Web/API/EventTarget/addEventListener
//使用`addEventListener`所传入的真正回调函数就是proxy函数
element.addEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture));
})
}
//删除handler
function remove(element, events, fn, selector, capture) {
var id = zid(element);
(events || '').split(/\s/).forEach(function(event) {
findHandlers(element, event, fn, selector).forEach(function(handler) {
delete handlers[id][handler.i];
if ('removeEventListener' in element)
element.removeEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture));
})
})
}

$.event = { add: add, remove: remove };

1.6 工具函数

1.6.1 zid函数

1
2
3
4
5
6
//通过一个_zid而不是通过DOM对象的引用来连接handler是因为:防止移除掉DOM元素后,
//handlers对象还保存着对这个DOM元素的引用。通过使用_zid就可以防止这种情况发生,
//避免了内存泄漏
function zid(element) {
return element._zid || (element._zid = _zid++);
}

1.6.2 focus和blur事件的冒泡问题

1
2
3
4
5
6
7
8
9
//focus和blur事件本身是不冒泡的,如果需要对这两个事件进行事件代理,就要运用一些小技巧。
//首先,如果浏览器支持focusin和focusout,就使用这两个可以冒泡事件来代替。
//如果浏览器不支持focusion和focusout,就利用focus和blur捕获不冒泡的特性,
//传入addEventListener中的第三个参数设置true,以此来进行事件代理
function eventCapture(handler, captureSetting) {
return handler.del &&
(!focusinSupported && (handler.e in focus)) ||
!!captureSetting;
}

1.6.3 实际传入到addEventListener第二个参数

1
2
3
4
5
6
7
8
9
10
// 构建事件代理中的事件对象
function createProxy(event) {
var key, proxy = { originalEvent: event }; // 新的事件对象有个originalEvent属性指向原对象
// 将原生事件对象的属性复制给新对象,除了returnValue、layerX、layerY和值为undefined的属性
// returnValue属性为beforeunload事件独有
for (key in event)
if (!ignoreProperties.test(key) && event[key] !== undefined) proxy[key] = event[key];
// 添加eventMethods里面的几个方法,并返回新的事件对象
return compatible(proxy, event);
}

1.6.4 其余工具函数

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
// 根据给定的参数在handlers变量中寻找对应的handler
function findHandlers(element, event, fn, selector) {
event = parse(event); // 解析event参数,分离出事件名和ns
if (event.ns) var matcher = matcherFor(event.ns);
// 取出所有属于element的handler,并且根据event、fn和selector参数进行筛选
return (handlers[zid(element)] || []).filter(function(handler) {
return handler && (!event.e || handler.e == event.e) // 事件名不同的过滤掉
&& (!event.ns || matcher.test(handler.ns)) // 命名空间不同的过滤掉
&& (!fn || zid(handler.fn) === zid(fn)) // 回调函数不同的过滤掉(通过_zid属性判断是否同一个函数)
&& (!selector || handler.sel == selector); // selector不同的过滤掉
})
}
//解析event参数,如 "click.abc",abc作为ns(命名空间)
function parse(event) {
var parts = ('' + event).split('.');
return { e: parts[0], ns: parts.slice(1).sort().join(' ') }
}
// 生成匹配的namespace表达式:'abc def' -> /(?:^| )abc .* ?def(?: |$)/
function matcherFor(ns) {
return new RegExp('(?:^| )' + ns.replace(' ', ' .* ?') + '(?: |$)')
}

function realEvent(type) {
return hover[type] || (focusinSupported && focus[type]) || type;
}

2.取消事件绑定

示例:

1
$('body').off('click', '#foo1');

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);
})
}

3.触发事件

$.Event:创建并初始化一个指定的DOM事件。如果给定properties对象,使用它来扩展出新的事件对象。默认情况下,事件被设置为冒泡方式;这个可以通过设置bubbles为false来关闭。
通过document.createEvent创建事件对象,然后通过dispatchEvent(源码中在$.fn.trigger和$.fn.triggerHandler中处理)来出发。

1
2
3
4
5
6
7
8
9
10
11
12
13
// Create the event.
var event = document.createEvent('Event');

// Define that the event name is 'build'.
event.initEvent('build', true, true);

// Listen for the event.
elem.addEventListener('build', function (e) {
// e.target matches elem
}, false);

// target can be any Element or other EventTarget.
elem.dispatchEvent(event);

上面有点要注意的就是当创建鼠标相关的事件时要在document.createEvent的第一个参数中传入MouseEvents,以提供更多的事件属性。鼠标相关的事件指的是:click、mousedown、mouseup和mousemove
示例:

1
2
3
4
$(document).on('mylib:change', function(e, from, to) {
console.log('change on %o with data %s, %s', e.target, from, to)
})
$(document.body).trigger('mylib:change', ['one', 'two'])

源码实现:

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
$.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
// 过直接调用focus()和blur()方法来触发对应事件,这算是对触发事件方法的一个优化
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);
}
});
};

// triggers event handlers on current element just as if an event occurred,
// doesn't trigger an actual event, doesn't bubble
// 直接触发事件的回调函数,而不是直接触发一个事件,所以也不冒泡
$.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;
};
//生成一个模拟事件,如果是鼠标相关事件,document.createEvent传入的第一个参数为'MouseEvents'
$.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);
};

4.bind,unbind,one实现

1
2
3
4
5
6
7
8
9
$.fn.bind = function(event, data, callback) {
return this.on(event, data, callback)
}
$.fn.unbind = function(event, callback) {
return this.off(event, callback)
}
$.fn.one = function(event, selector, data, callback) {
return this.on(event, selector, data, callback, 1)
}

还有省略一部分,全部代码挂在我的github上,对应文件夹v0.3.2(只实现on),v0.3.3(完整实现)。
https://github.com/zrysmt/DIY-zepto

参考阅读:

  • Zepto事件模块源码分析
  • relatedTarget属性解析
  • rollup.js配置解析

一步一步DIY zepto库,研究zepto源码7--动画模块(fx fx_method)

发表于 2016-12-05 | 更新于 2018-11-30 | 分类于 前端技术

代码挂在我的github上,对应文件夹v0.7.1。
https://github.com/zrysmt/DIY-zepto

注:要在github源代码中自己编译的话,要在基础包命令:npm run dist上要进行扩展了,输入命令:

1
2
3
4
MODULES="zepto event fx fx_methods" npm run dist
# on Windows
> SET MODULES=zepto event fx fx_methods
> npm run dist

1.示例Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
var div1 = $('#foo1');
div1.animate({
"width": "300px",
"height": "300px"
},
'slow', 'ease-in-out',
function() {
console.log('div1 animate callback');
// div2.hide('slow',function(){
div2.fadeOut('slow', function() {
console.log('div2 animate callback');
});
}, '2000');

2.fx

zepto的动画采用的是CSS3的动画/过渡,未做兼容。
核心方法是$.fn.animate = function(properties, duration, ease, callback, delay),实际上的处理逻辑是$.fn.anim = function(properties, duration, ease, callback, delay)
其本质就是设置好css3属性对象,然后用this.css(cssValues)方法是css3动画起作用。浏览器不支持的动画使用setTimeout

1
2
3
4
// duration为0,即浏览器不支持动画的情况,直接执行动画结束,执行回调。
if (duration <= 0) setTimeout(function() {
that.each(function() { wrappedCallback.call(this); });
}, 0);

整体的源代码和注释放在下面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
var Fx = function($) {
var prefix = '',
eventPrefix,
vendors = { Webkit: 'webkit', Moz: '', O: 'o' },
testEl = document.createElement('div'),
supportedTransforms = /^((translate|rotate|scale)(X|Y|Z|3d)?|matrix(3d)?|perspective|skew(X|Y)?)$/i,
transform,
transitionProperty, transitionDuration, transitionTiming, transitionDelay, //过渡
animationName, animationDuration, animationTiming, animationDelay, //动画
cssReset = {};

//将驼峰字符串转成css属性,如aB-->a-b
function dasherize(str) {
return str.replace(/([a-z])([A-Z])/, '$1-$2').toLowerCase();
}
//修正事件名
function normalizeEvent(name) {
return eventPrefix ? eventPrefix + name : name.toLowerCase();
}

/**
* 根据浏览器内核,设置CSS前缀,事件前缀
* 如css:-webkit- event:webkit
* 为prefix和eventPrefix赋值
*/
if (testEl.style.transform === undefined) {
$.each(vendors, function(vendor, event) {
if (testEl.style[vendor + 'TransitionProperty'] !== undefined) {
prefix = '-' + vendor.toLowerCase() + '-';
eventPrefix = event;
return false;
}
});
}

transform = prefix + 'transform';
//均为空''
cssReset[transitionProperty = prefix + 'transition-property'] =
cssReset[transitionDuration = prefix + 'transition-duration'] =
cssReset[transitionDelay = prefix + 'transition-delay'] =
cssReset[transitionTiming = prefix + 'transition-timing-function'] =
cssReset[animationName = prefix + 'animation-name'] =
cssReset[animationDuration = prefix + 'animation-duration'] =
cssReset[animationDelay = prefix + 'animation-delay'] =
cssReset[animationTiming = prefix + 'animation-timing-function'] = '';
/**
* 动画常量数据源
* @type {{off: boolean, speeds: {_default: number, fast: number, slow: number}, cssPrefix: string, transitionEnd: *, animationEnd: *}}
*/
$.fx = {
off: (eventPrefix === undefined && testEl.style.transitionProperty === undefined), //能力检测是否支持动画,具体检测是否支持过渡,支持过渡事件
speeds: { _default: 400, fast: 200, slow: 600 },
cssPrefix: prefix, //css 前缀 如-webkit-
transitionEnd: normalizeEvent('TransitionEnd'), //过渡结束事件
animationEnd: normalizeEvent('AnimationEnd') //动画播放结束事件
};
/**
* [animate 自定义动画]
* @param {[Object]} properties [属性变化成,如{"width":"300px"}]
* @param {[type]} duration [速度 如slow或者一个数字]
* @param {[type]} ease [变化的速率ease、linear、ease-in / ease-out、ease-in-out
cubic-bezier]
* @param {Function} callback [回调函数]
* @param {[type]} delay [延迟时间]
*/
$.fn.animate = function(properties, duration, ease, callback, delay) {
//参数处理
if ($.isFunction(duration)) //传参为function(properties,callback)
callback = duration, ease = undefined, duration = undefined;
if ($.isFunction(ease)) //传参为function(properties,duration,callback)
callback = ease, ease = undefined;
if ($.isPlainObject(duration)) //传参为function(properties,{})
ease = duration.easing, callback = duration.complete, delay = duration.delay, duration = duration.duration
//duration参数处理
if (duration) duration = (typeof duration == 'number' ? duration :
($.fx.speeds[duration] || $.fx.speeds._default)) / 1000;
if (delay) delay = parseFloat(delay) / 1000;
return this.anim(properties, duration, ease, callback, delay);
};
$.fn.anim = function(properties, duration, ease, callback, delay) {
var key, cssValues = {},
cssProperties, transforms = '',
that = this,
wrappedCallback, endEvent = $.fx.transitionEnd,
fired = false;
//修正好时间
if (duration === undefined) duration = $.fx.speeds._default / 1000;
if (delay === undefined) delay = 0;
if ($.fx.off) duration = 0; //如果浏览器不支持动画,持续时间设为0,直接跳动画结束

if (typeof properties == 'string') {
// keyframe animation
cssValues[animationName] = properties; //properties是动画名
cssValues[animationDuration] = duration + 's';
cssValues[animationDelay] = delay + 's';
cssValues[animationTiming] = (ease || 'linear');
endEvent = $.fx.animationEnd;
} else { //properties 是样式集对象
cssProperties = [];
// CSS transitions
for (key in properties) {
//是这些属性^((translate|rotate|scale)(X|Y|Z|3d)?|matrix(3d)?|perspective|skew(X|Y)?)
if (supportedTransforms.test(key)) {
transforms += key + '(' + properties[key] + ') '; //拼凑成变形方法
} else {
cssValues[key] = properties[key], cssProperties.push(dasherize(key));
}
}
if (transforms) cssValues[transform] = transforms, cssProperties.push(transform);
if (duration > 0 && typeof properties === 'object') {
cssValues[transitionProperty] = cssProperties.join(', ');
cssValues[transitionDuration] = duration + 's';
cssValues[transitionDelay] = delay + 's';
cssValues[transitionTiming] = (ease || 'linear');
}
}
//动画完成后的响应函数
wrappedCallback = function(event) {
if (typeof event !== 'undefined') {
if (event.target !== event.currentTarget) return; // makes sure the event didn't bubble from "below"
$(event.target).unbind(endEvent, wrappedCallback);
} else {
$(this).unbind(endEvent, wrappedCallback); // triggered by setTimeout
}

fired = true;
$(this).css(cssReset);

callback && callback.call(this);
};
//处理动画结束事件
if (duration > 0) {
//绑定动画结束事件
this.bind(endEvent, wrappedCallback);
// transitionEnd is not always firing on older Android phones
// so make sure it gets fired
//延时ms后执行动画,注意这里加了25ms,保持endEvent,动画先执行完。
//绑定过事件还做延时处理,是transitionEnd在older Android phones不一定触发
setTimeout(function() {
if (fired) return;
wrappedCallback.call(that);
}, ((duration + delay) * 1000) + 25);
}

// trigger page reflow so new elements can animate
//主动触发页面回流,刷新DOM,让接下来设置的动画可以正确播放
//更改 offsetTop、offsetLeft、 offsetWidth、offsetHeight;scrollTop、scrollLeft、
//scrollWidth、scrollHeight;clientTop、clientLeft、clientWidth、clientHeight;getComputedStyle() 、
//currentStyle()。这些都会触发回流。回流导致DOM重新渲染,平时要尽可能避免,
//但这里,为了动画即时生效播放,则主动触发回流,刷新DOM

this.size() && this.get(0).clientLeft;

//设置样式,启动动画
this.css(cssValues);

// duration为0,即浏览器不支持动画的情况,直接执行动画结束,执行回调。
if (duration <= 0) setTimeout(function() {
that.each(function() { wrappedCallback.call(this); });
}, 0);

return this;
};

testEl = null;
};
export default Fx;

3.fx_methods

源码中的fx_methods(fx_methods.js中)方法,说白了就是利用上面的fx.js文件下的$.fn.animate函数提供便捷的方法

整体源码和注释放在这里

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
var Fx_methods = function($) {
var document = window.document,
docElem = document.documentElement,
origShow = $.fn.show,
origHide = $.fn.hide,
origToggle = $.fn.toggle;

function anim(el, speed, opacity, scale, callback) {
if (typeof speed == 'function' && !callback) callback = speed, speed = undefined;
var props = { opacity: opacity };
if (scale) {
props.scale = scale;
el.css($.fx.cssPrefix + 'transform-origin', '0 0'); //设置变形原点
}
return el.animate(props, speed, null, callback); //不支持速率变化
}

function hide(el, speed, scale, callback) {
//$(dom).animate({opacity: 0, '-webkit-transform-origin': '0px 0px 0px', '-webkit-transform': 'scale(0, 0)' },800)
//设置了变形原点,缩放为0,它就会缩到左上角再透明
return anim(el, speed, 0, scale, function() {
origHide.call($(this));
callback && callback.call(this);
});
}

$.fn.show = function(speed, callback) {
origShow.call(this);
if (speed === undefined) {
speed = 0;
} else {
this.css('opacity', 0);
}
return anim(this, speed, 1, '1,1', callback);
};

$.fn.hide = function(speed, callback) {
if (speed === undefined) {
return origHide.call(this);
} else {
return hide(this, speed, '0,0', callback);
}
};

$.fn.toggle = function(speed, callback) {
if (speed === undefined || typeof speed == 'boolean') {
return origToggle.call(this, speed);
} else {
return this.each(function() {
var el = $(this);
el[el.css('display') == 'none' ? 'show' : 'hide'](speed, callback);
});

}
};

$.fn.fadeTo = function(speed, opacity, callback) {
return anim(this, speed, opacity, null, callback);
};

$.fn.fadeIn = function(speed, callback) {
var target = this.css('opacity');
if (target > 0){
this.css('opacity', 0);
}
else {
target = 1;
}
return origShow.call(this).fadeTo(speed, target, callback);
};

$.fn.fadeOut = function(speed, callback) {
return hide(this, speed, null, callback);
};

$.fn.fadeToggle = function(speed, callback) {
return this.each(function() {
var el = $(this);
el[
(el.css('opacity') == 0 || el.css('display') == 'none') ? 'fadeIn' : 'fadeOut'
](speed, callback);
});
};
};

export default Fx_methods;

全部代码挂在我的github上,本博文对应文件夹v0.7.x。
https://github.com/zrysmt/DIY-zepto

参考阅读:

  • Zepto源码分析-动画(fx fx_method)模块

一步一步DIY zepto库,研究zepto源码8--touch模块

发表于 2016-12-05 | 更新于 2018-11-30 | 分类于 前端技术

由于移动端众所周知的click 300ms延迟的缘故(用户碰触页面之后,需要等待一段时间来判断是不是双击(double tap)动作,而不是立即响应单击(click),等待的这段时间大约是300ms)。移动事件提供了touchstart、touchmove、touchend,却没有提供对tap的支持。许多主流框架都是自定义实现了tap事件,消除300ms的延迟,当然包括Zepto.js。

关于点击穿透的解决方案可以查看: 移动页面点击穿透问题解决方案。

此外,使用原生的touch事件也存在点击穿透的问题,因为click是在touch系列事件发生后大约300ms才触发的,混用touch和click肯定会导致点透问题。所以在移动端我们有必要使用类似Zepto.js的tap事件。

代码挂在我的github上,对应文件夹v0.8.1。
https://github.com/zrysmt/DIY-zepto

1.源码

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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192

var Touch = function($) {
var touch = {},
touchTimeout, tapTimeout, swipeTimeout, longTapTimeout,
longTapDelay = 750,
gesture;
// 判断滑动方向,返回Left, Right, Up, Down
function swipeDirection(x1, x2, y1, y2) {
return Math.abs(x1 - x2) >=
Math.abs(y1 - y2) ? (x1 - x2 > 0 ? 'Left' : 'Right') : (y1 - y2 > 0 ? 'Up' : 'Down');
}
//长按
function longTap() {
longTapTimeout = null;
if (touch.last) {
touch.el.trigger('longTap');
touch = {};
}
}
//取消长按
function cancelLongTap() {
if (longTapTimeout) clearTimeout(longTapTimeout);
longTapTimeout = null;
}
//取消所有
function cancelAll() {
if (touchTimeout) clearTimeout(touchTimeout);
if (tapTimeout) clearTimeout(tapTimeout);
if (swipeTimeout) clearTimeout(swipeTimeout);
if (longTapTimeout) clearTimeout(longTapTimeout);
touchTimeout = tapTimeout = swipeTimeout = longTapTimeout = null;
touch = {};
}
// IE的touch事件
function isPrimaryTouch(event) {
return (event.pointerType == 'touch' ||
event.pointerType == event.MSPOINTER_TYPE_TOUCH) && event.isPrimary
}
// IE鼠标事件
function isPointerEventType(e, type) {
return (e.type == 'pointer' + type ||
e.type.toLowerCase() == 'mspointer' + type)
}

$(document).ready(function() {
var now, delta, deltaX = 0,
deltaY = 0,
firstTouch, _isPointerType;
//IE的手势
if ('MSGesture' in window) {
gesture = new MSGesture();
gesture.target = document.body;
}
$(document)
.bind('MSGestureEnd', function(e) { //处理IE手势结束
var swipeDirectionFromVelocity =
e.velocityX > 1 ? 'Right' : e.velocityX < -1 ? 'Left' : e.velocityY > 1 ? 'Down' : e.velocityY < -1 ? 'Up' : null;
if (swipeDirectionFromVelocity) {
touch.el.trigger('swipe');
touch.el.trigger('swipe' + swipeDirectionFromVelocity);
}
})
// 处理手指接触
.on('touchstart MSPointerDown pointerdown', function(e) {
//排除非触摸设备
if ((_isPointerType = isPointerEventType(e, 'down')) &&
!isPrimaryTouch(e)) return;
firstTouch = _isPointerType ? e : e.touches[0]; // 获取起点位置数据
// 重置终点坐标
if (e.touches && e.touches.length === 1 && touch.x2) {
// Clear out touch movement data if we have it sticking around
// This can occur if touchcancel doesn't fire due to preventDefault, etc.
touch.x2 = undefined;
touch.y2 = undefined;
}
// 判断用户动作类型
now = Date.now();
delta = now - (touch.last || now); // 距离上次碰触的时间差
touch.el = $('tagName' in firstTouch.target ?
firstTouch.target : firstTouch.target.parentNode); // 手指碰触的元素
touchTimeout && clearTimeout(touchTimeout); // 重置touch事件处理器的Timeout ID
//记录起点坐标
touch.x1 = firstTouch.pageX;
touch.y1 = firstTouch.pageY;
//判断是否双击
if (delta > 0 && delta <= 250) touch.isDoubleTap = true;
touch.last = now;
// 注册长按事件处理器ID
longTapTimeout = setTimeout(longTap, longTapDelay);
// adds the current touch contact for IE gesture recognition
// 支持IE手势识别
if (gesture && _isPointerType) gesture.addPointer(e.pointerId);
})
// 处理手指滑动
.on('touchmove MSPointerMove pointermove', function(e) {
// 排除非触摸设备
if ((_isPointerType = isPointerEventType(e, 'move')) &&
!isPrimaryTouch(e)) return;
firstTouch = _isPointerType ? e : e.touches[0];
cancelLongTap(); // 取消长按事件处理器
touch.x2 = firstTouch.pageX;
touch.y2 = firstTouch.pageY;

deltaX += Math.abs(touch.x1 - touch.x2);
deltaY += Math.abs(touch.y1 - touch.y2);
})
// 处理手指离开
.on('touchend MSPointerUp pointerup', function(e) {
// 排除非触摸设备
if ((_isPointerType = isPointerEventType(e, 'up')) &&
!isPrimaryTouch(e)) return;
cancelLongTap(); // 取消长按事件处理器

// swipe 判定滑动动作(起点 - 终点的横向或者纵向距离超过30px)
if ((touch.x2 && Math.abs(touch.x1 - touch.x2) > 30) ||
(touch.y2 && Math.abs(touch.y1 - touch.y2) > 30)) {


// 注册长按事件处理器ID(立即准备执行长按)
swipeTimeout = setTimeout(function() {
if (touch.el) {
touch.el.trigger('swipe'); // 触发长按
// 触发向上|下|左|右的长按
touch.el.trigger('swipe' + (swipeDirection(touch.x1, touch.x2, touch.y1, touch.y2)))
}
touch = {}; // 清空数据,本次touch结束
}, 0);
}
// normal tap 正常轻触
else if ('last' in touch) { // 如果记录了上次接触时间
// don't fire tap when delta position changed by more than 30 pixels,
// for instance when moving to a point and back to origin
if (deltaX < 30 && deltaY < 30) {
// delay by one tick so we can cancel the 'tap' event if 'scroll' fires
// ('tap' fires before 'scroll')
//立即准备执行轻触,不立即执行是为了scroll时能取消执行轻触
tapTimeout = setTimeout(function() {
// trigger universal 'tap' with the option to cancelTouch()
// (cancelTouch cancels processing of single vs double taps for faster 'tap' response)
// 触发全局tap,cancelTouch()可以取消singleTap,doubleTap事件,以求更快响应轻触
var event = $.Event('tap');
event.cancelTouch = cancelAll;
// [by paper] fix -> "TypeError: 'undefined' is not an object (evaluating 'touch.el.trigger'), when double tap
if (touch.el) touch.el.trigger(event);

// trigger double tap immediately
// 立即触发doubleTap
if (touch.isDoubleTap) {
if (touch.el) touch.el.trigger('doubleTap');
touch = {};
}

// trigger single tap after 250ms of inactivity
// 250ms后触发singleTap
else {
touchTimeout = setTimeout(function() {
touchTimeout = null;
if (touch.el) touch.el.trigger('singleTap');
touch = {};
}, 250);
}
}, 0);
} else { // 如果是滑了一圈又回到起点,扔掉事件数据,不做处理
touch = {};
}
deltaX = deltaY = 0; // 重置横向,纵向滑动距离
}
})
// when the browser window loses focus,
// for example when a modal dialog is shown,
// cancel all ongoing events
// 浏览器窗口失去焦点时,取消所有事件处理动作
.on('touchcancel MSPointerCancel pointercancel', cancelAll);

// scrolling the window indicates intention of the user
// to scroll, not tap or swipe, so cancel all ongoing events
// 触发scroll时取消所有事件处理动作
$(window).on('scroll', cancelAll);
});

//在这里注册,在源码中触发
['swipe', 'swipeLeft', 'swipeRight', 'swipeUp', 'swipeDown',
'doubleTap', 'tap', 'singleTap', 'longTap'
].forEach(function(eventName) {
$.fn[eventName] = function(callback) {
return this.on(eventName, callback);
};
});

};

export default Touch;

2.源码分析

核心层分是把事件绑定到$(document)上分别进行处理

1
2
3
4
5
6
7
$(document)
.bind('MSGestureEnd', function(e){}//处理IE手势结束
.on('touchstart MSPointerDown pointerdown', function(e) {} // 处理手指接触
.on('touchmove MSPointerMove pointermove', function(e) {} // 处理手指滑动
.on('touchend MSPointerUp pointerup', function(e) {} // 处理手指离开
.on('touchcancel MSPointerCancel pointercancel', cancelAll);
// 浏览器窗口失去焦点时,取消所有事件处理动作
1
2
// 触发scroll时取消所有事件处理动作
$(window).on('scroll', cancelAll);

tap事件是通过touch事件模拟的

  • tap —元素tap的时候触发。
  • singleTap and doubleTap — 这一对事件可以用来检测元素上的单击和双击。(如果你不需要检测单击、双击,使用 tap 代替)。
  • longTap — 当一个元素被按住超过750ms触发。
  • swipe, swipeLeft, swipeRight, swipeUp, swipeDown — 当元素被划过时触发。(可选择给定的方向)
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
if (deltaX < 30 && deltaY < 30) {
//立即准备执行轻触,不立即执行是为了scroll时能取消执行轻触
tapTimeout = setTimeout(function() {
// 触发全局tap,cancelTouch()可以取消singleTap
// doubleTap事件,以求更快响应轻触
var event = $.Event('tap');
event.cancelTouch = cancelAll;
if (touch.el) touch.el.trigger(event);
// 立即触发doubleTap
if (touch.isDoubleTap) {
if (touch.el) touch.el.trigger('doubleTap');
touch = {};
}
// 250ms后触发singleTap
else {
touchTimeout = setTimeout(function() {
touchTimeout = null;
if (touch.el) touch.el.trigger('singleTap');
touch = {};
}, 250);
}
}, 0);
} else { // 如果是滑了一圈又回到起点,扔掉事件数据,不做处理
touch = {};
}

Zepto的touch模块也只实现了tap和swipe相关的动作,不支持复杂手势,需要支持复杂手势的话,可以使用 hammer.js ,hammer提供了完善的一整套手势支持( 注意 :hammer也存在点击穿透问题,仍然需要手动处理该问题)

代码挂在我的github上,对应文件夹v0.8.1。
https://github.com/zrysmt/DIY-zepto

参考阅读:

  • 移动页面点击穿透问题解决方案
  • Zepto的touch模块源码解读

使用javascript原生实现一个模板引擎

发表于 2016-12-05 | 更新于 2018-11-30 | 分类于 前端技术

模板引擎分为前端和后端的,前端常用的模板引擎如artTemplate,juicer渲染是在客户端完成的;后端的模板引擎如基于PHP的smarty,渲染是服务器完成的。

前两天看到一篇博客挺好的是用了不到20行代码实现一个前端的模板引擎,感觉挺有趣的,今天就来实现下

1.简单的例子

逻辑

1
2
3
4
5
6
7
var tplEngine = function(tpl, data) {
var re = /<%([^%>]+)?%>/g;
while (match = re.exec(tpl)) {
tpl = tpl.replace(match[0], data[match[1]]);
}
return tpl;
};

就是把<%name%>替换成data.name即可
测试

1
2
3
4
5
var template1 = '<p>Hello, my name is <%name%>. I\'m <%age%> years old.</p>';
console.log(tplEngine(template1, {
name: "Tom",
age: 29
}));

2. data属性复杂点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var tplEngine = function(tpl, data) {
var re = /<%([^%>]+)?%>/g;
var code = 'var r=[];\n',
cursor = 0;//辅助变量
var add = function(line, js) {//针对变量还是普通的片段分别处理
js ? code += 'r.push(' + line + ');\n' :
code += 'r.push("' + line.replace(/"/g, '\\"') + '");\n';
};
while (match = re.exec(tpl)) {
add(tpl.slice(cursor, match.index));
add("this."+match[1],true);//要替换的变量
cursor = match.index + match[0].length;
}
add(tpl.substr(cursor, tpl.length - cursor));
code += 'return r.join("");'; // <-- return the result
console.info(code);

return new Function(code.replace(/[\r\t\n]/g,'')).apply(data);
};

我们研究下new Function
构造函数

1
new Function ([arg1[, arg2[, ...argN]],] functionBody)

argN是传入的参数,当然可以省略
函数体是code.replace(/[\r\t\n]/g,''),apply将函数体的上下文环境(this)指向了data
测试

1
2
3
4
5
6
7
var template2 = '<p>Hello, my name is <%name%>. I\'m <%profile.age%> years old.</p>';
console.log(tplEngine(template2, {
name: "Kim",
profile: {
age: 29
}
}));

3.加入for if循环和判断语句

按照上面的测试

1
2
3
4
5
6
7
8
var template3 =
'My skills:' +
'<%for(var index in this.skills) {%>' +
'<a href="#"><%skills[index]%></a>' +
'<%}%>';
console.log(tplEngine(template3, {
skills: ["js", "html", "css"]
}));

报错:

1
Uncaught SyntaxError: Unexpected token for

打印结果r.push(for(var index in this.skills) {);是有问题的。

1
2
3
4
5
6
7
8
9
var r=[];
r.push("My skills:");
r.push(for(var index in this.skills) {);
r.push("<a href=\"#\">");
r.push(this.skills[index]);
r.push("</a>");
r.push(this.});
r.push("");
return r.join("");

修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var tplEngine = function(tpl, data) {
var re = /<%([^%>]+)?%>/g,
re2 = /(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g;
var code = 'var r=[];\n',
cursor = 0;
var add = function(line, js) {
js ? code += line.match(re2) ? line + '\n' : 'r.push(' + line + ');\n' :
code += 'r.push("' + line.replace(/"/g, '\\"') + '");\n';
};
while (match = re.exec(tpl)) {
add(tpl.slice(cursor, match.index));
re2.test(match[1]) ? add(match[1], true) : add("this." + match[1], true);
cursor = match.index + match[0].length;
}
add(tpl.substr(cursor, tpl.length - cursor));
code += 'return r.join("");';
console.info(code);

return new Function(code.replace(/[\r\t\n]/g, '')).apply(data);
};

我们可以打印code看看

1
code+='console.log(r);\n';

1
["My skills:", "<a href="#">", "js", "</a>", "<a href="#">", "html", "</a>", "<a href="#">", "css", "</a>", ""]

最终的结果

1
2
3
4
5
6
7
8
9
10
var r=[];
r.push("My skills:");
for(var index in this.skills) {
r.push("<a href=\"#\">");
r.push(this.skills[index]);
r.push("</a>");
}
r.push("");
console.log(r);
return r.join("");

解析结果

1
My skills:<a href="#">js</a><a href="#">html</a><a href="#">css</a>

参考阅读:

  • 只有20行Javascript代码!手把手教你写一个页面模板引擎

domReady机制探究及DOMContentLoaded研究

发表于 2016-11-10 | 更新于 2018-11-30 | 分类于 前端技术

domReady机制是很多框架和库都具有的种子模块,使用了在DOM树解析完成后就立即响应,不用等待图片等资源下载完成(onload执行时候表示这些资源完全下载完成)的一种机制,那怎么实现呢。

1)支持DOMContentLoaded事件的,就使用DOMContentLoaded事件;
2)不支持的,就用来自Diego Perini发现的著名Hack兼容。兼容原理大概就是,通过IE中的document.documentElement.doScroll(‘left’)来判断DOM树是否创建完毕或者使用监控script标签的onreadystatechange得到它的readyState属性判断【遗憾的是经过我们的实验,在IE下domReady机制总会在onload后执行】

1. domReady机制在IE7-8下

1.1 domReady机制源码(包括IE/非IE)

demo1.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title> DOMContentLoaded Demo</title>
</head>
<body>
<div id="div1"></div>
<script defer='defer' src="deferjs.js"></script>
<script src="http://cdn.bootcss.com/jquery/1.12.4/jquery.js"></script>
<script type="text/javascript" src="demo1.js"></script>
<script>
dom.Ready(function() {
console.info("我的domReady1");
});
</script>
</body>


</html>

deferjs.js

1
console.log("defer script");

demo1.js

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
dom = [];
dom.isReady = false;
dom.isFunction = function(obj) {
return Object.prototype.toString.call(obj) === "[object Function]";
};
dom.Ready = function(fn) {
dom.initReady(); //如果没有建成DOM树,则走第二步,存储起来一起杀
if (dom.isFunction(fn)) {
if (dom.isReady) {
fn(); //如果已经建成DOM,则来一个杀一个
} else {
dom.push(fn); //存储加载事件
}
}
};
dom.fireReady = function() {
if (dom.isReady) return;
dom.isReady = true;
for (var i = 0, n = dom.length; i < n; i++) {
var fn = dom[i];
fn();
}
dom.length = 0; //清空事件
};
dom.initReady = function() {
if (document.addEventListener) {//非IE
document.addEventListener("DOMContentLoaded", function() {
console.log("DOMContentLoaded");
document.removeEventListener("DOMContentLoaded", arguments.callee, false); //清除加载函数
dom.fireReady();
}, false);
} else {//IE走这条线
if (document.getElementById) {
document.write("<script id=\"ie-domReady\" defer='defer'src=\"//:\"><\/script>");
document.getElementById("ie-domReady").onreadystatechange = function() {
console.log(this.readyState);
if (this.readyState === "complete") {
//只针对IE readyState 的值 complete--脚本执行完成。
//这个时候DOM树肯定已经解析完成了,不支持defer属性
//会在onload函数之后执行。
dom.fireReady();
console.log('this.readyState === "complete"');
this.onreadystatechange = null;
this.parentNode.removeChild(this);
}
};
}
}
};
/**********测试**************************************************/
dom.Ready(function() {
console.info("我的domReady2");
});
/*$(document).ready(function() {
dom.Ready(function() {
console.info("我的domReady4在jquery的ready函数中");
});
console.log('jquery中的ready函数');
});*/
dom.Ready(function() {
console.info("我的domReady3");
});


console.log('在js中');
window.onload = function(){
console.log("onload函数");
};

1.2 背景知识介绍

  • document.readystate

readyState 属性返回当前文档的状态(载入中……)。
该属性返回以下值:

  • uninitialized - 还未开始载入
  • loading - 载入中
  • interactive - 已加载,文档与用户可以开始交互并引发DOMContentLoaded事件
  • complete - 载入完成
  • IE的 script的readyState
    FireFox的script 元素不支持onreadystatechange事件,只支持onload事件
    IE的 script 元素支持onreadystatechange事件,不支持onload事件

只针对IE readyState 的值 可能为 以下几个 :

  • “uninitialized” – 原始状态
  • “loading” – 下载数据中..
  • “loaded” – 下载完成
  • “interactive” – 还未执行完毕.
  • “complete” – 脚本执行完毕
  • defer和onload函数
1
<script defer='defer' src="deferjs.js"></script>

defer 属性仅适用于外部脚本(只有在使用 src 属性时)

  • 如果 async=”async”:脚本相对于页面的其余部分异步地执行(当页面继续进行解析时,脚本将被执行)
  • 如果不使用 async 且 defer=”defer”:脚本将在页面完成解析时执行
  • 如果既不使用 async 也不使用 defer:在浏览器继续解析页面之前,立即读取并执行脚本

1.3 结果分析

在IE7/8打印的结果是:

dom.fireReady函数在onload函数之后执行

1.4 IE下监控DOM树是否解析完成的其他做法

除了使用document.write("<script id=\"ie-domReady\" defer='defer'src=\"//:\"><\/script>")还可以监控DOM树是否解析完成
在更早的IE版本中,可以通过每隔一段时间执行一次document.documentElement.doScroll("left")来检测这一状态,因为这条代码在DOM加载完毕之前执行时会抛出错误(throw an error)

1
2
3
4
5
6
7
8
9
(function() {
try { //在DOM未建完之前调用元素的doScroll抛出错误
document.documentElement.doScroll('left');
} catch (e) { //延迟再试
setTimeout(arguments.callee, 50);
return;
}
init(); //没有错误则执行用户回调
})();

2. domReady机制在chrome中

将上面的demo2.js文件下注释的jquery的ready函数取消注释进行执行,得到结果是:

demo1.html中将script标签放入到<head>得到的结果是一样的。

在chrome中的顺序是:

  • document.readyState 为loading
    • jquery的ready函数外 (打印结果:在js中)
  • document.readyState 为interactive【DOM解析完成】
    • 带defer的script (打印结果:defer script)
    • jquery的ready函数里面 (打印结果:jquery中的ready函数)
    • 监听DOMContentLoaded要执行的函数 dom.fireReady(打印结果:DOMContentLoaded和我是domReady系列)
  • document.readyState 为compelete
    • onload函数(打印结果:onload函数)

3.总结

1,2区别:

  • 带defer的script标签,IE8以下中不支持defer属性
  • dom.fireReady在IE中的逻辑是在document.readyState=="compelte"后,会在onload函数之后紧接着执行,在chrome/Firfox的逻辑是在document.addEventListener("DOMContentLoaded",function(){})的回掉函数中。

综上所诉,执行的顺序应该为:

  • document.readyState 为loading
    • jquery的ready函数外
  • 【非IE下】document.readyState 为interactive【DOM解析完成】
    • 带defer的script
    • jquery的ready函数里面
    • 触发DOMContentLoaded事件,监听DOMContentLoaded要执行的函数
  • 【IE常用来判断DOM树是否解析完成】document.documentElement.doScroll 这时可以让HTML元素使用doScroll方法,抛出错误就是DOM树未解析完成
  • document.readyState 为compelete
    • onload函数(打印结果:onload函数)【图片flash等资源都加载完毕】

最后附上监测IE,在IE的onload函数后面执行执行的另外一种实现方式

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
//http://javascript.nwbox.com/IEContentLoaded/
//by Diego Perini 2007.10.5
function IEContentLoaded(w, fn) {
var d = w.document||document,
done = false,
init = function() {
if (!done) { //只执行一次
done = true;
fn();
}
};


(function() {
try { //在DOM未建完之前调用元素的doScroll抛出错误
d.documentElement.doScroll('left');
} catch (e) { //延迟再试
setTimeout(arguments.callee, 50);
return;
}
init(); //没有错误则执行用户回调
})();
// 如果用户是在domReady之后绑定这个函数呢?立即执行它
d.onreadystatechange = function() {
if (d.readyState == 'complete') {
d.onreadystatechange = null;
init();
}
};
}

参考阅读:

  • 司徒正美 - 《javascript框架设计》
  • 司徒正美-javascript的事件加载
  • 主流JS框架中DOMReady事件的实现
  • javascript的domReady
  • document.readyState的属性
  • DOMContentLoaded介绍
  • 又说 动态加载 script. ie 下 script Element 的 readyState状态

前端CSS&JS动画总结

发表于 2016-11-10 | 更新于 2018-11-30 | 分类于 前端技术

使用CSS3,我们可以很方便快捷的改变元素的宽度、高度,方位,角度,透明度等基本信息,但是这些不能满足我们的需求,而且浏览器对于CSS3的兼容性不好,所以这时候就需要拓展更多的js动画。

1.CSS3动画

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CSS3 动画</title>
<style type="text/css">
#taxiway {
width: 800px;
height: 100px;
background: #E8E8FF;
position: relative;
}
#move,
#move2{
position: absolute;
left: 0px;
width: 100px;
height: 100px;
background: #a9ea00;
}
#move3 {
position: absolute;
left: 0px;
width: 50px;
height: 50px;
background: #a9ea00;
}
.animate {
animation-duration: 3s;
animation-name: slidein;
animation-timing-function: ease-in-out;
animation-iteration-count: 2; /* 几次 */
animation-fill-mode: forwards;
}
@keyframes slidein {
from {
left: 0%;
background: white;
}
to {
left: 700px;
background: red;
}
}
.animate2 {
animation-duration: 3s;
animation-name: cycle;
animation-iteration-count: 2;
animation-direction: alternate;
}
@keyframes cycle {
to {
width: 200px;
height: 200px;
}
}
</style>
</head>
<body>
<div id="taxiway">
<div id="move"></div>
<div id="move2" class="animate"></div>
<div id="move3" class="animate2"></div>
</div>
</body>
</html>

有两个动画,class="animate",class="animate2"
第一种动画是:从左边(0%)到右边(700px)处,背景颜色从white变成red,并且来回变换两次(animation-iteration-count: 2);
第二种动画是:id="move3"元素从大小为50px,50px,变为200px,200px

2.JS动画

最基础的动画刚开始就是利用setTimeout和setInterval实现的。

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>动画</title>
<style type="text/css">
#taxiway {
width: 800px;
height: 100px;
background: #E8E8FF;
position: relative;
}
#move {
position: absolute;
left: 0px;
width: 100px;
height: 100px;
background: #a9ea00;
}
</style>
</head>

<body>
<div id="taxiway">
<div id="move"></div>
</div>
<script type="text/javascript" src="startAnimate.js">
</script>
</body>
</html>

startAnimate.js

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
window.onload = function() {
var el = document.getElementById('move');
var parent = document.getElementById('taxiway');
var distance = parent.offsetWidth - el.offsetWidth; //总距离
var begin = parseFloat(window.getComputedStyle(el, null).left); //开始位置
var end = begin + distance; //结束位置
var fps = 30;
var interval = 1000 / fps; //每隔多少ms刷新一次
var duration = 2000; //时长
var times = duration / 1000 * fps; //一共刷新这么多次
var step = distance / times; //每次移距离
console.log(distance, begin, end);

el.onclick = function() {
startAnimate(this);
}

function startAnimate(el) {
var beginTime = new Date();
var id = setInterval(function() {
var t = new Date - beginTime;
if (t >= duration) {
el.style.left = end + "px";
clearInterval(id);
console.info(t);
} else {
var per = t / duration; //当前进度 控制per就可以控制加减速
el.style.left = begin + per * distance + "px";
}
}, interval);
}
}

通过控制per的大小变化可以控制加减速,这里我们参照jquery.easing.js的曲线函数

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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
function bounceOut(x) {
var n1 = 7.5625,
d1 = 2.75;
if (x < 1 / d1) {
return n1 * x * x;
} else if (x < 2 / d1) {
return n1 * (x -= (1.5 / d1)) * x + .75;
} else if (x < 2.5 / d1) {
return n1 * (x -= (2.25 / d1)) * x + .9375;
} else {
return n1 * (x -= (2.625 / d1)) * x + .984375;
}
}
var pow = Math.pow,
sqrt = Math.sqrt,
sin = Math.sin,
cos = Math.cos,
PI = Math.PI,
c1 = 1.70158,
c2 = c1 * 1.525,
c3 = c1 + 1,
c4 = (2 * PI) / 3,
c5 = (2 * PI) / 4.5;
var easeSetting = {
liner: function(x) {
return x;
},
swing: function(x) {
return 0.5 - cos(x * Math.PI) / 2;
},
easeInQuad: function(x) {
return x * x;
},
easeOutQuad: function(x) {
return 1 - (1 - x) * (1 - x);
},
easeInOutQuad: function(x) {
return x < 0.5 ?
2 * x * x :
1 - pow(-2 * x + 2, 2) / 2;
},
easeInCubic: function(x) {
return x * x * x;
},
easeOutCubic: function(x) {
return 1 - pow(1 - x, 3);
},
easeInOutCubic: function(x) {
return x < 0.5 ?
4 * x * x * x :
1 - pow(-2 * x + 2, 3) / 2;
},
easeInQuart: function(x) {
return x * x * x * x;
},
easeOutQuart: function(x) {
return 1 - pow(1 - x, 4);
},
easeInOutQuart: function(x) {
return x < 0.5 ?
8 * x * x * x * x :
1 - pow(-2 * x + 2, 4) / 2;
},
easeInQuint: function(x) {
return x * x * x * x * x;
},
easeOutQuint: function(x) {
return 1 - pow(1 - x, 5);
},
easeInOutQuint: function(x) {
return x < 0.5 ?
16 * x * x * x * x * x :
1 - pow(-2 * x + 2, 5) / 2;
},
easeInSine: function(x) {
return 1 - cos(x * PI / 2);
},
easeOutSine: function(x) {
return sin(x * PI / 2);
},
easeInOutSine: function(x) {
return -(cos(PI * x) - 1) / 2;
},
easeInExpo: function(x) {
return x === 0 ? 0 : pow(2, 10 * x - 10);
},
easeOutExpo: function(x) {
return x === 1 ? 1 : 1 - pow(2, -10 * x);
},
easeInOutExpo: function(x) {
return x === 0 ? 0 : x === 1 ? 1 : x < 0.5 ?
pow(2, 20 * x - 10) / 2 :
(2 - pow(2, -20 * x + 10)) / 2;
},
easeInCirc: function(x) {
return 1 - sqrt(1 - pow(x, 2));
},
easeOutCirc: function(x) {
return sqrt(1 - pow(x - 1, 2));
},
easeInOutCirc: function(x) {
return x < 0.5 ?
(1 - sqrt(1 - pow(2 * x, 2))) / 2 :
(sqrt(1 - pow(-2 * x + 2, 2)) + 1) / 2;
},
easeInElastic: function(x) {
return x === 0 ? 0 : x === 1 ? 1 :
-pow(2, 10 * x - 10) * sin((x * 10 - 10.75) * c4);
},
easeOutElastic: function(x) {
return x === 0 ? 0 : x === 1 ? 1 :
pow(2, -10 * x) * sin((x * 10 - 0.75) * c4) + 1;
},
easeInOutElastic: function(x) {
return x === 0 ? 0 : x === 1 ? 1 : x < 0.5 ?
-(pow(2, 20 * x - 10) * sin((20 * x - 11.125) * c5)) / 2 :
pow(2, -20 * x + 10) * sin((20 * x - 11.125) * c5) / 2 + 1;
},
easeInBack: function(x) {
return c3 * x * x * x - c1 * x * x;
},
easeOutBack: function(x) {
return 1 + c3 * pow(x - 1, 3) + c1 * pow(x - 1, 2);
},
easeInOutBack: function(x) {
return x < 0.5 ?
(pow(2 * x, 2) * ((c2 + 1) * 2 * x - c2)) / 2 :
(pow(2 * x - 2, 2) * ((c2 + 1) * (x * 2 - 2) + c2) + 2) / 2;
},
easeInBounce: function(x) {
return 1 - bounceOut(1 - x);
},
easeOutBounce: bounceOut,
easeInOutBounce: function(x) {
return x < 0.5 ?
(1 - bounceOut(1 - 2 * x)) / 2 :
(1 + bounceOut(2 * x - 1)) / 2;
}
};

使用很简单

1
el.style.left = begin + easeSetting.easeInOutElastic(per) * distance + "px";

但是使用setInterval或setTimeout定时修改DOM、CSS实现动画比较消耗资源,照成页面比较卡顿,所以我们选择使用requestAnimationFrame得到连贯的逐帧动画。

3.requestAnimationFrame

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function draw() {
var per = (new Date - startTime) / duration;
var left = begin + easeSetting.easeInOutElastic(per) * distance;
el.style.left = left + "px";
if (progress < end) {
requestAnimationFrame(draw);//重绘UI
}
}
var requestAnimationFrame = window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame || window.msRequestAnimationFrame || function(callback) {
window.setTimeout(callback, 1000 / 60);
},
startTime = window.mozAnimationStartTime || Date.now(),
progress = 0;
requestAnimationFrame(draw);

我们在上面的例子中去兼容所有的浏览器,但是这个还不是很完美,司徒正美给出了几个解决方案,点击这里进行查看。 requestAnimationFrame动画控制详解一文中也提供了几中解决方案,我把支持包括兼容ios6的例子写在这里,以供参考:

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
// requestAnimationFrame polyfill by Erik Möller.
// Fixes from Paul Irish, Tino Zijdel, Andrew Mao, Klemen Slavič, Darius Bacon

// MIT license
if (!Date.now)
Date.now = function() { return new Date().getTime(); };

(function() {
'use strict';
var vendors = ['webkit', 'moz'];
for (var i = 0; i < vendors.length && !window.requestAnimationFrame; ++i) {
var vp = vendors[i];
window.requestAnimationFrame = window[vp+'RequestAnimationFrame'];
window.cancelAnimationFrame = (window[vp+'CancelAnimationFrame']
|| window[vp+'CancelRequestAnimationFrame']);
}
if (/iP(ad|hone|od).*OS 6/.test(window.navigator.userAgent) // iOS6 is buggy
|| !window.requestAnimationFrame || !window.cancelAnimationFrame) {
var lastTime = 0;
window.requestAnimationFrame = function(callback) {
var now = Date.now();
var nextTime = Math.max(lastTime + 16, now);
return setTimeout(function() { callback(lastTime = nextTime); },
nextTime - now);
};
window.cancelAnimationFrame = clearTimeout;
}
}());

4.动画库简单介绍

  • css库 – animate.css
    使用很简单,写在类名中即可
1
2
<link rel="stylesheet" href="animate.min.css">
<h1 class="animated infinite bounce">Example</h1>
  • js库velocity.js
1
2
3
4
5
6
7
8
9
10
<script src="http://cdn.bootcss.com/jquery/1.12.4/jquery.js"></script>
<script src="velocity.js"></script>
<script>
$('#move').velocity({opcity: 0.5})
.delay(1000).velocity({left:"+=400px"})
.velocity({rotateY:"360deg"},1000)
.fadeOut('slow', function() {
console.log("fadeout")
});;
</script>

参考阅读:

  • CSS vs JS动画:谁更快?
  • https://github.com/gdsmith/jquery.easing
  • http://gsgd.co.uk/sandbox/jquery/easing/
  • window.requestAnimationFrame–MDN
  • 《司徒正美-javascript框架设计–第十四章 动画引擎》
  • requestAnimationFrame动画控制详解
  • CSS3动画那么强,requestAnimationFrame还有毛线用?
  • 玩转HTML5移动页面(动效篇)

写给前端er的TCP/IP知识及《图解TCP/IP》读书笔记

发表于 2016-11-08 | 更新于 2018-11-30 | 分类于 HTTP/TCP/IP

1.分层

OSI参考模型分为7层,TCP/IP分为四层。

2.物理设备介绍

3.传输过程

4.分层介绍

4.1 数据链路层

几个关键的相关技术

  • MAC地址:用于识别数据链路层中互连的节点,在使用网卡(NIC)的情况下,MAC地址会烧入在ROM中
  • 以太网(Ethernet)
    以太网帧式,前端是前导码部分,后面是帧的本体


    帧尾叫做FCS,用来检测帧信息是否完整

4.2 网路层

4.2.1 IP协议–无连接型

  • 数据链路层和IP层的区别:

    1. IP地址的分类


    A类:0.0.0.0 ~ 127.0.0.0 【127为回环测试地址,如127.0.0.0为本机地址】
    B类:128.0.0.1 ~ 191.255.0.0
    C类:192.0.0.0 ~ 233.255.255.0
    D类:224.0.0.0 ~ 239.255.255.0 【用于多播】

    2. 单播、广播、多播

    单播:一对一
    广播:会被路由器屏蔽
    【例如:192.168.0.0/24广播地址为192.168.0.255/24】
    多播:能通过路由器,D类IP地址,从224.0.0.0 ~ 239.255.255.255
    其中224.0.0.0到224.0.0.255不需要路由控制,在同一个链路中能实现多播。

    3. 解决IP地址有限:


    标识方法:
    方法1:

    方法2:

    4. IP分片:

    数据链路不同,最大的传输单元(MTU)不同,所以需要对IP分片进行处理。分片只能在目标主机中进行重组。
  • ICMP通知MTU大小
    路径MTU发现机制(UDP情况下)

    路径MTU发现机制(TCP情况下)不同于上

    5. IPv6

    IP地址长度为128位,以每18比特为一组进行标记,如果出现连续的0,用“::”代替
  • IPv6地址结构:

    全局单播地址是世界上唯一的地址

    6. IPv4首部

    IP首部+IP载荷(数据)组成:

    7. IPv6首部

4.2.2 IP协议相关技术

1. DNS

管理主机名和IP地址之间对应关系的系统,叫做DNS系统。

  • DNS查询:

    第三步 会将IP地址信息暂时保存到缓存中,减少每次查询时的性能消耗。
    DNS的主要记录包括很多类型的数据,比如类型A值主机名的IP地址,PTR指IP地址的反向解析,即IP地址检索的主机名。

    2. ARP

    IP地址到Mac地址解析

    3.ICMP

    主要功能是确认IP包是否成功送达目的地址,通知在发送过程当中IP包被废弃的原因,改善网络的设置等。

    4.DHCP

    动态设置ip地址

4.3 TCP/UDP

  • TCP首部格式
  • 三次握手
  • 识别多个请求
  • 套接口

4.4 应用层

应用层有SSH,FTP,HTTP,TLS/SSL等

  • ftp使用两条TCP连接
  • javascript,CGI

React Router的一个完整示例

发表于 2016-11-04 | 更新于 2018-11-30 | 分类于 前端技术

本博文提供一个单网页结构网页(SPA)使用React Router路由控制跳转的完整例子。

可以在我的github 中clone或者fork
https://github.com/zrysmt/react-demo/tree/master/demo03

关于配置可以查看我之前的一篇博客:一步一步进入React的世界(React+Webpack+ES6组合配置)。

1.整个目录结构

  • build是编译后的文件夹
  • src 放入源码
    • components组件
      • global 通用组件和SCSS
      • … 分模块
    • app.js入口
  • index.html

2.源码

关于源码可以在开头给出的github中找到详细的完整例子,这里就介绍重要的几个文件源码
记住要安装react-router

1
npm i react-router -S

2.1 index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<title>Our Home,Our Heart</title>
<meta name="viewport" content="width=device-width,initial-scale = 1.0,user-scalable=no">
</head>

<body>
<div id="content">
</div>
<script src="build/bundle.js"></script>
</body>
</html>

2.2 入口文件app.js

关于react router的基础知识我们可以参考阮一峰老师的博客作为入门指导。

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
import React from 'react';
import ReactDOM from 'react-dom';
import {Router,Route,IndexRoute,hashHistory} from 'react-router';

import './components/global/global.scss';

import Nav from './components/global/menu';
import Home from './components/home/home';
import Story from './components/story/story';


class App extends React.Component{
render(){
return(
<div>
<Nav/>
{this.props.children}
</div>
)
}
}

ReactDOM.render((
<Router history={hashHistory}>
<Route path="/" component={App}>
<IndexRoute component={Home}/>
<Route path="/Story" component={Story}/>
</Route>
</Router>
),document.body
);

简单解释下:
组件App除了包含Nav组件,还应该包括主体内容
当使用index.html访问的时候,是在项目根目录下,这样会先加载APP组件,APP组件包含{this.props.children},便会加载<IndexRoute/>里面定义的组件Home。用户访问’/‘相当于:

1
2
3
4
<App>
<Nav/>
<Home/>
</App>

2.3 Nav组件

/components/global/menuLi.jsx
/components/global/menu.jsx

  • 最小一块组件menuLi.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React from 'react';
import {Link} from 'react-router';
class MenuLi extends React.Component{
render(){
let linkTo = this.props.name =="Home"?"/":"/"+this.props.name;
return (
<li>
<Link to={linkTo}>
{this.props.name}
</Link>
</li>
);
}
}
export default MenuLi;

Link组件用于取代<a>元素,生成一个链接,允许用户点击后跳转到另一个路由

  • Nav组件 menu.jsx
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
import React from 'react';
import ReactDOM from 'react-dom';
import MenuLi from './menuLi';
import './menu.scss';

let menuLis = ["Home","Story","Travel","TimeLine","Future"];
class MenuUl extends React.Component{
render(){
return(
<ul>
{
menuLis.map(function(menuLi) {
return <MenuLi name={menuLi}/>
})
}
</ul>
);
}
}
class Nav extends React.Component{
render(){
return(
<nav>
<div id="menu">
<MenuUl/>
</div>
</nav>
)
}
}
export default Nav;

2.4 Home组件

/components/home/home.jsx,示例比较简单

1
2
3
4
5
6
7
8
9
10
11
import React from 'react';
import ReactDOM from 'react-dom';
import "./home.scss";
class Home extends React.Component{
render(){
return (
<h5>这是home</h5>
);
}
}
export default Home;

2.5 Story组件

1
2
3
4
5
6
7
8
9
10
11
12
import React from 'react';
import ReactDOM from 'react-dom';
import "./story.scss";

class Story extends React.Component{
render(){
return (
<h5>这是story</h5>
);
}
}
export default Story;

其余几个组件不一一列出了

可以在我的github 中clone或者fork,查看完整的例子代码

参考阅读:

  • React Router 使用教程–阮一峰
1234…7

Ruyi Zhao

70 日志
5 分类
52 标签
© 2018 Ruyi Zhao
由 Hexo 强力驱动 v3.8.0
|
主题 – NexT.Pisces v6.5.0