Rumah > hujung hadapan web > View.js > Mari kita bincangkan mengapa Vue2 boleh mengakses atribut dalam pelbagai pilihan melalui ini

Mari kita bincangkan mengapa Vue2 boleh mengakses atribut dalam pelbagai pilihan melalui ini

青灯夜游
Lepaskan: 2022-12-08 20:22:39
ke hadapan
2476 orang telah melayarinya

Artikel ini akan membantu anda mentafsir kod sumber vue dan memperkenalkan sebab anda boleh menggunakan ini untuk mengakses atribut dalam pelbagai pilihan dalam Vue2. Saya harap ia akan membantu semua orang.

Mari kita bincangkan mengapa Vue2 boleh mengakses atribut dalam pelbagai pilihan melalui ini

Jika anda tidak berminat dengan cara membaca kod sumber di bawah, anda tidak perlu membacanya Anda boleh menggunakan ini untuk menavigasi terus ke [. Analisis Kod Sumber]

Cara membaca kod sumber

Terdapat banyak artikel tentang membaca kod sumber di Internet cara mereka sendiri, tetapi artikel di Internet semuanya diperhalusi dan memberitahu anda fail , apakah fungsi itu dan pembolehubah itu lakukan [Cadangan berkaitan: tutorial video vuejs, pembangunan bahagian hadapan web; ]

Tetapi ia tidak memberitahu anda cara mencari ini, Bagaimana ini difahami, bagaimana ini disahkan, bagaimana ini dihafal, dan bagaimana ini digunakan.

Saya bukan orang yang hebat, dan saya secara beransur-ansur mencari cara saya sendiri dalam proses penerokaan saya akan berkongsi cara saya di sini, dengan harapan dapat membantu semua orang.

Cara mencari titik permulaan

Semuanya sukar pada mulanya adalah yang paling sukar untuk projek hadapan fail kemasukan, biasanya daripada package.json Mulakan carian dengan medan main dalam

package.json Medan main dalam

biasanya kita boleh mencari permulaan titik kita ingin membaca melalui nilai medan ini.

VueTetapi untuk dist/vue.runtime.common.js, medan ini ialah

Fail ini adalah fail yang kami tidak fahami, jadi kami perlu mencari fail entri bagi kod sumber; Pada masa ini, kita perlu melihat medan

dalam 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"
  }
 }
Salin selepas log masuk
Anda dapat melihat bahawa terdapat banyak

dalam Vue package.json. Saya percaya semua orang boleh melihat ini, di sini kita hanya menumpukan pada dua skrip scripts dan dev; skrip build

digunakan untuk pembangunan, dan skrip dev digunakan untuk pembungkusan. Kita dapat melihat bahawa buildTerdapat pembolehubah persekitaran dev dalam skrip Nilai pembolehubah persekitaran ini ialah TARGET secara langsung mencari full-dev dalam scripts/config.js:

scripts/config.jsfull-dev

Dengan cara ini, anda boleh mencari konfigurasi yang sepadan dengan nilai ini:

Mari kita bincangkan mengapa Vue2 boleh mengakses atribut dalam pelbagai pilihan melalui ini

ialah fail kemasukan yang kami cari, dan fail ini ialah fail kemasukan kod sumber

, nilai berikut ialah

, kami boleh mencari fail ini dalam direktori
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
    }
}
Salin selepas log masuk
; 🎜> tetapi direktori

tidak ditemui dalam direktori akar, pada masa ini kami membuat tekaan berani sama ada terdapat konfigurasi alias Pada masa ini, saya kebetulan melihat fail entry di bawah Vue. Apabila saya membuka fail ini, saya mendapati bahawa terdapat alias web/entry-runtime-with-compiler.ts; untuk mengesahkan tekaan kami, kami boleh mencari web dalam

dan mendapati bahawa fail ini memang diimport:

webscripts Cari alias.js sekali lagi dan mendapati bahawa memang terdapat alias konfigurasi: web

Dengan cara ini kami boleh mengesahkan bahawa Mari kita bincangkan mengapa Vue2 boleh mengakses atribut dalam pelbagai pilihan melalui ini ialah direktori

dan kami boleh mencari

dalam direktori ini Fail ini; 🎜>Dengan cara ini, kami berjaya menemui fail kemasukan kod sumber

, dan kemudian kami boleh mula membaca kod sumber;
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')
}
Salin selepas log masuk
Saya menjumpai fail entri di atas, tetapi saya masih tidak tahu bagaimana untuk membaca kod sumber 🎜> Hampir semua kod sumber yang kami lihat sekarang menggunakan

modularisasi atau config.js modularisasi ini akan mempunyai alias atau

