vue 重学笔记三:起步
上文介绍了 安装和项目的创建,至于项目的打包,这里不再赘述,后期有空会出篇文章说下 项目的打包方面的优化。
重学笔记,并不适用于刚刚开始学 vue 的朋友,这中间我会在使用的基础上,去解读源码,所以会穿插源码,不过如果有兴趣学源码的朋友,也可以继续往下看。
要看一个项目,第一点肯定是去找它的入口。
入口:为什么 执行 npm run dev 后,执行了 index.html 和 main.js 文件
我们知道,启动 vue 项目,用的是 npm run dev ,那这行命令究竟做了什么?又是如何经由 index.html 文件,然后运行 main.js 文件的?
这里以 vue-cli2 创建(第二种创建方式)的项目为例,vue-cli3 创建的项目配置文件都被隐藏了,而 vue-cli2 中创建的项目配置文件是可以直接查看的。
先来看下 package.json 中配置了哪些命令:
"scripts": { "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js", "start": "npm run dev", "unit": "jest --config test/unit/jest.conf.js --coverage", "e2e": "node test/e2e/runner.js", "test": "npm run unit && npm run e2e", "lint": "eslint --ext .js,.vue src test/unit test/e2e/specs", "build": "node build/build.js" },
由以上命令可以看出来,npm run dev ,执行的是 package.json 中的 dev 那条命令,命令指向的是 build/webpack.dev.conf.js 文件,如下图:
// webpack.dev.conf.js ... const baseWebpackConfig = require('./webpack.base.conf') ... const devWebpackConfig = merge(baseWebpackConfig, { ... plugins: [ ... new HtmlWebpackPlugin({ filename: 'index.html', template: 'index.html', inject: true }), ... ] }) // webpack.base.conf.js module.exports = { context: path.resolve(__dirname, '../'), entry: { app: './src/main.js' }, }
先执行的是 webpack.dev.conf.js 文件,其中又引入了 webpack.base.conf.js 基础配置文件,同时在 dev.conf.js 文件中配置了 入口 index.html 文件,在 base.conf.js 文件中 配置了 入口 main.js 文件。
所以,在命令行中输入 npm run dev 后,会分别经由 webpack.dev.conf.js 文件和 webpack.base.conf.js 文件 执行 index.html 文件和 ./src/main.js 文件。
index.html
index.html ,是前端入门基础,就不再赘述。
main.js
main.js 文件最简单的代码如下:
import { createApp } from 'vue' import App from './App.vue' import router from './router' import store from './store' createApp(App).use(store).use(router).mount('#app')
今天先不说 use ,先只说 createApp() 和 mount() 函数。往下看,除开vue-cli ,来做一个最简单的应用。
声明式渲染
<div id="hello-vue" class="demo"> {{ message }} </div> <script> const HelloVueApp = { data() { return { message: 'Hello Vue!!' } } } Vue.createApp(HelloVueApp).mount('#hello-vue') </script>
createApp() 函数
用法:
const app = Vue.createApp({ /* 选项 */ })
作用:创建应用。传递给 createApp 的选项用于配置根组件
实现:
@file core/packages/runtime-dom/src/index.ts export const createApp = ((...args) => { // 创建传入的 app 组件对象
// ensureRenderer():确保是否需要渲染器(优化点),渲染相关的代码
// createApp 创建 app 变量 const app = ensureRenderer().createApp(...args) if (__DEV__) { injectNativeTagCheck(app) injectCompilerOptionsCheck(app) }
// 取出 app 中原有的 mount 方法 const { mount } = app
// 重写 mount 方法
// 重写的目的:跨平台(app.mount 中只包含和平台无关的代码)
// 这里重写的代码都是一些和 web 关系比较大的代码(比如其他平台也可以进行类似的重写) app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
// containerOrSelector 是 mount() 中传入的 外层 id / #app, 如 vue.createApp(HelloWorld).mount('#app')
// 获取渲染根节点 HTMLElement对象
// normalizeContainer 函数的作用是找到 id 为 #app 的DOM元素
const container = normalizeContainer(containerOrSelector) // container 根节点 ,如 #app 的元素 if (!container) return const component = app._component // app._component 传入 createApp 的根组件对象
if (!isFunction(component) && !component.render && !component.template) { component.template = container.innerHTML
// 2.x compat check if (__COMPAT__ && __DEV__) { for (let i = 0; i < container.attributes.length; i++) { const attr = container.attributes[i] if (attr.name !== 'v-cloak' && /^(v-|:|@)/.test(attr.name)) { compatUtils.warnDeprecation( DeprecationTypes.GLOBAL_MOUNT_CONTAINER, null ) break } } } } container.innerHTML = ''
//调用真正的 mount 函数,进行挂载 const proxy = mount(container, false, container instanceof SVGElement) if (container instanceof Element) { container.removeAttribute('v-cloak') container.setAttribute('data-v-app', '') } return proxy } return app }) as CreateAppFunction<Element>
ensureRenderer 函数
@file core/packages/runtime-dom/src/index.ts const rendererOptions = extend({ patchProp }, nodeOps) let renderer: Renderer<Element | ShadowRoot> | HydrationRenderer
// 是否需要渲染器 function ensureRenderer() { return ( renderer || (renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions)) ) }
ensureRenderer 是一个单例模式的函数,会返回一个 renderer【惰性创建renderer】,如果无 renderer则会调用 createRenderer 进行获取 renderer,获得了一个 app 实例;
normalizeContainer 函数
@file core/packages/runtime-dom/src/index.ts
function normalizeContainer( container: Element | ShadowRoot | string // #app ): Element | null { if (isString(container)) { const res = document.querySelector(container) // 获取 #app 元素 if (__DEV__ && !res) { warn(`Failed to mount app: mount target selector "${container}" returned null.`) } return res } if (__DEV__ && window.ShadowRoot && container instanceof window.ShadowRoot && container.mode === 'closed') { warn(`mounting on a ShadowRoot with \`{mode: "closed"}\` may lead to unpredictable bugs`) } return container as any }
normalizeContainer 的作用是去找到 id 为 #app 的 DOM 元素传递给缓存的 mount 方法。
mount() 函数
用法:
Vue.createApp(HelloVueApp).mount('#hello-vue')
作用:挂载应用。在使用 mount() 挂载应用时,该组件被用作渲染的起点。
源码见 此处。—— 改写的 mount。
应用上下文对象中的 mount 函数:
@file core/packages/runtime-core/src/apiCreateApp.ts const app: App = (context.app = { mount( rootContainer: HostElement, isHydrate?: boolean, isSVG?: boolean ): any { if (!isMounted) {
// 若还未渲染
// 调用 createVNode 获取 vnode,
// rootComponent 即为调用 createApp(config)的时候传递进来的 config 数据,
// rootProps 为 root props,前面提到过会对此验证,一般在使用过程中 rootProps 为 null const vnode = createVNode( //createVNode 生成 虚拟DOM rootComponent as ConcreteComponent, rootProps ) // store app context on the root VNode
// 存储 context 到根节点上 vnode.appContext = context // HMR root reload if(__DEV__){ context.reload = () => { render(cloneVNode(vnode), rootContainer, isSVG) } } // 调用渲染函数,核心渲染代码为 render 函数 if (isHydrate && hydrate) { hydrate(vnode && VNode<Node, Element>, rootContainer as any) } else { render(vnode, rootContainer, isSVG) } isMounted = true // 实例的 _container 保存为当前 rootContainer
app._container = rootContainer // rootContainer 增加属性 __vue_app__,置为当前 app 实例
;(rootContainer as any).__vue_app__ = app if (__DEV__ || __FEATURE_PROD_DEVTOOLS__){ app._instance = vnode.component devtoolsInitApp(app, version) } // 返回 vnode.component 的代理 return getExposeProxy(vnode.component) || vnode.component!.proxy } else if (__DEV__) { warn(`App has already been mounted...`) } } })
getExproseProxy
@file core/packages/runtime-core/src/component.ts export function getExposeProxy(instance: ComponentInternalInstance) { if (instance.exposed) { return ( instance.exposeProxy || (instance.exposeProxy = new Proxy(proxyRefs(markRaw(instance.exposed)), { get(target, key: string) { if (key in target) { return target[key] } else if (key in publicPropertiesMap) { return publicPropertiesMap[key](instance) } } })) ) } }
Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
用法如下:
let proxy = new Proxy(target, handler);
target:目标对象
handler:是一个对象,声明了代理 target 的指定行为,支持的拦截操作,一共 13 种,如下图:
(摘抄于:https://blog.csdn.net/qq_39576711/article/details/124022157)
参考的相关资料: