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相关部分的源码。

相关文章
相关标签/搜索