敏小静

敏小静 查看完整档案

广州编辑  |  填写毕业院校  |  填写所在公司/组织 segmentfault.com/u/minxiaojing 编辑
编辑

前端工程师

个人动态

敏小静 收藏了文章 · 2020-11-24

设计方案--浅析观察者模式和发布-订阅模式的区别

有时候面试的时候可能会被问到:

观察者模式和发布订阅模式的区别?

没有区别吧?

好像有区别吧?

我们首先来看一下“观察者模式”和“发布订阅模式”

一、观察者设计模式

理解设计模式在设计之初的是为了解决什么问题就能很好的在写代码中运用不同的设计模式。

所谓的观察者模式,其实为了实现松耦合(loosely coupled),也就是为了解耦。

当数据有变化有更新的时候,某个方法被调用,这时候就可以更新其他地方需要用到这个数据的地方。

举个例子:

以气象站为例,每当气象站测试数据有更新的时候,changed()方法都会被调用,于是我们在changed()方法中,更新气象仪器上的数据,比如温度、气压等等。

这样写没有毛病,但是,如果我们以后再changed()方法调用的时候,更新更多的信息,比如说湿度,这个时候我们需要去修改changed()方法的代码,这个就是紧耦合的坏处。

那怎么解决这个紧耦合的问题呢?

观察者模式中,changed()方法所在的实例对象,就是被观察者(subject),只需要维护一套观察者(observer)的集合,这些observer实现相同的接口,subject只需要知道:通知观察者(observer)时,需要调用哪一个统一方法就行。
imgaes

我们再来看一下简单的例子

function Subject(){
    this.observerData = [];
}
Subject.prototype = {
    add: function(observer) {
        this.observerData.push(observer);
    },
    remove:function(observer) {
        var observerData = this.observerData;
        for(let i=0,length=observerData.length;i<length;i++){
            if(observerData[i] === observer){
                observerData.splice(i,1)
            }
        }
    },
    notify: function() {
        var observerData = this.observerData;
        for(let i=0,length=observerData.length;i<length;i++){
            observerData[i].update();
        }
    }
}

function Observer(name){
    this.name = name;
}

Observer.prototype = {
    update: function() {
        console.log('hello,' + this.name);
    }
}

var sub = new Subject();

var obj1 = new Observer('saucxs');
var obj2 = new Observer('songEagle');

sub.add(obj1);
sub.add(obj2);
sub.notify();

这时候输出的是

hello,saucxs

hello,songEagle

上述代码中,我们创建了Subject对象和两个Observer对象,当前状态发生变更的时候则通过Subject对象的notify方法通知两个Observer队形,这两个Observer对象通过update方法进行更新。

二、发布订阅模式(Publisher && Subscriber)

其实觉得发布订阅模式里的Publisher,就是观察者模式的Subject,而Subscriber就是Observer。Publisher变化的时候,就会主动去通知Subscriber。

其实并不是这样的。

在发布订阅模式里,发布者并不会直接通知订阅者,换句话说,发布者和订阅者,彼此互补相识。

那他们之间如何进行通信的?是通过第三者,就是在消息队列中,我们常说的经纪人Broker。

image

发布者只需要告诉Broker,我要发消息。topic是“saucxs”

订阅者只需要告诉Broker,我要订阅topic是“saucxs”

于是,当Broker接收到发布者发来的消息,并且topic是saucxs的时候,就会把消息推送到订阅了topic的saucxs的订阅者,也可能是订阅者自己过来拉取,具体看代码实现。

也就是说,发布订阅模式中,发布者和订阅者,不是松耦合,是完全的解耦的。

放一张很简单的图,对比一下两个模式的区别:

image

三、总结

表面上看:

1、观察者模式中,两个角色:观察者和被观察者

2、发布订阅模式中,三个角色:发布者,订阅者,经纪人

深层次的看:

1、观察者模式:观察者和被观察者,是松耦合的关系

2、发布订阅模式:发布和订阅者是完全不存在耦合的

从使用层面上看:

1、观察者模式:多用于单个应用内部,维护的是单一事件对应多个依赖的

event -> [obj1,obj2,obj3,....]
2、发布订阅模式:多用于跨应用的模式,比如我们说的消息的中间件,维护的是多个事件以及依赖的

event1 -> [obj1,obj2,...]
event2 -> [obj1,obj3,...]

查看原文

敏小静 赞了文章 · 2020-10-27

ES6 系列之模拟实现 Symbol 类型

前言

实际上,Symbol 的很多特性都无法模拟实现……所以先让我们回顾下有哪些特性,然后挑点能实现的……当然在看的过程中,你也可以思考这个特性是否能实现,如果可以实现,该如何实现。

回顾

ES6 引入了一种新的原始数据类型 Symbol,表示独一无二的值。

1. Symbol 值通过 Symbol 函数生成,使用 typeof,结果为 "symbol"

