回顧
在上一系列中,我麼已經瞭解到$mount是如何工作的,最後通過呼叫 updateComponent方法來執行vm._update(vm._render(), hydrating);
,接下來我們來分析一下 vm._render()方法是如何執行的。
手寫 render 方法
首先來看一下使用template的形式,
new Vue({
el: '#app',
template: '<h1>hello world</h1>'
})
// 或者這種方式
new Vue({
el: '#app',
components: { App },
template: '<App/>'
})
複製程式碼
那麼,我們如何手寫 render 函式呢?仔細觀察下面這段程式碼,試想一下這裡的 createElement 引數是什麼 。
new Vue({
el: '#app',
render(createElement) {
return createElement('div', {
attrs: {
id: 'app1' //注意這裡的id 是 app1
}
}, this.value)
},
data() {
return {
value: 'render function'
}
}
})
複製程式碼
來看頁面效果:
請注意上面紅框框的 id, 是我們剛剛定義的 'app1'。所以我們為什麼不能將元素繫結在body/html這些元素上就知道了吧,因為它會替換掉頁面上的元素。
好了,進入正題,開始分析原始碼。
_render
Vue 的 _render 方法是例項的一個私有方法,它用來把例項渲染成一個虛擬 Node。它的定義在 src/core/instance/render.js 檔案中:
注意,在分析原始碼的時候一定要走主線,不能一次性將某一個方法全看完或看透徹,因為它可能依賴了很多其它變數或者方法,如果都去分析,可能到後面看著看著就什麼都看不懂了。在這裡我們只分析 render 方法,像下面的 _parentVnode、$slots、$vnode 等等在後面的系列章節中再分析。
Flow 是 facebook 出品的 JavaScript 靜態型別檢查工具。Vue.js 的原始碼利用了 Flow 做了靜態型別檢查,
function (): VNode
表示這個方法的返回值是一個 vnode。
在這段程式碼中,主要研究這一段程式碼
render.call(vm._renderProxy, vm.$createElement)
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
// 下面這兩個 if 先不用看
// reset _rendered flag on slots for duplicate slot check
if (process.env.NODE_ENV !== 'production') {
for (const key in vm.$slots) {
// $flow-disable-line
vm.$slots[key]._rendered = false
}
}
if (_parentVnode) {
vm.$scopedSlots = _parentVnode.data.scopedSlots || emptyObject
}
// 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 {
// 分析主線
// 從這裡我們可以看出,手寫render方法中的createElement引數就是 vm.$createElement方法
vnode = render.call(vm._renderProxy, vm.$createElement)
// 這個render 是 vm.$options.render
// vm._renderProxy 如果在生產環境下,其實就是 vm
// 如果在開發環境下,就是 Proxy 物件(ES6中的API,不瞭解的話可以去看看)
// vm.$createElement 定義在 initRender 函式中,初始化的時候定義的
// 手寫 render 函式建立 vnode 的方法
// vm.$createElement = function (a, b, c, d) {
// return createElement(vm, a, b, c, d, true);
// };
// 如果是編譯生成的render函式,建立vnode的方法則是下面這個方法
// vm._c = function (a, b, c, d) { return createElement(vm, a, b, c, d, false); };
} 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') {
if (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
}
} else {
vnode = vm._vnode
}
}
// 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
}
}
複製程式碼
_renderProxy 是什麼 ?
這個方法在初始化的時候就定義好了,在 initMixin中的 Vue.prototype._init() 方法中,我們來看一下是如何定義的:
if (process.env.NODE_ENV !== 'production') {
// 如果是開發環境,就呼叫 initProxy 方法
initProxy(vm);
} else {
// 否則就是 vm 例項
vm._renderProxy = vm;
}
複製程式碼
Proxy物件
描述: Proxy 可以理解成,在目標物件之前架設一層“攔截”,外界對該物件的訪問,都必須先通過這層攔截,因此提供了一種機制,可以對外界的訪問進行過濾和改寫。Proxy 這個詞的原意是代理,用在這裡表示由它來“代理”某些操作,可以譯為“代理器”。
- 詳情 戳 這 裡
initProxy 方法
路徑: src/core/instance/proxy.js
// proxy.js中還有一些其它方法,這裡我們只看 initProxy方法
let initProxy
if (process.env.NODE_ENV !== 'production') {
// 判斷我們的瀏覽器支不支援proxy方法
const hasProxy =
typeof Proxy !== 'undefined' && isNative(Proxy)
if (hasProxy) {
const isBuiltInModifier = makeMap('stop,prevent,self,ctrl,shift,alt,meta,exact')
config.keyCodes = new Proxy(config.keyCodes, {
set (target, key, value) {
if (isBuiltInModifier(key)) {
warn(`Avoid overwriting built-in modifier in config.keyCodes: .${key}`)
return false
} else {
target[key] = value
return true
}
}
})
}
var hasHandler = {
has: function has (target, key) {
var has = key in target;
var isAllowed = allowedGlobals(key) || key.charAt(0) === '_';
if (!has && !isAllowed) {
// 這個警告就是當你使用了沒有定義的變數或者方法時呼叫的方法
// 方法就不列出來了。
warnNonPresent(target, key);
}
return has || !isAllowed
}
};
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
// options.render上沒有_withStripped,所以handlers 就是hasHandler
vm._renderProxy = new Proxy(vm, handlers)
} else {
vm._renderProxy = vm
}
}
}
export { initProxy }
複製程式碼
最後
當執行完了 _initProxy 之後,再回到 render 方法中看如下這段程式碼
if (!(vnode instanceof VNode)) {
if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
// render function 返回了多個 vnode 根節點
// 應當返回單一 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
return vnode
複製程式碼
總結
vm._render 最終是通過執行 createElement 方法並返回的是 vnode,那這個 createElement 是如何建立 vnode 的呢 ? 在後面的兩個系列中,我會先介紹 virtual DOM 的概念,然後再分析 createElement 的實現。