- 👋 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 ...
lingxiao-zhu / blog Goto Github PK
View Code? Open in Web Editor NEW总结积累,读书笔记
总结积累,读书笔记
极客时间课程链接:http://gk.link/a/10icc
平常一直在用 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
}
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 用于解决行内属性变化引起 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));
// ...
}
函数组件是没有实例的,所以不能像类组件一样传递 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 下层的传输协议由 TCP/IP 换成了 SSL/TLS,由“HTTP over TCP/IP”变成了“HTTP over SSL/TLS”,让 HTTP 运行在了安全的 SSL/TLS 协议上,收发报文不再使用 Socket API,而是调用专门的安全接口。
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 字节的二进制串。
就是指加密和解密时使用的密钥都是同一个,是“对称”的。只要保证了密钥的安全,那整个通信过程就可以说具有了机密性。
TLS 里有非常多的对称加密算法可供选择,比如 RC4、DES、3DES、AES、ChaCha20 等,但前三种算法都被认为是不安全的,通常都禁止使用,目前常用的只有 AES 和 ChaCha20。
对称算法还有一个“分组模式”的概念,它可以让算法用固定长度的密钥加密任意长度的明文,把小秘密(即密钥)转化为大秘密(即密文)。
对称加密看上去好像完美地实现了机密性,但其中有一个很大的问题:如何把密钥安全地传递给对方,术语叫“密钥交换”。
非对称加密两个密钥,一个叫“公钥”(public key),一个叫“私钥”(private key)。两个密钥是不同的,“不对称”,公钥可以公开给任何人使用,而私钥必须严格保密。
公钥和私钥有个特别的“单向”性,都可以用来加密解密,但公钥加密后只能用私钥解密,反过来,私钥加密后也只能用公钥解密。
非对称加密可以解决“密钥交换”的问题。网站秘密保管私钥,在网上任意分发公钥,你想要登录网站只要用公钥加密就行了,密文只能由私钥持有者才能解密。而黑客因为没有私钥,所以就无法破解密文。
虽然非对称加密没有“密钥交换”的问题,但因为它们都是基于复杂的数学难题,运算速度很慢,即使是 ECC 也要比 AES 差上好几个数量级。
如果仅用非对称加密,虽然保证了安全,但通信速度有如乌龟、蜗牛,实用性就变成了零。
TLS 里使用的混合加密方式:在通信刚开始的时候使用非对称算法,比如 RSA、ECDHE,首先解决密钥交换的问题。然后用随机数产生对称算法使用的“会话密钥”(session key),再用公钥加密。因为会话密钥很短,通常只有 16 字节或 32 字节,所以慢一点也无所谓。
这样混合加密就解决了对称加密算法的密钥交换问题,而且安全和性能兼顾,完美地实现了机密性。
盒模型是包裹HTML元素的,由四个盒子组成:
既然我们的元素布局是由四个盒子组成的,那么当我们设置 width、height 属性时,是设置哪个盒子的尺寸呢?
语法:box-sizing: content-box | border-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。
顾名思义,就是指元素高宽包含从 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。
这里是我梳理的 流程图,包含 render阶段、commit阶段以及Diff算法。
极客时间链接:http://gk.link/a/10s3c
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
.box {
width: 400px;
height: 400px;
background-color: saddlebrown;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
}
.box {
width: 400px;
height: 400px;
background-color: saddlebrown;
position: absolute;
top: 50%;
left: 50%;
transform: translate3d(-200px, -200px, 0);
}
.box {
width: 400px;
height: 400px;
background-color: saddlebrown;
position: absolute;
top: 50%;
left: 50%;
margin-top: -200px;
margin-left: -200px;
}
.parent {
display: flex;
align-items: center;
justify-content: center;
}
.box {
background-color: saddlebrown;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.parent {
display: grid;
height: 500px;
width: 500px;
}
.box {
background-color: saddlebrown;
justify-self: center;
align-self: center;
}
ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。
基本上,ES6 的class可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。
今天就来看下隐藏在 Class 背后的技术点。本次解析基于 Babel LOOSE 模式。
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 () {};
};
其实是放到了实例上,初始化时进行赋值。
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,什么都不做的情况下,会:
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 不一样:
可以看出,其实 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开始排序
是没有重复值的排序(即使两天记录相等也是不重复的),可以利用它来实现分页
是连续排序,两个第二名仍然跟着第三名
是跳跃排序,两个第二名下来就是第四名
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 |
由于 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 |
事件循环是代理调用JS引擎执行JS代码的策略,它是一个在 JavaScript 引擎等待任务和执行任务之间转换的无限循环,与JS语言本身的无任何关系,代理可以是NodeJS,也可以是浏览器,NodeJS 是基于libuv实现的。
简单理解,事件循环就是JS主线程将耗时的任务交给代理去处理,代理处理完后放到数组中,等待主线程空闲后去取出执行,然后遇到耗时任务继续交给代理,这样无限循环。
NodeJS只有一个主线程,单机下只有一个主线程去处理所有的请求,这样遇到CPU密集的操作,会阻塞主线程,降低服务的性能。当有了事件循环,主线程遇到耗时的操作,就可以将其交给系统内核处理(系统是多线程的),然后继续处理后续的请求。
等待系统内核处理完后,将结果放在事件循环的队列中,待主线程空闲后,就去取出执行,这样就保证了服务的性能。
耗时的任务有很多种,比如I/O,网络请求,timer等等,那么NodeJS中他们的执行顺序是怎样的呢?
下图显示了事件循环操作顺序的简化概述:
每个阶段都有一个要执行的回调FIFO队列。当事件循环进入给定阶段时,会在该阶段的队列中执行回调,直到队列耗尽或执行了最大数量的回调,事件循环将移动到下一个阶段,依此类推。
本阶段执行已经被 setTimeout() 和 setInterval() 调度的回调函数,简单理解就是由这两个函数启动的回调函数。
本阶段执行某些系统操作(如 TCP 错误类型)的回调函数。
仅系统内部使用
执行与 I/O 相关的回调。当进入到该阶段:
会遍历同步执行队列中的回调函数,直到队列被清空或者执行到最大限制的数量。
会结束 poll 阶段,进入到 check阶段,执行回调函数。
当 poll 阶段和 check阶段都是空的,事件循环会停留在poll阶段等待io回调,并且监听timers阶段的回调函数是否到调度时间,
如果到了,就从timers里取出放到poll阶段执行。
setImmediate() 回调函数在这里执行,setImmediate 并不是立马执行,而是当事件循环 poll 中没有新的事件处理时就执行该部分。
执行一些关闭的回调函数,如 socket.on('close', ...)。
该方法会将回调函数存到 nextTickQueue,当当前阶段结束后,立马执行 nextTickQueue 中回调函数,执行完后才会进入到下一个阶段。有可能会阻塞eventloop。
缓存(Cache)是计算机领域里的一个重要概念,是优化系统性能的利器。
服务器标记资源有效期使用的头字段是“Cache-Control”,“Cache-Control”字段里的“max-age”和 Cookie 有点像,都是标记资源的有效期。
这里的 max-age 是“生存时间”,时间的计算起点是响应报文的创建时刻(即 Date 字段,也就是离开服务器的时刻),而不是客户端收到报文的时刻,也就是说包含了在链路传输过程中所有节点所停留的时间。
比如,服务器设定“max-age=5”,但因为网络质量很糟糕,等浏览器收到响应报文已经过去了 4 秒,那么这个资源在客户端就最多能够再存 1 秒钟,之后就会失效。
“max-age”是 HTTP 缓存控制最常用的属性,此外在响应报文里还可以用其他的属性来更精确地指示浏览器应该如何使用缓存:
那么客户端缓存后,是否就依照服务器的缓存策略进行呢?
答案并不是,客户端也有自己的缓存策略,其实不止服务器可以发“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
感兴趣的同学可以从这里去购买课程👇 《透视HTTP协议》
111
首先从过去的 CRT 显示器原理说起,CRT 的电子枪从帧缓冲区获取渲染的数据,然后从上到下一行行扫描,扫描完成后显示器就呈现【一帧画面】,随后电子枪回到初始位置继续下一次扫描。
而当一帧画面绘制完成后,电子枪回复到原位,准备画下一帧前,显示器会发出一个垂直同步信号,简称 VSync。
显示器通常以固定频率进行绘制完一帧,这个刷新率就是 VSync 信号产生的频率,一般为60赫兹,每次VSync间歇是16ms。
为了屏幕连续渲染不卡顿,那么当接收到VSync信号后,CPU+GPU计算图像并将放入帧缓冲区的时间需要控制在16ms内,
不然超过16ms还没计算出来,显示器拿不到要渲染的数据,就会跳过这次,造成了掉帧。
所以浏览器也需要跟上VSync的速度,计算一帧的时间控制在16ms内,达到60FPS, 60HZ ~~~ 60FPS。
所以赫兹和FPS是两个东西,赫兹是显示器渲染频率,物理相关;FPS对于前端来说,就是执行JS,计算样式,合成等。
浏览器的帧都集中在CPU计算上,不包含GPU部分。
从图上可以看出,浏览器一帧是从渲染进程中的合成(Compositor)线程接收到 VSync 信号开始的。
Compositor线程收到VSync信号和输入事件(input data),并且将输入事件发送到主线程。
主线程处理输入事件,所有的输入事件处理程序(touchmove, scroll, click)首先触发。
一般我们屏幕的帧率是每秒60帧,也就是60fps,但是某些事件触发的频率超过了这个数值,比如wheel,mousewheel,mousemove,pointermove,touchmove,这些连续性的事件一般每秒会触发60~120次,假如每一次触发事件都将事件发送到主线程处理,由于屏幕的刷新速率相对来说较低,这样使得主线程会触发过量的命中测试以及JS代码,使得性能有了没必要是损耗,浏览器会合并这些连续的事件,延迟到下一帧渲染时执行,也就是requestAnimationFrame之前。
我们的JS代码、回调、eventloop、microtasks就是在这个阶段执行,这个阶段是最容易耗时过长的,
当你准备更新动画时你应该调用此方法。这将使浏览器在下一次重绘之前调用你传入给该方法的动画函数(即你的回调函数)。
如果DOM被修改了,就会执行这个阶段。
会对任何新添加或修改的内容进行计算。此过程是根据匹配选择器(例如 .headline 或 .nav > .nav__item)计算出哪些元素应用哪些 CSS 规则的过程。从中知道规则之后,将应用规则并计算每个元素的最终样式。
计算每个可见元素的几何信息(每个元素的位置和大小)。 通常是针对整个文档进行的,通常会使计算成本与DOM大小成正比。
创建堆叠上下文和深度排序元素的过程。
布局 layout 之后,我们知道了不同元素的结构,样式,几何关系,我们要绘制出一个页面,我们要需要知道每个元素的绘制先后顺序,在绘制阶段,主线程会遍历布局树(layout tree),生成一系列的绘画记录。绘画记录可以看做是记录各元素绘制先后顺序的笔记。
由于页面的各部分可能被绘制到多层,由此它们需要按正确顺序绘制到屏幕上(进行分层),以便正确渲染页面;分层完成后,将图块信息传给Compositor线程。
2 到 9 步都是在主线程进行的,从图中从第六步到第九步都在Timeline中能很清楚的看见。
Compositor线程需要将图层切分为一块又一块的小图块(tiles),之后将这些小图块分别进行发送给一系列光栅线程(raster threads)进行光栅化,结束后光栅线程会将每个图块的光栅结果存在GPU Process的内存中。
随着各个图层的图块都被栅格化、任何新图块都将和输入数据(可能已在事件处理程序中被更改)一起被提交给GPU线程,然后GPU线程将图块上传到GPU,放入到帧缓冲区等待显示器进行下一次绘制。
到这里一帧基本上就做完,如果主线程还有剩余时间的话,就会执行 requestIdleCallback 处理一些优先级不高的事情。
先将要用到的 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);
// 改写 Child 类
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
const child = new Child('Larry', 27);
// 改写 Child 类
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
融合原型链继承和构造函数的优点,是 JavaScript 中最常用的继承模式。
通过这个方法可以解决组合继承的问题。
创建一个仅用于封装继承过程的函数,该函数在内部以某种形式来做增强对象,最后返回对象。
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;
参考谷歌开发者文档,通过自己的语言重新描述。
浏览器的一帧主要做了这件事情:
重排其实就是图中的第三步 Layout,Layout 是浏览器计算各元素几何信息的过程:元素的大小以及在页面中的位置。
根据所用的 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属性,将元素提为单独图层,启动硬件加速,不会影响到其他节点。
这里摘抄下 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是面向链接的,所以需要两端来维护链接的状态,标示位用于描述 TCP 段的行为。
每个标示位占了一个比特,可以混合使用。比如 ACK 和 SYN 同时为 1,代表同步请求和响应被合并了。这也是 TCP 协议,为什么是三次握手的原因之一。
根据对方窗口大小,也就是处理数据的能力,用于控制发送包的大小。
Maxiumun Segment Size,可选项,这个可选项控制了TCP段的大小,它是一个协商字段。
因为TCP是全双工的,所以要保证两边都知道和对方的链接都是通的。如果只是两次,有一方就收不到另外一方的ACK。
三次握手不止为了建立链接,还确认了 TCP包的序号的问题。
MSL是任何报文在网络上存在的最长时间;客户端之所以要等待,是因为客户端收到FIN后,返回ACK给服务端时,服务端就进入CLOSE的状态了,不会在恢复任何报文,所以客户端需要假设ACK的包发送失败,服务端可以会再次发FIN的包给自己,所以需要等待。
TCP 中每个发送的请求都需要响应。如果一个请求没有收到响应,发送方就会认为这次发送出现了故障,会触发重发。
但是一发一收的效率太低了,发送方需要等待,所以最后是一次性发出,慢慢等响应。
所以我们需要一种机制,来记录TCP段的发送情况
实际操作中,每个 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 个字节,当超过这个值的时候,就要小心一点
了,不能倒这么快了,可能快满了,再慢下来。
事件 是某事发生的信号。所有的 DOM 节点都生成这样的信号(但事件不仅限于 DOM)。常见的事件有 click、keydown 等。
通过事件捕获器我们可以分配一个处理程序给对应的信号,使得浏览器和 JS 可以进行交互。当事件发生时,浏览器会创建一个 事件对象,将详细信息放入其中,并将其作为参数传递给处理程序。
React 的事件系统是基于浏览器的事件机制下完全重写的,在使用上,和 DOM 元素的很相似,有几点区别:
但内在设计完全不一样,比如事件对象采用的是合成事件;事件全部挂载到 document 节点上,通过冒泡进行触发。
原生事件(阻止冒泡)会阻止合成事件的执行,合成事件(阻止冒泡)不会阻止原生事件的执行。
比如当我们给 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);
}
};
}
}
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()
TypeScript 3.0 引入了新的 unknown 类型,它是 any 类型对应的安全类型
外部加速 HTTP 协议的服务,CDN(Content Delivery Network 或 Content Distribution Network),中文名叫“内容分发网络”。
光速是有限的,虽然每秒 30 万公里,但这只是真空中的上限,在实际的电缆、光缆中的速度会下降到原本的三分之二左右,也就是 20 万公里 / 秒,这样一来,地理位置的距离导致的传输延迟就会变得比较明显了。
另外不要忘了, 互联网从逻辑上看是一张大网,但实际上是由许多小网络组成的,这其中就有小网络“互连互通”的问题,典型的就是各个电信运营商的网络,比如国内的电信、联通、移动三大家。这些小网络内部的沟通很顺畅,但网络之间却只有很少的联通点。如果你在 A 网络,而网站在 C 网络,那么就必须“跨网”传输,和成千上万的其他用户一起去“挤”连接点的“独木桥”。而带宽终究是有限的,能抢到多少只能看你的运气。
网络中还存在许多的路由器、网关,数据每经过一个节点,都要停顿一下,在二层、三层解析转发,这也会消耗一定的时间,带来延迟。
CDN 的最核心原则是“就近访问”,如果用户能够在本地几十公里的距离之内获取到数据,那么时延就基本上变成 0 了。
所以 CDN 投入了大笔资金,在全国、乃至全球的各个大枢纽城市都建立了机房,部署了大量拥有高存储高带宽的节点,构建了一个专用网络。这个网络是跨运营商、跨地域的,虽然内部也划分成多个小网络,但它们之间用高速专有线路连接,是真正的“信息高速公路”,基本上可以认为不存在网络拥堵。
有了这个高速的网路,CDN 就要“分发”源站的“内容”了,用到的“缓存代理”技术。使用“推”或者“拉”的手段,把源站的内容逐级缓存到网络的每一个节点上。
于是,用户在上网的时候就不直接访问源站,而是访问离他“最近的”一个 CDN 节点,术语叫“边缘节点”(edge node),其实就是缓存了源站内容的代理服务器,这样一来就省去了“长途跋涉”的时间成本,实现了“网络加速”。
全局负载均衡(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 的全局负载均衡系统,开始“智能调度”,主要的依据有这么几个:
缓存系统是 CDN 的另一个关键组成部分,相当于 CDN 的“心脏”。如果缓存系统的服务能力不够,不能很好地满足用户的需求,那 GSLB 调度算法再优秀也没有用。
但互联网上的资源是无穷无尽的,不管 CDN 厂商有多大的实力,也不可能把所有资源都缓存起来。所以,缓存系统只能有选择地缓存那些最常用的那些资源。
这里就有两个 CDN 的关键概念:“命中”和“回源”。
“命中”就是指用户访问的资源恰好在缓存系统里,可以直接返回给用户;
“回源”则正相反,缓存里没有,必须用代理的方式回源站取。
也就有了两个衡量 CDN 服务质量的指标:“命中率”和“回源率”。
这两个方法主要通过数据劫持来进行依赖收集,典型的应用有Vue和MobX,Vue源码实现可以参考 这里
由 ES5 提供,该方法可以在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。
用法:Object.defineProperty(obj, prop, descriptor)
参数:
注意,value和writable是一组,get和set是一组,不能交叉使用,否则会报错
var obj = {}
Object.defineProperty(obj, "key", {
value: "static"
});
/// 相当于
var obj = {}
Object.defineProperty(obj, "key", {
configurable: false,
enumerable: false,
writable: false,
value: "static"
});
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
},
})
}
由 ES6 提供,Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
用法:const p = new Proxy(target, handler)
参数:
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
var p = new Proxy({}, {
set: function(target, prop, value, receiver) {
target[prop] = value;
console.log('property set: ' + prop + ' = ' + value);
return true;
}
})
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
* */
浏览器的主进程(负责协调、主控),只有一个。作用:
为满足上面的功能,对应有以下线程:
当资源充足时,Network、Storage可以作为单独的进程。
这是为浏览器所有标签页和周边进程提供服务的单个进程。
随着帧的提交,GPU进程会将任何图块和其他数据(如四边形顶点和矩阵)上传到GPU,以实际将像素推送到屏幕。
GPU进程包含一个单独的线程,称为GPU线程,它实际上完成了工作。
每一个Tab页面就是一个渲染进程,作用就是渲染HTML页面,有多个。有以下线程:
每种类型的插件对应一个进程,仅当使用该插件时才创建,有多个。
可以通过 Chrome 浏览器的 窗口 -》任务管理器 进行查看。
以输入URL访问页面为例。
如果请求跨域的话,Network thread 不会将数据返回,而是抛出错误。
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.