easytuan / blog Goto Github PK
View Code? Open in Web Editor NEWLicense: MIT License
License: MIT License
我们先来看个实际中的场景:
在浏览器上打印出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的执行任务分为了两类:同步任务和异步任务。而异步任务,又分成了两类:
整体代码
、定时器
、I/O
Promise
、MutationObserver
、observer
这里我们需要注意一点,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长什么样,话不多说,先上图:
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
关于这 6 个阶段,官网描述为:
setTimeout()
和 setInterval()
的调度回调函数。setImmediate()
调度的之外),其余情况 Node
将在适当的时候在此阻塞。setImmediate()
回调函数在这里执行。socket.on('close', ...)
。这是一个在Node.js中实现但没有在浏览器中实现的API,我们先来看下它的定义:
该方法用来把一些需要长时间运行的操作放在一个回调函数里,在浏览器完成后面的其他语句后,就立刻执行这个回调函数。
该特性是非标准的,请尽量不要在生产环境中使用它!
看起来和setTimeout没什么区别,不过他们都运行在不同的阶段,setImmediate会在一旦在当前轮询阶段完成时,执行回调用。
我们来看一段比较坑爹的代码:
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
});
那你觉得是先输出setTimeout还是setImmediate呢?
其实我们在实现运行后发现,都有可能!!!
官方是这么说的:
setImmediate
总是被有限调用。精简一下,大致意思就是说:它们的执行任务优先级不一样。
process.nextTick()
是一个异步任务,它属于微任务。
process.nextTick()
将 callback 添加到"next tick queue"。 在 JavaScript 堆栈上的当前操作运行完成之后,且在允许事件循环继续之前,此队列将被完全排空。 如果递归地调用 process.nextTick()
,则可能会创建无限的循环。
我们通过了开头的一个小例子,引入了事件循环机制(Event Loop),而执行任务分为了两类:同步任务和异步任务。而异步任务,又分成了两类:宏任务和微任务。一般情况下,微任务的执行优于宏任务。当在极端场景下,比如微任务实在是太多了,Chrome V8会先执行宏任务。
而在Node.js中,任务的调度分为了六个阶段,这点和浏览器略有不同。因为所处的阶段不同setTimeout和setImmediate的执行顺序也会因当前上下午环境而改变。
我们要谈及RxJS,不得不先说下Reactive Extensions(Rx),这是一种编程模式,他的目标是对异步的集合进行操作(ps: 这种编辑模式有利有弊,当然这不是本文的重点,这里不再赘述,有兴趣的同学可以自行查阅相关资料。
)。Rx家族成员很多,如RxCpp、RxJava、Rx.NET、Rx.rb、RxPy等,我们从命名上不难看出,后缀就是语言的名称,那么就不难理解RxJS是Javascript语言对Reactive Extensions(Rx)的实现。
引自官方文档的概念:
RxJS 是一个库,它通过使用
observable
序列来编写异步和基于事件的程序。它提供了一个核心类型 Observable,附属类型 (Observer、 Schedulers、 Subjects) 和受 [Array#extras] 启发的操作符 (map、filter、reduce、every, 等等),这些数组操作符可以把异步事件作为集合来处理。
可以把 RxJS 当做是用来处理事件的
Lodash
。
官方文档引入了很多概念,如Observer、 Schedulers、 Subjects,这些对初学者并不友好,因为这些都是RxJS的概念。RxJS的学习路线对于大多数人来说都太陡峭了,很容易从入门到放弃,甚至还没入门就放弃了。
我们把概念做个精简,RxJS其实就是发布者和订阅者之间玩的游戏
。
在设计模式中,有一个模式就是发布订阅模式
,这个模式中,
大家都责任都很明确,但是必然有一些负责的情况,比如广播是私有的还是公开的,如果订阅者迟到了,那么前面的消息是否需要告知订阅者,对着这些情况的处理,RxJS定义了一套完整的操作符
,注意是完整的,这个世界上所有的情况都被涵盖在操作符
中了,你不必再造轮子了。
上文说到RxJS其实就是发布者和订阅者之间玩的游戏
,可能你还是不理解,那我们再说的具体点。
通过两个步骤,我们成功划分出了发布者和订阅者,但是在实际业务场景中,这些事件的订阅和发布会更加复杂,涉及一对多、多对一、多对多等场景。
而异步事件的处理则更加复杂,相信很多人都听说过回掉地狱
这个词,指的是在没有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)是一种非常棒的编码模式,不过我们得结合实际的业务场景做具体分析。
这里顺带提一下函数式
编程范式,首先,这是一种有约束的编程范式:
声明式,与之对应的是命令式,我们可以看下代码有何不同:
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
了,也没有了额外的变量声明。
函数的调用不会有额外的副作用(IO操作、DOM操作、网络请求等),每次的输入都有唯一确认的输出。
不改变源数据,举个例子:
const arr = [1, 2, 3, 4, 5]
// 改变了源数据
arr.push(6)
// 添加了新元素,但并没有改变原始数据,符合数据不可变性原则
function push2(source, item) {
return [...source, item]
}
这也是我最近做的例子,个人觉得结合RxJS会比较合适,就是IM聊天会话页。
我们先来梳理一下逻辑:
事件:
我们结合下UI交互,再组织下逻辑层,不难发现:
对于【聊天内容显示区域】,很明显是订阅者,订阅者的特点是什么?上文已提过:订阅者只负责接收到通知后完成自身的业务逻辑,而并不关心消息的来源
。
发布者有哪些?
一个订阅者支持订阅多个事件流吗?RxJS当然支持,我们可以通过合并事件流、转化事件流等手段,来达到我们的目的。
这里代码就不贴了,感兴趣的同学可以按上述思路实践一下。
本文还有很多RxJS的概念没有介绍到,比如:
如果大家有感兴趣的点,也欢迎随时讨论。
接上篇文章,本文我们来聊聊对于状态这块,Vue和React有何异同。
先说关键词命名,在Vue中,通过声明data来定义内部变量,data翻译为中文是数据的意思,这也符合Vue的整体设计,响应式数据,一个数据的变化,会引发一系列的关联动作。而在React中,则变成了关键词state,翻译过来为状态的含义,通过状态驱动视图的更新。
在Vue中,数据是响应式的,这个响应式包含两方面:
在实现上,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>
写到这里的时候,我产生了一个好奇的点,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会多一些,花了几个小时的时间重新过了一遍Vue的文档,毕竟已经快三年没接触了。两个事物在某些方面具有共通点,才能比较差异性,而差异性可能会是我们平常关注比较多的方向。在日常业务开发中,我们主要会关注两点,状态(state || data)和展示(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真灵活。
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.