芊芷鹤LY1234567

芊芷鹤LY1234567 查看完整档案

上海编辑咸阳职业技术学院  |  __ 编辑__  |  __ 编辑 github.com/qzhly1234567 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

芊芷鹤LY1234567 收藏了文章 · 3月25日

Js基础知识(三) - 作用域与闭包

作用域与闭包

如何用js创建10个button标签,点击每个按钮时打印按钮对应的序号?

看到上述问题,如果你能看出来这个问题实质上是考对作用域的理解,那么恭喜你,这篇文章你可以不用看了,说明你对作用域已经理解的很透彻了,但是如果你看不出来这是一道考作用域的题目,那么请看下文...

工作模式

在所有的语言中,作用域一般有两种主要的工作模式:词法作用域和动态作用域。词法作用域就是定义在词法阶段的作用域,是由写代码时将变量和块作用域写在哪里来决定的,不会改变。而动态作用域并不关心函数和作用域是如何声明以及在任何处声明的,只关心它们从何处调用。javascript是词法作用域工作模式。 看下面的例子体会一下:

function static () {
    var foo=1
    alert(foo)         
}      
!function () {
    var foo=2
    static(); // 如果是词法作用域会打印1,如果是动态作用域则打印2   
}();

作用域

在es6之前javascript的作用域只有全局作用域和局部作用域(函数作用域),是没有块级作用域的。在es6中提供了let,可以简单的定义一个块级作用域变量。使用let可以将变量绑定在所在的任意作用域中(通常是{...}内部),也就是说let为其声明的变量隐式的劫持了所在的块级作用域。
为了方便理解作用域,需要知道下面几个概念:

  • 自由变量:当前作用域没有的变量就称为自由变量
  • 作用域链:当前作用域没定义的变量(自由变量),会逐级向父级作用域寻找
  • 父级作用域: 哪个作用域定义了当前作用域,那就是当前作用域的父级作用域
var a = 1
function foo () {
    alert(a) // 在foo函数作用域中a就是自由变量,因为在foo中没有定义a,便向父级作用域(此为全局作用域)查找
}

作用域提升

关于作用域提升与js引擎线程运行原理有关,js引擎运行时会执行三步操作,第一步是先检查你的js代码有没有低级的语法错误,第二步是预编译,第三步是根据代码顺序解释一句执行一句。

第一步和第三步都很好理解,重点解释一下第二步预编译,所谓预编译就是在执行代码会把所有的变量声明和函数声明预先处理。当你写了一句var a = 1时,javascript会当成两个操作:var a;和a = 1;第一个是在预编译中执行的,此时只是声明了a这个变量,没有赋值操作,所以此阶段a的值为undefined。
正是因为预编译存在,所以javascript会存在作用域变量提升。看下面的例子可以更好的理解:

console.log(a) // undefined
var a = 1

//上述代码可以这样理解
var a // 此时a的值为undefined
console.log(a)
a = 1

变量提升有两点需要记住:

  • 只有声明才会被提升
foo()
foo =  function() { // 这里只是赋值表达式,不会被提升
    console.log(1)
}
function foo() { // 以function开头定义的函数才是声明,会被提升
    console.log(2)
}

// 可以这样理解

function foo() {
    console.log(2)
}
foo() // 2
foo =  function() {
    console.log(1)
}
  • 每个作用域都会提升,提升到当前作用域
foo()
function foo () {
    console.log(a) // undefinded
    var a = 1
}

//可以这样理解

function foo () {
    var a // undefined
    console.log(a)
    a = 1
}
foo()

闭包

对于闭包的定义,各种说法都有,在KYLE SIMPSON著的《你不知道的javascript》中是这样定义的:当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前此法作用域之外执行。

还有网络上不同版本的定义,还有的说在函数中定义函数,并返回函数,就是闭包。其实都差不多,各个版本都有一定的道理,但也不一定全对,因为目前还没有一个完美的、得到广泛认可的公认的定义。所以这里我们对闭包的定义也不便做更多的解释。如果你觉得有一个概念定义会对你理解闭包有帮助,我比较推荐《你不知道的javascript》中对闭包的定义。

从下面一个小例子先来认识一下闭包:

function foo () {
    var a = 1
    function fn() {
        console.log(a)
    }
    return fn()
}
var bar = foo()
bar() // 1

上述就是一个简单的闭包的例子,fn函数可以被执行,并且是在fn函数被定义的词法作用域的外面执行。

通常由于js引擎的垃圾回收机制,一个普通的函数在执行之后内部作用域以及内部变量会被销毁,垃圾机制用来回收释放不再使用内存空间。

正常来说,当foo执行之后,foo函数内部作用域会被销毁,但是闭包就会阻止垃圾回收,事实上内部作用域还存在,因为fn函数还在使用使用foo函数的内部作用域。

到现在为止应该对闭包有个初步的认识了,下面就来回过头去看看最开始预留的问题:如何用js创建10个button标签,点击每个按钮时打印按钮对应的序号?

先看一个错误的示例:

var i = 1
for (i=1;i<=10;i++){
    var btn = document.createElement("BUTTON")
    btn.innerHTML = i
    btn.addEventListener('click',function(event){
        alert(i)
    })
    document.getElementById("div").appendChild(btn)
}

大家可以把上面的代码测试一下,你会发现屏幕上出现了10个按钮,序号从0到9,但是当你点击每一个按钮的时候发现都是弹出11,这是因为当你点击按钮的时候for循环早已经执行完毕,这时i的值已经变成11,当点击执行到alert(i)的时候,发现当前作用域没有i,便去父作用域寻找i,这时i的值为11,所以会打印出11。

那么应该怎样才能达到我们想要的效果呢,我们知道IIFE函数其实也是普通函数,既然是函数就可以可以有自己的作用域,不妨利用IIFE函数来试试:

var i = 1
for (i=1;i<=10;i++){
    (function(num){
        var btn = document.createElement("BUTTON")
        btn.innerHTML = num
        btn.addEventListener('click',function(event){
            alert(num)
        })
        document.getElementById("div").appendChild(btn)
    })(i)    
}

每次循环创建一个IIFE函数,每个IIFE函数都有自己的局部作用域,这里通过向IIFE函数传值的方式在IIFE函数中创建局部变量num,每一个IIFE函数都有自己的num变量,这样在点击执行alert(num)的时候就会在当前作用域找到num。

查看原文

芊芷鹤LY1234567 赞了文章 · 3月25日

Js基础知识(三) - 作用域与闭包

作用域与闭包

如何用js创建10个button标签,点击每个按钮时打印按钮对应的序号?

看到上述问题,如果你能看出来这个问题实质上是考对作用域的理解,那么恭喜你,这篇文章你可以不用看了,说明你对作用域已经理解的很透彻了,但是如果你看不出来这是一道考作用域的题目,那么请看下文...

工作模式

在所有的语言中,作用域一般有两种主要的工作模式:词法作用域和动态作用域。词法作用域就是定义在词法阶段的作用域,是由写代码时将变量和块作用域写在哪里来决定的,不会改变。而动态作用域并不关心函数和作用域是如何声明以及在任何处声明的,只关心它们从何处调用。javascript是词法作用域工作模式。 看下面的例子体会一下:

function static () {
    var foo=1
    alert(foo)         
}      
!function () {
    var foo=2
    static(); // 如果是词法作用域会打印1,如果是动态作用域则打印2   
}();

作用域

在es6之前javascript的作用域只有全局作用域和局部作用域(函数作用域),是没有块级作用域的。在es6中提供了let,可以简单的定义一个块级作用域变量。使用let可以将变量绑定在所在的任意作用域中(通常是{...}内部),也就是说let为其声明的变量隐式的劫持了所在的块级作用域。
为了方便理解作用域,需要知道下面几个概念:

  • 自由变量:当前作用域没有的变量就称为自由变量
  • 作用域链:当前作用域没定义的变量(自由变量),会逐级向父级作用域寻找
  • 父级作用域: 哪个作用域定义了当前作用域,那就是当前作用域的父级作用域
var a = 1
function foo () {
    alert(a) // 在foo函数作用域中a就是自由变量,因为在foo中没有定义a,便向父级作用域(此为全局作用域)查找
}

作用域提升

关于作用域提升与js引擎线程运行原理有关,js引擎运行时会执行三步操作,第一步是先检查你的js代码有没有低级的语法错误,第二步是预编译,第三步是根据代码顺序解释一句执行一句。

第一步和第三步都很好理解,重点解释一下第二步预编译,所谓预编译就是在执行代码会把所有的变量声明和函数声明预先处理。当你写了一句var a = 1时,javascript会当成两个操作:var a;和a = 1;第一个是在预编译中执行的,此时只是声明了a这个变量,没有赋值操作,所以此阶段a的值为undefined。
正是因为预编译存在,所以javascript会存在作用域变量提升。看下面的例子可以更好的理解:

console.log(a) // undefined
var a = 1

//上述代码可以这样理解
var a // 此时a的值为undefined
console.log(a)
a = 1

变量提升有两点需要记住:

  • 只有声明才会被提升
foo()
foo =  function() { // 这里只是赋值表达式,不会被提升
    console.log(1)
}
function foo() { // 以function开头定义的函数才是声明,会被提升
    console.log(2)
}

// 可以这样理解

function foo() {
    console.log(2)
}
foo() // 2
foo =  function() {
    console.log(1)
}
  • 每个作用域都会提升,提升到当前作用域
foo()
function foo () {
    console.log(a) // undefinded
    var a = 1
}

//可以这样理解

function foo () {
    var a // undefined
    console.log(a)
    a = 1
}
foo()

闭包

对于闭包的定义,各种说法都有,在KYLE SIMPSON著的《你不知道的javascript》中是这样定义的:当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前此法作用域之外执行。

还有网络上不同版本的定义,还有的说在函数中定义函数,并返回函数,就是闭包。其实都差不多,各个版本都有一定的道理,但也不一定全对,因为目前还没有一个完美的、得到广泛认可的公认的定义。所以这里我们对闭包的定义也不便做更多的解释。如果你觉得有一个概念定义会对你理解闭包有帮助,我比较推荐《你不知道的javascript》中对闭包的定义。

从下面一个小例子先来认识一下闭包:

function foo () {
    var a = 1
    function fn() {
        console.log(a)
    }
    return fn()
}
var bar = foo()
bar() // 1

上述就是一个简单的闭包的例子,fn函数可以被执行,并且是在fn函数被定义的词法作用域的外面执行。

通常由于js引擎的垃圾回收机制,一个普通的函数在执行之后内部作用域以及内部变量会被销毁,垃圾机制用来回收释放不再使用内存空间。

正常来说,当foo执行之后,foo函数内部作用域会被销毁,但是闭包就会阻止垃圾回收,事实上内部作用域还存在,因为fn函数还在使用使用foo函数的内部作用域。

到现在为止应该对闭包有个初步的认识了,下面就来回过头去看看最开始预留的问题:如何用js创建10个button标签,点击每个按钮时打印按钮对应的序号?

先看一个错误的示例:

var i = 1
for (i=1;i<=10;i++){
    var btn = document.createElement("BUTTON")
    btn.innerHTML = i
    btn.addEventListener('click',function(event){
        alert(i)
    })
    document.getElementById("div").appendChild(btn)
}

大家可以把上面的代码测试一下,你会发现屏幕上出现了10个按钮,序号从0到9,但是当你点击每一个按钮的时候发现都是弹出11,这是因为当你点击按钮的时候for循环早已经执行完毕,这时i的值已经变成11,当点击执行到alert(i)的时候,发现当前作用域没有i,便去父作用域寻找i,这时i的值为11,所以会打印出11。

那么应该怎样才能达到我们想要的效果呢,我们知道IIFE函数其实也是普通函数,既然是函数就可以可以有自己的作用域,不妨利用IIFE函数来试试:

var i = 1
for (i=1;i<=10;i++){
    (function(num){
        var btn = document.createElement("BUTTON")
        btn.innerHTML = num
        btn.addEventListener('click',function(event){
            alert(num)
        })
        document.getElementById("div").appendChild(btn)
    })(i)    
}

每次循环创建一个IIFE函数,每个IIFE函数都有自己的局部作用域,这里通过向IIFE函数传值的方式在IIFE函数中创建局部变量num,每一个IIFE函数都有自己的num变量,这样在点击执行alert(num)的时候就会在当前作用域找到num。

查看原文

赞 7 收藏 11 评论 1

芊芷鹤LY1234567 关注了用户 · 3月25日

zhuqitao @zhuqitao

心有猛虎,细嗅蔷薇

关注 35

芊芷鹤LY1234567 收藏了文章 · 3月25日

JS核心知识归纳

整理进行中,如有不正,烦请提出,谢谢了。

JS基础

1.数据类型?

6基本数据类型: Undefined, Null, Boolean, Number, String ,Symbol
引用类型: Array Object Date Function

区别:
基本类型值保存在栈空间,我们通过按值来访问的。
引用类型,的值是对象,栈内存中存放地址指向堆内存中的对象。是按引用访问的。栈内存中存放的只是该对象的访问地址,在堆内存中为这个值分配空间。
1.基本数据类型不可以添加/删除属性和方法;
2.复制的方式不同;引用类型复制的时候,复制的是指针,2个变量实际指的是同一个对象。
3.函数的参数是按值传递的
检测数据类型 typeof instanceof
转型数据类型

显式:Boolean()/Number()/pareseInt()/pareseFloat()/toString()/String()
隐私: +、 赋值

区别: undefined与null 的区别

null:

是Null类型的值.
是个空值,空对象指针.
typeof null,结果为Object;
null用来表示尚未存在的对象.

undefined :

是Undefined类型的值。
typeof undefined,结果为undefined;
一个声明了变量,但未初始化值,结果就是undefined
没有返回值的函数,返回的也是undefined,
没有实参的形参也是undefined;

symbol
表示独一无二的值
Symbol 值通过Symbol函数生成
对象的属性名现在可以有两种类型,一种是原来就有的字符串,另一种就是新增的 Symbol 类型
Symbol函数前不能使用new命令,否则会报错。这是因为生成的 Symbol 是一个原始类型的值,不是对象
由于 Symbol 值不是对象,所以不能添加属性。基本上,它是一种类似于字符串的数据类型
Symbol函数的参数只是表示对当前 Symbol 值的描述,因此相同参数的Symbol函数的返回值是不相等的。因为symbol的每个值不相等。
Symbol 值不能与其他类型的值进行运算,会报错
Symbol 值可以显式转为字符串。
Symbol 值也可以转为布尔值,但是不能转为数值
Symbol 值作为对象属性名时,不能用点运算符。

2.面向对象?

何为面向对象
 万物皆对象,
 对象又有如下特点: 
 抽象:抓住核心问题
 封装:只能通过对象来访问方法
 继承:从已有的对象下继承出新的对象
 多态:多对象的不同形态
创建对象的5种方式
 1.工厂方式创建对象:面向对象中的封装函数(内置对象)
 2.构造函数创建对象
 优点:创建自定义函数意味着将来可以将它的实例标识为一种特定的类型,这是构造函数胜过工厂模式的地方
 缺点:每个方法都要在每个实例上重新创建一遍
 3、对象字面量方式创建对象
 4、用原型方式 
 1、优点:可以让所有的对象实例共享它所包含的属性和方法
 2、缺点:原型中是所有属性都是共享的,但是实例一般都是要有自己的单独属性的。所以一般很少单独使用原型模式。
 5.混合模型:构造函数模式定义实例属性,而原型模式用于定义方法和共享的属性
总结:使用上述的混合法

3.Array

数组的增加

array.push()   向数组末尾添加元素,返回的是添加后新数组的长度,原有数组改变
array.unshift()  向数组开头添加元素,返回的是添加后新数组的长度,原有数组改变
array.splice(n,m)   从索引n开始删除m个元素,把删除的内容当做新数组返回,原有数组
array.concat()
// ES6的合并数组
[...arr1, ...arr2, ...arr3]
Array.prototype.push.apply(arr1,arr2);)----将arr2追加到arr1中,返回数组的长度


数组的删除

array.pop() 删除数组的最后一项,返回的是删除的那一项,原有数组改变
array.shift() 删除数组的的第一项,返回的是删除的那一项,原有数组改变
splice(n,m,x) 从索引n开始删除m个元素,然后插入元素x,把删除的内容当做新数组返回,原有数组改变。作删除使用,x不传入数据既可。
slice(n,m) 从索引n开,到索引M 结束 ,返回删除项,原数组不变
length   减小数组的长度,实际是从数组尾部删除元素,改变原数组。

其实只有一种 splice(),但delete方法,我个人感觉算修改不属于删除,详情请见实例

clipboard.png

indeOf() 
lastIndexOf()
find()
findIndex()
includes()
[1, 4, -5, 10].find((n) => n < 0)    // -5
[1, 4, -5, 10].findIndex((n) => n < 0)    // 2
[1, 2, 3].includes(2)     // true

排序
array.reverse() 把数组倒过来排序,原有数组改变
array.sort() 可以实现由大到小或者由小到大的排序 但是直接写sort只能排序十以内的数字

3.1类型转换

数组 对象 字符串 的转换;

array -->string:

join("+") 把数组的每一项拿出来用指定分隔符隔开 ;
tostring 把数组的每一项拿出来用逗号隔开,原有数组不变

obj --> string

JSON.parse(); //可以将json字符串转换成json对象
JSON.stringify(); //可以将json对象转换成json对符串    

string-->array:

split(“,”)

obj --> array:

For… in循环赋值法
Array.from()

number --> array

Array.of()

string --> number (4种)

Number() 
parseFloat()  //  如果parseInt()和parseFloat()不能够把指定的字符串转换为数字,它们就会返回NaN  parseInt(''eleven"); // Returns Nan 
parseInt()  // parseInt()是取整,即丢弃小数部分,保留整数部分 parseInt(2.7) ==>3 
+number // var c = +'2'  typeof c ---number

string <-- number (4种)
var c = String(number)
var c = number + ''
var c = number.toString(8) // 将number 转换成8进制的数字 且c的类型是string
var c = number.toFixed(1); //数字转换为字符串,并且显示小数点后的指定的位数 例 number = 123.476,则c= 123.5"

深浅拷贝
对象和数组的拷贝有两种
浅拷贝即 拷贝了指针指向,当一个对象的值改变会影响另一个对象的值。
深拷贝, 拷贝的是真正的值。2者相互独立,互不干扰。
浅拷贝的方法4种方法
slice() concat() 赋值法 遍历
注:concat 和 slice 对一维数组 能算是深拷贝;2维的 是浅拷贝

var  a= [1,2,3,4]
b= a.concat();
c=a.concat();
b[0] = 5;
c[0] = 6;
a // [1,2,3,4]
b // [5,2,3,4]
c // [6,2,3,4]
var  aa= [[1,1],[2,2],3,4]
bb= aa.concat();
cc=aa.concat();
bb[1][0] = 5;
cc[0] = 6;
aa // [[1,1],[5,2],3,4]
b // [[1,1],[5,2],3,4]
c // [6,[5,2],3,4]
var shallowCopy = function(obj) {

// 只拷贝对象
if (typeof obj !== 'object') return;
// 根据obj的类型判断是新建一个数组还是对象
var newObj = obj instanceof Array ? [] : {};
// 遍历obj,并且判断是obj的属性才拷贝
for (var key in obj) {
    if (obj.hasOwnProperty(key)) {
        newObj[key] = obj[key];
    }
}
return newObj;
}

深拷贝的方法5种方法:
一维数组和对象的concat slice法 JSON.parse(JSON.stringify(arr)) 和遍历法 解构赋值法
示例:(前3种毕竟简单,这里也不表述)
解构赋值法:const a1 = [1, 2]; const a2 = [...a1];或者const [...a2] = a1;

var deepCopy = function(obj) {

if (typeof obj !== 'object') return;
var newObj = obj instanceof Array ? [] : {};
for (var key in obj) {
    if (obj.hasOwnProperty(key)) {
        newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key];
    }
}
return newObj;
} 


还有一些常用方法如filter() forEach() map() every() some()

详情请见:最全最细致的数组的方法整理 es5+es6

5.对象的属性

ES5的属性特性包括下面六个:
 configurable: 表示能否通过delete来删除属性从而重新定义属性,能够修改属性的特性,默认为true
 enumberable: 表示是否能通过for-in循环返回属性。默认为true
 writable: 是否可以修改属性, 默认为true
 value: 包含这个属性的数据值。读取属性值时3,从这个属性读,写入属性时,把新值保存到这个位置。默认值为undefine.
 getter: 在读取属性时,调用的函数
 setter: 在写入属性时调用的函数
 特别注意:一旦调用了Object.defineProperty方法之后,那些未定义的特性值除了configurable为false之外,其他都为undefined;

6.DOM

 DOM是针对HTML和XML文档的一个API(应用程序编程接口). DOM描绘了一个层次化的节点树, 允许开发人员添加, 移除和修改页面的某一部分.
 常用的DOM方法:
1)查找
a) getElementById(id) //通过元素Id,唯一性
b) getElementsByTagName() //通过标签名称
c) getElementsByName() //通过元素的Name属性的值(IE容错能力较强,

  会得到一个数组,其中包括id等于name值的)

2)添加、移除、替换、插入 复制
c) appendChild(node) // 可添加 可移动位置;可添加元素 可添加文本
d) removeChild(node)
e) replaceChild(取代别人的mode,已有节点) // 替换已有节点
f) insertBefore(插入节点,已有节点) //在已有的子节点前插入一个新的子节点
g) cloneNode(true/false) //深浅复制

3)创建

createDocumentFragment()    //创建一个DOM片段
 createElement()  //创建一个具体的元素 div  p  span
 createTextNode()   //创建一个文本节点

4)
i) getAttribute()
j) setAttribute()

5 常用的DOM属性
a) innerHTML 节点(元素)的文本值
b) parentNode 节点(元素)的父节点
c) childNodes
d) attributes 节点(元素)的属性节点

6.两个节点的关系

7.JS获取盒模型宽高

为了方便书写,以下用dom来表示获取的HTML的节点。

  1. dom.style.width/height

  这种方式只能取到dom元素内联样式所设置的宽高,也就是说如果该节点的样式是在style标签中或外联的CSS文件中设置的话,通过这种方法是获取不到dom的宽高的。

  1. dom.currentStyle.width/height

  这种方式获取的是在页面渲染完成后的结果,就是说不管是哪种方式设置的样式,都能获取到。
  但这种方式只有IE浏览器支持。

  1. window.getComputedStyle(dom).width/height

  这种方式的原理和2是一样的,这个可以兼容更多的浏览器,通用性好一些。

  1. dom.getBoundingClientRect().width/height

  这种方式是根据元素在视窗中的绝对位置来获取宽高的

  1. dom.offsetWidth/offsetHeight
    这个就没什么好说的了,最常用的,也是兼容最好的。

8.通用的事件侦听器

主要考核 事件处理程序 和event对象及其属性和方法

var EventUtil = {
        addHandler: function(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;
            }
        },
        removeHandler:function(element,type,handler) {//移除事件处理程序
            if(element.removeEventListener) {
                element.removeEventListener(type,handler,false)
            }
        },
        getEvent:function(event){// 获取事件对象
            return event ? event : window.event;
        },
        getTarget:function(event){// 获取事件的目标
            return event.target || event.srcElement;
        },
        preventDefault(){
            (event) =>{
                if(event.preventDefault){event.preventDefault()}
                else {event.returnValue= false}
            }
        },
        stopPropagation(){
             (event) =>{
                if(event.stopPropagation){event.stopPropagation()}
                else {event.cancelBubble= true;}
            }
        }
    }
    var list = document.getElementById('list')
    EventUtil.addHandler(list,'click',function(ev){
        event= EventUtil.getEvent(ev);
        var target = EventUtil.getTarget(event);
        alert(target.id);
    })

9.实现点击ul中li元素

方法有很多,闭包法、立即执行函数法、、事件委托法;
但本题主要考核 DOM事件流 利用事件委托,减少dom操作,提高性能

var list = document.getElementById('list')
 list.onclick = function(ev){
       var ev = ev || window.event;     
       var target = ev.target || ev.srcElement;
       console.log(target.id);
     }

10.Ajax

原生的写法

 var xhr = new XMLHttpRequest();
   xhr.onreadystatechange = function(){
       if(xhr.readyState == 4){
           console.log(xhr.status)
            if((xhr.status >= 200 && xhr.status < 300) || xhr.status ==304){
               var hh = xhr.responseText;
               var haha1 = document.getElementById('wokao');
               haha1.innerHTML = hh;
            }else{
                alert('failed11',xhr.status);
            } 
        }
    }
   xhr.open("get","http://10.10.65.109:8888/text.json",true);
   xhr.setRequestHeader("Accept", "application/json"); 
 //  xhr.responseType = "json";
   xhr.send(null);
   

何为异步? 异步与同步的概念
实现异步的方法:回调 事件 promise
Ajax最大特性:可以实现动态不刷新(局部刷新).
优点:

通过异步模式,提升了用户体验
优化了浏览器和服务器之间的传输,减少不必要的数据往返,减少了带宽占用
Ajax在客户端运行,承担了一部分本来由服务器承担的工作,减少了大用户量下的服务器负载。

缺点

1、ajax不支持浏览器back按钮。
2、安全问题 AJAX暴露了与服务器交互的细节。
3、对搜索引擎的支持比较弱。
4、破坏了程序的异常机制。
5、不容易调试。

**post 和get的区别:**

 GET请求会将参数跟在URL后进行传递,而POST请求则是作为HTTP消息的实体内容发送给WEB服务器。当然在Ajax请求中,这种区别对用户是不可见的。
 GET方式请求的数据会被浏览器缓存起来,因此其他人就可以从浏览器的历史记录中读取到这些数据,例如账号和密码等。在某种情况下,GET方式会带来严重的安全问题。而POST方式相对来说就可以避免这些问题。
 "GET方式提交的数据最多只能是1024字节",post无
 get请求和post请求在服务器端的区别:在客户端使用get请求时,服务器端使用Request.QueryString来获取参数,而客户端使用post请求时,服务器端使用Request.Form来获取参数.

**适用场景:**
POST用于创建资源,资源的内容会被编入HTTP请示的内容中。例如,处理订货表单、在数据库中加入新数据行等。

当请求无副作用时(如进行搜索),便可使用GET方法;当请求有副作用时(如添加数据行),则用POST方法。

若符合下列任一情况,则用POST方法:
  • 请求的结果有持续性的副作用,例如,数据库内添加新的数据行。
  • 若使用GET方法,则表单上收集的数据可能让URL过长。
  • 要传送的数据不是采用7位的ASCII编码。
    若符合下列任一情况,则用GET方法:
  • 请求是为了查找资源,HTML表单数据仅用来帮助搜索。
  • 请求结果无持续性的副作用。
  • 收集的数据及HTML表单内的输入字段名称的总长不超过1024个字符。
  1. 对cookie localStorage sessionStorage的理解

localStorage和sessionStorage都具有相同的操作方法,例如setItem、getItem和removeItem等

**cookie:**

 cookie是网站为了标示用户身份而储存在用户本地终端(Client Side)上的数据(通常经过加密)。
 cookie数据始终在同源的http请求中携带(即使不需要),记会在浏览器和服务器间来回传递。
 sessionStorage和localStorage不会自动把数据发给服务器,仅在本地保存。
 存储大小:
 cookie数据大小不能超过4k。
 sessionStorage和localStorage 虽然也有存储大小的限制,但比cookie大得多,可以达到5M或更大。
 有期时间:
 localStorage 存储持久数据,浏览器关闭后数据不丢失除非主动删除数据;
 sessionStorage 数据在当前浏览器窗口关闭后自动删除。
 cookie 设置的cookie过期时间之前一直有效,即使窗口或浏览器关闭
 作用域不同:
 sessionStorage不在不同的浏览器窗口中共享,即使是同一个页面;
 localStorage 在所有同源窗口中都是共享的;cookie也是在所有同源窗口中都是共享的。
 Web Storage 支持事件通知机制,可以将数据更新的通知发送给监听者。
 Web Storage 的 api 接口使用更方便。
sessionStorage用于本地存储一个会话(session)中的数据,这些数据只有在同一个会话中的页面才能访问并且当会话结束后数据也随之销毁。因此sessionStorage不是一种持久化的本地存储,仅仅是会话级别的存储。

而localStorage用于持久化的本地存储,除非主动删除数据,否则数据是永远不会过期的。
Web Storage的概念和cookie相似,区别是它是为了更大容量存储设计的。Cookie的大小是受限的,并且每次你请求一个新的页面的时候Cookie都会被发送过去,这样无形中浪费了带宽,另外cookie还需要指定作用域,不可以跨域调用。

除此之外,Web Storage拥有setItem,getItem,removeItem,clear等方法,不像cookie需要前端开发者自己封装setCookie,getCookie。
总之:cookie的作用是与服务器进行交互,作为HTTP规范的一部分而存在 ,而Web Storage仅仅是为了在本地“存储”数据而生
cookie优点:极高的扩展性和可用性 缺点: 数量和长度受限、安全问题
cookie 和session 的区别:
1、cookie数据存放在客户的浏览器上,session数据放在服务器上。
2、cookie不是很安全,别人可以分析存放在本地的COOKIE并进行COOKIE欺骗

考虑到安全应当使用session。

3、session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能

 考虑到减轻服务器性能方面,应当使用COOKIE。

4、单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie。
5、所以个人建议:

将登陆信息等重要信息存放为SESSION
其他信息如果需要保留,可以放在COOKIE中

12.IE的不同之处

IE支持currentStyle,FIrefox使用getComputStyle

IE 使用innerText,Firefox使用textContent

滤镜方面:IE:filter:alpha(opacity= num);Firefox:-moz-opacity:num

事件方面:IE:attachEvent:火狐是addEventListener

鼠标位置:IE是event.clientX;火狐是event.pageX

IE使用event.srcElement;Firefox使用event.target

IE中消除list的原点仅需margin:0即可达到最终效果;FIrefox需要设置margin:0;padding:0以及list-style:none

CSS圆角:ie7以下不支持圆角

13.this工作原理

原则,那就是this指的是调用函数的那个对象。

javascript 的this主要是看如何调用这个函数,而不是这个函数所在的作用域。obj.fn() fn中的 this 就是 obj。 fn() this是undifine, 而在js进入函数之前,会有 if(!this) { this = window} 这样的操作。

this 一共有六种不同的值:

普通函数调用,this为全局对象或是undefined

作为对象的方法,this为那个对象

new 表达式,this为以该函数为原型的新创建的对象

使用 apply/call指定 this

用bind绑定固定的this

事件处理函数中的this是当前的触发事件的DOM元素(event.currentTarget)
IE attachEvent添加的事件处理函数中this为window

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>this</title>
</head>
<body>
在严格模式下,一般的函数调用中 this 的值是 undefined。
<button onclick="console.log(this)" >
    show this
</button>
<p id="hahah">测试DOM元素(event.currentTarget)</p>
<p id="ha">测试DOM元素IE</p>
<script type="text/javascript">
var obj={a:1};
var aa = function func(a,b){ 
    console.log('this',this); 
    console.log('this',a);
    return 1;
}
var bb = function funb(){ console.log('this',this); }
bb.prototype= new aa();//

obj.f = aa;
/*
作为对象的方法 
**
*/
obj.f(123);//输出 obj 123
aa(123); //输出 window  123

(obj.f)();// obj 没懂
(a=obj.f)(); // window
(0,obj.f)(); //  window

/*
call spply bind中this
*
*
*/
aa.call('call',1);// call 1
aa.apply('apply',[1]); // apply 1
var cc = aa.bind('bind',1); 
cc(22); //bind 1
var cc1 = new cc(33)// func 1
/*
*
*new 表达式,this为以 该函数为原型 的新创建的对象
*/
var dd = new aa(11,22); // func {}
var bb1 = new bb();
/*
*
**事件处理函数中的this
**当前的触发事件的DOM元素(event.currentTarget)
**IE attachEvent添加的事件处理函数中this为window
*/
function evtHandle(e)
{
    console.log('e.currentTarget.id',this.id); // 
    console.log('e.target.id',this.id);        // 
}
var elements = document.getElementById('hahah');
var elements1 = document.getElementById('ha');
elements.addEventListener('click',evtHandle,false);
// ie专属  chrome下报错
 // elements1.attachEvent('onclick',evtHandle);

obj = { go: function() { console.info('oo',this) } };
(0 || obj.go)() // window
</script>    
</body>
</html>

14.函数

1.函数的调用方式

  1. 方法调用模型 var obj = { func : function(){};} obj.func()
  2. 函数调用模式  var aa = function(){} aa();
  3. 构造器调用模式
  4. apply/ call调用模式
  5. 立即执行函数(function(){}())

区别:函数调用模式,有函数提升的;即aa() 无需必须在var aa = function(){} 后面

2.return的含义

注:return不一定非得用在function 中,也可以直接放在html中,如:onsubmit="return false";

语法:return 表达式;
含义:语句结束函数执行,返回调用函数,而且把表达式的值作为函数的结果
通常函数经过一系列的处理后需要给外部返回一个值,这个值一般用return返回出去,也可以是说return是向函数返回返回值,并终止函数的运行.

return;

含义:即 return null 无函数返回值;
能中断方法的执行,但无法阻止事件的默认行为。
把控制权返回给页面。

return false;

    含义:相当于终止符;
    1. 一般是用来取消默认动作的。比如,终止表单提交。比如你单击一个链接除了触发你的onclick事件外
    还会触发一个默认事件就是执行页面的跳转。
    所以如果你想取消对象的默认动作(event.preventDefault();)就可以return false。
    2. return false 只在当前函数有效,不会影响其他外部函数的执行
    

retrun true;

    含义:相当于执行符。执行终止默认的事件行为
     返回正确的处理结果。


15.闭包

闭包是指有权访问另一个函数作用域中的变量的函数. 创建闭包常见方式,就是在一个函数内部创建另一个函数.
作用:
1.匿名自执行函数 (function (){ ... })(); 创建了一个匿名的函数,并立即执行它,由于外部无法引用它内部的变量,因此在执行完后很快就会被释放,关键是这种机制不会污染全局对象。
2.缓存, 可保留函数内部的值
3.实现封装
4.实现模板
5.给了js函数生成函数的能力,增加了js代码的抽象能力
缺点
1.造成内存泄露;变量内存无法被标记,导致内存不会被垃圾回收机制回收。
为什么要用
局部变量无法共享和长久的保存,而全局变量可能造成变量污染,所以我们希望有一种机制既可以长久的保存变量又不会造成全局污染。
如何使用
1.定义外层函数,封装被保护的局部变量。
2.定义内层函数,执行对外部函数变量的操作。
3.外层函数返回内层函数的对象,并且外层函数被调用,结果保存在一个全局的变量中。

var getNum;
function getCounter() { 
    var n = 1; 
    var inner = function () { return n++; }
    return inner;
}

getNum = getCounter();
console.log(getNum()); //1   2 3 4

16.call aplly bind

apply 、 call 、bind 三者都是用来改变函数的this对象的指向的;
apply 、 call 、bind 三者第一个参数都是this要指向的对象,也就是想指定的上下文;
apply 、 call 、bind 三者都可以利用后续参数传参;
bind是返回对应函数,便于稍后调用;
apply 、call 则是立即调用 。

function fn(a,b){
    console.log(this);
    console.log(a);
    console.log(b);
}
// bind(this,args...)
bf = fn.bind("Bind this",10); // 没有任何输出,也就是说没有执行这个函数
bf(); // "Bind this",10,undefined
bf(20);// “Bind this”,10,20
// 原函数不受影响
fn(1,2); //window, 1,2
bf2 = fn.bind("Bind this",1,2);
bf2(); // "Bind this",1,2

// call(this,args...)
fn.call("Call this",1) // "Call this",1,undefined
fn.call("Call this",1,2) // "Call this",1,2

// apply(this,[args])
fn.apply("Apply this",[1]) // "Apply this",1,undefined
fn.apply("Apply this",[1,2]) // "Apply this",1,2

17.js语言特性

特性,即封装、继承、多态
此处内容较多,便不详细叙述。注意一下继承的方式
继承6方式:
  1、拷贝继承:通用型 有new无new都可以用
  2、类式继承:new构造函数---利用构造函数(类)继承的方式
  3、原型继承:无new的对象---借助原型来实现对象继承对象
  4. 属性继承:调用父类的构造函数call
  5. 方法继承:用for in的形式 拷贝继承(jq也用拷贝继承)

18."use strict"

严格模式”的目的,主要有以下几个:

  • 消除Javascript语法的一些不合理、不严谨之处,减少一些怪异行为;

  - 消除代码运行的一些不安全之处,保证代码运行的安全;
  - 提高编译器效率,增加运行速度;
  - 为未来新版本的Javascript做好铺垫。
“严格模式”体现了Javascript更合理、更安全、更严谨的发展方向

19.加载方式

1.延迟加载有些 js 代码并不是页面初始化的时候就立刻需要的,而稍后的某些情况才需要的。延迟加载就是一开始并不加载这些暂时不用的js,而是在需要的时候或稍后再通过js 的控制来异步加载。
JS延迟加载有助于提高页面加载速度。

js的进程由解析和执行构成。所有的延迟加载方式都只是延迟了执行过程。解析从未停止
js的执行顺序是自上而下。
延迟加载几种方式:

defer 属性 defer的作用是,告诉浏览器,等到DOM加载完成后,再执行指定脚本。
async 属性 async的作用是,使用另一个进程下载脚本,下载时不会阻塞渲染,下载好了 就执行。
动态创建DOM方式  document.createElement 
使用jQuery的getScript方法
使用setTimeout延迟方法
让JS最后加载(放在文档尾部)

2.同步加载,又称阻塞模式,会阻止浏览器的后续处理,停止了后续的解析,因此停止了后续的文件加载(如图像)、渲染、代码执行。

3.异步加载:也就是说第一个fun请求数据时,数据还未返回时便开始执行第二个fun了
代表:ajax 回调 事件 promise

4.预加载
一种浏览器机制,使用浏览器空闲时间来预先下载/加载用户接下来很可能会浏览的页面/资源,当用户访问某个预加载的链接时,如果从缓存命中,页面就得以快速呈现
5.图片延迟加载的方式

js--进阶

1.原型链

许多OO语言支持两种继承方式:接口继承和实现继承。
接口继承只继承方法签名,实现继承则继承实际的方法。
由于函数无签名,在JavaScript中无法实现接口继承。所以只能实现方法继承。
实现继承主要依赖原型链。
基本思想:利用原型,让那个一个引用类型继承另一个引用类型的属性和方法。A.prototype= new B();让原型对象等于另一个类型的实例。
所有函数的默认原型都是Object的实例
即A继承了B ;B 继承了Object;

什么是原型对象。我们知道每个构造函数一旦创建都有prototype指针指向它的原型对象(构造函数.prototype)。而原型对象(构造函数.prototype)会默认生成一个constructor指针又指向构造函数。在创建实例时,每个实例有一个__proto__指向该原型对象。原型对象内创建的所有方法会被所有实例共享。
例:

 Function.prototype = {    
        constructor : Function,
        __proto__ : parent prototype,
        some prototype properties: ...
    };

原型对象中的方法属性是被所有实例共享的。如果含有引用类型的属性,如数组,修改person1中的数组属性,也会导致person2中的该属性发生变化。

什么是原型链?
函数的原型对象constructor默认指向函数本身,原型对象除了有原型属性外,为了实现继承,还有一个原型链指针proto,该指针指向上一层的原型对象,而上一层的原型对象的结构依然类似,这样利用proto一直指向Object的原型对象上,而Object的原型对象用Object.prototype.proto = null表示原型链的最顶端,如此变形成了javascript的原型链继承,同时也解释了为什么所有的javascript对象都具有Object的基本方法。

原型链就是创建一个构造函数,它会默认生成一个prototype属性并指向原型对象。使用下一个构造函数的原型对象作为这个构造函数的实例。即 A.prototype = new B(); 在下下一个构造函数的原型对象 = new nextFuction。这样下去就会构成一条实例与原型之间的链条,这就是原型链。

clipboard.png

2.继承

构造函数A()和实例a1 a2之间的关系;var a1 = new A(); var a2 = new A();
实例a1与实例a2是独立的
实例和原型的关系 a1.__proto__ = A.prototype
构造函数A()和其原型的关系 A.prototype.constructor=A

6种继承方式
原型链继承本质:
是子用类型B的原型等于超类型的实例,
B.prototype= new A()

构造函数继承本质:
是子用类型的构造函数内部调用A(),
B(){A.call(this,**);}

组合继承本质
原型链和构造函数的组合,原型继承方法,构造函数继承属性

原型式继承本质:基于已有对象创建一个对象,
即对象A的浅拷贝,
var b= Object.create(A)

寄生式继承本质:
创建一个用于封装继承过程的函数,
function b(A){var clone = Object.create(A) ;clone.=;return clone;}

寄生组合继承本质:
寄生式继承超类型A的原型,并将结果赋值给子类型的原型;
或对对象A的原型浅拷贝,
function B(**){

A.call(this,**)

}
function c(A,B){var clone = Object.create(A.prototype); clone.constructor = B;B.prototype=clone;) ;}

 function Person(name) {
            this.name = ['123','456','789',name];
        }
        Person.prototype.go = function(){
            return console.log(this.name)
        }
        Person.prototype.sex = ['男','女'];
        var ren = new Person();
        console.log('构造函数和原型的关系:',Person.prototype.constructor ===Person)
        console.log('实例和原型的关系',ren.__proto__ ===Person.prototype)
        
        // 原型链继承 
        // 缺点1.不能向person中传递参数;
        // 缺点2.超类型的原型属性会被其他实例共享,一个实例改变,则其他实例也改变。
        // 下面2个原型的顺序不能换  
        function Shuagnfeng(love) {
            this.love = ['fd','fddfdf']; 
        }
        Shuagnfeng.prototype = new Person();
        console.log('会误会超类型Person:',Shuagnfeng.prototype.constructor ===Person) //true
        console.log('构成原型链,',Shuagnfeng.prototype.__proto__ ===Person.prototype) //true
        Shuagnfeng.prototype.constructor ===Shuagnfeng;
        Shuagnfeng.prototype.goWork = function(){
           return  console.log('原型链继承的方法');
        }
        var child = new Person();
