万字长文,带你深入浅出ES6之Generator篇

前言

Generator,生成器函数,ES6中最难理解的语法,没有之一。从我的亲身经历来说,至少80%的前端不知道Generator的语法特征(小公司这个比例可能会更高),一方面是因为Generator这个语法在绝大部分的场景下,可以用别的语法平替(比如Async函数),导致很多人觉得理解它的代价与收获不成正比,所以还不如不学。另一方,前端使用状态模式的频率不高,对于迭代器模式来说的话,完全可以有更好的平替,比如数组或者对象。再者,因为Generator的语法牵涉到的知识点太多,容易让人抓不着重点,就好比一坨线团,没有理清它,强行去理的话,只会越理越乱。

我是通过学习阮一峰老师的《ECMAScript 6 入门》这本网络书籍,加上自己的一些编程和融会贯通理解到的,我个人的观点是阮一峰老师的书籍有些措辞有些过于专业了,导致我们以为JS引擎可能存在什么黑魔法,而越理越不清,所以本文旨在以接地气的方式向大家阐述Generator的一些技术特点。

但是,ES6推出这个东西肯定是有它的道理的,所谓存在即合理,为了提升我们的编程能力,攻坚克难,一起来搞懂Generator

单线程的JS

为什么要先聊这个知识点呢,我旨在向大家说明,其实Generator并没有逃出我们一直接受的单线程理论。

众所周知,假设我们在浏览器执行一段死循环代码,那浏览器当前的页面肯定卡死的。

比如下面的这段代码:

js 复制代码
function fn() {
    while(true) {
        // 大家不要执行这个代码,仅仅是个演示。
    }
}
fn();

因为你每时每刻都在让浏览器的渲染进程处理这个任务,渲染进程根本没有办法去响应你的其它操作,所以表现出来页面就是卡死了。

因为代码执行速度非常快,只要你不是死循环,在很短的时间内,浏览器就把你交给它的任务干了,所以它就有时间空出来干别的了。

所以,即便是单线程,但是可以让代码运行起来看起来像是多线程一样,我们拿下面这个图举例:

在上面的这个图中,每截红线表示浏览器那个时刻做的任务,每一截红线如果加起来的话,刚好等于时间轴的总长度,大家可以看出来,每个时刻,总是只能有一个任务在干活儿。

所以,正是因为浏览器的任务调度,让我们的代码看起来像是多线程运行一样。

迭代器与for-of循环

迭代器接口(Iterable)、指针对象(Iterator)和next方法返回值的规格可以描述如下。

ts 复制代码
// 迭代器接口,任何实现了这个接口的对象(即有属性[Symbol.iterator],返回值是一个Iterator规格的对象),都可以被迭代(即被for-of消费)的能力
interface Iterable {
  [Symbol.iterator](): Iterator;
}

interface Iterator<T, TReturn = any, TNext = undefined> {
    // 传入给下一次迭代的初始值
    next(...args: [] | [TNext]): IteratorResult<T, TReturn>;
    // for-of循环,中断的时候,可以执行的清理操作
    return?(value?: TReturn): IteratorResult<T, TReturn>;
    // 跟Generator相关
    throw?(e?: any): IteratorResult<T, TReturn>;
}

interface IterationResult {
  // 每一步迭代的值
  value: any;
  // 表示迭代是否完成
  done: boolean;
}

如果不考虑是ES6环境,也不考虑babel转义,上面的结构仍然是可以被遍历的,只不过这个遍历写起来会比较繁琐。

以下是我们直接操作迭代器进行遍历的代码:

ts 复制代码
function visitIterable<T>(it: Iterator<T>, fn: (val: T) => void) {
  let result = it.next();
  while (!result.done) {
    fn(result.value);
    result = it.next();
  }
}

使用这个迭代辅助函数:

ts 复制代码
const it = [1,2,3,4,5][Symbol.iterator]()
visitIterable(it, (val) => {
    // 输出值,1,2,3,4,5
    console.log(val);
})

