Vue源碼探祕(五)(_render 函數的實現)

引言

上一篇文章的結尾,咱們提到了在$mount函數的最後調用了mountComponent函數,而mountComponent函數內又定義了updateComponent函數:html

// src/core/instance/lifecycle.js
updateComponent = () => {
  vm._update(vm._render(), hydrating);
};
複製代碼

這裏面涉及到_update_render兩個函數。本篇文章咱們先來分析一下_render函數。vue

_render

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

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是什麼呢?看下對它的定義:bash

// src/core/instance/proxy.js
const hasProxy = typeof Proxy !== "undefined" && isNative(Proxy);
複製代碼

很簡單,就是判斷一下瀏覽器是否支持Proxyapp

若是支持就建立一個Proxy對象賦給vm._renderProxy;不支持就和生產環境同樣直接使用vm._renderProxyide

若是是在開發環境下而且瀏覽器支持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是否是這些全局的屬性、函數其中的任意一個。

因此isAllowedtrue的條件就是keyjs全局關鍵字或者非vm.$data下的以_開頭的字符串。

若是!has(訪問的keyvm不存在)和!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,咱們直接跳過catchfinally,來到最後。

判斷vnode是數組而且長度爲 1 的狀況下,直接取第一項。

若是vnode不是VNode類型(通常是因爲用戶編寫不規範致使渲染函數出錯),就去判斷vnode是否是數組,若是是的話拋出警告(說明用戶的template包含了多個根節點)。並建立一個空的VNode給到vnode。最後返回vnode

總結

到這裏,_render函數的大體流程就分析完成了。vm._render 最終是經過執行 createElement 方法並返回的是 vnode,它是一個虛擬 NodeVue 2.0 相比 Vue 1.0 最大的升級就是利用了 Virtual DOM

最後呢,我先拋出一個問題給到你們:爲何 Vue 要限制 template 只能有一個根節點呢?

其實這個問題是與上文最後提到的VNodeVirtual DOM相關的。下一篇文章中呢,我將帶你們一塊來看下Virtual DOM相關部分的源碼。

相關文章
相關標籤/搜索