v-for
Following on from the previous articles in this series, today’s goal is to learn about the v-for directive, which is used flexibly in Petite-Vue.
Know the grammar
- . of…
<ul>
<li v-for="item of list" :key="item.id">
<input v-model="item.text" />
</li>
</ul>
Copy the code
- . in…
<ul>
<li v-for="item in list" :key="item.id">
<input v-model="item.text" />
</li>
</ul>
Copy the code
- ({… }, index) in …
<ul>
<li v-for="({ id, text }, index) in list" :key="id">
<div>{{ index }} {{ { id, text } }}</div>
</li>
</ul>
Copy the code
The first is not too different from the second. The third uses a deconstructed assignment; In addition, the target data of v-for can be array, object, and number. In addition, the target data of V-for can be array, object, and number. of…) I want it to look like this:
const scope = { list: [...] };
function for_dir(ele) {
const valueExp = 'item';
const sourceExp = 'list';
const keyExp = 'item.id';
for (let i = 0; i < scope[sourceExp].length; i++) {
createForItem(ele, { [valueExp]: scope[sourceExp][i] }, i);
}
}
function createForItem(ele, source, index) {
// ...
}
Copy the code
First, I need to parse out the v-for instruction value, decompose the key information such as valueExp and sourceExp, and then create each sub-item through a loop. Here, I build a relatively isolated context by wrapping a data object corresponding to each sub-item. Of course in Petite-Vue it’s implemented by childContext. Whether in VUE or React, rendering for lists is an important means of performance optimization. Therefore, when updating, we can reduce DOM operations by comparing keys. Later, we will introduce how petite-Vue achieves performance optimization.
Instruction parsing
The value of the V-for instruction can actually be divided into two parts, in/of as a separator, the first part is the data mapping relationship required by the context of the child node, the second part is the mapping relationship of the master data source, so first through the regular separation of the two parts.
const forAliasRE = /([\s\S]*?) \s+(? :in|of)\s+([\s\S]*)/;Copy the code
Corresponding we focus on two parts of the two groups before and after two parts instruction value, here said a little bit about the the regular expression, matching in | v – for a fixed grammar, (? : in |) said no packet matches only specific structure, so the final match result only ([\ s \ s] *?) And ([\s\ s]*), \s\ s will match any character, *? ValueExp = in/of = in/of = in/of = in/of = of
- {id, text} [, index, objIndex]
- [id, text] [, index, objIndex]
- Item [, index, objIndex]
For the first two formats, it is necessary to use}] as a separator line, the first part is structured assignment, followed by index, structured assignment object may be array, here to determine, and then inside the assigned identifier extracted into the array; The following indexes include index and objIndex as optional items.
input:
`({ id, text }, index) of list`
output:
let sourceExp = 'list';
let valueExp = '{ id, text }';
let isArrayDestructure = false;
let destructureBindings = ['id', 'text'];
Copy the code
So far completed the command syntax parsing work, the code is not posted, mainly that a few more complex regular expressions, we can view here.
Loop to create
As mentioned earlier, you need a separate context for each child item (childContext), and childContext holds the data object, and how does childContext relate to the current context? Childcontext. scope__proto__ points to context.scope to ensure that state access is valid and ordered.
- Get data source according to sourceExp, determine the type of data source;
if (Array.isArray(source)) { for (let i = 0; i < source.length; i++) { ... } } else if (typeof source === 'number') { for (let i = 0; i < source; i++) { ... } } else if (isObject(source)) { for (let key in source) { ... }}Copy the code
- Create childContext
const parentScope = ctx.scope; const mergedScope = Object.create(parentScope); / / build mergedScope and parentContext. The scope of the contact Object. The prototype defineProperties (mergedScope, Object getOwnPropertyDescriptors (data)); Const reactiveProxy = reactive(new Proxy(mergedScope)) {// mergedScope wraps a response set(target, key, val, receiver) { // when setting a property that doesn't exist on current scope, // do not create it on the current scope and fallback to parent scope. if (receiver === reactiveProxy && ! target.hasOwnProperty(key)) { return Reflect.set(parentScope, key, val); } return Reflect.set(target, key, val, receiver); }})); return { ... ctx, scope: reactiveProxy, }Copy the code
- Set up the mapping between indexes and keys
Index can be obtained in the first step of the loop. If the key is not set, the default value is Index. After the index and key are ready, save them in Map for later update and optimization.
- Creating a DOM Node
The introduction of Block in the source code for dynamic node management, responsible for saving the node template and parent node, as well as insert, delete and other operations, relatively simple, not to say, specific can view the source;
Update the optimization
During the update, the key point is to compare the differences of nodes rendered before and after, which are mainly divided into new nodes, deleted nodes and updated nodes. First, assume that the data before and after the update are as follows:
---mount--- blocks = [b1, b2, b3]; keyToIndexMap = { b1->0, b2->1, b3->2 }; / / key < - > the index mapping relationship - update - blocks = (b1, b2, b3); PrevKeyToIndexMap = {b1->0, b2->1, b3->2}; NextBlocks = []; KeyToIndexMap = {b1->0, b3->1, b4->2}; // This update creates a mapping object based on the status objectCopy the code
If the key of each block that blocks is not included in the keyToIndexMap, then the block that blocks is mounted to should be deleted, as in b2:
for (let i = 0; i < blocks.length; i++) {
if (!keyToIndexMap.has(blocks[i].key)) {
blocks[i].remove();
}
}
Copy the code
PrevKeyToIndexMap (prevKeyToIndexMap, prevKeyToIndexMap, prevKeyToIndexMap, prevKeyToIndexMap, prevKeyToIndexMap, prevKeyToIndexMap, prevKeyToIndexMap, prevKeyToIndexMap, prevKeyToIndexMap, prevKeyToIndexMap, prevKeyToIndexMap, prevKeyToIndexMap, prevKeyToIndexMap, prevKeyToIndexMap, prevKeyToIndexMap, prevKeyToIndexMap, prevKeyToIndexMap, prevKeyToIndexMap) It is possible to insert the middle, so you need a positioning reference point, this reference point must be compared before and after two times to determine, and it must be adjacent to the newly inserted node, just like insertBefore, the specific algorithm will be talked about later, here is the last case — update. Update may change the position, which may be UI change, by comparing the key of this block with the prevKeyToIndexMap and keyToIndexMap indexes. UI change is a change in the state value, which can be merged by scope. Analysis to here, the process should be more clear, there is a problem is said in front of the reference point, next in the code to find the answer;
const nextBlocks = []; let i = childCtxs.length; While (I --) {const childCtx = childCtxs[I]; const childCtxs = childCtxs[I]; // Current context const oldIndex = prevKeyToIndexmap.get (childctx.key); // The context key is the same as the corresponding block key const next = childCtxs[I + 1]; // const nextBlockOldIndex = next && prevKeyToIndexMap.get(next.key); NextBlock = nextBlockOldIndex == null? undefined : blocks[nextBlockOldIndex]; If (oldIndex == null) {// Key does not exist on prevKeyToIndexMap, // new nextBlock [I] = mountBlock(childCtx, nextBlock? nextBlock.el : anchor ); } else { // update const block = (nextBlocks[i] = blocks[oldIndex]); Assign (block.ctx.scope, childctx.scope); // Update the existing block Object.assign(block.ctx.scope, childctx.scope); // scope merges to ensure UI updates if (oldIndex! // Moved if (blocks[oldIndex + 1]! == nextBlock) { block.insert(parent, nextBlock ? nextBlock.el : anchor); } } } } blocks = nextBlocks;Copy the code
According to the above code analysis, nextBlock is a very important registration point to determine the position of new insertion and update. So far, the analysis has completed the implementation of THE V-for instruction. Click here for the complete code of the specific implementation.