(先给大家解释一下这段代码const it = [1,2,3,4,5][Symbol.iterator](),首先创建一个数组,因为数组是实现了迭代器接口的,所以,通过这段代码,我们就创建了一个可迭代的对象,这个可迭代对象迭代的是刚才创建数组的元素。)

如果此刻,我们加需求了。假设,某个值能等于3,需要干点儿别的事儿,那么我的这个visitIterable函数就要加参数了。

我们也来实现一遍:

ts 复制代码
function visitIterable<T>(
  it: Iterator<T>,
  fn: (val: T) => void,
  // 增加一个参数,可以让迭代在某个条件下中断
  breakPredicate: (val: T) => boolean,
) {
  let result = it.next();
  while (!result.done) {
    fn(result.value);
    const flag = breakPredicate(result.value);
    if (flag) {
      break;
    }
    result = it.next();
  }
}

使用修改过的迭代辅助函数迭代:

ts 复制代码
const it = [1,2,3,4,5][Symbol.iterator]()
visitIterable(it, (val) => {
    // 输出值,1,2,3,4,5
    console.log(val);
}, v => v === 3)

上述代码还没有考虑到迭代过程中因为异常导致遍历提前终止的场景,啊,如果再接着考虑更多的场景,麻了麻了。

因此,ES6引入for-of循环就是为了解决我们实际开发中遍历迭代器繁琐的问题。看到这儿,很多同学就明白了了for-infor-of的区别了吧。

这也是为什么for-of循环的每项就是目标对象的其中一个值的原因,因为它的本质就是一个遍历语法糖。

对于上述代码,假设用for-of写的话,那就太简单了。

ts 复制代码
const it = [1,2,3,4,5][Symbol.iterator]()
for(const o of it) {
   if(o === 3) {
      break;
   }
   console.log(o);
}

需要注意一点的是,for-of循环只会遍历迭代器done的值为false的结果

对于迭代器的另外两个可选方法,一个是return,一个是throw

throw方法迭代器从本身的使用来说的话,暂时还用不到,它需要和Generator结合使用。

至于return方法,就是在for-of中断的时候执行的回调函数,阮一峰老师的博客里面给了另外一个例子,抛出异常打断for-of循环,会先执行return函数,再抛出异常。还有就是我们使用break语句打断。

js 复制代码
for (let line of readLinesSync(fileName)) {
  console.log(line);
  throw new Error();
}

在实际的使用过程中,我们几乎不会自己去处理迭代器,所以对于这两个方法,了解即可,遇到实际的场景再结合Chatgpt和搜索引擎便可以解决。

Generator函数的语法

在明白迭代器和for-of循环的基本知识点以后,我们便可以开始聊Generator的语法了。

比如我们要定义一个Generator函数,以下写法都是合法的,并且展示了绝大部分可能出现的场景:

ts 复制代码
// * 挨着谁没有关系
function* gen1() {}

function *gen2() {}

const gen3 = function *() {}

class A {
    * demo() {}
}

const obj = {
    *demo() {}
}

const obj2 = {
    *["Hello"]() {}
}

const obj3 = {
    test: function *() {}
}

如果我们要判断一个对象是否是Generator函数,可以采取以下方式:

js 复制代码
const isGenerator = (func) => {
  return func && func[Symbol.toStringTag] === "GeneratorFunction";
};

yield表达式

各位读者准备好了,要开始上难度了。

我个人的理解,我们应该把Generator和迭代器既要分开看,又要结合在一起看。

分开看,就是我们在编写代码的时候,只需要关注我们要做的事儿,Generator函数,就好像是一本日历,每个yield表达式,就定义的是一个阶段(return也可以把它看做是一个阶段,但是return这个阶段不一定存在)。

比如我们用下面的Generator函数来描述一年四季(4个阶段)的进程:

