理解JavaScript中的闭包

通俗的讲闭包就是在函数内部定义函数,内部的函数可以访问其外部函数的作用域。函数的变量可以被隐藏与作用域链之内,看起来像是变量被函数包裹了起来,因此被称作闭包。
从技术的角度讲,所有的JavaScript函数都是闭包,它们都是对象,它们都关联到作用域链。
理解闭包,需要首先了解JavaScript的词法作用域规则,函数的执行依赖变量作用域,这个作用域是在函数定义时绑定的,而不是在函数调用时绑定的。JavaScript函数对象的内部状态不仅包含函数的代码逻辑,还包含了当前的作用域链。
我们来看一下下面的在这个例子:

JavaScript Code:
  var scope = "global scope"
  function checkScope() {
    var scope = "local scope";
    function f() {return scope;}
    return f;
  }
  checkScope()();

运行代码可以发现,输出值为”local scope”,这意味着闭包可以访问外部函数的变量,即使外部函数已经执行完毕。

闭包存储的是对外部变量的引用,而不是真实的值。当闭包被调用之前就改变了外部函数的变量的值的时候,情况会变得很有趣。通过这一特性,我们可以在JavaScript实现私有变量。

JavaScript Code:
function car() {
  var status = "driving";
  return {
    getStatus: function() {
      return status;
    },

    setStatus: function(newStatus) {
      status = newStatus;
    }
  }
}

var audi = car();
audi.getStatus();
audi.setStatus("stop");
audi.getStatus();

通过分析可以发现,内部函数访问的外部函数的实际变量,而不是拷贝。为了深刻的理解这一特性,我们来看一下下面的例子:

JavaScript Code:
var handlers = function(nodes) {
  var i;
  for (i = 0; i < nodes.length; i += 1) {
    nodes[i].onClick = function(e) {
      console.log(i);
    };
  }
};

这段函数的本意是当用户点击某一节点的时候,打印出该节点的序号,但是事实是每次都返回节点的数目的值。这是因为事件处理器绑定了变量i本身,而不是在函数构造时的i的值。

修复这段代码的方式是循环外部构造一个函数,然后在循环体内调用它。

JavaScript Code:
var handlers = function(nodes) {
  var helper = function(i) {
    return function(e) {
      console.log(i);
    };
  };

  var i;
  for (i = 0, i < nodes.length, i += 1) {
    nodes[i].onClick = helper(i);
  }
};

这样,我们就能得到正确的结果了,当然还有一种改进的方式就是在循环体内构造一个立即调用的函数。
但是在循环中构造函数会带来不必要的计算,还会引起混淆,所以不建议这么做。

参考文献:

理解JavaScript中的this关键字

JavaScript函数调用中关于对象的引用一直是一个令人头疼的问题,造成这种困扰的很大一部分的原因是this关键字造成的。本文将深入探讨this关键字的各种用法,以及在不同情景中的表现。
this是一个关键字,不是变量,也不是属性名,JavaScript的语法不允许给this赋值,它没有作用域的限制。

普通的函数调用

在JavaScript中,每一个函数调用都会包含一个this值。对于普通的函数调用this指向全局对象,在严格模式下,this则为undefined
以函数形式调用的函数通常不用指定this关键字,不过可以使用this来判断当前是否处于严格模式中。

var strict = (function() { return !this; }());

普通函数调用的例子:

JavaScript Code:
function sayHi(name) {
  console.log(this + " says hello " + name);
}
sayHi("Jack");//[object Window] says hello Jack
sayHi.call("Jones", "Jack");//Jones says hello Jack
window.sayHi("Jack");//[object Window] says hello Jack
sayHi.call(window,"Jack");//[object Window] says hello Jack

嵌套的函数调用

嵌套的函数不会从调用它的函数中继承this,如果嵌套函数作为方法调用,那么this指向调用它的对象。如果嵌套函数作为函数调用,那么this
的值是全局对象或者undefined,这取决与处于何种模式下(严格模式、非严格模式)。如果想访问外部函数的this值,则需要将this的值保存到一个变量中。

JavaScript Code:
var person = {
  getName: function() {
    var that = this;
    console.log(this === person);//true
    f();
    function f() {
      console.log(this === person);//false
      console.log(that === person);//true
    }
  }
};

