vue2源码解析双向绑定
Vue.js是采用数据劫持结合发布-订阅模式,通过Object.defineproperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象
Object.defineProperty()使用方法
Object.defineProperty(obj,prop,descriptor)
- obj:要添加或者修改属性的对象
- prop:要添加或者修改属性的名称或Symbol
- descriptor:要添加或者修改的属性描述符
let person = {
name:'张三',
gender:'男'
}
Object.defineProperty(person,'age',{value:'12'})
console.log(person)//=>{name:'张三',gender:'男',age:12}
题外话
如果进入下面的if语句
if(a ===1 && a === 2 && a === 3){
console.log('You win!')
}
解决方法:调用Object.defineProperty()
let default = 0
Object.defineProperty(window,'a',{
get(){
return ++ default
}
})
if(a ===1 && a === 2 && a === 3){
console.log('You win!')
}
Object.defineProperty()大概思路及流程:
思路:
-
首先对数据进行劫持监听,设置一个Observer函数,用来监听所有属性的变化
-
属性发生变化,告诉订阅者watcher是否需要更新数据,如果订阅者有多个,则需要一个Dep收集订阅者,在监听器observer和watcher之间进行统一管理。
-
需要一个指令解析器compile,对需要监听的节点和属性进行扫描和解析。
流程:
第一步:需要一个监听器Observer,用来劫持并监听所有属性,如果属性发生变化,就通知订阅者。
- observe()方法的目的:循环遍历数据对象的每个数据(forEach),进行监听调用defineReactive()函数
- defineReactive()方法:为数据添加检测(调用Object.defineProperty()的get()、set()属性)
/*
* 循环遍历数据对象的每个属性,进行监听
*/
function observe(data){
if(!data || typeof data !== 'object'){
return
}
let keys = Object.keys(data)
keys.forEach((key) =>{
defineReactive(data,key,data[key])
})
}
/*
* 为数据添加检测
*/
function defineReactive(data,key,val){
Object.defineProperty(obj,key,{
get(){
console.log(`${key}属性被读取。。。`)
return val
},
set(newVal){
console.log(`${key}属性被修改。。。`)
val = newVal
}
})
}
第二步:实现一个订阅器Dep,用来收集订阅者,对监听器Observer和订阅者watcher进行统一管理
订阅器Dep主要负责收集订阅者,然后当数据变化时执行对应订阅者的更新函数。
/*
* 创建消息订阅器Dep
*/
function Dep(){
this.subs = []
}
Dep.prototype = {
addSub:function(sub){
this.subs.push(sub)
}
notify:function(){
this.subs.forEach(function(sub){
sub.update()
})
}
}
Dep.target = null
有了订阅器,再对defineReactive函数改造,向其植入订阅器
defineReactive: function(data, key, val) {
var dep = new Dep();
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function getter () {
if (Dep.target) {
dep.addSub(Dep.target);
}
return val;
},
set: function setter (newVal) {
if (newVal === val) return;
val = newVal;
dep.notify();
}
});
}
总结:设计了一个订阅器Dep类,该类里面定义了一些属性和方法,Dep.target是一个静态属性,是全局唯一的watcher,因为在统一时间只能有一个全局的watcher被计算,另外它的自身属性subs也是watcher的数组。
第三步:实现一个订阅者Watcher,可以收到属性的变化通知并执行相应的方法,从而更新视图
只要在订阅者watcher初始化的时候处罚对应的get函数执行添加订阅者操作即可。
function Watcher(vm, exp, cb) {
this.cb = cb;
this.vm = vm;
this.exp = exp;
this.value = this.get(); // 将自己添加到订阅器的操作
}
Watcher.prototype = {
update: function() {
this.run();
},
run: function() {
var value = this.vm.data[this.exp];
var oldVal = this.value;
if (value !== oldVal) {
this.value = value;
this.cb.call(this.vm, value, oldVal);
}
},
get: function() {
Dep.target = this; // 缓存自己,用于判断是否添加watcher。
var value = this.vm.data[this.exp]; // 强制执行监听器里的get函数
Dep.target = null; // 释放自己
return value;
},
};
第四步: 实现一个解析器Compile,可以解析每个节点的相关指令,对模板数据和订阅器进行初始化。
解析模板指令,并替换模板数据,初始化视图 将模板指令对应的节点绑定对应的更新函数,初始化相应的订阅器
Object.defineProperty()的缺点
-
vue2是一次递归到底的来实现响应式的 -
Vue无法检测到对象属性的新增或删除的变化只能劫持对象的属性,需要对每个对象,每个属性进行遍历。如果属性值是对象,就需要深度遍历。
- 对象新增属性的修改使用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式 property。
- 对象删除属性,可以使用Vue.delete(obj,propertyName/index)或者vue.$delete(obj,propertyName/index)
- 对象赋值多个新的属性,可以使用 Object.assign() 或 _.extend()。这样添加到对象上的新 property 不会触发更新。在这种情况下,你应该用原对象与要混合进去的对象的 property 一起创建一个新的对象。
this.someObject = Object.assign({},
this.someObject, { a: 1, b: 2 })
-
Vue不能检测数组的变化- 当你利用索引直接设置一个数组项时。Vue.set(vm.items, indexOfItem, newValue)
- 当你修改数组的长度时。vm.items.splice(newLength)
- vue2中是如何实现数组的响应式的?重写数组的部分方法实现响应式,限制在数组的push/pop/shift/unshift/splice/reverse七个方法
Proxy
Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
const p = new Proxy(target, handler)
-
target是要包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。 -
handler代理配置:带有钩子的对象,比如get钩子用于读取target属性,set钩子写入target属性等。
Proxy 源码解析
Proxy(data,{
get(target,key){
return target[key]
},
set(target,key,value){
let val = Reflect.set(target,key,value)
_that.$dep[key].forEach(item => item.update())
return val
}
})
proxy已知的两个问题
- Proxy本身不支持对象内部的深度检测,需要自己实现
- Proxy本身支持数组变化侦测,但会有很多次触发的风险
例子
const obj = {
info:{
name:'eason',
blogs:['webpack','babel','postcss']
}
}
function handler(){}
function createReactive(data,handler){
let res = {}
for(let key in data){
if(typeof data[key] === 'object'){
res[key] = createReactive(data[key],handler)
}else {
res[key] = data[key]
}
}
return new Proxy(res,{
get(target,key){
return Reflect.get(target,key)
},
set(target,key,val){
handler()
return Reflect.set(target,key,value)
}
})
}
let proxy = createReactive(obj,handler)
解析后的结果:
可以看到,对象内部的对象和数组都已经被代理了,但是当object是一个非常大且复杂的对象时,性能就不好。
修改如下
const obj = {
info:{
name:'eason',
blogs:['webpack','babel','postcss']
}
}
const handler = {
get(target,key,receiver){
const res = Reflect.get(target,key,reveiver)
// 创建Proxy并返回
if(isObject(res)){
return createReactiveObject(target[key],handler)
}else {
return res
}
},
set(target,key,value,receiver){
const res = Reflect.set(target,key,value,receiver)
return res
}
}
function createReactiveObject(target,handler){
observed = new Proxy(target,handler)
return observed
}
let proxy = createReactiveObject(obj,rawToReactive,reactiveToRaw,handler)
Proxy的优点
- Proxy直接代理整个对象而非对象属性,返回一个新对象
- Proxy可以监听数组的变化,多种拦截方法