Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

关于【高级技巧】十问 (读《红宝书》) #28

Open
Sunny-lucking opened this issue Apr 20, 2022 · 0 comments
Open

关于【高级技巧】十问 (读《红宝书》) #28

Sunny-lucking opened this issue Apr 20, 2022 · 0 comments

Comments

@Sunny-lucking
Copy link
Owner

第一问:安全类型检测——typeof和instanceof 区别以及缺陷,以及解决方案

这两个方法都可以用来判断变量类型

区别:前者是判断这个变量是什么类型,后者是判断这个变量是不是某种类型,返回的是布尔值

(1)typeof

缺陷:

1.不能判断变量具体的数据类型比如数组、正则、日期、对象,因为都会返回object,不过可以判断function,如果检测对象是正则表达式的时候,在Safari和Chrome中使用typeof的时候会错误的返回"function",其他的浏览器返回的是object.

2.判断null的时候返回的是一个object,这是js的一个缺陷,判断NaN的时候返回是number

(2)instanceof
可以用来检测这个变量是否为某种类型,返回的是布尔值,并且可以判断这个变量是否为某个函数的实例,它检测的是对象的原型


let num = 1
num instanceof Number // false

num = new Number(1)
num instanceof Number // true

明明都是num,而且都是1,只是因为第一个不是对象,是基本类型,所以直接返回false,而第二个是封装成对象,所以true。

这里要严格注意这个问题,有些说法是检测目标的__proto__与构造函数的prototype相同即返回true,这是不严谨的,检测的一定要是对象才行,如:

let num = 1
num.__proto__ === Number.prototype // true
num instanceof Number // false

num = new Number(1)
num.__proto__ === Number.prototype // true
num instanceof Number // true

num.__proto__ === (new Number(1)).__proto__ // true

此外,instanceof还有另外一个缺点:如果一个页面上有多个框架,即有多个全局环境,那么我在a框架里定义一个Array,然后在b框架里去用instanceof去判断,那么该array的原型链上不可能找到b框架里的array,则会判断该array不是一个array。

解决方案:使用Object.prototype.toString.call(value) 方法去调用对象,得到对象的构造函数名。可以解决instanceof的跨框架问题,缺点是对用户自定义的类型,它只会返回[object Object]

第二问:既然提到了instanceof,那手写实现下instanceof吧

// [1,2,3] instanceof Array ---- true

// L instanceof R
// 变量R的原型 存在于 变量L的原型链上
function instance_of(L,R){    
    // 验证如果为基本数据类型,就直接返回false
    const baseType = ['string', 'number','boolean','undefined','symbol']
    if(baseType.includes(typeof(L))) { return false }
    
    let RP  = R.prototype;  //取 R 的显示原型
    L = L.__proto__;       //取 L 的隐式原型
    while(true){           // 无线循环的写法(也可以使 for(;;) )
        if(L === null){    //找到最顶层
            return false;
        }
        if(L === RP){       //严格相等
            return true;
        }
        L = L.__proto__;  //没找到继续向上一层原型链查找
    }
}

第三问:作用域安全的构造函数--当我们new一个构造函数的时候可以获得一个实例,要是我们忘记写new了呢?

例如

function Person(){
    this.name = "小红"
}

p = Person();

这会发生什么问题?,怎么解决

这样直接使用,this会映射到全局对象window上。解决方法可以是:首先确认this对象是正确类型的实例。如果不是,那么会创建新的实例并返回。请看下面的例子

function Person(){
    if(this instanceof Person){
        this.name = "小红"
    }else{
        return  new Person()
    }
}

p = Person();

第四问:谈一下惰性载入函数

在JavaScript代码中,由于浏览器之间行为的差异,多数JavaScript代码包含了大量的if语句,以检查浏览器特性,解决不同浏览器的兼容问题。例如添加事件的函数:

function addEvent (element, type, handler) {
    if (element.addEventListener) {
        element.addEventListener(type, handler, false);
    } else if (element.attachEvent) {
        element.attachEvent("on" + type, handler);
    } else {
        element["on" + type] = handler;
    }
}

每次调用addEvent()的时候,都要对浏览器所支持的能力仔细检查。首先检查是否支持addEventListener方法,如果不支持再检查是否支持attachEvent方法,如果还不支持,就用DOM 0级的方法添加事件。在调用addEvent()过程中,每次这个过程都要走一遍。其实,浏览器支持其中的一种方法就会一直支持他,就没有必要再进行其他分支的检测了,也就是说if语句不必每次都执行,代码可以运行的更快一些。解决的方案称之为惰性载入。
所谓惰性载入,就是说函数的if分支只会执行一次,之后调用函数时,直接进入所支持的分支代码。有两种实现惰性载入的方式,第一种事函数在第一次调用时,对函数本身进行二次处理,该函数会被覆盖为符合分支条件的函数,这样对原函数的调用就不用再经过执行的分支了,我们可以用下面的方式使用惰性载入重写addEvent()。


function addEvent (type, element, handler) {
    if (element.addEventListener) {
        addEvent = function (type, element, handler) {
            element.addEventListener(type, handler, false);
        }
    }
    else if(element.attachEvent){
        addEvent = function (type, element, handler) {
            element.attachEvent('on' + type, handler);
        }
    }
    else{
        addEvent = function (type, element, handler) {
            element['on' + type] = handler;
        }
    }
    return addEvent(type, element, handler);
}

在这个惰性载入的addEvent()中,if语句的每个分支都会为addEvent变量赋值,有效覆盖了原函数。最后一步便是调用了新赋函数。下一次调用addEvent()的时候,便会直接调用新赋值的函数,这样就不用再执行if语句了。

第二种实现惰性载入的方式是在声明函数时就指定适当的函数。这样在第一次调用函数时就不会损失性能了,只在代码加载时会损失一点性能。一下就是按照这一思路重写的addEvent()。

var addEvent = (function () {
    if (document.addEventListener) {
        return function (type, element, fun) {
            element.addEventListener(type, fun, false);
        }
    }
    else if (document.attachEvent) {
        return function (type, element, fun) {
            element.attachEvent('on' + type, fun);
        }
    }
    else {
        return function (type, element, fun) {
            element['on' + type] = fun;
        }
    }
})();

这个例子中使用的技巧是创建一个匿名的自执行函数,通过不同的分支以确定应该使用那个函数实现,实际的逻辑都一样,不一样的地方就是使用了函数表达式(使用了var定义函数)和新增了一个匿名函数,另外每个分支都返回一个正确的函数,并立即将其赋值给变量addEvent。

惰性载入函数的优点只执行一次if分支,避免了函数每次执行时候都要执行if分支和不必要的代码,因此提升了代码性能,至于那种方式更合适,就要看您的需求而定了。

第五问:谈一下函数节流

概念:限制一个函数在一定时间内只能执行一次。

主要实现思路 就是通过 setTimeout 定时器,通过设置延时时间,在第一次调用时,创建定时器,先设定一个变量true,写入需要执行的函数。第二次执行这个函数时,会判断变量是否true,是则返回。当第一次的定时器执行完函数最后会设定变量为false。那么下次判断变量时则为false,函数会依次运行。目的在于在一定的时间内,保证多次函数的请求只执行最后一次调用。

函数节流的代码实现

function throttle(fn,wait){
    var timer = null;
    return function(){
        var context = this;
        var args = arguments;
        if(!timer){
            timer = setTimeout(function(){
                fn.apply(context,args);
                timer = null;
            },wait)
        }
    }
}
    
function handle(){
    console.log(Math.random());
}
    
window.addEventListener("mousemove",throttle(handle,1000));

函数节流的应用场景(throttle)

  • DOM 元素的拖拽功能实现(mousemove)
  • 高频点击提交,表单重复提交
  • 搜索联想(keyup)
  • 计算鼠标移动的距离(mousemove)
  • 监听滚动事件,比如是否滑到底部自动加载更多,用throttle来判断
  • 射击游戏的 mousedown/keydown 事件(单位时间只能发射一颗子弹)
  • 监听滚动事件判断是否到页面底部自动加载更多:给 scroll 加了 debounce 后,只有用户停止滚动后,- - 才会判断是否到了页面底部;如果是 throttle 的话,只要页面滚动就会间隔一段时间判断一次.