成员函数调用

一种常见的情形是成员函数的使用,通过该对象来调用。
JavaScript Code:
var person = {
name: “Jack”;
sayHi: function(thing) {
console.log(this.name + “ says hello “ + thing);
}
};
person.sayHi(“world!”);//Jack says hello world!

函数sayHi如何同person对象关联并不影响this的指向,我们来看一个动态关联的例子:

JavaScript Code:
var person = { name: "Jack"};
function sayHi(thing) {
    console.log(this.name + " says hello " + thing);
}
person.sayHi = sayHi;
person.sayHi("world!");//Jack says hello world!
sayHi("world!");//[object window] says hello world!

间接调用

我们可以通过callapply来间接的调用函数,这两个都允许显示的指定调用所需的this值,也就是说任何函数都可以作为任何对象的方法来调用,即使这个函数不是某对象的方法。这两个方法的不同之处在于call使用自有的实参列表作为函数的实参,apply接受一个数组。

JavaScript Code:
function sayHi(name) {
  console.log(this + " says hello " + name);
}
sayHi.call("Jones", "Jack");//Jones says hello Jack
sayHi.call(window,"Jack");//[object Window] says hello Jack
sayHi.apply("Jones", ["Jack"]);//Jones says hello Jack

有时候我们希望能够引用一个有着持久化this值的函数,那么我们可以使用bind函数。

JavaScript Code:
function bind(func, obj) {
  if (func.bind) {
    return func.bind(obj);
  } else {
    return function() {
      func.apply(obj, arguments);
    };
  }
}
var bindSayHi = bind(person.sayHi, person);
bindSayHi("world!");//Jack says hello world!

jQuery中的this

JavaScript Code:
$("button").click(function(event) {
  console.log($(this).prop("name"));
});

$(this)等同于jQuery中的this关键字,它被用在一个匿名函数内部,该函数在buttonclick方法中执行。jQuery$(this)绑定到调用click方法的对象上,所以$(this)指向$("button")对象,即使$(this)被用在匿名函数内部,外部this对其不可见。

参考文献:

理解JavaScript中的变量作用域及声明提前

在编程语言中,作用域控制着变量及参数的可见性与生命周期,能够减少名称冲突以及提供必要的自动内存管理。如果想要更好的理解及运用JavaScript,那么理解其作用域就显得很有必要,本文将深入探讨这一主题。
变量的作用域定义了在该变量的上下文中对于该变量的可见性,JavaScript中的变量有全局变量和局部变量之分。

局部变量

大多数语言都有块级作用域,在一个代码块中在所定义的代码块之外是不可见的,在代码块中定义的变量在代码块执行结束之后就会被释放掉。但是实际上JavaScript并不支持块级作用域,这常常会造成混淆。对于JavaScript来说,它拥有函数级的作用域,函数内部的变量只在其内部及内部定义的函数可见。
JavaScript Code:
var greet = “Hello world!”;
function sayHi() {
var greet = “Hello inner!”;
console.log(greet);//“Hello inner!”
}

console.log(greet);//“Hello world!”

#JavaScript没有块级作用域

让我们来看下面的一个例子:

JavaScript Code:
var greet = “Hello world!”
if (greet) {
greet = “hello inner!”
console.log(greet);//“Hello inner”
}
console.log(greet)://“Hello inner”
我们可以看到代码块外部定义的变量的值已经被改变。

#全局变量

所有在函数外部定义的变量都是全局变量,对整个应用可见,如果在函数体内定义变量,但是没有使用var关键字,那么它也将成为全局变量。

JavaScript Code:
var greetOuter = “Hello world!”;
greetAnother = “Hello another!”
function sayHi() {
greet = “Hello inner!”
console.log(greetOuter);//“Hello world!”
console.log(greetAnother);//“Hello another!”
console.log(greet);//“Hello inner!”
}
console.log(greet);//“Hello inner!”

#setTimeout中的变量实在全局作用域中执行的

需要注意的是setTimeout中的所有函数都是在全局作用域中执行的。

