# 核心

# 进程线程

任一时刻,CPU 总是运行一个进程,其他进程处于非运行状态。

一个进程就是一个程序的运行实例, 是操作系统分配资源的最小单位, 有自己独立的内存空间, 一个进程可以包括多个线程, 共享进程的内存空间

线程是 CPU 调度单位. 多线程可以并行处理任务,但是线程是不能单独存在的,它是由进程来启动和管理的. 进程中的任意一线程执行出错,都会导致整个进程的崩溃

进程的内存空间是天然独立的,线程的内存空间是天然共享的。所以, 进程通信需要通信机制, 线程间要防止竞争(同时修改,而导致的数据不一致)

# 互斥锁(Mutual exclusion 缩写 Mutex)

一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。防止多个线程同时读写某一块内存区域。

# 信号量(Semaphore)

某些内存区域,只能供给固定数目的线程使用, 用信号量来告知其它线程还能容纳的线程数. 用来保证多个线程不会互相冲突

提供协调机制,一方面防止进程之间和线程之间产生冲突,另一方面允许进程之间和线程之间共享资源。

mutex是semaphore的一种特殊情况(n=1时)但是,因为mutex较为简单,且效率高,所以在必须保证资源独占的情况下,还是采用这种设计。

# 协程(coroutine)

传统的编程语言异步编程的解决方案, 是通过多个线程互相协作,完成异步任务。

协程有点像函数,又有点像线程。协程通过 yield 命令暂停, 将执行权将交给其他协程, 其他协程执行完后将执行权限归还

ES6 中的 Generator 函数

function* gen() {
  const f1 = yield fs.readFile('file1')
  const f2 = yield fs.readFile('file2')
}

# CPU

打开 windows 资源管理器... 我的 CPU 是 I7 四核心, 8线程(单核模拟双核)

插槽: 对应 CPU 个数

内核: 又称核心(Die)是CPU最重要的组成部分, 各种 CPU 核心都具有固定的逻辑结构,一级缓存、二级缓存、执行单元等

逻辑处理器: 是 CPU 线程数的一种逻辑概念, CPU 核心可以模拟类似 多核心 的功能

对程序执行的影响: 多 CPU 可以实现进程并发, 多核心(意味着多线程)可以实现线程并发

参考:

进程与线程的一个简单解释

CPU个数、CPU核心数、CPU线程数

浏览器进程?线程?傻傻分不清楚!

# 内存

stack heap typeof null === 'object' 的解释

虽然 typeof null 会输出 object,但是这只是 JS 存在的一个悠久 Bug。在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象然而 null 表示为全零,所以将它错误的判断为 object

0.1+0.2为什么不等于0.3?

js 中数字均采用 64 位浮点数存储 3 === 3.0,表示范围 [2^-53, 2^53] 之间的数。在运算时转成 32 位整数

64 位浮点数表示:(-1)^s x M x 2^E 符号位 占 1 位,表示正负,1 =< M < 2 占 52 位,E 占 11 位

精度最多只能到53个二进制位,这意味着,绝对值小于等于2的53次方的整数,即-2^53到2^53,都可以精确表示。 能够表示的数值范围为2^1024到2^-1023(开区间),超出这个范围的数无法表示。

0.1和0.2在转换成二进制后会无限循环,由于标准位数的限制后面多余的位数会被截掉,此时就已经出现了精度的损失,相加后因浮点数小数位的限制而截断的二进制数字在转换为十进制就会变成0.30000000000000004

参考:

浮点数的二进制表示

# 垃圾回收机制

不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak), JavaScript 提供自动内存管理, 这被称为"垃圾回收机制"(garbage collector)

V8只能使用系统的一部分内存,具体来说,在64位系统下,V8最多只能分配1.4G, 在 32 位系统中,最多只能分配0.7G. 是由两个因素所共同决定的,一个是JS单线程的执行机制,另一个是JS垃圾回收机制的限制。

首先JS是单线程运行的,这意味着一旦进入到垃圾回收,那么其它的各种运行逻辑都要暂停; 另一方面垃圾回收其实是非常耗时间的操作,V8 官方是这样形容的:以 1.5GB 的垃圾回收堆内存为例,V8 做一次小的垃圾回收需要50ms 以上,做一次非增量式(ps:后面会解释)的垃圾回收甚至要 1s 以上。

V8 把堆内存分成 新生代内存和老生代内存 两部分(新生代就是临时分配的内存,存活时间短, 老生代是常驻内存,存活的时间长)

根据这两种不同种类的堆内存,V8 采用了不同的回收策略,来根据不同的场景做针对性的优化。

新生代内存空间一分为二: from to. 当进行垃圾回收时,V8 将From部分的对象检查一遍,如果是存活对象那么复制到To内存中,如果是非存活对象直接回收即可。

不过Scavenge 算法的劣势也非常明显,就是内存只能使用新生代内存的一半,但是它只存放生命周期短的对象,这种对象一般很少,因此时间性能非常优秀

新生代中的变量如果经过多次回收后依然存在,那么就会被放入到老生代内存中,这种现象就叫晋升。

老生代的垃圾回收机制当中:

由于JS的单线程机制,V8 在进行垃圾回收的时候,不可避免地会阻塞业务逻辑的执行,倘若老生代的垃圾回收任务很重,那么耗时会非常可怕,严重影响应用的性能V8 采取了增量标记的方案

第一步,进行标记-清除。首先会遍历堆中的所有对象,对它们做上标记,然后对于代码环境中使用的变量以及被强引用的变量取消标记,剩下的就是要删除的变量了,在随后的清除阶段对其进行空间的回收。

第二步,整理内存碎片。在清除阶段结束后,把存活的对象全部往一端靠拢。事实上也是整个过程中最耗时间的部分。

浏览器可以使用 chrome-devtools 的 performance 面板分析内存的占用, 也可以用 performance monitor 监控内存变化

Nodejsprocess.memoryUsage() 可以查看内存的占用情况. 使用 node --expose-gc 进入 repl 后, 可以手动执行 global.gc() 进行垃圾回收

process.memoryUsage()
<==
rss(resident set size):所有内存占用,包括指令区和堆栈。
heapTotal:"堆"占用的内存,包括用到的和没用到的。
heapUsed:用到的堆的部分。
external: V8 引擎内部的 C++ 对象占用的内存。

从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC。

Major GC 是清理永久代。

Full GC 是清理整个堆空间—包括年轻代和永久代。

参考:

JavaScript 内存泄漏教程

# 变量作用域

作用域就是一套规则,用于确定如何查找变量

JS 是的作用域属于词法作用域(静态作用域) shell 是动态作用域

