innerHTML误区

添加元素

innerHTML属性是个经常用到的原生属性.将html元素用字符串形式设置到父元素中,经常用于将动态拼接的html文本,显示在父容器里.

拼html字符串的办法已经被认为不妥,正经的办法是使用appendChild()或者createDocumentFragment().不过对于少量的html,当然也能用.

以前用jQuery库,对应的方法是$('#id').html(),在使用原生属性时,发现与jquery行为有所不同.

这可能是原生innerHTML属性在执行时,有判断过程.会判断父元素是否可以接受html文本.而jQuery已经处理过这个问题,它的html()方法分析过html字符串.

以下测试在谷歌浏览器80+版本,x64.

将div的子元素换成a标记

    <div id = "div1"></div>

    let div = document.getElementById('div1');
    div.innerHTML = '<a>link</a>';

    // 结果
    <div class="btn" id="div1"><a>link</a></div>

 

这是预期的结果,div里可以放a.如果换成tr就不能成功.

    div.innerHTML = '<tr><td>data</td></tr>';
    
    // 结果 没有加进去
    <div id = "div1"></div>

 

tr是表格table里的子元素,如果div换成table呢

    <table id="tab1"></table>

    let tab = document.getElementById('tab1');
    tab.innerHTML = '<tr><td>data</td></tr>';

    // 结果
    <table id="tab1"><tbody><tr><td>data</td></tr></tbody></table>

 

成功了.不过不太一样,table里多了一个tbody元素,而innerHTML时并没有这个tbody.这是原生innerHTML属性自己的行为.

可见,innerHTML也会分析设置的html字符串,如果发现"不合法"的,会拒绝加入.

使用html对象而不是字符串,可以实现预期效果

    let tr = document.createElement('tr');
    tr.innerHTML = '<td>data</td>';
    tab.appendChild(tr);

    // 结果
    <table id="tab1"><tr><td>data</td></tr></table>

 

建立tr元素对象,使用appendChild(tr)加到table,结果里没有tboby元素.

在项目中还是减少使用innerHTML,使用正规的appendChild()和createDocumentFragment()

执行js

当innerHTML一段html里包含js时,是不会执行的.最常见的情况是,从服务器ajax一段包含html,js的页面片段,使用innerHTML设置到容器div,结果js不执行.

如果使用jQuery的$('#id').html() 方法,不会有这个问题.js执行了.显然,jQuery的html()方法是做了工作的.查看源码时,发现jQuery解析了html字符串.

innerHTML里的js不执行,可能和浏览器的机制有关.innerHTML里的js可能被当成一般的文本了,所以不执行.

对于一段html,js混合的文本,尝试过以下方法可以让js执行.

一. 使用createContextualFragment()方法
    let htmlString='<div>let range = document.createRange();</div><script src="abc.js"><\/script><script>console.log("hello world")<\/script>';
    
    // 低版本浏览器不支持这个方法
    let range = document.createRange();
    let fragment = range.createContextualFragment(htmlString);

    document.body.appendChild(fragment);

这个方法解析htmlString,然后返回DocumentFragment对象,加到文档后,js会执行.

但是有一个问题,执行时没有按script标记的顺序.先执行了第二个script,后执行的abc.js

二. 解析法

使用js生成新的script标记,再添加到文档,是可以执行的.这个办法是分析htmlString,将script找出来,再重新生成一次.

为了让js顺序执行,可以在解析时将外联的js下载,变成内联的.具体做法,递归解析htmlString.

这段代码解析html字符串,返回一个文档片段对象,里面的js标记是重新生成的,对于外联会下载成内联,顺序执行.

代码有局限.html字符串中的script标记只能是第一子节点,不能包含在其它dom元素内,因为递归方法只遍历了子节点,没有遍历后代节点.

只能满足相对简单的script标记顺序执行,对于有复杂依赖的js,也不能保证顺序执行.

    // 解析html, val:html字符串 ,onReady:解析完成后的文档片段对象
    function parseHtml = (val, onReady) => {
      let framgSource;
      if (typeof val === 'string') {
          let range = document.createRange();
          framgSource = range.createContextualFragment(val);
      } else if (val instanceof DocumentFragment) {
          framgSource = val;
      } else if (val.length) {
          framgSource = document.createDocumentFragment();
          framgSource.append(...val);
      } else {
          framgSource = document.createDocumentFragment();
          framgSource.append(val);
      }
      // 放入fragment.(解析放入)
      let fragment = document.createDocumentFragment();
      _parseHtmlNodeLoad(fragment, framgSource, onReady);
    };

    // 递归
    function _parseHtmlNodeLoad = (toFragm, fromFragm, onReady) => {
        if (fromFragm.firstChild === null) {
            onReady(toFragm);
            return;
        }
        // script元素.设置到innerhtml时不会执行,要新建一个script对象,再添加
        if (fromFragm.firstChild.nodeName === 'SCRIPT') {
            let newScript = document.createElement('script');
            let src = fromFragm.firstChild.src;
            if (src) {
                // 外联的script,要加载下来,否则有执行顺序问题.外联的没有加载完,内联的就执行了.如果内联js依赖外联则出错.
                // 这个办法是获取js脚本,是设置到生成的script标签中.(变成内联的了)
                fetch(src).then(res => res.text())
                    .then((js) => {
                        newScript.innerHTML = js;
                        toFragm.append(newScript);
                        fromFragm.removeChild(fromFragm.firstChild);
                        _parseHtmlNodeLoad(toFragm, fromFragm, onReady);
                    });
            } else {
                // 内联的直接设置innerHtml
                newScript.innerHTML = fromFragm.firstChild.innerHTML;
                toFragm.append(newScript);
                fromFragm.removeChild(fromFragm.firstChild);
                _parseHtmlNodeLoad(toFragm, fromFragm, onReady);
            }
        } else {
            // 其它元素
            toFragm.append(fromFragm.firstChild);
            _parseHtmlNodeLoad(toFragm, fromFragm, onReady);
        }
    };

缓存文档片段

有时需要将文档中的一部分dom缓存起来,需要时再加入文档中.

由于innerHTML是字符串,所以一些dom的属性不会保存.比如select元素,在缓存前选择了"two",使用innerHTML缓存在还原时,选择会变成默认的"one".

使用node.cloneNode(true)复制节点方法也不行,也保存不了select元素的选中状态.

可以使用DocumentFragment对象的append()方法,添加这个div后,div会脱离文档,缓存到文档片段对象中.在放入文档中,它的状态不变.

这个办法没有"加工"要缓存的元素,只是将它移动了位置.从文档对象移动到文档片段对象.

    // select 选择了two
    <select>
      <option value="1">one</option>
      <option value="2">two</option>
      <option value="3">three</option>
    </select>

    // 使用innerHTML,将div的所有子元素存到变量中
    let dom = div.innerHTML;
    div.innerHTML = '';

    // 还原 (选择状态会丢失)
    div.innerHTML = dom;

    // cloneNode(true) (复制select节点,选择状态也会丢失)
    dom = div.cloneNode(true);
    div.append(dom);

    // 使用DocumentFragment对象的append()添加到文档片段对象,再放入文档中,状态不变.
    dom = document.createDocumentFragment();
    dom.append(div);
    div.append(dom);

posted @ 2020-03-15 13:48  mirrorspace  阅读(549)  评论(0编辑  收藏  举报