Preface
Recently, in learning the source code of vue2.0, I just started to look at the source code of vdom, but I really can't find the direction, because it is added to the implementation of vdom.
Many hooks of vue2.0 itself make reading more difficult. So I saw the first line saying that vdom of vue2.0 is snabbdom.
On the basis of that, snabbdom has less than 300 slocs, so it's better to start with snabbdom and get familiar with its principles.
With the vdom of vue2.0, the effect may be better.
What is virtual-dom
virtual-dom can be seen as a JavaScript tree that simulates the DOM tree. It mainly realizes a nonexistence through vnode.
A component in state, when its state is updated, triggers a change in virtual-dom data, and then passes through virtual-dom.
Compared with the real DOM, the real DOM is updated.
Why virtual-dom
We know that when we want to implement an interface with complex states, if we bind to every component that may change.
Events, bind field data, and soon because there are too many states, we will need to maintain more and more events and fields, as well as code.
More and more complex, so we wonder if we can separate the view from the state, as long as the view changes, the corresponding state also occurs.
Change, then change the state, and we can redraw the whole view. That's a good idea, but it's too expensive, so we did it again.
Think, can you just update the view whose state has changed? So virtual-dom came into being, and the state changes were fed back to vdom first.
vdom finds the minimum update view, and finally updates it to the real DOM in batches, so as to achieve performance improvement.
In addition, from the perspective of portability, virtual-dom also abstracts the real dom, which means virtual-dom corresponds to each other.
It is not the dom of the browser, but the components of different devices, which greatly facilitates the use of multiple platforms.
snabbdom directory structure
Well, that's all. Let's take a look at snabbdom first. I'm looking at this version. snabbdom
(Heart plug, typescript learning is not deep, see the latest version of a bit of effort, so choose a version before ts version). Okay, let's go ahead.
Look at the main directory structure of snabbdom.
Name | type | explain |
---|---|---|
dist | Folder | It contains snabddom packaged files |
examples | Folder | It contains examples of using snabbdom |
helpers | Folder | Contains tools needed for svg operations |
modules | Folder | Includes operations on attribute, props, class, dataset, eventlistner, style, hero |
perf | Folder | performance testing |
test | Folder | test |
h | file | Transforming state into vnode |
htmldomapi | file | Abstraction of native dom operation |
is | file | Judgment type |
snabbdom.bundle | file | snabbdom itself relies on packaging |
snabbdom | file | snabbdom core, including diff, patch and other operations |
thunk | file | thunk function realization under snabbdom |
vnode | file | Constructing vnode |
Snbbdom Source Tour
First stop vnode
First of all, we start with the simplest vnode. The function of vnode is very simple, that is to say, the input data is converted into vnode.
Object form
//VNode function for converting input to VNode /** * * @param sel selector * @param data Binding data * @param children Array of child nodes * @param text Current text node content * @param elm A Reference to Real dom element * @returns {{sel: *, data: *, children: *, text: *, elm: *, key: undefined}} */ module.exports = function ( sel, data, children, text, elm ) { var key = data === undefined ? undefined : data.key; return { sel: sel, data: data, children: children, text: text, elm: elm, key: key }; };
vnode has five main attributes:
Selsels correspond to selectors in the form of'div','div a','div a.b.c'.
Data corresponds to data bound by vnode, and can be of the following types: attribute, props, eventlistner, and
class,dataset,hookchildren subelement array
Text text, representing the text content in the node
There are references to the corresponding true dom element in elm
key is used to compare different vnode s
Second stop h
After vnode, it's h. h is also a wrapping function. It's mainly to make another layer of wrapping on vnode. The functions are as follows.
If it is svg, add a namespace to it
-
Wrap text in children into vnode form
var VNode = require ( './vnode' ); var is = require ( './is' ); //Add namespaces (svg is required) function addNS ( data, children, sel ) { data.ns = 'http://www.w3.org/2000/svg'; //If the selector if ( sel !== 'foreignObject' && children !== undefined ) { //Add namespaces recursively for child nodes for (var i = 0; i < children.length; ++i) { addNS ( children[ i ].data, children[ i ].children, children[ i ].sel ); } } } //Rendering VNode to VDOM /** * * @param sel selector * @param b data * @param c Subnode * @returns {{sel, data, children, text, elm, key}} */ module.exports = function h ( sel, b, c ) { var data = {}, children, text, i; //If there are child nodes if ( c !== undefined ) { //So the second item of h is data. data = b; //If c is an array, then there are child element nodes if ( is.array ( c ) ) { children = c; } //Otherwise, it's a sub text node else if ( is.primitive ( c ) ) { text = c; } } //If c does not exist and b only exists, then the vdom that needs to be rendered does not exist in the data part and only in the sub-node part. else if ( b !== undefined ) { if ( is.array ( b ) ) { children = b; } else if ( is.primitive ( b ) ) { text = b; } else { data = b; } } if ( is.array ( children ) ) { for (i = 0; i < children.length; ++i) { //If the existing node in the subnode array is of the original type, it means that the node is a text node, so we render it as a VNode containing text only. if ( is.primitive ( children[ i ] ) ) children[ i ] = VNode ( undefined, undefined, undefined, children[ i ] ); } } //If it's svg, you need to add a namespace for the node if ( sel[ 0 ] === 's' && sel[ 1 ] === 'v' && sel[ 2 ] === 'g' ) { addNS ( data, children, sel ); } return VNode ( sel, data, children, text, undefined ); };
Third stop htmldomapi
The htmlDOM API provides a layer of abstraction for native dom operations, which is not explained here.
Fourth station modules
modules mainly include attributes, class, props, dataset, eventlistener, hero, style.
These modules, attributes,class,props,dataset,eventlistener,style are ours.
These are also the default injections of snabbdom.bundle, which are needed everyday. Here we will introduce these modules in detail.
attributes
The main functions are as follows:
Delete attributes that do not exist in vnode from the properties of elm (including those boolean class attributes, if the new vnode is set to false, the same is deleted)
If oldvnode and vnode use the same name attribute, the corresponding attribute value is updated on elm
If vnode has new properties, add them to elm
-
If there is a namespace, set AttributeNS
var NamespaceURIs = { "xlink": "http://www.w3.org/1999/xlink" }; var booleanAttrs = ["allowfullscreen", "async", "autofocus", "autoplay", "checked", "compact", "controls", "declare", "default", "defaultchecked", "defaultmuted", "defaultselected", "defer", "disabled", "draggable", "enabled", "formnovalidate", "hidden", "indeterminate", "inert", "ismap", "itemscope", "loop", "multiple", "muted", "nohref", "noresize", "noshade", "novalidate", "nowrap", "open", "pauseonexit", "readonly", "required", "reversed", "scoped", "seamless", "selected", "sortable", "spellcheck", "translate", "truespeed", "typemustmatch", "visible"]; var booleanAttrsDict = Object.create(null); //Create a dictionary of attributes, default to true for(var i=0, len = booleanAttrs.length; i < len; i++) { booleanAttrsDict[booleanAttrs[i]] = true; } function updateAttrs(oldVnode, vnode) { var key, cur, old, elm = vnode.elm, oldAttrs = oldVnode.data.attrs, attrs = vnode.data.attrs, namespaceSplit; //If neither the old node nor the new node contains attributes, return immediately if (!oldAttrs && !attrs) return; oldAttrs = oldAttrs || {}; attrs = attrs || {}; // update modified attributes, add new attributes //Update changed attributes and add new attributes for (key in attrs) { cur = attrs[key]; old = oldAttrs[key]; //If the old attribute is different from the new one if (old !== cur) { //If it's a boolean class attribute, when vnode is set to false value, delete it directly instead of updating it if(!cur && booleanAttrsDict[key]) elm.removeAttribute(key); else { //Otherwise, update attribute values or add attributes //If a namespace exists namespaceSplit = key.split(":"); if(namespaceSplit.length > 1 && NamespaceURIs.hasOwnProperty(namespaceSplit[0])) elm.setAttributeNS(NamespaceURIs[namespaceSplit[0]], key, cur); else elm.setAttribute(key, cur); } } } //remove removed attributes // use `in` operator since the previous `for` iteration uses it (.i.e. add even attributes with undefined value) // the other option is to remove all attributes with value == undefined //Delete properties of old nodes that are not in new node properties for (key in oldAttrs) { if (!(key in attrs)) { elm.removeAttribute(key); } } } module.exports = {create: updateAttrs, update: updateAttrs};
class
The main functions are as follows:
Delete classes that do not exist in vnode or have a value of false from elm
-
Add the new class in vnode to elm
function updateClass(oldVnode, vnode) { var cur, name, elm = vnode.elm, oldClass = oldVnode.data.class, klass = vnode.data.class; //If neither the old node nor the new node has a class, return directly if (!oldClass && !klass) return; oldClass = oldClass || {}; klass = klass || {}; //Delete classes that do not exist for new nodes from old nodes for (name in oldClass) { if (!klass[name]) { elm.classList.remove(name); } } //If the class corresponding to the old node is set to false in the new node, delete the class and add the class if the new one is set to true for (name in klass) { cur = klass[name]; if (cur !== oldClass[name]) { elm.classList[cur ? 'add' : 'remove'](name); } } } module.exports = {create: updateClass, update: updateClass};
dataset
The main functions are as follows:
Delete attributes from attribute sets where vnode does not exist from elm
-
Update the attribute values in the attribute set
function updateDataset(oldVnode, vnode) { var elm = vnode.elm, oldDataset = oldVnode.data.dataset, dataset = vnode.data.dataset, key //If the old and new nodes do not have data sets, they are returned directly if (!oldDataset && !dataset) return; oldDataset = oldDataset || {}; dataset = dataset || {}; //Delete data sets that do not exist in the new node in the old node for (key in oldDataset) { if (!dataset[key]) { delete elm.dataset[key]; } } //Update Data Set for (key in dataset) { if (oldDataset[key] !== dataset[key]) { elm.dataset[key] = dataset[key]; } } } module.exports = {create: updateDataset, update: updateDataset}
eventlistener
In snabbdom, event processing is wrapped. The real DOM event triggers the operation of vnode. The main way is:
CreateListner => Return handler as event listener => Bind vnode => Handler as event handler for real DOM
After a real DOM event is triggered, => handler obtains the real DOM event object, => passes the real DOM event object into handleEvent => handleEvent to find it.
The corresponding vnode event handler is then called to modify the vnode
//In snabbdom, event processing is wrapped in a layer. The real DOM event triggers the operation of vnode. //The main approach is // CreateListner => Return handler as event listener => Bind vnode => Handler as event handler for real DOM //After a real DOM event is triggered, => handler obtains the real DOM event object, => passes the real DOM event object into handleEvent => handleEvent to find it. //The corresponding vnode event handler is then called to modify the vnode //Event handling for vnode function invokeHandler ( handler, vnode, event ) { if ( typeof handler === "function" ) { // call function handler //Call event handlers on vnode handler.call ( vnode, event, vnode ); } //Existence of event-bound data or presence of multi-event handlers else if ( typeof handler === "object" ) { //Explain that there is only one event handler if ( typeof handler[ 0 ] === "function" ) { //If there is only one bound data, call the data directly to improve performance. //Like on:{click:[handler,1]} if ( handler.length === 2 ) { handler[ 0 ].call ( vnode, handler[ 1 ], event, vnode ); } //If there are multiple bound data, they are converted into arrays and called in an apply manner, and apply performs worse than call. //For example: on:{click:[handler,1,2,3]} else { var args = handler.slice ( 1 ); args.push ( event ); args.push ( vnode ); handler[ 0 ].apply ( vnode, args ); } } else { //If there are different processors with multiple identical events, the call is recursive //For example, on:{click:[[handeler1,1],[handler,2]} for (var i = 0; i < handler.length; i++) { invokeHandler ( handler[ i ] ); } } } } /** * * @param event Real dom event object * @param vnode */ function handleEvent ( event, vnode ) { var name = event.type, on = vnode.data.on; // If the corresponding vnode event handler is found, the call is made if ( on && on[ name ] ) { invokeHandler ( on[ name ], vnode, event ); } } //Event listener generator for handling real DOM events function createListener () { return function handler ( event ) { handleEvent ( event, handler.vnode ); } } //Update Event Listening function updateEventListeners ( oldVnode, vnode ) { var oldOn = oldVnode.data.on, oldListener = oldVnode.listener, oldElm = oldVnode.elm, on = vnode && vnode.data.on, elm = vnode && vnode.elm, name; // optimization for reused immutable handlers //If the old and new event listeners are the same, return directly if ( oldOn === on ) { return; } // remove existing listeners which no longer used //If there is no event listener on the new node, delete all event listeners on the old node if ( oldOn && oldListener ) { // if element changed or deleted we remove all existing listeners unconditionally if ( !on ) { for (name in oldOn) { // remove listener if element was changed or existing listeners removed oldElm.removeEventListener ( name, oldListener, false ); } } else { //Delete event listeners where new nodes do not exist in old nodes for (name in oldOn) { // remove listener if existing listener removed if ( !on[ name ] ) { oldElm.removeEventListener ( name, oldListener, false ); } } } } // add new listeners which has not already attached if ( on ) { // reuse existing listener or create new //If a listener already exists on the oldvnode, the vnode is directly multiplexed, otherwise a new event handler is created. var listener = vnode.listener = oldVnode.listener || createListener (); // update vnode for listener //Binding vnode on event handler listener.vnode = vnode; // if element changed or added we add all needed listeners unconditionally' //If there is no event handler on oldvnode if ( !oldOn ) { for (name in on) { // add listener if element was changed or new listeners added //Add event handlers on vnode directly to elm elm.addEventListener ( name, listener, false ); } } else { for (name in on) { // add listener if new listener added //Otherwise, add event handlers that are not available on oldvnode if ( !oldOn[ name ] ) { elm.addEventListener ( name, listener, false ); } } } } } module.exports = { create: updateEventListeners, update: updateEventListeners, destroy: updateEventListeners };
props
Main functions:
Delete attributes that do not exist in vnode from elm
-
Update attributes on elm
function updateProps(oldVnode, vnode) { var key, cur, old, elm = vnode.elm, oldProps = oldVnode.data.props, props = vnode.data.props; //If neither new nor old nodes have attributes, they are returned directly if (!oldProps && !props) return; oldProps = oldProps || {}; props = props || {}; //Delete attributes that the new node does not have in the old node for (key in oldProps) { if (!props[key]) { delete elm[key]; } } //Update attributes for (key in props) { cur = props[key]; old = oldProps[key]; //If the attributes of old and new nodes are different, and the attributes of comparison are different from those of value or elm, then they need to be updated. if (old !== cur && (key !== 'value' || elm[key] !== cur)) { elm[key] = cur; } } } module.exports = {create: updateProps, update: updateProps};
style
The main functions are as follows:
Empty the style that exists in old vnode but does not exist in vnode
If delayed in vnode.style is different from oldvnode, the attribute value of delayed is updated, and the style of elm is set to that value in the next frame to achieve animation transition effect.
Direct update of style for non-delayed and remote
When vnode is destroy ed, the corresponding style is updated directly to the value of vnode.data.style.destory
-
When a vnode is reomve, if style.remove does not exist, call the global remove hook directly to the next remove process
If style. remote exists, then we need to set the remove animation transition effect and wait until the transition effect is over before calling it.
Next remove process//If there is a request Animation Frame, use it directly to optimize performance, otherwise use setTimeout var raf = (typeof window !== 'undefined' && window.requestAnimationFrame) || setTimeout; var nextFrame = function(fn) { raf(function() { raf(fn); }); }; //Realize animation effect through nextFrame function setNextFrame(obj, prop, val) { nextFrame(function() { obj[prop] = val; }); } function updateStyle(oldVnode, vnode) { var cur, name, elm = vnode.elm, oldStyle = oldVnode.data.style, style = vnode.data.style; //If neither old vnode nor vnode has a style, return directly if (!oldStyle && !style) return; oldStyle = oldStyle || {}; style = style || {}; var oldHasDel = 'delayed' in oldStyle; //Traversing the style of oldvnode for (name in oldStyle) { //If there is no such style in the vnode, empty it if (!style[name]) { elm.style[name] = ''; } } //If there is a delayed in the style of the vnode and it is different from the old vnode, set the delayed parameter in the next frame for (name in style) { cur = style[name]; if (name === 'delayed') { for (name in style.delayed) { cur = style.delayed[name]; if (!oldHasDel || cur !== oldStyle.delayed[name]) { setNextFrame(elm.style, name, cur); } } } //If it's not the style of delayed and remote, and it's different from the value of oldvnode, set the new value directly else if (name !== 'remove' && cur !== oldStyle[name]) { elm.style[name] = cur; } } } //Set the style when the node is destory function applyDestroyStyle(vnode) { var style, name, elm = vnode.elm, s = vnode.data.style; if (!s || !(style = s.destroy)) return; for (name in style) { elm.style[name] = style[name]; } } //Delete effect. When we delete an element, we call back the Delete Excessive Effect before we remove the node after the transition. function applyRemoveStyle(vnode, rm) { var s = vnode.data.style; //If there is no style or style. if (!s || !s.remove) { //Calling rm directly means actually calling the global remote hook rm(); return; } var name, elm = vnode.elm, idx, i = 0, maxDur = 0, compStyle, style = s.remove, amount = 0, applied = []; //Set and record the style before deleting the node after remove action for (name in style) { applied.push(name); elm.style[name] = style[name]; } compStyle = getComputedStyle(elm); //Get all the attributes that need to be transitioned var props = compStyle['transition-property'].split(', '); //For counting transition attributes, here applied. length >= amount, because some attributes do not need transition for (; i < props.length; ++i) { if(applied.indexOf(props[i]) !== -1) amount++; } //When the transition effect is complete, the remove node calls the next remove process elm.addEventListener('transitionend', function(ev) { if (ev.target === elm) --amount; if (amount === 0) rm(); }); } module.exports = {create: updateStyle, update: updateStyle, destroy: applyDestroyStyle, remove: applyRemoveStyle};
Station 5 is
Having eaten all these big pieces of modules, there's a tasty dessert. Its main function is to determine whether it's an array type or a primitive type.
//Is toolkit for judging whether it is array or primitive type module.exports = { array: Array.isArray, primitive: function(s) { return typeof s === 'string' || typeof s === 'number'; }, };
Half-way rest
Looking at so many source codes, I guess I'm tired. After all, it may be a little difficult to fully understand. Take a rest and digest it. The next chapter will see the biggest boss, snabbdom itself.