将DOM节点转换为图片

如何将DOM转换为图片,这个问题可能有点大,一时想不到答案,但我们把问题缩小一下,转换为SVG你可能就想起来了。
SVG能直接嵌入到HTML中,那反过来应该也行。回忆一下,SVG和DOM是不是长得很像?因为他们都是XML的一种方言,所以我们把DOM塞到SVG里面就行了。但是,问题来了,XHTML和SVG都有一个<title>标签,SVG怎么区分他们?其实这就好比变量之于命名空间,XML有各种方言,所以也有命名空间开区分它们。

<svg xmlns="http://www.w3.org/2000/svg">

SVG的开头一般长这个样子,这个xmlns就是xml name space的缩写。http://www.w3.org/2000/svg就是SVG的命名空间。
HTML的命名空间是<html xmlns="http://www.w3.org/1999/xhtml">

在SVG中引入其他命名空间要放在foreignObject标签内。

最后我们可以先将SVG转换为canvas,再通过canvastoDataURL方法来将其转换为对应格式的bash64编码。

如果你要转换DOM节点的比较复杂,推荐使用dom-to-image库来实现。

Object.create() 与 new 的区别

我们先看一下二者具体做了些什么:

new Test():

  • create new Object() obj
  • set obj.__proto__ to Test.prototype
  • return Test.call(obj) || obj;

最后一步如果构造函数未返回对象,则将该新对象返回,即this,如返回了对象,则替换this。

Object.create( Test.prototype )

  • create new Object() obj
  • set obj.__proto__ to Test.prototype
  • return obj;

所以说二者区别:
new X= Object.create( X.prototype )+ X.constructor()

看下面一个例子:

function Dog1(name){
   return {dogName: name};
}

function Dog2(name){}

Dog1.prototype.setName = Dog2.prototype.setName = function(name){
    this.name = name;
};

let pet1 = new Dog1('rocky'); 
console.log(pet1.dogName);           //rocky  
pet1.setName('skye');                //error

pet1 = new Dog2();
pet1.setName('skye');
console.log(pet1.name);              //skye            

let pet2 = Object.create(Dog1.prototype);
pet2.setName('skye');                //ok
console.log(pet2.name);              //skye
console.log(pet2.dogName);           //undefined

Dog1方法返回一个对象,所以替换了this,而我们使用Dog2就能访问原型上的方法了。

参考文章:
Understanding the difference between Object.create() and new SomeFunction()

JS 实现位图(Bitset)数据结构

位图数组中每个元素在内存中占用1位,所以可以节省存储空间,在索引,数据压缩等方面有广泛应用。
我们使用类型化数组Uint8Array储存位图,数组中每一个元素占8个字节所以最大值是255。关键是寻找偏移量,index>>3 右移三位相当于除8。可以找到对应的数组元素,1 << (index % 8) 找到位对应数组元素的哪一个字节。最后通过按位判断该为1或0。

判断存在时:使用按位与&(只有两个操作数相应的比特位都是1时,结果才为1)
修改某项:使用按位或|

Typescript代码如下:

export class Bitmap {
    private bin: Uint8Array;

    constructor(size: number) {
        this.bin = new Uint8Array(new ArrayBuffer((size >> 3) + 1));
    }

    public get(index: number): boolean {
        const row = index >> 3;
        const bit = 1 << (index % 8);
        return (this.bin[row] & bit) > 0;
    }

    public set(index: number, bool: boolean = true) {
        const row = index >> 3;
        const bit = 1 << (index % 8);
        if (bool) {
            this.bin[row] |= bit;
        } else {
            this.bin[row] &= (255 ^ bit);
        }
    };

    public fill(num: number = 0) {
        this.bin.fill(num);
    }

    public flip(index: number) {
        const row = Math.floor(index / 8);
        const bit = 1 << (index % 8);
        this.bin[row] ^= bit;
    }
}

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 实现依赖注入