林间有风

郭朝夕的日记小站

  • 首页
  • 关于
  • 标签
  • 归档

谈谈对Promise的理解

发表于 2024-04-07

文字理解

首先,Promise 是 JavaScript 中用于处理异步操作的一种对象,它代表了异步操作最终完成(或失败)及其结果值的状态。Promise 对象有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败),并且状态一旦改变就不会再变。

Promise 的主要优点在于它能够帮助我们更好地组织异步代码,避免回调地狱(Callback Hell)的问题。通过链式调用.then()和.catch()方法,我们可以将异步操作的结果传递给后续的处理函数,从而形成一个清晰、可预测的执行流程。

在 Promise 中,.then()方法用于指定当 Promise 成功(fulfilled)时执行的回调函数,而.catch()方法则用于指定当 Promise 失败(rejected)时执行的回调函数。这样,我们就可以将错误处理和正常流程分开,使得代码更加整洁和易于维护。

此外,Promise 还支持链式操作,即在一个.then()或.catch()方法内部返回一个新的 Promise 对象,从而实现多个异步操作的串联。这种链式调用方式使得代码逻辑更加清晰,也更容易理解和调试。

在实际开发中,Promise 广泛应用于各种异步场景,如网络请求、定时器、文件读写等。通过使用 Promise,我们可以更好地控制异步操作的执行顺序和结果处理,提高代码的可读性和可维护性。

需要注意的是,虽然 Promise 提供了很多便利,但它并不是万能的。在一些复杂的异步场景中,我们可能需要结合其他技术(如 async/await、generators 等)来更好地处理异步操作。因此,作为一名前端开发工程师,我们需要不断学习和掌握新的技术,以便更好地应对各种挑战和需求。

总之,Promise 是 JavaScript 中处理异步操作的重要工具之一,它能够帮助我们更好地组织代码、避免回调地狱问题,并提高代码的可读性和可维护性。在实际开发中,我们应该根据具体需求选择合适的异步处理技术,并结合其他技术来构建高效、可靠的 Web 应用。

实际使用场景

阅读全文 »

调试vue项目

发表于 2024-03-22

使用 vscode 调试 vue 项目

调试 vue2 项目

第一步我们先在 vscode 中创建一个调试项目,然后对launch.json进行配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"version": "0.2.0",
"configurations": [
{
"type": "Chrome",
"request": "launch",
"name": "Chrome浏览器调试司机端",
"url": "http://localhost:8080",
"webRoot": "${workspaceFolder}",
"sourceMapPathOverrides": {
"webpack:///src/*": "${workspaceFolder}/src/*"
}
}
]
}

第二步,我们在项目中打个断点。

第三步,在 vscode 中点击启动调试。

非原始值响应式方案代理数组(一)

发表于 2024-03-07

数组的索引与 length

拿下面这个例子来说,当通过数组索引访问元素的值时,已经能够建立响应联系了:

1
2
3
4
5
6
const arr = reactive(['foo'])

effect(() => {
console.log(arr[0]) // 'foo'
})
arr[0] = 'bar' // 能够触发响应

但是通过索引设置数组的元素值与设置对象的属性值仍然存在根本上的不同,这是因为数组对象部署的内部方法[[DefineOwnProperty]]不同于常规对象。实际上,当我们通过索引设置数组元素的值时,会执行数组对象所部署的内部方法[[Set]],这一步与设置常规对象的属性值一样。根据规范可知,内部方法[[Set]]其实依赖于[[DefineOwnProperty]],到了这里就体现出了差异。数组对象所部署的内部方法[[DefineOwnProperty]]的逻辑定义在规范的 10.4.2.1 节。

阅读全文 »

非原始值响应式方案代理数组

发表于 2024-03-07

代理数组

