state.js 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. /* @flow */
  2. import config from '../config'
  3. import Watcher from '../observer/watcher'
  4. import Dep, { pushTarget, popTarget } from '../observer/dep'
  5. import { isUpdatingChildComponent } from './lifecycle'
  6. import {
  7. set,
  8. del,
  9. observe,
  10. observerState,
  11. defineReactive
  12. } from '../observer/index'
  13. import {
  14. warn,
  15. bind,
  16. noop,
  17. hasOwn,
  18. hyphenate,
  19. isReserved,
  20. handleError,
  21. nativeWatch,
  22. validateProp,
  23. isPlainObject,
  24. isServerRendering,
  25. isReservedAttribute
  26. } from '../util/index'
  27. const sharedPropertyDefinition = {
  28. enumerable: true,
  29. configurable: true,
  30. get: noop,
  31. set: noop
  32. }
  33. export function proxy (target: Object, sourceKey: string, key: string) {
  34. sharedPropertyDefinition.get = function proxyGetter () {
  35. return this[sourceKey][key]
  36. }
  37. sharedPropertyDefinition.set = function proxySetter (val) {
  38. this[sourceKey][key] = val
  39. }
  40. Object.defineProperty(target, key, sharedPropertyDefinition)
  41. }
  42. export function initState (vm: Component) {
  43. vm._watchers = []
  44. const opts = vm.$options
  45. if (opts.props) initProps(vm, opts.props)
  46. if (opts.methods) initMethods(vm, opts.methods)
  47. if (opts.data) {
  48. initData(vm)
  49. } else {
  50. observe(vm._data = {}, true /* asRootData */)
  51. }
  52. if (opts.computed) initComputed(vm, opts.computed)
  53. if (opts.watch && opts.watch !== nativeWatch) {
  54. initWatch(vm, opts.watch)
  55. }
  56. }
  57. function initProps (vm: Component, propsOptions: Object) {
  58. const propsData = vm.$options.propsData || {}
  59. const props = vm._props = {}
  60. // cache prop keys so that future props updates can iterate using Array
  61. // instead of dynamic object key enumeration.
  62. const keys = vm.$options._propKeys = []
  63. const isRoot = !vm.$parent
  64. // root instance props should be converted
  65. observerState.shouldConvert = isRoot
  66. for (const key in propsOptions) {
  67. keys.push(key)
  68. const value = validateProp(key, propsOptions, propsData, vm)
  69. /* istanbul ignore else */
  70. if (process.env.NODE_ENV !== 'production') {
  71. const hyphenatedKey = hyphenate(key)
  72. if (isReservedAttribute(hyphenatedKey) ||
  73. config.isReservedAttr(hyphenatedKey)) {
  74. warn(
  75. `"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
  76. vm
  77. )
  78. }
  79. defineReactive(props, key, value, () => {
  80. if (vm.$parent && !isUpdatingChildComponent) {
  81. warn(
  82. `Avoid mutating a prop directly since the value will be ` +
  83. `overwritten whenever the parent component re-renders. ` +
  84. `Instead, use a data or computed property based on the prop's ` +
  85. `value. Prop being mutated: "${key}"`,
  86. vm
  87. )
  88. }
  89. })
  90. } else {
  91. defineReactive(props, key, value)
  92. }
  93. // static props are already proxied on the component's prototype
  94. // during Vue.extend(). We only need to proxy props defined at
  95. // instantiation here.
  96. if (!(key in vm)) {
  97. proxy(vm, `_props`, key)
  98. }
  99. }
  100. observerState.shouldConvert = true
  101. }
  102. function initData (vm: Component) {
  103. let data = vm.$options.data
  104. data = vm._data = typeof data === 'function'
  105. ? getData(data, vm)
  106. : data || {}
  107. if (!isPlainObject(data)) {
  108. data = {}
  109. process.env.NODE_ENV !== 'production' && warn(
  110. 'data functions should return an object:\n' +
  111. 'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
  112. vm
  113. )
  114. }
  115. // proxy data on instance
  116. const keys = Object.keys(data)
  117. const props = vm.$options.props
  118. const methods = vm.$options.methods
  119. let i = keys.length
  120. while (i--) {
  121. const key = keys[i]
  122. if (process.env.NODE_ENV !== 'production') {
  123. if (methods && hasOwn(methods, key)) {
  124. warn(
  125. `Method "${key}" has already been defined as a data property.`,
  126. vm
  127. )
  128. }
  129. }
  130. if (props && hasOwn(props, key)) {
  131. process.env.NODE_ENV !== 'production' && warn(
  132. `The data property "${key}" is already declared as a prop. ` +
  133. `Use prop default value instead.`,
  134. vm
  135. )
  136. } else if (!isReserved(key)) {
  137. proxy(vm, `_data`, key)
  138. }
  139. }
  140. // observe data
  141. observe(data, true /* asRootData */)
  142. }
  143. export function getData (data: Function, vm: Component): any {
  144. // #7573 disable dep collection when invoking data getters
  145. pushTarget()
  146. try {
  147. return data.call(vm, vm)
  148. } catch (e) {
  149. handleError(e, vm, `data()`)
  150. return {}
  151. } finally {
  152. popTarget()
  153. }
  154. }
  155. const computedWatcherOptions = { lazy: true }
  156. function initComputed (vm: Component, computed: Object) {
  157. // $flow-disable-line
  158. const watchers = vm._computedWatchers = Object.create(null)
  159. // computed properties are just getters during SSR
  160. const isSSR = isServerRendering()
  161. for (const key in computed) {
  162. const userDef = computed[key]
  163. const getter = typeof userDef === 'function' ? userDef : userDef.get
  164. if (process.env.NODE_ENV !== 'production' && getter == null) {
  165. warn(
  166. `Getter is missing for computed property "${key}".`,
  167. vm
  168. )
  169. }
  170. if (!isSSR) {
  171. // create internal watcher for the computed property.
  172. watchers[key] = new Watcher(
  173. vm,
  174. getter || noop,
  175. noop,
  176. computedWatcherOptions
  177. )
  178. }
  179. // component-defined computed properties are already defined on the
  180. // component prototype. We only need to define computed properties defined
  181. // at instantiation here.
  182. if (!(key in vm)) {
  183. defineComputed(vm, key, userDef)
  184. } else if (process.env.NODE_ENV !== 'production') {
  185. if (key in vm.$data) {
  186. warn(`The computed property "${key}" is already defined in data.`, vm)
  187. } else if (vm.$options.props && key in vm.$options.props) {
  188. warn(`The computed property "${key}" is already defined as a prop.`, vm)
  189. }
  190. }
  191. }
  192. }
  193. export function defineComputed (
  194. target: any,
  195. key: string,
  196. userDef: Object | Function
  197. ) {
  198. const shouldCache = !isServerRendering()
  199. if (typeof userDef === 'function') {
  200. sharedPropertyDefinition.get = shouldCache
  201. ? createComputedGetter(key)
  202. : userDef
  203. sharedPropertyDefinition.set = noop
  204. } else {
  205. sharedPropertyDefinition.get = userDef.get
  206. ? shouldCache && userDef.cache !== false
  207. ? createComputedGetter(key)
  208. : userDef.get
  209. : noop
  210. sharedPropertyDefinition.set = userDef.set
  211. ? userDef.set
  212. : noop
  213. }
  214. if (process.env.NODE_ENV !== 'production' &&
  215. sharedPropertyDefinition.set === noop) {
  216. sharedPropertyDefinition.set = function () {
  217. warn(
  218. `Computed property "${key}" was assigned to but it has no setter.`,
  219. this
  220. )
  221. }
  222. }
  223. Object.defineProperty(target, key, sharedPropertyDefinition)
  224. }
  225. function createComputedGetter (key) {
  226. return function computedGetter () {
  227. const watcher = this._computedWatchers && this._computedWatchers[key]
  228. if (watcher) {
  229. if (watcher.dirty) {
  230. watcher.evaluate()
  231. }
  232. if (Dep.target) {
  233. watcher.depend()
  234. }
  235. return watcher.value
  236. }
  237. }
  238. }
  239. function initMethods (vm: Component, methods: Object) {
  240. const props = vm.$options.props
  241. for (const key in methods) {
  242. if (process.env.NODE_ENV !== 'production') {
  243. if (methods[key] == null) {
  244. warn(
  245. `Method "${key}" has an undefined value in the component definition. ` +
  246. `Did you reference the function correctly?`,
  247. vm
  248. )
  249. }
  250. if (props && hasOwn(props, key)) {
  251. warn(
  252. `Method "${key}" has already been defined as a prop.`,
  253. vm
  254. )
  255. }
  256. if ((key in vm) && isReserved(key)) {
  257. warn(
  258. `Method "${key}" conflicts with an existing Vue instance method. ` +
  259. `Avoid defining component methods that start with _ or $.`
  260. )
  261. }
  262. }
  263. vm[key] = methods[key] == null ? noop : bind(methods[key], vm)
  264. }
  265. }
  266. function initWatch (vm: Component, watch: Object) {
  267. for (const key in watch) {
  268. const handler = watch[key]
  269. if (Array.isArray(handler)) {
  270. for (let i = 0; i < handler.length; i++) {
  271. createWatcher(vm, key, handler[i])
  272. }
  273. } else {
  274. createWatcher(vm, key, handler)
  275. }
  276. }
  277. }
  278. function createWatcher (
  279. vm: Component,
  280. expOrFn: string | Function,
  281. handler: any,
  282. options?: Object
  283. ) {
  284. if (isPlainObject(handler)) {
  285. options = handler
  286. handler = handler.handler
  287. }
  288. if (typeof handler === 'string') {
  289. handler = vm[handler]
  290. }
  291. return vm.$watch(expOrFn, handler, options)
  292. }
  293. export function stateMixin (Vue: Class<Component>) {
  294. // flow somehow has problems with directly declared definition object
  295. // when using Object.defineProperty, so we have to procedurally build up
  296. // the object here.
  297. const dataDef = {}
  298. dataDef.get = function () { return this._data }
  299. const propsDef = {}
  300. propsDef.get = function () { return this._props }
  301. if (process.env.NODE_ENV !== 'production') {
  302. dataDef.set = function (newData: Object) {
  303. warn(
  304. 'Avoid replacing instance root $data. ' +
  305. 'Use nested data properties instead.',
  306. this
  307. )
  308. }
  309. propsDef.set = function () {
  310. warn(`$props is readonly.`, this)
  311. }
  312. }
  313. Object.defineProperty(Vue.prototype, '$data', dataDef)
  314. Object.defineProperty(Vue.prototype, '$props', propsDef)
  315. Vue.prototype.$set = set
  316. Vue.prototype.$delete = del
  317. Vue.prototype.$watch = function (
  318. expOrFn: string | Function,
  319. cb: any,
  320. options?: Object
  321. ): Function {
  322. const vm: Component = this
  323. if (isPlainObject(cb)) {
  324. return createWatcher(vm, expOrFn, cb, options)
  325. }
  326. options = options || {}
  327. options.user = true
  328. const watcher = new Watcher(vm, expOrFn, cb, options)
  329. if (options.immediate) {
  330. cb.call(vm, watcher.value)
  331. }
  332. return function unwatchFn () {
  333. watcher.teardown()
  334. }
  335. }
  336. }