-
Notifications
You must be signed in to change notification settings - Fork 0
Description
有如下情形
<template>
<div>
<el-form :model="form">
<el-form-item label="选择供应商" required>
<el-select
v-model="form.supplierId"
filterable
remote
reserve-keyword
placeholder="请输入关键词"
:remote-method="remoteMethod"
:loading="loading"
>
<el-option
v-for="item in supplier"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
data() {
return {
form: {},
supplier: [],
loading: false
}
},
created() {
this.getSupplier()
},
methods: {
getSupplier() {
const self = this
self.$app.rpc(1, 'Supply/suppliers', [{}, { start: 0, size: 99 }], (result, error) => {
if (!error) {
if (result) {
self.supplier = result.list || []
self.form.supplierId = 1
}
}
})
}
}
}
</script>这段代码也很简单,使用了 element-ui 中的 select 组件,在初始化的时候,通过接口获取到了该组件的所有选项,并且还为该组件设置了一个初始值,这样这个组件在一开始的时候就选择了一个 id 为 1 的选项,这些都是很好理解的
但是这个代码在之后的执行就有些问题,问题是在重新切换该组件的选择项的时候, self.form.supplierId 的值确实是修改了,但是,该组件的 label 的展现却没有发生变化,并且不管切换多少个,都是不变的。
有经验的我们其实很快就能判断的出,其实就是 form 的修改,修改了内部的属性,在 Vue2 基于 Object.defineProperty(obj, key, descriptor) 来定义响应式对象的前提下,内部属性的修改,不会触发其 setter,因为其内部属性不是响应式的。这个问题很常见,解决方法大家也都清楚。
但是如果换一种写法,这种问题将不会出现
<template>
<div>
<el-form :model="form">
<el-form-item label="选择供应商" required>
<el-select
v-model="form.supplierId"
filterable
remote
reserve-keyword
placeholder="请输入关键词"
:remote-method="remoteMethod"
:loading="loading"
>
<el-option
v-for="item in supplier"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
data() {
return {
form: {
supplierId: ''
},
supplier: [],
loading: false
}
},
created() {
this.getSupplier()
},
methods: {
getSupplier() {
const self = this
self.$app.rpc(1, 'Supply/suppliers', [{}, { start: 0, size: 99 }], (result, error) => {
if (!error) {
if (result) {
self.supplier = result.list || []
self.form.supplierId = 1
}
}
})
}
}
}
</script>这段代码与上门那段代码唯一的不同就在于,在 data 刚开始的时候,就给 form 赋予了一个初始值,即 { supplierId: '' } 这也是本次我们要探究的问题,为什么这么写就可以了。
这时候我们就要去看 Vue2 的源码了,先从 data 处理的部分看起。
不过首先,我们先要对 Object.defineProperty 有个简单的认知
let val = 'msg'
const reactiveObj = {}
Object.defineProperty(reactiveObj, msg, {
get: function () {
// 当访问reactiveObj.msg时被调用
return val
},
set: function (newVal) {
// 当设置reactiveObj.msg时被调用
val = newVal
}
})在Vue的响应式对象中,它会在getter中收集依赖、在setter中派发更新,我们会在之后的章节中分别对getter的收集依赖,setter的派发更新做单独的讲解。
在介绍完Object.defineProperty,我们来回答一个问题,什么是响应式对象?在Vue.js中对于什么是响应式对象,我们可以简单的理解成:用Object.defineProperty()方法定义时同时提供了get和set选项,我们就可以将其称之为响应式对象。
在Vue.js实例化时,会把props、data和computed等变成响应式对象,在介绍响应式对象时,我们会重点介绍props和data的处理过程,这个过程发生在this._init()方法中的initState(vm)中。
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}这里我们来看一下 initData 是如何处理 data 相关的逻辑的:
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 */)
}这个逻辑很清楚,首先获取到 data 的值,然后做相关的校验,其中包括 data 应是一个普通对象,还有就是其中的 keys 不与 props 和 methods 里面的重名。校验都完成之后,就开始做最重要的两个逻辑,一个是 data 代理,一个是 data 的响应式。
proxy()方法是定义在src/core/instance/state.js文件中:
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
export function proxy (target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}代码分析:
noop:它代表空函数,空函数代表什么都不做。target:它是目标代理对象,在Vue.js中就是Vue实例。sourceKey:它是源属性,在props代理中传递的是_props内部私有属性。key:它是要代理的属性,在props中就是我们撰写的各种props属性。sharedPropertyDefinition:它就是Object.defineProperty(obj, key, descriptor)方法的descriptor参数,可以从上面代码中看到,在props代理中它提供了enumerable、configurable、get和set这几个选项。
假设我们有如下Vue实例:
export default {
data() {
return {
a: '123',
b: {}
}
}
}在proxy代理后,我们就能通过this.a和this.b代替this._data.a和this._data.b的形式直接访问或者设置值
最后就是重中之重,observe()方法以及Observer类。observe()方法定义与defineReactive()方法定义在同一个文件中,其代码如下:
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}代码分析:
- 首先对传递的
value进行了类型判断,不为对象或者是VNode实例时不进行任何操作,其中VNode是一个类,它会在生成虚拟DOM的时候使用到,我们会在后面进行介绍,isObject是一个定义在src/shared/utils.js文件中的工具方法。
export function isObject (obj: mixed): boolean {
return obj !== null && typeof obj === 'object'
}- 然后对
value使用hasOwn判断是否有__ob__属性且__ob__为Observer实例,添加这个属性是为了防止重复观察(避免重复定义响应式),既:如果已经是响应式对象了,直接返回,否则才会进行下一步操作。hasOwn是一个定义在src/shared/utils.js文件中的工具方法:
const hasOwnProperty = Object.prototype.hasOwnProperty
export function hasOwn (obj: Object | Array<*>, key: string): boolean {
return hasOwnProperty.call(obj, key)
}- 最后
value又进行了一些条件判断,其中最重要的两个条件为Array.isArray和isPlainObject,它们分别判断value是否为数组,是否为普通对象,其它几个边界条件暂时不做介绍。其中isPlainObject是一个定义在src/shared/utils.js文件中的工具方法:
export function isPlainObject (obj: any): boolean {
return _toString.call(obj) === '[object Object]'
}接下来,我们需要看一下Observer类的实现:
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])
}
}
}代码分析:
def为定义在src/core/utils/lang.js文件中的一个工具方法,def本质上也是对Object.defineProperty()方法的一层包裹封装,使用def定义__ob__的目的是让__ob__在对象属性遍历的时候不可被枚举出来。
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}- 在
Vue.js中对于纯对象和数组的响应式的处理方式是不同的,代码首先判断了value是否为数组。如果不是数组,则调用walk()方法。walk()方法实际上就是递归遍历对象属性,然后调用defineReactive()的过程,例如:
const nestedObj = {
a: {
b: {
c: 'c'
}
}
}
// 递归调用
defineReactive(nestedObj)
defineReactive(a)
defineReactive(b)
defineReactive(c)在这里其实我们就已经明白了,因为他会递归调用遍历里面的所有对象属性,然后将其都变成响应式的,所以,第二种写法就能使其发生变化。
如果是数组,则调用observeArray()方法,observeArray也是一个遍历递归调用的过程,只不过这里遍历的是数组,而不是对象的属性键。然后我们还发现,在observeArray()方法调用之前,还进行了hasProto判断,然后根据判断结果进行不同的操作。其中,hasProto是定义在src/core/util/env.js文件中的一个常量,它的目的就是为了判断当前浏览器是否支持__proto__属性:
export const hasProto = '__proto__' in {}我们都知道因为原生API某些限制因素,Vue.js对数组七种可以改变自身数组的方法提供了变异方法支持,这七种方法分别为:
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]对这七种方法的变异处理逻辑在src/core/ovserver/array.js文件中:
import { def } from '../util/index'
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* 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
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()
return result
})
})代码分析:
- 首先以
Array.prototype原型创建一个新的变量,这个变量会在protoAugment或者copyAugment方法的时候使用到。 - 然后遍历七种方法,使用
def来重新定义一个包裹方法。也就是说:当我们调用这七种任意一种方法的时候,首先调用我们的包裹方法,在包裹方法里面再调用原生对应的数组方法,这样做的目的是让我们可以在这个包裹方法中做我们自己的事情,例如notify,这个过程可以使用以下伪代码实例描述:
// Array.prototype.push方法为例
function mutatorFunc (value) {
const result = Array.prototype.push(value)
// do something
return result
}
export default {
data () {
return {
arr: []
}
},
created () {
this.arr.push('123')
// 相当于
mutatorFunc(123)
}
}然后我们接下来看一下protoAugment和copyAugment的实现,首先是最简单的protoAugment:
// 定义
const arr = []
export const arrayMethods = Object.create(arrayProto)
function protoAugment (target, src: Object) {
target.__proto__ = src
}
// 调用
protoAugment(arr, arrayMethods)
// 调用后
arr.__proto__ = {
// 省略其它
push: function () {},
pop: function () {},
shift: function () {},
unshift: function () {},
splice: function () {},
sort: function () {},
reverse: function () {}
}
arr.push()
arr.pop()
arr.shift()
arr.unshift()
arr.splice()
arr.sort()
arr.reverse()代码分析:当浏览器支持__proto__属性的时候,直接把__proto__指向我们创建的arrayMethods变量,这个包含我们在上面定义的七种变异方法。
当浏览器不支持__proto__属性的时候,我们就调用copyAugment方法:
// 定义
const arr = []
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
export const arrayMethods = Object.create(arrayProto)
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])
}
}
// 调用
copyAugment(arr, arrayMethods, arrayKeys)
// 调用后
arr = {
// 省略其它
push: function () {},
pop: function () {},
shift: function () {},
unshift: function () {},
splice: function () {},
sort: function () {},
reverse: function () {}
}
arr.push()
arr.pop()
arr.shift()
arr.unshift()
arr.splice()
arr.sort()
arr.reverse()代码分析:我们可以从代码中看到,当浏览器不支持__proto__的时候,会把我们创建的arrayMethods变量上所有的key,遍历赋值到value数组上。