第六问:谈一下函数防抖

概念:函数防抖(debounce),就是指触发事件后,在 n 秒内函数只能执行一次,如果触发事件后在 n 秒内又触发了事件,则会重新计算函数延执行时间。

函数防抖的要点,是需要一个 setTimeout 来辅助实现,延迟运行需要执行的代码。如果方法多次触发,则把上次记录的延迟执行代码用 clearTimeout 清掉,重新开始计时。若计时期间事件没有被重新触发,等延迟时间计时完毕,则执行目标代码。

函数防抖的代码实现

function debounce(fn,wait){
    var timer = null;
    return function(){
        if(timer !== null){
            clearTimeout(timer);
        }
        timer = setTimeout(fn,wait);
    }
}
    
function handle(){
    console.log(Math.random());
}
    
window.addEventListener("resize",debounce(handle,1000));

函数防抖的使用场景
函数防抖一般用在什么情况之下呢?一般用在,连续的事件只需触发一次回调的场合。具体有:

  • 搜索框搜索输入。只需用户最后一次输入完,再发送请求;
  • 用户名、手机号、邮箱输入验证;
  • 浏览器窗口大小改变后,只需窗口调整完后,再执行 resize 事件中的代码,防止重复渲染。

目前遇到过的用处就是这些,理解了原理与实现思路,小伙伴可以把它运用在任何需要的场合,提高代码质量。

第七问:谈一下requestAnimationFrame

动画原理
眼前所看到图像正在以每秒60次的频率刷新,由于刷新频率很高,因此你感觉不到它在刷新。而动画本质就是要让人眼看到图像被刷新而引起变化的视觉效果,这个变化要以连贯的、平滑的方式进行过渡。 那怎么样才能做到这种效果呢?

刷新频率为60Hz的屏幕每16.7ms刷新一次,我们在屏幕每次刷新前,将图像的位置向左移动一个像素,即1px。这样一来,屏幕每次刷出来的图像位置都比前一个要差1px,因此你会看到图像在移动;由于我们人眼的视觉停留效应,当前位置的图像停留在大脑的印象还没消失,紧接着图像又被移到了下一个位置,因此你才会看到图像在流畅的移动,这就是视觉效果上形成的动画。

与setTimeout相比较

理解了上面的概念以后,我们不难发现,setTimeout 其实就是通过设置一个间隔时间来不断的改变图像的位置,从而达到动画效果的。但我们会发现,利用seTimeout实现的动画在某些低端机上会出现卡顿、抖动的现象。 这种现象的产生有两个原因:

  • setTimeout的执行时间并不是确定的。在Javascript中, setTimeout 任务被放进了异步队列中,只有当主线程上的任务执行完以后,才会去检查该队列里的任务是否需要开始执行,因此 setTimeout 的实际执行时间一般要比其设定的时间晚一些。

  • 刷新频率受屏幕分辨率和屏幕尺寸的影响,因此不同设备的屏幕刷新频率可能会不同,而 setTimeout只能设置一个固定的时间间隔,这个时间不一定和屏幕的刷新时间相同。

以上两种情况都会导致setTimeout的执行步调和屏幕的刷新步调不一致,从而引起丢帧现象

requestAnimationFrame:与setTimeout相比,requestAnimationFrame最大的优势是由系统来决定回调函数的执行时机。具体一点讲,如果屏幕刷新率是60Hz,那么回调函数就每16.7ms被执行一次,如果刷新率是75Hz,那么这个时间间隔就变成了1000/75=13.3ms,换句话说就是,requestAnimationFrame的步伐跟着系统的刷新步伐走。它能保证回调函数在屏幕每一次的刷新间隔中只被执行一次,这样就不会引起丢帧现象,也不会导致动画出现卡顿的问题。