Shuagnfeng.prototype.name.push(121);
console.log('超类型实例2 child',child.name);

        var xiaozhang = new Shuagnfeng('dff');
        var xiaozhang1 = new Shuagnfeng('zzz');
        xiaozhang.go();
        xiaozhang.goWork();   
        xiaozhang.name.push('只对xiaozhang增加属于超类型Person中array值',22);
        xiaozhang.love.push('只对xiaozhang增加属于构造函数中array值',22);
        xiaozhang.sex.push('只对xiaozhang增加属于超类型的原型中array值');

        console.log('xiaozhang.超类型Person中name',xiaozhang.name);
        console.log('xiaozhang1.超类型Person中name',xiaozhang1.name);
        console.log('xiaozhang.构造函数中中love',xiaozhang.love);
        console.log('xiaozhang1.构造函数中love',xiaozhang1.love);
        console.log('xiaozhang.超类型的原型中sex',xiaozhang.sex);
        console.log('xiaozhang1.超类型的原型中sex',xiaozhang1.sex);


// 构造函数的继承
// 优点,不会改变引用类型的属性,能传递参数
// 缺点无法复用方法;
        function Jianbo() {
            Person.call(this,'构造函数的继承')
        }
        var xiaonie = new Jianbo();
        console.log('构造函数的继承xiaonie.name可传参数',xiaonie.name);
       // xiaonie.go(); // 不存在



// 组合继承
// 原型链继承方法、构造函数继承属性
        function Nb(love) {
            Person.call(this,'nb')
            this.love = love
        }
        Nb.prototype = new Person('12');
        console.log('12',Nb.prototype.constructor ===Person) // true 
        console.log('13',Nb.prototype.constructor ===Nb)// false 因为重写原型,会使原型失去了constructor属性
        Nb.prototype.constructor =Nb
         console.log('14',Nb.prototype.constructor ===Person) // false
        Nb.prototype.say = function(){
            console.log('say','say')
        }
        var nb= new Nb();
        var nb1= new Nb('组合继承');
        nb.name.push('xiaoniubi');
        console.log('nb.name',nb.name);
        console.log('nb1.name',nb1.name);
        console.log('nb1.love',nb1.love);
        nb.go();
        nb.say();

// 原型式继承Object.create()  将基础对象传给object()函数 
// 本质是浅复制  副本的引用类型会被改变
// 缺点:引用类型的属性会被共享  和原型链继承差不多
// 使用场景: 让一个对象与另一个对象的保持类似
function object(o){
    function F(){}
    F.prototype= o;
    return new F();
}
var animal = {
    name:'原型式继承默认值',
    friends:[1,2,3,4]
}
var haha= object(animal);
var gaga = Object.create(animal);
var haha1= object(animal);
console.log('haha',haha.name);
console.log('gaga',gaga.name);
haha.name='原型式继承name值变了';
haha.friends.push('原型式继承array值变了');
console.log('haha',haha.name);
console.log('haha1',haha1.name);
console.log('haha',haha.friends);
console.log('haha1',haha1.friends);

// 寄生式继承
// 能继承方法,但方法不能复用
function createAnother(original){
 var clone =  object(original);
 clone.say = function(){
    console.log('寄生式继承方法')
 }
  return clone;
}

var hh = createAnother(animal);
hh.name='寄生式继承'
console.log('hh',hh.name);
hh.say()       

// 寄生组合式
// 所有方式中最有效的方式 只调用继承的构造函数一次




// 判断实例和原型的关系  使用instanceof 和 isPrototypeOf()
        console.log('实例和原型的关系',xiaozhang instanceof Shuagnfeng);
        console.log('实例和原型的关系',xiaozhang instanceof Person);
        console.log('实例和原型的关系',xiaozhang instanceof Object);
        console.log('实例和原型的关系',Object.prototype.isPrototypeOf(xiaozhang));
        console.log(xiaozhang.__proto__); 

3.事件委托

事件处理程序 获取事件对象 事件目标
好处 减少DOM 操作 ,减少性能

<div id="list">
    <img id="1" data-original="1.png">
    <img id="3.2.2" data-original="3.2.2.png">
    <img id="3.2" data-original="3.2.png">
</div>

    <script type="text/javascript">
    var EventUtil = {
        addHandler: function(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;
            }
        },
        removeHandler:function(element,type,handler) {
            if(element.removeEventListener) {
                element.removeEventListener(type,handler,false)
            }
        },
        getEvent:function(event){
            return event ? event : window.event;
        },
        getTarget:function(event){
            return event.target || event.srcElement;
        },
    }
    var list = document.getElementById('list')
    EventUtil.addHandler(list,'click',function(ev){
        event= EventUtil.getEvent(ev);
        var target = EventUtil.getTarget(event);
        alert(target.id);
    })
    //    list.onclick = function(ev){
//          var ev = ev || window.event;
//     var target = ev.target || ev.srcElement;
//         console.log(target.id);
//     }
    </script>

4.跨域

造成跨域的原因
浏览器的同源策略,即XMLHttpRequest(XHR)对象只能访问同一域中的资源
这是种防止恶意行为的安全策略。
第二个:浏览器中不同域的框架之间是不能进行js的交互操作的。

何谓同源:URL由协议、域名、端口和路径组成,如果两个URL的协议、域名和端口相同,则表示它们同源。

在浏览器中,<script>、<img>、<iframe>、<link>等标签属于DOM, 非XHR对象,是可以加载跨域资源

跨域方法(实践中后两种最常用,所以重点介绍):
(1) 通过jsonp跨域
ajax请求受同源策略影响,不允许进行跨域请求,而script标签src属性中的链接却可以访问跨域的js脚本,利用这个特性,服务端不再返回JSON格式的数据,而是返回一段调用某个函数的js代码,在src中进行了调用,这样实现了跨域。
(2) 通过修改document.domain来跨子域
(3) 使用window.name来进行跨域
(4) 使用HTML5中新引进的window.postMessage方法来跨域传送数据
(5) 使用代理服务器,使用代理方式跨域更加直接,因为同源限制是浏览器实现的。如果请求不是从浏览器发起的,就不存在跨域问题了。

使用本方法跨域步骤如下:
1. 把访问其它域的请求替换为本域的请求
2. 服务器端的动态脚本负责将本域的请求转发成实际的请求

为了通过Ajax从http://localhost:8080访问http://localhost:8081/api,可以将请求发往http://localhost:8080/api。
然后利用Apache Web服务器的Reverse Proxy功能做如下配置:ProxyPass /api http://localhost:8081/api

(6) CORS全称是"跨域资源共享"(Cross-origin resource sharing),CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能 发送请求时,附加一个额外的Origin头部
IE:XDR(XDomainRequest) 创建一个xdr实例,调用open() ,再send()方法;
其他的,原生的支持,使用绝对的URL即可。

附:ajax的扩展,comment/Web Sockets
(7) fetch api

5.输入url之后,到底发生了什么?

clipboard.png

6 JS引擎

浏览器内核又可以分成两部分:
渲染引擎(layout engineer或者RenderingEngine)和JS引擎。
JS的引擎深入分析链接描述
10分钟理解JS引擎的执行机制
http://www.ruanyifeng.com/blo...
JS引擎负责对JavaScript进行解释、编译和执行,以使网页达到一些动态的效果。

js的几种引入方式
js引擎是单线程 异步的
--- 任务队列 事件 和回调函数 Event Loop
是通过的事件循环(event loop),实现单线程和异步的。
单线程:同一时刻只能执行一个代码块
将要执行的代码放在任务队列中,但js引擎执行代码块结束,事件循环会执行任务队列中的下一个任务。
Event Loop 负责监控代码执行和管理任务队列。
异步的,即可通过事件 回调等方式,向任务队列中添加新任务。

JS的执行机制是

首先判断JS是同步还是异步,同步就进入主进程,异步就进入event table
异步任务在event table中注册函数,当满足触发条件后,被推入event queue
同步任务进入主线程后一直执行,直到主线程空闲时,才会去event queue中查看是否有可执行的异步任务,如果有就推入主进程中
以上三步循环执行,这就是event loop

准确的划分方式是:
macro-task(宏任务):包括整体代码script,setTimeout,setInterval
micro-task(微任务):Promise,process.nextTick
按照这种分类方式:JS的执行机制是

执行一个宏任务,过程中如果遇到微任务,就将其放到微任务的【事件队列】里
当前宏任务执行完成后,会查看微任务的【事件队列】,并将里面全部的微任务依次执行完
重复以上2步骤,结合event loop(1) event loop(2) ,就是更为准确的JS执行机制了。

7 错误监控
前端错误的分类

  1. 运行时错误(代码错误)
  2. 资源加载错误
  3. 接口错误

错误的捕获方式

运行时错误的捕获方式:

try...catch
window.onerror 含有详细的error信息  
    window.onerror = function(msg, url, lineNo, columnNo, error){}
window.addEventListener('error')
     window.addEventListener('error', event =>  
        { console.log('addEventListener error:' + event.target); 
        }, true); 
   

资源加载错误:

object.onerror(如img,script)
performance.getEntries()
Error事件捕获

接口错误:

    所有http请求都是基于xmlHttpRequest或者fetch封装的。
    所以要捕获全局的接口错误,方法就是封装xmlHttpRequest或者fetch

结论
1.使用window.onerror捕获JS运行时错误
2.使用window.addEventListener(‘unhandledrejection’)捕获未处理的promise reject错误
3.重写console.error捕获console.error错误
4.在跨域脚本上配置crossorigin="anonymous"捕获跨域脚本错误
window.addEventListener(‘error’)捕获资源加载错误。因为它也能捕获js运行时错误,为避免重复上报js运行时错误,此时只有event.srcElement inatanceof HTMLScriptElement或HTMLLinkElement或HTMLImageElement时才上报
5.重写window.XMLHttpRequest和window.fetch捕获请求错误

延伸:跨域的js运行错误可以捕获吗,错误提示什么,应该怎么处理?

可以。
Script error
1.在script标签增加crossorigin属性
2.设置js资源响应头Access-Control-Allow-Orgin:*

上报错误的基本原理

1. 采用Ajax通信方式上报
2. 利用Image对象上报

更多

查看原文

芊芷鹤LY1234567 赞了文章 · 3月25日

JS核心知识归纳

整理进行中,如有不正,烦请提出,谢谢了。

JS基础

1.数据类型?

6基本数据类型: Undefined, Null, Boolean, Number, String ,Symbol
引用类型: Array Object Date Function

区别:
基本类型值保存在栈空间,我们通过按值来访问的。
引用类型,的值是对象,栈内存中存放地址指向堆内存中的对象。是按引用访问的。栈内存中存放的只是该对象的访问地址,在堆内存中为这个值分配空间。
1.基本数据类型不可以添加/删除属性和方法;
2.复制的方式不同;引用类型复制的时候,复制的是指针,2个变量实际指的是同一个对象。
3.函数的参数是按值传递的
检测数据类型 typeof instanceof
转型数据类型

显式:Boolean()/Number()/pareseInt()/pareseFloat()/toString()/String()
隐私: +、 赋值

区别: undefined与null 的区别

null:

是Null类型的值.
是个空值,空对象指针.
typeof null,结果为Object;
null用来表示尚未存在的对象.

undefined :

是Undefined类型的值。
typeof undefined,结果为undefined;
一个声明了变量,但未初始化值,结果就是undefined
没有返回值的函数,返回的也是undefined,
没有实参的形参也是undefined;

symbol
表示独一无二的值
Symbol 值通过Symbol函数生成
对象的属性名现在可以有两种类型,一种是原来就有的字符串,另一种就是新增的 Symbol 类型
Symbol函数前不能使用new命令,否则会报错。这是因为生成的 Symbol 是一个原始类型的值,不是对象
由于 Symbol 值不是对象,所以不能添加属性。基本上,它是一种类似于字符串的数据类型
Symbol函数的参数只是表示对当前 Symbol 值的描述,因此相同参数的Symbol函数的返回值是不相等的。因为symbol的每个值不相等。
Symbol 值不能与其他类型的值进行运算,会报错
Symbol 值可以显式转为字符串。
Symbol 值也可以转为布尔值,但是不能转为数值
Symbol 值作为对象属性名时,不能用点运算符。

2.面向对象?

何为面向对象
 万物皆对象,
 对象又有如下特点: 
 抽象:抓住核心问题
 封装:只能通过对象来访问方法
 继承:从已有的对象下继承出新的对象
 多态:多对象的不同形态
创建对象的5种方式
 1.工厂方式创建对象:面向对象中的封装函数(内置对象)
 2.构造函数创建对象
 优点:创建自定义函数意味着将来可以将它的实例标识为一种特定的类型,这是构造函数胜过工厂模式的地方
 缺点:每个方法都要在每个实例上重新创建一遍
 3、对象字面量方式创建对象
 4、用原型方式 
 1、优点:可以让所有的对象实例共享它所包含的属性和方法
 2、缺点:原型中是所有属性都是共享的,但是实例一般都是要有自己的单独属性的。所以一般很少单独使用原型模式。
 5.混合模型:构造函数模式定义实例属性,而原型模式用于定义方法和共享的属性
总结:使用上述的混合法

3.Array

数组的增加

array.push()   向数组末尾添加元素,返回的是添加后新数组的长度,原有数组改变
array.unshift()  向数组开头添加元素,返回的是添加后新数组的长度,原有数组改变
array.splice(n,m)   从索引n开始删除m个元素,把删除的内容当做新数组返回,原有数组
array.concat()
// ES6的合并数组
[...arr1, ...arr2, ...arr3]
Array.prototype.push.apply(arr1,arr2);)----将arr2追加到arr1中,返回数组的长度


数组的删除

array.pop() 删除数组的最后一项,返回的是删除的那一项,原有数组改变
array.shift() 删除数组的的第一项,返回的是删除的那一项,原有数组改变
splice(n,m,x) 从索引n开始删除m个元素,然后插入元素x,把删除的内容当做新数组返回,原有数组改变。作删除使用,x不传入数据既可。
slice(n,m) 从索引n开,到索引M 结束 ,返回删除项,原数组不变
length   减小数组的长度,实际是从数组尾部删除元素,改变原数组。

其实只有一种 splice(),但delete方法,我个人感觉算修改不属于删除,详情请见实例

clipboard.png

indeOf() 
lastIndexOf()
find()
findIndex()
includes()
[1, 4, -5, 10].find((n) => n < 0)    // -5
[1, 4, -5, 10].findIndex((n) => n < 0)    // 2
[1, 2, 3].includes(2)     // true

排序
array.reverse() 把数组倒过来排序,原有数组改变
array.sort() 可以实现由大到小或者由小到大的排序 但是直接写sort只能排序十以内的数字

3.1类型转换

数组 对象 字符串 的转换;

array -->string:

join("+") 把数组的每一项拿出来用指定分隔符隔开 ;
tostring 把数组的每一项拿出来用逗号隔开,原有数组不变

obj --> string

JSON.parse(); //可以将json字符串转换成json对象
JSON.stringify(); //可以将json对象转换成json对符串    

string-->array:

split(“,”)

obj --> array:

For… in循环赋值法
Array.from()

number --> array

Array.of()

string --> number (4种)

Number() 
parseFloat()  //  如果parseInt()和parseFloat()不能够把指定的字符串转换为数字,它们就会返回NaN  parseInt(''eleven"); // Returns Nan 
parseInt()  // parseInt()是取整,即丢弃小数部分,保留整数部分 parseInt(2.7) ==>3 
+number // var c = +'2'  typeof c ---number

string <-- number (4种)
var c = String(number)
var c = number + ''
var c = number.toString(8) // 将number 转换成8进制的数字 且c的类型是string
var c = number.toFixed(1); //数字转换为字符串,并且显示小数点后的指定的位数 例 number = 123.476,则c= 123.5"

深浅拷贝
对象和数组的拷贝有两种
浅拷贝即 拷贝了指针指向,当一个对象的值改变会影响另一个对象的值。
深拷贝, 拷贝的是真正的值。2者相互独立,互不干扰。
浅拷贝的方法4种方法
slice() concat() 赋值法 遍历
注:concat 和 slice 对一维数组 能算是深拷贝;2维的 是浅拷贝

var  a= [1,2,3,4]
b= a.concat();
c=a.concat();
b[0] = 5;
c[0] = 6;
a // [1,2,3,4]
b // [5,2,3,4]
c // [6,2,3,4]
var  aa= [[1,1],[2,2],3,4]
bb= aa.concat();
cc=aa.concat();
bb[1][0] = 5;
cc[0] = 6;
aa // [[1,1],[5,2],3,4]
b // [[1,1],[5,2],3,4]
c // [6,[5,2],3,4]
var shallowCopy = function(obj) {

// 只拷贝对象
if (typeof obj !== 'object') return;
// 根据obj的类型判断是新建一个数组还是对象
var newObj = obj instanceof Array ? [] : {};
// 遍历obj,并且判断是obj的属性才拷贝
for (var key in obj) {
    if (obj.hasOwnProperty(key)) {
        newObj[key] = obj[key];
    }
}
return newObj;
}

深拷贝的方法5种方法:
一维数组和对象的concat slice法 JSON.parse(JSON.stringify(arr)) 和遍历法 解构赋值法
示例:(前3种毕竟简单,这里也不表述)
解构赋值法:const a1 = [1, 2]; const a2 = [...a1];或者const [...a2] = a1;

var deepCopy = function(obj) {

if (typeof obj !== 'object') return;
var newObj = obj instanceof Array ? [] : {};
for (var key in obj) {
    if (obj.hasOwnProperty(key)) {
        newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key];
    }
}
return newObj;
} 


还有一些常用方法如filter() forEach() map() every() some()

详情请见:最全最细致的数组的方法整理 es5+es6

5.对象的属性

ES5的属性特性包括下面六个:
 configurable: 表示能否通过delete来删除属性从而重新定义属性,能够修改属性的特性,默认为true
 enumberable: 表示是否能通过for-in循环返回属性。默认为true
 writable: 是否可以修改属性, 默认为true
 value: 包含这个属性的数据值。读取属性值时3,从这个属性读,写入属性时,把新值保存到这个位置。默认值为undefine.
 getter: 在读取属性时,调用的函数
 setter: 在写入属性时调用的函数
 特别注意:一旦调用了Object.defineProperty方法之后,那些未定义的特性值除了configurable为false之外,其他都为undefined;

6.DOM

 DOM是针对HTML和XML文档的一个API(应用程序编程接口). DOM描绘了一个层次化的节点树, 允许开发人员添加, 移除和修改页面的某一部分.
 常用的DOM方法:
1)查找
a) getElementById(id) //通过元素Id,唯一性
b) getElementsByTagName() //通过标签名称
c) getElementsByName() //通过元素的Name属性的值(IE容错能力较强,

  会得到一个数组,其中包括id等于name值的)

2)添加、移除、替换、插入 复制
c) appendChild(node) // 可添加 可移动位置;可添加元素 可添加文本
d) removeChild(node)
e) replaceChild(取代别人的mode,已有节点) // 替换已有节点
f) insertBefore(插入节点,已有节点) //在已有的子节点前插入一个新的子节点
g) cloneNode(true/false) //深浅复制

3)创建

createDocumentFragment()    //创建一个DOM片段
 createElement()  //创建一个具体的元素 div  p  span
 createTextNode()   //创建一个文本节点

4)
i) getAttribute()
j) setAttribute()

5 常用的DOM属性
a) innerHTML 节点(元素)的文本值
b) parentNode 节点(元素)的父节点
c) childNodes
d) attributes 节点(元素)的属性节点

6.两个节点的关系

7.JS获取盒模型宽高

为了方便书写,以下用dom来表示获取的HTML的节点。

  1. dom.style.width/height

  这种方式只能取到dom元素内联样式所设置的宽高,也就是说如果该节点的样式是在style标签中或外联的CSS文件中设置的话,通过这种方法是获取不到dom的宽高的。

  1. dom.currentStyle.width/height

  这种方式获取的是在页面渲染完成后的结果,就是说不管是哪种方式设置的样式,都能获取到。
  但这种方式只有IE浏览器支持。

  1. window.getComputedStyle(dom).width/height

  这种方式的原理和2是一样的,这个可以兼容更多的浏览器,通用性好一些。

  1. dom.getBoundingClientRect().width/height

  这种方式是根据元素在视窗中的绝对位置来获取宽高的

  1. dom.offsetWidth/offsetHeight
    这个就没什么好说的了,最常用的,也是兼容最好的。

8.通用的事件侦听器

主要考核 事件处理程序 和event对象及其属性和方法

