GithubHelp home page GithubHelp logo

blog's People

Contributors

lihuakkk avatar

Stargazers

 avatar  avatar  avatar

Watchers

 avatar  avatar

blog's Issues

A Deep, Deep, Deep, Deep, Deep Dive into the Angular Compiler (译)

本文并不是原文的完全翻译,只对原文重点部分进行翻译。Angular编译器的文章一共有两篇,这是第一篇关于老的编译器的探索,第二篇是关于新的Ivy编译器的探索。

模版和视图

通常我们使用HTML描述DOM结构,在DOM上绑定数据。当应用启动的时候,AngularJs会为我们的模版生成对应的DOM树,并且将数据填充到DOM中相应的位置。比如,如果模版是 <h1>{{title}}</h1>,AngularJs会执行类似下面的代码(假设当前上下文中,组件控制器实例叫ctrl)。

const h1Element = document.createElement('h1');
h1Element.innerText = ctrl.title;

在AngularJs(最早版本的Angular,versions 1.x)中,HTML的解析和DOM的创建工作都是浏览器来完成的。DOM树创建完成后,AngularJs检查所有的DOM元素,找出里面的指令和所有的插值表达式,然后用实际的数据来替换它们。查看源码

但是AngularJs的这种浏览器解析方法存在下面几个问题。

首先,众多浏览器实现不一致。不同的浏览器解析相同的HTML有时会生成不同的DOM结构example,AngularJs必须要自己解决这些差异。另外,浏览器在解析出错的时候处理不是很好,它首先会尝试修复错误,通过自动闭合标签或者将错误的标签移到正常的标签附近。即使浏览器将错误抛出,通常也不会显示出错的行数。这些问题导致debug的时候很浪费时间。

