|
| 1 | +## 霖呆呆的Vue源码之路(一) |
| 2 | + |
| 3 | +### Vue源码从哪里开始看? |
| 4 | + |
| 5 | +去`GitHub`上把`Vue`源码这个项目下载到本地:[https://github.com/vuejs/vue](https://github.com/vuejs/vue) |
| 6 | + |
| 7 | +以下教材案例中讲解的版本是为`2.6.11`。 |
| 8 | + |
| 9 | +`Vue`源码是基于`Rollup`构建的,它构建相关的配置都在`scripts`目录下。 |
| 10 | + |
| 11 | +通常,我们查看一个基于`NPM`托管的项目都会有一个`package.json`文件,它记录的是对这个项目的描述。 |
| 12 | + |
| 13 | +所以首先让我们找到项目的根目录中的`package.json`,然后查询到它的`scripts`属性,找到`build`指令,看看它指向的是哪个文件: |
| 14 | + |
| 15 | +```json |
| 16 | +{ |
| 17 | + scripts: { |
| 18 | + "build": "node scripts/build.js", |
| 19 | + "build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer", |
| 20 | + "build:weex": "npm run build -- weex", |
| 21 | + } |
| 22 | +} |
| 23 | +``` |
| 24 | +然后通过以下过程的查找,找到`Vue`这个构造函数: |
| 25 | + |
| 26 | +``` |
| 27 | +scripts/build.js => scripts/config.js => src/platforms/web/runtime/index.js => |
| 28 | +src/core/index.js => src/core/instance/index.js |
| 29 | +``` |
| 30 | +可以看到,`src/core/instance/index.js`中导出的就是一个`Vue`构造函数。只不过在最终导出`Vue`的时候会经过一系列的处理。 |
| 31 | + |
| 32 | + |
| 33 | + |
| 34 | +小提示⏰:如果你是使用`vscode`查看`Vue`源码的话,`js`文件中有`ts`语法时,`vscode`会提示错误,此时可以在`vscode`的`setting.json`中添加这么一个配置: |
| 35 | + |
| 36 | +```javascript |
| 37 | +{ |
| 38 | + "javascript.validate.enable": false, // 禁用默认的 js 验证 |
| 39 | +} |
| 40 | +``` |
| 41 | + |
| 42 | +当然还有说可以:设置中搜索tsconfig ->Check JS Experimental Decorators 去掉勾选。 |
| 43 | + |
| 44 | + |
| 45 | + |
| 46 | +(但是我试了这样没有效果...) |
| 47 | + |
| 48 | +记住,当我们在看源码的时候,看任何一个复杂的函数时,我们找到自己想要看的东西才是关键,其它的非关键的内容你大可不必太过关注它,如果你是一行一行的看并且又试图一次性全部搞懂的话那会非常的吃力,所以我们应该是要抱有目的性的去看源码,例如我今天是想要了解`Vue`的响应式原理,那我大部分的精力肯定就都是在解决`数据是什么时候被劫持的?`,`依赖收集又是怎样实现的呢?`这类的问题上。 |
| 49 | + |
| 50 | +### 初始化Vue构造函数时会做哪些事? |
| 51 | + |
| 52 | +初始化Vue构造函数,也就是还未使用`new Vue()`的时候,会做这么几件事: |
| 53 | + |
| 54 | +- 调用一些以`Mixin`命名的函数,为`Vue.prototype`上添加实例方法 |
| 55 | +- 调用`initGlobalAPI`函数,为`Vue`构造函数添加静态方法 |
| 56 | +- 当然还有初始化`createPatchFunction、strats类、createCompilerCreator编译器`等 |
| 57 | + |
| 58 | + |
| 59 | + |
| 60 | +#### 为Vue.prototype上添加实例方法 |
| 61 | + |
| 62 | +`Vue`这个构造函数在创建的时候,会调用一些以`Mixin`命名的函数,为`Vue.prototype` 上添加方法,例如: |
| 63 | + |
| 64 | +1. `initMixin`: 给原型上添加`_init`方法,用于`Vue`构造函数实例化对象的时候用。 |
| 65 | +2. `stateMixin`: 给原型上添加`$set、$delete、$watch`方法 |
| 66 | +3. `eventsMixin`: 给原型上添加`$on、$off、$once、$emit`方法 |
| 67 | +4. `lifecycleMixin`: 给原型上添加`_update,$forceUpdate,$destroy`方法 |
| 68 | +5. `renderMixin`: 给原型上添加`_o,_n,_s,_l,_t,_q,_i,_m,_f,_k,_b,_v,_e,_u,_g,$nextTick,_render`方法 |
| 69 | + |
| 70 | +对应着源码的位置就是: |
| 71 | + |
| 72 | + |
| 73 | + |
| 74 | +``` |
| 75 | +src/core/instance/index.js |
| 76 | +``` |
| 77 | + |
| 78 | +记不住没有关系,你脑子里就想一下大概是做了这么一个事就可以了,比如`eventsMixin`这个方法,往总体看,它内部其实很简单: |
| 79 | + |
| 80 | +```javascript |
| 81 | +export function eventsMixin (Vue) { |
| 82 | + Vue.prototype.$on = function () {} |
| 83 | + Vue.prototype.$off = function () {} |
| 84 | + Vue.prototype.$once = function () {} |
| 85 | + Vue.prototype.$emit = function () {} |
| 86 | +} |
| 87 | +``` |
| 88 | +所以你会发现这些都是我们很容易就看得懂的东西,不就是给`Vue`这个构造函数的原型对象上添加一些属性和方法吗,那么如果我们`new Vue()`一个实例的时候,这个实例就可以继承到这些属性和方法了。 |
| 89 | + |
| 90 | + |
| 91 | +#### 为Vue构造函数添加静态方法 |
| 92 | + |
| 93 | +同时,也会调用`initGlobalAPI`函数,为`Vue`构造函数上添加静态方法,例如: |
| 94 | + |
| 95 | +1. 添加`Vue.set`静态方法,用于更新视图 |
| 96 | +2. 添加`Vue.delete`静态方法,用于删除数据 |
| 97 | +3. 添加`Vue.nextTick`静态方法,用于更新视图后回调递归 |
| 98 | +4. 添加`Vue.options`静态属性,并且在该对象中添加`components,directives,filters`静态对象 记录静态组件 |
| 99 | +5. 添加 `Vue.use、 Vue.mixin、Vue.extend`静态方法等等... |
| 100 | + |
| 101 | +对应这源码调用的位置就是: |
| 102 | + |
| 103 | + |
| 104 | + |
| 105 | +``` |
| 106 | +src/core/index.js |
| 107 | +``` |
| 108 | + |
| 109 | +看懂了之前的给`Vue.prototype`上添加实例方法,那么这里添加静态方法也很好理解了,我们知道,实例方法和静态方法的区别就是: |
| 110 | + |
| 111 | +- 实例方法是定义在实例对象或者实例对象的原型链上的方法,实例对象可以调用它 |
| 112 | +- 静态方法是定义在构造函数上的方法,并不能被实例对象所调用 |
| 113 | + |
| 114 | +(什么?你还不懂其中的区别?那你可得好好看看霖呆呆的这篇文章了:[【何不三连】比继承家业还要简单的JS继承题-封装篇(牛刀小试)](https://juejin.im/post/5e707417e51d45272054d5d3)) |
| 115 | + |
| 116 | +那么这里的`initGlobalAPI`从整体看,也非常的简单: |
| 117 | + |
| 118 | +``` |
| 119 | +export function initGlobalAPI (Vue) { |
| 120 | + // ... |
| 121 | + Vue.set = set |
| 122 | + Vue.delete = del |
| 123 | + Vue.nextTick = nextTick |
| 124 | + // ... |
| 125 | +} |
| 126 | +``` |
| 127 | + |
| 128 | + |
| 129 | +### new Vue实例化对象时会做哪些事? |
| 130 | + |
| 131 | +当`new Vue`实例化对象的时候,最主要的就是执行`this._init()`方法,这个方法是在上面👆「初始化Vue构造函数」时调用`initMixin()`函数时给`Vue.prototype`上添加的,对应这伪代码就是: |
| 132 | + |
| 133 | +*src/core/instance/index.js* |
| 134 | +```javascript |
| 135 | +import { initMixin } from './init' |
| 136 | + |
| 137 | +function Vue (options) { |
| 138 | + this._init(options) |
| 139 | +} |
| 140 | +initMixin(Vue) |
| 141 | + |
| 142 | +export default Vue |
| 143 | +``` |
| 144 | +*src/core/instance/init.js* |
| 145 | +```javascript |
| 146 | +export function initMixin (Vue) { |
| 147 | + Vue.prototype._init = function (options) { |
| 148 | + // 初始化实例对象的代码... |
| 149 | + } |
| 150 | +} |
| 151 | +``` |
| 152 | +所以当我们在执行`new Vue()`的时候,最关键的就是执行`_init()`函数。 |
| 153 | + |
| 154 | +而在此`_init()`函数中主要会做这么几件事: |
| 155 | + |
| 156 | +1. 初始化生命周期`initLifecycle` |
| 157 | +2. 初始化事件`initEvents` |
| 158 | +3. 初始化渲染`initRender` |
| 159 | +4. 调用`callHook`函数触发`beforeCreate`钩子函数 |
| 160 | +5. 调用`initInjections`获取到父组件的`provide`值并加入观察者中 |
| 161 | +6. 调用`initState`,初始化`data、props、methods、computed、watch`,并为`Vue`实例化对象`vm`添加`watchers`观察者队列 |
| 162 | +7. 以及调用`initProvide`方法和触发`created`钩子函数等等... |
| 163 | + |
| 164 | + |
| 165 | +对应着源码的位置就是: |
| 166 | + |
| 167 | + |
| 168 | + |
| 169 | + |
| 170 | +#### initState()方法做了哪些事? |
| 171 | + |
| 172 | +这一章节主要是研究`Vue`的响应式原理,所以让我们着重看一下`initState()`方法,毕竟它里面初始化了`data、props、methods、computed、watch`这些东西,它所处的源码位置: |
| 173 | + |
| 174 | + |
| 175 | + |
| 176 | +OK👌,各个函数的作用其实不用我写备注你们也能看出来是什么意思了。 |
| 177 | + |
| 178 | +例如`initProps`就是初始化`props`,`initData`就是初始化`data`,这些函数命名都很友好 😊。 |
| 179 | + |
| 180 | +每种初始化函数中都有各自复杂的逻辑,让我们来看看这个`initData`方法吧,它应该就是初始化数据的关键了: |
| 181 | + |
| 182 | + |
| 183 | + |
| 184 | +咋一看这个`initData`好像非常的复杂,但是其实我们只要把它拆成三部分来看就觉得它没什么了: |
| 185 | + |
| 186 | +- 判断`data`是不是一个方法,如果是方法的话就调用`getData`方法,`getData`中会有一段关键的代码:`return data.call(vm, vm)`,也就是执行`data()`这个方法,这里其实很好理解,还记得我们在创建一个`vue`组件的时候,里面的`data`要求返回的是一个函数吗?那么这一步的作用相当于是拿到这个返回函数中的数据。 |
| 187 | +- 判断`data`中每个属性有没有什么特殊的情况,例如是否和`methods、props`中的属性重名了,以及不是以`$、_`开头的属性则调用`proxy`方法。 |
| 188 | +- 最重要的一步就是把数据添加到观察者中了,也就是执行`observer(data, true)`方法。 |
| 189 | + |
| 190 | +(`proxy()`方法是一个代理,它有什么作用呢?唔...比如一种场景:使得原本需要 `app._data.msg` 操作才会触发`set`, 简化为 `app.msg` 就可以触发`set`) |
| 191 | + |
| 192 | + |
| 193 | + |
| 194 | + |
| 195 | +#### observe()方法做了哪些事? |
| 196 | + |
| 197 | +既然已经说了响应式核心的一个方法就是这个`observe()`,那么先让我们来看看它会做哪些事呢? |
| 198 | + |
| 199 | +首先让我们从输入,输出的角度来分析: |
| 200 | + |
| 201 | +```javascript |
| 202 | +// 输入:data 数据对象,也就是我们平常定义的: |
| 203 | +data () { |
| 204 | + return { |
| 205 | + 'msg': 'lindaidai' |
| 206 | + } |
| 207 | +} |
| 208 | +// 我们把这个对象简化为 { 'msg': 'lindaidai' } |
| 209 | + |
| 210 | +// 输出:一个Observer类的对象,它大概长成这样: |
| 211 | +{ |
| 212 | + value: { |
| 213 | + msg: 'lindaidai', |
| 214 | + __ob__: {...} |
| 215 | + }, |
| 216 | + dep: '<Dep>类型的对象', |
| 217 | + vmCount: 0 |
| 218 | +} |
| 219 | +``` |
| 220 | + |
| 221 | +其次让我们对应着源码来看: |
| 222 | + |
| 223 | + |
| 224 | + |
| 225 | +可以很直观的看到,这个`observe()`方法也是可以拆成这三部分来看的: |
| 226 | + |
| 227 | +- 判断该对象中是否有`__ob__`这个属性且已经是一个`Observer`实例对象了 |
| 228 | +- 经过一系列的判断,创建一个`Observer`实例对象 |
| 229 | +- `asRootData`为`true`则`vmCount++` |
| 230 | + |
| 231 | +并最终将这个`ob`返回。 |
| 232 | + |
| 233 | +所以这里的`observe()`方法我把它认为是一个转化的中间人,它把原本的`data`变为了一个可观察的`Observer`实例对象,真正实现`依赖收集`的逻辑都在`Observer`这个类中。 |
| 234 | + |
| 235 | + |
| 236 | + |
| 237 | +#### Observer类是做什么的? |
| 238 | + |
| 239 | +OK👌,其实不用我多说,你们也应该知道,`Observer`的作用就是遍历对象的所有属性将其进行双向绑定。 |
| 240 | + |
| 241 | + |
| 242 | + |
| 243 | +当`new Watcher()`的时候,会将`Dep.target`指向这个`Watcher`实例 |
| 244 | + |
0 commit comments