前端框架对比及实现
BY GGICE
摘要
- 特性对比
- 实现原理对比
- 社区及周边对比
- 总结
- 实现一个简单的类Mvc库
特性
Angular2
- 组件化
- 单项绑定
- 服务端渲染
- 支持Native
- WebComponents
- cli 工具
- 支持局部CSS
- 命令,依赖注入
- 跨平台
- typeScript
Vue2
- 组件化
- 单项绑定
- 服务端渲染
- 支持Native
- Web Components
- cli 工具
- 支持局部CSS
React
- 组件化
- 单项绑定
- 服务端渲染
- 支持Native
- WebComponents
- cli
- 可局部CSS
主要实现原理?
Vue2
- 变化追踪 Object.defineProperty
- VirtualDom snabbdom
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
渲染 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]
谢谢!
前端框架对比与实现
By weiwei ggice
前端框架对比与实现
- 1,721