├── .idea
├── blog.iml
├── modules.xml
├── vcs.xml
└── workspace.xml
├── README.md
├── canvas
└── 原生flappyBird.md
├── images
├── 1.jpg
├── 2.jpg
├── 3.jpg
├── 4.jpg
├── 5.jpg
├── 6.jpg
├── 7.jpg
├── 8.jpg
└── 9.jpg
└── js
├── Regular 源码学习.md
├── Vue指令相关(由clickoutside指令启发)
├── myDrag
├── myDrag.vue
├── mySort.js
└── vuedraggable.js
├── vue2 Dep Observer 与 Watcher之间的关联.md
└── 浅析Vue 中的patch和diff(上).md
/.idea/blog.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/workspace.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
65 |
66 |
67 |
68 |
69 | true
70 | DEFINITION_ORDER
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 | 1525254213982
141 |
142 |
143 | 1525254213982
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # blog
2 |
--------------------------------------------------------------------------------
/canvas/原生flappyBird.md:
--------------------------------------------------------------------------------
1 | 待整理:
2 |
3 | * 盒子检测(比较简单)
4 | * 像素检测 (当触发盒子检测后,在进行像素检测!非常赞)
5 | * Pipe生成的重写
6 |
--------------------------------------------------------------------------------
/images/1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnnVoV/blog/f2ce34fc53b085d08d1706a3b3280e5bd66bfe13/images/1.jpg
--------------------------------------------------------------------------------
/images/2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnnVoV/blog/f2ce34fc53b085d08d1706a3b3280e5bd66bfe13/images/2.jpg
--------------------------------------------------------------------------------
/images/3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnnVoV/blog/f2ce34fc53b085d08d1706a3b3280e5bd66bfe13/images/3.jpg
--------------------------------------------------------------------------------
/images/4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnnVoV/blog/f2ce34fc53b085d08d1706a3b3280e5bd66bfe13/images/4.jpg
--------------------------------------------------------------------------------
/images/5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnnVoV/blog/f2ce34fc53b085d08d1706a3b3280e5bd66bfe13/images/5.jpg
--------------------------------------------------------------------------------
/images/6.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnnVoV/blog/f2ce34fc53b085d08d1706a3b3280e5bd66bfe13/images/6.jpg
--------------------------------------------------------------------------------
/images/7.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnnVoV/blog/f2ce34fc53b085d08d1706a3b3280e5bd66bfe13/images/7.jpg
--------------------------------------------------------------------------------
/images/8.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnnVoV/blog/f2ce34fc53b085d08d1706a3b3280e5bd66bfe13/images/8.jpg
--------------------------------------------------------------------------------
/images/9.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnnVoV/blog/f2ce34fc53b085d08d1706a3b3280e5bd66bfe13/images/9.jpg
--------------------------------------------------------------------------------
/js/Regular 源码学习.md:
--------------------------------------------------------------------------------
1 | ## Regular 源码学习
2 |
3 | ### 1.获取template 字符串 ---> ast 语法树
4 | (template 先分词 得到token 然后语法解析生成ast 抽象语法树, 利用parse 与字符串模板本质不同的地方在于利用parse使我们的字符串模板变得有逻辑)
5 |
6 | ### 2. compile过程,得到对应dom, 生成group对象
7 | 得到语法树ast以后进入compile 阶段,compile 阶段主要做了什么呢? 我们会遍历我们的ast语法树通过_walk方法,根据ast节点的不同类型,执行对应的方法```walkers[ast.type]()```
8 | * ast的节点type有3种, text、expressoin、element;
9 | * 当遇到type='expression'的节点时,我们必然需要让表达式与data之间产生关联?如何做到的呢?通过watcher。那么watcher是如何创建的呢?我们继续往下看。
10 |
11 | ```javascript
12 | walkers.expression = function(ast, options){
13 | var cursor = options.cursor, node,
14 | mountNode = cursor && cursor.node;
15 |
16 | if(mountNode){
17 | //@BUG: if server render > in Expression will cause error
18 | var astText = _.toText( this.$get(ast) );
19 | node = walkers._handleMountText(cursor, astText);
20 |
21 | }else{
22 | node = document.createTextNode("");
23 | }
24 | // 如果是表达式类型,会进入这个$watch 方法, 第二个参数是cb, 接下来看下$watch方法
25 | this.$watch(ast, function(newval){
26 | dom.text(node, _.toText(newval));
27 | }, OPTIONS.STABLE_INIT )
28 | return node;
29 |
30 | }
31 | ```
32 | * $watch 里面做了什么呢?会执行touchExpression方法, touchExpression 会将ast中的exprBody转换为Function; 然后生成了一个watcher对象,watcher对象上会存有expr.get 和 expr.set 方法。
33 |
34 | * 然后进入digest阶段,进入checkSingleWatch方法,checkSingleWatch 主要执行了 ```now = watcher.get(this); // 很简单就是取data是对应表达式的值
35 | last = watcher.last; ```如果now 与 last 计算得到的值并不相等标记dirty=true
36 | * 注意一个细节,在type='expression'的方法里面,$watch有一个cb, 当我们得到了newVal = now后,会进入这个cb, 即把data 与expression相结合,计算得到的结果转换为了一个textNode
37 | ```javascript
38 | this.$watch(ast, function(newval){
39 | dom.text(node, _.toText(newval));
40 | }, OPTIONS.STABLE_INIT )
41 | ```
42 |
43 | * 最终每一次得到的真实dom 都会放入```new Group(res)```中,最终我们得到了group对象
44 |
45 | ```javascript
46 | $watch = function() {
47 | ...
48 | if(typeof expr === 'function'){
49 | get = expr.bind(this);
50 | }else{
51 | // 重点看这里
52 | expr = this.$expression(expr);
53 | get = expr.get;
54 | once = expr.once;
55 | }
56 | var watcher = {
57 | id: uid,
58 | get: get,
59 | fn: fn,
60 | once: once,
61 | force: options.force,
62 | // don't use ld to resolve array diff
63 | diff: options.diff,
64 | test: test,
65 | deep: options.deep,
66 | last: options.sync? get(this): options.last
67 | }
68 | // 并将它存储于_watchers 属性中
69 | this[options.stable? '_watchersForStable': '_watchers'].push(watcher);
70 | ...
71 | // 然后进入init 阶段
72 | // init state.
73 | if(options.init === true){
74 | var prephase = this.$phase;
75 | // 初始开始一次脏检查
76 | this.$phase = 'digest';
77 | // 进入checkSingleWatch 方法
78 | this._checkSingleWatch( watcher);
79 | this.$phase = prephase;
80 | }
81 | return watcher;
82 | }
83 | ```
84 | ```javascript
85 | _touchExpr: function(expr, ext){
86 | var rawget, ext = this.__ext__, touched = {};
87 | if(expr.type !== 'expression' || expr.touched) return expr;
88 |
89 | rawget = expr.get;
90 | if(!rawget){
91 | rawget = expr.get = new Function(_.ctxName, _.extName , _.prefix+ "return (" + expr.body + ")");
92 | expr.body = null;
93 | }
94 | touched.get = !ext? rawget: function(context, e){
95 | return rawget( context, e || ext )
96 | }
97 |
98 | if(expr.setbody && !expr.set){
99 | var setbody = expr.setbody;
100 | var filters = expr.filters;
101 | var self = this;
102 | if(!filters || !_.some(filters, function(filter){ return !self._f_(filter).set }) ){
103 | expr.set = function(ctx, value, ext){
104 | expr.set = new Function(_.ctxName, _.setName , _.extName, _.prefix + setbody);
105 | return expr.set(ctx, value, ext);
106 | }
107 | }
108 | expr.filters = expr.setbody = null;
109 | }
110 | if(expr.set){
111 | touched.set = !ext? expr.set : function(ctx, value){
112 | return expr.set(ctx, value, ext);
113 | }
114 | }
115 |
116 | touched.type = 'expression';
117 | touched.touched = true;
118 | touched.once = expr.once || expr.constant;
119 | return touched
120 | },
121 | ```
122 | 
123 | 
124 |
125 | ```
126 | _checkSingleWatch: function(watcher){
127 | var dirty = false;
128 | if(!watcher) return;
129 |
130 | var now, last, tlast, tnow, eq, diff;
131 |
132 | if(!watcher.test){
133 | // 进入expr.get 方法 执行function 的值,表达式里有个_sg_方法,我们看下具体指的是什么
134 | now = watcher.get(this);
135 | last = watcher.last;
136 |
137 | if(now !== last || watcher.force){
138 | tlast = _.typeOf(last);
139 | tnow = _.typeOf(now);
140 | eq = true;
141 |
142 | // !Object
143 | if( !(tnow === 'object' && tlast==='object' && watcher.deep) ){
144 | // Array
145 | if( tnow === 'array' && ( tlast=='undefined' || tlast === 'array') ){
146 | diff = diffArray(now, watcher.last || [], watcher.diff)
147 | if( tlast !== 'array' || diff === true || diff.length ) dirty = true;
148 | }else{
149 | eq = _.equals( now, last );
150 | if( !eq || watcher.force ){
151 | watcher.force = null;
152 | dirty = true;
153 | }
154 | }
155 | }else{
156 | diff = diffObject( now, last, watcher.diff );
157 | if( diff === true || diff.length ) dirty = true;
158 | }
159 | }
160 |
161 | } else{
162 | // @TODO 是否把多重改掉
163 | var result = watcher.test(this);
164 | if(result){
165 | dirty = true;
166 | watcher.fn.apply(this, result)
167 | }
168 | }
169 | if(dirty && !watcher.test){
170 | if(tnow === 'object' && watcher.deep || tnow === 'array'){
171 | watcher.last = _.clone(now);
172 | }else{
173 | watcher.last = now;
174 | }
175 | // 这里会执行通过$watch 传入的cb, 本质就是把刚刚模板与数值结合得到的值转换为textNode
176 | watcher.fn.call(this, now, last, diff)
177 | if(watcher.once) this.$unwatch(watcher)
178 | }
179 |
180 | return dirty;
181 | },
182 | ```
183 |
184 |
185 | ### 3.inject 阶段
186 | 进入inject 方法,将得到的dom插入到我们的页面上
187 |
188 | ```javascript
189 | inject: function(node, pos ){
190 | var group = this;
191 | var fragment = combine.node(group.group || group);
192 | if(node === false) {
193 | animate.remove(fragment)
194 | return group;
195 | }else{
196 | if(!fragment) return group;
197 | if(typeof node === 'string') node = dom.find(node);
198 | if(!node) throw Error('injected node is not found');
199 | // use animate to animate firstchildren
200 | animate.inject(fragment, node, pos);
201 | }
202 | // if it is a component
203 | if(group.$emit) {
204 | var preParent = group.parentNode;
205 | var newParent = (pos ==='after' || pos === 'before')? node.parentNode : node;
206 | group.parentNode = newParent;
207 | group.$emit("$inject", node, pos, preParent);
208 | }
209 | return group;
210 | },
211 | ```
212 | ### 4.数据更新 $update
213 | 当数据更新时,我们手动调用```$update()```方法进入```$digest```,遍历我们直接存储的watcher,每一个watcher会再次进入checkSingleWatch方法,再次计算此时的now与上一次的last值作对比,同样当发现last与now值不同时,触发callback,我们重点看下这个cb(上面有说过,再看下)
214 | ```javascript
215 | walkers.expression = function(ast, options){
216 | var cursor = options.cursor, node,
217 | mountNode = cursor && cursor.node;
218 |
219 | if(mountNode){
220 | //@BUG: if server render > in Expression will cause error
221 | var astText = _.toText( this.$get(ast) );
222 | node = walkers._handleMountText(cursor, astText);
223 |
224 | }else{
225 | node = document.createTextNode("");
226 | }
227 | // cb 就是后面的这个function, 主要做的事情就是把node的textProp改为了当前的newVal值,注意这里的cb是个闭包,所以当我们值更新的时候,可以直接拿到这个node, 对node进行修改
228 | this.$watch(ast, function(newval){
229 | dom.text(node, _.toText(newval));
230 | }, OPTIONS.STABLE_INIT )
231 | return node;
232 |
233 | }
234 | ```
235 |
236 | ### 关于$compile 的内部细节
237 |
238 | 1.调用$watch 方法,根据我们ast得到的expression, 生成一个expr.get 方法和expr.set 方法(其实和vue的render function 生成很相似,就是把模板转换为一个function, 只是少了依赖收集的这个过程,regular是脏检查,查不到依赖关系)
239 |
240 | ```javascript
241 | rawget = expr.get = new Function(_.ctxName, _.extName , _.prefix+ "return (" + expr.body + ")
242 | ```
243 | * expr.get:
244 |
245 | 
246 |
247 | * expr.set:
248 | 
249 |
250 | 2.然后生成一个新的watcher 对象,并且属性上有get,set等方法(就是上面的expr.get 和 expr.set); 然后将它push到我们的this._watches 观察者队列中
251 |
252 | ```javascript
253 | // 看下watcher结构
254 | var watcher = {
255 | id: uid,
256 | get: get,
257 | fn: fn,
258 | once: once,
259 | force: options.force,
260 | // don't use ld to resolve array diff
261 | diff: options.diff,
262 | test: test,
263 | deep: options.deep,
264 | last: options.sync? get(this): options.last
265 | }
266 | ```
267 | 3.然后开始digest 进入脏检查阶段从根节点root开始递归, 进入checkSingleWatch方法(避免重复检测)。重点的地方在 now = watcher.get(this)这里, 也就是开始执行我们上面得到的expr.get 方法。expr.get 执行完就得到了now,再与last进行对比,看下面的例子:
268 |
269 | ```javascript
270 | //expr.get 例子: 比如我们{{a}} 的到的get 方法是下面这个,我们的data.a = 1 然后我们看下_sg_
271 | return c._sg_('a', d, e)
272 |
273 | // simple accessor get
274 | // path 就是我们此时的key defaults 是我们的data 我们例子里的
{a}
最终会执行到return deaults[path]
275 | _sg_:function(path, defaults, ext){
276 | if( path === undefined ) return undefined;
277 | if(ext && typeof ext === 'object'){
278 | if(ext[path] !== undefined) return ext[path];
279 | }
280 | var computed = this.computed,
281 | computedProperty = computed[path];
282 | if(computedProperty){
283 | if(computedProperty.type==='expression' && !computedProperty.get) this._touchExpr(computedProperty);
284 | if(computedProperty.get) return computedProperty.get(this);
285 | else _.log("the computed '" + path + "' don't define the get function, get data."+path + " altnately", "warn")
286 | }
287 |
288 | if( defaults === undefined ){
289 | return undefined;
290 | }
291 | // 大部分简单情况我们返回了data[key]
292 | return defaults[path];
293 | }
294 | ```
295 |
296 | ```javascript
297 | _checkSingleWatch: function(watcher){
298 | var dirty = false;
299 | if(!watcher) return;
300 |
301 | var now, last, tlast, tnow, eq, diff;
302 |
303 | if(!watcher.test){
304 | // 获取get计算得到的当前值
305 | now = watcher.get(this);
306 | // 初始时,上一次没有值 undefined
307 | last = watcher.last;
308 |
309 | if(now !== last || watcher.force){
310 | tlast = _.typeOf(last);
311 | tnow = _.typeOf(now);
312 | eq = true;
313 |
314 | // !Object
315 | if( !(tnow === 'object' && tlast==='object' && watcher.deep) ){
316 | // Array
317 | if( tnow === 'array' && ( tlast=='undefined' || tlast === 'array') ){
318 | diff = diffArray(now, watcher.last || [], watcher.diff)
319 | if( tlast !== 'array' || diff === true || diff.length ) dirty = true;
320 | }else{
321 | // now 与 last 并不相等,进入这里
322 | eq = _.equals( now, last );
323 | if( !eq || watcher.force ){
324 | watcher.force = null;
325 | dirty = true;
326 | }
327 | }
328 | }else{
329 | diff = diffObject( now, last, watcher.diff );
330 | if( diff === true || diff.length ) dirty = true;
331 | }
332 | }
333 |
334 | } else{
335 | // @TODO 是否把多重改掉
336 | var result = watcher.test(this);
337 | if(result){
338 | dirty = true;
339 | watcher.fn.apply(this, result)
340 | }
341 | }
342 | if(dirty && !watcher.test){
343 | if(tnow === 'object' && watcher.deep || tnow === 'array'){
344 | watcher.last = _.clone(now);
345 | }else{
346 | watcher.last = now;
347 | }
348 | // 这里进入前面$watch 传入的cb 我在后面贴一下 (贴1)
349 | watcher.fn.call(this, now, last, diff)
350 | if(watcher.once) this.$unwatch(watcher)
351 | }
352 |
353 | return dirty;
354 | }
355 | // ------- 贴在了这里 -------
356 | // 贴1: cb 贴在了这里 就是把expression 通过get计算出来得到的值,转换为了一个textNode节点
357 | this.$watch(ast, function(newval){
358 | dom.text(node, _.toText(newval));
359 | }, OPTIONS.STABLE_INIT )
360 |
361 | // dom.text 方法
362 | dom.text = (function (){
363 | var map = {};
364 | if (dom.msie && dom.msie < 9) {
365 | map[1] = 'innerText';
366 | map[3] = 'nodeValue';
367 | } else {
368 | map[1] = map[3] = 'textContent';
369 | }
370 |
371 | return function (node, value) {
372 | var textProp = map[node.nodeType];
373 | if (value == null) {
374 | return textProp ? node[textProp] : '';
375 | }
376 | node[textProp] = value;
377 | // 得到了一个描述这个node相关的对象可以看做一个VDom
378 | }
379 | })();
380 | ```
381 |
382 | ## 题外话: 为什么使用脏检查(作者说)
383 |
384 | 1. 脏检查完全不关心你改变数据的方式,而常规的set, get的方式则会强加许多限制(vs Vue)
385 | 2. 脏检查可以实现批处理完数据之后,再去统一更新view.(Vue2 也可以, 它利用了microtask mutaionObserve, 或者Promise)
386 | 3. 脏检查其实比 GET/SET 更容易实现。脏检查是个单向的检查流程(请不要和双向绑定发生混淆),可以实现*_任意复杂度的表达式支持*。而get/set的方式则需要处理复杂依赖链,基本上表达式支持都是阉割的(使用时就像踩雷).
387 |
388 | 但是很显然,脏检查是低效的,它的效率基本上取决于你绑定的观察者数量,在Regular中,你可以通过[`@(Expression)`](https://regularjs.github.io/reference?syntax-zh#bind-once)元素来控制你的观察者数量。
389 |
390 | 然而结合这种类mvvm系统中,他又是高效的。因为监听模式带来了dom的局部更新,而dom操作恰恰又是隐藏的性能瓶颈所在。
391 |
392 | > Regular实际上在解析时,已经提取了表达式的依赖关系,在未来Observe到来时,可以调整为脏检查 + 依赖计算的方式(如:vue2)
393 |
394 | ### 与Vue2的一些对比
395 | 1.Regular 使用的是脏检查,而Vue2利用模板解析与getter,setter 结合使用了依赖收集
396 | 2.虽然作者说脏检查可以批处理数据之后再更新视图,但是我看到的当数据发生变化时,视图更新并不是批量的?(初始的第一次是批量的,这里我再看下);Vue2里使用watcher队列对dom的更新进行了批处理,当数据统一处理完后,再利用microtask 渲染视图。
397 |
398 | 参考文档:
399 | https://note.youdao.com/share/?id=f00a086b157b0c13b9c5ea98f180b799&type=note#/
400 | http://www.html-js.com/article/Regularjs-Chinese-guidelines-for-a-comprehensive-summary-of-the-front-template-technology
--------------------------------------------------------------------------------
/js/Vue指令相关(由clickoutside指令启发):
--------------------------------------------------------------------------------
1 | ## 背景
2 | 我有一个input和一个panel板子,在做这个组件的过程中,我卡在了当我鼠标点击板子以外的区域,板子要收起来这个步骤上。
3 |
4 | ## 遇到的问题
5 | 原先想法: 原本就是监听document.body的mouseclick事件,target不是我的文本框就收起来(如何判断是不是我特定文本框,我用的是样式判断)这就会导致多个相同组件会同时触发这个回调,这显然不正确,后来又想说再加个唯一标识区分,但总觉得怪怪的,于是看了一下element-ui的实现。
6 |
7 | ## 参考element-ui 的v-clickoutside
8 | 我发现element-ui 中有很多组件都会有这种功能,然后我找了一个date-picker组件看了下。发现它实现了一个clickoutside的指令,然后就深入看了一下
9 |
10 | ### 1.为什么用指令而没有直接写一个绑定函数
11 | 参考了[使用Vue Directive封装DOM操作](https://elegenthus.github.io/post/VueDirectivesTest/)这篇文章,我觉得讲的很好,为什么要使用directive呢?
12 | > 这是因为,为了实现View和ViewModel的分离,我们必须封装DOM操作,View层负责页面上的显示,ViewModel层负责改变操作数据,由于Vue是数据驱动的,属于ViewModel层,那么其中就不应该出现View层上的DOM操作,且,使用Vue Directive是和DOM元素的创建、销毁绑定的。
13 |
14 | 我个人觉得,最明显的优势在于vue指令中提供的钩子是与组件的生命周期关联的,利于我们绑定事件,销毁事件。
15 |
16 | ### 2.指令基础
17 | | 钩子函数 | 描述 |
18 | | ------| ------ |
19 | | bind | 构造函数,第一次绑定时调用 |
20 | | update | bind之后以初始值调用一次,之后每当绑定值变化时调用
21 | |unbind| 组件销毁时调用
22 |
23 | | 属性 | 描述 |
24 | | ------| ------ |
25 | | el | 绑定指令的dom元素 |
26 | | vm | 上下文ViewModel |
27 | | expression | 指令表达式 |
28 | | arg | 参数 |
29 | | name | 指令id |
30 | | modifiers| 指令的修饰符 |
31 | |descriptor| 指令的解析结果
32 |
33 | ### 探究clickouside 指令
34 | ```javascript
35 | 'use strict';
36 | var _dom = require('element-ui/lib/utils/dom');
37 | ...
38 | var nodeList = [];
39 | var ctx = '@@clickoutsideContext';
40 |
41 | var startClick = void 0;
42 | var seed = 0;
43 |
44 | (0, _dom.on)(document, 'mousedown', function (e) {
45 | return startClick = e;
46 | });
47 |
48 | (0, _dom.on)(document, 'mouseup', function (e) {
49 | nodeList.forEach(function (node) {
50 | return node[ctx].documentHandler(e, startClick);
51 | });
52 | });
53 |
54 | function createDocumentHandler(el, binding, vnode) {
55 | return function () {
56 | var mouseup = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
57 | var mousedown = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
58 | if (el.contains(mouseup.target) || el.contains(mousedown.target) || el === mouseup.target || vnode.context.popperElm && (vnode.context.popperElm.contains(mouseup.target) || vnode.context.popperElm.contains(mousedown.target))) return;
59 |
60 | if (binding.expression && el[ctx].methodName && vnode.context[el[ctx].methodName]) {
61 | vnode.context[el[ctx].methodName]();
62 | } else {
63 | el[ctx].bindingFn && el[ctx].bindingFn();
64 | }
65 | };
66 | }
67 |
68 | /**
69 | * v-clickoutside
70 | * @desc 点击元素外面才会触发的事件
71 | * @example
72 | * ```vue
73 | *
74 | * ```
75 | */
76 |
77 | exports.default = {
78 | bind: function bind(el, binding, vnode) {
79 | nodeList.push(el);
80 | var id = seed++;
81 | el[ctx] = {
82 | id: id,
83 | documentHandler: createDocumentHandler(el, binding, vnode),
84 | methodName: binding.expression,
85 | bindingFn: binding.value
86 | };
87 | },
88 | update: function update(el, binding, vnode) {
89 | el[ctx].documentHandler = createDocumentHandler(el, binding, vnode);
90 | el[ctx].methodName = binding.expression;
91 | el[ctx].bindingFn = binding.value;
92 | },
93 | unbind: function unbind(el) {
94 | var len = nodeList.length;
95 | for (var i = 0; i < len; i++) {
96 | if (nodeList[i][ctx].id === el[ctx].id) {
97 | nodeList.splice(i, 1);
98 | break;
99 | }
100 | }
101 | delete el[ctx];
102 | }
103 | };
104 | ```
105 | element-ui 中这个可能考虑的事件比较多,写的偏复杂了一点,我就抽下大致的思路,然后写一个我要的
106 | * 当脚本加载时,就去监听document的mouseup事件
107 | * bind时我要将我绑定的node推入我的nodeList数组里,并且给我的node做上一个id标记
108 | * mouseup时,利用el.contains(el.target)来看是否是el之外的节点触发, 如果是以外的就出发我们的bind.expression对应的cb
109 | ```javascript
110 | node.contains(otherNode)
111 | --------------------------
112 | This function checks to see if an element is in the page's body. As contains is inclusive and determining if the body contains itself isn't the intention of isInPage this case explicitly returns false.
113 | ```
114 | * 解绑时只要删掉nodeList中对应的那个节点,如果移除整个事件,会影响其他组件
115 |
116 | 所以我的指令就是下面这样:
117 |
118 | ```javascript
119 | var nodeList = [];
120 | var ctx = '@@clickoutside';
121 | var seed = 0;
122 |
123 | var cb = (e) => {
124 | var length = nodeList.length;
125 | var target = e.target;
126 | for (var i = 0; i < length; i++) {
127 | if (!nodeList[i].contains(target)) {
128 | nodeList[i][ctx].handler();
129 | }
130 | }
131 | }
132 |
133 | document.addEventListener('mouseup', cb);
134 | export default {
135 | bind(el, binding, vnode) {
136 | nodeList.push(el);
137 | el[ctx] = {
138 | id: seed++,
139 | handler: function() {
140 | // 注意通过vnode.context取到我们的cb
141 | vnode.context[binding.expression]();
142 | }
143 | }
144 | },
145 | unbind(el) {
146 | // 注意这里不能直接removeEventListener这样会影响同一个页面上的同名组件的使用了
147 | var len = nodeList.length;
148 | for (var i = 0; i < len; i++) {
149 | if (nodeList[i][ctx].id === el[ctx].id) {
150 | nodeList.splice(i, 1);
151 | break;
152 | }
153 | }
154 | delete el[ctx];
155 | }
156 | }
157 | ```
158 | ### 参考资料
159 | 1.使用Vue Directive封装DOM操作
160 | https://elegenthus.github.io/post/VueDirectivesTest/
161 | 2.Vue:指令(directive)简介
162 | https://axiu.me/coding/vue-directive/
163 | 3.Vue.js 指令
164 | http://imweb.io/topic/570a12a906f2400432c139aa
165 |
166 |
167 |
168 |
169 |
--------------------------------------------------------------------------------
/js/myDrag/myDrag.vue:
--------------------------------------------------------------------------------
1 |
53 |
--------------------------------------------------------------------------------
/js/myDrag/mySort.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 绑定事件相关方法
3 | */
4 | var _on = function (el, event, cb) {
5 | el.addEventListener(event, cb)
6 | }
7 | /**
8 | * el 是否匹配selector 选择规则
9 | * @param el
10 | * @param selector
11 | * @returns {*}
12 | * @private
13 | */
14 | var _matches = function (el, selector) {
15 | if (el.matches(selector)) {
16 | return el
17 | }
18 | return null
19 | }
20 | /**
21 | * 查找离el的target是selector的最近的元素
22 | * @param el
23 | * @param selector
24 | * @private
25 | */
26 | var _closest = function (el, selector) {
27 | while (el.parentNode !== document) {
28 | if (_matches(el, selector)) {
29 | return el
30 | }
31 | el = el.parentNode
32 | }
33 | }
34 |
35 | var _index = function(targetNode, selector) {
36 | var cur = 0;
37 | while(targetNode && (targetNode = targetNode.previousElementSibling)) {
38 | if (_matches(targetNode, selector)) cur++;
39 | }
40 | return cur;
41 | }
42 |
43 | export default class ST {
44 | constructor (el, options = {}) {
45 | this.el = el
46 | this.options = options
47 | this.init()
48 | }
49 |
50 | init () {
51 | this.bind()
52 | }
53 |
54 | bind () {
55 | _on(this.el, 'dragstart', this._dragStart.bind(this))
56 | _on(this.el, 'dragover', this._dragOver.bind(this))
57 | }
58 |
59 | _dragStart (e) {
60 | this.dragTarget = _closest(e.target, this.options.selector || '*')
61 | this.startIndex = e.startIndex = _index(this.dragTarget, this.options.selector);
62 | if (typeof this.options.onStart === 'function') {
63 | this.options.onStart.call(this, e)
64 | }
65 | }
66 |
67 | _dragOver (e) {
68 | var toNode = _closest(e.target, this.options.selector || '*')
69 | // 用于判断是插入前面还是插入在后面
70 | var after = (toNode.nextElementSibling !== this.dragTarget)
71 | if (toNode !== this.dragTarget) {
72 | // 为什么不会重复进入这个方法 因为移动后dragOver的target变化了
73 | toNode.parentNode.insertBefore(this.dragTarget, (after) ? toNode.nextElementSibling : toNode)
74 | if (typeof this.options.onMove === 'function') {
75 | e.endIndex = this.endIndex = _index(this.dragTarget, this.options.selector);
76 | e.startIndex = this.startIndex;
77 | this.options.onMove.call(this, e)
78 | }
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/js/myDrag/vuedraggable.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | var _extends = Object.assign || function (target) {
4 | for (var i = 1; i < arguments.length; i++) {
5 | var source = arguments[i]
6 | for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key] } }
7 | }
8 | return target
9 | };
10 |
11 | function _toConsumableArray (arr) {
12 | if (Array.isArray(arr)) {
13 | for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i] }
14 | return arr2
15 | } else { return Array.from(arr) }
16 | }
17 |
18 | (function () {
19 | 'use strict'
20 |
21 | if (!Array.from) {
22 | Array.from = function (object) {
23 | return [].slice.call(object)
24 | }
25 | }
26 |
27 | function buildAttribute (object, propName, value) {
28 | if (value == undefined) {
29 | return object
30 | }
31 | object = object == null ? {} : object
32 | object[propName] = value
33 | return object
34 | }
35 |
36 | function buildDraggable (Sortable) {
37 | function removeNode (node) {
38 | node.parentElement.removeChild(node)
39 | }
40 |
41 | function insertNodeAt (fatherNode, node, position) {
42 | var refNode = position === 0 ? fatherNode.children[0] : fatherNode.children[position - 1].nextSibling
43 | fatherNode.insertBefore(node, refNode)
44 | }
45 |
46 | function computeVmIndex (vnodes, element) {
47 | return vnodes.map(function (elt) {
48 | return elt.elm
49 | }).indexOf(element)
50 | }
51 |
52 | function _computeIndexes (slots, children, isTransition) {
53 | if (!slots) {
54 | return []
55 | }
56 |
57 | var elmFromNodes = slots.map(function (elt) {
58 | return elt.elm
59 | })
60 | var rawIndexes = [].concat(_toConsumableArray(children)).map(function (elt) {
61 | return elmFromNodes.indexOf(elt)
62 | })
63 | return isTransition ? rawIndexes.filter(function (ind) {
64 | return ind !== -1
65 | }) : rawIndexes
66 | }
67 |
68 | function emit (evtName, evtData) {
69 | var _this = this
70 | // 为什么要放在nextTick里面?
71 | this.$nextTick(function () {
72 | return _this.$emit(evtName.toLowerCase(), evtData)
73 | })
74 | }
75 |
76 | function delegateAndEmit (evtName) {
77 | var _this2 = this
78 |
79 | return function (evtData) {
80 | if (_this2.realList !== null) {
81 | _this2['onDrag' + evtName](evtData)
82 | }
83 | emit.call(_this2, evtName, evtData)
84 | }
85 | }
86 |
87 | // todo 这个等下看
88 | var eventsListened = ['Start', 'Add', 'Remove', 'Update', 'End']
89 | var eventsToEmit = ['Choose', 'Sort', 'Filter', 'Clone']
90 | var readonlyProperties = ['Move'].concat(eventsListened, eventsToEmit).map(function (evt) {
91 | return 'on' + evt
92 | })
93 | var draggingElement = null
94 |
95 | var props = {
96 | options: Object,
97 | list: {
98 | type: Array,
99 | required: false,
100 | default: null
101 | },
102 | value: {
103 | type: Array,
104 | required: false,
105 | default: null
106 | },
107 | noTransitionOnDrag: {
108 | type: Boolean,
109 | default: false
110 | },
111 | clone: {
112 | type: Function,
113 | default: function _default (original) {
114 | return original
115 | }
116 | },
117 | element: {
118 | type: String,
119 | default: 'div'
120 | },
121 | move: {
122 | type: Function,
123 | default: null
124 | },
125 | componentData: {
126 | type: Object,
127 | required: false,
128 | default: null
129 | }
130 | }
131 |
132 | var draggableComponent = {
133 | name: 'draggable',
134 |
135 | props: props,
136 |
137 | data: function data () {
138 | return {
139 | transitionMode: false,
140 | noneFunctionalComponentMode: false,
141 | init: false
142 | }
143 | },
144 | render: function render (h) {
145 | var slots = this.$slots.default
146 | if (slots && slots.length === 1) {
147 | var child = slots[0]
148 | if (child.componentOptions && child.componentOptions.tag === 'transition-group') {
149 | this.transitionMode = true
150 | }
151 | }
152 | var children = slots
153 | var footer = this.$slots.footer
154 |
155 | if (footer) {
156 | children = slots ? [].concat(_toConsumableArray(slots), _toConsumableArray(footer)) : [].concat(_toConsumableArray(footer))
157 | }
158 | var attributes = null
159 | var update = function update (name, value) {
160 | attributes = buildAttribute(attributes, name, value)
161 | }
162 | update('attrs', this.$attrs)
163 | if (this.componentData) {
164 |
165 | var _componentData = this.componentData,
166 | on = _componentData.on,
167 | _props = _componentData.props
168 |
169 | update('on', on)
170 | update('props', _props)
171 | }
172 | // this.element 哪里来的?props里默认是div
173 | return h(this.element, attributes, children)
174 | },
175 | mounted: function mounted () {
176 | var _this3 = this
177 |
178 | this.noneFunctionalComponentMode = this.element.toLowerCase() !== this.$el.nodeName.toLowerCase()
179 | if (this.noneFunctionalComponentMode && this.transitionMode) {
180 | throw new Error('Transition-group inside component is not supported. Please alter element value or remove transition-group. Current element value: ' + this.element)
181 | }
182 | var optionsAdded = {}
183 | eventsListened.forEach(function (elt) {
184 | optionsAdded['on' + elt] = delegateAndEmit.call(_this3, elt)
185 | })
186 |
187 | eventsToEmit.forEach(function (elt) {
188 | optionsAdded['on' + elt] = emit.bind(_this3, elt)
189 | })
190 |
191 | var options = _extends({}, this.options, optionsAdded, {
192 | onMove: function onMove (evt, originalEvent) {
193 | return _this3.onDragMove(evt, originalEvent)
194 | }
195 | })
196 | !('draggable' in options) && (options.draggable = '>*')
197 | console.log(options)
198 |
199 | this._sortable = new Sortable(this.rootContainer, options)
200 | this.computeIndexes()
201 | },
202 | beforeDestroy: function beforeDestroy () {
203 | this._sortable.destroy()
204 | },
205 |
206 | computed: {
207 | rootContainer: function rootContainer () {
208 | // 这里计算了根节点
209 | return this.transitionMode ? this.$el.children[0] : this.$el
210 | },
211 | isCloning: function isCloning () {
212 | return !!this.options && !!this.options.group && this.options.group.pull === 'clone'
213 | },
214 | realList: function realList () {
215 | return !!this.list ? this.list : this.value
216 | }
217 | },
218 |
219 | watch: {
220 | options: {
221 | handler: function handler (newOptionValue) {
222 | for (var property in newOptionValue) {
223 | if (readonlyProperties.indexOf(property) == -1) {
224 | this._sortable.option(property, newOptionValue[property])
225 | }
226 | }
227 | },
228 |
229 | deep: true
230 | },
231 |
232 | realList: function realList () {
233 | this.computeIndexes()
234 | }
235 | },
236 |
237 | methods: {
238 | getChildrenNodes: function getChildrenNodes () {
239 | if (!this.init) {
240 | this.noneFunctionalComponentMode = this.noneFunctionalComponentMode && this.$children.length == 1
241 | this.init = true
242 | }
243 |
244 | if (this.noneFunctionalComponentMode) {
245 | return this.$children[0].$slots.default
246 | }
247 | var rawNodes = this.$slots.default
248 | return this.transitionMode ? rawNodes[0].child.$slots.default : rawNodes
249 | },
250 | computeIndexes: function computeIndexes () {
251 | var _this4 = this
252 |
253 | this.$nextTick(function () {
254 | _this4.visibleIndexes = _computeIndexes(_this4.getChildrenNodes(), _this4.rootContainer.children, _this4.transitionMode)
255 | })
256 | },
257 | getUnderlyingVm: function getUnderlyingVm (htmlElt) {
258 | var index = computeVmIndex(this.getChildrenNodes() || [], htmlElt)
259 | if (index === -1) {
260 | //Edge case during move callback: related element might be
261 | //an element different from collection
262 | return null
263 | }
264 | var element = this.realList[index]
265 | return {index: index, element: element}
266 | },
267 | getUnderlyingPotencialDraggableComponent: function getUnderlyingPotencialDraggableComponent (_ref) {
268 | var __vue__ = _ref.__vue__
269 |
270 | if (!__vue__ || !__vue__.$options || __vue__.$options._componentTag !== 'transition-group') {
271 | return __vue__
272 | }
273 | return __vue__.$parent
274 | },
275 | emitChanges: function emitChanges (evt) {
276 | var _this5 = this
277 |
278 | this.$nextTick(function () {
279 | _this5.$emit('change', evt)
280 | })
281 | },
282 | alterList: function alterList (onList) {
283 | if (!!this.list) {
284 | onList(this.list)
285 | } else {
286 | var newList = [].concat(_toConsumableArray(this.value))
287 | onList(newList)
288 | this.$emit('input', newList)
289 | }
290 | },
291 | spliceList: function spliceList () {
292 | var _arguments = arguments
293 |
294 | var spliceList = function spliceList (list) {
295 | return list.splice.apply(list, _arguments)
296 | }
297 | this.alterList(spliceList)
298 | },
299 | updatePosition: function updatePosition (oldIndex, newIndex) {
300 | var updatePosition = function updatePosition (list) {
301 | return list.splice(newIndex, 0, list.splice(oldIndex, 1)[0])
302 | }
303 | this.alterList(updatePosition)
304 | },
305 | getRelatedContextFromMoveEvent: function getRelatedContextFromMoveEvent (_ref2) {
306 | var to = _ref2.to,
307 | related = _ref2.related
308 |
309 | var component = this.getUnderlyingPotencialDraggableComponent(to)
310 | if (!component) {
311 | return {component: component}
312 | }
313 | var list = component.realList
314 |
315 | var context = {list: list, component: component}
316 | if (to !== related && list && component.getUnderlyingVm) {
317 | var destination = component.getUnderlyingVm(related)
318 | if (destination) {
319 | return _extends(destination, context)
320 | }
321 | }
322 |
323 | return context
324 | },
325 | getVmIndex: function getVmIndex (domIndex) {
326 | var indexes = this.visibleIndexes
327 | var numberIndexes = indexes.length
328 | return domIndex > numberIndexes - 1 ? numberIndexes : indexes[domIndex]
329 | },
330 | getComponent: function getComponent () {
331 | return this.$slots.default[0].componentInstance
332 | },
333 | resetTransitionData: function resetTransitionData (index) {
334 | if (!this.noTransitionOnDrag || !this.transitionMode) {
335 | return
336 | }
337 | var nodes = this.getChildrenNodes()
338 | nodes[index].data = null
339 | var transitionContainer = this.getComponent()
340 | transitionContainer.children = []
341 | transitionContainer.kept = undefined
342 | },
343 | onDragStart: function onDragStart (evt) {
344 | this.context = this.getUnderlyingVm(evt.item)
345 | evt.item._underlying_vm_ = this.clone(this.context.element)
346 | draggingElement = evt.item
347 | },
348 | onDragAdd: function onDragAdd (evt) {
349 | var element = evt.item._underlying_vm_
350 | if (element === undefined) {
351 | return
352 | }
353 | removeNode(evt.item)
354 | var newIndex = this.getVmIndex(evt.newIndex)
355 | this.spliceList(newIndex, 0, element)
356 | this.computeIndexes()
357 | var added = {element: element, newIndex: newIndex}
358 | this.emitChanges({added: added})
359 | },
360 | onDragRemove: function onDragRemove (evt) {
361 | insertNodeAt(this.rootContainer, evt.item, evt.oldIndex)
362 | if (this.isCloning) {
363 | removeNode(evt.clone)
364 | return
365 | }
366 | var oldIndex = this.context.index
367 | this.spliceList(oldIndex, 1)
368 | var removed = {element: this.context.element, oldIndex: oldIndex}
369 | this.resetTransitionData(oldIndex)
370 | this.emitChanges({removed: removed})
371 | },
372 | onDragUpdate: function onDragUpdate (evt) {
373 | removeNode(evt.item)
374 | insertNodeAt(evt.from, evt.item, evt.oldIndex)
375 | var oldIndex = this.context.index
376 | var newIndex = this.getVmIndex(evt.newIndex)
377 | this.updatePosition(oldIndex, newIndex)
378 | var moved = {element: this.context.element, oldIndex: oldIndex, newIndex: newIndex}
379 | this.emitChanges({moved: moved})
380 | },
381 | computeFutureIndex: function computeFutureIndex (relatedContext, evt) {
382 | if (!relatedContext.element) {
383 | return 0
384 | }
385 | var domChildren = [].concat(_toConsumableArray(evt.to.children)).filter(function (el) {
386 | return el.style['display'] !== 'none'
387 | })
388 | var currentDOMIndex = domChildren.indexOf(evt.related)
389 | var currentIndex = relatedContext.component.getVmIndex(currentDOMIndex)
390 | var draggedInList = domChildren.indexOf(draggingElement) != -1
391 | return draggedInList || !evt.willInsertAfter ? currentIndex : currentIndex + 1
392 | },
393 | onDragMove: function onDragMove (evt, originalEvent) {
394 | var onMove = this.move
395 | if (!onMove || !this.realList) {
396 | return true
397 | }
398 |
399 | var relatedContext = this.getRelatedContextFromMoveEvent(evt)
400 | var draggedContext = this.context
401 | var futureIndex = this.computeFutureIndex(relatedContext, evt)
402 | _extends(draggedContext, {futureIndex: futureIndex})
403 | _extends(evt, {relatedContext: relatedContext, draggedContext: draggedContext})
404 | return onMove(evt, originalEvent)
405 | },
406 | onDragEnd: function onDragEnd (evt) {
407 | this.computeIndexes()
408 | draggingElement = null
409 | }
410 | }
411 | }
412 | return draggableComponent
413 | }
414 |
415 | if (typeof exports == 'object') {
416 | var Sortable = require('sortablejs')
417 | module.exports = buildDraggable(Sortable)
418 | } else if (typeof define == 'function' && define.amd) {
419 | define(['sortablejs'], function (Sortable) {
420 | return buildDraggable(Sortable)
421 | })
422 | } else if (window && window.Vue && window.Sortable) {
423 | var draggable = buildDraggable(window.Sortable)
424 | Vue.component('draggable', draggable)
425 | }
426 | })()
427 |
--------------------------------------------------------------------------------
/js/vue2 Dep Observer 与 Watcher之间的关联.md:
--------------------------------------------------------------------------------
1 | # Vue2中Dep, Observer 与Watcher 之间的关系(不含patch部分)
2 |
3 |
4 |
5 | 
6 |
7 | 
8 |
9 | ## 最简单的理解
10 | 按照我原先最简单的想法,我们既然给data 进行了defineProperty设置了getter和setter,为什么我们还需要依赖收集呢?为什么要引入Dep与Watcher?
11 |
12 | ## 为什么要引入依赖收集
13 | * 例子1:
14 | ```javascript
15 | new Vue({
16 | template:
17 | `
18 |
text1: {{text1}}
19 |
text2: {{text2}}
20 |
`,
21 | data: {
22 | text1: 'text1',
23 | text2: 'text2',
24 | text3: 'text3'
25 | }
26 | });
27 | ```
28 | text3在实际模板中并没有被用到,然而当text3的数据被修改的时候(this.text3 = 'test')的时候,同样会触发text3的setter,按照原先想法,这会导致重新执行渲染,这显然不正确。
29 |
30 | * 例子2:
31 | ```javascript
32 | var globalData = {
33 | a: 1,
34 | b: 2
35 | };
36 | new Vue({
37 | template:`
{{a+b}}
`,
38 | data: (){
39 | return globalData;
40 | }
41 | })
42 | ```
43 | 当globalData.a 发生变化或者当globalData.b发生变化时,我们的视图都需要更新,所以我们要收集这个视图依赖于数据a 和 数据b。因此我们需要依赖收集。
44 |
45 | 
46 |
47 |
48 |
49 | ## 前置知识
50 | ### vue官网在线模板编译
51 | https://cn.vuejs.org/v2/guide/render-function.html#%E6%A8%A1%E6%9D%BF%E7%BC%96%E8%AF%91
52 |
53 | ### ASTNode 类型(Abstract Syntax Tree)
54 | * ASTElement type:1
55 | * ASTText type:2
56 | * ASTExpression type:3
57 |
58 | ### render 函数一些函数定义
59 | * _c createElement
60 | * _m renderStatic(渲染静态结点)
61 | * _v createTextNode(创建文本dom)
62 | * _s toString(转换为字符串)
63 |
64 | ### VNode 结构
65 | ```javascript
66 | VNode: {
67 | tag: string | void; // 标签名
68 | data: VNodeData | void; // 结点相关属性数据
69 | children: ?Array
; // 子节点
70 | text: string | void; // 文本
71 | elm: Node | void; // dom元素
72 | ns: string | void; // 命名空间
73 | context: Component | void; // VNode所处Vue对象
74 | functionalContext: Component | void; // only for functional component root nodes
75 | key: string | number | void;
76 | componentOptions: VNodeComponentOptions | void; // VNode对象如果对应的是一个自定义组件,componentOptions保存组件相关事件、props数据等
77 | componentInstance: Component | void; // VNode对象如果对应的是一个自定义组件,componentInstance保存相对应的vue实例
78 | parent: VNode | void; // 当前自定义组件在父组件中的vnode
79 | raw: boolean; // contains raw HTML? (server only)
80 | isStatic: boolean; // 是否是静态内容
81 | isRootInsert: boolean; // necessary for enter transition check
82 | isComment: boolean; // empty comment placeholder?
83 | isCloned: boolean; // 是否是clone的VNode对象
84 | isOnce: boolean; // 是否是v-once元素的VNode对象
85 | }
86 | ```
87 | ### 真实DOM有什么问题,为什么要去使用虚拟DOM
88 | 每个DOM上的属性多达 228 个,而这些属性有 90% 多对我们来说都是无用的。VNode 就是简化版的真实 DOM 元素,保留了我们要的属性,并新增了一些在 diff 过程中需要使用的属性,例如 isStatic。
89 | 【总结】Virtual DOM 就是一个js对象,用它来更轻量地描述DOM
90 |
91 | ## 入口文件查找
92 | * 从package.json我们看到
93 | ```javascript
94 | "scripts": {
95 | "dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev"
96 | }
97 | ```
98 | * 从scripts/config.js我们看到
99 | ```javascript
100 | // Runtime+compiler development build (Browser)
101 | 'web-full-dev': {
102 | entry: resolve('web/entry-runtime-with-compiler.js'),
103 | dest: resolve('dist/vue.js'),
104 | format: 'umd',
105 | env: 'development',
106 | alias: { he: './entity-decoder' },
107 | banner
108 | },
109 | ```
110 | * 从web/entry-runtime-with-compiler.js我们看到
111 | ```javascript
112 | import Vue from './runtime/index'
113 | ```
114 | 然后--> /src/core/index.js --> /src/core/instance/index.js
115 | 最终在instance/index.js里面找到Vue的构造函数
116 | ```javascript
117 | function Vue (options) {
118 | if (process.env.NODE_ENV !== 'production' &&
119 | !(this instanceof Vue)
120 | ) {
121 | warn('Vue is a constructor and should be called with the `new` keyword')
122 | }
123 | this._init(options)
124 | }
125 | ```
126 | ## 设置情景分析
127 |
128 | 假设我们的实际场景为下面的脚本, 我们下面来分析一下
129 |
130 | ```javascript
131 | var data = {
132 | a:1,
133 | b:2
134 | };
135 | new Vue({
136 | el:'#app',
137 | template: `\
138 | \
139 | {{a+b}}
\
140 | 静态文本考拉地址
\
141 | \
142 | `,
143 | data(){
144 | return data;
145 | }
146 | });
147 | ```
148 |
149 |
150 |
151 | ## _init 入口函数
152 |
153 | ```javascript
154 | Vue.prototype._init = function (options) {
155 | initLifecycle(vm);
156 | initEvents(vm);
157 | initRender(vm);
158 | callHook(vm, 'beforeCreate');
159 | // 注意:beforeCreate阶段完成后,我们的options被merge到了vm.$options属性上,此时是获取不到this.xxx数据的,如果我们要获取data数据需要this.$options.data()来获取
160 | initInjections(vm);
161 | // 对data进行了一些Observe 执行了defineReactive 标记1
162 | initState();
163 | initProvide(vm); // resolve provide after data/props
164 | callHook(vm, 'created');
165 | /* istanbul ignore if */
166 | if ("development" !== 'production' && config.performance && mark) {
167 | vm._name = formatComponentName(vm, false);
168 | mark(endTag);
169 | measure(("vue " + (vm._name) + " init"), startTag, endTag);
170 | }
171 | if (vm.$options.el) {
172 | // 对模板进行了compile 生成了render function, 调用render function 生成了vmdom
173 | vm.$mount(vm.$options.el);
174 | }
175 | }
176 | ```
177 |
178 | ## 阶段一 initState()
179 | ```javascript
180 | function initState (vm) {
181 | vm._watchers = [];
182 | var opts = vm.$options;
183 | if (opts.props) { initProps(vm, opts.props); }
184 | if (opts.methods) { initMethods(vm, opts.methods); }
185 | if (opts.data) {
186 | initData(vm);
187 | } else {
188 | observe(vm._data = {}, true /* asRootData */);
189 | }
190 | if (opts.computed) { initComputed(vm, opts.computed); }
191 | if (opts.watch && opts.watch !== nativeWatch) {
192 | initWatch(vm, opts.watch);
193 | }
194 | }
195 | ```
196 |
197 | initState方法里面有调用initData,在initData方法里面最后调用了observe(data, true)。那我们来看下observe 方法
198 |
199 | ```javascript
200 | function initData (vm) {
201 | var data = vm.$options.data;
202 | data = vm._data = typeof data === 'function'
203 | ? getData(data, vm)
204 | : data || {};
205 | if (!isPlainObject(data)) {
206 | data = {};
207 | "development" !== 'production' && warn(
208 | 'data functions should return an object:\n' +
209 | 'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
210 | vm
211 | );
212 | }
213 | // proxy data on instance
214 | var keys = Object.keys(data);
215 | var props = vm.$options.props;
216 | var methods = vm.$options.methods;
217 | var i = keys.length;
218 | while (i--) {
219 | var key = keys[i];
220 | {
221 | if (methods && hasOwn(methods, key)) {
222 | warn(
223 | ("Method \"" + key + "\" has already been defined as a data property."),
224 | vm
225 | );
226 | }
227 | }
228 | if (props && hasOwn(props, key)) {
229 | "development" !== 'production' && warn(
230 | "The data property \"" + key + "\" is already declared as a prop. " +
231 | "Use prop default value instead.",
232 | vm
233 | );
234 | } else if (!isReserved(key)) {
235 | proxy(vm, "_data", key);
236 | }
237 | }
238 | // observe data 我们重点看这里
239 | observe(data, true /* asRootData */);
240 | }
241 |
242 | function observe (value, asRootData) {
243 | if (!isObject(value) || value instanceof VNode) {
244 | return
245 | }
246 | var ob;
247 | if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
248 | ob = value.__ob__;
249 | } else if (
250 | observerState.shouldConvert &&
251 | !isServerRendering() &&
252 | (Array.isArray(value) || isPlainObject(value)) &&
253 | Object.isExtensible(value) &&
254 | !value._isVue
255 | ) {
256 | // 重点看这里
257 | ob = new Observer(value);
258 | }
259 | if (asRootData && ob) {
260 | ob.vmCount++;
261 | }
262 | return ob
263 | }
264 |
265 | // 然后我们再看Observer 类
266 | var Observer = function Observer (value) {
267 | this.value = value;
268 | this.dep = new Dep();
269 | this.vmCount = 0;
270 | def(value, '__ob__', this);
271 | if (Array.isArray(value)) {
272 | var augment = hasProto
273 | ? protoAugment
274 | : copyAugment;
275 | augment(value, arrayMethods, arrayKeys);
276 | this.observeArray(value);
277 | } else {
278 | // 会进入walk方法,相当于在遍历我们的value
279 | this.walk(value);
280 | }
281 | };
282 |
283 | Observer.prototype.walk = function walk (obj) {
284 | var keys = Object.keys(obj);
285 | for (var i = 0; i < keys.length; i++) {
286 | // 这边开始遍历data的key, 每一次遍历都会new Dep()
287 | defineReactive(obj, keys[i], obj[keys[i]]);
288 | }
289 | };
290 |
291 | // defineReactive 里面就进入了我们所熟悉的defineProperty方法
292 | export function defineReactive (
293 | obj: Object,
294 | key: string,
295 | val: any,
296 | customSetter?: ?Function,
297 | shallow?: boolean
298 | ) {
299 | const dep = new Dep()
300 |
301 | const property = Object.getOwnPropertyDescriptor(obj, key)
302 | if (property && property.configurable === false) {
303 | return
304 | }
305 | // cater for pre-defined getter/setters
306 | const getter = property && property.get
307 | const setter = property && property.set
308 |
309 | Object.defineProperty(obj, key, {
310 | enumerable: true,
311 | configurable: true,
312 | get: function reactiveGetter () {
313 | // 这里有一点要注意,这里的getter是个闭包,所以我们每个key生成的dep都存了下来
314 | const value = getter ? getter.call(obj) : val
315 | // Dep.target 是什么呢?后面会讲
316 | if (Dep.target) {
317 | dep.depend()
318 | if (childOb) {
319 | childOb.dep.depend()
320 | if (Array.isArray(value)) {
321 | dependArray(value)
322 | }
323 | }
324 | }
325 | return value
326 | },
327 | set: function reactiveSetter (newVal) {
328 | const value = getter ? getter.call(obj) : val //先算出原先的值
329 | /* eslint-disable no-self-compare */
330 | if (newVal === value || (newVal !== newVal && value !== value)) {
331 | return
332 | }
333 | /* eslint-enable no-self-compare */
334 | if (process.env.NODE_ENV !== 'production' && customSetter) {
335 | customSetter()
336 | }
337 | if (setter) {
338 | setter.call(obj, newVal)
339 | } else {
340 | val = newVal
341 | }
342 | childOb = !shallow && observe(newVal) //observe(newvalue) 但是如果只是value会直接return
343 | dep.notify()
344 | // 触发dep.notify()方法 subs[i].update()
345 | // subs 存储的是watcher实例
346 | }
347 | })
348 | }
349 | ```
350 | ## 阶段二 进入$mount() 这个方法里面比较重要
351 | initState() 阶段完成以后,后面会执行到mount() 方法,这个方法比较关键我们一起来看下。
352 |
353 | ```javascript
354 | Vue.prototype._init = function (options) {
355 | initLifecycle(vm);
356 | initEvents(vm);
357 | initRender(vm);
358 | callHook(vm, 'beforeCreate');
359 | initInjections(vm); // resolve injections before data/props
360 | initState(vm);
361 | initProvide(vm); // resolve provide after data/props
362 | callHook(vm, 'created');
363 |
364 | /* istanbul ignore if */
365 | if ("development" !== 'production' && config.performance && mark) {
366 | vm._name = formatComponentName(vm, false);
367 | mark(endTag);
368 | measure(("vue " + (vm._name) + " init"), startTag, endTag);
369 | }
370 |
371 | if (vm.$options.el) {
372 | vm.$mount(vm.$options.el);
373 | }
374 | }
375 | ```
376 |
377 | 我们先来看一下$mount方法,我们发现一开始有一段赋值,其实就是先存下来Vue上的公共mount方法,然后又重写了公共的mount方法。
378 |
379 | ```javascript
380 | const mount = Vue.prototype.$mount
381 | Vue.prototype.$mount = function (
382 | el?: string | Element,
383 | hydrating?: boolean
384 | ): Component {
385 | ...
386 | }
387 | ```
388 |
389 |
390 |
391 | ```javascript
392 |
393 | const idToTemplate = cached(id => {
394 | const el = query(id)
395 | return el && el.innerHTML
396 | })
397 |
398 | const mount = Vue.prototype.$mount
399 | Vue.prototype.$mount = function (
400 | el?: string | Element,
401 | hydrating?: boolean
402 | ): Component {
403 | el = el && query(el)
404 | const options = this.$options
405 | if (!options.render) {
406 | // 我们会进入这个地方,因为我们没有写render函数,写的是template模板
407 | let template = options.template
408 | if (template) {
409 | if (typeof template === 'string') {
410 | if (template.charAt(0) === '#') {
411 | ...
412 | }
413 | } else if (template.nodeType) {
414 | template = template.innerHTML
415 | } else {
416 | ...
417 | }
418 | } else if (el) {
419 | template = getOuterHTML(el)
420 | }
421 |
422 | if (template) {
423 | //走到这里, 进入了compileToFunctions方法 重点注意开始编译环节
424 | const { render, staticRenderFns } = compileToFunctions(template, {
425 | shouldDecodeNewlines,
426 | delimiters: options.delimiters
427 | }, this)
428 | options.render = render
429 | options.staticRenderFns = staticRenderFns
430 | }
431 | }
432 | return mount.call(this, el, hydrating)
433 | }
434 |
435 | function getOuterHTML (el: Element): string {
436 | if (el.outerHTML) {
437 | return el.outerHTML
438 | } else {
439 | const container = document.createElement('div')
440 | container.appendChild(el.cloneNode(true))
441 | return container.innerHTML
442 | }
443 | }
444 | ```
445 | compileToFunctions方法做了什么事情呢,我先大体的介绍一下。它的最终目的是让template字符串模板——>render function 函数。compile这个编译过程在Vue2会经历3个阶段:
446 |
447 | * 把html生成**ast语法树** (Vue 源码中借鉴 jQuery 作者 [John Resig](https://zh.wikipedia.org/wiki/%E7%B4%84%E7%BF%B0%C2%B7%E9%9B%B7%E8%A5%BF%E6%A0%BC) 的 [HTML Parser](http://ejohn.org/blog/pure-javascript-html-parser/) 对模板进行解析)
448 | * 对ast语法树进行静态优化**optimize()** (找到静态结点,做标记就是在ast上添加了static属性优化diff)
449 | * 根据优化过的ast **generate**生成render function 字符串
450 |
451 | ```javascript
452 | var createCompiler = createCompilerCreator(function baseCompile (
453 | template,
454 | options
455 | ) {
456 | var ast = parse(template.trim(), options);
457 | if (options.optimize !== false) {
458 | optimize(ast, options);
459 | }
460 | var code = generate(ast, options);
461 | return {
462 | ast: ast,
463 | render: code.render,
464 | staticRenderFns: code.staticRenderFns
465 | }
466 | });
467 | var ref$1 = createCompiler(baseOptions);
468 | var compileToFunctions = ref$1.compileToFunctions;
469 |
470 | function createCompileToFunctionFn (compile) {
471 | var cache = Object.create(null);
472 |
473 | return function compileToFunctions (
474 | template,
475 | options,
476 | vm
477 | ) {
478 | options = extend({}, options);
479 | var warn$$1 = options.warn || warn;
480 | delete options.warn;
481 | ....
482 |
483 | // 走到了这里
484 | var key = options.delimiters
485 | ? String(options.delimiters) + template
486 | : template;
487 | if (cache[key]) {
488 | return cache[key]
489 | }
490 |
491 | // 进入这里开始compile, 我们先来分析这个compile
492 | var compiled = compile(template, options);
493 | ...
494 | var res = {};
495 | var fnGenErrors = [];
496 | res.render = createFunction(compiled.render, fnGenErrors);
497 | res.staticRenderFns = compiled.staticRenderFns.map(function (code) {
498 | return createFunction(code, fnGenErrors)
499 | });
500 |
501 | // check function generation errors.
502 | // this should only happen if there is a bug in the compiler itself.
503 | // mostly for codegen development use
504 | /* istanbul ignore if */
505 | {
506 | if ((!compiled.errors || !compiled.errors.length) && fnGenErrors.length) {
507 | warn$$1(
508 | "Failed to generate render function:\n\n" +
509 | fnGenErrors.map(function (ref) {
510 | var err = ref.err;
511 | var code = ref.code;
512 |
513 | return ((err.toString()) + " in\n\n" + code + "\n");
514 | }).join('\n'),
515 | vm
516 | );
517 | }
518 | }
519 |
520 | return (cache[key] = res)
521 | }
522 | }
523 |
524 | ```
525 | compile(template, options);会进入baseCompile方法, 我们来看下baseCompile方法里面的细节
526 |
527 | ```javascript
528 | var createCompiler = createCompilerCreator(function baseCompile (
529 | template,
530 | options
531 | ) {
532 | //可以看到我们大致的流程 ast->optimize->generate
533 | var ast = parse(template.trim(), options);
534 | if (options.optimize !== false) {
535 | optimize(ast, options);
536 | }
537 | var code = generate(ast, options);
538 | return {
539 | ast: ast,
540 | render: code.render,
541 | staticRenderFns: code.staticRenderFns
542 | }
543 | });
544 | ```
545 |
546 |
547 |
548 | ### 阶段二(一) 生成ast
549 |
550 | ```javascript
551 |
552 | {{a+b}}
553 | 静态文本考拉地址
554 |
555 | ```
556 | 大致理解一下html-parser吧, html-parser 会按照下面几步进行html的解析
557 | 1. 首先定义基本的ast结构
558 | ```javascript
559 | const element1 = {
560 | type: 1,
561 | tag: "section",
562 | attrsList: [{name: "id", value: "test"}],
563 | attrsMap: {id: "app"},
564 | parent: undefined,
565 | children: []
566 | }
567 | ```
568 | 2.对ast进行预处理(preTransforms)
569 | 对ast的预处理在weex中才会有,我们直接跳过。
570 | 3、 解析v-pre、v-if、v-for、v-once、slot、key、ref等指令。
571 | 4、 对ast的class 和 style中的属性进行处理
572 | 5、 解析v-bind、v-on以及普通属性
573 | 6、 根节点或v-else块等处理
574 | 7、 模板元素父子关系的建立
575 | 8、 对ast后处理(postTransforms)
576 |
577 | 最终生成的ast长下面这样:
578 |
579 | ```javascript
580 | {
581 | type: 1, //element类型
582 | tag: "section",
583 | attrsList:[{
584 | name: "id",
585 | value: "test"
586 | }],
587 | attrsMap: {id: "test"},
588 | children:[
589 | {
590 | type: 1,
591 | tag: "div",
592 | attrsList:[],
593 | attrsMap:{},
594 | children:[{
595 | type: 2,
596 | expression: "_s(a+b)",
597 | tokens:[{@binding: "a+b"}]
598 | }]},
599 | parent: {...},
600 | plain: true
601 | },
602 | {
603 | type: 3,
604 | text: ""
605 | },
606 | {
607 | type: 1,
608 | tag: "p",
609 | attrsList: [],
610 | attrsMap: {},
611 | parent: {...},
612 | children:[
613 | {
614 | type:3,
615 | text: "静态文本"
616 | },
617 | {
618 | ...
619 | }
620 | ]
621 | }
622 | ]
623 | }
624 | ```
625 |
626 |
627 |
628 | ## 阶段二 optimize() 静态结点标记
629 | 源码位置: src/compiler/optimizer.js
630 | ```javascript
631 | function markStatic (node: ASTNode) {
632 | // 通过isStatic 方法来判断node结点是否为静态结点
633 | node.static = isStatic(node)
634 | if (node.type === 1) {
635 | // do not make component slot content static. this avoids
636 | // 1. components not able to mutate slot nodes
637 | // 2. static slot content fails for hot-reloading
638 | if (
639 | !isPlatformReservedTag(node.tag) &&
640 | node.tag !== 'slot' &&
641 | node.attrsMap['inline-template'] == null
642 | ) {
643 | return
644 | }
645 | for (let i = 0, l = node.children.length; i < l; i++) {
646 | const child = node.children[i]
647 | markStatic(child)
648 | if (!child.static) {
649 | node.static = false
650 | }
651 | }
652 | }
653 | }
654 | ```
655 | 看一下isStatic方法
656 | ```javascript
657 | function isStatic (node: ASTNode): boolean {
658 | if (node.type === 2) { // expression
659 | return false // 表达式肯定不是静态结点
660 | }
661 | if (node.type === 3) { // text
662 | return true // 文本肯定是静态结点
663 | }
664 | return !!(node.pre // v-pre 指令,此时子节点是不做编译的
665 | || (
666 | !node.hasBindings && // no dynamic bindings
667 | !node.if && !node.for && // not v-if or v-for or v-else
668 | !isBuiltInTag(node.tag) && // not a built-in 内置标签包括slot 和 component
669 | isPlatformReservedTag(node.tag) && // 是平台保留标签html和svg标签
670 | !isDirectChildOfTemplateFor(node) &&
671 | Object.keys(node).every(isStaticKey) // 不是template标签的直接子元素且没有包含在for循环中
672 | ))
673 | }
674 | ```
675 | 然后我们的ast 会变成下面这样,标记了是否为静态结点和是否为静态根结点, 增加了一个static属性
676 | ```javascript
677 | {
678 | type: 1, //element类型
679 | tag: "section",
680 | attrsList:[{
681 | name: "id",
682 | value: "test"
683 | }],
684 | attrsMap: {id: "test"},
685 | children:[
686 | {
687 | type: 1,
688 | tag: "div",
689 | attrsList:[],
690 | attrsMap:{},
691 | children:[{
692 | type: 2,
693 | expression: "_s(a+b)",
694 | tokens:[{@binding: "a+b"}]
695 | }]},
696 | parent: {...},
697 | plain: true,
698 | static: false
699 | },
700 | {
701 | type: 3,
702 | text: "",
703 | static: true
704 | },
705 | {
706 | type: 1,
707 | tag: "p",
708 | attrsList: [],
709 | attrsMap: {},
710 | parent: {...},
711 | children:[
712 | {
713 | type:3,
714 | text: "静态文本"
715 | },
716 | {
717 | ...
718 | }
719 | ],
720 | static: true
721 | }
722 | ],
723 | plain: false,
724 | static: false
725 | }
726 | ```
727 | ## 阶段二(三) generate() 生成render function
728 | 源码位置:src/compiler/codegen/index.js
729 | 拿到ast结构以后,进入generate函数 var code = generate(ast, options);
730 | ```javascript
731 | export function generate (
732 | ast: ASTElement | void,
733 | options: CompilerOptions
734 | ) {
735 | // save previous staticRenderFns so generate calls can be nested
736 | ....
737 | const code = ast ? genElement(ast) : '_c("div")'
738 | staticRenderFns = prevStaticRenderFns
739 | onceCount = prevOnceCount
740 | // 返回的整体结构
741 | return {
742 | render: `with(this){return ${code}}`,
743 | staticRenderFns: currentStaticRenderFns
744 | }
745 | }
746 | ```
747 | 重点看下genElement 方法
748 | ```javascript
749 | function genElement (el: ASTElement): string {
750 | if (el.staticRoot && !el.staticProcessed) {
751 | return genStatic(el)
752 | } else if (el.once && !el.onceProcessed) {
753 | ...
754 | } else {
755 | // component or element
756 | let code
757 | if (el.component) {
758 | ...
759 | } else {
760 | // 大部分进入这里,plain 代表元素上是否没有属性
761 | // 后面先看下genData 方法
762 | const data = el.plain ? undefined : genData(el)
763 | // 后面看下genChildren方法
764 | const children = el.inlineTemplate ? null : genChildren(el, true);
765 |
766 | code = `_c('${el.tag}' ${
767 | data ? `,${data}` : '' // data
768 | }${
769 | children ? `,${children}` : '' // children
770 | })`
771 | }
772 | // module transforms
773 | for (let i = 0; i < transforms.length; i++) {
774 | code = transforms[i](el, code)
775 | }
776 | return code
777 | }
778 | }
779 | ```
780 | ```javascript
781 | function genData (el: ASTElement): string {
782 | let data = '{'
783 | ...
784 | if (el.attrs) {
785 | data += `attrs:{${genProps(el.attrs)}},`
786 | }
787 | ...
788 | data = data.replace(/,$/, '') + '}'
789 | ...
790 | return data // data 的结构是 attrs: {id: 'app'}
791 | }
792 |
793 | function genChildren (el: ASTElement, checkSkip?: boolean): string | void {
794 | const children = el.children
795 | if (children.length) {
796 | const el: any = children[0]
797 | // optimize single v-for
798 | if (children.length === 1 &&
799 | el.for &&
800 | el.tag !== 'template' &&
801 | el.tag !== 'slot') {
802 | return genElement(el)
803 | }
804 | const normalizationType = checkSkip ? getNormalizationType(children) : 0
805 | return `[${children.map(genNode).join(',')}]${
806 | normalizationType ? `,${normalizationType}` : ''
807 | }`
808 | }
809 | }
810 | ```
811 | 最后生成的就是下面这个对象
812 | ```javascript
813 | {
814 | render: "with(this){return _c('section',{attrs:{\"id\":\"test\"}},[_c('div',[_v(_s(a+b))]),_v(\" \"),_m(0)])}",
815 | staticRenderFns: [
816 | "with(this){return _c('p',[_v(\"静态文本\"),_c('a',{attrs:{\"href\":\"www.koala.com\"}},[_v(\"考拉地址\")])])}"
817 | ]
818 | }
819 |
820 | // render 是render function 字符串
821 | // staticRenderFns 是静态渲染函数数组
822 | ```
823 | 之后调用了 createFunction 方法其实就是new Function('string')。所以其实最终我们得到的是 render function
824 | ```javascript
825 | res.render = createFunction(compiled.render, fnGenErrors);
826 | res.staticRenderFns = compiled.staticRenderFns.map(function (code) {
827 | return createFunction(code, fnGenErrors)
828 | });
829 | options.render = render;
830 | options.staticRenderFns = staticRenderFns;
831 |
832 | function createFunction (code, errors) {
833 | try {
834 | return new Function(code)
835 | } catch (err) {
836 | errors.push({ err: err, code: code });
837 | return noop
838 | }
839 | }
840 | ```
841 | 然后compile结束后,我们得到render function 之后,开始执行$mount的公用方法(源码位置src/platforms/web/runtime/index.js) 其实就是调用了mountComponent方法
842 | ```javascript
843 | // 接下来进入这个方法
844 | return mount.call(this, el, hydrating)
845 | // public mount method
846 | Vue.prototype.$mount = function (
847 | el?: string | Element,
848 | hydrating?: boolean
849 | ): Component {
850 | el = el && inBrowser ? query(el) : undefined
851 | // 本质是进入这个方法
852 | return mountComponent(this, el, hydrating)
853 | }
854 | ```
855 |
856 | ## 阶段四 执行renderFunction 得到VNode
857 | ```javascript
858 | export function mountComponent (
859 | vm: Component,
860 | el: ?Element,
861 | hydrating?: boolean
862 | ): Component {
863 | ...
864 |
865 | callHook(vm, 'beforeMount')
866 | let updateComponent
867 | /* istanbul ignore if */
868 | if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
869 | ...
870 | } else {
871 |
872 | updateComponent = () => {
873 | // 重点看这里执行了render()方法生成了vnode
874 | vm._update(vm._render(), hydrating)
875 | }
876 | }
877 |
878 | // 入口:下面执行了new Watcher, 这是我们的重点入口[1]
879 | new Watcher(vm, updateComponent, noop, null, true /* isRenderWatcher */)
880 | hydrating = false
881 |
882 | // manually mounted instance, call mounted on self
883 | // mounted is called for render-created child components in its inserted hook
884 | if (vm.$vnode == null) {
885 | vm._isMounted = true
886 | callHook(vm, 'mounted')
887 | }
888 | return vm
889 | }
890 | ```
891 | ## 阶段五 进入Watcher 类
892 | ```javascript
893 | var Watcher = function Watcher (
894 | vm,
895 | expOrFn,
896 | cb,
897 | options,
898 | isRenderWatcher
899 | ) {
900 | ...
901 | if (typeof expOrFn === 'function') {
902 | this.getter = expOrFn;
903 | } else {
904 | this.getter = parsePath(expOrFn);
905 | if (!this.getter) {
906 | this.getter = function () {};
907 | "development" !== 'production' && warn(
908 | "Failed watching path: \"" + expOrFn + "\" " +
909 | 'Watcher only accepts simple dot-delimited paths. ' +
910 | 'For full control, use a function instead.',
911 | vm
912 | );
913 | }
914 | }
915 | // 重点看下这里,调用了Watcher的get方法,我们看下get方法
916 | this.value = this.lazy
917 | ? undefined
918 | : this.get();
919 | };
920 |
921 | Watcher.prototype.get = function get () {
922 | // 这里有个关键点
923 | pushTarget(this);
924 | var value;
925 | var vm = this.vm;
926 | try {
927 | value = this.getter.call(vm, vm);
928 | } catch (e) {
929 | if (this.user) {
930 | handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
931 | } else {
932 | throw e
933 | }
934 | } finally {
935 | // "touch" every property so they are all tracked as
936 | // dependencies for deep watching
937 | if (this.deep) {
938 | traverse(value);
939 | }
940 | popTarget();
941 | this.cleanupDeps();
942 | }
943 | return value
944 | };
945 | ```
946 | watcher 构造函数最后调用了this.get() , 首先调用了pushTarget(this)方法。这个方法把Dep.target设为this(即当前watcher实例)。然后执行了this.getter.call(vm, vm);这个this.getter 就是 this.getter = expOrFn; 即我们传入new Watcher 里面的第二个参数,updateComponent。updateComponent做了什么呢?它先执行了vm._render,然后执行了 vm._ update
947 |
948 | ```javascript
949 | Watcher.prototype.get = function get () {
950 | pushTarget(this);
951 | var value;
952 | var vm = this.vm;
953 | try {
954 | //getter对应new Watcher时我们传入的第二个参数 new Watcher(vm, updateComponent, noop, null, true /* isRenderWatcher */)即updateComponent
955 | value = this.getter.call(vm, vm);
956 | } catch (e) {
957 | if (this.user) {
958 | handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
959 | } else {
960 | throw e
961 | }
962 | } finally {
963 | // "touch" every property so they are all tracked as
964 | // dependencies for deep watching
965 | if (this.deep) {
966 | traverse(value);
967 | }
968 | popTarget();
969 | this.cleanupDeps();
970 | }
971 | return value
972 | };
973 |
974 | function pushTarget (_target) {
975 | if (Dep.target) { targetStack.push(Dep.target); }
976 | Dep.target = _target;
977 | }
978 |
979 | updateComponent = function () {
980 | vm._update(vm._render(), hydrating);
981 | };
982 | ```
983 |
984 | ## 阶段六 调用render()生成VDom
985 | 下面进入了updateComponent方法,会先执行vm._render(), 我们来看下vm._render()调用后得到了什么。
986 |
987 | ```javascript
988 | Vue.prototype._render = function () {
989 | var vm = this;
990 | var ref = vm.$options;
991 | var render = ref.render;
992 | var _parentVnode = ref._parentVnode;
993 |
994 | ...
995 | vm.$vnode = _parentVnode;
996 | // render self
997 | var vnode;
998 | try {
999 | // 重点看这里,其实就是执行了我们的render function
1000 | vnode = render.call(vm._renderProxy, vm.$createElement);
1001 | } catch (e) {
1002 | ...
1003 | }
1004 | // return empty vnode in case the render function errored out
1005 | if (!(vnode instanceof VNode)) {
1006 | if ("development" !== 'production' && Array.isArray(vnode)) {
1007 | warn(
1008 | 'Multiple root nodes returned from render function. Render function ' +
1009 | 'should return a single root node.',
1010 | vm
1011 | );
1012 | }
1013 | vnode = createEmptyVNode();
1014 | }
1015 | // set parent
1016 | vnode.parent = _parentVnode;
1017 | return vnode
1018 | };
1019 | }
1020 | ```
1021 |
1022 | vnode = render.call(vm._renderProxy, vm.$createElement);这个方法的调用,相当于执行了我们之前得到的render function。并且我们知道render function 里面有with(this) 此时我们把this指向了vm, 所以按照我们的例子, s(a+b) 在执行的时候会读取到vm.a 和 vm.b 就分别进入了a 和 b 的getter。再回头看下getter函数。
1023 |
1024 | 所以注意这里就是Watcher 与 Dep 关联的地方,可以认为compile 是Watcher 与 Dep之间的桥梁。
1025 |
1026 | ```javascript
1027 | Object.defineProperty(obj, key, {
1028 | enumerable: true,
1029 | configurable: true,
1030 | get: function reactiveGetter () {
1031 | var value = getter ? getter.call(obj) : val;
1032 | // 此时Dep.target 指向this, 因为watch实例生成时调用了pushTarget(this)
1033 | if (Dep.target) {
1034 | // 看下dep.depend 方法
1035 | dep.depend();
1036 | if (childOb) {
1037 | childOb.dep.depend();
1038 | if (Array.isArray(value)) {
1039 | dependArray(value);
1040 | }
1041 | }
1042 | }
1043 | return value
1044 | },
1045 | set: function() {...}
1046 | }
1047 |
1048 | Dep.prototype.depend = function depend () {
1049 | if (Dep.target) {
1050 | // 调用的是watcher的addDep方法
1051 | Dep.target.addDep(this);
1052 | }
1053 | };
1054 | Watcher.prototype.addDep = function addDep (dep) {
1055 | var id = dep.id;
1056 | if (!this.newDepIds.has(id)) {
1057 | this.newDepIds.add(id);
1058 | this.newDeps.push(dep);
1059 | if (!this.depIds.has(id)) {
1060 | // 这里调用了dep的addSub方法
1061 | dep.addSub(this);
1062 | }
1063 | }
1064 | };
1065 | Dep.prototype.addSub = function addSub (sub) {
1066 | this.subs.push(sub);
1067 | };
1068 | ```
1069 |
1070 | vm.render()方法最终执行完会生成VNode, 即完成了从render function —> VNode 的过程,我们看下此时vnode的样子, 大概像下面这样
1071 |
1072 | ```javascript
1073 | {
1074 | asyncFactory: undefined,
1075 | asyncMeta: undefined,
1076 | children:[
1077 | {tag: 'div', data: undefined, children:[VNode], text: undefined, elm: undefind ...},
1078 | {tag: undefined, data: undefined, children:undefined, text: "", elm: undefind ...},
1079 | {tag: 'p', data: undefined, children:[VNode, VNode], text: undefined, elm: undefind ...},
1080 | ],
1081 | context: vm,
1082 | data: {
1083 | attrs: {id: "test"}
1084 | },
1085 | ....
1086 | isStatic: false,
1087 | tag: 'section'
1088 | ...
1089 | }
1090 | ```
1091 |
1092 | 得到VNode 之后,调用vm.update 方法从VNode 生成DOM。update方法内部重点调用了patch方法,看下面。因为patch内容也比较复杂,所以此次并不讲解内部相关的具体流程,会大致看下dom创建的过程。patch方法内部大致会涉及下面3部分的处理:
1093 |
1094 | * 根据vnode 创建dom
1095 | * diff 算法
1096 | * 自定义组件处理
1097 |
1098 | ```javascript
1099 | Vue.prototype._update = function (vnode, hydrating) {
1100 | ...
1101 | vm.$el = vm.__patch__(
1102 | vm.$el, vnode, hydrating, false /* removeOnly */,
1103 | vm.$options._parentElm,
1104 | vm.$options._refElm
1105 | );
1106 | }
1107 | // patch方法内部通过调用createElm 来生成node
1108 | createElm(
1109 | vnode,
1110 | insertedVnodeQueue,
1111 | // extremely rare edge case: do not insert if old element is in a
1112 | // leaving transition. Only happens when combining transition +
1113 | // keep-alive + HOCs. (#4590)
1114 | oldElm._leaveCb ? null : parentElm$1,
1115 | nodeOps.nextSibling(oldElm)
1116 | );
1117 | // createElm 中主要通过下面这个方法来创建dom
1118 | vnode.elm = vnode.ns
1119 | ? nodeOps.createElementNS(vnode.ns, tag)
1120 | : nodeOps.createElement(tag, vnode)
1121 |
1122 | //nodeOps是什么,是一些创建dom相关的方法
1123 | import { namespaceMap } from 'web/util/index'
1124 | export function createElement (tagName: string, vnode: VNode): Element {
1125 | const elm = document.createElement(tagName)
1126 | if (tagName !== 'select') {
1127 | return elm
1128 | }
1129 | // false or null will remove the attribute but undefined will not
1130 | if (vnode.data && vnode.data.attrs && vnode.data.attrs.multiple !== undefined) {
1131 | elm.setAttribute('multiple', 'multiple')
1132 | }
1133 | return elm
1134 | }
1135 |
1136 | export function createElementNS (namespace: string, tagName: string): Element {
1137 | return document.createElementNS(namespaceMap[namespace], tagName)
1138 | }
1139 |
1140 | ...
1141 | ```
1142 |
1143 | 最后我们的结点生成并挂载了vnode.elm 上,打出来看下
1144 |
1145 | ```javascro[t
1146 | > vnode.elm
1147 |
1148 | 3
1149 |
1150 | 静态文本
1151 |
1152 |
1153 |
1154 | ```
1155 |
1156 | 接下来就会触发 insert (parent, elm, ref$$1) parent 是body, elm是上面得到的dom, 第三个参数没研究,此时elm就插入到了parent中。接下来又会回到mountComponent 接下去的方法
1157 |
1158 | ```javascript
1159 | function mountComponent () {
1160 | ...
1161 | new Watcher(...)
1162 | hydrating = false;
1163 | if(vm.$vnode == null){
1164 | vm._isMounted = true;
1165 | callHook(vm, 'mounted'); //触发了mounted钩子
1166 | }
1167 | return vm;
1168 | }
1169 |
1170 | ```
1171 |
1172 |
1173 |
1174 |
1175 | ## 当data更新
1176 | 当我们data的值发生了变化的时候,会进入setter函数。
1177 |
1178 | ```javascript
1179 | set: function reactiveSetter (newVal) {
1180 | var value = getter ? getter.call(obj) : val;
1181 | /* eslint-disable no-self-compare */
1182 | if (newVal === value || (newVal !== newVal && value !== value)) {
1183 | return
1184 | }
1185 | /* eslint-enable no-self-compare */
1186 | if ("development" !== 'production' && customSetter) {
1187 | customSetter();
1188 | }
1189 | if (setter) {
1190 | setter.call(obj, newVal);
1191 | } else {
1192 | val = newVal;
1193 | }
1194 | childOb = !shallow && observe(newVal);
1195 | // 进入dep.notify 通知watcher
1196 | dep.notify();
1197 | }
1198 | Dep.prototype.notify = function notify () {
1199 | // stabilize the subscriber list first
1200 | var subs = this.subs.slice();
1201 | for (var i = 0, l = subs.length; i < l; i++) {
1202 | subs[i].update();
1203 | }
1204 | };
1205 | Watcher.prototype.update = function update () {
1206 | /* istanbul ignore else */
1207 | if (this.lazy) {
1208 | this.dirty = true;
1209 | } else if (this.sync) {
1210 | this.run();
1211 | } else {
1212 | // 进入这里 这个方法最终会进入watcher.prototype.run 方法
1213 | queueWatcher(this);
1214 | }
1215 | };
1216 |
1217 | Watcher.prototype.run = function run () {
1218 | if (this.active) {
1219 | // 这里会调用this.get, 之前有讲过this.get里面会调用updateComponent,所以又会走到
1220 | // vm._update(vm._render(), hydrating);这个方法重新更新视图了
1221 | var value = this.get();
1222 | if (
1223 | value !== this.value ||
1224 | // Deep watchers and watchers on Object/Arrays should fire even
1225 | // when the value is the same, because the value may
1226 | // have mutated.
1227 | isObject(value) ||
1228 | this.deep
1229 | ) {
1230 | // set new value
1231 | var oldValue = this.value;
1232 | this.value = value;
1233 | if (this.user) {
1234 | try {
1235 | this.cb.call(this.vm, value, oldValue);
1236 | } catch (e) {
1237 | handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\""));
1238 | }
1239 | } else {
1240 | this.cb.call(this.vm, value, oldValue);
1241 | }
1242 | }
1243 | }
1244 | };
1245 | Watcher.prototype.get = function get () {
1246 | // 这里有个关键点
1247 | pushTarget(this);
1248 | var value;
1249 | var vm = this.vm;
1250 | try {
1251 | // 注意this.getter = expFunction
1252 | value = this.getter.call(vm, vm);
1253 | } catch (e) {
1254 | if (this.user) {
1255 | handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
1256 | } else {
1257 | throw e
1258 | }
1259 | } finally {
1260 | // "touch" every property so they are all tracked as
1261 | // dependencies for deep watching
1262 | if (this.deep) {
1263 | traverse(value);
1264 | }
1265 | popTarget();
1266 | this.cleanupDeps();
1267 | }
1268 | return value
1269 | };
1270 | ```
1271 |
1272 | 所以我们此时就理解了整个Observer,Dep 与Watcher之间的关系
1273 |
1274 | ###### 
1275 |
1276 |
1277 |
1278 | # 参考文献
1279 |
1280 | 1.[Vue2.0 源码阅读:模板渲染](https://zhouweicsu.github.io/blog/2017/04/21/vue-2-0-template/)
1281 |
1282 | 2.[compile—优化静态内容](https://github.com/liutao/vue2.0-source/blob/31838b20db51519794eceebc07ee82e8c01e7ef7/compile%E2%80%94%E2%80%94%E4%BC%98%E5%8C%96%E9%9D%99%E6%80%81%E5%86%85%E5%AE%B9.md)
1283 |
1284 | 3.[深入vue2.0底层思想——模板渲染](https://juejin.im/entry/59636d186fb9a06bc903b80e)
1285 |
1286 | 4.[Vue2 源码漫游(二)](https://segmentfault.com/a/1190000012002376)
1287 |
1288 | 5.[Vitual DOM 的内部工作原理](http://efe.baidu.com/blog/the-inner-workings-of-virtual-dom/)
1289 |
1290 |
1291 |
1292 |
1293 |
1294 |
--------------------------------------------------------------------------------
/js/浅析Vue 中的patch和diff(上).md:
--------------------------------------------------------------------------------
1 | ## 疑问
2 |
3 | 1.当我修改了属性值时,vdom立即进行diff,重新渲染视图了吗?
4 |
5 | 2.如果1是对的,那重复修改,性能岂不是很差?如果不是,1是如何实现的?
6 |
7 | 3.我们的nextTick 具体的实现是怎样的?什么时候需要用到它?
8 |
9 | 4.vdom diff的过程是怎样的?
10 |
11 | ## 梗概
12 |
13 | 关于vue数据更新渲染的几个知识点,先列一下:
14 |
15 | * 数据的更新是实时的,但是渲染是异步的。
16 |
17 | * 一旦数据变化,会把在同一个事件循环event loop中的观察到的watcher 推入一个队列(相同watcher实例不会重复推入)
18 |
19 | * DOM并不是马上更新视图的(想想也不可能,改动一次数据更新一次视图,肯定都是批量操作DOM的),vue 中的nextTick 用到了MicroTask和MacroTask,这需要我们去了解event loop(推荐先阅读下:[Tasks, microtasks, queues and schedules](https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/)
20 | )
21 |
22 | * 整个script是一个主任务, setTimeout 是一个macroTask, promise的回调是microtask, 顺序是
23 |
24 | 主script(第一个主任务) —> microTask (全部执行完)—> UI渲染 —> 下一个macroTask
25 |
26 | ```javascript
27 | // 先不看结果,想一下你的输出
28 | console.log('script start');
29 |
30 | setTimeout(function() {
31 | console.log('setTimeout');
32 | }, 0);
33 |
34 | Promise.resolve().then(function() {
35 | console.log('promise1');
36 | }).then(function() {
37 | console.log('promise2');
38 | });
39 |
40 | console.log('script end');
41 | // result:
42 | /**
43 | * script start
44 | * script end
45 | * promise1
46 | * promise2
47 | * setTimeout
48 | */
49 | ```
50 |
51 |
52 | * 所以dom diff 这个过程是在microtask中去处理的(也有的是强制走macrotask, 本例子走microtask)
53 | * 哪些会走macrotask 哪些会走microtask,为啥要区分,会写在拓展那一小节
54 |
55 | ## 例子
56 | 接下来,所有的讲解都会围绕下面这个例子
57 | ```javascript
58 |
59 |
60 |
61 |
62 |
67 |
68 |
69 |
70 |
71 |
72 |
77 |
99 |
100 |
101 | ```
102 |
103 | ## 入口
104 |
105 | #### 当```vm._update(vm._render(), hydrating)```
106 |
107 | 经过上一次分享,我们知道通过```vm._render()```方法,我们会获得我们的vdom; 接下去我们进入_update方法;我们看下内部的细节。
108 |
109 | ```javascript
110 | Vue.prototype._update = function (vnode, hydrating) {
111 | var vm = this;
112 | if (vm._isMounted) {
113 | callHook(vm, 'beforeUpdate');
114 | }
115 | ...
116 | // 初始时,我们是没有prevVnode的, 进入了patch方法
117 | if (!prevVnode) {
118 | // initial render
119 | // 初始化渲染,我们看下细节
120 | vm.$el = vm.__patch__(
121 | vm.$el, vnode, hydrating, false /* removeOnly */,
122 | vm.$options._parentElm,
123 | vm.$options._refElm
124 | );
125 | // no need for the ref nodes after initial patch
126 | // this prevents keeping a detached DOM tree in memory (#5851)
127 | vm.$options._parentElm = vm.$options._refElm = null;
128 | } else {
129 | // updates
130 | vm.$el = vm.__patch__(prevVnode, vnode);
131 | }
132 | activeInstance = prevActiveInstance;
133 | // update __vue__ reference
134 | if (prevEl) {
135 | prevEl.__vue__ = null;
136 | }
137 | if (vm.$el) {
138 | vm.$el.__vue__ = vm;
139 | }
140 | // if parent is an HOC, update its $el as well
141 | if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
142 | vm.$parent.$el = vm.$el;
143 | }
144 | // updated hook is called by the scheduler to ensure that children are
145 | // updated in a parent's updated hook.
146 | };
147 | ```
148 |
149 | #### 初始进入patch
150 |
151 | ```javascript
152 | // 当我们初始进入patch时,会进入createElm 根据我们的vnode 创建节点
153 | return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
154 | if (isUndef(vnode)) {
155 | if (isDef(oldVnode)) { invokeDestroyHook(oldVnode); }
156 | return
157 | }
158 | var isInitialPatch = false;
159 | var insertedVnodeQueue = [];
160 |
161 | if (isUndef(oldVnode)) {
162 | // empty mount (likely as component), create new root element
163 | isInitialPatch = true;
164 | createElm(vnode, insertedVnodeQueue, parentElm, refElm);
165 | } else {
166 | ...
167 | }
168 | }
169 | ```
170 |
171 | ## 数据更新时
172 |
173 | 假设我们的数据是```{a:1, b:1}```更新为了```{a:2, b:3}```, 我们下面看下细节
174 |
175 | * a值发生了变化, 进入它的setter ——> 进入 dep.notify() 依赖通知
176 | * dep.notify() ——> subs[i].update() subs存放的是watcher 实例,进入watcher的update()方法
177 | * watcher.update() ——> 将当前watcher 推入一个队列, 并且将flushSchedulerQueue(冲洗队列)这个动作放入nextTick(一个microtask 中),且将flushSchedulerQueue塞入callback数组
178 | * 继续往下走,b值发生变化,进入它的setter ——> 进入 dep.notify() 依赖通知
179 | * dep.notify() ——> subs[i].update() subs存放的是watcher 实例,进入watcher的update()方法
180 | * watcher.update() ——> 当前watcher已经放入队列,不再放入,继续往下
181 | * 遇到$nextTick(我们的cb) ——> 我们的cb也塞入callback数组
182 | * 主任务(script)全部走完 ——> 开始执行所有microtask
183 | * 开始执行flushCallBacks ——> flushSchedulerQueue(这里后面有watcher.run(), dom diff, 视图更新) ——> 我们的cb
184 | * 结束
185 | ```javascript
186 | /**
187 | mouted() {
188 | setTimeout(() => {
189 | data.a = 2;
190 | data.b = 3;
191 | this.$nextTick(()=> {
192 | console.log(document.querySelector('.f-error'));
193 | })
194 | }, 500);
195 | }
196 | **/
197 |
198 | new Vue({
199 | el: '#test',
200 | template: temp,
201 | data: function() {
202 | return data;
203 | },
204 | methods: {
205 | test() {
206 | data.a = 2;
207 | data.b = 3;
208 | }
209 | }
210 | });
211 |
212 | // 当我们data.a 的值改变时,会进入它的setter
213 | set: function reactiveSetter (newVal) {
214 | var value = getter ? getter.call(obj) : val;
215 | /* eslint-disable no-self-compare */
216 | if (newVal === value || (newVal !== newVal && value !== value)) {
217 | return
218 | }
219 | /* eslint-enable no-self-compare */
220 | if ("development" !== 'production' && customSetter) {
221 | customSetter();
222 | }
223 | if (setter) {
224 | setter.call(obj, newVal);
225 | } else {
226 | val = newVal;
227 | }
228 | childOb = !shallow && observe(newVal);
229 | // 当值更新时,dep会通知watcher
230 | dep.notify();
231 | }
232 |
233 | Dep.prototype.notify = function notify () {
234 | // stabilize the subscriber list first
235 | var subs = this.subs.slice();
236 | // 我们知道subs里面存放着我们的watcher实例, 进入watcher的update方法
237 | for (var i = 0, l = subs.length; i < l; i++) {
238 | subs[i].update();
239 | }
240 | };
241 |
242 | Watcher.prototype.update = function update () {
243 | /* istanbul ignore else */
244 | if (this.lazy) {
245 | this.dirty = true;
246 | } else if (this.sync) {
247 | this.run();
248 | } else {
249 | // 一般情况下,没有其他配置会进入这里,将我们的watcher推入队列
250 | queueWatcher(this);
251 | }
252 | };
253 |
254 | /**
255 | * Push a watcher into the watcher queue.
256 | * Jobs with duplicate IDs will be skipped unless it's
257 | * pushed when the queue is being flushed.
258 | */
259 | function queueWatcher (watcher) {
260 | var id = watcher.id;
261 | // 判断这个watcher是否已经放入过队列
262 | if (has[id] == null) {
263 | has[id] = true;
264 | if (!flushing) {
265 | queue.push(watcher);
266 | } else {
267 | // if already flushing, splice the watcher based on its id
268 | // if already past its id, it will be run next immediately.
269 | var i = queue.length - 1;
270 | while (i > index && queue[i].id > watcher.id) {
271 | i--;
272 | }
273 | queue.splice(i + 1, 0, watcher);
274 | }
275 | // queue the flush
276 | if (!waiting) {
277 | waiting = true;
278 | // 走到了这里, cb 是flushSchedulerQueue
279 | nextTick(flushSchedulerQueue);
280 | }
281 | }
282 | }
283 |
284 | // 进入了nextTick 方法,这里涉及到EventLoop相关的内容,后面会简单说一下
285 | function nextTick (cb, ctx) {
286 | var _resolve;
287 | // 将flushSchedulerQueue塞入cb
288 | callbacks.push(function () {
289 | if (cb) {
290 | try {
291 | cb.call(ctx);
292 | } catch (e) {
293 | handleError(e, ctx, 'nextTick');
294 | }
295 | } else if (_resolve) {
296 | _resolve(ctx);
297 | }
298 | });
299 | if (!pending) {
300 | pending = true;
301 | // 注意这里,一般情况下使用microTask但某些情境下会强制使用macroTask
302 | if (useMacroTask) {
303 | macroTimerFunc();
304 | } else {
305 | // 我们的例子会进入这里, microTimerFunc结果是什么呢?往下看
306 | microTimerFunc();
307 | }
308 | }
309 | // $flow-disable-line
310 | if (!cb && typeof Promise !== 'undefined') {
311 | return new Promise(function (resolve) {
312 | _resolve = resolve;
313 | })
314 | }
315 | }
316 |
317 | // Determine MicroTask defer implementation.
318 | /* istanbul ignore next, $flow-disable-line */
319 | if(typeof Promise !== 'undefined' && isNative(Promise)) {
320 | var p = Promise.resolve();
321 | microTimerFunc = function() {
322 | // 重点注意这里是promise的cb, 是一个microTask, 是在主script执行完才会执行的
323 | p.then(flushCallbacks);
324 | }
325 | }
326 | ```
327 |
328 | data.a执行完以后,开始走data.b, 流程都一样,只是当我们遇到watcher的update时有些区别
329 |
330 | ```javascript
331 | queueWatcher(this);
332 | function queueWatcher (watcher) {
333 | var id = watcher.id;
334 | // 判断这个watcher是否已经放入过队列, 当执行到data.b时已经放入过队列了, 所以不会继续往下走了(这个也很好理解)
335 | if (has[id] == null) {
336 | has[id] = true;
337 | if (!flushing) {
338 | queue.push(watcher);
339 | } else {
340 | // if already flushing, splice the watcher based on its id
341 | // if already past its id, it will be run next immediately.
342 | var i = queue.length - 1;
343 | while (i > index && queue[i].id > watcher.id) {
344 | i--;
345 | }
346 | queue.splice(i + 1, 0, watcher);
347 | }
348 | // queue the flush
349 | if (!waiting) {
350 | waiting = true;
351 | // 走到了这里
352 | nextTick(flushSchedulerQueue);
353 | }
354 | }
355 | }
356 | ```
357 |
358 | 然后进入```this.$nextTick```方法
359 |
360 | ```javascript
361 | Vue.prototype.$nextTick = function (fn) {
362 | return nextTick(fn, this)
363 | };
364 |
365 | function nextTick (cb, ctx) {
366 | var _resolve;
367 | // 将我们的cb塞入callbacks
368 | callbacks.push(function () {
369 | if (cb) {
370 | try {
371 | cb.call(ctx);
372 | } catch (e) {
373 | handleError(e, ctx, 'nextTick');
374 | }
375 | } else if (_resolve) {
376 | _resolve(ctx);
377 | }
378 | });
379 | // 因为上一次pending 已经置为true,所以此时不符合条件
380 | if (!pending) {
381 | pending = true;
382 | if (useMacroTask) {
383 | macroTimerFunc();
384 | } else {
385 | microTimerFunc();
386 | }
387 | }
388 | // $flow-disable-line
389 | if (!cb && typeof Promise !== 'undefined') {
390 | return new Promise(function (resolve) {
391 | _resolve = resolve;
392 | })
393 | }
394 | }
395 | ```
396 |
397 | 接下去,就到了我们之前讲到的,主script执行完了开始执行microTask, 进入flushSchedulerQueue方法。(tips: 推荐阅读[Tasks, microtasks, queues and schedules]( https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/)更好地了解EventLoop)
398 |
399 | ```javascript
400 | // p.then(flushCallbacks);
401 | function flushCallbacks () {
402 | pending = false;
403 | var copies = callbacks.slice(0);
404 | callbacks.length = 0;
405 | for (var i = 0; i < copies.length; i++) {
406 | copies[i]();
407 | }
408 | }
409 | // 开始遍历callbacks 执行其中的cb
410 | // 第一个cb 是 flushSchedulerQueue
411 | // 第二个cb 是 我们的 console.log(document.querySelector('.f-error'));
412 |
413 | // 第一个cb里面,watcher.run 最终会进入vdom的diff, 下一篇具体讲细节
414 | function flushSchedulerQueue () {
415 | flushing = true;
416 | var watcher, id;
417 | ...
418 | // do not cache length because more watchers might be pushed
419 | // as we run existing watchers
420 | for (index = 0; index < queue.length; index++) {
421 | watcher = queue[index];
422 | id = watcher.id;
423 | has[id] = null;
424 | watcher.run();
425 | // in dev build, check and stop circular updates.
426 | ...
427 | }
428 |
429 | // keep copies of post queues before resetting state
430 | var activatedQueue = activatedChildren.slice();
431 | var updatedQueue = queue.slice();
432 | resetSchedulerState();
433 |
434 | // call component updated and activated hooks
435 | callActivatedHooks(activatedQueue);
436 | callUpdatedHooks(updatedQueue);
437 |
438 | // devtool hook
439 | /* istanbul ignore if */
440 | if (devtools && config.devtools) {
441 | devtools.emit('flush');
442 | }
443 | }
444 | // 然后执行了我们的cb, 此时视图已经更新
445 | // 5
446 | ```
447 |
448 | ## 总结
449 |
450 | 这一篇blog主要是为了让大家清楚
451 |
452 | * 1.当数据更新时会将watcher 推入一个队列
453 | * 2.当多个数据更新时,更新完不会立即更新视图
454 | * 3.视图更新发生在nextTick, 利用microTask 实现
455 | * 4.vdom 如何进行diff的将放在下一篇
456 |
457 | ## 拓展
458 |
459 | ##### 思考1:不用nextTick
460 |
461 | 如果很好地理解了micoTask 与 macroTask之间的关系,那么也能很清楚的理解假设我们写成下面这样, 为什么不行了,自己试试喽!下一篇,会细致讲解vdom diff 的过程~
462 |
463 | ```javascript
464 | mounted() {
465 | setTimeout(() => {
466 | data.a = 2;
467 | data.b = 3;
468 | console.log(document.querySelector('.f-error'));
469 | }, 500);
470 | }
471 | ```
472 |
473 | ##### 思考2: 如果都用MicroTask有什么问题?
474 |
475 | 看下这个issue, [@click would trigger event other vnode @click event. #6566](https://github.com/vuejs/vue/issues/6566)
476 |
477 | 贴一下代码,**vue的版本是2.4.2**,在此版本下当你点击了‘expand is true’以后,expand click 和 off click都打印出来了,countA与countB都变成了1,文案还是expand is true
478 |
479 | ```javascript
480 |
481 |
482 |
483 |
484 |
485 | JS Bin
486 |
487 |
488 |
489 |
490 |
491 |
494 |
495 |
496 | Expand is False
497 |
498 |
499 | countA: {{countA}}
500 |
501 |
502 | countB: {{countB}}
503 |
504 |
505 | expand: {{expand}}
506 |
507 | Please Click `Expand is Ture`.
508 |
509 |
510 |
511 |
534 |
535 | ```
536 |
537 | 
538 |
539 | 尤大在这个issue下面给了回答,引一下:
540 |
541 | 
542 |
543 | 大致原因是:``````标签的点击动作触发了第一次nextTick(microTask), 然后我们得到了新的vdom并进行了渲染;microTask先于冒泡这个task,在microTask生成新dom的过程中,外层div添加了listener; 渲染完成后,冒泡触发了新的listener,所以又进入了新的cb。所以在Vue2.5版本中你会看到event handler使用了macroTask进行包裹
544 |
545 | ```javascript
546 | /** Vue.js v2.5.13 **/
547 | function add$1 (
548 | event,
549 | handler,
550 | once$$1,
551 | capture,
552 | passive
553 | ) {
554 | // 看这里
555 | handler = withMacroTask(handler);
556 | if (once$$1) { handler = createOnceHandler(handler, event, capture); }
557 | target$1.addEventListener(
558 | event,
559 | handler,
560 | supportsPassive
561 | ? { capture: capture, passive: passive }
562 | : capture
563 | );
564 | }
565 |
566 | /**
567 | * Wrap a function so that if any code inside triggers state change,
568 | * the changes are queued using a Task instead of a MicroTask.
569 | */
570 | function withMacroTask (fn) {
571 | return fn._withTask || (fn._withTask = function () {
572 | useMacroTask = true;
573 | var res = fn.apply(null, arguments);
574 | useMacroTask = false;
575 | return res
576 | })
577 | }
578 | ```
579 |
580 |
581 |
582 | ## 参考资料
583 |
584 | 1.vue2.0 正确理解Vue.nextTick()的用途 http://www.cnblogs.com/minigrasshopper/p/7879545.html
585 |
586 | 2.从event loop规范探究javaScript异步及浏览器更新渲染时机 https://github.com/aooy/blog/issues/5
587 |
588 | 3.Promise的队列与setTimeout的队列有何关联?https://www.zhihu.com/question/36972010/answer/71338002
589 |
590 | 4.JavaScript 运行机制详解:再谈Event Loop http://www.ruanyifeng.com/blog/2014/10/event-loop.html
591 |
592 | 5.Vue源码详解之nextTick:MutationObserver只是浮云,microtask才是核心!https://github.com/Ma63d/vue-analysis/issues/6
593 | https://chuckliu.me/#!/posts/58bd08a2b5187d2fb51c04f9
594 |
595 | 6.Tasks, microtasks, queues and schedules https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/
596 |
597 | 7.@click would trigger event other vnode @click event. #6566 https://github.com/vuejs/vue/issues/6566
--------------------------------------------------------------------------------