前端知识点总结

·  阅读 334
前端知识点总结

JavaScript 基础

1.1 执行上下文/作用域链/闭包

1.1.1 理解 JavaScript 中的执行上下文和执行栈
什么是执行上下文?
复制代码

简而言之,执行上下文是评估和执行 JavaScript 代码的环境的抽象概念。每当 JavaScript 代码在运行的时候,它都是在执行上下文中进行。

执行上下文的类型
复制代码

JavaScript 中有三种执行上下文类型

  • 全局执行上下文 — 这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事情:创建一个全局的 window 对象(浏览器的情况下),并且设置 this 的值等于这个全局对象,一个程序中只有有一个全局执行上下文。
  • 函数执行上下文 — 每当一个函数被调用时,都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序执行一系列步骤。
  • Eval函数执行上下文 — 执行在 eval 函数内部的代码也会有它属于自己的执行上下文,但是由于 JavaScript 开发者并不经常使用 eval。
执行栈
什么是执行栈?
复制代码

执行栈也就是在其它编程语言中所说的“调用栈”,是一种LIFO(后进先出)数据结构的栈,被用来存储代码运行时创建的所有执行上下文。

当 JavaScript 引擎第一次遇到你的脚本时,它会创建一个全局的执行上下文并且压入当前执行栈。每当引擎遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入栈的顶部。

引擎会执行那些执行上下文位于栈顶的函数。当该函数执行结束时,执行上下文从栈中弹出,控制流程到达当前栈中的下一个上下文。

例如下面的代码:

let a = 'Hello World!';

function first() {
  console.log('Inside first function');
  second();
  console.log('Again inside first function');
}

function second() {
  console.log('Inside second function');
}

first();
console.log('Inside Global Execution Context');
复制代码

执行栈图片 上述代码的执行上下文栈

当上述代码在浏览器加载时,JavaScript 引擎创建了一个全局执行上下文并把它压入当前执行栈。当遇到 first() 函数调用时,JavaScript 引擎为该函数创建一个新的执行上下文并把它压入当前执行栈的顶部。

当从 first() 函数内部调用 second() 函数时,JavaScript 引擎为 second() 函数创建了一个新的执行上下文并把它压入当前执行栈的顶部。当 second() 函数执行完毕,它的执行上下文会从当前栈弹出,并且控制流程到达下一个执行上下文,即 first() 函数的执行上下文。

当 first() 执行完毕,它的执行上下文从栈弹出,控制流程到达全局执行上下文。一旦所有代码执行完毕,JavaScript 引擎从当前栈中移除全局执行上下文。

怎么创建执行上下文?

创建执行上下文有两个阶段:1)创建阶段 2)执行阶段

The Creation Phase

在 JavaScript 代码执行前,执行上下文将经历创建阶段。在创建阶段发生三件事:

  1. this 值的决定,即我们所熟知的 This 绑定。
  2. 创建词法环境组件
  3. 创建变量环境组件。

所以执行上下文在概念上表示如下:

ExecutionContext = {
        ThisBinding = <this value>,
        LexicalEnvironment = { ... },
        VariableEnvironment = { ... },
    }
复制代码

This 绑定: 在全局执行上下文中,this 的值指向全局对象。(在 浏览器中,this 引用 Window 对象)

在函数执行上下文中,this 的值取决于该函数是如何被调用的。如果它被一个引用对象调用,那么 this 会被设置成 那个对象,否则 this 的值被设置为全局对象或者 undefined。

	let foo = {
                baz: function() {
                console.log(this);
              }
           }
            
            foo.baz();   // 'this' 引用 'foo', 因为 'baz' 被
                            // 对象 'foo' 调用
            let bar = foo.baz;
            
            bar();       // 'this' 指向全局 window 对象,因为
                        // 没有指定引用对象
                        
复制代码

词法环境

简单来说词法环境是一种持有标识符—变量映射的结构。(这里的标识符指的是变量/函数的名字,而变量是对实际对象[包含函数类型对象]或原始数据的引用)。

现在,在词法环境的内部有两个组件:(1) 环境记录器和 (2) 一个外部环境的引用。

  1. 环境记录器是存储变量和函数声明的实际位置。
  2. 外部环境的引用意味着它可以访问其父级词法环境(作用域)。

词法环境有两种类型:

  • 全局环境 在全局执行上下文中)是没有外部环境引用的词法环境。全局环境的外部环境引用是 null。它拥有内建的 Object/Array/等、在环境记录器内的原型函数(关联全局对象,比如 window 对象)还有任何用户定义的全局变量,并且 this的值指向全局对象。
  • 在函数环境中,函数内部用户定义的变量存储在环境记录器中。并且引用的外部环境可能是全局环境,或者任何包含此内部函数的外部函数。

变量环境

它同样是一个词法环境,其环境记录器持有变量声明语句在执行上下文中创建的绑定关系。

如上所述,变量环境也是一个词法环境,所以它有着上面定义的词法环境的所有属性。

在 ES6 中,词法环境组件和变量环境的一个不同就是前者被用来存储函数声明和变量(let 和 const)绑定,而后者只用来存储 var 变量绑定。

我们看点样例代码来理解上面的概念:

let a = 20;
const b = 30;
var c;

function multiply(e, f) {
 var g = 20;
 return e * f * g;
}

c = multiply(20, 30);
复制代码

执行上下文看起来像是这样:

GlobalExectionContext = {

  ThisBinding: <Global Object>,

  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // 在这里绑定标识符
      a: < uninitialized >,
      b: < uninitialized >,
      multiply: < func >
    }
    outer: <null>
  },

  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // 在这里绑定标识符
      c: undefined,
    }
    outer: <null>
  }
}

FunctionExectionContext = {
  ThisBinding: <Global Object>,

  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // 在这里绑定标识符
      Arguments: {0: 20, 1: 30, length: 2},
    },
    outer: <GlobalLexicalEnvironment>
  },

VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // 在这里绑定标识符
      g: undefined
    },
    outer: <GlobalLexicalEnvironment>
  }
}

复制代码

注意 — 只有遇到调用函数 multiply 时,函数执行上下文才会被创建。

可能你已经注意到 let 和 const 定义的变量并没有关联任何值,但 var 定义的变量被设成了 undefined。

这是因为在创建阶段时,引擎检查代码找出变量和函数声明,虽然函数声明完全存储在环境中,但是变量最初设置为 undefined(var 情况下),或者未初始化(let 和 const 情况下)。

这就是为什么你可以在声明之前访问 var 定义的变量(虽然是 undefined),但是在声明之前访问 let 和 const 的变量会得到一个引用错误。

这就是我们说的变量声明提升。

执行阶段

这是整篇文章中最简单的部分。在此阶段,完成对所有这些变量的分配,最后执行代码。

注意 — 在执行阶段,如果 JavaScript 引擎不能在源码中声明的实际位置找到 let 变量的值,它会被赋值为 undefined。

最后记住一点,特别重要:
词法作用域是指内部函数在定义的时候就决定了其外部作用域。这一点在闭包相关的部分尤为重要。
复制代码

1.2 this/call/apply/bind

要领:如果要判断一个函数的 this 绑定,就需要找到这个函数的直接调用位置,然后可以顺序按照下面四条规则来判断 this 的绑定对象:

  1. 由 new 调用:绑定到新创建的对象
  2. 由 call 或者 apply 、bind 调用:绑定到指定的对象
  3. 由上下文对象调用:绑定到上下文对象
  4. 默认:全局对象

注意:箭头函数不使用上面的绑定规则,根据外层的作用域来决定 this,继承外层函数调用的 this 绑定。

this 指向的三种情况:

  1. obj.fun() fun 中的 this->obj ,自动指向.前的对象

  2. new Fun() Fun 中的 this->正在创建的新对象,new 改变了函数内部的 this 指向,导致 this 指向实例化 new 的对象

  3. fun() 和匿名函数自调 this 默认->window,函数内部的 this,this 默认是指向 window

  4. 普通函数调用: 在严格模式下绑定到 undefined,否则绑定到全局对象。

说明:js 中没有特殊处理绑定 this 或者使用箭头函数,那么callback 函数默认是指向全局 window对象。

在掘金上文章的总结:

注意: 在判断js代码执行时判断的一个依据,切记

  1. 当在函数作用域中访问一个变量时,它的查找位置是从声明函数的位置开始然后向上层作用域中查找的,而不是在调用位置开始查找

2.this指向是在执行时确定的,不是定义时确定的, 箭头函数中的this指向 ===> 最靠近它的 绑定this指向的函数中的 this 的值

3.非箭头函数中this指向的判断标准: this总是指向当前函数所在的执行上下文

手写实现 call 方法

Function.prototype.call2 = function(context) {
    var context = context || window;
    // 首先要获取调用call的函数,用this可以获取
    context.fn = this;
    var args = [];
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    }

    // eval 把内部的字符串当做js代码来执行
    var result = eval('context.fn(' + args +')');

    delete context.fn
    return result;
}
复制代码

apply 的模拟实现

Function.prototype.apply = function (context, arr) {
    var context = Object(context) || window;
    context.fn = this;

    var result;
    if (!arr) {
        result = context.fn();
    }
    else {
        var args = [];
        for (var i = 0, len = arr.length; i < len; i++) {
            args.push('arr[' + i + ']');
        }
        result = eval('context.fn(' + args + ')')
    }

    delete context.fn
    return result;
}
复制代码

