Loading

JavaScriptDom编程艺术——第五六七章

前言

前几天做实训作业时候发现JS功底真的不咋地,尤其是在操作DOM上。

尽管现在前端程序员直接操作DOM的机会不多了,但是不补上,总觉得缺点什么。

开始刷《JavaScript DOM编程艺术》。

平稳退化

平稳退化的意思就是当用户的浏览器不支持JavaScript时,你的网站也能让用户正常访问,而非无法使用。

尽管现在几乎没有不支持JavaScript的浏览器了,也没有什么人会禁用JavaScript,但是平稳退化也是一个成功的前端页面所必要的。

用一个弹出窗口的例子说明。

// HTML
<a href="javascript:popUp('http://cnblogs.com/lilpig')">Pop Window</a>

// JS
function popUp(link){
    window.open(link,"popup","width=320,height=480");
}

打开这个页面可能出现三种情况

  1. 点击Pop Window,弹出一个本博客主页的窗口。
  2. 点击Pop Window,浏览器把href中的内容当成链接在当前窗口打开了。这是因为浏览器不支持JS造成的。
  3. 点击Pop Window,什么也没发生。这可能是用户禁用了浏览器的JS功能。

所以这个例子就不能平稳退化。

如果我们保留正常的href属性作为保底,然后使用onclick监听器,来调用popUp,并通过return false来阻止a标签点击后的默认行为,这样能在用户端不支持JS时平稳退化,保留一个保底的行为,至少它会打开href中的链接。

<a href="http://cnblogs.com/lilpig" onclick="popUp('http://cnblogs.com/lilpig');return false;">Pop Window</a>

更简单的写法如下,使用this

<a href="http://cnblogs.com/lilpig" onclick="popUp(this.href);return false;">Pop Window</a>

向CSS学习

在HTML中编写大量和JS相关的代码始终不太好,这会把HTML文档弄乱,并且利于后期的维护。想想,如果我们当前页面中有几百个上面的popUp的调用,并且类似的调用分散在很多个页面中,这时我觉得popUp这个名字太模糊了,表意不清淅,我想更改它的名字,这时可是一个大工程,即使现代的IDE都提供了批量替换功能,我看到这个需求也会头疼。

所以,HTML就让它专注于文档结构的表达,CSS就让它专注于页面的效果,JS就让它专注于页面的交互,各司其职。

// HTML
<a href="http://cnblogs.com/lilpig" class="popup">CNBLOG</a>
<a href="http://baidu.com" class="popup">BAIDU</a>
<a href="http://bilibili.com" class="popup">BILIBILI</a>

// JS
window.onload = function(){
    var popup_elems = document.getElementsByClassName("popup");
    for(var i = 0; i < popup_elems.length; i++){
        var popup_elem = popup_elems[i];
        console.log(popup_elem.tagName)
        if(popup_elem.tagName.toLowerCase() === 'a'){
            popup_elem.onclick = function(){
                popUp(this.href);
                return false;
            }
        }
    }
}

这样,代码看上去虽然多了,却将不同的文件类型的工作分离了。

关于before和after的思考

CSS中有 before和after两个伪类,用于向文档中插入一些新内容。

从平稳退化的角度考虑,一些关键的、会影响一个操作是否完成的功能,千万不要使用before和after实现,因为这并非所有浏览器都支持。

谁在意?

就像上面说的,现在几乎没有一个浏览器不支持JavaScript也没有人关闭JavaScript功能。那需要因为几个极其特殊并且极少的个例而考虑平稳退化问题吗?

其实,这样更多的原因是保证网站对爬虫友好,大部分爬虫不支持解析JavaScript,如果你想要你的网站在搜索引擎的排名不那么难看,那么最好保证平稳退化。

<a href="http://cnblogs.com/lilpig" onclick="popUp(this.href);return false;">Pop Window</a>

就像上面,爬虫虽不能理解onclick中的内容,但是不耽误它理解href中的内容。

向后兼容

由于前期浏览器厂商的战争历史,JS有很多不好的,不规范的设计。即使现在W3C标准已经让这些问题几乎不复存在,但如果你想向后兼容,还是多写一些代码。(我不想)

