Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

一些个面试题与js知识点 #69

Open
5Mi opened this issue Feb 16, 2017 · 10 comments
Open

一些个面试题与js知识点 #69

5Mi opened this issue Feb 16, 2017 · 10 comments

Comments

@5Mi
Copy link
Owner

5Mi commented Feb 16, 2017

转自前端进阶

MS

  • ECMAScript中的所有参数传递的都是,不可能通过引用传递参数。 被传递的值会被复制给一个局部变量(arguments)

  • js slice 与 splice

    • slice是指定在一个数组中的元素创建一个新的数组,即原数组不会变,slice需要两个参数,起点和终点。它会返回一个包含了从起点开始,到终点之前之间所有元素的新数组。
        var x = [14, 3, 77];
        var y = x.slice(1, 2);
        console.log(x);   // [14, 3, 77]
        console.log(y);   // [3]
    • splice它能够实现对数组元素的删除、插入、替换操作,返回值为被操作的值。
    var x = [14, 3, 77]
    var y = x.splice(1, 2)
    console.log(x)   // [14]
    console.log(y)   // [3, 77]
  • 字符串回文与匹配 与乱序

    //回文
    function isMatch(str1,str2){
        return str1 === str2.split('').reverse().join('')   
    }
    
    //匹配(含有相同字符)
    fucntion match(str1,str2){
        return str1.split('').sort().join('') === str2.split('').sort().join('');
    }
    
    //乱序
    function luanxu(str){
        return str.split('').sort(function(){return Math.random()-0.5}).join('');
    }
  • javascript 中的 this 引用的是函数据以执行的环境对象——或者也可以说是 this 值(当在网页的全局作用域中调用函数时, this 对象引用的就是 window)。
    window.color = "red";
    
    var o = { color: "blue" };

    function sayColor(){
        alert(this.color); 
    }

    sayColor();//"red"

    o.sayColor = sayColor; 
    o.sayColor();//blue
  • 运算符优先级表

    优先级 高到低 同级不同运算从左到右
    20 圆括号

    19 成员访问.

    19 需计算的成员访问[]

    19 new(带参数列表 例: new...(...) )

    19 函数调用 例: ...(...)

    18 new(无参数列表) 例: new ...

    17 后置自增,后置自减 a++ a--

    ...


    (new foo 等同于 new foo(), 只能用在不传递任何参数的情况)

  • 函数声明提升 和 变量声明提升

    alert(sum(10,10)); 
    var sum = function(num1, num2){
        return num1 + num2; 
    };
    //等同于
    var sum;
    alert(sum(10,10)); //报错 此时sum为undefined
    sum = function(num1, num2){
        return num1 + num2; 
    };
    • 变量的声明提升
    if(!("a" in window)){
        var a = 1;
    }
    alert(a);
    //上述代码相当于
    var a;
    if(!("a" in window)){
        a = 1;
    }
    alert(a);
    // alert undefined

    变量提升也有优先级, 函数声明 > arguments > 变量声明

  • 函数作用域链包含两个对象:的作用域链包含两个对象:它自己的变量对象(其中 定义着 arguments 对象)全局环境的变量对象

    • 每个环境都 可以向上搜索作用域链,以查询变量和函数名;但任何环境都不能通过向下搜索作用域链而进入另一个 执行环境。

    作用域链是定死的,函数引用的变量在哪里定义,引用的就是哪里的变量.

    var color = "blue";
    
    function changeColor(){ 
        var anotherColor = "red";
    
        function swapColors(){ 
            var tempColor = anotherColor; 
            anotherColor = color; 
            color = tempColor;
            // 这里可以访问 color、anotherColor 和 tempColor
        }
        // 这里可以访问 color 和 anotherColor,但不能访问 tempColor
        swapColors();
        // 即便此处有局部变量color 访问的也是全局变量color
        winColor();//输出red
    }
    function winColor(){
        console.log(color);
    }
    
    // 这里只能访问 color changeColor();
    changeColor();
    //
    var a = 1;
    function Fn1(){
        var a = 2;
        Fn2();//1
    }
    //函数引用的变量在哪里定义,引用的就是哪里的变量.
    //函数定义在此作用域链已定,能访问到的a仅为全局变量a
    function Fn2(){
        console.log(a);
    }
    Fn1();
    • 如果存在一个局部的变量的定义,则搜索会自动停止,不再进入另一个变量对 象。换句话说,如果局部环境中存在着同名标识符,就不会使用位于父环境中的标识符
    var color = "blue";
    function getColor(){ 
            var color = "red"; 
            return color; 
    } 
    alert(getColor()); //"red"
    • 例题 输出什么
    var a = 1;
    
    function fn(){
          console.log(a); 
          var a = 5;
          console.log(a);  
          a++;
          var a;
          fn3();
          fn2();
          console.log(a);
        
          function fn2(){
            console.log(a); 
            a = 20;
          }
    }
    
    function fn3(){
          console.log(a)
          a = 200;
    }
    
    fn();
    console.log(a); 
    //输出
    //undefined
    //5
    //1
    //6
    //20
    //200
  • 函数的隐式转换

    对象通过valueOf方法,把自己转换成数字,通过toString方法,把自己转换成字符串

    如果字符串和数字相加,JavaScript会自动把数字转换成字符的,不管数字在前还是字符串在前,字符串和数字相加结果是字符串

    一个对象同时存在valueOf方法和toString方法,那么,valueOf方法总是会被优先调用的

    参考

function fn() {
    return 20;
}
console.log(fn + 10); 
// 输出字符串
//function fn() {
//    return 20;
//}10

//...

function fn() {
    return 20;
}

fn.toString = function() {
    return 10;
}

console.log(fn + 10);  // 输出结果是20

//...

function fn() {
    return 20;
}

fn.toString = function() {
    return 10;
}

fn.valueOf = function() {
    return 5;
}

console.log(fn + 10); // 输出结果是15
  • =====Object.is()

  • 获取页面用到哪些元素document.getElementsByTagName('*');

    document.all能取得当前页面所有的element,判断nodeType===1就是element了,取nodeName就是标签名称

  • js 中的 new

    new的过程

    1.开辟一块内存空间,创建一个新的空对象

    2.执行构造函数,对这个空对象进行构造

    3.给这个空对象添加__proto__属性(绑定原型)

    4.返回操作后的对象 隐式的return this

        var createObject = function(){
          var obj = new Object(),   //创建空对象
          Constructor = [].shift.call(arguments);   //取到构造函数,赋值给Constructor变量
        
          obj.__proto__ = Constructor.prototype;    //绑定原型
        
          var ret = Constructor.apply(obj, arguments);    //执行构造函数
        
          return typeof ret === 'object' ? ret : obj;   //返回这个对象 隐式的return this
        };
    
        var shock = createObject(Rocker, 'Shock');
        
        console.log(shock.name);  //Shock
        
        console.log(shock.getName());  //Shock
        
        console.log(Object.getPrototypeOf(shock) === Rocker.prototype); //true
