Decided to follow huang Yi teacher vue2 source course to learn the source of vue2, learning process, as far as possible output their income, improve learning efficiency, the level is limited, wrong words please correct ~

Clone the vue source code locally and switch to branch 2.6.

Introduction

Data drive is the core idea of VUE.

As data changes, JQuery updates the view by manipulating the DOM, while VUE updates the view automatically, decoupling the DOM from the data.

{{message}}
hello

Remember: analyze the source code, do the main task first, then the side task ~~~

What happened to New Vue

Find out what files are involved in implementing this functionality.

To find it from entry documents, Vue in SRC/core/instance/index of the definition of js:

function Vue(options) {
  if(! (this instanceof Vue)) {
    warn("Vue is a constructor and should be called with the `new` keyword");
  }
  // Execute this method
  this._init(options);
}

// These are all methods mounted on the prototype
initMixin(Vue);
stateMixin(Vue);
eventsMixin(Vue);
lifecycleMixin(Vue);
renderMixin(Vue);
Copy the code

There is one more small detail: the writing order, although this._init is in initMixin(Vue); . Before, but actually this._init is only executed when new, and before new, initMixin(Vue); . Already executed, so this._init can call methods on all prototypes.

InitMixin defines the _init method, so find init.js:

All sorts of things, but what is the code that makes {{message}} hello?

Tip: Add debugging

To find the answer to this problem, you need to create a demo, import a vue file, open the debugger with the code related to init, and find the code that {{message}} becomes Hello after execution.

I was a bit lazy on my part. I created a TML in the clone vUE library and introduced vUE:

<div id="app">
  <div>{{message}}</div>
</div>
<! -- change this to your own path -->
<script src="vue/dist/vue.js"></script>
<script>
  new Vue({
    el: "#app".data: {
      message: "hello",}});</script>
Copy the code

Then search the_init, and then run it in the browserz.html

Got it! $mount is the key to making {{message}} hello!

Why this.message

The method has been found. Here’s another thing. Why can properties defined in data be accessed directly by this.xx?

After using the debugger, this time take a closer look. The properties in this start very small, and then more and more:

Hey west! It soon becomes apparent that after initState(VM) is executed, this has a message attribute on top of it, which clearly does something about it.

Check it out in state.js:

/ / initState method
initData(vm);

// initData method, where you see that data is mounted on the 'this._data' property on the instance
data = vm._data = typeof data === "function" ? getData(data, vm) : data || {};
proxy(vm, `_data`, key);

// the proxy method is mapped so that when this. Xx is called, this._data.xx is called
Object.defineProperty(target, key, sharedPropertyDefinition);
Copy the code

Vue instance mount implementation

Mount this.$mount method.

To find the definition of a particular method, SRC searches globally under $mount

$mount is found in three places:

  • platforms/web/entry-runtime-with-compiler.js
  • platforms/web/runtime/index.js
  • platforms/weex/runtime/index.js

In fact, it is also very good to understand, said before vue three platforms web, WEEx, server, the last one certainly does not need, the first two, WEEX does not support template is a string or EL is a string mode, so weeX only one, and the Web has two

web/entry-runtime-with-compiler.js

First take a look at the web/entry – the runtime – with – compiler. Js

  • Cache original$mountWhen rewritten, is in the original$mountAdd function to
  • elFirst check whether incoming, and then unified byqueryTo obtaindom
  • Once you’ve got the DOM, let’s see if it’s okaybodyorhtmlIf so, warn and terminate
  • ifoptionsThere areRender functionI’m just going to go back to the original$mount; If not, willtemplateorelintoRender functionAnd then you call the original$mount
  • Focus onSo let’s see, here, how do I put ittemplateorelintoRender functionIt’s basically findingDOMstring
    • There is noRender functionThere are only two cases: yestemplateorel, will eventuallyDOMString assigned totemplate
    • prioritytemplate:
      • templateIs a string#At the beginning, get the element as id and return the element’sinnerHTML.
      • templateIf it is an element, return the value of the elementinnerHTML
      • Other cases are not supported
    • There is notemplateAnd seeel, to get elouterHTML, in the assignment totemplate
  • thenDOMString to usecompileToFunctionsProcessing intoRender function
// Cache the original, when rewritten, to add functionality on the original basis
const mount = Vue.prototype.$mount;

/ / rewrite
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
) :Component {
  // If we pass in an EL, we fetch the EL. Query returns the DOM element
  el = el && query(el);

  /* istanbul ignore if */
  // Can't be body, HTML
  if (el === document.body || el === document.documentElement) { process.env.NODE_ENV ! = ="production" &&
      warn(
        `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
      );
    return this;
  }

  / / cache the options
  const options = this.$options;
  // resolve template/el and convert to render function
  if(! options.render) {let template = options.template;
    if (template) {
      // If template is a string, only the beginning of the string is supported
      if (typeof template === "string") {
        if (template.charAt(0) = = ="#") {
          template = idToTemplate(template);
          /* istanbul ignore if */
          if(process.env.NODE_ENV ! = ="production" && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`.this); }}}else if (template.nodeType) {
        // element returns the content directlytemplate = template.innerHTML; }}else if (el) {
      // even if there is el, it is still assigned to template
      template = getOuterHTML(el);
    }
    if (template) {
      // Template is uniformly converted to the render function
      const{ render, staticRenderFns } = compileToFunctions( template, ); options.render = render; options.staticRenderFns = staticRenderFns; }}return mount.call(this, el, hydrating);
};

Vue.compile = compileToFunctions;

export default Vue;
Copy the code

Tip: Warn is only available in informal environments

This tip is easy to use in everyday development

const isProduction =
  process.env.NODE_ENV === "production"(! isProduction) && warn("some warn");
Copy the code

Tip: Determine if the element is body or HTML

This is also easy

const isBodyOrHtml = el === document.body || el === document.documentElement;
Copy the code

Tip: Deal with the exception first

Always deal with the exception first and the relatively normal last

if(someError){
  return. }return.Copy the code

Tip: EL can be a string or an element

This is easy to use once it involves getting elements

function query(el) {
  if (typeof el === "string") {
    const selected = document.querySelector(el);
    if(! selected) { process.env.NODE_ENV ! = ="production" &&
        warn("Cannot find element: " + el);
      return document.createElement("div");
    }
    return selected;
  }
  return el;
}
Copy the code

Elements can also be determined using xx.nodeType

Tip: Add new functionality to existing functions

If you want to add new functions to existing functions, you can cache the original method first and then rewrite it. The advantage of this is that there is no need to add or delete code to the original function, and different conditions may require different rewrite, higher flexibility:

let print = function (name) {
  console.log(name);
};
print("hello");

// If you want to add new features
let oldPrint = print;
print = function (name) {
  console.log("Want to add additional features");
  oldPrint.call(this, name);
};
print("hello");
Copy the code

$mount: platforms/web/runtime/index, js

Above $mount, change is platforms/web/runtime/index of the definition of js $mount, note that the runtime here is definitely needed, so to achieve this. Compiler depends, so the other file is implemented.

So let’s go ahead and look at the definition here

import { mountComponent } from "core/instance/lifecycle";
// public mount method
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
) :Component {
  el = el && inBrowser ? query(el) : undefined;
  return mountComponent(this, el, hydrating);
};
Copy the code

Code is clear and get el, then let mountComponent implementation, continue to explore the core/instance/lifecycle. Js

We defined updateComponent, and then instantiated Watcher, the technical term for rendering Watcher

import Watcher from ".. /observer/watcher";

function mountComponent(vm) {
  updateComponent = () = > {
    vm._update(vm._render(), hydrating);
  };
  // Render Watcher, updateComponent is uploaded here
  new Watcher( vm, updateComponent, noop, {}, true /* isRenderWatcher */ );
  return vm;
}
Copy the code

The updateComponent method is called in the render Watcher callback, where the vm._render method is called first to generate the virtual Node, and finally vm._update is called to update the DOM

Watcher does two things here, one is to execute the callback function on initialization and the other is to execute the callback function when the monitored data in the VM instance changes.

render

Let’s focus on _render, print the vm instance, see this method on the prototype, search globally, and quickly know that in core/instance/render.js:

The _render function returns vnode, which is returned by the $createElement method

Vue.prototype._render = function () {
  // ...
  // render self
  let vnode;
  try {
    $createElement = 'vm.$createElement'
    vnode = render.call(vm._renderProxy, vm.$createElement);
  } catch (e) {
    // ...
  }
  // if the returned array contains only a single node, allow it
  // set parent
  vnode.parent = _parentVnode;
  return vnode;
};
Copy the code

Understand VNode and Patch

  • Vnode is just thatVirtual dom(Virtual DOM), because the real node(Real DOM) attributes and methods are very many, frequent operation cost performance, while vNode has relatively simplified attributes, itself is easy to map into the real node, of course, VNode and Node are both objects in nature.
  • Patch is to make a vNode into a real node and insert it into a document (rendering)

Vnode and Patch can be run locally:

<body>
  <div id="app"></div>
  <script>
    / / vnode is essentially object, {" sel ":" div ", "data" : {}, "text", "Hello World"}, equivalent to describe the < div > Hello < / div >
    let vnode = new VNode("div"."hello");
    console.log(JSON.stringify(vnode));

    let app = document.querySelector("#app"); / / true dom

    /* Patch converts the second vnode into a real DOM, inserts it into the document, and destroys the first vnode. * Here is a detail, if the first node is a real node, it is internally converted to a vNode and then operated on */
    patch(app, vnode);
    // After patch, elm is now available on vNode
    console.log(vnode);

    /* The definition of each function */

    / / class VNode
    function VNode(tag, text, elm) {
      this.sel = tag;
      this.text = text;
      this.elm = elm;
    }

    / / create a vnode
    function createElement(tag, text, elm) {
      return new VNode(tag, text, elm);
    }

    // The real DOM is converted to a vNode
    function elToVNode(el) {
      return createElement(el.tagName, el.textContent, el);
    }

    // Patch converts the second VNode into a real DOM and inserts it into the document. The first node is compared with the second node in the sense of existence, so as to accurately render
    function patch(oldVNode, newVNode) {
      // If the first node is a real node, convert it to vnode
      if (oldVNode.nodeType) {
        oldVNode = elToVNode(oldVNode);
      }

      // Convert the second node to a real DOM
      let node = document.createElement(newVNode.sel);
      node.textContent = newVNode.text;
      newVNode.elm = node;

      // Insert into the document
      oldVNode.elm.parentNode.insertBefore(node, oldVNode.elm);

      // Destroy the first node.
      oldVNode.elm.parentNode.removeChild(oldVNode.elm);

      return newVNode.elm;
    }
  </script>
</body>
Copy the code

After having a concept of VNode and patch, continue to source ~

CreateElement in the source code returns a VNode

It’s easy to find in SRC /core/vdom/create-element.js. CreateElement actually calls _createElement internally, which returns a VNode.

Children represents the children of the current VNode, which is of any type and needs to be normalized as a standard VNode array, according to the normalizationType specification. (normalizationType mainly distinguishes whether the render function is compiled or handwritten)

import { normalizeChildren, simpleNormalizeChildren } from './helpers/index'
const SIMPLE_NORMALIZE = 1
const ALWAYS_NORMALIZE = 2
export function createElement (context: Component, tag: any, data: any, children: any, normalizationType: any, alwaysNormalize: boolean) :VNode | Array<VNode> {
  // If the data parameter does not exist, the following parameter is preceded
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE
  }
  return _createElement(context, tag, data, children, normalizationType)
}

export function _createElement (context: Component, tag? : string | Class<Component> |Function | Object, data? : VNodeData, children? : any, normalizationType? : number) :VNode | Array<VNode> {
  // For different normalizationtypes, different processing of children is actually flat children
  if (normalizationType === ALWAYS_NORMALIZE) {
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children)
  }
  let vnode, ns
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    if (config.isReservedTag(tag)) {
      // platform built-in elements
      Vnode supports both string and component types, but we'll just look at strings for now
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined.undefined, context
      )
    }
}
Copy the code

The children of standardization

SRC/core/vdom/helpers/normalize – children. Js two methods:

  • simpleNormalizeChildrenThis is a very simple method ifchildrenIf it’s two dimensions, pat it flat, the children will always be[vnode,vnode]I’m only thinking about two dimensions here
  • normalizeChildrenThis one is a little bit more complicated, but given the multidimensional situation, you have to recurse, and you end up flat
// 1. When the children contains components - because a functional component
// may return an Array instead of a single root. In this case, just a simple
// normalization is needed - if any child is an Array, we flatten the whole
// thing with Array.prototype.concat. It is guaranteed to be only 1-level deep
// because functional components already normalize their own children.
export function simpleNormalizeChildren(children: any) {
  for (let i = 0; i < children.length; i++) {
    if (Array.isArray(children[i])) {
      return Array.prototype.concat.apply([], children); }}return children;
}

// 2. When the children contains constructs that always generated nested Arrays,
// e.g. <template>, <slot>, v-for, or when the children is provided by user
// with hand-written render functions / JSX. In such cases a full normalization
// is needed to cater to all possible types of children values.
export function normalizeChildren(children: any): ?Array<VNode> {
  return isPrimitive(children)
    ? [createTextVNode(children)]
    : Array.isArray(children)
    ? normalizeArrayChildren(children)
    : undefined;
}

function isTextNode(node) :boolean {
  return isDef(node) && isDef(node.text) && isFalse(node.isComment);
}

function normalizeArrayChildren(children: any, nestedIndex? : string) :Array<VNode> {
  const res = [];
  let i, c, lastIndex, last;
  for (i = 0; i < children.length; i++) {
    c = children[i];
    if (isUndef(c) || typeof c === "boolean") continue;
    lastIndex = res.length - 1;
    last = res[lastIndex];
    // nested
    if (Array.isArray(c)) {
      if (c.length > 0) {
        c = normalizeArrayChildren(c, `${nestedIndex || ""}_${i}`);
        // Merge adjacent text Nodes Merge
        if (isTextNode(c[0]) && isTextNode(last)) {
          res[lastIndex] = createTextVNode(last.text + (c[0]: any).text); c.shift(); } res.push.apply(res, c); }}}return res;
}
Copy the code

Tip: Flatten a two-dimensional array

To flatten a two-dimensional array, use concat:

function flat(arr) {
  return[].concat(... arr); }Copy the code

[] concat (1, [4]) is [1, 4]

Update actually does most of thatpatch

To find a specific method, the basic is search, the later will not repeat.

_update in SRC/core/instance/lifecycle. Js

// The hydrating server, otherwise false
Vue.prototype._update = function (vnode: VNode, hydrating? : boolean) {
  const vm: Component = this;
  // Cache the previous DOM
  const prevEl = vm.$el;
  // Cache the previous vNode
  const prevVnode = vm._vnode;
  const restoreActiveInstance = setActiveInstance(vm);
  // Newly generated vNode assignment
  vm._vnode = vnode;

  if(! prevVnode) {Create a real DOM from vNode and insert it into the document. __Patch__ returns the real DOM
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
    console.log(vm.$el); }};Copy the code

Take a closer look at __Patch__, where you need to layer up, and actually look very similar to the body logic in the simple example above

But why use createPatchFunction to generate patches?

Vue has three platforms. As mentioned earlier, dom rendering is not required on the server side, so ignore that. Both the Web and WEEX require rendering, but the “platform DOM” approach is different, and the attribute modules included in the “DOM” are created and updated differently. So each platform has its own nodeOps and Modules, so it’s stored on its own platform. However, patch’s main logic is similar, so differentiation parameters are solidified in advance through createPatchFunction, so nodeOps and modules are not passed every time patch is called. This kind of programming skill of function Currification is also worth learning.

/* src/platforms/web/runtime/index.js */
import { patch } from "./patch";
// The browser does, the server does not. Noop is an empty function
Vue.prototype.__patch__ = inBrowser ? patch : noop;

/* src/platforms/web/runtime/patch.js */
import { createPatchFunction } from "core/vdom/patch";
Modules attrs, klass, events, domProps, etc
export const patch: Function = createPatchFunction({ nodeOps, modules });

/* src/core/vdom/patch.js */
export function createPatchFunction(backend) {
  / /... Define many functions related to the real DOM
  return function patch(oldVnode, vnode, hydrating, removeOnly) {
    // For the first rendering, it is the real DOM and only the code associated with it is posted
    const isRealElement = isDef(oldVnode.nodeType);
    // replacing existing element
    // Cache the old DOM
    const oldElm = oldVnode.elm;
    // Cache the old DOM parent element
    const parentElm = nodeOps.parentNode(oldElm);

    // create new node creates a DOM from the new vNode and inserts the new DOM in front of the old element in the parent element
    createElm(
      vnode,
      insertedVnodeQueue,
      oldElm._leaveCb ? null : parentElm,
      nodeOps.nextSibling(oldElm)
    );

    // destroy old node
    if (isDef(parentElm)) {
      // Remove the old DOM
      removeVnodes(parentElm, [oldVnode], 0.0);
    }

    // return a new DOM
    return vnode.elm;
  };
}
Copy the code

The debugger to debug _update

<div id="app">{{message}}</div>

<! -- Absolute path -->
<script src="vue/dist/vue.js"></script>
<script>
  const vm2 = new Vue({
    el: "#app".data: {
      message: "hello",},render(createElement) {
      return createElement(
        "section",
        {
          attrs: { id: "box"}},this.message ); }});</script>
Copy the code

Dist /vue.js, put a breakpoint here

Vue.prototype._update = function (vnode, hydrating) {
  debugger;
};
Copy the code

It is important to make it clear that the initial #app is the actual dom and the subsequent section#box is vnode, where the argument vnode refers to section#box and vm.$el refers to #app

conclusion

When vUE is introduced, the first rendering process looks like this:

initMixin(Vue); / /... When VUE was introduced, Vue.prototype already had module properties and methods mounted. src/core/instance/init.js

new Vue({ el: "#app" }); // The user creates the VM instance

this._init(options); // src/core/instance/index.js

vm.$mount(vm.$options.el); / / _init method in SRC/core/instance/init. Js


template = getOuterHTML(el);
const { render, staticRenderFns } = compileToFunctions(template);
options.render = render;
mount.call(this, el, hydrating); // $mount is used to render the template or EL dom string. After that, call runtime's $mount method SRC /platforms/web/entry-runtime-with-compiler.js



mountComponent(this, el, hydrating); / / $mount method in the runtime, as this has been the render function SRC/platforms/web/runtime/index, js


updateComponent = () = > {
  vm._update(vm._render(), hydrating);
};
new Watcher(vm, updateComponent, noop, {}, true /* isRenderWatcher */) _render(vm._render(), hydrating)._render returns vnode._update turns the vnode into a dom. For the first time and then inserted into the document (rendering), rendering, and _render is actually perform the render function parameters SRC/core/instance/lifecycle. Js


const { render, _parentVnode } = vm.$options
vnode = render.call(vm._renderProxy, vm.$createElement)
return return vnode / / and _render function in SRC/core/instance/render. Js


vm.$el = vm.__patch__(vm.$el, vnode) / / _update __patch__ in function is to create real dom and inserted into the document in the SRC/core/instance/lifecycle. Js

Vue.prototype.__patch__ = inBrowser ? patch : noop / / SRC / __patch__ is patch method platforms/web/runtime/index, js

export const patch: Function = createPatchFunction({ nodeOps, modules })
/ / web under the patch generated by createPatchFunction SRC/platforms/web/runtime/patch. Js


function createPatchFunction(backend){
  const { modules, nodeOps } = backend

  function createElm ( vnode, insertedVnodeQueue, parentElm, refElm) {
    const data = vnode.data
    const children = vnode.children
    const tag = vnode.tag
    nodeOps.createElement(tag, vnode)
  }
  return function patch (oldVnode, vnode, hydrating, removeOnly) {
    const oldElm = oldVnode.elm
    // Cache the old DOM parent element
    const parentElm = nodeOps.parentNode(oldElm)
    // create new node creates a DOM from the new vNode and inserts the new DOM in front of the old element in the parent element
    createElm( vnode, insertedVnodeQueue, parentElm, nodeOps.nextSibling(oldElm) )
    // Remove the old DOM
    removeVnodes(parentElm, [oldVnode], 0.0)
    // return a new DOM
    return vnode.elm
  }
} // The patch generation function here uses modules of different platforms, but the logic of patch is similar to SRC /core/vdom/patch.js
Copy the code

Here, with the help of Teacher Huang Yi’s diagram, shows how to render the template and data into the final DOM from the main line:

reference

  • Huang Yi teacher vuE2 source decryption course
  • Vue. Js technology revealed
  • The use of Snabbdom