跳到主要内容

Vue-双向绑定

可观测数据

Vue中的数据绑定是通过Object.defineProperty这个Api实现的.

下面是一个简单的例子:

let car = {}
let val = 3000
Object.defineProperty(car, 'price', {
enumerable: true,
configurable: true,
get(){
console.log('price属性被读取了')
return val
},
set(newVal){
console.log('price属性被修改了')
val = newVal
}
})

在读取或者设置car.price的时候, get()set()就会被触发.

将一个对象的所有属性都变得如同该属性一样, 可以通过一个封装的Obeserver类来实现:

// 源码位置:src/core/observer/index.js

/**
* Observer类会通过递归的方式把一个对象的所有属性都转化成可观测对象
*/
export class Observer {
constructor (value) {
this.value = value
// 给value新增一个__ob__属性,值为该value的Observer实例
// 相当于为value打上标记,表示它已经被转化成响应式了,避免重复操作
def(value,'__ob__',this)
if (Array.isArray(value)) {
// 当value为数组时的逻辑
// ...
} else {
this.walk(value)
}
}

walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
}
/**
* 使一个对象转化成可观测对象
* @param { Object } obj 对象
* @param { String } key 对象的key
* @param { Any } val 对象的某个key的值
*/
function defineReactive (obj,key,val) {
// 如果只传了obj和key,那么val = obj[key]
if (arguments.length === 2) {
val = obj[key]
}
if(typeof val === 'object'){
new Observer(val)
}
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get(){
console.log(`${key}属性被读取了`);
return val;
},
set(newVal){
if(val === newVal){
return
}
console.log(`${key}属性被修改了`);
val = newVal;
}
})
}

在这里的代码中, 定义了observer类, 用来将一个正常的Object转换成可观测的object, 并且给value新增一个__ob__属性, 值为该valueObserver实例, 这个操作相当于给value打上标记, 表示已经为转换为响应式了, 避免重复操作.

通过判断数据的类型, 只有object类型的数据才会调用walk将每一个属性转换成getter/setter的形式来侦测变化. 最后, 在defineReactive中传入的属性值是一个obejct时使用new observer(val)来继续递归子属性. 这样就讲一个对象所有的属性都添加监听了.

用这种方法定义一个响应式变量:

let car = new Observer({
'brand':'BMW',
'price':3000
})

依赖收集

Vue的运行原理是: 数据=>视图. 当数据发生变化的时候就会通知视图更新. 如何通知? 在Vue中的处理方法是通过依赖收集.

在可观测的数据被获取时会触发getter属性, 因此可以在getter中收集这个依赖, 同样的, 当这个数据变化时会触发setter属性, 所以可以在setter中通知依赖进行更新.

将这个依赖收集的过程抽象为一个依赖管理器, 就得到了Dep类:

// 源码位置:src/core/observer/dep.js
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)
}
}
// 通知所有依赖更新
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}

/**
* Remove an item from an array
*/
export function remove (arr, item) {
if (arr.length) {
const index = arr.indexOf(item)
if (index > -1) {
return arr.splice(index, 1)
}
}
}

在上面的依赖管理器Dep类中, 先初始化了一个subs数组, 用来存放依赖, 并且定义了几个实例方向用来对依赖进行添加, 删除, 通知等操作.

有了依赖管理器之后, 就可以在getter中收集依赖, 在setter中通知依赖的更新:

function defineReactive (obj,key,val) {
if (arguments.length === 2) {
val = obj[key]
}
if(typeof val === 'object'){
new Observer(val)
}
const dep = new Dep() //实例化一个依赖管理器,生成一个依赖管理数组dep
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get(){
dep.depend() // 在getter中收集依赖
return val;
},
set(newVal){
if(val === newVal){
return
}
val = newVal;
dep.notify() // 在setter中通知依赖更新
}
})
}

依赖观察者

Vue中还实现了一个Watcher的类, 而Watcher类的实例就是依赖的观察者, 也就是依赖的使用者. 数据变化时, 我们不直接通知依赖更新, 而是通知依赖对应的Watch实例, 由Watcher实例去通知真正的视图:

export default class Watcher {
constructor (vm,expOrFn,cb) {
this.vm = vm;
this.cb = cb;
this.getter = parsePath(expOrFn)
this.value = this.get()
}
get () {
window.target = this;
const vm = this.vm
let value = this.getter.call(vm, vm)
window.target = undefined;
return value
}
update () {
const oldValue = this.value
this.value = this.get()
this.cb.call(this.vm, this.value, oldValue)
}
}

/**
* Parse simple path.
* 把一个形如'data.a.b.c'的字符串路径所表示的值,从真实的data对象中取出来
* 例如:
* data = {a:{b:{c:2}}}
* parsePath('a.b.c')(data) // 2
*/
const bailRE = /[^\w.$]/
export function parsePath (path) {
if (bailRE.test(path)) {
return
}
const segments = path.split('.')
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
}

