Use ES6′ s new Proxy to implement a data binding instance

  es6, javascript, vue.js

Project address:https://github.com/jrainlau/mog
Online experience:http://codepen.io/jrainlau/pe …


As a front-end developer, I have stepped on too many “data binding” pits. Earlier, they all passedjQueryTools such as these manually complete these functions, but when the amount of data is very large, these manual work makes me very painful. Until it is usedVueJS, the pain was brought to an end.

VueJSOne of its selling points is “data binding.” Users don’t need to care how data is bound to dom, they just need to pay attention to the data, becauseVueJSHas automatically helped us to complete these tasks.

This is really amazing. I fell hopelessly in love with itVueJSAnd use it in my own project. With the deepening of its application, I want to know more about its principle.

VueJSHow is data binding done?

After reading the official documents, I saw the following passage:

Pass an ordinary Javascript object to Vue instance as its data option, Vue will traverse its properties and use Object.defineProperty to convert them to getter/setter.

The key words areObject.definPropertyInMDN documentThe inside says so:

Object.defineProperty()The method directly defines the attribute of an object or modifies an existing attribute of an object and returns the object.

Let’s write an example to test it.

First, create an Iron Man object and give him some attributes:

let ironman = {
 name: 'Tony Stark',
 sex: 'male',
 age: '35'
 }

Now we useObject.defineProperty()Method to modify some of his properties, and output the modified content in the console:

Object.defineProperty(ironman, 'age', {
 set (val) {
 console.log(`Set age to ${val}`)
 return val
 }
 })
 
 ironman.age = '48'
 // --> Set age to 48

It looks perfect. If you putconsole.log('Set age to ${val}')insteadelement.innerHTML = valDoes this mean that data binding has been completed?

Let’s revise the properties of Iron Man again:

let ironman = {
 name: 'Tony Stark',
 sex: 'male',
 age: '35',
 hobbies: ['girl', 'money', 'game']
 }

Well … he is a playboy. Now I want to add some “hobbies” to him and see the corresponding output on the console:

Object.defineProperty(ironman.hobbies, 'push', {
 value () {
 console.log(`Push ${arguments[0]} to ${this}`)
 this[this.length] = arguments[0]
 }
 })
 
 ironman.hobbies.push('wine')
 console.log(ironman.hobbies)
 
 // --> Push wine to girl,money,game
 // --> [ 'girl', 'money', 'game', 'wine' ]

Before that, I was usingget()Method to track changes in an object’s attributes, but for an array, we cannot use this method, we usevalue()Method to replace. Although it is also effective, it is not the best way. Is there a better way to simplify these methods of tracking changes in the properties of objects or arrays?

At ECMA2015,ProxyIs a good choice

what isProxy? InMDN documentThis is what the Chinese say (by mistake):

Proxy can be understood as setting up a layer of “interception” before the target object, through which external access to the object must first pass, thus providing a mechanism to filter and rewrite external access.

ProxyIt’s a new feature of ECMA2015. It’s very powerful, but I won’t discuss too much about it, except the one we need now. Now let’s create a new Proxy instance:

let ironmanProxy = new Proxy(ironman, {
 set (target, property, value) {
 target[property] = value
 console.log('change....')
 return true
 }
 })
 
 ironmanProxy.age = '48'
 console.log(ironman.age)
 
 // --> change  ....
 // --> 48

Meet expectations. What about arrays?

let ironmanProxy = new Proxy(ironman.hobbies, {
 set (target, property, value) {
 target[property] = value
 console.log('change....')
 return true
 }
 })
 
 ironmanProxy.push('wine')
 console.log(ironman.hobbies)
 
 // --> change  ...
 // --> change  ...
 // --> [ 'girl', 'money', 'game', 'wine' ]

Still in line with expectations! But why output twicechange ...? Because whenever I triggerpush()Method, thelengthAttributes andbodyThe content has been modified, so it will cause two changes.

Real-time data binding

The core problem has been solved and other problems can be considered.

Imagine that we have a template and data objects:

<!  -- html template -->
 <p>Hello, my name is {{name}}, I enjoy eatting {{hobbies.food}}</p>
 
 <!  -- javascript -->
 let ironman = {
 name: 'Tony Stark',
 sex: 'male',
 age: '35',
 hobbies: {
 food: 'banana',
 drink: 'wine'
 }
 }

From the previous code, we know that if we want to track the property changes of an object, we should pass this property in as the first parameter.ProxyExamples. Let’s create a newProxyInstance of the function!

function $setData (dataObj, fn) {
 let self = this
 let once = false
 let $d = new Proxy(dataObj, {
 set (target, property, value) {
 if (!  once) {
 target[property] = value
 once = true
 /* Do something here */
 }
 return true
 }
 })
 fn($d)
 }

It can be used in the following ways:

$setData(dataObj, ($d) => {
 /*
 * dataObj.someProps = something
 */
 })
 
 //or
 
 $setData(dataObj.arrayProps, ($d) => {
 /*
 * dataObj.push(something)
 */
 })

In addition, we should implement the mapping of templates to data objects so that we can useTony StarkTo replace{{name}}.

function replaceFun(str, data) {
 let self = this
 return str.replace(/{{([^{}]*)}}/g, (a, b) => {
 return data[b]
 })
 }
 
 replaceFun('My name is {{name}}', { name: 'xxx' })
 // --> My name is xxx

This function is useful for{ name: 'xx', age: 18 }The single-level attribute object of works well, but for example{ hobbies: { food: 'apple', drink: 'milk' } }Such multi-layer attribute objects are powerless. For example, if the template keyword is{{hobbies.food}}, thenreplaceFun()Function should returndata['hobbies']['food'].

In order to solve this problem, another function:

function getObjProp (obj, propsName) {
 let propsArr = propsName.split('.')
 function rec(o, pName) {
 if (!  o[pName] instanceof Array && o[pName] instanceof Object) {
 return rec(o[pName], propsArr.shift())
 }
 return o[pName]
 }
 return rec(obj, propsArr.shift())
 }
 
 getObjProp({ data: { hobbies: { food: 'apple', drink: 'milk' } } }, 'hobbies.food')
 // --> return  { food: 'apple', drink: 'milk' }

FinalreplaceFun()The function should look like this:

function replaceFun(str, data) {
 let self = this
 return str.replace(/{{([^{}]*)}}/g, (a, b) => {
 let r = self._getObjProp(data, b);
 console.log(a, b, r)
 if (typeof r === 'string' || typeof r === 'number') {
 return r
 } else {
 return self._getObjProp(r, b.split('.')[1])
 }
 })
 }

An example of data binding is called “Mog.”

For no reason, it is called “Mog”.

class Mog {
 constructor (options) {
 this.$data = options.data
 this.$el = options.el
 this.$tpl = options.template
 this._render(this.$tpl, this.$data)
 }
 
 $setData (dataObj, fn) {
 let self = this
 let once = false
 let $d = new Proxy(dataObj, {
 set (target, property, value) {
 if (!  once) {
 target[property] = value
 once = true
 self._render(self.$tpl, self.$data)
 }
 return true
 }
 })
 fn($d)
 }
 
 _render (tplString, data) {
 document.querySelector(this.$el).innerHTML = this._replaceFun(tplString, data)
 }
 
 _replaceFun(str, data) {
 let self = this
 return str.replace(/{{([^{}]*)}}/g, (a, b) => {
 let r = self._getObjProp(data, b);
 console.log(a, b, r)
 if (typeof r === 'string' || typeof r === 'number') {
 return r
 } else {
 return self._getObjProp(r, b.split('.')[1])
 }
 })
 }
 
 _getObjProp (obj, propsName) {
 let propsArr = propsName.split('.')
 function rec(o, pName) {
 if (!  o[pName] instanceof Array && o[pName] instanceof Object) {
 return rec(o[pName], propsArr.shift())
 }
 return o[pName]
 }
 return rec(obj, propsArr.shift())
 }
 
 }

Use:

<!  -- html -->
 
 <div id="app">
 <p>
 Hello everyone, my name is <span>{{name}}</span>,   I am a mini <span>{{lang}}</span> framework for just <span>{{work}}</span>. I can bind data from <span>{{supports.0}}</span>,   <span>{{supports.1}}</span> and <span>{{supports.2}}</span>. What's more, I was created by <span>{{info.author}}</span>,   and was written in <span>{{info.jsVersion}}</span>. My motto is "<span>{{motto}}</span>".
 </p>
 </div>
 <div id="input-wrapper">
 Motto: <input type="text" id="set-motto" autofocus>
 </div>
<!  -- javascript -->
 
 let template = document.querySelector('#app').innerHTML
 
 let mog = new Mog({
 template: template,
 el: '#app',
 data: {
 name: 'mog',
 lang: 'javascript',
 work: 'data binding',
 supports: ['String', 'Array', 'Object'],
 info: {
 author: 'Jrain',
 jsVersion: 'Ecma2015'
 },
 motto: 'Every dog has his day'
 }
 })
 
 document.querySelector('#set-motto').oninput = (e) => {
 mog.$setData(mog.$data, ($d) => {
 $d.motto = e.target.value
 })
 }

You can stay atHereExperience online.

Postscript

MogJust an experimental project for learning data binding, the code is still not elegant enough and the function is not rich enough. But this little toy made me learn a lot. If you are interested in it, welcome toHereFork the project and add some ideas.

Thank you for reading!