响应式原理-Promise的使用详解(一)

615 阅读7分钟

跟着coderwhy学习

1.对象的依赖管理

  • 我们目前是创建了一个Depend对象,用来管理对于name变化需要监听的响应函数:
    • 但是实际开发中我们会有不同的对象,另外会有不同的属性需要管理;
    • 我们如何可以使用一种数据结构来管理不同对象的不同依赖关系呢?
  • 在前面刚刚学习过WeakMap,并且在学习WeakMap的时候我讲到了后面通过WeakMap如何管理这种响应式的数据依赖:

image.png

2.对象依赖管理的实现

  • 可以写一个getDepend函数专门来管理这种依赖关系:
// 封装一个获取depend函数
const targetMap = new WeakMap()
function getDepend(target, key) {
    // 根据target对象获取map的过程
    let map = targetMap.get(target)
    if (!map) {
        map = new Map()
        targetMap.set(target, map)
    }

    // 根据key获取depend对象
    let depend = map.get(key)
    if (!depend) {
        depend = new Depend()
        map.set(key, depend)
    }
    return depend
}

const objProxy = new Proxy(obj, {
    get: function(target, key, receiver) {
        // 根据target.key获取对应的depend
        const depend = getDepend(target, key)
        // 给depend对象中添加响应函数
        depend.addDepend(activeReactiveFn)

        return Reflect.get(target, key, receiver)
    },
    set: function(target, key, newValue, receiver) {
        Reflect.set(target, key, newValue, receiver)
        // depend.notify()
        const depend = getDepend(target, key)
        depend.notify()
    }
})

3.正确的依赖收集

  • 之前收集依赖的地方是在 watchFn 中:
    • 但是这种收集依赖的方式我们根本不知道是哪一个key的哪一个depend需要收集依赖;
    • 你只能针对一个单独的depend对象来添加你的依赖对象;
  • 那么正确的应该是在哪里收集呢?应该在我们调用了Proxy的get捕获器时
    • 因为如果一个函数中使用了某个对象的key,那么它应该被收集依赖;
// 封装一个响应式的函数
let activeReactiveFn = null
function watchFn(fn) {
    activeReactiveFn = fn
    fn()
    activeReactiveFn = null
}

const objProxy = new Proxy(obj, {
    get: function(target, key, receiver) {
        // 根据target.key获取对应的depend
        const depend = getDepend(target, key)
        // 给depend对象中添加响应函数
        depend.addDepend(activeReactiveFn)
        return Reflect.get(target, key, receiver)
    },
    set: function(target, key, newValue, receiver) {
        Reflect.set(target, key, newValue, receiver)
        const depend = getDepend(target, key)
        depend.notify()
    }
})

4.对Depend重构

  • 但是这里有两个问题:
    • 问题一:如果函数中有用到两次key,比如name,那么这个函数会被收集两次;
    • 问题二:我们并不希望将添加reactiveFn放到get中,以为它是属于Dep的行为;
  • 所以我们需要对Depend类进行重构:
    • 解决问题一的方法:不使用数组,而是使用Set;
    • 解决问题二的方法:添加一个新的方法,用于收集依赖;

image.png

image.png

image.png

5.Vue2响应式原理

  • 前面所实现的响应式的代码,其实就是Vue3中的响应式原理:
    • Vue3主要是通过Proxy来监听数据的变化以及收集相关的依赖的;
    • Vue2中通过我们前面学习过的Object.defineProerty的方式来实现对象属性的监听;
  • 我们可以将reactive函数进行如下的重构:
    • 在传入对象时,我们可以遍历所有的key,并且通过属性存储描述符来监听属性的获取和修改;
    • 在setter和getter方法中的逻辑和前面的Proxy是一致 的;
function reactive2(obj) {
    Object.keys(obj).forEach(key => {
        let value = obj[key]
        Object.defineProperty(obj, key, {
            get: function () {
                const depend = getDepend(obj, key)
                    depend.depend()
                    return value
                },
            set: function(newValue) {
                value = newValue
                const depend = getDepend(obj, key)
                depend.notify()
            }
        })
    })
    return obj
}

