【JS编程接口】手写DOM库

262 阅读9分钟

DOM提供的api实在是不好用,每个API又臭又长

源代码链接

一、一些术语

  • 我们把提供给其他人用的工具代码叫做库
  • 比如jQuery、Underscore
  1. API
  • 库暴露出来的函数或属性叫做API (应用编程接口)
  1. 框架
  • 当你的库变得很大,并且需要学习才能看懂,
  • 那么这个库就叫框架,比如Vue / React
  1. 注意
  • 编程界的术语大部分都很随便,没有固定的解释
  • 所以意会即可

二、对象风格

window.dom 是我们提供的全局对象

// 怎么样把一个对象声明为全局对象呢?
// 把对象挂载到window上

dom = window.dom

三、增删改查

1.创建节点

dom.create(`<div><span>hi</span></div>`) // 用于创建节点
  • 一般我们创造节点的目的就是在别的节点中插入此节点,
  • 那么我要封装一个 以输入 html格式的 的 create函数
  • 能够在创造节点的同时在里面加一些其他节点
  • 传入的字符串要是以有标签的 以html的形式来 dom.create源代码
create(string) {
    // 创建容器    template标签可以容纳任意元素
    const container = document.createElement('template')
    // 要trim,防止拿到空字符
    container.innerHTML = string.trim()
    // 必须要 使用 .content  要不然拿不到
    return container.content.firstChild

    // 或者
    // container.innerHTML = string
    // return container.content.children[0]
  }
  //用法
  dom.create('<div>你好</div>')

2.新增弟弟

思路:由于dom只提供了Insertbefore操作,没有提供insertAfter操作, 所以我们需要使用一点黑魔法假设有两个节点 div1 --- > div3我们想要再div1后面加一个div2, 这就等价于在div3前面插入div2所以我们现有node.nextSibling获取当前节点的下一个节点,再insertBefore

after(node, newNode) {
   // 找到此节点的爸爸然后调用insertBefore(插入某个节点的前面)方法,
   //把 newNode 插入到下一个节点的前面
   node.parentNode.insertBefore(newNode, node.nextSibling)
 }
 // 用法示例:
dom.before(div1,div2)

3.新增哥哥

思路:找到爸爸节点,然后insertBefore

before(node, newNode) {
   // 正常的返回DOM原生的添加前面的节点的方法即可
   node.
   parentNode.insertBefore(newNode, Node)
 }
// 用法示例:
dom.before(div1,div2)

4.新增儿子

思路:找到爸爸节点,appendChild

append(parent, node) {
      parent.appendChild(node)
  }
  // 用法示例:
let div1 = dom.create('<div>1</div>')
let div2 = dom.create('<div>2</div>')
dom.append(div1,div2)

5.新增爸爸

wrap(node, parent) {
        // 把Newparent 放到node前面
        // 把node append到newparent里
        // 目标: div1
        //        ↓----> div2
        // 变成  div1
        //        ↓----> div3
        //                ↓----> div2
  			// 先把div3 放到div2的前面,再div3.appendChild(div2)
      dom.before(node, parent)
      dom.append(parent, node)
    }
// 用法示例:
let div1 = dom.create('<div>1</div>')
let div2 = dom.create('<div>2</div>')
dom.wrap(div1,div2)

使用方法

const div3 = dom.create('<div id="parent"></div>')
dom.wrap(test, div3)

1.删除节点

思路:找到爸爸节点, removeChild

remove(node){
        node.parentNode.removeChild(node)//让这个节点的爸爸删除这个儿子
        return node//还可以保留这个节点的引用
    
// 用法示例:
dom.remove(div1)

2.删除后代

用法: 删除这个节点的所有子代

思路: 遍历删除它的所有子节点,并返回 删除的节点

不能用for循环的原因:因为每次 dom.remove 删除的时候,它的长度就会随之改变, 而我们又在for循环它,因此我测试时候会出现bug,因此我们选择使用 while 循环 解决。

 // empty 把所有子节点删掉
    // 坑:childNodes.length每次的Length会变化
    empty(node) {
        // const {childNodes} = node 等价于const childNodes = node.childNodes
        const array = []
        let x = node.firstChild
        while (x) {
            array.push(dom.remove(node.firstChild))
            x = node.firstChild
        }
        return array
    }
// 用法示例:
dom.empty(div1))

1.读写属性

  • 这里用到了重载,即当函数的参数不一样时,做不一样的处理
  • 当只有两个参数时,就读属性,返回属性
  • 当有三个参数时,就修改属性
  • 这里由于同时可以读写属性,用到了getter和setter设计模式
// 根据参数的个数,实现不同的函数,这叫函数的重载
    attr(node, name, value) {
        if (arguments.length === 3) {
            node.setAttribute(name, value)
        } else if (arguments.length === 2) {
            return node.getAttribute(name)
        }
    },

