Vue - 如何实现一个双向绑定
JS - 如何实现一个类似 vue 的双向绑定 Github JS 实现代码
先来看一张图:
这张图我做个简要的描述:
首先创建一个实例对象,分别触发了 compile 解析指令 和 observer 监听器,
compile 解析指令则循环递归 解析 类似 v-model 这样的指令,初始化 data 绑定数据,同时每个节点创建一个订阅者 watcher ,
observer 监听器 则利用了 Object.defineProperty() 方法的描述属性里边的 set,get方法,来监听数据变化,
get 方法是在创建实例对象,生成dom节点的时候都会触发,固:在compile 解析编译的时候,依次给每一个节点添加了一个订阅者到主题对象 Dep
set 方法则是数据发生改变了,通知Dep订阅器里的所有wachter,然后找到对应订阅者 wachter 触发对应 update 更新视图
简单的说明就是这样了。
双向绑定原理
vue数据双向绑定是通过数据劫持结合发布者-订阅者模式的方式来实现的。
具体点儿
Vue双向数据绑定的原理就是利用了 Object.defineProperty() 这个方法重新定义了对象获取属性值(get)和设置属性值(set)的操作来实现的2.实现一个监听器Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者。
3.实现一个订阅者Watcher,每一个Watcher都绑定一个 update,watcher 可以收到属性的变化通知并执行相应的 update ,从而更新视图。
4.实现MVVM,双向绑定
以下实践里边的几个方法我就不做介绍了,感兴趣可查询
Object.defineProperty()
createDocumentFragment()
Object.keys()
话不多说:直接上代码:实现一个解析器Compile
/* 第一步 1,创建文档碎片,劫持所有dom节点,重绘dom节点 2,重绘dom节点,初始化文档碎片绑定数据 实现文档编译 compile */ function getDocumentFragment(node, vm) { var flag = document.createDocumentFragment(); var child; while (child = node.firstChild) { /* while (child = node.firstChild) 相当于 child = node.firstChild while (child) */ compile(child, vm); flag.appendChild(child); } node.appendChild(flag); } function compile(node, vm) { /* nodeType 返回数字,表示当前节点类型 1 Element 代表元素 Element, Text, 2 Attr 代表属性 Text, EntityReference 3 Text 代表元素或属性中的文本内容。 . . . 更多请查看文档 */ if (node.nodeType === 1) { // 获取当前元素的attr属性 var attr = node.attributes; for (let i = 0; i < attr.length; i++) { // nodeName 是attr属性 key 即名称 , 匹配自定义 v-m if (attr[i].nodeName === 'v-m') { // 获取当前值 即 v-m = "test" 里边的 test let name = attr[i].nodeValue; // 当前节点输入事件 node.addEventListener('keyup', function (e) { vm[name] = e.target.value; }); // 页面元素写值 vm.data[name] 即 vm.data['test'] 即 MVVM node.value = vm.data[name]; //最后移除标签中的 v-m 属性 node.removeAttribute('v-m'); // 为每一个节点创建一个 watcher new Watcher(vm, node, name, "input"); } } /* 继续递归调用 文档编译 实现 视图更新 ; */ if (child = node.firstChild) { /* if (child = node.firstChild) 相当于 child = node.firstChild id(child) */ compile(child, vm); } } if (node.nodeType === 3) { let reg = /\{\{(.*)\}\}/; if (reg.test(node.nodeValue)) { let name = RegExp.$1.trim(); node.nodeValue = vm.data[name]; // 为每一个节点创建一个 watcher new Watcher(vm, node, name, "text"); } } }
实现一个监听器Observer
/* 第二步 实现一个数据监听 1,获取当前实例对象的 data 属性 key observer(当前实例对象 data ,当前实例对象) 2,使用 Object.defineProperty 方法 实现监听 */ function observe(data, vm) { Object.keys(data).forEach(function (key) { defineReactive(vm, key, data[key]); }); } function defineReactive(vm, key, val) { /* Object.defineProperty obj 要在其上定义属性的对象。 prop 要定义或修改的属性的名称。 descriptor 将被定义或修改的属性描述符。 描述符有很多,就包括我们要市用 set , get 方法 */ var dep = new Dep(); Object.defineProperty(vm, key, { get: function () { /* if (Dep.target) dep.addSub(Dep.target); 看到这段代码不要差异,生成每一个 dom节点,都会走 get 方法 这里为每一个节点 添加一个订阅者 到主题对象 Dep */ if (Dep.target) dep.addSub(Dep.target); console.log(val) return val; }, set: function (newValue) { if (newValue === val) return; val = newValue; console.log(val + "=>" + newValue) // 通知所有订阅者 dep.notify(); } }); }
实现一个订阅者Watcher
/* 第三步 1,实现一个 watcher 观察者/订阅者 订阅者原型上挂在两个方法 分别是 update 渲染视图 2,定义一个消息订阅器 很简单,维护一个数组,用来收集订阅者 消息订阅器原型挂载两个方法 分别是 addSub 添加一个订阅者 notify 数据变动 通知 这个订阅者的 update 方法 */ function Watcher(vm, node, name, nodeType) { Dep.target = this; this.vm = vm; this.node = node; this.name = name; this.nodeType = nodeType; this.update(); console.log(Dep.target) Dep.target = null; } Watcher.prototype = { update: function () { /* this.node 指向当前修改的 dom 元素 this.vm 指向当前 dom 的实例对象 根据 nodeType 类型 赋值渲染页面 */ if (this.nodeType === 'text') { this.node.nodeValue = this.vm[this.name] } if (this.nodeType === 'input') { this.node.value = this.vm[this.name] } } } function Dep() { this.subs = []; } Dep.prototype = { addSub: function (sub) { this.subs.push(sub); }, notify: function () { this.subs.forEach(function (sub) { sub.update(); }); } }
实现类似Vue的MVVM
/* 创建一个构造函数,并生成实例化对象 vm */ function Vue(o) { this.id = o.el; this.data = o.data; observe(this.data, this); getDocumentFragment(document.getElementById(this.id), this); } var vm = new Vue({ el: 'app', data: { msg: 'HiSen', test: 'Hello,MVVM' } });
也许看到最后大家也没有看出个所以然,曾几何时的我跟你们一样,看来看去,就是这么几段代码;建议:拿下我的源码,自己跑一跑,看一看,是骡子是马拉出来溜溜。