vue源码解析一初始化流程及数据响应式过程梳理

2次阅读

共计 8703 个字符,预计需要花费 22 分钟才能阅读完成。

学习目标

vue 源码分析(一)
  1. 掌握源码学习方法
  2. 初始化流程梳理
  3. 深入理解数据响应式过程

配置环境

  1. 首先拷贝源码:git clon https://github.com/vuejs/vue.git
  2. cd ./vue
  3. npm i
  4. 安装 rollup:打包工具
  5. 需要全局安装 npm i -g rollup
  6. 修改 /yuanma-vue/vue/package.json 中 dev 的脚本:添加 –sourcemap
  7. 执行 dev 脚本:npm run dev
  8. 在 examples 中去添加 test 文件

注:打完断点可以进行测试,源码运行流程

源码环境

  "name": "vue",
  "version": "2.6.11",

文件目录

vue
├──dist #发布目录
├──examples #范例,测试代码在这里
├──flow #试代码
├──packages #核心代码之外的独立库
├──scripts #构建脚本
├──src #源码
├──test #ts 类型声明,上面 flow
└──types #对 flow 的类型声明 

重点:源码

  src # 源码  
  ├──compiler  # 编辑器相关
  ├──core  # 核心代码
  ├──────components # 通用组建如 keep-alive
  ├──────global-api # 全局 API
  ├──────instance # 构建函数等
  ├──────observer # 响应式相关
  ├──────vdom # 虚拟 DOM 相关
  └──────platforms # 平台独特的代码 代码扩充

dist 中不同命名输出的解释

common:nodejs 打包的语言输出,服务端渲染。vue.common.dev.js 
esm:版本是 webpack 打包用的版本 es6 的模块
umd:universal module definition(通用模块定义)  兼容 common 和 amd 用于浏览器;前端浏览器 

加 runtime 含义的不同
 添加指不包含编译器:体较小;仅包含运行时,(就不可以用字符串模版)运行时必须经过 webpack,因为 webpack 在程序运行之前,提前打包编译,所以会将 template,<p>{{foo}}</p> 等等都预编译成渲染函数,在 webpack 情况下,就不需要编译器
不添加指包含编译器
比如:当我们在 
new Vue({template:'<div></div>' // 这种格式就需要 需要添加编译器 字符串模版})

<!-- <script src="../../dist/vue.js"></script> -->
// 什么没有没有加的,umd 格式 vue.js

vue 源码分析 - 初始化流程

入口文件:

路径:src/platforms/web/entry-runtime-with-compiler.js

// 首先获取到定义的 $mount 方法,并赋值给 mount;
//主要目的是要重写 $mount 方法;还需要执行之前的 mount 方法;
// 所以主要是扩展 $mount 方法

const mount = Vue.prototype.$mount  
Vue.prototype.$mount = function (el?: string |Element, hydrating?: boolean): Component {  
    **1. 用户传进来的 el 宿主元素 **  
    **2. 处理选项:render/template/el**  
    const options = this.$options  
    if(!options.render){......}else if(el){.....} //** 都是在处理选项  **
    从这里可以看出来,在 vue 处理 render/template/el 的时候的优先级。// 首选判断 render 是否存在
    if (!options.render) { // 当 render 不存在的时候,才会进到里面的选项。// 如果 template 存在,再走 template 的 if 选项, 紧跟着是 else if  
        let template = options.template  
        if (template) {运行编译:将模板为 render 函数}else if (el) {}  
        从而可以得知 优先级为:render> template > el
        }  
    }  
    扩展 $mount 的主要目的是:判断是否需要编译出渲染函数  
    在组件开发的情况下,会有一个 main.js  
    main.js  
    new vue({render(h){return h(App) // 编译根组建 App  
            }  
    }).$mount('#app')
    render 需要配合 $mount 来使用
}
Vue.prototype.$mount 的来源

路径:src/platforms/web/entry-runtime-with-compiler.js 中
import Vue from ‘./runtime/index’
路径 src/platforms/web/runtime/index.js 文件中,目录还是在 web 平台

web 平台运行时的首页需要实现的功能有哪些:**1\. 安装平台 patch 函数,主要是用于初始化和更新两个功能(虚拟 dom、diff)**  
// install platform patch function  
Vue.prototype.\_\_patch\_\_ = inBrowser ? patch : noop  
**2. 实现 $mount 函数 **  
Vue.prototype.$mount = function (...){  
    el:宿主  
    **$mount 唯一做的一件事:挂载组建 就是将 vnode 转化为 node 可以理解为 vdome 转化为 dome**  
    return mountComponent(this, el, hydrating)  
}

查找 vue 来源

初始化全局的 API

在 src/platforms/web/runtime/index.js 文件中,

import Vue from 'core/index'  
Vue.prototype.$mount = function (){}  
路径:src/core/index.js  
1. 初始化全局的 API
initGlobalAPI(Vue)  
如:set delete nextTick .... 等等  
具体的定义可以进到 initGlobalAPI 里来看,举例子:在 import {initUse} from './use' 中进入 initUse  
可以看到 Vue.use 方法  
Vue.use 方法,之前疑惑为什么要在,plugin 去定义 install 方法,从源码就可以看到约定的 install
if (typeof plugin.install === 'function') {.....}

在 src/core/index.js 中
import Vue from ‘./instance/index’
initGlobalAPI(Vue)

查找 vue 的引入

路径:/src/core/instance/index.js
这才是 Vue 实例,构造函数声明的位置
在这,构造函数

    function Vue (options) {......  
    // 在这里面做了一件事,是初始化方法  
    this.\_init(options)  
    }  
    // 实现实例方法和属性  
    initMixin(Vue) // 这里做了一次混入,init 方法的声明 \_init()  
    stateMixin(Vue) // 和状态相关的实例方法 $set/$delete/$watch /.....  
    eventsMixin(Vue) // 和事件相关的方法 $on/$off/$once/$emit  
    lifecycleMixin(Vue) // 和生命周期相关的方法  
    renderMixin(Vue) // 渲染相关:声明和实现 $nextTick /\_render
    
initMixin(Vue) 声明_init

路径:src/core/instance/init.js

 export function initMixin (Vue(将 vue 进行传入): Class<Component>) {.....  
    // \_init 做一个实例方法:\_init 用下划线的目的是,内部使用  
    Vue.prototype.\_init = function (options(传进来的选项)?: Object) {.....  
       初始化的核心代码在这里 
       // 选项的合并:vue 会有默认选项,和用户传进来的选项,进行合并,比如用户传进来的 el  
       if (options && options.\_isComponent) {.....}            else {.....}  
    }  
}
stateMixin(Vue) // 和状态相关的实例方法 **

路径:src/core/instance/state.js

 export function stateMixin (Vue: Class<Component>){.....  
//$set  
 Vue.prototype.$set = set  
//$del  
 Vue.prototype.$delete = del  
//$watch  
Vue.prototype.$watch = function (....){....}  
}
lifecycleMixin(Vue) // 和生命周期相关的方法

路径:src/core/instance/lifecycle.js

 export function lifecycleMixin (Vue: Class<Component>) {......  
       // 定义例如 \_update:更新 将虚拟 dome 转化为真实 dome;Vue.prototype.\_update = function (vnode: VNode, hydrating?: boolean) {}  
       .......  
       // 强制更行  
       Vue.prototype.**$forceUpdate**\= function () {  
          const vm: Component = this  
          if (vm.\_watcher) {  
           // 主要就是将所有的 watcher 更新一遍  
            vm.\_watcher.update()}  
       }  
        // 实例销毁  
        Vue.prototype.**$destroy**\= function () {}             ..... 等等  
}
initMixin(Vue) 声明_init 里面完成的事情
nitLifecycle(vm) // $parent $root $children  
initEvents(vm) // 事件监听
initRender(vm) // vm.$slots // vm.$createElement
// 在 beforeCreate 之前,上面的是一个初始准备  
声明周期调用的统一方法是 callHook,这个方法  
callHook(vm, 'beforeCreate') // 去派发注册事件,所以在 beforeCreate 中,是可以访问到上面三个内容的  
initInjections(vm) // resolve injections before data/props   
initState(vm) // 核心:数据初始化  
// 为什么 Injections 会在 Provide 之前去注册?因为继承问题,Injec 可以拿到之前的祖辈的,之后在进行分发下去,这样,Provide 就可以拿到祖辈传过来了,以及在 state 里面注册的数据 
initProvide(vm) // resolve provide after data/props  
callHook(vm, 'created') // 所有数据相关的,都在 beforCreate 和 created 之间,上面三个内容,才会将数据变成响应式。
initLifecycle(vm)

路径:src/core/instance/lifecycle.js

export function initLifecycle (vm: Component) {.....  
初始化的时候,只有根组建和父组建
}
initEvents(vm)

路径:src/core/instance/events.js

export function initEvents (vm: Component(当前组建)) {vm.\_events = Object.create(null)  
    vm.\_hasHookEvent = false  
    // init parent attached events  
    // 事件处理 - 谁派发,谁监听;// vm.$options.\_parentListeners:从选项中拿到,在 parent(父级)中去声明的事件监听器  
    const listeners = vm.$options.\_parentListeners // 初始化父组件添加的事件  
    if (listeners) { // 如果父组建注册是事件存在,监听器存在  
        // 子组建就去注册事件  
        updateComponentListeners(vm, listeners)  
    }  
}
initRender(vm)

路径:src/core/instance/render.js

vm.$slots
vm.$createElement// 在 new Vue 中的“h”

vue 源码分析 - 数据响应式

数据响应式是 MVVM 的特点,vue 数据双向绑定的原理:是利用了 JS 语 言特性 Object.defineProperty(),通过定义对象属性 setter 方法拦截对象属性变更,从而将数值的变化 转换为 UI 的变化。
具体实现是在 Vue 初始化时,会调用 initState,它会初始化 data,props 等
initState(vm)

路径:src/core/instance/state.js

export function initState (vm: Component) {.....  
    // 判断 data 是否存在  
    if (opts.data) { // 如果 data 存在,走这里,因为要看数据的双向绑定,所以找到 initData  
        initData(vm)  
     } else {observe(vm.\_data = {}, true /\* asRootData \*/)  
     }  
}
当前目录下 -initData(vm)
function initData (vm: Component) {....  
    // 传进来的 data 可能是函数,或者对象,目的是防止数据污染;let data = vm.$options.data  
   data = vm.\_data = typeof data === 'function'  
   ? getData(data, vm)  
   : data || {}  
  
....  
   // 代理、重复判断
   const keys = Object.keys(data)  
   while (i--) {....  
      // 属性和方法不能是重复的判断
      if (process.env.NODE\_ENV !== 'production') {}  
      if (props && hasOwn(props, key)) {}}  
    // 对 data 数据响应式处理  
    observe(data, true /\* asRootData \*/)  
}
对 data 数据响应式处理

observe(data, true /* asRootData */)
路径:src/core/observer/index.js

export function observe (value: any, asRootData: ?boolean): Observer | void {....  
    // 为传进来的对象 value,创建一个 observer 实例;任何一个对象都伴随一个 observer 实例  
    // 返回一个 observer 实例  
    // 用 observer 实例,来判断类型 以及内部响应式处理  
  
    // 如果做过响应式,那么 \_ob\_ 存在,就直接返回  
    if (hasOwn(value, '\_\_ob\_\_') && value.\_\_ob\_\_ instanceof Observer) {ob = value.\_\_ob\_\_} else if (...){...}else{...  
       // 如果未处理,就创建一个新的实例 ob 实例  
       ob = new Observer(value)  
     }  
}
new Observer(value)

路径:src/core/observer/index.js

export class Observer {constructor (value: any) {// 可以理解为小管家:比如在 data 中的数据是对象包对象的存在,data:{obj:{foo:foo}}, 对对象来说,是对对象新             增和删除的通知工作;由小管家来通知的。// 当需要修改 obj 里面的属性,是在肚子里发生的事情,所以需要小管家来进行处理  
       // 如果 obj 是一个数组,选项的删除也是需要小管家  
       // 对象属性变化或者数组元素变化需要小管家通知更新  
       this.dep = new Dep() // 小管家 
    // 判断传入的 value 类型,做相应的处理
       if (Array.isArray(value)) { // 当是 Array 时的处理  
             // 覆盖数组实例的原型  
        }else{  
            // 对象的处理
            this.walk(value)  
       }  
    }  
    walk (obj: Object) {....  
         const keys = Object.keys(obj)  
         // 遍历当前的所有 keys  
         for (let i = 0; i < keys.length; i++) {  
            // 对每个 key 进行拦截  
            defineReactive(obj, keys\[i\])  
         }  
    }  
}
defineReactive
export function defineReactive (  
   obj: Object,  
  key: string,  
  val: any,  
  customSetter?: ?Function,  
  shallow?: boolean  
) {......  
  
    // 大管家 和 key 一对一的关系  
    const dep = new Dep()  
   // 用户自定义一些属性  
   const property = Object.getOwnPropertyDescriptor(obj, key)  
   ..............  
  
  // 核心:递归,如果 val 是对象,则获取一个子 oberser 实例,一个对象就会有一个 observer 实例  
  
  let childOb = !shallow && observe(val)  
  // 做递归处理  
  Object.defineProperty(obj, key, {  
      enumerable: true,  
      configurable: true,  
      get: function reactiveGetter () {  
          // 依赖收集  
          const value = getter ? getter.call(obj) : val  
          // Dep.target 可以理解为 watcher 的实例,每次触发 watcher 实例的时候,会手动触发,get 事件一下获取当前                 值,将实例放在 Dep.target 上;if (Dep.target) {// dep 和 watcher 是 n 对 n 的关系,除了页面渲染 {{name}} 会触发 watcher 以外;还有可能是 computed 或者 w                    atch 触发的事件监听;// 双向添加两者关系  
                dep.depend()  
               // 若存在子 ob  
               if (childOb) {  
               // 把当前的 watcher 和子 ob 中的 dep 建立关系  
               childOb.dep.depend()  
               if (Array.isArray(value)) {dependArray(value)  
               }  
           }  
        }  
        return value  
      },  
      set: function reactiveSetter (newVal) {}}  
}
dep.depend

路径:src/core/observer/dep.js

depend () {if (Dep.target) {  
    // 执行的是当前 watcher 的 addDep 
    Dep.target.addDep(this)  
    }  
}
Dep.target.addDep

找到 watcher 的 addDep 事件
路径:src/core/observer/watcher.js

addDep (dep: Dep) { //dep 和 wacter 相互保存的关系  
    const id = dep.id  
    if (!this.newDepIds.has(id)) { // 是否和当前的 dep 建立关系  
        // 没有就创建 dep 和 watcher 的关系  
        this.newDepIds.add(id)  
        this.newDeps.push(dep)  
        if (!this.depIds.has(id)) {  
            // 创建 dep 和 watcher  
            dep.addSub(this)  
        }  
    }  
} 
dep.addSub

src/core/observer/dep.js

export default class Dep {  
    // 往自己的 sub 里面去 push  
    addSub (sub: Watcher) {this.subs.push(sub)  
    }  
}

学习资料:

思维导图 vue 源码解析(一):https://www.processon.com/vie…
获取 vue 源码项目地址:https://github.com/vuejs/vue
迁出项目: git clone https://github.com/vuejs/vue.git
当前版本号:2.6.11

我写的主要是梳理,初始化流程和响应式过程的一个学习思路。如果有错误,欢迎补充。如果觉得在这里看文档不太清晰,可以去我写的思维导图的地址去看下。
接下来还会整理出源码的其他内容,尽情期待~~~拜拜

正文完
 0