bind 的构造函数模拟效果

Function.prototype.bind2 = function (context) {

    if (typeof this !== "function") {
      throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
    }

    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);
    var fNOP = function () {};

    var fbound = function () {
        self.apply(this instanceof self ? this : context,
        args.concat(Array.prototype.slice.call(arguments)));
    }

    fNOP.prototype = this.prototype;
    fbound.prototype = new fNOP();

    return fbound;

}
复制代码

使用 apply 的方式实现 bind

Function.prototype.bind2 = function (context) {

    var self = this;
    // 获取bind2函数从第二个参数到最后一个参数
    var args = Array.prototype.slice.call(arguments, 1);

    return function () {
        // 这个时候的arguments是指bind返回的函数传入的参数
        var bindArgs = Array.prototype.slice.call(arguments);
        self.apply(context, args.concat(bindArgs));
    }

}
复制代码

关于this的指向问题

  • 如果一个函数中有this,但是它没有被上一级的对象所调用,那么this指向的就是window,这里需要说明的是在js的严格版中this指向的不是window,但是我们这里不探讨严格版的问题,你想了解可以自行上网查找
  • 如果一个函数中有this,这个函数有被上一级的对象所调用,那么this指向的就是上一级的对象.
  • 如果一个函数中有this,这个函数中包含多个对象,尽管这个函数是被最外层的对象所调用,this指向的也只是它上一级的对象
  • 特殊情况:this永远指向的是最后调用它的对象,也就是看它执行的时候是谁调用的
var o = {
    a:10,
    b:{
        a:12,
        fn:function(){
            console.log(this.a); //undefined
            console.log(this); //window
        }
    }
}
var j = o.b.fn;
j();
复制代码
  • 构造函数中的this
function Fn(){
    this.user = "追梦子";
}
var a = new Fn();
console.log(a.user); //追梦子
复制代码

这种情况很正常,因为new操作符做了三件事

var obj  = {};

obj.__proto__ = Fn.prototype;

Fn.call(obj);
复制代码

如果有返回值的话:

function fn()  
{  
    this.user = '追梦子';  
    return {};  
}
var a = new fn;  
console.log(a.user); //undefined
复制代码
function fn()  
{  
    this.user = '追梦子';  
    return function(){};
}
var a = new fn;  
console.log(a.user); //undefined
复制代码
function fn()  
{  
    this.user = '追梦子';  
    return 1;
}
var a = new fn;  
console.log(a.user); //追梦子
复制代码
function fn()  
{  
    this.user = '追梦子';  
    return undefined;
}
var a = new fn;  
console.log(a.user); //追梦子
复制代码

结论:如果返回值是一个对象,那么this指向的就是那个返回的对象,如果返回值不是一个对象那么this还是指向函数的实例,虽然null也是对象,但是在这里this还是指向那个函数的实例,因为null比较特。

1.3 原型/继承

我们得到一个定律

每个对象都有 __proto__ 属性,但只有函数对象才有 prototype 属性
复制代码

原型对象定义:

顾名思义,它就是一个普通对象。从现在开始要牢牢记住原型对象就是  Person.prototype,
复制代码
function Person() {}
Person.prototype.name = 'Zaxlct';
Person.prototype.age  = 28;
Person.prototype.job  = 'Software Engineer';
Person.prototype.sayName = function() {
  alert(this.name);
}

变形为:
Person.prototype = {
	name: 'zs',
	age: 28,
	job: '程序猿',
	sayName: function(){
		console.log(this.name);
	}
}
复制代码

在默认情况下,所有的原型对象都会自动获得一个 constructor(构造函数)属性,这个属性(是一个指针)指向 prototype 属性所在的函数(Person)

上面这句话有点拗口,我们「翻译」一下:A 有一个默认的 constructor 属性,这个属性是一个指针,指向 Person(也就是构造函数)。即:

Person.prototype.constructor == Person
复制代码

结论:原型对象(Person.prototype)是构造函数(Person)的一个实例。

实例的 proto 指向的是创建实例的构造函数的原型对象,举例说明:

Person.prototype.constructor == Person;
person1._proto_  == Person.prototype;
person1.constructor == Person;
复制代码

总结:

  • 原型和原型链是 JS 实现继承的一种模式
  • 原型链的形成真正靠 proto 而非 prototype

1.3.1 六种手写继承(待补充)

1.4 Promise

手动写法:

class Promise{
	constructor(executor){
		this.state = 'pending';
		this.value = undefined;
		this.reason = undefined;
		this.onResolvedCallbacks = [];
		this.onRejectedCallbacks = [];
		let resolve = value => {
			if (this.state === 'pending') {
				this.state = 'fulfilled';
				this.value = value;
				this.onResolvedCallbacks.forEach(fn => fn());
			}
		};
		let reject = reason => {
			if (this.state  === 'pending') {
				this.state = 'rejected';
				this.reason = reason;
				this.onRejectedCallbacks.forEach(fn => fn());
			}
		};
		try{
			executor(resolve, reject);
		} catch (err) {
			reject(err);
		}
	}
	then(onFulfilled, onRejected) {
		// onFulfilled 如果是不是函数,就忽略 onFulfilled,直接返回 value
		onFulfilled = typeof  onFulfilled === 'function' ? onFulfilled : value => value;
		// onRejected如果不是函数,就忽略onRejected,直接扔出错误
               onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err };
               
		 let promise2 =  new Promise((resolve, reject) => {
		 	if (this.state === 'fulfilled') {
		 	    setTimeout(() => {
		 		    try {
		 			    let x = onFulfilled(this.value);
		 			    resolvePromise(promise2, x, resove, reject);
		 		    } catch  (e) {
		 			    reject(e);
		 		    }
		 	    }, 0);
		 	};
		 	if (this.state === 'rejected') {
		 		try {
		 			ley x = onRejected(this.reason);
		 			resolvePromise(promise2, x, resolve, reject);
		 		} catch (e) {
		 			reject(e)
		 		}
		 	};
		 	if (this.state === 'pending') {
		 		this.onResolvedCallbacks.push(() => {
		 		    // 异步
                                setTimeout(() => {
                                    try {
                                    let x = onFulfilled(this.value);
                                    resolvePromise(promise2, x, resolve, reject);
                                    } catch (e) {
                                    reject(e);
                                    }
                                }, 0);
		 		});
		 		this.onRejectedCallbacks.push(() => {
                                    // 异步
                                    setTimeout(() => {
                                        try {
                                        let x = onRejected(this.reason);
                                        resolvePromise(promise2, x, resolve, reject);
                                        } catch (e) {
                                        reject(e);
                                        }
                                    }, 0)
                             });
		 	};
		 });
		 // 返回promise,  完成链式
		 return promise2
	}
	
	catch(fn)	{
		return this.then(null, fn);
	}
}

function resolvePromise(promise2, x, resolve, reject){
  if(x === promise2){
    return reject(new TypeError('Chaining cycle detected for promise'));
  }
  let called;
  if (x != null && (typeof x === 'object' || typeof x === 'function')) {
    try {
      let then = x.then;
      if (typeof then === 'function') { 
        then.call(x, y => {
          if(called)return;
          called = true;
          resolvePromise(promise2, y, resolve, reject);
        }, err => {
          if(called)return;
          called = true;
          reject(err);
        })
      } else {
        resolve(x);
      }
    } catch (e) {
      if(called)return;
      called = true;
      reject(e); 
    }
  } else {
    resolve(x);
  }
}
复制代码

理解(调用顺序)

let myPromise = new Promise1((resolve, reject) => {
    console.log('000');
    setTimeout(() => {
    resolve('111');
    }, 1000);
});
myPromise.then((res) => {
    console.log(res + '--' + new Date().valueOf());
});
复制代码

上面代码在执行的时候的调用顺序是这样:

  1. 首先会调用 Promise 的 constructor 然后会执行它中的代码
  2. 会执行 console.log('000')...这个函数中的代码
  3. 当调用 myPromise.then(...), 然后会执行 源码中的 then 中的代码,然后会进行回调函数的搜集,异步请求情况下,肯定会走 this.state === 'pending' 然后分别加入回调函数到对应的集合中
  4. 此时异步回调的结果返回了,然后调用 resolve() 或者 reject() 然后就会调用 constructor 中的对应的方法
  5. 然后调用到then中之前调价到集合中的方法,然后执行 let x = onFulfilled(this.value); resolvePromise(promise2, x, resolve, reject);
  6. 其实 resolvePromise 方法主要是为了链式调用,或者说在then中返回了一个新的 Promise 对象。而且此函数是一个递归调用。

Promise中的其他函数,all 和 race

//resolve方法
Promise.resolve = function(val){
  return new Promise((resolve,reject)=>{
    resolve(val)
  });
}
//reject方法
Promise.reject = function(val){
  return new Promise((resolve,reject)=>{
    reject(val)
  });
}
//race方法 
Promise.race = function(promises){
  return new Promise((resolve,reject)=>{
    for(let i=0;i<promises.length;i++){
      promises[i].then(resolve,reject)
    };
  })
}
//all方法(获取所有的promise,都执行then,把结果放到数组,一起返回)
Promise.all = function(promises){
  let arr = [];
  let i = 0;
  function processData(index,data){
    arr[index] = data;
    i++;
    if(i == promises.length){
      resolve(arr);
    };
  };
  return new Promise((resolve,reject)=>{
    for(let i=0;i<promises.length;i++){
      promises[i].then(data=>{
        processData(i,data);
      },reject);
    };
  });
}
复制代码

