1. js中~~
和 |
的妙用
1.1 js中的按位取反运算符~
JS按位取反运算符~,是对一个表达式执行位非(求非)运算。如1 = -2,-3=2,~true=-2,~false=-1
1.2 如何按位取反
计算
按位取反的运算规则步骤:
1.21 十进制转成原码
转成二进制原码,最高位是符号位,0为正数,1为负数
十进制 ----> 原码
1 ----> 00000001
-1 ----> 10000001
1.22原码转成反码
正数的反码就是原码,负数的反码是符号位不变
,其余位取反
十进制 ----> 原码 ----> 反码
1 ----> 00000001 ----> 00000001
-1 ----> 10000001 ----> 11111110
1.23 反码转成补码
正数的补码还是原码,负数的补码是在反码的基础上加1
十进制 ----> 原码 ----> 反码 ----> 补码
1 ----> 00000001 ----> 00000001 ----> 00000001
-1 ----> 10000001 ----> 11111110 ----> 11111111
1.24 补码取反得原码
正整数补码取反之后符号位置为1,是一个负整数,所以再按照负整数计算补码的方式逆运算得到原码
逆运算得到原码,首先将取反的补码转成反码,公式:反码=补码 - 1,然后将反码转成原码,符号位不变,其他位取反
十进制 ----> 原码 ----> 反码 ----> 补码 ----> 补码取反 ----> 取反补码转成反码 ----> 转成原码
1 ----> 00000001 ----> 0000001 ----> 00000001 ----> 11111110 ----> 11111101 ----> 10000010
负整数补码取反之后符号位置为0,是一个正整数,因正整数的反码与补码就是本身,所以不需要再进行逆运算
十进制 ----> 原码 ----> 反码 ----> 补码 ----> 补码取反得原码
-1 ----> 10000001 ----> 11111110 ----> 11111111 ----> 00000000
1.25 将原码转成二进制
十进制 ----> 原码 ----> 反码 ----> 补码 ----> 补码取反 ----> 取反补码转成反码 ----> 转成原码 ----> 转成二进制
1 ----> 00000001 ----> 0000001 ----> 00000001 ----> 11111110 ----> 11111101 ----> 10000010 ----> -2
十进制 ----> 原码 ----> 反码 ----> 补码 ----> 补码取反得原码 ----> 转成二进制
-1 ----> 10000001 ----> 11111110 ----> 11111111 ----> 00000000 ----> 0
所以,1=-2,-1=0
1.3 使用 ~ 和!的区别
findIndex是查询是否在数组中,存在则返回索引,不存在返回-1,通过~取反,得到的效果与使用!相同,但是两者返回的值不同。
~ 返回的是一个整数类型
!返回的是一个boolean类型
let arr = [1,2,3,4,5];
console.log(~arr.findIndex(d=>d===1)?'1存在':'1不存在'); //1存在
console.log(~arr.findIndex(d=>d===6)?'6存在':'6不存在'); //6不存在
console.log(~arr.findIndex(d=>d===6)) //0
console.log(!arr.findIndex(d=>d===6)) //false
1.4 取反再取反~~的作用
操作符~, 是按位取反的意思,表面上~~(取反再取反)没有意义,实际上在JS中可以将浮点数变成整数。
console.log(~~1.11); //1
console.log(~~-25.11); //-25
1.41 双~的用法
~~
它代表双非按位取反运算符,是比Math.floor()更快的方法。需要注意,对于正数,它向下取整;对于负数,向上取整;非数字取值为0,它具体的表现形式为:
~~null; // => 0
~~undefined; // => 0
~~Infinity; // => 0
--NaN; // => 0
~~0; // => 0
~~{}; // => 0
~~[]; // => 0
~~(1/0); // => 0
~~false; // => 0
~~true; // => 1
~~1.9; // => 1
~~-1.9; // => -1
1.5 |
的用法,通常用来取整
1.2|0 // 1
1.8|0 // 1
-1.2|0 // -1
2. 模拟实现 new 操作符
new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。new 关键字会进行如下的操作:
- 创建一个空的简单JavaScript对象(即
{}
); - 链接该对象(即设置该对象的构造函数)到另一个对象 ;
- 将步骤1新创建的对象作为
this
的上下文 ; - 如果该函数没有返回对象,则返回
this
。
代码实现:
function new_object() {
// 创建一个空的对象
let obj = new Object()
// 获得构造函数
let Con = [].shift.call(arguments)
// 链接到原型 (不推荐使用)
obj.__proto__ = Con.prototype
// 绑定 this,执行构造函数
let result = Con.apply(obj, arguments)
// 确保 new 出来的是个对象
return typeof result === 'object' ? result : obj
}
警告: 通过现代浏览器的操作属性的便利性,可以改变一个对象的
[[Prototype]]
属性, 这种行为在每一个JavaScript引擎和浏览器中都是一个非常慢且影响性能的操作,使用这种方式来改变和继承属性是对性能影响非常严重的,并且性能消耗的时间也不是简单的花费在obj.__proto__ = ...
语句上, 它还会影响到所有继承来自该[[Prototype]]
的对象,如果你关心性能,你就不应该在一个对象中修改它的 [[Prototype]]。相反, 创建一个新的且可以继承[[Prototype]]
的对象,推荐使用Object.create()
—MDN
所以进一步优化 new
实现:
// 优化后 new 实现
function create() {
// 1、获得构造函数,同时删除 arguments 中第一个参数
Con = [].shift.call(arguments);
// 2、创建一个空的对象并链接到原型,obj 可以访问构造函数原型中的属性
let obj = Object.create(Con.prototype);
// 3、绑定 this 实现继承,obj 可以访问到构造函数中的属性
let ret = Con.apply(obj, arguments);
// 4、优先返回构造函数返回的对象
return ret instanceof Object ? ret : obj;
};
3. 解析 call/apply 原理,并手写 call/apply 实现
3.1 Function.prototype.call()
call()
方法调用一个函数, 其具有一个指定的 this
值和多个参数(参数的列表)。
func.call(thisArg, arg1, arg2, ...)
它运行 func
,提供的第一个参数 thisArg
作为 this
,后面的作为参数。
看一个简单的例子:
function sayWord() {
var talk = [this.name, 'say', this.word].join(' ');
console.log(talk);
}
var bottle = {
name: 'bottle',
word: 'hello'
};
// 使用 call 将 bottle 传递为 sayWord 的 this
sayWord.call(bottle);
// bottle say hello
所以,call
主要实现了以下两个功能:
call
改变了this
的指向bottle
执行了sayWord
函数
3.11 模拟实现 call
模拟实现 call
有三步:
- 将函数设置为对象的属性
- 执行函数
- 删除对象的这个属性
Function.prototype.call = function (context) {
// 将函数设为对象的属性
// 注意:非严格模式下,
// 指定为 null 和 undefined 的 this 值会自动指向全局对象(浏览器中就是 window 对象)
// 值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的自动包装对象(用 Object() 转换)
context = context ? Object(context) : window;
context.fn = this;
// 执行该函数
let args = [...arguments].slice(1);
let result = context.fn(...args);
// 删除该函数
delete context.fn
// 注意:函数是可以有返回值的
return result;
}
3.2 Function.prototype.apply()
apply()
方法调用一个具有给定 this
值的函数,以及作为一个数组(或类似数组对象)
提供的参数。
func.apply(thisArg, [argsArray])
它运行 func
设置 this = context
并使用类数组对象 args
作为参数列表。
例如,这两个调用几乎相同:
func(1, 2, 3);
func.apply(context, [1, 2, 3])
两个都运行 func
给定的参数是 1,2,3
。但是 apply
也设置了 this = context
。
call
和 apply
之间唯一的语法区别
是 call
接受一个参数列表
,而 apply
则接受带有一个类数组对象
。
需要注意:Chrome 14 以及 Internet Explorer 9 仍然不接受类数组对象
。如果传入类数组对象,它们会抛出异常。
3.21 模拟实现 apply
Function.prototype.apply = function (context, arr) {
context = context ? Object(context) : window;
context.fn = this;
let result;
if (!arr) {
result = context.fn();
} else {
result = context.fn(...arr);
}
delete context.fn
return result;
}
4. 解析 bind 原理,并手写 bind 实现
4.1 bind()
bind()
方法创建一个新的函数,在bind()
被调用时,这个新函数的this
被指定为bind()
的第一个参数,而其余参数将作为新函数的参数,供调用时使用。— MDN
bind
方法与 call / apply
最大的不同就是前者返回一个绑定上下文的函数,而后两者是直接执行了函数。
例子:
let value = 2;
let foo = {
value: 1
};
function bar(name, age) {
return {
value: this.value,
name: name,
age: age
}
};
bar.call(foo, "Jack", 20); // 直接执行了函数
// {value: 1, name: "Jack", age: 20}
let bindFoo1 = bar.bind(foo, "Jack", 20); // 返回一个函数
bindFoo1();
// {value: 1, name: "Jack", age: 20}
let bindFoo2 = bar.bind(foo, "Jack"); // 返回一个函数
bindFoo2(20);
// {value: 1, name: "Jack", age: 20}
通过上述代码可以看出 bind
有如下特性:
- 1、指定
this
- 2、传入参数
- 3、返回一个函数
- 4、柯里化
4.2 模拟实现:
Function.prototype.bind = function (context) {
// 调用 bind 的不是函数,需要抛出异常
if (typeof this !== "function") {
throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
}
// this 指向调用者
var self = this;
// 实现第2点,因为第1个参数是指定的this,所以只截取第1个之后的参数
var args = Array.prototype.slice.call(arguments, 1);
// 实现第3点,返回一个函数
return function () {
// 实现第4点,这时的arguments是指bind返回的函数传入的参数
// 即 return function 的参数
var bindArgs = Array.prototype.slice.call(arguments);
// 实现第1点
return self.apply( context, args.concat(bindArgs) );
}
}
但还有一个问题,bind
有以下一个特性:
一个绑定函数也能使用 new 操作符创建对象:这种行为就像把原函数当成构造器,提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。
来个例子说明下:
let value = 2;
let foo = {
value: 1
};
function bar(name, age) {
this.habit = 'shopping';
console.log(this.value);
console.log(name);
console.log(age);
}
bar.prototype.friend = 'kevin';
let bindFoo = bar.bind(foo, 'Jack');
let obj = new bindFoo(20);
// undefined
// Jack
// 20
obj.habit;
// shopping
obj.friend;
// kevin
上面例子中,运行结果 this.value
输出为 undefined
,这不是全局 value
也不是 foo
对象中的 value
,这说明 bind
的 this
对象失效了,new
的实现中生成一个新的对象,这个时候的 this
指向的是 obj
。
这个可以通过修改返回函数的原型来实现,代码如下:
Function.prototype.bind = function (context) {
// 调用 bind 的不是函数,需要抛出异常
if (typeof this !== "function") {
throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
}
// this 指向调用者
var self = this;
// 实现第2点,因为第1个参数是指定的this,所以只截取第1个之后的参数
var args = Array.prototype.slice.call(arguments, 1);
// 创建一个空对象
var fNOP = function () {};
// 实现第3点,返回一个函数
var fBound = function () {
// 实现第4点,获取 bind 返回函数的参数
var bindArgs = Array.prototype.slice.call(arguments);
// 然后同传入参数合并成一个参数数组,并作为 self.apply() 的第二个参数
return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
// 注释1
}
// 注释2
// 空对象的原型指向绑定函数的原型
fNOP.prototype = this.prototype;
// 空对象的实例赋值给 fBound.prototype
fBound.prototype = new fNOP();
return fBound;
}
注释1 :
- 当作为构造函数时,
this
指向实例,此时this instanceof fBound
结果为true
,可以让实例获得来自绑定函数的值,即上例中实例会具有habit
属性。 - 当作为普通函数时,
this
指向window
,此时结果为false
,将绑定函数的this
指向context
注释2 :
- 修改返回函数的
prototype
为绑定函数的prototype
,实例就可以继承绑定函数的原型中的值,即上例中obj
可以获取到bar
原型上的friend
- 至于为什么使用一个空对象
fNOP
作为中介,把fBound.prototype
赋值为空对象的实例(原型式继承),这是因为直接fBound.prototype = this.prototype
有一个缺点,修改fBound.prototype
的时候,也会直接修改this.prototype
;其实也可以直接使用ES5的Object.create()
方法生成一个新对象,但bind
和Object.create()
都是ES5方法,部分IE浏览器(IE < 9)并不支
注意: bind()
函数在 ES5 才被加入,所以并不是所有浏览器都支持,IE8
及以下的版本中不被支持,如果需要兼容可以使用 Polyfill 来实现
5. 浅拷贝和深拷贝,实现 Object 的深拷贝.
5.1 在js中,对象是引用类型。如果给一个变量赋值给一个对象的时候,这时候变量和对象都是指向同一个引用,即
let obj1 = {a: 1}
let obj2 = obj1
console.log(obj2.a) // 1
obj2.a = 2
console.log(obj2.a) // 2
console.log(obj1.a) // 2
由于指向的是同一个引用,即 obj2
属性的值变化了,那么 obj1
也会跟着变化。
5.2 深拷贝的实现
5.21 JSON.parse(JSON.stringify(object))
通常可以使用 JSON.parse(JSON.stringify(object))
来解决
let obj1 = {
a: 1,
b: {
c: 2
}
}
let obj2 = JSON.parse(JSON.stringify(obj1))
obj2.b.c = '3'
console.log(obj1.b.c) // 2
但是使用 JSON.parse(JSON.stringify(object))
也有弊端
- 会忽略 undefined
- 会忽略 symbol
- 不能序列化函数
- 不能解决循环引用的对象
5.22 typeof + 递归
遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深拷贝
// 递归判断是否对象和数组
function deepClone(obj, target) {
if(!obj) return
for(let key in obj) {
if(obj.hasOwnProperty(key)) {
if(Array.isArray(key) || (typeof obj[key] === 'object' && obj[key] !== null)) {
target[key] = []
deepClone(obj[key], target[key])
} else {
target[key] = obj[key]
}
}
}
return target
}
5.23 Object.prototype.toString.call + 递归
function checkedType(target) {
return Object.prototype.toString.call(target).slice(8, -1)
}
//实现深度克隆---对象/数组
function clone(target) {
//判断拷贝的数据类型
//初始化变量result 成为最终克隆的数据
let result, targetType = checkedType(target)
if (targetType === 'Object') {
result = {}
} else if (targetType === 'Array') {
result = []
} else {
return target
}
//遍历目标数据
for (let i in target) {
//获取遍历数据结构的每一项值。
let value = target[i]
//判断目标结构里的每一值是否存在对象/数组
if (checkedType(value) === 'Object' ||
checkedType(value) === 'Array') { //对象/数组里嵌套了对象/数组
//继续遍历获取到value值
result[i] = clone(value)
} else { //获取到value值是基本的数据类型或者是函数。
result[i] = value;
}
}
return result
}
6. 节流函数 throttle
debounce
与 throttle
是开发中常用的高阶函数,作用都是为了防止函数被高频调用,换句话说就是,用来控制某个函数在一定时间内执行多少次。
6.1 使用场景
比如绑定响应鼠标移动、窗口大小调整、滚屏等事件时,绑定的函数触发的频率会很频繁。若稍处理函数微复杂,需要较多的运算执行时间和资源,往往会出现延迟,甚至导致假死或者卡顿感。为了优化性能,这时就很有必要使用 debounce
或 throttle
了。
6.2 debounce
与 throttle
区别
防抖 (debounce) :多次触发,只在最后一次触发时,执行目标函数。
节流(throttle):限制目标函数调用的频率,比如:1s内不能调用2次。
6.3 手写一个 throttle
实现方案有以下两种:
- 第一种是用时间戳来判断是否已到执行时间,记录上次执行的时间戳,然后每次触发事件执行回调,回调中判断当前时间戳距离上次执行时间戳的间隔是否已经达到时间差(Xms) ,如果是则执行,并更新上次执行的时间戳,如此循环。
- 第二种方法是使用定时器,比如当
scroll
事件刚触发时,打印一个 hello world,然后设置个1000ms
的定时器,此后每次触发scroll
事件触发回调,如果已经存在定时器,则回调不执行方法,直到定时器触发,handler
被清除,然后重新设置定时器。
这里我们采用第一种方案来实现,通过闭包保存一个 previous
变量,每次触发 throttle
函数时判断当前时间和 previous
的时间差,如果这段时间差小于等待时间,那就忽略本次事件触发。如果大于等待时间就把 previous
设置为当前时间并执行函数 fn。
// fn 是需要执行的函数
// wait 是时间间隔
const throttle = (fn, wait = 50) => {
// 上一次执行 fn 的时间
let previous = 0
// 将 throttle 处理结果当作函数返回
return function(...args) {
// 获取当前时间,转换成时间戳,单位毫秒
let now = +new Date()
// 将当前时间和上一次执行函数的时间进行对比
// 大于等待时间就把 previous 设置为当前时间并执行函数 fn
if (now - previous > wait) {
previous = now
fn.apply(this, args)
}
}
}
// DEMO
// 执行 throttle 函数返回新函数
const betterFn = throttle(() => console.log('fn 函数执行了'), 1000)
// 每 10 毫秒执行一次 betterFn 函数,但是只有时间差大于 1000 时才会执行 fn
setInterval(betterFn, 10)
6.4 underscore 源码解读
上述代码实现了一个简单的节流函数,不过 underscore
实现了更高级的功能,即新增了两个功能
- 配置是否需要响应事件刚开始的那次回调(
leading
参数,false
时忽略) - 配置是否需要响应事件结束后的那次回调(
trailing
参数,false
时忽略)
const throttle = function(func, wait, options) {
var timeout, context, args, result;
// 上一次执行回调的时间戳
var previous = 0;
// 无传入参数时,初始化 options 为空对象
if (!options) options = {};
var later = function() {
// 当设置 { leading: false } 时
// 每次触发回调函数后设置 previous 为 0
// 不然为当前时间
previous = options.leading === false ? 0 : _.now();
// 防止内存泄漏,置为 null 便于后面根据 !timeout 设置新的 timeout
timeout = null;
// 执行函数
result = func.apply(context, args);
if (!timeout) context = args = null;
};
// 每次触发事件回调都执行这个函数
// 函数内判断是否执行 func
// func 才是我们业务层代码想要执行的函数
var throttled = function() {
// 记录当前时间
var now = _.now();
// 第一次执行时(此时 previous 为 0,之后为上一次时间戳)
// 并且设置了 { leading: false }(表示第一次回调不执行)
// 此时设置 previous 为当前值,表示刚执行过,本次就不执行了
if (!previous && options.leading === false) previous = now;
// 距离下次触发 func 还需要等待的时间
var remaining = wait - (now - previous);
context = this;
args = arguments;
// 要么是到了间隔时间了,随即触发方法(remaining <= 0)
// 要么是没有传入 {leading: false},且第一次触发回调,即立即触发
// 此时 previous 为 0,wait - (now - previous) 也满足 <= 0
// 之后便会把 previous 值迅速置为 now
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
// clearTimeout(timeout) 并不会把 timeout 设为 null
// 手动设置,便于后续判断
timeout = null;
}
// 设置 previous 为当前时间
previous = now;
// 执行 func 函数
result = func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
// 最后一次需要触发的情况
// 如果已经存在一个定时器,则不会进入该 if 分支
// 如果 {trailing: false},即最后一次不需要触发了,也不会进入这个分支
// 间隔 remaining milliseconds 后触发 later 方法
timeout = setTimeout(later, remaining);
}
return result;
};
// 手动取消
throttled.cancel = function() {
clearTimeout(timeout);
previous = 0;
timeout = context = args = null;
};
// 执行 _.throttle 返回 throttled 函数
return throttled;
};
7. 防抖函数 debounce
7.1 防抖函数 debounce
指的是某个函数在某段时间内,无论触发了多少次回调,都只执行最后一次
实现原理就是利用定时器,函数第一次执行时设定一个定时器,之后调用时发现已经设定过定时器就清空之前的定时器,并重新设定一个新的定时器,如果存在没有被清空的定时器,当定时器计时结束后触发函数执行。
// fn 是需要防抖处理的函数
// wait 是时间间隔
function debounce(fn, wait = 50) {
// 通过闭包缓存一个定时器 id
let timer = null
// 将 debounce 处理结果当作函数返回
// 触发事件回调时执行这个返回函数
return function(...args) {
// this保存给context
const context = this
// 如果已经设定过定时器就清空上一次的定时器
if (timer) clearTimeout(timer)
// 开始设定一个新的定时器,定时器结束后执行传入的函数 fn
timer = setTimeout(() => {
fn.apply(context, args)
}, wait)
}
}
// DEMO
// 执行 debounce 函数返回新函数
const betterFn = debounce(() => console.log('fn 防抖执行了'), 1000)
// 停止滑动 1 秒后执行函数 () => console.log('fn 防抖执行了')
document.addEventListener('scroll', betterFn)
7.2 不过 underscore
中的 debounce
还有第三个参数:immediate
。这个参数是做什么用的呢?
传参 immediate 为 true, debounce会在 wait 时间间隔的开始调用这个函数 。(注:并且在 wait 的时间之内,不会再次调用。)在类似不小心点了提交按钮两下而提交了两次的情况下很有用。
把 true
传递给 immediate
参数,会让 debounce
在 wait
时间开始计算之前就触发函数(也就是没有任何延时就触发函数),而不是过了 wait
时间才触发函数,而且在 wait
时间内也不会触发(相当于把 fn
的执行锁住)。 如果不小心点了两次提交按钮,第二次提交就会不会执行。
那我们根据 immediate
的值来决定如何执行 fn
。如果是 immediate
的情况下,我们立即执行 fn
,并在 wait
时间内锁住 fn
的执行, wait
时间之后再触发,才会重新执行 fn
,以此类推。
// immediate 表示第一次是否立即执行
function debounce(fn, wait = 50, immediate) {
let timer = null
return function(...args) {
// this保存给context
const context = this
if (timer) clearTimeout(timer)
// immediate 为 true 表示第一次触发后执行
// timer 为空表示首次触发
if (immediate && !timer) {
fn.apply(context, args)
}
timer = setTimeout(() => {
fn.apply(context, args)
}, wait)
}
}
// DEMO
// 执行 debounce 函数返回新函数
const betterFn = debounce(() => console.log('fn 防抖执行了'), 1000, true)
// 第一次触发 scroll 执行一次 fn,后续只有在停止滑动 1 秒后才执行函数 fn
document.addEventListener('scroll', betterFn)
7.3 underscore 源码解析
看完了上文的基本版代码,感觉还是比较轻松的,现在来学习下 underscore 是如何实现 debounce 函数的,学习一下优秀的思想,直接上代码和注释,本源码解析依赖于 underscore 1.9.1 版本实现。
// 此处的三个参数上文都有解释
_.debounce = function(func, wait, immediate) {
// timeout 表示定时器
// result 表示 func 执行返回值
var timeout, result;
// 定时器计时结束后
// 1、清空计时器,使之不影响下次连续事件的触发
// 2、触发执行 func
var later = function(context, args) {
timeout = null;
// if (args) 判断是为了过滤立即触发的
// 关联在于 _.delay 和 restArguments
if (args) result = func.apply(context, args);
};
// 将 debounce 处理结果当作函数返回
var debounced = restArguments(function(args) {
if (timeout) clearTimeout(timeout);
if (immediate) {
// 第一次触发后会设置 timeout,
// 根据 timeout 是否为空可以判断是否是首次触发
var callNow = !timeout;
timeout = setTimeout(later, wait);
if (callNow) result = func.apply(this, args);
} else {
// 设置定时器
timeout = _.delay(later, wait, this, args);
}
return result;
});
// 新增 手动取消
debounced.cancel = function() {
clearTimeout(timeout);
timeout = null;
};
return debounced;
};
// 根据给定的毫秒 wait 延迟执行函数 func
_.delay = restArguments(function(func, wait, args) {
return setTimeout(function() {
return func.apply(null, args);
}, wait);
});
相比上文的基本版实现,underscore
多了以下几点功能。
- 1、函数
func
的执行结束后返回结果值result
- 2、定时器计时结束后清除
timeout
,使之不影响下次连续事件的触发 - 3、新增了手动取消功能
cancel
- 4、
immediate
为true
后只会在第一次触发时执行,频繁触发回调结束后不会再执行
8. setTimeout 实现机制与原理
let setTimeout = (fn, timeout, ...args) => {
// 初始当前时间
const start = +new Date()
let timer, now
const loop = () => {
timer = window.requestAnimationFrame(loop)
// 再次运行时获取当前时间
now = +new Date()
// 当前运行时间 - 初始当前时间 >= 等待时间 ===>> 跳出
if (now - start >= timeout) {
fn.apply(this, args)
window.cancelAnimationFrame(timer)
}
}
window.requestAnimationFrame(loop)
}
function showName(){
console.log("Hello")
}
let timerID = setTimeout(showName, 1000);
// 在 1 秒后打印 “Hello”
注意:JavaScript 定时器函数像
setTimeout
和setInterval
都不是 ECMAScript 规范或者任何 JavaScript 实现的一部分。 定时器功能由浏览器实现,它们的实现在不同浏览器之间会有所不同。 定时器也可以由 Node.js 运行时本身实现。在浏览器里主要的定时器函数是作为
Window
对象的接口,Window
对象同时拥有很多其他方法和对象。该接口使其所有元素在 JavaScript 全局作用域中都可用。这就是为什么你可以直接在浏览器控制台执行setTimeout
。在 node 里,定时器是
global
对象的一部分,这点很像浏览器中的Window
。你可以在 Node里看到定时器的源码 这里 ,在浏览器中定时器的源码在 这里 。
9. 手写 async/await 的实现
await 内部实现了 generator,其实 await 就是 generator 加上 Promise的语法糖,且内部实现了自动执行 generator。如果你熟悉 co 的话,其实自己就可以实现这样的语法糖。
/**
* async/await 实现
* @param {*} generatorFunc
*/
function asyncToGenerator(generatorFunc) {
// 返回的是一个新的函数
return function(...args) {
// 先调用generator函数 生成迭代器
// 对应 var gen = testG()
const gen = generatorFunc.apply(this, args)
// 返回一个promise 因为外部是用.then的方式 或者await的方式去使用这个函数的返回值的
// var test = asyncToGenerator(testG)
// test().then(res => console.log(res))
return new Promise((resolve, reject) => {
// 内部定义一个step函数 用来一步一步的跨过yield的阻碍
// key有next和throw两种取值,分别对应了gen的next和throw方法
// arg参数则是用来把promise resolve出来的值交给下一个yield
function step(key, arg) {
let genResult
// 这个方法需要包裹在try catch中
// 如果报错了 就把promise给reject掉 外部通过.catch可以获取到错误
try {
genResult = gen[key](arg)
} catch (error) {
return reject(error)
}
// gen.next() 得到的结果是一个 { value, done } 的结构
const { value, done } = genResult
if (done) {
// 如果已经完成了 就直接resolve这个promise
// 这个done是在最后一次调用next后才会为true
// 以本文的例子来说 此时的结果是 { done: true, value: 'success' }
// 这个value也就是generator函数最后的返回值
return resolve(value)
} else {
// 除了最后结束的时候外,每次调用gen.next()
// 其实是返回 { value: Promise, done: false } 的结构,
// 这里要注意的是Promise.resolve可以接受一个promise为参数
// 并且这个promise参数被resolve的时候,这个then才会被调用
return Promise.resolve(
// 这个value对应的是yield后面的promise
value
).then(
// value这个promise被resove的时候,就会执行next
// 并且只要done不是true的时候 就会递归的往下解开promise
// 对应gen.next().value.then(value => {
// gen.next(value).value.then(value2 => {
// gen.next()
//
// // 此时done为true了 整个promise被resolve了
// // 最外部的test().then(res => console.log(res))的then就开始执行了
// })
// })
function onResolve(val) {
step("next", val)
},
// 如果promise被reject了 就再次进入step函数
// 不同的是,这次的try catch中调用的是gen.throw(err)
// 那么自然就被catch到 然后把promise给reject掉啦
function onReject(err) {
step("throw", err)
},
)
}
}
step("next")
})
}
}
var getData = () => new Promise(resolve => setTimeout(() => resolve('data'), 1000));
function* testG() {
const data = yield getData();
console.log('data: ', data);
const data2 = yield getData();
console.log('data2: ', data2);
return 'success';
}
var gen = asyncToGenerator(testG);
gen().then(res => console.log(res));
10. 简易版 useState 实现
10.1 基础版 useState
简单实现:只是数组
通过数组实现,初始化的时候,创建两个数组:states
与 setters
,设置光标 cursor
为 0
- 第一次调用
useState
时,创建一个setter
函数放入setters
中,并初始化一个state
放入states
中 - 之后每次重新渲染时,都会重置光标
cursor
为0
,通过cursor
从states
与setters
获取[state, setter]
返回
每次更新 state
时,都是通过调用 setter
函数修改对应的 state
值,这种对应关系是通过 cursor
闭包来实现的
代码实现:
let states = []
let setters = []
let firstRun = true
let cursor = 0
// 使用工厂模式生成一个 createSetter,通过 cursor 指定指向的是哪个 state
function createSetter(cursor) {
return function(newVal) { // 闭包
states[cursor] = newVal
}
}
function useState(initVal) {
// 首次
if(firstRun) {
states.push(initVal)
setters.push(createSetter(cursor))
firstRun = false
}
let state = states[cursor]
let setter = setters[cursor]
// 光标移动到下一个位置
cursor++
// 返回
return [state, setter]
}
使用
function App() {
// 每次重置 cursor
cursor = 0
return <RenderFunctionComponent />
}
function RenderFunctionComponent() {
const [firstName, setFirstName] = useState("Rudi");
const [lastName, setLastName] = useState("Yardley");
return (
<Button onClick={() => setFirstName("Fred")}>Fred</Button>
);
}
10.2 进阶版 useState
简单实现:只是链表
在真实的 React Hooks 中,这种关系其实是通过链表实现的,首先我们需要明确以下内容:
在 React 中最多会同时存在两棵 Fiber
树。当前屏幕上显示内容对应的 Fiber 树称为 current Fiber
树,正在内存中构建的 Fiber
树称为 workInProgress Fiber
树。current Fiber
树中的 Fiber
节点被称为 current fiber
,workInProgress Fiber
树中的 Fiber
节点被称为 workInProgress fiber
,他们通过 alternate
属性连接。
// workInProgressHook 指针,指向当前 hook 对象
let workInProgressHook = null
// workInProgressHook fiber,这里指的是 App 组件
let fiber = {
stateNode: App, // App 组件
memoizedState: null // hooks 链表,初始为 null
}
// 是否是首次渲染
let isMount = true
function schedule() {
workInProgressHook = fiber.memoizedState
const app = fiber.stateNode()
isMount = false
return app
}
function useState(initVal) {
let hook
// 首次会生成 hook 对象,并形成链表结构,绑定在 workInProgress 的 memoizedState 属性上
if(isMount) {
// 每个 hook 对象,例如 state hook、memo hook、ref hook 等
hook = {
memoizedState: initVal, // 当前state的值,例如 useState(initVal)
action: null, // update 函数
next: null // 因为是采用链表的形式连接起来,next指向下一个 hook
}
// 绑定在 workInProgress 的 memoizedState 属性上
if(!fiber.memoizedState) {
// 如果是第一个 hook 对象
fiber.memoizedState = hook
} else {
// 如果不是, 将 hook 追加到链尾
workInProgressHook.next = hook
}
// 指针指向当前 hook,链表尾部,最新 hook
workInProgressHook = hook
} else {
// 拿到当前的 hook
hook = workInProgressHook
// workInProgressHook 指向链表的下一个 hook
workInProgressHook = workInProgressHook.next
}
// 状态更新,拿到 current hook,调用 action 函数,更新到最新 state
let baseState = hook.memoizedState
// 执行 update
if(hook.action) {
// 更新最新值
let action = hook.action
// 如果是 setNum(num=>num+1) 形式
if(typeof action === 'function') {
baseState = action(baseState)
} else {
baseState = action
}
// 清空 action
hook.action = null
}
// 更新最新值
hook.memoizedState = baseState
// 返回最新值 baseState、dispatchAction
return [baseState, dispatchAction(hook)]
}
// action 函数
function dispatchAction(hook) {
return function (action) {
hook.action = action
}
}
10.3 优化版:useState 是如何更新的
更近一步,其实 useState
有两个阶段,负责初始化的 mountState
与负责更新的 updateState
,在 mountState
阶段会创建一个 state
hook.queue
对象,保存负责更新的信息(包含 pending
,待更新队列),以及一个负责更新的函数 dispatchAction
(就是 setNum
,第三个参数就是 queue
)
// 因此,实际的 hook 是这样的
// 每个 hook 对象,例如 state hook、memo hook、ref hook 等
let hook = {
memoizedState: initVal, // 当前state的值,例如 useState(initVal)
queue: {
pending: null
}, // update 待更新队列, 链表的形式存储
next: null // 因为是采用链表的形式连接起来,next指向下一个 hook
}
// 调用updateNum实际上调用这个,queue就是当前hooks对应的queue。
function dispatchAction(queue, action) {
// 每一个任务对应一个update
const update = {
// 更新执行的函数
action,
// 与同一个Hook的其他更新形成链表
next: null,
};
// ...
}
每次更新的时候(updateState
)都会创建一个 update
对象,里面记录了此次更新的信息,然后将此update
放入待更新的 pending
队列中,最后,dispatchAction
判断当前 fiber
没有处于更新阶段
- 如果处于渲染阶段,那么不需要我们在更新当前函数组件,只需要更新一下当前
update
的expirationTime
即可 - 没有处于更新阶段,获取最新的
state
,和上一次的currentState
,进行浅比较 -
- 如果相等,那么就退出
- 不相等,那么调用
scheduleUpdateOnFiber
调度渲染当前fiber