除此之外,requestAnimationFrame还有以下两个优势:

  • CPU节能:使用setTimeout实现的动画,当页面被隐藏或最小化时,setTimeout 仍然在后台执行动画任务,由于此时页面处于不可见或不可用状态,刷新动画是没有意义的,完全是浪费CPU资源。而requestAnimationFrame则完全不同,当页面处理未激活的状态下,该页面的屏幕刷新任务也会被系统暂停,因此跟着系统步伐走的requestAnimationFrame也会停止渲染,当页面被激活时,动画就从上次停留的地方继续执行,有效节省了CPU开销。

  • 函数节流:在高频率事件(resize,scroll等)中,为了防止在一个刷新间隔内发生多次函数执行,使用requestAnimationFrame可保证每个刷新间隔内,函数只被执行一次,这样既能保证流畅性,也能更好的节省函数执行的开销。一个刷新间隔内函数执行多次时没有意义的,因为显示器每16.7ms刷新一次,多次绘制并不会在屏幕上体现出来。

第八问:web计时,你知道该怎么计算首屏,白屏时间吗?

白屏时间
白屏时间指的是浏览器开始显示内容的时间。因此我们只需要知道是浏览器开始显示内容的时间点,即页面白屏结束时间点即可获取到页面的白屏时间。

计算白屏时间
因此,我们通常认为浏览器开始渲染 标签或者解析完 标签的时刻就是页面白屏结束的时间点。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>白屏</title>
  <script type="text/javascript">
    // 不兼容performance.timing 的浏览器,如IE8
    window.pageStartTime = Date.now();
  </script>
  <!-- 页面 CSS 资源 -->
  <link rel="stylesheet" href="common.css">
  <link rel="stylesheet" href="page.css">
  <script type="text/javascript">
    // 白屏时间�结束点
    window.firstPaint = Date.now();
  </script>
</head>
<body>
  <!-- 页面内容 -->
</body>
</html>

因此白屏时间则可以这样计算出:

可使用 Performance API 时:

白屏时间 = firstPaint - performance.timing.navigationStart;

不可使用 Performance API 时:

白屏时间 = firstPaint - pageStartTime; //虽然我们知道这并不准确,毕竟DNS解析,tcp三次握手等都没计算入内。

首屏时间
首屏时间是指用户打开网站开始,到浏览器首屏内容渲染完成的时间。对于用户体验来说,首屏时间是用户对一个网站的重要体验因素。通常一个网站,如果首屏时间在5秒以内是比较优秀的,10秒以内是可以接受的,10秒以上就不可容忍了。超过10秒的首屏时间用户会选择刷新页面或立刻离开。


通常计算首屏的方法有

  • 首屏模块标签标记法

  • 统计首屏内加载最慢的图片的时间

  • 自定义首屏内容计算法

    1、首屏模块标签标记法

首屏模块标签标记法,通常适用于首屏内容不需要通过拉取数据才能生存以及页面不考虑图片等资源加载的情况。我们会在 HTML 文档中对应首屏内容的标签结束位置,使用内联的 JavaScript 代码记录当前时间戳。如下所示:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>首屏</title>
  <script type="text/javascript">
    window.pageStartTime = Date.now();
  </script>
  <link rel="stylesheet" href="common.css">
  <link rel="stylesheet" href="page.css">
</head>
<body>
  <!-- 首屏可见模块1 -->
  <div class="module-1"></div>
  <!-- 首屏可见模块2 -->
  <div class="module-2"></div>
  <script type="text/javascript">
    window.firstScreen = Date.now();
  </script>
  <!-- 首屏不可见模块3 -->
  <div class="module-3"></div>
    <!-- 首屏不可见模块4 -->
  <div class="module-4"></div>
</body>
</html>

此时首屏时间等于 firstScreen - performance.timing.navigationStart;

事实上首屏模块标签标记法 在业务中的情况比较少,大多数页面都需要通过接口拉取数据才能完整展示,因此我们会使用 JavaScript 脚本来判断首屏页面内容加载情况。

2、统计首屏内图片完成加载的时间

通常我们首屏内容加载最慢的就是图片资源,因此我们会把首屏内加载最慢的图片的时间当做首屏的时间。

