理解Node.js事件驅(qū)動(dòng)架構(gòu)

原文鏈接:Understanding Node.js Event-Driven Architecture
作者:Samer Buna
譯者:李序鍇
本文已獲得作者授權(quán),轉(zhuǎn)載請(qǐng)注明出處撑教。

大多數(shù)的Node對(duì)象(如HTTP requests, responses以及streams)都實(shí)現(xiàn)了EventEmitter模塊疲酌,這樣它們就能夠觸發(fā)和監(jiān)聽(tīng)事件饿凛。


事件驅(qū)動(dòng)特性最簡(jiǎn)單的形式就是通用的Node.js函數(shù)當(dāng)中的一些回調(diào)風(fēng)格(例如:fs.readFile)荠列。在這個(gè)類(lèi)比中刽酱,事件會(huì)立即啟動(dòng)(當(dāng)Node準(zhǔn)備調(diào)用回調(diào)時(shí))而回調(diào)則充當(dāng)事件處理器的角色撒遣。
當(dāng)你準(zhǔn)備好請(qǐng)調(diào)用我断盛,Node!
Node處理異步事件的最初方式就是通過(guò)回調(diào)愉舔。而這已經(jīng)是JavaScript擁有原生promises支持以及async/await特性之前很就以前的事情了。
回調(diào)基本上都是你傳遞給其它函數(shù)的函數(shù)伙菜。這可能是因?yàn)樵贘avaScript當(dāng)中函數(shù)是第一類(lèi)對(duì)象轩缤。
回調(diào)并不會(huì)在代碼當(dāng)中注明它是異步調(diào)用,理解這一點(diǎn)很關(guān)鍵。函數(shù)可以通過(guò)同步和異步兩種方式觸發(fā)回調(diào)火的。例如:如下是一個(gè)宿主函數(shù)fileSize壶愤,它接收一個(gè)回調(diào)函數(shù)cb,根據(jù)條件的不同它可以執(zhí)行同步和異步兩種方式的回調(diào)馏鹤。

function fileSize (fileName, cb) {
  if (typeof fileName !== 'string') {
    return cb(new TypeError('argument should be string')); // Sync
  }
  fs.stat(fileName, (err, stats) => {
    if (err) { return cb(err); } // Async
    cb(null, stats.size); // Async
  });
}

但是這是一種會(huì)導(dǎo)致預(yù)料之外錯(cuò)誤的糟糕實(shí)踐征椒。它使得宿主函數(shù)總是以同步或者異步方式執(zhí)行回調(diào)。
我們來(lái)探尋一個(gè)使用回調(diào)風(fēng)格書(shū)寫(xiě)的異步Node函數(shù)的典型例子湃累。

const readFileAsArray = function(file, cb) {
  fs.readFile(file, function(err, data) {
    if (err) {
      return cb(err);
    }
    const lines = data.toString().trim().split('\n');
    cb(null, lines);
  });
};

readFileAsArray接受文件路徑和回調(diào)函數(shù)作為參數(shù)勃救。其讀取文件內(nèi)容并將文件內(nèi)容分割為數(shù)組,再利用該數(shù)組調(diào)用回調(diào)函數(shù)治力。
下方是一個(gè)應(yīng)用實(shí)例蒙秒。假設(shè)我們?cè)谕?jí)目錄下有一個(gè)numbers.txt文件,其內(nèi)容如下:

10
11
12
13
14
15

假設(shè)我們的任務(wù)是統(tǒng)計(jì)該文件當(dāng)中的奇數(shù)個(gè)數(shù)宵统,我們可以使用readFileAsArray來(lái)簡(jiǎn)化代碼:

readFileAsArray('./numbers.txt', (err, lines) => {
  if (err) throw err;
  const numbers = lines.map(Number);
  const oddNumbers = numbers.filter(n => n%2 === 1);
  console.log('Odd numbers count:', oddNumbers.length);
});

這段代碼將數(shù)字內(nèi)容讀取成了字符串?dāng)?shù)組晕讲,之后解析成數(shù)字,再之后統(tǒng)計(jì)了奇數(shù)的數(shù)量马澈。

