先看看咱们要实现的目标是什么,以下动图:html
单向数据绑定:把Model 绑定到View上,当咱们用js修改模型 Model 时候,视图View上对应的内容也会改动,这就是 数据动,页面动 。前端
双向数据绑定:简言之 数据动 页面动,页面动,数据动, 典型的应用就是在作表单时候,输入框的内容改动后,跟该输入框的value 的值改动。vue
看vue 官网上的这个V-model 的演示案例:git
要说出这个好处的时候,也只有在实际场景中才能对应的显示出来。好比咱们须要实时显示数据,咱们一边说话,一边实时显示咱们说的话的文字内容,等等。这让我想起了去年参加云栖大会,台上的大佬一边说话,下面的字幕实时更新。(固然实现这个技术有不少技术点,咱们不讨论这个内容,小编也才疏学浅,搞不懂)github
以上的都是废话,咱们直接看看怎么实现这个双向数据绑定。缓存
Vue实现双向数据绑定的原理:数据劫持 + 发布订阅模式(有的也称为观察者模式)app
数据劫持的核心技术: Object.defineProperty()dom
**vue 3.0 已经用的不是这个技术了,采用是 原生的 Proxy,听说速度可以提高100%,截张尤大的ppt,** 2018-11-21 修改本篇笔记ide
(香菇,刚研究会一点,就立马变了,这就是前端世界),Proxy 的方式将会在本系列笔记结束后,再记录这个技术点的使用函数
先上一个参照代码,它长这个样子:
var book = { _year: 2004, edition: 1 }; Object.defineProperty(book, "year", { get: function(){
console.log('访问year了,返回_year') return this._year; }, set: function(newValue){ if (newValue > 2004) { this._year = newValue;
console.log('从新设置_year了,并返回edition') this.edition += newValue - 2004; } } }); book.year = 2005; alert(book.edition); //2
---摘自 JavaScript高级程序设计
Object.defineProperty() 的具体介绍,咱们本文不作具体展开,查看我这里的一篇文章,Object.defineProperty。 咱们这里先要知道这么一个事情。这个方法要传入三个参数,传入的数据对象data,属性key,描述符对象。其中,描述符(descriptor)对象的属 性必须是:configurable、enumerable、writable 和 value。设置其中的一或多个值,能够修改 对应的特性值。咱们须要用的是这访问器属性。当咱们在读取访问器属性时,会调用 getter 函数,这个函数负责返回有效的值;在写入访问器属性时,会调用 setter 函数并传入新值,这个函数负责决定如何处理数据。上文代码上的 get 方法,在读取属性时调用的函数,set方法,在写入属性时调用的函数。
我画了一个图,来理解这个模式,以下图:
代码解释:
//下面封装一个单例模式,内容是发布订阅模式 let event = { eventList: [], listener: function (key, fn) { if (!this.eventList[key]) { //没有订阅过此类消息,建立一个缓存列表 this.eventList[key] = []; } this.eventList[key].push(fn) }, trigger: function () { let key = Array.prototype.shift.call(arguments); // marry let fns = this.eventList[key]; if (!fns || fns.length == 0) { //没有订阅 则返回 return false; } for (let i = 0, fn; fn = fns[i++];) { fn.apply(this, arguments) // 调用 event.listen 里面的 fn 方法,经过apply将当前执行的对象指向当前的this,arguments 传进 fn 函数 } }, remove: function (key, fn) { // 取消订阅 let fns = this.eventList[key]; if (!fns) { return false; } if(!fn) { fns && (fns.length = 0) } else { for (let l = fns.length-1; l>=0; l--) { let _fn = fns[l]; if( _fn === fn) { fns.splice(l, 1) } } } }, install: function (obj) { for (let i in this) { if (i === 'install') { return false } obj[i] = this[i]; } } } let testMsg = {} event.install(testMsg) // 上面方法 就会将event的方法 浅拷贝给 testMsg, 这样testMsg就有 event的方法和属性 testMsg.listener('rich', fn1 = (name) => { console.log(`${name}知道你有钱了`) }) testMsg.listener('borrowMoney', fn2 = (name) => { console.log(`${name}想问你借钱`) }) // listen方法将事件 放进队列 // testMsg.remove('rich', fn1) // 取消订阅 // trigger方法,处理事件队列的方法,调用listen的函数的里面的回调函数 fn testMsg.trigger('rich', '张三') testMsg.trigger('rich', '张三2') testMsg.trigger('borrowMoney', '李四') testMsg.trigger('borrowMoney', '李四2') // 代码总结: // 订阅的事件具备对应的key // 经过listener方法,将具体的事件队列保存到 eventList ,能够理解为缓存列表也能够是事件队列; // 执行trigger 方法,将事件队列拿出来执行调用 // remove 方法根据对应的key值,删除对应的订阅事件 // 模式总结:封装一个单例event, 执行installEvent方法,将想要event对象拷贝到某个对象中去, // 发布者trigger方法,监听者listen方法 ,trigger 发布一个东西,listen立马知道你要发布的东西
function Myvue (options) { this.$options = options this.$el = document.querySelector(options.el); this.$data = options.data; Object.keys(this.$data).forEach(key => { this.$prop = key; }) this.init() } Myvue.prototype.init = function () { // 监听数据变化 observer(this.$data); // 得到值 // let value = this.$data[this.$prop]; // 不通过模板编译直接 通知订阅者更新dom // new Watcher(this,this.$prop,value => { // console.log(`watcher ${this.$prop}的改动,要有动静了`) // this.$el.textContent = value // }) //通知模板编译来执行页面上模板变量替换 new Compile(this) }
<script> const vm = new Myvue({ el: "#app", data: { name: "vue 双向数据绑定test1" } }); </script>
未完待续,错误之处,敬请指出,共同进步!
文章参考:
http://www.javashuo.com/article/p-zupmyrcc-dw.html
https://github.com/youngwind/blog/issues/87
http://www.javashuo.com/article/p-rnawiolt-hw.html
后记:代码只作基本实现,不作代码健壮性处理,一些错误处理已经忽略