x-for.js 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. import { addScopeToNode, refreshScope } from '../scope'
  2. import { evaluateLater } from '../evaluator'
  3. import { directive } from '../directives'
  4. import { reactive } from '../reactivity'
  5. import { initTree } from '../lifecycle'
  6. import { mutateDom } from '../mutation'
  7. import { flushJobs } from '../scheduler'
  8. import { warn } from '../utils/warn'
  9. directive('for', (el, { expression }, { effect, cleanup }) => {
  10. let iteratorNames = parseForExpression(expression)
  11. let evaluateItems = evaluateLater(el, iteratorNames.items)
  12. let evaluateKey = evaluateLater(el,
  13. // the x-bind:key expression is stored for our use instead of evaluated.
  14. el._x_keyExpression || 'index'
  15. )
  16. el._x_prevKeys = []
  17. el._x_lookup = {}
  18. effect(() => loop(el, iteratorNames, evaluateItems, evaluateKey))
  19. cleanup(() => {
  20. Object.values(el._x_lookup).forEach(el => el.remove())
  21. delete el._x_prevKeys
  22. delete el._x_lookup
  23. })
  24. })
  25. let shouldFastRender = true
  26. function loop(el, iteratorNames, evaluateItems, evaluateKey) {
  27. let isObject = i => typeof i === 'object' && ! Array.isArray(i)
  28. let templateEl = el
  29. evaluateItems(items => {
  30. // Prepare yourself. There's a lot going on here. Take heart,
  31. // every bit of complexity in this function was added for
  32. // the purpose of making Alpine fast with large datas.
  33. // Support number literals. Ex: x-for="i in 100"
  34. if (isNumeric(items) && items >= 0) {
  35. items = Array.from(Array(items).keys(), i => i + 1)
  36. }
  37. if (items === undefined) items = []
  38. let lookup = el._x_lookup
  39. let prevKeys = el._x_prevKeys
  40. let scopes = []
  41. let keys = []
  42. // In order to preserve DOM elements (move instead of replace)
  43. // we need to generate all the keys for every iteration up
  44. // front. These will be our source of truth for diffing.
  45. if (isObject(items)) {
  46. items = Object.entries(items).map(([key, value]) => {
  47. let scope = getIterationScopeVariables(iteratorNames, value, key, items)
  48. evaluateKey(value => keys.push(value), { scope: { index: key, ...scope} })
  49. scopes.push(scope)
  50. })
  51. } else {
  52. for (let i = 0; i < items.length; i++) {
  53. let scope = getIterationScopeVariables(iteratorNames, items[i], i, items)
  54. evaluateKey(value => keys.push(value), { scope: { index: i, ...scope} })
  55. scopes.push(scope)
  56. }
  57. }
  58. // Rather than making DOM manipulations inside one large loop, we'll
  59. // instead track which mutations need to be made in the following
  60. // arrays. After we're finished, we can batch them at the end.
  61. let adds = []
  62. let moves = []
  63. let removes = []
  64. let sames = []
  65. // First, we track elements that will need to be removed.
  66. for (let i = 0; i < prevKeys.length; i++) {
  67. let key = prevKeys[i]
  68. if (keys.indexOf(key) === -1) removes.push(key)
  69. }
  70. // Notice we're mutating prevKeys as we go. This makes it
  71. // so that we can efficiently make incremental comparisons.
  72. prevKeys = prevKeys.filter(key => ! removes.includes(key))
  73. let lastKey = 'template'
  74. // This is the important part of the diffing algo. Identifying
  75. // which keys (future DOM elements) are new, which ones have
  76. // or haven't moved (noting where they moved to / from).
  77. for (let i = 0; i < keys.length; i++) {
  78. let key = keys[i]
  79. let prevIndex = prevKeys.indexOf(key)
  80. if (prevIndex === -1) {
  81. // New key found.
  82. prevKeys.splice(i, 0, key)
  83. adds.push([lastKey, i])
  84. } else if (prevIndex !== i) {
  85. // A key has moved.
  86. let keyInSpot = prevKeys.splice(i, 1)[0]
  87. let keyForSpot = prevKeys.splice(prevIndex - 1, 1)[0]
  88. prevKeys.splice(i, 0, keyForSpot)
  89. prevKeys.splice(prevIndex, 0, keyInSpot)
  90. moves.push([keyInSpot, keyForSpot])
  91. } else {
  92. // This key hasn't moved, but we'll still keep track
  93. // so that we can refresh it later on.
  94. sames.push(key)
  95. }
  96. lastKey = key
  97. }
  98. // Now that we've done the diffing work, we can apply the mutations
  99. // in batches for both separating types work and optimizing
  100. // for browser performance.
  101. // We'll remove all the nodes that need to be removed,
  102. // letting the mutation observer pick them up and
  103. // clean up any side effects they had.
  104. for (let i = 0; i < removes.length; i++) {
  105. let key = removes[i]
  106. lookup[key].remove()
  107. lookup[key] = null
  108. delete lookup[key]
  109. }
  110. // Here we'll move elements around, skipping
  111. // mutation observer triggers by using "mutateDom".
  112. for (let i = 0; i < moves.length; i++) {
  113. let [keyInSpot, keyForSpot] = moves[i]
  114. let elInSpot = lookup[keyInSpot]
  115. let elForSpot = lookup[keyForSpot]
  116. let marker = document.createElement('div')
  117. mutateDom(() => {
  118. elForSpot.after(marker)
  119. elInSpot.after(elForSpot)
  120. marker.before(elInSpot)
  121. marker.remove()
  122. })
  123. refreshScope(elForSpot, scopes[keys.indexOf(keyForSpot)])
  124. }
  125. // We can now create and add new elements.
  126. for (let i = 0; i < adds.length; i++) {
  127. let [lastKey, index] = adds[i]
  128. let lastEl = (lastKey === 'template') ? templateEl : lookup[lastKey]
  129. let scope = scopes[index]
  130. let key = keys[index]
  131. let clone = document.importNode(templateEl.content, true).firstElementChild
  132. addScopeToNode(clone, reactive(scope), templateEl)
  133. mutateDom(() => {
  134. lastEl.after(clone)
  135. initTree(clone)
  136. })
  137. if (typeof key === 'object') {
  138. warn('x-for key cannot be an object, it must be a string or an integer', templateEl)
  139. }
  140. lookup[key] = clone
  141. }
  142. // If an element hasn't changed, we still want to "refresh" the
  143. // data it depends on in case the data has changed in an
  144. // "unobservable" way.
  145. for (let i = 0; i < sames.length; i++) {
  146. refreshScope(lookup[sames[i]], scopes[keys.indexOf(sames[i])])
  147. }
  148. // Now we'll log the keys (and the order they're in) for comparing
  149. // against next time.
  150. templateEl._x_prevKeys = keys
  151. })
  152. }
  153. // This was taken from VueJS 2.* core. Thanks Vue!
  154. function parseForExpression(expression) {
  155. let forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
  156. let stripParensRE = /^\s*\(|\)\s*$/g
  157. let forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
  158. let inMatch = expression.match(forAliasRE)
  159. if (! inMatch) return
  160. let res = {}
  161. res.items = inMatch[2].trim()
  162. let item = inMatch[1].replace(stripParensRE, '').trim()
  163. let iteratorMatch = item.match(forIteratorRE)
  164. if (iteratorMatch) {
  165. res.item = item.replace(forIteratorRE, '').trim()
  166. res.index = iteratorMatch[1].trim()
  167. if (iteratorMatch[2]) {
  168. res.collection = iteratorMatch[2].trim()
  169. }
  170. } else {
  171. res.item = item
  172. }
  173. return res
  174. }
  175. function getIterationScopeVariables(iteratorNames, item, index, items) {
  176. // We must create a new object, so each iteration has a new scope
  177. let scopeVariables = {}
  178. // Support array destructuring ([foo, bar]).
  179. if (/^\[.*\]$/.test(iteratorNames.item) && Array.isArray(item)) {
  180. let names = iteratorNames.item.replace('[', '').replace(']', '').split(',').map(i => i.trim())
  181. names.forEach((name, i) => {
  182. scopeVariables[name] = item[i]
  183. })
  184. // Support object destructuring ({ foo: 'oof', bar: 'rab' }).
  185. } else if (/^\{.*\}$/.test(iteratorNames.item) && ! Array.isArray(item) && typeof item === 'object') {
  186. let names = iteratorNames.item.replace('{', '').replace('}', '').split(',').map(i => i.trim())
  187. names.forEach(name => {
  188. scopeVariables[name] = item[name]
  189. })
  190. } else {
  191. scopeVariables[iteratorNames.item] = item
  192. }
  193. if (iteratorNames.index) scopeVariables[iteratorNames.index] = index
  194. if (iteratorNames.collection) scopeVariables[iteratorNames.collection] = items
  195. return scopeVariables
  196. }
  197. function isNumeric(subject){
  198. return ! Array.isArray(subject) && ! isNaN(subject)
  199. }