6.异步任务的处理

  • 在ES6出来之后,有很多关于Promise的讲解、文章,也有很多经典的书籍讲解Promise

    • 虽然等你学会Promise之后,会觉得Promise不过如此,但是在初次接触的时候都会觉得这个东西不好理解;
  • 那么这里我从一个实际的例子来作为切入点:

    • 我们调用一个函数,这个函数中发送网络请求(我们可以用定时器来模拟);
    • 如果发送网络请求成功了,那么告知调用者发送成功,并且将相关数据返回过去;
    • 如果发送网络请求失败了,那么告知调用者发送失败,并且告知错误信息;
    function requestData(url) {
        // 模拟请求网络请求
        setTimeout(() => {
            if (url === 'mint') {
                // 请求成功
                let names = ['abc','cba','nba']
                successCallback(names)
            } else { 
                // 请求失败
            }
        }, 3000)
    }
    
    function requestData(url, successCallback, failureCallback) {
        // 模拟请求网络请求
        setTimeout(() => {
            // 拿到请求结果
            // url 传入的是mint,请求成
            if (url === 'mint') {
                //成功
                let names = ['abc','cba','nba']
                successCallback(names)
            } else { // 否则请求失败
                // 失败
                let errMessage = '请求失败, url错误'
                failureCallback(errMessage)
            }
        }, 3000)
    }
    

7.什么是Promise呢?

  • 在上面的解决方案中,我们确确实实可以解决请求函数得到结果之后,获取到对应的回调,但是它存在两个主要的问题:

    • 第一,我们需要自己来设计回调函数、回调函数的名称、回调函数的使用等;
    • 第二,对于不同的人、不同的框架设计出来的方案是不同的,那么我们必须耐心去看别人的源码或者文档,以便可以理解它这个函数到底怎么用;
  • 来看一下Promise的API是怎么样的:

    • Promise是一个类,可以翻译成 承诺、许诺 、期约;
    • 当我们需要给予调用者一个承诺:待会儿我会给你回调数据时,就可以创建一个Promise的对象;
    • 在通过new创建Promise对象时,我们需要传入一个回调函数,我们称之为executor
      • 这个回调函数会被立即执行,并且给传入另外两个回调函数resolve、reject;
      • 当我们调用resolve回调函数时,会执行Promise对象的then方法传入的回调函数;
      • 当我们调用reject回调函数时,会执行Promise对象的catch方法传入的回调函数;

8.Promise的代码结构

  • 来看一下Promise代码结构
const promise = new Promise((resolve, reject) => {
    // 调用resolve, 那么then传入的回调会被执行
    resolve("哈哈哈哈")
    // 调用reject,那么catch传入的回调会被执行
    reject("错误信息")
})

promise.then(res => {
    console.log(res)
}).catch(err => {
     console.log(err)
})
  • 上面Promise使用过程,我们可以将它划分成三个状态:
    • (pending): 初始状态,既没有被兑现,也没有被拒绝;
      • 当执行executor中的代码时,处于该状态;
    • 现(fulfilled): 意味着操作成功完成;
      • 执行了resolve时,处于该状态;
    • 绝(rejected): 意味着操作失败;
      • 执行了reject时,处于该状态;

9.Promise重构请求

  • 那么有了Promise,我们就可以将之前的代码进行重构了:
function requestData(url) {
  // 异步请求的代码会被放入到executor中
  return new Promise((resolve, reject) => {
    // 模拟网络请求
    setTimeout(() => {
      // 拿到请求的结果
      // url传入的是mint, 请求成功
      if (url === "mint") {
        // 成功
        let names = ["abc", "cba", "nba"]
        resolve(names)
      } else { // 否则请求失败
        // 失败
        let errMessage = "请求失败, url错误"
        reject(errMessage)
      }
    }, 3000);
  })
}

10.Executor

  • Executor是在创建Promise时需要传入的一个回调函数,这个回调函数会被立即执行,并且传入两个参数:
new Promise((resolve, reject) => {
    console.log('executor代码
}
  • 我们会在Executor中确定我们的Promise状态:
    • resolve,可以兑现(fulfilled)Promise的状态,我们也可以称之为已决议(resolved);
    • reject,可以拒绝(reject)Promise的状态;
  • 这里需要注意:一旦状态被确定下来,Promise的状态会被 锁死,该Promise的状态是不可更改的
    • 调用resolve的时候,如果resolve传入的值本身不是一个Promise,那么会将该Promise的状态变成 兑现(fulfilled);
  • 后我们去调用reject时,已经不会有任何的响应了(并不是这行代码不会执行,而是无法改变Promise状态);

11.resolve不同值的区别

  • 一:如果resolve传入一个普通的值或者对象,那么这个值会作为then回调的参数;
  • 情况二:如果resolve中传入的是另外一个Promise,那么这个新Promise会决定原Promise的状态:
  • 情况三:如果resolve中传入的是一个对象,并且这个对象有实现then方法,那么会执行该then方法,并且根据then方法的结果来决定Promise的状态:

image.png