var s = Symbol();
console.log(typeof s); // "symbol"

2. Symbol 函数前不能使用 new 命令,否则会报错。这是因为生成的 Symbol 是一个原始类型的值,不是对象。

3. instanceof 的结果为 false

var s = Symbol('foo');
console.log(s instanceof Symbol); // false

4. Symbol 函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。

var s1 = Symbol('foo');
console.log(s1); // Symbol(foo)

5. 如果 Symbol 的参数是一个对象,就会调用该对象的 toString 方法,将其转为字符串,然后才生成一个 Symbol 值。

const obj = {
  toString() {
    return 'abc';
  }
};
const sym = Symbol(obj);
console.log(sym); // Symbol(abc)

6. Symbol 函数的参数只是表示对当前 Symbol 值的描述,相同参数的 Symbol 函数的返回值是不相等的。

// 没有参数的情况
var s1 = Symbol();
var s2 = Symbol();

console.log(s1 === s2); // false

// 有参数的情况
var s1 = Symbol('foo');
var s2 = Symbol('foo');

console.log(s1 === s2); // false

7. Symbol 值不能与其他类型的值进行运算,会报错。

var sym = Symbol('My symbol');

console.log("your symbol is " + sym); // TypeError: can't convert symbol to string

8. Symbol 值可以显式转为字符串。

var sym = Symbol('My symbol');

console.log(String(sym)); // 'Symbol(My symbol)'
console.log(sym.toString()); // 'Symbol(My symbol)'

9. Symbol 值可以作为标识符,用于对象的属性名,可以保证不会出现同名的属性。

var mySymbol = Symbol();

// 第一种写法
var a = {};
a[mySymbol] = 'Hello!';

// 第二种写法
var a = {
  [mySymbol]: 'Hello!'
};

// 第三种写法
var a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });

// 以上写法都得到同样结果
console.log(a[mySymbol]); // "Hello!"

10. Symbol 作为属性名,该属性不会出现在 for...in、for...of 循环中,也不会被 Object.keys()、Object.getOwnPropertyNames()、JSON.stringify() 返回。但是,它也不是私有属性,有一个 Object.getOwnPropertySymbols 方法,可以获取指定对象的所有 Symbol 属性名。

var obj = {};
var a = Symbol('a');
var b = Symbol('b');

obj[a] = 'Hello';
obj[b] = 'World';

var objectSymbols = Object.getOwnPropertySymbols(obj);

console.log(objectSymbols);
// [Symbol(a), Symbol(b)]

11. 如果我们希望使用同一个 Symbol 值,可以使用 Symbol.for。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建并返回一个以该字符串为名称的 Symbol 值。

var s1 = Symbol.for('foo');
var s2 = Symbol.for('foo');

console.log(s1 === s2); // true

12. Symbol.keyFor 方法返回一个已登记的 Symbol 类型值的 key。

var s1 = Symbol.for("foo");
console.log(Symbol.keyFor(s1)); // "foo"

var s2 = Symbol("foo");
console.log(Symbol.keyFor(s2) ); // undefined

分析

看完以上的特性,你觉得哪些特性是可以模拟实现的呢?

如果我们要模拟实现一个 Symbol 的话,基本的思路就是构建一个 Symbol 函数,然后直接返回一个独一无二的值。

不过在此之前,我们先看看规范中调用 Symbol 时到底做了哪些工作:

Symbol ( [ description ] )

When Symbol is called with optional argument description, the following steps are taken:

  1. If NewTarget is not undefined, throw a TypeError exception.
  2. If description is undefined, var descString be undefined.
  3. Else, var descString be ToString(description).
  4. ReturnIfAbrupt(descString).
  5. Return a new unique Symbol value whose [[Description]] value is descString.

当调用 Symbol 的时候,会采用以下步骤:

  1. 如果使用 new ,就报错
  2. 如果 description 是 undefined,让 descString 为 undefined
  3. 否则 让 descString 为 ToString(description)
  4. 如果报错,就返回
  5. 返回一个新的唯一的 Symbol 值,它的内部属性 [[Description]] 值为 descString

考虑到还需要定义一个 [[Description]] 属性,如果直接返回一个基本类型的值,是无法做到这一点的,所以我们最终还是返回一个对象。

第一版

参照着规范,其实我们已经可以开始写起来了:

// 第一版
(function() {
    var root = this;

    var SymbolPolyfill = function Symbol(description) {

        // 实现特性第 2 点:Symbol 函数前不能使用 new 命令
        if (this instanceof SymbolPolyfill) throw new TypeError('Symbol is not a constructor');

        // 实现特性第 5 点:如果 Symbol 的参数是一个对象,就会调用该对象的 toString 方法,将其转为字符串,然后才生成一个 Symbol 值。
        var descString = description === undefined ? undefined : String(description)

        var symbol = Object.create(null)

        Object.defineProperties(symbol, {
            '__Description__': {
                value: descString,
                writable: false,
                enumerable: false,
                configurable: false
            }
        });

        // 实现特性第 6 点,因为调用该方法,返回的是一个新对象,两个对象之间,只要引用不同,就不会相同
        return symbol;
    }

    root.SymbolPolyfill = SymbolPolyfill;
})();

只是参照着规范,我们已经实现了特性的第 2、5、6 点。

第二版

我们来看看其他的特性该如何实现:

1. 使用 typeof,结果为 "symbol"。

利用 ES5,我们并不能修改 typeof 操作符的结果,所以这个无法实现。

3. instanceof 的结果为 false

因为不是通过 new 的方式实现的,所以 instanceof 的结果自然是 false。

4. Symbol 函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述。主要是为了在控制台显示,或者转为字符串时,比较容易区分。

当我们打印一个原生 Symbol 值的时候:

console.log(Symbol('1')); // Symbol(1)

可是我们模拟实现的时候返回的却是一个对象,所以这个也是无法实现的,当然你修改 console.log 这个方法是另讲。

8. Symbol 值可以显式转为字符串。

var sym = Symbol('My symbol');

console.log(String(sym)); // 'Symbol(My symbol)'
console.log(sym.toString()); // 'Symbol(My symbol)'

当调用 String 方法的时候,如果该对象有 toString 方法,就会调用该 toString 方法,所以我们只要给返回的对象添加一个 toString 方法,即可实现这两个效果。

// 第二版

// 前面面代码相同 ……

var symbol = Object.create({
    toString: function() {
        return 'Symbol(' + this.__Description__ + ')';
    },
});

// 后面代码相同 ……

第三版

9. Symbol 值可以作为标识符,用于对象的属性名,可以保证不会出现同名的属性。

看着好像没什么,这点其实和第 8 点是冲突的,这是因为当我们模拟的所谓 Symbol 值其实是一个有着 toString 方法的 对象,当对象作为对象的属性名的时候,就会进行隐式类型转换,还是会调用我们添加的 toString 方法,对于 Symbol('foo') 和 Symbol('foo')两个 Symbol 值,虽然描述一样,但是因为是两个对象,所以并不相等,但是当作为对象的属性名的时候,都会隐式转换为 Symbol(foo) 字符串,这个时候就会造成同名的属性。举个例子:

var a = SymbolPolyfill('foo');
var b = SymbolPolyfill('foo');

console.log(a ===  b); // false

var o = {};
o[a] = 'hello';
o[b] = 'hi';

console.log(o); // {Symbol(foo): 'hi'}

为了防止不会出现同名的属性,毕竟这是一个非常重要的特性,迫不得已,我们需要修改 toString 方法,让它返回一个唯一值,所以第 8 点就无法实现了,而且我们还需要再写一个用来生成 唯一值的方法,就命名为 generateName,我们将该唯一值添加到返回对象的 __Name__ 属性中保存下来。

// 第三版
(function() {
    var root = this;

    var generateName = (function(){
        var postfix = 0;
        return function(descString){
            postfix++;
            return '@@' + descString + '_' + postfix
        }
    })()

    var SymbolPolyfill = function Symbol(description) {

        if (this instanceof SymbolPolyfill) throw new TypeError('Symbol is not a constructor');

        var descString = description === undefined ? undefined : String(description)

        var symbol = Object.create({
            toString: function() {
                return this.__Name__;
            }
        })

        Object.defineProperties(symbol, {
            '__Description__': {
                value: descString,
                writable: false,
                enumerable: false,
                configurable: false
            },
            '__Name__': {
                value: generateName(descString),
                writable: false,
                enumerable: false,
                configurable: false
            }
        });

        return symbol;
    }


    root.SymbolPolyfill = SymbolPolyfill;

})()

此时再看下这个例子:

var a = SymbolPolyfill('foo');
var b = SymbolPolyfill('foo');

console.log(a ===  b); // false

var o = {};
o[a] = 'hello';
o[b] = 'hi';

console.log(o); // Object { "@@foo_1": "hello", "@@foo_2": "hi" }

第四版

我们再看看接下来的特性。

7.Symbol 值不能与其他类型的值进行运算,会报错。

+ 操作符为例,当进行隐式类型转换的时候,会先调用对象的 valueOf 方法,如果没有返回基本值,就会再调用 toString 方法,所以我们考虑在 valueOf 方法中进行报错,比如:

var symbol = Object.create({
    valueOf: function() {
        throw new Error('Cannot convert a Symbol value')
    }
})

console.log('1' + symbol); // 报错

看着很简单的解决了这个问题,可是如果我们是显式调用 valueOf 方法呢?对于一个原生的 Symbol 值:

var s1 = Symbol('foo')
console.log(s1.valueOf()); // Symbol(foo)

是的,对于原生 Symbol,显式调用 valueOf 方法,会直接返回该 Symbol 值,而我们又无法判断是显式还是隐式的调用,所以这个我们就只能实现一半,要不然实现隐式调用报错,要不然实现显式调用返回该值,那……我们选择不报错的那个吧,即后者。

我们迫不得已的修改 valueOf 函数:

// 第四版
// 前面面代码相同 ……

var symbol = Object.create({
    toString: function() {
        return this.__Name__;
    },
    valueOf: function() {
        return this;
    }
});
// 后面代码相同 ……

第五版

10. Symbol 作为属性名,该属性不会出现在 for...in、for...of 循环中,也不会被 Object.keys()、Object.getOwnPropertyNames()、JSON.stringify() 返回。但是,它也不是私有属性,有一个 Object.getOwnPropertySymbols 方法,可以获取指定对象的所有 Symbol 属性名。

嗯,无法实现。

11. 有时,我们希望重新使用同一个Symbol值,Symbol.for方法可以做到这一点。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的Symbol值。如果有,就返回这个Symbol值,否则就新建并返回一个以该字符串为名称的Symbol值。

这个实现类似于函数记忆,我们建立一个对象,用来储存已经创建的 Symbol 值即可。

12. Symbol.keyFor 方法返回一个已登记的 Symbol 类型值的 key。

遍历 forMap,查找该值对应的键值即可。

// 第五版
// 前面代码相同 ……
var SymbolPolyfill = function() { ... }

var forMap = {};

Object.defineProperties(SymbolPolyfill, {
    'for': {
        value: function(description) {
            var descString = description === undefined ? undefined : String(description)
            return forMap[descString] ? forMap[descString] : forMap[descString] = SymbolPolyfill(descString);
        },
        writable: true,
        enumerable: false,
        configurable: true
    },
    'keyFor': {
        value: function(symbol) {
            for (var key in forMap) {
                if (forMap[key] === symbol) return key;
            }
        },
        writable: true,
        enumerable: false,
        configurable: true
    }
});
// 后面代码相同 ……

完整实现

综上所述:

无法实现的特性有:1、4、7、8、10

可以实现的特性有:2、3、5、6、9、11、12

最后的实现如下:

(function() {
    var root = this;

    var generateName = (function(){
        var postfix = 0;
        return function(descString){
            postfix++;
            return '@@' + descString + '_' + postfix
        }
    })()

    var SymbolPolyfill = function Symbol(description) {

        if (this instanceof SymbolPolyfill) throw new TypeError('Symbol is not a constructor');

        var descString = description === undefined ? undefined : String(description)

        var symbol = Object.create({
            toString: function() {
                return this.__Name__;
            },
            valueOf: function() {
                return this;
            }
        })

        Object.defineProperties(symbol, {
            '__Description__': {
                value: descString,
                writable: false,
                enumerable: false,
                configurable: false
            },
            '__Name__': {
                value: generateName(descString),
                writable: false,
                enumerable: false,
                configurable: false
            }
        });

        return symbol;
    }

    var forMap = {};

    Object.defineProperties(SymbolPolyfill, {
        'for': {
            value: function(description) {
                var descString = description === undefined ? undefined : String(description)
                return forMap[descString] ? forMap[descString] : forMap[descString] = SymbolPolyfill(descString);
            },
            writable: true,
            enumerable: false,
            configurable: true
        },
        'keyFor': {
            value: function(symbol) {
                for (var key in forMap) {
                    if (forMap[key] === symbol) return key;
                }
            },
            writable: true,
            enumerable: false,
            configurable: true
        }
    });

    root.SymbolPolyfill = SymbolPolyfill;

})()

ES6 系列

ES6 系列目录地址:https://github.com/mqyqingfeng/Blog

ES6 系列预计写二十篇左右,旨在加深 ES6 部分知识点的理解,重点讲解块级作用域、标签模板、箭头函数、Symbol、Set、Map 以及 Promise 的模拟实现、模块加载方案、异步处理等内容。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

查看原文

赞 33 收藏 21 评论 1

敏小静 发布了文章 · 2020-10-19

html之离线缓存

实例 - 完整的 Manifest 文件

  

  

1,什么是应用程序缓存(Application Cache)

  HTML5 引入了应用程序缓存,这意味着 web 应用可进行缓存,并可在没有因特网连接时进行访问。

  离线缓存:

    离线缓存可以将站点的一些文件缓存到本地,它是浏览器自己的一种机制,

    将需要的文件缓存下来,以便后期即使没有连接网络,被缓存的页面也可以展示。

    即使有网络,优先本地存储的资源

2,应用程序缓存为应用带来三个优势:

  • 离线浏览 - 用户可在应用离线时使用它们

    * 速度 - 已缓存资源加载得更快

    * 减少服务器负载 - 浏览器将只从服务器下载更新过或更改过的资源。

  * 【

    在没有网络的时候可以访问到缓存的对应的站点页面,包括html,js,css,img等等文件
    在有网络的时候,浏览器也会优先使用已离线存储的文件,返回一个200(from cache)头。这跟HTTP的缓存使用策略是不同的
    资源的缓存可以带来更好的用户体验,当用户使用自己的流量上网时,本地缓存不仅可以提高用户访问速度,而且大大节约用户的使用流量。
     】

3,如何实现离线缓存:

a)Cache Manifest 基础

如需启用应用程序缓存,请在文档的<html> 标签中包含 manifest 属性:

    * 在需要缓存的html根节点上面添加属性 manifest ,属性值是一个 . appcache 文件;

    Appcache 是一个控制缓存文件

    * 在同目录下创建demo.appcache 文件,幷添加配置项

  b)Manifest 文件

manifest 文件是简单的文本文件,它告知浏览器被缓存的内容(以及不缓存的内容)。

      manifest 文件可分为三个部分:

        CACHE MANIFEST - 在此标题下列出的文件将在首次下载后进行缓存

        NETWORK - 在此标题下列出的文件需要与服务器的连接,且不会被缓存

        FALLBACK - 在此标题下列出的文件规定当页面无法访问时的回退页面(比如 404 页面)

      * CACHE MANIFEST

      第一行,CACHE MANIFEST,是必需的:

      CACHE MANIFEST

      /theme.css

      /logo.gif

      /main.js

      上面manifest 文件列出了三个资源:一个 CSS 文件,一个 GIF 图像,一个 JavaScript 文件

      * NETWORK

      NETWORK:

      login.php

      可以使用星号来指示所有其他资源/文件都需要因特网连接:

      NETWORK:

      *

      * FALLBACK

      FALLBACK:

      /html/   /offline.html

      第一个 URI 是资源,第二个是替补。

4,更新缓存:

一旦应用被缓存,它就会保持缓存直到发生下列情况:

    用户清空浏览器缓存

    manifest 文件被修改

    由程序来更新应用缓存

 5,在服务器端将.appcache文件的mime类型配置成 text/cache-manifest

  下面以phpstudy为例

   打开,mime.types ,在后面添加----

   

6,在网页中打开

    

关于应用程序缓存的注意事项

**-------------

提示:以 "#" 开头的是注释行,(# 2012-02-21 v1.0.0)但也可满足其他用途。应用的缓存只会在其 manifest 文件改变时被更新。如果您编辑了一幅图像,或者修改了一个 JavaScript 函数,这些改变都不会被重新缓存。更新注释行中的日期和版本号是一种使浏览器重新缓存文件的办法。**

浏览器对缓存数据的容量限制可能不太一样(某些浏览器的限制是每个站点 5MB)。

查看原文

赞 0 收藏 0 评论 0

敏小静 发布了文章 · 2020-10-18

vue响应式系统(变化检测)

变化侦测分为两种类型,一种是“”推,另一种是“拉”。
Angular和react的变化侦测都属于“拉”,也就是说,当状态发生变化时,它不知道哪个状态发生变化了,只知道状态有可能改变了,然后就会发送一个信号告诉框架,框架收到信号后,就会进行一个暴力对比找到哪些DOM需要重新渲染的。这也是Angular的脏检查(基于zoom.js,利用$digest函数触发)的过程,react用的是虚拟DOM。
Vue的变化侦测属于“推”。当状态发生变化,Vue在一定程度上能马上知道哪些状态发生改变,具有更细粒度的更新;也因为粒度越细,每个状态绑定的依赖就越多,依赖追踪在内存中的开销就越大,因此,Vue也引入了虚拟DOM的概念,将一个状态的细粒度绑定到组件(Vue的另一核心:单文件组件化),这样子当状态发生改变,就会通知到组件,组件内部再使用虚拟DOM进行对比更新。
众所周知,Vue2.x的版本使用Object.defineProperty,Vue3.x使用ES6的Proxy来进行变化侦测的。下面主要讲Object和Array的变化侦测:
(注:以下代码块来自Vue2.6.10源码)

一、Object的变化侦测
1.Object通过Object.defineProperty将属性转换成getter/setter的形式来追踪变化,在getter中收集依赖,在setter中触发依赖。

第一版Object.defineProperty:

function defineReactive$$1 (obj, key, val) {
     Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get: function reactiveGetter () {
        return val
      },
      set: function reactiveSetter (newVal) {
         if(val === newVal){
            return
         }
         val = newVal;
      }
    });
  }
    

2.在getter中收集依赖,依赖被存储在Dep中,在Dep中对依赖进行添加,删除以及更新等操作。

Dep的封装:
var uid = 0;
var Dep = function Dep () {
    this.id = uid++;
    this.subs = [];
  };

  Dep.prototype.addSub = function addSub (sub) {
    this.subs.push(sub);
  };

  Dep.prototype.removeSub = function removeSub (sub) {
    remove(this.subs, sub);
  };

  Dep.prototype.depend = function depend () {
    if (Dep.target) {
      Dep.target.addDep(this);
    }
  };

  Dep.prototype.notify = function notify () {
    // stabilize the subscriber list first
    var subs = this.subs.slice();
    for (var i = 0, l = subs.length; i < l; i++) {
      subs[i].update();
    }
  };

  // The current target watcher being evaluated.
  // This is globally unique because only one watcher
  // can be evaluated at a time.
  Dep.target = null;
  var targetStack = [];

  function pushTarget (target) {
    targetStack.push(target);
    Dep.target = target;
  }

  function popTarget () {
    targetStack.pop();
    Dep.target = targetStack[targetStack.length - 1];
  }
  function remove (arr, item) {
    if (arr.length) {
      var index = arr.indexOf(item);
      if (index > -1) {
        return arr.splice(index, 1)
      }
    }
  }

第二版Object.defineProperty:

function defineReactive$$1 (obj,key,val) {
    var dep = new Dep();//新增
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get: function reactiveGetter () {
        if (Dep.target) {//新增
          dep.depend();//新增
        }
        return val
      },
      set: function reactiveSetter (newVal) {
        if (newVal === val) {
          return
        }
        val = newVal;
        dep.notify();//新增
      }
    });
  }

3.所谓的依赖就是wather,只有watcher触发的getter就会去收集依赖到Dep中去,当数据发生变化时,会循环依赖列表,把所有的watcher通知一遍。
watcher原理:先把自己设置到全局唯一的指定位置(pushTarget(this)),然后读取数据( value = this.getter.call(vm, vm);)触发该数据的getter。接着,在getter中就会从全局唯一的那个位置读取正在读取数据的watcher,并把这个watcher收集到Dep中。

  /**
   * A watcher parses an expression, collects dependencies,
   * and fires callback when the expression value changes.
   * This is used for both the $watch() api and directives.
   */
  var Watcher = function Watcher (vm,expOrFn,cb) {
    this.vm = vm;
    this.cb = cb;
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn;
    } else {
      this.getter = parsePath(expOrFn);//parsePath读取一个字符串的keypath
    }
  };

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  Watcher.prototype.get = function get () {
    pushTarget(this);//this就是当前watcher实例。看上面的pushTarget,把watcher实例赋值给Dep。target
    var value;
    var vm = this.vm;
    try {
      value = this.getter.call(vm, vm);//读取值的时候,触发getter,就可以将this主动添加到Dep(依赖收集)
    } catch (e) {
        throw e
    } finally {
      
    }
    return value
  };
  /**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
  Watcher.prototype.update = function update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true;
    } else if (this.sync) {
      this.run();
    } else {
      queueWatcher(this);
    }
  };

4.创建Observer方法递归object中的所有数据(包括子数据)都转换成响应式形式。只有Object类型才会调用walk将每个属性转换成getter/setter形式侦测,在defineReactive$$1中新增observe(new Observer(value);)来递归子属性。

var Observer = function Observer (value) {
    this.value = value;
    if (Array.isArray(value)) {
    } else {
      this.walk(value);
    }
  };
    Observer.prototype.walk = function walk (obj) {
    var keys = Object.keys(obj);
    for (var i = 0; i < keys.length; i++) {
      defineReactive$$1(obj, keys[i]);
    }
  };

5.无法跟踪新增属性和删除属性,可以用Vue提供的vm.$set和vm.$delete解决
6.总结图解:
image

二、Array的变化侦测
1.在Observer中使用拦截器覆盖那些即将转换成响应式的Array类型数据的原型。(value.__proto__=Object.create(Array.prototype);)

function copyAugment (target, src, keys) {
  for (var i = 0, l = keys.length; i < l; i++) {
    var key = keys[i];
    def(target, key, src[key]);
  }
}
 function def (obj, key, val, enumerable) {
    Object.defineProperty(obj, key, {
      value: val,
      enumerable: !!enumerable,
      writable: true,
      configurable: true
    });
  }
  //鉴于有些浏览器不支持__proto__属性,在这个判断对象中是否有__proto__
  var hasProto = '__proto__' in {};
  var arrayKeys = Object.getOwnPropertyNames(arrayMethods);
  var Observer = function Observer (value) {
  this.dep = new Dep();//创建一个Dep实例,存放依赖列表
  def(value, '__ob__', this);//在value上增加一个不可枚举的属性__ob__,这个属性的值是Observer实例。这样就可以通过__ob__访问Observer实例,
  if (Array.isArray(value)) {
      if (hasProto) {
      //__proto__是指向原型的,通过它来覆盖侦测数据的原型;Object.create返回一个带有Array原型对象和属性的新对象。
        value.__proto__=Object.create(Array.prototype);
      } else {
        copyAugment(value, arrayMethods, arrayKeys);
      }
    }
  };
2.Array也是在getter中收集依赖,在拦截器中触发依赖,并将依赖保存在Observer实例的Dep中。
function observe (value, asRootData) {
    if (!isObject(value) || value instanceof VNode) {
      return
    }
    var ob;
    if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
      ob = value.__ob__;
    } else {
      ob = new Observer(value);
    }
    
    return ob
  }
function defineReactive$$1 (obj, key, val) {
     var childOb = observe(val);//为val创建Observer实例
     Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get: function reactiveGetter () {
      if (childOb) {
           childOb.dep.depend();//收集依赖
         }
        return val
      },
      set: function reactiveSetter (newVal) {
         if(val === newVal){
            return
         }
         val = newVal;
      }
    });
  }

3.向依赖发送通知(通过this.__ob__访问Observer实例,调用Observer中Dep的notify发送通知依赖)

var methodsToPatch = [
    'push',
    'pop',
    'shift',
    'unshift',
    'splice',
    'sort',
    'reverse'
  ];

  /**
   * Intercept mutating methods and emit events
   */
  methodsToPatch.forEach(function (method) {
    def(arrayMethods, method, function mutator () {
      var ob = this.__ob__;//重点
      var inserted;//获取数组中的新增元素
      switch (method) {
        case 'push':
        case 'unshift':
          inserted = args;
          break
        case 'splice':
          inserted = args.slice(2);
          break
      }
      //侦听新增元素的变化
      if (inserted) { ob.observeArray(inserted); }
      // notify change
      ob.dep.notify();//重点
      return result
    });
  });

4.监听数组中元素的变化

var Observer = function Observer (value) {
    if (Array.isArray(value)) {
      this.observeArray(value);
    } else {
      this.walk(value);
    }
  };
  //侦听每一项
  Observer.prototype.observeArray = function observeArray (items) {
    for (var i = 0, l = items.length; i < l; i++) {
      observe(items[i]);
    }
  };

5.因为Array的变化侦测是通过拦截原型的方式实现的,对于this.lsit[0]=2或this.list.length=0;是无法拦截侦测的。

总结:

  • Array和Object都是在getter触发收集依赖的
  • Object通过Observer(创建实例)递归所有数据的侦测,当从外界读取数据,在watcher读取数据,触发getter,将依赖(watcher)收集到Dep中,当数据发生变化时,会触发setter,从而向Dep中的watcher依赖发送通知,watcher接收到通知之后,通知外界视图或者回调函数
  • Array通过Observer(创建实例)递归所有数据并创建拦截器覆盖原型的方式进行侦测,在Obserer方法中给value(侦测得数组)新增一个不可枚举的__ob__属性,并且该属性的值就是Observer实例;在getter中将依赖收集在Observer实例中的Dep去,当数据发生变化时,通过this.__ob__.dep访问Observer实例的dep去向依赖发送通知。
查看原文

赞 0 收藏 0 评论 0

敏小静 发布了文章 · 2020-08-05

js之防抖和节流

赞 0 收藏 0 评论 0

敏小静 发布了文章 · 2020-08-01

前端常见跨域解决方案(大方向)

一、iframe标签、JSONP(动态创建script标签)
二、代理:nginx(服务器反向代理)、前端proxy代理(node+webpack+proxy、node+express+proxy)
三、CORS策略(跨域资源共享)

普通跨域请求:只需服务端设置Access-control-Allow-Origin;若要带上Cookie,前后端都需要设置。前端设置xhr.withCredentials = true;
CORS思想是使用自定义的HTTP头部让浏览器和服务器进行沟通,从而请求的成功和失败

四、Web sockets全双工,双向通信(同源策略对web sockets不适用);同时只有支持web sockets协议的服务器才能正常工作。

查看原文

赞 0 收藏 0 评论 0

敏小静 发布了文章 · 2020-07-28

下载文件功能

用a标签的download下载,如果是第三方资源的话,就需要请求回来,否则的话,就会被当做链接打开。还要看看后端有没有CORS策略阻止。

//判断是不是ie浏览器
   IEVersion() {
      let userAgent = navigator.userAgent; //取得浏览器的userAgent字符串  
      let isIE = userAgent.indexOf("compatible") > -1 && userAgent.indexOf("MSIE") > -1; //判断是否IE<11浏览器  
      let isEdge = userAgent.indexOf("Edge") > -1 && !isIE; //判断是否IE的Edge浏览器  
      let isIE11 = userAgent.indexOf('Trident') > -1 && userAgent.indexOf("rv:11.0") > -1;
      if(isIE || isEdge || isIE11) {
        return true
      }
    },
 download(){
      const fileName = 'ITSell.jpeg' // 导出文件名
      // 对于<a>标签,只有 Firefox 和 Chrome(内核) 支持 download 属性
      // IE10以上支持blob但是依然不支持download
     
        const blob = new Blob([content]) // 构造一个blob对象来处理数据。content是请求返回的blob数据;请求的时候,要加上responseType = 'blob'让服务器返回blob类型
      if ('download' in document.createElement('a') && !IEVersion()) { // 支持a标签download的浏览器
        const link = document.createElement('a') // 创建a标签
        link.download = fileName // a标签添加属性
        link.style.display = 'none'
        link.href =URL.createObjectURL(blob);
        document.body.appendChild(link)
        link.click() // 执行下载
        URL.revokeObjectURL(link.href) // 释放url
        document.body.removeChild(link) // 释放标签
      } else { // 其他浏览器
        navigator.msSaveBlob(blob, fileName);
      }
    },
查看原文

赞 0 收藏 0 评论 0

敏小静 收藏了文章 · 2020-07-17

ElementUI select 把整个option(对象)作为值

大部分时候我们使用select,选中选项我们只需要他的ID值,如果同时要在其他地方展示label或者获取选中对象中的其他值怎么办。看图。
关键设置有两处。

1 option中 :value="item"

2 selection中 value-key="id"

clipboard.png

查看原文

敏小静 发布了文章 · 2020-05-28

object对象和Array数组的变化检测(响应式原理)

变化检测顾名思义就是检测数据发生变化时,响应数据的更新。
它分为两种类型:一种是推,一种是拉;Angular和React的变化检测都属于’拉‘。就是说,当状态发生变化时,它不知道哪个状态发生变化,就发送一个信号给框架,框架使用暴力检测DOM来更新状态。这也是Angular脏检查的原理,React使用虚拟DOM的原理。
而Vue.js是使用’推‘的形式,就是一定程度上知道哪个状态发生了变化,从而进行更新。但是它的依赖相对来说也会比较多。

有两种方式可以检测到变化:Object.definePropertyES6的Proxy

Object.defineProperty(obj, prop, desc)
**Object.defineProperty()的作用就是直接在一个对象上定义一个新属性,或者修改一个已经存在的属性。**

obj 需要定义属性的当前对象
prop 当前需要定义的属性名
desc 属性描述符

如:let Person = {}
Object.defineProperty(Person, 'name', {
   value: 'jack',
   writable: true // 是否可以改变
   configurable:true
}) 

由于ES6在浏览器的支持度不是很理想,所以Vue.js(2.x.x)是用Object.defineProperty来实现的。

Vue在初始化实例时对属性执行了getter/setter的转化,所以属性必须在data()上才能被Vue转化成响应式。(深入响应式原理)

查看原文

赞 0 收藏 0 评论 0

敏小静 发布了文章 · 2020-05-21

使用require.context实现自动导入模块

1.什么是require.context

一个webpack的api,通过执行require.context函数获取一个特定的上下文,主要用来实现自动化导入模块,在前端工程中,如果遇到从一个文件夹引入很多模块的情况,可以使用这个api,它会遍历文件夹中的指定文件,然后自动导入,使得不需要每次显式的调用import导入模块
2.什么时候需要用到require.context
如果有以下情况,可以考虑使用require.context替换
image.png
image.png
在我的项目中,有n多个品牌,其中每个品牌对应自己条款细则,如果一个个的import导入路由的话,显然有点力不从心,看起来又很杂。这个时候我们只需用require.context遍历template文件夹把这些vue的路由整合到一个文件再导入。

3.分析require.context

require.context函数接受三个参数

  1. directory {String} -读取文件的路径
  2. useSubdirectories {Boolean} -是否遍历文件的子目录
  3. regExp {RegExp} -匹配文件的正则
语法: require.context(directory, useSubdirectories = false, regExp = /^.//);

借用webpakc官网的例子

require.context('./test', false, /.test.js$/);

上面的代码遍历当前目录下的test文件夹的所有.test.js结尾的文件,不遍历子目录
值得注意的是require.context函数执行后返回的是一个函数,并且这个函数有3个属性

  1. resolve {Function} -接受一个参数request,request为test文件夹下面匹配文件的相对路径,返回这个匹配文件相对于整个工程的相对路径
  2. keys {Function} -返回匹配成功模块的名字组成的数组
  3. id {String} -执行环境的id,返回的是一个字符串,主要用在module.hot.accept,应该是热加载?

这三个都是作为函数的属性(注意是作为函数的属性,函数也是对象,有对应的属性)

4.把template目录下的vue文件的路由整合到template.router.js

image.png
其中files(key).default返回的就是我们vue-component的结构;把confinRouter暴露出去,然后再到我们的主体路由引入这个template.router.js

image.png

原文参考:https://www.jianshu.com/p/c894ea00dfec

查看原文

赞 0 收藏 0 评论 0

认证与成就

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

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-08-01
个人主页被 733 人浏览