├── .editorconfig ├── .gitignore ├── README.md ├── book ├── chapter1.md ├── chapter2.md ├── chapter3.md ├── chapter4.md └── chapter5.md └── code ├── .eslinrc ├── dist └── vue.js ├── example └── index.html ├── karma.conf.js ├── package.json ├── rollup.config.js ├── src ├── config.js ├── global-api │ ├── extend.js │ └── index.js ├── index.js ├── instance │ ├── event.js │ ├── index.js │ ├── init.js │ ├── lifecycle.js │ ├── render.js │ └── state.js ├── observer │ ├── array.js │ ├── dep.js │ ├── index.js │ └── watcher.js ├── platform │ └── web │ │ ├── index.js │ │ ├── modules │ │ ├── attrs.js │ │ ├── class.js │ │ ├── events.js │ │ ├── index.js │ │ └── style.js │ │ ├── node-ops.js │ │ ├── patch.js │ │ ├── platform.js │ │ └── util.js ├── shared │ ├── constants.js │ └── util.js ├── util │ └── index.js └── vdom │ ├── create-component.js │ ├── create-element.js │ ├── helpers │ ├── extract-props.js │ └── index.js │ ├── patch.js │ └── vnode.js └── test ├── observer ├── observer.js └── watcher.js ├── reactivity.spec.js └── vdom ├── create-component.spec.js ├── create-element.spec.js ├── modules └── attrs.spec.js └── patch ├── children.spec.js ├── element.spec.js └── hooks.spec.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = false 9 | insert_final_newline = false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | code/node_modules 3 | package-lock.json 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Build your own Vuejs 2 | 3 | > This project is working in progress, stay tuned. 4 | 5 | This repository includes *Build your own Vuejs* book and the code for it. 6 | 7 | In the book *Build your own Vuejs*, we will build a Vuejs from scratch. And you will learn how Vuejs works internally, which helps a lot for your daily development with vue. 8 | 9 | Inspired by the amazing book *Build your own Angularjs*, the code of *Build your own Vuejs* will be developed in a test-driven way. 10 | 11 | We'll focus on Vuejs 2.0. And we assume our reader have played around with Vuejs once and know basics about Vuejs APIs. 12 | 13 | Table of Contents 14 | 15 | + [Chapter1: Vuejs Overview](https://github.com/jsrebuild/build-your-own-vuejs/blob/master/book/chapter1.md) 16 | + [Chapter2: Reactivity system](https://github.com/jsrebuild/build-your-own-vuejs/blob/master/book/chapter2.md) 17 | + [Chapter3: Virtual DOM](https://github.com/jsrebuild/build-your-own-vuejs/blob/master/book/chapter3.md) 18 | + [Chapter4: Built-in modules: directives, attributes, class and style](https://github.com/jsrebuild/build-your-own-vuejs/blob/master/book/chapter4.md) 19 | + [Chapter5: Instance methods and global API](https://github.com/jsrebuild/build-your-own-vuejs/blob/master/book/chapter5.md) 20 | + Chapter6: Advanced features 21 | 22 | 23 | ### Code 24 | 25 | #### develop 26 | 27 | `npm run watch` 28 | 29 | #### test 30 | 31 | `npm run test` 32 | 33 | #### build 34 | 35 | `npm run build` 36 | 37 | ### Lisence 38 | 39 | Well, do whatever your like. 40 | 41 | -------------------------------------------------------------------------------- /book/chapter1.md: -------------------------------------------------------------------------------- 1 | ## Chapter1: Vuejs Overview 2 | 3 | Vuejs is a simple yet powerful MVVM library. It helps us to build a modern user interface for the web. 4 | 5 | By the time of writing, Vuejs has 36,312 stars on Github. And 230,250 monthly downloads on npm. Vuejs 2.0 brings in a lightweight virtual DOM implementation for render layer. This unlock more possibilities like server-side rendering and native component rendering. 6 | 7 | Vuejs claims to be a progressive JavaScript framework. Though the core library of Vuejs is quite small. Vuejs has many accompanying tools & supporting libraries. So you can build large-scale application using the Vuejs ecosystem. 8 | 9 | 10 | ### Components of Vuejs internals 11 | 12 | Let's get acquaintance with the core components of Vuejs internals. Vue internals falls into serval parts: 13 | 14 | ![Vue internal](https://occc3ev3l.qnssl.com/Vue%20source%20overview.png) 15 | 16 | 17 | #### Instance lifecycle 18 | 19 | A new Vue instance will go through several phases. Such as observing data, initializing events, compiling the template, and render. And you can register lifecycle hooks that will be called in the specific phase. 20 | 21 | #### Reactivity system 22 | 23 | The so called *reactivity system* is where vue's data-view binding magic comes from. When you set vue instance's data, the view updated accordingly, and vice versa. 24 | 25 | Vue use `Object.defineProperty` to make data object's property reactive. Along with the famous *Observer Pattern* to link data change and view render together. 26 | 27 | 28 | #### Virtual DOM 29 | 30 | Virtual DOM is the tree representation of the actual DOM tree that lives in the memory as JavaScript Objects. 31 | 32 | When data changes, vue will render a brand new virtual DOM tree, and keep the old one. The virtual DOM module diff two trees and patch the change into the actual DOM tree. 33 | 34 | Vue use [snabbdom](https://github.com/snabbdom/snabbdom) as the base of its virtual DOM implementation. And modify a bit to make it work with Vue's other component. 35 | 36 | #### Compiler 37 | 38 | The job of the compiler is to compile template into render functions(ASTs). It parses HTML along with Vue directives (Vue directives are just plain HTML attribute) and other entities into a tree. It also detects the maximum static sub trees (sub trees with no dynamic bindings) and hoists them out of the render. The HTML parser Vue uses is originally written by [John Resig](http://ejohn.org). 39 | 40 | > We will not cover the implementation detail of the Compiler in this book. Since we can use build tools to compile vue template into render functions in build time, Compiler is not a part of vue runtime. And we can even write render functions directly, so Compiler is not an essential part to understand vue internals. 41 | 42 | 43 | ### Set up development environment 44 | 45 | Before we can start building our own Vue.js, we need to set up a few things. Including module bundler and testing tools, since we will use a test-driven workflow. 46 | 47 | Since this is a JavaScript project, and we'gonna use some fancy tools, the first thing to do is run `npm init` and set up some information about this project. 48 | 49 | 50 | #### Set up Rollup for module bundling 51 | 52 | We will use Rollup for module bundling. [Rollup](http://rollupjs.org) is a JavaScript module bundler. It allows you to write your application or library as a set of modules – using modern ES2015 import/export syntax. And Vuejs use Rollup for module bundling too. 53 | 54 | We gotta write a configuration for Rollup to make it work. Under root directory, touch `rollup.conf.js`: 55 | 56 | ``` 57 | export default { 58 | input: 'src/instance/index.js', 59 | output: { 60 | name: 'Vue', 61 | file: 'dist/vue.js', 62 | format: 'iife' 63 | }, 64 | }; 65 | ``` 66 | And don't forget to run `npm install rollup rollup-watch --save-dev`. 67 | 68 | #### Set up Karma and Jasmine for testing 69 | 70 | Testing will require quite a few packages, run: 71 | 72 | ``` 73 | npm install karma jasmine karma-jasmine karma-chrome-launcher 74 | karma-rollup-plugin karma-rollup-preprocessor buble rollup-plugin-buble --save-dev 75 | ``` 76 | 77 | Under root directory, touch `karma.conf.js`: 78 | 79 | ``` 80 | module.exports = function(config) { 81 | config.set({ 82 | files: [{ pattern: 'test/**/*.spec.js', watched: false }], 83 | frameworks: ['jasmine'], 84 | browsers: ['Chrome'], 85 | preprocessors: { 86 | './test/**/*.js': ['rollup'] 87 | }, 88 | rollupPreprocessor: { 89 | plugins: [ 90 | require('rollup-plugin-buble')(), 91 | ], 92 | output: { 93 | format: 'iife', 94 | name: 'Vue', 95 | sourcemap: 'inline' 96 | } 97 | } 98 | }) 99 | } 100 | ``` 101 | 102 | #### Directory structure 103 | 104 | ``` 105 | - package.json 106 | - rollup.conf.js 107 | - node_modules 108 | - dist 109 | - test 110 | - src 111 | - observer 112 | - instance 113 | - util 114 | - vdom 115 | 116 | ``` 117 | 118 | 119 | ### Bootstrapping 120 | 121 | We'll add some npm script for convenience. 122 | 123 | *package.json* 124 | 125 | ``` 126 | "scripts": { 127 | "build": "rollup -c", 128 | "watch": "rollup -c -w", 129 | "test": "karma start" 130 | } 131 | ``` 132 | 133 | To bootstrap our own Vuejs, let's write our first test case. 134 | 135 | *test/options/options.spec.js* 136 | 137 | ``` 138 | import Vue from "../src/instance/index"; 139 | 140 | describe('Proxy test', function() { 141 | it('should proxy vm._data.a = vm.a', function() { 142 | var vm = new Vue({ 143 | data:{ 144 | a:2 145 | } 146 | }) 147 | expect(vm.a).toEqual(2); 148 | }); 149 | }); 150 | ``` 151 | 152 | This test case tests whether props on vm's data like `vm._data.a` are proxied to vm itself, like `vm.a`. This is one of Vue's little tricks. 153 | 154 | So we can write our first line of real code now, in 155 | 156 | *src/instance/index.js* 157 | 158 | ``` 159 | import { initMixin } from './init' 160 | 161 | function Vue (options) { 162 | this._init(options) 163 | } 164 | 165 | initMixin(Vue) 166 | 167 | export default Vue 168 | ``` 169 | This is nothing exciting, just Vue constructor calling `this._init`. So let's find out how the `initMixin` fucntion work: 170 | 171 | 172 | *src/instance/init.js* 173 | 174 | ``` 175 | import { initState } from './state' 176 | 177 | export function initMixin (Vue) { 178 | Vue.prototype._init = function (options) { 179 | var vm = this 180 | vm.$options = options 181 | initState(vm) 182 | } 183 | } 184 | ``` 185 | 186 | The instance method of Vue Class are injected using a mixin pattern. We'll find this mixin pattern quite common when writing Vuejs's instance method later. Mixin is just a function that takes a constructor, add some methods to its prototype, and return the constructor. 187 | 188 | So `initMixin` add `_init` method to `Vue.prototype`. And this method calls `initState` from `state.js`: 189 | 190 | *src/instance/state.js* 191 | 192 | ``` 193 | 194 | export function initState(vm) { 195 | initData(vm) 196 | } 197 | 198 | function initData(vm) { 199 | var data = vm.$options.data 200 | vm._data = data 201 | // proxy data on instance 202 | var keys = Object.keys(data) 203 | 204 | var i = keys.length 205 | while (i--) { 206 | proxy(vm, keys[i]) 207 | } 208 | } 209 | 210 | function proxy(vm, key) { 211 | Object.defineProperty(vm, key, { 212 | configurable: true, 213 | enumerable: true, 214 | get: function proxyGetter() { 215 | return vm._data[key] 216 | }, 217 | set: function proxySetter(val) { 218 | vm._data[key] = val 219 | } 220 | }) 221 | } 222 | ``` 223 | 224 | Finally, we got to the place where proxy takes place. `initState` calls `initData`, and `initData` iterates all keys of `vm._data`, calls `proxy` on each value. 225 | 226 | `proxy` define a property on `vm` using the same key, and this property has both getter and setter, which actually get/set data from `vm._data`. 227 | 228 | So that's how `vm.a` is proxied to `vm._data.a`. 229 | 230 | Run `npm run build` and `npm run test`. You should see something like this: 231 | 232 | ![success](http://cdn4.snapgram.co/images/2016/12/11/ScreenShot2016-12-12at2.02.17AM.png) 233 | 234 | Bravo! You successfully bootstrapped your own Vuejs! Keep working! -------------------------------------------------------------------------------- /book/chapter2.md: -------------------------------------------------------------------------------- 1 | ## Chapter2: Reactivity system 2 | 3 | 4 | Vue's reactivity system makes data binding between model and view simple and intuitive. Data is defined as a plain JavaScript object. When data changes, the view updated automatically to reflex the lastest state. It works like a charm. 5 | 6 | Under the hood, Vuejs will walk through all of the data's properties and convert them to getter/setters using `Object.defineProperty`. 7 | 8 | Each primitive key-value pair in data has an `Observer` instance. The observer will send a signal for watchers who subscribed the value change event earlier. 9 | 10 | And each `Vue` instance has a `Watcher` instance which records any properties “touched” during the component’s render as dependencies. When data changes, watcher will re-collect dependencies and run the callback passed when the watcher is initialized. 11 | 12 | So how do observer notify watcher for data change? Observer pattern to the rescue! We define a new class called `Dep`, which means "Dependence", to serve as a mediator. Observer instance has a reference for all the deps it needs to notify when data changes. And each dep instance knows which watcher it needs to update. 13 | 14 | That's basically how the reactivity system works from a 100,000 feet view. In the next few sections, we'll have a closer look at the implementation details of the reactivity system. 15 | 16 | 17 | 18 | ### 2.1 Dep 19 | 20 | The implemetation of `Dep` is stratforwd. Each dep instance has a uid to for identification. The `subs` array records all watchers subscribe to this dep instance. `Dep.prototype.notify` call each subscribers' update method in `subs` array. `Dep.prototype.depend` is used for dependency collecttion during watcher's re-evaluation. We'll come to watchers later. For now you should only konw that `Dep.target` is the watcher instance being re-evaluated at the moment. Since this property is a static, so `Dep.target` works globally and points to one watcher at a time. 21 | 22 | *src/observer/dep.js* 23 | 24 | ``` 25 | var uid = 0 26 | 27 | // Dep contructor 28 | export default function Dep(argument) { 29 | this.id = uid++ 30 | this.subs = [] 31 | } 32 | 33 | Dep.prototype.addSub = function(sub) { 34 | this.subs.push(sub) 35 | } 36 | 37 | Dep.prototype.removeSub = function(sub) { 38 | remove(this.subs, sub) 39 | } 40 | 41 | Dep.prototype.depend = function() { 42 | if (Dep.target) { 43 | Dep.target.addDep(this) 44 | } 45 | } 46 | 47 | Dep.prototype.notify = function() { 48 | var subs = this.subs.slice() 49 | for (var i = 0, l = subs.length; i < l; i++) { 50 | subs[i].update() 51 | } 52 | } 53 | 54 | Dep.target = null 55 | ``` 56 | 57 | ### 2.2Observer basics 58 | 59 | 60 | We start by a boilerplate like this: 61 | 62 | *src/observer/index.js* 63 | 64 | ``` 65 | // Observer constructor 66 | export function Observer(value) { 67 | 68 | } 69 | 70 | // API for observe value 71 | export function observe (value){ 72 | 73 | } 74 | ``` 75 | Before we implement `Observer`, we'll write a test first. 76 | 77 | *test/observer/observer.spec.js* 78 | 79 | ``` 80 | import { 81 | Observer, 82 | observe 83 | } from "../../src/observer/index" 84 | import Dep from '../../src/observer/dep' 85 | 86 | describe('Observer test', function() { 87 | it('observing object prop change', function() { 88 | const obj = { a:1, b:{a:1}, c:NaN} 89 | observe(obj) 90 | // mock a watcher! 91 | const watcher = { 92 | deps: [], 93 | addDep (dep) { 94 | this.deps.push(dep) 95 | dep.addSub(this) 96 | }, 97 | update: jasmine.createSpy() 98 | } 99 | // observing primitive value 100 | Dep.target = watcher 101 | obj.a 102 | Dep.target = null 103 | expect(watcher.deps.length).toBe(1) // obj.a 104 | obj.a = 3 105 | expect(watcher.update.calls.count()).toBe(1) 106 | watcher.deps = [] 107 | }); 108 | 109 | }); 110 | ``` 111 | 112 | First, we define a plain JavaScript object `obj` as data. Then we use `observe` function to make data reactive. Since we haven't implement watcher yet, we need to mock a watcher. A watcher has a `deps` array for dependency bookkeeping. The `update` method will be called when data changes. We'll come to `addDep` later this section. 113 | 114 | Here we use a jasmine's spy function as a placeholder. A spy function has no real functionality. It keeps information like how many times it's been called and the parameters being passed in when called. 115 | 116 | Then we set the global `Dep.target` to `watcher`, and get `obj.a.b`. If the data is reactive, then the watcher's update method will be called. 117 | 118 | So let's foucus on the `observe` fucntion first. The code is listed below. It first checks if the value is an object. If so, it then checks if this value already has a `Observer` instance attched by checking its `__ob__` property. 119 | 120 | If there is no exsiting `Observer` instance, it will initiate a new `Observer` instance with the value and return it. 121 | 122 | *src/observer/index.js* 123 | 124 | ``` 125 | import { 126 | hasOwn, 127 | isObject 128 | } 129 | from '../util/index' 130 | 131 | export function observe (value){ 132 | if (!isObject(value)) { 133 | return 134 | } 135 | var ob 136 | if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { 137 | ob = value.__ob__ 138 | } else { 139 | ob = new Observer(value) 140 | } 141 | return ob 142 | } 143 | ``` 144 | 145 | Here, we need a little utility function `hasOwn`, which is a simple warpper for `Object.prototype.hasOwnProperty`: 146 | 147 | *src/util/index.js* 148 | 149 | ``` 150 | var hasOwnProperty = Object.prototype.hasOwnProperty 151 | export function hasOwn (obj, key) { 152 | return hasOwnProperty.call(obj, key) 153 | } 154 | ``` 155 | 156 | And another utility function `isObject`: 157 | 158 | *src/util/index.js* 159 | 160 | ``` 161 | ··· 162 | export function isObject (obj) { 163 | return obj !== null && typeof obj === 'object' 164 | } 165 | ``` 166 | 167 | Now it's time to look at the `Observer` constructor. It will init a `Dep` instance, and it calls `walk` with the value. And it attachs observer to `value` as `__ob__ ` property. 168 | 169 | *src/observer/index.js* 170 | 171 | ``` 172 | import { 173 | def, //new 174 | hasOwn, 175 | isObject 176 | } 177 | from '../util/index' 178 | 179 | export function Observer(value) { 180 | this.value = value 181 | this.dep = new Dep() 182 | this.walk(value) 183 | def(value, '__ob__', this) 184 | } 185 | ``` 186 | 187 | `def` here is a new utility function which define property for object key using `Object.defineProperty()` API. 188 | 189 | *src/util/index.js* 190 | 191 | ``` 192 | ··· 193 | export function def (obj, key, val, enumerable) { 194 | Object.defineProperty(obj, key, { 195 | value: val, 196 | enumerable: !!enumerable, 197 | writable: true, 198 | configurable: true 199 | }) 200 | } 201 | ``` 202 | 203 | The `walk` method just iterate over the object, call each value with `defineReactive`. 204 | 205 | *src/observer/index.js* 206 | 207 | ``` 208 | Observer.prototype.walk = function(obj) { 209 | var keys = Object.keys(obj) 210 | for (var i = 0; i < keys.length; i++) { 211 | defineReactive(obj, keys[i], obj[keys[i]]) 212 | } 213 | } 214 | ``` 215 | 216 | 217 | `defineReactive` is where `Object.defineProperty` comes into play. 218 | 219 | *src/observer/index.js* 220 | 221 | ``` 222 | export function defineReactive (obj, key, val) { 223 | var dep = new Dep() 224 | Object.defineProperty(obj, key, { 225 | enumerable: true, 226 | configurable: true, 227 | get: function reactiveGetter () { 228 | var value = val 229 | if (Dep.target) { 230 | dep.depend() 231 | } 232 | return value 233 | }, 234 | set: function reactiveSetter (newVal) { 235 | var value = val 236 | if (newVal === value || (newVal !== newVal && value !== value)) { 237 | return 238 | } 239 | val = newVal 240 | dep.notify() 241 | } 242 | }) 243 | } 244 | ``` 245 | 246 | The `reactiveGetter` function checks if `Dep.target` exists, which means the getter is triggered during a watcher dependency re-collection. When that happens, we add dependency by calling `dep.depend()`. `dep.depend()` actually calls `Dep.target.addDep(dep)`. Since `Dep.target` is a watcher, is equals `watcher.addDep(dep)`. Let's see what `addDep`do: 247 | 248 | ``` 249 | addDep (dep) { 250 | this.deps.push(dep) 251 | dep.addSub(this) 252 | } 253 | ``` 254 | 255 | It pushes `dep` to watcher's `deps` array. It also pushes the target watcher to the dep's `subs` array. So that's how dependencies are tracked. 256 | 257 | The `reactiveSetter` function simply set the new value if the new value is not the same with the old one. And it notifies watcher to update by calling `dep.notify()`. Let's review the previous Dep section: 258 | 259 | ``` 260 | Dep.prototype.notify = function() { 261 | var subs = this.subs.slice() 262 | for (var i = 0, l = subs.length; i < l; i++) { 263 | subs[i].update() 264 | } 265 | } 266 | ``` 267 | `Dep.prototype.notify` calls each watcher's `update` methods in the `subs` array. Well, yes, the watchers're the same watchers that were pushed into the `subs` array during `Dep.target.addDep(dep)`. So things're all connected. 268 | 269 | Let's try `npm run test`. The test case we wrote earilier should all pass. 270 | 271 | ### 2.3 Observing nested object 272 | 273 | We can only observe simple plain object with primitive values at this time. So in the section we'll add support for observing non-primitive value, like object. 274 | 275 | First we're gonna modify the test case a bit: 276 | 277 | *test/observer/observer.spec.js* 278 | 279 | ``` 280 | 281 | describe('Observer test', function() { 282 | it('observing object prop change', function() { 283 | ··· 284 | // observing non-primitive value 285 | Dep.target = watcher 286 | obj.b.a 287 | Dep.target = null 288 | expect(watcher.deps.length).toBe(3) // obj.b + b + b.a 289 | obj.b.a = 3 290 | expect(watcher.update.calls.count()).toBe(1) 291 | watcher.deps = [] 292 | }); 293 | ``` 294 | 295 | `obj.b` is a object itself. So we check if the value change on `obj.b` is notified to see if non-primitive value observing is supported. 296 | 297 | The solution is straightforward, we'll recursively call `observer` function on `val`. If `val` is not an object, the `observer` will return. So when we use `defineReactive` to observe a key-value pair, we keep call `observe` fucntion and keep the return value in `childOb`. 298 | 299 | *src/observer/index.js* 300 | 301 | ``` 302 | export function defineReactive (obj, key, val) { 303 | var dep = new Dep() 304 | var childOb = observe(val) // new 305 | Object.defineProperty(obj, key, { 306 | ··· 307 | }) 308 | } 309 | ``` 310 | 311 | The reason that we need to keep the reference of child observer is we need to re-collect dependencies on child objects when getter is called: 312 | 313 | *src/observer/index.js* 314 | 315 | ``` 316 | ··· 317 | get: function reactiveGetter () { 318 | var value = val 319 | if (Dep.target) { 320 | dep.depend() 321 | // re-collect for childOb 322 | if (childOb) { 323 | childOb.dep.depend() 324 | } 325 | } 326 | return value 327 | } 328 | ··· 329 | ``` 330 | 331 | And we also need to re-observe child value when setter is called: 332 | 333 | *src/observer/index.js* 334 | 335 | ``` 336 | ··· 337 | set: function reactiveSetter (newVal) { 338 | var value = val 339 | if (newVal === value || (newVal !== newVal && value !== value)) { 340 | return 341 | } 342 | val = newVal 343 | childOb = observe(newVal) //new 344 | dep.notify() 345 | } 346 | ··· 347 | ``` 348 | 349 | ### 2.4 Observing set/delete of data 350 | 351 | Vue has some caveats on observing data change. Vue cannot detect property **addition** or **deletion** due to the way Vue handles data change. Data change will only be detected when getter or setter is called, but set/delete of data will call neither getter or setter. 352 | 353 | However, it’s possible to add reactive properties to a nested object using the `Vue.set(object, key, value)` method. And delete reactive properties using the `Vue.delete(object, key, value)` method. 354 | 355 | Let's write a test case for this, as always: 356 | 357 | *test/observer/observer.spec.js* 358 | 359 | ``` 360 | import { 361 | Observer, 362 | observe, 363 | set as setProp, //new 364 | del as delProp //new 365 | } 366 | from "../../src/observer/index" 367 | import { 368 | hasOwn, 369 | isObject 370 | } 371 | from '../util/index' //new 372 | 373 | describe('Observer test', function() { 374 | // new test case 375 | it('observing set/delete', function() { 376 | const obj1 = { 377 | a: 1 378 | } 379 | // should notify set/delete data 380 | const ob1 = observe(obj1) 381 | const dep1 = ob1.dep 382 | spyOn(dep1, 'notify') 383 | setProp(obj1, 'b', 2) 384 | expect(obj1.b).toBe(2) 385 | expect(dep1.notify.calls.count()).toBe(1) 386 | delProp(obj1, 'a') 387 | expect(hasOwn(obj1, 'a')).toBe(false) 388 | expect(dep1.notify.calls.count()).toBe(2) 389 | // set existing key, should be a plain set and not 390 | // trigger own ob's notify 391 | setProp(obj1, 'b', 3) 392 | expect(obj1.b).toBe(3) 393 | expect(dep1.notify.calls.count()).toBe(2) 394 | // should ignore deleting non-existing key 395 | delProp(obj1, 'a') 396 | expect(dep1.notify.calls.count()).toBe(3) 397 | }); 398 | ··· 399 | } 400 | ``` 401 | 402 | We add a new test case called `observing set/delete` in `Observer test`. 403 | 404 | Now we can implement these two methods: 405 | 406 | *src/observer/index.js* 407 | 408 | ``` 409 | export function set (obj, key, val) { 410 | if (hasOwn(obj, key)) { 411 | obj[key] = val 412 | return 413 | } 414 | const ob = obj.__ob__ 415 | if (!ob) { 416 | obj[key] = val 417 | return 418 | } 419 | defineReactive(ob.value, key, val) 420 | ob.dep.notify() 421 | return val 422 | } 423 | 424 | export function del (obj, key) { 425 | const ob = obj.__ob__ 426 | if (!hasOwn(obj, key)) { 427 | return 428 | } 429 | delete obj[key] 430 | if (!ob) { 431 | return 432 | } 433 | ob.dep.notify() 434 | } 435 | ``` 436 | 437 | The function `set` will first check if the key exists. If the key exists, we simply give it a new value and return. Then we'll check if this object is reactive using `obj.__ob__`, if not, we'll return. If the key is not there yet, we'll make this key-value pair reactive using `defineReactive`, and call `ob.dep.notify()` to notify the obj's value is changed. 438 | 439 | The function `del` is almost the same expect it delete value using `delete` operator. 440 | 441 | ### 2.5 Observing array 442 | 443 | Our implemetation has one flawn yet, it can't observe array mutaion. Since accessing array element using subscrpt syntax will not trigger getter. So the old school getter/setter is not suitable for array change dectection. 444 | 445 | In order to watch array change, we need to hajack a few array method like `Array.prototype.pop()` and `Array.prototype.shift()`. And instead of using subscrpt syntax to set array value, we'll use `Vue.set` API inplemented in the last secion. 446 | 447 | Here is the test case for observing array mutation, when we using `Array` API that will cause mutation, the change will be observed. And each of array's element will be observed, too. 448 | 449 | *test/observer/observer.spec.js* 450 | 451 | ``` 452 | describe('Observer test', function() { 453 | // new 454 | it('observing array mutation', () => { 455 | const arr = [] 456 | const ob = observe(arr) 457 | const dep = ob.dep 458 | spyOn(dep, 'notify') 459 | const objs = [{}, {}, {}] 460 | arr.push(objs[0]) 461 | arr.pop() 462 | arr.unshift(objs[1]) 463 | arr.shift() 464 | arr.splice(0, 0, objs[2]) 465 | arr.sort() 466 | arr.reverse() 467 | expect(dep.notify.calls.count()).toBe(7) 468 | // inserted elements should be observed 469 | objs.forEach(obj => { 470 | expect(obj.__ob__ instanceof Observer).toBe(true) 471 | }) 472 | }); 473 | ··· 474 | } 475 | ``` 476 | 477 | The first step is handle array in `Observer`: 478 | 479 | *src/observer/index.js* 480 | 481 | ``` 482 | export function Observer(value) { 483 | this.value = value 484 | this.dep = new Dep() 485 | //this.walk(value) //deleted 486 | // new 487 | if(Array.isArray(value)){ 488 | this.observeArray(value) 489 | }else{ 490 | this.walk(value) 491 | } 492 | def(value, '__ob__', this) 493 | } 494 | ``` 495 | 496 | `observeArray` just iterate over the array and call `observe` on every item. 497 | 498 | *src/observer/index.js* 499 | 500 | ``` 501 | ··· 502 | Observer.prototype.observeArray = function(items) { 503 | for (let i = 0, l = items.length; i < l; i++) { 504 | observe(items[i]) 505 | } 506 | } 507 | ``` 508 | 509 | Next we're going to warp the original `Array` method by modifying the prototype chain. 510 | 511 | First, we create a singleton that has all the array mutation method. Those array methods are warpped with other logic that deals with change detection. 512 | 513 | *src/observer/array.js* 514 | 515 | ``` 516 | import { def } from '../util/index' 517 | 518 | const arrayProto = Array.prototype 519 | export const arrayMethods = Object.create(arrayProto) 520 | 521 | /** 522 | * Intercept mutating methods and emit events 523 | */ 524 | ;[ 525 | 'push', 526 | 'pop', 527 | 'shift', 528 | 'unshift', 529 | 'splice', 530 | 'sort', 531 | 'reverse' 532 | ] 533 | .forEach(function (method) { 534 | // cache original method 535 | const original = arrayProto[method] 536 | def(arrayMethods, method, function mutator () { 537 | let i = arguments.length 538 | const args = new Array(i) 539 | while (i--) { 540 | args[i] = arguments[i] 541 | } 542 | const result = original.apply(this, args) 543 | const ob = this.__ob__ 544 | let inserted 545 | switch (method) { 546 | case 'push': 547 | inserted = args 548 | break 549 | case 'unshift': 550 | inserted = args 551 | break 552 | case 'splice': 553 | inserted = args.slice(2) 554 | break 555 | } 556 | if (inserted) ob.observeArray(inserted) 557 | // notify change 558 | ob.dep.notify() 559 | return result 560 | }) 561 | }) 562 | ``` 563 | 564 | `arrayMethods` is the singleton that has all array mutation method. 565 | 566 | For all the methods in array: 567 | 568 | ``` 569 | ['push','pop','shift','unshift','splice','sort','reverse'] 570 | ``` 571 | 572 | We define a `mutator` function that warps the original method. 573 | 574 | In the `mutator` function, we first get the arguments as an array. Next, we apply the original array method with the arguments array and keep the result. 575 | 576 | For the case when adding new items to array, we call `observeArray` on the new array items. 577 | 578 | Finaly, we notify change using `ob.dep.notify()`, and return the result. 579 | 580 | Second, we need to add this singleton into the prototype chain. 581 | 582 | If we can use `__proto__` in the current browser, we'll directly point the array's prototype to the singleton we created recently. 583 | 584 | If this is not the case, we'll mix `arrayMethods` singleton into the observed array. 585 | 586 | So we need a few helper funtion: 587 | 588 | *src/observer/index.js* 589 | 590 | ``` 591 | // helpers 592 | /** 593 | * Augment an target Object or Array by intercepting 594 | * the prototype chain using __proto__ 595 | */ 596 | function protoAugment (target, src) { 597 | target.__proto__ = src 598 | } 599 | 600 | /** 601 | * Augment an target Object or Array by defining 602 | * properties. 603 | */ 604 | function copyAugment (target, src, keys) { 605 | for (let i = 0, l = keys.length; i < l; i++) { 606 | var key = keys[i] 607 | def(target, key, src[key]) 608 | } 609 | } 610 | ``` 611 | 612 | In `Observer` function, we use `protoAugment` or `copyAugment` depending on whether we can use `__proto__` or not, to augment the original array: 613 | 614 | *src/observer/index.js* 615 | 616 | ``` 617 | import { 618 | def, 619 | hasOwn, 620 | hasProto, //new 621 | isObject 622 | } 623 | from '../util/index' 624 | 625 | export function Observer(value) { 626 | this.value = value 627 | this.dep = new Dep() 628 | if(Array.isArray(value)){ 629 | //new 630 | var augment = hasProto 631 | ? protoAugment 632 | : copyAugment 633 | augment(value, arrayMethods, arrayKeys) 634 | this.observeArray(value) 635 | }else{ 636 | this.walk(value) 637 | } 638 | def(value, '__ob__', this) 639 | } 640 | ``` 641 | 642 | The definiion of `hasProto` is trival: 643 | 644 | *src/util/index.js* 645 | 646 | ``` 647 | ··· 648 | export var hasProto = '__proto__' in {} 649 | ``` 650 | 651 | That should be enough to pass the `observing array mutation` test. 652 | 653 | //something about dependArray(value) 654 | 655 | ### 2.6 Watcher 656 | 657 | We had mocked the `Watcher` in previous test like this: 658 | 659 | ``` 660 | const watcher = { 661 | deps: [], 662 | addDep (dep) { 663 | this.deps.push(dep) 664 | dep.addSub(this) 665 | }, 666 | update: jasmine.createSpy() 667 | } 668 | ``` 669 | 670 | So watcher here is basically a object which has a `deps` property that records all dependencies of this watcher, and it also has a `addDep` method for adding dependency, and a `update` method that will be called when the data watched has changed. 671 | 672 | Let's take a look at the Watcher constructor signature: 673 | 674 | ``` 675 | constructor ( 676 | vm: Component, 677 | expOrFn: string | Function, 678 | cb: Function, 679 | options?: Object 680 | ) 681 | ``` 682 | 683 | So the Watcher constructor takes a `expOrFn` paramater, and a callback `cb`. The `expOrFn` is a expression or a function which is evaluated when initializing a watcher. The callback is called when that watcher need to run. 684 | 685 | The test below should shed some light on how watcher works. 686 | 687 | *test/observer/watcher.spec.js* 688 | 689 | ``` 690 | import Vue from "../../src/instance/index"; 691 | import Watcher from "../../src/observer/watcher"; 692 | 693 | describe('Wathcer test', function() { 694 | it('should call callback when simple data change', function() { 695 | var vm = new Vue({ 696 | data:{ 697 | a:2 698 | } 699 | }) 700 | var cb = jasmine.createSpy('callback'); 701 | var watcher = new Watcher(vm, function(){ 702 | var a = vm.a 703 | }, cb) 704 | vm.a = 5; 705 | expect(cb).toHaveBeenCalled(); 706 | }); 707 | }); 708 | ``` 709 | 710 | The `expOrFn` is evaluated so the vm's data's specific reactive getter is called(In the case, `vm.a`'s getter). The watcher set itself as the current target of dep. So `vm.a`'s dep will push this watcher instance to it's `subs` array. And watcher will push `vm.a`'s dep to it's `deps` array. When `vm.a`'s setter is called, `vm.a`'s dep's `subs` array will be iterated and each watcher in `subs` array's `update` method will be called. Finally the callback of watcher will be called. 711 | 712 | Now we can start inplement the Watcher Class: 713 | 714 | **src/observer/watcher.js** 715 | 716 | ``` 717 | let uid = 0 718 | 719 | export default function Watcher(vm, expOrFn, cb, options) { 720 | options = options ? options : {} 721 | this.vm = vm 722 | vm._watchers.push(this) 723 | this.cb = cb 724 | this.id = ++uid 725 | 726 | // options 727 | this.deps = [] 728 | this.newDeps = [] 729 | this.depIds = new Set() 730 | this.newDepIds = new Set() 731 | this.getter = expOrFn 732 | this.value = this.get() 733 | } 734 | ``` 735 | 736 | The `Watcher` class initialize some properties. Each `Watcher` instance has a unique id for further use. This is set via `this.id = ++uid`. `this.deps` and `this.newDeps` are array of deps object, these arrays are used for Deps bookkeeping. We'll see why we need two arrays to achieve that later. `this.depIds` and `this.newDepIds` are the id set of the corresponding deps array. We can lookup whether particular dep instance exists in the deps array or not quickly through these sets. 737 | 738 | The last two line evaluate the expression/function passed in. This step is where dependency collection happens. Next we need to implement `Watcher.prototype.get`. 739 | 740 | **src/observer/watcher.js** 741 | 742 | ``` 743 | Watcher.prototype.get = function() { 744 | pushTarget(this) 745 | var value = this.getter.call(this.vm, this.vm) 746 | popTarget() 747 | this.cleanupDeps() 748 | return value 749 | } 750 | ``` 751 | 752 | `Watcher.prototype.get` method first push the current `Watcher` instance as the `Dep.target`. Then get the value of through `this.getter.call(this.vm, this.vm)`. The value is not important if the getter is a function. 753 | 754 | After that, we need to pop target, and clean up. Cleanning up is needed because every time the `Watcher` instance is re-evaluate, the bookkeeping of the dep-watcher mapping is different. We need to update dep's sub array, and watcher's deps array, when certain data has changed. 755 | 756 | So that's why we need two arrays in the `Watcher` constructor. The `newDep` array and `newDepIds` array are used for a new dependency collection run. The last time's dependency is saved in the `dep` and `depIds` array. What `cleanupDeps` does is simply move the data in the `newDep` and `newDepIds` array to the `dep` and `depIds` array, and reset the `newDep` and `newDepIds` array. 757 | 758 | **src/observer/watcher.js** 759 | 760 | ``` 761 | /** 762 | * Add a dependency to this directive. 763 | */ 764 | Watcher.prototype.addDep = function(dep) { 765 | var id = dep.id 766 | if (!this.newDepIds.has(id)) { 767 | this.newDepIds.add(id) 768 | this.newDeps.push(dep) 769 | if (!this.depIds.has(id)) { 770 | dep.addSub(this) 771 | } 772 | } 773 | } 774 | 775 | /** 776 | * Clean up for dependency collection. 777 | */ 778 | Watcher.prototype.cleanupDeps = function() { 779 | var i = this.deps.length 780 | while (i--) { 781 | var dep = this.deps[i] 782 | if (!this.newDepIds.has(dep.id)) { 783 | dep.removeSub(this) 784 | } 785 | } 786 | var tmp = this.depIds 787 | this.depIds = this.newDepIds 788 | this.newDepIds = tmp 789 | this.newDepIds.clear() 790 | tmp = this.deps 791 | this.deps = this.newDeps 792 | this.newDeps = [] 793 | } 794 | ``` 795 | 796 | Finally, the `Watcher.prototype.update` and `Watcher.prototype.run` method are used when the `Wathcher` instance need to re-evaluate. `Watcher.prototype.update` simply calls `Watcher.prototype.run`(The warpper here is used for further asnyc batch mechanism). 797 | 798 | `Watcher.prototype.run` calls `this.get` to get the new value, and calls the callback of the `Wathcher` instance to notify user that the data has changed. 799 | 800 | **src/observer/watcher.js** 801 | 802 | ``` 803 | Watcher.prototype.update = function() { 804 | console.log("update!!") 805 | this.run() 806 | } 807 | 808 | Watcher.prototype.run = function() { 809 | var value = this.get() 810 | var oldValue = this.value 811 | this.value = value 812 | this.cb.call(this.vm, value, oldValue) 813 | } 814 | ``` 815 | 816 | 817 | ### 2.7 Async Batch Queue 818 | 819 | The unit test part will use Vue contructor. So this part should be moved to later chapters. 820 | 821 | Introduction: Why Async Batch Queue? 822 | 823 | unit test 824 | 825 | **src/observer/scheduler.js** 826 | 827 | 828 | queue, flushQueue 829 | 830 | ``` 831 | ``` 832 | 833 | next Tick 834 | 835 | ### 2.8 Warp up 836 | 837 | Todo 838 | 839 | 840 | -------------------------------------------------------------------------------- /book/chapter3.md: -------------------------------------------------------------------------------- 1 | ## Chapter3: Virtual DOM 2 | 3 | 4 | ### 3.1 A brief introduction to Virtual DOM 5 | 6 | 7 | Virtual DOM is the an abstraction of DOM. We use a light weight JavaScript object to present a real DOM node. Each component's view structure can be expressed by a Virtual DOM tree. When the component render for the first time, we get the the Virtual DOM tree using the `render` function. The Virtual DOM tree is then transformed and inserted into the real DOM. And when the component's data has changed, we'll re-render to get a new the Virtual DOM tree, calculate the minimal differences(insertion, addition, deletion, movement) needed to transform the old Virtual DOM tree to the shape of the new one. Finally we apply these changes to the real DOM(The last two steps are called patching in most Virtual DOM implementation). 8 | 9 | The reason why Vuejs use Virtual DOM rather than binding DOM manipulations directly to data changes is fwe can achieve cross-platform render by switching the backend of Virtual DOM. So Virtual DOM actually is not exactly an abstraction of DOM, it's an abstraction of the component's view's structure. We can use all kinds of backend to render the Virtual DOM tree, such as iOS and Android. 10 | 11 | Besides, the abstraction layer provided by Virtual DOM will made declarative programming style straightforward. 12 | 13 | We had a famous equation in declarative data-driven style front-end development: 14 | 15 | `UI = render(state)` 16 | 17 | The render function takes the component state and produce DOM by apply the state with the Virtual DOM tree. So the Virtual DOM is a key infrastructure of declarative UI programming. 18 | 19 | ### 3.2 How does Vue transform template into Virtual DOM 20 | 21 | [Vue's official documentation on render function](https://vuejs.org/v2/guide/render-function.html) is highly recommended. You **should** read this to understand that Vue's template is really a syntactic sugar underneath. 22 | 23 | The template below: 24 | 25 | ``` 26 |
27 | I'm a template! 28 |
29 | ``` 30 | 31 | Will be compiled into: 32 | 33 | ``` 34 | function anonymous( 35 | ) { 36 | with(this){return _c('div',[_v("I'm a template!\n")])} 37 | } 38 | ``` 39 | 40 | `_c` is the alias for `createElement`. This API create a Virtual DOM node instance. And we can pass an array of children nodes to `createElement`, so the result of the render function will be a tree of Virtual DOM node. 41 | 42 | So Vue's template is compiled into render function in build time ( With the help from vue-loader ). When Vue re-render the UI, the render fucntion is called. And it returns a new Virtual DOM tree. 43 | 44 | 45 | ### 3.3 Virtual DOM and the component system 46 | 47 | Each virtual DOM node is an abstraction of a real DOM node. But how about a component? 48 | 49 | In Vuejs, a component has a corresponding virtual DOM node(`VNode` instance), this `VNode` instance is regarded as a placeholder for the component in the Virtual DOM tree. This placeholder `VNode` instance has only one children, the Virtual DOM node corresponding to the component's **root DOM** Node. 50 | 51 | *should be a image here to visualize this problem* 52 | 53 | ### 3.4 `VNode` Class 54 | 55 | We need to define the structure for the `VNode` Class first. 56 | 57 | *src/vdom/vnode.js* 58 | 59 | ``` 60 | export default function VNode(tag, data, children, text, elm, context, componentOptions) { 61 | this.tag = tag 62 | this.data = data 63 | this.children = children 64 | this.text = text 65 | this.elm = elm 66 | this.context = context 67 | this.key = data && data.key 68 | this.componentOptions = componentOptions 69 | this.componentInstance = undefined 70 | this.parent = undefined 71 | this.isComment = false 72 | } 73 | ``` 74 | 75 | Here we define `VNode`'s attributes using a constructor function. A Virtual DOM Node has some DOM-related attributes like tag, text and ns. And it also has Vue component related infomation like componentOptions and componentInstance. 76 | 77 | The children attribute is a pointer pointing to the children of the node, and the parent attribute point to the parent of that node. You know it, Virtual DOM is a tree. 78 | 79 | The most useful attribute for a `VNode` is the data attribute. It has all the props, directives, event handlers, class, and styles you defined in your template stored. 80 | 81 | 82 | ### 3.5 `create-element` API 83 | 84 | We're gonna implement the famous `h` function in Vue's render function! 85 | 86 | > JSX use `h` as the alias for `createElement`. Vue use `_c` instead. 87 | 88 | Let's write the test case for `createElement`: 89 | 90 | *test/vdom/create-element.spec.js* 91 | 92 | ``` 93 | import Vue from "src/index" 94 | import { createEmptyVNode } from 'src/vdom/vnode' 95 | 96 | describe('create-element', () => { 97 | it('render vnode with basic reserved tag using createElement', () => { 98 | const vm = new Vue({ 99 | data: { msg: 'hello world' } 100 | }) 101 | const h = vm.$createElement 102 | const vnode = h('p', {}) 103 | expect(vnode.tag).toBe('p') 104 | expect(vnode.data).toEqual({}) 105 | expect(vnode.children).toBeUndefined() 106 | expect(vnode.text).toBeUndefined() 107 | expect(vnode.elm).toBeUndefined() 108 | expect(vnode.ns).toBeUndefined() 109 | expect(vnode.context).toEqual(vm) 110 | }) 111 | 112 | it('render vnode with component using createElement', () => { 113 | const vm = new Vue({ 114 | data: { message: 'hello world' }, 115 | components: { 116 | 'my-component': { 117 | props: ['msg'] 118 | } 119 | } 120 | }) 121 | const h = vm.$createElement 122 | const vnode = h('my-component', { props: { msg: vm.message }}) 123 | expect(vnode.tag).toMatch(/vue-component-[0-9]+/) 124 | expect(vnode.componentOptions.propsData).toEqual({ msg: vm.message }) 125 | expect(vnode.children).toBeUndefined() 126 | expect(vnode.text).toBeUndefined() 127 | expect(vnode.elm).toBeUndefined() 128 | expect(vnode.ns).toBeUndefined() 129 | expect(vnode.context).toEqual(vm) 130 | }) 131 | 132 | it('render vnode with custom tag using createElement', () => { 133 | const vm = new Vue({ 134 | data: { msg: 'hello world' } 135 | }) 136 | const h = vm.$createElement 137 | const tag = 'custom-tag' 138 | const vnode = h(tag, {}) 139 | expect(vnode.tag).toBe('custom-tag') 140 | expect(vnode.data).toEqual({}) 141 | expect(vnode.children).toBeUndefined() 142 | expect(vnode.text).toBeUndefined() 143 | expect(vnode.elm).toBeUndefined() 144 | expect(vnode.ns).toBeUndefined() 145 | expect(vnode.context).toEqual(vm) 146 | expect(vnode.componentOptions).toBeUndefined() 147 | }) 148 | 149 | it('render empty vnode with falsy tag using createElement', () => { 150 | const vm = new Vue({ 151 | data: { msg: 'hello world' } 152 | }) 153 | const h = vm.$createElement 154 | const vnode = h(null, {}) 155 | expect(vnode).toEqual(createEmptyVNode()) 156 | }) 157 | }) 158 | ``` 159 | 160 | The tag passed to `createElement` should be one of: 161 | 162 | + platform built-in element's ( reserved tag ) tag name 163 | + Vue component's tag name 164 | + custom element's ( Web Component ) tag name 165 | + null 166 | 167 | The `createElement` function should handle those situations. For platform built-in element and custom element, we just render the origin tag. For Vue component, we create a Vue component Node, this API will be implement in the next section. For null, we return a empty VNode. 168 | 169 | The implementation is pretty straightforward: 170 | 171 | ``` 172 | import VNode, { createEmptyVNode } from "./vnode"; 173 | import config from "../config"; 174 | import { createComponent } from "./create-component"; 175 | 176 | import { 177 | isDef, 178 | isUndef, 179 | isTrue, 180 | isPrimitive, 181 | resolveAsset 182 | } from "../util/index"; 183 | 184 | export function createElement( 185 | context, 186 | tag, 187 | data, 188 | children 189 | ) { 190 | let vnode, ns; 191 | if (typeof tag === "string") { 192 | let Ctor; 193 | if (config.isReservedTag(tag)) { 194 | // platform built-in elements 195 | vnode = new VNode( 196 | config.parsePlatformTagName(tag), 197 | data, 198 | children, 199 | undefined, 200 | undefined, 201 | context 202 | ); 203 | } else if ( 204 | isDef((Ctor = resolveAsset(context.$options, "components", tag))) 205 | ) { 206 | // component 207 | vnode = createComponent(Ctor, data, context, children, tag); 208 | } else { 209 | // unknown or unlisted namespaced elements 210 | // check at runtime because it may get assigned a namespace when its 211 | // parent normalizes children 212 | vnode = new VNode(tag, data, children, undefined, undefined, context); 213 | } 214 | } else { 215 | // direct component options / constructor 216 | vnode = createComponent(tag, data, context, children); 217 | } 218 | if (!isDef(vnode)) { 219 | return createEmptyVNode(); 220 | } 221 | return vnode; 222 | } 223 | ``` 224 | 225 | ### 3.6 `create-component` API 226 | 227 | > Pre-compose-summary: Create-component implement the idea introduced in section 3.3. That is, create a placeholder VNode for a Vue component. 228 | 229 | 230 | ### 3.7 Patching Virtual DOM 231 | 232 | > Pre-compose-summary: Patch is the key fucntion for VDOM module. Introduce the function of patching and the basic flow for patching. 233 | 234 | ### 3.8 The hook mechanism and the patch lifecycle 235 | 236 | > Pre-compose-summary: Introduce the lifecycle for patching: init, create, insert, prepatch, postpatch, update, remove, destroy. And the VDOM plugin mechanism based on hooks. 237 | 238 | ### 3.9 Patching children 239 | 240 | > Pre-compose-summary: The MAGICAL DIFFING Algorithm 241 | 242 | ### 3.10 Connecting Vue and Virtual DOM 243 | 244 | > Pre-compose-summary: Call render when watcher is notified 245 | 246 | ### 3.11 Warp up -------------------------------------------------------------------------------- /book/chapter4.md: -------------------------------------------------------------------------------- 1 | ## Chapter4: Built-in modules: directives, attributes, class and style -------------------------------------------------------------------------------- /book/chapter5.md: -------------------------------------------------------------------------------- 1 | ## Chapter5: Instance methods and global API -------------------------------------------------------------------------------- /code/.eslinrc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsrebuild/build-your-own-vuejs/aa273934889be7d0900e23135e5d7b5cf2d30a88/code/.eslinrc -------------------------------------------------------------------------------- /code/dist/vue.js: -------------------------------------------------------------------------------- 1 | var Vue = (function () { 2 | 'use strict'; 3 | 4 | function VNode(tag, data, children, text, elm, context, componentOptions) { 5 | this.tag = tag; 6 | this.data = data; 7 | this.children = children; 8 | this.text = text; 9 | this.elm = elm; 10 | this.ns = undefined; 11 | this.context = context; 12 | this.functionalContext = undefined; 13 | this.key = data && data.key; 14 | this.componentOptions = componentOptions; 15 | this.componentInstance = undefined; 16 | this.parent = undefined; 17 | this.raw = false; 18 | this.isStatic = false; 19 | this.isRootInsert = true; 20 | this.isComment = false; 21 | this.isCloned = false; 22 | this.isOnce = false; 23 | } 24 | 25 | const createEmptyVNode = () => { 26 | const node = new VNode(); 27 | node.text = ''; 28 | node.isComment = true; 29 | return node 30 | }; 31 | 32 | /** 33 | * Strict object type check. Only returns true 34 | * for plain JavaScript objects. 35 | */ 36 | function isPlainObject(obj) { 37 | return _toString.call(obj) === '[object Object]' 38 | } 39 | 40 | /** 41 | * Create a cached version of a pure function. 42 | */ 43 | function cached(fn) { 44 | const cache = Object.create(null); 45 | return (function cachedFn (str) { 46 | const hit = cache[str]; 47 | return hit || (cache[str] = fn(str)) 48 | }) 49 | } 50 | 51 | /** 52 | * Camelize a hyphen-delimited string. 53 | */ 54 | const camelizeRE = /-(\w)/g; 55 | const camelize = cached((str) => { 56 | return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '') 57 | }); 58 | 59 | /** 60 | * Hyphenate a camelCase string. 61 | */ 62 | const hyphenateRE = /\B([A-Z])/g; 63 | const hyphenate = cached((str) => { 64 | return str.replace(hyphenateRE, '-$1').toLowerCase() 65 | }); 66 | 67 | /** 68 | * Capitalize a string. 69 | */ 70 | const capitalize = cached((str) => { 71 | return str.charAt(0).toUpperCase() + str.slice(1) 72 | }); 73 | 74 | function makeMap ( 75 | str, 76 | expectsLowerCase 77 | ) { 78 | const map = Object.create(null); 79 | const list = str.split(','); 80 | for (let i = 0; i < list.length; i++) { 81 | map[list[i]] = true; 82 | } 83 | return expectsLowerCase 84 | ? val => map[val.toLowerCase()] 85 | : val => map[val] 86 | } 87 | 88 | /** 89 | * Return same value 90 | */ 91 | const identity = (_) => _; 92 | 93 | const isHTMLTag = makeMap( 94 | 'html,body,base,head,link,meta,style,title,' + 95 | 'address,article,aside,footer,header,h1,h2,h3,h4,h5,h6,hgroup,nav,section,' + 96 | 'div,dd,dl,dt,figcaption,figure,picture,hr,img,li,main,ol,p,pre,ul,' + 97 | 'a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,rtc,ruby,' + 98 | 's,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video,' + 99 | 'embed,object,param,source,canvas,script,noscript,del,ins,' + 100 | 'caption,col,colgroup,table,thead,tbody,td,th,tr,' + 101 | 'button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,' + 102 | 'output,progress,select,textarea,' + 103 | 'details,dialog,menu,menuitem,summary,' + 104 | 'content,element,shadow,template,blockquote,iframe,tfoot' 105 | ); 106 | 107 | // this map is intentionally selective, only covering SVG elements that may 108 | // contain child elements. 109 | const isSVG = makeMap( 110 | 'svg,animate,circle,clippath,cursor,defs,desc,ellipse,filter,font-face,' + 111 | 'foreignObject,g,glyph,image,line,marker,mask,missing-glyph,path,pattern,' + 112 | 'polygon,polyline,rect,switch,symbol,text,textpath,tspan,use,view', 113 | true 114 | ); 115 | 116 | 117 | var config = ({ 118 | /** 119 | * Check if a tag is reserved so that it cannot be registered as a 120 | * component. This is platform-dependent and may be overwritten. 121 | */ 122 | isReservedTag: (tag) => { 123 | return isHTMLTag(tag) || isSVG(tag) 124 | }, 125 | 126 | /** 127 | * Parse the real tag name for the specific platform. 128 | */ 129 | parsePlatformTagName: identity, 130 | }) 131 | 132 | function isReserved (str) { 133 | var c = (str + '').charCodeAt(0); 134 | return c === 0x24 || c === 0x5F 135 | } 136 | 137 | function noop () {} 138 | 139 | /** 140 | * Resolve an asset. 141 | * This function is used because child instances need access 142 | * to assets defined in its ancestor chain. 143 | */ 144 | function resolveAsset ( 145 | options, 146 | type, 147 | id, 148 | warnMissing 149 | ) { 150 | if (typeof id !== 'string') { 151 | return 152 | } 153 | const assets = options[type]; 154 | // check local registration variations first 155 | if (hasOwn(assets, id)) return assets[id] 156 | const camelizedId = camelize(id); 157 | if (hasOwn(assets, camelizedId)) return assets[camelizedId] 158 | const PascalCaseId = capitalize(camelizedId); 159 | if (hasOwn(assets, PascalCaseId)) return assets[PascalCaseId] 160 | // fallback to prototype chain 161 | const res = assets[id] || assets[camelizedId] || assets[PascalCaseId]; 162 | return res 163 | } 164 | 165 | function def (obj, key, val, enumerable) { 166 | Object.defineProperty(obj, key, { 167 | value: val, 168 | enumerable: !!enumerable, 169 | writable: true, 170 | configurable: true 171 | }); 172 | } 173 | 174 | var hasOwnProperty = Object.prototype.hasOwnProperty; 175 | function hasOwn (obj, key) { 176 | return hasOwnProperty.call(obj, key) 177 | } 178 | 179 | function isObject (obj) { 180 | return obj !== null && typeof obj === 'object' 181 | } 182 | 183 | // can we use __proto__? 184 | var hasProto = '__proto__' in {}; 185 | 186 | function isUndef (v){ 187 | return v === undefined || v === null 188 | } 189 | 190 | function isDef (v){ 191 | return v !== undefined && v !== null 192 | } 193 | 194 | 195 | 196 | var strats = Object.create(null); 197 | /** 198 | * Default strategy. 199 | */ 200 | var defaultStrat = function (parentVal, childVal) { 201 | return childVal === undefined 202 | ? parentVal 203 | : childVal 204 | }; 205 | 206 | // strats.data = function ( 207 | // parentVal, 208 | // childVal, 209 | // vm 210 | // ) { 211 | // if (!vm) { 212 | // if (childVal && typeof childVal !== 'function') { 213 | // return parentVal 214 | // } 215 | // return mergeDataOrFn(parentVal, childVal) 216 | // } 217 | 218 | // return mergeDataOrFn(parentVal, childVal, vm) 219 | // } 220 | 221 | /** 222 | * Ensure all props option syntax are normalized into the 223 | * Object-based format. 224 | */ 225 | function normalizeProps (options, vm) { 226 | const props = options.props; 227 | if (!props) return 228 | const res = {}; 229 | let i, val, name; 230 | if (Array.isArray(props)) { 231 | i = props.length; 232 | while (i--) { 233 | val = props[i]; 234 | if (typeof val === 'string') { 235 | name = camelize(val); 236 | res[name] = { type: null }; 237 | } 238 | } 239 | } else if (isPlainObject(props)) { 240 | for (const key in props) { 241 | val = props[key]; 242 | name = camelize(key); 243 | res[name] = isPlainObject(val) 244 | ? val 245 | : { type: val }; 246 | } 247 | } 248 | options.props = res; 249 | } 250 | 251 | /** 252 | * Merge two option objects into a new one. 253 | * Core utility used in both instantiation and inheritance. 254 | */ 255 | function mergeOptions ( 256 | parent, 257 | child, 258 | vm 259 | ){ 260 | if (typeof child === 'function') { 261 | child = child.options; 262 | } 263 | normalizeProps(child, vm); 264 | 265 | // normalizeInject(child, vm) 266 | // normalizeDirectives(child) 267 | const extendsFrom = child.extends; 268 | if (extendsFrom) { 269 | parent = mergeOptions(parent, extendsFrom, vm); 270 | } 271 | if (child.mixins) { 272 | for (let i = 0, l = child.mixins.length; i < l; i++) { 273 | parent = mergeOptions(parent, child.mixins[i], vm); 274 | } 275 | } 276 | const options = {}; 277 | let key; 278 | for (key in parent) { 279 | mergeField(key); 280 | } 281 | for (key in child) { 282 | if (!hasOwn(parent, key)) { 283 | mergeField(key); 284 | } 285 | } 286 | function mergeField (key) { 287 | 288 | const strat = strats[key] || defaultStrat; 289 | options[key] = strat(parent[key], child[key], vm, key); 290 | } 291 | return options 292 | } 293 | 294 | /* @flow */ 295 | 296 | function extractPropsFromVNodeData( 297 | data, 298 | Ctor, 299 | tag 300 | ) { 301 | // we are only extracting raw values here. 302 | // validation and default values are handled in the child 303 | // component itself. 304 | const propOptions = Ctor.options.props; 305 | if (isUndef(propOptions)) { 306 | return 307 | } 308 | const res = {}; 309 | const { attrs, props } = data; 310 | if (isDef(attrs) || isDef(props)) { 311 | for (const key in propOptions) { 312 | const altKey = hyphenate(key); 313 | checkProp(res, props, key, altKey, true) || 314 | checkProp(res, attrs, key, altKey, false); 315 | } 316 | } 317 | return res 318 | } 319 | 320 | function checkProp( 321 | res, 322 | hash, 323 | key, 324 | altKey, 325 | preserve 326 | ) { 327 | if (isDef(hash)) { 328 | if (hasOwn(hash, key)) { 329 | res[key] = hash[key]; 330 | if (!preserve) { 331 | delete hash[key]; 332 | } 333 | return true 334 | } else if (hasOwn(hash, altKey)) { 335 | res[key] = hash[altKey]; 336 | if (!preserve) { 337 | delete hash[altKey]; 338 | } 339 | return true 340 | } 341 | } 342 | return false 343 | } 344 | 345 | // create的时候主要是返回VNode,真正的创建在render的时候。 346 | // 这个文件主要包括一个createComponent函数(返回VNode),和一组Component占位VNode专用的VNode钩子。 347 | // 在patch的时候,比如init的时候,这个钩子就会调用createComponentInstanceForVnode初始化节点 348 | 349 | function createComponent ( Ctor, data, context, children, tag){ 350 | if (isUndef(Ctor)) { 351 | return 352 | } 353 | 354 | const baseCtor = context.$options._base; 355 | 356 | // plain options object: turn it into a constructor 357 | if (isObject(Ctor)) { 358 | Ctor = baseCtor.extend(Ctor); 359 | } 360 | 361 | // resolve constructor options in case global mixins are applied after 362 | // component constructor creation 363 | // resolveConstructorOptions(Ctor) 364 | 365 | data = data || {}; 366 | 367 | // // transform component v-model data into props & events 368 | // if (isDef(data.model)) { 369 | // transformModel(Ctor.options, data) 370 | // } 371 | 372 | // extract props 373 | const propsData = extractPropsFromVNodeData(data, Ctor, tag); 374 | 375 | // // functional component 376 | // if (isTrue(Ctor.options.functional)) { 377 | // return createFunctionalComponent(Ctor, propsData, data, context, children) 378 | // } 379 | 380 | // extract listeners, since these needs to be treated as 381 | // child component listeners instead of DOM listeners 382 | const listeners = data.on; 383 | // replace with listeners with .native modifier 384 | data.on = data.nativeOn; 385 | 386 | // if (isTrue(Ctor.options.abstract)) { 387 | // // abstract components do not keep anything 388 | // // other than props & listeners 389 | // data = {} 390 | // } 391 | 392 | // merge component management hooks onto the placeholder node 393 | // mergeHooks(data) 394 | 395 | // return a placeholder vnode 396 | const name = Ctor.options.name || tag; 397 | const vnode = new VNode( 398 | `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`, 399 | data, undefined, undefined, undefined, context, 400 | { Ctor, propsData, listeners, tag, children } 401 | ); 402 | return vnode 403 | } 404 | 405 | /** 406 | * normalization这一步应该是在Compile的时候就已经做了,这里我们先不加相关的处理 407 | */ 408 | 409 | 410 | function createElement( 411 | context, 412 | tag, 413 | data, 414 | children 415 | ) { 416 | return _createElement(context, tag, data, children); 417 | } 418 | 419 | function _createElement( 420 | context, 421 | tag, 422 | data, 423 | children 424 | ) { 425 | // if (normalizationType === ALWAYS_NORMALIZE) { 426 | // children = normalizeChildren(children) 427 | // } else if (normalizationType === SIMPLE_NORMALIZE) { 428 | // children = simpleNormalizeChildren(children) 429 | // } 430 | let vnode; 431 | if (typeof tag === "string") { 432 | let Ctor; 433 | if (config.isReservedTag(tag)) { 434 | // platform built-in elements 435 | vnode = new VNode( 436 | config.parsePlatformTagName(tag), 437 | data, 438 | children, 439 | undefined, 440 | undefined, 441 | context 442 | ); 443 | } else if ( 444 | isDef((Ctor = resolveAsset(context.$options, "components", tag))) 445 | ) { 446 | // component 447 | vnode = createComponent(Ctor, data, context, children, tag); 448 | } else { 449 | // unknown or unlisted namespaced elements 450 | // check at runtime because it may get assigned a namespace when its 451 | // parent normalizes children 452 | vnode = new VNode(tag, data, children, undefined, undefined, context); 453 | } 454 | } else { 455 | // direct component options / constructor 456 | vnode = createComponent(tag, data, context, children); 457 | } 458 | if (!isDef(vnode)) { 459 | return createEmptyVNode(); 460 | } 461 | return vnode; 462 | } 463 | 464 | function initRender (vm) { 465 | vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false); 466 | vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true); 467 | } 468 | 469 | function renderMixin (Vue) { 470 | Vue.prototype.$nextTick = function (fn) { 471 | 472 | }; 473 | Vue.prototype._render = function () { 474 | 475 | }; 476 | } 477 | 478 | var uid = 0; 479 | 480 | function Dep(argument) { 481 | this.id = uid++; 482 | this.subs = []; 483 | } 484 | 485 | Dep.prototype.addSub = function(sub) { 486 | this.subs.push(sub); 487 | }; 488 | 489 | Dep.prototype.removeSub = function(sub) { 490 | remove(this.subs, sub); 491 | }; 492 | 493 | Dep.prototype.depend = function() { 494 | if (Dep.target) { 495 | Dep.target.addDep(this); 496 | } 497 | }; 498 | 499 | Dep.prototype.notify = function() { 500 | var subs = this.subs.slice(); 501 | for (var i = 0, l = subs.length; i < l; i++) { 502 | subs[i].update(); 503 | } 504 | }; 505 | 506 | Dep.target = null; 507 | var targetStack = []; 508 | 509 | function pushTarget (_target) { 510 | if (Dep.target) targetStack.push(Dep.target); 511 | Dep.target = _target; 512 | } 513 | 514 | function popTarget () { 515 | Dep.target = targetStack.pop(); 516 | } 517 | 518 | let uid$1 = 0; 519 | 520 | function Watcher(vm, expOrFn, cb, options) { 521 | options = options ? options : {}; 522 | this.vm = vm; 523 | vm._watchers.push(this); 524 | this.cb = cb; 525 | this.id = ++uid$1; 526 | // options 527 | this.deep = !!options.deep; 528 | this.user = !!options.user; 529 | this.lazy = !!options.lazy; 530 | this.sync = !!options.sync; 531 | this.deps = []; 532 | this.newDeps = []; 533 | this.depIds = new Set(); 534 | this.newDepIds = new Set(); 535 | if (typeof expOrFn === 'function') { 536 | this.getter = expOrFn; 537 | } 538 | this.value = this.lazy ? undefined : this.get(); 539 | } 540 | 541 | Watcher.prototype.get = function() { 542 | pushTarget(this); 543 | var value = this.getter.call(this.vm, this.vm); 544 | // "touch" every property so they are all tracked as 545 | // dependencies for deep watching 546 | // if (this.deep) { 547 | // traverse(value) 548 | // } 549 | popTarget(); 550 | this.cleanupDeps(); 551 | return value 552 | }; 553 | 554 | /** 555 | * Add a dependency to this directive. 556 | */ 557 | Watcher.prototype.addDep = function(dep) { 558 | var id = dep.id; 559 | if (!this.newDepIds.has(id)) { 560 | this.newDepIds.add(id); 561 | this.newDeps.push(dep); 562 | if (!this.depIds.has(id)) { 563 | dep.addSub(this); 564 | } 565 | } 566 | }; 567 | 568 | Watcher.prototype.update = function() { 569 | this.run(); 570 | }; 571 | 572 | Watcher.prototype.run = function() { 573 | var value = this.get(); 574 | var oldValue = this.value; 575 | this.value = value; 576 | this.cb.call(this.vm, value, oldValue); 577 | }; 578 | 579 | /** 580 | * Clean up for dependency collection. 581 | */ 582 | Watcher.prototype.cleanupDeps = function() { 583 | var i = this.deps.length; 584 | while (i--) { 585 | var dep = this.deps[i]; 586 | if (!this.newDepIds.has(dep.id)) { 587 | dep.removeSub(this); 588 | } 589 | } 590 | var tmp = this.depIds; 591 | this.depIds = this.newDepIds; 592 | this.newDepIds = tmp; 593 | this.newDepIds.clear(); 594 | tmp = this.deps; 595 | this.deps = this.newDeps; 596 | this.newDeps = []; 597 | }; 598 | 599 | /* 600 | * not type checking this file because flow doesn't play well with 601 | * dynamically accessing methods on Array prototype 602 | */ 603 | 604 | const arrayProto = Array.prototype; 605 | const arrayMethods = Object.create(arrayProto) 606 | 607 | /** 608 | * Intercept mutating methods and emit events 609 | */ 610 | ;[ 611 | 'push', 612 | 'pop', 613 | 'shift', 614 | 'unshift', 615 | 'splice', 616 | 'sort', 617 | 'reverse' 618 | ] 619 | .forEach(function (method) { 620 | // cache original method 621 | const original = arrayProto[method]; 622 | def(arrayMethods, method, function mutator () { 623 | // avoid leaking arguments: 624 | // http://jsperf.com/closure-with-arguments 625 | let i = arguments.length; 626 | const args = new Array(i); 627 | while (i--) { 628 | args[i] = arguments[i]; 629 | } 630 | const result = original.apply(this, args); 631 | const ob = this.__ob__; 632 | let inserted; 633 | switch (method) { 634 | case 'push': 635 | inserted = args; 636 | break 637 | case 'unshift': 638 | inserted = args; 639 | break 640 | case 'splice': 641 | inserted = args.slice(2); 642 | break 643 | } 644 | if (inserted) ob.observeArray(inserted); 645 | // notify change 646 | ob.dep.notify(); 647 | return result 648 | }); 649 | }); 650 | 651 | var arrayKeys = Object.getOwnPropertyNames(arrayMethods); 652 | 653 | function Observer(value) { 654 | this.value = value; 655 | this.dep = new Dep(); 656 | //this.walk(value) 657 | if(Array.isArray(value)){ 658 | var augment = hasProto 659 | ? protoAugment 660 | : copyAugment; 661 | augment(value, arrayMethods, arrayKeys); 662 | this.observeArray(value); 663 | }else{ 664 | this.walk(value); 665 | } 666 | def(value, '__ob__', this); 667 | } 668 | 669 | Observer.prototype.walk = function(obj) { 670 | var keys = Object.keys(obj); 671 | for (var i = 0; i < keys.length; i++) { 672 | defineReactive(obj, keys[i], obj[keys[i]]); 673 | } 674 | }; 675 | 676 | Observer.prototype.observeArray = function(items) { 677 | for (let i = 0, l = items.length; i < l; i++) { 678 | observe(items[i]); 679 | } 680 | }; 681 | 682 | function observe(value) { 683 | if (!isObject(value)) { 684 | return 685 | } 686 | var ob; 687 | if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { 688 | ob = value.__ob__; 689 | } else { 690 | ob = new Observer(value); 691 | } 692 | return ob 693 | } 694 | 695 | function defineReactive(obj, key, val) { 696 | var dep = new Dep(); 697 | var childOb = observe(val); 698 | Object.defineProperty(obj, key, { 699 | enumerable: true, 700 | configurable: true, 701 | get: function reactiveGetter() { 702 | var value = val; 703 | if (Dep.target) { 704 | dep.depend(); 705 | if (childOb) { 706 | childOb.dep.depend(); 707 | } 708 | // if (Array.isArray(value)) { 709 | // dependArray(value) 710 | // } 711 | } 712 | return value 713 | }, 714 | set: function reactiveSetter(newVal) { 715 | var value = val; 716 | if (newVal === value || (newVal !== newVal && value !== value)) { 717 | return 718 | } 719 | val = newVal; 720 | childOb = observe(newVal); 721 | dep.notify(); 722 | } 723 | }); 724 | } 725 | 726 | // helpers 727 | 728 | /** 729 | * Augment an target Object or Array by intercepting 730 | * the prototype chain using __proto__ 731 | */ 732 | function protoAugment (target, src) { 733 | /* eslint-disable no-proto */ 734 | target.__proto__ = src; 735 | /* eslint-enable no-proto */ 736 | } 737 | 738 | /** 739 | * Augment an target Object or Array by defining 740 | * hidden properties. 741 | * 742 | * istanbul ignore next 743 | */ 744 | function copyAugment (target, src, keys) { 745 | for (let i = 0, l = keys.length; i < l; i++) { 746 | var key = keys[i]; 747 | def(target, key, src[key]); 748 | } 749 | } 750 | 751 | function initState(vm) { 752 | vm._watchers = []; 753 | //initProps(vm) 754 | //initMethods(vm) 755 | initData(vm); 756 | //initComputed(vm) 757 | //initWatch(vm) 758 | } 759 | 760 | function initData(vm) { 761 | var data = vm.$options.data; 762 | data = vm._data = typeof data === 'function' 763 | ? getData(data, vm) 764 | : data || {}; 765 | // proxy data on instance 766 | var keys = Object.keys(data); 767 | 768 | var i = keys.length; 769 | while (i--) { 770 | proxy(vm, keys[i]); 771 | } 772 | 773 | // observe data 774 | observe(data); 775 | } 776 | 777 | function getData(data, vm) { 778 | return data.call(vm, vm) 779 | } 780 | 781 | function proxy(vm, key) { 782 | if (!isReserved(key)) { 783 | Object.defineProperty(vm, key, { 784 | configurable: true, 785 | enumerable: true, 786 | get: function proxyGetter() { 787 | return vm._data[key] 788 | }, 789 | set: function proxySetter(val) { 790 | vm._data[key] = val; 791 | } 792 | }); 793 | } 794 | } 795 | 796 | function initLifecycle(vm) { 797 | vm._watcher = null; 798 | } 799 | 800 | function lifecycleMixin(Vue) { 801 | Vue.prototype._update = function (vnode) { 802 | 803 | }; 804 | Vue.prototype.$mount = function(el) { 805 | 806 | var vm = this; 807 | vm._watcher = new Watcher(vm, function(){ 808 | console.log(vm.a, "update!!!"); 809 | }, noop); 810 | return vm 811 | }; 812 | Vue.prototype.$destroy = function() { 813 | 814 | }; 815 | } 816 | 817 | function initMixin(Vue) { 818 | Vue.prototype._init = function (options) { 819 | var vm = this; 820 | // vm.$options = options; 821 | vm.$options = mergeOptions( 822 | resolveConstructorOptions(vm.constructor), 823 | options || {}, 824 | vm 825 | ); 826 | 827 | // should be in global api 828 | vm.$options._base = Vue; 829 | 830 | initLifecycle(vm); 831 | initState(vm); 832 | initRender(vm); 833 | 834 | vm.$mount(options); 835 | }; 836 | } 837 | 838 | function resolveConstructorOptions(Ctor) { 839 | let options = Ctor.options; 840 | // if (Ctor.super) { 841 | // const superOptions = resolveConstructorOptions(Ctor.super) 842 | // const cachedSuperOptions = Ctor.superOptions 843 | // if (superOptions !== cachedSuperOptions) { 844 | // // super option changed, 845 | // // need to resolve new options. 846 | // Ctor.superOptions = superOptions 847 | // // check if there are any late-modified/attached options (#4976) 848 | // const modifiedOptions = resolveModifiedOptions(Ctor) 849 | // // update base extend options 850 | // if (modifiedOptions) { 851 | // extend(Ctor.extendOptions, modifiedOptions) 852 | // } 853 | // options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions) 854 | // if (options.name) { 855 | // options.components[options.name] = Ctor 856 | // } 857 | // } 858 | // } 859 | return options 860 | } 861 | 862 | function Vue (options) { 863 | this._init(options); 864 | } 865 | 866 | initMixin(Vue); 867 | lifecycleMixin(Vue); 868 | renderMixin(Vue); 869 | 870 | return Vue; 871 | 872 | }()); 873 | -------------------------------------------------------------------------------- /code/example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Vue test 7 | 13 | 14 | 15 | 16 | 17 | 18 | 20 | 23 | 27 | 28 | 29 | --> 38 | 39 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /code/karma.conf.js: -------------------------------------------------------------------------------- 1 | const alias = require('rollup-plugin-alias'); 2 | const path = require('path') 3 | const resolve = p => path.resolve(__dirname, './', p) 4 | 5 | module.exports = function(config) { 6 | config.set({ 7 | files: [{ pattern: 'test/**/*.spec.js', watched: false }], 8 | frameworks: ['jasmine'], 9 | 10 | browsers: ['Chrome'], 11 | preprocessors: { 12 | './test/**/*.js': ['rollup'] 13 | }, 14 | rollupPreprocessor: { 15 | plugins: [ 16 | require('rollup-plugin-buble')(), 17 | alias({ 18 | core: resolve('src/'), 19 | src: resolve('src/'), 20 | shared: resolve('src/shared') 21 | }) 22 | ], 23 | output: { 24 | format: 'iife', 25 | name: 'Vue', 26 | sourcemap: 'inline' 27 | } 28 | } 29 | }) 30 | } -------------------------------------------------------------------------------- /code/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "build-your-own-vuejs", 3 | "version": "1.0.0", 4 | "description": "Build Vuejs from scratch to learn how it works ", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "rollup -c", 8 | "watch": "rollup -c -w", 9 | "test": "karma start" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/jsrebuild/build-your-own-vuejs.git" 14 | }, 15 | "keywords": [ 16 | "vuejs" 17 | ], 18 | "author": "zxc0328", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/jsrebuild/build-your-own-vuejs/issues" 22 | }, 23 | "homepage": "https://github.com/jsrebuild/build-your-own-vuejs#readme", 24 | "dependencies": {}, 25 | "devDependencies": { 26 | "buble": "^0.14.2", 27 | "jasmine": "^3.1.0", 28 | "karma": "^2.0.2", 29 | "karma-chrome-launcher": "^2.2.0", 30 | "karma-jasmine": "^1.1.2", 31 | "karma-rollup-plugin": "^0.2.4", 32 | "karma-rollup-preprocessor": "^6.0.0", 33 | "rollup": "^0.59.1", 34 | "rollup-plugin-alias": "^1.4.0", 35 | "rollup-plugin-buble": "^0.19.2", 36 | "rollup-watch": "^2.5.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /code/rollup.config.js: -------------------------------------------------------------------------------- 1 | const alias = require('rollup-plugin-alias'); 2 | const path = require('path') 3 | const resolve = p => path.resolve(__dirname, './', p) 4 | 5 | export default { 6 | input: 'src/instance/index.js', 7 | output: { 8 | name: 'Vue', 9 | file: 'dist/vue.js', 10 | format: 'iife' 11 | }, 12 | plugins: [ 13 | alias({ 14 | core: resolve('src/'), 15 | shared: resolve('src/shared') 16 | }) 17 | ] 18 | }; -------------------------------------------------------------------------------- /code/src/config.js: -------------------------------------------------------------------------------- 1 | import { 2 | makeMap, 3 | identity 4 | } from './shared/util' 5 | 6 | const isHTMLTag = makeMap( 7 | 'html,body,base,head,link,meta,style,title,' + 8 | 'address,article,aside,footer,header,h1,h2,h3,h4,h5,h6,hgroup,nav,section,' + 9 | 'div,dd,dl,dt,figcaption,figure,picture,hr,img,li,main,ol,p,pre,ul,' + 10 | 'a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,rtc,ruby,' + 11 | 's,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video,' + 12 | 'embed,object,param,source,canvas,script,noscript,del,ins,' + 13 | 'caption,col,colgroup,table,thead,tbody,td,th,tr,' + 14 | 'button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,' + 15 | 'output,progress,select,textarea,' + 16 | 'details,dialog,menu,menuitem,summary,' + 17 | 'content,element,shadow,template,blockquote,iframe,tfoot' 18 | ) 19 | 20 | // this map is intentionally selective, only covering SVG elements that may 21 | // contain child elements. 22 | const isSVG = makeMap( 23 | 'svg,animate,circle,clippath,cursor,defs,desc,ellipse,filter,font-face,' + 24 | 'foreignObject,g,glyph,image,line,marker,mask,missing-glyph,path,pattern,' + 25 | 'polygon,polyline,rect,switch,symbol,text,textpath,tspan,use,view', 26 | true 27 | ) 28 | 29 | 30 | export default ({ 31 | /** 32 | * Check if a tag is reserved so that it cannot be registered as a 33 | * component. This is platform-dependent and may be overwritten. 34 | */ 35 | isReservedTag: (tag) => { 36 | return isHTMLTag(tag) || isSVG(tag) 37 | }, 38 | 39 | /** 40 | * Parse the real tag name for the specific platform. 41 | */ 42 | parsePlatformTagName: identity, 43 | }) 44 | -------------------------------------------------------------------------------- /code/src/global-api/extend.js: -------------------------------------------------------------------------------- 1 | import { ASSET_TYPES } from '../shared/constants' 2 | import { extend, mergeOptions } from '../util/index' 3 | import { proxy } from '../instance/state' 4 | 5 | export function initExtend (Vue) { 6 | /** 7 | * Each instance constructor, including Vue, has a unique 8 | * cid. This enables us to create wrapped "child 9 | * constructors" for prototypal inheritance and cache them. 10 | */ 11 | Vue.cid = 0 12 | let cid = 1 13 | 14 | /** 15 | * Class inheritance 16 | */ 17 | Vue.extend = function (extendOptions){ 18 | extendOptions = extendOptions || {} 19 | const Super = this 20 | const SuperId = Super.cid 21 | const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {}) 22 | if (cachedCtors[SuperId]) { 23 | return cachedCtors[SuperId] 24 | } 25 | 26 | const name = extendOptions.name || Super.options.name 27 | 28 | const Sub = function VueComponent (options) { 29 | this._init(options) 30 | } 31 | Sub.prototype = Object.create(Super.prototype) 32 | Sub.prototype.constructor = Sub 33 | Sub.cid = cid++ 34 | Sub.options = mergeOptions( 35 | Super.options, 36 | extendOptions 37 | ) 38 | Sub['super'] = Super 39 | 40 | // For props and computed properties, we define the proxy getters on 41 | // the Vue instances at extension time, on the extended prototype. This 42 | // avoids Object.defineProperty calls for each instance created. 43 | if (Sub.options.props) { 44 | initProps(Sub) 45 | } 46 | if (Sub.options.computed) { 47 | initComputed(Sub) 48 | } 49 | 50 | // allow further extension/mixin/plugin usage 51 | Sub.extend = Super.extend 52 | Sub.mixin = Super.mixin 53 | Sub.use = Super.use 54 | 55 | // create asset registers, so extended classes 56 | // can have their private assets too. 57 | ASSET_TYPES.forEach(function (type) { 58 | Sub[type] = Super[type] 59 | }) 60 | // enable recursive self-lookup 61 | if (name) { 62 | Sub.options.components[name] = Sub 63 | } 64 | 65 | // keep a reference to the super options at extension time. 66 | // later at instantiation we can check if Super's options have 67 | // been updated. 68 | Sub.superOptions = Super.options 69 | Sub.extendOptions = extendOptions 70 | Sub.sealedOptions = extend({}, Sub.options) 71 | 72 | // cache constructor 73 | cachedCtors[SuperId] = Sub 74 | return Sub 75 | } 76 | } 77 | 78 | function initProps (Comp) { 79 | const props = Comp.options.props 80 | for (const key in props) { 81 | proxy(Comp.prototype, `_props`, key) 82 | } 83 | } 84 | 85 | // function initComputed (Comp) { 86 | // const computed = Comp.options.computed 87 | // for (const key in computed) { 88 | // defineComputed(Comp.prototype, key, computed[key]) 89 | // } 90 | // } 91 | -------------------------------------------------------------------------------- /code/src/global-api/index.js: -------------------------------------------------------------------------------- 1 | import { initExtend } from './extend' 2 | import { ASSET_TYPES } from '../shared/constants' 3 | import { extend } from '../shared/util' 4 | 5 | export function initGlobalAPI (Vue) { 6 | Vue.options = Object.create(null) 7 | ASSET_TYPES.forEach(type => { 8 | Vue.options[type + 's'] = Object.create(null) 9 | }) 10 | 11 | // this is used to identify the "base" constructor to extend all plain-object 12 | // components with in Weex's multi-instance scenarios. 13 | Vue.options._base = Vue 14 | 15 | // if (!Vue.options.components) { 16 | // Vue.options.components = {} 17 | // } 18 | 19 | initExtend(Vue) 20 | } -------------------------------------------------------------------------------- /code/src/index.js: -------------------------------------------------------------------------------- 1 | import Vue from './instance/index' 2 | import { initGlobalAPI } from './global-api/index' 3 | 4 | initGlobalAPI(Vue) 5 | 6 | Vue.version = '__VERSION__' 7 | 8 | export default Vue -------------------------------------------------------------------------------- /code/src/instance/event.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsrebuild/build-your-own-vuejs/aa273934889be7d0900e23135e5d7b5cf2d30a88/code/src/instance/event.js -------------------------------------------------------------------------------- /code/src/instance/index.js: -------------------------------------------------------------------------------- 1 | import { initMixin } from './init' 2 | import { lifecycleMixin } from './lifecycle' 3 | import { stateMixin } from './state' 4 | import { renderMixin } from './render' 5 | 6 | function Vue (options) { 7 | this._init(options) 8 | } 9 | 10 | initMixin(Vue) 11 | stateMixin(Vue) 12 | lifecycleMixin(Vue) 13 | renderMixin(Vue) 14 | 15 | export default Vue -------------------------------------------------------------------------------- /code/src/instance/init.js: -------------------------------------------------------------------------------- 1 | import { initRender } from './render' 2 | import { initState } from './state' 3 | import { initLifecycle } from './lifecycle' 4 | import { mergeOptions } from '../util/index' 5 | 6 | export function initMixin(Vue) { 7 | Vue.prototype._init = function (options) { 8 | var vm = this 9 | // vm.$options = options; 10 | vm.$options = mergeOptions( 11 | resolveConstructorOptions(vm.constructor), 12 | options || {}, 13 | vm 14 | ) 15 | 16 | // should be in global api 17 | vm.$options._base = Vue 18 | 19 | initLifecycle(vm) 20 | initState(vm) 21 | initRender(vm) 22 | 23 | vm.$mount(options) 24 | } 25 | } 26 | 27 | export function resolveConstructorOptions(Ctor) { 28 | let options = Ctor.options 29 | // if (Ctor.super) { 30 | // const superOptions = resolveConstructorOptions(Ctor.super) 31 | // const cachedSuperOptions = Ctor.superOptions 32 | // if (superOptions !== cachedSuperOptions) { 33 | // // super option changed, 34 | // // need to resolve new options. 35 | // Ctor.superOptions = superOptions 36 | // // check if there are any late-modified/attached options (#4976) 37 | // const modifiedOptions = resolveModifiedOptions(Ctor) 38 | // // update base extend options 39 | // if (modifiedOptions) { 40 | // extend(Ctor.extendOptions, modifiedOptions) 41 | // } 42 | // options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions) 43 | // if (options.name) { 44 | // options.components[options.name] = Ctor 45 | // } 46 | // } 47 | // } 48 | return options 49 | } -------------------------------------------------------------------------------- /code/src/instance/lifecycle.js: -------------------------------------------------------------------------------- 1 | import { noop } from '../util/index' 2 | import Watcher from '../observer/watcher' 3 | 4 | export function initLifecycle(vm) { 5 | vm._watcher = null 6 | } 7 | 8 | export function lifecycleMixin(Vue) { 9 | Vue.prototype._update = function (vnode) { 10 | 11 | } 12 | Vue.prototype.$mount = function(el) { 13 | 14 | var vm = this 15 | vm._watcher = new Watcher(vm, function(){ 16 | console.log(vm.a, "update!!!") 17 | }, noop) 18 | return vm 19 | } 20 | Vue.prototype.$destroy = function() { 21 | 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /code/src/instance/render.js: -------------------------------------------------------------------------------- 1 | import { createElement } from '../vdom/create-element' 2 | 3 | export function initRender (vm) { 4 | vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false) 5 | vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true) 6 | } 7 | 8 | export function renderMixin (Vue) { 9 | Vue.prototype.$nextTick = function (fn) { 10 | 11 | } 12 | Vue.prototype._render = function () { 13 | 14 | } 15 | } -------------------------------------------------------------------------------- /code/src/instance/state.js: -------------------------------------------------------------------------------- 1 | import Watcher from '../observer/watcher' 2 | import Dep from '../observer/dep' 3 | 4 | import { 5 | observe 6 | } from '../observer/index' 7 | 8 | import { 9 | isReserved 10 | } from '../util/index' 11 | 12 | export function initState(vm) { 13 | vm._watchers = [] 14 | //initProps(vm) 15 | //initMethods(vm) 16 | initData(vm) 17 | //initComputed(vm) 18 | //initWatch(vm) 19 | } 20 | 21 | function initData(vm) { 22 | var data = vm.$options.data 23 | data = vm._data = typeof data === 'function' 24 | ? getData(data, vm) 25 | : data || {} 26 | // proxy data on instance 27 | var keys = Object.keys(data) 28 | 29 | var i = keys.length 30 | while (i--) { 31 | proxy(vm, keys[i]) 32 | } 33 | 34 | // observe data 35 | observe(data) 36 | } 37 | 38 | function getData(data, vm) { 39 | return data.call(vm, vm) 40 | } 41 | 42 | export function stateMixin(Vue) { 43 | 44 | } 45 | 46 | export function proxy(vm, key) { 47 | if (!isReserved(key)) { 48 | Object.defineProperty(vm, key, { 49 | configurable: true, 50 | enumerable: true, 51 | get: function proxyGetter() { 52 | return vm._data[key] 53 | }, 54 | set: function proxySetter(val) { 55 | vm._data[key] = val 56 | } 57 | }) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /code/src/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 | // cache original method 25 | const original = arrayProto[method] 26 | def(arrayMethods, method, function mutator () { 27 | // avoid leaking arguments: 28 | // http://jsperf.com/closure-with-arguments 29 | let i = arguments.length 30 | const args = new Array(i) 31 | while (i--) { 32 | args[i] = arguments[i] 33 | } 34 | const result = original.apply(this, args) 35 | const ob = this.__ob__ 36 | let inserted 37 | switch (method) { 38 | case 'push': 39 | inserted = args 40 | break 41 | case 'unshift': 42 | inserted = args 43 | break 44 | case 'splice': 45 | inserted = args.slice(2) 46 | break 47 | } 48 | if (inserted) ob.observeArray(inserted) 49 | // notify change 50 | ob.dep.notify() 51 | return result 52 | }) 53 | }) -------------------------------------------------------------------------------- /code/src/observer/dep.js: -------------------------------------------------------------------------------- 1 | var uid = 0 2 | 3 | export default function Dep(argument) { 4 | this.id = uid++ 5 | this.subs = [] 6 | } 7 | 8 | Dep.prototype.addSub = function(sub) { 9 | this.subs.push(sub) 10 | } 11 | 12 | Dep.prototype.removeSub = function(sub) { 13 | remove(this.subs, sub) 14 | } 15 | 16 | Dep.prototype.depend = function() { 17 | if (Dep.target) { 18 | Dep.target.addDep(this) 19 | } 20 | } 21 | 22 | Dep.prototype.notify = function() { 23 | var subs = this.subs.slice() 24 | for (var i = 0, l = subs.length; i < l; i++) { 25 | subs[i].update() 26 | } 27 | } 28 | 29 | Dep.target = null 30 | var targetStack = [] 31 | 32 | export function pushTarget (_target) { 33 | if (Dep.target) targetStack.push(Dep.target) 34 | Dep.target = _target 35 | } 36 | 37 | export function popTarget () { 38 | Dep.target = targetStack.pop() 39 | } -------------------------------------------------------------------------------- /code/src/observer/index.js: -------------------------------------------------------------------------------- 1 | import Dep from './dep' 2 | import { 3 | def, 4 | hasOwn, 5 | hasProto, 6 | isObject 7 | } 8 | from '../util/index' 9 | import { arrayMethods } from './array' 10 | 11 | var arrayKeys = Object.getOwnPropertyNames(arrayMethods) 12 | 13 | export function Observer(value) { 14 | this.value = value 15 | this.dep = new Dep() 16 | //this.walk(value) 17 | if(Array.isArray(value)){ 18 | var augment = hasProto 19 | ? protoAugment 20 | : copyAugment 21 | augment(value, arrayMethods, arrayKeys) 22 | this.observeArray(value) 23 | }else{ 24 | this.walk(value) 25 | } 26 | def(value, '__ob__', this) 27 | } 28 | 29 | Observer.prototype.walk = function(obj) { 30 | var keys = Object.keys(obj) 31 | for (var i = 0; i < keys.length; i++) { 32 | defineReactive(obj, keys[i], obj[keys[i]]) 33 | } 34 | } 35 | 36 | Observer.prototype.observeArray = function(items) { 37 | for (let i = 0, l = items.length; i < l; i++) { 38 | observe(items[i]) 39 | } 40 | } 41 | 42 | export function observe(value) { 43 | if (!isObject(value)) { 44 | return 45 | } 46 | var ob 47 | if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { 48 | ob = value.__ob__ 49 | } else { 50 | ob = new Observer(value) 51 | } 52 | return ob 53 | } 54 | 55 | export function defineReactive(obj, key, val) { 56 | var dep = new Dep() 57 | var childOb = observe(val) 58 | Object.defineProperty(obj, key, { 59 | enumerable: true, 60 | configurable: true, 61 | get: function reactiveGetter() { 62 | var value = val 63 | if (Dep.target) { 64 | dep.depend() 65 | if (childOb) { 66 | childOb.dep.depend() 67 | } 68 | // if (Array.isArray(value)) { 69 | // dependArray(value) 70 | // } 71 | } 72 | return value 73 | }, 74 | set: function reactiveSetter(newVal) { 75 | var value = val 76 | if (newVal === value || (newVal !== newVal && value !== value)) { 77 | return 78 | } 79 | val = newVal 80 | childOb = observe(newVal) 81 | dep.notify() 82 | } 83 | }) 84 | } 85 | 86 | // helpers 87 | 88 | /** 89 | * Augment an target Object or Array by intercepting 90 | * the prototype chain using __proto__ 91 | */ 92 | function protoAugment (target, src) { 93 | /* eslint-disable no-proto */ 94 | target.__proto__ = src 95 | /* eslint-enable no-proto */ 96 | } 97 | 98 | /** 99 | * Augment an target Object or Array by defining 100 | * hidden properties. 101 | * 102 | * istanbul ignore next 103 | */ 104 | function copyAugment (target, src, keys) { 105 | for (let i = 0, l = keys.length; i < l; i++) { 106 | var key = keys[i] 107 | def(target, key, src[key]) 108 | } 109 | } 110 | 111 | /** 112 | * Set a property on an object. Adds the new property and 113 | * triggers change notification if the property doesn't 114 | * already exist. 115 | */ 116 | export function set(obj, key, val) { 117 | // if (Array.isArray(obj)) { 118 | // obj.length = Math.max(obj.length, key) 119 | // obj.splice(key, 1, val) 120 | // return val 121 | // } 122 | if (hasOwn(obj, key)) { 123 | obj[key] = val 124 | return 125 | } 126 | const ob = obj.__ob__ 127 | if (!ob) { 128 | obj[key] = val 129 | return 130 | } 131 | defineReactive(ob.value, key, val) 132 | ob.dep.notify() 133 | return val 134 | } 135 | 136 | /** 137 | * Delete a property and trigger change if necessary. 138 | */ 139 | export function del(obj, key) { 140 | const ob = obj.__ob__ 141 | if (!hasOwn(obj, key)) { 142 | return 143 | } 144 | delete obj[key] 145 | if (!ob) { 146 | return 147 | } 148 | ob.dep.notify() 149 | } 150 | 151 | function dependArray (value) { 152 | for (let e, i = 0, l = value.length; i < l; i++) { 153 | e = value[i] 154 | e && e.__ob__ && e.__ob__.dep.depend() 155 | if (Array.isArray(e)) { 156 | dependArray(e) 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /code/src/observer/watcher.js: -------------------------------------------------------------------------------- 1 | import Dep, { 2 | pushTarget, popTarget 3 | } 4 | from './dep' 5 | 6 | let uid = 0 7 | 8 | export default function Watcher(vm, expOrFn, cb, options) { 9 | options = options ? options : {} 10 | this.vm = vm 11 | vm._watchers.push(this) 12 | this.cb = cb 13 | this.id = ++uid 14 | // options 15 | this.deep = !!options.deep 16 | this.user = !!options.user 17 | this.lazy = !!options.lazy 18 | this.sync = !!options.sync 19 | this.deps = [] 20 | this.newDeps = [] 21 | this.depIds = new Set() 22 | this.newDepIds = new Set() 23 | if (typeof expOrFn === 'function') { 24 | this.getter = expOrFn 25 | } 26 | this.value = this.lazy ? undefined : this.get() 27 | } 28 | 29 | Watcher.prototype.get = function() { 30 | pushTarget(this) 31 | var value = this.getter.call(this.vm, this.vm) 32 | // "touch" every property so they are all tracked as 33 | // dependencies for deep watching 34 | // if (this.deep) { 35 | // traverse(value) 36 | // } 37 | popTarget() 38 | this.cleanupDeps() 39 | return value 40 | } 41 | 42 | /** 43 | * Add a dependency to this directive. 44 | */ 45 | Watcher.prototype.addDep = function(dep) { 46 | var id = dep.id 47 | if (!this.newDepIds.has(id)) { 48 | this.newDepIds.add(id) 49 | this.newDeps.push(dep) 50 | if (!this.depIds.has(id)) { 51 | dep.addSub(this) 52 | } 53 | } 54 | } 55 | 56 | Watcher.prototype.update = function() { 57 | this.run() 58 | } 59 | 60 | Watcher.prototype.run = function() { 61 | var value = this.get() 62 | var oldValue = this.value 63 | this.value = value 64 | this.cb.call(this.vm, value, oldValue) 65 | } 66 | 67 | /** 68 | * Clean up for dependency collection. 69 | */ 70 | Watcher.prototype.cleanupDeps = function() { 71 | var i = this.deps.length 72 | while (i--) { 73 | var dep = this.deps[i] 74 | if (!this.newDepIds.has(dep.id)) { 75 | dep.removeSub(this) 76 | } 77 | } 78 | var tmp = this.depIds 79 | this.depIds = this.newDepIds 80 | this.newDepIds = tmp 81 | this.newDepIds.clear() 82 | tmp = this.deps 83 | this.deps = this.newDeps 84 | this.newDeps = [] 85 | } 86 | -------------------------------------------------------------------------------- /code/src/platform/web/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsrebuild/build-your-own-vuejs/aa273934889be7d0900e23135e5d7b5cf2d30a88/code/src/platform/web/index.js -------------------------------------------------------------------------------- /code/src/platform/web/modules/attrs.js: -------------------------------------------------------------------------------- 1 | import { 2 | isDef, 3 | isUndef 4 | } from 'core/util/index' 5 | 6 | import { 7 | isBooleanAttr, 8 | isEnumeratedAttr, 9 | isFalsyAttrValue 10 | } from '../util' 11 | 12 | function updateAttrs (oldVnode, vnode) { 13 | const opts = vnode.componentOptions 14 | // if (isDef(opts) && opts.Ctor.options.inheritAttrs === false) { 15 | // return 16 | // } 17 | // if (isUndef(oldVnode.data.attrs) && isUndef(vnode.data.attrs)) { 18 | // return 19 | // } 20 | let key, cur, old 21 | const elm = vnode.elm 22 | const oldAttrs = oldVnode.data.attrs || {} 23 | let attrs = vnode.data.attrs || {} 24 | // // clone observed objects, as the user probably wants to mutate it 25 | // if (isDef(attrs.__ob__)) { 26 | // attrs = vnode.data.attrs = extend({}, attrs) 27 | // } 28 | 29 | for (key in attrs) { 30 | cur = attrs[key] 31 | old = oldAttrs[key] 32 | if (old !== cur) { 33 | setAttr(elm, key, cur) 34 | } 35 | } 36 | 37 | for (key in oldAttrs) { 38 | if (isUndef(attrs[key])) { 39 | elm.removeAttribute(key) 40 | } 41 | } 42 | } 43 | 44 | function setAttr (el, key, value) { 45 | if (isBooleanAttr(key)) { 46 | // set attribute for blank value 47 | // e.g. 48 | if (isFalsyAttrValue(value)) { 49 | el.removeAttribute(key) 50 | } else { 51 | // technically allowfullscreen is a boolean attribute for