Node回調(diào)風(fēng)格在此處獲得了充分應(yīng)用瓢省。該回調(diào)的第一個(gè)參數(shù)是可為空的err參數(shù),我們將該回調(diào)作為最后一個(gè)參數(shù)傳遞給宿主函數(shù)痊班。由于用戶的閱讀習(xí)慣問(wèn)題勤婚,因此你應(yīng)該在你的函數(shù)一直按這種形式書(shū)寫(xiě)。讓回調(diào)作為宿主函數(shù)的的最后一個(gè)參數(shù)辩块,將error對(duì)象作為回調(diào)函數(shù)的第一個(gè)參數(shù)蛔六。

新版JavaScript對(duì)于回調(diào)的替代形式

在新版JavaScript當(dāng)中,我們有了promise對(duì)象废亭。在異步APIs中Promises可以作為異步回調(diào)的一種替代形式国章。promise對(duì)象允許我們分別處理success和error的cases而非在同一處同時(shí)傳遞callback和error兩個(gè)參數(shù),并且promise也允許我們串聯(lián)多重異步調(diào)用而不是進(jìn)行嵌套豆村。

如果readFileAsArray函數(shù)支持promises液兽,我們可以做如下應(yīng)用:

readFileAsArray('./numbers.txt')
  .then(lines => {
    const numbers = lines.map(Number);
    const oddNumbers = numbers.filter(n => n%2 === 1);
    console.log('Odd numbers count:', oddNumbers.length);
  })
  .catch(console.error);

我們沒(méi)有傳遞callback函數(shù),而是調(diào)用了.then函數(shù)作為宿主函數(shù)的的返回值掌动。這個(gè).then函數(shù)通常能讓我們達(dá)到跟利用帶有callback函數(shù)的代碼同樣的效果四啰,而且我們也能夠像之前一樣在其上做處理。為了處理errors粗恢,我們?cè)谀┪蔡砑恿?catch代碼塊柑晒,當(dāng)發(fā)生錯(cuò)誤時(shí)我們利用.catch代碼塊進(jìn)行處理。
由于新Promise對(duì)象的存在眷射,讓宿主函數(shù)在新版JavaScript支持promise接口變得更加容易匙赞。修改如下的readFileAsArray函數(shù)讓其支持promise接口佛掖,以及支持之前已經(jīng)支持的callback接口。

const readFileAsArray = function(file, cb = () => {}) {
  return new Promise((resolve, reject) => {
    fs.readFile(file, function(err, data) {
      if (err) {
        reject(err);
        return cb(err);
      }
      const lines = data.toString().trim().split('\n');
      resolve(lines);
      cb(null, lines);
    });
  });
};

因此我們讓函數(shù)返回一個(gè)Promise對(duì)象涌庭,這個(gè)Promise對(duì)象包含著fs.readFile異步回調(diào)芥被。該promise對(duì)象對(duì)外暴露兩個(gè)參數(shù)(一個(gè)resolve函數(shù)以及一個(gè)reject函數(shù))。
我們總是會(huì)運(yùn)用promise的reject函數(shù)執(zhí)行回調(diào)來(lái)處理error坐榆,同時(shí)也總是利用resolve函數(shù)執(zhí)行回調(diào)來(lái)處理data拴魄。
另外我們?cè)谶@個(gè)例子當(dāng)中需要為回調(diào)參數(shù)設(shè)置一個(gè)默認(rèn)值,因?yàn)檫@段代碼有可能會(huì)被用于promise接口席镀。我們可以使用一種簡(jiǎn)單的空函數(shù)作為默認(rèn)值匹中,如:() => {}。

以async/await方式執(zhí)行promises

當(dāng)需要循環(huán)嵌套異步函數(shù)時(shí)愉昆,添加promise接口會(huì)讓你的代碼更易于維護(hù)职员。回調(diào)則會(huì)讓情況變得復(fù)雜跛溉。
Promises稍稍改善了一些這種狀況焊切,而函數(shù)生成器則帶來(lái)更多的優(yōu)化。即是說(shuō)處理異步代碼更新近的替代方式是使用async函數(shù)芳室,它能讓我們像是以一種同步的方式處理異步的代碼专肪,這回讓代碼更具有可讀性。
我們通過(guò)async/await方式執(zhí)行readFileAsArray函數(shù)堪侯,代碼如下:

async function countOdd () {
  try {
    const lines = await readFileAsArray('./numbers');
    const numbers = lines.map(Number);
    const oddCount = numbers.filter(n => n%2 === 1).length;
    console.log('Odd numbers count:', oddCount);
  } catch(err) {
    console.error(err);
  }
}
countOdd();

我們先創(chuàng)建了一個(gè)異步函數(shù)嚎尤,就是在普通函數(shù)的function之前加上關(guān)鍵字async。在這個(gè)異步函數(shù)當(dāng)中伍宦,我們調(diào)用readFileAsArray函數(shù)(假設(shè)它返回了lines變量)芽死,為了讓這種方式生效,我們使用關(guān)鍵字await次洼。之后关贵,我們繼續(xù)執(zhí)行代碼就如同對(duì)readFileAsArray進(jìn)行同步調(diào)用一樣。

為了讓代碼順利運(yùn)行卖毁,我們執(zhí)行異步函數(shù)揖曾。這讓代碼變得非常簡(jiǎn)潔且易于閱讀。而為了能夠處理errors亥啦,我們需要將異步調(diào)用包裹在try/catch語(yǔ)句中炭剪。

通過(guò)這種async/await特性,我們就不必使用其它特殊的API(例如.then和.catch)翔脱。我們僅僅需要特別標(biāo)記一下函數(shù)就可以使用純粹的JavaScript進(jìn)行編程奴拦。

我們可以在任何支持promise接口的函數(shù)當(dāng)中使用async/await特性,但是我們不能將其用于回調(diào)風(fēng)格的異步函數(shù)之中(例如:setTimeout)届吁。

EventEmitter模塊

EventEmitter是一種支持Node當(dāng)中對(duì)象間通信的模塊粱坤。EventEmitter是Node異步事件驅(qū)動(dòng)架構(gòu)的核心隶糕。很多Node內(nèi)置模塊都是繼承自EventEmitter。
概念很簡(jiǎn)單:emitter對(duì)象發(fā)出已經(jīng)命名好的事件站玄,該事件會(huì)觸發(fā)之前注冊(cè)好的監(jiān)聽(tīng)器。因此濒旦,一個(gè)emitter對(duì)象通常有兩個(gè)主要特性:

  • 發(fā)出命名事件
  • 注冊(cè)和注銷(xiāo)監(jiān)聽(tīng)函數(shù)

為了理解EventEmitter株旷,我們創(chuàng)建一個(gè)繼承自EventEmitter的類(lèi)(class)

class MyEmitter extends EventEmitter {
}

Emitter對(duì)象是我們通過(guò)MyEmitter類(lèi)(繼承自類(lèi)EventEmitter)實(shí)例化生成的。

const myEmitter = new MyEmitter();

在這些emitter對(duì)象生命周期的任何階段尔邓,我們都可以通過(guò)emit函數(shù)發(fā)射我們想要發(fā)射的任何命名事件晾剖。

myEmitter.emit('something-happened');

單次事件的觸發(fā)表明已經(jīng)滿足某些條件。該條件在觸發(fā)對(duì)象中通常是一種狀態(tài)變化梯嗽。

我們可以通過(guò)on方法添加監(jiān)聽(tīng)函數(shù)齿尽,每當(dāng)發(fā)射器對(duì)象觸發(fā)相關(guān)聯(lián)的命名事件時(shí)這些監(jiān)聽(tīng)函數(shù)都會(huì)執(zhí)行。

事件!==異步
我們來(lái)看一個(gè)例子:

const EventEmitter = require('events');

class WithLog extends EventEmitter {
  execute(taskFunc) {
    console.log('Before executing');
    this.emit('begin');
    taskFunc();
    this.emit('end');
    console.log('After executing');
  }
}

