├── README.md ├── diff.md ├── pathStateMachine.md ├── vue-event.md ├── vue-observe.md └── why-vuex-is-better-than-redux-for-vue.md /README.md: -------------------------------------------------------------------------------- 1 | 所有文章会不持续更新 2 | 3 | [Vue数据绑定和响应式原理](https://github.com/banama/aboutVue/blob/master/vue-observe.md) 4 | 5 | [Vue列表渲染性能优化原理](https://github.com/banama/aboutVue/blob/master/diff.md) 6 | 7 | [Vue的事件解读](https://github.com/banama/aboutVue/blob/master/vue-event.md) 8 | 9 | [Vue 模板表达式解析和 path 状态机](https://github.com/banama/aboutVue/blob/master/pathStateMachine.md) 10 | 11 | [为什么 Vuex 比 Redux 更适合 Vue.js](https://github.com/banama/aboutVue/blob/master/why-vuex-is-better-than-redux-for-vue.md) 12 | -------------------------------------------------------------------------------- /diff.md: -------------------------------------------------------------------------------- 1 | Vue列表渲染性能优化原理 2 | 3 | Vue 是一个高效的 mvvm 框架,这得益于作者已经帮我们框架内部做了足够的优化,比如各个细节的缓存( parseText 结果的缓存,compile 编译结果的缓存等)。 4 | 5 | 大列表是容易造成性能问题的地方,一不小心就会造成大量的重绘和重排。Vue 的列表渲染实现在 v-for 指令的 update 方法, 性能优化的大部分细节在 [diff](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/directives/public/for.js#L106) 函数。 6 | 7 | 列表渲染时会为迭代列表的每一份数据并为他们生成各自对应的片段对象 frag ,frag.node 为片段对象的 DOM 元素。 8 | 9 | ##frag缓存和track-by 10 | 11 | 在列表渲染过程中,当列表数据发生变化时,为了避免 frag 的重复创建和大规模的重新渲染, Vue 会尽可能复用缓存的 frag ,高效的缓存 frag 命中率也是 DOM 元素复用的关键。 12 | 13 | 以下例子 14 | 15 | ``` 16 | new Vue({ 17 | template: ` 18 | 21 | `, 22 | data: function(){ 23 | return { 24 | model: [1, 2, 3] 25 | } 26 | } 27 | 28 | }) 29 | ``` 30 | 当这个组件中的列表首次渲染时,Vue 会将[创建](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/directives/public/for.js#L264)的 frag 缓存到 [dir.cache](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/directives/public/for.js#L80) 。默认通过数据对象的特征来决定对已有作用域和 DOM 元素的复用程度。例如当数据对象为Array时,缓存 id 为数组的 value ,当数据对象为 Object 时,缓存 id 为对象的 $key 。对于这个例子来说三个缓存 id 为1、2、3。 31 | 32 | 这样在上面的例子中,如果 vm.model 变为 [3, 2, 1],新的列表的三个片段的缓存 id 分别为3、2、1,因此我们能做到复用全部已创建的frag。 33 | 34 | v-for 指令中片段 frag 的缓存 id 计算规则在 [getTrackByKey](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/directives/public/for.js#L648) ,从中可以看到,当 track-by 不存在时,缓存 id 将取数组的 value 或对象的 key 。但是这里有一个问题,如果数组出现重复值,会出现缓存 id 冲突的[警告](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/directives/public/for.js#L417)。副作用就是会忽略重复的片段,这是因为相同的缓存 id 获取的 frag 将会引用同一个 DOM 元素。当该 DOM 元素在复用算法中处理过一次后会将 frag 的 reused 属性[变为 false ](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/directives/public/for.js#L220),这就导致 v-for 指令会重新尝试将该[插入到DOM](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/directives/public/for.js#L215)中,然而因为文档中已经存在该 DOM 元素,就会导致插入失败。 35 | 36 | 这时 Vue 提示我们可以使用 `track-by='$index'` ,它将使用数据的索引作为缓存 id ,索引作为缓存id一定是唯一的,但同时新旧数据的相同索引的缓存 id 是相同的,所使用的 frag 也是同一份。这回导致新列表的的 frags 失去和数据顺序的映射关系。而 frag.node 的数据在[flushBatcherQueue](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/batcher.js#L37)更新。因为这种更新列表的方式不会移动DOM元素,而是在原位更新,简单地以对应索引的新值刷新,并不会受重复值的影响,这让数据替换非常高效,但同时也会有副作用,比如如果片段存有私有数据或状态,原位更新模式并不会同步状态。如下例 37 | 38 | [[track-by='index'副作用]](https://jsfiddle.net/banama/kmgwqtc9/) 39 | 40 | 在第一个例子中,没有开启 `track-by='$index'` , v-for 指令会根据数组的值作为缓存 id 缓存每项 frag,当反转列表的顺序是,Vue 会根据缓存 id 为列表的每一项取出可复用的frag,但是 frags 的顺序也是反转的,Vue 会通过DOM元素移动算法将每个片段移动到正确的位置,因此当 input 有输入时,因为整个 DOM 节点发生了移动,input 的输入内容并没有错乱,这意味着我们没有丢失 frag 内 DOM 元素的私有状态。 41 | 42 | 再看第二个例子,开启了 `track-by='$index'` 之后,v-for 指令会根据数据的索引作为缓存 id 缓存每项片段,当反转列表的顺序时,每项的frag 会复用以索引为缓存id所缓存的 frag,所以生成的 frags 和原列表的 frags 是一样的,根据DOM元素移动算法,列表的 DOM 节点并没有移动,每个片段的数据更新会在接下来的流程中更新,所以会发现每个片段的数据更新了,但是因为 DOM 元素节点没有移动,因此每个 DOM 节点中input的输入状态并没有根据元素的变化而更新。 43 | 44 | `track-by='$index'` 是一种简洁高效的优化手段,但是使用的时候你必须明白他做了什么。 45 | 46 | 在一些富交互的列表使用 `track-by=‘$index’` 需要格外谨慎,但是在非 `track-by=‘$index’` 模式我们仍可通过 track-by 尽量优化。有时候 v-for 指令并不能最大化优化,比如 47 | 48 | ``` 49 | vm.model = { 50 | a: { id: 1, val: "model1"}, 51 | b: { id: 2, val: "model2"}, 52 | c: { id: 3, val: "model2"}, 53 | } 54 | 55 | 列表更新 56 | 57 | vm.model = { 58 | d: { id: 1, val: "model1"}, 59 | e: { id: 2, val: "model2"}, 60 | f: { id: 3, val: "model2"} 61 | } 62 | ``` 63 | 64 | 默认情况 v-for 指令对于对象数据会将对象的键作为缓存 id,上面的例子发现列表更新后,对象的键没有重复,所以导致缓存一个都没有命中,而列表更新的结果也是重新渲染整个列表。但上面例子很明显可以看出,如果能够将对象的 a.id、b.id、c.id 作为缓存 id ,那么当列表更新时,所有缓存都能够命中,甚至连DOM元的移动都不需要, `track-by='id'` 就是做这样的事情. 65 | 66 | 67 | ##DOM元素移动和启发算法 68 | 69 | diff 算法中另一个重要的优化是 DOM 节点的移动。DOM 节点移动的性能开销非常大,因此减少 DOM 节点移动次数是算法的核心。当然开启 `track-by='$index'`不需要移动DOM元素,只需插入缺少的节点即可。 70 | 71 | 72 | 假如一个列表的 DOM 节点可以全部复用,那么列表的更新的核心就是 DOM 节点移动到合适的位置。简化之后就是下面的场景 73 | 74 | old [1, 2, 3, 4, 5, 6, 7] 75 | 76 | 更新为 77 | 78 | new [7, 6, 5, 4, 3, 2, 1] 79 | 80 | 先声明一下 81 | 82 | ``` 83 | 先声明 84 | index = 索引 85 | target = new[index] 86 | targetPrv = new[index - 1] 87 | current = map -> target (target在old里的映射) 88 | currentPrv = current.prv 89 | move(a, index) 在old中将a元素移动到索引为index的位置 90 | ``` 91 | 92 | 让我们来想想怎么移动 93 | 94 | ``` 95 | loop new.length 96 | move(current, index) 97 | ``` 98 | 这个算法没有问题,可以顺利完成任务,但是这个算法会移动 new.length 次,几乎是最糟糕的情况,有时候节点在正确的地方并不需要移动。 99 | 100 | 例如 101 | 102 | ``` 103 | old [1, 2, 3, 4, 5, 6, 7] 104 | new [7, 6, 5, 4, 3 ,2 ,1] 105 | 完全反转,需要移动7次 106 | 107 | old [1, 2, 3, 4, 5, 6, 7] 108 | new [1, 2, 3, 4, 5, 6, 7] 109 | 很明显移动次数应该为0,但是还是会移动7次 110 | ``` 111 | 112 | 如果在移动之前判断一下,他是不是在正确的位置。而这里对于是否处在正确的判断,是看他们的前一个元素是否相同。 113 | 114 | ``` 115 | loop new.length 116 | if targetPrv !== currentPrv 117 | move(current, index) 118 | ``` 119 | 120 | 这样做可以在大部分情况下避免不必要的移动,比如对于 121 | 122 | ``` 123 | old [1, 2, 3, 4, 5, 6, 7] 124 | new [7, 1, 2, 3, 4, 5, 6] 125 | ``` 126 | 127 | index => 0 128 | 129 | target = 7 130 | targetPrv = undefined 131 | current = 7 132 | currentPrv = 6 133 | 134 | undefined !== 6 135 | move(7, 0) 136 | 137 | old [7, 1, 2, 3, 4, 5, 6] 138 | 139 | 我们发现只需移动一次就得到想要结果。 140 | 141 | 再看一个类似的例子 142 | 143 | ``` 144 | old [1, 2, 3, 4, 5, 6, 7] 145 | new [2, 3, 4, 5, 6, 7, 1] 146 | ``` 147 | 148 | 这个例子和上面的例子差不多,很明显只需要移动一次就可以得到想要的结果,然而结果却出乎意料,看一看是怎样移动的 149 | 150 | 151 | index => 0 152 | 153 | target = 2 154 | targetPrv = 1 155 | current = 1 156 | currentPrv = undefined 157 | 158 | 1 !== undefined 159 | move(1, 0) 160 | 161 | old [2, 1, 3, 4, 5, 6, 7] 162 | 163 | index => 1 164 | 165 | target = 3 166 | targetPrv = 2 167 | current = 3 168 | currentPrv = 1 169 | 170 | 2 !== 1 171 | move(3] 1) 172 | 173 | old [2, 3, 1, 4, 5, 6, 7] 174 | 175 | ...... 176 | 177 | old [2, 3, 4, 5, 6, 1, 7] index => 6 178 | 179 | target = 7 180 | targetPrv = 6 181 | current = 7 182 | currentPrv = 1 183 | 184 | 6 !== 1 185 | move(7, 6) 186 | 187 | old [2, 3, 4, 5, 6, 7, 1] 188 | 189 | 直到最后我们的发现,直到移动了7次才得到想要的结果,第一个元素每一次移动都向后冒泡,成功的混淆了每一次判断结果。这个问题在[isseuse#1807](https://github.com/vuejs/vue/issues/1807)有详细说明。 190 | 191 | 如果我们能忽略第一次移动,那么之后的每一次判断都会成功 192 | 193 | old [1, 2, 3, 4, 5, 6, 7] 194 | 195 | new [2, 3, 4, 5, 6, 7, 1] 196 | 197 | index = 1 198 | 199 | target = 3 200 | targetPrv = 2 201 | current = 3 202 | currentPrv = 2 203 | 204 | 2 !== 2 205 | move(3, 1) 206 | 207 | old [2, 3, 4, 5, 6, 7, 1] 208 | 209 | ...... 210 | 211 | old [2, 3, 4, 5, 6, 7, 1] index = 6 212 | 213 | target = 1 214 | targetPrv = 7 215 | current = 1 216 | currentPrv = undefined 217 | 218 | 7 !== undefined 219 | move(1, 6) 220 | 221 | old [2, 3, 4, 5, 6, 7, 1] 222 | 223 | 这样的结果才是我们想要的。 224 | 225 | 现在 Vue 判断移动的条件是 226 | 227 | ``` 228 | if targetPrv !== currentPrv && 229 | (!currentPrv || currentPrv.prv !== targetPrv) 230 | move(current, index) 231 | ``` 232 | 233 | DOM 移动的[核心代码](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/directives/public/for.js#L194-L221)。Vue 声称实现了一些启发算法,以最大化复用 DOM 元素。这里的启发指的是执行 DOM 元素移动的条件,通过判断元素是否在正确的相对位置上,来于评估当前移动是否必要以及是否会造成愚蠢移动,没错,说的正是[1, 2, 3, 4, 5, 6,7] => [2, 3, 4, 5, 6, 7, 1]。 234 | 235 | ##列表渲染优化实践 236 | 237 | Vue 已经为我们做了大量无脑优化,主要在提高 DOM 元素的复用率和减少DOM 元素移动次数算法两方面,具体原理在上文两节中已经分析。DOM 元素的移动算法优化开发者不能做什么,但在提高DOM元素的复用率仍给开发者留有优化余地。 238 | 239 | 但是优化方式也十分简单,即给v-for列表加一个 `track-by` 属性,提示Vue 如何判断对象时同一份数据,提高缓存命中率。甚至可以直接加 `track-by="$index"` 原位复用DOM元素,这种优化效率最高,但是副作用上文中也说了,会丢失原DOM元素的临时状态组件私有状态,因此在交互复杂的列表中可能会有意想不到的问题,这时使用`非$index的track-by`也可以做到尽可能的性能优化。 240 | 241 | 242 | 243 | 244 | -------------------------------------------------------------------------------- /pathStateMachine.md: -------------------------------------------------------------------------------- 1 | Vue 模板表达式解析和 path 状态机 2 | 3 | Vue 模板中数据绑定语法是是采用 "Mustache" 语法(双大括号), Mustache 标签内的文本称为绑定表达式。在 Vue 中,一段绑定表达式由一个简单的 JavaScript 表达式和可选的一个或多个过滤器构成。在解析组件模板的时候会将这段表达式根据组件数据更新到 DOM 中。 4 | 5 | ## 表达式解析 6 | 7 | Vue 的表达式是通过自己的解析来获取数据的,因此并不是所有的 javascript 表达式都支持。下面是一个表达式的解析的过程 8 | 9 | ``` 10 | {{ message.split('').reverse().join('') }} 11 | ``` 12 | 13 | Vue 首先会将这段文本[解析](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/parsers/text.js#L62-L86)为 tokens, 14 | 15 | ``` 16 | var tokens = Vue.parsers.text.parseText("{{ message.split('').reverse().join('') }}") 17 | ``` 18 | 19 | 结果为 20 | 21 | ``` 22 | tokens = [ 23 | { 24 | html: false, 25 | hasOneTime: false, 26 | tag: true, 27 | value: "message.split('').reverse().join('')" 28 | } 29 | ] 30 | ``` 31 | 32 | 然后将 token [转化](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/parsers/text.js#L107-L113)成表达式 33 | 34 | ``` 35 | var expression = Vue.parsers.text.tokensToExp(tokens) 36 | ``` 37 | 38 | 结果为 39 | 40 | ``` 41 | expression = "message.split('').reverse().join('')" 42 | ``` 43 | 44 | 这个 expression 正是创建 watcher 时所用到的表达式,watcher 在为模板表达式与响应数据建立联系的时候会去解析该表达式并获取值。 45 | 46 | ``` 47 | var res = Vue.parsers.expression.parseExpression(expression) 48 | ``` 49 | 50 | Vue 解析表达式其实是为该表达式定义 getter 和 setter 方法,但并不是所有表达式都可以定义 setter 方法,可以设置 setter 方法的表达式必须是一个合法的数据路径,比如 `model.raw`。 Vue 解析这个表达式,如果他是一个合法的对象访问路径,setter 方法才可以被设置成功,否则 [setter 方法为 undefined](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/parsers/expression.js#L178-L181),但也并不是所有合法数据都可以这样做,必须要保证这个路径的属性值是存在的,否则当执行表达式的 setter 方法时将会[报错](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/parsers/path.js#L339-L341)。 51 | 52 | 53 | 表达式的 getter 方法结合组件数据获取表达式的值,通过 `Function` 构造器为表达式的 javscript 字符串创建一个函数,从而访问到 Vue 表达式的真实值。 54 | 55 | ``` 56 | var getter = function(expression){ 57 | return new Function('scope', return 'scope.' + expression + ";") 58 | } 59 | ``` 60 | 获取表达式的值时,执行 getter 方法从作用域对象内取值 61 | 62 | ``` 63 | var model = { 64 | message: "data from res getter", 65 | 66 | } 67 | getter.call(model, model) // retteg ser morf atad 68 | ``` 69 | 70 | Vue中的双向绑定原理,正是设置表达式的 setter 方法,在视图中改变组件数据,驱动数据更新。上文中说到并不是所有的表达式都可以设置 setter 方法,必须是一个合法的路径,Vue 通过状态机模式实现了对字符串路径的解析。 71 | 72 | ## path 状态机 73 | 74 | path 指的是一个对象的属性访问路径,比如 `b.c.d` 来访问对象 75 | 76 | ``` 77 | a = { 78 | b: { 79 | c: { 80 | d: "e" 81 | } 82 | } 83 | } 84 | ``` 85 | 86 | 的属性,很明显该路径对应的值是 `"e"`。Vue 将表达式的访问路径字符串解析成更易于 js 使用的状态。 `b.c.d` 将会被解析成 `['b', 'c', 'd']`,这样如果将该路径的属性值设为 f,执行 `a[pathAry[0]][pathAry[1]][pathAry[2]] = 'f'` 即可为改字符串访问路径下的属性赋值。 87 | 88 | Vue 实现了一个 [parse](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/parsers/path.js#L164) 方法解析 path。一个合法 path 是有规律的,比如 `a][` 这样一个 path 明显是不合法,因为当路径第一个字符为 a 时,对于对象访问路径来说,第二个字符可能存在的情况 89 | 90 | 1. 字符,则仍为属性的名字,拼接到前一个字符串之后作为新的属性名 91 | 2. `.` , 则 a 为第一级属性访问key,接着遍历第三个字符,访问下一级属性 92 | 3. `[` ,同上一种情况 93 | 4. `undefined` ,没有字符串,解析完毕。 94 | 95 | 因此当第二个字符串为 `]` 不符合当前状态所期望的输入,因此解析失败。 96 | 97 | Vue 的状态机模式解析 path 实际上是将 path 的每个索引的字符视为一个状态,将接下来一个字符视为当前状态的输入,并根据输入进行状态转移以及响应操作,如果输入不是期望的,那么状态机将异常中止。只有状态机正常运行直到转移到结束状态,才算解析成功。 98 | 99 | Vue 的 pathStateMachine 有八种状态,例如 [BEFORE_PATH](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/parsers/path.js#L33-L38) 100 | 101 | BEFORE_PATH 是 pathStateMachine 的初始状态,它的状态模型为 102 | 103 | ``` 104 | pathStateMachine[BEFORE_PATH] = { 105 | 'ws': [BEFORE_PATH], 106 | 'ident': [IN_IDENT, APPEND], 107 | '[': [IN_SUB_PATH], 108 | 'eof': [AFTER_PATH] 109 | } 110 | ``` 111 | 112 | 从状态模型中知道 BEFORE_PATH 接受四种输入 113 | 114 | - `ws`,状态转移到 BEFORE_PATH 115 | - `indent`,状态转移到 IN_IDENT,并执行 APPEND 操作 116 | - `[`,状态转移到 IN_SUB_PATH 117 | - `eof`,AFTER_PATH 118 | 119 | 输入 ws、indent、eof 具体代表什么,可以在 [getPathCharType](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/parsers/path.js#L90-L135) 看到定义。其他7种状态模型可在 [vue/src/parsers/path.js](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/parsers/path.js#L33-L81) 看到。 120 | 121 | 122 | 状态机运行过程中,Vue 在通过 action 处理每一级 path 的路径值。比如当处于状态 IN_IDENT 时,再次输入字符,会执行 APPEND 操作,将该字符串与之前的字符/字符串拼接。再次输入 `.` 或 `[` 会执行 PUSH 操作,将之间的字符串视为访问对象的一个属性。Vue 的 pathStateMachine 有四种 action,他们主要是根据 path 特征和状态提取出对象的访问属性,并按照层级关系依次推入数组。详细[见代码](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/parsers/path.js#L173-L207)。 123 | 124 | 125 | 下面是是一个详细例子分析状态机的状态转移过程,需要分析的 path 为 `md[0].da["ky"]` 126 | 127 | ``` 128 | 先声明 129 | keys = 存放对象访问属性的数组 130 | key = 临时变量 131 | index = 索引 132 | mode = 当前状态 133 | input = 输入 134 | transfer = 状态转移 135 | action = 操作 136 | ``` 137 | 现在进入状态极 138 | 139 | index = 0 140 | 141 | ``` 142 | mode = BEFORE_PATH 143 | input = 'm' 144 | transfer => IN_IDENT 145 | action => APPEND 146 | keys = [] 147 | key = 'm' 148 | ``` 149 | 150 | index = 1 151 | 152 | ``` 153 | mode = IN_IDENT 154 | input = 'd' 155 | transfer => IN_IDENT 156 | action => APPEND 157 | keys = [] 158 | key = 'md' 159 | ``` 160 | 161 | index = 2 162 | 163 | ``` 164 | mode = IN_IDENT 165 | input = '[' 166 | transfer => IN_SUB_PATH 167 | action => PUSH 168 | keys = ['md'] 169 | key = undefined 170 | ``` 171 | 172 | index = 3 173 | 174 | ``` 175 | mode = IN_SUB_PATH 176 | input = '0' 177 | transfer => IN_SUB_PATH 178 | action => APPEND 179 | keys = ['md'] 180 | key = '0' 181 | ``` 182 | 183 | index = 4 184 | 185 | ``` 186 | mode = IN_SUB_PATH 187 | input = ']' 188 | transfer => IN_PATH 189 | action => INC_SUB_PATH_DEPTH 190 | keys = ['md', '0'] 191 | key = undefined 192 | ``` 193 | 194 | 195 | index = 5 196 | 197 | ``` 198 | mode = IN_PATH 199 | input = '.' 200 | transfer => BEFORE_IDENT 201 | action => None 202 | keys = ['md', '0'] 203 | key = undefined 204 | ``` 205 | 206 | index = 6 207 | 208 | ``` 209 | mode = BEFORE_IDENT 210 | input = 'd' 211 | transfer => IN_IDENT 212 | action => APPEND 213 | keys = ['md', '0'] 214 | key = 'd' 215 | ``` 216 | 217 | index = 7 218 | 219 | ``` 220 | mode = IN_IDENT 221 | input = 'a' 222 | transfer => IN_IDENT 223 | action => APPEND 224 | keys = ['md', '0'] 225 | key = 'da' 226 | ``` 227 | 228 | index = 8 229 | 230 | ``` 231 | mode = IN_IDENT 232 | input = '[' 233 | transfer => IN_SUB_PATH 234 | action => PUSH 235 | keys = ['md', '0', 'da'] 236 | key = undefined 237 | ``` 238 | 239 | index = 9 240 | 241 | ``` 242 | mode = IN_SUB_PATH 243 | input = '"' 244 | transfer => IN_DOUBLE_QUOTE 245 | action => APPEND 246 | keys = ['md', '0', 'da'] 247 | key = '"' 248 | ``` 249 | 250 | index = 10 251 | 252 | ``` 253 | mode = IN_DOUBLE_QUOTE 254 | input = 'k' 255 | transfer => IN_DOUBLE_QUOTE 256 | action => APPEND 257 | keys = ['md', '0', 'da'] 258 | key = '"k' 259 | ``` 260 | 261 | 262 | index = 11 263 | 264 | ``` 265 | mode = IN_DOUBLE_QUOTE 266 | input = 'y' 267 | transfer => IN_DOUBLE_QUOTE 268 | action => APPEND 269 | keys = ['md', '0', 'da'] 270 | key = '"ky' 271 | ``` 272 | 273 | index = 12 274 | 275 | ``` 276 | mode = IN_DOUBLE_QUOTE 277 | input = '"' 278 | transfer => IN_SUB_PATH 279 | action => APPEND 280 | keys = ['md', '0', 'da'] 281 | key = '"ky"' 282 | ``` 283 | 284 | index = 13 285 | 286 | ``` 287 | mode = IN_SUB_PATH 288 | input = ']' 289 | transfer => IN_PATH 290 | action => PUSH_SUB_PATH 291 | keys = ['md', '0', 'da', 'ky'] 292 | key = undefined 293 | ``` 294 | 295 | index = 14 296 | 297 | ``` 298 | mode = IN_SUB_PATH 299 | input = 'eof' 300 | transfer => AFTER_PATH 301 | action => None 302 | keys = ['md', '0', 'da', 'ky'] 303 | key = undefined 304 | ``` 305 | 306 | 至此状态机结束。最后,更清晰的了解状态转移可以看 @勾三股四 的[图](http://img2.tbcdn.cn/L1/461/1/3acfc1236df2d6cd068dd8540e0b0baeb4b8916b)。 307 | 308 | -------------------------------------------------------------------------------- /vue-event.md: -------------------------------------------------------------------------------- 1 | Vue的事件解读 2 | 3 | 这里有一个容易混淆的地方,在Vue中事件有两方面的内容,一方面是自定义事件,一方面是为DOM绑定事件。 4 | 5 | #DOM事件 6 | 7 | 在Vue中为DOM元素绑定事件的具体方法在文章中的[方法与事件处理器](http://cn.vuejs.org/guide/events.html)章节,通过v-on指令或事件语法糖 `@` 为DOM元素绑定事件。Vue解析组件模板后,在绑定更新 v-on 指令时会为DOM元素[绑定事件](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/directives/public/on.js#L131)(当然如果元素为 iframe ,会等到 iframe 加载完成后再为其绑定事件)。 8 | 9 | Vue中为DOM元素绑定事件是采用DOM2级事件的处理方式,因为Vue服务的是IE9以上的现代浏览器,他们也都是支持DOM2级事件。因此下例中 10 | 11 | ``` 12 |
13 | ``` 14 | 15 | 16 | 实际上相当于 17 | 18 | ``` 19 | el.addEventListener('click', func) 20 | ``` 21 | 22 | 所以 `addEventListener` 支持绑定的事件,`v-on` 指令也都支持。同样的理论上也可以解绑事件,虽然也有相应的 api ,但是Vue文档中并没有显示地告诉我们怎么做。 23 | 24 | 在代码中可以看到,每个 `v-on` 指令都有一个 [reset](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/directives/public/on.js#L140) 方法, reset 方法是当指令所绑定方法发生改变时,重新绑定事件之前的解绑操作,我们可以利用这个 api 来解绑事件。因此如果需要解绑事件,我们可以遍历 vm._directives 找到相应该指令,进行解绑。 25 | 26 | 当然既然是采用DOM2级事件处理,也可以使用 `removeEventListener` 直接进行解绑,看这个 [demo](https://jsfiddle.net/banama/73k5fe13/)。 执行解绑操作后 btn1 的确解绑成功了,但 btn2 没有解绑成功,这要说到 v-on 指令的[修饰符](http://cn.vuejs.org/api/#v-on),见源码中对带有修饰符的 handler 的[处理](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/directives/public/on.js#L104-L123)。顾名思义,修饰符修饰过的 handler 做了更多的事情,Vue的处理是包装原 handler 新的 handler 用于向DOM元素绑定,而解绑时仍然解绑原方法当然会失败。 27 | 28 | 当然这只是分析Vue的事件绑定原理,大多数情况下我们并不需要去解绑事件。合理的利用事件委托可以解决大部分由事件绑定引起的性能问题。 29 | 30 | ##自定义事件 31 | 32 | Vue自定义事件是为组件间通信设计,自定义事件提供了 $on、$off、$once、$emit、$broadcast、$dispatch 几个 api,非常简洁。 33 | 34 | 首先提两个vm的私有变量,vm._events 和 vm._eventCount。每个vm实例所有的自定义事件都将存储在 vm._events,而 vm._eventsCount 存储的是执行事件广播后子组件触发自定义事件处理程序的数量,这是为了事件广播优化而来的,如果 vm._eventsCount[event] 数量为零,当事件广播时则可断定子组件没有该事件的监听器,就没必要向子组件层层捕获该事件监听器了。 35 | 36 | ###$on 37 | 38 | 注册一个自定义事件,注册事件很简单,首先将其挂载到该实例下 39 | 40 | ``` 41 | vm._events[event] = fn 42 | ``` 43 | 44 | 然后是向上传播,更新各个组件的 _eventsCount。这里需要注意,我们可以通过 $on 为生命周期注册钩子,[点击](https://jsfiddle.net/banama/w605txu4/)查看demo,但是生命周期不可冒泡和广播,所以需要更新 eventsCount 前需要过滤。 查看[modifyListenerCount](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/instance/api/events.js#L191-L201) 45 | 46 | ###$once 47 | 48 | 因为 $once 注册的事件是一次性的,执行完后卸载,所以其实 $once 调用 $on 来注册事件的函数是[包装](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/instance/api/events.js#L28-L32)过的。 49 | 50 | ###$off 51 | 52 | 理解了注册事件的流程(其实就是更改 _events 和 _eventsCount)那么卸载事件也就很清晰了。 53 | 54 | 但是$off支持三种卸载方式 55 | 56 | 1、[如果没有参数,则删除所有的事件监听器](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/instance/api/events.js#L48-L59) 57 | 58 | 遍历 _events,冒泡更新每个事件的 _eventsCount,清空 vm._events 59 | 60 | 2、[ 如果只提供了事件,则删除这个事件所有的监听器](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/instance/api/events.js#L65-L69) 61 | 62 | 冒泡更新每个事件的 _eventsCount,vm._events 中剔除该事件 63 | 64 | 3、[如果同时提供了事件与回调,则只删除这个回调](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/instance/api/events.js#L73-L80) 65 | 66 | 遍历 vm._events[event] 的事件处理方法,如果该事件处理方法和回调相同,则从 vm._events[event] 剔除该事件处理方法,并冒泡更新该事件的 _eventsCount 67 | 68 | ###$emit 69 | 70 | 触发事件,直接遍历 vm._events[event] 的每个事件处理程序并执行。 71 | 72 | $emit 返回 shouldPropagate,shouldPropagate 是一个布尔值,取决于父链上的是否存在该事件的监听器以及,事件处理程序返回的值。他决定 $dispatch 是否停止冒泡。 73 | 74 | ###dispatch 75 | 76 | 派发事件。首先在实例上触发该事件,默认情况下将会停止冒泡传播,但如果 $emit 返回的 shouldPropagate 为 true,则该事件会继续沿父链向上传播,即在父组件继续向上派发事件。 77 | 78 | ###broadcast 79 | 80 | 事件广播。深度优先遍历子组件,并执行各个子组件的监听器事件处理程序,在绑定和卸载自定义事件时会会每个组件维护一个 vm._eventsCount,而它的作用正是在深度遍历的时候给予提示,避免不必要的深度遍历。 81 | 82 | 83 | 通过自定义事件在组件之间的传播,我们可以利用它进行组件通信。组件通信在应用开发过程中是一个棘手的问题,因为它直接关系到整个应用的健壮和可维护程度,在开发大型项目中建议引入vuex,从应用架构的角度来考虑组件通信相比这种事件形式更容易维护,比如多个子组件都有派发事件与父组件进行通信,如果子组件派发事件不注意命名规范,出现命名重复情况,那么父组件监听器根本不知道这个事件是从哪里派发过来的以技如何处理,这是隐患之一。如果采用这种方式进行组件通信,那么必将导致子组件大量派发事件,那么父组件将要维护大量的事件监听器,如果时间久了,很容易忘记监听器和派发事件子组件的对应关系,这又增加了开发与维护成本。充斥着事件派发的组件维护成本也是一个容易留坑的地方。此外通过事件可以进行父子组件的通信,但兄弟组件的通信有需要增加不少开发成本。 84 | 85 | ###组件的自定义事件 86 | 87 | 在上文分析DOM元素绑定事件中,我们用到这个例子 88 | 89 | ``` 90 |
91 | ``` 92 | 93 | 但是有时候会出现 v-on 为组件绑定事件的情况,如 94 | 95 | ``` 96 | 97 | ``` 98 | 99 | 上文中没有分析到,留在这里说,这里有两个明显区别 100 | 101 | * 是组件而不是DOM元素 102 | * 自定义事件而不是DOM事件 103 | 104 | 因此显然 `addEventLisntener` 不适用,而且Vue执行的也是和第一个例子完全不同的处理方式, 对其的处理在 [registerComponentEvents](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/instance/internal/events.js#L20)。它其实是为组件注册自定义事件。这里 v-on 指令绑定的结果是 demoVm._events[myfunc] = [func] 以及更新 _eventsCount。 105 | 106 | 查看这个 [demo](https://jsfiddle.net/banama/43tswmdo/) 。 107 | 108 | 可见 v-on 指令既可为DOM元素绑定事件也可为组件绑定自定义事件。明白了这个,这个[issue](https://github.com/vuejs/vue/issues/3098) 的原因也就很明了了。 109 | -------------------------------------------------------------------------------- /vue-observe.md: -------------------------------------------------------------------------------- 1 | Vue数据绑定和响应式原理 2 | 3 | 4 | 当实例化一个Vue构造函数,会执行 Vue 的 init 方法,在 init 方法中主要执行三部分内容,一是初始化环境变量,而是处理 Vue 组件数据,三是解析挂载组件。以上三部分内容构成了 Vue 的整个执行过程。 5 | 6 | 7 | Vue 实现了一个 `观察者-消费者(订阅者)` 模式来实现数据驱动视图。通过设定对象属性的 setter/getter 方法来监听数据的变化,而每个属性的 setter 方法就是一个观察者, 当属性变化将会向订阅者发送消息,从而驱动视图更新。 8 | 9 | 10 | Vue 的订阅者 watcher 实现在 [/src/watchr.js](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/watcher.js#L36) 。构建一个 watcher 最重要的是 expOrFn 和 cb 两个参数,cb 是订阅者收到消息后需要执行的回调,一般来说这个回调都是视图指令的更新方法,从而达到视图的更新,但是这也不是必须的,订阅回调也可以是一个和任何无关的纯函数。一个订阅者最重要的是要知道自己订阅了什么,watcher 分析 expOrFn 的 getter 方法,从而间接获得订阅的对象属性。 11 | 12 | 13 | ##数据订阅 14 | 15 | 16 | Vue 的数据订阅主要在上述的第二个阶段。在生命周期 init 和 created 之间执行,这部分实现了对[ options.data 的处理](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/instance/internal/state.js#L48),[ $options.props 的处理](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/instance/internal/state.js#L45),[ $options.computed 的处理](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/instance/internal/state.js#L49),[ $options.methods 的处理](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/instance/internal/state.js#L45),[ $options.events 的处理](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/instance/internal/events.js#L18),[ $options.watch 的处理](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/instance/internal/events.js#L18)等。 17 | 18 | 19 | 这里主要讲对 options.data 的处理 [_initData](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/instance/internal/state.js#L79) 方法。在 _initData 这个方法中 $options.data 并不是原来的 $options.data , 而是在一个`mergedInstanceDataFn`,这是因为在合并父组件和子组件 options 的 mergeOptions 方法中,对 options 做了特殊处理,对于 $options.data 而言新的 $options.data 是一个封装过的合并父子 $options.data 的新函数 mergeInstanceDataFn。我们遵循的开发实践是倾向于将一个复用组件的 data 属性设为返回原生对象的函数而不是纯对象,因为如果 data 为纯对象,一个组件的多个实例的 data 属性将是同一个对象的引用,者可能会导致意想不到的 bug ,这个在[文档](http://cn.vuejs.org/guide/components.html#组件选项问题)里也有说明。 20 | 21 | 22 | 从 observe 函数正式进入了对数据对象的观察,Vue 中响应式数据都有一个 `__ob__` 属性属性作为标记,这个属性其实就是该对象的观察器。如果数据已经是响应式的,将会跳过对该对象的重新观察,直接返回观察器。 在接下来的处理流程中会根据对象和数组分别处理,因为对象可以定义属性的 setter 方法,对于数组遍历每项的对象递归递归执行 observe 。观察数据的最终处理是 [defineReactive](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/observer/index.js#L194) 方法。 ES5 定义属性的setter和getter方法本身很简单,需要理解的地方是 Vue 定义处理订阅者和观察的关系。 23 | 24 | 25 | defineReactive 方法中看到每一个数据的 setter 和 getter 函数都是闭包,因此对于每一个数据都存在一个私有变量 dep 用于存放订阅器。简单来说 Vue 在执行属性 getter 方法时收集依赖(收集订阅者),执行属性的 setter 方法时给订阅者发消息。那依赖收集具体是怎么做的?从[这段代码](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/observer/index.js#L212-L223)中看到并不是每次执行属性的 getter 方法都会触发依赖收集,而是只有当 Dep.target 存在时才会触发依赖收集。 Dep.target 是一个“全局变量”,从后文中可以知道 Dep.target 变量存的是一个订阅者对象。这样就可以理解了,观察器收集订阅对象必然要知道是否有依赖可收集,而不是盲目收集。 26 | 27 | 28 | Vue 解析组件模板的时候将解析出来的指令绑定到 vm 上,这里还涉及组件解析、指令绑定、表达式解析等部分内容,先忽略这部部分内容细节。Vue 中视图的更新其实就是指令的更新,为了做到数据驱动视图更新,需要注册一个订阅者订阅数据的变化,通过回调来进行指令更新。 29 | 30 | ``` 31 |
32 | 33 |
34 | 35 | new Vue({ 36 | el: "#app", 37 | data: { 38 | model: "the model" 39 | } 40 | }) 41 | ``` 42 | 43 | 上例。Vue 解析模板当解析出 v-text 指令时,会为该 DOM 元素注册指令,并将其绑定到 vm 上。绑定指令时会根据指令的信息为 v-text 指令注册一个订阅器 44 | 45 | ``` 46 | new Watcher(vm, 'model', dir.update) 47 | ``` 48 | 订阅器在创建的时候会根据指令的 expression 分析出该表达式的 getter 方法,并执行 getter 方法。getter 方法在真正处理取值之前 watcher 会将 Dep.target 设为他自己。这就告诉 Vue 现在在整个系统中,他才是主角。那么可以预见在接下来 getter 取值过程中,如果该表达式的数据涉及到获取 vm 的响应式数据将会触发该响应数据的依赖收集,而且订阅者一定是自己。这时该 watcher 会把自己加入该响应数据的依赖,并将响应数据的依赖对象存到自己的 deps ( deps 里存的其实是各个响应数据依赖对象的引用因此可以手动改动响应数据的订阅依赖,但是最好不要这么做,这在计算属性中大有用处)。 49 | 50 | 51 | 这样就建立起了观察者和消费者的关系,当 vm.model 发生改变时,model 的 setter 方法将会向其所有的订阅者发送消息,触发指令的更新,从而做到视图的更新。 52 | 53 | 54 | 订阅器的回调并不一定是更新指令,只有在解析模板过程中注册的订阅者回调才是更新指令。Vue 组件的 $options.watch 也可以创建订阅者,如下例 55 | 56 | ``` 57 | new Vue({ 58 | data: function(){ 59 | return {model: 'the model'} 60 | }, 61 | watch: { 62 | 'model': function(){ 63 | console.log('hello' + this.model) 64 | } 65 | } 66 | }) 67 | ``` 68 | 69 | [_initEvents](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/instance/internal/events.js#L23) 在对 $options.watch 进行处理的时候,也是构造了一个订阅者,只是和在解析指令过程中构造的订阅者不同,这里构建的订阅者不用通过解析指令来获得回调,而是直接一个纯函数。 70 | 71 | 72 | 73 | ## 计算属性原理 74 | 75 | 如果清楚了Vue的响应式原理,那么理解计算属性也会变的很容易。 76 | 77 | `$options.computed` 的处理在 [_initComputed](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/instance/internal/state.js#L208) ,在这个方法中 Vue 将计算属性的 key 挂载到 vm 下(并没有任何重复 key 的检测以及警告,又因为 _initComputed 是 _initState的最后一个方法 ,所以会直接覆盖,因此这里要自己避免 vm 下挂载的属性的重复,见最后章节),并定义了其 getter 和 setter 方法, setter 方法没有复杂的地方,详细可看[代码](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/instance/internal/state.js#L225), getter 是通过 [makeComputedGetter](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/instance/internal/state.js#L234) 生成的一个函数,当生成 getter 的时候,这里为该计算属性构建了一个 watcher ,这个 watcher 和之前见到的都不一样,因为它构建的时候[回调为 null ](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/instance/internal/state.js#L235,c12),这意味着即使其他响应式数据将该订阅者收集为依赖,那么当数据响应时,该订阅者也不会有任何作为。的确是这样,因为这个订阅器其实是一个"中间订阅器",它的存在并不是为了触发订阅者做什么,而是为了帮助监听该计算属性变化的订阅器订阅消息。 78 | 79 | 看下面的例子 80 | 81 | ``` 82 | new Vue({ 83 | template: 'computed', 84 | data: { 85 | raw: 1 86 | }, 87 | computed: { 88 | model: function(){ 89 | return this.raw + 1 90 | } 91 | }, 92 | watch: { 93 | 'model': function(){ 94 | console.log('the computed') 95 | } 96 | } 97 | }) 98 | ``` 99 | 100 | 在计算属性处理完成后,会发现在 vm 下挂载了一个 key 为 model 的属性,该属性的 getter 为 `makeComputedGetter` ,如果没有接下来的watch,那么该计算属性用法和 101 | 102 | ``` 103 | vm.model = function(){ 104 | return this.raw + 1 105 | } 106 | ``` 107 | 108 | 差不多,虽然在 vm.model 的闭包 getter 里已经构建了一个watcher,并且一旦调用 getter 方法,该 watcher 就会被收集到 vm.raw 的私有变量 dep 里。但是这样对于响应式是没有任何意义的,因为并没有谁会因为 vm.raw 的变化而做什么。直到有订阅者开始订阅 vm.model 变化的消息,计算属性才变的有意义。而计算属性的核心 `makeComputedGetter` 正是来处理这个事情。 109 | 110 | 当为 vm.model 创建一个订阅者的时候,watcher 会执行 vm.model 的 getter 方法,同样的在执行 getter 之前,会先将 Dep.target 设为该 watcher,makeComputedGetter 做的事情其实是分析计算属性的依赖,有两个步骤。 111 | 112 | 一是[evalute](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/instance/internal/state.js#L240)。该方法做的是 113 | 114 | * 缓存当前 Dep.target 115 | * 为执行 makeComputedGetter 闭包内的 watcher (正是之前所说没什么实际意义的订阅者,)的 get 方法。因为层层关系,最终会执行到 vm.raw 的 getter 方法,因为 Dep.target 不为空,所以这时会进行依赖收集,将该 Dep.target 收集为 vm.raw 为订阅者,同时将该响应数据的 dep 加入该 watcher 的 deps 中 116 | * 还原Dep.target 117 | 118 | 119 | 二是 [watcher.depend](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/instance/internal/state.js#L243) ,depend 方法将目前的订阅者的订阅对象"分享"给 Dep.target 120 | 121 | 122 | 经过以上两步,通过"中间订阅者"这个 watcher 就实现了真正意义的计算属性,计算属性也变为响应式属性。 123 | 124 | 125 | ## 追踪(订阅) 126 | 127 | 128 | 看这个 [demo](https://jsfiddle.net/xcd8b8yd/1) 129 | 130 | 131 | 132 | 上文中讲到 Vue 是先定义响应式数据 ,然后再解析指令的过程中收集依赖。但在上面例子中的 demo1 组件在声明周期 ready 的时候,依赖收集的过程已经执行完毕,这时候为 vm 挂载一个属性,这个属性一定不是响应式的。但是 Vue 提供了动态添加响应的 api ,上面例子中 demo2 组件就是这样一个例子,但是`$set`到底做了什么。 133 | 134 | * [解析表达式](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/instance/api/data.js#L48) 135 | * [解析表达式的path](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/parsers/expression.js#L173) 136 | * [设置path的值](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/parsers/expression.js#L176) 137 | * [为vm添加响应式属性](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/util/lang.js#L64) 138 | * [依赖收集](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/util/lang.js#L65) 139 | 140 | 141 | 经过以上几个步骤就可以动态添加响应式属性。 142 | 143 | 144 | 不同于对象,数组还存在一些 api 可以改变数组本身,而不是通过 setter 方法来改变,这种情况采用常规的观察器是不能观察到的。Vue 对于这种数据变动,[采用Monkey patching扩展Array原型链上的方法](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/observer/array.js#L19)手动给订阅者发消息。例如 pop 方法 145 | 146 | ``` 147 | var pop = Array.prototype.pop 148 | Array.prototype.pop = function(){ 149 | var i = arguments.length 150 | var args = new Array(i) 151 | while(i--){ 152 | args[i] = arguments[i] 153 | } 154 | var result = pop.apply(this, args) 155 | var ob = this.__ob__ // 所有被监测的数据都会添加__ob__属性,https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/observer/index.js#L43 156 | ob.dep.notify() 157 | return result 158 | } 159 | ``` 160 | 161 | 可知被检测的数组发生变化也会触发更新。但在 js 中数组的方法分为两种,一种是变异方法,即方法调用会使数组本身发生变化,例如 pop、push 等,这些方法会直接给订阅者发消息。另一种是非变异方法,即方法调用会返回一个新的数组,原数组本身并不会发生变化,这时如果想要给订阅者发消息只需要将该数组赋给原数组就可以。这里并不用担心 Vue 会重新渲染整个列表,因为 Vue 为 v-for 指令做了巧妙的优化,即通过缓存、track-by以及所谓的[启发算法](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/directives/public/for.js#L648)优化过的 DOM 元素移动算法来实现 DOM 元素的最大化复用,从最大程度上避免重新渲染所带来性能消耗。 162 | 163 | ##其他 164 | 165 | 在 _initData 这个私有方法中,Vue 将 $options.data 所返回的数据全部代理到了 options._data 上。$options.props 和 $options.data 被观察后都会将该属性挂载到 vm 根节点上 166 | 167 | $options.props 的处理流程 168 | 169 | * [_initProps](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/instance/internal/state.js#L45) 170 | * [compileAndLinkProps](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/instance/internal/state.js#L71) 171 | * [compileProps](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/compiler/compile.js#L184) 172 | * [initProp](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/compiler/compile-props.js#L261) 173 | * `defineReactive(vm, prop.path, value)` 最后经props挂载在 vm 上 174 | 175 | $options.data 的处理流程 176 | 177 | * [_initData](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/instance/internal/state.js#L48) 178 | * [vm._proxy](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/instance/internal/state.js#L159) 179 | * 通过 `Object.defineProperty` 将 data 挂载到 vm 上 180 | 181 | 182 | 因此有可能会出现 props 和 data key 重复了的情况,这时会有[警告](https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/instance/internal/state.js#L103-L107),因为 _initProps 在 _initData 之前执行,所以也并不会覆盖,然而还有其他的情况可能并不会这么友好,比如 $options.methods 和 $options.computed 有相同的 key。 183 | -------------------------------------------------------------------------------- /why-vuex-is-better-than-redux-for-vue.md: -------------------------------------------------------------------------------- 1 | 为什么 Vuex 比 Redux 更适合 Vue.js 2 | 3 | 这边文章首先分析 Vuex 的原理,从 Vuex 的实现分析 redux 配合 Vue 的问题。对 redux 分析的比较浅显,抛砖引玉,欢迎讨论。 4 | 5 | ## Vuex 的原理 6 | 7 | Vuex 的实现分成两部分,一部分是创建 store,另一部分是在组件中挂载 store,并解析 vuex 属性。 8 | 9 | 在开始一个 vuex 应用之前,首先要根据 state 和 mutations 为 Vue 生成一个状态管理器。这其中只有一件比较重要的事情,就是通过为 state 构建一个 Vue 实例将 state 变成响应式数据。 10 | 11 | 在一个 vuex 应用中,每个组件的形式是这样 12 | 13 | ``` 14 | new Vue({ 15 | template: '', 16 | store: store, 17 | vuex: { 18 | getters: {}, 19 | actions: {} 20 | } 21 | }) 22 | ``` 23 | 24 | Vuex 在通过插件形式安装的时候会将 vuexInit 函数加入 vm 生命周期的 init 或 beforeCreate 阶段,因此在组件的对应生命周期会执行 vuexInit 函数来挂载store。对于 store ,Vuex 的处理方式是将它直接挂载到 $options.$store 以供使用,组件的 store 属性其实也并不是必需的,当组件的 store 属性不存在时,Vuex 会去 `vm.$options.parent` 寻找 `$store`,但无论是组件本身有 sore 属性还是直接从父组件获取,vm.$store(不是 vm.store ) 都是必需的。否则 Vuex 会抛出[警告](https://github.com/vuejs/vuex/blob/5385d415edeabb1e991d35678d36a90328ea0553/src/override.js#L37-L40)。 25 | 26 | 接下来是对 vuex 属性,Vuex 会将 vuex 属性解构成 getters 和 actions。并将 getters 的每个属性都挂载 vm 下(有可能被组件的 $options.data() 的属性覆盖),同时定义每个值的 getter 方法,但并不会定义 setter 方法,这是因为根据 Vuex 的设计是不允许开发者直接在组件内更改 store.state,而对数据的改动要通过 vuex.actions 内的方法。在这里 getter 方法其实是 `makeComputedGetter`,如果你读过 [Vue数据绑定和响应式原理](https://github.com/banama/aboutVue/blob/master/vue-observe.md) 的计算属性章节,可能会很熟悉这个函数。没错,Vuex 实际上将 vm.vuex.getter 内的属性当作当前 vm 的计算属性来处理。和计算属性的区别是计算属性依赖计算的是 vm.$options.data 内的值,而 vm.vuex.getter 的属性依赖计算的是 store.\_vm.$options.data。这样所有组件的渲染都将都可以直接从状态树拿数据来渲染 UI 。 27 | 28 | 所有组件也都可以和状态树交互,但不允许直接更改状态树数据,而是要通过 vm.vuex.actions 内的方法。Vuex 会将这些方法绑定到组件的 `$options.methods` ,因此他们和组件的方法一样,可以在模板解析中通过 v-on 指定绑定到 DOM 元素上。而 actions 内的方法将通过 dispatch 触发 mutations 来更新全局状态。 29 | 30 | Vuex 是一套类 flux 的架构,以上的用法有时候可能会让人感觉十分冗余,比如为什么组件不允许直接修改 store 实例的状态,而是通过actions、mutations ? 其实也不是那么绝对,我们总有办法能做到,甚至一点不复杂, `vm.$store.state.key = value` 这样就随手更新了全局状态树,只有在开启严格模式后( Strict Mode ),Vuex 才会检测 state 是否是在 mutations 中修改。Vuex 屏蔽了 vm.$store.state.setter 方法,但是仍旧提供了一个 api [replaceState](https://github.com/vuejs/vuex/blob/5385d415edeabb1e991d35678d36a90328ea0553/src/index.js#L87)。其实就像 MVC 一样,架构给我们提供了最佳实践的建议,通过“约定”来提升大型项目的健壮和可维护性。比如通过 action 和 mutations 更新 store,从组件中解耦数据请求和逻辑,虽然“绕弯子”但可以让大型项目更容易理解和维护。我们仍然可以灵活的按照自己的想法进行开发,但是必须明白自己所做事情代价与回报。 31 | 32 | 再比如 Vuex 要求 mutations 必须事同步的。先看看下面的例子 33 | 34 | [muattions 也可以是异步的](https://jsfiddle.net/banama/q9fgmu5v/) 35 | 36 | 会发现如果触发一个异步的操作时,大部分情况异步的 action 和 异步的 mutations 都是没问题的,因为 state 本来就是相当于一个计算属性,只要在异步的回调里修改了 state,都会给订阅器发消息触发视图的更新。但是如果有一个场景,我们需要处理 mutation 和 state 的对象关系,比如 logger 中间件,如果 mutations 中存在异步操作,那么当 mutation 被触发时 logger 中间件已经开始打印,但状态树的状态还未更新,这就失去了日志打印状态的变化。同时 mutations 与 状态树正确的对应关系也正是实现`时间旅行`的关键。因此异步的 mutation 其实是有副作用的,只是有时候并不会对我们造成影响。 37 | 38 | 虽然我们可以灵活的使用 Vuex ,但个人建议最好遵循官网文档的推荐。 39 | 40 | 如果理解了 Vuex 的原理,那么来看看 Vue + redux 。redux 是一个泛用的 flux 实现,在 redux 的哲学中推崇 state 是不可修改的,因此在 redux 的 reducer 更新数据一般是这样的 41 | 42 | ``` 43 | state = Object.assign({}, state) 44 | state.count++ 45 | return state 46 | ``` 47 | 48 | redux 推荐使用 Object.assign() 新建了一个副本,但是 Vue 定义每一个响应式数据的 __ob__ 都是不可枚举的,因此使用 Object.assign 为 state 创建的副本将会丢失监听器,同时如果使用 redux 作为 Vue 的状态管理器,当 redux 的状态树的更新时,我们需要依赖 redux 实现的监听器来检测 store 的变化,将其重新挂载到 vm,这将导致 Vue 的响应系统重新为 store 创建监听器,每次数据更新 model 的性能开销活生生抵消了 Vue 的优化。 49 | 50 | 其实 redux 配合 Vue 使用还有另一个取巧的方式,那就是在 reducer 更新 state 的时候直接修改 state,因为 state 已经是响应式数据,因此这样直接更改 state 可以直接触发视图的更新而并不用依赖 redux 本身对 state 的监听。但反过来,这种严重违背 redux 设计哲学的用法可能会引发各种副作用,是为了用 redux 而用 redux。 51 | 52 | 当然这里只是对 redux 浅显的分析,相信进行深入优化 redux 能够配合 Vue 不损失性能并优雅的使用,但为什么不使用 Vuex 呢?它比 redux 更简单,而且为 Vue 而生。 53 | --------------------------------------------------------------------------------