Vue 2.3.4原始碼分析之檢視渲染
最近公司一直使用Vue 2進行前端開發,正好趁最近時間空閒,閱讀了一下vue原始碼。現在我看原始碼的理解記錄如下。這篇博文主要分析檢視渲染。
Vue 2檢視渲染主要使用虛擬DOM實現的,流程如下:
1.在$mount方法中,首先判斷是否提供了render方法,如果沒有就需要解析模板生成一個render方法。
Vue$3.prototype.$mount = function ( el, hydrating ) { el = el && query(el); /* istanbul ignore if */ if (el === document.body || el === document.documentElement) { "development" !== 'production' && warn( "Do not mount Vue to <html> or <body> - mount to normal elements instead." ); return this } var options = this.$options; // resolve template/el and convert to render function if (!options.render) {//判斷是否提供了render方法 var template = options.template; if (template) {//判斷是否提供了template模板 if (typeof template === 'string') { if (template.charAt(0) === '#') { template = idToTemplate(template); /* istanbul ignore if */ if ("development" !== 'production' && !template) { warn( ("Template element not found or is empty: " + (options.template)), this ); } } } else if (template.nodeType) { template = template.innerHTML; } else { { warn('invalid template option:' + template, this); } return this } } else if (el) { template = getOuterHTML(el);//如果沒有提供template就根據el.outerHTML生成一個template } if (template) { /* istanbul ignore if */ if ("development" !== 'production' && config.performance && mark) { mark('compile'); } var ref = compileToFunctions(template, { shouldDecodeNewlines: shouldDecodeNewlines, delimiters: options.delimiters }, this); var render = ref.render;//將模板解析生成的render方法 var staticRenderFns = ref.staticRenderFns; options.render = render; options.staticRenderFns = staticRenderFns; /* istanbul ignore if */ if ("development" !== 'production' && config.performance && mark) { mark('compile end'); measure(((this._name) + " compile"), 'compile', 'compile end'); } } } return mount.call(this, el, hydrating) };
在該方法中,如果既沒有提供render,也沒有提供template,那麼會根據我們傳入的id,通過document.querySelector('#id').outerHTML生成一個template。所以render方法有三種方式得到,一種是我們程式設計師直接提供,這種方式不需要編譯模板,最接近編譯器,第二種就是我們提供一個template模組,第三種是我們不提供render和template,直接根據我們傳入的id生成template。
拿到render方法後,vm._render會根據render方法生成VNode,Vnode就是虛擬DOM,然後呼叫vm._update建立真實的DOM
function mountComponent ( vm, el, hydrating ) { vm.$el = el; if (!vm.$options.render) { vm.$options.render = createEmptyVNode; { /* istanbul ignore if */ if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') || vm.$options.el || el) { warn( 'You are using the runtime-only build of Vue where the template ' + 'compiler is not available. Either pre-compile the templates into ' + 'render functions, or use the compiler-included build.', vm ); } else { warn( 'Failed to mount component: template or render function not defined.', vm ); } } } callHook(vm, 'beforeMount'); var updateComponent; /* istanbul ignore if */ if ("development" !== 'production' && config.performance && mark) { updateComponent = function () { var name = vm._name; var id = vm._uid; var startTag = "vue-perf-start:" + id; var endTag = "vue-perf-end:" + id; mark(startTag); var vnode = vm._render(); mark(endTag); measure((name + " render"), startTag, endTag); mark(startTag); vm._update(vnode, hydrating); mark(endTag); measure((name + " patch"), startTag, endTag); }; } else { updateComponent = function () { vm._update(vm._render(), hydrating);//vm._render()會根據render方法生成VNode,vm._update會根據VNode生成真是的DOM }; } vm._watcher = new Watcher(vm, updateComponent, noop); hydrating = false; // manually mounted instance, call mounted on self // mounted is called for render-created child components in its inserted hook if (vm.$vnode == null) { vm._isMounted = true; callHook(vm, 'mounted'); } return vm }
生成真實的DOM分為兩種情況:
1.如果是初始化時,會直接呼叫createElm建立新真實的DOM;
2.如果是DOM的更新的話,需要進行Vnode的diff比較。首先會呼叫sameVnode(oldVnode,vnode)方法對新老vnode進行基本屬性的比較,如果基本屬性一樣,說明是區域性重新整理,就進行diff比較,diff主要是patchVnode方法實現的,
function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) { if (oldVnode === vnode) { return } var elm = vnode.elm = oldVnode.elm; if (isTrue(oldVnode.isAsyncPlaceholder)) { if (isDef(vnode.asyncFactory.resolved)) { hydrate(oldVnode.elm, vnode, insertedVnodeQueue); } else { vnode.isAsyncPlaceholder = true; } return } // reuse element for static trees. // note we only do this if the vnode is cloned - // if the new node is not cloned it means the render functions have been // reset by the hot-reload-api and we need to do a proper re-render. if (isTrue(vnode.isStatic) && isTrue(oldVnode.isStatic) && vnode.key === oldVnode.key && (isTrue(vnode.isCloned) || isTrue(vnode.isOnce)) ) { vnode.componentInstance = oldVnode.componentInstance; return } var i; var data = vnode.data; if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) { i(oldVnode, vnode); } var oldCh = oldVnode.children; var ch = vnode.children; if (isDef(data) && isPatchable(vnode)) { for (i = 0; i < cbs.update.length; ++i) { cbs.update[i](oldVnode, vnode); }//進行attrs,class,domlisener,style等更新 if (isDef(i = data.hook) && isDef(i = i.update)) { i(oldVnode, vnode); } } if (isUndef(vnode.text)) { if (isDef(oldCh) && isDef(ch)) { if (oldCh !== ch) { updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly); }//如果新老vnode的子節點陣列不相同,就執行updateChildren方法 } else if (isDef(ch)) {//如果新vnode有子節點,老vnode沒有,就新增新vnodes if (isDef(oldVnode.text)) { nodeOps.setTextContent(elm, ''); } addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue); } else if (isDef(oldCh)) { removeVnodes(elm, oldCh, 0, oldCh.length - 1);//如果新vnode沒有子節點,老vnode有,就刪除老vnodes } else if (isDef(oldVnode.text)) { nodeOps.setTextContent(elm, ''); } } else if (oldVnode.text !== vnode.text) { nodeOps.setTextContent(elm, vnode.text);//如果text不相同,更新text } if (isDef(data)) { if (isDef(i = data.hook) && isDef(i = i.postpatch)) { i(oldVnode, vnode); } } }
在patchVnode中最後還執行了如下操作,將新vnode進行相關屬性的更新,最後執行createElm更新dom
當然,data屬性變化並不是立即更新dom,而是會延時批量更新,首頁判斷是否相容Promise,否則判斷是否相容MutationOberver,如果都不相容就是要setTimeout延時。但Promise和MutationOberver的效能要好於setTimeout(詳情請參考:https://blog.csdn.net/zhoulu001/article/details/79531969)。
/**
* Defer a task to execute it asynchronously.
*/
var nextTick = (function () {
var callbacks = [];
var pending = false;
var timerFunc;
function nextTickHandler () {
pending = false;
var copies = callbacks.slice(0);
callbacks.length = 0;
for (var i = 0; i < copies.length; i++) {
copies[i]();
}
}
// the nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
/* istanbul ignore if */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
var p = Promise.resolve();
var logError = function (err) { console.error(err); };
timerFunc = function () {
p.then(nextTickHandler).catch(logError);
// in problematic UIWebViews, Promise.then doesn't completely break, but
// it can get stuck in a weird state where callbacks are pushed into the
// microtask queue but the queue isn't being flushed, until the browser
// needs to do some other work, e.g. handle a timer. Therefore we can
// "force" the microtask queue to be flushed by adding an empty timer.
if (isIOS) { setTimeout(noop); }
};
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// use MutationObserver where native Promise is not available,
// e.g. PhantomJS, iOS7, Android 4.4
var counter = 1;
var observer = new MutationObserver(nextTickHandler);
var textNode = document.createTextNode(String(counter));
observer.observe(textNode, {
characterData: true
});
timerFunc = function () {
counter = (counter + 1) % 2;
textNode.data = String(counter);
};
} else {
// fallback to setTimeout
/* istanbul ignore next */
timerFunc = function () {
setTimeout(nextTickHandler, 0);
};
}