preface

I’m going to write a collection of 100 questions about Vue, just kidding, I don’t know how many there will be, add them when I see them, keep updating…

How does Vue listen for array changes?

Answer: Let’s start with a simple example

<template>
  <div id="app">
    <ul>
      <li v-for="(item, i) in list" :key="i">{{ item }}</li>
    </ul>
    <h1 @click="add">add</h1>
  </div>
</template>

<script>
export default {
  name: 'App'.data: function () {
    return {
      list: [1.2.3.4.5].list1: [{name: ' '}, 2 ,3 ,4 ,5]}},methods: {
    add () {
      this.num[1] = 2 // This modification is invalid and can only be triggered by methods defined in methodsToPatch.
      // var num = [1, 2, 3, 4, 5] 
      // num. Push (6); // Augment augment function
      // function protoAugment (target, SRC) {target = [1, 2, 3, 4, 5] This target is augment from data genus
      // We don't add squares to all arrays
      // method, but an array defined in data.
      //
      Name = 'Wayag' // This.list1[0].name = 'Wayag' // This can be triggered because each item in the array is called by observe()
                                   // Call observe({name: 'Wayag'}) and change it as before
                                   // Reactive data
      // target.__proto__ = src;
      // }}}}</script>
Copy the code
var methodsToPatch = [
  'push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse'
];
methodsToPatch.forEach(function (method) {
  // cache original method
  var original = arrayProto[method];
  def(arrayMethods, method, function mutator () {
    var args = [], len = arguments.length;
    while ( len-- ) args[ len ] = arguments[ len ];

    var result = original.apply(this, args);
    // This is the array list that data is currently listening on
    var ob = this.__ob__;
    var inserted;
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args;
        break
      case 'splice':
        inserted = args.slice(2);
        break
    }
    if (inserted) { ob.observeArray(inserted); }
    ob.dep.notify();
    return result
  });
});

Copy the code
var Observer = function Observer (value) {
  this.value = value;
  this.dep = new Dep();
  this.vmCount = 0;
  // Add an __ob__ attribute to the current array element, which is used to retrieve dep (this.__ob__.dep.notify()) when using array methods.
  def(value, '__ob__'.this);
  // Value is an array
  if (Array.isArray(value)) {
    if (hasProto) {
      protoAugment(value, arrayMethods);
    } else {
      copyAugment(value, arrayMethods, arrayKeys);
    }
    this.observeArray(value);
  } else {
    this.walk(value); }};Copy the code

InitData = {list; [1, 2, 3, 4, 5]} define active$$1 var childOb =! Shallow && observe(val) : assign array [1, 2, 3, 4, 5] to observe(new Observer([1, 2, 3, 4, 5]) [1, 2, 3, 4, 5].__ob__ = new Observer([1, 2, 3, 4, 5]). If a subsequent execution of the array method intercepted here triggers [1, 2, 3, 4, 5].__ob__.dep.notify(), then when is the value assigned to [1, 2, 3, 4, 5].__ob__.dep? Object.defineproperty get is triggered when accessing vm.list. Childob.dep.depend () calls the watcher that is currently accessing vm.list and adds the watcher to childob.dep. Now you know why the New Observer() instance collects the DEP. In fact, you can choose to manually invoke the subscription watcher for the property collection in an array or other place where the set function of Object.defineProperty cannot be triggered.

function defineReactive$$1 (obj, key, val, customSetter, shallow) {
  var dep = new Dep();
  var property = Object.getOwnPropertyDescriptor(obj, key);
  if (property && property.configurable === false) {
    return
  }
  // cater for pre-defined getter/setters
  var getter = property && property.get;
  var setter = property && property.set;
  if((! getter || setter) &&arguments.length === 2) {
    val = obj[key];
  }
  varchildOb = ! shallow && observe(val);Object.defineProperty(obj, key, {
    enumerable: true.configurable: true.get: function reactiveGetter () {
      var value = getter ? getter.call(obj) : val;
      if (Dep.target) {
        dep.depend();
        if (childOb) {
          childOb.dep.depend();
          if (Array.isArray(value)) { dependArray(value); }}}return value
    },
    set: function reactiveSetter (newVal) {
      debugger
      var value = getter ? getter.call(obj) : val;
      /* eslint-disable no-self-compare */
      if(newVal === value || (newVal ! == newVal && value ! == value)) {return
      }
      /* eslint-enable no-self-compare */
      if(process.env.NODE_ENV ! = ='production' && customSetter) {
        customSetter();
      }
      // #7981: for accessor properties without setter
      if(getter && ! setter) {return }
      if (setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      childOb = !shallow && observe(newVal);
      dep.notify();
    }
  });
}
Copy the code

2 why must data be a function in a VUE component?

Note in advance: The Vuecomponent constructor is also stored in options._ctor. If the Vuecomponent constructor is used repeatedly, the Vuecomponent constructor will be stored in options._ctor. Extend is returned by cachedCtors[SuperId=this.cid] on the same page (provided it is the same Vue constructor), but not on sub.extend. Because cid = 1, sub.cid = CID ++), both return the child component constructor saved by options._ctor.

Answer: Look directly at the source code

// Initialize data
function initData (vm) {
  var data = vm.$options.data;
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {};
    
    ...省略
}
Copy the code

When data is a function, vm._data = data.call(VM, VM). If the VM component is called multiple times within a page (not necessarily within the same page, but wherever the same component is called repeatedly), it returns a new object each time. But if data is an object, as shown in the following code, we know that in our knowledge of stereotypes and stereotype chains, when two objects point to the stereotype of the constructor and modify the attributes of the stereotype, both objects access the attributes of the same stereotype object.

function A() {
}
A.$options={
  data: {
    num: 1}}let a1 = new A()
let a2 = new A()
a1 = Object.create(A.$options)
a2 = Object.create(A.$options)
a1.data.num++
console.log(a1.data.num) / / 2
console.log(a2.data.num) / / 2
Copy the code

How does vue source code work? We all know that vue.prototype. _init is called when a child component is initialized, and initInternalComponent is called when the child component is initialized.

function initInternalComponent (vm, options) {
  $options is derived from vm. Constructive. options, regardless of which page the subcomponent is currently invoked on. $options.__proto__ = vm.constructive. options, pointing to the same constructor prototype '
  var opts = vm.$options = Object.create(vm.constructor.options);
  // doing this because it's faster than dynamic enumeration.
  var parentVnode = options._parentVnode;
  opts.parent = options.parent;
  opts._parentVnode = parentVnode;

  var vnodeComponentOptions = parentVnode.componentOptions;
  opts.propsData = vnodeComponentOptions.propsData;
  opts._parentListeners = vnodeComponentOptions.listeners;
  opts._renderChildren = vnodeComponentOptions.children;
  opts._componentTag = vnodeComponentOptions.tag;
  if(options.render) { opts.render = options.render; opts.staticRenderFns = options.staticRenderFns; }}Copy the code

3 Why not use index as key in Vue?

Answer: Before we can answer the question, we need to talk about the component update process, starting with a simple example:

<ul>
  <li>1</li>
  <li>2</li>
</ul>

Copy the code

The template will be converted to the following vNode

{
  tag: 'ul'.children: [{tag: 'li'.children: [{vnode: { text: '1'}}}, {tag: 'li'.children: [{vnode: { text: '2'}}}}],]Copy the code

After obtaining the Vnode, it is the process of update (patch), converting the Vnode into a real DOM and mounting it to the node. However, before converting, we need to judge the DIff algorithm of the Vnode to see whether any node can be reused. This explains why it’s best not to use index as a key and what scenarios can cause errors.

Without further ado, let’s take a look at the Patch function

function patch (oldVnode, vnode, hydrating, removeOnly) {
    var isInitialPatch = false;
    var insertedVnodeQueue = [];
    if (isUndef(oldVnode)) { // oldVnode does not exist when the child component is initialized, so the update process should go else
      isInitialPatch = true;
      createElm(vnode, insertedVnodeQueue);
    } else {
      
      
'(oldVnode is in index.html
// div#app) or the component update process will enter here var isRealElement = isDef(oldVnode.nodeType); if(! isRealElement && sameVnode(oldVnode, vnode)) {// This is where the root VNode of the VM instance (child component) enters patchVnode(oldVnode, vnode, insertedVnodeQueue, null.null, removeOnly); } else { // Here is the patch process of the outermost Vue instance / /... slightly } return vnode.elm } Copy the code

Next, enter the patchVnode process of the child component

function patchVnode (oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly) {
    if (oldVnode === vnode) {
      return
    }
    // vnode.elm assigned to oldvNode. elm
    var elm = vnode.elm = oldVnode.elm;
    var i;
    var data = vnode.data;
    // Data. hook is the child component of vnode, ordinary nodes only attrs, etc.
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
      i(oldVnode, vnode);
    }
    // Patch is a comparison between oldVnode and vnode's child nodes
    var oldCh = oldVnode.children;  // Get the child vnode of oldVnode
    var ch = vnode.children;        // Get the child node of vNode
    // Add the isPatchable function
    ` `'js function isPatchable (vnode) {// vnode.componentInstance {// vnode.componentInstance} Root vnodes up to vm._vnode are common nodes, not component nodes. while (vnode.componentInstance) { vnode = vnode.componentInstance._vnode; } // The text node has no tag attribute return isDef(vnode.tag)} '` `
    Vnodes are either child components or normal nodes (except text nodes).
    if (isDef(data) && isPatchable(vnode)) {
      Cbs. update is an array. If a node has class or other attributes, those attributes will be updated when the node is updated.
      for (i = 0; i < cbs.update.length; ++i) { cbs.update[i](oldVnode, vnode); }
      I.hook. update, which is used to update the attrs attribute
      if(isDef(i = data.hook) && isDef(i = i.update)) {i(oldVnode, vnode); }}// If the new vnode is not a text node, compare the vNodes of the old and new nodes with the diff algorithm in updateChildren.
    if (isUndef(vnode.text)) {
      // Both old and new nodes have children
      if (isDef(oldCh) && isDef(ch)) {
        // diff
        if (oldCh !== ch) { updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly); }     
      } else if (isDef(ch)) {
      // If only the new vNode has children
        if(process.env.NODE_ENV ! = ='production') {
          checkDuplicateKeys(ch);
        }
        // The old vnode was a text node. First empty the text node, and then insert the child ch of the new vnode.
        if (isDef(oldVnode.text)) { nodeOps.setTextContent(elm, ' '); }
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
      } else if (isDef(oldCh)) {
        // If only child nodes of the old node exist, delete the child nodes of the old node.
        removeVnodes(oldCh, 0, oldCh.length - 1);
      } else if (isDef(oldVnode.text)) {
        // There are no nodes and the old node is a text node.
        nodeOps.setTextContent(elm, ' '); }}else if(oldVnode.text ! == vnode.text) {// The old node and the new node are both text nodes, and the text is not equal.nodeOps.setTextContent(elm, vnode.text); }}Copy the code

When the children of both the old and new nodes exist and are not equal, the updateChildren function (diff) is called.

  function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    var oldStartIdx = 0; // The old child starts the subscript
    var newStartIdx = 0; // The new child starts the subscript
    var oldEndIdx = oldCh.length - 1; // End subscript of the old child node
    var oldStartVnode = oldCh[0]; // The first node of the old child node
    var oldEndVnode = oldCh[oldEndIdx]; // Last node of the old child node
    var newEndIdx = newCh.length - 1; // End subscript of the new child node
    var newStartVnode = newCh[0]; // First node of the new child node
    var newEndVnode = newCh[newEndIdx]; // New child node tail node
    var oldKeyToIdx, idxInOld, vnodeToMove, refElm;
    var canMove = true;

    if(process.env.NODE_ENV ! = ='production') {
      // Remember the duplicate key in this loop?
      `("Duplicate keys detected: '" + key + "'. This may cause an update error.")`
      checkDuplicateKeys(newCh);
    }
    // Check that the start subscript cannot be greater than the end subscript
    // Redundant node insertion operations.
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (isUndef(oldStartVnode)) {
        oldStartVnode = oldCh[++oldStartIdx];
      } else if (isUndef(oldEndVnode)) {
        oldEndVnode = oldCh[--oldEndIdx];
        
        // If the old first index node is the same as the new first index node
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
 
        // Diff updates the old header index node and the new header index node to achieve the effect of reuse nodes
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
         // The old header is indexed backwards
        oldStartVnode = oldCh[++oldStartIdx];
        // The new header is indexed backwards
        newStartVnode = newCh[++newStartIdx];
        
        // If the old tail index node is similar to the new one, it can be reused
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        // The old tail index node and the new tail index node are updated
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx);
        // Old tail index forward
        oldEndVnode = oldCh[--oldEndIdx];
        // New tail index forward
        newEndVnode = newCh[--newEndIdx];
      } else if (sameVnode(oldStartVnode, newEndVnode)) {
        // patch the old header index and the new tail index
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx);
        // The old vNode starts inserting into the real DOM, with the old head moving to the right and the new tail moving to the left
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm));
        // Place the first child of the old node one bit behind and the last child of the new node one bit ahead
        oldStartVnode = oldCh[++oldStartIdx];
        newEndVnode = newCh[--newEndIdx];
      } else if (sameVnode(oldEndVnode, newStartVnode)) {
        // Same as above, the old tail index and the new header also have similar possibility
        // Patch the old header index and the new tail index
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
        // The old vNode starts to insert into the real DOM, with the new head moving left and the old tail moving right
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
        oldEndVnode = oldCh[--oldEndIdx];
        newStartVnode = newCh[++newStartIdx];
      } else {
         // If none of the above criteria is true, then we need the key-index table for maximum reuse
         // If the key-index table of the old node does not exist, it is created
        if (isUndef(oldKeyToIdx)) { oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); }
        // Find the position of the new node in the old node group
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);
       If the new node does not exist in the old node, we create a new element, which we insert in front of the old first index node (createElm 4)
        if (isUndef(idxInOld)) { // New element
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
        } else {
          // If the old node has this new node
          vnodeToMove = oldCh[idxInOld];
          // Compare the new node with the new first index, if the type is the same, patch
          if (sameVnode(vnodeToMove, newStartVnode)) {
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
            // Then set the corresponding node in the old node group to undefined, which means that the node has been traversed, not traversed, otherwise there may be a repeat insert problem
            oldCh[idxInOld] = undefined;
            // If there is no group offset, insert it before the old first node
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm);
          } else {
           // Create a node with a different type and insert it before the old first index (createElm 4)
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx); }}// Move the new head back one bitnewStartVnode = newCh[++newStartIdx]; }}// If the old first index is greater than the old last index, the old node group has been traversed, and the remaining new vNodes are added to the position of the last new node
    if (oldStartIdx > oldEndIdx) {
      refElm = isUndef(newCh[newEndIdx + 1])?null : newCh[newEndIdx + 1].elm;
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
    } else if (newStartIdx > newEndIdx) { // If the new node group is traversed first, the remaining nodes in the old node group are not needed, so it is directly deletedremoveVnodes(oldCh, oldStartIdx, oldEndIdx); }}// sameVnode
 function sameVnode (a, b) {
  // select * from 'a' and 'b'; undefined === undefined = true.
  return (
    a.key === b.key && (
      (
        a.tag === b.tag &&
        a.isComment === b.isComment &&
        isDef(a.data) === isDef(b.data) &&
        sameInputType(a, b)
      ) || (
        isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}
Copy the code

Let’s look at the diff process

Having said diff, we now know why it is best not to use index as the key of a node:

1. In the scene of reverse order, there is actually a very simple operation to reuse the old child nodes. However, when index is used as the key value, even after the array is reversed, the index remains in order, which means that the old node is not used and diff is invalid.

2. Use index to delete an array, causing an error, as follows:

When we want to delete the first node, we will delete the last node. Why is this?

This is due to Vue’s mechanism for updating child components. If a component is introduced into a page, it will only see if some of the attributes you declared in the template passed to the child are updated. In this case, we delete the first data in the array, but render the VNode with the key starting at zero. During the process of patchVnode, the sameVnode function will determine that the two nodes with the key value of 0 of the old and new nodes are SamevNodes. Originally, it is intended to conduct patchVnode on its child nodes, but since the child nodes are component Vnodes, patchVnode does not compare component Vnodes. If it is a component, vNode will enter the prepatch function and call updateChildComponent to assign props[key] to update the child component. Because the parent component does not pass the changing parameter to the child component, it will not trigger the child component update. After continuous comparison of nodes, it was found that newCh and oldCh did not have the last node, so the tail node of the old node was deleted. Let’s insert the updateChildComponent function

    function updateChildComponent() {
      / / update the props
      if (propsData && vm.$options.props) {
        toggleObserving(false);
        var props = vm._props;
        var propKeys = vm.$options._propKeys || [];
        for (var i = 0; i < propKeys.length; i++) {
          var key = propKeys[i];
          var propOptions = vm.$options.props; // wtf flow?
          // Assign props[key], which calls object.defineProperty's set function, and then subcomponents
          // dep.nofity(), to update the child component.
          props[key] = validateProp(key, propOptions, propsData, vm);
        }
        toggleObserving(true);
        // keep a copy of raw propsDatavm.$options.propsData = propsData; }}Copy the code
<body>
  <div id="app">
    <ul>
      <li v-for="(value, index) in arr" :key="index">
        <test />
      </li>
    </ul>
    <button @click="handleDelete">delete</button>
  </div>
  </div>
</body>
<script>
  new Vue({
    name: "App".el: '#app'.data() {
      return {
        arr: [1.2.3]}; },methods: {
      handleDelete() {
        this.arr.splice(0.1); }},components: {
      test: {
        template: "<li>{{Math.random()}}</li>"}}})</script>
Copy the code

If you set the props parameter to the test component, and then a comparison between the old and new nodes shows that the parameters passed are changed, the reactive data defined before by the props component is triggered to update the child component, so the view changes.

/ / modify
<body>
  <div id="app">
    <ul>
      <li v-for="(value, index) in arr" :key="index">
        <test :i="value" />
      </li>
    </ul>
    <button @click="handleDelete">delete</button>
  </div>
  </div>
</body>
<script>
  new Vue({
    name: "App".el: '#app'.data() {
      return {
        arr: [1.2.3]}; },methods: {
      handleDelete() {
        this.arr.splice(0.1); }},components: {
      test: {
        template: "<li>{{Math.random()}}</li>".props: {
          i: {
            type: Number.default: 0}}}}})</script>
Copy the code

4 The difference between V-IF and V-show

Template -> ast (parse, parseHTML) -> render (generate, genElement)) will be compiled into a ternary expression in render.

The v-show command will be compiled and stored in vnode.data. When createElm generates the real DOM, the properties in vnode.data will be attached to the real DOM. El.style. display = ‘none’), the control style will hide the corresponding node if the condition is not met (display:none)

Usage scenarios: V-if is suitable for scenarios where conditions are rarely changed at runtime and do not need to be switched frequently

V-show is suitable for scenarios where conditions need to be switched very frequently

5 talk about vUE built-in instructions

  1. v-once:

    ProcessOnce adds the attribute isOnce to the element (the Ast tree) after the template is converted to the Ast tree: GenOnce -> genStatic _m() -> renderStatic

    Note: The element or component that defines it is rendered only once, including all child nodes of the element or component. After the first rendering, the element or component will not be re-rendered as the data changes and will be treated as static content

    function renderStatic (index, isInFor) {
      var cached = this._staticTrees || (this._staticTrees = []);
      var tree = cached[index];
      // Read cache (not in v-for, otherwise invalid)
      if(tree && ! isInFor) {return tree
      }
      // Cached into a cached object for subsequent reuse
      tree = cached[index] = this.$options.staticRenderFns[index].call(
        this._renderProxy,
        null.this // for render fns generated for functional component templates
      );
      markStatic(tree, ("__static__" + index), false);
      return tree
    }
Copy the code

Chestnut:

new Vue({
  template: `
      

{{ flag }}

'
.data() { return { flag: true}}})Copy the code

2.v-cloak

To be continued…