当前位置: 首页 > news >正文

使用proxy实现一个双向绑定

 

如何实现

  • 在学习vue的时候,vue是通过劫持数据的变化,监听到数据变化时改变前端视图。
  • 那么要实现双向绑定,必然需要一个监听数据的方法。如文章标题所示,这里使用的 proxy实现数据的监听。
  • 当监听到数据变化时,需要一个watcher响应并调用更新数据的compile方法去更新前端视图。
  • 在vue中 v-model 作为绑定的入口。当我们监听到前端input输入信息并绑定了数据项的时候,需要先告知watcher,由watcher改变监听器的数据。
  • 大概的双向绑定的原理为:

1.实现一个observer(数据监听器)

利用 proxy 实现一个数据监听器很简单,因为 proxy 是监听整个对象的变化的,所以可以这样写:

class VM {
        constructor(options, elementId) {
            this.data = options.data || {}; // 监听的数据对象
            this.el = document.querySelector(elementId);
            this.init(); // 初始化
        }
        
        // 初始化
        init() {
            this.observer();
        }
        
        // 监听数据变化方法
        observer() {
            const handler = {
                get: (target, propkey) => {
                    console.log(`监听到${propkey}被取啦,值为:${target[propkey]}`);
                    return target[propkey];
                },
                set: (target, propkey, value) => {
                    if(target[propkey] !== value){
                        console.log(`监听到${propkey}变化啦,值变为:${value}`);
                    }
                    return true;
                }
            };
            this.data = new Proxy(this.data, handler);
        }
    }
    
    // 测试一下
    const vm = new VM({
        data: {
            name: 'defaultName',
            test: 'defaultTest',
        },
    }, '#app');
    
    vm.data.name = 'changeName'; // 监听到name变化啦,值变为:changeName
    vm.data.test = 'changeTest'; // 监听到test变化啦,值变为:changeTest
    
    vm.data.name; // 监听到name被取啦,值为:changeName 
    vm.data.test; // 监听到test被取啦,值为:changeTest
复制代码

这样,数据监听器已经基本实现了,但是现在这样只能监听到数据的变化,不能改变前端的视图信息。现在需要实现一个更改前端信息的方法,在VM类中添加方法 changeElementData

// 改变前端数据
    changeElementData(value) {
        this.el.innerHTML = value;
    }
复制代码

在监听到数据变化时调用 changeElementData 改变前端数据, handler 的 set 方法中调用方法

set(target, propkey, value) {
        this.changeElementData(value);
        return true;
    }
复制代码

在init中设置一个定时器更改数据

init() {
        this.observer();
        
        setTimeout(() => {
            console.log('change data !!');
            this.data.name = 'hello world';
        }, 1000)
    }
复制代码

已经可以看到监听到的信息改变到前端了,但是!

这样写死的绑定数据显然是没有意义,现在实现的逻辑大概如下面的图

2.实现数据动态更新到前端

上面实现了一个简单的数据绑定展示,但是只能绑定一个指定的节点去改变此节点的数据绑定。这样显然是不能满足的,我们知道vue中是以 {{key}} 这样的形式去绑定展示的数据的,而且vue中是监听指定的节点的所有子节点的。因此对象中需要在 VIEW 和 OBSERVER 之间添加一个监听层 WATCHER 。当监听到数据发生变化时,通过 WATCHER 去改变 VIEW ,如图:

根据这个流程,下一步我们需要做的是:

{{text}}

在VM类的构造器中添加三个参数

constructor() {
        this.fragment = null; // 文档片段
        this.matchModuleReg = new RegExp('\{\{\s*.*?\s*\}\}', 'gi'); // 匹配所有{{}}模版
        this.nodeArr = []; // 所有带有模板的前端结点
    }
复制代码

新建一个方法遍历 el 中的所有节点,并存放到 fragment 中

/**
     *  创建一个文档片段
     */
    createDocumentFragment() {
        let fragment = document.createDocumentFragment();
        let child = this.el.firstChild;
        // 循环添加到文档片段中
        while (child) {
            this.fragment.appendChild(child);
            child = this.el.firstChild;
        }
        this.fragment = fragment;
    }
