vm options什么意思_vue2.0源码解读 数据驱动:new Vue() 发生了什么?

本文深入解析Vue2.0的源码,探讨数据驱动的实现,特别是new Vue()时发生的关键步骤。内容包括选项合并、实例属性初始化、事件处理、数据响应式化及生命周期钩子。通过分析initData和proxy方法,揭示如何通过数据代理实现this.message访问data中的属性。
摘要由CSDN通过智能技术生成
关注“ 前端学苑 ” ,坚持每天进步一点点

b6f4479d2503151187c3c130c5f31bf5.png

「~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上,也就是数据驱动视图,这个渲染过程就是我们要着重分析的。

详细如下:91de10d1b779d2819e6a288935e243b5.png

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

然后接下来的会执行 实例上的代理数据 判断,如下图所示:

673a5725259939ed3c035e35da3c8288.png

会判断从 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。

7e83187fe1ac37c2ca0a23ff58e52365.png 划重点,写总结 :

通过源码分析 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。

69553af0078bf5d009d04d8f27608cd7.png

常见问题 (源码分析)

* 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。

69553af0078bf5d009d04d8f27608cd7.png

参与资料:

Vue.js 技术揭秘: 

https://ustbhuangyi.github.io/vue-analysis/

Vue 源码地址 :   

https://github.com/vuejs/vue

觉得本文对你有帮助?请分享给更多人

关注「前端学苑」加星标,提升前端技能

2add0a9f6a3ca32da373e7b2f9546a2a.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值