在上一篇文章的結尾,咱們提到了在$mount
函數的最後調用了mountComponent
函數,而mountComponent
函數內又定義了updateComponent
函數:html
// src/core/instance/lifecycle.js
updateComponent = () => {
vm._update(vm._render(), hydrating);
};
複製代碼
這裏面涉及到_update
和_render
兩個函數。本篇文章咱們先來分析一下_render
函數。vue
Vue
的 _render
方法是實例的一個私有方法,它用來把實例渲染成一個虛擬 Node
。定義在 src/core/instance/render.js
文件中:node
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
方法的調用。咱們先來看一下這段代碼:react
vnode = render.call(vm._renderProxy, vm.$createElement);
複製代碼
這裏的vm._renderProxy
是什麼呢?api
回顧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
是什麼呢?看下對它的定義:bash
// src/core/instance/proxy.js
const hasProxy = typeof Proxy !== "undefined" && isNative(Proxy);
複製代碼
很簡單,就是判斷一下瀏覽器是否支持Proxy
。app
若是支持就建立一個Proxy
對象賦給vm._renderProxy
;不支持就和生產環境同樣直接使用vm._renderProxy
。ide
若是是在開發環境下而且瀏覽器支持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
的定義是在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
相關部分的源碼。