沙雕爹爹

Stay foolish, stay humble.


  • Home

  • Archives

lodash源码之debounce/throttle

Posted on 2019-04-22

debounce源码
throttle源码

debounce

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
193
194
195
196
197
198
199
200
import isObject from './isObject.js'
import root from './.internal/root.js'

/**
* 创建一个函数,延迟执行`func`,直到上次`func`执行之后`wait`毫秒,或者下一次浏览器绘制帧。
* 函数返回一个`cancel`函数来取消延迟的`func`的执行,和一个`flush`方法来立即执行。
* 接收一个options对象,用来指定`func`是在`wait`时间间隔的头/尾执行。
* `func`使用最新的传参执行。一组调用只返回最新那次函数调用的计算结果。
*
*
* **Note:** 如果`leading`和`trailing`选项都是`true`,只有当`func`在`wait`时间段内执行超过一次,才会在时间段的尾边界执行。
* 如果`wait`是`0`,并且`leading`是`false`,`func`延迟到下一次tick,类似setTimeout 0。
* 如果`wait`被忽略,并且环境中有`requestAnimationFrame`方法,`func`将延迟到下一次帧绘制时执行(特指16ms)。
* 查看[David Corbacho的文章](https://css-tricks.com/debouncing-throttling-explained-examples/)了解debounce和throttle的不同之处。
*
* @since 0.1.0
* @category Function
* @param {Function} func The function to debounce.
* @param {number} [wait=0]
* The number of milliseconds to delay; if omitted, `requestAnimationFrame` is
* used (if available).
* @param {Object} [options={}] The options object.
* @param {boolean} [options.leading=false]
* Specify invoking on the leading edge of the timeout.
* @param {number} [options.maxWait]
* The maximum time `func` is allowed to be delayed before it's invoked.
* @param {boolean} [options.trailing=true]
* Specify invoking on the trailing edge of the timeout.
* @returns {Function} Returns the new debounced function.
* @example
*
* // Avoid costly calculations while the window size is in flux.
* jQuery(window).on('resize', debounce(calculateLayout, 150))
*
* // Invoke `sendMail` when clicked, debouncing subsequent calls.
* jQuery(element).on('click', debounce(sendMail, 300, {
* 'leading': true,
* 'trailing': false
* }))
*
* // Ensure `batchLog` is invoked once after 1 second of debounced calls.
* const debounced = debounce(batchLog, 250, { 'maxWait': 1000 })
* const source = new EventSource('/stream')
* jQuery(source).on('message', debounced)
*
* // Cancel the trailing debounced invocation.
* jQuery(window).on('popstate', debounced.cancel)
*
* // Check for pending invocations.
* const status = debounced.pending() ? "Pending..." : "Ready"
*/
function debounce(func, wait, options) {
let lastArgs,
lastThis,
maxWait,
result,
timerId,
lastCallTime

let lastInvokeTime = 0
let leading = false
let maxing = false
let trailing = true

// Bypass `requestAnimationFrame` by explicitly setting `wait=0`.
const useRAF = (!wait && wait !== 0 && typeof root.requestAnimationFrame === 'function')

if (typeof func !== 'function') {
throw new TypeError('Expected a function')
}
wait = +wait || 0
if (isObject(options)) {
leading = !!options.leading
maxing = 'maxWait' in options
maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait
trailing = 'trailing' in options ? !!options.trailing : trailing
}

function invokeFunc(time) {
const args = lastArgs
const thisArg = lastThis

lastArgs = lastThis = undefined
lastInvokeTime = time
result = func.apply(thisArg, args)
return result
}

function startTimer(pendingFunc, wait) {
if (useRAF) {
root.cancelAnimationFrame(timerId);
return root.requestAnimationFrame(pendingFunc)
}
return setTimeout(pendingFunc, wait)
}

function cancelTimer(id) {
if (useRAF) {
return root.cancelAnimationFrame(id)
}
clearTimeout(id)
}

function leadingEdge(time) {
// Reset any `maxWait` timer.
lastInvokeTime = time
// Start the timer for the trailing edge.
timerId = startTimer(timerExpired, wait)
// Invoke the leading edge.
return leading ? invokeFunc(time) : result
}

function remainingWait(time) {
const timeSinceLastCall = time - lastCallTime
const timeSinceLastInvoke = time - lastInvokeTime
const timeWaiting = wait - timeSinceLastCall

return maxing
? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
: timeWaiting
}

function shouldInvoke(time) {
const timeSinceLastCall = time - lastCallTime
const timeSinceLastInvoke = time - lastInvokeTime

// Either this is the first call, activity has stopped and we're at the
// trailing edge, the system time has gone backwards and we're treating
// it as the trailing edge, or we've hit the `maxWait` limit.
return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
(timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait))
}

function timerExpired() {
const time = Date.now()
if (shouldInvoke(time)) {
return trailingEdge(time)
}
// Restart the timer.
timerId = startTimer(timerExpired, remainingWait(time))
}

function trailingEdge(time) {
timerId = undefined

// Only invoke if we have `lastArgs` which means `func` has been
// debounced at least once.
if (trailing && lastArgs) {
return invokeFunc(time)
}
lastArgs = lastThis = undefined
return result
}

function cancel() {
if (timerId !== undefined) {
cancelTimer(timerId)
}
lastInvokeTime = 0
lastArgs = lastCallTime = lastThis = timerId = undefined
}

function flush() {
return timerId === undefined ? result : trailingEdge(Date.now())
}

function pending() {
return timerId !== undefined
}

function debounced(...args) {
const time = Date.now()
const isInvoking = shouldInvoke(time)

lastArgs = args
lastThis = this
lastCallTime = time

if (isInvoking) {
if (timerId === undefined) {
return leadingEdge(lastCallTime)
}
if (maxing) {
// Handle invocations in a tight loop.
timerId = startTimer(timerExpired, wait)
return invokeFunc(lastCallTime)
}
}
if (timerId === undefined) {
timerId = startTimer(timerExpired, wait)
}
return result
}
debounced.cancel = cancel
debounced.flush = flush
debounced.pending = pending
return debounced
}

export default debounce

throttle

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
import debounce from './debounce.js'
import isObject from './isObject.js'

/**
* Creates a throttled function that only invokes `func` at most once per
* every `wait` milliseconds (or once per browser frame). The throttled function
* comes with a `cancel` method to cancel delayed `func` invocations and a
* `flush` method to immediately invoke them. Provide `options` to indicate
* whether `func` should be invoked on the leading and/or trailing edge of the
* `wait` timeout. The `func` is invoked with the last arguments provided to the
* throttled function. Subsequent calls to the throttled function return the
* result of the last `func` invocation.
*
* **Note:** If `leading` and `trailing` options are `true`, `func` is
* invoked on the trailing edge of the timeout only if the throttled function
* is invoked more than once during the `wait` timeout.
*
* If `wait` is `0` and `leading` is `false`, `func` invocation is deferred
* until the next tick, similar to `setTimeout` with a timeout of `0`.
*
* If `wait` is omitted in an environment with `requestAnimationFrame`, `func`
* invocation will be deferred until the next frame is drawn (typically about
* 16ms).
*
* See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/)
* for details over the differences between `throttle` and `debounce`.
*
* @since 0.1.0
* @category Function
* @param {Function} func The function to throttle.
* @param {number} [wait=0]
* The number of milliseconds to throttle invocations to; if omitted,
* `requestAnimationFrame` is used (if available).
* @param {Object} [options={}] The options object.
* @param {boolean} [options.leading=true]
* Specify invoking on the leading edge of the timeout.
* @param {boolean} [options.trailing=true]
* Specify invoking on the trailing edge of the timeout.
* @returns {Function} Returns the new throttled function.
* @example
*
* // Avoid excessively updating the position while scrolling.
* jQuery(window).on('scroll', throttle(updatePosition, 100))
*
* // Invoke `renewToken` when the click event is fired, but not more than once every 5 minutes.
* const throttled = throttle(renewToken, 300000, { 'trailing': false })
* jQuery(element).on('click', throttled)
*
* // Cancel the trailing throttled invocation.
* jQuery(window).on('popstate', throttled.cancel)
*/
function throttle(func, wait, options) {
let leading = true
let trailing = true

if (typeof func !== 'function') {
throw new TypeError('Expected a function')
}
if (isObject(options)) {
leading = 'leading' in options ? !!options.leading : leading
trailing = 'trailing' in options ? !!options.trailing : trailing
}
return debounce(func, wait, {
leading,
trailing,
'maxWait': wait,
})
}

export default throttle

rc-notification源码解析

Posted on 2019-04-22

github地址
示例地址

Notification

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
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
import Animate from 'rc-animate';
import createChainedFunction from 'rc-util/lib/createChainedFunction';
import classnames from 'classnames';
import Notice from './Notice';

// 用来生成唯一标识符uuid
let seed = 0;
const now = Date.now();

function getUuid() {
return `rcNotification_${now}_${seed++}`;
}

// Notification -> Animate -> Notices
class Notification extends Component {
static propTypes = {
prefixCls: PropTypes.string,
transitionName: PropTypes.string,
animation: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
style: PropTypes.object,
maxCount: PropTypes.number,
closeIcon: PropTypes.node,
};

static defaultProps = {
prefixCls: 'rc-notification',
animation: 'fade',
style: {
top: 65,
left: '50%',
},
};

state = {
notices: [],
};

// transitionName
getTransitionName() {
const props = this.props;
let transitionName = props.transitionName;
if (!transitionName && props.animation) {
transitionName = `${props.prefixCls}-${props.animation}`;
}
return transitionName;
}

// 添加notice
add = (notice) => {
const key = notice.key = notice.key || getUuid();
const { maxCount } = this.props;
this.setState(previousState => {
const notices = previousState.notices;
const noticeIndex = notices.map(v => v.key).indexOf(key);
const updatedNotices = notices.concat();

// 已存在key值相同的notice,则用新的替换旧的
if (noticeIndex !== -1) {
updatedNotices.splice(noticeIndex, 1, notice);
} else {
// 超出notice的maxCount,则先删除第一个,再添加到队列末尾
// 没有超出则直接添加到队列末尾
if (maxCount && notices.length >= maxCount) {
// 使用第一项的key来更新新添加的项(让react移除现有的,而不是先删除再挂载)。
// 复用旧的key值是为了:a) 外部人工控制;b)内部react的key属性不是很好用。

// XXX, use key of first item to update new added (let React to move exsiting
// instead of remove and mount). Same key was used before for both a) external
// manual control and b) internal react 'key' prop , which is not that good.
notice.updateKey = updatedNotices[0].updateKey || updatedNotices[0].key;
updatedNotices.shift();
}
updatedNotices.push(notice);
}
return {
notices: updatedNotices,
};
});
}

// 删除特定key的notice
remove = (key) => {
this.setState(previousState => {
return {
notices: previousState.notices.filter(notice => notice.key !== key),
};
});
}

render() {
const props = this.props;
const { notices } = this.state;

// notice节点,每个notice对应一个Notice组件实例
const noticeNodes = notices.map((notice, index) => {
const update = Boolean(index === notices.length - 1 && notice.updateKey);
const key = notice.updateKey ? notice.updateKey : notice.key;
const onClose = createChainedFunction(this.remove.bind(this, notice.key), notice.onClose);
return (<Notice
prefixCls={props.prefixCls}
{...notice}
key={key}
update={update}
onClose={onClose}
onClick={notice.onClick}
closeIcon={props.closeIcon}
>
{notice.content}
</Notice>);
});
const className = {
[props.prefixCls]: 1,
[props.className]: !!props.className,
};
return (
<div className={classnames(className)} style={props.style}>
<Animate transitionName={this.getTransitionName()}>{noticeNodes}</Animate>
</div>
);
}
}

// 静态方法,创建Notification实例
Notification.newInstance = function newNotificationInstance(properties, callback) {

// getContainer是Notification instance的容器,最终append到body里
const { getContainer, ...props } = properties || {};
const div = document.createElement('div');
if (getContainer) {
const root = getContainer();
root.appendChild(div);
} else {
document.body.appendChild(div);
}
let called = false;

// 拿到notification的ref
// 包装成{notice, removeNotice, destroy, component},并传给callback
function ref(notification) {
if (called) {
return;
}
called = true;
callback({
notice(noticeProps) {
notification.add(noticeProps);
},
removeNotice(key) {
notification.remove(key);
},
component: notification,
destroy() {
ReactDOM.unmountComponentAtNode(div);
div.parentNode.removeChild(div);
},
});
}
ReactDOM.render(<Notification {...props} ref={ref} />, div);
};

export default Notification;

Notice

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
import React, { Component } from 'react';
import classNames from 'classnames';
import PropTypes from 'prop-types';

