목차
방법으로 이동하실 수 있습니다. 소스 코드 읽기
시작점을 찾는 방법
如何阅读源码
带有目的的阅读源码
源码分析
initState
initProps
initMethods
initData
initComputed
动手时间
总结
웹 프론트엔드 View.js Vue2가 이를 통해 다양한 옵션의 속성에 접근할 수 있는 이유에 대해 이야기해보겠습니다.

Vue2가 이를 통해 다양한 옵션의 속성에 접근할 수 있는 이유에 대해 이야기해보겠습니다.

Dec 08, 2022 pm 08:22 PM
vue vue.js this

이 글은 vue의 소스 코드를 해석하는 데 도움이 될 것이며, 이를 사용하여 Vue2의 다양한 옵션에서 속성에 액세스할 수 있는 이유를 소개하는 글이 모든 사람에게 도움이 되기를 바랍니다!

Vue2가 이를 통해 다양한 옵션의 속성에 접근할 수 있는 이유에 대해 이야기해보겠습니다.

아래 소스코드를 읽는 방법이 궁금하지 않으시면, 이를 이용하시면 바로 [소스코드 분석]

방법으로 이동하실 수 있습니다. 소스 코드 읽기

인터넷에 소스 코드 읽기에 대한 기사가 많이 있습니다. 다들 각자의 방식이 있지만 인터넷에 있는 기사는 모두 정제되어 어떤 파일, 어떤 기능, 어떤 변수가 무엇을 하는지 알려줍니다. ; [관련 권장 사항: vuejs 비디오 튜토리얼, 웹 프론트엔드 개발]

하지만 이것을 찾는 방법, 이해하는 방법, 확인하는 방법, 기억하는 방법 및 방법을 알려줍니다. 그것들을 적용하십시오.

나는 훌륭한 사람이 아니며 탐색 과정에서 점차 나만의 길을 찾았습니다. 모든 사람에게 도움이 되기를 바라면서 여기에서 내 길을 공유하겠습니다.

시작점을 찾는 방법

모든 것이 처음에는 어렵습니다. 시작점을 찾는 것이 가장 어렵습니다. 프론트 엔드 프로젝트에서는 일반적으로 의 <code>에서 항목 파일을 찾고 싶습니다. package.json 메인 필드 찾기를 시작하세요. 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"
  }
 }
로그인 후 복사

可以看到Vuepackage.json中有很多的scripts,这些相信大家都可以看得懂,这里我们只关注devbuild这两个脚本;

dev脚本是用来开发的,build脚本是用来打包的,我们可以看到dev脚本中有一个TARGET的环境变量,这个环境变量的值是full-dev,我们可以在scripts/config.js中找到这个值;

直接在scripts/config.js中搜索full-dev

Vue2가 이를 통해 다양한 옵션의 속성에 접근할 수 있는 이유에 대해 이야기해보겠습니다.

这样就可以找到这个值对应的配置:

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的别名;

Vue2가 이를 통해 다양한 옵션의 속성에 접근할 수 있는 이유에 대해 이야기해보겠습니다.

代码如下:

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这个文件;

Vue2가 이를 통해 다양한 옵션의 속성에 접근할 수 있는 이유에 대해 이야기해보겠습니다.

这样我们就成功的找到了Vue的源码入口文件,接下来我们就可以开始阅读源码了;

如何阅读源码

上面找到了入口文件,但是还是不知道如何阅读源码,这个时候我们就需要一些技巧了,这里我就分享一下我自己的阅读源码的技巧;

像我们现在看的源码几乎都是使用esm模块化或者commonjs模块化的,这些都会有一个export或者module.exports,我们可以通过这个来看导出了什么;

只看导出的内容,其他的暂时不用管,直接找到最终导出的内容,例如Vue

package.jsonmain 필드는 일반적으로 이 패키지의 항목 파일을 나타냅니다. 이 필드의 값을 사용하여 읽고 싶은 내용의 시작점을 찾을 수 있습니다. 🎜🎜하지만 Vue의 경우 이 필드는 dist/vue.runtime.common.js입니다. 이 파일은 컴파일된 파일이므로 우리가 찾아야 합니다. 🎜🎜이번에는 package.jsonscripts 필드를 살펴봐야 합니다. 🎜
import Vue from &#39;./runtime-with-compiler&#39;

