Vue Object.defineProperty及ProxyVue实现双向数据绑定

Vue Object.defineProperty及ProxyVue实现双向数据绑定

388
2020-09-10 10:31:03

双向数据绑定无非就是,视图 => 数据,数据 => 视图的更新过程

Vue Object.defineProperty及ProxyVue实现双向数据绑定 (https://www.wpmee.com/) javascript教程 第1张

以下的方案中的实现思路:

定义一个Vue的构造函数并初始化这个函数(myVue.prototype._init)

实现数据层的更新:数据劫持,定义一个 obverse 函数重写data的set和get(myVue.prototype._obsever)

实现视图层的更新:订阅者模式,定义个 Watcher 函数实现对DOM的更新(Watcher)

将数据和视图层进行绑定,解析指令v-bind、v-model、v-click(myVue.prototype._compile)

创建Vue实例(new myVue)

1.object.defineproperty方式实现双向数据绑定

<!DOCTYPE html>

<html>

 

<head>

<title>myVue</title>

<style>

#app{

text-align: center;

}

</style>

</head>

 

<body>

<div id="app">

<form>

<input type="text" v-model="number" />

<button type="button" v-click="increment">增加</button>

</form>

<h3 v-bind="number"></h3>

</div>

</body>

<script>

 

// 定义一个myVue构造函数

function myVue(option) {

this._init(option)

}

 

myVue.prototype._init = function (options) { // 传了一个配置对象

this.$options = options // options 为上面使用时传入的结构体,包括el,data,methods

this.$el = document.querySelector(options.el) // el是 #app, this.$el是id为app的Element元素

this.$data = options.data // this.$data = {number: 0}

this.$methods = options.methods // this.$methods = {increment: function(){}}

 

 

// _binding保存着model与view的映射关系,也就是我们前面定义的Watcher的实例。当model改变时,我们会触发其中的指令类更新,保证view也能实时更新

this._binding = {}

 

this._obsever(this.$data)

this._compile(this.$el)

}

 

// 数据劫持:更新数据

myVue.prototype._obsever = function (obj) {

let _this = this

Object.keys(obj).forEach((key) => { // 遍历obj对象

if (obj.hasOwnProperty(key)) { // 判断 obj 对象是否包含 key属性

_this._binding[key] = [] // 按照前面的数据,_binding = {number: []} 存储 每一个 new Watcher

}

let value = obj[key]

if (typeof value === 'object') { //如果值还是对象,则遍历处理

_this._obsever(value)

}

Object.defineProperty(_this.$data, key, {

enumerable: true,

configurable: true,

get: () => { // 获取 value 值

return value

},

set: (newVal) => { // 更新 value 值

if (value !== newVal) {

value = newVal

_this._binding[key].forEach((item) => { // 当number改变时,触发_binding[number] 中的绑定的Watcher类的更新

item.update() // 调 Watcher 实例的 update 方法更新 DOM

})

}

}

})

})

}

 

// 订阅者模式: 绑定更新函数,实现对 DOM 元素的更新

function Watcher(el, data, key, attr) {

this.el = el // 指令对应的DOM元素

this.data = data // this.$data 数据: {number: 0, count: 0}

this.key = key // 指令绑定的值,本例如"number"

this.attr = attr // 绑定的属性值,本例为"innerHTML","value"

 

this.update()

}

// 比如 H3.innerHTML = this.data.number; 当number改变时,会触发这个update函数,保证对应的DOM内容进行了更新

Watcher.prototype.update = function () {

this.el[this.attr] = this.data[this.key]

}

 

// 将view与model进行绑定,解析指令(v-bind,v-model,v-clickde)等

myVue.prototype._compile = function (el) { // root 为id为app的Element元素,也就是我们的根元素

let _this = this

let nodes = Array.prototype.slice.call(el.children) // 将为数组转化为真正的数组

nodes.map(node => {

if (node.children.length && node.children.length > 0) { // 对所有元素进行遍历,并进行处理

_this._compile(node)

}

if (node.hasAttribute('v-click')) { // 如果有v-click属性,我们监听它的onclick事件,触发increment事件,即number++

let attrVal = node.getAttribute('v-click')

node.onclick = _this.$methods[attrVal].bind(_this.$data) // bind是使data的作用域与method函数的作用域保持一致

}

 

// 如果有v-model属性,并且元素是INPUT或者TEXTAREA,我们监听它的input事件

if (node.hasAttribute('v-model') && (node.tagName === 'INPUT' || node.tagName === 'TEXTAREA')) {

let attrVal = node.getAttribute('v-model')

 

_this._binding[attrVal].push(new Watcher(

node, // 对应的 DOM 节点

_this.$data,

attrVal, // v-model 绑定的值

'value'

))

node.addEventListener('input', () => {

_this.$data[attrVal] = node.value // 使number 的值与 node的value保持一致,已经实现了双向绑定

})

}

if (node.hasAttribute('v-bind')) {

let attrVal = node.getAttribute('v-bind')

_this._binding[attrVal].push(new Watcher(

node,

_this.$data,

attrVal, // v-bind 绑定的值

'innerHTML'

))

}

})

}

 

 

window.onload = () => { // 当文档内容完全加载完成会触发该事件,避免获取不到对象的情况

new myVue({

el: '#app',

data: {

number: 0,

count: 0

},

methods: {

increment() {

this.number++

},

incre() {

this.count++

}

}

})

}

</script>

 

</html>

2.Proxy 实现双向数据绑定

<!DOCTYPE html>

<html>

 

<head>

<title>myVue</title>

<style>

#app{

text-align: center;

}

</style>

</head>

 

<body>

<div id="app">

<form>

<input type="text" v-model="number" />

<button type="button" v-click="increment">增加</button>

</form>

<h3 v-bind="number"></h3>

</div>

</body>

<script>

 

// 定义一个myVue构造函数

function myVue(option) {

this._init(option)

}

 

myVue.prototype._init = function (options) { // 传了一个配置对象

this.$options = options // options 为上面使用时传入的结构体,包括el,data,methods

this.$el = document.querySelector(options.el) // el是 #app, this.$el是id为app的Element元素

this.$data = options.data // this.$data = {number: 0}

this.$methods = options.methods // this.$methods = {increment: function(){}}

 

this._binding = {}

this._obsever(this.$data)

this._complie(this.$el)

 

}

 

// 数据劫持:更新数据

myVue.prototype._obsever = function (data) {

let _this = this

let handler = {

get(target, key) {

return target[key]; // 获取该对象上key的值

},

set(target, key, newValue) {

let res = Reflect.set(target, key, newValue); // 将新值分配给属性的函数

_this._binding[key].map(item => {

item.update();

});

return res;

}

};

// 把代理器返回的对象代理到this.$data,即this.$data是代理后的对象,外部每次对this.$data进行操作时,实际上执行的是这段代码里handler对象上的方法

this.$data = new Proxy(data, handler);

}

 

// 将view与model进行绑定,解析指令(v-bind,v-model,v-clickde)等

myVue.prototype._complie = function (el) { // el 为id为app的Element元素,也就是我们的根元素

let _this = this

let nodes = Array.prototype.slice.call(el.children) // 将为数组转化为真正的数组

 

nodes.map(node => {

if (node.children.length && node.children.length > 0) this._complie(node)

if (node.hasAttribute('v-click')) { // 如果有v-click属性,我们监听它的onclick事件,触发increment事件,即number++

let attrVal = node.getAttribute('v-click')

node.onclick = _this.$methods[attrVal].bind(_this.$data) // bind是使data的作用域与method函数的作用域保持一致

}

 

// 如果有v-model属性,并且元素是INPUT或者TEXTAREA,我们监听它的input事件

if (node.hasAttribute('v-model') && (node.tagName === 'INPUT' || node.tagName === 'TEXTAREA')) {

let attrVal = node.getAttribute('v-model')

 

console.log(_this._binding)

if (!_this._binding[attrVal]) _this._binding[attrVal] = []

_this._binding[attrVal].push(new Watcher(

node, // 对应的 DOM 节点

_this.$data,

attrVal, // v-model 绑定的值

'value',

))

node.addEventListener('input', () => {

_this.$data[attrVal] = node.value // 使number 的值与 node的value保持一致,已经实现了双向绑定

})

}

if (node.hasAttribute('v-bind')) {

let attrVal = node.getAttribute('v-bind')

if (!_this._binding[attrVal]) _this._binding[attrVal] = []

_this._binding[attrVal].push(new Watcher(

node,

_this.$data,

attrVal, // v-bind 绑定的值

'innerHTML',

))

}

 

})

}

// 绑定更新函数,实现对 DOM 元素的更新

function Watcher(el, data, key, attr) {

this.el = el // 指令对应的DOM元素

this.data = data // 代理的对象 this.$data 数据: {number: 0, count: 0}

this.key = key // 指令绑定的值,本例如"num"

this.attr = attr // 绑定的属性值,本例为"innerHTML","value"

 

this.update()

}

// 比如 H3.innerHTML = this.data.number; 当number改变时,会触发这个update函数,保证对应的DOM内容进行了更新

Watcher.prototype.update = function () {

this.el[this.attr] = this.data[this.key]

}

 

window.onload = () => { // 当文档内容完全加载完成会触发该事件,避免获取不到对象的情况

new myVue({

el: '#app',

data: {

number: 0,

count: 0

},

methods: {

increment() {

this.number++

},

incre() {

this.count++

}

}

})

}

</script>

 

</html>

3.将上面代码改成class的写法

<!DOCTYPE html>

<html>

 

<head>

<title>myVue</title>

<style>

#app{

text-align: center;

}

</style>

</head>

 

<body>

<div id="app">

<form>

<input type="text" v-model="number" />

<button type="button" v-click="increment">增加</button>

</form>

<h3 v-bind="number"></h3>

</div>

</body>

<script>

 

class MyVue {

constructor(options) { // 接收了一个配置对象

this.$options = options // options 为上面使用时传入的结构体,包括el,data,methods

this.$el = document.querySelector(options.el) // el是 #app, this.$el是id为app的Element元素

this.$data = options.data // this.$data = {number: 0}

this.$methods = options.methods // this.$methods = {increment: function(){}}

 

this._binding = {}

this._obsever(this.$data)

this._complie(this.$el)

}

_obsever (data) { // 数据劫持:更新数据

let _this = this

let handler = {

get(target, key) {

return target[key]; // 获取该对象上key的值

},

set(target, key, newValue) {

let res = Reflect.set(target, key, newValue); // 将新值分配给属性的函数

_this._binding[key].map(item => {

item.update();

});

return res;

}

};

// 把代理器返回的对象代理到this.$data,即this.$data是代理后的对象,外部每次对this.$data进行操作时,实际上执行的是这段代码里handler对象上的方法

this.$data = new Proxy(data, handler);

}

_complie(el) { // el 为id为app的Element元素,也就是我们的根元素

let _this = this

let nodes = Array.prototype.slice.call(el.children) // 将为数组转化为真正的数组

 

nodes.map(node => {

if (node.children.length && node.children.length > 0) this._complie(node)

if (node.hasAttribute('v-click')) { // 如果有v-click属性,我们监听它的onclick事件,触发increment事件,即number++

let attrVal = node.getAttribute('v-click')

node.onclick = _this.$methods[attrVal].bind(_this.$data) // bind是使data的作用域与method函数的作用域保持一致

}

 

// 如果有v-model属性,并且元素是INPUT或者TEXTAREA,我们监听它的input事件

if (node.hasAttribute('v-model') && (node.tagName === 'INPUT' || node.tagName === 'TEXTAREA')) {

let attrVal = node.getAttribute('v-model')

if (!_this._binding[attrVal]) _this._binding[attrVal] = []

_this._binding[attrVal].push(new Watcher(

node, // 对应的 DOM 节点

_this.$data,

attrVal, // v-model 绑定的值

'value',

))

node.addEventListener('input', () => {

_this.$data[attrVal] = node.value // 使number 的值与 node的value保持一致,已经实现了双向绑定

})

}

if (node.hasAttribute('v-bind')) {

let attrVal = node.getAttribute('v-bind')

if (!_this._binding[attrVal]) _this._binding[attrVal] = []

_this._binding[attrVal].push(new Watcher(

node,

_this.$data,

attrVal, // v-bind 绑定的值

'innerHTML',

))

}

 

})

}

}

 

class Watcher {

constructor (el, data, key, attr) {

this.el = el // 指令对应的DOM元素

this.data = data // 代理的对象 this.$data 数据: {number: 0, count: 0}

this.key = key // 指令绑定的值,本例如"num"

this.attr = attr // 绑定的属性值,本例为"innerHTML","value"

this.update()

}

 

update () {

this.el[this.attr] = this.data[this.key]

}

}

 

 

 

window.onload = () => { // 当文档内容完全加载完成会触发该事件,避免获取不到对象的情况

new MyVue({

el: '#app',

data: {

number: 0,

count: 0

},

methods: {

increment() {

this.number++

},

incre() {

this.count++

}

}

})

}

</script>

 

</html>

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。

Vue Object.defineProperty及ProxyVue实现双向数据绑定 (https://www.wpmee.com/) javascript教程 第2张

喜欢这个文章就点个赞分享给好友吧~

END
相关标签

发表评论

一次支付终身使用
免费版本更新
靠谱的技术支持
成为会员