使用ES6新特性实现简单的MVVM(1)--数据驱动
尝试使用es6新特性,自己来实现一个mvvm及vue的各种特性。
相关代码放在github,会持续更新,欢迎赏个star。
本篇文章为系列文章的第一篇,会比较容易理解,后续会持续更新后面的记录。
文章首发于本人博客
最简单的watcher
从开始接触Vue开始,我们便对它的“数据响应”赞叹不绝,那么我们首先,来实现一个最简单的watcher,来监听数据,以进行对应的操作,类似后续会涉及的dom操作等。
Proxy
我们都知道,Vue使用Object.defineProperty来进行数据监听,监听obj的get和set方法。在ES6中,Proxy可以拦截某些操作的默认行为,也就是对目标对象的访问进行拦截,过滤和改写。我们可以利用这个特性来实现对数据的监听:
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 result = Reflect.set(target, prop, value) fn(value, oldValue) return result } }) }
结果:
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辅助函数:
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的各属性进行监听,当访问对应的节点时,我们创建并且为他添加各种属性等:
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。类似这样:
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) ) } })
因此,我们需要实现这样一个类,来处理我们的参数,并进行实例的初始化,监听,以及渲染控制等。
export default class W { constructor (config) {} /** * observe data */ _initData () {} /** * 渲染节点 */ _renderDom () {} }
初始化数据
首先,我们进行数据初始化,将数据置为observable,在对其修改的时候进行监听:
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)) }
在这里我们需要注意两点:
我们的data参数为一个function
这个原因在vue官方文档已经说过,当我们直接使用对象的时候,不同的实例间会共享同一个对象,导致出现对一个组件进行修改,另一个组件也进行修改的问题。具体可以查看data-必须是函数我们返回的是this._vm而不是this
我们这里做了两步操作,首先将this与data进行合并,再将整个对象进行监听,并赋值到_vm属性上。
这样,我们通过new W()初始化的实例,则可以访问到我们的data属性及方法,并且具有数据驱动的特性了。
更新DOM
我们已经为watcher的回调添加了dom更新的事件,我们只要在这里执行render函数,并挂载到对应的el上即可:
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可以访问到本实例的特性。我猜你已经想到了,我们可以使用bind,call和apply来实现它:
/** * 为所有的函数绑定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) } } }
测试
简单的架子拼接完成,我们来进行测试下我们的成果,我们需要实现两点功能:
可以按照我们的render函数正常挂载,并可访问到data上的数据
通过对实例进行修改,修改会自动更新到节点上
代码:
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
...等等
相关代码放在github,会持续更新,欢迎赏个star。
敬请期待!