Giter VIP home page Giter VIP logo

blog's Introduction

  • 👋 Hi, I’m @lingxiao-Zhu
  • 👀 I’m interested in Hybrid Mobile Development
  • 🌱 I’m currently learning Android
  • 💞️ I’m looking to collaborate on ...
  • 📫 How to reach me ...

blog's People

Contributors

lingxiao-zhu avatar

Stargazers

 avatar  avatar  avatar

Watchers

 avatar

blog's Issues

React Hooks 高频问题解析

平常一直在用 hooks,但对 hooks 很多深入用法了解甚少,在这里希望通过官方的 FAQ 进行总结和学习。

函数组件中有类似实例变量的东西吗?

可以通过 useRef 创建容器,不仅可以保存 DOM 的 ref,还可以存储任何的值,在函数组件更新的时候,会返回上一次的值,而不是重新创建,起到实例变量的作用。

useRef 仅能用在 FunctionComponent,createRef 仅能用在 ClassComponent。因为如果 createRef 用在 FunctionComponent 中时,每次更新都会创建新的对象。

不建议放到 render 阶段修改 ref 的值,因为 render 阶段可能被打断,不建议做副作用,修改 Ref 属于副作用操作。

// bad
function App() {
  const valueRef = React.useRef();
  valueRef.current += 1;
  return <div />;
}
// good
function App() {
  const valueRef = React.useRef();
  function add() {
    valueRef.current += 1;
  }
  // call add when you need
}

如何获取上一轮的 props 或 state?

useRef 还可以用于模拟 oldProps 的概念,记录上一次的 state,配合 useEffect 存储一个较老的值,最常用来拿到 previousProps。

function Counter() {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);

  const addCount = () => {
    setCount(count+1);
  }

  return <div>
  <h1>Now: {count}, before: {prevCount}</h1>
  <button onClick={addCount}>add count</button>
  </div>;
}

function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
    console.log(1)
  });
  return ref.current;
}

如何从 useCallback 读取一个经常变化的值?

useCallback 用于解决行内属性变化引起 rerender,但是如果 useCallback 的依赖是频繁变化的 state,那么其实没有起优化的作用,比如:

function Counter() {

  const [value, setValue] = useState(1);

  const handleClick = useCallback(()=>{
    setValue(value+1);
  }, [value])

  return <div>
    <h1>{value}</h1>
    <Memo onClick={handleClick}>add one</Memo>
  </div>;
}

function CustomButton({onClick}){
  console.log('CustomButton render')
  return <button onClick={onClick}>add one</button>
}

const Memo = React.memo(CustomButton)

当我们点击 CustomButton 时,value 改变,导致 useCallback 失效 CustomButton render。

如果你想要记住的函数是一个事件处理器并且在渲染期间没有被用到,你可以 把 ref 当做实例变量来用:

function Counter() {
  const [value, setValue] = useState(1);
  const valueRef = useRef(null)
  useEffect(()=>{
    valueRef.current = value;
  })
  const handleClick = useCallback(()=>{
    const currentRefVal = valueRef.current;
    setValue(currentRefVal+1);
  }, [valueRef])
  return <div>
    <h1>{value}</h1>
    <Memo handleClick={handleClick}/>
  </div>;
}

function CustomButton({handleClick}){
  console.log('CustomTitle render')
  return <button onClick={handleClick}>click</button>
}

const Memo = React.memo(CustomButton)

如何惰性创建昂贵的对象?

第一个常见的使用场景是当创建初始 state 很昂贵时:

function Table(props) {
  // ⚠️ createRows() 每次渲染都会被调用
  const [rows, setRows] = useState(createRows(props.count));
  // ...
}

为避免重新创建被忽略的初始 state,我们可以传一个 函数 给 useState:

function Table(props) {
  // ✅ createRows() 只会被调用一次
  const [rows, setRows] = useState(() => createRows(props.count));
  // ...
}

如何操作子函数组件的 DOM

函数组件是没有实例的,所以不能像类组件一样传递 ref 值,但可以用 useImperativeHandle 来实现。

useImperativeHandle(ref, createHandle, [deps])

useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。

useImperativeHandle 应当与 forwardRef 一起使用:

function FancyInput(props, ref) {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));
  return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);

《透视HTTP协议》HTTPS如何保证传输的安全

什么是HTTPS

它把 HTTP 下层的传输协议由 TCP/IP 换成了 SSL/TLS,由“HTTP over TCP/IP”变成了“HTTP over SSL/TLS”,让 HTTP 运行在了安全的 SSL/TLS 协议上,收发报文不再使用 Socket API,而是调用专门的安全接口。
image

SSL/TLS

SSL 发展到 v3 时已经证明了它自身是一个非常好的安全通信协议,于是互联网工程组 IETF 在 1999 年把它改名为 TLS(传输层安全,Transport Layer Security),正式标准化,版本号从 1.0 重新算起,所以 TLS1.0 实际上就是 SSLv3.1。

TLS 由记录协议、握手协议、警告协议、变更密码规范协议、扩展协议等几个子协议组成,综合使用了对称加密、非对称加密、身份认证等许多密码学前沿技术。

浏览器和服务器在使用 TLS 建立连接时需要选择一组恰当的加密算法来实现安全通信,这些算法的组合被称为“密码套件”(cipher suite,也叫加密套件)。

TLS 的密码套件命名非常规范,格式很固定。基本的形式是“密钥交换算法 + 签名算法 + 对称加密算法 + 摘要算法”。

比如:ECDHE-RSA-AES256-GCM-SHA384:“握手时使用 ECDHE 算法进行密钥交换,用 RSA 签名和身份认证,握手后的通信使用 AES 对称算法,密钥长度 256 位,分组模式是 GCM,摘要算法 SHA384 用于消息认证和产生随机数。”

对称加密和非对称加密

实现机密性最常用的手段是“加密”(encrypt),就是把消息用某种方式转换成谁也看不懂的乱码,只有掌握特殊“钥匙”的人才能再转换出原始文本。

这里的“钥匙”就叫做“密钥”(key),加密前的消息叫“明文”(plain text/clear text),加密后的乱码叫“密文”(cipher text),使用密钥还原明文的过程叫“解密”(decrypt),是加密的反操作,加密解密的操作过程就是“加密算法”。

“密钥”就是一长串的数字,但约定俗成的度量单位是“位”(bit),而不是“字节”(byte)。比如,说密钥长度是 128,就是 16 字节的二进制串,密钥长度 1024,就是 128 字节的二进制串。

对称加密

就是指加密和解密时使用的密钥都是同一个,是“对称”的。只要保证了密钥的安全,那整个通信过程就可以说具有了机密性。
image
TLS 里有非常多的对称加密算法可供选择,比如 RC4、DES、3DES、AES、ChaCha20 等,但前三种算法都被认为是不安全的,通常都禁止使用,目前常用的只有 AES 和 ChaCha20。

对称算法还有一个“分组模式”的概念,它可以让算法用固定长度的密钥加密任意长度的明文,把小秘密(即密钥)转化为大秘密(即密文)。

非对称加密

对称加密看上去好像完美地实现了机密性,但其中有一个很大的问题:如何把密钥安全地传递给对方,术语叫“密钥交换”

非对称加密两个密钥,一个叫“公钥”(public key),一个叫“私钥”(private key)。两个密钥是不同的,“不对称”,公钥可以公开给任何人使用,而私钥必须严格保密。

公钥和私钥有个特别的“单向”性,都可以用来加密解密,但公钥加密后只能用私钥解密,反过来,私钥加密后也只能用公钥解密。

非对称加密可以解决“密钥交换”的问题。网站秘密保管私钥,在网上任意分发公钥,你想要登录网站只要用公钥加密就行了,密文只能由私钥持有者才能解密。而黑客因为没有私钥,所以就无法破解密文。
image

混合加密

虽然非对称加密没有“密钥交换”的问题,但因为它们都是基于复杂的数学难题,运算速度很慢,即使是 ECC 也要比 AES 差上好几个数量级。

如果仅用非对称加密,虽然保证了安全,但通信速度有如乌龟、蜗牛,实用性就变成了零。

TLS 里使用的混合加密方式:在通信刚开始的时候使用非对称算法,比如 RSA、ECDHE,首先解决密钥交换的问题。然后用随机数产生对称算法使用的“会话密钥”(session key),再用公钥加密。因为会话密钥很短,通常只有 16 字节或 32 字节,所以慢一点也无所谓。
image

这样混合加密就解决了对称加密算法的密钥交换问题,而且安全和性能兼顾,完美地实现了机密性。

CSS盒模型

什么是盒模型

盒模型是包裹HTML元素的,由四个盒子组成:

  • content box:展示元素内容的区域。
  • padding box:清除内容周围的区域,是透明的。
  • border box:边框区域,包裹 content 和 padding 区域。
  • margin box:距离其他HTML元素的区域,也是透明的。

image

box-sizing

既然我们的元素布局是由四个盒子组成的,那么当我们设置 widthheight 属性时,是设置哪个盒子的尺寸呢?

语法:box-sizing: content-box | border-box

content-box

默认值,width 与 height 只包括内容的宽和高, 不包括边框,内边距,外边距。 width = content-box

.box{
   width: 100px;
   border: 10px solid #000;
   padding: 10px;
   margin: 10px;
   box-sizing: content-box;
}

此时的width只是content-box的宽度,所以元素的盒子总宽度 100+102+102+10*2 = 160px。

border-box

顾名思义,就是指元素高宽包含从 content-box 一直到 border-box。width = content-box + padding-box + border-box

.box{
   width: 100px;
   border: 10px solid #000;
   padding: 10px;
   margin: 10px;
   box-sizing: content-box;
}

此时盒子总宽度 100 + margin的 10*2 = 120px。

CSS 元素水平垂直居中

行内元素

text-align + vertical-align + :after,支持定高定宽或者非定高定宽。

<style>
  .parent {
    width: 400px;
    height: 400px;
    background-color: rosybrown;
    text-align: center;
  }
  .parent::after {
    content: '';
    display: inline-block;
    vertical-align: middle;
    height: 100%;
  }
  .child {
    vertical-align: middle;
    display: inline-block;
    height: 200px;
    width: 200px;
    background-color: red;
  }
</style>
<body>
  <div class="parent">
    <div class="child"></div>
  </div>
</body>

vertical-align: middle

块状元素

1、定高定宽 + 绝对定位 + margin:auto

.box {
  width: 400px;
  height: 400px;
  background-color: saddlebrown;
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  margin: auto;
}

2、定高定宽 + 绝对定位 + translate

.box {
  width: 400px;
  height: 400px;
  background-color: saddlebrown;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate3d(-200px, -200px, 0);
}

3、定高定宽 + 绝对定位 + margin反向偏移

.box {
  width: 400px;
  height: 400px;
  background-color: saddlebrown;
  position: absolute;
  top: 50%;
  left: 50%;
  margin-top: -200px;
  margin-left: -200px;
}

4、(非)定高定宽 + 父元素 flex 布局

.parent {
  display: flex;
  align-items: center;
  justify-content: center;
}

5、(非)定高定宽 + translate 负 50%

.box {
    background-color: saddlebrown;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
  }

6、(非)定高定宽 + grid布局

.parent {
  display: grid;
  height: 500px;
  width: 500px;
}
.box {
  background-color: saddlebrown;
  justify-self: center;
  align-self: center;
}

ES6 的类转换成 ES5 是什么样子?

前言

ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。

基本上,ES6 的class可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。

今天就来看下隐藏在 Class 背后的技术点。本次解析基于 Babel LOOSE 模式

  • constructor
  • 声明方法
  • getter、setter
  • static
  • 继承
  • super

constructor

class A {
  constructor(){
    this.name = 1;
  }
}

// 对应 

var A = function A() {
  this.name = 1;
};

constructor 的作用就是给实例对象添加属性。

声明方法

class A {
  sayName(){
  	console.log(this.name)
  } 
}

// 对应

var A = /*#__PURE__*/function () {
  function A() {}

  var _proto = A.prototype;

  _proto.sayName = function sayName() {
    console.log(this.name);
  };

  return A;
}();

可以看到,当我们在 class 上声明一个方法,其实是将方法挂载到原型上。如果我们通过箭头函数的方式声明呢?

class A {
  sayName = () => {}
}
// 对应
var A = function A() {
  this.sayName = function () {};
};

其实是放到了实例上,初始化时进行赋值。

getter、setter

class A {
  get age(){
  	return 27
  }
}

// 对应

var A = /*#__PURE__*/ (function () {
  function A() {}

  const props = [
    {
      key: "age",
      get: function get() {
        return 27;
      },
    },
  ];

  for (var i = 0; i < props.length; i++) {
    var descriptor = props[i];
    descriptor.enumerable = descriptor.enumerable || false;
    descriptor.configurable = true;
    if ("value" in descriptor) descriptor.writable = true;
    Object.defineProperty(A.prototype, descriptor.key, descriptor);
  }

  return A;
})();

可以看到,首先把 getter 函数转成一个 descriptor 对象,然后通过 Object.defineProperty 挂载到类的原型上,不能枚举

静态方法

class A {
  static sayName(){}
}
// 对应
var A = /*#__PURE__*/function () {
  function A() {}

  A.sayName = function sayName() {};

  return A;
}();

继承

class B {}

class A extends B {}
// 对应
var A = /*#__PURE__*/ (function (_B) {
  A.prototype = Object.create(_B.prototype);
  A.prototype.constructor = A;
  Object.setPrototypeOf(A, _B);

  function A() {
    var _this;

    _this = _B.call(this) || this;
    _this.name = 1;
    return _this;
  }

  return A;
})(B);

只使用 extends,什么都不做的情况下,会:

  • 将子类的 prototype 指向父类的 prototype。
  • 子类的 prototype.constructor 指向自己。
  • 子类的 proto 指向父类。
  • 调用父类初始化子类 this。

super

class B {}

class A extends B{
  constructor(){
  	super();
  }
  sayName(){
  	super.sayName()
  }
  static sayAge(){
    super.sayAge();
  }
}

// 对应

// 省略继承部分
function A() {
  return _B.call(this) || this;
}

var _proto = A.prototype;

_proto.sayName = function sayName() {
  _B.prototype.sayName.call(this);
};

A.sayAge = function sayAge() {
  _B.sayAge.call(this);
};

可以看到不同阶段的 super 不一样:

  • 在构造函数中,super 是父类构造函数。
  • 在方法中,super 是父类的原型链。
  • 在静态方法中,super 是父类本身。

总结

可以看出,其实 ES6 的实现和 ES5 中的寄生组合式继承差不多。

详解开窗函数

在做数据分析时,我们常常需要对数据进行去重,比如对某字段进行 group by,但是在 group by 以后,我们只能通过聚合函数去取其他字段的值,比如 AVG、COUNT、MAX 等等,而拿不到数据库的原始值。

假设有这样一张表

user_id version err_msg
1 1.1 no space
1 1.0 timeout
2 1.0 no space
2 1.1 permission deny

我们希望对用户去重,取出每个用户最新版本的错误信息,最后效果是这样的:

user_id version err_msg
1 1.1 no space
2 1.1 permission deny

如果只是聚合函数是完成不了的:

select * from 'table1' group by user_id

这样会报错,因为我们没有指定 version 和 err_msg 的聚合规则,改进一下

select MAX(version) from 'table1' group by user_id

可以得到:

user_id version
1 1.1
2 1.1

但是我们无法用聚合函数去取 err_msg 的值,因为字符串没有可比性。

这时就需要开窗函数出马了。

语法简介

row_number() over(partition by col1 order by col2)
dense_rank() over(partition by col1 order by col2)
rank() over(partition by col1 order by col2)

三个分析函数都是按照col1分组内从1开始排序

row_number()

是没有重复值的排序(即使两天记录相等也是不重复的),可以利用它来实现分页

dense_rank()

是连续排序,两个第二名仍然跟着第三名

rank()      

是跳跃排序,两个第二名下来就是第四名

开窗函数来实现

select *, row_number() over(partition by input1.`user_id` order by input1.`version` DESC) r from 
(select * from 'table1') input1

根据version倒序,最大的version排第一行,然后根据user_id分组,得到数据:

user_id version err_msg r
1 1.1 no space 1
1 1.0 timeout 2
2 1.1 permission deny 1
2 1.0 no space 2

最后,我们通过 r 就可以把需要的数据取出来了,
这里为了简便,把上方的临时表取别名 input2

select * from input2 where r = 1

最后得到:

user_id version err_msg r
1 1.1 no space 1
2 1.1 permission deny 1

clickhouse

由于 ch 不支持 rank 函数,所以需要曲线救国

我们来实现 row_number 的功能,直接上代码

新根据version排序

select * from 'table1' order by version desc

得到input1

user_id version err_msg
1 1.1 no space
2 1.1 permission deny
1 1.0 timeout
2 1.0 no space
select 
  groupArray(version) as version, 
  groupArray(err_msg) as err_msg,  
  arrayEnumerate(version) as r
from input1
group by user_id

得到input2

user_id version err_msg r
1 [1.1, 1.0] [no space, timeout] [1,2]
2 [1.1, 1.0] [permission deny, no space] [1,2]

接着将input2 展开

select * from input2 array join version, err_msg, r

得到 input3

user_id version err_msg r
1 1.1 no space 1
1 1.0 timeout 2
2 1.1 permission deny 1
2 1.0 no space 2

最后还是一样的:

select * from input3 where r = 1

得到:

user_id version err_msg r
1 1.1 no space 1
2 1.1 permission deny 1

Node.js 事件循环

什么是事件循环

事件循环是代理调用JS引擎执行JS代码的策略,它是一个在 JavaScript 引擎等待任务和执行任务之间转换的无限循环,与JS语言本身的无任何关系,代理可以是NodeJS,也可以是浏览器,NodeJS 是基于libuv实现的。

简单理解,事件循环就是JS主线程将耗时的任务交给代理去处理,代理处理完后放到数组中,等待主线程空闲后去取出执行,然后遇到耗时任务继续交给代理,这样无限循环。

为什么需要事件循环

NodeJS只有一个主线程,单机下只有一个主线程去处理所有的请求,这样遇到CPU密集的操作,会阻塞主线程,降低服务的性能。当有了事件循环,主线程遇到耗时的操作,就可以将其交给系统内核处理(系统是多线程的),然后继续处理后续的请求。

等待系统内核处理完后,将结果放在事件循环的队列中,待主线程空闲后,就去取出执行,这样就保证了服务的性能。

循环阶段的顺序和详情

耗时的任务有很多种,比如I/O,网络请求,timer等等,那么NodeJS中他们的执行顺序是怎样的呢?

下图显示了事件循环操作顺序的简化概述:

image

每个阶段都有一个要执行的回调FIFO队列。当事件循环进入给定阶段时,会在该阶段的队列中执行回调,直到队列耗尽或执行了最大数量的回调,事件循环将移动到下一个阶段,依此类推。

timers

本阶段执行已经被 setTimeout() 和 setInterval() 调度的回调函数,简单理解就是由这两个函数启动的回调函数。

pending callbacks

本阶段执行某些系统操作(如 TCP 错误类型)的回调函数。

idle、prepare

仅系统内部使用

poll

执行与 I/O 相关的回调。当进入到该阶段:

如果 poll 队列不是空的

会遍历同步执行队列中的回调函数,直到队列被清空或者执行到最大限制的数量。

如果 poll 队列是空的,但是 setImmediate 中有回调函数

会结束 poll 阶段,进入到 check阶段,执行回调函数。

当 poll 阶段和 check阶段都是空的,事件循环会停留在poll阶段等待io回调,并且监听timers阶段的回调函数是否到调度时间,
如果到了,就从timers里取出放到poll阶段执行。

check

setImmediate() 回调函数在这里执行,setImmediate 并不是立马执行,而是当事件循环 poll 中没有新的事件处理时就执行该部分。

close callbacks

执行一些关闭的回调函数,如 socket.on('close', ...)。

理解 process.nextTick()

该方法会将回调函数存到 nextTickQueue,当当前阶段结束后,立马执行 nextTickQueue 中回调函数,执行完后才会进入到下一个阶段。有可能会阻塞eventloop。

《透视HTTP协议》缓存控制

HTTP 缓存控制

缓存(Cache)是计算机领域里的一个重要概念,是优化系统性能的利器。

服务器的缓存控制

服务器标记资源有效期使用的头字段是“Cache-Control”,“Cache-Control”字段里的“max-age”和 Cookie 有点像,都是标记资源的有效期。

这里的 max-age 是“生存时间”,时间的计算起点是响应报文的创建时刻(即 Date 字段,也就是离开服务器的时刻),而不是客户端收到报文的时刻,也就是说包含了在链路传输过程中所有节点所停留的时间。

比如,服务器设定“max-age=5”,但因为网络质量很糟糕,等浏览器收到响应报文已经过去了 4 秒,那么这个资源在客户端就最多能够再存 1 秒钟,之后就会失效。

“max-age”是 HTTP 缓存控制最常用的属性,此外在响应报文里还可以用其他的属性来更精确地指示浏览器应该如何使用缓存:

  • no-store:不允许缓存,用于某些变化非常频繁的数据,例如秒杀页面;
  • no-cache:允许浏览器缓存,但在使用之前必须要去服务器验证是否过期,是否有最新的版本,有的话更新浏览器缓存,所以缓存不存在过期时间;
  • must-revalidate:又是一个和 no-cache 相似的词,它的意思是如果缓存不过期就可以继续使用,但过期了如果还想用就必须去服务器验证。

客户端的缓存控制

那么客户端缓存后,是否就依照服务器的缓存策略进行呢?

答案并不是,客户端也有自己的缓存策略,其实不止服务器可以发“Cache-Control”头,浏览器也可以发“Cache-Control”(只能发这个字段),也就是说请求 - 应答的双方都可以用这个字段进行缓存控制,互相协商缓存的使用策略。就算服务器设置了max-age,当客户端携带了max-age=0,也不会使用缓存。

比如说,服务器设置了max-age=30,此时过了5s秒,客户端发起了请求,max-age=0,就是说要最新的资源,此时缓存的资源过了5s了,所以不会用,而且去源站拿。

当你点“刷新”按钮的时候,浏览器会在请求头里加一个“Cache-Control: max-age=0”,返回200。

当你“强制刷新”的时候,浏览器会在请求头里加一个“Cache-Control: no-cache”,可能返回304或者200。

在“前进”“后退”“跳转”这些重定向动作中浏览器不会“夹带私货”,只用最基本的请求头,没有“Cache-Control”,所以就会检查缓存,直接利用之前的资源,不再进行网络通信。

条件请求

浏览器是如何跟服务器检查缓存是否有更新呢?

条件请求一共有 5 个头字段,我们最常用的是“if-Modified-Since”和“If-None-Match”这两个。其他三个是“If-Unmodified-Since”“If-Match”和“If-Range”。

需要第一次的响应报文预先提供“Last-modified”和“ETag”,然后第二次请求时就可以带上缓存里的原值,验证资源是否是最新的。

如果资源没有变,服务器就回应一个“304 Not Modified”,表示缓存依然有效,浏览器就可以更新一下有效期,然后放心大胆地使用缓存了。

ETag 是“实体标签”(Entity Tag)的缩写,是资源的一个唯一标识,主要是用来解决修改时间无法准确区分文件变化的问题。

比如,一个文件在一秒内修改了多次,但因为修改时间是秒级,所以这一秒内的新版本无法区分。再比如,一个文件定期更新,但有时会是同样的内容,实际上没有变化,用修改时间就会误以为发生了变化,传送给浏览器就会浪费带宽。使用 ETag 就可以精确地识别资源的变动情况,让浏览器能够更有效地利用缓存。

如果响应报文里提供了 Last-Modified,但没有 Cache-Controll 或者 Expires,浏览器会采用启发算法,计算一个缓存时间:(Date - Last-Modified)*10

浏览器一帧都做了什么?

屏幕显示原理

首先从过去的 CRT 显示器原理说起,CRT 的电子枪从帧缓冲区获取渲染的数据,然后从上到下一行行扫描,扫描完成后显示器就呈现【一帧画面】,随后电子枪回到初始位置继续下一次扫描。

而当一帧画面绘制完成后,电子枪回复到原位,准备画下一帧前,显示器会发出一个垂直同步信号,简称 VSync。

显示器通常以固定频率进行绘制完一帧,这个刷新率就是 VSync 信号产生的频率,一般为60赫兹,每次VSync间歇是16ms。

为了屏幕连续渲染不卡顿,那么当接收到VSync信号后,CPU+GPU计算图像并将放入帧缓冲区的时间需要控制在16ms内,

不然超过16ms还没计算出来,显示器拿不到要渲染的数据,就会跳过这次,造成了掉帧

所以浏览器也需要跟上VSync的速度,计算一帧的时间控制在16ms内,达到60FPS, 60HZ ~~~ 60FPS。

image

所以赫兹和FPS是两个东西,赫兹是显示器渲染频率,物理相关;FPS对于前端来说,就是执行JS,计算样式,合成等。

浏览器每一帧的组成

浏览器的帧都集中在CPU计算上,不包含GPU部分。

image

从图上可以看出,浏览器一帧是从渲染进程中的合成(Compositor)线程接收到 VSync 信号开始的。

第一步 Frame Start

Compositor线程收到VSync信号和输入事件(input data),并且将输入事件发送到主线程。

第二步 Input event handlers

主线程处理输入事件,所有的输入事件处理程序(touchmove, scroll, click)首先触发。

一般我们屏幕的帧率是每秒60帧,也就是60fps,但是某些事件触发的频率超过了这个数值,比如wheel,mousewheel,mousemove,pointermove,touchmove,这些连续性的事件一般每秒会触发60~120次,假如每一次触发事件都将事件发送到主线程处理,由于屏幕的刷新速率相对来说较低,这样使得主线程会触发过量的命中测试以及JS代码,使得性能有了没必要是损耗,浏览器会合并这些连续的事件,延迟到下一帧渲染时执行,也就是requestAnimationFrame之前。

我们的JS代码、回调、eventloop、microtasks就是在这个阶段执行,这个阶段是最容易耗时过长的,

第三步 RequestAnimationFrame

当你准备更新动画时你应该调用此方法。这将使浏览器在下一次重绘之前调用你传入给该方法的动画函数(即你的回调函数)。

第四步 Parse Html

如果DOM被修改了,就会执行这个阶段。

第五步 Reclaculate Styles

会对任何新添加或修改的内容进行计算。此过程是根据匹配选择器(例如 .headline 或 .nav > .nav__item)计算出哪些元素应用哪些 CSS 规则的过程。从中知道规则之后,将应用规则并计算每个元素的最终样式。

第六步 Layout

计算每个可见元素的几何信息(每个元素的位置和大小)。 通常是针对整个文档进行的,通常会使计算成本与DOM大小成正比。

第七步 Update Layer Tree

创建堆叠上下文和深度排序元素的过程。

第八步 Paint

布局 layout 之后,我们知道了不同元素的结构,样式,几何关系,我们要绘制出一个页面,我们要需要知道每个元素的绘制先后顺序,在绘制阶段,主线程会遍历布局树(layout tree),生成一系列的绘画记录。绘画记录可以看做是记录各元素绘制先后顺序的笔记。

第九步 Composite

由于页面的各部分可能被绘制到多层,由此它们需要按正确顺序绘制到屏幕上(进行分层),以便正确渲染页面;分层完成后,将图块信息传给Compositor线程。

2 到 9 步都是在主线程进行的,从图中从第六步到第九步都在Timeline中能很清楚的看见。

image

第十步 Roaster Scheduled and Rasterize

Compositor线程需要将图层切分为一块又一块的小图块(tiles),之后将这些小图块分别进行发送给一系列光栅线程(raster threads)进行光栅化,结束后光栅线程会将每个图块的光栅结果存在GPU Process的内存中。

第十一步 Frame End

随着各个图层的图块都被栅格化、任何新图块都将和输入数据(可能已在事件处理程序中被更改)一起被提交给GPU线程,然后GPU线程将图块上传到GPU,放入到帧缓冲区等待显示器进行下一次绘制。

到这里一帧基本上就做完,如果主线程还有剩余时间的话,就会执行 requestIdleCallback 处理一些优先级不高的事情。

Javascript 常见继承方式和优缺点

先将要用到的 Parent、Child 类写在前面。

function Parent(name) {
  this.hobbies = ['唱', '跳', 'rap'];
  this.name = name;
}

Parent.prototype.sayName = function () {
  console.log(this.name);
};

function Child(age) {
  this.age = age;
}

原型链继承

Child.prototype = new Parent('Larry');
const child = new Child(27);

缺点

  • 引用类型的属性被所有实例共享,比如 Parent 类的 hobbies 属性会被所有 Child 实例改变。
  • 实例化 Child 类时,不能向 Parent 传参。

借助构造函数继承

// 改写 Child 类
function Child(name, age) {
  Parent.call(this, name);
  this.age = age;
}
const child = new Child('Larry', 27);

优点

  • 避免类引用类型被共享。
  • 通过 call 方法,改变 Parent 构造函数的 this 指向。

缺点

  • 不能访问 Parent 类的原型属性。

组合继承

// 改写 Child 类
function Child(name, age) {
  Parent.call(this, name);
  this.age = age;
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;

优点

融合原型链继承和构造函数的优点,是 JavaScript 中最常用的继承模式。

缺点

  • 调用了两次 Parent 构造函数,Parent 实例属性同时存在于 Child 实例和原型链上,造成原型链污染。

寄生组合继承

通过这个方法可以解决组合继承的问题。

先介绍下寄生继承

创建一个仅用于封装继承过程的函数,该函数在内部以某种形式来做增强对象,最后返回对象。

function createObj(o) {
  const clone = Object.create(o);
  clone.sayName = function () {
    console.log('hi');
  };
  return clone;
}
Child.prototype = createObj(new Parent('Larry'));
const child = new Child(27);

寄生组合

function Child(name, age) {
  Parent.call(this, name);
  this.age = age;
}
const prototype = createObj(Parent.prototype);
prototype.constructor = Child;
Child.prototype = prototype;

什么是重排?

参考谷歌开发者文档,通过自己的语言重新描述。

什么是重排

浏览器的一帧主要做了这件事情:
image
重排其实就是图中的第三步 LayoutLayout 是浏览器计算各元素几何信息的过程:元素的大小以及在页面中的位置。
根据所用的 CSS、元素的内容或父级元素,每个元素都将有显式或隐含的大小信息。

记住这几个阶段很重要

为什么要避免重排

Layout 几乎总是作用到整个文档。 如果有大量元素,将需要很长时间来算出所有元素的位置和尺寸。

常见引起重排的问题

强制同步布局

JavaScript 阶段强制浏览器提前执行 Layout。这被称为强制同步布局

// 将JS函数放到一帧的最开始运行
requestAnimationFrame(logBoxHeight);

function logBoxHeight() {
  console.log(box.offsetHeight);
}

在示例中,目前正在 JavaScript 阶段执行JS,这样写没什么问题,不会引起重排,因为来自上一帧的所有旧布局值是已知的。

现在我们来改下logBoxHeight这个函数

function logBoxHeight() {
  box.classList.add('super-big');
  // 输出元素的offsetHeight之前,为元素添加了新的类
  console.log(box.offsetHeight);
}

现在,为了回答高度问题,浏览器必须先应用样式更改(由于增加了 super-big 类)Style,然后运行 Layout,这时它才能返回正确的高度。这是不必要的,把一帧的时间拉长了,可能导致掉帧卡顿

正确的方式应该是:

function logBoxHeight() {
  // 先输出上一帧的offsetHeight
  console.log(box.offsetHeight);
  // 再添加新的类,就不会出发强制同步布局了
  box.classList.add('super-big');
}

大部分情况下,并不需要应用样式然后查询值;使用上一帧的值就足够了。

如何避免重排

样式集中改变

批量的修改CSS样式,因为通过设置style属性改变结点样式的话,每一次设置都会触发一次reflow,所以最好是使用class属性

// bad
var left = 10;
var top = 10;
el.style.left = left + "px";
el.style.top = top + "px";

// 当top和left的值是动态计算而成时...
// better 
el.style.cssText += "; left: " + left + "px; top: " + top + "px;";

// better
el.className += " className";

读写分离

对元素的修改,先统一取值,再统一赋值

// bad 强制刷新 触发四次重排+重绘
div.style.left = div.offsetLeft + 1 + 'px';
div.style.top = div.offsetTop + 1 + 'px';
div.style.right = div.offsetRight + 1 + 'px';
div.style.bottom = div.offsetBottom + 1 + 'px';

// good 缓存布局信息 相当于读写分离 触发一次重排+重绘
var curLeft = div.offsetLeft;
var curTop = div.offsetTop;
var curRight = div.offsetRight;
var curBottom = div.offsetBottom;

div.style.left = curLeft + 1 + 'px';
div.style.top = curTop + 1 + 'px';
div.style.right = curRight + 1 + 'px';
div.style.bottom = curBottom + 1 + 'px';

脱离文档流

使用绝对定位会使的该元素单独成为渲染树中 body 的一个子元素,重排开销比较小,不会对其它节点造成太多影响。当你在这些节点上放置这个元素时,一些其它在这个区域内的节点可能需要重绘,但是不需要重排。

硬件加速

通过设置will-change、translate等CSS属性,将元素提为单独图层,启动硬件加速,不会影响到其他节点。

《JavaScript函数式编程指南》

  • 函数式编程目标是使用函数来抽象作用在数据之上的控制流与操作。
  • 将函数看作永不会修改数据的闭合功能单一,必然可以减少潜在的bug。
  • 函数式编程是指为创建不可变的程序,通过消除外部可见的副作用,来对纯函数的声明式的求值过程。
  • 当考虑设计应用时,你应该问问自己:
    • 可扩展性:我是否需要不断的重构代码来支持额外的功能?
    • 易模块化:如果我更改了一个文件,另外一个文件会不会收到影响?
    • 可重用性:是否有很多重复的代码?
    • 可测性:给这些函数添加单元测试是否困难?
    • 易推理性:我写的代码是否非结构化严重并难以推理?
  • 在函数式编程中,要做到不可变的思考,就需要将任何对象视为数值,类似于字符串和数字,这样做函数可以把对象传来传去,而不用担心被串改。
  • 函数式的控制流能够在不需要研究任何内部细节的条件下提供该程序意图的清晰结构,这样就能更深刻地了解代码,并获知数据在不同阶段是如何流入和流出的。
  • 面向对象大多是命令式的,因此在很大程度上依赖于使用基于对象的封装来保护其自身和继承的可变状态的完整性,再通过实例方法来暴露或修改这些状态,其结果是,对象的数据与行为以一种内聚的形式紧耦合在一起。而函数式编程中,一切都是不可变,数据和行为是松耦合的。
  • 方法链的缺点是由于方法与所属的对象紧密耦合在一起,也就限制了链中可以使用的方法数量,不能轻松的将不同函数连接在一起。
  • 以point-free的风格编写,并用函数组合子来组织的程序控制流,可解决现实问题。

React.Lazy 懒加载解析

为什么需要懒加载

这里摘抄下 React 官网的

打包是个非常棒的技术,但随着你的应用增长,你的代码包也将随之增长。尤其是在整合了体积巨大的第三方库的情况下。你需要关注你代码包中所包含的代码,以避免因体积过大而导致加载时间过长。

为了避免搞出大体积的代码包,在前期就思考该问题并对代码包进行分割是个不错的选择。 代码分割是由诸如 Webpack,Rollup 和 Browserify(factor-bundle)这类打包器支持的一项技术,能够创建多个包并在运行时动态加载。

对你的应用进行代码分割能够帮助你“懒加载”当前用户所需要的内容,能够显著地提高你的应用性能。尽管并没有减少应用整体的代码体积,但你可以避免加载用户永远不需要的代码,并在初始加载的时候减少所需加载的代码量。

先来看下如何使用的

import React, { Suspense } from 'react';

const OtherComponent = React.lazy(() => import('./OtherComponent'));

function MyComponent() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <OtherComponent />
      </Suspense>
    </div>
  );
}

原理解析

当 Webpack 遇到 import() 方法时,会自动进行代码分割,返回一个 Promise 对象,Webpack 这块感兴趣的看 这里

一开始,我们会调用 React.lazy 这个方法,传入一个函数:

export function lazy(ctor) {
  let thenable = null;
  return {
    then(resolve, reject) {
      if (thenable === null) {
        thenable = ctor();
        ctor = null;
      }
      return thenable.then(resolve, reject);
    },
    _reactStatus: -1,
    _reactResult: null,
  };
}

这个方法主要返回了一个对象,包含一个 then 方法,_reactStatus 值为 -1,_reactResult 为 null。

然后在 ReactDOM 构建 fiber 树的过程中,会执行 createFiberFromElement 方法:

function createFiberFromElement(element){
   const type = element.type;
  if (typeof type === 'object' && type !== null) {
   switch (type.$$typeof) {
    case REACT_PROVIDER_TYPE:
      fiberTag = ContextProvider;
      break getTag;
    case REACT_CONTEXT_TYPE:
      // This is a consumer
      fiberTag = ContextConsumer;
      break getTag;
    case REACT_FORWARD_REF_TYPE:
      fiberTag = ForwardRef;
      break getTag;
    default: {
      if (typeof type.then === 'function') {
        fiberTag = IndeterminateComponent;
        break getTag;
      }
    }
  }
}

这里设置这个 fiber 节点的类型是 IndeterminateComponent,接着 React 会执行 beginWork:

function beginWork(){
  case IndeterminateComponent: {
      const Component = workInProgress.type;
      return mountIndeterminateComponent(
        current,
        workInProgress,
        Component,
        renderExpirationTime,
      );
    }
}

beginWork 里判断了节点类型是 IndeterminateComponent,就会执行 mountIndeterminateComponent

function mountIndeterminateComponent(
  current,
  workInProgress,
  Component,
  renderExpirationTime
) {
    Component = readLazyComponentType(Component);
    const resolvedTag = (workInProgress.tag = resolveLazyComponentTag(
      workInProgress,
      Component,
    ));
}

这里执行的 readLazyComponentType 是最关键的:

export function readLazyComponentType<T>(thenable: Thenable<T>): T {
  const status = thenable._reactStatus;
  switch (status) {
    case Resolved:
      const Component: T = thenable._reactResult;
      return Component;
    case Rejected:
      throw thenable._reactResult;
    case Pending:
      throw thenable;
    default: {
      thenable._reactStatus = Pending;
      thenable.then(
        resolvedValue => {
          if (thenable._reactStatus === Pending) {
            thenable._reactStatus = Resolved;
            if (typeof resolvedValue === 'object' && resolvedValue !== null) {
              // If the `default` property is not empty, assume it's the result
              // of an async import() and use that. Otherwise, use the
              // resolved value itself.
              const defaultExport = (resolvedValue: any).default;
              resolvedValue =
                defaultExport !== undefined && defaultExport !== null
                  ? defaultExport
                  : resolvedValue;
            } else {
              resolvedValue = resolvedValue;
            }
            thenable._reactResult = resolvedValue;
          }
        },
        error => {
          if (thenable._reactStatus === Pending) {
            thenable._reactStatus = Rejected;
            thenable._reactResult = error;
          }
        },
      );
      throw thenable;
    }
  }
}

一开始,_reactStatus 是 -1,所以会进入 default 的情况,这里会调用 then 方法,会发起异步请求下载chunk,并且抛出一个 thenable 的错误,_reactStatus 为 Pending。

由于 readLazyComponentType throw 了一个错误,所以 mountIndeterminateComponent 终止。

这时 Suspense 组件就起作用了,当监听到错误后,就将 fallback 的值展示出来,来看个 Suspense 组件简单的实现:

class Suspense extends React.Component {
  state = {
    promise: null
  }

  componentDidCatch(e) {
    if (e instanceof Promise) {
      this.setState({
        promise: e
      }, () => {
        e.then(() => {
          this.setState({
            promise: null
          })
        })
      })
    }
  }

  render() {
    const { fallback, children } = this.props
    const { promise } = this.state
    return <>
      { promise ? fallback : children }
    </>
  }
}

当异步请求完全后,lazy 组件对应的 fiber 节点自身状态变为 Resolved,并且给属性 _reactResult 赋值。

此时 组件会重新渲染,然后又走进 mountIndeterminateComponent 方法,此时 readLazyComponentType 直接返回 resolve,mountIndeterminateComponent 就会继续往后执行,构建 DOM 树了。

详解 TCP 协议

TCP 协议

TCP是一个传输层协议,提供端到端的可靠传输,支持是全双工,是一个连接导向的协议。

TCP 包头格式

image

端口号

由于TCP协议上层是应用层,应用通过端口号区分,所以会携带端口号。

序号、确认序号

由于不能保证TCP包是顺序到达的,但要组装成有序的,所以需要序号;如果知道对方是否收到我发的包,就需要确认序号出马。

标示位

TCP是面向链接的,所以需要两端来维护链接的状态,标示位用于描述 TCP 段的行为。

每个标示位占了一个比特,可以混合使用。比如 ACK 和 SYN 同时为 1,代表同步请求和响应被合并了。这也是 TCP 协议,为什么是三次握手的原因之一。

窗口大小

根据对方窗口大小,也就是处理数据的能力,用于控制发送包的大小。

MSS

Maxiumun Segment Size,可选项,这个可选项控制了TCP段的大小,它是一个协商字段。

三次握手

状态时序图
image

握手过程

  • 客户端和服务端状态都是 CLOSED
  • 服务器端启动,监听某个端口,状态变为 LISTEN
  • 客户端发SYN给服务端,客户端变成 SYN_SNET
  • 服务端收到后,ACK 客户端的 SYN,表示收到了;并且也给客户端发 SYN,测试客户端能否收到。服务端状态变为 SYN_RCVD
  • 客户端收到SYN和ACK后,对SYN 进行 ACK,并且状态变为 ESTABLISHED,因为一发一收完成了。
  • 服务端收到客户端的ACK,并且状态变为 ESTABLISHED,因为一发一收完成了。

为什么是三次,不是二次

因为TCP是全双工的,所以要保证两边都知道和对方的链接都是通的。如果只是两次,有一方就收不到另外一方的ACK。

握手的用意

三次握手不止为了建立链接,还确认了 TCP包的序号的问题。

四次挥手

状态时序图
image

挥手过程

  • 客户端和服务端状态都是 ESTABLISHED
  • 客户端发送 FIN,表示发起断开链接的请求,状态变为 FIN_WAIT1
  • 服务端收到 FIN,返回 ACK,表示我收到了,但还有数据没发完,等一下,服务器状态变为 CLOSE_WAIT
  • 客户端收到后,状态变为 FIN_WAIT2,等待服务端处理。
  • 服务端处理完后,发送一个FIN,告诉客户端我处理完了,状态变为 LAST_ACK
  • 客户端收到FIN后,返回一个ACK,告诉服务端,我知道了,我再等待一会,你可以断开了,客户端状态变为TIME_WAIT
  • 服务器端收到ACK后,状态进入**CLOSE
  • 客户端等待2个MSL后,没有收到服务端的包了,说明服务端已经成功CLOSE了,然后自己状态也变为CLOSE

为什么要等待2个MSL

MSL是任何报文在网络上存在的最长时间;客户端之所以要等待,是因为客户端收到FIN后,返回ACK给服务端时,服务端就进入CLOSE的状态了,不会在恢复任何报文,所以客户端需要假设ACK的包发送失败,服务端可以会再次发FIN的包给自己,所以需要等待。

TCP稳定性

滑动窗口

TCP 中每个发送的请求都需要响应。如果一个请求没有收到响应,发送方就会认为这次发送出现了故障,会触发重发。
image
但是一发一收的效率太低了,发送方需要等待,所以最后是一次性发出,慢慢等响应。
image
所以我们需要一种机制,来记录TCP段的发送情况

image

  • 深绿色:已发送已确认
  • 浅蓝色:已发送未确认
  • 白色:可以发送还没发送
  • 紫色:暂时不能发送。

实际操作中,每个 TCP 段的大小不同,限制数量会让接收方的缓冲区不好操作,因此实际操作中窗口大小单位是字节数。

超时重试

对每一个发送了,但是没有 ACK 的包,都有设一个定时器,超过了一定的时间,就重新尝试。

快速重传

例如段 1、段 2、段 4 到了,但是段 3 没有到。 接收方可以发送多次段 3 的 ACK。如果发送方收到多个段 3 的 ACK,就会重发段 3。这个机制称为快速重传。这和超时重发不同,是一种催促的机制。

为了不让发送方误以为段 3 已经收到了,在快速重传的情况下,接收方即便收到发来的段 4,依然会发段 3 的 ACK(不发段 4 的 ACK),直到发送方把段 3 重传。

流量控制

当接收方处理不过来后,可以通过确认信息修改窗口的大小,甚至设置为0,则发送方将暂时停止发送。
如果接收端还是一直不处理数据,则随着确认的包越来越多,窗口越来越小,直到为 0,发送方也停止发送。

如果这样的话,发送方会定时发送窗口探测数据包,看是否有机会调整窗口的大小。

拥塞控制

流量控制是担心发送方把接收方的缓存塞满了,但是如果接收方的处理能力很强,我们如何尽可能的发送更多的数据,最大限度的利用带宽,这就是拥塞控制做的事情。

发的太少,浪费带宽;发的太多,包丢失,重传导致效率低。

慢启动

要一开始慢慢的倒,然后发现总能够倒进去,就可以越倒越快。这叫作慢启动。
接着就指数增长,1到2,2到4,4到8。有一个值 ssthresh 为 65535 个字节,当超过这个值的时候,就要小心一点
了,不能倒这么快了,可能快满了,再慢下来。

React 事件系统原理

浏览器事件系统

事件 是某事发生的信号。所有的 DOM 节点都生成这样的信号(但事件不仅限于 DOM)。常见的事件有 click、keydown 等。
通过事件捕获器我们可以分配一个处理程序给对应的信号,使得浏览器和 JS 可以进行交互。当事件发生时,浏览器会创建一个 事件对象,将详细信息放入其中,并将其作为参数传递给处理程序。

React 事件系统

React 的事件系统是基于浏览器的事件机制下完全重写的,在使用上,和 DOM 元素的很相似,有几点区别:

  • React 事件的命名采用小驼峰式(camelCase),而不是纯小写。
  • 使用 JSX 语法时你需要传入一个函数作为事件处理函数,而不是一个字符串。
  • 在 React 中另一个不同点是你不能通过返回 false 的方式阻止默认行为。你必须显式的使用 preventDefault 。

但内在设计完全不一样,比如事件对象采用的是合成事件;事件全部挂载到 document 节点上,通过冒泡进行触发。

原生事件(阻止冒泡)会阻止合成事件的执行,合成事件(阻止冒泡)不会阻止原生事件的执行。

React 为什么需要合成事件

  • 减少内存消耗,提升性能,不需要注册那么多的事件了,一种事件类型只在 document 上注册一次。
  • 统一规范,解决 ie 事件兼容问题,简化事件逻辑。
  • 跨端复用。

比如当我们给 input 元素增加 onChange 事件,React 其实还帮我们注册了很多事件,比如 keyDown、keyUp、blur 等,使得我们在向文本框输入内容的是你,是可以实时的得到内容的。

然而原生只注册一个onchange的话,需要在失去焦点的时候才能触发这个事件,所以这个原生事件的缺陷react也帮我们弥补了。

react在给document注册事件的时候也是对兼容性做了处理。

function listen(target, eventType, callback) {
  if (target.addEventListener) {
    target.addEventListener(eventType, callback, false);
    return {
      remove: function remove() {
        target.removeEventListener(eventType, callback, false);
      }
    };
  } else if (target.attachEvent) {
    target.attachEvent('on' + eventType, callback);
    return {
      remove: function remove() {
        target.detachEvent('on' + eventType, callback);
      }
    };
  }
}

React 事件系统运转

事件注册

image

finalizeInitialChildren 中,会调用 setInitialProperties,给最后要渲染的DOM对象设置属性,对于受控组件,会调用 ensureListeningTo。

// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange');

function ensureListeningTo(rootContainerElement, registrationName) {
  const isDocumentOrFragment =
    rootContainerElement.nodeType === DOCUMENT_NODE ||
    rootContainerElement.nodeType === DOCUMENT_FRAGMENT_NODE;
  const doc = isDocumentOrFragment
    ? rootContainerElement
    : rootContainerElement.ownerDocument;
  listenTo(registrationName, doc);
}

可以看到,这里选择的 DOM 元素就是 DOCUMENT_NODE,和我们之前说的一样。

然后 listenTo 中会调用 trapCapturedEvent 这个方法,会调用 addEventLisener 监听一个 dispatchEvent 方法。

export function trapCapturedEvent(
  topLevelType: DOMTopLevelEventType,
  element: Document | Element,
) {
  if (!element) {
    return null;
  }
  const dispatch = isInteractiveTopLevelEventType(topLevelType)
    ? dispatchInteractiveEvent
    : dispatchEvent;

  addEventCaptureListener(
    element,
    getRawEventName(topLevelType),
    // Check if interactive and wrap in interactiveUpdates
    dispatch.bind(null, topLevelType),
  );
}

通过上面就完成了注册声明的回调,在 react 里所有事件的触发都是通过 dispatchEvent方法统一进行派发的,而不是在注册的时候直接注册声明的回调。

事件分发

在 react 里所有事件的触发都是通过 dispatchEvent方法统一进行派发的,我们先看 dispatchEvent 这个方法:

export function dispatchEvent(
  topLevelType: DOMTopLevelEventType,
  nativeEvent: AnyNativeEvent,
) {
  if (!_enabled) {
    return;
  }

  const nativeEventTarget = getEventTarget(nativeEvent);
  let targetInst = getClosestInstanceFromNode(nativeEventTarget);
  if (
    targetInst !== null &&
    typeof targetInst.tag === 'number' &&
    !isFiberMounted(targetInst)
  ) {
    // If we get an event (ex: img onload) before committing that
    // component's mount, ignore it for now (that is, treat it as if it was an
    // event on a non-React tree). We might also consider queueing events and
    // dispatching them after the mount.
    targetInst = null;
  }

  const bookKeeping = getTopLevelCallbackBookKeeping(
    topLevelType,
    nativeEvent,
    targetInst,
  );

  try {
    // Event queue being processed in the same cycle allows
    // `preventDefault`.
    batchedUpdates(handleTopLevel, bookKeeping);
  } finally {
    releaseTopLevelCallbackBookKeeping(bookKeeping);
  }
}

方法中调用了 batchedUpdates(handleTopLevel, bookKeeping),handleTopLevel 会遍历找出所有的父节点,并调用 runExtractedEventsInBatch 中的 extractEvents 生成合成事件,最后调用 runEventsInBatch 执行。

export function runExtractedEventsInBatch(
  topLevelType: TopLevelType,
  targetInst: null | Fiber,
  nativeEvent: AnyNativeEvent,
  nativeEventTarget: EventTarget,
) {
  const events = extractEvents(
    topLevelType,
    targetInst,
    nativeEvent,
    nativeEventTarget,
  );
  runEventsInBatch(events, false);
}

生成合成事件之后,会调用 accumulateTwoPhaseDispatches(event),该方法最终会调用 traverseTwoPhase。

/**
 * Simulates the traversal of a two-phase, capture/bubble event dispatch.
 */
export function traverseTwoPhase(inst, fn, arg) {
  const path = [];
  while (inst) {
    path.push(inst);
    inst = getParent(inst);
  }
  let i;
  for (i = path.length; i-- > 0; ) {
    fn(path[i], 'captured', arg);
  }
  for (i = 0; i < path.length; i++) {
    fn(path[i], 'bubbled', arg);
  }
}

模拟过程中会把所有事件监听函数及其对应的节点都加入到 event(合成事件) 的属性中。

function accumulateDirectionalDispatches(inst, phase, event) {
  if (__DEV__) {
    warningWithoutStack(inst, 'Dispatching inst must not be null');
  }
  const listener = listenerAtPhase(inst, event, phase);
  if (listener) {
    event._dispatchListeners = accumulateInto(
      event._dispatchListeners,
      listener,
    );
    event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
  }
}

最后依次执行所有的事件:

export function runEventsInBatch(events) {
  if (events !== null) {
    eventQueue = accumulateInto(eventQueue, events);
  }

  // Set `eventQueue` to null before processing it so that we can tell if more
  // events get enqueued while processing.
  const processingEventQueue = eventQueue;
  eventQueue = null;

  if (!processingEventQueue) {
    return;
  }

  forEachAccumulated(
    processingEventQueue,
    executeDispatchesAndReleaseTopLevel,
  );
}

在 executeDispatchesAndReleaseTopLevel 中,会判断合成事件是否 isPersistent,不是的话会释放

const executeDispatchesAndRelease = function(
  event: ReactSyntheticEvent,
  simulated: boolean,
) {
  if (event) {
    executeDispatchesInOrder(event, simulated);

    if (!event.isPersistent()) {
      event.constructor.release(event);
    }
  }
};

这也就是为什么当我们在需要异步读取操作一个合成事件对象的时候,需要执行 event.persist(),不然 React 就会释放掉。

React 17 里已经取消了事件池,无需再执行 e.persist()

《透视HTTP协议》CDN

CDN

外部加速 HTTP 协议的服务,CDN(Content Delivery Network 或 Content Distribution Network),中文名叫“内容分发网络”。

为什么要有网络加速?

光速是有限的,虽然每秒 30 万公里,但这只是真空中的上限,在实际的电缆、光缆中的速度会下降到原本的三分之二左右,也就是 20 万公里 / 秒,这样一来,地理位置的距离导致的传输延迟就会变得比较明显了。

另外不要忘了, 互联网从逻辑上看是一张大网,但实际上是由许多小网络组成的,这其中就有小网络“互连互通”的问题,典型的就是各个电信运营商的网络,比如国内的电信、联通、移动三大家。这些小网络内部的沟通很顺畅,但网络之间却只有很少的联通点。如果你在 A 网络,而网站在 C 网络,那么就必须“跨网”传输,和成千上万的其他用户一起去“挤”连接点的“独木桥”。而带宽终究是有限的,能抢到多少只能看你的运气。

网络中还存在许多的路由器、网关,数据每经过一个节点,都要停顿一下,在二层、三层解析转发,这也会消耗一定的时间,带来延迟。

什么是CDN?

CDN 的最核心原则是“就近访问”,如果用户能够在本地几十公里的距离之内获取到数据,那么时延就基本上变成 0 了。

所以 CDN 投入了大笔资金,在全国、乃至全球的各个大枢纽城市都建立了机房,部署了大量拥有高存储高带宽的节点,构建了一个专用网络。这个网络是跨运营商、跨地域的,虽然内部也划分成多个小网络,但它们之间用高速专有线路连接,是真正的“信息高速公路”,基本上可以认为不存在网络拥堵。

有了这个高速的网路,CDN 就要“分发”源站的“内容”了,用到的“缓存代理”技术。使用“推”或者“拉”的手段,把源站的内容逐级缓存到网络的每一个节点上。

于是,用户在上网的时候就不直接访问源站,而是访问离他“最近的”一个 CDN 节点,术语叫“边缘节点”(edge node),其实就是缓存了源站内容的代理服务器,这样一来就省去了“长途跋涉”的时间成本,实现了“网络加速”。
image

CDN 的负载均衡

全局负载均衡(Global Sever Load Balance)一般简称为 GSLB,它是 CDN 的“大脑”,主要的职责是当用户接入网络的时候在 CDN 专网中挑选出一个“最佳”节点提供服务,解决的是用户如何找到“最近的”边缘节点,对整个 CDN 网络进行“负载均衡”。

原来没有 CDN 的时候,权威 DNS 返回的是网站自己服务器的实际 IP 地址,浏览器收到 DNS 解析结果后直连网站。

但加入 CDN 后就不一样了,权威 DNS 返回的不是 IP 地址,而是一个 CNAME( Canonical Name ) 别名记录,指向的就是 CDN 的 GSLB。它有点像是 HTTP/2 里“Alt-Svc”的意思,告诉外面:“我这里暂时没法给你真正的地址,你去另外一个地方再查查看吧。”

因为没拿到 IP 地址,于是本地 DNS 就会向 GSLB 再发起请求,这样就进入了 CDN 的全局负载均衡系统,开始“智能调度”,主要的依据有这么几个:

  • 看用户的 IP 地址,查表得知地理位置,找相对最近的边缘节点;
  • 看用户所在的运营商网络,找相同网络的边缘节点;
  • 检查边缘节点的负载情况,找负载较轻的节点;
  • 其他,比如节点的“健康状况”、服务能力、带宽、响应时间等。

CDN 的缓存代理

缓存系统是 CDN 的另一个关键组成部分,相当于 CDN 的“心脏”。如果缓存系统的服务能力不够,不能很好地满足用户的需求,那 GSLB 调度算法再优秀也没有用。

但互联网上的资源是无穷无尽的,不管 CDN 厂商有多大的实力,也不可能把所有资源都缓存起来。所以,缓存系统只能有选择地缓存那些最常用的那些资源。

这里就有两个 CDN 的关键概念:“命中”和“回源”。

“命中”就是指用户访问的资源恰好在缓存系统里,可以直接返回给用户;

“回源”则正相反,缓存里没有,必须用代理的方式回源站取。

也就有了两个衡量 CDN 服务质量的指标:“命中率”和“回源率”。

Proxy vs Object.defineProperty

前言

这两个方法主要通过数据劫持来进行依赖收集,典型的应用有Vue和MobX,Vue源码实现可以参考 这里

Object.defineProperty

由 ES5 提供,该方法可以在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。

用法:Object.defineProperty(obj, prop, descriptor)

参数:

  • obj:要定义属性的对象。
  • prop:要定义或修改的属性的名称或 Symbol 。
  • descriptor:要定义或修改的属性描述符。
    • configurable {boolean} 该属性的描述符是否能够被改变,默认值false
    • enumerable {boolean} 该属性是否能被枚举,默认值false
    • writable {boolean} value是否能被修改,默认值false
    • value {any} 该属性的值
    • get {function} 当访问该属性时,会调用此函数,默认值undefined
    • set {function} 当修改该属性时,会调用此函数,默认值undefined

注意,value和writable是一组,get和set是一组,不能交叉使用,否则会报错

使用方法

value和writable

var obj = {}
Object.defineProperty(obj, "key", {
  value: "static"
});

/// 相当于
var obj = {}
Object.defineProperty(obj, "key", {
  configurable: false,
  enumerable: false,
  writable: false,
  value: "static"
});

get和set

var obj = {}

let key = 'static';

Object.defineProperty(obj, "key", {
  configurable: false,
  enumerable: false,
  get(){ return key },
  set(newVal){ key = newVal },
});

监听每个属性的读写

var obj = {
  name: 'Larry',
  age: 27
}

// 用于存放真实数据
var _obj = {...obj}

for(let i in obj){
  let value = obj[i];
  Object.defineProperty(obj, i, {
    configurable: true,
    enumerable: true,
    get(){ 
      console.log('get key:' + i)
      return _obj[i]
    },
    set(newVal){ 
      console.log('set key:' + i)
      _obj[i]  = newVal 
    },
  })
}

defineProperty缺点

  • 只能监听对象已有属性,动态增加的属性需要重新监听(vue.set)
  • 不能监听数组的变化(vue可以是因为重写了push等方法,但是依然监听不了length变化和修改arr[x]的值)

Proxy

由 ES6 提供,Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。

  • Proxy 可以直接监听对象而非属性
  • Proxy 可以直接监听数组的变化,比如直接修改数组 length 以及 arr[x] = xxx
  • Proxy 有多达 13 种拦截方法,不限于 apply、ownKeys、deleteProperty、has 等等是 Object.defineProperty 不具备的。

用法:const p = new Proxy(target, handler)

参数:

  • target:要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
  • handler:一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。
    • handler.get(target, property, receiver):方法用于拦截对象的读取属性操作。
      • target:目标对象。
      • property:被获取的属性名。
      • receiver:Proxy或者继承Proxy的对象
    • handler.set(target, property, value, receiver):方法用于拦截对象的读取属性操作。
      • value:新属性值。

使用方法

getter

var p = new Proxy({}, {
  get: function(target, prop, receiver) {
    console.log("called: " + prop);
    return 10;
  }
});
console.log(p.a); // a是新属性,也触发了getter,"called: a", 10

如果要访问的目标属性是不可写和不可配置的,则返回的值必须与该目标属性的值相同。

var obj = {};
Object.defineProperty(obj, "a", {
  configurable: false,
  enumerable: false,
  value: 10,
  writable: false
});

var p = new Proxy(obj, {
  get: function(target, prop) {
    return 20;
  }
});

p.a; //会抛出TypeError

setter

var p = new Proxy({}, {
  set: function(target, prop, value, receiver) {
    target[prop] = value;
    console.log('property set: ' + prop + ' = ' + value);
    return true;
  }
})

实现一个依赖收集, MobX的autorun

const data = {
    name: 'Larry'
}

const observers = new Map();

const proxy = new Proxy({}, {
    get(target, prop) {
        if (runningFunc) {
            if (observers.has(prop)) {
                const deps = observers.get(prop);
                deps.push(runningFunc);
            } else {
                observers.set(prop, [runningFunc])
            }
        }
        return data[prop]
    },
    set(target, prop, value) {
        data[prop] = value;
        const deps = observers.get(prop);
        deps.forEach(dep => dep())
    }
})

let runningFunc;
function autorun(func) {
    runningFunc = func;
    func();
}

// 初始化执行依次,设置runningFunc
autorun(() => {
    console.log(proxy.name); // �触发getter,
    console.log('autorun executed')
})

setTimeout(() => {
    proxy.name = 'lingxiao';  // 触发setter
}, 2000)

/**
 * 依次输出:
 * Larry
 * autorun executed
 * lingxiao
 * autorun executed
 * */

浏览器多进程架构

常见进程

浏览器进程

浏览器的主进程(负责协调、主控),只有一个。作用:

  • 负责浏览器界面显示,与用户交互。如前进,后退等
  • 负责各个页面的管理,创建和销毁其他进程
  • 将Renderer进程得到的内存中的Bitmap,绘制到用户界面上
  • 网络资源的管理,下载等

为满足上面的功能,对应有以下线程:

  • UI thread:控制浏览器上的按钮及输入框;
  • Network thread:处理网络请求,从网上获取数据;
  • Storage thread:控制文件等的访问;

当资源充足时,Network、Storage可以作为单独的进程。

GPU进程

这是为浏览器所有标签页和周边进程提供服务的单个进程。
随着帧的提交,GPU进程会将任何图块和其他数据(如四边形顶点和矩阵)上传到GPU,以实际将像素推送到屏幕。
GPU进程包含一个单独的线程,称为GPU线程,它实际上完成了工作。

渲染进程

每一个Tab页面就是一个渲染进程,作用就是渲染HTML页面,有多个。有以下线程:

  • Main Thread:负责解析HTML文件、执行JS代码、计算图像布局信息、合成图层。
  • Composite Thread:负责接收信号、生成合成帧,并将合成帧提交给CPU进程。
  • Raster Thread:负责将图层信息转化为显示器中的像素。
  • Worker Thread:弥补JS单线程计算能力差的缺点,用于计算。

插件进程

每种类型的插件对应一个进程,仅当使用该插件时才创建,有多个。

可以通过 Chrome 浏览器的 窗口 -》任务管理器 进行查看。

进程之间如何协作的

以输入URL访问页面为例。

  • 用户输入URL,浏览器进程调用 UI thread 进行将当前Tab设置为 loading 状态。
  • UI thread 通知 Network thread 进行资源请求,包含DNS解析、建立TCP链接等。
  • Network thread接收到服务器的响应后,开始解析HTTP响应报文。

如果请求跨域的话,Network thread 不会将数据返回,而是抛出错误。

  • 浏览器进程将 Network thread 中准备好的数据通过 IPC 发送给渲染进程。
  • 渲染进程开始加载资源及渲染页面。
    • Main Thread 解析HTML,如果遇到外部资源,会通知浏览器进程的network thread进行下载。
    • Main Thread 构建DOM,构建CSSOM,合并成 Render Tree。
    • Main Thread 通过 Render tree 计算出每个元素的尺寸和位置,几何关系,得到 Layout tree。
    • Main Thread 遍历 Layout tree,生成各元素绘制的先后顺序。
    • Main Thread 对元素进行分层,并通知 Composite Thread 生成渲染帧。
    • Composite Thread 将每个图层分为多个快,调用 Raster Thread 进行光栅化。
    • Composite Thread 会将每个图块的光栅结果存在 GPU Process 的内存中。
    • GPU Process 将渲染帧发送给GPU进行渲染。

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.