function customNew(tarConstructor, ...args) {
  const obj = Object.create({});
  const res = tarConstructor.apply(obj, args);
  obj.__proto__ = tarConstructor.prototype;
  return typeof res === "object" ? res : obj;
}
  • 访问属性对象 .[]
    []中可以用变量

    var a = {name:'a'}
    var b = {val:123}
    a.b['val']//报错 a.b 此时为undefined
    a.b = 111 //a {name:'a',b:111}
    a[b.val] = 'ok' //a {name:'a',b:111,123:'ok'}
  • this

    函数调用可等价转化为call形式;

        var obj = {
      foo: function(){
        console.log(this)
      }
    }
    
    var bar = obj.foo
    obj.foo() // 打印出的 this 是 obj
    bar() // 打印出的 this 是 window
    var length=10;
    function fn(){
        console.log(this.length);
    }
    var obj = {
        length:5,
        method: function (fn) {
    
            fn();
            //此处运行的this 指向 arguments  等同于arguments[0].call(arguments);
            arguments[0](); 
        }
    };
    obj.method(fn);
    obj.method(fn, 123);
    //10
    //1
    //10
    //2
  • 数组去重

    var arr = [1,2,3,1,1,1,1];
    function toHeavy(array){
        var cacheArr = [],cache = {};
        for(var i = 0;i<array.length;i++){
            if(!cache[array[i]]){
                cache[array[i]] = array[i];
                cacheArr.push(array[i]);
            }
        }
        return cacheArr;
    }
    toHeavy(arr);//[1,2,3]
    
    const arr = [...new Set([1, 2, 3, 3])]
    // [1, 2, 3]
  • 值交换 数组克隆
    var a = 1,b = 2;
    a = [b,b=a][0];// a为2 b为1 
    // 数组赋值是引用传递
    
    var c = [1,2,3];
    var d = c.slice();//克隆数组
  • 123456每隔一秒打印一个
    for(var i = 1; i<7; i++){
        (function(j){
            setTimeout(function(){
                console.log(j);
            },j*1000);
        })(i)
    }
    //或直接用let
    for(let i = 1; i<7; i++){
        
        setTimeout(function(){
            console.log(i);
        },i*1000);
        
    }
  • 模拟call , apply函数

    参考call,apply,bind

      //
      f.call(o);
      f.apply(o);
      //以上代码 和 以下代码功能类似(假设对象o中预先不存在名为m的属性)
      o.m = f;     //将f存储为o的临时方法
      o.m();       //调用它,不传入参数
      delete o.m;  //将临时方法删除
    
    
    

模拟实现

        //
    //somefn.apply(someObj,[arguments]);
        Function.prototype.customApply = function(context,argarr){
            //不传入上下文用window;
            var ctx = context || window;

            //需保证ctx 没有 tempFn (不会与原有属性冲突); 考虑使用es6 Symbol
            // var tempFn = Symbol();
            // ctx[tempFn] = this;
            
            //模拟Symbol; 
            var tempFn = customSymbol(ctx);
            //ctx.tempFn .调用都是以字符串为属性名调用 如 ctx.tempFn 为 ctx['tempFn']
            ctx[tempFn] = this;
            //if不传参数数组
            if(argarr == void 0 || argarr.length == 0) return ctx.tempFn();

            //不使用apply将参数数组展开 添加为函数参数
            //参数数组展开后 传入方法中 (而不是传入一个数组,考虑使用es6 ctx[tempFn](...argarr))
            // var fn_result = ctx[tempFn](...argarr);
            
            var fnstr = "ctx[tempFn]("
            for(var i = 0,len = argarr.length;i < len;i++){
                //其实这里应该再判断下每个参数的 typeof
                //最后一个参数时
                if(i == len-1){
                    fnstr+=("'" + argarr[i] + "'" + ")"); 
                }else{
                    fnstr+=("'" + argarr[i] + "'" + ",");
                }
            }
            var fn_result = eval(fnstr);

            delete ctx.tempFn;
            return fn_result;
        }
        
            //简单模拟Symbol属性
    function customSymbol(obj){
        var randomstr_prop = '00' + Math.random();
        if(obj.hasOwnProperty(randomstr_prop)){
            //如果obj已经有了这个属性,递归调用,直到没有这个属性
            arguments.callee(obj); 
        }else{
            return randomstr_prop;
        }
    }
        
        var person = {
            name:'xiaoming',
            sayHi:function(age,something){
                console.log(this.name + ':i am ' + age +' and ' + something);
            }
        }
        var dog = {
            name:'poki'
        }
        // person.sayHi.apply(dog,[5]);
        //不使用es6展开参数数组的话 这时{name:'123'} toString()为[object Object] 有bug
        //应在展开参数数组时 typeof 判断类型
        person.sayHi.customApply(dog,[6,{name:'123'}]);
        // person.sayHi.customApply(dog,[6,'hi']);
  • 模拟bind函数

    bind方法

    语法

    fun.bind(thisArg[, arg1[, arg2[, ...]]])

    参数thisArg

    当绑定函数被调用时,该参数会作为原函数运行时的 this 指向。当使用new 操作符调用绑定函数时,该参数无效。

    arg1, arg2, ...

    当绑定函数被调用时,这些参数将置于实参之前传递给被绑定的方法。

    返回值

    返回由指定的this值和初始化参数改造的原函数拷贝

        //如果有bind就用原生的
        //Function.prototype.bind = Function.prototype.bind || function(context){
        Function.prototype.bind = function(context){
            
            if (typeof this !== "function") {
                // closest thing possible to the ECMAScript 5
                // internal IsCallable function
                throw new TypeError("Function.prototype.bind - what is trying tobe bound is not callable");
            }
        
            var me = this;
            //arg1, arg2, ...这些参数将置于实参之前传递给被绑定的方法
            var args = Array.prototype.slice.call(arguments,1);
            
            var F = function(){};
            F.prototype = this.prototype;
            
            var bound = function(){
                
                var innerArgs = Array.prototype.slice.call(arguments);
                //这些参数将置于实参之前传递给被绑定的方法
                //finalArgs 为 .bind()中的额外参数 与 bind处理后的函数的实参 组成的数组
                var finalArgs = args.concat(innerArgs);
                
                //instanceof 指出对象是否是特定类的一个实例。true的话说明用了new操作 令context无效
                //
                return me.apply(this instanceof F?this:context||this,finalArgs)
            }
            //继承原函数
            bound.prototype = new F();
            return bound;
        }
  • 返回类型

    Object.prototype.toString.call(value) == '[object Array]'
