0%

tree shaking如何工作的呢?

虽然 tree shaking 的概念在 1990 就提出了,但直到 ES6 的 ES6-style 模块出现后才真正被利用起来。

在ES6以前,我们可以使用CommonJS引入模块:require(),这种引入是动态的,也意味着我们可以基于条件来导入需要的代码

但是CommonJS规范无法确定在实际运行前需要或者不需要某些模块,所以CommonJS不适合tree-shaking机制。在 ES6 中,引入了完全静态的导入语法:import。

因为tree shaking只能在静态modules下工作。ECMAScript 6 模块加载是静态的,因此整个依赖树可以被静态地推导出解析语法树。所以在 ES6 中使用 tree shaking 是非常容易的。

tree shaking的原理是什么?

  • ES6 Module引入进行静态分析,故而编译的时候正确判断到底加载了哪些模块

  • 静态分析程序流,判断哪些模块和变量未被使用或者引用,进而删除对应代码

  • webpack中,tree-shaking的实现一是先标记出模块导出值哪些没有被用过,二是使用Terser删除这些没被用到的导出语句,标记过程分为三个步骤:

    (1)Make阶段,收集模块导出变量记录到模块关系依赖图ModuleGraph变量中

    (2)Seal阶段,遍历ModuleGraph标记模块导出变量有没有被使用

    (3)生成产物时,若变量没有被其他模块使用则删除对应导出语句

使用

将文件标记为无副作用(side-effect-free)

在一个纯粹的 ESM 模块世界中,识别出哪些文件有副作用很简单。然而,我们的项目无法达到这种纯度,所以,此时有必要向 webpack 的 compiler 提供提示哪些代码是“纯粹部分”。

这种方式是通过 package.json 的 "sideEffects" 属性来实现的。

1
2
3
4
{
"name": "your-project",
"sideEffects": false
}

如同上面提到的,如果所有代码都不包含副作用,我们就可以简单地将该属性标记为 false,来告知 webpack,它可以安全地删除未用到的 export 导出。

「副作用」的定义是,在导入时会执行特殊行为的代码,而不是仅仅暴露一个 export 或多个 export。举例说明,例如 polyfill,它影响全局作用域,并且通常不提供 export。

压缩输出

通过如上方式,我们已经可以通过 importexport 语法,找出那些需要删除的“未使用代码(dead code)”,然而,我们不只是要找出,还需要在 bundle 中删除它们。为此,我们将使用 -p(production) 这个 webpack 编译标记,来启用 uglifyjs 压缩插件。

注意,--optimize-minimize 标记也会在 webpack 内部调用 UglifyJsPlugin

从 webpack 4 开始,也可以通过 "mode" 配置选项轻松切换到压缩输出,只需设置为 "production"

webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
const path = require('path');

module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
- }
+ },
+ mode: "production"
};

注意,也可以在命令行接口中使用 --optimize-minimize 标记,来使用 UglifyJSPlugin。为了学会使用 tree shaking,你必须……

  • 使用 ES2015 模块语法(即 importexport)。

  • 在项目 package.json 文件中,添加一个 “sideEffects” 入口。

  • 引入一个能够删除未引用代码(dead code)的压缩工具(minifier)(例如 UglifyJSPlugin)。

  • 配置optimization对象的usedExports为true

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // webpack.config.js
    module.exports = {
    entry: "./src/index",
    mode: "production",
    devtool: false,
    optimization: {
    usedExports: true,
    },
    };

是什么

HMR 全称是Hot Module Replacement,理解为模块热替换,指在应用程序运行过程中,替换,添加,删除模块,而无需重新刷新整个应用。

例如,我们在应用运行过程中修改了某个模块,通过自动刷新会导致整个应用的整体刷新,那页面中的状态信息都会丢失,如果使用HME,就可以实现只将修改的模块实时替换至应用中,不必完全舒心整个应用。

在webpack中配置热模块:

webpack.config.js

1
2
3
4
5
6
7
const webpack=require('webpack')
module.exports={
...
devServer:{
hot:true
}
}

通过上述这种配置,如果我们修改并保存CSS文件,确实能够以不刷新地形式更新到页面中,但是,当我们修改并保存js文件之后,页面依旧舒刷新了,这里并没有触发热模块,所以,HMR并不像Webpack的其他特性一样可以开箱即用,需要取指定那些模块发生更新时进行HMR

1
2
3
4
5
if(module.hot){
module.hot.accept('./util.js',()=>{
console.log('utils.js更新了')
})
}

实现原理

  • Webpack Compile:将JS源代码编译成bundle.js
  • HMR Server:用来将热更新的文件输出给HMR Runtime
  • Bundle Server:静态资源文件服务器,提供文件访问路径
  • HMR Runtime:socket服务器,会被注入到浏览器,更新文件的变化
  • bundle.js:构建输出的文件
  • 在HMR Runtime和HMR Server之间建立websocket,即图上4号线,用于实时更新文件变化

分成两个阶段:

启动阶段为1-2-A-B

在编写未经过webpack打包的源码后,Webpack Compile将源码和HMR Runtime一起编译成bundle文件,传输Bundle Server静态资源服务器

更新阶段为1-2-3-4

当某一个文件或者模块发生变化时,webpack监听到文件变化对文件重新编译打包,编译生成唯一的hash值,这个hash值用来作为下一次热更新的标识,根据变化的内容生成两个补丁文件:manifest(包含了hash和chunkId,用来说明变化的内容)和chunk.js模块,由于socket服务器在HMR Runtime和HMR Server之间建立websocket链接,单文件发生改动的时候,服务端会向浏览器推送一条消息,消息包含文件改动后生成的hash值,如下图的h属性,做为下一次热更新的标识

在浏览器接收到这条消息之前,浏览器已经在上一次socket消息中记住了此时的hash标识,这时候我们会创建一个ajax去服务端请求获取到变化内容的manifest文件

manifest文件包含重新build生成的hash值,以及变化的模块,对应上图的c属性,浏览器根据manifest文件获取模块变化的内容,从而触发render流程,实现局部模块更新

总结

关于webpack热模块更新总结:

  • 通过webpack-dev-server创建两个服务器,提供静态资源的服务(express)和Socket服务
  • express server负责直接提供静态资源的服务(打包后的资源直接被浏览器请求和解析)
  • socket server是一个websocket的长连接,双方可以通信
  • 当socket server监听到对应的模块发生变化时,会生成两个文件.json(manifest文件)和.js文件(update chunk)
  • 通过长连接,socket server可以直接将这两个文件主动发送给客户端(浏览器)
  • 浏览器拿到两个新的文件后,通过HMR runtime机制,加载这两个文件,并且针对修改的模块进行更新

object.freeze()=不可扩展(preventExtensions)+不可配置(configurable:false)+不可写(writable:fasle)

object.seal()=不可扩展+不可配置

preventExtensions=不可添加属性+不可改__proto__

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const _objectFreeze = object => {
// 补全代码
//Object.defineProperty(object,)
const props=Object.getOwnPropertyNames(object);
props.forEach(prop=>{
const des=Object.getOwnPropertyDescriptor(object,prop);
if(des.get||des.set){
Object.defineProperty(object,prop,{
configurable:false,//小写
writable:false,
get:des.get,
set:des.set

})
}else{
Object.defineProperty(object,prop,{
configurable:false,
writable:false,

})

}

})
return Object.preventExtensions(object);

}

数据属性的值不可更改,访问器属性(有getter和setter)也同样(但由于是函数调用,给人的错觉是还是可以修改这个属性)。如果一个属性的值是个对象,则这个对象中的属性是可以修改的,除非它也是个冻结对象。数组作为一种对象,被冻结,其元素不能被修改。没有数组元素可以被添加或移除。

这个方法返回传递的对象,而不是创建一个被冻结的副本。

匹配邮箱:

{username@hostname.com|edu|info)

1
/^[A-Za-z0-9]+([-.][A-Za-z0-9]+)*@[A-Za-z0-9]+(\.[A-Za-z0-9]+)*\.[A-Za-z]{2,6}$/

用户名可以包含点号和连字符,但是用户名以字符开头因此用/\w+[-.\w+]*,但是\w会匹配ASCII字母和数字,但有些系统中\w能够匹配非ASCII字母,,因此将\w换成[-A-Za-z0-9],结尾的域名一般在2-6个字符

匹配URL

比如匹配http或者https开头的url

1
/^((http|https)):\/\/)?[-A-Za-z0-9]+(\.[A-Za-z0-9]+)*\.[A-Za-z]{2,6}[a-zA-Z0-9_:\@&?=+,.!~%*\$]*(?<![.,?!])$/

主机名仍然为[A-Za-z0-9]+(.[A-Za-z0-9]+)*.[A-Za-z]{2,6},路径名[a-zA-Z0-9_:@&?=+,.!~%*$]*可有可无,结尾不能是./?!这些符号

或者

根据题目要求判断参数URL是否合法。首先URL结构一般包括协议、主机名、主机端口、路径、请求信息、哈希,而本题协议已给出为HTTP(S),使用正则匹配URL,核心步骤有:

  1. 首先必须是以http(s)开头并且可以不包含协议头部信息
  2. 主机名可以使用”-“符号,所以两种情况都要判断,包含”-“或不包含”-“
  3. 顶级域名很多,直接判断”.”之后是否为字母即可
  4. 最后判断端口、路径和哈希,这些参数可有可无

参考答案

1
2
3
4
const _isUrl = url => {
return /^((http|https):\/\/)?(([A-Za-z0-9]+-[A-Za-z0-9]+|[A-Za-z0-9]+)\.)+([A-Za-z]+)(:\d+)?(\/.*)?(\?.*)?(#.*)?$/.test(url)
}

匹配手机号

手机号一般是1开头,第二位只能是3-9,最多9个数字

1
/^1[3-9]\d{9}$}

Dom事件流

DOM2事件流分为3个阶段:事件捕获,到达目标和事件冒泡,事件捕获最先发生,为提前拦截事件提供了可能,然后,实际的目标元素接收到事件后,最后一个阶段是冒泡,最迟要在这个阶段响应事件。

实际目标(div元素)在补货阶段不会接收到事件,这是因为捕获阶段从document到html到body就结束,下一个阶段,即会在div元素上触发事件的‘到达目标阶段,通常在事处理中被认为是事件冒泡的第一部分,然后,冒泡阶段开始,事件反转传播到文档

事件处理程序

HTML事件处理程序:

这种交互能力是通过为onclick属性指定JavaScript代码值实现。

showMessage()函数时单独在script元素中定义,也可以在外部文件定义,作为事件处理程序的代码可以访问全局作用域中的一切

1
2
3
4
5
6
<script>
function showMessage(){
console.log('hello world')
}
</script>
<input type='button' value='Click me' onclick="showMessage()"/>

DOM0事件处理程序

每个元素(包括window和document)都有通常小写的事件处理程序属性,比如onclick,只要把这个属性赋值为一个函数即可

1
2
3
4
5
let btn=document.getElementById('myBtn');
btn.onclick=function(){
console.log(this.id)//myBtn

}

所赋函数被视为元素的方法,因此,事件处理程序会在元素的作用域中运行,即this等于元素以这种方式添加事件处理程序时注册在事件流的冒泡阶段

通过将事件处理程序属性的值设置为null,可以移除通过Dom0添加的事件处理程序

DOM2事件处理程序

DOM2 Events为事件处理程序的赋值和移除定义了两个方法:addEventListener()和removeEventListener(),它们接收3个参数:事件名,事件处理函数和一个布尔值,true表示在捕获阶段调用事件处理程序,false(默认值)表示在冒泡阶段调用事件处理程序

1
2
3
4
let btn=document.getElementById("myBtn")
btn.addEventListener('click',()=>{
console.log(this.id)
},false)

使用DOM2的主要优势是可以为同一个事件添加多个事件处理程序

通过addEventListener()添加的事件处理程序通过removeEventListener()移除要传入与添加时同样的参数,因此无法使用addEventListener()添加的匿名函数移除