/**
* 1. 展示notice内容,关闭icon
* 2. 点击事件、关闭事件
* 3. 持续时间后调用`props.onClose()`
*/
export default class Notice extends Component {
static propTypes = {
duration: PropTypes.number,
onClose: PropTypes.func,
children: PropTypes.any,
update: PropTypes.bool,
closeIcon: PropTypes.node,
};

static defaultProps = {
onEnd() {
},
onClose() {
},
duration: 1.5,
style: {
right: '50%',
},
};

componentDidMount() {
this.startCloseTimer();
}

componentDidUpdate(prevProps) {
if (this.props.duration !== prevProps.duration
|| this.props.update) {
this.restartCloseTimer();
}
}

componentWillUnmount() {
this.clearCloseTimer();
}

close = (e) => {
if (e) {
e.stopPropagation();
}
this.clearCloseTimer();
this.props.onClose();
}

startCloseTimer = () => {
if (this.props.duration) {
this.closeTimer = setTimeout(() => {
this.close();
}, this.props.duration * 1000);
}
}

clearCloseTimer = () => {
if (this.closeTimer) {
clearTimeout(this.closeTimer);
this.closeTimer = null;
}
}

restartCloseTimer() {
this.clearCloseTimer();
this.startCloseTimer();
}

render() {
const props = this.props;
const componentClass = `${props.prefixCls}-notice`;
const className = {
[`${componentClass}`]: 1,
[`${componentClass}-closable`]: props.closable,
[props.className]: !!props.className,
};
return (
<div
className={classNames(className)}
style={props.style}
onMouseEnter={this.clearCloseTimer}
onMouseLeave={this.startCloseTimer}
onClick={props.onClick}
>
<div className={`${componentClass}-content`}>{props.children}</div>
{props.closable ?
<a tabIndex="0" onClick={this.close} className={`${componentClass}-close`}>
{props.closeIcon || <span className={`${componentClass}-close-x`}/>}
</a> : null
}
</div>
);
}
}

用法

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
/* eslint-disable no-console */
import 'rc-notification/assets/index.css';
import Notification from 'rc-notification';
import React from 'react';
import ReactDOM from 'react-dom';
let notification = null;
Notification.newInstance({}, (n) => notification = n);

function simpleFn() {
notification.notice({
content: <span>simple show</span>,
onClose() {
console.log('simple close');
},
});
}

function durationFn() {
notification.notice({
content: <span>can not close...</span>,
duration: null,
});
}

function closableFn() {
notification.notice({
content: <span>closable</span>,
duration: null,
onClose() {
console.log('closable close');
},
closable: true,
onClick() {
console.log('clicked!!!');
},
});
}

function close(key) {
notification.removeNotice(key);
}

function manualClose() {
const key = Date.now();
notification.notice({
content: <div>
<p>click below button to close</p>
<button onClick={close.bind(null, key)}>close</button>
</div>,
key,
duration: null,
});
}

let counter = 0;
let intervalKey;
function updatableFn() {
if (counter !== 0) {
return;
}

const key = 'updatable-notification';
const initialProps = {
content: `Timer: ${counter}s`,
key,
duration: null,
closable: true,
onClose() {
clearInterval(intervalKey);
counter = 0;
},
};

notification.notice(initialProps);
intervalKey = setInterval(() => {
notification.notice({ ...initialProps, content: `Timer: ${++counter}s` });
}, 1000);
}

let notification2 = null;
const clearPath = 'M793 242H366v-74c0-6.7-7.7-10.4-12.9' +
'-6.3l-142 112c-4.1 3.2-4.1 9.4 0 12.6l142 112c' +
'5.2 4.1 12.9 0.4 12.9-6.3v-74h415v470H175c-4.4' +
' 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h618c35.3 0 64-' +
'28.7 64-64V306c0-35.3-28.7-64-64-64z';

const getSvg = (path, props = {}, align = false) => {
return (
<i {...props}>
<svg
viewBox="0 0 1024 1024"
width="1em"
height="1em"
fill="currentColor"
style={align ? { verticalAlign: '-.125em ' } : {}}
>
<path d={path} />
</svg>
</i>
);
};
Notification.newInstance({
closeIcon: getSvg(clearPath, {}, true),
}, (n) => {
notification2 = n;
});
function customCloseIconFn() {
notification2.notice({
content: 'It is using custom close icon...',
closable: true,
duration: 0,
});
}

ReactDOM.render(<div>
<div>
<button onClick={simpleFn}>simple show</button>
<button onClick={durationFn}>duration=0</button>
<button onClick={closableFn}>closable</button>
<button onClick={manualClose}>controlled close</button>
<button onClick={updatableFn}>updatable</button>
<div>
<button onClick={customCloseIconFn}>custom close icon</button>
</div>
</div>
</div>, document.getElementById('__react-content'));

antd message源码分析

Posted on 2019-04-22

github地址。
调用了 rc-notification 模块。

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
/* global Promise */
import * as React from 'react';
import Notification from 'rc-notification';
import Icon from '../icon';

let defaultDuration = 3; // 持续时间 s
let defaultTop: number; // css top值
let messageInstance: any; // message实例
let key = 1; // 自增key
let prefixCls = 'ant-message'; // prefix cls
let transitionName = 'move-up'; // transition
let getContainer: () => HTMLElement;// container
let maxCount: number; // message个数阈值

/**
* 如果有 messageInstance 则 callback(messageInstance)
* 否则创建一个 messageInstance 再 callback(messageInstance)
* @param {any} callback
*/
function getMessageInstance(callback: (i: any) => void) {
if (messageInstance) {
callback(messageInstance);
return;
}
Notification.newInstance({
prefixCls,
transitionName,
style: { top: defaultTop }, // 覆盖原来的样式
getContainer,
maxCount,
}, (instance: any) => {
if (messageInstance) {
callback(messageInstance);
return;
}
messageInstance = instance;
callback(instance);
});
}

type NoticeType = 'info' | 'success' | 'error' | 'warning' | 'loading';

export interface ThenableArgument {
(_: any): any;
}

export interface MessageType {
(): void;
then: (fill: ThenableArgument, reject: ThenableArgument) => Promise<any>;
promise: Promise<any>;
}

/**
* @param {React.ReactNode} content message组件的content
* @param {func | number} duration 持续时间
* @param {NoticeType} type 类型
*/
function notice(
content: React.ReactNode,
duration: (() => void) | number = defaultDuration,
type: NoticeType,
onClose?: () => void,
): MessageType {
const iconType = ({
info: 'info-circle',
success: 'check-circle',
error: 'cross-circle',
warning: 'exclamation-circle',
loading: 'loading',
})[type];

if (typeof duration === 'function') {
onClose = duration;
duration = defaultDuration;
}

const target = key++;

// 调用 messageInstance,并将 onClose 作为 callback 传进去
const closePromise = new Promise((resolve) => {
const callback = () => {
if (typeof onClose === 'function') {
onClose();
}
return resolve(true);
};
getMessageInstance((instance) => {
instance.notice({
key: target,
duration,
style: {},
content: (
<div className={`${prefixCls}-custom-content ${prefixCls}-${type}`}>
<Icon type={iconType} />
<span>{content}</span>
</div>
),
onClose: callback,
});
});
});
const result: any = () => {
if (messageInstance) {
messageInstance.removeNotice(target);
}
};
result.then = (filled: ThenableArgument, rejected: ThenableArgument) => closePromise.then(filled, rejected);
result.promise = closePromise;

// 返回remove notice的函数
return result;
}

type ConfigContent = React.ReactNode | string;
type ConfigDuration = number | (() => void);
export type ConfigOnClose = () => void;

export interface ConfigOptions {
top?: number;
duration?: number;
prefixCls?: string;
getContainer?: () => HTMLElement;
transitionName?: string;
maxCount?: number;
}

// 入口
export default {
info(content: ConfigContent, duration?: ConfigDuration, onClose?: ConfigOnClose) {
return notice(content, duration, 'info', onClose);
},
success(content: ConfigContent, duration?: ConfigDuration, onClose?: ConfigOnClose) {
return notice(content, duration, 'success', onClose);
},
error(content: ConfigContent, duration?: ConfigDuration, onClose?: ConfigOnClose) {
return notice(content, duration, 'error', onClose);
},
// Departed usage, please use warning()
warn(content: ConfigContent, duration?: ConfigDuration, onClose?: ConfigOnClose) {
return notice(content, duration, 'warning', onClose);
},
warning(content: ConfigContent, duration?: ConfigDuration, onClose?: ConfigOnClose) {
return notice(content, duration, 'warning', onClose);
},
loading(content: ConfigContent, duration?: ConfigDuration, onClose?: ConfigOnClose) {
return notice(content, duration, 'loading', onClose);
},
config(options: ConfigOptions) {
if (options.top !== undefined) {
defaultTop = options.top;
messageInstance = null; // delete messageInstance for new defaultTop
}
if (options.duration !== undefined) {
defaultDuration = options.duration;
}
if (options.prefixCls !== undefined) {
prefixCls = options.prefixCls;
}
if (options.getContainer !== undefined) {
getContainer = options.getContainer;
}
if (options.transitionName !== undefined) {
transitionName = options.transitionName;
messageInstance = null; // delete messageInstance for new transitionName
}
if (options.maxCount !== undefined) {
maxCount = options.maxCount;
messageInstance = null;
}
},
destroy() {
if (messageInstance) {
messageInstance.destroy();
messageInstance = null;
}
},
};

Node.js事件循环,定时器和process.nextTick(转)

Posted on 2019-04-21

原文链接
英文链接

什么是事件轮询

事件循环是Node.js处理非阻塞I/O操作的机制——尽管Javascript是单线程处理的——当有可能的时候,它们会把操作转移到系统内核中区。

既然目前大多数内核都是多线程的,它们可在后台处理多种操作。当其中的一个操作完成的时候,内核通知Node.js将适合的回调函数添加到轮询队列中等待时机执行。我们在本文后面会进行详细介绍。

事件轮询机制解析

当Node.js启动后,它会初始化事件轮询;处理已提供的输入脚本(或丢入REPL,本文不涉及到),它可能会调用一些异步的API函数调用,安排任务处理事件,或者调用process.nextTick(),然后开始处理事件循环。

下面的图表显示了事件循环的概述以及操作顺序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
   ┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘

注意:每个框框里每一步都是事件循环机制的一个阶段

每个阶段都有一个FIFO队列来执行回调。虽然每个阶段都是特殊的,但通常情况下,当事件循环进入给定的阶段时,它将执行特定于该阶段的任何操作,然后在该阶段的队列中执行回调,直到队列用尽或最大回调数已执行。当该队列已用尽或达到回调限制,事件循环将移动到下一阶段,等等。

由于这些操作中的任何一个都可能计划更多的操作,并且在轮询阶段处理的新事件由内核排队,因此在处理轮询事件时,轮询事件可以排队。因此,长时间运行回调可以允许轮询阶段运行大量长于计时器的阈值。有关详细信息,请参阅计时器和轮询部分。

注意 :在Windows和Unix/Linux实现之间存在细微的差异,但这对演示来说并不重要。最重要的部分在这里。实际上由七或八个步骤,但我们关心但是Node.js实际上使用以上的某些步骤。

阶段概述

  • 定时器:本阶段执行已经安排的setTimeout()和setInterval()的回调函数
  • 待定回调:执行延迟到下一个循环迭代的I/O回调。
  • idle,prepare:仅系统内部使用。
  • 轮询:检索新的I/O事件;执行于I/O相关的回调(几乎所有情况下,除了关闭的回调函数,它们由计时器和setImmediate()排定的之外),其余情况node将在此处阻塞。
  • 检测:setImmediate()回调函数在这里执行。
  • 关闭的回调函数:一些准备关闭的回调函数,如:socket.on('close', ...)。

在每次运行的事件循环之间,Node.js检查它是否在等待任何异步I/O或计时器,如果没有的话,则关闭干净。

阶段的详细概述

定时器

定时器指定可执行所提供回调的阈值,而不是用户希望其执行的确切时间。计时器回调将尽可能早地运行,因为它们可以在指定的时间间隔后进行调度。但是操作系统调度或其它调度的运行可能会延迟它们。

注意: 轮询阶段控制何时定时器执行。

例如,假设计划在100毫秒后执行超时阈值,然后脚本开始异步读取文件,这需要95毫秒:

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
const fs = require('fs');

function someAsyncOperation(callback) {
// Assume this takes 95ms to complete
fs.readFile('/path/to/file', callback);
}

const timeoutScheduled = Date.now();

setTimeout(() => {
const delay = Date.now() - timeoutScheduled;

console.log(`${delay}ms have passed since I was scheduled`);
}, 100);


// do someAsyncOperation which takes 95 ms to complete
someAsyncOperation(() => {
const startCallback = Date.now();

// do something that will take 10ms...
while (Date.now() - startCallback < 10) {
// do nothing
}
});

当事件循环进入轮询阶段时,它有一个空队列(此时fs.readFile()尚未完成),因此它将等待毫秒数,直到达到最快的计时器阈值为止。当它等待95毫秒通过时,fs.readFile()完成读取文件,它需要10毫秒完成的回调将添加到轮询队列中并执行。当回调完成时,队列中不再有回调,因此事件循环将看到已达到最快计时器的阈值,然后将回滚到计时器阶段,以执行定时器的回调。在本实例中,将看到计划中的定时器和执行的回调之间的总延迟将为105毫秒。

注意: 为了防止 轮询 阶段饿死事件循环,libuv(实现Node.js事件循环和平台的所有异步行为的C函数库),在停止轮询以获得更多事件之前,还有一个最大的(系统依赖)。

挂起的回调函数

轮询

轮询阶段有两个重要的功能:

  1. 计算应该阻塞和轮询I/O的时间。
  2. 然后,处理轮询队列里的事件。

当事件循环进入轮询阶段且没有计划计时器时,将发生以下两种情况之一:

  • 如果轮询队列不是空的,事件循环将循环访问其回调队列并同步执行它们,直到队列已用尽,或者达到里与系统相关的硬限制。
  • 如果轮询队列是空的,还有两件事发生:
    • 如果脚本已按setImmediate()排定,则事件循环将结束轮询阶段,并继续检查阶段以执行这些计划脚本。
    • 如果脚本尚未按setImmediate()排定,则事件循环将等待回调添加到队列中,然后立即执行。

一旦轮询队列唯恐,事件循环将检查已达到时间阈值的计时器。如果一个或多个计时器已准备就绪,则事件循环将绕回计时器阶段以执行这些计时器的回调。

检查阶段

此阶段允许人员在轮询阶段完成后立即执行回调。如果轮询阶段变为空闲状态,并且脚本已排队使用setImmediate(),则事件循环可以继续到检查阶段而不是等待。

setImmediate()实际上是一个在事件循环的单独阶段运行的特殊计时器。它使用一个libuv API来安排回调在轮询阶段完成后执行。

通常,在执行代码时,事件循环最终会命中轮询阶段,等待传入连接、请求等。但是,如果回调已计划为setImmediate(),并且轮询阶段变为空闲状态,则它将结束并继续到检查阶段而不是等待轮询事件。

关闭的回调函数

如果套接字或处理函数突然关闭(例如socket.destroy()),则'close'事件将在这个阶段发出。否则它将通过process.nextTick()发出。

setImmediate()对比setTimeout()

setImmediate()和setTimeout()很类似,但何时调用行为完全不同。

  • setImmediate()设计为在当前轮询阶段完成后执行脚本。
  • setTimeout()计划在毫秒的最小阈值经过后运行的脚本。

执行计时器但顺序将根据调用它们的上下文而异。如果二者都从主模块内调用,则计时将首进程性能的约束(这可能会受到计算机上运行的其它应用程序的影响)。

例如,如果运行的是不属于I/O周期(即主模块)的以下脚本,则执行两个计时器的顺序是非确定性的,因为它受进程性能的约束:

1
2
3
4
5
6
7
8
// timeout_vs_immediate.js
setTimeout(() => {
console.log('timeout');
}, 0);

setImmediate(() => {
console.log('immediate');
});
1
2
3
4
5
6
7
$ node timeout_vs_immediate.js
timeout
immediate

$ node timeout_vs_immediate.js
immediate
timeout

但是,如果你把这两个函数放入一个I/O循环内调用,setImmediate总是被优先调用:

1
2
3
4
5
6
7
8
9
10
11
// timeout_vs_immediate.js
const fs = require('fs');

fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
1
2
3
4
5
6
7
$ node timeout_vs_immediate.js
immediate
timeout

$ node timeout_vs_immediate.js
immediate
timeout

使用setImmediate()超过setTimeout()的主要优点是setImmediate()在任何计时器(如果在 I/O 周期内)都将始终执行,而不依赖于存在多少个计时器。

process.nextTick()

理解process.nextTick()

您可能已经注意到process.nextTick()在关系图中没有显示,即使它是异步API的一部分。这是因为process.nextTick()在技术上不是事件循环的一部分。相反,无论事件循环的当前阶段如何,都将在当前操作完成后处理nextTickQueue。这里的一个操作被视作为一个从 C++ 底层处理开始过渡,并且处理需要执行的 JavaScript 代码。

回顾我们的关系图,任何时候在给定的阶段中调用process.nextTick(),所有传递到process.nextTick()的回调将在事件循环继续之前得到解决。这可能会造成一些糟糕的情况, 因为它允许您通过进行递归process.nextTick()来“饿死”您的I/O调用,阻止事件循环到达轮询阶段。

为什么会允许这样?

为什么这样的事情会包含在 Node.js 中?它的一部分是一个设计理念,其中 API 应该始终是异步的,即使它不必是。以此代码段为例:

1
2
3
4
5
function apiCall(arg, callback) {
if (typeof arg !== 'string')
return process.nextTick(callback,
new TypeError('argument should be string'));
}

代码段进行参数检查。如果不正确,则会将错误传递给回调函数。最近对API进行了更新,允许将参数传递给process.nextTick(),允许它在回调后传递任何参数作为回调的参数传播,这样就不必嵌套函数了。

我们正在做的是将错误传递给用户,但仅在我们允许用户的其余代码执行之后。通过使用process.nextTick(),我们保证apiCall()始终在用户代码的其余部分之后运行其回调函数,并在允许事件循环之前继续进行。为了实现这一点,JS调用栈被允许展开,然后立即执行提供的回调,允许进行递归调用process.nextTick(),而不达到RangeError: 超过 v8 的最大调用堆栈大小。

这种哲学可能会导致一些潜在的问题。 以此代码段为例:

1
2
3
4
5
6
7
8
9
10
11
12
let bar;

// this has an asynchronous signature, but calls callback synchronously
function someAsyncApiCall(callback) { callback(); }

// the callback is called before `someAsyncApiCall` completes.
someAsyncApiCall(() => {
// since someAsyncApiCall has completed, bar hasn't been assigned any value
console.log('bar', bar); // undefined
});

bar = 1;

用户将someAsyncApiCall()定义为具有异步签名,但实际上它是同步运行的。当调用它时,提供给someAsyncApiCall()的回调在同一阶段调用事件循环,因为someAsyncApiCall()实际上并没有异步执行任何事情。因此,回调尝试引用 bar,即使它在范围内可能还没有该变量,因为脚本无法运行到完成。

通过将回调置于process.nextTick()中,脚本仍具有运行完成的能力,允许在调用回调之前初始化所有变量、函数等。它还具有不允许事件循环继续的优点。在允许事件循环继续之前,对用户发出错误警报可能很有用。下面是使用process.nextTick()的上一个示例:

1
2
3
4
5
6
7
8
9
10
11
let bar;

function someAsyncApiCall(callback) {
process.nextTick(callback);
}

someAsyncApiCall(() => {
console.log('bar', bar); // 1
});

bar = 1;

这又是另外一个真实的例子:

1
2
3
const server = net.createServer(() => {}).listen(8080);

server.on('listening', () => {});

只有端口通过时,端口才会立即被绑定。之后可以立即调用'listening'回调。问题是.on('listening')回调在这个时候还没有被设置。

为了绕过此现象,'listening'事件在nextTick()中排队,以允许脚本运行到完成阶段。这允许用户设置所需的任何事件处理程序。

process.nextTick()对比setImmediate()

就用户而言我们有两个类似的调用,但它们的名称令人费解。

  • process.nextTick()在同一个阶段立即执行。
  • setImmediate()在以下迭代或 ‘tick’ 上触发事件循环。

实质上,这两个名称应该交换,因为process.nextTick()比setImmediate()触发得更直接,但这是过去遗留问题,因此不太可能改变。如果贸然进行名称交换,将破坏 npm 上的大部分软件包。每天都有新的模块在不断增长,这意味着我们我们每等待一天,就有更多的潜在破损在发生。所以说尽管这些名称使人感到困惑,但它们的名字本身不会改变。

我们建议开发人员在所有情况下都使用setImmediate(),因为它更容易被推理(并且它导致代码与更广泛的环境,如浏览器 JS 所兼容。)

为什么要使用 process.nextTick()?

主要有两个原因:

  1. 允许用户处理错误,清理任何不需要的资源,或者在事件循环继续之前重试请求。
  2. 有时在调用堆栈已解除但在事件循环继续之前,必须允许回调运行。

一个例子就是要符合用户的期望。简单示例:

1
2
3
4
5
const server = net.createServer();
server.on('connection', (conn) => { });

server.listen(8080);
server.on('listening', () => { });

假设listen()在事件循环开始时运行,但侦听回调被放置在setImmediate()中。除非通过主机名,否则将立即绑定到端口。为使事件循环继续进行,它必须命中轮询阶段,这意味着可能会收到连接,从而允许在侦听事件之前激发连接事件。

另一个示例运行的函数构造函数是从EventEmitter继承的,它想调用构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
const EventEmitter = require('events');
const util = require('util');

function MyEmitter() {
EventEmitter.call(this);
this.emit('event');
}
util.inherits(MyEmitter, EventEmitter);

const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
console.log('an event occurred!');
});

不能立即从构造函数中发出事件。因为脚本不会处理到用户为该事件分配回调的点。因此,在构造函数本身中可以使用process.nextTick()来设置回调,以便在构造函数完成后发出该事件,从而提供预期的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const EventEmitter = require('events');
const util = require('util');

function MyEmitter() {
EventEmitter.call(this);

// use nextTick to emit the event once a handler is assigned
process.nextTick(() => {
this.emit('event');
});
}
util.inherits(MyEmitter, EventEmitter);

const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
console.log('an event occurred!');
});

不会做动画的前端不是好开发(转)

Posted on 2019-04-21

原文链接

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

Posted on 2019-04-21

原文链接
英文原文
CSS Triggers

GPU是如何合成图像的

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

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

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

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

  • 画每个复合层的图像
  • 准备图层的数据
  • 准备动画的着色器(如果需要)
  • 向GPU发送数据

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

隐式合成

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

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

  • 3d或透视变换css属性,例如translate3d,translateZ等等(js一般通过这种方式,使元素获得复合层)
  • <video><iframe><canvas><webgl>等元素
  • 混合插件(如flash)
  • 元素自身的opacity和transform做 CSS 动画
  • 拥有css过滤器的元素
  • 使用will-change属性
  • position:fixed
  • 元素有一个z-index较低且包含一个复合层的兄弟元素(换句话说就是该元素在复合层上面渲染)

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

内存占用

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

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

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

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

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

在chrome的开发者工具中打开Setting——>Experiments——>layers可以看到每个层的内存占用。

GPU动画的优点和缺点

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

  • 每秒60帧,动画平滑、流畅
  • 一个合适的动画工作在一个单独的线程,它不会被大量的js计算阻塞
  • 3D“变换”是便宜的

缺点:

  • 提升一个元素到复合层需要额外的重绘,有时这是慢的。(即我们得到的是一个全层重绘,而不是一个增量)
  • 绘图层必须传输到GPU。取决于层的数量和传输可能会非常缓慢。这可能让一个元素在中低档设备上闪烁。
  • 每个复合层都需要消耗额外的内存,过多的内存可能导致浏览器的崩溃。
  • 如果你不考虑隐式合成,而使用重绘;会导致额外的内存占用,并且浏览器崩溃的概率是非常高的。
  • 我们会有视觉假象,例如在Safari中的文本渲染,在某些情况下页面内容将消失或变形。

优化技巧

避免隐式合成

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

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

动画中只使用transform和opacity

减小复合层的尺寸

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

用css动画而不是js动画

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

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

优化技巧总结

  • 减少浏览器的重排和重绘的发生
  • 不要使用table布局
  • css动画中尽量只使用transform和opacity,这不会发生重排和重绘
  • 尽可能地只使用css做动画
  • 避免浏览器的隐式合成
  • 改变复合层的尺寸

深入浏览器理解CSS animations 和 transitions的性能问题(转)

Posted on 2019-04-21

原文链接

浏览器内部两个重要的线程

  • 主线程
  • 合成线程

一般情况下,主线程负责:

  • Javascript
  • 计算DOM && CSSOM
  • 计算Layout
  • 将元素绘制到一个或多个位图中
  • 将这些位图交给合成线程

相应地,合成线程负责:

  • 通过GPU将位图绘制到屏幕上
  • 同志主线程更新页面中可见或即将变成可见到部分到位图
  • 计算出页面中哪部分是可见到
  • 计算出当你在滚动页面时哪部分是即将变成可见到
  • 当你在滚动页面时将相应位置到元素移动到可视区域

当用户滚动页面时,合成线程会通知主线程更新页面中最新可见部分到位图。但是,如果主线程响应慢,合成线程不会等待,而是马上绘制已经生成到位图,还没准备好的部分用白色进行填充。

GPU

快在于:

  1. 绘制位图到屏幕上
  2. 一遍又一遍绘制相同到位图
  3. 将同一位图绘制到不同位置,执行旋转以及缩放处理

慢在于:

  1. 将位图加载到它的内存中

因此,修改元素的height要比修改transform属性性能消耗大。这是因为对height的修改需要不断的relayout、repaint,这两个步骤的计算量可能是巨大的。而transform属性不会更改元素或它周围的元素的布局。transform属性会对元素的整体产生影响,它会对整个元素进行缩放、旋转、移动处理。

以下CSS属性在动画处理方面是比较快的:

  • CSS transform
  • CSS opacity
  • CSS filter

深入理解ES6-迭代器(Iterator)和生成器(generator)

Posted on 2019-04-21

用循环语句迭代数据时,必须要初始化一个变量来记录每一次迭代在数据集合中的位置,迭代器的使用可以极大的简化数据操作。

什么是迭代器Iterator

迭代器是一种特殊的对象,具有专门的接口,所有迭代器对象都有一个next方法,每次调用都返回一个结果对象。

结果对象有两个属性:一个是value,表示下一个将要返回的值;另一个是done,是一个布尔值,当没有更多可返回数据时返回true。

迭代器还会保存一个内部指针,用来指向当前集合中值的位置,没调用一次next()方法,都会返回下一个可用的值。

如果最后一个值返回后再调用next()方法,返回的对象中属性done为true,value则包含迭代器最终返回的值,这个返回值不是数据集的一部分,与函数的返回值类似,是函数调用过程中最后一次给调用者传递信息的方法,如果没有相关数据则返回undefined。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ES5 语法创建一个迭代器
function createIterator(items) {
var i = 0;
return {
next: function() {
var done = (i >= items.length);
var value = !done ? items[i++] : undefined;

return {
done: done,
value: value
};
}
};
}

var iterator = createIterator([1, 2, 3]);

console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: 3, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"

什么是生成器Generator

生成器是一种返回迭代器的函数,通过function关键字后的星号(*)来表示,函数中会用到新的关键字yield。星号可以紧挨着function关键字,也可以在中间添加一个空格:

1
2
3
4
5
function *createIterator() {
yield 1;
yield 2;
yield 3;
}

yield关键字也是ES6的新特性,可以通过它来指定调用迭代器的next()方法时的返回值及返回顺序。

生成器函数最有序的部分大概是:每当执行完一条yield语句后函数就会自动停止执行,直到再次调用迭代器的next()方法才会继续执行下一个yield语句。

使用yield关键字可以返回任何值或表达式,因此可以通过生成器函数批量的给迭代器添加元素。

1
2
3
4
5
6
7
8
9
10
11
12
function *createIterator(items) {
for(let i = 0, len = items.length; i < len; i++) {
yield items[i];
}
}

let iterator = createIterator([1, 2, 3]);

console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: 3, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"

yield的使用限制

yield关键字只可在生成器内部使用,在其它地方使用会导致程序抛出语法错误,即便在生成器内部的函数里使用也是如此。常见案例:

1
2
3
4
5
6
function *createIterator(items) {
items.forEach(function(item) {
// 语法错误
yield item + 1;
})
}

可迭代对象和for-of循环

可迭代对象具有Symbol.iterator属性,是一种与迭代器密切相关的对象。Symbol.iterator通过指定的函数可以返回一个作用于附属对象的迭代器。在ES6中,所有的集合对象(数组、Set集合及Map集合)和字符串都是可迭代对象,这些对象中都有默认的迭代器。

for-of循环每执行一次都会调用可迭代对象的next()方法,并将迭代器返回的结果对象的value属性存储在一个变量中,循环将持续执行这一过程直到返回对象的done属性的值为true。

访问默认迭代器

可以通过Symbol.iterator来访问对象默认的迭代器。

由于具有Symbol.iterator属性的对象都有默认的迭代器,因此可以用它来检测对象是否为可迭代对象:

1
2
3
function isIterable(object) {
return typeof object[Symbol.iterator] === 'function';
}

创建可迭代对象

默认情况下,开发者定义的对象都是不可迭代对象,但如果给Symbol.iterator属性添加一个生成器,则可以将其变为可迭代对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let collection = {
items: [],
*[Symbol.iterator]() {
for (let item of this.items) {
yield item;
}
}
}

collection.items.push(1);
collection.items.push(2);
collection.items.push(3);

for (let x of collection) {
console.log(x);
}

// 输出结果
1
2
3

内建迭代器

在ES6中有3中类型但集合对象:数组、Map集合与Set集合。为了更好的访问对象中的内容,这3种对象都内建来三种迭代器:

  • entries()
  • values()
  • keys()

entries()迭代器(TODO)

values()迭代器(TODO)

keys()迭代器(TODO)

字符串迭代器

由于方括号操作的是编码单元而非字符,因此无法正确访问双字节字符。由于双字节字符被视作两个独立的编码单元,在使用方括号获取双字节字符时得到的是两个空。

所幸,ES6的目标是全面支持Unicode,并且我们可以通过改变字符串的默认迭代器来解决这个问题,使其操作字符而不是编码单元。

NodeList迭代器

自从ES6添加了默认迭代器后,DOM定义中的NodeList类型(定义在HTML标准而不是ES6标准中)也拥有了默认迭代器,其行为与数组的默认迭代器完全一致。所以可以将NodeList应用于for-of循环及其他支持对象默认迭代器的地方。

展开运算符与非数组可迭代对象

由于展开运算符可以作用于任意可迭代对象,因此如果想将可迭代对象转换为数组,这是最简单的方法。你既可以将字符串中的每一个字符(不是编码单元)存入新数组中,也可以将浏览器中的NodeList对象中的每一个节点存入新数组中。

1
2
3
4
5
6
7
let set = new Set([1, 2, 3]),
map = new Map(['name', 'Nicholas'], ['age', 25]),
arrSet = [...set],
arrMap = [...map];

console.log(arrSet); // [1, 2, 3]
console.log(arrMap); // ['name', 'Nicholas'], ['age', 25]

高级迭代器功能

给迭代器传递参数(TODO)

使用React Virtualized渲染列表(译)

Posted on 2019-04-20

原文链接

使用React处理数据相对来说比较容易,因为React的设计就是把数据当作状态。但是当你需要处理的数据量很大的时候,麻烦就来了。比如你要处理一个包含500-1000条记录的数据集,这会产生巨大的计算量并导致性能问题。下面我们将学习如何使用虚拟列表来“看起来”渲染了一个长列表。

我们将使用React Virtualized组件来实现我们的需求。它让我们可以以很小的代价渲染大集合数据。

设置

React Virtualized官方已经有很详细的介绍了,可以去它们的github去看看。

我们需要大量的数据,下面我们就来造一些。

1
2
3
4
5
6
7
8
9
10
11
function createRecord(count) {
let records = [];

for (let i = 0; i < count; i++) {
records.push({
username: faker.internet.userName(),
email: faker.internet.email()
});
}
return records;
}

下面,我们设置一个数字来创造我们需要的数据:

const records = createRecord(1000);

好了,现在我们有需要渲染的数据了。

创建一个虚拟列表

这里是我们创建的一个列表,我们引入使用了库提供的一些展示样式,本篇post不讨论这个。
现在开始感受一下这个demo,速度超级快,是不是?

你可能想知道这背后到底发生了什么,结果发现是一系列很疯狂和酷的sizing、positioning、transform和transitions,是这些技术让一条条记录进入/离开可视区。数据都在那里并渲染了,React Virtualized创建了一个window,当用户scroll的时候,一条条记录将滑入/出我们的视野。

为了渲染虚拟列表,我们需要List组件,它内部渲染了一个Grid组件。

首先,我们从设置rowRenderer,开始,它是负责渲染单挑数据的组件。

1
2
3
4
5
6
7
8
rowRenderer = ({ index, isScrolling, key, style }) => {
return (
<div key={key} style={style}>
<div>{this.props.data[index].username}</div>
<div>{this.props.data[index].email}</div>
</div>
);
};

它返回一个包含两个div的div,里面的两个div分别是username和email`。可以看出,这是一个简单的展示用户信息的列表。

rowRenderer接受几个参数,下面是这些参数的细节:

  • index: 记录的数值ID
  • isScrolling: 代表List组件是否发生scrolling
  • isVisible: 代表这条数据是否在可视区内
  • key: 这条记录在数组中的位置
  • parent: 定义这个列表是否是另一个列表的parent/child
  • style: 定位这条数据的style对象

下面我们再深入了解一些rowRenderer函数,我们把它放到List组件中:

1
2
3
4
5
6
7
8
<List
rowCount={this.props.data.length}
width={width}
height={height}
rowHeight={rowHeight}
rowRenderer={this.rowRenderer}
overscanRowCount={3}
/>

你可能注意到这里的几个参数:

  • rowCount: 接收代表列表长度的数字
  • width: 列表的宽度
  • height: 列表的高度
  • rowHeight: 每条数据的高度
  • rowRenderer: 用来渲染每条数据的模板,我们将传入之前定义的rowRenderer函数
  • overscanRowCount:
    用来渲染用户scroll方向额外的数据,防止用户滑动太快,虚拟内容来不及渲染。

最后,代码应该是这样的:

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
const { List } = ReactVirtualized

...

const height = 700;
const rowHeight = 40;
const width = 800;

class App extends React.Component {
rowRenderer = ({ index, isScrolling, key, style }) => {
return (
<div key={key} style={style}>
<div>{this.props.data[index].username}</div>
<div>{this.props.data[index].email}</div>
</div>
);
};

render() {
return (
<div>
<h2>Details</h2>
<List
rowCount={this.props.data.length}
width={width}
height={height}
rowHeight={rowHeight}
rowRenderer={this.rowRenderer}
overscanRowCount={3}
/>
</div>
);
}
}

Cell measure

文档里介绍,cell
measure是一个高阶组件,用来暂时渲染列表。现在我们还看不到它,但数据已经在里面被处理并准备好展示了。

什么时候我们需要关心cell measure?最常见的use
case是当我们需要动态计算rowHeight的时候。React
Virtualized在渲染每一行的时候,会缓存它们的高度值,这样当数据滑出可视区的时候我们也不用再计算它的高度——不管里面的内容是什么,高度都是对的。

首先,我们创建自己的缓存cache,在我们组件的constructor里用CellMeasureCache:

1
2
3
4
5
6
7
constructor() {
super()
this.cache = new CellMeasurerCache({
fixedWidth: true,
defaultHeight: 100
})
}

当我们设置List组件的时候,把cache带上:

1
2
3
4
5
6
7
8
9
<List
rowCount={this.props.data.length}
width={rowWidth}
height={listHeight}
deferredMeasurementCache={this.cache}
rowHeight={this.cache.rowHeight}
rowRenderer={this.renderRow}
overscanRowCount={3}
/>

传给deferredMeasurementCache的值会被用来暂时渲染数据,接着——当rowHeight的计算结果出来的时候——额外的行会流入,就像它们一直在那里。

接着我们将在rowRenderer函数里使用CellMeasure替换我们之前的div:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
rowRenderer = ({ index, parent, key, style }) => {
return (
<CellMeasurer
key={key}
cache={this.cache}
parent={parent}
columnIndex={0}
rowIndex={index}
>
<div style={style}>
<div>{this.props.data[index].username}</div>
<div>{this.props.data[index].email}</div>
</div>
</CellMeasurer>
);
};

现在数据已经被获取、缓存并准备好展示在虚拟window里了!

虚拟table

虽然本片post主要说列表,但万一当我们需要渲染table怎么办?React
Virtualized也帮我们做了这件事情。这时我们需要使用Table和Column组件。

下面是代码:

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
class App extends React.Component {
render() {
return (
<div>
<h2>Details</h2>
<Table
width={500}
height={300}
headerHeight={20}
rowHeight={40}
rowCount={this.props.data.length}
rowGetter={({ index }) => this.props.data[index]}
>
<Column
label='Username'
dataKey='username'
width={100}
/>

<Column
width={200}
label='Email'
dataKey='email'
/>
</Table>
</div>
);
}
}

这个Table组件包括下面的参数:

  • width
  • height
  • headerHeight
  • rowHeight
  • rowCount
  • rowGetter: 返回这行的数据

如果看一下Column组件,你会发现我们设置了一个dataKey参数。它是每条数据拥有的独一无二的id。

总结

希望这篇post可以帮你了解React
Virtualized可以做哪些事情,它如何让列表渲染变得很快,并如何在项目中使用它。

我们只讨论了皮毛,这个库覆盖了更多的use case,如在scroll的时候为记录generate
placeholders、实时获取/缓存数据的无限加载组件等等。

它将给你很多可以play with的东西!

此外,这个包维护的很好,实际上你可以加入Slack group来跟踪这个项目,贡献它,和其他folks取得联系。

还有一条值得注意的是,React Virtualized在StackOverflow上有它自己的标签,这是一个寻找问题的答案的地方,也可以po出你的问题。

周震南

Posted on 2019-04-20

周震南,2000年的小弟弟,今年6月才19岁,但真的很酷,我完全被圈粉了。

第一次的表演是小星星

在创造营2019的第二期,和一个老前辈battle,唱的小星星,那段rap真的太酷了。

vlog

创造营vlog-周震南,就是酷酷的,到了切苹果的时候,才暴露出18岁小孩子的一面,无厘头和幼稚。但还是透着酷。

公演

作为队长,对队员的表现不满意,连续两天练习到凌晨五、六点。有队员对这样的练习计划表示质疑,太累了,效率很低,不如早点休息,然后第二天早点开始。周震南回应很简单:“谁都不想练到五六点,为什么练到五六点,不是因为还没练好吗?练完了大家都可以回去睡觉。”这里表达的是唯结果论,几点睡有什么关系,达不到目标就不要休息。

公演结束,胜利的眼泪

第一次公演结束,投票结果周震南队赢了,接着就是周震南啜泣的声音。为什么哭了?不知道,只有自己知道。我觉得他应该是压力太大了。自己对自己要求很高,同时其他人对他要求也很高,公演练习的过程很辛苦,最终的结果是好的,让他觉得付出有了收获。

00后

00后都已经这么努力了。

1…456

沙雕爹爹

Personal tech blog.

55 posts
45 tags
© 2019 沙雕爹爹
Powered by Hexo
|
Theme — NexT.Muse v5.1.4