持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第 8 天,点击查看活动详情
start
Vue.js 对我们的 data 到底做了哪些处理?
现在开始探索。
我先列一下初始化 data 经过了那几个步骤。
-
new Vue() -
this._init() -
this.initState(vm) -
this.initData -
observe()
简述一下初始化 data 的逻辑
1. new Vue()
new Vue({
el: '#app',
data() {
return { a: { name: '你好' } }
},
})
上面是我们使用的场景,new Vue() 相当于执行 Vue 这个函数。
src\core\instance\index.js
function Vue(options) {
if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue)) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
执行 Vue 这个函数,会执行 this._init(options) ,并且传入对应的参数options。 _init 来自于 Vue 的原型。
src\core\instance\init.js
// initMixin 会在 `/src/core/instance/index.js`中执行 (传入的是 Vue构造函数)
export function initMixin(Vue: Class<Component>) {
// Vue 原型上添加 _init方法
// 如果是 _开头,则可以理解为是提供给内部使用的内部属性。如果是 $开头是提供给用户使用的外部属性。
Vue.prototype._init = function (options?: Object) {
// 1. 存储当前的this,到变量 vm 上
const vm: Component = this
// a uid
// 2. 实例的一个唯一标识
vm._uid = uid++
// 性能检测
let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
// a flag to avoid this being observed
// 避免被观察到的标志 (Vue实例,不被转换为响应式)
vm._isVue = true
// merge options
// 3. 主要操作就是合并配置options 到 vm.$options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// 4.开始初始化 例如 生命周期,事件,Render state....
// expose real self
vm._self = vm
// initLifecycle函数,向实例中挂载属性。
initLifecycle(vm)
// initEvents 主要做了: 1.定义属性_events; 2.初始化了父组件注册了的子组件
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
// initInjections 主要做了:初始化inject, 本质上是,匹配 子组件到上层组件的的_provided 和 inject是否有同名属性。
initInjections(vm) // resolve injections before data/props
// initState 主要做了: 依次初始化: props methods data computed watch
initState(vm)
// initProvide 主要做了:初始化 provide
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)
}
}
}
结合上述的代码和相关的注释。可以了解到
_init主要做的操作:
- 合并传入的配置 option;
- 初始化属性。
在初始化属性中,先看和 data 有关的
initState(vm);,这里的 vm 存储的是 Vue 实例。
src\core\instance\state.js
// 初始化状态
export function initState(vm: Component) {
// 在实例上定义了一个_watchers, 这里的 _watchers 后续会存储这个组件的所有 watcher实例
vm._watchers = []
const opts = vm.$options
// 根据实例的配置 ($options)依次初始化 props methods data computed watch
// 这里的顺序很重要,也就解释了为什么 watch中为什么可以监听computed
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
// 这里如果默认没有传入 data. 返回一个响应式的空对象
observe((vm._data = {}), true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
// nativeWatch是因为 火狐浏览器的object.prototype上有一个属性watch
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
由上方相关代码可以得知,initState 实现的功能是按顺序依次对 props methods data computed watch 做初始化。
- 这里可以解释为什么
watch中可以使用computed
仔细看看 data 相关的方法
// 如果传入的配置存在 data =》 initData.
if (opts.data) {
initData(vm)
} else {
// 这里如果默认没有传入 data. 返回一个响应式的空对象
observe((vm._data = {}), true /* asRootData */)
}
对是否传入 data 做一个逻辑判断,如果没有传入 data 配置,默认赋值一个空对象给,实例的_data;
如果传入了 data 配置, 调用 initData
// 初始化data
function initData(vm: Component) {
// 拿到data
let data = vm.$options.data
// 判断传入的data是不是函数 是函数就 getData 处理一下,不是函数拿来直接使用,注意一下 他这里把传入的 data 也在 vm._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]
// 是否和 methods props重 复
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)) {
// 和 props 类似,这里也做了代理 , 我们 this.xxxData 其实访问的还是 this._data.xxxData
proxy(vm, `_data`, key)
}
}
// observe data
// 监听 data
observe(data, true /* asRootData */)
}
// 如果传入的 data 是函数,执行该函数,执行后的返回值。
export function getData(data: Function, vm: Component): any {
// #7573 disable dep collection when invoking data getters
// 在调用数据getter时禁用dep收集
pushTarget()
try {
return data.call(vm, vm)
} catch (e) {
handleError(e, vm, `data()`)
return {}
} finally {
popTarget()
}
}
// 代理 `vm.xxxProps 或者 this.xxxProps` 实际上访问的是=> `vm._props.xxxProps`
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)
}
/**
* Check if a string starts with $ or _
* 检查字符串是否以$或_开头
*/
export function isReserved(str: string): boolean {
// 取第一项,然后判断是否是 $ or _
// charCodeAt() 方法可返回指定位置的字符的 Unicode 编码。
const c = (str + '').charCodeAt(0)
// 0x24 =>36 =>$ 0x5f =>95 =>_
return c === 0x24 || c === 0x5f
}
initData主要操作:
-
处理我们传入的 data,如果是函数,执行改函数,返回对应过得返回值;
-
处理好的数据赋值给
vm._data;这也就解释了为什么,实例上会有一个
_data属性,而且this.a===this._data.a -
遍历 data,看属性名是否和 method props 重复;
-
检查 data 中的属性名是否是
$或_开头; -
使用 proxy 函数代理我们的数据,(作用,方便我们直接通过 this 使用,
this.xxxData访问的本质还是this._data.xxxData); -
最后最重要的逻辑,使用 observe 处理我们的 data
observe(data, true /* asRootData */);
end
本文主要讲述了, 处理 data 的前置逻辑。
- 处理 data。(是函数就执行该函数,返回对应的返回值)
- 校验了 data 是不是对象;
- 校验了是否和 methods, props 是否重名;
- 校验了是否是
$或_开头; - 使用 proxy 函数代理
_data的数据; - 核心的响应式逻辑,都在 observe
思考
_data中的属性为什么和 data 中的同名属性完全相等?
因为两者本来就是一样的,直接通过 this 使用,
this.xxxData访问的本质还是this._data.xxxData。
- 为什么 Vue 组件中定义 data 推荐使用函数返回值的形式?
// 重点的几行代码
let data = vm.$options.data
data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {}
// 如果 data 不是函数,直接赋值 `vm.$options.data`
// 如果这个组件被多次使用,此时多次使用的组件的 data 存储的引用地址,指向的是同一个。(会导致组件的data之间相互影响。)
- 本章节提到的
proxy方法
- 注意需要和 ES6 的
Proxy区分。 - 主要作用是:代理
vm.xxxProps 或者 this.xxxProps实际上访问的是=>vm._props.xxxProps - 除了 data, props 也使用
proxy方法做了代理。 - 这就解释了实例上的
_data_props属性的作用。
end
- 本节内容主要是梳理了,从
new Vue(), 到真正处理 data 的initData方法 - 处理
data最终都会通过observe方法。