提到 Vue 的响应式,一般指的是视图跟随数据的改变而更新。开发上带来的便利是,在须要更新视图呈现时,只需修改视图渲染所须要的数据便可,而不用手动操做DOM。从实现来讲,能够分为两个部分:html
咱们很熟悉如何监听鼠标的点击,键盘的输入等用户事件,可是不多直接去监听一个数据改变的事件。虽然,不存在数据改变这个事件,可是监听数据改变是能够作到的,而且从程序设计角度来讲,和给事件绑定一个回调函数没有本质的不一样。vue
为了比较监听普通事件和监听数据改变的区别,咱们先使用事件的方式,来实现“响应式”视图更新。node
下面的代码中,咱们定义了数据变量data
和视图更新函数update
。update
函数在更新视图时,读取了data
的text
属性做为视图节点的文本内容。而后监听一个input
元素的input
事件,事件的回调函数中,将用户输入的值替换data.text
的当前值,而后调用update
函数,通知视图进行更新。react
<input id='text' /> <div id='app'></div> <script> /* 定义渲染数据和视图更新函数 */ var data = { text: 'hello' } function update() { document.getElementById('app').textContent = data.text } update() /* 绑定 input 事件,在修改数据后更新视图 */ var textElm = document.getElementById('text') textElm.value = data.text textElm.addEventListener('input', function() { data.text = this.value update() }) </script> 复制代码
借助input
事件,咱们间接实现了“响应式”,但它只是起到一个纽带的做用,不能直接对数据的改变做出响应。浏览器
Object.defineProperty(obj, prop, descriptor)
能够给对象添加或者修改已有属性。函数接受三个参数:markdown
String
或Symbol
类型Object
类型这里重点须要了解的是属性描述符对象 descriptor
。descriptor
支持如下字段:app
configurable
: Boolean
,为true
时,才能改变属性描述符,以及删除属性enumerable
: Boolean
,为true
时,能够经过for ... in
或 Object.keys
方法枚举value
: 该属性对应的值。能够是任何有效的 JavaScript 值writable
: Boolean
,为true
时,属性值,也就是 value
才能被赋值运算符改变get
: 属性的 getter 函数,当访问该属性时,会调用此函数set
: 属性的 setter 函数,当属性值被修改时,会调用此函数其中 value
和 writable
只能出如今数据描述符
中;而get
和set
只能出如今存取描述符
中。一个属性描述符descriptor
只能是其中之一,所以当定义了 value
或 writable
,就不能再定义 get
或 set
,不然报错 Cannot both specify accessors and a value or writable attribute
。反之亦然。函数
因为,咱们须要在对象属性改变时得到通知,我须要使用存取描述符
来定义对象属性,即定义set
来响应属性值的修改,定义get
来响应属性的访问。oop
以上文的data
为例,咱们但愿在经过data.text = xxx
的方式改变对象的属性值时,更新视图,因此要从新定义属性text
的描述符,在set
函数中调用视图更新函数update
。这里还须要定义get
,由于,我不但须要对属性值更改时做出响应,同时在update
函数中,咱们还须要读取data.text
的值,而若是不定义get
,获取的值就为undefined
。
var data = { text: 'hello' } var text = data.text Object.defineProperty(data, 'text', { get: function() { return text }, set: function(newValue) { if (text !== newValue) { text = newValue update() } } }) 复制代码
这样定义后,咱们即可以直接修改data.text
值更新视图了。读者能够将如下完整代码,保存到一个 html
文件中,而后在浏览器控制台中经过data.text = 'world'
赋值的方式,查看视图的变化。
<div id='app'></div> <script> /* 定义渲染数据和视图更新函数 */ var data = { text: 'hello' } function update() { document.getElementById('app').textContent = data.text } update() /* 使用 Object.defineProperty 实现响应式视图更新 */ var text = data.text Object.defineProperty(data, 'text', { get: function() { return text }, set: function(newValue) { if (text !== newValue) { text = newValue update() } } }) </script> 复制代码
这里只是针对data
的属性text
定义响应式。为了代码更加通用,以用于任意对象,能够编写一个函数defineReactive(obj, key, update)
(函数名参考了 Vue2 的定义,读者能够在 Vue2 源码中搜索该函数)。
function defineReactive(obj, key, update) { var value = obj[key] Object.defineProperty(obj, key, { get: function() { return value }, set: function(newValue) { if (value !== newValue) { value = newValue update() } } }) return obj } 复制代码
因而上面的代码能够改写成:
var data = { text: 'hello' } function update() { document.getElementById('app').textContent = data.text } update() defineReactive(data, 'text', update) 复制代码
响应对象属性改变,除了Object.definProperty
外,浏览器还支持另外一个全局的构造函数Proxy
,用于自定义对象的基本操做,如:属性查找,赋值,枚举,函数调用等。相比而言,前者只能自定义对象属性的访问和赋值。
Proxy
的使用方法以下:
const proxy = new Proxy(target, handler) 复制代码
handelr
对象支持的方法(一般被称为traps
,中文翻译为陷阱,能够理解为钩子或者执行某项操做的回调函数)有:
in
操做符时调用delete
操做符时调用Object.getOwnPropertyNames
方法和Object.getOwnPropertySymbols
方法时调用new
操做符时调用Object.defineProperty
方法时调用Object.getOwnPropertyDescriptor
方法时调用Object.getPrototypeOf
方法时调用Object.setPrototypeOf
方法时调用Object.isExtensible
方法时调用Object.preventExtensions
方法时调用能够看到Proxy
对对象自定义行为的控制比Object.defineProperty
更加全面。这里,咱们重点关注和后者
相同部分,即get
和set
。虽然名称都是get
和set
,但方法的传参不一样。Object.defineProperty
是针对对象的某个属性定义get
和set
,而Proxy
是针对整个对象。而且经过Proxy
构造函数返回的是一个proxy
实例,而不是原对象。所以,Proxy
中的get
和set
参数比Object.defineProperty
的多了两个参数:
target
之前文的data
对象为例,定义get
和set
方法以下:
const dataProxy = new Proxy(data, { get(obj, key) { return obj[key] }, set(obj, key, newValue) { obj[key] = newValue // 表示成功 return true } }) 复制代码
这里和Object.defineProperty
还有最大不一样的是,前者响应式在新返回的代理对象生效,而对原对象属性尽心访问和修改是不会触发set
和get
回调的。所以,若是使用Proxy
重写前文的响应式视图更新,须要在读取和设置对象属性时使用dataProxy
,完整代码以下:
<div id='app'></div> <script> function reactive(target, update) { var targetProxy = new Proxy(target, { get(obj, key) { return obj[key] }, set(obj, key, newValue) { obj[key] = newValue update() // 表示成功 return true } }) return targetProxy } var data = { text: 'hello' } var dataProxy = reactive(data, update) function update() { document.getElementById('app').textContent = dataProxy.text } update() </script> 复制代码
若是一样在浏览器控制台修改数据,咱们应该使用dataProxy.text = 'xxx'
而不是 data.text = 'xxxx'
。
在《手写 Vue (一)》中,咱们实现了基于虚拟 DOM 的视图挂载。如今结合响应式实现虚拟 DOM 的到真实 DOM 的响应式更新。
完整代码以下:
function Vue(options) { var vm = this function update () { vm.update() } var data = options.data var keys = Object.keys(data) for (var i = 0, l = keys.length; i < l; i++) { var key = keys[i] this[key] = data[key] defineReactive(this, key, update) } this.$options = options } Vue.prototype.render = function() { var render = this.$options.render return render.call(this, createVNode) } Vue.prototype.update = function() { var vnode = this.render() this.$el = createElm(vnode, this.$el.parentNode, this.$el) } Vue.prototype.$mount = function (id) { this.$el = document.querySelector(id) this.update() return this } function createVNode(tag, data, children) { var vnode = { tag: tag, data: undefined, children: undefined, text: undefined } if (typeof data === 'string') { vnode.text = data } else { vnode.data = data if (Array.isArray(children)) { vnode.children = children } else { vnode.children = [ children ] } } return vnode } function createElm(vnode, parentElm, refElm) { var elm // 建立真实DOM节点 if (vnode.tag) { elm = document.createElement(vnode.tag) } else if (vnode.text) { elm = document.createTextNode(vnode.text) } // 将真实DOM节点插入到文档中 if (refElm) { parentElm.insertBefore(elm, refElm) parentElm.removeChild(refElm) } else { parentElm.appendChild(elm) } // 递归建立子节点 if (Array.isArray(vnode.children)) { for (var i = 0, l = vnode.children.length; i < l; i++) { var childVNode = vnode.children[i] createElm(childVNode, elm) } } else if (vnode.text) { elm.textContent = vnode.text } return elm } function defineReactive(obj, key, update) { var value = obj[key] Object.defineProperty(obj, key, { get: function() { return value }, set: function(newValue) { if (value !== newValue) { value = newValue update() } } }) return obj } 复制代码
将以上代码保存到文件myvue_2.js
中,再新建html文件myvue_2.html
,替换如下内容:
<div id="app"></div> <script src="myvue_2.js"></script> <script> var vm = new Vue( { data: { text: 'hello world!' }, render(h) { return h('div', this.text) } } ).$mount('#app') </script> 复制代码
尝试在浏览器控制台输入:
vm.text = 'anything you like!!!' 复制代码
若是看到显示内容即时更新为你修改的内容,那么,恭喜你成功作到了和 Vue 同样的响应式视图更新。
咱们成功利用set
拦截,实现了响应式视图更新,可是还不够完美,由于,咱们对data
对象中任何属性的赋值都会执行视图更新操做,而无论update
是否用到了这个属性。这意味着,若是data
有不少个属性,但并不是全部属性都会用于视图的渲染,这样咱们就会作一些多余的视图更新操做,显然这是没有意义的性能开销。要作到自动根据update
中实际使用的到属性,只对用到的属性执行视图更新,就涉及到依赖的搜集
。关于依赖搜集
的实现,咱们在下一篇文章中继续探讨。