引言
在上一篇文章的結尾,我們提到了在$mount
函式的最後呼叫了mountComponent
函式,而mountComponent
函式內又定義了updateComponent
函式:
// src/core/instance/lifecycle.js
updateComponent = () => {
vm._update(vm._render(), hydrating);
};
複製程式碼
這裡面涉及到_update
和_render
兩個函式。本篇文章我們先來分析一下_render
函式。
_render
Vue
的 _render
方法是例項的一個私有方法,它用來把例項渲染成一個虛擬 Node
。定義在 src/core/instance/render.js
檔案中:
Vue.prototype._render = function(): VNode {
const vm: Component = this;
const { render, _parentVnode } = vm.$options;
if (_parentVnode) {
vm.$scopedSlots = normalizeScopedSlots(
_parentVnode.data.scopedSlots,
vm.$slots,
vm.$scopedSlots
);
}
// set parent vnode. this allows render functions to have access
// to the data on the placeholder node.
vm.$vnode = _parentVnode;
// render self
let vnode;
try {
// There's no need to maintain a stack because all render fns are called
// separately from one another. Nested component's render fns are called
// when parent component is patched.
currentRenderingInstance = vm;
vnode = render.call(vm._renderProxy, vm.$createElement);
} catch (e) {
handleError(e, vm, `render`);
// return error render result,
// or previous vnode to prevent render error causing blank component
/* istanbul ignore else */
if (process.env.NODE_ENV !== "production" && vm.$options.renderError) {
try {
vnode = vm.$options.renderError.call(
vm._renderProxy,
vm.$createElement,
e
);
} catch (e) {
handleError(e, vm, `renderError`);
vnode = vm._vnode;
}
} else {
vnode = vm._vnode;
}
} finally {
currentRenderingInstance = null;
}
// if the returned array contains only a single node, allow it
if (Array.isArray(vnode) && vnode.length === 1) {
vnode = vnode[0];
}
// return empty vnode in case the render function errored out
if (!(vnode instanceof VNode)) {
if (process.env.NODE_ENV !== "production" && Array.isArray(vnode)) {
warn(
"Multiple root nodes returned from render function. Render function " +
"should return a single root node.",
vm
);
}
vnode = createEmptyVNode();
}
// set parent
vnode.parent = _parentVnode;
return vnode;
};
複製程式碼
這段程式碼最關鍵的是render
方法的呼叫。我們先來看一下這段程式碼:
vnode = render.call(vm._renderProxy, vm.$createElement);
複製程式碼
這裡的vm._renderProxy
是什麼呢?
vm._renderProxy
回顧new Vue發生了什麼?,我們介紹了_init
函式,其中有這麼一段程式碼:
// src/core/instance/init.js
Vue.prototype._init = function(options?: Object) {
//...
/* istanbul ignore else */
if (process.env.NODE_ENV !== "production") {
initProxy(vm);
} else {
vm._renderProxy = vm;
}
// ...
};
複製程式碼
表示在生產環境下,vm._renderProxy
就是vm
本身;在開發環境下則呼叫initProxy
方法,將vm
作為引數傳入,來看下initProxy
函式:
// src/core/instance/proxy.js
let initProxy;
initProxy = function initProxy(vm) {
if (hasProxy) {
// determine which proxy handler to use
const options = vm.$options;
const handlers =
options.render && options.render._withStripped ? getHandler : hasHandler;
vm._renderProxy = new Proxy(vm, handlers);
} else {
vm._renderProxy = vm;
}
};
複製程式碼
hasProxy
是什麼呢?看下對它的定義:
// src/core/instance/proxy.js
const hasProxy = typeof Proxy !== "undefined" && isNative(Proxy);
複製程式碼
很簡單,就是判斷一下瀏覽器是否支援Proxy
。
如果支援就建立一個Proxy
物件賦給vm._renderProxy
;不支援就和生產環境一樣直接使用vm._renderProxy
。
如果是在開發環境下並且瀏覽器支援Proxy
的情況下,會建立一個Proxy
物件,這裡的第二個引數handlers
,它的定義是:
// src/core/instance/proxy.js
const handlers =
options.render && options.render._withStripped ? getHandler : hasHandler;
複製程式碼
handlers
,是負責定義代理行為的物件。options.render._withStripped
的取值一般情況下都是false
,所以handlers
的取值為hasHandler
我們來看下hasHandler
:
// src/core/instance/proxy.js
const hasHandler = {
has(target, key) {
const has = key in target;
const isAllowed =
allowedGlobals(key) ||
(typeof key === "string" &&
key.charAt(0) === "_" &&
!(key in target.$data));
if (!has && !isAllowed) {
if (key in target.$data) warnReservedPrefix(target, key);
else warnNonPresent(target, key);
}
return has || !isAllowed;
}
};
複製程式碼
hasHandler
物件裡面定義了一個has
函式。has
函式的執行邏輯是求出屬性查詢的結果然後存入 has
,下面的 isAllowed
涉及到一個函式 allowedGlobals
,來看看這個函式:
// src/core/instance/proxy.js
const allowedGlobals = makeMap(
"Infinity,undefined,NaN,isFinite,isNaN," +
"parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent," +
"Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl," +
"require" // for Webpack/Browserify
);
複製程式碼
這裡傳入了各種js
的全域性屬性、函式作為makeMap
的引數,其實很容易看出來,allowedGlobals
就是檢查key
是不是這些全域性的屬性、函式其中的任意一個。
所以isAllowed
為true
的條件就是key
是js全域性關鍵字
或者非vm.$data
下的以_
開頭的字串。
如果!has
(訪問的key
在vm
不存在)和!isAllowed
同時成立的話,進入if
語句。這裡面有兩種情況,分別對應兩個不同的警告,先來看第一個:
// src/core/instance/proxy.js
const warnReservedPrefix = (target, key) => {
warn(
`Property "${key}" must be accessed with "$data.${key}" because ` +
'properties starting with "$" or "_" are not proxied in the Vue instance to ' +
"prevent conflicts with Vue internals. " +
"See: https://vuejs.org/v2/api/#data",
target
);
};
複製程式碼
警告資訊的大致意思是: 在Vue
中,以$
或_
開頭的屬性不會被代理,因為有可能與內建屬性產生衝突。如果你設定的屬性以$
或_
開頭,那麼不能直接通過vm.key
這種形式訪問,而是需要通過vm.$data.key
來訪問。
第二個警告是針對我們的key
沒有在data
中定義:
// src/core/instance/proxy.js
const warnNonPresent = (target, key) => {
warn(
`Property or method "${key}" is not defined on the instance but ` +
'referenced during render. Make sure that this property is reactive, ' +
'either in the data option, or for class-based components, by ' +
'initializing the property. ' +
'See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.',
target
)
}
複製程式碼
這個報錯資訊,我想你一定不陌生。就是這種:
到這裡,我們就大致把vm._renderProxy
分析完成了,回到上文中這一行程式碼:
vnode = render.call(vm._renderProxy, vm.$createElement);
複製程式碼
我們再來看下vm.$createElement
。
vm.$createElement
vm.$createElement
的定義是在initRender
函式中:
function initRender(vm: Component) {
// ...
// bind the createElement fn to this instance
// so that we get proper render context inside it.
// args order: tag, data, children, normalizationType, alwaysNormalize
// internal version is used by render functions compiled from templates
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false);
// normalization is always applied for the public version, used in
// user-written render functions.
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true);
// ...
}
複製程式碼
這裡我們先省略其他部分程式碼,只關注中間這兩行。這兩行是分別給例項vm
加上_c
和$createElement
方法。這兩個方法都呼叫了createElement
方法,只是最後一個引數值不同。
從註釋可以很清晰的看出兩者的不同,vm._c
是內部函式,它是被模板編譯成的 render
函式使用;而 vm.$createElement
是提供給使用者編寫的 render
函式使用。
為了更好的理解這兩個函式,下面看兩個例子:
如果我們手動編寫render
函式,通常是這樣寫的:
<div id="app"></div>
複製程式碼
<script>
render: function (createElement) {
return createElement('div', {
attrs: {
id: 'app'
},
}, this.message)
},
data() {
return {
message: '森林小哥哥'
}
}
</script>
複製程式碼
這裡我們編寫的 render
函式的引數 createElement
其實就是 vm.$createElement,所以我也可以這麼寫:
render: function () {
return this.$createElement('div', {
attrs: {
id: 'app'
},
}, this.message)
},
data() {
return {
message: '森林小哥哥'
}
}
複製程式碼
如果我們使用字串模版,那麼是這樣寫的:
<div id="app">{{ message }}</div>
<script>
var app = new Vue({
el: "#app",
data() {
return {
message: "森林小哥哥"
};
}
});
</script>
複製程式碼
這種使用字串模板的情況,使用的就是vm._c
了。
使用字串模板的話,在相關程式碼執行完前,會先在頁面顯示
{{ message }}
,然後再展示森林小哥哥
;而我們手動編寫render
函式的話,根據上一節的分析,內部就不用執行把字串模板轉換成render
函式這個操作,並且是空白頁面之後立即就顯示森林小哥哥
,使用者體驗會更好。
我們重新回顧下_render
函式:
// src/core/instance/render.js
Vue.prototype._render = function(): VNode {
const vm: Component = this;
const { render, _parentVnode } = vm.$options;
if (_parentVnode) {
vm.$scopedSlots = normalizeScopedSlots(
_parentVnode.data.scopedSlots,
vm.$slots,
vm.$scopedSlots
);
}
// set parent vnode. this allows render functions to have access
// to the data on the placeholder node.
vm.$vnode = _parentVnode;
// render self
let vnode;
try {
// There's no need to maintain a stack because all render fns are called
// separately from one another. Nested component's render fns are called
// when parent component is patched.
currentRenderingInstance = vm;
vnode = render.call(vm._renderProxy, vm.$createElement);
} catch (e) {
handleError(e, vm, `render`);
// return error render result,
// or previous vnode to prevent render error causing blank component
/* istanbul ignore else */
if (process.env.NODE_ENV !== "production" && vm.$options.renderError) {
try {
vnode = vm.$options.renderError.call(
vm._renderProxy,
vm.$createElement,
e
);
} catch (e) {
handleError(e, vm, `renderError`);
vnode = vm._vnode;
}
} else {
vnode = vm._vnode;
}
} finally {
currentRenderingInstance = null;
}
// if the returned array contains only a single node, allow it
if (Array.isArray(vnode) && vnode.length === 1) {
vnode = vnode[0];
}
// return empty vnode in case the render function errored out
if (!(vnode instanceof VNode)) {
if (process.env.NODE_ENV !== "production" && Array.isArray(vnode)) {
warn(
"Multiple root nodes returned from render function. Render function " +
"should return a single root node.",
vm
);
}
vnode = createEmptyVNode();
}
// set parent
vnode.parent = _parentVnode;
return vnode;
};
複製程式碼
這裡vm.$createElement
被作為引數給了render
函式,最後會返回一個VNode
,我們直接跳過catch
和finally
,來到最後。
判斷vnode
是陣列並且長度為 1 的情況下,直接取第一項。
如果vnode
不是VNode
型別(一般是由於使用者編寫不規範導致渲染函式出錯),就去判斷vnode
是不是陣列,如果是的話丟擲警告(說明使用者的template
包含了多個根節點)。並建立一個空的VNode
給到vnode
。最後返回vnode
。
總結
到這裡,_render
函式的大致流程就分析完成了。vm._render
最終是通過執行 createElement
方法並返回的是 vnode
,它是一個虛擬 Node
。Vue 2.0
相比 Vue 1.0
最大的升級就是利用了 Virtual DOM
。
最後呢,我先丟擲一個問題給到大家:為什麼 Vue
要限制 template
只能有一個根節點呢?
其實這個問題是與上文最後提到的VNode
和Virtual DOM
相關的。下一篇文章中呢,我將帶大家一塊來看下Virtual DOM
相關部分的原始碼。