专注、坚持

深入 Vue(三) —— Vue 2 中的渲染(草稿)

2021.02.25 by kingcos

开始

上一篇文章中,我们先简单地介绍了 Vue 2 的运行时与编译器的区别,本篇继续尝试深入渲染部分。

仅运行时

import App from './App'

Vue.config.productionTip = false

/* eslint-disable no-new */
new Vue({
  el: '#app',
  render: h => h(App)
})

这里 render 更清晰的代码如下:

new Vue({
  el: '#app',
  render: (fn) => {
    return fn('App', App)
  }
})

调用链路

上回书说到,当我们在仅运行时的 render 函数中断点时,调用栈如下:

Vue -> Vue._init -> Vue.$mount(挂载) -> mountComponent -> Watcher -> Wacther.get -> updateComponent -> Vue._render -> render(渲染)

我们简单从源码里梳理以上的调用关系如下:

// main.js

new Vue({ // <--- HERE
  el: '#app',
  components: { App },
  // template: '<App/>'
  render: (fn) => {
    return fn('App', App)
  }
})

// vue.runtime.esm.js
function Vue(options) {
    this._init(options); // <--- HERE
}

function initMixin$1(Vue) {
    Vue.prototype._init = function (options) {
        if (vm.$options.el) {
            vm.$mount(vm.$options.el); // <--- HERE
        }
    };
}

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

function mountComponent(vm, el, hydrating) {
    var updateComponent;
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    }
    else {
        updateComponent = function () {
            vm._update(vm._render(), hydrating); // <--- HERE
        };
    }
    new Watcher(vm, updateComponent, noop, watcherOptions, true /* isRenderWatcher */); // <--- HERE
    return vm;
}

var Watcher = /** @class */ (function () {
    function Watcher(vm, expOrFn, cb, options, isRenderWatcher) {
        // parse expression for getter
        if (isFunction(expOrFn)) {
            this.getter = expOrFn;
        }
        this.value = this.lazy ? undefined : this.get(); // <--- HERE
    }
    /**
     * Evaluate the getter, and re-collect dependencies.
     */
    Watcher.prototype.get = function () {
        try {
            value = this.getter.call(vm, vm); // <--- HERE
        }
        return value;
    };
    return Watcher;
}());

function renderMixin(Vue) {
    Vue.prototype._render = function () {
        var vm = this;
        var vnode;
        try {
            vnode = render.call(vm._renderProxy, vm.$createElement); // <--- HERE
        }
        return vnode;
    };
}

How _render works?

我们把 _render 源码的核心部分抽出如下:

// main.js
new Vue({
  el: '#app',
  render: (fn) => {
    return fn('App', App)
  }
})

// vue.runtime.esm.js
function renderMixin(Vue) {
    Vue.prototype._render = function () {
        var vm = this;
        // _a: { el: '#app', ... }, render: render 函数, _parentVnode: undefined
        var _a = vm.$options, render = _a.render, _parentVnode = _a._parentVnode;
        
        // render self
        var vnode;
        try {
            // =====>>> HERE <<<=====
            vnode = render.call(vm._renderProxy, vm.$createElement);
        }
        // set parent
        vnode.parent = _parentVnode;
        return vnode;
    };
}

我们将此处的 render 函数的调用及后续链路梳理得更清晰一些:

// this -> vm._renderProxy
// vnode = render(vm.$createElement)
vnode = vm.$createElement('App', App)

function initRender(vm) {
    vm.$createElement = function (a, b, c, d) { return createElement$1(vm, a, b, c, d, true); };
}

function createElement$1(context, tag, data, children, normalizationType, alwaysNormalize) {
    // context: vm, tag: 'App', alwaysNormalize: true, 其他:undefined
    // if (isTrue(alwaysNormalize)) {
    //     normalizationType = ALWAYS_NORMALIZE;
    // }
    return _createElement(context, tag, data, children, normalizationType);
}

function _createElement(context, tag, data, children, normalizationType) {
    var vnode, ns;
    if (typeof tag === 'string') {
        var Ctor = void 0;
        ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag);
        if ((!data || !data.pre) &&
            isDef((Ctor = resolveAsset(context.$options, 'components', tag)))) {
            // component
            vnode = createComponent(Ctor, data, context, children, tag);
        }
    }
    if (isDef(vnode)) {
        if (isDef(data))
            registerDeepBindings(data);
        return vnode;
    }
}

function createComponent(Ctor, data, context, children, tag) {
    if (isUndef(Ctor)) {
        return;
    }
    var 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: ".concat(String(Ctor)), context);
        }
        return;
    }
    // async component
    var asyncFactory;
    // @ts-expect-error
    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)) {
        // @ts-expect-error
        transformModel(Ctor.options, data);
    }
    // extract props
    // @ts-expect-error
    var propsData = extractPropsFromVNodeData(data, Ctor, tag);
    // functional component
    // @ts-expect-error
    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
    var listeners = data.on;
    // replace with listeners with .native modifier
    // so it gets processed during parent component patch.
    data.on = data.nativeOn;
    // @ts-expect-error
    if (isTrue(Ctor.options.abstract)) {
        // abstract components do not keep anything
        // other than props & listeners & slot
        // work around flow
        var slot = data.slot;
        data = {};
        if (slot) {
            data.slot = slot;
        }
    }
    // install component management hooks onto the placeholder node
    installComponentHooks(data);
    // return a placeholder vnode
    // @ts-expect-error
    var name = getComponentName(Ctor.options) || tag;
    var vnode = new VNode(
    // @ts-expect-error
    "vue-component-".concat(Ctor.cid).concat(name ? "-".concat(name) : ''), data, undefined, undefined, undefined, context, 
    // @ts-expect-error
    { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children }, asyncFactory);
    return vnode;
}
function renderMixin(Vue) {
    Vue.prototype._render = function () {
        var vm = this;
        // _a: { el: '#app', ... }, render: render 函数, _parentVnode: undefined
        var _a = vm.$options, render = _a.render, _parentVnode = _a._parentVnode;
        if (_parentVnode && vm._isMounted) {
            // 跳过
            vm.$scopedSlots = normalizeScopedSlots(vm.$parent, _parentVnode.data.scopedSlots, vm.$slots, vm.$scopedSlots);
            if (vm._slotsProxy) {
                syncSetupSlots(vm._slotsProxy, vm.$scopedSlots);
            }
        }
        // set parent vnode. this allows render functions to have access
        // to the data on the placeholder node.
        // vm.$vnode === undefined
        vm.$vnode = _parentVnode;
        // render self
        var vnode;
        try {
            // There's no need to maintain a stack because all render fns are called
            // separately from one another. Nested component's render fns are called
            // when parent component is patched.
            setCurrentInstance(vm);
            currentRenderingInstance = vm;
            vnode = render.call(vm._renderProxy, vm.$createElement);
        }
        catch (e) {
            handleError(e, vm, "render");
            // return error render result,
            // or previous vnode to prevent render error causing blank component
            /* istanbul ignore else */
            if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {
                try {
                    vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e);
                }
                catch (e) {
                    handleError(e, vm, "renderError");
                    vnode = vm._vnode;
                }
            }
            else {
                vnode = vm._vnode;
            }
        }
        finally {
            currentRenderingInstance = null;
            setCurrentInstance();
        }
        // if the returned array contains only a single node, allow it
        if (isArray(vnode) && vnode.length === 1) {
            vnode = vnode[0];
        }
        // return empty vnode in case the render function errored out
        if (!(vnode instanceof VNode)) {
            if (process.env.NODE_ENV !== 'production' && 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;
    };
}

function setCurrentInstance(vm) {
    if (vm === void 0) { vm = null; }
    if (!vm)
        currentInstance && currentInstance._scope.off();
    currentInstance = vm;
    vm && vm._scope.on();
}

How _update works?

function lifecycleMixin(Vue) {
    Vue.prototype._update = function (vnode, hydrating) {
        var vm = this;
        var prevEl = vm.$el;
        var prevVnode = vm._vnode;
        var restoreActiveInstance = setActiveInstance(vm);
        vm._vnode = vnode;
        // Vue.prototype.__patch__ is injected in entry points
        // based on the rendering backend used.
        if (!prevVnode) {
            // initial render
            // ===> HERE <===
            vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
        }
        else {
            // updates
            vm.$el = vm.__patch__(prevVnode, vnode);
        }
        restoreActiveInstance();
        // 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
        var wrapper = vm;
        while (wrapper &&
            wrapper.$vnode &&
            wrapper.$parent &&
            wrapper.$vnode === wrapper.$parent._vnode) {
            wrapper.$parent.$el = wrapper.$el;
            wrapper = wrapper.$parent;
        }
        // updated hook is called by the scheduler to ensure that children are
        // updated in a parent's updated hook.
    };
}

Tips

Vue 2 中的 vm

function initMixin$1(Vue) {
    Vue.prototype._init = function (options) {
        var vm = this;
        // expose real self
        vm._self = vm;
    }
}

我们在 Vue 源码中经常看到 vm,其在 _init 中被赋值了 self —— 即 Vue 对象本身。vm 本身其实是 ViewModel 的缩写。

fn.call() & fn.apply()

function fn(arg1, arg2) {
    console.log(this, arg1, arg2);
}

fn();     // Window {} undefined undefined
fn(1);    // Window {} 1 undefined
fn(1, 2); // Window {} 1 2

fn.call();         // Window {} undefined undefined
fn.call(1);        // Number {1} undefined undefined
fn.call(1, 2);     // Number {1} 2 undefined
fn.call(1, 2, 3);  // Number {1} 2 3

fn.apply();           // Window {} undefined undefined
fn.apply(1);          // Number {1} undefined undefined
fn.apply(1, [2]);     // Number {1} 2 undefined
fn.apply(1, [2, 3]);  // Number {1} 2 3

call() 的功能也是函数调用,但差别在于第一个参数将被当作 this 指向;apply() 的功能则也是函数调用,但不同的是其最多有两个参数,且第二个参数可以理解为一个参数数组,摊开后传递给调用的函数。

那么此时我有个疑问,如何实现类似两者功能的函数呢?

function fn(arg1, arg2) {
    console.log(this, arg1, arg2);
}

// 使用原型让所有函数均可调用
Function.prototype.v_call = function (ctx, ...args) {
    // ctx 为 this 指向,默认为全局 window
    ctx = ctx || window;
    // 将当前函数设为上下文原型的一个属性,避免非对象类型无法保存 func
    ctx.__proto__.func = this
    // 执行函数并保存返回值,注意这里要避免在原型上执行
    const result = ctx.func(...args)
    // 删除上下文原型的 fn 属性
    delete ctx.__proto__.func;
    // 返回函数执行结果
    return result;
}

Function.prototype.v_apply = function (ctx, args) {
    // this 指向,默认为全局 window
    ctx = ctx || window;
    // 将当前函数设为上下文原型的一个属性,避免非对象类型无法保存 func
    ctx.__proto__.func = this
    // 执行函数并保存返回值,注意这里要避免在原型上执行
    const result = ctx.func(...(args || []))
    // 删除上下文原型的 fn 属性
    delete ctx.__proto__.func;
    // 返回函数执行结果
    return result;
}

fn.v_call();         // Window {} undefined undefined
fn.v_call(1);        // Number {1} undefined undefined
fn.v_call(1, 2);     // Number {1} 2 undefined
fn.v_call(1, 2, 3);  // Number {1} 2 3

fn.v_apply();           // Window {} undefined undefined
fn.v_apply(1);          // Number {1} undefined undefined
fn.v_apply(1, [2]);     // Number {1} 2 undefined
fn.v_apply(1, [2, 3]);  // Number {1} 2 3

Q: JavaScript 内置的 call()、apply() 函数的源码具体在哪里 A: `call() 和 apply() 方法是 JavaScript 中的内置函数,它们的实现源码在 JavaScript 引擎中,并不是开放的标准库函数,因此无法直接查看其源码。不过,根据规范,它们的实现方式可以由各个 JavaScript 引擎厂商自行实现,只需要遵循规范定义的函数签名和行为即可。不同 JavaScript 引擎的实现方式可能会有所不同。

void 0

在 JavaScript 中,通常使用 void 0 来代替 undefined。这是因为 undefined 不是 JavaScript 的保留字,可能被程序中的其他变量或函数名覆盖。使用 void 0 可以保证得到的始终是 undefined,而不会受到其他因素的影响。

const undefined = 1;
let a = undefined;
console.log(a);
// 1

1

Watcher 的具体功能类似 miniVue 实现中的 Watcher,负责依赖收集与通知。这里我们暂时略过 Watcher 的源码分析,先对 _render_update 一探究竟:

function lifecycleMixin(Vue) {
    Vue.prototype._update = function (vnode, hydrating) {
        var vm = this;
        var prevEl = vm.$el;
        var prevVnode = vm._vnode;
        var restoreActiveInstance = setActiveInstance(vm);
        vm._vnode = vnode;
        // Vue.prototype.__patch__ is injected in entry points
        // based on the rendering backend used.
        if (!prevVnode) {
            // initial render
            // ===> HERE <===
            vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
        }
        else {
            // updates
            vm.$el = vm.__patch__(prevVnode, vnode);
        }
        restoreActiveInstance();
        // 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
        var wrapper = vm;
        while (wrapper &&
            wrapper.$vnode &&
            wrapper.$parent &&
            wrapper.$vnode === wrapper.$parent._vnode) {
            wrapper.$parent.$el = wrapper.$el;
            wrapper = wrapper.$parent;
        }
        // updated hook is called by the scheduler to ensure that children are
        // updated in a parent's updated hook.
    };
}

function renderMixin(Vue) { 
    Vue.prototype._render = function () {
        var vm = this;
        // _a: { el: '#app', ... }, render: render 函数, _parentVnode: undefined
        var _a = vm.$options, render = _a.render, _parentVnode = _a._parentVnode;
        // 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 {
            // There's no need to maintain a stack because all render fns are called
            // separately from one another. Nested component's render fns are called
            // when parent component is patched.
            setCurrentInstance(vm);
            currentRenderingInstance = vm;
            vnode = render.call(vm._renderProxy, vm.$createElement);
        }
        catch (e) {}
        finally {
            currentRenderingInstance = null;
            setCurrentInstance();
        }
        // set parent
        vnode.parent = _parentVnode;
        return vnode;
    };
}
点击此处即可查看完整源码
function renderMixin(Vue) { 
    Vue.prototype._render = function () {
        var vm = this;
        // _a: { el: '#app', ... }, render: render 函数, _parentVnode: undefined
        var _a = vm.$options, render = _a.render, _parentVnode = _a._parentVnode;
        if (_parentVnode && vm._isMounted) {
            // 跳过
            vm.$scopedSlots = normalizeScopedSlots(vm.$parent, _parentVnode.data.scopedSlots, vm.$slots, vm.$scopedSlots);
            if (vm._slotsProxy) {
                syncSetupSlots(vm._slotsProxy, vm.$scopedSlots);
            }
        }
        // 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 {
            // There's no need to maintain a stack because all render fns are called
            // separately from one another. Nested component's render fns are called
            // when parent component is patched.
            setCurrentInstance(vm);
            currentRenderingInstance = vm;
            vnode = render.call(vm._renderProxy, vm.$createElement);
        }
        catch (e) {
            // 跳过
            handleError(e, vm, "render");
            // return error render result,
            // or previous vnode to prevent render error causing blank component
            /* istanbul ignore else */
            if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {
                try {
                    vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e);
                }
                catch (e) {
                    handleError(e, vm, "renderError");
                    vnode = vm._vnode;
                }
            }
            else {
                vnode = vm._vnode;
            }
        }
        finally {
            currentRenderingInstance = null;
            setCurrentInstance();
        }
        // if the returned array contains only a single node, allow it
        // 跳过
        if (isArray(vnode) && vnode.length === 1) {
            vnode = vnode[0];
        }
        // return empty vnode in case the render function errored out
        // 跳过
        if (!(vnode instanceof VNode)) {
            if (process.env.NODE_ENV !== 'production' && 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;
    };
}

挂载时机