0%

React 框架的终极目的是:高效的用 js 生成HTML

为了实现这个目的,react 维护了一个 virtual DOM ,然后再根据 virtual DOM 生成真正的 DOM,来达到「高效」的目的。说到高效,其实就是回答这两个问题:

  1. When do I re-render? Answer: When I observe that the data is dirty.
  2. How do I re-render efficiently? Answer: Using a virtual DOM to generate a real DOM patch.

客户端常见的判断 re-render 时机的方法有下面两种:

  1. diff。这种方法是:每个渲染周期(俗称每一帧),都会进行 render,然后 render 出来的结构,跟上一次 render 的结果进行 diff;找出不同的部分,再对 DOM 中对应的部分进行更新。
  2. 数据变动检测。检测会导致界面变动的数据源,当数据有变化时,进行 re-render。

两种方式都有自己的优势和劣势,更多的时候,视图框架会把两者结合起来用。
react 主要用的第二种方法,因为它效率更高。因为本质上讲,方法一是 diff virtual DOM,方法二是 diff props and state。一般情况下,props & state 的数据量会小一些。当然,你也可以写一个 Component,props 结构非常复杂,但是只用了其中一个字段用来渲染,渲染的节点数也很少。这种设计属于不好的设计,应该尽量避免。好的 Component 应该是 props 尽量扁平和简洁,去掉根渲染无关的字段。 这样才能充分发挥 react 的渲染性能。

更多关于 virtual DOM

React 为了实现如上的构想,为我们提供了很多概念和代码,下面逐一讲解:

jsx 标记语言

React 使用 jsx 标记语言。它跟 html 语言非常相似,使用起来非常方便。如果没有它,我们只能通过 Rect.createElement( 'canvas', {…props} ) 的方式来生成一个 cavans 的描述,然后,react 会把这个描述「渲染」成真正的 Html <canvas ...props />。甚至,如果没有 react 框架,我们只能用 let element = document.createElement('canvas', [, options]) 来创建一个 Html 元素。也许,你会觉得这三种方式代码量差不多。但是,当有多层嵌套的视图结构时,jsx 标记语言的优势就显现出来了。你可以试着把以下 jsx 语言「翻译」为后面两种写法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
render() {
const taskCount = this.props.taskCount;

return (<div className="step-content create-data-content">
<div className="info-bar" >
<div>
<div className="ui-font-secondary4">{'总共统计'}</div>
<div className="ui-font-h2">{`${taskCount}条任务`}</div>
</div>
<div>
{this.renderTabBar()}
</div>
</div>
{this.renderContent()}
</div>);
}

组件抽象 Component

React 提供了一个类 Component 作为所有组件的基类。它的意义是:

  1. 它为我们创造了一个「组件」的概念,代表可重用的可视元素的最小单位。我们按照这个概念去开发,能很容易写出高复用性的视图代码。
  2. 它提供了基本的生命周期回调函数,满足我们多样的需求。比如我们常用的: componentDidMount, componentWillReceiveProps, componentWillUnmount
  3. 提供了 props 接口,为父容器和子容器之间信息交流提供标准通道。
  4. 提供 state,作为容器内部状态管理的落脚地。

可视组组件的描述

每个组件都有一个 render 方法。

高效的翻译器:描述 =>

写在前面

这篇博客的前身是 《React 新手必须知道的10件事》,结果写着写着,「每件事」都远远超过了预计的300~500字的限制。给读者的阅读造成了极大的困扰。故将《10件事》拆开成若干篇,每一篇只讲一个主题。

正文

React 最推荐的数据交互方式是:props & onChnage。在这种交互方式里:对于一个可视组件 ComponentA,用 props 来向它发送信息,而用 onChange 回调函数来接收 ComponentA 发送的·信息。在程序世界里,我们更喜欢把上述「交互方式」称为「接口」,虽然这个「接口」不是我们在面向对象语言里的 interface,但是跟 interface 有着类似的功能。 我们暂且把这个「接口规范」取名为 「props & onChange 接口规范」。

React 还是给了另外一种方法来进行数据交互:ref & method。在这种交互方式里,我们通过 <ComponentA ref={ r => this.refOfComponentA = r } 的方式来获得 ComponentA 对象的引用,然后用 this.refOfComponentA.someMethod() 来向它发送信息。我们把这交互方式称为 「ref & method 接口规范」。在典型的客户端开发环境里(iOS、Android、Windows PC等),这种方式更为常见,并且对函数调用更加友好,更「像」程序语言。但是,对于 React 新手,我们强烈不建议使用这种借口规范,除非你对 React 整个机制非常了解,仍然想用它。因为它严重破坏了 React 组件的一致性。原因有:

  1. React 的可视组件的层级结一般是在 jsx 文件中以一种类似于 html 的语言来表示的,这种表示方式既方便又直观,表达力很强。在这种特殊的 jsx 语言里,「props & onChange接口规范」很容易且自然的被遵守。而如果用 「ref & method接口规范」,你不得不跳转到很多行以外,才能明白信息的传递过程,既不利于代码编写,也不利于阅读。
  2. 我们避免不了用 props 方式来进行数据传递。我们说「避免不了」,因为很多原因,在此仅列举两个:一、jsx 文件中,Html 内置元素只能通过 props 来传递参数;二、很多第三方库(如果我们在开发一个大型项目,必定有很多「轮子」不用自己造),也必须通过 props 来传递参数。所以,props 不可避免;而同时存在两种接口规范,是没有意义且容易出错的。
  3. 第三个原因可能比较「经验化」。如果现在不能理解和认同,你听听就好;反正,当你使用过的优秀开源框架足够多,你肯定会明白的:当你新接触一个框架时,暂时抛弃自己以往的习惯,转而遵守它的语言规范,是最好的选择。原因很简单:
    1. 一个框架从出生到出名,一定有自己与众不同的框架思想,才能从其他同类型框架中脱引而出。时间的验证,是有意义的。
    2. 过于轻率的使用其他的编程思想,会多处碰壁;也不利于你真正了解此框架的优势和瓶颈。

  1. Props & onChange 的原罪 。「props & onChange 接口规范」它不是一个典型的「程序接口规范」。
    1. 当你拿到一个可视组件的 ref,却没有类似 setProps() 这样的方法来改变其 props,你只能在 render() 方法中,通过 jsx 语言来设置其 props。这意味着父元素必须保存并维护这个 props 对应的值,而更多时候,父容器只是想一次性的设置一个值,然后就走,让以后的事情交给子元素自己去维护。当然,你也可以通过 ref 来获取子元素的应用,然后调用其成员方法来达到一次性改变属性的目的,但又会带来其他问题,请看下一条。
    2. 在大多数客户端框架里,有一个视图类 ComponentAcompA = new ComponentA()compoA 即是对这个可视组件的表示。而,在 React 世界里,compA.render() 返回的结果(结果的结构参见下图),才是这个可视组件的表示。这种不同带来的后果是,我们很难对已经渲染过的节点进行后续更改。当然 React 提供了 React.cloneElement(element,props) 接口来改变,但是这个接口怎么看都不像一种正派的接口,而像是。。。hack。因为这种改变,其他人时候很难看懂的。换句话说,我们不是通过一个明确定义的接口,而是“随性的”,“肆无忌惮”的修改了内存。函数返回结果
    3. React 用 props & PropType 的方式重新定义了一种接口规范,定义了一个组件的输入和输出。输入 props 一般是 string, boolean, number, object;输出参数一般是 functionPropType 定义了每个 props 的取值范围、 是否必填等信息。这整个一套机制是跟面向对象语言里的 interface 的功能是高度重合的,也就是说,React 重新发明了轮子。并且这个轮子有自己很大的局限性:他不是能自表达的。什么意思呢?在典型的面向对象语言里(以 java 为例), 假设有一下代码:
      1
      2
      3
      4
      5
      6
      inteface IRunable{
      run();
      getSpeed();
      }

      function clone(IRunable){}
      这段代码里的 clone 函数的第一个参数必须满足 IRunable 接口,否则编译器会抛出编译错误。但是,切换到 props 接口规范,假设我们定义了一个对象的接口:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      const SomePropType = PropType.shape({
      name: PropType.string.isRequired,
      uuid: PropType.stirng.isRequired,
      onChange: PropType.func.isRequired,
      });

      function clone(element){}. // element 是一个必须满足 SomePropType 接口的元素。

      const AnotherPropType = PropType.shape({
      elment: ???how??? // 一个必须满足 SomePropType 接口的元素。
      })
      你没办法定义一个 clone 方法,限制它的第一个参数必须是满足某个 PropType 定义的组件对象;甚至,你也没办法定义一个接口 AnotherPropType,使得它的某个属性满足 SomePropType
  2. Controller 和 View 融为一谈。React 本身是一个 View 层的工具,他最擅长的是把数据 render() 成 dom 元素。在 Android SDK 里,有一个东西跟它的功能很相似: layout xml。但是在 React 的设计理念里,没有对 view 和 controller 的明确的概念的区分。我们可以在 Component 里做 view 层该做的事情(根据给定数据显示界面),也可以做 controller 应该做的事情(处理交互动作、处理网络请求)。以至于有很多博客来“教” React 的开发者来明确分这两个概念,参考博客Presentational and Container Components。当然你可以说,这是开发者自身素质和抽象能力的体现,但是作为一个使用量如此巨大的框架,官方居然没有为用户构造和区分这个概念,还要用户自己去发现和总结,不得不说,是一大黑点。

  1. 尽量用 props & onChange,不要用 ref 获取引用然后调用方法。详情参考博客:一等公民 props & onChange
  2. React 只是一个视图框架,请尽量在 Component 里只做他擅长的事情。尽量写无状态的 Component。视图以外的事情,比如控制层、数据层、网络层,需要借助其他框架来完成。详情参考博客:React 仅仅是一个 view 框架
  3. 避免重复造轮子。react 有丰富的第三方 Component & Utils & everything。写任何组件前请先看看这里:awesome-react-components
  4. Less state,more PureComponent. 深刻理解和区分 Presentational Component 和 Container Component。前者决定组件如何显示,更关心对已知数据的展示,大量操作 dom,很少有 state;后者更关心数据的获取和更新,关心交互操作,很少直接操作 dom,可能包括很多 state。详解请参考:Presentational and Container Components
  5. 理解 React 的单向数据流,了解他的优势和局限。详情:React 组件数据流 && 组件间沟通
  6. 如果是中大型项目,请添加静态类型检查。TypeScript 或者其他类似的解决方案。因为 js 太自由了,很容易对一个对象增加和删除一个字段。如果,恰好其他人需要看这段代码,可能需要追溯好几条街,阅读7、8个代码文件,才知道某个对象的一个对象是从哪里来,结构如何。在多人配合的项目中,这种「自由」带来的便利,远远抵不上代码可读性降低带来的阻碍。

