从数据绑定开始
数据绑定是目前主流前端框架普及的一个重要原因,它们让开发者专注于处理数据而非DOM的实现。Angular是基于scope
的脏检查机制,React是组件的state
,Vue则是基于Object.defineProperty
,今天我们将Vue的数据绑定原理和新API的特性和优势。
Vue是双向绑定吗?
不是,原则上Vue的子组件不能改变父组件传下来的数据(prop),但可以通过v-model
这样的语法糖去实现,事实上,Vue和React十分类似,都是采用了单向数据流,这样做更有利于状态的追踪和管理。
Vue如何实现数据绑定
可以分成2部分:数据监听=>数据映射
映射很好理解,数据传入模板或者render函数编译成虚拟DOM,虚拟DOM保存了节点的标签(如div h3等)、绑定的数据(data、methods、props、computed等)以及子节点/关联节点,再根据虚拟DOM的信息映射成真实DOM
数据监听基于Object.defineProperty
,下面开始着重介绍
Object.defineProperty(以下简称OD)
这是JavaScript定义对象属性的一个api,通过调用该方法,我们可以定义一个对象的属性及属性描述符,可以理解为属性的属性
Object.defineProperty(object, key, descriptor)
其中,descriptor可以定义如下内容:
详情参考mdn文档
interface PropertyDescriptor {
configurable?: boolean; // 可以对该属性进行删改
enumerable?: boolean; // 是否可以被for in 或者 Object.key迭代获取
value?: any; // 属性值,默认为undefined
writable?: boolean; //是否可以赋值
get?(): any; // 如果定义了getter,当获取到这个属性后,无视默认值,读取getter的返回值
set?(v: any): void; // 对这个属性赋值后触发的回调
}
既然能够通过劫持对象的获取与设置,那么这里边就可以做一些文章了 ,比如我想设计一个高温预警系统,当温度达到40度时发出警告:
const Temperature = {
degree: 28
}
Object.defineProperty (Temperature, 'degree', {
set (value) {
if (value > 40) { alert('高温红色预警!') }
}
})
Temperature.degree = 28 // 不会触发预警
Temperature.degree = 41 // 触发预警
Vue的监听机制同理,当一个data对象定义时,Vue会对data所有的属性设置setter和getter。假设我有一个组件:
export default {
data () {
foo: 1
},
template: `<h3>{{foo}}</h3>`
}
工作原理如下
先对Data定义属性'foo'并添加getter/setter和Dep(dependeny依赖)
当访问foo字段时,触发了getter(1.),getter函数中先收集
Data.foo
依赖(2.),再返回返回初始值value(3.)。当foo值改变后,触发了setter(4.),setter函数中通知Dep(5.)进行更新,通过更新调度后最终返回更新后的结果(6.)。
Object.defineProperty的问题
1.对每一个key都要添加描述符:
在我之前的文章提到过,data中的每一个属性都有监听,这样做比较浪费JavaScript的开销,无法监听到对象属性的添加和删除(需要通过Vue.set和Vue.delete处理)
2.无法响应对象的增删和数组的长度等方法:
如果直接往对象里添加一个属性(如往o = {a:1}
中添加o.b = 2
),或者改变数组长度,Vue无法触发监听
var a = [1,2,3,4,5]
a.forEach((v,k,a)=>{
Object.defineProperty(a,k, {
get: ()=>{console.log('你获取了a'); return v},
set: (newVal)=>{ alert(`你设置了${newVal}`)}
})
})
a.push(6) // 没有任何反应
a.length = 2 // 没有任何反应
对此,Vue对数组的方法如pop、push、sort等提供了响应补丁,还提供了Vue.set方法做兼容处理。
Proxy
既然OD方法存在这些方面的缺陷,那么使用Proxy无疑是很好的替代品:
Proxy(target, handler)
区别于OD,我们可以对整个对象进行监听操作,且看MDN文档示例代码:
let handler = {
get: function(target, name){
return name in target ? target[name] : 37;
}
};
let p = new Proxy({}, handler);
p.a = 1;
p.b = undefined;
console.log(p.a, p.b); // 1, undefined
console.log('c' in p, p.c); // false, 37
而且对数组也能劫持:
var proxyArr = new Proxy(arr, {
get (target, key) {
alert(`你获取了${target}.${key}`)
return target[key]
},
set (target, key, value) {
alert(`你设置了${target}.${key}->${value}`)
}
})
proxyArr[0] // 触发访问元素下标getter
proxyArr.sort // 触发访问数组方法的getter
proxyArr.length = 2 // 触发setter
proxyArr.push(1) // 触发setter和每个数组遍历的getter
另外,Proxy跟Reflect时相辅相成的,参见https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect
其中,Reflect.get(foo, 'key')
等价于foo.key
,Reflect.set(foo,'key','value')
等价于foo.key = 'value'
,Reflect.has(foo, 'key')
等价于'key' in foo
一般来说,需要在Proxy的Hanlder中使用Reflect的API,那么上面的handler.get
应该改为:
function(target, name){
return Reflect.has(target, name) ? Reflect.get(target, name) : 37;
}
Vue3.0中的Proxy
上个月(19年10月),Vue3.0 - vue-next
在github开放,根据上文介绍的Proxy和Reflect,我们来看看3.0如何使用Proxy做响应式数据的:
响应式的代码在packages/reactivity/src/reactive.ts
中,我省略了边界判断的代码,直接上主线:
首先导入Proxy需要的Hanlder (这里先不讲,我们在后面解释)
import {
mutableHandlers, // 可变代理Handlers
/* 省略其他Handlers */
} from './baseHandlers'
import {
mutableCollectionHandlers, // 专门针对Set/Map/WeakSet/WeakMap的Hanlers
} from './collectionHandlers'
创建2个Map,用来存储原始数据与响应式数据的相互映射
// WeakMaps that store {raw <-> observed} pairs.
const rawToReactive = new WeakMap<any, any>() // 原始对应响应式
const reactiveToRaw = new WeakMap<any, any>() // 响应式对应原始
这样一来,原始与响应之间可以双向映射;
接下来,利用这2种映射表,创建一个入口函数,传入原始值、映射表和Hanlders
export function reactive(target: object) {
return createReactiveObject(
target,
rawToReactive,
reactiveToRaw,
mutableHandlers,
mutableCollectionHandlers
)
}
function createReactiveObject(
target: unknown, // 原数据
toProxy: WeakMap<any, any>, // rawToReactive
toRaw: WeakMap<any, any>, // reactiveToRaw
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
) {
// 省略原数据边界检查代码
//
const handlers = collectionTypes.has(target.constructor)
? collectionHandlers
: baseHandlers
observed = new Proxy(target, handlers)
toProxy.set(target, observed) // 响应map中添加响应与原始映射
toRaw.set(observed, target) // 原始map中添加原始与响应映射
if (!targetMap.has(target)) {
targetMap.set(target, new Map())
}
return observed
}
回过头来看Handlers代码
export const mutableHandlers: ProxyHandler<object> = {
get: createGetter(false),
set,
deleteProperty,
has,
ownKeys
}
分析其中的getter和Setter
function createGetter(isReadonly: boolean, unwrap = true) {
return function get(target: object, key: string | symbol, receiver: object) {
let res = Reflect.get(target, key, receiver)
if (unwrap && isRef(res)) {
res = res.value
} else {
track(target, OperationTypes.GET, key)
}
return isObject(res)
? isReadonly
? // need to lazy access readonly and reactive here to avoid
// circular dependency
readonly(res)
: reactive(res)
: res
}
}
getter中先执行了track函数(对应了2.x的Dep.depend),再根据值的类型返回原始值/只读类型和递归响应式的值。
function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
value = toRaw(value)
const oldValue = (target as any)[key]
if (isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
}
const hadKey = hasOwn(target, key)
const result = Reflect.set(target, key, value, receiver)
// don't trigger if target is something up in the prototype chain of original
if (target === toRaw(receiver)) {
if (!hadKey) {
trigger(target, OperationTypes.ADD, key)
} else if (hasChanged(value, oldValue)) {
trigger(target, OperationTypes.SET, key)
}
}
return result
}
setter中的trigger对应了2.xDep.notify
,并且因为再setter里能够获取到目标对象,所以也自然能知道到底是添加还是修改了。
可以看到,Proxy对于对象劫持要灵活且有用得多,最主要的是相对于OD,Proxy额外生成的Getter和Setter更少,更节约内存(当然,嵌套的Object还得递归监听这点没变)。这也就是为什么Vue3.0会使用Proxy
替代Object.defineProperty
的原因了(同时也是我为什么在前文中说“仅限2.0”了)。
网友评论