其次,使用浏览器来解析渲染模版的时候,这在客户端很容易。但是实现服务端渲染会很复杂,而且容易出错。(看这里这里

最后,浏览器解析HTML的标签和属性是不区分大小写的,不仅如此,浏览器还不会为你保存原来的大小写,它会将所有的标签名转为大写,属性转为小写。运行下面的代码印证。

document.createElement('h1').nodeName;

你会得到大写的"H1"。这也是为什么Angular使用指令的时候使用kebab-case的形式(i.e. ng-if, ng-model, etc.) 而不使用camelCase形式。

综上,浏览器解析HTML,会存在不同的浏览器实现不一致,缺少具体错误信息,缺少服务端渲染的支持和缺少属性的大小写等问题。

这也是为什么需要compiler存在。compiler代替浏览器为我们解析HTML,使不同浏览器解析的结果一致,同时它也可以在服务端运行(毕竟它只是一段纯JS代码),提供详细的错误信息,保存标签和属性的大小写。

Angular Compiler是一个不可思议的工作。它有超过1MB的代码量,是Angular团队超过一年的工作成果。它在为帮我们解析模版的同时还生成了高效的代码,这些代码使用最小的CPU和内存来生成和更新DOM。

image

Compiler的目标(一值都是)是更小的内存占用,更快的页面加载速度和更高效的变更检测,参考Generating Less Code

package.json文件里添加下面这行代码:

"scripts": {
  ...,
  "compile": "ngc"
}

运行

"scripts": {
    npm run compile
}

几秒之后,在工程目录下会生成很多文件,app.component.html文件被转成app.component.ngfactory.ts,app.module.ts文件被转成app.module.ngfactory.ts。

组件(视图生成和变更检测)

Angular compiler将我们3行的HTML模版转换成了app.component.ngfactory.ts,如果你打开这个文件,你会看到很多难以理解的代码,这些代码给机器运行的代码不适合阅读。需要一点耐心和反向推测的技,还好,TypeScript这个时候会帮很大忙。

image

首先我们会看到很多不好理解的方法名,这些方法名以希腊字母Theta开头后面跟着3个英语字母(e.g. Ɵvid)。这个希腊字母ɵ 被Angular团队用来表示框架的私有方法,用户不应用直接调用,这些方法在不同的Angular版本中很有可能发生变化。

至于为什么使用3个字母简写而不是使用方法的全名,这只是简单的为了减少最后bundle file 的大小。

修改app.component.html文件,然后编译运行 npm run compile,查看编译后对应的文件,比如做如下修改。

<h1>Hi, {{title + title}}</h1>

编译后的结果如下:
image

主要变化是一个名为View_AppComponent_0的方法。这个方法由两部分组成:第一部分定义视图,包括元素,属性和文本等,第二个部分是变更检测。这个形式可以提高Angular执行效率,第一部分只运行一次,第二部分只有在Angular执行变更检测的时候执行。

这部分内容跳过了关于样式处理的部分,如果你感兴趣请参考这个video

模块化

Angular应用使用模块的方式将组件和服务关联起来。组件通过模块导出和注入,编译器查看每个模块确保每个组件之间的引用正常。跟AngularJs不同的是,管道和组件不是全局可用的。只有在模块内声明过或者在引入的模块内声明过的,在当前模块内才是可用的。这种方式在大型应用里可以避免命名冲突的问题。

接下来让我们探索下Angular是怎么实现依赖注入的。你可能会猜测存在一个对象或者Map用来用来匹配类名或者token。当然AngularJs使用对象来实现这个功能,缺点是对象的索引必须是字符串。
Angular中,我们不仅可以使用字符串做依赖注入的token,还可以使用类和对象。这是怎么实现的呢?

打开app.module.ngfactory.ts文件,我们会看到一个很长的getInternal()方法,这就是Angular实现依赖注入的方法。当看到一系列的if语句的时候,我最初的想法是他们这么做可能是出于性能考虑,这种情况下if语句可能是最高效的方法。
在我问Angular开发团队之后才知道真正的原因。为了更方便的移除没有用到的代码,编译器检测会将没有用到的服务从最终的bundle文件中移除。

image

一旦有if语句匹配了,会执行里面的getter方法,返回一个对应的服务实例。这样看来,依赖注入就是一系列的if语句的集合。

image

编译后的代码会帮我们处理服务之间存在的依赖。在初始化当前服务之前,先找到依赖的服务,然后把它们以参数的形式传到当前服务的构造函数里:

image

更多资料:
ng-conf 2017 talk about the Angular 4.0 Compiler
ng-conf 2017: DiY Angular Compiler

产品概念与编程

产品是由一个一个概念组成的,类似于面向对象编程里面的对象,不同的是概念更加抽象。

产品概念与最小表达力原则

之前看最小表达力原则的时候理解就是将代码划分为最小的单元,现在回顾看,代码(应用)的最小单元应该是概念,将概念细分为最小单元,然后将其用代码表达出来。这是我理解的最小表达力原则。

先说一个例子:

在开发 IM 功能的一个需求,当有新消息来的时候会有一下情况:

1、正在跟目标聊天的时候,消息面板滚动到底部,不新增未读数,不提新新消息通知
2、查看历史记录的时候,消息面板不滚动到底部,新增新消息未读数,提示新消息
3、在跟别人聊天的时候,新增新消息未读数,提示新消息

之前开发的处理,用了很多分支判断,不具有可维护性,阅读体验极差。分析需求,最终导致的用户看到的结果:

  • 是否消息面板滚动到底部
  • 是否有未读数
  • 是否有新消息通知

这三个状态是否变化只取决于一个条件,是否在跟目标处于聊天状态。
聊天态,这个就是我们抽象出来的一个概念。通常情况下产品会直接告诉你这个概念,但是当产品没有跟我们将出来这个概念的时候,需要我们自己去提炼概念。

聊天态 = 聊天对象是目标 + 聊天面板在底部(没有在查看历史消息)

提炼概念的好处不仅可以提高编程能力,写出来的代码也会很简洁,可维护性好,可扩展性好。

比如新增一个需求,当用户鼠标焦点不在当前窗口也要提示新消息。

聊天状态 = 当前窗口获取焦点 + 聊天对象是目标 + 聊天面板在底部(没有在查看历史消息)

只需要改动一行代码就可以满足需求。

代码实现:

const safeDistance = 180;

function isFocus() {
  return document.hasFocus();
}

function ifChatting(chatId, element) {
  if (!element) return false;
  // 聊天对象是目标
  const isCurrentChatItem = currentChatId === chatId;
  // 聊天面板在底部或者距离底部只有一点距离
  const { scrollHeight, clientHeight, scrollTop } = element;
  const isSafeDistance = scrollHeight === clientHeight || scrollHeight - clientHeight - scrollTop < safeDistance;
  // isFocus 当前窗口获取焦点
  return isFocus() && isCurrentChatItem && isSafeDistance;
}

const chatting = ifChatting(chatId, scrollElement);
if (chatting || isSelfSend) {
  // 新增未读
  addReadMask()
  // 新消息提示
  newNotifatication()
  // 滚动到底部
  scrollBottom()
}

最后

提炼概念还有一个好处,当你发现一个概念很别扭或者实现很复杂的时候,需要重点思考,是自己的实现有问题还是产品概念本身不合逻辑。
如果是概念本身不合理,就要及时跟产品沟通找出问题所在。

Zone.js基础概念及其在Angular中的应用

什么是Zone.js?

Zone.js是Angular团队在开发ng2的时候引入的用来监听异步事件的一个库,使用monkey-patches的方式重写了所有浏览器的异步api。Zone.js本身不依赖ng2,在任何框架或非框架应用里面都可以使用。ng2里面通过扩展Zone.js为NgZone,这是ng2的一个核心模块。

Zone.js可以用来做什么?

Zone.js为我们提供了一系列的Hook。这些Hook可以帮我们拦截异步操作的调度和回调,在异步操作发生前和结束后执行额外的代码。最常见的例子,异步操作结束后通知框架刷新,和在每个异步操作前添加记录可以在出错的时候取回以获取更加准确的错误信息。
Zone.js还可以在不同的异步任务里面共享数据。

Zone.js是怎么调度异步api的?

简单来说,一个回调使用的异步api,该api被调度的时候,任务被捕获,然后将任务存在当前active的Zone里。调度函数写在该异步api被monkey-patches的地方,查看patchTimer
下面列举了三种类型的可调度的任务:

  1. [MicroTask] 当前任务结束后立即执行,不可取消的任务。
  2. [MacroTask] 一段时间后执行,可取消任务。
  3. [EventTask] 事件监听任务,可执行0或多次。

怎么使用Zone.js?

库加载完成后会生产一个特殊的Root Zone,它的存在对于用户来说是无感的,所有的Zone都是它的后代。

通常我们通过fork函数来生成一个新的Zone,该函数需要一个描述Zone基本信息对象作为参数,具体请看ZoneSpec。通过run方法,将函数在该Zone运行。

const childZone = Zone.current.fork(ZoneSpec);
childZone.run(Fn)

zoneSpec可以继承吗?

有如下代码:

const parentZoneSpec = {
  name: 'parentZone',
  onInvokeTask(parentZoneDelegate, currentZone, targetZone, task) {
    console.log('parentZoneSpec, onInvokeTask');
    return parentZoneDelegate.invokeTask(targetZone, task);
  },
  onFork(parentZoneDelegate, currentZone, targetZone, zoneSpec) {
    console.log('parentZone onFork');
    return parentZoneDelegate.fork(targetZone, zoneSpec)
  }
}

const childZoneSpec = {
  name: 'childZone',
  onInvoke(parentZoneDelegate, currentZone, targetZone, delegate, applyThis, applyArgs, source) {
    console.log('childZone > onInvoke', arguments);
    return parentZoneDelegate.invoke(targetZone, delegate, applyThis, applyArgs, source);
  },
  onFork(parentZoneDelegate, currentZone, targetZone, zoneSpec) {
    console.log('childZoneSpec onFork');
    return parentZoneDelegate.fork(targetZone, zoneSpec)
  },
  onInvokeTask(parentZoneDelegate, currentZone, targetZone, task) {
    console.log('childZoneSpec, onInvokeTask');
    return parentZoneDelegate.invokeTask(targetZone, task);
  },
}

const grandSonZoneSpec = {
  name: 'grandSonZone',
}

function settime() {
  console.log('settime Schedule')
  setTimeout(function() {
    console.log('settime callback')
  }, 1000)
}

const grandSonZone = Zone.current.fork(parentZoneSpec).fork(childZoneSpec);
grandSonZone.run(settime)

稍微解释下:
我们通过zoneSpec指定的部分hook函数的参数里面的currentZone表示hook定义的Zone,targetZone表示函数调用发生的Zone,这一点类似Dom Event里面的currentTargettarget的区别。

上面的代码的执行结果如下:

parentZone onFork
childZone > onInvoke
normal function
childZoneSpec, onInvokeTask
parentZoneSpec, onInvokeTask
settime

结果表明如果parent的Spec定义了hook,那么不管child有没有定义,parent的hook都会执行。它们不存在继承关系,更像是执行了遍历操作。

通过onFork这个Hook为引子,查看源码里的实现,另外,在源码里面以ZS结尾的表示对象的类型是ZoneSpec。Dlgt结尾的通常表示对象的类型是ZoneDelegate。

this._forkZS = zoneSpec && (zoneSpec && zoneSpec.onFork ? zoneSpec : parentDelegate._forkZS);
 ...
ZoneDelegate.prototype.fork = function (targetZone, zoneSpec) {
  return this._forkZS ? this._forkZS.onFork(this._forkDlgt, this.zone, targetZone, zoneSpec) :
  new Zone(targetZone, zoneSpec);
};

可以发现如果zoneSpec.onFork不存在就调用parentDelegate._forkZS,直到Root Zone。

Ng in NgZone

目前为止我们对Zone.js的基础概念和api有了一些了解,接下来让我们看下NgZone是怎么扩展Zone.js来触发Angular Change Detect。
Angular虽然引入了Zone.js,但是只是用它对异步事件的检测监听来触发UI更新。

离开了NgZone,Angular仍然可以正常工作,只不过需要我们手动触发Change Detect。

NgZone通过forkInnerZoneWithAngularBehavior这个方法的fork了一个名为"angular"的实例,并且将其赋给了zone._inner,所有的Angular相关的代码都在这个实例里面运行。于此对应的是zone._outer,在这个实例里面可以运行独立于Angular这个框架的代码,不会触发Change Detect。

接下来让我们看下NgZone如何通知Angular执行Change Detect。链接

this._zone.onMicrotaskEmpty.subscribe({next: () => { this._zone.run(() => { this.tick(); }); }});

这是Angular运行Change Detect的代码,通过订阅onMicrotaskEmpty事件启动更新。发布事件的代码如下:

源码链接

function checkStable(zone: NgZonePrivate) {
  if (zone._nesting == 0 && !zone.hasPendingMicrotasks && !zone.isStable) {
    try {
      zone._nesting++;
      zone.onMicrotaskEmpty.emit(null);
    } finally {
      zone._nesting--;
      if (!zone.hasPendingMicrotasks) {
        try {
          zone.runOutsideAngular(() => zone.onStable.emit(null));
        } finally {
          zone.isStable = true;
        }
      }
    }
  }
}

解释下判断条件:
有任务进来的时候执行onEnter,zone._nesting++,任务结束执行onLeave,zone._nesting—。
存在嵌套任务的时候会起作用,所有任务执行完 zone._nesting == 0 成立。

zone.hasPendingMicrotasks等于hasTaskState.microTask,hasTaskState这是Zone。js的一个借口,用来统计当前任务数。
zone.hasPendingMicrotasks在onHasTask这个Hook里面更新,zone.hasPendingMicrotasks = hasTaskState.microTaskhasTaskState这是Zone.js的一个接口,用来统计各个类型任务状态。

zone.isStable用于判断系统是否处于稳定状态, 从源码可以看到,当有任务进来的时候isStable=false,当执行完changeDetach后isStable=true。

未完待续...

websocket 封装

websocket 封装

需要 npm i pubsub-js --save

功能:

  • 服务器连接
  • 服务器登录
  • 心跳保活
  • 离线检测
  • 断线重连
  • 重连通知
  • 消息处理

    支持promise回调,支持重试,超时处理等

  • 异常日志
  • 支持同一个用户只有一个设备在线

用例:

// 消息promise封装
import { sendMessage } from 'message/index';

sendMessage({
  // pn2RequestPatch.js rules定义
  operation: 'request_offline_message',
  data: {
    /* ... */
  }
})
  .then(() => {})
  .catch(() => {})

vue-router 路由匹配机制

分析目标,router-view 如何正确渲染对应的组件。

路由初始化

根据我们自定义的路由规则,初始化路由项,通过遍历递归的方式建立路由索引 pathMap,nameMap,pathList 和父子路由之间的引用关系(子路由有 parent 属性,指向父路由)

// 省略一些细节,只关注核心代码逻辑
function addRouteRecord (
  pathList: Array<string>,
  pathMap: Dictionary<RouteRecord>,
  nameMap: Dictionary<RouteRecord>,
  route: RouteConfig,
  parent?: RouteRecord,
  matchAs?: string
) {
  const { path, name } = route
    // ...省略
  const record: RouteRecord = {
    // ...省略
    path: normalizedPath,
    components: route.components || { default: route.component },
    name,
    parent,
    // ...省略
  }
  ...
  if (route.children) {
    // ...省略
    route.children.forEach(child => {
      const childMatchAs = matchAs
        ? cleanPath(`${matchAs}/${child.path}`)
        : undefined
      addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
    })
  }
  // ...省略
}

路由匹配

路由切换的时候,都是通过 match 函数找到目标路由。

  /**
   * 可以看到这里调用_createRoute返回对应的路由项
   * 省略一些细节,只关注核心代码逻辑
   * */
  function match (
    raw: RawLocation,
    currentRoute?: Route,
    redirectedFrom?: Location
  ): Route {
    // ...省略
    const location = normalizeLocation(raw, currentRoute, false, router)
    const { name } = location
    if (name) {
      const record = nameMap[name]
      // ...省略
      return _createRoute(record, location, redirectedFrom)
    }
    // ...省略
  }

  /**
   * 调用真正的生成路由项的函数createRoute
   * */
   function _createRoute (
    record: ?RouteRecord,
    location: Location,
    redirectedFrom?: Location
  ): Route {
    // ...省略
    return createRoute(record, location, redirectedFrom, router)
  }

   /**
   * 路由项里面有一个属性matched,是由formatMatch格式化record之后返回的,后面router-view里面将用到这个* 属性来展示对应的组件
   * */
  export function createRoute (
  record: ?RouteRecord,
  location: Location,
  redirectedFrom?: ?Location,
  router?: VueRouter
): Route {
  const stringifyQuery = router && router.options.stringifyQuery
  // ...省略
  const route: Route = {
    // ...省略
    name: location.name || (record && record.name),
    params: location.params || {},
    matched: record ? formatMatch(record) : [] //
    // ...省略
  }

  return Object.freeze(route)
}

  /**
  * 根据初始化的时候建立的父子关系,返回matched的路由及其父级
  * */
function formatMatch (record: ?RouteRecord): Array<RouteRecord> {
  const res = []
  while (record) {
    res.unshift(record)
    record = record.parent
  }
  return res
}

路由展示

router-view 组件核心**之一就是找到 router-view 之间嵌套的深度

/**
 * depth表示当前组件的层级
 * */
while (parent && parent._routerRoot !== parent) {
  const vnodeData = parent.$vnode ? parent.$vnode.data : {};
  if (vnodeData.routerView) {
    depth++;
  }
  if (vnodeData.keepAlive && parent._directInactive && parent._inactive) {
    inactive = true;
  }
  parent = parent.$parent;
}

// ...省略
/**
 * 结合之前我们分析matched的定义和depth的概念,可以得到当前router-view需要渲染的组件
 * */
const matched = route.matched[depth];
const component = matched && matched.components[name];

分治策略

分治策略通过将一个复杂的大问题分解成很多类似的小问题,然后由无数小问题的解得到最终的大问题的解。一般的解题步骤是列出状态转移方程和临界条件。

Q: Binary Watch (problemId: 401)

一个二进制手表,上面有 4 个 LED 灯表示小时,下面有 6 个 LED 灯表示分钟。

image

图中显示的时间是"3:25"

给一个非负整数 n,n 表示当前亮着的灯数,求可以表示的时间有那些。

Example 1:

Input: n = 1;
Return: [
  "1:00",
  "2:00",
  "4:00",
  "8:00",
  "0:01",
  "0:02",
  "0:04",
  "0:08",
  "0:16",
  "0:32"
];

Note:

  • 输出顺序无关
  • 小时表示不以 0 开头,01:00 -> 1:00
  • 个位数分钟需要 0 前缀,10:2 -> 10:02

PS: 针对这个问题,下面的解法不是最合适的,只是当时我使用着下面的方法求解。

解法:

var readBinaryWatch = function(num) {
  if (num === 0) return ["0:00"];
  function zuHe(array, n) {
    if (array.length === n) {
      return [array.reduce((sum, a) => sum + a)];
    }
    if (n === 1) {
      return array.concat();
    }
    let _temp = array.concat();
    const char = _temp.shift();
    return zuHe(_temp, n - 1)
      .map(item => char + item)
      .concat(zuHe(_temp, n));
  }
  const hourMap = {
    0: ["0"]
  };
  const minMap = {
    0: ["00"]
  };
  function initMap(obj, array, max, prefix) {
    let len = array.length;
    while (len > 0) {
      obj[len] = zuHe(array.concat(), len)
        .filter(item => item < max)
        .map(item => (item < 10 ? prefix + item : String(item)));
      len--;
    }
  }
  initMap(hourMap, [1, 2, 4, 8], 12, "");
  initMap(minMap, [1, 2, 4, 8, 16, 32], 60, "0");

  const result = [];
  let _num = 0;
  while (_num <= num) {
    const _hourArray = hourMap[_num];
    if (_hourArray && _hourArray.length > 0) {
      const _minArray = minMap[num - _num];
      if (_minArray && _minArray.length > 0) {
        _hourArray.forEach(_hour => {
          _minArray.forEach(_min => result.push(_hour + ":" + _min));
        });
      }
    }
    _num++;
  }
  return result;
};

解释:

/**
这种解法是将所有的灯的情况列举出来。
核心方法是zuHe这个函数,这个函数对无序排列组合的一个实现。
array表示元素,n表示从array中取几个元素进行组合。
例:array = ['a', 'b', 'c', 'd', 'e'], n = 3;
第一步:移除第一个元素'a',在剩下的元素里面['b', 'c', 'd', 'e']中取(n-1)个进行组合,将组合结果每个加上'a'
第二部:移除第一个元素'a',在剩下的元素里面['b', 'c', 'd', 'e']中取n个进行组合
当n=1的时候返回当前的array
当array.length === n,返回array里面每个元素相加的结果。
状态转移函数:
first = array.shift()
array2 = array

zuHe(array, n) = (first + zuHe(array2, n - 1)) + zuHe(array2, n);
if(n = 1) return array;
if(array.length === n) return array[0]+array[1]+...array[n-1]

*/

function zuHe(array, n) {
  if (array.length === n) {
    return [array.reduce((sum, a) => sum + a)];
  }
  if (n === 1) {
    return array.concat();
  }
  let _temp = array.concat();
  const char = _temp.shift();
  return zuHe(_temp, n - 1)
    .map(item => char + item)
    .concat(zuHe(_temp, n));
}

Q: Median of Two Sorted Arrays (problemId: 4)

给两个已经排序的数组nums1和nums2,对应的长度是m和n。
找到这两个数组的中位数。
你可以认为nums1和nums2不可同时为空。

Example 1:

nums1 = [1, 3]
nums2 = [2]
The median is 2.0

Example 2:

nums1 = [1, 2]
nums2 = [3, 4]
The median is (2 + 3)/2 = 2.5

解法:

var findMedianSortedArrays = function(nums1, nums2) {
    function findIndex(arr, num, offset) {
      const length = arr.length;
      const first = arr[0];
      const last = arr[length - 1];
      const midIndex = Math.floor(length / 2);
      const mid = arr[midIndex];
      if(num <= first) {
        return offset + 0;
      }
      if(num >= last) {
        return offset + length;
      }
      if(num === mid) {
        return offset + midIndex;
      }
      if(num > mid) {
        return findIndex(arr.splice(midIndex), num, offset + midIndex);
      } else {
        return findIndex(arr.splice(0, midIndex), num, offset);
      }

    }
    function concatSortedArrays(array1, array2) {
      const len1 = array1.length;
      const len2 = array2.length;
      let dist, sour;
      if(len1 < len2) {
        dist = array2;
        sour = array1;
      } else {
        dist = array1;
        sour = array2;
      }
      for(let i = 0; i < sour.length; i++) {
        const num = sour[i];
        const insertIndex = findIndex(dist.concat(), num, 0);
        dist.splice(insertIndex, 0, num);
      }
      return dist;
    }
    const array = concatSortedArrays(nums1, nums2);
    const length = array.length;
    if(length % 2 === 0) {
      return (array[length / 2 - 1] + array[length / 2]) / 2 
    } else {
      return array[Math.floor(length/2)];
    }
};

解释:

/**
上面的解法只是我想这么做,受业务影响更倾向于找到一个通用的解法。
先将两个数组合并,然后找到中位数。
核心是findIndex这个函数,利用的二分法的**查找插入Index。
*/

Redux源码分析 —— compose实现

Redux源码中的compose函数分析

关于Redux源码和**分析已经有很多很好的文章,在这里只讨论其中的一个工具函数,compose。Redux ApplyMiddleware通过compose函数将中间件串联起来。

compose的定义

将多个单参数的函数从右到左串联在一起,前一个函数的返回值做后一个函数的参数,最右边的函数可以有多个参数。

compose(f, g, h) = (...args) => f(g(h(...args)))

compose的实现

源码中compose的实现非常简洁,核心代码更是只有一行。

export default function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }
  
  if (funcs.length === 1) {
    return funcs[0]
  } 
  
  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

要理解上面的代码,首先我们要知道compose函数的一些特点:

compose(f, g, h, j) = compose(compose(f, g), h, j);
compose(f, g, h, j) = compose(f, compose(g, h, j));

源码中的实现是利用第一个等式,每次取最前面的两个函数compose,返回一个新的函数跟下一个函数compose。

也可以使用第二个等式的**,先将除了第一个函数之外的所有函数compose,返回的新函数再跟第一个函数compose,实现如下:

function compose2(...funcs) {
  const [a, ...funcs2] = funcs;
  
  if(funcs.length === 0) {
    return arg => arg;
  }
  
  if(funcs.length === 1) {
    return funcs[0];
  }
  
  if(funcs.length === 2) {
    const [b] = funcs2;
    return (...args) => a(b(...args));
  }
  
  return compose2(...[a, compose2(...funcs2)])
}
    

如果有更好的实现,欢迎讨论

THE END

移动端布局前置知识

由来

最初的目的是为了搞清楚1css px 与 1 device px的关系,以及为什么显示相同尺寸(视觉上)的图片,Retina屏需要更高的分辨率和尺寸。

先说结论:

  1. 同一个设备上1 device px的大小是不变的。
  2. 1 css px的大小跟当前页面的缩放比例有关。

device pixels 和 CSS pixels

首先要知道device pixels 和 css pixels是不同的概念,对于同一个设备device pixels是不变的,它决定着这个设备的分辨率。

接下来分析CSS pixels。
假设我们的显示器是1024px宽,然后我们有一个元素width: 128px,此时这个元素占显示器的1/8宽。浏览器缩放至200%,这个时候元素占显示器的1/4。设备像素没有发生改变,而css 像素却占了更多的设备像素。
现代浏览器对缩放的处理只是对css像素的拉伸。也就是说元素的宽度并没有从128px变到256px,虽然元素占的空间确实是原来的两倍。从形式上看,元素还是128px宽,只不过占个256设备像素。

下面几个图片将更加清楚的说明这个问题:
浅蓝代表css px,深蓝表示device px。

100% 缩放。1css px = 1 device px

image

缩小,css px 收缩,一个设备像素大于1个css px

image

放大,css px放大,1个css px 大于一个device px

image

通过上面的图片可以知道,在100%缩放的情况下有:
1 css px = 1 device px

layout viewport,visual viewport,ideal viewport

因为移动端屏幕的宽度远比桌面屏幕的尺寸要小,同一个网站如果想要在移动端使用,除了针对移动端开发一个新的页面,就只能缩放原页面到适应移动端屏幕的大小。
在不指定meta viewport标签的情况下,浏览器会帮我们将页面缩放到适合显示的尺寸。
浏览器是怎么做到的呢?

移动端浏览器将viewport分为两个visual viewport 和 layout viewport,(后面还有一个ideal viewport),所有viewport的单位都是css px。
layout viewport用来计算页面布局,visual viewport用来显示完成布局的页面。

前面说浏览器layout viewport用来计算页面布局,layout viewport的宽度是怎么确定的呢?
不同的浏览器对layout viewport有一个默认的宽度,Safari iPhone使用980px,Android WebKit 800px。我们还可以通过meta viewport标签指定layout viewport的宽度,这个接下来会细说。通过document.documentElement.clientWidth可以获取layout viewport的宽。

移动端浏览器在使用默认layout viewport宽度的时候,初始页面显示会将页面缩放至完全适合屏幕的尺寸。
此时visual viewport = layout viewport

当我们手动缩放页面的时候layout viewport是不会变的,但是我们看到的区域发生了改变,所以visual viewport的宽度是会变的。
以上的讨论是当移动端跟pc端共用一个页面的时候,大多数情况下我们会为移动端单独开发一个页面,接下来我们就讨论这个case。
这种情况下,显然浏览器默认的layout viewport的宽度是不合适的,我们需要为它单独设置值,我们需要详细了解meta viewport。

meta viewport的语法如下:

<meta name="viewport" content="name=value,name=value">

一共有下面这些指令可选:

  • width 设置layout viewport的宽度(数字或者device-width
  • initial-scale 设置页面的初始缩放比例和layout viewport的宽。
  • minimum-scale 设置页面的最小缩放比例。
  • maximum-scale 设置页面的最大缩放比例。
  • user-scalable 将这个设为no阻止用户手动缩放页面。

所有scale指令都是以ideal viewport为基础计算的,跟layout viewport无关。所以maximum-scale=3的意思是最大缩放比例是ideal viewport的300%。

什么是ideal viewport?

没有使用retina屏幕的老的设备的 ideal viewport的尺寸等于设备的物理像素数。那些有更高物理像素密度的设备的ideal viewport可能跟老的设备的ideal viewport一致,因为这个尺寸对于设备来说真的很理想。
一直到5S,iPhone的理想ideal viewport都是320px,不管是不是使用了retina屏幕,因为320px对这些iPhone设备是对适合页面显示的尺寸。
同一个设备的ideal viewport是不变的。

页面布局用的是layout viewport,那么ideal viewport有什么用呢?
上面说了ideal viewport是最适合移动端布局的尺寸。

<meta name="viewport" content="width=device-width,initial-scale=1">

这段代码可以将layout viewport = ideal viewport再为移动端设备开发页面的时候很有用。

影响Layout viewport width的因素总结如下:
当页面不存在viewport meta标签的时候,浏览器使用自己默认的尺寸,大部分是980px。
当存在viewport meta标签的时候,如果单独存在width,或者单独存在initial-scale的时候取各自的计算值。
initial-scale的计算方法:ideal viewport/initialScale。
当两者同时存在的时候,取两者之间的大的那个值。
除了我们设置的 minimum-scale 和 maximum-scale外,浏览器存在最小layout viewport和最大layout viewport。

PS:最佳的图片显示是一个图片像素对应一个设备像素这样的显示方式,可以最大限度的还原图像也不会浪费尺寸。

参考资料:
A tale of two viewports — part one
A tale of two viewports — part two
Meta viewport

两种方法提升Angular2 ChangeDetection的效率

ChangeDetection的效率决定了一个框架的效率。除了框架本身的实现,开发者使用的一些方法也关系着ChangeDetection的效率。

Change Detector相关知识

Angular中每一个组件都有自己的Change Detector,应用的组件树也对应着一个Change Detector树。
Angular的Change Detection总是从上到下执行。
Angular提供了两种ChangeDetectionStrategy:
一种是Default策略,这个是默认值,当Angular收到需要更新UI的通知,所有的组件都执行自己的Change Detector。
一种是OnPush策略,当Angular收到需要更新UI的通知,只有在组件Input属性的引用发生变化的时候执行自己的Change Detector。
如果一个组件的采用了OnPush更新策略,那么它的子组件将会继承这个更新策略。

使用下面的代码启用OnPush策略:

@Component({
  ...
  changeDetection: ChangeDetectionStrategy.OnPush
})

immutable data VS Observables

在采用OnPush策略的前提下,我们可以使用下面两种方法提高ChangeDetection的效率。

方法一:使用不可变数据结构( immutable data structure)
这个方法需要额外引入一个库immutable-js

var vData = someAPIForImmutables.create({
              name: 'Pascal Precht'
            });
var vData2 = vData.set('name', 'Christoph Burgdorf');
vData1 === vData2 // false

只要有变化就会生成一个新的对象。
将组件的input属性全设置为immutable对象,只要input改变组件就会执行自己的Change Detector,反之就不执行。
这样子应用在执行Change Detect的时候只要一个组件的引用没有发生改变,那以这个组件为根的整个子树都会跳过Change Detect。

image

方法二:使用 Observables
Observables对象的引用是不会发生改变的。所以整个组件树都不会执行Change Detect。但是我们可以通过订阅数据流,然后通过markForCheck函数来标记需要执行ChangeDetect的组件路径(从root组件到当前组件的路径)。

示例如下:

constructor(private cd: ChangeDetectorRef) {}
ngOnInit() {
  this.dataStream.subscribe(() => {
    this.counter++; // application state changed
    this.cd.markForCheck(); // marks path
  })
}}

使用markForCheck标记前

image

使用markForCheck标记后

image

开始执行change detection

image

参考资料:
ANGULAR CHANGE DETECTION EXPLAINED

关于抽象

起因

做开发这几年,初期可以明显感觉到提升,中期慢慢感觉一切没有那么新鲜,没有了最初的激情。直到开始有意识的用抽象的概念去看事物,发现了更大的世界,也是这个时候开始真正做到前端框架无关。

从开始接触编程到现在一直记得的一句话是:程序 = 算法 + 数据结构
算法还是那个算法,但是随着编程经验的增加,对数据结构有了不同的理解。

针对一个问题,后期算法可以不断的优化,数据结构一旦确定了,后期再修改需要付出很多精力,可能还需要跨部门合作。同一个问题不同角度的理解对应着不同的数据结构模型,好的数据模型可以使应用层次清晰,易维护,易扩展。在将对问题的理解转换成对应的合理的数据结构模型的过程,是对问题的抽象

对一个应用来说除去算法,剩下的部分就是应用的抽象,我认为应用 = 算法 + 抽象。抽象无处不在,从使用的框架到具体的场景,我们有意无意中都在使用抽象的概念。

对一个框架来说,以Angular为例,从DOM角度它将一个应用抽象成组件树,从逻辑的角度,它将应用抽象为controller + service + filter + directive这几个大的方面。从AngularJs到Angular2,它的抽象层次和概念从来没有变过,Angular2使用typscript和模块组织代码,使代码更容易做到按需。AngularJs里面也有组件,只是Angular2强制必须使用组件来组织应用的DOM结构,但是对组件的抽象层次都是一样的,区别是Angular2的组件抽象更具体。从抽象的角度看,AngularJs和Angular2还是一脉相承的,只是写法变了,加入了更多的更优秀的概念。

具体到场景,当接到迭代任务的时候,将任务拆解,那些是自身的逻辑,那些是需要与其他模块交互的,将其按照合理的方式组织起来,并且开发过程使用的各种设计模式(比如:工厂模式,单例模式等),这也是一种抽象。在我们重构代码的时候,有一些复用的代码,合理的复用也是在抽象,因为复用的是代码包含的逻辑,而不是代码本身。

个人抽象的基本原则:
合理的分层设计
底层抽象
避免过度抽象
尽量降低副作用的影响

深入分析Angularjs中指令link执行的顺序

起因

问题来自开发过程中要实现一个复杂拖拽功能,因为使用的框架是angularJs,所以使用angularjs-dragula插件。

先说结论:
AngularJs 中指令 link 的顺序并不是严格从上到下,深度优先,ng-repeat 存在的时候会影响指令 link 执行的顺序。

因为ngRepeat里面使用$scope.$watchCollection来监听变量和更新元素。
$watchCollection里面调用的是$watch,完整的调用链如下:

$watchCollection => $watch => $evalAsync => $browser.defer => setTimeout

所以如果 ngRepeat 里面包含其他的指令,这些指令将会放在 nextTick 在执行。

简单介绍下 angularjs-dragula 这个指令:

function link(scope, elem, attrs) {
  var dragulaScope = scope.dragulaScope || scope.$parent;
  var container = elem[0];
  var name = scope.$eval(attrs.dragula);
  var drake;

  var bag = dragulaService.find(dragulaScope, name);
  if (bag) {
    drake = bag.drake;
    drake.containers.push(container);
  } else {
    drake = dragula({
      containers: [container]
    });
    dragulaService.add(dragulaScope, name, drake);
  }

  scope.$watch("dragulaModel", function(newValue, oldValue) {
    if (!newValue) {
      return;
    }

    if (drake.models) {
      var modelIndex = oldValue ? drake.models.indexOf(oldValue) : -1;
      if (modelIndex >= 0) {
        drake.models.splice(modelIndex, 1, newValue);
      } else {
        drake.models.push(newValue);
      }
    } else {
      drake.models = [newValue];
    }

    dragulaService.handleModels(dragulaScope, drake);
  });
}

drake.containers是一个数组,代表可被拖拽的 element,在指令link执行的时候添加元素。
drake.models是一个数组,代表可被拖拽的 element 的数据模型,在指令scope.$watch触发的时候添加新的元素。
scope.$watch第一次执行是在scope.$digest的时候执行,此时不管有没有数据发生变化。
正常情况下这两个数组里面的元素是一一对应的。

到现在其实已经很明了,指令 link 执行的顺序会受到 ng-repeat 的影响,但是 scope.$digest()的顺序不会受影响,严格按照从上到下,深度优先执行。

只针对插件来说,拖拽双方都是在 ng-repeat 里面也不会有问题,有问题的是一方是在 ng-repeat 里面,另一个不再。
解决方式:将不在 ng-repeat 指令里面的元素,放在 ng-if 里面,ng-if 会改变指令 link 函数执行的顺序。

axios 封装

axios 封装 源码地址

功能:

  • 超时报错
  • 网络错误,请求重试
  • 请求合并

用例:

import { packedRequestFactroy } from 'packRequest';

function request(ids) {
  return fetch(url, {
    data: ids
  });
}

const fetchAvatar = packedRequestFactroy(request);

export { fetchAvatar };

移动端自适应布局-rem方案

布局宽度

布局宽度即layout width,单位是px。布局宽度是rem自适应方案的基础,根据不同的设备计算出不同布局宽度,是自适应方案的核心。
计算布局宽度前,还需要知道一个相关概念,ideal viewport,没有使用retina屏幕的老的设备的ideal viewport的尺寸等于设备的物理像素数。那些有更高物理像素密度的设备的ideal viewport可能跟老的设备的ideal viewport一致,即物理像素数翻倍但是ideal viewport尺寸不变。

layout width的确定由meta标签viewport属性确定。

<!-- 一个设置meta viewport的例子 -->
<meta
  name="viewport"
  content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>

参数解释:

  • width 设置 layout viewport 的宽度(数字或者 device-width)
  • initial-scale. 设置页面的初始缩放比例和 layout viewport 的宽
  • minimum-scale.设置页面的最小缩放比例
  • maximum-scale. 设置页面的最大缩放比例
  • user-scalable.将这个设为 no 阻止用户手动缩放页面

所有scale指令都是以ideal viewport为基础计算的,跟layout viewport无关。

更详细的内容参考之前的一篇移动端布局前置知识

根据 layout width 计算出根 html 节点字体大小。比如设计稿是按照 750px 设计的,通常我们将页面划分为 10 等分,即 1rem = 75px,这是我们开发时候的基准。这个时候我们根 html 节点字体大小应该为 layout width/10
,这是页面展示的时候使用的基准。

图片模糊问题

知道了问题才能解决它,这里有人总结的很好,可以直接参考。
知乎关于移动端适配,你必须要知道的第九条,作者conard。

案例分析

