PAJK-FE BLOG

我是副标题


  • 首页

  • 分类

  • 关于

  • 归档

  • 标签

  • 搜索

尤大live笔记

发表于 2017-06-04 | peanut | 分类于 browser | | 阅读次数

vue2.0 源码

vue2.0 源码学习。

框架选型

框架选型不谈场景都是刷流氓,不同的场景下、开发者群体、偏好不一致,多种方案的并存是必然的有益的。
如何选型:经验和对框架工具背后想要解决的问题深入理解

组件

主流框架都以组件作为基本的抽象单元,组件的发展史 page->application,模块封装.
react 揭示了组件可以是函数,
组件的分类

  • 接入型 container
  • 展示型 component
  • 交互型 比如各类加强版的表单组件,通常强调复用
  • 功能型 比如 <router-view>,<transition>,作为一种扩展、抽象机制存在。

模板和 jsx 对比,jsx 灵活性高,模板在以展示型为主的场景下不差,jsx 在功能性组件的书写远超模板
colocation: 把该放的东西放一起和以前的 html、js、css 分离相对应

变化侦测机制和渲染机制

渲染中的申明式和命令式
命令式的维护问题,申明式申明数据和 dom 的映射关系,不需要手动去维护操作
view = render(state)
vue的模板也是编译成渲染函数,模板和 jsx 的本质是一样的,具体实现可以是 vdom 也可以是细粒度的绑定如 vue1;
变化侦测:变化侦测 ppt。

被人诟病,为啥 vue 的声明式写法就是推崇的? 前者的作用域的全局的,vue 模板是在特定的作用域中。另外的原因是:colocation,jsx 或模板中处理器是在相关联的文件中,还是维护性问题。
变化侦测分为push 和 pull,react 中的setState和 angular1中的脏检查都是pull,对于数据的变化,要有钩子来触发更新,暴力的比对如 react 中的 vdom 的 diff,暴力的遍历比对,找出了变化的部分;push如 vue 的响应式和 rxjs,在数据变动后,能确切的知道具体什么变了,相对细粒度的更新,pull 是粗粒度的,所以要帮助 pull 来提高性能,如 purecomponent 、shouldComponenetUpdata等,push 是细粒度的,我们知道的信息多一点,代价就是每一个绑定都要有 watch,带来内存的开销和依赖追踪的开销。
vue2选择中等粒度的更新,组件级别是 push,每一个组件是一个响应式的 watcher,当数据变化的直接知道具体哪些组件变了,组件内部是 vdom 的比对
本质的区别是:用侦测成本换一定程度的自动优化,各有利弊

状态管理

flux reflux redux mobx vuex rxjs
本质:从原事件映射到状态的改变再到 ui 的变化,申明式的渲染已经解决了状态到 ui 的变化,状态管理就是如何管理将事件源映射到状态变化的这个过程,如何将映射的过程从视图组件剥离出来,如何组织这部分代码,提高维护性,这是状态管理要解决的问题。
redux 和 mobx,两种范式: redux 函数式、immutable;mobx 和 vuex数据可以变,响应式,已经做好了申明式的副作用
把 Vue 当 redux 用
把 Vue 当 MobX 用
都没有回答如何处理异步,redux 中间件生态,vuex 和 mobx action 中搞
简单的 curd 应用没必要,复杂的如实时服务端推送,竞态,rxjs
组件的全局状态和局部状态如何区分,没有很明显的界限,Elm 中的解决方案

路由

url 映射到组件树的过程
react-router 4 用组件自身来做路由,功能性组件,路由表分散,耦合在组件中
web 路由和 app 路由

css 方案

主流的 CSS 方案

  • 跟 JS 完全解耦,靠预处理器和比如 BEM 这样的规范来保持可维护性,偏传统
  • CSS Modules,依然是 CSS,但是通过编译来避免 CSS 类名的全局冲突
  • 各类 CSS-in-JS 方案,React 社区为代表,比较激进
  • Vue 的单文件组件 CSS,或是 Angular 的组件 CSS(写在装饰器里面),一种比较折中的方案

传统 css 的一些问题:

  1. 作用域
  2. Critical CSS
  3. Atomic CSS
  4. 分发复用
  5. 跨平台复用

css-in-js

构建工具

grunt gulp webpack
构建工具解决的其实是几方面的问题:

  • 任务的自动化
  • 开发体验和效率(新的语言功能,语法糖,hot reload 等等)
  • 部署相关的需求
  • 编译时优化

关于部署

服务端数据通信

传统restful接口、数据直连、实时推送
实时推送方案:Meteor
数据关联: GraphQL,复杂数据的关联比 restful 表达力更强,

服务端渲染

vue 服务端渲染

跨平台渲染

weex rn, 框架的渲染机制与 dom 解耦,不一定要 vdom,把框架节点操作与原生的渲染引擎对接

