Cosen95 / blog

关注行业前沿,分享所见所学。持续输出优质文章 :rocket:
212 stars 15 forks source link

Vue源码探秘(_render 函数) #29

Open Cosen95 opened 4 years ago

Cosen95 commented 4 years ago

引言

在上一篇文章的结尾,我们提到了在$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

在之前的文章中,我有介绍_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
  )
}

这个报错信息,我想你一定不陌生。就是这种:

到这里,我们就大致把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相关部分的源码。

zhzhch335 commented 2 years ago

........所以为啥写了两遍

Cosen95 commented 2 years ago

........所以为啥写了两遍

已经去除重复部分了🐶