数组只是一个特殊的对象而已,因此想要更好地实现对数组的代理,就有必要了解相比普通对象,数组到底有何特殊之处。在之前的文章中我们知道在 JavaScript 中有两种对象:常规对象和异质对象。而这次要介绍的数组就是一个异质对象,这是因为数组对象的[[DefineOwnProperty]]内部方法与常规对象不同。换句话说,数组对象除了[[DefineOwnProperty]]这个内部方法之外,其他内部方法的逻辑都与常规对象相同。因此,当实现对数组的代理时,用于代理普通对象的大部分代码可以继续使用,如下所示:

1
2
3
4
5
6
const arr = reactive(['foo'])

effect(() => {
console.log(arr[0]) // 'foo'
})
arr[0] = 'bar' // 能够触发响应

上面这段代码能够按照预期工作。实际上,当我们通过索引读取或者设置数组元素的值时,代理对象的get/set拦截函数也会执行,因此我们不需要做任何额外的工作,就能够让数组索引的读取和设置操作是响应式的了。

但对数组的操作与普通对象的操作仍然存在不同,下面总结了所有对数组元素或属性的“读取”操作。

  • 通过索引访问数组元素值:arr[0]。
  • 访问数组的长度:arr.length。
  • 把数组作为对象,使用 for…in 循环遍历。
  • 使用 for…of 迭代遍历数组。
  • 数组的原型方法,如 concat/join/every/some/find/findIndex/includes 等,以及其他所有不改变原数组的原型方法。

可以看到,对数组的读取操作要比普通对象丰富得多。我们再来看看对数组元素或属性的设置操作有哪些。

  • 通过索引修改数组元素值:arr[1] = 3。
  • 修改数组长度:arr.length = 0.
  • 数组的栈方法:push/pop/shift/unshift。
  • 修改原数组的原型方法:splice/fill/sort 等。

除了通过数组索引修改数组元素值这种基本操作之外,数组本身还有很多会修改原数组的原型方法。调用这些方法也属于对数组的操作,有些方法的操作语义是“读取”,而有些方法的操作语义是“设置”。因此,当这些操作发生时,也应该正确的建立响应联系或触发响应。

从上面列出的这些对数组的操作来看,似乎代理数组的难度要比代理普通对象的难度大很多。但事实并非如此,这是因为数组本身也是对象,只不过它是异质对象罢了,它与常规对象的差异并不大。因此,大部分用来代理常规对象的代码对于数组也是生效的。

非原始值响应式方案(六)

发表于 2024-03-06

只读和浅只读

我们希望一些数据是只读的,当用户尝试修改只读数据时,会收到一条警告信息。这样就实现了对数据的保护,例如组件接收到的 props 对象应该是一个只读数据。这是就要用到接下来我们要讨论的readonly函数,它能够将一个数据变成只读的:

1
2
3
const obj = readonly({ foo: 1 })

obj.foo = 2

只读本质上也是对数据对象的代理,我们同样可以使用createReactive函数来实现。如下面的代码所示,我们为 createReactive 函数增加第三个参数 isReadonly:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 增加第三个参数isReadonly 代表是否只读,默认为false,即非只读
function createReactive(obj, isShallow = false, isReadonly = false) {
return new Proxy(obj, {
// 拦截设置操作
set(target, key, newVal, receiver) {
if (isReadonly) {
console.warn(`属性 ${key}是只读的`)
return true
}
// 先获取旧值
const oldVal = target[key]
// type看看操作类型是添加新属性还是设置已有属性
const type = Object.prototype.hasOwnProperty.call(target, key)
? 'SET'
: 'ADD'
// 拿到执行结果
const res = Reflect.set(target, key, newVal, receiver)
// 如果两者相等,说明receiver是target的代理对象
if (target === receiver.raw) {
if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
trigger(target, key, type)
}
}
return res
},
deleteProperty(target, key) {
// 如果是只读的,则打印警告信息并返回
if (isReadonly) {
console.warn(`属性 ${key}是只读的`)
}
// 检查被操作的属性是否是对象自身的属性
const hadKey = Object.prototype.hasOwnProperty.call(target, key)
// 使用Reflect.deleteProperty完成属性的删除
const res = Reflect.deleteProperty(target, key)

if (res && hadKey) {
// 只有当被删除的属性是自身属性且成功被删除时,才触发更新
trigger(target, key, 'DELETE')
}
// 返回结果
return res
},
})
}
阅读全文 »

非原始值响应式方案(五)

发表于 2024-03-06

浅响应与深响应

本节中我们将要介绍reactive和shallowReactive的区别,即浅响应与深响应的区别。实际上,我们目前所实现的reactive是浅响应的。拿如下代码来说:

1
2
3
4
5
6
const obj = reactive({ foo: { bar: 1 } })

effect(() => {
console.log(obj.foo.bar)
})
obj.foo.bar = 2

首先,创建 obj 代理对象,该对象的 foo 属性值也是一个对象,即{bar: 1}。接着,在副作用函数内部访问 obj.foo.bar 的值。但是我们发现,后续对 obj.foo.bar 的修改不能触发副作用函数重新执行,这是为什么呢?我们来看一下现在的实现:

1
2
3
4
5
6
7
8
9
10
11
12
function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
if (key === 'raw') {
return target
}
track(target, key)

return Reflect.get(target, key, receiver)
},
})
}
阅读全文 »

非原始值的响应式方案(四)

发表于 2024-03-05

合理触发响应

在非原始值的响应式方案(三)这一章中我们处理了很多边界条件。比如我们需要明确知道操作的类型是'ADD'还是'SET',或者是其他操作类型,从而正确的触发响应。但是想要合理的触发响应,还有许多工作要做。

首先我们来看第一个问题,即当值没有发生变化时,应该不需要触发响应才对:

1
2
3
4
5
6
7
8
9
10
const obj = { name: '张三' }
const p = new Proxy(obj, {
/** ... */
})

effect(() => {
console.log(p.name)
})

p.name = '张三'

如上面的代码所示,p.name的初始值为’张三’,当为p.name设置新的值时,如果值没有发生变化时则不需要触发响应。为了满足需求,我们需要修改set拦截函数的代码,在调用trigger函数触发响应之前,需要检查值是否真的发生了变化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const p = new Proxy(obj, {
set(target, key, newVal, receiver) {
// 先获取旧值
const oldVal = target[key]

// 看看操作类型是设置已有属性还是添加新属性
const type = Object.prototype.hasOwnProperty.call(target, key)
? 'SET'
: 'ADD'
// 拿到执行结果
const res = Reflect.set(target, key, newVal, receiver)
// 比较新值与旧值 只要当不全等的时候才会触发响应
if (oldVal !== newVal) {
trigger(target, key, type)
}

return res
}
})

如上面代码所示,我们在 set 拦截函数内首先获取旧值 oldVal, 接着比较新值与旧值,只有当他们不完全相等时才会触发响应。现在,如果我们再次测试开头的那个例子,会发现重新设置相同的值已经不会触发响应了。

阅读全文 »

非原始值的响应式方案(三)

发表于 2024-03-04

如何代理Object

前面我们使用get拦截函数去拦截对属性的读取操作。但在响应系统中,“读取”是一个很宽泛的概念,例如使用in操作符检查对象上是否具有给定的key也属于“读取”操作,如下面的代码所示:

1
2
3
effect(() => {
'foo' in obj
})

这本质上也是在进行“读取操作”。响应系统应该拦截一切读取操作,以便当数据变化的时候能够正确触发响应。下面列出了对一个普通对象的所有可能的读取操作。

  • 访问属性:obj.name。
  • 判断对象或原型上是否存在给定的 key:key in obj。
  • 使用 for…in 循环遍历对象:for (const key in obj){}。

接下来,我们逐步讨论如何拦截这些读取操作。首先是对于属性的读取,例如obj.name,我们知道这可以通过get拦截函数实现:

1
2
3
4
5
6
7
8
9
10
const obj = { name: '张三' }

const p = new Proxy(obj, {
get(target, key, receiver) {
// 建立联系
track(target, key)
// 返回属性值
return Reflect.get(target, key, receiver)
},
})

对于in操作符,应该如何拦截呢?通过查看规范可知,in 操作符的运算结果是通过调用一个叫作HasProperty的抽象方法得到的。关于这个抽象方法,我们查询规范得知它的返回值是通过调用对象的内部方法[[HasProperty]]得到的。而[[HasProperty]]内部方法可以在我们之前的表格中找到,和它对应的拦截器函数名字叫has,因此我们可以通过has拦截函数实现对 in 操作符的代理:

1
2
3
4
5
6
7
8
const obj = { name: '张三' }

const p = new Proxy(obj, {
has(target, key) {
track(target, key)
return Reflect.has(target, key)
},
})

这样当我们在副作用函数中通过 in 操作符操作响应式数据时,就能够建立依赖关系:

1
2
3
effect(() => {
'name' in p // 建立依赖关系
})
阅读全文 »

非原始值响应式方案(二)

发表于 2024-03-04

JavaScript对象和proxy的工作原理

我们经常听到这样一句话:“JavaScript中一切皆对象。”那么,到底什么是对象呢?根据ECMAScript规范,在JavaScript中有两种对象,其中一种叫作常规对象,另外一种叫做异质对象。这两种对象包含了JavaScript中所有的对象,任何不属于常规对象的都是异质对象。那么到底什么是常规对象,什么是异质对象呢?

满足以下3点的都是常规对象:

  • 对于表5-1列出的内部方法,必须使用ECMA规范10.1.x节给出的定义实现;
  • 对于内部方法[[Call]],必须使用ECMA规范10.2.1节给出的定义实现;
  • 对于内部方法[[Construct]],必须使用ECMA规范10.2.2给出的定义实现。

而不符合这3点要求的对象都是异质对象。例如Proxy对象的内部方法[[Get]]没有使用ECMA规范的10.1.8节给出的定义实现,所以Proxy是一个异质对象。

接下来,我们就具体看看Proxy对象。既然Proxy也是对象,那么它本身也部署了上述必要的内部方法,当我们通过代理对象访问属性值时:

1
2
const p = new Proxy(obj, { /** ... */ })
p.foo

实际上,引擎会调用部署在对象p上的内部方法[[Get]]。到这一步,其实代理对象和普通对象没有什么区别。他们的区别在于内部方法的[[Get]]的实现,这里就体现了内部方法的多态性,即不同的对象部署相同的内部方法,但是它们的行为可能不同,具体的不同表现在,如果在创建代理对象的时候没有指定对应的拦截函数,例如没有指定get()拦截函数,那么当我们通过代理对象访问属性值时,代理对象的内部方法[[Get]]会调用原始对象的内部方法[[Get]]来获取属性值,这其实就是代理透明性质。

现在明白了,创建代理对象时指定的拦截函数,实际上是用来自定义代理对象本身(这里指的是对象P)的内部方法和行为的,而不是用来指定被代理对象(这里指的是对象obj)的内部方法和行为的。下面这张表格列出了Proxy对象部署的所有内部方法以及用来自定义内部方法和行为的拦截函数名字。

内部方法 处理器函数
[[GetPropertyOf]] getPropertyOf
[[SetPropertyOf]] setPropertyOf
[[IsExtensible]] isExtensible
[[PrevenExtensions]] prevenExtensions
[[GetOwnProperty]] getOwnPropertyDescriptor
[[DefineOwnProperty]] defineProperty
[[HasProperty]] has
[[Get]] get
[[Set]] set
[[Delete]] deleteProperty
[[OwnPropertyKeys]] ownKeys
[[Call]] apply
[[Construct]] construct
其中,[[Call]]和[[Construct]]这两个内部方法只有当被代理对象是函数和构造函数时才会部署。

由上表可知,当我们要拦截删除属性时,可以使用deleteProperty拦截函数实现:

1
2
3
4
5
6
7
8
9
const obj = { foo: 1 }
const p = new Proxy(obj, {
deleteProperty(target, key) {
return Reflect.deleteProperty(target, key)
}
})
console.log(obj.foo) // 1
delete p.foo
console.log(p.foo) // 未定义