// 利用这个方法,可以写一个返回数据类型的方法
var isType = function (obj) {
     return Object.prototype.toString.call(obj).slice(8,-1); 
}
  • 函数 节流与防抖 参考
    
    //节流
        function throttle(fun,delay){
            var last = null;
            //第三个参数开始为fun所需参数
            var args = [].slice.call(arguments,2);
            return function(){
                var now = new Date();
                if(now - last > delay){
                    fun.apply(this,args);
                    last = now;
                }
            }
        }
        document.body.onresize = throttle(function(ok){
            console.log(ok);
        },2000,'arg1')

        //防抖
        function debouce(fun,delay){
            var timer = null;
            //第三个参数开始为fun所需参数
            var args = [].slice.call(arguments,2);
            return function(){
                clearTimeout(timer);
                timer = setTimeout(function(){
                    fun.apply(this,args);
                },delay);
            }
        }
        document.body.onclick = debouce(function(arg){
            console.log('click!!!' + arg);
        },1000,'argarg')
        
       // 异步
       function debounce(fn, delay, immediate) {
    let timer = null;

    return function() {
        const context = this;
        const args = arguments;

        return new Promise((resolve, reject) => {
            timer && clearTimeout(timer);

            if (immediate) {
                const doNow = !timer;

                timer = setTimeout(() => {
                    timer = null;
                }, delay);

                doNow && resolve(fn.apply(context, args));
            }
            else {
                timer = setTimeout(() => {
                    resolve(fn.apply(context, args));
                }, delay);
            }
        });
    };
}
        
  • css div垂直居中
//dad为外层
.dad {
    position: relative;
}
.son {
    position: absolute;
    margin: auto;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
}
//----
.dad {
    position: relative;
}
.son {
    width: 100px;
    height: 100px;
    position: absolute;
    top: 50%;
    left: 50%;
    margin-top: -50px;
    margin-left: -50px;
}
//----
.dad{
    position:relative;
}
.son{
    position:absolute;
    top:50%;
    left:50%;
    transform:translate(-50%,-50%);
}
//----
#dad {
    display: flex;
    justify-content: center;
    align-items: center
}
@5Mi
Copy link
Owner Author

5Mi commented May 17, 2017

/*
 * 经典面试题
 * 函数参数不定回调函数数目不定
 * 编写函数实现:
 * add(1,2,3,4,5)==15
 * add(1,2)(3,4)(5)==15
 */
function add() {
    // 第一次执行时,定义一个数组专门用来存储所有的参数
    var _args = [].slice.call(arguments);
    // 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值
    var adder = function () {
        var _adder = function() {
            [].push.apply(_args, [].slice.call(arguments));
            return _adder;
        };

        // 利用隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回
        _adder.toString = function () {
            return _args.reduce(function (a, b) {
                return a + b;
            });
        }

        return _adder;
    }
    return adder.apply(null, _args);
}
// 输出结果,可自由组合的参数
console.log(add(1, 2, 3, 4, 5));  // 15
console.log(add(1, 2, 3, 4)(5));  // 15
console.log(add(1)(2)(3)(4)(5));  // 15

@5Mi
Copy link
Owner Author

5Mi commented Aug 3, 2017

function Parent() {
            this.a = 1;
            this.b = [1, 2, this.a];
            this.c = { demo: 5 };
            this.show = function () {
                console.log(this.a , this.b , this.c.demo );
            }
        }
        function Child() {
            this.a = 2;
            this.change = function () {
                this.b.push(this.a);
                this.a = this.b.length;
                this.c.demo = this.a++;
            }
        }
        Child.prototype = new Parent(); 
        var parent = new Parent();
        var child1 = new Child();
        var child2 = new Child();
        child1.a = 11;
        child2.a = 12;
        parent.show();
        child1.show();
        child2.show();
        child1.change();
        child2.change();
        parent.show();
        child1.show();
        child2.show();

参考

@5Mi
Copy link
Owner Author

5Mi commented Oct 8, 2018

(function () {
    var x,y;  // 外部变量提升
    try {
        throw new Error();
    } catch (x/* 内部的x */) {
		x = 1; //内部的x,和上面声明的x不是一回事!!
         y = 2; //内部没有声明,作用域链向上找,外面的y
        console.log(x); //当然是1
    }
    console.log(x);  //只声明,未赋值,undefined
    console.log(y);  //就是2了
})();

// 1
// undefined
// 2

@5Mi
Copy link
Owner Author

5Mi commented Mar 2, 2019

js事件循环

js 异步执行的运行机制。

  1. 所有任务都在主线程上执行,形成一个执行栈。
  2. 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
  3. 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列"。那些对应的异步任务,结束等待状态,进入执行栈并开始执行。
  4. 主线程不断重复上面的第三步。

宏任务与微任务:

异步任务分为 宏任务(macrotask) 与 微任务 (microtask),不同的API注册的任务会依次进入自身对应的队列中,然后等待 Event Loop 将它们依次压入执行栈中执行。

  • 宏任务(macrotask):
    script(整体代码)、setTimeout、setInterval、UI 渲染、 I/O、postMessage、 MessageChannel、setImmediate(Node.js 环境)
  • 微任务(microtask):
    Promise、 MutaionObserver、process.nextTick(Node.js环境)

Event Loop(事件循环)中,每一次循环称为 tick, 每一次tick的任务如下:

  • 执行栈选择最先进入队列的宏任务(通常是script整体代码),如果有则执行
  • 检查是否存在 Microtask,如果存在则不停的执行,直至清空 microtask 队列
  • 更新render(每一次事件循环,浏览器都可能会去更新渲染)
  • 重复以上步骤
function testSometing() {
    console.log("执行testSometing");
    return "testSometing";
}

async function testAsync() {
    console.log("执行testAsync");
    return Promise.resolve("hello async");
}

async function test() {
    console.log("test start...");
    const v1 = await testSometing();//关键点1
    console.log(v1);
    const v2 = await testAsync();
    console.log(v2);
    console.log(v1, v2);
}

test();

var promise = new Promise((resolve)=> { console.log("promise start.."); resolve("promise");});//关键点2
promise.then((val)=> console.log(val));

console.log("test end...")

// VM256:12 test start...
// VM256:2 执行testSometing
// VM256:22 promise start..
// VM256:25 test end...
// VM256:14 testSometing
// VM256:7 执行testAsync
// VM256:23 promise
// VM256:16 hello async
// VM256:17 testSometing hello async

出处参考
出处参考
出处参考

@5Mi
Copy link
Owner Author

5Mi commented Jul 4, 2019

@5Mi
Copy link
Owner Author

5Mi commented Jun 15, 2020

  • 性能优化相关
    • 减少http请求

      • css js onInOne(代码合并)
      • inline image(10kb以下图片转成base64)
      • 客户端缓存(mate Cache-Control)
    • 布局背景图片使用样式background

    • css sprites雪碧图

    • 延时加载,预加载,按需加载

    • 减少dom数量与dom操作

    • 服务端静态资源长缓存 静态资源加MD5戳

    • 减少Cookie 去除无必要Cookie

    • 样式表置顶 --> <body> 内容 --> <script>(脚本置底) --></body>

    • 使用外部css和js方便浏览器缓存

    • 精简代码 代码压缩混淆 去除空格注释 优化压缩图片

    • 200(from cache) 替换 304

      • 304为请求后台确认资源 之后返回not modified
      • 200(from cache)为直接使用浏览器缓存
      • 服务端设置 页面meta标签无效 参考
    • 异步无阻塞加载JS: defer,async

    • css关键样式提取,内联, 使用骨架屏, 静态页面无头浏览器预渲染

    • preload,prefetch

    • dns-prefetch,preconnect


