preface

Long warning! The reading time is 5-10m. By focusing on code, you will learn how Vue is responsive and how it works.

v1-minimalist

Principle:

  • Object.defineproperty hijacks data transformation to update dom
  • Event listening, changing data

<body>
  <main>
    <input type="text" id="input"Value :<span id="span"></span></label>
  </main>
  <script src="./main.js"></script>
</body>
Copy the code
const obj = {};
const inputDom = document.querySelector('#input');
const spanDom = document.querySelector('#span');

Object.defineProperty(obj, 'txt', {
  get() {},
  set(newVal) {
    inputDom.value = newVal;
    spanDom.innerHTML = newVal;
  }
})

inputDom.addEventListener('input', (e) => {
  obj.txt = e.target.value
})

Copy the code

See the effect

v2-observer

Principle:

  • Listener Observer: Used to hijack data changes and notify publishers of Dep.
  • Publisher Dep: Responsible for collecting the subscriber Watcher and delivering the subscriber Watcher when notified by the listener Observer
  • Subscriber Watcher: Executes the corresponding function when a message from the publisher is received

  1. The publisher Dep
let uid = 0;

class Dep {
  constructor() { this.id = uid++; this.subs = []; } // Add subscriber addSub(sub) {this.subs.push(sub)} // notify subscriber of updatesnotify() {
    this.subs.forEach(sub => sub.update())
  }
  // 
  depend() {dep.target.adddep (this) // If new Dep, // When pointing to the currently active Watcher => execute get to facilitate the collection of dependencies (exclude unnecessary dependencies) dep. target = null;Copy the code
  1. The subscriber Watcher
import Dep from './Dep'class Watcher { constructor(vm, expOrFn, cb) { this.depIds = {}; // Store the subscriber's id this.vm = vm; // expOrFn = expOrFn; // Subscribe data key this.cb = cb; // Data update callback this.val = this.get(); // First instance, trigger get, collect dependencies}get() {// When the current subscriber (Watcher) reads the value of the subscribed data, notify the subscriber administrator to collect the current subscriber dep. target = this; // expOrFn; // Get const val = this.vm._data[this.exporfn]; Dep.target = null;return val
  }
  update() {
    this.run()
  }
  run () {
    const val = this.get();
    if(val ! == this.val || isObject(val)) { this.val = val; this.cb.call(this.vm, val); } } addDep(dep) {if(! this.depIds.hasOwnProperty(dep.id)) { dep.addSub(this) this.depIds[dep.id] = dep; }}}function isObject (obj) {
  returnobj ! == null && typeof obj ==='object'
}

export default Watcher;
Copy the code
  1. The listener Observer
class Observer {
  constructor(value) {
    this.value = value
    this.dep = new Dep()
    def(value, '__ob__', this)
    ifProtoAugment (value, arrayMethods) this.observearray (value)}else{// object, traversal properties, This.walk (value)}} walk(value) {object.keys (value).foreach (key => this.convert(key, value[key]))} convert(key, val) { defineReactive(this.value, key, val) } observeArray (items) {for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

functiondefineReactive(obj, key, val) { const dep = new Dep(); // Add data hijacking recursivelylet chlidOb = observe(val);
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true.get() {
      if (Dep.target) {
        dep.depend();
        if (chlidOb) {
          chlidOb.dep.depend()
          if (Array.isArray(val)) {
            dependArray(val)
          }
        }
      }
      return val
    },
    set(newVal) {
      if (newVal === val) return;
      val = newVal;
      chlidOb = observe(newVal);
      dep.notify()
    }
  })
}
Copy the code

DefineProperty can’t listen for array changes, which is why this.arr[index] = XXX didn’t update the page when we started using Vue. We had to use array methods (wrapped by VUE) to achieve the desired effect. I’m trying to figure out how to change Array.

  1. observeArray
import { def } from './util'

const arrayProto = Array.prototype
exportConst arrayMethods = object.create (arrayProto) const arrayStopatch = ['push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse' ]

methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]
  def(arrayMethods, method, functionmutator (... args) { const result = original.apply(this, args) const ob = this.__ob__let 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

The actual use

const vm = new Vue({
  data: {
    txt: ' ',
    arr: []
  },
});

inputDom.addEventListener('input', e => vm.txt = e.target.value);

buttonDom.addEventListener('click', e => vm.arr.push(1));

vm.$watch('txt', txt => spanDom.innerHTML = txt);
vm.$watch('arr', arr => span1Dom.innerHTML = arr);
Copy the code

See the effect

v3-template

V2 requires developers to manipulate the DOM, which is not MVVM at all. Follow the vue, implement a simple template compiler, processing templates; Binding data; Mount the dom; Achieve the effect of isolating DOM operations.

Principle:

  • Generate a DOM tree by passing the template string through innerHTML
  • Traverse dom nodes, parse instructions (V-if/V-for /…) , bind data ({{… }}), mount the update function

  1. parser
export default function parseHTML(template) {
  const box = document.createElement('div')
  box.innerHTML = template
  const fragment = nodeToFragment(box);
  return fragment
}
export function nodeToFragment(el) {
  const fragment = document.createDocumentFragment();
  let child = el.firstChild;
  while (child) {
    fragment.appendChild(child);
    child = el.firstChild
  }
  return fragment;
}

Copy the code
  1. patch
export default function patch(el, vm) {
  const childNodes = el.childNodes;
  [].slice.call(childNodes).forEach(function(node) {
      const text = node.textContent;

      if(node.nodeType == 1) {// Element node patchElement(node, vm); }else if(node.nodeType == 3) {// patchText(node, vm, text); }if(node.childNodes && node.childNodes.length) { patch(node, vm); }});returnel } <! --patchElement-->export default functionpatchElement(node, vm) { const nodeAttrs = node.attributes; const nodeAttrsArr = Array.from(nodeAttrs) nodeAttrsArr.forEach((attr) => { const { name, value } = attr; // Default directiveif (dirRE.test(name)) {
      if (bindRE.test(name)) {  // v-bind
        const dir = name.replace(bindRE, ' ')
        handleBind(node, vm, value, dir)
      } else if (modelRE.test(name)) {  // v-model
        const dir = name.replace(modelRE, ' ')
        handleModel(node, vm, value, dir)
      } else if (onRE.test(name)) {  // v-on/@
        const dir = name.replace(onRE, ' ')
        handleEvent(node, vm, value, dir)
      } else if (ifArr.includes(name)) {  // v-if
        handleIf(node, vm, value, name)
      } else if (forRE.test(name)) { // v-for handleFor(node, vm, value) } node.removeAttribute(name); }})returnnode }; <! --patchText--> const defaultTagRE = /\{\{(.*)\}\}/export default function patchText(node, vm, text) {
  if(defaultTagRE.test(text)) { const exp = defaultTagRE.exec(text)[1] const initText = vm[exp]; updateText(node, initText); new Watcher(vm, exp, (value) => updateText(node, value)); }}function updateText(node, value) {
  node.textContent = isUndef(value) ? ' ' : value;
}

Copy the code
  1. Directives (for example)
<! --v-bind-->export function handleBind (node, vm, exp, dir) {
  const val = vm[exp];
  updateAttr(node, val);
  new Watcher(vm, exp, (value) => updateAttr(node, value));
}

const updateAttr = (node, attr, value) => node.setAttribute(attr, isUndef(value) ? ' ': value); <! --v-model-->export function handleModel (node, vm, exp, dir) {
  let val = vm[exp];
  updateModel(node, val);
  new Watcher(vm, exp, (value) => updateModel(node, value));
  handleEvent(node, vm, (e) => {
    const newValue = e.target.value;
    if (val === newValue) return;
    vm[exp] = newValue;
    val = newValue;
  }, 'input')}export function handleEvent (node, vm, exp, dir) {
  const eventType = dir;
  const cb = isFun(exp) ? exp : vm[exp].bind(vm);
  if (eventType && cb) {
    node.addEventListener(eventType, e => cb(e), false);
  }
}

const updateModel = (node, value) => node.value = isUndef(value) ? ' ': value; <! --v-for-->export function handleFor (node, vm, exp) {
  const inMatch = exp.match(forAliasRE)
  if (!inMatch) return;
  exp = inMatch[2].trim();
  const alias = inMatch[1].trim();
  const val = vm[exp];
  const oldIndex = getIndex(node);
  const parentNode = node.parentNode;
  parentNode.removeChild(node);
  node.removeAttribute('v-for');
  const templateNode = node.cloneNode(true);
  appendForNode(parentNode, templateNode, val, alias, oldIndex);
  new Watcher(vm, exp, (value) => appendForNode(parentNode, templateNode, val, alias, oldIndex));
}

function appendForNode(parentNode, node, arr, alias, oldIndex) {
  removeOldNode(parentNode, oldIndex)
  for (const key in arr) {
    const templateNode = node.cloneNode(true)
    const patchNode = patch(templateNode, {[alias]: arr[key]})
    patchNode.setAttribute('data-for'.true)
    parentNode.appendChild(patchNode)
  }
}
Copy the code

Now, let’s try it out with a template

let vm = new Vue({
  el: '#app',
  template: 
  `<div>
    <input v-model="txt" type="text"/>
    <input @input="input" type="text"/ > < br / > < label > value: < span > {{TXT}} < / span > < / label > < br / > < button @ click ="addArr"> array +1</button> <br /> <label"item in arr">{{item}}</span></label>
    <br />
    <label v-if="txt"> Yes :<span>{{TXT}}</span></label> <label V-else ="txt"> No </label> </div> ', data: {TXT:' ',
    arr: [1, 2, 3]
  },
  methods: {
    input(e) {
      const newValue = e.target.value;
      if (this.txt === newValue) return;
      this.txt = newValue;
    },
    addArr() {
      this.arr.push(this.arr.length + 1)
    }
  }
});
Copy the code

v4-vdom

As a consumer frame rather than a toy (hehe! Still a toy…) Of course, we want to ensure that the development and maintenance at the same time, our performance is acceptable.

Obviously, updating the DOM frequently because of data changes is not what we want. The solution vUE gives is a VNode (object describing dom)

Principle:

  • Parse: Compiles a template to an AST
  • Generate: concatenate Function string according to AST, construct render Function with new Function (borrow with to extend scope)

Ex. :

  • Render generates a virtual DOM tree using Vnode’s create function
  • Diff algorithm is used to compare the updated DOM operations and perform the update. The DIFF algorithm of VUE refers to SanbbDOM. If you want to know the development history of DIFF algorithm, you can refer to the evolution of part of DIFF algorithm

  1. The parse code is a bit trivial, so you can look at the source code directly
  2. generate
export function generate (ast) {
  const code = ast ? genElement(ast) : '_c("div")'
  return {
    render: `with(this){return ${code}} `}}export function genElement (el) {
  if(el.for && ! el.forProcessed) {return genFor(el)
  } else if(el.if && ! el.ifProcessed) {return genIf(el)
  } else {
    let code
    let data
    if(! el.plain) { data = genData(el) } const children = genChildren(el,true)
    code = `_c('${el.tag}'${
      data ? `,${data}` : ' ' // data
    }${
      children ? `,${children}` : ' ' // children
    })`
    return code
  }
}
....
Copy the code
  1. The rough rules of patch are as follows:
    1. Only compare nodes of the same level (reduce complexity)
    2. Double-end comparison to find nodes with the same end position (find the update route with the least number of operations)
    3. If the same node is not found after the two-end comparison, the search is traversed
    4. Check whether the two nodes are the same (sameVnode: A.key === B.key && A.teag === B.teag). Otherwise, delete the old node and create a new one. If yes, go to 4
    5. Text nodes replace text, and element nodes compare child nodes (recursively)
function updateChildren (parentElm, oldCh, newCh) {
  let oldStartIdx = 0
  let newStartIdx = 0
  let oldEndIdx = oldCh.length - 1
  let oldStartVnode = oldCh[0]
  let oldEndVnode = oldCh[oldEndIdx]
  let newEndIdx = newCh.length - 1
  let newStartVnode = newCh[0]
  let newEndVnode = newCh[newEndIdx]
  let oldKeyToIdx, idxInOld, vnodeToMove, refElm

  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if (isUndef(oldStartVnode)) {
      oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
    } else if (isUndef(oldEndVnode)) {
      oldEndVnode = oldCh[--oldEndIdx]
    } else if(sameVnode(oldStartVnode, newStartVnode)) {// oldStart == newStart Updates patchVnode(oldStartVnode, newStartVnode) oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] }else if(sameVnode(oldEndVnode, newEndVnode)) {// oldEnd == newEnd patchVnode(oldEndVnode, newEndVnode) oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] }else if(sameVnode(oldStartVnode, newEndVnode)) {// oldStart == newEnd Update node newEndVnode) nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)) oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] }else if(sameVnode(oldEndVnode, newStartVnode)) {// oldStart == newEnd Update node newStartVnode) nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] }else {
      if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
      idxInOld = isDef(newStartVnode.key)
        ? oldKeyToIdx[newStartVnode.key]
        : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
      if (isUndef(idxInOld)) { // New element
        createElm(newStartVnode, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
      } else {
        vnodeToMove = oldCh[idxInOld]
        if (sameVnode(vnodeToMove, newStartVnode)) {
          patchVnode(vnodeToMove, newStartVnode)
          oldCh[idxInOld] = undefined
          nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
        } else{// Same key, but different element. CreateElm (newStartVnode, parentElm, oldStartvNode. elm,false, newCh, newStartIdx)
        }
      }
      newStartVnode = newCh[++newStartIdx]
    }
  }
  if(oldStartIdx > oldEndIdx) {refElm = isUndef(newCh[newEndIdx + 1])? null : newCh[newEndIdx + 1].elm addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx) }else if(newStartIdx > newEndIdx) {removeVnodes(oldCh, oldStartIdx, oldEndIdx)}}Copy the code

Take a look at the breakdown action:

Judging from sameVnode, it is not difficult to see that in the scenario of v-for looping out the list, setting the key on the element directly instructs diff whether to reuse DOM.

Type on the board and point out two problems that we had writing

  1. Use index as the key. In both cases, the DOM is reused. Because index === index (undefind == undefind);
  2. When adding, deleting, or changing the order of a list, it is recommended to set a unique ID as a key to maximize diff guidance and avoid incorrect rendering.

This step completes the basic manipulation of vUE, leaving the extension component/filter/mixin/ lifecycle features to be broken down. The above code is mainly to describe the vUE operation process, part of the reference vUE source, but lost a lot of details, interested students can refer to vUE source analysis.

In fact, the virtual DOM is about much more than improving performance. Once we have rules that describe the UI, vUE alone, independent of the normal host environment, can be a browser, weeX, or Node running SSR; In the larger context, this makes it possible for native cross-ends, such as RN; Of course, there are also cross-platform implementation from the compile stage, such as Taro/ Uniapp.

For the next release, see vue3. X to implement some new features.

A chat Vue3 +

Pain points for data hijacking

As mentioned earlier, Vue2. X uses defineProperty to hijack data. There are two problems with this approach.

First, the traversal recursion defines OB one by one when initialization is required. Second, it is impossible to hijack the array changes, not that there is no scheme to hijack the array, based on performance considerations, Vue adopted the transformation of the array method;Copy the code

Vue3.0 uses a new hijacking solution Proxy to solve the above problems once and for all. However, in terms of the current domestic environment, there are still a large number of users of low version OF IE, and the compatible version will still use the 2.x mechanism

The process of logic reuse

  1. mixins

Problems solved by ✅ :

Copy any component characteristics (properties and methods) to the desired component for reuseCopy the code

Problems caused by ❌ :

When multiple mixins work together, problems arise with unclear data sources and possible naming conflictsCopy the code
  1. slot-scope

Problems solved by ✅ :

Let component common functions be encapsulated, and different logic distributed through slotsCopy the code

Problems caused by ❌ :

When multiple components are nested, it is not clear which component provides which variable in the template. Additional instance components are required, resulting in additional performance overheadCopy the code
  1. HOC

Problems solved by ✅ :

With the idea of layering, incoming parameters and methods can be processed and distributedCopy the code

Problems caused by ❌ :

Compared with React, Vue HOC is particularly awkward to use. Because the original parent-child component relationship is split, there are properties and methods and real ref passing problems, such as v-models, that need to be handled manually by higher-order components. Similar to slot-scope, there is a performance overhead due to the need for additional instance componentsCopy the code
  1. Function-based API

✅ Solved problems (cases)

From the official case, there are no side effects caused by the above scheme. Whether or not the function-based API will result in a lot of spaghetti code, as the community seems to think, is something that needs to be seen in practice.Copy the code

About the next generation

The following content, purely personal YY, do not like light spray.

For the next generation, React has already pointed in a small direction –Fiber. And let’s not talk about its emergence will bring revolutionary performance improvement for the front end like VDOM, just the idea of cyclic task scheduling is very compatible with JS development ideas, Vue will draw on temporarily is not clear, but at least there will be suitable for Vue scheme.

Doing more articles at compile time, and doing more between the developer and the machine, on the one hand allows developers to focus on logic rather than code organization; On the other hand, improving runtime efficiency, taking advantage of the current hot example of WebAssembly, certainly compilers into code that is easier for machines to understand and execute, will allow the framework to write more judgment to address issues such as adaptation and the difficulty of debugging online. Proper separation of compiler and Runtime code is also an issue that the framework must consider.

Then there is Service Worker. At present, PWA is really widely used. I believe that with the further promotion of Google (Apple will still obstruct it), it will also be applied in various frameworks as a standard, such as putting Diff into WebWorker. This is much more interesting than the idea of a small program with two threads, but I still respect the role of a small program as a platform. But each small program interface and quality is different, there is no standard, to wait for small program consumers -JD continue to explore…

reference

Vue2.6.10 source

Part about the evolution of diff algorithm

Vue compiles comparison

The React Fiber architecture