共计 11895 个字符,预计需要花费 30 分钟才能阅读完成。
前言
自己学习 Vue 也有不少时间了,特别是喜欢研究一些原理,一直以来都是看别人的博客学习,这次想分享学习的心得,而且这也是我第一次发博客, 写的不好请见谅,文章随缘更新(最近有时间),感谢观看
注: 一定要搭配源码看,版本为:2.6.10
在 new Vue 之前的初始化
我们先来到 `node_modules\vue\src\core\instance\index.js`, 这里是 Vue 的入口
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' &&
!(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(Vue)
, stateMixin(Vue)
, eventsMixin(Vue)
, lifecycleMixin(Vue)
, renderMixin(Vue)
-
initMixin(Vue)
文件位置:
node_modules\vue\src\core\instance\init.js
export function initMixin (Vue: Class<Component>) {Vue.prototype._init = function (options?: Object) {// ... 具体逻辑后续讲解} }
很明显,在
Vue.prototype
上初始化了一个_init
方法,它是在我们 new Vue 后进行了调用,至于具体实现后面详解。 -
stateMixin(Vue)
文件位置:
node_modules\vue\src\core\instance\state.js
export function stateMixin (Vue: Class<Component>) { // flow somehow has problems with directly declared definition object // when using Object.defineProperty, so we have to procedurally build up // the object here. const dataDef = {} // 定义一个 data 空对象,后面就会知道,这是对 $data 属性的特性设置 dataDef.get = function () { return this._data} // 定义 data 的 getter 方法 const propsDef = {} propsDef.get = function () { return this._props}// 定义 props 的 getter 方法 if (process.env.NODE_ENV !== 'production') {dataDef.set = function () { warn( 'Avoid replacing instance root $data.' + 'Use nested data properties instead.', this ) } propsDef.set = function () {warn(`$props is readonly.`, this) } } Object.defineProperty(Vue.prototype, '$data', dataDef) Object.defineProperty(Vue.prototype, '$props', propsDef) Vue.prototype.$set = set Vue.prototype.$delete = del Vue.prototype.$watch = function ( expOrFn: string | Function, cb: any, options?: Object ): Function { const vm: Component = this if (isPlainObject(cb)) {return createWatcher(vm, expOrFn, cb, options) } options = options || {} options.user = true const watcher = new Watcher(vm, expOrFn, cb, options) if (options.immediate) { try {cb.call(vm, watcher.value) } catch (error) {handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`) } } return function unwatchFn () {watcher.teardown() } } }
接下来我们一句句分析这个函数做了什么
首先创建了两个
dataDef
,propsDef
对象,并且设置了get
属性访问器,返回了_data
,_props
,至于具体作用在下面会有说明const dataDef = {} dataDef.get = function () { return this._data} const propsDef = {} propsDef.get = function () { return this._props}
接下来是这段代码,如果不是生产环境,也就是开发环境,我们是不能直接赋值给
$data
和$props
属性值的if (process.env.NODE_ENV !== 'production') {dataDef.set = function () { warn( 'Avoid replacing instance root $data.' + 'Use nested data properties instead.', this ) } propsDef.set = function () {warn(`$props is readonly.`, this) } } // this.$data = ...,this.$props 这样都是会报警告的
在原型上添加属性访问器,也就是我们可以在组件内直接使用
this.$data
访问// 在原型中添加 $data 属性,并设置访问器,返回的是_data Object.defineProperty(Vue.prototype, '$data', dataDef) // 在原型中添加 $props 属性,并设置访问器,返回的是_props Object.defineProperty(Vue.prototype, '$props', propsDef)
我们来看下这个
$data
到底是什么<template> <div class="about"> <h1>{{appName}}</h1> <h1>{{username}}</h1> </div> </template> <script> export default {data() { return {count: 1} }, mounted() { // 打印了 this console.log(this.$data); } }; </script>
打印结果很明显,就是 vue 实例的
data
方法返回的对象,并且包含__ob__属性,表示它被观测了,这些我们以后会在数据响应会详细讲解{__ob__: Observer} count: 1 __ob__: Observer {value: {…}, dep: Dep, vmCount: 1} get count: ƒ reactiveGetter() set count: ƒ reactiveSetter(newVal) __proto__:
接下来我们继续进行, 它在 Vue 原型上添加了两个方法,set 和 del, 那么他们到底是用来做什么的呢,我们直接定位到这两个方法的实现, 并且下面两个代码实现我将以注释的形式讲解,请跳转到响应的源代码
// 原型上添加 $set 和 $delete 方法 Vue.prototype.$set = set Vue.prototype.$delete = del
// src/core/observer/index.js // @param {*} target 目标对象 // @param {*} key 要添加的键 // @param {*} val 要添加的值 export function set (target: Array<any> | Object, key: any, val: any): any { // 如果目标对象是 undefined 或者 null 或者是原始类型就警告 if (process.env.NODE_ENV !== 'production' && (isUndef(target) || isPrimitive(target)) ) {warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`) } // 如果目标是数组,并且 key 是有效的索引,也就是说必须是整数 // 整个 if 所干的事情就是,删除原来的值,并替换新的值,然后直接返回新的值 if (Array.isArray(target) && isValidArrayIndex(key)) { // 就找到最大的那个长度 target.length = Math.max(target.length, key) // 如果 key 对应的下标有值,就删掉数组下标对应的值,然后添加新的值,否则不会删除,添加新的值 target.splice(key, 1, val) // 注意这里的 splice 是变异后的方法,会触发响应 return val // 直接返回属性的值 } // 如果 key 是对象的 key,并且不是系统原生的属性,比如说一些 toString, 之类的存在于 Object.prototype 原型的属性 if (key in target && !(key in Object.prototype)) { // 直接改变目标对象的属性值,并返回,注意改变值会触发响应,如果页面有使用到这个属性,就会自动改变页面的这个值 target[key] = val return val } // 获取__ob__对象 const ob = (target: any).__ob__ // 这句话的意思就是,避免给 Vue 实例添加响应式的属性或者给他的 $data if (target._isVue || (ob && ob.vmCount)) { process.env.NODE_ENV !== 'production' && warn( 'Avoid adding reactive properties to a Vue instance or its root $data' + 'at runtime - declare it upfront in the data option.' ) return val } // 如果 key 属性不在目标对象上,也就是说本来就不存在于目标对象上,也就意味着,不是响应式的 // 当我们赋值后,会自动添加响应式,并触发页面更新 if (!ob) {target[key] = val return val } // ob.value 设置为响应式 defineReactive(ob.value, key, val) // 通知页面更新 ob.dep.notify() return val }
总结下
Vue.prototype.$set
所做的事情,其实就是向响应式对象中添加一个属性,并确保这个新属性同样是响应式的,且触发视图更新。它必须用于向响应式对象上添加新属性,因为 Vue 无法探测普通的新增属性(官方文档原话)。接下来我们再来看看
$del
实现export function del (target: Array<any> | Object, key: any) { if (process.env.NODE_ENV !== 'production' && (isUndef(target) || isPrimitive(target)) ) {warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`) } if (Array.isArray(target) && isValidArrayIndex(key)) {target.splice(key, 1) return } const ob = (target: any).__ob__ if (target._isVue || (ob && ob.vmCount)) { process.env.NODE_ENV !== 'production' && warn( 'Avoid deleting properties on a Vue instance or its root $data' + '- just set it to null.' ) return } if (!hasOwn(target, key)) {return} delete target[key] if (!ob) {return} ob.dep.notify()}
我想不用多说,根据上面的下面不难看出,就是删除响应式数据中的一个属性,并且更新视图。
官方文档介绍:删除对象的属性。如果对象是响应式的,确保删除能触发更新视图。这个方法主要用于避开 Vue 不能检测到属性被删除的限制,但是你应该很少会使用它。
然后我们回到初始化时候的代码
Vue.prototype.$set = set Vue.prototype.$delete = del // 添加 $watch 方法 Vue.prototype.$watch = function ( expOrFn: string | Function, cb: any, options?: Object ): Function {// ... 后续}
以上代码又是在原型上添加了一个
$watch
方法,简单说一下,其实就是监听一个数据的变化,在以后的数据响应会详细讲解回到最开始我们看到的四个初始化方法, 我们继续分析
initMixin(Vue) stateMixin(Vue) eventsMixin(Vue) lifecycleMixin(Vue) renderMixin(Vue)
-
eventsMixin(Vue)
文件位置:
node_modules\vue\src\core\instance\events.js
export function eventsMixin (Vue: Class<Component>) {Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component { // ... return vm } Vue.prototype.$once = function (event: string, fn: Function): Component { // ... return vm } Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component { // ... return vm } Vue.prototype.$emit = function (event: string): Component { // ... return vm } }
分别在
Vue.prototype
,初始化了四个方法$on
,$once
,$off
,$emit
,这些代码的实现我们暂且不看,以后都会有讲解,不过我们还是讲一下具体作用- vm.$on: 监听当前实例上的自定义事件。事件可以由
vm.$emit
触发。回调函数会接收所有传入事件触发函数的额外参数。 - vm.$emit: 监听一个自定义事件,但是只触发一次,在第一次触发之后移除监听器。
- vm.$off: 移除自定义事件监听器。
- vm.$once: 监听一个自定义事件,但是只触发一次,在第一次触发之后移除监听器。
以上方法,有个常见的使用场景:eventBus,我们可以再次
new Vue()
一个实例,专门用来父子组件直接的传值
- vm.$on: 监听当前实例上的自定义事件。事件可以由
lifecycleMixin(Vue)
文件位置: node_modules\vue\src\core\instance\lifecycle.js
export function lifecycleMixin (Vue: Class<Component>) {Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {// ..}
Vue.prototype.$forceUpdate = function () {
const vm: Component = this
if (vm._watcher) {vm._watcher.update()
}
}
Vue.prototype.$destroy = function () {// ..}
}
在 Vue.prototype
上初始化 _update
, $forceUpdate
, $destroy
三个方法
- _update:用于渲染 dom 或者更新 dom 到视图
- $forceUpdate:迫使 Vue 实例重新渲染。注意它仅仅影响实例本身和插入插槽内容的子组件,而不是所有子组件。
- $destroy:完全销毁一个实例。清理它与其它实例的连接,解绑它的全部指令及事件监听器。
renderMixin(Vue)
文件位置:node_modules\vue\src\core\instance\render.js
export function renderMixin (Vue: Class<Component>) {
// install runtime convenience helpers
installRenderHelpers(Vue.prototype)
Vue.prototype.$nextTick = function (fn: Function) {return nextTick(fn, this)
}
Vue.prototype.$nextTick = function (): VNode {
// ...
return vnode
}
}
在 Vue.prototype
上初始化$nextTick
- $nextTick:将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,然后等待 DOM 更新。它跟全局方法
Vue.nextTick
一样,不同的是回调的this
自动绑定到调用它的实例上。
initGlobalAPI
我们定位到 src\core\global-api\index.js
文件,Vue 除了初始化上面的一些实例方法,也添加了下面这些静态方法
export function initGlobalAPI (Vue: GlobalAPI) {
// config
const configDef = {}
configDef.get = () => config
if (process.env.NODE_ENV !== 'production') {configDef.set = () => {
warn('Do not replace the Vue.config object, set individual fields instead.')
}
}
Object.defineProperty(Vue, 'config', configDef)
// exposed util methods.
// NOTE: these are not considered part of the public API - avoid relying on
// them unless you are aware of the risk.
Vue.util = {
warn,
extend,
mergeOptions,
defineReactive
}
Vue.set = set
Vue.delete = del
Vue.nextTick = nextTick
// 2.6 explicit observable API
Vue.observable = <T>(obj: T): T => {observe(obj)
return obj
}
Vue.options = Object.create(null)
ASSET_TYPES.forEach(type => {Vue.options[type + 's'] = Object.create(null)
})
// this is used to identify the "base" constructor to extend all plain-object
// components with in Weex's multi-instance scenarios.
Vue.options._base = Vue
// 这里把 keep-alive 组件注册到全局
extend(Vue.options.components, builtInComponents)
initUse(Vue)
initMixin(Vue)
initExtend(Vue)
initAssetRegisters(Vue)
}
我们来看下具体做了哪些事情
const configDef = {}
configDef.get = () => config
if (process.env.NODE_ENV !== 'production') {
// 无法更改 config,否则警告
configDef.set = () => {
warn('Do not replace the Vue.config object, set individual fields instead.')
}
}
// 添加配置对象
Object.defineProperty(Vue, 'config', configDef)
// 添加了 `util` 对象,里面有一些方法,`warn:用于警告 `,`extend: 合并两个对象 `,`mergeOptions: 合并选项 `,`defineReactive:设置响应式数据 `
Vue.util = {
warn,
extend,
mergeOptions,
defineReactive
}
Vue.set = set
Vue.delete = del
Vue.nextTick = nextTick
// 这三个不必多说和之前的初始化一样
Vue.observable = <T>(obj: T): T => {observe(obj)
return obj
}
// 深度观察对象,并设置为响应式
// ASSET_TYPES
// export const ASSET_TYPES = [
// 'component',
// 'directive',
// 'filter'
// ]
Vue.options = Object.create(null)
ASSET_TYPES.forEach(type => {Vue.options[type + 's'] = Object.create(null)
})
// 在 Vue 的 options 属性上添加一些属性,初始为空对象,包括我们常见的 components,directives,filter
// 是不是很眼熟
Vue.options._base = Vue
// Vue.options 添加了_base 属性指向 Vue,此时的 options 如下
/*
Vue.options = {components:{},
directives:{},
filter:{},
_base: Vue
}
*/
extend(Vue.options.components, builtInComponents)
builtInComponents
这个又是什么鬼?我们来看看是什么
// src/core/components/index.js
import KeepAlive from './keep-alive'
export default {KeepAlive}
原来是把 KeepAlive
组件添加到了 components
中
接下来又是各种初始化, 注意千万别把之前的初始化混淆
initUse(Vue) 添加了 use 方法, 一般用来注册插件
initMixin(Vue) 添加了 mixin 方法,混入全局的钩子函数
initExtend(Vue) 使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象
initAssetRegisters(Vue) 注册全局组件,指令或过滤器
- initUse:位置
src/core/global-api/use.js
,添加了 use 方法, 一般用来注册插件 - initMixin:位置
src/core/global-api/mixin.js
, 添加了 mixin 方法,混入全局的钩子函数 -
initExtend:位置
src/core/global-api/extend.js
, 使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象// 样例 var Profile = Vue.extend({template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>', data: function () { return { firstName: 'Walter', lastName: 'White', alias: 'Heisenberg' } } })
-
initAssetRegisters:位置
src/core/global-api/assets.js
, 注册全局组件,指令或过滤器Vue.component('my-component', { /* ... */}) Vue.directive('my-directive', {bind: function () {}, inserted: function () {}, update: function () {}, componentUpdated: function () {}, unbind: function () {} }) Vue.filter('my-filter', function (value) {// 返回处理后的值})
经过了 initGlobalAPI(Vue)
最终返回了 Vue, 其他代码暂且不看,主要用来处理服务端渲染
import Vue from './instance/index'
import {initGlobalAPI} from './global-api/index'
import {isServerRendering} from 'core/util/env'
import {FunctionalRenderContext} from 'core/vdom/create-functional-component'
initGlobalAPI(Vue)
Object.defineProperty(Vue.prototype, '$isServer', {get: isServerRendering})
Object.defineProperty(Vue.prototype, '$ssrContext', {get () {
/* istanbul ignore next */
return this.$vnode && this.$vnode.ssrContext
}
})
Object.defineProperty(Vue, 'FunctionalRenderContext', {value: FunctionalRenderContext})
Vue.version = '__VERSION__'
export default Vue
总结
经过了initMixin(Vue)
,stateMixin(Vue)
,eventsMixin(Vue)
,lifecycleMixin(Vue)
,renderMixin(Vue)
,Vue.prototype 有哪些方法呢
Vue.prototype = {
_init: f, // new Vue 初始化时调用
$data: get => _data
$props: get => _props,
$set: f,
$delete: f,
$watch: f, // 监听 data 数据变化的方法
$on: f, // 事件方法
$once: f, // 事件方法
$emit: f, // 手动触发事件方法
_update: f, // 初始化或者更新,把 VNode 渲染为真正的 DOM 节点
$forceUpdate: f, //
$destroy: f
}
经过 initGlobalAPI
添加了许多静态方法和对象
1.Vue.util = {
warn,
extend,
mergeOptions,
defineReactive
}
2.Vue.set = set
3.Vue.delete = del
4.Vue.nextTick = nextTick
5.Vue.observable = <T>(obj: T): T => {observe(obj)
return obj
}
6.Vue.options = Object.create(null)
7.Vue.options[type + 's'] = Object.create(null)
8. 创建全局的 keep-alive 组件
9.initUse(Vue) 添加了 use 方法, 一般用来注册插件
10.initMixin(Vue) 添加了 mixin 方法,混入全局的钩子函数
11.initExtend(Vue) 使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象
12.initAssetRegisters(Vue) 注册全局组件,指令或过滤器
下一篇:new Vue()
后发生了什么