阅读视图

发现新文章,点击刷新页面。

vue2vue3响应式

响应式基础

vue开篇提到了怎么在vue的选项式写法中声明组件状态,就是在对象中写一个data属性,这个属性要是一个函数,这个函数要返回一个对象,返回的对象会被vue在合适的时候调用赋予它响应的能力,然后vue会把这个对象上的属性都放到组件自身上, 我们再讨论接下来的问题之前c,先展示vue2以及vue3是怎么大致实现响应式的, 帮助理解

vue2响应式

vue2实现响应式的思路就是给对象加setter和getter,把这些属性全部挂载到组件实例对象上, 然后给每个属性添加上setter更新值的时候要触发的响应函数就可以实现响应式了,具体看下面这个js例子

class Dep {
  constructor() {
    this.bukets = [];
  }
  addDep(fn) {
    this.bukets.push(fn);
  }

  notify() {
    this.bukets.forEach((fn) => {
      fn.update();
    });
  }
}


//观察者
class Watcher {
  constructor(obj, name, updateCb) {
    this.updateCb = updateCb;
    this.init(obj, name);
  }
  init(obj, name) {
    //把注册函数送出去,注册好响应式
    Dep.target = this;
    obj[name]; // 触发Dep响应,添加进这个watcher者
    this.update();
    Dep.target = null;
  }

  update() {
    this.updateCb();
  }
}


//定义给对象响应式属性
const defineReactive = (obj, key, val) => {
  //为这个属性实例化一个观察者
  const dep = new Dep();
  Object.defineProperty(obj, key, {
    get() {
      //当触发key时,说明要使用这个依赖
      if (Dep.target) {
        dep.addDep(Dep.target);
      }
      return val;
    },
    set(newVal) {
      val = newVal;
      //通知
      dep.notify();
      
    }
  });
};


<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    #app {
      display: inline-flex;
      column-gap: 10px;
      padding: 10px 12px;
      border-radius: 8px;
      margin: 100px 200px;
      background-color: #f5f5f5;
      cursor: pointer;
      user-select: none;
    }
    #app span {
      display: flex;
      justify-content: center;
      align-items: center;
      width: 20px;
      height: 30px;
      background-color: #ececec;
    }
  </style>
</head>
<body>
  <div id="app">
    <span data-action="sub">-</span>
    <span class="count"></span>
    <span data-action="add">+</span>
  </div>

  <script src="./index.js"></script>
  <script>
    let obj = {};
    defineReactive(obj, "count", 0);
    const countEle = document.querySelector(".count");


    new Watcher(obj, "count", () => {
      countEle.innerText = obj.count;
    });

    document.querySelector("[data-action='sub']").addEventListener("click", () => {
      obj.count--;
    });
    document.querySelector("[data-action='add']").addEventListener("click", () => {
      obj.count++;
    });

  </script>
</body>
</html>



关注我们重点的最开头的四个函数,这就是vue2大致实现响应式的样子,我们可以看到,我们实际上是给data指定的数据使用Object.defineProperty定义了get和set函数, , 然后在初始的时候在get函数里添加上watcher,,在这个属性触发set的时候,我们通知这些watcher使用最新的值进行更新,这就是大致流程, 然后我们再来看看vue3对于响应式是怎么实现的

vue3响应式

let activeFn;

const effect = (fn) => {
  activeFn = fn;
  fn();
  activeFn = null;
};

const buckets = new WeakMap();

const trigger = (target, property) => {
  const depsMap = buckets.get(target);
  if (!depsMap) {
    return ;
  }

  const fns = depsMap.get(property);

  console.log(fns, "fns");
  fns && fns.forEach(fn => fn());
};
const track = (target, property) => {
  let depsMap = buckets.get(target);
  if (!depsMap) {
    buckets.set(target, (depsMap = new Map()));
  }

  let deps = depsMap.get(property);

  if (!deps) {
    depsMap.set(property, (deps = new Set()));
  }

  deps.add(activeFn);
};

