在后面文章中,具体探讨了Vue申明流程,Vuejs响应式实现流程,虚构Dom及模版编译流程,感兴趣的童鞋能够本人查看。

  1. Vue申明过程。
  2. Vuejs响应式实现流程。
  3. 虚构Dom及模版编译。

本篇文章将持续探讨Vuejs中一些罕用办法的实现过程,蕴含$set,component,extend。

  • 为什么探讨$set办法?

在Vue中能够通过this.$set()为一个响应式数据增加新的属性或者响应式数组增加新项,外部是如何实现的?

$delete外部实现和$set外部实现类似,探讨一个,能够触类旁通。

  • 为什么探讨component办法?

component办法用于注册组件,探讨此办法,能够弄清楚vuejs外部是如何注册及渲染组件的。

$set

在官网文档中,实例办法$set是Vue静态方法set的一个别名,二者实现原理一样。

应用set办法能够在响应式数据中增加新的属性或者新项:

<body>  <div id="app">    <span>      <strong>{{person.name}}</strong>    </span>    <span>{{person.age}}</span>  </div>  <script>    let vm = new Vue({      el: '#app',      data() {        return {          person: {            name: 'zs'          }        }      }    })    vm.$set(vm.$data.person, 'age', 12)  </script></body>

$set办法定义在core/instance/state.js文件中:

export function stateMixin(Vue: Class<Component>) {    Vue.prototype.$set = set    Vue.prototype.$delete = del}

其调用的是set函数,该函数定义在core/observer/index.js中:

  1. 判断指标target是否是一个响应式对象,如果指标没有定义或者是一个非响应式对象,那么在测试环境下就会收回正告:
if (process.env.NODE_ENV !== 'production' &&    (isUndef(target) || isPrimitive(target))) {    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)}
  1. 如果指标target是一个数组,那么首先判断key是否是一个无效的索引数字,而后判断target数组是否蕴含key传入的索引,如果不能蕴含,则调用length批改数组长度,而后再调用splice批改数组插入值。
if (Array.isArray(target) && isValidArrayIndex(key)) {    target.length = Math.max(target.length, key)    target.splice(key, 1, val)    return val}

在后面响应式原理中曾经说过,通过splice批改数组,可能触发响应。

  1. 如果新增的属性曾经存在,那么阐明此属性曾经增加了响应式,间接返回后果即可。
if (key in target && !(key in Object.prototype)) {    target[key] = val    return val}
  1. 获取target上存储的ob对象,如果存在ob对象,那么就通过Object.defineProperty为新增加的属性增加getter/setter。而后调用ob.dep.notify办法触发更新。
const ob = (target: any).__ob__// target._isVue代表target是Vue实例// ob && ob.vmCount 代表target指向的是$dataif (target._isVue || (ob && ob.vmCount)) {    process.env.NODE_ENV !== 'production' && warn(        'Avoid adding reactive properties to a Vue instance or its root $data ' +        'at runtime - declare it upfront in the data option.'    )    return val}if (!ob) {    target[key] = val    return val}defineReactive(ob.value, key, val)ob.dep.notify()return val

extend

Vue.extend是应用一个蕴含组件选项的对象创立一个继承自Vue的子类。

官网示例如下:

<div id="mount-point"></div>
// 创立结构器var Profile = Vue.extend({  template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>',  data: function () {    return {      firstName: 'Walter',      lastName: 'White',      alias: 'Heisenberg'    }  }})// 创立 Profile 实例,并挂载到一个元素上。new Profile().$mount('#mount-point')

这样做的益处是:

在通常vue-cli我的项目中,咱们能够通过路由将不同的Dom挂载到id为app的div中,然而相似alert等,应该增加在body节点上,此时就能够用Vue.extend定义一个Alert类,而后在适合的机会渲染并挂载到body中:

const alertComponent = new Alert().$mount()document.body.appendChild(alertComponent.$el)

extend办法定义在core/global-api/extend.js中:

  1. 创立原型链继承,核心内容是将新生成的子类的prototype指向一个原型为Vue的对象:Sub.prototype = Object.create(Super.prototype)
extendOptions = extendOptions || {}const Super = thisconst SuperId = Super.cidconst cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})if (cachedCtors[SuperId]) {    return cachedCtors[SuperId]}const name = extendOptions.name || Super.options.nameif (process.env.NODE_ENV !== 'production' && name) {    validateComponentName(name)}const Sub = function VueComponent(options) {    this._init(options)}Sub.prototype = Object.create(Super.prototype)Sub.prototype.constructor = SubSub.cid = cid++Sub.options = mergeOptions(    Super.options,    extendOptions)Sub['super'] = Super
  1. 增加实例成员和动态成员:
if (Sub.options.props) {    initProps(Sub)}if (Sub.options.computed) {    initComputed(Sub)}Sub.extend = Super.extendSub.mixin = Super.mixinSub.use = Super.useASSET_TYPES.forEach(function (type) {    Sub[type] = Super[type]})if (name) {    Sub.options.components[name] = Sub}Sub.superOptions = Super.optionsSub.extendOptions = extendOptionsSub.sealedOptions = extend({}, Sub.options)cachedCtors[SuperId] = Sub

component

Vue.component办法用于注册全局组件,本局部将摸索全局组件如何实例化及渲染。

Vue.component办法定义在core/global-api/assets.js文件中:

ASSET_TYPES.forEach(type => {    Vue[type] = function (        id: string,        definition: Function | Object    ): Function | Object | void {        if (!definition) {            return this.options[type + 's'][id]        } else {            if (process.env.NODE_ENV !== 'production' && type === 'component') {                validateComponentName(id)            }            if (type === 'component' && isPlainObject(definition)) {                definition.name = definition.name || id                // 调用extend办法生成一个Vue的子类                definition = this.options._base.extend(definition)            }            if (type === 'directive' && typeof definition === 'function') {                definition = { bind: definition, update: definition }            }            this.options[type + 's'][id] = definition            return definition        }    }})

在注册时,会调用extend办法生成一个子类并增加到全局的Vue.options.components对象上。

组件Vnode创立过程

上面是一个应用组件的示例:

const Comp = Vue.component('comp', {    template: '<div>Hello Component</div>'})const vm = new Vue(    {        el: '#app',        render(h) {            return h(Comp)        }    })

在render函数中通过h(Comp)的形式创立组件Vnode,在虚构Dom一章中咱们说过h参数其实就是createElement函数,该函数定义在core/vdeom/create-element.js中,外部调用了_createElement函数:

export function _createElement(    context: Component,    tag?: string | Class<Component> | Function | Object,    data?: VNodeData,    children?: any,    normalizationType?: number): VNode | Array<VNode> {    // ...    if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {        // 调用createComponent函数生成组件Vnode        vnode = createComponent(Ctor, data, context, children, tag)    }    // ...}

在此函数中,如果Ctor是一个组件,那么就调用createComponent生成Vnode。

export function createComponent (    Ctor: Class<Component> | Function | Object | void,    data: ?VNodeData,    context: Component,    children: ?Array<VNode>,    tag?: string  ): VNode | Array<VNode> | void {    // ...    installComponentHooks(data)    const name = Ctor.options.name || tag    const vnode = new VNode(      `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,      data, undefined, undefined, undefined, context,      { Ctor, propsData, listeners, tag, children },      asyncFactory    )    // ...      return vnode  }

在createComponent办法中会调用installComponentHooks函数合并componentVNodeHooks 中预约义的钩子函数和用户传入的钩子函数。

const componentVNodeHooks = {    init(vnode: VNodeWithData, hydrating: boolean): ?boolean {        if (            vnode.componentInstance &&            !vnode.componentInstance._isDestroyed &&            vnode.data.keepAlive        ) {            const mountedNode: any = vnode            componentVNodeHooks.prepatch(mountedNode, mountedNode)        } else {            // 创立组件实例并增加到vnode.componentInstance属性上。            const child = vnode.componentInstance = createComponentInstanceForVnode(                vnode,                activeInstance            )            // 执行$mount办法,将组件挂载到页面            child.$mount(hydrating ? vnode.elm : undefined, hydrating)        }    },    prepatch(oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {        //...    },    insert(vnode: MountedComponentVNode) {        //...    },    destroy(vnode: MountedComponentVNode) {        // ...    }}

在预约义的init钩子函数中,会创立组件实例并调用$mount办法将组件挂在到页面。

export function createComponentInstanceForVnode(    vnode: any, // we know it's MountedComponentVNode but flow doesn't    parent: any, // activeInstance in lifecycle state): Component {    const options: InternalComponentOptions = {        _isComponent: true,        _parentVnode: vnode,        parent    }    const inlineTemplate = vnode.data.inlineTemplate    if (isDef(inlineTemplate)) {        options.render = inlineTemplate.render        options.staticRenderFns = inlineTemplate.staticRenderFns    }    // 创立组件实例    return new vnode.componentOptions.Ctor(options)}

那么init钩子函数什么时候被执行?

通过虚构Dom的工作机制能够看出,当页面首次渲染和数据变动的时候会执行patch函数,在patch函数外部会调用createComponent函数,此函数定义在core/vdom/patch.js中:

function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {    let i = vnode.data    if (isDef(i)) {        const isReactivated = isDef(vnode.componentInstance) && i.keepAlive        // 1. 调用init钩子函数,组件在创立vnode的时候曾经增加了此钩子函数        if (isDef(i = i.hook) && isDef(i = i.init)) {            i(vnode, false /* hydrating */)        }        // 2. 判断vnode是否定义了componentInstance属性,此属性在init钩子函数中用于寄存组件实例        if (isDef(vnode.componentInstance)) {             // 调用其余钩子函数,用于设置部分作用域款式等            initComponent(vnode, insertedVnodeQueue)            // 把组件dom插入到父元素中            insert(parentElm, vnode.elm, refElm)            if (isTrue(isReactivated)) {                reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)            }            return true        }    }}

在此函数中实现了init钩子函数调用及挂载dom。

此时组件从注册到渲染实现的整个流程曾经梳理结束,总结以下,能够分为以下几个步骤:

  1. Vue.component注册组件。
  2. 在申明Vue组件的render函数时,用h生成组件Vnode
  3. 在h函数外部会调用createComponent创立组件Vnode,此过程中会增加内置init钩子函数。
  4. 首次渲染或者数据变更时会调用patch函数,此函数外部会调用里一个createComponent函数。
  5. createComponent函数会调用init钩子函数生成组件实例。
  6. init钩子函数外部会创立组件实例并调用$mount函数渲染。
  7. createComponent会将渲染后的dom增加到父元素中。