0
点赞
收藏
分享

微信扫一扫

Vue--双向绑定的原理--手写双向绑定(模仿vue)


简介

说明

        本文用示例介绍Vue的双向绑定的原理(实现方式)。

什么是双向绑定

        双向绑定:数据和视图任意一个变化,则另一个也跟着变化。也就是:数据变化更新视图,视图变化更新数据。

Vue是MVVM模型。数据属于Model,视图属于View。本文要介绍的双向绑定就是:VM(ViewModel)。

MVVM表示的是 Model-View-ViewModel


  • Model:模型层。负责处理业务逻辑以及和服务器端进行交互
  • View:视图层:负责将数据模型转化为UI展示出来,可以简单理解为HTML页面
  • ViewModel:视图模型层。用来连接Model和View,是Model和View之间的通信桥梁

Vue双向绑定的实现方案

官网

​​深入响应式原理 — Vue.js​​

简介

Vue是通过数据劫持和观察者模式(订阅者模式)来实现双向绑定的。

数据劫持

观察者模式

目的


监视数据的变化。

用于数据变化时更新视图。



监视视图的变化。

用于视图变化时更新数据。


实现方案


Vue2: Object.defineProperty()

Vue3: Proxy(ES6的新特性)


对每个元素节点进行扫描和解析并绑定更新函数。

Object.defineProperty

优点

  1. 兼容性好。(ES5就已支持)

缺点


  1. 不能监听数组;
  1. 因为数组没有getter和setter。(数组长度不确定,如果太长性能负担太大)
  1. 只能监听属性,而不是整个对象;需要遍历属性;
  2. 只能监听属性变化(set、get),不能监听属性的删减;

Proxy

优点


  1. 可以直接监听数组的变化
  2. 可以直接监听整个对象,,而非是对象的某个属性
  3. 拦截方法丰富: 多达13种, 不限于 get、set、deleteProperty、has等。

缺点

  1. 兼容性差。(ES6的新特性)

详述

双向绑定需要三个部分:监听器(Observer),解析器(Compile),订阅者(Watcher)。


  • 监听器(Observer)

  • 监听数据的变化。
  • 对数据的属性进行递归遍历,都加上setter和getter。给数据赋值时,会触发setter;读数据时,会触发getter。

  • 解析器(Compile)

  • 作用1:初始化页面。
  • 对每个元素节点进行扫描和解析,利用正则将页面中的"{{xxx}}"替换成data中对应的数据
  • 作用2:监听视图的变动。
  • 将每个指令对应的节点绑定更新函数,添加订阅者。一旦订阅者发出通知,就执行相应的更新函数。

  • 订阅者(Watcher)

  • 连接Observer和Compile 之间的桥梁。
  • 通过Observer监听数据变化,更新视图。
  • 通过Compile监听视图变化,更新数据。


手写双向绑定(简单实现)

需求:将输入的内容(“视图”内容)保存到对象(“数据”内容)中,对象保存时,同时更新<div>的内容(“视图”内容)。

代码

<!doctype html>
<html lang="en">

<head>
<meta charset="UTF-8">
<title>This is title</title>
</head>

<body>

<input type="text" id="id-input"><br/>
<div id="id-div"></div>

<script>
let user = {};

let input = document.querySelector("#id-input");
let text = document.querySelector("#id-div");

// 数据到视图 model => view
Object.defineProperty(user,"name",{
get:function(){
console.log('获取user的name');
return this._name;
},
set:function(val){
console.log('修改user的name为:' + val);
this._name = val;
text.textContent = this._name;
}
})

// 视图到数据 view => model
input.addEventListener('input',function(e){
user.name = e.target.value;
console.log("-----------------------")
console.log("监听器中将user的name设置为:" + user.name);
})
</script>

</body>
</html>

测试

手写双向绑定(模仿vue2)

代码

<!doctype html>
<html lang="en">

<head>
<meta charset="UTF-8">
<title>This is title</title>
</head>

<body>

<!-- 实现vue -->
<div id="app">
<input type="text" v-model="text">
{{ text }}
</div>

<script type="text/javascript">

function defineReactive(obj, key, value) {
let dep = new Dep();
Object.defineProperty(obj, key, {
get: function () {
if (Dep.target) {
dep.addSub(Dep.target);
}
return value
},
set: function (newVal) {
// 数据没有改变则不需要通知
if (newVal === value) {
return
}
value = newVal;
console.log('新值:' + value);

// 数据改变,通知订阅者
dep.notify();
}
})
}

// 观察者函数
function observe(obj, vm) {
for (let key of Object.keys(obj)) {
defineReactive(vm, key, obj[key]);
}
}

/*编译函数*/
function compile(node, vm) {
let reg = /\{\{(.*)\}\}/; // 来匹配 {{ xxx }} 中的xxx
// 如果是元素节点
if (node.nodeType === 1) {
let attr = node.attributes;
// 解析元素节点的所有属性
for (let i = 0; i < attr.length; i++) {
if (attr[i].nodeName === 'v-model') {
let name = attr[i].nodeValue; // 看看是与哪一个数据相关
node.addEventListener('input', function (e) {
vm[name] = e.target.value; // 将实例的text 修改为最新值
});
node.value = vm[name]; // 将data的值赋给该node
node.removeAttribute('v-model');
}
}
}
// 如果是文本节点
if (node.nodeType === 3) {
if (reg.test(node.nodeValue)) {
let name = RegExp.$1; // 获取到匹配的字符串
name = name.trim();
// node.nodeValue = vm[name]; // 将data的值赋给该node

new Watcher(vm, node, name); // 不直接通过赋值的操作,而是通过绑定一个订阅者
}
}
}

// Watcher构造函数
function Watcher(vm, node, name) {
Dep.target = this; // Dep.target 是一个全局变量
this.vm = vm;
this.node = node;
this.name = name;
this.update();
Dep.target = null;
}
Watcher.prototype = {
update() {
this.get();
this.node.nodeValue = this.value; // 注意,这是更改节点内容的关键
},
get() {
this.value = this.vm[this.name]; // 触发相应的get
}
}

// dep构造函数
function Dep() {
this.subs = [];
}
Dep.prototype = {
addSub(sub) {
this.subs.push(sub);
},
notify() {
this.subs.forEach(function (sub) {
sub.update();
})
}
}


function nodeToFragment(node, vm) {
let fragment = document.createDocumentFragment();
let child;
while (child = node.firstChild) {
compile(child, vm);
fragment.appendChild(child);
}
return fragment
}

// Vue构造函数
function MyVue(options) {
this.data = options.data;
let data = this.data;

observe(data, this);

let id = options.el;
let dom = nodeToFragment(document.getElementById(id), this);
// 处理完所有dom节点后,重新将内容添加回去
document.getElementById(id).appendChild(dom);
}

let vm = new MyVue({
el: 'app',
data: {
text: 'hello world'
}
});

</script>

</body>
</html>

测试

其他网址

​​通俗易懂了解Vue双向绑定原理及实现 - osc_2x36yftz的个人空间 - OSCHINA - 中文开源技术交流社区​​

​​手写JS(七)--实现VUE的双向绑定 - 掘金​​


举报

相关推荐

0 条评论