谁使用了数据, 谁就是依赖, 我们就为它创建一个watcher实例, 在创建Watcher实例的过程中会自动的把自己添加到这个数据对应的依赖管理器中, 以后这个Watcher实例就代表这个依赖, 当数据发生变化的时候, 就有Watcher去通知真正的依赖.

Watcher类的代码逻辑如下:

  1. 实例化Watcher类, 执行其构造函数
  2. 在构造函数中调用this.get()实例方法
  3. get()方法中, 首先通过window.target=this把实例自身赋给一个全局的唯一对象window.target上, 然后通过let value=this.getter.call(vm,vm)获取被依赖的数据, 获取被依赖数据的目的是触发数据上的getter, 在getter中调用dep.depend()收集依赖, 在dep.depend()中取到挂载window.target上的值并存入依赖数据, 在get()方法的最后将window.target释放.
  4. 当数据变化时, 会触发数据的setter, 在setter中调用了dep.botify()方法, 在dep.notify()中比耐力所有的依赖, 执行依赖的update方法, 在该方法中调用更新回调, 更新界面

image

需要注意的是, Object.defineProperty方法实现了对object的数据监控, 但是只能观察到取值和赋值, 而无法观察到属性的添加和删除, 因此需要借助额外的Vue.setVue.delete进行处理.

数组的观察

由于Object.defineProperty只能观察Object而不能观察Array, 所以Vue设计了另外一套双向绑定机制.

由于我们在使用Vue的时候一般会把数组定义在Data中, 也就是一个对象中, 所以也可以在define进行依赖收集.

data(){
return {
arr:[1,2,3]
}
}

但是数组数据的变化则需要通过另一套机制来实现

数组方法拦截器

Vue中, 对于Array数据的变化的检测则是通过重写数组方法来实现的, 比如下面的push方法:

let arr = [1,2,3]
arr.push(4)
Array.prototype.newPush = function(val){
console.log('arr被修改了')
this.push(val)
}
arr.newPush(4)

本质上, Vue创建了一个数组方法拦截器, Array原型中拦截器主要有push,pop,shift,unshift,splice,sort,reverse, 代码如下:

// 源码位置:/src/core/observer/array.js

const arrayProto = Array.prototype
// 创建一个对象作为拦截器
export const arrayMethods = Object.create(arrayProto)

// 改变数组自身内容的7个方法
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]

/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
const original = arrayProto[method] // 缓存原生方法
Object.defineProperty(arrayMethods, method, {
enumerable: false,
configurable: true,
writable: true,
value:function mutator(...args){
const result = original.apply(this, args)
return result
}
})
})

实际上在调用这些方法的时候会先调用mutator函数来执行发送变化通知.

然后把这个拦截器挂载到数据实例与Array.prototype之间:

// 源码位置:/src/core/observer/index.js
export class Observer {
constructor (value) {
this.value = value
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
} else {
this.walk(value)
}
}
}
// 能力检测:判断__proto__是否可用,因为有的浏览器不支持该属性
export const hasProto = '__proto__' in {}

const arrayKeys = Object.getOwnPropertyNames(arrayMethods)

/**
* Augment an target Object or Array by intercepting
* the prototype chain using __proto__
*/
function protoAugment (target, src: Object, keys: any) {
target.__proto__ = src
}

/**
* Augment an target Object or Array by defining
* hidden properties.
*/
/* istanbul ignore next */
function copyAugment (target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}

数组依赖收集

数据数组的依赖同样的getter中收集, 而给数组数据添加getter/setter都是在Observe中完成的, 所以我们也应该在Observer类中收集依赖:

// 源码位置:/src/core/observer/index.js
export class Observer {
constructor (value) {
this.value = value
this.dep = new Dep() // 实例化一个依赖管理器,用来收集数组依赖
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
} else {
this.walk(value)
}
}
}

依赖管理器定义在Observer类中, 而我们需要在getter中收集依赖, 也就是说必须在 getter中能够访问到Observer的依赖管理器:

function defineReactive (obj,key,val) {
let childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get(){
if (childOb) {
childOb.dep.depend()
}
return val;
},
set(newVal){
if(val === newVal){
return
}
val = newVal;
dep.notify() // 在setter中通知依赖更新
}
})
}

/**
* Attempt to create an observer instance for a value,
* returns the new observer if successfully observed,
* or the existing observer if the value already has one.
* 尝试为value创建一个0bserver实例,如果创建成功,直接返回新创建的Observer实例。
* 如果 Value 已经存在一个Observer实例,则直接返回它
*/
export function observe (value, asRootData){
if (!isObject(value) || value instanceof VNode) {
return
}
let ob
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else {
ob = new Observer(value)
}
return ob
}