JavaScript Code:
// setTimeout中的this对象指向全局变量window
var highValue = 200;
var constantVal = 2;
var myObj = {
highValue: 20,
constantVal: 5,
calculateIt: function () {
setTimeout (function () {
console.log(this.constantVal * this.highValue);
}, 2000);
}
}
myObj.calculateIt(); // 400

声明提前

JavaScript的函数作用域是指在函数体内声明的所有变量在函数体内始终可见,也就是说,在函数体内变量在声明之前已经可以使用,JavaScript的这个特性被称为声明提前。JavaScript中的所有变量声明都被提升至函数体的顶部,但是不包括赋值。声明提前的这一步操作是在JavaScript引擎预编译时进行的,是在代码开始执行之前。

JavaScript Code:
var greet = "Hello world!";
function sayHi() {
  console.log(greet);//"undefined"
  var greet = "Hello inner";
  console.log(greet);//"Hello inner"
}

由于声明提前的存在,greet的声明被提升到了函数体的顶部,在函数体内的局部变量拥有更高的优先级,因此覆盖掉了同名的全局变量。声明提前只是提升了变量的声明,但是赋值操作并没有提前,还停留在原来的位置,所以函数体内的第一行语句输出为undefined

另外,需要指出的一点是,函数声明会比变量声明拥有更高的优先级,在提升的过程中会覆盖掉同名的变量声明,但是函数表达式则没有这一特性。

由于声明提前的存在以及JavaScript没有块级作用域,所以推荐的做法是将变量的声明放在函数体的顶部,可以真实的反应变量的作用域。

参考文献:

理解Rails中的delegate模块

Rails中有一个非常炫酷的associations特性能够帮助我们很方便的创建链式方法,它看起来像是这样子的:

Ruby code:
product.provider.name
provider.address.city
company.building.city

但是,这破坏了“得墨忒耳”法则,我们更希望通过如下方式进行调用:

Ruby code:
product.provider_name
provider.address_city #or provider.city
company.city

为了实现这样的调用方式,通常我们需要在对应的model中定义一些方法:
  
Ruby code:
class Product < ActiveRecord::Base
belongs_to :provider

  def provider_name
    provider.name
  end

  # More methods to access the provider's attributes
end

class Provider < ActiveRecord::Base
  has_many :products

  # Has a name attribute
end

这是一个很好的解决方案,但是当我们有许多的属性时,就要定义更多的方法,这会增加代码的体积,也会更容易的引入bug。更进一步的我们可以引入ruby的动态特性。

Ruby code:
%w[name age address].each do |attr|
  define_method "provider_#{attr}" do
    provider.send("#{attr}")
  end
end

很酷的解决方案,但是仍然不够简洁,我们可以利用Rails给我们提供的轮子Delegate提供更加优雅的解决方案。

Ruby code:
class Product < ActiveRecord::Base
  belongs_to :provider

  delegate :name, to: :provider, prefix: true
end

class Provider < ActiveRecord::Base
  has_many :products

  # Has a name attribute
end

这样,我们就可以实现前文所述的调用方式了,关于Delegate,必须以hash结尾。
若定义了prefix: true,则需要在调用时加上委托对象前缀,当然也可以自定义前缀。同时也提供了一个allow_nil的选项,如果定义为true,则被委托的对象不存在时,不会抛出异常。除此之外,还提供对实例变量、类变量、常量甚至类本身的代理。需要指出的是如果对实例变量委托,那么即使定义了allow_nil: true,也会抛出异常。需要了解更多,在文档中有更加详细的说明。

参考文献:

Git合并多次提交

在工作中,有时候会遇到多次针对一个问题的多次提交,因而会产生很多的提交记录,这让人看起来很不爽。
那么怎么样才能将多次的提交合并呢?答案是通过rebase命令。

当你执行了git rebase --interactive HEAD~2 master命令后,将会出现如下的交互式画面:

Code:
pick e30746d modify ruby case statement
pick d870d17 fix display problems

# Rebase 15e28f4..d870d17 onto 15e28f4
#
# Commands:
#  p, pick = use commit
#  r, reword = use commit, but edit the commit message
#  e, edit = use commit, but stop for amending
#  s, squash = use commit, but meld into previous commit
#  f, fixup = like "squash", but discard this commit's log message
#  x, exec = run command (the rest of the line) using shell

注释掉的部分是git给出的操作提示,这里选择squash选项来替换掉pick,需要注意的是,最上方的那一行pick
需要保留,否则会出现Cannot 'squash' without a previous commit的错误提示。如果你不小心替换掉了,那么
需要使用git rebase --abort再来一遍。

退出编辑器之后,将会进入到下一个界面,如下所示:

Code:
# This is a combination of 2 commits.
# The first commit's message is:

modify ruby case statement

# This is the 2nd commit message:

fix display problems

没有被注释掉的部分是前两次的提交记录,可以选择保留,也可以选择删掉重写,然后退出就可以了。
之后就可以通过git log --oneline --decorate查看提交记录,发现前几次的提交已经合并掉了。

另外一种合并的方式:

code:
$ git reset HEAD~5
$ git add .
$ git commit -am "Here's the bug fix that closes #28"
$ git push --force

如果你想修复之前的提交,又不想有多次记录,那么可以在修复的这次提交中使用:

Code:
git commit --fixup bbb2222
git rebase --interactive --autosquash bbb1111
#bbb1111为你需要修复的提交的前一次提交

参考文献:

【翻译】利用Nginx加速Rails下载

Rails有一个选项可以用来启用X-Accel-Redirect,但是这并不是全部。为了能够使其工作,还需要对Nginx进行一些配置,这稍微有点繁琐。下面说一下我是怎么做的。

问题:高效的传送文件

扩展Rails应用全部都是有关与减少请求数,使得等待的时间越短越好。将耗费时间的请求使用异步的方式在后台运行,分发静态资源请求到CDN,使用缓存降低响应时间。

但是有一类情景需要耗费比较长的时间,但是并不适合与一上的任意一种情况,那就是通过Rails应用传递大文件。你可能需要解决这个问题,因为文件需要加密(例如Rails session),或者是动态生成的。在这些情景下,你不能提前将其上传到CDN,异步传送可能会造成不好的用户体验。

那么如何才能传送大文件的过程中不阻塞Rails应用呢?

Nginx和SEND_FILE是如何工作的?

如果你正在使用Nginx,那么解决方案就是使用X-Accel-Redirect,它是这样工作的。

  1. Rails控制器调用send_file方法,准备一个将要被下载的文件。对于一个生成好的文件,通常会被放在<rails root>/tmp文件夹下。
  2. Rack检测X-Accel-Mapping头部是否存在于请求中。如果存在,它就通过这种映射将send_file路径转换成Nginx能够理解的URI。
  3. Rack发送一个包含X-Accel-Redirect头的空响应给Nginx,这个头部告诉Nginx”请加载这个URI,并将其作为响应”。从Rails应用的角度来讲,在这一点上响应已经完成了,并且能够继续接受其他的请求。
  4. Nginx完成当前请求并处理一段时间,内部通过X-Accel-Redirect头部提供的URI进行重定向。
  5. Nginx查询配置文件(例如location指令)来找到URI指向的文件。假定该文件存在,它将会高效的将文件传送给客户端。

如你所见,一个简单的sned_file只有在一系列的步骤都设置正确才会成功,下面是让其正确工作的步骤。

设置Rails

首先,Rails需要被告知使用X-Accel-Redirect特性。否则Rails自身将会使用一个更加基础也更加慢的方式来处理I/O流。

为了启用Nginx加速,反注释掉config/environments/production.rb中的代码:

config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX

设置Nginx路径来伺服文件

如果不做配置,Nginx并不能伺服文件系统中的任何文件,我们需要设置document root和一个URI来访问到将要传送的文件。为了防止和Rails的路由产生冲突,我选择使用__send_file_accel作为URI。

document root需要仔细选择以让Rails应用能够轻松的访问到。我的项目使用了 Capistrano, Rails应用被部署在/home/deployer/apps/my_app/releases,所以很自然的root路径为:

code:
# Allow NGINX to serve any file in /home/deployer/apps/my_app/releases
# via a special internal-only location.
location /__send_file_accel {
    internal;
    alias /home/deployer/apps/my_app/releases;
}

发送X-Accel-Mapping

假设我们有以下的文件将要传送:

/home/deployer/apps/my_app/releases/20151003173639/tmp/file.zip

这意味者Nginx将会收到以下头部:

X-Accel-Redirect: /__send_file_accel/20151003173639/tmp/file.zip

Rails(更精确地将Rack)需要直到怎么进行转换,下面是通过Nginx发送给Rails的正确头部(比如将其放在Unicorn的反向代理文件中):

code:
proxy_set_header X-Sendfile-Type X-Accel-Redirect;
proxy_set_header X-Accel-Mapping /home/deployer/apps/my_app/releases/=/__send_file_accel/;

测试

下面是一个通过Rails控制器传送文件的小例子:

send_file Rails.root.join("tmp", "file.zip")

在下载的过程中,你将会在Rails的production.log中看到如下的内容:

code:
Sent file /home/deployer/apps/nginx-test/releases/20151011032644/tmp/file.zip (0.2ms)
Completed 200 OK in 2ms (ActiveRecord: 0.0ms)

在响应头中,你将会看到是Rails还是Nginx在传送文件的线索。如果响应头部中包含X-Request-Id,这就意味这是Rails在伺服。

code:
X-Request-Id: a4d62cdb-569b-4120-b1ff-e2adbf77039a
X-Runtime: 0.005559

如果所有的配置都正确的话,你将不会看到以上内容,也就是说Nginx在传递文件,而不是Rails。

这就是所有的步骤,你现在将传递文件的责任从Rails传递给Nginx了。

参考文献:

工厂方法模式

意图

定义一个用于创建对象的接口,让子类决定实例化哪一个类,Factory Method使一个类的实例化延迟到其子类。

工厂方法模式是面向对象设计中最为常用的模式,这种模式是创建对象的最好方式之一。在工厂方法模式中,我们创建对象不需要向客户暴露创建的逻辑,转而使用通用的接口去引用一个新创建的对象。

实现

下面是一个具体的实现。

class Car
  def run
    raise NotImplementedError, "must implement run method in subclass"
  end
end

class BMW < Car
  def run
    p "BMW on the way!"
  end
end

class Audi < Car
  def run
    p "Audi on the way!"
  end
end

class CarFactory
  def make_car car
    car.safe_constantize.new
  end
end

class DriveCar
  def drive
    car = CarFactory.new.make_car "BMW"
    car.run
  end
end

在上面的实现中,我们首先中创建一个Car, 其中定义了必须被子类复写的方法。然后创建两个子类,BMW, Audi类。这两个类都覆写了父类run方法, 然后定义一个CarFactory用来返回特定的车辆实例。工厂方法不在将与特定应用有关的类绑定到代码中。

适用性

  • 当一个类不知道它所要创建的对象的类的时候。
  • 当一个类希望由它的子类来指定要创建的对象的时候。
  • 当类将创建的对象的职责委托给多个帮助子类中的某一个,并且你希望将哪一个帮助子类是代理者的这一信息局部化的时候。

参考文献:

抽象工厂模式

意图

抽象工厂模式提供一系列相关或者相互依赖的对象的接口,而无需指定它们具体的类。客户类不需要直接构建对象,它会调用该接口提供的方法。

实现

下面是一个具体的实现。

class AbstractMazeFactory
  def make_maze
    raise NotImplementedError, "You should implement this method"
  end

  def make_wall
    raise NotImplementedError, "You should implement this method"
  end

  def make_room
    raise NotImplementedError, "You should implement this method"
  end
end

class MazeFactory < AbstractMazeFactory
   def make_maze
     Maze.new
   end

   def make_wall wall
     wall.camelize.constantlize.new
   end

   def make_room
     Room.new
   end
end

class Room
  def room_number
    p "i am room number 1"
  end
end

class Maze
  def maze
    p "maze builded complete"
  end
end

class AbstractWall
  def feature
    raise NotImplementedError, "You should implement this method"
  end
end

class IronWall < AbstractWall
  def feature
    p "I am an Iron Wall"
  end
end

class BrickWall < AbstractWall
  def feature
    p "I am a Brick Wall"
  end
end

