1. 程式人生 > >Vue檢視渲染原理解析,從構建VNode到生成真實節點樹

Vue檢視渲染原理解析,從構建VNode到生成真實節點樹

## 前言 在 `Vue` 核心中除了響應式原理外,檢視渲染也是重中之重。我們都知道每次更新資料,都會走檢視渲染的邏輯,而這當中牽扯的邏輯也是十分繁瑣。 本文主要解析的是初始化檢視渲染流程,你將會了解到從掛載元件開始,`Vue` 是如何構建 `VNode`,又是如何將 `VNode` 轉為真實節點並掛載到頁面。 ## 掛載元件($mount) `Vue` 是一個建構函式,通過 `new` 關鍵字進行例項化。 ```js // src/core/instance/index.js function Vue (options) { if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue) ) { warn('Vue is a constructor and should be called with the `new` keyword') } this._init(options) } ``` 在例項化時,會呼叫 `_init` 進行初始化。 ```js // src/core/instance/init.js Vue.prototype._init = function (options?: Object) { const vm: Component = this // ... if (vm.$options.el) { vm.$mount(vm.$options.el) } } ``` `_init` 內會呼叫 `$mount` 來掛載元件,而 `$mount` 方法實際呼叫的是 `mountComponent`。 ```js // src/core/instance/lifecycle.js export function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean ): Component { vm.$el = el // ... callHook(vm, 'beforeMount') let updateComponent /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { // ... } else { updateComponent = () => { vm._update(vm._render(), hydrating) // 渲染頁面函式 } } // we set this to vm._watcher inside the watcher's constructor // since the watcher's initial patch may call $forceUpdate (e.g. inside child // component's mounted hook), which relies on vm._watcher being already defined new Watcher(vm, updateComponent, noop, { // 渲染watcher before () { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */) 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 } ``` `mountComponent` 除了呼叫一些生命週期的鉤子函式外,最主要是 `updateComponent`,它就是負責渲染檢視的核心方法,其只有一行核心程式碼: ```js vm._update(vm._render(), hydrating) ``` `vm._render` 建立並返回 `VNode`,`vm._update` 接受 `VNode` 將其轉為真實節點。 `updateComponent` 會被傳入 `渲染Watcher`,每當資料變化觸發 `Watcher` 更新就會執行該函式,重新渲染檢視。`updateComponent` 在傳入 `渲染Watcher` 後會被執行一次進行初始化頁面渲染。 所以我們著重分析的是 `vm._render` 和 `vm._update` 兩個方法,這也是本文主要了解的原理——`Vue` 檢視渲染流程。 ## 構建VNode(_render) 首先是 `_render` 方法,它用來構建元件的 `VNode`。 ```js // src/core/instance/render.js Vue.prototype._render = function () { const { render, _parentVnode } = vm.$options vnode = render.call(vm._renderProxy, vm.$createElement) return vnode } ``` `_render` 內部會執行 `render` 方法並返回構建好的 `VNode`。`render` 一般是模板編譯後生成的方法,也有可能是使用者自定義。 ```js // src/core/instance/render.js export function initRender (vm) { vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false) vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true) } ``` `initRender` 在初始化就會執行為例項上繫結兩個方法,分別是 `vm._c` 和 `vm.$createElement`。它們兩者都是呼叫 `createElement` 方法,它是建立 `VNode` 的核心方法,最後一個引數用於區別是否為使用者自定義。 `vm._c` 應用場景是在編譯生成的 `render` 函式中呼叫,`vm.$createElement` 則用於使用者自定義 `render` 函式的場景。就像上面 `render` 在呼叫時會傳入引數 `vm.$createElement`,我們在自定義 `render` 函式接收到的引數就是它。 ### createElement ```js // src/core/vdom/create-elemenet.js export function createElement ( context: Component, tag: any, data: any, children: any, normalizationType: any, alwaysNormalize: boolean ): VNode | Array { if (Array.isArray(data) || isPrimitive(data)) { normalizationType = children children = data data = undefined } if (isTrue(alwaysNormalize)) { normalizationType = ALWAYS_NORMALIZE } return _createElement(context, tag, data, children, normalizationType) } ``` `createElement` 方法實際上是對 `_createElement` 方法的封裝,它允許傳入的引數更加靈活。 ```js export function _createElement ( context: Component, tag?: string | Class | Function | Object, data?: VNodeData, children?: any, normalizationType?: number ): VNode | Array { if (isDef(data) && isDef(data.is)) { tag = data.is } if (!tag) { // in case of component :is set to falsy value return createEmptyVNode() } // support single function children as default scoped slot if (Array.isArray(children) && typeof children[0] === 'function' ) { data = data || {} data.scopedSlots = { default: children[0] } children.length = 0 } if (normalizationType === ALWAYS_NORMALIZE) { children = normalizeChildren(children) } else if (normalizationType === SIMPLE_NORMALIZE) { children = simpleNormalizeChildren(children) } let vnode, ns if (typeof tag === 'string') { let Ctor ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag) if (config.isReservedTag(tag)) { // platform built-in elements vnode = new VNode( config.parsePlatformTagName(tag), data, children, undefined, undefined, context ) } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) { // component vnode = createComponent(Ctor, data, context, children, tag) } else { // unknown or unlisted namespaced elements // check at runtime because it may get assigned a namespace when its // parent normalizes children vnode = new VNode( tag, data, children, undefined, undefined, context ) } } else { // direct component options / constructor vnode = createComponent(tag, data, context, children) } if (Array.isArray(vnode)) { return vnode } else if (isDef(vnode)) { if (isDef(ns)) applyNS(vnode, ns) if (isDef(data)) registerDeepBindings(data) return vnode } else { return createEmptyVNode() } } ``` `_createElement` 引數中會接收 `children`,它表示當前 `VNode` 的子節點,因為它是任意型別的,所以接下來需要將其規範為標準的 `VNode` 陣列; ```js // 這裡規範化 children if (normalizationType === ALWAYS_NORMALIZE) { children = normalizeChildren(children) } else if (normalizationType === SIMPLE_NORMALIZE) { children = simpleNormalizeChildren(children) } ``` `simpleNormalizeChildren` 和 `normalizeChildren` 均用於規範化 `children`。由 `normalizationType` 判斷 `render` 函式是編譯生成的還是使用者自定義的。 ```js // 1. When the children contains components - because a functional component // may return an Array instead of a single root. In this case, just a simple // normalization is needed - if any child is an Array, we flatten the whole // thing with Array.prototype.concat. It is guaranteed to be only 1-level deep // because functional components already normalize their own children. export function simpleNormalizeChildren (children: any) { for (let i = 0; i < children.length; i++) { if (Array.isArray(children[i])) { return Array.prototype.concat.apply([], children) } } return children } // 2. When the children contains constructs that always generated nested Arrays, // e.g.