├── LICENSE ├── README.md ├── code ├── ISSUE.md ├── build │ └── config.js ├── package.json └── src │ ├── compiler │ ├── codegen │ │ └── index.js │ ├── create-compiler.js │ ├── helper.js │ ├── index.js │ ├── optimizer.js │ ├── parser │ │ ├── html-parser.js │ │ ├── index.js │ │ └── text-parser.js │ └── to-function.js │ ├── core │ ├── index.js │ ├── instance │ │ ├── index.js │ │ ├── init.js │ │ ├── lifecycle.js │ │ ├── render.js │ │ └── state.js │ ├── observer │ │ ├── array.js │ │ ├── dep.js │ │ ├── index.js │ │ │ ├── export-class-Observer.js │ │ │ ├── export-function-defineReactive.js │ │ │ └── export-function-observe.js │ │ ├── scheduler.js │ │ └── watcher.js │ └── vdom │ │ ├── create-element.js │ │ ├── patch.js │ │ └── vnode.js │ └── platforms │ └── web │ ├── compiler │ └── index.js │ ├── entry-runtime-with-compiler.js │ └── runtime │ ├── index.js │ └── patch.js └── test ├── demo1.html ├── demo2.html ├── htmlparser ├── demo.html └── simplehtmlparser.js └── vue-2.4.0.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 王福朋 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 快速了解 Vue2 MVVM 2 | 3 | vue2 除了 MVVM 之外,组件化和 SSR 都是很重要的部分,但本文范围只针对 MVVM 。对 vue 不了解的同学可查阅 [vue 教程](https://cn.vuejs.org/v2/guide/) 。 4 | 5 | 另外,对于这种经典、复杂框架的学习和源码阅读,我也就不求甚解了,因为甚解的成本太高了。因此,能通过最短的时间学习了解大概的流程,得到我想要的就可以,毕竟我也不会去维护 vue 的源码。2/8 原则,会让你做事更加有效率。 6 | 7 | ------- 8 | 9 | ## 目录 10 | 11 | - [什么是-mvvm](#什么是-mvvm) 12 | - [介绍](#介绍) 13 | - [带来的改变](#带来的改变) 14 | - [Vue2 MVVM 几大要素](#vue2-mvvm-几大要素) 15 | - [整体流程](#整体流程) 16 | - [关于精简后的源码](#关于精简后的源码) 17 | - [响应式](#响应式) 18 | - [objectdefineproperty](#objectdefineproperty) 19 | - [数组的变化如何监听](#数组的变化如何监听) 20 | - [给-model-分配一个-observer-实例](#给-model-分配一个-observer-实例) 21 | - [walk](#walk) 22 | - [definereactive](#definereactive) 23 | - [dep](#dep) 24 | - [整体流程](#整体流程-1) 25 | - [考虑递归](#考虑递归) 26 | - [接下来](#接下来) 27 | - [模板解析](#模板解析) 28 | - [模板和 html](#模板和-html) 29 | - [模板处理的三个步骤](#模板处理的三个步骤) 30 | - [生成 ast](#生成-ast) 31 | - [优化 ast 找到最大静态子树](#优化-ast-找到最大静态子树) 32 | - [生成 render 函数](#生成-render-函数) 33 | - [整理流程](#整理流程) 34 | - [和绑定依赖的关系](#和绑定依赖的关系) 35 | - [接下来](#接下来-1) 36 | - [虚拟 dom](#虚拟-dom) 37 | - [vdom 的基本使用](#vdom-的基本使用) 38 | - [render 函数生成 vnode](#render-函数生成-vnode) 39 | - [watcher](#watcher) 40 | - [触发通知](#触发通知) 41 | - [整体流程](#整体流程-2) 42 | - [如何阅读简化后的源码](#如何阅读简化后的源码) 43 | - [最后](#最后) 44 | - [参考链接](#参考链接) 45 | - [关于作者](#关于作者) 46 | 47 | ------ 48 | 49 | ## 什么是 MVVM 50 | 51 | 先来说说我对 MVVM 的理解 52 | 53 | ### 介绍 54 | 55 | MVVM 拆解开来就是 Model View ViewModel ,对设计模式多少了解一些的读者,应该知道 Model View 是什么,关键在于 ViewModel。我在最开始接触 vue 并试图理解 MVVM 的时候,是从下图开始的 56 | 57 | ![](https://user-images.githubusercontent.com/9583120/32172846-0a520f48-bd4b-11e7-9e2b-1ebdcb293387.jpeg) 58 | 59 | 当然,还得再结合一个简单的 vue 的代码示例 60 | 61 | ```html 62 | 63 |
64 |

{{price}} 元

65 | 66 |
67 | 68 | 85 | ``` 86 | 87 | **View** 即视图,我觉得更好的解释是“模板”,就是代码中`
...
`的内容,用于显示信息以及交互事件的绑定,写过 html 的都明白。 88 | 89 | **Model** 即模型(或数据),跟 MVC 中的 Model 一样,就是想要显示到模型上的数据,也是我们需要在程序生命周期中可能需要更新的数据。View 和 Model 分开,两者无需相互关心,相比于 jquery 时代,这已经是设计上的一个巨大进步。 90 | 91 | 两者分开之后得通过 **ViewModel** 连接起来,`el: '#app'`牵着 View ,`data: model`牵着 Model ,还有一个`methods`(其实不仅仅是`methods`,还有其他配置)充当 controller 的角色,可以修改 Model 的值。 92 | 93 | ### 带来的改变 94 | 95 | 如果用 jquery 实现上述功能,那肯定得将 click 事件绑定到 DOM 上,数据的更新会直接修改 DOM —— 总之吧,什么事儿都得操作 DOM 。这样,我们就会将 View Model Controller 完全耦合在一起,很容易搞成意大利面一样的程序。 96 | 97 | 完全分开之后,click 事件直接关联到 ViewModel 中,事件直接修改 Model ,然后由框架自动去完成 View 的更新。相比于手动操作 DOM ,**你可以通过修改 Model 去控制 View 更新,这样在降低耦合的同时,也更加符合人的逻辑习惯**,简直让人欲罢不能。 98 | 99 | 从 ng react vue 以及其他 MVVM 框架,基本已经用于全球各种 web 系统的开发中,而且是很快推广普及,可见开发人员对它的认可 —— 这也是读者学习 MVVM 的必要性! 100 | 101 | ------- 102 | 103 | ## Vue2 MVVM 几大要素 104 | 105 | 要实现一个基本的 MVVM ,至少需要以下要素: 106 | 107 | **响应式** 108 | 109 | MVVM 的核心在于 View 和 Model 分离之后的数据绑定,即每次修改 Model 中的数据,View 都能随时得到更新,而不是手动去修改 DOM 。这就需要有一套完善的响应机制,其实就是一种观察者模式。在初始化页面的时候,监听 Model 中的数据,一旦有变化,立即触发通知,更新 View 。 110 | 111 | **模板引擎** 112 | 113 | 模板语法上就是一段 html 代码片段,但是却有好多 vue 定义的指令(directive),例如上文代码中的`{{price}}` `v-on`,还有常用的如`v-model` `v-bind` `v-if` `v-for`等。纯 html 代码的特点是静态,而加上这些指令之后,该模板就不再是静态的,**而是动态、有逻辑的模板**。 114 | 115 | 自然,模板本身肯定处理不了逻辑,必须借助 JS 才能处理逻辑。那如何把模板放在 JS 中呢 ———— 模板引擎。模板引擎会把一个 html 片段最终解析成一个 JS 函数,让它真正动起来。 116 | 117 | **虚拟 DOM** 118 | 119 | Model 中的数据一旦有变化,就会重新渲染 View ,但是变化也是有范围的。如果 Model 和 View 都比较复杂,而 Model 中的只有一点点数据的变化,就导致了 View 的全部渲染,可显然不合适,性能上也不允许。 120 | 121 | 如果是直接去操作 DOM 修改 View 就很难做到性能的极致,而 vdom 就能做到。ViewModel 不会直接操作 DOM 而是把所有对 DOM 的操作都一股脑塞给 vdom ,vdom 进行 diff 之后,再决定要真正修改哪些 DOM 节点。 122 | 123 | ------- 124 | 125 | ## 整体流程 126 | 127 | ![](https://user-images.githubusercontent.com/9583120/31386983-622b714a-ad8e-11e7-97c7-02204e7a388f.png) 128 | 129 | 以上是一个完成流程的概述,介绍 Vue MVVM 把这张图放出来是最合适的。图的上部,`compiler` `PARSER` `CODEGEN` 表示的是上文中说的**模板引擎**。图的下部,`Observer` `Dep` `Watcher` 表示的是上文说的**响应式**。vdom 没有在途中表述出来,它其实是隐藏在`render`里面了。 130 | 131 | 还未开始详细介绍 MVVM 之前,该图也不用太关心细节,知道一个大概即可,待介绍完之后,还会再来回顾这幅图。另外,在阅读下文章节的时候,读者也可以经常来回顾一下这幅图,相信随着你看的进展,这幅图会慢慢理解。 132 | 133 | ---- 134 | 135 | ## 关于精简后的源码 136 | 137 | 进入 [这里](./code) 查看精简之后的 Vue 源码,根据 v2.4.2 版本,下文在讲述 MVVM 的时候会经常用到这份精简的源码。精简的依据有两种: 138 | 139 | - 忽略了 MVVM 之外(如组件、ssr、weex、全局 API 等)的代码 140 | - 只关注 MVVM 的核心功能,其他增加易用性的功能(watch computed 等)忽略 141 | 142 | 阅读这份代码,可以从`./code/src/platforms/web/entry-runtime-with-compiler.js`这个入口开始。下文会先介绍 MVVM 的实现逻辑,不会涉及太多源码,文章最后再结合之前的内容来介绍阅读源码的流程(我觉得这样更加合理)。 143 | 144 | 注意,这份代码只供阅读,不能执行。 145 | 146 | ------- 147 | 148 | ## 响应式 149 | 150 | ### Object.defineProperty 151 | 152 | > `Object.defineProperty`是 ES5 中新增的一个 API 目前(2017.11)看来,浏览器兼容性已经不是问题,特别是针对移动端。 153 | 154 | 对于一个简单的 JS 对象,每当获取属性、重新赋值属性的时候,都要能够监听到(至于为何有这种需求,先不要管)。以下方式是无法满足需求的 155 | 156 | ```js 157 | var obj = { 158 | name: 'zhangsan', 159 | age: 25 160 | } 161 | console.log(obj.name) // 获取属性的时候,如何监听到? 162 | obj.age = 26 // 赋值属性的时候,如何监听到? 163 | ``` 164 | 165 | 借用`Object.defineProperty`就可以实现这种需求 166 | 167 | ```js 168 | var obj = {} 169 | var name = 'zhangsan' 170 | Object.defineProperty(obj, "name", { 171 | get: function () { 172 | console.log('get') 173 | return name 174 | }, 175 | set: function (newVal) { 176 | console.log('set') 177 | name = newVal 178 | } 179 | }); 180 | 181 | console.log(obj.name) // 可以监听到 182 | obj.name = 'lisi' // 可以监听到 183 | ``` 184 | 185 | 回想一下 Vue 的基本使用,修改 model 的值之后,view 会立刻被修改,这个逻辑用到的核心 API 就是`Object.defineProperty`。当你要向别人介绍 Vue MVVM 的内部实现,第一个提到的 API 也应该是`Object.defineProperty`。 186 | 187 | ### 数组的变化如何监听? 188 | 189 | 针对 JS 对象可以使用`Object.defineProperty`来做监控,但是针对数组元素的改变,却用不了,例如数组的`push` `pop`等。 190 | 191 | Vue 解决这个问题的办法也比较简单粗暴,直接将需要监听的数组的原型修改了。**注意,并不是将`Array.prototype`中的方法改了,那样会造成全局污染,后果严重**。看如下例子: 192 | 193 | ```js 194 | var arr1 = [1, 2, 3] 195 | var arr2 = [100, 200, 300] 196 | arr1.__proto__ = { 197 | push: function (val) { 198 | console.log('push', val) 199 | return Array.prototype.push.call(arr1, val) 200 | }, 201 | pop: function () { 202 | console.log('pop') 203 | return Array.prototype.pop.call(arr1) 204 | } 205 | // 其他原型方法暂时省略。。。。 206 | } 207 | 208 | arr1.push(4) // 可监听到 209 | arr1.pop() // 可监听到 210 | arr2.push(400) // 不受影响 211 | arr2.pop() // 不受影响 212 | ``` 213 | 214 | Vue 实现的时候会考虑更加全面,不会这么简单粗暴的赋值,但是基本原理都是这样的。如果读者不是特别抠细节的话,了解到这里就 OK 了。 215 | 216 | ### 给 model 分配一个 Observer 实例 217 | 218 | 以上介绍了针对 JS 对象和数组,监听数据变化的技术方案。接下来通过一个简单的例子,看一下 Vue 如何做对数据进行监控。先定义一个简单的 Vue 使用示例。 219 | 220 | ```js 221 | var vm = new Vue({ 222 | data: { 223 | price: 100 224 | } 225 | }) 226 | ``` 227 | 228 | 拿到`data`之后(这里的`data`其实就是 Model ),先赋值一个`__ob__`属性,值是`Observer`的实例,即`data.__ob__ = new Observer(data)`。首先看一下`Observer`函数的定义,为了让读者第一时间看明白原理和流程,把 Vue 源码进行了极致的简化(甚至把数组的处理都去掉了)。 229 | 230 | ```js 231 | export class Observer { 232 | value: any; 233 | dep: Dep; 234 | 235 | constructor (value: any) { 236 | this.value = value 237 | this.dep = new Dep() 238 | this.walk(value) 239 | } 240 | 241 | walk (obj: Object) { 242 | const keys = Object.keys(obj) 243 | for (let i = 0; i < keys.length; i++) { 244 | defineReactive(obj, keys[i], obj[keys[i]]) 245 | } 246 | } 247 | } 248 | ``` 249 | 250 | 有两个属性和一个方法。`value`属性就指向了`data`本身,`dep`属性指向一个新创建的`Dep`的实例(`Dep`我们后面再说),还有一个`walk`方法。 251 | 252 | ### walk 253 | 254 | `walk`方法一看就明白,遍历`data`的所有属性,然后执行`defineReactive(data, key, value)`方法。其实,就是将要执行上文介绍的`Object.defineProperty`来绑定监听。 255 | 256 | 看到这里,就明白 Vue 是对初始化时 Model 中已有的数据进行监听。如果初始化完成,再去手动扩展 Model 的属性,新扩展的就无法进行监听了,处分使用 Vue 提供的特有的扩展 API 。 257 | 258 | > PS:其实 JS 对象才会走`walk`方法,数组会走另一个方法(上文说过对象和数组的监听方式不一样,因此原因)。但是为了理解简单,就先不管数组了,不影响继续往前赶。 259 | 260 | ### defineReactive 261 | 262 | 按照之前的思路 263 | 264 | ```js 265 | var vm = new Vue({ 266 | data: { 267 | price: 100 268 | } 269 | }) 270 | ``` 271 | 272 | 执行这个方法时的参数应该是这样的`defineReactive(data, key, value)`,`key`和`value`即是`data`的属性和属性值,虽然这里只有一个`price`属性。`defineReactive`的内部实现可以简化为: 273 | 274 | ```js 275 | export function defineReactive ( 276 | obj: Object, 277 | key: string, 278 | val: any 279 | ) { 280 | // 每次执行 defineReactive 都会创建一个 dep ,它会一直存在于闭包中 281 | const dep = new Dep() 282 | 283 | Object.defineProperty(obj, key, { 284 | get: function reactiveGetter () { 285 | if (Dep.target) { 286 | dep.depend() 287 | } 288 | return val 289 | }, 290 | set: function reactiveSetter (newVal) { 291 | val = newVal 292 | dep.notify() 293 | } 294 | }) 295 | } 296 | ``` 297 | 298 | 代码跟之前介绍`Object.defineProperty`的时候很相似,区别在于函数中有一个`const dep = new Dep()`(第二次遇到了`Dep`)。而且,在`get`中执行`dep.depend()`,在`set`中触发`dep.notify()`。 299 | 300 | ### Dep 301 | 302 | 至于`Dep`是什么,现在无需详细关注,因为它还依赖于另外一个函数,因此现在讲不明白。但是,读者如果有设计模式的了解,再加上你对 Vue 的了解,应该能猜到写什么。 303 | 304 | 是的,`dep.depend()`就是**绑定依赖**,`dep.notify()`就是**触发通知**,标准的观察者模式。而且,还有知道,是在`get`的时候绑定依赖,在`set`的时候触发通知。在`set`时候触发通知这个很容易理解,要不然修改 Model ,View 怎么会更新呢。那么为何在`get`时候绑定依赖?—— 你不绑定,怎么知道触发什么通知?**而且更重要的,只有`get`过的属性才会绑定依赖,未被`get`过的属性就忽略不管**。这样就保证了只有和 View 有关联的 Model 中的属性才会被绑定依赖关系,这一点很重要。 305 | 306 | ### 整体流程 307 | 308 | ![](https://camo.githubusercontent.com/b518a0ca8d7a5d7d6f6198b8184b84201c4ab5ab/687474703a2f2f696d61676573323031372e636e626c6f67732e636f6d2f626c6f672f3133383031322f3230313731312f3133383031322d32303137313130353137313935383230312d323031333336323535362e706e67) 309 | 310 | 参照文章一开始的整体流程图,响应式这部分讲解的就是红框标出的区域。简单说来: 311 | 312 | - 就是拿到`data`,创建`Observer`实例 313 | - 创建过程中会使用`walk`遍历(先不考虑数组的情况)所有`data`的属性 314 | - 针对`data`每一个属性都执行`defineReactive(data, key, value)`(即使用`Object.defineProperty`监听`get`和`set`) 315 | - `get`的时候会绑定依赖,`set`的时候会触发通知,即观察者模式(至于如何绑定依赖、如何触发通知,下文会讲解)。 316 | 317 | ### 考虑递归 318 | 319 | 以上讲解为了便于快速理解都是拿结构很简单的 Model 做示例,即`data`的属性都是值类型,而不是对象。 320 | 321 | ```js 322 | var vm = new Vue({ 323 | data: { 324 | price: 100 325 | } 326 | }) 327 | ``` 328 | 329 | 如果`data`的属性中再有对象或者数组,层级结构变得很复杂,如 330 | 331 | ```js 332 | var vm = new Vue({ 333 | data: { 334 | list: [ 335 | {x: 100}, 336 | {y: 200}, 337 | {z: 300} 338 | ], 339 | info: { 340 | a: 'a', 341 | b: { 342 | c: 'c' 343 | } 344 | } 345 | } 346 | }) 347 | ``` 348 | 349 | 这种复杂的情况必须通过**递归**来解决。如果读者对上面简单的示例都理解了,外加你有良好的编程能力(熟悉递归),那这个过程应该不难理解(虽然逻辑上比较绕)—— 关键是要有对递归的熟练掌握。 350 | 351 | > PS:为何要针对`data`创建`Observer`实例,而不是直接对`data`执行`walk`?是因为`Observer`实例中有一个`dep`属性,在递归时候会被用到。展开来讲非常绕(递归本来也不适合用语言去描述),自己去代码中找吧,在`defineReactive`函数中搜索`childOb`就能找到相关信息。 352 | 353 | ### 接下来 354 | 355 | 最后留了两个未完待续的点:第一,`get`中绑定依赖;第二,`set`中触发通知。针对这个简单的示例: 356 | 357 | ```js 358 | var vm = new Vue({ 359 | el: '#app', 360 | data: model, 361 | methods: { 362 | addHandle: function () { 363 | this.price++ 364 | } 365 | } 366 | }) 367 | ``` 368 | 369 | 第二点的入口能轻易找到,即`this.price++`,修改`data`属性会触发`set`,然后触发通知,更新 View(虽然现在还不清楚如何更新 View,别急)。但是针对第一点,`get`到`price`的地方肯定是模板(即`

{{price}} 元

`)中显示的时候,接下来就讲一下如何处理模板、如何`get`到属性。看看 Vue 强大的模板引擎是如何工作的,这也是 Vue2 相比于 Vue1 升级的重点之一。 370 | 371 | 最后,也可参考 Vue 教程中的 [深入理解响应式](https://cn.vuejs.org/v2/guide/reactivity.html) ,不过这里面还包含了一些当前未讲到的内容,下文会继续讲解。 372 | 373 | ------- 374 | 375 | ## 模板解析 376 | 377 | 模板解析这部分逻辑比较复杂,也借用了一些其他工具,而且是我们平时工作中不怎么使用的。但是它是理解 MVVM 的关键,我尽量简单描述,读者还是应该坚持看完。 378 | 379 | ### 模板和 html 380 | 381 | ```html 382 |
383 |

{{price}} 元

384 |
385 | ``` 386 | 387 | 这是一个最简单的模板,看似是一个 html 直接可以在浏览器中运行,但是它不是。因为 html 不认识`{{price}}`,也不认识`v-on` `v-if` `v-for`等。对于 Vue 来说,**模板就是模板,就是一段非结构化的 JS 字符串,不是 html 代码**。模板到 html 还要经历好几层,第一步要结构化解析,第二步要生成 vdom ,第三步再渲染为页面。“模板解析”这部分,只关注前两步。 388 | 389 | ### 模板处理的三个步骤 390 | 391 | 参考源码 [./code/src/compiler/index.js](./code/src/compiler/index.js) 中的关键代码,这就是 Vue 处理模板的三个步骤。 392 | 393 | ```js 394 | // 传入一个函数作为参数 395 | function baseCompile ( 396 | template: string, 397 | options: CompilerOptions 398 | ): CompiledResult { 399 | // parse 定义于 ./parser/index 400 | // 将 html 转换为 ast (抽象语法树) 401 | const ast = parse(template.trim(), options) 402 | // optimize 定义于 ./optimizer 403 | // 标记 ast 中的静态节点,即不包含变量无需改变的节点 404 | optimize(ast, options) 405 | // generate 定义于 ./codegen/index 406 | // 返回 {render: '..render函数体..(字符串)', staticRenderFns: Array} 407 | const code = generate(ast, options) 408 | 409 | // 返回 CompiledResult 格式 410 | return { 411 | ast, 412 | render: code.render, 413 | staticRenderFns: code.staticRenderFns 414 | } 415 | } 416 | ``` 417 | 418 | 下面先简单介绍一下每一步的作用,看不明白可以直接往下看,下面有更加详细的介绍。 419 | 420 | 第一步,是将模板处理为 AST,即抽象语法树(Abstract Syntax Tree)。正如浏览器将 HTML 代码处理为结构化的 DOM 树一样,Vue 将模板处理为结构化的 AST ,每个节点都包含了标签、属性以及指令和指令类型,如`v-for` `v-text`等。这个过程,只要读者能理解“将非结构化的字符串处理成结构化的 JSON 数据”,就能理解该过程的用意。 421 | 422 | 第二步,优化 AST ,找到最大静态子树。这一步就是要分清楚,在 AST 当中,哪些是动态的,哪些是静态的。其实,通过判断该节点以及其子节点有没有关联指令就可以判断。指令中绑定了 Model 的数据,Model 一更新,动态的节点肯定要随时更新。而静态节点和 Model 都没有关系,因此只更新一次即可。总之,第二步是为了提高后面更新 View 的效率。 423 | 424 | 第三步,生成 render 函数,将模板字符串转换为 JS 真正可执行的函数。可直接看下文的详细介绍。 425 | 426 | ### 生成 AST 427 | 428 | ```html 429 |
430 |

{{price}} 元

431 |
432 | ``` 433 | 434 | 上面是一个最简单的模板,它经过 Vue 的处理并生成的 AST 简化之后大概这样 435 | 436 | ```js 437 | { 438 | type: 1, // 对应 nodeType 439 | tag: 'div', 440 | attrs : {id: 'app'}, 441 | children: [ 442 | { 443 | type: 1, 444 | tag: 'p', 445 | attrs: {}, 446 | children: [ 447 | { 448 | type: 2, // text 类型 449 | text: '{{price}} 元', 450 | expression: '_s(price)+" 元"' 451 | } 452 | ] 453 | } 454 | ] 455 | } 456 | ``` 457 | 458 | 模板和 AST 的对应关系应该不用多说了,有 JS DOM 操作基础的读者应该都能看得懂。重点关注一下 AST 中的`expression: '_s(price)+" 元"'`,对应的是原模板中的`{{price}}`。从表达式的形式可以猜测到,`_s(price)`就是一个函数。但是现在还处于一个字符串中,字符串如何当做函数来执行?—— 下文会有介绍,但是也可以提前想想`new Function(...)`怎么使用。 459 | 460 | 这里仅仅是拿`{{price}}`这样最简单的一个表达式指令做例子。不同的指令,在 AST 中会有不同的形式来表示。读者可以自己运行一个 Vue 的 demo 然后在 Vue.js 源码中搜索`var ast = parse(template.trim(), options);`,然后再下一行加`console.log(ast)`自己去看。总结一下,Vue 将模板字符串转化为 AST 主要有两个目的,最终目的还是让 JS 代码能读懂这个模板: 461 | 462 | - 将非结构化的字符串转化为结构化的对象 463 | - 将 html 和 JS 都不能直接识别的指令,转化为 JS 能识别的表达式形式 464 | 465 | > PS:人和计算机是一对矛盾,人易读懂的(如模板、指令)计算机却不易不懂,计算机易读懂的(表达式、复杂的函数体)人却很难读懂。这个矛盾一直存在着,人也一直妥协着,因为计算式傻笨傻笨的不懂得像人妥协。 466 | 467 | 接着来说一下 Vue 是如何将模板字符串解析成 AST 结构的。 468 | 469 | 假如我们现在要做一个简单的搜索引擎或者网络爬虫,从成千上网的网页上抓取各种页面信息,那么最终抓取到的结构是什么呢?—— 是一个一个的 html 文件。接下来该如何分析这些 html 文件呢(或者是 html 字符串),肯定是先得将这坨字符串进行结构化。这种场景,想来大家也能猜到,业界肯定已经早就有了现成的工具去做这件事,可以上网搜一下`htmlparser`。 470 | 471 | Vue 就参考了 htmlparser 去解析模板,源码参见 [./code/src/compiler/parser/html-parser.js](./code/src/compiler/parser/html-parser.js) 。我自己总结的简化版源码中,将该代码省略了。因为它太复杂,不易于阅读,我找打了一个更加简单的理解方式 simplehtmlparser.js —— 为了投机取巧也不容易。 472 | 473 | ```html 474 | 475 | 499 | ``` 500 | 501 | 以上是使用 simplehtmlparser.js 的例子,源码在 [./test/htmlparser/demo.html](./test/htmlparser/demo.html) 中。从这个例子的使用,基本就能看出 Vue 使用 htmlparser 的过程,想了解 htmlparser 的内部逻辑,也可参考 [./test/htmlparser/simplehtmlparser.html](./test/htmlparser/simplehtmlparser.html) ,只有 100 行代码,非常简单易懂。 502 | 503 | 从 demo 中看,htmlparser 接收到模板字符串,然后能分析出每个 tag 的开始、结束,tag 的类型和属性。有了这些,我们就能将一个模板字符串生成一个 AST 。具体的过程,大家可以参考源码 [./code/src/compiler/parser/index.js](./code/src/compiler/parser/index.js) 504 | 505 | ### 优化 AST 找到最大静态子树 506 | 507 | ```html 508 |
509 |

{{price}} 元

510 |
511 | ``` 512 | 513 | 这个模板生成 AST ,是没有静态节点的,因为都是`{{price}}`的父节点,`price`一变都得跟着受牵连。但是下面这个模板就会有静态节点 514 | 515 | ```html 516 |
517 |
518 |

白日依山尽

519 |

黄河入海流

520 |
521 |

{{price}} 元

522 |
523 | ``` 524 | 525 | 和 Model 数据(即`price`)相关的都是动态节点,其他无关的都是静态节点。因此,这些模板对应的节点都是静态的: 526 | 527 | ```html 528 |
529 |

白日依山尽

530 |

黄河入海流

531 |
532 | ``` 533 | 534 | 最后,根据节点的父子关系,要找出最外层的静态节点,因为只要该节点确认为静态,其子节点都无需关心,肯定也是静态的。这个最外层的静态节点就是`
`,即标题中提到的“最大静态子树”。那么,该模板最终生成的 AST 简化之后是这样的: 535 | 536 | ```js 537 | { 538 | type: 1, 539 | tag: 'div', 540 | attrs: {id: 'app'}, 541 | children: [ 542 | { 543 | type: 1, 544 | tag: 'div', 545 | attrs: {id: 'static-div'}, 546 | children: [ 547 | // 省略两个静态的 p 节点 548 | ], 549 | static: true 550 | }, 551 | { 552 | // 省略

{{price}} 元

节点 553 | } 554 | ], 555 | static: false 556 | } 557 | ``` 558 | 559 | 在知道如何标注出静态节点的最后,还是要再次强调一下标注最大静态子树的用意:根据 MVVM 的交互方式,Model 的数据修改要同时修改 View ,那些 View 中和 Model 没有关系的部分,就没必要随着 Model 的变化而修改了,因此要将其标注为静态节点。最终目的就是要更加更新 View 的效率。 560 | 561 | ### 生成 render 函数 562 | 563 | ```html 564 |
565 |

{{price}} 元

566 |
567 | ``` 568 | 569 | 以上模板中的`price`完全可以通过`vm.price`获取,这个没问题。然后还需要定义一个函数`_c`,这个函数用于创建一个 node(不管是 elem 还是 vnode),再定义一个函数`_t`,用于创建文本标签。 570 | 571 | ```js 572 | function _c(tag, attrs, children) { 573 | // 省略函数体 574 | } 575 | function _t(text) { 576 | // 省略函数体 577 | } 578 | ``` 579 | 580 | 然后,根据模板的内容生成如下字符串,注意字符串中的`_c` `_t`和`this.price` 581 | 582 | ```js 583 | var code = '_c("div", {id: "app"}, [_c("p", {}, [_t(this.price)])])' 584 | ``` 585 | 586 | 再然后,就可以通过`var render = new Function(code)`来生成一个函数,最终生成的函数其实就是这样一个格式: 587 | 588 | ```js 589 | render = function () { 590 | _c('div', {id: 'app'}, [ 591 | _c('p', {}, [ 592 | _t(this.prirce + ' 元') 593 | ]) 594 | ]) 595 | } 596 | ``` 597 | 598 | 看以上代码的函数和最初的模板,是不是很相似?他们的区别在于:模板是 html 格式,但是 html 不会识别`{{price}}`;而函数完全是 JS 代码,`this.price`放在 JS 中完全可以被运行。 599 | 600 | 以上还是以一个最简单的模板为例,模板如果变的复杂,需要注意两方面: 601 | 602 | - 不同的指令,生成 render 函数会不一样。指令越多,render 函数会变得越复杂。 603 | - 如果有静态的节点,将不会在 render 函数中体现,而是在`staticRenderFns`中体现。静态节点只会在初次显示 View 的时候被执行,后续的 Model 变化将不会再触发静态节点的渲染。 604 | 605 | ### 整理流程 606 | 607 | ![](https://user-images.githubusercontent.com/9583120/32430235-03ac6c58-c294-11e7-9acb-1067da0fb730.png) 608 | 609 | 参考文章一开始给出的流程图,模板渲染就是图中红框标出那部分。简单看来就是 3 部分: 610 | 611 | - 根据模板生成 AST 612 | - 优化 AST 找出其静态,不依赖 Model 改变而改变的部分 613 | - 根据优化后的 AST 生成 render 函数 614 | 615 | ### 和绑定依赖的关系 616 | 617 | 回顾文章一开始介绍响应式的那部分,通过`Object.defineProperty`的设置,在 Model 属性`get`的时候会被绑定依赖。现在看一下 render 函数,在 render 函数执行的时候,肯定会触发`get`,从而绑定依赖。 618 | 619 | 因此,到这里,绑定依赖的逻辑,就首先清楚了。 620 | 621 | ### 接下来 622 | 623 | 该部分最后生成是 render 函数,那么 render 函数何时被执行,以及执行的时候生成了什么,下文将要介绍。 624 | 625 | ------- 626 | 627 | ## 虚拟 DOM 628 | 629 | 这部分是围绕着 vdom 来介绍 View 的渲染,包括 View 的渲染和更新、以及响应式如何触发这种更新机制。但是,不会深入讲解 vdom 内部的原理。Vue2 源码中的 vdom 也不是完全自己写的,而是将 [Snabbdom](https://github.com/snabbdom/snabbdom) 这一经典的 vdom 开源库集成进来的。想要深入学习 vdom 可参考: 630 | 631 | - 经典博客 [深度解析 vdom](https://github.com/livoras/blog/issues/13) 632 | - 《Vue.js 权威指南》一书中介绍 vdom 的章节,通过示例和图形的方式介绍的比较清晰 633 | - Snabbdom 的使用和实现,具体资源网上去搜 634 | 635 | ### vdom 的基本使用 636 | 637 | 浏览器中解析 html 会生成 DOM 树,它是由一个一个的 node 节点组成。同理,vdom 也是由一个一个的 vnode 组成。vdom 、 vnode 都是用 JS 对象的方式来模拟真实的 DOM 或者 node 。 638 | 639 | 参考 [Snabbdom](https://github.com/snabbdom/snabbdom) 的样例 640 | 641 | ```js 642 | // 获取容器元素 643 | var container = document.getElementById('container'); 644 | // 创造一个 vnode 645 | var vnode = h('div#container.two.classes', {on: {click: someFn}}, [ 646 | h('span', {style: {fontWeight: 'bold'}}, 'This is bold'), 647 | ' and this is just normal text', 648 | h('a', {props: {href: '/foo'}}, 'I\'ll take you places!') 649 | ]); 650 | // Patch into empty DOM element – this modifies the DOM as a side effect 651 | patch(container, vnode); 652 | ``` 653 | 654 | `h`函数可以生成一个 vnode ,然后通过`patch`函数将生成的 vnode 渲染到真实的 node(`container`)中。这其中涉及不到 vdom 的核心算法 —— diff 。继续追加几行代码: 655 | 656 | ```js 657 | var newVnode = h('div#container.two.classes', {on: {click: anotherEventHandler}}, [ 658 | h('span', {style: {fontWeight: 'normal', fontStyle: 'italic'}}, 'This is now italic type'), 659 | ' and this is still just normal text', 660 | h('a', {props: {href: '/bar'}}, 'I\'ll take you places!') 661 | ]); 662 | // Second `patch` invocation 663 | patch(vnode, newVnode); // Snabbdom efficiently updates the old view to the new state 664 | ``` 665 | 666 | 又生成了新的 vnode `newVnode` ,然后`patch(vnode, newVnode)`。这其中就会通过 diff 算法去对比`vnode`和`newVnode`,对比两者的区别,最后渲染真实 DOM 时候,只会将有区别的部分重新渲染。在浏览器中,DOM 的任何操作都是昂贵的,减少 DOM 的变动,就会提高性能。 667 | 668 | ### render 函数生成 vnode 669 | 670 | 以上示例中,vnode 是手动生成的,在 Vue 中 render 函数就会生成 vnode —— render 函数就是刚刚介绍过的。这样一串联,整个流程就很清晰了: 671 | 672 | - 解析模板最终生成 render 函数。 673 | - 初次渲染时,直接执行 render 函数(执行过程中,会触发 Model 属性的`get`从而绑定依赖)。render 函数会生成 vnode ,然后 patch 到真实的 DOM 中,完成 View 的渲染。 674 | - Model 属性发生变化时,触发通知,重新执行 render 函数,生成 newVnode ,然后`patch(vnode, newVnode)`,针对两者进行 diff 算法,最终将有区别的部分重新渲染。 675 | - Model 属性再次发生变化时,又会触发通知 …… 676 | 677 | 另外,还有一个重要信息。如果连续修改多个 Model 属性,那么会连续触发通知、重新渲染 View 吗?—— 肯定不会,**View 的渲染是异步的**。即,Vue 会一次性集合多个 Model 的变更,最后一次性渲染 View ,提高性能。 678 | 679 | 以上描述的触发 render 函数的过程,可以从源码 [./code/src/core/instance/lifecycle.js] 中`mountComponent`中找到。这一行最关键`vm._watcher = new Watcher(vm, updateComponent, noop)`,其中`updateComponent`就会触发执行 render 函数。 680 | 681 | 下面就重点介绍一下`new Watcher`是干嘛用的。 682 | 683 | ### Watcher 684 | 685 | Watch 的定义在 [./code/src/core/observer/watcher.js](./code/src/core/observer/watcher.js) 中,简化后的代码如下 686 | 687 | ```js 688 | import Dep, { pushTarget, popTarget } from './dep' 689 | 690 | export default class Watcher { 691 | constructor ( 692 | vm: Component, 693 | expOrFn: string | Function, 694 | cb: Function 695 | ) { 696 | // 传入的函数,赋值给 this.geter 697 | this.getter = expOrFn 698 | // 执行 get() 方法 699 | this.value = this.get() 700 | } 701 | 702 | get () { 703 | // 将 this (即 Wathcer 示例)给全局的 Dep.target 704 | pushTarget(this) 705 | let value 706 | const vm = this.vm 707 | try { 708 | // this.getter 即 new Watcher(...) 传入的第二个参数 expOrFn 709 | // 这一步,即顺便执行了 expOrFn 710 | value = this.getter.call(vm, vm) 711 | } catch (e) { 712 | // ... 713 | } finally { 714 | popTarget() 715 | } 716 | return value 717 | } 718 | 719 | // dep.subs[i].notify() 会执行到这里 720 | update () { 721 | // 异步处理 Watcher 722 | // queueWatcher 异步调用了 run() 方法,因为:如果一次性更新多个属性,无需每个都 update 一遍,异步就解决了这个问题 723 | queueWatcher(this) 724 | } 725 | 726 | run () { 727 | // 执行 get ,即执行 this.getter.call(vm, vm) ,即执行 expOrFn 728 | this.get() 729 | } 730 | } 731 | ``` 732 | 733 | 结合调用它的语句`new Watcher(vm, updateComponent, noop)`,将`updateComponent`复制给`this.getter`,然后代码会执行到`get`函数。 734 | 735 | 首先,`pushTarget(this)`将当前的 Wacther 实例赋值给了`Dep.target`。这里要联想到上文介绍响应式的时候的一段代码 736 | 737 | ```js 738 | Object.defineProperty(obj, key, { 739 | get: function reactiveGetter () { 740 | if (Dep.target) { 741 | dep.depend() 742 | } 743 | return val 744 | }, 745 | set: function reactiveSetter (newVal) { 746 | val = newVal 747 | dep.notify() 748 | } 749 | }) 750 | ``` 751 | 752 | 注意看上面代码中的`if (Dep.target) { dep.depend() }`。此前一直说“触发`get`时候绑定依赖”这句话,到现在终于可以有一个结论:绑定的依赖就是这个`new Watcher(...)`。 753 | 754 | 然后,执行了`this.getter.call(vm, vm)`,即将`updateComponent`执行了,触发了初次的渲染。函数的执行将真正触发`get`,然后绑定依赖。过程基本走通了。 755 | 756 | ### 触发通知 757 | 758 | 当 Model 属性修改时会触发到上面代码中的`set`函数,然后执行`dep.notify()`。继续看看这个`notify`函数的内容 759 | 760 | ```js 761 | export default class Dep { 762 | // 省略 N 行 763 | 764 | notify () { 765 | // stabilize the subscriber list first 766 | const subs = this.subs.slice() 767 | for (let i = 0, l = subs.length; i < l; i++) { 768 | // 在 defineReactive 的 setter 中触发 769 | // subs[i] 是一个 Watcher 实例 770 | subs[i].update() 771 | } 772 | } 773 | } 774 | ``` 775 | 776 | 这里的`subs`就存储着所有的`Watcher`实例,然后遍历去触发它们的`update`方法。找到上文粘贴出来的`Watcher`的源码,其中`update`这么定义的: 777 | 778 | ```js 779 | // dep.subs[i].notify() 会执行到这里 780 | update () { 781 | // 异步处理 Watcher 782 | // queueWatcher 异步调用了 run() 方法,因为:如果一次性更新多个属性,无需每个都 update 一遍,异步就解决了这个问题 783 | queueWatcher(this) 784 | } 785 | 786 | run () { 787 | // 执行 get ,即执行 this.getter.call(vm, vm) ,即执行 expOrFn 788 | this.get() 789 | } 790 | ``` 791 | 792 | 看一下代码的注释也基本就能明白了,`update`会异步调用`run`(为何是异步调用,上文也介绍过了)。然后`run`中执行的`this.get()`函数上文已经介绍过,会触发传进来的`updateComponent`函数,也就触发了 View 的更新。 793 | 794 | ### 整体流程 795 | 796 | 该部分虽然名字是“虚拟 DOM”,但是有一半儿介绍了响应式的内容。这也是没办法,Vue MVVM 的整体流程就是这么走的。因此,该部分要求读者明白两点内容: 797 | 798 | - vdom 的生成和 patch 799 | - 完整的响应式流程 800 | 801 | 阅读至此,先忽略掉一些细节,再看文章最开始的这张图,应该会和一开始看不一样吧?图中所画的流程,肯定都能看懂了。 802 | 803 | ![](https://user-images.githubusercontent.com/9583120/31386983-622b714a-ad8e-11e7-97c7-02204e7a388f.png) 804 | 805 | ----- 806 | 807 | ## 如何阅读简化后的源码 808 | 809 | 下面简单说一下如何阅读我整理出来的只针对 MVVM 的简化后的 Vue2 的源码 810 | 811 | - `code/package.json`中,`scripts`规定了打包的各种命令,只看`dev`就好了 812 | - 通过`dev`打包命令,可以对应到`code/build/config.js`,并对应到`web-full-dev`的配置,最终对应到`./code/src/platforms/web/entry-runtime-with-compiler.js` 813 | - 然后剩下的代码,就是可以通过 JS 模块化规则找到。大的模块分为:`src/compiler`是模板编译相关,`src/core/instance`是 Vue 实例相关,`src/core/observer`是响应式相关,`src/core/vdom`是 vdom 相关。 814 | 815 | vue2 源码结构非常清晰,读者如果自己做框架和工具的话,可以参考这个经典框架的源码。 816 | 817 | ----- 818 | 819 | ## 最后 820 | 821 | MVVM 涉及的内容、代码都很多,虽然是快速了解,但是篇幅也很大。外加自己也是现学现卖,难免有些杂乱,读者如有问题或者建议,欢迎给我 [提交 issue](https://github.com/wangfupeng1988/learn-vue2-mvvm/issues) 。 822 | 823 | ------- 824 | 825 | ## 参考链接 826 | 827 | - https://segmentfault.com/a/1190000004346467 828 | - http://www.cnblogs.com/libin-1/p/6845669.html 829 | - https://github.com/xiaofuzi/deep-in-vue/blob/master/src/the-super-tiny-vue.js 830 | - https://github.com/KevinHu-1024/kevins-blog/issues/1 831 | - https://github.com/KevinHu-1024/kevins-blog/issues/5 832 | - https://github.com/liutao/vue2.0-source 833 | - http://www.jianshu.com/p/758da47bfdac 834 | - http://www.jianshu.com/p/bef1c1ee5a0e 835 | - http://www.jianshu.com/p/d3a15a1f94a0 836 | - https://github.com/luobotang/simply-vue 837 | - http://zhouweicsu.github.io/blog/2017/03/07/vue-2-0-reactivity/ 838 | - https://segmentfault.com/a/1190000007334535 839 | - https://segmentfault.com/a/1190000007484936 840 | - http://www.cnblogs.com/aaronjs/p/7274965.html 841 | - http://www.jackpu.com/-zhang-tu-bang-zhu-ni-xiao-hua-vue2-0de-yuan-ma/ 842 | - https://github.com/youngwind/blog/issues 843 | - https://www.zybuluo.com/zhouweicsu/note/729712 844 | - https://github.com/snabbdom/snabbdom 845 | - https://github.com/Matt-Esch/virtual-dom 846 | - https://gmiam.com/post/evo.html 847 | - https://github.com/livoras/blog/issues/13 848 | - https://calendar.perfplanet.com/2013/diff/ 849 | - https://github.com/georgebbbb/fakeVue 850 | - http://hcysun.me/2016/04/28/JavaScript%E5%AE%9E%E7%8E%B0MVVM%E4%B9%8B%E6%88%91%E5%B0%B1%E6%98%AF%E6%83%B3%E7%9B%91%E6%B5%8B%E4%B8%80%E4%B8%AA%E6%99%AE%E9%80%9A%E5%AF%B9%E8%B1%A1%E7%9A%84%E5%8F%98%E5%8C%96/ 851 | - https://github.com/answershuto/learnVue 852 | - https://github.com/xufei/blog/issues/10 853 | - https://cn.vuejs.org/v2/guide/reactivity.html 854 | - https://item.jd.com/12028224.html 855 | 856 | ## 关于作者 857 | 858 | - 关注作者的博客 - 《[深入理解javascript原型和闭包系列](http://www.cnblogs.com/wangfupeng1988/p/4001284.html)》《[深入理解javascript异步系列](https://github.com/wangfupeng1988/js-async-tutorial)》《[换个思路学习nodejs](https://github.com/wangfupeng1988/node-tutorial)》《[CSS知多少](http://www.cnblogs.com/wangfupeng1988/p/4325007.html)》 859 | - 学习作者的教程 - 《[前端JS高级面试](https://coding.imooc.com/class/190.html)》《[前端JS基础面试题](http://coding.imooc.com/class/115.html)》《[React.js模拟大众点评webapp](http://coding.imooc.com/class/99.html)》《[zepto设计与源码分析](http://www.imooc.com/learn/745)》《[json2.js源码解读](http://study.163.com/course/courseMain.htm?courseId=691008)》 860 | 861 | 如果你感觉有收获,欢迎给我打赏 ———— 以激励我更多输出优质开源内容 862 | 863 | ![](https://camo.githubusercontent.com/e1558b631931e0a1606c769a61f48770cc0ccb56/687474703a2f2f696d61676573323031352e636e626c6f67732e636f6d2f626c6f672f3133383031322f3230313730322f3133383031322d32303137303232383131323233373739382d313530373139363634332e706e67) -------------------------------------------------------------------------------- /code/ISSUE.md: -------------------------------------------------------------------------------- 1 | 2 | ## ISSUE 3 | 4 | - 生成 ast 时,各个 directive 如何处理,如(v-model v-if v-for 等) 5 | - 声明周期的流程串联 6 | - 渲染时,各个 directive 如何实现的 7 | - 异步渲染是如何实现的 8 | - 静态子树是如何渲染的,又是如何避免 re-render 时候不渲染的 9 | -------------------------------------------------------------------------------- /code/build/config.js: -------------------------------------------------------------------------------- 1 | const builds = { 2 | // Runtime+compiler development build (Browser) 3 | 'web-full-dev': { 4 | // 入口文件指向 platforms/web/entry-runtime-with-compiler.js 5 | entry: resolve('web/entry-runtime-with-compiler.js'), 6 | dest: resolve('dist/vue.js'), 7 | format: 'umd', 8 | env: 'development', 9 | alias: { he: './entity-decoder' }, 10 | banner 11 | } 12 | } 13 | 14 | if (process.env.TARGET) { 15 | module.exports = genConfig(builds[process.env.TARGET]) 16 | } else { 17 | exports.getBuild = name => genConfig(builds[name]) 18 | exports.getAllBuilds = () => Object.keys(builds).map(name => genConfig(builds[name])) 19 | } -------------------------------------------------------------------------------- /code/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue", 3 | "version": "2.4.2", 4 | "description": "Reactive, component-oriented view layer for modern web interfaces.", 5 | "main": "dist/vue.runtime.common.js", 6 | "module": "dist/vue.runtime.esm.js", 7 | "unpkg": "dist/vue.js", 8 | "typings": "types/index.d.ts", 9 | "files": [ 10 | "src", 11 | "dist/*.js", 12 | "types/*.d.ts" 13 | ], 14 | "scripts": { 15 | "dev": "rollup -w -c build/config.js --environment TARGET:web-full-dev" 16 | } 17 | } -------------------------------------------------------------------------------- /code/src/compiler/codegen/index.js: -------------------------------------------------------------------------------- 1 | 2 | export function generate ( 3 | ast: ASTElement | void, 4 | options: CompilerOptions 5 | ): CodegenResult { 6 | // 重点关注 state.staticRenderFns ,最大静态子树的渲染函数会放在其中 7 | const state = new CodegenState(options) 8 | // 根据 ast 生成的 render 函数体 9 | // genElement 定义于本文件中 10 | const code = ast ? genElement(ast, state) : '_c("div")' 11 | return { 12 | render: `with(this){return ${code}}`, 13 | staticRenderFns: state.staticRenderFns 14 | } 15 | } 16 | 17 | // 生成 render 函数体的代码 18 | export function genElement (el: ASTElement, state: CodegenState): string { 19 | if (el.staticRoot && !el.staticProcessed) { 20 | return genStatic(el, state) 21 | } else if (el.once && !el.onceProcessed) { 22 | return genOnce(el, state) 23 | } else if (el.for && !el.forProcessed) { 24 | return genFor(el, state) 25 | } else if (el.if && !el.ifProcessed) { 26 | return genIf(el, state) 27 | } else { 28 | // data 是一个 stirng 化 JSON 格式,包含当前节点的所有信息 29 | const data = genData(el, state) 30 | // 返回一个数组的 string 形式,子元素的信息 31 | const children = genChildren(el, state, true) 32 | const code = `_c('${el.tag}'${ // _c 即 createElement 33 | data ? `,${data}` : '' // data 34 | }${ 35 | children ? `,${children}` : '' // children 36 | })` 37 | return code 38 | } 39 | } 40 | 41 | // hoist static sub-trees out 42 | function genStatic (el: ASTElement, state: CodegenState): string { 43 | el.staticProcessed = true 44 | // 用上了 state.staticRenderFns 45 | state.staticRenderFns.push(`with(this){return ${genElement(el, state)}}`) 46 | // 返回 '_m(n)' n 即是 staticRenderFns 元素的 index 47 | return `_m(${state.staticRenderFns.length - 1})` // _m 即 renderStatic 48 | } 49 | 50 | // v-once 51 | function genOnce (el: ASTElement, state: CodegenState): string { 52 | el.onceProcessed = true 53 | // 注意 el.onceProcessed = true 之后,执行到 genStatic 54 | // 会在继续执行到 genElement ,此时 onceProcessed = true 就能起到作用 55 | return genStatic(el, state) 56 | } 57 | 58 | // v-for 59 | export function genFor ( 60 | el: any, 61 | state: CodegenState 62 | ): string { 63 | const exp = el.for 64 | const alias = el.alias 65 | const iterator1 = el.iterator1 ? `,${el.iterator1}` : '' 66 | const iterator2 = el.iterator2 ? `,${el.iterator2}` : '' 67 | 68 | el.forProcessed = true // avoid recursion 69 | return `${'_l'}((${exp}),` + /* _l 即 renderList */ 70 | `function(${alias}${iterator1}${iterator2}){` + 71 | `return ${genElement(el, state)}` + 72 | '})' 73 | } 74 | 75 | export function genIf ( 76 | el: any, 77 | state: CodegenState, 78 | altGen?: Function, 79 | altEmpty?: string 80 | ): string { 81 | el.ifProcessed = true // avoid recursion 82 | // ????? 83 | return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty) 84 | } 85 | 86 | export function genData (el: ASTElement, state: CodegenState): string { 87 | let data = '{' 88 | 89 | // directives first. 90 | // directives may mutate the el's other properties before they are generated. 91 | const dirs = genDirectives(el, state) 92 | // dirs 格式如 'directives:[{name:"show",rawName:"v-show",value:(show),expression:"show"}, {...}]' 93 | if (dirs) data += dirs + ',' 94 | 95 | // key 96 | if (el.key) { 97 | data += `key:${el.key},` 98 | } 99 | 100 | // attributes 101 | if (el.attrs) { 102 | data += `attrs:{${genProps(el.attrs)}},` 103 | } 104 | 105 | // event handlers 106 | if (el.events) { 107 | data += `${genHandlers(el.events, false, state.warn)},` 108 | } 109 | 110 | // component v-model 111 | if (el.model) { 112 | data += `model:{value:${ 113 | el.model.value 114 | },callback:${ 115 | el.model.callback 116 | },expression:${ 117 | el.model.expression 118 | }},` 119 | } 120 | 121 | data = data.replace(/,$/, '') + '}' 122 | // 最终 data 会是一个 stirng 化 JSON 格式,包含当前节点的所有信息 123 | return data 124 | } 125 | 126 | export function genChildren ( 127 | el: ASTElement, 128 | state: CodegenState 129 | ): string | void { 130 | const children = el.children 131 | if (children.length) { 132 | // optimize single v-for 133 | if (children.length === 1 && el.for) { 134 | // 再调用 genElement 135 | return genElement(el, state) 136 | } 137 | // 注意:针对 v-for 的情况,不能走正常的递归子节点的处理,需要特殊处理 138 | 139 | // 递归分析子节点 140 | return `[${children.map(c => genNode(c, state)).join(',')}]` 141 | } 142 | } 143 | 144 | function genNode (node: ASTNode, state: CodegenState): string { 145 | if (node.type === 1) { 146 | // 普通节点 147 | return genElement(node, state) 148 | } if (node.type === 3 && node.isComment) { 149 | // 注释 150 | return genComment(node) 151 | } else { 152 | // 文字 153 | return genText(node) 154 | } 155 | } 156 | 157 | export function genComment (comment: ASTText): string { 158 | return `_e(${JSON.stringify(comment.text)})` // _e 即 createEmptyVNode 159 | } 160 | 161 | export function genText (text: ASTText | ASTExpression): string { 162 | return `_v(${text.type === 2 /* _v 即 createTextVNode */ 163 | ? text.expression // no need for () because already wrapped in _s() 164 | : JSON.stringify(text.text) 165 | })` 166 | } 167 | 168 | function genDirectives (el: ASTElement, state: CodegenState): string | void { 169 | const dirs = el.directives 170 | if (!dirs) return 171 | let res = 'directives:[' 172 | let i, l, dir 173 | for (i = 0, l = dirs.length; i < l; i++) { 174 | dir = dirs[i] 175 | res += `{name:"${dir.name}",rawName:"${dir.rawName}"${ 176 | dir.value ? `,value:(${dir.value}),expression:${JSON.stringify(dir.value)}` : '' 177 | },` 178 | } 179 | // 返回 directives:[{name:"show",rawName:"v-show",value:(show),expression:"show"}, {...}] 格式 180 | return res.slice(0, -1) + ']' 181 | } -------------------------------------------------------------------------------- /code/src/compiler/create-compiler.js: -------------------------------------------------------------------------------- 1 | import { createCompileToFunctionFn } from './to-function' 2 | 3 | export function createCompilerCreator (baseCompile: Function): Function { 4 | // 传入一个函数参数 baseCompile ,最终返回一个函数 5 | return function createCompiler (baseOptions: CompilerOptions) { 6 | 7 | // 定义 compile 函数 8 | function compile ( 9 | template: string, 10 | options?: CompilerOptions 11 | ): CompiledResult { 12 | // baseCompile 是传递过来的函数,最终返回值格式 {ast, render, staticRenderFns} 13 | const compiled = baseCompile(template, finalOptions) 14 | return compiled 15 | } 16 | 17 | // 返回 18 | return { 19 | compile, 20 | compileToFunctions: createCompileToFunctionFn(compile) 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /code/src/compiler/helper.js: -------------------------------------------------------------------------------- 1 | export function addHandler ( 2 | el: ASTElement, 3 | name: string, 4 | value: string 5 | ) { 6 | let events 7 | // 事件都存储在 el.events 中 8 | events = el.events || (el.events = {}) 9 | // 新事件 10 | const newHandler = { value } 11 | // 看是否已有 name 的其他事件 12 | const handlers = events[name] 13 | if (Array.isArray(handlers)) { 14 | handlers.push(newHandler) 15 | } else if (handlers) { 16 | events[name] = [handlers, newHandler] 17 | } else { 18 | // 存储事件 19 | events[name] = newHandler 20 | } 21 | } 22 | 23 | export function addAttr (el: ASTElement, name: string, value: string) { 24 | (el.attrs || (el.attrs = [])).push({ name, value }) 25 | } 26 | 27 | export function addDirective ( 28 | el: ASTElement, 29 | name: string, 30 | rawName: string, 31 | value: string 32 | ) { 33 | (el.directives || (el.directives = [])).push({ name, rawName, value }) 34 | } 35 | 36 | export function getAndRemoveAttr (el: ASTElement, name: string): ?string { 37 | let val 38 | if ((val = el.attrsMap[name]) != null) { 39 | const list = el.attrsList 40 | for (let i = 0, l = list.length; i < l; i++) { 41 | if (list[i].name === name) { 42 | list.splice(i, 1) 43 | break 44 | } 45 | } 46 | } 47 | return val 48 | } -------------------------------------------------------------------------------- /code/src/compiler/index.js: -------------------------------------------------------------------------------- 1 | import { createCompilerCreator } from './create-compiler' 2 | import { parse } from './parser/index' 3 | import { optimize } from './optimizer' 4 | import { generate } from './codegen/index' 5 | 6 | // createCompilerCreator 定义于 ./create-compiler 7 | export const createCompiler = createCompilerCreator( 8 | // 传入一个函数作为参数 9 | function baseCompile ( 10 | template: string, 11 | options: CompilerOptions 12 | ): CompiledResult { 13 | // parse 定义于 ./parser/index 14 | // 将 html 转换为 ast (抽象语法树) 15 | const ast = parse(template.trim(), options) 16 | // optimize 定义于 ./optimizer 17 | // 标记 ast 中的静态节点,即不包含变量无需改变的节点 18 | optimize(ast, options) 19 | // generate 定义于 ./codegen/index 20 | // 返回 {render: '..render函数体..(字符串)', staticRenderFns: Array} 21 | const code = generate(ast, options) 22 | 23 | // 返回 CompiledResult 格式 24 | return { 25 | ast, 26 | render: code.render, 27 | staticRenderFns: code.staticRenderFns 28 | } 29 | } 30 | ) -------------------------------------------------------------------------------- /code/src/compiler/optimizer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Goal of the optimizer: walk the generated template AST tree 3 | * and detect sub-trees that are purely static, i.e. parts of 4 | * the DOM that never needs to change. 5 | * 6 | * Once we detect these sub-trees, we can: 7 | * 8 | * 1. Hoist them into constants, so that we no longer need to 9 | * create fresh nodes for them on each re-render; 10 | * 2. Completely skip them in the patching process. 11 | */ 12 | export function optimize (root: ?ASTElement) { 13 | if (!root) return 14 | // first pass: mark all non-static nodes. 15 | // 标记所有静态节点 16 | markStatic(root) 17 | // second pass: mark static roots. 18 | // 找出最大的静态子树 19 | markStaticRoots(root) 20 | } 21 | 22 | // 标记静态节点 23 | function markStatic (node: ASTNode) { 24 | // 获取该节点是否是 static 25 | node.static = isStatic(node) 26 | if (node.type === 1) { 27 | for (let i = 0, l = node.children.length; i < l; i++) { 28 | const child = node.children[i] 29 | // 递归子节点 30 | markStatic(child) 31 | if (!child.static) { 32 | // 只要有一个子节点不是 static ,那么当前节点就肯定不是 static 33 | node.static = false 34 | } 35 | } 36 | } 37 | } 38 | 39 | // 找出最大的静态子树 40 | function markStaticRoots (node: ASTNode) { 41 | if (node.type === 1) { 42 | if (node.static && node.children.length && !( 43 | // 下面两行代码如

abc

44 | node.children.length === 1 && 45 | node.children[0].type === 3 46 | )) { 47 | // 满足以下情况才能设置 staticRoor = true 48 | // 第一,node.static 49 | // 第二,node.children.length 50 | // 第三,不能是

abc

这种只有一个根节点,且根节点 type === 3 51 | node.staticRoot = true 52 | 53 | // 一旦当前节点标记为 staticRoot = true 之后,就退出,不再去深入递归子节点 54 | // 因为该情况下,子节点肯定都是 static 的情况 55 | // 这样就找到了“最大的静态子树” 56 | return 57 | } else { 58 | // 标记当前节点 59 | node.staticRoot = false 60 | } 61 | if (node.children) { 62 | for (let i = 0, l = node.children.length; i < l; i++) { 63 | // 递归子节点 64 | markStaticRoots(node.children[i]) 65 | } 66 | } 67 | } 68 | } 69 | 70 | function isStatic (node: ASTNode): boolean { 71 | if (node.type === 2) { // expression 72 | return false 73 | } 74 | if (node.type === 3) { // text 75 | return true 76 | } 77 | return !!( 78 | !node.hasBindings && // no dynamic bindings 79 | !node.if && !node.for && // not v-if or v-for or v-else 80 | ) 81 | } -------------------------------------------------------------------------------- /code/src/compiler/parser/html-parser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Not type-checking this file because it's mostly vendor code. 3 | */ 4 | 5 | /*! 6 | * HTML Parser By John Resig (ejohn.org) 7 | * Modified by Juriy "kangax" Zaytsev 8 | * Original code by Erik Arvidsson, Mozilla Public License 9 | * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js 10 | */ 11 | 12 | export function parseHTML (html, options) { 13 | // ... 14 | } -------------------------------------------------------------------------------- /code/src/compiler/parser/index.js: -------------------------------------------------------------------------------- 1 | // 介绍这个之前,可以先用 simplehtmlparser.js 做例子 2 | 3 | import { parseHTML } from './html-parser' 4 | import { parseText } from './text-parser' 5 | 6 | import { 7 | addProp, 8 | addAttr, 9 | baseWarn, 10 | addHandler, 11 | addDirective, 12 | getBindingAttr, 13 | getAndRemoveAttr, 14 | pluckModuleFunction 15 | } from '../helpers' 16 | 17 | export const onRE = /^@|^v-on:/ 18 | export const dirRE = /^v-|^@|^:/ 19 | export const forAliasRE = /(.*?)\s+(?:in|of)\s+(.*)/ 20 | export const forIteratorRE = /\((\{[^}]*\}|[^,]*),([^,]*)(?:,([^,]*))?\)/ 21 | 22 | const argRE = /:(.*)$/ 23 | const bindRE = /^:|^v-bind:/ 24 | const modifierRE = /\.[^.]+/g 25 | 26 | /** 27 | * Convert HTML string to AST. 28 | */ 29 | export function parse ( 30 | template: string, 31 | options: CompilerOptions 32 | ): ASTElement | void { 33 | // 节点栈 34 | const stack = [] 35 | // 根节点,最终改回返回 36 | let root 37 | // 当前的父节点 38 | let currentParent 39 | 40 | parseHTML(template, { 41 | // node 的开始 42 | start (tag, attrs, unary) { 43 | // unary 是否一元标签,如 44 | 45 | const element = { 46 | type: 1, 47 | tag, 48 | // attrsList 数组形式 49 | attrsList: attrs, 50 | // attrsMap 对象形式,如 {id: 'app', 'v-text': 'xxx'} 51 | attrsMap: makeAttrsMap(attrs), 52 | parent: currentParent, 53 | children: [] 54 | } 55 | 56 | // 处理 v-for ,生成 element.for, element.alias 57 | processFor(element) 58 | // 处理 v-if ,生成 element.if, element.else, element.elseif 59 | processIf(element) 60 | // 处理 v-once ,生成 element.once 61 | processOnce(element) 62 | // 处理 key ,生成 element.key 63 | processKey(element) 64 | 65 | // 处理属性 66 | // 第一,处理指令:v-bind v-on 以及其他指令的处理 67 | // 第二,处理普通的 html 属性,如 style class 等 68 | processAttrs(element) 69 | 70 | // tree management 71 | if (!root) { 72 | // 确定根节点 73 | root = element 74 | } 75 | if (currentParent) { 76 | // 当前有根节点 77 | currentParent.children.push(element) 78 | element.parent = currentParent 79 | } 80 | if (!unary) { 81 | // 不是一元标签( 等) 82 | currentParent = element 83 | stack.push(element) 84 | } 85 | }, 86 | // node 的结束 87 | end () { 88 | // pop stack 89 | stack.length -= 1 90 | currentParent = stack[stack.length - 1] 91 | }, 92 | // 字符 93 | chars (text: string) { 94 | const children = currentParent.children 95 | let expression 96 | // 处理字符 97 | expression = parseText(text) // 如 '_s(price)+" 元"' ,_s 在 core/instance/render.js 中定义 98 | children.push({ 99 | type: 2, 100 | expression, 101 | text 102 | }) 103 | }, 104 | // 注释内容 105 | comment (text: string) { 106 | currentParent.children.push({ 107 | type: 3, 108 | text, 109 | isComment: true 110 | }) 111 | } 112 | }) 113 | return root 114 | } 115 | 116 | function makeAttrsMap (attrs: Array): Object { 117 | const map = {} 118 | for (let i = 0, l = attrs.length; i < l; i++) { 119 | map[attrs[i].name] = attrs[i].value 120 | } 121 | return map 122 | } 123 | 124 | // 处理 v-for 125 | function processFor (el) { 126 | let exp 127 | // 获取表达式,例如 'item in list' 128 | if ((exp = getAndRemoveAttr(el, 'v-for'))) { 129 | // inMatch 即 ['item in list', 'item', 'list'] 130 | const inMatch = exp.match(forAliasRE) 131 | if (!inMatch) { 132 | return 133 | } 134 | el.for = inMatch[2].trim() // 'list' 135 | const alias = inMatch[1].trim() // 'item' 136 | 137 | // 如果是 '(item, index) in list' 情况下 138 | // iteratorMatch 即 ["(item, index)", "item", "index", undefined] 139 | const iteratorMatch = alias.match(forIteratorRE) 140 | if (iteratorMatch) { 141 | el.alias = iteratorMatch[1].trim() // 'item' 142 | el.iterator1 = iteratorMatch[2].trim() // 'index' 143 | if (iteratorMatch[3]) { 144 | el.iterator2 = iteratorMatch[3].trim() 145 | } 146 | } else { 147 | // 普通的 'item in list' 这种情况 148 | el.alias = alias // 'item' 149 | } 150 | } 151 | } 152 | 153 | // 处理 v-if 154 | function processIf (el) { 155 | // 获取表达式 156 | const exp = getAndRemoveAttr(el, 'v-if') 157 | if (exp) { 158 | el.if = exp 159 | } else { 160 | if (getAndRemoveAttr(el, 'v-else') != null) { 161 | el.else = true 162 | } 163 | const elseif = getAndRemoveAttr(el, 'v-else-if') 164 | if (elseif) { 165 | el.elseif = elseif 166 | } 167 | } 168 | } 169 | 170 | // 处理 v-once 171 | function processOnce (el) { 172 | const once = getAndRemoveAttr(el, 'v-once') 173 | if (once != null) { 174 | el.once = true 175 | } 176 | } 177 | 178 | // 处理 key 179 | function processKey (el) { 180 | const exp = getBindingAttr(el, 'key') 181 | if (exp) { 182 | el.key = exp 183 | } 184 | } 185 | 186 | function processAttrs (el) { 187 | // 获取属性列表 188 | const list = el.attrsList 189 | let i, l, name, rawName, value, modifiers, isProp 190 | for (i = 0, l = list.length; i < l; i++) { 191 | // 获取属性的 name 和 value 192 | name = rawName = list[i].name 193 | value = list[i].value 194 | 195 | if (dirRE.test(name)) { // 如果该属性是指令,如 v- @ : 特性 196 | // mark element as dynamic 197 | el.hasBindings = true 198 | 199 | if (bindRE.test(name)) { // v-bind ,如 'v-bind:class' 200 | name = name.replace(bindRE, '') // 去掉 name 中的 'v-bind:' 201 | // el.attrs.push(name, value) 202 | addAttr(el, name, value) 203 | } else if (onRE.test(name)) { // v-on ,如 'v-on:click' 204 | name = name.replace(onRE, '') // 去掉 name 中的 'v-on' 205 | // el.events[name] = [{value: value}, ...] // 多个事件就数组形式,单个时间就普通对象形式 {value: value} 206 | addHandler(el, name, value) 207 | } else { // 普通指令 如 v-show 208 | name = name.replace(dirRE, '') // 去掉指令前缀 'v-show' -> 'show' 209 | // el.directives.push({name, rawname, value}) 210 | addDirective(el, name, rawName, value); 211 | } 212 | } else { // 不是指令 213 | // 普通属性加入 el.attrs 214 | // el.attrs.push(name, value) 215 | addAttr(el, name, JSON.stringify(value)) 216 | } 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /code/src/compiler/parser/text-parser.js: -------------------------------------------------------------------------------- 1 | const tagRE = /\{\{((?:.|\n)+?)\}\}/g 2 | 3 | export function parseText (text) { 4 | // text 如 '{{price}} 元' 或者 '价格 {{price}}' 形式 5 | if (!tagRE.test(text)) { 6 | return 7 | } 8 | const tokens = [] 9 | let lastIndex = tagRE.lastIndex = 0 10 | let match, index 11 | while ((match = tagRE.exec(text))) { 12 | index = match.index 13 | // push text token 14 | if (index > lastIndex) { 15 | // '价格 {{price}}' 情况下,token.push('"价格 "') 16 | tokens.push(JSON.stringify(text.slice(lastIndex, index))) 17 | } 18 | // tag token 19 | const exp = match[1].trim() 20 | // token.push('_s(price)') , _s 在 core/instance/render.js 中定义 21 | tokens.push(`_s(${exp})`) 22 | lastIndex = index + match[0].length 23 | } 24 | if (lastIndex < text.length) { 25 | // '{{price}} 元' 情况下,token.push('" 元"') 26 | tokens.push(JSON.stringify(text.slice(lastIndex))) 27 | } 28 | // 最终返回 '_s(price)+" 元"' 或者 '"价格 "+_s(price)' 29 | return tokens.join('+') 30 | } -------------------------------------------------------------------------------- /code/src/compiler/to-function.js: -------------------------------------------------------------------------------- 1 | 2 | // 创建一个函数 3 | function createFunction (code, errors) { 4 | return new Function(code) 5 | } 6 | 7 | export function createCompileToFunctionFn (compile: Function): Function { 8 | // 接收一个函数,返回一个函数 9 | return function compileToFunctions ( 10 | template: string, 11 | options?: CompilerOptions, 12 | vm?: Component 13 | ): CompiledFunctionResult { 14 | const cache = {} 15 | const res = {} 16 | 17 | const compiled = compile(template, options) 18 | res.render = createFunction(compiled.render, fnGenErrors) 19 | res.staticRenderFns = compiled.staticRenderFns.map(code => { 20 | return createFunction(code, fnGenErrors) 21 | }) 22 | 23 | return (cache[key] = res) 24 | } 25 | } -------------------------------------------------------------------------------- /code/src/core/index.js: -------------------------------------------------------------------------------- 1 | import Vue from './instance/index' 2 | 3 | export default Vue -------------------------------------------------------------------------------- /code/src/core/instance/index.js: -------------------------------------------------------------------------------- 1 | import { initMixin } from './init' 2 | import { renderMixin } from './render' 3 | import { lifecycleMixin } from './lifecycle' 4 | 5 | function Vue (options) { 6 | // 创建之后,立刻初始化, _init 定义于 initMixin 中 7 | this._init(options) 8 | } 9 | 10 | // 定义 Vue.prototype._init 11 | initMixin(Vue) 12 | 13 | // 定义 Vue.prototype._update 14 | lifecycleMixin(Vue) 15 | 16 | // 定义 Vue.prototype._render 以及 _o _n _s 等 vdom 使用的缩写函数 17 | renderMixin(Vue) 18 | 19 | export default Vue 20 | -------------------------------------------------------------------------------- /code/src/core/instance/init.js: -------------------------------------------------------------------------------- 1 | 2 | import { initProxy } from './proxy' 3 | import { initState } from './state' 4 | import { initRender } from './render' 5 | import { initLifecycle, callHook } from './lifecycle' 6 | 7 | export function initMixin (Vue: Class) { 8 | Vue.prototype._init = function (options?: Object) { 9 | // options 即用户在 new Vue({...}) 时传入的对象 10 | 11 | const vm = this 12 | 13 | // 将所有的配置属性都混合在一起,放在 vm.$options 对象中,集中处理 14 | // 这里可暂时只关注将 options 中的属性 15 | vm.$options = mergeOptions( 16 | resolveConstructorOptions(vm.constructor), 17 | options || {}, 18 | vm 19 | ) 20 | 21 | vm._self = vm 22 | 23 | // 为 vm 扩展了一些属性 24 | initLifecycle(vm) 25 | // 定义 vm.$createElement 和 vm._c 26 | initRender(vm) 27 | // 触发 beforeCreate 生命周期钩子函数 28 | callHook(vm, 'beforeCreate') 29 | // observe(data) 30 | initState(vm) 31 | // 触发 created 生命周期钩子函数 32 | callHook(vm, 'created') 33 | 34 | if (vm.$options.el) { 35 | // 在 platforms/web/entry-runtime-with-compiler.js 中定义 36 | vm.$mount(vm.$options.el) 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /code/src/core/instance/lifecycle.js: -------------------------------------------------------------------------------- 1 | import { createEmptyVNode } from '../vdom/vnode' 2 | import Watcher from '../observer/watcher' 3 | 4 | export function callHook (vm: Component, hook: string) { 5 | // vm.$options[hook] 可获取一开始 new Vue({...}) 中配置的钩子函数 6 | const handlers = vm.$options[hook] 7 | if (handlers) { 8 | for (let i = 0, j = handlers.length; i < j; i++) { 9 | handlers[i].call(vm) 10 | } 11 | } 12 | } 13 | 14 | export function initLifecycle (vm: Component) { 15 | vm.$parent = parent 16 | vm.$root = parent ? parent.$root : vm 17 | 18 | vm.$children = [] 19 | vm.$refs = {} 20 | 21 | vm._watcher = null 22 | vm._inactive = null 23 | vm._directInactive = false 24 | vm._isMounted = false 25 | vm._isDestroyed = false 26 | vm._isBeingDestroyed = false 27 | } 28 | 29 | export function lifecycleMixin (Vue: Class) { 30 | Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { 31 | const prevVnode = vm._vnode 32 | vm._vnode = vnode 33 | // Vue.prototype.__patch__ is injected in entry points 34 | // based on the rendering backend used. 35 | if (!prevVnode) { 36 | // initial render 37 | vm.$el = vm.__patch__(vm.$el, vnode) 38 | } else { 39 | // updates 40 | vm.$el = vm.__patch__(prevVnode, vnode) 41 | } 42 | } 43 | } 44 | 45 | export function mountComponent ( 46 | vm: Component, 47 | el: ?Element, 48 | hydrating?: boolean 49 | ): Component { 50 | if (!vm.$options.render) { 51 | // createEmptyVNode 定义于 ../vdom/vnode ,一个函数,用于创建一个空白的虚拟 DOM 节点 52 | // 赋值给 vm.$options.render 53 | vm.$options.render = createEmptyVNode 54 | } 55 | 56 | // callHook 在本文件定义,触发某个时机的钩子函数 57 | callHook(vm, 'beforeMount') 58 | 59 | let updateComponent = () => { 60 | // _update 定义于本文件 lifecycleMixin 中 61 | // _render 定义于 ./render.js _render 中,vm._render() 返回一个 vnode 62 | vm._update(vm._render(), hydrating) 63 | } 64 | 65 | // Watcher 定义于 ../observer/watcher 66 | vm._watcher = new Watcher(vm, updateComponent, noop) 67 | 68 | if (vm.$vnode == null) { 69 | vm._isMounted = true 70 | callHook(vm, 'mounted') 71 | } 72 | 73 | return vm; 74 | } -------------------------------------------------------------------------------- /code/src/core/instance/render.js: -------------------------------------------------------------------------------- 1 | import { createElement } from '../vdom/create-element' 2 | 3 | import { 4 | warn, 5 | nextTick, 6 | toNumber, 7 | toString, 8 | looseEqual, 9 | emptyObject, 10 | handleError, 11 | looseIndexOf, 12 | defineReactive 13 | } from '../util/index' 14 | 15 | import { createElement } from '../vdom/create-element' 16 | import { renderList } from './render-helpers/render-list' 17 | import { renderSlot } from './render-helpers/render-slot' 18 | import { resolveFilter } from './render-helpers/resolve-filter' 19 | import { checkKeyCodes } from './render-helpers/check-keycodes' 20 | import { bindObjectProps } from './render-helpers/bind-object-props' 21 | import { renderStatic, markOnce } from './render-helpers/render-static' 22 | import { bindObjectListeners } from './render-helpers/bind-object-listeners' 23 | import { resolveSlots, resolveScopedSlots } from './render-helpers/resolve-slots' 24 | 25 | export function initRender (vm: Component) { 26 | // createElement 即生成一个 vnode 27 | vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false) 28 | vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true) 29 | } 30 | 31 | export function renderMixin (Vue: Class) { 32 | // 将 vm 渲染为 vnode 对象 33 | Vue.prototype._render = function (): VNode { 34 | 35 | // 举一个最简单的例子,如果模板是

{{price}} 元

36 | // 则 render 函数体就是 with(this){return _c('div',{attrs:{"id":"app"}},[_c('p',[_v(_s(price)+" 元")])])} 37 | // 生成 render 函数,执行 render.call(...) 之后返回的是 vnode 实例 38 | // 具体就要看 _c 函数如何定义(_c 就是 createElemenet ,定义于本文中) 39 | 40 | var vm = this; 41 | var ref = vm.$options; 42 | var render = ref.render; 43 | var staticRenderFns = ref.staticRenderFns; 44 | var _parentVnode = ref._parentVnode; 45 | 46 | var vnode; 47 | // vm.$createElement 定义于本文件 initRender 函数中 48 | // vm._renderProxy 就是 vm 本身 49 | vnode = render.call(vm._renderProxy, vm.$createElement); 50 | // set parent 51 | vnode.parent = _parentVnode; 52 | // 返回 53 | return vnode 54 | } 55 | 56 | Vue.prototype._o = markOnce 57 | Vue.prototype._n = toNumber 58 | Vue.prototype._s = toString 59 | Vue.prototype._l = renderList 60 | Vue.prototype._t = renderSlot 61 | Vue.prototype._q = looseEqual 62 | Vue.prototype._i = looseIndexOf 63 | Vue.prototype._m = renderStatic 64 | Vue.prototype._f = resolveFilter 65 | Vue.prototype._k = checkKeyCodes 66 | Vue.prototype._b = bindObjectProps 67 | Vue.prototype._v = createTextVNode 68 | Vue.prototype._e = createEmptyVNode 69 | Vue.prototype._u = resolveScopedSlots 70 | Vue.prototype._g = bindObjectListeners 71 | } -------------------------------------------------------------------------------- /code/src/core/instance/state.js: -------------------------------------------------------------------------------- 1 | import { observe } from '../observer/index' 2 | 3 | // 代理方法 4 | export function proxy (target: Object, sourceKey: string, key: string) { 5 | sharedPropertyDefinition.get = function proxyGetter () { 6 | return this[sourceKey][key] 7 | } 8 | sharedPropertyDefinition.set = function proxySetter (val) { 9 | this[sourceKey][key] = val 10 | } 11 | Object.defineProperty(target, key, sharedPropertyDefinition) 12 | } 13 | 14 | function initMethods (vm: Component, methods: Object) { 15 | for (const key in methods) { 16 | // 将每个 method 赋值给 mv 对象 17 | vm[key] = methods[key] == null ? noop : bind(methods[key], vm) // bind(fn, ctx) 即用 ctx 作为 fn 执行时的 this 18 | } 19 | } 20 | 21 | function initData (vm: Component) { 22 | // 获取 data 23 | let data = vm.$options.data 24 | data = vm._data = typeof data === 'function' 25 | ? getData(data, vm) 26 | : data || {} 27 | // 获取 data 的 key 28 | const keys = Object.keys(data) 29 | 30 | // 遍历所有的 data 31 | let i = keys.length 32 | while (i--) { 33 | const key = keys[i] 34 | // 代理到 vm._data 35 | proxy(vm, `_data`, key) 36 | } 37 | 38 | // 监听 data ,重点重点重点!!! 39 | observe(data, true /* asRootData */) 40 | } 41 | 42 | export function initState (vm: Component) { 43 | vm._watchers = [] 44 | 45 | // 通过 vm.$options 可获取 new Vue({...}) 中配置的 data methods 等信息 46 | const opts = vm.$options 47 | 48 | // 将每个 method 都赋值到 vm 实例 49 | initMethods(vm, opts.methods) 50 | // 将所有 data 属性都代理到 vm._data ,并且 observe(data) 51 | initData(vm) 52 | } -------------------------------------------------------------------------------- /code/src/core/observer/array.js: -------------------------------------------------------------------------------- 1 | /* 2 | * not type checking this file because flow doesn't play well with 3 | * dynamically accessing methods on Array prototype 4 | */ 5 | 6 | import { def } from '../util/index' 7 | 8 | const arrayProto = Array.prototype 9 | export const arrayMethods = Object.create(arrayProto) 10 | 11 | /** 12 | * Intercept mutating methods and emit events 13 | */ 14 | ;[ 15 | 'push', 16 | 'pop', 17 | 'shift', 18 | 'unshift', 19 | 'splice', 20 | 'sort', 21 | 'reverse' 22 | ] 23 | .forEach(function (method) { 24 | // 原始的方法,如 Array.prototype.push 25 | const original = arrayProto[method] 26 | 27 | // 将 arrayMethods(export出去的) 的方法重写 28 | def(arrayMethods, method, function mutator (...args) { 29 | // 用原生函数执行,获取结果 30 | const result = original.apply(this, args) 31 | // this 即被 observe 的对象,即当前的数组 32 | const ob = this.__ob__ 33 | // 记录即将新增到数组中的元素 34 | let inserted 35 | switch (method) { 36 | case 'push': 37 | case 'unshift': 38 | inserted = args 39 | break 40 | case 'splice': 41 | inserted = args.slice(2) 42 | break 43 | } 44 | // 有新元素加入数组,也要被及时 observe 45 | if (inserted) ob.observeArray(inserted) 46 | 47 | // notify change 48 | ob.dep.notify() 49 | 50 | return result 51 | }) 52 | }) -------------------------------------------------------------------------------- /code/src/core/observer/dep.js: -------------------------------------------------------------------------------- 1 | import type Watcher from './watcher' 2 | 3 | /** 4 | * A dep is an observable that can have multiple 5 | * directives subscribing to it. 6 | */ 7 | export default class Dep { 8 | static target: ?Watcher; 9 | id: number; 10 | subs: Array; 11 | 12 | constructor () { 13 | this.id = uid++ 14 | this.subs = [] 15 | } 16 | 17 | addSub (sub: Watcher) { 18 | this.subs.push(sub) 19 | } 20 | 21 | removeSub (sub: Watcher) { 22 | remove(this.subs, sub) 23 | } 24 | 25 | depend () { 26 | if (Dep.target) { 27 | // Dep.target 是一个 Watcher 实例 28 | Dep.target.addDep(this) 29 | } 30 | } 31 | 32 | notify () { 33 | // stabilize the subscriber list first 34 | const subs = this.subs.slice() 35 | for (let i = 0, l = subs.length; i < l; i++) { 36 | // 在 defineReactive 的 setter 中触发 37 | // subs[i] 是一个 Watcher 实例 38 | subs[i].update() 39 | } 40 | } 41 | } 42 | 43 | // the current target watcher being evaluated. 44 | // this is globally unique because there could be only one 45 | // watcher being evaluated at any time. 46 | Dep.target = null 47 | const targetStack = [] 48 | 49 | export function pushTarget (_target: Watcher) { 50 | if (Dep.target) targetStack.push(Dep.target) 51 | Dep.target = _target 52 | } 53 | 54 | export function popTarget () { 55 | Dep.target = targetStack.pop() 56 | } -------------------------------------------------------------------------------- /code/src/core/observer/index.js/export-class-Observer.js: -------------------------------------------------------------------------------- 1 | import Dep from './dep' 2 | import { arrayMethods } from './array' 3 | 4 | /** 5 | * Observer class that are attached to each observed 6 | * object. Once attached, the observer converts target 7 | * object's property keys into getter/setters that 8 | * collect dependencies and dispatches updates. 9 | */ 10 | export class Observer { 11 | value: any; 12 | dep: Dep; 13 | 14 | constructor (value: any) { 15 | this.value = value 16 | // 新建 Dep 对象 17 | this.dep = new Dep() 18 | 19 | // 一个 value 一旦创建 observer 示例,即记录下来,无需重复创建 20 | def(value, '__ob__', this) 21 | 22 | if (Array.isArray(value)) { 23 | // 数组 24 | const augment = protoAugment 25 | // 将数组 value 的 prototype 重写,以便数组更新时能监控到 26 | augment(value, arrayMethods, arrayKeys) 27 | // 监听数组 28 | this.observeArray(value) 29 | } else { 30 | // 普通对象 31 | this.walk(value) 32 | } 33 | } 34 | 35 | /** 36 | * Walk through each property and convert them into 37 | * getter/setters. This method should only be called when 38 | * value type is Object. 39 | */ 40 | walk (obj: Object) { 41 | const keys = Object.keys(obj) 42 | for (let i = 0; i < keys.length; i++) { 43 | // defineProperty 44 | defineReactive(obj, keys[i], obj[keys[i]]) 45 | } 46 | } 47 | 48 | /** 49 | * Observe a list of Array items. 50 | */ 51 | observeArray (items: Array) { 52 | for (let i = 0, l = items.length; i < l; i++) { 53 | observe(items[i]) 54 | } 55 | } 56 | } 57 | 58 | /** 59 | * Augment an target Object or Array by intercepting 60 | * the prototype chain using __proto__ 61 | */ 62 | function protoAugment (target, src: Object, keys: any) { 63 | /* eslint-disable no-proto */ 64 | target.__proto__ = src 65 | /* eslint-enable no-proto */ 66 | } -------------------------------------------------------------------------------- /code/src/core/observer/index.js/export-function-defineReactive.js: -------------------------------------------------------------------------------- 1 | import Dep from './dep' 2 | 3 | export function defineReactive ( 4 | obj: Object, 5 | key: string, 6 | val: any 7 | ) { 8 | // 每次执行 defineReactive 都会创建一个 dep ,它会一直存在于闭包中 9 | const dep = new Dep() 10 | 11 | // cater for pre-defined getter/setters 12 | const property = Object.getOwnPropertyDescriptor(obj, key) 13 | const getter = property && property.get 14 | const setter = property && property.set 15 | 16 | // 递归子节点,observe 即本文件定义 17 | let childOb = observe(val) 18 | 19 | Object.defineProperty(obj, key, { 20 | enumerable: true, 21 | configurable: true, 22 | 23 | // 注意:vm 的 data 中的属性,只有触发这里的 getter 才能绑定 dep 24 | // 在 mount 的时候,执行了 render 函数,而 render 函数需要获取 vm.xxx 渲染视图 25 | // 此前已经将 data 中的属性都 proxy 到了 vm 对象,并且又有 defineProperty 的定义 26 | // 因此执行 render 函数,就能触发这里的 getter ,也就绑定了 dep 27 | get: function reactiveGetter () { 28 | const value = getter ? getter.call(obj) : val 29 | // Dep.target 是 mount 时候,创建 new Watcher() 时赋值的 30 | if (Dep.target) { 31 | 32 | // dep.depend() 即 Dep.target.addDep(this) 33 | // Dep.target 是一个 Watcher 实例,this 即当前的 dep 实例 34 | // 相当于把 dep 实例绑定给了 Watcher 实例 35 | dep.depend() 36 | 37 | if (childOb) { 38 | // 对象子元素,绑定依赖 39 | childOb.dep.depend() 40 | } 41 | if (Array.isArray(value)) { 42 | // 数组,绑定依赖 43 | dependArray(value) 44 | } 45 | } 46 | // 返回值 47 | return value 48 | }, 49 | set: function reactiveSetter (newVal) { 50 | const value = getter ? getter.call(obj) : val 51 | if (newVal === value || (newVal !== newVal && value !== value)) { 52 | return 53 | } 54 | if (setter) { 55 | setter.call(obj, newVal) 56 | } else { 57 | val = newVal 58 | } 59 | // 重新 observe 60 | childOb = observe(newVal) 61 | // 触发通知 62 | dep.notify() 63 | } 64 | }) 65 | } 66 | 67 | /** 68 | * Collect dependencies on array elements when the array is touched, since 69 | * we cannot intercept array element access like property getters. 70 | */ 71 | function dependArray (value: Array) { 72 | for (let e, i = 0, l = value.length; i < l; i++) { 73 | e = value[i] 74 | e && e.__ob__ && e.__ob__.dep.depend() 75 | if (Array.isArray(e)) { 76 | dependArray(e) 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /code/src/core/observer/index.js/export-function-observe.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Attempt to create an observer instance for a value, 3 | * returns the new observer if successfully observed, 4 | * or the existing observer if the value already has one. 5 | */ 6 | export function observe (value: any, asRootData: ?boolean): Observer | void { 7 | // 最初从 initData 进来的时候,value 即 new Vue(...) 配置的 data 属性 8 | // 后续递归的时候,可能是 data 的各个属性或者子属性 9 | 10 | if (!isObject(value)) { 11 | // value 必须是对象 12 | return 13 | } 14 | let ob 15 | if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { 16 | // 已有的,就直接返回 17 | ob = value.__ob__ 18 | } else { 19 | // 创建一个新的(其中会把创建的示例绑定到 value.__ob__ 上) 20 | ob = new Observer(value) 21 | } 22 | return ob 23 | } -------------------------------------------------------------------------------- /code/src/core/observer/scheduler.js: -------------------------------------------------------------------------------- 1 | 2 | import { nextTick } from '../util/index' 3 | 4 | const queue: Array = [] 5 | 6 | /** 7 | * Push a watcher into the watcher queue. 8 | * Jobs with duplicate IDs will be skipped unless it's 9 | * pushed when the queue is being flushed. 10 | */ 11 | export function queueWatcher (watcher: Watcher) { 12 | 13 | } -------------------------------------------------------------------------------- /code/src/core/observer/watcher.js: -------------------------------------------------------------------------------- 1 | import { queueWatcher } from './scheduler' 2 | import Dep, { pushTarget, popTarget } from './dep' 3 | 4 | export default class Watcher { 5 | constructor ( 6 | vm: Component, 7 | expOrFn: string | Function, 8 | cb: Function, 9 | options?: Object 10 | ) { 11 | this.vm = vm 12 | // vm._watchers 在 initState 时候赋值为 [] 13 | vm._watchers.push(this) 14 | 15 | this.cb = cb 16 | this.deps = [] 17 | this.newDeps = [] 18 | this.depIds = new Set() 19 | this.newDepIds = new Set() 20 | 21 | // 传入的函数,赋值给 this.geter 22 | this.getter = expOrFn 23 | 24 | // 执行 get() 方法 25 | this.value = this.get() 26 | } 27 | 28 | get () { 29 | // 将 this (即 Wathcer 示例)给全局的 Dep.target 30 | pushTarget(this) 31 | let value 32 | const vm = this.vm 33 | try { 34 | // this.getter 即 new Watcher(...) 传入的第二个参数 expOrFn 35 | // 这一步,即顺便执行了 expOrFn 36 | value = this.getter.call(vm, vm) 37 | } catch (e) { 38 | // ... 39 | } finally { 40 | popTarget() 41 | } 42 | return value 43 | } 44 | 45 | /** 46 | * Add a dependency to this directive. 47 | */ 48 | // 在 defineReactive 的 getter 中触发 49 | addDep (dep: Dep) { 50 | const id = dep.id 51 | if (!this.newDepIds.has(id)) { 52 | this.newDepIds.add(id) 53 | this.newDeps.push(dep) 54 | if (!this.depIds.has(id)) { 55 | // 将 this (即 Watcher 实例)添加到 dep.subs 中 56 | dep.addSub(this) // 即 this.subs.push(sub) 57 | } 58 | } 59 | } 60 | 61 | // dep.subs[i].notify() 会执行到这里 62 | update () { 63 | // 异步处理 Watcher 64 | // queueWatcher 异步调用了 run() 方法,因为:如果一次性更新多个属性,无需每个都 update 一遍,异步就解决了这个问题 65 | queueWatcher(this) 66 | } 67 | 68 | run () { 69 | // 执行 get ,即执行 this.getter.call(vm, vm) ,即执行 expOrFn 70 | this.get() 71 | } 72 | } -------------------------------------------------------------------------------- /code/src/core/vdom/create-element.js: -------------------------------------------------------------------------------- 1 | export function createElement ( 2 | context: Component, 3 | tag: any, 4 | data: any, 5 | children: any 6 | ): VNode { 7 | // 创建一个 vnode 8 | return _createElement(context, tag, data, children) 9 | } 10 | 11 | export function _createElement ( 12 | context: Component, 13 | tag?: string | Class | Function | Object, 14 | data?: VNodeData, 15 | children?: any 16 | ): VNode { 17 | 18 | vnode = new VNode( 19 | tag, data, children 20 | ) 21 | 22 | return vnode 23 | } -------------------------------------------------------------------------------- /code/src/core/vdom/patch.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Virtual DOM patching algorithm based on Snabbdom by 3 | * Simon Friis Vindum (@paldepind) 4 | * Licensed under the MIT License 5 | * https://github.com/paldepind/snabbdom/blob/master/LICENSE 6 | * 7 | * modified by Evan You (@yyx990803) // 借鉴了 Snabbdom 并自己修改了 8 | */ 9 | 10 | export function createPatchFunction (backend) { 11 | 12 | // 创建空元素 13 | function emptyNodeAt (elm) { 14 | return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm) 15 | } 16 | 17 | function insert (parent, elm, ref) { 18 | if (isDef(parent)) { 19 | if (isDef(ref)) { 20 | if (ref.parentNode === parent) { 21 | nodeOps.insertBefore(parent, elm, ref) 22 | } 23 | } else { 24 | nodeOps.appendChild(parent, elm) 25 | } 26 | } 27 | } 28 | 29 | // 创建子元素 30 | function createChildren (vnode, children) { 31 | if (Array.isArray(children)) { 32 | for (let i = 0; i < children.length; ++i) { 33 | createElm(children[i]) 34 | } 35 | } else if (isPrimitive(vnode.text)) { 36 | nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(vnode.text)) 37 | } 38 | } 39 | 40 | // 创建元素 41 | function createElm (vnode) { 42 | const data = vnode.data 43 | const children = vnode.children 44 | const tag = vnode.tag 45 | 46 | if (isDef(tag)) { 47 | vnode.elm = nodeOps.createElement(tag, vnode) 48 | createChildren(vnode, children) 49 | insert(parentElm, vnode.elm, refElm) 50 | } 51 | } else if (isTrue(vnode.isComment)) { 52 | vnode.elm = nodeOps.createComment(vnode.text) 53 | insert(parentElm, vnode.elm, refElm) 54 | } else { 55 | vnode.elm = nodeOps.createTextNode(vnode.text) 56 | insert(parentElm, vnode.elm, refElm) 57 | } 58 | } 59 | 60 | function updateChildren (parentElm, oldCh, newCh) { 61 | // 源码中处理逻辑较复杂,可通过图示解释 62 | } 63 | 64 | // patch existing root node 65 | function patchVnode (oldVnode, vnode) { 66 | if (oldVnode === vnode) { 67 | return 68 | } 69 | const oldCh = oldVnode.children 70 | const ch = vnode.children 71 | 72 | // 源码中处理逻辑较复杂,其中最重要的就是 updateChildren 方法 73 | updateChildren(elm, oldCh, ch) 74 | } 75 | 76 | // vdom diff 的核心算法 77 | return function patch (oldVnode, vnode) { 78 | if (isUndef(oldVnode)) { 79 | // 依据当前 vnode 创建元素 80 | createElm(vnode) 81 | } else { 82 | // oldVnode 是否是 html 元素 83 | const isRealElement = isDef(oldVnode.nodeType) 84 | if (!isRealElement && sameVnode(oldVnode, vnode)) { // oldVnode 和 vnode 是一个节点的话 85 | // patch existing root node 86 | patchVnode(oldVnode, vnode) 87 | } else { 88 | if (isRealElement) { // oldVnode 是 html 元素 89 | // 依据 html 元素创建一个空 vnode 对象( vnode 中只有一个 tag 其他都是空的 ) 90 | oldVnode = emptyNodeAt(oldVnode) 91 | } 92 | // 获取父元素 93 | const oldElm = oldVnode.elm 94 | const parentElm = nodeOps.parentNode(oldElm) 95 | 96 | // 依据当前 vnode 创建元素 97 | createElm(vnode) 98 | 99 | if (isDef(parentElm)) { 100 | removeVnodes(parentElm, [oldVnode], 0, 0) 101 | } 102 | } 103 | } 104 | return vnode.elm 105 | } 106 | } -------------------------------------------------------------------------------- /code/src/core/vdom/vnode.js: -------------------------------------------------------------------------------- 1 | export const createEmptyVNode = (text: string = '') => { 2 | // VNode 定义于本文件中 3 | const node = new VNode() 4 | node.text = text 5 | node.isComment = true 6 | return node 7 | } 8 | 9 | export default class VNode { 10 | constructor ( 11 | tag?: string, 12 | data?: VNodeData, 13 | children?: ?Array 14 | ) { 15 | this.tag = tag 16 | this.data = data 17 | this.children = children 18 | } 19 | } -------------------------------------------------------------------------------- /code/src/platforms/web/compiler/index.js: -------------------------------------------------------------------------------- 1 | import { createCompiler } from 'compiler/index' 2 | 3 | const { compile, compileToFunctions } = createCompiler() 4 | 5 | export { compile, compileToFunctions } -------------------------------------------------------------------------------- /code/src/platforms/web/entry-runtime-with-compiler.js: -------------------------------------------------------------------------------- 1 | 2 | import Vue from './runtime/index' 3 | import { compileToFunctions } from './compiler/index' 4 | 5 | // $mount 在 ./runtime/index 中定义 6 | const mount = Vue.prototype.$mount; 7 | 8 | // 重新定义 $mount 9 | Vue.prototype.$mount = function ( 10 | el?: string | Element, 11 | hydrating?: boolean 12 | ): Component { 13 | const options = this.$options 14 | 15 | // resolve template/el and convert to render function 16 | // 如果用户没有传递 render 函数,就解析 template/el 生成 render 函数 17 | if (!options.render) { 18 | let template = options.template 19 | if (template) { 20 | // 如果 # 开头(id),拿到这个元素的模板,重新赋值给 template 21 | // 如果是 node 节点,拿到 innerHTML 重新赋值给 template 22 | } else if (el) { 23 | // 拿到 el 的模板,赋值给 template 24 | template = getOuterHTML(el) 25 | } 26 | if (template) { 27 | // compileToFunctions 定义于 ./compiler/index 28 | const { render, staticRenderFns } = compileToFunctions(template, {...}, this) 29 | 30 | // 将生成的 render 函数复制给 this.$options.render 31 | options.render = render 32 | // 生成 render 函数时,会将静态的方法存储到 staticRenderFns 中,可见 codegen 的 genStatic 方法 33 | options.staticRenderFns = staticRenderFns 34 | } 35 | } 36 | 37 | // 最终执行到 core/instance/lifecycle.js 中的 mountComponent 方法 38 | // hydrating 和 ssr 相关,暂不考虑 39 | return mount.call(this, el, hydrating) 40 | } 41 | 42 | export default Vue -------------------------------------------------------------------------------- /code/src/platforms/web/runtime/index.js: -------------------------------------------------------------------------------- 1 | import { patch } from './patch' 2 | import Vue from 'core/index' 3 | import { mountComponent } from 'core/instance/lifecycle' 4 | 5 | // install platform patch function 6 | Vue.prototype.__patch__ = inBrowser ? patch : noop 7 | 8 | // 此处定义 $mmount 9 | Vue.prototype.$mount = function ( 10 | el?: string | Element, 11 | hydrating?: boolean 12 | ): Component { 13 | el = el && inBrowser ? query(el) : undefined 14 | // mountComponent 在 core/instance/lifecycle 中定义 15 | // hydrating 和 ssr 相关,暂忽略 16 | return mountComponent(this, el, hydrating) 17 | } -------------------------------------------------------------------------------- /code/src/platforms/web/runtime/patch.js: -------------------------------------------------------------------------------- 1 | 2 | import { createPatchFunction } from 'core/vdom/patch' 3 | 4 | export const patch: Function = createPatchFunction({ nodeOps, modules }) 5 | -------------------------------------------------------------------------------- /test/demo1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | demo1 6 | 7 | 8 | 9 |
10 |

{{price}} 元

11 |
12 | 13 | 27 | 28 | -------------------------------------------------------------------------------- /test/demo2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | demo2 6 | 7 | 8 |

test

9 | 10 | 41 | 42 | -------------------------------------------------------------------------------- /test/htmlparser/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | simple html parser 7 | 8 | 9 |

simple html parser

10 | 11 | 12 | 35 | 36 | -------------------------------------------------------------------------------- /test/htmlparser/simplehtmlparser.js: -------------------------------------------------------------------------------- 1 | // Copyright 2004 Erik Arvidsson. All Rights Reserved. 2 | // 3 | // This code is triple licensed using Apache Software License 2.0, 4 | // Mozilla Public License or GNU Public License 5 | // 6 | /////////////////////////////////////////////////////////////////////////////// 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not 9 | // use this file except in compliance with the License. You may obtain a copy 10 | // of the License at http://www.apache.org/licenses/LICENSE-2.0 11 | // 12 | /////////////////////////////////////////////////////////////////////////////// 13 | // 14 | // The contents of this file are subject to the Mozilla Public License 15 | // Version 1.1 (the "License"); you may not use this file except in 16 | // compliance with the License. You may obtain a copy of the License at 17 | // http://www.mozilla.org/MPL/ 18 | // 19 | // Software distributed under the License is distributed on an "AS IS" 20 | // basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the 21 | // License for the specific language governing rights and limitations 22 | // under the License. 23 | // 24 | // The Original Code is Simple HTML Parser. 25 | // 26 | // The Initial Developer of the Original Code is Erik Arvidsson. 27 | // Portions created by Erik Arvidssson are Copyright (C) 2004. All Rights 28 | // Reserved. 29 | // 30 | /////////////////////////////////////////////////////////////////////////////// 31 | // 32 | // This program is free software; you can redistribute it and/or 33 | // modify it under the terms of the GNU General Public License 34 | // as published by the Free Software Foundation; either version 2 35 | // of the License, or (at your option) any later version. 36 | // 37 | // This program is distributed in the hope that it will be useful, 38 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 39 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 40 | // GNU General Public License for more details. 41 | // 42 | // You should have received a copy of the GNU General Public License 43 | // along with this program; if not, write to the Free Software 44 | // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 45 | // 46 | /////////////////////////////////////////////////////////////////////////////// 47 | /* 48 | var handler ={ 49 | startElement: function (sTagName, oAttrs) {}, 50 | endElement: function (sTagName) {}, 51 | characters: function (s) {}, 52 | comment: function (s) {} 53 | }; 54 | */ 55 | 56 | function SimpleHtmlParser() {} 57 | 58 | SimpleHtmlParser.prototype = { 59 | 60 | handler: null, 61 | 62 | // regexps 63 | startTagRe: /^<([^>\s\/]+)((\s+[^=>\s]+(\s*=\s*((\"[^"]*\")|(\'[^']*\')|[^>\s]+))?)*)\s*\/?\s*>/m, 64 | endTagRe: /^<\/([^>\s]+)[^>]*>/m, 65 | attrRe: /([^=\s]+)(\s*=\s*((\"([^"]*)\")|(\'([^']*)\')|[^>\s]+))?/gm, 66 | 67 | parse: function(s, oHandler) { 68 | if (oHandler) this.contentHandler = oHandler; 69 | 70 | var i = 0; 71 | var res, lc, lm, rc, index; 72 | var treatAsChars = false; 73 | var oThis = this; 74 | while (s.length > 0) { 75 | // Comment 76 | if (s.substring(0, 4) == ""); 78 | if (index != -1) { 79 | this.contentHandler.comment(s.substring(4, index)); 80 | s = s.substring(index + 3); 81 | treatAsChars = false; 82 | } else { 83 | treatAsChars = true; 84 | } 85 | } 86 | 87 | // end tag 88 | else if (s.substring(0, 2) == "