vue 源码学习(一)入门和响应式原理

 约 23 分钟
vue 版本为 2.6.11 博客的篇幅有点大,如果觉得比较繁琐的,可以跳着看,里面也没有粘大量的源码,我会吧git上源码的链接贴出来,大家可以对照着源码链接或者把源码下载下来对照着看

看了很久的vue 的源码,也看了好多关于源码的贴子,自己也尝试了写了好几次vue源码的帖子,一是觉得写的没有章法思路不够清晰,二是觉得vue3都出了我现在写vue2的源码学习有点晚了所以没有发表,后来想想自己写出来可以沉淀一些东西,并且也给大家提供一个阅读源码的思路, 希望能写出一篇对我和对大家都有益处的帖子

首先设定目标, 我们不可能一行不差的把vue的源码都看一遍(如果每行都看很快就会失去方向和兴趣),所以我们要知道我们看源码的目标是什么,以及看到什么程度就认为吧vue 的真正的核心代码和思想都学会了。
我认为把下面的这些点都了解的透彻了就算是真正的学到了vue 的精髓

  1. vdom
  2. compiler
  3. 响应式原理 (watch, computed,收集依赖,getter setter)
  4. 指令原理
  5. filter 原理
  6. vue2 和vue3 的核心区别点

上面的点就是我们学习的目标,一定要先设定目标,要不就不知道我们学习源码的意义,学着学着就放弃了,我们要学会以目标为导向。

首先我们找到 vue 项目的入口然后我们从我认为vue中最重要的最核心的内容响应式原理开始学起

找到入口

入口文件

如果想要知道如何找到入口请看 找入口的思路,如果觉得没必要看可以跳过

入口文件为platforms/web/entry-runtime.js 或者 platforms/web/entry-runtime-with-compiler.js 前者为不带 compiler 的后者为带有 compiler 的,因为我们的目标设定中有 compiler 的内容,所以我们就选后者为我们本次源码学习的入口,对文件的依赖进行分析找到 VUe 的构造函数文件在 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)  // 添加 _init 方法  
stateMixin(Vue) // $set $delete $watch  $data  $props  
eventsMixin(Vue) //$on $once $off $emit  
lifecycleMixin(Vue) //_update  $forceUpdate  $destroy  
renderMixin(Vue) //$nextTick  _render

这个文件的主要作用是定义了Vue 并丰富了他的原型上的方法(我在上面的注释中标注了每个方法分别对应了添加了那些原型方法),看一下vue的构函数,发现只调用了 _init 方法,整个Vue的入口就是 _init 这个方法了

_init 方法

我们对 _init 方法的内容进行分析

Vue.prototype._init = function (options?: Object) {
  ...
 if (options && options._isComponent) {  
   initInternalComponent(vm, options)  
 } else {  
    vm.$options = mergeOptions(  
        resolveConstructorOptions(vm.constructor),  
  options || {},  
  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')
 if (vm.$options.el) {  
  vm.$mount(vm.$options.el)  
 }
  ...
}

我这里省略了一部分源码,大家可以对照着自己下载下来的源码看,这个 _init 方法做了做了好多的事情,我们不可能在这里一次说清楚,并且,我们要知道我们真正想要看的是什么,找到这个文件的目的是为了让我们更好的达成目标(当然不是说大家就不用看阿,只是不要太深究,因为每一个方法都有特别深的调用链,如果看下去肯定会失去方向跑偏的哦),通过语义上看,我们要看的响应式原理应该从 initState 方法入手 往下深入的学习(vue的命名规范非常的好,通过命名我就能轻松的看出来每个方法实现的功能,也是我们值得学习的地方)。

找入口的思路

如何找到入口呢,其实写框架和我们平常写代码一样,想想,我们如果接触到一个陌生的项目应该如何找到项目的入口呢,肯定是先看 package.json,然后分析其中的 script ,根据经验和语义的判断得出(大家也可以看看vue 的开发者文档之类的也能找到一些介绍,我觉得找一个入口不想太费时间所有就没有那么严谨的去看),vue 打包的命令应该为 "build": "node scripts/build.js",,我们现在分析这个文件,这个文件不用太仔细的看,如果真的想要学习 rollup 打包的话可以深入的看一看,我们的目的是找到项目真真的入口,在这里深入的去学习只会把我们带偏。

文件 scripts/build.js

let builds = require('./config').getAllBuilds()

文件 scripts/config.js

const builds = {
  ...
  'web-runtime-esm': {  
    entry: resolve('web/entry-runtime.js'),  
    dest: resolve('dist/vue.runtime.esm.js'), 
    ...
  },
  'web-full-dev': {  
    entry: resolve('web/entry-runtime-with-compiler.js'),  
    dest: resolve('dist/vue.js'),  
  },
  ...
}

上面我粘出来的代码就是我们所寻找的入口文件,我是如何定位到这两个的呢,通过看package.json 中的 main,module,unpkg 这三个配置分别为我们使用vue 库时候的 cjs,esm,cdn的引入文件,他们对应的entry就是入口文件了。

响应式原理

首先我们看vue的官方文档,对响应式原理的介绍非常的详细(一定要看,一定要看,并且要记到心里,深刻理解,面试的时候会问的哦 ),我这里就不在多做赘述 官方文档

首先用一句话来解释响应式原理 当组件的数据发生变化的时候触发组件的重新渲染
响应式原理核心的点有哪些,也就是我们学习响应式原理最终要学会什么,也就是我们细分的 目标

  1. watcher
  2. observer
  3. getter, setter
  4. 依赖收集 (dependency collect)依赖和watcher的关系

我认为吧上面的几个点搞明白也就真正的搞明白了响应式原理

下面我们从源码入手开始分析响应式原理

本来准备从 _init 中调用的 _initState 中深入分析的,但是其实你在看了 _initState 方法之后你会发现,这里面大量的依赖了 watcher observer 和dep 这几个类,所以我们先把这几个类完全的搞清楚再看 _initState 方法这样有助于我们的阅读

阅读源码不一定非要按照调用栈一层层的扒代码学习,当你发现这部分代码强烈的依赖另一部分代码的时候,可以先搞懂他依赖的那一部分,这样更方便我们的理解。

我们首先想想 vue 的实现响应式原理的逻辑,再去看这三个类,如果我们忘了核心的思想去看代码会特别的迷茫,就像没有需求写代码一样,并且在我们自己写代码的过程中也是先想清楚了这个类要实现什么功能,再动手去写这个类,回顾一下思想, 大致流程是 为data设置getter 和setter, getter 的时候收集依赖,setter 的时候调用依赖的回调。

源代码一般篇幅比较大调用栈比较深,当遇到不理解的问题时回顾基本原理和目标,就不会丢了方向了。

总的关系

如果不想看代码细节的同学,可以跳过下面的代码细节,这里总结了一下 Observer watcher 和dep 各自的主要功能和他们之间的关系

observer 主要功能,将传入的data 转换为Observer data ,就是设置新的get 和set 方法

dep 对象主要是数据和 watcher 之间的一个桥梁,存放的是数据更新时需要调用的 watcher,并在数据更新的时候触发所有watcher 的update

watcher 对象是监控数据变化并调用回调,与dep的关系是,dep触发watcher 的更新,一个watcher可以有多个dep

Observer

文件:core/observer/index.js

我们大致的浏览这个中的内容,导出了一个 Observer类,defineReactive 和 observe 这个方法,其他的方法先不看,等用到的时候再看。

observe方法

  1. 如果传入的不是一个对象或者传入的是一个 Vnode 就return
  2. 如果传入的对象中包含 __ob__ob = value.__ob__
  3. 否则如果 shouldObserve==true 加上其他的一些校验通过的话 ob = new Observer(value)
  4. 如果 asRootData && obvmCount++ 如果是组件的跟data对象 记录一下 vmCount

总的来说这个方法就是 new Observer 接下来我们看一下Observer 类

Observer类

构造函数 constructor(value: any)

  1. this.value 的值为传入的value
  2. this.dep = new Dep()
  3. this.vmCount = 0 记录依赖组件的数量
  4. 把this对象添加到 value 对象的 __ob__ 属性上
  5. 如果value为数组调用observeArray 否则调用 walk

walk(obj: Object) 方法

循环对象的keys,调用 defineReactive重新设置属性的 get和set

observeArray(items: Array<any>) 方法

循环数组对象,调用observe(item[i]),吧数组中的每一个对象都设置为响应式数据

defineReactive方法
主要参数: obj 对象, key修改的字段
主要功能: 1.获取当前传入key对应值的 Observer 实例 2. 为 传入对象的key属性设置get和set

我们知道vue 响应式是在 get 的时候收集依赖,在set 的时候调用回调的,我们看看这里是如何收集和调用回调的呢?

const dep = new Dep()
...
let childOb = !shallow && observe(val)
...
get: function reactiveGetter() {  
  const value = getter ? getter.call(obj) : val  
  if (Dep.target) {  
    dep.depend()  
    if (childOb) {  
      childOb.dep.depend()  
      if (Array.isArray(value)) {  
        dependArray(value)  
      }  
    }  
  }  
  return value  
},
set: function reactiveSetter(newVal) {  
  const value = getter ? getter.call(obj) : val  
  /* eslint-disable no-self-compare */  
  if (newVal === value || (newVal !== newVal && value !== value)) {  
    return  
  }  
  /* eslint-enable no-self-compare */  
  if (process.env.NODE_ENV !== 'production' && customSetter) {  
    customSetter()  
  }  
  // #7981: for accessor properties without setter  
  if (getter && !setter) return  
  if (setter) {  
    setter.call(obj, newVal)  
  } else {  
    val = newVal  
  }  
  childOb = !shallow && observe(newVal)  
  dep.notify()  
}

get 方法调用 dep 的depend 的方法收集依赖
set 方法调用 dep 的 notify 发通知更改

总结:Observer 的使命就是吧data 转换为一个响应式的数据,数据添加 __ob__get set 方法收集依赖和通知更新

Dep 依赖

文件 core/observer/dep.js

属性介绍 1. 是id从0 开始计数,每次new 一个新实例加一 2. subs:指订阅了该dep的所有watcher 对象

方法介绍

addSub 添加订阅者
removeSub 删除指定订阅者
depend 收集依赖
notify 通知更新,遍历所有的订阅者,调用订阅者(watcher)的update方法

静态变量target

当前的系统中的 watcher 对象 ,同一时刻只能有一个存在,通过调用 pushTarget 和 popTarget 这两个方法来设置

Watcher

文件 core/observer/watcher.js

构造函数逻辑

  1. 如果为 isRenderWatcher 情况为vm._watcher = this 赋值 (在render的时候会用到可以先不关注)
  2. 把当前的实例放到 vm._watchers 对象中,vm._watchers中包含当前vm的所有 watcher对象
  3. 把options 的值赋值到 this 对象上,需要特别关注的几个属性

    1. deep
    2. lazy 为true的时候不调用 get 方法, initComputed 的时候lazy为true
    3. sync 表示的是同步更新,如果sync为false watcher 对象则放到一个队列中执行
  4. 把cb 赋值给 this.cb, cb为 watcher 对象的回调,每次数据更新的时候都会调用
  5. 添加id属性 watcher 对象的标识,从0 开始计数,每 new 一个实例 +1
  6. 添加active属性 当前watcher 对象的状态,如果active 为false则当前watcher对象不再生效
  7. 添加dirty 属性 当lazy为true 的时候 dirty 也为true,lazy为false 的时候不会直接调用get方法,dirty用来标识get是否被调用过,调用之后变为 false
  8. 添加deps 属性 watcher当前订阅的所有dep 对象
  9. 添加 newDeps 属性 重新运算之后当前 watcher 订阅的所有dep 对象 (主要用于新老依赖做对比,清除dep中无用的subs)
  10. 添加 depIds 属性 watcher当前订阅的所有dep 对象的id
  11. newDepIds 属性 重新运算之后当前 watcher 订阅的所有dep 对象的id
  12. expression 用于异常处理的提示
  13. 添加getter 属性,getter属性由 expOrFn 转化而来的一个 function ,如果expOrFn 的值为一个function则getter 为该方法,否则调用parsePath 方法将表达式转换为一个方法,此时getter方法的返回值为监控的数据 (line 79~91)
  14. 添加value 属性, value属性为 watcher 对象监控的数据, 当lazy 为true(computed配置里的watcher 对象的lazy为true) 的时候为undefined ,lazy为false的时候调用 get 方法收集依赖并获取value

get方法的逻辑

  1. 调用 dep 中的 pushTarget 方法把dep中的 Dep.target 设置为当前的 watcher 对象,表示后面所有的依赖收集都收集到当前的 watcher对象上
  2. 调用 this.getter 方法实现依赖的收集,this.getter 方法执行过程中所有被observer过的对象的依赖都会收集到当前的可以 watcher 对象上,
  3. 在finally 方法中如果deep为true 则调用traverse方法对最终的value进行递归,实现对所有的子属性的依赖收集,最后调用 dep中的popTarget方法,关掉收集,并调用 cleanupDeps 方法

addDep方法的逻辑

  1. 为当前的 watcher 对象添加newDepIds, 判断当前watcher 中newDepIds 对象是否包含 dep的id,如果不包含,为当前的watcher 对象的newDepIds 和newDeps 添加新的依赖
  2. 把当前的watcher对象添加到 dep 对象的subs属性中 ,如果 this.depIds 中不存在 depid 则把这个当前的watcher 对象放到dep对象的subs 中

cleanupDeps方法的逻辑

get 方法调用的

  1. 对deps(老的依赖)进行循环,如果newDepIds中不包含循环项目,则说明当前watcher对象没有依赖老的dep对象,则调用dep.removeSub(this) 把 dep subs中的当前watcher 清除
  2. 把 depIds 更新为 newDepIds,把newDepIds 清空
  3. 把 deps 更新为 newDeps, 把 newDeps 清空

update方法的逻辑

dep 的notify 方法中调用的

在监听数据发生变化的时候更新,如果 lazy==truethis.dirty = true 不更新,如果sync为true 则同步更新否则调用 queueWatcher(this)方法放入队列执行

run方法

run方法主要是重新计算value, 并执行回调

evaluate 方法

这个方法只在lazy为true的时候调用
计算value 的值, 并吧dirty 设置为false

teardown方法

清空watcher 对象上的依赖,以及清空 dep 上的subs 的watcher 对象,并且吧 this.active 设置为 false

下面我们看 initState 方法

入口方法_init 中调用的initState 方法

export function initState(vm: Component) {  
  vm._watchers = []  
  const opts = vm.$options  
  if (opts.props) initProps(vm, opts.props)  
  if (opts.methods) initMethods(vm, opts.methods)  
  if (opts.data) {  
    initData(vm)  
  } else {  
    observe(vm._data = {}, true /* asRootData */)  
  }  
  if (opts.computed) initComputed(vm, opts.computed)  
  if (opts.watch && opts.watch !== nativeWatch) {  
    initWatch(vm, opts.watch)  
  }  
}

initState 方法定义了 _watchers 变量, 并调用 initProps, initMethods, initData ,initComputed, initWatcher 分别对 props, methods, data,computed,watcher进行初始化

initProps

  1. 定义 vm._props
  2. 遍历 propsOptions 调用 defineReactive 把 propsData 中的数据转化为响应式数据 , 然后调用 proxy(vm, '_props', key) 这个把propsData 中的数据通过代理的方式挂到 vm 上

initData

  1. vm._data 赋值,如果 options.data是一个方法则调用 getData(data,vm) 否则直接返回 options.data , getData 方法主要是执行了 options.data方法
  2. 循环data的keys, 调用 proxy(vm, "_data", key) 方法,将_data上的属性使用代理的方式挂到vm 上
  3. 调用 observe(data, true);方法吧 data对象转换为 observe 之后的对象

initComputed

  1. 定义了 vm._computedWatchers
  2. 循环 options.computed 将 computed 中的每一个方法 new 一个 Watcher 对象放到了 vm._computedWatchers,需要注意的是 这里 watcher 对象没有回调,并且设置了 options的{lazy: true}这意味着,computed 的方法不是立即被调用的
  3. 调用 defineComputed 方法把 compute 上的每个key 作为一个变量挂到的 vm 上,变量的get方法是通过调用 createComputedGetter 这个方法返回的一个方法
  4. createComputedGetter 方法:根据传入的key 调用_computedWatchers[key]的 get方法, 最后返回watcher 的 value

initWatch

循环 options.watcher并且调用 createWatcher ,createWatcher 又调用了vm.$watch, 这个方法 new 了一个 Watcher对象

initMethods

内容比较简单主要是将 options.methods 中的方法挂载到 vm 对象上并别将 this 指向了 vm

跟Vue3 的区别

我还没有看vue3 的源码等看完之后在补充上来

阅读 293

推荐阅读
目录