开始
在上一篇文章中,我们先简单地介绍了 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;
};
}