一.访问器属性
1.1 属性类型
1.1.1 数据属性
数据属性包含一个数据值的位置,在这个位置可以读取和写入值。数据属性有 4 个描述其行为的特性:
- [[Configurable]]:默认值为 true。表示能否通过 delete 删除属性,能否修改属性的特性,能否把属性修改为访问器属性。
- [[Enumberable]]:默认值为 true。表示能否通过 for-in 循环返回属性。
- [[Writable]]:默认为t rue。表示能否修改属性的值。
- [[Value]]:默认为 undefined。包含这个属性的数据值。
1.1.2 访问器属性
访问器属性不包含数据值,包含一对儿 gettter 和 setter 函数,这两个函数都不是必需的。函数名很直白,在读取访问器属性的时候会调用 getter 函数,在设置访问器属性时会调用 setter 函数写入新值。访问器也有4个特性:
- [Configurable]]:默认值为 true。表示能否通过 delete 删除属性,能否修改属性的特性,能否把属性修改为数据属性。
- [[Enumberable]]:默认值为 true。表示能否通过 for-in 循环返回属性。
- [[Get]]:默认为 undefined。读取属性时调用的函数。
- [[Set]]:默认为 undefined。写入属性时调用的函数。
1.2 两个属性方法
通常,我们可能会这样定义一个对象:
var film = {
name: '瘋狂的外星人',
type: ['comedy']
};
复制代码
film 对象包含两个(数据)属性 name & type,数据属性可以这样直接定义,那如何定义一个访问器属性?需要借助 Object 对象的静态方法:defineProperty。
1.2.1 Object.defineProperty
Object.defineProperty(obj,prop,descriptor)
复制代码
可以直接在对象上定义新属性,或者修改对象上已经存在的属性。方法接收三个参数,属性所在的对象,属性的名字,一个描述符对象。 上面展示了用该方法定义访问器属性,我们也可以这样用:
1.2.2 Object.getOwnPropertyDescriptor
Object.getOwnPropertyDescriptor(obj,prop)
复制代码
可以获取对象自身属性的描述,需要注意的是属性是对象自身的属性,不包含原型链上的属性。
二.Vue中的响应式实现
Vue 一大特点就是非侵入性的响应式系统。关于 vue 的响应式原理解析已经有很多优先的文章了,在这儿只简要的描述下流程。 Vue 内部有三个主要的类:Observe,Dep,Watcher。
2.1 Observe
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value)
}
}
/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
复制代码
Observe 实例化是在 initData 方法中(Vue 实例化时会调用 initState 方法,initData 在 initState 方法中调用)。构造函数传入的 value 就是 Vue 组件 data 返回的数据对象,实例属性 dep 是 Dep 类的一个实例,后面再说这个类的作用。然后会对 value 做判断,对于数组会调用 observeArray 方法,对纯对象调用 walk 方法。 observeArray 会遍历数组再次调用 observe 方法,observe 方法又会初始化一 个Observe类。walk 方法是遍历对象的 key 调用 defineReactive 方法。
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
复制代码
defineReactive 方法首先会初始化一个 Dep 类的实例 dep,然后会去检查传入的对象属性,如果存在子对象,就会再次调用 observe 方法,保证对象 obj 所有子属性都会调用 defineReactive 方法变成访问器属性。然后用 Object.defineProperty 设置 getter 和 setter。触发 getter 会调用 dep 的原型方法 depend 方法,作用就是收集依赖。触发 setter 会调用 dep 的原型方法 notify,通知订阅者做更新。
总体就是 Observe 在实例化过程中,将所有数据属性转换为访问器属性,每个属性都持有一个 Dep 类的实例 dep,触发 getter 做依赖收集,记录到 dep 的实例属性 subs 中。触发 setter 通知 dep.subs 记录的订阅者做更新。
2.2 Dep
let uid = 0
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
Dep.target = null
const targetStack = []
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
复制代码
Dep 类有两个实例属性:id,subs。数组 subs 保存的都是 Watcher 的实例。Dep 还有个静态属性 target,保存时也是 Watcher 的实例,这保证了同一时间只会有一个 Watcher 实例在做计算。targetStack 维护了一个 Watcher 栈,保证 Watcher 的有序进行。
2.3 Watcher
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
lazy: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
deps: Array<Dep>;
newDeps: Array<Dep>;
depIds: SimpleSet;
newDepIds: SimpleSet;
before: ?Function;
getter: Function;
value: any;
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
this.before = options.before
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
this.value = this.lazy
? undefined
: this.get()
}
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
run () {
if (this.active) {
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
...
}
复制代码
Watcher 实例属性中有 4 个含 dep 的属性,用来维护 Dep 实例。在构造函数最后会调用 get 方法,在 get 方法中,首先调用 pushTarget,将当前的 Watcher 推入 targetStack 栈中,并赋值给 Dep.Target。然后调用 getter,getter 就是实例化的时候传入的第二个参数。需要找一下 Watcher 在什么做的初始化。
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== 'production') {
/* istanbul ignore if */
if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
vm.$options.el || el) {
warn(
'You are using the runtime-only build of Vue where the template ' +
'compiler is not available. Either pre-compile the templates into ' +
'render functions, or use the compiler-included build.',
vm
)
} else {
warn(
'Failed to mount component: template or render function not defined.',
vm
)
}
}
}
callHook(vm, 'beforeMount')
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
const name = vm._name
const id = vm._uid
const startTag = `vue-perf-start:${id}`
const endTag = `vue-perf-end:${id}`
mark(startTag)
const vnode = vm._render()
mark(endTag)
measure(`vue ${name} render`, startTag, endTag)
mark(startTag)
vm._update(vnode, hydrating)
mark(endTag)
measure(`vue ${name} patch`, startTag, endTag)
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
复制代码
根据函数名,可以猜测是在组件挂载的时候调用的。getter 就是上图的 updateComponent,在这个方法中会先后调用 Vue 组件实例的 render 和 update 方法。render 作用就是生成 vnode,在这个过程中会去访问组件的 data 属性,从而触发组件实例的访问器属性,也就是在 Observe 实例化过程中调用 defineReactive 设置的 getter 和 setter。
到此三个主要的类:Observe,Dep,Watcher基本算是成功会师了。
2.4 流程梳理
梳理下三个类流程,我们写的 Vue 组件在实例化的时候,会做一些初始化,顺序如下图:
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
复制代码
在 initState 方法中,会初始化 props,method,data.....,初始化 data 是通过调用 initData 方法
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(key)) {
proxy(vm, `_data`, key)
}
}
// observe data
observe(data, true /* asRootData */)
}
复制代码
proxy 方法通过 Object.defineProperty 做了属性代理,结果就是通过 vm.xx 可以对 vm._data.xx 进行读写操作。
然后调用 observe 方法,在该方法中会实例化一个 Observe 类,Observe 将 vm.xx 由数据属性转变为访问器属性,设置 getter 和 setter,每个属性的 getter 都会实例化一个 Dep 类,getter 收集依赖,setter 通知更新。然后在 Vue 组件实例化最后调用 vm.$mount 方法,mount 过程中会实例化一个 Watcher,在实例化过程中会调用 Vue 组件的 render 方法生成 vnode,这一过程就会触发访问器属性的 getter,将当前的 Watcher 实例推入 targetStack 栈中,赋值给 Dep.target,并保存在对应属性 getter 中维护的 dep.subs 中。
当数据变动后,触发 setter,调用对应属性维护的 dep 的 notify 方法,通知 dep.subs 中保存的 Watcher 实例更新。
三.Object.defineProperty 存在的问题
在 vue2.x 中响应式实现的过程中,有两处调用了 Object.defineProperty,第一处属性代理,第二处转换为访问器属性,设置 getter/setter。 所以面临的第一个问题,转化为响应式的属性必须要求该属性在初始化的时候就存在。借用官网的例子:
var vm = new Vue({
data:{
a:1
}
})
// `vm.a` 是响应的
vm.b = 2
// `vm.b` 是非响应的
复制代码
当然我们可以通过调用使用 Vue.set 方法将响应属性添加到嵌套的对象上。
第二个问题在 defineReactive 方法对属性执行 getter/setter 转化时,遍历+递归,利用闭包存储数据值的副本,数据量很大的时候意味着设置很多个 getter/setter,性能方面多少有些影响。总结原因就是因为 getter/setter 设置的是属性级别,不能对对象级别做代理。
四. Proxy
var p = new Proxy(target, handler);
复制代码
target 为拦截的对象,handler 为拦截行为,Proxy 支持的拦截一共13种:
- get(target, propKey, receiver):拦截对象属性的读取,比如proxy.foo和proxy['foo']。
- set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = v或proxy['foo'] = v,返回一个布尔值。
- has(target, propKey):拦截propKey in proxy的操作,返回一个布尔值。
- deleteProperty(target, propKey):拦截delete proxy[propKey]的操作,返回一个布尔值。
- ownKeys(target):拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
- getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
- defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。
- preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。
- getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象。
- isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。
- setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
- apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。
- construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)。
详细的可以查看阮一峰老师的博客
Proxy 很好的解决了 defineProperty 的局限性,它是对对象实现的拦截。意味着一个很复杂的 data 对象结构,我们不需要遍历+递归设置 getter/setter,后需添加的属性也可以是响应式的。只需要针对 data 对象做一个代理。在效率和内存上都是不小的改进。
<html>
<body>
<div id="app">
<input type="text" id="a" disabled />
<input type="text" id="b" disabled />
<input type="text" id="c" disabled />
</div>
<script>
var data = {};
var proxy = new Proxy(data, {
set: function(target, propKey, value) {
if (document.getElementById(propKey)) {
document.getElementById(propKey).value = value;
}
},
deleteProperty: function(target, propKey) {
if (document.getElementById(propKey)) {
document.getElementById(propKey).value = '';
}
}
});
</script>
</body>
</html>
复制代码
一个很简陋的例子,在控制台修改 proxy 的值,就会更新页面,可以动态添加删除。如果是用 Object.defineProperty,那么首先 data 需要有 a,b,c 三个属性,定义三个变量存储值,然后调用三次 Object.defineProperty 方法设置三次 getter/setter,而且无法对 delete 操作做出反应。对比下来,简直就是单车变摩托。Proxy 让代码更具有抽象性。
五. 结束语
改用 Proxy,意味着在兼容性方面 Vue 做出了大跨步的尝试,可以跟过时的浏览器拜拜了(对,说的就是 IE)。
另外,Proxy 能给 Vue 带来多大的提升,水平有限,算是抛砖引玉了,期待大佬指教。(第一次在掘金发文章,写的一般,瑟瑟发抖。 0.0)