由于浏览器对每个页面的 TCP 连接数有限制,使得并不是所有图片都能立刻开始下载和显示。因此我们在 DOM树 构建完成后将会去遍历首屏内的所有图片标签,并且监听所有图片标签 onload 事件,最终遍历图片标签的加载时间的最大值,并用这个最大值减去 navigationStart 即可获得近似的首屏时间。

此时首屏时间等于 加载最慢的图片的时间点 - performance.timing.navigationStart;
//首屏时间尝试:
//1,获取首屏基线高度
//2,计算出基线dom元素之上的所有图片元素
//3,所有图片onload之后为首屏显示时间
//

function getOffsetTop(ele) {
    var offsetTop = ele.offsetTop;
    if (ele.offsetParent !== null) {
        offsetTop += getOffsetTop(ele.offsetParent);
    }
    return offsetTop;
}

var firstScreenHeight = win.screen.height;
var firstScreenImgs = [];
var isFindLastImg = false;
var allImgLoaded = false;
var t = setInterval(function() {
    var i, img;
    if (isFindLastImg) {
        if (firstScreenImgs.length) {
            for (i = 0; i < firstScreenImgs.length; i++) {
                img = firstScreenImgs[i];
                if (!img.complete) {
                    allImgLoaded = false;
                    break;
                } else {
                    allImgLoaded = true;
                }
            }
        } else {
            allImgLoaded = true;
        }
        if (allImgLoaded) {
            collect.add({
                firstScreenLoaded: startTime - Date.now()
            });
            clearInterval(t);
        }
    } else {
        var imgs = body.querySelector('img');
        for (i = 0; i<imgs.length; i++) {
            img = imgs[i];
            var imgOffsetTop = getOffsetTop(img);
            if (imgOffsetTop > firstScreenHeight) {
                isFindLastImg = true;
                break;
            } else if (imgOffsetTop <= firstScreenHeight && !img.hasPushed) {
                img.hasPushed = 1;
                firstScreenImgs.push(img);
            }
        }
    }
}, 0);
doc.addEventListener('DOMContentLoaded', function() {
    var imgs = body.querySelector('img');
    if (!imgs.length) {
        isFindLastImg = true;
    }
});

win.addEventListener('load', function() {
    allImgLoaded = true;
    isFindLastImg = true;
    if (t) {
        clearInterval(t);
    }
    collect.log(collect.global);
});

解释一下思路,大概就是判断首屏有没有图片,如果没图片就用domready时间,如果有图,分2种情况,图在首屏,图不在首屏,如果在则收集,并判断加载状态,加载完毕之后则首屏完成加载,如果首屏没图,找到首屏下面的图,立刻触发首屏完毕。可以想象这么做前端收集是不准的,但是可以确保最晚不会超过win load,所以应该还算有些意义。。没办法,移动端很多浏览器不支持performance api,所以土办法前端收集,想出这么个黑魔法,在基线插入节点收集也是个办法,但是不友好,而且现在手机屏幕这么多。。

3、自定义模块内容计算法

由于统计首屏内图片完成加载的时间比较复杂。因此我们在业务中通常会通过自定义模块内容,来简化计算首屏时间。如下面的做法:

  • 忽略图片等资源加载情况,只考虑页面主要 DOM
  • 只考虑首屏的主要模块,而不是严格意义首屏线以上的所有内容

实际上用performance.timing来计算首屏加载时间与白屏时间非常简单与精确。不过目前只支持IE10和chrome
贴下其API的使用

                                                   var navigationStart = performance.timing.navigationStart;
//1488984540668
console.log(navigationStart);

//Wed Mar 08 2017 22:49:44 GMT+0800 (中国标准时间)
console.log(new Date(new Date(navigationStart)));
复制代码
  redirectStart:到当前页面的重定向开始的时间。但只有在重定向的页面来自同一个域时这个属性才会有值;否则,值为0
  redirectEnd:到当前页面的重定向结束的时间。但只有在重定向的页面来自同一个域时这个属性才会有值;否则,值为0

