美文网首页vue
vue解析4-组件

vue解析4-组件

作者: 百里哈哈 | 来源:发表于2020-04-12 13:23 被阅读0次

组件

组件本身是来自Vue的继承, 在Vue初始化的时候会对component类型进行函数注册。


QQ20200412-0.png

继承函数 extend代码如下

Vue.extend = function (extendOptions) {
    extendOptions = extendOptions || {};
    var Super = this;
    var SuperId = Super.cid;
    var cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {});
    if (cachedCtors[SuperId]) {
      return cachedCtors[SuperId]
    }

    var name = extendOptions.name || Super.options.name;
    if (process.env.NODE_ENV !== 'production' && name) {
      validateComponentName(name);
    }

    var Sub = function VueComponent (options) {
      this._init(options);
    };
    Sub.prototype = Object.create(Super.prototype);
    Sub.prototype.constructor = Sub;
    Sub.cid = cid++;
    Sub.options = mergeOptions(
      Super.options,
      extendOptions
    );
    Sub['super'] = Super;

    // For props and computed properties, we define the proxy getters on
    // the Vue instances at extension time, on the extended prototype. This
    // avoids Object.defineProperty calls for each instance created.
    if (Sub.options.props) {
      initProps$1(Sub);
    }
    if (Sub.options.computed) {
      initComputed$1(Sub);
    }

    // allow further extension/mixin/plugin usage
    Sub.extend = Super.extend;
    Sub.mixin = Super.mixin;
    Sub.use = Super.use;

    // create asset registers, so extended classes
    // can have their private assets too.
    ASSET_TYPES.forEach(function (type) {
      Sub[type] = Super[type];
    });
    // enable recursive self-lookup
    if (name) {
      Sub.options.components[name] = Sub;
    }

    // keep a reference to the super options at extension time.
    // later at instantiation we can check if Super's options have
    // been updated.
    Sub.superOptions = Super.options;
    Sub.extendOptions = extendOptions;
    Sub.sealedOptions = extend({}, Sub.options);

    // cache constructor
    cachedCtors[SuperId] = Sub;
    return Sub
  };
}

render生成vdom时 component的元素会挂载Vue的子类, patch的过程中执行子类的init方法,从而递归的一层层组件的渲染。

props

遵循数据单向的原则, 如果在子组件中试图改变props, vue会有报错提示,然而并不阻止数据的变更。
其判断逻辑代码如下

defineReactive$$1(props, key, value, function () {
        if (!isRoot && !isUpdatingChildComponent) {
          warn(
            "Avoid mutating a prop directly since the value will be " +
            "overwritten whenever the parent component re-renders. " +
            "Instead, use a data or computed property based on the prop's " +
            "value. Prop being mutated: \"" + key + "\"",
            vm
          );
        }
      });

测试示例如下
compTest.vue

<template>
    <div>
        my name is {{name}}
        <p>
            the msg is {{msg}}
            <span @click="changeMsg"> change the msg</span>
            <span @click="changeName"> try change prop</span>
        </p>
    </div>
</template>
<script>
export default {
    props: {
        name: {
            type: String,
            default: null
        }
    },
    data() {
        return {
            msg: 'ddd'
        }
    },
    methods: {
        changeMsg() {
            this.msg = 'two' + Date.now();
        },
        changeName() {
            this.name = 'haha' + Date.now()
        }
    }
}
</script>

slot

在Vue渲染过程中,牢记其核心规则父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。
匿名插槽示例
app.vue

<compTest :name="name">
      <div>the top </div>
    </compTest>

compTest.vue

<template>
    <div>
        <slot></slot>
        my name is {{name}}
        <p>
            the msg is {{msg}}
            <span @click="changeMsg"> change the msg</span>
            <span @click="changeName"> try change prop</span>
        </p>
    </div>
</template>

编译后的代码
父元素的render

return e("div", { attrs: { id: "app" } }, [e("compTest", { attrs: { name: this.name } },
                        [e("div", [this._v("the top ")])]), this._v(" "), e("div", { on: { click: this.change } }, [this._v("click me")])], 1)


子元素的render

return t("div", [n._t("default"), n._v("\n    my name is " + n._s(n.name) + "\n    "), 
                    t("p", [n._v("\n        the msg is " + n._s(n.msg) + "\n        "), t("span", { on: { click: n.changeMsg } }, 
                    [n._v(" change the msg")]), n._v(" "), t("span", { on: { click: n.changeName } }, [n._v(" try change prop")])])], 2)

通过生成的render函数我们可知,slot内部的元素仍然是在父作用域下进行vnode的生成。在子元素中可通过.slots获取的插槽信息, 从而实现在子元素中的渲染

function resolveSlots (
  children,
  context
) {
  if (!children || !children.length) {
    return {}
  }
  var slots = {};
  for (var i = 0, l = children.length; i < l; i++) {
    var child = children[i];
    var data = child.data;
    // remove slot attribute if the node is resolved as a Vue slot node
    if (data && data.attrs && data.attrs.slot) {
      delete data.attrs.slot;
    }
    // named slots should only be respected if the vnode was rendered in the
    // same context.
    if ((child.context === context || child.fnContext === context) &&
      data && data.slot != null
    ) {
      var name = data.slot;
      var slot = (slots[name] || (slots[name] = []));
      if (child.tag === 'template') {
        slot.push.apply(slot, child.children || []);
      } else {
        slot.push(child);
      }
    } else {
      (slots.default || (slots.default = [])).push(child);
    }
  }
  // ignore slots that contains only whitespace
  for (var name$1 in slots) {
    if (slots[name$1].every(isWhitespace)) {
      delete slots[name$1];
    }
  }
  return slots
}

其函数的调用轨迹 this._init ——>initRender——>resolveSlots

作用域插槽
示例代码
app.vue

<template>
  <div id="app">
    <slotsTest>
      <template v-slot:default="slotProps">
        {{slotProps.user.firstName}}
      </template>
    </slotsTest>
  </div>
</template>

slotTest.vue

<template>
    <div>
        <slot :user=user>
        </slot>
        {{user.lastName}}
    </div>
</template>
<script>
export default {
    data() {
        return {
            user:{
                firstName: 'haha',
                lastName: 'ali'
            }
        }
    }
}
</script>

build后的代码
父元素的render

return t("div", { attrs: { id: "app" } }, [t("slotsTest", { scopedSlots: n._u([{ key: "default", fn: function (e) { return [n._v("\n      " + n._s(e.user.firstName) + "\n    ")] } }]) })], 1)



子元素的render

return (this._self._c || n)("div", [this._t("default", null, { user: this.user }), this._v("\n    " + this._s(this.user.lastName) + "\n")], 2)



有上面代码可知,slot的编译过程仍是在父元素中完成。只不过生成的是个function 方法, 在子元素render的过程中会调用该方法,其对应的作用域data会作为参数传入。

函数式组件

函数式组件是一个接受一些 props , 无状态无实例,故而没有生命周期。
简单示例
app.vue

<funCompJS :name="name"></funCompJS>

funCompJS.jsx

 const funCompJS = {
    functional: true,
    props: {
        name: {
            type: String,
            default: null
        }
    },
    render (h, context) {
        return (
            <div>
                the content is {context.props.name}
            </div>
        );
    }
}
export default funCompJS

在函数式组件中可通过context获取上下文

函数式组件createFunctionalComponent方法

function createFunctionalComponent (
  Ctor,
  propsData,
  data,
  contextVm,
  children
) {
  var options = Ctor.options;
  var props = {};
  var propOptions = options.props;
  if (isDef(propOptions)) {
    for (var key in propOptions) {
      props[key] = validateProp(key, propOptions, propsData || emptyObject);
    }
  } else {
    if (isDef(data.attrs)) { mergeProps(props, data.attrs); }
    if (isDef(data.props)) { mergeProps(props, data.props); }
  }

// 函数式render上下文
  var renderContext = new FunctionalRenderContext(
    data,
    props,
    children,
    contextVm,
    Ctor
  );

  var vnode = options.render.call(null, renderContext._c, renderContext);

  if (vnode instanceof VNode) {
    return cloneAndMarkFunctionalResult(vnode, data, renderContext.parent, options, renderContext)
  } else if (Array.isArray(vnode)) {
    var vnodes = normalizeChildren(vnode) || [];
    var res = new Array(vnodes.length);
    for (var i = 0; i < vnodes.length; i++) {
      res[i] = cloneAndMarkFunctionalResult(vnodes[i], data, renderContext.parent, options, renderContext);
    }
    return res
  }
}

调用轨迹
createComponent——>createFunctionalComponent——>var vnode = options.render.call(null, renderContext._c, renderContext);

异步组件

在代码优化中有些可以通过采用异步方案来实现优化的目的。
其中异步组件就是我们所需要考虑的
简单是示例
可以在components中进行异步引入
方法一

'async-comp': () => import('./asyncComp.vue')

方法二

'async-comp': function (resolve) {
      require(['./asyncComp.vue'], resolve)
    }

在createComponent方法中会对异步组件进行处理,异步组件首先会创建一个comment node的节点进行占位,等待异步组件加载完成后进行渲染替换

// async component
  var asyncFactory;
  if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor;
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor);
    if (Ctor === undefined) {
      // return a placeholder node for async component, which is rendered
      // as a comment node but preserves all the raw information for the node.
      // the information will be used for async server-rendering and hydration.
      return createAsyncPlaceholder(
        asyncFactory,
        data,
        context,
        children,
        tag
      )
    }
  }

加载异步组件方法resolveAsyncComponent

function resolveAsyncComponent (
  factory,
  baseCtor
) {
  if (isTrue(factory.error) && isDef(factory.errorComp)) {
    return factory.errorComp
  }

  if (isDef(factory.resolved)) {
    return factory.resolved
  }

  var owner = currentRenderingInstance;
  if (owner && isDef(factory.owners) && factory.owners.indexOf(owner) === -1) {
    // already pending
    factory.owners.push(owner);
  }

  if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
    return factory.loadingComp
  }

  if (owner && !isDef(factory.owners)) {
    var owners = factory.owners = [owner];
    var sync = true;
    var timerLoading = null;
    var timerTimeout = null

    ;(owner).$on('hook:destroyed', function () { return remove(owners, owner); });

    var forceRender = function (renderCompleted) {
      for (var i = 0, l = owners.length; i < l; i++) {
        (owners[i]).$forceUpdate();
      }

      if (renderCompleted) {
        owners.length = 0;
        if (timerLoading !== null) {
          clearTimeout(timerLoading);
          timerLoading = null;
        }
        if (timerTimeout !== null) {
          clearTimeout(timerTimeout);
          timerTimeout = null;
        }
      }
    };

    var resolve = once(function (res) {
      // cache resolved
      factory.resolved = ensureCtor(res, baseCtor);
      // invoke callbacks only if this is not a synchronous resolve
      // (async resolves are shimmed as synchronous during SSR)
      if (!sync) {
        forceRender(true);
      } else {
        owners.length = 0;
      }
    });

    var reject = once(function (reason) {
      process.env.NODE_ENV !== 'production' && warn(
        "Failed to resolve async component: " + (String(factory)) +
        (reason ? ("\nReason: " + reason) : '')
      );
      if (isDef(factory.errorComp)) {
        factory.error = true;
        forceRender(true);
      }
    });

    var res = factory(resolve, reject);

    if (isObject(res)) {
      if (isPromise(res)) {
        // () => Promise
        if (isUndef(factory.resolved)) {
          res.then(resolve, reject);
        }
      } else if (isPromise(res.component)) {
        res.component.then(resolve, reject);

        if (isDef(res.error)) {
          factory.errorComp = ensureCtor(res.error, baseCtor);
        }

        if (isDef(res.loading)) {
          factory.loadingComp = ensureCtor(res.loading, baseCtor);
          if (res.delay === 0) {
            factory.loading = true;
          } else {
            timerLoading = setTimeout(function () {
              timerLoading = null;
              if (isUndef(factory.resolved) && isUndef(factory.error)) {
                factory.loading = true;
                forceRender(false);
              }
            }, res.delay || 200);
          }
        }

        if (isDef(res.timeout)) {
          timerTimeout = setTimeout(function () {
            timerTimeout = null;
            if (isUndef(factory.resolved)) {
              reject(
                process.env.NODE_ENV !== 'production'
                  ? ("timeout (" + (res.timeout) + "ms)")
                  : null
              );
            }
          }, res.timeout);
        }
      }
    }

    sync = false;
    // return in case resolved synchronously
    return factory.loading
      ? factory.loadingComp
      : factory.resolved
  }
}

有代码可知,可为异步组件设置loading的展示组件,如果有loading时。首先render的为loading样式,而非commont占位节点。
可使用下面代码进行测试

'async-comp': () => ({
      component: import('./asyncComp.vue'),
      loading: LoadingComp,
      delay: 200
    })

相关文章

网友评论

    本文标题:vue解析4-组件

    本文链接:https://www.haomeiwen.com/subject/mesvmhtx.html