海屿文章列表

JS 监听对象变化的几种方式

一、使用Object.defineProperties

Object.defineProperty 实现对对象的监听,我们是在对象上设置getter/setter,所以也就仅限于监听对象属性的修改。

var obj = {
  foo: 1,
  bar: 2
};
var obj = Object.defineProperties({}, {
    "foo":{
        get:function(){
            console.log("Get:"+this.value);
        },
        set:function(val){
            console.log("Set:"+val);
            this.value = val;
        }
    },

    "bar":{         
        get:function(){
            console.log("Get:"+this.value);
        },
        set:function(val){
            console.log("Set:"+val);
            this.value = val;
        }
    }
 });

Vue就用这种方式实现的双向绑定。

当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。

但这种方式也有他的问题:

  • 用索引直接设置一个数组的项不会触发变更检测
  • 对象属性的增删
Vue.set(vm.items, indexOfItem, newValue)
Vue.set(object, key, value)

二、Proxy

ES6代理实现数据绑定,实际上我们依然是使用 getter/setter 只不过我们没有在对象上设置,而是代理了 getter/setter 的行为。
Proxy 一般与 Reflect 一起使用。Reflect 对象的方法与Proxy对象的方法一一对应。Reflect上获取默认行为,Proxy修改默认行为,Reflect一般作为的Proxy的基础。

var proxied = new Proxy(obj, {
  get: function(target, prop) {
    console.log({ type: 'get', target, prop });
    return Reflect.get(target, prop);
  },
  set: function(target, prop, value) {
    console.log({ type: 'set', target, prop, value });
    return Reflect.set(target, prop, value);
  }
});

nx-observe使用代理机制实现了一个完整的数据绑定解决方案。
注意:Proxy不是语法糖,所以bable无法将其转换成ES5。

三、Observable

理解观察者模式
Observable是一个接受观察者并返回函数的函数。

   class Observable {
        constructor(subscribe) {
            this._subscribe = subscribe;
        }

        subscribe(observer) {
            return this._subscribe(observer);
        }
    }

观察者observer:一个带有nexterrorcomplete方法的对象。

const observer = {
  next: x => console.log(x),
  error: err => console.error(err),
  complete: () => console.log('done'),
}

Promise转化为Observable

static fromPromise(promise) {
  return new Observable(observer => {
    promise.then(val => {
      observer.next(val); observer.complete();
    })
    .catch(e => {
      observer.error(val); observer.complete();
    });
  })
}

测试:

const promise1 = new Promise((res, rej) => {
    setTimeout(() => {
        res(1)
    }, 1000)
})

Observable.fromPromise(promise1).subscribe(observer);

使用RxJS5时,可以使用BehaviorSubject监听对象。

  • BehaviorSubject 可以給予初始值
  • 每一个 Observer 都可以在注册在当下,立刻取得目前 BehavoirSubject 的值(规observable仅在收到onnext时触发)

参考文章:
BehaviorSubject vs Observable?

JS中依赖注入的几种实现方式

Di.jpg

依赖注入与控制反转

  • 控制反转(Inversion of Control)是一种思想
  • 依赖注入(Dependency injection)是一种设计模式

依赖注入的实现

其实依赖注入原理很简单,就两点:

  1. 实现一个服务池
  2. 解析依赖项,并将对应的服务传入需要它的函数

服务池简单,可以直接用对象模拟。

   class Injector {
        constructor() {
            this.dependencies = {};
        }

        register(key, value) {
            this.dependencies[key] = value;
        }
    }

   const service = function () {
        return {name: 'Service'};
    }
    const router = function () {
        return {name: 'Router'};
    }

    const injector = new Injector();

    injector.register('service', service)
    injector.register('router', router)

关键是解析函数的依赖,不同的库有不同的方式,但原理大同小异。

直接声明依赖

先描述依赖,然后再写函数就是RequireJS / AMD的思想。
这种方法让解析函数的依赖非常简单。

    class Injector {
        constructor() {
            this.dependencies = {};
        }

        register(key, value) {
            this.dependencies[key] = value;
        }
        resolve(deps, func, scope = {}) {
            for (const d of deps) {
                if (this.dependencies[d]) {
                    scope[d] = this.dependencies[d];
                } else {
                    throw new Error('Can\'t resolve ' + d);
                }
            }

            return function () {
                func.apply(scope, Array.from(arguments))
            };
        }
    }

结果:

    var doSomething = injector.resolve(['service', 'router'], other => {
        console.log(this.service().name) //Service
        console.log(this.router().name)  //Router
        console.log(other)               //Other
    });
    doSomething("Other");

这种方式还需要我们先描述依赖,有没有一种方法能让注入器自动识别要注入那种依赖?

正则

这种方法就是Angular.js获取依赖的方法的方法。

app.controller('MainCtrl', function (service, router) {
    // do something
});

Angular.js通过这段正则/^function\s*[^\(]*\(\s*([^\)]*)\)/m匹配参数,获得函数的依赖项。
我们只需要简单修改一下resolve函数。

resolve(func, scope = {}) {
    const deps = func.toString().match(/^function\s*[^\(]*\(\s*([^\)]*)\)/m)[1].replace(/ /g, '').split(',');
    return function () {
        const args = Array.from(arguments);
        deps.forEach((e, i) => {
            if (this.dependencies[e]) args.splice(i, 0, this.dependencies[e]);
        });
        func.apply(scope, args);
    }.bind(this)
}

结果:

    var doSomething = injector.resolve(function (service, router, other) {
        console.log(service().name)
        console.log(router().name)
        console.log(other)
    });

    doSomething("Other");

但这种方式压缩代码会改变参数名,从而找不到依赖,Angular.js采用如下方式避免这个问题:

app.controller('MainCtrl', ['service', 'router', function (service, router, other) { }]);

这种方式其实和第一种差不多,具体实现就不再赘述。

reflect-metadata

Typescript中利用reflect-metadata库,通过反射机制,获取参数类型列表,来实现依赖注入。

import "reflect-metadata";

const classPool: Array<Function> = [];

// 标记可被注入类
export function injectable(constructor: Function) {
    let paramsTypes: Array<Function> = Reflect.getMetadata('design:paramtypes', constructor);
    if (classPool.indexOf(constructor) !== -1) return;
    if (paramsTypes.length) {
        paramsTypes.forEach(v => {
            if (v === constructor) {
                throw new Error('不可以依赖自身');
            } else if (classPool.indexOf(v) === -1) {
                throw new Error(`参数[${(v as any).name}]不可被注入`);
            }
        });
    }
    classPool.push(constructor);
}


export function create<T>(constructor: { new(...args: Array<any>): T }): T {
    let paramsTypes: Array<Function> = Reflect.getMetadata('design:paramtypes', constructor);
    let paramInstances = paramsTypes.map(v => {
        if (classPool.indexOf(v) === -1) throw new Error(`参数[${(v as any).name}]不可被注入`);
        return v.length ? create(v as any) : new (v as any)();  // 参数有依赖项则进行递归
    });
    return new constructor(...paramInstances);
}

参考文章:
https://modernweb.com/dependency-injection-in-javascript/
TypeScript 实现依赖注入

删除DOM节点后该节点的事件处理程序还在吗?

一个DOM被删除并不会直接释放对应的event listener,如果删除的DOM元素是无引用的(没有指向它的引用),那么元素本身以及与之关联的任何事件处理程序/侦听器会被由垃圾收集器回收。

var a = document.createElement('div');
var b = document.createElement('p');
// Add event listeners to b etc...
a.appendChild(b);
a.removeChild(b);
b = null; 
// A reference to 'b' no longer exists 
// Therefore the element and any event listeners attached to it are removed.

但如果存在仍指向该元素的引用,则该元素及其事件侦听器将保留在内存中。

var a = document.createElement('div');
var b = document.createElement('p'); 
// Add event listeners to b etc...
a.appendChild(b);
a.removeChild(b); 
// A reference to 'b' still exists 
// Therefore the element and any associated event listeners are still retained.

编译 php 提示 cURL 找不到

问题:
确定机器已经安装curl,而且通过--with-curl=指定路径也不管用。错误信息如下:

checking for cURL 7.10.5 or greater... configure: error: cURL version 7.10.5 or later is required to compile php with cURL support

解决方法:
这些都不允许您在启用cURL的情况下编译PHP。

要使用cURL进行编译,需要libcurl头文件(.h文件)。 它们通常位于/usr/include/curl中。 它们通常捆绑在一个单独的开发包中。

例如,在Ubuntu中安装libcurl

sudo apt-get install libcurl4-gnutls-dev
或CentOS:

sudo yum install curl-devel
然后你可以这样做:

./configure --with-curl #其他选项......
如果手动编译cURL,则可以指定不带lib或include后缀的文件的路径。 (例如:/usr/local如果cURL标题位于/usr/local/include/curl中)。

原文地址:Compiling php with curl, where is curl installed?

Promise 版本的数组遍历

我们想要对数组中的值分别进行异步操作,但是数组对象的遍历方法只支持同步方法,如果传入异步函数后其结果往往超出预期,
所以我们就要自己来实现这些遍历方法的异步版本。

这里指的遍历方法包括:map、reduce、reduceRight、forEach、filter、some、every。
我们的需求有三种:串行,并行,当其中一个Promise完成或拒绝后退出。

这里我们以一个简单的异步函数为例:

let multi = num => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (num) {
        resolve(num ** 2)
      } else {
        reject(new Error('num not specified'))
      }
    }, 1000)
  })
}

map

[1, 2, 3].map(async item => multi(item))
// > [Promise, Promise, Promise]

因为map能够返回Promise数组,所以我们用Promise.all将返回的数组包起来就可以了。

await Promise.all([1, 2, 3].map(multi(item))
// > [1, 4, 9]

forEach

直接使用forEach会并行执行,如果需要串行的话就需要我们自己来实现一个新方法。

Array.prototype.forEachSync = async function (callback, thisArg) {
  for (let [index, item] of Object.entries(this)) {
    await callback(item, index, this)
}

some 和 every

some在匹配到第一个true之后就会终止遍历,every反之。

Array.prototype.someSync = async function (callback, thisArg) {
  for (let [index, item] of Object.entries(this)) {
    if (await callback(item, index, this)) return true
  }
  return false
}
await [1, 2, 3].someSync(async item => multi(item))
// > true

Reduce

Promise.all 虽然能并发多个请求,但它有一个特点:错误优先。一旦出现一个reject,那么Promise.all也会reject。但大多数情况下,如果某一个请求出现问题,我们希望他不要影响接下来的请求。

[1, 2, 3].map(async item => multi(item)).reduce((task, item) => {
  return task.then(() => item);
}, Promise.resolve())

这里执行map之后全部异步请求已经发起了,我们只需要顺序处理其返回的Promise数组。task的初始值为Promise.resolve(),它相当于一个中间变量,储存当前的 promise

控制并发数

我们实现了顺序请求,还有并发请求,但是着两种方案都是两种极端,考虑一下如果我们需要发出成百上千个请求,顺序执行时间太长,并发执行对机器压力太大,所以我们需要一种折中方案,控制并发请求。

worker函数每次取出tasks中的第一个值,然后执行异步函数,完成后递归执行。默认我们有三个worker,每个worker完成后都会从目前tasks再取出一个继续执行,直到tasks为空。

function mockAPI(result, time = 1000) {
    time = time * result;
    return new Promise((resolve, reject) => {
        setTimeout(() => {
             console.log(result, time);
            resolve(result);
        }, time);
    });
}

function loadLimit(tasks, promiseFunction, MAX_CURRENCY = 3) {
    const promises = result = [];

    function worker(tasks) {
        const task = tasks.shift();
        if (!task) return;
        return promiseFunction(task).then(res => {
            result.push(res);
            return worker(tasks);
        });
    }

    for (let i = 0; i < MAX_CURRENCY; i += 1) {
        promises.push(worker(tasks));
    }

    return result;
}

loadLimit([5, 2, 3, 1, 2, 3, 1, 2, 2, 3, 3], mockAPI)