jQuery的$.ready()函数在Vanilla JavaScript中的等价写法 - 如何在页面/DOM准备就绪时调用一个函数

1874

在jQuery中,我们都知道神奇的.ready()函数:

$('document').ready(function(){});

然而,假设我想运行一个没有任何库支持的标准JavaScript函数,并且我想在页面准备好处理它时立即启动该函数。应该如何正确处理?

我知道可以这样做:

window.onload="myFunction()";

或者我可以使用 body 标签:

<body onload="myFunction()">

我甚至可以尝试放在页面底部的任何位置,但是结束的bodyhtml标签之后:

<script type="text/javascript">
    myFunction();
</script>

有没有一种跨浏览器(旧/新)兼容的方法,可以像jQuery的$.ready()一样发出一个或多个函数?


11
看这个:https://dev59.com/A3RA5IYBdhLWcg3w2x6W - Dan A.
10个回答

2605
在没有一个可以为你完成所有跨浏览器兼容性的框架的情况下,最简单的方法就是在body结束时调用你的代码。这比使用onload处理程序更快,因为它只等待DOM准备就绪,而不是等待所有图像加载完成。并且,这适用于每个浏览器。
<!doctype html>
<html>
<head>
</head>
<body>
Your HTML here

<script>
// self executing function here
(function() {
   // your page initialization code here
   // the DOM will be available here

})();
</script>
</body>
</html>

对于现代浏览器(IE9及以上版本,以及任何版本的Chrome、Firefox或Safari),如果你想要实现类似jQuery的$(document).ready()方法,可以在任何位置调用而不必担心调用脚本所在的位置,只需使用以下代码:

function docReady(fn) {
    // see if DOM is already available
    if (document.readyState === "complete" || document.readyState === "interactive") {
        // call on next available tick
        setTimeout(fn, 1);
    } else {
        document.addEventListener("DOMContentLoaded", fn);
    }
}    

使用方法:

docReady(function() {
    // DOM is loaded and ready for manipulation here
});

如果你需要完全的跨浏览器兼容性(包括旧版本的IE),而且你不想等待window.onload,那么你可能应该去看看像jQuery这样的框架是如何实现它的$(document).ready()方法的。根据浏览器的能力,这个过程可能会非常复杂。

以下是jQuery所做的一些事情(无论脚本标签放在哪里都可以工作)。

如果被支持,它尝试使用标准的:

document.addEventListener('DOMContentLoaded', fn, false);

备选项为:

window.addEventListener('load', fn, false )

或者对于较旧版本的IE,它使用:
document.attachEvent("onreadystatechange", fn);

具备回退功能,可以使用以下备选方案:

window.attachEvent("onload", fn);

还有一些在IE代码路径中的解决方法,我不太明白,但看起来与框架有关。


这是一个完整的替代jQuery的.ready()的纯JavaScript编写的方法:

(function(funcName, baseObj) {
    // The public function name defaults to window.docReady
    // but you can pass in your own object and own function name and those will be used
    // if you want to put them in a different namespace
    funcName = funcName || "docReady";
    baseObj = baseObj || window;
    var readyList = [];
    var readyFired = false;
    var readyEventHandlersInstalled = false;

    // call this when the document is ready
    // this function protects itself against being called more than once
    function ready() {
        if (!readyFired) {
            // this must be set to true before we start calling callbacks
            readyFired = true;
            for (var i = 0; i < readyList.length; i++) {
                // if a callback here happens to add new ready handlers,
                // the docReady() function will see that it already fired
                // and will schedule the callback to run right after
                // this event loop finishes so all handlers will still execute
                // in order and no new ones will be added to the readyList
                // while we are processing the list
                readyList[i].fn.call(window, readyList[i].ctx);
            }
            // allow any closures held by these functions to free
            readyList = [];
        }
    }

    function readyStateChange() {
        if ( document.readyState === "complete" ) {
            ready();
        }
    }

    // This is the one public interface
    // docReady(fn, context);
    // the context argument is optional - if present, it will be passed
    // as an argument to the callback
    baseObj[funcName] = function(callback, context) {
        if (typeof callback !== "function") {
            throw new TypeError("callback for docReady(fn) must be a function");
        }
        // if ready has already fired, then just schedule the callback
        // to fire asynchronously, but right away
        if (readyFired) {
            setTimeout(function() {callback(context);}, 1);
            return;
        } else {
            // add the function and context to the list
            readyList.push({fn: callback, ctx: context});
        }
        // if document already ready to go, schedule the ready function to run
        if (document.readyState === "complete") {
            setTimeout(ready, 1);
        } else if (!readyEventHandlersInstalled) {
            // otherwise if we don't have event handlers installed, install them
            if (document.addEventListener) {
                // first choice is DOMContentLoaded event
                document.addEventListener("DOMContentLoaded", ready, false);
                // backup is window load event
                window.addEventListener("load", ready, false);
            } else {
                // must be IE
                document.attachEvent("onreadystatechange", readyStateChange);
                window.attachEvent("onload", ready);
            }
            readyEventHandlersInstalled = true;
        }
    }
})("docReady", window);

代码的最新版本已经在GitHub上公开共享,网址为https://github.com/jfriend00/docReady

用法:

// pass a function reference
docReady(fn);

// use an anonymous function
docReady(function() {
    // code here
});

// pass a function reference and a context
// the context will be passed to the function as the first argument
docReady(fn, context);

// use an anonymous function with a context
docReady(function(context) {
    // code here that can use the context argument that was passed to docReady
}, ctx);

这已经在以下情况下进行了测试:

IE6 and up
Firefox 3.6 and up
Chrome 14 and up
Safari 5.1 and up
Opera 11.6 and up
Multiple iOS devices
Multiple Android devices

实现和测试环境: http://jsfiddle.net/jfriend00/YfD3C/


以下是其工作原理的概述:

  1. 创建一个IIFE(立即调用函数表达式),以便我们可以有非公共状态变量。
  2. 声明一个公共函数docReady(fn, context)
  3. 当调用docReady(fn, context)时,检查准备好的处理程序是否已经触发。如果是这样,则使用setTimeout(fn, 1)调度新添加的回调在此JS线程完成后立即触发。
  4. 如果还没有触发准备好的处理程序,则将此新回调添加到稍后要调用的所有回调列表中。
  5. 检查文档是否已准备就绪。如果是这样,则执行所有准备好的处理程序。
  6. 如果我们还没有安装事件侦听器来知道文档何时准备就绪,则现在安装它们。
  7. 如果document.addEventListener存在,则为"DOMContentLoaded""load"事件使用.addEventListener()安装事件处理程序。 "Load"是安全备份事件,不应该需要。
  8. 如果document.addEventListener不存在,则使用.attachEvent()安装"onreadystatechange""onload"事件的事件处理程序。
  9. onreadystatechange事件中,检查document.readyState === "complete"是否为真,如果是,则调用一个函数来触发所有准备好的处理程序。
  10. 在所有其他事件处理程序中,调用一个函数以触发所有准备好的处理程序。
  11. 在用于调用所有就绪处理程序的函数中,检查状态变量,以查看我们是否已经触发。如果是这样,则不执行任何操作。如果我们尚未被调用,则循环遍历就绪函数数组,并按照添加的顺序调用每个函数。设置标志以指示这些都已经调用,因此它们永远不会再次执行。
  12. 清除函数数组,以便可以释放它们可能正在使用的任何闭包。

使用docReady()注册的处理程序保证按照它们注册的顺序触发。

如果您在文档已经准备好后调用docReady(fn),则将使用setTimeout(fn, 1)安排回调以在当前执行线程完成后立即执行。这允许调用代码始终假定它们是异步回调,稍后将被调用,即使稍后就是JS当前线程完成时,并保留调用顺序。


