5Mi / wumi_blog

for recording improvimg my experience
21 stars 0 forks source link

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

Open 5Mi opened 7 years ago

5Mi commented 7 years ago

转自前端进阶

MS

    window.color = "red";

    var o = { color: "blue" };

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

    sayColor();//"red"

    o.sayColor = sayColor; 
    o.sayColor();//blue
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
    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();//克隆数组
    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);

    }

模拟实现

        //
    //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']);
    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);
            }
        });
    };
}
//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 commented 7 years ago
/*
 * 经典面试题
 * 函数参数不定回调函数数目不定
 * 编写函数实现:
 * 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 commented 7 years ago
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 commented 6 years ago
(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 commented 5 years ago

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 commented 5 years ago

掘金上看到的不错的

掘金上, 细节很值得回顾的基础题

5Mi commented 4 years ago

性能优化速记


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

参考



DNS Prefetching Preconnect


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

5Mi commented 3 years ago

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 commented 3 years ago

浏览器相关

渲染相关

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

browser_render

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,图片加载

常见状态码

参考

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

安全机制

事件触发三阶段

捕获 -> 目标 -> 冒泡

event-captrue


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

浏览器相关

5Mi commented 3 years ago

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 是一个 类,在它的构造函数中,定义了一些属性:

当我们去实例化一个渲染 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 commented 3 years ago

重绘与重排

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

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

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

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

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

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

重排

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

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

重绘

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

合成

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

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

提升合成层的最好方式是使用 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区别