Home>Article>Web Front-end> Let’s talk about why Vue2 can access properties in various options through this
This article will help you interpret thevuesource code and introduce why you can use this to access properties in various options in Vue2. I hope it will be helpful to everyone!
If you are not interested in how to read the source code below, you don’t need to read it. You can use this to directly navigate to [Source Code Analysis]
There are many articles about source code reading on the Internet. Everyone has their own way, but the articles on the Internet are all refined and tell you which file , what does that function and that variable do; [Related recommendations:vuejs video tutorial,web front-end development]
But I didn’t tell you how to find these, How are these understood, how are these verified, how are these memorized, and how are these applied.
I am not a great master, and I gradually found my own way in the process of exploration. I will share my way here, hoping to help everyone.
Everything is difficult at the beginning. Finding the starting point is the hardest. For front-end projects, we want to find the entry file, usually fromStart searching with the
mainfield in package.json
; the
field inpackage.json
represents this package. Entry file, usually we can find the starting point we want to read through the value of this field.
But forVue
, this field isdist/vue.runtime.common.js
. This file is a compiled file and we cannot understand it. , so we need to find the entry file of the source code;
At this time we need to look at thescripts
field inpackage.json
:
{ "scripts": { "dev": "rollup -w -c scripts/config.js --environment TARGET:full-dev", "dev:cjs": "rollup -w -c scripts/config.js --environment TARGET:runtime-cjs-dev", "dev:esm": "rollup -w -c scripts/config.js --environment TARGET:runtime-esm", "dev:ssr": "rollup -w -c scripts/config.js --environment TARGET:server-renderer", "dev:compiler": "rollup -w -c scripts/config.js --environment TARGET:compiler ", "build": "node scripts/build.js", "build:ssr": "npm run build -- runtime-cjs,server-renderer", "build:types": "rimraf temp && tsc --declaration --emitDeclarationOnly --outDir temp && api-extractor run && api-extractor run -c packages/compiler-sfc/api-extractor.json", "test": "npm run ts-check && npm run test:types && npm run test:unit && npm run test:e2e && npm run test:ssr && npm run test:sfc", "test:unit": "vitest run test/unit", "test:ssr": "npm run build:ssr && vitest run server-renderer", "test:sfc": "vitest run compiler-sfc", "test:e2e": "npm run build -- full-prod,server-renderer-basic && vitest run test/e2e", "test:transition": "karma start test/transition/karma.conf.js", "test:types": "npm run build:types && tsc -p ./types/tsconfig.json", "format": "prettier --write --parser typescript "(src|test|packages|types)/**/*.ts"", "ts-check": "tsc -p tsconfig.json --noEmit", "ts-check:test": "tsc -p test/tsconfig.json --noEmit", "bench:ssr": "npm run build:ssr && node benchmarks/ssr/renderToString.js && node benchmarks/ssr/renderToStream.js", "release": "node scripts/release.js", "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s" } }
Okay I saw that there are a lot ofscripts
in thepackage.json
ofVue
. I believe everyone can understand these. Here we only focus ondev
andbuild
are two scripts;
dev
script is used for development,build
script is used for packaging, we can We see that there is aTARGET
environment variable in thedev
script. The value of this environment variable isfull-dev
. We can set it inscripts/config. Find this value in js
;
Searchfull-dev
directly inscripts/config.js
:
In this way, you can find the configuration corresponding to this value:
var config = { 'full-dev': { entry: resolve('web/entry-runtime-with-compiler.ts'), dest: resolve('dist/vue.js'), format: 'umd', env: 'development', alias: { he: './entity-decoder' }, banner } }
entry
field is the entry file we are looking for, and this file is the source code ofVue
Entry file, the following value isweb/entry-runtime-with-compiler.ts
, we can find this file in theweb
directory;
but there is no Find theweb
directory in the root directory. At this time, we boldly guess whether there is an alias configuration. At this time, I also happened to see analias.js under
scripts.
file, open this file, and find that there is an alias ofweb
;
module.exports = { vue: resolve('src/platforms/web/entry-runtime-with-compiler'), compiler: resolve('src/compiler'), core: resolve('src/core'), web: resolve('src/platforms/web'), weex: resolve('src/platforms/weex'), shared: resolve('src/shared') }For verification Our guess is that we can search
aliasin
config.jsand find that this file is indeed introduced:
const aliases = require('./alias') const resolve = p => { const base = p.split('/')[0] if (aliases[base]) { return path.resolve(aliases[base], p.slice(base.length + 1)) } else { return path.resolve(__dirname, '../', p) } }Search again
aliases, and found that there is indeed a configuration alias:
// 省略部分代码 const config = { plugins: [ alias({ entries: Object.assign({}, aliases, opts.alias) }), ].concat(opts.plugins || []), }so that we can confirm that
webis the directory
src/platforms/web, and we can find it# in this directory ##entry-runtime-with-compiler.ts
This file;
In this way, we successfully found the source code entry file of
Vue, then we can start reading the source code;
Almost all the source codes we look at now use
esmmodularity orcommonjs
Modular, these will have aexport
ormodule.exports
, we can use this to see what is exported;only see the exported content, others Ignore it for the time being and directly find the final exported content, such as the source code of
: 这个时候就去找 这个时候就去找 这个时候就去找 这个时候就去找 这样我们就找到 阅读源码的目的一定要清晰,当然你可以说目的就是了解 等等... 例如我们的这次阅读计划就是了解 上面顺序不分先后,但是答案一定是在源码中。 上面已经找到了 首先看一下 有这么多东西,我们不用管,要清晰目的,我们在使用 也就是 我们要知道 很明显构造函数只做了一件事,就是调用了 那么我们就去找 盲猜一波,见名知意: 我们就去找这些混入的方法,一个一个的找,找到 代码这么多没必要全都看,记住我们的目的是找到 先简化代码,不看没有意义的代码: 传递过来的 在 继续往下看,我们有目的的看代码,只需要看有 记住我们的目的,只需要关心 可以看到只剩下了 虽然没有得到我们想要的,但是从这里我们也得到了一个重要信息, 现在我们分析要多一步了,参数只有 操作了 盲猜一波: 这里面最有可能是我们想要的是 已经找到我们想要的了,现在开始正式分析 根据代码结构可以看到, 我们可以用 看到这里也明白了,为什么在 代码很多,我们依然不用关心其他的代码,只关心 这里真正有关的就两个地方: 这里的 这里通过 不是很好理解,那我们来自己就用这些代码实现一下: 上面的代码只是为了方便理解,所以会忽略一些细节,比如 跟着之前的思路,我们忽略无关代码,简化后的代码如下: 这里的 这里的 简单的实现一下: 简化之后的代码如下: 这里的实现方式和 注意: 简化之后的代码如下: 这里的实现主要是通过 仔细看下来,其实实现方式还是和 不过里面的 上面我们已经分析了 上面已经简单了实现了 注意:上面的代码对比于文章中写的示例有改动,主要是为了实现最后打印结果正确,增加了赋值操作。 通过上面的分析,让我们对构造函数的 The above is the detailed content of Let’s talk about why Vue2 can access properties in various options through this. For more information, please follow other related articles on the PHP Chinese website!entry-runtime-with-compiler.ts
的导出内容:import Vue from './runtime-with-compiler' export default Vue
runtime-with-compiler.ts
的导出内容:
runtime-with-compiler.ts
的导出内容:import Vue from './runtime/index' export default Vue as GlobalAPI
runtime/index.ts
的导出内容:
runtime/index.ts
的导出内容:import Vue from 'core/index' export default Vue
core/index.ts
的导出内容:
core/index.ts
的导出内容:import Vue from './instance/index' export default Vue
instance/index.ts
的导出内容:
instance/index.ts
的导出内容:function Vue(options) { if (__DEV__ && !(this instanceof Vue)) { warn('Vue is a constructor and should be called with the `new` keyword') } this._init(options) } export default Vue as unknown as GlobalAPI
Vue
的构造函数了,这个时候我们就可以开始阅读源码了;带有目的的阅读源码
Vue
的实现原理,但是这个目的太宽泛了,我们可以把目的细化一下,例如:
Vue
的生命周期是怎么实现的Vue
的数据响应式是怎么实现的Vue
的模板编译是怎么实现的Vue
的组件化是怎么实现的Vue
的插槽是怎么实现的Vue
的this
为什么可以访问到选项中的各种属性,这里再细分为:
Vue
的this
是怎么访问到data
的Vue
的this
是怎么访问到methods
的Vue
的this
是怎么访问到computed
的Vue
的this
是怎么访问到props
的源码分析
Vue
的入口文件,接下来我们就可以开始阅读源码了,这里我就以Vue
的this
为什么可以访问到选项中的各种属性为例,来分析Vue
的源码;instance/index.ts
的源码: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' import type { GlobalAPI } from 'types/global-api' function Vue(options) { if (__DEV__ && !(this instanceof Vue)) { warn('Vue is a constructor and should be called with the `new` keyword') } this._init(options) } //@ts-expect-error Vue has function type initMixin(Vue) //@ts-expect-error Vue has function type stateMixin(Vue) //@ts-expect-error Vue has function type eventsMixin(Vue) //@ts-expect-error Vue has function type lifecycleMixin(Vue) //@ts-expect-error Vue has function type renderMixin(Vue) export default Vue as unknown as GlobalAPI
Vue
的时候,通常是下面这样的:const vm = new Vue({ data() { return { msg: 'hello world' } }, methods: { say() { console.log(this.msg) } } }); vm.say();
Vue
的构造函数接收一个选项对象,这个选项对象中有data
和methods
;Vue
的this
为什么可以访问到data
和methods
,那么我们就要找到Vue
的构造函数中是怎么把data
和methods
挂载到this
上的;this._init(options)
:this._init(options)
_init
方法,这个方法在哪我们不知道,但是继续分析源码,我们可以看到下面会执行很多xxxMixin
的函数,并且Vue
作为参数传入://@ts-expect-error Vue has function type initMixin(Vue) //@ts-expect-error Vue has function type stateMixin(Vue) //@ts-expect-error Vue has function type eventsMixin(Vue) //@ts-expect-error Vue has function type lifecycleMixin(Vue) //@ts-expect-error Vue has function type renderMixin(Vue)
initMixin
:初始化混入stateMixin
:状态混入eventsMixin
:事件混入lifecycleMixin
:生命周期混入renderMixin
:渲染混入initMixin
,直接就找了_init
方法:export function initMixin(Vue: typeof Component) { Vue.prototype._init = function (options?: Record
data
和methods
是怎么挂载到this
上的;export function initMixin(Vue) { Vue.prototype._init = function (options) { const vm = this } }
Vue
并没有做太多事情,只是把_init
方法挂载到了Vue.prototype
上;_init
方法中,vm
被赋值为this
,这里的this
就是Vue
的实例,也就是我们的vm
;vm
和options
组合出现的代码,于是就看到了:if (options && options._isComponent) { initInternalComponent(vm, options) } else { vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) }
_isComponent
前面带有_
,说明是私有属性,我们通过new Vue
创建的实例时走到现在是没有这个属性的,所以走到else
分支;resolveConstructorOptions(vm.constructor)
中没有传递options
,所以不看这个方法,直接看mergeOptions
:export function mergeOptions(parent, child, vm) { if (__DEV__) { checkComponents(child) } if (isFunction(child)) { // @ts-expect-error child = child.options } normalizeProps(child, vm) normalizeInject(child, vm) normalizeDirectives(child) // Apply extends and mixins on the child options, // but only if it is a raw options object that isn't // the result of another mergeOptions call. // Only merged options has the _base property. if (!child._base) { if (child.extends) { parent = mergeOptions(parent, child.extends, vm) } if (child.mixins) { for (let i = 0, l = child.mixins.length; i < l; i++) { parent = mergeOptions(parent, child.mixins[i], vm) } } } const options = {} let key for (key in parent) { mergeField(key) } for (key in child) { if (!hasOwn(parent, key)) { mergeField(key) } } function mergeField(key) { const strat = strats[key] || defaultStrat options[key] = strat(parent[key], child[key], vm, key) } return options }
vm
和options
组合出现的代码,child
就是options
,vm
就是vm
,简化之后:export function mergeOptions(parent, child, vm) { normalizeProps(child, vm) normalizeInject(child, vm) normalizeDirectives(child) return options }
normalizeProps
、normalizeInject
、normalizeDirectives
这三个方法,值得我们关注,但是见名知意,这三个方法可能并不是我们想要的,跟进去看一眼也确实不是;mergeOptions
最后会返回一个options
对象,这个对象就是我们的options
,最后被vm.$options
接收;vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm )
vm
的函数也是需要引起我们的注意的,继续往下看:if (__DEV__) { initProxy(vm) } else { vm._renderProxy = vm }
vm
,但是内部没有操作$options
,跳过,继续往下看:initLifecycle(vm) initEvents(vm) initRender(vm) callHook(vm, 'beforeCreate', undefined, false /* setContext */) initInjections(vm) // resolve injections before data/props initState(vm) initProvide(vm) // resolve provide after data/props callHook(vm, 'created')
initLifecycle
、initEvents
、initRender
、initInjections
、initState
、initProvide
这些方法都是操作vm
的;
initLifecycle
:初始化生命周期initEvents
:初始化事件initRender
:初始化渲染initInjections
:初始化注入initState
:初始化状态initProvide
:初始化依赖注入callHook
:调用钩子initState
,跟进去看一下:export function initState(vm) { const opts = vm.$options if (opts.props) initProps(vm, opts.props) // Composition API initSetup(vm) if (opts.methods) initMethods(vm, opts.methods) if (opts.data) { initData(vm) } else { const ob = observe((vm._data = {})) ob && ob.vmCount++ } if (opts.computed) initComputed(vm, opts.computed) if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch) } }
initState
。initState
initState
主要做了以下几件事:
props
setup
methods
data
computed
watch
this
来访问的属性是props
、methods
、data
、computed
;props
中定义了一个属性,在data
、methods
、computed
中就不能再定义了,因为props
是最先初始化的,后面的也是同理。initProps
initProps
的作用是初始化props
,跟进去看一下:function initProps(vm, propsOptions) { const propsData = vm.$options.propsData || {} const props = (vm._props = shallowReactive({})) // cache prop keys so that future props updates can iterate using Array // instead of dynamic object key enumeration. const keys = (vm.$options._propKeys = []) const isRoot = !vm.$parent // root instance props should be converted if (!isRoot) { toggleObserving(false) } for (const key in propsOptions) { keys.push(key) const value = validateProp(key, propsOptions, propsData, vm) /* istanbul ignore else */ if (__DEV__) { const hyphenatedKey = hyphenate(key) if ( isReservedAttribute(hyphenatedKey) || config.isReservedAttr(hyphenatedKey) ) { warn( `"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`, vm ) } defineReactive(props, key, value, () => { if (!isRoot && !isUpdatingChildComponent) { warn( `Avoid mutating a prop directly since the value will be ` + `overwritten whenever the parent component re-renders. ` + `Instead, use a data or computed property based on the prop's ` + `value. Prop being mutated: "${key}"`, vm ) } }) } else { defineReactive(props, key, value) } // static props are already proxied on the component's prototype // during Vue.extend(). We only need to proxy props defined at // instantiation here. if (!(key in vm)) { proxy(vm, `_props`, key) } } toggleObserving(true) }
props
是怎么挂载到vm
上的,根据我上面的方法,简化后的代码如下:function initProps(vm, propsOptions) { vm._props = shallowReactive({}) for (const key in propsOptions) { const value = validateProp(key, propsOptions, propsData, vm) if (!(key in vm)) { proxy(vm, `_props`, key) } } }
validateProp
:看名字就知道是验证props
,跳过proxy
:代理,很可疑,跟进去看一下:export function proxy(target, sourceKey, key) { sharedPropertyDefinition.get = function proxyGetter() { return this[sourceKey][key] } sharedPropertyDefinition.set = function proxySetter(val) { this[sourceKey][key] = val } Object.defineProperty(target, key, sharedPropertyDefinition) }
target
就是vm
,sourceKey
就是_props
,key
就是props
的属性名;Object.defineProperty
把vm
的属性代理到_props
上,这样就可以通过this
访问到props
了。var options = { props: { name: { type: String, default: 'default name' } } } function Vue(options) { const vm = this initProps(vm, options.props) } function initProps(vm, propsOptions) { vm._props = {} for (const key in propsOptions) { proxy(vm, `_props`, key) } } function proxy(target, sourceKey, key) { Object.defineProperty(target, key, { get() { return this[sourceKey][key] }, set(val) { this[sourceKey][key] = val } }) } const vm = new Vue(options) console.log(vm.name); console.log(vm._props.name); vm.name = 'name' console.log(vm.name); console.log(vm._props.name);
props
的验证等等,真实挂载在_props
上的props
是通过defineReactive
实现的,我这里直接是空的,这些超出了本文的范围。initMethods
initMethods
的代码如下:function initMethods(vm, methods) { const props = vm.$options.props for (const key in methods) { if (__DEV__) { if (typeof methods[key] !== 'function') { warn( `Method "${key}" has type "${typeof methods[ key ]}" in the component definition. ` + `Did you reference the function correctly?`, vm ) } if (props && hasOwn(props, key)) { warn(`Method "${key}" has already been defined as a prop.`, vm) } if (key in vm && isReserved(key)) { warn( `Method "${key}" conflicts with an existing Vue instance method. ` + `Avoid defining component methods that start with _ or $.` ) } } vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm) } }
function initMethods(vm, methods) { for (const key in methods) { vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm) } }
noop
和bind
在之前的文章中有出现过,可以去看一下:【源码共读】Vue2源码 shared 模块中的36个实用工具函数分析vm[key]
就是methods
的方法,这样就可以通过this
访问到methods
中定义的方法了。bind
的作用是把methods
中定义的函数的this
指向vm
,这样就可以在methods
中使用this
就是vm
了。var options = { methods: { say() { console.log('say'); } } } function Vue(options) { const vm = this initMethods(vm, options.methods) } function initMethods(vm, methods) { for (const key in methods) { vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm) } } function noop() {} function polyfillBind(fn, ctx) { function boundFn(a) { const l = arguments.length return l ? l > 1 ? fn.apply(ctx, arguments) : fn.call(ctx, a) : fn.call(ctx) } boundFn._length = fn.length return boundFn } function nativeBind(fn, ctx) { return fn.bind(ctx) } const bind = Function.prototype.bind ? nativeBind : polyfillBind const vm = new Vue(options) vm.say()
initData
initData
的代码如下:function initData(vm) { let data = vm.$options.data data = vm._data = isFunction(data) ? getData(data, vm) : data || {} if (!isPlainObject(data)) { data = {} __DEV__ && warn( 'data functions should return an object:\n' + 'https://v2.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 (__DEV__) { if (methods && hasOwn(methods, key)) { warn(`Method "${key}" has already been defined as a data property.`, vm) } } if (props && hasOwn(props, key)) { __DEV__ && 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 const ob = observe(data) ob && ob.vmCount++ }
function initData(vm) { let data = vm.$options.data // proxy data on instance const keys = Object.keys(data) let i = keys.length while (i--) { const key = keys[i] proxy(vm, `_data`, key) } }
initProps
是一样的,都是通过proxy
把data
中的属性代理到vm
上。
initData
的获取值的地方是其他的不相同,这里只做提醒,不做详细分析。initComputed
initComputed
的代码如下:function initComputed(vm, computed) { // $flow-disable-line const watchers = (vm._computedWatchers = Object.create(null)) // computed properties are just getters during SSR const isSSR = isServerRendering() for (const key in computed) { const userDef = computed[key] const getter = isFunction(userDef) ? userDef : userDef.get if (__DEV__ && getter == null) { warn(`Getter is missing for computed property "${key}".`, vm) } if (!isSSR) { // create internal watcher for the computed property. watchers[key] = new Watcher( vm, getter || noop, noop, computedWatcherOptions ) } // component-defined computed properties are already defined on the // component prototype. We only need to define computed properties defined // at instantiation here. if (!(key in vm)) { defineComputed(vm, key, userDef) } else if (__DEV__) { if (key in vm.$data) { warn(`The computed property "${key}" is already defined in data.`, vm) } else if (vm.$options.props && key in vm.$options.props) { warn(`The computed property "${key}" is already defined as a prop.`, vm) } else if (vm.$options.methods && key in vm.$options.methods) { warn( `The computed property "${key}" is already defined as a method.`, vm ) } } } }
function initComputed(vm, computed) { for (const key in computed) { const userDef = computed[key] const getter = userDef defineComputed(vm, key, userDef) } }
defineComputed
来定义computed
属性,进去瞅瞅:export function defineComputed(target, key, userDef) { const shouldCache = !isServerRendering() if (isFunction(userDef)) { sharedPropertyDefinition.get = shouldCache ? createComputedGetter(key) : createGetterInvoker(userDef) sharedPropertyDefinition.set = noop } else { sharedPropertyDefinition.get = userDef.get ? shouldCache && userDef.cache !== false ? createComputedGetter(key) : createGetterInvoker(userDef.get) : noop sharedPropertyDefinition.set = userDef.set || noop } if (__DEV__ && sharedPropertyDefinition.set === noop) { sharedPropertyDefinition.set = function () { warn( `Computed property "${key}" was assigned to but it has no setter.`, this ) } } Object.defineProperty(target, key, sharedPropertyDefinition) }
initProps
和initData
一样,都是通过Object.defineProperty
来定义属性;getter
和setter
是通过createComputedGetter
和createGetterInvoker
来创建的,这里不做过多分析。动手时间
props
、methods
、data
、computed
的属性为什么可以直接通过this
来访问,那么我们现在就来实现一下这个功能。initProps
、initMethods
,而initData
和initComputed
的实现方式和initProps
的方式一样,所以我们直接复用就好了:function Vue(options) { this._init(options) } Vue.prototype._init = function (options) { const vm = this vm.$options = options initState(vm) } function initState(vm) { const opts = vm.$options if (opts.props) initProps(vm, opts.props) if (opts.methods) initMethods(vm, opts.methods) if (opts.data) initData(vm) if (opts.computed) initComputed(vm, opts.computed) } function initProps(vm, propsOptions) { vm._props = {} for (const key in propsOptions) { vm._props[key] = propsOptions[key].default proxy(vm, `_props`, key) } } function proxy(target, sourceKey, key) { Object.defineProperty(target, key, { get() { return this[sourceKey][key] }, set(val) { this[sourceKey][key] = val } }) } function initMethods(vm, methods) { for (const key in methods) { vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm) } } function noop() {} function polyfillBind(fn, ctx) { function boundFn(a) { const l = arguments.length return l ? l > 1 ? fn.apply(ctx, arguments) : fn.call(ctx, a) : fn.call(ctx) } boundFn._length = fn.length return boundFn } function nativeBind(fn, ctx) { return fn.bind(ctx) } const bind = Function.prototype.bind ? nativeBind : polyfillBind function initData(vm) { vm._data = {} for (const key in vm.$options.data) { vm._data[key] = vm.$options.data[key] proxy(vm, `_data`, key) } } function initComputed(vm, computed) { for (const key in computed) { const userDef = computed[key] const getter = userDef defineComputed(vm, key, bind(userDef, vm)) } } function defineComputed(target, key, userDef) { Object.defineProperty(target, key, { get() { return userDef() }, }) } const vm = new Vue({ props: { a: { type: String, default: 'default' } }, data: { b: 1 }, methods: { c() { console.log(this.b) } }, computed: { d() { return this.b + 1 } } }) console.log('props a: default',vm.a) console.log('data b: 1', vm.b) vm.c() // 1 console.log('computed d: 2', vm.d)
总结
this
有了更深的理解,同时对于this
指向的问题也有了更深的理解。