vue源码解读-component机制

栏目: 编程语言 · 发布时间: 7年前

内容简介:vue源码解读-component机制

身为原来的jquery,angular使用者。后面接触了react和vue。渐渐的喜欢上了vue。抱着学习的态度呀。看看源码。果然菜要付出代价。一步步单步调试。头好疼。看到哪里记到哪里。来一点点心得。错误的地方请帮我指出来。谢谢。最近看的是vue component部分。

先上一段最简单的代码,来剖析component机制:
<div id="app">
  <div>{{a}}</div>
  <input v-model="heihei">
  <button v-on:click="click1">
  click1
  </button>
  <my-component>
  <div slot='dudu'>111</div>
  <Child>{{a}}</Child>
  </my-component>
  <button @click.stop="click2">
  click2
  </button>
</div>
</body>
<script src="vue.js"></script>
<script type="text/javascript">
var Child = {
  template: '<div>A custom component!</div>'
} 
Vue.component('my-component', {
  name: 'my-component',
  template: '<div>A custom component!<Child></Child><slot></slot></div>',
  components: {
    Child:Child
  },
  created(){
    console.log(this);
  },
  mounted(){
    console.log(this);
  }
})
    new Vue({
  el: '#app',
  data: function () {
    return {
      heihei:{name:3333},
      a:1
    }
  },
  components: {
    Child:Child
  },
  created(){
  
  },
  methods: {
    click1: function(){
      console.log(this);
    },
    click2: function(){
      alert('click2')
    }
  }
})
</script>

我们按照浏览器的思维逐行来。执行到脚本时。首先执行了

Vue.component('my-component', {
  name: 'my-component',
  template: '<div>A custom component!<Child></Child><slot></slot></div>',
  components: {
    Child:Child
  },
  created(){
    console.log(this);
  },
  mounted(){
    console.log(this);
  }
})

我们来看看这个函数经历了什么:

vue.js初始化的时候。调用了initGlobalAPI(vue),为vue挂上了 工具 函数vue.component

经过initGlobalAPI(vue)中的initAssetRegisters (Vue) 后。变为

vue.component = function (
      id,
      definition
    ) {
      if (!definition) {
        return this.options[type + 's'][id]
      } else {
        /* istanbul ignore if */
        {
          if (type === 'component' && config.isReservedTag(id)) {
            warn(
              'Do not use built-in or reserved HTML elements as component ' +
              'id: ' + id
            );
          }
        }
        if (type === 'component' && isPlainObject(definition)) {
          definition.name = definition.name || id;
          definition = this.options._base.extend(definition);
        }
        if (type === 'directive' && typeof definition === 'function') {
          definition = { bind: definition, update: definition };
        }
        this.options[type + 's'][id] = definition;//全局的组件或者指令和过滤器。统一挂在vue.options上。等待init的时候利用策略合并侵入实例。供实例使用
        return definition
      }
    };

this.options._base在initGlobalAPI(vue)中为Vue.options._base = Vue;

so vue.component调用了vue.extend。找到了源头。我们来好好看看这个vue.extend这个function。代码如下:

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]//如果组件已经被缓存在extendOptions上则直接取出
    }

    var name = extendOptions.name || Super.options.name;
    {
      if (!/^[a-zA-Z][\w-]*$/.test(name)) {
        warn(
          'Invalid component name: "' + name + '". Component names ' +
          'can only contain alphanumeric characters and the hyphen, ' +
          'and must start with a letter.'//校验组件名
        );
      }
    }

    var Sub = function VueComponent (options) {
      this._init(options);
    };
    Sub.prototype = Object.create(Super.prototype);
    Sub.prototype.constructor = Sub;//将vue上原型的方法挂在Sub.prototype中,Sub的实例同时也继承了vue.prototype上的所有属性和方法。
    Sub.cid = cid++;
    Sub.options = mergeOptions(
      Super.options,
      extendOptions//通过vue的合并策略合并添加项到新的构造器上
    );
    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) {   //处理props和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.              //在新的构造器上挂上vue的工具方法
    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;//缓存组件构造器在extendOptions上
    return Sub
  };
}

function initProps$1 (Comp) {
  var props = Comp.options.props;
  for (var key in props) {
    proxy(Comp.prototype, "_props", key);
  }
}

function initComputed$1 (Comp) {
  var computed = Comp.options.computed;
  for (var key in computed) {
    defineComputed(Comp.prototype, key, computed[key]);
  }
}

总的来说vue.extend是返回了一个带有附加配置相的新的vue的构造器。在函数中,构造器叫做Sub,等待render时候初始化。

经过vue.component的调用。vue增加了一个全局组件my-component;此时vue.options.component如下图:

vue源码解读-component机制

前三个是vue内置的三个组件,在initgloabalapi的时候初始化。

至此全局组件创建完成。全局组件放置在最底层。在以后的策略合并里会在子组件中的component项的__proto__中。

通过组件的递归创建渲染来看vue整体的生命周期(理解vue如何巧妙构建应用)

上图:

vue源码解读-component机制

vue官方的生命周期图,其实也就是vue组件的构成的生命周期。沿着new Vue()我们来大概看看这些生命周期在什么阶段

Vue.prototype._init = function (options) {
    var vm = this;
    // a uid
    vm._uid = uid$1++;

    var startTag, endTag;
    /* istanbul ignore if */
    if ("development" !== 'production' && config.performance && mark) {
      startTag = "vue-perf-init:" + (vm._uid);
      endTag = "vue-perf-end:" + (vm._uid);
      mark(startTag);
    }

    // a flag to avoid this being observed
    vm._isVue = true;
    // merge options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options);//内部组件调用此快捷方法
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),//策略合并,每项属性都有对应的合并规则
        options || {},
        vm
      );
    }
    /* istanbul ignore else */
    {
      initProxy(vm);//属性代理,即vm.xx = vm.data.xx
    }
    // expose real self
    vm._self = vm;
    initLifecycle(vm);//初始化生命周期状态变量,建立子父关系初始值,如$children,$parent.
    initEvents(vm);// 初始化事件
    initRender(vm);//初始化render核心函数_$createElement和$slots $scopedSlots等
    callHook(vm, 'beforeCreate');
    initInjections(vm); // resolve injections before data/props
    initState(vm);//利用数据劫持做响应式
    initProvide(vm); //resolve provide after data/props
    callHook(vm, 'created');

    /* istanbul ignore if */
    if ("development" !== 'production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false);
      mark(endTag);
      measure(((vm._name) + " init"), startTag, endTag);
    }

    if (vm.$options.el) {
      vm.$mount(vm.$options.el);//如果有el配置相则主动挂载。触发之后的compile.render
    }
  };

介绍了大概的_init函数,我们继续往下看程序的运行。完成了vue.component()之后。开始执行new vue(),创建实例。

对照_init函数。我们知道它分别进行了对传入参数的合并。初始化实例参数。创建响应的响应式。最后挂载:vm.$mount(vm.$options.el);

简单说说挂载。好吧。我们还是往方法里面看,挂载的时候发生了什么:

// public mount method
Vue$3.prototype.$mount = function (
  el,
  hydrating
) {
  el = el && inBrowser ? query(el) : undefined;
  return mountComponent(this, el, hydrating)
};

var mount = Vue$3.prototype.$mount//缓存mount,用来触发render
Vue$3.prototype.$mount = function (//核心mount用来构建render函数
  el,
  hydrating
) {
  el = el && query(el);

  /* istanbul ignore if */
  if (el === document.body || el === document.documentElement) {
    "development" !== 'production' && warn(
      "Do not mount Vue to <html> or <body> - mount to normal elements instead."//检测,排除不可挂载的元素
    );
    return this
  }

  var options = this.$options;
  // resolve template/el and convert to render function
  if (!options.render) {
    var template = options.template;//假如输入的是template模版时。
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          template = idToTemplate(template);
          /* istanbul ignore if */
          if ("development" !== 'production' && !template) {
            warn(
              ("Template element not found or is empty: " + (options.template)),
              this
            );
          }
        }
      } else if (template.nodeType) {
        template = template.innerHTML;//输入的是dom节点时
      } else {
        {
          warn('invalid template option:' + template, this);
        }
        return this
      }
    } else if (el) {
      template = getOuterHTML(el);//如果是一个id,如此次初始化挂载的id=app,会取到id=app的html
    }
    if (template) {
      /* istanbul ignore if */
      if ("development" !== 'production' && config.performance && mark) {
        mark('compile');
      }

      var ref = compileToFunctions(template, {
        shouldDecodeNewlines: shouldDecodeNewlines,//核心compile函数。用于生成render函数。这里不细说
        delimiters: options.delimiters
      }, this);
      var render = ref.render;
      var staticRenderFns = ref.staticRenderFns;
      options.render = render;//挂载render到实例options中。待调用
      options.staticRenderFns = staticRenderFns;//静态的元素区分开。提升性能,后续虚拟dom树比较时,不会比较静态节点

      /* istanbul ignore if */
      if ("development" !== 'production' && config.performance && mark) {
        mark('compile end');
        measure(((this._name) + " compile"), 'compile', 'compile end');
      }
    }
  }
  return mount.call(this, el, hydrating)//利用缓存的mount调用准备好的render
};

