Vue原始碼探祕(五)(_render 函式的實現)

前端森林發表於2020-03-23

引言

上一篇文章的結尾,我們提到了在$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是不是這些全域性的屬性、函式其中的任意一個。

所以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
  )
}
複製程式碼

這個報錯資訊,我想你一定不陌生。就是這種:

Vue原始碼探祕(五)(_render 函式的實現)

到這裡,我們就大致把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相關部分的原始碼。

Vue原始碼探祕(五)(_render 函式的實現)

相關文章