“Something’s a little bit off here.” That’s what I predict your first thought to be upon seeing my cubicle for the first time. There’s no screen or mouse in sight. Instead there’s a guy hammering away on a keyboard, staring at seemingly nothing.
It’s only me, and my colleagues can assure you that I’m mostly harmless. I’m a software developer working at Vincit offices in Tampere. I’m also blind. In this blog post I’m going to shed some light on the way I work.
Correct. I can perceive sunlight and some other really bright lights but that’s about it. In essence, nothing that would be useful for me at work.
准确来说,我觉察到阳光和其他明亮的光线,不过也仅限这些。其实,这对我的工作也并没有什么帮助。
What are you doing there, then?
你工作内容是什么?
The same as almost everyone else, that is: making software and bantering with my colleagues whenever the time permits. I have worked in full stack web projects with a focus on the backend. I have also taken up the role of a general accessibility consultant – or police; depends on how you look at it.
The computer I use is a perfectly normal laptop running Windows 10. It’s in the software where the “magic happens”. I use a program called a screen reader to access the computer. A screen reader intercepts what’s happening on the screen and presents that information via braille (through a separate braille display) or synthetic speech. And it’s not the kind of synthetic speech you hear in today’s smart assistants. I use a robotic-sounding voice which speaks at around 450 words per minute. For comparison, English is commonly spoken at around 120-150 words per minute. There’s one additional quirk in my setup: Since I need to read both Finnish and English regularly I’m reading English with a Finnish speech synthesizer. Back in the old days screen readers weren’t smart enough to switch between languages automatically, so this was what I got used to. Here’s a sample of this paragraph being read as I would read it:
我用的电脑是一台运行 Windows 10 的普通笔记本。是其中的软件让一切变得神奇。我使用一款叫做屏幕阅读器的程序来访问电脑。屏幕阅读器监听屏幕上的变化并通过盲文(需要单独的盲文设备)或合成的声音来展示给用户。这并不是你如今听到的各种智能助理的合成声音。我使用一种机械声音,每分钟能说 450 个单词。相比较而言,英语正常语速每分钟 120-150 个单词。我有一个怪癖:我既说英语也说芬兰语,我用芬兰语合成器读英语,因为老旧的屏幕阅读器在语言之间切换不够智能,所以我习惯这样做。下面是个例子是阅读这个段落,我能听懂。
A mouse is naturally not very useful to me so I work exclusively at the keyboard. The commands I use should be familiar to anyone reading this post: Arrow keys and the tab key move you around inside a window, alt+tab changes between windows etc. Screen readers also have a whole lot of shortcuts of their own, such as reading various parts of the active window or turning some of their features on or off.
It’s when reading web pages and other formatted documents that things get a little interesting. You see, a screen reader presents its information in chunks. That chunk is most often a line but it may also be a word, a character or any other arbitrary piece of text. For example, if I press the down arrow key on a web page I hear the next line of the page. This type of reading means that I can’t just scan the contents of my screen the same way a sighted person would do with their eyes. Instead, I have to read through everything chunk by chunk, or skip over those chunks I don’t care about.
Speech or braille alone can’t paint an accurate representation of how a window is laid out visually. All the information is presented to me in a linear fashion. If you copy a web page and paste it into notepad you get a rough idea of how web pages look to me. It’s just a bunch of lines stacked on top of another with most of the formatting stripped out. However, a screen reader can pick up on the semantics used in the HTML of the web page, so that links, headings, form fields etc. are announced to me correctly. That’s right: I don’t know that a check box is a check box if it’s only styled to look like one. However, more on that later; I’ll be devoting an entire post to this subject. Just remember that the example I just gave is a crime against humanity.
语音或盲文并不能描绘出窗口的显示布局。信息以线性方式呈现给我。如果你把网页复制粘贴进记事本,你就能明白我看到的网页是什么样子的。就是剥离大部分格式的多行文本。然而屏幕阅读器可以获取网页上的 HTML 语法,所以我也能知道超链接、标题、表单等等。事实上,如果非复选框元素展示成复选框样式,我并不能知道这是复选框。我之后将写一篇文章详细讲述这些内容,记住我刚刚举的是个“反人类”例子。
(译者注:突然感到自责和羞愧,深深明白了一个道理:不要用各种有含意义的传统标签 hack 布局和样式,也不要因为 css 的强大而懒得使用各种有含义的传统标签。共勉)
I spend a good deal of my time working at the command line. In fact I rarely use any other graphical applications than a web browser and an editor. I’ve found that it’s often much quicker to do the task at hand on the command line than to use an interface which was primarily designed with mouse users in mind.
So, given my love of the command line, why am I sticking with Windows, the operating system not known for its elegant command line tools? The answer is simple: Windows is the most accessible operating system there is. NVDA, my screen reader of choice is open source and maintained more actively than any other screen reader out there. If I had the choice I would use Mac OS since in my opinion it strikes a neat balance between usability and functionality. Unfortunately VoiceOver, the screen reader built in to Mac OS, suffers from long release cycles and general neglect, and its navigation models aren’t really compatible with my particular way of working. There’s also a screen reader for the Gnome desktop and, while excellently maintained for such a minor user base, there are still rough edges that make it unsuitable for my daily use. So, Windows it is. I’ve been compensating for Windows’ inherent deficiencies by living inside Git Bash which comes with an excellent set of GNU and other command line utilities out of the box.
既然我如此热爱命令行,为什么我却要选择 Windows 这个并不以命令行出名的操作系统呢?答案很简单:Windows 是最方便的操作系统。NVDA是我所选择的屏幕阅读器,它是开源的并且维护比其他阅读器更频繁。如果上天再我一次机会,我可能会选 Mac 系统,因为我认为它是易用性和功能性平衡的典范。不幸的是 Mac 系统上的屏幕阅读器 VoiceOver 经历了漫长的发布周期从而被遗忘,并且它的导航模型和我独特的工作方式并不协调。当然这里也有一个 Gnome 桌面上的屏幕阅读器,虽然用户很少,依然被很好地维护着,不过还有一些不完善的地方和我日常工作不协调。所以,我选择 Windows。由 GNU 诞生的 Git Bash 和其他命令行工具弥补了 Windows 内置命令行的缺陷。
How can you code?
你如何写代码?
It took me quite a long time to figure out why this question was such a big deal for so many people. Remember what I said earlier about reading text line by line? That’s how I read code. I do skip over the lines that aren’t useful to me, or maybe listen only halfway through them just for context, but whenever I actually need to know what’s going on I have to read everything as if I were reading a novel. Naturally I can’t just read through a huge codebase like that. In those cases I have to abstract some parts of the code in my mind: this component takes x as its input and returns y, never mind what it actually does.
我花费好长时间才明白为什么大家觉得这个问题是个很高深的问题。记得我上面说过一行一行地阅读文本吗?我也是通过这种方式读代码。通常我会跳过无用的行,或仅听半行来获取内容,但当我需要知道完整信息的时候,我不得不像读小说一样读完所有东西。我当然无法阅读整个代码库。这种情况下我会在脑中抽象一部分代码:这个组件输入 x 返回 y,并不用关心细节逻辑。
This type of reading makes me do some coding tasks a little bit differently than my sighted colleagues. For example, when doing a code review I prefer to look at the raw diff output whenever I can. Side-by-side diffs are not useful to me, in fact they are a distraction if anything. The + and – signs are also a far better indicator of modified lines than background colours, not because I couldn’t get the names of those colours, but because “plus” takes far less time to say than some convoluted shade of red that is used for highlighting an added line. (I am looking at you, Gerrit.)
You might think that indentation and other code formatting would be totally irrelevant to me since those are primarily visual concerns. This is not true: proper indentation helps me just as much as it does a sighted programmer. Whenever I’m reading code in braille (which, by the way, is a lot more efficient than with speech) it gives me a good visual clue of where I am, just like it does for a sighted programmer. I also get verbal announcements whenever I enter an indented or unindented block of text. This information helps me to paint a map of the code in my head. In fact Python was the first real programming language I picked up (Php doesn’t count) and its forced indentation never posed a problem for me. I’m a strong advocate of a clean and consistent coding style for a number of reasons, but mostly because not having one makes my life much more difficult
Spoiler alert: The answer to this question doesn’t start with either V or E. (Granted, I do use Vim for crafting git commit messages and other quick notes on the command line. I consider myself neutral on this particular minefield.) A year ago my answer would have been, of all things, Notepad++. It’s a lightweight, well-made text editor that gets the job done. However, a year ago I hadn’t worked in a large-scale Java project. When that eventually happened it was time to pick between Notepad++ and my sanity. I ended up clinging to the latter (as long as I can, anyway) and ditching Notepad++ in favour of IntelliJ IDEA. It has been my editor of choice ever since. I have a deeply-rooted aversion towards IDEs since most of them are either inaccessible or inefficient to work with solely on the keyboard. Chances are that I would have switched to using an IDE a lot sooner if I was sighted.
剧透一下:这个答案并不是以 V 或者 E 开头(我虽然通过命令行用 Vim 来写 git commit 信息和其他备注。我认为我在这场圣战中是中立的)(译者注:Vim 和 Emacs 梗)一年前我认为 Notepad++ 最棒,它是轻量级的做工精细的文本编辑器。然而一年前我还没有接触大规模 Java 项目,当我接触这种项目时,意味着我应该在 Notepad++ 和理智之间做个选择。最后我选择理智,抛弃 Notepad++ 转投 IntelliJ IDEA 的怀抱。从那之后 IntelliJ IDEA 便是我首选编辑器。我曾对各种 IDE 有深深怨念,它们大多数在纯键盘流操作下麻烦又低效。如果我视力没问题,我肯定早就跳到 IDE 阵营了。
But why Notepad++, you might ask. There are more advanced lightweight editors out there like Sublime text or Atom. The answer is simple: neither of them is accessible to screen readers. Text-mode editors like Vim aren’t an option either, since the screen reader I use has some problems in its support of console applications that prevent those editors from being used for anything longer than a commit message. Sadly, accessibility is the one thing that has the last word on the tools I use. If it’s not workable enough that I can use it efficiently, it’s out of the question.
You would think that frontend development was so inherently visual that it would be no place for a blind developer, and for the most part that is true. You won’t find me doing a basic Proof-of-Concept on my own, since those projects tend to be mostly about getting the looks right and adding the real functionality later.
However, I’ve had my fair share of Angular and React work too. How’s that? Many web apps of today have a lot going on under the hood in the browser. For example, I once worked a couple of weeks adding internationalization support to a somewhat complex Angular app. I didn’t need to do any visual changes at all.
I’ve found that libraries like Bootstrap are a godsend for people like me. Because of the grid system I can lay out a rough version of the user interface on my own. Despite this all the interface-related changes I’m doing are going through a pair of eyes before shipping to the customer. So, to sum up: I can do frontend development up to a point, at least while not touching the presentation layer too much.
There are certainly a lot of things I had to leave out of this blog post. As promised I’ll be devoting a post to the art of making web pages more accessible, since the lack of proper semantics is one of my pet peeves. However, there’s a good chance I won’t leave it at that. Stay tuned!
Bogosort. It even has a strange name! It’s literally classified as a Stupid sort.
Basically, you randomly put each element in a random place.
If it isn’t sorted, you randomly put each element in a random place.
If it isn’t sorted after that… you get the picture. Here’s an example:
4, 7, 9, 6, 5, 5, 2, 1 (unsorted)
2, 5, 4, 7, 5, 9, 6, 1 (randomly placed)
1, 4, 5, 6, 9, 7, 5, 2 (randomly placed again)
1, 2, 4, 5, 5, 6, 7, 9 (wow that was lucky)
Basically you keep randomly shuffling until you get a sorted array.
It’s easily one of the least efficient sorting algorithms, unless you are really, really lucky, and it has the horrific expected run time of O(n!), but tends to O(∞) the more elements you add.
If the list is not sorted, destroy the universe (this step is left as an activity for the reader).
Any surviving universes will then have the sorted version of the list.
Works in O(N) time!*
*note: this figure relies on the accuracy of the many worlds theory of quantum mechanics. if the many worlds theory of quantum mechanics is not accurate, your algorithm is unlikely to work in O(N) time.
I didn’t invent this. I saw it somewhere.
A student went to a copy shop to print some materials. He wanted to print 2 copies. Somehow, instead of printing out 2 copies, he printed every pages twice. Let me illustrate.
Pages he wanted : 1 2 3 4 … N; 1 2 3 4 … N
Pages he got: 1 1 2 2 3 3 4 4 …. N N
So when he was trying to sort the pages by picking up one and putting on the left pile and then picking another and putting on the right pile, the owner of the shop took over.
He first put the first page on left and then picked 2 pages and put on the right, then 2 left, 2 right ……
Sorting speed doubled……
It is a sorting algorithm that is of humorous nature and not useful. It’s based on the principle of multiply and surrender, a tongue-in-cheek joke of divide and conquer. It was published in 1986 by Andrei Broder and Jorge Stolfi in their paper Pessimal Algorithms and Simplexity Analysis. Here is the pseudocode:
这是一个非常幽默却没什么用的排序算法。它基于“合而不治”的原则(分治算法基本思想“分而治之”的反义词,文字游戏),它由 Andrei Broder 和 Jorge Stolfi 于 1986 年发表在论文《Pessimal Algorithms and Simplexity Analysis(最坏排序和简单性分析)》中,伪代码如下:
function slowSort(array,start,end){
if( start >= end ) return; //已经不能再慢了
middle = floor( (start+end)/2 );
//递归
slowSort(array,start,middle);
slowSort(array,middle+1,end);
//比较得出最大值放在队尾
if( array[end] < array[middle] )
swap array[end] and array[middle]
//去掉最大值之后再排序
slowsort(array,start,end-1);
}
Sort the first half recursively
Sort the second half recursively
Find the maximum of the whole array by comparing the middle with the end, placing maximum at the end of the list.
Recursively sort the entire list without the maximum.
递归排序好前一半
递归排序好后一半
比较中间和队尾的值,得到整个数组的最大值,将最大值放到队尾。
去掉最大值,递归整个数组
Stack Sort
Stack 排序
It searches for all the posts on StackOverflow with ‘sort a list’ in their title, extracts code snippets from them and then runs these code snippets until the list is sorted, which I think it verifies by actually traversing the list. It was posted on xkcd and an implementation can be found at stacksort .
It is as follows:
Create and compile a random program.
Run the random program on the input array.
If the program produces sorted output, you are done.
Else repeat from the beginning.
The alpha particles from the sun flip a few bits in the memory once a while, so this algorithm basically hopes that this flipping can cause the elements to be sorted. The way it works is:
Check if the array is sorted.
If the array is sorted, return the array.
If the array is not sorted, wait for 10 seconds and pray for having bit flips caused by solar radiation, just in the correct order then repeat from step 1.
It is a linear time algorithm which sorts a sequence of items requiring O(n) stack space in a stable manner. It requires a parallel processor. For simplicity, assume we are sorting a list of natural numbers. The sorting method is illustrated using uncooked rods of spaghetti.
Convert the data to positive real values scaled to the length of a piece of spaghetti.
Write each value onto a piece of spaghetti (a spaghetto) and break the spaghetti to the length equal to the scaled values.
Hold all resulting spaghetti in a bundle and slam the end on a flat surface.
Take the spaghetto which sticks out i.e. the longest one, read the value, convert back to the original value and write this down.
Repeat until all spaghetti are processed.
This works in O(n) time complexity.
Gather up the comrades and show them the list.
Ask the comrades to raise their hands if they believe the list is sorted.
Select any comrade who did not raise his hand, and executes him as a traitor.
Repeat steps 2 and 3 until everyone agrees that the list is sorted.
Whatever state your list is in, it’s already sorted meaning: The probability of the original input list being in the exact order it’s in is 1/(n!). There is such a small likelihood of this that it’s clearly absurd to say that this happened by chance, so it must have been consciously put in that order my an intelligent sorted. Therefore it’s safe to assume that it’s already optimally sorted in some way that transcends our naive mortal understanding of ‘ascending order’. Any attempt io change that orders to conform to our own preconceptions would actually make it less sorted.
It’s just a bubble sort, but perform every comparison by searching the internet. For example, “Which is greater – 0.211 or 0.75?”.
这是一种冒泡排序,但每次比较都依靠互联网的搜索。比如 “0.211 和 0.75 哪个大?”
Committee Sort
委员会排序
To sort a list comprised of N natural numbers, start by printing out N copies of the whole list on paper.
Now form N committees from any poor souls who happen to be milling around the office. The number of people on each committe should correspond to their particular value in the list.
Each committee is given their sheet of paper and told to, through a meeting or some other means, decide where in the sorted list their number should fall.
The list will be naturally sorted by the order that the committees return with their results
排序一个包含 N 个自然数的数组,首先用纸打印出 N 份整个数组。
然后在办公室周围选择几个恰好路过的倒霉委员。每个委员对应数组中的一个数字。
给每个委员一份打印的数组,并让他们通过开会或其他手段,来决定自己代表的数字应该在有序数组中的位置。
当这些委员有结论并答复你时,数组自然排好序了。
JavaScript has a feature called Scope. Though the concept of scope is not that easy to understand for many new developers, I will try my best to explain them to you in the simplest scope. Understanding scope will make your code stand out, reduce errors and help you make powerful design patterns with it.
Scope is the accessibility of variables, functions, and objects in some particular part of your code during runtime. In other words, scope determines the visibility of variables and other resources in areas of your code.
So, what’s the point in limiting the visibility of variables and not having everything available everywhere in your code? One advantage is that scope provides some level of security to your code. One common principle of computer security is that users should only have access to the stuff they need at a time.
Think of computer administrators. As they have a lot of control over the company’s systems, it might seem okay to grant full access user account to them. Suppose you have a company with three administrators, all of them having full access to the systems and everything is working smooth. But suddenly something bad happens and one of your system gets infected with a malicious virus. Now you don’t know whose mistake that was? You realize that you should them basic user accounts and only grant full access privileges when needed. This will help you track changes and keep an account of who did what. This is called The Principle of Least Access. Seems intuitive? This principle is also applied to programming language designs, where it is called scope in most programming languages including JavaScript which we are going to study next.
As you continue on in your programming journey, you will realize that scoping parts of your code helps improve efficiency, track bugs and reduce them. Scope also solves the naming problem when you have variables with the same name but in different scopes. Remember not to confuse scope with context. They are both different features.
In the JavaScript language there are two types of scopes:
在 JavaScript 中有两种作用域
Global Scope
Local Scope
全局作用域
局部作用域
Variables defined inside a function are in local scope while variables defined outside of a function are in the global scope. Each function when invoked creates a new scope.
When you start writing JavaScript in a document, you are already in the Global scope. There is only one Global scope throughout a JavaScript document. A variable is in the Global scope if it’s defined outside of a function.
// the scope is by default global
var name = 'Hammad';
Variables inside the Global scope can be accessed and altered in any other scope.
全局作用域里的变量能够在其他作用域中被访问和修改。
var name = 'Hammad';
console.log(name); // logs 'Hammad'
function logName() {
console.log(name); // 'name' is accessible here and everywhere else
}
logName(); // logs 'Hammad'
Local Scope
局部作用域
Variables defined inside a function are in the local scope. And they have a different scope for every call of that function. This means that variables having the same name can be used in different functions. This is because those variables are bound to their respective functions, each having different scopes, and are not accessible in other functions.
// Global Scope
function someFunction() {
// Local Scope ##1
function someOtherFunction() {
// Local Scope ##2
}
}
// Global Scope
function anotherFunction() {
// Local Scope ##3
}
// Global Scope
Block Statements
块级声明
Block statements like if and switch conditions or for and while loops, unlike functions, don’t create a new scope. Variables defined inside of a block statement will remain in the scope they were already in.
if (true) {
// this 'if' conditional block doesn't create a new scope
var name = 'Hammad'; // name is still in the global scope
}
console.log(name); // logs 'Hammad'
ECMAScript 6 introduced the let and const keywords. These keywords can be used in place of the var keyword.
ECMAScript 6 引入了let和const关键字。这些关键字可以代替var。
var name = 'Hammad';
let likes = 'Coding';
const skills = 'Javascript and PHP';
Contrary to the var keyword, the let and const keywords support the declaration of local scope inside block statements.
和var关键字不同,let和const关键字支持在块级声明中创建使用局部作用域。
if (true) {
// this 'if' conditional block doesn't create a scope
// name is in the global scope because of the 'var' keyword
var name = 'Hammad';
// likes is in the local scope because of the 'let' keyword
let likes = 'Coding';
// skills is in the local scope because of the 'const' keyword
const skills = 'JavaScript and PHP';
}
console.log(name); // logs 'Hammad'
console.log(likes); // Uncaught ReferenceError: likes is not defined
console.log(skills); // Uncaught ReferenceError: skills is not defined
Global scope lives as long as your application lives. Local Scope lives as long as your functions are called and executed.
一个应用中全局作用域的生存周期与该应用相同。局部作用域只在该函数调用执行期间存在。
Context
上下文(Context)
Many developers often confuse scope and context as if they equally refer to the same concepts. But this is not the case. Scope is what we discussed above and Context is used to refer to the value of this in some particular part of your code. Scope refers to the visibility of variables and context refers to the value of this in the same scope. We can also change the context using function methods, which we will discuss later. In the global scope context is always the Window object.
// logs: Window {speechSynthesis: SpeechSynthesis, caches: CacheStorage, localStorage: Storage…}
console.log(this);
function logFunction() {
console.log(this);
}
// logs: Window {speechSynthesis: SpeechSynthesis, caches: CacheStorage, localStorage: Storage…}
// because logFunction() is not a property of an object
logFunction();
If scope is in the method of an object, context will be the object the method is part of.
如果作用域定义在一个对象的方法中,上下文就是这个方法所在的那个对象。
class User {
logName() {
console.log(this);
}
}
(new User).logName(); // logs User {}
(new User).logName() is a short way of storing your object in a variable and then calling the logName function on it. Here, you don’t need to create a new variable.
(new User).logName()是创建对象关联到变量并调用logName方法的一种简便形式。通过这种方式你并不需要创建一个新的变量。
One thing you’ll notice is that the value of context behaves differently if you call your functions using the new keyword. The context will then be set to the instance of the called function. Consider one of the examples above with the function called with the new keyword.
function logFunction() {
console.log(this);
}
new logFunction(); // logs logFunction {}
When a function is called in Strict Mode, the context will default to undefined.
当在严格模式(strict mode)中调用函数时,上下文默认是 undefined。
Execution Context
执行环境
To remove all confusions and from what we studied above, the word context in Execution Context refers to scope and not context. This is a weird naming convention but because of the JavaScipt specification, we are tied to it.
JavaScript is a single-threaded language so it can only execute a single task at a time. The rest of the tasks are queued in the Execution Context. As I told you earlier that when the JavaScript interpreter starts to execute your code, the context (scope) is by default set to be global. This global context is appended to your execution context which is actually the first context that starts the execution context.
After that, each function call (invocation) would append its context to the execution context. The same thing happens when an another function is called inside that function or somewhere else.
Once the browser is done with the code in that context, that context will then be popped off from the execution context and the state of the current context in the execution context will be transferred to the parent context. The browser always executes the execution context that is at the top of the execution stack (which is actually the innermost level of scope in your code).
There can only be one global context but any number of function contexts.
The execution context has two phases of creation and code execution.
全局环境只能有一个,函数环境可以有任意多个。
执行环境有两个阶段:创建和执行。
CREATION PHASE
创建阶段
The first phase that is the creation phase is present when a function is called but its code is not yet executed. Three main things that happen in the creation phase are:
第一阶段是创建阶段,是函数刚被调用但代码并未执行的时候。创建阶段主要发生了 3 件事。
Creation of the Variable (Activation) Object,
Creation of the Scope Chain, and
Setting of the value of context (this)
创建变量对象
创建作用域链
设置上下文(this)的值
Variable Object
变量对象
The Variable Object, also known as the activation object, contains all of the variables, functions and other declarations that are defined in a particular branch of the execution context. When a function is called, the interpreter scans it for all resources including function arguments, variables, and other declarations. Everything, when packed into a single object, becomes the the Variable Object.
'variableObject': {
// contains function arguments, inner variable and function declarations
}
Scope Chain
作用域链
In the creation phase of the execution context, the scope chain is created after the variable object. The scope chain itself contains the variable object. The Scope Chain is used to resolve variables. When asked to resolve a variable, JavaScript always starts at the innermost level of the code nest and keeps jumping back to the parent scope until it finds the variable or any other resource it is looking for. The scope chain can simply be defined as an object containing the variable object of its own execution context and all the other execution contexts of it parents, an object having a bunch of other objects.
'scopeChain': {
// contains its own variable object and other variable objects of the parent execution contexts
}
The Execution Context Object
执行环境对象
The execution context can be represented as an abstract object like this:
执行环境可以用下面抽象对象表示:
executionContextObject = {
'scopeChain': {}, // contains its own variableObject and other variableObject of the parent execution contexts
'variableObject': {}, // contains function arguments, inner variable and function declarations
'this': valueOfThis
}
CODE EXECUTION PHASE
代码执行阶段
In the second phase of the execution context, that is the code execution phase, other values are assigned and the code is finally executed.
执行环境的第二个阶段就是代码执行阶段,进行其他赋值操作并且代码最终被执行。
Lexical Scope
词法作用域
Lexical Scope means that in a nested group of functions, the inner functions have access to the variables and other resources of their parent scope. This means that the child functions are lexically bound to the execution context of their parents. Lexical scope is sometimes also referred to as Static Scope.
function grandfather() {
var name = 'Hammad';
// likes is not accessible here
function parent() {
// name is accessible here
// likes is not accessible here
function child() {
// Innermost level of the scope chain
// name is also accessible here
var likes = 'Coding';
}
}
}
The thing you will notice about lexical scope is that it works forward, meaning name can be accessed by its children’s execution contexts. But it doesn’t work backward to its parents, meaning that the variable likes cannot be accessed by its parents. This also tells us that variables having the same name in different execution contexts gain precedence from top to bottom of the execution stack. A variable, having a name similar to another variable, in the innermost function (topmost context of the execution stack) will have higher precedence.
The concept of closures is closely related to Lexical Scope, which we studied above. A Closure is created when an inner function tries to access the scope chain of its outer function meaning the variables outside of the immediate lexical scope. Closures contain their own scope chain, the scope chain of their parents and the global scope.
A closure can not only access the variables defined in its outer function but also the arguments of the outer function.
闭包不仅能访问外部函数的变量,也能访问外部函数的参数。
A closure can also access the variables of its outer function even after the function has returned. This allows the returned function to maintain access to all the resources of the outer function.
When you return an inner function from a function, that returned function will not be called when you try to call the outer function. You must first save the invocation of the outer function in a separate variable and then call the variable as a function. Consider this example:
function greet() {
name = 'Hammad';
return function () {
console.log('Hi ' + name);
}
}
greet(); // nothing happens, no errors
// the returned function from greet() gets saved in greetLetter
greetLetter = greet();
// calling greetLetter calls the returned function from the greet() function
greetLetter(); // logs 'Hi Hammad'
The key thing to note here is that greetLetter function can access the name variable of the greet function even after it has been returned. One way to call the returned function from the greet function without variable assignment is by using parentheses () two times ()() like this:
function greet() {
name = 'Hammad';
return function () {
console.log('Hi ' + name);
}
}
greet()(); // logs 'Hi Hammad'
Public and Private Scope
共有作用域和私有作用域
In many other programming languages, you can set the visibility of properties and methods of classes using public, private and protected scopes. Consider this example using the PHP language:
// Public Scope
public $property;
public function method() {
// ...
}
// Private Sccpe
private $property;
private function method() {
// ...
}
// Protected Scope
protected $property;
protected function method() {
// ...
}
Encapsulating functions from the public (global) scope saves them from vulnerable attacks. But in JavaScript, there is no such thing as public or private scope. However, we can emulate this feature using closures. To keep everything separate from the global we must first encapsulate our functions within a function like this:
The parenthesis at the end of the function tells the interpreter to execute it as soon as it reads it without invocation. We can add functions and variables in it and they will not accessible outside. But what if we want to access them outside, meaning we want some of them to be public and some of them to be private? One type of closure, we can use, is called the Module Pattern which allows us to scope our functions using both public and private scopes in an object.
var Module = (function() {
function privateMethod() {
// do something
}
return {
publicMethod: function() {
// can call privateMethod();
}
};
})();
The return statement of the Module contains our public functions. The private functions are just those that are not returned. Not returning functions makes them inaccessible outside of the Module namespace. But our public functions can access our private functions which make them handy for helper functions, AJAX calls, and other things.
Module.publicMethod(); // works
Module.privateMethod(); // Uncaught ReferenceError: privateMethod is not defined
One convention is to begin private functions with an underscore, and returning an anonymous object containing our public functions. This makes them easy to manage in a long object. This is how it looks:
var Module = (function () {
function _privateMethod() {
// do something
}
function publicMethod() {
// do something
}
return {
publicMethod: publicMethod,
}
})();
IMMEDIATELY-INVOKED FUNCTION EXPRESSION (IIFE)
立即执行函数表达式(IIFE)
Another type of closure is the Immediately-Invoked Function Expression (IIFE). This is a self-invoked anonymous function called in the context of window, meaning that the value of this is set window. This exposes a single global interface to interact with. This is how it looks:
另一种形式的闭包是立即执行函数表达式(Immediately-Invoked Function Expression,IIFE)。这是一种在 window 上下文中自调用的匿名函数,也就是说this的值是window。它暴露了一个单一全局接口用来交互。如下所示:
(function(window) {
// do anything
})(this);
Changing Context with .call(), .apply() and .bind()
使用 .call(), .apply() 和 .bind() 改变上下文
Call and Apply functions are used to change the context while calling a function. This gives you incredible programming capabilities (and some ultimate powers to Rule The World). To use the call or apply function, you just need to call it on the function instead of invoking the function using a pair of parenthesis and pass the context as the first argument. The function’s own arguments can be passed after the context.
function hello() {
// do something...
}
hello(); // the way you usually call it
hello.call(context); // here you can pass the context(value of this) as the first argument
hello.apply(context); // here you can pass the context(value of this) as the first argument
The difference between .call() and .apply() is that in Call, you pass the rest of the arguments as a list separated by a comma while apply allows you to pass the arguments in an array.
function introduce(name, interest) {
console.log('Hi! I\'m '+ name +' and I like '+ interest +'.');
console.log('The value of this is '+ this +'.')
}
introduce('Hammad', 'Coding'); // the way you usually call it
introduce.call(window, 'Batman', 'to save Gotham'); // pass the arguments one by one after the contextt
introduce.apply('Hi', ['Bruce Wayne', 'businesses']); // pass the arguments in an array after the context
// Output:
// Hi! I'm Hammad and I like Coding.
// The value of this is [object Window].
// Hi! I'm Batman and I like to save Gotham.
// The value of this is [object Window].
// Hi! I'm Bruce Wayne and I like businesses.
// The value of this is Hi.
Call is slightly faster in performance than Apply.
Call 比 Apply 的效率高一点。
The following example takes a list of items in the document and logs them to the console one by one.
下面这个例子列举文档中所有项目,然后依次在控制台打印出来。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Things to learn</title>
</head>
<body>
<h1>Things to Learn to Rule the World</h1>
<ul>
<li>Learn PHP</li>
<li>Learn Laravel</li>
<li>Learn JavaScript</li>
<li>Learn VueJS</li>
<li>Learn CLI</li>
<li>Learn Git</li>
<li>Learn Astral Projection</li>
</ul>
<script>
// Saves a NodeList of all list items on the page in listItems
var listItems = document.querySelectorAll('ul li');
// Loops through each of the Node in the listItems NodeList and logs its content
for (var i = 0; i < listItems.length; i++) {
(function () {
console.log(this.innerHTML);
}).call(listItems[i]);
}
// Output logs:
// Learn PHP
// Learn Laravel
// Learn JavaScript
// Learn VueJS
// Learn CLI
// Learn Git
// Learn Astral Projection
</script>
</body>
</html>
The HTML only contains an unordered list of items. The JavaScript then selects all of them from the DOM. The list is looped over till the end of the items in the list. Inside the loop, we log the content of the list item to the console.
HTML文档中仅包含一个无序列表。JavaScript 从 DOM 中选取它们。列表项会被从头到尾循环一遍。在循环时,我们把列表项的内容输出到控制台。
This log statement is wrapped in a function wrapped in parentheses on which the call function is called. The corresponding list item is passed to the call function so that the the keyword in the console statement logs the innerHTML of the correct object.
Objects can have methods, likewise functions being objects can also have methods. In fact, a JavaScript function comes with four built-in methods which are:
对象可以有方法,同样函数对象也可以有方法。事实上,JavaScript 函数有 4 个内置方法:
Function.prototype.apply()
Function.prototype.bind() (Introduced in ECMAScript 5 (ES5))
Function.prototype.call()
Function.prototype.toString()
Function.prototype.toString() returns a string representation of the source code of the function.
Function.prototype.toString()返回函数代码的字符串表示。
Till now, we have discussed .call(), .apply(), and toString(). Unlike Call and Apply, Bind doesn’t itself call the function, it can only be used to bind the value of context and other arguments before calling the function. Using Bind in one of the examples from above:
(function introduce(name, interest) {
console.log('Hi! I\'m '+ name +' and I like '+ interest +'.');
console.log('The value of this is '+ this +'.')
}).bind(window, 'Hammad', 'Cosmology')();
// logs:
// Hi! I'm Hammad and I like Cosmology.
// The value of this is [object Window].
Bind is like the call function, it allows you pass the rest of the arguments one by one separated by a comma and not like apply, in which you pass the arguments in an array.
Bind 像call函数一样用逗号分隔其他传入参数,不像apply那样用数组传入参数。
Conclusion
结论
These concepts are radical to JavaScript and important to understand if you want to approach more advanced topics. I hope you got a better understanding of JavaScript Scope and things around it. If something just didn’t click, feel free to ask in the comments below.
Each year for April Fools’, rather than a prank, we like to create a project that explores the way that humans interact at large scales. This year we came up with Place, a collaborative canvas on which a single user could only place a single tile every five minutes. This limitation de-emphasized the importance of the individual and necessitated the collaboration of many users in order to achieve complex creations. Each tile placed was relayed to observers in real-time.
Multiple engineering teams (frontend, backend, mobile) worked on the project and most of it was built using existing technology at Reddit. This post details how we approached building Place from a technical perspective.
But first, if you want to check out the code for yourself, you can find it here. And if you’re interested in working on projects like Place in the future, we’re hiring!
Defining requirements for an April Fools’ project is extremely important because it will launch with zero ramp-up and be available immediately to all of Reddit’s users. If it doesn’t work perfectly out of the gate, it’s unlikely to attract enough users to make for an interesting experience.
The board must be 1000 tiles by 1000 tiles so it feels very large.
All clients must be kept in sync with the same view of the current board state, otherwise users with different versions of the board will have difficulty collaborating.
We should support at least 100,000 simultaneous users.
Users can place one tile every 5 minutes, so we must support an average update rate of 100,000 tiles per 5 minutes (333 updates/s).
The project must be designed in such a way that it’s unlikely to affect the rest of the site’s normal function even with very high traffic to r/Place.
The configuration must be flexible in case there are unexpected bottlenecks or failures. This means that board size and tile cooldown should be adjustable on the fly in case data sizes are too large or update rates are too high.
The API should be generally open and transparent so the reddit community can build on it (bots, extensions, data collection, external visualizations, etc) if they choose to do so.
API 必须开放和透明,reddit 社区如果对此有兴趣,可以在此之上构建项目(机器人、扩展、数据收集、外部可视化等等)。
Backend
后端
Implementation decisions
实施决策
The main challenge for the backend was keeping all the clients in sync with the state of the board. Our solution was to initialize the client state by having it listen for real-time tile placements immediately and then make a request for the full board. The full board in the response could be a few seconds stale as long as we also had real-time placements starting from before it was generated. When the client received the full board it replayed all the real-time placements it received while waiting. All subsequent tile placements could be drawn to the board immediately as they were received.
For this scheme to work we needed the request for the full state of the board to be as fast as possible. Our initial approach was to store the full board in a single row in Cassandra and each request for the full board would read that entire row. The format for each column in the row was:
Because the board contained 1 million tiles this meant that we had to read a row with 1 million columns. On our production cluster this read took up to 30 seconds, which was unacceptably slow and could have put excessive strain on Cassandra.
Our next approach was to store the full board in redis. We used a bitfield of 1 million 4 bit integers. Each 4 bit integer was able to encode a 4 bit color, and the x,y coordinates were determined by the offset (offset = x + 1000y) within the bitfield. We could read the entire board state by reading the entire bitfield. We were able to update individual tiles by updating the value of the bitfield at a specific offset (no need for locking or read/modify/write). We still needed to store the full details in Cassandra so that users could inspect individual tiles to see who placed them and when. We also planned on using Cassandra to restore the board in case of a redis failure. Reading the entire board from redis took less than 100ms, which was fast enough.
Illustration showing how colors were stored in redis, using a 2×2 board:
插图展示了我们如何用 redis 储存 2×2 画板的颜色:
We were concerned about exceeding maximum read bandwidth on redis. If many clients connected or refreshed at once they would simultaneously request the full state of the board, all triggering reads from redis. Because the board was a shared global state the obvious solution was to use caching. We decided to cache at the CDN (Fastly) layer because it was simple to implement and it meant the cache was as close to clients as possible which would help response speed. Requests for the full state of the board were cached by Fastly with an expiration of 1 second. We also added the stale-while-revalidate cache control header option to prevent more requests from falling through than we wanted when the cached board expired. Fastly maintains around 33 POPs which do independent caching, so we expected to get at most 33 requests per second for the full board.
We used our websocket service to publish updates to all the clients. We’ve had success using it in production for reddit live threads with over 100,000 simultaneous viewers, live PM notifications, and other features. The websocket service has also been a cornerstone of our past April Fools projects such as The Button and Robin. For r/Place, clients maintained a websocket connection to receive real-time tile placement updates.
我们使用我们的 websocket 服务 向所有客户端推送更新。我们已经成功地在 reddit live 生产环境中应用过它,来处理超过 100000 的并发用户,比如 live PM notifications 功能或其他特性。wesocket 服务也曾是我们过去愚人节项目的基础,比如 The Button 和 Robin 两个项目。对于 r/Place 项目,客户端维护一个 websocket 链接来接收实时的小块变化更新。
API
API
Retrieve the full board 检索整个画板
Requests first went to Fastly. If there was an unexpired copy of the board it would be returned immediately without hitting the reddit application servers. Otherwise, if there was a cache miss or the copy was too old, the reddit application would read the full board from redis and return that to Fastly to be cached and returned to the client.
Request rate and response time as measured by the reddit application:
reddit 应用测量的请求速率和响应时间:
Notice that the request rate never exceeds 33/s, meaning that the caching by Fastly was very effective at preventing most requests from hitting the reddit application.
When a request did hit the reddit application the read from redis was very fast.
当请求访问 reddit 应用时,redis 的读取操作非常迅速。
Draw a tile 绘制一个小块
The steps for drawing a tile were:
绘制一个小块的步骤如下:
Read the timestamp of the user’s last tile placement from Cassandra. If it was more recent than the cooldown period (5 minutes) reject the draw attempt and return an error to the user.
Write the tile details to redis and Cassandra.
Write the current timestamp as the user’s last tile placement in Cassandra.
Tell the websocket service to send a message to all connected clients with the new tile.
We actually had a race condition here that allowed users to place multiple tiles at once. There was no locking around the steps 1-3 so simultaneous tile draw attempts could all pass the check at step 1 and then draw multiple tiles at step 2. It seems that some users discovered this error or had bots that didn’t gracefully follow the ratelimits so there were about 15,000 tiles drawn that abused this error (~0.09% of all tiles placed).
Request rate and response time as measured by the reddit application:
reddit 应用测量的请求速率和响应时间:
We experienced a maximum tile placement rate of almost 200/s. This was below our calculated maximum rate of 333/s (average of 100,000 users placing a tile every 5 minutes).
Requests for individual tiles resulted in a read straight from Cassandra.
直接从 Cassandra 请求单个小块。
Request rate and response time as measured by the reddit application:
reddit 应用测量的请求速率和响应时间:
This endpoint was very popular. In addition to regular client requests, people wrote scrapers to retrieve the entire board one tile at a time. Since this endpoint wasn’t cached by the CDN, all requests ended up being served by the reddit application.
Response times for these requests were pretty fast and stable throughout the project.
在整个项目中,这些请求的响应时间非常迅速稳定。
Websockets
Websockets
We don’t have isolated metrics for r/Place’s effect on the websocket service, but we can estimate and subtract the baseline use from the values before the project started and after it ended.
The baseline before r/Place began was around 20,000 connections and it peaked at 100,000 connections, so we probably had around 80,000 users connected to r/Place at its peak.
Building the frontend for Place involved many of the challenges for cross-platform app development. We wanted Place to be a seamless experience on all of our major platforms including desktop web, mobile web, iOS and Android.
The UI in place needed to do three important things:
r/Place 的 UI 需要做三件很重要的事:
Display the state of the board in real time
Facilitate user interaction with the board
Work on all of our platforms, including our mobile apps
实时展示画板状态。
让用户和画板交互方便容易
在我们所有平台上正常运行,包括移动端 app。
The main focus of the UI was the canvas, and the Canvas API was a perfect fit for it. We used a single 1000 x 1000 <canvas> element, drawing each tile as a single pixel.
UI 的主要焦点集中在了 canvas,并且 Canvas API 完全能胜任要求。我们使用一个 1000 x 1000 的 <canvas> 元素,把每个小块当做一个像素进行绘制。
Drawing the canvas
绘制 canvas
The canvas needed to represent the state of the board in real time. We needed to draw the state of the entire board when the page loaded, and draw updates to the board state that came in over websockets. There are generally three ways to go about updating a canvas element using the CanvasRenderingContext2D interface:
The first option wouldn’t work for us since since we didn’t already have the board in image form, leaving options 2 and 3. Updating individual tiles using fillRect() was very straightforward: when a websocket update comes in, just draw a 1 x 1 rectangle at the (x, y) position. This worked OK in general, but wasn’t great for drawing the initial state of the board. The putImageData() method was a much better fit for this, since we were able to define the color of each pixel in a single ImageData object and draw the whole canvas at once.
Using putImageData() requires defining the board state as a Uint8ClampedArray, where each value is an 8-bit unsigned integer clamped to 0-255. Each value represents a single color channel (red, green, blue, and alpha), and each pixel requires 4 items in the array. A 2 x 2 canvas would require a 16-byte array, with the first 4 bytes representing the top left pixel on the canvas, and the last 4 bytes representing the bottom right pixel.
Illustration showing how canvas pixels relate to their Uint8ClampedArray representation:
插图展示了 canvas 像素和 Uint8ClampedArray 映射关系:
For place’s canvas, the array is 4 million bytes long, or 4MB.
对于 r/Place 的 canvas,数组大小是四百万字节,也就是 4MB。
On the backend, the board state is stored as a 4-bit bitfield. Each color is represented by a number between 0 and 15, allowing us to pack 2 pixels of color information into each byte. In order to use this on the client, we needed to do 3 things:
Pull the binary data down to the client from our API
“Unpack” the data
Map the 4-bit colors to useable 32-bit colors
将二进制数据从我们的 API 拉取到客户端。
“解压”数据
将 4 位颜色映射成可用的 32 位颜色。
To pull down the binary data, we used the Fetch API in browsers that support it. For those that don’t, we fell back to a normal XMLHttpRequest with responseType set to “arraybuffer”.
为了拉取二进制数据,我们在支持 Fetch API 的浏览器中使用此 API。在不支持的浏览器中,我们使用 XMLHttpRequest,并把 responseType 设置为 “arraybuffer”。
The binary data we receive from the API contains 2 pixels of color data in each byte. The smallest TypedArray constructors we have allow us to work with binary data in 1-byte units. This is inconvenient for use on the client so the first thing we do is to “unpack” that data so it’s easier to work with. This process is straightforward, we just iterate over the packed data and split out the high and low order bits, copying them into separate bytes of another array. Finally, the 4-bit color values needed to be mapped to useable 32-bit colors.
The ImageData structure needed to use the putImageData() method requires the end result to be readable as a Uint8ClampedArray with the color channel bytes in RGBA order. This meant we needed to do another round of “unpacking”, splitting each color into its component channel bytes and putting them into the correct index. Needing to do 4 writes per pixel was also inconvenient, but luckily there was another option.
TypedArray objects are essentially array views into ArrayBuffer instances, which actually represent the binary data. One neat thing about them is that multiple TypedArray instances can read and write to the same underlying ArrayBuffer instance. Instead of writing 4 values into an 8-bit array, we could write a single value into a 32-bit array! Using a Uint32Array to write, we were able to easily update a tile’s color by updating a single array index. The only change required was that we had to store our color palette in reverse-byte order (ABGR) so that the bytes automatically fell in the correct position when read using the Uint8ClampedArray.
Using the drawRect() method was working OK for drawing individual pixel updates as they came in, but it had one major drawbacks: large bursts of updates coming in at the same time could cripple browser performance. We knew that updates to the board state would be very frequent, so we needed to address this issue.
Instead of redrawing the canvas immediately each time a websocket update came in, we wanted to be able to batch multiple websocket updates that come in around the same time and draw them all at once. We made two changes to do this:
By moving the drawing into an animation loop, we were able to write websocket updates to the ArrayBuffer immediately and defer the actual drawing. All websocket updates in between frames (about 16ms) were batched into a single draw. Because we used requestAnimationFrame, this also meant that if draws took too long (longer than 16ms), only the refresh rate of the canvas would be affected (rather than crippling the entire browser).
Equally importantly, the canvas needed to facilitate user interaction. The core way that users can interact with the canvas is to place tiles on it. Precisely drawing individual pixels at 100% scale would be extremely painful and error prone, so we also needed to be able to zoom in (a lot!). We also needed to be able to pan around the canvas easily, since it was too large to fit on most screens (especially when zoomed in).
Users were only allowed to draw tiles once every 5 minutes, so misplaced tiles would be especially painful. We had to zoom in on the canvas enough that each tile would be a fairly large target for drawing. This was especially important for touch devices. We used a 40x scale for this, giving each tile a 40 x 40 target area. To apply the zoom, we wrapped the <canvas> element in a <div> that we applied a CSS transform: scale(40, 40) to. This worked great for placing tiles, but wasn’t ideal for viewing the board (especially on small screens), so we made this toggleable between two zoom levels: 40x for drawing, 4x for viewing.
用户只能每五分钟绘制一次小块,所以选错小块非常令人不爽。我们需要把 canvas 放大到每个小块都成为一个相当大的目标。这在触摸设备上尤其重要。我们使用 40x 的放大比例,给每个小块 40 x 40 的目标区域。为了应用缩放,我们把<canvas>元素包裹进一个<div>,并给 div 设置 CSS 属性transform: scale(40, 40)。这样一来,小块的布置变得非常方便,但整个画板的显示并不理想(尤其是在小屏幕上),所以我们混合使用两种缩放级别:40x 用于绘制,4x 用于显示。
Using CSS to scale up the canvas made it easy to keep the code that handled drawing the board separate from the code that handled scaling, but unfortunately this approach had some issues. When scaling up an image (or canvas), browsers default to algorithms that apply “smoothing” to the image. This works OK in some cases, but it completely ruins pixel art by turning it into a blurry mess. The good news it that there’s another CSS, image-rendering, which allows us to ask browsers to not do that. The bad news is that not all browsers fully support that property.
We needed another way to scale up the canvas for these browsers. I mentioned earlier on that there are generally three ways to go about drawing to a canvas. The first method, drawImage(), supports drawing an existing image or another canvas into a canvas. It also supports scaling that image up or down when drawing it, and though upscaling has the same blurring issue by default that upscaling in CSS has, this can be disabled in a more cross-browser compatible way by turning off the CanvasRenderingContext2D.imageSmoothingEnabled flag.
So the fix for our blurry canvas problem was to add another step to the rendering process. We introduced another <canvas> element, this one sized and positioned to fit across the container element (i.e. the viewable area of the board). After redrawing the canvas, we use drawImage() to draw the visible portion of it into this new display canvas at the proper scale. Since this extra step adds a little overhead to the rendering process, we only did this for browsers that don’t support the CSS image-rendering property.
The canvas is a fairly big image, especially when zoomed in, so we needed to provide ways of navigating it. To adjust the position of the canvas on the screen, we took a similar approach to what we did with scaling: we wrapped the <canvas> element in another <div> that we applied CSS transform: translate(x, y) to. Using a separate div made it easy to control the order that these transforms were applied to the canvas, which was important for preventing the camera from moving when toggling the zoom level.
We ended up supporting a variety of ways to adjust the camera position, including:
我们最后支持多种方式调整视角位置,包括:
Click and drag
Click to move
Keyboard navigation
点击拖拽
点击移动
键盘导航
Each of these methods required a slightly different approach.
每种操作需要一点不同的实现方式。
Click-and-drag 点击拖拽
The primary way of navigating was click-and-drag (or touch-and-drag). We stored the x, y position of the mousedown event. On each mousemove event, we found the offset of the mouse position relative to that start position, then added that offset to the existing saved canvas offset. The camera position was updated immediately so that this form of navigation felt really responsive.
We also allowed clicking on a tile to center that tile on the screen. To accomplish this, we had to keep track of the distance moved between the mousedown and mouseup events, in order to distinguish “clicks” from “drags”. If the mouse did not move enough to be considered a “drag”, we adjusted the camera position by the difference between the mouse position and the point at the center of the screen. Unlike click-and-drag movement, the camera position was updated with an easing function applied. Instead of setting the new position immediately, we saved it as a “target” position. Inside the animation loop (the same one used to redraw the canvas), we moved the current camera position closer to the target using an easing function. This prevented the camera move from feeling too jarring.
We also supported navigating with the keyboard, using either the WASD keys or the arrow keys. The four direction keys controlled an internal movement vector. This vector defaulted to (0, 0) when no movement keys were down, and each of the direction keys added or subtracted 1 from either the x or y component of the vector when pressed. For example, pressing the “right” and “up” keys would set the movement vector to (1, -1). This movement vector was then used inside the animation loop to move the camera.
我们也支持键盘导航,既可以使用 WASD 键也可以使用方向键。四个键控制内置 移动向量。没有按键按下时,向量默认是 (0, 0),每个按键按下时会增加或减少向量的 x 或 y 轴 1 个单位。举个例子,按下“右”和“上”键会把移动向量设置成 (1,-1)。这个移动向量随后应用在动画循环中,来移动视角。
During the animation loop, a movement speed was calculated based on the current zoom level using the formula:
This made keyboard navigation faster when zoomed out, which felt a lot more natural.
在缩小状态下,键盘导航移动速度更快,这样显得更自然。
The movement vector is then normalized and multiplied by the movement speed, then applied to the current camera position. We normalized the vector to make sure diagonal movement was the same speed as orthogonal movement, which also helped it feel more natural. Finally, we applied the same kind of easing function to changes to the movement vector itself. This smoothed out changes in movement direction and speed, making the camera feel much more fluid and juicy.
There were a couple of additional challenges to embedding the canvas in the mobile apps for iOS and Android. First, we needed to authenticate the user so they could place tiles. Unlike on the web, where authentication is session based, with the mobile apps we use OAuth. This means that the app needs to provide the webview with an access token for the currently logged in user. The safest way to do this was to inject the oauth authorization headers by making a javascript call from the app to the webview (this would’ve also allowed us to set other headers if needed). It was then a matter of passing the authorization headers along with each api call.
For the iOS side we additionally implemented notification support when your next tile was ready to be placed on the canvas. Since tile placement occurred completely in the webview we needed to implement a callback to the native app. Fortunately with iOS 8 and higher this is possible with a simple javascript call:
The delegate method in the app then schedules a notification based on the cooldown timer that was passed in.
应用中的委派方法根据传入的冷却计时器,会随后调度发送一条通知。
What We Learned
我们学到了什么
You’ll always miss something
你总会疏漏一些事
Since we had planned everything out perfectly, we knew when we launched, nothing could possibly go wrong. We had load tested the frontend, load tested the backend, there was simply no way we humans could have made any other mistakes.
The launch went smoothly. Over the course of the morning, as the popularity of r/Place went up, so did the number of connections and traffic to our websockets instances:
No big deal, and exactly what we expected. Strangely enough, we thought we were network-bound on those instances and figured we had a lot more headway. Looking at the CPU of the instances, however, painted a different picture:
并没有什么惊喜,所有和我们预期的一样。奇怪的是,我们怀疑限制了这些服务器实例的网络带宽,因为我们预计会有更大的流量。查看了一下 CPU 的实例情况,却显示出一幅不同的图片:
Those are 8-core instances, so it was clear they were reaching their limits. Why were these boxes suddenly behaving so differently? We chalked it up to place being a much different workload type than they’d seen before. After all, these were lots of very tiny messages; we typically send out larger messages like live thread updates and notifications. We also usually don’t have that many people all receiving the same message, so a lot of things were different.
Still, no big deal, we figured we’d just scale it and call it a day. The on-call person doubled the number of instances and went to a doctor’s appointment, not a care in the world.
That graph may seem unassuming if it weren’t for the fact that it was for our production Rabbit MQ instance, which handles not only our websockets messages but basically everything that reddit.com relies on. And it wasn’t happy; it wasn’t happy at all.
After a lot of investigating, hand-wringing, and instance upgrading, we narrowed down the problem to the management interface. It had always seemed kind of slow, and we realized that the rabbit diamond collector we use for getting our stats was querying it regularly. We believe that the additional exchanges created when launching new websockets instances, combined with the throughput of messages we were receiving on those exchanges, caused rabbit to buckle while trying to do bookkeeping to do queries for the admin interface. So we turned it off, and things got better.
If you’re wondering why we kept adjusting the timeouts on placing pixels, there you have it. We were trying to relieve pressure to keep the whole project running. This is also the reason why, during one period, some pixels were taking a long time to show up.
The reasons for the adjustments were entirely technical. Although it was cool to watch r/Place/new after making the change:
尽管调整完看 r/Place/new 版块很有意思,但调整完全出于技术原因:
So maybe that was part of the motivation.
或许这也是调整的部分动机。
Bots Will Be Bots
机器人终归是机器人
We ran into one more slight hiccup at the end of the project. In general, one of our recurring problems is clients with bad retry behavior. A lot of clients, when faced with an error, will simply retry. And retry. And retry. This means whenever there is a hiccup on the site, it can often turn into a retry storm from some clients who have not been programmed to back-off in the case of trouble.
When we turned off place, the endpoints that a lot of bots were hitting started returning non-200s. Code like this wasn’t very nice. Thankfully, this was easy to block at the Fastly layer.
This project could not have come together so successfully without a tremendous amount of teamwork. We’d like to thank u/gooeyblob, u/egonkasper, u/eggplanticarus, u/spladug, u/thephilthe, u/d3fect and everyone else who contributed to the r/Place team, for making this April Fools’ experiment possible.
SETNX key value
将 key 的值设为 value ,当且仅当 key 不存在。
若给定的 key 已经存在,则 SETNX 不做任何动作。
SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。
设置成功,返回 1 。设置失败,返回 0 。
很简单,返回 1 取到锁;返回 0 没有取到锁。
异常状况
上面其实已经解决了分布式锁的最基本的问题,包括对锁的操作。下面,我们就要考虑一些特殊情况了,比如某个线程挂掉或网络问题或其他原因导致没有释放锁,这样一来所有线程就陷入了死锁,为了解决这个问题,我们可以为锁设置超时时间,value 值设置为 当前时间戳+过期时长,当其他线程无法获取锁时,可以查看 value 值是否已过期,如果过期,则可以删除掉 value ,执行上面的争用锁的操作。
我很喜欢透明这个词,在软件领域的意思就是关注应该关注的东西,不应该关注的东西应该是透明的、看不到的。
原业务是后端提供一个 HTML,前端各渠道各自编写相同逻辑处理 HTML 里面的信息,进而展示给用户。这样的设计真的是非常痛苦,首先各端工作量增加,其次耦合性太高,最重要的是需要使用 iframe 或引入 jquery 来执行逻辑。重构之后由后端处理完 HTML 的信息,传给各端一个纯展示用的 HTML,对于各端来说这个 HTML 里的内容是透明的,他们不必关注,只需要展示就可以了。
另一个是 HTML 里面有很多需要填写的关键信息夹杂在内容之中,在不同的 HTML 中有可能有相同的关键信息,原业务对相同的关键信息用相同 ID 的 input 标签作为区分,后端再根据 ID 来存取所填的值。这样导致每创建一个新的 HTML 都要开发人员编写并保存一个完整页面,并且确保其中关键信息的填空的 ID 和约定一致。重构之后 HTML 的文本内容和关键信息分离,文本内容相当于纯文案,关键信息则是 key-value 列表,由后端装填到一个 HTML 模板中即可,这样一来,这些业务无关的内容对于后端和前端来说都是透明的,文本内容和关键信息的修改不需要投入开发人员。
灰度
重构一个正在运行的服务,新旧逻辑和数据不互通,不能保证同一时间迁移数据的,而且变更内容过多,影响范围较大,一定要有一个灰度方案,保证出现问题能够把影响面降到最小,新的服务能够逐步稳定地替代旧服务。
首先是灰度范围的选择,从业务的哪一层面来划分灰度范围,选取的颗粒度不能太大也不能太小,根据业务来寻找一个层面进行划分。
然后要考虑如何方便地调整灰度范围直至全量,以及如何保证后续新的业务主体自动进入灰度范围,我在数据库的字典表用一条数据记录灰度范围,也就是一些类型 ID 用逗号拼接成的字符串,代码逻辑会根据业务主体判断其类别是否在灰度范围,当灰度范围调整时,更新这条数据,当这条数据为空字符串时,代码判断为全量切换,即所有业务主体走新的业务逻辑,保证后续新增的类型自动进入灰度范围。
最后是灰度策略的制定,灰度范围外的业务如何处理,在灰度范围内但没有新业务基础数据的怎么处理,其他上下游业务如何保证不受灰度策略的影响。我在这次重构中,灰度范围外的业务主体仍然按照旧的业务逻辑处理,加到了灰度范围中但没有新业务基础数据的,仍然按照旧业务逻辑处理。新的业务保证上下游数据的正常交互,就像一条河流在中间分成了两条河道,对应新旧协议,然后又汇聚回原河流。
var a = new Array();
a[2]=2;//指定第三位,数组的长度就是3
alert(a.length);//3
var a = new Array();
a["two"] = 2;
alert(a.length);//0
var b = new Object();
a[b] = 2;
alert(a.length);//0