Vue render函数实现

918 阅读5分钟

render

renderMixin

给Vue原型添加$nextTick和_render两个方法

$nextTick

代码位于/src/core/util/next-tick.js

JS 执行是单线程的,它是基于事件循环的

  1. 所有同步任务都在主线程上执行,组成一个执行栈

  2. 主线程之外还有一个任务队列,当异步任务运行得到结果,就在任务队列中放置一个事件

  3. 当执行栈所有同步任务执行完毕,js去读取任务队列,那些已经执行完的异步任务进入执行栈,进行执行

  4. 主线程循环上面几个步骤

    const callbacks = []
    let pending = false
    export function nextTick (cb?: Function, ctx?: Object) {
        let _resolve
        callbacks.push(() => {
            if (cb) {
            try {
                cb.call(ctx)
            } catch (e) {
                handleError(e, ctx, 'nextTick')
            }
            } else if (_resolve) {
            _resolve(ctx)
            }
        })
        if (!pending) {
            pending = true
            timerFunc()
        }
        ...
    }

nextTick源码可以看出,我们在nextTick中传入的回调函数会被存在callbacks数组中,最后一次性在timerFunc中执行

这里使用 callbacks而不是直接在nextTick中执行回调函数的原因是保证在同一个tick内多次执行nextTick,不会开启多个异步任务,而把这些异步任务都压成一个同步任务,在下一个tick执行完毕,这里通过pending锁定。

// $flow-disable-line
    if (!cb && typeof Promise !== 'undefined') {
        return new Promise(resolve => {
        _resolve = resolve
        })
    }

当nextTick不传入回调的时候,我们可以通过this.$nextTick().then去调用,即在函数中把Promise赋值给了_resolve

    let timerFunc

    if (typeof Promise !== 'undefined' && isNative(Promise)) {
        const p = Promise.resolve()
        timerFunc = () => {
            p.then(flushCallbacks)
            if (isIOS) setTimeout(noop)
        }
        isUsingMicroTask = true
    } else if (!isIE && typeof MutationObserver !== 'undefined' && (
        isNative(MutationObserver) ||
        // PhantomJS and iOS 7.x
        MutationObserver.toString() === '[object MutationObserverConstructor]'
        )) {
        let counter = 1
        const observer = new MutationObserver(flushCallbacks)
        const textNode = document.createTextNode(String(counter))
        observer.observe(textNode, {
            characterData: true
        })
        timerFunc = () => {
            counter = (counter + 1) % 2
            textNode.data = String(counter)
        }
        isUsingMicroTask = true
    } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
        timerFunc = () => {
            setImmediate(flushCallbacks)
        }
    } else {
        // Fallback to setTimeout.
        timerFunc = () => {
            setTimeout(flushCallbacks, 0)
        }
    }

Vue会先去判断是否原生支持Promise,如果支持就通过Promise.resolve().then(flushCallbacks)

如果不支持Promise,就降级为MutationObserver

如果Promise和MutationObserver都不支持,就执行setImmediate

以上三个都不支持就用setTimeout代替

    function flushCallbacks () {
        pending = false
        const copies = callbacks.slice(0)
        callbacks.length = 0
        for (let i = 0; i < copies.length; i++) {
            copies[i]()
        }
    }

flushCallbacks很简单,就是把pending重置false,拷贝callbacks,对callbacks清0,统一执行传入的回调队列

_render

    vnode = render.call(vm._renderProxy, vm.$createElement)

把Vue实例渲染成VNode形式,定义在src/core/instance/render.js中

    initProxy = function initProxy (vm) {
        if (hasProxy) {
            // determine which proxy handler to use
            const options = vm.$options
            const handlers = options.render && options.render._withStripped
                ? getHandler
                : hasHandler
            vm._renderProxy = new Proxy(vm, handlers)
        } else {
            vm._renderProxy = vm
        }
    }

在开发环境会去调用initProxy方法,否则就是vm本身,该方法会在new Vue()时调用_init方法执行,定义在src/core/instance/proxy.js中

export function initRender (vm: Component) {
    ...省略
    vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)

    vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
    ...省略
}

vm.$createElement 方法定义是在执行 initRender 方法的时候, 调用vm.$createElement就是调用createElement方法,该方法定义在src/core/vdom/create-element.js中

createElement

createElement是在执行initRender时注册到当前实例的, 代码在src/core/instance/render.js中

    export function initRender (vm: Component) {
        ...省略
        
        vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
        
        vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

        ...省略
    }

createElement方法实际上是对_createElement方法的封装

    export function createElement (
        context: Component,
        tag: any,
        data: any,
        children: any,
        normalizationType: any,
        alwaysNormalize: boolean
        ): VNode | Array<VNode> {
        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)
    }

在createElement中,首先检测data的类型,通过判断data是不是数组,以及是不是基本类型,来判断data是否传入.如果没有传入,则将所有的参数向前赋值,且data = undifined. 然后,判断判断传入的alwaysNormalize参数是否为真,为真的话,赋值给ALWAYS_NORMALIZE常量.最后,再调用_createElement函数,可以看到,createElement是对参数做了一些处理以后,将其传给_createElement函数

_createElement,第一步判断data不能是响应式的,vnode中的data如果是响应式的就抛出警告,返回一个空的vnode

    export function _createElement (
        context: Component,
        tag?: string | Class<Component> | Function | Object,
        data?: VNodeData,
        children?: any,
        normalizationType?: number
    ): VNode | Array<VNode> {
        if (isDef(data) && isDef((data: any).__ob__)) {
            process.env.NODE_ENV !== '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()
        }
        ...
    }

normalize-children

对传入的children进行拍平, 代码在src/core/vdom/helpers/normalize-children.js中

// 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)
    }

simpleNormalizeChildren

主要完成的功能是将children类数组的第一层转换为一个一维数组,当children中包含组件是,函数式组件可能会返回一个数组而不是单独的根节点.因此,当child是数组时,我们 把整个数组利用Array.prototype.concat进行一次抹平.这里只进行1层抹平,函数式组件已经规范化了自己的子组件

// 1. When the children contains components - because a functional component
// may return an Array instead of a single root. In this case, just a simple
// normalization is needed - if any child is an Array, we flatten the whole
// thing with Array.prototype.concat. It is guaranteed to be only 1-level deep
// because functional components already normalize their own children.
    export function simpleNormalizeChildren (children: any) {
        for (let i = 0; i < children.length; i++) {
            if (Array.isArray(children[i])) {
            return Array.prototype.concat.apply([], children)
            }
        }
        return children
    }

normalizeChildren

当子级包含始嵌套数组,template,slot,v-for,手写的render函数,判断children中的元素是不是数组,如果是的话,就递归调用数组,并将每个元素保存在数组中返回

// 2. When the children contains constructs that always generated nested Arrays,
// e.g. <template>, <slot>, v-for, or when the children is provided by user
// with hand-written render functions / JSX. In such cases a full normalization
// is needed to cater to all possible types of children values.
    export function normalizeChildren (children: any): ?Array<VNode> {
        return isPrimitive(children)
            ? [createTextVNode(children)]
            : Array.isArray(children)
            ? normalizeArrayChildren(children)
            : undefined
    }

createComponent

我们在讲render函数的时候提到,_render会通过执行vm.createElement去实现返回vnode,而vm.createElement我们在讲createElement中也提到过,没有看过的同学可以点击链接看下

render函数实现) createElement

_createElement回顾

下面我们来讲接createComponent,先回顾下_createElement的实现,如果是常规html节点则创建一个vnode,否则创建一个组件vnode

// src/core/vdom/create-element.js
    if (typeof tag === 'string') {
        let Ctor
        ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
        if (config.isReservedTag(tag)) {
            // platform built-in elements
            if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn)) {
                warn(
                `The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
                context
                )
            }
            vnode = new VNode(
                config.parsePlatformTagName(tag), data, children,
                undefined, undefined, context
            )
            } else if ((!data || !data.pre) && 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)
    }

正式进入createComponent

主要就是通过Vue.extend实例化子组件,分一下几个步骤,代码在src/core/vdom/create-component.js中

  • Vue.extend实例化子组件
  • resolveAsyncComponent处理异步组件
  • resolveConstructorOptions合并配置
  • transformModel处理v-model
  • extractPropsFromVNodeData对props处理
  • createFunctionalComponent处理函数式组件
  • 对events做处理
  • installComponentHooks安装全局钩子
  • 返回子组件vnode
    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
        }

        const baseCtor = context.$options._base

        // plain options object: turn it into a constructor
        if (isObject(Ctor)) {
            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.
                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
        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
        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
    }

Vue.extend

Vue.extend实际上就是构造一个Vue的子类,通过原型继承的方式把一个纯对象转换一个继承于Vue的构造器Sub并返回,然后对Sub这个对象本身扩展了一些属性,如扩展options,添加全局API等;并且对配置中的props和computed做了初始化工作;最后对于这个Sub构造函数做了缓存,避免多次执行 Vue.extend的时候对同一个子组件重复构造

代码定义在src/core/global-api/extend.js

    Vue.extend = function (extendOptions: Object): Function {
        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

        // 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(Sub)
        }
        if (Sub.options.computed) {
            initComputed(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
    }

installComponentHooks

installComponentHooks安装全局钩子,就是遍历componentVNodeHooks对象把钩子函数合并到data.hook上,在合并过程中,如果某个时机的钩子已经存在 data.hook 中,那么通过执行mergeHook函数做合并,依次执行这两个钩子函数

const hooksToMerge = Object.keys(componentVNodeHooks)

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
        }
    }
}

const componentVNodeHooks = {
    init(vnode: VNodeWithData, hydrating: boolean): ?boolean {
        if (
            vnode.componentInstance &&
            !vnode.componentInstance._isDestroyed &&
            vnode.data.keepAlive
        ) {
            // kept-alive components, treat as a patch
            const mountedNode: any = vnode // work around flow
            componentVNodeHooks.prepatch(mountedNode, mountedNode)
        } else {
            const child = vnode.componentInstance = createComponentInstanceForVnode(
                vnode,
                activeInstance
            )
            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
        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 */)
            }
        }
    }
}

大佬留步,Star一下,感谢