新规范

web component新规范

babel拾遗

发表于 2017-06-03 | 布谷 | 分类于 workflow | | 阅读次数

我们在使用 babel的时候,常常搞不清楚需要安装哪些依赖,babel本身繁杂的体系也容易让人混淆,这里就自己看到的部分稍作解释,欢迎补充

1、 .babelrc

这是 babel 的配置文件,rc 表示自动执行,主要配置项有:presets, plugins, ignore等; 示例:

1
2
3
4
5
{
"presets": ["es2015", "react"],
"plugins": ["transform-decorators-legacy"]
"ignore": ["lib/*.js"]
}

其中presets是一系列plugins的集合。
例如,编译 react 项目中的 jsx 语法,既可以使用 plugin: babel-plugin-transform-react-jsx,也可以使用包含该 plugin 的 preset: react

2、 babel-core

看名字,babel-core似乎是必备的依赖,其实babel-core提供了一些编译方法供手工调用;也就是说,我们常规的使用 webpack 或者 babelrc 的业务项目,是不需要安装babel-core的依赖的

3、 babel-polyfill

OK,说道正题了, 首先我们知道 babel 的作用,是用来编译es6,es7 等浏览器尚未全面支持的新语法和 api。
实际上呢,babel 为它的编译功能做了分类:

  • presets 和 plugins 负责编译箭头函数等新语法
  • babel-polyfill负责编译新的 api 如:Promise,Set等;以及定义在全局对象上的方法如 Object.assign

babel-polyfill 实现编译新 api 以及全局方法的方式是污染全局变量,以及修改原生对象的 prototype,这在一般的业务项目中,并不会引起太大的问题。
但是如果我开发的是一个库,可能被引入到各种第三方应用中,这就大大的不好了;所以不嫌麻烦的 babel 又给大家准备了新套餐 babel-runtime

4、 babel-runtime 与 babel-plugin-transform-runtime

babel-runtime 的业务范围和 babel-polyfill 一致(稍有不同,后面会说到),不过使用的方式是提供 helper 函数,嘛意思呢,也就是说,
如果你使用Promise,那么我定义一个叫做Promise的对象,供你在需要的地方引入;
如果你使用Object.assign, 那么我写一个同样功能的方法,供你引入使用

babel-runtime通过这种方式,避免污染全局变量和修改原生对象的prototype,例如,下面是一个 babel-runtime 对 { [name]: 'JavaScript' } 的编译后代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
'use strict';
function _defineProperty(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true
});
} else {
obj[key] = value;
}
return obj;
}
var obj = _defineProperty({}, 'name', 'JavaScript');

OK, 现在我们知道babel-runtime的作用了,但是我们总不能在业务代码中一个个手工引入这些 helper 函数吧,所以 babel 提供了 babel-plugin-transform-runtime的插件,用于编译时自动引入 helper 函数。

并且 babel-plugin-transform-runtime 本身依赖了 babel-runtime,所以在项目中使用的时候不需要再额外指定 babel-runtime 的依赖

思考题:
对于实例方法,如 "foobar".includes("foo"), babel-runtime可以实现编译吗?

答案是不可以,因为实例方法显然必须通过修改原生对象的prototype才能实现

5、合理的配置

