摘要:前言最近在學習的源碼,剛開始看其源碼,著實找不到方向,因為其在的實現(xiàn)上還加入了很多本身的鉤子,加大了閱讀難度。
前言
最近在學習vue2.0的源碼,剛開始看其vdom源碼,著實找不到方向,因為其在vdom的實現(xiàn)上還加
入了很多vue2.0本身的鉤子,加大了閱讀難度。于是看到第一行尤大說vue2.0的vdom是在snabbdom
的基礎上改過來的,而snabbdom只有不到300sloc,那不妨先從snabbdom入手,熟悉其中的原理,
再配合vue2.0的vdom看,效果可能更好。
virtual-dom可以看做一棵模擬了DOM樹的JavaScript樹,其主要是通過vnode,實現(xiàn)一個無
狀態(tài)的組件,當組件狀態(tài)發(fā)生更新時,然后觸發(fā)virtual-dom數(shù)據(jù)的變化,然后通過virtual-dom
和真實DOM的比對,再對真實dom更新。
我們知道,當我們希望實現(xiàn)一個具有復雜狀態(tài)的界面時,如果我們在每個可能發(fā)生變化的組件上都綁定
事件,綁定字段數(shù)據(jù),那么很快由于狀態(tài)太多,我們需要維護的事件和字段將會越來越多,代碼也會
越來越復雜,于是,我們想我們可不可以將視圖和狀態(tài)分開來,只要視圖發(fā)生變化,對應狀態(tài)也發(fā)生
變化,然后狀態(tài)變化,我們再重繪整個視圖就好了。這樣的想法雖好,但是代價太高了,于是我們又
想,能不能只更新狀態(tài)發(fā)生變化的視圖?于是virtual-dom應運而生,狀態(tài)變化先反饋到vdom上,
vdom在找到最小更新視圖,最后批量更新到真實DOM上,從而達到性能的提升。
除此之外,從移植性上看,virtual-dom還對真實dom做了一次抽象,這意味著virtual-dom對應
的可以不是瀏覽器的dom,而是不同設備的組件,極大的方便了多平臺的使用。
好了,說了這么多,我們先來看看snabbdom吧,我看的是這個版本的snabbdom
(心塞,typescript學的不深,看最新版的有點吃力,所以選了ts版本前的一個版本)。好了我們先
看看snabbdom的主要目錄結構。
名稱 | 類型 | 解釋 |
---|---|---|
dist | 文件夾 | 里面包含了snabddom打包后的文件 |
examples | 文件夾 | 里面包含了使用snabbdom的例子 |
helpers | 文件夾 | 包含svg操作需要的工具 |
modules | 文件夾 | 包含了對attribute,props,class,dataset,eventlistner,style,hero的操作 |
perf | 文件夾 | 性能測試 |
test | 文件夾 | 測試 |
h | 文件 | 把狀態(tài)轉化為vnode |
htmldomapi | 文件 | 原生dom操作的抽象 |
is | 文件 | 判斷類型 |
snabbdom.bundle | 文件 | snabbdom本身依賴打包 |
snabbdom | 文件 | snabbdom 核心,包含diff,patch等操作 |
thunk | 文件 | snabbdom下的thunk功能實現(xiàn) |
vnode | 文件 | 構造vnode |
首先,我們從最簡單的vnode開始入手,vnode實現(xiàn)的功能非常簡單,就是講輸入的數(shù)據(jù)轉化為vnode
對象的形式
//VNode函數(shù),用于將輸入轉化成VNode /** * * @param sel 選擇器 * @param data 綁定的數(shù)據(jù) * @param children 子節(jié)點數(shù)組 * @param text 當前text節(jié)點內(nèi)容 * @param elm 對真實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主要有5大屬性:
sel 對應的是選擇器,如"div","div#a","div#a.b.c"的形式
data 對應的是vnode綁定的數(shù)據(jù),可以有以下類型:attribute、props、eventlistner、
class、dataset、hook
children 子元素數(shù)組
text 文本,代表該節(jié)點中的文本內(nèi)容
elm 里面存儲著對對應的真實dom element的引用
key 用于不同vnode之間的比對
第二站 h說完vnode,就到h了,h也是一個包裝函數(shù),主要是在vnode上再做一層包裝,實現(xiàn)功能如下
如果是svg,則為其添加命名空間
將children中的text包裝成vnode形式
var VNode = require ( "./vnode" ); var is = require ( "./is" ); //添加命名空間(svg才需要) function addNS ( data, children, sel ) { data.ns = "http://www.w3.org/2000/svg"; //如果選擇器 if ( sel !== "foreignObject" && children !== undefined ) { //遞歸為子節(jié)點添加命名空間 for (var i = 0; i < children.length; ++i) { addNS ( children[ i ].data, children[ i ].children, children[ i ].sel ); } } } //將VNode渲染為VDOM /** * * @param sel 選擇器 * @param b 數(shù)據(jù) * @param c 子節(jié)點 * @returns {{sel, data, children, text, elm, key}} */ module.exports = function h ( sel, b, c ) { var data = {}, children, text, i; //如果存在子節(jié)點 if ( c !== undefined ) { //那么h的第二項就是data data = b; //如果c是數(shù)組,那么存在子element節(jié)點 if ( is.array ( c ) ) { children = c; } //否則為子text節(jié)點 else if ( is.primitive ( c ) ) { text = c; } } //如果c不存在,只存在b,那么說明需要渲染的vdom不存在data部分,只存在子節(jié)點部分 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) { //如果子節(jié)點數(shù)組中,存在節(jié)點是原始類型,說明該節(jié)點是text節(jié)點,因此我們將它渲染為一個只包含text的VNode if ( is.primitive ( children[ i ] ) ) children[ i ] = VNode ( undefined, undefined, undefined, children[ i ] ); } } //如果是svg,需要為節(jié)點添加命名空間 if ( sel[ 0 ] === "s" && sel[ 1 ] === "v" && sel[ 2 ] === "g" ) { addNS ( data, children, sel ); } return VNode ( sel, data, children, text, undefined ); };第三站 htmldomapi
htmldomapi中提供了對原生dom操作的一層抽象,這里就不再闡述了
第四站 modulesmodules中主要包含attributes,class,props,dataset,eventlistener,hero,style
這些模塊,其中attributes,class,props,dataset,eventlistener,style這些模塊是我們
日常所需要的,也是snabbdom.bundle默認注入的也是這幾個,這里就詳細介紹這幾個模塊
主要功能如下:
從elm的屬性中刪除vnode中不存在的屬性(包括那些boolean類屬性,如果新vnode設置為false,同樣刪除)
如果oldvnode與vnode用同名屬性,則在elm上更新對應屬性值
如果vnode有新屬性,則添加到elm中
如果存在命名空間,則用setAttributeNS設置
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); //創(chuàng)建屬性字典,默認為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; //如果舊節(jié)點和新節(jié)點都不包含屬性,立刻返回 if (!oldAttrs && !attrs) return; oldAttrs = oldAttrs || {}; attrs = attrs || {}; // update modified attributes, add new attributes //更新改變了的屬性,添加新的屬性 for (key in attrs) { cur = attrs[key]; old = oldAttrs[key]; //如果舊的屬性和新的屬性不同 if (old !== cur) { //如果是boolean類屬性,當vnode設置為falsy value時,直接刪除,而不是更新值 if(!cur && booleanAttrsDict[key]) elm.removeAttribute(key); else { //否則更新屬性值或者添加屬性 //如果存在命名空間 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 //刪除不在新節(jié)點屬性中的舊節(jié)點的屬性 for (key in oldAttrs) { if (!(key in attrs)) { elm.removeAttribute(key); } } } module.exports = {create: updateAttrs, update: updateAttrs};class
主要功能如下:
從elm中刪除vnode中不存在的或者值為false的類
將vnode中新的class添加到elm上去
function updateClass(oldVnode, vnode) { var cur, name, elm = vnode.elm, oldClass = oldVnode.data.class, klass = vnode.data.class; //如果舊節(jié)點和新節(jié)點都沒有class,直接返回 if (!oldClass && !klass) return; oldClass = oldClass || {}; klass = klass || {}; //從舊節(jié)點中刪除新節(jié)點不存在的類 for (name in oldClass) { if (!klass[name]) { elm.classList.remove(name); } } //如果新節(jié)點中對應舊節(jié)點的類設置為false,則刪除該類,如果新設置為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
主要功能如下:
從elm中刪除vnode不存在的屬性集中的屬性
更新屬性集中的屬性值
function updateDataset(oldVnode, vnode) { var elm = vnode.elm, oldDataset = oldVnode.data.dataset, dataset = vnode.data.dataset, key //如果新舊節(jié)點都沒數(shù)據(jù)集,則直接返回 if (!oldDataset && !dataset) return; oldDataset = oldDataset || {}; dataset = dataset || {}; //刪除舊節(jié)點中在新節(jié)點不存在的數(shù)據(jù)集 for (key in oldDataset) { if (!dataset[key]) { delete elm.dataset[key]; } } //更新數(shù)據(jù)集 for (key in dataset) { if (oldDataset[key] !== dataset[key]) { elm.dataset[key] = dataset[key]; } } } module.exports = {create: updateDataset, update: updateDataset}eventlistener
snabbdom中對事件處理做了一層包裝,真實DOM的事件觸發(fā)的是對vnode的操作,主要途徑是:
createListner => 返回handler作事件監(jiān)聽生成器 =>handler上綁定vnode =>將handler作真實DOM的事件處理器
真實DOM事件觸發(fā)后 => handler獲得真實DOM的事件對象 => 將真實DOM事件對象傳入handleEvent => handleEvent找到
對應的vnode事件處理器,然后調(diào)用這個處理器從而修改vnode
//snabbdom中對事件處理做了一層包裝,真實DOM的事件觸發(fā)的是對vnode的操作 //主要途徑是 // createListner => 返回handler作事件監(jiān)聽生成器 =>handler上綁定vnode =>將handler作真實DOM的事件處理器 //真實DOM事件觸發(fā)后 => handler獲得真實DOM的事件對象 => 將真實DOM事件對象傳入handleEvent => handleEvent找到 //對應的vnode事件處理器,然后調(diào)用這個處理器從而修改vnode //對vnode進行事件處理 function invokeHandler ( handler, vnode, event ) { if ( typeof handler === "function" ) { // call function handler //將事件處理器在vnode上調(diào)用 handler.call ( vnode, event, vnode ); } //存在事件綁定數(shù)據(jù)或者存在多事件處理器 else if ( typeof handler === "object" ) { //說明只有一個事件處理器 if ( typeof handler[ 0 ] === "function" ) { //如果綁定數(shù)據(jù)只有一個,則直接將數(shù)據(jù)用call的方式調(diào)用,提高性能 //形如on:{click:[handler,1]} if ( handler.length === 2 ) { handler[ 0 ].call ( vnode, handler[ 1 ], event, vnode ); } //如果存在多個綁定數(shù)據(jù),則要轉化為數(shù)組,用apply的方式調(diào)用,而apply性能比call差 //形如:on:{click:[handler,1,2,3]} else { var args = handler.slice ( 1 ); args.push ( event ); args.push ( vnode ); handler[ 0 ].apply ( vnode, args ); } } else { //如果存在多個相同事件的不同處理器,則遞歸調(diào)用 //如on:{click:[[handeler1,1],[handler,2]]} for (var i = 0; i < handler.length; i++) { invokeHandler ( handler[ i ] ); } } } } /** * * @param event 真實dom的事件對象 * @param vnode */ function handleEvent ( event, vnode ) { var name = event.type, on = vnode.data.on; // 如果找到對應的vnode事件處理器,則調(diào)用 if ( on && on[ name ] ) { invokeHandler ( on[ name ], vnode, event ); } } //事件監(jiān)聽器生成器,用于處理真實DOM事件 function createListener () { return function handler ( event ) { handleEvent ( event, handler.vnode ); } } //更新事件監(jiān)聽 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 //如果新舊事件監(jiān)聽器一樣,則直接返回 if ( oldOn === on ) { return; } // remove existing listeners which no longer used //如果新節(jié)點上沒有事件監(jiān)聽,則將舊節(jié)點上的事件監(jiān)聽都刪除 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 { //刪除舊節(jié)點中新節(jié)點不存在的事件監(jiān)聽 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 //如果oldvnode上已經(jīng)有l(wèi)istener,則vnode直接復用,否則則新建事件處理器 var listener = vnode.listener = oldVnode.listener || createListener (); // update vnode for listener //在事件處理器上綁定vnode listener.vnode = vnode; // if element changed or added we add all needed listeners unconditionally‘ //如果oldvnode上沒有事件處理器 if ( !oldOn ) { for (name in on) { // add listener if element was changed or new listeners added //直接將vnode上的事件處理器添加到elm上 elm.addEventListener ( name, listener, false ); } } else { for (name in on) { // add listener if new listener added //否則添加oldvnode上沒有的事件處理器 if ( !oldOn[ name ] ) { elm.addEventListener ( name, listener, false ); } } } } } module.exports = { create: updateEventListeners, update: updateEventListeners, destroy: updateEventListeners };props
主要功能:
從elm上刪除vnode中不存在的屬性
更新elm上的屬性
function updateProps(oldVnode, vnode) { var key, cur, old, elm = vnode.elm, oldProps = oldVnode.data.props, props = vnode.data.props; //如果新舊節(jié)點都不存在屬性,則直接返回 if (!oldProps && !props) return; oldProps = oldProps || {}; props = props || {}; //刪除舊節(jié)點中新節(jié)點沒有的屬性 for (key in oldProps) { if (!props[key]) { delete elm[key]; } } //更新屬性 for (key in props) { cur = props[key]; old = oldProps[key]; //如果新舊節(jié)點屬性不同,且對比的屬性不是value或者elm上對應屬性和新屬性也不同,那么就需要更新 if (old !== cur && (key !== "value" || elm[key] !== cur)) { elm[key] = cur; } } } module.exports = {create: updateProps, update: updateProps};style
主要功能如下:
將elm上存在于oldvnode中但不存在于vnode中不存在的style置空
如果vnode.style中的delayed與oldvnode的不同,則更新delayed的屬性值,并在下一幀將elm的style設置為該值,從而實現(xiàn)動畫過渡效果
非delayed和remove的style直接更新
vnode被destroy時,直接將對應style更新為vnode.data.style.destory的值
vnode被reomve時,如果style.remove不存在,直接調(diào)用全局remove鉤子進入下一個remove過程
如果style.remove存在,那么我們就需要設置remove動畫過渡效果,等到過渡效果結束之后,才調(diào)用
下一個remove過程
//如果存在requestAnimationFrame,則直接使用,以優(yōu)化性能,否則用setTimeout var raf = (typeof window !== "undefined" && window.requestAnimationFrame) || setTimeout; var nextFrame = function(fn) { raf(function() { raf(fn); }); }; //通過nextFrame來實現(xiàn)動畫效果 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; //如果oldvnode和vnode都沒有style,直接返回 if (!oldStyle && !style) return; oldStyle = oldStyle || {}; style = style || {}; var oldHasDel = "delayed" in oldStyle; //遍歷oldvnode的style for (name in oldStyle) { //如果vnode中無該style,則置空 if (!style[name]) { elm.style[name] = ""; } } //如果vnode的style中有delayed且與oldvnode中的不同,則在下一幀設置delayed的參數(shù) 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); } } } //如果不是delayed和remove的style,且不同于oldvnode的值,則直接設置新值 else if (name !== "remove" && cur !== oldStyle[name]) { elm.style[name] = cur; } } } //設置節(jié)點被destory時的style 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]; } } //刪除效果,當我們刪除一個元素時,先回調(diào)用刪除過度效果,過渡完才會將節(jié)點remove function applyRemoveStyle(vnode, rm) { var s = vnode.data.style; //如果沒有style或沒有style.remove if (!s || !s.remove) { //直接調(diào)用rm,即實際上是調(diào)用全局的remove鉤子 rm(); return; } var name, elm = vnode.elm, idx, i = 0, maxDur = 0, compStyle, style = s.remove, amount = 0, applied = []; //設置并記錄remove動作后刪除節(jié)點前的樣式 for (name in style) { applied.push(name); elm.style[name] = style[name]; } compStyle = getComputedStyle(elm); //拿到所有需要過渡的屬性 var props = compStyle["transition-property"].split(", "); //對過渡屬性計數(shù),這里applied.length >=amount,因為有些屬性是不需要過渡的 for (; i < props.length; ++i) { if(applied.indexOf(props[i]) !== -1) amount++; } //當過渡效果的完成后,才remove節(jié)點,調(diào)用下一個remove過程 elm.addEventListener("transitionend", function(ev) { if (ev.target === elm) --amount; if (amount === 0) rm(); }); } module.exports = {create: updateStyle, update: updateStyle, destroy: applyDestroyStyle, remove: applyRemoveStyle};第五站 is
啃完modules這些大部頭,總算有個比較好吃的甜品了,他主要功能就是判斷是否為array類型或者原始類型
//is工具庫,用于判斷是否為array或者原始類型 module.exports = { array: Array.isArray, primitive: function(s) { return typeof s === "string" || typeof s === "number"; }, };中途休息
看了這么多源碼,估計也累了吧,畢竟一下完全理解可能有點難,不妨先休息一下,消化一下,下一章將會見到最大的boss——snabbdom本身!
文章版權歸作者所有,未經(jīng)允許請勿轉載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/88234.html
摘要:前言在上一章我們學習了,等模塊,在這一篇我們將會學習到的核心功能和功能。如果父節(jié)點沒變化,我們就比較所有同層的子節(jié)點,對這些子節(jié)點進行刪除創(chuàng)建移位操作。只需要對兩個進行判斷是否相似,如果相似,則對他們進行操作,否則直接用替換。 前言 在上一章我們學習了,modules,vnode,h,htmldomapi,is等模塊,在這一篇我們將會學習到snabbdom的核心功能——patchVno...
摘要:閑聊在學的過程中,虛擬應該是聽的最多的概念之一,得知其是借鑒進行開發(fā),故習之。以我的觀點來看,多個相同元素渲染時,則需要為每個元素添加值。 閑聊:在學vue的過程中,虛擬dom應該是聽的最多的概念之一,得知其是借鑒snabbdom.js進行開發(fā),故習之。由于我工作處于IE8的環(huán)境,對ES6,TS這些知識的練習也只是淺嘗輒止,而snabbdom.js從v.0.5.4這個版本后開始使用TS...
摘要:司徒正美的一款了不起的化方案,支持到。行代碼內(nèi)實現(xiàn)一個胡子大哈實現(xiàn)的作品其實就是的了源碼學習個人文章源碼學習個人文章源碼學習個人文章源碼學習個人文章這幾片文章的作者都是司徒正美,全面的解析和官方的對比。 前言 在過去的一個多月中,為了能夠更深入的學習,使用React,了解React內(nèi)部算法,數(shù)據(jù)結構,我自己,從零開始寫了一個玩具框架。 截止今日,終于可以發(fā)布第一個版本,因為就在昨天,我...
摘要:閱讀源碼的時候,想了解虛擬結構的實現(xiàn),發(fā)現(xiàn)在的地方。然而慢慢的人們發(fā)現(xiàn),在我們的代碼中布滿了一系列操作的代碼。源碼解析系列源碼解析一準備工作源碼解析二函數(shù)源碼解析三對象源碼解析四方法源碼解析五鉤子源碼解析六模塊源碼解析七事件處理個人博客地址 前言 虛擬 DOM 結構概念隨著 react 的誕生而火起來,之后 vue2.0 也加入了虛擬 DOM 的概念。 閱讀 vue 源碼的時候,想了解...
閱讀 3479·2023-04-25 22:45
閱讀 1282·2021-11-11 16:54
閱讀 2790·2019-08-30 15:44
閱讀 3190·2019-08-30 15:44
閱讀 1646·2019-08-30 13:55
閱讀 941·2019-08-29 18:45
閱讀 1195·2019-08-29 17:25
閱讀 1007·2019-08-29 12:59