不算春节,你有多久没有回过家?3~5年?之所以不把春节算上,是因为春节期间的家乡,笼罩着团圆的气氛,充斥着归乡的人群;而这些,在大多数时间都不属于家乡。
而你,有多久没有看过,常态下的家乡?

岳父大人不远千里来机场接机,对老婆来说,从小到大,这也是第一次。我想,大概是因为,我们越长大,回家的时间就越“宝贵”,“宝贵”到父母驱车200公里也要多争取两个小时时间在一起。

回程路上,岳父大人执意要开车。见他路上渐现疲态,我换到驾驶座,车速刚加速到100码,他已睡着。

接近凌晨到家,岳父大人乐此不彼的带我们展示他为我们精心修葺的千尺豪宅。他为两个宝贝女儿每人装修了一间卧室。说是卧室,比深圳的“精装50平单身公寓”还要大。卧室外面,60平米的阳台,像大海一样宽阔,我傻了眼。我走到阳台边,周围安静得只剩下蛐蛐声,一辆车的声音都没有。我仰望天空,星空清澈无比,心里顿时五味杂陈。见到在土地上如此铺张挥霍的家,再想到深圳那个精打细算的家,不禁怀疑起自己:是哪里不对?一路参观,所到之处,各种细节,处处透露着二老的良苦用心。即便款式有些老套,但一点也不妨碍我感动得一塌糊涂。想到这样宽敞精致、良苦用心的房子,使用率不到1%,又为二老感到不值。

回到自己家,好多年没吃过樱桃,就带上爸妈一起上山摘樱桃。

摘要:本文是一个流水文,主要记录我在 LeanCloud 云平台上使用 node.js 搭建web服务器的过程。其中遇到的困难和需要记忆的知识点,在此做一个汇总。