以iphone6为例,devicePixelRatio = 2,ideal viewport = 375。根据flexible.js计算得到initial-scale=0.5,此时layout viewport = 750(375/0.5)
设计稿上width=100px的图片,对应到布局宽度上还是100px,只不过占用的物理尺寸是50px(为了方便直接用px,这里页面不缩放,px大小固定),相当于压缩了一倍,像素点跟设备像素比例1:1,所以可以高清展示。

附flexible.js源码

// flexible.js 源码
(function(win, lib) {
  var doc = win.document;
  var docEl = doc.documentElement;
  var metaEl = doc.querySelector('meta[name="viewport"]');
  var flexibleEl = doc.querySelector('meta[name="flexible"]');
  var dpr = 0;
  var scale = 0;
  var tid;
  var flexible = lib.flexible || (lib.flexible = {});

  if (metaEl) {
    var match = metaEl.getAttribute('content').match(/initial\-scale=([\d\.]+)/);
    if (match) {
      scale = parseFloat(match[1]);
      dpr = parseInt(1 / scale);
    }
  } else if (flexibleEl) {
    var content = flexibleEl.getAttribute('content');
    if (content) {
      var initialDpr = content.match(/initial\-dpr=([\d\.]+)/);
      var maximumDpr = content.match(/maximum\-dpr=([\d\.]+)/);
      if (initialDpr) {
        dpr = parseFloat(initialDpr[1]);
        scale = parseFloat((1 / dpr).toFixed(2));
      }
      if (maximumDpr) {
        dpr = parseFloat(maximumDpr[1]);
        scale = parseFloat((1 / dpr).toFixed(2));
      }
    }
  }
  if (!dpr && !scale) {
    var isAndroid = win.navigator.appVersion.match(/android/gi);
    var isIPhone = win.navigator.appVersion.match(/iphone/gi);
    var devicePixelRatio = win.devicePixelRatio;
    if (isIPhone) {
      // iOS下,对于2和3的屏,用2倍的方案,其余的用1倍方案
      if (devicePixelRatio >= 3 && (!dpr || dpr >= 3)) {
        dpr = 3;
      } else if (devicePixelRatio >= 2 && (!dpr || dpr >= 2)) {
        dpr = 2;
      } else {
        dpr = 1;
      }
    } else {
      // 其他设备下,仍旧使用1倍的方案
      dpr = 1;
    }
    scale = 1 / dpr;
  }
  docEl.setAttribute('data-dpr', dpr);
  if (!metaEl) {
    metaEl = doc.createElement('meta');
    metaEl.setAttribute('name', 'viewport');
    metaEl.setAttribute(
      'content',
      'initial-scale=' +
        scale +
        ', maximum-scale=' +
        scale +
        ', minimum-scale=' +
        scale +
        ', user-scalable=no'
    );
    if (docEl.firstElementChild) {
      docEl.firstElementChild.appendChild(metaEl);
    } else {
      var wrap = doc.createElement('div');
      wrap.appendChild(metaEl);
      doc.write(wrap.innerHTML);
    }
  }
  function refreshRem() {
    var width = docEl.getBoundingClientRect().width;
    if (width / dpr > 540) {
      width = 540 * dpr;
    }
    var rem = width / 10;
    docEl.style.fontSize = rem + 'px';
    flexible.rem = win.rem = rem;
  }
  win.addEventListener(
    'resize',
    function() {
      clearTimeout(tid);
      tid = setTimeout(refreshRem, 300);
    },
    false
  );
  win.addEventListener(
    'pageshow',
    function(e) {
      if (e.persisted) {
        clearTimeout(tid);
        tid = setTimeout(refreshRem, 300);
      }
    },
    false
  );
  if (doc.readyState === 'complete') {
    doc.body.style.fontSize = 12 * dpr + 'px';
  } else {
    doc.addEventListener(
      'DOMContentLoaded',
      function(e) {
        doc.body.style.fontSize = 12 * dpr + 'px';
      },
      false
    );
  }

  refreshRem();
  flexible.dpr = win.dpr = dpr;
  flexible.refreshRem = refreshRem;
  flexible.rem2px = function(d) {
    var val = parseFloat(d) * this.rem;
    if (typeof d === 'string' && d.match(/rem$/)) {
      val += 'px';
    }
    return val;
  };
  flexible.px2rem = function(d) {
    var val = parseFloat(d) / this.rem;
    if (typeof d === 'string' && d.match(/px$/)) {
      val += 'rem';
    }
    return val;
  };
})(window, window['lib'] || (window['lib'] = {}));

Array.reduce

Array.reduce

MDN定义 arr.reduce(callback( accumulator, currentValue[, index[, array]] )[, initialValue])
这是我非常喜欢的一个函数,个人理解用作状态转移, 可以代替一些递归操作,使用好了可以事半功倍。

累加

或许是最常用的方式

const array1 = [1, 2, 3, 4];
const reducer = (accumulator, currentValue) => accumulator + currentValue;

// 1 + 2 + 3 + 4
console.log(array1.reduce(reducer));
// expected output: 10

根据路径获取树形结构中的节点

vuex中构建状态树通过path获取节点内容,源码

function getNestedState (state, path) {
  return path.reduce((state, key) => state[key], state)
}

const state = {a: {b: {c: {d: 'd'}}}} 
const path = ['a', 'b', 'c']
getNestedState(state, path) 
// => { d: 'd' }

函数串行执行

redux中间件核心函数,中间件串行执行 源码

