相信每位前端人都被问过Vue
双向数据绑定的原理是什么吧?应该也很快能答出来是经过Object.defineProperty让数据的每一个属性变成getter/setter
实现的,但这仅仅只回答了一半,由于Object
和Array
的实现方式是不同的,这也是为何标题是Object
篇的缘由。(建议先看总结,再一步步看实现过程)javascript
首先了解一下下面的概念:前端
这个概念就通俗点说了,想详细了解的可自行查阅资料java
// 给定一个数组 arr = [1, 2, 3], 想要一个新的数组每一项都加一
const arr = [1, 2, 3];
// 命令式 告诉浏览器循环数组,每个元素+1,而后push进新数组
let newArr1 = [];
for (let i = 0; i < arr.length; i++) {
newArr1.push(arr[i]+1);
}
console.log(newArr1) // 拿到新数组
// 声明式 告诉浏览器新数组的每一项是旧数组对应的每一项加一
let result = arr.map(item => {
return item + 1;
})
console.log(result) // 新的数组
复制代码
为何要说这个呢?由于Vue.js是声明式的,按API文档的要求来写Vue就知道要作什么。 (回头看好像偏题了,无论了就当巩固知识吧😂)编程
在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象, Vue.js是利用这个方法修改data
对象的属性。数组
let name = 'test';
let obj = {};
Object.defineProperty(obj, 'name', {
configurable: true, // 可修改,可删除
enumerable: true, //可枚举
get: function() { // 读值触发
console.log('读取数据');
return name;
},
set: function(newVal) { // 赋值触发
if(name === newVal){
return;
}
console.log('从新赋值');
name = newVal;
}
})
console.log(obj.name);
obj.name = '赋值';
//打印出
// 读取数据
// test
// 从新赋值
// 赋值
复制代码
到这里已经算是Vue的Object
双向数据绑定原理了。浏览器
实现完整的Object
对象的双向数据绑定,Vue作了那些操做呢?函数
经过上面的概念介绍就知道Object.defineProperty
是作数据监控的,获取值的时候get
被触发进行相应操做,设置数据时,set
被触发这时就能知道数据是否被改变。那咱们是否是就很清楚知道能够在数据被调用触发get
函数的时候,去收集那些地方使用了对应的数据了呢?而后在设置的时候,触发set
函数去通知get
收集好的依赖进行相应的操做呢?好了,下面就针对目前这个理解,对Object.defineProperty
进行封装工具
function defineReactive(data, key, val) {
//let dep = [];
let dep = new Dep() // 修改
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function() {
// 收集依赖
// dep.push(window.target) // window.target后面会定义,很6的操做,期待一下
dep.depend() // 修改
return val
},
set: function(newVal){
if(val === newVal){
return
}
// 触发依赖
// for(let i=0; i<dep.length; i++){
// dep[i](newVal, val);
// }
dep.notify() // 修改
val = newVal
}
})
}
复制代码
这里就实现了在get
的时候,收集依赖保存到dep
这个数组中,当触发set
的时候,就把dep
中的每一个依赖触发。在源码里是把dep
封装成一个类,来管理依赖的,下面就实现一下Dep
这个类吧。ui
export default class Dep {
constructor() {
this.subs = []
}
addSub(sub) {
this.subs.push(sub)
}
removeSub(sub) {
remove(this.subs, sub)
}
depend() {
if(window.target){
this.addSub(window.target) // window.target是什么?
}
}
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update() // window.target的update方法
}
}
}
function remove (arr, item) {
if(arr.length){
const index = arr.indexOf(item)
if(index > -1){
return arr.splice(index, 1)
}
}
}
复制代码
这样咱们封装的Dep
类就能够收集依赖、删除依赖、通知依赖,那咱们就要把这个Dep
类用上,对上面的defineReactive
进行修改一下。Dep收集到的依赖看代码都知道是window.target
,当数据发生变化的时候,调用window.target
的update
方法进行响应更新。this
源码里有个Watcher
类,它的实例就是咱们收集的window.target
,下面先来看看Vue中的一个用法
vm.$watch('user.name', function(newVal, oldVal){
console.log('个人新名叫' + newVal); // 就是update函数
})
复制代码
当Vue实例中的data.user.name
被修改时,会触发function
的执行,也就是说须要把这个函数添加到data.user.name
的依赖中,怎么收集呢?是否是调一下data.user.name
的get
方法就能够了。那么Watcher
要作的就是把本身的实例添加到对应属性的Dep
中,同时也有通知去更新的能力,下面写下Watcher
export default class Watcher {
constructor (vm, expOrFn, cb) {
this.vm = vm
this.getter = parsePath(expOrFn);
this.cb = cb;
this.value = this.get() // 获取初始值
}
get() {
window.target = this // 把当前实例暴露给Dep,Dep就知道依赖是谁了
let value = this.getter.call(this.vm, this.vm) // 取一下值,触发vm实例上对应属性的get方法收集依赖
window.target = undefined // 用完给别人用
return value
}
update() {
const oldValue = this.value // 旧值
this.value = this.get() // 获取新值
this.cb.call(this.vm, this.value, oldValue)
}
}
复制代码
到这里再回顾一下上面写好的几个程序,你会发现之间都是很巧妙的结合了,特别是Watcher
的实例,把本身给添加到Dep
中了,反正我本身是以为这操做特6。这里也说明了Vue中de$watch
是经过Watcher
实现的。
固然parsePath
还没说是什么,结合上面的例子和Watcher
应该知道parsePath
返回的是一个方法,而且被调用后返回一个值,也就是获取值的功能,下面来实现一下
const bailRE = /[^\w.$]/
export function parsePath (path) {
if (bailRE.test(path)) {
return
}
const segments = path.spilt('.')
return function(obj){
for(let i = 0; i < segments.length; i++){
if(!obj) return
obj = obj[segments[i]]
}
return obj
}
}
复制代码
Watcher中的this.getter.call(this.vm, this.vm)
把parsePath
的返回的函数指向this.vm
,并把this.vm
当参数传过去取值。
Vue中data
的每个属性都会被监测到,实际上咱们使用defineReactive
就能够监测,若是一个data
有不少属性,那是否是要调用不少次呢,那么就有了Observer
这个工具类把每一个属性变成getter/setter
,来码上
export class Observer {
constructor(value) {
this.value = value
if(!Array.isArray(value)){
this.walk(value)
}
}
walk(obj) {
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
}
复制代码
那么new Observer(obj)
就能把obj
下的属性都变成getter/setter
了,若是obj[key]
依然是一个对象呢?是否是要继续new Observer(obj[key])
呀,那么就是defineReactive
拿到obj[key]
时,须要进行判断是否是对象,是的话就进行递归,那么加上这一步操做
function defineReactive(data, key, val) {
if(typeof val === 'object') {
new Observer(val)
}
let dep = new Dep()
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function() {
dep.depend()
return val
},
set: function(newVal){
if(val === newVal){
return
}
dep.notify()
val = newVal
}
})
}
复制代码
到这里Vue中Object
是数据响应已经完成了,可是有缺陷你们都很清楚,就是给data
新增属性或者删除属性时,没法监测,上面的实现过程都是依赖现有属性进行的,可是Vue提供$set
和$delete
去实现这两个功能,相信弄懂上面的代码,这两个的实现就不难了。
对Vue中Object
的数据响应,我总结的一句话就是“定义getter/setter
备用,“用”:收集依赖,“变”:触发依赖
Observer
和defineReactive
把属性变成getter/setter
;Watcher
在getter
中把依赖收集到dep
;setter
告诉dep
数据变化了,dep
通知Watcher
去更新;