vue双向绑定的简单实现

网友投稿 169 2023-06-23


vue双向绑定的简单实现

研究了一下vue双向绑定的原理,所以简单记录一下,以下例子只是简单实现,还请大家不要吐槽~

之前也了解过vue是通过数据劫持+订阅发布模式来实现MVVM的双向绑定的,但一直没仔细研究,这次深入学习了一下,借此机会分享给大家。

首先先将流程图给大家看一下

参考文章:Vuhttp://e.js双向绑定的实现原理

jpvwDj

如果大家看了我的解释也能够完全理解的话,那就更好啦啦啦啦啦~哈哈

好,下面我会从2个角度开始讲解,先上单向绑定,再由单向绑定过渡到双向绑定;

首先,先为大家解释一下单向绑定model => view层的逻辑

1、劫持dom结构;

2、创建文档碎片,利用文档碎片重构dom结构;

3、在重构的过程中解析dom结构实现MVVM构造函数实例化后的数据初始化视图数据;

4、利用判断dom一级子元素是否依然有子元素从而进行所有子元素的单向绑定;

5、将文档碎片添加至根节点中.

这就是我总结的关于单向绑定的逻辑了,下面利用代码跟大家解释

//dom结构

{{msg}}

//one-way-binding.js

//判断每个dom节点是否拥有子节点,若有则返回该节点

function isChild(node){

//这里使用childNodes可以读取text文本节点,所以不用children

if(node.childNodes.length ===0){

return false;

}

else{

return node;

}

}

//利用文档碎片劫持dom结构及数据,进而进行dom的重构

function nodeToFragment(node,vm){

var frag = document.createDocumentFragment();

var child;

while(child = node.firstChild){

//一级dom节点数据绑定

compile(child,vm);

//判断每个一级dom节点是否有二级节点,若有则递归处理文档碎片

if(isChild(child)){

//递归实现二级dom节点的重构

nodeToFragment(isChild(child),vm);

}

frag.appendChild(child);

}

//将文档碎片添加至对应node中,最后为id为app的元素下

node.appendChild(frag);

}

//初始化绑定数据

function compile(node,vm){

//node节点为元素节点时

if(node.nodeType === 1){

var attr = node.attributes;

//遍历当前节点的所有属性

for(var i=0;i

if(attr[i].nodeName === 'v-model'){

//属性名

var name = attr[i].nodeValue;

//将data下对应属性名的值赋值给当前节点值

//这里因为node是input标签所以值为node.value

node.value = vm.data[name];

//最后标签中的v-model属性也可以功成身退了,删除它

node.removeAttribute(attr[i].nodeName);

}

}

}

//node节点为text文本节点#text时

if(node.nodeType === 3){

var reg = /\{\{(.*)\}\}/;

if(reg.test(node.nodeValue.trim())){

//将正则匹配到的{{}}中的字符串赋值给name

var name = RegExp.$1;

//利用name对应赋值相应的节点值

node.nodeValue = vm.data[name];

}

}

}

//MVVM构造函数,这里我就写成Vue了

function Vue(options){

this.id = options.el;

this.data = options.data;

//将根节点与实例化后的对象作为参数传入

nodeToFragment(document.getElementById(this.id),this);

}

//实例化

var vm = new Vue({

el:'app',

data:{

msg:'hello,two-ways-binding',

test:'test key'

}

})

上述就是简单的单向绑定了,整个逻辑实际上非常简单,我再来跟大家说明一下

1、为了令model层的数据可以绑定到view层的dom上,所以我们想了一个办法来替换dom中的一些元素值,而明显一个个替换时不可取的,因为大量的dom操作会降低程序的运行效率,你想想,每次dom操作可都是一次对dom整体的遍历过程~,所以我们觉得采用文档碎片的形式,将dom一次全部劫持,在内存中执行全部数据绑定操作,最后只进行一次dom操作,即添加子节点来解决这个频繁操作dom的问题,你也可以理解为中间的一层存在于内存中的虚拟dom;

2、那么既然如此,我们就要首先劫持所有dom节点,这里我们利用nodeToFragment函数来劫持;

3、在每次劫持对应dom节点的过程中,我们也会相对应的实现对该dom元素的数据绑定,以求在最后直接添加到为根节点的子元素即可,这个过程我们就在nodeToFragment函数中插入了compile函数来初始化绑定,并且添加递归函数实现所有子元素的初始绑定;

4、在compile函数中我们添加的数据又从何而来呢?对,正是因为这点,所以我们建立MVVM的构造函数Vue来实现数据支持,并实现在实例化时就执行nodeToFragment同时重构dom和实现初始化绑定compile;

5、好了,单向绑定就是这么简单,4个函数即可Vue => nodeToFragment => compile => isChild。

完成图如下

好了,再回过来看看整体的流程图,我们已经实现了这一块了

接下来,休息下,大家准备开始流程图后面的双向绑定,ok,还是按照单向绑定的顺序,先跟大家讲明实现逻辑;

1、创建数据监听者observer去监听view层数据的变化;(利用Object.defineProperty劫持所有要用到的数据)

2、当view层数据变化后,通过通知者Dep通知订阅者去实现数据的更新;(通知后,遍历所有用到数据的订阅者更新数据)

3、订阅者watcher接收到view层数据变更后,重新对变化的数据进行赋值,改变model层,从而改变所有view层用到过该数据的地方。(更新数据,并改变view层所有用到该数据的节点值)

上面是实现逻辑,下面将通过具体代码告诉大家每一步的做法,由于双向绑定中订阅者会涉及初始化绑定的过程,所以代码量较多,我会在大更改处用——为大家框出来

//判断每个dom节点是否拥有子节点,若有则返回该节点

function isChild(node){

if(node.childNodes.length ===0){

return false;

}

else{

return node;

}

}

//利用文档碎片劫持dom结构及数据,进而进行dom的重构

function nodeToFragment(node,vm){

var frag = document.createDocumentFragment();

var child;

while(child = node.firstChild){

//一级dom节点数据绑定

compile(child,vm);

//判断每个一级dom节点是否有二级节点,若有则递归处理文档碎片

if(isChild(child)){

nodeToFragment(isChild(child),vm);

}

frag.appendChild(child);

}

node.appendChild(frag);

}

//初始化绑定数据

function compile(node,vm){

//node节点为元素节点时

if(node.nodeType === 1){

var attr = node.attributes;

for(var i=0;i

if(attr[i].nodeName === 'v-model'){

var name = attr[i].nodeValue;

//特殊处理input标签

//------------------------

if(node.nodeName === 'INPUT'){

node.addEventListener('keyup',function(e){

vm[name] = e.target.value;

})

}

//由于数据已经由data劫持至vm下,所以直接赋值vm[name]即可触发getter访问器

node.value = vm[name];

//-------------------------

node.removeAttribute(attr[i].nodeName);

}

}

}

//node节点为text文本节点时

if(node.nodeType === 3){

var reg = /\{\{(.*)\}\}/;

if(reg.test(node.nodeValue.trim())){

var name = RegExp.$1;

//node.nodeValue = vm[name];

//----------------------

//为每个节点建立订阅者,通过订阅者watcher初始化及更新视图数据

new watcher(vm,node,name);

//-----------------------

}

}

}

//----------------------------------------------------------------

//订阅者(为每个节点的数据建立watcher队列,每次接受更改数据需求后,利用劫持数据执行对应节点的数据更新)

function watcher(vm,node,name){

//将每个挂载了数据的dom节点添加到通知者列表,要保证每次创建watcher时只有一个添加目标,否则后续会因为watcher是全局而被覆盖,所以每次要清空目标

Dep.target = this;

this.vm = vm;

this.node = node;

this.namhttp://e = name;

//执行update的时候会调用监听者劫持的getter事件,从而添加到watcher队列,因为update中有访问this.vm[this.name]

this.update();

//为保证只有一个全局watcher,添加到队列后,清空全局watcher

Dep.target = null;

}

watcher.prototype = {

update(){

this.get();

//input标签特殊处理化

if(this.node.nodeName === 'INPUT'){

this.node.value = this.value;

}

else{

this.node.nodeValue = this.value;

}

},

get(){

//这里调用了数据劫持的getter

this.value = this.vm[this.name];

}

};

//通知者(将监听者的更改信息需求发送给订阅者,告诉订阅者哪些数据需要更改)

function Dep(){

this.subs = [];

}

Dep.prototype = {

addSub(watcher){

//添加用到数据的节点进入watcher队列

this.subs.push(watcher);

},

notify(){

//遍历watcher队列,令相应数据节点重新更新view层数据,model => view

this.subs.forEach(function(watcher){

watcher.update();

})

}

};

//监听者(利用setter监听view => model的数据变化,发出通知更改model数据后再从model => view更新视图所有用到该数据的地方)

function observer(data,vm){

//遍历劫持data下所有属性

Object.keys(data).forEach(function(key){

defineReactive(vm,key,data[key]);

})

}

function defineReactive(vm,key,val){

//新建通知者

var dep = new Dep();

//灵活利用setter与getter访问器

Object.defineProperty(vm,key,{

get(){

//初始化数据更新时将每个数据的watcher添加至队列栈中

if(Dep.target) dep.addSub(Dep.target);

return val;

},

set(newVal){

if(val === newVal) return ;

//初始化后,文档碎片中的虚拟dom已与model层数据绑定起来了

val = newVal;

//同步更新model中data属性下的数据

vm.data[key] = val;

//数据有改动时向通知者发送通知

dep.notify();

}

})

}

//---------------------------------------------------------------

function Vue(options){

this.id = options.el;

this.data = options.data;

observer(this.data,this);

nodeToFragment(document.getElementById(this.id),this);

}

var vm = new Vue({

el:'app',

data:{

msg:'hello,two-ways-binding',

test:'test key'

}

})

好的,到这里双向绑定的讲解也就结束了,代码量确实有点多,但是我们看到其实逻辑在你熟悉后并不复杂,特别是参照了上文的流程图后,其实就是:

1、通过observer劫持所有model层数据到vue下,并在劫持时灵活运用getter与setter访问器属性来在虚拟dom初始化数据绑定时,利用此时的get方法绑定初始化数据进入通知者队列,后续初始化完成后,在view层数据发生变化时,利用set方法及时利用通知者发出通知;

2、在dep通知者接收到有一处dom节点数据更改的通知时,遍历watcher队列及告诉watcher订阅者,view层数据有所变动model层已经相应改变,你要重新执行update将model层的数据更新到view层所有用到该数据的地方(比如我们利用input实现的双向绑定就不止一个dom节点内使用了,而是多个,所以必须整体遍历修改)。

3、这是一个model => view => model =>view的过程,真正的逻辑顺序为model => view,view => model,model => view,反复的过程~

贴上结果图

初始未改动view层数据图

修改view层数据后图

最后大家再看一次流程图,看看整个逻辑是不是跟流程图一模一样,通过流程图就可以回忆起这个逻辑过程,写多2遍就可以记住!

以上只是通过简单实现来告诉大家vue的数据劫持+订阅发布模式这个双向绑定的原理,其中有很多细节上的不足可能未作处理,还请见谅~

if(attr[i].nodeName === 'v-model'){

//属性名

var name = attr[i].nodeValue;

//将data下对应属性名的值赋值给当前节点值

//这里因为node是input标签所以值为node.value

node.value = vm.data[name];

//最后标签中的v-model属性也可以功成身退了,删除它

node.removeAttribute(attr[i].nodeName);

}

}

}

//node节点为text文本节点#text时

if(node.nodeType === 3){

var reg = /\{\{(.*)\}\}/;

if(reg.test(node.nodeValue.trim())){

//将正则匹配到的{{}}中的字符串赋值给name

var name = RegExp.$1;

//利用name对应赋值相应的节点值

node.nodeValue = vm.data[name];

}

}

}

//MVVM构造函数,这里我就写成Vue了

function Vue(options){

this.id = options.el;

this.data = options.data;

//将根节点与实例化后的对象作为参数传入

nodeToFragment(document.getElementById(this.id),this);

}

//实例化

var vm = new Vue({

el:'app',

data:{

msg:'hello,two-ways-binding',

test:'test key'

}

})

上述就是简单的单向绑定了,整个逻辑实际上非常简单,我再来跟大家说明一下

1、为了令model层的数据可以绑定到view层的dom上,所以我们想了一个办法来替换dom中的一些元素值,而明显一个个替换时不可取的,因为大量的dom操作会降低程序的运行效率,你想想,每次dom操作可都是一次对dom整体的遍历过程~,所以我们觉得采用文档碎片的形式,将dom一次全部劫持,在内存中执行全部数据绑定操作,最后只进行一次dom操作,即添加子节点来解决这个频繁操作dom的问题,你也可以理解为中间的一层存在于内存中的虚拟dom;

2、那么既然如此,我们就要首先劫持所有dom节点,这里我们利用nodeToFragment函数来劫持;

3、在每次劫持对应dom节点的过程中,我们也会相对应的实现对该dom元素的数据绑定,以求在最后直接添加到为根节点的子元素即可,这个过程我们就在nodeToFragment函数中插入了compile函数来初始化绑定,并且添加递归函数实现所有子元素的初始绑定;

4、在compile函数中我们添加的数据又从何而来呢?对,正是因为这点,所以我们建立MVVM的构造函数Vue来实现数据支持,并实现在实例化时就执行nodeToFragment同时重构dom和实现初始化绑定compile;

5、好了,单向绑定就是这么简单,4个函数即可Vue => nodeToFragment => compile => isChild。

完成图如下

好了,再回过来看看整体的流程图,我们已经实现了这一块了

接下来,休息下,大家准备开始流程图后面的双向绑定,ok,还是按照单向绑定的顺序,先跟大家讲明实现逻辑;

1、创建数据监听者observer去监听view层数据的变化;(利用Object.defineProperty劫持所有要用到的数据)

2、当view层数据变化后,通过通知者Dep通知订阅者去实现数据的更新;(通知后,遍历所有用到数据的订阅者更新数据)

3、订阅者watcher接收到view层数据变更后,重新对变化的数据进行赋值,改变model层,从而改变所有view层用到过该数据的地方。(更新数据,并改变view层所有用到该数据的节点值)

上面是实现逻辑,下面将通过具体代码告诉大家每一步的做法,由于双向绑定中订阅者会涉及初始化绑定的过程,所以代码量较多,我会在大更改处用——为大家框出来

//判断每个dom节点是否拥有子节点,若有则返回该节点

function isChild(node){

if(node.childNodes.length ===0){

return false;

}

else{

return node;

}

}

//利用文档碎片劫持dom结构及数据,进而进行dom的重构

function nodeToFragment(node,vm){

var frag = document.createDocumentFragment();

var child;

while(child = node.firstChild){

//一级dom节点数据绑定

compile(child,vm);

//判断每个一级dom节点是否有二级节点,若有则递归处理文档碎片

if(isChild(child)){

nodeToFragment(isChild(child),vm);

}

frag.appendChild(child);

}

node.appendChild(frag);

}

//初始化绑定数据

function compile(node,vm){

//node节点为元素节点时

if(node.nodeType === 1){

var attr = node.attributes;

for(var i=0;i

if(attr[i].nodeName === 'v-model'){

var name = attr[i].nodeValue;

//特殊处理input标签

//------------------------

if(node.nodeName === 'INPUT'){

node.addEventListener('keyup',function(e){

vm[name] = e.target.value;

})

}

//由于数据已经由data劫持至vm下,所以直接赋值vm[name]即可触发getter访问器

node.value = vm[name];

//-------------------------

node.removeAttribute(attr[i].nodeName);

}

}

}

//node节点为text文本节点时

if(node.nodeType === 3){

var reg = /\{\{(.*)\}\}/;

if(reg.test(node.nodeValue.trim())){

var name = RegExp.$1;

//node.nodeValue = vm[name];

//----------------------

//为每个节点建立订阅者,通过订阅者watcher初始化及更新视图数据

new watcher(vm,node,name);

//-----------------------

}

}

}

//----------------------------------------------------------------

//订阅者(为每个节点的数据建立watcher队列,每次接受更改数据需求后,利用劫持数据执行对应节点的数据更新)

function watcher(vm,node,name){

//将每个挂载了数据的dom节点添加到通知者列表,要保证每次创建watcher时只有一个添加目标,否则后续会因为watcher是全局而被覆盖,所以每次要清空目标

Dep.target = this;

this.vm = vm;

this.node = node;

this.namhttp://e = name;

//执行update的时候会调用监听者劫持的getter事件,从而添加到watcher队列,因为update中有访问this.vm[this.name]

this.update();

//为保证只有一个全局watcher,添加到队列后,清空全局watcher

Dep.target = null;

}

watcher.prototype = {

update(){

this.get();

//input标签特殊处理化

if(this.node.nodeName === 'INPUT'){

this.node.value = this.value;

}

else{

this.node.nodeValue = this.value;

}

},

get(){

//这里调用了数据劫持的getter

this.value = this.vm[this.name];

}

};

//通知者(将监听者的更改信息需求发送给订阅者,告诉订阅者哪些数据需要更改)

function Dep(){

this.subs = [];

}

Dep.prototype = {

addSub(watcher){

//添加用到数据的节点进入watcher队列

this.subs.push(watcher);

},

notify(){

//遍历watcher队列,令相应数据节点重新更新view层数据,model => view

this.subs.forEach(function(watcher){

watcher.update();

})

}

};

//监听者(利用setter监听view => model的数据变化,发出通知更改model数据后再从model => view更新视图所有用到该数据的地方)

function observer(data,vm){

//遍历劫持data下所有属性

Object.keys(data).forEach(function(key){

defineReactive(vm,key,data[key]);

})

}

function defineReactive(vm,key,val){

//新建通知者

var dep = new Dep();

//灵活利用setter与getter访问器

Object.defineProperty(vm,key,{

get(){

//初始化数据更新时将每个数据的watcher添加至队列栈中

if(Dep.target) dep.addSub(Dep.target);

return val;

},

set(newVal){

if(val === newVal) return ;

//初始化后,文档碎片中的虚拟dom已与model层数据绑定起来了

val = newVal;

//同步更新model中data属性下的数据

vm.data[key] = val;

//数据有改动时向通知者发送通知

dep.notify();

}

})

}

//---------------------------------------------------------------

function Vue(options){

this.id = options.el;

this.data = options.data;

observer(this.data,this);

nodeToFragment(document.getElementById(this.id),this);

}

var vm = new Vue({

el:'app',

data:{

msg:'hello,two-ways-binding',

test:'test key'

}

})

好的,到这里双向绑定的讲解也就结束了,代码量确实有点多,但是我们看到其实逻辑在你熟悉后并不复杂,特别是参照了上文的流程图后,其实就是:

1、通过observer劫持所有model层数据到vue下,并在劫持时灵活运用getter与setter访问器属性来在虚拟dom初始化数据绑定时,利用此时的get方法绑定初始化数据进入通知者队列,后续初始化完成后,在view层数据发生变化时,利用set方法及时利用通知者发出通知;

2、在dep通知者接收到有一处dom节点数据更改的通知时,遍历watcher队列及告诉watcher订阅者,view层数据有所变动model层已经相应改变,你要重新执行update将model层的数据更新到view层所有用到该数据的地方(比如我们利用input实现的双向绑定就不止一个dom节点内使用了,而是多个,所以必须整体遍历修改)。

3、这是一个model => view => model =>view的过程,真正的逻辑顺序为model => view,view => model,model => view,反复的过程~

贴上结果图

初始未改动view层数据图

修改view层数据后图

最后大家再看一次流程图,看看整个逻辑是不是跟流程图一模一样,通过流程图就可以回忆起这个逻辑过程,写多2遍就可以记住!

以上只是通过简单实现来告诉大家vue的数据劫持+订阅发布模式这个双向绑定的原理,其中有很多细节上的不足可能未作处理,还请见谅~

if(attr[i].nodeName === 'v-model'){

var name = attr[i].nodeValue;

//特殊处理input标签

//------------------------

if(node.nodeName === 'INPUT'){

node.addEventListener('keyup',function(e){

vm[name] = e.target.value;

})

}

//由于数据已经由data劫持至vm下,所以直接赋值vm[name]即可触发getter访问器

node.value = vm[name];

//-------------------------

node.removeAttribute(attr[i].nodeName);

}

}

}

//node节点为text文本节点时

if(node.nodeType === 3){

var reg = /\{\{(.*)\}\}/;

if(reg.test(node.nodeValue.trim())){

var name = RegExp.$1;

//node.nodeValue = vm[name];

//----------------------

//为每个节点建立订阅者,通过订阅者watcher初始化及更新视图数据

new watcher(vm,node,name);

//-----------------------

}

}

}

//----------------------------------------------------------------

//订阅者(为每个节点的数据建立watcher队列,每次接受更改数据需求后,利用劫持数据执行对应节点的数据更新)

function watcher(vm,node,name){

//将每个挂载了数据的dom节点添加到通知者列表,要保证每次创建watcher时只有一个添加目标,否则后续会因为watcher是全局而被覆盖,所以每次要清空目标

Dep.target = this;

this.vm = vm;

this.node = node;

this.namhttp://e = name;

//执行update的时候会调用监听者劫持的getter事件,从而添加到watcher队列,因为update中有访问this.vm[this.name]

this.update();

//为保证只有一个全局watcher,添加到队列后,清空全局watcher

Dep.target = null;

}

watcher.prototype = {

update(){

this.get();

//input标签特殊处理化

if(this.node.nodeName === 'INPUT'){

this.node.value = this.value;

}

else{

this.node.nodeValue = this.value;

}

},

get(){

//这里调用了数据劫持的getter

this.value = this.vm[this.name];

}

};

//通知者(将监听者的更改信息需求发送给订阅者,告诉订阅者哪些数据需要更改)

function Dep(){

this.subs = [];

}

Dep.prototype = {

addSub(watcher){

//添加用到数据的节点进入watcher队列

this.subs.push(watcher);

},

notify(){

//遍历watcher队列,令相应数据节点重新更新view层数据,model => view

this.subs.forEach(function(watcher){

watcher.update();

})

}

};

//监听者(利用setter监听view => model的数据变化,发出通知更改model数据后再从model => view更新视图所有用到该数据的地方)

function observer(data,vm){

//遍历劫持data下所有属性

Object.keys(data).forEach(function(key){

defineReactive(vm,key,data[key]);

})

}

function defineReactive(vm,key,val){

//新建通知者

var dep = new Dep();

//灵活利用setter与getter访问器

Object.defineProperty(vm,key,{

get(){

//初始化数据更新时将每个数据的watcher添加至队列栈中

if(Dep.target) dep.addSub(Dep.target);

return val;

},

set(newVal){

if(val === newVal) return ;

//初始化后,文档碎片中的虚拟dom已与model层数据绑定起来了

val = newVal;

//同步更新model中data属性下的数据

vm.data[key] = val;

//数据有改动时向通知者发送通知

dep.notify();

}

})

}

//---------------------------------------------------------------

function Vue(options){

this.id = options.el;

this.data = options.data;

observer(this.data,this);

nodeToFragment(document.getElementById(this.id),this);

}

var vm = new Vue({

el:'app',

data:{

msg:'hello,two-ways-binding',

test:'test key'

}

})

好的,到这里双向绑定的讲解也就结束了,代码量确实有点多,但是我们看到其实逻辑在你熟悉后并不复杂,特别是参照了上文的流程图后,其实就是:

1、通过observer劫持所有model层数据到vue下,并在劫持时灵活运用getter与setter访问器属性来在虚拟dom初始化数据绑定时,利用此时的get方法绑定初始化数据进入通知者队列,后续初始化完成后,在view层数据发生变化时,利用set方法及时利用通知者发出通知;

2、在dep通知者接收到有一处dom节点数据更改的通知时,遍历watcher队列及告诉watcher订阅者,view层数据有所变动model层已经相应改变,你要重新执行update将model层的数据更新到view层所有用到该数据的地方(比如我们利用input实现的双向绑定就不止一个dom节点内使用了,而是多个,所以必须整体遍历修改)。

3、这是一个model => view => model =>view的过程,真正的逻辑顺序为model => view,view => model,model => view,反复的过程~

贴上结果图

初始未改动view层数据图

修改view层数据后图

最后大家再看一次流程图,看看整个逻辑是不是跟流程图一模一样,通过流程图就可以回忆起这个逻辑过程,写多2遍就可以记住!

以上只是通过简单实现来告诉大家vue的数据劫持+订阅发布模式这个双向绑定的原理,其中有很多细节上的不足可能未作处理,还请见谅~


版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:Java实现多线程断点下载实例代码(下载过程中可以暂停)
下一篇:深入理解java异常处理机制及应用
相关文章

 发表评论

暂时没有评论,来抢沙发吧~