class ClayWall < AbatractWall
  def feature
    p "I am a Clay Wall"
  end
end

class Client
  def make_maze
    maze_factory = MazeFactory.new
    room = maze_factory.make_room
    room.room_number
    iron_wall = maze_factory.make_wall "iron_wall"
    iron_wall.feature
    brick_wall = maze_factory.make_wall "brick_wall"
    brick_wall.feature
    clay_wall =  maze_factory.make_wall "clay_wall"
    clay_wall.feature
    maze = maze_factory.make_maze
    maze.maze
  end
end

在上面的实现中,我们首先中创建一个AbstractMazeFactory,其中定义了必须被子类复写的方法。然后创建一个MazeFactory类,
复写父类所有的方法,并返回Room,MazeWall子类的实例。最后利用Client类构造出一个迷宫。

适用性

  • 一个系统要独立于它的产品的创建、组合或者实现时。
  • 一个系统要由多个产品系列中的一个来配置时。
  • 当你强调一系列相关的产品的对象的设计以便进行联合使用时。
  • 当你提供一个产品类库,而只想显示它的接口而不是实现时。

优缺点

  • 它分离了具体的类。
  • 它使得易于交换产品序列。
  • 它有利于产品的一致性。
  • 它难以支持新种类的产品

参考文献:

Git Clean

今天从版本库pull一个项目,然后出现了如下错误

error: The following untracked working tree files would be overwritten by merge:

经过一番google之后,找到了如下解决方案:

git clean  -d  -fx ""

在运行了上面的命令之后,造成了更严重的后果,所有的未被追踪的文件和被忽略的文件全部都被删除了。。。查阅文档之后在发现这条命令干了以下的事情:

  • -x 被忽略的文件和没有加入到git版本库的文件都会被删除
  • -d 删除没有被追踪的文件和文件夹
  • -f 强制执行

为了避免再次发生这样的事情,应该先运行一下如下命令,看对文件会造成什么影响。

git clean -dfx --dry-run
  • #-n, –dry-run 不会删除任何文件,只是告诉你会发生什么
  • #-q 只报告错误,不会报告被成功删除的文件
  • #-e 会忽略.gitignore文件中定义的文件,同时包括$GIT_DIR/info/exclude。还有自定义的忽略规则。
  • #-X 移除git忽略的文件,但会保存已经创建的文件。

【翻译】Angular2 变更检测

本文讲深入探讨Angular2中的变更检测系统。

概览

一个Angular2的应用是由组件组成的树。
Alt "angular2"
一个Angular2应用是一个反应式的系统,变更检测是其核心。
Alt "angular2"
每一个组件都有一个变更检测器,用来检测在末班中定义的绑定。一个绑定的例子:[todo]='t'。变更检测器使用深度优先策略遍历绑定。在Angular2中并没有一个通用的机制来实现双向绑定(但是你仍然可以实现双向绑定和ng-model,你可以通过阅读“这篇”文章了解更多)。这也是为什么变更检测是没有环的树,这使得该系统拥有更好的性能。更重要的是Angular2能够保证更好的预测系统的行为和更容易推理。

Angular2到底有多快?
变更检测器默认遍历树的每一个节点以检测其是否变化,这种行为会在每一个浏览器事件发生时被触发。虽然看起来性能非常的低,但是实际上Angular2能够在几毫秒的时间内进行成百上千的简单检测(具体的数量依赖于不同的浏览器平台)。怎么实现这一令人印象深刻的结果是另一篇文章将要探讨的内容。

因为Javascript并没有提供对象变化突变保障,所以Angular2表现的很保守,每次都会进行所有的检测。但是我们知道可以通过使用不可变对象或者可观察对象来持有属性。Angular2以前不可以利用这种特性,现在可以了。

不可变对象

如果一个组件仅仅依赖与其绑定,并且绑定是不可变的,那么该组件只有在其中的一个绑定发生了变化时才变化。因此,我们可以跳过变更探测树的子树直到该事件发生。当事件发生时,我们只用检测子树一次,然后就关闭它直到下一次变化发生。

Alt "angular2"