const withLog = new WithLog();

withLog.on('begin', () => console.log('About to execute'));
withLog.on('end', () => console.log('Done with execute'));

withLog.execute(() => console.log('*** Executing task ***'));

WithLog類(lèi)是一個(gè)事件發(fā)射器灯节。它定義了一個(gè)函數(shù)執(zhí)行的實(shí)例循头。該執(zhí)行函數(shù)接受一個(gè)參數(shù)(一個(gè)任務(wù)函數(shù))并用日志聲明包裹該執(zhí)行函數(shù)。這些日志聲明會(huì)在函數(shù)執(zhí)行前后觸發(fā)炎疆。

為了查看函數(shù)的執(zhí)行順序卡骂,我們?cè)趦蓚€(gè)命名事件上添加了監(jiān)聽(tīng)器,最后會(huì)執(zhí)行一個(gè)樣本任務(wù)以啟動(dòng)其它函數(shù)形入。
如下是函數(shù)的輸出結(jié)果:

Before executing
About to execute
*** Executing task ***
Done with execute
After executing

對(duì)于這些輸出結(jié)果我希望你注意到它們都是同步執(zhí)行的全跨。這段代碼當(dāng)中沒(méi)有異步操作。

  • 首先輸出了"Before executing"
  • 以begin命名的事件輸出了"About to execute"
  • 實(shí)際執(zhí)行行(hang)之后輸出了"Executing task"
  • 以end命名的事件輸出了"Done with execute"
  • 最后我們得到了"After executing"

如同plain-old回調(diào)一樣亿遂,事件跟代碼是同步還是異步執(zhí)行沒(méi)有什么關(guān)聯(lián)浓若。
這點(diǎn)很重要,因?yàn)槿绻覀儌魅氘惒絫askFunc函數(shù)去執(zhí)行的話蛇数,事件的觸發(fā)將會(huì)變得不夠精準(zhǔn)挪钓。
我們可以用帶有setImmediate的調(diào)用來(lái)模擬上面的例子:

// ...

withLog.execute(() => {
  setImmediate(() => {
    console.log('*** Executing task ***')
  });
});

輸出就會(huì)變成像下面這樣:

Before executing
About to execute
Done with execute
After executing
*** Executing task ***

這是錯(cuò)誤的。異步調(diào)用(其調(diào)用了"Done with execute"和"After executing")之后的那幾行已經(jīng)不再準(zhǔn)確苞慢。

為了在一個(gè)異步函數(shù)執(zhí)行完成之后觸發(fā)事件诵原,我們需要結(jié)合基于事件通信的回調(diào)機(jī)制。下面的例子會(huì)進(jìn)行說(shuō)明挽放。

使用事件而非回調(diào)的好處在于我們可以通過(guò)定義多個(gè)監(jiān)聽(tīng)器對(duì)同一信號(hào)響應(yīng)多次绍赛。用回調(diào)實(shí)現(xiàn)相同功能的話,我們需要在單個(gè)回調(diào)當(dāng)中書(shū)寫(xiě)更多的邏輯辑畦。事件是一種很好的實(shí)現(xiàn)方式吗蚌,它讓?xiě)?yīng)用程序能夠通過(guò)多個(gè)外部插件在其核心之上構(gòu)建功能。你可以將它們當(dāng)做hook points纯出,它們會(huì)為狀態(tài)變化作特定的記錄蚯妇。

異步事件
我們現(xiàn)在將這個(gè)同步的簡(jiǎn)單例子改寫(xiě)成更加實(shí)用的異步代碼敷燎。

const fs = require('fs');
const EventEmitter = require('events');

class WithTime extends EventEmitter {
  execute(asyncFunc, ...args) {
    this.emit('begin');
    console.time('execute');
    asyncFunc(...args, (err, data) => {
      if (err) {
        return this.emit('error', err);
      }

      this.emit('data', data);
      console.timeEnd('execute');
      this.emit('end');
    });
  }
}

const withTime = new WithTime();

withTime.on('begin', () => console.log('About to execute'));
withTime.on('end', () => console.log('Done with execute'));

withTime.execute(fs.readFile, __filename);

WithTime類(lèi)執(zhí)行了asyncFunc并通過(guò)使用console.time以及console.timeEnd記錄了asyncFunc的執(zhí)行時(shí)間。在程序執(zhí)行前后箩言,它觸發(fā)了事件執(zhí)行的正確順序硬贯。同時(shí)利用error/data事件去處理異步調(diào)用當(dāng)中的常見(jiàn)信號(hào)。

我們傳入fs.readFile函數(shù)(它是異步函數(shù))來(lái)測(cè)試withTime發(fā)射器陨收。而非用回調(diào)來(lái)處理文件饭豹,如此我便能夠監(jiān)聽(tīng)數(shù)據(jù)對(duì)象了。

當(dāng)執(zhí)行這段代碼時(shí)务漩,我們得到事件的正確執(zhí)行順序拄衰。不出所料,我們獲得了指定代碼的執(zhí)行時(shí)間饵骨,這很有用處:

About to execute
execute: 4.507ms
Done with execute

我們?cè)鯓咏Y(jié)合帶有事件監(jiān)聽(tīng)器的回調(diào)來(lái)實(shí)現(xiàn)呢翘悉?如果asynFunc函數(shù)也支持promises的話,我們可以使用async/await特性來(lái)實(shí)現(xiàn)相同的功能:

class WithTime extends EventEmitter {
  async execute(asyncFunc, ...args) {
    this.emit('begin');
    try {
      console.time('execute');
      const data = await asyncFunc(...args);
      this.emit('data', data);
      console.timeEnd('execute');
      this.emit('end');
    } catch(err) {
      this.emit('error', err);
    }
  }
}

我不太清楚你的情況居触,但是相較于基于回調(diào)和帶有.then/.catch的代碼段我覺(jué)得以上代碼更容易理解妖混。async/await特性讓我們更接近于JavaScript語(yǔ)言本身,我認(rèn)為這是一個(gè)重大進(jìn)展饼煞。
事件參數(shù)和錯(cuò)誤
在前面的例子當(dāng)中源葫,有兩個(gè)事件都是由額外的參數(shù)來(lái)觸發(fā)。
error事件是通過(guò)一個(gè)error對(duì)象觸發(fā)砖瞧。

this.emit('error', err);

data事件則是由一個(gè)data對(duì)象觸發(fā)息堂。

this.emit('data', data);

我們可以根據(jù)需要在命名事件之后使用任意數(shù)量的參數(shù),所有參數(shù)在我們?yōu)檫@些命名事件注冊(cè)的監(jiān)聽(tīng)函數(shù)當(dāng)中都可以使用块促。
例如荣堰,為了處理data事件,我們注冊(cè)的監(jiān)聽(tīng)事件將能夠獲得我們傳遞給被發(fā)射事件的數(shù)據(jù)參數(shù)竭翠,而其就是asyncFunc函數(shù)暴露的數(shù)據(jù)對(duì)象振坚。

withTime.on('data', (data) => {
  // do something with data
});

error事件通常是一種特殊情況。在基于回調(diào)的例子當(dāng)中斋扰,如果我們不使用監(jiān)聽(tīng)器處理error事件的話渡八,node進(jìn)程實(shí)際上會(huì)退出。
為了說(shuō)明這種情況传货,我們用一個(gè)惡性參數(shù)對(duì)執(zhí)行方法做再一次調(diào)用:

class WithTime extends EventEmitter {
  execute(asyncFunc, ...args) {
    console.time('execute');
    asyncFunc(...args, (err, data) => {
      if (err) {
        return this.emit('error', err); // Not Handled
      }

      console.timeEnd('execute');
    });
  }
}

const withTime = new WithTime();

withTime.execute(fs.readFile, ''); // BAD CALL
withTime.execute(fs.readFile, __filename);

如上代碼的第一次執(zhí)行會(huì)引起錯(cuò)誤屎鳍。node進(jìn)程將會(huì)崩潰并退出:

events.js:163
      throw er; // Unhandled 'error' event
      ^
Error: ENOENT: no such file or directory, open ''

第二次執(zhí)行會(huì)受到崩潰影響根本不會(huì)繼續(xù)往下執(zhí)行。
如果為該特殊error事件注冊(cè)監(jiān)聽(tīng)器的話问裕,node進(jìn)程的行為將會(huì)發(fā)生變化逮壁。例如:

withTime.on('error', (err) => {
  // do something with err, for example log it somewhere
  console.log(err)
});

如果執(zhí)行以上代碼,第一次執(zhí)行的錯(cuò)誤會(huì)被播報(bào)粮宛,但是node進(jìn)程不會(huì)崩潰和退出窥淆。其它的執(zhí)行調(diào)用會(huì)正常結(jié)束:

{ Error: ENOENT: no such file or directory, open '' errno: -2, code: 'ENOENT', syscall: 'open', path: '' }
execute: 4.276ms

現(xiàn)在基于promise的函數(shù)會(huì)有不同的行為并且只是輸出警告卖宠,但是最終會(huì)發(fā)生變化:

UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: ENOENT: no such file or directory, open ''
DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

處理已觸發(fā)錯(cuò)誤當(dāng)中異常的另一種方式是為全局uncaughtException進(jìn)程事件注冊(cè)一個(gè)監(jiān)聽(tīng)器。然而忧饭,全部捕捉errors是一種糟糕的想法扛伍。
對(duì)uncaughtException的通常建議是避免使用它,但是如果非用不可的話(播報(bào)發(fā)生的情況或者直接清除)眷昆,你應(yīng)該直接讓進(jìn)程退出蜒秤。

process.on('uncaughtException', (err) => {
  // something went unhandled.
  // Do any cleanup and exit anyway!

  console.error(err); // don't do just that.

  // FORCE exit the process too.
  process.exit(1);
});

然而,想象一下多個(gè)error事件在同一時(shí)間發(fā)生亚斋。這意味著上面的uncaughtException監(jiān)聽(tīng)器會(huì)啟動(dòng)多次,對(duì)于cleanup代碼這會(huì)是個(gè)問(wèn)題攘滩。一個(gè)典型的例子就是有多次調(diào)用用于數(shù)據(jù)庫(kù)關(guān)閉操作帅刊。
EventEmitter模塊對(duì)外暴露一個(gè)一次性的方法。該方法只會(huì)啟動(dòng)一次監(jiān)聽(tīng)器漂问,不會(huì)每次都進(jìn)行響應(yīng)赖瞒。因此,這是一個(gè)使用uncaughtException的實(shí)際用例蚤假,因?yàn)橥ㄟ^(guò)第一個(gè)未捕捉的異常我們將會(huì)進(jìn)行清理操作栏饮,而且無(wú)論如何我們都將退出進(jìn)程。
監(jiān)聽(tīng)器的順序
如果我們?yōu)橥皇录?cè)了多個(gè)監(jiān)聽(tīng)器磷仰,這些監(jiān)聽(tīng)器會(huì)按照順序執(zhí)行袍嬉。注冊(cè)的第一個(gè)監(jiān)聽(tīng)器將會(huì)第一個(gè)執(zhí)行。

// ?????
withTime.on('data', (data) => {
  console.log(`Length: ${data.length}`);
});

// ?????
withTime.on('data', (data) => {
  console.log(`Characters: ${data.toString().length}`);
});

withTime.execute(fs.readFile, __filename);

上方代碼中帶有"Length"的那一行會(huì)先于帶有"Characters"的那一行執(zhí)行灶平,因?yàn)檫@是我們定義監(jiān)聽(tīng)器的順序伺通。
如果你需要定義一個(gè)新監(jiān)聽(tīng)器但是需要該監(jiān)聽(tīng)器第一個(gè)執(zhí)行,你可以使用prependListener方法:

// ?????
withTime.on('data', (data) => {
  console.log(`Length: ${data.length}`);
});

// ?????
withTime.prependListener('data', (data) => {
  console.log(`Characters: ${data.toString().length}`);
});

withTime.execute(fs.readFile, __filename);

上方代碼會(huì)先執(zhí)行帶有"Character"的那一行逢享。
最后罐监,如果需要移除一個(gè)監(jiān)聽(tīng)器,你可以使用removeListener方法瞒爬。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末弓柱,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子侧但,更是在濱河造成了極大的恐慌矢空,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,946評(píng)論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件俊犯,死亡現(xiàn)場(chǎng)離奇詭異妇多,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)燕侠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,336評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)者祖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)立莉,“玉大人,你說(shuō)我怎么就攤上這事七问◎殉埽” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 169,716評(píng)論 0 364
  • 文/不壞的土叔 我叫張陵械巡,是天一觀的道長(zhǎng)刹淌。 經(jīng)常有香客問(wèn)我,道長(zhǎng)讥耗,這世上最難降的妖魔是什么有勾? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 60,222評(píng)論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮古程,結(jié)果婚禮上蔼卡,老公的妹妹穿的比我還像新娘。我一直安慰自己挣磨,他們只是感情好雇逞,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,223評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著茁裙,像睡著了一般塘砸。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上晤锥,一...
    開(kāi)封第一講書(shū)人閱讀 52,807評(píng)論 1 314
  • 那天掉蔬,我揣著相機(jī)與錄音,去河邊找鬼查近。 笑死眉踱,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的霜威。 我是一名探鬼主播谈喳,決...
    沈念sama閱讀 41,235評(píng)論 3 424
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼戈泼!你這毒婦竟也來(lái)了婿禽?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 40,189評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤大猛,失蹤者是張志新(化名)和其女友劉穎扭倾,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體挽绩,經(jīng)...
    沈念sama閱讀 46,712評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡膛壹,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,775評(píng)論 3 343
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片模聋。...
    茶點(diǎn)故事閱讀 40,926評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡肩民,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出链方,到底是詐尸還是另有隱情持痰,我是刑警寧澤,帶...
    沈念sama閱讀 36,580評(píng)論 5 351
  • 正文 年R本政府宣布祟蚀,位于F島的核電站工窍,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏前酿。R本人自食惡果不足惜患雏,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,259評(píng)論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望罢维。 院中可真熱鬧纵苛,春花似錦、人聲如沸言津。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,750評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)悬槽。三九已至,卻和暖如春瞬浓,著一層夾襖步出監(jiān)牢的瞬間初婆,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,867評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工猿棉, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留磅叛,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,368評(píng)論 3 379
  • 正文 我出身青樓萨赁,卻偏偏與公主長(zhǎng)得像弊琴,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子杖爽,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,930評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容

  • Module definition patterns 除了作為加載依賴(lài)的機(jī)制之外敲董,模塊系統(tǒng)也是一種用于定義AP...
    宮若石閱讀 471評(píng)論 0 0
  • 你不知道JS:異步 第三章:Promises 在第二章,我們指出了采用回調(diào)來(lái)表達(dá)異步和管理并發(fā)時(shí)的兩種主要不足:缺...
    purple_force閱讀 2,072評(píng)論 0 4
  • # 模塊機(jī)制 node采用模塊化結(jié)構(gòu)慰安,按照CommonJS規(guī)范定義和使用模塊腋寨,模塊與文件是一一對(duì)應(yīng)關(guān)系,即加載一個(gè)...
    RichRand閱讀 2,515評(píng)論 0 3
  • 官方中文版原文鏈接 感謝社區(qū)中各位的大力支持化焕,譯者再次奉上一點(diǎn)點(diǎn)福利:阿里云產(chǎn)品券萄窜,享受所有官網(wǎng)優(yōu)惠,并抽取幸運(yùn)大...
    HetfieldJoe閱讀 11,028評(píng)論 26 95
  • 你不知道JS:異步 第三章:Promises 接上篇3-1 錯(cuò)誤處理(Error Handling) 在異步編程中...
    purple_force閱讀 1,401評(píng)論 0 2