repeat.js 16 KB

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