专注、坚持

深入 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', data: App, children: undeifined, normalizationType: undeifined, alwaysNormalize: true
    return _createElement(context, tag, data, children, normalizationType);
}

function _createElement(context, tag, data, children, normalizationType) {
    // context: vm, tag: 'App', data: App, children: undeifined, normalizationType: undeifined
    var vnode, ns;
    if (typeof tag === 'string') {
        var Ctor = void 0; // undefined
        // !data.pre === true
        if ((!data || !data.pre) &&
            isDef((Ctor = resolveAsset(context.$options, 'components', tag)))) {
            // Ctor === context.$options['components']['App']
            // component
            vnode = createComponent(Ctor, data, context, children, tag);
        }
    }
    if (isDef(vnode)) {
        if (isDef(data))
            registerDeepBindings(data);
        return vnode;
    }
}

function resolveAsset(options, type, id, warnMissing) {
    // options: $options, type: 'components', id: 'App', warnMissing: undefined
    var assets = options[type];
    // check local registration variations first
    if (hasOwn(assets, id))
        return assets[id];
}

function createComponent(Ctor, data, context, children, tag) {
    // Ctor: context.$options['components']['App'], data, 
    var baseCtor = context.$options._base;
    // plain options object: turn it into a constructor
    if (isObject(Ctor)) {
        Ctor = baseCtor.extend(Ctor);
    }
    // async component
    var asyncFactory;
    data = data || {};
    // resolve constructor options in case global mixins are applied after
    // component constructor creation
    resolveConstructorOptions(Ctor);
    // extract props
    // @ts-expect-error
    var propsData = extractPropsFromVNodeData(data, Ctor, tag);
    // 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;
    // 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;
}

var VNode = /** @class */ (function () {
    function VNode(tag, data, children, text, elm, context, componentOptions, asyncFactory) {
        this.tag = tag;
        this.data = data;
        this.children = children;
        this.text = text;
        this.elm = elm;
        this.ns = undefined;
        this.context = context;
        this.fnContext = undefined;
        this.fnOptions = undefined;
        this.fnScopeId = undefined;
        this.key = data && data.key;
        this.componentOptions = componentOptions;
        this.componentInstance = undefined;
        this.parent = undefined;
        this.raw = false;
        this.isStatic = false;
        this.isRootInsert = true;
        this.isComment = false;
        this.isCloned = false;
        this.isOnce = false;
        this.asyncFactory = asyncFactory;
        this.asyncMeta = undefined;
        this.isAsyncPlaceholder = false;
    }
    Object.defineProperty(VNode.prototype, "child", {
        // DEPRECATED: alias for componentInstance for backwards compat.
        /* istanbul ignore next */
        get: function () {
            return this.componentInstance;
        },
        enumerable: false,
        configurable: true
    });
    return VNode;
}());

简而言之,render 函数最后构造了一个 VNode 对象。

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

App 是如何被加载的?

Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0_vue__ = __webpack_require__("./node_modules/vue/dist/vue.runtime.esm.js");
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_1__App__ = __webpack_require__("./src/App.vue");

__WEBPACK_IMPORTED_MODULE_0_vue__["default"].config.productionTip = false;

new __WEBPACK_IMPORTED_MODULE_0_vue__["default"]({
  el: '#app',
  components: { App: __WEBPACK_IMPORTED_MODULE_1__App__["a" /* default */] },
  render: function render(fn) {
    return fn('App', __WEBPACK_IMPORTED_MODULE_1__App__["a" /* default */]);
  }
});

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

挂载时机