我们现在来看看最佳的babel配置是怎样的

  • presets 和 plugins 可以冗余一点,babel会按需使用
  • 如果用到 Promise, Set,Map 等新api,增加 babel-plugin-transform-runtime 的依赖
  • 如果使用到类似 "foobar".includes("foo") 等实例方法,额外引入对应的 polyfill(参考:https://github.com/zloirock/core-js)

最后,babel的文档真特么烂!

如果你觉得阅读本文有收获,请给我打赏,谢谢

react事件机制源码分析

发表于 2017-06-03 | peanut | 分类于 react | | 阅读次数

React事件机制

本文主要从源码角度讨论以下问题

  • react事件和原生 dom 事件的区别,混用的时候的问题。
  • react 是如何在虚拟 dom 上完成事件的绑定,绑定在哪?
  • 既然 react 采用的事件代理,那它又是如何做到事件冒泡的效果,如何阻止冒泡的,提供的阻止冒泡的 api 和原生阻止冒泡的 api 有何关联?
  • react 存在事件捕获吗?他是真正的事件捕获吗?又是如何做到捕获的效果?
  • react 事件的插件机制及合成的事件对象(reatc 支持的7个事件插件)

react事件绑定阶段(绑定阶段和事件处理器存储阶段)

绑定阶段

1:当我们在 jsx 上绑定了 react 提供的事件绑定方式,那么绑定的该事件是如何被注册到事件系统的呢?组件的 mounting 和 updata都会去解析 jsx 上绑定的事件,这里我们只看 mounting阶段;
react事务: 其源码在src/shared/utils/Transaction.js. (以下用到的)transaction: react的方法执行是基于事务的,Transaction就是给需要执行的方法fn用wrapper封装了 initializeAll 和 closeAll 方法。且支持多次封装。再通过 Transaction 提供的 perform 方法执行。 perform执行前,调用所有initializeAll 方法。perform 方法执行后,调用所有closeAll方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  mountComponent: function(rootID, transaction, context) {
// 这里只取关键部分分析
this._rootNodeID = rootID;

var props = this._currentElement.props;
var mountImage;
if (transaction.useCreateElement) {
} else {
// 创建每个元素的开始标签和解析元素上的props,如 style、事件、等
var tagOpen = this._createOpenTagMarkupAndPutListeners(transaction, props);
// 子标签作为 content 解析,会进入到mountChildren,mountChildren作用如mountComponent 同样会解析标签上的一些属性
var tagContent = this._createContentMarkup(transaction, props, context);
if (!tagContent && omittedCloseTags[this._tag]) {
mountImage = tagOpen + '/>';
} else {
mountImage =
tagOpen + '>' + tagContent + '</' + this._currentElement.type + '>';
}
}
return mountImage;
},

1
2
3
4
5
6
7
8
9
10
11
_createOpenTagMarkupAndPutListeners: function(transaction, props) {
var ret = '<' + this._currentElement.type;
// 这里的 props 包含标签上所有可以定义的属性,只分析事件
for (var propKey in props) {
// registrationNameModules 这是加载 react 应用时,注册到 react EventPluginHub这个事件插件中心所有 react 支持的事件类型
if (registrationNameModules.hasOwnProperty(propKey)) {
if (propValue) {
enqueuePutListener(this._rootNodeID, propKey, propValue, transaction);
}
}
}

2:主要来看这个enqueuePutListener,这个函数主要完成事件绑定在 document 对象上,和把相关的事件信息进行存储。
这里我们可以看出 jsx 中申明的事件确实绑定在 document 对象上了,在每一个标签解析完毕后,会把 domId,事件名称、事件处理函数存储在后面提到的listenBank这个对象中;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function enqueuePutListener(id, registrationName, listener, transaction) {
var container = ReactMount.findReactContainerForID(id);
if (container) {
// 确保元素类型是节点类型,并且取到 document 节点
var doc = container.nodeType === ELEMENT_NODE_TYPE ?
container.ownerDocument :
container;
// 在 document 对象上完成事件的绑定
listenTo(registrationName, doc);
}
// 该方法会在jsx中每一个嵌套的dom标签渲染完毕(依次往上递归)会去调用
transaction.getReactMountReady().enqueue(putListener, {
id: id,
registrationName: registrationName,
listener: listener,
});
}

3:listenTo方法,主要是处理一些浏览器冒泡和捕获兼容性的问题,同时可以看出,同种类型的事件只绑定一次,保证了效率

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
listenTo: function(registrationName, contentDocumentHandle) {
var mountAt = contentDocumentHandle;
var isListening = getListeningForDocument(mountAt);
var dependencies =
EventPluginRegistry.registrationNameDependencies[registrationName];

var topLevelTypes = EventConstants.topLevelTypes;
for (var i = 0; i < dependencies.length; i++) {
var dependency = dependencies[i];
// 以下分支只看最后一个,上面的几个都是特殊处理过的,如兼容性什么的
if (!(
isListening.hasOwnProperty(dependency) &&
isListening[dependency]
)) {
if (dependency === topLevelTypes.topWheel) {
} else if (dependency === topLevelTypes.topScroll) {
} else if (dependency === topLevelTypes.topFocus ||
dependency === topLevelTypes.topBlur) {
} else if (topEventMapping.hasOwnProperty(dependency)) {
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(
dependency,
topEventMapping[dependency],
mountAt
);
}
isListening[dependency] = true;
}
}
},

4:react 中会提供捕获的api,如onClickCapture, 在事件收集阶段,react都统一处理了,如onClick、 onClickCapture转化为统一的命名如topClcik,从这我们也能察觉到,这个捕获的 api 是不是真正意义上的捕获?trapBubbledEvent 完成了事件绑定到 document 上的任务,后续具体的事件
执行,要注意 ReactEventListener.dispatchEvent.bind(null, topLevelType),是如何在一个节点上模拟出冒泡和捕获的效果。

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
trapBubbledEvent: function (topLevelType, handlerBaseName, element) {
if (!element) {
return null;
}
return EventListener.listen(
element, // 绑定到的DOM目标,也就是document
handlerBaseName, // eventType
ReactEventListener.dispatchEvent.bind(null, topLevelType)); // document上的原生事件触发后回调
},

listen: function listen(target, eventType, callback) {
if (target.addEventListener) {
// 将原生事件添加到target这个dom上,也就是document上。
target.addEventListener(eventType, callback, false);
return {
// 删除事件,这个由React自己回调,不需要调用者来销毁。但仅仅对于React合成事件才行
remove: function remove() {
target.removeEventListener(eventType, callback, false);
}
};
} else if (target.attachEvent) {
// attach和detach的方式
target.attachEvent('on' + eventType, callback);
return {
remove: function remove() {
target.detachEvent('on' + eventType, callback);
}
};
}
},

总结一下: 通过以上阶段react初步完成同种类型事件的单次绑定且绑定在document对象,接下来我们再看相关的事件信息如事件名,domid,事件处理器是如何存储在listenBank 这个事件对象中的。接着上面的 enqueuePutListener()中的transaction.getReactMountReady()方法

事件句柄存储阶段

添加事件处理器到listenerBank这个统一的事件处理器对象中,jsx中每一个嵌套的dom标签渲染完毕(依次往上递归)会去调用

1
2
3
4
5
6
putListener: function(id, registrationName, listener) {
// eg: listenerBank['onclick'][nodeId]
var bankForRegistrationName =
listenerBank[registrationName] || (listenerBank[registrationName] = {});
bankForRegistrationName[id] = listener;
},

事件触发执行阶段(分复合事件对象合成阶段和事件函数队列执行)

看到这其实我们就明白了,react 的这一套事件机制其实还是在利用原生事件的冒泡机制,所以原生事件和 react 事件混合使用会有一些坑,比如说在 componentDidMount拿到真实 dom 通过原生事件的方式来绑定一些事件,阻止冒泡,那我们通过 react 绑定的事件是不会响应的等等,稍后一并总结

复合事件对象合成阶段

1:先说下 react 所支持的事件插件,BeforeInputEventPlugin, ChangeEventPlugin, DOMEventPluginOrder, EnterLeaveEventPlugin,
SelectEventPlugin, SimpleEventPlugin, TapEventPlugin, 总共7个事件插件。SimpleEventPlugin插件是我们常用的,支持的事件类型有焦点事件、键盘事件、鼠标事件、拖放事件、触摸事件、鼠标滚轮事件、ui事件, 这些事件类型继承自SyntheticEvent(提供事件对象中相同的部分)。每种事件类型合成的事件对象参数有相同的部分又有定制的部分,当然一个操作可能会引起多种事件类型,如鼠标点击会引起鼠标事件和 ui 事件。所以 reatc 会根据特定的事件类型去合成特定的事件对象(可能是多个事件类型复合而成)。就以我们常用的SimpleEventPlugin插件支持的鼠标事件大概说一下,鼠标事件又分很多种,比如单击事件、右键菜单事件、鼠标进出等、下图为鼠标事件对象中的一些参数,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var MouseEventInterface = {
screenX: null,
screenY: null,
clientX: null,
clientY: null,
ctrlKey: null,
shiftKey: null,
altKey: null,
metaKey: null,
getModifierState: getEventModifierState,
button: function(event) {
// 一些兼容性的处理
},
buttons: null,
relatedTarget: function(event) {
// 同上
},
// "Proprietary" Interface.
pageX: function(event) {
},
pageY: function(event) {
},
};

SyntheticEvent: 提供事件对象共同的部分,和在原型上挂一些复合事件对象的方法如preventDefault、stopPropagation、persist、destructor(看起来是不是很熟悉,这就是 react 提供出来的关于事件对象的 api), 注意一下this.isPropagationStopped、this.isPropagationStopped,这是后续我们会看到的一个关键标识。

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
var EventInterface = {
type: null,
currentTarget: emptyFunction.thatReturnsNull,
eventPhase: null,
bubbles: null,
cancelable: null,
timeStamp: function(event) {
return event.timeStamp || Date.now();
},
defaultPrevented: null,
isTrusted: null,
};

assign(SyntheticEvent.prototype, {

preventDefault: function() {
this.defaultPrevented = true;
var event = this.nativeEvent;
if (!event) {
return;
}
if (event.preventDefault) {
event.preventDefault();
} else {
event.returnValue = false;
}
// 注意这个方法,这是才是 react 提供出来阻止默认事件的方法,以上为了在具体的触发元素上同步信息
this.isDefaultPrevented = emptyFunction.thatReturnsTrue;
},

stopPropagation: function() {
var event = this.nativeEvent;
if (!event) {
return;
}
if (event.stopPropagation) {
event.stopPropagation();
} else {
event.cancelBubble = true;
}
// 注意这个方法,这是才是 react 提供出来阻止冒泡的方法,以上为了在具体的触发元素上同步信息
this.isPropagationStopped = emptyFunction.thatReturnsTrue;
},
persist: function() {
this.isPersistent = emptyFunction.thatReturnsTrue;
},
isPersistent: emptyFunction.thatReturnsFalse,
destructor: function() {
var Interface = this.constructor.Interface;
for (var propName in Interface) {
this[propName] = null;
}
this.dispatchConfig = null;
this.dispatchMarker = null;
this.nativeEvent = null;
},

});

2:以上只是介绍了 react 的事件插件,下面我们继续看看如何合成这个复合事件对象,在第一部分我们已经完成了事件的绑定和事件句柄的存储,接下来就分析一下执行阶段中的事件对象的合成,react 的这一套事件机制是建立在原生事件的基础上的,我们在具体的元素上如 button 点击了一下,那么他会通过事件流中的冒泡阶段到达 document 上,我们在第一阶段已经完成了 document 上该类型事件的绑定,剩下的就是去执行这个队列,接上面的trapBubbledEvent,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// topLevelType:讲相关的事件名称转为 top 开头的,如topClick。前面讲到过的
// nativeEvent: 用户触发click等事件时,浏览器传递的原生事件
dispatchEvent: function (topLevelType, nativeEvent) {
// 静态池、批处理的为了提高性能的不说了
var bookKeeping = TopLevelCallbackBookKeeping.getPooled(topLevelType, nativeEvent);
try {
// 放入批处理队列中,React事件流也是一个消息队列的方式
ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
} finally {
TopLevelCallbackBookKeeping.release(bookKeeping);
}
}
handleTopLevel: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {
// 采用对象池的方式构造出合成事件。不同的eventType的合成事件不同
var events = EventPluginHub.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget);
// 批处理队列中的events
runEventQueueInBatch(events);
}

3 根据事件类型构造事件对象,不同的事件类型合成的事件对象参数上面已经说过,主要看事件对象中参数中的 _dispatchIDs 和 _dispatchListeners的生成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
extractEvents: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {
var events;
// EventPluginHub可以存储React合成事件的callback,也存储了一些plugin,这些plugin在EventPluginHub初始化时就注册就来了
var plugins = EventPluginRegistry.plugins;
for (var i = 0; i < plugins.length; i++) {
var possiblePlugin = plugins[i];
if (possiblePlugin) {
// 根据eventType构造不同的合成事件SyntheticEvent, 如SimpleEventPlugin
var extractedEvents = possiblePlugin.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget);
if (extractedEvents) {
// 将构造好的合成事件extractedEvents添加到events数组中,这样就保存了所有plugin构造的合成事件
events = accumulateInto(events, extractedEvents);
}
}
}
return events;
},

