Coder Social home page Coder Social logo

blog's People

Contributors

xuanweih avatar

Stargazers

 avatar

Watchers

 avatar

blog's Issues

什么是作用域

作用域的概念

先说概念:
作用域是指程序源代码中定义变量的区域。
作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。
JavaScript 采用词法作用域(lexical scoping),也就是静态作用域。

有哪些作用域

一般情况下我们会听到如下名词

词法作用域
动态作用域
全局作用域
局部作用域
块级作用域
词法作用域

词法作用域对应的就是静态作用域.那么什么是静态作用域 我们拿个例子来说明一下

var hxw = 'xixi'
function say() {
    console.log(hxw)
}
function sayHi() {
   var hxw = 'haha'
   say()
}
sayHi()
结果输出的是 'xixi'

执行函数say() 首先查找函数内部有没有变量hxw 如果没有就往上继续查找.
这里输出的是xixi hxw来自于全局变量
说明函数在声明时它的作用域就已经决定了 而不是调用时
这就是静态作用域

动态作用域

顾名思义 是与静态作用域对立. 函数不是声明时确定作用域而是在调用时确定.
与this的机制相关.不关心何时声明 只关心何时调用

全局作用域

直接编写在 script 标签之中的JS代码,都是全局作用域;
或者是一个单独的 JS 文件中的。
全局作用域在页面打开时创建,页面关闭时销毁;
在全局作用域中有一个全局对象 window(代表的是一个浏览器的窗口,由浏览器创建),可以直接使用。

局部作用域

在js中 通常指的就是函数作用域了
在函数内部就是局部作用域,这个代码的名字只在函数的内部起作用
调用函数时创建函数作用域,函数执行完毕之后,函数作用域销毁;
每调用一次函数就会创建一个新的函数作用域,它们之间是相互独立的。

块级作用域

块级作用域是es6新增的概念,
对于 出现在{ } 大括号里面的代码都可以理解是在一个块级作用域中
let const声明方式只对同一个块级作用域有效.

实现promise的相关api

在上一篇中 实现promise
我们从0-1实现了一个promise
这篇我们再来手动实现以下 和promise有关的api和方法
假设class MyPromise已经实现

promise.prototype.catch

catch 相当于是调用了.then的第二个方法
所以 catch实现也很简单
class MyPromise {
  catch (rejectFn) {
     return this.then(undefined,rejectFn)
  }
}

promise.prototype.finally

  finally 传入一个回调函数,无论前面状态是否成功或者失败,都会执行这个回调, 并且最后返回也可以继续
  进行then的链式调用
 finally (callback) {
     return this.then(
         val => MyPromise.resolve(callback()).then(()=>val),
         reason => MyPromise.resolve(callback()).then(()=>{throw reason})
     )
 }

resolve

  resolve 对传入的值进行判断 如果是promise类型 则直接返回 否则返回一个执行状态的promise
  static resolve (val) {
    if (val instanceof Mypromise) return val
   return new MyPromise(resolve => resolve(val))
  }

reject

   reject 和resolve原理相通
    static reject (value) {
        return new MyPromise((resolve,reject) => reject(value))
    }

all

 all 传入一个数组, 会在这个数组里的所有数据处理完成后返回一个结果数组, 如果有一个报错则将错误返回
  static all (promiseArr) {
      let index = 0
      let res = []
      return  new Mypromise((resolve,reject)=> {
         promiseArr.forEach((p,i)=>{
             MyPromise.resolve(p).then(val => {
                 index++
                 res[i] = val
               // 当数组遍历完则输出全部的结果
                if(promiseArr.length === i) {
                     resolve(res)
                }
              },err => {
                 reject(err)
               })
          }) 
     }) 
  }

race

race方法则是返回最先运算结束的那个结果.且有报错直接返回报错
    static race (promiseArr) {
       return new MyPromise.then((resolve,reject)=> {
          for (let i of promiseArr) {
              MyPromise.resolve(i).then(val=>{
                resolve(val)
              },err=>{
                  reject(err)
              })
          }
       })
    }

js继承

关于继承的一步步实践

<script>
//   1. call继承
// function Parent () {
//     this.name = 'father'
//     this.getName = function () {
//         console.log(this.name)
//     }
// }
// Parent.prototype.smile = function () {
//     console.log('smile')
// }
// function Son () {
//     Parent.call(this)
//     this.type = 'son'
// }

// let son = new Son()
// let f= new Parent()
// console.log(f.smile())
// console.log(son.smile())
// 存在问题 如果父类的原型链上有 方法 子类无法继承

// 2. 原型链继承
// function Parent1 () {
//     this.name = 'parent1'
//     this.arr = [1,2,3,4]
// }
// Parent1.prototype.laugh = function () {
//     console.log('laugh')
// }
// function Son1 () {
//     this.type = 'son1'
// }
// Son1.prototype = new Parent1()
// let son1 = new Son1()
// console.log(son1.laugh)
// let son2 = new Son1()
// son1.arr.push(1)
// console.log(son2.arr)
// 解决不能用原型链上的方法
// 因为公用了一个原型链 所以实例对象之间会互相影响

// 3. 组合继承  将前两种结合
// function Parent2 () {
//     this.name = 'parent2'
//     this.arr = [1,2,3,4]
// }
// Parent2.prototype.laugh = function () {
//     console.log('laugh')
// }
// function Son2 () {
//     Parent2.call(this)
//     this.type = 'son2'
// }
// Son2.prototype = new Parent2()
// let son3 = new Son2()
// console.log(son3.laugh)
// let son4 = new Son2()
// son3.arr.push(1)
// console.log(son4.arr)
// 问题 Parent2构造函数 其实都相当于 多执行
// 多执行一次 Son2.prototype = new Parent2()

// 4. 优化组合继承
// function Parent3 () {
//     this.name = 'parent3'
//     this.arr = [1,2,3,4]
// }
// Parent3.prototype.laugh = function () {
//     console.log('laugh')
// }
// function Son3 () {
//     Parent3.call(this)
//     this.type = 'son3'
// }
// Son3.prototype = Parent3.prototype 
// let son5 = new Son3()
// console.log(son5) // 发现constructor 是Parent3 本来应该是Son3

