1.我是怎么去读源码的
参考资料:《我是如何阅读源码的》 这个大佬讲的文章特别好。如果只是想去看看源码,可以通过这种方式来阅读源码。 但是文章中没有详细说明是如何去配置在vue项目中怎么去阅读的。 所以基于上面的文章和我本人的一些研究后,加入了几个详细的步骤。方法跟大佬讲的react源码阅读方式类似。
如何基于vue文件去阅读源码
1.初始化一个vue3的项目 2.配置vue.config.js文件
module.exports = {
configureWebpack:{
externals: {
vue: 'Vue'
}
}
}
3.在public/index.html文件添加,端口就是你服务启动的端口
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
+ <script src="http://127.0.0.1:8080/script/mvue.js"></script>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
4.npm run start 即可
react 源码配置补充
react 的源码文件必须放到public 文件夹下,不可以放到其他地方,不然会有报错。(血的教训)
进入render 方法前,先准备一些常用知识:位运算、 移位运算、weakset weakmap,准备好了再往下。不然有点蒙蔽
接下来就是正菜了
import {createApp} from 'vue'
import App from './App.vue'
const app = createApp(App)
// app.config.kk = '123'
app.mount('#app')
从入口开始:
研究createApp 方法干了啥。找到什么时候节点挂载到root根节点,什么时候进行的挂载。
提供的源码都是精简过的,切记!!!
const createApp = ((...args) => {
const app = ensureRenderer().createApp(...args); //先不看这句,看了会放弃
const { mount } = app;// 这句也是
// 下面的可以看下了,app.mount 这个语句就是重写我们写的app.mount 方法
app.mount = (containerOrSelector) => {
// 获取根节点 一种是string格式 一种是dom对象格式
const container = normalizeContainer(containerOrSelector);
if (!container)
return;
// clear content before mounting,清除root 节点内的内容
container.innerHTML = '';
// 调用 前面声明的 app 内的 mount 方法
const proxy = mount(container, false, container instanceof SVGElement);
// 返回proxy ,来实现响应式数据
return proxy;
};
// 看下 normalizeContainer 方法
function normalizeContainer(container) {
// 如果传入string 则通过 selector 方法获取节点
if (isString(container)) {
const res = document.querySelector(container);
if (!res) {
warn$1(`Failed to mount app: mount target selector "${container}" returned null.`);
}
return res;
}
// 如果传入节点直接返回
return container;
}
现在准备去探索 ensureRenderer 的内心世界了,先上代码
let renderer;
function ensureRenderer() {
return (renderer ||
(renderer = createRenderer(rendererOptions)));
}
// 这个很重要,首先看renderer , 这里是个惰性声明。如果存在则return ,如果不存在则初始化
// 现在开始头秃的 rendererOptions 这个东西吧
const rendererOptions = extend({ patchProp }, nodeOps);
// 就是去读取dom 节点属性的方法。例如<div data-a='111'>123</div>,patchProp就是读取data-a方法的,然后根据不通的key值进行不同的操作,比如 style class on:click等 。
const patchProp = function(){
if (key === 'class') {
patchClass(el, nextValue, isSVG);
}
else if (key === 'style') {
patchStyle(el, prevValue, nextValue);
}
...
}
// nodeOps 就是对dom节点的操作方法集合 。
const nodeOps = {
insert: (child, parent, anchor) => {
parent.insertBefore(child, anchor || null);
},
remove: child => {
const parent = child.parentNode;
if (parent) {
parent.removeChild(child);
}
},
...
}
// 所有 rendererOptions 是操作dom节点,及操作dom属性的 一些方法集合成的对象。这个要记清楚,后面全是跟这鬼玩意有关系的。
现在开始进入 createRenderer 吧
// 这次真没删代码,就这句
// 记住 options = rendererOptions
function createRenderer(options) {
return baseCreateRenderer(options);
}
进入 baseCreateRenderer ,首先深吸一口气。
// baseCreateRenderer 这函数有1215行代码
function baseCreateRenderer(options) {
target.__VUE__ = true;
const { insert: hostInsert, remove: hostRemove, patchProp: hostPatchProp, createElement: hostCreateElement, setText: hostSetText, setElementText: hostSetElementText, parentNode: hostParentNode, nextSibling: hostNextSibling, setScopeId: hostSetScopeId = NOOP} = options;
// options = rendererOptions 就是dom 操作的方法,切记
const patch = (xxxx)=>{
...
}
const processElement = ()=>{}
const processComponent = ()=>{}
...
const render = (vnode, container, isSVG) => {
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode, null, null, true);
}
}
else {
patch(container._vnode || null, vnode, container, null, null, null, isSVG);
}
flushPostFlushCbs();
container._vnode = vnode;
};
return {
render,
hydrate,
//const app = ensureRenderer().createApp(...args); 到这就回到了我们的第一句了。
createApp: createAppAPI(render, hydrate)
};
}
这个函数就看到这里,先不管他干了啥吧,会掉头发的,当然如果觉得自己每次剪头发都要打薄的可以试试,反正我不敢,接下来看下 createAppAPI 这个函数
function createAppAPI(render, hydrate) {
//hydrate 这个参数可以直接跳过了。
//const app = ensureRenderer().createApp(...args); 到这就回到了我们的第一句了。
return function createApp(rootComponent, rootProps = null) {
// 初始化时rootprops 肯定是空的。这里跳过
if (rootProps != null && !isObject(rootProps)) {
warn$1(`root props passed to app.mount() must be an object.`);
rootProps = null;
}
//初始化一个app的context ,用来描述 app这个对象的特性
const context = createAppContext();
// 使用 new set 保证插件引入的唯一性
const installedPlugins = new Set();
// 这个也要记住,程序运行到这一步时。 节点是没有挂载的。所以isMounted = false
let isMounted = false;
const app = (context.app = {
_uid: uid++,
_component: rootComponent,
_props: rootProps,
_container: null,
_context: context,
_instance: null,
version,
get config() {
return context.config;
},
set config(v) {
{
warn$1(`app.config cannot be replaced. Modify individual options instead.`);
}
},
use(plugin, ...options) {
// 已删代码,
// 使用方式
app.use('element UI')
return app;
},
mixin(mixin) {
// 已删代码,
// 使用方式
// app.mixins('xxx.js')
return app;
},
component(name, component) {
// 已删代码,
// 使用方式
// app.component(App)
context.components[name] = component;
return app;
},
directive(name, directive) {
// 已删代码,自定义指令
// 使用方式
// app.directive('v-cli',xxxx)
// 然后使用方式:<div v-cli='xxxxx'>3333</div>
return app;
},
mount(rootContainer, isSVG) {
//重头戏
if (!isMounted) {
// 把 根节点转换成 虚拟dom vnode
const vnode = createVNode(rootComponent, rootProps);
// 声明
vnode.appContext = context;
// HMR root reload, vue 热更新相关。
{
context.reload = () => {
render(cloneVNode(vnode), rootContainer, isSVG);
};
}
// 执行render 方法去做dom节点挂载
// 有兴趣可以打印一下 rootComponent rootProps vnode rootContainer 这几个的值 。
render(vnode, rootContainer, isSVG);
isMounted = true;
app._container = rootContainer;
rootContainer.__vue_app__ = app;
{
app._instance = vnode.component;
devtoolsInitApp(app, version);
}
return getExposeProxy(vnode.component) || vnode.component.proxy;
}
else {
warn$1(`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)\``);
}
},
unmount() {
if (isMounted) {
render(null, app._container);
{
app._instance = null;
devtoolsUnmountApp(app);
}
delete app._container.__vue_app__;
}
else {
warn$1(`Cannot unmount an app that is not mounted.`);
}
},
provide(key, value) {
if (key in context.provides) {
warn$1(`App already provides property with key "${String(key)}". ` +
`It will be overwritten with the new value.`);
}
// TypeScript doesn't allow symbols as index type
// https://github.com/Microsoft/TypeScript/issues/24587
context.provides[key] = value;
return app;
}
});
return app;
};
}
看下 createAppContext 方法
function createAppContext() {
return {
app: null,
config: {
isNativeTag: NO,
performance: false,
globalProperties: {},
optionMergeStrategies: {},
errorHandler: undefined,
warnHandler: undefined,
compilerOptions: {}
},
mixins: [],
components: {},
directives: {},
provides: Object.create(null),
optionsCache: new WeakMap(),
propsCache: new WeakMap(),
emitsCache: new WeakMap()
};
}
认识一下 patchFlag
patchFlag 是 complier 时的 transform 阶段解析 AST Element 打上的优化标识。并且,顾名思义 patchFlag,patch 一词表示着它会为 runtime 时的 patchVNode 提供依据,从而实现靶向更新 VNode 的效果。因此,这样一来一往,也就是耳熟能详的 Vue3 巧妙结合 runtime 与 compiler 实现靶向更新和静态提升。
而在源码中 patchFlag 被定义为一个数字枚举类型,每一个枚举值对应的标识意义会是这样:
并且,值得一提的是整体上 patchFlag 的分为两大类:
当 patchFlag 的值大于 0 时,代表所对应的元素在 patchVNode 时或 render 时是可以被优化生成或更新的。 当 patchFlag 的值小于 0 时,代表所对应的元素在 patchVNode 时,是需要被 full diff,即进行递归遍历 VNode tree 的比较更新过程。
// patchFlags 声明
PatchFlags {
TEXT = 1,1/动态文本节点
CLASS = 1<<1,1/ 2// 动态 classSTYLE= 1<<2,// 4//动态 style
PROPS = 1<< 3,// 8// 动态属性,但不包含类名和样式
FULL_PROPS = 1<<4,// 16 //具有动态 key属性,当key改变时,需要进行完整的 diff 比较。
HYDRATE_EVENTS = 1<<5,// 32//带有监听事件的节点
STABLE_FRAGMENT = 1<<6,// 64//一个不会改变子节点顺序的 fragment
KEYED_FRAGMENT = 1<<7,// 128//带有key属性的 fragment 或部分子字节有
keyUNKEYED_FRAGMENT = 1<<8,// 256//子节点没有key 的 fragment
NEED_ PATCH =1<<9,//512//一个节点只会进行非 props比较
DYNAMIC_SLOTS = 1 << 10,//1024 // 动态的插槽
// SPECIAL FLAGS (下面是特殊的)---------------------------------------------------------
// 以下是特殊的flag,不会在优化中被用到,是内置的特殊flag
// 表示他是静态节点,他的内容永远不会改变,对于hydrate的过程中,不会需要再对其子节点进行diff
HOISTED = -1,
BAIL = -2, // 用来表示一个节点的diff应该结束
}
认识一下 shapeFlag
这里的定义就涉及到了以为运算了。将节点类型枚举定义。参考类似dom的nodeType (不晓得nodeType是啥的自己百度)
参考资料2:segmentfault.com/a/119000002… 参考资料3:zhuanlan.zhihu.com/p/356382676 假装自己准备好了。然后进入 render 方法。render 方法来源于 baseCreateRenderer 函数内
const render = (vnode, container, isSVG) => {
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode, null, null, true);
}
}
else {
// 先不考虑错误情况,所以一定会进入 patch 方法, patch 方法也是来源于baseCreateRenderer 方法
patch(container._vnode || null, vnode, container, null, null, null, isSVG);
}
flushPostFlushCbs();
container._vnode = vnode;
};
进入patch 方法
// 删减后的代码
const patch = (n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, slotScopeIds = null, optimized = isHmrUpdating ? false : !!n2.dynamicChildren) => {
const { shapeFlag } = n2;
console.log('patch 会进入两次 => 注意')
// shapeFlag & 1 位运算
if (shapeFlag & 1 /* ELEMENT */) {
console.log('第二次进入','processElement 函数')
processElement(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
}else if (shapeFlag & 6 /* COMPONENT */) {
console.log('第一次进入','processComponent 函数')
processComponent(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
}
};
第一次进入 processComponent 方法,现在看下 processComponent 方法
const processComponent = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) => {
n2.slotScopeIds = slotScopeIds;
if (n1 == null) {
// 初始化所以只能是挂载component 不可能是updatecomponent
console.log('挂载 component ')
mountComponent(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized);
}
else {
updateComponent(n1, n2, optimized);
}
};
mountComponent
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);
};
// 重点知识 instance
// instance Vue 实例初始化的对象,
//setupComponent 初始化instance 上的一些属性和方法,可以先不关注
//setupRenderEffect 主要关注这个方法
setupRenderEffect
const setupRenderEffect = (instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized) => {
// componentUpdateFn 方法是最重要的。将实现组件的挂载和渲染。
const componentUpdateFn = () => {
if (!instance.isMounted) {
console.log('将component 转化为vnode 虚拟dom节点 ')
const subTree = (instance.subTree = renderComponentRoot(instance));
// console.log('vnode树',subTree)
console.log('准备第二次进入 patch')
patch(null, subTree, container, anchor, instance, parentSuspense, isSVG);
initialVNode.el = subTree.el;
instance.isMounted = true;
initialVNode = container = anchor = null;
}
};
// create reactive effect for rendering
// 1 注册effect 。这个极其重要。 关系到后面的数据响应式刷新视图。 此次先不深究
const effect = (instance.effect = new ReactiveEffect(componentUpdateFn, () => queueJob(instance.update), instance.scope // track it in component's effect scope
));
//
const update = (instance.update = effect.run.bind(effect));
//
update.id = instance.uid;
update();
};
再次进入了patch ,此时 组件会进入 processElement ,因为此时 component 组件转为vnode,shapeFlag值已经改了。
processElement
const processElement = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) => {
isSVG = isSVG || n2.type === 'svg';
if (n1 == null) {
// 只是初始化,不存在更新动作,所以直接进入了挂载节点方法
mountElement(n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
}
else {
patchElement(n1, n2, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
}
};
mountElement
const mountElement = (vnode, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) => {
let el;
const { props, shapeFlag } = vnode;
//hostCreateElement 来自 options = rendererOptions 就是dom 操作的方法,切记
el = vnode.el = hostCreateElement(vnode.type, isSVG, props && props.is, props);
// mount children first, since some props may rely on child content
// being already rendered, e.g. `<select value>`
// 位运算,得出节点的类型,如果是节点是text ,直接写入
if (shapeFlag & 8 /* TEXT_CHILDREN */) {
hostSetElementText(el, vnode.children);
}
// 给el挂载当前的vnode ,为更新做数据缓存
Object.defineProperty(el, '__vnode', {
value: vnode,
enumerable: false
});
// 缓存父组件
Object.defineProperty(el, '__vueParentComponent', {
value: parentComponent,
enumerable: false
});
// 插入节点
hostInsert(el, container, anchor);
};
到了这一步,节点已经插入到了跟节点,已经渲染到页面了。然后就是return proxy。
生命周期源码阅读
生命周期源码阅读是要从 mountComponent 方法开始的,因为此时生成了instance 组件实例
const mountComponent = (initialVNode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
const instance = (initialVNode.component = createComponentInstance(initialVNode, parentComponent, parentSuspense));
// 生命周期主要是看 setupComponent 函数,传入了 组件实例
setupComponent(instance);
setupRenderEffect(instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized);
};
进入 setupComponent 函数,脑动屏蔽掉ssr的方法
function setupComponent(instance, isSSR = false) {
isInSSRComponentSetup = isSSR;
const { props, children } = instance.vnode;
// 判断组件是有状态的还是无状态的。这个对于react 同学来说还是很常见的
const isStateful = isStatefulComponent(instance);
// 初始化传入的props
initProps(instance, props, isStateful, isSSR);
// 初始化slot 引入
initSlots(instance, children);
const setupResult = isStateful
? setupStatefulComponent(instance, isSSR)
: undefined;
isInSSRComponentSetup = false;
return setupResult;
}
正常 vue 的写法都是有状态组件 。所以我们进入了setupStatefulComponent 方法, 因为我们现在探索的是组件声明周期源码,所以可以不去看关于setup 相关的代码
function setupStatefulComponent(instance, isSSR) {
const Component = instance.type;
{
if (Component.name) {
validateComponentName(Component.name, instance.appContext.config);
}
if (Component.components) {
const names = Object.keys(Component.components);
for (let i = 0; i < names.length; i++) {
validateComponentName(names[i], instance.appContext.config);
}
}
if (Component.directives) {
const names = Object.keys(Component.directives);
for (let i = 0; i < names.length; i++) {
validateDirectiveName(names[i]);
}
}
if (Component.compilerOptions && isRuntimeOnly()) {
warn$1(`jinggao`);
}
}
// 0. create render proxy property access cache
instance.accessCache = Object.create(null);
// 1. create public instance / render proxy
// also mark it raw so it's never observed
instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers));
{
exposePropsOnRenderContext(instance);
}
// 2. call setup()
const { setup } = Component;
// 这个方法暂时先关注 finishComponentSetup 方法
finishComponentSetup(instance, isSSR);
}
进入 finishComponentSetup 方法,
function finishComponentSetup(instance, isSSR, skipOptions) {
const Component = instance.type;
...
{
setCurrentInstance(instance);
pauseTracking();
// 注入options ,关注这个方法即可。先放弃其他的方法
applyOptions(instance);
resetTracking();
unsetCurrentInstance();
}
...
}
现在进入 applyOptions 方法
function applyOptions(instance) {
const options = resolveMergedOptions(instance);
const publicThis = instance.proxy;
const ctx = instance.ctx;
// do not cache property access on public proxy during state initialization
shouldCacheAccess = false;
if (options.beforeCreate) {
callHook(options.beforeCreate, instance, "bc" /* BEFORE_CREATE */);
}
```
const {
// state
data: dataOptions, computed: computedOptions, methods, watch: watchOptions, provide: provideOptions, inject: injectOptions,
// lifecycle
created, beforeMount, mounted, beforeUpdate, updated, activated, deactivated, beforeDestroy, beforeUnmount, destroyed, unmounted, render, renderTracked, renderTriggered, errorCaptured, serverPrefetch,
// public API
expose, inheritAttrs,
// assets
components, directives, filters } = options;
...
// 当声明周期是 beforecreate 和 created 时 是直接调用 callhook 方法,这里先不去看callhook 方法
if (created) {
callHook(created, instance, "c" /* CREATED */);
}
function registerLifecycleHook(register, hook) {
if (isArray(hook)) {
hook.forEach(_hook => register(_hook.bind(publicThis)));
}
else if (hook) {
register(hook.bind(publicThis));
}
}
// 我们来看下在其他声明周期的声明下,是做了什么呢。
registerLifecycleHook(onBeforeMount, beforeMount);
// registerLifecycleHook 这个方法就可以发现,是调用了 onBeforeMount 方法
...
}
现在我们进入 createHook 方法
const createHook = (lifecycle) => (hook, target = currentInstance) =>
// post-create lifecycle registrations are noops during SSR (except for serverPrefetch)
(!isInSSRComponentSetup || lifecycle === "sp" /* SERVER_PREFETCH */) &&
injectHook(lifecycle, hook, target);
const onBeforeMount = createHook("bm" /* BEFORE_MOUNT */);
// 通过 createHOOK 又进入了 injectHook 方法
现在是进入 injectHook 方法
function injectHook(type, hook, target = currentInstance, prepend = false) {
if (target) {
const hooks = target[type] || (target[type] = []);
const wrappedHook = hook.__weh ||
(hook.__weh = (...args) => {
if (target.isUnmounted) {
return;
}
setCurrentInstance(target);
// callWithAsyncErrorHandling 进入
const res = callWithAsyncErrorHandling(hook, target, type, args);
unsetCurrentInstance();
resetTracking();
return res;
});
if (prepend) {
hooks.unshift(wrappedHook);
}
else {
hooks.push(wrappedHook);
}
return wrappedHook;
}
}
进入 callWithAsyncErrorHandling 方法
function callWithAsyncErrorHandling(fn, instance, type, args) {
if (isFunction(fn)) {
// 因为在源码中默认传入的就是个function,所以只需要关注 callWithErrorHandling
const res = callWithErrorHandling(fn, instance, type, args);
if (res && isPromise(res)) {
res.catch(err => {
handleError(err, instance, type);
});
}
return res;
}
...
}
进入 callWithErrorHandling 方法
function callWithErrorHandling(fn, instance, type, args) {
let res;
try {
// 在这里执行生命了周期 ,总算找到了最后生命周期函数执行的调用了。
res = args ? fn(...args) : fn();
}
catch (err) {
handleError(err, instance, type);
}
return res;
}
现在再来看下 beforecreate 和 created 的 callhook 方法
function callHook(hook, instance, type) {
callWithAsyncErrorHandling(isArray(hook)
? hook.map(h => h.bind(instance.proxy))
: hook.bind(instance.proxy), instance, type);
}
发现执行了 callWithAsyncErrorHandling 方法
function callWithAsyncErrorHandling(fn, instance, type, args) {
if (isFunction(fn)) {
//最后还是来到了这里
const res = callWithErrorHandling(fn, instance, type, args);
if (res && isPromise(res)) {
res.catch(err => {
handleError(err, instance, type);
});
}
return res;
}
const values = [];
for (let i = 0; i < fn.length; i++) {
values.push(callWithAsyncErrorHandling(fn[i], instance, type, args));
}
return values;
}
生命周期流转与触发
组件生命周期都是在组件实例化成instance 时,注入到 instance实例上的,然后根据组件不同的状态流转的过程中,判断对应的生命周期函数是否存在,然后去执行。
proxy 监听数据变动是怎么更新到视图的呢
diff算法的优化点
diff算法的双while执行顺序分析
... 未完待续(内容预告:proxy代理的数据是怎么去更新视图的。diff算法解析。)