首页

Vue Router源码分析 History

发表于2016年12月23日 分类: js 标签: vue-router vue js

上篇中介绍了 vue-router 的整体流程,但是具体的 history 部分没有具体分析,本文就具体分析下和 history 相关的细节。

初始化 Router

通过整体流程可以知道在路由实例化的时候会根据当前 mode 模式来选择实例化对应的History类,这里再来回顾下,在 src/index.js 中:

// ...
import { HashHistory, getHash } from './history/hash'
import { HTML5History, getLocation } from './history/html5'
import { AbstractHistory } from './history/abstract'
// ...
export default class VueRouter {
// ...
  constructor (options: RouterOptions = {}) {
// ...
    // 默认模式是 hash
    let mode = options.mode || 'hash'
    // 如果设置的是 history 但是如果浏览器不支持的话 
    // 强制退回到 hash
    this.fallback = mode === 'history' && !supportsHistory
    if (this.fallback) {
      mode = 'hash'
    }
    // 不在浏览器中 强制 abstract 模式
    if (!inBrowser) {
      mode = 'abstract'
    }
    this.mode = mode
    // 根据不同模式选择实例化对应的 History 类
    switch (mode) {
      case 'history':
        this.history = new HTML5History(this, options.base)
        break
      case 'hash':
        // 细节 传入了 fallback
        this.history = new HashHistory(this, options.base, this.fallback)
        break
      case 'abstract':
        this.history = new AbstractHistory(this)
        break
      default:
        assert(false, `invalid mode: ${mode}`)
    }
  }
// ...

可以看到 vue-router 提供了三种模式:hash(默认)、history 以及 abstract 模式,还不了解具体区别的可以在文档 中查看,有很详细的解释。下面就这三种模式初始化一一来进行分析。

更多

Vue Router源码分析 整体流程

发表于2016年11月23日 分类: js 标签: vue-router vue js

在现在单页应用这么火爆的年代,路由已经成为了我们开发应用必不可少的利器;而纵观各大框架,都会有对应的强大路由支持。Vue.js 因其性能、通用、易用、体积、学习成本低等特点已经成为了广大前端们的新宠,而其对应的路由 vue-router 也是设计的简单好用,功能强大。本文就从源码来分析下 Vue.js 官方路由 vue-router 的整体流程。

本文主要以 vue-router 的 2.0.3 版本来进行分析。

首先来张整体的图:

vue-router.js流程图

先对整体有个大概的印象,下边就以官方仓库下 examples/basic 基础例子来一点点具体分析整个流程。

目录结构

先来看看整体的目录结构:

vue-router 目录结构图

和流程相关的主要需要关注点的就是 componentshistory 目录以及 create-matcher.jscreate-route-map.jsindex.jsinstall.js。下面就从 basic 应用入口开始来分析 vue-router 的整个流程。

入口

首先看应用入口的代码部分:

import Vue from 'vue'
import VueRouter from 'vue-router'

// 1. 插件
// 安装 <router-view> and <router-link> 组件
// 且给当前应用下所有的组件都注入 $router and $route 对象
Vue.use(VueRouter)

// 2. 定义各个路由下使用的组件,简称路由组件
const Home = { template: '<div>home</div>' }
const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }

// 3. 创建 VueRouter 实例 router
const router = new VueRouter({
  mode: 'history',
  base: __dirname,
  routes: [
    { path: '/', component: Home },
    { path: '/foo', component: Foo },
    { path: '/bar', component: Bar }
  ]
})

// 4. 创建 启动应用
// 一定要确认注入了 router 
// 在 <router-view> 中将会渲染路由组件
new Vue({
  router,
  template: `
    <div id="app">
      <h1>Basic</h1>
      <ul>
        <li><router-link to="/">/</router-link></li>
        <li><router-link to="/foo">/foo</router-link></li>
        <li><router-link to="/bar">/bar</router-link></li>
        <router-link tag="li" to="/bar">/bar</router-link>
      </ul>
      <router-view class="view"></router-view>
    </div>
  `
}).$mount('#app')

作为插件

上边代码中关键的第 1 步,利用 Vue.js 提供的插件机制 .use(plugin) 来安装 VueRouter,而这个插件机制则会调用该 plugin 对象的 install 方法(当然如果该 plugin 没有该方法的话会把 plugin 自身作为函数来调用);下边来看下 vue-router 这个插件具体的实现部分。

更多

也说css之overflow

发表于2016年10月26日 分类: css 标签: overflow css

overflow 是一个非常常用的 CSS 属性,一般来说会认为很简单,其实细究之后就会发现他还有很多小特性或者说意想不到的结果;下面就介绍下(在浏览器环境下)关于 overflow 的小总结。

哪些元素上有效?

首先 overflow 是应用到哪些元素上有效的,任意元素?当然不是,它只能应用于块容器上。那什么是块容器呢,简单来说:除了 table 和可替换(置换)元素之外的块级元素都是块容器元素;但是反过来说块容器元素一定是块级元素的吗?当然也是 NO ,这是因为对于非替换的 inline-block 元素和 table-cell 元素而言,他们是块容器元素但是却不是块级元素。

作用

overflow 属性指定了一个块容器元素在其内容溢出这个元素的时候,内容是否裁掉。

overflow的值

可以取的值有:visible(初始值)、 hiddenscrollautoinherit

下面分别介绍下他们的含义:

更多

移动端material风格日期时间选择器

发表于2016年10月23日 分类: js 标签: date-time-picker date-picker time-picker 日期选择器

好多时候在移动端需要一个的日期选择器,由于在应用上有可能应用各种框架库(Vue.js, React.js, zepto.js等);所以说一个无依赖的,这样易于上层进行封装。直接开门见山,先来张动图看看效果:

Select date and time

可以看出整个风格就是 Material Design 风格的,主要特点就是:

  • 手势操作:左划右划切换月份,当然动画效果还是要有的。

  • 快速选择年月:点击部分切换到选择年界面,点击月日周来回切换 到主日期选择界面和月份选择界面(如果在非日期选择主界面点击就会切换到主界面,如果在主界面点击就切换到快速选择月份界面);当然选择年一级选择月份界面要有顺滑的滑动效果。

  • 钟表样式时间选择:直接、简单选择时间。

更多

Http协议206状态码

发表于2015年11月12日 分类: server 标签: http http range http content-range 206状态码

之前一直不知道这个是用来干什么用的,面试的时候被问到了,所以就查阅相关文章,这里记下自己的理解。

首先来看206状态码(Partial Content)代表什么意思,来自维基百科:

服务器已经成功处理了部分GET请求。类似于FlashGet或者迅雷这类的HTTP 下载工具都是使用此类响应实现断点续传或者将一个大文档分解为多个下载段同时下载。

该请求必须包含Range头信息来指示客户端希望得到的内容范围,并且可能包含If-Range来作为请求条件。

响应必须包含如下的头部域:

1) Content-Range用以指示本次响应中返回的内容的范围;如果是Content-Type为multipart/byteranges的多段下载,则每一multipart段中都应包含Content-Range域用以指示本段的内容范围。假如响应中包含Content-Length,那么它的数值必须匹配它返回的内容范围的真实字节数。 2) Date

3) ETag和/或Content-Location,假如同样的请求本应该返回200响应。

4) Expires, Cache-Control,和/或Vary,假如其值可能与之前相同变量的其他响应对应的值不同的话。

假如本响应请求使用了If-Range强缓存验证,那么本次响应不应该包含其他实体头;假如本响应的请求使用了If-Range弱缓存验证,那么本次响应禁止包含其他实体头;这避免了缓存的实体内容和更新了的实体头信息之间的不一致。否则,本响应就应当包含所有本应该返回200响应中应当返回的所有实体头部域。

假如ETag或Last-Modified头部不能精确匹配的话,则客户端缓存应禁止将206响应返回的内容与之前任何缓存过的内容组合在一起。 任何不支持Range以及Content-Range头的缓存都禁止缓存206响应返回的内容。

可以看出,206代表的意思也就是响应了部分内容,那肯定就需要有上边解释说的一些消息头来控制怎么通过每次得打部分内容的方式来完成整个的接收。利用的场景也就是对于大文件下载比较多,也就实现了断点续传的功能。

