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

vue 版本为 2.6.11 博客的篇幅有点大,若是以为比较繁琐的,能够跳着看,里面也没有粘大量的源码,我会吧git上源码的连接贴出来,你们能够对照着源码连接或者把源码下载下来对照着看

看了好久的vue 的源码,也看了好多关于源码的贴子,本身也尝试了写了好几回vue源码的帖子,一是以为写的没有章法思路不够清晰,二是以为vue3都出了我如今写vue2的源码学习有点晚了因此没有发表,后来想一想本身写出来能够沉淀一些东西,而且也给你们提供一个阅读源码的思路, 但愿能写出一篇对我和对你们都有益处的帖子html

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

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

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

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

找到入口

入口文件

若是想要知道如何找到入口请看 找入口的思路,若是以为不必看能够跳过 git

入口文件为platforms/web/entry-runtime.js 或者 platforms/web/entry-runtime-with-compiler.js 前者为不带 compiler 的后者为带有 compiler 的,由于咱们的目标设定中有 compiler 的内容,因此咱们就选后者为咱们本次源码学习的入口,对文件的依赖进行分析找到 VUe 的构造函数文件在 core/instance/index.js 这个文件中github

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 这个方法了web

_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的命名规范很是的好,经过命名我就能轻松的看出来每一个方法实现的功能,也是咱们值得学习的地方)。express

找入口的思路

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

文件 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 的源码等看完以后在补充上来

相关文章
相关标签/搜索