前端框架对比及实现

BY GGICE

摘要

  • 特性对比
  • 实现原理对比
  • 社区及周边对比
  • 总结
  • 实现一个简单的类Mvc库

特性

Angular2

  • 组件化
  • 单项绑定
  • 服务端渲染
  • 支持Native
  • WebComponents
  • cli 工具
  • 支持局部CSS
  • 命令,依赖注入
  • 跨平台
  • typeScript

 

 

Vue2

  • 组件化
  • 单项绑定
  • 服务端渲染
  • 支持Native
  • Web Components
  • cli 工具
  • 支持局部CSS

 

 

 

React

  • 组件化
  • 单项绑定
  • 服务端渲染
  • 支持Native
  • WebComponents
  • cli
  • 可局部CSS


主要实现原理?

Vue2

Set, get 变化追踪

 


/**
 * core/observer/index.js
 * Define a reactive property on an Object.
 */
export function defineReactive (
 
) {
  ......
  Object.defineProperty(obj, key, {
    
    get: function reactiveGetter () {
      ......
      return value
    },
    set: function reactiveSetter (newVal) {
      ......
      dep.notify()
    }
  })
}


/**
 * core/observer/dep.js
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 */
export default class Dep {
  ......
  notify () {
    // stablize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}


/**
 * core/observer/watcher.js
 * 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.
 */
export default class Watcher {
  /**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
    
  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  get () {
    pushTarget(this)
    //接下来去触发reRander
    const value = this.getter.call(this.vm, this.vm)
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
      traverse(value)
    }
    popTarget()
    this.cleanupDeps()
    return value
  }

  /**
   * Scheduler job interface.
   * Will be called by the scheduler.
   */
  run () {
    if (this.active) {
      //执行get方法
      const value = this.get()
      if (
        value !== this.value ||
        // Deep watchers and watchers on Object/Arrays should fire even
        // when the value is the same, because the value may
        // have mutated.
        isObject(value) ||
        this.deep
      ) {
        // set new value
        const oldValue = this.value
        this.value = value
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            /* istanbul ignore else */
            if (config.errorHandler) {
              config.errorHandler.call(null, e, this.vm)
            } else {
              process.env.NODE_ENV !== 'production' && warn(
                `Error in watcher "${this.expression}"`,
                this.vm
              )
              throw e
            }
          }
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }
}


/**
 * core/observer/scheduler.js
 * Push a watcher into the watcher queue.
 * Jobs with duplicate IDs will be skipped unless it's
 * pushed when the queue is being flushed.
 */
export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(watcher)
    } else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      let i = queue.length - 1
      while (i >= 0 && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(Math.max(i, index) + 1, 0, watcher)
    }
    // queue the flush
    if (!waiting) {
      waiting = true
      nextTick(flushSchedulerQueue)
    }
  }
}

Vue做的优化

  • 将元素的attributes中不会变化的那部分提取出来,在对比两个v-node的时候,直接跳过这部分字段。
  • 将v-tree中纯静态的sub-tree提取出来,在对比两棵v-tree的时候,直接跳过这棵子树。

 

拓展

Angular 2

  • 事件拦截
  • 每个组件进行一次检查
  • 组件内脏检查

Zone.js 

// lib/node/events.ts
// 替换原生事件 
// For EventEmitter
const EE_ADD_LISTENER = 'addListener';
const EE_PREPEND_LISTENER = 'prependListener';
const EE_REMOVE_LISTENER = 'removeListener';
const EE_REMOVE_ALL_LISTENER = 'removeAllListeners';
const EE_LISTENERS = 'listeners';
const EE_ON = 'on';

const zoneAwareAddListener = callAndReturnFirstParam(
    makeZoneAwareAddListener(EE_ADD_LISTENER, EE_REMOVE_LISTENER, false, true, false));
const zoneAwarePrependListener = callAndReturnFirstParam(
    makeZoneAwareAddListener(EE_PREPEND_LISTENER, EE_REMOVE_LISTENER, false, true, true));
const zoneAwareRemoveListener =
    callAndReturnFirstParam(makeZoneAwareRemoveListener(EE_REMOVE_LISTENER, false));
const zoneAwareRemoveAllListeners =
    callAndReturnFirstParam(makeZoneAwareRemoveAllListeners(EE_REMOVE_ALL_LISTENER, false));
const zoneAwareListeners = makeZoneAwareListeners(EE_LISTENERS);

export function patchEventEmitterMethods(obj: any): boolean {
  if (obj && obj.addListener) {
    patchMethod(obj, EE_ADD_LISTENER, () => zoneAwareAddListener);
    patchMethod(obj, EE_PREPEND_LISTENER, () => zoneAwarePrependListener);
    patchMethod(obj, EE_REMOVE_LISTENER, () => zoneAwareRemoveListener);
    patchMethod(obj, EE_REMOVE_ALL_LISTENER, () => zoneAwareRemoveAllListeners);
    patchMethod(obj, EE_LISTENERS, () => zoneAwareListeners);
    obj[EE_ON] = obj[EE_ADD_LISTENER];
    return true;
  } else {
    return false;
  }
}

// 时间事件打补丁的例子
// lib/common/timers.ts
export function patchTimer(window: any, setName: string, cancelName: string, nameSuffix: string) {

  function scheduleTask(task: Task) {
    .....
    tasksByHandleId[data.handleId] = task;
    return task;
  }

  setNative =
      patchMethod(window, setName, (delegate: Function) => function(self: any, args: any[]) {
          .......
          return delegate.apply(window, args);
        }
      });
}

脏检查

//directive ng-click 的check
// modules/@angular/common/src/directives/ng_class.ts
   
  ngDoCheck(): void {
    if (this._iterableDiffer) {
      const changes = this._iterableDiffer.diff(this._rawClass);
      if (changes) {
        this._applyIterableChanges(changes);
      }
    } else if (this._keyValueDiffer) {
      const changes = this._keyValueDiffer.diff(this._rawClass);
      if (changes) {
        this._applyKeyValueChanges(changes);
      }
    }
  }

// 实际的对比
// modules/@angular/core/src/change_detection/differs/default_keyvalue_differ.ts
diff(map: Map<any, any>|{[k: string]: any}): any {
    if (!map) {
      map = new Map();
    } else if (!(map instanceof Map || isJsObject(map))) {
      throw new Error(`Error trying to diff '${map}'`);
    }

    return this.check(map) ? this : null;
  }

check() {
 .....
}

可优化(脏检查)

  • 使模型成为可观察者,避免不必要检查
  • Immutable data

拓展

React

  • Virtual Dom
  • Redux

Virtul Dom

  • Dom转化为Js对象,驻留内存
  • Diff
  • batching, batching把所有的DOM操作搜集起来,一次性提交给真实的DOM

Diff

  • 只对比同级元素,当发现不存在则删除,多则创建
  • 同级元素有不同的key区分

可优化

  • Immutable Data

性能对比

Vue vs Reat

Vue

渲染 10,000 个列表项 100 次(本地测试)

周边对比

Angular2

 

Watch: 2215

Star: 19442

Fork: 5039

 

大小:565K(2.0.0)

Vue2

 

Watch: 2242

Star: 38589

Fork: 4723

 

大小: 70.4kb (2.1.8)

React

 

Watch: 4006

Star: 56952

Fork: 10276

 

大小: 152kb (15.3.8)

总结

Angular 2 性能良好,错误检查完善,可靠性强,依然庞大、复杂,有一定学习成本

Vue 2 性能良好,学习成本较低,API数量少,体积小,整体较为轻量。错误检查、可靠性相对弱

React 性能良好,简化思维模式,编码逻辑,可维护性强, 错误检查完善,有一定的学习成本(小于Angular2)

实现一个前端界面库

基于Web Components

  • Custom Elements
  • HTML Templates
  • Shadwo DOM
  • HTML Imports

Composability (可组合)

Encapsulation (封装)

 Reusability (可复用)

Bird.js

  • attribute change变化检查
  • Diff Shadow DOM
  • 天然的局部CSS

Component 实现

//一个基本的component 
//bird.js

const { console } = window

class Base extends HTMLElement {
  constructor() {
    super()
  }

  createdCallback() {
    this._init()
    this._rander()
  }

  attachedCallback() {
    const { attached } = this
    
    attached && attached.apply(this)
  }
  
  detachedCallback() {
    const { removed } = this

    removed && removed.apply(this)
  }

  attributeChangedCallback(name, oldVal, newVal) {
    const { attributeChanged } = this
    this._reRander(name)
    attributeChanged && attributeChanged.apply(this, name, oldVal, newVal)
  }

  _init() {
    const options = this.getOptions()
    const { template , data, created } = options

    this._tempShadow = document.createElement('div').createShadowRoot()
    this._shadow = this.createShadowRoot()
    if(!template) {
      this.template = null
      console.warn('No template!')
    } else {
      this.template = template
    }
    if(!data) {
      this.data = null
      console.warn('No data!')
    } else {
      this.data = data
    }

    this._bind()
    created && created.apply(this)
  }

  _applyDataToAttr(data) {
    for(var key in data) {
      this.setAttribute(key, JSON.stringify(data[key]))
    }
  }

  /**
   * 绑定data到Attribute
   */
  _bind() {
    const { data } = this
    this._applyDataToAttr(data)
  }

  /**
   * 操作data的方法
   */
  setData(data) {
    this.data = Object.assign(this.data, data)
    this._applyDataToAttr(data)
  }

  /**
   * 模板引擎
   * TODO 需要优化匹配
   */
  _parse() {
    const { template, data } = this
    var html = template.replace(/\s*/g, '')
    ......
    try {
      html = eval(html)
    } catch(e) {
      window.console.warn(e)
    }
    if(_styles) {
      html = '<style>' + _styles + '</style>' + html
    }
    return html
  }

  _render() {
    this._shadow.innerHTML = this._parse()
    this._bindEvents()
  }

  _reRender() {
    this._tempShadow.innerHTML = this._parse()
    this._diff(this._tempShadow, this._shadow)
  }

  /**
   *  需要支持增删改
   *  1.改  done
   *  2.增
   *  3.删
   */
  _diff(newDom, oldDom) {
    if(newDom.innerHTML === oldDom.innerHTML) {
      return console.log('diff Same!')
    }
    [].forEach.call(newDom.childNodes, (el, index) => {
      if(el.innerHTML !== oldDom.childNodes[index].innerHTML) {
        if(el.childNodes.length > 1) {
          this._diff(el, oldDom.childNodes[index])
        } else {
          oldDom.childNodes[index].innerHTML = el.innerHTML
        }
      }
    })
  }

  /**
   * 绑定事件的方法,需要在render之后执行
   */
  _bindEvents() {
    var els = this._shadow

    this._buildChildEvents(els)
  }

  _buildChildEvents(fEl) {
    [].forEach.call(fEl.childNodes, (el) => {
      if(el.attributes && el.attributes.length > 0) {
        [].forEach.call(el.attributes, (attr) => {
          var funName = attr.value.match(/function\[(\w+)\]/)
          var eventName, funText

          if(!funName || !funName[1]) {
            return
          }
          funText = this.data[funName[1]]
          eventName = attr.name.replace('on-', '').toLowerCase()
          if(funText) {
            el.addEventListener(eventName, funText.bind(this))
          }
        })  
      }
      this._buildChildEvents(el)
    })
  }

}

export default Base
// 调试程序
// debug.js

var App = new Bird({
  template: `
    <p>Text: </p>
    <hello-text>1</hello-text>
    <p>List: </p>
    <user-list>2</user-list>
    <p>Input: </p>
    <text-input></text-input>
  `,
  el: '#app'
})

App.component('hello-text', {
  template: `<div>{text}</div>`,
  styles: `
    div {
      color: red;
      font-size: 17px;
    }
  `,
  created() {
    var that = this
    setTimeout(function(){
      that.setData({
        text: '我发生变化了 haha!'
      })
    }, 2000)
  },
  attached() {

  },
  removed() {

  },
  attributeChanged(name, oldVal, newVal) {

  },
  data: {
    text: 'Hello Bird.js',
    test: 'test'
  }
})

App.component('user-list', {
  template: `
  <div>
    {users.map(user => "
      <p>我叫{user.name}, 年龄{user.age}</p>
    ").join('')}
  <div>
  `,
  data: {
    users: [{
      name: 'test',
      age: 20
    },
    {
      name: 'test3',
      age: 21
    }]
  }
})

App.component('text-input', {
  template: `<div>
              <input on-keyup={inputChange}>
              <div>{result}</div>
            </div>`,

  data: {
    inputChange(e) {
      this.setData({
        result: e.target.value
      })
    }
  }
})

Demo[bird.js]

谢谢!

Made with Slides.com