vue实现简易的双向数据绑定

Cherise ·
更新时间:2024-09-20
· 848 次阅读

主要是通过数据劫持和发布订阅一起实现的

双向数据绑定 数据更新时,可以更新视图 视图的数据更新是,可以反向更新模型
组成说明
Observe监听器 劫持数据, 感知数据变化, 发出通知给订阅者, 在get中将订阅者添加到订阅器中 Dep消息订阅器 存储订阅者, 通知订阅者调用更新函数 订阅者Wather取出模型值,更新视图 解析器Compile 解析指令, 更新模板数据, 初始化视图, 实例化一个订阅者, 将更新函数绑定到订阅者上, 可以在接收通知二次更新视图, 对于v-model还需要监听input事件,实现视图到模型的数据流动
基本结构
HTML模板   <div id="app">     <form>       <input type="text" v-model="username">     </form>     <p v-bind="username"></p>   </div> 一个根节点#app 表单元素,里面包含input, 使用v-model指令绑定数据username p元素上使用v-bind绑定数username
MyVue类

简单的模拟Vue类

将实例化时的选项options, 数据options.data进行保存 此外,通过options.el获取dom元素,存储到$el上

    class MyVue {       constructor(options) {         this.$options = options         this.$el = document.querySelector(this.$options.el)         this.$data = options.data       }     } 实例化MyVue

实例化一个MyVue,传递选项进去,选项中指定绑定的元素el和数据对象data

    const myVm = new MyVue({       el: '#app',       data: {         username: 'LastStarDust'       }     }) Observe监听器实现

劫持数据是为了修改数据的时候可以感知, 发出通知, 执行更新视图操作

    class MyVue {       constructor(options) {         // ...         // 监视数据的属性         this.observable(this.$data)       }       // 递归遍历数据对象的所有属性, 进行数据属性的劫持 { username: 'LastStarDust' }       observable(obj) {         // obj为空或者不是对象, 不做任何操作         const isEmpty = !obj || typeof obj !== 'object'         if(isEmpty) {           return         }         // ['username']         const keys = Object.keys(obj)         keys.forEach(key => {           // 如果属性值是对象,递归调用           let val = obj[key]           if(typeof val === 'object') {             this.observable(val)           }           // this.defineReactive(this.$data, 'username', 'LastStarDust')           this.defineReactive(obj, key, val)         })         return obj       }       // 数据劫持,修改属性的get和set方法       defineReactive(obj, key, val) {         Object.defineProperty(obj, key, {           enumerable: true,           configurable: true,           get() {             console.log(`取出${key}属性值: 值为${val}`)             return val           },           set(newVal) {             // 没有发生变化, 不做更新             if(newVal === val) {               return             }             console.log(`更新属性${key}的值为: ${newVal}`)             val = newVal           }         })       }     } Dep消息订阅器

存储订阅者, 收到通知时,取出订阅者,调用订阅者的update方法

    // 定义消息订阅器     class Dep {       // 静态属性 Dep.target,这是一个全局唯一 的Watcher,因为在同一时间只能有一个全局的 Watcher       static target = null       constructor() {         // 存储订阅者         this.subs = []       }       // 添加订阅者       add(sub) {         this.subs.push(sub)       }       // 通知       notify() {         this.subs.forEach(sub => {           // 调用订阅者的update方法           sub.update()         })       }     } 将消息订阅器添加到数据劫持过程中

为每一个属性添加订阅者

      defineReactive(obj, key, val) {         const dep = new Dep()         Object.defineProperty(obj, key, {           enumerable: true,           configurable: true,           get() {             // 会在初始化时, 触发属性get()方法,来到这里Dep.target有值,将其作为订阅者存储起来,在触发属性的set()方法时,调用notify方法             if(Dep.target) {               dep.add(Dep.target)             }             console.log(`取出${key}属性值: 值为${val}`)             return val           },           set(newVal) {             // 没有发生变化, 不做更新             if(newVal === val) {               return             }             console.log(`更新属性${key}的值为: ${newVal}`)             val = newVal             dep.notify()           }         })       } 订阅者Wather

从模型中取出数据并更新视图

    // 定义订阅者类     class Wather {       constructor(vm, exp, cb) {         this.vm = vm // vm实例         this.exp = exp // 指令对应的字符串值, 如v-model="username", exp相当于"username"         this.cb = cb // 回到函数 更新视图时调用         this.value = this.get() // 将自己添加到消息订阅器Dep中       }       get() {         // 将当前订阅者作为全局唯一的Wather,添加到Dep.target上         Dep.target = this         // 获取数据,触发属性的getter方法         const value = this.vm.$data[this.exp]         // 在执行添加到消息订阅Dep后, 重置Dep.target         Dep.target = null         return value       }       // 执行更新       update() {         this.run()       }       run() {         // 从Model模型中取出属性值         const newVal = this.vm.$data[this.exp]         const oldVal = this.value         if(newVal === oldVal) {           return false         }         // 执行回调函数, 将vm实例,新值,旧值传递过去         this.cb.call(this.vm, newVal, oldVal)       }     } 解析器Compile
解析模板指令,并替换模板数据,初始化视图; 将模板指令对应的节点绑定对应的更新函数,初始化相应的订阅器; 初始化编译器, 存储el对应的dom元素, 存储vm实例, 调用初始化方法 在初始化方法中, 从根节点开始, 取出根节点的所有子节点, 逐个对节点进行解析 解析节点过程中 解析指令存在, 取出绑定值, 替换模板数据, 完成首次视图的初始化 给指令对应的节点绑定更新函数, 并实例化一个订阅器Wather 对于v-model指令, 监听'input'事件,实现视图更新是,去更新模型的数据     // 定义解析器     // 解析指令,替换模板数据,初始视图     // 模板的指令绑定更新函数, 数据更新时, 更新视图     class Compile {       constructor(el, vm) {         this.el = el         this.vm = vm         this.init(this.el)       }       init(el) {         this.compileEle(el)       }       compileEle(ele) {         const nodes = ele.children // 遍历节点进行解析         for(const node of nodes) { // 如果有子节点,递归调用           if(node.children && node.children.length !== 0) {             this.compileEle(node)           }           // 指令时v-model并且是标签是输入标签           const hasVmodel = node.hasAttribute('v-model')           const isInputTag = ['INPUT', 'TEXTAREA'].indexOf(node.tagName) !== -1           if(hasVmodel && isInputTag) {             const exp = node.getAttribute('v-model')             const val = this.vm.$data[exp]             const attr = 'value'             // 初次模型值推到视图层,初始化视图             this.modelToView(node, val, attr)             // 实例化一个订阅者, 将更新函数绑定到订阅者上, 未来数据更新,可以更新视图             new Wather(this.vm, exp, (newVal)=> {               this.modelToView(node, newVal, attr)             })             // 监听视图的改变             node.addEventListener('input', (e) => {               this.viewToModel(exp, e.target.value)             })           } // 指令时v-bind           if(node.hasAttribute('v-bind')) {             const exp = node.getAttribute('v-bind')             const val = this.vm.$data[exp]             const attr = 'innerHTML'             // 初次模型值推到视图层,初始化视图             this.modelToView(node, val, attr)             // 实例化一个订阅者, 将更新函数绑定到订阅者上, 未来数据更新,可以更新视图             new Wather(this.vm, exp, (newVal)=> {               this.modelToView(node, newVal, attr)             })           }         }       }       // 将模型值更新到视图       modelToView(node, val, attr) {         node[attr] = val       }       // 将视图值更新到模型上       viewToModel(exp, val) {         this.vm.$data[exp] = val       }     } 完整代码 <!DOCTYPE html> <html lang="en"> <head>   <meta charset="UTF-8">   <meta name="viewport" content="width=device-width, initial-scale=1.0">   <title>Document</title> </head> <body>   <div id="app">     <form>       <input type="text" v-model="username">     </form>     <div>       <span v-bind="username"></span>     </div>     <p v-bind="username"></p>   </div>   <script>     class MyVue {       constructor(options) {         this.$options = options         this.$el = document.querySelector(this.$options.el)         this.$data = options.data         // 监视数据的属性         this.observable(this.$data)         // 编译节点         new Compile(this.$el, this)       }       // 递归遍历数据对象的所有属性, 进行数据属性的劫持 { username: 'LastStarDust' }       observable(obj) {         // obj为空或者不是对象, 不做任何操作         const isEmpty = !obj || typeof obj !== 'object'         if(isEmpty) {           return         }         // ['username']         const keys = Object.keys(obj)         keys.forEach(key => {           // 如果属性值是对象,递归调用           let val = obj[key]           if(typeof val === 'object') {             this.observable(val)           }           // this.defineReactive(this.$data, 'username', 'LastStarDust')           this.defineReactive(obj, key, val)         })         return obj       }       // 数据劫持,修改属性的get和set方法       defineReactive(obj, key, val) {         const dep = new Dep()         Object.defineProperty(obj, key, {           enumerable: true,           configurable: true,           get() {             // 会在初始化时, 触发属性get()方法,来到这里Dep.target有值,将其作为订阅者存储起来,在触发属性的set()方法时,调用notify方法             if(Dep.target) {               dep.add(Dep.target)             }             console.log(`取出${key}属性值: 值为${val}`)             return val           },           set(newVal) {             // 没有发生变化, 不做更新             if(newVal === val) {               return             }             console.log(`更新属性${key}的值为: ${newVal}`)             val = newVal             dep.notify()           }         })       }     }     // 定义消息订阅器     class Dep {       // 静态属性 Dep.target,这是一个全局唯一 的Watcher,因为在同一时间只能有一个全局的 Watcher       static target = null       constructor() {         // 存储订阅者         this.subs = []       }       // 添加订阅者       add(sub) {         this.subs.push(sub)       }       // 通知       notify() {         this.subs.forEach(sub => {           // 调用订阅者的update方法           sub.update()         })       }     }     // 定义订阅者类     class Wather {       constructor(vm, exp, cb) {         this.vm = vm // vm实例         this.exp = exp // 指令对应的字符串值, 如v-model="username", exp相当于"username"         this.cb = cb // 回到函数 更新视图时调用         this.value = this.get() // 将自己添加到消息订阅器Dep中       }       get() {         // 将当前订阅者作为全局唯一的Wather,添加到Dep.target上         Dep.target = this         // 获取数据,触发属性的getter方法         const value = this.vm.$data[this.exp]         // 在执行添加到消息订阅Dep后, 重置Dep.target         Dep.target = null         return value       }       // 执行更新       update() {         this.run()       }       run() {         // 从Model模型中取出属性值         const newVal = this.vm.$data[this.exp]         const oldVal = this.value         if(newVal === oldVal) {           return false         }         // 执行回调函数, 将vm实例,新值,旧值传递过去         this.cb.call(this.vm, newVal, oldVal)       }     }     // 定义解析器     // 解析指令,替换模板数据,初始视图     // 模板的指令绑定更新函数, 数据更新时, 更新视图     class Compile {       constructor(el, vm) {         this.el = el         this.vm = vm         this.init(this.el)       }       init(el) {         this.compileEle(el)       }       compileEle(ele) {         const nodes = ele.children         for(const node of nodes) {           if(node.children && node.children.length !== 0) {             // 递归调用, 编译子节点             this.compileEle(node)           }           // 指令时v-model并且是标签是输入标签           const hasVmodel = node.hasAttribute('v-model')           const isInputTag = ['INPUT', 'TEXTAREA'].indexOf(node.tagName) !== -1           if(hasVmodel && isInputTag) {             const exp = node.getAttribute('v-model')             const val = this.vm.$data[exp]             const attr = 'value'             // 初次模型值推到视图层,初始化视图             this.modelToView(node, val, attr)             // 实例化一个订阅者, 将更新函数绑定到订阅者上, 未来数据更新,可以更新视图             new Wather(this.vm, exp, (newVal)=> {               this.modelToView(node, newVal, attr)             })             // 监听视图的改变             node.addEventListener('input', (e) => {               this.viewToModel(exp, e.target.value)             })           }           if(node.hasAttribute('v-bind')) {             const exp = node.getAttribute('v-bind')             const val = this.vm.$data[exp]             const attr = 'innerHTML'             // 初次模型值推到视图层,初始化视图             this.modelToView(node, val, attr)             // 实例化一个订阅者, 将更新函数绑定到订阅者上, 未来数据更新,可以更新视图             new Wather(this.vm, exp, (newVal)=> {               this.modelToView(node, newVal, attr)             })           }         }       }       // 将模型值更新到视图       modelToView(node, val, attr) {         node[attr] = val       }       // 将视图值更新到模型上       viewToModel(exp, val) {         this.vm.$data[exp] = val       }     }     const myVm = new MyVue({       el: '#app',       data: {         username: 'LastStarDust'       }     })     // console.log(Dep.target)   </script> </body> </html>

以上就是vue实现简易的双向数据绑定的详细内容,更多关于vue 实现双向数据绑定的资料请关注软件开发网其它相关文章!



VUE 数据绑定 数据

需要 登录 后方可回复, 如果你还没有账号请 注册新账号