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