1
为什么setTimeout(fn, 1)被使用,而不是setTimeout(fn, 0)? - David Long
6
@David - 实际上并不重要,因为浏览器的最小超时时间已经是约4毫秒。总体思路是我们想要向代码读者传达这个 setTimeout() 将在事件循环的未来一个刻度中被触发,而不是立即触发。虽然 setTimeout(fn, 0) 也会在事件循环的未来一个刻度中被触发,但如果我使用非零值来说明它将会在未来发生,而不是立即发生,对于阅读程度较低的用户更易理解。无论如何都不是什么大问题。 - jfriend00
1
谢谢您!我目前正在使用约束布局(Android风格)为Web开发一个布局库,并且需要优化加载时间并消除初始无序闪烁,因为在JS启动之前默认DOM状态。您的答案真的很有帮助。 - gbenroscience

351

如果你正在使用不带jQuery的纯VANILLA JavaScript,那么你必须使用(Internet Explorer 9或更高版本):

document.addEventListener("DOMContentLoaded", function(event) {
    // Your code to run since DOM is loaded and ready
});

上面是等价于 jQuery .ready 的内容:

$(document).ready(function() {
    console.log("Ready!");
});

这也可以用速记方式写成,jQuery将在ready事件发生后运行。

$(function() {
    console.log("ready!");
});

请勿与以下内容混淆(以下内容不意味着DOM已准备好):

请勿使用像这样自执行的IIFE

 Example:

(function() {
   // Your page initialization code here  - WRONG
   // The DOM will be available here   - WRONG
})();

这个IIFE不会等待您的DOM加载完成。(我甚至指的是最新版本的Chrome浏览器!)


