├── .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 | 
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 | 
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 |
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