repeat.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608
  1. var _ = require('../util')
  2. var isObject = _.isObject
  3. var isPlainObject = _.isPlainObject
  4. var textParser = require('../parsers/text')
  5. var expParser = require('../parsers/expression')
  6. var templateParser = require('../parsers/template')
  7. var compile = require('../compiler/compile')
  8. var transclude = require('../compiler/transclude')
  9. var uid = 0
  10. // async component resolution states
  11. var UNRESOLVED = 0
  12. var PENDING = 1
  13. var RESOLVED = 2
  14. var ABORTED = 3
  15. module.exports = {
  16. /**
  17. * Setup.
  18. */
  19. bind: function () {
  20. // uid as a cache identifier
  21. this.id = '__v_repeat_' + (++uid)
  22. // we need to insert the objToArray converter
  23. // as the first read filter, because it has to be invoked
  24. // before any user filters. (can't do it in `update`)
  25. if (!this.filters) {
  26. this.filters = {}
  27. }
  28. // add the object -> array convert filter
  29. var objectConverter = _.bind(objToArray, this)
  30. if (!this.filters.read) {
  31. this.filters.read = [objectConverter]
  32. } else {
  33. this.filters.read.unshift(objectConverter)
  34. }
  35. // setup anchor node
  36. this.anchor = _.createAnchor('v-repeat')
  37. _.replace(this.el, this.anchor)
  38. // check if this is a block repeat
  39. this.template = this.el.tagName === 'TEMPLATE'
  40. ? templateParser.parse(this.el, true)
  41. : this.el
  42. // check other directives that need to be handled
  43. // at v-repeat level
  44. this.checkIf()
  45. this.checkRef()
  46. this.checkComponent()
  47. // check for trackby param
  48. this.idKey =
  49. this._checkParam('track-by') ||
  50. this._checkParam('trackby') // 0.11.0 compat
  51. this.cache = Object.create(null)
  52. },
  53. /**
  54. * Warn against v-if usage.
  55. */
  56. checkIf: function () {
  57. if (_.attr(this.el, 'if') !== null) {
  58. _.warn(
  59. 'Don\'t use v-if with v-repeat. ' +
  60. 'Use v-show or the "filterBy" filter instead.'
  61. )
  62. }
  63. },
  64. /**
  65. * Check if v-ref/ v-el is also present.
  66. */
  67. checkRef: function () {
  68. var refID = _.attr(this.el, 'ref')
  69. this.refID = refID
  70. ? this.vm.$interpolate(refID)
  71. : null
  72. var elId = _.attr(this.el, 'el')
  73. this.elId = elId
  74. ? this.vm.$interpolate(elId)
  75. : null
  76. },
  77. /**
  78. * Check the component constructor to use for repeated
  79. * instances. If static we resolve it now, otherwise it
  80. * needs to be resolved at build time with actual data.
  81. */
  82. checkComponent: function () {
  83. this.componentState = UNRESOLVED
  84. var options = this.vm.$options
  85. var id = _.checkComponent(this.el, options)
  86. if (!id) {
  87. // default constructor
  88. this.Ctor = _.Vue
  89. // inline repeats should inherit
  90. this.inherit = true
  91. // important: transclude with no options, just
  92. // to ensure block start and block end
  93. this.template = transclude(this.template)
  94. var copy = _.extend({}, options)
  95. copy._asComponent = false
  96. this._linkFn = compile(this.template, copy)
  97. } else {
  98. this.Ctor = null
  99. this.asComponent = true
  100. // check inline-template
  101. if (this._checkParam('inline-template') !== null) {
  102. // extract inline template as a DocumentFragment
  103. this.inlineTempalte = _.extractContent(this.el, true)
  104. }
  105. var tokens = textParser.parse(id)
  106. if (tokens) {
  107. // dynamic component to be resolved later
  108. var ctorExp = textParser.tokensToExp(tokens)
  109. this.ctorGetter = expParser.parse(ctorExp).get
  110. } else {
  111. // static
  112. this.componentId = id
  113. this.pendingData = null
  114. }
  115. }
  116. },
  117. resolveComponent: function () {
  118. this.componentState = PENDING
  119. this.vm._resolveComponent(this.componentId, _.bind(function (Ctor) {
  120. if (this.componentState === ABORTED) {
  121. return
  122. }
  123. this.Ctor = Ctor
  124. var merged = _.mergeOptions(Ctor.options, {}, {
  125. $parent: this.vm
  126. })
  127. merged.template = this.inlineTempalte || merged.template
  128. merged._asComponent = true
  129. merged._parent = this.vm
  130. this.template = transclude(this.template, merged)
  131. // Important: mark the template as a root node so that
  132. // custom element components don't get compiled twice.
  133. // fixes #822
  134. this.template.__vue__ = true
  135. this._linkFn = compile(this.template, merged)
  136. this.componentState = RESOLVED
  137. this.realUpdate(this.pendingData)
  138. this.pendingData = null
  139. }, this))
  140. },
  141. /**
  142. * Resolve a dynamic component to use for an instance.
  143. * The tricky part here is that there could be dynamic
  144. * components depending on instance data.
  145. *
  146. * @param {Object} data
  147. * @param {Object} meta
  148. * @return {Function}
  149. */
  150. resolveDynamicComponent: function (data, meta) {
  151. // create a temporary context object and copy data
  152. // and meta properties onto it.
  153. // use _.define to avoid accidentally overwriting scope
  154. // properties.
  155. var context = Object.create(this.vm)
  156. var key
  157. for (key in data) {
  158. _.define(context, key, data[key])
  159. }
  160. for (key in meta) {
  161. _.define(context, key, meta[key])
  162. }
  163. var id = this.ctorGetter.call(context, context)
  164. var Ctor = _.resolveAsset(this.vm.$options, 'components', id)
  165. _.assertAsset(Ctor, 'component', id)
  166. if (!Ctor.options) {
  167. _.warn(
  168. 'Async resolution is not supported for v-repeat ' +
  169. '+ dynamic component. (component: ' + id + ')'
  170. )
  171. return _.Vue
  172. }
  173. return Ctor
  174. },
  175. /**
  176. * Update.
  177. * This is called whenever the Array mutates. If we have
  178. * a component, we might need to wait for it to resolve
  179. * asynchronously.
  180. *
  181. * @param {Array|Number|String} data
  182. */
  183. update: function (data) {
  184. if (this.componentId) {
  185. var state = this.componentState
  186. if (state === UNRESOLVED) {
  187. this.pendingData = data
  188. // once resolved, it will call realUpdate
  189. this.resolveComponent()
  190. } else if (state === PENDING) {
  191. this.pendingData = data
  192. } else if (state === RESOLVED) {
  193. this.realUpdate(data)
  194. }
  195. } else {
  196. this.realUpdate(data)
  197. }
  198. },
  199. /**
  200. * The real update that actually modifies the DOM.
  201. *
  202. * @param {Array|Number|String} data
  203. */
  204. realUpdate: function (data) {
  205. data = data || []
  206. var type = typeof data
  207. if (type === 'number') {
  208. data = range(data)
  209. } else if (type === 'string') {
  210. data = _.toArray(data)
  211. }
  212. this.vms = this.diff(data, this.vms)
  213. // update v-ref
  214. if (this.refID) {
  215. this.vm.$[this.refID] = this.vms
  216. }
  217. if (this.elId) {
  218. this.vm.$$[this.elId] = this.vms.map(function (vm) {
  219. return vm.$el
  220. })
  221. }
  222. },
  223. /**
  224. * Diff, based on new data and old data, determine the
  225. * minimum amount of DOM manipulations needed to make the
  226. * DOM reflect the new data Array.
  227. *
  228. * The algorithm diffs the new data Array by storing a
  229. * hidden reference to an owner vm instance on previously
  230. * seen data. This allows us to achieve O(n) which is
  231. * better than a levenshtein distance based algorithm,
  232. * which is O(m * n).
  233. *
  234. * @param {Array} data
  235. * @param {Array} oldVms
  236. * @return {Array}
  237. */
  238. diff: function (data, oldVms) {
  239. var idKey = this.idKey
  240. var converted = this.converted
  241. var anchor = this.anchor
  242. var alias = this.arg
  243. var init = !oldVms
  244. var vms = new Array(data.length)
  245. var obj, raw, vm, i, l
  246. // First pass, go through the new Array and fill up
  247. // the new vms array. If a piece of data has a cached
  248. // instance for it, we reuse it. Otherwise build a new
  249. // instance.
  250. for (i = 0, l = data.length; i < l; i++) {
  251. obj = data[i]
  252. raw = converted ? obj.$value : obj
  253. vm = !init && this.getVm(raw, converted ? obj.$key : null)
  254. if (vm) { // reusable instance
  255. vm._reused = true
  256. vm.$index = i // update $index
  257. // update data for track-by or object repeat,
  258. // since in these two cases the data is replaced
  259. // rather than mutated.
  260. if (idKey || converted) {
  261. if (alias) {
  262. vm[alias] = raw
  263. } else if (_.isPlainObject(raw)) {
  264. vm.$data = raw
  265. } else {
  266. vm.$value = raw
  267. }
  268. }
  269. } else { // new instance
  270. vm = this.build(obj, i, true)
  271. // the _new flag is used in the second pass for
  272. // vm cache retrival, but if this is the init phase
  273. // the flag can just be set to false directly.
  274. vm._new = !init
  275. vm._reused = false
  276. }
  277. vms[i] = vm
  278. // insert if this is first run
  279. if (init) {
  280. vm.$before(anchor)
  281. }
  282. }
  283. // if this is the first run, we're done.
  284. if (init) {
  285. return vms
  286. }
  287. // Second pass, go through the old vm instances and
  288. // destroy those who are not reused (and remove them
  289. // from cache)
  290. for (i = 0, l = oldVms.length; i < l; i++) {
  291. vm = oldVms[i]
  292. if (!vm._reused) {
  293. this.uncacheVm(vm)
  294. vm.$destroy(true)
  295. }
  296. }
  297. // final pass, move/insert new instances into the
  298. // right place. We're going in reverse here because
  299. // insertBefore relies on the next sibling to be
  300. // resolved.
  301. var targetNext, currentNext
  302. i = vms.length
  303. while (i--) {
  304. vm = vms[i]
  305. // this is the vm that we should be in front of
  306. targetNext = vms[i + 1]
  307. if (!targetNext) {
  308. // This is the last item. If it's reused then
  309. // everything else will eventually be in the right
  310. // place, so no need to touch it. Otherwise, insert
  311. // it.
  312. if (!vm._reused) {
  313. vm.$before(anchor)
  314. }
  315. } else {
  316. var nextEl = targetNext.$el
  317. if (vm._reused) {
  318. // this is the vm we are actually in front of
  319. currentNext = findNextVm(vm, anchor)
  320. // we only need to move if we are not in the right
  321. // place already.
  322. if (currentNext !== targetNext) {
  323. vm.$before(nextEl, null, false)
  324. }
  325. } else {
  326. // new instance, insert to existing next
  327. vm.$before(nextEl)
  328. }
  329. }
  330. vm._new = false
  331. vm._reused = false
  332. }
  333. return vms
  334. },
  335. /**
  336. * Build a new instance and cache it.
  337. *
  338. * @param {Object} data
  339. * @param {Number} index
  340. * @param {Boolean} needCache
  341. */
  342. build: function (data, index, needCache) {
  343. var meta = { $index: index }
  344. if (this.converted) {
  345. meta.$key = data.$key
  346. }
  347. var raw = this.converted ? data.$value : data
  348. var alias = this.arg
  349. if (alias) {
  350. data = {}
  351. data[alias] = raw
  352. } else if (!isPlainObject(raw)) {
  353. // non-object values
  354. data = {}
  355. meta.$value = raw
  356. } else {
  357. // default
  358. data = raw
  359. }
  360. // resolve constructor
  361. var Ctor = this.Ctor || this.resolveDynamicComponent(data, meta)
  362. var vm = this.vm.$addChild({
  363. el: templateParser.clone(this.template),
  364. _asComponent: this.asComponent,
  365. _host: this._host,
  366. _linkFn: this._linkFn,
  367. _meta: meta,
  368. data: data,
  369. inherit: this.inherit,
  370. template: this.inlineTempalte
  371. }, Ctor)
  372. // flag this instance as a repeat instance
  373. // so that we can skip it in vm._digest
  374. vm._repeat = true
  375. // cache instance
  376. if (needCache) {
  377. this.cacheVm(raw, vm, this.converted ? meta.$key : null)
  378. }
  379. // sync back changes for two-way bindings of primitive values
  380. var type = typeof raw
  381. if (type === 'string' || type === 'number') {
  382. var dir = this
  383. vm.$watch(alias || '$value', function (val) {
  384. dir._withLock(function () {
  385. if (dir.converted) {
  386. dir.rawValue[vm.$key] = val
  387. } else {
  388. dir.rawValue.$set(vm.$index, val)
  389. }
  390. })
  391. })
  392. }
  393. return vm
  394. },
  395. /**
  396. * Unbind, teardown everything
  397. */
  398. unbind: function () {
  399. this.componentState = ABORTED
  400. if (this.refID) {
  401. this.vm.$[this.refID] = null
  402. }
  403. if (this.vms) {
  404. var i = this.vms.length
  405. var vm
  406. while (i--) {
  407. vm = this.vms[i]
  408. this.uncacheVm(vm)
  409. vm.$destroy()
  410. }
  411. }
  412. },
  413. /**
  414. * Cache a vm instance based on its data.
  415. *
  416. * If the data is an object, we save the vm's reference on
  417. * the data object as a hidden property. Otherwise we
  418. * cache them in an object and for each primitive value
  419. * there is an array in case there are duplicates.
  420. *
  421. * @param {Object} data
  422. * @param {Vue} vm
  423. * @param {String} [key]
  424. */
  425. cacheVm: function (data, vm, key) {
  426. var idKey = this.idKey
  427. var cache = this.cache
  428. var id
  429. if (key || idKey) {
  430. id = idKey ? data[idKey] : key
  431. if (!cache[id]) {
  432. cache[id] = vm
  433. } else {
  434. _.warn('Duplicate track-by key in v-repeat: ' + id)
  435. }
  436. } else if (isObject(data)) {
  437. id = this.id
  438. if (data.hasOwnProperty(id)) {
  439. if (data[id] === null) {
  440. data[id] = vm
  441. } else {
  442. _.warn(
  443. 'Duplicate objects are not supported in v-repeat ' +
  444. 'when using components or transitions.'
  445. )
  446. }
  447. } else {
  448. _.define(data, id, vm)
  449. }
  450. } else {
  451. if (!cache[data]) {
  452. cache[data] = [vm]
  453. } else {
  454. cache[data].push(vm)
  455. }
  456. }
  457. vm._raw = data
  458. },
  459. /**
  460. * Try to get a cached instance from a piece of data.
  461. *
  462. * @param {Object} data
  463. * @param {String} [key]
  464. * @return {Vue|undefined}
  465. */
  466. getVm: function (data, key) {
  467. var idKey = this.idKey
  468. if (key || idKey) {
  469. var id = idKey ? data[idKey] : key
  470. return this.cache[id]
  471. } else if (isObject(data)) {
  472. return data[this.id]
  473. } else {
  474. var cached = this.cache[data]
  475. if (cached) {
  476. var i = 0
  477. var vm = cached[i]
  478. // since duplicated vm instances might be a reused
  479. // one OR a newly created one, we need to return the
  480. // first instance that is neither of these.
  481. while (vm && (vm._reused || vm._new)) {
  482. vm = cached[++i]
  483. }
  484. return vm
  485. }
  486. }
  487. },
  488. /**
  489. * Delete a cached vm instance.
  490. *
  491. * @param {Vue} vm
  492. */
  493. uncacheVm: function (vm) {
  494. var data = vm._raw
  495. var idKey = this.idKey
  496. if (idKey || this.converted) {
  497. var id = idKey ? data[idKey] : vm.$key
  498. this.cache[id] = null
  499. } else if (isObject(data)) {
  500. data[this.id] = null
  501. vm._raw = null
  502. } else {
  503. this.cache[data].pop()
  504. }
  505. }
  506. }
  507. /**
  508. * Helper to find the next element that is an instance
  509. * root node. This is necessary because a destroyed vm's
  510. * element could still be lingering in the DOM before its
  511. * leaving transition finishes, but its __vue__ reference
  512. * should have been removed so we can skip them.
  513. *
  514. * @param {Vue} vm
  515. * @param {Comment|Text} anchor
  516. * @return {Vue}
  517. */
  518. function findNextVm (vm, anchor) {
  519. var el = (vm._blockEnd || vm.$el).nextSibling
  520. while (!el.__vue__ && el !== anchor) {
  521. el = el.nextSibling
  522. }
  523. return el.__vue__
  524. }
  525. /**
  526. * Attempt to convert non-Array objects to array.
  527. * This is the default filter installed to every v-repeat
  528. * directive.
  529. *
  530. * It will be called with **the directive** as `this`
  531. * context so that we can mark the repeat array as converted
  532. * from an object.
  533. *
  534. * @param {*} obj
  535. * @return {Array}
  536. * @private
  537. */
  538. function objToArray (obj) {
  539. // regardless of type, store the un-filtered raw value.
  540. this.rawValue = obj
  541. if (!isPlainObject(obj)) {
  542. return obj
  543. }
  544. var keys = Object.keys(obj)
  545. var i = keys.length
  546. var res = new Array(i)
  547. var key
  548. while (i--) {
  549. key = keys[i]
  550. res[i] = {
  551. $key: key,
  552. $value: obj[key]
  553. }
  554. }
  555. // `this` points to the repeat directive instance
  556. this.converted = true
  557. return res
  558. }
  559. /**
  560. * Create a range array from given number.
  561. *
  562. * @param {Number} n
  563. * @return {Array}
  564. */
  565. function range (n) {
  566. var i = -1
  567. var ret = new Array(n)
  568. while (++i < n) {
  569. ret[i] = i
  570. }
  571. return ret
  572. }