$mount方法的核心其实就是准备好组件的render函数。这里最核心的一个方法就是:

var ref = compileToFunctions(template, {
        shouldDecodeNewlines: shouldDecodeNewlines,//核心compile函数。用于生成render函数。这里不细说
        delimiters: options.delimiters
      }, this);

compileToFunctions这个函数中主要做了两件事:

1:对模版进行compile(按标签解析,生成ast(抽象语法树)

2:利用generate(ast, options),生成render函数语法

我们来看看最后实例生成的render函数:

vue源码解读-component机制

没有错就是这个样子,很有感觉。生成的render函数保存在options中,等待调用

好吧。开始调用吧。

mount.call(this, el, hydrating)=》mountComponent(this, el, hydrating)=》updateComponent = function () {
      vm._update(vm._render(), hydrating);
    };=》vm._watcher = new Watcher(vm, updateComponent, noop);

new Watcher中会主动调用updateComponent去touch依赖(给页面中引用过的data中的变量假如监听)正式调用render函数。既然都说了。那就来看看render函数:

Vue.prototype._render = function () {
    var vm = this;
    var ref = vm.$options;
    var render = ref.render;
    var staticRenderFns = ref.staticRenderFns;
    var _parentVnode = ref._parentVnode;

    if (vm._isMounted) {
      // clone slot nodes on re-renders
      for (var key in vm.$slots) {
        vm.$slots[key] = cloneVNodes(vm.$slots[key]);
      }
    }

    vm.$scopedSlots = (_parentVnode && _parentVnode.data.scopedSlots) || emptyObject;

    if (staticRenderFns && !vm._staticTrees) {
      vm._staticTrees = [];
    }
    // 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 {
      vnode = render.call(vm._renderProxy, vm.$createElement);//核心函数,调用render
    } catch (e) {
      handleError(e, vm, "render function");
      // return error render result,
      // or previous vnode to prevent render error causing blank component
      /* istanbul ignore else */
      {
        vnode = vm.$options.renderError
          ? vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
          : vm._vnode;
      }
    }
    // return empty vnode in case the render function errored out
    if (!(vnode instanceof VNode)) {
      if ("development" !== '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);

然后结合render函数。看看发生了什么。vm.$createElement是核心的创建虚拟dom的函数。

继续看看核心构建虚拟dom函数:

function createElement (
  context,
  tag,
  data,
  children,//children是该元素下的所有子元素
  normalizationType,
  alwaysNormalize
) {
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children;
    children = data;
    data = undefined;
  }
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE;
  }
  return _createElement(context, tag, data, children, normalizationType)
}

function _createElement (
  context,
  tag,
  data,
  children,
  normalizationType
) {
  if (isDef(data) && isDef((data).__ob__)) {
    "development" !== 'production' && warn(
      "Avoid using observed data object as vnode data: " + (JSON.stringify(data)) + "\n" +
      'Always create fresh vnode data objects in each render!',
      context
    );
    return createEmptyVNode()
  }
  if (!tag) {
    // in case of component :is set to falsy value
    return createEmptyVNode()
  }
  // support single function children as default scoped slot
  if (Array.isArray(children) &&
      typeof children[0] === 'function') {
    data = data || {};
    data.scopedSlots = { default: children[0] };
    children.length = 0;
  }
  if (normalizationType === ALWAYS_NORMALIZE) {
    children = normalizeChildren(children);
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children);
  }
  var vnode, ns;
  if (typeof tag === 'string') {
    var Ctor;
    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 (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {假如是组件则从上下文中取出组件的构造相关参数
      // component
      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);
  }
  if (vnode !== undefined) {
    if (ns) { applyNS(vnode, ns); }
    return vnode
  } else {
    return createEmptyVNode()
  }
}

这里其实我们不难看出vue在构造虚拟dom时。递归的去调用createElement去生成虚拟dom树。当children是组件或者是普通元素时。做不同的处理。这里我们关注的是。当子元素是组建时。这里调用了

vnode = createComponent(tag, data, context, children);

细心的人可以在去看看这个函数做了什么。简单来说这个函数将组件的构造参数取出来,放置在元素的componentOptions上。供后续创建真实dom时。标记该元素是组件。递归初始化。

跳过这些沉重的。我们直接看看我们的这个html生成的最终的虚拟dom长什么样。如下:

vue源码解读-component机制

我们在来看看我们的my-component组件长什么样子:

vue源码解读-component机制

componentOptios上存着初始化组件需要的参数。

构建好虚拟dom后。vue进入update阶段:

这个阶段vue会判断先前有无该元素。是否为第一次渲染。假如是第一次。那么直接创建。如果不是有先前的ovnode,则比较差异。最小化更新。看看具体函数:

nction lifecycleMixin (Vue) {
  Vue.prototype._update = function (vnode, hydrating) {
    var vm = this;
    if (vm._isMounted) {
      callHook(vm, 'beforeUpdate');
    }
    var prevEl = vm.$el;
    var prevVnode = vm._vnode;//取出缓存的久的虚拟dom
    var prevActiveInstance = activeInstance;
    activeInstance = vm;
    vm._vnode = vnode;//缓存当前vnode,供下次更新使用
    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.
    if (!prevVnode) {//假如第一次渲染。直接创建
      // initial render
      vm.$el = vm.__patch__(
        vm.$el, vnode, hydrating, false /* removeOnly */,
        vm.$options._parentElm,
        vm.$options._refElm
      );
    } else {
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode);//更新时。会比较差异
    }
    activeInstance = prevActiveInstance;
    // update __vue__ reference
    if (prevEl) {
      prevEl.__vue__ = null;
    }
    if (vm.$el) {
      vm.$el.__vue__ = vm;
    }
    // if parent is an HOC, update its $el as well
    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      vm.$parent.$el = vm.$el;
    }
    // updated hook is called by the scheduler to ensure that children are
    // updated in a parent's updated hook.
  };