// 5. 寄生组合继承
// function Parent4 () {
//     this.name = 'parent4'
//     this.arr = [1,2,3,4]
// }
// Parent4.prototype.laugh = function () {
//     console.log('laugh')
// }
// function Son4 () {
//     Parent4.call(this)
//     this.type = 'son4'
// }
// Son4.prototype = Object.create(Parent4.prototype) // Object.create相当于 {}.__proto__ = Parent4.prototype
// Son4.prototype.constructor = Son4 
// let son6 = new Son4()
// console.log(son6)
// 没什么大问题了

// 6. class继承
class Point {
    constructor() {
        this.x = 1
        this.y = 2
    }
    toString () {
       console.log('toString')
       return '111'
    }
}
class ColorPoint extends Point {
  constructor(color, x, y) {
    super(x, y); // 调用父类的constructor(x, y) 且子类必须在constructor中调用super 否则子类得不到this 对象
    this.color = color;
  }
  toString() {
    return this.color + ' ' + super.toString(); // 调用父类的toString()
  }
}
let np = new ColorPoint('gre')
console.log(np)
console.log(np.toString())

// 通过babel 转译后的es5 class
// function _possibleConstructorReturn (self, call) { 
// 		// ...
// 		return call && (typeof call === 'object' || typeof call === 'function') ? call : self; 
// }
// function _inherits (subClass, superClass) { 
//     // ...
//     //看到没有
// 		subClass.prototype = Object.create(superClass && superClass.prototype, { 
// 				constructor: { 
// 						value: subClass, 
// 						enumerable: false, 
// 						writable: true, 
// 						configurable: true 
// 				} 
// 		}); 
// 		if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; 
// }
// var Parent = function Parent () {
// 		// 验证是否是 Parent 构造出来的 this
// 		_classCallCheck(this, Parent);
// };
// var Child = (function (_Parent) {
// 		_inherits(Child, _Parent);

// 		function Child () {
// 				_classCallCheck(this, Child);
		
// 				return _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).apply(this, arguments));
// 		}

// 		return Child;
// }(Parent));

// 继承的核心就在于_inherits函数 使用的依旧是组合寄生式继承 xx.prototype = Object.create()
// 不过这里更加优秀 使用 Object.setPrototypeOf(subClass, superClass)继承父类的静态方法
// Object.setPrototypeOf(object, prototype)                                                                                                                                                                              
</script>
总结: 1. 单纯call继承 缺点:不能继承到父级原型上的方法
2. 原型继承 child.prototype = new Parent()  缺点: 公用了原型链 实例之间的修改会造成影响
3. 组合式 结合了上面两种 缺点 相当于重复执行了两个 child.prototype = new parent()
4. 组合式升级 缺点 实例的父级对不上
5. 寄生组合  child.prototype =  Object.create(parent.prototype)

理解js的执行上下文.

什么是执行上下文

es3中指的是 作用域 变量对象 this值
es5之后指的 词法环境 变量环境 this值

本文主要围绕es3来理解执行上下文 es5版本会在之后的内容中提及

js执行上下文

每次运行js代码时,js引擎都会产生一个全局执行上下文的环境.一般情况下,执行上下文分为三种:

1.全局执行上下文, 不在任何函数对象中的代码也会处于一个全局对象中, 他会生成一个全局对象,对于浏览器来说这个对象就是window, 用var声明的变量会挂载为window的属性, 函数也会挂载为window的方法, 然后会让this指向这个全局对象, 对一个程序来说,只有一个全局执行上下文
2.函数执行上下文, 函数执行上下文在一段代码中会有很多个, 每调用(重点是要调用的)一个函数,就会产生一个函数执行上下文,这个上下文也可以称为局部执行上下文.
3.eval执行上下文, eval在js不常用,但是在js非严格模式下eval可以改变全局下的变量的值,但是在严格模式下只能存储在局部(执行eval函数会创建执行上下文。 只不过严格模式执行eval函数,不会作用于它的外层作用域,所以修改x不会生效。)

JS 函数执行上下文的创建过程

简单来讲就是以下几步:
#####创建过程
1.创建 初始化参数arguments 提升函数声明和变量声明
2.产生作用域链
3. 确定this指向
#####执行过程
4. 执行代码
5. 代码执行完毕 出栈 回收

经典面试

取一道经典例子来分析 执行上下文

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}
checkscope()();
两段代码都会打印'local scope'。虽然两段代码执行的结果一样,但是两段代码究竟有哪些不同呢?

以第一段代码为例, 我们看看这个过程中到底发生了什么:
执行过程如下:

1. 执行全局代码,创建全局执行上下文, 全局上下文被压入执行上下文栈
ECStack = [
   globalContext
]
2. 全局上下文初始化
globalContetx = {
   VO: [global, scope, checkscope],
   Scope: [globalContext. VO],
    this: globalContext.VO
}
初始化的同时 checkScope 函数被创建 同时保存作用域链到函数的内部属性
checkScope.[[scope]] = [
   globalContext.VO
]
3.  执行 checkscope函数, 创建checkscope函数执行上下文, checkscope函数执行上下文被压入执行上下文栈中:
 ESStack = [
   checkscopeContext,
   globalContext
]
4. checkscope函数执行上下文初始化
  复制函数[[scope]]属性创建作用域链
  用arguments创建活动对象
  初始化活动对象 加入形参 函数声明 变量声明
  将活动对象压入checkscope 作用域链顶部
同时 f 函数被创建,保存作用域链到 f 函数的内部属性[[scope]]
    checkscopeContext = {
        AO: {
            arguments: {
                length: 0
            },
            scope: undefined,
            f: reference to function f(){}
        },
        Scope: [AO, globalContext.VO],
        this: undefined
    }
5.执行 f 函数,创建 f 函数执行上下文,f 函数执行上下文被压入执行上下文栈

    ECStack = [
        fContext,
        checkscopeContext,
        globalContext
    ];
6.f 函数执行上下文初始化, 以下跟第 4 步相同:

复制函数 [[scope]] 属性创建作用域链
用 arguments 创建活动对象
初始化活动对象,即加入形参、函数声明、变量声明
将活动对象压入 f 作用域链顶端
    fContext = {
        AO: {
            arguments: {
                length: 0
            }
        },
        Scope: [AO, checkscopeContext.AO, globalContext.VO],
        this: undefined
    }
7.f 函数执行,沿着作用域链查找 scope 值,返回 scope 值

8.f 函数执行完毕,f 函数上下文从执行上下文栈中弹出

    ECStack = [
        checkscopeContext,
        globalContext
    ];
9.checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈中弹出

    ECStack = [
        globalContext
    ];

闭包

闭包

闭包真的是前端程序员的一道鸿沟.每次尝试去理解它都有不一样的感触.
记得最早一有个老师和我说 你永远只要记得,闭包的作用就两个:
1.提供有限的访问权限
2.延长变量的生命周期

很长一段时间内,我对1的理解出现了一些偏差.因为当时那个老师举得例子大概是------

    /*闭包作用01
        提供有限的访问权限
     */

    //通过函数来访问对象
    function test (  ) {
        var name = '少岛主';
        var yanzhi = 101;
        //使用四个闭包函数来访问局部变量
        function getName (  ) {
            return name;
        };
        function setName (value  ) {
            if (value.indexOf('狗') == -1){//名字不能有狗,否则无法修改
                name = value;
            }

        };
        function  getYanzhi(  ) {
                return yanzhi;
        };
        function setYanzhi (value  ) {
            if (value >= 100){//颜值必须大于100,否则无法修改
                yanzhi = value;
            }
        };
        return {
            getName:getName,
            setName:setName,
            getYanzhi:getYanzhi,
            setYanzhi:setYanzhi,
        };
    };
    //执行一次外部函数,声明两个局部变量。外部函数返回了一个对象,这个对象包含四个方法
    var banzhang = test();

    banzhang.setName('班长');//修改name
    banzhang.setName('二狗子');//修改无效
    console.log ( banzhang.getName () );//获取name

    banzhang.setYanzhi(200);//修改有效
    banzhang.setYanzhi(99);//修改无效
    console.log ( banzhang.getYanzhi () );

让我一度以为闭包是可以让 外部访问到内部函数的变量.
在一次面试中还回答 闭包就是外部函数可以访问内部的变量.让面试官震惊
回想起来确实有些可笑

闭包的定义

那么有人会问了 什么是闭包.
对于闭包的定义有很多种:
举几个常见的

1. 一个函数可以访问另一个函数中的变量 
2. 能够访问自由变量的函数, 自由变量指的是非函数内的局部作用域和形参的变量
3. 根据词法作用域的规则, 一个内部函数总是可以访问外部函数中的变量, 
当调用一个外部函数返回了内部函数时,即使外部函数执行完毕被销毁了,
 内部函数中引用外部函数的变量依旧保存在内存中,
 这些被保存在内存变量中的集合就称为闭包,比如外部函数是foo 这些变量的合集就称为foo函数的闭包

闭包产生的本质

内部函数存在对父级作用域的引用

闭包的作用

1. 可以从内部函数访问外部函数的变量,并保存在内存里, 可供之后使用
2. 避免全局变量污染
3. 实现变量私有化

闭包应用举例

/**

  • 闭包使用场景
  • 闭包: 引用了上一级上下文(作用域链中的)变量对象的函数
  • 利用闭包特性去做一些事情
  • 1.函数作用域内的变量无法被外部访问, 即函数作用域内的变量是私有的
  • 2.函数体内的变量数据存放在堆内存中,在作用域链(上下文)中被引用时,
  • 不会被销毁,有记忆功能, 可以保存状态
    */
  1. 模块封装 减少全局变量污染 沙箱模式
var sandbox = (function () {
    // 这样声明为模块私有变量,外界无法直接访问
    var id = 0;
    function add() { console.log(id++) }
    return {add};
}());
sandbox.add() 1
sandbox.add() 2
  1. 函数节流防抖(以防抖为例)
function debounce(fn, delay=1000) {
    let timer 
   return function () {
    let context = this;
    let args = argument
      if (timer) clearTimeout()    
      timer = setTimeout(()=> {
       fn.apply(context, args)
    },delay)
  }
}

// 3.模拟一个单例: 只会实例化一次

class FactoryFood {
  constructor(name) {
    this.name = name
  }
  getName() {
    console.log(this.name)
    return this.name
  }
}

// 创建多个实例
function creatFood(name) {
  return new FactoryFood(name)
}

let apple = creatFood('apple')
let pie = creatFood('pie')
apple.getName()
pie.getName()

// 如何保证只创建一个实例, 就需要闭包维护一个标志符
let creatOneFood = (function() {
  let isInstance = null // 记录状态
  return function (name) {
    if (!isInstance) isInstance = new FactoryFood(name)
    return isInstance
  }
})()

let xxx = creatOneFood('xxx')
let ooo = creatOneFood('ooo')
xxx.getName() // xxx
ooo.getName() // xxx

模块化

模块化分类

前端常见的模块化规范
AMD 基于requireJs
CMD  基于seajs
CommonJS node中使用的模块化
ESModule es6新出的js模块化

AMD

AMD模块化规范主要是基于requireJs的
下面我们来简单介绍一下 requirejs大概是如何使用的

/*
    语法结构:
    1. 如果是使用自己的本身为参照
    define({})
*/

define({
    name:'xx'
    add: function(xx,xxx) {
        
    }
})
require(['js/xxx'],function(module){
    module.xxx
})

如果是导入了其他模块
/*
    语法结构:
    2. define([引入其他模块地址],回调函数(引入模块别名));
    别名可以在函数里面去调用其他模块提供的方法
*/
// 一个返回对象的匿名模块
define(['js/1_math.js'],function(math){
    // 减法
    var subtraction = function (num1, num2){
        return num1 - num2;
    }
    // 把方法返回出去
    return {
        add : math.add,//加法
        sub : subtraction//减法
    }
});

// 如果定义时 
define('模块别名',[],function)
使用时可以配置 require.config({
    paths:{
        '别名': '路径'
    }
})
require(['mypath'],function(module){})

CMD

seajs定义时和requirejs很相似