函数执行时所在的作用域,是定义时的作用域,而不是调用时所在的作用域。

变量作用域有: 全局, 函数, try .. catch, eval(严格模式下), 块级(es6)

变量作用域的嵌套形成作用域链(chain scope)

# this

this 就是函数运行时所在的环境对象.

  • 全局上下文直接调用函数
  • 对象.方法的形式调用
  • DOM 事件绑定(特殊)
  • new 构造函数绑定
  • call apply bind 显示绑定(据说参数多的时候使用 apply 更好, 反之)

在参数少的情况下,call 的性能优于 apply,反之 apply 的性能更好

参考:

Javascript 的 this 用法

# 闭包(closure)

闭包就是能够读取其他函数内部变量的函数。简单理解成"定义在一个函数内部的函数"。

表现形式:

  • 函数返回一个函数
  • 函数作为参数传递
  • 事件监听器的回调函数
  • IIFE(立即执行函数表达式)

在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁

作用: 读取函数内部的变量, 让这些变量始终保持在内存中; 封装对象的私有属性和私有方法

注意: 函数中的变量都被保存在内存中,内存消耗很大

参考:

学习Javascript闭包(Closure)

# 面向对象

JavaScript 支持面向对象编程, 也支持函数式编程.

面向对象的特点: 继承,

而 JavaScript 继承的实现是基于原型对象(构造函数的 prototype 属性)实现的

# 原型

prototype 对象的 isPrototypeOf() 判断它是不是实例的原型:

setPrototypeOf getPrototypeOf

instanceof 能否判断基本数据类型?

123 instanceof Number //false new Number(1) instanceof Number // true Number(1) instanceof Number // false Number instanceof Function // true

所有的对象(除了 null) 都可以扩展属性.

所有的引用类型都有一个 proto 属性(隐式原型,非标准属性),它是一个普通对象

所有的函数(除了箭头函数, call, bind, apply)都有一个 prototype 属性(显示原型),它是一个普通对象

对象的 隐式原型 指向 它的构造函数的 显示原型

for in 循环会遍历可枚举的原型属性,高级浏览器可能会屏蔽原型上的属性,但还是建议写上 hasOwnProperty() 判断

Symbol typeof Symbol.hasInstance // symbol typeof Symbol.iterator // symbol

# 原型链

JavaScript 的设计受到 C++ Java 的影响, Javascript 里面所有的数据类型(除 null,undefined)都是对象(和 Java 类似), 必须有一种机制,将所有对象联系起来, 使用 new 来执行构造函数, 返回一个对象, 为了节约内存, 构造函数加入 prototype 属性, 用于共享属性和方法

由于所有的实例对象共享同一个prototype对象,那么从外界看起来,prototype对象就好像是实例对象的原型,而实例对象则好像"继承"了prototype对象一样

原型链的形成: JavaScript对象通过prototype指向父类对象,直到指向Object对象为止,这样就形成了一个原型指向的链条, 即原型链。

访问对象上不存在的属性时, 会代理到对象的构造函数的 prototype 属性上, prototype 上找不到的时候, 再代理到 constructor.prototype 属性的构造函数的 prototype 属性上, 直到 null

f.__proto__  === Foo.prototype // 是一个对象
Foo.prototype.__proto__ === Object.prototype

Object.prototype.__proto__ === null

f.__proto__.__proto__ === Object.prototype
f.__proto__.__proto__.__proto__ === null

# 继承

继承是从抽象到具象,从高级到低级的关系 Animal => Dog => Husky

  1. 构造函数继承
function Cat (name, color) {
  Animal.apply(this, arguments)
  ...
}
  1. prototype 继承
// 法一
Cat.prototype = new Animal()
Cat.prototype.constructor = Cat
// 法二
Cat.prototype = Animal.prototype
Cat.prototype.constructor = Cat
// 法三
Cat.prototype = Object.create(Animal.prototype)
Cat.prototype.constructor = Cat

上述继承方式, 编写和阅读都很费力, 所以, ES5 提出了一个新的方法 Object.create(), 更方便地生成对象

  1. Object.create 创建对象
var Cat = {
  name: 'mium~',
  eat: function () {}
}
var cat = Object.create(Cat, { })
  1. 极简主义继承
const Animal = {
  eat () {},
  create () {
    const animal = {
      type: 'xx',
      run () {}
    }
    return animal
  }
}
const Dog = {
  like: '骨头', // 共享数据
  create (name) {
    const dog = Animal.create() // 继承属性方法
    dog.name = name // 私有
    dog.bark = function () {} // 私有
    return dog
  }
}

# ES6 class

class 语法声明构造函数,是普通构造函数的写法的语法糖。js 内部最终会转成普通函数的形式。 class 语法模仿了 java 和 C# 。class 关键字声明了一个构造函数,和 function 关键字声明一个函数效果是一样的。java 和 C# 的类是一个对象

class 的继承本质还是语法糖,js 内部使用函数的 prototype 属性(原型链的方式)实现继承

class 语法的优势 语法上更贴合面向对象的写法,缩小语言之间的差异性,不能时间都耗在 hello world 上了 实现继承更加容易,易读,和理解,学习成本低

# in for..in for..of

基于原型的继承

对象的 hasOwnProperty() 判断其属性是不是继承的

# 继承存在的问题

无法决定继承哪些属性,所有属性都得继承。

参考:

Javascript继承机制的设计思想

# 面向组合

继承存在问题, 使用面向组合的设计方式。

# 函数式编程

编程范式(programming paradigm) 有 面向对象编程(Object-oriented programming)和过程式编程(Procedural programming)函数式编程(functional programming)

函数式编程就是如何编写程序的方法论。它属于"结构化编程"的一种,主要思想是把运算过程尽量写成一系列嵌套的函数调用.

特点:

  1. 函数是"第一等公民", 指的是函数与其他数据类型一样, 可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值。
  2. 只用"表达式",不用"语句", 每一步都是单纯的运算,而且都有返回值。不要有不必要的读写行为,保持计算过程的单纯性。
  3. 没有副作用 函数要保持独立,功能就是返回一个新的值, 不得修改外部变量的值
  4. 不修改状态, 函数式编程使用参数保存状态, 内部根据参数的不同执行不同的逻辑
  5. Referential transparency, 函数的运行只依赖于输入的参数, 参数不变, 返回值就是确定的