js 复制代码
function* atAllSeasons() {
    console.log('spring');
    yield;
    console.log('summer');
    yield;
    console.log('autumn');
    yield;
    console.log('winter');
    return;
}

好了,我们现在要忘掉这个东西是个函数,我们需要把它暂时把它想象成链表

然后,现在,我告诉你,这个东西其实不是链表,它是一个工厂函数,这个函数每次执行完成,都返回上面这样的一个链表。

接着,我又要告诉你,这个链表 是一个可迭代对象,它可以被for-of遍历。

于是,我们根据已得到的知识点,遍历一下这个链表

js 复制代码
const linkedList = atAllSeasons();
for(const a of linkedList) {
    console.log(a);
}

然后,你可能对控制台的输出比较好奇,为什么a全部都是undefined呢?

好了,我们接下来要给大家传递第一个知识点了,yield表达式后面的内容,可以作为迭代器迭代过程中每次都返回值,即IterationResult接口中的value字段。

我们把之前写的四季的Generator函数改写一下,可以使得在for-of循环的时候,输出'春','夏','秋','冬'的效果。

js 复制代码
function* atAllSeasons() {
    yield '春';
    yield '夏';
    yield '秋';
    yield '冬';
}

为什么我们不用return改用4个yield了呢,因为是这样的,对于Generator得到的迭代器,return语句算作的效果是IterationResulttrue的那一次迭代,for-of不消费那次结果,所以我们就用4个yield表达式。

所以,现在大家应该知道了吧,yield的效果有点儿像普通函数的return语句,它的效果就是让Generator得到的迭代器某个节点的值是它返回的值。

说到这儿,这就是为什么我说在理解Generator的时候不要把它和迭代器混在一起看原因,就像一团线团,我们只要把它理顺了之后,这个问题就变得简单了。

事情到这儿,还没有完,最开始我们给出迭代器的TS类型定义的时候,可以看到next方法是有参数的。现在,我要告诉大家,next函数在执行的时候,我们传递的参数,可以作为yield表达式的返回值。

大家有点儿懵,可以接着看下图:

然后,我们再来改写一下四季的函数:

js 复制代码
function* atAllSeasons() {
    const springMsg = yield '春';
    console.log('进入到了夏天我接受到了来自春天的消息',springMsg);
    const summerMsg = yield '夏';
    console.log('进入到了秋天我接受到了来自夏天的消息',summerMsg);
    const autumnMsg = yield '秋';
    console.log('进入到了冬天我接受到了来自秋天的消息',autumnMsg);
    yield '冬';
}

const it = atAllSeasons();
for(const seaon of it) {
    console.log(season)
}

这段代码执行起来,结果是这样的: 全打印的是undefined,为什么呢?因为for-of在执行代码的时候,它不知道需要向后面传递信息,所以打印的值就都是undefined,因此,我们想要实现向后面的迭代节点传递信息的效果,就只能回到最开始我们提到的用手动遍历的方式。

js 复制代码
function visitIterable(it) {
  let result = it.next();
  while (!result.done) {
    // 假设我们就把本次迭代节点的值传递给下一个节点
    const val = result.value;
    result = it.next(val);
  }
}

const it = atAllSeasons();
visitIterable(it);

这段代码的执行效果如下:

这儿有个关键的知识点:

由于next方法的参数表示上一个yield表达式的返回值,所以在第一次使用next方法时,传递参数是无效的。V8 引擎直接忽略第一次使用next方法时的参数,只有从第二次使用next方法开始,参数才是有效的。从语义上讲,第一个next方法用来启动遍历器对象,所以不用带有参数。

其实我们稍加思索也能明白这个问题,第一个yield表达式之前肯定不会有yield表达式的 ,那第一个next方法调用时传递的参数给谁用呢,既然没有人需要它,那还传它干嘛。

yeild* 表达式

回到刚才我们聊到的四季的那个Generator函数。根据上文的阐述,我让大家将其理解为一个返回链表的工厂函数,既然是链表,我们现在想在这个链表中插入一串节点。这种场景下,使用yield*,就可以让引擎帮我们遍历内部的迭代器。

还是老规矩,先来个图: 得到一个新的迭代器:

js 复制代码
const arr = [1,2,3,4,5];

function* atAllSeasons() {
    const springMsg = yield '春';
    yield* arr[Symbol.iterator]();
    console.log('进入到了夏天我接受到了来自春天的消息',springMsg);
    const summerMsg = yield '夏';
    console.log('进入到了秋天我接受到了来自夏天的消息',summerMsg);
    const autumnMsg = yield '秋';
    console.log('进入到了冬天我接受到了来自秋天的消息',autumnMsg);
    yield '冬';
}

const it = atAllSeasons();
for(const val of it) {
    console.log(val)
}

看看代码的执行效果,是不是真的就像我们描述的那样,哈哈哈:

再看看,yield*表达式在处理其它Generatorreturn表达式会不会有什么坑点之类的。

javascript 复制代码
function* demo() {
    yield 1;
    yield 2;
    yield 3;
    yield 4;
    yield 5;
    return 6;
}

function* atAllSeasons() {
    const springMsg = yield '春';
    yield* demo();
    console.log('进入到了夏天我接受到了来自春天的消息',springMsg);
    const summerMsg = yield '夏';
    console.log('进入到了秋天我接受到了来自夏天的消息',summerMsg);
    const autumnMsg = yield '秋';
    console.log('进入到了冬天我接受到了来自秋天的消息',autumnMsg);
    yield '冬';
}

const it = atAllSeasons();
for(const val of it) {
    console.log(val)
}

执行结果如下:

可见,yield*并不会将内部Generator函数的IterationResultdonetrue的那个节点加入到最终的迭代器的迭代节点中

最后,再看看,yield*是不是跟yield性质一样,next函数的入参能够作为它的返回值。

js 复制代码
function* demo() {
    yield 1;
    yield 2;
    yield 3;
    yield 4;
    yield 5;
    return 6;
}

function* atAllSeasons() {
    const springMsg = yield '春';
    const inputParams = yield* demo();
    console.log('上一个迭代器测试',inputParams);
    console.log('进入到了夏天我接受到了来自春天的消息',springMsg);
    const summerMsg = yield '夏';
    console.log('进入到了秋天我接受到了来自夏天的消息',summerMsg);
    const autumnMsg = yield '秋';
    console.log('进入到了冬天我接受到了来自秋天的消息',autumnMsg);
    yield '冬';
}

function visitIterable(it) {
  let result = it.next();
  while (!result.done) {
    // 假设我们就把本次迭代节点的值传递给下一个节点
    const val = result.value;
    console.log(val);
    result = it.next(val);
  }
}

const it = atAllSeasons();
visitIterable(it);

竟然得到的是donetrue的那个节点的值。 改写一下demo生成器函数,再看看是否输出的是是donetrue的那个节点的值。

js 复制代码
function* demo() {
    yield 1;
    yield 2;
    yield 3;
    yield 4;
    yield 5;
}

确实是! 所以到此,我们又可以得出一个结论,yield*的返回值是第一次IterationResultdonetrue的节点值

Generator.prototype.return

这个方法是一个我们手动终结的方法,就比如有这样的场景,我因为某些上下文的关系,我想直接终止迭代器的遍历。

在这种场景下,我们就可以直接调用Generatorreturn方法,返回值是donetrue的节点并且value就是return方法的入参。一般情况下,当我们再调用Generator函数执行得到的那个迭代器对象,遍历就已经完成了,得到的值就是{ value: undefined, done: true }

比如:

js 复制代码
function* gen() {
  yield 1;
  yield 2;
  yield 3;
}

var g = gen();

g.next()        // { value: 1, done: false }
g.return('foo') // { value: "foo", done: true }
g.next()        // { value: undefined, done: true }

不过,有一个场景例外,比如,我们在Generator内部署了try-cacth-finally代码,当我们再调用Generator函数执行得到的那个迭代器对象时,仍然可以继续向后面迭代。

比如:

js 复制代码
function* numbers () {
  yield 1;
  try {
    yield 2;
    yield 3;
  } finally {
    yield 4;
    yield 5;
  }
  yield 6;
}
var g = numbers();
g.next() // { value: 1, done: false }
g.next() // { value: 2, done: false }
g.return(7) // { value: 4, done: false }
g.next() // { value: 5, done: false }
g.next() // { value: 7, done: true }

理解起来,就是finally强制改变了Generator内部的状态流程。

Generator.prototype.throw

之前聊迭代器的时候就聊到了它,这个就只会在用于控制Generator函数内部的状态的时候有用。

它的用途也是强制中断迭代器对象,它的入参是一个错误信息,最好是Error对象的实例。

js 复制代码
function *gen() {
    yield 1;
    yield 2;
    yield 3;
    yield 4;
    yield 5;
}

const it = gen();
const res = it.next();
const err = it.throw(new Error('Bad Request'))
console.log(res, err)

当调用了throw方法之后,当我们再调用Generator函数执行得到的那个迭代器对象,遍历就已经完成了,得到的值就是{ value: undefined, done: true }

跟之前的Genarator.prototype.return方法有些类似,如果我们在调用throw方法的过程中,当时这个遍历器对象遍历的状态还处在内部包try-catch包裹的状态时,可以继续向后面迭代。

比如:

js 复制代码
function *gen() {
    try {
        yield 1;
        yield 2;
    } catch (exp){ 
       console.log(exp);
       yield 3;
       yield 4;
    } 
}

上面的代码在不同的时机抛出错误,效果完全不一样。

js 复制代码
const it = gen();
it.next();
it.throw(new Error('test error'));
it.next();
it.next();
it.next();

如果要真的探究上面的这段代码的迭代流程是怎么流转的,得看一下这个代码经过babel编译之后的代码是什么样的:

js 复制代码
var _marked = /*#__PURE__*/_regeneratorRuntime().mark(gen);
function gen() {
  return _regeneratorRuntime().wrap(function gen$(_context) {
    while (1) switch (_context.prev = _context.next) {
      case 0:
        _context.prev = 0;
        _context.next = 3;
        return 1;
      case 3:
        _context.next = 5;
        return 2;
      case 5:
        _context.next = 14;
        break;
      case 7:
        _context.prev = 7;
        _context.t0 = _context["catch"](0);
        console.log(_context.t0);
        _context.next = 12;
        return 3;
      case 12:
        _context.next = 14;
        return 4;
      case 14:
        _context.next = 16;
        return 5;
      case 16:
      case "end":
        return _context.stop();
    }
  }, _marked, null, [[0, 7]]);
}

大家可以直接打开babel的在线编译地址即可:Babel · The compiler for next generation JavaScript (babeljs.cn) 像这种调用,就不太符合我们之前所描述的: 这个具体就只能看regenerator这个库内部的实现了,本文就不展开讲了,有兴趣的同学可以自己debug一下就明白里面的流程控制了。

到这个位置,我们其实就已经把Generator的语法特点弄清楚的大差不差了。

最后,再给大家留一个思考题:

js 复制代码
function *gen() {
    let count = 1;
    while(true) {
        yield ++count;
    }
}

上述代码会造成浏览器的死循环吗?请说明原因。

在上文中,我们并没有用一些比较晦涩难懂的词汇来描述Generator的内部流程,而是采用接地气的方式阐述了Generator的执行流程。

Generator的应用

相信大家在实际开发中应该都没有使用过Generator吧,目前根据我的技术眼界,我所知就redux-saga使用了Generator,就是因为这个语法让很多同学都难受。

但是,你学习到此明白了Generator的用法之后,假如再去看saga肯定会有新的理解了。

作为迭代器的语法糖

相信大家一定都看到过这个代码吧。

