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

青灯夜游
풀어 주다: 2022-12-08 20:22:39
앞으로
2420명이 탐색했습니다.

이 글은 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="https://img.php.cn/upload/article/000/000 /024/6b576450e311a756fcf2db56ccd053cf-2.png" 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 중국어 웹사이트의 기타 관련 기사를 참조하세요!

관련 라벨:
원천:juejin.cn
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿
회사 소개 부인 성명 Sitemap
PHP 중국어 웹사이트:공공복지 온라인 PHP 교육,PHP 학습자의 빠른 성장을 도와주세요!