javascript同步和异步的区别与实现方式

JavaScript017

javascript同步和异步的区别与实现方式,第1张

javascript语言是单线程机制。所谓单线程就是按次序执行,执行完一个任务再执行下一个。

对于浏览器来说,也就是无法在渲染页面的同时执行代码。

单线程机制的优点在于实现起来较为简单,运行环境相对简单。缺点在于,如果中间有任务需要响应时间过长,经常会导致

页面加载错误或者浏览器无响应的状况。这就是所谓的“同步模式”,程序执行顺序与任务排列顺序一致。对于浏览器来说,

同步模式效率较低,耗时长的任务都应该使用异步模式;而在服务器端,异步模式则是唯一的模式,如果采用同步模式个人认为

服务器很快就会出现12306在高峰期的表现。。。。

异步模式的四种方式:

1.回调函数callback

所谓回调函数,就是将函数作为参数传到需要回调的函数内部再执行。

典型的例子就是发送ajax请求。例如:

$.ajax({

async: false,

cache: false,

dataType: 'json',

url: "url",

success: function(data) {

console.log('success')

},

error: function(data) {

console.log('error')

}

})

当发送ajax请求后,等待回应的过程不会堵塞程序运行,耗时的操作相当于延后执行。

回调函数的优点在于简单,容易理解,但是可读性较差,耦合度较高,不易于维护。

2.事件驱动

javascript可以称之为是基于对象的语言,而基于对象的基本特征就是事件驱动(Event-Driven)。

事件驱动,指的是由鼠标和热键的动作引发的一连串的程序操作。

例如,为页面上的某个

$('#btn').onclick(function(){

console.log('click button')

})

绑定事件相当于在元素上进行监听,是否执行注册的事件代码取决于事件是否发生。

优点在于容易理解,一个元素上可以绑定多个事件,有利于实现模块化;但是缺点在于称为事件驱动的模型后,流程不清晰。

3.发布/订阅

发布订阅模式(publish-subscribe pattern)又称为观察者模式(Observer pattern)。

该模式中,有两类对象:观察者和目标对象。目标对象中存在着一份观察者的列表,当目标对象

的状态发生改变时,主动通知观察者,从而建立一种发布/订阅的关系。

jquery有相关的插件,在这不是重点不细说了。。。。回头写个实现贴上来

4.promise模式

promise对象是CommonJS工作组提供的一种规范,用于异步编程的统一接口。

promise对象通常实现一种then的方法,用来在注册状态发生改变时作为对应的回调函数。

promise模式在任何时刻都处于以下三种状态之一:未完成(unfulfilled)、已完成(resolved)和拒绝(rejected)。以CommonJS

Promise/A

标准为例,promise对象上的then方法负责添加针对已完成和拒绝状态下的处理函数。then方法会返回另一个promise对象,以便于形成promise管道,这种返回promise对象的方式能够支持开发人员把异步操作串联起来,如then(resolvedHandler,

rejectedHandler)。resolvedHandler

回调函数在promise对象进入完成状态时会触发,并传递结果;rejectedHandler函数会在拒绝状态下调用。

Jquery在1.5的版本中引入了一个新的概念叫Deferred,就是CommonJS promise A标准的一种衍生。可以在jQuery中创建

$.Deferref的对象。同时也对发送ajax请求以及数据类型有了新的修改,参考JQuery API。

除了以上四种,javascript中还可以利用各种函数模拟异步方式,更有诡异的诸如用同步调用异步的case

只能用team里同事形容java和javascript的一句话作为结尾:

“写java像在高速路上开车,写javascript像在草原上开车”-------------以此来形容javascript这种无类型的语言有多自由

but,如果草原上都是坑。

vue.js 采用数据劫持结合发布者-订阅者模式的方式,通过 Object.defineProperty() 来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。

首先我们为每个vue属性用Object.defineProperty()实现数据劫持,在监听数据的过程中,为每个属性分配一个订阅者集合的管理数组dep;然后在编译的时候在该属性的数组dep中添加订阅者 watcher,v-model会添加一个订阅者,{{}}也会,v-bind也会,只要用到该属性的指令理论上都会,接着为input会添加监听事件,修改值就会为该属性赋值,触发该属性的set方法,在set方法内通知订阅者数组dep,订阅者数组循环调用各订阅者的update方法更新视图。

实现步骤:修改输入框内容 =>在事件回调函数中修改属性值 =>触发属性的 set 方法=>发出通知 dep.notify() =>触发订阅者的 update 方法 =>更新视图。

流程图

在实例化一个Vue对象的时候,会传进去一个data对象,之后分成两个进程,一个进程是对挂载目标元素模板里的v-model和{{ }};两个指令进行编译。另一个进程是对传进去的data对象里面的数据进行监听。

上图中,observe是利用Object.defineProperty()对传入的data对象进行数据监听,在数据改变的时候触发该属性的set方法,更新该属性的值,并发布消息,我(该属性)的值变了。

compile是编译器,找到vue的指令v-model所在的元素,将data中该属性的值赋给元素的value,并给这个元素添加二级监听器,在元素的值改变的时候,将新值赋给data里面同名属性,这个时候就完成了单向数据绑定,视图 >>模型。

那么最终的由模型到视图的更新,依赖于dep和watcher,dep会收集订阅者,就是绑定了data里面属性的元素,在数据更新的时候,会触发该属性的set方法,在set里触发该属性的消息发布通知函数。而Watcher根据收到的数据变化通知,更新相应的数据。

dep这个东东给大家解释一下,就是data里的每个属性都有一个dep对象,dep对象里可以有很多订阅者(watcher),但是只有一个添加订阅者的方法和一个发布变化通知的方法,就是模板上可以有多处元素绑定data里的同一个属性值,所以dep是依赖于data里面的属性的。

而Watcher是每个{{ }}有一个,初次编译的时候,会在new的时候自动更新一下模板的数据,等到下次数据改变的时候,由dep通知数据更新,直接调用watcher的update方法,更新模板的绑定数据。

observer 模块共分为这几个部分:

示意图如下:

Observer的构造函数

value是需要被观察的数据对象,在构造函数中,会给value增加 ob 属性,作为数据已经被Observer观察的标志。如果value是数组,就使用observeArray遍历value,对value中每一个元素调用observe分别进行观察。如果value是对象,则使用walk遍历value上每个key,对每个key调用defineReactive来获得该key的set/get控制权。

Dep是Observer与Watcher之间的纽带,也可以认为Dep是服务于Observer的订阅系统。Watcher订阅某个Observer的Dep,当Observer观察的数据发生变化时,通过Dep通知各个已经订阅的Watcher。

Watcher是用来订阅数据的变化的并执行相应操作(例如更新视图)的。Watcher的构造器函数定义如下:

参数中,vm表示组件实例,expOrFn表示要订阅的数据字段(字符串表示,例如a.b.c)或是一个要执行的函数,cb表示watcher运行后的回调函数,options是选项对象,包含deep、user、lazy等配置。

Object.defineProperty(obj, prop, descriptor) ,这个语法内有三个参数,分别为 obj (要定义属性的对象) prop (要定义或修改的属性的名称或 Symbol ) descriptor (要定义或修改的属性描述符=>具体的改变方法)

简单地说,就是用这个方法来定义一个值。当调用时我们使用了它里面的get方法,当我们给这个属性赋值时,又用到了它里面的set方法;

主要解释第三个参数{

value: 设置属性的值

writable: 值是否可以重写。true | false

enumerable: 目标属性是否可以被枚举。true | false(就是能不能被遍历出来)

configurable: 目标属性是否可以被删除或是否可以再次修改特性 true | false

set: 目标属性设置值的方法

get:目标属性获取值的方法

}

set是一个函数,接收一个新值,会在值被重写或修改的时候触发这个函数

get是一个函数,返回一个值,会在属性被调用的时候触发。

Object.defineProperty()详解

Object.defineProperty()官方文档

已经了解到vue是通过数据劫持的方式来做数据绑定的,其中最核心的方法便是通过Object.defineProperty()来实现对属性的劫持,那么在设置或者获取的时候我们就可以在get或者set方法里假如其他的触发函数,达到监听数据变动的目的。

我们知道通过Object.defineProperty()可以实现数据劫持,它的属性在赋值的时候触发set方法,

当然要是这么粗暴,肯定不行,性能会出很多的问题。

observer用来实现对每个vue中的data中定义的属性循环用Object.defineProperty()实现数据劫持,以便利用其中的setter和getter,然后通知订阅者,订阅者会触发它的update方法,对视图进行更新。

为什么要订阅者 :在vue中v-model,v-name,{{}}等都可以对数据进行显示,也就是说假如一个属性都通过这三个指令了,那么每当这个属性改变的时候,相应的这个三个指令的html视图也必须改变,于是vue中就是每当有这样的可能用到双向绑定的指令,就在一个Dep中增加一个订阅者,其订阅者只是更新自己的指令对应的数据,也就是v-model='name'和{{name}}有两个对应的订阅者,各自管理自己的地方。每当属性的set方法触发,就循环更新Dep中的订阅者。

订阅发布模式(又称观察者模式)定义了一种一对多的关系,让多个观察者同时监听某一个主题对象,这个主题对象的状态发生改变时就会通知所有观察者对象。

发布者发出通知 =>主题对象收到通知并推送给订阅者 =>订阅者执行相应操作

举个例子:

2.实现compile: compile的目的就是解析各种指令称真正的html。

这样一来就实现了vue的数据双向绑定。

参考链接:

理解VUE双向数据绑定原理和实现---赵佳乐

Vue的双向数据绑定原理

vue双向绑定原理分析

Vue原理解析之observer模块

深入响应式原理

前端

深入node.js 3 模板引擎原理 事件 文件操作 可读流的实现原

模板引擎是基于new Function + with 实现的。

ejs使用

实现:

思路:借助fs的readFile先读取文件内容,然后使用正则表达式替换掉即可。

打印的结果是一样的。

复杂的情况呢?拼接字符串,拼成想要的代码

主要难点就是在字符串的拼接,第二部分,将全文分为三部分,然后拼接对应的,如

let str = ""

with(obj){

str+= `<!DOCTYPE html>

<html lang="en">

<head>

<meta charset="UTF-8">

<meta http-equiv="X-UA-Compatible" content="IE=edge">

<meta name="viewport" content="width=device-width, initial-scale=1.0">

<title>Document</title>

</head>

<body>

`

arr.forEach(item=>{

str+=`

<li>

${item}

</li>

`

})

str+=`

</body>

</html>`}

return str

登录后复制

这就是拼出来的字符串,然后再new Function,包裹一层函数,将with的obj传入,返回str。

大概长这样。

效果就是:

所以本质就是将获取到的内容,使用正则表达式匹配,拼接字符串成我们想要的内容,用with包裹,改变内部作用域,再通过new Function将str包装成一个函数,传入对应的值给obj。然后运行之后str就能正常通过作用域获取值赋值。

buffer

在服务端,需要一个东西来标识内存,但不能是字符串,因为字符串无法标识图片。node中使用buffer来标识内存的数据。他把内存转换成了16进制来显示(16进制比较短)buffer每个字节的取值范围就是0-0xff(十进制的255).

node中buffer可以和字符串任意的转换(可能出现乱码)

编码规范:ASCII ->GB8030/GBK ->unicode ->UTF8

Buffer代表的是内存,内存是一段固定空间,产生的内存是固定大小,不能随意增加。

扩容:需要动态创建一个新的内存,把内容迁移过去。

创建一个长度为5的buffer,有点像数组,但是数组可以扩展,而buffer不可以扩展。

还有一种声明buffer。

Buffer.form。

一般使用alloc来声明一个buffer,或者把字符串转换成Buffer使用。文件操作也是采用Buffer形式。

buffer使用

无论是二进制还是16进制,表现得东西是一样的。

base64编码:

base64可以放在任何路劲的链接里,可以减少请求次数。但是文件大小会变大。比如webpack中的asset/type,把一些小的文件转换成了Base64编码内嵌到了文件当中,虽然可以减少请求次数,但也增大了文件的大小。

base64的来源就是将每个字节都转化为小于64的值。没有加密功能,因为规则很简单。如

第一步:将buffer中每个位置的值转为二进制。如上。

一个字节有八位,八位的最大值是255,有可能超过64。而base64编码是要将每个字节转化为小于64的值。所以就取每个字节的6位。6位的最大值就是2*6 - 1 = 63。也就是:

第二步:将38的形式转成64的,保证每个字节小于64。将其转为十进制。

第三步,通过特定的编码规则转换即完成。

将我们获取到的十进制传入,因为每个字节都是小于64的,所以不超过。

完成。

buffer的常用方法

除了form和alloc还有

slice

// slice

const a = Buffer.from([1,2,3,4,5])

const d = a.slice(0,2)

d[1] = 4

console.log(d)

console.log(a)

登录后复制

与数组的用法相同,但是他并不是浅复制,而是直接关联在一起。改变d也会改变a。而数组的slice是浅复制。改变原始数据的值不会改变。

copy

将Buffer的数据拷贝到另一个数据上。

const a = Buffer.from([1, 2, 3, 4, 5])

const d = Buffer.alloc(5)

a.copy(d, 1, 2, 3)//四个参数,拷贝到哪里?从d的第一个开始拷贝 a的a[2]->a[3]

console.log(d)

登录后复制

copy四个参数,分别是拷贝的目标d。从d的第几个长度开始。拷贝a的第2到第3位。

所以应该是 <Buffer 00 03 00 00 00 >

concat

用于拼接buffer

Buffer.concat(arr, index)

第二个参数是拼接出来的Buffer的长度,如果大于原本的长度,用00填写。

Buffer.myConcat = function (

bufferList,

length = bufferList.reduce((a, b) =>a + b.length, 0)

) {

let bigBuffer = Buffer.alloc(length)

let offset = 0

bufferList.forEach((item) =>{

//使用copy每次拷贝一份然后offset向下走。

item.copy(bigBuffer, offset)

offset += item.length

})

return bigBuffer

}

登录后复制

借助copy,逐个拷贝一份即可。

文件操作

fs模块有两种基本api,同步,异步。

io操作,input,output,输入输出。

读取时默认是buffer类型。

写入的时候,默认会将内容以utf8格式写入,如果文件不存在则创建。

读取的data是Buffer类型,写入的是utf8格式。

这种读写适合小文件

读取文件某段内容的办法

fs.open用于打开一个文件。fs.read用来读取内容并且写入到buffer中。

fs.write用于将内容写入某个文件之中。如上,打开了b.txt,然后用fs.wirte。五个参数,分别是fd,buffer,从buffer的第0个位置,到buffer的第0个位置,从b.txt的第0位开始写,回调函数。

写入成功。

这种写法也不太美观,每次都需要fs.open然后fs.read或者fs.wirte,容易造成回调地狱。

流 Stream的出现

源码的实现步骤:

fs的CreateReadStrem是new了一个ReadStream,他是基于Stream的Readable类的,然后自己实现了_read方法。供Stream.prototype.read调用。

1 内部会先open文件,然后直接直接继续读取操作,默认是调用了pause暂停。

2 监听用户是否绑定了data事件,resume,是的话开始读取事件

3 调用fs.read 将数据读取出来。

4 调用this.push去emit data事件,然后判断是否可以读取更多再去读取。

第一种: fs.readFile(需要将文件读取到磁盘中,占用内容)=>fs.wirteFile

第二种: fs.open =>fs.read =>fs.write 回调地狱。

实现读取三个字节写入三个字节。采用fs.open fs.read fs.write的方法。

实现copy方法。

看实现:

首先创建一个三字节的Buffer。

然后使用fs.open打开要读取和要写入的文件。

因为我们是每三个每三个读取,所以需要采用递归方式,一直读取文件。

直到读取完毕,调用回调函数。fs.read和fs.write的参数都是类似的,即fd,buffer,buffer的start,buffer的end,读取文件/写入文件的start、回调函数(err,真正读取到的个数/真正写入的个数)

现在基本实现了读一部分,写一部分,但是读写的逻辑写在一起了,需要把他拆开。

流 Stream模块

可读流

不是一下子把文件都读取完毕,而是可以控制读取的个数和读取的速率。

流的概念跟fs没有关系,fs基于stream模块底层扩展了一个文件读写方法。

所以fs.open,fs.read等需要的参数,createReadStream也需要、

返回一个对象,获取需要监听data事件。

close事件在end事件之后触发。

由此可以看出:流的内部基于 fs.open fs.close fs.read fs.write以及事件机制。

暂停是不再触发data事件

rs.resume()是恢复。

实现readStream

从vscode调试源码得知

实现思路:

createReadStream内部new了一个ReadStream的实例,ReadStream是来自于Stream模块。

做一系列参数默认后, 调用this.open方法,这个方法会去调用fs.open去打开文件,打开之后触发事件,从回调的形式发布订阅模式,然后监听事件,当发现有用户注册了data事件之后,调用fs.read,j监听open事件,在open之后再去读取文件等等。

这样我们的读写逻辑就分离开了,从回调的形式变成了发布订阅模式,有利于解耦。

第一步:

第一步:参数初始化,并调用fs.open

open打开之后会触发open事件,注意,这里是异步的

第二步: 监听用户注册的data事件,当用户注册了data事件才去调用fs.read。调用this.read的时候open还没完成。

所以第一次read的时候需要判断,然后只监听一次open事件,重复打开read事件。

这个end和start配合,表示读取文件从哪到哪的位置,但是end是包后的,比如上面的end为4,实际上读取到的是5。

创建buffer存放读取的内容,再判断应该读取多少内容,以哪个小为准。

然后打开fs.read。将读取到的buffer发布出去。再次调用this.read去继续读取。

start=1,end=4,读取到2345的内容,正确。

不给end,每次3个3个的读取。

接着实现暂停,需要一个开关。

这样就基本完成了。

总结:

一开始实现的的copy方法,也是利用fs.open, fs.read, fs.write等,通过回调的形式完成的,这样虽能完成,但是内聚度较高,容易形成回调地狱。

而基于fs模块,和events模块,实现的可读流,可以有效的解耦刚才的代码,通过发布订阅模式,在一开始订阅事件,在每个时间点对应发布事件,然后代码执行,各司其职。

open和close是文件流独有的,

可读流具备:on (‘data’ | ‘end’ | ‘error’), resume, pause这些方法。

相关代码:

// copy

const fs = require("fs")

const path = require("path")

// let buf = Buffer.alloc(3)

// //open打开一个文件,第一个参数是路劲,第二个参数是打开文件用途,第三个是回调函数。

// fs.open(path.resolve(__dirname, "a.txt"), "r", function (err, fd) {

// // fd 是file descriptor文件描述

// console.log("fd", fd)

// //读取a.txt的内容,并且将内容写入buf中的第0个位置到3第三个位置,从a.txt的第六个位置开始

// fs.read(fd, buf, 0, 3, 3, function (err, data) {

// fs.open(path.resolve(__dirname, "./b.txt"), "w", function (err, fd2) {

// fs.write(fd2, buf, 0, 3, 0, function (err, data2) {

// console.log("buf", buf)

// console.log("data2", data2)

// })

// })

// })

// })

function copy(source, target, cb) {

const BUFFER_SIZE = 3

const buffer = Buffer.alloc(BUFFER_SIZE)

//每次读入文件的位置

let r_offset = 0

//每次写入新文件的位置

let w_offset = 0

//读取一部分数据,写入一部分数据

//第三个参数可以是权限 权限有三个组合 rwx 可读可写可执行 r的权限是4,w的权限是2,x的权限是1 421 = 777 可写可读可执行

// 0o666表示最大的权限,默认不用写。

//读取的文件必须要存在。写入文件不存在会创建,如果文件有内容会清空。

fs.open(source, "r", function (err, fd1) {

//打开写的文件

fs.open(target, "w", function (err, fd2) {

//每次读取三个写入三个,回调的方式实现的功能,需要用递归

// 同步代码则可以采用while循环

function next() {

fs.read(

fd1,

buffer,

0,

BUFFER_SIZE,

r_offset,

function (err, bytesRed) {

// bytesRed真正读取到的个数

if (err) {

cb("读取失败")

return

}

if (bytesRed) {

//将读取到的内容写入target目标

fs.write(

fd2,

buffer,

0,

bytesRed,

w_offset,

function (err, written) {

if (err) retrun

// written 真正写入的个数

r_offset += bytesRed//每次写入之后,下一次读取的内容就该往前进

w_offset += written

next()

}

)

} else {

//读取内容为空,结束

fs.close(fd1, () =>{})

fs.close(fd2, () =>{})

cb()

}

}

)

}

next()

})

})

}

copy("./a.txt", "b.txt", (err, data) =>{

console.log("copy success")

})

登录后复制

createStream的实现

const EventMitter = require("events")

const fs = require("fs")

class ReadStream extends EventMitter {

constructor(path, options = {}) {

super()

this.path = path

//操作

this.flags = options.flags || "r"

this.encoding = options.encoding || null

this.autoClose = options.autoClose || true

this.start = options.start || 0

this.end = options.end || Infinity//读取的个数,包后的,如果是1 ,就可能读取到0,1,2

this.highWaterMark = options.highWaterMark || 64 * 1024

this.emitClose = options.emitClose || false

this.offset = this.start// 每次读取文件的位移数

this.flowing = true//暂停继续开关

this.open()// 文件操作,注意这个方法是异步的。

// events可以监听newListener,可以获取注册的所有事件

this.on("newListener", function (type) {

if (type === "data") {

//用户订阅了data事件,才去读取。

this.read()//这时候文件还没open,fd为undefined。

}

})

}

pause() {

//暂停

this.flowing = false

}

resume() {

//继续

if (!this.flowing) {

this.flowing = true

this.read()

}

}

destory(err) {

if (err) {

this.emit("error", err)

}

if (this.autoClose) {

fs.close(this.fd, () =>{

this.emit("close")

})

}

}

read() {

//希望在open之后打开

if (typeof this.fd !== "number") {

this.once("open", (fd) =>{

//之前实现的copy这段逻辑是写在了fs.open里面,换成发布订阅模式之后就可以分离出来。

this.read()//第二次read的时候,fd有值了

})

} else {

//判断每次读取多少。因为this.end是包后的,比如start = 0, end = 1, 那么读取的就是 0 , 1, 2所以需要+1

const howMutchToRead = Math.min(

this.end - this.offset + 1,

this.highWaterMark

)

const buffer = Buffer.alloc(howMutchToRead)

fs.read(

this.fd,

buffer,

0,

howMutchToRead,

this.offset,

(err, byteRead) =>{

if (err) {

this.destory(err)

} else {

if (byteRead) {

//读取到文件,发布data事件,发送真正读取到的内容

this.offset += byteRead

this.emit("data", buffer.slice(0, byteRead))

this.flowing &&this.read()

} else {

this.emit("end")

if (this.autoClose) {

this.destory()

}

}

}

}

)

}

}

open() {

fs.open(this.path, this.flags, (err, fd) =>{

if (err) {

//报错:

this.destory(err)

}

//从回调的形式变成了发布订阅模式

this.fd = fd

this.emit("open", fd)

})

}

}

const rs = new ReadStream("./a.txt", {

flags: "r", //创建可读流。

encoding: null, //默认Buffer

autoClose: true, //自动关闭,相当于读取完毕调用fs.close

emitClose: true, //触发close事件

start: 0, //从文件哪里开始读取

highWaterMark: 3, //每次读取的数据个数,默认是64*1025字节。

//end: 4, // 比如这个就会读取 1到5的内容

})

rs.on("open", () =>{

console.log("文件打开了")

})

// rs.on("data", (data) =>{

// console.log("监听Data事件", data)

// })

// //底层还是 fs.open fs.read fs.close

// const rs = fs.createReadStream("./a.txt", {

// flags: "r", //创建可读流。

// encoding: null, //默认Buffer

// autoClose: true, //自动关闭,相当于读取完毕调用fs.close

// emitClose: true, //触发close事件

// start: 0, //从文件哪里开始读取

// highWaterMark: 3, //每次读取的数据个数,默认是64*1025字节。

// })//返回一个对象

//没监听前,是非流动模式,监听后,是流动模式。

//监听data事件,并且不停触发

rs.on("data", function (chunk) {

console.log(chunk)

//暂停

rs.pause()

})

rs.on("end", function () {

console.log("读取完毕")

})

rs.on("close", function () {

console.log("文件关闭")

})

setInterval(() =>{

console.log("一秒后")

rs.resume()

}, 1000)