安装node.js

一定要是最新版本,否则各种 warn 和 err。
安装的时候 warn 一定要处理,否则会导致意想不到的 err。

安装 avoscloud 工具

一定要使用最新版本,因为 leanengine 2.0 和 3.0 版本的项目结构发生了变化,老版本的命令行工具不能正确找到对应的文件,无法启动。

node.js 学习

node.js 是一个异步事件驱动的 web 框架。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const http = require('http');

const hostname = '127.0.0.1';
const port = 1337;

var server = http.createServer();
server.on('request', (req, res) => {
var method = request.method;
var url = request.url;

req.on('data', function(chunk) {
body.push(chunk);
}).on('end', function() {
body = Buffer.concat(body).toString();
// at this point, `body` has the entire request body stored in it as a string
}).on('error', function(err) {
// This prints the error message and stack trace to `stderr`.
console.error(err.stack);
});

res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello World\n');
});
server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});

express 学习

Express的文件结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.
├── app.js
├── bin
│ └── www
├── package.json
├── public
│ ├── images
│ ├── javascripts
│ └── stylesheets
│ └── style.css
├── routes
│ ├── index.js
│ └── users.js
└── views
├── error.jade
├── index.jade
└── layout.jade

routing

Route definition takes the following structure:

1
app.METHOD(PATH, HANDLER)

Where:

  • app is an instance of express.
  • METHOD is an HTTP request method.
  • PATH is a path on the server.
  • HANDLER is the function executed when the route is matched.

PATH 还支持正则表达式:

This route path will match abcd, abxcd, abRANDOMcd, ab123cd, and so on.

1
2
3
app.get('/ab*cd', function(req, res) {
res.send('ab*cd');
});

This route path will match /abe and /abcde.

1
2
3
app.get('/ab(cd)?e', function(req, res) {
res.send('ab(cd)?e');
});

express.Router

利用 express.Router 中间件可以创建一个子应用来响应特定的请求。
以下的代码,表示把所有 /birds 路径下的请求都托付给 birds.js 文件处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// birds.js

var express = require('express');
var router = express.Router();

// middleware that is specific to this router
router.use(function timeLog(req, res, next) {
console.log('Time: ', Date.now());
next();
});
// define the home page route
router.get('/', function(req, res) {
res.send('Birds home page');
});
// define the about route
router.get('/about', function(req, res) {
res.send('About birds');
});

module.exports = router;

app 中:

1
2
3
//app.js
var birds = require('./birds');
app.use('/birds', birds);

middleware

http://expressjs.com/en/guide/using-middleware.html

中间件可以应用在 app 上,也可以应用在 router 上,可以使所有的请求都必须先经过中间件,最后到达具体的请求处理函数。下面是一个记录请求时间的中间件,可以记录任何请求的请求时间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var express = require('express');
var app = express();

var requestTime = function (req, res, next) {
req.requestTime = Date.now();
next();
};

app.use(requestTime);

app.get('/', function (req, res) {
var responseText = 'Hello World!';
responseText += 'Requested at: ' + req.requestTime + '';
res.send(responseText);
});

app.listen(3000);

express有一些自带的中间件,也有很多扩展中间件,最常用的是 static 中间件,用来访问静态资源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var options = {
dotfiles: 'ignore',
etag: false,
extensions: ['htm', 'html'],
index: false,
maxAge: '1d',
redirect: false,
setHeaders: function (res, path, stat) {
res.set('x-timestamp', Date.now());
}
}

app.use(express.static('public', options));
app.use(express.static('uploads'));
app.use(express.static('files'));

template

jade

1
2
3
4
5
//app.js
app.set('view engine', 'jade');
app.get('/', function (req, res) {
res.render('index', { title: 'Hey', message: 'Hello there!'});
});
1
2
3
4
5
6
//index.jade
html
head
title= title
body
h1= message

jade 语法非常简单:http://jade-lang.com

ejs

摘要: 别轻易打开微信APP,你的iPhone电量会被它耗干。

简介