4:当我们在具体 dom 上触发了事件(click),会先从根组件到具体触发事件的 dom向下遍历,依次取出我们之前在listenBank 中存储的相关捕获类型的事件的监听器(onClickCapture)和domID,然后在从具体 dom 向上遍历至根组件,依次取出冒泡类型的监听器(onClick)和 domId, 分别挂载在事件对象中的 _dispatchListeners和 _dispatchIDs中,至此这个完整的事件对象合成完毕。accumulateDirectionalDispatches完成挂载。

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
function accumulateTwoPhaseDispatchesSkipTarget(events) {
// 对合成的 events 分别执行 accumulateTwoPhaseDispatchesSingleSkipTarget
forEachAccumulated(events, accumulateTwoPhaseDispatchesSingleSkipTarget);
}
// traverseTwoPhaseSkipTarget 会在遍历 dom 的结构中依次调用accumulateDirectionalDispatches
function accumulateTwoPhaseDispatchesSingleSkipTarget(event) {
if (event && event.dispatchConfig.phasedRegistrationNames) {
EventPluginHub.injection.getInstanceHandle().traverseTwoPhaseSkipTarget(
event.dispatchMarker,
accumulateDirectionalDispatches,
event
);
}
}
// domID,正在遍历的 dom,upwards向上还是向下,模拟捕获和冒泡的顺序,event复合事件对象
function accumulateDirectionalDispatches(domID, upwards, event) {
var phase = upwards ? PropagationPhases.bubbled : PropagationPhases.captured;
var listener = listenerAtPhase(domID, event, phase);
if (listener) {
event._dispatchListeners =
accumulateInto(event._dispatchListeners, listener);
event._dispatchIDs = accumulateInto(event._dispatchIDs, domID);
}
}
// 先向下,再向上
traverseTwoPhaseSkipTarget: function(targetID, cb, arg) {
if (targetID) {
traverseParentPath('', targetID, cb, arg, true, true);
traverseParentPath(targetID, '', cb, arg, true, true);
}
},
function traverseParentPath(start, stop, cb, arg, skipFirst, skipLast) {
start = start || '';
stop = stop || '';
var traverseUp = isAncestorIDOf(stop, start);
// Traverse from `start` to `stop` one depth at a time.
var depth = 0;
var traverse = traverseUp ? getParentID : getNextDescendantID;
for (var id = start; /* until break */; id = traverse(id, stop)) {
var ret;
if ((!skipFirst || id !== start) && (!skipLast || id !== stop)) {
ret = cb(id, traverseUp, arg);
}
if (ret === false || id === stop) {
// Only break //after// visiting `stop`.
break;
}
}
}

执行阶段

1:剩下的就是取出复合事件对象中的_dispatchListeners这个队列来执行,在SyntheticEvent中我们说到,react 提供的几个关于事件对象的几个 api,如preventDefault、stopPropagation、persist,我们说过注意一下this.isPropagationStopped、this.isPropagationStopped,以stopPropagation 这个 api 为列,在事件处理其中我们调用 e.stopPropagation()、e.preventDefault(),这两个虽然看起来和原生的 api 长得一样(也仅仅是长得一样),其实调用他们时,该事件流已经传播到了 document上了,为了保持和原生事件对象的参数同步(毕竟复合对象中有个nativeEventz这个里面是原生的事件对象)所以拿到原生对象再去调用一下相关的方法,并且设置了event.isPropagationStopped = 返回值为 true 的函数,所以在执行这个事件处理器队列的时候, 会 break,达到了阻止冒泡的效果(并非真正的阻止冒泡,真正的冒泡是存在多级嵌套的父子关系的元素,依次传递)。同时可以看到事件处理器默认有两个参数,复合事件对象和 domid。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function executeDispatchesInOrder(event, simulated) {
var dispatchListeners = event._dispatchListeners;
var dispatchIDs = event._dispatchIDs;
if (Array.isArray(dispatchListeners)) {
for (var i = 0; i < dispatchListeners.length; i++) {
if (event.isPropagationStopped()) {
break;
}
// Listeners and IDs are two parallel arrays that are always in sync.
executeDispatch(event, simulated, dispatchListeners[i], dispatchIDs[i]);
}
} else if (dispatchListeners) {
executeDispatch(event, simulated, dispatchListeners, dispatchIDs);
}
event._dispatchListeners = null;
event._dispatchIDs = null;
}

react 事件系统架构图

现在我们再来看看 fb 官方给出的 react 整个事件系统的架构,我们就能很容易的看明白整个流程。

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
Overview of React and the event system:
*
* +------------+ .
* | DOM | .
* +------------+ .
* | .
* v .
* +------------+ .
* | ReactEvent | .
* | Listener | .
* +------------+ . +-----------+
* | . +--------+|SimpleEvent|
* | . | |Plugin |
* +-----|------+ . v +-----------+
* | | | . +--------------+ +------------+
* | +-----------.--->|EventPluginHub| | Event |
* | | . | | +-----------+ | Propagators|
* | ReactEvent | . | | |TapEvent | |------------|
* | Emitter | . | |<---+|Plugin | |other plugin|
* | | . | | +-----------+ | utilities |
* | +-----------.--->| | +------------+
* | | | . +--------------+
* +-----|------+ . ^ +-----------+
* | . | |Enter/Leave|
* + . +-------+|Plugin |
* +-------------+ . +-----------+
* | application | .
* |-------------| .
* | | .
* | | .
* +-------------+ .
* .
* React Core . General Purpose Event Plugin System
*/

ReactEventListener:负责事件注册和事件分发。React将DOM事件全都注册到document这个节点上。事件分发主要调用dispatchEvent进行。
ReactEventEmitter:负责每个组件上事件的执行。
EventPluginHub:负责事件的存储,合成事件以对象池的方式实现创建和销毁,大大提高了性能。(核心)
SimpleEventPlugin等plugin:根据不同的事件类型,构造不同的合成事件。

看到这我们应该很容易的能回答之前的问题了,以及开发中遇到的和事件相关的坑大家应该知道为什么了吧。

浅析修饰器(Decorator)在React中的应用

发表于 2017-06-02 | iloveplus | 分类于 react | | 阅读次数

修饰器原理理解

修饰器(Decorator)是一个函数,用来修改类的行为。这是ES7的一个提案,目前Babel转码器已经支持(需安装babel-core和babel-plugin-transform-decorators)。修饰器对类的行为的改变,是代码编译时发生的,而不是在运行时。这意味着,修饰器能在编译阶段运行代码。
下面我们从一个例子来了解修饰器它的内部是怎么运行的:

未使用修饰器:

转码前:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React, { Component } from 'react';

const testable = (target) => {
target.title = '使用了修饰器';
}

class ListDemo extends Component {
render() {
let title = ListDemo.title || '未使用修饰器';
return (
<div>{title}</div>
)
}
}

export default ListDemo;

转码后:

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
var testable = function testable(target) {
target.title = '使用了修饰器';
};

var ListDemo = function (_Component) {
(0, _inherits3.default)(ListDemo, _Component);

function ListDemo() {
(0, _classCallCheck3.default)(this, ListDemo);
return (0, _possibleConstructorReturn3.default)(this, (ListDemo.__proto__ || (0, _getPrototypeOf2.default)(ListDemo)).apply(this, arguments));
}

(0, _createClass3.default)(ListDemo, [{
key: 'render',
value: function render() {
var title = ListDemo.title || '未使用修饰器';
return _react2.default.createElement(
'div',
null,
title
);
}
}]);
return ListDemo;
}(_react.Component);

exports.default = ListDemo;

使用修饰器:

转码前:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React, { Component } from 'react';

const testable = (target) => {
target.title = '使用了修饰器';
}

@testable
class ListDemo extends Component {
render() {
let title = ListDemo.title || '未使用修饰器';
return (
<div>{title}</div>
)
}
}

export default ListDemo;

转码后:

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
var testable = function testable(target) {
target.title = '使用了修饰器';
};

var ListDemo = testable(_class = function (_Component) {
(0, _inherits3.default)(ListDemo, _Component);

function ListDemo() {
(0, _classCallCheck3.default)(this, ListDemo);
return (0, _possibleConstructorReturn3.default)(this, (ListDemo.__proto__ || (0, _getPrototypeOf2.default)(ListDemo)).apply(this, arguments));
}

(0, _createClass3.default)(ListDemo, [{
key: 'render',
value: function render() {
var title = ListDemo.title || '未使用修饰器';
return _react2.default.createElement(
'div',
null,
title
);
}
}]);
return ListDemo;
}(_react.Component)) || _class;

exports.default = ListDemo;

对比转换后的结果,我们发现,使用修饰器等效于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React, { Component } from 'react';

const testable = (target) => {
target.title = '使用了修饰器';
}

class ListDemo extends Component {
render() {
let title = ListDemo.title || '未使用修饰器';
return (
<div>{title}</div>
)
}
}

export default testable(ListDemo) || ListDemo;

其中@testable就是一个修饰器,它修改了ListDemo这个类的行为,为它加上了静态属性title。也就是说,修饰器本质就是编译时执行的函数,其中第一个参数,就是所要修饰的目标类。基本上,修饰器的行为就是下面这样:

1
2
3
4
5
6
7
@decorator
class A {}

// 等同于

class A {}
A = decorator(A) || A;

可是在应用过程中往往只传了一个Class作为参数不够灵活怎么办?如上例若title是可变的,这时我们可以在外层套一个函数,只要最后返回的是一个Decorator即可,而无需管套了多少个函数传多少个参数,修改后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React, { Component } from 'react';

const testable = (title) => (target) => {
target.title = title;
}

@testable('使用了修饰器')
class ListDemo extends Component {
render() {
let title = ListDemo.title || '未使用修饰器';
return (
<div>{title}</div>
)
}
}

export default ListDemo;

理解了上面的例子后,接下来我们来看看修饰器在react中的高阶用法。

修饰器高阶应用

我们知道进入列表页往往需要上拉刷新,下拉加载等通用事件和效果,这时我们可以利用修饰器。如下:

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
import React, { Component } from 'react';
import Refresh from './Refresh';
import More from './More';

/*
* hasLoader: 加载更多功能
* hasRefresh: 下拉刷新功能
* pageSize: 每页数据条数
*/
const loadMore = (hasLoader, hasRefresh, pageSize = 10) => (ComposedComponent) => class LoadMore extends Component {
componentDidMount() {

}

//todo:相关处理逻辑

render() {
if (!hasLoader && !hasRefresh) {
return <ComposedComponent {...this.props} />;
}

return (
<div>
{ hasRefresh && <Refresh /*...*/ />}
<ComposedComponent {...this.props} />
{ hasLoader && <More /*...*/ />}
</div>
);
}
};

@loadMore(true, true, 10)
class ListDemo extends Component {
//...
}

export default ListDemo;

修饰器在react-redux中的应用

1
2
3
4
5
6
7
8
9
10
11
12
13
//定义一个组件
class Home extends React.Component {
//....
}

//以往从状态树取出对应的数据,让后通过props传给组件使用通过react-redux自带的connect()方法
export default connect(state => ({todos: state.todos}))(Home);

//使用装饰器的话就变成这样,似乎简单了很多
@connect(state => ({ todos: state.todos }))
class Home extends React.Component {
//....
}

参考资料

http://es6.ruanyifeng.com/#docs/decorator
http://zhw-island.com/decorator-in-react/
https://www.w3cschool.cn/ecmascript/43uq1q5z.html

浏览器事件链原理

发表于 2017-05-24 | yoution | 分类于 browser | | 阅读次数

浏览器事件链

绑定事件

1
2
3
4
5
//options: 
// capture : Boolean
// once : Boolean
// passive : Boolean
element.addEventListener(type, listener, options | useCapture)
1
2
3
4
//内部结构:
EventListenerMap
key : type
value: [{listener, options}, ...]

触发事件

从target_element上开始触发事件

事件类型

  • Event:CAPTURING_PHASE (捕获事件,从父元素到子元素)
  • Event:AT_TARGET (命中事件,触发元素)
  • Event:BUBBLING_PHASE (冒泡事件,从子元素到父元素)

事件链EventPath

EventPath = [targetElement, …, HTMLBodyElement, HTMLHtmlElement, HTMLDocument, HTMLDocument]
第2个HTMLDocument的currentTarget为DomWindow

事件执行过程

  • CAPTURING_PHASE:path=[EventPath.last,…,EventPath.first+1] ==>currentTarget->fireEventListeners
  • AT_TARGET:path=[EventPath.first] ==>currentTarget->fireEventListeners
  • BUBBLING_PHASE:path=[EventPath.first+1,…,EventPath.last] ==> currentTarget->fireEventListeners

问题

当一个Element上同时绑定capture和bubbling事件时,如何触发?

  • 非At_TARGET的Element,按照正常执行顺序,先执行capture路径,再target,再bubbling路径
  • 在At_TARGET 的Element, 只按照绑定时候的顺序执行,不区分capture和bubbling

原因:

1
2
3
4
5
6
7
8
//fireEventListeners时
//CAPTURING_PHASE和BUBBLING_PHASE过程时,如果eventPhase和useCapture不匹配,
//会被拦截后 continue,
//但是在At_TARGET的过程没有这层拦截
if (event.eventPhase() == Event::CAPTURING_PHASE && !registeredListener->useCapture())
continue;
if (event.eventPhase() == Event::BUBBLING_PHASE && registeredListener->useCapture())
continue;

atom前端实用插件

发表于 2017-05-24 | Rock | 分类于 工具 | | 阅读次数

工欲善其事,必先利其器。

今天给大家推荐atom编辑器的插件安装教程和一些前端实用插件~

查看已安装插件

在terminal输入
apm ls

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Built-in Atom Packages (xxx)
.........省略n多内置插件

下面是已安装的插件
Community Packages (10) /Users/rock/.atom/packages
├── atom-beautify@0.29.24
├── atom-ctags@5.0.0
├── autocomplete-plus@2.35.4
├── colorpicker@0.3.0
├── csscomb@0.3.1
├── emmet-atom@2.4.3
├── javascript-snippets@1.2.1
├── minimap@4.28.2
├── nuclide@0.166.0
└── scss-snippets@0.6.0

安装插件

可以先去
atom安装包搜索页
找到自己喜欢的插件后

1.
apm install packageName

2.

1
Installing autoprefixer to /Users/rock/.atom/packages ✓(这个勾说明已经安装成功了)

3.
apm uninstall packageName (从入门到放弃)

是不是很简单呢?

下面有同学吐槽说 apm install 安装失败了怎么办?

alt

好吧 我再讲讲别的安装方法

1.
输入cd ~/.atom/packages/ (atom路径下packages目录)

2.
找到插件地址后进入git repo跳转的链接
minimap -》 git地址

3.
用git clone 插件目录
git clone https://github.com/atom-minimap/minimap.git

4.
然后可以直接在~/.atom/packages/目录下apm install minimap

或者在packages目录进入package后直接安装 cd minimap && apm install

5.
rm -rf ~/.atom/packages/minimap(暴力删除方法)

实用插件推荐

  • emmet
    zend-coding
  • autoprefixer
    样式兼容补全
  • minimap
    抄袭sublime的代码小地图
  • javascript-snippets
    js代码自动补全
  • react-snippets
    react 补全
  • autocomplete-plus
    js代码补全建议
  • terminal-plus
    terminal for atom
  • atom-beautify
    代码格式化
  • colorpicker
    取色器(好像有bug)
  • hyperclick
    传伟同学推荐的文件跳转
  • fast-eslint
    轻量级eslint 提示

浅谈使用Json Web Token和Cookie的利弊

发表于 2017-05-22 | CP | 分类于 browser | | 阅读次数

Authentication

现在基本上有两种不同的方式来处理服务器端的认证(Authentication)问题。

  1. Cookie-Based Authentication:最常用的是利用cookie来存储认证信息,并包含在发送请求中用于服务器端的核查
  2. Token-Based Authentication:本文主要介绍的Token为基础的认证方式。

Json Web Token(JWT)

JWT是一个以JSON为基准的标准规范,通过JSON来传递消息,具体定义可以参见这里
JWT自身会携带所有的信息,包括payload和signature。
JWT常常通过HTTP的header来传送信息,同时也可以通过URL来传送。

阅读全文 »
12
PAJK-FE

PAJK-FE

平安健康前端博客

17 日志
7 分类
8 标签
GitHub
© 2017 - 2018 PAJK-FE
由 Hexo 强力驱动
主题 - NexT.Mist