【简易版】如何实现一个简单的双向绑定——Vue2.0 | MVVM | 数据劫持+发布订阅

23 篇文章 1 订阅
4 篇文章 2 订阅

什么是简易版? 众所周知,vue想要实现数据绑定需要有以下几个主要成分:

  • Vue类,负责初始化一些信息
  • Observer,负责订阅data中的各个变量
  • Watcher,负责发布变化,如果数据改动,通知各个HTML节点跟着改动
  • Compiler,负责解析挂载到#app上的HTML节点

理论上这三个就足够了,如果要仅仅实现一个简单的vue。什么是简单的vue?比如数据只有一层,如下所示:

new Vue({
	data: {
		name: "诸葛连弩",
		age: 18
	},
	methods: {
		add () {
			this.age ++;
		}
	}
})

这就是简易版的Vue,它说容易其实也很容易实现,把watcher全部挂到Vue实例上就好了。

什么是困难版的Vue?即数据支持多层嵌套,且加入Dep类。这个Dep类可太tm抽象了,就Dep和Watcher的关系,watcher和dep的来回调用,那够你琢磨两天的了。

这篇文章主要讲述的是简易版的Vue应该如何实现。正式开始


1 从MVC到MVVM

1.1 MVC

View UI布局,展示数据。
Model 管理数据。
Controller 响应用户操作,并将 Model 更新到 View 上。

但这依然有三个痛点

  • 开发者在代码中大量调用相同的 DOM API, 处理繁琐 ,操作冗余,使得代码难以维护。
  • 大量的DOM 操作使页面渲染性能降低,加载速度变慢,影响用户体验。
  • 当 Model 频繁发生变化,开发者需要主动更新到View ;当用户的操作导致 Model 发生变化,开发者同样需要将变化的数据同步到Model 中,这样的工作不仅繁琐,而且很难维护复杂多变的数据状态。

其实,早期jquery 的出现就是为了前端能更简洁的操作DOM 而设计的,但它只解决了第一个问题,另外两个问题始终伴随着前端一直存在。

1.2 MVVM 的出现,完美解决以上三个问题

MVVM 由 Model,View,ViewModel 三部分构成,Model 层代表数据模型,也可以在Model中定义数据修改和操作的业务逻辑;View 代表UI 组件,它负责将数据模型转化成UI 展现出来,ViewModel 是一个同步View 和 Model的对象。

在MVVM架构下,View 和 Model 之间并没有直接的联系,而是通过ViewModel进行交互,Model 和 ViewModel 之间的交互是双向的, 因此View 数据的变化会同步到Model中,而Model 数据的变化也会立即反应到View 上。

ViewModel 通过双向数据绑定把 View 层和 Model 层连接了起来,而View 和 Model 之间的同步工作完全是自动的,无需人为干涉,因此开发者只需关注业务逻辑,不需要手动操作DOM, 不需要关注数据状态的同步问题,复杂的数据状态维护完全由 MVVM 来统一管理。

参考资料: 一像素的博客


2 数据绑定

*重要补充

网上对vue的简单实现各不统一,可以说水平参差不齐,因此,这里强烈推荐阅读Vue2.6的源码,还是比较平易近人的,但是有的地方还是比较难理解,比如Dep类和Watcher类的关系,以及Dep.target的作用,以及addDep到底是在干嘛。比较好的参考资料有:

  1. 重点源码分析,observer、dep、compiler、watcher等
  2. 依赖收集(讲解Dep和Watcher的关系)
  3. 好的文章1,其实看这篇文章就够了,比下面那篇更清楚
  4. 好的文章2
  5. 究极之更好的文章

在Vue3.0到来之前,抓紧补一下Vue2的知识——数据绑定。

先来看一下当前(2020年)主流MVVM框架的实现原理:

  1. 发布-订阅者模式(backbone.js) 最早的mvvm框架
    一般通过pub、sub的方式来实现数据和视图的绑定 使用起来比较麻烦。

  2. 脏值检查(angular.js)
    用定时器轮训检测数据是否发生改变,性能低。

  3. (vue用什么方式实现的数据绑定?)
    数据劫持 Object.defineProperty给对象的属性增加修饰符来劫持各个属性的setter getter(获取值和设置值的时候 你都能知道然后就可以修改页面的视图了) 此外还结合了发布订阅模式 把所有订阅 指定 统一做更新的处理。IE8以下不支持Object.defineProperty这个属性的 所以vue只兼容到ie9。

2.1 我的理解

首先要知道,vue的数据双向绑定原理是“数据劫持” + “发布、订阅”。

数据改变只发生在以下两种情况:

  • View层先改变,体现在用户手动改变网页中的元素,可以通过监听HTML事件来改变数据层
  • Model层先改变,体现在按照业务需求改变Js中的数据(例如Ajax向后台通信拿到新的数据),可以通过Object.defineProperty来劫持数据的变化,监听者察觉到数据变化后把变化通知给发布者,再由发布者去更新网页中的数据,然后再释放变化。

因此,我们的简单实现需要以下几个模块:

  1. observer,负责监听js中数据的变化,其实就是监听Vue.$data中的数据,如果发现数据变化通知发布者改变网页内容。
  2. watcher,负责发布页面变化。
  3. compiler,这是个隐藏的,虽然上面没有提到他,但它可以说是很重要的一环,它负责连接Model和View,它是一个模板解析器,负责处理拥有Vue特性的HTML元素,它把每一个需要特殊处理的HTMLElement元素都记录了下来,并保存为watcher,比如拥有v-modelinput组件,v-on:click的按钮,v-bindp标签。当发布者watcher要去更新视图的时候,直接调用自身的upload函数即可。

2.2 先看看myVue长什么样子

// myVue实例
window.myapp = new myVue({
    el: "#app",
    data: {
        number: 0,
        number2: 1
    },
    methods: {
        increment () {
            this.number ++;
        },
        increment2 () {
            this.number2 ++;
        }
    }
});

并且准备一个myVue对象

// myVue对象
function myVue(options = {}) {
    this.$options = options;
    // 挂载的html dom元素
    this.$el = document.querySelector(options.el);
    // 初始化数据
    this.$_data = options.data;
    this.$_methods = options.methods;
    // 一个保存发布者watchers的队列
    this._binding = {};
    // 开始订阅(监听)$_data对象
    this._observe(this.$_data);
    // 解析vue模板
    this._compile(this.$el);
}

2.3 创建订阅者

然后实现_observe函数

myVue.prototype._observe = function (obj) {
	// 非空且是一个对象才会被订阅
    if (!obj || typeof obj !== "object") {
        return;
    }
    // 遍历每一个key
    Object.keys(obj).forEach( key => {
    	// 一定要先把value保存出来,不然每一次访问obj都会触发get函数
        let value = obj[key];
        // 递归订阅该value
        this._observe(value);
        // 对每一个key创建一个指令池,里面存放的是发布者watchers
        this._binding[key] = {
            _directives: []
        }
        // 取出当前key的指令池
        let binding = this._binding[key];
        // 开始劫持
        Object.defineProperty(obj, key, {
            get () {
                console.log(`获取`);
                return value;
            },
            set (newVal) {
                console.log(`更新${newVal}`);
                if (value !== newVal) {
                    value = newVal;
                    // 如果数据变化,通知涉及该key的全部发布者,并让他们更新页面
                    // 当然,目前还没有发布者,指令池是空的
                    binding._directives.forEach(item => item.Update());
                }
            }
        })
    })
}

2.4 编写模板编译器

myVue.prototype._compile = function (root) { // root等于要挂载的html元素<div id="app"></div>对应的dom节点
    let _this = this;
    let nodes = root.children;
    // 从根节点开始,解析html文件
    for (let i=0; i<nodes.length; i++) {
        let node = nodes[i];
        // 如果root的子元素还有子元素,那么递归解析
        if (node.children.length !== 0) {
            this._compile(node);
        }

        // 解析v-click指令,注意这里要用闭包
        if (node.hasAttribute("v-click")) {
        	// 为html元素绑定click事件
            node.onclick = (function () {
            	// 获取html中定义的函数名
                let attrVal = node.getAttribute("v-click");
                // 使this指向一致
                return _this.$_methods[attrVal].bind(_this.$_data);
            })();
        }

        // 解析v-model指令,闭包同理
        if (node.hasAttribute("v-model") &&
            (node.tagName === "INPUT" || node.tagName === "TEXTAREA")
        ) {
        	// 为input\textarea元素添加事件监听
            node.addEventListener("input", (function (index) {
                let attrVal = node.getAttribute("v-model");
                // 核心!为v-model指定的变量,添加发布者watcher,并保存到该变量的指令池
                // 并且在其中记录是哪个HTML元素对应该变量,并记录如果要修改内容,对应的是该HTML元素的那个属性
                // 比如<input />对应的就是value属性
                _this._binding[attrVal]._directives.push(new Watcher(
                    "input",
                    node, // HTML元素
                    _this, // myVue实例
                    attrVal, // 对应的myVue.$_data中的哪个变量
                    "value" // 比如<input />对应的就是value属性
                ))
                return function () {
                	// 这才是input真正触发的事件,把value赋值给$_data
                	// 这里有问题吧,这只能一层data
                    _this.$_data[attrVal] = nodes[index].value;
                }
            })(i))
        }

        // v-bind,同上
        if (node.hasAttribute("v-bind")) {
            let attrVal = node.getAttribute("v-bind");
            _this._binding[attrVal]._directives.push(new Watcher(
                "text",
                node,
                _this,
                attrVal,
                "innerHTML"
            ))
        }
    }
}

2.5 编写发布者

// 构造函数
function Watcher (name, el, vm, exp, attr) {
    this.name = name;
    this.el = el;
    this.vm = vm;
    this.exp = exp;
    this.attr = attr;
    this.Update();
}
// 发布更新
Watcher.prototype.Update = function () {
    this.el[this.attr] = this.vm.$_data[this.exp];
}

完整代码

上源码:一共不到150行,其中主要是参考了呆头呆脑丶的文章进行学习,感谢这些热爱分享的人。当然不可能照搬,结合ES6进行了一些小修改。

// index.html
<!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="number">
            <button type="button" v-click="increment">add one</button>
        </form>
        <h3 v-bind="number"></h3>
        <form>
            <input type="text" v-model="number2">
            <button type="button" v-click="increment2">add one</button>
        </form>
        <h3 v-bind="number2"></h3>
    </div>
    <script type="module" src="./index.js"></script>
</body>
</html>
// index.js
import myVue from "./myVue.js";

window.myapp = new myVue({
    el: "#app",
    data: {
        number: 0,
        number2: 1
    },
    methods: {
        increment () {
            this.number ++;
        },
        increment2 () {
            this.number2 ++;
        }
    }
});
// myVue.js
function myVue(options = {}) {
    this.$options = options;
    this.$el = document.querySelector(options.el);
    this.$_data = options.data;
    this.$_methods = options.methods;
    this._binding = {};
    this._observe(this.$_data);
    this._compile(this.$el);
}

myVue.prototype._compile = function (root) {
    let _this = this;
    let nodes = root.children;
    for (let i=0; i<nodes.length; i++) {
        let node = nodes[i];
        if (node.children.length !== 0) {
            this._compile(node);
        }

        // 解析v-click
        if (node.hasAttribute("v-click")) {
            node.onclick = (function () {
                let attrVal = node.getAttribute("v-click");
                return _this.$_methods[attrVal].bind(_this.$_data); // 这里不是很理解
            })();
        }

        // 解析v-model
        if (node.hasAttribute("v-model") &&
            (node.tagName === "INPUT" || node.tagName === "TEXTAREA")
        ) {
            node.addEventListener("input", (function (index) {
                let attrVal = node.getAttribute("v-model");
                _this._binding[attrVal]._directives.push(new Watcher(
                    "input",
                    node,
                    _this,
                    attrVal,
                    "value"
                ))
                return function () {
                    _this.$_data[attrVal] = nodes[index].value;
                }
            })(i))
        }

        // 解析v-bind
        if (node.hasAttribute("v-bind")) {
            let attrVal = node.getAttribute("v-bind");
            _this._binding[attrVal]._directives.push(new Watcher(
                "text",
                node,
                _this,
                attrVal,
                "innerHTML"
            ))
        }
    }
}

myVue.prototype._observe = function (obj) {
    if (!obj || typeof obj !== "object") {
        return;
    }
    Object.keys(obj).forEach( key => {
        let value = obj[key];
        this._observe(value);
        this._binding[key] = {
            _directives: []
        }
        let binding = this._binding[key];
        Object.defineProperty(obj, key, {
            get () {
                console.log(`获取`);
                return value;
            },
            set (newVal) {
                console.log(`更新${newVal}`);
                if (value !== newVal) {
                    value = newVal;
                    binding._directives.forEach(item => item.Update());
                }
            }
        })
    })

}

function Watcher (name, el, vm, exp, attr) {
    this.name = name;
    this.el = el;
    this.vm = vm;
    this.exp = exp;
    this.attr = attr;
    this.Update();
}

Watcher.prototype.Update = function () {
    this.el[this.attr] = this.vm.$_data[this.exp];
}

export default myVue;
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
MVVM是Model-View-ViewModel的简写,它是MVC的改进版。MVVM分为三个部分,即模型层(Model)、视图层(View)和连接桥梁(ViewModel)。模型层主要负责业务数据相关,视图层负责展示视图,而ViewModel作为连接桥梁,负责监听模型和视图的变化,实现双向绑定的功能。 使用MVVM的好处在于将视图的状态和行为抽象化,实现视图UI与业务逻辑的分离。MVVM支持双向绑定,当模型层数据发生修改时,ViewModel会察觉变化并通知视图层进行相应的修改;反之,当视图层发生修改时,ViewModel也会通知模型层进行数据的修改,实现视图与模型层的解耦。 关于Vue中的MVVM实现原理,具体可以参考Vue的源码实现。根据引用中的代码,可以看出,在Vue中,创建一个MVVM实例时,会将数据对象传入,并通过observe方法进行监听。同时,Vue还会创建一个Compile实例来编译模板,并将MVVM实例作为参数传入。然后,通过数据劫持和模板编译的机制,最终实现MVVM双向绑定功能。 总结来说,VueMVVM原理主要是通过数据劫持和模板编译来实现的,数据劫持负责监听数据的变化,模板编译负责将数据和视图进行绑定。这样就实现数据双向绑定,当数据变化时,视图会自动更新,并且当视图发生变化时,数据也会相应地更新。<span class="em">1</span><span class="em">2</span><span class="em">3</span><span class="em">4</span>

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值