var EventUtil = {
        addHandler: function(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;
            }
        },
        removeHandler:function(element,type,handler) {//移除事件处理程序
            if(element.removeEventListener) {
                element.removeEventListener(type,handler,false)
            }
        },
        getEvent:function(event){// 获取事件对象
            return event ? event : window.event;
        },
        getTarget:function(event){// 获取事件的目标
            return event.target || event.srcElement;
        },
        preventDefault(){
            (event) =>{
                if(event.preventDefault){event.preventDefault()}
                else {event.returnValue= false}
            }
        },
        stopPropagation(){
             (event) =>{
                if(event.stopPropagation){event.stopPropagation()}
                else {event.cancelBubble= true;}
            }
        }
    }
    var list = document.getElementById('list')
    EventUtil.addHandler(list,'click',function(ev){
        event= EventUtil.getEvent(ev);
        var target = EventUtil.getTarget(event);
        alert(target.id);
    })

9.实现点击ul中li元素

方法有很多,闭包法、立即执行函数法、、事件委托法;
但本题主要考核 DOM事件流 利用事件委托,减少dom操作,提高性能

var list = document.getElementById('list')
 list.onclick = function(ev){
       var ev = ev || window.event;     
       var target = ev.target || ev.srcElement;
       console.log(target.id);
     }

10.Ajax

原生的写法

 var xhr = new XMLHttpRequest();
   xhr.onreadystatechange = function(){
       if(xhr.readyState == 4){
           console.log(xhr.status)
            if((xhr.status >= 200 && xhr.status < 300) || xhr.status ==304){
               var hh = xhr.responseText;
               var haha1 = document.getElementById('wokao');
               haha1.innerHTML = hh;
            }else{
                alert('failed11',xhr.status);
            } 
        }
    }
   xhr.open("get","http://10.10.65.109:8888/text.json",true);
   xhr.setRequestHeader("Accept", "application/json"); 
 //  xhr.responseType = "json";
   xhr.send(null);
   

何为异步? 异步与同步的概念
实现异步的方法:回调 事件 promise
Ajax最大特性:可以实现动态不刷新(局部刷新).
优点:

通过异步模式,提升了用户体验
优化了浏览器和服务器之间的传输,减少不必要的数据往返,减少了带宽占用
Ajax在客户端运行,承担了一部分本来由服务器承担的工作,减少了大用户量下的服务器负载。

缺点

1、ajax不支持浏览器back按钮。
2、安全问题 AJAX暴露了与服务器交互的细节。
3、对搜索引擎的支持比较弱。
4、破坏了程序的异常机制。
5、不容易调试。

**post 和get的区别:**

 GET请求会将参数跟在URL后进行传递,而POST请求则是作为HTTP消息的实体内容发送给WEB服务器。当然在Ajax请求中,这种区别对用户是不可见的。
 GET方式请求的数据会被浏览器缓存起来,因此其他人就可以从浏览器的历史记录中读取到这些数据,例如账号和密码等。在某种情况下,GET方式会带来严重的安全问题。而POST方式相对来说就可以避免这些问题。
 "GET方式提交的数据最多只能是1024字节",post无
 get请求和post请求在服务器端的区别:在客户端使用get请求时,服务器端使用Request.QueryString来获取参数,而客户端使用post请求时,服务器端使用Request.Form来获取参数.

**适用场景:**
POST用于创建资源,资源的内容会被编入HTTP请示的内容中。例如,处理订货表单、在数据库中加入新数据行等。

当请求无副作用时(如进行搜索),便可使用GET方法;当请求有副作用时(如添加数据行),则用POST方法。

若符合下列任一情况,则用POST方法:
  • 请求的结果有持续性的副作用,例如,数据库内添加新的数据行。
  • 若使用GET方法,则表单上收集的数据可能让URL过长。
  • 要传送的数据不是采用7位的ASCII编码。
    若符合下列任一情况,则用GET方法:
  • 请求是为了查找资源,HTML表单数据仅用来帮助搜索。
  • 请求结果无持续性的副作用。
  • 收集的数据及HTML表单内的输入字段名称的总长不超过1024个字符。
  1. 对cookie localStorage sessionStorage的理解

localStorage和sessionStorage都具有相同的操作方法,例如setItem、getItem和removeItem等

**cookie:**

 cookie是网站为了标示用户身份而储存在用户本地终端(Client Side)上的数据(通常经过加密)。
 cookie数据始终在同源的http请求中携带(即使不需要),记会在浏览器和服务器间来回传递。
 sessionStorage和localStorage不会自动把数据发给服务器,仅在本地保存。
 存储大小:
 cookie数据大小不能超过4k。
 sessionStorage和localStorage 虽然也有存储大小的限制,但比cookie大得多,可以达到5M或更大。
 有期时间:
 localStorage 存储持久数据,浏览器关闭后数据不丢失除非主动删除数据;
 sessionStorage 数据在当前浏览器窗口关闭后自动删除。
 cookie 设置的cookie过期时间之前一直有效,即使窗口或浏览器关闭
 作用域不同:
 sessionStorage不在不同的浏览器窗口中共享,即使是同一个页面;
 localStorage 在所有同源窗口中都是共享的;cookie也是在所有同源窗口中都是共享的。
 Web Storage 支持事件通知机制,可以将数据更新的通知发送给监听者。
 Web Storage 的 api 接口使用更方便。
sessionStorage用于本地存储一个会话(session)中的数据,这些数据只有在同一个会话中的页面才能访问并且当会话结束后数据也随之销毁。因此sessionStorage不是一种持久化的本地存储,仅仅是会话级别的存储。

而localStorage用于持久化的本地存储,除非主动删除数据,否则数据是永远不会过期的。
Web Storage的概念和cookie相似,区别是它是为了更大容量存储设计的。Cookie的大小是受限的,并且每次你请求一个新的页面的时候Cookie都会被发送过去,这样无形中浪费了带宽,另外cookie还需要指定作用域,不可以跨域调用。

除此之外,Web Storage拥有setItem,getItem,removeItem,clear等方法,不像cookie需要前端开发者自己封装setCookie,getCookie。
总之:cookie的作用是与服务器进行交互,作为HTTP规范的一部分而存在 ,而Web Storage仅仅是为了在本地“存储”数据而生
cookie优点:极高的扩展性和可用性 缺点: 数量和长度受限、安全问题
cookie 和session 的区别:
1、cookie数据存放在客户的浏览器上,session数据放在服务器上。
2、cookie不是很安全,别人可以分析存放在本地的COOKIE并进行COOKIE欺骗

考虑到安全应当使用session。

3、session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能

 考虑到减轻服务器性能方面,应当使用COOKIE。

4、单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie。
5、所以个人建议:

将登陆信息等重要信息存放为SESSION
其他信息如果需要保留,可以放在COOKIE中

12.IE的不同之处

IE支持currentStyle,FIrefox使用getComputStyle

IE 使用innerText,Firefox使用textContent

滤镜方面:IE:filter:alpha(opacity= num);Firefox:-moz-opacity:num

事件方面:IE:attachEvent:火狐是addEventListener

鼠标位置:IE是event.clientX;火狐是event.pageX

IE使用event.srcElement;Firefox使用event.target

IE中消除list的原点仅需margin:0即可达到最终效果;FIrefox需要设置margin:0;padding:0以及list-style:none

CSS圆角:ie7以下不支持圆角

13.this工作原理

原则,那就是this指的是调用函数的那个对象。

javascript 的this主要是看如何调用这个函数,而不是这个函数所在的作用域。obj.fn() fn中的 this 就是 obj。 fn() this是undifine, 而在js进入函数之前,会有 if(!this) { this = window} 这样的操作。

this 一共有六种不同的值:

普通函数调用,this为全局对象或是undefined

作为对象的方法,this为那个对象

new 表达式,this为以该函数为原型的新创建的对象

使用 apply/call指定 this

用bind绑定固定的this

事件处理函数中的this是当前的触发事件的DOM元素(event.currentTarget)
IE attachEvent添加的事件处理函数中this为window

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>this</title>
</head>
<body>
在严格模式下,一般的函数调用中 this 的值是 undefined。
<button onclick="console.log(this)" >
    show this
</button>
<p id="hahah">测试DOM元素(event.currentTarget)</p>
<p id="ha">测试DOM元素IE</p>
<script type="text/javascript">
var obj={a:1};
var aa = function func(a,b){ 
    console.log('this',this); 
    console.log('this',a);
    return 1;
}
var bb = function funb(){ console.log('this',this); }
bb.prototype= new aa();//

obj.f = aa;
/*
作为对象的方法 
**
*/
obj.f(123);//输出 obj 123
aa(123); //输出 window  123

(obj.f)();// obj 没懂
(a=obj.f)(); // window
(0,obj.f)(); //  window

/*
call spply bind中this
*
*
*/
aa.call('call',1);// call 1
aa.apply('apply',[1]); // apply 1
var cc = aa.bind('bind',1); 
cc(22); //bind 1
var cc1 = new cc(33)// func 1
/*
*
*new 表达式,this为以 该函数为原型 的新创建的对象
*/
var dd = new aa(11,22); // func {}
var bb1 = new bb();
/*
*
**事件处理函数中的this
**当前的触发事件的DOM元素(event.currentTarget)
**IE attachEvent添加的事件处理函数中this为window
*/
function evtHandle(e)
{
    console.log('e.currentTarget.id',this.id); // 
    console.log('e.target.id',this.id);        // 
}
var elements = document.getElementById('hahah');
var elements1 = document.getElementById('ha');
elements.addEventListener('click',evtHandle,false);
// ie专属  chrome下报错
 // elements1.attachEvent('onclick',evtHandle);

obj = { go: function() { console.info('oo',this) } };
(0 || obj.go)() // window
</script>    
</body>
</html>

14.函数

1.函数的调用方式

  1. 方法调用模型 var obj = { func : function(){};} obj.func()
  2. 函数调用模式  var aa = function(){} aa();
  3. 构造器调用模式
  4. apply/ call调用模式
  5. 立即执行函数(function(){}())

区别:函数调用模式,有函数提升的;即aa() 无需必须在var aa = function(){} 后面

2.return的含义

注:return不一定非得用在function 中,也可以直接放在html中,如:onsubmit="return false";

语法:return 表达式;
含义:语句结束函数执行,返回调用函数,而且把表达式的值作为函数的结果
通常函数经过一系列的处理后需要给外部返回一个值,这个值一般用return返回出去,也可以是说return是向函数返回返回值,并终止函数的运行.

return;

含义:即 return null 无函数返回值;
能中断方法的执行,但无法阻止事件的默认行为。
把控制权返回给页面。

return false;

    含义:相当于终止符;
    1. 一般是用来取消默认动作的。比如,终止表单提交。比如你单击一个链接除了触发你的onclick事件外
    还会触发一个默认事件就是执行页面的跳转。
    所以如果你想取消对象的默认动作(event.preventDefault();)就可以return false。
    2. return false 只在当前函数有效,不会影响其他外部函数的执行
    

retrun true;

    含义:相当于执行符。执行终止默认的事件行为
     返回正确的处理结果。


15.闭包

闭包是指有权访问另一个函数作用域中的变量的函数. 创建闭包常见方式,就是在一个函数内部创建另一个函数.
作用:
1.匿名自执行函数 (function (){ ... })(); 创建了一个匿名的函数,并立即执行它,由于外部无法引用它内部的变量,因此在执行完后很快就会被释放,关键是这种机制不会污染全局对象。
2.缓存, 可保留函数内部的值
3.实现封装
4.实现模板
5.给了js函数生成函数的能力,增加了js代码的抽象能力
缺点
1.造成内存泄露;变量内存无法被标记,导致内存不会被垃圾回收机制回收。
为什么要用
局部变量无法共享和长久的保存,而全局变量可能造成变量污染,所以我们希望有一种机制既可以长久的保存变量又不会造成全局污染。
如何使用
1.定义外层函数,封装被保护的局部变量。
2.定义内层函数,执行对外部函数变量的操作。
3.外层函数返回内层函数的对象,并且外层函数被调用,结果保存在一个全局的变量中。

var getNum;
function getCounter() { 
    var n = 1; 
    var inner = function () { return n++; }
    return inner;
}

getNum = getCounter();
console.log(getNum()); //1   2 3 4

16.call aplly bind

apply 、 call 、bind 三者都是用来改变函数的this对象的指向的;
apply 、 call 、bind 三者第一个参数都是this要指向的对象,也就是想指定的上下文;
apply 、 call 、bind 三者都可以利用后续参数传参;
bind是返回对应函数,便于稍后调用;
apply 、call 则是立即调用 。

function fn(a,b){
    console.log(this);
    console.log(a);
    console.log(b);
}
// bind(this,args...)
bf = fn.bind("Bind this",10); // 没有任何输出,也就是说没有执行这个函数
bf(); // "Bind this",10,undefined
bf(20);// “Bind this”,10,20
// 原函数不受影响
fn(1,2); //window, 1,2
bf2 = fn.bind("Bind this",1,2);
bf2(); // "Bind this",1,2

// call(this,args...)
fn.call("Call this",1) // "Call this",1,undefined
fn.call("Call this",1,2) // "Call this",1,2

// apply(this,[args])
fn.apply("Apply this",[1]) // "Apply this",1,undefined
fn.apply("Apply this",[1,2]) // "Apply this",1,2

17.js语言特性

特性,即封装、继承、多态
此处内容较多,便不详细叙述。注意一下继承的方式
继承6方式:
  1、拷贝继承:通用型 有new无new都可以用
  2、类式继承:new构造函数---利用构造函数(类)继承的方式
  3、原型继承:无new的对象---借助原型来实现对象继承对象
  4. 属性继承:调用父类的构造函数call
  5. 方法继承:用for in的形式 拷贝继承(jq也用拷贝继承)

18."use strict"

严格模式”的目的,主要有以下几个:

  • 消除Javascript语法的一些不合理、不严谨之处,减少一些怪异行为;

  - 消除代码运行的一些不安全之处,保证代码运行的安全;
  - 提高编译器效率,增加运行速度;
  - 为未来新版本的Javascript做好铺垫。
“严格模式”体现了Javascript更合理、更安全、更严谨的发展方向

19.加载方式

1.延迟加载有些 js 代码并不是页面初始化的时候就立刻需要的,而稍后的某些情况才需要的。延迟加载就是一开始并不加载这些暂时不用的js,而是在需要的时候或稍后再通过js 的控制来异步加载。
JS延迟加载有助于提高页面加载速度。

js的进程由解析和执行构成。所有的延迟加载方式都只是延迟了执行过程。解析从未停止
js的执行顺序是自上而下。
延迟加载几种方式:

defer 属性 defer的作用是,告诉浏览器,等到DOM加载完成后,再执行指定脚本。
async 属性 async的作用是,使用另一个进程下载脚本,下载时不会阻塞渲染,下载好了 就执行。
动态创建DOM方式  document.createElement 
使用jQuery的getScript方法
使用setTimeout延迟方法
让JS最后加载(放在文档尾部)

2.同步加载,又称阻塞模式,会阻止浏览器的后续处理,停止了后续的解析,因此停止了后续的文件加载(如图像)、渲染、代码执行。

3.异步加载:也就是说第一个fun请求数据时,数据还未返回时便开始执行第二个fun了
代表:ajax 回调 事件 promise

4.预加载
一种浏览器机制,使用浏览器空闲时间来预先下载/加载用户接下来很可能会浏览的页面/资源,当用户访问某个预加载的链接时,如果从缓存命中,页面就得以快速呈现
5.图片延迟加载的方式

js--进阶

1.原型链

许多OO语言支持两种继承方式:接口继承和实现继承。
接口继承只继承方法签名,实现继承则继承实际的方法。
由于函数无签名,在JavaScript中无法实现接口继承。所以只能实现方法继承。
实现继承主要依赖原型链。
基本思想:利用原型,让那个一个引用类型继承另一个引用类型的属性和方法。A.prototype= new B();让原型对象等于另一个类型的实例。
所有函数的默认原型都是Object的实例
即A继承了B ;B 继承了Object;

什么是原型对象。我们知道每个构造函数一旦创建都有prototype指针指向它的原型对象(构造函数.prototype)。而原型对象(构造函数.prototype)会默认生成一个constructor指针又指向构造函数。在创建实例时,每个实例有一个__proto__指向该原型对象。原型对象内创建的所有方法会被所有实例共享。
例:

 Function.prototype = {    
        constructor : Function,
        __proto__ : parent prototype,
        some prototype properties: ...
    };

原型对象中的方法属性是被所有实例共享的。如果含有引用类型的属性,如数组,修改person1中的数组属性,也会导致person2中的该属性发生变化。

什么是原型链?
函数的原型对象constructor默认指向函数本身,原型对象除了有原型属性外,为了实现继承,还有一个原型链指针proto,该指针指向上一层的原型对象,而上一层的原型对象的结构依然类似,这样利用proto一直指向Object的原型对象上,而Object的原型对象用Object.prototype.proto = null表示原型链的最顶端,如此变形成了javascript的原型链继承,同时也解释了为什么所有的javascript对象都具有Object的基本方法。

原型链就是创建一个构造函数,它会默认生成一个prototype属性并指向原型对象。使用下一个构造函数的原型对象作为这个构造函数的实例。即 A.prototype = new B(); 在下下一个构造函数的原型对象 = new nextFuction。这样下去就会构成一条实例与原型之间的链条,这就是原型链。

clipboard.png

2.继承

构造函数A()和实例a1 a2之间的关系;var a1 = new A(); var a2 = new A();
实例a1与实例a2是独立的
实例和原型的关系 a1.__proto__ = A.prototype
构造函数A()和其原型的关系 A.prototype.constructor=A

6种继承方式
原型链继承本质:
是子用类型B的原型等于超类型的实例,
B.prototype= new A()

构造函数继承本质:
是子用类型的构造函数内部调用A(),
B(){A.call(this,**);}

组合继承本质
原型链和构造函数的组合,原型继承方法,构造函数继承属性

原型式继承本质:基于已有对象创建一个对象,
即对象A的浅拷贝,
var b= Object.create(A)

寄生式继承本质:
创建一个用于封装继承过程的函数,
function b(A){var clone = Object.create(A) ;clone.=;return clone;}

寄生组合继承本质:
寄生式继承超类型A的原型,并将结果赋值给子类型的原型;
或对对象A的原型浅拷贝,
function B(**){

A.call(this,**)

}
function c(A,B){var clone = Object.create(A.prototype); clone.constructor = B;B.prototype=clone;) ;}

 function Person(name) {
            this.name = ['123','456','789',name];
        }
        Person.prototype.go = function(){
            return console.log(this.name)
        }
        Person.prototype.sex = ['男','女'];
        var ren = new Person();
        console.log('构造函数和原型的关系:',Person.prototype.constructor ===Person)
        console.log('实例和原型的关系',ren.__proto__ ===Person.prototype)
        
        // 原型链继承 
        // 缺点1.不能向person中传递参数;
        // 缺点2.超类型的原型属性会被其他实例共享,一个实例改变,则其他实例也改变。
        // 下面2个原型的顺序不能换  
        function Shuagnfeng(love) {
            this.love = ['fd','fddfdf']; 
        }
        Shuagnfeng.prototype = new Person();
        console.log('会误会超类型Person:',Shuagnfeng.prototype.constructor ===Person) //true
        console.log('构成原型链,',Shuagnfeng.prototype.__proto__ ===Person.prototype) //true
        Shuagnfeng.prototype.constructor ===Shuagnfeng;
        Shuagnfeng.prototype.goWork = function(){
           return  console.log('原型链继承的方法');
        }
        var child = new Person();
Shuagnfeng.prototype.name.push(121);
console.log('超类型实例2 child',child.name);

        var xiaozhang = new Shuagnfeng('dff');
        var xiaozhang1 = new Shuagnfeng('zzz');
        xiaozhang.go();
        xiaozhang.goWork();   
        xiaozhang.name.push('只对xiaozhang增加属于超类型Person中array值',22);
        xiaozhang.love.push('只对xiaozhang增加属于构造函数中array值',22);
        xiaozhang.sex.push('只对xiaozhang增加属于超类型的原型中array值');

        console.log('xiaozhang.超类型Person中name',xiaozhang.name);
        console.log('xiaozhang1.超类型Person中name',xiaozhang1.name);
        console.log('xiaozhang.构造函数中中love',xiaozhang.love);
        console.log('xiaozhang1.构造函数中love',xiaozhang1.love);
        console.log('xiaozhang.超类型的原型中sex',xiaozhang.sex);
        console.log('xiaozhang1.超类型的原型中sex',xiaozhang1.sex);


// 构造函数的继承
// 优点,不会改变引用类型的属性,能传递参数
// 缺点无法复用方法;
        function Jianbo() {
            Person.call(this,'构造函数的继承')
        }
        var xiaonie = new Jianbo();
        console.log('构造函数的继承xiaonie.name可传参数',xiaonie.name);
       // xiaonie.go(); // 不存在



// 组合继承
// 原型链继承方法、构造函数继承属性
        function Nb(love) {
            Person.call(this,'nb')
            this.love = love
        }
        Nb.prototype = new Person('12');
        console.log('12',Nb.prototype.constructor ===Person) // true 
        console.log('13',Nb.prototype.constructor ===Nb)// false 因为重写原型,会使原型失去了constructor属性
        Nb.prototype.constructor =Nb
         console.log('14',Nb.prototype.constructor ===Person) // false
        Nb.prototype.say = function(){
            console.log('say','say')
        }
        var nb= new Nb();
        var nb1= new Nb('组合继承');
        nb.name.push('xiaoniubi');
        console.log('nb.name',nb.name);
        console.log('nb1.name',nb1.name);
        console.log('nb1.love',nb1.love);
        nb.go();
        nb.say();