define({
    add : function(a,b){
        return a + b;
    }
});
使用时
// 引用模块方法seajs.use("地址",回调函数)
// 注意:需要从当前目录(./)开始找,.js后缀可以省略
seajs.use("./js/1_math",function(math){
    console.log(math.add(111,222));
})
// 如果要调用其他模块define 可以是下面这种写法
define(function(require,exports,module){
    // require的路径是从当前文件所在路径开始找
    var m = require('1_math');
    // 1. 第一种写法
    // exports.add = m.add;
    // exports.sub = function(a,b){
    //     return a - b;
    // }

    // 2.第二种写法
    module.exports = {
        add : m.add,
        sub : function(a,b){
            return a - b;
        }
    }
});
// 使用时一样是 seajs.use('地址','回调函数')
seajs.use('./js/2_math',function(math){
    console.log(math.sub(222,111));
})

CommonJS 与 ESmodule

CommonJS 在前端使用中就非常常见了,
作用域为模块内部
模块内部有一个module对象记录模块的状态,且有一个exports属性
用于导出 module.export = {} 是一个导出引用
导入的时候使用require()
require是一个函数,执行文件的路径参数,从而去执行文件模块的内容
再次调用时 因为module.loaded属性变为true 所以直接读取缓存

ESmodule
由js语法实现, 解析编译时确定模块的引用, 默认是严格模式
导出模块 可以直接export 或者 export defalut
export 可以有多个 但是export defalut只能有一个 使用export defalut之后 import可以任意定义名称 可以理解为export 导出的是一个引用关系,所以当使用export导出一个值的时候是会报错的,如果导出export{xx}就没有问题
导入时使用 import xx from ''
或者 impor {} from
或者 import * from

commonjs是动态加载 运行时加载
esmodule是编译时加载 是静态的

commonjs是一个模块执行结果的拷贝
esmodule是模块的内部的只读属性引用

了解HTTP的有关知识

http 报文结构

http的报文结构一般是由 起始行,头部,空行,实体组成

请求 起始行 一般是  请求方法+路径+http版本 get/home/http1.1
响应 由 方法+状态码+原因 http1.1/200 ok

头部: 请求头 请求行 空行      响应头 响应行 空行

空行 用于区分头部和实体

实体 请求体 响应体

请求方式

http1.1
head 获取资源元信息
get 获取数据
post 提交数据 上传数据
put 修改数据
delete 删除数据(没用到)
connect 建立连接隧道 用于代理服务器
options 列出可对资源实行的请求方法 用来跨域请求
trace 追踪请求-响应的传输路径

get post 的区别

get 传输数据放在url里 post放在请求体里
get 浏览器会缓存get请求 post默认不会
get 只能接受ACS II字符 post不受限制
get get执行相同操作结果相同 post不一定 幂等性
get tcp只发一次数据包 post 发两次第一次先发header 响应100后再发body
火狐浏览器只发一次

状态码

1xx: 表示目前是协议处理的中间状态,还需要后续操作
2xx: 请求成功
3xx: 重定向 资源位置发生变动
4xx: 请求报文出错
5xx: 服务端出错

101: switching Protocols http变更为websocket 如果服务端同意变更就会报101


200: 请求成功
204: 请求成功但是响应体没数据
206: 分段响应成功 http分块下载以及断点续传的时候 会带上content-range字段

301: 永久重定向
302: 暂时性重定向
304: 命中协商缓存

403 服务器禁止访问
404 没找到页面
405 请求方法不允许
408 请求时间太长

500 服务器出错
501 服务端不支持你的请求的功能
502 服务器是好的 但是访问的时候出错了
503 服务器繁忙



200响应成功
204响应成功但没数据
206分段响应成功
301永久重定向
302临时重定向
304命中协商缓存
400不好的请求
404页面没找到
405请求方式不对
415不支持的媒体方式
500服务器内部错误
502访问出错
503服务器超负荷

Accept字段

1. 支持编码  Accept
text: text/html, text/plain, text/css 等
image: image/gif, image/jpeg, image/png 等
audio/video: audio/mpeg, video/mp4 等
application: application/json, application/javascript, application/pdf, application/octet-stream

2. 支持压缩
Accept-Encoding:  gzip deflate br 一种专门为http发明的压缩算法
3. 支持语言
Acept-language:   zn-ch zh ,en
4. 字符集
Accept-Charset: charset=utf-8

http 如何处理大文件传输

支持一定要响应头 返回accept-ranges:none

服务端需要添加content-range  
range字段拆解 range: bytes=0-9


多段
这个时候出现了一个非常关键的字段Content-Type: multipart/byteranges;boundary=00000010101,它代表了信息量是这样的:
请求一定是多段数据请求
响应体中的分隔符是 00000010101

cookie

cookie 是用于解决http无状态带来的问题,因为http不会存储传输过程上下文的信息,所以可以通过cookie来保存, 本质上cookie就是浏览器中一个很小的文本文件,内部以键值对的形式来存储.
可以在服务端通过 set-cookie: xxx来设置

cookie的相关属性:

生命周期: expires 过期时间 max-age 时间段
如果cookie过期了,则不会传给服务端
如果在Set-Cookie时不通过Expries、Max-Age两个字段设置Cookie的时效性,
那么这个Cookie是一个简单的会话期Cookie。它在关闭浏览器是会被自动删除。
如果设置了Expries、Max-Age那么这个Cookie在指定时间内都是有效的。
当Cookie的过期时间被设定时,设定的日期和时间只与客户端相关,而不是服务端。

作用域: 通过 domain 和path来限制cookie的域名和路径,如果domain和path没有匹配成功的话,也不会携带上cookie,对于路径来说/表示域名下面的任意路径都可以使用cookie

前端安全: 
如果带上的是secure 说明只能通过https传输cookie
如果cookie字段带上httponly 说明只能通过http传输 不能js访问 防止xss
想要防止csrf 通过sameSite字段的值设置不同程度上的限制:
strict 完全禁止其他域名下的请求携嗲cookie
lax 只有get请求提交表单和 a标签发送的get可以带cookie
none 任何情况都带cookie

缺点: 容量缺陷 只有4k
性能缺陷 每次请求都会带.请求多造成性能浪费 通过作用域可以有所改善
安全缺陷 httponly为false时 js就可以直接获取

session

session 是一种记录客户状态的机制,不同于cookie的是session是存在服务端的,而cookie数存在客户端的,解决了客户敏感信息不安全的问题 存在客户端的是一个sessionid,客户端发起请求的时候 会将这个sessionid带上,然后服务端根据sessionid获取对应的session数据

时效性:
Session在用户第一次访问服务器的时候自动创建,Session生成后,只要用户继续访问,服务器就会更新Session的最后访问时间,并维护该Session。 用户每访问服务器一次,无论是否读写Session,服务器都认为该用户的Session"活跃(active)"了一次。

当有越来越多的用户访问服务器,Session也会越来越多; 为防止内存溢出,服务器会把长时间内没有活跃的Session从内存删除。 这个时间就是Session的超时时间。如果超过了超时时间没访问过服务器,Session就自动失效了。

过期:
session 依赖于 cookie, 它存在与 cookie 中; 当 cookie 被清除时, sessionid 也会被清理掉.
所以, 在Set-Cookie时设置Expries或Max-Age,其实就是设置session的失效时间。 或者直接把Sessionid储存在本地。

cookie-session差异

Session是在服务端保存的一个数据结构,用来跟踪用户的状态,这个数据可以保存在集群、数据库、文件中;
Cookie是客户端保存用户信息的一种机制,用来记录用户的一些信息,也是实现Session的一种方式。

跨域cors

IE10以上可用 前端不需要增加什么配置 对于前端来说最省事的跨域方法

cors : 
简单请求:符合 请求类型为get post head
且请求头取值范围accept accept accept-language content-language 且content-type为
application/x-www-form-urlencoded、multipart/form-data、text/plain)

简单请求发出后 浏览器会自动在请求头添加 origin
服务端接收到之后会返回
access-control-allow-origin
Access-Control-Allow-Credentials false表示不带cookie
Access-Control-Expose-Headers

简
非简单请求:
非简单请求会有一次预检请求,预检请求使用options方法
浏览器会带上origin源地址 以及host目标地址还有以下关键字段
Access-Control-Request-Method, 列出 CORS 请求用到哪个HTTP方法
Access-Control-Request-Headers,指定 CORS 请求将要加上什么请求头

相应的预检请求响应其中有这样几个关键的响应头字段:

Access-Control-Allow-Origin: 表示可以允许请求的源,可以填具体的源名,也可以填*表示允许任意源请求。
Access-Control-Allow-Methods: 表示允许的请求方法列表。
Access-Control-Allow-Credentials: 简单请求中已经介绍。
Access-Control-Allow-Headers: 表示允许发送的请求头字段
Access-Control-Max-Age: 预检请求的有效期,在此期间,不用发出另外一条预检请求

预检响应完成后,如果请求符合预期,则真正的cors请求就会发出否则会触发
xhrrequest的onerror
CORS 真正的请求就容易多了,现在它和简单请求的情况是一样的。浏览器自动加上Origin字段,服务端响应头返回Access-Control-Allow-Origin。

http2.0


头部压缩: http1.1及以前通常是会对请求体进行压缩,通过Content-Encoding头部字段来指定,当请求头部字段比较复杂时,特别是get请求,参数都在请求头中, http2.0中有对应的算法 hpack来压缩头部字段.
hpack的两大特点是:1 在服务端和客户端之间建立哈希表, 对于已经传输的过得信息,可以通过存索引的方式来避免每次都重复传递大量的信息. 2 对于字符串和整数进行哈夫曼编码,哈夫曼编码的原理就是先将所有出现的字符建立一张索引表,然后让出现次数多的字符对应的索引尽可能短,传输的时候也是传输这样的索引序列,可以达到非常高的压缩率。 


什么是多路复用:
http/2 不再使用请求头+请求体的明文形式进行传输,而是把他们拆一个个二进制的帧,010101这样传输, 用headers帧存放头部字段,bodys帧存放底请求体. 分帧之后服务器看到到就不是http队列了而是一堆乱序的二进制帧.就没有了排序的问题.
hhtp/2 通信双方都可以使用 这种二进制帧的传输序列叫做流,http/2使用流在一个tcp链接上面进行多个二进制帧的通信 就是多路复用


服务器推送 server push 

增加服务器可以给客户端发送消息的功能 不在局限于一定被动接受要请求才能响应 服务端可以自己新建一个流与客户端通信.
比如客服端请求一个html文件,tcp建立成功以后服务端可以把html里引用到的一些资源一并返给他减少等待时间

https 是如何保证安全传输的

首先 https并不是一个新的协议,只是在http上做了一些改良.通过中间人来完成一些安全上的升级. https通过对称加密和非对称加密结合加密而成

对称加密: A 与 B 用的同样一把钥匙解密 安全性不好
非对称加密:有AB两把密钥, A用来解密B加密过的数据包  B用来解密A加密过的数据包

从传输过程来说 浏览器向服务器先发送一个 client_random 以及一个加密列表
服务端接收到了以后 返回一个server_random 以及一个加密方法.公钥返回
且浏览器端保留一个私钥

浏览器接收到server_random后 再生成一个predom用公钥加密传给服务端
服务端私钥解密 predom 获取 用 pre+client+server 获取最终密钥
浏览器端也使用这个密钥 对称加密

中间人没有私钥 所以无法获取predom

但是有黑客可以DNS劫持 用自己的服务器替换目标服务器
这时候就需要有一个机构来证明这个服务器是真正的目标服务器
服务运营者要向第三方寻取授权
这个机构就是CA  
数字证书主要两个功能
1证明服务端的身份 2 返回公钥
浏览器接收到证书以后 验明身份之后就生成一个predom

浏览器如何验明身份
CA在证书上前面时会得到一个hash函数 通过函数计算明文内容得到A
通过公钥解密明文内容得到B 比较验证

如果浏览器不知道CA的可靠性 就会往上查找上一级的CA 根级CA一般是存储在操作系统中

ajax

1. new xmlhttprequest
2. xhr.open('get','url',true) 请求方法 url 以及是否异步

3.xhr.send() get请求没有参数 post 请求需要带上请求体
4. window.onreadystatechange = function () {
    
    状态 0请求未初始化 1 服务器已经建立连接 2请求已接收 3请求处理中 4请求已完成
    if (xhr.readystate === 4) {
        响应码判断
    }
    
}

原型链

什么是原型链

每个构造函数都会有一个prototype属性, 而构造函数的实例对象会有一个__proto__属性指向构造函数的原型prototype上,
访问一个实例的某个属性或者方法时,先从自身的构造函数中寻找,如果没找到,就会在对象的原型上找,如果原型上没有就会继续往上, function.prototype object.prototype上找 直到找到终点null还没有找到 则报错, 这个寻找过程中经历的链式结构就称为原型链.

一张觉得画的很明确的图

avtar

类数组

类数组

js中还存在一类特殊的数组,他们不是数组 但是却又有length属性
常见的Arguments是进行函数调用时,除了指定的参数外,还另外创建的一个隐藏对象,只不过它的属性从0开始排,
依次为0,1,2...最后还有callee和length属性。我们也把这样的对象称为类数组。

常见的类数组

除arguments以外 还有哪些常见的类数组呢:

  1. 用getElementByTagName/ClassName/Name()获得的HTMLCollection
  2. 用querySlector获得的nodeList

类数组转数组

类数组不能使用数组的api 所以工作中我们还需要将类数组转换进行对应的操作

  1. 方案一 Object.prototype.call.slice(arguments)
  2. 方案二 Array.from(arguments)
  3. 方案三 [...arguments]
  4. 方案四 Array.prototype.concat.apply([], arguments)

实现一个async/await

基于generator函数实现一个async/await

// 尽管Promise通过链式调用取代了回调嵌套,但是对于过多的链式调用可读性仍然不好,
// 流程控制也不方便, es7提出的async函数,终于让js对异步操作有了终极解决方案

// 而实际上 async/awiat是对generator生成器的封装
// es6 引入的generator函数 可以通过yield关键字把函数执行流程挂起,通过next()方法切入下一个状态
// 为改变执行流程提供了可能

// 而 *函数/yield 就和 async/await用法非常类似了
// 不一样之处就在于 
// async/await 不需要手动调用next
// async函数返回是promise对象 而generator是返回生成器对象
// await能够返回promise的resolve/reject的值

// 实现一个async/await 就是基于上述几点对generator的封装`


// 传入一个generator函数
function run (gen) {
   // 返回的是一个promise对象
   return new Promise((resolve,reject)=>{
        let g = gen()
        // 递归调用step的过程  
        function step (val) {
            // 异常捕获
            try {
               var res = g.next(val)
            } catch (err) {
               reject(err)
            }
            // res是一个对象 {done:false,value:xx} 递归的终止条件就是res.done为true说明调用结束
            let {value, done} = res
            // 如果done说明调用结束了
            if (done) return value
            Promise.resolve(value).then(val => {
                // 等待promise完成自动执行下一个next 并传入resolve的值
                step(val)
            }, err => {
                g.throw(err)
            })
        }
        // 第一次执行
        step()
   })
}

//测试代码

function* myGenerator() {
  console.log(yield Promise.resolve(1))   //1
  console.log(yield Promise.resolve(2))   //2
  console.log(yield Promise.resolve(3))   //3
}

run(myGenerator)

总结 思路其实非常简单:自动递归调用step -> 异常捕获 -> 返回promise  
简单的说,我们封装了一个run方法,run方法里我们把执行下一步的操作封装成step(),
每次Promise.then()的时候都去执行step(),实现自动迭代的效果。在迭代的过程中,
我们还把resolve的值传入gen.next(),使得yield得以返回Promise的resolve的值





浅谈输入url的过程

输入一个url的过程发生了什么?

用户操作:

  1. 输入的如果不是合法的url则会调用浏览器的默认搜索引擎拼接url,如果是合法的会补齐url
  2. 调用浏览器的beforeUnload的钩子里的功能
    输入url:
  3. 浏览器通过ipc线程向网络进程发送指令
  4. 检测强缓存 如果有强缓存直接返回200 并加载 强缓存http1.0/expire http1.1 cache-contrl max-age
  5. dns解析 映射对应的ip的地址 (dns缓存)
  6. tcp队列 如果超过6个则需要排队 否则继续
  7. tcp 三次握手:
    1. 客户端发起 SYN seq=x 并消耗一个序列号 进入Syn_sent状态
    2. 服务端接收到客户端发来的信号 (确认了客户端的发送能力) 并发送 一个ack=x+1 seq=y ACK SYN 然后进入syn_rcvd状态 (半连接队列)
  8. 客户端接收到服务端发来的信号(确认了服务端的接收与发送能力) 发送一个ack=y+1 seq=x+1
    然后进入established状态 服务端接收到之后也进入established状态 握手完成 开始通信
  9. 发起http请求 如果有重定向301 302的话 重新定位到新的域名下面重复上述操作,否则的话看是否返回304 判断是不是有协商缓存(http1.0 last-modify http1.1 etag 请求头带上if-modify-since if-none-match) 如果也没有 就是200
  10. http响应数据返回后,浏览器主线程发起提交文档操作,主线程通知渲染进程准备接受数据
  11. 渲染进程和网络进程建立通信管道
  12. 渲染进程渲染准备:
    1. 渲染进程解析html为dom树
      2 .计算css 标准化css 渲染进程解析css为stylesheet
    2. 根据dom树和stylesheet的规则,将dom树为可见的节点构建出布局树
    3. 对布局树进行分层,一些3d动画效果和具有z-index,需要切割clip的图层会有自己独立的一层,如果没有独立层级的就会继承父元素的层级
    4. 图层绘制, 渲染进程把绘制列表提交到合成线程
    5. 合成线程把图层转化为图块,并在光栅化线程池中转化为位图
    6. 合成线程发送绘制指令drawQuad发送给浏览器
    7. 浏览器根据drawQuad消息绘制页面 并显示在电脑上
  13. tcp 四次挥手(应该是响应完成后就会有 与渲染并行)::
    .四次挥手可以由两方发出 以客户端发出挥手信号为例
    1 客户发起挥手请求 FIN seq=x 进入FIN_wait1状态
    2 服务端收到客户端发起挥手后 立刻响应一个 ack=x+1 以及序列号seq=y 进入close_wait状态 此时并没有发出FIN信号 此时客户端收到信号进入 FIN_wait2状态
    (因为服务端在接收到FIN, 往往不会立即返回FIN, 必须等到服务端所有的报文都发送完毕了,才能发FIN。因此先发一个ACK表示已经收到客户端的FIN,延迟一段时间才发FIN。这就造成了四次挥手。)
    3.服务端再发出一次挥手,这次.携带FIN信号以及seq=y ack=x+1 并进入last_ack状态
    4.客户端收到FIN信号后 响应 ack=y+1 并发出一个seq=x+1 进入time_wait状态 并且等待2MSLl时间后进入closed状态( 考虑发送时间和如果丢包的再发一次的时间 所以等待两个2MSL
    5. 服务端收到客户端的响应后进入closed状态
    (正常来说如果挥手完成后断开,http1,.1之后 会检查connection字段是否有keep-alive保持长连接,用于节省每次都要建立连接的时间.
    完结 撒花

了解缓存机制

缓存作为web中不可缺少的部分,对于一名程序猿来说是非常重要滴,
本文主要从三个方向来记录一下 我所了解的缓存机制

缓存

DNS缓存

什么是DNS

域名系统 domain name system
dns是作为域名和ip地址相互映射的一个分布式数据库
为了方便用户能更便捷地访问互联网不用记太多的ip串 dns协议运行在udp协议之上 使用端口号53

DNS 解析

dns解析就是通过域名得到对应的ip地址的过程,就是域名解析
www.hxw.com -> 123.13.1.1

DNS 缓存

能够缓存dns地方有很多
浏览器 操作系统 local dns 根域名服务器都会对dns结果做一定程度的缓存
而这个缓存 就在dns查询的过程中起效果
dns查询过程如下:
1.浏览器如果存在 则域名解析结束
2.浏览器如果无缓存,读取操作系统的hosts文件看是否存在映射关系 如果有解析结束
3.如果本地hosts不存在映射 在本地dns服务器 (isp服务器或者自己手动设置的dns服务器)
4.如果本地dns服务器也没有 则向根服务器发出请求 进行递归查询

CDN 缓存

什么是CDN

内容分发网络 content delivery network

CDN 加速

CDN加速与常规dns查找不一样的地方在于,当localdnd得到域名记录后向智能调度dns查找域名的ip地址
智能调度dns根据一定的算法和策略(比如静态拓扑,容量)将最适合的cnd节点ip地址回应给localdns

CDN缓存

cnd的缓存,如果浏览器本地缓存失效了,浏览器会像cnd边缘节点发起请求,类似于浏览器缓存,cnd边缘节点也有一套缓存机制
也遵循http协议 通过http响应头中的
cache-control max-age
来设置cdn边缘节点的缓存时间
当浏览器向CDN节点请求数据时,CDN节点会判断缓存数据是否过期,若缓存数据并没有过期,则直接将缓存数据返回给客户端;否则,CDN节点就会向服务器发出回源请求,从服务器拉取最新数据,更新本地缓存,并将最新数据返回给客户端。 CDN服务商一般会提供基于文件后缀、目录多个维度来指定CDN缓存时间,为用户提供更精细化的缓存管理。

CDN回源

当 cdn 缓存服务器中没有符合客户端要求的资源的时候,缓存服务器会请求上一级缓存服务器,以此类推,直到获取到。最后如果还是没有,就会回到我们自己的服务器去获取资源。 那都有哪些时候会回源呢?没有资源,资源过期,访问的资源是不缓存资源等都会导致回源

浏览器缓存(http缓存)

什么是浏览器缓存

浏览器缓存就是浏览器通过保存http获取的所有资源,是浏览器网络资源存储在本地的一种行为.

浏览器缓存分类

强缓存

强缓存在加载资源时会读取本地缓存中的headers, 如果命中强缓存则不需要向服务端发送请求
header中 可以通过字段 expires 和 cache-control
http1.0时期 常用 expires表示过期时间, 这个时间代表资源失效时间,在这个时间之前则命中缓存
缺陷是失效时间是一个绝对时间,如果客户端与服务端的时间有差异时 会造成误差
http1.1 采用了一个新字段 cache-control来控制强缓存
利用 max-age值来判断,max-age是一个相对时间,单位为s
cache-control 除了 max-age以外 还有如下值
no-cache 代表需要进行协商缓存发送到服务器
no-store 代表禁止缓存
public 表示可以被所有用户缓存 包括中间代理服务器
private 表示只能被终端用户的浏览器缓存 不允许cdn等中间代理服务器缓存
cache-control和expires同时设置时 cache-control的的优先级更高

协商缓存

当强缓存没有命中的时候,浏览器会发送一个请求到服务器, 服务器根据header中的部分信息来判断是否命中缓存,
如果命中缓存 则返回304,告诉浏览器资源未更新,可使用本地的缓存
协商缓存中 header中的信息指的是 last-modify/if-modify-since 和 etag/if-none-match

last-modify/if-modify-since
浏览器第一次请求一个资源的时候 服务器会在header中加上一个last-modify
last-modify是一个时间标识 标记最后的修改时间
当浏览器再次请求浏览器的时候 请求头会带上if-modify-since,该值为缓存之前返回的last-modify
服务器收到if-modify-since后 会根据资源最后的修改时间判断是否命中缓存 如果命中返回304 并且不会返回资源内容,并且不会返回 Last-Modify。

etag/if-none-match
etag/if-none-match返回的是一个校验码, etag可以保证每一个资源是唯一的,资源变化都会导致etag变化
服务器根据浏览器送上的if-none-match判断是否命中
与 Last-Modified 不一样的是,当服务器返回 304 Not Modified 的响应时,由于 ETag 重新生成过,response header 中还会把这个 ETag 返回,即使这个 ETag 跟之前的没有变化。
Last-Modified 与 ETag 是可以一起使用的,服务器会优先验证 ETag,一致的情况下,才会继续比对 Last-Modified,最后才决定是否返回 304。

last-modify效率会更高,但是etag的精准度更高
last-modify有两大缺陷. 最低时间单位是1s 要是变化在1s内无法检测到, 以及如果编辑了资源文件,但是文件内容并没有更改,这样也会造成缓存失效。
两种配置都存在时, 服务器会优先考虑ETag

缓存位置

三级缓存原理 (访问缓存优先级)

先在内存中查找,如果有,直接加载。
如果内存中不存在,则在硬盘中查找,如果有直接加载。
如果硬盘中也没有,那么就进行网络请求。
请求获取的资源缓存到硬盘和内存。

也有另一个版本的说法 浏览器中的缓存位置一共有四种,按优先级从高到低排列分别是:

Service Worker

Service Worker 借鉴了 Web Worker的 思路,即让 JS 运行在主线程之外,由于它脱离了浏览器的窗体,因此无法直接访问DOM。虽然如此,但它仍然能帮助我们完成很多有用的功能,比如离线缓存、消息推送和网络代理等功能。其中的离线缓存就是 Service Worker Cache。

Memory Cache 内存存储

MemoryCache顾名思义,就是将资源缓存到内存中,等待下次访问时不需要重新下载资源,而直接从内存中获取。Webkit早已支持memoryCache。
目前Webkit资源分成两类,一类是主资源,比如HTML页面,或者下载项,一类是派生资源,比如HTML页面中内嵌的图片或者脚本链接,分别对应代码中两个类:MainResourceLoader和SubresourceLoader。虽然Webkit支持memoryCache,但是也只是针对派生资源,它对应的类为CachedResource,用于保存原始数据(比如CSS,JS等),以及解码过的图片数据。

Disk Cache 硬盘存储

顾名思义,就是将资源缓存到磁盘中,等待下次访问时不需要重新下载资源,而直接从磁盘中获取,它的直接操作对象为CurlCacheManager。
因为CSS文件加载一次就可渲染出来,我们不会频繁读取它,所以它不适合缓存到内存中,但是js之类的脚本却随时可能会执行,如果脚本在磁盘当中,我们在执行脚本的时候需要从磁盘取到内存中来,这样IO开销就很大了,有可能导致浏览器失去响应。

Push Cache

即推送缓存,这是浏览器缓存的最后一道防线。它是 HTTP/2 中的内容,虽然现在应用的并不广泛,但随着 HTTP/2 的推广,它的应用越来越广泛

总结

对于浏览器缓存而言
当浏览器再次访问一个已经访问过的资源时,它会这样做:
1.看看是否命中强缓存,如果命中,就直接使用缓存了。
2.如果没有命中强缓存,就发请求到服务器检查是否命中协商缓存。
3.如果命中协商缓存,服务器会返回 304 告诉浏览器使用本地缓存。
4.否则,返回最新的资源。

实现一个promise

实现一个promise/ promise的实现原理

以前总觉得手写promise是很难的, 但是当我们拆解把这个手写的过程作为理解promise的一种形式,
一步步的递进式写,其实还是不那么难的

实现的代码

// 手写promise
// 步骤 首先搭建promise模型
// 1.状态机
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
class MyPromise {
    // 传入一个执行器 执行回调
    constructor(executor) {
        this._resolveQueue = [] // 成功队列
        this._rejectQueue = [] // 失败队列
        this._status = PENDING
        this._value = undefined
        let _resolve = (val) => {
            const run = () => {
                this._value = val
                if (this._status !== PENDING) return
                this._status = FULFILLED
                while (this._resolveQueue.length) {
                    const callback = this._resolveQueue.unshift()
                    callback(val)
                }
            }
          setTimeout(run)
        }
        let _reject = (val) => {
            const run = () => {
                this._value = val
                if (this._status !== PENDING) return
                this._status = REJECTED
                while (this._rejectQueue.length) {
                    const callback = this._rejectQueue.unshift()
                    callback(val)
                }
            }
            setTimeout(run)

        }
        // 5.executor 如果为同步任务 要把resolve 和reject做成异步任务处理
        executor(_resolve, _reject)
    }
    then(resolveFn, rejectFn) {
        // 3.then值的透传
        typeof resolveFn !== 'function' ? resolveFn = val => val : null
        typeof rejectFn !== 'function' ? rejectFn = reason => reason : null
        //2. then 返回一个promise保证链式调用
        return new MyPromise((resolve, reject) => {
            const fulfulledFn = (val) => {
                try {
                    // 返回对应顺序的then执行
                    let x = resolveFn(val)
                    x instanceof MyPromise ? x.then(resolve, reject) : resolve(val)
                } catch (e) {
                    reject(e)
                }
            }

            const rejectedFn = (err) => {
                try {
                    // 返回对应顺序的then执行
                    let x = rejectFn(val)
                    x instanceof MyPromise ? x.then(resolve, reject) : resolve(val)
                } catch (e) {
                    reject(e)
                }
            }
            // 4.可能出现promise不在pending 比如直接调用promise.resolve()
            switch (this._status) {
                case PENDING:
                    this._rejectQueue.push(rejectedFn)
                    this._resolveQueue.push(fulfulledFn)
                    break
                case FULFILLED:
                    fulfulledFn(this._value)
                    break
                case REJECTED:
                    rejectedFn(this._value)
                    break
            }

        })
    }
}

总结

总结起来过程是以下几步
1. 整体起promise架子: promise接受一个executor() 在new promise时立即执行这个回调
executor中的异步任务进入任务队列-> then里面接受成功或者失败的回调函数->executor中执行队列中的回调
观察者模式: then进行依赖收集->异步触发resolve->resolve执行依赖
2. promise规范 用3状态表示这个promise的状态进程 pending->fulfilled pending->reject 且不可逆
3. then 是支持链式调用的 所以返回的也是一个promise
4. 多个.then还是要注意顺序的.如果执行完成就进行下一个then
5. then如果传入的参数不是函数 则会发生透传
6. promise可以直接调promise.resolve() 会出现promise状态就直接是已经变更状态的问题
7. resolve reject进任务队列应该是异步任务  这里通过settimeout来实现 (如果executor为同步且resolve,reject也是同步的话,可能会出现promise->resolve->then是不符合promise执行流程的 所以一定得是异步)

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.