就用上面的给a标签添加点击监听弹出窗口来解释,上面的代码并不能在不支持getElementsByClassName的浏览器中运行,添加一行检测代码即可。

window.onload = function(){
    if(!document.getElementsByClassName) return false;

    var popup_elems = document.getElementsByClassName("popup");
    for(var i = 0; i < popup_elems.length; i++){
        var popup_elem = popup_elems[i];
        console.log(popup_elem.tagName)
        if(popup_elem.tagName.toLowerCase() === 'a'){
            popup_elem.onclick = function(){
                popUp(this.href);
                return false;
            }
        }
    }
}

性能

减少DOM访问

  • 能只遍历一次树,千万不要遍历两次
  • 合并放置脚本并放在body最后
  • 压缩脚本

图片库实例

曾在第四章时有一个图片库的案例,这里使用上面的知识来改造它。

下面是第四章的源代码

<!-- body中的片段 -->
<h1>Snapshots</h1>

<ul>
    <li><a href="./imgs/lion.jfif" title="狮子狮子" onclick="showPic(this);return false;">狮子</a></li>
    <li><a href="./imgs/car.webp" title="汽车汽车" onclick="showPic(this);return false;">汽车</a></li>
    <li><a href="./imgs/4.webp" title="三文鱼三文鱼" onclick="showPic(this);return false;">三文鱼</a></li>
    <li><a href="./imgs/2.webp" title="矿山矿山" onclick="showPic(this);return false;">矿山</a></li>
</ul>

<img id="img" alt="">

<p id="desc">Choose An Image.</p>
body{
    padding: 20px;
    background-color: cornsilk;
}
ul {
    list-style: none;
    margin: 0;
    padding: 0;
}
li{
    float: left;
    margin: 20px;
}

img{
    display: block;
    width: 400px;
    height: 300px;
    clear: both;
}
function showPic(picLink){
    var img_link = picLink.getAttribute("href");
    var img_desc = picLink.getAttribute("title");
    var img_elem = document.getElementById("img");
    var desc_elem = document.getElementById("desc").firstChild;

    img_elem.setAttribute("src",img_link);
    desc_elem.nodeValue = img_desc;
}

平稳退化

该代码支持平稳退化。

JS与HTML分离

这个代码中的JS与HTML并不是分离的。onclick写在了html中。

想解决它也很简单,我们只需要在HTML中添加一个“钩子”,让JS能根据钩子来操作图片库即可。

给ul标签添加id

<ul id="gallery">
    ...
</ul>

在JS中完成这部分的逻辑:

function prepare(){
    let gallery = document.getElementById("gallery");
    let links = gallery.getElementsByTagName("a");
    for(var i = 0; i < links.length; i++){
        links[i].onclick = function(){
            showPic(this);
            return false;
        }
    }
}

window.onload

上面光是定义了方法,还没有执行它,我们需要让这个方法被调用。

window.onload = prepare;

这里给window.onload赋值,将prepare方法赋予它,方法会在前端页面加载完毕后被调用。

问题是,如果我想要在窗口加载完成后调用多个函数,怎么办。

window.onload = function(){
    function1();
    function2();
}

这样是一个解决办法,但是还不够优美,当js写的太长时,每次我想将我的新函数放到onload中时,还得翻阅代码,找到onload的定义处,再把它添加进去。

var _onloadFunctions = []
function addOnLoad(func){
    _onloadFunctions.push(func);
}
window.onload = function(){
    for(var i=0;i<_onloadFunctions.length;i++){
        _onloadFunctions[i]()
    }
}

这样我们就可以在想添加函数到onload的位置直接调用

addOnLoad(functionName);

注意,书中的写法如下

我没这样写是因为,仔细观察这个方法,每次添加一个新方法,都会增加一层函数的调用栈,虽然这并不会太影响性能,但总觉得不太好。

下面是三次调用得到的实际window.onload

addLoadEvent(func1);
addLoadEvent(func2);
addLoadEvent(func3);


// 实际你得到的onload
window.onload = function(){
    function(){
        function(){
            func1();
        }();
        func2();
    }();
    func3();
}

向后兼容

保证向后兼容需要添加一些检测代码。

在prepare中添加这两行代码即可。

if(!document.getElementById) return false;
if(!document.getElementsByTagName) return false;

不要做过多的假设

function showPic(picLink){
    var img_link = picLink.getAttribute("href");
    var img_desc = picLink.getAttribute("title");
    var img_elem = document.getElementById("img");
    var desc_elem = document.getElementById("desc").firstChild;

    img_elem.setAttribute("src",img_link);
    desc_elem.nodeValue = img_desc;
}

showPic方法中,我假设了id为imgdesc的元素在页面中一定存在,而如果它们不存在,js的执行就会出错。

修改showPic方法,添加一些检测代码即可避免错误

function showPic(picLink){
    if(!document.getElementById("img")) return false;

    var img_link = picLink.getAttribute("href");
    var img_desc = picLink.getAttribute("title");
    var img_elem = document.getElementById("img");
    img_elem.setAttribute("src",img_link);

    if(document.getElementById("desc")) {
        var desc_elem = document.getElementById("desc").firstChild;
        desc_elem.nodeValue = img_desc;
    }
    return true;
}

但是这又会引入新的错误,如果页面中没有img,平稳退化原则就会崩塌,这时可以修改prepare中的一些代码:

links[i].onclick = function(){
    return !showPic(this);
}

因为onclick返回false代表阻止默认行为,true代表不阻止,showPic返回false代表设置图片到img中失败,true代表成功,当设置失败了,我们就不阻止默认行为,让其平稳退化。所以这里返回需要取反。

一些思考

首先在代码里到处是return显然违反了一个函数只能有一个入口和一个出口的规约,但是不这样写,就会出现很多的嵌套if。

if(document.getElementById){
    if(document.getElementBy...){
        if(...){

        }
    }
}

或者是很长的逻辑表达式

if(document.getElementById && document.getElementBy... && ...){

}

这两种办法可读性都不怎么好。

作者的说法是,如果非要多个返回值,就让它集中在方法开头,也就是前几行用于做这种检测,并返回,后面才是方法的逻辑。

但是这样做又会出现一个问题,你看showPic,其中因为遵守上面的规约,所以重复的调用了很多次document.getElementById,这是无用的调用,要知道每次使用这个都是在遍历树。

但如果你复用它就无法遵守规约

var img = document.getElementById("img");
if(!img) return;

img.setAttribute(img_link);

我的想法是,只要可读性还过得去,并且这点可读性上的牺牲来换取性能上的提高是划算的话,就写吧。

尽信书不如无书。

更进一层的分离

其实,如果这个图片库是你开发给其他人使用的,那么它必须为了支持你的图片库而在HTML代码中添加一个img和一个p标签来先显示图片。实际上还是将行为和结构耦合了。

能否将这里解耦?

这就需要我们的JS中自动创建这两个元素,并添加到页面中。

创建元素

复习,DOM中有哪几种节点?

答:元素节点、文本节点、属性节点。(参考书中第三章还是第四章我忘了)。

DOM API为创建这些节点提供了一些方法。

  • createElement(tagName) 用于创建一个元素节点,使用tagName作为元素的标签
  • createTextNode(text) 创建一个文本节点,并把text作为其中的文本
  • createAttribute(attributeName) 创建一个属性节点,属性名是attribute

首先,大部分浏览器支持更简单的方法,比如innerHTML属性等,但那些并不是DOM标准中定义的,本章只用DOM API中定义的方法。

也许把createTextNode改成createText会更好????

操作元素

  • parentElement.appendChild(childElement) 在父元素中追加子元素,既然是追加,新增元素在所有已有子元素的后面
  • parentElement.insertBefore(childElement,targetElement) 在父元素中插入子元素,插入的位置在targetElement之前

为什么这两个方法名取的还是如此的丑陋!!!

加强图片库

删除之前页面中的img和desc,添加如下的代码到js中,自动创建两个图片库必要的元素并添加到页面中。

function addEssentialElement(){
    if(!document.createElement) return false;
    if(!document.createAttribute) return false;
    if(!document.createTextNode) return false;

    var img = document.createElement("img");
    var img_id = document.createAttribute("id");
    img_id.value = "img";
    img.setAttributeNode(img_id);


    var desc = document.createElement("p");
    var desc_id = document.createAttribute("id");
    desc_id.value = "desc";
    desc.setAttributeNode(desc_id);
    desc.appendChild(document.createTextNode("Choose an image."))

    document.body.appendChild(img);
    document.body.appendChild(desc);
}

addOnLoad(addEssentialElement);

好几拨丑啊这代码说实话,就他妈像swing那种丑陋。

于是我写了一个简单的库,尽管库里的代码很丑,也没有太多的验证机制,但是调用方可以很优雅的调用它,就像你在使用FlutterJetpack Compose,使用组合来在js中创建视图。

如下是使用这个库后的代码,下面是为了展示出更多库的特性所写的代码,实际上并不需要这么多。

function addEssentialElement(){
    append(
        document.body,
        div({
            id: "gallery",
            styles: {
                width: "400px",
                height: "300px",
            },
            children: [
                img({
                    id: "img"
                }),
                p({
                    id: "desc",
                    text: text("Choose an Image.")
                })
            ]
        })
    )
}

简化后的代码:

function addEssentialElement(){
    appendAll(document.body,[
        img({
            id: "img"
        }),
        p({
            id: "desc",
            text: text("Choose an Image.")
        })
    ]);
};

以下为库的代码

// jsui.js 只是写着玩 请勿将此库用于生产环境!!!!!!!!!!!


function _autoMixinToAttribute(attrName,metaData){
    if(metaData[attrName]){
        if(!metaData.attributes) metaData.attributes = {};
        if(!metaData.attributes[attrName])
            metaData.attributes[attrName] = metaData[attrName];
    }
}
function element(metaData){
    if(!metaData.tag)return p({
        styles: {
            color: "red"
        },
        text: text("ERROR Element has no tag!!!")
    });

    let e = document.createElement(metaData.tag);
    if(metaData.styles){
        let styleString="";
        for(let style in metaData.styles){
            styleString += (style+": "+metaData.styles[style] + "; ");
        }
        if(!metaData.attributes) metaData.attributes = {};
        metaData.attributes["style"] = styleString;
    }
    if(metaData.attributes){
        for(let attr in metaData.attributes){
            let attrNode = document.createAttribute(attr);
            attrNode.value = metaData.attributes[attr];
            e.setAttributeNode(attrNode);
        }
    }
    if(metaData.text){
        e.appendChild(document.createTextNode(metaData.text));
    }
    if(metaData.children){
        appendAll(e,metaData.children)
    }
    return e;
}

function _elementWithTag(tag,metaData){
    metaData["tag"] = tag;
    _autoMixinToAttribute("id",metaData);
    _autoMixinToAttribute("class",metaData);
    return element(metaData);
}
function text(content){
    return content;
}

function p(metaData){
    return _elementWithTag("p",metaData);
}

function div(metaData){
    return _elementWithTag("div",metaData);
}

function img(metaData){
    // 提供src的简易写法
    _autoMixinToAttribute("src",metaData);
    return _elementWithTag("img",metaData);
}

function span(metaData){
    return _elementWithTag("span",metaData);
}

function head(metaData){
    if(!head.level)return head({
        level: 1,
        styles: {
            color: "red"
        },
        text: text("ERROR HEAD NO LEVEL!!!")
    });

    if(head.level==1) return _elementWithTag("h1",metaData);
    if(head.level==2) return _elementWithTag("h2",metaData);
    if(head.level==3) return _elementWithTag("h3",metaData);
    if(head.level==4) return _elementWithTag("h4",metaData);
    if(head.level==5) return _elementWithTag("h5",metaData);
    if(head.level==6) return _elementWithTag("h6",metaData);
    else return head({
        level: 1,
        styles: {
            color: "red"
        },
        text: text("ERROR HEAD LEVEL IS NOT VALID!!! YOU CAN USE 1~6 ONLY")
    })
}

function br(metaData){
    if(!metaData) metaData = {}
    return _elementWithTag("br",metaData);
}

function append(parentElement,childElement){
    parentElement.appendChild(childElement);
}

function appendAll(parentElement,childElements){
    for(let i in childElements){
        append(parentElement,childElements[i]);
    }
}
posted @ 2021-06-25 09:41  yudoge  阅读(75)  评论(0编辑  收藏  举报