请添加某些代码使得以下代码正常运行:

js 复制代码
const obj = { a: 1, b: 2 } 
// 在此处添加你的代码 
const [a, b] = obj;

如果Get不到这题的考点的同学那就没办法了,你还需要继续修炼,哈哈哈。如果知道可以给obj对象补迭代器的同学很容易就写出以下的代码:

js 复制代码
obj[Symbol.iterator] = function () {
    const keys = Object.keys(this);
    let idx = 0;
    const _this = this;
    return {
      next() {
        const i = idx++;
        return {
          value: _this[keys[i]],
          done: i >= keys.length,
        };
      },
    };
  }

但是,我要是说,这个代码可以写的更简洁,你一定要先"哇"为敬,哈哈哈。

如果使用Generator实现:

js 复制代码
// 定义一个Generator,读取目标对象的
function *objIterator(obj) {
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      yield obj[key];
    }
  }
}

const obj = { a: 1, b: 2 };

obj[Symbol.iterator] = function() {
  return objIterator(this)
}

// 在此处添加你的代码 
const [a, b] = obj;

照顾到第一次学Generator的同学,我objIterator这个函数所完成的功能阐述一下,因为Generator函数返回的是一个 返回内容是类似链表的工厂函数,这个函数的操作就是每次迭代都取一下目标对象的一个key,然后作为迭代节点的value返回,当解构赋值遍历迭代器的时候,就好比把这个对象上的每个属性值都消费了一下,从而完成了赋值。

状态模式的语法糖

在我之前的设计模式的章节中,就已经给大家聊过了设计模式在前端开发中的实际应用(一)------状态模式

如果我们要自己去写状态模式的代码的话,会显得比较繁琐,相当于要自己去切换上下文,而有了Generator我们就可以直接利用Generator的能力为我们进行状态切换了。

以下就是一个红绿灯的例子:

js 复制代码
function* func() {
  while (true) {
    yield console.log("红灯亮起");
    yield console.log("绿灯亮起");
    yield console.log("黄灯闪烁,红灯即将亮起");
  }
}

const light = func();

function start(immediate) {
  setTimeout(() => {
    light.next();
    start();
  }, 1000);
  immediate && light.next();
}

具体业务下使用状态模式肯定不会有这么简单,大家可以结合Generator.prototype.throw方法来进行状态控制就可以满足相应的业务需求。

异步操作同步表达------作为async函数的polyfill

到这个位置,就已经可以聊到async函数的polyfill实现了。

首先要说明一点,很多同学有一个错误的认知,就是觉得async函数底层就是babel编译的结果那样的实现,这种认知我觉得是站不住脚的,因为V8如果原生支持的话,有些操作比如可以直接用C++直接操作底层那效率肯定是要比regenenrator库的实现要高的,但是为了老的浏览器能够用上这种能力,于是regenerator提供了一个状态管理的能力,能够模拟出async函数的那种效果,所以不要把两者混为一谈。

我们先理一下async函数的特点。

async函数内部可以有多个await,每次遇到await表达式,先执行后面的逻辑,到这个位置,还是在同步任务上执行代码。此刻JS就继续去执行别的同步代码了。过了一会儿,await右边的Promise的状态变成了fulfilled之后,就可以把Promise的返回值交给等号左边的变量了。如果后续再有await表达式,重复刚才说的步骤。假设这个过程中,都没有异常发生,函数正常结束,就把最后return的值作为最终这个Promise的值返回;假设在其中任何一个过程发生未捕获的错误,那么就直接把错误的原因作为Promiserejected的reason。

还有一个问题,就是上述的这个过程中,我假设了一个条件:每次迭代对象的value值都是一个Promise

理清楚这个设计思路之后,剩下的编码就比较简单了。

这就是阮一峰老师给出的spawn函数的实现,我改了一下变量名,可以帮助我们更好的理解:

js 复制代码
// 外界传入一个Generator,这个Generator的迭代对象的每个节点的value都是一个Promise
function spawn(genF) {
  // 对外返回一个Promise,Generator迭代过程中出错则返回错误的reason,否则一直向后迭代,
  // 并且把Generator函数执行得到的迭代器对象最后的Promise的结果作为这个Promise的结果
  return new Promise(function (resolve, reject) {
    // 获取到迭代器对象
    const iterator = genF();
    
    function step(nextF) {
      let itNode;
      try {
        itNode = nextF();
      } catch (e) {
        // 执行过程中有任何错误,直接结束,把错误信息作为Promise的错误reason
        return reject(e);
      }
      // 迭代完成了,把最后一个值作为最终Promise的返回值
      if (itNode.done) {
        return resolve(itNode.value);
      }
      
      // 递归调用,实现遍历迭代器的效果
      Promise.resolve(itNode.value).then(
        (v) => {
          step(function () {
            // 把上一个迭代节点的返回值,即yield的返回值作为next函数的入参。
            return iterator.next(v);
          });
        },
        (e) => {
          step(function () {
            // 出错,直接调用Generator的throw方法终止迭代。
            return iterator.throw(e);
          });
        }
      );
    }
    
    // 开始迭代
    step(() => {
      // 因为开始迭代的过程中,第一个yield表达式不依赖next方法的入参,所以传一个undefined就好。
      return iterator.next(undefined);
    });
    
  });
}

这儿我们可以这样来理解,快过年了,我们要放鞭炮,可是运气不好,去一个无良商家那儿买到了一串鞭炮,这串鞭炮的质量不好,每次都必须等上面一个鞭炮炸了以后,下面一个鞭炮才能炸。

以上就是async函数的polyfill实现了。

总结

Generator作为ES6中最难懂的语法,一旦完全掌握了,对我们编程能力有立竿见影的提升。不要把它想的很神秘,它一直都没有逃离我们学习的基础知识点。

Generator的语法中不难看出,用一些数据结构和算法的思维来理解迭代器可以起到相当有效的作用。并且,在这个过程中,我们也明确的看到了很多知识点不是单独存在的,他们是相辅相成的,只有掌握好了每个基础的知识点,才能更好的理解其它的知识点,当我们把所有的知识点都掌握之后,知识形成一个体系,看什么东西都觉得很快了。

有些同学一直在抱怨前端的知识更新迭代快,我个人觉得还好,怎么说呢,如果说我们要去记忆框架的API的话,那确实忙不过来,既然我们没办法让别人不造轮子,那我们就直接抓根本,从源头上拿捏,学好JS和计算机底层的一些知识点,面对新的东西可能真的就是熟悉一下API就可以信手拈来了,哈哈哈。

如果大家喜欢我的文章,可以多多点赞收藏加关注,你们的认可是我最好的更新动力,😁。

相关推荐
jessezappy7 分钟前
jQuery-Word-Export 使用记录及完整修正文件下载 jquery.wordexport.js
前端·word·jquery·filesaver·word-export
旧林84334 分钟前
第八章 利用CSS制作导航菜单
前端·css
yngsqq1 小时前
c#使用高版本8.0步骤
java·前端·c#
Myli_ing1 小时前
考研倒计时-配色+1
前端·javascript·考研
余道各努力,千里自同风1 小时前
前端 vue 如何区分开发环境
前端·javascript·vue.js
PandaCave1 小时前
vue工程运行、构建、引用环境参数学习记录
javascript·vue.js·学习
软件小伟2 小时前
Vue3+element-plus 实现中英文切换(Vue-i18n组件的使用)
前端·javascript·vue.js
醉の虾2 小时前
Vue3 使用v-for 渲染列表数据后更新
前端·javascript·vue.js
张小小大智慧2 小时前
TypeScript 的发展与基本语法
前端·javascript·typescript
hummhumm2 小时前
第 22 章 - Go语言 测试与基准测试
java·大数据·开发语言·前端·python·golang·log4j