好处:

  1. 代码简洁,开发快速, 函数式编程大量使用函数,减少了代码的重复
  2. 接近自然语言,易于理解, 比如: add(1,2).multiply(3).subtract(4)
  3. 更方便的代码管理, 每一个函数都可以被看做独立单元,很有利于进行单元测试(unit testing)和除错(debugging),以及模块化组合。
  4. 易于"并发编程", 不需要考虑"死锁"(deadlock),因为它不修改变量, 不存在"锁"线程的问题
  5. 代码的热升级, 函数式编程没有副作用, 内部实现是外部无关的, 可以在运行状态下直接升级代码,不需要重启

# 函子

函子是函数式编程里面最重要的数据类型, 也是基本的运算单位和功能单位. 学习函数式编程,实际上就是学习函子的各种运算

由于可以把运算方法封装在函子里面,所以又衍生出各种不同类型的函子,有多少种运算,就有多少种函子。函数式编程就变成了运用不同的函子,解决实际问题。

of 方法, Maybe 函子, Either 函子, ap 函子, Monad 函子, IO 操作

# 合成

compose(f, compose(g, h)) 等同于 compose(compose(f, g), h) 等同于 compose(f, g, h)

f(x)和g(x)合成为f(g(x)),有一个隐藏的前提,就是f和g都只能接受一个参数, 如果可以接受多个参数,比如f(x, y)和g(a, b, c), 这时就需要函数柯里化.

# 柯里化(currying)

函数柯里化就是一个多参数的函数,转化为单参数函数

# 偏函数

固化参数, 从而减少参数个数, 使用起来更加简洁

# 尾调优化(Tail Call optimization)

尾调优化就是指某个函数的最后一步操作只是调用另一个函数. function fun() { return continue() }, 不能有其他操作

优点: 由于是尾调用, 执行栈只保留内层函数的调用记录, 这将大大节省内存

递归改写成"尾调优化"的形式, 就不会发生栈溢出!

// 阶乘
function factorial(n, total = 1) {
  if (n === 1) return total
  return factorial(n - 1, n * total)
}

现状:

严格模式下 ES6 支持尾递归优化, 但是

尾调用优化依旧有隐式优化和调用栈丢失的问题, 各大浏览器(除了safari ?)根本就没部署尾调用优化, V8 也不行

使用"蹦床函数"代替尾递归

// 递归用 bind 改写成 返回函数的形式
function factorial (n, total = 1) {
  if (n === 1) return total;
  return factorial.bind(null, n - 1, n * total);
}
function trampoline (f) {
  while (f && f instanceof Function) {
    f = f()
  }
  return f
}
trampoline(factorial(19999)) // Infinity

参考:

函数式编程初探

函数式编程入门教程

函数范式 - gitbook

尾调用优化

# JS 执行过程

LHS查询 LHS : 指的是赋值操作的左端。在LHS查询的时候,如果标识符一直找不到声明的位置,那么最终就会在全局环境生成一个全局变量

RHS查询 它指的是赋值操作的源头 RHS查询的时候,如果找不到对应的标识符,就会抛出一个异常:ReferenceError

# 编译

  1. 词法解析

把字符串解析成一个个有意义的代码块,这些代码块也被称为词法单元(token), 提取出 关键字 变量名 操作符

  1. 生成 AST

接下来语法分析阶段,将生成的这些 token 数据,根据一定的语法规则转化为逐级嵌套的 AST, 编译器/解释器后续的工作都要依靠 AST

  1. 代码生成

生成 AST 之后,直接通过 V8 的解释器(也叫Ignition)来生成字节码, 字节码仍然需要转换为机器码,通过解释器来逐行将 字节码 转成 机器码, 大大降低了内存的压力.

在执行字节码的过程中,如果发现某一部分代码重复出现,那么 V8 将它记做热点代码(HotSpot),然后将这么代码编译成机器码保存起来, 再遇到它们时直接执行相应的机器码

所以 JS 并不是完全的解释型语言, 这种字节码跟编译器和解释器结合的技术,我们称之为即时编译(JIT)

直接转成机器码的体积太大,引发了严重的内存占用问题, 字节码是介于AST 和 机器码之间的一种代码,但是与特定类型的机器码无关,字节码需要通过解释器将其转换为机器码然后执行

# Event Loop

# 对 Event Loop 的解释

Event Loop 是计算机系统的一种运行机制, 可以解决 JS 单线程运行带来的一些问题。

JS 中的任务分同步任务和异步任务, 同步任务总是比异步任务更早执行!

同步任务在主线程上执行, 形成执行栈(execution context stack), 依次执行栈中的任务, 并把的异步任务依次放入在任务队列(task queue)中

一旦执行栈为空, 系统就会读取"任务队列", 把达到执行时机的异步任务, 依次放入执行栈中(主线程)执行

主线程从"任务队列"中读取事件,这个过程是循环不断的, 直到任务队列清空. 所以整个的这种运行机制又称为事件循环(Event Loop)

# 任务队列

宏任务(MacroTask): 如 setTimeout setInterval setImmediate(IE10, IE11, Node) requestAnimationFrame Script UI Rendering? GC? I/O ?

微任务(MicroTask): 如 Promise MutationObserver process.nextTick

任务队列可以理解事件队列(Event queue), 是一个先进先出的数据结构.

执行栈中遇到宏任务放进宏任务队列, 遇到微任务放进微任务队列(这里宏任务和微任务均是指异步任务).

当宏任务队列和微任务队列都不为空时, 其中微任务队列会较宏任务队列优先进入执行栈执行, 直到微任务执行完才执行宏任务. 也就是说任务是一队一队执行的.

当执行队列中的任务时, 如果该任务又产生了异步操作(新的任务),

这时, 如果新任务是宏任务, 则会被放到宏任务队列. 等待下轮事件循环的时候执行

如果新任务是微任务, 则会放到微任务队列, 待当前任务执行完之后, 立即执行微任务队列的任务!

所以, 在执行宏任务队列的一个宏任务时, 如果它产生若干个微任务, 待这个宏任务执行完后, 下一个宏任务不会进入执行栈!! 而是它产生的微任务队列优先进入执行栈! 直到微任务队列空了(注意微任务也能产生微任务, 进而补充微任务队列), 下一个宏任务才会进入执行栈!!

定时器作为宏任务, 没有到期是不会进入执行栈的! 与上面的逻辑不矛盾

**注意技术变更:**

对于上述情况, Node11 以下版本和新的浏览器表现不一致! 在执行宏任务队列的宏任务时, 是挨个挨个执行的(前提是定时器已到期)! 执行期间产生的微任务只是依次放入微任务队列中

HTML5标准规定了setTimeout()的第二个参数的最小值(最短间隔),不得低于4毫秒, 但是我测试 Chrome 80 并没有这个限制

Promise.then() await 会产生一个新的微任务

在微任务中部署 DOM 操作更接近渲染时机

# Node.js 里的 Event Loop

NodeJS 运行机制不同于浏览器环境, Node 可以跟系统内核对话, 所以异步语法比浏览器更复杂. libuv 库专门负责各种回调函数的执行时间

nodejs system

根据上图, Node.js 执行流程:

V8引擎解析JavaScript脚本 ==> 解析后的代码,调用Node API ==> libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎 ==> V8引擎再将结果返回给用户

Node Event Loop 图示

Node 开始执行脚本时,会先进行事件循环的初始化

同步任务 ==> 发出异步请求 ==> 规划定时器生效的时间 ==> 执行 process.nextTick 等等 ==> 事件循环就正式开始

对各阶段的解释:

  1. 定时器阶段(timers)

处理 setTimeoutsetInterval 的回调函数. 如果当前时间满足定时器的条件, 就执行定时器的回调函数,否则就离开这个阶段.

  1. I/O callbacks

所有的回调函数都在这个阶段执行, 除了 setTimeout setInterval setImmediate 及用于关闭请求的回调函数: 比如socket.on('close', ...)

  1. idle/prepare handlers

该阶段只供 libuv 内部调用,这里可以忽略。

  1. 轮询阶段(Poll)

用于等待还未返回的 I/O 事件,比如服务器的回应、用户移动鼠标等等。如果没有其他异步任务要处理, 会一直停留在这个阶段.

  1. check

该阶段执行 setImmediate 的回调函数。

  1. close callbacks

执行关闭请求的回调函数: 比如 socket.on('close', ...)

Node.js 中队列的执行顺序为: 执行栈中的 Script ==> nextTick 队列 ==> 微任务队列 ==> 宏任务队列

浏览器中的微任务是在每个相应的宏任务中执行的,而 Node.js 中的微任务是在不同阶段之间执行的

Node.js 增加了两个定时器 process.nextTick (微任务) setImmediate(宏任务)

process.nextTick 是一个独立于 Event Loop 的任务队列, 相当于在当前"执行栈"的尾部插入任务, 只有执行完该队列才会进入下一次事件循环. 所以是所有异步任务里面最快执行的! 但是 process.nextTick 语句嵌套会导主线程被占用!

setImmediate 是向宏任务队列插入任务, 所以只会在下一次事件循环执行, 这和 setTimeout 类似. 但是, Nodejs 中定时器没有最小时间限制, 受性能影响最快也要 1ms

setImmediate(fn, 0) setTimeout(fn, 0) 哪个先执行? 如果当前循环已经过了 timers 阶段又没到 check 阶段, 就一定是 setImmediate, 否则不确定!

参考:

JavaScript 运行机制详解:再谈Event Loop

Node 定时器详解

eventLoop中为什么既有微任务又有宏任务?

# JS 异步编程

js 执行引擎是单线程的, 浏览器内核开辟事件触发线程,主要用于控制事件, 定时器触发线程, HTTP 异步请求线程 这三线程协助 js 主线程

JavaScript 使用单线程的原因大概是不想让浏览器变得太复杂,因为多线程需要共享资源、且有可能修改彼此的运行结果, JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程.

为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质

JavaScript 语言对异步编程的实现,就是回调函数. 异步逻辑可读性差,没有按照书写的顺序执行, callback 回调不容易模块化

但是异步任务里嵌套异步任务, 每次回调都要处理成功或失败,. 代码会横向发展, 混乱难以维护. 称之为回调黑洞(callback hell)

# Promise

状态变化 pending => fullfilled | pending => rejected

实例必须实现 then 方法,then 返回的必须是一个 promise 实例

如果 then 没有手动返回一个 promise 实例,则默认返回当前实例

状态凝固一定会执行 Promise.finally() 并返回一个 Promise, finally return 的参数不会被下一个 then 接受

如果 错误没有被 catch, 后面的 then 不会执行, catch 后的 return 值, 能被后续的 then 接收

Promise 的优点: 回调函数延迟绑定、回调返回值穿透和错误冒泡。避免了 callback 嵌套, 解耦了代码, 很好的体现了 "开放封闭原则"

普通调用函数时,必须传入(注册)回调,否则回调不执行

promise 实例通过 then 方法,随时可以注册回调函数,并拿到回调结果。 即函数调用函数回调分离,怎样具有良好的扩展性,集成性。这是js中实现异步的巨大进步

Promise 的不足:

Promise 的最大问题是代码冗余,原来的任务被Promise 包装了一下,不管什么操作,一眼看去都是一堆 then,原来的语义变得很不清楚。

# Iterator

可迭代数据类型: 数组, 类数组, Set, Map

# Generator

Generator 函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权(即暂停执行)。

Generator 函数就是遍历器生成函数, 执行后返回遍历器

ES6 Generator

# async..await

协程+Promise 书写同步代码的方式实现异步任务, 是目前最好的异步解决方案.

注意: 在 Array.forEach 的回调函数使用 async 不能保证回调按遍历顺序执行!

[3, 2, 1].forEach(async num => {
  const result = await new Promise(
    (resolve, reject) => {
      setTimeout(() => resolve(num), 1000 * num)
    }
  )
  console.log(result)
}) // 1 2 3

forEach 是高级函数, 但其底层使用的是 for 循环. 所以上述代码效果如下:

async function callback (num) {
  const result = await new Promise(
    (resolve, reject) => {
      setTimeout(() => resolve(num), 1000 * num)
    }
  )
  console.log(result)
}
for (const i in [3, 2, 1]) {
  const num = [3, 2, 1][i]
  callback(num)
} // 1 2 3

对于高级函数都存在这种情况, 如果要实现按遍历顺序执行, 不要使用高阶函数

# Thunk 函数

JS 中的 Thunk 函数是将多参数函数,替换成单参数的版本,且只接受回调函数作为参数

任何函数,只要参数有回调函数,就能写成 Thunk 函数的形式

比如:

// 正常版本的readFile(多参数版本)
fs.readFile(fileName, callback)
// Thunk版本的readFile(单参数版本)
var readFileThunk = Thunk(fileName)
readFileThunk(callback)
function Thunk(fileName){
  return function (callback){
    return fs.readFile(fileName, callback)
  }
}

Thunkify 模块可以实现转换

Thunk 函数的作用在于可以自动执行 Generator 函数, 前提是每一个异步操作,都要是 Thunk 函数(只接受回调函数作为参数)

const readFile = thunkify(fs.readFile)
function* gen(){
  yield readFile('fileA')
  yield readFile('fileB')
  // ...
  yield readFile('fileN')
}

run(gen)

