vue2源码记录(2)

组件化思想

js 复制代码
export function createComponent(
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  if (isUndef(Ctor)) {
    return;
  }

  // context.$options._base 在 initGlobalAPI 中定义, 就是 Vue 本身
  const baseCtor = context.$options._base;

  // plain options object: turn it into a constructor
  // 组件局部注册
  if (isObject(Ctor)) {
    // 相当于调用 Vue.extend,得到一个构造器
    Ctor = baseCtor.extend(Ctor);
  }

  // if at this stage it's not a constructor or an async component factory,
  // reject.
  // 如果不是一个函数,返回错误,组件定义有问题
  if (typeof Ctor !== "function") {
    if (process.env.NODE_ENV !== "production") {
      warn(`Invalid Component definition: ${String(Ctor)}`, context);
    }
    return;
  }

  // 对异步组件的处理
  // async component
  let 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.

      // 是创建一个注释节点vnode
      return createAsyncPlaceholder(asyncFactory, data, context, children, tag);
    }
  }

  data = data || {};

  // resolve constructor options in case global mixins are applied after
  // component constructor creation
  // 构造器配置合并
  resolveConstructorOptions(Ctor);

  // transform component v-model data into props & events
  // 组件的 v-model
  if (isDef(data.model)) {
    transformModel(Ctor.options, data);
  }

  // extract props
  const propsData = extractPropsFromVNodeData(data, Ctor, tag);

  // functional component
  if (isTrue(Ctor.options.functional)) {
    return createFunctionalComponent(Ctor, propsData, data, context, children);
  }

  // extract listeners, since these needs to be treated as
  // child component listeners instead of DOM listeners
  const listeners = data.on;
  // replace with listeners with .native modifier
  // so it gets processed during parent component patch.
  data.on = data.nativeOn;

  if (isTrue(Ctor.options.abstract)) {
    // abstract components do not keep anything
    // other than props & listeners & slot

    // work around flow
    const slot = data.slot;
    data = {};
    if (slot) {
      data.slot = slot;
    }
  }

  // 安装一些组件的钩子
  // install component management hooks onto the placeholder node
  installComponentHooks(data);

  // return a placeholder vnode
  const name = Ctor.options.name || tag;
  // 创建组件 VNode
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ""}`,
    data,
    undefined,
    undefined,
    undefined,
    context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  );

  // Weex specific: invoke recycle-list optimized @render function for
  // extracting cell-slot template.
  // https://github.com/Hanks10100/weex-native-directive/tree/master/component
  /* istanbul ignore if */
  if (__WEEX__ && isRecyclableComponent(vnode)) {
    return renderRecyclableComponentTemplate(vnode);
  }

  return vnode;
}

构造子类构造函数

把编写的对象组件转变为一个构造函数

js 复制代码
  // context.$options._base 在 initGlobalAPI 中定义, 就是 Vue 本身
  const baseCtor = context.$options._base;

  // plain options object: turn it into a constructor
  // 组件局部注册
  if (isObject(Ctor)) {
    // 相当于调用 Vue.extend,得到一个构造器
    Ctor = baseCtor.extend(Ctor);
  }

extend方法

js 复制代码
  // 使用 Vue 构造器,创建一个"子类",该子类同样支持进一步的扩展
  // 扩展时可以传递一些默认配置,就像 Vue 也会有一些默认配置
  // 默认配置如果和基类有冲突则会进行选项合并(mergeOptions)
  Vue.extend = function (extendOptions: Object): Function {
    extendOptions = extendOptions || {}
    const Super = this
    const SuperId = Super.cid

    // 判断缓存中有没有存在,有就直接使用
    // 比如:多次调用 Vue.extend 传入同一个配置项(extendOptions),这时就会启用该缓存
    const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
    if (cachedCtors[SuperId]) {
      return cachedCtors[SuperId]
    }

    const name = extendOptions.name || Super.options.name
    if (process.env.NODE_ENV !== 'production' && name) {
      // 校验组件名
      validateComponentName(name)
    }

    // 定义 Sub 构造函数,和 Vue 构造函数一致
    const Sub = function VueComponent (options) {
      // 里面也是和 Vue 构造函数一样,使用 this._init 进行初始化
      this._init(options)
    }
    // 通过寄生组合继承 Vue
    Sub.prototype = Object.create(Super.prototype)
    Sub.prototype.constructor = Sub
    Sub.cid = cid++
    // 将 Vue 的配置合并到自己的配置里
    Sub.options = mergeOptions(
      Super.options,
      extendOptions
    )
    Sub['super'] = Super

    // 将 props 代理到 Sub.prototype._props 对象上
    // 在组件内可以通过 this._props 的方式访问
    if (Sub.options.props) {
      initProps(Sub)
    }
    // 将 computed 代理到 Sub.prototype 对象上
    // 在组件内可以通过 this.computed[key] 的方式访问
    if (Sub.options.computed) {
      initComputed(Sub)
    }

    // allow further extension/mixin/plugin usage
    // 定义组件的 extend、mixin、use,允许在 Sub 基础上再进一步构造子类
    Sub.extend = Super.extend
    Sub.mixin = Super.mixin
    Sub.use = Super.use

    // create asset registers, so extended classes
    // can have their private assets too.
    // 定义 component、filter、directive 三个静态方法
    ASSET_TYPES.forEach(function (type) {
      Sub[type] = Super[type]
    })

    // enable recursive self-lookup
    // 如果组件设置了 name 属性,将自己注册到自己的 components 选项中,这也是递归组件的原理
    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
    // 把继承后的 Sub 缓存,好处: 当下次创建 Sub 时,发现已有,就直接使用
    cachedCtors[SuperId] = Sub
    return Sub
  }

采用继承的方式把对象转换为继承自Vue的构造器Sub并返回,做了一些额外的处理比如name,props,computed等,并且进行了缓存。 实例化Sub时,会执行this._init进行Vue实例的初始化。

安装组件钩子函数

VNode在patch流程中对外暴露了各种时机的钩子函数,方便做一些额外的事情。

js 复制代码
// 将componentVNodeHooks 钩子函数合并到组件data.hook中
function installComponentHooks(data: VNodeData) {
  const hooks = data.hook || (data.hook = {});
  for (let i = 0; i < hooksToMerge.length; i++) {
    const key = hooksToMerge[i];
    const existing = hooks[key];
    const toMerge = componentVNodeHooks[key];
    if (existing !== toMerge && !(existing && existing._merged)) {
      hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge;
    }
  }
}

内置的hooks

js 复制代码
const componentVNodeHooks = {
  init(vnode: VNodeWithData, hydrating: boolean): ?boolean {
    if (
      vnode.componentInstance &&
      !vnode.componentInstance._isDestroyed &&
      vnode.data.keepAlive
    ) {
      // kept-alive components, treat as a patch
      // keep-alive 包裹的组件走这里
      const mountedNode: any = vnode; // work around flow
      // 只调用 prepatch 更新实例属性
      componentVNodeHooks.prepatch(mountedNode, mountedNode);
    } else {
      // createComponentInstanceForVnode 会 new Vue 构造组件实例并赋值到 componentInstance
      const child = (vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      ));
      // 挂载组件-因为在执行_init时,没有el,所以组件自己接管了$mount过程
      child.$mount(hydrating ? vnode.elm : undefined, hydrating);
    }
  },

  prepatch(oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
    const options = vnode.componentOptions;
    const child = (vnode.componentInstance = oldVnode.componentInstance);
    updateChildComponent(
      child,
      options.propsData, // updated props
      options.listeners, // updated listeners
      vnode, // new parent vnode
      options.children // new children
    );
  },

  insert(vnode: MountedComponentVNode) {
    const { context, componentInstance } = vnode;
    // 首次渲染,执行的是 mounted 钩子,不会去执行 updated 钩子
    if (!componentInstance._isMounted) {
      componentInstance._isMounted = true;
      callHook(componentInstance, "mounted");
    }
    if (vnode.data.keepAlive) {
      if (context._isMounted) {
        // vue-router#1212
        // During updates, a kept-alive component's child components may
        // change, so directly walking the tree here may call activated hooks
        // on incorrect children. Instead we push them into a queue which will
        // be processed after the whole patch process ended.
        queueActivatedComponent(componentInstance);
      } else {
        activateChildComponent(componentInstance, true /* direct */);
      }
    }
  },

  destroy(vnode: MountedComponentVNode) {
    const { componentInstance } = vnode;
    if (!componentInstance._isDestroyed) {
      if (!vnode.data.keepAlive) {
        componentInstance.$destroy();
      } else {
        deactivateChildComponent(componentInstance, true /* direct */);
      }
    }
  },
};

实例化vnode

js 复制代码
const name = Ctor.options.name || tag;
  // 创建组件 VNode
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ""}`,
    data,
    undefined,
    undefined,
    undefined,
    context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  );
  return vnode;