更多

Http 协议中的transfer Encoding

发表于2015年11月12日 分类: server 标签: http transfer-encoding http header 转载

这是转载自ququ的一片文章,讲些的很不错,特转载过来。

原文地址:https://imququ.com/post/transfer-encoding-header-in-http.html

本文作为我的博客HTTP 相关专题新的一篇,主要讨论 HTTP 协议中的 Transfer-Encoding。这个专题我会根据自己的理解,以尽量通俗的讲述,结合代码示例和实际场景来说明问题,欢迎大家关注和留言交流。 Transfer-Encoding,是一个 HTTP 头部字段,字面意思是「传输编码」。实际上,HTTP 协议中还有另外一个头部与编码有关:Content-Encoding(内容编码)。Content-Encoding 通常用于对实体内容进行压缩编码,目的是优化传输,例如用 gzip 压缩文本文件,能大幅减小体积。内容编码通常是选择性的,例如 jpg / png 这类文件一般不开启,因为图片格式已经是高度压缩过的,再压一遍没什么效果不说还浪费 CPU。

而 Transfer-Encoding 则是用来改变报文格式,它不但不会减少实体内容传输大小,甚至还会使传输变大,那它的作用是什么呢?本文接下来主要就是讲这个。我们先记住一点,Content-Encoding 和 Transfer-Encoding 二者是相辅相成的,对于一个 HTTP 报文,很可能同时进行了内容编码和传输编码。

Persistent Connection

暂时把 Transfer-Encoding 放一边,我们来看 HTTP 协议中另外一个重要概念:Persistent Connection(持久连接,通俗说法长连接)。我们知道 HTTP 运行在 TCP 连接之上,自然也有着跟 TCP 一样的三次握手、慢启动等特性,为了尽可能的提高 HTTP 性能,使用持久连接就显得尤为重要了。为此,HTTP 协议引入了相应的机制。

HTTP/1.0 的持久连接机制是后来才引入的,通过 Connection: keep-alive 这个头部来实现,服务端和客户端都可以使用它告诉对方在发送完数据之后不需要断开 TCP 连接,以备后用。HTTP/1.1 则规定所有连接都必须是持久的,除非显式地在头部加上 Connection: close。所以实际上,HTTP/1.1 中 Connection 这个头部字段已经没有 keep-alive 这个取值了,但由于历史原因,很多 Web Server 和浏览器,还是保留着给 HTTP/1.1 长连接发送 Connection: keep-alive 的习惯。

浏览器重用已经打开的空闲持久连接,可以避开缓慢的三次握手,还可以避免遇上 TCP 慢启动的拥塞适应阶段,听起来十分美妙。为了深入研究持久连接的特性,我决定用 Node 写一个最简单的 Web Server 用于测试,Node 提供了 http 模块用于快速创建 HTTP Web Server,但我需要更多的控制,所以用 net 模块创建了一个 TCP Server:

require('net').createServer(function(sock) {
    sock.on('data', function(data) {
        sock.write('HTTP/1.1 200 OK\r\n');
        sock.write('\r\n');
        sock.write('hello world!');
        sock.destroy();
    });
}).listen(9090, '127.0.0.1');

启动服务后,在浏览器里访问 127.0.0.1:9090,正确输出了指定内容,一切正常。去掉 sock.destroy() 这一行,让它变成持久连接,重启服务后再访问一下。这次的结果就有点奇怪了:迟迟看不到输出,通过 Network 查看请求状态,一直是 pending。 这是因为,对于非持久连接,浏览器可以通过连接是否关闭来界定请求或响应实体的边界;而对于持久连接,这种方法显然不奏效。上例中,尽管我已经发送完所有数据,但浏览器并不知道这一点,它无法得知这个打开的连接上是否还会有新数据进来,只能傻傻地等了。

更多

Http 协议中 vary 的一些研究

发表于2015年11月12日 分类: server 标签: http http vary http header 转载

本篇还是转载自ququ的文章,地址:https://imququ.com/post/vary-header-in-http.html

经常抓包看 HTTP 请求的同学应该对 Vary 这个响应头字段并不陌生,它有什么用?用 PageSpeed 工具检查页面时,经常看到「Specify a Vary: Accept-Encoding header(请指定一个 Vary: Accept-Encoding 标头)」这样的建议,为什么要这样做?本文记录我对 Vary 的一些研究,其中就包含这些问题的答案。

HTTP 内容协商

要了解 Vary 的作用,先得了解 HTTP 的内容协商机制。有时候,同一个 URL 可以提供多份不同的文档,这就要求服务端和客户端之间有一个选择最合适版本的机制,这就是内容协商。

协商方式有两种,一种是服务端把文档可用版本列表发给客户端让用户选,这可以使用 300 Multiple Choices 状态码来实现。这种方案有不少问题,首先多一次网络往返;其次服务端同一文档的某些版本可能是为拥有某些技术特征的客户端准备的,而普通用户不一定了解这些细节。举个例子,服务端通常可以将静态资源输出为压缩和未压缩两个版本,压缩版显然是为支持压缩的客户端而准备的,但如果让普通用户选,很可能选择错误的版本。

所以 HTTP 的内容协商通常使用另外一种方案:服务端根据客户端发送的请求头中某些字段自动发送最合适的版本。可以用于这个机制的请求头字段又分两种:内容协商专用字段(Accept 字段)、其他字段。

首先来看 Accept 字段,详见下表:

请求头字段 说明 响应头字段
Accept 告知服务器发送何种媒体类型 Content-Type
Accept-Language 告知服务器发送何种语言 Content-Language
Accept-Charset 告知服务器发送何种字符集 Content-Type
Accept-Encoding 告知服务器采用何种压缩方式 Content-Encoding

例如客户端发送以下请求头:

Accept:*/*
Accept-Encoding:gzip,deflate,sdch
Accept-Language:zh-CN,en-US;q=0.8,en;q=0.6

表示它可以接受任何 MIME 类型的资源;支持采用 gzip、deflate 或 sdch 压缩过的资源;可以接受 zh-CN、en-US 和 en 三种语言,并且 zh-CN 的权重最高(q 取值 0 - 1,最高为 1,最低为 0,默认为 1),服务端应该优先返回语言等于 zh-CN 的版本。

更多

Angular装饰器的应用

发表于2015年11月04日 分类: js 标签: angular angular.js js

angular.js官方网站上有关于decorator的文档,点击这里查看。干的事情很简单,就是注册一个service的装饰器,支持依赖注入;通过他允许我们去覆盖或者修改service的原本功能。

项目中有一个这样的场景,需要路由发生改变,但是呢,改变之后的路由和当前的路由除了url不同,其他的都是相同的,也就意味着controller是一样的,得到的模板内容是一样的。在这种场景下,希望的是路由会发生变化,但是不希望ng-view的元素不发生变化。你可能会说,直接用search或者hash来更改地址多好,再配合reloadOnSearch:false不就万事大吉了吗,事实是不符合老板要求。。。所以这里需要一种方案来解决这个问题。

既然只需要对ng-view部分做修改,他是一个指令,而指令在angular中其实也是按照service处理的。对于ng-view指令而言,其实就是通过$provide.factory注册了一个服务名字为ngViewDirective,由此想到能不能修改下这个指令定义的默认逻辑呢?答案是肯定的,通过decorator就可以做到。

这个需求可以换个说法,也就是在路由定义的时候,多指定一个参数state,值是一个字符串名字,在发生改变的时候,判断下当前的这个值和上次的值是否相同,且顺便判断下templatecontroller是否一样;如果都为true的话,就认为不需要更新ng-view元素了。

所以说下面就是实现部分了:

angular.module('ngRoute').decorator('ngViewDirective', ['$route', '$delegate', function ($route, $delegate) {
  // 保留原link函数
  var oldL = $delegate[0].link;
  // 修改compile值
  $delegate[0].compile = function () {
    var lastR = null;
    var el;
    // 新的link
    return function (scope, $element, attr, ctrl, $transclude) {
      // 代理的$transclude函数
      var trs = function (scope, cb) {
        var current = $route.current;
        if (lastR && current && lastR.state &&
            lastR.state === current.state &&
            lastR.controller === current.controller &&
            lastR.locals.$template === current.locals.$template) {
          // 返回保留的el
          return el;
        }
        lastR = current;
        el = $transclude(scope, cb);
        return el;
      };
      trs.$$boundTransclude = $transclude.$$boundTransclude;
      // 调用原始的link
      oldL(scope, $element, attr, ctrl, trs);
    }
  };
  // 把原service实例返回
  return $delegate;
}]);

注意上边的一些细节:

  1. 为什么是直接修改的$delegate[0].compile的值,而不是直接修改其link值呢?最开始的时候我也是这样想的,但是发现无效,为什么呢?后来想到了在指令处理过程中不会去判断link,而是去判断的compile,也就是说在angular的指令处理中,如果你写了link而没有写compile的话,angular会给你造一个compile,然后返回你定义的link

  2. 可以看到核心是把调用link时的最后一个参数$transclude给代理代理掉了,然后在其中根据上次的route信息和当前的route信息来判断是否需要调用真正的$transclude得到新的内容。

  3. 判断模板是否相同的时候,采用的是locals.$template,这是为了处理$templateUrl的情况,得到$templateUrl的模板内容后会给locals.$template赋值。

这样就实现了需求,有state且上次的route信息和当前的route信息中的state是相同的,然后再加上controller$template的判断就可以了。

使用的话很简单:

$routeProvider
  .when('/a', {
    template: 'a',
    controller: 'aa',
    state: 'a'
  })
  .when('/b', {
    template: 'a',
    controller: 'aa',
    state: 'a'
  })
  .when('/c', {
    template: 'c',
    controller: 'cc'
  })
  .otherwise('/a')

上边的配置,默认路由是/a,假设现在路由更新为/b,那么由于/a/b的路由配置中state相同都是a,且template和controller都是相同的,所以说此时不会创建新的ng-view,当然controller也不会重新实例化;然后路由更新到/c,一切回归正常的默认行为,内容发生更新。

decorator还是很有用的,把它用在合适的场景(虽然这个场景是不大合理的)会提供一种解决问题的新思路!

更多

从输入url到页面加载完的过程中都发生了什么事情

发表于2015年11月03日 分类: server 标签: browser http dns cdn net server

这其实是一个经典的面试题了,都可以自由发挥各个方面,说出自己的理解,而且涉及的面也是巨多,就看怎么看待了。本篇就可以说是我对于这个问题的一些综合、总结;其中不会涉及深层次(硬件啊等)的一些东西,例如说按下按键发生了什么事情,关于这些更基础更深入的建议可以看下百度的文章从输入 URL 到页面加载完成的过程中都发生了什么事情?

首先来看第一个点,输入的URL。

URL

URL,英文是Uniform / Universal Resource Locator,中文的翻译就是统一资源定位符,俗称网页地址,简短的说法是网址,用于完整地描述Internet上网页和其他资源的地址的一种标识方法。它从左到右由如下部分构成:

  • 传送协议protocol:最常用的是HTTP协议(超文本传输协议),它也是目前WWW中应用最广的协议;其他也还有ftpfilehttps、、mailtogit等,当然也有自定义的协议(私有协议),例如tencent

  • 主机host:通常为域名或者IP地址,当然在其前面还可以有连接到服务器所需的用户名和密码

  • 端口号port:以数字形式表示,每种协议都有自己默认的端口号,例如http协议的默认端口号就是80,https的默认端口号就是443等

  • 路径path:以“/”字元区别路径中的每一个目录名称,一般表示的就是主机上的一个目录或文件地址

  • 查询query:以“?”字元为起点,每个参数以“&”隔开,再以“=”分开参数名称与其对应的值

  • 片段fragment:也就是在浏览器环境下location的hash值,用于指定网络资源中的片断,一般用于定位到某个位置

参考:

介绍了URL,下边直说最简单的通过浏览器发起HTTP请求资源过程,没有代理,通过域名访问的情况。根据http://blog.csdn.net/iaiti/article/details/28339145中相关介绍,将要发生如下事情:

  1. 浏览器查询缓存,如果缓存存在跳到第9步

  2. 浏览器询问操作系统服务器的IP地址

  3. 操作系统做DNS查询,返回IP地址给浏览器

  4. 浏览器打开对服务器的TCP连接

  5. 浏览器通过TCP连接发送HTTP请求

  6. 浏览器接收HTTP响应并且可能关掉TCP连接,或者是重新使用连接处理新请求(也就是keepalive)

  7. 浏览器检查HTTP响应是否为一个重定向(3xx 结果状态码 ),一个验证请求(401),错误(4xx 5xx)等等,这些都是不同响应的正常处理(2xx)

  8. 如果响应可缓存,将存入缓存

  9. 浏览器解码响应(例如:如果它是gziped压缩)

  10. 浏览器决定如何处理这些响应(例如,它是HTML页面,一张图片,一段音乐)

  11. 浏览器展现响应,对未知类型还会弹出下载对话框(现在一般不会弹出了,用户对浏览器设置而定)

下边就来说下这个过程中一些关键点。

更多

Angular.js源码分析之ngrepeat

发表于2015年11月03日 分类: js 标签: angular angular.js 源码分析 angular源码分析 js

在angular中一旦涉及到列表类的往往就轮到ng-repeat这个指令出马了,其实就是循环数组或者对象,然后渲染得到内容。核心就是利用$transclude,他可以被调用多次,而且会自动创建单独的scope,用$transclude来做再好不过了。

首先来看下其源码:

var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) {
  var NG_REMOVED = '$$NG_REMOVED';
  var ngRepeatMinErr = minErr('ngRepeat');

  var updateScope = function(scope, index, valueIdentifier, value, keyIdentifier, key, arrayLength) {
    // 增加额外的一些属性值
    scope[valueIdentifier] = value;
    if (keyIdentifier) scope[keyIdentifier] = key;
    scope.$index = index;
    scope.$first = (index === 0);
    scope.$last = (index === (arrayLength - 1));
    scope.$middle = !(scope.$first || scope.$last);
    // jshint bitwise: false
    scope.$odd = !(scope.$even = (index&1) === 0);
    // jshint bitwise: true
  };

  var getBlockStart = function(block) {
    return block.clone[0];
  };

  var getBlockEnd = function(block) {
    return block.clone[block.clone.length - 1];
  };

  return {
    restrict: 'A',
    multiElement: true, // 跨域多个元素可以
    transclude: 'element',
    priority: 1000,
    terminal: true,
    $$tlb: true,
    compile: function ngRepeatCompile($element, $attr) {
      // ngRepeat的表达式
      var expression = $attr.ngRepeat;
      var ngRepeatEndComment = document.createComment(' end ngRepeat: ' + expression + ' ');
      
      // 省略

      return function ngRepeatLink($scope, $element, $attr, ctrl, $transclude) {
        
        if (trackByExpGetter) {
          trackByIdExpFn = function(key, value, index) {
            // assign key, value, and $index to the locals so that they can be used in hash functions
            if (keyIdentifier) hashFnLocals[keyIdentifier] = key;
            hashFnLocals[valueIdentifier] = value;
            hashFnLocals.$index = index;
            return trackByExpGetter($scope, hashFnLocals);
          };
        }

        var lastBlockMap = createMap();

        // 监控集合属性变化
        $scope.$watchCollection(rhs, function ngRepeatAction(collection) {
          var index, length,
              previousNode = $element[0],     // 应该插入到previousNode节点之后
              nextBlockMap = createMap(),
              collectionLength,
              key, value, // key/value of iteration
              trackById,
              trackByIdFn,
              collectionKeys,
              block,       // last object information {scope, element, id}
              nextBlockOrder,
              elementsToRemove;

          if (aliasAs) {
            // 如果有 as 语法
            $scope[aliasAs] = collection;
          }

          if (isArrayLike(collection)) {
            // 类似于数组
            collectionKeys = collection;
            trackByIdFn = trackByIdExpFn || trackByIdArrayFn;
          } else {
            // 对象 但是要得到key组成的数组
            trackByIdFn = trackByIdExpFn || trackByIdObjFn;
            // if object, extract keys, in enumeration order, unsorted
            collectionKeys = [];
            for (var itemKey in collection) {
              if (hasOwnProperty.call(collection, itemKey) && itemKey.charAt(0) !== '$') {
                collectionKeys.push(itemKey);
              }
            }
          }

          collectionLength = collectionKeys.length;
          nextBlockOrder = new Array(collectionLength);

          // 定位现有的那些项
          for (index = 0; index < collectionLength; index++) {
            key = (collection === collectionKeys) ? index : collectionKeys[index];
            value = collection[key];
            trackById = trackByIdFn(key, value, index);
            if (lastBlockMap[trackById]) {
              // 如果存在就利用
              block = lastBlockMap[trackById];
              delete lastBlockMap[trackById];
              nextBlockMap[trackById] = block;
              nextBlockOrder[index] = block;
            } else if (nextBlockMap[trackById]) {
              // 现在的里边存在 说明 key 存在重复 报错
              forEach(nextBlockOrder, function(block) {
                if (block && block.scope) lastBlockMap[block.id] = block;
              });
              throw ngRepeatMinErr('dupes',
                  "Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}, Duplicate value: {2}",
                  expression, trackById, value);
            } else {
              // 全新的项
              nextBlockOrder[index] = {id: trackById, scope: undefined, clone: undefined};
              nextBlockMap[trackById] = true;
            }
          }

          // 移除剩余的哪些项 在新的里边不存在
          for (var blockKey in lastBlockMap) {
            block = lastBlockMap[blockKey];
            elementsToRemove = getBlockNodes(block.clone);
            $animate.leave(elementsToRemove);
            if (elementsToRemove[0].parentNode) {
              // if the element was not removed yet because of pending animation, mark it as deleted
              // so that we can ignore it later
              for (index = 0, length = elementsToRemove.length; index < length; index++) {
                elementsToRemove[index][NG_REMOVED] = true;
              }
            }
            // 销毁
            block.scope.$destroy();
          }

          // 循环构建内容
          for (index = 0; index < collectionLength; index++) {
            key = (collection === collectionKeys) ? index : collectionKeys[index];
            value = collection[key];
            block = nextBlockOrder[index];

            if (block.scope) {
              // 以前就存在的

              nextNode = previousNode;

              // skip nodes that are already pending removal via leave animation
              do {
                nextNode = nextNode.nextSibling;
              } while (nextNode && nextNode[NG_REMOVED]);

              if (getBlockStart(block) != nextNode) {
                // 移除掉
                $animate.move(getBlockNodes(block.clone), null, jqLite(previousNode));
              }
              previousNode = getBlockEnd(block);
              // 更新scope
              updateScope(block.scope, index, valueIdentifier, value, keyIdentifier, key, collectionLength);
            } else {
              // 新的项 通过$transclude得到新的copy内容
              $transclude(function ngRepeatTransclude(clone, scope) {
                block.scope = scope;
                // http://jsperf.com/clone-vs-createcomment
                var endNode = ngRepeatEndComment.cloneNode(false);
                clone[clone.length++] = endNode;

                // 插入元素
                $animate.enter(clone, null, jqLite(previousNode));
                previousNode = endNode;
                // 保留clone节点
                block.clone = clone;
                nextBlockMap[block.id] = block;
                // 更新scope
                updateScope(block.scope, index, valueIdentifier, value, keyIdentifier, key, collectionLength);
              });
            }
          }
          lastBlockMap = nextBlockMap;
        });
      };
    }
  };
}];

可以看出上边的基本过程,得到repeat的表达式,然后进行简单的解析,之后就返回了link函数;在link函数中主要是watch了集合的变化,发生改变时,如果有新增的项,那么就通过$transclude得到新的clone内容,然后插入即可;如果是删除的话,直接也是移除了;而对于已存在的要去更新对应的scope信息。

核心还是利用$transclude会每次克隆一份原始内容,而且本身会创建scope,所以就最大效用的发挥了其作用。

更多