1.5 深浅拷贝

概念:

浅拷贝:只是拷贝的一个引用,和被拷贝对象指向的是同一个对象,如果修改了其中一个对象的数据,那么对应的另外一个也会被修改。

深拷贝:直接拷贝了一个和原来对象一样的数据,然后这两个对象之间没有任何关系,修改其中一个对象中的数据,也不会影响到另一个数据。

前提条件:对于基本数据类型的拷贝,并没有深浅拷贝的区别,我们所说的深浅拷贝都是对于引用数据类型而言的

实现深拷贝的方法和优缺点:

  1. let cloneobj = JSON.parse(JSON.stringify(originobbj)); 此种方式确实实现了深拷贝,但是它有 一个缺点:在转换过程中会忽略 undefined、function、symbol,所以对象中如果存在此种数据结构,会被直接忽略掉。
  2. Object.assign()、concat()、slice()、(...展开运算符)这些都是一层的深拷贝,如果属性是引用类型,那么它拷贝的也只是属性值,也就是引用指针,所以有不能完全实现深拷贝。准确的说如果只有一层,同时数据都是基本数据类型,那么可以实现深拷贝,反之则是浅拷贝。
  3. 要想真正意思上的深拷贝,请递归操作

手写深拷贝

function cloneDeep3(source, uniqueList) {
    if (!isObject(source)) return source; 
    if (!uniqueList) uniqueList = []; // 新增代码,初始化数组
        
    var target = Array.isArray(source) ? [] : {};
    // 数据已经存在,返回保存的数据
    var uniqueData = find(uniqueList, source);
    if (uniqueData) {
        return uniqueData.target;
    };
        
    // 数据不存在,保存源数据,以及对应的引用
    uniqueList.push({
        source: source,
        target: target
    });

    for(var key in source) {
        if (Object.prototype.hasOwnProperty.call(source, key)) {
            if (isObject(source[key])) {
                target[key] = cloneDeep3(source[key], uniqueList);  // 新增代码,传入数组
            } else {
                target[key] = source[key];
            }
        }
    }
    return target;
}

// 新增方法,用于查找
function find(arr, item) {
    for(var i = 0; i < arr.length; i++) {
        if (arr[i].source === item) {
            return arr[i];
        }
    }
    return null;
}
复制代码

最简单的实现

function cloneDeep2(source) {

    if (!isObject(source)) return source; // 非对象返回自身
      
    var target = Array.isArray(source) ? [] : {};
    for(var key in source) {
        if (Object.prototype.hasOwnProperty.call(source, key)) {
            if (isObject(source[key])) {
                target[key] = cloneDeep2(source[key]); // 注意这里
            } else {
                target[key] = source[key];
            }
        }
    }
    return target;
}
复制代码

使用链表的方式解决循环引用的写法

function deepCopy3(obj) {
    // hash表,记录所有的对象的引用关系
    let map = new WeakMap();
    function dp(obj) {
        let result = null;
        let keys = Object.keys(obj);
        let key = null,
            temp = null,
            existobj = null;

        existobj = map.get(obj);
        //如果这个对象已经被记录则直接返回
        if(existobj) {
            return existobj;
        }

        result = {}
        map.set(obj, result);

        for(let i =0,len=keys.length;i<len;i++) {
            key = keys[i];
            temp = obj[key];
            if(temp && typeof temp === 'object') {
                result[key] = dp(temp);
            }else {
                result[key] = temp;
            }
        }
        return result;
    }
    return dp(obj);
}
复制代码

1.6 事件机制/Event Loop (参考网址:zhuanlan.zhihu.com/p/34229323)

介绍一些基础的概念:

JavaScript Engine 和 JavaScript Runtime

简单来说,为了让 JavaScript 运行起来,要完成两部分工作:

  • 编译并执行 JavaScript 代码,完成内存分配、垃圾回收等;
  • 为了 JavaScript 提供一些对象或机制,使它能够与外界交互。

在 JavaScript 运行的时候,JavaScript Engine 会创建和维护相应的堆(Heap) 和栈(Stack),同时通过 JavaScript Runtime 提供的一系列 API (例如 setTimeout、XMLHTTPRequest等)来完成各种各样的任务。

JavaScript 是一种单线程的编程语言,只有一个调用栈,决定了它同一时间只能做一件事。

在 JavaScript 的运行的过程中,真正负责执行 JavaScript 代码的只有一个线程,通常称为主线程,各种任务都会用排队的方式来同步执行。

然而 JavaScript 却又是一个非阻塞(Non-blocking)、异步(Asynchronous)、并发式(Concurrent)的编程语言,这就得说说 JavaScript 的事件循环(Event Loop)机制了。

Event Loop

时间循环(Event Loop) 是让 JavaScript 做到既是单线程,又绝对不会阻塞的核心机制,也是 JavaScript 并发模型(Cocurrency Model) 的基础,是用来协调各种事件、用户交互、脚本执行、UI渲染、网络请求等的一种机制。

说的简单一点:Event Loop 只不过是实现异步的一种机制而已

注意点:在 JavaScript Engine 中(以 V8 为例),只是实现了 ECMAScript 标准,而并不关心什么 Event Loop。也就是说 Event Loop 是属于 JavaScript Runtime 的,是由宿主环境提供的(比如浏览器)。所以千万不要搞错了,这也是前面介绍 JavaScript Engine 和 Runtime 的原因

Event Loop 中的任务队列

在执行和协调各种任务时,Event Loop 会维护自己的一个任务队列,任务队列分为 Task Queue 和 Microtask Queue 两种。

  1. Task Queue

一个 Event Loop 会有一个或多个 Task Queue,这是一个先进先出(FIFO)的有序列表,存放着来自不同 Task Source(任务源)的 Task。

在 HTML 标准中,定义了几种常见的 Task Source:

  • DOM manipulation(DOM 操作);
  • User interaction(用户交互);
  • Networking(网络请求);
  • History traversal(History API 操作)。

Task Source 的定义非常的宽泛,常见的鼠标、键盘事件,AJAX,数据库操作(例如 IndexedDB),以及定时器相关的 setTimeout、setInterval 等等都属于 Task Source,所有来自这些 Task Source 的 Task 都会被放到对应的 Task Queue 中等待处理。

对于 Task、Task Queue 和 Task Source,有如下规定:

  • 来自相同 Task Source 的 Task,必须放在同一个 Task Queue 中;
  • 来自不同 Task Source 的 Task,可以放在不同的 Task Queue 中;
  • 同一个 Task Queue 内的 Task 是按顺序执行的;
  • 但对于不同的 Task Queue(Task Source),浏览器会进行调度,允许优先执行来自特定 Task Source 的 Task。

2.Microtask Queue

Microtask Queue 与 Task Queue 类似,也是一个有序列表。不同之处在于,一个 Event Loop 只有一个 Microtask Queue。

在 HTML 标准中,并没有明确规定 Microtask Source,通常认为有以下几种:

  • Promise
  • MutationObserver
JavaScript Runtime 的运行机制

了解了 Event Loop 和队列的基本概念后,就可以从相对宏观的角度先了解一下 JavaScript Runtime 的运行机制了,简化后的步骤如下:

  1. 主线程不断循环;
  2. 对于同步任务,创建执行上下文,按顺序进入执行栈;
  3. 对于异步任务:
  • 与步骤2相同,同步执行这段代码;
  • 与相应的 Task (或 Microtask) 添加到 Event Loop 的任务队列;
  • 由其他线程来执行具体的异步操作。

其他线程是指:尽管 JavaScript 是单线程的,但浏览器内核是多线程的,它会将 GUI 渲染、定时器触发、HTTP 请求等工作交给专门的线程来处理。

另外,在 Node.js 中,异步操作会优先由 OS 或第三方系统提供的异步接口来执行,然后才由线程池处理。

4.当主线程执行完当前执行栈中的所有任务,就会去读取 Event Loop 的任务队列,取出并执行任务;

5.重复以上步骤。

还是拿 setTimeout 举个栗子:

  1. 主线程同步执行这个 setTimeout 函数本身。
  2. 将负责执行这个 setTimeout 的回调函数的 Task 添加到 Task Queue。
  3. 定时器开始工作(实际上是靠 Event Loop 不断循环检查系统时间来判断是否已经到达指定的时间点)。
  4. 主线程继续执行其他任务。
  5. 当执行栈为空,且定时器触发时,主线程取出 Task 并执行相应的回调函数。很明显,执行 setTimeout 不会导致阻塞。当然,如果主线程很忙的话(执行栈一直非空),就会出现明明时间已经到了,却也不执行回调的现象,所以类似 setTimeout 这样的回调函数都是没法保证执行时机的。

很明显,执行 setTimeout 不会导致阻塞。当然,如果主线程很忙的话(执行栈一直非空),就会出现明明时间已经到了,却也不执行回调的现象,所以类似 setTimeout 这样的回调函数都是没法保证执行时机的。

Event Loop 处理模型

前面简单介绍了 JavaScript Runtime 的整个运行流程,而 Event Loop 作为其中的重要一环,它的每一次循环过程也相当复杂,因此将它单独拿出来介绍。下面我会尽量保持 HTML 标准中对处理模型(Processing Model)的定义,并尽量简化,步骤如下(3 步):

  1. 执行 Task:从 Task Queue 中取出最老的一个 Task 并执行;如果没有 Task,直接跳过。
  2. 执行 Microtasks:遍历 Microtask Queue 并执行所有 Microtask(参考 Perform a microtask checkpoint)。
  3. 进入 Update the rendering(更新渲染)阶段:
  • 设置 Performance API 中 now() 的返回值。Performance API 属于 W3C High Resolution Time API 的一部分,用于前端性能测量,能够细粒度的测量首次渲染、首次渲染内容等的各项绘制指标,是前端性能追踪的重要技术手段,感兴趣的同学可关注。

  • 遍历本次 Event Loop 相关的 Documents,执行更新渲染。在迭代执行过程中,浏览器会根据各种因素判断是否要跳过本次更新。

  • 当浏览器确认继续本次更新后,处理更新渲染相关工作:

1.触发各种事件:Resize、Scroll、Media Queries、CSS Animations、Fullscreen API。

2.执行 animation frame callbacks,window.requestAnimationFrame 就在这里。

3.更新 intersection observations,也就是 Intersection Observer API(可用于图片懒加载)。更新渲染和 UI,将最终结果提交到界面上。

至此,Event Loop 的一次循环结束。。。还是用一张简图来描述吧(process.nextTick 被特意标注出来以示区别)。

Microtask Queue 执行时机

在上面介绍的 Event Loop 处理模型中,Microtask Queue 会在第 2 步时被执行。实际上按照 HTML 标准,在以下几种情况中 Microtask Queue 都会被执行:

  1. 某个 Task 执行完毕时(即上述情况)。
  2. 进入脚本执行(Calling scripts)的清理阶段(Clean up after running script)时。
  3. 创建和插入节点时。
  4. 解析 XML 文档时。

同时,在当前 Event Loop 轮次中动态添加进来的 Microtasks,也会在本次 Event Loop 循环中全部执行完(上图其实已经画出来了)。

最后一定要注意的是,执行 Microtasks 是有前提的:当前执行栈必须为空,且没有正在运行的执行上下文。否则,就必须等到执行栈中的任务全部执行完毕,才能开始执行 Microtasks。

也就是说:JavaScript 会确保当前执行的同步代码不会被 Microtasks 打断。

下面是一个经典的例子:

<div id="outer">
    <div id="inner"></div>
</div>

const inner = document.getElementById("inner");
const outer = document.getElementById("outer");

// 监听 outer 的属性变化。
new MutationObserver(() => console.log("mutate outer"))
    .observe(outer, { attributes: true });

// 处理 click 事件。
function onClick(){
    console.log("click");
    setTimeout(() => console.log("timeout"), 0);
    Promise.resolve().then(() => console.log("promise"));
    outer.setAttribute("data-mutation", Math.random());
}

// 监听 click 事件。
inner.addEventListener("click", onClick);
outer.addEventListener("click", onClick);
inner.click();

/*
  点击按钮调用
  click
  promise
  mutate outer
  click
  promise
  mutate outer
  timeout
  timeout

  主动调用 inner.click();
  click
  click
  end
  promise
  mutate outer
  promise
  timeout
  timeout
*/

复制代码

结论:为什么主动调用和用户点击调用,两者的调用顺利是不一样的呢?

根据多方查证,只有当用户真正在游览器上进行交互操作触发的事件才会引发游览器的事件线程,进而遵循js事件循环的原理——异步处理;这种就是正常情况下的处理顺序。

js代码里面的主动调用只被视为发布/订阅(事件本质)的代码执行而已——同步处理。所以触发中的onClick函数都是要同步处理的(主线程),所以优先级是比微任务要高。

函数式编程

请参考网址:juejin.cn/post/684490…

Web Worker

可参考网址:github.com/linxiangjun…

ES6大版本的详细细节

参考网址:juejin.cn/post/684490…

Vue中8种组件通信方式

一、props / $emit

这个很简单,我们这里面就不再做阐述总结

二、children/children / parent

// 父组件中
<template>
  <div class="hello_world">
    <div>{{msg}}</div>
    <com-a></com-a>
    <button @click="changeA">点击改变子组件值</button>
  </div>
</template>

<script>
import ComA from './test/comA.vue'
export default {
  name: 'HelloWorld',
  components: { ComA },
  data() {
    return {
      msg: 'Welcome'
    }
  },

  methods: {
    changeA() {
      // 获取到子组件A
      this.$children[0].messageA = 'this is new value'
    }
  }
}
</script>
复制代码
// 子组件中
<template>
  <div class="com_a">
    <span>{{messageA}}</span>
    <p>获取父组件的值为:  {{parentVal}}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      messageA: 'this is old'
    }
  },
  computed:{
    parentVal(){
      return this.$parent.msg;
    }
  }
}
</script>
复制代码

注意事项:要注意边界情况,如在#app上拿parent得到的是newVue()的实例,在这实例上再拿parent得到的是new Vue()的实例,在这实例上再拿parent得到的是undefined,而在最底层的子组件拿children是个空数组。也要注意得到children是个空数组。也要注意得到parent和children的值不一样,children的值不一样,children 的值是数组,而$parent是个对象。

三、provide/ inject

provide/ inject 是vue2.2.0新增的api, 简单来说就是父组件中通过provide来提供变量, 然后再子组件中通过inject来注入变量。

这个是父组件

<template>
  <div>
    <div class="hello_world">
      <span>I am father:</span>
      <span style="margin-right: 20px;">{{msg}}</span>
      <button @click="changeM">改变user</button>
      <button @click="changeA">点击改变子组件值</button><br/><br/>
      <div>=======我是孩子=======</div><br/>
      <transformChild></transformChild>
    </div>
  </div>
</template>

<script>
  import transformChild from './transformChild';
  export default {
    data() {
      return {
        msg: 'Welcome',
        user: { name: 'zs' },
        grade: 2
      }
    },
    provide() {
      return {
        /*
          原因:是由于provide/inject机制不支持响应式编程的,后续对provide返回的对象直接修改不会重新刷新provide/inject机制, 也就是provide返回的对象的最顶层的响应机制会失效,且无法对对象顶层属性进行操作
          如果引用的属性是基本数据类型的话,也不会响应的
          this.user.name = 'ls'; 这种方式是可以做响应的
          this.user = {age: 222}; 此种方式是不行的,因为要想响应,就需要引用的原始对象不变,才能响应
        */ 
        // for: this.user
        getContext: () => ({
          user: this.user,
          grade: this.grade,
      })
      };
    },
    methods: {
      changeA(){
        this.$children[0].messageA = 'this is new value'
      },
      changeM(){
        // this.user.name = 'ls';
        this.user = {age: 222};
        this.grade += 1;
      }
    },
    components: {
      transformChild
    }
  }
</script>

<style lang="scss" scoped>

</style>
复制代码
此组件为子组件

<template>
  <div class="com_a">
    <span>{{messageA}}</span>
    <p>获取父组件的值为:  {{parentVal}}</p>
    <button @click="changeSuper">改变父组件的值</button>
    <p>{{ context.user }}</p>
    <p>{{ context.grade }}</p>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        messageA: 'this is old'
      }
    },
      // inject: ['for'],
    inject: ['getContext'],
    computed: {
      parentVal(){
        return this.$parent.msg;
      },
      context(){
        return this.getContext();
      }
    },
    methods: {
      changeSuper(){
        this.$parent.msg = 'Thanks'
      }
    }
  }
</script>

<style lang="scss" scoped>

</style>
复制代码

由于provide/inject机制不支持响应式编程的,后续对provide返回的对象直接修改不会重新刷新provide/inject机制, 也就是provide返回的对象的最顶层的响应机制会失效,且无法对对象顶层属性进行操作。这个机制会导致以下三种方式不能实现响应式传递:

  • 上文中的context不能在computed中声明。因为每次computed都会返回一个新的值(引用),而provide只会记录一开始的context的那个引用, 后续数据发生变更, 新的context不会被刷新到provide中去。
  • 上文中的context就算data中声明的, 但如果在某个地方执行了this.context = {...}, 新的context也不会被更新在provide, provide中的context永远是在初始化时复制给他的那个引用。这会导致在父组件中context可以动态刷新, 但是子组件中的context不会动态刷新
  • 直接在provide函数中返回上文中的context,那么user, grade就会成为顶层属性,在created中进行的重新赋值操作和后续的重新赋值操作都不会响应到provide中, 将会失去响应式。

最好的使用方式:

provide() {
    return {
      getContext: () => ({
          user: this.user,
          grade: this.grade,
      })
    };
}
复制代码

在子组件中

inject: ['getContext'],
computed: {
    context() {
        return this.getContext();
    }
}
复制代码

四、ref / refs

ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例,可以通过实例直接调用组件的方法或访问数据, 我们看一个ref 来访问组件的例子:

// 子组件 A.vue

export default {
  data () {
    return {
      name: 'Vue.js'
    }
  },
  methods: {
    sayHello () {
      console.log('hello')
    }
  }
}
复制代码
// 父组件 app.vue

<template>
  <component-a ref="comA"></component-a>
</template>
<script>
  export default {
    mounted () {
      const comA = this.$refs.comA;
      console.log(comA.name);  // Vue.js
      comA.sayHello();  // hello
    }
  }
</script>
复制代码

五、eventBus

eventBus 又称为事件总线,在vue中可以使用它来作为沟通桥梁的概念, 就像是所有组件共用相同的事件中心,可以向该中心注册发送事件或接收事件, 所以组件都可以通知其他组件。

  • eventBus也有不方便之处, 当项目较大,就容易造成难以维护的灾难

在Vue的项目中怎么使用eventBus来实现组件之间的数据通信呢?具体通过下面几个步骤

1.初始化

首先需要创建一个事件总线并将其导出, 以便其他模块可以使用或者监听它.

// event-bus.js

import Vue from 'vue'
export const EventBus = new Vue()
复制代码

2.发送事件

假设你有两个组件: additionNum 和 showNum, 这两个组件可以是兄弟组件也可以是父子组件;这里我们以兄弟组件为例:

<template>
  <div>
    <show-num-com></show-num-com>
    <addition-num-com></addition-num-com>
  </div>
</template>

<script>
import showNumCom from './showNum.vue'
import additionNumCom from './additionNum.vue'
export default {
  components: { showNumCom, additionNumCom }
}
</script>

  <div>
    <show-num-com></show-num-com>
    <addition-num-com></addition-num-com>
  </div>
</template>

<script>
import showNumCom from './showNum.vue'
import additionNumCom from './additionNum.vue'
export default {
  components: { showNumCom, additionNumCom }
}
</script>
复制代码
// addtionNum.vue 中发送事件

<template>
  <div>
    <button @click="additionHandle">+加法器</button>    
  </div>
</template>

<script>
import {EventBus} from './event-bus.js'
console.log(EventBus)
export default {
  data(){
    return{
      num:1
    }
  },

  methods:{
    additionHandle(){
      EventBus.$emit('addition', {
        num:this.num++
      })
    }
  }
}
</script>
复制代码

3.接收事件

// showNum.vue 中接收事件

<template>
  <div>计算和: {{count}}</div>
</template>

<script>
import { EventBus } from './event-bus.js'
export default {
  data() {
    return {
      count: 0
    }
  },

  mounted() {
    EventBus.$on('addition', param => {
      this.count = this.count + param.num;
    })
  }
}
</script>
复制代码

这样就实现了在组件addtionNum.vue中点击相加按钮, 在showNum.vue中利用传递来的 num 展示求和的结果.

4.移除事件监听者

如果想移除事件的监听, 可以像下面这样操作:

import { eventBus } from 'event-bus.js'
EventBus.$off('addition', {})
复制代码

六、Vuex

具体的实现到处都是文章,请自行查阅。

七、localStorage / sessionStorage

这种通信比较简单,缺点是数据和状态比较混乱,不太容易维护。 通过window.localStorage.getItem(key)获取数据 通过window.localStorage.setItem(key,value)存储数据

  • 注意用JSON.parse() / JSON.stringify() 做数据格式转换 localStorage / sessionStorage可以结合vuex, 实现数据的持久保存,同时使用vuex解决数据和状态混乱问题.

attrsattrs与 listeners

现在我们来讨论一种情况, 我们一开始给出的组件关系图中A组件与D组件是隔代关系, 那它们之前进行通信有哪些方式呢?

  1. 使用props绑定来进行一级一级的信息传递, 如果D组件中状态改变需要传递数据给A, 使用事件系统一级级往上传递
  2. 使用eventBus,这种情况下还是比较适合使用, 但是碰到多人合作开发时, 代码维护性较低, 可读性也低
  3. 使用Vuex来进行数据管理, 但是如果仅仅是传递数据, 而不做中间处理,使用Vuex处理感觉有点大材小用了.

在vue2.4中,为了解决该需求,引入了attrsattrs 和listeners , 新增了inheritAttrs 选项。 在版本2.4以前,默认情况下,父作用域中不作为 prop 被识别 (且获取) 的特性绑定 (class 和 style 除外),将会“回退”且作为普通的HTML特性应用在子组件的根元素上。接下来看一个跨级通信的例子:

// app.vue
// index.vue

<template>
  <div>
    <child-com1
      :name="name"
      :age="age"
      :gender="gender"
      :height="height"
      title="程序员成长指北"
    ></child-com1>
  </div>
</template>
<script>
const childCom1 = () => import("./childCom1.vue");
export default {
  components: { childCom1 },
  data() {
    return {
      name: "zhang",
      age: "18",
      gender: "女",
      height: "158"
    };
  }
};
</script>
复制代码
// childCom1.vue

<template class="border">
  <div>
    <p>name: {{ name}}</p>
    <p>childCom1的$attrs: {{ $attrs }}</p>
    <child-com2 v-bind="$attrs"></child-com2>
  </div>
</template>
<script>
const childCom2 = () => import("./childCom2.vue");
export default {
  components: {
    childCom2
  },
  inheritAttrs: false, // 可以关闭自动挂载到组件根元素上的没有在props声明的属性
  props: {
    name: String // name作为props属性绑定
  },
  created() {
    console.log(this.$attrs);
     // { "age": "18", "gender": "女", "height": "158", "title": "程序员成长指北" }
  }
};
</script>
复制代码
// childCom2.vue

<template>
  <div class="border">
    <p>age: {{ age}}</p>
    <p>childCom2: {{ $attrs }}</p>
  </div>
</template>
<script>

export default {
  inheritAttrs: false,
  props: {
    age: String
  },
  created() {
    console.log(this.$attrs); 
    // { "gender": "女", "height": "158", "title": "程序员成长指北" }
  }
};
</script>
复制代码

总结

常见使用场景可以分为三类:

  • 父子组件通信: props; parent/parent / children; provide / inject ; ref ; attrs/attrs / listeners
  • 兄弟组件通信: eventBus ; vuex
  • 跨级通信: eventBus;Vuex;provide / inject 、attrs/attrs / listeners

数组的全排列

第一种纯递归

var res = [];
function permutation(arr,len,index){
    if(len == index){
        res.push(arr.slice());
    }
    for(var i = index;i<len;i++){
        [arr[i],arr[index]] = [arr[index],arr[i]];
        console.log(arr);
        permutation(arr,len,index+1);
        console.log(arr);
        [arr[i],arr[index]] = [arr[index],arr[i]];
    }
}
permutation([1,2,3],3,0);
console.log(res);
复制代码

第二种做了一些临界的处理,效率可能更高(这种方法更好理解)

function permutation(arr){
	if(arr.length == 1)
		return arr;
	else if(arr.length == 2)
		return [[arr[0],arr[1]],[arr[1],arr[0]]];
	else{
		var temp = [];
		for(var i=0;i<arr.length;i++){
			var save = arr[i];
			arr.splice(i,1);
			var res = permutation(arr);
			arr.splice(i,0,save);
			for(var j=0;j<res.length;j++){
				res[j].unshift(save);
				temp.push(res[j]);
			}
		}
		return temp;
	}
}
var result = permutation([1,2,3,4]);
console.log(result);
复制代码

哈希表

概念解释:

  1. 哈希化:将大数字 转换成数组范围 内下标的过程,我们称之为哈希化

  2. 哈希函数:通常我们会将单词转成大数字,大数字再进行哈希化的代码实现放在一个函数中,这个函数我们称为哈希函数。

  3. 哈希表:最终 将数据插入到的这个数组,对整个结构的封装,我们 就称之为一个哈希表。

  4. 冲突:就像我们之前所提到的,在通常情况下,哈希函数输入的范围一定会远远大于输出的范围,所以在使用哈希表时一定会遇到冲突,哪怕我们使用了完美的哈希函数,当输入的键足够多也会产生冲突。然而多数的哈希函数都是不够完美的,所以仍然存在发生哈希碰撞的可能,这时就需要一些方法来解决哈希碰撞的问题,常见方法的就是 开放寻址法拉链法

  5. 链地址法(拉链法):

    • 从图片中我们可以看出,链地址法解决冲突的办法是每个 数组单元中存储的不再是单个数据,而是一个链条
    • 这个链条使用什么数据呢?常见的是数组或者链表。
    • 比如链表,也就是每个数组单元中存储着一个链表 ,一旦发现重复,将重复的元素插入到链表的首端或末端即可。
    • 当查询时,先根据哈希化后的下标值找到对应的位置,再取出链表,依次查找需要查找的数据。
    • 数组还是链表?
    • 数组或者链表效率上差不多。当插入的数据放在 数组或者链表的最前面,因为觉得新插入数据用于取出的可能性更大,这种情况最好使用链表,因为数组在首位插入数据,所有的后面的数据项都要依次先后移动 一个位置。
    • 具体选择链表还是数组,看具体需求。
  6. 开放地址法:主要的工作方式是寻找空白的单元格来添加重复的数据。探索这个位置的方式不同,有三种方法:

    • 线性探测:线性探测就是从index的位置+1开始一点点的查找合适的位置来放置冲突元素,合适的位置就是空的位置。查找的时候规则,也是通过哈希函数得到的下标索引开始查找,直到遇到空位置结束,如果还是没有找到的话,就标志此数据在哈希表中没有存储。 **存在问题:**线性探测有一个很严重的问题就是聚集,(聚集:一连串的填充单元就叫做聚集),聚集会影响哈希表的性能,无论是插入、查询、删除都会影响;因为插入元素的时候,遇到聚集之后,就会连续探测,这个过程探测的次数多,随意影响性能。
    • 二次探测:二次探测只要是对探测步长做了优化,比如从下标值x开始,x+1²,x+2²,x+3²。**问题:**比如我们连续插入的是32-112-82-2-192,那么依次累加的时候补长是相同的。
    • 再哈希法:把关键字用另一个哈希函数再一次哈希化,用这次哈希化的结果作为步长,需要具备以下特点:和第一个哈希函数不同,不能输出为0,其实计算机专家已经设计出了工作很好的哈希函数:stepSize = constant - (key%constany) , 其中constant是质数,且小于数组的容量
    • 装填因子:装填因子表示当前哈希表中已经包含的数据项和整个哈希表长度的比例。装填因子=总数据项/哈希表长度。
    • 性能:链地址法会随着装填因子的增加,而呈现线性增加,而开放地址法会成指数性增加,所以在实际的开发中链地址法选择会多点。
  7. 如何衡量一个哈希函数的优劣?

    • 快速的计算:哈希表的优势在于效率,所以快速获取到对应的hashCode非常重要,我们需要快速获取元素的hashCode,这里面可能会用到霍纳算法进行优化。
    • 均匀的分布:哈希表中,无论是链地址法还是开放地址法,当多个元素映射到同一个位置时候,都会影响效率,所以,优秀的哈希函数应该尽可能将元素映射到不同的位置,让元素在哈希表中均匀的分布。哈希表中的常数最好使用质数。
  8. Java中的HashMap的实现:

    • HashMap中index的计算公式:index = HashCode(key) & (length - 1)
    • 比如计算book的hashcode,结果为十进制的3029737,二进制的101110001110101110 1001
    • 假定HashMap长度是默认的 16,计算Length-1的结果为十进制的15,二进制的1111
    • 把以上两个结果做与运算,1001,十进制是9,所以 index=9
    • 这样的方式相对于取模来说性能是高的,因为计算机运算接近二进制
  9. 如何实现准确的计时器(实现原理:当每一次定时器执行时后,都去获取系统的时间来进行修正,虽然每次运行可能会有误差,但是通过系统时间对每次运行的修复,能够让后面每一次时间都得到一个补偿)

参考网址:(https://mp.weixin.qq.com/s/-mje2nwKUpWoFdJsDcKaXg)
function timer() {
	   var speed = 500,
	   counter = 1, 
	   start = new Date().getTime();
	   
	   function instance(){
	    var ideal = (counter * speed),
	    real = (new Date().getTime() - start);
	    
	    counter++;
	    var diff = (real - ideal);
	    // 通过系统时间进行修复
	    window.setTimeout(function() { instance(); }, (speed - diff)); 
	   };
	   
	   window.setTimeout(function() { instance(); }, speed);
	}
复制代码
  1. 区分 splice、slice、split
String -> split('x') 按照某个标示分割字符串,拆分成集合,但是不改变原字符串
	  	   -> slice(start, end) 此处的 start、end都是索引值,end为非必填项,
	  	      从start开始到end的前一个位置结束[),意思是包含开始,不包括结束索引
Array  -> slice(start, end) 和顶上字符串的操作一样,如果end不写的话,就是从
			  start索引开始一直到数据的最后
		   -> splice(index,howmany,item1,item2,item....)) 参数最少是两个,
		   	  开始的位置索引,howmany表示从起始位置开始要替换或删除几个元素,
		   	  item为可选项目,表示要替换的新元素,有则替换没有则表示删除
复制代码
  1. 滑动窗口算法
var lengthOfLongestSubstring = function (s) {
    let map = new Map();
    let i = -1
    let res = 0
    let n = s.length
    for (let j = 0; j < n; j++) {
        if (map.has(s[j])) {
            i = Math.max(i, map.get(s[j]))
        }
        res = Math.max(res, j - i)
        map.set(s[j], j)
    }
    return res
};
// 测试案例
let str = 'abwcdefcrtyupc'
let maxLen = lengthOfLongestSubstring(str);
console.log(maxLen);  // 7
复制代码

核心思想: * 首先设置 i = -1 这样的好处就是在出现重复元素之前一直是累加的状态, 然后更新res * 把元素的值作为key, 索引作为value * 当查询该元素在map中出现过, 那么就更新 i 的值为上次出现时的下标索引 * 那么窗口的开始索引就不是 -1 而是当前出现重复的元素在map中保存的索引值,也就是视图窗口的起始位置从0移动到i的位置 * 举例: abwcdefc 当遍历到此字符串中的c时,更新 i 的值为 3,就是说滑动视图的开始位置是3,然后视图的宽度也就是 j - i 的值,然后和 当前res作比较, 取大值进行记录,最后返回.

Vue 源码的解析

function mergeData (to: Object, from: ?Object): Object {
  // 没有 from 直接返回 to
  if (!from) return to
  let key, toVal, fromVal
  const keys = Object.keys(from)
  // 遍历 from 的 key
  for (let i = 0; i < keys.length; i++) {
    key = keys[i]
    toVal = to[key]
    fromVal = from[key]
    // 如果 from 对象中的 key 不在 to 对象中,则使用 set 函数为 to 对象设置 key 及相应的值
    if (!hasOwn(to, key)) {
      set(to, key, fromVal)
    // 如果 from 对象中的 key 也在 to 对象中,且这两个属性的值都是纯对象则递归进行深度合并
    } else if (isPlainObject(toVal) && isPlainObject(fromVal)) {
      mergeData(toVal, fromVal)
    }
    // 其他情况什么都不做
  }
  return to
}
复制代码

说明:顶上就是vue源码中data的合并策略(这是最核心的代码,但不是全部),核心思想就是如果from不存在,就直接返回to,如果存在的话,就判断from中的key是否在to中,如果不存在,就通过Set函数,直接添加到to对象中,如果val是对象的话,就递归进行深层合并,其他的话不做任何处理;无论哪种情况,最后都会返回 mergedDataFn 或者mergedInstanceDataFn 函数,所以说 data 被 mergeOptions 处理后,返回的一定是一个函数,这样就可以通过函数返回数据对象,保证了每个组件实例都有一个唯一的数据副本,避免了组件间数据互相影响。后面讲到 Vue 的初始化的时候大家会看到,在初始化数据状态的时候,就是通过执行 strats.data 函数来获取数据并对其进行处理的.

. 为什么不在合并阶段就把数据合并好,而是要等到初始化的时候再合并数据?

这个问题是什么意思呢?我们知道在合并阶段 strats.data 将被处理成一个函数,但是这个函数并没有被执行,而是到了后面初始化的阶段才执行的,这个时候才会调用 mergeData 对数据进行合并处理,那这么做的目的是什么呢?

其实这么做是有原因的,后面讲到 Vue 的初始化的时候,大家就会发现 inject 和 props 这两个选项的初始化是先于 data 选项的,这就保证了我们能够使用 props 初始化 data 中的数据

生命周期的合并策略

function mergeHook (
  parentVal: ?Array<Function>,
  childVal: ?Function | ?Array<Function>
): ?Array<Function> {
  return childVal
    ? parentVal
      ? parentVal.concat(childVal)
      : Array.isArray(childVal)
        ? childVal
        : [childVal]
    : parentVal
}

LIFECYCLE_HOOKS.forEach(hook => {
  strats[hook] = mergeHook
})

export const LIFECYCLE_HOOKS = [
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeUpdate',
  'updated',
  'beforeDestroy',
  'destroyed',
  'activated',
  'deactivated',
  'errorCaptured'
]
复制代码

三目运算符代码解析后的状态

return (是否有 childVal,即判断组件的选项中是否有对应名字的生命周期钩子函数)
  ? 如果有 childVal 则判断是否有 parentVal
    ? 如果有 parentVal 则使用 concat 方法将二者合并为一个数组
    : 如果没有 parentVal 则判断 childVal 是不是一个数组
      ? 如果 childVal 是一个数组则直接返回
      : 否则将其作为数组的元素,然后返回数组
  : 如果没有 childVal 则直接返回 parentVal
复制代码

如上就是对 mergeHook 函数的解读,我们可以发现,在经过 mergeHook 函数处理之后,组件选项的生命周期钩子函数被合并成一个数组。第一个三目运算符需要注意,它判断是否有 childVal,即组件的选项是否写了生命周期钩子函数,如果没有则直接返回了 parentVal,这里有个问题:parentVal 一定是数组吗?答案是:如果有 parentVal 那么其一定是数组,如果没有 parentVal 那么 strats[hooks] 函数根本不会执行:

new Vue({
  created: function () {
    console.log('created')
  }
})
复制代码

如果以这段代码为例,那么对于 strats.created 策略函数来讲(注意这里的 strats.created 就是 mergeHooks),childVal 就是我们例子中的 created 选项,它是一个函数。parentVal 应该是 Vue.options.created,但 Vue.options.created 是不存在的,所以最终经过 strats.created 函数的处理将返回一个数组:

options.created = [
  function () {
    console.log('created')
  }  
]
复制代码

比如:

const Parent = Vue.extend({
  created: function () {
    console.log('parentVal')
  }
})

const Child = new Parent({
  created: function () {
    console.log('childVal')
  }
})
复制代码

其中 Child 是使用 new Parent 生成的,所以对于 Child 来讲,childVal 是:

created: function () {
  console.log('childVal')
}
复制代码

而 parentVal 已经不是 Vue.options.created 了,而是 Parent.options.created,那么 Parent.options.created 是什么呢?它其实是通过 Vue.extend 函数内部的 mergeOptions 处理过的,所以它应该是这样的:

Parent.options.created = [
  created: function () {
    console.log('parentVal')
  }
]
复制代码

关键在这句:parentVal.concat(childVal),将 parentVal 和 childVal 合并成一个数组。所以最终结果如下:

[
  created: function () {
    console.log('parentVal')
  },
  created: function () {
    console.log('childVal')
  }
]
复制代码

另外我们注意第三个三目运算符:

: Array.isArray(childVal)
  ? childVal
  : [childVal]
复制代码

它判断了 childVal 是不是数组,这说明什么?说明了生命周期钩子是可以写成数组的,虽然 Vue 的文档里没有,不信你可以试试:

new Vue({
  created: [
    function () {
      console.log('first')
    },
    function () {
      console.log('second')
    },
    function () {
      console.log('third')
    }
  ]
})
复制代码

资源(assets)选项的合并策略

在 Vue 中 directives、filters 以及 components 被认为是资源,其实很好理解,指令、过滤器和组件都是可以作为第三方应用来提供的,比如你需要一个模拟滚动的组件,你当然可以选用超级强大的第三方组件 scroll-flip-page,所以这样看来 scroll-flip-page 就可以认为是资源,除了组件之外指令和过滤器也都是同样的道理。

而我们接下来要看的代码就是用来合并处理 directives、filters 以及 components 等资源选项的,看如下代码:

/**
 * Assets
 *
 * When a vm is present (instance creation), we need to do
 * a three-way merge between constructor options, instance
 * options and parent options.
 */
function mergeAssets (
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): Object {
  const res = Object.create(parentVal || null)
  if (childVal) {
    process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm)
    return extend(res, childVal)
  } else {
    return res
  }
}

ASSET_TYPES.forEach(function (type) {
  strats[type + 's'] = mergeAssets
})

export const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]
复制代码

上面的代码本身逻辑很简单,首先以 parentVal 为原型创建对象 res,然后判断是否有 childVal,如果有的话使用 extend 函数将 childVal 上的属性混合到 res 对象上并返回。如果没有 childVal 则直接返回 res。

举个例子,大家知道任何组件的模板中我们都可以直接使用 组件或者 等,但是我们并没有在我们自己的组件实例的 components 选项中显式地声明这些组件。那么这是怎么做到的呢?其实答案就在 mergeAssets 函数中。以下面的代码为例:

var v = new Vue({
  el: '#app',
  components: {
    ChildComponent: ChildComponent
  }
})
复制代码

上面的代码中,我们创建了一个 Vue 实例,并注册了一个子组件 ChildComponent,此时 mergeAssets 方法内的 childVal 就是例子中的 components 选项:

components: {
  ChildComponent: ChildComponent
}
复制代码

而 parentVal 就是 Vue.options.components,我们知道 Vue.options 如下:

Vue.options = {
	components: {
	  KeepAlive,
	  Transition,
	  TransitionGroup
	},
	directives: Object.create(null),
	directives:{
	  model,
	  show
	},
	filters: Object.create(null),
	_base: Vue
}
复制代码

所以 Vue.options.components 就应该是一个对象:

{
  KeepAlive,
  Transition,
  TransitionGroup
}
复制代码

也就是说 parentVal 就是如上包含三个内置组件的对象,所以经过如下这句话之后

const res = Object.create(parentVal || null)
复制代码

你可以通过 res.KeepAlive 访问到 KeepAlive 对象,因为虽然 res 对象自身属性没有 KeepAlive,但是它的原型上有。

然后再经过 return extend(res, childVal) 这句话之后,res 变量将被添加 ChildComponent 属性,最终 res 如下:

res = {
  ChildComponent
  // 原型
  __proto__: {
    KeepAlive,
    Transition,
    TransitionGroup
  }
}
复制代码

所以这就是为什么我们不用显式地注册组件就能够使用一些内置组件的原因,同时这也是内置组件的实现方式,通过 Vue.extend 创建出来的子类也是一样的道理,一层一层地通过原型进行组件的搜索.

这里面使用到了一个原型的模式,这样每个组件默认添加了 KeepAlive 、Transition、TransitionGroup。

选项 watch 的合并策略

/**
 * Watchers.
 *
 * Watchers hashes should not overwrite one
 * another, so we merge them as arrays.
 */
strats.watch = function (
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): ?Object {
  // work around Firefox's Object.prototype.watch...
  if (parentVal === nativeWatch) parentVal = undefined
  if (childVal === nativeWatch) childVal = undefined
  /* istanbul ignore if */
  if (!childVal) return Object.create(parentVal || null)
  if (process.env.NODE_ENV !== 'production') {
    assertObjectType(key, childVal, vm)
  }
  if (!parentVal) return childVal
  const ret = {}
  extend(ret, parentVal)
  for (const key in childVal) {
    let parent = ret[key]
    const child = childVal[key]
    if (parent && !Array.isArray(parent)) {
      parent = [parent]
    }
    ret[key] = parent
      ? parent.concat(child)
      : Array.isArray(child) ? child : [child]
  }
  return ret
}
复制代码
// work around Firefox's Object.prototype.watch...
if (parentVal === nativeWatch) parentVal = undefined
if (childVal === nativeWatch) childVal = undefined
复制代码

其中 nativeWatch 来自于 core/util/env.js 文件,大家可以在 core/util 目录下的工具方法全解 中查看其作用。在 Firefox 浏览器中 Object.prototype 拥有原生的 watch 函数,所以即便一个普通的对象你没有定义 watch 属性,但是依然可以通过原型链访问到原生的 watch 属性,这就会给 Vue 在处理选项的时候造成迷惑,因为 Vue 也提供了一个叫做 watch 的选项,即使你的组件选项中没有写 watch 选项,但是 Vue 通过原型访问到了原生的 watch。这不是我们想要的,所以上面两句代码的目的是一个变通方案,当发现组件选项是浏览器原生的 watch 时,那说明用户并没有提供 Vue 的 watch 选项,直接重置为 undefined

if (!childVal) return Object.create(parentVal || null)
复制代码

检测了是否有 childVal,即组件选项是否有 watch 选项,如果没有的话,直接以 parentVal 为原型创建对象并返回(如果有 parentVal 的话)。

如果组件选项中有 watch 选项,即 childVal 存在,则代码继续执行,接下来将执行这段代码:

if (process.env.NODE_ENV !== 'production') {
  assertObjectType(key, childVal, vm)
}
if (!parentVal) return childVal
复制代码

由于此时 childVal 存在,所以在非生产环境下使用 assertObjectType 函数对 childVal 进行类型检测,检测其是否是一个纯对象,我们知道 Vue 的 watch 选项需要是一个纯对象。接着判断是否有 parentVal,如果没有的话则直接返回 childVal,即直接使用组件选项的 watch。

如果存在 parentVal,那么代码继续执行,此时 parentVal 以及 childVal 都将存在,那么就需要做合并处理了,也就是下面要执行的代码:

// 定义 ret 常量,其值为一个对象
const ret = {}
// 将 parentVal 的属性混合到 ret 中,后面处理的都将是 ret 对象,最后返回的也是 ret 对象
extend(ret, parentVal)
// 遍历 childVal
for (const key in childVal) {
  // 由于遍历的是 childVal,所以 key 是子选项的 key,父选项中未必能获取到值,所以 parent 未必有值
  let parent = ret[key]
  // child 是肯定有值的,因为遍历的就是 childVal 本身
  const child = childVal[key]
  // 这个 if 分支的作用就是如果 parent 存在,就将其转为数组
  if (parent && !Array.isArray(parent)) {
    parent = [parent]
  }
  ret[key] = parent
    // 最后,如果 parent 存在,此时的 parent 应该已经被转为数组了,所以直接将 child concat 进去
    ? parent.concat(child)
    // 如果 parent 不存在,直接将 child 转为数组返回
    : Array.isArray(child) ? child : [child]
}
// 最后返回新的 ret 对象
return ret
复制代码

上面的代码段中写了很详细的注释。首先定义了 ret 常量,最后返回的也是 ret 常量,所以中间的代码是在充实 ret 常量。之后使用 extend 函数将 parentVal 的属性混合到 ret 中。然后开始一个 for in 循环遍历 childVal,这个循环的目的是:检测子选项中的值是否也在父选项中,如果在的话将父子选项合并到一个数组,否则直接把子选项变成一个数组返回

举个例子:

// 创建子类
const Sub = Vue.extend({
  // 检测 test 的变化
  watch: {
    test: function () {
      console.log('extend: test change')
    }
  }
})

// 使用子类创建实例
const v = new Sub({
  el: '#app',
  data: {
    test: 1
  },
  // 检测 test 的变化
  watch: {
    test: function () {
      console.log('instance: test change')
    }
  }
})

// 修改 test 的值
v.test = 2
复制代码

上面的代码中,当我们修改 v.test 的值时,两个观察 test 变化的函数都将被执行。

我们使用子类 Sub 创建了实例 v,对于实例 v 来讲,其 childVal 就是组件选项的 watch:

watch: {
  test: function () {
    console.log('instance: test change')
  }
}
复制代码

而其 parentVal 就是 Sub.options,实际上就是:

watch: {
  test: function () {
    console.log('extend: test change')
  }
}
复制代码

最终这两个 watch 选项将被合并为一个数组:

watch: {
  test: [
    function () {
      console.log('extend: test change')
    },
    function () {
      console.log('instance: test change')
    }
  ]
}
复制代码

选项 props、methods、inject、computed 的合并策略

/**
 * Other object hashes.
 */
strats.props =
strats.methods =
strats.inject =
strats.computed = function (
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): ?Object {
  if (childVal && process.env.NODE_ENV !== 'production') {
    assertObjectType(key, childVal, vm)
  }
  if (!parentVal) return childVal
  const ret = Object.create(null)
  extend(ret, parentVal)
  if (childVal) extend(ret, childVal)
  return ret
}
复制代码

这段代码的作用是在 strats 策略对象上添加 props、methods、inject 以及 computed 策略函数,顾名思义这些策略函数是分别用来合并处理同名选项的,并且所使用的策略相同。

对于 props、methods、inject 以及 computed 这四个选项有一个共同点,就是它们的结构都是纯对象,虽然我们在书写 props 或者 inject 选项的时候可能是一个数组,但是在 Vue的思路之选项的规范化 一节中我们知道,Vue 内部都将其规范化为了一个对象。所以我们看看 Vue 是如何处理这些对象散列的。

策略函数内容如下:

// 如果存在 childVal,那么在非生产环境下要检查 childVal 的类型
if (childVal && process.env.NODE_ENV !== 'production') {
  assertObjectType(key, childVal, vm)
}
// parentVal 不存在的情况下直接返回 childVal
if (!parentVal) return childVal
// 如果 parentVal 存在,则创建 ret 对象,然后分别将 parentVal 和 childVal 的属性混合到 ret 中,注意:由于 childVal 将覆盖 parentVal 的同名属性
const ret = Object.create(null)
extend(ret, parentVal)
if (childVal) extend(ret, childVal)
// 最后返回 ret 对象。
return ret
复制代码

首先,会检测 childVal 是否存在,即子选项是否有相关的属性,如果有的话在非生产环境下需要使用 assertObjectType 检测其类型,保证其类型是纯对象。然后会判断 parentVal 是否存在,不存在的话直接返回子选项。

如果 parentVal 存在,则使用 extend 方法将其属性混合到新对象 ret 中,如果 childVal 也存在的话,那么同样会再使用 extend 函数将其属性混合到 ret 中,所以如果父子选项中有相同的键,那么子选项会把父选项覆盖掉。

以上就是 props、methods、inject 以及 computed 这四个属性的通用合并策略。

选项 provide 的合并策略

最后一个选项的合并策略,就是 provide 选项的合并策略,只有一句代码,如下:

strats.provide = mergeDataOrFn
复制代码

也就是说 provide 选项的合并策略与 data 选项的合并策略相同,都是使用 mergeDataOrFn 函数。

选项处理小结

现在我们了解了 Vue 中是如何合并处理选项的,接下来我们稍微做一个总结:

  • 对于 el、propsData 选项使用默认的合并策略 defaultStrat。
  • 对于 data 选项,使用 mergeDataOrFn 函数进行处理,最终结果是 data 选项将变成一个函数,且该函数的执行结果为真正的数据对象。
  • 对于 生命周期钩子 选项,将合并成数组,使得父子选项中的钩子函数都能够被执行
  • 对于 directives、filters 以及 components 等资源选项,父子选项将以原型链的形式被处理,正是因为这样我们才能够在任何地方都使用内置组件、指令等。
  • 对于 watch 选项的合并处理,类似于生命周期钩子,如果父子选项都有相同的观测字段,将被合并为数组,这样观察者都将被执行。
  • 对于 props、methods、inject、computed 选项,父选项始终可用,但是子选项会覆盖同名的父选项字段。
  • 对于 provide 选项,其合并策略使用与 data 选项相同的 mergeDataOrFn 函数。
  • 最后,以上没有提及到的选项都将使默认选项 defaultStrat。
  • 最最后,默认合并策略函数 defaultStrat 的策略是:只要子选项不是 undefined 就使用子选项,否则使用父选项。

再看 mixins 和 extends

在 Vue选项的规范化 一节中,我们讲到了 mergeOptions 函数中的如下这段代码:

const extendsFrom = child.extends
if (extendsFrom) {
  parent = mergeOptions(parent, extendsFrom, vm)
}
if (child.mixins) {
  for (let i = 0, l = child.mixins.length; i < l; i++) {
    parent = mergeOptions(parent, child.mixins[i], vm)
  }
}
复制代码

当时候我们并没有深入讲解,因为当时我们还不了解 mergeOptions 函数的作用,但是现在我们可以回头来看一下这段代码了。

我们知道 mixins 在 Vue 中用于解决代码复用的问题,比如混入 created 生命周期钩子,用于打印一句话:

const consoleMixin = {
  created () {
    console.log('created:mixins')
  }
}

new Vue ({
  mixins: [consoleMixin],
  created () {
    console.log('created:instance')
  }
})
复制代码

运行以上代码,将打印两句话:

// created:mixins
// created:instance
复制代码

这是因为 mergeOptions 函数在处理 mixins 选项的时候递归调用了 mergeOptions 函数将 mixins 合并到了 parent 中,并将合并后生成的新对象作为新的 parent:

if (child.mixins) {
  for (let i = 0, l = child.mixins.length; i < l; i++) {
    parent = mergeOptions(parent, child.mixins[i], vm)
  }
}
复制代码

上例中我们只涉及到 created 生命周期钩子的合并,所以会使用生命周期钩子的合并策略函数进行处理,现在我们已经知道 mergeOptions 会把生命周期选项合并为一个数组,所以所有的生命周期钩子都会被执行。那么不仅仅是生命周期钩子,任何写在 mixins 中的选项,都会使用 mergeOptions 中相应的合并策略进行处理,这就是 mixins 的实现方式。

对于 extends 选项,与 mixins 相同,甚至由于 extends 选项只能是一个对象,而不能是数组,反而要比 mixins 的实现更为简单,连遍历都不需要。

为什么我们在项目中可以这样访问data中的数据

const vm = new Vue({
	el: '#app',
	data: {
		name: 'Li lei'
	}
})

console.log(vm.name) // Li lei
复制代码

能这样访问data中的原理主要是这样

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
    
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(key)) {
      proxy(vm, `_data`, key)     // 这句是重点
    }
  }	
}


const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

// 这下面的代码才是为啥可以直接在实例 this/vm 上能直接访问data中的元素(ps: name  -> Li lei)
export function proxy (target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
复制代码

Vue中如何避免依赖的重复收集

<div id="demo">
  <p>{{name}}</p>
</div>

// 编译生成的渲染函数是一个匿名函数
function anonymous () {
  with (this) {
    return _c('div',
      { attrs:{ "id": "demo" } },
      [_v("\n      "+_s(name)+"\n    ")]
    )
  }
}

// Watcher
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()

// 当_init中调用$mounte(), 其实是调用定义在lifecycle.js中的-> mountComponent, 然后创建一个 Watcher

调用
this.value = this.lazy
      ? undefined
      : this.get()
      
// 调用 this.get();

  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }
 
// 调用顶上的get(),其实是触发之前的渲染函数 
updateComponent = () => {
    vm._update(vm._render(), hydrating)
}
这样就触发了虚拟dom -> 真是dom 然后就会触发 name的get方法,但是在触发get之前,咱们通过 putTarget(this)

// Dep.js
export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

depend () {
    if (Dep.target) {
        Dep.target.addDep(this)
    }
}


// observer -> index.js -> defineReactive
get: function reactiveGetter () {
    const value = getter ? getter.call(obj) : val
    if (Dep.target) {
    dep.depend()
    if (childOb) {
        childOb.dep.depend()
        if (Array.isArray(value)) {
        dependArray(value)
        }
      }
    }
    return value
},

这个是在data中的某个属性的get方法,会调用 dep.depend();  这个里面会调用 Dep.target.addDep(this)
Dep.target 指向 当前的 Watcher 

// dep.js
depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
}

// watcher.js
addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        dep.addSub(this)
      }
    }
}

1、newDepIds 属性用来在一次求值中避免收集重复的观察者
2、每次求值并收集观察者完成之后会清空 newDepIds 和 newDeps 这两个属性的值,并且在被清空之前把值分别赋给了 depIds 属性和 deps 属性
3、depIds 属性用来避免重复求值时收集重复的观察者

通过以上三点内容我们可以总结出一个结论,即 newDepIds 和 newDeps 这两个属性的值所存储的总是当次求值所收集到的 Dep 实例对象,而 depIds 和 deps 这两个属性的值所存储的总是上一次求值过程中所收集到的 Dep 实例对象。

除了以上三点之外,其实 deps 属性还能够用来移除废弃的观察者,cleanupDeps 方法中开头的那段 while 循环就是用来实现这个功能的,如下代码所示:

cleanupDeps () {
  let i = this.deps.length
  while (i--) {
    const dep = this.deps[i]
    if (!this.newDepIds.has(dep.id)) {
      dep.removeSub(this)
    }
  }
  // 省略...
}

这段 while 循环就是对 deps 数组进行遍历,也就是对上一次求值所收集到的 Dep 对象进行遍历,然后在循环内部检查上一次求值所收集到的 Dep 实例对象是否存在于当前这次求值所收集到的 Dep 实例对象中,如果不存在则说明该 Dep 实例对象已经和该观察者不存在依赖关系了,这时就会调用 dep.removeSub(this) 方法并以该观察者实例对象作为参数传递,从而将该观察者对象从 Dep 实例对象中移除
复制代码
分类:
前端
标签:
分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改