美文网首页
手把手教你读Vue2源码-1

手把手教你读Vue2源码-1

作者: miao8862 | 来源:发表于2021-04-22 23:59 被阅读0次

    vue源码:https://github.com/vuejs/

    这里,我的调试环境为:
    window10 x64
    vue2.5

    今天的目标:搭建调试环境,找入口文件

    要学习源码,就要先学会如何调试源码,所以我们第一步可以先拉取vue源码,然后在本地配置下调试环境:

    cd ./vue
    npm i
    npm npm i -g rollup
    // 修改package.json中script中dev运行脚本,添加--sourcemap
    "dev": "rollup -w -c build/config.js --sourcemap --environment TARGET:web-full-dev",
    
    npm run dev
    
    // 之后就会在dist目录下生成vue.js,方便我们用于测试和调试了~
    

    配制调试环境的坑

    1. 安装过程可能提示找不到PhantomJS:
      PhantomJS not found on PATH
      Downloading https://github.com/Medium/phantomjs/releases/download/v2.1.1/phantomjs-2.1.1-windows.zip Saving to C:\Users\ADMINI~1\AppData\Local\Temp\phantomjs\phantomjs-2.1.1-windows.zip Receiving...
      根据提示,去下载压缩包,放到对应位置就好

    2. 提示以下错误
      Error: Could not load D:\vue-source\vue\src\core/config (imported by D:\vue-source\vue\src\platforms\web\entry-runtime-with-compiler.js): ENOENT: no such file or directory, open 'D:\vue-source\vue\src\core/config'
      查了下资料,说是rollup-plugin-alias插件中解析路径的问题,有人提PR了(https://github.com/vuejs/vue/issues/2771),尤大大说是没有针对window10做处理造成的,解决方法是将 node_modules/rollup-plugin-alias/dist/rollup-plugin-alias.js 改为

    // var entry = options[toReplace]
    // 81行,上面那句,改为:
    var entry = normalizeId(options[toReplace]);
    

    打包后dist输出的文件一些后缀说明

    dist目录.png
    • 有runtime字样的:说明只能在运行时运行,不包含编译器(也就是如果我们直接使用template模板,是不能正常编译识别的)
    • common:commonjs规范,用于webpack1
    • esm:es模块,用于webpack2+
    • 没带以上字样的: 使用umd,统一模块标准,兼容cjs和amd,用于浏览器,也是之后我们要用于测试的文件

    我们可以在test文件夹下,创建我们的测试文件test1.html:


    创建测试文件test1.html
    <!-- test/test1.html -->
    <head>
      <script src="../dist/vue.js"></script>
    </head>
    <body>
      <div id="demo">
        <h1>初始化流程</h1>
        <p>{{msg}}</p>
      </div>
      <script>
        const app = new Vue({
          el: "#demo",
          data: {
            msg: 'hello'
          }
        })
      </script>
    </body>
    

    找入口文件

    1. 在package.json中,dev运行脚本中找配置文件(-c 指向配置文件):rollup -w -c build/config.js --sourcemap --environment TARGET:web-full-dev
    2. 进入配置文件中,根据TARGET找到对应的配置文件TARGET:web-full-dev,搜索这个环境,让到对应的entry入口文件
    // 1. build/config.js根据target环境,来找entry入口
    const builds = {
      // Runtime+compiler development build (Browser)
      'web-full-dev': {
        entry: resolve('web/entry-runtime-with-compiler.js'),
        dest: resolve('dist/vue.js'),
        format: 'umd',
        env: 'development',
        alias: { he: './entity-decoder' },
        banner
      },
    }
    
    // 2. 查看resolve解析方法,从中看出web是在别名文件中有对应地址
    const resolve = p => {
      const base = p.split('/')[0]
      if (aliases[base]) {
        return path.resolve(aliases[base], p.slice(base.length + 1))
      } else {
        return path.resolve(__dirname, '../', p)
      }
    }
    
    // 3. 根据aliases找到alias.js文件,从中找到web对应的相应地址
    module.exports = {
      web: resolve('src/platforms/web'),
    }
    
    // 4. 最后根据拼接规则,我们终于找到真正的对应入口
    src/platforms/web/entry-runtime-with-compiler.js
    

    查看入口文件

    带个问题去看源码,以下这个vue实例中,最终挂载起作用是的哪个?

    // render,template,el哪个的优先级高?
    const app = new Vue({
      el: "#demo",
      template: "<div>template</div>",
      render(h) {
        return h('div', 'render')
      },
      data: {
        foo: 'foo'
      }
    })
    
    // 答案:render > template > el
    

    可以从源码找答案,在主要的地方我已添加中文注释(英文注释是源码本身的),可查看对应注释地方:

    // 保存原来的$mount
    const mount = Vue.prototype.$mount
    // 覆盖默认的$mount
    Vue.prototype.$mount = function (
      el?: string | Element,
      hydrating?: boolean
    ): Component {
      el = el && query(el)
    
      // 如果看到有以下这样注释的,一般用于调试阶段输出一些警告信息,我们在学习时为了简单点,可以直接忽略的部分
      /* istanbul ignore if */
      if (el === document.body || el === document.documentElement) {
        process.env.NODE_ENV !== 'production' && warn(
          `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
        )
        return this
      }
    
      // 从这里开始,解析我们的配置
      const options = this.$options
      // resolve template/el and convert to render function
      if (!options.render) {
        let template = options.template
        if (template) {
          if (typeof template === 'string') {
            if (template.charAt(0) === '#') {
              template = idToTemplate(template)
              //...
            }
          } else if (template.nodeType) {
            template = template.innerHTML
          } else {
            if (process.env.NODE_ENV !== 'production') {
              warn('invalid template option:' + template, this)
            }
            return this
          }
        } else if (el) {
          template = getOuterHTML(el)
        }
    
        // 如果存在模板,执行编译
        if (template) {
          // ...
    
          // 编译得到渲染函数
          const { render, staticRenderFns } = compileToFunctions(template, {
            shouldDecodeNewlines,
            delimiters: options.delimiters,
            comments: options.comments
          }, this)
          options.render = render
          options.staticRenderFns = staticRenderFns
    
        }
      }
      // 最后执行挂载,可以看到使用的是父级原来的mount方式挂载
      return mount.call(this, el, hydrating)
    }
    

    从以上1,2,3步骤中,我们就可以得出刚刚的答案了。

    src/platforms/web/entry-runtime-with-compiler.js文件作用:入口文件,覆盖$mount,执行模板解析和编译工作

    找Vue的构造函数

    这里主要找Vue的构造函数,中间路过一些文件会写一些大概的作用,但主线不会偏离我们的目标

    1. 在入口文件entry-runtime-with-compiler.js中,可以查看Vue引入文件
    import Vue from './runtime/index'
    
    1. /runtime/index.js文件
    import Vue from 'core/index'
    
    // 定义了patch补丁:将虚拟dom转为真实dom
    Vue.prototype.__patch__ = inBrowser ? patch : noop
    
    // 定义$mount
    // public mount method
    Vue.prototype.$mount = function (
      el?: string | Element,
      hydrating?: boolean
    ): Component {
      el = el && inBrowser ? query(el) : undefined
      return mountComponent(this, el, hydrating)
    }
    
    1. core/index.js文件
    import Vue from './instance/index'
    
    // 定义了全局API
    initGlobalAPI(Vue)
    
    1. src/core/instance/index.js文件
      终于找到了Vue的构造函数,它只做了一件事,就是初始化,这个初始化方法是通过minxin传送到这个文件的,所以我们接下来是要去查看Init的方法,这也是我们以后要常看的一个文件
    // 构造函数
    function Vue (options) {
      if (process.env.NODE_ENV !== 'production' &&
        !(this instanceof Vue)
      ) {
        warn('Vue is a constructor and should be called with the `new` keyword')
      }
      // 只做了一件事,初始化
      this._init(options)
    }
    
    initMixin(Vue)  // _init方法是通过mixin传入的,从这里可以找到初始化方法
    stateMixin(Vue)
    eventsMixin(Vue)
    lifecycleMixin(Vue)
    renderMixin(Vue)
    
    export default Vue
    

    初始化方法定义的文件 src/core/instance/init.js

    划重点,比较重要,可以看出初始化操作主要做以下事件:

    initLifecycle(vm) // 初始化生命周期,声明$parten,$root,$children(空的),$refs
    initEvents(vm)  // 对父组件传入的事件添加监听
    initRender(vm)  // 声明$slot,$createElement()
    callHook(vm, 'beforeCreate') // 调用beforeCreate钩子
    initInjections(vm) // 注入数据 resolve injections before data/props
    initState(vm) // 重中之重:数据初始化,响应式
    initProvide(vm) // 提供数据 resolve provide after data/props
    callHook(vm, 'created') // 调用created钩子
    
    // 定义初始化方法
    export function initMixin (Vue: Class<Component>) {
      Vue.prototype._init = function (options?: Object) {
        const vm: Component = this
        // a uid
        vm._uid = uid++
    
        // ...
    
        // a flag to avoid this being observed
        vm._isVue = true
    
        // 合并选项,将用户设置的options和vue默认设置的options,做一个合并处理
        // merge options
        if (options && options._isComponent) {
          // optimize internal component instantiation
          // since dynamic options merging is pretty slow, and none of the
          // internal component options needs special treatment.
          initInternalComponent(vm, options)
        } else {
          vm.$options = mergeOptions(
            resolveConstructorOptions(vm.constructor),
            options || {},
            vm
          )
        }
        //...
        
        // 重点在这里,初始化的一堆操作!
        // expose real self
        vm._self = vm
        initLifecycle(vm) // 初始化生命周期,声明$parten,$root,$children(空的),$refs,这里说明创建组件是自上而下的
        initEvents(vm)  // 对父组件传入的事件添加监听
        initRender(vm)  // 声明$slot,$createElement()
        callHook(vm, 'beforeCreate') // 调用beforeCreate钩子
        initInjections(vm) // 注入数据 resolve injections before data/props
        initState(vm) // 重中之重:数据初始化,响应式
        initProvide(vm) // 提供数据 resolve provide after data/props
        callHook(vm, 'created') // 调用created钩子
    
        // ...
    
        if (vm.$options.el) {
          vm.$mount(vm.$options.el)
        }
      }
    }
    

    如有错误之处,还望指出哈

    相关文章

      网友评论

          本文标题:手把手教你读Vue2源码-1

          本文链接:https://www.haomeiwen.com/subject/lyyqrltx.html