使用方法

// 修改 <div id="test" title="hi">test</div>
// #test的title属性值为 hello world
dom.attr(test, 'title', 'hello world')  //修改属性
const title = dom.attr(test, 'title')   //读属性
console.log(`title: ${title}`)

2.读写文本内容

  • 如果只有一个参数就读文本内容,返回node.innerText
  • 如果有两个参数,就写文本内容 node.innerText = string
  • 同时还对不同浏览器做了适配,这里运用了适配器模式
text(node, string) {
        if (arguments.length === 2) {
            // 适配不同浏览器
            if ('innerText' in node) { //ie
                node.innerText = string
            } else { // firefox / chrome
                node.textContent = string
            }
        } else if (arguments.length === 1) {
            if ('innerText' in node) { //ie
                return node.innerText
            } else { // firefox / chrome
                return node.textContent
            }
        }

    }
// 用法示例:
dom.text(div1) //读取div1里的文本内容
dom.text(div1,'你好') //改写div1里的文本内容

3.读写HTML内容 dom.html(node, ?)

 html(node, string) {
        if (arguments.length === 2) {
            //修改
            node.innerHTML = string
        } else if (arguments.length === 1) {
            // 获取内容
            return node.innerHTML
        }
    }

// 用法示例:
dom.html(div1) //读取div1标签里的HTML内容
dom.html(div1,'<span>你好</span>') //改写div1标签里的HTML内容

4.修改style dom.style(node, {color:'red'})

// 原生DOM读取、修改样式:
node.style.name
node.style.name = 'value'

// 封装:
function style(node,name,value){
	if(arguments.length === 3){
    	node.style[name] = value
    }else if(arugments.length === 2){
    	return node.style[name]
    }
}

// 用法示例:
dom.style(div1,'color') //读取div1标签的样式里的color的值
dom.style(div1,'color','red') //修改div1标签的样式里的color的值

// 有的人喜欢这样写:dom.style(div1,{color:'red'}),所以可以继续优化:
function style(node,name,value){
	if(arguments.length === 3){
    	node.style[name] = value
    }else if(arugments.length === 2){
    	if(typeof name === 'string'){
        	//如果输入的第二个参数的数据类型是字符串的话,就返回样式属性的值
            renturn node.style[name]
        }else if(typeof name === 'object'){
        	//如果输入的第二个参数的数据类型是对象的话,就执行下面的代码
        	const object = name //把name的值赋值给object变量
            for(let key in object){ //在object变量里放入一个key,代表属性值
            	node.style[key] = object[key]
            }
        }
    }
}
// 用法示例:
dom.style(div1,{color:'red'})
// 这里name、object和key有点绕,下面配一个图示就能看的容易一些了:

image.png

instanceof

用typeof可以判断一个输入的变量的数据类型,当判断的东西是对象时,我们还可以用instanceof;

instanceof 翻译是实例的意思,是一个对象运算符,用来判断一个对象是谁的实例,检测构造函数的prototype属性是否出现在某个实例对象的原型链上。所以也可以用来检测对象数据类型。

用法:obj1 instanceof obj2

如果 obj1 是 obj2 的实例的话,就返回true,不是的话,就返回false;

instanceof 和 typeof 的不同:

  1. 形式不同:(typeof sth) 和 (sth1 instanceof sth2)
  2. typeof 可以判断所有数据类型的变量,返回值是字符串,返回值有:number、string、symbol、bool、null、undefined、function、object,而instanceof仅适用于对象这一数据类型;
  3. typeof 用来判断丰富的对象实例时,只能返回一个'object'字符串。用instanceof用来判断对象,可以返回true或false;
  4. instanceof 可以对不同的对象实例进行判断,判断方法是根据对象的原型链依次向下查询,而 typeof 就不能;

所以之前的代码也可以用 instanceof 来改写

function style(node,name,value){
	if(arguments.length === 3){
    	node.style[name] = value
    }else if(arguments.length === 2){
    	if(typeof name === 'string'){
            return node.style[name]
        }else if(name instanceof Object){
        	// 对比: typeof name === 'object'
        	const object = name 
            for(let key in object){
            	node.style[key] = object[key]
            }
        }
    }
}

5.添加、删除class

// 原生DOM对class的操作:
node.classList.add(className) //添加一个类
node.classList.remove(className) //移除一个类
node.classList.contains(className)//检测一个节点有没有某个类

// 封装:
windom.dom = {
	class:{
    	add(node,className){
        	node.classList.add(className)
        },
        remove(node,className){
        	node.classList.remove(className)
        },
        has(node,className){
        	return node.classList.contains(className)
        }
    }
}

// 用法示例:
dom.class.add(div1,'red')
dom.class.remove(div1,'red')
dom.class.has(div1,'red')

6.添加、删除事件监听

// 原生DOM的事件监听:
// 自身属性里的onclick:
node.onclick = function(){...} //添加一个鼠标点击事件
node.onclick = null //移除鼠标点击事件

// 原型链里的EventTarget.prototype里的方法:
node.addEventListener(click,function(){...}) //添加一个鼠标点击事件
node.removeEventListener(click,function(){...}) //移除鼠标点击事件

// 封装:
function on(node,eventName,fn){
	node.addEventListener(eventName,fn)
}
function off(node,eventName,fn){
	node.removeEventListener(eventName,fn)
}

// 用法示例:
dom.on(div1,'click',function(){console.log('hi')})

function sayHi(){console.log('hi')}
dom.on(div1,'click',sayHi)
dom.off(div1,'click',sayHi)

// 用off的话,必须要提前把函数名字定好(使用具名函数,就是具有名字的函数),不然使用匿名函数是无效的:
// 错误示范:
dom.on(div1,'click',function(){console.log('hi')})
dom.off(div1,'click',function(){console.log('hi')})
// 这样写off是不生效的,点击了依然还会执行原来添加的功能

1.获取标签

// 原生DOM里寻找元素的办法:
doucment.getElementById('id')
document.getElementsByClassName('div')[0]
document.getElementsByClassName('red')[0]
document.querySelector('#id')
document.querySelectorAll('.red')[0]

// 封装:
function find(selector){
	return document.querySelectorAll(selector)
}

// 用法示例:
dom.find('#test')[0] //因为返回的是个伪数组,所以要记得在后面加下标

// 拓展:
// 假如我想在下面的代码里把p标签从div标签里找出来,如何实现?
<div id='test'>
	<p class='red'>文字</p>
</div>

// 实现:
function find(selector,scope){ //scope 范围的意思
	return (scope || document).querySelectorAll(selector)
}

// 用法示例:
let test = dom.find('#test')[0]
dom.find('.red',test)

2.获取父元素和子元素

// 获取父元素
function parent(node){
	return node.parentNode
}

// 获取子元素
function children(node){
	return node.children
}

// 用法示例:
dom.parent(div1)
dom.children(div1)

3.获取兄弟姐妹元素

// 原生DOM里用 node.parentNode.children 可以获取到兄弟姐妹元素,所以可以封装为:
function siblings(node){
	return node.parentNode.children
}

// 这样可以获取到所有的兄弟姐妹元素,但是里面也包括了自己,所以要把自己给排除出去,剩下的才是真正严格意义上的所有兄弟姐妹节点
// 这里的思路要用到数组的过滤方法:filter,但是返回的是一个伪数组,所以要先把返回结果变成数组再过滤
function siblings(node){
	return Array.from(node.parentNode.children).filter((n)=> n !== node)
    // 使用Array.from 使之变成数组
    // filter((n)=> n !== node) 意思是如果n不等于node,就留下来放到数组里,等于node就给过滤出去
}

// 用法示例:
dom.siblings(div1)

4.获取哥哥和弟弟元素

// 原生DOM获取下一个节点:node.nextSibling
// 原生DOM获取上一个节点:node.previousSibling
// 注意这里是节点,不是元素,因为获取到的可能是文本节点,而我们想获取的不是文本节点,所以在封装的时候要排除:

// 获取弟弟元素
function next(node){
	let x = node.nextSibling
    while(x && x.nodeType === 3){
    	// 当x存在时,并且节点类型为3,也就是文本节点时,就继续寻找下一个节点,直到不是文本节点为止
        x = x.nextSibling
    }
    return x
}

// 获取哥哥元素
function pervious(node){
	let x = node.previousSibling
    while(x && x.nodeType === 3){
    	x = x.previousSibling
    }
    return x
}

odeType详细信息:nodeType MDN

5.遍历所有节点

function each(nodeList,fn){
	for(let i = 0;i<nodeList.length;i++){
    	fn.call(null,nodeList[i]) // this设置为空
    }
}

// 用法示例:
// html:
<div id="div1">
        <div id="div2">2</div>
        <div id="div3">3</div>
</div>

// js:
let divList = document.querySelector('#div1').children
dom.each(divList,(n) => n.style.color = 'red')

6.获取元素排行老几

// 思路:把给出元素的所有兄弟姐妹元素都找出来遍历循环,循环到自己的时候返回数字
function index(node){
	const list = dom.children(node.parentNode)
    let i //之所以在循环外面声明i,是因为用let声明时,作用域仅限于循环内,到外面就不能返回了
    for(i = 0;i<list.length;i++){
    	if(list[i] === node){
        	break;
        }
    }
    return i
}