Try using the new ES6 features to implement an MVVM and vUE features yourself. The relevant code is posted on Github and will be updated continuously. Welcome to star. This article is the first in a series that will be easier to understand and will continue to be updated. The article was first published in my blog

The simplest watcher

Since we started working with Vue, we have been impressed by its “data responsiveness”. So let’s first implement a simple Watcher to listen to data for corresponding operations, like the DOM operations that will be involved later.

Proxy

As we all know, Vue uses Object.defineProperty for data listening, listening for OBJ’s GET and set methods. In ES6, proxies can intercept the default behavior of certain operations, that is, intercepting, filtering, and rewriting access to target objects. We can use this feature to listen on data:

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
    }
  })
}Copy the code

Results:

let obj = watcher({ a: 1 }, (val, oldVal) => {
  console.log('old =>> ', oldVal)
  console.log('new =>> ', val)
})

obj.a = 2
// old =>> , 1
// new =>> , 2Copy the code

Simple Dom manipulation

Now that we can listen for simple data operations (although there are problems), let’s just listen to the DOM and do the data operations. Instead of parsing templates, we can implement a DOM helper function using Proxy:

const dom = new Proxy({}, { get (target, tagName) { return (attrs = {}, ... Childrens) => {const elem = document.createElement(tagName) // Add attribute attrs.forEach(attr => Elem. AddAttribute (attr, attrs[attr]) // Add child element childrens. ForEach (child => {const child = typeof Child === 'string'? document.createTextNode(child) : child elem.appendChild(child) }) return elem } } })Copy the code

That is, we listen for attributes of the DOM, and when accessing the corresponding node, we create and add attributes to it:

dom.div( {class: 'wrap'}, 'helloworld', dom.a({ href: 'https://www.360.cn' }, <div class="wrap"> helloWorld <a href="https://www.360.cn"> Welcome to 360</a> </div>Copy the code

Spliced foundation frame

We’ll call our little shelf ‘W’ here to make it actually work. Similar to Vue syntax, we need to watch our data and update the DOM during instantiation. Something like this:

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)
    )
  }
})Copy the code

Therefore, we need to implement a class that handles our arguments, initializes instances, listens, and controls rendering.

Export default class W {constructor (config) {} /** * observe data */ _initData () {} /** * render dom () {}}Copy the code

Initialize data

First of all, we initialize the data, the data set of observables, at the time of the modified monitor:

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))
}Copy the code

Two things to note here:

  1. The reason that our data parameter is a function has been mentioned in the official vUE document. When we directly use objects, different instances will share the same object, resulting in the problem of modifying one component and the other component. Look at data- it has to be a function
  2. We return this._vm instead of this. We do two things here, first merge this with data, then listen on the entire object and assign it to the _VM property.

Thus, instances we initialize with new W() have access to our data properties and methods, and are data-driven.

Update the DOM

We’ve added the DOM update event to the Watcher callback, so we just call the render function here and mount it to the corresponding EL:

const { render, el } = this._config
const targetEl = document.querySelector(el)
const renderDom = render()

targetEl.innerHTML = ''
targetEl.appendChild(renderDom)Copy the code

Binding this

We’ll notice that we use this. MSG in the render function of config to access the MSG attribute of data, so we need to implement this as a way to access this instance in each component. As I guess you already know, we can use bind,call, and apply to do this:

/** * Bind 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) } } }Copy the code

test

After simple shelf splicing is completed, let’s test our results. We need to achieve two functions:

  1. We can mount it as normal with our render function and access the data on data
  2. By making changes to the instance, the changes are automatically updated to the node

Code:

const vm = new W({ el: 'body', data () { return { msg: 'hello world' } }, render () { return dom.div({ class: 'wrap' }, dom.a({ href: 'http://www.360.cn'}, enclosing MSG))}}) / / test modify vm setInterval (_ = > {vm. MSG = 'hello world = > > > "+ new Date ()}, 1000).Copy the code

Results:

The most basic functionality has been implemented!

conclusion

We have only realized the most simple data-driven function this time, and there are still a lot of processing to be done in the future. We will also sort out and implement one by one. You can continue to pay attention to it, for example:

  • Array change monitor
  • Object deep listening
  • Update the queue
  • The render process records only related attributes
  • Template rendering
  • v-model
  • . , etc.

The relevant code is posted on Github and will be updated continuously. Welcome to star.

Stay tuned!