腾讯DeepOcean原创文章:dopro.io/vue-mvvm-re…javascript
var a = {};
Object.defineProperty(a, 'b', { value: 123, // 设置的属性值 writable: false, // 是否只读 enumerable: false, // 是否可枚举 configurable: false //
});
console.log(a.b); //123复制代码
方法使用很简单,它接受三个参数,并且都是必填的html
// 经常使用定义
var obj = {};
Object.defineProperty(obj, 'school', { enumerable: true, get: function() {
// 获取属性值时会调用get方法 }, set: function(newVal) {
// 设置属性值时会调用set方法 return newVal } });复制代码
咱们经过这个Object.defineProperty这个方法,能够实现对定义的引用数据类型的实现监听,被方法监听后的对象,里面定义的值发生被获取和设置操做的时候,会分别触发Object.defineProperty里面参数三的get和set方法。前端
function MyVue(options = {}) {
// 将全部的属性挂载到$options身上 this.$options = options;
// 获取到data数据(Model) var data = this._data = this.$options.data;
// 劫持数据 observe(data) }
// 给须要观察的对象都添加 Object.defineProperty 的监听
function Observe(data) {
for (let key in data) {
let val = data[key];
// 递归 =》来实现深层的数据监听 observe(val)
Object.defineProperty(data, key, { enumerable: true, get() {
return val }, set(newval) { if (val === newval) { //设置的值是否和之前是同样的,若是是就什么都不作 return } val = newval // 这里要把新设置的值也在添加一次数据劫持来实现深度响应, observe(newval); } }) } }
function observe(data) {
// 这里作一下数据类型的判断,只有引用数据类型才去作数据劫持 if (typeof data != 'object') return return new Observe(data) }复制代码
1)以上代码作了这些事情,先定义了初始换构造函数MyVue咱们经过它来获取到咱们传进来的数据data和咱们定义的DOM节点范围,而后把data传进定好的数据劫持方法observevue
2)Observe实现了对数据的监听总体逻辑,这里有个细节点,没有直接用构造函数Observe去劫持咱们的数据,而是写多了一个observe的小方法用来new Observe,而且在里面作了引用数据类型的判断。这样作的目的是为了方便递归来实现数据结构的深层监听,由于咱们的data结构确定是复杂多样的,例以下面代码java
// 这里数据结构嵌套不少,要实现深层的数据监听采用了递归的实现方式
data: { a: {b:2} , c:{q:12,k:{name:'binhemori'}} , f:'mvvvm',o:[12,5,9,8]}复制代码
3)这里还须要注意的是咱们在set方法里面有再一次把设置的新值,执行了一遍observe方法,是为了实现深度响应,由于在赋值的时候可能会赋值一个引用数据类型的值,咱们知道vue有个特色,是不能新增不存在的属性和不能存在属性没有get和set方法的,若是赋值进来的新属性值是引用数据类型,就会把咱们原先执行过数据劫持方法的对象地址给替换掉,而新对象是没有通过数据劫持的就是没有get和set的方法,因此咱们在设置新值的时候须要在从新给他执行一遍observe数据劫持,确保开发者无论怎样去设置值的时候都能被劫持到node
说了这么多,咱们来使用一下看看有没有实现对数据的劫持(数据监听)吧设计模式
数组
<div id="app">
<div> <div>这里的数据1======<span style="color: red;">{{a.b}}</span></div> <div>这里是数据2======<span style="color: green;">{{c}}</span></div> </div> <input type="text" v-model="a.b" value=""> </div> <!-- 引入本身定义的mvvm模块 --> <script src="./mvvm.js"></script> 复制代码<script type="text/javascript"> var myvue = new MyVue({ el: '#app', data: { a: { b: '你好' }, c: 12, o: [12, 5, 9, 8] } })
</script>复制代码
能够看到对咱们所定义的data中的数据都已经有了get和set方法了,到这里咱们对data中数据的变化都是能够监听的到了微信
数据代理,咱们用过vue的都知道,在实际使用中是能直接经过实例+属性(vm.a)直接获取到数据的,而咱们上面的代码要获取到数据还须要这样myvue._data.a这样来获取到数据,中间多了一个 _data 的环节,这样使用起来不是很方便的,下面咱们来实现让咱们的实例this来代理( _data)数据,从而实现 myvue.a 这样的操做能够直接获取到数据数据结构
function MyVue(options = {}) {
// 将全部的属性挂载到$options身上 this.$options = options;
// 获取到data数据(Model) var data = this._data = this.$options.data; observe(data);
// this 就代理数据 this._data for (const key in data) {
Object.defineProperty(this, key, { enumerable: true, get() {
// this.a 这里取值的时候 其实是去_data中的值 return this._data[key] }, set(newVal) { // 设置值的时候其实也是去改this._data.a的值 this._data[key] = newVal } }) } }复制代码
以上代码实现了咱们的数据代理,就是在构建实例的时候,把data中的数据遍历一次出来,依次加到咱们this上,加的过程当中也不要忘记添加Object.defineProperty,只要是数据咱们都须要添加监听。以下图咱们已经实现了对数据的代理
咱们已经完成对数据劫持也实现了this对数据的代理,那么接下来要作的就是怎样把数据编译到咱们的DOM节点上面,也就是让视图层(view)要展现咱们的数据了
// 将数据和节点挂载在一块儿
function Compile(el, vm) {
// el表示替换的范围 vm.$el = document.querySelector(el);
// 这里注意咱们没有去直接操做DOM,而是把这个步骤移到内存中来操做,这里的操做是不会引起DOM节点的回流 let fragment = document.createDocumentFragment(); // 文档碎片 let child;
while (child = vm.$el.firstChild) {
// 将app的内容移入内存中 fragment.appendChild(child); }
replace(fragment)
function replace(fragment) {
Array.from(fragment.childNodes).forEach(function (node) { //循环每一层 let text = node.textContent;
let reg = /\{\{(.*)\}\}/g;
// 这里作了判断只有文本节点才去匹配,并且还要带{{***}}的字符串 if (node.nodeType === 3 && reg.test(text)) {
// 把匹配到的内容拆分红数组
let arr = RegExp.$1.split('.'); let val = vm;
// 这里对咱们匹配到的定义数组,会依次去遍历它,来实现对实例的深度赋值 arr.forEach(function (k) { // this.a.b this.c val = val[k] })
// 用字符串的replace方法替换掉咱们获取到的数据val node.textContent = text.replace(/\{\{(.*)\}\}/, val) }
// 这里作了判断,若是有子节点的话 使用递归 if (node.childNodes) { replace(node) } }) }
// 最后把编译完的DOM加入到app元素中 vm.$el.appendChild(fragment) }复制代码
以上代码实现咱们对数据的编译Compile以下图,能够看到咱们把获取到el下面全部的子节点都存储到了文档碎片 fragment 中暂时存储了起来(放到内存中),由于这里要去频繁的操做DOM和查找DOM,因此移到内存中操做
在成功的将咱们的数据绑定到了DOM节点以后,要实现咱们的视图层(view)跟数据层(model)的关联,如今实际上尚未关联起来,由于没法经过改数据值来引起视图的变化,实现这步以前先聊一下JS中比较经常使用的设计模式发布订阅模式也是vue实现双向数据绑定的很关键的一步
咱们先简单手动实现一个(就是一个数组关系)
// 发布订阅
function Dep() {
this.subs = [] }
// 订阅
Dep.prototype.addSub = function (sub) {
this.subs.push(sub) }
// 通知
Dep.prototype.notify = function (sub) {
this.subs.forEach(item => item.update()) }
// watcher是一个类,经过这个类建立的函数都会有update的方法
function Watcher(fn) {
this.fn = fn; } Watcher.prototype.update = function () {
this.fn() }复制代码
这里用Dep方法来实现订阅和通知,在这个类中有addSub(添加)和notify(通知)两个方法,咱们把将要作的事情(函数)经过addSub添加进数组里,等时机一到就notify通知里面全部的方法执行
你们会发现为何要另外定义一个建立函数的方法watcher,而不是直接把方法扔到addSub中好,这样不是画蛇添足嘛?其实这样作的有它的目的,其中一个好处就是咱们经过watcher建立的函数都会有一个update执行的方法能够方便咱们调用。而另一个用处我下面会讲到,先把它运用起来吧
function replace(fragment) {
Array.from(fragment.childNodes).forEach(function (node) { let text = node.textContent;
let reg = /\{\{(.*)\}\}/g;
if (node.nodeType === 3 && reg.test(text)) {
let arr = RegExp.$1.split('.');
let val = vm; arr.forEach(function (k) { val = val[k] })
// 在这里运用了Watcher函数来新增要操做的事情 new Watcher(vm, RegExp.$1, function (newVal) { node.textContent = text.replace(/\{\{(.*)\}\}/, newVal) }) 复制代码 node.textContent = text.replace(/{{(.*)}}/, val) }
if (node.childNodes) { replace(node) } }) }复制代码
能够看到咱们把定义函数的方法watcher加到了replace方法里面,可是这里的watcher更刚写编写的多了两个形参vm、RegExp.$1,并且写法也新增了一些内容,由于当new Watcher的时候会引起发生几个操做,来看代码:
// vm作数据代理的地方
function MyVue(options = {}) {
this.$options = options;
var data = this._data = this.$options.data; observe(data);
for (const key in data) {
Object.defineProperty(this, key, { enumerable: true, get() {
return this._data[key] }, set(newVal) { this._data[key] = newVal } }) } }
// 数据劫持函数
function Observe(data) { let dep = new Dep();
for (let key in data) {
let val = data[key]; observe(val)
Object.defineProperty(data, key, { enumerable: true, get() {
/* 获取值的时候 Dep.target
对于着 watcher的实例,把他建立的实例加到订阅队列中
*/ Dep.target && dep.addSub(Dep.target); return val }, set(newval) { if (val === newval) {
return } val = newval; observe(newval);
// 设置值的时候让全部的watcher.update方法执行便可触发全部数据更新 dep.notify() } }) } }
function Watcher(vm, exp, fn) {
this.fn = fn;
// 这里咱们新增了一些内容,用来能够获取对于的数据 this.vm = vm;
this.exp = exp; Dep.target = this let val = vm;
let arr = exp.split('.');
/* 执行这一步的时候操做的是vm.a,
而这一步操做其实就是操做的vm._data.a的操做,
会触发this代理的数据和_data上面的数据
*/ arr.forEach(function (k) { val = val[k] }) Dep.target = null; }
// 这里是设置值操做
Watcher.prototype.update = function () {
let val = this.vm;
let arr = this.exp.split('.'); arr.forEach(function (k) { val = val[k] })
this.fn(val) //这里面要传一个新值
}复制代码
这里开始会有点绕,必定要理解好操做数据的时候会触发的那个实例上面数据的get和set,操做的是那个数据这个思惟
1)首先看在Watcher构造函数中新增了一些私有属性分别表明:
arr.forEach(function (k) {
// arr = [a,b] val = val[k] })复制代码
get() {
// 走到这里的时候 Dep.target 已经存储了 watcher的当前实例实例,把他建立的实例加到订阅队列中 Dep.target && dep.addSub(Dep.target); return val },
// 把要作的更新视图层的操做方法用Watcher定义好,里面已经定义好了要操做的对象
new Watcher(vm, RegExp.$1, function (newVal) { node.textContent = text.replace(/\{\{(.*)\}\}/, newVal) }) 复制代码Watcher.prototype.update = function () {
let val = this.vm;
let arr = this.exp.split('.'); arr.forEach(function (k) { val = val[k] }) this.fn(val) // 把对于的新值传递到方法里面
}复制代码
这里由于加多了一层 vm.a 这样的数据代理,因此逻辑有点绕,记住这句话就好理解操做 vm.a 代理数据上面值的时候,其实就是操做的vm._data中的数据因此会触发两个地方的get和set方法,好说这么多,咱们来看是否实现数据变更触发视图层的变化吧
这里就实现了数据的变动触发视图层的更新操做了
最后一步就来实现视图层的变动触发数据结构的变动操做,上面咱们已经把视图与数据关联最核心的代码讲解了,剩下视图变动触发数据变动就比较好实现了
<div id="app">
<div> <div>这里的数据1======<span style="color: red;">{{a.b}}</span></div> <div>这里是数据2======<span style="color: green;">{{c}}</span></div> </div> <input type="text" v-model="a.b" value="">
</div>
<!-- 引入本身定义的mvvm模块 -->
<script src="./mvvm.js"></script>
<script type="text/javascript"> var myvue = new MyVue({ el: '#app', data: { a: { b: '你好' }, c: 12, o: [12, 5, 9, 8] } })
</script>复制代码
// 获取全部元素节点
if (node.nodeType === 1) {
let nodeAttr = node.attributes
Array.from(nodeAttr).forEach(function (attr) {
let name = attr.name; // v-model="a.b" let exp = attr.value; // a.b 复制代码 if (name.indexOf('v-') >= 0) {
let val = vm;
let arr = exp.split('.'); arr.forEach(function (n) { val = val[n] })
// 这个还好处理,取到对应的值设置给input.value就好 node.value = val; }
// 这里也要定义一个Watcher,由于数据变动的时候也要变动带有v-model属性名的值 new Watcher(vm, exp, function (newVal) { node.value = newVal })
// 这里是视图变化的时候,变动数据结构上面的值 node.addEventListener('input', function (e) {
let newVal = e.target.value
if (name.indexOf('v-') >= 0) {
let val = vm;
let arr = exp.split('.'); arr.forEach(function (k,index) { if (typeof val[k] === 'object') { val = val[k] } else{
if (index === arr.length-1) { val[k] = newVal } } }) } }) }) }复制代码
上面代码对数据变动触发视图层变动的逻辑更上一节同样便可,主要是node.addEventListener('input')这里设置数据的问题,其实原理跟第六节关联视图(view)与数据(model)的逻辑同样,有必定须要注意的是这边加了一个引用数据类型的判断,否则他的循环会到最底层的数据类型值(也就是基础数据类型) 1)这里判断到取到的不是对象数据类型,不作替换操做 (val = val[k]) 2)判断是否是已经最后一个层级了index === arr.length-1,若是是的话直接把input中的值赋值进当前数据中便可
arr.forEach(function (k,index) { if (typeof val[k] === 'object') {
// 若是有嵌套的话就继续查找 val = val[k] } else{
if (index === arr.length-1) {
// 查找到最后一个后直接赋值 val[k] = newVal } } })复制代码
以上是整个mvvm双向数据绑定的简单实现原理,内容有些哪里解释不通顺的地方或有更好的意见欢迎留言:)