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)

 

 

 

参考的相关资料:

1、createApp 的源码原理

posted on 2022-03-21 17:20  bala001  阅读(388)  评论(0编辑  收藏  举报

导航