Git Product home page Git Product logo

blog's People

Contributors

easytuan avatar

Watchers

 avatar  avatar

blog's Issues

浅谈JavaScript的事件循环机制(Event Loop)

前言

我们先来看个实际中的场景:

在浏览器上打印出1~100万。

对于这个需求,我们第一个反应的代码是这样的:

  for (let i = 0; i < 100 * 10000; i++) {
    const div = document.createElement("div");
    div.innerText = i;
    document.body.append(div);
  }

看着好像没问题,当然这里有的同学会说,你这里操作了100万次dom,性能损耗很严重。这是一个问题,但是这个不是本文讨论的重点。

我们实际运行一下这段代码,发现一开始的时间(很短)会无法正常做交互,比如鼠标悬浮,选中文字等操作。那么为什么会造成这个现象呢?不急,我们一步步分析。

浏览器的渲染机制

以Chrome为例,一个标签页独占一个渲染进程,而JavaScript解释线程是属于这个渲染进程内的。

我们先聊下前置知识,JavaScript执行和屏幕渲染是互斥的,原因是屏幕渲染需要根据DOM结构,而JavaScript是有能力修改DOM的,所以会在JavaScript执行完毕后,执行渲染动作。一般来说,屏幕的渲染是60HZ,差不多16ms需要切换一张图片,人眼才不会意识到卡顿。

那么我们再回来看刚刚的代码

  for (let i = 0; i < 100 * 10000; i++) {
  }

结论很明显了,这句代码的执行明显超过了16ms!!!

事件循环机制

事件循环机制(Event Loop)就是为了解决这个问题而提出的。

简单来说,我们把大任务切成了多个小任务,让这些任务的执行和渲染流程交错进行。

计算100万个(1s) => 屏幕渲染

计算10万个(0.1s) => 屏幕渲染 => 计算10万个(0.1s) => 屏幕渲染 => 计算10万个(0.1s) => ...

这里就不贴出具体的代码实现了,感兴趣的同学可以自己动手写一下。

再探事件循环机制

事件循环机制(Event Loop)出现后,JavaScript的执行任务分为了两类:同步任务和异步任务。而异步任务,又分成了两类:

  • 宏任务(Macrotask)整体代码定时器I/O
  • 微任务(Microtask)PromiseMutationObserverobserver

这里我们需要注意一点,Promise本身是同步代码,但是它的回调then catch是异步

  new Promise((res, rej) => {
    res('ok') // 同步任务
  }).then((result) => {
    console.log(result) // 异步任务中的微任务
  })

其实我们会好奇,为什么是分成这两类?为什么不是三类或者一类?其实这是一种权衡的策略。宏任务的定义为耗时长的任务,微任务为耗时短的任务。在实际执行中,需要执行的任务分成多,所以就会有优先级的问题,其实宏任务和微任务的制定就是一种折中,为了权衡执行时间和运行效率。

优先级问题

我们刚刚提到了优先级的问题,那我们就来展开聊聊。一般情况下,微任务的执行优于宏任务。为什么是一般情况下呢?我们来看下这段代码:

  for(let i = 0; i < 10; i++) {
    setTimeout(() => {
      console.log('宏任务开始')
      for (let j = 0; j < 10; j++) {
        microtask();
      }
    })
  }

  function microtask() {
    return new Promise(async (res) => {
      console.log('微任务开始')
      await microtask();
      res();
    })
  }

注册10个宏任务 => 运行第1个宏任务 => 注册10个微任务 => 微任务执行过程中又注册了微任务 => 微任务执行过程中又注册了微任务 => ...

这是一个微任务的死循环,如果按微任务的执行优于宏任务的论据,宏任务是不可能触发第二个的,因为微任务队列一直在被push。

但是实际情况下,我们是可以看到第二个宏任务开始的。这是为什么呢?其实是Chrome V8做的一个小策略,在微任务过多的时候,会执行下一个宏任务。

初探Node.js之Event Loop

我们先以宏观视角看下在Node.js中Event Loop长什么样,话不多说,先上图:

   ┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘

关于这 6 个阶段,官网描述为:

  • 定时器(timers):本阶段执行已经被 setTimeout()setInterval() 的调度回调函数。
  • 待定回调(pending callbacks):执行延迟到下一个循环迭代的 I/O 回调。
  • idle, prepare:仅系统内部使用。
  • 轮询(poll):检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,那些由计时器和 setImmediate() 调度的之外),其余情况 Node 将在适当的时候在此阻塞。
  • 检测(check)setImmediate() 回调函数在这里执行。
  • 关闭的回调函数(close callbacks):一些关闭的回调函数,如:socket.on('close', ...)

setImmediate

这是一个在Node.js中实现但没有在浏览器中实现的API,我们先来看下它的定义:

该方法用来把一些需要长时间运行的操作放在一个回调函数里,在浏览器完成后面的其他语句后,就立刻执行这个回调函数。

该特性是非标准的,请尽量不要在生产环境中使用它!

看起来和setTimeout没什么区别,不过他们都运行在不同的阶段,setImmediate会在一旦在当前轮询阶段完成时,执行回调用。

我们来看一段比较坑爹的代码:

  setTimeout(() => {
    console.log('setTimeout');
  }, 0);

  setImmediate(() => {
    console.log('setImmediate');
  });

那你觉得是先输出setTimeout还是setImmediate呢?

其实我们在实现运行后发现,都有可能!!!

官方是这么说的:

  • 执行计时器的顺序将根据调用它们的上下文而异。
  • 如果两则都从主模块内调用,则计时器将受到进程性能的约束(这可能会受到计算机上其他正在运行应用程序的影响)。
  • 如果你将这两个函数放入一个 I/O 循环内调用,setImmediate 总是被有限调用。

精简一下,大致意思就是说:它们的执行任务优先级不一样。

process.nextTick()

process.nextTick() 是一个异步任务,它属于微任务。

process.nextTick() 将 callback 添加到"next tick queue"。 在 JavaScript 堆栈上的当前操作运行完成之后,且在允许事件循环继续之前,此队列将被完全排空。 如果递归地调用 process.nextTick(),则可能会创建无限的循环。

小结

我们通过了开头的一个小例子,引入了事件循环机制(Event Loop),而执行任务分为了两类:同步任务和异步任务。而异步任务,又分成了两类:宏任务和微任务。一般情况下,微任务的执行优于宏任务。当在极端场景下,比如微任务实在是太多了,Chrome V8会先执行宏任务。

而在Node.js中,任务的调度分为了六个阶段,这点和浏览器略有不同。因为所处的阶段不同setTimeout和setImmediate的执行顺序也会因当前上下午环境而改变。

漫谈RxJS之基础原理篇

我们要谈及RxJS,不得不先说下Reactive Extensions(Rx),这是一种编程模式,他的目标是对异步的集合进行操作(ps: 这种编辑模式有利有弊,当然这不是本文的重点,这里不再赘述,有兴趣的同学可以自行查阅相关资料。)。Rx家族成员很多,如RxCpp、RxJava、Rx.NET、Rx.rb、RxPy等,我们从命名上不难看出,后缀就是语言的名称,那么就不难理解RxJS是Javascript语言对Reactive Extensions(Rx)的实现。

RxJS是什么?

引自官方文档的概念:

RxJS 是一个库,它通过使用 observable 序列来编写异步和基于事件的程序。它提供了一个核心类型 Observable,附属类型 (Observer、 Schedulers、 Subjects) 和受 [Array#extras] 启发的操作符 (map、filter、reduce、every, 等等),这些数组操作符可以把异步事件作为集合来处理。

可以把 RxJS 当做是用来处理事件的 Lodash

官方文档引入了很多概念,如Observer、 Schedulers、 Subjects,这些对初学者并不友好,因为这些都是RxJS的概念。RxJS的学习路线对于大多数人来说都太陡峭了,很容易从入门到放弃,甚至还没入门就放弃了。

我们把概念做个精简,RxJS其实就是发布者和订阅者之间玩的游戏

在设计模式中,有一个模式就是发布订阅模式,这个模式中,

  • 发布者只负责事件的通知
  • 订阅者只负责接收到通知后完成自身的业务逻辑,而并不关心消息的来源

大家都责任都很明确,但是必然有一些负责的情况,比如广播是私有的还是公开的,如果订阅者迟到了,那么前面的消息是否需要告知订阅者,对着这些情况的处理,RxJS定义了一套完整的操作符,注意是完整的,这个世界上所有的情况都被涵盖在操作符中了,你不必再造轮子了。

解决了什么问题?

上文说到RxJS其实就是发布者和订阅者之间玩的游戏,可能你还是不理解,那我们再说的具体点。

  1. RxJS是一个事件的处理库,用于处理各种事件,比如DOM的点击、定时器的触发、API的消息获取
  2. 事件需要订阅,比如DOM的点击通过addEventListener,API获取通过注册回调函数

通过两个步骤,我们成功划分出了发布者和订阅者,但是在实际业务场景中,这些事件的订阅和发布会更加复杂,涉及一对多、多对一、多对多等场景。

而异步事件的处理则更加复杂,相信很多人都听说过回掉地狱这个词,指的是在没有promise前,API的订阅需要用回调函数进行注册,一旦API有串行请求,就很容易写出冲击波代码,像这样:

    function getList() {
        request('url', {
            succcess: function(res) {
                request('url', {
                    data: res,
                    succcess: function(res) {
                        request('url', {
                            data: res,
                            succcess: function(res) {
                                // TODO
                            }
                        })
                    }
                })
            }
        })
    }

在promise成为规范后,后面async await这种用同步写异步代码的方式流行后,这种情况得到了改善,但是在RxJS中,你会看到一个不一样的编码方式,毕竟promise也属于另一个编程范式。

import { ajax } from 'rxjs/ajax';
import { concatMap } from 'rxjs/operators';

/* 1. 判断登录
 * 2. 获取基础信息
 * 3. 根据基础信息换取商品信息
 */
const order$ = ajax('url') // 1. 判断登录
  .pipe(
    concatMap(isLogin => ajax('url')), // 2. 获取基础信息
    concatMap(info => ajax('url')), // 3. 根据基础信息换取商品信息
  )
  .subscribe(goods => {
      // TODO
  });

带来了什么新问题?

不得不说,Reactive Extensions(Rx)是一种非常棒的编码模式,不过我们得结合实际的业务场景做具体分析。

  1. 团队内的成员是否具备高度抽象思维,因为这是一种函数式、响应式编程范式,团队成员是否可以快速学习进入开发?
  2. RxJS最擅长处理的是异步事件,那么我们的业务场景是否真的有这么复杂吗?
  3. RxJS的代码是高度抽象的,抽象的代码是不如命令式代码易读,这点无可厚非,那么我们该如何组织我们的业务代码?

函数式编程范式

这里顺带提一下函数式编程范式,首先,这是一种有约束的编程范式:

  1. 声明式(Declarative)

声明式,与之对应的是命令式,我们可以看下代码有何不同:

const arr = [1, 2, 3, 4, 5]
// 命令式
for (let i = 0; i < arr.length; i++) {
    console.log(arr[i])
}

// 声明式
arr.map((item) => {
    console.log(item)
})

我们不难发现,使用声明式,代码简洁很多,看不到for了,也没有了额外的变量声明。

  1. 纯函数(Pure Function)

函数的调用不会有额外的副作用(IO操作、DOM操作、网络请求等),每次的输入都有唯一确认的输出。

  1. 数据不可变性(Immutability)

不改变源数据,举个例子:

const arr = [1, 2, 3, 4, 5]
// 改变了源数据
arr.push(6)

// 添加了新元素,但并没有改变原始数据,符合数据不可变性原则
function push2(source, item) {
    return [...source, item]
}

结合实际业务场景

这也是我最近做的例子,个人觉得结合RxJS会比较合适,就是IM聊天会话页。

我们先来梳理一下逻辑:

事件:

  1. 通过HTTP GET拉取历史会话列表;
  2. 通过websocket协议实时接收消息;
  3. 输入事件;
  4. 发送内容事件;
  5. 点击内容事件(如图片预览、视频播放、语音播放等);

我们结合下UI交互,再组织下逻辑层,不难发现:

  1. 对于【聊天内容显示区域】,很明显是订阅者,订阅者的特点是什么?上文已提过:订阅者只负责接收到通知后完成自身的业务逻辑,而并不关心消息的来源

  2. 发布者有哪些?

    • 通过websocket推送的消息(异步事件)
    • 通过HTTP GET拉取的消息(异步事件)
    • 用户点击发送的消息(同步事件)

一个订阅者支持订阅多个事件流吗?RxJS当然支持,我们可以通过合并事件流、转化事件流等手段,来达到我们的目的。

这里代码就不贴了,感兴趣的同学可以按上述思路实践一下。

小结

本文还有很多RxJS的概念没有介绍到,比如:

  • Hot Observable和Cold Observable用于解决存在多个订阅者,有人会迟到的问题
  • 经典的弹珠图表示法,让我们可以清晰的看到事件的流向
  • 多播,用于处理一个事件流存在多个订阅者的问题
  • 异常处理,上游的数据出错了如何处理
  • ...

如果大家有感兴趣的点,也欢迎随时讨论。

简述Vue和React开发体验的异同(状态层)

前言

接上篇文章,本文我们来聊聊对于状态这块,Vue和React有何异同。

state vs data

先说关键词命名,在Vue中,通过声明data来定义内部变量,data翻译为中文是数据的意思,这也符合Vue的整体设计,响应式数据,一个数据的变化,会引发一系列的关联动作。而在React中,则变成了关键词state,翻译过来为状态的含义,通过状态驱动视图的更新。

在Vue中,数据是响应式的,这个响应式包含两方面:

  • JS内存中的变量值发生变化,通知DOM进行绘制
  • DOM中元素内容发生变化,通知JS内存中的变量值改变

在实现上,Vue利用JS的API,实现了点运算符的重载,利用如v-model等显式声明,通过对DOM元素的监听,及时反馈至内存变量的更新。Vue的这种做法其实是有一定的性能损耗的,但是带来是开发者的低门槛、高效率。

React在状态更新这块采用的是显式声明(setState),状态更新后,调用render函数,通知DOM进行绘制,数据流的传递是单向的(当然想实现Vue中的反向更新也是简单的,只是官方并不提倡)

我们来看个简单的例子,实现input组件输入值的绑定:

Vue:
<template>
    <input type="text" v-model="value" />
</template>
<script>
    export default {
        data() {
            return {
                value: ""
            }
        }
    }
</script>

React:
function render() {
    const [value, setValue] = useState("");
    return (
       <input type="text" onChange={(e) => setValue(e.target.value)} />
    )
}

Vue可以通过v-model这个指令把数据进行了响应式关联,可以减少写监听回调的一部分代码,但事实上,Vue也可以写成这样:

Vue:
<template>
    <input type="text" :value="value" @input="setValue">
</template>
<script>
    export default {
        data() {
            return {
                value: ""
            }
        },
        methods: {
            setValue(e) {
                this.value = e.target.value;
            }
        }
    }
</script>

虽然真实开发中大家不会这么写,谁叫Vue已经提供了现成的指令了呢。

在Vue中,有一个比较好用的功能:computed,可以监听依赖项的改变而进行时时计算,比如我们想实现第三个输入框为前两个输入框的值,我们会这么写:

Vue:
<template>
    <input type="text" v-model="a" /> +
    <input type="text" v-model="b" /> =
    <input type="text" v-model="sum" />
</template>
<script>
    export default {
        data() {
            return {
                a: "",
                b: "",
            }
        },
        computed: {
            sum() {
                return this.a + this.b
            }
        }
    }
</script>

computed的依赖收集

写到这里的时候,我产生了一个好奇的点,sum函数的调用时机如何确定,直观反应是需要收集该函数的依赖项,在React中,直接会要求开发者传入一个deps数组,只要每次比较数组的引用地址就可以确定,而在Vue中并没有要求开发者声明。常见的依赖收集有以下两种方式

  • 静态词法分析
  • 执行函数后获取

静态分析会比较消耗CPU性能,举个例子:

computed: {
    sum() {
        const a = "x";
        const b = a;
        const c = b;
        return this[c] * 2
    }
}

很明显,这个函数的依赖是x变量,但是需要推到x变量需要依次推导 c => b => a => "x",如果里面涉及到循环引用还会更加复杂,显然Vue不会走这条路。那么执行代码呢,这条路貌似是可行的,因为在Vue中数据的读取操作可以被监听到,那么思路可以是:执行一次sum函数,完毕后取所有被调用get的变量,组成依赖项,查阅相关资料后,证明Vue也是这么做的。

如果代码写成这样的话:

computed: {
    sum() {
        if (Math.random() < 0.5) {
            return;
        }
        return this.a + this.b
    }
}

这种情况下,有一半的概率导致数据无法及时更新,因为是首次执行的时候确定依赖项的。如果想在React实现这个需求,可以自己封装自定义hooks,可以模拟实现computed的功能。

逻辑复用

再来谈谈逻辑的复用性,在React还没出Hooks之前,和Vue差不多,UI描述和逻辑是混在一起的,想抽也抽不出来。所以在平时开发中大部分场景都是复制代码的形式来做的,能做的一些事情也比较有限,比如抽工具库、精心设计一个组件的规格。在React Hooks出现后,这件事发生了一些变化,UI描述和逻辑天然被拆开了,逻辑的复用也变得顺理成章了。

总结

  • 从数据驱动UI上来说,React和Vue没有什么本质上的区别,状态改变通知UI层重绘,只是Vue会比较隐晦,React则通过setState显式调用

  • Vue面向开发者做了很多“小工具”,来帮助开发者提高生产效率

  • 从开发体验上来说,还是仁者见仁智者见智了,毕竟大家的编码习惯都受着很多因素的影响

简述Vue和React开发体验的异同(UI层)

前言

最近刚加入新公司,组内的前端技术栈以Vue为主,之前在上家公司写React会多一些,花了几个小时的时间重新过了一遍Vue的文档,毕竟已经快三年没接触了。两个事物在某些方面具有共通点,才能比较差异性,而差异性可能会是我们平常关注比较多的方向。在日常业务开发中,我们主要会关注两点,状态(state || data)和展示(UI),本文以阐述UI为主,状态层面的问题会在后续文章中做剖析。

UI层的描述方式

Vue是以template进行描述,而React以JSX进行描述。(当然Vue也可以用jsx进行描述,但其实本质还是template,只是提供开发的一层糖,这里不再做展开讨论。)

Vue:
<template>
    <div>hello</div>
</template>

React:
function render() {
    return (
       <div>hello</div>
    )
}

这么一看感觉上差不多,但是要知道Vue也好,React也罢,都是现代化的前端开发套件,在开发的时候势必要考虑这段代码是否可以复用,放到不同的上下文后如何做通信。到这一步的时候,我们的代码就变成了这样:

Vue:
<template>
    <div>{{text}}</div>
</template>
<script>
    export default {
      props: ['text']
    }
</script>

React:
function render(props) {
    return (
       <div>{props.text}</div>
    )
}

借助jsx天然的优势,在React中,传参和组件规格的定义可以直接使用函数的特性,而在Vue中,因为是模版,模版是不可以直接执行的,是需要把模版字符串拿来做解析的,然后再根据当前的上下文信息,去替换类似{{text}}这样的字符串,而组件规格的定义,也落到了script里,用js去做描述。而对于当前组件内可用的状态,在Vue中没有明确的区分,不论是data还是props,都可以通过this.xxx进行访问。在React中,还是有区分的,分别通过props和state去做访问。

我们再看看如何子组件调用父组件方法的

Vue:
<template>
    <div @click="onClick">{{text}}</div>
</template>
<script>
    export default {
      props: ['text'],
      methods: {
          onClick() {
              this.$emit('click')
          }
      }
    }
</script>

React:
function render(props) {
    return (
       <div onClick={props.click}>{props.text}</div>
    )
}

React同样也是借助函数的特性,简单的完成函数调用。在Vue中得借助自身的runtime来实现,通过调用$emit,$emit继续去查找并触发目标事件,完成通信。

说完了传属性、传函数,当然在日常开发中还有传UI描述(element)的需求。

Vue:
<template>
    <modal>
        <solt></solt>
    </modal>
</template>

React:
function render(props) {
    return (
       <Modal>{props.children}</Modal>
    )
}

我们以一个模态框举例,想要实现调用者能自绘内容区域,这时候通过属性就不够用,得传入一段UI描述(element),在Vue中,使用了一个关键字solt插槽来占位,在实例化时做替换,而在React中,一如既往的变量引用。

我们在调用modal组件的时间,代码是这样的

Vue:
<template>
    <modal>
        <template>
            <div>hello</div>
        <template>
    </modal>
</template>

React:
function render(props) {
    return (
       <Modal>
           <div>hello</div>
       </Modal>
    )
}

好像差不多,不过涉及到多个element传入的时候,会变成这样

Vue:
<template>
    <modal>
        <template #title>
            <div>hello</div>
        <template>
        <template #content>
            <div>hello</div>
        <template>
    </modal>
</template>

React:
function render(props) {
    return (
       <Modal title={<div>hello</div>}>
           <div>hello</div>
       </Modal>
    )
}

是不是不太一样,其实在React中也可以写成像Vu e一样平铺,不过在子组件那就成了props.children[0] props.children[1] 这样的模式了,不够语义话,所以会放在自定义参数那,只是参数类型是element,从数据结构上来说,都是一个name对应一个element,只是在写法上略有差异。

结语

Vue在template这块还是做了很多事情的,比如没有提到的指令系统等。通过描述来代替逻辑编码。好处很明显,描述会更加易读,上手成本低,但弊端也恰恰是对方的优势,逻辑编码的灵活性更高。但凡事都讲究一个够用,DSL设计的足够完备,能满足日常的开发需求,也就够了。

在Vue中,判断只有一种写法:
<template>
    <div v-if="show">hello</div>
</template>

在React中,判断有N中写法:
function render1(props) {
    if (props.show) {
        return <div>hello</div>
    }
    return null;
}
function render2(props) {
    return props.show ? <div>hello</div> : null;
}
function render3(props) {
    return props.show && <div>hello</div>;
}
...

嗯,JS真灵活。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.