实例化一个vnode并返回,和普通元素的vnode不同,组件的vnode没有children。

patch中createComponent

js 复制代码
  function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
    let i = vnode.data // 这个是 组件的 VNodeData
    if (isDef(i)) {
       // isReactivated 用来判断组件是否缓存。
      const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
      if (isDef(i = i.hook) && isDef(i = i.init)) {
        // 执行组件初始化的内部钩子 init
        i(vnode, false /* hydrating */ )
      }
      // after calling the init hook, if the vnode is a child component
      // it should've created a child instance and mounted it. the child
      // component also has set the placeholder vnode's elm.
      // in that case we can just return the element and be done.
      if (isDef(vnode.componentInstance)) {
        initComponent(vnode, insertedVnodeQueue)
        insert(parentElm, vnode.elm, refElm) // 插入顺序:先子后父
        if (isTrue(isReactivated)) {
          reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
        }
        return true
      }
    }
  }

vnode是组件VNode时,i就是init钩子,init钩子函数调用 createComponentInstanceForVnode创建Vue实例。

js 复制代码
export function createComponentInstanceForVnode(
  vnode: any, // we know it's MountedComponentVNode but flow doesn't
  // parent:是 activeInstance, 在 lifecycle 中
  parent: any // activeInstance in lifecycle state
): Component {
  const options: InternalComponentOptions = {
    _isComponent: true,
    _parentVnode: vnode,
    parent,
  };
  // check inline-template render functions
  const inlineTemplate = vnode.data.inlineTemplate;
  if (isDef(inlineTemplate)) {
    options.render = inlineTemplate.render;
    options.staticRenderFns = inlineTemplate.staticRenderFns;
  }
  // 执行 vue 子组件实例化
  return new vnode.componentOptions.Ctor(options);
}

vnode.componentOptions.Ctor是构造子类构造函数的Sub,相当于new Sub(options).在执行实例的初始化方法时,组件则会调用initInternalComponent方法。

js 复制代码
// 组件的配置合并
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
  const opts = vm.$options = Object.create(vm.constructor.options)
  // doing this because it's faster than dynamic enumeration.
  const parentVnode = options._parentVnode
  opts.parent = options.parent
  opts._parentVnode = parentVnode

  const vnodeComponentOptions = parentVnode.componentOptions
  opts.propsData = vnodeComponentOptions.propsData
  opts._parentListeners = vnodeComponentOptions.listeners
  opts._renderChildren = vnodeComponentOptions.children
  opts._componentTag = vnodeComponentOptions.tag

  if (options.render) {
    opts.render = options.render
    opts.staticRenderFns = options.staticRenderFns
  }
}

组件$mount方法之后执行的render方法中,将当前组件渲染的vnode的parent执行父VNode_parentVnode,建立父子关系。在update方法中,

js 复制代码
vm._vnode.parent === vm.$vnode

在$mount之前会调用initLifecycle(vm)方法。

js 复制代码
export function initLifecycle (vm: Component) {
  const options = vm.$options

  // locate first non-abstract parent
  let parent = options.parent
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    parent.$children.push(vm)
  }

  vm.$parent = parent
  // ...
}

vm.parent用来保留当前vm的父实例,将当前vm存储到父实例的children中。在update过程中,使用prevActiveInstance保留父实例。在完成固件patch之后,如果组件patch过程中又创建了子组件,DOM的插入顺序是先子后父。

总结:

组件化创建三个关键的步骤是:构造子类构造函数、安装组件钩子函数和实例化vnode。因为组件被转化为继承自Vue的子类构造函数,在初始化过程中也是调用_init方法,经过render方法创建虚拟DOM,通过patch和createElm创建真实DOM,和使用普通元素有一些区别,特别是父子关系的处理,在DOM插入时深度遍历,递归调用,先子后父。

相关推荐
吴永琦(桂林电子科技大学)36 分钟前
HTML5
前端·html·html5
爱因斯坦乐39 分钟前
【HTML】纯前端网页小游戏-戳破彩泡
前端·javascript·html
恋猫de小郭44 分钟前
注意,暂时不要升级 MacOS ,Flutter/RN 等构建 ipa 可能会因 「ITMS-90048」This bundle is invalid 被拒绝
android·前端·flutter
大莲芒5 小时前
react 15-16-17-18各版本的核心区别、底层原理及演进逻辑的深度解析--react17
前端·react.js·前端框架
木木黄木木7 小时前
html5炫酷3D文字效果项目开发实践
前端·3d·html5
Li_Ning217 小时前
【接口重复请求】axios通过AbortController解决页面切换过快,接口重复请求问题
前端
胡八一8 小时前
Window调试 ios 的 Safari 浏览器
前端·ios·safari
Dontla8 小时前
前端页面鼠标移动监控(鼠标运动、鼠标监控)鼠标节流处理、throttle、限制触发频率(setTimeout、clearInterval)
前端·javascript
再学一点就睡8 小时前
深拷贝与浅拷贝:代码世界里的永恒与瞬间
前端·javascript
CrimsonHu8 小时前
B站首页的 Banner 这么好看,我用原生 JS + 三大框架统统给你复刻一遍!
前端·javascript·css