关于javascript:大前端进阶读懂vuejs源码4

34次阅读

共计 7353 个字符,预计需要花费 19 分钟才能阅读完成。

在后面文章中,具体探讨了 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 指向的是 $data
if (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 = this
const SuperId = Super.cid
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)
}

const 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
  1. 增加实例成员和动态成员:
if (Sub.options.props) {initProps(Sub)
}
if (Sub.options.computed) {initComputed(Sub)
}

Sub.extend = Super.extend
Sub.mixin = Super.mixin
Sub.use = Super.use

ASSET_TYPES.forEach(function (type) {Sub[type] = Super[type]
})

if (name) {Sub.options.components[name] = Sub
}

Sub.superOptions = Super.options
Sub.extendOptions = extendOptions
Sub.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 增加到父元素中。

正文完
 0