从Vue官网得到源码(https://unpkg.com/vue@next),拷贝到本地文件,然后创建如下html:
<html>
<head></head>
<body>
<div id="counter">
Counter: {{ counter }}
</div>
<script src="./vue.js"></script>
<script>
const Counter = {
data() {
return {
counter: 0,
}
},
mounted() {
setTimeout(() => {
this.counter++
}, 1000)
}
}
debugge
let app = Vue.createApp(Counter);
app.mount('#counter')
</script>
</body>
</html>
用浏览器访问可以看到我们自定义的{{counter}}内存被正确的替换成了下面js中声明的Counter对象中的值,并且mounted中的方法也被执行了,赋值发生后页面也自动更新了。
问题:这一切是怎么做到的?
答案:从断点开始慢慢阅读Vue做了什么。
首先,Vue.js文件声明了一个Vue变量,通过立即执行函数,在内部做了很多变量和函数声明,而暴露给外部使用的只有一部分:
var Vue = (function(exports){
...
exports.createApp = createApp;
...
return exports;
}({}))
看下createApp做了什么:
const createApp = ((...args) => {
// createApp 得到实例和上下文 并相互绑定
const app = ensureRenderer().createApp(...args);
{
// 给上下文注入2个属性检测方法
injectNativeTagCheck(app);
injectCustomElementCheck(app);
}
// 取出我们创建的实例上的 mount 方法
const { mount } = app;
// 重新赋值一个 所以我们创建好实例后调用的mount其实是这个
app.mount = (containerOrSelector) => {
// 得到模板挂载DOM节点 所有渲染出来的节点都会被挂载到这个下面
const container = normalizeContainer(containerOrSelector);
if (!container)
return;
const component = app._component;
// 实例上的 _component 在初始化的时候就是 我们声明的实例参数对象 它此时不是函数
if (!isFunction(component) && !component.render && !component.template) {
// 赋值 template
component.template = container.innerHTML;
}
// clear content before mounting
// 清除原DOM节点的内容 不需要了 因为会整体被替换成vue渲染的
container.innerHTML = '';
// 执行 mount 后得到一个代理 proxy:
const proxy = mount(container);
if (container instanceof Element) {
// 修改2个属性
container.removeAttribute('v-cloak');
container.setAttribute('data-v-app', '');
}
return proxy;
};
return app;
});
它做了如下工作:
1. 创建全局render,初始化好必备的渲染函数
2. 调用render的createApp得到一个app实例
3. 注入2个方法
4. 重写实例mount方法
5. 返回app实例
我们重点看下1和2
function baseCreateRenderer(options, createHydrationFns) {
...
声明了很多方法 这些方法以后会用到 现在先放一放
...
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
}
function createAppContext() {
return {
app: null,
config: {
isNativeTag: NO,
performance: false,
globalProperties: {},
optionMergeStrategies: {},
isCustomElement: NO,
errorHandler: undefined,
warnHandler: undefined
},
mixins: [],
components: {},
directives: {},
provides: Object.create(null)
};
}
let uid$1 = 0;
function createAppAPI(render, hydrate) {
return function createApp(rootComponent, rootProps = null) {
if (rootProps != null && !isObject(rootProps)) {
warn(`root props passed to app.mount() must be an object.`);
rootProps = null;
}
// 上下文 实例和上下文相互持有绑定对应
const context = createAppContext();
const installedPlugins = new Set();
let isMounted = false;
const app = (context.app = {
_uid: uid$1++,
_component: rootComponent,
_props: rootProps,
_container: null,
_context: context,
version,
get config() {
return context.config;
},
set config(v) {
{
warn(`app.config cannot be replaced. Modify individual options instead.`);
}
},
...
// 挂载到根节点下
mount(rootContainer, isHydrate) {
...
},
...
});
return app;
};
}
可以看到,除了得到app实例外,其实还有一个app上下文和实例相互指向。
至此,createApp的工作简要的分析完了。接下来看下我们调用app实例上的mount方法做了什么:
mount(rootContainer, isHydrate) {
if (!isMounted) {
// 创建一个虚拟DOM节点
const vnode = createVNode(rootComponent, rootProps);
console.log('cur vnode: ', vnode)
// store app context on the root VNode.
// this will be set on the root instance on initial mount.
// 绑定vnode的上下文
vnode.appContext = context;
// HMR root reload
{
context.reload = () => {
render(cloneVNode(vnode), rootContainer);
};
}
if (isHydrate && hydrate) {
hydrate(vnode, rootContainer);
}
else {
// 渲染:把虚拟dom节点转化为实际的dom节点并插入到实际dom根节点下
render(vnode, rootContainer);
}
isMounted = true;
// 实例 和 dom根节点相互指向对方
app._container = rootContainer;
rootContainer.__vue_app__ = app;
{
devtoolsInitApp(app, version);
}
return vnode.component.proxy;
}
else {
warn(`App has already been mounted.\n` +
`If you want to remount the same app, move your app creation logic ` +
`into a factory function and create fresh app instances for each ` +
`mount - e.g. \`const createMyApp = () => createApp(App)\``);
}
}
它做了如下工作:
1. 初始化VNode节点
2. 调用render方法
3. 设置挂载flag和app的dom容器
4. 返回vnode的组件实例的代理
先看下1:
// 创建虚拟DOM节点
function _createVNode(type, props = null, children = null, patchFlag = 0, dynamicProps = null, isBlockNode = false) {
...
const shapeFlag = isString(type)
? 1 /* ELEMENT */
: isSuspense(type)
? 128 /* SUSPENSE */
: isTeleport(type)
? 64 /* TELEPORT */
: isObject(type)
? 4 /* STATEFUL_COMPONENT */
: isFunction(type)
? 2 /* FUNCTIONAL_COMPONENT */
: 0;
if (shapeFlag & 4 /* STATEFUL_COMPONENT */ && isProxy(type)) {
type = toRaw(type);
warn(`Vue received a Component which was made a reactive object. This can ` +
`lead to unnecessary performance overhead, and should be avoided by ` +
`marking the component with \`markRaw\` or using \`shallowRef\` ` +
`instead of \`ref\`.`, `\nComponent that was made reactive: `, type);
}
// vnode模板 初始化时:currentScopeId 为 null
const vnode = {
__v_isVNode: true,
["__v_skip" /* SKIP */]: true,
type,
props,
key: props && normalizeKey(props),
ref: props && normalizeRef(props),
scopeId: currentScopeId,
children: null,
component: null,
suspense: null,
ssContent: null,
ssFallback: null,
dirs: null,
transition: null,
el: null,
anchor: null,
target: null,
targetAnchor: null,
staticCount: 0,
shapeFlag,
patchFlag,
dynamicProps,
dynamicChildren: null,
appContext: null
};
...
return vnode;
}
从目前简单的参数来看,它就是返回一个vnode结构,注意此时它的shapeFlag为4,type就是我们传入的参数对象
而 render函数就是之前的render对象中声明的,通过闭包访问到它:
const render = (vnode, container) => {
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode, null, null, true);
}
}
else {
// 直接调用patch 把vnode的内容当做补丁打到实际的dom跟节点上
patch(container._vnode || null, vnode, container);
}
flushPostFlushCbs();
container._vnode = vnode;
};
它调用了 patch 方法,而 path 方法有能力可以把一个vonde节点映射到宿主dom节点上;flushPostFlushCbs方法由于此时没有任务需要执行,先跳过不看。
const patch = (n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, optimized = false) => {
// 初始化的时候 n1 为 null n2为准备渲染的vnode container为待插入的父节点
// patching & not same type, unmount old tree
...
// 初始化的时候直接走这里
const { type, ref, shapeFlag } = n2;
switch (type) {
case Text:
processText(n1, n2, container, anchor);
break;
case Comment:
processCommentNode(n1, n2, container, anchor);
break;
case Static:
if (n1 == null) {
mountStaticNode(n2, container, anchor, isSVG);
}
else {
patchStaticNode(n1, n2, container, isSVG);
}
break;
case Fragment:
processFragment(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized);
break;
default:
// 不是以上4种实际的dom节点
if (shapeFlag & 1 /* ELEMENT */) {
processElement(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized);
}
else if (shapeFlag & 6 /* COMPONENT */) {
// 初始化的时候走这里
processComponent(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized);
}
else if (shapeFlag & 64 /* TELEPORT */) {
type.process(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized, internals);
}
else if (shapeFlag & 128 /* SUSPENSE */) {
type.process(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized, internals);
}
else {
warn('Invalid VNode type:', type, `(${typeof type})`);
}
}
// set ref
if (ref != null && parentComponent) {
setRef(ref, n1 && n1.ref, parentSuspense, n2);
}
};
可以看到 path 也是一个比较高级的方法,它根据传入参数的类型来决定调用其他的渲染方法,当前情况下我们走的是 processComponent
const processComponent = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
if (n1 == null) {
if (n2.shapeFlag & 512 /* COMPONENT_KEPT_ALIVE */) {
parentComponent.ctx.activate(n2, container, anchor, isSVG, optimized);
}
else {
// 初始化走这里 挂载组件到根目标节点
mountComponent(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized);
}
}
else {
updateComponent(n1, n2, optimized);
}
};
继续走 mountComponent 很明显:把n2这个vnode节点挂载到 container 节点下就可以了。
const mountComponent = (initialVNode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
// 先得到这个组件对应的组件实例
const instance = (initialVNode.component = createComponentInstance(initialVNode, parentComponent, parentSuspense));
...
setupComponent(instance);
...
setupRenderEffect(instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized);
};
它做了如下工作:
1. 创建一个组件实例, 同时 initialVNode.component 也指向它,我们之前 app实例上的mount方法返回的 proxy就是来自于组件实例上的属性
2. 设置组件实例(内部render方法以及参数响应式等等很多工作)
3. 创建renderEffect并且同步执行一次,触发渲染,完成依赖收集,更新页面等等
先看下1:
const publicPropertiesMap = extend(Object.create(null), {
$: i => i,
$el: i => i.vnode.el,
$data: i => i.data,
$props: i => (shallowReadonly(i.props) ),
$attrs: i => (shallowReadonly(i.attrs) ),
$slots: i => (shallowReadonly(i.slots) ),
$refs: i => (shallowReadonly(i.refs) ),
$parent: i => getPublicInstance(i.parent),
$root: i => getPublicInstance(i.root),
$emit: i => i.emit,
$options: i => (resolveMergedOptions(i) ),
$forceUpdate: i => () => queueJob(i.update),
$nextTick: i => nextTick.bind(i.proxy),
$watch: i => (instanceWatch.bind(i) )
});
function createRenderContext(instance) {
const target = {};
// expose internal instance for proxy handlers
Object.defineProperty(target, `_`, {
configurable: true,
enumerable: false,
get: () => instance
});
// expose public properties
Object.keys(publicPropertiesMap).forEach(key => {
Object.defineProperty(target, key, {
configurable: true,
enumerable: false,
get: () => publicPropertiesMap[key](instance),
// intercepted by the proxy so no need for implementation,
// but needed to prevent set errors
set: NOOP
});
});
// expose global properties
const { globalProperties } = instance.appContext.config;
Object.keys(globalProperties).forEach(key => {
Object.defineProperty(target, key, {
configurable: true,
enumerable: false,
get: () => globalProperties[key],
set: NOOP
});
});
return target;
}
function createComponentInstance(vnode, parent, suspense) {
const type = vnode.type;
// inherit parent app context - or - if root, adopt from root vnode
const appContext = (parent ? parent.appContext : vnode.appContext) || emptyAppContext;
const instance = {
uid: uid$2++,
vnode,
type,
parent,
appContext,
root: null,
next: null,
subTree: null,
update: null,
render: null,
proxy: null,
exposed: null,
withProxy: null,
effects: null,
provides: parent ? parent.provides : Object.create(appContext.provides),
accessCache: null,
renderCache: [],
// local resovled assets
components: null,
directives: null,
// resolved props and emits options
propsOptions: normalizePropsOptions(type, appContext),
emitsOptions: normalizeEmitsOptions(type, appContext),
// emit
emit: null,
emitted: null,
// state
ctx: EMPTY_OBJ,
data: EMPTY_OBJ,
props: EMPTY_OBJ,
attrs: EMPTY_OBJ,
slots: EMPTY_OBJ,
refs: EMPTY_OBJ,
setupState: EMPTY_OBJ,
setupContext: null,
// suspense related
suspense,
suspenseId: suspense ? suspense.pendingId : 0,
asyncDep: null,
asyncResolved: false,
// lifecycle hooks
// not using enums here because it results in computed properties
isMounted: false,
isUnmounted: false,
isDeactivated: false,
bc: null,
c: null,
bm: null,
m: null,
bu: null,
u: null,
um: null,
bum: null,
da: null,
a: null,
rtg: null,
rtc: null,
ec: null
};
{
instance.ctx = createRenderContext(instance);
}
instance.root = parent ? parent.root : instance;
instance.emit = emit.bind(null, instance);
return instance;
}
可以看到,组件实例上面放了很多属性,其中有一个ctx代表数据上下文,通过createRenderContext得到,其实我们平常在生命周期方法中方法访问到this其实就是指向这个组件实例的,
而我们的数据读写其实就是来自于ctx,只不过它等下会被代理拦截而已。这个组件实例上面拥有我们目前为止所有得到的信息。
function setupComponent(instance, isSSR = false) {
isInSSRComponentSetup = isSSR;
const { props, children } = instance.vnode;
const isStateful = isStatefulComponent(instance);
...
const setupResult = isStateful
? setupStatefulComponent(instance, isSSR)
: undefined;
...
isInSSRComponentSetup = false;
return setupResult;
}
function setupStatefulComponent(instance, isSSR) {
const Component = instance.type;
...
// 这里的 proxy 就是前文mount返回的proxy 它确实是ctx的代理 而 PublicInstanceProxyHandlers 控制着我们对 组件实例的数据字段的访问结果 下文再仔细分析
instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers);
const { setup } = Component;
if (setup) {
...
}
else {
finishComponentSetup(instance);
}
}
function finishComponentSetup(instance, isSSR) {
const Component = instance.type;
// template / render function normalization
if (!instance.render) {
// could be set from setup()
if (compile && Component.template && !Component.render) {
{
startMeasure(instance, `compile`);
}
// 在这里编译模板得到render函数
Component.render = compile(Component.template, {
isCustomElement: instance.appContext.config.isCustomElement,
delimiters: Component.delimiters
});
{
endMeasure(instance, `compile`);
}
}
instance.render = (Component.render || NOOP);
// for runtime-compiled render functions using `with` blocks, the rende
// proxy used needs a different `has` handler which is more performant and
// also only allows a whitelist of globals to fallthrough.
if (instance.render._rc) {
instance.withProxy = new Proxy(instance.ctx, RuntimeCompiledPublicInstanceProxyHandlers);
}
}
// support for 2.x options
{
currentInstance = instance;
pauseTracking();
applyOptions(instance, Component);
resetTracking();
currentInstance = null;
}
...
}
重要的只有2个方法:
1. compile 得到当前template对应的render函数
2. applyOptions 把 我们设置的组件对象的属性添加到组件实例上
先看下1:
const compileCache = Object.create(null);
// 这里就是编译函数
function compileToFunction(template, options) {
if (!isString(template)) {
if (template.nodeType) {
template = template.innerHTML;
}
else {
warn(`invalid template option: `, template);
return NOOP;
}
}
const key = template;
const cached = compileCache[key];
if (cached) {
return cached;
}
if (template[0] === '#') {
const el = document.querySelector(template);
if (!el) {
warn(`Template element not found or is empty: ${template}`);
}
// __UNSAFE__
// Reason: potential execution of JS expressions in in-DOM template.
// The user must make sure the in-DOM template is trusted. If it's rendered
// by the server, the template should not contain any user data.
template = el ? el.innerHTML : ``;
}
const { code } = compile$1(template, extend({
hoistStatic: true,
onError(err) {
{
const message = `Template compilation error: ${err.message}`;
const codeFrame = err.loc &&
generateCodeFrame(template, err.loc.start.offset, err.loc.end.offset);
warn(codeFrame ? `${message}\n${codeFrame}` : message);
}
}
}, options));
// The wildcard import results in a huge object with every export
// with keys that cannot be mangled, and can be quite heavy size-wise.
// In the global build we know `Vue` is available globally so we can avoid
// the wildcard object.
const render = (new Function(code)()
);
render._rc = true;
return (compileCache[key] = render);
}
registerRuntimeCompiler(compileToFunction);
可以看到 它主要调用了 compile$1 得到 render的函数代码,然后new Function生成了它,并且做了缓存
function compile$1(template, options = {}) {
return baseCompile(template, extend({}, parserOptions, options, {
nodeTransforms: [
// ignore <script> and <tag>
// this is not put inside DOMNodeTransforms because that list is used
// by compiler-ssr to generate vnode fallback branches
ignoreSideEffectTags,
...DOMNodeTransforms,
...(options.nodeTransforms || [])
],
directiveTransforms: extend({}, DOMDirectiveTransforms, options.directiveTransforms || {}),
transformHoist: null
}));
}
// options对下包含很多函数方法 都是来自上面合并过来的
function baseCompile(template, options = {}) {
const onError = options.onError || defaultOnError;
const isModuleMode = options.mode === 'module';
/* istanbul ignore if */
{
if (options.prefixIdentifiers === true) {
onError(createCompilerError(45 /* X_PREFIX_ID_NOT_SUPPORTED */));
}
else if (isModuleMode) {
onError(createCompilerError(46 /* X_MODULE_MODE_NOT_SUPPORTED */));
}
}
const prefixIdentifiers = !true ;
if (options.cacheHandlers) {
onError(createCompilerError(47 /* X_CACHE_HANDLER_NOT_SUPPORTED */));
}
if (options.scopeId && !isModuleMode) {
onError(createCompilerError(48 /* X_SCOPE_ID_NOT_SUPPORTED */));
}
const ast = isString(template) ? baseParse(template, options) : template;
const [nodeTransforms, directiveTransforms] = getBaseTransformPreset();
transform(ast, extend({}, options, {
prefixIdentifiers,
nodeTransforms: [
...nodeTransforms,
...(options.nodeTransforms || []) // user transforms
],
directiveTransforms: extend({}, directiveTransforms, options.directiveTransforms || {} // user transforms
)
}));
return generate(ast, extend({}, options, {
prefixIdentifiers
}));
}
上述方法 主要做了3个事情:
1. basrParse得到抽象语法树
2. transform转化节点信息
3. generate生成render函数代码
接下来我们仔细看下它们的输入输出变化:
basrParse 入参为:"
Counter: {{ counter }}
" 和 一些辅助函数
返回的是:
{
"type": 0,
"children": [
{
"type": 2,
"content": " Counter: ",
"loc": {
"start": {
"column": 1,
"line": 1,
"offset": 0
},
"end": {
"column": 18,
"line": 2,
"offset": 18
},
"source": "\n Counter: "
}
},
{
"type": 5,
"content": {
"type": 4,
"isStatic": false,
"constType": 0,
"content": "counter",
"loc": {
"start": {
"column": 21,
"line": 2,
"offset": 21
},
"end": {
"column": 28,
"line": 2,
"offset": 28
},
"source": "counter"
}
},
"loc": {
"start": {
"column": 18,
"line": 2,
"offset": 18
},
"end": {
"column": 31,
"line": 2,
"offset": 31
},
"source": "{{ counter }}"
}
}
],
"helpers": [],
"components": [],
"directives": [],
"hoists": [],
"imports": [],
"cached": 0,
"temps": 0,
"loc": {
"start": {
"column": 1,
"line": 1,
"offset": 0
},
"end": {
"column": 5,
"line": 3,
"offset": 36
},
"source": "\n Counter: {{ counter }}\n "
}
}
可以看到,我们的模板字符串被转化成了ast,其中包含了每个节点的位置信息和字段信息以及节点类型信息
源码先不看,后面单独再写文章分析
再看下 transform 用一堆转换函数对ast执行完后的变化:
{
"type": 0,
"children": [
{
"type": 8,
"loc": {
"start": {
"column": 1,
"line": 1,
"offset": 0
},
"end": {
"column": 18,
"line": 2,
"offset": 18
},
"source": "\n Counter: "
},
"children": [
{
"type": 2,
"content": " Counter: ",
"loc": {
"start": {
"column": 1,
"line": 1,
"offset": 0
},
"end": {
"column": 18,
"line": 2,
"offset": 18
},
"source": "\n Counter: "
}
},
" + ",
{
"type": 5,
"content": {
"type": 4,
"isStatic": false,
"constType": 0,
"content": "counter",
"loc": {
"start": {
"column": 21,
"line": 2,
"offset": 21
},
"end": {
"column": 28,
"line": 2,
"offset": 28
},
"source": "counter"
}
},
"loc": {
"start": {
"column": 18,
"line": 2,
"offset": 18
},
"end": {
"column": 31,
"line": 2,
"offset": 31
},
"source": "{{ counter }}"
}
}
]
}
],
"helpers": [
Symbol(toDisplayString)
],
"components": [],
"directives": [],
"hoists": [],
"imports": [],
"cached": 0,
"temps": 0,
"codegenNode": {
"type": 8,
"loc": {
"start": {
"column": 1,
"line": 1,
"offset": 0
},
"end": {
"column": 18,
"line": 2,
"offset": 18
},
"source": "\n Counter: "
},
"children": [
{
"type": 2,
"content": " Counter: ",
"loc": {
"start": {
"column": 1,
"line": 1,
"offset": 0
},
"end": {
"column": 18,
"line": 2,
"offset": 18
},
"source": "\n Counter: "
}
},
" + ",
{
"type": 5,
"content": {
"type": 4,
"isStatic": false,
"constType": 0,
"content": "counter",
"loc": {
"start": {
"column": 21,
"line": 2,
"offset": 21
},
"end": {
"column": 28,
"line": 2,
"offset": 28
},
"source": "counter"
}
},
"loc": {
"start": {
"column": 18,
"line": 2,
"offset": 18
},
"end": {
"column": 31,
"line": 2,
"offset": 31
},
"source": "{{ counter }}"
}
}
]
},
"loc": {
"start": {
"column": 1,
"line": 1,
"offset": 0
},
"end": {
"column": 5,
"line": 3,
"offset": 36
},
"source": "\n Counter: {{ counter }}\n "
}
}
可以看到目前主要多了2个地方:
1. helpers 代表可能要用到的一些转换辅助函数的key名
2. codegenNode 里面存放接下来要用到的代码生成节点信息
从 generate方法中返回的 code 字段为:
"const _Vue = Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { toDisplayString: _toDisplayString } = _Vue
return " Counter: " + _toDisplayString(counter)
}
}"
可以看到 基本上都是和上面的codegenNode信息是相对应,而_ctx也明确指示了这个render函数执行的时候会绑定特定的this
由于文章字数限制,剩下的内容在第二篇中。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。