const reactive = (data) => {
   return new Proxy(data, {
    set(target, property, newVal, receiver) {
      trigger(target, property);
      return Reflect.set(target, property, newVal, receiver);
    },
    get(target, property, receiver) {
      if (activeFn) {
        console.log(target,property, "target-property");
        track(target, property);
      }
      console.log("触发set");
      return Reflect.get(target, property, receiver);
    }
  });
};
  <style>
    #app {
      display: inline-flex;
      column-gap: 10px;
      padding: 10px 12px;
      border-radius: 8px;
      margin: 100px 200px;
      background-color: #f5f5f5;
      cursor: pointer;
      user-select: none;
    }
    #app span {
      display: flex;
      justify-content: center;
      align-items: center;
      width: 20px;
      height: 30px;
      background-color: #ececec;
    }
  </style>
</head>
<body>
  <div id="app">
    <span data-action="sub">-</span>
    <span class="count"></span>
    <span data-action="add">+</span>
  </div>

  <script src="./index2.js"></script>
  <script>
    let obj = {count: 0};
    obj = reactive(obj);
    const countEle = document.querySelector(".count");

    
    effect(() => {
      countEle.innerText = obj.count;
    });


    document.querySelector("[data-action='sub']").addEventListener("click", () => {
      obj.count--;
    });
    document.querySelector("[data-action='add']").addEventListener("click", () => {
      obj.count++;
    });

  </script>
</body>

我们可以看到,我们基于Proxy实现的响应式系统是现有一个obj对象, 然后我们定义了一个代理对象,我们后续都是操作这个代理对象去实现响应式更新

总结

基于上述描述,我们可以知道,vue2的响应式的确是在原始对象上定义了一个新的属性然后设置get和set,我们在这个对象属性上触发了set的时候,也会触发响应函数更新, 在vue3的时候,是现有原始的对象,我们给这个对象设置了一个代理对象,后续的响应式都是通过触发代理对象的set和get实现的,在代理对象上触发了set的时候,会触发响应函数更新, 完全与原始对象解耦了。同时也可以注意到,我们在vue2的实现中,并没有return 一个函数或者是包含函数的对象,但是我们的属性val,却因为defineProperty的实现而被留存了下来,通过这种形式也实现了一个闭包,所以我们可以说,没有return一个使用了内部变量的函数就不是闭包的说法是错误的,只要实现了将内部变量外泄到外部代码,并且外部代码只能受控的间接访问这个内部变量的这么个现象,我们就可以认为是一个闭包,return一个使用了内部变量的函数只是实现的一个具体方法。

回到Vue文档

查看下面一个vue文档给出的例子

export default {
  data() {
    return {
      someObject: {}
    }
  },
  mounted() {
    const newObject = {}
    this.someObject = newObject

    console.log(newObject === this.someObject) // false
  }
}

当你在复制后再访问this.someObject, 这个时候因为触发了this的set函数,属性是someObject, 所以在vue3中会创建一个新的响应式对象,然后复制给this.someObject,这个对象是代理后的对象,它的原始对象是newObject, 而对于vue2,它会接受这个对象,然后在这个对象上设置getter和setter,把这个对象转换成响应式 由于转换是在同一个对象上进行的 ,所以文档说当你在赋值后再访问this.someObject, 此值已经是原来的newOject的一个响应式代理,与vue2 不同的是,这里的原始的newObject不会变为响应式,请确保始终通过this来访问响应式状态

声明方法

先看下面一个例子

export default {
  data() {
    return {
      count: 0
    }
  },
  methods: {
    increment() {
      this.count++
    }
  },
  mounted() {
    // 在其他方法或是生命周期中也可以调用方法
    this.increment()
  }
}

vue文档在这里说不应该使用箭头函数,因为箭头函数的this值是跟着作用域走了,而在对象中使用 ...() {}, 的形式相当于function () {} ,其中的this是由调用方觉定的,所以这里的methods中的方法使用箭头函数后如果是顶层的箭头函数的this就是window,不会改变