我们相信公平和透明的薪酬机制很有益于公司的长期健康发展。我们在设计薪酬体系时重点考虑了几个问题:

  • 避免薪酬倒挂:很多公司,尤其是大的互联网公司在人才竞争激烈的年份给出较高的入职薪水,而老员工的薪水因为惯性没有相应提高,所以造成了新员工入职薪酬高于老员工的不公平现象。
  • 消除薪酬谈判带来的不公平:在招聘时,很多公司往往会根据候选人之前的薪酬以及他/她的期望值在可接受的范围内确定 offer。这样的方式事实上惩罚了之前薪酬偏低的人和不善于薪酬谈判的人,而有利于之前薪酬偏高或者善于薪酬谈判的人,造成了能力和贡献相似的人薪酬产生较大差别,也就导致了组织内部的不公平。所以我们决定从机制上保证给出的 offer 完全取决于我们自己对候选人的独立评判,与其他因素无关。做到这一点,自然也就不应有任何因素导致我们对 offer 进行调整,就可以把薪酬谈判从招聘中去掉。
  • 简单透明:作为一个精简的创业公司,我们需要一个透明、自动化的机制来保证在薪酬这样的敏感问题上尽可能的公平。当人为因素越少,机制越透明时,大家就越不需要把注意力放在这些方面,日常运作也更简单高效。
  • 保持灵活性,避免线性等级:专业领域的专家和承担管理角色的同事对团队都很重要,这需要在薪酬体系中体现。

月薪的计算

每位同事的月薪由下面的公式得出:

Salary = Function × Experience + Impact + Choice + Adjustment

其中 Function 是不同职位类型的月薪基数, 会随公司发展的情况进行调整。当前各类型的基数如下:

Function Base Amount
工程师 12000
设计师 8000
运营 8000
市场 8000
人力资源 7000

以上是我们现有的职位类型,将来会根据需要增加新的类型。

Experience 是不同经验和能力等级。对于新加入的成员这个等级会根据面试情况确定。对于在职的同事,会根据实际工作情况进行定期 review 和调整。这是一个团队内部的标准,和外部的通常标准或其他公司的职称标准完全无关。Experience 所对应的数值如下表所示:

Experience Level Experience Amount
E0 0.8
E1 1.0
E2 1.2
E3 1.4
E4 1.6
E5 1.8
E6 2.0

Impact 指的是在团队中的影响力和贡献,对应的数值如下图所示:

Impact Level Impact Amount
I0 0
I1 5,000
I2 8,000
I3 10,000

由于在面试中无法衡量将来在团队中的实际贡献,我们在新的 offer 中通常会把 impact 定为 I0 留待将来调整。

Choice 除联合创始人月薪的 Choice 部分为 0 外,其他同事在入职时默认 Choice 部分为月薪其他部分的 10%。我们会按期权激励计划授予每位同事期权。新同事在入职的第二个月底前可以在期权和月薪间做一个倾向性选择。选择期权的同事可以放弃月薪的 Choice 部分而在原定期权基础上多获得 20% 的期权。

Adjustment 这是一个对所有人固定的调整,目前为 1000 元。

例子

假设某个 function 的基数是 13000,那么这个 function 的 experience level 为 E3、impact level 为 I1 的同事,她的 Function × Experience + Impact 部分就是 13000 × 1.4 + 5000 = 23200。如果她选择更多期权,那么她的月薪就是 23200 + 1000 = 24200。如果她选择更多月薪,那么她的月薪就是 23200 × 110% + 1000 = 26520。

奖金

目前我们周期性的奖金是每年的年终奖。年终奖的金额是浮动的,规则详见 工作的评价和反馈机制。

结语

以上的机制借鉴了 Buffer 的 Open Salaries,具体的公式按照我们的情况做了调整, 希望让每个人都能清楚自己薪酬的由来以及未来的空间,也让潜在的应聘者一目了然了解可能的薪酬范围。

所有同事的薪酬以及发给候选人的 offer 都严格遵照这个标准。我们有可能不定期地调整各类职位的基数以适应公司发展的不同阶段并保持竞争力。由于这样的调整是整体的,所以不会导致倒挂等问题。另外由于影响最大的因素是 function 和 experience,这个公式也保留了足够的灵活性。比如一个很资深并作为项目负责人的工程师薪酬完全可能达到或超过 VP 和 C-Level 级别的管理者。

任何制度都会经历在实践中完善的过程,我们会根据反馈在发展中不断地完善这个薪酬体系。