export default function compose(...funcs: Function[]) {
  if (funcs.length === 0) {
    // infer the argument type so it is usable in inference down the line
    return <T>(arg: T) => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  return funcs.reduce((a, b) => (...args: any) => a(b(...args)))
}

promise 串行

串行执行promise,参考

function runPromiseByQueue(promises) {
  promises.reduce(
    (currentPromise, nextPromise) => currentPromise.then(() => nextPromise()),
    Promise.resolve()
  );
}

AWS S3 上传封装

aws s3 上传封装

npm i aws-sdk spark-md5 --save

功能:

  • 单/多文件上传
  • 上传取消
  • 视频/图片缩略图上传
  • 上传进度
  • 上传速度
  • 上传失败回调
  • 上传成功回调

用例:

import uploadFileFactory from 'index';
/*
* fileList Array[formatFile]
* formatFile = {
  file: File, // 本地文件
  name,
  type,  // PICTURE | VIDEO | DOC 
  size,
  fileId,
  key,
  bucket,
  uploadStatus: 'wait', // 'wait' | 'uploading' | 'finished' | 'failed'
}
**/
const uploadCancel = uploadFileFactory(
  fileList,
  function onprogress(data) {
    // progress && speed
    console.log('progress', data);
  },
  function onerror(error) {
    console.log(error);
  },
  function onend() {
    console.log('end');
  }
);

uploadCancel(); // 取消上传

逻辑思维

训练逻辑思维能力,可以帮助我们编程的时候思考更全面,减少因为疏忽导致的不必要的 bug。

Q: Nth Digit (problemId: 400)

找到无穷整数队列 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, ...中的第 n 个数。

Note:
n 是正整数而且 n < Math.pow(2, 31)

Example 1:

Input: 3;
Output: 3;

Example 2:

Input: 11
Output: 0
Explanation: 在队列1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, ... 第11个数字是10中的第二个数0。

解法:

var findNthDigit = function(n) {
  if (n < 10) return n;
  let temp;
  function getN(index) {
    let sum = 0,
      n = 1;
    while (sum < index) {
      temp = sum;
      sum += 9 * Math.pow(10, n - 1) * n;
      n++;
    }
    return n - 1;
  }
  const N = getN(n);
  const _index = Math.ceil((n - temp) / N) - 1;
  const num = Math.pow(10, N - 1) + _index;
  let y = (n - temp) % N;
  if (y === 0) {
    y = N;
  }
  return ("" + num).charAt(y - 1);
};

解释:

先找到第n个数所在的数字,然后在找到对应的位置。

Q: Maximum Subarray (problemId: 53)

给一个整数数组,找出一个连续子串(至少包括一个数字)满足字串和在所有字串中最大,并返回这个字串的和。

Example 1:

Input: [-2,1,-3,4,-1,2,1,-5,4]
Output: 6
Explanation: [4,-1,2,1] has the largest sum = 6.

解法:

var maxSubArray = function(nums) {
  const len = nums.length;
  let lastSum;
  let max = nums[0];
  if (max < 0) {
    lastSum = 0;
  } else {
    lastSum = max;
  }
  for (let i = 1; i < len; i++) {
    const value = nums[i];
    lastSum += value;
    if (lastSum > max) {
      max = lastSum;
    }
    if (lastSum < 0) {
      lastSum = 0;
    }
  }
  return max;
};

解释:

max表示当前最大的字串和,lastSum用作探针查找比当前更大字串和。

团队协作

带领团队近一年的时间,虽然人少,但都是经验丰富的开发,从一个人到现在的四个,关于协作开发还是有一点自己的理解

  • 开发规范和流程提前规范好
  • 信息透明,跟每个成员同步项目现状,存在的问题及原因,以及项目未来的规划
  • 每个成员对项目的模块都要熟悉,即使不是自己开发的
  • 关键模块实现要开技术方案会,每个成员都要参与讨论,一旦确定方案尽量不要更改
  • 关键模块文档自觉维护
  • 关注成员发展,尽可能做到能力相差不大
  • 提交前互相review
  • 至少每月一次会议,吐槽或者交流遇到的问题

重构实践

准备工作

重构前准备工作,为重构提供依据。可以向产品提要求,或者上一个开发留下来的文档。尽可能多跟产品,后端,测试沟通,他们反馈的问题可以做以后架构设计时考虑的因素。

  • 整理产品文档,技术文档和接口文档
  • 熟悉应用,多使用,记录使用过程中发现的问题
  • 跟测试沟通,记录测试反馈的问题
  • 跟产品沟通,尽量详细了解项目的历史和存在的问题
  • 跟后端沟通,他们最了解项目逻辑实现,收集他们反馈的问题
  • 列出主要模块及模块之间交互的接口

阶段重构

  • 根据上一步的产品文档和反馈的问题,设计并给出当前你认为合理的架构或解决方案。不要着急开始去做,先完善方案。
  • 从一个bug或者小的功能或UI改动切入项目,先熟悉原作者的思路以及编程风格
  • 以一个完整的功能为单位,当对这个功能涉及的所有case可以掌握的时候,可以这个功能进行重构

    每个功能重构前,先完善文档和流程图。

  • 迭代过程中,涉及到的模块,可以跟随迭代重构。因为通常来说不会给到完整的时间来把整个应用给重构掉,一般都是跟随迭代。

重构技巧

  • 重构**是解耦,低内聚高耦合
  • 超过350行的文件要重点关注,根据功能可以拆分为多个文件
  • 项目中用到的技术,使用业界推荐的方案
  • 重点关注异常处理,对于一个应用来说,主流程重要,异常处理和超时处理也很重要
  • 移除不在使用的代码,或者过期的注释

重构的好处

  • 完善产品文档和技术文档
  • 用户体验优化
  • 对产品的梳理,找出之前设计不合理的地方,帮助产品做的更好

项目实践

godap是一款IM+文件分享的应用,分移动端,web端,客户端。web端是基于Vue开发的,客户端是Electron打包的web端。

接手之初,项目存在的问题,内存泄漏、初始化流程不明确,接口设计不合理,UI风格不统一,文件上传卡住,消息发送接收不准确,页面卡顿等一系列问题。

godap的重构基本上是跟随迭代或者依据bug进行的。

初始化流程不明确

这个问题的表现之一,新消息没有通知。
应用启动之后,websocket消息服务器连接成功,但是联系人列表还没有返回,别人给你发消息缺少消息通知。
解决方式梳理初始化流程,画出流程图和时序图,严格按照应用初始化时序来。

应用初始化的改造还包括添加资源加载页避免白屏,增加初始化loading页,给用户一个友好的过渡等等

内存泄漏

项目使用Vue开发,在组件跟模块函数交互的时候把组件实例当作参数穿到模块函数里。这样导致Vue实例销毁的时候,其占用的内存得不到释放,随着应用的使用到这内存泄漏越来越多。

解决方法,浏览代码的时候发现原开发这样做的原因是使用store或者router。所以直接在对应的模块文件里面引入Store或者Router代替。因为Store和Router都是单例,不存在内存问题。
因为存在大量这样的函数,在处理这一块的时候用了很多时间,同时把里面的一些大文件拆分成多个小文件,每个文件里面只有1到3个函数,为后续重构做准备。

消息展示顺序问题

先收到message1,再收到message2,自己发送message3,三个消息展示顺序跟接收到的时间不一致。造成这个问题的原因是,只要有消息过来,根据时间戳对消息排序。接收到的消息是服务器时间,自己发送的消息是本地时间。当本地时间和服务器时间有偏差的时候就回导致消息展示顺序问题。而且这样子也会造成很多额外排序计算。

解决方法,重新梳理消息处理逻辑,加入同步服务器时间戳逻辑,历史消息根据服务器时间排序,同步消息和本地发送的消息直接加到消息队列底部。历史消息过来的时候使用二分法先找到插入的位置,减少计算量。

请求响应回调时机不确定

因为一些原因,加群退群是通过websocket跟服务器进行通信,依靠websocket回调返回结果,请求跟处理分散在不同的文件,没有可维护性且不可靠。这样的问题,网络好的时候可能不会关注,网络情况差的时候体验就会很糟糕。

解决方法,封装websocket针对一些请求特殊处理返回promise,并且加入超时处理。请求跟处理在一个代码块,增强可维护性和可靠性

文件上传卡住,上传进度展示不对

原因s3配置不对,导致异常回调没有执行。另外,存在多处上传逻辑,代码冗余严重,复用性差。

重新配置s3 config,封装上传模块删除冗余代码,失败后增加重试逻辑。

反思不足

经过半年多的重构和迭代,godap已经可以作为一个成熟的应用在市场推广,但仍然存在很多不足。

  • 部分设计和文档没有及时记录
  • 前期架构设计没有随着重构的进行没有及时跟上
  • 产品文档没有及时同步更新

工具函数

插入新元素

/**
 * 二分法获取在有序数组插入新元素的位置
 * @param { sortedArray } 有序数组,可以为对象
 * @param { targetValue } 要插入的元素
 * @param { mapFn } 转换函数
 * @returns number 插入位置
 * @example
 *
 * getInsertPositionInSortedArray([{a:1}, {a:3}, 2, (item) => item.a])
 * // => 1
 * getInsertPositionInSortedArray([1, 3], 2)
 * // => 1
 */

function getInsertPositionInSortedArray(sortedArray = [], targetValue = 0, mapFn) {
  if (sortedArray.length === 0) return 0;
  if (!mapFn) {
    mapFn = value => value;
  }

  function findIndex(searchingArray, offset) {
    const length = searchingArray.length;
    const first = mapFn(searchingArray[0]);
    const last = mapFn(searchingArray[length - 1]);
    const midIndex = Math.floor(length / 2);
    const mid = mapFn(searchingArray[midIndex]);
    if (targetValue <= first) {
      return offset + 0;
    }
    if (targetValue >= last) {
      return offset + length;
    }
    if (targetValue === mid) {
      return offset + midIndex;
    }
    if (targetValue > mid) {
      return findIndex(searchingArray.splice(midIndex), offset + midIndex);
    } else {
      return findIndex(searchingArray.splice(0, midIndex), offset);
    }
  }

  return findIndex(sortedArray, 0)
}

全角转半角

/**
 * 全角输入转半角,通常用于搜索输入格式化
 * @param { str } 全角字符串
 * @returns number 半角字符串
 * @example
 *
 * ToCDB(`e`)
 * // => 'e'
 */

function ToCDB(str) {
  var tmp = "";
  for (var i = 0; i < str.length; i++) {
    if (str.charCodeAt(i) == 12288) {
      tmp += String.fromCharCode(str.charCodeAt(i) - 12256);
      continue;
    }
    if (str.charCodeAt(i) > 65280 && str.charCodeAt(i) < 65375) {
      tmp += String.fromCharCode(str.charCodeAt(i) - 65248);
    } else {
      tmp += String.fromCharCode(str.charCodeAt(i));
    }
  }
  return tmp
}

首字母获取

/**
 * 获取对应的大写首字母,拼音排序时使用
 * @param { str } 字符
 * @returns str
 * @example
 *
 * getFristLetterOfCharacter('L')
 * // => 'L'
 * getFristLetterOfCharacter('l')
 * // => 'L'
 * getFristLetterOfCharacter('李')
 * // => 'L'
 * getFristLetterOfCharacter('3')
 * // => '#'
 */

function getFristLetterOfCharacter(str = '') {
  const character = str[0];
  const letters = 'ABCDEFGHJKLMNOPQRSTWXYZ';
  const specialLetter = '#';
  let firstLetter = specialLetter;

  // if (/[^\u4e00-\u9fa5]/.test(character)) {
  if (/[a-zA-Z]/.test(character)) {
    try {
      firstLetter = character.toUpperCase();
    } catch (error) {
      console.log('getFristLetterOfCharacter', firstLetter);
    }
  } else if (/[0-9]/.test(character)) {
    firstLetter = specialLetter;
  } else {
    const boundaryChar = '驁簿錯鵽樲鰒餜靃攟鬠纙鞪黁漚曝裠鶸蜶籜鶩鑂韻糳';
    try {
      if (!String.prototype.localeCompare) {
        throw Error('String.prototype.localeCompare not supported.');
      }
      for (let i = 0; i < boundaryChar.length; i++) {
        if (boundaryChar[i].localeCompare(character, 'zh-CN-u-co-pinyin') >= 0) {
          firstLetter = letters[i];
          break;
        }
      }
    } catch (e) {
      console.log('getFristLetterOfCharacter', e);
    }
  }
  return firstLetter;
}

首字母排序

/**
 * 根据字符的首字母进行排序
 * @param { sortArray } 要排序的数组
 * @returns `{"A":[],"B":[],"C":[],"D":[],"E":[],"F":[],"G":[],"H":[],"I":[],"J":[],"K":[],"L":["李","l"],"M":[],"N":[],"O":[],"P":[],"Q":[],"R":[],"S":["s"],"T":[],"U":[],"V":[],"W":[],"X":[],"Y":[],"Z":[],"#":[]}`
 * @example
 *
 * sortByFirstLetter(['l','s','李'])
 * // => `{"A":[],"B":[],"C":[],"D":[],"E":[],"F":[],"G":[],"H":[],"I":[],"J":[],"K":[],"L":["李","l"],"M":[],"N":[],"O":[],"P":[],"Q":[],"R":[],"S":["s"],"T":[],"U":[],"V":[],"W":[],"X":[],"Y":[],"Z":[],"#":[]}`
 */

function sortByFirstLetter(sortArray = [], mapFn) {
  const _sortArray = sortArray.concat([]);
  if (!mapFn) {
    mapFn = value => value;
  }

  if (!typeof mapFn === 'function') {
    return _sortArray;
  }

  const sortMap = {
    // '#': []
  };
  const [charCodeStart, charCodeEnd] = ['A'.charCodeAt(), 'Z'.charCodeAt()];
  // init sortMap
  for (let code = charCodeStart; code <= charCodeEnd; code++) {
    sortMap[String.fromCharCode(code)] = [];
  }

  sortMap['#'] = [];

  _sortArray.forEach(item => {
    const firstLetter = getFristLetterOfCharacter(mapFn(item));
    sortMap[firstLetter].push(item);
  });

  //排序
  for (const firstLetter in sortMap) {
    const list = sortMap[firstLetter];
    if (list.length != 0) {
      list.sort((a, b) => mapFn(a).localeCompare(mapFn(b), 'zh-CN-u-co-pinyin'));
    }
  }

  return sortMap;
}

操作系统检测

/**
 * 检测当前操作系统
 * @returns windows | mac | linux | web
*/

function detectOs() {
  let ua = navigator.userAgent
  let isWin = (navigator.platform == "Win32") || (navigator.platform == "Windows")
  let isMac = (navigator.platform == "Mac68K") || (navigator.platform == "MacPPC") || (navigator.platform == "Macintosh") || (navigator.platform == "MacIntel")
  let isUnix = (navigator.platform == "X11") && !isWin && !isMac
  if (isWin) return 'windows'
  if (isMac) return 'mac'
  if (isUnix) return 'linux'
  return 'web'
}

服务器时间同步

/**
 * 检测当前操作系统的时间与系统时间偏移
*/

let offset = 0;

function getLocalTime() {
  return Date.now()
}

// 默认请求往返耗时相同
// 接口响应速度越快,修正的时间越准
// 允许存在0.5s的时间错位
function syncServeTimestamp() {
  const obj = {
    requestTime: getLocalTime()
  }
  return getServeTime()
    .then(res => {
      obj.responseTime = getLocalTime()
      obj.serverTime = res.data.data;
      const trafficSpend = (obj.responseTime - obj.requestTime) / 2;
      const currentTime = obj.serverTime - trafficSpend; // 请求发出去的时候服务器时间
      offset = Math.floor((currentTime - obj.requestTime));
      if (Math.abs(offset) <= 500) {
        offset = 0
      }
      console.log(`比服务器时间${offset > 0 ? '慢' : '快' }${Math.abs(offset / 1000)}秒`);
    })
    .catch(error => {
      console.log('get serveTime error', error);
      throw(error);
    })
}

function getCurrentTime() {
  return Date.now() + offset
}

promise lock

/**
 * 异步函数未结束前再次调用无效
 * @param { asyncFn } 要lock的异步函数
 * @param { identity } 标识,通常时函数名
 * @returns function
 * @example
 * 
 * fn = promisePendingLock(asyncFn))
 * fn() // 返回正确promise
 * fn() // 返回error pendingLocked
 */

export function promisePendingLock(asyncFn, identity) {
  identity = identity || asyncFn.name;
  let asyncFnStatus = 'wait';
  return (...params) => {
    if (asyncFnStatus === 'wait') {
      asyncFnStatus = 'execute';
      return asyncFn(...params)
        .then(r => {
          asyncFnStatus = 'wait';
          return r;
        })
        .catch(e => {
          asyncFnStatus = 'wait';
          throw(e)
        });
    }
    asyncFnStatus = 'pendingLocked';
    return Promise.reject('pendingLocked')
  };
}

节流

/**
 * lodash 节流函数
 */

export function throttle(func, wait) {
  let ctx, args, rtn, timeoutID; // caching
  let last = 0;

  return function throttled() {
    ctx = this;
    args = arguments;
    let delta = new Date() - last;
    if (!timeoutID)
      if (delta >= wait) call();
      else timeoutID = setTimeout(call, wait - delta);
    return rtn;
  };

  function call() {
    timeoutID = 0;
    last = +new Date();
    rtn = func.apply(ctx, args);
    ctx = null;
    args = null;
  }
}

pick

/**
 * 获取对象中指定的key
 * @param { object } 目标对象
 * @param { keys } 数组,指定获取的key
 * @returns object
 * @example
 * 
 * pick({a: '1', b: '2', c: '3'}, ['a', 'c'])
 * // => {a: '1', c: '3'}
 */

function pick(object, keys) {
  return keys.reduce((obj, key) => {
    if (object && object.hasOwnProperty(key)) {
      obj[key] = object[key];
    }
    return obj;
  }, {});
}

邮箱格式

/**
 * 邮箱格式
 */

function checkEmail(email) {
  const reg = /\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*/
  return reg.test(email);
}

前端内存泄漏分析以及在AngularJs中的实践

内存泄漏分析

对于hybrid App来说,应用的内存管理是个很重要的一环,如果存在问题,很容易导致应用卡顿或崩溃。

先说结论

short live objectlong live object 交互的时候(通常是以回调函数的形式),最容易因为开发者的疏忽产生内存泄漏问题。

内存相关的知识

内存在应用中存在的形式可以抽象为具有原始类型(如数字和字符串)和对象(关联数组)的图表,可以表示为一个由多个互连的点组成的图表,如下图所示:
image

内存图从根开始,根可以是浏览器的 window 对象或 Node.js 中的 Global 对象。

对象如何占用内存?

对象通过以两种方式占用内存:

  1. 直接通过对象自身占用。
  2. 通过保持对其他对象的引用隐式占用,这种方式可以阻止这些对象被垃圾回收器(简称 GC)自动处置。所以即使一个小对象也可能通过阻止其他对象被自动垃圾回收进程处理的方式间接地占用大量内存。
那些对象会被回收?

任何无法从根到达的对象都会被 GC 回收。上图中,9,10节点占用的内存将会被回收。

作用域链和闭包

当一个函数被创建的时候,跟这个函数一起被创建的是这个函数的作用域链。作用域链里面包括所有这个函数可以被访问到的数据集合。

当一个闭包函数被创建的时候,这个闭包函数的作用域链也被创建,该作用域链对宿主函数的作用域链是存在引用关系的,所以闭包函数里面可以访问宿主函数的变量。

如果闭包函数如果被宿主函数外的变量引用,这个变量会对宿主函数的作用域链存在一个间接引用。所以这种情况下,即使宿主函数执行结束,它的作用域链里面的变量也不会被回收。要解决这个问题,就要切断闭包函数的引用,通常是将引用闭包函数的变量置为null。

AngularJs中的实践

这是一段在Controller里面的代码:

function Ctrl($interval, $scope) {
  $interval(function() {
      $scope.m = 'mm';
  }, 10 * 1000)
}

$interval的第一个参数是一个回调函数同时也形成了一个闭包,这个闭包保存着对$scope的引用。如果$scope destroy的时候$interval服务未执行cancel方法,闭包函数会对$scope一直保持着引用关系,这时会发生内存泄漏问题。

解决方法:

$scope.$on('$destroy', function() {
  $interval.cancel(intervalId);
})

在看另一段在Controller里面的代码:

UserService.onNameChange(function(newName) {
  $scope.userName = newName;
});

这里使用一个UserService服务用来更新用户信息,同样回调函数形成了一个闭包,跟上面的原因类似,这个闭包对$scope存在引用,如果不断开引用,会阻止系统对$scope及其相关联对象的内存回收。

解决方法:使用$broadcasts,或者使用合适的方式解决内存隐患问题(比如将函数引用设为null)。

总结下上面的例子,编程时应该:

  1. 充分理解框架及其生命周期
  2. short live objectlong live object通过回调函数交互时留意。

参考资料:
内存术语
JavaScript 核心概念之作用域和闭包
Fixing Memory Leaks in AngularJS and other JavaScript Applications

位运算

计算机的本质就是1010,掌握位运算可以帮助我们理解计算机原理。利用位元算的一些特点,对一些题目可以给出更高效的算法。

位操作的资料:MDN 按位操作符

Q: Single Number (problemId: 136)

给一个非空的整数数组,里面的元素只有一个元素出现了一次,其他都是成对出现的,找出那个只出现了一次的元素。

Example 1:

Input: [2,2,1]
Output: 1

Example 2:

Input: [4,1,2,1,2]
Output: 4

解法:

var singleNumber = function(nums) {
    return nums.reduce((result, num) => result ^= num, 0);
};

解释:

a ^ b = b ^ a
(a ^ b) ^ c = a ^ (b ^ c)
a ^ a = 0
a ^ 0 = a
a ^ b ^ b ^ c ^ c = a ^ 0 ^ 0 = a

Q: Sum of Two Integers (problemId: 371)

不使用+和-操作符,计算两个整数之和。

Example 1:

Input: a = 1, b = 2
Output: 3

Example 2:

Input: a = -2, b = 3
Output: 1

解法:

var getSum = function(a, b) {
    return b === 0 ? a : getSum(a^b, (a&b) << 1);
};

解释:

/**
    10101010
  + 01001010

  通过a^b相加对应的比特位,通过a&b获取进位的比特位
*/

localStorage封装

localStorage封装 源码地址

优点:

  • 只需要设置一次key
  • 不需要关心数据类型

用例:

import localName from 'localName';

localName.set('gittttt')
localName.get() // gittttt

localName.remove() 
localName.get() // null

Inside Ivy: Exploring the New Angular Compiler(译)

本文并不是原文的完全翻译,只对原文重点部分进行翻译。主要内容是对新的Ivy编译器的探索,可以参考第一篇关于老的编译器的文章。

Ivy,发音跟罗马数字IIV类似得名,是 Angular 的第三代渲染引擎。
相较于早期的编译器,Ivy是 “tree-shaking friendly,”,它会为我们移除没有用到的代码(即使是Angular自身的功能),缩小打包后的文件体积。Ivy单独编译每个文件,这会减小重新编译所花费的时间。简而言之,Ivy将给我们带来更小的编译后的文件,更快的重新编译的速度和更简单的编译后的代码。

Running Ivy

Ivy目前仍处于开发阶段,你可以在这里查看进度,在Angular 6.x版本,部分功能已经可用,被称作“RendererV3”。

即使Ivy还没有100%开发完成,我们还是可以一窥下编译后的代码长什么样子。
新建一个Angular项目:

ng new ivy-internals

然后在tsconfig.json文件里添加下面的代码启用Ivy编译器:

"angularCompilerOptions": {
  "enableIvy": true
}

然后在新创建的项目目录下执行ngc:

node_modules/.bin/ngc

我们可以在dist/out-tsc目录下查看生成的代码。下面是摘自AppComponent的代码。

<div style="text-align:center">
  <h1>
    Welcome to {{ title }}!
  </h1>
  <img width="300" alt="Angular Logo" src="">
</div>
<h2>Here are some links to help you start: </h2>

生成的代码在dist/out-tsc/src/app/app.component.js这个文件中,摘录如下:

 i0.ɵE(0, "div", _c0);
 i0.ɵE(1, "h1");
 i0.ɵT(2);
 i0.ɵe();
 i0.ɵE(3, "img", _c1);
 i0.ɵe();
 i0.ɵe();
 i0.ɵE(4, "h2");
 i0.ɵT(5, "Here are some links to help you start: ");
 i0.ɵe();

Ivy将组件模版内容编译成JavaScript代码,可以对比老版本编译器生成的代码:

image

显然Ivy生成的代码更加简单!修改组件模版文件 ( src/app/app.component.html ) 然后再次运行编译,观察刚刚做的修改对生成的代码产生的影响。

Understanding the Generated Code

我们梳理下生成的代码,看看每一行的作用,生成文件的第一行:

var i0 = require("@angular/core");

i0表示Angular的核心模块,上面那些函数都是这个模块导出的。
这个希腊字母ɵ 被Angular团队用来表示框架的私有方法,用户不应用直接调用,因为这种方法在不同的Angular版本中很有可能发生变化。
这些方法都是Angular的私有方法,使用VS Code代码提示工具我们可以看到每一个方法的详细信
息。

image

类似,ɵT表示text,ɵe表示elementEnd。有了这些知识,我们可以将生成的代码重写成更加可读的代码。

var core = require("angular/core");
//...
core.elementStart(0, "div", _c0);
core.elementStart(1, "h1");
core.text(2);
core.elementEnd();
core.elementStart(3, "img", _c1);
core.elementEnd();
core.elementEnd();
core.elementStart(4, "h2");
core.text(5, "Here are some links to help you start: ");
core.elementEnd();

上面的代码对应的组件模版如下:

<div style="text-align:center">
  <h1>
    Welcome to {{ title }}!
  </h1>
  <img width="300" alt="Angular Logo" src="">
</div>
<h2>Here are some links to help you start: </h2>

我们很容易就可以发现:
1、每一个HTML开始标签对应core.elementStart()
2、每一个HTML结束标签对应core.elementEnd()
3、每一个文本节点对应一个core.text()
elementStart和text方法的第一个参数是一个数字,每次调用这两个函数,这个数字都会增加。Angular以这些数字为索引将这些生成的元素引用保存在一个数组里面。
elementStart第三个参数是可选参数,这个参数是当前DOM节点的属性集合,我们可以查看_c0的值验证,_c0 = ["style", "text-align:center"]。

目前为止,我们已经初步了解了编译器如何利用编译生产的代码渲染组件模版。这部分代码其实是名为AppComponent.ngComponentDef的属性的一部分。这个属性包括关于组件的所有元数据,比如css选择器,变更检查策略,模版等。

Do-It-Yourself Ivy

了解过被Ivy编译过的代码,我们可以尝试使用Ivy一样的RendererV3 API自己构建组件。
我们要写的代码跟编译器生成的类似,只不过我们会以一种更可读的方式来编写,从一个简单的组件开始。

源文件

import { Component } from '@angular/core';

@Component({
selector: 'manual-component',
template: '<h2>Hello, Component</h2>',
})

export class ManualComponent {
}

编译器提取@component装饰器里的数据,生成组件类对应的静态属性。为了模拟编译过程,我们需要移除
@component装饰器,使用ngComponentDef这个静态属性:

源文件

import * as core from '@angular/core';

export class ManualComponent {
  static ngComponentDef = core.ɵdefineComponent({
    type: ManualComponent,
    selectors: [['manual-component']],
    factory: () => new ManualComponent(),
    template: (rf: core.ɵRenderFlags, ctx: ManualComponent) => {
      // Compiled template goes here
    },
  });
}

调用ɵdefineComponent为组件定义元数据。元数据包括组件的类型(以后依赖注入用),selector(s)是这个组件被其他组件模版使用时的名字,factory返回这个组件的一个新的实例,template函数返回组件模版的定义。template函数渲染组件视图和当组件的属性发生变化时更新UI。我们使用上面遇到的方法ɵE, ɵe and ɵT来编写这个模版。

源文件

template: (rf: core.ɵRenderFlags, ctx: ManualComponent) => {
      core.ɵE(0, 'h2');                 // Open h2 element
      core.ɵT(1, 'Hello, Component');   // Add text
      core.ɵe();                        // Close h2 element
    },

现在我们还没使用template函数提供的rf和ctx参数,别急很快我们就会用到。

Our first manually-crafted app

Angular导出了一个名为ɵrenderComponent的方法,将组件渲染到浏览器。我们只要确保在index.html文件中有一个HTML标签匹配我们的组件选择器<manual-component>,然后将下面的代码添加到源文件的最后一行。

core.ɵrenderComponent(ManualComponent);

现在我们实现了手动编译一个Angular应用,虽然只有16行。

Adding Change Detection

为了让组件可以交互,我们把变更检测(Change Detection)加上。修改组件让用户可以自定义“Hello.”之后的内容。我们需要在组件类中新增一个name属性和一个更新name值的方法。

源文件

export class ManualComponent {
  name = 'Component';

  updateName(newName: string) {
    this.name = newName;
  }
  
  // ...
}

接下来我们修改template函数,用name的值来代替静态的文本。

源文件

template: (rf: core.ɵRenderFlags, ctx: ManualComponent) => {
  if (rf & 1) {   // Create: This runs only on first render
    core.ɵE(0, 'h2'); 
    core.ɵT(1, 'Hello, ');
    core.ɵT(2);   // <-- Placeholder for the name
    core.ɵe();
  }
  if (rf & 2) {   // Update: This runs on every change detection
   core.ɵt(2, ctx.name);  // ctx is our component instance
  }
},

与原来相比多了两个与rf值相关的if语句。Angular用rf参数来表示这个组件是否时第一个创建或者是否需要更新组件内容在一个变更检测期间(第二个触发条件)。
组件初始化渲染的时候,我们创建所有的元素。当变更检测触发的时候,我们只需要更新变化部分的UI。ɵt对应的是Angular导出的textBinding方法。

image

从上面的注释可以看到第一个参数表示元素的索引,第二个是要插入的文本的值。在我们的例子里,我们在第5行生成一个空的索引为2的文本元素,将其作为name属性的占位符,当变更检测执行的时候在第9行更新它的内容。

现在我们看到的仍然是“Hello, Component”。
为了实现真正的交互,我们要添加一个input输入框和updateName()这个事件监听输入事件。

源文件

template: (rf: core.ɵRenderFlags, ctx: ManualComponent) => {
  if (rf & 1) {
    core.ɵE(0, 'h2'); 
    core.ɵT(1, 'Hello, ');
    core.ɵT(2);
    core.ɵe();
    core.ɵT(3, 'Your name: ');
    core.ɵE(4, 'input'); 
    core.ɵL('input', $event => ctx.updateName($event.target.value));
    core.ɵe();
  }
  // ...
},

第9行是绑定事件的地方,ɵL方法为最近定义的元素绑定事件监听。第一个参数是事件类型(这我们这个例子里面是 input,当元素的内容发生改变的时候出触发),第二个参数是一个callback。这个回调函数以DOM Event作为参数,从这里获取目标的值并将其传到组件里面定义的接收函数。
跟下面的HTML代码实现的效果等效。

Your name: <input (input)="updateName($event.target.value)" />

现在,你可以修改输入框的内容,然后你会看到对应的打招呼的内容发生改变。但是,在模版初始化的时候input没有显示对应的值,需要在执行变更检测阶段添加一段代码:

源文件

template: (rf: core.ɵRenderFlags, ctx: ManualComponent) => {
  if (rf & 1) { ... }
  if (rf & 2) {
    core.ɵt(2, ctx.name);
    core.ɵp(4, 'value', ctx.name);
  }
}

实现组件交互的时候,我们引入了另一个方法ɵp,这个方法会根据我们提供的元素索引和新的值更新对应的元素。上面的例子中,元素索引值是4,这是我们定义input元素时的索引,然后将ctx.name的值赋给该元素的value属性。
最终我们使用Ivy rendered API从头开始简单完成了一个数据双向绑定。
目前为止,我们已经熟悉了Ivy生成的一些基础构建代码:知道怎么创建一个元素和文本节点,知道怎么绑定属性和事件监听,知道了怎么处理变更检测。

One More Thing

在本文结束前,在看一下另一个有意思的问题: 编译器怎么处理子模版的?如果模版里面使用了ngIf 或者 ngFor,编译器对它们有不同的处理。接下来,了解下手工编译模版里面使用ngIf。
首先安装@angular/common包,这是
ngIf定义的地方。

import { NgIf } from '@angular/common';

因为NgIf尚未经过编译,为了能在我们的模版里面使用,需要添加一些额外的代码。
使用之前用过的ɵdefineComponent的姊妹方法,ɵdefineDirective这个方法定义指令的元数据:

源文件

(NgIf as any).ngDirectiveDef = core.ɵdefineDirective({
  type: NgIf,
  selectors: [['', 'ngIf', '']],
  factory: () => new NgIf(core.ɵinjectViewContainerRef(), core.ɵinjectTemplateRef()),
  inputs: {ngIf: 'ngIf', ngIfThen: 'ngIfThen', ngIfElse: 'ngIfElse'}
});

这段代码的出处在这里 in Angular’s source code,就在ngFor的定义下面。
现在我们已经正确设置了NgIf指令,可以把它添加进组件的指令列表里面:

源文件

static ngComponentDef = core.ɵdefineComponent({
  directives: [NgIf],
  // ...
});

接下来,我们定义一个使用*ngIf的子模版。
例如我们想要显示一个图片,我们在template函数定义个新的template函数。

源文件

function ifTemplate(rf: core.ɵRenderFlags, ctx: ManualComponent) {
  if (rf & 1) {
    core.ɵE(0, 'div');
    core.ɵE(1, 'img', ['src', 'https://pbs.twimg.com/tweet_video_thumb/C80o289UQAAKIqp.jpg']);
    core.ɵe();
  }
}

这个template函数跟我们之前见到的没什么不同,在div元素里面创建了一个img元素。
把上面的这些代码片段放在一起,同时将ngIf指令添加进组件的template函数代码里面。

源文件

template: (rf: core.ɵRenderFlags, ctx: ManualComponent) => {
  if (rf & 1) {
    // ...
    core.ɵC(5, ifTemplate, null, ['ngIf']);
  }
  if (rf & 2) {
    // ...
    core.ɵp(5, 'ngIf', (ctx.name === 'Igor'));
  }

  function ifTemplate(rf: core.ɵRenderFlags, ctx: ManualComponent) {
    // ...
  }
},

第4行使用一个新方法ɵC。这个方法声明了一个容器元素,第一个参数是指元素的索引,跟之前索引的作用一样,第二个参数是我们自己定义的子模版生成函数,生成的模版将被放在容器元素里面。第三个参数是元素的标签名,在我们这里没有对应的值。最后一个是指令列表和这个元素的属性集合,ngIf在这里使用。
第8行通过判断ctx.name的值是否等于’Igor’来决定ngIf属性的执行结果。
等价HTML代码如下:

<div *ngIf="name === 'Igor'">
  <img src="...">
</div>

比较经过Ivy编译生产的代码和我们手动编译的代码,感觉还不错。
THE END

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.