console.log(performance.timing.redirectStart);//0
console.log(performance.timing.redirectEnd);//0
  fetchStart:开始通过HTTP GET取得页面的时间

console.log(performance.timing.fetchStart);//1488984540668
  domainLookupStart:开始査询当前页面DNS的时间,如果使用了本地缓存或持久连接,则与fetchStart值相等
  domainLookupEnd:査询当前页面DNS结束的时间,如果使用了本地缓存或持久连接,则与fetchStart值相等

console.log(performance.timing.domainLookupStart);//1488984540670
console.log(performance.timing.domainLookupEnd);//1488984540671
  connectStart:浏览器尝试连接服务器的时间
  secureConnectionStart:浏览器尝试以SSL方式连接服务器的时间。不使用SSL方式连接时,这个属性的值为0 
  connectEnd:浏览器成功连接到服务器的时间

console.log(performance.timing.connectStart);//1488984540671
console.log(performance.timing.secureConnectionStart);//0
console.log(performance.timing.connectEnd);//1488984540719
  requestStart:浏览器开始请求页面的时间
  responseStart:浏览器接收到页面第一字节的时间
  responseEnd:浏览器接收到页面所有内容的时间

console.log(performance.timing.requestStart);//1488984540720
console.log(performance.timing.responseStart);//1488984540901
console.log(performance.timing.responseEnd);//1488984540902
  unloadEventStart:前一个页面的unload事件开始的时间。但只有在前一个页面与当前页面来自同一个域时这个属性才会有值;否则,值为0
  unloadEventEnd:前一个页面的unload事件结束的时间。但只有在前一个页面与当前页面来自同一个域时这个属性才会有值;否则,值为0

console.log(performance.timing.unloadEventStart);//1488984540902
console.log(performance.timing.unloadEventEnd);//1488984540903
  domLoading:document.readyState变为"loading"的时间,即开始解析DOM树的时间
  domInteractive:document.readyState变为"interactive"的时间,即完成完成解析DOM树的时间
  domContentLoadedEventStart:发生DOMContentloaded事件的时间,即开始加载网页内资源的时间
  domContentLoadedEventEnd:DOMContentLoaded事件已经发生且执行完所有事件处理程序的时间,网页内资源加载完成的时间
  domComplete:document.readyState变为"complete"的时间,即DOM树解析完成、网页内资源准备就绪的时间

console.log(performance.timing.domLoading);//1488984540905
console.log(performance.timing.domInteractive);//1488984540932
console.log(performance.timing.domContentLoadedEventStart);//1488984540932
console.log(performance.timing.domContentLoadedEventEnd);//1488984540932
console.log(performance.timing.domComplete);//1488984540932
  loadEventStart:发生load事件的时间,也就是load回调函数开始执行的时间 
  loadEventEnd:load事件已经发生且执行完所有事件处理程序的时间

console.log(performance.timing.loadEventStart);//1488984540933
console.log(performance.timing.loadEventEnd);//1488984540933
                                                       

第九问:你知道web Worker吗?

多线程技术在服务端技术中已经发展的很成熟了,而在Web端的应用中却一直是鸡肋
在新的标准中,提供的新的WebWork API,让前端的异步工作变得异常简单。
使用:创建一个Worker对象,指向一个js文件,然后通过Worker对象往js文件发送消息,js文件内部的处理逻辑,处理完毕后,再发送消息回到当前页面,纯异步方式,不影响当前主页面渲染。

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title></title>
    <script type="text/javascript">
        //创建线程 work对象
        var work = new Worker("work.js");      //work文件中不要存在跟ui代码
        //发送消息
        work.postMessage("100");
        // 监听消息
        work.onmessage = function(event) {
            alert(event.data);
        };
    </script>
</head>
<body>

</body>
</html>          

work.js

  onmessage = function (event) {
    //从1加到num
    var num = event.data;
    var result = 0;
    for (var i = 1; i <= num; i++) {
        result += i;
    }
    postMessage(result);
}
   
@Sunny-lucking Sunny-lucking changed the title 关于【高级技巧】十问 (读红宝书) 关于【高级技巧】十问 (读《红宝书》) Apr 20, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant