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.
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
$mount
When rewritten, is in the original$mount
Add function to el
First check whether incoming, and then unified byquery
To obtaindom
- Once you’ve got the DOM, let’s see if it’s okay
body
orhtml
If so, warn and terminate - if
options
There areRender function
I’m just going to go back to the original$mount
; If not, willtemplate
orel
intoRender function
And then you call the original$mount
- Focus onSo let’s see, here, how do I put it
template
orel
intoRender function
It’s basically findingDOM
string- There is no
Render function
There are only two cases: yestemplate
orel
, will eventuallyDOM
String assigned totemplate
- priority
template
:template
Is a string#
At the beginning, get the element as id and return the element’sinnerHTML
.template
If it is an element, return the value of the elementinnerHTML
- Other cases are not supported
- There is no
template
And seeel
, to get elouterHTML
, in the assignment totemplate
- There is no
- then
DOM
String to usecompileToFunctions
Processing 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 that
Virtual 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:
simpleNormalizeChildren
This is a very simple method ifchildren
If it’s two dimensions, pat it flat, the children will always be[vnode,vnode]
I’m only thinking about two dimensions herenormalizeChildren
This 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