「~vue2.0源码解读 ~」
Vue 一个核心思想是数据驱动,相信大家一定对这个不陌生。所谓数据驱动,就是视图由数据驱动生成。相比传统的使用jQuery等前端库直接操作DOM,大大提高了开发效率,代码简洁易维护。
本篇文章主要是 Vue源码探秘 之 “ 数据驱动 ” 。
主题内容:new Vue() 发生了什么?这同样也是Vue必考面试题之一。
正文从这里开始~~~
先来看一个简单的例子:
<div id="app">
{{ message }}
div>
<script>var app = new Vue({el: '#app',
data() {return {message: 'hello Vue!'
}
}
})script>
最终页面展示效果是:
页面上显示hello Vue!,也就是说Vue将js里的数据渲染到DOM上,也就是数据驱动视图,这个渲染过程就是我们要着重分析的。
详细如下:new Vue() 发生了什么?
1、new Vue() 发生了什么?
1)结论:new Vue()是创建Vue实例,它内部执行了根实例的初始化过程。
2)具体包括以下操作:
选项合并
$children,$refs,$slots,$createElement等实例属性的方法初始化
自定义事件处理
数据响应式处理
生命周期钩子调用 (beforecreate created)
可能的挂载
3)总结:new Vue()创建了根实例并准备好数据和方法,未来执行挂载时,此过程还会递归的应用于它的子组件上,最终形成一个有紧密关系的组件实例树。
详细分析:
当我们new Vue时,会执行以下Vue函数,里边_init()方法,是通过initMixin函数定义的,继续往下看:
1、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'
// 初始化Vue
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(thisinstanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
// 在import Vue from ‘vue’ 时,会执行以下函数
initMixin(Vue) // 函数执行时,定义了Vue原型上会定义_init 方法
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue
我们看到Vue 是通过new关键字进行实例化的,然后再这个函数里面调用了this._init方法。如果你对初始化还不是很清楚,建议参考上文对 初始化过程 的梳理性文章。
2、构造函数的核心是调用了_init方法
代码地址:src/core/instance/init.js
Vue.prototype._init = function(options?: Object) {
/***** ① *****/
const vm: Component = this; // 当前 Vue 实例
vm._uid = uid++; // 当前 Vue 实例唯一标识
let startTag, endTag;
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`;
endTag = `vue-perf-end:${vm._uid}`;
mark(startTag);
}
vm._isVue = true; // 一个标志,避免该对象被响应系统观测
/***** ② *****/
/***** 对 Vue 提供的 props、data、methods等选项进行合并处理 *****/
// _isComponent 内部选项:在 Vue 创建组件的时候才会生成
if (options && options._isComponent) {
// 优化内部组件实例化,因为动态选项合并非常慢,而且没有一个内部组件选项需要特殊处理。 initInternalComponent(vm, options);
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor), // parentVal
options || {}, // childVal
vm
);
}
// 设置渲染函数的作用域代理,其目的是提供更好的提示信息
if (process.env.NODE_ENV !== 'production') {
initProxy(vm);
} else {
vm._renderProxy = vm;
}
vm._self = vm; // 暴露真实的实例本身
/***** ③ *****/
/***** 执行相关初始化程序及调用初期生命周期函数 ****/
initLifecycle(vm); // 初始化生命周期
initEvents(vm); // 初始化事件
initRender(vm); // 初始化渲染
callHook(vm, 'beforeCreate'); // 调用生命周期钩子函数 -- beforeCreate
initInjections(vm); // resolve injections before data/props
initState(vm); // 初始化 initProps、initMethods、initData、initComputed、initWatch
initProvide(vm); // resolve provide after data/props
callHook(vm, 'created'); // 此时还没有任何挂载的操作,所以在 created 中是不能访问DOM的,即不能访问 $el
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方法主要作用① 主要是做一些参数的初始化,例如uid啊等。
② 主要是合并option,合并了options配置,这样就可以通过vm.$options.el等访问到创建实例时传入的option。
③ 主要是一堆初始化的函数,初始化生命周期,初始化事件中心,初始化渲染,初始化 data、props、computed、watcher 等等
④ 主要是$mount后 完成渲染,判断vm实例是否存在vm.$options.el,存在的话就将vm挂载到这个dom节点上,存在会执行$mount方法,在执行$mount之前页面并没有改变,完成渲染,经过这一步,页面上就会从:{{message}} 变为 'Hello Vue !'。
那有的人可能疑惑,我看完了init还是不理解,data是如何渲染到页面上的,通过哪个方法渲染的,接下来我们说一下,我们定义了data后,为什么我们可以访问到message呢?
3、分析下initState是如何处理data的:
代码地址:src/core/instance/state.js
exportfunction initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
// 如果定义了 props 那么就初始化 proprs
if (opts.props) initProps(vm, opts.props)
// 如果定义了 methods 就初始化所有的方法
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
// 如果定义了 data 就初始化所有的字段,这是咱们接下来要分析的
initData(vm)
} else {
// 如果没有定义 data 就默认赋值为空对象
observe(vm._data = {}, true/* asRootData */)
}
// 后面同理 计算属性和wather
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
我们可以看到在这个函数中首先它会判断是否有props,有的话执行initProps,同样data和methods一样的逻辑,initState函数是对props、methods、data、computed、watch这几项的处理。
4、我们重点来看下initData这个函数。
在initData方法中,主要做的工作有:
将vm.$options.data映射到vm._data中,使得可以通过vm._data访问数据变量。
处理data与props、methods之间的变量命名冲突的情况。
通过代理方法proxy来实现vm直接访问data中的数据变量。
1)现在只关注initData对data的初始化:
代码地址:src/core/instance/state.js
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
)
} elseif (!isReserved(key)) {
proxy(vm, `_data`, key)
}
}
// observe data
observe(data, true/* asRootData */)
}
从vm.$options.data里面拿到data也就是我们在new Vue里面定义的对象message,拿到后判断它是否是个function,一般我们推荐data是个函数,return出来一个对象,判断的结果会执行getData函数:
export function getData (data: Function, vm: Component): any {
// #7573 disable dep collection when invoking data getters
pushTarget()
try {
return data.call(vm, vm) } catch (e) {
handleError(e, vm, `data()`)
return {}
} finally {
popTarget()
}
}
getData函数返回的data.call(vm, vm) 这个对象,然后就可以得出data = vm._data
然后接下来的会执行 实例上的代理数据 判断,如下图所示:
会判断从 vm.$option的props以及methods判断两者是否相等 ,因为有时候可能在props以及method会有同名,最终这两者都会挂载上vm上。
2)如果变量命名冲突的情况,那么如何挂载到vm上,挂载的实现是依靠proxy实现的:
下面是proxy部分的代码:
代码地址:src/core/instance/state.js
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
} //设置通用数据属性变量。此变量就是对vm._data(也就是vm.$options.data)的代理
exportfunction proxy (target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
} // 对属性变量添加get方法。实际上读取的是vm._data,也就是此变量就是对vm._data
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
proxy方法内部的实现实际上就是定义了一个get和set,这里面最终使用Object.defineProperty就是把传入的target即vm的key代理了下,通过劫持vm实例中属性的getter和setter来实现对data的代理。
看到这里我们理解了,当我们调用this.message时候,实际上我们调用了this._data.message,因为它实际使用了proxy做了代理 proxy(vm, `data`, key) 会将_data作为sourceKey传入,这就是我们可以使用this.message可以访问到this._data.message。
* 划重点,写总结 :
通过源码分析 new Vue() 发生了什么?
1、Vue实例化调用 _init() 方法
2、_init() 方法主要作用
① 主要是做一些参数的初始化,例如uid啊等。② 主要是合并option,合并了options配置,这样就可以通过vm.$options.el等访问到创建实例时传入的option。
③ 主要是一堆初始化的函数,初始化生命周期,初始化事件中心,初始化渲染,初始化 data、props、computed、watcher 等等
④ 主要是$mount后 完成渲染,判断vm实例是否存在vm.$options.el,存在的话就将vm挂载到这个dom节点上,存在会执行$mount方法,在执行$mount之前页面并没有改变,完成渲染,经过这一步,页面上就会从:{{message}} 变为 'Hello Vue !'。
3、data是如何渲染到页面上的,通过哪个方法渲染的。
为什么我们可以访问到message呢?
① initState函数是对props、methods、data、computed、watch这几项的判断处理。
② 在initData方法中,主要做的工作有:
1)将vm.$options.data映射到vm._data中,使得可以通过vm._data访问数据变量。
2)处理data与props、methods之间的变量命名冲突的情况。
3)通过代理方法proxy来实现vm直接访问data中的数据变量。
最后:proxy做了代理 proxy(vm, `data`, key) 会将_data作为sourceKey传入,使得this.message可以访问到this._data.message。
常见问题 (源码分析)
* 1、模版插值 message 是何时被替换成指定的字符串变量的?
答案:真正将 message 插值替换为真正变量的操作都在 vm.$mount 。
* 2、为什么可以通过 `this.xxx` 可以访问对应的字段呢?
在源码中,有一个方法为initData(),方法所在为:initMixin() -> _init() -> initState() ->initData() 在initData方法中,主要做的工作有:
1) 将vm.$options.data映射到vm._data中,使得可以通过vm._data访问数据变量
2) 处理data与props、methods之间的变量命名冲突的情况
3) 通过代理方法proxy来实现vm直接访问data中的数据变量
通过 Object.defineProperty 把data上的属性代理到vm上,vm即this对象。
这就是可以通过this.message访问data属性的原因。
* 3、beforeCreate钩子可以干什么?
beforeCreate():创建前的状态,初始化事件和生命周期。可以加载一些比如 loading加载动画,在页面渲染前出现的内容。
因为在beforeCreate(vue初始化阶段),这个时候data中的变量还没有被挂载到this上,这个时候访问值会是undefined。
参与资料:
Vue.js 技术揭秘:
https://ustbhuangyi.github.io/vue-analysis/
Vue 源码地址 :
https://github.com/vuejs/vue
觉得本文对你有帮助?请分享给更多人
关注「前端学苑」加星标,提升前端技能