function run (gen) {
  const it = gen()
  function next(err, data) {
    // 第一次没有值
    if (err || data) {
      console.log(err, data)
    }
    const result = it.next(err || data)
    if (result.done) return
    // result.value 接受 readFile 的回调函数作为参数
    result.value(next)
  }
  next()
}

# co 模块

co 函数库其实就是将两种自动执行器(Thunk 函数和 Promise 对象),包装成一个库。

使用 co 的前提条件是,Generator 函数的 yield 命令后面,只能是 Thunk 函数或 Promise 对象。确保每一步 next() 的返回值,是 Promise 对象

参考:

Javascript异步编程的4种方法

Generator函数的含义与用法

Thunk函数的含义与用法

co函数库的含义与用法

async 函数的含义和用法

# SEO

  1. 描述网站信息

<title> 值强调重点 <meta name="description" content="xx"> 把页面内容高度概括 <meta name="keywords" content="yy">列举出重要关键

  1. 语义化的HTML代码,符合W3C规范:语义化代码让搜索引擎容易理解网页, 非装饰性图片必须加 title
  2. 重要内容HTML代码放在最前, 不要用js输出, 搜索引擎抓取HTML顺序是从上到下,保证重要内容一定会被抓取, 爬虫一般不会执行js获取内容, 也不会抓取iframe中的内容 重要内容:
  3. 提高网站速度:网站速度是搜索引擎排序的一个重要指标
  4. 单页应用路由使用 #!/ 开头 Google 会抓取, 最好使用服务端渲染
  5. 花钱

# 模块化

早期的模块, 使用立即执行的函数表达式, 通过传入参数, 表明所依赖的其它模块

# CommonJS

node 的模块系统是参照 CommonJS 的规范实现的, 使得 JS 具有了自己的模块规范

require 查找模块的顺序:

require('koa')

# 依次搜寻如下目录
# /home/serve/node_modules/
# /home/node_modules/
# /node_modules/

# 在上述目录中按顺序搜寻如下文件
# koa.js
# koa.json
# koa.node
# 上述不存在则,搜寻如下目录中的
# koa/package.json(main字段)
# koa/index.js
# koa/index.json
# koa/index.node

# 如果给定路径(相对路径或绝对路径)如 require('../utils'), 会按解析之后的唯一路径, 按如上述逻辑搜寻

实现方式


require 加载模时执行模块的代码(动态加载), 加载后会缓存起来, 二次加载直接从缓存读取, 缓存是根据绝对路径(也作为模块Id)识别模块的

exports 变指向 module.exports, 可以 module.exports = xxx exports.xxx = yyy 不能 exports = zzz(切断了和 module 的联系)

删除指定模块的缓存 delete require.cache[moduleName]

对循环加载的处理:

CommonJS 的做法是,一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出。

比如 a 依赖 b, b 又依赖 a, 假设 main.js 依次引入 a b:

// a.js
console.log(1)
var b = require('./b.js')
console.log(5, b) // { msg: 'I am module a' }
exports.msg = 'I am module b'
console.log(6)
// b.js
console.log(2)
var a = require('./a.js')
console.log(3, a) // {} 因为 a 的导出语句未执行!
exports.msg = 'I am module a'
console.log(4)
// main.js
var a = require('./a.js')
// 1, 2, 3, 4, 5, 6

a 执行, 发现依赖 b, 开始执行 b, 执行中发现循环依赖(依赖 a)! 只导入 a 中已执行的部分,继续往下执行。

b 执行完了, 回到 a 继续执行(从 require('b') 开始)。

结果: b 没有拿到 a 的导出,a 可以拿到 b 所有的导出.

而 b 只能拿到 a 中 require('b') 语句之前导出的内容(即获取不到该语句之后的导出, 因此有执行报错的风险!)

# ES Moudle

浏览器和服务器通用的模块方案, node 端使用 .mjs 文件, 使用参数 --experimental-modules 执行代码, 浏览器使用 <script type="module">

CommonJS 和 AMD 模块,都是在代码运行时加载相应的模块。ES6 的模块编译时就完成了模块的加载,也叫静态加载。一个提案引入 import() 函数来实现动态加载模块。该函数是异步加载,结果返回的是一个 Promise 对象

import() 函数接受和 import 命令一样的参数,但该函数是异步加载,结果返回的是一个 Promise 对象。使用该函数很容易实现按需动态加载。

ES6 模块自动采用严格模式, import 命令有提升效果, 类似 function 声明, 但建议写最前面, 利用阅读

ES6模块的运行机制与CommonJS不一样,它遇到模块加载命令import时,不会去执行模块,而是只生成一个引用。等到真的需要用到时,再到模块里面去取值。

ES6模块是动态引用,不存在缓存值的问题. 通过 export 输出的模块内部变化时,同过 import 获取该模块可获得模块内部实时的值。而且模块里面的变量,绑定其所在的模块

对循环加载的处理: 相当于没处理, 如果是两个模块内函数相互调用, 和在同一个模块中相互调用结果一样: Maximum call stack size exceeded

注意:

export default 的本质是把后面的值赋给 default 变量,所以后面不能接 let const var 等声明语句

import 类似 const 声明, 不能重新赋值, 可改写引用类型的属性(等于直接改写原模块属性)

export default function foo() {}

上述函数名 foo 可写可不写,其实 export default 输出的是匿名函数,import 时可以自定义函数名。加载后是获取不到函数名 foo 的。

# AMD 规范

随着网页功能越来越复杂,引入的 js 脚本越来越多,加载脚本的时间越来越长,而维护这些脚本也变更困难起来。require.js 的诞生,解决这两个问题

Asynchronous Module Definition(异步模块定义), nodejs 的模块(CommonJS 规范)是同步加载, 不适用于浏览器, 所以 AMD 规范诞生

不兼容浏览器的根本原因是缺少四个 node 变量 module exports require global

主要有两个 Javascript 库实现了 AMD 规范:require.js curl.js

  • 实现 js 脚本异步按需加载
  • 管理模块之间的依赖关系,使代码便于维护

require.js 定义了两个函数,define 和 require ,分别用于定义模块和加载模块。

html 中引入

<script src="lib/js/require.js" data-main="js/main"></script>

main.js 入口文件

require.config({
  // 配置模块路径
  baseUrl: 'lib/js',
  paths: {
    jquery: 'jquery.min'
    moduleA: 'moduleA'
  },
  // 引入非 AMD 规范的的模块使用 shim
  shim: {
    myLib: {
      deps: ['jquery'],
      exports: 'common/js/myLib'
    }
  }
})

定义模块

// 第一参数可选,表示当前模块依赖的其他模块。
define(['jquery'], function($) {
  return function getTime() {
    // do somethings
  }
})

使用模块

require(['jquery', 'moduleA'], function($, moduleA) {
  var imgs = $('img')
  var time = moduleA.getTime()
  // ...
})

AMD 规范允许输出的模块兼容 CommonJS 规范

define(function (require, exports, module) {
  var mA = require('moduleA')
  // ...
  exports.moduleB = {
    bar:  'hello',
    foo: function () {
      // ...
    }
  }
})

兼容 CommonJS 和 AMD 的写法

(function (global, factory){
  // CommonJS
  if (typeof module === 'object' && typeof module.exports === 'object') {
    module.exports = factory()
  }
  // AMD
  else if (typeof define === 'function' && define.amd) {
    define([], factory)
  }
  // Browser
  else {
    global['libName'] = factory()
  }
})(global, function () {
  return {}
})

支持插件

domready 插件使回调函数在 DOM 解构加载完成后执行,html 插件可以让你加载 html 文件,还有 json、text、image 插件等等

require(['domready', 'html!template/view1.html'], function(doc, html) {
  console.log('document is ready now !')
  document.body.appendChild(html)
})

参考:

# CMD

# UMD

参考:

# 类型转换

  • 如果有 [Symbol.toPrimitive]()方法,优先调用再返回
  • 调用 valueOf(),如果转换为原始类型,则返回
  • 调用 toString(),如果转换为原始类型,则返回
  • 如果都没有返回原始类型,会报错
var o = {
  [Symbol.toPrimitive](hint) { // 只能返回原始类型
    if (hint == 'number') return 10 // +o ==> 10
    if (hint == 'string') return 'hello' // `${o}` ==> "hello"
    return true // 1+o ==> 2
  }, // 优先
  valueOf () { return 2 }, // 其次
  toString () { return 3 }, // 再其次
}

有些对象比如 Date 优先级不同

# Web Worker

为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程, 在主线程运行的同时,Worker 线程在后台运行,两者互不干扰. 分担计算密集型或高延迟的任务

Worker 线程和主线程不在同一个上下文环境, 通过 postMessage 通信,

Worker 线程一旦新建成功,就会始终运行, 一旦使用完毕,就应该关闭以节约资源

限制:

同源限制, 无法读取主线程所在网页的 DOM 对象, 也无法使用document、window、parent这些对象, 不能执行alert()方法和confirm()方法, Worker 脚本必须来源网络, 不能本地文件

但是可以使用 navigator 对象和 location 对象, 可以使用 XMLHttpRequest 对象发出 AJAX 请求

WebWorker只属于某个页面,不会和其他页面的Render进程(浏览器内核进程)共享

SharedWorker是浏览器所有页面共享的,不能采用与Worker同样的方式实现,因为它不隶属于某个Render进程,可以为多个Render进程共享使用

# 专用线程

用法:

// main thread
const worker = new Worker('./worker.js', { name: 'worker1' })
worker.postMessage({ data })
worker.onMessage = function (eve) {
  console.log(eve.data) // 数据
  worker.terminate() // 关闭
}
// 监听 worker 线程错误
worker.onerror(function (event) {
  console.log(event.lineno)
  console.log(event.filename)
  console.log(event.message)
})
// 发送的数据无法序列化时, 触发 messageerror 事件
// worker.js
self.name // worker1
self.addEventListener('message', function (e) {
  if (e.data === 'STOP') {
    this.close() // 用于在 Worker 内部关闭自身。
  }
}, false)
// worker 内加载其它脚本
importScripts('script1.js', 'script2.js');
// this self 代表线程自身,即子线程的全局对象, 也可以省略不写

Firefox 支持在 worker 线程中在建 worker 线程

通信数据是传值(拷贝), 对体积大的二进制内容会消耗性能, 主线程可以直接转移数据的控制权给子线程, 从而不需要拷贝, 影像处理、声音处理、3D 运算等就非常方便了

// Transferable Objects 格式
worker.postMessage(arrayBuffer, [arrayBuffer]);

创建一个 worker 线程执行主线程的代码的方式

function createWorker(f) {
  var blob = new Blob(['(' + f.toString() +')()'])
  var url = window.URL.createObjectURL(blob)
  var worker = new Worker(url)
  return worker
}

# 共享线程

参考:

Web Worker 使用教程

# WebSocket

参考:

WebSocket 教程 Server-Sent Events 教程

# Service Worker

它脱离于浏览器窗体,因此无法直接访问 DOM。这样独立的个性使得 Service Worker 的“个人行为”无法干扰页面的性能,这个“幕后工作者”可以帮我们实现离线缓存、消息推送和网络代理等功能。我们借助 Service worker 实现的离线缓存就称为 Service Worker Cache。

生命周期包括 install、active、working。一旦 Service Worker 被 install,它将始终存在,只会在 active 与 working 之间切换,除非我们主动终止它

// main
navigator.serviceWorker.register('./sw.js')
  .then(() => console.log('success'))
  .catch((e) => console.error(e))
//sw.js
self.oninstall = function (e) {
  e.waitUntil(
    self.caches.open('v1.0.0').then(cache => {
      return cache.addAll([
        '/index.html',
        '/main.css',
        '/main.js'
      ])
    })
  )
}
self.onfetch = function (e) {
  e.respondWith(
    self.caches.match(e.request).then(res => {
      if (res) return res
      return fetch(e.request).then(res => {
        if (!res || res.status !== 200) return res
        self.caches.open('v1.0.0').then(cache => {
          cache.put(e.request, res)
        })
        return res.clone()
      })
    })
  )

}

必须使用 HTTPS

# IndexedDB

操作是同步的,

IndexedDB 250MB+ 乃至无容量限制 (WebStorage 2.5MB 到 10MB 之间, )

操作是异步的. (WebStorage 是同步的)

支持事务(只要有一步失败,整个事务就都取消, 不存在只改写一部分数据的情况)

支持二进制储存, 提供查找接口,还能建立索引

同源限制

近 NoSQL 数据库

# 严格模式

参考:

Javascript 严格模式详解

# BEM

Block__Element--Modifier

# Web Components

自定义元素的名称必须包含连词线

使用 js

<!-- 可以用 html 定义模板 -->
<template id="tpl">
  <!-- 内部样式 -->
  <style>
    /* :host 代表组件自己 */
    :host { height: 400px; width: 300px; text-align: center }
    .avatar { height: 80px }
    .des { color: #999; font-size: 16px }
  </style>
  <span class="des"></span>
</template>
<script>
  class MyAvatar extends HTMLElement {
    constructor() {
      super();
      // Shadow DOM, 该组件是封闭的,不允许外部访问
      const shadow = this.attachShadow({ mode: 'closed' })

      // 可以用js创建元素
      var image = document.createElement('img');
      image.src = this.getAttribute('url')
      image.classList.add('avatar')
      shadow.append(image)

      // 可以使用 template 的元素, 方便书写大量 html
      var des = document.getElementById('tpl').content.cloneNode(true);
      // 获取传入的参数
      des.innerText = this.getAttribute('des')
      shadow.appendChild(des);

      // 绑定事件
      this.$des = shadow.querySelector('.des')
      this.$des.addEventListener('click', function (e) { ... })
    }
  }
  window.customElements.define('my-avatar', MyAvatar)
</script>

<!-- 使用 -->
<my-avatar des="this is my avater" url="./avatar.png"><my-avatar>

参考:

Web Components 入门实例教程

# CDN

# 原理

  1. 当用户点击网站页面上的内容URL,经过本地DNS系统解析,DNS系统会最终将域名的解析权交给CNAME指向的CDN专用DNS服务器。

  2. CDN的DNS服务器将CDN的全局负载均衡设备IP地址返回用户。

  3. 用户向CDN的全局负载均衡设备发起内容URL访问请求。

  4. CDN全局负载均衡设备根据用户IP地址,以及用户请求的内容URL,选择一台用户所属区域的区域负载均衡设备,告诉用户向这台设备发起请求。

  5. 区域负载均衡设备会为用户选择一台合适的缓存服务器提供服务,选择的依据包括:根据用户IP地址,判断哪一台服务器距用户最近;根据用户所请求的URL中携带的内容名称,判断哪一台服务器上有用户所需内容;查询各个服务器当前的负载情况,判断哪一台服务器尚有服务能力。基于以上这些条件的综合分析之后,区域负载均衡设备会向全局负载均衡设备返回一台缓存服务器的IP地址。

  6. 全局负载均衡设备把服务器的IP地址返回给用户。

  7. 如果这台缓存服务器上并没有用户想要的内容,那么这台服务器就要向它的上一级缓存服务器请求内容,直至追溯到网站的源服务器将内容拉到本地。

参考:

CDN的基本原理和基础架构

CDN的基本工作过程

# 浏览器

浏览器内核可以分成两部分:渲染引擎(Layout Engine 或者 Rendering Engine)和 JS 引擎。

渲染引擎又包括了 HTML 解释器、CSS 解释器、图层布局计算模块、视图绘制模块

目前市面上常见的浏览器内核可以分为这四种:Trident(IE)、Gecko(火狐)、Blink(Chrome、Opera)、Safari(Webkit)。

HTML、CSS 和 JS,都具有阻塞渲染的特性。

# SOLID 原则

  1. 单一职责原则(Single-Resposibility Principle)

其核心思想为:一个类,最好只做一件事,只有一个引起它的变化。单一职责原则可以看做是低耦合、高内聚在面向对象原则上的引申,将职责定义为引起变化的原因,以提高内聚性来减少引起变化的原因。职责过多,可能引起它变化的原因就越多,这将导致职责依赖,相互之间就产生影响,从而大大损伤其内聚性和耦合度。通常意义下的单一职责,就是指只有一种单一功能,不要为类实现过多的功能点,以保证实体只有一个引起它变化的原因。 专注,是一个人优良的品质;同样的,单一也是一个类的优良设计。交杂不清的职责将使得代码看起来特别别扭牵一发而动全身,有失美感和必然导致丑陋的系统错误风险。

  1. 开放封闭原则(Open-Closed principle)

其核心思想是:软件实体应该是可扩展的,而不可修改的。也就是,对扩展开放,对修改封闭的。开放封闭原则主要体现在两个方面1、对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。2、对修改封闭,意味着类一旦设计完成,就可以独立完成其工作,而不要对其进行任何尝试的修改。 实现开开放封闭原则的核心思想就是对抽象编程,而不对具体编程,因为抽象相对稳定。让类依赖于固定的抽象,所以修改就是封闭的;而通过面向对象的继承和多态机制,又可以实现对抽象类的继承,通过覆写其方法来改变固有行为,实现新的拓展方法,所以就是开放的。 “需求总是变化”没有不变的软件,所以就需要用封闭开放原则来封闭变化满足需求,同时还能保持软件内部的封装体系稳定,不被需求的变化影响。

  1. Liskov替换原则(Liskov-Substituion Principle)

其核心思想是:子类必须能够替换其基类。这一思想体现为对继承机制的约束规范,只有子类能够替换基类时,才能保证系统在运行期内识别子类,这是保证继承复用的基础。在父类和子类的具体行为中,必须严格把握继承层次中的关系和特征,将基类替换为子类,程序的行为不会发生任何变化。同时,这一约束反过来则是不成立的,子类可以替换基类,但是基类不一定能替换子类。 Liskov替换原则,主要着眼于对抽象和多态建立在继承的基础上,因此只有遵循了Liskov替换原则,才能保证继承复用是可靠地。实现的方法是面向接口编程:将公共部分抽象为基类接口或抽象类,通过Extract Abstract Class,在子类中通过覆写父类的方法实现新的方式支持同样的职责。 Liskov替换原则是关于继承机制的设计原则,违反了Liskov替换原则就必然导致违反开放封闭原则。 Liskov替换原则能够保证系统具有良好的拓展性,同时实现基于多态的抽象机制,能够减少代码冗余,避免运行期的类型判别。

  1. 依赖倒置原则(Dependecy-Inversion Principle)

其核心思想是:依赖于抽象。具体而言就是高层模块不依赖于底层模块,二者都同依赖于抽象;抽象不依赖于具体,具体依赖于抽象。 我们知道,依赖一定会存在于类与类、模块与模块之间。当两个模块之间存在紧密的耦合关系时,最好的方法就是分离接口和实现:在依赖之间定义一个抽象的接口使得高层模块调用接口,而底层模块实现接口的定义,以此来有效控制耦合关系,达到依赖于抽象的设计目标。 抽象的稳定性决定了系统的稳定性,因为抽象是不变的,依赖于抽象是面向对象设计的精髓,也是依赖倒置原则的核心。 依赖于抽象是一个通用的原则,而某些时候依赖于细节则是在所难免的,必须权衡在抽象和具体之间的取舍,方法不是一层不变的。依赖于抽象,就是对接口编程,不要对实现编程。

  1. 接口隔离原则(Interface-Segregation Principle)

其核心思想是:使用多个小的专门的接口,而不要使用一个大的总接口。 具体而言,接口隔离原则体现在:接口应该是内聚的,应该避免“胖”接口。一个类对另外一个类的依赖应该建立在最小的接口上,不要强迫依赖不用的方法,这是一种接口污染。 接口有效地将细节和抽象隔离,体现了对抽象编程的一切好处,接口隔离强调接口的单一性。而胖接口存在明显的弊端,会导致实现的类型必须完全实现接口的所有方法、属性等;而某些时候,实现类型并非需要所有的接口定义,在设计上这是“浪费”,而且在实施上这会带来潜在的问题,对胖接口的修改将导致一连串的客户端程序需要修改,有时候这是一种灾难。在这种情况下,将胖接口分解为多个特点的定制化方法,使得客户端仅仅依赖于它们的实际调用的方法,从而解除了客户端不会依赖于它们不用的方法。 分离的手段主要有以下两种:1、委托分离,通过增加一个新的类型来委托客户的请求,隔离客户和接口的直接依赖,但是会增加系统的开销。2、多重继承分离,通过接口多继承来实现客户的需求,这种方式是较好的。

以上就是5个基本的面向对象设计原则,它们就像面向对象程序设计中的金科玉律,遵守它们可以使我们的代码更加鲜活,易于复用,易于拓展,灵活优雅。不同的设计模式对应不同的需求,而设计原则则代表永恒的灵魂,需要在实践中时时刻刻地遵守。就如ARTHUR J.RIEL在那边《OOD启示录》中所说的:“你并不必严格遵守这些原则,违背它们也不会被处以宗教刑罚。但你应当把这些原则看做警铃,若违背了其中的一条,那么警铃就会响起。”

# 移动端开发

# 点击"穿透"

移动端点击事件是模拟的, touchstart ==> touchmove ==> touchend ==> click. dblclick 是经过 2 次 touch 时间后模拟的

当被 touch 的元素隐藏后, 模拟的点击事件在原来的坐标被触发, 坐标对应的元素被点击, 就是所谓的点击"穿透"

解决方案:

  1. 有一个 faskclick 库, 但再结合第三方库使用时会有坑
  2. 先用 viewport 解决延时问题, 之后只是要 click 事件
  3. 只考虑移动端, 全用 touch 事件

# viewport

<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">

click 事件延迟, 是浏览器为了实现双击缩放, 或其他的依赖延迟的手势而产生的

只有设置了 width=device-widthinitial-scale=1user-scalable=no 可以避免 300ms 点击延迟,

如果有一个值不是上述值, 组合后可能仍然存在点击延迟! IOS 已经禁止设置 user-scalable=no

通过 css 解决延迟 html { touch-action: manipulation } 表示浏览器只允许进行滚动和持续缩放操作,类似双击缩放这种非标准操作就不可以

viewpoint是什么

# 混合开发(hybrid)

# hybrid 是什么,为何要用hybrid

  • 是什么?

前端和客户端 混合开发 不是所有场景都适合 hybird 开发

  • 存在价值?

可以快速迭代更新,无需 App 审核(App 可以访问大量系统权限),体验流畅,和 NA 差距小。减少开发沟通成本,双端共用一套代码

  • webview?

是 App 中的一类组件,是一个统称,不是具体的组件的名称 是一个小型的浏览器的内核,可以承载 h5 页面。

  • hybird 实现流程

前端做好静态页面(js css html),将文件交给客户端 客户端把静态文件保存在 app 中 客户端在一个 webview 中 使用 file 协议(速度快)加载静态文件。

# hybrid 更新流程

把静态页面保存在服务端,app 打开时下载最新的 静态文件保存在 app 中

静态文件 zip 打上时间戳,app 校验是否有更新,下载 zip 解压

# hybrid 和 h5 有何区别

hybrid 优点:体验好,和 NA 基本一致;可快速迭代,无需 app 审核 hybrid 缺点:开发成本高,联调、测试、查 bug 比较麻烦;运维成本高

场景:

使用 NA 开发的场景:要求体验极致,高频使用,比如说 app 首页 使用 hybird 的场景:体验要求高,变化频繁,比如新闻详情页 使用 h5:体验无要求,比如 举报,反馈页面不常用功能

# js 如何与客户端通信

hybrid 前端如何获取新闻内容? 不能使用 ajax:因为速度慢,而且跨域。客户端获取内容(可以预先获取),js 拿到内容,渲染页面。(类似 jsonp)

# 通讯的基本形式

调用接口,传递参数,监听回调

# 对 schema 协议的理解和使用

  • schema 协议基本使用

是前端和客户端通讯的约定。协议的名字,app 自己定义 如 weixin://dl/scan 扫一扫

  • 封装 schema
function (window, undefined) {
  function invokeShare (data, callback) {
    _invoke('share', data, callback)
  }
  function invokeLogin (data, callback) {
    _invoke('login', data, callback)
  }
  function invokeScan (data, callback) {
    _invoke('scan', data, callback)
  }

  window.invoke = {
    share: invokeShare,
    login: invokeLogin,
    scan: invokeScan,
  }
  function _invoke(action, data, callback) {
    // 拼接 schema 协议
    var schema = 'myapp://utils/'
    schema += action
    if (typeof data === 'object') {
      var q = ''
      for (var k in data) {
        if (data.hasOwnProperty(k)) q += `&${k}=${data[k]}`
      }
      schema += '?' + q.slice('1')
    }
    // 处理 callback 作为参数
    var callbackName = ''
    if (typeof callback === 'string') {
      callbackName = callback
    } else {
       // call 必须在 window 下能被客户端访问到
      callbackName = action + Date.now()
      window[callbackName] = callback
    }
    schema += '&callback=' + callbackName
    // 触发
    var img = new Image()
    img.onerror = function(e) { }
    img.onload = function (e) { }
    img.src = schema
  }
})(window)
  • 调用
window.invoke.share({ title: 'today news', des: 'xxx is xx dead!' }, function (result) {
  if (result.error === 0) {
    // 调用成功
  } else {

  }
})

invoke.js 内置到客户端,没有网络请求,黑客看不到 schema 协议更安全。

客户端每次启动 webview 都执行 invoke.js

# DOM 事件

# DOM 事件级别

DOM 0 element.onclick = function () {} 只能在冒泡阶段监听

为啥没有 DOM 1 ?

DOM 2 element.addEventListener = ('click', function () {}, ?option)

DOM 3 怎加了很多事件类型

# DOM 事件模型

# DOM 事件流

# Event 对象常见应用

# 自定义事件

onclick和addEventerListener中 this 默认指向绑定事件的元素。

IE比较奇异,使用attachEvent,里面的this默认指向window