事件委托(事件代理)
事件委托也可以叫事件代理,是事件冒泡与事件捕获的运用。
- 基本概念
一般来讲,会把一个或者一组元素的事件委托到它的父层或者更外层元素上,真正绑定事件的是外层元素,当事件响应到需要绑定的元素上时(事件捕获),会通过事件冒泡机制从而触发它的外层元素的绑定事件上,然后在外层元素上去执行函数。
- 动态绑定事件
在很多时候,我们需要通过 AJAX 或者用户操作动态的增加或者去除列表项元素,那么在每一次改变的时候都需要重新给新增的元素绑定事件,给即将删去的元素解绑事件;
如果用了事件委托就没有这种麻烦了,因为事件是绑定在父层的,和目标元素的增减是没有关系的,执行到目标元素是在真正响应执行事件函数的过程中去匹配的;
所以使用事件在动态绑定事件的情况下是可以减少很多重复工作的。
- item 1
- item 2
- item 3
......
- item n
document.getElementById('list').addEventListener('click', function (e) {
// 兼容性处理
var event = e || window.event;
var target = event.target || event.srcElement;
if (target.matches('li.class-1')) {
console.log('the content is: ', target.innerHTML);
}
});
自定义事件
可以代码触发事件执行,同时可以配合代理等功能实现修改数据界面自动更新
- Event()——Event对象的构造函数
- event = new Event(typeArg, eventInit);
- typeArg:指定事件类型,传递一个字符串。这里的事件类型指的是像点击事件(click)、提交事件(submit)、加载事件(load)等等。
- eventInit:可选,传递EventInit类型的字典。实际上这个EventInit类型的字典也就是我们使用InitEvent()时需要传递的参数,以键值对的形式传递,不过它可以多选一个参数:
- bubbles:事件是否支持冒泡,传递一个boolean类型的参数,默认值为false。
- cancelable:是否可取消事件的默认行为,传递一个boolean类型的参数,默认值为false。
- composed:事件是否会触发shadow DOM(阴影DOM)根节点之外的事件监听器,传递一个boolean类型的参数,默认值为false。
- CustomEvent()——CustomEvent对象的构造函数
CustomEvent()可以像Event()那样赋值,但它可以在Web Workers中使用(与主线程分离的另一个线程),可以传递跟事件关联的相关值(detail)。
- event = new CustomEvent(typeArg, customEventInit);
- typeArg:指定事件类型,传递一个字符串。这里的事件类型指的是像点击事件(click)、提交事件(submit)、加载事件(load)等等。
- customEventInit:可选。传递一个CustomEventInit字典。实际上这个字典就是我们使用initCustomEvent()时需要的参数,这个参数就是事件相关值(detail):
- detail:可选,默认值为null,类型为any(也就是说可以传递任意类型的参数)。这个值就是和事件相关联的值。
- CustomEventInit字典也可以接受EventInit字典的参数,
WebComponents
基本使用(复制阮一峰博客)
这种自定义的 HTML 标签,称为自定义元素(custom element)。根据规范,自定义元素的名称必须包含连词线,用与区别原生的 HTML元素。所以,
class UserCard extends HTMLElement {
constructor() {
super();
}
}
上面代码中,UserCard就是自定义元素的类。注意,这个类的父类是HTMLElement,因此继承了 HTML 元素的特性。接着,使用浏览器原生的customElements.define()方法,告诉浏览器
window.customElements.define('user-card', UserCard);
- 自定义元素的内容
自定义元素
class UserCard extends HTMLElement {
constructor() {
super();
var image = document.createElement('img');
image.src = 'https://semantic-ui.com/images/avatar2/large/kristy.png';
image.classList.add('image');
var container = document.createElement('div');
container.classList.add('container');
var name = document.createElement('p');
name.classList.add('name');
name.innerText = 'User Name';
var email = document.createElement('p');
email.classList.add('email');
email.innerText = '[email protected]';
var button = document.createElement('button');
button.classList.add('button');
button.innerText = 'Follow';
container.append(name, email, button);
this.append(image, container);
}
}
- 标签
使用 JavaScript 写上一节的 DOM 结构很麻烦,Web Components API提供了标签,可以在它里面使用 HTML 定义 DOM。
User Name
然后,改写一下自定义元素的类,为自定义元素加载。
class UserCard extends HTMLElement {
constructor() {
super();
var templateElem = document.getElementById('userCardTemplate');
//true深度克隆
var content = templateElem.content.cloneNode(true);
this.appendChild(content);
}
}
上面代码中,获取节点以后,克隆了它的所有子元素,这是因为可能有多个自定义元素的实例,这个模板还要留给其他实例使用,所以不能直接移动它的子元素。
- 完整代码如下
...
- 添加样式
自定义元素还没有样式,可以给它指定全局样式,比如下面这样。
user-card {
/* ... */
}
但是,组件的样式应该与代码封装在一起,只对自定义元素生效,不影响外部的全局样式。所以,可以把样式写在里面。
User Name
上面代码中,样式里面的:host伪类,指代自定义元素本身。
- 自定义元素的参数
内容现在是在里面设定的,为了方便使用,把它改成参数。
代码也相应改造
最后,改一下类的代码,把参数加到自定义元素里面。
class UserCard extends HTMLElement {
constructor() {
super();
var templateElem = document.getElementById('userCardTemplate');
var content = templateElem.content.cloneNode(true);
content.querySelector('img').setAttribute('src', this.getAttribute('image'));
content.querySelector('.container>.name').innerText = this.getAttribute('name');
content.querySelector('.container>.email').innerText = this.getAttribute('email');
this.appendChild(content);
}
}
window.customElements.define('user-card', UserCard);
- Shadow DOM
如果不希望用户能够看到
自定义元素的this.attachShadow()方法开启 Shadow DOM
class UserCard extends HTMLElement {
constructor() {
super();
var shadow = this.attachShadow( { mode: 'closed' } );
var templateElem = document.getElementById('userCardTemplate');
var content = templateElem.content.cloneNode(true);
content.querySelector('img').setAttribute('src', this.getAttribute('image'));
content.querySelector('.container>.name').innerText = this.getAttribute('name');
content.querySelector('.container>.email').innerText = this.getAttribute('email');
shadow.appendChild(content);
}
}
window.customElements.define('user-card', UserCard);
this.attachShadow()方法的参数{ mode: 'closed' },表示 Shadow DOM 是封闭的,不允许外部访问
- 组件的扩展
用户卡片是一个静态组件,如果要与用户互动,也很简单,就是在类里面监听各种事件。
this.$button = shadow.querySelector('button');
this.$button.addEventListener('click', () => {
// do something
});
上面的例子中,与网页代码放在一起,其实可以用脚本把注入网页。这样的话,JavaScript 脚本跟就能封装成一个 JS 文件,成为独立的组件文件。网页只要加载这个脚本,就能使用
扩展原生组件
Document
实际案例
Document
asdaoj
下面是通过函数名,动态注册函数到webcomponents实例的工具类,拿来使用即可
export default class popEvent {
constructor(option) {
/*
* 接收四个参数:
* 1,对象的this
* 2,要监听的元素, 不传则为对象this
* 3,要监听的事件,默认监听点击事件
* 4,是否冒泡, 默认冒泡
* */
this.eventObj = option.obj;
this.target = option.target || this.eventObj;
this.eventType = option.eventType || 'click';
this.popup = option.popup || true;
this.bindEvent();
}
bindEvent() {
let _this = this;
_this.target.addEventListener(_this.eventType, function (ev) {
let target = ev.target;
let dataset, parent, num, b;
popup(target);
function popup(obj) {
if (obj === document) {
return false;
}
dataset = obj.dataset;
num = Object.keys(dataset).length;
parent = obj.parentNode;
if (num < 1) {
popup(parent);
num = 0;
} else {
for (b in dataset) {
if (_this.eventObj.__proto__[b]) {
_this.eventObj.__proto__[b].call(_this.eventObj, {
obj: obj,
ev: ev,
target: dataset[b],
data: _this.eventObj
});
}
}
_this.popup && popup(parent);
}
}
})
}
}
Webcomponents实体类逻辑函数
import popEvent from './event.js';
let temp = document.createElement('template');
document.body.appendChild(temp);
// 此处是把模板用作字符串处理,实际上也可以放在html页面中(template中不会显示在页面上),或者js脚本字符串形式导入,或者http传递
temp.innerHTML = `
{{ title}}
{{content}}
知道了
马上来抢
`;
export default class myCom extends HTMLElement {
constructor() {
super();
console.log(this.innerHTML);
let attr = this.attributes;
this._data = {
title: attr.title ? attr.title.value : '默认的标题',
content: attr.content ? attr.content.value : '这里是默认的内容,这个人很懒,什么有意义的内容都没有留下',
}
this.render();
this.bindEvent();
this.compileNode(this.obj);
this.observe(this._data);
setTimeout(() => {
this._data.title = '这里是修改的标题'
}, 2000)
}
//响应式,这样注册之后,可以直接修改数据就可以变化显示内容
observe(data) {
let _this = this;
this._data = new Proxy(data, {
set(obj, prop, value) {
let event = new CustomEvent(prop, {
detail: value
});
_this.dispatchEvent(event);
return Reflect.set(...arguments);
}
});
}
//拦截事件,防止传递到下一个Element,直接return false即可;
// if (btn) {
// btn.bind('click',function() {
// return false
// })
// }
render() {
this.btn = document.createElement('button');
this.btn.innerText = '点击显示弹窗';
this.btn.setAttribute('data-open', 'true');
this.obj = document.createElement('div');
//深度克隆
this.obj.append(temp.content.cloneNode(true));
this.append(this.obj, this.btn);
this.hide();
}
compileNode(el) {
let child = el.childNodes;
[...child].forEach((node) => {
if (node.nodeType === 3) {
let text = node.textContent;
//正则表达式用于替换内容{{}}
let reg = /\{\{\s*([^\s\{\}]+)\s*\}\}/g;
if (reg.test(text)) {
let $1 = RegExp.$1;
this._data[$1] && (node.textContent = text.replace(reg, this._data[$1]));
this.addEventListener($1, (e) => {
node.textContent = text.replace(reg, e.detail)
})
};
} else if (node.nodeType === 1) {
let attrs = node.attributes;
if (attrs.hasOwnProperty('v-model')) {
let keyname = attrs['v-model'].nodeValue;
node.value = this._data[keyname];
node.addEventListener('input', e => {
this._data[keyname] = node.value;
});
// console.log(this._data);
// console.log(attrs['v-model'].nodeValue);
}
if (node.childNodes.length > 0) {
this.compileNode(node);
}
}
})
}
bindEvent() {
this.event = new popEvent({
obj: this
});
}
open() {
this.obj.style.display = 'block';
}
hide() {
console.log('点击了mask');
this.obj.style.display = 'none';
}
cancel() {
console.log('点击了取消');
}
confirm() {
console.log('点击了确定');
}
test() {
console.log('触发了弹窗的点击事件');
}
}
window.customElements.define('my-com', myCom);
let style = document.createElement('style');
document.body.appendChild(style);
style.innerText = `
body {
margin: 0;
}
.dialog_box {
width: 521px;
height: 563px;
border-radius: 8px;
box-shadow: 2px 2px 5px 0 rgba(0, 0, 0, 0.4);
background-color: #fff;
position: absolute;
padding: 0 40px;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
}
.dialog_box.bg1 {
background-image: url("img/bg_03.png");
background-repeat: no-repeat;
background-size: 100%;
}
.dialog_box.bg2 {
background-image: url("img/bg_06.png");
background-repeat: no-repeat;
background-size: 100%;
}
.dialog_header {
height: 215px;
}
.dialog_title {
height: 57px;
font-size: 30px;
text-align: center;
line-height: 57px;
}
.dialog_content {
height: 130px;
text-align: center;
font-size: 24px;
line-height: 29px;
padding: 30px 52px 0;
}
.btn_line {
height: 110px;
display: flex;
justify-content: center;
}
.btn {
width: 221px;
height: 74px;
border-radius: 4px;
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.3);
font-size: 30px;
line-height: 74px;
text-align: center;
cursor: pointer;
margin: 0 20px;
}
.btn.cancel {
background: #e7e7e7;
}
.btn.confirm {
background: #e5322e;
color: #fff;
}
.mask {
background: rgba(0, 0, 0, 0.5);
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
}`
前端大数据量插入,不卡顿方案
渲染大数据时,合理使用 createDocumentFragment
和 requestAnimationFrame
,将操作切分为一小段一小段执行。
createDOcumentFragment
1 . createDOcumentFragment()方法,是用来创建一个虚拟的节点对象,或者说,是用来创建文档碎片节点。它可以包含各种类型的节点,在创建之初是空的。
2 . DocumentFragment 节点不属于文档树,继承的 ParentNode 属性总是 null 。它有一个很实用的特点,当请求把一个DocumentFragment节点插入文档树时,插入的不是DocumentFragment自身,而是它的所有子孙节点,即插入的是括号里的节点。这个特性使得DocumentFragment成了占位符,暂时存放那些一次插入文档的节点。它还有利于实现文档的剪切、复制和粘贴操作。
另外当需要添加多个dom元素时,如果先将这些元素添加到DocumentFragment 中,再统一将DocumentFragment添加到页面,会减少页面渲染dom的次数,效率会明显提升。
requestAnimationFrame
requestAnimationFrame 比起 setTimeout 、 setlnterval 的优势主要有两点:
1 、 requestAnimationFrame会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率,一般来说,这个频率为每秒60帧。
2 、在隐藏或不可见的元素中,requestAnimationFrame将不会进行重绘或回流,这当然就意昧着更少的的 cpu , gpu 和内存使用量。
总结来说:通过requestAnimationFrame可以在每一帧渲染得时候执行插入操作,这样可以避免卡顿,不会因为递归导致光执行插入,而卡顿(连页面都滚动不了)。
而createDOcumentFragment其实就相当于虚拟节点,被插入到它内部得节点,在没有被其他dom使用之前,可以理解成不涉及渲染消耗,只是纯数据。这两者结合不是可以插入N条数据还不卡顿,而是实现插入得过程不卡顿。因为不论再怎么优化,只要数据足够大,内存依然会爆炸
Document
迭代
- 迭代和遍历的区别
迭代:1. 有序的 2. 连续的
- 为什么Object不能遍历?
下面开始针对这开始说明
- 生成器相关
Promise
Promise的状态
- pending - 进行中
- fulfilled - 成功
- rejected - 失败
Promise的特性
- promise状态不受外界影响
- promise固化性:一旦promise状态变化后就不可在更改了,例如:只能从pending->fulfilled或者pending->rejected
案例一:固化
const fs = require('fs');
function readFile(pathname) {
return new Promise((resolve, reject) => {
console.log(1);
false.readFile(pathname, 'utf-8', (err, data) => {
if (err) {
reject(err);
return;
} else {
resolve(data);
}
})
})
}
let promise=readFile('./1.html');
promise.then(function(data) {
console.log(1,data);
promise.then(function(data) {
console.log(2,data);
})
},function(err) {
console.log("失败回调",err);
})
//resovle:then不论是嵌套几层,都是可以立刻拿到data的
//而reject返回也一样,不过都是resovle只能成功回调接收
//而reject返回结果只能err回调接收
//这就是promise的固化
执行流程案例一
Promise静态方法
- thenable对象
// thenable对象:
let obj = {
then(resolve, reject) {
// resolve(11)
reject(22)
}
}
//Promise.resolve 传递进去对象就会被自动调用obj内部的then
//如果then内部是reject就是失败回调,resolve就会走成功回调
let p1 = Promise.resolve(obj);
p1.then(function (data) {
console.log(data);
}, function (err) {
console.log('err' + err); //err22
});
//但是reject时候不会调用then函数
let p11 = Promise.reject(obj);
p11.then(function (data) {
console.log(11, data);
}, function (err) {
console.log('err11', err);//err11 {then;f} //注意此处是对象是特别的
});
执行流程案例二
Promise.resolve().then(function () {
console.log('promise1');
setTimeout(() => {
console.log('timeout2');
}, 0);
})
setTimeout(() => {
console.log('timeout1');
Promise.resolve().then(function () {
console.log('promise2');
})
}, 0);
//下面的一个setTimeout是先注册的,Promise.resolve().then也注册到任务队列
//然后执行then之后其内部的settimeout才注册上
//答案: promise1 timeout1 promise2 timeout1
//其他静态方法:
// Promise.all:let p1=Promise.all([p2,p3,p4])
// Promise.race: let p1=Promise.race([p2,p3,p4])
//all:全部成功才成功,否则就是失败
//race:谁最快返回谁,不论是成功还是失败
Promise链式调用
生成器以及co模块原理和演化过程
//自定义实现promisify
function promisify(fn) {
return function (...args) {
return new Promise((resolve, reject) => {
fn(...args, (err, data) => {
if (err) {
reject(err);
return
}
resolve(data);
})
})
}
};
let readFile = promisify(fs.readFile);
//方式一
// readFile('./name.txt')
// .then(res=>readFile(res))
// .then(res=>readFile(res))
// .then(res=>console.log(res))
//本意就是:文件连续读取,然后把文件内部文件名传递下去一层层读
//最后打印最后一个文件的内容
//因为then内部返回会被自动包装成Promise所以这就避免了回调地狱
//方式二:演进
// 生成器和执行器
function* read() {
let v1 = yield readFile('./name.txt', 'utf-8');
let v2 = yield readFile(v1, 'utf-8');
let v3 = yield readFile(v2, 'utf-8');
console.log(v3);
}
//执行器逻辑
// let iter = read();
// let {value,done}=iter.next();
// value.then(v1=>{
// let {value,done}=iter.next(v1);
// value.then(v2=>{
// let {value,done}=iter.next(v2);
// value.then(v3=>{
// iter.next(v3);//这个值就返回给了上面的console.log(v3);
// })
// })
// })
//封装的写法:演进
function co(iter) {
return new Promise((resolve, reject) => {
let next = function (data) {
let { value, done } = iter.next(data);
if (done) {
resolve(data);
} else {
value.then(val => {
next(val);
},reject);
}
}
next();
})
}
let promise=co(read());
promise.then(res=>{
console.log(res);
})
//最终方案:async await其实就是上面co的进阶版
async function readSync() {
let v1 = await readFile('./name.txt', 'utf-8');
let v2 = await readFile(v1, 'utf-8');
let v3 = await readFile(v2, 'utf-8');
return v3;
}
let pawait=readSync();
pawait.then(res=>{
console.log(res);
})