export default Vue
로그인 후 복사
로그인 후 복사
🎜Vue를 볼 수 있습니다. </ code>의 <code>package.json에는 많은 scripts가 있습니다. 여기서는 dev에만 집중하겠습니다. > 및 build 이 두 스크립트는 🎜🎜dev 스크립트가 개발에 사용되고 build 스크립트가 패키징에 사용되는 것을 볼 수 있습니다. code> dev 스크립트에 TARGET 환경 변수가 있습니다. 이 환경 변수의 값은 full-dev에서 설정할 수 있습니다. scripts/config.js; 🎜🎜 scripts/config.js에서 full-dev를 직접 검색하세요: 🎜🎜Vue2가 이를 통해 다양한 옵션의 속성에 접근할 수 있는 이유에 대해 이야기해보겠습니다.🎜🎜이런 식으로 이에 해당하는 구성을 찾을 수 있습니다. value: 🎜
import Vue from &#39;./runtime/index&#39;

export default Vue as GlobalAPI
로그인 후 복사
로그인 후 복사
🎜< code>entry 필드는 우리가 찾고 있는 엔트리 파일입니다. 이 파일은 Vue의 소스코드 엔트리 파일이고, 다음 값은 입니다. web/entry-runtime-with-compiler.ts , web 디렉토리에서 이 파일을 찾을 수 있습니다. 🎜🎜그러나 web 디렉토리는 다음에서 찾을 수 없습니다. 이때, 우연히 scripts 아래에 alias.js 파일이 보였습니다. 파일을 열어보니 이 들어 있었습니다. 🎜🎜Vue2가 이를 통해 다양한 옵션의 속성에 접근할 수 있는 이유에 대해 이야기해보겠습니다.🎜🎜코드는 다음과 같습니다.🎜
import Vue from &#39;core/index&#39;

export default Vue
로그인 후 복사
로그인 후 복사
🎜우리의 추측을 확인하기 위해 alias<를 검색할 수 있습니다. /code>를 <code>config.js에 입력하고 이 파일이 실제로 도입된 것을 확인합니다:🎜
import Vue from &#39;./instance/index&#39;

export default Vue
로그인 후 복사
로그인 후 복사
🎜again aliases를 검색한 후 실제로 구성된 별칭이 있음을 발견했습니다: 🎜
function Vue(options) {
    if (__DEV__ && !(this instanceof Vue)) {
        warn(&#39;Vue is a constructor and should be called with the `new` keyword&#39;)
    }
    this._init(options)
}

export default Vue as unknown as GlobalAPI
로그인 후 복사
로그인 후 복사
🎜이 방법으로 websrc/platforms/web 디렉터리인지 확인할 수 있습니다. entry-runtime-with-compiler.ts</ 파일을 찾을 수 있습니다. code> 🎜🎜<img src="/static/imghw/default1.png" data-src="https://img.php.cn/upload/article/000/000/024/02ca18f40c739ec6bb99dea57560d00e-3.png" class="lazy" alt="Vue2가 이를 통해 다양한 옵션의 속성에 접근할 수 있는 이유에 대해 이야기해보겠습니다." loading="lazy "/>🎜🎜이런 식으로 <code>Vue의 소스 코드 입력 파일을 성공적으로 찾았고, 소스 코드 읽기를 시작할 수 있습니다. 🎜

🎜소스 코드 읽는 방법🎜🎜🎜위의 항목 파일을 찾았지만 여전히 소스 코드를 읽는 방법을 모르겠습니다. 이때 소스를 읽는 방법을 알려 드리겠습니다. 🎜🎜 현재 우리가 보고 있는 거의 모든 소스 코드는 esm 모듈화 또는 commonjs 모듈화를 사용합니다. code>module.exports를 사용하여 내보낸 내용을 확인할 수 있습니다. 🎜🎜내보낸 콘텐츠만 보고 다른 콘텐츠는 무시하고 소스 코드와 같은 최종 내보낸 콘텐츠를 직접 찾을 수 있습니다. Vue: 🎜
  • entry-runtime-with-compiler.ts的导出内容:

import Vue from &#39;./runtime-with-compiler&#39;

export default Vue
로그인 후 복사
로그인 후 복사

这个时候就去找runtime-with-compiler.ts的导出内容:

  • runtime-with-compiler.ts的导出内容:

import Vue from &#39;./runtime/index&#39;

export default Vue as GlobalAPI
로그인 후 복사
로그인 후 복사

这个时候就去找runtime/index.ts的导出内容:

  • runtime/index.ts的导出内容:

import Vue from &#39;core/index&#39;

export default Vue
로그인 후 복사
로그인 후 복사

这个时候就去找core/index.ts的导出内容:

  • core/index.ts的导出内容:

import Vue from &#39;./instance/index&#39;

export default Vue
로그인 후 복사
로그인 후 복사

这个时候就去找instance/index.ts的导出内容:

  • instance/index.ts的导出内容:

function Vue(options) {
    if (__DEV__ && !(this instanceof Vue)) {
        warn(&#39;Vue is a constructor and should be called with the `new` keyword&#39;)
    }
    this._init(options)
}

export default Vue as unknown as GlobalAPI
로그인 후 복사
로그인 후 복사

这样我们就找到Vue的构造函数了,这个时候我们就可以开始阅读源码了;

带有目的的阅读源码

阅读源码的目的一定要清晰,当然你可以说目的就是了解Vue的实现原理,但是这个目的太宽泛了,我们可以把目的细化一下,例如:

  • Vue的生命周期是怎么实现的

  • Vue的数据响应式是怎么实现的

  • Vue的模板编译是怎么实现的

  • Vue的组件化是怎么实现的

  • Vue的插槽是怎么实现的

  • 等等...

例如我们的这次阅读计划就是了解Vuethis为什么可以访问到选项中的各种属性,这里再细分为:

  • Vuethis是怎么访问到data

  • Vuethis是怎么访问到methods

  • Vuethis是怎么访问到computed

  • Vuethis是怎么访问到props

上面顺序不分先后,但是答案一定是在源码中。

源码分析

上面已经找到了Vue的入口文件,接下来我们就可以开始阅读源码了,这里我就以Vuethis为什么可以访问到选项中的各种属性为例,来分析Vue的源码;

首先看一下instance/index.ts的源码:

import { initMixin } from &#39;./init&#39;
import { stateMixin } from &#39;./state&#39;
import { renderMixin } from &#39;./render&#39;
import { eventsMixin } from &#39;./events&#39;
import { lifecycleMixin } from &#39;./lifecycle&#39;
import { warn } from &#39;../util/index&#39;
import type { GlobalAPI } from &#39;types/global-api&#39;

function Vue(options) {
  if (__DEV__ && !(this instanceof Vue)) {
    warn(&#39;Vue is a constructor and should be called with the `new` keyword&#39;)
  }
  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: &#39;hello world&#39;
    }
  },
  methods: {
    say() {
      console.log(this.msg)
    }
  }
});

vm.say();
로그인 후 복사

也就是Vue的构造函数接收一个选项对象,这个选项对象中有datamethods

我们要知道Vuethis为什么可以访问到datamethods,那么我们就要找到Vue的构造函数中是怎么把datamethods挂载到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, &#39;beforeCreate&#39;, undefined, false /* setContext */)
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, &#39;created&#39;)

    /* 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)
    }
  }
}
로그인 후 복사

代码这么多没必要全都看,记住我们的目的是找到datamethods是怎么挂载到this上的;

先简化代码,不看没有意义的代码:

export function initMixin(Vue) {
  Vue.prototype._init = function (options) {
    const vm = this
  }
}
로그인 후 복사

传递过来的Vue并没有做太多事情,只是把_init方法挂载到了Vue.prototype上;

_init方法中,vm被赋值为this,这里的this就是Vue的实例,也就是我们的vm

继续往下看,我们有目的的看代码,只需要看有vmoptions组合出现的代码,于是就看到了:

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&#39;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
}
로그인 후 복사

记住我们的目的,只需要关心vmoptions组合出现的代码,child就是optionsvm就是vm,简化之后:

export function mergeOptions(parent, child, vm) {

  normalizeProps(child, vm)
  normalizeInject(child, vm)
  normalizeDirectives(child)

  return options
}
로그인 후 복사

可以看到只剩下了normalizePropsnormalizeInjectnormalizeDirectives这三个方法,值得我们关注,但是见名知意,这三个方法可能并不是我们想要的,跟进去看一眼也确实不是;

虽然没有得到我们想要的,但是从这里我们也得到了一个重要信息,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, &#39;beforeCreate&#39;, undefined, false /* setContext */)
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, &#39;created&#39;)
로그인 후 복사

initLifecycleinitEventsinitRenderinitInjectionsinitStateinitProvide这些方法都是操作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来访问的属性是propsmethodsdatacomputed

看到这里也明白了,为什么在props中定义了一个属性,在datamethodscomputed中就不能再定义了,因为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&#39;s ` +
              `value. Prop being mutated: "${key}"`,
            vm
          )
        }
      })
    } else {
      defineReactive(props, key, value)
    }
    // static props are already proxied on the component&#39;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就是vmsourceKey就是_propskey就是props的属性名;

这里通过Object.definePropertyvm的属性代理到_props上,这样就可以通过this访问到props了。

不是很好理解,那我们来自己就用这些代码实现一下:

var options = {
    props: {
        name: {
            type: String,
            default: &#39;default name&#39;
        }
    }
}

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 = &#39;name&#39;

console.log(vm.name);
console.log(vm._props.name);
로그인 후 복사

Vue2가 이를 통해 다양한 옵션의 속성에 접근할 수 있는 이유에 대해 이야기해보겠습니다.

上面的代码只是为了方便理解,所以会忽略一些细节,比如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] !== &#39;function&#39;) {
        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] !== &#39;function&#39; ? noop : bind(methods[key], vm)
  }
}
로그인 후 복사

跟着之前的思路,我们忽略无关代码,简化后的代码如下:

function initMethods(vm, methods) {
    for (const key in methods) {
        vm[key] = typeof methods[key] !== &#39;function&#39; ? noop : bind(methods[key], vm)
    }
}
로그인 후 복사

这里的noopbind在之前的文章中有出现过,可以去看一下:【源码共读】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] !== &#39;function&#39; ? 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(
        &#39;data functions should return an object:\n&#39; +
          &#39;https://v2.vuejs.org/v2/guide/components.html#data-Must-Be-a-Function&#39;,
        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是一样的,都是通过proxydata中的属性代理到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)
}
로그인 후 복사

仔细看下来,其实实现方式还是和initPropsinitData一样,都是通过Object.defineProperty来定义属性;

不过里面的gettersetter是通过createComputedGettercreateGetterInvoker来创建的,这里不做过多分析。

动手时间

上面我们已经分析了propsmethodsdatacomputed的属性为什么可以直接通过this来访问,那么我们现在就来实现一下这个功能。

上面已经简单了实现了initPropsinitMethods,而initDatainitComputed的实现方式和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] !== &#39;function&#39; ? 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指向的问题也有了更深的理解。

(学习视频分享:vuejs入门教程编程基础视频

위 내용은 Vue2가 이를 통해 다양한 옵션의 속성에 접근할 수 있는 이유에 대해 이야기해보겠습니다.의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.

핫 AI 도구

Undresser.AI Undress

Undresser.AI Undress

사실적인 누드 사진을 만들기 위한 AI 기반 앱

AI Clothes Remover

AI Clothes Remover

사진에서 옷을 제거하는 온라인 AI 도구입니다.

Undress AI Tool

Undress AI Tool

무료로 이미지를 벗다

Clothoff.io

Clothoff.io

AI 옷 제거제

AI Hentai Generator

AI Hentai Generator

AI Hentai를 무료로 생성하십시오.

인기 기사

R.E.P.O. 에너지 결정과 그들이하는 일 (노란색 크리스탈)
3 몇 주 전 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O. 최고의 그래픽 설정
3 몇 주 전 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O. 아무도들을 수없는 경우 오디오를 수정하는 방법
3 몇 주 전 By 尊渡假赌尊渡假赌尊渡假赌
WWE 2K25 : Myrise에서 모든 것을 잠금 해제하는 방법
4 몇 주 전 By 尊渡假赌尊渡假赌尊渡假赌

뜨거운 도구

메모장++7.3.1

메모장++7.3.1

사용하기 쉬운 무료 코드 편집기

SublimeText3 중국어 버전

SublimeText3 중국어 버전

중국어 버전, 사용하기 매우 쉽습니다.

스튜디오 13.0.1 보내기

스튜디오 13.0.1 보내기

강력한 PHP 통합 개발 환경

드림위버 CS6

드림위버 CS6

시각적 웹 개발 도구

SublimeText3 Mac 버전

SublimeText3 Mac 버전

신 수준의 코드 편집 소프트웨어(SublimeText3)

vue.js vs. React : 프로젝트 별 고려 사항 vue.js vs. React : 프로젝트 별 고려 사항 Apr 09, 2025 am 12:01 AM

vue.js는 중소형 프로젝트 및 빠른 반복에 적합한 반면 React는 크고 복잡한 응용 프로그램에 적합합니다. 1) vue.js는 사용하기 쉽고 팀이 불충분하거나 프로젝트 규모가 작는 상황에 적합합니다. 2) React는 더 풍부한 생태계를 가지고 있으며 고성능 및 복잡한 기능적 요구가있는 프로젝트에 적합합니다.

vue.js로 JS 파일을 참조하는 방법 vue.js로 JS 파일을 참조하는 방법 Apr 07, 2025 pm 11:27 PM

vue.js에서 JS 파일을 참조하는 세 가지 방법이 있습니다. & lt; script & gt; 꼬리표;; mounted () 라이프 사이클 후크를 사용한 동적 가져 오기; Vuex State Management Library를 통해 수입.

Vue 용 버튼에 기능을 추가하는 방법 Vue 용 버튼에 기능을 추가하는 방법 Apr 08, 2025 am 08:51 AM

HTML 템플릿의 버튼을 메소드에 바인딩하여 VUE 버튼에 함수를 추가 할 수 있습니다. 메소드를 정의하고 VUE 인스턴스에서 기능 로직을 작성하십시오.

Vue에서 시계를 사용하는 방법 Vue에서 시계를 사용하는 방법 Apr 07, 2025 pm 11:36 PM

vue.js의 시계 옵션을 사용하면 개발자가 특정 데이터의 변경 사항을들을 수 있습니다. 데이터가 변경되면 콜백 기능을 트리거하여 업데이트보기 또는 기타 작업을 수행합니다. 구성 옵션에는 즉시 콜백을 실행할지 여부와 DEEP를 지정하는 즉시 포함되며, 이는 객체 또는 어레이에 대한 변경 사항을 재귀 적으로 듣는 지 여부를 지정합니다.

Vue에서 부트 스트랩을 사용하는 방법 Vue에서 부트 스트랩을 사용하는 방법 Apr 07, 2025 pm 11:33 PM

vue.js에서 bootstrap 사용은 5 단계로 나뉩니다 : Bootstrap 설치. main.js.의 부트 스트랩 가져 오기 부트 스트랩 구성 요소를 템플릿에서 직접 사용하십시오. 선택 사항 : 사용자 정의 스타일. 선택 사항 : 플러그인을 사용하십시오.

Vue가 이전 페이지로 돌아 오는 방법 Vue가 이전 페이지로 돌아 오는 방법 Apr 07, 2025 pm 11:30 PM

vue.js는 이전 페이지로 돌아갈 수있는 네 가지 방법이 있습니다. $ router.go (-1) $ router.back () 사용 & lt; router-link to = & quot;/quot; Component Window.history.back () 및 메소드 선택은 장면에 따라 다릅니다.

Vue는 천막/텍스트 스크롤 효과를 인식합니다 Vue는 천막/텍스트 스크롤 효과를 인식합니다 Apr 07, 2025 pm 10:51 PM

CSS 애니메이션 또는 타사 라이브러리를 사용하여 VUE에서 Marquee/Text Scrolling Effects를 구현하십시오. 이 기사는 CSS 애니메이션 사용 방법을 소개합니다. & lt; div & gt; CSS 애니메이션을 정의하고 오버플로를 설정하십시오 : 숨겨진, 너비 및 애니메이션. 키 프레임을 정의하고 변환을 설정하십시오 : Translatex () 애니메이션의 시작과 끝에서. 지속 시간, 스크롤 속도 및 방향과 같은 애니메이션 속성을 조정하십시오.

VUE 버전을 쿼리하는 방법 VUE 버전을 쿼리하는 방법 Apr 07, 2025 pm 11:24 PM

Vue DevTools를 사용하여 브라우저 콘솔에서 vue 탭을 보면 VUE 버전을 쿼리 할 수 ​​있습니다. npm을 사용하여 "npm list -g vue"명령을 실행하십시오. package.json 파일의 "종속성"객체에서 vue 항목을 찾으십시오. Vue Cli 프로젝트의 경우 "vue -version"명령을 실행하십시오. & lt; script & gt에서 버전 정보를 확인하십시오. vue 파일을 나타내는 html 파일의 태그.

See all articles