// 原型式继承Object.create()  将基础对象传给object()函数 
// 本质是浅复制  副本的引用类型会被改变
// 缺点:引用类型的属性会被共享  和原型链继承差不多
// 使用场景: 让一个对象与另一个对象的保持类似
function object(o){
    function F(){}
    F.prototype= o;
    return new F();
}
var animal = {
    name:'原型式继承默认值',
    friends:[1,2,3,4]
}
var haha= object(animal);
var gaga = Object.create(animal);
var haha1= object(animal);
console.log('haha',haha.name);
console.log('gaga',gaga.name);
haha.name='原型式继承name值变了';
haha.friends.push('原型式继承array值变了');
console.log('haha',haha.name);
console.log('haha1',haha1.name);
console.log('haha',haha.friends);
console.log('haha1',haha1.friends);

// 寄生式继承
// 能继承方法,但方法不能复用
function createAnother(original){
 var clone =  object(original);
 clone.say = function(){
    console.log('寄生式继承方法')
 }
  return clone;
}

var hh = createAnother(animal);
hh.name='寄生式继承'
console.log('hh',hh.name);
hh.say()       

// 寄生组合式
// 所有方式中最有效的方式 只调用继承的构造函数一次




// 判断实例和原型的关系  使用instanceof 和 isPrototypeOf()
        console.log('实例和原型的关系',xiaozhang instanceof Shuagnfeng);
        console.log('实例和原型的关系',xiaozhang instanceof Person);
        console.log('实例和原型的关系',xiaozhang instanceof Object);
        console.log('实例和原型的关系',Object.prototype.isPrototypeOf(xiaozhang));
        console.log(xiaozhang.__proto__); 

3.事件委托

事件处理程序 获取事件对象 事件目标
好处 减少DOM 操作 ,减少性能

<div id="list">
    <img id="1" data-original="1.png">
    <img id="3.2.2" data-original="3.2.2.png">
    <img id="3.2" data-original="3.2.png">
</div>

    <script type="text/javascript">
    var EventUtil = {
        addHandler: function(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;
            }
        },
        removeHandler:function(element,type,handler) {
            if(element.removeEventListener) {
                element.removeEventListener(type,handler,false)
            }
        },
        getEvent:function(event){
            return event ? event : window.event;
        },
        getTarget:function(event){
            return event.target || event.srcElement;
        },
    }
    var list = document.getElementById('list')
    EventUtil.addHandler(list,'click',function(ev){
        event= EventUtil.getEvent(ev);
        var target = EventUtil.getTarget(event);
        alert(target.id);
    })
    //    list.onclick = function(ev){
//          var ev = ev || window.event;
//     var target = ev.target || ev.srcElement;
//         console.log(target.id);
//     }
    </script>

4.跨域

造成跨域的原因
浏览器的同源策略,即XMLHttpRequest(XHR)对象只能访问同一域中的资源
这是种防止恶意行为的安全策略。
第二个:浏览器中不同域的框架之间是不能进行js的交互操作的。

何谓同源:URL由协议、域名、端口和路径组成,如果两个URL的协议、域名和端口相同,则表示它们同源。

在浏览器中,<script>、<img>、<iframe>、<link>等标签属于DOM, 非XHR对象,是可以加载跨域资源

跨域方法(实践中后两种最常用,所以重点介绍):
(1) 通过jsonp跨域
ajax请求受同源策略影响,不允许进行跨域请求,而script标签src属性中的链接却可以访问跨域的js脚本,利用这个特性,服务端不再返回JSON格式的数据,而是返回一段调用某个函数的js代码,在src中进行了调用,这样实现了跨域。
(2) 通过修改document.domain来跨子域
(3) 使用window.name来进行跨域
(4) 使用HTML5中新引进的window.postMessage方法来跨域传送数据
(5) 使用代理服务器,使用代理方式跨域更加直接,因为同源限制是浏览器实现的。如果请求不是从浏览器发起的,就不存在跨域问题了。

使用本方法跨域步骤如下:
1. 把访问其它域的请求替换为本域的请求
2. 服务器端的动态脚本负责将本域的请求转发成实际的请求

为了通过Ajax从http://localhost:8080访问http://localhost:8081/api,可以将请求发往http://localhost:8080/api。
然后利用Apache Web服务器的Reverse Proxy功能做如下配置:ProxyPass /api http://localhost:8081/api

(6) CORS全称是"跨域资源共享"(Cross-origin resource sharing),CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能 发送请求时,附加一个额外的Origin头部
IE:XDR(XDomainRequest) 创建一个xdr实例,调用open() ,再send()方法;
其他的,原生的支持,使用绝对的URL即可。

附:ajax的扩展,comment/Web Sockets
(7) fetch api

5.输入url之后,到底发生了什么?

clipboard.png

6 JS引擎

浏览器内核又可以分成两部分:
渲染引擎(layout engineer或者RenderingEngine)和JS引擎。
JS的引擎深入分析链接描述
10分钟理解JS引擎的执行机制
http://www.ruanyifeng.com/blo...
JS引擎负责对JavaScript进行解释、编译和执行,以使网页达到一些动态的效果。

js的几种引入方式
js引擎是单线程 异步的
--- 任务队列 事件 和回调函数 Event Loop
是通过的事件循环(event loop),实现单线程和异步的。
单线程:同一时刻只能执行一个代码块
将要执行的代码放在任务队列中,但js引擎执行代码块结束,事件循环会执行任务队列中的下一个任务。
Event Loop 负责监控代码执行和管理任务队列。
异步的,即可通过事件 回调等方式,向任务队列中添加新任务。

JS的执行机制是

首先判断JS是同步还是异步,同步就进入主进程,异步就进入event table
异步任务在event table中注册函数,当满足触发条件后,被推入event queue
同步任务进入主线程后一直执行,直到主线程空闲时,才会去event queue中查看是否有可执行的异步任务,如果有就推入主进程中
以上三步循环执行,这就是event loop

准确的划分方式是:
macro-task(宏任务):包括整体代码script,setTimeout,setInterval
micro-task(微任务):Promise,process.nextTick
按照这种分类方式:JS的执行机制是

执行一个宏任务,过程中如果遇到微任务,就将其放到微任务的【事件队列】里
当前宏任务执行完成后,会查看微任务的【事件队列】,并将里面全部的微任务依次执行完
重复以上2步骤,结合event loop(1) event loop(2) ,就是更为准确的JS执行机制了。

7 错误监控
前端错误的分类

  1. 运行时错误(代码错误)
  2. 资源加载错误
  3. 接口错误

错误的捕获方式

运行时错误的捕获方式:

try...catch
window.onerror 含有详细的error信息  
    window.onerror = function(msg, url, lineNo, columnNo, error){}
window.addEventListener('error')
     window.addEventListener('error', event =>  
        { console.log('addEventListener error:' + event.target); 
        }, true); 
   

资源加载错误:

object.onerror(如img,script)
performance.getEntries()
Error事件捕获

接口错误:

    所有http请求都是基于xmlHttpRequest或者fetch封装的。
    所以要捕获全局的接口错误,方法就是封装xmlHttpRequest或者fetch

结论
1.使用window.onerror捕获JS运行时错误
2.使用window.addEventListener(‘unhandledrejection’)捕获未处理的promise reject错误
3.重写console.error捕获console.error错误
4.在跨域脚本上配置crossorigin="anonymous"捕获跨域脚本错误
window.addEventListener(‘error’)捕获资源加载错误。因为它也能捕获js运行时错误,为避免重复上报js运行时错误,此时只有event.srcElement inatanceof HTMLScriptElement或HTMLLinkElement或HTMLImageElement时才上报
5.重写window.XMLHttpRequest和window.fetch捕获请求错误

延伸:跨域的js运行错误可以捕获吗,错误提示什么,应该怎么处理?

可以。
Script error
1.在script标签增加crossorigin属性
2.设置js资源响应头Access-Control-Allow-Orgin:*

上报错误的基本原理

1. 采用Ajax通信方式上报
2. 利用Image对象上报

更多

查看原文

赞 16 收藏 34 评论 2

芊芷鹤LY1234567 赞了文章 · 1月28日

歪门邪道性能优化:魔改三方库源码,性能提高几十倍!

本文会分享一个React性能优化的故事,这也是我在工作中真实遇到的故事,最终我们是通过魔改第三方库源码将它性能提高了几十倍。这个第三方库也是很有名的,在GitHub上有4.5k star,这就是:react-big-calendar

这个工作不是我一个人做的,而是我们团队几个月前共同完成的,我觉得挺有意思,就将它复盘总结了一下,分享给大家

在本文中你可以看到:

  1. React常用性能分析工具的使用介绍
  2. 性能问题的定位思路
  3. 常见性能优化的方式和效果:PureComponent, shouldComponentUpdate, Context, 按需渲染等等
  4. 对于第三方库的问题的解决思路

关于我工作中遇到的故事,我前面其实也分享过两篇文章了:

  1. 速度提高几百倍,记一次数据结构在实际工作中的运用
  2. 使用mono-repo实现跨项目组件共享

特别是速度提高几百倍,记一次数据结构在实际工作中的运用,这篇文章在某平台单篇阅读都有三万多,有些朋友也提出了质疑。觉得我这篇文章里面提到的问题现实中不太可能遇到,里面的性能优化更多是偏理论的,有点杞人忧天。这个观点我基本是认可的,我在那篇文章正文也提到过可能是个伪需求,但是技术问题本来很多就是理论上的,我们在leetcode上刷题还是纯理论呢,理论结合实际才能发挥其真正的价值,即使是杞人忧天,但是性能确实快上了那么一点点,也给大家提供了另一个思路,我觉得也是值得的。

与之相对的,本文提到的问题完全不是杞人忧天了,而是实打实的用户需求,我们经过用户调研,发现用户确实有这么多数据量,需求上不可能再压缩了,只能技术上优化,这也是逼得我们去改第三方库源码的原因。

需求背景

老规矩,为了让大家快速理解我们遇到的问题,我会简单讲一下我们的需求背景。我还是在那家外企,不久前我们接到一个需求:做一个体育场馆管理Web App。这里面有一个核心功能是场馆日程的管理,有点类似于大家Outlook里面的Calendar。大家如果用过Outlook,应该对他的Calendar有印象,基本上我们的会议及其他日程安排都可以很方便的放在里面。我们要做的这个也是类似的,体育场馆的老板可以用这个日历来管理他下面场地的预定。

假设你现在是一个羽毛球场的老板,来了个客户说,嘿,老板,这周六场地有空吗,我订一个小时呢!场馆每天都很多预定,你也不记得周六有没有空,所以你打开我们的网站,看了下日历:

image-20210117111412119

你发现1月15号,也就是星期五有两个预定,周六还全是空闲的,于是给他说:你运气真好,周六目前还没人预定,时段随便挑!上面这个截图是react-big-calendar的官方示例,我们也是选定用他来搭建我们自己的应用。

真实场景

上面这个例子只是说明下我们的应用场景,里面预定只有两个,场地只有一块。但是我们真实的客户可比这个大多了,根据我们的调研,我们较大的客户有数百块场地,每个场地每天的预定可能有二三十个。上面那个例子我们换个生意比较好的老板,假设这个老板有20块羽毛球场地,每天客户都很多,某天还是来了个客户说,嘿,老板,这周六场地有空吗,我订一个小时呢!但是这个老板生意很好,他看到的日历是这样的:

image-20210117112848684

本周场馆1全满!!如果老板想要为客户找到一个有空的场地,他需要连续切换场馆1,场馆2。。。一直到场馆20,手都点酸了。。。为了减少老板手的负担,我们的产品经理提出一个需求,同时在页面上显示10个场馆的日历,好在react-big-calendar本身就是支持这个的,他把这个叫做resources

性能爆炸

看起来我们要的基本功能react-big-calendar都能提供,前途还是很美好的,直到我们将真实的数据渲染到页面上。。。我们的预定不仅仅是展示,还需要支持一系列的操作,比如编辑,复制,剪切,粘贴,拖拽等等。当然这一切操作的前提都是选中这个预定,下面这个截图是我选中某个预定的耗时:

image-20210117114847440

仅仅是一个最简单的点击事件,脚本执行耗时6827ms,渲染耗时708ms,总计耗时7.5s左右,这TM!这玩意儿还想卖钱?送给我,我都不想用

可能有朋友不知道这个性能怎么看,这其实是Chrome自带的性能工具,基本步骤是:

  1. 打开Chrome调试工具,点到Performance一栏
  2. 点击左上角的小圆点,开始录制
  3. 执行你想要的操作,我这里就是点击一个预定
  4. 等你想要的结果出来,我这里就是点击的预定颜色加深
  5. 再点击左上角的小圆点,结束录制就可以看到了

为了让大家看得更清楚,我这里录制了一个操作的动图,这个图可以看到,点击操作的响应花了很长时间,Chrome加载这个性能数据也花了很长时间:

Jan-17-2021 12-51-51

测试数据量

上面仅仅一个点击耗时就七八秒,是因为我故意用了很大数据量吗?不是!我的测试数据量是完全按照用户真实场景计算的:同时显示10个场馆,每个场馆每天20个预定,上面使用的是周视图,也就是可以同时看到7天的数据,那总共显示的预定就是:

10 * 20 * 7 = 1400,总共1400个预定显示在页面上。

为了跟上面这个龟速点击做个对比,我再放下优化后的动图,让大家对后面这个长篇大论实现的效果先有个预期:

Jan-20-2021 16-42-53

定位问题

我们一般印象中,React不至于这么慢啊,如果慢了,大概率是写代码的人没写好!我们都知道React有个虚拟树,当一个状态改变了,我们只需要更新与这个状态相关的节点就行了,出现这种情况,是不是他干了其他不必要的更新与渲染呢?为了解决这个疑惑,我们安装了React专用调试工具:React Developer Tools。这是一个Chrome的插件,Chrome插件市场可以下载,安装成功后,Chrome的调试工具下面会多两个Tab页:

image-20210117130740746

Components这个Tab下有个设置,打开这个设置可以看到你每次操作触发哪些组件更新,我们就是从这里面发现了一点惊喜:

image-20210117130951475

为了看清楚点击事件触发哪些更新,我们先减少数据量,只保留一两个预定,然后打开这个设置看看:

Jan-17-2021 13-21-55

哼,这有点意思。。。我只是点击一个预定,你把整个日历的所有组件都给我更新了!那整个日历有多少组件呢?上面这个图可以看出10:00 AM10:30 AM之间是一个大格子,其实这个大格子中间还有条分割线,只是颜色较淡,看的不明显,也就是说每15分钟就是一个格子。这个15分钟是可以配置的,你也可以设置为1分钟,但是那样格子更多,性能更差!我们是根据需求给用户提供了15分钟,30分钟,1小时等三个选项。当用户选择15分钟的时候,渲染的格子最多,性能最差。

那如果一个格子是15分钟,总共有多少格子呢?一天是24 * 60 = 1440分钟,15分钟一个格子,总共96个格子。我们周视图最多展示7天,那就是7 * 96 = 672格子,最多可以展示10个场馆,就是672 * 10 = 6720个格子,这还没算日期和时间本身占据的组件,四舍五入一下姑且就算7000个格子吧。

我仅仅是点击一下预定,你就把作为背景的7000个格子全部给我更新一遍,怪不得性能差

再仔细看下上面这个动图,我点击的是小的那个事件,当我点击他时,注意大的那个事件也更新了,外面也有个蓝框,不是很明显,但是确实是更新了,在我后面调试打Log的时候也证实了这一点。所以在真实1400条数据下,被更新的还有另外1399个事件,这其实也是不必要的。

我这里提到的事件和前文提到的预定是一个东西,react-big-calendar里面将这个称为event,也就是事件,对应我们业务的意义就是预定

为什么会这样?

这个现象我好像似曾相识,也是我们经常会犯的一个性能上的问题:将一个状态放到最顶层,然后一层一层往下传,当下面某个元素更新了这个状态,会导致根节点更新,从而触发下面所有子节点的更新。这里说的更新并不一定要重新渲染DOM节点,但是会运行每个子节点的render函数,然后根据render函数运行结果来做diff,看看要不要更新这个DOM节点。React在这一步会帮我们省略不必要的DOM操作,但是render函数的运行却是必须的,而成千上万次render函数的运行也会消耗大量性能。

说到这个我想起以前看到过的一个资料,也是讲这个问题的,他用了一个一万行的列表来做例子,原文在这里:high-performance-redux。下面这个例子来源于这篇文章:

function itemsReducer(state = initial_state, action) {
  switch (action.type) {
  case 'MARK':
    return state.map((item) =>
      action.id === item.id ?
        {...item, marked: !item.marked } :
        item
    );
  default:
    return state;
  }
}

class App extends Component {
  render() {
    const { items, markItem } = this.props;
    return (
      <div>
        {items.map(item =>
          <Item key={item.id} id={item.id} marked={item.marked} onClick={markItem} />
        )}
      </div>
    );
  }
};

function mapStateToProps(state) {
  return state;
}

const markItem = (id) => ({type: 'MARK', id});

export default connect(
  mapStateToProps,
  {markItem}
)(App);

上面这段代码不复杂,就是一个App,接收一个items参数,然后将这个参数全部渲染成Item组件,然后你可以点击单个Item来改变他的选中状态,运行效果如下:

Jan-17-2021 15-17-38

这段代码所有数据都在items里面,这个参数从顶层App传进去,当点击Item的时候改变items数据,从而更新整个列表。这个运行结果跟我们上面的Calendar有类似的问题,当单条Item状态改变的时候,其他没有涉及的Item也会更新。原因也是一样的:顶层的参数items改变了。

说实话,类似的写法我见过很多,即使不是从App传入,也会从其他大的组件节点传入,从而引起类似的问题。当数据量少的时候,这个问题不明显,很多时候都被忽略了,像上面这个图,即使一万条数据,因为每个Item都很简单,所以运行一万次render你也不会明显感知出来,在控制台看也就一百多毫秒。但是我们面临的Calendar就复杂多了,每个子节点的运算逻辑都更复杂,最终将我们的响应速度拖累到了七八秒上。

优化方案

还是先说这个一万条的列表,原作者除了提出问题外,也提出了解决方案:顶层App只传id,Item渲染的数据自己连接redux store获取。下面这段代码同样来自这篇文章:

// index.js
function items(state = initial_state, action) {
  switch (action.type) {
  case 'MARK':
    const item = state[action.id];
    return {
      ...state,
      [action.id]: {...item, marked: !item.marked}
    };
  default:
    return state;
  }
}

function ids(state = initial_ids, action) {
  return state;
}

function itemsReducer(state = {}, action) {
  return {
    // 注意这里,数据多了一个ids
    ids: ids(state.ids, action),
    items: items(state.items, action),
  }
}

const store = createStore(itemsReducer);

export default class NaiveList extends Component {
  render() {
    return (
      <Provider store={store}>
        <App />
      </Provider>
    );
  }
}
// app.js
class App extends Component {
  static rerenderViz = true;
  render() {
    // App组件只使用ids来渲染列表,不关心具体的数据
    const { ids } = this.props;
    return (
      <div>
        {
          ids.map(id => {
            return <Item key={id} id={id} />;
          })
        }
      </div>
    );
  }
};

function mapStateToProps(state) {
  return {ids: state.ids};
}

export default connect(mapStateToProps)(App);
// Item.js
// Item组件自己去连接Redux获取数据
class Item extends Component {
  constructor() {
    super();
    this.onClick = this.onClick.bind(this);
  }

  onClick() {
    this.props.markItem(this.props.id);
  }

  render() {
    const {id, marked} = this.props.item;
    const bgColor = marked ? '#ECF0F1' : '#fff';
    return (
      <div
        onClick={this.onClick}
      >
        {id}
      </div>
    );
  }
}

function mapStateToProps(_, initialProps) {
  const { id } = initialProps;
  return (state) => {
    const { items } = state;
    return {
      item: items[id],
    };
  }
}

const markItem = (id) => ({type: 'MARK', id});

export default connect(mapStateToProps, {markItem})(Item);

这段代码的优化主要在这几个地方:

  1. 将数据从单纯的items拆分成了idsitems
  2. 顶层组件App使用ids来渲染列表,ids里面只有id,所以只要不是增加和删除,仅仅单条数据的状态变化,ids并不需要变化,所以App不会更新。
  3. Item组件自己去连接自己需要的数据,当自己关心的数据变化时才更新,其他组件的数据变化并不会触发更新。

拆解第三方库源码

上面通过使用调试工具我看到了一个熟悉的现象,并猜到了他慢的原因,但是目前仅仅是猜测,具体是不是这个原因还要看看他的源码才能确认。好在我在看他的源码前先去看了下他的文档,然后发现了这个:

image-20210117162411789

react-big-calendar接收两个参数onSelectEventselectedselected表示当前被选中的事件(预定),onSelectEvent可以用来改变selected的值。也就是说当我们选中某个预定的时候,会改变selected的值,由于这个参数是从顶层往下传的,所以他会引起下面所有子节点的更新,在我们这里就是差不多7000个背景格子 + 1399个其他事件,这样就导致不需要更新的组件更新了。

顶层selected换成Context?

react-big-calendar在顶层设计selected这样一个参数是可以理解的,因为使用者可以通过修改这个值来控制选中的事件。这样选中一个事件就有了两个途径:

  1. 用户通过点击某个事件来改变selected的值
  2. 开发者可以在外部直接修改selected的值来选中某个事件

有了前面一万条数据列表优化的经验,我们知道对于这种问题的处理办法了:使用selected的组件自己去连接Redux获取值,而不是从顶部传入。可惜,react-big-calendar并没有使用Redux,也没有使用其他任何状态管理库。如果他使用Redux,我们还可以考虑添加一个action来给外部修改selected,可惜他没有。没有Redux就玩不转了吗?当然不是!React其实自带一个全局状态共享的功能,那就是ContextReact Context API官方有详细介绍我之前的一篇文章也介绍过他的基本使用方法,这里不再讲述他的基本用法,我这里想提的是他的另一个特性:使用Context Provider包裹时,如果你传入的value变了,会运行下面所有节点的render函数,这跟前面提到的普通props是一样的。但是,如果Provider下面的儿子节点是PureComponent,可以不运行儿子节点的render函数,而直接运行使用这个value的孙子节点

什么意思呢,下面我将我们面临的问题简化来说明下。假设我们只有三层,第一层是顶层容器Calendar,第二层是背景的空白格子(儿子),第三层是真正需要使用selected的事件(孙子):

image-20210119144005794

示例代码如下:

// SelectContext.js
// 一个简单的Context
import React from 'react'

const SelectContext = React.createContext()

export default SelectContext;
// Calendar.js
// 使用Context Provider包裹,接收参数selected,渲染背景Background
import SelectContext from './SelectContext';

class Calendar extends Component {
  constructor(...args) {
    super(...args)
    
    this.state = {
      selected: null
    };
    
    this.setSelected = this.setSelected.bind(this);
  }
  
  setSelected(selected) {
    this.setState({ selected })
  }
  
  componentDidMount() {
    const { selected } = this.props;
    
    this.setSelected(selected);
  }
  
  render() {
    const { selected } = this.state;
    const value = {
      selected,
      setSelected: this.setSelected
    }
    
    return (
        <SelectContext.Provider value={value}>
          <Background />
      </SelectContext.Provider>
    )
  }
}
// Background.js
// 继承自PureComponent,渲染背景格子和事件Event
class Background extends PureComponent {
  render() {
    const { events } = this.props;
    return  (
        <div>
          <div>这里面是7000个背景格子</div>
          下面是渲染1400个事件
          {events.map(event => <Event event={event}/>)}
      </div>
    )
  }
}
// Event.js
// 从Context中取selected来决定自己的渲染样式
import SelectContext from './SelectContext';

class Event extends Component {
  render() {
    const { selected, setSelected } = this.context;
    const { event } = this.props;
    
    return (
        <div className={ selected === event ? 'class1' : 'class2'} onClick={() => setSelected(event)}>
      </div>
    )
  }
}

Event.contextType = SelectContext;    // 连接Context

什么是PureComponent?

我们知道如果我们想阻止一个组件的render函数运行,我们可以在shouldComponentUpdate返回false,当新的props相对于老的props来说没有变化时,其实就不需要运行rendershouldComponentUpdate就可以这样写:

shouldComponentUpdate(nextProps) {
    const fields = Object.keys(this.props)
    const fieldsLength = fields.length
    let flag = false

    for (let i = 0; i < fieldsLength; i = i + 1) {
      const field = fields[i]
      if (
        this.props[field] !== nextProps[field]
      ) {
        flag = true
        break
      }
    }

    return flag
  }

这段代码就是将新的nextProps与老的props一一进行对比,如果一样就返回false,不需要运行render。而PureComponent其实就是React官方帮我们实现了这样一个shouldComponentUpdate。所以我们上面的Background组件继承自PureComponent,就自带了这么一个优化。如果Background本身的参数没有变化,他就不会更新,而Event因为自己连接了SelectContext,所以当SelectContext的值变化的时候,Event会更新。这就实现了我前面说的如果Provider下面的儿子节点是PureComponent,可以不运行儿子节点的render函数,而直接运行使用这个value的孙子节点

PureComponent不起作用

理想是美好的,现实是骨感的。。。理论上来说,如果我将中间儿子这层改成了PureComponent,背景上7000个格子就不应该更新了,性能应该大幅提高才对。但是我测试后发现并没有什么用,这7000个格子还是更新了,什么鬼?其实这是PureComponent本身的一个问题:只进行浅比较。注意this.props[field] !== nextProps[field],如果this.props[field]是个引用对象呢,比如对象,数组之类的?因为他是浅比较,所以即使前后属性内容没变,但是引用地址变了,这两个就不一样了,就会导致组件的更新!

而在react-big-calendar里面大量存在这种计算后返回新的对象的操作,比如他在顶层Calendar里面有这种操作:

image-20210119151326161

代码地址:https://github.com/jquense/react-big-calendar/blob/master/src/Calendar.js#L790

这行代码的意思是每次props改变都去重新计算状态state,而他的计算代码是这样的:

image-20210119151747973

代码地址:https://github.com/jquense/react-big-calendar/blob/master/src/Calendar.js#L794

注意他的返回值是一个新的对象,而且这个对象里面的属性,比如localizer的计算方法mergeWithDefaults也是这样,每次都返回新的对象:

image-20210119151956459

代码地址:https://github.com/jquense/react-big-calendar/blob/master/src/localizer.js#L39

这样会导致中间儿子节点每次接受到的props虽然内容是一样的,但是因为是一个新对象,即使使用了PureComponent,其运行结果也是需要更新。这种操作在他的源码中大量存在,其实从功能角度来说,这样写是可以理解的,因为我有时候也会这么干。。。有时候某个属性更新了,不太确定要不要更新下面的组件,干脆直接返回一个新对象触发更新,省事是省事了,但是面对我们这种近万个组件的时候性能就崩了。。。

歪门邪道shouldComponentUpdate

如果只有一两个属性是这样返回新对象,我还可以考虑给他重构下,但是调试了一下发现有大量的属性都是这样,咱也不是他作者,也不知道会不会改坏功能,没敢乱动。但是不动性能也绷不住啊,想来想去,还是在儿子的shouldComponentUpdate上动点手脚吧。简单的this.props[field] !== nextProps[field]判断肯定是不行的,因为引用地址变啦,但是他内容其实是没变,那我们就判断他的内容吧。两个对象的深度比较需要使用递归,也可以参考React diff算法来进行性能优化,但是无论你怎么优化这个算法,性能最差的时候都是两个对象一样的时候,因为他们是一样的,你需要遍历到最深处才能肯定他们是一样的,如果对象很深,这种递归算法不见得会比运行一遍render快,而我们面临的大多数情况都是这种性能最差的情况。所以递归对比不太靠谱,其实如果你对这些数据心里有数,没有循环引用什么的,你可以考虑直接将两个对象转化为字符串来进行对比,也就是

JSON.stringify(this.props[field]) !== JSON.stringify(nextProps[field])

注意,这种方式只适用于你对props数据了解,没有循环引用,没有变化的Symbol,函数之类的属性,因为JSON.stringify执行时会丢掉Symbol和函数,所以我说他是歪门邪道性能优化

将这个转化为字符串比较的shouldComponentUpdate加到背景格子的组件上,性能得到了明显增强,点击相应速度从7.5秒下降到了5.3秒左右。

image-20210119160608456

按需渲染

上面我们用shouldComponentUpdate阻止了7000个背景格子的更新,响应时间下降了两秒多,但是还是需要5秒多时间,这也很难接受,还需要进一步优化。按照我们之前说的如果还能阻止另外1399个事件的更新那就更好了,但是经过对他数据结构的分析,我们发现他的数据结构跟我们前面举的列表例子还不一样。我们列表的例子所有数据都在items里面,是否选中是item的一个属性,而react-big-calendar的数据结构里面eventselectedEvent是两个不同的属性,每个事件通过判断自己的event是否等于selectedEvent来判断自己是否被选中。这造成的结果就是每次我们选中一个事件,selectedEvent的值都会变化,每个事件的属性都会变化,也就是会更新,运行render函数。如果不改这种数据结构,是阻止不了另外1399个事件更新的。但是改这个数据结构改动太大,对于一个第三方库,我们又不想动这么多,怎么办呢?

这条路走不通了,我们完全可以换一个思路,背景7000个格子,再加上1400个事件,用户屏幕有那么大吗,看得完吗?肯定是看不完的,既然看不完,那我们只渲染他能看到部分不就可以了!按照这个思路,我们找到了一个库:react-visibility-sensor。这个库使用方法也很简单:

function MyComponent (props) {
  return (
    <VisibilitySensor>
      {({isVisible}) =>
        <div>I am {isVisible ? 'visible' : 'invisible'}</div>
      }
    </VisibilitySensor>
  );
}

结合我们前面说的,我们可以将VisibilitySensor套在Background上面:

class Background extends PureComponent {
  render() {
    return (
      <VisibilitySensor>
        {({isVisible}) =>
          <Event isVisible={isVisible}/>
        }
      </VisibilitySensor>
    )
  }
}

然后Event组件如果发现自己处于不可见状态,就不用渲染了,只有当自己可见时才渲染:

class Event extends Component {
  render() {
    const { selected } = this.context;
    const { isVisible, event } = this.props;
    
    return (
      { isVisible ? (
       <div className={ selected === event ? 'class1' : 'class2'}>
          复杂内容
       </div>
      ) : null}
    )
  }
}

Event.contextType = SelectContext;

按照这个思路我们又改了一下,发现性能又提升了,整体时间下降到了大概4.1秒:

image-20210120140421092

仔细看上图,我们发现渲染事件Rendering时间从1秒左右下降到了43毫秒,快了二十几倍,这得益于渲染内容的减少,但是Scripting时间,也就是脚本执行时间仍然高达4.1秒,还需要进一步优化。

砍掉mousedown事件

渲染这块已经没有太多办法可以用了,只能看看Scripting了,我们发现性能图上鼠标事件有点刺眼:

image-20210119170345316

一次点击同时触发了三个点击事件:mousedownmouseupclick。如果我们能干掉mousedownmouseup是不是时间又可以省一半,先去看看他注册这两个事件时干什么的吧。可以直接在代码里面全局搜mousedown,最终发现都是在Selection.js,通过对这个类代码的阅读,发现他是个典型的观察者模式,然后再搜new Selection找到使用的地方,发现mousedownmouseup主要是用来实现事件的拖拽功能的,mousedown标记拖拽开始,mouseup标记拖拽结束。如果我把它去掉,拖拽功能就没有了。经过跟产品经理沟通,我们后面是需要拖拽的,所以这个不能删。

事情进行到这里,我也没有更多办法了,但是响应时间还是有4秒,真是让人头大

image-20210120144109109

反正没啥好办法了,我就随便点着玩,突然,我发现mousedown的调用栈好像有点问题:

image-20210120144433528

这个调用栈我用数字分成了三块:

  1. 这里面有很多熟悉的函数名啊,像啥performUnitOfWorkbeginWork,这不都是我在React Fiber这篇文章中提过的吗?所以这些是React自己内部的函数调用
  2. render函数,这是某个组件的渲染函数
  3. 这个render里面又调用了renderEvents函数,看起来是用来渲染事件列表的,主要的时间都耗在这里了

mousedown监听本身我是干不掉了,但是里面的执行是不是可以优化呢?renderEvents已经是库自己写的代码了,所以可以直接全局搜,看看在哪里执行的。最终发现是在TimeGrid.jsrender函数被执行了,其实这个是不需要执行的,我们直接把前面歪门邪道的shouldComponentUpdate复制过来就可以阻止他的执行。然后再看下性能数据呢:

image-20210120145945555

我们发现Scripting下降到了3.2秒左右,比之前减少约800毫秒,而mousedown的时间也从之前的几百毫秒下降到了50毫秒,在图上几乎都看不到了,mouseup事件也不怎么看得到了,又算进了一步吧~

忍痛阉割功能

到目前为止,我们的性能优化都没有阉割功能,响应速度从7.5秒下降到了3秒多一点,优化差不多一倍。但是,目前这速度还是要三秒多,别说作为一个工程师了,作为一个用户我都忍不了。咋办呢?我们是真的有点黔驴技穷了。。。

看看上面那个性能图,主要消耗时间的有两个,一个是click事件,还有个timertimer到现在我还不知道他哪里来的,但是click事件我们是知道的,就是用户点击某个事件后,更改SelectContextselected属性,然后selected属性从顶层节点传入触发下面组件的更新,中间儿子节点通过shouldComponentUpdate跳过更新,孙子节点直接连接SelectContext获取selected属性更新自己的状态。这个流程是我们前面优化过的,但是,等等,这个貌似还有点问题。

在我们的场景中,中间儿子节点其实包含了高达7000个背景格子,虽然我们通过shouldComponentUpdate跳过了render的执行,但是7000个shouldComponentUpdate本省执行也是需要时间的啊!有没有办法连shouldComponentUpdate的执行也跳过呢?这貌似是个新的思路,但是经过我们的讨论,发现没办法在保持功能的情况下做到,但是可以适度阉割一个功能就可以做到,那阉割的功能是哪个呢?那就是暴露给外部的受控selected属性!

前面我们提到过选中一个事件有两个途径:

  1. 用户通过点击某个事件来改变selected的值
  2. 开发者可以在外部直接修改selected的值来选中某个事件

之所以selected要放在顶层组件上就是为了实现第二个功能,让外部开发者可以通过这个受控的selected属性来改变选中的事件。但是经过我们评估,外部修改selected这个并不是我们的需求,我们的需求都是用户点击来选中,也就是说外部修改selected这个功能我们可以不要。

如果不要这个功能那就有得玩了,selected完全不用放在顶层了,只需要放在事件外层的容器上就行,这样,改变selected值只会触发事件的更新,啥背景格子的更新压根就不会触发,那怎么改呢?在我们前面的Calendar -- Background -- Event模型上再加一层EventContainer,变成Calendar -- Background -- EventContainer -- EventSelectContext.Provider也不用包裹Calendar了,直接包裹EventContainer就行。代码大概是这个样子:

// Calendar.js
// Calendar简单了,不用接受selected参数,也不用SelectContext.Provider包裹了
class Calendar extends Component {
  render() {
    return (
      <Background />
    )
  }
}
// Background.js
// Background要不要使用shouldComponentUpdate阻止更新可以看看还有没有其他参数变化,因为selected已经从顶层拿掉了
// 改变selected本来就不会触发Background更新
// Background不再渲染单个事件,而是渲染EventContainer
class Background extends PureComponent {
  render() {
    const { events } = this.props;
    return  (
        <div>
          <div>这里面是7000个背景格子</div>
          下面是渲染1400个事件
          <EventContainer events={events}/>
      </div>
    )
  }
}
// EventContainer.js
// EventContainer需要SelectContext.Provider包裹
// 代码类似之前的Calendar
import SelectContext from './SelectContext';

class EventContainer extends Component {
  constructor(...args) {
    super(...args)
    
    this.state = {
      selected: null
    };
    
    this.setSelected = this.setSelected.bind(this);
  }
  
  setSelected(selected) {
    this.setState({ selected })
  }
  
  render() {
    const { selected } = this.state;
    const { events } = this.props;
    const value = {
      selected,
      setSelected: this.setSelected
    }
    
    return (
        <SelectContext.Provider value={value}>
          {events.map(event => <Event event={event}/>)}
      </SelectContext.Provider>
    )
  }
}
// Event.js
// Event跟之前是一样的,从Context中取selected来决定自己的渲染样式
import SelectContext from './SelectContext';

class Event extends Component {
  render() {
    const { selected, setSelected } = this.context;
    const { event } = this.props;
    
    return (
        <div className={ selected === event ? 'class1' : 'class2'} onClick={() => setSelected(event)}>
      </div>
    )
  }
}

Event.contextType = SelectContext;    // 连接Context

这种结构最大的变化就是当selected变化的时候,更新的节点是EventContainer,而不是顶层Calendar,这样就不会触发Calendar下其他节点的更新。缺点就是Calendar无法从外部接收selected了。

需要注意一点是,如果像我们这样EventContainer下面直接渲染Event列表,selected不用Context也可以,可以直接作为EventContainerstate但是如果EventContainerEvent中间还有层级,需要穿透传递,仍然需要Context,中间层级和以前的类似,使用shouldComponentUpdate阻止更新

还有一点,因为selected不在顶层了,所以selected更新也不会触发中间Background更新了,所以Background上的shouldComponentUpdate也可以删掉了。

我们这样优化后,性能又提升了:

image-20210120161336248

现在Scripting时间直接从3.2秒降到了800毫秒,其中click事件只有163毫秒,现在从我使用来看,卡顿已经不明显了,直接录个动图来对比下吧:

Jan-20-2021 16-42-53

上面这个动图已经基本看不出卡顿了,但是我们性能图上为啥还有800毫秒呢,而且有一个很长的Timer Fired。经过我们的仔细排查,发现这其实是个乌龙,Timer Fired在我一开始录制性能就出现了,那时候我还在切换页面,还没来得及点击呢,如果我们点进去会发现他其实是按需渲染引入的react-visibility-sensor的一个检查元素可见性的定时任务,并不是我们点击事件的响应时间。把这块去掉,我们点击事件的响应时间其实不到200毫秒。

从7秒多优化到不到200毫秒,三十多倍的性能优化,终于可以交差了,哈哈😃

总结

本文分享的是我工作中实际遇到的一个案例,实现的效果是将7秒左右的响应时间优化到了不到200毫秒,优化了三十几倍,优化的代价是牺牲了一个不常用的功能。

本来想着要是优化好了可以给这个库提个PR,造福大家的。但是优化方案确实有点歪门邪道:

  1. 使用了JSON.stringify来进行shouldComponentUpdate的对比优化,对于函数,Symbol属性的改变没法监听到,不适合开放使用,只能在数据自己可控的情况下小规模使用。
  2. 牺牲了一个暴露给外部的受控属性selected,破坏了功能。

基于这两点,PR我们就没提了,而是将修改后的代码放到了自己的私有NPM仓库。

下面再来总结下本文面临的问题和优化思路:

遇到的问题

我们需求是要做一个体育场馆的管理日历,所以我们使用了react-big-calendar这个库。我们需求的数据量是渲染7000个背景格子,然后在这个背景格子上渲染1400个事件。这近万个组件渲染后,我们发现仅仅一次点击就需要7秒多,完全不能用。经过细致排查,我们发现慢的原因是点击事件的时候会改变一个属性selected。这个属性是从顶层传下来的,改变后会导致所有组件更新,也就是所有组件都会运行render函数。

第一步优化

为了阻止不必要的render运行,我们引入了Context,将selected放到Context上进行透传。中间层级因为不需要使用selected属性,所以可以使用shouldComponentUpdate来阻止render的运行,底层需要使用selected的组件自行连接Context获取。

第一步优化的效果

响应时间从7秒多下降到5秒多。

第一步优化的问题

底层事件仍然有1400个,获取selected属性后,1400个组件更新仍然要花大量的时间。

第二步优化

为了减少点击后更新的事件数量,我们为事件引入按需渲染,只渲染用户可见的事件组件。同时我们还对mousedownmouseup进行了优化,也是使用shouldComponentUpdate阻止了不必要的更新。

第二步优化效果

响应时间从5秒多下降到3秒多。

第二步优化的问题

响应时间仍然有三秒多,经过分析发现,背景7000个格子虽然使用shouldComponentUpdate阻止了render函数的运行,但是shouldComponentUpdate本身运行7000次也要费很长时间。

第三步优化

为了让7000背景格子连shouldComponentUpdate都不运行,我们忍痛阉割了顶层受控的selected属性,直接将它放到了事件的容器上,它的更新再也不会触发背景格子的更新了,也就是连shouldComponentUpdate都不运行了。

第三步优化效果

响应时间从3秒多下降到不到200毫秒。

第三步优化的问题

功能被阉割了,其他完美!

参考资料:

react-big-calendar仓库

high-performance-redux

文章的最后,感谢你花费宝贵的时间阅读本文,如果本文给了你一点点帮助或者启发,请不要吝啬你的赞和GitHub小星星,你的支持是作者持续创作的动力。

欢迎关注我的公众号进击的大前端第一时间获取高质量原创~

“前端进阶知识”系列文章源码地址: https://github.com/dennis-jiang/Front-End-Knowledges

1270_300二维码_2.png

查看原文

赞 34 收藏 19 评论 12

芊芷鹤LY1234567 关注了用户 · 1月28日

前端小智 @minnanitkong

我不是什么大牛,我其实想做的就是一个传播者。内容可能过于基础,但对于刚入门的人来说或许是一个窗口,一个解惑之窗。我要先坚持分享20年,大家来一起见证吧。

关注 10231

芊芷鹤LY1234567 收藏了文章 · 2020-10-01

React 架构的演变 - 从递归到循环

这篇文章是 React 架构演变的第二篇,上一篇主要介绍了更新机制从同步修改为异步,这一篇重点介绍 Fiber 架构下通过循环遍历更新的过程,之所以要使用循环遍历的方式,是因为递归更新过程一旦开始就不能暂停,只能不断向下,直到递归结束或者出现异常。

递归更新的实现

React 15 的递归更新逻辑是先将需要更新的组件放入脏组件队列(这里在上篇文章已经介绍过,没看过的可以先看看《React 架构的演变 - 从同步到异步》),然后取出组件进行一次递归,不停向下寻找子节点来查找是否需要更新。

下面使用一段代码来简单描述一下这个过程:

updateComponent (prevElement, nextElement) {
  if (
    // 如果组件的 type 和 key 都没有发生变化,进行更新
    prevElement.type === nextElement.type &&
    prevElement.key === nextElement.key
  ) {
    // 文本节点更新
    if (prevElement.type === 'text') {
        if (prevElement.value !== nextElement.value) {
            this.replaceText(nextElement.value)
        }
    }
    // DOM 节点的更新
    else {
      // 先更新 DOM 属性
      this.updateProps(prevElement, nextElement)
      // 再更新 children
      this.updateChildren(prevElement, nextElement)
    }
  }
  // 如果组件的 type 和 key 发生变化,直接重新渲染组件
  else {
    // 触发 unmount 生命周期
    ReactReconciler.unmountComponent(prevElement)
    // 渲染新的组件
    this._instantiateReactComponent(nextElement)
  }
},
updateChildren (prevElement, nextElement) {
  var prevChildren = prevElement.children
  var nextChildren = nextElement.children
  // 省略通过 key 重新排序的 diff 过程
  if (prevChildren === null) { } // 渲染新的子节点
  if (nextChildren === null) { } // 清空所有子节点
  // 子节点对比
  prevChildren.forEach((prevChild, index) => {
    const nextChild = nextChildren[index]
    // 递归过程
    this.updateComponent(prevChild, nextChild)
  })
}

为了更清晰的看到这个过程,我们还是写一个简单的Demo,构造一个 3 * 3 的 Table 组件。

Table

// https://codesandbox.io/embed/react-sync-demo-nlijf
class Col extends React.Component {
  render() {
    // 渲染之前暂停 8ms,给 render 制造一点点压力
    const start = performance.now()
    while (performance.now() - start < 8)
    return <td>{this.props.children}</td>
  }
}

export default class Demo extends React.Component {
  state = {
    val: 0
  }
  render() {
    const { val } = this.state
    const array = Array(3).fill()
    // 构造一个 3 * 3 表格
    const rows = array.map(
      (_, row) => <tr key={row}>
        {array.map(
          (_, col) => <Col key={col}>{val}</Col>
        )}
      </tr>
    )
    return (
      <table className="table">
        <tbody>{rows}</tbody>
      </table>
    )
  }
}

然后每秒对 Table 里面的值更新一次,让 val 每次 + 1,从 0 ~ 9 不停循环。

Table Loop

// https://codesandbox.io/embed/react-sync-demo-nlijf
export default class Demo extends React.Component {
    tick = () => {
    setTimeout(() => {
      this.setState({ val: next < 10 ? next : 0 })
      this.tick()
    }, 1000)
  }
  componentDidMount() {
    this.tick()
  }
}

完整代码的线上地址: https://codesandbox.io/embed/react-sync-demo-nlijf。Demo 组件每次调用 setState,React 会先判断该组件的类型有没有发生修改,如果有就整个组件进行重新渲染,如果没有会更新 state,然后向下判断 table 组件,table 组件继续向下判断 tr 组件,tr 组件再向下判断 td 组件,最后发现 td 组件下的文本节点发生了修改,通过 DOM API 更新。

Update

通过 Performance 的函数调用堆栈也能清晰的看到这个过程,updateComponent 之后 的 updateChildren 会继续调用子组件的 updateComponent,直到递归完所有组件,表示更新完成。

调用堆栈

递归的缺点很明显,不能暂停更新,一旦开始必须从头到尾,这与 React 16 拆分时间片,给浏览器喘口气的理念明显不符,所以 React 必须要切换架构,将虚拟 DOM 从树形结构修改为链表结构。

可循环的 Fiber

这里说的链表结构就是 Fiber 了,链表结构最大的优势就是可以通过循环的方式来遍历,只要记住当前遍历的位置,即使中断后也能快速还原,重新开始遍历。

我们先看看一个 Fiber 节点的数据结构:

function FiberNode (tag, key) {
  // 节点 key,主要用于了优化列表 diff
  this.key = key
  // 节点类型;FunctionComponent: 0, ClassComponent: 1, HostRoot: 3 ...
  this.tag = tag

    // 子节点
  this.child = null
  // 父节点
  this.return = null 
  // 兄弟节点
  this.sibling = null
  
  // 更新队列,用于暂存 setState 的值
  this.updateQueue = null
  
  // 节点更新过期时间,用于时间分片
  // react 17 改为:lanes、childLanes
  this.expirationTime = NoLanes
  this.childExpirationTime = NoLanes

  // 对应到页面的真实 DOM 节点
  this.stateNode = null
  // Fiber 节点的副本,可以理解为备胎,主要用于提升更新的性能
  this.alternate = null
}

下面举个例子,我们这里有一段普通的 HTML 文本:

<table class="table">
  <tr>
    <td>1</td>
    <td>1</td>
  </tr>
  <tr>
    <td>1</td>
  </tr>
</table>

在之前的 React 版本中,jsx 会转化为 createElement 方法,创建树形结构的虚拟 DOM。

const VDOMRoot = {
  type: 'table',
  props: { className: 'table' },
  children: [
    {
      type: 'tr',
      props: { },
      children: [
        {
          type: 'td',
          props: { },
          children: [{type: 'text', value: '1'}]
        },
        {
          type: 'td',
          props: { },
          children: [{type: 'text', value: '1'}]
        }
      ]
    },
    {
      type: 'tr',
      props: { },
      children: [
        {
          type: 'td',
          props: { },
          children: [{type: 'text', value: '1'}]
        }
      ]
    }
  ]
}

Fiber 架构下,结构如下:

// 有所简化,并非与 React 真实的 Fiber 结构一致
const FiberRoot = {
  type: 'table',
  return: null,
  sibling: null,
  child: {
    type: 'tr',
    return: FiberNode, // table 的 FiberNode
    sibling: {
      type: 'tr',
      return: FiberNode, // table 的 FiberNode
      sibling: null,
      child: {
        type: 'td',
        return: FiberNode, // tr 的 FiberNode
        sibling: {
          type: 'td',
          return: FiberNode, // tr 的 FiberNode
          sibling: null,
          child: null,
          text: '1' // 子节点仅有文本节点
        },
        child: null,
        text: '1' // 子节点仅有文本节点
      }
    },
    child: {
      type: 'td',
      return: FiberNode, // tr 的 FiberNode
      sibling: null,
      child: null,
      text: '1' // 子节点仅有文本节点
    }
  }
}

Fiber

循环更新的实现

那么,在 setState 的时候,React 是如何进行一次 Fiber 的遍历的呢?

let workInProgress = FiberRoot

// 遍历 Fiber 节点,如果时间片时间用完就停止遍历
function workLoopConcurrent() {
  while (
    workInProgress !== null &&
    !shouldYield() // 用于判断当前时间片是否到期
  ) {
    performUnitOfWork(workInProgress)
  }
}

function performUnitOfWork() {
  const next = beginWork(workInProgress) // 返回当前 Fiber 的 child
  if (next) { // child 存在
    // 重置 workInProgress 为 child
    workInProgress = next
  } else { // child 不存在
    // 向上回溯节点
    let completedWork = workInProgress
    while (completedWork !== null) {
      // 收集副作用,主要是用于标记节点是否需要操作 DOM
      completeWork(completedWork)

      // 获取 Fiber.sibling
      let siblingFiber = workInProgress.sibling
      if (siblingFiber) {
        // sibling 存在,则跳出 complete 流程,继续 beginWork
        workInProgress = siblingFiber
        return;
      }

      completedWork = completedWork.return
      workInProgress = completedWork
    }
  }
}

function beginWork(workInProgress) {
  // 调用 render 方法,创建子 Fiber,进行 diff
  // 操作完毕后,返回当前 Fiber 的 child
  return workInProgress.child
}
function completeWork(workInProgress) {
  // 收集节点副作用
}

Fiber 的遍历本质上就是一个循环,全局有一个 workInProgress 变量,用来存储当前正在 diff 的节点,先通过 beginWork 方法对当前节点然后进行 diff 操作(diff 之前会调用 render,重新计算 state、prop),并返回当前节点的第一个子节点( fiber.child)作为新的工作节点,直到不存在子节点。然后,对当前节点调用 completedWork 方法,存储 beginWork 过程中产生的副作用,如果当前节点存在兄弟节点( fiber.sibling),则将工作节点修改为兄弟节点,重新进入 beginWork 流程。直到 completedWork 重新返回到根节点,执行 commitRoot 将所有的副作用反应到真实 DOM 中。

Fiber work loop

在一次遍历过程中,每个节点都会经历 beginWorkcompleteWork ,直到返回到根节点,最后通过 commitRoot 将所有的更新提交,关于这部分的内容可以看:《React 技术揭秘》

时间分片的秘密

前面说过,Fiber 结构的遍历是支持中断恢复,为了观察这个过程,我们将之前的 3 * 3 的 Table 组件改成 Concurrent 模式,线上地址:https://codesandbox.io/embed/react-async-demo-h1lbz。由于每次调用 Col 组件的 render 部分需要耗时 8ms,会超出了一个时间片,所以每个 td 部分都会暂停一次。

class Col extends React.Component {
  render() {
    // 渲染之前暂停 8ms,给 render 制造一点点压力
    const start = performance.now();
    while (performance.now() - start < 8);
    return <td>{this.props.children}</td>
  }
}

在这个 3 * 3 组件里,一共有 9 个 Col 组件,所以会有 9 次耗时任务,分散在 9 个时间片进行,通过 Performance 的调用栈可以看到具体情况:

异步模式的调用栈

在非 Concurrent 模式下,Fiber 节点的遍历是一次性进行的,并不会切分多个时间片,差别就是在遍历的时候调用了 workLoopSync 方法,该方法并不会判断时间片是否用完。

// 遍历 Fiber 节点
function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress)
  }
}

同步模式的调用栈

通过上面的分析可以看出, shouldYield 方法决定了当前时间片是否已经用完,这也是决定 React 是同步渲染还是异步渲染的关键。如果去除任务优先级的概念,shouldYield 方法可以说很简单,就是判断了当前的时间,是否已经超过了预设的 deadline

function getCurrentTime() {
  return performance.now()
}
function shouldYield() {
  // 获取当前时间
  var currentTime = getCurrentTime()
  return currentTime >= deadline
}

deadline 又是如何得的呢?可以回顾上一篇文章(《React 架构的演变 - 从同步到异步》)提到的 ChannelMessage,更新开始的时候会通过 requestHostCallback(即:port2.send)发送异步消息,在 performWorkUntilDeadline (即:port1.onmessage)中接收消息。performWorkUntilDeadline 每次接收到消息时,表示已经进入了下一个任务队列,这个时候就会更新 deadline

异步调用栈

var channel = new MessageChannel()
var port = channel.port2
channel.port1.onmessage = function performWorkUntilDeadline() {
  if (scheduledHostCallback !== null) {
    var currentTime = getCurrentTime()
    // 重置超时时间 
    deadline = currentTime + yieldInterval
    
    var hasTimeRemaining = true
    var hasMoreWork = scheduledHostCallback()

    if (!hasMoreWork) {
      // 已经没有任务了,修改状态 
      isMessageLoopRunning = false;
      scheduledHostCallback = null;
    } else {
      // 还有任务,放到下个任务队列执行,给浏览器喘息的机会 
      port.postMessage (null);
    }
  } else {
    isMessageLoopRunning = false;
  }
}

requestHostCallback = function (callback) {
  //callback 挂载到 scheduledHostCallback
  scheduledHostCallback = callback
  if (!isMessageLoopRunning) {
    isMessageLoopRunning = true
    // 推送消息,下个队列队列调用 callback
    port.postMessage (null)
  }
}

超时时间的设置就是在当前时间的基础上加上了一个 yieldInterval, 这个 yieldInterval 的值,默认是 5ms。

deadline = currentTime + yieldInterval

同时 React 也提供了修改 yieldInterval 的手段,通过手动指定 fps,来确定一帧的具体时间(单位:ms),fps 越高,一个时间分片的时间就越短,对设备的性能要求就越高。

forceFrameRate = function (fps) {
  if (fps < 0 || fps > 125) {
    // 帧率仅支持 0~125
    return
  }

  if (fps > 0) {
    // 一般 60 fps 的设备
    // 一个时间分片的时间为 Math.floor(1000/60) = 16
    yieldInterval = Math.floor(1000 / fps)
  } else {
    // reset the framerate
    yieldInterval = 5
  }
}

总结

下面我们将异步逻辑、循环更新、时间分片串联起来。先回顾一下之前的文章讲过,Concurrent 模式下,setState 后的调用顺序:

Component.setState()
  => enqueueSetState()
  => scheduleUpdate()
  => scheduleCallback(performConcurrentWorkOnRoot)
  => requestHostCallback()
  => postMessage()
  => performWorkUntilDeadline()

scheduleCallback 方法会将传入的回调(performConcurrentWorkOnRoot)组装成一个任务放入 taskQueue 中,然后调用 requestHostCallback 发送一个消息,进入异步任务。performWorkUntilDeadline 接收到异步消息,从 taskQueue 取出任务开始执行,这里的任务就是之前传入的 performConcurrentWorkOnRoot 方法,这个方法最后会调用workLoopConcurrentworkLoopConcurrent 前面已经介绍过了,这个不再重复)。如果 workLoopConcurrent 是由于超时中断的,hasMoreWork 返回为 true,通过 postMessage 发送消息,将操作延迟到下一个任务队列。

流程图

到这里整个流程已经结束,希望大家看完文章能有所收获,下一篇文章会介绍 Fiber 架构下 Hooks 的实现。

image

查看原文

芊芷鹤LY1234567 赞了文章 · 2020-06-01

CSS 创意构想 - Part 1 / 2

image.png

背景

本人一直对 CSS 很感兴趣,刚好我们团队有一位擅长CSS的大佬:COCO。

于是我找到他, 建议做一期关于CSS的分享, 于是就有了《CSS创意构想》。

分享的内容很棒,里面有很多技巧, 有的很实用, 有的很华丽。

听完之后, 我觉得十分受用, 就想结合自己的一些理解,再次整理一番加深印象, 二次吸收, 所以就有了今天的文章。

满满的干货,也分享给大家, 希望对大家有所启发。

PS: 文章内示例均来自coco的博客, 感兴趣的可以移步到这里


布局

平行四边形

先抛一个问题, 如何实现平行四边形布局效果?
image.png

  • transform
  • 叠加三角形

仅仅实现形状的话, 上面两种方式都是可以的。

但是, 如果图形内部还有文字,需要正常排布,transform 、叠加三角形 都不可行。

那怎么办呢? 答案就是: shape-outside.

image.png

  • shape-outside定义了一个可以是非矩形的形状,相邻的内联内容应围绕该形状进行排版
  • clip-pathCSS 属性可以创建一个只有元素的部分区域可以显示的剪切区域

clip-path 语法:

{ 
    clip-path: circle(50px at 0  100px); 
    clip-path: ellipse(); 
    clip-path: inset(10px  10px  10px  10px); 
    clip-path: polygon(10px  10px, 20px  20px, 30px  30px);
}

图文混排 Demo:

https://codepen.io/Chokcoco/p...

CSS Shapes Demo:

https://codepen.io/Chokcoco/p...

一个适用的场景: 适配 iPhone X刘海头

镂空

使用阴影,可以非常简单的模拟遮罩效果, 并且,圆角也是没有问题的。

box-shadow: 0 0 0 100vmax rgba(0, 0, 0, .5);

image.png

垂直居中

抛出个问题: 最快的水平垂直居中方法是什么?

你首先想到的是不是: flex, center, center ?

其实margin也可以:

<div class="g-container">  
    <div class="g-box"></div>  
</div>

.g-container { display: flex; } 
.g-box { margin: auto; }

原理:

在 flex 格式化上下文中,设置了 margin: auto 的元素,在通过 justify-content 和 align-self 进行对齐之前,任何正处于空闲的空间都会分配到该方向的自动 margin 中去。

Live Demo:

https://codepen.io/Chokcoco/p...

还有这种常见的左右布局, 也可以巧用margin来实现:

image.png

<ul class="g-nav">
    <li>导航A</li>
    <li>导航B</li>
    <li>导航C</li>
    <li>导航D</li>
    <li class="g-login">登陆</li>
    <li>注册</li>
</ul>

.g-nav { display: flex; } 
.g-login { margin-left: auto; }

自动页脚

我们经常会遇到需要放置在页脚的元素, 高度超过一屏时, 自动顶下去:

这种效果也有很多种实现方式, 巧用margin可以快速实现:

<div class="g-container">
    <div class="g-real-box">
        ...
    </div>
    <div class="g-footer"></div>
</div>
.g-container {
    height: 100vh;
    display: flex;
    flex-direction: column;
}

.g-footer {
    margin-top: auto;
    flex-shrink: 0;
    height: 30px;
    background: deeppink;
}

阴影

  • 阴影分为外阴影和内阴影(inset)
  • 阴影可以是多重阴影
  • 阴影可以进行动画

使用阴影可以轻松得到图片本身,并且任意改变颜色及大小.

比如, 把发散半径设置为0:

image.png

code:

div {
    width: 80px;
    height: 80px;
    background: #fff;
    box-shadow: 
        80px 80px 0 0 yellow, 
        -80px 80px 0 10px green;
}

使用阴影模拟多重边框

image.png

代码实现:

{
  background: #fff;
  box-shadow:  
    0 0 0 2px red,
    0 0 0 4px orange,
    0 0 0 6px yellow,
    0 0 0 8px green,
    0 0 0 10px cyan;
}

使用阴影画出一朵云:

image.png

其实是用不同的阴影叠加而成:

image.png

image.png

.bgShadowCloud {
    width: 100px;
    height: 100px;
    margin: 50px auto!important;
    background: #fff;
    border-radius: 50%;
    box-shadow: 120px 0px 0 -10px #795548, 95px 20px 0 0px #607D8B, 30px 30px 0 -10px green, 90px -20px 0 0px #FFC107, 40px -40px 0 0px #2196F3;
    animation: change 6s infinite;
}

内阴影

模拟圆月变月牙:

image.png

image.png

代码实现:

image.png

动态效果:
https://codepen.io/Chokcoco/p...

特殊阴影

有的时候,CSS 的 box-shadow 无法很好去实现一些特殊阴影,可以利用一些小技巧进行模拟。

长阴影

  • 借助两个伪元素的 transform: skew() 变换
  • 伪元素背景从实色到透明色的背景色变化

image.png

实现代码:

<div class="bgLongShadow"></div>

.bgLongShadow {
    position: relative;
    width: 268px;
    height: 269px;
    background: #fff;
    margin: 20px auto!important;
    background-size: contain;
    background-position: center;
    background-repeat: no-repeat;
}

.bgLongShadow::before {
    transform-origin: 0 50%;
    transform: translate(100%, 0) skewY(45deg) scaleX(.3);
    background: linear-gradient(90deg, rgba(255, 255, 255, .3), transparent);
    animation: shadowMoveY 5s infinite linear alternate;
}

.bgLongShadow::before, .bgLongShadow::after {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: -1;
}

立体投影

image.png

以第一个为例, 其实是用了一点障眼法:

image.png

代码实现:

<div class="bgSolidShadow"></div>

div.bgSolidShadow {
    position: relative;
    width: 600px;
    height: 100px;
    margin: 50px auto;
    background: hsl(48, 100%, 50%);
    border-radius: 20px;
    box-shadow: 0 0 5px 2px hsl(48, 100%, 45%);
    cursor: pointer;
}

.bgSolidShadow::before {
    content: "";
    position: absolute;
    top: 40%;
    left: 5%;
    right: 5%;
    bottom: 0;
    border-radius: 10px;
    transform: translate(0, -15%) rotate(-2deg);
    transform-origin: center center;
    box-shadow: 0 0 10px 12px hsl(0, 0%, 0%);
    z-index: -1;
}

彩色阴影

  • 其实是借助了高斯模糊滤镜

image.png

{
    filter: blur(10px) brightness(90%) opacity(.85);
}

整体阴影

看下图:

image.png

这个的阴影如何实现? box-shadow ?

先看一下DOM结构:


<div class="flecha"></div>

// 三角
.flecha {
    position: relative;
    margin: 50px auto!important;
    width: 0;
    height: 0;
    border-top: 90px solid transparent!important;
    border-right: 90px solid #FFC000!important;
    transform: rotate(10deg);
    box-shadow: 0px 0px 10px rgba(255, 220, 0, .8);
}

// 尾巴

.flecha:after {
    content: "";
    position: absolute;
    border: 0 solid transparent;
    border-top: 30px solid #FFC000!important;
    border-radius: 200px 0 0 0!important;
    top: -119px;
    left: -98px;
    width: 120px;
    height: 120px;
    transform: rotate(45deg);
}

此时的效果:

image.png

加上box-shadow:

{
    box-shadow: 0px 0px 10px rgba(255, 220, 0, .8);
}

image.png

可见是不行的, 那该怎么办呢?

答案是: 使用 drop-shadow

{ 
  filter: drop-shadow(0px  0px  10px  rgba(255, 220, 0, .8)); 
}

image.png

完美~

利用drop-shadow单标签实现抖音 LOGO

image

代码实现:

div {
    position: relative;
    width: 37px;
    height: 218px;
    background: #fff;
    filter:drop-shadow(-10px -10px 0 #24f6f0) contrast(150%) brightness(110%);
    box-shadow: 11.6px 10px 0 0 #fe2d52;
    
    &::before {
        ....
        filter: drop-shadow(16px 0px 0 #fe2d52);
    }
    
    &::after {
        ....
        filter:drop-shadow(14px 0 0 #fe2d52);
    }
}

Live demo:

https://codepen.io/Chokcoco/p...

十分酷炫~

使用阴影的扩散半径实现内切圆角

image.png

Live Demo:

https://codepen.io/Chokcoco/p...

渐变

  • 渐变分为线性渐变、径向渐变、角向渐变(圆锥渐变)
  • 渐变可以控制各种角度
  • 渐变可以是多重渐变叠加
  • 渐变不能进行动画 (animation/transtion)

线性渐变

image.png

{ background: linear-gradient(45deg, #fff, #ffcc00); }

合理控制颜色百分比,可以让渐变颜色变成实色

image.png

{
    background: linear-gradient(45deg, #fff 50%, #ffcc00 50%);
}

某一段的颜色可以是透明的

image.png

{ background: linear-gradient(45deg, #fff  33%, transparent 33%, transparent 66%, #ffcc00  66% ); }

背景重复

  • 可以利用 repeating-linear-gradient、repeating-radial-gradient 进行重复
  • 也可以利用 background-size 和 background-repeat 进行重复控制

image.png

背景动画

可以不去改变颜色本身,而是利用其它元素进行动画

transform、filter、mix-blend-mode 等等

比如, 实现一个背景色持续变化的元素:

image.png

设置overflow: hidden 截断多余的部分, 使用动画不断改变背景块的位置, 即可实现效果。

<div class="g-linear-gradient-5"></div>

.g-linear-gradient-5 {
    width: 80px;
    height: 80px;
    border: 2px solid #fff!important;
    position: relative;
    overflow: hidden;
}

.g-linear-gradient-5::before {
    content: "";
    position: absolute;
    top: -100%;
    left: -100%;
    bottom: -100%;
    right: -100%;
    background: linear-gradient(45deg, #ffc700 0%, #e91e1e 50%, #6f27b0 100%);
    background-size: 100% 100%;
    animation: bgposition 8s infinite linear alternate;
    z-index: -1;
}

利用透明色,实现切角

image.png

{
    background:
        linear-gradient(135deg, transparent 15px, #ffcc00 0)
        top left,
        linear-gradient(-135deg, transparent 15px, #ffcc00 0)
        top right,
        linear-gradient(-45deg, transparent 15px, #ffcc00 0)
        bottom right,
        linear-gradient(45deg, transparent 15px, #ffcc00 0)
        bottom left;
    background-size: 60% 60%;
    background-repeat: no-repeat;
}

实现进度条效果

利用重复线性渐变 + transform 实现进度条效果

https://codepen.io/Chokcoco/p...

实现优惠券的边纹

image.png

{
    background-image: 
        radial-gradient(circle at 1px 8px, transparent 6px, #ffcc00 6px, #ffcc00 0px),
        radial-gradient(circle at 199px 8px, transparent 6px, #ffcc00 6px, #ffcc00 0px);
    background-size: 200px 18px;
    background-position: 0 0, 200px 0;
    background-repeat-x: no-repeat;
}

多种渐变的组合

多种渐变的组合,还能创造出各种有意思的图形, 比如:

image.png

https://codepen.io/Chokcoco/p...

实现气泡按钮点击效果

image.png

使用了background-position+background-size

https://codepen.io/Chokcoco/p...

圆锥渐变

  • linear-gradient 线性渐变的方向是一条直线,可以是任何角度
  • radial-gradient 径向渐变是从圆心点以椭圆形状向外扩散

起始点是图形中心,然后以顺时针方向绕中心实现渐变效果

image.png


{ background: conic-gradient(deeppink, yellowgreen); }

实现颜色表盘

image.png

{ background: conic-gradient(red, #ff4d00, #ff9900, #ffe600, #ccff00, #80ff00, #33ff00, #00ff1a, #00ff66, #00ffb3, cyan, #00b3ff, #0066ff, #001aff, #3300ff, #8000ff, #cc00ff, #ff00e6, #ff0099, #ff004d, red); }

又或者是这种积分表盘:

https://codepen.io/alphardex/...

结尾

文章已经很长, 为了阅读上的方便, 剩余的一部分将会在下一篇文章里。

剩余内容包括:

  • 混合模式
  • 滤镜
  • 伪元素
  • 波浪效果
  • 滚动指示器
  • 滚动视差

部分示例缺少代码, 需要逐个准备, 比较耗时。

持续更新, 敬请期待,

关注我

如果你觉得这篇内容对你挺有启发,那就关注我吧~

图片

更多精彩:

聊聊 ESM、Bundleless 、Vite 、Snowpack

记一次 「 无限列表 」滚动优化

「 面试三板斧 」之 代码分割(上)

「 面试三板斧 」之缓存 (上)

「 面试三板斧 」之缓存 (下)

「 面试三板斧 」之 HTTP (上)

「 面试三板斧 」之 HTTP (下)

「 面试三板斧 」之  this

查看原文

赞 52 收藏 32 评论 9

芊芷鹤LY1234567 赞了文章 · 2020-06-01

通过 41 个 问题增加对学习 Git 的理解

作者:Duomly
译者:前端小智
来源:dev.to
点赞再看,养成习惯

本文 GitHubhttps://github.com/qq44924588... 上已经收录,更多往期高赞文章的分类,也整理了很多我的文档,和教程资料。欢迎Star和完善,大家面试可以参照考点复习,希望我们一起有点东西。


为了保证的可读性,本文采用意译而非直译。

1. 你最喜欢的 Git 命令是什么

个人比较喜欢 git add -p. 这增加了“补丁模式”的变化,这是一个内置的命令行程序。它遍历了每个更改,并要求确认是否要执行它们。

这个命令迫使咱们放慢速度并检查更改文件。作为开发人员,咱们有时常常急于提交,我自己也经常这样,做完运行 git add . 才发现把调试的代码也提交上去了。

2. 为什么你更喜欢直接使用 git 命令

作为开发人员,咱们也经常使用其它命令来做其它事情,也不差用 git 的命令来做事。

此外,git 命令也是非常短的,非常容易学习,并且使用命令可以了解 git 的工作流程,这样也间接改进了开发工作流程。

3. 如何使用 stage 命令

stageadd .的内置别名。

4.如何在分支中保存更改并 checkout 到其他分支

因此,可以使用 git stash 临时存储更改或提交 WIP,目的是要有未修改前的环境。就我个人而言,我更喜欢使用 WIP 提交而不是 stash,因为它们更容易引用和共享。

WIP = Work in Progress

研发中的代码想存储起来,但是又避免研发中的代码被合并,开发就会创建一个WIP的分支

WIP MR

WIP MR 含义是 在工作过程中的合并请求,是一个我们在 GitLab 中避免 MR 在准备就绪前被合并的技术。只需要添加 WIP: 在 MR 的标题开头,它将不会被合并,除非你把 WIP: 删除。

5.什么时候使用 git stash

发现有一个类是多余的,想删掉它又担心以后需要查看它的代码,想保存它但又不想增加一个脏的提交。这时就可以考虑 git stash

6.如何使用 git 命令

对任何命令使用 --help选项,例如,git stash --help

7. 什么是“ git flow”?

Git Flow 定义了一个项目发布的分支模型,为管理具有预定发布周期的大型项目提供了一个健壮的框架,是由 Vincent Driessen 提出的一个 git 操作流程标准、解决当分支过多时 , 如何有效快速管理这些分支。

8.什么是 GitHub flow ?

GitHub flow,顾名思义,就是 GitHub 所推崇的 Workflow。(千万不要理解成 GitHub 上才能用的 Workflow), 基本上,GitHub Flow 是master/feature分支工作流程的品牌名称。

GitHub flow 的核心优势在于其流程带来的自动化可能性,能够做到其它流程无法实现的检查过程,并极大简化开发团队的体力劳动,真正发挥自身的价值。

9.你更喜欢哪种分支策略?

大多数 Git项目都是 “Git flow”。这些项目中只有少数需要这种策略,通常是因为它是版本化的软件。

master/feature 分支策略更易于管理,尤其是在刚入门时,如果需要,切换到 “git flow” 非常容易。

10. git open 命令是做啥用的

这是一个单独的命令,可以作为 npm 包使用。

11.当在其他分支中添加的文件仍然在工作分支中显示为未跟踪或修改时,如何重置分支

这通常是“工作索引”不干净时切换分支的结果。

在 git 中没有内置的方法来纠正这一点。通常通过确保提示符有一个 “status” 指示符并在每次更改分支时运行诸如 git status 之类的命令来避免这种情况。这些习惯会让咱们尽早发现这些问题,这样就可以在新的分支上 stashcommit 这些更改。

12. 如何重命名分支?

git branch -m current-branch-name new-branch-name

13. 如何使用 cherry-pick

git cherry-pick [reference] 请记住,这是一个重新应用的命令,因此它将更改提交 SHA。

14. 如果从一个分支恢复(例如 HEAD~3),是否可以再次返回到 HEAD(比如恢复上一次更新)

在这种情况下,通过运行 git reset --hard HEAD~1 立即撤消还原提交(即 HEAD 提交)。

15. 什么时候使用 git pullgit fetch

git pull将下载提交到当前分支。记住,git pull实际上是 fetchmerge 命令的组合。

git fetch将从远程获取最新的引用。

一个很好的类比是播客播放器或电子邮件客户端。咱们可能会检索最新的播客或电子邮件(fetch),但实际上尚未在本地下载播客或电子邮件附件(pull)。

16. 为什么有时需要使用 --force 来强制提交更改

rebase 是一个可以重新提交的命令,它改变了 SHA1 hash。如果是这样,本地提交历史将不再与其远程分支保持一致。

当这种情况发生时,push 会被拒绝。只有在被拒绝时,才应该考虑使用 git push --force。这样做将用本地提交历史覆盖远程提交历史。所以可以回过头来想想,想想为什么要使用 --force

17. 可以使用分支合并多个分支,然后将该分支发送给 master 吗?

当然可以,在大多数 git 工作流下,分支通常会累积来自多个其他分支的更改,最终这些分支会被合并到主分支。

18. 应该从一个非常老的分支做一个 rebase 吗?

除非是迫不得已。

根据你的工作流,可以将旧的分支合并到主分支中。

如果你需要一个最新的分支,我更喜欢 rebase。它只提供更改且更清晰的历史记录,而不是来自其他分支或合并的提交。

然而,尽管总是可能的,但是使用 rebase 可能是一个痛苦的过程,因为每次提交都要重新应用。这可能会导致多重冲突。如果是这样,我通常使用rebase --abort 并使用 merge 来一次性解决所有冲突。

19. 使用 rebase -i 时,squashfixup 有什么区别

squash 和 fixup 结合两个提交。squash 暂停 rebase 进程,并允许咱们调整提交的消息。fixup 自动使用来自第一次提交的消息。

20. 通常,当使用 master 重新建立功能分支时,对于每次提交都需要解决冲突?

是的。由于每次提交的更改都会在 rebase 期间重新应用,所以必须在冲突发生时解决它们。

这意味着在提交之前就已经有了提交冲突,如果没有正确地解决它,那么下面的许多提交也可能发生冲突。为了限制这一点,我经常使用 rebase -i 来压缩提交历史记录,以便更轻松地使用它。

如果许多提交之间仍然存在冲突,可以使用 merge

21.在与 master 合并之前,有必要更新我的分支吗

根据你的工作流,可以将旧的分支合并到主分支中。如果你的工作流仅使用 "fast-forward"合并,那么有必要在合并之前更新你的分支。

Git fast forward 提交

多人协同开发,使用 Git 经常会看到警告信息包含术语:fast forward, 这是何义?

简单来说就是提交到远程中心仓库的代码必须是按照时间顺序的。

比如 A 从中心仓库拿到代码后,对文件 f 进行了修改。然后 push 到中心仓库。

BA 之前就拿到了中心仓库的代码,在 A push 成功之后也对 f 文件进行了修改。这个时候 B 也运行 push 命令推送代码。

会收到一个类似下面的信息:

chenshu@sloop2:~/work/189/appengine$ git pushTo 
ssh://csfreebird@10.112.18.189:29418/appengine.git ! [rejected]       
master -> master (non-fast-forward)error: failed to push some refs to 
'ssh://csfreebird@10.112.18.189:29418/appengine.git'To prevent you from losing 
history, non-fast-forward updates were rejectedMerge the remote changes (e.g. 'git 
pull') before pushing again.  See the'Note about fast-forwards' section of 'git push --help' for details.

提醒你非快进方式的更新被拒绝了,需要先从中心仓库pull到最新版本,merge后再 push.

fast forward 能够保证不会强制覆盖别人的代码,确保了多人协同开发。尽量不要使用 non fast forward方法提交代码。

22. 需要使用 GitKraken 这种可视化工具吗

我比较喜欢用命令方式使用 git,因为这使我能够完全控制管理变更,就像使用命令来改进我的开发过程一样。

当然,某些可视化操作(如管理分支和查看文件差异)在GUI中总是更好。我个人认为在合并过程中在浏览器中查看这些内容就足够了。

23. 当提交已经被推送时,可以做一个 --amend 修改吗?

可以,git commit –amend 既可以对上次提交的内容进行修改,也可以修改提交说明。

24.在做迭代内容时,当完成一个小功能需要先拉一个 pull request 请求,还是都做完这个迭代内容后在拉一个 pull request 请求

咱们通常做法是,完成一个迭代的内容后在拉一个 pull request。然而,如果你某个任务上花了很长时间,先合并做的功能可能是有益的。这样做可以防止对分支的依赖或过时,所以做完一个拉一个请求,还是全部做完在拉一个请求,这决于你正在进行的更改的类型。

25. 在将分支合并到 master 之前,需要先创建一个 release 分支吗?

这在很大程度上取决于你们公司的部署过程。创建 release 分支对于将多个分支的工作分组在一起并将它们合并到主分支之前进行整体测试是有益的。

由于源分支保持独立和未合并,所以在最后的合并中拥有更大的灵活性。

26. 如何从 master 获取一些提交?比方说,我不想执行最后一次提交,而是进行一次 rebase

假设 master 分支是咱们的主分支,咱们不希望有选择地从它的历史记录中提取提交,这会以后引起冲突。

咱们想要 mergerebase 分支的所有更改。要从主分支之外的分支提取选择提交,可以使用 git cherry-pick

27. 如何在 git 终端配置颜色

默认情况 下git 是黑白的。

git config --global color.status auto 
git config --global color.diff auto 
git config --global color.branch auto 
git config --global color.interactive auto

配置之后,就有颜色了。

28. 有没有更好的命令来替代 git push -force ?

实际上,没有其他方法可以替代 git push—force。虽然这样,如果正确地使用 mergerebase 更新分支,则无需使用 git push --force

只有当你运行了更改本地提交历史的命令时,才应该使用 git push --force

29. 当我在 git rebase - 选择drop时,是否删除了与该提交相关的代码?

是的。要恢复这段代码,需要在 reflogrebase 之前找到一个状态。

30. 如何自动跟踪远程分支

通常,当你 checkout 或创建分支时,Git 会自动设置分支跟踪。

如果没有,则可以在下一次使用以下命令进行更新时:git push -u remote-name branch-name

或者可以使用以下命令显式设置它:git branch --set-upstream-to = remote-name / branch-name

31. 在 rebase 分支之前更新分支,是一个好的习惯吗?

我认为是这样的,原因很简单,用git rebase -i 组织或压缩提交,首先在更新过程中提供更多的上下文。

32. 有没有一种方法可以将提交拆分为更多的提交(与 fixup/squash 相反)?

可以在rebase -i过程中使用 exec 命令来尝试修改工作索引并拆分更改。还可以使用 git reset 来撤消最近的提交,并将它们的更改放入工作索引中,然后将它们的更改分离到新的提交中。

33.有没有办法查看已修复的提交?

git log

查看日志,找到对应的修改记录,但是这种查找只能看到文件,而不是文件的内容。

git blame 文件名

查看这个文件的修改记录,默认显示整个文件,也可以通过参数 -L <start>,<end>来检查需要修改的某几行。

如果查看之前提交的内容可以使用 git show commitId

34. rebase --skip 作用是啥?

咱们知道 rebase 的过程首先会产生 rebase 分支(master)的备份,放到(no branch )临时分支中。再将支线分支(branch)的每一次提交修改,以补丁的形式,一个个的重新应用到主干分支上。这个过程是一个循环应用补丁的过程,期间只要补丁产生冲突,就会停止循环,等待手动解决冲突。这个冲突指的是上一个合并后版本与补丁之间的冲突。

git rebase --skip 命令,可以跳过某一次补丁(存在上一轮冲突的解决方案中,已经包含了这一轮的补丁内容,这样会使补丁无效,需要跳过),这个命令慎用。

35. 如何删除远程分支?

可以使用:git push origin:branch-name-to-remove 或使用 -d选项:git push -d origin someother -branch-2 来删除远程分支。

要删除对远程分支的本地引用,可以运行:git remote prune origin

36. checkoutreset 有什么区别

这两个命令都可以用来撤销更改。checkout 可能更健壮,因为它不仅允许撤消当前更改,而且还允许通过检索文件的旧版本撤消一组更改。

默认情况下,reset更适合于更改工作索引中更改的状态。因此,它实际上只处理当前的变化。

git checkout -- file;撤销对工作区修改;这个命令是以最新的存储时间节点(add和commit)为参照,覆盖工作区对应文件file;这个命令改变的是工作区。

git reset HEAD -- file;清空 add 命令向暂存区提交的关于 file 文件的修改(Ustage);这个命令仅改变暂存区,并不改变工作区,这意味着在无任何其他操作的情况下,工作区中的实际文件同该命令运行之前无任何变化

37. 在正常的工作流程中应该避免使用哪些命令

一些可能会破坏历史记录的内容,例如:

git push origin master -f (千万不要这样做)
git revert
git cherry-pick (changes from master)

在正常的工作流程下,尽量避免直接使用git merge,因为这通常是通过拉请求(pull requests)构建到流程中的。

38. 如果我有一个分支(B)指向另一个分支(A),而我又有另一个分支(C),它需要(A)和(B)及 mast 分支的代码,怎么个流程才能更新(C)?

这取决于几件事:

如果 AB 可以合并到 master,刚可以将 AB 合并到 master 中,然后用master的更新 C

如果 AB 不能合并到 master,可以简单地将 B 合并到 C 中,因为 B 已经包含了 A 的变更。

在极端的情况下,可以将 ABmaster 合并到 C 中。然而,为了避免冲突,合并的顺序可能很重要。

39. 你使用的别名有哪些

我常用的一些 git 别名如下:

alias.unstage reset HEAD --
alias.append commit --amend --no-edit
alias.wip commit -m "WIP"
alias.logo log --oneline
alias.lola log --graph --oneline --decorate --all

40. 鲜为人知的 git 命令有哪些?

git bisect 是查找代码中存在的bug的救命工具。虽然只使用过几次,但它的精确度令人印象深刻,节省了大量时间。

git archive 是用于打包一组更改的好工具。这有助于与第三方或 mico-deployment 共享工作。

git reflog 可能是众所周知的,但值得一提,因为它提供了一种在出错时“撤消”命令的好方法。

41. 你能推荐一些关于Git的书籍吗

我建议至少阅读Pro Git的前三章。这些年来,每看到一遍,或多或少都有收获。

<Git 学习指南> 也不错。


代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

原文:https://dev.to/gonedark/42-gi...


交流

文章每周持续更新,可以微信搜索「 大迁世界 」第一时间阅读和催更(比博客早一到两篇哟),本文 GitHub https://github.com/qq449245884/xiaozhi 已经收录,整理了很多我的文档,欢迎Star和完善,大家面试可以参照考点复习,另外关注公众号,后台回复福利,即可看到福利,你懂的。

查看原文

赞 28 收藏 21 评论 0

认证与成就

  • 获得 0 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2018-05-14
个人主页被 409 人浏览