这里要强调的是,deleteProperty实现的是代理对象p的内部方法和行为,所以为了删除被代理对象上的属性值,我们需要使用Reflect.deleteProperty(target, key)来完成。

非原始值的响应式方案(一)

发表于 2024-03-01

理解Proxy和Reflect

既然Vue.js3的响应式数据是基于Proxy实现的,那么我们就有必要了解Proxy和与之关联的Reflect。简单说,使用Proxy可以创建一个代理对象。它能够实现对其他对象的代理,这里的关键词是其他对象, 也就是说,它只能代理对象,无法代理非对象值,例如字符串、布尔值等。那么代理指的是什么呢?所谓代理,指的是对一个对象基本语义的代理。它允许我们拦截并重新定义对一个对象的基本操作。这句话的关键词比较多,我们逐一解释。

什么是基本语义?给出一个对象obj,可以对它进行一些操作,例如读取属性值、设置属性值:

1
2
obj.name // 读取属性name属性值
obj.name = 'lisi' // 读取和设置属性name的值

类似这种读取属性、设置属性值的操作,就属于基本语义的操作,即基本操作。既然是基本操作,那就可以使用Proxy进行拦截:

1
2
3
4
5
6
7
8
const p = new Proxy(obj, {
get() {
/*... */
},
set() {
/*... */
}
})

如上代码所示,Proxy构造函数接收两个参数。第一个参数是被代理的对象,第二个参数也是一个对象,这个对象是一对夹子(trap)。其中get函数用来拦截读取操作,set函数用来拦截设置操作。
在JavaScript的世界里,万物皆对象。例如一个函数也是一个对象,所以调用函数也是对一个对象的基本操作:

1
2
3
4
5
const fn = (name) => {
console.log('我是:', name)
}
// 调用函数是对对象的基本操作
fn()

因此我们可以使用Proxy来拦截函数的基本操作,这里我们使用apply拦截函数的调用:

1
2
3
4
5
6
7
const p2 = new Proxy(fn, {
appy(target, thisArg, argArray) {
target.call(thisArg, ...argArray)
}
})

p2('guozhaoxi') // 输出:'我是:guozhaoxi'

上面两个例子说明了什么是基本操作。Proxy只能够拦截一个对象的基本操作。那么,什么是非基本操作呢?其实调用对象下的方法就是典型的非基本操作,我们叫它复合操作:

1
obj.fn()

实际上,调用一个对象下的方法,是由两个基本语义组成的。第一个基本语义是get,即先通过get操作得到obj.fn属性。第二个基本语义是函数调用,即通过get得到obj.fn的值后再调用它,也就是我们上面所说到的apply。理解Proxy只能够代理对象的基本语义很重要,后续我们讲解如何实现对数组或者Map、Set等数据类型的代理时,都利用了Proxy的这个特点。

理解了Proxy,我们再来讨论Reflect。Reflect是一个全局对象,其下有许多方法。例如:

1
2
3
4
Reflect.get()
Reflect.set()
Reflect.apply()
// ...

你可能已经注意到了,Reflect下的方法与Proxy的拦截器方法名字相同,其实这不是偶然。任何在Proxy的拦截器中能够找到的方法,都能够在Reflect中找到同名函数,那么这些函数的作用是什么呢?其实它们的作用一点都不神秘。拿Reflect.get函数来说,它的功能就是提供了访问一个对象属性的默认行为,例如下面这两个操作是等价的:

1
2
3
4
5
const obj = { count: 1 }
// 直接读取
console.log(obj.count) // 1
//使用Reflect.get读取
console.log(Reflect.get(obj, 'count')) // 1

既然操作等价,那么它存在的意义是什么呢?实际上Reflect.get函数还能接受第三个参数,即指定接收者receiver,你可以把它理解为函数调用过程中的this,例如:

1
2
const obj = { count: 1 }
console.log(Reflect.get('obj', 'count', { count: 2 })) // 输出2 而不是1

在这段代码中,我们指定第三个参数receiver为一个对象{count: 2},这是读取到的值是receiver对象的count属性值。为了说明问题,回顾一下前面实现响应式数据的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const obj = { count: 1 }

const p = new Proxy(obj, {
get(target, key) {
track(target, key)

// 这里没有使用Reflect.get完成读取
return target[key]
},
set(target, key, newVal) {
// 这里没有使用Reflect.set完成设置
target[key] = newVal
trigger(target, key)
}
})

这是之前用来实现响应式数据的基本代码。在get和set拦截函数中,我们都是直接使用原始对象target来完成对属性的读取和设置操作的,其中原始对象target就是上述代码中的obj对象。
那么这段代码有什么问题呢?我们借助effect让问题暴露出来。首先我们修改一下obj对象,为它添加一个bar属性:

1
2
3
4
5
6
const obj = {
foo: 1,
get bar() {
return this.foo
}
}

可以看到bar属性是一个访问器属性,它返回了this.foo属性的值。接着我们在effect副作用函数中通过代理对象p访问bar属性:

1
2
3
effect(() => {
console.log(bar) // 1
})

我们来分析一下这个过程发生了什么。当effect注册的副作用函数执行时,会读取p.bar属性,它发现p.bar是一个访问器属性,因此执行getter函数。由于在getter函数中通过this.foo读取了foo属性值,因此我们认为副作用函数与属性foo之间建立了联系。当我们修改p.foo的值时应该能够触发响应,使得副作用函数重新执行才对。然而实际并非如此,当我们尝试修改p.foo的值时:

1
p.foo++

副作用函数并没有重新执行,问题出在哪里呢?实际上,问题就出在bar属性的访问器函数getter里:

1
2
3
4
5
6
7
const obj = {
foo: 1,
get bar() {
// 这里的this指向的是谁
return this.foo
}
}

当我们使用this.foo读取foo属性值时,这里的this指向的是谁呢?我们回顾一下整个流程。首先,我们通过代理对象p访问p.bar,这会触发代理对象的get拦截函数执行:

1
2
3
4
5
6
const p = new Proxy(obj, {
get(target, key) {
track(target, key)
return target[key]
}
})

在get拦截函数内,通过target[key]返回属性值。其中target是原始对象obj,而key就是字符串’bar’,所以target[key]相当于obj.bar。因此,当我们使用p.bar访问bar属性时,它的target函数内的this指向的其实是原始对象obj,这说明我们最终访问的其实是obj.foo。

很显然,在副作用函数内通过原始对象访问它的某个属性是不会建立响应联系的,这等价于:

1
2
3
4
effect(() => {
// obj是原始对象,不是代理对象,这样的访问不能建立响应联系
obj.foo
})

因为这样做不会建立响应联系,所以就出现了无法触发响应的问题。那么这个问题应该如何解决呢?这时Reflect.get函数就派上用场了。先给出解决问题的代码:

1
2
3
4
5
6
const p = new Proxy(obj,{
get(target, key, receiver) {
track(target, key)
return Reflect.get(obj, key, receiver)
}
})

如上面代码所示,代理对象的get拦截函数接收第三个参数receiver,它代表谁在读取属性,例如:

1
p.bar // 代理对象p在读取bar属性

当我们使用代理对象p访问bar属性时,那么receiver就是p。你可以把它理解为函数调用中的this。接着关键的一步发生了,我们使用Reflect.get(target, key, receiver)代替target[key],这里的关键点就是第三个参数receiver。我们已经知道它就是代理对象p,所以访问器属性bar的getter函数内的this指向代理对象p:

1
2
3
4
5
6
7
const obj = {
foo: 1,
get bar() {
// 这里的this指向的是代理对象p
return this.foo
}
}

可以看到,this由原始对象obj变成了代理对象p。很显然,这会在副作用函数与响应式数据之间建立响应联系,从而达到依赖收集的效果。如果此时再对p.foo进行自增操作,会发现已经能够触发副作用函数重新执行了。

上一页1234…7下一页

62 日志
19 标签
© 2025 郭朝夕
冀ICP备18008237号