其余章节请看:javascript
vue 快速入门 系列html
在 初步认识 vue 这篇文章的 hello-world 示例中,咱们经过修改数据(app.seen = false
),页面中的一行文本(如今你看到我了
)就不见了。vue
这里涉及到 Vue 一个重要特性:响应式系统。数据模型只是普通的 JavaScript 对象,当咱们修改时,视图会被更新。而变化侦测是响应式系统的核心。java
下面咱们就来模拟侦测数据变化的逻辑。react
强调一下咱们要作的事情:数据变化,通知到外界(外界再作一些本身的逻辑处理,好比从新渲染视图)。es6
开始编码以前,咱们首先得回答如下几个问题:npm
编码以下(可直接运行):数组
// 全局变量,用于存储依赖 let globalData = undefined; // 将数据转为响应式 function defineReactive (obj,key,val) { // 依赖列表 let dependList = [] Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function () { // 收集依赖(Watcher) globalData && dependList.push(globalData) return val }, set: function reactiveSetter (newVal) { if(val === newVal){ return } // 通知依赖项(Watcher) dependList.forEach(w => { w.update(newVal, val) }) val = newVal } }); } // 依赖 class Watcher{ constructor(data, key, callback){ this.data = data; this.key = key; this.callback = callback; this.val = this.get(); } // 这段代码能够将本身添加到依赖列表中 get(){ // 将依赖保存在 globalData globalData = this; // 读数据的时候收集依赖 let value = this.data[this.key] globalData = undefined return value; } // 数据改变时收到通知,而后再通知到外界 update(newVal, oldVal){ this.callback(newVal, oldVal) } } /* 如下是测试代码 */ let data = {}; // 将 name 属性转为响应式 defineReactive(data, 'age', '88') // 当数据 age 改变时,会通知到 Watcher,再由 Watcher 通知到外界 new Watcher(data, 'age', (newVal, oldVal) => { console.log(`外界:newVal = ${newVal} ; oldVal = ${oldVal}`) }) data.age -= 1 // 控制台输出: 外界:newVal = 87 ; oldVal = 88
在控制台下继续执行 data.age -= 1
,则会输出 外界:newVal = 86 ; oldVal = 87
。app
附上一张 Data、defineReactive、dependList、Watcher和外界的关系图。测试
首先经过 defineReactive() 方法将 data 转为响应式(defineReactive(data, 'age', '88')
)。
外界经过 Watcher 读取数据(let value = this.data[this.key]
),数据的 getter 则会被触发,因而经过 globalData 收集Watcher。
当数据被修改(data.age -= 1
), 会触发 setter,会通知依赖(dependList),依赖则会通知 Watcher(w.update(newVal, val)
),最后 Watcher 再通知给外界。
思考一下:上面的例子,继续执行 delete data.age
会通知到外界吗?
不会。由于不会触发 setter。请接着看:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> </head> <body> <div id='app'> <section> {{ p1.name }} {{ p1.age }} </section> </div> <script> const app = new Vue({ el: '#app', data: { p1: { name: 'ph', age: 18 } } }) </script> </body> </html>
运行后,页面会显示 ph 18
。咱们知道更改数据,视图会从新渲染,因而在控制台执行 delete app.p1.name
,发现页面没有变化。这与上面示例中执行 delete data.age
同样,都不会触发setter,也就不会通知到外界。
为了解决这个问题,Vue提供了两个 API(稍后将介绍它们):vm.$set 和 vm.$delete。
若是你继续执行 app.$delete(app.p1, 'age')
,你会发现页面没有任何信息了(name 属性已经用 delete 删除了,只是当时没有从新渲染而已)。
注:若是这里执行 app.p1.sex = 'man'
,用到数据 p1 的地方也不会被通知到,这个问题能够经过 vm.$set 解决。
假如数据是 let data = {a:1, b:[11, 22]}
,经过 Object.defineProperty 将其转为响应式以后,咱们修改数据 data.a = 2
,会通知到外界,这个好理解;同理 data.b = [11, 22, 33]
也会通知到外界,但若是换一种方式修改数据 b,就像这样 data.b.push(33)
,是不会通知到外界的,由于没走 setter。请看示例:
function defineReactive(obj, key, val) { Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function () { console.log(`get val = ${val}`) return val }, set: function reactiveSetter (newVal) { if(val === newVal){ return } console.log(`set val = ${newVal}; oldVal = ${val}`) val = newVal } }); } // 如下是测试代码 {1} let data = {} defineReactive(data, 'a', [11,22]) data.a.push(33) // get val = 11,22 (没有触发 setter) {2} data.a // get val = 11,22,33 data.a = 1 // set val = 1; oldVal = 11,22,33(触发 setter)
经过 push() 方法改变数组的值,确实没有触发 setter(行{2}),也就不能通知外界。这里好像说明了一个问题:经过 Object.definePropery() 方法,只能将对象转为响应式,不能将数组转为响应式。
其实 Object.definePropery() 能够将数组转为响应式。请看示例:
// 继续上面的例子,将测试代码(行{1})改成: let data = [] defineReactive(data, '0', 11) data[0] = 22 // set val = 22; oldVal = 11 data.push(33) // 不会触发 {10}
虽然 Object.definePropery() 能够将数组转为响应式,但经过 data.push(33)
(行{10})这种方式修改数组,仍然不会通知到外界。
因此在 Vue 中,将数据转为响应式,用了两套方式:对象使用 Object.defineProperty();数组则使用另外一套。
es6 中能够用 Proxy 侦测数组的变化。请看示例:
let data = [11,22] let p = new Proxy(data, { set: function(target, prop, value, receiver) { target[prop] = value; console.log('property set: ' + prop + ' = ' + value); return true; } }) console.log(p) p.push(33) /* 输出: [ 11, 22 ] property set: 2 = 33 property set: length = 3 */
es6 之前就稍微麻烦点,可使用拦截器。原理是:当咱们执行 [].push()
时会调用数组原型(Array.prototype)中的方法。咱们在 [].push()
和 Array.prototype
之间增长一个拦截器,之后调用 [].push()
时先执行拦截器中的 push() 方法,拦截器中的 push() 在调用 Array.prototype 中的 push() 方法。请看示例:
// 数组原型 let arrayPrototype = Array.prototype // 建立拦截器 let interceptor = Object.create(arrayPrototype) // 将拦截器与原始数组的方法关联起来 ;('push,pop,unshift,shift,splice,sort,reverse').split(',') .forEach(method => { let origin = arrayPrototype[method]; Object.defineProperty(interceptor, method, { value: function(...args){ console.log(`拦截器: args = ${args}`) return origin.apply(this, args); }, enumerable: false, writable: true, configurable: true }) }); // 测试 let arr1 = ['a'] let arr2 = [10] arr1.push('b') // 侦测数组 arr2 的变化 Object.setPrototypeOf(arr2, interceptor) // {20} arr2.push(11) // 拦截器: args = 11 arr2.unshift(22) // 拦截器: args = 22
这个例子将能改变数组自身内容的 7 个方法都加入到了拦截器。若是须要侦测哪一个数组的变化,就将该数组的原型指向拦截器(行{20})。当咱们经过 push 等 7 个方法修改该数组时,则会在拦截器中触发,从而能够通知外界。
到这里,咱们只完成了侦测数组变化的任务。
数据变化,通知到外界。上文编码的实现只是针对 Object 数据,而这里须要针对 Array 数据。
咱们也来思考一下一样的问题:
{a: [11,22]}
好比咱们要使用 a 数组,确定得访问对象的属性 a。就到这里,不在继续展开了。接下来的文章中,我会将 vue 中与数据侦测相关的源码摘出来,配合本文,简单分析一下。
// 须要本身引入 vue.js。后续也尽量只罗列核心代码 <div id='app'> <section> {{ p1[0] }} {{ p1[1] }} </section> </div> <script> const app = new Vue({ el: '#app', data: { p1: ['ph', '18'] } }) </script>
运行后在页面显示 ph 18
,控制台执行 app.p1[0] = 'lj'
页面没反应,由于数组只有调用指定的 7 个方法才能经过拦截器通知外界。若是执行 app.$set(app.p1, 0, 'pm')
页面内容会变成 pm 18
。
其余章节请看: