有時候咱們會有把一整段 HTML 動態塞進頁面的需求,例如渲染了一個模板,從服務器端獲取了一段廣告代碼等。通常狀況下咱們使用 container.innerHTML
便可。可是當 HTML 中出現 script
標籤時,直接使用 innerHTML
並不會執行它。javascript
<div id="test">Hello HTML</div>
<script> document.getElementById('test').innerHTML = 'Hello JS'; </script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.4.2/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.4.2/react-dom.min.js"></script>
<script> ReactDOM.render(React.createElement('div', null, 'Hello React'), document.getElementById('test')); </script>複製代碼
一個常見的例子裏包含普通的 HTML 內容,<script>
裏的 inline script,經過 src
引用的外部 script。若是咱們嘗試直接用 innerHTML
賦值只會獲得一個 Hello HTML
。然後面的 <script>
標籤無一例外沒有執行。html
咱們知道經過 appendChild
把 <script>
標籤直接塞進頁面是能夠執行和加載裏面的 js 的(JSONP
就是經過這種方法實現的,參見以前的文章:JSONP 的實現 - 知乎專欄。java
因此其實咱們須要作的就只是把全部的 <script>
找出來,而後經過 appendChild
塞到頁面裏便可。node
function runScript(script){
// 直接 document.head.appendChild(script) 是不會生效的,須要從新建立一個
const newScript = document.createElement('script');
// 獲取 inline script
newScript.innerHTML = script.innerHTML;
// 存在 src 屬性的話
const src = script.getAttribute('src');
if (src) newScript.setAttribute('src', src);
document.head.appendChild(newScript);
document.head.removeChild(newScript);
}
function setHTMLWithScript(container, rawHTML){
container.innerHTML = rawHTML;
const scripts = container.querySelectorAll('script');
for (let script of scripts) {
runScript(script);
}
}複製代碼
當咱們嘗試用上面的 setHTMLWithScript(document.body, html)
時有一個問題,就是 script
的加載和執行並不是同步的,咱們會獲得一個 Hello, JS
。react
而下面的 <script>
依賴前面的 <script>
執行加載完成是一個很是常見的需求,由於在正常的靜態網頁裏就是這樣的,雖然全部的遠程腳本都是異步加載的,但後面的 <script>
會等待前面的加載執行後纔開始執行。jquery
爲了讓異常處理和異步流程的控制更方便,咱們讓 runScript
返回一個 Promise,而後只須要一個簡單的 reduce
就能夠把異步邏輯串聯起來:git
function runScript(script){
return new Promise((reslove, rejected) => {
// 直接 document.head.appendChild(script) 是不會生效的,須要從新建立一個
const newScript = document.createElement('script');
// 獲取 inline script
newScript.innerHTML = script.innerHTML;
// 存在 src 屬性的話
const src = script.getAttribute('src');
if (src) newScript.setAttribute('src', src);
// script 加載完成和錯誤處理
newScript.onload = () => reslove();
newScript.onerror = err => rejected();
document.head.appendChild(newScript);
document.head.removeChild(newScript);
if (!src) {
// 若是是 inline script 執行是同步的
reslove();
}
})
}
function setHTMLWithScript(container, rawHTML){
container.innerHTML = rawHTML;
const scripts = container.querySelectorAll('script');
return Array.prototype.slice.apply(scripts).reduce((chain, script) => {
return chain.then(() => runScript(script));
}, Promise.resolve());
}複製代碼
獲得預期的 Hello React
。github
其實這裏有一點和直接渲染不一致的地方,就是腳本的加載也是同步的,後面的腳本會等待以前的腳本執行完纔會加載,不過從 js 層面彷佛沒有辦法解決這個問題。web
熟悉 JQuery 的同窗可能知道 $.html
其實會直接執行裏面的 <script>
標籤,不過是同步的,在 $.html
的代碼中,能夠看到 jQuery 判斷知足必定條件下直接使用 innerHTML
,隨便執行一個 $('body').html(test<script></script>)
而後打個斷點,ajax
能夠看到這裏作了一個簡單的正則判斷,若是碰到 <script><style><link>
標籤就用 jQuery 本身實現的 append
,繼續追蹤下去,
顯然 jQuery 在這裏徹底沒有考慮 <script>
先後的依賴。對於 inline script 的標籤也是直接經過 eval
實現的而不是新建一個插入到文檔裏。
JQuery 也有幾個 issue 討論是否要按照順序執行,但最後決定保持現狀:Scripts in inner html are not exectuted sequentially in order · Issue #2538 · jquery/jquery。
除了寫進去再用 querySelectorAll
把 script 全都拿出來複製一遍外,IE11 以上的瀏覽器也能夠經過 createContextualFragment
直接把 html 轉換成 DOM 節點而後 append 到頁面上:
var tagString = "<div>I am a div node</div><script>console.log('test')</script>";
var range = document.createRange();
// make the parent of the first div in the document becomes the context node
range.selectNode(document.body);
var documentFragment = range.createContextualFragment(tagString);
undefined
document.body.appendChild(documentFragment)複製代碼
也能夠用這種方法來實現上面的功能。
上面的代碼都只是順手的探索,沒有考慮兼容性方面的問題,例如 IE 不支持 script 的 onload 事件等,可能須要 onreadystatechange
來實現。
DOMContentLoaded
早已經完成,若是有須要,咱們可能要在腳本加載完成後,從新觸發一下
setHTMLWithScript(document.body, rawHTML)
.then(() => {
var DOMContentLoadedEvent = document.createEvent('Event');
DOMContentLoadedEvent.initEvent('DOMContentLoaded', true, true);
document.dispatchEvent(DOMContentLoadedEvent);
})複製代碼
在靜態頁面中,<script>
標籤裏若是出現 document.write
,會直接在 <script>
插入的位置寫入,這種方法常被用於廣告投放腳原本定位本身的位置。
而當咱們在動態插入時文檔已經關閉,會直接 write
到整個頁面上,若是有必要能夠暫時替換 document.write
來實現。
getCurrentScript
是另外一個定位 <script>
標籤所在位置的方法,之因此不太經常使用是由於 IE 不兼容它,若是咱們要考慮兼容這個方法新產生的 <script>
標籤就不該該往 <head>
裏 append,而是插入到原來所在的位置。
以上方法都只是模擬靜態 <script>
解析的過程,通常來講咱們不要求行爲徹底一致(畢竟跨域異步加載同步執行這點 JS 就沒法模擬),可是能夠按照咱們的需求去實現它的行爲。
這種方法也只適用於一部分場景,若是有更復雜的 JS 動態加載需求應該考慮使用 requirejs
等 AMD Loader。