play() 失败,因为用户没有先与文档进行交互。 - CS QGB
1
是的,第一个交给我 :D 如果您想将纯脚本添加到SharePoint脚本编辑器中,请使用以下代码.. document.addEventListener("DOMContentLoaded", function(event) - user1409736
为了使播放正常工作,您必须向“video”标签添加“muted”参数。 - Ruben Alves

190

我想在这里提及一些可能的方法,包括一个纯JavaScript技巧,可适用于所有浏览器

// with jQuery 
$(document).ready(function(){ /* ... */ });

// shorter jQuery version 
$(function(){ /* ... */ });

// without jQuery (doesn't work in older IEs)
document.addEventListener('DOMContentLoaded', function(){ 
    // your code goes here
}, false);

// and here's the trick (works everywhere)
function r(f){/in/.test(document.readyState)?setTimeout('r('+f+')',9):f()}
// use like
r(function(){
    alert('DOM Ready!');
});

这里的技巧,就像原作者所解释的那样,是检查document.readyState属性。如果它包含字符串in(如uninitializedloading,前两个DOM准备状态中的一个),我们设置一个超时并再次检查。否则,我们执行传递的函数。
这里是jsFiddle的技巧,适用于所有浏览器
感谢Tutorialzine将此内容纳入他们的书籍中。

56
非常糟糕的方法,使用超时循环和任意的9毫秒间隔,并使用eval。仅检查/in/也没有太多意义。 - Vitim.us

81

已测试在IE9、最新版本的Firefox和Chrome中,并且也支持在IE8中使用。

document.onreadystatechange = function () {
  var state = document.readyState;
  if (state == 'interactive') {
      init();
  } else if (state == 'complete') {
      initOnCompleteLoad();
  }
}​;

示例: http://jsfiddle.net/electricvisions/Jacck/

更新 - 可重复使用版本

我刚开发了以下内容。它是一个相当简单的等效于jQuery或Dom ready,没有向后兼容性。它可能需要进一步改进。在Chrome、Firefox和IE(10/11)的最新版本中进行了测试,并且应该在旧版浏览器中按照评论中的说明工作。如果我发现任何问题,我会进行更新。

window.readyHandlers = [];
window.ready = function ready(handler) {
  window.readyHandlers.push(handler);
  handleState();
};

window.handleState = function handleState () {
  if (['interactive', 'complete'].indexOf(document.readyState) > -1) {
    while(window.readyHandlers.length > 0) {
      (window.readyHandlers.shift())();
    }
  }
};

document.onreadystatechange = window.handleState;

用法:

ready(function () {
  // your code here
});

这段代码是用来处理异步加载JS的,但是除非你进行了压缩,否则你可能希望同步加载此脚本。我在开发中发现这很有用。

现代浏览器还支持异步加载脚本,这进一步增强了用户体验。支持异步加载意味着可以同时下载多个脚本,并仍然呈现页面。只需注意依赖于其他异步加载的脚本时要小心,或者使用一个类似Browserify的压缩工具来处理依赖关系。


25

我不太确定你在问什么,但也许这可以帮助你:

window.onload = function(){
    // Code. . .

}
或者:
window.onload = main;

function main(){
    // Code. . .

}

对我来说,这似乎是正确的答案,而且比其他方案简单得多。 - Julian Knight
虽然使用onload更简单且看起来是正确的,但它可能在DOM准备好之前就被触发,与使用DOMContentLoaded不同;尽管由于帖子的年代久远,我们可以原谅楼主,但上述评论是较新的,应该考虑到这一点。 - forbiddenera

25

HubSpot的好心人提供了一个资源,您可以在其中找到纯Javascript方法来实现许多jQuery功能,包括ready

http://youmightnotneedjquery.com/#ready

function ready(fn) {
  if (document.readyState != 'loading'){
    fn();
  } else if (document.addEventListener) {
    document.addEventListener('DOMContentLoaded', fn);
  } else {
    document.attachEvent('onreadystatechange', function() {
      if (document.readyState != 'loading')
        fn();
    });
  }
}

使用示例:

ready(function() { alert('hello'); });

12

您的方法(将脚本放置在闭合 body 标签之前)

<script>
   myFunction()
</script>
</body>
</html>

使用这种方式可以可靠地支持旧的和新的浏览器。


7

准备好

function ready(fn){var d=document;(d.readyState=='loading')?d.addEventListener('DOMContentLoaded',fn):fn();}

使用方法

ready(function(){
    //some code
});

For self invoking code

(function(fn){var d=document;(d.readyState=='loading')?d.addEventListener('DOMContentLoaded',fn):fn();})(function(){

    //Some Code here
    //DOM is avaliable
    //var h1s = document.querySelector("h1");

});

支持:IE9+


1
这个答案是多余的,它已经在这里提到了:https://dev59.com/PWkw5IYBdhLWcg3wbqAS#30757781 - Ram Patra
1
@RamPatra 不是很对。这个答案更加简洁,写起来也更加舒适。 - I try so hard but I cry harder

4
这是Ram-swaroop的“适用于所有浏览器”版本的已清理、未使用eval的代码,可在所有浏览器中运行!
function onReady(yourMethod) {
  var readyStateCheckInterval = setInterval(function() {
    if (document && document.readyState === 'complete') { // Or 'interactive'
      clearInterval(readyStateCheckInterval);
      yourMethod();
    }
  }, 10);
}
// use like
onReady(function() { alert('hello'); } );

然而,它确实会等待额外的10毫秒才能运行,因此这里有一种更复杂的方式,不应该这样做:

function onReady(yourMethod) {
  if (document.readyState === 'complete') { // Or also compare to 'interactive'
    setTimeout(yourMethod, 1); // Schedule to run immediately
  }
  else {
    readyStateCheckInterval = setInterval(function() {
      if (document.readyState === 'complete') { // Or also compare to 'interactive'
        clearInterval(readyStateCheckInterval);
        yourMethod();
      }
    }, 10);
  }
}

// Use like
onReady(function() { alert('hello'); } );

// Or
onReady(functionName);

另请参阅如何在没有框架的情况下检查DOM是否准备就绪?


3

document.ondomcontentready=function(){}这个看起来可以解决问题,但它并不支持所有浏览器。

似乎你应该只需使用jQuery min。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接