如果我们非常激进的处理不可变对象,大多数时间变更检测树的很大一部分都可以被关闭。
Alt "angular2"
实现这一功能,只需要将变更检测的策略设置为ON_PUSH

@Component({changeDetection:ON_PUSH})
class ImmutableTodoCmp {
  todo:Todo;
}

可观察对象

如果一个组件仅仅依赖于其绑定,并且该绑定是一个可观察对象,那么该组件只有在绑定触发了一个事件时才会变化。所以我们能够跳过变更检测树的子树直到改事件发生。当改事件发生时,我们只用检测子树一次,然后关闭它直到下乙烯事件发生。

虽然看起来和不可变对象的例子很像,实际上他们之间是有很大的区别的。如果你拥有一个由拥有不可变绑定对象的组件组成的树,一个变化需要从根节点开始遍历组件。这并不是我们处理可观察对象的方式。

让我们使用一个小例子来说明这一问题。

type ObservableTodo = Observable<Todo>;
type ObservableTodos = Observable<Array<ObservableTodo>>;

@Component({selector:’todos’})
class ObservableTodosCmp {
  todos:ObservableTodos;
  //...
}

模板示例:

<todo *ng-for="var t of todos" todo = "t"></todo>

ObservableTodoCmp:

@Component({selector:’todo’})
  class ObservableTodoCmp {
    todo:ObservableTodo;
    //...
  }

如你所见,Todos组件只用有一个对由todos数组组成的可观察对象的引用,所以它检测不到单个todos组件的变化。

处理的方式是在可观察对象todo触发一个事件时,检查从根到该变化的todo组件的路径。变更检测系统能够确保这种事情的发生。

假设我们的系统仅仅使用可观察对象,当系统启动时,Angular2将会检测检测所有的对象。
Alt "angular2"
所以经过了第一步之后,状态将会变成如下所示的样子。
Alt "angular2"

让我们假设todo可观测对象触发了一个事件,那么系统状态装回转变成如下所示的样子。
Alt "angular2"

当检测完了App_ChangeDetector, Todos_ChangeDetector和Todo_ChangeDetector之后,将会回到如下所示的状态。
Alt "angular2"

假设这些变化极少发生,组件组成一颗平衡树,那么使用可观测对象将会使变更检测的复杂度由O(N)变为O(logN),其中N是系统中绑定对象的数量。

可观测对象会造成级联更新么?

可观测对象的名声不太好,是因为他们会造成级联更新。任何一个有个大规模系统设计经验并且用到了可观测模型的开发者将会明白我在说什么。一个可观测对象的更新会造成一系列的可观测对象触发更新。沿着这条路经的某个view也会随着更新,这种系统会很难进行推测。

在Angular2中使用可观测对象将不会造成这种问题。通过一个可观察对象触发的事件只是标示从该组件到根的路径作为下次将要检测的路径。所以更新的顺序和是否使用客人观察对象没有关系。这是非常重要的,使得使用可观察对象变成一个非常简单的优化,不会改变你处理系统的方式。

是否需要到处使用可观察对象/不可变对象

不,你不需要。你可以在你的系统中部分的使用可观察对象(比如一些巨大的表),这一部分将会获得性能上的改进。更进一步,你可以任意组合不同类型的组件,并且获得他们所有的好处。例如,一个可观测组件可以包含一个不可变组件,同时自身也可以包含一个可观察对象。即使在这种情况下,对象检测系统会使得蔓延式变化所需要的检测的数量最小化。

没有特殊情况

对可变化对象和可观察对象的支持并没有固化在变更检测系统中。这种类型的组件并没有什么特殊之处,所以你可以书写你自己的指令,以一种更只能的方式来使用变更检测。例如,想象一下一条指令每个几秒就更新一下它的内容。

总结

  • 一个Angular2应用是一个响应式的系统。
  • 变更检测系统从根到叶子节点传播绑定。
  • 不像Angular1.×,变化检测图是一个有向树。结果是,改系统拥有更好的性能和可预测。
  • 默认情况下,变更检测系统遍历整个树,但是如果你使用不可变对象或者可观察对象,你将利用其优势,当他们真正变化时,你只需要检测树的一部分。
  • 这些优化可以随意组合,不会破坏变更检测提供的保证。

原文: