几种实现双向绑定的做法
目前几种主流的mvc框架都实现了单向数据绑定,而双向绑定无非就是在单项绑定的基础上给可输入元素添加了change事件,来动态修改model和view。
实现数据绑定的做法有大致如下几种
发布者-订阅者模式
脏值检查
数据劫持
发布者-订阅者模式
一般通过sub, pub的方式实现数据和视图的绑定监听,更新数据方式通常做法时vm.set('property', value).
脏值检查
angular.js 时通过脏值检测的的方式比较数据是否有变更,来决定是否更新视图,最简单的方式就是通过setInterval() 定时轮询检测数据变动,angular的做法时,只有在指定的事件触发进入脏值检测
DOM事件
XHR响应事件
浏览器Location变更事件
Timer事件
执行digest() 或 apply()
数据劫持
vue.js 则时采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter, 在数据变动时发布消息给订阅者,触发相应的监听回调。
思路整理
1、实现一个数据监听器Observer, 能够对数据对象的所有属性进行监听,如果变动可拿到最新值并通知订阅者
2、实现一个指令解析器Compile, 对每个元素节点的指令进行扫描和解析, 根据指令模板替换数据,以及绑定相应的更新函数
3、实现一个Watcher,作为连接Observer 和 Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图
4、入口函数,整合以上三者
流程图
数据监听器
function observe(obj, vm) { // 对传入的对象 遍历 并分别添加 object.defineProperty Object.keys(obj).forEach((key) => { defineReactive(vm, key, obj[key]) }) } function defineReactive(vm, key, val){ var dep = new Dep(); Object.defineProperty(vm, key, val){ get: function () { if(Dep.target) dep.addSub(Dep.target) return val; }, set: function(newval) { if(newval === val) return val = newval; // 通知订阅者 dep.notify(); } } } // 需要实现一个消息订阅器 function Dep() { // 消息订阅的容器是一个数组 数组的每一项都是指代一个view 和 model的中间者 this.subs = []; } Dep.prototype = { addSub: function (sub){ this.subs.push(sub) }, notify: function () { this.subs.forEach((sub) => { //在这里,需要配合watcher进行更新 sub.update() }) } }
实现Compile
function nodeToFragment(node, vm) { var flag = document.createDocumentFragment(); var child; while(child = node.firstChild) { compile(child, vm); // 将子节点劫持到文本节点中 flag.appendChild(child) } return flag; } function compile(node, vm) { var reg = /\{\{(.*)\}\}/; if(node.nodeType === 1) { var attr = node.attributes; for(var i = 0; i< attr.length; i++){ if(attr[i].nodeName === "v-model") { // 此时 name为text var name = attr[i].nodeValue; // 增加数据的变化监听 node.addEventListener('input', (e)=>{ vm[name] = e.target.value; }) // 在这里 因为 我们的数据监听器 已经封装了vm[name] node.value = vm[name]; node.removeAttribute('v-model') } } new Watcher(vm, node, name, 'input') } if(node.nodeType === 3){ if(reg.test(node.nodeValue)){ var name + RegExp.$1; name = name.trim(); new Watcher(vm, node, name, 'text') } } }
watcher观察函数
//订阅者 搭建数据监听变化和变异模板的桥梁 function Watcher(vm, node, name, nodeType) { Dep.target = this; this.vm = vm; this.node = node; this.name = name; this.nodeType = nodeType this.update() Dep.target = null; } Watcher.prototype = { update: function () { this.get() if (this.nodeType === 'text') { this.node.nodeValue = this.value } if (this.nodeType === 'input') { this.node.value = this.value } }, get: function () { this.value = this.vm[this.name]; } }
入口函数
function Vue(options) { // 将options里面的data属性 放入数据监听器 this.data = options.data; var data = this.data; observe(data, this); // this指代vm // 对指定id的dom 进行页面的渲染 this.$el = options.el; var id = this.$el; var Dom = nodeToFragment(document.getElementById(id), this); // 编译完成之后 将dom 添加到节点中 document.getElementById(id).appendChild(Dom) } var vm = new Vue({ el: 'app', data: { text: 'hello world', name: '你好,全世界' } }); vm.data.text = 'majunchang' document.getElementsByClassName('btn')[0].onclick = function () { vm.text = 'majunchang' vm.name = '又疑瑶台镜,飞在青云端' }