性能优化速记

  • code

    • 减少不必要的变量, 函数, 闭包
    • 适当缓存变量, 避免每次取值(如元素宽高引起重排, 循环中频繁取值等)
    • 及时释放, 事件解绑, 定时器, 对象引用
    • vue 中不需要响应式的数据 Object.freeze, 纯展示可用函数式组件
    • 不要频繁获取操作 dom, 统一一次性操作,批量化操作, 减少不必要的 dom, 减少 dom 数量, 减少 iframe
    • 使用类合并样式,避免逐条改变样式, 使用display控制DOM显隐,将DOM离线化
    • css 减少嵌套, 减少多余选择器, 减少通配选择器, 0值不加单位
    • 使用 esmodule, treeshaking
  • runtime

    • 默认图片, 图片懒加载
    • 代码分割, 按需加载
    • 列表分页, 虚拟列表
    • webworker 多线程计算
    • 防抖与节流
    • 请求缓存
    • css gpu 加速 will-change transform:translateZ(0);
    • css 内容可见性(content-visibility)
    • 时间分片 requestAnimationframe requestIdleCallback
  • deploy & http

    • 代码混淆,合并,压缩, gzip br, tree shaking, scope hosting, 资源压缩, 图片格式webp
    • 外链缓存 (js,css 使用外链, webpack external), 强缓存 (content hash), 协商缓存
    • 缓存使用优先级:
      1. Service Worker
      2. Memory Cache(内存)
      3. Disk Cache(硬盘)
    • 合理使用本地缓存: cookie,storage,indexDB 离线缓存Service Worker
    • 使用 CDN 加速
    • 减少cookie传输
    • link: preload , prefatch, dns-prefetch, preconnect, prerender
    • script: async, defer
    • http1.0 keep-alive, 多域名
    • 优先使用 http2.0, 多路复用 chunk 颗粒更细, 头部压缩
    • 样式表置顶 --> body 内容 --> script(脚本置底) --> body
    • css 关键样式提取,内联, 使用骨架屏, 静态页面无头浏览器预渲染

webkit 主资源与派生资源:

主资源,比如 HTML 页面,或者下载项,一类是派生资源,比如 HTML 页面中内嵌的图片或者脚本链接

200 from memory cache

不访问服务器,直接读缓存,从内存中读取缓存。此时的数据时缓存到内存中的,当kill进程后,也就是浏览器关闭以后,数据将不存在。仅派生资源(缓存 js脚本文件,css样式表文件,font字体文件,图片文件等静态文件)

200 from disk cache

不访问服务器,直接读缓存,从磁盘中读取缓存,当kill进程时,数据还是存在。这种方式也只能缓存派生资源

304 Not Modified

访问服务器,服务器返回此状态码表示资源仍可用, 然后从缓存中读取数据

三级缓存原理

先去内存看,如果有,直接加载
如果内存没有,择取硬盘获取,如果有直接加载
如果硬盘也没有,那么就进行网络请求
加载到的资源缓存到硬盘和内存

所以我们可以来解释这个现象 ,图片为例:

访问-> 200 -> 退出浏览器
再进来-> 200(from disk cache) -> 刷新 -> 200(from memory cache)


<script src="script.js">
没有 defer 或 async,浏览器会立即加载并执行指定的脚本,“立即”指的是在渲染该 script 标签之下的文档元素之前,也就是说不等待后续载入的文档元素,读到就加载并执行。(加载后,执行完,才能继续渲染)

<script async src="script.js">
有 async,加载和渲染后续文档元素的过程将和 script.js 的加载与执行并行进行(异步)。(async加载完就执行,但加载时不影响后续渲染)

<script defer src="myscript.js">
有 defer,加载后续文档元素的过程将和 script.js 的加载并行进行(异步),但是 script.js 的执行要在所有元素解析完成之后,DOMContentLoaded 事件触发之前完成。(defer执行按加载顺序)

script,async,defer

  • async: 当我们在 script 标记添加 async 属性以后,浏览器遇到这个 script 标记时会继续解析 DOM,同时脚本也不会被 CSSOM 阻止,即不会阻止 CRP。
  • defer: 与 async 的区别在于,脚本需要等到文档解析后( DOMContentLoaded 事件前)执行,而 async 允许脚本在文档解析时位于后台运行(两者下载的过程不会阻塞 DOM,但执行会)

参考


  • dns 预解析

    <link rel="dns-prefetch" href="//yuchengkai.cn" />

    DNS 解析也是需要时间的,可以通过预解析的方式来预先获得域名所对应的 IP。

    参考

  • preconnect

    <!-- CORS 的跨域请求,那么也要加上 crossorigin 的属性 -->
    <link href="https://cdn.domain.com" rel="preconnect" crossorigin />

    preconnect 允许浏览器在一个 HTTP 请求正式发给服务器前预先执行一些操作,这包括 DNS 解析,TLS 协商,TCP 握手,这消除了往返延迟并为用户节省了时间。

    参考

  • preload 与 prefetch

    <link rel="preload" href="/path/to/style.css" as="style" />

    preload 提前加载 顾名思义就是一种预加载的方式,它通过声明向浏览器声明一个需要提交加载的资源,当资源真正被使用的时候立即执行,就无需等待网络的消耗

    这种方式比通过 Link 方式加载资源方式更快,请求在返回还没到解析页面的时候就已经开始预加载资源了。

    <link rel="prefetch" href="/path/to/style.css" as="style" />

    prefetch 预判加载 prefetch 跟 preload 不同,

    1. 它的作用是告诉浏览器未来可能会使用到的某个资源,浏览器就会在闲时去加载对应的资源,若能预测到用户的行为,比如懒加载,点击到其它页面等则相当于提前预加载了需要的资源。
    2. 对于使用 prefetch 获取资源,其优先级默认为最低,Lowest,可以认为当浏览器空闲的时候才会去获取的资源。
      而对于 preload 获取资源,可以通过 "as" 或者 "type" 属性来标识他们请求资源的优先级(比如说 preload 使用 as="style" 属性将获得最高的优先级,即使资源不是样式文件)
    3. 没有 “as” 属性的将被看作异步请求
  • prerender

    <link rel="prerender" href="http://example.com" />

    可以通过预渲染将下载的文件预先在后台渲染,可以使用以下代码开启预渲染

    预渲染虽然可以提高页面的加载速度,但是要确保该页面百分百会被用户在之后打开,否则就白白浪费资源去渲染


DNS Prefetching
Preconnect


参考,性能指标,相关优化
转载,Web性能优化地图
参考

@5Mi
Copy link
Owner Author

5Mi commented May 14, 2021

cookie 相关

cookie一般用于保存信息,你向同一个服务器发请求时会带上浏览器保存的对于那个服务器的cookie,而不管你从哪个网站发请求。所以如果用cookie校验权限则会导致csrf攻击的成立, 即 当你在当前网站(A网站)登录后, 浏览第三方页面(伪造网站等), 在第三方页面上发起对A网站的请求

但目前一般权限校验采用JWT,请求头Authoritarian传递token校验用户权限,规避cookie自动携带的隐患

再就是目前 cookie same-site 属性的使用, 限制跨站请求时 携带cookie的行为. 即加上之前 cookie的 domain属性和path属性, 当向服务器发起请求时满足 domainpath 才携带传递, same-site 则跟进一步要校验当前访问的网站网站内请求的服务地址是否跨站, 下图为 same-site: Lax 等值之间的区别

cookie-samesite

再就是第三方cookie Third-party cookies

But when you visit a domain such as www.somedomain.com, the web pages on that domain may feature content from a third-party domain. For instance, there may be an advertisement run by www.anotherdomain.com showing graphic advert banners. When your web browser asks for the banner image from www.anotherdomain.com, that third-party domain is allowed to set a cookie. Each domain can only read the cookie it created, so there should be no way of www.anotherdomain.com reading the cookie created by www.somedomain.com. So what's the problem?

Some people don't like third-party cookies for the following reason: suppose that the majority of sites on the internet have banner adverts from www.anotherdomain.com. Now it's possible for the advertiser to use its third-party cookie to identify you as you move from one site with its adverts to another site with its adverts.

Even though the advertiser from www.anotherdomain.com may not know your name, it can use the random ID number in the cookie to build up an anonymous profile of the sites you visit. Then, when it spots the unique ID in the third-party cookie, it can say to itself: "visitor 3E7ETW278UT regularly visits a music site, so show him/her adverts about music and music products".

Some people don't like the idea of advertising companies building up profiles about their browsing habits, even if the profile is anonymous.

如果大多网站都有嵌入类似 阿里妈妈, 百度分析,google分析,faceboo广告等第三方请求(js,跨域请求,链接等), 则same-site普及前,不同站点间浏览时,即浏览 A网站->B网站->C网站, 如果A,B,C都有同一个第三方分析的js, 那么第三方的cookie就会在多个网站的第三方请求中携带, 则第三方虽然不清楚你具体的用户信息,但仍可分析当前特定用户的浏览行为,访问习惯, 做到精准推送,用户行为分析

@5Mi
Copy link
Owner Author

5Mi commented May 15, 2021

浏览器相关

渲染相关

  1. 处理 HTML 并构建 DOM 树。
  2. 处理 CSS 构建 CSSOM 树。
  3. 将 DOM 与 CSSOM 合并成一个渲染树。
  4. 根据渲染树来布局,计算每个节点的位置。
  5. 调用 GPU 绘制,合成图层,显示在屏幕上

browser_render

  • CSS不会阻塞DOM解析,但会阻塞DOM渲染。
  • CSS会阻塞JS执行,并不会阻塞JS文件下载

DOM 和 CSSOM通常是并行构建的,所以 「CSS 加载不会阻塞 DOM 的解析」
然而由于Render Tree 是依赖DOM Tree和 CSSOM Tree的,所以它必须等到两者都加载完毕后,完成相应的构建,才开始渲染,因此, 「CSS加载会阻塞DOM渲染」
由于 JavaScript 是可操纵 DOM 和 css 样式 的,如果在修改这些元素属性同时渲染界面(即 JavaScript 线程和 UI 线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。
因此为了防止渲染出现不可预期的结果,浏览器设置 「GUI 渲染线程与 JavaScript 引擎为互斥」 的关系

参考

Load 和 DOMContentLoaded 区别

Load 事件触发代表页面中的 DOM,CSS,JS,图片已经全部加载完毕。

DOMContentLoaded 事件触发代表初始的 HTML 被完全加载和解析,不需要等待 CSS,JS,图片加载

常见状态码

  • 成功

    • 200 OK : 请求成功
  • 重定向

    • 301 Moved Permanently: 永久重定向 被请求的资源已永久移动到新位置
    • 302 Move temporarily: 临时重定向

    301和302状态码都表示重定向,就是说浏览器在拿到服务器返回的这个状态码后会自动跳转到一个新的URL地址,这个地址可以从响应的Location首部中获取(用户看到的效果就是他输入的地址A瞬间变成了另一个地址B)——这是它们的共同点。

    他们的不同在于。301表示旧地址A的资源已经被永久地移除了(这个资源不可访问了),搜索引擎在抓取新内容的同时也将旧的网址交换为重定向之后的网址;302表示旧地址A的资源还在(仍然可以访问),这个重定向只是临时地从旧地址A跳转到地址B,搜索引擎会抓取新的内容而保存旧的网址 参考

    • 304 Not Modified: 资源未修改,可使用缓存 (协商缓存)
  • 请求错误 (资源访问受限)

    • 400 Bad Request: 语义有误,当前请求无法被服务器理解等
    • 401 Unauthorized: 当前请求需要用户验证
    • 403 Forbidden: 禁止访问 服务器已经理解请求,但是拒绝执行它
    • 404 Not Found: 资源未找到 请求所希望得到的资源未被在服务器上发现
    • 405 Method Not Allowed: 请求行中指定的请求方法不能被用于请求相应的资源
  • 服务器错误

    • 500 Internal Server Error: 服务器未知错误, 一般可能出现在服务端源码错误
    • 502 Bad Gateway: 作为网关或者代理工作的服务器尝试执行请求时,从上游服务器接收到无效的响应。

参考

浏览器缓存, 强缓存,协商缓存

  • 强缓存

    • Expires (过期时间)
    • Cache-Control: max-age
      Cache-Control:max-age=6000表示资源返回后6000秒内,可以直接使用缓存

    当Expires和Cache-Control同时存在时,优先考虑Cache-Control。
    当然了,当缓存资源失效了,也就是没有命中强缓存,接下来就进入协商缓存

  • 协商缓存

    • Last-Modified:

      这个字段表示的是「最后修改时间」。在浏览器第一次给服务器发送请求后,服务器会在响应头中加上这个字段。

      浏览器接收到后,「如果再次请求」,会在请求头中携带If-Modified-Since字段,这个字段的值也就是服务器传来的最后修改时间

      服务器拿到请求头中的If-Modified-Since的字段后,其实会和这个服务器中该资源的最后修改时间对比:

      如果请求头中的这个值小于最后修改时间,说明是时候更新了。返回新的资源,跟常规的HTTP请求响应的流程一样。否则返回304,告诉浏览器直接使用缓存。

    • ETag

      ETag是服务器根据当前文件的内容,对文件生成唯一的标识,比如MD5算法,只要里面的内容有改动,这个值就会修改,服务器通过把响应头把该字段给浏览器。

      浏览器接受到ETag值,会在下次请求的时候,将这个值作为「If-None-Match」这个字段的内容,发给服务器。

      服务器接收到「If-None-Match」后,会跟服务器上该资源的「ETag」进行比对👇

      如果两者一样的话,直接返回304,告诉浏览器直接使用缓存如果不一样的话,说明内容更新了,返回新的资源,跟常规的HTTP请求响应的流程一样

    • 两者对比

      性能上,Last-Modified优于ETag,Last-Modified记录的是时间点,而Etag需要根据文件的MD5算法生成对应的hash值。精度上,ETag优于Last-Modified。ETag按照内容给资源带上标识,能准确感知资源变化,Last-Modified在某些场景并不能准确感知变化,比如👇

      编辑了资源文件,但是文件内容并没有更改,这样也会造成缓存失效。Last-Modified 能够感知的单位时间是秒,如果文件在 1 秒内改变了多次,那么这时候的 Last-Modified 并没有体现出修改了。

    最后,「如果两种方式都支持的话,服务器会优先考虑ETag」。

安全机制

  • 同源策略与跨域

    • 如果协议、域名或者端口有一个不同就是跨域,Ajax 请求会失败(发送成功,服务器可接收,浏览器会拦截返回由于跨域失败)

    • image,style,script标签没有跨域限制

    • 不同源的客户端脚本 (如当前源与跨域iframe) ,在没有明确授权的情况下,不能读写对方的资源。

      1. Cookie、LocalStorage 和 IndexDB 无法读取
      2. DOM 和 JS 对象无法获取
      3. Ajax请求发送不出去
  • 跨域解决方案

    • JSONP

    JSONP 使用简单且兼容性不错,但是只限于 get 请求, 利用 <script> 标签没有跨域限制的漏洞。通过 <script> 标签指向一个需要访问的地址并提供一个回调函数来接收数据

    <!-- 服务器将返回js内容为 一个回调函数的执行  -->
    <!-- jsonp(参数写有服务器返回的数据) -->
    <script src="http://domain/api?param1=a&param2=b&callback=jsonp"></script>
    <script>
      function jsonp(data) {
      	console.log(data)
        }
    </script>
    function jsonp(url, jsonpCallback, success) {
      let script = document.createElement('script')
      script.src = url
      script.async = true
      script.type = 'text/javascript'
      window[jsonpCallback] = function(data) {
        success && success(data)
      }
      document.body.appendChild(script)
    }
    jsonp('http://xxx', 'callback', function(value) {
      console.log(value)
    })
    • CORS 跨域资源共享

      服务端设置 Access-Control-Allow-Origin 就可以开启 CORS。 该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源

      跨域资源共享,简单请求,复杂请求

    • nginx,devserver等服务代理 proxy

    • iframe间使用postMessage [实现跨文档消息传输]

        <!-- 父页面中 -->
        <iframe id="iframe" src="http://someIframePage.com/index.html"></iframe>
        <script>      
        // https://developer.mozilla.org/zh-CN/docs/Web/API/Window/postMessage 
          var iframe = document.getElementById('iframe');
          // 向iframe传送跨域数据
          iframe.contentWindow.postMessage(JSON.stringify(data), 'http://someIframePage.com.com');
      
          // 接受postMessage消息, 监听message事件
          window.addEventListener("message", receiveMessage, false)
          function receiveMessage(event){
            var origin = event.origin || event.originalEvent.origin
            if (origin === 'http://someIframePage.com.com') {
              console.log('验证通过')
            }
          }
        </script>
      <!-- iframe页面中 -->
      <script>
        window.parent.postMessage(JSON.stringify(data), 'http://parentPage.com')
      
        // 接受postMessage消息, 监听message事件
        window.addEventListener("message", receiveMessage, false)
        function receiveMessage(event){
          var origin = event.origin || event.originalEvent.origin
          if (origin === 'http://parentPage.com') {
            console.log('验证通过')
          }
        }
      </script>  

事件触发三阶段

捕获 -> 目标 -> 冒泡

event-captrue


「查缺补漏」送你18道浏览器面试题

浏览器相关

@5Mi
Copy link
Owner Author

5Mi commented May 19, 2021

vue 响应式原理:

简要实现双向事件绑定:

  1. 实现一个监听器 Observer ,用来劫持并监听所有属性,如果属性发生变化,就通知订阅者;
  2. 实现一个订阅器 Dep,用来收集订阅者,对监听器 Observer 和 订阅者 Watcher 进行统一管理;
  3. 实现一个订阅者 Watcher,可以收到属性的变化通知并执行相应的方法,从而更新视图;
  4. 实现一个解析器 Compile,可以解析每个节点的相关指令,对模板数据和订阅器进行初始化

监听器 Observer 实现

/**
  * 循环遍历数据对象的每个属性
  */
function observable(obj) {
    if (!obj || typeof obj !== 'object') {
        return;
    }
    let keys = Object.keys(obj);
    keys.forEach((key) => {
        defineReactive(obj, key, obj[key])
    })
    return obj;
}
/**
 * 将对象的属性用 Object.defineProperty() 进行设置
 */
defineReactive: function(data, key, val) {
	var dep = new Dep();
	Object.defineProperty(data, key, {
		enumerable: true,
		configurable: true,
		get: function getter () {
      // 收集依赖
			if (Dep.target) {
				dep.addSub(Dep.target);
			}
			return val;
		},
		set: function setter (newVal) {
			if (newVal === val) {
				return;
			}
			val = newVal;
      // 通知订阅者
			dep.notify();
		}
	});
}

消息订阅器 Dep,用来容纳所有的“订阅者”。订阅器 Dep 主要负责收集订阅者,然后当数据变化的时候后执行对应订阅者的更新函数

function Dep () {
    // 储存订阅者 watcher 
    this.subs = [];
}
Dep.prototype = {
    addSub: function(sub) {
        this.subs.push(sub);
    },
    notify: function() {
        this.subs.forEach(function(sub) {
            sub.update();
        });
    }
};
Dep.target = null;

订阅者 Watcher 初始化的时候触发watcherget 函数去执行添加订阅者操作, 通过赋值 Dep.target 缓存下订阅者,接着获取响应对象的值,触发其getter 使watcherdep收集 , 收集依赖后再将Dep.target清空,

function Watcher(vm, exp, cb) {
    this.vm = vm;
    this.exp = exp;
    this.cb = cb;
    this.value = this.get();  // 将自己添加到订阅器的操作
}

Watcher.prototype = {
    update: function() {
        this.run();
        // 实际中会区分更新是 同步还是异步的
        // 异步的话 执行异步watcher队列 queueWatcher(this)
    },
    run: function() {
        var value = this.vm.data[this.exp];
        var oldVal = this.value;
        if (value !== oldVal) {
            this.value = value;
            this.cb.call(this.vm, value, oldVal);
        }
    },
    get: function() {
        Dep.target = this; // 全局变量 订阅者 赋值
        var value = this.vm.data[this.exp]  // 强制执行监听器里的get函数,使dep收集依赖
        Dep.target = null; // 全局变量 订阅者 释放
        return value;
    }
};

订阅者 Watcher 分析如下:

订阅者 Watcher 是一个 类,在它的构造函数中,定义了一些属性:

  • vm: 一个 Vue 的实例对象;
  • exp: 是 node 节点的 v-model 等指令的属性值 或者插值符号中的属性。如 v-model="name"exp 就是name;
  • cb:Watcher 绑定的更新函数;

当我们去实例化一个渲染 watcher 的时候,首先进入 watcher 的构造函数逻辑,就会执行它的 this.get() 方法,进入 get 函数,首先会执行

Dep.target = this; // 将自己赋值为全局的订阅者

实际上就是把 Dep.target 赋值为当前的渲染 watcher ,接着又执行了

// 强制执行响应对象的get函数, 使dep收集Dep.target 即当前watcher
let value = this.vm.data[this.exp]  

在这个过程中会对 vm 上的数据访问,其实就是为了触发数据对象的 getter

每个对象值的 getter 都持有一个 dep,在触发 getter 的时候会调用 dep.depend() 方法,也就会执行this.addSub(Dep.target),即把当前的 watcher 订阅到这个数据持有的 depwatchers 中,这个目的是为后续数据变化时候能通知到哪些 watchers 做准备。

这样实际上已经完成了一个依赖收集的过程。那么到这里就结束了吗?其实并没有,完成依赖收集后,还需要把 Dep.target 恢复成上一个状态,即

Dep.target = null; // 释放自己

update() 函数是用来当数据发生变化时调用 Watcher 自身的更新函数进行更新的操作。先通过 let value = this.vm.data[this.exp]; 获取到最新的数据,然后将其与之前 get() 获得的旧数据进行比较,如果不一样,则调用更新函数 cb 进行更新

解析器 Compile 关键逻辑代码分析

​ 通过监听器 Observer 订阅器 Dep 和订阅者 Watcher 的实现,其实就已经实现了一个双向数据绑定的例子,但是整个过程都没有去解析 dom 节点,而是直接固定某个节点进行替换数据的,所以接下来需要实现一个解析器 Compile 来做解析和绑定工作。

解析器 Compile 实现步骤:

  • 解析模板指令,并替换模板数据,初始化视图;
  • 将模板指令对应的节点绑定对应的更新函数,初始化相应的订阅器;

我们下面对 {{变量}} 这种形式的指令处理的关键代码进行分析,感受解析器 Compile 的处理逻辑,关键代码如下:

  compileText: function(node, exp) {
    var self = this;
    var initText = this.vm[exp]; // 获取属性值
    this.updateText(node, initText); // dom 更新节点文本值
      // 将这个指令初始化为一个订阅者,后续 exp 改变时,就会触发这个更新回调,从而更新视图
    new Watcher(this.vm, exp, function (value) { 
      self.updateText(node, value);
    });
  }

简单理解为:

  1. Compile解析组件模板, 将更新函数传入 new watcher实例的回调
  2. watcher实例调用自身get 赋值监听器dep.target,再获取响应对象的值, 来触发可响应对象的get
  3. 可响应对象get中 将当前存放有watcher实例的 dep.target 存入此属性对应的dep实例的sub数组
  4. 当可响应对象(observer)的值改动,触发对应的set函数,此属性对应的dep实例将调用dep.notify()遍历所有订阅者watcher实例
  5. watcher实例调用其run()方法,执行new watcher时传入的回调, 即 Complie解析时 {{变量}} 对应的更新函数(修改对应的dom内容等)

vue nexttick 原理:

<template>
  <div class="box">{{msg}}</div>
</template>

export default {
  name: 'index',
  data () {
    return {
      msg: 'hello'
    }
  },
  mounted () {
    this.msg = 'world'
    let box = document.getElementsByClassName('box')[0]
    console.log(box.innerHTML) // hello
  }
}

以上代码可以看到,修改数据后dom并没有立刻更新,vue中dom的更新机制是异步的,(会对异步更新队列中的watcher去重再统一更新dom) 无法通过同步代码获取,需要使用nextTick,在下一次事件循环中获取

this.msg = 'world'
let box = document.getElementsByClassName('box')[0]
// 如果我们需要获取数据更新后的dom信息,
// 比如动态获取宽高、位置信息等,需要使用nextTick
this.$nextTick(() => {
  console.log(box.innerHTML) // world
})

解析:

双向绑定原理: setter->Dep->Watcher->update 触发至watcher.update() 执行异步队列 queueWatcher

// watcher update
update () {
  /* istanbul ignore else */
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
    // dom操作 执行异步队列 queueWatcher
    queueWatcher(this)
  }
}

