🏗 👏
🔥 ❄️
🤖 👾
💭 ❤️
🧑💻
lihroff / blog Goto Github PK
View Code? Open in Web Editor NEW💼此项目使用Issues来归档记录博客文章 (个人Blog、翻译、转载)
💼此项目使用Issues来归档记录博客文章 (个人Blog、翻译、转载)
🏗 👏
🔥 ❄️
🤖 👾
💭 ❤️
🧑💻
Function composition是结合两个或更多函数产生一个新函数的过程。将函数组合在一起就像是将一系列管道拼凑在一起,以便我们的数据流通。
简单的说,函数`f`和`g`的组合可以被定义为`f(g(x))`,它是从里到外 -- 右至左去执行的。换句话说,执行的顺序为:
让我们在代码中更仔细地看一下.想象你想要转换用户全名到URL slugs,以便为每个用户提供一个配置文件页面。为此,你需要经历一下步骤:
这里是一个简单的实现:
const toSlug = input => encodeURIComponent(
input.split(' ')
.map(str => str.toLowerCase())
.join('-')
);
不算太差...但是如果我告诉你可以更具可读性呢?
想象一下,这些操作都是对应可组合函数。这就可以写成:
const toSlug = input => encodeURIComponent(
join('-')(
map(toLowerCase)(
split(' ')(
input
)
)
)
);
console.log(toSlug('JS Cheerleader')); // 'js-cheerleader'
这比我们第一次尝试更难阅读,但至此我们已经走了一步。
为了实现这一目标, 我们为这些常用方法( split()
, join()
and map()
)使用了可组合模式 ,这里是其实现:
const curry = fn => (...args) => fn.bind(null, ...args);
const map = curry((fn, arr) => arr.map(fn));
const join = curry((str, arr) => arr.join(str));
const toLowerCase = str => str.toLowerCase();
const split = curry((splitOn, str) => str.split(splitOn));
除了toLowerCase()
之外,所有这些功能的生产测试版本都可以从Lodash / fp获得。您可以像这样导入它们:
import { curry, map, join, split } from 'lodash/fp';
或者 --- (cherry pick)
const curry = require('lodash/fp/curry');
const map = require('lodash/fp/map');
//...
我在这里有点懒。请注意,这种curry在技术上不是真正的curry,它总能产生一元函数。相反,这是一个简单的partial application
. 见“What’s the Difference Between Curry and Partial Application?”,但是为了本演示的目的,它将与真正的curry函数互换。
看看我们toSlug()
实现,有一些东西真的困扰我:
const toSlug = input => encodeURIComponent(
join('-')(
map(toLowerCase)(
split(' ')(
input
)
)
)
);
console.log(toSlug('JS Cheerleader')); // 'js-cheerleader'
这看对我来说看起来嵌套太多,还有一点难读。我们可以通过一个函数来展平嵌套,该函数将自动为我们组合这些函数,这意味着它将从一个函数获取输出并自动将其传入到下一个函数的输入,直到它吐出最终值。
想象,我们有一些通用方法它看起来想做下面这些事,它接受一些值并将函数应用于每个值,累积计算出单一结果。这些值本省可以是方法。这个函数被叫做reduce()
,但是为了满足以上compose函数行为,我们需要从右至左累计计算。
好事是,reduceRight()
方法正式我们寻找的:
const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);
类似reduce()
,reduceRight()
方法接受reducer函数和一个初始值(x
)。我们在这个函数数组上(从右至左),依次轮流累计计算。
使用compose,我们可以在不嵌套的情况下重写上面的组合:
const toSlug = compose(
encodeURIComponent,
join('-'),
map(toLowerCase),
split(' ')
);
console.log(toSlug('JS Cheerleader')); // 'js-cheerleader'
同样,compose()
一样 在lodash/fp中:
import { compose } from 'lodash/fp';
// or
const compose = require('lodash/fp/compose');
当你从数学形式思🤔的组合时,组合是很棒的......但是如果你想从左到右的顺序思考怎么办呢?
这又另一种形式通常叫做pipe()
. Lodash中叫做flow()
:
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);
const fn1 = s => s.toLowerCase();
const fn2 = s => s.split('').reverse().join('');
const fn3 = s => s + '!'
const newFunc = pipe(fn1, fn2, fn3);
const result = newFunc('Time'); // emit!
注意它的实现和compos()
十分相似只是它的调用顺序刚好相反。
来看一下toSlug()
函数的pipe()
实现:
const toSlug = pipe(
split(' '),
map(toLowerCase),
join('-'),
encodeURIComponent
);
console.log(toSlug('JS Cheerleader')); // 'js-cheerleader'
对我来说,这更可读一点。
original
核心的函数式程序员在整个应用中定义功能函数。我常使用它来消除中间过程的临时变量。仔细查看toSlug()
的pipe()
版本,您可能会注意到一些特殊的东西。
在命令式变成中,当你对一些变量做转换,你需要知道这个转换过程每一步对应中间变量对引用,上面的pipe()
实现是以无点的方式编写的,这意味着它根本不识别它运行的参数。
我经常使用pipes在像单元测试和Redux状态管理的reducer里来消除了对中间变量的需求,这些中间变量仅存在于一个操作和下一个操作之间的瞬态值。
这听起来可能有些诡异,但是你练习使用它,你会发现在函数式编程中,你正在使用非常抽象的通用函数,其中事物的名称并不重要。名字只是妨碍了。您可能会开始将变量视为不必要的样板。
这就是说,我认为无点的风格可一走的很远。它可能变得太密集,难以理解,但如果你感到困惑,这里有一点小建议... you can tap into the flow to trace what’s going on::
const trace = curry((label, x) => {
console.log(`== ${ label }: ${ x }`);
return x;
});
这是你如何使用:
const toSlug = pipe(
trace('input'),
split(' '),
map(toLowerCase),
trace('after map'),
join('-'),
encodeURIComponent
);
console.log(toSlug('JS Cheerleader'));
// '== input: JS Cheerleader'
// '== after map: js,cheerleader'
// 'js-cheerleader'
trace()
只是更通用的tap()
的一种特殊形式,它允许你为pipe流中的每一个值(初始值,中间值,最终值)执行一些行为。明白了?pipe?tap?你可以这样写tap()
:
const tap = curry((fn, x) => {
fn(x);
return x;
});
现在你可以看见trace()
只是tap()
的一个特例:
const trace = label => {
return tap(x => console.log(`== ${ label }: ${ x }`));
};
您应该开始了解函数式编程是什么样的,以及部分应用程序和currying如何与函数组合协作以帮助您编写更易读且更少样板的程序。
ECMAScript 2015 中引入的 JavaScript 类实质上是 JavaScript 现有的基于原型的继承的语法糖。类语法不会为JavaScript引入新的面向对象的继承模型。
那么上图中 Test 类声明被 babel presets + class-properties 转译后是如何的呢:
首先转译的代码运行在严格模式下 "use strict"
, 本质上 Babel 会解析出 AST 在根据 AST词法分析将源代码转换成向下兼容的代码,其中主要包含了2部分:
function _instanceof(left, right) {
if (
right != null &&
typeof Symbol !== "undefined" &&
right[Symbol.hasInstance]
) {
return !!right[Symbol.hasInstance](left);
} else {
return left instanceof right;
}
}
Tips: 优先使用右值的Symbol.hasInstance
方法检查,若Symbol不可用在直接使用instanceof
判断
function _classCallCheck(instance, Constructor) {
if (!_instanceof(instance, Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}
Tips:prop
参数为一个数组,包含要定义的多个属性,每个都是一个属性描述符对象 { enumerable = false, configurable = true, writable, value}
外加 key
属性用于调用 Object.defineProperty
时定义属性名。
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 Test =
/*#__PURE__*/
(function() {
function Test() {
_classCallCheck(this, Test);
_defineProperty(this, "C", function() {
console.log("Instance method");
});
_defineProperty(this, "a", undefined);
this.a = undefined;
this.A = function() {
console.log("Instance method");
};
}
_createClass(
Test,
[
{
key: "A",
value: function A() {
console.log("Prototype method");
}
},
{
key: "getA",
get: function get() {
return this.a;
}
},
{
key: "SetA",
set: function set(a) {
this.a = a;
}
}
],
[
{
key: "B",
value: function B() {
console.log("Static method");
}
}
]
);
return Test;
})();
_defineProperty(Test, "b", undefined);
通过 babel 转译后的代码就可知道类在以原型链
为基础的JavaScript语言最终实现方式。
首先关于这些名称可能很多人都很模糊不清,尽管都是有关迭代遍历但并不清楚彼此的定义和区别,那现在我们就慢慢的解释清楚。
那我们优先从每一个术语称呼开始吧,来看看这些术语对应的中文名:
iterable object: [可]迭代对象
Iteration protocols: 迭代协议
generator:生成器
细心的同学大概已经发现 Iteration protocols 是复数。是的在协议里其实有2种协议分别是:
Iteration protocols
iterator protocol:迭代器协议
iterable protocol:可迭代协议
在以往的JavaScripts中已经存在许多对集合类型对迭代方法,例如:for .. in
, for 循环
, map()
, forEach()
... 这些语法或者API都是有JavaScript内部实现如何进行迭代集合。例如:
for .. in
语法就是遍历所有non-Symbol的可枚举属性;map()
API 就是对数组对索引依次遍历得到一个新数组;那么我们如何对一个变量实现我们自定义的迭代方式呢,这就需要依靠迭代协议。其实在我看来 迭代器协议 与 可迭代协议 是非常类似的以至于初探时后很难弄清,下面我们分别来看看2个协议。
其实 迭代器协议
就是一个对象上定义了一个next
属性,而这个next
属性的定义有一定的要求,满足的话这就是一个实现了迭代器协议的对象,也被叫做 iterator : 迭代器
。
那么这个next
属性有声明要求呢?
next
是一个方法,它不接受任何参数传入;next
这个方法会返回一个对象,它包含2个属性 value
& done
,其中 value
代表本次迭代得到的数据而 done
用来表示迭代是否结束。例如:✌️PS:迭代器协议只是一种协议它指定了一个对象的迭代行为控制(每次迭代返回值、迭代终点),但如何自动迭代运行还需要自己编码实现(不然你得完全编码不停的iterator.next()、iterator.next()、iterator.next()),且每次迭代的上下文你得自己想办法保留。
其实可迭代协议
是一个对象是拥有 @@iterator 属性,而这个属性键的定义来自 Symbol.iterator
, 同样@@iterator 属性有一定要求,满足要求就实现了可迭代协议。
这些要求分别是:
这就是两种迭代协议对内容与区别,那么说完迭代协议我们可以来谈谈iterable object。
可迭代对象是对象上实现了 iterable protocol - 可迭代协议
的对象,且可以使用build-ins语法进行迭代,例如 for (let i in iterable)
、 [...iterable]
。
❌ Uncaught TypeError: object is not iterable (cannot read property Symbol(Symbol.iterator))
at <anonymous>:1:1
目前有很多JavaScript内置对数据集合已经实现了迭代器协议,有:
const iterable = [10, 20, 30];
for (const value of iterable) {
console.log(value); // 10 20 30
}
const iterable = 'boo';
for (const value of iterable) {
console.log(value); // 'b' 'o' 'o'
}
到此你可能发现了要实现自定义迭代行为在写法上还是很复杂对,并且这里存在两种迭代协议,不同的库或者工具可能选用某一种方法实现迭代对象的行为,那么就会可能造成不兼容。但由于2中协议期本质又是十分类似所有我们可以创造一个同时满足迭代器协议和可迭代协议的对象,它类似:
var myIterator = {
next: function() {
// ...
},
[Symbol.iterator]: function() { return this }
}
这样看起来还是很复杂,于是有了我们最后要说的 generator
generator对象
由 generator函数
返回,它既符合[可]迭代协议,又符合迭代器协议,就像刚刚那种模版写法。
它的写法如下:
// 生成器函数
function* gen() {
yield 1;
yield 2;
}
// 生成器对象
const g = gen();
JavaScript支持了生成器语法我们就可以更快的实现自定义的迭代对象了,例如上面的一个例子我用生成器实现是这样的:
具体的 generator
语法再次不再过多解释,这就是 generator 与 itera... 之间的关系。
这句话什么意思,我们先来看一个MDN例子🌰:
const gen = (function *(){
yield 1;
yield 2;
yield 3;
})();
for (const o of gen) {
console.log(o);
break; // Closes iterator
}
// The generator should not be re-used, the following does not make sense!
for (const o of gen) {
console.log(o); // Never called.
}
以上代码我们可以知道 generator对象 就像是一个一次性消费品(一次性筷子🥢)被迭代行为操作一次后将不会再次进行迭代。
基本上所有的东西就说完了,在补充说明最后一点东东
async/await
语法糖的引入,使得异步代码的编写与阅读更加方便。Proxy
模拟实现Lazy EvaluationIn programming language theory, lazy evaluation, or call-by-need is an evaluation strategy which delays the evaluation of an expression until its value is needed (non-strict evaluation) and which also avoids repeated evaluations (sharing).
const lazy = function (option = {}) {
const { context } = option
return function (value) {
var funcStack = [];
var oproxy = new Proxy({} , {
get : function (target, fnName) {
if (fnName === 'value') {
return funcStack.reduce(function (val, fn) {
return fn(val);
},value);
}
funcStack.push(memoize(context !== undefined ? context[fnName] : window[fnName]));
return oproxy;
}
});
return oproxy;
}
};
const shallowEqual = (newV, oldV) => newV === oldV
const fullEqual = (newArgs, lastArgs) => (
newArgs.length === lastArgs.length &&
newArgs.every(
(newArg, index) => shallowEqual(newArg, lastArgs[index]),
)
)
function memoize(fn, isEqual = fullEqual) {
let lastThis;
let lastArgs;
let lastResult;
let calledOnce = false;
return function(...newArgs) {
if (calledOnce && lastThis === this && isEqual(newArgs, lastArgs)) {
return lastResult;
}
lastResult = fn.apply(this, newArgs);
calledOnce = true;
lastThis = this;
lastArgs = newArgs;
return lastResult;
}
}
前置:目前ECMAScript对JavsScript并未实现对象属性私有化,同时public、private等字段在JavaScript中不是保留关键字。
可使用
Reflect.ownKeys()
访问该对象等自身属性(不受enumerable
影响,且不会遍历原型属性)。 Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。
forEach
语句如何跳出循环♻️?
forEach()
为每个数组元素执行callback函数;不像map()
或者reduce()
,它总是返回undefined
值,并且不可链式调用。没有办法中止或者跳出 forEach() 循环,除了抛出一个异常。如果你需要这样,这是错误的forEach
使用场景
若你需要提前终止循环,你可以使用 refer:
注:若条件允许,也可以使用 filter() 提前过滤出需要遍历的部分,再用 forEach() 处理。
int
?使用加法运算符+
可以快速实现int
类型转换。
let int = "15";
int = +int;
console.log(int); // Result: 15
console.log(typeof int); Result: "number"
这也可以适用于将布尔值转换为数字,
true -> 1 false -> 0
注意:可能存在这样的上下文,其中+
将被解释为连接运算符而不是加法运算符。当发生这种情况时(你想要返回一个整数而不是浮点数),你可以使用两个代数:~~
。
对数值n
进行按位NOT运算符(~)
得到对结果是: -n - 1, 例如:
~15 = -15 - 1 = -16 | ~-16 = -(-16) - 1 = 15
使用2次按位NOT运算符其实会抵消掉之前对运算,所以也可用于int
类型转换(额外性能损失)。
按位OR运算符(|)
是将浮点数
截断为整数
的快捷方法, 如:
console.log(16.6 | 0, -16.6 | 0) -> 16, -16
准确的说任何
按位运算符
会强制浮点数为整数,导致数值直接发生截断后再进行位运算。
而按位OR运算符(n | i)
的实际效果是将n(n != ±1)
从小数处截断得到整数s
再➕i
, 例如:
console.log(1.2 | 1) -> 1 // ⚠️
console.log(-1 | 1) -> -1 // ⚠️
console.log(16 | 1) -> 17
console.log(16.4 | 1) -> 17
console.log(16.5 | 1) -> 17
//
console.log(-16 | 1) -> 15
console.log(-16.4 | 1) -> 15
console.log(-16.5 | 1) -> 15
由以上示例可使用( n | 0)
将数值从小数位截断:
console.log(16.1 | 0) -> 16
console.log(-16.1 | 0) -> 16
附加:使用
/
与|
运算符可以实现去除整数最后几位数字 :
console.log(1553 / 10 | 0) // Result: 155
console.log(1553 / 100 | 0) // Result: 15
console.log(1553 / 1000 | 0) // Result: 1
异步构造是一种不好的设计(ES也不提倡async constructor
提案)。构造函数应该是一个单一函数,它设置实例的初始状态,而不会去其他设置。
最近在RNcoding中遇到类似Platform.select()
功能涉及原生实现用于选择不同情况下所对应到值,eg:
type BiometricType = "FACE" | "FINGERPRINT" | "NONE"
class Biometic {
bioType: BiometricType
async init () {
this.bioType = await nativeM.getBiometicType()
}
select<T>(specifics: { [type in BiometricType ]?: T }): T {
return specifics[this.bioType]
}
}
export default Biometic
敏捷团队都是自我组织的,拥有跨越团队的技能组合。这部分是通过代码审查完成的。code review可以帮助开发人员学习代码库,并帮助他们学习增加技能的新技术和技术。
当开发人员完成某个问题时(或新功能...),另一位开发人员会查看代码并考虑以下问题:
code review应与团队现有流程整合。例如,例如,如果团队正在使用任务分支工作流,则在编写完所有代码并运行并通过自动化测试之后,但在代码合并到上游之前启动代码审查。这确保了代码审查员花费时间检查机器遗漏的事情,并防止糟糕的编码决策污染主要的开发线。
无论开发方法如何,每个团队都可以从代码审查中受益。然而,敏捷团队可以实现巨大的利益,因为团队中的工作是分散的。没有人是唯一知道代码库特定部分的人。简而言之,代码审查有助于促进整个代码库在整个团队的知识共享。
所有敏捷团队的核心是无与伦比的灵活性:具有将积压工作分散出来并在所有团队成员中执行的能力。因此,团队能够更好地围绕新工作,因为没有人是“关键路径”("critical path." )。全栈工程师可以处理前端工作以及服务器端工作。
还记得estimation小节吗?估算是一项团队练习,随着产品知识在整个团队中传播,团队会做出更好的估算。随着新功能添加到现有代码中,原始开发人员可以提供良好的反馈和估计。此外,任何代码审阅者也会接触到代码库中该领域的复杂性,已知问题和关注点。然后,代码审阅者分享代码库中该部分原始开发人员的知识。这种做法创造了多种知情输入,当用于最终估计时,总是使估计更加可靠
没人想成为一段代码的唯一联系人。同样,没有人愿意深入研究他们没有写过的关键代码 - 特别是在生产紧急情况期间。代码审查在整个团队中分享知识,以便任何团队成员都可以接管并继续操纵船舶。 (We love mixed metaphors at Atlassian!) 但重点在于:没有一个开发人员是关键路径,这也意味着团队成员可以根据需要休假。
敏捷的一个特殊方面是,当新成员加入团队时,更多经验丰富的工程师会指导新成员。代码审查有助于促进有关代码库的交流。通常,隐藏在代码中的知识潜藏在团队code review期间。敏锐眼光👀的新成员,需要以新的视角发现代码库中粗糙,耗时的区域。因此,代码审查还有助于确保利用现有知识锻炼新的洞察力。
当然,他们需要时间。但那段时间并没有浪费 - 远非如此。
以下是三种优化方法。
在Atlassian,许多团队在检入代码库之前需要对任何代码进行两次审核。听起来👂有些过重?实际上,并非如此。当作者选择reviewers是,他们会使用网络告知团队。任何两位工程师都可以提供意见,这使得流程分散,以便没有人成为瓶颈,并确保整个团队的代码审查覆盖率很高。
在合并上游之前要求进行代码审查可确保没有代码进入未查看状态。这意味着在凌晨2点做出的可疑架构决策以及实习生对工厂模式的不当使用,在他们有机会对您的应用程序产生持久(和令人遗憾的)影响之前就会被捕获。
当开发人员知道他们的代码将由同事进行审核时,他们会做出额外的努力来确保所有测试都通过,并且代码设计得很好,因为他们可以使代码顺利进行。这种正念也倾向于使编码过程本身更顺畅,最终更快。
如果在开发周期的早期需要反馈及支持,请不要等待代码审查。早期反馈并经常提供更好的代码,所以不要害羞打扰其他人 - 无论何时出现时。它会让你的工作变得更好,但它也会让你的队友更好地进行代码审查。良性循环继续......!
Evaluate (eval) - 评估/执行
import()
语句允许我们动态的引入ECMAScript 模块. 但是他们也可以当作eval()
函数的一种替代品用来执行JavaScript代码(as Andrea Giammarchi recently pointed out to me)。这篇博客文章解释了它是如何工作的。
eval()
函数不支持export
和import
eval()的一个重要限制是它不支持模块语法,例如export和import。
如果我们使用import()
而不是eval()
,那么我们实际上可以执行模块代码,这将在本博客文章的后面部分看到。
未来,我们将有Realms, 大致来说, 这是功能更强大的eval 并支持模块。
import()
执行简单的代码让我们通过import()
来执行一句 console.log
const js = `console.log('Hello everyone!');`;
const encodedJs = encodeURIComponent(js);
const dataUri = 'data:text/javascript;charset=utf-8,' + encodedJs;
import(dataUri);
// > Promise {<pending>}
// Hello everyone!
发生了什么?
警告import()
不支持data URI。
由import()
返回的 Promise 的完成值是一个命名模块对象,这使我们可以访问其default值和其他命名导出值。在以下示例中,我们访问默认导出:
const js = `export default 'Returned value'`;
const dataUri = 'data:text/javascript;charset=utf-8,'
+ encodeURIComponent(js);
import(dataUri)
.then((namespaceObject) => {
console.log(namespaceObject.default)
// assert.equal(namespaceObject.default, 'Returned value');
});
// > Promise {<pending>}
// Returned value
使用适当的 esm 方法(稍后将介绍其实现),我们可以重写前面的示例,并通过Tagged templates创建数据URI:
const dataUri = esm`export default 'Returned value'`;
import(dataUri)
.then((namespaceObject) => {
assert.equal(namespaceObject.default, 'Returned value');
});
esm 的实现如下:
function esm(templateStrings, ...substitutions) {
let js = templateStrings.raw[0];
for (let i=0; i<substitutions.length; i++) {
js += substitutions[i] + templateStrings.raw[i+1];
}
return 'data:text/javascript;base64,' + btoa(js);
}
对于编码,我们已从charset = utf-8切换为base64。相比之下:
每种都有其利弊:
btoa()
是一个全局工具方法,注意:
通过 tagged templates, 我们可以嵌套 data URI 并对导入m1模块m2模块进行编码:
const m1 = esm`export function f() { return 'Hello!' }`;
const m2 = esm`import {f} from '${m1}'; export default f()+f();`;
import(m2)
.then(ns => assert.equal(ns.default, 'Hello!Hello!'));
从 GitLab 8.0 开始,GitLab CI 就已经集成在 GitLab 中,我们只要在项目中添加一个 .gitlab-ci.yml
文件,然后添加一个 Runner,即可进行持续集成。 而且随着 GitLab 的升级,GitLab CI 变得越来越强大,本文将介绍如何使用 GitLab CI 进行持续集成。
在介绍 GitLab CI 之前,我们先看看一些持续集成相关的概念。
一次 Pipeline 其实相当于一次构建任务,里面可以包含多个流程,如安装依赖、运行测试、编译、部署测试服务器、部署生产服务器等流程。
任何提交或者 Merge Request 的合并都可以触发 Pipeline,如下图所示:
+------------------+ +----------------+
| | trigger | |
| Commit / MR +---------->+ Pipeline |
| | | |
+------------------+ +----------------+
Stages 表示构建阶段,说白了就是上面提到的流程。
我们可以在一次 Pipeline 中定义多个 Stages,这些 Stages 会有以下特点:
因此,Stages 和 Pipeline 的关系就是:
+--------------------------------------------------------+
| |
| Pipeline |
| |
| +-----------+ +------------+ +------------+ |
| | Stage 1 |---->| Stage 2 |----->| Stage 3 | |
| +-----------+ +------------+ +------------+ |
| |
+--------------------------------------------------------+
Jobs 表示构建工作,表示某个 Stage 里面执行的工作。
我们可以在 Stages 里面定义多个 Jobs,这些 Jobs 会有以下特点:
所以,Jobs 和 Stage 的关系图就是:
+------------------------------------------+
| |
| Stage 1 |
| |
| +---------+ +---------+ +---------+ |
| | Job 1 | | Job 2 | | Job 3 | |
| +---------+ +---------+ +---------+ |
| |
+------------------------------------------+
理解了上面的基本概念之后,有没有觉得少了些什么东西 —— 由谁来执行这些构建任务呢?
答案就是 GitLab Runner 了!
想问为什么不是 GitLab CI 来运行那些构建任务?
一般来说,构建任务都会占用很多的系统资源 (譬如编译代码),而 GitLab CI 又是 GitLab 的一部分,如果由 GitLab CI 来运行构建任务的话,在执行构建任务的时候,GitLab 的性能会大幅下降。
GitLab CI 最大的作用是管理各个项目的构建状态,因此,运行构建任务这种浪费资源的事情就交给 GitLab Runner 来做拉!
因为 GitLab Runner 可以安装到不同的机器上,所以在构建任务运行期间并不会影响到 GitLab 的性能~
安装 GitLab Runner 太简单了,按照着 官方文档 的教程来就好拉!
下面是 Debian/Ubuntu/CentOS 的安装方法,其他系统去参考官方文档:
# For Debian/Ubuntu
$ curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-ci-multi-runner/script.deb.sh | sudo bash
$ sudo apt-get install gitlab-ci-multi-runner
# For CentOS
$ curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-ci-multi-runner/script.rpm.sh | sudo bash
$ sudo yum install gitlab-ci-multi-runner
安装好 GitLab Runner 之后,我们只要启动 Runner 然后和 CI 绑定就可以了:
当注册好 Runner 之后,可以用 [sudo] gitlab-ci-multi-runner list
命令来查看各个 Runner 的状态:
...
配置好 Runner 之后,我们要做的事情就是在项目根目录中添加.gitlab-ci.yml
文件了。
当我们添加了.gitlab-ci.yml
文件后,每次提交代码或者合并 MR 都会自动运行构建任务了。
还记得 Pipeline 是怎么触发的吗?Pipeline 也是通过提交代码或者合并 MR 来触发的!
那么 Pipeline 和 .gitlab-ci.yml 有什么关系呢?
其实 .gitlab-ci.yml 就是在定义 Pipeline 而已拉!
我们先来看看 .gitlab-ci.yml
是怎么写的:
# 定义 stages
stages:
- build
- test
# 定义 job
job1:
stage: test
script:
- echo "I am job1"
- echo "I am in test stage"
# 定义 job
job2:
stage: build
script:
- echo "I am job2"
- echo "I am in build stage"
写起来很简单吧!用 stages
关键字来定义 Pipeline
中的各个构建阶段,然后用一些非关键字来定义 jobs。
每个 job
中可以可以再用 stage
关键字来指定该 job
对应哪个 stage
。
job
里面的 script
关键字是最关键的地方了,也是每个 job
中必须要包含的,它表示每个 job
要执行的命令。
回想一下我们之前提到的 Stages 和 Jobs 的关系,然后猜猜上面例子的运行结果?
I am job2
I am in build stage
I am job1
I am in test stage
根据我们在 stages 中的定义,build 阶段要在 test 阶段之前运行,所以 stage:build 的 jobs 会先运行,之后才会运行 stage:test 的 jobs。
下面介绍一些常用的关键字,想要更加详尽的内容请前往 官方文档
定义 Stages,默认有三个 Stages,分别是 build
, test
, deploy
。
stages
的别名。
定义任何 Jobs
运行前都会执行的命令。
要求 GitLab 8.7+ 和 GitLab Runner 1.2+
定义任何 Jobs 运行完后都会执行的命令。
要求 GitLab Runner 0.5.0+
定义环境变量。
如果定义了 Job 级别的环境变量的话,该 Job 会优先使用 Job 级别的环境变量。
要求 GitLab Runner 0.7.0+
定义需要缓存的文件。
每个 Job 开始的时候,Runner 都会删掉 .gitignore 里面的文件。
如果有些文件 (如 node_modules/) 需要多个 Jobs 共用的话,我们只能让每个 Job 都先执行一遍 npm install。
这样很不方便,因此我们需要对这些文件进行缓存。缓存了的文件除了可以跨 Jobs 使用外,还可以跨 Pipeline 使用。
...
定义 Job 要运行的命令,必填项。
定义 Job 的 stage,默认为 test。
定义 Job 中生成的附件。
当该 Job 运行成功后,生成的文件可以作为附件 (如生成的二进制文件) 保留下来,打包发送到 GitLab,之后我们可以在 GitLab 的项目页面下下载该附件。
注意,不要把 artifacts
和 cache
混淆了。
下面给出一个我自己在用的例子:
stages:
- install_deps
- test
- build
- deploy_test
- deploy_production
cache:
key: ${CI_BUILD_REF_NAME}
paths:
- node_modules/
- dist/
# 安装依赖
install_deps:
stage: install_deps
only:
- develop
- master
script:
- npm install
# 运行测试用例
test:
stage: test
only:
- develop
- master
script:
- npm run test
# 编译
build:
stage: build
only:
- develop
- master
script:
- npm run clean
- npm run build:client
- npm run build:server
# 部署测试服务器
deploy_test:
stage: deploy_test
only:
- develop
script:
- pm2 delete app || true
- pm2 start app.js --name app
# 部署生产服务器
deploy_production:
stage: deploy_production
only:
- master
script:
- bash scripts/deploy/deploy.sh
上面的配置把一次 Pipeline 分成五个阶段:
安装依赖(install_deps)
运行测试(test)
编译(build)
部署测试服务器(deploy_test)
部署生产服务器(deploy_production)
设置 Job.only 后,只有当 develop 分支和 master 分支有提交的时候才会触发相关的 Jobs。
注意,我这里用 GitLab Runner 所在的服务器作为测试服务器。
P=2, Q =7
2 * 7 = 14 <== N
1 * 6 = 6 <== n
e = 5 | (or other)
5 * d % 6 = 1 ==> d = 5
c1 = 2(原文) ^ 5 % 14 = 4(密文)
c2 = 3(原文) ^ 5 % 14 = 5(密文)
m1 = 4(密文) ^ 5 % 14 = 2(原文)
m2 = 5(密文) ^ 5 % 14 = 3(原文)
现阶段 大数的质因分解是非常困难与复杂的。
在现有业务中,通常我们会从远端fetch data
然后将它赋值到mobx
的可观察的数据中,所以往往赋值发上在callback
上.
mobx4中可以通过配置强制启用严格模式
configure({enforceAction: true})
所以我们大多数现有写法雷同于用action.bound
封装一个赋值方法,如下:
@action.bound
fetchDataSuccess(str, { data }) {
this[str] = data;
}
// invoke
@action
fetchSettleYears() {
this.settleYears = []
fetchSettleYears().then(
rep => this.fetchDataSuccess('settleYears', rep), // <-
error => {}
)
}
这样写有很多弊端,比如:
action
中显得太过麻烦;[]
运算符来形成一个属性赋值模式,但在使用中可能传入错误参数名字符串(由于拼写等)。runInAction
runInAction
将自动把被修饰方法绑定this
并wrap在Action中
例如:
import { observable, action, runInAction } from 'mobx';
//....
fetchSettleYears() {
this.settleYears = []
fetchSettleYears().then(
({data}) => runInAction(() => this.settleYears = data),
error => {}
)
}
注意到一点在fetchSettleYears
中我们并没有使用action修饰方法,因为他并不是真实修改可观察数据源的触法点
en ~ 感觉好一点
async/await
替换 promise
fetchSettleYears = async () => {
const {data} = await fetchSettleYears()
runInAction(() => this.settleYears = data)
}
这样是不是更简洁。
The
babel-plugin-mobx-deep-action
plugin scans for all functions, marked as actions, and then marks all nested functions, which created inside actions as actions too.
The
babel-plugin-mobx-async-action
Converts all async actions to mobx's 4 flow utility call.
yarn add / npm install -D babel-plugin-mobx-deep-action babel-plugin-mobx-async-action
在babel中添加此plugin
//.babelrc
"plugins": [
"mobx-deep-action",
"mobx-async-action"
]
@action
fetchSettleYears() {
this.settleYears = []
fetchSettleYears().then(
({data}) => this.settleYears = data, // <-
error => {}
)
}
@action
fetchSettleYears = async () => {
const {data} = await fetchSettleYears()
this.settleYears = await data // <-
}
flow
and generator function
直接参考实例,现主流推荐方式(代码简洁,逻辑清晰)
import { flow } from 'mobx'
fetchSettleYears = flow(function*() {
const { data } = yield fetchSettleYears()
this.settleYears = data
})
缺点: [mobx] Flow expects one 1 argument and cannot be used as decorator.
在写一些应用时常常直接使用原始的css flex,其中属性较多经常需要参考CSS_TRICKS上的文章,想想干脆直接将该文章 trans 为中文方便查看。
我们的CSS flexbox布局综合指南。该完整指南解释有关flexbox的一切,专注父元素(flex容器 |
flex container
)和子元素(flex元素 |flex items
)的所有不同可能属性。它还包括历史,演示,模式和浏览器支持图表(可能未完全搬运)。
翻译中弹性item
可指flex 容器中的item,亦可指期长宽可伸缩变化。
Flexbox布局(flex box)模块(截至2017年10月的W3C候选推荐标准)旨在提供更有效的布局方式,容器中的项目之间对齐和分配空间,即使它们的大小未知和/或动态(因此单词“flex”)。
flex布局背后主要的想法是赋予修改容器子元素宽/高(和顺序)以最好的方式填充可用空间。(主要适用于所有类型的显示设备和屏幕尺寸)。Flex容器拉伸项目以填充可用空间,或缩小它们以防止溢出。
最重要的是,flexbox布局与方向无关,而不是常规布局(基于垂直的块和基于水平的内联块)。虽然这些页面适用于页面,但它们缺乏灵活性(没有双关语)来支持大型或复杂的应用程序(特别是在方向更改,调整大小,拉伸,缩小等方面)。
注意:Flexbox布局最适合应用程序的组件和小规模布局,而Grid布局则适用于更大规模的布局。
flexbox是一整套系统而不是一个单一属性,它包含了一系列的属性。其中一些是被用在容器上的(父元素,被叫做“flex container”)其他的则被用来设置子元素(被叫做“flex items”)。
如果“常规”布局基于块和内联流方向,则flex布局基于“flex-flow directions”。请查看规范中的这个图,解释flex布局背后的主要**。
子元素将被以此放置在main axis
(从main-start
至main end
)或者corss-axis
(从corss-start
至corss end
) [注:主轴或纵轴]
主轴 main axis:主轴数flex 容器沿主要方向放置item的轴。 flex-direction
属性
main-start | main-end: flex item被放置在flex container中起始与main-start终止与main-end
main-size:从main-start到main-end的长度它决定了flex container的width或者height
cross-axis:垂直于主轴的轴称为横轴。其方向取决于主轴方向。
[其余cross-* 类比与 main-*] ......
以下介绍真实设置在父元素的 css 属性
在元素上声明display属性则定义了一个flex container;内联或块级取决于给定的值。它为所有直接孩子提供了flex上下文。
.container {
display: flex; /* or inline-flex */
}
// Note that CSS columns have no effect on a flex container.
次属性定义了主轴(main-axis),导致flex item在flex contain中的排列顺序。
.container {
flex-direction: row(default) | row-reverse | column | column-reverse;
}
left to right
从左至右;在right to left
从右至左row
类似,但是是从上至下column
相反默认,flex items将试图排列在一行。您可以更改它并允许item根据需要使用此属性进行换行。
.container{
flex-wrap: nowrap(default) | wrap | wrap-reverse;
}
wrap
相反缩写
flex-flow: <‘flex-direction’> || <‘flex-wrap’>
此定义沿主轴的对其方式。它有助于构建item之间的额外空间(当item都没有flex-grow时
)或者是弹性的但已经达到最大值。It also exerts some control over the alignment of items when they overflow the line.
.container {
justify-content: flex-start(default) | flex-end | center | space-between | space-around | space-evenly;
}
此定义了如何沿当前行的纵轴布置弹性item的默认行为。可以将其视为纵轴(垂直于主轴)的对齐版本。
.container {
align-items: stretch(default) | flex-start | flex-end | center | baseline;
}
This aligns a flex container's lines within when there is extra space in the cross-axis, similar to how justify-content aligns individual items within the main-axis.
当纵轴上有额外的空间时,这会将flex容器的线对齐,类似于在主轴内对齐内容的内容。
**注意:**此属性对应只有一行flex item没有效果
.container {
align-content: flex-start | flex-end | center | space-between | space-around | stretch(default)(仍然受限与max-height/max-width);
}
以下介绍真实设置在子元素的 css 属性
默认情况下,flex item按源顺序排列。但是,order属性控制它们在flex容器中的显示顺序。
.item {
order: <integer>; /* 默认 0 */
}
这定义了flex item在必要时拉伸的能力。这根据每一个item的比例来分配
默认值0代表不会被拉伸, 其他值按照分配比例划分
.item {
flex-grow: <number>; /* 默认 0 */
// 负数是非法的
}
这定义了flex item在必要时缩小的能力。
默认为1代表总items超过了 flex container 的大小会被均匀的收缩(但父元素中flex-wrap必须是 nowrap)
.item {
flex-shrink: <number>; /* 默认 1 */
// 负数是非法的
}
This defines the default size of an element before the remaining space is distributed. It can be a length (e.g. 20%, 5rem, etc.) or a keyword. The auto keyword means "look at my width or height property" (which was temporarily done by the main-size keyword until deprecated). The content keyword means "size it based on the item's content" - this keyword isn't well supported yet, so it's hard to test and harder to know what its brethren max-content, min-content, and fit-content do.
.item {
flex-basis: <length> | auto; /* default auto */
}
If set to 0, the extra space around content isn't factored in. If set to auto, the extra space is distributed based on its flex-grow value. See this graphic.
This is the shorthand for flex-grow, flex-shrink and flex-basis combined. The second and third parameters (flex-shrink and flex-basis) are optional. Default is 0 1 auto.
.item {
flex: none | [ <'flex-grow'> <'flex-shrink'>? || <'flex-basis'> ]
}
It is recommended that you use this shorthand property rather than set the individual properties. The short hand sets the other values intelligently.
此允许改写某一个item默认但对其方式。
请参阅align-items说明以了解可用值。
.item {
align-self: auto | flex-start | flex-end | center | baseline | stretch;
}
请注意,float,clear和vertical-align对flex item没有影响。
由版本分解"flexbox":
-(new)表示规范中的最新语法(例如display:flex;)
所以你很想知道被叫做 Reactive Programming 的
新东西, 特别是它的变体包括 Rx, Bacon.js, RAC 和其他。
... ...
学习之旅中最苦难的部分是 以Reactive思考。这就需要抛弃老式的声明式编程和典型编程习惯,强制你的大脑使用不同的范式工作。这方面我没有在互联网上找到任何的指南,并且我认为在世界上需有关于如何以Reactive思考的实用教程,所以你可以开始了。阅读库文档可以点亮你的道路,我希望这也能帮助到你。
在网上有太多不好的解释和定义。... ...
Reactive Programming 是结合 异步 数据流 的编程方式
简言之,这些都不是新事物。Event buses(???) 或者你典型的点击事件就是真实的一个异步事件流,你可以观察它执行一些副作用。流十分廉价和普及,任何事物都可以成为流:变量,用户输入,属性,缓存,数据结构,等。例如,想象一下,你的Twitter提醒将是一个与点击事件相同的数据流。你可以监听这个流并做成相应反应。
**在此之上,你被给予了一个惊人的 toolbox of functions to combine, 创建、过滤这些任意的流。**这就是“功能性”魔术引入。流可以被当作另一个的输入。甚至多个流都可以作为其他流的输入。你可以合并2条流。你可以过滤流获得只包含你感兴趣的事件的另一条流。你可以将流中数据映射到另一条流中。
如果流对于Reactive来说是如此重要,那么让我们仔细看看它们,从我们熟悉的“点击按钮”事件流开始。
流是一系列随事件推进的事件。它可以发出三种形式:一种值(某种类型<联系Promise.resolve(value)>),一个错误(error),或者“完成的(completed)”信号。 考虑到“完成”发生,例如,当包含该按钮的当前窗口或视图关闭时。
我们仅异步捕获这些发出的事件,通过定义一个方法将在值发出时被调用。有时候剩余的这两种可被省略,你可以仅仅专注定义值发出时对应的方法。监听此流被叫做 subscribing 。这些定义的方法是 observers。 流是被观察的主体(或“可观察的”)。这恰好就是观察者模式。
绘制该图的另一种方法是使用ASCII,我们将在本教程的某些部分使用它:
--a---b-c---d---X---|->
a, b, c, d are emitted values
X is an error
| is the 'completed' signal
---> is the timeline
因为这已经非常熟悉,我不希望你觉得无聊,让我开始些新的:我们将创建一个点击事件流从原始点击事件流中转换而来。
首先,让我们创建一个计数器流,指示单击按钮的次数。通常 Reactive 库,每条流都有许多方法附加其上,例如map
,filter
,scan
,等。当你调用这些方法,例如 clickStream.map(f)
, 它返回一条基于点击流的 新的流 。它无论如何也不会修改原始的点击流。这个特性叫做不可变性,它和Reactive流相随就像煎饼和糖浆搭配一般。这允许我们链接像clickStream.map(f).scan(g)
这样的函数:
clickStream: ---c----c--c----c------c-->
vvvvv map(c becomes 1) vvvv
---1----1--1----1------1-->
vvvvvvvvv scan(+) vvvvvvvvv
counterStream: ---1----2--3----4------5-->
map(f)
方法通过你提供的方法f
更替(到新的流中)每一个发出的值。在此例中,我们我们映射点击的值变为数字1. scan(g)
方法累积所有流上原始的值,产生新值x = g(accumulated, current)
, 这里g
是一个相加方法。接着counterStream
发出点击时触发的总次数。
为了演示Reactive的强大, 我们假设你想要流上的"双击"事件。为了使它更有意思,我们想得到的新流中的双击和三击,或者通常,多次点击(2此或者更多)。想想我们用原始的方式完成会不会很复杂。
好的,在Reactive中非常的简单。实际上,真真的逻辑就4行代码。但我们先忽略代码。思考以下图例以帮助我们更好的理解和构建流,无论你是菜鸟还是专家。
灰色块状表示方法转换流成为新流。首先我们累积流上但点击,whenever 250 milliseconds of "event silence" has happened (that's what buffer(stream.throttle(250ms)) does, in a nutshell. 不必担心不理解这点细节,我们现在只是演示Reactive。这样结果是一流列表,然我们我应用map()
同映射每一个列表得到每个列表的长度。最后我们忽略长度为1的值通过filter(x >= 2)
方法。就是这样:3个操作生产我们想要的流。接着我们可以定义它,对我们的期望作出反应。
我希望你享受这美妙的方法。这里的例子只是冰山一角:你可以应用同样的操作在不通的流上,例如,在API相应流;另一方面,还有许多其他的方法可以使用。
Reactive Programming提高了代码的抽象级别,因此您可以专注于定义业务逻辑的事件的相互依赖性,而不必不断地调整大量的实现细节。 使用RP的代码可能更简洁。
现代webapps和移动应用程序中的好处更加明显,这些应用程序与大量与数据事件相关的UI事件具有高度交互性。10年前,与网页的交互基本上是关于向后端提交长格式并对前端执行简单渲染。 应用程序已经发展为更加实时:修改单个表单字段可以自动触发向后端的保存,“喜欢”某些内容可以实时反映给其他连接的用户,等等。
如今的应用程序拥有丰富的各种实时事件,可为用户提供高度互动的体验。 我们需要工具来正确处理它,而Reactive Programming就是一个答案。
让我们深入了解真实的东西。 一个真实的例子,提供了如何在RP中思考的分步指南。 没有合成的例子,没有半解释的概念。 在本教程结束时,我们将生成真正的功能代码,同时了解我们为什么要做每件事。
我选取了 JavaScript 和 RxJS作为例子的工具,因为: JavaScript已然成为当今最流行的语言, 并且 Rx* library family 在各种语言和平台上都有实现(.NET, Python, C++, JAVA....)。所以无论你的工具选取是什么,按照本教程,您可以获得具体的益处。
在Twitter中有一个UI元素用于建议你可以关注其他账户:
我们将专注莫让其核心功能,即:
我们可以抛弃其他的功能和按钮因为他们是次要的。实际上Twitter最近意见关闭了未授权访问的API, 让我们构建此UI去关注Github的用户。这里是Github 获取用户的 API
The complete code for this is ready at http://jsfiddle.net/staltz/8jFJH/48/ in case you want to take a peak already.
异步函数声明定义了一个异步函数。异步函数是一个通过事件循环异步操作的函数,使用隐式Promise返回其结果。但是使用异步函数的代码的语法和结构更像是使用标准同步函数。
由 babel preset 也可看出 async 函数属于 es2017 标准提案的功能语法。
在 babel preset 中勾选上 es2017 可得到 转译后的兼容代码如下:
由 img-2 可以看出 async / await 实际上只是 generator fn 语法糖(line: 38-42), 其内部还是依靠 generator fn 返回的迭代器对象。但由于 generator 函数的执行控制时需要使用迭代器对象上next()
方法控制,那async如何控制执行流程的呢? 这就依赖于 img-1 上的实现。
我们将上述代码添加注释以便能够洞察其基本实现:
// #2.
// asyncGeneratorStep 基于 promise 控制 generator fn 的执行
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
try {
// 内部首先调用 next 或 throw 方法
var info = gen[key](arg);
var value = info.value;
} catch (error) {
reject(error);
return;
}
// 若迭代器 finished 则直接fulfilled promise
if (info.done) {
resolve(value);
} else {
// 否则使用 then 链接再次调用
Promise.resolve(value).then(_next, _throw);
}
}
// #1.
// _asyncToGenerator定义一个方法接受一个 <generator fn>
function _asyncToGenerator(fn) {
// _asyncToGenerator 返回一个 fn
return function() {
// 该 fn 记录了调用时参数以及this只想
var self = this,
args = arguments;
// 该 fn 返回一个 Promise 实例
return new Promise(function(resolve, reject) {
// executor 中首先调用 generator fn 得到一个迭代器对象 gen
var gen = fn.apply(self, args);
// 定义,封装 _next 方法,其接受一个参数(value)[该参数用于2此迭代之间数据传递].
function _next(value) {
// 调用 asyncGeneratorStep
asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
}
// 定义,封装 _throw 方法
function _throw(err) {
asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
}
// 首次调用next(类似 gen.next())
_next(undefined);
});
};
}
近期参与了一个项目,大致上是提供一个模版网站,在样式的实现和维护上逐渐暴露出系列问题,整理下自己的想法.
2021年了,在angular
, react
, vue
等主流前端框架大行其道的时代,在CSS方案层出不穷的年代,像BootStrap
,Tailwind
还有必要使用吗?
我对此的看法稍后提到,在此之前我们先看看目前大部分CSS
框架是什么。
由于CSS
属于声明式语言,不像JavaScript
那样的命令式语言构成命令逻辑。这样好处在于用户只需声明对应样式代码,
不需要关心内部的渲染机制等就能得到期望的样式装饰效果,因此CSS
的真正魅力在于艺术,是更具创作力的编程语言来丰富页面感官。
既然CSS
是充满艺术的声明式语言,那么CSS
框架能为我们提供什么?从多个框架的介绍中不难发现许多高频字:responsive, mobile-first, quickly, utility
。再从源码中你会发现不管它们各自使用什么方案最终为用户提供的都是一些预定义的样式类,通过这些类能:
所以用户最终主要还是在class
属性上使用这些预定义的类名。但CSS
更多的乐趣(动效、艺术感)是样式框架少有满足的。
CSS
框架回归上面的问题, 我的看法是:
如果你觉得 👆 是团队需要考虑的那么我建议你使用CSS框架。当然用了样式框架也会产生新问题,比较凸显的是:
Pre-defined modular CSS class and utilities with responsive.
经过上面项目的洗礼,我自己也好久没有使用样式框架了。初在项目上使用时有种生疏感,体现在:
所以最近开始造了 budwise 这个轮子(主要还是因为它不复杂👌)。
首先说说为什么主要用 sass
来实现它。经过阅读与学习其他框架源码,目前大多数都通过两种方式:一是 sass、less
等富样式语言编译生成;二是通过 js
编写在转换生成样式。我更青睐第一种因为它更接近css
易读性好, 性能也不是第一因素(编译构建阶段生成)还有社区中用 sass
编写样式也更受欢迎.
在做BudWise
时并没有考虑取代主流样式框架,我觉得也不现实。但是在实现中做出了一些和其他框架的差异的考量:
Dart-Sass
提供便利性在有精力的前提下,造轮子对我带来的许多直观的意义,不限于:
CSS
知识, 权衡实现方案的优劣与不足,寻找改善方案.👀👀👀
我们以一个toy demo 开始.
// dom level
-> div (onclick)
-> p
-> span
问题🤔: 为什么当我们点击<p>
, <span>
click 事件也触发了?
冒泡是事件的一种传递机制,当事件触发时,事件会以相反的顺序传播从目标元素传递至父级元素,最后以Window结束。
就拿前面的例子🌰来说,当用户点击<span>
元素时,事件会从下至上(子元素->父元素)依次触发元素的click事件。
我们知道在事件的执行机制中除了冒泡还有一种就是捕获。
其实这是不完全正确的!!!,依据W3C的定义, 事件的执行分为了3阶段:
Capture(捕获阶段): 事件对象通过Window传播到目标的祖先父级再到自身。
Target(目标阶段): 事件对象到达目标自身, 当该事件类型指定不Bubble, 则事件将在此阶段终止(稍后解释)。
Bubble(冒泡阶段): 事件对象以相反的顺序传播通过目标的父级祖先,并以Window结束。
所以当一个事件发生时标准的传递流为 捕获 -> 目标 -> 冒泡
当我们程序在主流的浏览器运行时,我们在 html 中使用 on[eventType]
或者 javascript 中使用 element. addEventListener(eventType, listener)
这些Web API像是忽略了 捕获阶段 只会运行 目标阶段 和 冒泡阶段。
问题🤔:那么这时候就产生了一个问题? 若我们在一个嵌套的Dom上分别添加事件,我们就不能改变事件的触发顺序(子元素绑定的事件会先于父元素绑定的事件触发),我们如何改变这个顺序?
options ={}
| useCapture = boolean 】感谢 🙏 Web API 的完备性,如果你看过 addEventListener
API 的定义,你会发现,声明是有第三个参数选项的,它可传入一个 boolean 或者 一个对象。
target.addEventListener(type, listener[, options]);
target.addEventListener(type, listener[, useCapture]);
所以当你在一个 target 上添加对应的事件监听时, 你可以这样写:
elem.addEventListener(_, _, {capture: true})
// 对象的形式还接受更多的option: {once,...}
// 或者
elem.addEventListener(_, _, true)
他们是等价的,表示监听的回调将会在 捕获阶段 被触发。那么这样我们可以解决上面的问题。
例如:
// dom level
-> div (onclick= log('div', true))
-> p (onclick = log('p'))
-> span
// 此时输出顺序为 div -> p
// 相反
-> div (onclick= log('div'))
-> p (onclick = log('p'))
-> span
// 此时输出顺序为 p -> div
首先对于listener来说,它不仅仅只能传入function
还能是一个对象但这个对象必须实现Event
接口(包含handleEvent(fn)
属性),在此我们不对它过多解释,具体可以去参看文档.
接下来我们具体说说 listener 被调用时传入的参数 Event 或 简写为 e
// dom level
-> div (onclick)
-> p
-> span
还是使用之前的例子,我们解释几个误区(这里可能存在错误🙅,希望大佬发现后不吝纠正):
<div>
添加了 click 事件,是否代表 <p>
, <span>
也添加了 click 事件
答: ❌, 事实上在 <div>
添加了 click 与它的子元素并没有任何关系,但是可以通过
Event
对象拿到触发事件的真真对象,这看起来就好像是 <div>
的子元素同样添加了此事件监听,所以回调是发生在目标元素身上的(click 的 listener callback 是发生在 div上的)。
答: 从之前的 addEventListener API 我们知道 事件是可以绑定到 冒泡 或者 捕获 阶段的,当没有设置是默认是在 冒泡阶段 所以只会触发一次,那就是在对应的绑定阶段。 注意: 既然添加事件是区分阶段,那么在移除此事件时也需要明确对应阶段。
解释了上述误区,我们回过来说 e.target
:
当事件触发时,e.target
(read-only)的值为最深嵌套相关元素,而并不一定为添加事件所对应元素。例如上诉例子,当你点击<span>
时事件传递冒泡走到<div>
元素其 click 事件回调被触发,而e.target
的值是 span
元素对象。
我们知道了 e.target
(read-only) 并不一定为添加事件所对应元素,那么如何在回调中知道是那个元素添加的此事件监听呢? 这就是 e.currentTarget
,另外在 listener 方法内部也可以直接使用 this
,它等同于(this = event.currentTarget
)
e.path
(read-only)为一个数组eg: [span, p, div, body, html, document, Window]
它表示从 e.target
到 window
所经历到元素层级。
我们在事件传递阶段讲 捕获阶段 的时候提到 可以提前终止不在进行冒泡阶段。 这是怎么做的了,其实可以在捕获阶段添加的监听事件回调被调用时候调用 e.stopPropagation()
来阻止事件在DOM中进一步传播。 由于历史原因也可以调用 e.cancelBubble = true
来阻止事件冒泡(但不建议使用此属性,最好使用 stopPropagation
方法)
Event
对象上还有许多属性,在这里不会全部罗列,最常用的基本上就是以上几个,其余属性还可以拿到很多信息,但部分属性可能并不是标准
以上基本是你需要知道EventListener的所有知识,如有不全或错误请指教👆
在ant-design-pro(antpro)中有一系列的Authrize
组件他们是antpro里授权的实现其中包含多种使用场景,而umi框架中使用配置式路由内置提供了特定的属性Routes
能在配置路由时集成权限控制。
下面会参数关于antpro和umi细节:
权限组件,通过比对现有权限与准入权限,决定相关元素(子组件 \ noMatch组件)的展示。
查看Authrize的入口index.js
import Authorized from './Authorized';
import AuthorizedRoute from './AuthorizedRoute';
import Secured from './Secured';
import check from './CheckPermissions';
import renderAuthorize from './renderAuthorize';
Authorized.Secured = Secured;
Authorized.AuthorizedRoute = AuthorizedRoute;
Authorized.check = check;
export default renderAuthorize(Authorized);
从源码可以看到其导入了5个对象。前面4个都对应着权限组件的不同使用场景,其中Authorized
是最常用的其他3个都作为属性挂载在Authorized
。最后导入的是renderAuthorize
, 然后默认导出renderAuthorize(Authorized)
renderAuthorize
那么renderAuthorize
是干什么用的呢?
let CURRENT = 'NULL';
/**
* use authority or getAuthority
* @param {string|()=>String} currentAuthority
*/
const renderAuthorize = Authorized => currentAuthority => {
if (currentAuthority) {
if (typeof currentAuthority === 'function') {
CURRENT = currentAuthority();
}
if (
Object.prototype.toString.call(currentAuthority) === '[object String]' ||
Array.isArray(currentAuthority)
) {
CURRENT = currentAuthority;
}
} else {
CURRENT = 'NULL';
}
return Authorized;
};
export { CURRENT };
export default Authorized => renderAuthorize(Authorized);
由源码可以看出renderAuthorize
是一个高阶函数, 在第一次调用时传入Authorized
,再次调用时传入currentAuthority
此参数代表当前[用户]拥有权限(当currentAuthority
为函数此时会执行函数将返回结果赋值到CURRENT
, 当currentAuthority
为数组或者字符串则直接赋值到CURRENT
, 否者CURRENT
为字符串NULL,而这个CURRENT
就缓冲这当前[用户]拥有权限, 可以从该文件导出使用),最后执行返回第一次调用的Authorized
组件。
Authorized
、Secured
、 AuthorizedRoute
、 check
是什么?有什么区别?Authorized
import CheckPermissions from './CheckPermissions';
const Authorized = ({ children, authority, noMatch = null }) => {
const childrenRender = typeof children === 'undefined' ? null : children;
return CheckPermissions(authority, childrenRender, noMatch);
};
export default Authorized;
由源码可以看出Authorized就是一个纯函数组件它接受三个属性children, authority, noMatch
,然后返回CheckPermissions
的执行结果。children:表示权限判断通过时展示什么,authority:表示展示children需要那些权限(之一),noMatch:表示权限判断失败时展示什么
AuthorizedRoute
import React from 'react';
import { Route, Redirect } from 'umi';
import Authorized from './Authorized';
const AuthorizedRoute = ({ component: Component, render, authority, redirectPath, ...rest }) => (
<Authorized
authority={authority}
noMatch={<Route {...rest} render={() => <Redirect to={{ pathname: redirectPath }} />} />}
>
<Route {...rest} render={props => (Component ? <Component {...props} /> : render(props))} />
</Authorized>
);
export default AuthorizedRoute;
AuthorizedRoute
和Authorized
类似,区别在于noMatch使用Route
render Redirect
到其他路由,权限判断通过时也是由Route
包裹。
而Secured
差不多是Authorized
到另一种用法,例如:
const { Secured } = RenderAuthorized('user');
@Secured('admin')
class TestSecuredString extends React.Component {
render() {
return (
<Alert message="user Passed!" type="success" showIcon />
)
}
}
Secured
将作为类修饰符使用,参入到参数就是Authorized
中到authority
参数。
check
import React from 'react';
import PromiseRender from './PromiseRender';
import { CURRENT } from './renderAuthorize';
/**
* 通用权限检查方法
* Common check permissions method
* @param { 权限判定 | Permission judgment } authority
* @param { 你的权限 | Your permission description } currentAuthority
* @param { 通过的组件 | Passing components } target
* @param { 未通过的组件 | no pass components } Exception
*/
const checkPermissions = (authority, currentAuthority, target, Exception) => {
// 没有判定权限.默认查看所有
// Retirement authority, return target;
if (!authority) {
return target;
}
// 数组处理
if (Array.isArray(authority)) {
if (Array.isArray(currentAuthority)) {
if (currentAuthority.some(item => authority.includes(item))) {
return target;
}
} else if (authority.includes(currentAuthority)) {
return target;
}
return Exception;
}
// string 处理
if (typeof authority === 'string') {
if (Array.isArray(currentAuthority)) {
if (currentAuthority.some(item => authority === item)) {
return target;
}
} else if (authority === currentAuthority) {
return target;
}
return Exception;
}
// Promise 处理
if (authority instanceof Promise) {
return <PromiseRender ok={target} error={Exception} promise={authority} />;
}
// Function 处理
if (typeof authority === 'function') {
try {
const bool = authority(currentAuthority);
// 函数执行后返回值是 Promise
if (bool instanceof Promise) {
return <PromiseRender ok={target} error={Exception} promise={bool} />;
}
if (bool) {
return target;
}
return Exception;
} catch (error) {
throw error;
}
}
throw new Error('unsupported parameters');
};
export { checkPermissions };
const check = (authority, target, Exception) =>
checkPermissions(authority, CURRENT, target, Exception);
export default check;
由源码可处导出check
方法接受的参数与Authorized
接受的属性相同
authority === this.props.authority
target === this.props.children
Exception === this.props.noMatch
check
方法直接返回调用checkPermissions
的结果。那么checkPermissions
是什么,这就是antpro中如何实现准入权限
与现有权限
的判断,最后导出渲染不同的结果。
上图中在配置路由中传入了2个额外的属性Routes
与authority
, 他们是干什么用的呢?
看出umi源文件umi/packages/umi/src/renderRoutes.js
,由如下代码:
function withRoutes(route) {
if (RouteInstanceMap.has(route)) {
return RouteInstanceMap.get(route);
}
const { Routes } = route;
let len = Routes.length - 1;
let Component = args => {
const { render, ...props } = args;
return render(props);
};
while (len >= 0) {
const AuthRoute = Routes[len];
const OldComponent = Component;
Component = props => (
<AuthRoute {...props}>
<OldComponent {...props} />
</AuthRoute>
);
len -= 1;
}
由此看出:
const { Routes } = route;
从每个route对象上读取Routes属性while
循环嵌套 外部传入Routes
权限组件,而传入authority
属性通过spread operater 传入到Routes
权限组件这就是权限组件的准入权限的列表。大致就是这样在配置上实现了权限控制。
注:为减少歧义本文中所有并发或并行均指示有处理多个任务的能力,并不是严格意义上的并发与并行.
你可能已经以一种方式或者其他途径听过Go。它备受欢迎是有一定道理的。Go 快速、间断,背后有一个强大的社区。学习该语言最令人激动的方面是它的并发模型。Go的并发原生支持(原语)(concurrency primitives)使它能简单快乐的创建并发、多线程程序。我将通过插图介绍Go的并发原语,以使这些概念能够为将来学习所用。本文适用于刚接触Go并想开始了解Go并发原语(go例程和通道)的人员。
你或许之前写过许多单线程程序。使用多个方法执行一个特殊的任务是编程中一个常见的模式,但是它们是等到上一部分程序返回数据后才被调用。
这就是我们最初设置第一个示例的方式,该程序可开采矿石。此示例中的功能执行:寻找矿石,开采矿石 和 冶炼矿石 。在我们的示例中,矿山和矿石被表示为字符串数组,每个函数都接受并返回一个“已处理”字符串数组。对于单线程应用程序,该程序将设计如下。
这里有3个主要的方法。 finder、miner、smelter 。在此版本的程序中,我们的方法运行在一个单一线程上,一个接着一个 --- 并且这个单线程(这只地鼠名叫Gary)需要做所有的事情。
func main() {
theMine := [5]string{“rock”, “ore”, “ore”, “rock”, “ore”}
foundOre := finder(theMine)
minedOre := miner(foundOre)
smelter(minedOre)
}
在每个函数的末尾打印出结果数组“ ore”,我们得到以下输出:
From Finder: [ore ore ore]
From Miner: [minedOre minedOre minedOre]
From Smelter: [smeltedOre smeltedOre smeltedOre]
这种方式是的程序设计简洁,但是如果你想利用多线程并独立调用每个发放会发生什么呢?这就是并发编程起作用的地方。
这种挖掘效率更高。现在,多个线程(地鼠们)正在独立工作。因此,整个操作并不全在Gary身上。有一个地鼠在寻找矿石,一个在开采矿石,另一个在冶炼矿石–可能也是同时在做。
为了将这种类型的功能引入我们的代码中,我们需要做两件事:一中可以创建独立工作地鼠的方法和一个地鼠们可以相互交流(传递矿石)的方式。这就是Go并发中自建的: go routine 和 channel。
Go routines 可以被认为是轻量的线程。 创建 go routines 就是将 go
关键字添加到调用方法前面一样简单。举一个简单的例子,让我们创建两个查找器函数,使用go关键字调用它们,并在每次在矿井中发现“矿石”时将它们打印出来。
func main() {
theMine := [5]string{“rock”, “ore”, “ore”, “rock”, “ore”}
go finder1(theMine)
go finder2(theMine)
<-time.After(time.Second * 5) // 目前你可以忽略它
}
这是我们程序的一次输出:
Finder 1 found ore!
Finder 2 found ore!
Finder 1 found ore!
Finder 1 found ore!
Finder 2 found ore!
Finder 2 found ore!
从上面的输出中可以看到,查找正在同时运行。谁先找到矿石没有可靠的顺序,多次运行后顺序并不总是相同。
这是巨大的进步!现在,我们有了一种简单的方法来建立多线程(多线程)程序,但是当我们需要独立的go例程相互通信时会发生什么呢?欢迎来到神奇的 channel 世界。
Channels 使得 go routines可以相互通信。你可以将channels想象为一个通道,每一个routine都可以发送和接受其他routine信息。
myFirstChannel := make(chan string)
Go routines 可以发送和接受数据与一个channel之上。这通过使用一个箭头<-
表明数据的流向来完成。
myFirstChannel <- "hello" // Send
myVariable := <- myFirstChannel // Receive
现在,通过使用渠道,我们可以让寻找矿石的地鼠立即将他们发现的东西发送给破碎矿石的地鼠,而无需等待发现所有事情。
我已经更新了示例,因此将查找程序代码和挖掘方法设置为未命名(匿名)的功能。如果您从未见过lambda函数没有过多关注程序的那部分,那就知道每个函数都用go关键字调用了,因此它们是在自己的go例程上运行的。重要的是要注意go例程如何使用通道oreChan在彼此之间传递数据。不用担心,我将在最后解释未命名的功能。
func main() {
theMine := []string{"ore1", "ore2", "ore3"}
oreChan := make(chan string)
// Finder
go func(mine []string) {
for _, item := range mine {
oreChan <- item //send
}
}(theMine)
// Ore Breaker
go func() {
for i := 0; i < 3; i++ {
foundOre := <-oreChan //receive
fmt.Println("Miner: Received " + foundOre + " from finder")
}
}()
<-time.After(time.Second * 5) // 再一次再次忽略
}
你可看到如下输出
Miner: Received ore1 from finder
Miner: Received ore2 from finder
Miner: Received ore3 from finder
太好了,现在我们可以在程序中的不同go例程(地鼠)之间发送数据了。在开始编写带有通道的复杂程序之前,让我们首先介绍一些对理解通道属性至关重要的知识。
在各种情况下,通道都会阻止例程执行。这样一来,我们的go例程就可以彼此同步一会儿,然后再继续独立运行。(注:形成异步控制)
一旦一个 go routine(地鼠)往 channel 发送数据, 发送的 go routine 将阻塞,直到另一个go例程接收到通道上发送的内容为止。
与阻止发送类似,尚未发送任何东西时, 一个 go routine 可以阻塞直到从通道获取一个值。
阻塞起初可能会有些混乱,但是您可以将其视为两个go例程(密码)之间的事务。无论地鼠是在等待钱还是在汇款,它都会等到交易中的另一位伙伴出现。
现在我们有两种不同的方式可与阻塞 go routine 在 channel 中的传递,让我们来讨论两种不同的 channel 类型: unbuffered 和 buffered 。选择使用哪种类型的通道可以更改程序的行为。
在前面的所有示例中,我们一直在使用无缓冲通道。使它们与众不同的原因是一次只能通过通道容纳一个数据。
在并发程序中,时机并不总是完美。在我们的采矿示例中,我们可能会遇到这样一种情况,我们的勘测地鼠在开采地鼠处理一件矿石的时间内可以找到3块矿石。为了不让勘测地鼠花费大部分时间等待向破碎的地鼠发送一些矿石直到完成,我们可以使用缓冲通道。让我们首先创建一个容量为3的缓冲通道。
bufferedChan := make(chan string, 3)
缓冲通道类似无缓冲通道,但是有一点不同,我们可以不必在另一个 go runtine 读取通道的数据前发送多条数据到通道之中。
bufferedChan := make(chan string, 3)
go func() {
bufferedChan <- "first"
fmt.Println("Sent 1st")
bufferedChan <- "second"
fmt.Println("Sent 2nd")
bufferedChan <- "third"
fmt.Println("Sent 3rd")
}()
<-time.After(time.Second * 1)
go func() {
firstRead := <- bufferedChan
fmt.Println("Receiving..")
fmt.Println(firstRead)
secondRead := <- bufferedChan
fmt.Println(secondRead)
thirdRead := <- bufferedChan
fmt.Println(thirdRead)
}()
输出
Sent 1st
Sent 2nd
Sent 3rd
Receiving..
first
second
third
为简单起见,我们不会在最终程序中使用缓冲的通道,但是理解使用哪种 Channel 在你的并发模型中很重要。
现在,借助go例程和通道的强大功能,我们可以使用Go的并发原语编写一个充分利用多个线程的程序。
theMine := [5]string{"rock", "ore", "ore", "rock", "ore"}
oreChannel := make(chan string)
minedOreChan := make(chan string)
// Finder
go func(mine [5]string) {
for _, item := range mine {
if item == "ore" {
oreChannel <- item //send item on oreChannel
}
}
}(theMine)
// Ore Breaker
go func() {
for i := 0; i < 3; i++ {
foundOre := <-oreChannel //read from oreChannel
fmt.Println("From Finder: ", foundOre)
minedOreChan <- "minedOre" //send to minedOreChan
}
}()
// Smelter
go func() {
for i := 0; i < 3; i++ {
minedOre := <-minedOreChan //read from minedOreChan
fmt.Println("From Miner: ", minedOre)
fmt.Println("From Smelter: Ore is smelted")
}
}()
<-time.After(time.Second * 5) // Again, you can ignore this
输出:
From Finder: ore
From Finder: ore
From Miner: minedOre
From Smelter: Ore is smelted
From Miner: minedOre
From Smelter: Ore is smelted
From Finder: ore
From Miner: minedOre
From Smelter: Ore is smelted
与原始示例相比,这是一个很大的改进!现在,我们的每个函数都在自己的go例程上独立运行。另外,每当加工一块矿石时,它都会进入我们采矿线的下一个阶段。
为了始终专注于了解通道的基本知识和执行例程,上面没有提到一些重要信息-如果您不知道,可能会在开始编程时造成一些麻烦。现在,您已经了解了go例程和通道的工作原理,下面让我们了解一些在开始使用go例程和通道进行编码之前应该了解的信息。
与我们使用go关键字将函数设置为在自己的go例程上运行的方式类似,我们可以使用以下格式创建匿名函数以在其go例程上运行:
// Anonymous go routine
go func() {
fmt.Println("I'm running in my own go routine")
}()
这样,如果我们只需要调用一次函数,就可以将其放在自己的go例程中运行,而不必担心创建正式的函数声明。
main 函数实际上在自己的 go routine 中运行!更重要的是要知道,一旦主函数返回,它将关闭当前正在运行的所有其他go例程。这就是为什么我们在主要功能的底部有一个计时器的原因-它创建了一个通道并在5秒钟后发送了一个值。
<-time.After(time.Second * 5) //Receiving from channel after 5 sec
还记得go例程将如何阻止读取,直到发送一些东西?通过在上面添加此代码,这正是主例程正在发生的事情。主例程将阻塞,使我们的其他go例程有5秒钟的额外运行时间。
现在,有更好的方法来处理阻塞主函数的操作,直到完成所有其他go例程。通常的做法是创建一个完成的通道,主要功能在等待读取时会阻塞该通道。完成工作后,写入此通道,程序将结束。
func main() {
doneChan := make(chan string)
go func() {
// Do some work…
doneChan <- “I’m all done!”
}()
<-doneChan // block until go routine signals work is done
}
In a previous example we had our miner reading from a channel in a for loop that went through 3 iterations. What would happen if we didn’t know exactly how many pieces of ore would come from the finder? Well, similar to doing ranges over collections, you can range over a channel.
Updating our previous miner function, we could write:
// Ore Breaker
go func() {
for foundOre := range oreChan {
fmt.Println(“Miner: Received “ + foundOre + “ from finder”)
}
}()
Since the miner needs to read everything that the finder sends him, ranging over the channel here makes sure we receive everything that gets sent.
Note: Ranging over a channel will block until another item is sent on the channel. The only way to stop the go routine from blocking after all sends have occurred is by closing the channel with ‘close(channel)’
But you just told us all about how channels block go routines?! True, but there is a technique where you can make a non-blocking read on a channel, using Go’s select case
structure. By using the structure below, your go routine will read from the channel if there’s something there, or run the default case.
go func(){
myChan <- “Message!”
}()
select {
case msg := <- myChan:
fmt.Println(msg)
default:
fmt.Println(“No Msg”)
}
<-time.After(time.Second * 1)
select {
case msg := <- myChan:
fmt.Println(msg)
default:
fmt.Println(“No Msg”)
}
Non-blocking sends use the same select case structure to perform their non-blocking operations, the only difference is our case would look like a send rather than a receive.
select {
case myChan <- “message”:
fmt.Println(“sent the message”)
default:
fmt.Println(“no message sent”)
}
Origin: Lazy loading (and preloading) components in React 16.6
React 16.6加入了一些新的特征使code splitting(代码分割)更加简单: React.lazy()
.
让我们以一个小demo来看看如何以及为什么使用这个特征。
我们又一个app可以显示股票列表。当你点击一份股票你可以看到一份图表:
这个应用就是这样的。你可以在github repo 上看到源码(同时通过pull request来查看每次提交的需求和修改)。
本片,我们仅仅关心App.js
文件:
我们有一个App
组件它接受一系列股票并显示<StockTable>
. 当你从表格中选择一份股票,App
组件将针对于这份股票显示<StockChart/>
。
有什么问题?好吧,我们希望我们的应用程序能够快速启动并尽可能快地显示<StockTable />
,但是我们要等浏览器下载( 并解压缩,解析,编译和运行)<StockChart/>
的代码。
让我们看一下显示<StockTable />
所需的时间:
显示StockTable需要2470毫秒(模拟的快速3G网络和4倍速的CPU)。
预期所致,里面包含react,react-dom和其他一些react依赖。但是我们还包含moment,lodash和victory,这些仅仅是在<StockChart />
显示时的需要,并不是<StockTable />
所用到的。
我们可以做些什么来避免依赖关系来减慢的加载速度?我们可以懒惰加载组件(lazy-load the component)。
使用 dynamic import 我们可以将我们的JavaScript代码一分为二,一份main 文件仅仅是显示<StockTable />
所需要的代码另一份是<StackChart>
所需要的代码和依赖。
这种技术非常有用,React 16.6添加了一个API,使其更易于与React组件一起使用:React.lazy()
。
为了使用React.lazy()
我们对App.js
做了两处改动:
首先,我们使用React.lazy()
传入一个方法它返回一 dynamic import 来代替原来的静态导入(static import)。现在浏览器不会下载./StockChart.js
(及其依赖项),直到我们第一次渲染它。
但是当React视图渲染<StockChart />
并且它还没有载入代码时会发生什么? 这就是我们为什么引入<React.Suspense/>
。它会渲染fallback
属性来代替它的children
属性,直到子节点全部加载完毕。
现在我们的app将有2个bundled文件:
main.js文件是36KB。另一个文件是89KB,其代码来自./StockChart
及其所有依赖项。
浏览器需要760毫秒来下载主js文件(而不是1250毫秒)和61毫秒来评估脚本(而不是487毫秒)。<StockTable />
以1546毫秒(而不是2470毫秒)显示。
我们使我们的app加载的更快,但是我们有了另一个问题:
在显示图表之前注意“正在加载...”(试一试)
当用户第一次点击一份股票时, "Loading" fallback 生效。那是因为我们需要等到浏览器加载<StockChart />
的代码。
如果我们需要摆脱“Loading” fallback, 我们需要在用户点击股票之前加载那些代码。
预加载代码的简单方法是在调用React.lazy()
之前启动动态导入:
当我们调用动态导入时,组件将开始加载,而不会阻止<StockTable />
的呈现。
Take a look at how the trace changed from the original eager-loading app:
【注: 本质上是使用promise异步加载其他JS代码而不去阻塞主线程渲染】。
现在,用户将仅仅在显示表格后不到一秒的时间内第一次点击一份股票才能看见“Loading” fallback 试一试。
You could also enhance the
lazy
function to make it easier to preload components whenever you need:
对于我们的小Demo这些已经够了。 对于大型的apps Lazy component在渲染之前也许有其他的lazy code或者数据待加载。所以用户还需要等待其他的加载。
另一种预加载组件的方法是在需要之前实际渲染它,我们想渲染它但我们不想显示它,所以我们将它隐藏起来:
React将在第一次呈现应用时开始加载<StockChart />
, 但是这次他实际上会试图渲染<StockChart />
, 所以如果需要加载任何其他依赖项(代码或数据),它将被加载。
我们将惰性组件包装在隐藏的div中,因此在加载后它不会显示任何内容。我们用另一个<React.Suspense />
将div
包装在另一个<React.Suspense />
中,因此它在加载时不会显示任何内容。
Note: hidden is the HTML attribute for indicating that the element is not yet relevant. The browser won’t render elements with this attribute. React doesn’t do anything special with that attribute(but it may start giving hidden elements a lower priority in future releases).
最后一种方法在许多情况下很有用,但它有一些问题。
首先,使用隐藏属性隐藏渲染的惰性组件不是防弹的。例如,懒加载组件可以使用portal这将不会被隐藏。(这有只用hack way不需要而外的div并且同样适用于portals,但是这是hack,它会被打破)。
第二,及时将组件显示为隐藏仍然加入了不必要渲染的节点到DOM上,这是一个性能问题。
A better aproach would be to tell react to render the lazy component but without comitting it to the DOM after it’s loaded. But, as far as I know, it isn’t possible with the current version of React.
我们可以做的另一项改进是在预加载图表组件时重用我们渲染的元素,因此当我们想要实际显示图表时,React不需要再次创建它们。如果我们知道用户将点击什么库存,我们甚至可以在用户点击之前使用正确的数据呈现它(就像这样)。
就是这些,谢谢阅读。
<img src srcset ...
实现响应式图像定义不同大小的同一图像,允许浏览器来选择合适的图像源。
// xxx/logo.png & xxx/[email protected]
<Img
src={`${ICON_BASE}/logo.png`}
srcset={`${ICON_BASE}/[email protected] 2x`}
...
/>
/*** basic usage
srcset="
url size,
url size
"
***/
Read more: https://html.com/attributes/img-srcset
悬停缩放(zoom-on-hover)效果是一种吸引注意力到可点击图像的好方法。当用户将鼠标悬停在其上方时,图像会略微放大,但其尺寸保持不变。
:hover
选择器上设置图片放大效果;overflow
属性为 hidden, 并固定其width, height
属性..inner-img {
transition: 0.3s;
}
.inner-img:hover {
transform: scale(1.1);
}
.img-wrapper {
width: 400px;
height: 400px;
overflow: hidden;
}
Demo: https://codepen.io/woo_tommy_l/pen/oNNpGXN
通过
radial-gradient()
函数,其由一个从原点辐射开的在两个或者多个颜色之前的渐变组成。
#example {
background: radial-gradient(150px 15px ellipse at bottom, green 0%, transparent 100%)
// background: radial-gradient(150px 15px ellipse at 50% 100%, green 0%, transparent 100%)
}
Demo: https://www.w3schools.com/code/tryit.asp?filename=GCVMITBN6HOR
使用 CSS
line-clamp
属性设置截断文本的最大行数.
Demo : https://codepen.io/woo_tommy_l/pen/NWRevmK
注
-webkit-
前缀。clamp.js
(https://github.com/josephschmitt/Clamp.js)]来动态设置节流[throttle]与防抖[debounce]在前端领域经常涉及,下面我们会尝试解释其中的原理与差异与实现以及一些应用场景
leading
和trailing
(前置或后置)或both。immediate
option与上面所谈到的设置leading: true
类似。debounce: Debounce technique allows us to "group" multiple sequential calls in a single one.
防抖: 防抖技术允许我们捆绑多个连续调用成为单一的一次调用。
可简单的理解防抖是将一次调用发生时的前后时间(TIMING)断内不允许再次触发,若多次触发则方法的真实调用根据设置可以在:
例如当设置leading: true
且 TIME = 400ms
/**
* 返回一个函数,只要它一直被触发将不会被调用
* 函数将在其不再被触发的N毫秒后调用,如果immediate被传入那么
* 函数将在第一次触发是立即调用
*
*/
// es6 syntax import & export
export function deBounce(func, delay, immediate) {
let timeout;
return function executedFunction() {
const context = this;
const args = arguments;
var later = function() {
timeout = null;
if(!immediate) func.apply(context, args);
}
const callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, delay);
if (callNow) func.apply(context, args);
}
}
// 这是其中的一种实现关于leading与trailing可自行调整immediate。
这个简单的举个🌰: 在autocomplete中keypress事件与ajax配合使用可减少不必要的请求,可以参考Corbacho所作的demo.
throttle: Throttle technique don't allow us to execute our function more than once every X milliseconds.
节流: 节流技术是我们不能在X毫秒内触发第二次函数调用。
简单的理解节流就像控制水龙头单位时间内的出水量一样,在一个设定时间段内只能触发一次调用。若在一个时间段内连续触发多次函数真实调用根据设置可以在:
例如当设置leading: true
且 TIME = 400ms
可见第一段中我一直在触发函数但正式但函数调用是在调用后但400ms后再次调用,在看第二段在首次触发后我在接着但300和400ms均匀触发函数但是后面不再触发导致函数没有方式第二次调用...
/**
* 简单做法,leading
*/
export function throttle(fn, limit) {
let delay = false
return (...args) => {
if (!delay) {
fn.apply(this, args)
delay = true
setTimeout(() => {
delay = false
}, limit)
}
}
}
个人看过一个比较有趣的例子是使用节流实现无限下拉,使用节流控制是保证用户在获取新内容可以即使但又不会过于频繁, demo在此。
开篇首先提出2个混淆的术语:Authentication
& Authorization
。这2个对我感受他们均涉及到会话安全但侧重点有所不用,如下:
Authentication:认证,简言之 - 你是谁
如:登录
Authorization:授权,简言之 - 你被允许访问什么
如:登录系统后可访问级别,第三方应用登录授权
答:基于角色的授权方式是一种方式,其基本**是传递一个允许查看给定路径的角色列表,并检查当前登录用户所有权限是否是该列表中的角色之一。
以下列举有参考ant-design-pro项目对于授权的实现
首先我们可以改进的是将授权的逻辑封装到一个独立的组建中并使用 function child以防止在未授权用户的情况下mounting组建并在你的路由组建(route handler)渲染它
// Route handler
class YourRoute extends React.Component {
constructor(props) {
super(props)
// Load user from wherever into state.
}
render() {
return <Authorization allowed={this.props.allowed} user={this.state.user}>
{() =>
/* the rest of your page */
}
<Authorization>
}
}
export default YourRoute
// Router configuration
<Router history={BrowserHistory}>
<Route path="/" component={App}>
<Route
allowed={['manager', 'admin']}
path="feature"
component={YourRoute}
/>
</Route>
</Router>
以此我们有了可复用的认证逻辑,这朝着正确方向迈出的一步。我们甚至可以使Authorization
组建去加载用户角色本身,这样就只需要allowed属性。但是每一个route现在都需要写入授权这看起来不怎么好。
所以有个其他选择,HOC,可以移动授权逻辑完全移出路由组建。假设 Authorization HOC 自己加载当前登录用户角色,他看起来像
// Authorization HOC
const Authorization = (WrappedComponent, allowedRoles) =>
return class WithAuthorization extends React.Component {
constructor(props) {
super(props)
// In this case the user is hardcoded, but it could be loaded from anywhere.
// Redux, MobX, RxJS, Backbone...
this.state = {
user: {
name: 'vcarl',
role: 'admin'
}
}
}
render() {
const { role } = this.state.user
if (allowedRoles.includes(role)) {
return <WrappedComponent {...this.props} />
} else {
return <h1>No page for you!</h1>
}
}
}
// Route handler
class YourRoute extends React.Component {
render() {
return <div>
/* the rest of your page */
</div>
}
}
export default Authorization(YourRoute, ['manager', 'admin'])
// Router configuration
<Router history={BrowserHistory}>
<Route path="/" component={App}>
<Route
path="feature"
component={YourRoute}
/>
</Route>
</Router>
我几乎没有包含授权HOC的实现,因为它取决于您用于存储数据的方式,是否要显示错误消息或在失败时重定向到其他位置,您正在使用哪个router等有很多未知数,我想关注模式而不是实现细节。重要的是它检查当前登录的用户并呈现他们有足够角色的包装组件。
因为在何时调用HOC是无关紧要的,我们可以将方法调用移至router configuration.
// Route handler
class YourRoute extends React.Component {
render() {
return <div>
/* the rest of your page */
</div>
}
}
- export default Authorization(YourRoute, ['manager', 'admin'])
+ export default YourRoute
// Router configuration
<Router history={BrowserHistory}>
<Route path="/" component={App}>
<Route
path="feature"
- component={YourRoute}
+ component={Authorization(YourRoute, ['manager', 'admin'])}
/>
</Route>
</Router>
接着,我们现在在路由组建(route handler)中没有一丝授权逻辑,没有什么通过react-router
传入component,我们所有允许的角色都在一个文件中定义。但是你知道,如果我们有很多route,这将意味着许多重复的角色定义。
让我们来做2个改动,反转我们传入HOC方法的两个参数顺序并是HOC curry化。我们的HOC成为返回HOC函数。如果使用Redux,这种类型的函数调用看起来应该很熟悉,因为它与connect使用的机制相同。
// Authorization HOC
- const Authorization = (WrappedComponent, allowedRoles) =>
+ const Authorization = (allowedRoles) =>
+ (WrappedComponent) =>
return class WithAuthorization extends React.Component {
constructor(props) {
super(props)
// ...
// Router configuration
<Router history={BrowserHistory}>
<Route path="/" component={App}>
<Route
path="feature"
- component={Authorization(YourRoute, ['manager', 'admin'])}
+ component={Authorization(['manager', 'admin'])(YourRoute)}
/>
</Route>
</Router>
所以我们现在可以这样使用(事先定义好各类role HOC)
// Router configuration
+ const Manager = Authorization(['manager', 'admin'])
<Router history={BrowserHistory}>
<Route path="/" component={App}>
<Route
path="feature"
- component={Authorization(['manager', 'admin'])(YourRoute)}
+ component={Manager(YourRoute)}
/>
</Route>
</Router>
现在我们可以预先定义一系列的role HOC然后我们可以在router configuration 使用
// Router configuration
const User = Authorization(['user', 'manager', 'admin'])
const Manager = Authorization(['manager', 'admin'])
const Admin = Authorization(['admin'])
<Router history={BrowserHistory}>
<Route path="/" component={App}>
<Route path="users" component={User(Users)}>
<Route path=":id/edit" component={Manager(EditUser)} />
<Route path="create" component={Admin(CreateUser)} />
</Route>
</Route>
</Router>
当然,客户端授权只是其中的一部分。后端应始终强制执行用户角色,因为客户端上的所有数据都可以从devtools更改。
其他: #14 - ant-design-pro 与 umi 相辅相成的授权实现
Interface
对深度解密Go语言之关于 interface 的 10 个问题的总结
Go 中不用显示的声明需要实现某个接口而是在结构上实现某个接口的方法。
// >>> Golang >>>
type Ixxx interface {
Method()
}
func (x Xxx) Method() {
}
// >>> Java >>>
public interface Ixxx {
public void method();
}
public class xxx implements Ixxx {
public void method() {
}
}
GitFlow是Git的一个分支管理模式,由Vincent Driessen创造。它由引起了很多重视由于它是非常适合团队合作和开发团体扩展的。
GitFlow最突出的一个优点是非常适合平行开发,它通过隔离已完成的工作与新的开发任务。新的开发任务(如新需求和非紧急bug修复)在feature branches上完成,并仅仅当开发准备上线时候merge
回
虽然中断是BadThing(tm),但如果要求您从一个任务切换到另一个任务,您需要做的就是提交更改,然后为新任务创建新的功能分支。完成该任务后,只需checkout
原先功能分支,然后就可以继续中断。
功能分支还使两个或更多开发人员可以更轻松地在同一功能上进行协作,因为每个功能分支都是沙箱,其中唯一的更改是使新功能正常工作所需的更改。这使得十分容易检查各个成员做了一些什么事在新功能点上。
随着新开发的完成,它将合并回develop branch,这是所有尚未发布的已完成功能的临时区域。因此,当下一个版本分支开发时,它将自动包含已完成的所有新内容。
GitFlow支持hoxfix branches -- 由tagged创建的分支。您可以使用这些进行紧急更改,因为您知道此修补程序仅包含您的紧急修复程序。您不会意外地同时合并新开发项目。
新功能(新功能,非紧急错误修复)在feature branches中开发:
Feature branches从develop branch分支拉取出来,完成的功能和修复在准备发布时合并回develop branch:
当什么时候要发布,发布时从** develop创建一个release branch**:
在release branch上的代码被部署在合适的测试环境,并且任意的问题都直接在release branch上修复。这种** deploy -> test -> fix -> redeploy -> retest**循环指导完全准备正式发布上线给客户受用。
当发布完成,release branch需要同时merge回master和develop,以保证任何在release branch上的修改都不会意外的在新开发时丢失。
master branch追踪最终发布的
代码。commits到master上的代码全部来自与release branches and hotfix branches的merge.
Hotfix branches被用来解决紧急修复:
他们也是从(master branch)tagged上拉取的分支
,当它们完成后需要同时合并会master和develop确保在下一个常规版本发生时不会意外丢失此修补程序。
Nginx是一款Web服务器,具备Web的基本功能:基于REST架构风格,以统一资源描述符或者资源定位符作为沟通一句,通过HTTP为浏览器等客户端程序提供各种网络服务.
现代互联网应用大多基于CS模式(client-server)。代理服务(proxy server)是一种服务(计算机系统或应用程序),其充当来自从其他客户端的请求寻求服务器资源的的中介。例如:
反向代理(reverse proxy):
反向代理接收来自Internet的请求并将其转发到内部网络中的服务器。那些发出请求的人连接到代理,可能不知道内部网络。
在反向代理中(事实上,这种情况基本发生在所有的大型网站的页面请求中),客户端发送的请求,想要访问server服务器上的内容。但将被发送到一个代理服务器proxy,这个代理服务器将把请求代理到和自己属于同一个LAN下的内部服务器上,而用户真正想获得的内容就储存在这些内部服务器上。代理对用户是透明的,即无感知。不论加不加这个反向代理,用户都是通过相同的请求进行的,且不需要任何额外的操作;代理服务器通过代理内部服务器接受域外客户端的请求,并将请求发送到对应的内部服务器上。
正向代理(forward proxy)
通俗等讲正向代理(一般又直接叫做代理)是用来做转发代理的。当一个客户端在Internet上对文件传输服务器进行连接尝试时,其请求必须首先通过转发代理。根据转发代理的设置,可以允许或拒绝请求。如果允许,则将请求转发到防火墙(可有可没有),然后转发到文件传输服务器。从文件传输服务器的角度来看,它是发出请求的代理服务器,而不是客户端。因此,当服务器响应时,它会对代理响应。但是当转发代理接收到响应时,它会将其识别为对之前经历的请求的响应。然后它又将响应发送给发出请求的客户端。由此可以看出正向代理对用户来说并非透明,即是你自己配置或者清楚知道请求将被代理服务器代理。
nginx是高度模块化对,有很多基本对功能都能通过对其设置配置而达到,比如以下几点常用功能:
函数式编程实践 💯
主要项目结构
ramda # root dir
│ dist # 输出目录
│ test # unit test目录
│ lib # 性能测试 & 指标
│ scripts
│
└───source
│ │ F.js # 输出方法
│ │ T.js # ...
│ │
│ └───internal # 内部方法
│ │ file111.txt
│ │ file112.txt
│ │ ...
│
│ # 杂项文件
│ .eslintrc
│ .eslintignore
│ package.json
│ README.md
│ ...
└
false
/**
* A function that always returns `false`. Any passed in parameters are ignored.
*
* @func
* @memberOf R
* @since v0.9.0
* @category Function
* @sig * -> Boolean
* @param {*}
* @return {Boolean}
* @see R.T
* @example
*
* R.F(); //=> false
*/
var F = function() {return false;};
export default F;
T
函数与F
函数类似不同的是调用T
永远返回true
为什么要有这2个函数? --- ramda作为JS函数式变成的最佳实践,它提倡纯函数式风格一切的逻辑都是在函数的相互配合下完成的这可以使你的工作更加简单,代码更加优雅
Ramda中的函数是自动被curry化的(现目前做法实际是将所有
export
的方法都通过了_curryN
去做📦wrapped);
Ramda函数的参数顺序为了便于curry都是被排列过的。待处理操作的数据通常最后提供。(并有filp
方法和placehold
解决自定义方法参数顺序问题)
ramda
柯里化实现'@@functional/placeholder': true
, 这使得调用被curry化的函数在参数传递时候不需要依次传入而可以使用占位符先传递后续参数/**
* A special placeholder value used to specify "gaps" within curried functions,
* allowing partial application of any combination of arguments, regardless of
* their positions.
*
* If `g` is a curried ternary function and `_` is `R.__`, the following are
* equivalent:
*
* - `g(1, 2, 3)`
* - `g(_, 2, 3)(1)`
* - `g(_, _, 3)(1)(2)`
* - `g(_, _, 3)(1, 2)`
* - `g(_, 2, _)(1, 3)`
* - `g(_, 2)(1)(3)`
* - `g(_, 2)(1, 3)`
* - `g(_, 2)(_, 3)(1)`
*
* @name __
* @constant
* @memberOf R
* @since v0.6.0
* @category Function
* @example
*
* const greet = R.replace('{name}', R.__, 'Hello, {name}!');
* greet('Alice'); //=> 'Hello, Alice!'
*/
export default {'@@functional/placeholder': true};
placehold
对象export default function _isPlaceholder(a) {
return a != null &&
typeof a === 'object' &&
a['@@functional/placeholder'] === true;
}
由于JavaScript使用较为灵活,定义函数后参数的传递不必按照函数签名传递,实际参数可多可少。实参可由
arguments
获取,它是一个类数组对象。
在
ramda
内部为了使curry
化更加高效其预定义了_curry[1 | 2 | 3]
他们是针对待curry
化函数真实对应实参调用为1 | 2 | 3个时分别对应使用。
这样做为什么会高效? 因为知道了一个
curry
函数实际调用时参数的个数这样curry
化阶段就不再需要判断这个函数是否还有待其它参数传递的逻辑处理。
注: fn’s arity 代表 fn.length
one-arity
一元函数( = 形参个数为1)柯里化实现import _isPlaceholder from './_isPlaceholder';
/**
* Optimized internal one-arity curry function.
*
* @private
* @category Function
* @param {Function} fn The function to curry.
* @return {Function} The curried function.
*/
export default function _curry1(fn) {
return function f1(a) {
// 若实际调用时为传递实参或者实参为placehold对象则依旧返回currid fn 否则触发调用
if (arguments.length === 0 || _isPlaceholder(a)) {
return f1;
} else {
return fn.apply(this, arguments);
}
};
}
import _curry1 from './_curry1';
import _isPlaceholder from './_isPlaceholder';
/**
* Optimized internal two-arity curry function.
*
* @private
* @category Function
* @param {Function} fn The function to curry.
* @return {Function} The curried function.
*/
export default function _curry2(fn) {
return function f2(a, b) {
switch (arguments.length) {
case 0:
return f2;
case 1:
return _isPlaceholder(a)
? f2
: _curry1(function(_b) { return fn(a, _b); });
default:
return _isPlaceholder(a) && _isPlaceholder(b)
? f2
: _isPlaceholder(a)
? _curry1(function(_a) { return fn(_a, b); })
: _isPlaceholder(b)
? _curry1(function(_b) { return fn(a, _b); })
: fn(a, b);
}
};
}
由于有了_curry1.js的实现那么_curry2实际上就只需要根据实参的数量和类型判断是应该直接返回
curried fn
;还是调用_curry1去curry化一个新函数该函数接受一个参数执行返回原fn
调用结果;或者直接触发原fn
调用。具体逻辑如上
import _curry1 from './_curry1';
import _curry2 from './_curry2';
import _isPlaceholder from './_isPlaceholder';
/**
* Optimized internal three-arity curry function.
*
* @private
* @category Function
* @param {Function} fn The function to curry.
* @return {Function} The curried function.
*/
export default function _curry3(fn) {
return function f3(a, b, c) {
switch (arguments.length) {
case 0:
return f3;
case 1:
return _isPlaceholder(a)
? f3
: _curry2(function(_b, _c) { return fn(a, _b, _c); });
case 2:
return _isPlaceholder(a) && _isPlaceholder(b)
? f3
: _isPlaceholder(a)
? _curry2(function(_a, _c) { return fn(_a, b, _c); })
: _isPlaceholder(b)
? _curry2(function(_b, _c) { return fn(a, _b, _c); })
: _curry1(function(_c) { return fn(a, b, _c); });
default:
return _isPlaceholder(a) && _isPlaceholder(b) && _isPlaceholder(c)
? f3
: _isPlaceholder(a) && _isPlaceholder(b)
? _curry2(function(_a, _b) { return fn(_a, _b, c); })
: _isPlaceholder(a) && _isPlaceholder(c)
? _curry2(function(_a, _c) { return fn(_a, b, _c); })
: _isPlaceholder(b) && _isPlaceholder(c)
? _curry2(function(_b, _c) { return fn(a, _b, _c); })
: _isPlaceholder(a)
? _curry1(function(_a) { return fn(_a, b, c); })
: _isPlaceholder(b)
? _curry1(function(_b) { return fn(a, _b, c); })
: _isPlaceholder(c)
? _curry1(function(_c) { return fn(a, b, _c); })
: fn(a, b, c);
}
};
}
同理 _curry3 依赖 _curry[1 | 2] 的实现
看了_curry[1 | 2 | 3]可以发掘
ramda
柯里化的模式:基础实现_curry1, 然后后续的_curryN都需要依赖_curry[N-1] ... _curry1. 其中使用switch
语法去检测实参的个数分别控制,每个分支有去判断各个实参是否为占位符决定使用前一个curry化方法去科里一个新函数还是返回被curried fn
export default function _arity(n, fn) {
/* eslint-disable no-unused-vars */
switch (n) {
case 0: return function() { return fn.apply(this, arguments); };
case 1: return function(a0) { return fn.apply(this, arguments); };
case 2: return function(a0, a1) { return fn.apply(this, arguments); };
case 3: return function(a0, a1, a2) { return fn.apply(this, arguments); };
case 4: return function(a0, a1, a2, a3) { return fn.apply(this, arguments); };
case 5: return function(a0, a1, a2, a3, a4) { return fn.apply(this, arguments); };
case 6: return function(a0, a1, a2, a3, a4, a5) { return fn.apply(this, arguments); };
case 7: return function(a0, a1, a2, a3, a4, a5, a6) { return fn.apply(this, arguments); };
case 8: return function(a0, a1, a2, a3, a4, a5, a6, a7) { return fn.apply(this, arguments); };
case 9: return function(a0, a1, a2, a3, a4, a5, a6, a7, a8) { return fn.apply(this, arguments); };
case 10: return function(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9) { return fn.apply(this, arguments); };
default: throw new Error('First argument to _arity must be a non-negative integer no greater than ten');
}
}
由此方法的实现可以看出该方法接受2个参数。第一个为number类型(代表待curry化函数的形参个数),第二个参数为function类型(代表待curry化函数本身)
_arity
内部逻辑就是一个简单的switch case
语句,根据第一个参数返回不同的函数(其函数签名不同主要是定义形参的个数)。调用返回的实际上就是返回调用fn.apply(this, arguments);
的结果。
由此也能看见,
Ramda
中curry的从实现层面还是由一定缺陷的(当待curry化当函数形参大于10个时会抛出异常)。但从艺术与哲学&实用性方面的思考也没必要覆盖所有场景,在编程最佳实践中大多数人都会告诉你定义的方法形参最多不要超过6
个左右,如果你超过了就需要考虑是否有必要对方法进行拆分(基于SOILD原则)。
import _arity from './_arity';
import _isPlaceholder from './_isPlaceholder';
/**
* Internal curryN function.
*
* @private
* @category Function
* @param {Number} The arity of the curried function.
* @param {Array} An array of arguments received thus far.
* @param {Function} The function to curry.
* @return {Function} The curried function.
*/
export default function _curryN(length, received, fn) {
return function() {
var combined = [];
var argsIdx = 0;
var left = length;
var combinedIdx = 0;
while (combinedIdx < received.length || argsIdx < arguments.length) {
var result;
if (combinedIdx < received.length &&
(!_isPlaceholder(received[combinedIdx]) ||
argsIdx >= arguments.length)) {
result = received[combinedIdx];
} else {
result = arguments[argsIdx];
argsIdx += 1;
}
combined[combinedIdx] = result;
if (!_isPlaceholder(result)) {
left -= 1;
}
combinedIdx += 1;
}
return left <= 0
? fn.apply(this, combined)
: _arity(left, _curryN(length, combined, fn));
};
}
Ramda
的柯里化实现基于一个原则以函数签名来确定柯里化(实际上由fn.length
可以找到形参的个数),由函数签名可以知道此函数内部逻辑处理了多少参数,当调用传递的实参个数不够时是不会触发真实调用的。
柯里化的实现描述比较复杂但本质就是循环♻️检查已接受到的参数和本次传入的传入是否完整。完整则触发调用,不完整则使用
_arity
模版继续递归调用_curryN
ramda
最终暴露出来的curry方法import _arity from './internal/_arity';
import _curry1 from './internal/_curry1';
import _curry2 from './internal/_curry2';
import _curryN from './internal/_curryN';
/**
* Returns a curried equivalent of the provided function, with the specified
* arity. The curried function has two unusual capabilities. First, its
* arguments needn't be provided one at a time. If `g` is `R.curryN(3, f)`, the
* following are equivalent:
*
* - `g(1)(2)(3)`
* - `g(1)(2, 3)`
* - `g(1, 2)(3)`
* - `g(1, 2, 3)`
*
* Secondly, the special placeholder value [`R.__`](#__) may be used to specify
* "gaps", allowing partial application of any combination of arguments,
* regardless of their positions. If `g` is as above and `_` is [`R.__`](#__),
* the following are equivalent:
*
* - `g(1, 2, 3)`
* - `g(_, 2, 3)(1)`
* - `g(_, _, 3)(1)(2)`
* - `g(_, _, 3)(1, 2)`
* - `g(_, 2)(1)(3)`
* - `g(_, 2)(1, 3)`
* - `g(_, 2)(_, 3)(1)`
*
* @func
* @memberOf R
* @since v0.5.0
* @category Function
* @sig Number -> (* -> a) -> (* -> a)
* @param {Number} length The arity for the returned function.
* @param {Function} fn The function to curry.
* @return {Function} A new, curried function.
* @see R.curry
* @example
*
* const sumArgs = (...args) => R.sum(args);
*
* const curriedAddFourNumbers = R.curryN(4, sumArgs);
* const f = curriedAddFourNumbers(1, 2);
* const g = f(3);
* g(4); //=> 10
*/
var curryN = _curry2(function curryN(length, fn) {
if (length === 1) {
return _curry1(fn);
}
return _arity(length, _curryN(length, [], fn));
});
export default curryN;
import _curry1 from './internal/_curry1';
import curryN from './curryN';
/**
* Returns a curried equivalent of the provided function. The curried function
* has two unusual capabilities. First, its arguments needn't be provided one
* at a time. If `f` is a ternary function and `g` is `R.curry(f)`, the
* following are equivalent:
*
* - `g(1)(2)(3)`
* - `g(1)(2, 3)`
* - `g(1, 2)(3)`
* - `g(1, 2, 3)`
*
* Secondly, the special placeholder value [`R.__`](#__) may be used to specify
* "gaps", allowing partial application of any combination of arguments,
* regardless of their positions. If `g` is as above and `_` is [`R.__`](#__),
* the following are equivalent:
*
* - `g(1, 2, 3)`
* - `g(_, 2, 3)(1)`
* - `g(_, _, 3)(1)(2)`
* - `g(_, _, 3)(1, 2)`
* - `g(_, 2)(1)(3)`
* - `g(_, 2)(1, 3)`
* - `g(_, 2)(_, 3)(1)`
*
* @func
* @memberOf R
* @since v0.1.0
* @category Function
* @sig (* -> a) -> (* -> a)
* @param {Function} fn The function to curry.
* @return {Function} A new, curried function.
* @see R.curryN, R.partial
* @example
*
* const addFourNumbers = (a, b, c, d) => a + b + c + d;
*
* const curriedAddFourNumbers = R.curry(addFourNumbers);
* const f = curriedAddFourNumbers(1, 2);
* const g = f(3);
* g(4); //=> 10
*/
var curry = _curry1(function curry(fn) {
return curryN(fn.length, fn);
});
export default curry;
题外话
Ramda
在其柯里化实现中提供了2个feature
: 1. 可以使用ramda._
占位符; 2.可以使用flip
方法反转参数传递顺序
使用ES2015的 rest params 加上 fn.length
可以实现一个类似curry方法:
const curry = fn => (...args) =>
args.length >= fn.length
? fn(...args)
: (...innerArgs) => fn(...args, ...innerArgs);
上面的方法实现有一定缺陷,它只能满足2步柯里,没有实现任意层次的柯里化,接下来我们进行改造
const curry = fn => (...args) =>
args.length >= fn.length
? fn(...args)
- : (...innerArgs) => fn(...args, ...innerArgs);
+ : curry((...innerArgs) => fn(...args, ...innerArgs));
上面的方法依然有问题。因为使用Rest parameters的方法是不能由fn.length
获取arity
(元)[得到的值为0
]。所以我们最终改造柯里化实现为:
const curry = fn => {
const curryN = (n, fn) => (...args) =>
args.length >= n
? fn(...args)
: curryN(n-args.length, (...innerArgs) => fn(...args, ...innerArgs))
return curryN(fn.length, fn)
}
import _curry1 from './internal/_curry1';
import curryN from './curryN';
/**
* Returns a new function much like the supplied one, except that the first two
* arguments' order is reversed.
*
* @func
* @memberOf R
* @since v0.1.0
* @category Function
* @sig ((a, b, c, ...) -> z) -> (b -> a -> c -> ... -> z)
* @param {Function} fn The function to invoke with its first two parameters reversed.
* @return {*} The result of invoking `fn` with its first two parameters' order reversed.
* @example
*
* const mergeThree = (a, b, c) => [].concat(a, b, c);
*
* mergeThree(1, 2, 3); //=> [1, 2, 3]
*
* R.flip(mergeThree)(1, 2, 3); //=> [2, 1, 3]
* @symb R.flip(f)(a, b, c) = f(b, a, c)
*/
var flip = _curry1(function flip(fn) {
return curryN(fn.length, function(a, b) {
var args = Array.prototype.slice.call(arguments, 0);
args[0] = b;
args[1] = a;
return fn.apply(this, args);
});
});
export default flip;
Flip
接受一个函数作为参数,并返回一个函数,内部使用Array.prototype.slice
方法将arguments
转换为数组并置换数组前2个参数的位置,然后将该数组作为参数传递调用原函数
undefined
与不声明此属性有和区别🤔?
in
操作符处理逻辑不同。未声明的props返回为false
、声明为undefined
的props返回true
const test = {
prop: undefined
}
// 测试如下
'prop' in test
// -> true
'none' in test
// -> false
ps: 此例中prop
为已经声明属性赋值为undefined
(调用Object.defineProperty
),而none
为未声明属性访问未声明属性将返回undefined
你可以指定一个函数作为第二个参数。在这种情况下,当匹配执行后, 该函数就会执行。 函数的返回值作为替换字符串。 (注意: 上面提到的特殊替换参数在这里不能被使用。) 另外要注意的是, 如果第一个参数是正则表达式, 并且其为全局匹配模式, 那么这个方法将被多次调用, 每次匹配都会被调用。
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/String/replace#%E6%8C%87%E5%AE%9A%E4%B8%80%E4%B8%AA%E5%87%BD%E6%95%B0%E4%BD%9C%E4%B8%BA%E5%8F%82%E6%95%B0
let x =0
let y = 0
const str = '%s%s%s%s'
str.replace(/%s/g, x++)
// => 0000
x
// => 1
str.replace(/%s/g, ()=>y++)
// => 0123
y
// => 4
1.拷贝类型
JSON.parse(JSON.stringify(obj))
{ ...obj }
、 Object.assign({}, obj)
2.相互差异与各自issua
JSON.parse(JSON.stringify(obj))
writable
). 参见以下:// #1
const test1 = { fn: ()=>{}, num: 1 }
console.log(JSON.parse(JSON.stringify(test1)))
// #2
const test2 = { }
test2.circle = test2
JSON.parse(JSON.stringify(test1)) // Error
// #3
const test3 = {}
Object.defineProperties(test3, {
unWrite: {value: 1, enumerable: true},
unIteration: { value: 2, writable: true}
})
const copied = JSON.parse(JSON.stringify(test3))
test3.unWrite = 0;
copied.unWrite = 0;
console.log(test3, copied)
{ ...obj }
vs Object.assign({}, obj)
关于使用深拷贝补充: 1.https://news.ycombinator.com/item?id=16233330
\x
与\u
的区别?字符编码(character code)是特定Unicode字符的数字表示. 在JavaScript中可以使用
string.charCodeAt()
查看一个字符特定的Unicode编码(直到U+ffff: 0-65535)。
'\uxxxx'
为字符以十六进制的Unicode编码,长度为4个字符: '\u0000'
-> '\uffff'
'\xxx'
同'\uxxxx'
但长度为2个字符,即任何字符代码低于256的字符(即扩展ASCII范围内的任何字符)都可以使用前缀为\ x的十六进制编码字符代码进行转义。 \x
对于在低版本兼容性上优于 \u
(ie <= 8 支持 \x
但不支持 \u
)如果未传递值或未定义,则默认函数参数允许使用默认值初始化命名参数。
babel
之类的转译器可见,默认参数值将导致实际函数签名发生变化,形参个数变为从原始函数的第一个参数到第一个带有默认值的参数前一位参数。(这回导致某些异常,eg. 对函数进行柯里化)具体可见下图:package.json
固定版本和可变版本安装依赖有什么区别(不考虑.lock
文件)?yarn.lock (yarn)
与 package-lock.json (npm)
安装依赖处理上有什么差异 ?A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.