响应式状态新增属性

当我们在vue2的响应式状态上新增一个属性的时候,vue2没有办法检测到变化,查看下面一个例子

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>数组列表渲染重点</title>
</head>

<body>
  <div id="app">
    {{obj.nested.count}}
    {{JSON.stringify(obj.nested)}}
    <button @click="mutateDeeply">增加</button>
  </div>
  <script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.11/vue.js"></script>
  <script>
    const app = new Vue({
      el: "#app",
      data() {
        return {
          obj: {
            nested: { count: 0 },
          }
        }
      },
      methods: {
        mutateDeeply() {
          // 以下都会按照期望工作
          this.obj.nested.count++
        }
      }
    })
  </script>
</body>

</html>

如果我们在控制台输入app.obj.nested.count2 = 2;可以发现,这个时候我们的页面并没有发生变化,如果我们换成vue3的写法,会怎么样,请查看下面一个例子

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>数组列表渲染重点</title>
</head>

<body>
  <div id="app">
    {{obj.nested.count}}
    {{JSON.stringify(obj.nested)}}
    <button @click="mutateDeeply">增加</button>
  </div>
  <script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.22/vue.global.min.js"></script>
  <script>
  const app = Vue.createApp({
  data() {
    return {
      obj: {
        nested: { count: 0 },
      }
    }
  },
  methods: {
    mutateDeeply() {
      // 以下都会按照期望工作
      this.obj.nested.count++
    }
  }
}).mount("#app");
  </script>
</body>

</html>

如果我们在上面的这个例子控制台中实时的添加app.obj.nested.count2 = 2;可以看到,页面发生了变化! 这是为什么呢,其实,vue2的响应是基于definePRoperty,这就意味着vue2在实现响应式的时候在统一注册响应式的阶段在对象的属性上定义setter/getter,这个时候新增一个属性,压根就没有给这个对象赋予一个setter/getter,所以也就不会触发setter/getter了,如果是在vue3中,我们使用代理对象,响应是基于整个对象的,如果你新增了一个属性,这个时候就会触发整个对象的getter/setter,然后更新整个页面,所以最后的区别也还是因为vue2的响应式是基于对象属性的,而vue3的响应式是基于整个对象的,这是我们在响应式系统上讨论的vue3和vue2的第二个区别

模板语法部分

vue可以渐进式增强HTML的前提是vue的模板语法不会使得浏览器报错,这样我们才可以在dom中内嵌vue语法,

所以vue文档在开头就说了一件事情, 就是vue的模板语法是语法合格的HTML

同时在在SFC(也就是后缀为.vue的文件)中, 模板被编译器编译后还会对代码进行优化,像手写vue提供的渲染函数代码没有经过编译阶段,就没有这个编译阶段,所以vue文档在开头说了另一件事情就是手写渲染函数而不采用模板,不会受到和模板同等级的编译优化

总结一下,vue文档开头说了两句话,一件是写了vue的语法的模板放浏览器上跑也没有问题,一件是编译器会对SFC的模板进行优化要比自己手写渲染函数好。

v-html

先看下面一个例子,

<body>
  <div id="app">

  </div>

  <!-- App组件 -->
  <template id="appTemplate">
    <div style="width: 100px;height: 100px;background-color: pink;" v-html="rawHtml">

    </div>
  </template>


  <script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.22/vue.global.prod.min.js"></script>
  <script>
    const App = {
      data() {
        return {
          rawHtml: "this is Compo {{count}}"
        }
      },
      template: "#appTemplate",
    };



    const app = Vue.createApp(App);


    app.mount("#app");
  </script>
</body>

可以看到v-html的作用就是插入一个html片段,在这个属性中没有办法使用Vue语法,很适合用来渲染服务器返回的html片段

Attribute绑定

<div v-bind:id="dynamicId"></div>

<div :id="dynamicId"></div>

在这里div标签的id属性的值会保持和组件的dynamicId属性一致,如果绑定的值是null或者是undefined,那么这个属性会从渲染的元素上移除,下面是这个用法的简写语法,而且当要绑定的参数id和提供值的数据dynamicId名字一样的话,还可以更加简写

<div v-bind:id></div>
<div :id></div>

注意,这里需要有一个注意点,那就是当你给属性使用一些假值的时候,它是否还会渲染,请看下面一个例子

<body>
  <div id="app">

  </div>

  <!-- App组件 -->
  <template id="appTemplate">
    <div :id>这是div</div>
  </template>


  <script src="./node_modules/vue/dist/vue.global.prod.js"></script>
  <script>
    const App = {
      data() {
        return {
          id: 0
                }
      },
      template: "#appTemplate"
    };



    const app = Vue.createApp(App);


    app.mount("#app");
  </script>

// 上述渲染结果为 id: "0"

//如果id为false
//则渲染id: "false"

//如果id为""
//则渲染id

//如果为null, undefined
//那么就不会渲染这个属性了

可以看到除了null,undefined不会渲染外,其它假值都会作为这个标签属性id的值,vue中除此之外还提到了布尔型Attribute,它不会将你的数据映射到属性的值上,而是判断你的数据真假,如果为真,则会把这个属性渲染到元素上,如果判断为假,则不会渲染这个属性, 会有这种布尔型属性,主要是和html的一些布尔属性有关,disabled, hidden这种,当你把属性写在标签上的时候,不管你给什么值,都会执行这个属性应有的效果, vue在处理的时候当然不能直接把数据映射上去,判断数据的真假然后决定这个属性是否应该存在是比较好的处理了, 请看下面一个例子

<body>
  <div id="app">

  </div>

  <!-- App组件 -->
  <template id="appTemplate">
    <div :hidden="id">这是div</div>
  </template>


  <script src="./node_modules/vue/dist/vue.global.prod.js"></script>
  <script>
    const App = {
      data() {
        return {
          id: null
        }
      },
      template: "#appTemplate"
    };

    const app = Vue.createApp(App);
    app.mount("#app");
  </script>
</body>

//这里没有渲染hidden, 如果id为undefined也是一样

//如果id为 0
//则没有渲染 hidden


//如果id为""
//则渲染出了 hidden


//如果id为 false
//则没有渲染hidden


可以看到,对于布尔型Attribute,vue的判断也和正常的js不一致,vue没有把本该是一个假值的""空字符串也定义为空,但是对于其它常见的假值false, 0, null, undefined,就不会渲染这个属性,我们也可以批量的绑定多个值。

<body>
  <div id="app">

  </div>

  <!-- App组件 -->
  <template id="appTemplate">
    <div v-bind="objectAttr">这是div</div>
  </template>

  <script src="./node_modules/vue/dist/vue.global.prod.js"></script>
  <script>
    const App = {
      data() {
        return {
          objectAttr: {
            id: false,
            class: "wrapper"
          }
        }
      },
      template: "#appTemplate"
    };

    const app = Vue.createApp(App);
    app.mount("#app");
  </script>
</body>

//这里id: false也可以渲染出来,因为id不是一个布尔型属性

指令

指令是带有v-前缀的特殊attribute, 上述描述的v-bind也就是一个指令,可以说v-bind:id.prevent="idValue"中,v-bind用于声明要使用的指令,:id部分是指令的参数部分,而idValue是属性的值部分, 其中prevent是修饰符的部分,请看下面一个例子

<a href="https://bilibili.com" @click.prevent="">bilibili
  <span>a的子元素span</span>
</a>

动态参数

vue还提供了一种动态参数的用法, 它具体的用法是v-bind:[attributeName]='url', 这里的attributeName是一个组件的数据,属性名称由这个attributeName的值决定,如果这里的attributeName为href,则在模板上就会渲染属性href='....',,但是这种动态用法很容易踩坑,,请先查看下面一个例子,

<body>
  <div id="app"></div>

  <template id="appTemplate">
   <img :[attributeName]='url'>
  </template>

  <!-- 使用 Vue 3 CDN -->
  <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
  <script>
    const { createApp } = Vue;
    
    const App = {
      data() {
        return {
          attributeName: "src",
          url: "https://picsum.photos/200/300"  // 测试图片
        };
      },
      template: "#appTemplate"
    };

    createApp(App).mount("#app");
  </script>
</body>

并没有渲染出我们的图片!!!为什么??? 不妨让我看看dom的结构是什么样的

联想截图_20260211215405.png

可以看到,浏览器对属性进行了归一化处理,大写的字符被转化为小写的了,所以这里最好是把数据也写成小写形式,如下

 attributename: "src",

我们还可以使用动态绑定多个值规避这一点

   <img v-bind='{[attributeName]: url}'>

当然也可以使用计算属性计算出结果对象然后v-bind绑定上, 或者直接把DOM写在template字符串上, 当然在SFC上不会有这个问题,SFC没有把Vue模板给浏览器先解析一遍, 在vue文档中也描述了这个问题,当使用DOM内嵌模版时,我们需要避免在名称中使用大写字母,因为浏览器会强制将其转换为小写

动态参数在内嵌模板的dom中的问题

最后我们讨论来着vue文档的这么一句话,动态参数表达式因为某些字符串的缘故有一些语法限制,比如空格和引号,在HTML attribute名称中都是不合法

这是因为HTML属性名被规定必须是由一个或多个非空白的字符组成的,而且一定不能包含, 空格双引号, 单引号, =号 , <,>等特殊字符, 请看下面一个例子

<a :[foo + 1]="value"> ... </a>

我们看它在浏览器上是什么样子,

联想截图_20260211224948.png

一摸一样!,我们再看看这个标签元素的属性结构,

联想截图_20260211225020.png

原本我们想表达的一个完整意思 foo + 1被打断了!!! 为什么会这样呢,其实是浏览器宽松的解析导致的,它不会发生什么报错,它只会按HTML规则解析每一个HTML属性,所以这里浏览器看上去保留了上面写的完整字符串好像是真的作为属性了,但实际上它已经把属性按HTML分词规则拆成了多个属性,vue显然没办法解析这样逻辑被拆分出去的不完整属性, 所以让我们回到vue文档,比如空格和引号,这样的不合法结构无法被解析为正确的HTML属性,但是你可以写成foo+1,是可以被解析的,因为它没有空格和引号了,请看下面一个例子。

  <div id="app"></div>


  <!-- 使用 Vue 3 CDN -->
  <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
  <script>
    const { createApp } = Vue;
    
    const App = {
      data() {
        return {
          src: 1
        }
      },
        template: `<a href="https://bilibili.com" @click.prevent="">bilibili
      <span :[src+1]="1">a的子元素span</span>
    </a>`
      };

    createApp(App).mount("#app");
  </script>

这段代码结果中a标签渲染出的属性为2="1"。

也可以使用SFC和template规避这个问题,像上面说的,SFC和template都没有让浏览器预先处理标签,自然也就能处理一些浏览器没办法处理的。

显示移除绑定

在文档中还有这么一句话,动态参数中表达式的值应当是一个字符串,或者是null, 特殊值null意为显示移除该绑定,其它非字符串的值会触发警告

请看下面一段代码

  <div id="app"></div>


  <!-- 使用 Vue 3 CDN -->
  <script src="./node_modules/vue/dist/vue.global.prod.js"></script>
  <script>
    const { createApp } = Vue;
    
    const App = {
      data() {
        return {
          src: 1,
          clickon: 'click'
        }
      },
        template: `<a href="https://bilibili.com" @[clickon].prevent="">bilibili
      <span :[src+1]="1">a的子元素span</span>
    </a>`
      };

    const rootComponent = createApp(App).mount("#app");
  </script>

可以看到我们的a标签的事件监听器确实监听名为click的事件

联想截图_20260211232624.png

当我们在控制台设置rootComponent.clickon为null的时候后再次点击a标签,这时页面就发生了跳转,但其实设置成其它非click的字符串也是可以的,null在这里是给了一个在某某个状态下动态参数的值为null的时候,什么也不监听/什么属性也不添加的选项,要不然给一个字符串,肯定会添加上别的事件监听/属性

生成器下(生成器异步)

生成器下(生成器异步)

上一章讨论了生成器作为一个产生值的机制的特性,但生成器远远不止这些,更多的时候我们关注的是生成器在异步编程中的使用

生成器 + Promise

简略的描述,生成器异步就是我们在生成器中yield出一个 Promise,然后在Promise完成的时候重新执行生成器的后续代码

function foo(x, y) {
    return request(
    "http://some.url?x=1y=2")
}

function *main() {
    try {
        const text = yield foo(11, 31);
        console.log(text);
    } catch (err) {
        console.error( err );
    }
}
const it = main();

const p = it.next().value;

p.then(function (text) {
    it.next(text);
}, function (error) {
    it.throw(error);
});

上述代码是一个生成器+Promise的例子, 要想驱动器我们的main生成器,只需要在步骤后then继续执行即可,这段代码有不足之处,就是无法自动的帮助我们去实现Promise驱动生成器,可以看到上面我还是手动的写then回调函数去执行生成器, 我们需要不管内部有多少个异步步骤,都可以顺序的执行, 而且不需要我们有几个步骤就写几个next这么麻烦,我们完全可以把这些逻辑隐藏于某个工具函数之内,请看下面的例子

//第一个参数是一个生成器,后续的参数是传递给生成器的
//返回一个Promise
//当返回的Promise决议的时候,生成器也就执行完成了
function run(gen, ...args) {
    const it = gen.apply(this, args);
    
    
    return Promise.resolve()
      .then(function handleNext(value) {
        const ans = it.next(value);//执行
        
        return (function handleResult(ans) {
            //ans是执行结果
            if (ans.done) {
                return ans.value;//执行完毕
            }
            //没有执行完毕,我们需要继续异步下去
            return Promise.resolve(ans.value)
              .then(handleNext, function handleError(err) {
                return Promise.resolve(it.throw(err))
                  .then(handleResult);
            });           
        })(ans);
    })
}

下面的代码演示如何使用这个run函数,

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <img src="" alt="" data-action="show-dog1">
  <img src="" alt="" data-action="show-dog2">
  <img src="" alt="" data-action="show-dog3">
  <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>//引入axios
  <script src="./run.js"> //run.js内容就是上例中定义的函数
  </script>
  <script>

    function *main () {
      const {data: { message }} = yield axios.get("https://dog.ceo/api/breeds/image/random");//一个友爱的能获得狗狗图片链接的api网站, 可以访问其官网https://dog.ceo/dog-api/

      document.querySelector("[data-action='show-dog1']").src = message;

      const {data: { message: message2 }} = yield axios.get("https://dog.ceo/api/breeds/image/random");

      document.querySelector("[data-action='show-dog2']").src =message2;

      const {data: { message: message3 }} = yield axios.get("https://dog.ceo/api/breeds/image/random");

      document.querySelector("[data-action='show-dog3']").src =message3;
    }

    try {
      run(main)
       .then((ans) => {
        console.log("ans", ans); //这里接受生成器最后return的值,在该例中为undefined
       });
    } catch (err) {
      console.log(err);
    }

  </script>
</body>
</html>

run会运行你的生成器,直到结束,这样我们在生成器中就统一了异步和同步,所有的代码都可以以顺序的步骤执行,而我们不必在于是异步还是同步,完全可以避免写异步回调代码,

Async Await

在ES8中引入了async, await,这意味着我们也不需要使用写生成异步和run了,Async Await顺序的代码格式避免回调异步带来的回调地狱,回调信任问题一系列问题,如果按时间描述js异步的发展,大概就是从回调异步时代 到Promise ,然后生成器被大神发掘出来了,发现Promise + 生成器 有c#的async/await的效果,js官方觉得这是一个很好的用法,所以在es8中出了async/await, async/await就是Promise + 生成器的语法糖,可以认为promise + 生成器是其基石。下面再回到生成 + Promise的异步的解析

在生成器中并发执行Promise

在上面的写法中是没办法并发执行的, 想要实现并发

function *foo() {
    const p1 = request("https://some.url.1");
    const p2 = request("https://some.url.2");
    
    const r1 = yield p1;
    const r2 = yield p2;
    
    const r3 = yield request("https://some.url.3/?v=" + r1 + "," + r2);
    
    console.log(r3);   
}

run(foo);

这里实现并发的办法就是让异步请求先出发,等所有请求都执行后我们再yield Promise,也可以使用Promise.all实现并发, 下面覆写这个例子

function *foo() {
    const result = yield Promise.all(
    request("https://some.url.1"),
    request("https://some.url.2"));
    
    const r3 = yield request("https://some.url.3/?v=" + r1 + "," + r2);
    console.log(r3);
}

我们还可以把Promise.all封装在一个函数中,使得foo生成器的简洁性,这样从生成器的角度看并不需要关系底层的异步是怎么实现的,我们实现生成器 + Promise要尽量把异步逻辑封装在底层

生成器委托

怎么在一个生成器中调用另一个生成器,并且重要的是,对待调用的生成器内的异步代码就像直接写在生成器内部一样(也就是说也能顺序执行调用的生成器内部的代码不管同步异步)

function *foo() {
    const r2 = yield request("https://some.url.2");
    const r3 = yield request("htpps://some.url.3/?v=" + r2);
    
    return r3;
}

function *bar() {
    const r1 = yield request("http://some.url.1");
    
    //通过run 函数调用foo
    const r3 = yield run(foo);
    
    console.log(r3);
}

run(bars);

为什么可以这样,因为我们run函数是返回一个Promise的,就像上面说的,我们把Promise的细节封装了,通过run(foo)你只需要直到两件事,run(foo)返回一个Promise,既然是Promise,我们就yield就可以了, 同时run(foo)产生的Promise完成的时候就是foo完成的时候,决议值就是r3,如果对前面说的感到不理解,我再简单的补充一点,就是run本意就是返回一个Promise,,既然是Promise,我们当前可以像yield request那样yield它而不用管底层细节, es有一种称为生成器委托的语法 yield*,先看下面一个简单的用法介绍yield *,

function *foo () {
    console.log("*foo() starting");
    yield 3;
    yield 4;
    console.log("*foo() finished");
}

function *bar() {
    yield 1;
    yield 2;
    yield *foo();
    yield 5;
}


const it = bar();

it.next().value // 1
it.next().value //2
it.next().value 
// *foo() starting
// 3
it.next().value
//4
it.next().value
//*foo() finished
// 5

当我们消费完bar的前两个yield后,再next,这个时候,控制权转给了foo,这个时候控制的是foo而不是bar,这也就是为什么称之为委托,因为bar把自己的迭代控制委托给了foo,要是在控制foo的时候一直不next,bar也没办法进行了,当it迭代器控制消耗完了整个foo后,控制权就会自动转回bar,我们现在可以使用生成器委托覆写上述生成器委托下的第一个例子,在那个例子中使用了run(foo);我们现在可以让生成器更 `干净一点`

function *foo() {
    const r2 = request("https://some.url.2");
    const r3 = request("https://some.url.3?v=" + r2);
    
    return r3;
}


function *bar() {
    const r1 = request("https://some.url.1");
    
    const r3 = yield *foo();
    
    console.log(r3);
}

run(bar);

生成器委托其实就相当于函数调用,用来组织分散的代码,

消息委托

生成器委托的作用不只在于控制生成器,也可以用它实现双向消息传递工作,请看下面一个例子

function *foo() {
    console.log("inside *foo(): ", yield "B");
    
    console.log("inside *foo():", yield "C");
    
    return "D";
}

function *bar() {
    console.log("inside *bar():", yield "A");
    
    console.log("inside *bar(): ", yield *foo());
    
    console.log("inside *bar(): ", yield "E");
    
    return "F";
}

const it = bar();

console.log("outSide:", it.next().value);
//outside: "A"

console.log("outside", it.next(1).value);
//inside *bar:  1
//outside: B;

console.log("outside", it.next(2).value);
//inside *foo: 2
// outside: C


console.log("outside:", it.next(3).value);
//inside *foo 3
//inside *bar: D
//outside: "E"

console.log("outside:", it.next(4).value);
//inside *bar: 4
//outside: E

在这里我们就实现了和委托的生成器传递消息,外界传入2,3都传递到了foo中,foo yield的B,C页传递给了外界迭代器控制方(it),除此之外错误和异常也可以被双向传递,

function *foo() {
    try {
        yield "B";
    } catch (err) {
        console.log("error caught inside *foo():", err);
    }
    
    yield "C";
    
    throw "D";
}
functtion *baz() {
    throw "F";
}
function *bar() {
    yield "A";
    
    try {
        yield *foo();
    } catch (err) {
        console.log("error caugth inside *bar(): ", err);
    }
    
    yield "E";
    
    yield *baz();
    
    yield "G";
}


const it = bar();


console.log("outside:", it.next().value);
//outside:  A;

console.log("outside:", it.next(1).value);
//outside: B

console.log("outside:", it.throw(2).value);//外界向内抛入一个错误
//error caugth inside *foo () 2
//"outside: C"

console.loog("ouside:",it.next(3).value);
//error caugth insde *bar "D";
// ouside: E

try {
    console.log("outside", it.next(4).value);
} catch(err) {
    console.log("error cautgh outside:", err);
}
//error caugth ouside: F
//控制器结束

通过以上,我们可以总结出,当时有生成器委托的时候,和正常生成器其实没有什么区别对于外界的控制器(it)来说,它不在乎控制的是foo还是bar抑或是baz,它把这些看作是一个生成器,就像和一个生成器那样和其内部的各生成器进行双向的信息传递

生成器并发

在上面我们讨论过生成器并发Promise,在这里我们讨论并发生成器,

const res = [];  
function *reqData(url) {
    res.push(yield request(url));
}

 const it1 = reqData("https://some.url.1");
 const it2 = reqData("https://some.url.2");

const p1 = it1.next();
const p2 = it2.next();

p1.
  then(function (data) {
    it1.next(data);
    return p2;
}).then(function (data) {
    it2.next(data);
})

这里的生成器是并发的,并且通过then给这两个生成器安排好了结果位置,但是,这段代码手工程度很高,没办法让生成器自动的协调,

看下面一个例子



function runAll(...args) {
    const result = [];
    //同步并发执行Promise
    args = args.forEach(function (item) {
        item = item();
        item.next();
        return item;
    });
    
   
   function * fn() {
        args.forEach(function (item,idx) {
            let p = item.next();
          res[idx] = yield p; 
        });
    };
    
    run(fn);
    
    return result;
}
runAll(function *() {
    const p1 = request("....");
    
    yield;
    
    res.push(yield p1);
}, function *() {
    const p2 = request(".....");
    
    yield;
    
    res.push(yield p2);
});

这个例子避免了手动的去书写Promise的then链,但是这样的写法也不算是真正实现生成器并发,真正的runAll很复杂,所以没有提出

总结

生成器异步就是生成器加Promise,要求yield出一个Promise,由外部控制,但在现在完全可以使用async/await

❌