前端(vue)入門到精通課程,老師線上輔導:聯絡老師
Apipost = Postman + Swagger + Mock + Jmeter 超好用的API偵錯工具:
下方的如何閱讀原始碼不感興趣可以不用看,可以通過這個直接定位到【】
網上有很多關於原始碼閱讀的文章,每個人都有自己的方式,但是網上的文章都是精煉之後的,告訴你哪個檔案、那個函數、那個變數是幹什麼的;【相關推薦:、】
但是沒有告訴你這些是怎麼找到的,這些是怎麼理解的,這些是怎麼驗證的,這些是怎麼記憶的,這些是怎麼應用的。
我也不是什麼大神,也是在摸索的過程中,逐漸找到了自己的方式,我這裡就分享一下我的方式,希望能幫助到大家。
萬事開頭難,找到起點是最難的,對於前端專案,我們想要找到入口檔案,一般都是從package.json
中的main
欄位開始找;
package.json
中的main
欄位代表的是這個包的入口檔案,通常我們可以通過這個欄位的值來找到我們要閱讀的起點。
但是對於Vue
來說,這個欄位是dist/vue.runtime.common.js
,這個檔案是編譯後的檔案,我們是看不懂的,所以需要找到原始碼的入口檔案;
這個時候我們就需要看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
,這些相信大家都可以看得懂,這裡我們只關注dev
和build
這兩個指令碼;
dev
指令碼是用來開發的,build
指令碼是用來打包的,我們可以看到dev
指令碼中有一個TARGET
的環境變數,這個環境變數的值是full-dev
,我們可以在scripts/config.js
中找到這個值;
直接在scripts/config.js
中搜尋full-dev
:
這樣就可以找到這個值對應的設定:
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
欄位就是我們要找的入口檔案,這個檔案就是Vue
的原始碼入口檔案,後面的值是web/entry-runtime-with-compiler.ts
,我們可以在web
目錄下找到這個檔案;
但是並沒有在根目錄下找到web
目錄,這個時候我們就大膽猜測,是不是有別名設定,這個時候我也正好在scripts
下看到了一個alias.js
檔案,開啟這個檔案,發現裡面有一個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')
}
登入後複製
為了驗證我們的猜測,我們可以在config.js
中搜一下alias
,發現確實有引入這個檔案:
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
,發現確實有設定別名:
// 省略部分程式碼
const config = {
plugins: [
alias({
entries: Object.assign({}, aliases, opts.alias)
}),
].concat(opts.plugins || []),
}
登入後複製
這樣我們就可以確認,web
就是src/platforms/web
這個目錄,我們可以在這個目錄下找到entry-runtime-with-compiler.ts
這個檔案;
這樣我們就成功的找到了Vue
的原始碼入口檔案,接下來我們就可以開始閱讀原始碼了;
上面找到了入口檔案,但是還是不知道如何閱讀原始碼,這個時候我們就需要一些技巧了,這裡我就分享一下我自己的閱讀原始碼的技巧;
像我們現在看的原始碼幾乎都是使用esm
模組化或者commonjs
模組化的,這些都會有一個export
或者module.exports
,我們可以通過這個來看匯出了什麼;
只看匯出的內容,其他的暫時不用管,直接找到最終匯出的內容,例如Vue
的原始碼:
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<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
的範例,也就是我們的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
主要做了以下幾件事:
props
setup
methods
data
computed
watch
我們可以用this
來存取的屬性是props
、methods
、data
、computed
;
看到這裡也明白了,為什麼在props
中定義了一個屬性,在data
、methods
、computed
中就不能再定義了,因為props
是最先初始化的,後面的也是同理。
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
的程式碼如下:
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
在之前的文章中有出現過,可以去看一下:
這裡的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
的程式碼如下:
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
的程式碼如下:
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
指向的問題也有了更深的理解。
(學習視訊分享:、)
以上就是聊聊Vue2為什麼能通過this存取各種選項中屬性的詳細內容,更多請關注TW511.COM其它相關文章!