1
2
3
4
5
6
7
8
let btn=document.getElementById("myBtn")
btn.addEventListener('click',()=>{
console.log(this.id)
},false)
btn.removeEventListener('click',()=>{
console.log(this.id)
},false)//没有效果

1
2
3
4
5
6
let handler=()=>{
console.log(this.id)
}
let btn=document.getElementById("myBtn")
btn.addEventListener('click',handler,false);
btn.removeEventListener('click',handler,false)//有效果

事件对象

DOM事件对象

event对象时传给事件处理程序唯一参数

2

在事件处理程序内部,this对象始终等于currentTarget的值,而target只包含事件的实际目标,如果事件处理程序直接添加在了意图目标,则this,currentTarget,target的值一样

如果添加在父结点上,则不一样

1
2
3
4
5
document.body.onclick=function(e){
console.log(e.currentTarget===document.body);//true
console.log(this===document.body)//true
console.log(e.target===document.getElementById('myBtn'))//true
}
  1. this和currentTarget等于document.body:它是注册事件处理程序的元素
  2. target属性等于按钮本身:按钮时click的真正目标
  3. 由于按钮本身没有注册事件处理程序,因此click事件冒泡到document.body,触发了在它上面注册的处理程序

type属性可以处理多个事件

preventDefault()方法用于阻止特定事件的默认动作,比如链接的默认行为是在被单击时导航到href属性指定的URL,阻止这个导航可以在onclick事件处理程序中取消,通过preventDefault()取消默认行为,事件对象的cancleable属性被设为true

stopPropagation()方法用于立即阻止事件流在DOM结构中传播,取消后序事件的捕获或冒泡。

1
2
3
4
5
6
7
8
9
let btn=document.getElementById("myBtn")
btn.addEventListener('click',()=>{
console.log(this.id)
event.stopPropagation();
})
document.body.onclick=function(){
console.log('body click')
}

由于click事件不会传播到document.body,因此onclick事件处理程序永远不会执行

eventPhase用于确定事件流所处阶段,如果事件处理程序在捕获阶段被调用,则eventphase等于1,到达目标阶段为2,冒泡阶段为3

1
2
3
4
5
6
7
8
9
10
11
12
let btn=document.getElementById('myBtn')
btn.onclick=function(e){
console.log(e.eventPhase);//2
}
document.body.addEventListener('click',(e)=>{
console.log(e.eventPhase);//1
})
document.body.onclick=function(e){
console.log(e.eventPhase);//3


}

一、数据类型存储

前面文章我们讲到,JavaScript中存在两大数据类型:

  • 基本类型
  • 引用类型

基本类型数据保存在在栈内存中

引用类型数据保存在堆内存中,引用数据类型的变量是一个指向堆内存中实际对象的引用,存在栈中

二、浅拷贝

浅拷贝,指的是创建新的数据,这个数据有着原始数据属性值的一份精确拷贝

如果属性是基本类型,拷贝的就是基本类型的值。如果属性是引用类型,拷贝的就是内存地址

即浅拷贝是拷贝一层,深层次的引用类型则共享内存地址

下面简单实现一个浅拷贝

1
2
3
4
5
6
7
8
9
function shallowClone(obj){
let newObj={};
for(let prop in obj){
if(obj.hasOwnProperty(prop)){
newObj.prop=obj.prop;
}
}
return newObj;
}

JavaScript中,存在浅拷贝的现象有:

  • Object.assign
  • Array.prototype.slice(), Array.prototype.concat()
  • 使用拓展运算符实现的复制

Object.assign()

1
2
3
4
5
6
7
8
9
10
11
12
var obj = {
age: 18,
nature: ['smart', 'good'],
names: {
name1: 'fx',
name2: 'xka'
},
love: function () {
console.log('fx is a great girl')
}
}
var newObj = Object.assign({}, fxObj);

slice()

1
2
3
4
5
const fxArr = ["One", "Two", "Three"]
const fxArrs = fxArr.slice(0)
fxArrs[1] = "love";
console.log(fxArr) // ["One", "Two", "Three"]
console.log(fxArrs) // ["One", "love", "Three"]

concat()

1
2
3
4
5
const fxArr = ["One", "Two", "Three"]
const fxArrs = fxArr.concat()
fxArrs[1] = "love";
console.log(fxArr) // ["One", "Two", "Three"]
console.log(fxArrs) // ["One", "love", "Three"]

拓展运算符

1
2
3
4
5
const fxArr = ["One", "Two", "Three"]
const fxArrs = [...fxArr]
fxArrs[1] = "love";
console.log(fxArr) // ["One", "Two", "Three"]
console.log(fxArrs) // ["One", "love", "Three"]

三、深拷贝

深拷贝开辟一个新的栈,两个对象属完成相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性

常见的深拷贝方式有:

  • _.cloneDeep()
  • jQuery.extend()
  • JSON.stringify()
  • 手写循环递归

_.cloneDeep()

1
2
3
4
5
6
7
8
const _ = require('lodash');
const obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
const obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);// false

jQuery.extend()

1
2
3
4
5
6
7
8
const $ = require('jquery');
const obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
const obj2 = $.extend(true, {}, obj1);
console.log(obj1.b.f === obj2.b.f); // false

JSON.stringify()

1
const obj2=JSON.parse(JSON.stringify(obj1));

但是这种方式存在弊端,会忽略undefinedsymbol函数,而且无法深拷贝循环引用

1
2
3
4
5
6
7
8
const obj = {
name: 'A',
name1: undefined,
name3: function() {},
name4: Symbol('A')
}
const obj2 = JSON.parse(JSON.stringify(obj));
console.log(obj2); // {name: "A"}

循环递归

这里用WeakMap原因:WeakMap的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。

关于WeakMap看

[]: https://www.bookstack.cn/read/es6-3rd/spilt.4.docs-set-map.md

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const cloneDeep1 = (target, hash = new WeakMap()) => {
// 对于传入参数处理
if (typeof target !== 'object' || target === null) {
return target;
}
// 哈希表中存在直接返回
if (hash.has(target)) return hash.get(target);

const cloneTarget = Array.isArray(target) ? [] : {};
hash.set(target, cloneTarget);

// 针对Symbol属性
const symKeys = Object.getOwnPropertySymbols(target);
if (symKeys.length) {
symKeys.forEach(symKey => {
if (typeof target[symKey] === 'object' && target[symKey] !== null) {
cloneTarget[symKey] = cloneDeep1(target[symKey]);
} else {
cloneTarget[symKey] = target[symKey];
}
})
}

for (const i in target) {
if (Object.prototype.hasOwnProperty.call(target, i)) {//调用原型链上的hasOwnProperty方法,当我们将一个属性值定义为hasOwnProperty时能够准确调用原型链上的方法
cloneTarget[i] =
typeof target[i] === 'object' && target[i] !== null
? cloneDeep1(target[i], hash)
: target[i];
}
}
return cloneTarget;
}

小结

前提为拷贝类型为引用类型的情况下:

  • 浅拷贝是拷贝一层,属性为对象时,浅拷贝是复制,两个对象指向同一个地址
  • 深拷贝是递归拷贝深层次,属性为对象时,深拷贝是新开栈,两个对象指向不同的地址

一、line-height是什么
line-height 属性设置行间的距离(行高),说的直白一点,就是设置两段段文本之间的距离如果我们把一段文本的line-height设置为父容器的高度就可以实现文本垂直居中了。

二、分析其原理
首先来看个图

如图,每一行文字,可看成由上间距、文本内容、下间距构成,根据行高的标准定义,行高等于两条基线之间的距离,即第一行的3-4+上下间距+第二行的1-2+2-3,因为css中每一行的上间距和下间距肯定是相等的,所以代换一下,行高就等于它本身的上间距+下间距+文本高度。因此,我们也可以把行高记为,行高就是一行的高度,这一行的高度中包含了上下两个间距和文本内容本身。而文本内容在每一行中都是居中的,所以利用这个原理,就可以实现垂直居中。

看个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<style type="text/css">
.container {
width: 100px;
height: 200px;
line-height: 200px;
margin: 0 auto;
border: 1px solid red;
}
</style>
</head>
<body>
<div class="container">
哈哈哈
</div>
</body>
</html>

如上图所示。本例子中,我们设置div的高度为200px,然后里面有一行文本,我们设置了行高为200px,设置完200px后,文字本身16px不会改变,变的是它的上间距和下间距。正如上图所描述的那样。我想到这,你就应该明白了line-height为什么可以使其垂直居中了。

为什么 canvas 认为跨域图片数据为 污染的数据

当请求跨域图片数据,而未满足跨域请求资源的条件时。如果canvas使用未经跨域允许的图片的原始数据,这些是不可信的数据,可能会暴露页面的数据。

请求图片资源 - 同域

Request Headers带有cookie。图片数据是被canvas信任的。

请求图片资源 - 跨域

默认情况下,直接请求跨域图片。因为不符合跨域请求资源的条件,图片数据是不被canvas信任的。

为了解决图片跨域资源共享的问题, 元素提供了支持的属性:crossOrigin,该属性一共有两个值可选:anonymous 和 use-credentials,下面列举了两者的使用场景,以及满足的条件。

(anonymous:匿名的)

anonymous use-credentials
用途 匿名请求跨域图片资源,不会发送证书(比如cookie等) 具名请求跨域图片资源,会携带证书数据
Request Headers origin origin、cookie
Response headers Access-Control-Allow-Origin Access-Control-Allow-Origin、Access-Control-Allow-Credentials
所需条件 Access-Control-Allow-Origin 字段值需要包含请求域。 Access-Control-Allow-Origin 字段值需要包含请求域,且不能为通配符 *。Access-Control-Allow-Credentials 字段值需要为 true,表明允许请求发送证书数据。
1
2
3
4
5
6
7
8
9
const canvas=document.createElement('canvas');
const context=canvas.getContext('2d');
const img=new Image();
img.crossOrigin='anonymous';
img.onload=()=>{
context.drawImage(this,0,0);
context.getImageData(0,0,img.width,img.height);
}
img.src="https://b.com/a.png"

实现图片懒加载

懒加载是一种网页性能优化方式,提升用户体验,比如懒加载图片,进入页面时,我们只请求可视区域的图片资源

总结:

全部加载会影响用户体验

浪费用户的流量,有些用户不想全部看完,全部加载会耗费大量流量

实现方式:

html实现

最简单的实现方式给img标签加上 loading=”lazy”

1
<img src="./example.jpg" loading="lazy">

js实现原理

  • 通过js监听页面的滚动
  • 使用js实现的原理主要是判断当前图片是否到了可视区域:
  • 拿到所有图片的dom
  • 遍历每个图片判断当前图片是否到达了可视区域范围
  • 如果到了就设置图片的src属性
  • 绑定window的scroll事件,对其进行事件监听

在页面初始化时,img图片的src放在data-src属性上,当匀速处于可视区范围,就把data-src赋值给src属性,完成图片加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Lazyload</title>
<style>
img {
display: block;
margin-bottom: 50px;
height: 200px;
width: 400px;
}
</style>
</head>
<body>
<img src="./img/default.png" data-src="./img/1.jpg" />
<img src="./img/default.png" data-src="./img/2.jpg" />
<img src="./img/default.png" data-src="./img/3.jpg" />
<img src="./img/default.png" data-src="./img/4.jpg" />
<img src="./img/default.png" data-src="./img/5.jpg" />
<img src="./img/default.png" data-src="./img/6.jpg" />
<img src="./img/default.png" data-src="./img/7.jpg" />
<img src="./img/default.png" data-src="./img/8.jpg" />
<img src="./img/default.png" data-src="./img/9.jpg" />
<img src="./img/default.png" data-src="./img/10.jpg" />
</body>
</html>

先获取所有图片的dom,通过document.body.clientHeight获取可视区高度,在使用element.getBoundingClientRect()直接得到元素相对浏览器的top值,遍历每个图片判断图片是否到达了可视区域

1
2
3
4
5
6
7
8
9
10
11
12
13
function lazyLoad(){
let viewHeight=document.body.clientHeight;//获取可视区高度
let imgs=document.querySelectorAll("img[data-src]")
imgs.forEach(item=>{
if(item.data-src=='')return;
let rect=item.getBoundingClientRect();
if(rect.top<viewHeight&&rect.bottom>=0){
item.src=item.data-src
item.removeAttribute('data-src')

}
})
}

这样存在性能问题,因为scroll事件会在很短事件内触发多次,严重影响页面性能,为了提高网页性能,我们需要一个节流函数来控制函数的多次触发,在一段时间内只执行一次回调

1
2
3
4
5
6
7
8
9
10
11
12
13
function throttle(fn,delay){
let timer=null;
return function(fn,...args){
const context=this;
if(!temer){
timer=setTimeout(()=>{
fn.apply(context,args);
timer=null;
})
}
}

}

scroll

1
window.addEventListener("scroll",throttle(lazyload,200))

拓展: IntersectionObserver

通过上面例子的实现,我们要实现懒加载都需要去监听 scroll 事件,尽管我们可以通过函数节流的方式来阻止高频率的执行函数,但是我们还是需要去计算 scrollTop,offsetHeight 等属性,有没有简单的不需要计算这些属性的方式呢,答案就是 IntersectionObserver

IntersectionObserver 是一个比较新的 API,可以自动”观察”元素是否可见,Chrome 51+ 已经支持。由于可见(visible)的本质是,目标元素与视口产生一个交叉区,所以这个 API 叫做”交叉观察器”。我们来看一下它的用法

[]: https://developer.mozilla.org/zh-CN/docs/Web/API/IntersectionObserver

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const imgs=document.querySelectorAll("img[data-src]");
const config={
rootMargin:'0px',
threshold:0
}
let observer=new IntersectionObserver((entries,self)=>{
entries.forEach((entry)=>{
if(entry.isIntersecting){
let img=entry.target;
let realsrc=img.dataset.src;
if(src){
img.src=realsrc;
img.removeAttribute('data-src')
}
self.unobserve(entry.target);//解除观察
}
})
},config)
imgs.forEach((image)=>{
observer.observe(image)
})

Object.create()

1
2
3
4
5
6
Object.creat=function(o){
var F=function(){};
F.prototype=o;
return new F();

}

内部定义一个新对象,并且让F.prototype对象赋值为引进的对象/函数o,并return一个新的对象

new

1
2
3
4
5
6
7
function(o,...args){
var o1={};
o1.__proto__=o.prototype;
var res=o.call(o1,args);
return res typeof Object?res:o1;

}

区别:

1
2
3
4
5
6
7
8
var Base=function(){
this.a=2;

}
var o1=new Base();
var o2=Object.create(Base);
console.log(o1.a);//2
console.log(o2.a);//undefined

Object.create失去了原来对象属性的访问:F创建后函数调用结束后被销毁,o2直接指向Base构造函数,o2没有指向它的prototype,因此o2.consructor不能通过prototype对找到构造函数,所以是undefined,但是如果Base是一个对象,则o2.a值为2,因为o2的[[Prototype]]指向了对象Base

1
2
3
4
5
6
7
8
var Base = function () {
this.a = 2
}
Base.prototype.a = 3;
var o1 = new Base();
var o2 = Object.create(Base);
console.log(o1.a); // 2
console.log(o2.a); // undefined

这里Base.prototype.a会去找它的[[Prototype]]上看是否有a,没有就重新设置了一个a,值为3,**而实际上o1.constructor通过默认的[[Prototype]]委托指向Base.prototype,进而委托到constructor指向的构造函数。所以这里的o1.a=2;o2.a仍旧是undefined,因为o2直接指向的是Base,没有指向它的prototype

小结

小结

比较 new Object.create
构造函数 保留原构造函数属性 丢失原构造函数属性
原型链 原构造函数prototype属性 原构造函数/(对象)本身
作用对象 function function和object