本篇文章帶大家解讀vue源碼,來介紹一下vue2中為什么可以使用 this 訪問各種選項(xiàng)中的屬性,希望對大家有所幫助!
下方的如何閱讀源碼不感興趣可以不用看,可以通過這個(gè)直接定位到【源碼分析】
網(wǎng)上有很多關(guān)于源碼閱讀的文章,每個(gè)人都有自己的方式,但是網(wǎng)上的文章都是精煉之后的,告訴你哪個(gè)文件、那個(gè)函數(shù)、那個(gè)變量是干什么的;【相關(guān)推薦:vuejs視頻教程、web前端開發(fā)】
但是沒有告訴你這些是怎么找到的,這些是怎么理解的,這些是怎么驗(yàn)證的,這些是怎么記憶的,這些是怎么應(yīng)用的。
我也不是什么大神,也是在摸索的過程中,逐漸找到了自己的方式,我這里就分享一下我的方式,希望能幫助到大家。
立即學(xué)習(xí)“前端免費(fèi)學(xué)習(xí)筆記(深入)”;
萬事開頭難,找到起點(diǎn)是最難的,對于前端項(xiàng)目,我們想要找到入口文件,一般都是從package.json中的main字段開始找;
package.json中的main字段代表的是這個(gè)包的入口文件,通常我們可以通過這個(gè)字段的值來找到我們要閱讀的起點(diǎn)。
但是對于Vue來說,這個(gè)字段是dist/vue.runtime.common.js,這個(gè)文件是編譯后的文件,我們是看不懂的,所以需要找到源碼的入口文件;
這個(gè)時(shí)候我們就需要看package.json中的scripts字段:
{ "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" } }
可以看到Vue的package.json中有很多的scripts,這些相信大家都可以看得懂,這里我們只關(guān)注dev和build這兩個(gè)腳本;
dev腳本是用來開發(fā)的,build腳本是用來打包的,我們可以看到dev腳本中有一個(gè)TARGET的環(huán)境變量,這個(gè)環(huán)境變量的值是full-dev,我們可以在scripts/config.js中找到這個(gè)值;
直接在scripts/config.js中搜索full-dev:
這樣就可以找到這個(gè)值對應(yīng)的配置:
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字段就是我們要找的入口文件,這個(gè)文件就是Vue的源碼入口文件,后面的值是web/entry-runtime-with-compiler.ts,我們可以在web目錄下找到這個(gè)文件;
但是并沒有在根目錄下找到web目錄,這個(gè)時(shí)候我們就大膽猜測,是不是有別名配置,這個(gè)時(shí)候我也正好在scripts下看到了一個(gè)alias.js文件,打開這個(gè)文件,發(fā)現(xiàn)里面有一個(gè)web的別名;
代碼如下:
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') }
為了驗(yàn)證我們的猜測,我們可以在config.js中搜一下alias,發(fā)現(xiàn)確實(shí)有引入這個(gè)文件:
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) } }
再搜一下aliases,發(fā)現(xiàn)確實(shí)有配置別名:
// 省略部分代碼 const config = { plugins: [ alias({ entries: Object.assign({}, aliases, opts.alias) }), ].concat(opts.plugins || []), }
這樣我們就可以確認(rèn),web就是src/platforms/web這個(gè)目錄,我們可以在這個(gè)目錄下找到entry-runtime-with-compiler.ts這個(gè)文件;
這樣我們就成功的找到了Vue的源碼入口文件,接下來我們就可以開始閱讀源碼了;
上面找到了入口文件,但是還是不知道如何閱讀源碼,這個(gè)時(shí)候我們就需要一些技巧了,這里我就分享一下我自己的閱讀源碼的技巧;
像我們現(xiàn)在看的源碼幾乎都是使用esm模塊化或者commonjs模塊化的,這些都會有一個(gè)export或者module.exports,我們可以通過這個(gè)來看導(dǎo)出了什么;
只看導(dǎo)出的內(nèi)容,其他的暫時(shí)不用管,直接找到最終導(dǎo)出的內(nèi)容,例如Vue的源碼:
entry-runtime-with-compiler.ts的導(dǎo)出內(nèi)容:
import Vue from './runtime-with-compiler' export default Vue
這個(gè)時(shí)候就去找runtime-with-compiler.ts的導(dǎo)出內(nèi)容:
runtime-with-compiler.ts的導(dǎo)出內(nèi)容:
import Vue from './runtime/index' export default Vue as GlobalAPI
這個(gè)時(shí)候就去找runtime/index.ts的導(dǎo)出內(nèi)容:
runtime/index.ts的導(dǎo)出內(nèi)容:
import Vue from 'core/index' export default Vue
這個(gè)時(shí)候就去找core/index.ts的導(dǎo)出內(nèi)容:
core/index.ts的導(dǎo)出內(nèi)容:
import Vue from './instance/index' export default Vue
這個(gè)時(shí)候就去找instance/index.ts的導(dǎo)出內(nèi)容:
instance/index.ts的導(dǎo)出內(nèi)容:
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的構(gòu)造函數(shù)了,這個(gè)時(shí)候我們就可以開始閱讀源碼了;
閱讀源碼的目的一定要清晰,當(dāng)然你可以說目的就是了解Vue的實(shí)現(xiàn)原理,但是這個(gè)目的太寬泛了,我們可以把目的細(xì)化一下,例如:
Vue的生命周期是怎么實(shí)現(xiàn)的
Vue的數(shù)據(jù)響應(yīng)式是怎么實(shí)現(xiàn)的
Vue的模板編譯是怎么實(shí)現(xiàn)的
Vue的組件化是怎么實(shí)現(xiàn)的
Vue的插槽是怎么實(shí)現(xiàn)的
等等...
例如我們的這次閱讀計(jì)劃就是了解Vue的this為什么可以訪問到選項(xiàng)中的各種屬性,這里再細(xì)分為:
Vue的this是怎么訪問到data的
Vue的this是怎么訪問到methods的
Vue的this是怎么訪問到computed的
Vue的this是怎么訪問到props的
上面順序不分先后,但是答案一定是在源碼中。
上面已經(jīng)找到了Vue的入口文件,接下來我們就可以開始閱讀源碼了,這里我就以Vue的this為什么可以訪問到選項(xiàng)中的各種屬性為例,來分析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的時(shí)候,通常是下面這樣的:
const vm = new Vue({ data() { return { msg: 'hello world' } }, methods: { say() { console.log(this.msg) } } }); vm.say();
也就是Vue的構(gòu)造函數(shù)接收一個(gè)選項(xiàng)對象,這個(gè)選項(xiàng)對象中有data和methods;
我們要知道Vue的this為什么可以訪問到data和methods,那么我們就要找到Vue的構(gòu)造函數(shù)中是怎么把data和methods掛載到this上的;
很明顯構(gòu)造函數(shù)只做了一件事,就是調(diào)用了this._init(options):
this._init(options)
那么我們就去找_init方法,這個(gè)方法在哪我們不知道,但是繼續(xù)分析源碼,我們可以看到下面會執(zhí)行很多xxxMixin的函數(shù),并且Vue作為參數(shù)傳入:
//@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:狀態(tài)混入
eventsMixin:事件混入
lifecycleMixin:生命周期混入
renderMixin:渲染混入
我們就去找這些混入的方法,一個(gè)一個(gè)的找,找到initMixin,直接就找了_init方法:
export function initMixin(Vue: typeof Component) { Vue.prototype._init = function (options?: Record<string, any>) { const vm: Component = this // a uid vm._uid = uid++ let startTag, endTag /* istanbul ignore if */ if (__DEV__ && config.performance && mark) { startTag = `vue-perf-start:${vm._uid}` endTag = `vue-perf-end:${vm._uid}` mark(startTag) } // a flag to mark this as a Vue instance without having to do instanceof // check vm._isVue = true // avoid instances from being observed vm.__v_skip = true // effect scope vm._scope = new EffectScope(true /* detached */) vm._scope._vm = true // 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 as any) } else { vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor as any), options || {}, vm ) } /* istanbul ignore else */ if (__DEV__) { initProxy(vm) } else { vm._renderProxy = vm } // expose real self vm._self = vm 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') /* istanbul ignore if */ if (__DEV__ && 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) } } }
代碼這么多沒必要全都看,記住我們的目的是找到data和methods是怎么掛載到this上的;
先簡化代碼,不看沒有意義的代碼:
export function initMixin(Vue) { Vue.prototype._init = function (options) { const vm = this } }
傳遞過來的Vue并沒有做太多事情,只是把_init方法掛載到了Vue.prototype上;
在_init方法中,vm被賦值為this,這里的this就是Vue的實(shí)例,也就是我們的vm;
繼續(xù)往下看,我們有目的的看代碼,只需要看有vm和options組合出現(xiàn)的代碼,于是就看到了:
if (options && options._isComponent) { initInternalComponent(vm, options) } else { vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) }
_isComponent前面帶有_,說明是私有屬性,我們通過new Vue創(chuàng)建的實(shí)例時(shí)走到現(xiàn)在是沒有這個(gè)屬性的,所以走到else分支;
resolveConstructorOptions(vm.constructor)中沒有傳遞options,所以不看這個(gè)方法,直接看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 }
記住我們的目的,只需要關(guān)心vm和options組合出現(xiàn)的代碼,child就是options,vm就是vm,簡化之后:
export function mergeOptions(parent, child, vm) { normalizeProps(child, vm) normalizeInject(child, vm) normalizeDirectives(child) return options }
可以看到只剩下了normalizeProps、normalizeInject、normalizeDirectives這三個(gè)方法,值得我們關(guān)注,但是見名知意,這三個(gè)方法可能并不是我們想要的,跟進(jìn)去看一眼也確實(shí)不是;
雖然沒有得到我們想要的,但是從這里我們也得到了一個(gè)重要信息,mergeOptions最后會返回一個(gè)options對象,這個(gè)對象就是我們的options,最后被vm.$options接收;
vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm )
現(xiàn)在我們分析要多一步了,參數(shù)只有vm的函數(shù)也是需要引起我們的注意的,繼續(xù)往下看:
if (__DEV__) { initProxy(vm) } else { vm._renderProxy = vm }
操作了vm,但是內(nèi)部沒有操作$options,跳過,繼續(xù)往下看:
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的;
盲猜一波:
這里面最有可能是我們想要的是initState,跟進(jìn)去看一下:
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) } }
已經(jīng)找到我們想要的了,現(xiàn)在開始正式分析initState。
根據(jù)代碼結(jié)構(gòu)可以看到,initState主要做了以下幾件事:
我們可以用this來訪問的屬性是props、methods、data、computed;
看到這里也明白了,為什么在props中定義了一個(gè)屬性,在data、methods、computed中就不能再定義了,因?yàn)閜rops是最先初始化的,后面的也是同理。
initProps的作用是初始化props,跟進(jìn)去看一下:
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) }
代碼很多,我們依然不用關(guān)心其他的代碼,只關(guān)心props是怎么掛載到vm上的,根據(jù)我上面的方法,簡化后的代碼如下:
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) } } }
這里真正有關(guān)的就兩個(gè)地方:
validateProp:看名字就知道是驗(yàn)證props,跳過
proxy:代理,很可疑,跟進(jìn)去看一下:
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了。
不是很好理解,那我們來自己就用這些代碼實(shí)現(xiàn)一下:
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);
上面的代碼只是為了方便理解,所以會忽略一些細(xì)節(jié),比如props的驗(yàn)證等等,真實(shí)掛載在_props上的props是通過defineReactive實(shí)現(xiàn)的,我這里直接是空的,這些超出了本文的范圍。
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) } }
跟著之前的思路,我們忽略無關(guān)代碼,簡化后的代碼如下:
function initMethods(vm, methods) { for (const key in methods) { vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm) } }
這里的noop和bind在之前的文章中有出現(xiàn)過,可以去看一下:【源碼共讀】Vue2源碼 shared 模塊中的36個(gè)實(shí)用工具函數(shù)分析
這里的vm[key]就是methods的方法,這樣就可以通過this訪問到methods中定義的方法了。
bind的作用是把methods中定義的函數(shù)的this指向vm,這樣就可以在methods中使用this就是vm了。
簡單的實(shí)現(xiàn)一下:
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的代碼如下:
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) } }
這里的實(shí)現(xiàn)方式和initProps是一樣的,都是通過proxy把data中的屬性代理到vm上。
注意:initData的獲取值的地方是其他的不相同,這里只做提醒,不做詳細(xì)分析。
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) } }
這里的實(shí)現(xiàn)主要是通過defineComputed來定義computed屬性,進(jìn)去瞅瞅:
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) }
仔細(xì)看下來,其實(shí)實(shí)現(xiàn)方式還是和initProps和initData一樣,都是通過Object.defineProperty來定義屬性;
不過里面的getter和setter是通過createComputedGetter和createGetterInvoker來創(chuàng)建的,這里不做過多分析。
上面我們已經(jīng)分析了props、methods、data、computed的屬性為什么可以直接通過this來訪問,那么我們現(xiàn)在就來實(shí)現(xiàn)一下這個(gè)功能。
上面已經(jīng)簡單了實(shí)現(xiàn)了initProps、initMethods,而initData和initComputed的實(shí)現(xiàn)方式和initProps的方式一樣,所以我們直接復(fù)用就好了:
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)
注意:上面的代碼對比于文章中寫的示例有改動,主要是為了實(shí)現(xiàn)最后打印結(jié)果正確,增加了賦值操作。
通過上面的分析,讓我們對構(gòu)造函數(shù)的this有了更深的理解,同時(shí)對于this指向的問題也有了更深的理解。
(學(xué)習(xí)視頻分享:vuejs入門教程、編程基礎(chǔ)視頻)
以上就是聊聊Vue2為什么能通過this訪問各種選項(xiàng)中屬性的詳細(xì)內(nèi)容,更多請關(guān)注php中文網(wǎng)其它相關(guān)文章!
每個(gè)人都需要一臺速度更快、更穩(wěn)定的 PC。隨著時(shí)間的推移,垃圾文件、舊注冊表數(shù)據(jù)和不必要的后臺進(jìn)程會占用資源并降低性能。幸運(yùn)的是,許多工具可以讓 Windows 保持平穩(wěn)運(yùn)行。
微信掃碼
關(guān)注PHP中文網(wǎng)服務(wù)號
QQ掃碼
加入技術(shù)交流群
Copyright 2014-2025 http://ipnx.cn/ All Rights Reserved | php.cn | 湘ICP備2023035733號