最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

一文詳解如何檢測(cè)并解決JS代碼中的死循環(huán)

 更新時(shí)間:2023年09月11日 08:24:49   作者:francecil  
這篇文章主要想和大家來(lái)一起探討一下能否通過(guò)靜態(tài)分析的方式檢測(cè)出死循環(huán),如果不能,我們又應(yīng)該如何在不借用其他線程的情況下,解決死循環(huán)卡住問(wèn)題,感興趣的可以了解下

背景

之前做的一個(gè)需求,需要探測(cè)用戶 js 代碼是否存在死循環(huán)。若發(fā)現(xiàn)死循環(huán),則提前拋錯(cuò),而不是繼續(xù)執(zhí)行直至線程卡死。

業(yè)界也有挺多類似的需求,比如 CodeSandbox 沙盒的 Infinite Loop Protection,可以避免用戶在調(diào)試代碼時(shí)寫了死循環(huán)導(dǎo)致頁(yè)面標(biāo)簽崩潰。

能否通過(guò)靜態(tài)分析的方式檢測(cè)出死循環(huán)?如果不能,我們又應(yīng)該如何在不借用其他線程的情況下,解決死循環(huán)卡住問(wèn)題?

下面就讓我們一起來(lái)分析下這些問(wèn)題吧。

死循環(huán) Case

什么情況下會(huì)導(dǎo)致死循環(huán)?列舉了常見(jiàn)的幾種情況:

  • 無(wú)限循環(huán):循環(huán)條件始終為正 ,且循環(huán)體中沒(méi)有中斷語(yǔ)句
  • 無(wú)限遞歸調(diào)用
  • 無(wú)限渲染:表現(xiàn)在 React 等視圖框架,渲染函數(shù)執(zhí)行時(shí)又觸發(fā)了數(shù)據(jù)變動(dòng)
  • ...

無(wú)限循環(huán)

while (true) { // 循環(huán)條件也可能是一個(gè)很復(fù)雜、有外部入?yún)⒌呐袛嗾Z(yǔ)句,但始終為正
  // 死循環(huán)
  if(1 !== 2) { // 中止條件永不觸發(fā)
      return
  }
}

這類場(chǎng)景,循環(huán)條件始終為正,而在循環(huán)體中,要么沒(méi)有中止條件,要么中止條件永遠(yuǎn)不觸發(fā),進(jìn)而導(dǎo)致線程卡死。

無(wú)限遞歸調(diào)用

(function recursive() {
  recursive(); // 死循環(huán)
})();

對(duì)于這類情況,執(zhí)行引擎在達(dá)到最大遞歸調(diào)用棧深度后,便會(huì)拋出 RangeError ,我們無(wú)需主動(dòng)處理。

RangeError: Maximum call stack size exceeded

無(wú)限渲染

這里以 React 框架為例,在 render 函數(shù)中又觸發(fā)了數(shù)據(jù)的變更。這邊的用例比較直白,現(xiàn)實(shí)中的用例可能會(huì)非常隱蔽。

import React from "react";
export default class App extends React.Component {
  constructor() {
    super();
    this.state = {
      num: 1
    };
  }
  render() {
    this.setState((state) => ({ state: state + 1 }));
    return <div>{this.state.num}</div>;
  }
}
import React, { useState, useEffect } from "react";

export default function App() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    setCount(count + 1); // infinite loop
  }, [count]);
  return <div>hello</div>;
}

第二個(gè)用例,控制臺(tái)輸出了以下報(bào)錯(cuò),并且渲染卡死。

檢測(cè)死循環(huán)

能否通過(guò)靜態(tài)分析的方式,檢測(cè)出一段代碼存在死循環(huán)?

先考慮第一種 「無(wú)限循環(huán)」 場(chǎng)景,如果我們發(fā)現(xiàn)循環(huán)條件執(zhí)行結(jié)果始終為 true ,且循環(huán)體中沒(méi)有中止語(yǔ)句(throw/return/break),那么這類用例必定是死循環(huán)。

while(true) {
    // 死循環(huán)
}

然而這樣的代碼畢竟是少數(shù),大部分用例是在不經(jīng)意間寫出死循環(huán)的,比如

