Vue 最独特的特性之一,是其非侵入性的响应式系统。数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。这使得状态管理很是简单直接,不过理解其工做原理一样重要,这样你能够避开一些常见的问题。----官方文档 本文将针对响应式原理作一个详细介绍,而且带你实现一个基础版的响应式系统。本文的代码请猛戳Github博客javascript
咱们先来看个例子:html
<div id="app">
<div>Price :¥{{ price }}</div>
<div>Total:¥{{ price * quantity }}</div>
<div>Taxes: ¥{{ totalPriceWithTax }}</div>
<button @click="changePrice">改变价格</button>
</div>
复制代码
var app = new Vue({
el: '#app',
data() {
return {
price: 5.0,
quantity: 2
};
},
computed: {
totalPriceWithTax() {
return this.price * this.quantity * 1.03;
}
},
methods: {
changePrice() {
this.price = 10;
}
}
})
复制代码
上例中当price 发生变化的时候,Vue就知道本身须要作三件事情:前端
数据发生变化后,会从新对页面渲染,这就是Vue响应式,那么这一切是怎么作到的呢?vue
想完成这个过程,咱们须要:java
对应专业俗语分别是:react
首先有个问题,在Javascript中,如何侦测一个对象的变化? 其实有两种办法能够侦测到变化:使用Object.defineProperty
和ES6的Proxy
,这就是进行数据劫持或数据代理。这部分代码主要参考珠峰架构课。git
Vue经过设定对象属性的 setter/getter 方法来监听数据的变化,经过getter进行依赖收集,而每一个setter方法就是一个观察者,在数据变动的时候通知订阅者更新视图。github
function render () {
console.log('模拟视图渲染')
}
let data = {
name: '浪里行舟',
location: { x: 100, y: 100 }
}
observe(data)
function observe (obj) { // 咱们来用它使对象变成可观察的
// 判断类型
if (!obj || typeof obj !== 'object') {
return
}
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
function defineReactive (obj, key, value) {
// 递归子属性
observe(value)
Object.defineProperty(obj, key, {
enumerable: true, //可枚举(能够遍历)
configurable: true, //可配置(好比能够删除)
get: function reactiveGetter () {
console.log('get', value) // 监听
return value
},
set: function reactiveSetter (newVal) {
observe(newVal) //若是赋值是一个对象,也要递归子属性
if (newVal !== value) {
console.log('set', newVal) // 监听
render()
value = newVal
}
}
})
}
}
data.location = {
x: 1000,
y: 1000
} //set {x: 1000,y: 1000} 模拟视图渲染
data.name // get 浪里行舟
复制代码
上面这段代码的主要做用在于:observe
这个函数传入一个 obj
(须要被追踪变化的对象),经过遍历全部属性的方式对该对象的每个属性都经过 defineReactive
处理,以此来达到实现侦测对象变化。值得注意的是,observe
会进行递归调用。 那咱们如何侦测Vue中data
中的数据,其实也很简单:面试
class Vue {
/* Vue构造类 */
constructor(options) {
this._data = options.data;
observer(this._data);
}
}
复制代码
这样咱们只要 new 一个 Vue 对象,就会将 data
中的数据进行追踪变化。 不过这种方式有几个注意点需补充说明:vue-router
data.location.a=1
)。这是由于 Vue 经过Object.defineProperty
来将对象的key转换成getter/setter
的形式来追踪变化,但getter/setter
只能追踪一个数据是否被修改,没法追踪新增属性和删除属性。若是是删除属性,咱们能够用vm.$delete
实现,那若是是新增属性,该怎么办呢? 1)可使用 Vue.set(location, a, 1)
方法向嵌套对象添加响应式属性; 2)也能够给这个对象从新赋值,好比data.location = {...data.location,a:1}
Object.defineProperty
不能监听数组的变化,须要进行数组方法的重写,具体代码以下:function render() {
console.log('模拟视图渲染')
}
let obj = [1, 2, 3]
let methods = ['pop', 'shift', 'unshift', 'sort', 'reverse', 'splice', 'push']
// 先获取到原来的原型上的方法
let arrayProto = Array.prototype
// 建立一个本身的原型 而且重写methods这些方法
let proto = Object.create(arrayProto)
methods.forEach(method => {
proto[method] = function() {
// AOP
arrayProto[method].call(this, ...arguments)
render()
}
})
function observer(obj) {
// 把全部的属性定义成set/get的方式
if (Array.isArray(obj)) {
obj.__proto__ = proto
return
}
if (typeof obj == 'object') {
for (let key in obj) {
defineReactive(obj, key, obj[key])
}
}
}
function defineReactive(data, key, value) {
observer(value)
Object.defineProperty(data, key, {
get() {
return value
},
set(newValue) {
observer(newValue)
if (newValue !== value) {
render()
value = newValue
}
}
})
}
observer(obj)
function $set(data, key, value) {
defineReactive(data, key, value)
}
obj.push(123, 55)
console.log(obj) //[1, 2, 3, 123, 55]
复制代码
这种方法将数组的经常使用方法进行重写,进而覆盖掉原生的数组方法,重写以后的数组方法须要可以被拦截。但有些数组操做Vue时拦截不到的,固然也就没办法响应,好比:
obj.length-- // 不支持数组的长度变化
obj[0]=1 // 修改数组中第一个元素,也没法侦测数组的变化
复制代码
ES6提供了元编程的能力,因此有能力拦截,Vue3.0可能会用ES6中Proxy 做为实现数据代理的主要方式。
Proxy
是 JavaScript 2015 的一个新特性。Proxy
的代理是针对整个对象的,而不是对象的某个属性,所以不一样于 Object.defineProperty
的必须遍历对象每一个属性,Proxy
只须要作一层代理就能够监听同级结构下的全部属性变化,固然对于深层结构,递归仍是须要进行的。此外**Proxy
支持代理数组的变化。**
function render() {
console.log('模拟视图的更新')
}
let obj = {
name: '前端工匠',
age: { age: 100 },
arr: [1, 2, 3]
}
let handler = {
get(target, key) {
// 若是取的值是对象就在对这个对象进行数据劫持
if (typeof target[key] == 'object' && target[key] !== null) {
return new Proxy(target[key], handler)
}
return Reflect.get(target, key)
},
set(target, key, value) {
if (key === 'length') return true
render()
return Reflect.set(target, key, value)
}
}
let proxy = new Proxy(obj, handler)
proxy.age.name = '浪里行舟' // 支持新增属性
console.log(proxy.age.name) // 模拟视图的更新 浪里行舟
proxy.arr[0] = '浪里行舟' //支持数组的内容发生变化
console.log(proxy.arr) // 模拟视图的更新 ['浪里行舟', 2, 3 ]
proxy.arr.length-- // 无效
复制代码
以上代码不只精简,并且仍是实现一套代码对对象和数组的侦测都适用。不过Proxy
兼容性不太好!
咱们之因此要观察数据,其目的在于当数据的属性发生变化时,能够通知那些曾经使用了该数据的地方。好比第一例子中,模板中使用了price 数据,当它发生变化时,要向使用了它的地方发送通知。那若是多个Vue实例中共用一个变量,以下面这个例子:
let globalData = {
text: '浪里行舟'
};
let test1 = new Vue({
template:
`<div>
<span>{{text}}</span>
<div>`,
data: globalData
});
let test2 = new Vue({
template:
`<div>
<span>{{text}}</span>
<div>`,
data: globalData
});
复制代码
若是咱们执行下面这条语句:
globalData.text = '前端工匠';
复制代码
此时咱们须要通知 test1 以及 test2 这两个Vue实例进行视图的更新,咱们只有经过收集依赖才能知道哪些地方依赖个人数据,以及数据更新时派发更新。那依赖收集是如何实现的?其中的核心思想就是“事件发布订阅模式”。接下来咱们先介绍两个重要角色-- 订阅者 Dep和观察者 Watcher ,而后阐述收集依赖的如何实现的。
收集依赖须要为依赖找一个存储依赖的地方,为此咱们建立了Dep,它用来收集依赖、删除依赖和向依赖发送消息等。
因而咱们先来实现一个订阅者 Dep 类,用于解耦属性的依赖收集和派发更新操做,说得具体点,它的主要做用是用来存放 Watcher 观察者对象。咱们能够把Watcher理解成一个中介的角色,数据发生变化时通知它,而后它再通知其余地方。
class Dep {
constructor () {
/* 用来存放Watcher对象的数组 */
this.subs = [];
}
/* 在subs中添加一个Watcher对象 */
addSub (sub) {
this.subs.push(sub);
}
/* 通知全部Watcher对象更新视图 */
notify () {
this.subs.forEach((sub) => {
sub.update();
})
}
}
复制代码
以上代码主要作两件事情:
因此当须要依赖收集的时候调用 addSub,当须要派发更新的时候调用 notify。调用也很简单:
let dp = new Dep()
dp.addSub(() => {
console.log('emit here')
})
dp.notify()
复制代码
Vue 中定义一个 Watcher 类来表示观察订阅依赖。至于为啥引入Watcher,《深刻浅出vue.js》给出了很好的解释:
当属性发生变化后,咱们要通知用到数据的地方,而使用这个数据的地方有不少,并且类型还不同,既有多是模板,也有多是用户写的一个watch,这时须要抽象出一个能集中处理这些状况的类。而后,咱们在依赖收集阶段只收集这个封装好的类的实例进来,通知也只通知它一个,再由它负责通知其余地方。
依赖收集的目的是将观察者 Watcher 对象存放到当前闭包中的订阅者 Dep 的 subs 中。造成以下所示的这样一个关系(图参考《剖析 Vue.js 内部运行机制》)。
class Watcher {
constructor(obj, key, cb) {
// 将 Dep.target 指向本身
// 而后触发属性的 getter 添加监听
// 最后将 Dep.target 置空
Dep.target = this
this.cb = cb
this.obj = obj
this.key = key
this.value = obj[key]
Dep.target = null
}
update() {
// 得到新值
this.value = this.obj[this.key]
// 咱们定义一个 cb 函数,这个函数用来模拟视图更新,调用它即表明更新视图
this.cb(this.value)
}
}
复制代码
以上就是 Watcher 的简单实现,在执行构造函数的时候将 Dep.target
指向自身,从而使得收集到了对应的 Watcher,在派发更新的时候取出对应的 Watcher ,而后执行 update
函数。
所谓的依赖,其实就是Watcher。至于如何收集依赖,总结起来就一句话,**在getter中收集依赖,在setter中触发依赖。**先收集依赖,即把用到该数据的地方收集起来,而后等属性发生变化时,把以前收集好的依赖循环触发一遍就好了。
具体来讲,当外界经过Watcher读取数据时,便会触发getter从而将Watcher添加到依赖中,哪一个Watcher触发了getter,就把哪一个Watcher收集到Dep中。当数据发生变化时,会循环依赖列表,把全部的Watcher都通知一遍。
最后咱们对 defineReactive 函数进行改造,在自定义函数中添加依赖收集和派发更新相关的代码,实现了一个简易的数据响应式。
function observe (obj) {
// 判断类型
if (!obj || typeof obj !== 'object') {
return
}
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
function defineReactive (obj, key, value) {
observe(value) // 递归子属性
let dp = new Dep() //新增
Object.defineProperty(obj, key, {
enumerable: true, //可枚举(能够遍历)
configurable: true, //可配置(好比能够删除)
get: function reactiveGetter () {
console.log('get', value) // 监听
// 将 Watcher 添加到订阅
if (Dep.target) {
dp.addSub(Dep.target) // 新增
}
return value
},
set: function reactiveSetter (newVal) {
observe(newVal) //若是赋值是一个对象,也要递归子属性
if (newVal !== value) {
console.log('set', newVal) // 监听
render()
value = newVal
// 执行 watcher 的 update 方法
dp.notify() //新增
}
}
})
}
}
class Vue {
constructor(options) {
this._data = options.data;
observer(this._data);
/* 新建一个Watcher观察者对象,这时候Dep.target会指向这个Watcher对象 */
new Watcher();
console.log('模拟视图渲染');
}
}
复制代码
当 render function 被渲染的时候,读取所需对象的值,会触发 reactiveGetter 函数把当前的 Watcher 对象(存放在 Dep.target 中)收集到 Dep 类中去。以后若是修改对象的值,则会触发 reactiveSetter 方法,通知 Dep 类调用 notify 来触发全部 Watcher 对象的 update 方法更新对应视图。
最后咱们依照下图(参考《深刻浅出vue.js》),再来回顾下整个过程:
new Vue()
后, Vue 会调用_init
函数进行初始化,也就是init 过程,在 这个过程Data经过Observer转换成了getter/setter的形式,来对数据追踪变化,当被设置的对象被读取的时候会执行getter
函数,而在当被赋值的时候会执行 setter
函数。setter
, setter
通知以前依赖收集获得的 Dep 中的每个 Watcher,告诉它们本身的值改变了,须要从新渲染视图。这时候这些 Watcher就会开始调用 update
来更新视图。给你们推荐一个好用的BUG监控工具Fundebug,欢迎免费试用!
欢迎关注公众号:前端工匠,你的成长咱们一块儿见证!