__patch__函数我们就不细看了。算了看一下:

return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) { invokeDestroyHook(oldVnode); }
      return
    }

    var isInitialPatch = false;
    var insertedVnodeQueue = [];

    if (isUndef(oldVnode)) {
      // empty mount (likely as component), create new root element
      isInitialPatch = true;//假如第一次渲染。直接创建
      createElm(vnode, insertedVnodeQueue, parentElm, refElm);
    } else {
      var isRealElement = isDef(oldVnode.nodeType);
      if (!isRealElement && sameVnode(oldVnode, vnode)) {//假如更新并且前后虚拟dom相似,这里相似有自己的一个算法。比如tag,key必需一致。才会去diff比较
        // patch existing root node
        patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly);
      } else {
        if (isRealElement) {
          // mounting to a real element
          // check if this is server-rendered content and if we can perform
          // a successful hydration.
          if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
            oldVnode.removeAttribute(SSR_ATTR);
            hydrating = true;
          }
          if (isTrue(hydrating)) {
            if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
              invokeInsertHook(vnode, insertedVnodeQueue, true);
              return oldVnode
            } else {
              warn(
                'The client-side rendered virtual DOM tree is not matching ' +
                'server-rendered content. This is likely caused by incorrect ' +
                'HTML markup, for example nesting block-level elements inside ' +
                '<p>, or missing <tbody>. Bailing hydration and performing ' +
                'full client-side render.'
              );
            }
          }
          // either not server-rendered, or hydration failed.
          // create an empty node and replace it
          oldVnode = emptyNodeAt(oldVnode);
        }
        // replacing existing element
        var oldElm = oldVnode.elm;
        var parentElm$1 = nodeOps.parentNode(oldElm);
        createElm(
          vnode,
          insertedVnodeQueue,
          // extremely rare edge case: do not insert if old element is in a
          // leaving transition. Only happens when combining transition +
          // keep-alive + HOCs. (#4590)
          oldElm._leaveCb ? null : parentElm$1,
          nodeOps.nextSibling(oldElm)
        );

        if (isDef(vnode.parent)) {
          // component root element replaced.
          // update parent placeholder node element, recursively
          var ancestor = vnode.parent;
          while (ancestor) {
            ancestor.elm = vnode.elm;
            ancestor = ancestor.parent;
          }
          if (isPatchable(vnode)) {
            for (var i = 0; i < cbs.create.length; ++i) {
              cbs.create[i](emptyNode, vnode.parent);
            }
          }
        }

        if (isDef(parentElm$1)) {
          removeVnodes(parentElm$1, [oldVnode], 0, 0);
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode);
        }
      }
    }

    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
    return vnode.elm
  }

patch方法中核心的是createElm:看懂这个函数非常重要代码如下

function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested) {
    vnode.isRootInsert = !nested; // for transition enter check
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {//根据之前保存的componentoptions来识别是否为组件。若是。则进这个逻辑
      return
    }

    var data = vnode.data;
    var children = vnode.children;
    var tag = vnode.tag;
    if (isDef(tag)) {
      {
        if (data && data.pre) {
          inPre++;
        }
        if (
          !inPre &&
          !vnode.ns &&
          !(config.ignoredElements.length && config.ignoredElements.indexOf(tag) > -1) &&
          config.isUnknownElement(tag)
        ) {
          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);
      setScope(vnode);

      /* istanbul ignore if */
      {
        createChildren(vnode, children, insertedVnodeQueue);
        if (isDef(data)) {
          invokeCreateHooks(vnode, insertedVnodeQueue);
        }
        insert(parentElm, vnode.elm, refElm);
      }

      if ("development" !== 'production' && data && data.pre) {
        inPre--;
      }
    } 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);
    }
  }

我们这边还是先关注自己的组件部分。当children是组件元素时,很显然调用了createComponent(vnode, insertedVnodeQueue, parentElm, refElm);

var componentVNodeHooks = {
  init: function init (
    vnode,
    hydrating,
    parentElm,
    refElm
  ) {
    if (!vnode.componentInstance || vnode.componentInstance._isDestroyed) {
      var child = vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance,
        parentElm,//调用了组件内部的_init方法递归创建子组件。正式进入子组件的生命周期
        refElm
      );
      child.$mount(hydrating ? vnode.elm : undefined, hydrating);触发子组件的挂载。出发子组件的编译和render。又重新来一遍/直到子组件完全渲染好。再开始creelem下一个child
    } else if (vnode.data.keepAlive) {
      // kept-alive components, treat as a patch
      var mountedNode = vnode; // work around flow
      componentVNodeHooks.prepatch(mountedNode, mountedNode);
    }
  },

这里就是递归创建子组件的核心部分.

总结: 第一次写这个vue。失败了。切模块切的不够细。组件机制感觉用了好多东西。这个面太大了。自己讲的时候也不知道该细讲还是。。。

总的来说:vue在comp创建虚拟dom的时候,如果元素是组件。则准备好组件的构造参数。包括模版和数据等等。组件中的元素如slot,和child放在组件元素的children下。供后面的内容分发用组件中的元素也是在父组件的作用域内编译的。看—_render()函数就知道。然后在vue需要将虚拟dom变为真实dom时。遇到组件元素时。开始递归初始化。直到把组件compile,render构建完后。开始构建下一个元素。最后添加到真实id=app上。并且把旧的删了。哈哈。随便写了


以上所述就是小编给大家介绍的《vue源码解读-component机制》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Stylin' with CSS

Stylin' with CSS

Wyke-Smith, Charles / 2012-10 / $ 50.84

In this completely revised edition of his bestselling Stylin' with CSS, veteran designer and programmer Charles Wyke-Smith guides you through a comprehensive overview of designing Web pages with CSS, ......一起来看看 《Stylin' with CSS》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

随机密码生成器
随机密码生成器

多种字符组合密码

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具