复制代码

匹配 {{}} 的数据并替换模版

/**
     *  匹配模板
     *  @param { string } key 触发更新的key
     *  @param { documentElement } fragment 结点
     */
    matchElementModule(key, fragment) {
        const childNodes = fragment || this.fragment.childNodes;
        [].slice.call(childNodes).forEach((node) => {
            if (node.nodeType === 3 && this.matchModuleReg.test(node.textContent)) {
                node.defaultContent = node.textContent; // 将初始化的前端内容保存到节点的defaultContent中
                this.changeData(node);
                this.nodeArr.push(node); // 保存带有模板的结点
            }

            // 递归遍历子节点
            if(node.childNodes && node.childNodes.length) {
                this.matchElementModule(key, node.childNodes);
            }
        })
    }
    
    /**
     * 改变视图数据
     * @param { documentElement } node
     */
    changeData(node) {
        const matchArr = node.defaultContent.match(this.matchModuleReg); // 获取所有需要匹配的模板
        let tmpStr = node.defaultContent;
        for(const key of matchArr) {
            tmpStr = tmpStr.replace(key, this.data[key.replace(/\{\{|\}\}|\s*/g, '')] || '');
        }
        node.textContent = tmpStr;
    }
复制代码

实现watcher,数据变化是触发此watcher更新前端

watcher(key) {
        for(const node of this.nodeArr) {
            this.changeData(node);
        }
    }
复制代码

在 init 和 proxy 的 set 方法中执行新增的方法

init() {
        this.observer();
        this.createDocumentFragment(this.el); // 将绑定的节点都放入文档片段中
        for (const key of Object.keys(this.data)) {
            this.matchElementModule(key);
        }
        this.el.appendChild(this.fragment); // 将初始化的数据输出到前端
    }
    
    set: () => {
        if(target[propkey] !== value) {
            target[propkey] = value;
            this.watcher(propkey);
        }
        return true;
    }
复制代码

测试一下:

3.实现数据双向绑定

现在我们的程序已经可以通过改变data动态地改变前端的展示了,接下来需要实现的是一个类似VUE v-model 绑定input的方法,通过input输入动态地将输入的信息输出到对应的前端模板上。大概的流程图如下:

一个简单的实现流程大概如下:

  1. 获取所有带有v-model的input结点
  2. 监听输入的信息并设置到对应的data中

在constructor中添加

constructor() {
        this.modelObj = {};
    }
    
复制代码

在VM类中新增方法

// 绑定 y-model
    bindModelData(key, node) {
        if (this.data[key]) {
            node.addEventListener('input', (e) => {
                this.data[key] = e.target.value;
            }, false);
        }
    }
    
    // 设置 y-model 值
    setModelData(key, node) {
        node.value = this.data[key];
    }

    // 检查y-model属性
    checkAttribute(node) {
        return node.getAttribute('y-model');
    }
复制代码

在 watcher 中执行 setModelData 方法, matchElementModule 中执行 bindModelData 方法。

修改后的 matchElementModule 和 watcher 方法如下

matchElementModule(key, fragment) {
        const childNodes = fragment || this.fragment.childNodes;
        [].slice.call(childNodes).forEach((node) => {

            // 监听所有带有y-model的结点
            if (node.getAttribute && this.checkAttribute(node)) {
                const tmpAttribute = this.checkAttribute(node);
                if(!this.modelObj[tmpAttribute]) {
                    this.modelObj[tmpAttribute] = [];
                };
                this.modelObj[tmpAttribute].push(node);
                this.setModelData(tmpAttribute, node);
                this.bindModelData(tmpAttribute, node);
            }

            // 保存所有带有{{}}模版的结点
            if (node.nodeType === 3 && this.matchModuleReg.test(node.textContent)) {
                node.defaultContent = node.textContent; // 将初始化的前端内容保存到节点的defaultContent中
                this.changeData(node);
                this.nodeArr.push(node); // 保存带有模板的结点
            }

            // 递归遍历子节点
            if(node.childNodes && node.childNodes.length) {
                this.matchElementModule(key, node.childNodes);
            }
        })
    }
    
    watcher(key) {
        if (this.modelObj[key]) {
            this.modelObj[key].forEach(node => {
                this.setModelData(key, node);
            })
        }
        for(const node of this.nodeArr) {
            this.changeData(node);
        }
    }
复制代码

来看一下是否已经成功绑定了,写一下测试代码:

成功!!

最终的代码如下:

class VM {
            constructor(options, elementId) {
                this.data = options.data || {}; // 监听的数据对象
                this.el = document.querySelector(elementId);
                this.fragment = null; // 文档片段
                this.matchModuleReg = new RegExp('\{\{\s*.*?\s*\}\}', 'gi'); // 匹配所有{{}}模版
                this.nodeArr = []; // 所有带有模板的前端结点
                this.modelObj = {}; // 绑定y-model的对象
                this.init(); // 初始化
            }

            // 初始化
            init() {
                this.observer();
                this.createDocumentFragment();
                for (const key of Object.keys(this.data)) {
                    this.matchElementModule(key);
                }
                this.el.appendChild(this.fragment);
            }

            // 监听数据变化方法
            observer() {
                const handler = {
                    get: (target, propkey) => {
                        return target[propkey];
                    },
                    set: (target, propkey, value) => {
                        if(target[propkey] !== value) {
                            target[propkey] = value;
                            this.watcher(propkey);
                        }
                        return true;
                    }
                };
                this.data = new Proxy(this.data, handler);
            }

            /**
             *  创建一个文档片段
             */
             createDocumentFragment() {
                let documentFragment = document.createDocumentFragment();
                let child = this.el.firstChild;
                // 循环向文档片段添加节点
                while (child) {
                    documentFragment.appendChild(child);
                    
                    child = this.el.firstChild;
                }
                this.fragment = documentFragment;
            }

            /**
            *  匹配模板
            *  @param { string } key 触发更新的key
            *  @param { documentElement } fragment 结点
            */
            matchElementModule(key, fragment) {
                const childNodes = fragment || this.fragment.childNodes;
                [].slice.call(childNodes).forEach((node) => {

                    // 监听所有带有y-model的结点
                    if (node.getAttribute && this.checkAttribute(node)) {
                        const tmpAttribute = this.checkAttribute(node);
                        if(!this.modelObj[tmpAttribute]) {
                            this.modelObj[tmpAttribute] = [];
                        };
                        this.modelObj[tmpAttribute].push(node);
                        this.setModelData(tmpAttribute, node);
                        this.bindModelData(tmpAttribute, node);
                    }

                    // 保存所有带有{{}}模版的结点
                    if (node.nodeType === 3 && this.matchModuleReg.test(node.textContent)) {
                        node.defaultContent = node.textContent; // 将初始化的前端内容保存到节点的defaultContent中
                        this.changeData(node);
                        this.nodeArr.push(node); // 保存带有模板的结点
                    }

                    // 递归遍历子节点
                    if(node.childNodes && node.childNodes.length) {
                        this.matchElementModule(key, node.childNodes);
                    }
                })
            }
            
            /**
             * 改变视图数据
             * @param { documentElement } node
             */
            changeData(node) {
                const matchArr = node.defaultContent.match(this.matchModuleReg); // 获取所有需要匹配的模板
                let tmpStr = node.defaultContent;
                for(const key of matchArr) {
                    tmpStr = tmpStr.replace(key, this.data[key.replace(/\{\{|\}\}|\s*/g, '')] || '');
                }
                node.textContent = tmpStr;
            }

            watcher(key) {
                if (this.modelObj[key]) {
                    this.modelObj[key].forEach(node => {
                        this.setModelData(key, node);
                    })
                }
                for(const node of this.nodeArr) {
                    this.changeData(node);
                }
            }

            // 绑定 y-model
            bindModelData(key, node) {
                if (this.data[key]) {
                    node.addEventListener('input', (e) => {
                        this.data[key] = e.target.value;
                    }, false);
                }
            }
            
            // 设置 y-model 值
            setModelData(key, node) {
                node.value = this.data[key];
            }

            // 检查y-model属性
            checkAttribute(node) {
                return node.getAttribute('y-model');
            }
        }
复制代码

最后

本节我们使用 Proxy ,从监听器开始,到观察者一步步实现了一个模仿VUE的双向绑定,代码中也许会有很多写的不严谨的地方,如发现错误麻烦大佬们指出~~

相关文章:

  • 最全的 Vue 性能优化指南,经典收藏
  • web前端开发不可不知的十个小妙招,让工作更有效率,快收藏吧
  • 翻译:原型继承是如何工作的
  • 35岁并不是程序员的坎,只是你没有真的认清事实
  • Web安全的重要性(面试必备),被黑无数次还不怕吗?
  • 学前端,css与javascript是重难点,基础不好一切白费!
  • Web前端开发应该必备的编码原则
  • vue全家桶开发 去哪儿 项目总结
  • 这「五类人」最适合转Web前端,必须要了解的前端工程师
  • 每周分享,前端自学「书籍推荐」
  • 「程序员之路」年轻,总得做些什么吧(致那些还未定型的程序员)
  • 自学入门,省去几万学费,web前端必须要知道的「基础知识」
  • 身为前端,你不得不懂的一些HTTP知识(附赠3道面试题)
  • web前端30个项目列表,学完即可上手做项目
  • 还在羡慕程序员工资高吗?看完这篇前端学习计划,你也可以拿高薪
  • Javascript Math对象和Date对象常用方法详解
  • MySQL的数据类型
  • oschina
  • tab.js分享及浏览器兼容性问题汇总
  • ucore操作系统实验笔记 - 重新理解中断
  • web标准化(下)
  • 聊聊hikari连接池的leakDetectionThreshold
  • 使用API自动生成工具优化前端工作流
  • 世界编程语言排行榜2008年06月(ActionScript 挺进20强)
  • 限制Java线程池运行线程以及等待线程数量的策略
  • 3月7日云栖精选夜读 | RSA 2019安全大会:企业资产管理成行业新风向标,云上安全占绝对优势 ...
  • 选择阿里云数据库HBase版十大理由
  • ​云纳万物 · 数皆有言|2021 七牛云战略发布会启幕,邀您赴约
  • (1)(1.13) SiK无线电高级配置(六)
  • (1)bark-ml
  • (6)【Python/机器学习/深度学习】Machine-Learning模型与算法应用—使用Adaboost建模及工作环境下的数据分析整理
  • (html5)在移动端input输入搜索项后 输入法下面为什么不想百度那样出现前往? 而我的出现的是换行...
  • (超详细)2-YOLOV5改进-添加SimAM注意力机制
  • (附源码)springboot电竞专题网站 毕业设计 641314
  • (附源码)springboot学生选课系统 毕业设计 612555
  • (附源码)计算机毕业设计SSM保险客户管理系统
  • (力扣题库)跳跃游戏II(c++)
  • (六)什么是Vite——热更新时vite、webpack做了什么
  • (十五)Flask覆写wsgi_app函数实现自定义中间件
  • (一)Spring Cloud 直击微服务作用、架构应用、hystrix降级
  • (原創) 如何動態建立二維陣列(多維陣列)? (.NET) (C#)
  • (原創) 如何解决make kernel时『clock skew detected』的warning? (OS) (Linux)
  • .NET 3.0 Framework已经被添加到WindowUpdate
  • .Net 4.0并行库实用性演练
  • .net core 控制台应用程序读取配置文件app.config
  • .NET Core实战项目之CMS 第一章 入门篇-开篇及总体规划
  • .net Stream篇(六)
  • .NET/C# 编译期能确定的字符串会在字符串暂存池中不会被 GC 垃圾回收掉
  • .NET国产化改造探索(三)、银河麒麟安装.NET 8环境
  • .vue文件怎么使用_vue调试工具vue-devtools的安装
  • ?
  • @KafkaListener注解详解(一)| 常用参数详解
  • @Transactional 竟也能解决分布式事务?
  • @Transient注解
  • [ NOI 2001 ] 食物链