queueWatcher -> nextTick(flushSchedulerQueue)

function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(watcher)
    } else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // queue the flush
    if (!waiting) {
      // 通过waiting 保证nextTick只执行一次
      waiting = true
      // 最终queueWatcher 方法会把flushSchedulerQueue 传入到nextTick中执行
      nextTick(flushSchedulerQueue)
    }
  }
}

其中 flushSchedulerQueue -> watcher.run()

function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id
  ...
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    // 遍历执行渲染watcher的run方法 完成视图更新
    watcher.run()
  }
  // 重置waiting变量 
  resetSchedulerState()
  ...
}

当数据变化最终会把flushSchedulerQueue传入到nextTick中执行flushSchedulerQueue函数会遍历执行watcher.run()方法,watcher.run()方法最终会完成视图更新,接下来我们看关键的nextTick方法到底是啥

nextTick方法会将传进来的回调push进callbacks数组,然后执行timerFunc方法

export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  // push进callbacks数组
  callbacks.push(() => {
     cb.call(ctx)
  })
  if (!pending) {
    pending = true
    // 执行timerFunc方法
    timerFunc()
  }
}

timerFunc 决定将 nexttick回调 推入微任务还是 宏任务

let timerFunc
// 判断是否原生支持Promise
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    // 如果原生支持Promise 用Promise执行flushCallbacks
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
// 判断是否原生支持MutationObserver
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  let counter = 1
  // 如果原生支持MutationObserver 用MutationObserver执行flushCallbacks
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
// 判断是否原生支持setImmediate 
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  timerFunc = () => {
  // 如果原生支持setImmediate  用setImmediate执行flushCallbacks
    setImmediate(flushCallbacks)
  }
// 都不支持的情况下使用setTimeout 0
} else {
  timerFunc = () => {
    // 使用setTimeout执行flushCallbacks
    setTimeout(flushCallbacks, 0)
  }
}

// flushCallbacks 最终执行nextTick 方法传进来的回调函数
function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

响应式对象设置属性值 响应式触发setter->Dep->Watcher->update-> queueWatcher -> nextTick(flushSchedulerQueue) -> nextticktimerFunc 决定如何执行nextTickHandlerflushSchedulerQueue 推入微任务还是 宏任务 -> flushSchedulerQueue 队列根据watcherId去重后 执行每一个watcher.run() 直到清空队列, 此处watcher.run() 则含有同步更新dom操作(diff算法patch之后dom已被操作更新)

之后用户再使用vue.nextTick(callback)则可确保回调函数在dom跟新后执行

由于宏任务耗费的时间是大于微任务的,所以在浏览器支持的情况下,优先使用微任务。如果浏览器不支持微任务,再使用宏任务
延迟调用优先级如下:
Promise > MutationObserver > setImmediate > setTimeout

参考 event loop

macrotask -> microtask queue -> UI render -> macrotask (nextTick 循环)

注意不要把“UI渲染”跟“DOM更新”这2个混为一谈了,“UI渲染”确实是所有微任务完成之后,是异步的。而“DOM更新”是同步的(程序中),只不过用户想在页面观察到变化需要等待UI渲染之后

Vue nextTick 机制

vue源码解析:nextTick

0 到 1 掌握:Vue 核心之数据双向绑定


vue-vuex中使用commit提交mutation来修改state的源码解析

开启严格模式,仅需在创建 store 的时候传入 strict: true;
在严格模式下,无论何时发生了状态变更且不是由 mutation 函数引起的,将会抛出错误。这能保证所有的状态变更都能被调试工具跟踪到。

vuex 设置严格模式参数为true, 调用了 $watch 函数来观察state的变化。当state变化时,会判断 store._committing(这个值在调用commit方法时才修改) 的值,如果不为 true,就会报出异常

vuex enableStrictMode

if(process.env.NODE_ENV !== 'production'){
  assert(store._committing,`Do not mutate vuex store state outside mutation handlers.`)
}

通过commit 调用mutation 才修改_committing开关

commit

虽然直接修改state,state可以修改成功,并且依然是响应式的, 哪怕在严格模式下也只是抛出错误(依旧可修改,并可响应),但是还是应该按照规范使用commit提交mutation的方式, 这样才能被vuex 及其vue开发工具更好的管理记录,数据的流向还原才更加清晰

@5Mi
Copy link
Owner Author

5Mi commented May 19, 2021

重绘与重排

重新渲染,就需要重新生成布局和重新绘制。前者叫做重排(reflow 或 回流),后者叫做重绘(repaint)

需要注意的是,重绘不一定需要重排,重排必然导致重绘。为了提高网页性能,就要降低"重排"和"重绘"的频率和成本,尽量少触发重新渲染

浏览器为了重新渲染部分或整个页面,重新计算页面元素位置和几何结构的进程叫做reflow

  • 一个 DOM 元素的几何属性变化,常见的几何属性有width、height、padding、margin、left、top、border 等等, 这个很好理解。
  • 使可见的 DOM 节点发生增减或者移动。
  • 读写 offset族、scroll族和client族属性的时候,浏览器为了获取这些值,需要进行回流操作。
  • 调用 window.getComputedStyle 方法。
  • 内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代。
  • 页面一开始渲染的时候(这肯定避免不了)
  • 浏览器的窗口尺寸变化(因为回流是根据视口的大小来计算元素的位置和大小的)

一些常用且会导致回流的属性和方法:

  • clientWidth、clientHeight、clientTop、clientLeft
  • offsetWidth、offsetHeight、offsetTop、offsetLeft
  • scrollWidth、scrollHeight、scrollTop、scrollLeft
  • scrollIntoView()、scrollIntoViewIfNeeded()
  • getComputedStyle()
  • getBoundingClientRect()
  • scrollTo()

当你获取布局信息的操作的时候,会强制队列刷新,比如当你访问以上属性或者使用以上方法,以上属性和方法都需要返回最新的布局信息,因此浏览器不得不清空队列,触发回流重绘来返回正确的值。因此,我们在修改样式的时候,最好避免使用上面列出的属性,他们都会刷新渲染队列。如果要使用它们,最好将值缓存起来

触发回流的时候,如果 DOM 结构发生改变,则重新渲染 DOM 树,然后将后面的流程(包括主线程之外的任务)全部走一遍

重排

当页面中元素样式的改变并不影响它在文档流中的位置时(例如:colorbackground-colorvisibility等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘

于没有导致 DOM 几何属性的变化,因此元素的位置信息不需要更新,从而省去布局的过程,流程如下:

重绘

跳过了布局树建图层树,直接去绘制列表,然后在去分块,生成位图等一系列操作。可以看到,重绘不一定导致回流,但回流一定发生了重绘

合成

还有一种情况:就是更改了一个既不要布局也不要绘制的属性,那么渲染引擎会跳过布局和绘制,直接执行后续的合成操作,这个过程就叫合成。

举个例子:比如使用CSS的transform来实现动画效果,避免了回流跟重绘,直接在非主线程中执行合成动画操作。显然这样子的效率更高,毕竟这个是在非主线程上合成的,没有占用主线程资源,另外也避开了布局和绘制两个子阶段,所以相对于重绘和重排,合成能大大提升绘制效率
利用这一点好处:

  • 合成层的位图,会交由 GPU 合成,比 CPU 处理要快
  • 当需要 repaint 时,只需要 repaint 本身,不会影响到其他的层
  • 对于 transform 和 opacity 效果,不会触发 layout 和 paint

提升合成层的最好方式是使用 CSS 的 will-change 属性

回流-重绘-合成

你真的了解回流和重绘吗

css优化之重排与重绘

css3的translate会引起重排吗, 并不会,不是同一个复合图层

不会,因为 GPU 进程会为其开启一个新的复合图层,不会影响默认复合图层(就是普通文档流),所以并不会影响周边的 DOM 结构,而属性的改变也会交给 GPU 处理,不会进行重排。使 GPU 进程开启一个新的复合图层的方式还有 3D 动画,过渡动画,以及 opacity 属性,还有一些标签,这些都可以创建新的复合图层。这些方式叫做硬件加速方式。你可以想象成新的复合图层和默认复合图层是两幅画,相互独立,不会彼此影响。降低重排的方式:要么减少次数,要么降低影响范围,创建新的复合图层就是第二种优化方式。绝对布局虽然脱离了文档流,但不会创建新的复合图层,因此当绝对布局改变时,不会影响普通文档流的 render tree,但是依然会绘制整个默认复合图层,对普通文档流是有影响的。普通文档流就是默认复合图层,不要介意我交换使用它们如果你要使用硬件加速方式降低重排的影响,请不要过度使用,创建新的复合图层是有额外消耗的,比如更多的内存消耗,并且在使用硬件加速方式时,配合 z-index 一起使用,尽可能使新的复合图层的元素层级等级最高


各种height, width

scrollWidth,clientWidth,offsetWidth的区别

无滚动
scrollWidth,clientWidth,offsetWidth
有滚动
scrollWidth,clientWidth,offsetWidth

width

pageX,clientX,screenX,offsetX区别

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant