Vue render 函数使用以及原理

Vue.js 使用了基于 HTML 的模板语法,所以我们一般直接写模板,例如:

template: '<div style="width: 200px; height: 200px; border: 5px solid red">{{msg}}</div>'

当然我们也可以直接写render函数。下面我们就看看怎么用render函数达到相同的效果。

<html>
 
<head>
<style type="text/css">

</style>
</head>
 
<body>
    <script src="./vue.js"></script>

    <div id="app">
        <my-render-comp :msg="message"></my-render-comp>
    </div>
    
<script>

    Vue.component('myRenderComp', {
        props : ["msg"],
        //template: '<div style="width: 200px; height: 200px; border: 5px solid red">{{msg}}</div>'
        render(createElement){
            return createElement(
                "div",
                {
                    style : {
                        width: "200px",
                        height: "200px",
                        border: "5px solid red"                        
                    }
                },
                this.msg
            )
        }
    })

    const app = new Vue({
        data(){
            return{
                message : "my render component"
            }
        }
    }).$mount("#app");

</script>
</body>
</html>

效果如下:

代码很简单,关键是render函数的参数createElement, 此参数是个函数,是用来创建Vnode的函数。而这个函数有四个参数,我们引用Vue官网上对这4个参数的描述:

// @returns {VNode}
createElement(
  // {String | Object | Function}
  // 一个 HTML 标签名、组件选项对象,或者
  // resolve 了上述任何一种的一个 async 函数。必填项。
  'div',

  // {Object}
  // 一个与模板中属性对应的数据对象。可选。
  {
    // (详情见下一节)
  },

  // {String | Array}
  // 子级虚拟节点 (VNodes),由 `createElement()` 构建而成,
  // 也可以使用字符串来生成“文本虚拟节点”。可选。
  [
    '先写一些文字',
    createElement('h1', '一则头条'),
    createElement(MyComponent, {
      props: {
        someProp: 'foobar'
      }
    })
  ]
)

下面我们深入源码看看怎么使用render函数的。

    Vue.prototype._render = function () {
      var vm = this;
      var ref = vm.$options;
      var render = ref.render; //在这里取出的就是组件中的render函数
      var _parentVnode = ref._parentVnode;

      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
      var vnode;
      try {
        // There's no need to maintain a stack becaues 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); //把vm.$createElement 
                         //传递给render函数的参数,且render函数的上下文是vm,render函数要返回
                         //vnode
      } catch (e) {
        handleError(e, vm, "render");
        //some code
      }

vm.$createElement 传递给了render函数的参数,在render函数中要调用vm.$createElement函数。

vm.$createElement = function (a, b, c, d) { return createElement(vm, a, b, c, d, true); };

  function createElement (
    context,
    tag,
    data,
    children,
    normalizationType,
    alwaysNormalize
  ) {
    //在这里判断data如果是数组或简单类型变量,则认为data不存在,把data赋值给第三个参数children
    if (Array.isArray(data) || isPrimitive(data)) {
      normalizationType = children;
      children = data;
      data = undefined;
    }
    if (isTrue(alwaysNormalize)) {
      normalizationType = ALWAYS_NORMALIZE;
    }
    console.log("liubbc data is: " + JSON.stringify(data));
    return _createElement(context, tag, data, children, normalizationType)
  }
  function _createElement (
    context,
    tag,
    data,
    children,
    normalizationType
  ) {

  // some code
    if (typeof tag === 'string') {
      var 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 ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 
                 'components', tag))) {
        // component 不是元素节点,而是组件节点my-render-comp,所以走这个else if分支
        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);
    }
  

走到这里就和用模板一样了。render函数省去了模板编译步骤这一步。模板最终也是编译成render函数。

下面我们看看render函数中,createElement函数中的第二个参数是怎么处理的。

                {
                    style : {
                        width: "200px",
                        height: "200px",
                        border: "5px solid red"                        
                    }
                },

从Vue官方网站上看到这个参数是object类型。对象属性可以是style,class等。我们从源码看看,对象属性还可以有哪些。

其实在render函数生成Vnode阶段只是把这个参数传递给Vnode对象的data属性,在基于Vnode创建真实Dom阶段会处理这个data属性,之前文章也分析过基于Vnode创建真实Dom过程,我们就直接看createElm函数了。

    function createElm (
      vnode,
      insertedVnodeQueue,
      parentElm,
      refElm,
      nested,
      ownerArray,
      index
    ) {
      if (isDef(vnode.elm) && isDef(ownerArray)) {
        // This vnode was used in a previous render!
        // now it's used as a new node, overwriting its elm would cause
        // potential patch errors down the road when it's used as an insertion
        // reference node. Instead, we clone the node on-demand before creating
        // associated DOM element for it.
        vnode = ownerArray[index] = cloneVNode(vnode);
      }

      vnode.isRootInsert = !nested; // for transition enter check
      if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
        return
      }

      var data = vnode.data;
      var children = vnode.children;
      var tag = vnode.tag;
      if (isDef(tag)) {
        //普通元素节点
        {
          if (data && data.pre) {
            creatingElmInVPre++;
          }
          if (isUnknownElement$$1(vnode, creatingElmInVPre)) {
            warn(
              'Unknown custom element: <' + tag + '> - did you ' +
              'register the component correctly? For recursive components, ' +
              'make sure to provide the "name" option.',
              vnode.context
            );
          }
        }

        vnode.elm = vnode.ns
          ? nodeOps.createElementNS(vnode.ns, tag)
          : nodeOps.createElement(tag, vnode); //创建真实Dom节点
        setScope(vnode);

        /* istanbul ignore if */
        {
          createChildren(vnode, children, insertedVnodeQueue);
          if (isDef(data)) {    
            //在这里遍历create hooks,之前文章分析过register ref过程
            invokeCreateHooks(vnode, insertedVnodeQueue);
          }
          insert(parentElm, vnode.elm, refElm);//挂载节点到父节点
        }

        if (data && data.pre) {
          creatingElmInVPre--;
        }
      } else if (isTrue(vnode.isComment)) {
        vnode.elm = nodeOps.createComment(vnode.text);
        insert(parentElm, vnode.elm, refElm);
      } else {
        vnode.elm = nodeOps.createTextNode(vnode.text);
        insert(parentElm, vnode.elm, refElm);
      }
    }

我们之前文章分析过Vue $refs用法,其实ref也作为data对象中的一个属性,和style,class属性处理过程是一样的。这里我们再分析一遍。

          if (isDef(data)) {    
            //在这里遍历create hooks,之前文章分析过register ref过程
            invokeCreateHooks(vnode, insertedVnodeQueue);
          }
  function updateStyle (oldVnode, vnode) {
    //这个函数对style属性进行处理
    var data = vnode.data;
    var oldData = oldVnode.data;

    if (isUndef(data.staticStyle) && isUndef(data.style) &&
      isUndef(oldData.staticStyle) && isUndef(oldData.style)
    ) {
      return
    }

    var cur, name;
    var el = vnode.elm;
    var oldStaticStyle = oldData.staticStyle;
    var oldStyleBinding = oldData.normalizedStyle || oldData.style || {};

    // if static style exists, stylebinding already merged into it when doing 
       normalizeStyleData
    var oldStyle = oldStaticStyle || oldStyleBinding;

    var style = normalizeStyleBinding(vnode.data.style) || {};

    // store normalized style under a different key for next diff
    // make sure to clone it if it's reactive, since the user likely wants
    // to mutate it.
    vnode.data.normalizedStyle = isDef(style.__ob__)
      ? extend({}, style)
      : style;

    var newStyle = getStyle(vnode, true);

    for (name in oldStyle) {
      if (isUndef(newStyle[name])) {
        setProp(el, name, '');
      }
    }
    for (name in newStyle) {
      cur = newStyle[name];
      if (cur !== oldStyle[name]) {
        // ie9 setting to null has no effect, must use empty string
        setProp(el, name, cur == null ? '' : cur); //在这里对节点加样式
      }
    }
  }


  var style = {
    create: updateStyle,
    update: updateStyle
  };


  var platformModules = [
    //data对象中可以包含以下属性
    attrs,
    klass,
    events,
    domProps,
    style,  //style 在这里处理的
    transition
  ];

  /*  */

  // the directive module should be applied last, after all
  // built-in modules have been applied.
  var modules = platformModules.concat(baseModules);

  var patch = createPatchFunction({ nodeOps: nodeOps, modules: modules });

  function createPatchFunction (backend) {
    var i, j;
    var cbs = {};

    var modules = backend.modules;
    var nodeOps = backend.nodeOps;
    //在这里注册钩子函数
    for (i = 0; i < hooks.length; ++i) {
      cbs[hooks[i]] = [];
      for (j = 0; j < modules.length; ++j) {
        if (isDef(modules[j][hooks[i]])) {
          cbs[hooks[i]].push(modules[j][hooks[i]]);
        }
      }
    }

写的比较粗糙,但大体流程,关键代码都在这了,如果哪里还有不明白的地方,请留言。

发布了180 篇原创文章 · 获赞 16 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/liubangbo/article/details/103941511