vue源码小韩初篇

414 阅读5分钟

初来乍到请多关注~❤️

首先把vue的源码copy的地址告诉你们:github.com/vuejs/vue

地址告诉你们了,你们肯定要clone下来瞅一眼,那么命令行输入:

  1. git clone github.com/vuejs/vue.g…

  2. cd ./vue/

  3. npm i (安装到phantom时就可终止)慢的烦人,不要也罢。。

顺带脚说一下最外层目录里的文件名称里面都是存放一些什么样的东西,以及文件名代表含义:

  1. dist 发布目录

  2. examples 范例 也许会出现测试的代码

  3. flow 针对flow的类型提示(vue3的话应该是没有这个文件的)

  4. types 针对ts的类型声明

  5. node_modules(npm i)下载自动产出的包的文件统一存放

  6. packages 核心代码之外的独立库

  7. script 构建的脚本

  8. src 源码代码存放(核心代码)

话不能说一半调人胃口,那再啰嗦一下src目录里的文件名称代表含义:

  1. compiler 编译器相关

  2. core 核心代码(常回家看看)
    components 通用组件(缓存keep-alive)
    global-api 全局的API
    instance 构造函数等等
    observer 响应式相关(数据响应式)
    vdom 虚拟dom相关 diff算法

  3. platforms 平台特有的代码

接下来就开始我们的源码学习了

一. 先要找到入口文件,通过package点json里面的dev的参数找线索:

"dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev"

找到 scripts/config.js 里面搜索 web-full-dev 找到配置:

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
  }

entry就是入口,后面紧跟了一个resolve方法,这个方法里传了一个不知名的地址,这个方法的作用就是可以根据别名找到文件的完整路径

进到这个aliases文件就可以看到对应的(web/..)的完整路径 web:

resolve('src/platforms/web')

const aliases = require('./alias')
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)
  }
}

最终得到完整的entry入口文件的路径就是:

src/platforms/web/entry-runtime-with-compiler.js

找到vue构造函数 (src/core/instance/index.js)

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) 
stateMixin(Vue) 
eventsMixin(Vue) 
lifecycleMixin(Vue) 
renderMixin(Vue)

解析:这个函数就干了一件事就是执行了一个初始化函数this.init()并且它是一个实例方法,那么init从何而来:initMixin(vue)扩展了_init方法

/src/core/instance/init.js  找到 initMixi

大概从50行开始就是核心代码,是什么呢,各种初始化

vm._self = vm
    initLifecycle(vm)  //生命周期的初始化
    initEvents(vm)  //事件的初始化(自定义事件的监听)
    initRender(vm)  //渲染的初始化
    callHook(vm, 'beforeCreate') //所谓的生命周期的钩子是用这种方式派发的
    initInjections(vm) // resolve injections before data/props
    initState(vm)  //数据的初始化
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')
拿出一个初始化来说:那就生命周期 !initLifecycle(vm)

它做了什么事:

先说这个函数去哪看(路径):

/src/core/instance/lifecycle.js

然而它息息相关的东西就是:parentparent children $root 这些属性的初始化

export function initLifecycle (vm: Component) {
  const options = vm.$options

  // locate first non-abstract parent
  let parent = options.parent
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    parent.$children.push(vm)
  }

  vm.$parent = parent
  vm.$root = parent ? parent.$root : vm

  vm.$children = []
  vm.$refs = {}

  vm._watcher = null
  vm._inactive = null
  vm._directInactive = false
  vm._isMounted = false
  vm._isDestroyed = false
  vm._isBeingDestroyed = false
}
一点一点说哈~

截屏2020-12-04 下午3.33.11.png

const options = vm.$options

这行其实就是一个简单的赋值,咱接着往下走

let parent = options.parent
  if (parent && !options.abstract) {
    ////////
  }

当前vm实例有父实例parent,则赋值给parent变量。如果父实例存在,且该实例不是抽象组件。则执行下面代码~

while (parent.$options.abstract && parent.$parent) {
  parent = parent.$parent
}
parent.$children.push(vm)

注意while循环内的条件parent.options.abstract && parent.parent,如果父实例parent是抽象组件,则继续找parent上的parent。直到找到非抽象组件为止。之后把当前vm实例push到定位的第一个非抽象parent的$children属性上。这样我们就说完了怎么找vm的parent属性。

之后我们回到initLifecycle继续往下看

 vm.$parent = parent
  vm.$root = parent ? parent.$root : vm

  vm.$children = []
  vm.$refs = {}
  vm._watcher = null
  vm._inactive = null
  vm._directInactive = false
  vm._isMounted = false
  vm._isDestroyed = false
  vm._isBeingDestroyed = false

这些代码都是为vm一些属性赋值。这些属性的作用:

$parent:指定已创建的实例之父实例,在两者之间建立父子关系。子实例可以用 this.$parent 访问父实例,子实例被推入父实例的 $children 数组中



$root:当前组件的vue跟实例,如果当前实例没有父实例,此实例将会是自己



$children:当前实例的直接子组件,它并不保证顺序,也不是响应式的



$refs:一个对象,持有已经注册过的ref的所有子组件



_watcher:组件实例的相对应的watcher实例对象



_inactive:表示keep-alive中组件状态,如被激活,该值为false,反之为true。



_directInactive:也是表示keep-alive中组件状态的属性。



_isMounted:当前实例是否完全挂载



_isDestroyed:当前实例是否已经被销毁



_isBeingDestroyed:当前实例是否正在被销毁还没有完全销毁

initLifecycle方法的逻辑比较简单,主要对vue实例一些属性进行赋值。

其实简单说一个方法还是会有些不太好理解和不太好串联起来,所以需要跑测试来方便自己阅读理解代码

那么如何创建文件跑测试:

  1. 在和src相同目录下有一个examples,在examples这个文件夹里创建一个test文件夹
  2. 在test文件夹里新建html,在html创建实例new Vue() 我顺带简单介绍一下啊
  3. 打开调试sources,断点打在new Vue()看看发生了什么事情
  4. new Vue()就是执行了一个init初始化方法
  5. init方法执行最重要的就是选项的合并,默认选项和用户选项的合并,再有就是执行了各种属性的初始化(生命周期 事件 渲染 数据)生命周期的相关调用
  6. 带着疑问跑测试 比如:{{foo}} 为什么写这个可以在页面显示值
  7. 那么就可以去看$mount(/src/platforms/web/runtime/index.js)
  8. 打断点看看return什么东西,随之进到那个方法里看具体执行了什么
  9. 执行了更新组件,创建watcher(一个组件一个watcher)
  10. 就是这样去一点一点查看源码~~~