, dan kami boleh menggunakannya untuk melihat perkara yang dieksport ;
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)
  }
}
Salin selepas log masuk

Hanya lihat kandungan yang dieksport, abaikan perkara lain buat masa ini dan cari terus kandungan akhir yang dieksport, seperti kod sumber aliases:

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

import Vue from './runtime-with-compiler'

export default Vue
Salin selepas log masuk

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

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

import Vue from './runtime/index'

export default Vue as GlobalAPI
Salin selepas log masuk

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

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

import Vue from 'core/index'

export default Vue
Salin selepas log masuk

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

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

import Vue from './instance/index'

export default Vue
Salin selepas log masuk

这个时候就去找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
Salin selepas log masuk

这样我们就找到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 './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
Salin selepas log masuk

有这么多东西,我们不用管,要清晰目的,我们在使用Vue的时候,通常是下面这样的:

const vm = new Vue({
  data() {
    return {
      msg: 'hello world'
    }
  },
  methods: {
    say() {
      console.log(this.msg)
    }
  }
});

vm.say();
Salin selepas log masuk

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

我们要知道Vuethis为什么可以访问到datamethods,那么我们就要找到Vue的构造函数中是怎么把datamethods挂载到this上的;

很明显构造函数只做了一件事,就是调用了this._init(options)

this._init(options)
Salin selepas log masuk

那么我们就去找_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)
Salin selepas log masuk

盲猜一波,见名知意:

  • 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)
    }
  }
}
Salin selepas log masuk

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

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

export function initMixin(Vue) {
  Vue.prototype._init = function (options) {
    const vm = this
  }
}
Salin selepas log masuk

传递过来的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
    )
}
Salin selepas log masuk

_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
}
Salin selepas log masuk

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

export function mergeOptions(parent, child, vm) {

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

  return options
}
Salin selepas log masuk

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

虽然没有得到我们想要的,但是从这里我们也得到了一个重要信息,mergeOptions最后会返回一个options对象,这个对象就是我们的options,最后被vm.$options接收;

vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
    )
Salin selepas log masuk

现在我们分析要多一步了,参数只有vm的函数也是需要引起我们的注意的,继续往下看:

if (__DEV__) {
    initProxy(vm)
} else {
    vm._renderProxy = vm
}
Salin selepas log masuk

操作了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;)
Salin selepas log masuk

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)
  }
}
Salin selepas log masuk

已经找到我们想要的了,现在开始正式分析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)
}
Salin selepas log masuk

代码很多,我们依然不用关心其他的代码,只关心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)
        }
    }
}
Salin selepas log masuk

这里真正有关的就两个地方:

  • 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)
}
Salin selepas log masuk

这里的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);
Salin selepas log masuk

Mari kita bincangkan mengapa Vue2 boleh mengakses atribut dalam pelbagai pilihan melalui ini

上面的代码只是为了方便理解,所以会忽略一些细节,比如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)
  }
}
Salin selepas log masuk

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

function initMethods(vm, methods) {
    for (const key in methods) {
        vm[key] = typeof methods[key] !== &#39;function&#39; ? noop : bind(methods[key], vm)
    }
}
Salin selepas log masuk

这里的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()
Salin selepas log masuk

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++
}
Salin selepas log masuk

简化之后的代码如下:

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)
    }
}
Salin selepas log masuk

这里的实现方式和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
        )
      }
    }
  }
}
Salin selepas log masuk

简化之后的代码如下:

function initComputed(vm, computed) {
    for (const key in computed) {
        const userDef = computed[key]
        const getter = userDef

        defineComputed(vm, key, userDef)
    }
}
Salin selepas log masuk

这里的实现主要是通过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)
}
Salin selepas log masuk

仔细看下来,其实实现方式还是和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)
Salin selepas log masuk

注意:上面的代码对比于文章中写的示例有改动,主要是为了实现最后打印结果正确,增加了赋值操作。

总结

通过上面的分析,让我们对构造函数的this有了更深的理解,同时对于this指向的问题也有了更深的理解。

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

Atas ialah kandungan terperinci Mari kita bincangkan mengapa Vue2 boleh mengakses atribut dalam pelbagai pilihan melalui ini. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Label berkaitan:
sumber:juejin.cn
Kenyataan Laman Web ini
Kandungan artikel ini disumbangkan secara sukarela oleh netizen, dan hak cipta adalah milik pengarang asal. Laman web ini tidak memikul tanggungjawab undang-undang yang sepadan. Jika anda menemui sebarang kandungan yang disyaki plagiarisme atau pelanggaran, sila hubungi admin@php.cn
Tutorial Popular
Lagi>
Muat turun terkini
Lagi>
kesan web
Kod sumber laman web
Bahan laman web
Templat hujung hadapan