通知依赖:

首先要能访问到依赖, 然后调用依赖管理器的dep.notify()方法, 让它去通知以来更新:

/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
// notify change
ob.dep.notify()
return result
})
})

由于拦截器是挂载到数组数据的原型上, 所以拦截器中的this就是数据value, 拿到value上的Observer类实例, 从而就可以调用Observer类实例上面依赖管理器的dep.notify()方法, 以达到通知依赖的目的.

深度检测

前文的Array数据的变化侦测仅仅是数组自身变化的侦测, 比如数组新增一个元素或者删除一个元素, 在Vue中, 不论是Object类型或者Array型数据所实现的数据变化侦测都是深度侦测, 所谓深度检测就是不但侦测数据自身的变化, 还要侦测数据中所有子数据的变化.

比如:

let arr = [
{
name:'NLRX'
age:'18'
}
]

在源码中实现如下:

export class Observer {
value: any;
dep: Dep;

constructor (value: any) {
this.value = value
this.dep = new Dep()
def(value, '__ob__', this)
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value) // 将数组中的所有元素都转化为可被侦测的响应式
} else {
this.walk(value)
}
}

/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}

export function observe (value, asRootData){
if (!isObject(value) || value instanceof VNode) {
return
}
let ob
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else {
ob = new Observer(value)
}
return ob
}

Array数据中, 调用observerArray()方法, 比那里数组中的每一个元素, 通过调用observer函数, 将每一个元素转化为可侦测的响应式数据.

而对应object数据, 在defineReactive函数中进行了递归操作.

数组新增元素的侦测

对于新增元素的侦测, 需要先拿到这个新增的元素, 然后调用observer函数将其转化. 分别监听push,unshift,splice三个方法进行处理:

/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args // 如果是push或unshift方法,那么传入参数就是新增的元素
break
case 'splice':
inserted = args.slice(2) // 如果是splice方法,那么传入参数列表中下标为2的就是新增的元素
break
}
if (inserted) ob.observeArray(inserted) // 调用observe函数将新增的元素转化成响应式
// notify change
ob.dep.notify()
return result
})
})

在拦截器定义代码中, 如果是pushunshift方法, 那么传入参数就是新增的元素, 如果是splice方法, 那么传入参数列表中下标为2的就是新增元素吗拿到新增的元素, 就可以调用observe函数将新增的元素转化为响应式.

不足

只有通过数组方法对数组进行操作时才能侦测到, 但是数组下标则不可以, 因此需要通过Vue.setVue.delete来手动的侦测.

实现一个简单的双向绑定

模板:

<div>{{ name }}</div>

解析模板添加订阅:

function observe(obj) {
// 判断类型
if (!obj || typeof obj !== 'object') {
return;
}
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key]);
});
}

function defineReactive(obj, key, val) {
// 递归子属性
observe(val);
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 val;
},
set: function reactiveSetter(newVal) {
console.log('change value');
val = newVal;
// 执行 watcher 的 update 方法
dp.notify();
}
});
}

// 通过 Dep 解耦
class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
// sub 是 Watcher 实例
this.subs.push(sub);
}
notify() {
this.subs.forEach(sub => {
sub.update();
});
}
}
// 全局属性,通过该属性配置 Watcher
Dep.target = null;

function update(value) {
document.querySelector('div').innerText = value;
}

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];
// 调用 update 方法更新 Dom
this.cb(this.value);
}
}
var data = { name: 'yck' };
observe(data);
// 模拟解析到 `{{name}}` 触发的操作
new Watcher(data, 'name', update);
// update Dom innerText
data.name = 'yyy';

Proxy 与 Object.defineProperty 的区别

Object.defineProperty 虽然已经能够实现双向绑定了,但是他还是有缺陷的。

  • 对一个对象进行删除和添加属性操作时无法劫持的
  • 如果存在深层的嵌套对象关系, 需要深层的递归进行监听, 造成性能的极大问题
  • 数组API的方法也无法监听到

Proxy 就没以上的问题,原生支持监听数组变化,并且可以直接对整个对象进行拦截:

let onWatch = (obj, setBind, getLogger) => {
let handler = {
get(target, property, receiver) {
getLogger(target, property);
return Reflect.get(target, property, receiver);
},
set(target, property, value, receiver) {
setBind(value);
return Reflect.set(target, property, value);
}
};
return new Proxy(obj, handler);
};

let obj = { a: 1 };
let value;
let p = onWatch(
obj,
v => {
value = v;
},
(target, property) => {
console.log(`Get '${property}' = ${target[property]}`);
}
);
p.a = 2; // bind `value` to `2`
p.a; // -> Get 'a' = 2

参考链接