使用ES6新特性实现简单的MVVM(1)--数据驱动

尝试使用es6新特性,自己来实现一个mvvm及vue的各种特性。
相关代码放在github,会持续更新,欢迎赏个star。
本篇文章为系列文章的第一篇,会比较容易理解,后续会持续更新后面的记录。

最简单的watcher

从开始接触Vue开始,我们便对它的“数据响应”赞叹不绝,那么我们首先,来实现一个最简单的watcher,来监听数据,以进行对应的操作,类似后续会涉及的dom操作等。

Proxy

我们都知道,Vue使用Object.defineProperty来进行数据监听,监听obj的get和set方法。在ES6中,Proxy可以拦截某些操作的默认行为,也就是对目标对象的访问进行拦截,过滤和改写。我们可以利用这个特性来实现对数据的监听:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const watcher = (obj, fn) => {
return new Proxy(obj, {
get (target, prop, receiver) {
return Reflect.get(target, prop, receiver)
},
set (target, prop, value) {
const oldValue = Reflect.get(target, prop, receiver)
const return = Reflect.set(target, prop, value)
fn(value, oldValue)
return result
}
})
}

结果:

1
2
3
4
5
6
7
8
let obj = watcher({ a: 1 }, (val, oldVal) => {
console.log('old =>> ', oldVal)
console.log('new =>> ', val)
})
obj.a = 2
// old =>> , 1
// new =>> , 2

简单的Dom操作

我们已经可以对简单的数据操作进行监听(虽然还有各种问题),接下来,我们只要在监听到dom后进行数据操作即可。解析模板什么的我们就先不做了,我们可以继续利用Proxy实现一个dom辅助函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const dom = new Proxy({}, {
get (target, tagName) {
return (attrs = {}, ...childrens) => {
// 创建节点
const elem = document.createElement(tagName)
// 添加attribute
attrs.forEach(attr => elem.addAttribute(attr, attrs[attr])
// 添加子元素
childrens.forEach(child => {
const child = typeof child === 'string'
? document.createTextNode(child)
: child
elem.appendChild(child)
})
return elem
}
}
})

也就是说,我们为dom的各属性进行监听,当访问对应的节点时,我们创建并且为他添加各种属性等:

1
2
3
4
5
6
7
8
9
10
11
12
13
dom.div(
{class: 'wrap'},
'helloworld',
dom.a({
href: 'https://www.360.cn'
}, 'welcome to 360')
)
// 输出
<div class="wrap">
helloworld
<a href="https://www.360.cn">welcome to 360</a>
</div>

拼接基础框架

我们在这里给我们的这个小架子起名为’W’,让它可以真正的运行起来。类似Vue的语法,我们需要在进行实例化的时候,watch我们的data,并且更新dom。类似这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const vm = new W({
el: 'body',
data () {
return {
msg: 'hello world'
}
},
render () {
return dom.div({
class: 'wrap'
},
dom.a({
href: 'http://www.360.cn'
}, this.msg)
)
}
})

因此,我们需要实现这样一个类,来处理我们的参数,并进行实例的初始化,监听,以及渲染控制等。

1
2
3
4
5
6
7
8
9
10
11
12
13
export default class W {
constructor (config) {}
/**
* observe data
*/
_initData () {}
/**
* 渲染节点
*/
_renderDom () {}
}

初始化数据

首先,我们进行数据初始化,将数据置为observable,在对其修改的时候进行监听:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import watcher from './data.js'
class W {
constructor (config) {
const { data = () => {} } = config
this._config = config
this._initData(data)
return this._vm
}
}
_initData (data) {
this._vm = watcher(Object.assign({}, this, data()), this._renderDom.bind(this))
}

在这里我们需要注意两点:

  1. 我们的data参数为一个function
    这个原因在vue官方文档已经说过,当我们直接使用对象的时候,不同的实例间会共享同一个对象,导致出现对一个组件进行修改,另一个组件也进行修改的问题。

具体可以查看data-必须是函数

  1. 我们返回的是this._vm而不是this
    我们这里做了两步操作,首先将this与data进行合并,再将整个对象进行监听,并赋值到_vm属性上。

这样,我们通过new W()初始化的实例,则可以访问到我们的data属性及方法,并且具有数据驱动的特性了。

更新DOM

我们已经为watcher的回调添加了dom更新的事件,我们只要在这里执行render函数,并挂载到对应的el上即可:

1
2
3
4
5
6
const { render, el } = this._config
const targetEl = document.querySelector(el)
const renderDom = render()
targetEl.innerHTML = ''
targetEl.appendChild(renderDom)

绑定this

我们会发现,我们在config的render函数中,使用了this.msg来访问data的msg属性,因此我们需要实现在各组件中,通过this可以访问到本实例的特性。我猜你已经想到了,我们可以使用bing,call和apply来实现它:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 为所有的函数绑定this
*/
bindVM () {
const { _config } = this
for(let key of Object.keys(_config)) {
const val = _config[key]
if (typeof(val) === 'function') {
_config[key] = val.bind(this._vm)
}
}
}

测试

简单的架子拼接完成,我们来进行测试下我们的成果,我们需要实现两点功能:

  1. 可以按照我们的render函数正常挂载,并可访问到data上的数据
  2. 通过对实例进行修改,修改会自动更新到节点上

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const vm = new W({
el: 'body',
data () {
return {
msg: 'hello world'
}
},
render () {
return dom.div({
class: 'wrap'
},
dom.a({
href: 'http://www.360.cn'
}, this.msg)
)
}
})
// 测试修改vm
setInterval(_ => {
vm.msg = 'hello world =>>>' + new Date()
}, 1000)

结果:

结果

最基本的功能已经实现啦!

结语

本次我们只实现了最最最简单的数据驱动功能,后续还有很多需要进行处理,我们也会对其一一进行梳理和实现,大家可以持续关注下,例如:

  • 数组变动监听
  • object深度监听
  • 更新队列
  • render过程中记录仅相关的属性
  • 模板渲染
  • v-model
  • …等等

敬请期待!