引言
对于vue如何实现双向数据绑定的原理,现在大部分同学都能快速说出是利用Object.defineProperty
劫持对象的访问器,在属性值发生变化时我们可以获取变化,然后根据变化进行后续响应,vue3.0利用的是Proxy。好像似是而非懂了,但问到是如何劫持,如何响应的又懵懵懂懂说不出所以然。希望通过我的分享能帮助大家更加深刻的理解vue的实现原理,更好的在工作中运用好vue。本次分享的内容主要是vue的核心部分的代码,我觉得掌握一个框架主要是要了解到他的核心实现,不用纠结于每一处的细节,把时间用再更有意义的事上。
Object.defineProperty
本次讲解的vue源码还是vue2.0版本,所以双向数据绑定的核心还是Object.defineProperty这个方法。简单介绍下我们用到的属性
enumerable
,属性是否可枚举,默认 false。configurable
,属性是否可以被修改或者删除,默认 false。get
,获取属性的方法。set
,设置属性的方法
详细介绍请看Object.defineProperty文档。主要是利用了get和set,当属性被访问时会执行get方法,在get中我们会加入收集依赖的逻辑,收集到与该属性相关联的依赖,当该属性改变时会执行set方法,在set中我们会通知之前收集的依赖,进行页面上数据更新。
开始
我们是如何使用vue的呢,先引入然后对vue实例化,就像下面这样
import Vue from 'vue';
new Vue({
....
})
那vue实例化的时候到底做了什么呢,我们一步步来分析,其核心源码在vue源码中的src/core/instance/index.js文件中,我们一探究竟。
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'
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)
}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue
initMixin
看命名像是混入一些功能初始化,我们再看看initMixin代码
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
...
// merge 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
)
}
...
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')
...
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
_init方法在Vue实例化的时候会调用,我们主要关注下merge options部分,这部分主要的功能是把业务逻辑以及组件的一些特性全都放到了vm.$options中了,后续的操作我们都可以从vm.$options拿到可用的信息。后续是一些功能的初始化,我们主要关注initState方法,在该方法有对data数据的初始化处理。我们接着看initState代码
export function initState (vm: Component) {
...
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
...
}
再继续看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 */)
}
首先对data进行一些取值判断。然后遍历循环data中的key,先判断key是否与方法名,属性名,保留字同名,如果没有执行proxy方法。我们访问属性是通过this._data.xxx,proxy方法会帮我们设置一层代理,也就是重新进行一次数据访问拦截。我们直接this.xxx就可以了
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)
}
最后执行observe方法,到这里终于要进入vue响应式原理的核心实现了,对于响应式的实现,我会再下一节进行详细的分享。跳转下一篇