while (x > y && (x % 2 === 0 || y % 2 === 1)) {
  // 死循環(huán),復(fù)雜條件難以分析
}

判斷復(fù)雜、涉及外部輸出,需要運(yùn)行時(shí)分析,故純靜態(tài)分析難以判斷

該問(wèn)題在可計(jì)算性領(lǐng)域被稱為停機(jī)問(wèn)題,已被證明無(wú)法通過(guò)一個(gè)通用算法分析出一段代碼是否存在死循環(huán)

運(yùn)行時(shí)判斷

既然靜態(tài)分析無(wú)法解決,那么是否換個(gè)思路:給循環(huán)體加點(diǎn)判斷代碼,當(dāng)循環(huán)次數(shù)過(guò)多或者循環(huán)執(zhí)行過(guò)久的時(shí)候,就認(rèn)為是死循環(huán),并拋出異常。

我們先以執(zhí)行過(guò)久作為死循環(huán)判斷條件 (后面會(huì)繼續(xù)優(yōu)化)

對(duì)于無(wú)限循環(huán)的場(chǎng)景,可以這么處理:

while(true) {
    // 死循環(huán)
}
// 調(diào)整為
let _loopStart = Date.now() 
while(true) {
    if(Date.now() - _loopStart > MAX_TIMEOUT) {
        throw new RangeError('Potential infinite loop: exceeded')
    }
    // 死循環(huán)
}

for 循環(huán)、do...while 循環(huán)同理轉(zhuǎn)換。

對(duì)于循環(huán)的場(chǎng)景,可以這么處理:

import React from "react";
let _loopStart = Date.now() 
export default class App extends React.Component {
  constructor() {
    super();
    this.state = {
      num: 1
    };
  }
  render() {
    if(Date.now() - _loopStart > MAX_TIMEOUT) {
        console.warn('Potential infinite loop: exceeded')
        return;
    }
    this.setState((state) => ({ state: state + 1 }));
    return <div>{this.state.num}</div>;
  }
}

現(xiàn)在,我們就擁有了中止無(wú)限循環(huán)代碼的能力。

至于代碼是如何插入的,下一節(jié)會(huì)給出 babel 插件代碼。

現(xiàn)在的問(wèn)題是,使用執(zhí)行時(shí)長(zhǎng)作為判斷條件,是否合理?上面的第二個(gè)用例「無(wú)限渲染」很明顯就不正確,另外涉及異步場(chǎng)景,也依然有問(wèn)題。

for(let i=0;i<10;i++){
    await fetch('/xxx')
}

用頻率代替時(shí)長(zhǎng)

我們可以換個(gè)思路,統(tǒng)計(jì)兩次循環(huán)之間的間隔。若足夠小,說(shuō)明是同步代碼死循環(huán);若足夠大,說(shuō)明是異步循環(huán)調(diào)用,可以不用考慮。

關(guān)于足夠小,我們可以粗淺的以 4ms 作為界限。通常來(lái)說(shuō), 1ms 能夠執(zhí)行數(shù)百次指令,只要循環(huán)體中的代碼不是非常復(fù)雜,通常都能夠在 4ms 內(nèi)返回。再加入最大執(zhí)行次數(shù)進(jìn)行綜合判斷

while(true) {
    // 死循環(huán)
}
// 調(diào)整為
const MAX_ITERATIONS = 2000 // 最大可循環(huán)次數(shù)
const MAX_INTERVAL = 4 // 最大執(zhí)行間隔
let lastDate = Date.now() 
let loopCount = 0
while(true) {
    loopCount++
    if(Date.now() - lastDate <= MAX_INTERVAL && loopCount % MAX_ITERATIONS === 0) {
        throw new RangeError('Potential infinite loop: exceeded')
    } else {
        lastDate = Date.now()
    }
    // 死循環(huán)
}

Babel 處理

根據(jù)上面的分析,我們可以使用 babel 寫一個(gè)插件快速驗(yàn)證

關(guān)于 babel 插件的知識(shí),可以查看中文官方文檔

const MAX_ITERATIONS = 2000; // 最大迭代次數(shù)
const MAX_INTERVAL = 4; // 最大執(zhí)行間隔
module.exports = ({ types: t, template }) => {
  // 生成循環(huán)體判斷條件
  const buildGuard = template(`
    %%iterator%%++
    if (%%iterator%% % %%maxIterations%% === 0 && Date.now() - %%lastDate%% <= %%maxInterval%%) {
      throw new RangeError('Potential infinite loop: exceeded ');
    } else {
        %%lastDate%% = Date.now()
    }
  `);
  return {
    visitor: {
      "WhileStatement|ForStatement|DoWhileStatement": (path) => {
        // 新增變量:執(zhí)行次數(shù)
        const iterator = path.scope.parent.generateUidIdentifier("loopIt");
        const iteratorInit = t.numericLiteral(0);
        path.scope.parent.push({
          id: iterator,
          init: iteratorInit,
        });
        // 新增變量:上次執(zhí)行時(shí)間
        const lastDate = path.scope.parent.generateUidIdentifier("lastDate");
        const lastDateInit = t.callExpression(
          t.memberExpression(t.identifier("Date"), t.identifier("now")),
          []
        );
        path.scope.parent.push({
          id: lastDate,
          init: lastDateInit,
        });
        // 插入循環(huán)體
        const guard = buildGuard({
          iterator,
          maxIterations: t.numericLiteral(MAX_ITERATIONS),
          lastDate,
          maxInterval: t.numericLiteral(MAX_INTERVAL) 
        });
        // 處理 No block statement 的情況,比如 `while (1) 1;`
        if (!path.get("body").isBlockStatement()) {
          const statement = path.get("body").node;
          path.get("body").replaceWith(t.blockStatement([guard, statement]));
        } else {
          path.get("body").unshiftContainer("body", guard);
        }
      },
      // 類組件函數(shù),略
      ClassDeclaration: (path, file) => {},
      // 箭頭函數(shù)組件,略
      VariableDeclaration: (path, file) => {
        // 判斷是否為 JSX 函數(shù),可以通過(guò) ReturnStatement 是否為 JSXFragment/JSXElement 進(jìn)行判斷
      },
      // 普通函數(shù)組件,略
      FunctionDeclaration: (path, file) => {},
    },
  };
};

測(cè)試一下

const babel = require("@babel/core");
// 測(cè)試插件
const code = `
while(true){
    for(;;) {
    }
}
`;
const result = babel.transformSync(code, {
  plugins: [require("./plugin")],
  // presets: ["@babel/preset-env"],
});
console.log(result.code);

得到如下輸出

"use strict";
var _loopIt = 0,
  _lastDate = Date.now();
while (true) {
  var _loopIt2 = 0,
    _lastDate2 = Date.now();
  _loopIt++;
  if (_loopIt % 2000 === 0 && Date.now() - _lastDate <= 4) {
    throw new RangeError('Potential infinite loop: exceeded ');
  } else {
    _lastDate = Date.now();
  }
  for (;;) {
    _loopIt2++;
    if (_loopIt2 % 2000 === 0 && Date.now() - _lastDate2 <= 4) {
      throw new RangeError('Potential infinite loop: exceeded ');
    } else {
      _lastDate2 = Date.now();
    }
  }
}

正好滿足我們的需求。

最后

需要再次聲明的是,本文提供的方案僅處理了常見(jiàn)了無(wú)限循環(huán)用例。

在實(shí)際項(xiàng)目中,用戶可以通過(guò) eval 、 new Function 等各種方案脫離這個(gè)檢測(cè)機(jī)制,難以完全避免。

此時(shí)可能需要想的是,用戶都這么寫了,那我們還需要為他考慮么?

到此這篇關(guān)于一文詳解如何檢測(cè)并解決JS代碼中的死循環(huán)的文章就介紹到這了,更多相關(guān)JS死循環(huán)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • JavaScript DOM常用操作代碼匯總

    JavaScript DOM常用操作代碼匯總

    這篇文章主要介紹了JavaScript DOM常用操作代碼匯總,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2020-07-07
  • TypeScript前端高頻面試題總結(jié)大全(2026最新版)

    TypeScript前端高頻面試題總結(jié)大全(2026最新版)

    TypeScript作為JavaScript的超集,已經(jīng)成為前端開(kāi)發(fā)中不可或缺的技術(shù),這篇文章主要介紹了TypeScript前端高頻面試題總結(jié)大全的相關(guān)資料,文中通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下
    2026-05-05
  • 強(qiáng)悍無(wú)比的WEB開(kāi)發(fā)好助手FireBug(Firefox Plugin)

    強(qiáng)悍無(wú)比的WEB開(kāi)發(fā)好助手FireBug(Firefox Plugin)

    強(qiáng)悍無(wú)比的WEB開(kāi)發(fā)好助手FireBug(Firefox Plugin)...
    2007-01-01
  • JS實(shí)現(xiàn)圖片輪播效果實(shí)例詳解【可自動(dòng)和手動(dòng)】

    JS實(shí)現(xiàn)圖片輪播效果實(shí)例詳解【可自動(dòng)和手動(dòng)】

    這篇文章主要介紹了JS實(shí)現(xiàn)圖片輪播效果,結(jié)合完整實(shí)例形式分析了javascript可自動(dòng)和手動(dòng)輪播圖的原理、布局與輪播功能相關(guān)實(shí)現(xiàn)技巧,需要的朋友可以參考下
    2019-04-04
  • javascript多物體運(yùn)動(dòng)實(shí)現(xiàn)方法分析

    javascript多物體運(yùn)動(dòng)實(shí)現(xiàn)方法分析

    這篇文章主要介紹了javascript多物體運(yùn)動(dòng)實(shí)現(xiàn)方法,結(jié)合實(shí)例形式分析了JavaScript多物體運(yùn)動(dòng)的相關(guān)注意事項(xiàng)與具體實(shí)現(xiàn)代碼,包含四個(gè)div塊的橫向、豎向移動(dòng),顏色與邊框漸變效果,需要的朋友可以參考下
    2016-01-01
  • js計(jì)算字符串長(zhǎng)度包含的中文是utf8格式

    js計(jì)算字符串長(zhǎng)度包含的中文是utf8格式

    使用js寫的計(jì)算字符串長(zhǎng)度且其中中文是utf8格式的,具體的實(shí)現(xiàn)如下,感興趣的朋友可以參考下
    2013-10-10
  • fingerprint.js的安裝及基本使用方法

    fingerprint.js的安裝及基本使用方法

    FingerprintJS是基于JS的瀏覽器指紋庫(kù),通過(guò)采集設(shè)備屬性生成唯一用戶標(biāo)識(shí),這篇文章主要介紹了fingerprint.js的安裝及基本使用方法的相關(guān)資料,文中通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下
    2025-06-06
  • PHP抓取HTTPS內(nèi)容和錯(cuò)誤處理的方法

    PHP抓取HTTPS內(nèi)容和錯(cuò)誤處理的方法

    這篇文章主要介紹了PHP抓取HTTPS內(nèi)容的實(shí)現(xiàn)方法,以及在抓取的時(shí)候遇到的一個(gè)HTTPS問(wèn)題的處理辦法,有需要的朋友們可以參考借鑒,下面來(lái)一起看看吧。
    2016-09-09
  • 深入理解JS addLoadEvent函數(shù)

    深入理解JS addLoadEvent函數(shù)

    下面小編就為大家?guī)?lái)一篇深入理解JS addLoadEvent函數(shù)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2016-05-05
  • js實(shí)現(xiàn)簡(jiǎn)單擲骰子效果

    js實(shí)現(xiàn)簡(jiǎn)單擲骰子效果

    這篇文章主要為大家詳細(xì)介紹了js實(shí)現(xiàn)簡(jiǎn)單擲骰子效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2019-10-10

最新評(píng)論

类乌齐县| 南川市| 渝北区| 通化县| 山阳县| 米泉市| 古田县| 郎溪县| 大兴区| 绵阳市| 双桥区| 石狮市| 琼结县| 太仆寺旗| 任丘市| 凤阳县| 肥西县| 兖州市| 开封市| 柳河县| 勃利县| 松滋市| 宾川县| 遵义市| 德化县| 襄垣县| 兰西县| 天峻县| 汉阴县| 南陵县| 宾川县| 兴和县| 台安县| 中超| 桂平市| 钟山县| 谢通门县| 磴口县| 衡水市| 吐鲁番市| 青铜峡市|