首页IT科技vue实现双向绑定原理,自己实现一个(vue2.0 双向绑定原理分析及简单实现)

vue实现双向绑定原理,自己实现一个(vue2.0 双向绑定原理分析及简单实现)

时间2025-09-16 19:36:41分类IT科技浏览7343
导读:Vue用了有一段时间了,每当有人问到Vue双向绑定是怎么回事的时候,总是不能给大家解释的很清楚,正好最近有时间把它梳理一下,让自己理解的更清楚,下次有人问我的时候,可以侃侃而谈?。...

Vue用了有一段时间了                 ,每当有人问到Vue双向绑定是怎么回事的时候                           ,总是不能给大家解释的很清楚         ,正好最近有时间把它梳理一下                 ,让自己理解的更清楚                          ,下次有人问我的时候         ,可以侃侃而谈?                  。

一                  、首先介绍Object.defineProperty()方法

//直接在一个对象上定义一个新属性         ,或者修改一个已经存在的属性                          , 并返回这个对象 Object.defineProperty(obj,prop,descriptor) 参数 obj 需要定义属性的对象                          。 prop 需被定义或修改的属性名         。 descriptor 需被定义或修改的属性的描述符         。

1.1 属性描述符默认值

属性 默认值 说明 configurable false 描述属性是否可以被删除                  ,默认为 false enumerable false 描述属性是否可以被for...in或Object.keys枚举         ,默认为 false writable false 描述属性是否可以修改                          ,默认为 false get undefined 当访问属性时触发该方法                  ,默认为undefined set undefined 当属性被修改时触发该方法,默认为undefined value undefined 属性值                          ,默认为undefined // Object.defineProperty(对象                           ,属性,属性描述符) var obj={} console.log(obj:,obj); Object.defineProperty(obj, name, { value: James }); console.log(obj的默认值:,obj); delete obj.name; console.log(obj删除后:, obj); console.log(obj枚举:, Object.keys(obj)); obj.name = 库里; console.log(obj修改后:, obj); Object.defineProperty(obj, name, {value: 库里});

运行结果:

从运行结果可以发现                 ,使用Object.defineProperty()定义的属性                           ,默认是不可以被修改         ,不可以被枚举                 ,不可以被删除的                          。可以与常规的方式定义属性对比一下:如果不使用Object.defineProperty()定义的属性                          ,默认是可以修改                          、枚举         、删除的:

const obj = {}; obj.name = James; console.log(枚举:, Object.keys(obj)); obj.name = 库里; console.log(修改:, obj); delete obj.name; console.log(删除:, obj);

运行结果:

1.2 修改属性描述符

const o = {}; Object.defineProperty(o, name, { value: James, // name属性值 writable: true, // 可以被修改 enumerable: true, // 可以被枚举 configurable: true, // 可以被删除 }); console.log(o); console.log(枚举:, Object.keys(o)); o.name = 科比; console.log(修改:, o); Object.defineProperty(o, name, { value: Po }); console.log(修改:, o); delete o.name; console.log(删除:, o);

运行结果:

结果表明         ,修改writable         、enumerable                          、configurable这三个描述符为true时         ,属性可以被修改                 、枚举和删除                 。

注意:

1         、如果writable为false                          ,configurable为true时                  ,通过o.name = "科比"是无法修改成功的         ,但是使用Object.defineProperty()修改是可以成功的

2                           、如果writable和configurable都为false时                          ,如果使用Object.defineProperty()修改属性值会报错:Cannot redefine property: name

1.3 enumerable

const o = {}; Object.defineProperty(o, name, { value: James, enumerable: true }); Object.defineProperty(o, contact, { value: (str) => { return str+ baby }, enumerable: false }); Object.defineProperty(o, age, { value: 18 }); o.skill = 前端; console.log(枚举:, Object.keys(o)); console.log(trim: , o.contact(nihao)) console.log(`o.propertyIsEnumerable(name): `, o.propertyIsEnumerable(name)); console.log(`o.propertyIsEnumerable(contact): `, o.propertyIsEnumerable(contact)); console.log(`o.propertyIsEnumerable(age): `, o.propertyIsEnumerable(age));

运行结果:

1.4 get和set

注:设置set或者get                  ,就不能在设置value和wriable,否则会报错

const o = { __email: }; Object.defineProperty(o, email, { enumerable: true, configurable: true, // writable: true, // 如果设置了get或者set                          ,writable和value属性必须注释掉 // value: , // writable和value无法与set和get共存 get: function () { // 如果设置了get 或者 set 就不能设置writable和value console.log(get, this); return My email is + this.__email; }, set: function (newVal) { console.log(set, newVal); this.__email = newVal; } }); console.log(o); o.email = laowang@163.com; o.email; console.log(o); o.email = laozhang@163.com; console.log(o);

运行结果:

二                 、原理分析

2.1 最简单的双向绑定

<!DOCTYPE html> <head> <title>最简单的双向绑定</title> </head> <body> <div> <input type="text" name="name" id="name" /> </div> </body> <script> var data={ __name: }; Object.defineProperty(data,name,{ enumerable: true, configurable: true, // writable: true, // 如果设置了get或者set                           ,writable和value属性必须注释掉 // value: , // writable和value无法与set和get共存 get: function () { // 如果设置了get 或者 set 就不能设置writable和value return this.__name; }, set: function (newVal) { this.__name=newVal; //更新属性 document.querySelector(#name).value = newVal; //更新视图 } }); //监听input事件,更新name document.querySelector(#name).addEventListener("input",(event)=>{ data.name=event.currentTarget.value }) </script> </html>

运行结果:

文本框输入"老王"                 ,查看name属性变为"老王";修改name属性为"老张"                           ,文本框变为“老张                 ”;

最简单的双向绑定完成了?

2.2 Vue双向绑定

vue.js 则是采用数据劫持结合发布者-订阅者模式的方式         ,通过Object.defineProperty()来劫持各个属性的setter                 ,getter                          ,在数据变动时发布消息给订阅者         ,触发相应的监听回调         。读完这句话是不是还有50%的懵逼         ,接下来继续分析                           。

双向绑定的经典示例图                          ,各位细品:

分析每个模块的作用:

Observer:数据监听器                  ,对每个vue的data中定义的属性循环用Object.defineProperty()实现数据劫持         ,以便利用其中的setter和getter                          ,然后通知订阅者                  ,订阅者会触发它的update方法,对视图进行更新

Compile:指令解析器                          ,对每个元素节点的指令进行扫描和解析                           ,根据指令模板替换数据,以及绑定相应的更新函数

Watcher:作为连接Observer和Compile的桥梁                 ,能够订阅并收到每个属性变动的通知                           ,执行指令绑定的相应回调函数         ,从而更新视图

Dep:依赖收集                 ,每个属性都有一个依赖收集对象                          ,存储订阅该属性的Watcher

Updater:更新视图

结合原理         ,自定义实现Vue的双向绑定

1、首先创建index.html

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>2.0双向绑定原理</title> <script src="https://www.cnblogs.com/lisong/p/Dep.js"></script> <script src="https://www.cnblogs.com/lisong/p/MYVM.js"></script> <script src="https://www.cnblogs.com/lisong/p/Observer.js"></script> <script src="https://www.cnblogs.com/lisong/p/Watcher.js"></script> <script src="https://www.cnblogs.com/lisong/p/TemplateCompiler.js"></script> </head> <body> <div id="app"> <!--模拟vue指令绑定name属性 --> <span v-text="name"></span> <!--模拟vue指令v-model双向绑定 --> <input type="text" v-model="name"> <!-- 模拟{{}} --> {{name}} </div> <script> //假设已经有MYVM对象         ,实例化该对象 //params是一个对象 el是要挂载的dom data是一个对象包含响应式属性 var vm = new MYVM({ el: #app, data: { name: James } }) </script> </body> </html>

2                           、创建MYVM.js                          ,主要作用是调用Observer进行数据劫持和调用TemplateCompiler进行模板解析

function MYVM(options){ //属性初始化 this.$vm=this; this.$el=options.el; this.$data=options.data; //视图必须存在 if(this.$el){ //添加属性观察对象(实现数据挟持) new Observer(this.$data) //创建模板编译器                  ,来解析视图 this.$compiler = new TemplateCompiler(this.$el, this.$vm) } }

3                          、创建Observer.js         ,实现数据劫持

//数据解析                          ,完成对数据属性的劫持 function Observer(data){ //判断data是否有效且data必须是对象 if(!data || typeof data !==object ){ return }else{ var keys=Object.keys(data) keys.forEach((key)=>{ this.defineReactive(data,key,data[key]) }) } } Observer.prototype.defineReactive=function(obj,key,val){ Object.defineProperty(obj,key,{ //是否可遍历 enumerable: true, //是否可删除 configurable: false, //取值 get(){ return val }, //修改值 set(newVal){ val=newVal } }) }

上面代码完成了数据属性的劫持                  ,读取和修改属性会执行get、set,运行结果:

4                  、给Observer.js增加订阅和发布功能                          ,新建Dep.js                           ,进行订阅和发布管理

//创建订阅发布者 //1.管理订阅 //2.集体通知 function Dep(){ this.subs=[]; } //添加订阅 //参数sub是watcher对象 Dep.prototype.addSub=(sub)=>{ this.subs.push(sub) } //集体通知,更新视图 Dep.prototype.notify=()=>{ this.subs.forEach((sub) => { sub.update() }) }

5                          、把Dep安装到Observer.js                 ,代码如下

//数据解析                           ,完成对数据属性的劫持 function Observer(data){ //判断data是否有效且data必须是对象 if(!data || typeof data !==object ){ return }else{ var keys=Object.keys(data) keys.forEach((key)=>{ this.defineReactive(data,key,data[key]) }) } } Observer.prototype.defineReactive=function(obj,key,val){ //创建Dep实例 var dep=new Dep(); Object.defineProperty(obj,key,{ //是否可遍历 enumerable: true, //是否可删除 configurable: false, //取值 get(){ //watcher创建时         ,完成订阅 //检查target是否有watcher                 ,有的话进行订阅 var watcher = Dep.target; watcher && dep.addSub(watcher) return val }, //修改值 set(newVal){ val=newVal dep.notify() } }) }

var dep=new Dep() 创建了Dep的实例

get的时候检查是否有watcher                          ,有就添加到订阅数组

set的时候通知所有的订阅者         ,进行视图更新

至此属性数据劫持         ,订阅和发布就已经实现完了

6         、接下来实现模板编译器                          ,首先创建TemplateCompiler.js

// 创建模板编译工具 // el 要编译的dom节点 // vm MYVM的当前实例 function TemplateCompiler(el,vm){ this.el = this.isElementNode(el) ? el : document.querySelector(el); this.vm = vm; if (this.el) { //将对应范围的html放入内存fragment var fragment = this.node2Fragment(this.el) //编译模板 this.compile(fragment) //将数据放回页面 this.el.appendChild(fragment) } } //是否是元素节点 TemplateCompiler.prototype.isElementNode=function(node){ return node.nodeType===1 } //是否是文本节点 TemplateCompiler.prototype.isTextNode=function(node){ return node.nodeType===3 } //转成数组 TemplateCompiler.prototype.toArray=function(arr){ return [].slice.call(arr) } //判断是否是指令属性 TemplateCompiler.prototype.isDirective=function(directiveName){ return directiveName.indexOf(v-) >= 0; } //读取dom到内存 TemplateCompiler.prototype.node2Fragment=function(node){ var fragment=document.createDocumentFragment(); var child; //while(child=node.firstChild)这行代码                  ,每次运行会把firstChild从node中取出         ,指导取出来是null就终止循环 while(child=node.firstChild){ fragment.appendChild(child) } return fragment; } //编译模板 TemplateCompiler.prototype.compile=function(fragment){ var childNodes = fragment.childNodes; var arr = this.toArray(childNodes); arr.forEach(node => { //判断是否是元素节点 if(this.isElementNode(node)){ this.compileElement(node); }else{ //定义文本表达式验证规则 var textReg = /\{\{(.+)\}\}/; var expr = node.textContent; if (textReg.test(expr)) { //获取绑定的属性 expr = RegExp.$1; //调用方法编译 this.compileText(node, expr) } } }); } //解析元素节点 TemplateCompiler.prototype.compileElement=function(node){ //获取节点所有属性 var arrs=node.attributes; this.toArray(arrs).forEach(attr => { //获取属性名称 var attrName=attr.name; if(this.isDirective(attrName)){ //获取v-modal的modal var type = attrName.split(-)[1] //获取属性对应的值(绑定的属性) var expr = attr.value; CompilerUtils[type] && CompilerUtils[type](node, this.vm, expr) } }); } //解析文本节点 TemplateCompiler.prototype.compileText=function(node,expr){ CompilerUtils.text(node, this.vm, expr) }

TemplateCompiler的主要逻辑:

a                  、dom节点读入到内存

b                          、遍历所有节点                          ,判断节点类型                  ,元素节点和文本节点分别使用不同方法编译

c         、元素节点编译,遍历所有属性                          ,根据指令名称称找到CompilerUtils对应的指令处理方法                           ,执行视图初始化和订阅

d         、文本节点编译,正则匹配找到绑定的属性                 ,使用CompilerUtils的text执行初始化和订阅

7                          、创建CompilerUtils编辑工具对象                           ,实现视图初始化和订阅

//编译工具 CompilerUtils = { //对应视图v-modal指令         ,使用该方法进行视图初始化和订阅 //params node当前节点 vm myvm对象 expr绑定的属性 //modal方法执行一次                 ,进行视图初始化                 、事件订阅                          ,添加视图到模型的事件 model(node, vm, expr) { //节点更新方法 var updateFn = this.updater.modelUpdater; //初始化         ,更新node的值 updateFn && updateFn(node, vm.$data[expr]) //实例化一个订阅者         ,添加到订阅数组 new Watcher(vm, expr, (newValue) => { //发布的时候                          ,按照之前的规则                  ,对节点进行更新 updateFn && updateFn(node, newValue) }) //视图到模型(观察者模式) node.addEventListener(input, (e) => { //获取新值放到模型 var newValue = e.target.value; vm.$data[expr] = newValue; }) }, //对应视图v-text指令         ,使用该方法进行视图初始化和订阅 //params node当前节点 vm myvm对象 expr绑定的属性 //text方法执行一次                          ,进行视图初始化         、事件订阅 text(node, vm, expr) { //text更新方法 var updateFn = this.updater.textUpdater; //初始化                  ,更新text的值 updateFn && updateFn(node, vm.$data[expr]) //实例化一个订阅者,添加到订阅数组 new Watcher(vm, expr, (newValue) => { //发布的时候                          ,按照之前的规则                           ,对文本节点进行更新 updateFn && updateFn(node, newValue) }) }, updater: { //v-text数据更新 textUpdater(node, value) { node.textContent = value; }, //v-model数据更新 modelUpdater(node, value) { node.value = value; } } }

CompilerUtils的主要逻辑:

a                           、根据指令对节点进行数据初始化,实例化观察者Watcher到订阅数组

b                 、不同的指令进行不同的逻辑处理

8、创建Watcher.js                 ,实现订阅者逻辑

//声明一个订阅者 //vm 全局vm对象 //expr 属性名称 //cb 发布时需要执行的方法 function Watcher(vm, expr, cb) { //缓存重要属性 this.vm = vm; this.expr = expr; this.cb = cb; //缓存当前值                           ,为更新时做对比 this.value = this.get() } Watcher.prototype.get=function(){ //设置全局Dep的target为当前订阅者 Dep.target = this; //获取属性的当前值         ,获取时会执行属性的get方法                 ,get方法会判断target是否为空                          ,不为空就添加订阅者 var value = this.vm.$data[this.expr] //清空全局 Dep.target = null; return value; } Watcher.prototype.update=function(){ //获取新值 var newValue = this.vm.$data[this.expr] //获取老值 var old = this.value; //判断后 if (newValue !== old) { //执行回调 this.cb(newValue) } }

Watcher的主要逻辑:

a                           、get 把当前订阅者添加到属性对应的依赖数组         ,保存值

b                          、update 发布的时候执行         ,进行新老值对比                          ,更新节点内容

到此一个简单的MVVM框架就完成了                  ,整体运行效果如下:

梳理过程中参考很多大佬文章         ,感谢各位                 。看完基本能把VUE2.0的双向绑定原理讲清楚了                          ,希望能帮助有缘人                  ,?!

创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!

展开全文READ MORE
文案修改神器免费手机版下载(智能文案改写软件让写作更轻松) 亚马逊aws故障(AmazonAurora的故障检测和自动恢复机制是如何设计的)