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

1,379 阅读4分钟
原文链接: segmentfault.com

尝试使用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 return = 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))
}

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

  1. 我们的data参数为一个function
    这个原因在vue官方文档已经说过,当我们直接使用对象的时候,不同的实例间会共享同一个对象,导致出现对一个组件进行修改,另一个组件也进行修改的问题。具体可以查看data-必须是函数
  2. 我们返回的是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)
    }
  }
}

测试

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

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

代码:

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。

敬请期待!