├── .gitignore ├── src ├── img │ ├── 1.1.png │ ├── 11.1.png │ ├── 11.2.png │ ├── 12.1.png │ ├── 13.1.gif │ ├── 13.2.png │ ├── 13.3.png │ ├── 13.4.gif │ ├── 2.1.png │ ├── 2.2.png │ ├── 3.1.png │ ├── 3.2.png │ ├── 4.1.png │ ├── 5.1.png │ ├── 5.2.png │ ├── 5.3.png │ ├── 5.4.png │ ├── 6.1.png │ ├── 6.2.png │ ├── 7.1.png │ ├── 7.2.png │ ├── 8.1.png │ ├── 8.2.png │ ├── 8.3.png │ ├── 8.4.png │ ├── 8.5.png │ ├── 8.6.png │ ├── 8.7.png │ ├── 9.1.png │ ├── 9.2.png │ ├── 9.3.png │ └── vue.jpg ├── 动态组件的深入分析.md ├── 基础的数据代理检测.md ├── 来,跟我一起实现diff算法.md ├── 彻底搞懂Vue中keep-alive的魔法-上.md ├── 组件高级用法.md ├── 深入响应式系统构建-上.md ├── 组件基础剖析.md ├── 实例挂载流程和模板编译.md ├── 完整渲染流程.md ├── vue插槽,你想了解的都在这里.md ├── 深入响应式系统构建-下.md ├── 彻底搞懂Vue中keep-alive的魔法-下.md ├── 你真的了解v-model的语法糖了吗.md └── 深入响应式系统构建-中.md ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /src/img/1.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ocean1509/In-depth-analysis-of-Vue/HEAD/src/img/1.1.png -------------------------------------------------------------------------------- /src/img/11.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ocean1509/In-depth-analysis-of-Vue/HEAD/src/img/11.1.png -------------------------------------------------------------------------------- /src/img/11.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ocean1509/In-depth-analysis-of-Vue/HEAD/src/img/11.2.png -------------------------------------------------------------------------------- /src/img/12.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ocean1509/In-depth-analysis-of-Vue/HEAD/src/img/12.1.png -------------------------------------------------------------------------------- /src/img/13.1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ocean1509/In-depth-analysis-of-Vue/HEAD/src/img/13.1.gif -------------------------------------------------------------------------------- /src/img/13.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ocean1509/In-depth-analysis-of-Vue/HEAD/src/img/13.2.png -------------------------------------------------------------------------------- /src/img/13.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ocean1509/In-depth-analysis-of-Vue/HEAD/src/img/13.3.png -------------------------------------------------------------------------------- /src/img/13.4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ocean1509/In-depth-analysis-of-Vue/HEAD/src/img/13.4.gif -------------------------------------------------------------------------------- /src/img/2.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ocean1509/In-depth-analysis-of-Vue/HEAD/src/img/2.1.png -------------------------------------------------------------------------------- /src/img/2.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ocean1509/In-depth-analysis-of-Vue/HEAD/src/img/2.2.png -------------------------------------------------------------------------------- /src/img/3.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ocean1509/In-depth-analysis-of-Vue/HEAD/src/img/3.1.png -------------------------------------------------------------------------------- /src/img/3.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ocean1509/In-depth-analysis-of-Vue/HEAD/src/img/3.2.png -------------------------------------------------------------------------------- /src/img/4.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ocean1509/In-depth-analysis-of-Vue/HEAD/src/img/4.1.png -------------------------------------------------------------------------------- /src/img/5.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ocean1509/In-depth-analysis-of-Vue/HEAD/src/img/5.1.png -------------------------------------------------------------------------------- /src/img/5.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ocean1509/In-depth-analysis-of-Vue/HEAD/src/img/5.2.png -------------------------------------------------------------------------------- /src/img/5.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ocean1509/In-depth-analysis-of-Vue/HEAD/src/img/5.3.png -------------------------------------------------------------------------------- /src/img/5.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ocean1509/In-depth-analysis-of-Vue/HEAD/src/img/5.4.png -------------------------------------------------------------------------------- /src/img/6.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ocean1509/In-depth-analysis-of-Vue/HEAD/src/img/6.1.png -------------------------------------------------------------------------------- /src/img/6.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ocean1509/In-depth-analysis-of-Vue/HEAD/src/img/6.2.png -------------------------------------------------------------------------------- /src/img/7.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ocean1509/In-depth-analysis-of-Vue/HEAD/src/img/7.1.png -------------------------------------------------------------------------------- /src/img/7.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ocean1509/In-depth-analysis-of-Vue/HEAD/src/img/7.2.png -------------------------------------------------------------------------------- /src/img/8.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ocean1509/In-depth-analysis-of-Vue/HEAD/src/img/8.1.png -------------------------------------------------------------------------------- /src/img/8.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ocean1509/In-depth-analysis-of-Vue/HEAD/src/img/8.2.png -------------------------------------------------------------------------------- /src/img/8.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ocean1509/In-depth-analysis-of-Vue/HEAD/src/img/8.3.png -------------------------------------------------------------------------------- /src/img/8.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ocean1509/In-depth-analysis-of-Vue/HEAD/src/img/8.4.png -------------------------------------------------------------------------------- /src/img/8.5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ocean1509/In-depth-analysis-of-Vue/HEAD/src/img/8.5.png -------------------------------------------------------------------------------- /src/img/8.6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ocean1509/In-depth-analysis-of-Vue/HEAD/src/img/8.6.png -------------------------------------------------------------------------------- /src/img/8.7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ocean1509/In-depth-analysis-of-Vue/HEAD/src/img/8.7.png -------------------------------------------------------------------------------- /src/img/9.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ocean1509/In-depth-analysis-of-Vue/HEAD/src/img/9.1.png -------------------------------------------------------------------------------- /src/img/9.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ocean1509/In-depth-analysis-of-Vue/HEAD/src/img/9.2.png -------------------------------------------------------------------------------- /src/img/9.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ocean1509/In-depth-analysis-of-Vue/HEAD/src/img/9.3.png -------------------------------------------------------------------------------- /src/img/vue.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ocean1509/In-depth-analysis-of-Vue/HEAD/src/img/vue.jpg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "analysisofvue", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "dependencies": {}, 7 | "devDependencies": { 8 | "gitbook-plugin-copy-code-button": "0.0.2", 9 | "gitbook-theme-comscore": "0.0.3" 10 | }, 11 | "scripts": { 12 | "test": "echo \"Error: no test specified\" && exit 1" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/Ocean1509/In-depth-analysis-of-Vue.git" 17 | }, 18 | "author": "", 19 | "license": "ISC", 20 | "bugs": { 21 | "url": "https://github.com/Ocean1509/In-depth-analysis-of-Vue/issues" 22 | }, 23 | "homepage": "https://github.com/Ocean1509/In-depth-analysis-of-Vue#readme" 24 | } 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 深入剖析Vue源码 2 | 3 | ![](https://user-gold-cdn.xitu.io/2019/10/14/16dc938b94904285?imageView2/1/w/1080/h/320/q/85/format/webp/interlace/1) 4 | 5 | ## 网站地址 6 | 7 | [深入剖析Vue源码](https://book.penblog.cn) 8 | 9 | 10 | ## 章节结构 11 | 12 | #### 丰富的选项合并策略 13 | ```new Vue```是运行```Vue```框架的第一步,```Vue```作为构造器,实例化阶段的第一步是执行初始化过程,而选项合并是初始化的开始。我们会向构造器中传递各种类型的可配置选项,例如```data,props```,或者像```mounted```这类生命周期钩子。而除了这些用户自定义的选项,```Vue```还提供了很多内部的选项,这些选项遵循什么样的合并规则就是这一节分析的重点。 14 | 15 | 16 | #### 基础的数据代理 17 | 使用```Vue```做开发的同学都知道,```Vue```的核心是它的响应式系统,而响应式系统的核心是利用了```Object.defineProperty```进行数据拦截,这一节内容会深入分析```Vue```中两种数据拦截的方式:```Object.defineProperty,Proxy```,尽管响应式系统用的是兼容性更好的```Object.defineProperty```,但是```proxy```也在源码中使用上了,其中的一个例子就是用作数据过滤筛选。 18 | 19 | 20 | #### 完整挂载流程和模板编译 21 | ```Vue```版本提供了运行时版本和同时包含编译器和运行时的版本,他们都有各自的使用场景。除了介绍两者的区别外,文章的核心还介绍了实例在挂载阶段的完整流程,虽然不会对流程中的每个具体环节展开分析,但是可以知道大致完整的挂载思路。文章最后还介绍了编译器巧妙的设计思路。 22 | 23 | 24 | 25 | #### 完整渲染流程 26 | ```Virtual DOM```是```js```操作和```DOM```渲染之间的桥梁,```JS```对```DOM```节点的操作,都会批量反应到```Virtual DOM```这个节点描述对象上,它的理念很大程度提高了渲染的性能。有了上一节的基础,这一节会分析两个挂载阶段的核心过程,```render,update```,```render```阶段会将模板编译渲染函数,解析成```Virtual DOM```树,```update```阶段会将```Virtual DOM```树映射为真实的```DOM```节点。 27 | 28 | 29 | 30 | #### 组件基础剖析 31 | 组件是```Vue```另一个核心,组件化开发是衡量```Vue```开发能力的标准。文章会从组件的注册开始,介绍全局注册和局部注册在实现原理上的区别,另外组件的挂载流程也是分析的重点,这一切也都依赖于前面介绍过的渲染流程。 32 | 33 | 34 | 35 | #### 组件高级用法 36 | 除了基础的组件用法,```Vue```还提供了高级的用法,例如异步组件和函数组件。异步组件是首屏性能优化的解决方案,深入它的实现原理更有助于我们在开发中首屏性能问题。而函数式组件也有其独特的使用场景。 37 | 38 | 39 | 40 | #### 深入响应式系统构建- 上,中,下 41 | 响应式系统构建是```Vue```的核心,也是难点,这个系列会有三篇的内容去尝试分析内部的实现细节。从响应式数据的构建,再到每种数据类型依赖收集和派发更新的分析。文章也模拟了一个简易版的响应式系统方便深层次源码的分析。在响应式系统构建中,还有很多的特殊情况需要考虑,例如数组的响应式构建,对象的异常处理等。 42 | 43 | 44 | 45 | #### diff算法的实现 46 | ```virtual dom```引入的另一个关键是在旧节点发生改变时,利用```diff```算法比较新旧节点的差异,以达到最小变化的改变真实节点。文章会从脱离框架的角度实现一个```diff```算法。 47 | 48 | 49 | 50 | #### 揭秘Vue的事件机制 51 | ```Vue```提供了很多实用的功能给用户,其中一个就是使用模板去进行事件监听。```@click```作为事件指令会在模板编译阶段解析,并且会在真实节点的渲染阶段进行相关事件的绑定。而对于组件的事件而言,他提供了子父组件通信的方式,本质上是在同个子组件内部维护了一个事件总线。更多的内容可以参考文章的分析。 52 | 53 | 54 | 55 | #### 你想了解的```Vue```插槽 56 | ```Vue```组件的另一个重要概念是插槽,它允许你以一种不同于严格的父子关系的方式组合组件。插槽为你提供了一个将内容放置到新位置或使组件更通用的出口。这一节将围绕官网对插槽内容的介绍思路,按照普通插槽,具名插槽,再到作用域插槽的思路,逐步深入内部的实现原理。 57 | 58 | 59 | #### v-model的语法糖 60 | 我们都知道```v-model```是实现双向数据绑定的核心,但如果深入源码我们可以知道,```v-model```的核心只是通过事件触发去改变表单的值。除此之前```v-model```语法糖还在组合输入过程做了一系列的优化。另外组件上使用```v-model```本质上只是一个子父组件通信的语法糖。 61 | 62 | 63 | 64 | #### 动态组件的深入分析 65 | 这一节,我们又回到了组件的分析。动态组件是我们平时开发中高频率使用的东西。核心是```is```属性的使用。文末还粗略介绍了另一个概念,动态组件。 66 | 67 | 68 | 69 | #### keep-alive的魔法 70 | 内置组件中最重要,也是最经常使用的是```keep-alive```组件,我们将```keep-alive```配合动态组件```is```使用,达到在切换组件的同时,将旧组件进行缓存,以便保留初始状态的目的。```keep-alive```有不同于其他组件的生命周期,并且他在缓存上也做了优化。 71 | 72 | 73 | ##### 码字不易,感谢支持 74 | ![](https://static.sitestack.cn/projects/5865c0921b69e6006b3145a1/15cdfd3ae70566f9.png) 75 | ![](https://static.sitestack.cn/projects/5865c0921b69e6006b3145a1/15cdfd3ed2af6660.png) 76 | 77 | 78 | -------------------------------------------------------------------------------- /src/动态组件的深入分析.md: -------------------------------------------------------------------------------- 1 | > 前面花了两节的内容介绍了组件,从组件的原理讲到组件的应用,包括异步组件和函数式组件的实现和使用场景。众所周知,组件是贯穿整个Vue设计理念的东西,并且也是指导我们开发的核心思想,所以接下来的几篇文章,将重新回到组件的内容去做源码分析,首先会从常用的动态组件开始,包括内联模板的原理,最后会简单的提到内置组件的概念,为之后的文章埋下伏笔。 2 | 3 | ## 12.1 动态组件 4 | 动态组件我相信大部分在开发的过程中都会用到,当我们需要在不同的组件之间进行状态切换时,动态组件可以很好的满足我们的需求,其中的核心是```component```标签和```is```属性的使用。 5 | 6 | ### 12.1.1 基本用法 7 | 例子是一个动态组件的基本使用场景,当点击按钮时,视图根据```this.chooseTabs```值在组件```child1,child2,child3```间切换。 8 | ```html 9 | // vue 10 |
11 | 12 | 13 | 14 | 15 | 16 |
17 | ``` 18 | ```js 19 | // js 20 | var child1 = { 21 | template: '
content1
', 22 | } 23 | var child2 = { 24 | template: '
content2
' 25 | } 26 | var child3 = { 27 | template: '
content3
' 28 | } 29 | var vm = new Vue({ 30 | el: '#app', 31 | components: { 32 | child1, 33 | child2, 34 | child3 35 | }, 36 | methods: { 37 | changeTabs(tab) { 38 | this.chooseTabs = tab; 39 | } 40 | } 41 | }) 42 | ``` 43 | ### 12.1.2 AST解析 44 | ``的解读和前面几篇内容一致,会从```AST```解析阶段说起,过程也不会专注每一个细节,而是把和以往处理方式不同的地方特别说明。针对动态组件解析的差异,集中在```processComponent```上,由于**标签上```is```属性的存在,它会在最终的```ast```树上打上```component```属性的标志。** 45 | ```js 46 | // 针对动态组件的解析 47 | function processComponent (el) { 48 | var binding; 49 | // 拿到is属性所对应的值 50 | if ((binding = getBindingAttr(el, 'is'))) { 51 | // ast树上多了component的属性 52 | el.component = binding; 53 | } 54 | if (getAndRemoveAttr(el, 'inline-template') != null) { 55 | el.inlineTemplate = true; 56 | } 57 | } 58 | ``` 59 | 60 | 最终的```ast```树如下: 61 | 62 | ![](./img/12.1.png) 63 | 64 | 65 | 66 | ### 12.1.3 render函数 67 | 有了```ast```树,接下来是根据```ast```树生成可执行的```render```函数,由于有```component```属性,```render```函数的产生过程会走```genComponent```分支。 68 | ```js 69 | // render函数生成函数 70 | var code = generate(ast, options); 71 | 72 | // generate函数的实现 73 | function generate (ast,options) { 74 | var state = new CodegenState(options); 75 | var code = ast ? genElement(ast, state) : '_c("div")'; 76 | return { 77 | render: ("with(this){return " + code + "}"), 78 | staticRenderFns: state.staticRenderFns 79 | } 80 | } 81 | 82 | function genElement(el, state) { 83 | ··· 84 | var code; 85 | // 动态组件分支 86 | if (el.component) { 87 | code = genComponent(el.component, el, state); 88 | } 89 | } 90 | 91 | ``` 92 | 针对动态组件的处理逻辑其实很简单,当没有内联模板标志时(后面会讲),拿到后续的子节点进行拼接,和普通组件唯一的区别在于,```_c```的第一个参数不再是一个指定的字符串,而是一个代表组件的变量。 93 | ```js 94 | // 针对动态组件的处理 95 | function genComponent ( 96 | componentName, 97 | el, 98 | state 99 | ) { 100 | // 拥有inlineTemplate属性时,children为null 101 | var children = el.inlineTemplate ? null : genChildren(el, state, true); 102 | return ("_c(" + componentName + "," + (genData$2(el, state)) + (children ? ("," + children) : '') + ")") 103 | } 104 | ``` 105 | 106 | 107 | ### 12.1.4 普通组件和动态组件的对比 108 | 109 | 其实我们可以对比普通组件和动态组件在```render```函数上的区别,结果一目了然。 110 | 111 | #### 普通组件的render函数 112 | 113 | ```js 114 | "with(this){return _c('div',{attrs:{"id":"app"}},[_c('child1',[_v(_s(test))])],1)}" 115 | ``` 116 | 117 | #### 动态组件的render函数 118 | 119 | ```js 120 | "with(this){return _c('div',{attrs:{"id":"app"}},[_c(chooseTabs,{tag:"component"})],1)}" 121 | ``` 122 | 123 | 124 | 125 | 简单的总结,动态组件和普通组件的区别在于: 126 | 127 | 1. `ast`阶段新增了```component```属性,这是动态组件的标志 128 | 2. 产生```render```函数阶段由于```component```属性的存在,会执行```genComponent```分支,```genComponent```会针对动态组件的执行函数进行特殊的处理,和普通组件不同的是,```_c```的第一个参数不再是不变的字符串,而是指定的组件名变量。 129 | 3. `render`到```vnode```阶段和普通组件的流程相同,只是字符串换成了变量,并有```{ tag: 'component' }```的```data```属性。例子中```chooseTabs```此时取的是```child1```。 130 | 131 | **有了```render```函数,接下来从vnode到真实节点的过程和普通组件在流程和思路上基本一致,这一阶段可以回顾之前介绍组件流程的分析** 132 | 133 | 134 | 135 | ### 12.1.5 疑惑 136 | 由于自己对源码的理解还不够透彻,读了动态组件的创建流程之后,心中产生了一个疑问,从原理的过程分析,动态组件的核心其实是```is```这个关键字,它在编译阶段就以```component```属性将该组件定义为动态组件,而```component```作为标签好像并没有特别大的用途,只要有```is```关键字的存在,组件标签名设置为任意自定义标签都可以达到动态组件的效果?(```componenta, componentb```)。这个字符串仅以```{ tag: 'component' }```的形式存在于```vnode```的```data```属性存在。那是不是说明,所谓动态组件只是由于```is```的单方面限制?那```component```标签的意义又在哪里? 137 | 138 | 139 | 140 | ## 12.2 内联模板 141 | 由于动态组件除了有```is```作为传值外,还可以有```inline-template```作为配置,借此前提,刚好可以理清楚```Vue```中内联模板的原理和设计思想。```Vue```在官网有一句醒目的话,提示我们```inline-template``` 会让模板的作用域变得更加难以理解。因此建议尽量使用```template```选项来定义模板,而不是用内联模板的形式。接下来,我们通过源码去定位一下所谓作用域难以理解的原因。 142 | 143 | 我们先简单调整上面的例子,从使用角度上入手: 144 | ```html 145 | // html 146 |
147 | 148 | 149 | 150 | 151 | {{test}} 152 | 153 |
154 | ``` 155 | ```js 156 | // js 157 | var child1 = { 158 | data() { 159 | return { 160 | test: 'content1' 161 | } 162 | } 163 | } 164 | var child2 = { 165 | data() { 166 | return { 167 | test: 'content2' 168 | } 169 | } 170 | } 171 | var child3 = { 172 | data() { 173 | return { 174 | test: 'content3' 175 | } 176 | } 177 | } 178 | var vm = new Vue({ 179 | el: '#app', 180 | components: { 181 | child1, 182 | child2, 183 | child3 184 | }, 185 | data() { 186 | return { 187 | chooseTabs: 'child1', 188 | } 189 | }, 190 | methods: { 191 | changeTabs(tab) { 192 | this.chooseTabs = tab; 193 | } 194 | } 195 | }) 196 | ``` 197 | 例子中达到的效果和文章第一个例子一致,很明显和以往认知最大的差异在于,父组件里的环境可以访问到子组件内部的环境变量。初看觉得挺不可思议的。我们回忆一下之前父组件能访问到子组件的情形,从大的方向上有两个: 198 | 199 | **1. 采用事件机制,子组件通过```$emit```事件,将子组件的状态告知父组件,达到父访问子的目的。** 200 | 201 | **2. 利用作用域插槽的方式,将子的变量通过```props```的形式传递给父,而父通过```v-slot```的语法糖去接收,而我们之前分析的结果是,这种方式本质上还是通过事件派发的形式去通知父组件。** 202 | 203 | 之前分析过程也有提过父组件无法访问到子环境的变量,其核心的原因在于: 204 | **父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。** 205 | 那么我们有理由猜想,内联模板是不是违背了这一原则,让父的内容放到了子组件创建过程去编译呢?我们接着往下看: 206 | 207 | 208 | 回到```ast```解析阶段,前面分析到,针对动态组件的解析,关键在于```processComponent```函数对```is```属性的处理,其中还有一个关键是对```inline-template```的处理,它会在```ast```树上增加```inlineTemplate```属性。 209 | ```js 210 | // 针对动态组件的解析 211 | function processComponent (el) { 212 | var binding; 213 | // 拿到is属性所对应的值 214 | if ((binding = getBindingAttr(el, 'is'))) { 215 | // ast树上多了component的属性 216 | el.component = binding; 217 | } 218 | // 添加inlineTemplate属性 219 | if (getAndRemoveAttr(el, 'inline-template') != null) { 220 | el.inlineTemplate = true; 221 | } 222 | } 223 | ``` 224 | 225 | 226 | `render`函数生成阶段由于```inlineTemplate```的存在,**父的```render```函数的子节点为```null```,这一步也决定了```inline-template```下的模板并不是在父组件阶段编译的**,那模板是如何传递到子组件的编译过程呢?**答案是模板以属性的形式存在,待到子实例时拿到属性值** 227 | 228 | ```js 229 | function genComponent (componentName,el,state) { 230 | // 拥有inlineTemplate属性时,children为null 231 | var children = el.inlineTemplate ? null : genChildren(el, state, true); 232 | return ("_c(" + componentName + "," + (genData$2(el, state)) + (children ? ("," + children) : '') + ")") 233 | } 234 | ``` 235 | 236 | 我们看看最终```render```函数的结果,其中模板以```{render: function(){···}}```的形式存在于父组件的```inlineTemplate```属性中。 237 | 238 | 239 | ```js 240 | "_c('div',{attrs:{"id":"app"}},[_c(chooseTabs,{tag:"component",inlineTemplate:{render:function(){with(this){return _c('span',[_v(_s(test))])}},staticRenderFns:[]}})],1)" 241 | ``` 242 | 243 | 最终```vnode```结果也显示,```inlineTemplate```对象会保留在父组件的```data```属性中。 244 | 245 | ```js 246 | // vnode结果 247 | { 248 | data: { 249 | inlineTemplate: { 250 | render: function() {} 251 | }, 252 | tag: 'component' 253 | }, 254 | tag: "vue-component-1-child1" 255 | } 256 | ``` 257 | 258 | 有了```vnode```后,来到了关键的最后一步,根据```vnode```生成真实节点的过程。从根节点开始,遇到```vue-component-1-child1```,会经历实例化创建子组件的过程,实例化子组件前会先对```inlineTemplate```属性进行处理。 259 | 260 | ```js 261 | function createComponentInstanceForVnode (vnode,parent) { 262 | // 子组件的默认选项 263 | var options = { 264 | _isComponent: true, 265 | _parentVnode: vnode, 266 | parent: parent 267 | }; 268 | var inlineTemplate = vnode.data.inlineTemplate; 269 | // 内联模板的处理,分别拿到render函数和staticRenderFns 270 | if (isDef(inlineTemplate)) { 271 | options.render = inlineTemplate.render; 272 | options.staticRenderFns = inlineTemplate.staticRenderFns; 273 | } 274 | // 执行vue子组件实例化 275 | return new vnode.componentOptions.Ctor(options) 276 | } 277 | ``` 278 | 子组件的默认选项配置会根据```vnode```上的```inlineTemplate```属性拿到模板的```render```函数。分析到这一步结论已经很清楚了。**内联模板的内容最终会在子组件中解析,所以模板中可以拿到子组件的作用域这个现象也不足为奇了。** 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | ## 12.3 内置组件 287 | 最后说说```Vue```思想中的另一个概念,内置组件,其实```vue```的官方文档有对内置组件进行了列举,分别是```component, transition, transition-group, keep-alive, slot```,其中``````我们在插槽这一节已经详细介绍过,而```component```的使用这一节也花了大量的篇幅从使用到原理进行了分析。然而学习了```slot,component```之后,我开始意识到```slot```和```component```并不是真正的内置组件。**内置组件是已经在源码初始化阶段就全局注册好的组件。**而``````和``````并没有被当成一个组件去处理,因此也没有组件的生命周期。```slot```只会在```render```函数阶段转换成```renderSlot```函数进行处理,而```component```也只是借助```is```属性将```createElement```的第一个参数从字符串转换为变量,仅此而已。因此重新回到概念的理解,**内置组件是源码自身提供的组件,**所以这一部分内容的重点,会放在内置组件是什么时候注册的,编译时有哪些不同这两个问题上来。这一部分只是一个抛砖引玉,接下来会有文章专门详细介绍```keep-alive,transition, transition-group```的实现原理。 288 | 289 | 290 | ### 12.3.1 构造器定义组件 291 | `Vue`初始化阶段会在构造器的```components```属性添加三个组件对象,每个组件对象的写法和我们在自定义组件过程的写法一致,有```render```函数,有生命周期,也会定义各种数据。 292 | ```js 293 | // keep-alive组件选项 294 | var KeepAlive = { 295 | render: function() {} 296 | } 297 | 298 | // transition 组件选项 299 | var Transition = { 300 | render: function() {} 301 | } 302 | 303 | // transition-group 组件选项 304 | var TransitionGroup = { 305 | render: function() {}, 306 | methods: {}, 307 | ··· 308 | } 309 | 310 | var builtInComponents = { 311 | KeepAlive: KeepAlive 312 | }; 313 | 314 | var platformComponents = { 315 | Transition: Transition, 316 | TransitionGroup: TransitionGroup 317 | }; 318 | 319 | // Vue构造器的选项配置,compoents选项合并 320 | extend(Vue.options.components, builtInComponents); 321 | extend(Vue.options.components, platformComponents); 322 | ``` 323 | 324 | 325 | `extend`方法我们在系列的开头,分析选项合并的时候有说过,将对象上的属性合并到源对象中,属性相同则覆盖。 326 | ```js 327 | // 将_from对象合并到to对象,属性相同时,则覆盖to对象的属性 328 | function extend (to, _from) { 329 | for (var key in _from) { 330 | to[key] = _from[key]; 331 | } 332 | return to 333 | } 334 | 335 | ``` 336 | 最终```Vue```构造器拥有了三个组件的配置选项。 337 | ```js 338 | Vue.components = { 339 | keepAlive: {}, 340 | transition: {}, 341 | transition-group: {}, 342 | } 343 | ``` 344 | 345 | ### 12.3.2 注册内置组件 346 | 仅仅有定义是不够的。组件需要被全局使用还得进行全局的注册,这其实在选项合并章节已经阐述清楚了。Vue实例在初始化过程中,最重要的第一步是进行选项的合并,而像内置组件这些资源类选项会有专门的选项合并策略,**最终构造器上的组件选项会以原型链的形式注册到实例的```compoonents```选项中(指令和过滤器同理)。** 347 | 348 | ```js 349 | // 资源选项 350 | var ASSET_TYPES = [ 351 | 'component', 352 | 'directive', 353 | 'filter' 354 | ]; 355 | 356 | // 定义资源合并的策略 357 | ASSET_TYPES.forEach(function (type) { 358 | strats[type + 's'] = mergeAssets; // 定义默认策略 359 | }); 360 | 361 | function mergeAssets (parentVal,childVal,vm,key) { 362 | var res = Object.create(parentVal || null); // 以parentVal为原型创建一个空对象 363 | if (childVal) { 364 | assertObjectType(key, childVal, vm); // components,filters,directives选项必须为对象 365 | return extend(res, childVal) // 子类选项赋值给空对象 366 | } else { 367 | return res 368 | } 369 | } 370 | ``` 371 | 372 | 关键的两步一个是```var res = Object.create(parentVal || null);```,它会以```parentVal```为原型创建一个空对象,最后是通过```extend```将用户自定义的```component```选项复制到空对象中。选项合并之后,内置组件也因此在全局完成了注册。 373 | 374 | ```json 375 | { 376 | components: { 377 | child1, 378 | __proto__: { 379 | keepAlive: {}, 380 | transition: {}, 381 | transitionGroup: {} 382 | } 383 | } 384 | } 385 | ``` 386 | 387 | 最后我们看看内置组件对象中并没有```template```模板,而是```render```函数,除了减少了耗性能的模板解析过程,我认为重要的原因是内置组件并没有渲染的实体。最后的最后,让我们一起期待后续对```keep-alive```原理分析,敬请期待。 388 | 389 | 390 | ### 12.4 小结 391 | 这节我们详细的介绍了动态组件的原理,我们经常使用``````以达到不同组件切换的目的,实际上是由于```is```这个关键字让模板编译成```render```函数时,组件```render```的标签是变量,这样在渲染阶段,随着数据的不同会渲染不同的组件。动态组件还有一种用法是使用内联模板去访问子组件的数据,这又增加了一种子父组件通信的方法。但是官方并不建议我们这样做,因为内联模板会让作用域变得混乱。内联组件实现父子通信的原理是它让父组件的编译过程放到了子组件,这样顺利成章的父组件就可以访问到子组件的变量。文章的最后引出了内置组件,```Vue```中真正的内置组件只有```keep-alive, transition, transition-group```三种,他们本质上是在内部定义好组件选项,并进行全局注册。下一节,我们将进入```keep-alive```内置组件的深度分析。 -------------------------------------------------------------------------------- /src/基础的数据代理检测.md: -------------------------------------------------------------------------------- 1 | 2 | > 简单回顾一下这个系列的前两节,前两节花了大量的篇幅介绍了```Vue```的选项合并,选项合并是```Vue```实例初始化的开始,```Vue```为开发者提供了丰富的选项配置,而每个选项都严格规定了合并的策略。然而这只是初始化中的第一步,这一节我们将对另一个重点的概念深入的分析,他就是**数据代理**,我们知道```Vue```大量利用了代理的思想,而除了响应式系统外,还有哪些场景也需要进行数据代理呢?这是我们这节分析的重点。 3 | 4 | 5 | ## 2.1 数据代理的含义 6 | 数据代理的另一个说法是数据劫持,当我们在访问或者修改对象的某个属性时,数据劫持可以拦截这个行为并进行额外的操作或者修改返回的结果。而我们知道```Vue```响应式系统的核心就是数据代理,代理使得数据在访问时进行依赖收集,在修改更新时对依赖进行更新,这是响应式系统的核心思路。而这一切离不开```Vue```对数据做了拦截代理。然而响应式并不是本节讨论的重点,这一节我们将看看数据代理在其他场景下的应用。在分析之前,我们需要掌握两种实现数据代理的方法: 7 | ```Object.defineProperty``` 和 ```Proxy```。 8 | 9 | 10 | ### 2.1.1 Object.defineProperty 11 | > 官方定义:```Object.defineProperty()```方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。 12 | 13 | 基本用法: 14 | ```js 15 | Object.defineProperty(obj, prop, descriptor) 16 | ``` 17 | 18 | `Object.defineProperty()`可以用来精确添加或修改对象的属性,只需要在```descriptor```对象中将属性特性描述清楚,```descriptor```的属性描述符有两种形式,一种是数据描述符,另一种是存取描述符,我们分别看看各自的特点。 19 | 20 | 1. 数据描述符,它拥有四个属性配置 21 | 22 | - `configurable`:数据是否可删除,可配置 23 | - `enumerable`:属性是否可枚举 24 | - `value`:属性值,默认为```undefined``` 25 | - `writable`:属性是否可读写 26 | 27 | 2. 存取描述符,它同样拥有四个属性选项 28 | 29 | - `configurable`:数据是否可删除,可配置 30 | - `enumerable`:属性是否可枚举 31 | - `get`:一个给属性提供 ```getter``` 的方法,如果没有 ```getter``` 则为 ```undefined```。 32 | - `set`:一个给属性提供 ```setter``` 的方法,如果没有 ```setter``` 则为 ```undefined```。 33 | 34 | **需要注意的是: 数据描述符的```value,writable``` 和 存取描述符中的```get, set```属性不能同时存在,否则会抛出异常。** 35 | 有了```Object.defineProperty```方法,我们可以方便的利用存取描述符中的```getter/setter```来进行数据的监听,这也是响应式构建的雏形。```getter```方法可以让我们在访问数据时做额外的操作处理,```setter```方法使得我们可以在数据更新时修改返回的结果。看看下面的例子,由于设置了数据代理,当我们访问对象```o```的```a```属性时,会触发```getter```执行钩子函数,当修改```a```属性的值时,会触发```setter```钩子函数去修改返回的结果。 36 | ```js 37 | var o = {} 38 | var value; 39 | Object.defineProperty(o, 'a', { 40 | get() { 41 | console.log('获取值') 42 | return value 43 | }, 44 | set(v) { 45 | console.log('设置值') 46 | value = qqq 47 | } 48 | }) 49 | o.a = 'sss' 50 | // 设置值 51 | console.log(o.a) 52 | // 获取值 53 | // 'qqq' 54 | 55 | ``` 56 | 57 | 前面说到```Object.defineProperty```的```get```和```set```方法是对对象进行监测并响应变化,那么数组类型是否也可以监测呢,参照监听属性的思路,我们用数组的下标作为属性,数组的元素作为拦截对象,看看```Object.defineProperty```是否可以对数组的数据进行监控拦截。 58 | ```js 59 | var arr = [1,2,3]; 60 | arr.forEach((item, index) => { 61 | Object.defineProperty(arr, index, { 62 | get() { 63 | console.log('数组被getter拦截') 64 | return item 65 | }, 66 | set(value) { 67 | console.log('数组被setter拦截') 68 | return item = value 69 | } 70 | }) 71 | }) 72 | 73 | arr[1] = 4; 74 | console.log(arr) 75 | // 结果 76 | 数组被setter拦截 77 | 数组被getter拦截 78 | 4 79 | ``` 80 | 显然,**已知长度的数组是可以通过索引属性来设置属性的访问器属性的。**但是数组的添加确无法进行拦截,这个也很好理解,不管是通过```arr.push()```还是```arr[10] = 10```添加的数据,数组所添加的索引值并没有预先加入数据拦截中,所以自然无法进行拦截处理。这个也是使用```Object.defineProperty```进行数据代理的弊端。为了解决这个问题,```Vue```在响应式系统中对数组的方法进行了重写,间接的解决了这个问题,详细细节可以参考后续的响应式系统分析。 81 | 82 | 另外如果需要拦截的对象属性嵌套多层,如果没有递归去调用```Object.defineProperty```进行拦截,深层次的数据也依然无法监测。 83 | 84 | ### 2.1.2 Proxy 85 | 为了解决像数组这类无法进行数据拦截,以及深层次的嵌套问题,```es6```引入了```Proxy```的概念,它是真正在语言层面对数据拦截的定义。和```Object.defineProperty```一样,```Proxy```可以修改某些操作的默认行为,但是不同的是,**```Proxy```针对目标对象会创建一个新的实例对象,并将目标对象代理到新的实例对象上,**。 本质的区别是后者会创建一个新的对象对原对象做代理,外界对原对象的访问,都必须先通过这层代理进行拦截处理。而拦截的结果是**我们只要通过操作新的实例对象就能间接的操作真正的目标对象了**。针对```Proxy```,下面是基础的写法: 86 | ```js 87 | var obj = {} 88 | var nobj = new Proxy(obj, { 89 | get(target, key, receiver) { 90 | console.log('获取值') 91 | return Reflect.get(target, key, receiver) 92 | }, 93 | set(target, key, value, receiver) { 94 | console.log('设置值') 95 | return Reflect.set(target, key, value, receiver) 96 | } 97 | }) 98 | 99 | nobj.a = '代理' 100 | console.log(obj) 101 | // 结果 102 | 设置值 103 | {a: "代理"} 104 | ``` 105 | 106 | 107 | 上面的```get,set```是```Proxy```支持的拦截方法,而```Proxy``` 支持的拦截操作有13种之多,具体可以参照[ES6-Proxy](http://es6.ruanyifeng.com/#docs/proxy)文档,前面提到,```Object.defineProperty```的```getter```和```setter```方法并不适合监听拦截数组的变化,那么新引入的```Proxy```又能否做到呢?我们看下面的例子。 108 | 109 | ```js 110 | var arr = [1, 2, 3] 111 | let obj = new Proxy(arr, { 112 | get: function (target, key, receiver) { 113 | // console.log("获取数组元素" + key); 114 | return Reflect.get(target, key, receiver); 115 | }, 116 | set: function (target, key, receiver) { 117 | console.log('设置数组'); 118 | return Reflect.set(target, key, receiver); 119 | } 120 | }) 121 | // 1. 改变已存在索引的数据 122 | obj[2] = 3 123 | // result: 设置数组 124 | // 2. push,unshift添加数据 125 | obj.push(4) 126 | // result: 设置数组 * 2 (索引和length属性都会触发setter) 127 | // // 3. 直接通过索引添加数组 128 | obj[5] = 5 129 | // result: 设置数组 * 2 130 | // // 4. 删除数组元素 131 | obj.splice(1, 1) 132 | 133 | ``` 134 | 135 | 显然```Proxy```完美的解决了数组的监听检测问题,针对数组添加数据,删除数据的不同方法,代理都能很好的拦截处理。另外```Proxy```也很好的解决了深层次嵌套对象的问题,具体读者可以自行举例分析。 136 | 137 | ## 2.2 initProxy 138 | 数据拦截的思想除了为构建响应式系统准备,它也可以为**数据进行筛选过滤**,我们接着往下看初始化的代码,在合并选项后,```vue```接下来会为```vm```实例设置一层代理,这层代理可以为**vue在模板渲染时进行一层数据筛选**,这个过程究竟怎么发生的,我们看代码的实现。 139 | 140 | ```js 141 | Vue.prototype._init = function(options) { 142 | // 选项合并 143 | ... 144 | { 145 | // 对vm实例进行一层代理 146 | initProxy(vm); 147 | } 148 | ... 149 | } 150 | ``` 151 | `initProxy`的实现如下: 152 | ```js 153 | // 代理函数 154 | var initProxy = function initProxy (vm) { 155 | 156 | if (hasProxy) { 157 | var options = vm.$options; 158 | var handlers = options.render && options.render._withStripped 159 | ? getHandler 160 | : hasHandler; 161 | // 代理vm实例到vm属性_renderProxy 162 | vm._renderProxy = new Proxy(vm, handlers); 163 | } else { 164 | vm._renderProxy = vm; 165 | } 166 | }; 167 | ``` 168 | 169 | 首先是判断浏览器是否支持原生的```proxy```。 170 | ```js 171 | var hasProxy = 172 | typeof Proxy !== 'undefined' && isNative(Proxy); 173 | ``` 174 | 当浏览器支持```Proxy```时,```vm._renderProxy```会代理```vm```实例,并且代理过程也会随着参数的不同呈现不同的效果;当浏览器不支持```Proxy```时,直接将```vm```赋值给```vm._renderProxy```。 175 | 176 | 读到这里,我相信大家会有很多的疑惑。 177 | **1. 这层代理的访问时机是什么,也就是说什么场景会触发这层代理** 178 | **2. 参数```options.render._withStripped```代表着什么,```getHandler```和```hasHandler```又有什么不同。** 179 | **3. 如何理解为模板数据的访问进行数据筛选过滤。到底有什么数据需要过滤。** 180 | **4. 只有在支持原生```proxy```环境下才会建立这层代理,那么在旧的浏览器,非法的数据又将如何展示。** 181 | 182 | 带着这些疑惑,我们接着往下分析。 183 | 184 | ### 2.2.1 触发代理 185 | 源码中```vm._renderProxy```的使用出现在```Vue```实例的```_render```方法中,```Vue.prototype._render```是将渲染函数转换成```Virtual DOM```的方法,这部分是关于实例的挂载和模板引擎的解析,笔者并不会在这一章节中深入分析,我们只需要先有一个认知,**```Vue```内部在```js```和真实```DOM```节点中设立了一个中间层,这个中间层就是```Virtual DOM```,遵循```js -> virtual -> 真实dom```的转换过程,而```Vue.prototype._render```是前半段的转换,**当我们调用```render```函数时,代理的```vm._renderProxy```对象便会访问到。 186 | ```js 187 | Vue.prototype._render = function () { 188 | ··· 189 | // 调用vm._renderProxy 190 | vnode = render.call(vm._renderProxy, vm.$createElement); 191 | } 192 | ``` 193 | 194 | 那么代理的处理函数又是什么?我们回过头看看代理选项```handlers```的实现。 195 | ```handers```函数会根据 ```options.render._withStripped```的不同执行不同的代理函数,**当使用类似```webpack```这样的打包工具时,通常会使用```vue-loader```插件进行模板的编译,这个时候```options.render```是存在的,并且```_withStripped```的属性也会设置为```true```**(关于编译版本和运行时版本的区别可以参考后面章节),所以此时代理的选项是```hasHandler```,在其他场景下,代理的选项是```getHandler```。```getHandler,hasHandler```的逻辑相似,我们只分析使用```vue-loader```场景下```hasHandler```的逻辑。另外的逻辑,读者可以自行分析。 196 | 197 | ```js 198 | var hasHandler = { 199 | // key in obj或者with作用域时,会触发has的钩子 200 | has: function has (target, key) { 201 | ··· 202 | } 203 | }; 204 | ``` 205 | `hasHandler`函数定义了```has```的钩子,前面介绍过,```proxy```的钩子有13个之多,而```has```是其中一个,它用来拦截```propKey in proxy```的操作,返回一个布尔值。而除了拦截 ```in``` 操作符外,```has```钩子同样可以用来拦截```with```语句下的作用对象。例如: 206 | ```js 207 | var obj = { 208 | a: 1 209 | } 210 | var nObj = new Proxy(obj, { 211 | has(target, key) { 212 | console.log(target) // { a: 1 } 213 | console.log(key) // a 214 | return true 215 | } 216 | }) 217 | 218 | with(nObj) { 219 | a = 2 220 | } 221 | ``` 222 | 那么这两个触发条件是否跟```_render```过程有直接的关系呢?答案是肯定的。```vnode = render.call(vm._renderProxy, vm.$createElement);```的主体是```render```函数,而这个```render```函数就是包装成```with```的执行语句,**在执行```with```语句的过程中,该作用域下变量的访问都会触发```has```钩子,这也是模板渲染时之所有会触发代理拦截的原因。**我们通过代码来观察```render```函数的原形。 223 | ```js 224 | var vm = new Vue({ 225 | el: '#app' 226 | }) 227 | console.log(vm.$options.render) 228 | 229 | //输出, 模板渲染使用with语句 230 | ƒ anonymous() { 231 | with(this){return _c('div',{attrs:{"id":"app"}},[_v(_s(message)+_s(_test))])} 232 | } 233 | ``` 234 | 235 | 236 | ### 2.2.2 数据过滤 237 | 我们已经大致知道了```Proxy```代理的访问时机,那么设置这层代理的作用又在哪里呢?首先思考一个问题,我们通过```data```选项去设置实例数据,那么这些数据可以随着个人的习惯任意命名吗?显然不是的,如果你使用```js```的关键字(像```Object,Array,NaN```)去命名,这是不被允许的。另一方面,```Vue```源码内部使用了以```$,_```作为开头的内部变量,所以以```$,_```开头的变量名也是不被允许的,这就构成了数据过滤监测的前提。接下来我们具体看```hasHandler```的细节实现。 238 | 239 | ```js 240 | var hasHandler = { 241 | has: function has (target, key) { 242 | var has = key in target; 243 | // isAllowed用来判断模板上出现的变量是否合法。 244 | var isAllowed = allowedGlobals(key) || 245 | (typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data)); 246 | // _和$开头的变量不允许出现在定义的数据中,因为他是vue内部保留属性的开头。 247 | // 1. warnReservedPrefix: 警告不能以$ _开头的变量 248 | // 2. warnNonPresent: 警告模板出现的变量在vue实例中未定义 249 | if (!has && !isAllowed) { 250 | if (key in target.$data) { warnReservedPrefix(target, key); } 251 | else { warnNonPresent(target, key); } 252 | } 253 | return has || !isAllowed 254 | } 255 | }; 256 | 257 | // 模板中允许出现的非vue实例定义的变量 258 | var allowedGlobals = makeMap( 259 | 'Infinity,undefined,NaN,isFinite,isNaN,' + 260 | 'parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,' + 261 | 'Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,' + 262 | 'require' // for Webpack/Browserify 263 | ); 264 | ``` 265 | 首先```allowedGlobals```定义了```javascript```保留的关键字,这些关键字是不允许作为用户变量存在的。```(typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data)```的逻辑对以```$,_```开头,或者是否是```data```中未定义的变量做判断过滤。这里对未定义变量的场景多解释几句,前面说到,代理的对象```vm.renderProxy```是在执行```_render```函数中访问的,而在使用了```template```模板的情况下,```render```函数是对模板的解析结果,换言之,之所以会触发数据代理拦截是因为模板中使用了变量,例如```
{{message}}}
```。而如果我们在模板中使用了未定义的变量,这个过程就被```proxy```拦截,并定义为不合法的变量使用。 266 | 267 | 268 | 我们可以看看两个报错信息的源代码(是不是很熟悉): 269 | ```js 270 | // 模板使用未定义的变量 271 | var warnNonPresent = function (target, key) { 272 | warn( 273 | "Property or method \"" + key + "\" is not defined on the instance but " + 274 | 'referenced during render. Make sure that this property is reactive, ' + 275 | 'either in the data option, or for class-based components, by ' + 276 | 'initializing the property. ' + 277 | 'See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.', 278 | target 279 | ); 280 | }; 281 | 282 | // 使用$,_开头的变量 283 | var warnReservedPrefix = function (target, key) { 284 | warn( 285 | "Property \"" + key + "\" must be accessed with \"$data." + key + "\" because " + 286 | 'properties starting with "$" or "_" are not proxied in the Vue instance to ' + 287 | 'prevent conflicts with Vue internals' + 288 | 'See: https://vuejs.org/v2/api/#data', 289 | target 290 | ); 291 | }; 292 | ``` 293 | 294 | 分析到这里,前面的疑惑只剩下最后一个问题。只有在浏览器支持```proxy```的情况下,才会执行```initProxy```设置代理,那么在不支持的情况下,数据过滤就失效了,此时非法的数据定义还能正常运行吗?我们先对比下面两个结论。 295 | 296 | ```js 297 | // 模板中使用_开头的变量,且在data选项中有定义 298 |
{{_test}}
299 | new Vue({ 300 | el: '#app', 301 | data: { 302 | _test: 'proxy' 303 | } 304 | }) 305 | ``` 306 | 307 | 308 | 1. 支持```proxy```浏览器的结果 309 | 310 | ![](./img/2.1.png) 311 | 312 | 2. 不支持```proxy```浏览器的结果 313 | 314 | ![](./img/2.2.png) 315 | 316 | 317 | 显然,在没有经过代理的情况下,使用```_```开头的变量依旧会 318 | 报错,但是它变成了```js```语言层面的错误,表示该变量没有被声明。但是这个报错无法在```Vue```这一层知道错误的详细信息,而这就是能使用```Proxy```的好处。接着我们会思考,既然已经在```data```选项中定义了```_test```变量,为什么访问时还是找不到变量的定义呢? 319 | 原来在初始化数据阶段,```Vue```已经为数据进行了一层筛选的代理。具体看```initData```对数据的代理,其他实现细节不在本节讨论范围内。 320 | 321 | ```js 322 | function initData(vm) { 323 | vm._data = typeof data === 'function' ? getData(data, vm) : data || {} 324 | if (!isReserved(key)) { 325 | // 数据代理,用户可直接通过vm实例返回data数据 326 | proxy(vm, "_data", key); 327 | } 328 | } 329 | 330 | function isReserved (str) { 331 | var c = (str + '').charCodeAt(0); 332 | // 首字符是$, _的字符串 333 | return c === 0x24 || c === 0x5F 334 | } 335 | ``` 336 | `vm._data`可以拿到最终```data```选项合并的结果,```isReserved```会过滤以```$,_```开头的变量,```proxy```会为实例数据的访问做代理,当我们访问```this.message```时,实际上访问的是```this._data.message```,而有了```isReserved```的筛选,即使```this._data._test```存在,我们依旧无法在访问```this._test```时拿到```_test```变量。这就解释了为什么会有变量没有被声明的语法错误,而```proxy```的实现,又是基于上述提到的```Object.defineProperty```来实现的。 337 | 338 | ```js 339 | function proxy (target, sourceKey, key) { 340 | sharedPropertyDefinition.get = function proxyGetter () { 341 | // 当访问this[key]时,会代理访问this._data[key]的值 342 | return this[sourceKey][key] 343 | }; 344 | sharedPropertyDefinition.set = function proxySetter (val) { 345 | this[sourceKey][key] = val; 346 | }; 347 | Object.defineProperty(target, key, sharedPropertyDefinition); 348 | } 349 | ``` 350 | 351 | 352 | 353 | ## 2.3 小结 354 | 这一节内容,详细的介绍了数据代理在```Vue```的实现思路和另一个应用场景,数据代理是一种设计模式,也是一种编程思想,```Object.defineProperty```和```Proxy```都可以实现数据代理,但是他们各有优劣,前者兼容性较好,但是却无法对数组或者嵌套的对象进行代理监测,而```Proxy```基本可以解决所有的问题,但是对兼容性要求很高。```Vue```中的响应式系统是以```Object.defineProperty```实现的,但是这并不代表没有```Proxy```的应用。```initProxy```就是其中的例子,这层代理会在模板渲染时对一些非法或者没有定义的变量进行筛选判断,和没有数据代理相比,非法的数据定义错误会提前到应用层捕获,这也有利于开发者对错误的排查。 -------------------------------------------------------------------------------- /src/来,跟我一起实现diff算法.md: -------------------------------------------------------------------------------- 1 | > 这一节,依然是**深入剖析Vue源码系列**,上几节内容介绍了```Virtual DOM```是Vue在渲染机制上做的优化,而渲染的核心在于数据变化时,如何高效的更新节点,这就是diff算法。由于源码中关于```diff```算法部分流程复杂,直接剖析每个流程不易于理解,所以这一节我们换一个思路,参考源码来手动实现一个简易版的```diff```算法。 2 | 3 | 之前讲到```Vue```在渲染机制的优化上,引入了```Virtual DOM```的概念,利用```Virtual DOM```描述一个真实的```DOM```,本质上是在```JS```和真实```DOM```之间架起了一层缓冲层。当我们通过大量的```JS```运算,并将最终结果反应到浏览器进行渲染时,```Virtual DOM```可以将多个改动合并成一个批量的操作,从而减少 ```dom``` 重排的次数,进而缩短了生成渲染树和绘制节点所花的时间,达到渲染优化的目的。之前的章节,我们简单的介绍了```Vue```中```Vnode```的概念,以及创建```Vnode```到```渲染Vnode```再到真实```DOM```的过程。如果有忘记流程的,可以参考前面的章节分析。 4 | 5 | 6 | **从```render```函数到创建虚拟```DOM```,再到渲染真实节点,这一过程是完整的,也是容易理解的。然而引入虚拟```DOM```的核心不在这里,而在于当数据发生变化时,如何最优化数据变动到视图更新的过程。这一个过程才是```Vnode```更新视图的核心,也就是常说的```diff```算法。**下面跟着我来实现一个简易版的```diff```算法 7 | 8 | ## 8.1 创建基础类 9 | 代码编写过程会遇到很多基本类型的判断,第一步需要先将这些方法封装。 10 | ```js 11 | class Util { 12 | constructor() {} 13 | // 检测基础类型 14 | _isPrimitive(value) { 15 | return (typeof value === 'string' || typeof value === 'number' || typeof value === 'symbol' || typeof value === 'boolean') 16 | } 17 | // 判断值不为空 18 | _isDef(v) { 19 | return v !== undefined && v !== null 20 | } 21 | } 22 | // 工具类的使用 23 | const util = new Util() 24 | ``` 25 | 26 | ## 8.2 创建Vnode 27 | `Vnode`这个类在之前章节已经分析过源码,本质上是用一个对象去描述一个真实的```DOM```元素,简易版关注点在于元素的```tag```标签,元素的属性集合```data```,元素的子节点```children```,```text```为元素的文本节点,简单的描述类如下: 28 | ```js 29 | class VNode { 30 | constructor(tag, data, children) { 31 | this.tag = tag; 32 | this.data = data; 33 | this.children = children; 34 | this.elm = '' 35 | // text属性用于标志Vnode节点没有其他子节点,只有纯文本 36 | this.text = util._isPrimitive(this.children) ? this.children : '' 37 | } 38 | } 39 | ``` 40 | ## 8.3 模拟渲染过程 41 | 接下来需要创建另一个类模拟将```render```函数转换为```Vnode```,并将```Vnode```渲染为真实```DOM```的过程,我们将这个类定义为```Vn```,```Vn```具有两个基本的方法```createVnode, createElement```, 分别实现创建虚拟```Vnode```,和创建真实```DOM```的过程。 42 | 43 | ### 8.3.1 createVnode 44 | `createVnode`模拟```Vue```中```render```函数的实现思路,目的是将数据转换为虚拟的```Vnode```,先看具体的使用和定义。 45 | 46 | ```js 47 | // index.html 48 | 49 | 72 | 73 | 74 | 75 | // diff.js 76 | (function(global) { 77 | class Vn { 78 | constructor() {} 79 | // 创建虚拟Vnode 80 | createVnode(tag, data, children) { 81 | return new VNode(tag, data, children) 82 | } 83 | } 84 | global.vn = new Vn() 85 | }(this)) 86 | 87 | ``` 88 | 这是一个完整的```Vnode```对象,我们已经可以用这个对象来简单的描述一个```DOM```节点,而```createElement```就是将这个对象对应到真实节点的过程。最终我们希望的结果是这样的。 89 | 90 | **Vnode对象** 91 | 92 | ![](./img/8.1.png) 93 | 94 | 95 | **渲染结果** 96 | 97 | ![](./img/8.2.png) 98 | ### 8.3.2 createElement 99 | 渲染真实```DOM```的过程就是遍历```Vnode```对象,递归创建真实节点的过程,这个不是本文的重点,所以我们可以粗糙的实现。 100 | ```js 101 | class Vn { 102 | createElement(vnode, options) { 103 | let el = options.el; 104 | if(!el || !document.querySelector(el)) return console.error('无法找到根节点') 105 | let _createElement = vnode => { 106 | const { tag, data, children } = vnode; 107 | const ele = document.createElement(tag); 108 | // 添加属性 109 | this.setAttr(ele, data); 110 | // 简单的文本节点,只要创建文本节点即可 111 | if (util._isPrimitive(children)) { 112 | const testEle = document.createTextNode(children); 113 | ele.appendChild(testEle) 114 | } else { 115 | // 复杂的子节点需要遍历子节点递归创建节点。 116 | children.map(c => ele.appendChild(_createElement(c))) 117 | } 118 | return ele 119 | } 120 | document.querySelector(el).appendChild(_createElement(vnode)) 121 | } 122 | } 123 | ``` 124 | ### 8.3.3 setAttr 125 | `setAttr`是为节点设置属性的方法,利用```DOM```原生的```setAttribute```为每个节点设置属性值。 126 | ```js 127 | class Vn { 128 | setAttr(el, data) { 129 | if (!el) return 130 | const attrs = data.attrs; 131 | if (!attrs) return; 132 | Object.keys(attrs).forEach(a => { 133 | el.setAttribute(a, attrs[a]); 134 | }) 135 | } 136 | } 137 | ``` 138 | 至此一个简单的 **数据 -> ```Virtual DOM``` => 真实```DOM```**的模型搭建成功,这也是数据变化、比较、更新的基础。 139 | 140 | 141 | ## 8.4 diff算法实现 142 | 更新组件的过程首先是响应式数据发生了变化,数据频繁的修改如果直接渲染到真实```DOM```上会引起整个```DOM```树的重绘和重排,频繁的重绘和重排是极其消耗性能的。如何优化这一渲染过程,```Vue```源码中给出了两个具体的思路,其中一个是在介绍响应式系统时提到的将多次修改推到一个队列中,在下一个```tick```去执行视图更新,另一个就是接下来要着重介绍的```diff```算法,将需要修改的数据进行比较,并只渲染必要的```DOM```。 143 | 144 | 数据的改变最终会导致节点的改变,所以```diff```算法的核心在于在尽可能小变动的前提下找到需要更新的节点,直接调用原生相关```DOM```方法修改视图。不管是真实```DOM```还是前面创建的```Virtual DOM```,都可以理解为一颗```DOM```树,**算法比较节点不同时,只会进行同层节点的比较,不会跨层进行比较,这也大大减少了算法复杂度。** 145 | 146 | 147 | ### 8.4.1 diffVnode 148 | 在之前的基础上,我们实现一个思路,1秒之后数据发生改变。 149 | ```js 150 | // index.html 151 | setTimeout(function() { 152 | arr = [{ 153 | tag: 'span', 154 | text: 1 155 | },{ 156 | tag: 'strong', 157 | text: 2 158 | },{ 159 | tag: 'i', 160 | text: 3 161 | },{ 162 | tag: 'i', 163 | text: 4 164 | }] 165 | // newVnode 表示改变后新的Vnode树 166 | const newVnode = createVnode(); 167 | // diffVnode会比较新旧Vnode树,并完成视图更新 168 | vn.diffVnode(newVnode, preVnode); 169 | }) 170 | ``` 171 | `diffVnode`的逻辑,会对比新旧节点的不同,并完成视图渲染更新 172 | ```js 173 | class Vn { 174 | ··· 175 | diffVnode(nVnode, oVnode) { 176 | if (!this._sameVnode(nVnode, oVnode)) { 177 | // 直接更新根节点及所有子节点 178 | return *** 179 | } 180 | this.generateElm(vonde); 181 | this.patchVnode(nVnode, oVnode); 182 | } 183 | } 184 | ``` 185 | ### 8.4.2 _sameVnode 186 | 新旧节点的对比是算法的第一步,如果新旧节点的根节点不是同一个节点,则直接替换节点。这遵从上面提到的原则,**只进行同层节点的比较,节点不一致,直接用新节点及其子节点替换旧节点**。为了理解方便,我们假定节点相同的判断是```tag```标签是否一致(实际源码要复杂)。 187 | ```js 188 | class Vn { 189 | _sameVnode(n, o) { 190 | return n.tag === o.tag; 191 | } 192 | } 193 | ``` 194 | ### 8.4.3 generateElm 195 | `generateElm`的作用是跟踪每个节点实际的真实节点,方便在对比虚拟节点后实时更新真实```DOM```节点。虽然```Vue```源码中做法不同,但是这不是分析```diff```的重点。 196 | ```js 197 | class Vn { 198 | generateElm(vnode) { 199 | const traverseTree = (v, parentEl) => { 200 | let children = v.children; 201 | if(Array.isArray(children)) { 202 | children.forEach((c, i) => { 203 | c.elm = parentEl.childNodes[i]; 204 | traverseTree(c, c.elm) 205 | }) 206 | } 207 | } 208 | traverseTree(vnode, this.el); 209 | } 210 | } 211 | ``` 212 | 执行```generateElm```方法后,我们可以在旧节点的```Vnode```中跟踪到每个```Virtual DOM```的真实节点信息。 213 | 214 | ### 8.4.4 patchVnode 215 | `patchVnode`是新旧```Vnode```对比的核心方法,对比的逻辑如下。 216 | 1. 节点相同,且节点除了拥有文本节点外没有其他子节点。这种情况下直接替换文本内容。 217 | 2. 新节点没有子节点,旧节点有子节点,则删除旧节点所有子节点。 218 | 3. 旧节点没有子节点,新节点有子节点,则用新的所有子节点去更新旧节点。 219 | 4. 新旧都存在子节点。则对比子节点内容做操作。 220 | 221 | 代码逻辑如下: 222 | ```js 223 | class Vn { 224 | patchVnode(nVnode, oVnode) { 225 | 226 | if(nVnode.text && nVnode.text !== oVnode) { 227 | // 当前真实dom元素 228 | let ele = oVnode.elm 229 | // 子节点为文本节点 230 | ele.textContent = nVnode.text; 231 | } else { 232 | const oldCh = oVnode.children; 233 | const newCh = nVnode.children; 234 | // 新旧节点都存在。对比子节点 235 | if (util._isDef(oldCh) && util._isDef(newCh)) { 236 | this.updateChildren(ele, newCh, oldCh) 237 | } else if (util._isDef(oldCh)) { 238 | // 新节点没有子节点 239 | } else { 240 | // 老节点没有子节点 241 | } 242 | } 243 | } 244 | } 245 | ``` 246 | 上述例子在```patchVnode```过程中,新旧子节点都存在,所以会走```updateChildren```分支。 247 | 248 | ### 8.4.5 updateChildren 249 | 子节点的对比,我们通过文字和画图的形式分析,通过图解的形式可以很清晰看到```diff```算法的巧妙之处。 250 | 251 | 大致逻辑是: 252 | 1. 旧节点的起始位置为```oldStartIndex```,截至位置为```oldEndIndex```,新节点的起始位置为```newStartIndex```,截至位置为```newEndIndex```。 253 | 2. 新旧```children```的起始位置的元素两两对比,顺序是```newStartVnode, oldStartVnode```; ```newEndVnode, oldEndVnode```;```newEndVnode, oldStartVnode```;```newStartIndex, oldEndIndex``` 254 | 3. ```newStartVnode, oldStartVnode```节点相同,执行一次```patchVnode```过程,也就是递归对比相应子节点,并替换节点的过程。```oldStartIndex,newStartIndex```都像右移动一位。 255 | 4. ```newEndVnode, oldEndVnode```节点相同,执行一次```patchVnode```过程,递归对比相应子节点,并替换节点。```oldEndIndex, newEndIndex```都像左移动一位。 256 | 5. ```newEndVnode, oldStartVnode```节点相同,执行一次```patchVnode```过程,并将旧的```oldStartVnode```移动到尾部,```oldStartIndex```右移一味,```newEndIndex```左移一位。 257 | 6. ```newStartIndex, oldEndIndex```节点相同,执行一次```patchVnode```过程,并将旧的```oldEndVnode```移动到头部,```oldEndIndex```左移一味,```newStartIndex```右移一位。 258 | 7. 四种组合都不相同,则会搜索旧节点所有子节点,找到将这个旧节点和```newStartVnode```执行```patchVnode```过程。 259 | 8. 不断对比的过程使得```oldStartIndex```不断逼近```oldEndIndex```,```newStartIndex```不断逼近```newEndIndex```。当```oldEndIndex <= oldStartIndex```说明旧节点已经遍历完了,此时只要批量增加新节点即可。当```newEndIndex <= newStartIndex```说明旧节点还有剩下,此时只要批量删除旧节点即可。 260 | 261 | 262 | 结合前面的例子: 263 | 264 | 265 | 第一步: 266 | 267 | ![](./img/8.3.png) 268 | 269 | 第二步: 270 | 271 | ![](./img/8.4.png) 272 | 273 | 第三步: 274 | 275 | ![](./img/8.5.png) 276 | 277 | 第三步: 278 | 279 | 280 | ![](./img/8.6.png) 281 | 282 | 第四步: 283 | 284 | ![](./img/8.7.png) 285 | 286 | 根据这些步骤,代码实现如下: 287 | 288 | ```js 289 | class Vn { 290 | updateChildren(el, newCh, oldCh) { 291 | // 新children开始标志 292 | let newStartIndex = 0; 293 | // 旧children开始标志 294 | let oldStartIndex = 0; 295 | // 新children结束标志 296 | let newEndIndex = newCh.length - 1; 297 | // 旧children结束标志 298 | let oldEndIndex = oldCh.length - 1; 299 | let oldKeyToId; 300 | let idxInOld; 301 | let newStartVnode = newCh[newStartIndex]; 302 | let oldStartVnode = oldCh[oldStartIndex]; 303 | let newEndVnode = newCh[newEndIndex]; 304 | let oldEndVnode = oldCh[oldEndIndex]; 305 | // 遍历结束条件 306 | while (newStartIndex <= newEndIndex && oldStartIndex <= oldEndIndex) { 307 | // 新children开始节点和旧开始节点相同 308 | if (this._sameVnode(newStartVnode, oldStartVnode)) { 309 | this.patchVnode(newCh[newStartIndex], oldCh[oldStartIndex]); 310 | newStartVnode = newCh[++newStartIndex]; 311 | oldStartVnode = oldCh[++oldStartIndex] 312 | } else if (this._sameVnode(newEndVnode, oldEndVnode)) { 313 | // 新childre结束节点和旧结束节点相同 314 | this.patchVnode(newCh[newEndIndex], oldCh[oldEndIndex]) 315 | oldEndVnode = oldCh[--oldEndIndex]; 316 | newEndVnode = newCh[--newEndIndex] 317 | } else if (this._sameVnode(newEndVnode, oldStartVnode)) { 318 | // 新childre结束节点和旧开始节点相同 319 | this.patchVnode(newCh[newEndIndex], oldCh[oldStartIndex]) 320 | // 旧的oldStartVnode移动到尾部 321 | el.insertBefore(oldCh[oldStartIndex].elm, null); 322 | oldStartVnode = oldCh[++oldStartIndex]; 323 | newEndVnode = newCh[--newEndIndex]; 324 | } else if (this._sameVnode(newStartVnode, oldEndVnode)) { 325 | // 新children开始节点和旧结束节点相同 326 | this.patchVnode(newCh[newStartIndex], oldCh[oldEndIndex]); 327 | el.insertBefore(oldCh[oldEndIndex].elm, oldCh[oldStartIndex].elm); 328 | oldEndVnode = oldCh[--oldEndIndex]; 329 | newStartVnode = newCh[++newStartIndex]; 330 | } else { 331 | // 都不符合的处理,查找新节点中与对比旧节点相同的vnode 332 | this.findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx); 333 | } 334 | } 335 | // 新节点比旧节点多,批量增加节点 336 | if(oldEndIndex <= oldStartIndex) { 337 | for (let i = newStartIndex; i <= newEndIndex; i++) { 338 | // 批量增加节点 339 | this.createElm(oldCh[oldEndIndex].elm, newCh[i]) 340 | } 341 | } 342 | } 343 | 344 | createElm(el, vnode) { 345 | let tag = vnode.tag; 346 | const ele = document.createElement(tag); 347 | this._setAttrs(ele, vnode.data); 348 | const testEle = document.createTextNode(vnode.children); 349 | ele.appendChild(testEle) 350 | el.parentNode.insertBefore(ele, el.nextSibling) 351 | } 352 | 353 | // 查找匹配值 354 | findIdxInOld(newStartVnode, oldCh, start, end) { 355 | for (var i = start; i < end; i++) { 356 | var c = oldCh[i]; 357 | if (util.isDef(c) && this.sameVnode(newStartVnode, c)) { return i } 358 | } 359 | } 360 | } 361 | ``` 362 | 363 | ## 8.5 diff算法优化 364 | 前面有个分支,当四种比较节点都找不到匹配时,会调用```findIdxInOld```找到旧节点中和新的比较节点一致的节点。节点搜索在数量级较大时是缓慢的。查看```Vue```的源码,发现它在这一个环节做了优化,也就是我们经常在编写列表时被要求加入的唯一属性**key**,有了这个唯一的标志位,我们可以对旧节点建立简单的字典查询,只要有```key```值便可以方便的搜索到符合要求的旧节点。修改代码: 365 | ```js 366 | class Vn { 367 | updateChildren() { 368 | ··· 369 | } else { 370 | // 都不符合的处理,查找新节点中与对比旧节点相同的vnode 371 | if (!oldKeyToId) oldKeyToId = this.createKeyMap(oldCh, oldStartIndex, oldEndIndex); 372 | idxInOld = util._isDef(newStartVnode.key) ? oldKeyToId[newStartVnode.key] : this.findIdxInOld(newStartVnode, oldCh, oldStartIndex, oldEndIndex); 373 | // 后续操作 374 | } 375 | } 376 | // 建立字典 377 | createKeyMap(oldCh, start, old) { 378 | const map = {}; 379 | for(let i = start; i < old; i++) { 380 | if(oldCh.key) map[key] = i; 381 | } 382 | return map; 383 | } 384 | } 385 | 386 | 387 | ``` 388 | 389 | ## 8.6 问题思考 390 | 最后我们思考一个问题,```Virtual DOM``` 的重绘性能真的比单纯的```innerHTML```要好吗,其实并不是这样的,作者的[解释](https://www.zhihu.com/question/31809713/answer/53544875) 391 | 392 | > - `innerHTML: render html string O(template size) +` 重新创建所有 ```DOM``` 元素 ```O(DOM size)``` 393 | 394 | > - `Virtual DOM: render Virtual DOM + diff O(template size) +` 必要的 ```DOM``` 更新 ```O(DOM change)``` 395 | 396 | > - `Virtual DOM render + diff` 显然比渲染 html 字符串要慢,但是!它依然是纯 js 层面的计算,比起后面的 ```DOM``` 操作来说,依然便宜了太多。可以看到,```innerHTML``` 的总计算量不管是 ```js``` 计算还是 ```DOM ```操作都是和整个界面的大小相关,但```Virtual DOM``` 的计算量里面,只有 ```js``` 计算和界面大小相关,DOM 操作是和数据的变动量相关的。 397 | -------------------------------------------------------------------------------- /src/彻底搞懂Vue中keep-alive的魔法-上.md: -------------------------------------------------------------------------------- 1 | > 前言:上一节最后稍微提到了```Vue```内置组件的相关内容,从这一节开始,将会对某个具体的内置组件进行分析。首先是```keep-alive```,它是我们日常开发中经常使用的组件,我们在不同组件间切换时,经常要求保持组件的状态,以避免重复渲染组件造成的性能损耗,而```keep-alive```经常和上一节介绍的动态组件结合起来使用。由于内容过多,```keep-alive```的源码分析将分为上下两部分,这一节主要围绕```keep-alive```的首次渲染展开。 2 | 3 | 4 | 5 | ## 13.1 基本用法 6 | `keep-alive`的使用只需要在动态组件的最外层添加标签即可。 7 | 8 | ```html 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | ``` 18 | 19 | 20 | ```js 21 | 22 | var child1 = { 23 | template: '

{{num}}

', 24 | data() { 25 | return { 26 | num: 1 27 | } 28 | }, 29 | methods: { 30 | add() { 31 | this.num++ 32 | } 33 | }, 34 | } 35 | var child2 = { 36 | template: '
child2
' 37 | } 38 | var vm = new Vue({ 39 | el: '#app', 40 | components: { 41 | child1, 42 | child2, 43 | }, 44 | data() { 45 | return { 46 | chooseTabs: 'child1', 47 | } 48 | }, 49 | methods: { 50 | changeTabs(tab) { 51 | this.chooseTabs = tab; 52 | } 53 | } 54 | }) 55 | ``` 56 | 57 | 58 | 59 | 简单的结果如下,动态组件在```child1,child2```之间来回切换,当第二次切到```child1```时,```child1```保留着原来的数据状态,```num = 5```。 60 | 61 | ![](./img/13.1.gif) 62 | 63 | ## 13.2 从模板编译到生成vnode 64 | 按照以往分析的经验,我们会从模板的解析开始说起,第一个疑问便是:内置组件和普通组件在编译过程有区别吗?答案是没有的,不管是内置的还是用户定义组件,本质上组件在模板编译成```render```函数的处理方式是一致的,这里的细节不展开分析,有疑惑的可以参考前几节的原理分析。最终针对```keep-alive```的```render```函数的结果如下: 65 | 66 | ```js 67 | with(this){···_c('keep-alive',{attrs:{"include":"child2"}},[_c(chooseTabs,{tag:"component"})],1)} 68 | ``` 69 | 70 | 71 | 有了```render```函数,接下来从子开始到父会执行生成```Vnode```对象的过程,```_c('keep-alive'···)```的处理,会执行```createElement```生成组件```Vnode```,其中由于```keep-alive```是组件,所以会调用```createComponent```函数去创建子组件```Vnode```,```createComponent```之前也有分析过,这个环节和创建普通组件```Vnode```不同之处在于,```keep-alive```的```Vnode```会剔除多余的属性内容,**由于```keep-alive```除了```slot```属性之外,其他属性在组件内部并没有意义,例如```class```样式,``````等,所以在```Vnode```层剔除掉多余的属性是有意义的。而``````的写法在2.6以上的版本也已经被废弃。**(其中```abstract```作为抽象组件的标志,以及其作用我们后面会讲到) 72 | 73 | ```js 74 | // 创建子组件Vnode过程 75 | function createComponent(Ctordata,context,children,tag) { 76 | // abstract是内置组件(抽象组件)的标志 77 | if (isTrue(Ctor.options.abstract)) { 78 | // 只保留slot属性,其他标签属性都被移除,在vnode对象上不再存在 79 | var slot = data.slot; 80 | data = {}; 81 | if (slot) { 82 | data.slot = slot; 83 | } 84 | } 85 | } 86 | ``` 87 | 88 | ## 13.3 初次渲染 89 | 90 | `keep-alive`之所以特别,是因为它不会重复渲染相同的组件,只会利用初次渲染保留的缓存去更新节点。所以为了全面了解它的实现原理,我们需要从```keep-alive```的首次渲染开始说起。 91 | 92 | 93 | ### 13.3.1 流程图 94 | 为了理清楚流程,我大致画了一个流程图,流程图大致覆盖了初始渲染```keep-alive```所执行的过程,接下来会照着这个过程进行源码分析。 95 | 96 | ![](./img/13.2.png) 97 | 98 | 和渲染普通组件相同的是,```Vue```会拿到前面生成的```Vnode```对象执行真实节点创建的过程,也就是熟悉的```patch```过程,```patch```执行阶段会调用```createElm```创建真实```dom```,在创建节点途中,```keep-alive```的```vnode```对象会被认定是一个组件```Vnode```,因此针对组件```Vnode```又会执行```createComponent```函数,它会对```keep-alive```组件进行初始化和实例化。 99 | 100 | ```js 101 | function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) { 102 | var i = vnode.data; 103 | if (isDef(i)) { 104 | // isReactivated用来判断组件是否缓存。 105 | var isReactivated = isDef(vnode.componentInstance) && i.keepAlive; 106 | if (isDef(i = i.hook) && isDef(i = i.init)) { 107 | // 执行组件初始化的内部钩子 init 108 | i(vnode, false /* hydrating */); 109 | } 110 | if (isDef(vnode.componentInstance)) { 111 | // 其中一个作用是保留真实dom到vnode中 112 | initComponent(vnode, insertedVnodeQueue); 113 | insert(parentElm, vnode.elm, refElm); 114 | if (isTrue(isReactivated)) { 115 | reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm); 116 | } 117 | return true 118 | } 119 | } 120 | } 121 | ``` 122 | `keep-alive`组件会先调用内部钩子```init```方法进行初始化操作,我们先看看```init```过程做了什么操作。 123 | 124 | ```js 125 | // 组件内部钩子 126 | var componentVNodeHooks = { 127 | init: function init (vnode, hydrating) { 128 | if ( 129 | vnode.componentInstance && 130 | !vnode.componentInstance._isDestroyed && 131 | vnode.data.keepAlive 132 | ) { 133 | // kept-alive components, treat as a patch 134 | var mountedNode = vnode; // work around flow 135 | componentVNodeHooks.prepatch(mountedNode, mountedNode); 136 | } else { 137 | // 将组件实例赋值给vnode的componentInstance属性 138 | var child = vnode.componentInstance = createComponentInstanceForVnode( 139 | vnode, 140 | activeInstance 141 | ); 142 | child.$mount(hydrating ? vnode.elm : undefined, hydrating); 143 | } 144 | }, 145 | // 后面分析 146 | prepatch: function() {} 147 | } 148 | ``` 149 | 第一次执行,很明显组件```vnode```没有```componentInstance```属性,```vnode.data.keepAlive```也没有值,所以会**调用```createComponentInstanceForVnode```方法进行组件实例化并将组件实例赋值给```vnode```的```componentInstance```属性,** 最终执行组件实例的```$mount```方法进行实例挂载。 150 | 151 | `createComponentInstanceForVnode`就是组件实例化的过程,而组件实例化从系列的第一篇就开始说了,无非就是一系列选项合并,初始化事件,生命周期等初始化操作。 152 | 153 | ```js 154 | function createComponentInstanceForVnode (vnode, parent) { 155 | var options = { 156 | _isComponent: true, 157 | _parentVnode: vnode, 158 | parent: parent 159 | }; 160 | // 内联模板的处理,忽略这部分代码 161 | ··· 162 | // 执行vue子组件实例化 163 | return new vnode.componentOptions.Ctor(options) 164 | } 165 | ``` 166 | 167 | ### 13.3.2 内置组件选项 168 | 我们在使用组件的时候经常利用对象的形式定义组件选项,包括```data,method,computed```等,并在父组件或根组件中注册。```keep-alive```同样遵循这个道理,内置两字也说明了```keep-alive```是在```Vue```源码中内置好的选项配置,并且也已经注册到全局,这一部分的源码可以参考组态组件小节末尾对内置组件构造器和注册过程的介绍。这一部分我们重点关注一下```keep-alive```的具体选项。 169 | 170 | ```js 171 | // keepalive组件选项 172 | var KeepAlive = { 173 | name: 'keep-alive', 174 | // 抽象组件的标志 175 | abstract: true, 176 | // keep-alive允许使用的props 177 | props: { 178 | include: patternTypes, 179 | exclude: patternTypes, 180 | max: [String, Number] 181 | }, 182 | 183 | created: function created () { 184 | // 缓存组件vnode 185 | this.cache = Object.create(null); 186 | // 缓存组件名 187 | this.keys = []; 188 | }, 189 | 190 | destroyed: function destroyed () { 191 | for (var key in this.cache) { 192 | pruneCacheEntry(this.cache, key, this.keys); 193 | } 194 | }, 195 | 196 | mounted: function mounted () { 197 | var this$1 = this; 198 | // 动态include和exclude 199 | // 对include exclue的监听 200 | this.$watch('include', function (val) { 201 | pruneCache(this$1, function (name) { return matches(val, name); }); 202 | }); 203 | this.$watch('exclude', function (val) { 204 | pruneCache(this$1, function (name) { return !matches(val, name); }); 205 | }); 206 | }, 207 | // keep-alive的渲染函数 208 | render: function render () { 209 | // 拿到keep-alive下插槽的值 210 | var slot = this.$slots.default; 211 | // 第一个vnode节点 212 | var vnode = getFirstComponentChild(slot); 213 | // 拿到第一个组件实例 214 | var componentOptions = vnode && vnode.componentOptions; 215 | // keep-alive的第一个子组件实例存在 216 | if (componentOptions) { 217 | // check pattern 218 | //拿到第一个vnode节点的name 219 | var name = getComponentName(componentOptions); 220 | var ref = this; 221 | var include = ref.include; 222 | var exclude = ref.exclude; 223 | // 通过判断子组件是否满足缓存匹配 224 | if ( 225 | // not included 226 | (include && (!name || !matches(include, name))) || 227 | // excluded 228 | (exclude && name && matches(exclude, name)) 229 | ) { 230 | return vnode 231 | } 232 | 233 | var ref$1 = this; 234 | var cache = ref$1.cache; 235 | var keys = ref$1.keys; 236 | var key = vnode.key == null 237 | ? componentOptions.Ctor.cid + (componentOptions.tag ? ("::" + (componentOptions.tag)) : '') 238 | : vnode.key; 239 | // 再次命中缓存 240 | if (cache[key]) { 241 | vnode.componentInstance = cache[key].componentInstance; 242 | // make current key freshest 243 | remove(keys, key); 244 | keys.push(key); 245 | } else { 246 | // 初次渲染时,将vnode缓存 247 | cache[key] = vnode; 248 | keys.push(key); 249 | // prune oldest entry 250 | if (this.max && keys.length > parseInt(this.max)) { 251 | pruneCacheEntry(cache, keys[0], keys, this._vnode); 252 | } 253 | } 254 | // 为缓存组件打上标志 255 | vnode.data.keepAlive = true; 256 | } 257 | // 将渲染的vnode返回 258 | return vnode || (slot && slot[0]) 259 | } 260 | }; 261 | ``` 262 | `keep-alive`选项跟我们平时写的组件选项还是基本类似的,唯一的不同是```keep-ailve```组件没有用```template```而是使用```render```函数。```keep-alive```本质上只是存缓存和拿缓存的过程,并没有实际的节点渲染,所以使用```render```处理是最优的选择。 263 | 264 | ### 13.3.3 缓存vnode 265 | 266 | 还是先回到流程图的分析。上面说到```keep-alive```在执行组件实例化之后会进行组件的挂载。而挂载```$mount```又回到```vm._render(),vm._update()```的过程。由于```keep-alive```拥有```render```函数,所以我们可以直接将焦点放在```render```函数的实现上。 267 | 268 | - 首先是获取```keep-alive```下插槽的内容,也就是```keep-alive```需要渲染的子组件,例子中是```chil1 Vnode```对象,源码中对应```getFirstComponentChild```函数。 269 | 270 | ```js 271 | function getFirstComponentChild (children) { 272 | if (Array.isArray(children)) { 273 | for (var i = 0; i < children.length; i++) { 274 | var c = children[i]; 275 | // 组件实例存在,则返回,理论上返回第一个组件vnode 276 | if (isDef(c) && (isDef(c.componentOptions) || isAsyncPlaceholder(c))) { 277 | return c 278 | } 279 | } 280 | } 281 | } 282 | ``` 283 | 284 | - 判断组件满足缓存的匹配条件,在```keep-alive```组件的使用过程中,```Vue```源码允许我们是用```include, exclude```来定义匹配条件,```include```规定了只有名称匹配的组件才会被缓存,```exclude```规定了任何名称匹配的组件都不会被缓存。更者,我们可以使用```max```来限制可以缓存多少匹配实例,而为什么要做数量的限制呢?我们后文会提到。 285 | 286 | 拿到子组件的实例后,我们需要先进行是否满足匹配条件的判断,**其中匹配的规则允许使用数组,字符串,正则的形式。** 287 | 288 | ```js 289 | var include = ref.include; 290 | var exclude = ref.exclude; 291 | // 通过判断子组件是否满足缓存匹配 292 | if ( 293 | // not included 294 | (include && (!name || !matches(include, name))) || 295 | // excluded 296 | (exclude && name && matches(exclude, name)) 297 | ) { 298 | return vnode 299 | } 300 | 301 | // matches 302 | function matches (pattern, name) { 303 | // 允许使用数组['child1', 'child2'] 304 | if (Array.isArray(pattern)) { 305 | return pattern.indexOf(name) > -1 306 | } else if (typeof pattern === 'string') { 307 | // 允许使用字符串 child1,child2 308 | return pattern.split(',').indexOf(name) > -1 309 | } else if (isRegExp(pattern)) { 310 | // 允许使用正则 /^child{1,2}$/g 311 | return pattern.test(name) 312 | } 313 | /* istanbul ignore next */ 314 | return false 315 | } 316 | ``` 317 | 318 | 如果组件不满足缓存的要求,则直接返回组件的```vnode```,不做任何处理,此时组件会进入正常的挂载环节。 319 | 320 | 3. `render`函数执行的关键一步是缓存```vnode```,由于是第一次执行```render```函数,选项中的```cache```和```keys```数据都没有值,其中```cache```是一个空对象,我们将用它来缓存```{ name: vnode }```枚举,而```keys```我们用来缓存组件名。 321 | **因此我们在第一次渲染```keep-alive```时,会将需要渲染的子组件```vnode```进行缓存。** 322 | ```js 323 | cache[key] = vnode; 324 | keys.push(key); 325 | ``` 326 | 327 | 4. 将已经缓存的```vnode```打上标记, 并将子组件的```Vnode```返回。 328 | ```vnode.data.keepAlive = true``` 329 | 330 | 331 | ### 13.3.4 真实节点的保存 332 | 333 | 我们再回到```createComponent```的逻辑,之前提到```createComponent```会先执行```keep-alive```组件的初始化流程,也包括了子组件的挂载。并且我们通过```componentInstance```拿到了```keep-alive```组件的实例,而接下来**重要的一步是将真实的```dom```保存再```vnode```中**。 334 | 335 | ```js 336 | function createComponent(vnode, insertedVnodeQueue) { 337 | ··· 338 | if (isDef(vnode.componentInstance)) { 339 | // 其中一个作用是保留真实dom到vnode中 340 | initComponent(vnode, insertedVnodeQueue); 341 | // 将真实节点添加到父节点中 342 | insert(parentElm, vnode.elm, refElm); 343 | if (isTrue(isReactivated)) { 344 | reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm); 345 | } 346 | return true 347 | } 348 | } 349 | ``` 350 | `insert`的源码不列举出来,它只是简单的调用操作```dom```的```api```,将子节点插入到父节点中,我们可以重点看看```initComponent```关键步骤的逻辑。 351 | 352 | ```js 353 | function initComponent() { 354 | ··· 355 | // vnode保留真实节点 356 | vnode.elm = vnode.componentInstance.$el; 357 | ··· 358 | } 359 | ``` 360 | 361 | **因此,我们很清晰的回到之前遗留下来的问题,为什么```keep-alive```需要一个```max```来限制缓存组件的数量。原因就是```keep-alive```缓存的组件数据除了包括```vnode```这一描述对象外,还保留着真实的```dom```节点,而我们知道真实节点对象是庞大的,所以大量保留缓存组件是耗费性能的。因此我们需要严格控制缓存的组件数量,而在缓存策略上也需要做优化,这点我们在下一篇文章也继续提到。** 362 | 363 | 由于```isReactivated```为```false```,```reactivateComponent```函数也不会执行。至此```keep-alive```的初次渲染流程分析完毕。 364 | 365 | **如果忽略步骤的分析,只对初次渲染流程做一个总结:内置的```keep-alive```组件,让子组件在第一次渲染的时候将```vnode```和真实的```elm```进行了缓存。** 366 | 367 | 368 | 369 | ## 13.4 抽象组件 370 | 这一节的最后顺便提一下上文提到的抽象组件的概念。```Vue```提供的内置组件都有一个描述组件类型的选项,这个选项就是```{ astract: true }```,它表明了该组件是抽象组件。什么是抽象组件,为什么要有这一类型的区别呢?我觉得归根究底有两个方面的原因。 371 | 1. 抽象组件没有真实的节点,它在组件渲染阶段不会去解析渲染成真实的```dom```节点,而只是作为中间的数据过渡层处理,在```keep-alive```中是对组件缓存的处理。 372 | 2. 在我们介绍组件初始化的时候曾经说到父子组件会显式的建立一层关系,这层关系奠定了父子组件之间通信的基础。我们可以再次回顾一下```initLifecycle```的代码。 373 | 374 | ```js 375 | Vue.prototype._init = function() { 376 | ··· 377 | var vm = this; 378 | initLifecycle(vm) 379 | } 380 | 381 | function initLifecycle (vm) { 382 | var options = vm.$options; 383 | 384 | var parent = options.parent; 385 | if (parent && !options.abstract) { 386 | // 如果有abstract属性,一直往上层寻找,直到不是抽象组件 387 | while (parent.$options.abstract && parent.$parent) { 388 | parent = parent.$parent; 389 | } 390 | parent.$children.push(vm); 391 | } 392 | ··· 393 | } 394 | ``` 395 | 子组件在注册阶段会把父实例挂载到自身选项的```parent```属性上,在```initLifecycle```过程中,会反向拿到```parent```上的父组件```vnode```,并为其```$children```属性添加该子组件```vnode```,如果在反向找父组件的过程中,父组件拥有```abstract```属性,即可判定该组件为抽象组件,此时利用```parent```的链条往上寻找,直到组件不是抽象组件为止。```initLifecycle```的处理,让每个组件都能找到上层的父组件以及下层的子组件,使得组件之间形成一个紧密的关系树。 396 | 397 | ### 13.5 小结 398 | 这一节介绍了```Vue```内置组件中一个最重要的,也是最常用的组件```keep-alive```,在日常开发中,我们经常将```keep-alive```配合动态组件```is```使用,达到切换组件的同时,将旧的组件缓存。最终达到保留初始状态的目的。在第一次组件渲染时,```keep-alive```会将组件```Vnode```以及对应的真实节点进行缓存。而当再次渲染组件时,```keep-alive```是如何利用这些缓存的?源码又对缓存进行了优化?并且```keep-alive```组件的生命周期又包括哪些,这些疑问我们将在下一节一一展开。 399 | -------------------------------------------------------------------------------- /src/组件高级用法.md: -------------------------------------------------------------------------------- 1 | > 我们知道,组件是```Vue```体系的核心,熟练使用组件是掌握```Vue```进行开发的基础。上一节中,我们深入了解了```Vue```组件注册到使用渲染的完整流程。这一节我们会在上一节的基础上介绍组件的两个高级用法:异步组件和函数式组件。 2 | 3 | ## 6.1 异步组件 4 | ### 6.1.1 使用场景 5 | `Vue`作为单页面应用遇到最棘手的问题是首屏加载时间的问题,单页面应用会把页面脚本打包成一个文件,这个文件包含着所有业务和非业务的代码,而脚本文件过大也是造成首页渲染速度缓慢的原因。因此作为首屏性能优化的课题,最常用的处理方法是对文件的拆分和代码的分离。按需加载的概念也是在这个前提下引入的。我们往往会把一些非首屏的组件设计成异步组件,部分不影响初次视觉体验的组件也可以设计为异步组件。这个思想就是**按需加载**。通俗点理解,按需加载的思想让应用在需要使用某个组件时才去请求加载组件代码。我们借助```webpack```打包后的结果会更加直观。 6 | 7 | 8 | ![](./img/6.1.png) 9 | 10 | ![](./img/6.2.png) 11 | `webpack`遇到异步组件,会将其从主脚本中分离,减少脚本体积,加快首屏加载时间。当遇到场景需要使用该组件时,才会去加载组件脚本。 12 | 13 | 14 | ### 6.1.2 工厂函数 15 | 16 | `Vue`中允许用户通过工厂函数的形式定义组件,这个工厂函数会异步解析组件定义,组件需要渲染的时候才会触发该工厂函数,加载结果会进行缓存,以供下一次调用组件时使用。 17 | 具体使用: 18 | ```js 19 | // 全局注册: 20 | Vue.component('asyncComponent', function(resolve, reject) { 21 | require(['./test.vue'], resolve) 22 | }) 23 | // 局部注册: 24 | var vm = new Vue({ 25 | el: '#app', 26 | template: '
', 27 | components: { 28 | asyncComponent: (resolve, reject) => require(['./test.vue'], resolve), 29 | // 另外写法 30 | asyncComponent: () => import('./test.vue'), 31 | } 32 | }) 33 | ``` 34 | 35 | 36 | 37 | 38 | ### 6.1.3 流程分析 39 | 40 | 有了上一节组件注册的基础,我们来分析异步组件的实现逻辑。简单回忆一下上一节的流程,实例的挂载流程分为根据渲染函数创建```Vnode```和根据```Vnode```产生真实节点的过程。期间创建```Vnode```过程,如果遇到子的占位符节点会调用```creatComponent```,这里会为子组件做选项合并和钩子挂载的操作,并创建一个以```vue-component-```为标记的子```Vnode```,而异步组件的处理逻辑也是在这个阶段处理。 41 | 42 | ```js 43 | // 创建子组件过程 44 | function createComponent ( 45 | Ctor, // 子类构造器 46 | data, 47 | context, // vm实例 48 | children, // 子节点 49 | tag // 子组件占位符 50 | ) { 51 | ··· 52 | // 针对局部注册组件创建子类构造器 53 | if (isObject(Ctor)) { 54 | Ctor = baseCtor.extend(Ctor); 55 | } 56 | // 异步组件分支 57 | var asyncFactory; 58 | if (isUndef(Ctor.cid)) { 59 | // 异步工厂函数 60 | asyncFactory = Ctor; 61 | // 创建异步组件函数 62 | Ctor = resolveAsyncComponent(asyncFactory, baseCtor); 63 | if (Ctor === undefined) { 64 | return createAsyncPlaceholder( 65 | asyncFactory, 66 | data, 67 | context, 68 | children, 69 | tag 70 | ) 71 | } 72 | } 73 | ··· 74 | // 创建子组件vnode 75 | var vnode = new VNode( 76 | ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')), 77 | data, undefined, undefined, undefined, context, 78 | { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children }, 79 | asyncFactory 80 | ); 81 | 82 | return vnode 83 | } 84 | ``` 85 | **工厂函数的用法使得```Vue.component(name, options)```的第二个参数不是一个对象,因此不论是全局注册还是局部注册,都不会执行```Vue.extend```生成一个子组件的构造器,**所以```Ctor.cid```不会存在,代码会进入异步组件的分支。 86 | 87 | 异步组件分支的核心是```resolveAsyncComponent```,它的处理逻辑分支众多,我们先关心工厂函数处理部分。 88 | ```js 89 | function resolveAsyncComponent ( 90 | factory, 91 | baseCtor 92 | ) { 93 | if (!isDef(factory.owners)) { 94 | 95 | // 异步请求成功处理 96 | var resolve = function() {} 97 | // 异步请求失败处理 98 | var reject = function() {} 99 | 100 | // 创建子组件时会先执行工厂函数,并将resolve和reject传入 101 | var res = factory(resolve, reject); 102 | 103 | // resolved 同步返回 104 | return factory.loading 105 | ? factory.loadingComp 106 | : factory.resolved 107 | } 108 | } 109 | ``` 110 | 如果经常使用```promise```进行开发,我们很容易发现,这部分代码像极了```promsie```原理内部的实现,针对异步组件工厂函数的写法,大致可以总结出以下三个步骤: 111 | 1. 定义异步请求成功的函数处理,定义异步请求失败的函数处理; 112 | 2. 执行组件定义的工厂函数; 113 | 3. 同步返回请求成功的函数处理。 114 | 115 | `resolve, reject`的实现,都是```once```方法执行的结果,所以我们先关注一下高级函数```once```的原理。**为了防止当多个地方调用异步组件时,```resolve,reject```不会重复执行,```once```函数保证了函数在代码只执行一次。也就是说,```once```缓存了已经请求过的异步组件** 116 | 117 | ```js 118 | // once函数保证了这个调用函数只在系统中调用一次 119 | function once (fn) { 120 | // 利用闭包特性将called作为标志位 121 | var called = false; 122 | return function () { 123 | // 调用过则不再调用 124 | if (!called) { 125 | called = true; 126 | fn.apply(this, arguments); 127 | } 128 | } 129 | } 130 | ``` 131 | 132 | 成功```resolve```和失败```reject```的详细处理逻辑如下: 133 | ```js 134 | // 成功处理 135 | var resolve = once(function (res) { 136 | // 转成组件构造器,并将其缓存到resolved属性中。 137 | factory.resolved = ensureCtor(res, baseCtor); 138 | if (!sync) { 139 | //强制更新渲染视图 140 | forceRender(true); 141 | } else { 142 | owners.length = 0; 143 | } 144 | }); 145 | // 失败处理 146 | var reject = once(function (reason) { 147 | warn( 148 | "Failed to resolve async component: " + (String(factory)) + 149 | (reason ? ("\nReason: " + reason) : '') 150 | ); 151 | if (isDef(factory.errorComp)) { 152 | factory.error = true; 153 | forceRender(true); 154 | } 155 | }); 156 | ``` 157 | 异步组件加载完毕,会调用```resolve```定义的方法,方法会通过```ensureCtor```将加载完成的组件转换为组件构造器,并存储在```resolved```属性中,其中 ```ensureCtor```的定义为: 158 | ```js 159 | function ensureCtor (comp, base) { 160 | if (comp.__esModule ||(hasSymbol && comp[Symbol.toStringTag] === 'Module')) { 161 | comp = comp.default; 162 | } 163 | // comp结果为对象时,调用extend方法创建一个子类构造器 164 | return isObject(comp) 165 | ? base.extend(comp) 166 | : comp 167 | } 168 | ``` 169 | 组件构造器创建完毕,会进行一次视图的重新渲染,**由于```Vue```是数据驱动视图渲染的,而组件在加载到完毕的过程中,并没有数据发生变化,因此需要手动强制更新视图。**```forceRender```函数的内部会拿到每个调用异步组件的实例,执行原型上的```$forceUpdate```方法,这部分的知识等到响应式系统时介绍。 170 | 171 | 异步组件加载失败后,会调用```reject```定义的方法,方法会提示并标记错误,最后同样会强制更新视图。 172 | 173 | 174 | 回到异步组件创建的流程,执行异步过程会同步为加载中的异步组件创建一个注释节点```Vnode``` 175 | ```js 176 | function createComponent (){ 177 | ··· 178 | // 创建异步组件函数 179 | Ctor = resolveAsyncComponent(asyncFactory, baseCtor); 180 | if (Ctor === undefined) { 181 | // 创建注释节点 182 | return createAsyncPlaceholder(asyncFactory,data,context,children,tag) 183 | } 184 | } 185 | ``` 186 | `createAsyncPlaceholder`的定义也很简单,其中```createEmptyVNode```之前有介绍过,是创建一个注释节点```vnode```,而```asyncFactory,asyncMeta```都是用来标注该节点为异步组件的临时节点和相关属性。 187 | ```js 188 | // 创建注释Vnode 189 | function createAsyncPlaceholder (factory,data,context,children,tag) { 190 | var node = createEmptyVNode(); 191 | node.asyncFactory = factory; 192 | node.asyncMeta = { data: data, context: context, children: children, tag: tag }; 193 | return node 194 | } 195 | ``` 196 | 执行```forceRender```触发组件的重新渲染过程时,又会再次调用```resolveAsyncComponent```,这时返回值```Ctor```不再为 ```undefined```了,因此会正常走组件的```render,patch```过程。这时,旧的注释节点也会被取代。 197 | 198 | ### 6.1.4 Promise异步组件 199 | 异步组件的第二种写法是在工厂函数中返回一个```promise```对象,我们知道```import```是```es6```引入模块加载的用法,但是```import```是一个静态加载的方法,它会优先模块内的其他语句执行。因此引入了```import()```,```import()```是一个运行时加载模块的方法,可以用来类比```require()```方法,区别在于前者是一个异步方法,后者是同步的,且```import()```会返回一个```promise```对象。 200 | 201 | 202 | 具体用法: 203 | ```js 204 | Vue.component('asyncComponent', () => import('./test.vue')) 205 | ``` 206 | 源码依然走着异步组件处理分支,并且大部分的处理过程还是工厂函数的逻辑处理,区别在于执行异步函数后会返回一个```promise```对象,成功加载则执行```resolve```,失败加载则执行```reject```. 207 | ```js 208 | var res = factory(resolve, reject); 209 | // res是返回的promise 210 | if (isObject(res)) { 211 | if (isPromise(res)) { 212 | if (isUndef(factory.resolved)) { 213 | // 核心处理 214 | res.then(resolve, reject); 215 | } 216 | } 217 | } 218 | ``` 219 | 其中```promise```对象的判断最简单的是判断是否有```then```和```catch```方法: 220 | ```js 221 | // 判断promise对象的方法 222 | function isPromise (val) { 223 | return (isDef(val) && typeof val.then === 'function' && typeof val.catch === 'function') 224 | } 225 | ``` 226 | 227 | ### 6.1.5 高级异步组件 228 | 为了在操作上更加灵活,比如使用```loading```组件处理组件加载时间过长的等待问题,使用```error```组件处理加载组件失败的错误提示等,```Vue```在2.3.0+版本新增了返回对象形式的异步组件格式,对象中可以定义需要加载的组件```component```,加载中显示的组件```loading```,加载失败的组件```error```,以及各种延时超时设置,源码同样进入异步组件分支。 229 | ```js 230 | Vue.component('asyncComponent', () => ({ 231 | // 需要加载的组件 (应该是一个 `Promise` 对象) 232 | component: import('./MyComponent.vue'), 233 | // 异步组件加载时使用的组件 234 | loading: LoadingComponent, 235 | // 加载失败时使用的组件 236 | error: ErrorComponent, 237 | // 展示加载时组件的延时时间。默认值是 200 (毫秒) 238 | delay: 200, 239 | // 如果提供了超时时间且组件加载也超时了, 240 | // 则使用加载失败时使用的组件。默认值是:`Infinity` 241 | timeout: 3000 242 | })) 243 | ``` 244 | 异步组件函数执行后返回一个对象,并且对象的```component```执行会返回一个```promise```对象,因此进入高级异步组件处理分支。 245 | ```js 246 | if (isObject(res)) { 247 | if (isPromise(res)) {} 248 | // 返回对象,且res.component返回一个promise对象,进入分支 249 | // 高级异步组件处理分支 250 | else if (isPromise(res.component)) { 251 | // 和promise异步组件处理方式相同 252 | res.component.then(resolve, reject); 253 | ··· 254 | } 255 | } 256 | ``` 257 | 异步组件会等待响应成功失败的结果,与此同时,代码继续同步执行。高级选项设置中如果设置了```error```和```loading```组件,会同时创建两个子类的构造器, 258 | ```js 259 | if (isDef(res.error)) { 260 | // 异步错误时组件的处理,创建错误组件的子类构造器,并赋值给errorComp 261 | factory.errorComp = ensureCtor(res.error, baseCtor); 262 | } 263 | 264 | if (isDef(res.loading)) { 265 | // 异步加载时组件的处理,创建错误组件的子类构造器,并赋值给errorComp 266 | factory.loadingComp = ensureCtor(res.loading, baseCtor); 267 | } 268 | ``` 269 | 如果存在```delay```属性,则通过```settimeout```设置```loading```组件显示的延迟时间。```factory.loading```属性用来标注是否是显示```loading```组件。 270 | ```js 271 | if (res.delay === 0) { 272 | factory.loading = true; 273 | } else { 274 | // 超过时间会成功加载,则执行失败结果 275 | setTimeout(function () { 276 | if (isUndef(factory.resolved) && isUndef(factory.error)) { 277 | factory.loading = true; 278 | forceRender(false); 279 | } 280 | }, res.delay || 200); 281 | } 282 | ``` 283 | 如果在```timeout```时间内,异步组件还未执行```resolve```的成功结果,即```resolve```没有赋值,则进行```reject```失败处理。 284 | 285 | 接下来依然是渲染注释节点或者渲染```loading```组件,等待异步处理结果,根据处理结果重新渲染视图节点,相似过程不再阐述。 286 | 287 | ### 6.1.6 wepack异步组件用法 288 | `webpack`作为```Vue```应用构建工具的标配,我们需要知道```Vue```如何结合```webpack ```进行异步组件的代码分离,并且需要关注分离后的文件名,这个名字在```webpack```中称为```chunkName```。```webpack```为异步组件的加载提供了两种写法。 289 | - `require.ensure`:它是```webpack```传统提供给异步组件的写法,在编译时,```webpack```会静态地解析代码中的 ```require.ensure()```,同时将模块添加到一个分开的 ```chunk``` 中,其中函数的第三个参数为分离代码块的名字。修改后的代码写法如下: 290 | 291 | ```js 292 | Vue.component('asyncComponent', function (resolve, reject) { 293 | require.ensure([], function () { 294 | resolve(require('./test.vue')); 295 | }, 'asyncComponent'); // asyncComponent为chunkname 296 | }) 297 | ``` 298 | 299 | - `import(/* webpackChunkName: "asyncComponent" */, component)`: 有了```es6```,```import```的写法是现今官方最推荐的做法,其中通过注释```webpackChunkName```来指定分离后组件模块的命名。修改后的写法如下: 300 | 301 | ```js 302 | Vue.component('asyncComponent', () => import(/* webpackChunkName: "asyncComponent" */, './test.vue')) 303 | ``` 304 | 305 | 至此,我们已经掌握了所有异步组件的写法,并深入了解了其内部的实现细节。我相信全面的掌握异步组件对今后单页面性能优化方面会起到积极的指导作用。 306 | 307 | ## 6.2 函数式组件 308 | `Vue`提供了一种可以让组件变为无状态、无实例的函数化组件。从原理上说,一般子组件都会经过实例化的过程,而单纯的函数组件并没有这个过程,它可以简单理解为一个中间层,只处理数据,不创建实例,也是由于这个行为,它的渲染开销会低很多。实际的应用场景是,当我们需要在多个组件中选择一个来代为渲染,或者在将```children,props,data```等数据传递给子组件前进行数据处理时,我们都可以用函数式组件来完成,它本质上也是对组件的一个外部包装。 309 | 310 | ### 6.2.1 使用场景 311 | 312 | - 定义两个组件对象,```test1,test2``` 313 | ```js 314 | var test1 = { 315 | props: ['msg'], 316 | render: function (createElement, context) { 317 | return createElement('h1', this.msg) 318 | } 319 | } 320 | var test2 = { 321 | props: ['msg'], 322 | render: function (createElement, context) { 323 | return createElement('h2', this.msg) 324 | } 325 | } 326 | ``` 327 | - 定义一个函数式组件,它会根据计算结果选择其中一个组件进行选项 328 | ```js 329 | Vue.component('test3', { 330 | // 函数式组件的标志 functional设置为true 331 | functional: true, 332 | props: ['msg'], 333 | render: function (createElement, context) { 334 | var get = function() { 335 | return test1 336 | } 337 | return createElement(get(), context) 338 | } 339 | }) 340 | ``` 341 | - 函数式组件的使用 342 | ```js 343 | 344 | 345 | new Vue({ 346 | el: '#app', 347 | data: { 348 | msg: 'test' 349 | } 350 | }) 351 | ``` 352 | - 最终渲染的结果为: 353 | ```js 354 |

test

355 | ``` 356 | 357 | ### 6.2.2 源码分析 358 | 函数式组件会在组件的对象定义中,将```functional```属性设置为```true```,这个属性是区别普通组件和函数式组件的关键。同样的在遇到子组件占位符时,会进入```createComponent```进行子组件```Vnode```的创建。**由于```functional```属性的存在,代码会进入函数式组件的分支中,并返回```createFunctionalComponent```调用的结果。**注意,执行完```createFunctionalComponent```后,后续创建子```Vnode```的逻辑不会执行,这也是之后在创建真实节点过程中不会有子```Vnode```去实例化子组件的原因。(无实例) 359 | ```js 360 | function createComponent(){ 361 | ··· 362 | if (isTrue(Ctor.options.functional)) { 363 | return createFunctionalComponent(Ctor, propsData, data, context, children) 364 | } 365 | } 366 | ``` 367 | `createFunctionalComponent`方法会对传入的数据进行检测和合并,实例化```FunctionalRenderContext```,最终调用函数式组件自定义的```render```方法执行渲染过程。 368 | ```js 369 | function createFunctionalComponent( 370 | Ctor, // 函数式组件构造器 371 | propsData, // 传入组件的props 372 | data, // 占位符组件传入的attr属性 373 | context, // vue实例 374 | children// 子节点 375 | ){ 376 | // 数据检测合并 377 | var options = Ctor.options; 378 | var props = {}; 379 | var propOptions = options.props; 380 | if (isDef(propOptions)) { 381 | for (var key in propOptions) { 382 | props[key] = validateProp(key, propOptions, propsData || emptyObject); 383 | } 384 | } else { 385 | // 合并attrs 386 | if (isDef(data.attrs)) { mergeProps(props, data.attrs); } 387 | // 合并props 388 | if (isDef(data.props)) { mergeProps(props, data.props); } 389 | } 390 | var renderContext = new FunctionalRenderContext(data,props,children,contextVm,Ctor); 391 | // 调用函数式组件中自定的render函数 392 | var vnode = options.render.call(null, renderContext._c, renderContext) 393 | } 394 | ``` 395 | 而```FunctionalRenderContext```这个类最终的目的是定义一个和真实组件渲染不同的```render```方法。 396 | ```js 397 | function FunctionalRenderContext() { 398 | // 省略其他逻辑 399 | this._c = function (a, b, c, d) { return createElement(contextVm, a, b, c, d, needNormalization); }; 400 | } 401 | ``` 402 | 执行```render```函数的过程,又会递归调用```createElement```的方法,这时的组件已经是真实的组件,开始执行正常的组件挂载流程。 403 | 404 | 问题:为什么函数式组件需要定义一个不同的```createElement```方法?- 函数式组件```createElement```和以往唯一的不同是,最后一个参数的不同,之前章节有说到,```createElement```会根据最后一个参数决定是否对子```Vnode```进行拍平,一般情况下,```children```编译生成结果都是```Vnode```类型,只有函数式组件比较特殊,它可以返回一个数组,这时候拍平就是有必要的。我们看下面的例子: 405 | ```js 406 | Vue.component('test', { 407 | functional: true, 408 | render: function (createElement, context) { 409 | return context.slots().default 410 | } 411 | }) 412 | 413 | 414 |

slot1

415 |

slot

416 |
417 | ``` 418 | 此时函数式组件```test```的```render```函数返回的是两个```slot```的```Vnode```,它是以数组的形式存在的,这就是需要拍平的场景。 419 | 420 | 简单总结一下函数式组件,从源码中可以看出,函数式组件并不会像普通组件那样有实例化组件的过程,因此包括组件的生命周期,组件的数据管理这些过程都没有,它只会原封不动的接收传递给组件的数据做处理,并渲染需要的内容。因此作为纯粹的函数可以也大大降低渲染的开销。 421 | 422 | 423 | ## 6.3 小结 424 | 这一小节在组件基础之上介绍了两个进阶的用法,异步组件和函数式组件。它们都是为了解决某些类型场景引入的高级组件用法。其中异步组件是首屏性能优化的一个解决方案,并且```Vue```提供了多达三种的使用方法,高级配置的用法更让异步组件的使用更加灵活。当然大部分情况下,我们会结合```webpack```进行使用。另外,函数式组件在多组件中选择渲染内容的场景作用非凡,由于是一个无实例的组件,它在渲染开销上比普通组件的性能更好。 425 | -------------------------------------------------------------------------------- /src/深入响应式系统构建-上.md: -------------------------------------------------------------------------------- 1 | > 从这一小节开始,正式进入```Vue```源码的核心,也是难点之一,响应式系统的构建。这一节将作为分析响应式构建过程源码的入门,主要分为两大块,第一块是针对响应式数据```props,methods,data,computed,wather```初始化过程的分析,另一块则是在保留源码设计理念的前提下,尝试手动构建一个基础的响应式系统。有了这两个基础内容的铺垫,下一篇进行源码具体细节的分析会更加得心应手。 2 | 3 | ## 7.1 数据初始化 4 | 回顾一下之前的内容,我们对```Vue```源码的分析是从初始化开始,初始化```_init```会执行一系列的过程,这个过程包括了配置选项的合并,数据的监测代理,最后才是实例的挂载。而在实例挂载前还有意忽略了一个重要的过程,**数据的初始化**(即```initState(vm)```)。```initState```的过程,是对数据进行响应式设计的过程,过程会针对```props,methods,data,computed```和```watch```做数据的初始化处理,并将他们转换为响应式对象,接下来我们会逐步分析每一个过程。 5 | ```js 6 | function initState (vm) { 7 | vm._watchers = []; 8 | var opts = vm.$options; 9 | // 初始化props 10 | if (opts.props) { initProps(vm, opts.props); } 11 | // 初始化methods 12 | if (opts.methods) { initMethods(vm, opts.methods); } 13 | // 初始化data 14 | if (opts.data) { 15 | initData(vm); 16 | } else { 17 | // 如果没有定义data,则创建一个空对象,并设置为响应式 18 | observe(vm._data = {}, true /* asRootData */); 19 | } 20 | // 初始化computed 21 | if (opts.computed) { initComputed(vm, opts.computed); } 22 | // 初始化watch 23 | if (opts.watch && opts.watch !== nativeWatch) { 24 | initWatch(vm, opts.watch); 25 | } 26 | } 27 | ``` 28 | 29 | ## 7.2 initProps 30 | 简单回顾一下```props```的用法,父组件通过属性的形式将数据传递给子组件,子组件通过```props```属性接收父组件传递的值。 31 | ```js 32 | // 父组件 33 | 34 | var vm = new Vue({ 35 | el: '#app', 36 | data() { 37 | return { 38 | test: 'child' 39 | } 40 | } 41 | }) 42 | // 子组件 43 | Vue.component('child', { 44 | template: '
{{test}}
', 45 | props: ['test'] 46 | }) 47 | ``` 48 | 因此分析```props```需要分析父组件和子组件的两个过程,我们先看父组件对传递值的处理。按照以往文章介绍的那样,父组件优先进行模板编译得到一个```render```函数,在解析过程中遇到子组件的属性,```:test=test```会被解析成```{ attrs: {test: test}}```并作为子组件的```render```函数存在,如下所示: 49 | ```js 50 | with(){..._c('child',{attrs:{"test":test}})} 51 | ``` 52 | `render`解析```Vnode```的过程遇到```child```这个子占位符节点,因此会进入创建子组件```Vnode```的过程,创建子```Vnode```过程是调用```createComponent```,这个阶段我们在组件章节有分析过,在组件的高级用法也有分析过,最终会调用```new Vnode```去创建子```Vnode```。而对于```props```的处理,```extractPropsFromVNodeData```会对```attrs```属性进行规范校验后,最后会把校验后的结果以```propsData```属性的形式传入```Vnode```构造器中。总结来说,```props```传递给占位符组件的写法,会以```propsData```的形式作为子组件```Vnode```的属性存在。下面会分析具体的细节。 53 | 54 | 55 | ```js 56 | // 创建子组件过程 57 | function createComponent() { 58 | // props校验 59 | var propsData = extractPropsFromVNodeData(data, Ctor, tag); 60 | ··· 61 | // 创建子组件vnode 62 | var vnode = new VNode( 63 | ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')), 64 | data, undefined, undefined, undefined, context, 65 | { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children }, 66 | asyncFactory 67 | ); 68 | } 69 | ``` 70 | 71 | ### 7.2.1 props的命名规范 72 | 73 | 先看检测```props```规范性的过程。**```props```编译后的结果有两种,其中```attrs```前面分析过,是编译生成```render```函数针对属性的处理,而```props```是针对用户自写```render```函数的属性值。**因此需要同时对这两种方式进行校验。 74 | ```js 75 | function extractPropsFromVNodeData (data,Ctor,tag) { 76 | // Ctor为子类构造器 77 | ··· 78 | var res = {}; 79 | // 子组件props选项 80 | var propOptions = Ctor.options.props; 81 | // data.attrs针对编译生成的render函数,data.props针对用户自定义的render函数 82 | var attrs = data.attrs; 83 | var props = data.props; 84 | if (isDef(attrs) || isDef(props)) { 85 | for (var key in propOptions) { 86 | // aB 形式转成 a-b 87 | var altKey = hyphenate(key); 88 | { 89 | var keyInLowerCase = key.toLowerCase(); 90 | if ( 91 | key !== keyInLowerCase && 92 | attrs && hasOwn(attrs, keyInLowerCase) 93 | ) { 94 | // 警告 95 | } 96 | } 97 | } 98 | } 99 | } 100 | ``` 101 | 重点说一下源码在这一部分的处理,**HTML对大小写是不敏感的,所有的浏览器会把大写字符解释为小写字符,因此我们在使用```DOM```中的模板时,cameCase(驼峰命名法)的```props```名需要使用其等价的 ```kebab-case``` (短横线分隔命名) 命代替**。 102 | **即: ``````需要写成``````** 103 | 104 | ### 7.2.2 响应式数据props 105 | 刚才说到分析```props```需要两个过程,前面已经针对父组件对```props```的处理做了描述,而对于子组件而言,我们是通过```props```选项去接收父组件传递的值。我们再看看子组件对```props```的处理: 106 | 107 | 108 | 子组件处理```props```的过程,是发生在父组件```_update```阶段,这个阶段是```Vnode```生成真实节点的过程,期间会遇到子```Vnode```,这时会调用```createComponent```去实例化子组件。而实例化子组件的过程又回到了```_init```初始化,此时又会经历选项的合并,针对```props```选项,最终会统一成```{props: { test: { type: null }}}```的写法。接着会调用```initProps```, ```initProps```做的事情,简单概括一句话就是,将组件的```props```数据设置为响应式数据。 109 | 110 | ```js 111 | function initProps (vm, propsOptions) { 112 | var propsData = vm.$options.propsData || {}; 113 | var loop = function(key) { 114 | ··· 115 | defineReactive(props,key,value,cb); 116 | if (!(key in vm)) { 117 | proxy(vm, "_props", key); 118 | } 119 | } 120 | // 遍历props,执行loop设置为响应式数据。 121 | for (var key in propsOptions) loop( key ); 122 | } 123 | ``` 124 | 其中```proxy(vm, "_props", key);```为```props```做了一层代理,用户通过```vm.XXX```可以代理访问到```vm._props```上的值。针对```defineReactive```,本质上是利用```Object.defineProperty```对数据的```getter,setter```方法进行重写,具体的原理可以参考数据代理章节的内容,在这小节后半段也会有一个基本的实现。 125 | 126 | 127 | ## 7.3 initMethods 128 | `initMethod`方法和这一节介绍的响应式没有任何的关系,他的实现也相对简单,主要是保证```methods```方法定义必须是函数,且命名不能和```props```重复,最终会将定义的方法都挂载到根实例上。 129 | ```js 130 | function initMethods (vm, methods) { 131 | var props = vm.$options.props; 132 | for (var key in methods) { 133 | { 134 | // method必须为函数形式 135 | if (typeof methods[key] !== 'function') { 136 | warn( 137 | "Method \"" + key + "\" has type \"" + (typeof methods[key]) + "\" in the component definition. " + 138 | "Did you reference the function correctly?", 139 | vm 140 | ); 141 | } 142 | // methods方法名不能和props重复 143 | if (props && hasOwn(props, key)) { 144 | warn( 145 | ("Method \"" + key + "\" has already been defined as a prop."), 146 | vm 147 | ); 148 | } 149 | // 不能以_ or $.这些Vue保留标志开头 150 | if ((key in vm) && isReserved(key)) { 151 | warn( 152 | "Method \"" + key + "\" conflicts with an existing Vue instance method. " + 153 | "Avoid defining component methods that start with _ or $." 154 | ); 155 | } 156 | } 157 | // 直接挂载到实例的属性上,可以通过vm[method]访问。 158 | vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm); 159 | } 160 | } 161 | ``` 162 | 163 | 164 | ## 7.4 initData 165 | `data`在初始化选项合并时会生成一个函数,只有在执行函数时才会返回真正的数据,所以```initData```方法会先执行拿到组件的```data```数据,并且会对对象每个属性的命名进行校验,保证不能和```props,methods```重复。最后的核心方法是```observe```,```observe```方法是将**数据对象标记为响应式对象**,并对对象的每个属性进行响应式处理。与此同时,和```props```的代理处理方式一样,```proxy```会对```data```做一层代理,直接通过```vm.XXX```可以代理访问到```vm._data```上挂载的对象属性。 166 | 167 | ```js 168 | function initData(vm) { 169 | var data = vm.$options.data; 170 | // 根实例时,data是一个对象,子组件的data是一个函数,其中getData会调用函数返回data对象 171 | data = vm._data = typeof data === 'function'? getData(data, vm): data || {}; 172 | var keys = Object.keys(data); 173 | var props = vm.$options.props; 174 | var methods = vm.$options.methods; 175 | var i = keys.length; 176 | while (i--) { 177 | var key = keys[i]; 178 | { 179 | // 命名不能和方法重复 180 | if (methods && hasOwn(methods, key)) { 181 | warn(("Method \"" + key + "\" has already been defined as a data property."),vm); 182 | } 183 | } 184 | // 命名不能和props重复 185 | if (props && hasOwn(props, key)) { 186 | warn("The data property \"" + key + "\" is already declared as a prop. " + "Use prop default value instead.",vm); 187 | } else if (!isReserved(key)) { 188 | // 数据代理,用户可直接通过vm实例返回data数据 189 | proxy(vm, "_data", key); 190 | } 191 | } 192 | // observe data 193 | observe(data, true /* asRootData */); 194 | } 195 | ``` 196 | **最后讲讲```observe```,```observe```具体的行为是将数据对象添加一个不可枚举的属性```__ob__```,标志对象是一个响应式对象,并且拿到每个对象的属性值,重写```getter,setter```方法,使得每个属性值都是响应式数据。详细的代码我们后面分析。** 197 | 198 | 199 | ## 7.5 initComputed 200 | 和上面的分析方法一样,```initComputed```是```computed```数据的初始化,不同之处在于以下几点: 201 | 1. `computed`可以是对象,也可以是函数,但是对象必须有```getter```方法,因此如果```computed```中的属性值是对象时需要进行验证。 202 | 2. 针对```computed```的每个属性,要创建一个监听的依赖,也就是实例化一个```watcher```,```watcher```的定义,可以暂时理解为数据使用的依赖本身,一个```watcher```实例代表多了一个需要被监听的数据依赖。 203 | 204 | 除了不同点,```initComputed```也会将每个属性设置成响应式的数据,同样的,也会对```computed```的命名做检测,防止与```props,data```冲突。 205 | 206 | ```js 207 | function initComputed (vm, computed) { 208 | ··· 209 | for (var key in computed) { 210 | var userDef = computed[key]; 211 | var getter = typeof userDef === 'function' ? userDef : userDef.get; 212 | // computed属性为对象时,要保证有getter方法 213 | if (getter == null) { 214 | warn(("Getter is missing for computed property \"" + key + "\"."),vm); 215 | } 216 | if (!isSSR) { 217 | // 创建computed watcher 218 | watchers[key] = new Watcher(vm,getter || noop,noop,computedWatcherOptions); 219 | } 220 | if (!(key in vm)) { 221 | // 设置为响应式数据 222 | defineComputed(vm, key, userDef); 223 | } else { 224 | // 不能和props,data命名冲突 225 | if (key in vm.$data) { 226 | warn(("The computed property \"" + key + "\" is already defined in data."), vm); 227 | } else if (vm.$options.props && key in vm.$options.props) { 228 | warn(("The computed property \"" + key + "\" is already defined as a prop."), vm); 229 | } 230 | } 231 | } 232 | } 233 | ``` 234 | 235 | 显然```Vue```提供了很多种数据供开发者使用,但是分析完后发现每个处理的核心都是将数据转化成响应式数据,有了响应式数据,如何构建一个响应式系统呢?前面提到的```watcher```又是什么东西?构建响应式系统还需要其他的东西吗?接下来我们尝试着去实现一个极简风的响应式系统。 236 | 237 | ## 7.6 极简风的响应式系统 238 | `Vue`的响应式系统构建是比较复杂的,直接进入源码分析构建的每一个流程会让理解变得困难,因此我觉得在尽可能保留源码的设计逻辑下,用最小的代码构建一个最基础的响应式系统是有必要的。对```Dep,Watcher,Observer```概念的初步认识,也有助于下一篇对响应式系统设计细节的分析。 239 | 240 | ### 7.6.1 框架搭建 241 | 我们以```MyVue```作为类响应式框架,框架的搭建不做赘述。我们模拟```Vue```源码的实现思路,实例化```MyVue```时会传递一个选项配置,精简的代码只有一个```id```挂载元素和一个数据对象```data```。模拟源码的思路,我们在实例化时会先进行数据的初始化,这一步就是响应式的构建,我们稍后分析。数据初始化后开始进行真实```DOM```的挂载。 242 | ```js 243 | var vm = new MyVue({ 244 | id: '#app', 245 | data: { 246 | test: 12 247 | } 248 | }) 249 | // myVue.js 250 | (function(global) { 251 | class MyVue { 252 | constructor(options) { 253 | this.options = options; 254 | // 数据的初始化 255 | this.initData(options); 256 | let el = this.options.id; 257 | // 实例的挂载 258 | this.$mount(el); 259 | } 260 | initData(options) { 261 | } 262 | $mount(el) { 263 | } 264 | } 265 | }(window)) 266 | ``` 267 | ### 7.6.2 设置响应式对象 - Observer 268 | 首先引入一个类```Observer```,这个类的目的是将数据变成响应式对象,利用```Object.defineProperty```对数据的```getter,setter```方法进行改写。在数据读取```getter```阶段我们会进行**依赖的收集**,在数据的修改```setter```阶段,我们会进行**依赖的更新**(这两个概念的介绍放在后面)。因此在数据初始化阶段,我们会利用```Observer```这个类将数据对象修改为相应式对象,而这是所有流程的基础。 269 | ```js 270 | class MyVue { 271 | initData(options) { 272 | if(!options.data) return; 273 | this.data = options.data; 274 | // 将数据重置getter,setter方法 275 | new Observer(options.data); 276 | } 277 | } 278 | // Observer类的定义 279 | class Observer { 280 | constructor(data) { 281 | // 实例化时执行walk方法对每个数据属性重写getter,setter方法 282 | this.walk(data) 283 | } 284 | 285 | walk(obj) { 286 | const keys = Object.keys(obj); 287 | for(let i = 0;i< keys.length; i++) { 288 | // Object.defineProperty的处理逻辑 289 | defineReactive(obj, keys[i]) 290 | } 291 | } 292 | } 293 | ``` 294 | ### 7.6.3 依赖本身 - Watcher 295 | 我们可以这样理解,一个```Watcher```实例就是一个依赖,数据不管是在渲染模板时使用还是在用户计算时使用,都可以算做一个需要监听的依赖,```watcher```中记录着这个依赖监听的状态,以及如何更新操作的方法。 296 | ```js 297 | // 监听的依赖 298 | class Watcher { 299 | constructor(expOrFn, isRenderWatcher) { 300 | this.getter = expOrFn; 301 | // Watcher.prototype.get的调用会进行状态的更新。 302 | this.get(); 303 | } 304 | 305 | get() {} 306 | } 307 | ``` 308 | 那么哪个时间点会实例化```watcher```并更新数据状态呢?显然在渲染数据到真实```DOM```时可以创建```watcher```。```$mount```流程前面章节介绍过,会经历模板生成```render```函数和```render```函数渲染真实```DOM```的过程。我们对代码做了精简,```updateView```浓缩了这一过程。 309 | ```js 310 | class MyVue { 311 | $mount(el) { 312 | // 直接改写innerHTML 313 | const updateView = _ => { 314 | let innerHtml = document.querySelector(el).innerHTML; 315 | let key = innerHtml.match(/{(\w+)}/)[1]; 316 | document.querySelector(el).innerHTML = this.options.data[key] 317 | } 318 | // 创建一个渲染的依赖。 319 | new Watcher(updateView, true) 320 | } 321 | } 322 | ``` 323 | ### 7.6.4 依赖管理 - Dep 324 | `watcher`如果理解为每个数据需要监听的依赖,那么```Dep``` 可以理解为对依赖的一种管理。数据可以在渲染中使用,也可以在计算属性中使用。相应的每个数据对应的```watcher```也有很多。而我们在更新数据时,如何通知到数据相关的每一个依赖,这就需要```Dep```进行通知管理了。并且浏览器同一时间只能更新一个```watcher```,所以也需要一个属性去记录当前更新的```watcher```。而```Dep```这个类只需要做两件事情,将依赖进行收集,派发依赖进行更新。 325 | ```js 326 | let uid = 0; 327 | class Dep { 328 | constructor() { 329 | this.id = uid++; 330 | this.subs = [] 331 | } 332 | // 依赖收集 333 | depend() { 334 | if(Dep.target) { 335 | // Dep.target是当前的watcher,将当前的依赖推到subs中 336 | this.subs.push(Dep.target) 337 | } 338 | } 339 | // 派发更新 340 | notify() { 341 | const subs = this.subs.slice(); 342 | for (var i = 0, l = subs.length; i < l; i++) { 343 | // 遍历dep中的依赖,对每个依赖执行更新操作 344 | subs[i].update(); 345 | } 346 | } 347 | } 348 | 349 | Dep.target = null; 350 | ``` 351 | 352 | ### 7.6.5 依赖管理过程 - defineReactive 353 | 我们看看数据拦截的过程。前面的```Observer```实例化最终会调用```defineReactive```重写```getter,setter```方法。这个方法开始会实例化一个```Dep```,也就是创建一个数据的依赖管理。在重写的```getter```方法中会进行依赖的收集,也就是调用```dep.depend```的方法。在```setter```阶段,比较两个数不同后,会调用依赖的派发更新。即```dep.notify``` 354 | ```js 355 | const defineReactive = (obj, key) => { 356 | const dep = new Dep(); 357 | const property = Object.getOwnPropertyDescriptor(obj); 358 | let val = obj[key] 359 | if(property && property.configurable === false) return; 360 | Object.defineProperty(obj, key, { 361 | configurable: true, 362 | enumerable: true, 363 | get() { 364 | // 做依赖的收集 365 | if(Dep.target) { 366 | dep.depend() 367 | } 368 | return val 369 | }, 370 | set(nval) { 371 | if(nval === val) return 372 | // 派发更新 373 | val = nval 374 | dep.notify(); 375 | } 376 | }) 377 | } 378 | ``` 379 | 回过头来看```watcher```,实例化```watcher```时会将```Dep.target```设置为当前的```watcher```,执行完状态更新函数之后,再将```Dep.target```置空。这样在收集依赖时只要将```Dep.target```当前的```watcher push```到```Dep```的```subs```数组即可。而在派发更新阶段也只需要重新更新状态即可。 380 | 381 | ```js 382 | class Watcher { 383 | constructor(expOrFn, isRenderWatcher) { 384 | this.getter = expOrFn; 385 | // Watcher.prototype.get的调用会进行状态的更新。 386 | this.get(); 387 | } 388 | 389 | get() { 390 | // 当前执行的watcher 391 | Dep.target = this 392 | this.getter() 393 | Dep.target = null; 394 | } 395 | update() { 396 | this.get() 397 | } 398 | } 399 | ``` 400 | ### 7.6.6 结果 401 | 一个极简的响应式系统搭建完成。在精简代码的同时,保持了源码设计的思想和逻辑。有了这一步的基础,接下来深入分析源码中每个环节的实现细节会更加简单。 402 | 403 | 404 | ## 7.7 小结 405 | 这一节内容,我们正式进入响应式系统的介绍,前面在数据代理章节,我们学过```Object.defineProperty```,这是一个用来进行数据拦截的方法,而响应式系统构建的基础就是数据的拦截。我们先介绍了```Vue```内部在初始化数据的过程,最终得出的结论是,不管是```data,computed```,还是其他的用户定义数据,最终都是调用```Object.defineProperty```进行数据拦截。而文章的最后,我们在保留源码设计思想和逻辑的前提下,构建出了一个简化版的响应式系统。完整的功能有助于我们下一节对源码具体实现细节的分析和思考。 -------------------------------------------------------------------------------- /src/组件基础剖析.md: -------------------------------------------------------------------------------- 1 | > 组件是```Vue```的一个重要核心,我们在进行项目工程化时,会将页面的结构组件化。组件化意味着独立和共享,而两个结论并不矛盾,独立的组件开发可以让开发者专注于某个功能项的开发和扩展,而组件的设计理念又使得功能项更加具有复用性,不同的页面可以进行组件功能的共享。对于开发者而言,编写```Vue```组件是掌握```Vue```开发的核心基础,```Vue```官网也花了大量的篇幅介绍了组件的体系和各种使用方法。这一节内容,我们会深入```Vue```组件内部的源码,了解**组件注册的实现思路,并结合上一节介绍的实例挂载分析组件渲染挂载的基本流程,最后我们将分析组件和组件之间是如何建立联系的**。我相信,掌握这些底层的实现思路对于我们今后在解决```vue```组件相关问题上会有明显的帮助。 2 | 3 | ## 5.1 组件两种注册方式 4 | 熟悉```Vue```开发流程的都知道,```Vue```组件在使用之前需要进行注册,而注册的方式有两种,全局注册和局部注册。在进入源码分析之前,我们先回忆一下两者的用法,以便后续掌握两者的差异。 5 | 6 | ### 5.1.1 全局注册 7 | ```js 8 | Vue.component('my-test', { 9 | template: '
{{test}}
', 10 | data () { 11 | return { 12 | test: 1212 13 | } 14 | } 15 | }) 16 | var vm = new Vue({ 17 | el: '#app', 18 | template: '
' 19 | }) 20 | ``` 21 | **其中组件的全局注册需要在全局实例化Vue前调用**,注册之后可以用在任何新创建的```Vue```实例中调用。 22 | ### 5.1.2 局部注册 23 | ```js 24 | var myTest = { 25 | template: '
{{test}}
', 26 | data () { 27 | return { 28 | test: 1212 29 | } 30 | } 31 | } 32 | var vm = new Vue({ 33 | el: '#app', 34 | component: { 35 | myTest 36 | } 37 | }) 38 | ``` 39 | 当只需要在某个局部用到某个组件时,可以使用局部注册的方式进行组件注册,此时局部注册的组件只能在注册该组件内部使用。 40 | 41 | ### 5.1.3 注册过程 42 | 在简单回顾组件的两种注册方式后,我们来看注册过程到底发生了什么,我们以全局组件注册为例。它通过```Vue.component(name, {...})```进行组件注册,```Vue.component```是在```Vue```源码引入阶段定义的静态方法。 43 | ```js 44 | // 初始化全局api 45 | initAssetRegisters(Vue); 46 | var ASSET_TYPES = [ 47 | 'component', 48 | 'directive', 49 | 'filter' 50 | ]; 51 | function initAssetRegisters(Vue){ 52 | // 定义ASSET_TYPES中每个属性的方法,其中包括component 53 | ASSET_TYPES.forEach(function (type) { 54 | // type: component,directive,filter 55 | Vue[type] = function (id,definition) { 56 | if (!definition) { 57 | // 直接返回注册组件的构造函数 58 | return this.options[type + 's'][id] 59 | } 60 | ... 61 | if (type === 'component') { 62 | // 验证component组件名字是否合法 63 | validateComponentName(id); 64 | } 65 | if (type === 'component' && isPlainObject(definition)) { 66 | // 组件名称设置 67 | definition.name = definition.name || id; 68 | // Vue.extend() 创建子组件,返回子类构造器 69 | definition = this.options._base.extend(definition); 70 | } 71 | // 为Vue.options 上的component属性添加将子类构造器 72 | this.options[type + 's'][id] = definition; 73 | return definition 74 | } 75 | }); 76 | } 77 | ``` 78 | 79 | `Vue.components`有两个参数,一个是需要注册组件的组件名,另一个是组件选项,如果第二个参数没有传递,则会直接返回注册过的组件选项。否则意味着需要对该组件进行注册,注册过程先会对组件名的合法性进行检测,要求组件名不允许出现非法的标签,包括```Vue```内置的组件名,如```slot, component```等。 80 | ```js 81 | function validateComponentName(name) { 82 | if (!new RegExp(("^[a-zA-Z][\\-\\.0-9_" + (unicodeRegExp.source) + "]*$")).test(name)) { 83 | // 正则判断检测是否为非法的标签 84 | warn( 85 | 'Invalid component name: "' + name + '". Component names ' + 86 | 'should conform to valid custom element name in html5 specification.' 87 | ); 88 | } 89 | // 不能使用Vue自身自定义的组件名,如slot, component,不能使用html的保留标签,如 h1, svg等 90 | if (isBuiltInTag(name) || config.isReservedTag(name)) { 91 | warn( 92 | 'Do not use built-in or reserved HTML elements as component ' + 93 | 'id: ' + name 94 | ); 95 | } 96 | } 97 | ``` 98 | 在经过组件名的合法性检测后,会调用```extend```方法为组件创建一个子类构造器,此时的```this.options._base```代表的就是```Vue```构造器。```extend```方法的定义在介绍选项合并章节有重点介绍过,它会**基于父类去创建一个子类**,此时的父类是```Vue```,并且创建过程子类会继承父类的方法,并会和父类的选项进行合并,最终返回一个子类构造器。 99 | 100 | 代码处还有一个逻辑,```Vue.component()```默认会把第一个参数作为组件名称,但是如果组件选项有```name```属性时,```name```属性值会将组件名覆盖。 101 | 102 | 103 | **总结起来,全局注册组件就是```Vue```实例化前创建一个基于```Vue```的子类构造器,并将组件的信息加载到实例```options.components```对象中。** 104 | 105 | 106 | **接下来自然而然会想到一个问题,局部注册和全局注册在实现上的区别体现在哪里?**我们不急着分析局部组件的注册流程,先以全局注册的组件为基础,看看作为组件,它的挂载流程有什么不同。 107 | 108 | 109 | ## 5.2 组件Vnode创建 110 | 上一节内容我们介绍了```Vue```如何将一个模板,通过```render```函数的转换,最终生成一个```Vnode tree```的,在不包含组件的情况下,```_render```函数的最后一步是直接调用```new Vnode```去创建一个完整的```Vnode tree```。然而有一大部分的分支我们并没有分析,那就是遇到组件占位符的场景。执行阶段如果遇到组件,处理过程要比想像中复杂得多,我们通过一张流程图展开分析。 111 | 112 | ### 5.2.1 Vnode创建流程图 113 | 114 | ![](./img/5.1.png) 115 | 116 | ### 5.2.2 具体流程分析 117 | 我们结合实际的例子对照着流程图分析一下这个过程: 118 | 119 | - 场景 120 | ```js 121 | Vue.component('test', { 122 | template: '' 123 | }) 124 | var vm = new Vue({ 125 | el: '#app', 126 | template: '
' 127 | }) 128 | ``` 129 | - 父```render```函数 130 | ```js 131 | function() { 132 | with(this){return _c('div',[_c('test')],1)} 133 | } 134 | ``` 135 | 136 | 137 | - `Vue`根实例初始化会执行 ```vm.$mount(vm.$options.el)```实例挂载的过程,按照之前的逻辑,完整流程会经历```render```函数生成```Vnode```,以及```Vnode```生成真实```DOM```的过程。 138 | - `render`函数生成```Vnode```过程中,子会优先父执行生成```Vnode```过程,也就是```_c('test')```函数会先被执行。```'test'```会先判断是普通的```html```标签还是组件的占位符。 139 | - 如果为一般标签,会执行```new Vnode```过程,这也是上一章节我们分析的过程;如果是组件的占位符,则会在判断组件已经被注册过的前提下进入```createComponent```创建子组件```Vnode```的过程。 140 | - `createComponent`是创建组件```Vnode```的过程,创建过程会再次合并选项配置,并安装组件相关的内部钩子(后面文章会再次提到内部钩子的作用),最后通过```new Vnode()```生成以```vue-component```开头的```Virtual DOM``` 141 | - `render`函数执行过程也是一个循环递归调用创建```Vnode```的过程,执行3,4步之后,完整的生成了一个包含各个子组件的```Vnode tree``` 142 | 143 | 144 | `_createElement`函数的实现之前章节分析过一部分,我们重点看看组件相关的操作。 145 | 146 | ```js 147 | // 内部执行将render函数转化为Vnode的函数 148 | function _createElement(context,tag,data,children,normalizationType) { 149 | ··· 150 | if (typeof tag === 'string') { 151 | // 子节点的标签为普通的html标签,直接创建Vnode 152 | if (config.isReservedTag(tag)) { 153 | vnode = new VNode( 154 | config.parsePlatformTagName(tag), data, children, 155 | undefined, undefined, context 156 | ); 157 | // 子节点标签为注册过的组件标签名,则子组件Vnode的创建过程 158 | } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) { 159 | // 创建子组件Vnode 160 | vnode = createComponent(Ctor, data, context, children, tag); 161 | } 162 | } 163 | } 164 | ``` 165 | `config.isReservedTag(tag)`用来判断标签是否为普通的```html```标签,如果是普通节点会直接创建```Vnode```节点,如果不是,则需要判断这个占位符组件是否已经注册到,我们可以通过```context.$options.components[组件名]```拿到注册后的组件选项。如何判断组件是否已经全局注册,看看```resolveAsset```的实现。 166 | 167 | ```js 168 | // 需要明确组件是否已经被注册 169 | function resolveAsset (options,type,id,warnMissing) { 170 | // 标签为字符串 171 | if (typeof id !== 'string') { 172 | return 173 | } 174 | // 这里是 options.component 175 | var assets = options[type]; 176 | // 这里的分支分别支持大小写,驼峰的命名规范 177 | if (hasOwn(assets, id)) { return assets[id] } 178 | var camelizedId = camelize(id); 179 | if (hasOwn(assets, camelizedId)) { return assets[camelizedId] } 180 | var PascalCaseId = capitalize(camelizedId); 181 | if (hasOwn(assets, PascalCaseId)) { return assets[PascalCaseId] } 182 | // fallback to prototype chain 183 | var res = assets[id] || assets[camelizedId] || assets[PascalCaseId]; 184 | if (warnMissing && !res) { 185 | warn( 186 | 'Failed to resolve ' + type.slice(0, -1) + ': ' + id, 187 | options 188 | ); 189 | } 190 | // 最终返回子类的构造器 191 | return res 192 | } 193 | ``` 194 | 195 | 拿到注册过的子类构造器后,调用```createComponent```方法创建子组件```Vnode``` 196 | 197 | ```js 198 | // 创建子组件过程 199 | function createComponent ( 200 | Ctor, // 子类构造器 201 | data, 202 | context, // vm实例 203 | children, // 子节点 204 | tag // 子组件占位符 205 | ) { 206 | ··· 207 | // Vue.options里的_base属性存储Vue构造器 208 | var baseCtor = context.$options._base; 209 | 210 | // 针对局部组件注册场景 211 | if (isObject(Ctor)) { 212 | Ctor = baseCtor.extend(Ctor); 213 | } 214 | data = data || {}; 215 | // 构造器配置合并 216 | resolveConstructorOptions(Ctor); 217 | // 挂载组件钩子 218 | installComponentHooks(data); 219 | 220 | // return a placeholder vnode 221 | var name = Ctor.options.name || tag; 222 | // 创建子组件vnode,名称以 vue-component- 开头 223 | var vnode = new VNode(("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),data, undefined, undefined, undefined, context,{ Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },asyncFactory); 224 | 225 | return vnode 226 | } 227 | ``` 228 | 这里将大部分的代码都拿掉了,只留下创建```Vnode```相关的代码,最终会通过```new Vue```实例化一个名称以```vue-component-```开头的```Vnode```节点。其中两个关键的步骤是配置合并和安装组件钩子函数,选项合并的内容可以查看这个系列的前两节,这里看看```installComponentHooks```安装组件钩子函数时做了哪些操作。 229 | ```js 230 | // 组件内部自带钩子 231 | var componentVNodeHooks = { 232 | init: function init (vnode, hydrating) { 233 | }, 234 | prepatch: function prepatch (oldVnode, vnode) { 235 | }, 236 | insert: function insert (vnode) { 237 | }, 238 | destroy: function destroy (vnode) { 239 | } 240 | }; 241 | var hooksToMerge = Object.keys(componentVNodeHooks); 242 | // 将componentVNodeHooks 钩子函数合并到组件data.hook中 243 | function installComponentHooks (data) { 244 | var hooks = data.hook || (data.hook = {}); 245 | for (var i = 0; i < hooksToMerge.length; i++) { 246 | var key = hooksToMerge[i]; 247 | var existing = hooks[key]; 248 | var toMerge = componentVNodeHooks[key]; 249 | // 如果钩子函数存在,则执行mergeHook$1方法合并 250 | if (existing !== toMerge && !(existing && existing._merged)) { 251 | hooks[key] = existing ? mergeHook$1(toMerge, existing) : toMerge; 252 | } 253 | } 254 | } 255 | function mergeHook$1 (f1, f2) { 256 | // 返回一个依次执行f1,f2的函数 257 | var merged = function (a, b) { 258 | f1(a, b); 259 | f2(a, b); 260 | }; 261 | merged._merged = true; 262 | return merged 263 | } 264 | ``` 265 | 组件默认自带的这几个钩子函数会在后续```patch```过程的不同阶段执行,这部分内容不在本节的讨论范围。 266 | 267 | 268 | ### 5.2.3 局部注册和全局注册的区别 269 | 在说到全局注册和局部注册的用法时留下了一个问题,局部注册和全局注册两者的区别在哪里。其实局部注册的原理同样简单,我们使用局部注册组件时会通过在父组件选项配置中的```components```添加子组件的对象配置,这和全局注册后在```Vue```的```options.component```添加子组件构造器的结果很相似。区别在于: 270 | 271 | **1.局部注册添加的对象配置是在某个组件下,而全局注册添加的子组件是在根实例下。** 272 | 273 | **2.局部注册添加的是一个子组件的配置对象,而全局注册添加的是一个子类构造器。** 274 | 275 | 因此局部注册中缺少了一步构建子类构造器的过程,这个过程放在哪里进行呢? 回到```createComponent```的源码,源码中根据选项是对象还是函数来区分局部和全局注册组件,**如果选项的值是对象,则该组件是局部注册的组件,此时在创建子```Vnode```时会调用 父类的```extend```方法去创建一个子类构造器。** 276 | ```js 277 | function createComponent (...) { 278 | ... 279 | var baseCtor = context.$options._base; 280 | 281 | // 针对局部组件注册场景 282 | if (isObject(Ctor)) { 283 | Ctor = baseCtor.extend(Ctor); 284 | } 285 | } 286 | 287 | ``` 288 | 289 | ## 5.3 组件Vnode渲染真实DOM 290 | 根据前面的分析,不管是全局注册的组件还是局部注册的组件,组件并没有进行实例化,那么组件实例化的过程发生在哪个阶段呢?我们接着看```Vnode tree```渲染真实```DOM```的过程。 291 | 292 | ### 5.3.1 真实节点渲染流程图 293 | 294 | ![](./img/5.2.png) 295 | 296 | 297 | ### 5.3.2 具体流程分析 298 | 1. 经过```vm._render()```生成完整的```Virtual Dom```树后,紧接着执行```Vnode```渲染真实```DOM```的过程,这个过程是```vm.update()```方法的执行,而其核心是```vm.__patch__```。 299 | 2. ```vm.__patch__```内部会通过 ```createElm```去创建真实的```DOM```元素,期间遇到子```Vnode```会递归调用```createElm```方法。 300 | 3. 递归调用过程中,判断该节点类型是否为组件类型是通过```createComponent```方法判断的,该方法和渲染```Vnode```阶段的方法```createComponent```不同,他会调用子组件的```init```初始化钩子函数,并完成组件的```DOM```插入。 301 | 4. ```init```初始化钩子函数的核心是```new```实例化这个子组件并将子组件进行挂载,实例化子组件的过程又回到合并配置,初始化生命周期,初始化事件中心,初始化渲染的过程。实例挂载又会执行```$mount```过程。 302 | 5. 完成所有子组件的实例化和节点挂载后,最后才回到根节点的挂载。 303 | 304 | 305 | `__patch__`核心代码是通过```createElm```创建真实节点,当创建过程中遇到子```vnode```时,会调用```createChildren```,```createChildren```的目的是对子```vnode```递归调用```createElm```创建子组件节点。 306 | ```js 307 | // 创建真实dom 308 | function createElm (vnode,insertedVnodeQueue,parentElm,refElm,nested,ownerArray,index) { 309 | ··· 310 | // 递归创建子组件真实节点,直到完成所有子组件的渲染才进行根节点的真实节点插入 311 | if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) { 312 | return 313 | } 314 | ··· 315 | var children = vnode.children; 316 | // 317 | createChildren(vnode, children, insertedVnodeQueue); 318 | ··· 319 | insert(parentElm, vnode.elm, refElm); 320 | } 321 | function createChildren(vnode, children, insertedVnodeQueue) { 322 | for (var i = 0; i < children.length; ++i) { 323 | // 遍历子节点,递归调用创建真实dom节点的方法 - createElm 324 | createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i); 325 | } 326 | } 327 | ``` 328 | `createComponent`方法会对子组件```Vnode```进行处理中,还记得在```Vnode```生成阶段为子```Vnode```安装了一系列的钩子函数吗,在这个步骤我们可以通过是否拥有这些定义好的钩子来判断是否是已经注册过的子组件,如果条件满足,则执行组件的```init```钩子。 329 | 330 | `init`钩子做的事情只有两个,**实例化组件构造器,执行子组件的挂载流程。**(```keep-alive```分支看具体的文章分析) 331 | 332 | ```js 333 | function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) { 334 | var i = vnode.data; 335 | // 是否有钩子函数可以作为判断是否为组件的唯一条件 336 | if (isDef(i = i.hook) && isDef(i = i.init)) { 337 | // 执行init钩子函数 338 | i(vnode, false /* hydrating */); 339 | } 340 | ··· 341 | } 342 | var componentVNodeHooks = { 343 | // 忽略keepAlive过程 344 | // 实例化 345 | var child = vnode.componentInstance = createComponentInstanceForVnode(vnode,activeInstance); 346 | // 挂载 347 | child.$mount(hydrating ? vnode.elm : undefined, hydrating); 348 | } 349 | function createComponentInstanceForVnode(vnode, parent) { 350 | ··· 351 | // 实例化Vue子组件实例 352 | return new vnode.componentOptions.Ctor(options) 353 | } 354 | 355 | ``` 356 | 显然```Vnode```生成真实```DOM```的过程也是一个不断递归创建子节点的过程,```patch```过程如果遇到子```Vnode```,会优先实例化子组件,并且执行子组件的挂载流程,而挂载流程又会回到```_render,_update```的过程。在所有的子```Vnode```递归挂载后,最终才会真正挂载根节点。 357 | 358 | ## 5.4 建立组件联系 359 | 360 | 日常开发中,我们可以通过```vm.$parent```拿到父实例,也可以在父实例中通过```vm.$children```拿到实例中的子组件。显然,```Vue```在组件和组件之间建立了一层关联。接下来的内容,我们将探索如何建立组件之间的联系。 361 | 362 | 不管是父实例还是子实例,在初始化实例阶段有一个```initLifecycle```的过程。这个过程会**把当前实例添加到父实例的```$children```属性中,并设置自身的```$parent```属性指向父实例。**举一个具体的应用场景: 363 | ```js 364 |
365 | 366 |
367 | Vue.component('component-a', { 368 | template: '
a
' 369 | }) 370 | var vm = new Vue({ el: '#app'}) 371 | console.log(vm) // 将实例对象输出 372 | ``` 373 | 由于```vue```实例向上没有父实例,所以```vm.$parent```为```undefined```,```vm```的```$children```属性指向子组件```componentA``` 的实例。 374 | 375 | ![](./img/5.3.png) 376 | 377 | 子组件```componentA```的 ```$parent```属性指向它的父级```vm```实例,它的```$children```属性指向为空 378 | 379 | ![](./img/5.4.png) 380 | 381 | 382 | 源码解析如下: 383 | ```js 384 | function initLifecycle (vm) { 385 | var options = vm.$options; 386 | // 子组件注册时,会把父组件的实例挂载到自身选项的parent上 387 | var parent = options.parent; 388 | // 如果是子组件,并且该组件不是抽象组件时,将该组件的实例添加到父组件的$parent属性上,如果父组件是抽象组件,则一直往上层寻找,直到该父级组件不是抽象组件,并将,将该组件的实例添加到父组件的$parent属性 389 | if (parent && !options.abstract) { 390 | while (parent.$options.abstract && parent.$parent) { 391 | parent = parent.$parent; 392 | } 393 | parent.$children.push(vm); 394 | } 395 | // 将自身的$parent属性指向父实例。 396 | vm.$parent = parent; 397 | vm.$root = parent ? parent.$root : vm; 398 | 399 | vm.$children = []; 400 | vm.$refs = {}; 401 | 402 | vm._watcher = null; 403 | vm._inactive = null; 404 | vm._directInactive = false; 405 | // 该实例是否挂载 406 | vm._isMounted = false; 407 | // 该实例是否被销毁 408 | vm._isDestroyed = false; 409 | // 该实例是否正在被销毁 410 | vm._isBeingDestroyed = false; 411 | } 412 | 413 | ``` 414 | 最后简单讲讲抽象组件,在```vue```中有很多内置的抽象组件,例如```,```等,这些抽象组件并不会出现在子父级的路径上,并且它们也不会参与```DOM```的渲染。 415 | 416 | 417 | ## 5.5 小结 418 | 这一小节,结合了实际的例子分析了组件注册流程到组件挂载渲染流程,```Vue```中我们可以定义全局的组件,也可以定义局部的组件,全局组件需要进行全局注册,核心方法是```Vue.component```,他需要在根组件实例化前进行声明注册,原因是我们需要在实例化前拿到组件的配置信息并合并到```options.components```选项中。注册的本质是调用```extend```创建一个子类构造器,全局和局部的不同是局部创建子类构造器是发生在创建子组件```Vnode```阶段。而创建子```Vnode```阶段最关键的一步是定义了很多内部使用的钩子。有了一个完整的```Vnode tree```接下来会进入真正```DOM```的生成,在这个阶段如果遇到子组件```Vnode```会进行子构造器的实例化,并完成子组件的挂载。递归完成子组件的挂载后,最终才又回到根组件的挂载。 419 | 有了组件的基本知识,下一节我们重点分析一下组件的进阶用法。 420 | -------------------------------------------------------------------------------- /src/实例挂载流程和模板编译.md: -------------------------------------------------------------------------------- 1 | >前面几节我们从```new Vue```创建实例开始,介绍了创建实例时执行初始化流程中的重要两步,配置选项的资源合并,以及响应式系统的核心思想,数据代理。在合并章节,我们对```Vue```丰富的选项合并策略有了基本的认知,在数据代理章节我们又对代理拦截的意义和使用场景有了深入的认识。按照```Vue```源码的设计思路,初始化过程还会进行很多操作,例如组件之间创建关联,初始化事件中心,初始化数据并建立响应式系统等,并最终将模板和数据渲染成为```dom```节点。如果直接按流程的先后顺序分析每个步骤的实现细节,会有很多概念很难理解。因此在这一章节,我们先重点分析一个概念,**实例的挂载渲染流程。** 2 | 3 | ## 3.1 Runtime Only VS Runtime + Compiler 4 | 在正文开始之前,我们先了解一下```vue```基于源码构建的两个版本,一个是```runtime only```(一个只包含运行时的版本),另一个是```runtime + compiler```(一个同时包含编译器和运行时的版本)。而两个版本的区别仅在于后者包含了一个编译器。 5 | 6 | 什么是编译器,百度百科这样解释道: 7 | 8 | >简单讲,编译器就是将“一种语言(通常为高级语言)”翻译为“另一种语言(通常为低级语言)”的程序。一个现代编译器的主要工作流程:源代码 (source code) → 预处理器 (preprocessor) → 编译器 (compiler) → 目标代码 (object code) → 链接器 (Linker) → 可执行程序 (executables)。 9 | 10 | 通俗点讲,编译器是一个提供了将**源代码**转化为**目标代码**的工具。从```Vue```的角度出发,内置的编译器实现了将```template```模板转换编译为可执行```javascript```脚本的功能。 11 | 12 | 13 | ### 3.1.1 Runtime + Compiler 14 | 一个完整的```Vue```版本是包含编译器的,我们可以使用```template```进行模板编写。编译器会自动将模板字符串编译成渲染函数的代码,源码中就是```render```函数。 15 | 如果你需要在客户端编译模板 (比如传入一个字符串给 ```template``` 选项,或挂载到一个元素上并以其 ```DOM``` 内部的 HTML 作为模板),就需要一个包含编译器的版本。 16 | ```js 17 | // 需要编译器的版本 18 | new Vue({ 19 | template: '
{{ hi }}
' 20 | }) 21 | ``` 22 | 23 | 24 | ### 3.1.2 Runtime Only 25 | 只包含运行时的代码拥有创建```Vue```实例、渲染并处理```Virtual DOM```等功能,基本上就是除去编译器外的完整代码。```Runtime Only```的适用场景有两种: 26 | 1.我们在选项中通过手写```render```函数去定义渲染过程,这个时候并不需要包含编译器的版本便可完整执行。 27 | 28 | ```js 29 | // 不需要编译器 30 | new Vue({ 31 | render (h) { 32 | return h('div', this.hi) 33 | } 34 | }) 35 | ``` 36 | 2.借助```vue-loader```这样的编译工具进行编译,当我们利用```webpack```进行```Vue```的工程化开发时,常常会利用```vue-loader```对```.vue```进行编译,尽管我们也是利用```template```模板标签去书写代码,但是此时的```Vue```已经不需要利用编译器去负责模板的编译工作了,这个过程交给了插件去实现。 37 | 38 | 39 | 很明显,编译过程对性能会造成一定的损耗,并且由于加入了编译的流程代码,```Vue```代码的总体积也更加庞大(运行时版本相比完整版体积要小大约 30%)。因此在实际开发中,我们需要借助像```webpack```的```vue-loader```这类工具进行编译,将```Vue```对模板的编译阶段合并到```webpack```的构建流程中,这样不仅减少了生产环境代码的体积,也大大提高了运行时的性能,一举两得。 40 | 41 | 42 | ## 3.2 实例挂载的基本思路 43 | 有了上面的基础,我们回头看初始化```_init```的代码,在代码中我们观察到```initProxy```后有一系列的函数调用,这些函数包括了创建组件关联,初始化事件处理,定义渲染函数,构建数据响应式系统等,最后还有一段代码,在```el```存在的情况下,实例会调用```$mount```进行实例挂载。 44 | ```js 45 | Vue.prototype._init = function (options) { 46 | ··· 47 | // 选项合并 48 | vm.$options = mergeOptions( 49 | resolveConstructorOptions(vm.constructor), 50 | options || {}, 51 | vm 52 | ); 53 | // 数据代理 54 | initProxy(vm); 55 | vm._self = vm; 56 | initLifecycle(vm); 57 | // 初始化事件处理 58 | initEvents(vm); 59 | // 定义渲染函数 60 | initRender(vm); 61 | // 构建响应式系统 62 | initState(vm); 63 | // 等等 64 | ··· 65 | if (vm.$options.el) { 66 | vm.$mount(vm.$options.el); 67 | } 68 | } 69 | ``` 70 | 以手写```template```模板为例,理清楚什么是挂载。**我们会在选项中传递```template```为属性的模板字符串,如```
{{message}}
```,最终这个模板字符串通过中间过程将其转成真实的```DOM```节点,并挂载到选项中```el```代表的根节点上完成视图渲染。这个中间过程就是接下来要分析的挂载流程。** 71 | 72 | 73 | `Vue`挂载的流程是比较复杂的,接下来我将通过**流程图,代码分析**两种方式为大家展示挂载的真实过程。 74 | 75 | ### 3.2.1 流程图 76 | 77 | ![](./img/3.1.png) 78 | 如果用一句话概括挂载的过程,可以描述为**确认挂载节点,编译模板为```render```函数,渲染函数转换```Virtual DOM```,创建真实节点。** 79 | 80 | ### 3.2.2 代码分析 81 | 接下来我们从代码的角度去剖析挂载的流程。挂载的代码较多,下面只提取骨架相关的部分代码。 82 | 83 | ```js 84 | // 内部真正实现挂载的方法 85 | Vue.prototype.$mount = function (el, hydrating) { 86 | el = el && inBrowser ? query(el) : undefined; 87 | // 调用mountComponent方法挂载 88 | return mountComponent(this, el, hydrating) 89 | }; 90 | // 缓存了原型上的 $mount 方法 91 | var mount = Vue.prototype.$mount; 92 | 93 | // 重新定义$mount,为包含编译器和不包含编译器的版本提供不同封装,最终调用的是缓存原型上的$mount方法 94 | Vue.prototype.$mount = function (el, hydrating) { 95 | // 获取挂载元素 96 | el = el && query(el); 97 | // 挂载元素不能为跟节点 98 | if (el === document.body || el === document.documentElement) { 99 | warn( 100 | "Do not mount Vue to or - mount to normal elements instead." 101 | ); 102 | return this 103 | } 104 | var options = this.$options; 105 | // 需要编译 or 不需要编译 106 | // render选项不存在,代表是template模板的形式,此时需要进行模板的编译过程 107 | if (!options.render) { 108 | ··· 109 | // 使用内部编译器编译模板 110 | } 111 | // 无论是template模板还是手写render函数最终调用缓存的$mount方法 112 | return mount.call(this, el, hydrating) 113 | } 114 | // mountComponent方法思路 115 | function mountComponent(vm, el, hydrating) { 116 | // 定义updateComponent方法,在watch回调时调用。 117 | updateComponent = function () { 118 | // render函数渲染成虚拟DOM, 虚拟DOM渲染成真实的DOM 119 | vm._update(vm._render(), hydrating); 120 | }; 121 | // 实例化渲染watcher 122 | new Watcher(vm, updateComponent, noop, {}) 123 | } 124 | 125 | ``` 126 | 127 | 我们用语言描述挂载流程的基本思路。 128 | 129 | - 确定挂载的```DOM```元素,这个```DOM```需要保证不能为```html,body```这类跟节点。 130 | - 我们知道渲染有两种方式,一种是通过```template```模板字符串,另一种是手写```render```函数,前面提到```template```模板需要运行时进行编译,而后一个可以直接用```render```选项作为渲染函数。因此挂载阶段会有两条分支,```template```模板会先经过模板的解析,最终编译成```render```渲染函数参与实例挂载,而手写```render```函数可以绕过编译阶段,直接调用挂载的```$mount```方法。 131 | - 针对```template```而言,它会利用```Vue```内部的编译器进行模板的编译,字符串模板会转换为抽象的语法树,即```AST```树,并最终转化为一个类似```function(){with(){}}```的渲染函数,这是我们后面讨论的重点。 132 | - 无论是```template```模板还是手写```render```函数,最终都将进入```mountComponent```过程,这个阶段会实例化一个渲染```watcher```,具体```watcher```的内容,另外放章节讨论。我们先知道一个结论,渲染```watcher```的回调函数有两个执行时机,一个是在初始化时执行,另一个是当```vm```实例检测到数据发生变化时会再次执行回调函数。 133 | - 回调函数是执行```updateComponent```的过程,这个方法有两个阶段,一个是```vm._render```,另一个是```vm._update```。 ```vm._render```会执行前面生成的```render```渲染函数,并生成一个```Virtual Dom tree```,而```vm._update```会将这个```Virtual Dom tree```转化为真实的```DOM```节点。 134 | 135 | 136 | 137 | 138 | 139 | ## 3.3 模板编译 140 | 通过文章前半段的学习,我们对```Vue```的挂载流程有了一个初略的认识。这里有两个大的流程需要我们详细去理解,一个是```template```模板的编译,另一个是```updateComponent```的实现细节。```updateComponent```的过程,我们放到下一章节重点分析,而这一节剩余的内容我们将会围绕模板编译的设计思路展开。 141 | 142 | (编译器的实现细节是异常复杂的,要在短篇幅内将整个编译的过程掌握是不切实际的,并且从大方向上也不需要完全理清编译的流程。因此针对模板,文章分析只是浅尝即止,更多的细节读者可以自行分析) 143 | 144 | 145 | ## 3.3.1 template的三种写法 146 | `template`模板的编写有三种方式,分别是: 147 | 148 | - 字符串模板 149 | 150 | ```js 151 | var vm = new Vue({ 152 | el: '#app', 153 | template: '
模板字符串
' 154 | }) 155 | ``` 156 | - 选择符匹配元素的 ```innerHTML```模板 157 | 158 | ```js 159 |
160 |
test1
161 | 164 |
165 | var vm = new Vue({ 166 | el: '#app', 167 | template: '#test' 168 | }) 169 | ``` 170 | 171 | - `dom`元素匹配元素的```innerHTML```模板 172 | 173 | ```js 174 |
175 |
test1
176 |
test2
177 |
178 | var vm = new Vue({ 179 | el: '#app', 180 | template: document.querySelector('#test') 181 | }) 182 | 183 | ``` 184 | 模板编译的前提需要对```template```模板字符串的合法性进行检测,三种写法对应代码的三个不同分支。 185 | ```js 186 | Vue.prototype.$mount = function () { 187 | ··· 188 | if(!options.render) { 189 | var template = options.template; 190 | if (template) { 191 | // 针对字符串模板和选择符匹配模板 192 | if (typeof template === 'string') { 193 | // 选择符匹配模板,以'#'为前缀的选择器 194 | if (template.charAt(0) === '#') { 195 | // 获取匹配元素的innerHTML 196 | template = idToTemplate(template); 197 | /* istanbul ignore if */ 198 | if (!template) { 199 | warn( 200 | ("Template element not found or is empty: " + (options.template)), 201 | this 202 | ); 203 | } 204 | } 205 | // 针对dom元素匹配 206 | } else if (template.nodeType) { 207 | // 获取匹配元素的innerHTML 208 | template = template.innerHTML; 209 | } else { 210 | // 其他类型则判定为非法传入 211 | { 212 | warn('invalid template option:' + template, this); 213 | } 214 | return this 215 | } 216 | } else if (el) { 217 | // 如果没有传入template模板,则默认以el元素所属的根节点作为基础模板 218 | template = getOuterHTML(el); 219 | } 220 | } 221 | } 222 | 223 | // 判断el元素是否存在 224 | function query (el) { 225 | if (typeof el === 'string') { 226 | var selected = document.querySelector(el); 227 | if (!selected) { 228 | warn( 229 | 'Cannot find element: ' + el 230 | ); 231 | return document.createElement('div') 232 | } 233 | return selected 234 | } else { 235 | return el 236 | } 237 | } 238 | var idToTemplate = cached(function (id) { 239 | var el = query(id); 240 | return el && el.innerHTML 241 | }); 242 | ``` 243 | **注意:其中X-Template模板的方式一般用于模板特别大的 demo 或极小型的应用,官方不建议在其他情形下使用,因为这会将模板和组件的其它定义分离开。** 244 | 245 | 246 | ## 3.3.2 编译流程图解 247 | `vue`源码中编译的设计思路是比较绕,涉及的函数处理逻辑比较多,实现流程中巧妙的运用了偏函数的技巧将配置项处理和编译核心逻辑抽取出来,为了理解这个设计思路,我画了一个逻辑图帮助理解。 248 | 249 | ![](./img/3.2.png) 250 | 251 | 252 | ## 3.3.3 逻辑解析 253 | 即便有流程图,编译逻辑理解起来依然比较晦涩,接下来,结合代码分析每个环节的执行过程。 254 | ```js 255 | Vue.prototype.$mount = function () { 256 | ··· 257 | if(!options.render) { 258 | var template = options.template; 259 | if (template) { 260 | var ref = compileToFunctions(template, { 261 | outputSourceRange: "development" !== 'production', 262 | shouldDecodeNewlines: shouldDecodeNewlines, 263 | shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref, 264 | delimiters: options.delimiters, 265 | comments: options.comments 266 | }, this); 267 | var render = ref.render; 268 | } 269 | ... 270 | } 271 | } 272 | ``` 273 | `compileToFunctions`有三个参数,一个是```template```模板,另一个是编译的配置信息,并且这个方法是对外暴露的编译方法,用户可以自定义配置信息进行模板的编译。最后一个参数是```Vue```实例。 274 | 275 | ```js 276 | // 将compileToFunction方法暴露给Vue作为静态方法存在 277 | Vue.compile = compileToFunctions; 278 | ``` 279 | 280 | 在```Vue```的官方文档中,```Vue.compile```只允许传递一个```template```模板参数,这是否意味着用户无法决定某些编译的行为?显然不是的,我们看回代码,有两个选项配置可以提供给用户,用户只需要在实例化```Vue```时传递选项改变配置,他们分别是: 281 | 282 | 1.`delimiters`: 该选项可以改变纯文本插入分隔符,当不传递值时,```Vue```默认的分隔符为 ```{{}}```。如果我们想使用其他模板,可以通过```delimiters```修改。 283 | 284 | 2.`comments` : 当设为 ```true``` 时,将会保留且渲染模板中的 ```HTML```注释。默认行为是舍弃它们。 285 | 286 | **注意,由于这两个选项是在完整版的编译流程读取的配置,所以在运行时版本配置这两个选项是无效的** 287 | 288 | 接着我们一步步寻找```compileToFunctions```的根源。 289 | 290 | 首先我们需要有一个认知,**不同平台对```Vue```的编译过程是不一样的,也就是说基础的编译方法会随着平台的不同有区别,编译阶段的配置选项也因为平台的不同呈现差异。但是设计者又不希望在相同平台下编译不同模板时,每次都要传入相同的配置选项。这才有了源码中较为复杂的编译实现。** 291 | 292 | ```js 293 | var createCompiler = createCompilerCreator(function baseCompile (template,options) { 294 | //把模板解析成抽象的语法树 295 | var ast = parse(template.trim(), options); 296 | // 配置中有代码优化选项则会对Ast语法树进行优化 297 | if (options.optimize !== false) { 298 | optimize(ast, options); 299 | } 300 | var code = generate(ast, options); 301 | return { 302 | ast: ast, 303 | render: code.render, 304 | staticRenderFns: code.staticRenderFns 305 | } 306 | }); 307 | 308 | var ref$1 = createCompiler(baseOptions); 309 | var compile = ref$1.compile; 310 | var compileToFunctions = ref$1.compileToFunctions; 311 | ``` 312 | 这部分代码是在```Vue```引入阶段定义的,```createCompilerCreator```在传递了一个```baseCompile```函数作为参数后,返回了一个编译器的生成器,也就是```createCompiler```,有了这个生成器,当将编译配置选项```baseOptions```传入后,这个编译器生成器便**生成了一个指定环境指定配置下的编译器**,而其中编译执行函数就是返回对象的```compileToFunctions```。 313 | 314 | 这里的```baseCompile```是真正执行编译功能的地方,也就是前面说到的特定平台的编译方法。它在源码初始化时就已经作为参数的形式保存在内存变量中。我们先看看```baseCompile```的大致流程。 315 | 316 | `baseCompile`函数的参数有两个,一个是后续传入的```template```模板,另一个是编译需要的配置参数。函数实现的功能如下几个: 317 | - 1.把模板解析成抽象的语法树,简称```AST```,代码中对应```parse```部分。 318 | - 2.可选:优化```AST```语法树,执行```optimize```方法。 319 | - 3.根据不同平台将```AST```语法树转换成渲染函数,对应的```generate```函数 320 | 321 | 322 | 接下来具体看看```createCompilerCreator```的实现: 323 | ```js 324 | function createCompilerCreator (baseCompile) { 325 | return function createCompiler (baseOptions) { 326 | // 内部定义compile方法 327 | function compile (template, options) { 328 | ··· 329 | } 330 | return { 331 | compile: compile, 332 | compileToFunctions: createCompileToFunctionFn(compile) 333 | } 334 | } 335 | } 336 | ``` 337 | `createCompilerCreator`函数只有一个作用,利用**偏函数**的思想将```baseCompile```这一基础的编译方法缓存,并返回一个编程器生成器,当执行```var ref$1 = createCompiler(baseOptions);```时,```createCompiler```会将内部定义的```compile```和```compileToFunctions```返回。 338 | 339 | 我们继续关注```compileToFunctions```的由来,它是```createCompileToFunctionFn```函数以```compile```为参数返回的方法,接着看```createCompileToFunctionFn```的实现逻辑。 340 | 341 | 342 | ```js 343 | function createCompileToFunctionFn (compile) { 344 | var cache = Object.create(null); 345 | 346 | return function compileToFunctions (template,options,vm) { 347 | options = extend({}, options); 348 | ··· 349 | // 缓存的作用:避免重复编译同个模板造成性能的浪费 350 | if (cache[key]) { 351 | return cache[key] 352 | } 353 | // 执行编译方法 354 | var compiled = compile(template, options); 355 | ··· 356 | // turn code into functions 357 | var res = {}; 358 | var fnGenErrors = []; 359 | // 编译出的函数体字符串作为参数传递给createFunction,返回最终的render函数 360 | res.render = createFunction(compiled.render, fnGenErrors); 361 | res.staticRenderFns = compiled.staticRenderFns.map(function (code) { 362 | return createFunction(code, fnGenErrors) 363 | }); 364 | ··· 365 | return (cache[key] = res) 366 | } 367 | } 368 | ``` 369 | 370 | `createCompileToFunctionFn`利用了闭包的概念,将编译过的模板进行缓存,```cache```会将之前编译过的结果保留下来,利用缓存可以避免重复编译引起的浪费性能。```createCompileToFunctionFn```最终会将```compileToFunctions```方法返回。 371 | 372 | 接下来,我们分析一下```compileToFunctions```的实现逻辑。在判断不使用缓存的编译结果后,```compileToFunctions```会执行```compile```方法,这个方法是前面分析```createCompiler```时,返回的内部```compile```方法,所以我们需要先看看```compile```的实现。 373 | 374 | ```js 375 | function createCompiler (baseOptions) { 376 | function compile (template, options) { 377 | var finalOptions = Object.create(baseOptions); 378 | var errors = []; 379 | var tips = []; 380 | var warn = function (msg, range, tip) { 381 | (tip ? tips : errors).push(msg); 382 | }; 383 | // 选项合并 384 | if (options) { 385 | ··· 386 | // 这里会将用户传递的配置和系统自带编译配置进行合并 387 | } 388 | 389 | finalOptions.warn = warn; 390 | // 将剔除空格后的模板以及合并选项后的配置作为参数传递给baseCompile方法 391 | var compiled = baseCompile(template.trim(), finalOptions); 392 | { 393 | detectErrors(compiled.ast, warn); 394 | } 395 | compiled.errors = errors; 396 | compiled.tips = tips; 397 | return compiled 398 | } 399 | return { 400 | compile: compile, 401 | compileToFunctions: createCompileToFunctionFn(compile) 402 | } 403 | } 404 | ``` 405 | 我们看到```compile```真正执行的方法,是一开始在创建编译器生成器时,传入的基础编译方法```baseCompile```,```baseCompile```真正执行的时候,会将用户传递的编译配置和系统自带的编译配置选项合并,这也是开头提到编译器设计思想的精髓。 406 | 407 | 执行完```compile```会返回一个对象,```ast```顾名思义是模板解析成的抽象语法树,```render```是最终生成的```with```语句,```staticRenderFns```是以数组形式存在的静态```render```。 408 | ```js 409 | { 410 | ast: ast, 411 | render: code.render, 412 | staticRenderFns: code.staticRenderFns 413 | } 414 | ``` 415 | 416 | 而```createCompileToFunctionFn```最终会返回另外两个包装过的属性```render, staticRenderFns```,他们的核心是**将 ```with```语句封装成执行函数。** 417 | 418 | ```js 419 | // 编译出的函数体字符串作为参数传递给createFunction,返回最终的render函数 420 | res.render = createFunction(compiled.render, fnGenErrors); 421 | res.staticRenderFns = compiled.staticRenderFns.map(function (code) { 422 | return createFunction(code, fnGenErrors) 423 | }); 424 | 425 | function createFunction (code, errors) { 426 | try { 427 | return new Function(code) 428 | } catch (err) { 429 | errors.push({ err: err, code: code }); 430 | return noop 431 | } 432 | } 433 | ``` 434 | 435 | 436 | 至此,```Vue```中关于编译器的设计思路也基本梳理清楚了,一开始看代码的时候,总觉得编译逻辑的设计特别的绕,分析完代码后发现,这正是作者思路巧妙的地方。```Vue```在不同平台上有不同的编译过程,而每个编译过程的```baseOptions```选项会有所不同,同时也提供了一些选项供用户去配置,整个设计思想深刻的应用了偏函数的设计思想,而偏函数又是闭包的应用。作者利用偏函数将不同平台的编译方式进行缓存,同时剥离出编译相关的选项合并,这些方式都是值得我们日常学习的。 437 | 438 | 编译的核心是```parse,generate```过程,这两个过程笔者并没有分析,原因是抽象语法树的解析分支较多,需要结合实际的代码场景才更好理解。这两部分的代码会在后面介绍到具体逻辑功能章节时再次提及。 439 | 440 | 441 | 442 | ## 3.4 小结 443 | 这一节的内容有两大块,首先详细的介绍了实例在挂载阶段的完整流程,当我们传入选项进行实例化时,最终的目的是将选项渲染成页面真实的可视节点。这个选项有两种形式,一个是以```template```模板字符串传入,另一个是手写```render```函数形式传入,不论哪种,最终会以```render```函数的形式参与挂载,```render```是一个用函数封装好的```with```语句。渲染真实节点前需要将```render```函数解析成虚拟```DOM```,虚拟```DOM```是```js```和真实```DOM```之间的桥梁。最终的```_update```过程让将虚拟```DOM```渲染成真实节点。第二个大块主要介绍了作者在编译器设计时巧妙的实现思路。过程大量运用了偏函数的概念,将编译过程进行缓存并且将选项合并从编译过程中剥离。这些设计理念、思想都是值得我们开发者学习和借鉴的。 -------------------------------------------------------------------------------- /src/完整渲染流程.md: -------------------------------------------------------------------------------- 1 | > 继上一节内容,我们将```Vue```复杂的挂载流程通过图解流程,代码分析的方式简单梳理了一遍,最后也讲到了模板编译的大致流程。然而在挂载的核心处,我们并没有分析模板编译后渲染函数是如何转换为可视化```DOM```节点的。因此这一章节,我们将重新回到```Vue```实例挂载的最后一个环节:渲染```DOM```节点。在渲染真实```DOM```的过程中,```Vue```引进了虚拟```DOM```的概念,这是```Vue```架构设计中另一个重要的理念。虚拟```DOM```作为```JS```对象和真实```DOM```中间的一个缓冲层,对```JS```频繁操作```DOM```的引起的性能问题有很好的缓解作用。 2 | 3 | ## 4.1 Virtual DOM 4 | 5 | ### 4.1.1 浏览器的渲染流程 6 | 当浏览器接收到一个```Html```文件时,```JS```引擎和浏览器的渲染引擎便开始工作了。从渲染引擎的角度,它首先会将```html```文件解析成一个```DOM```树,与此同时,浏览器将识别并加载```CSS```样式,并和```DOM```树一起合并为一个渲染树。有了渲染树后,渲染引擎将计算所有元素的位置信息,最后通过绘制,在屏幕上打印最终的内容。```JS```引擎和渲染引擎虽然是两个独立的线程,但是JS引擎却可以触发渲染引擎工作,当我们通过脚本去修改元素位置或外观时,```JS```引擎会利用```DOM```相关的```API```方法去操作```DOM```对象,此时渲染引擎变开始工作,渲染引擎会触发回流或者重绘。下面是回流重绘的两个概念: 7 | 8 | - 回流: 当我们对```DOM```的修改引发了元素尺寸的变化时,浏览器需要重新计算元素的大小和位置,最后将重新计算的结果绘制出来,这个过程称为回流。 9 | - 重绘: 当我们对```DOM```的修改只单纯改变元素的颜色时,浏览器此时并不需要重新计算元素的大小和位置,而只要重新绘制新样式。这个过程称为重绘。 10 | 11 | **很显然回流比重绘更加耗费性能**。 12 | 13 | 通过了解浏览器基本的渲染机制,我们很容易联想到当不断的通过```JS```修改```DOM```时,不经意间会触发到渲染引擎的回流或者重绘,这个性能开销是非常巨大的。因此为了降低开销,我们需要做的是尽可能减少```DOM```操作。有什么方法可以做到呢? 14 | 15 | ### 4.1.2 缓冲层-虚拟DOM 16 | 虚拟```DOM```是为了解决频繁操作```DOM```引发性能问题的产物。虚拟```DOM```(下面称为```Virtual DOM```)是将页面的状态抽象为```JS```对象的形式,本质上是```JS```和真实```DOM```的中间层,当我们想用```JS```脚本大批量进行```DOM```操作时,会优先作用于```Virtual DOM```这个```JS```对象,最后通过对比将要改动的部分通知并更新到真实的```DOM```。尽管最终还是操作真实的```DOM```,但```Virtual DOM```可以将多个改动合并成一个批量的操作,从而减少 ```DOM``` 重排的次数,进而缩短了生成渲染树和绘制所花的时间。 17 | 18 | 我们看一个真实的```DOM```包含了什么: 19 | 20 | ![](./img/4.1.png) 21 | 浏览器将一个真实```DOM```设计得很复杂,不仅包含了自身的属性描述,大小位置等定义,也囊括了```DOM```拥有的浏览器事件等。正因为如此复杂的结构,我们频繁去操作```DOM```或多或少会带来浏览器的性能问题。而作为数据和真实```DOM```之间的一层缓冲,```Virtual DOM``` 只是用来映射到真实```DOM```的渲染,因此不需要包含操作 ```DOM``` 的方法,它只要在对象中重点关注几个属性即可。 22 | ```js 23 | // 真实DOM 24 |
dom
25 | 26 | // 真实DOM对应的JS对象 27 | { 28 | tag: 'div', 29 | data: { 30 | id: 'real' 31 | }, 32 | children: [{ 33 | tag: 'span', 34 | children: 'dom' 35 | }] 36 | } 37 | ``` 38 | 39 | ## 4.2 Vnode 40 | `Vue`在渲染机制的优化上,同样引进了```virtual dom```的概念,它是用```Vnode```这个构造函数去描述一个```DOM```节点。 41 | 42 | ### 4.2.1 Vnode构造函数 43 | ```js 44 | var VNode = function VNode (tag,data,children,text,elm,context,componentOptions,asyncFactory) { 45 | this.tag = tag; // 标签 46 | this.data = data; // 数据 47 | this.children = children; // 子节点 48 | this.text = text; 49 | ··· 50 | ··· 51 | }; 52 | ``` 53 | `Vnode`定义的属性差不多有20几个,显然用```Vnode```对象要比真实```DOM```对象描述的内容要简单得多,它只用来单纯描述节点的关键属性,例如标签名,数据,子节点等。并没有保留跟浏览器相关的```DOM```方法。除此之外,```Vnode```也会有其他的属性用来扩展```Vue```的灵活性。 54 | 55 | 源码中也定义了创建```Vnode```的相关方法。 56 | 57 | 58 | ### 4.2.2 创建Vnode注释节点 59 | ```js 60 | // 创建注释vnode节点 61 | var createEmptyVNode = function (text) { 62 | if ( text === void 0 ) text = ''; 63 | 64 | var node = new VNode(); 65 | node.text = text; 66 | node.isComment = true; // 标记注释节点 67 | return node 68 | }; 69 | ``` 70 | 71 | ### 4.2.3 创建Vnode文本节点 72 | ```js 73 | // 创建文本vnode节点 74 | function createTextVNode (val) { 75 | return new VNode(undefined, undefined, undefined, String(val)) 76 | } 77 | ``` 78 | ### 4.2.4 克隆vnode 79 | ```js 80 | function cloneVNode (vnode) { 81 | var cloned = new VNode( 82 | vnode.tag, 83 | vnode.data, 84 | vnode.children && vnode.children.slice(), 85 | vnode.text, 86 | vnode.elm, 87 | vnode.context, 88 | vnode.componentOptions, 89 | vnode.asyncFactory 90 | ); 91 | cloned.ns = vnode.ns; 92 | cloned.isStatic = vnode.isStatic; 93 | cloned.key = vnode.key; 94 | cloned.isComment = vnode.isComment; 95 | cloned.fnContext = vnode.fnContext; 96 | cloned.fnOptions = vnode.fnOptions; 97 | cloned.fnScopeId = vnode.fnScopeId; 98 | cloned.asyncMeta = vnode.asyncMeta; 99 | cloned.isCloned = true; 100 | return cloned 101 | } 102 | ``` 103 | **注意:```cloneVnode```对```Vnode```的克隆只是一层浅拷贝,它不会对子节点进行深度克隆。** 104 | 105 | ## 4.3 Virtual DOM的创建 106 | 先简单回顾一下挂载的流程,挂载的过程是调用```Vue```实例上```$mount```方法,而```$mount```的核心是```mountComponent```函数。如果我们传递的是```template```模板,模板会先经过编译器的解析,并最终根据不同平台生成对应代码,此时对应的就是将```with```语句封装好的```render```函数;如果传递的是```render```函数,则跳过模板编译过程,直接进入下一个阶段。下一阶段是拿到```render```函数,调用```vm._render()```方法将```render```函数转化为```Virtual DOM```,并最终通过```vm._update()```方法将```Virtual DOM```渲染为真实的```DOM```节点。 107 | 108 | ```js 109 | Vue.prototype.$mount = function(el, hydrating) { 110 | ··· 111 | return mountComponent(this, el) 112 | } 113 | function mountComponent() { 114 | ··· 115 | updateComponent = function () { 116 | vm._update(vm._render(), hydrating); 117 | }; 118 | } 119 | 120 | ``` 121 | 我们先看看```vm._render()```方法是如何**将render函数转化为Virtual DOM**的。 122 | 123 | 回顾一下第一章节内容,文章介绍了```Vue```在代码引入时会定义很多属性和方法,其中有一个```renderMixin```过程,我们之前只提到了它会定义跟渲染有关的函数,实际上它只定义了两个重要的方法,```_render```函数就是其中一个。 124 | 125 | ```js 126 | // 引入Vue时,执行renderMixin方法,该方法定义了Vue原型上的几个方法,其中一个便是 _render函数 127 | renderMixin();// 128 | function renderMixin() { 129 | Vue.prototype._render = function() { 130 | var ref = vm.$options; 131 | var render = ref.render; 132 | ··· 133 | try { 134 | vnode = render.call(vm._renderProxy, vm.$createElement); 135 | } catch (e) { 136 | ··· 137 | } 138 | ··· 139 | return vnode 140 | } 141 | } 142 | ``` 143 | 抛开其他代码,_render函数的核心是```render.call(vm._renderProxy, vm.$createElement)```部分,```vm._renderProxy```在数据代理分析过,本质上是为了做数据过滤检测,它也绑定了```render```函数执行时的```this```指向。```vm.$createElement```方法会作为```render```函数的参数传入。**回忆一下,在手写```render```函数时,我们会利用```render```函数的第一个参数```createElement```进行渲染函数的编写,这里的```createElement```参数就是定义好的```$createElement```方法。** 144 | 145 | ```js 146 | new Vue({ 147 | el: '#app', 148 | render: function(createElement) { 149 | return createElement('div', {}, this.message) 150 | }, 151 | data() { 152 | return { 153 | message: 'dom' 154 | } 155 | } 156 | }) 157 | ``` 158 | 初始化```_init```时,有一个```initRender```函数,它就是用来定义渲染函数方法的,其中就有``````vm.$createElement``````方法的定义,除了```$createElement```,```_c```方法的定义也类似。其中 ```vm._c``` 是```template```内部编译成```render```函数时调用的方法,```vm.$createElement```是手写```render```函数时调用的方法。**两者的唯一区别仅仅是最后一个参数的不同。通过模板生成的```render```方法可以保证子节点都是```Vnode```,而手写的```render```需要一些检验和转换。** 159 | 160 | 161 | ```js 162 | function initRender(vm) { 163 | vm._c = function(a, b, c, d) { return createElement(vm, a, b, c, d, false); } 164 | vm.$createElement = function (a, b, c, d) { return createElement(vm, a, b, c, d, true); }; 165 | } 166 | ``` 167 | 168 | `createElement` 方法实际上是对 ```_createElement``` 方法的封装,在调用```_createElement```前,它会先对传入的参数进行处理,毕竟手写的```render```函数参数规格不统一。举一个简单的例子。 169 | ```js 170 | // 没有data 171 | new Vue({ 172 | el: '#app', 173 | render: function(createElement) { 174 | return createElement('div', this.message) 175 | }, 176 | data() { 177 | return { 178 | message: 'dom' 179 | } 180 | } 181 | }) 182 | // 有data 183 | new Vue({ 184 | el: '#app', 185 | render: function(createElement) { 186 | return createElement('div', {}, this.message) 187 | }, 188 | data() { 189 | return { 190 | message: 'dom' 191 | } 192 | } 193 | }) 194 | ``` 195 | 这里如果第二个参数是变量或者数组,则默认是没有传递```data```,因为```data```一般是对象形式存在。 196 | 197 | ```js 198 | function createElement ( 199 | context, // vm 实例 200 | tag, // 标签 201 | data, // 节点相关数据,属性 202 | children, // 子节点 203 | normalizationType, 204 | alwaysNormalize // 区分内部编译生成的render还是手写render 205 | ) { 206 | // 对传入参数做处理,如果没有data,则将第三个参数作为第四个参数使用,往上类推。 207 | if (Array.isArray(data) || isPrimitive(data)) { 208 | normalizationType = children; 209 | children = data; 210 | data = undefined; 211 | } 212 | // 根据是alwaysNormalize 区分是内部编译使用的,还是用户手写render使用的 213 | if (isTrue(alwaysNormalize)) { 214 | normalizationType = ALWAYS_NORMALIZE; 215 | } 216 | return _createElement(context, tag, data, children, normalizationType) // 真正生成Vnode的方法 217 | } 218 | ``` 219 | 220 | ### 4.3.1 数据规范检测 221 | 222 | `Vue`既然暴露给用户用```render```函数去手写渲染模板,就需要考虑用户操作带来的不确定性,因此```_createElement```在创建```Vnode```前会先数据的规范性进行检测,将不合法的数据类型错误提前暴露给用户。接下来将列举几个在实际场景中容易犯的错误,也方便我们理解源码中对这类错误的处理。 223 | 224 | 1. 用响应式对象做```data```属性 225 | ```js 226 | new Vue({ 227 | el: '#app', 228 | render: function (createElement, context) { 229 | return createElement('div', this.observeData, this.show) 230 | }, 231 | data() { 232 | return { 233 | show: 'dom', 234 | observeData: { 235 | attr: { 236 | id: 'test' 237 | } 238 | } 239 | } 240 | } 241 | }) 242 | ``` 243 | 2. 当特殊属性key的值为非字符串,非数字类型时 244 | ```js 245 | new Vue({ 246 | el: '#app', 247 | render: function(createElement) { 248 | return createElement('div', { key: this.lists }, this.lists.map(l => { 249 | return createElement('span', l.name) 250 | })) 251 | }, 252 | data() { 253 | return { 254 | lists: [{ 255 | name: '111' 256 | }, 257 | { 258 | name: '222' 259 | } 260 | ], 261 | } 262 | } 263 | }) 264 | ``` 265 | 这些规范都会在创建```Vnode```节点之前发现并报错,源代码如下: 266 | ```js 267 | function _createElement (context,tag,data,children,normalizationType) { 268 | // 1. 数据对象不能是定义在Vue data属性中的响应式数据。 269 | if (isDef(data) && isDef((data).__ob__)) { 270 | warn( 271 | "Avoid using observed data object as vnode data: " + (JSON.stringify(data)) + "\n" + 272 | 'Always create fresh vnode data objects in each render!', 273 | context 274 | ); 275 | return createEmptyVNode() // 返回注释节点 276 | } 277 | if (isDef(data) && isDef(data.is)) { 278 | tag = data.is; 279 | } 280 | if (!tag) { 281 | // 防止动态组件 :is 属性设置为false时,需要做特殊处理 282 | return createEmptyVNode() 283 | } 284 | // 2. key值只能为string,number这些原始数据类型 285 | if (isDef(data) && isDef(data.key) && !isPrimitive(data.key) 286 | ) { 287 | { 288 | warn( 289 | 'Avoid using non-primitive value as key, ' + 290 | 'use string/number value instead.', 291 | context 292 | ); 293 | } 294 | } 295 | ··· 296 | } 297 | ``` 298 | 这些规范性检测保证了后续```Virtual DOM tree```的完整生成。 299 | 300 | ### 4.3.2 子节点children规范化 301 | 302 | 303 | `Virtual DOM tree`是由每个```Vnode```以树状形式拼成的虚拟```DOM```树,我们在转换真实节点时需要的就是这样一个完整的```Virtual DOM tree```,因此我们需要保证每一个子节点都是```Vnode```类型,这里分两种场景分析。 304 | - 模板编译```render```函数,理论上```template```模板通过编译生成的```render```函数都是```Vnode```类型,但是有一个例外,函数式组件返回的是一个数组(这个特殊例子,可以看函数式组件的文章分析),这个时候```Vue```的处理是将整个```children```拍平成一维数组。 305 | - 用户定义```render```函数,这个时候又分为两种情况,一个是当```chidren```为文本节点时,这时候通过前面介绍的```createTextVNode``` 创建一个文本节点的 ```VNode```; 另一种相对复杂,当```children```中有```v-for```的时候会出现嵌套数组,这时候的处理逻辑是,遍历```children```,对每个节点进行判断,如果依旧是数组,则继续递归调用,直到类型为基础类型时,调用```createTextVnode```方法转化为```Vnode```。这样经过递归,```children```也变成了一个类型为```Vnode```的数组。 306 | 307 | ```js 308 | function _createElement() { 309 | ··· 310 | if (normalizationType === ALWAYS_NORMALIZE) { 311 | // 用户定义render函数 312 | children = normalizeChildren(children); 313 | } else if (normalizationType === SIMPLE_NORMALIZE) { 314 | // 模板编译生成的的render函数 315 | children = simpleNormalizeChildren(children); 316 | } 317 | } 318 | 319 | // 处理编译生成的render 函数 320 | function simpleNormalizeChildren (children) { 321 | for (var i = 0; i < children.length; i++) { 322 | // 子节点为数组时,进行开平操作,压成一维数组。 323 | if (Array.isArray(children[i])) { 324 | return Array.prototype.concat.apply([], children) 325 | } 326 | } 327 | return children 328 | } 329 | 330 | // 处理用户定义的render函数 331 | function normalizeChildren (children) { 332 | // 递归调用,直到子节点是基础类型,则调用创建文本节点Vnode 333 | return isPrimitive(children) 334 | ? [createTextVNode(children)] 335 | : Array.isArray(children) 336 | ? normalizeArrayChildren(children) 337 | : undefined 338 | } 339 | 340 | // 判断是否基础类型 341 | function isPrimitive (value) { 342 | return ( 343 | typeof value === 'string' || 344 | typeof value === 'number' || 345 | typeof value === 'symbol' || 346 | typeof value === 'boolean' 347 | ) 348 | } 349 | ``` 350 | 351 | ### 4.3.4 实际场景 352 | 在数据检测和组件规范化后,接下来通过```new VNode()```便可以生成一棵完整的```VNode```树,注意在```_render```过程中会遇到子组件,这个时候会优先去做子组件的初始化,这部分放到组件环节专门分析。我们用一个实际的例子,结束```render```函数到```Virtual DOM```的分析。 353 | 354 | - `template`模板形式 355 | ```js 356 | var vm = new Vue({ 357 | el: '#app', 358 | template: '
virtual dom
' 359 | }) 360 | ``` 361 | - 模板编译生成```render```函数 362 | ```js 363 | (function() { 364 | with(this){ 365 | return _c('div',[_c('span',[_v("virual dom")])]) 366 | } 367 | }) 368 | ``` 369 | - `Virtual DOM tree`的结果(省略版) 370 | ```js 371 | { 372 | tag: 'div', 373 | children: [{ 374 | tag: 'span', 375 | children: [{ 376 | tag: undefined, 377 | text: 'virtual dom' 378 | }] 379 | }] 380 | } 381 | ``` 382 | 383 | 384 | ## 4.4 虚拟Vnode映射成真实DOM 385 | 回到 ```updateComponent```的最后一个过程,虚拟的```DOM```树在生成```virtual dom```后,会调用```Vue```原型上```_update```方法,将虚拟```DOM```映射成为真实的```DOM```。从源码上可以知道,```_update```的调用时机有两个,一个是发生在初次渲染阶段,另一个发生数据更新阶段。 386 | 387 | ```js 388 | updateComponent = function () { 389 | // render生成虚拟DOM,update渲染真实DOM 390 | vm._update(vm._render(), hydrating); 391 | }; 392 | ``` 393 | `vm._update`方法的定义在```lifecycleMixin```中。 394 | ```js 395 | lifecycleMixin() 396 | function lifecycleMixin() { 397 | Vue.prototype._update = function (vnode, hydrating) { 398 | var vm = this; 399 | var prevEl = vm.$el; 400 | var prevVnode = vm._vnode; // prevVnode为旧vnode节点 401 | // 通过是否有旧节点判断是初次渲染还是数据更新 402 | if (!prevVnode) { 403 | // 初次渲染 404 | vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false) 405 | } else { 406 | // 数据更新 407 | vm.$el = vm.__patch__(prevVnode, vnode); 408 | } 409 | } 410 | ``` 411 | `_update`的核心是```__patch__```方法,如果是服务端渲染,由于没有```DOM```,```_patch```方法是一个空函数,在有```DOM```对象的浏览器环境下,```__patch__```是```patch```函数的引用。 412 | ``` 413 | // 浏览器端才有DOM,服务端没有dom,所以patch为一个空函数 414 | Vue.prototype.__patch__ = inBrowser ? patch : noop; 415 | ``` 416 | 417 | 而```patch```方法又是```createPatchFunction```方法的返回值,```createPatchFunction```方法传递一个对象作为参数,对象拥有两个属性,```nodeOps```和```modules```,```nodeOps```封装了一系列操作原生```DOM```对象的方法。而```modules```定义了模块的钩子函数。 418 | ```js 419 | var patch = createPatchFunction({ nodeOps: nodeOps, modules: modules }); 420 | 421 | // 将操作dom对象的方法合集做冻结操作 422 | var nodeOps = /*#__PURE__*/Object.freeze({ 423 | createElement: createElement$1, 424 | createElementNS: createElementNS, 425 | createTextNode: createTextNode, 426 | createComment: createComment, 427 | insertBefore: insertBefore, 428 | removeChild: removeChild, 429 | appendChild: appendChild, 430 | parentNode: parentNode, 431 | nextSibling: nextSibling, 432 | tagName: tagName, 433 | setTextContent: setTextContent, 434 | setStyleScope: setStyleScope 435 | }); 436 | 437 | // 定义了模块的钩子函数 438 | var platformModules = [ 439 | attrs, 440 | klass, 441 | events, 442 | domProps, 443 | style, 444 | transition 445 | ]; 446 | 447 | var modules = platformModules.concat(baseModules); 448 | ``` 449 | 450 | 真正的```createPatchFunction```函数有一千多行代码,这里就不方便列举出来了,它的内部首先定义了一系列辅助的方法,而核心是通过调用```createElm```方法进行```dom```操作,创建节点,插入子节点,递归创建一个完整的```DOM```树并插入到```Body```中。并且在产生真实阶段阶段,会有```diff```算法来判断前后```Vnode```的差异,以求最小化改变真实阶段。后面会有一个章节的内容去讲解```diff```算法。```createPatchFunction```的过程只需要先记住一些结论,函数内部会调用封装好的```DOM api```,根据```Virtual DOM```的结果去生成真实的节点。其中如果遇到组件```Vnode```时,会递归调用子组件的挂载过程,这个过程我们也会放到后面章节去分析。 451 | 452 | ## 4.5 小结 453 | 这一节分析了```mountComponent```的两个核心方法,```render```和```update```,在分析前重点介绍了存在于```JS```操作和```DOM```渲染的桥梁:```Virtual DOM```。```JS```对```DOM```节点的批量操作会先直接反应到```Virtual DOM```这个描述对象上,最终的结果才会直接作用到真实节点上。可以说,```Virtual DOM```很大程度提高了渲染的性能。文章重点介绍了```render```函数转换成```Virtual DOM```的过程,并大致描述了```_update```函数的实现思路。其实这两个过程都牵扯到组件,所以这一节对很多环节都无法深入分析,下一节开始会进入组件的专题。我相信分析完组件后,读者会对整个渲染过程会有更深刻的理解和思考。 454 | -------------------------------------------------------------------------------- /src/vue插槽,你想了解的都在这里.md: -------------------------------------------------------------------------------- 1 | > Vue组件的另一个重要概念是插槽,它允许你以一种不同于严格的父子关系的方式组合组件。插槽为你提供了一个将内容放置到新位置或使组件更通用的出口。这一节将围绕官网对插槽内容的介绍思路,按照普通插槽,具名插槽,再到作用域插槽的思路,逐步深入内部的实现原理,有对插槽使用不熟悉的,可以先参考官网对[插槽](https://cn.vuejs.org/v2/guide/components-slots.html)的介绍。 2 | 3 | ## 10.1 普通插槽 4 | 插槽将``````作为子组件承载分发的载体,简单的用法如下 5 | ### 10.1.1 基础用法 6 | ``` 7 | var child = { 8 | template: `
` 9 | } 10 | var vm = new Vue({ 11 | el: '#app', 12 | components: { 13 | child 14 | }, 15 | template: `
test
` 16 | }) 17 | // 最终渲染结果 18 |
test
19 | ``` 20 | ### 10.1.2 组件挂载原理 21 | 插槽的原理,贯穿了整个组件系统编译到渲染的过程,所以首先需要回顾一下对组件相关编译渲染流程,简单总结一下几点: 22 | 1. 从根实例入手进行实例的挂载,如果有手写的```render```函数,则直接进入```$mount```挂载流程。 23 | 2. 只有```template```模板则需要对模板进行解析,这里分为两个阶段,一个是将模板解析为```AST```树,另一个是根据不同平台生成执行代码,例如```render```函数。 24 | 3. ```$mount```流程也分为两步,第一步是将```render```函数生成```Vnode```树,如果遇到子组件会先生成子组件,子组件会以```vue-componet-```为```tag```标记,另一步是把```Vnode```渲染成真正的DOM节点。 25 | 4. 创建真实节点过程中,如果遇到子的占位符组件会进行子组件的实例化过程,这个过程又将回到流程的第一步。 26 | 27 | 接下来我们对```slot```的分析将围绕这四个具体的流程展开。 28 | 29 | 30 | ### 10.1.3 父组件处理 31 | 回到组件实例流程中,父组件会优先于子组件进行实例的挂载,模板的解析和```render```函数的生成阶段在处理上没有特殊的差异,这里就不展开分析。接下来是```render```函数生成```Vnode```的过程,在这个阶段会遇到子的占位符节点(即:```child```),因此会为子组件创建子的```Vnode```。```createComponent```执行了创建子占位节点```Vnode```的过程。我们把重点放在最终```Vnode```代码的生成。 32 | ```js 33 | // 创建子Vnode过程 34 | function createComponent ( 35 | Ctor, // 子类构造器 36 | data, 37 | context, // vm实例 38 | children, // 父组件需要分发的内容 39 | tag // 子组件占位符 40 | ){ 41 | ··· 42 | // 创建子vnode,其中父保留的children属性会以选项的形式传递给Vnode 43 | var vnode = new VNode( 44 | ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')), 45 | data, undefined, undefined, undefined, context, 46 | { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children }, 47 | asyncFactory 48 | ); 49 | } 50 | // Vnode构造器 51 | var VNode = function VNode (tag,data,children,text,elm,context,componentOptions,asyncFactory) { 52 | ··· 53 | this.componentOptions = componentOptions; // 子组件的选项相关 54 | } 55 | ``` 56 | `createComponent`函数接收的第四个参数```children```就是父组件需要分发的内容。在创建子```Vnode```过程中,会以会```componentOptions```配置传入```Vnode```构造器中。**最终```Vnode```中父组件需要分发的内容以```componentOptions```属性的形式存在,这是插槽分析的第一步**。 57 | 58 | ### 10.1.4 子组件流程 59 | 父组件的最后一个阶段是将```Vnode```渲染为真正的DOM节点,在这个过程中如果遇到子```Vnode```会优先实例化子组件并进行一系列子组件的渲染流程。子组件初始化会先调用```_init```方法,并且和父组件不同的是,子组件会调用```initInternalComponent```方法拿到父组件拥有的相关配置信息,并赋值给子组件自身的配置选项。 60 | 61 | ```js 62 | // 子组件的初始化 63 | Vue.prototype._init = function(options) { 64 | if (options && options._isComponent) { 65 | initInternalComponent(vm, options); 66 | } 67 | initRender(vm) 68 | } 69 | function initInternalComponent (vm, options) { 70 | var opts = vm.$options = Object.create(vm.constructor.options); 71 | var parentVnode = options._parentVnode; 72 | opts.parent = options.parent; 73 | opts._parentVnode = parentVnode; 74 | // componentOptions为子vnode记录的相关信息 75 | var vnodeComponentOptions = parentVnode.componentOptions; 76 | opts.propsData = vnodeComponentOptions.propsData; 77 | opts._parentListeners = vnodeComponentOptions.listeners; 78 | // 父组件需要分发的内容赋值给子选项配置的_renderChildren 79 | opts._renderChildren = vnodeComponentOptions.children; 80 | opts._componentTag = vnodeComponentOptions.tag; 81 | 82 | if (options.render) { 83 | opts.render = options.render; 84 | opts.staticRenderFns = options.staticRenderFns; 85 | } 86 | } 87 | ``` 88 | 最终在**子组件实例的配置中拿到了父组件保存的分发内容,记录在组件实例```$options._renderChildren```中,这是第二步的重点**。 89 | 90 | 接下来是子组件的实例化会进入```initRender```阶段,在这个过程会**将配置的```_renderChildren```属性做规范化处理,并将他赋值给子实例上的```$slot```属性,这是第三步的重点**。 91 | 92 | ```js 93 | function initRender(vm) { 94 | ··· 95 | vm.$slots = resolveSlots(options._renderChildren, renderContext);// $slots拿到了子占位符节点的_renderchildren(即需要分发的内容),保留作为子实例的属性 96 | } 97 | 98 | function resolveSlots (children,context) { 99 | // children是父组件需要分发到子组件的Vnode节点,如果不存在,则没有分发内容 100 | if (!children || !children.length) { 101 | return {} 102 | } 103 | var slots = {}; 104 | for (var i = 0, l = children.length; i < l; i++) { 105 | var child = children[i]; 106 | var data = child.data; 107 | // remove slot attribute if the node is resolved as a Vue slot node 108 | if (data && data.attrs && data.attrs.slot) { 109 | delete data.attrs.slot; 110 | } 111 | // named slots should only be respected if the vnode was rendered in the 112 | // same context. 113 | // 分支1为具名插槽的逻辑,放后分析 114 | if ((child.context === context || child.fnContext === context) && 115 | data && data.slot != null 116 | ) { 117 | var name = data.slot; 118 | var slot = (slots[name] || (slots[name] = [])); 119 | if (child.tag === 'template') { 120 | slot.push.apply(slot, child.children || []); 121 | } else { 122 | slot.push(child); 123 | } 124 | } else { 125 | // 普通插槽的重点,核心逻辑是构造{ default: [children] }对象返回 126 | (slots.default || (slots.default = [])).push(child); 127 | } 128 | } 129 | return slots 130 | } 131 | ``` 132 | 其中普通插槽的处理逻辑核心在```(slots.default || (slots.default = [])).push(child);```,即以数组的形式赋值给```default```属性,并以```$slot```属性的形式保存在子组件的实例中。 133 | 134 | 135 | 随后子组件也会走挂载的流程,同样会经历```template```模板到```render```函数,再到```Vnode```,最后渲染真实```DOM```的过程。解析```AST```阶段,```slot```标签和其他普通标签处理相同,**不同之处在于```AST```生成```render```函数阶段,对```slot```标签的处理,会使用```_t函数```进行包裹。这是关键步骤的第四步** 136 | 137 | 子组件渲染的大致流程简单梳理如下: 138 | ```js 139 | // ast 生成 render函数 140 | var code = generate(ast, options); 141 | // generate实现 142 | function generate(ast, options) { 143 | var state = new CodegenState(options); 144 | var code = ast ? genElement(ast, state) : '_c("div")'; 145 | return { 146 | render: ("with(this){return " + code + "}"), 147 | staticRenderFns: state.staticRenderFns 148 | } 149 | } 150 | // genElement实现 151 | function genElement(el, state) { 152 | // 针对slot标签的处理走```genSlot```分支 153 | if (el.tag === 'slot') { 154 | return genSlot(el, state) 155 | } 156 | } 157 | // 核心genSlot原理 158 | function genSlot (el, state) { 159 | // slotName记录着插槽的唯一标志名,默认为default 160 | var slotName = el.slotName || '"default"'; 161 | // 如果子组件的插槽还有子元素,则会递归调执行子元素的创建过程 162 | var children = genChildren(el, state); 163 | // 通过_t函数包裹 164 | var res = "_t(" + slotName + (children ? ("," + children) : ''); 165 | // 具名插槽的其他处理 166 | ··· 167 | return res + ')' 168 | } 169 | ``` 170 | 最终子组件的```render```函数为: 171 | ```js 172 | "with(this){return _c('div',{staticClass:"child"},[_t("default")],2)}" 173 | ``` 174 | 175 | **第五步到了子组件渲染为```Vnode```的过程。```render```函数执行阶段会执行```_t()```函数,```_t```函数是```renderSlot```函数简写,它会在```Vnode```树中进行分发内容的替换**,具体看看实现逻辑。 176 | ```js 177 | 178 | // target._t = renderSlot; 179 | 180 | // render函数渲染Vnode函数 181 | Vue.prototype._render = function() { 182 | var _parentVnode = ref._parentVnode; 183 | if (_parentVnode) { 184 | // slots的规范化处理并赋值给$scopedSlots属性。 185 | vm.$scopedSlots = normalizeScopedSlots( 186 | _parentVnode.data.scopedSlots, 187 | vm.$slots, // 记录父组件的插槽内容 188 | vm.$scopedSlots 189 | ); 190 | } 191 | } 192 | ``` 193 | 194 | `normalizeScopedSlots`的逻辑较长,但并不是本节的重点。拿到```$scopedSlots```属性后会执行真正的```render```函数,其中```_t```的执行逻辑如下: 195 | ```js 196 | // 渲染slot组件内容 197 | function renderSlot ( 198 | name, 199 | fallback, // slot插槽后备内容(针对后备内容) 200 | props, // 子传给父的值(作用域插槽) 201 | bindObject 202 | ) { 203 | // scopedSlotFn拿到父组件插槽的执行函数,默认slotname为default 204 | var scopedSlotFn = this.$scopedSlots[name]; 205 | var nodes; 206 | // 具名插槽分支(暂时忽略) 207 | if (scopedSlotFn) { // scoped slot 208 | props = props || {}; 209 | if (bindObject) { 210 | if (!isObject(bindObject)) { 211 | warn( 212 | 'slot v-bind without argument expects an Object', 213 | this 214 | ); 215 | } 216 | props = extend(extend({}, bindObject), props); 217 | } 218 | // 执行时将子组件传递给父组件的值传入fn 219 | nodes = scopedSlotFn(props) || fallback; 220 | } else { 221 | // 如果父占位符组件没有插槽内容,this.$slots不会有值,此时vnode节点为后备内容节点。 222 | nodes = this.$slots[name] || fallback; 223 | } 224 | 225 | var target = props && props.slot; 226 | if (target) { 227 | return this.$createElement('template', { slot: target }, nodes) 228 | } else { 229 | return nodes 230 | } 231 | } 232 | ``` 233 | `renderSlot`执行过程会拿到父组件需要分发的内容,最终```Vnode```树将父元素的插槽替换掉子组件的```slot```组件。 234 | 235 | **最后一步就是子组件真实节点的渲染了,这点没有什么特别点,和以往介绍的流程一致**。 236 | 237 | 至此,一个完整且简单的插槽流程分析完毕。接下来看插槽深层次的用法。 238 | 239 | ## 10.2 具有后备内容的插槽 240 | 有时为一个插槽设置具体的后备 (也就是默认的) 内容是很有用的,它只会在没有提供内容的时候被渲染。查看源码发现后备内容插槽的逻辑也很好理解。 241 | ```js 242 | var child = { 243 | template: `
后备内容
` 244 | } 245 | var vm = new Vue({ 246 | el: '#app', 247 | components: { 248 | child 249 | }, 250 | template: `
` 251 | }) 252 | // 父没有插槽内容,子的slot会渲染后备内容 253 |
后备内容
254 | ``` 255 | 父组件没有需要分发的内容,子组件会默认显示插槽里面的内容。源码中的不同体现在下面的几点。 256 | 1. 父组件渲染过程由于没有需要分发的子节点,所以不再需要拥有```componentOptions.children```属性来记录内容。 257 | 2. 因此子组件也拿不到```$slot```属性的内容. 258 | 3. 子组件的```render```函数最后在```_t```函数参数会携带第二个参数,该参数以数组的形式传入```slot```插槽的后备内容。例```with(this){return _c('div',{staticClass:"child"},[_t("default",[_v("test")])],2)}``` 259 | 4. 渲染子```Vnode```会执行```renderSlot(即:_t)```函数时,第二个参数```fallback```有值,且```this.$slots```没值,```vnode```会直接返回后备内容作为渲染对象。 260 | 261 | ```js 262 | function renderSlot ( 263 | name, 264 | fallback, // slot插槽后备内容(针对后备内容) 265 | props, // 子传给父的值(作用域插槽) 266 | bindObject 267 | ){ 268 | if() { 269 | ··· 270 | }else{ 271 | //fallback为后备内容 272 | // 如果父占位符组件没有插槽内容,this.$slots不会有值,此时vnode节点为后备内容节点。 273 | nodes = this.$slots[name] || fallback; 274 | } 275 | } 276 | 277 | ``` 278 | 279 | 最终,在父组件没有提供内容时,```slot```的后备内容被渲染。 280 | 281 | 有了这些基础,我们再来看官网给的一条规则。 282 | 283 | > 父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。 284 | 285 | 父组件模板的内容在父组件编译阶段就确定了,并且保存在```componentOptions```属性中,而子组件有自身初始化```init```的过程,这个过程同样会进行子作用域的模板编译,因此两部分内容是相对独立的。 286 | 287 | ## 10.3 具名插槽 288 | 往往我们需要灵活的使用插槽进行通用组件的开发,要求父组件每个模板对应子组件中每个插槽,这时我们可以使用``````的```name```属性,同样举个简单的例子。 289 | ```js 290 | var child = { 291 | template: `
`, 292 | } 293 | var vm = new Vue({ 294 | el: '#app', 295 | components: { 296 | child 297 | }, 298 | template: `
`, 299 | }) 300 | ``` 301 | 渲染结果: 302 | ```js 303 |
头部底部
304 | ``` 305 | 接下来我们在普通插槽的基础上,看看源码在具名插槽实现上的区别。 306 | 307 | ### 10.3.1 模板编译的差别 308 | 父组件在编译```AST```阶段和普通节点的过程不同,具名插槽一般会在```template```模板中用```v-slot:```来标注指定插槽,这一阶段会在编译阶段特殊处理。最终的```AST```树会携带```scopedSlots```用来记录具名插槽的内容 309 | ```js 310 | { 311 | scopedSlots: { 312 | footer: { ··· }, 313 | header: { ··· } 314 | } 315 | } 316 | ``` 317 | `AST`生成```render```函数的过程也不详细分析了,我们只分析父组件最终返回的结果(如果对```parse, generate```感兴趣的同学,可以直接看源码分析,编译阶段冗长且难以讲解,跳过这部分分析) 318 | 319 | ```js 320 | with(this){return _c('div',{attrs:{"id":"app"}},[_c('child',{scopedSlots:_u([{key:"header",fn:function(){return [_c('span',[_v("头部")])]},proxy:true},{key:"footer",fn:function(){return [_c('span',[_v("底部")])]},proxy:true}])})],1)} 321 | ``` 322 | 很明显,父组件的插槽内容用```_u```函数封装成数组的形式,并赋值到```scopedSlots```属性中,而每一个插槽以对象形式描述,```key```代表插槽名,```fn```是一个返回执行结果的函数。 323 | 324 | ### 10.3.2 父组件vnode生成阶段 325 | 326 | 照例进入父组件生成```Vnode```阶段,其中```_u```函数的原形是```resolveScopedSlots```,其中第一个参数就是插槽数组。 327 | ```js 328 | // vnode生成阶段针对具名插槽的处理 _u (target._u = resolveScopedSlots) 329 | function resolveScopedSlots (fns,res,hasDynamicKeys,contentHashKey) { 330 | res = res || { $stable: !hasDynamicKeys }; 331 | for (var i = 0; i < fns.length; i++) { 332 | var slot = fns[i]; 333 | // fn是数组需要递归处理。 334 | if (Array.isArray(slot)) { 335 | resolveScopedSlots(slot, res, hasDynamicKeys); 336 | } else if (slot) { 337 | // marker for reverse proxying v-slot without scope on this.$slots 338 | if (slot.proxy) { // 针对proxy的处理 339 | slot.fn.proxy = true; 340 | } 341 | // 最终返回一个对象,对象以slotname作为属性,以fn作为值 342 | res[slot.key] = slot.fn; 343 | } 344 | } 345 | if (contentHashKey) { 346 | (res).$key = contentHashKey; 347 | } 348 | return res 349 | } 350 | ``` 351 | 最终父组件的```vnode```节点的```data```属性上多了```scopedSlots```数组。**回顾一下,具名插槽和普通插槽实现上有明显的不同,普通插槽是以```componentOptions.child```的形式保留在父组件中,而具名插槽是以```scopedSlots```属性的形式存储到```data```属性中。** 352 | ```js 353 | // vnode 354 | { 355 | scopedSlots: [{ 356 | 'header': fn, 357 | 'footer': fn 358 | }] 359 | } 360 | ``` 361 | 362 | ### 10.3.3 子组件渲染Vnode过程 363 | 364 | 子组件在解析成```AST```树阶段的不同,在于对```slot```标签的```name```属性的解析,而在```render```生成```Vnode```过程中,```slot```的规范化处理针对具名插槽会进行特殊的处理,回到```normalizeScopedSlots```的代码 365 | ```js 366 | vm.$scopedSlots = normalizeScopedSlots( 367 | _parentVnode.data.scopedSlots, // 此时的第一个参数会拿到父组件插槽相关的数据 368 | vm.$slots, // 记录父组件的插槽内容 369 | vm.$scopedSlots 370 | ); 371 | 372 | ``` 373 | 最终子组件实例上的```$scopedSlots```属性会携带父组件插槽相关的内容。 374 | ```js 375 | // 子组件Vnode 376 | { 377 | $scopedSlots: [{ 378 | 'header': f, 379 | 'footer': f 380 | }] 381 | } 382 | ``` 383 | 384 | ### 10.3.4 子组件渲染真实dom 385 | 386 | 和普通插槽类似,子组件渲染真实节点的过程会执行子```render```函数中的```_t```方法,这部分的源码会和普通插槽走不同的分支,其中```this.$scopedSlots```根据上面分析会记录着父组件插槽内容相关的数据,所以会和普通插槽走不同的分支。而最终的核心是执行```nodes = scopedSlotFn(props)```,也就是执行```function(){return [_c('span',[_v("头部")])]}```,具名插槽之所以是函数的形式执行而不是直接返回结果,我们在后面揭晓。 387 | ```js 388 | function renderSlot ( 389 | name, 390 | fallback, // slot插槽后备内容 391 | props, // 子传给父的值 392 | bindObject 393 | ){ 394 | var scopedSlotFn = this.$scopedSlots[name]; 395 | var nodes; 396 | // 针对具名插槽,特点是$scopedSlots有值 397 | if (scopedSlotFn) { // scoped slot 398 | props = props || {}; 399 | if (bindObject) { 400 | if (!isObject(bindObject)) { 401 | warn('slot v-bind without argument expects an Object',this); 402 | } 403 | props = extend(extend({}, bindObject), props); 404 | } 405 | // 执行时将子组件传递给父组件的值传入fn 406 | nodes = scopedSlotFn(props) || fallback; 407 | }··· 408 | } 409 | ``` 410 | 至此子组件通过```slotName```找到了对应父组件的插槽内容。 411 | 412 | 413 | ## 10.4 作用域插槽 414 | 最后说说作用域插槽,我们可以利用作用域插槽让父组件的插槽内容访问到子组件的数据,具体的用法是在子组件中以属性的方式记录在子组件中,父组件通过```v-slot:[name]=[props]```的形式拿到子组件传递的值。子组件``````元素上的特性称为**插槽```Props```**,另外,vue2.6以后的版本已经弃用了```slot-scoped```,采用```v-slot```代替。 415 | ```js 416 | var child = { 417 | template: `
`, 418 | data() { 419 | return { 420 | user: { 421 | firstname: 'test' 422 | } 423 | } 424 | } 425 | } 426 | var vm = new Vue({ 427 | el: '#app', 428 | components: { 429 | child 430 | }, 431 | template: `
` 432 | }) 433 | ``` 434 | 435 | 作用域插槽和具名插槽的原理类似,我们接着往下看。 436 | 437 | ### 10.4.1 父组件编译阶段 438 | 作用域插槽和具名插槽在父组件的用法基本相同,区别在于```v-slot```定义了一个插槽```props```的名字,参考对于具名插槽的分析,生成```render```函数阶段```fn```函数会携带```props```参数传入。即: 439 | 440 | ```js 441 | with(this){return _c('div',{attrs:{"id":"app"}},[_c('child',{scopedSlots:_u([{key:"default",fn:function(slotProps){return [_v(_s(slotProps.user.firstname))]}}])})],1)} 442 | ``` 443 | 444 | ### 10.4.2 子组件渲染 445 | 在子组件编译阶段,```:user="user"```会以属性的形式解析,最终在```render```函数生成阶段以对象参数的形式传递```_t```函数。 446 | 447 | ```js 448 | with(this){return _c('div',[_t("default",null,{"user":user})],2)} 449 | ``` 450 | 451 | 子组件渲染Vnode阶段,根据前面分析会执行```renderSlot```函数,这个函数前面分析过,对于作用域插槽的处理,集中体现在函数传入的第三个参数。 452 | ```js 453 | // 渲染slot组件vnode 454 | function renderSlot( 455 | name, 456 | fallback, 457 | props, // 子传给父的值 { user: user } 458 | bindObject 459 | ) { 460 | // scopedSlotFn拿到父组件插槽的执行函数,默认slotname为default 461 | var scopedSlotFn = this.$scopedSlots[name]; 462 | var nodes; 463 | // 具名插槽分支 464 | if (scopedSlotFn) { // scoped slot 465 | props = props || {}; 466 | if (bindObject) { 467 | if (!isObject(bindObject)) { 468 | warn( 469 | 'slot v-bind without argument expects an Object', 470 | this 471 | ); 472 | } 473 | // 合并props 474 | props = extend(extend({}, bindObject), props); 475 | } 476 | // 执行时将子组件传递给父组件的值传入fn 477 | nodes = scopedSlotFn(props) || fallback; 478 | } 479 | ``` 480 | 最终将子组件的插槽```props```作为参数传递给执行函数执行。**回过头看看为什么具名插槽是函数的形式执行而不是直接返回结果。学完作用域插槽我们发现这就是设计巧妙的地方,函数的形式让执行过程更加灵活,作用域插槽只需要以参数的形式将插槽```props```传入便可以得到想要的结果。** 481 | 482 | ### 10.4.3 思考 483 | 作用域插槽这个概念一开始我很难理解,单纯从定义和源码的结论上看,父组件的插槽内容可以访问到子组件的数据,这不是明显的子父之间的信息通信吗,在事件章节我们知道,子父组件之间的通信完全可以通过事件```$emit,$on```的形式来完成,那么为什么还需要增加一个插槽```props```的概念呢。我们看看作者的解释。 484 | 485 | > 插槽 ```prop``` 允许我们将插槽转换为可复用的模板,这些模板可以基于输入的 ```prop``` 渲染出不同的内容 486 | 487 | 从我自身的角度理解,作用域插槽提供了一种方式,当你需要封装一个通用,可复用的逻辑模块,并且这个模块给外部使用者提供了一个便利,允许你在使用组件时自定义部分布局,这时候作用域插槽就派上大用场了,再到具体的思想,我们可以看看几个工具库[Vue Virtual Scroller](https://github.com/Akryum/vue-virtual-scroller), [Vue Promised](https://github.com/posva/vue-promised)对这一思想的应用。 488 | 489 | -------------------------------------------------------------------------------- /src/深入响应式系统构建-下.md: -------------------------------------------------------------------------------- 1 | > 上一节,我们深入分析了以```data,computed```为数据创建响应式系统的过程,并对其中依赖收集和派发更新的过程进行了详细的分析。然而在使用和分析过程中依然存在或多或少的问题,这一节我们将针对这些问题展开分析,最后我们也会分析一下```watch```的响应式过程。这篇文章将作为响应式系统分析的完结篇。 2 | 3 | ## 7.12 数组检测 4 | 在之前介绍数据代理章节,我们已经详细介绍过```Vue```数据代理的技术是利用了```Object.defineProperty```,```Object.defineProperty```让我们可以方便的利用存取描述符中的```getter/setter```来进行数据的监听,在```get,set```钩子中分别做不同的操作,达到数据拦截的目的。然而```Object.defineProperty```的```get,set```方法只能检测到对象属性的变化,对于数组的变化(例如插入删除数组元素等操作),```Object.defineProperty```却无法达到目的,这也是利用```Object.defineProperty```进行数据监控的缺陷,虽然```es6```中的```proxy```可以完美解决这一问题,但毕竟有兼容性问题,所以我们还需要研究```Vue```在```Object.defineProperty```的基础上如何对数组进行监听检测。 5 | 6 | ### 7.12.1 数组方法的重写 7 | 既然数组已经不能再通过数据的```getter,setter```方法去监听变化了,```Vue```的做法是对数组方法进行重写,在保留原数组功能的前提下,对数组进行额外的操作处理。也就是重新定义了数组方法。 8 | 9 | ```js 10 | var arrayProto = Array.prototype; 11 | // 新建一个继承于Array的对象 12 | var arrayMethods = Object.create(arrayProto); 13 | 14 | // 数组拥有的方法 15 | var methodsToPatch = [ 16 | 'push', 17 | 'pop', 18 | 'shift', 19 | 'unshift', 20 | 'splice', 21 | 'sort', 22 | 'reverse' 23 | ]; 24 | ``` 25 | `arrayMethods`是基于原始```Array```类为原型继承的一个对象类,由于原型链的继承,```arrayMethod```拥有数组的所有方法,接下来对这个新的数组类的方法进行改写。 26 | ```js 27 | methodsToPatch.forEach(function (method) { 28 | // 缓冲原始数组的方法 29 | var original = arrayProto[method]; 30 | // 利用Object.defineProperty对方法的执行进行改写 31 | def(arrayMethods, method, function mutator () {}); 32 | }); 33 | 34 | function def (obj, key, val, enumerable) { 35 | Object.defineProperty(obj, key, { 36 | value: val, 37 | enumerable: !!enumerable, 38 | writable: true, 39 | configurable: true 40 | }); 41 | } 42 | 43 | ``` 44 | 45 | 这里对数组方法设置了代理,当执行```arrayMethods```的数组方法时,会代理执行```mutator```函数,这个函数的具体实现,我们放到数组的派发更新中介绍。 46 | 47 | 48 | **仅仅创建一个新的数组方法合集是不够的,我们在访问数组时,如何不调用原生的数组方法,而是将过程指向这个新的类,这是下一步的重点。** 49 | 50 | 回到数据初始化过程,也就是执行```initData```阶段,上一篇内容花了大篇幅介绍过数据初始化会为```data```数据创建一个```Observer```类,当时我们只讲述了```Observer```类会为每个非数组的属性进行数据拦截,重新定义```getter,setter```方法,除此之外对于数组类型的数据,我们有意跳过分析了。这里,我们重点看看对于数组拦截的处理。 51 | 52 | ```js 53 | var Observer = function Observer (value) { 54 | this.value = value; 55 | this.dep = new Dep(); 56 | this.vmCount = 0; 57 | // 将__ob__属性设置成不可枚举属性。外部无法通过遍历获取。 58 | def(value, '__ob__', this); 59 | // 数组处理 60 | if (Array.isArray(value)) { 61 | if (hasProto) { 62 | protoAugment(value, arrayMethods); 63 | } else { 64 | copyAugment(value, arrayMethods, arrayKeys); 65 | } 66 | this.observeArray(value); 67 | } else { 68 | // 对象处理 69 | this.walk(value); 70 | } 71 | } 72 | ``` 73 | 数组处理的分支分为两个,```hasProto```的判断条件,```hasProto```用来判断当前环境下是否支持```__proto__```属性。而数组的处理会根据是否支持这一属性来决定执行```protoAugment, copyAugment```过程, 74 | 75 | ```js 76 | // __proto__属性的判断 77 | var hasProto = '__proto__' in {}; 78 | ``` 79 | 80 | **当支持```__proto__```时,执行```protoAugment```会将当前数组的原型指向新的数组类```arrayMethods```,如果不支持```__proto__```,则通过代理设置,在访问数组方法时代理访问新数组类中的数组方法。** 81 | ```js 82 | //直接通过原型指向的方式 83 | 84 | function protoAugment (target, src) { 85 | target.__proto__ = src; 86 | } 87 | 88 | // 通过数据代理的方式 89 | function copyAugment (target, src, keys) { 90 | for (var i = 0, l = keys.length; i < l; i++) { 91 | var key = keys[i]; 92 | def(target, key, src[key]); 93 | } 94 | } 95 | ``` 96 | 有了这两步的处理,接下来我们在实例内部调用```push, unshift```等数组的方法时,会执行```arrayMethods```类的方法。这也是数组进行依赖收集和派发更新的前提。 97 | 98 | 99 | ### 7.12.2 依赖收集 100 | 由于数据初始化阶段会利用```Object.definePrototype```进行数据访问的改写,数组的访问同样会被```getter```所拦截。由于是数组,拦截过程会做特殊处理,后面我们再看看```dependArray```的原理。 101 | ```js 102 | function defineReactive###1() { 103 | ··· 104 | var childOb = !shallow && observe(val); 105 | 106 | Object.defineProperty(obj, key, { 107 | enumerable: true, 108 | configurable: true, 109 | get: function reactiveGetter () { 110 | var value = getter ? getter.call(obj) : val; 111 | if (Dep.target) { 112 | dep.depend(); 113 | if (childOb) { 114 | childOb.dep.depend(); 115 | if (Array.isArray(value)) { 116 | dependArray(value); 117 | } 118 | } 119 | } 120 | return value 121 | }, 122 | set() {} 123 | } 124 | 125 | ``` 126 | `childOb`是标志属性值是否为基础类型的标志,```observe```如果遇到基本类型数据,则直接返回,不做任何处理,如果遇到对象或者数组则会递归实例化```Observer```,会为每个子属性设置响应式数据,最终返回```Observer```实例。而实例化```Observer```又回到之前的老流程: 127 | **添加```__ob__```属性,如果遇到数组则进行原型重指向,遇到对象则定义```getter,setter```,这一过程前面分析过,就不再阐述。** 128 | 129 | 130 | 在访问到数组时,由于```childOb```的存在,会执行```childOb.dep.depend();```进行依赖收集,该```Observer```实例的```dep```属性会收集当前的```watcher```作为依赖保存,```dependArray```保证了如果数组元素是数组或者对象,需要递归去为内部的元素收集相关的依赖。 131 | ```js 132 | function dependArray (value) { 133 | for (var e = (void 0), i = 0, l = value.length; i < l; i++) { 134 | e = value[i]; 135 | e && e.__ob__ && e.__ob__.dep.depend(); 136 | if (Array.isArray(e)) { 137 | dependArray(e); 138 | } 139 | } 140 | } 141 | 142 | ``` 143 | 144 | 我们可以通过截图看最终依赖收集的结果。 145 | 146 | 收集前 147 | 148 | ![](./img/7.1.png) 149 | 150 | 收集后 151 | 152 | ![](./img/7.2.png) 153 | 154 | 155 | ### 7.12.3 派发更新 156 | 当调用数组的方法去添加或者删除数据时,数据的```setter```方法是无法拦截的,所以我们唯一可以拦截的过程就是调用数组方法的时候,前面介绍过,数组方法的调用会代理到新类```arrayMethods```的方法中,而```arrayMethods```的数组方法是进行重写过的。具体我们看他的定义。 157 | 158 | ```js 159 | methodsToPatch.forEach(function (method) { 160 | var original = arrayProto[method]; 161 | def(arrayMethods, method, function mutator () { 162 | var args = [], len = arguments.length; 163 | while ( len-- ) args[ len ] = arguments[ len ]; 164 | // 执行原数组方法 165 | var result = original.apply(this, args); 166 | var ob = this.__ob__; 167 | var inserted; 168 | switch (method) { 169 | case 'push': 170 | case 'unshift': 171 | inserted = args; 172 | break 173 | case 'splice': 174 | inserted = args.slice(2); 175 | break 176 | } 177 | if (inserted) { ob.observeArray(inserted); } 178 | // notify change 179 | ob.dep.notify(); 180 | return result 181 | }); 182 | }); 183 | 184 | ``` 185 | `mutator`是重写的数组方法,首先会调用原始的数组方法进行运算,这保证了与原始数组类型的方法一致性,```args```保存了数组方法调用传递的参数。之后取出数组的```__ob__```也就是之前保存的```Observer```实例,调用```ob.dep.notify();```进行依赖的派发更新,前面知道了。```Observer```实例的```dep```是```Dep```的实例,他收集了需要监听的```watcher```依赖,而```notify```会对依赖进行重新计算并更新。具体看```Dep.prototype.notify = function notify () {}```函数的分析,这里也不重复赘述。 186 | 187 | 回到代码中,```inserted```变量用来标志数组是否是增加了元素,如果增加的元素不是原始类型,而是数组对象类型,则需要触发```observeArray```方法,对每个元素进行依赖收集。 188 | 189 | ```js 190 | Observer.prototype.observeArray = function observeArray (items) { 191 | for (var i = 0, l = items.length; i < l; i++) { 192 | observe(items[i]); 193 | } 194 | }; 195 | ``` 196 | **总的来说。数组的改变不会触发```setter```进行依赖更新,所以```Vue```创建了一个新的数组类,重写了数组的方法,将数组方法指向了新的数组类。同时在访问到数组时依旧触发```getter```进行依赖收集,在更改数组时,触发数组新方法运算,并进行依赖的派发。** 197 | 198 | 现在我们回过头看看Vue的官方文档对于数组检测时的注意事项: 199 | > ```Vue``` 不能检测以下数组的变动: 200 | > - 当你利用索引直接设置一个数组项时,例如:```vm.items[indexOfItem] = newValue``` 201 | > - 当你修改数组的长度时,例如:```vm.items.length = newLength``` 202 | 203 | 显然有了上述的分析我们很容易理解数组检测带来的弊端,即使```Vue```重写了数组的方法,以便在设置数组时进行拦截处理,但是不管是通过索引还是直接修改长度,都是无法触发依赖更新的。 204 | 205 | 206 | ## 7.13 对象检测异常 207 | 我们在实际开发中经常遇到一种场景,对象```test: { a: 1 }```要添加一个属性```b```,这时如果我们使用```test.b = 2```的方式去添加,这个过程```Vue```是无法检测到的,理由也很简单。我们在对对象进行依赖收集的时候,会为对象的每个属性都进行收集依赖,而直接通过```test.b```添加的新属性并没有依赖收集的过程,因此当之后数据```b```发生改变时也不会进行依赖的更新。 208 | 209 | 了解决这一问题,```Vue```提供了```Vue.set(object, propertyName, value)```的静态方法和```vm.$set(object, propertyName, value)```的实例方法,我们看具体怎么完成新属性的依赖收集过程。 210 | ```js 211 | Vue.set = set 212 | function set (target, key, val) { 213 | //target必须为非空对象 214 | if (isUndef(target) || isPrimitive(target) 215 | ) { 216 | warn(("Cannot set reactive property on undefined, null, or primitive value: " + ((target)))); 217 | } 218 | // 数组场景,调用重写的splice方法,对新添加属性收集依赖。 219 | if (Array.isArray(target) && isValidArrayIndex(key)) { 220 | target.length = Math.max(target.length, key); 221 | target.splice(key, 1, val); 222 | return val 223 | } 224 | // 新增对象的属性存在时,直接返回新属性,触发依赖收集 225 | if (key in target && !(key in Object.prototype)) { 226 | target[key] = val; 227 | return val 228 | } 229 | // 拿到目标源的Observer 实例 230 | var ob = (target).__ob__; 231 | if (target._isVue || (ob && ob.vmCount)) { 232 | warn( 233 | 'Avoid adding reactive properties to a Vue instance or its root $data ' + 234 | 'at runtime - declare it upfront in the data option.' 235 | ); 236 | return val 237 | } 238 | // 目标源对象本身不是一个响应式对象,则不需要处理 239 | if (!ob) { 240 | target[key] = val; 241 | return val 242 | } 243 | // 手动调用defineReactive,为新属性设置getter,setter 244 | defineReactive###1(ob.value, key, val); 245 | ob.dep.notify(); 246 | return val 247 | } 248 | ``` 249 | 按照分支分为不同的四个处理逻辑: 250 | 1. 目标对象必须为非空的对象,可以是数组,否则抛出异常。 251 | 2. 如果目标对象是数组时,调用数组的```splice```方法,而前面分析数组检测时,遇到数组新增元素的场景,会调用```ob.observeArray(inserted)```对数组新增的元素收集依赖。 252 | 3. 新增的属性值在原对象中已经存在,则手动访问新的属性值,这一过程会触发依赖收集。 253 | 4. 手动定义新属性的```getter,setter```方法,并通过```notify```触发依赖更新。 254 | 255 | 256 | ## 7.14 nextTick 257 | 258 | 在上一节的内容中,我们说到数据修改时会触发```setter```方法进行依赖的派发更新,而更新时会将每个```watcher```推到队列中,等待下一个```tick```到来时再执行```DOM```的渲染更新操作。这个就是异步更新的过程。为了说明异步更新的概念,需要牵扯到浏览器的事件循环机制和最优的渲染时机问题。由于这不是文章的主线,我只用简单的语言概述。 259 | 260 | ### 7.14.1 事件循环机制 261 | 262 | 1. 完整的事件循环机制需要了解两种异步队列:```macro-task```和```micro-task``` 263 | 2. ```macro-task```常见的有 ```setTimeout, setInterval, setImmediate, script脚本, I/O操作,UI渲染``` 264 | 3. ```micro-task```常见的有 ```promise, process.nextTick, MutationObserver```等 265 | 4. 完整事件循环流程为: 266 | 4.1 ```micro-task```空,```macro-task```队列只有```script```脚本,推出```macro-task```的```script```任务执行,脚本执行期间产生的```macro-task,micro-task```推到对应的队列中 267 | 4.2 执行全部```micro-task```里的微任务事件 268 | 4.3 执行```DOM```操作,渲染更新页面 269 | 4.4 执行```web worker```等相关任务 270 | 4.5 循环,取出```macro-task```中一个宏任务事件执行,重复4的操作。 271 | 272 | 273 | 从上面的流程中我们可以发现,最好的渲染过程发生在微任务队列的执行过程中,此时他离页面渲染过程最近,因此我们可以借助微任务队列来实现异步更新,它可以让复杂批量的运算操作运行在JS层面,而视图的渲染只关心最终的结果,这大大降低了性能的损耗。 274 | 275 | 举一个这一做法好处的例子: 276 | 由于```Vue```是数据驱动视图更新渲染,如果我们在一个操作中重复对一个响应式数据进行计算,例如 在一个循环中执行```this.num ++ ```一千次,由于响应式系统的存在,数据变化触发```setter```,```setter```触发依赖派发更新,更新调用```run```进行视图的重新渲染。这一次循环,视图渲染要执行一千次,很明显这是很浪费性能的,我们只需要关注最后第一千次在界面上更新的结果而已。所以利用异步更新显得格外重要。 277 | 278 | ### 7.14.2 基本实现 279 | 280 | `Vue`用一个```queue```收集依赖的执行,在下次微任务执行的时候统一执行```queue```中```Watcher```的```run```操作,与此同时,相同```id```的```watcher```不会重复添加到```queue```中,因此也不会重复执行多次的视图渲染。我们看```nextTick```的实现。 281 | 282 | ```js 283 | // 原型上定义的方法 284 | Vue.prototype.$nextTick = function (fn) { 285 | return nextTick(fn, this) 286 | }; 287 | // 构造函数上定义的方法 288 | Vue.nextTick = nextTick; 289 | 290 | // 实际的定义 291 | var callbacks = []; 292 | function nextTick (cb, ctx) { 293 | var _resolve; 294 | // callbacks是维护微任务的数组。 295 | callbacks.push(function () { 296 | if (cb) { 297 | try { 298 | cb.call(ctx); 299 | } catch (e) { 300 | handleError(e, ctx, 'nextTick'); 301 | } 302 | } else if (_resolve) { 303 | _resolve(ctx); 304 | } 305 | }); 306 | if (!pending) { 307 | pending = true; 308 | // 将维护的队列推到微任务队列中维护 309 | timerFunc(); 310 | } 311 | // nextTick没有传递参数,且浏览器支持Promise,则返回一个promise对象 312 | if (!cb && typeof Promise !== 'undefined') { 313 | return new Promise(function (resolve) { 314 | _resolve = resolve; 315 | }) 316 | } 317 | } 318 | ``` 319 | 320 | `nextTick`定义为一个函数,使用方式为```Vue.nextTick( [callback, context] )```,当```callback```经过```nextTick```封装后,```callback```会在下一个```tick```中执行调用。从实现上,```callbacks```是一个维护了需要在下一个```tick```中执行的任务的队列,它的每个元素都是需要执行的函数。```pending```是判断是否在等待执行微任务队列的标志。而```timerFunc```是真正将任务队列推到微任务队列中的函数。我们看```timerFunc```的实现。 321 | 322 | 323 | 1.如果浏览器执行```Promise```,那么默认以```Promsie```将执行过程推到微任务队列中。 324 | 325 | ```js 326 | var timerFunc; 327 | 328 | if (typeof Promise !== 'undefined' && isNative(Promise)) { 329 | var p = Promise.resolve(); 330 | timerFunc = function () { 331 | p.then(flushCallbacks); 332 | // 手机端的兼容代码 333 | if (isIOS) { setTimeout(noop); } 334 | }; 335 | // 使用微任务队列的标志 336 | isUsingMicroTask = true; 337 | } 338 | ``` 339 | 340 | `flushCallbacks`是异步更新的函数,他会取出callbacks数组的每一个任务,执行任务,具体定义如下: 341 | ```js 342 | function flushCallbacks () { 343 | pending = false; 344 | var copies = callbacks.slice(0); 345 | // 取出callbacks数组的每一个任务,执行任务 346 | callbacks.length = 0; 347 | for (var i = 0; i < copies.length; i++) { 348 | copies[i](); 349 | } 350 | } 351 | ``` 352 | 353 | 2.不支持```promise```,支持```MutataionObserver``` 354 | 355 | ```js 356 | else if (!isIE && typeof MutationObserver !== 'undefined' && ( 357 | isNative(MutationObserver) || 358 | // PhantomJS and iOS 7.x 359 | MutationObserver.toString() === '[object MutationObserverConstructor]' 360 | )) { 361 | var counter = 1; 362 | var observer = new MutationObserver(flushCallbacks); 363 | var textNode = document.createTextNode(String(counter)); 364 | observer.observe(textNode, { 365 | characterData: true 366 | }); 367 | timerFunc = function () { 368 | counter = (counter + 1) % 2; 369 | textNode.data = String(counter); 370 | }; 371 | isUsingMicroTask = true; 372 | } 373 | ``` 374 | 375 | 3.如果不支持微任务方法,则会使用宏任务方法,```setImmediate```会先被使用 376 | 377 | ```js 378 | else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { 379 | // Fallback to setImmediate. 380 | // Techinically it leverages the (macro) task queue, 381 | // but it is still a better choice than setTimeout. 382 | timerFunc = function () { 383 | setImmediate(flushCallbacks); 384 | }; 385 | } 386 | ``` 387 | 4.所有方法都不适合,会使用宏任务方法中的```setTimeout``` 388 | 389 | ```js 390 | else { 391 | timerFunc = function () { 392 | setTimeout(flushCallbacks, 0); 393 | }; 394 | } 395 | ``` 396 | 397 | **当```nextTick```不传递任何参数时,可以作为一个```promise```用**,例如: 398 | ```js 399 | nextTick().then(() => {}) 400 | ``` 401 | 402 | ### 7.14.3 使用场景 403 | 说了这么多原理性的东西,回过头来看看```nextTick```的使用场景,由于异步更新的原理,我们在某一时间改变的数据并不会触发视图的更新,而是需要等下一个```tick```到来时才会更新视图,下面是一个典型场景: 404 | 405 | ```js 406 | 407 | 408 | // js 409 | data() { 410 | show: false 411 | }, 412 | mounted() { 413 | this.show = true; 414 | this.$refs.myInput.focus();// 报错 415 | } 416 | ``` 417 | 数据改变时,视图并不会同时改变,因此需要使用```nextTick``` 418 | ```js 419 | mounted() { 420 | this.show = true; 421 | this.$nextTick(function() { 422 | this.$refs.myInput.focus();// 正常 423 | }) 424 | } 425 | ``` 426 | 427 | ## 7.15 watch 428 | 到这里,关于响应式系统的分析大部分内容已经分析完毕,我们上一节还遗留着一个问题,```Vue```对用户手动添加的```watch```如何进行数据拦截。我们先看看两种基本的使用形式。 429 | ```js 430 | // watch选项 431 | var vm = new Vue({ 432 | el: '#app', 433 | data() { 434 | return { 435 | num: 12 436 | } 437 | }, 438 | watch: { 439 | num() {} 440 | } 441 | }) 442 | vm.num = 111 443 | 444 | // $watch api方式 445 | vm.$watch('num', function() {}, { 446 | deep: , 447 | immediate: , 448 | }) 449 | ``` 450 | 451 | ### 7.15.1 依赖收集 452 | 我们以```watch```选项的方式来分析```watch```的细节,同样从初始化说起,初始化数据会执行```initWatch```,```initWatch```的核心是```createWatcher```。 453 | 454 | ```js 455 | function initWatch (vm, watch) { 456 | for (var key in watch) { 457 | var handler = watch[key]; 458 | // handler可以是数组的形式,执行多个回调 459 | if (Array.isArray(handler)) { 460 | for (var i = 0; i < handler.length; i++) { 461 | createWatcher(vm, key, handler[i]); 462 | } 463 | } else { 464 | createWatcher(vm, key, handler); 465 | } 466 | } 467 | } 468 | 469 | function createWatcher (vm,expOrFn,handler,options) { 470 | // 针对watch是对象的形式,此时回调回选项中的handler 471 | if (isPlainObject(handler)) { 472 | options = handler; 473 | handler = handler.handler; 474 | } 475 | if (typeof handler === 'string') { 476 | handler = vm[handler]; 477 | } 478 | return vm.$watch(expOrFn, handler, options) 479 | } 480 | ``` 481 | 无论是选项的形式,还是```api```的形式,最终都会调用实例的```$watch```方法,其中```expOrFn```是监听的字符串,```handler```是监听的回调函数,```options```是相关配置。我们重点看看```$watch```的实现。 482 | ```js 483 | Vue.prototype.$watch = function (expOrFn,cb,options) { 484 | var vm = this; 485 | if (isPlainObject(cb)) { 486 | return createWatcher(vm, expOrFn, cb, options) 487 | } 488 | options = options || {}; 489 | options.user = true; 490 | var watcher = new Watcher(vm, expOrFn, cb, options); 491 | // 当watch有immediate选项时,立即执行cb方法,即不需要等待属性变化,立刻执行回调。 492 | if (options.immediate) { 493 | try { 494 | cb.call(vm, watcher.value); 495 | } catch (error) { 496 | handleError(error, vm, ("callback for immediate watcher \"" + (watcher.expression) + "\"")); 497 | } 498 | } 499 | return function unwatchFn () { 500 | watcher.teardown(); 501 | } 502 | }; 503 | } 504 | ``` 505 | `$watch`的核心是创建一个```user watcher```,```options.user```是当前用户定义```watcher```的标志。如果有```immediate```属性,则立即执行回调函数。 506 | 而实例化```watcher```时会执行一次```getter```求值,这时,```user watcher```会作为依赖被数据所收集。这个过程可以参考```data```的分析。 507 | 508 | ```js 509 | var Watcher = function Watcher() { 510 | ··· 511 | this.value = this.lazy 512 | ? undefined 513 | : this.get(); 514 | } 515 | 516 | Watcher.prototype.get = function get() { 517 | ··· 518 | try { 519 | // getter回调函数,触发依赖收集 520 | value = this.getter.call(vm, vm); 521 | } 522 | } 523 | ``` 524 | 525 | ### 7.15.2 派发更新 526 | `watch`派发更新的过程很好理解,数据发生改变时,```setter```拦截对依赖进行更新,而此前```user watcher```已经被当成依赖收集了。这个时候依赖的更新就是回调函数的执行。 527 | 528 | 529 | 530 | ## 7.16 小结 531 | 这一节是响应式系统构建的完结篇,```data,computed```如何进行响应式系统设计,这在上一节内容已经详细分析,这一节针对一些特殊场景做了分析。例如由于```Object.defineProperty```自身的缺陷,无法对数组的新增删除进行拦截检测,因此```Vue```对数组进行了特殊处理,重写了数组的方法,并在方法中对数据进行拦截。我们也重点介绍了```nextTick```的原理,利用浏览器的事件循环机制来达到最优的渲染时机。文章的最后补充了```watch```在响应式设计的原理,用户自定义的```watch```会创建一个依赖,这个依赖在数据改变时会执行回调。 532 | -------------------------------------------------------------------------------- /src/彻底搞懂Vue中keep-alive的魔法-下.md: -------------------------------------------------------------------------------- 1 | > 上一节,我们对```keep-alive```组件的初始渲染流程以及组件的配置信息进行了源码分析。初始渲染流程最关键的一步是对渲染的组件```Vnode```进行缓存,其中也包括了组件的真实节点存储。有了第一次的缓存,当再次渲染组件时,```keep-alive```又拥有哪些魔法呢?接下来我们将彻底揭开这一层面纱。 2 | 3 | ## 13.5 准备工作 4 | 上一节对```keep-alive```组件的分析,是从我画的一个流程图开始的。如果不想回过头看上一节的内容,可以参考以下的简单总结。 5 | 1. `keep-alive`是源码内部定义的组件选项配置,它会先注册为全局组件供开发者全局使用,其中```render```函数定义了它的渲染过程 6 | 2. 和普通组件一致,当父在创建真实节点的过程中,遇到```keep-alive```的组件会进行组件的初始化和实例化。 7 | 3. 实例化会执行挂载```$mount```的过程,这一步会执行```keep-alive```选项中的```render```函数。 8 | 4. `render`函数在初始渲染时,会将渲染的子```Vnode```进行缓存。同时**对应的子真实节点也会被缓存起来**。 9 | 10 | 那么,当再次需要渲染到已经被渲染过的组件时,```keep-alive```的处理又有什么不同呢? 11 | 12 | ### 13.5.1 基础使用 13 | 为了文章的完整性,我依旧把基础的使用展示出来,其中加入了生命周期的使用,方便后续对```keep-alive```生命周期的分析。 14 | ```html 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | ``` 24 | ```js 25 | var child1 = { 26 | template: '

{{num}}

', 27 | data() { 28 | return { 29 | num: 1 30 | } 31 | }, 32 | methods: { 33 | add() { 34 | this.num++ 35 | } 36 | }, 37 | mounted() { 38 | console.log('child1 mounted') 39 | }, 40 | activated() { 41 | console.log('child1 activated') 42 | }, 43 | deactivated() { 44 | console.log('child1 deactivated') 45 | }, 46 | destoryed() { 47 | console.log('child1 destoryed') 48 | } 49 | } 50 | var child2 = { 51 | template: '
child2
', 52 | mounted() { 53 | console.log('child2 mounted') 54 | }, 55 | activated() { 56 | console.log('child2 activated') 57 | }, 58 | deactivated() { 59 | console.log('child2 deactivated') 60 | }, 61 | destoryed() { 62 | console.log('child2 destoryed') 63 | } 64 | } 65 | 66 | var vm = new Vue({ 67 | el: '#app', 68 | components: { 69 | child1, 70 | child2, 71 | }, 72 | data() { 73 | return { 74 | chooseTabs: 'child1', 75 | } 76 | }, 77 | methods: { 78 | changeTabs(tab) { 79 | this.chooseTabs = tab; 80 | } 81 | } 82 | }) 83 | ``` 84 | ### 13.5.2 流程图 85 | 和首次渲染的分析一致,再次渲染的过程我依旧画了一个简单的流程图。 86 | 87 | ![](./img/13.3.png) 88 | 89 | ## 13.6 流程分析 90 | ### 13.6.1 重新渲染组件 91 | 再次渲染的流程从数据改变说起,在这个例子中,动态组件中```chooseTabs```数据的变化会引起依赖派发更新的过程(这个系列有三篇文章详细介绍了vue响应式系统的底层实现,感兴趣的同学可以借鉴)。简单来说,```chooseTabs```这个数据在初始化阶段会收集使用到该数据的相关依赖。当数据发生改变时,收集过的依赖会进行派发更新操作。 92 | 93 | 其中,父组件中负责实例挂载的过程作为依赖会被执行,即执行父组件的```vm._update(vm._render(), hydrating);```。```_render```和```_update```分别代表两个过程,其中```_render```函数会根据数据的变化为组件生成新的```Vnode```节点,而```_update```最终会为新的```Vnode```生成真实的节点。而在生成真实节点的过程中,会利用```vitrual dom```的```diff```算法对前后```vnode```节点进行对比,使之尽可能少的更改真实节点,这一部分内容可以回顾[深入剖析Vue源码 - 来,跟我一起实现diff算法!](https://juejin.im/post/5d3967a56fb9a07efc49cca1),里面详细阐述了利用```diff```算法进行节点差异对比的思路。 94 | 95 | `patch`是新旧```Vnode```节点对比的过程,而```patchVnode```是其中核心的步骤,我们忽略```patchVnode```其他的流程,关注到其中对子组件执行```prepatch```钩子的过程中。 96 | 97 | ```js 98 | function patchVnode (oldVnode,vnode,insertedVnodeQueue,ownerArray,index,removeOnly) { 99 | ··· 100 | // 新vnode 执行prepatch钩子 101 | if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) { 102 | i(oldVnode, vnode); 103 | } 104 | ··· 105 | } 106 | ``` 107 | 执行```prepatch```钩子时会拿到新旧组件的实例并执行```updateChildComponent```函数。而```updateChildComponent```会对针对新的组件实例对旧实例进行状态的更新,包括```props,listeners```等,最终会**调用```vue```提供的全局```vm.$forceUpdate()```方法进行实例的重新渲染。** 108 | 109 | ```js 110 | var componentVNodeHooks = { 111 | // 之前分析的init钩子 112 | init: function() {}, 113 | prepatch: function prepatch (oldVnode, vnode) { 114 | // 新组件实例 115 | var options = vnode.componentOptions; 116 | // 旧组件实例 117 | var child = vnode.componentInstance = oldVnode.componentInstance; 118 | updateChildComponent( 119 | child, 120 | options.propsData, // updated props 121 | options.listeners, // updated listeners 122 | vnode, // new parent vnode 123 | options.children // new children 124 | ); 125 | }, 126 | } 127 | 128 | function updateChildComponent() { 129 | // 更新旧的状态,不分析这个过程 130 | ··· 131 | // 迫使实例重新渲染。 132 | vm.$forceUpdate(); 133 | } 134 | ``` 135 | 136 | 先看看```$forceUpdate```做了什么操作。```$forceUpdate```是源码对外暴露的一个api,他们迫使```Vue```实例重新渲染,本质上是执行实例所收集的依赖,在例子中```watcher```对应的是```keep-alive```的```vm._update(vm._render(), hydrating);```过程。 137 | ```js 138 | Vue.prototype.$forceUpdate = function () { 139 | var vm = this; 140 | if (vm._watcher) { 141 | vm._watcher.update(); 142 | } 143 | }; 144 | ``` 145 | 146 | ### 13.6.2 重用缓存组件 147 | 由于```vm.$forceUpdate()```会强迫```keep-alive```组件进行重新渲染,因此```keep-alive```组件会再一次执行```render```过程。这一次由于第一次对```vnode```的缓存,```keep-alive```在实例的```cache```对象中找到了缓存的组件。 148 | 149 | ```js 150 | // keepalive组件选项 151 | var keepAlive = { 152 | name: 'keep-alive', 153 | abstract: true, 154 | render: function render () { 155 | // 拿到keep-alive下插槽的值 156 | var slot = this.$slots.default; 157 | // 第一个vnode节点 158 | var vnode = getFirstComponentChild(slot); 159 | // 拿到第一个组件实例 160 | var componentOptions = vnode && vnode.componentOptions; 161 | // keep-alive的第一个子组件实例存在 162 | if (componentOptions) { 163 | // check pattern 164 | //拿到第一个vnode节点的name 165 | var name = getComponentName(componentOptions); 166 | var ref = this; 167 | var include = ref.include; 168 | var exclude = ref.exclude; 169 | // 通过判断子组件是否满足缓存匹配 170 | if ( 171 | // not included 172 | (include && (!name || !matches(include, name))) || 173 | // excluded 174 | (exclude && name && matches(exclude, name)) 175 | ) { 176 | return vnode 177 | } 178 | 179 | var ref$1 = this; 180 | var cache = ref$1.cache; 181 | var keys = ref$1.keys; 182 | var key = vnode.key == null ? componentOptions.Ctor.cid + (componentOptions.tag ? ("::" + (componentOptions.tag)) : '') 183 | : vnode.key; 184 | // ==== 关注点在这里 ==== 185 | if (cache[key]) { 186 | // 直接取出缓存组件 187 | vnode.componentInstance = cache[key].componentInstance; 188 | // keys命中的组件名移到数组末端 189 | remove(keys, key); 190 | keys.push(key); 191 | } else { 192 | // 初次渲染时,将vnode缓存 193 | cache[key] = vnode; 194 | keys.push(key); 195 | // prune oldest entry 196 | if (this.max && keys.length > parseInt(this.max)) { 197 | pruneCacheEntry(cache, keys[0], keys, this._vnode); 198 | } 199 | } 200 | 201 | vnode.data.keepAlive = true; 202 | } 203 | return vnode || (slot && slot[0]) 204 | } 205 | } 206 | 207 | ``` 208 | `render`函数前面逻辑可以参考前一篇文章,由于```cache```对象中存储了再次使用的```vnode```对象,所以直接通过```cache[key]```取出缓存的组件实例并赋值给```vnode```的```componentInstance```属性。可能在读到这里的时候,会对源码中```keys```这个数组的作用,以及```pruneCacheEntry```的功能有疑惑,这里我们放到文章末尾讲缓存优化策略时解答。 209 | 210 | 211 | ### 13.6.3 真实节点的替换 212 | 213 | 执行了```keep-alive```组件的```_render```过程,接下来是```_update```产生真实的节点,同样的,```keep-alive```下有```child1```子组件,所以```_update```过程会调用```createComponent```递归创建子组件```vnode```,这个过程在初次渲染时也有分析过,我们可以对比一下,再次渲染时流程有哪些不同。 214 | 215 | ```js 216 | function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) { 217 | // vnode为缓存的vnode 218 | var i = vnode.data; 219 | if (isDef(i)) { 220 | // 此时isReactivated为true 221 | var isReactivated = isDef(vnode.componentInstance) && i.keepAlive; 222 | if (isDef(i = i.hook) && isDef(i = i.init)) { 223 | i(vnode, false /* hydrating */); 224 | } 225 | if (isDef(vnode.componentInstance)) { 226 | // 其中一个作用是保留真实dom到vnode中 227 | initComponent(vnode, insertedVnodeQueue); 228 | insert(parentElm, vnode.elm, refElm); 229 | if (isTrue(isReactivated)) { 230 | reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm); 231 | } 232 | return true 233 | } 234 | } 235 | } 236 | ``` 237 | **此时的```vnode```是缓存取出的子组件```vnode```**,并且由于在第一次渲染时对组件进行了标记```vnode.data.keepAlive = true;```,所以```isReactivated```的值为```true```,```i.init```依旧会执行子组件的初始化过程。但是这个过程由于有缓存,所以执行过程也不完全相同。 238 | 239 | ```js 240 | var componentVNodeHooks = { 241 | init: function init (vnode, hydrating) { 242 | if ( 243 | vnode.componentInstance && 244 | !vnode.componentInstance._isDestroyed && 245 | vnode.data.keepAlive 246 | ) { 247 | // 当有keepAlive标志时,执行prepatch钩子 248 | var mountedNode = vnode; // work around flow 249 | componentVNodeHooks.prepatch(mountedNode, mountedNode); 250 | } else { 251 | var child = vnode.componentInstance = createComponentInstanceForVnode( 252 | vnode, 253 | activeInstance 254 | ); 255 | child.$mount(hydrating ? vnode.elm : undefined, hydrating); 256 | } 257 | }, 258 | } 259 | ``` 260 | 显然因为有```keepAlive```的标志,所以子组件不再走挂载流程,只是执行```prepatch```钩子对组件状态进行更新。并且很好的利用了缓存```vnode```之前保留的真实节点进行节点的替换。 261 | 262 | 263 | ## 13.7 生命周期 264 | 我们通过例子来观察```keep-alive```生命周期和普通组件的不同。 265 | 266 | ![](./img/13.4.gif) 267 | 268 | 在我们从```child1```切换到```child2```,再切回```child1```过程中,```chil1```不会再执行```mounted```钩子,只会执行```activated```钩子,而```child2```也不会执行```destoryed```钩子,只会执行```deactivated```钩子,这是为什么?```child2```的```deactivated```钩子又要比```child1```的```activated```提前执行,这又是为什么? 269 | 270 | ### 13.7.1 deactivated 271 | 我们先从组件的销毁开始说起,当```child1```切换到```child2```时,```child1```会执行```deactivated```钩子而不是```destoryed```钩子,这是为什么? 272 | 前面分析```patch```过程会对新旧节点的改变进行对比,从而尽可能范围小的去操作真实节点,当完成```diff```算法并对节点操作完毕后,接下来还有一个重要的步骤是**对旧的组件执行销毁移除操作**。这一步的代码如下: 273 | 274 | ```js 275 | function patch(···) { 276 | // 分析过的patchVnode过程 277 | // 销毁旧节点 278 | if (isDef(parentElm)) { 279 | removeVnodes(parentElm, [oldVnode], 0, 0); 280 | } else if (isDef(oldVnode.tag)) { 281 | invokeDestroyHook(oldVnode); 282 | } 283 | } 284 | 285 | function removeVnodes (parentElm, vnodes, startIdx, endIdx) { 286 | // startIdx,endIdx都为0 287 | for (; startIdx <= endIdx; ++startIdx) { 288 | // ch 会拿到需要销毁的组件 289 | var ch = vnodes[startIdx]; 290 | if (isDef(ch)) { 291 | if (isDef(ch.tag)) { 292 | // 真实节点的移除操作 293 | removeAndInvokeRemoveHook(ch); 294 | invokeDestroyHook(ch); 295 | } else { // Text node 296 | removeNode(ch.elm); 297 | } 298 | } 299 | } 300 | } 301 | ``` 302 | 303 | `removeAndInvokeRemoveHook`会对旧的节点进行移除操作,其中关键的一步是会将真实节点从父元素中删除,有兴趣可以自行查看这部分逻辑。```invokeDestroyHook```是执行销毁组件钩子的核心。如果该组件下存在子组件,会递归去调用```invokeDestroyHook```执行销毁操作。销毁过程会执行组件内部的```destory```钩子。 304 | ```js 305 | function invokeDestroyHook (vnode) { 306 | var i, j; 307 | var data = vnode.data; 308 | if (isDef(data)) { 309 | if (isDef(i = data.hook) && isDef(i = i.destroy)) { i(vnode); } 310 | // 执行组件内部destroy钩子 311 | for (i = 0; i < cbs.destroy.length; ++i) { cbs.destroy[i](vnode); } 312 | } 313 | // 如果组件存在子组件,则遍历子组件去递归调用invokeDestoryHook执行钩子 314 | if (isDef(i = vnode.children)) { 315 | for (j = 0; j < vnode.children.length; ++j) { 316 | invokeDestroyHook(vnode.children[j]); 317 | } 318 | } 319 | } 320 | ``` 321 | 组件内部钩子前面已经介绍了```init```和```prepatch```钩子,而```destroy```钩子的逻辑更加简单。 322 | ```js 323 | var componentVNodeHooks = { 324 | destroy: function destroy (vnode) { 325 | // 组件实例 326 | var componentInstance = vnode.componentInstance; 327 | // 如果实例还未被销毁 328 | if (!componentInstance._isDestroyed) { 329 | // 不是keep-alive组件则执行销毁操作 330 | if (!vnode.data.keepAlive) { 331 | componentInstance.$destroy(); 332 | } else { 333 | // 如果是已经缓存的组件 334 | deactivateChildComponent(componentInstance, true /* direct */); 335 | } 336 | } 337 | } 338 | } 339 | ``` 340 | 当组件是```keep-alive```缓存过的组件,即已经用```keepAlive```标记过,则不会执行实例的销毁,即```componentInstance.$destroy()```的过程。```$destroy```过程会做一系列的组件销毁操作,其中的```beforeDestroy,destoryed```钩子也是在```$destory```过程中调用,而```deactivateChildComponent```的处理过程却完全不同。 341 | 342 | ```js 343 | function deactivateChildComponent (vm, direct) { 344 | if (direct) { 345 | // 346 | vm._directInactive = true; 347 | if (isInInactiveTree(vm)) { 348 | return 349 | } 350 | } 351 | if (!vm._inactive) { 352 | // 已经被停用 353 | vm._inactive = true; 354 | // 对子组件同样会执行停用处理 355 | for (var i = 0; i < vm.$children.length; i++) { 356 | deactivateChildComponent(vm.$children[i]); 357 | } 358 | // 最终调用deactivated钩子 359 | callHook(vm, 'deactivated'); 360 | } 361 | } 362 | ``` 363 | `_directInactive`是用来标记这个被打上停用标签的组件是否是最顶层的组件。而```_inactive```是停用的标志,同样的子组件也需要递归去调用```deactivateChildComponent```,打上停用的标记。**最终会执行用户定义的```deactivated```钩子。** 364 | 365 | ### 13.7.2 activated 366 | 现在回过头看看```activated```的执行时机,同样是```patch```过程,在对旧节点移除并执行销毁或者停用的钩子后,对新节点也会执行相应的钩子。**这也是停用的钩子比启用的钩子先执行的原因。** 367 | ```js 368 | function patch(···) { 369 | // patchVnode过程 370 | // 销毁旧节点 371 | { 372 | if (isDef(parentElm)) { 373 | removeVnodes(parentElm, [oldVnode], 0, 0); 374 | } else if (isDef(oldVnode.tag)) { 375 | invokeDestroyHook(oldVnode); 376 | } 377 | } 378 | // 执行组件内部的insert钩子 379 | invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch); 380 | } 381 | 382 | function invokeInsertHook (vnode, queue, initial) { 383 | // delay insert hooks for component root nodes, invoke them after the 384 | // 当节点已经被插入时,会延迟执行insert钩子 385 | if (isTrue(initial) && isDef(vnode.parent)) { 386 | vnode.parent.data.pendingInsert = queue; 387 | } else { 388 | for (var i = 0; i < queue.length; ++i) { 389 | queue[i].data.hook.insert(queue[i]); 390 | } 391 | } 392 | } 393 | ``` 394 | 同样的组件内部的```insert```钩子逻辑如下: 395 | ```js 396 | // 组件内部自带钩子 397 | var componentVNodeHooks = { 398 | insert: function insert (vnode) { 399 | var context = vnode.context; 400 | var componentInstance = vnode.componentInstance; 401 | // 实例已经被挂载 402 | if (!componentInstance._isMounted) { 403 | componentInstance._isMounted = true; 404 | callHook(componentInstance, 'mounted'); 405 | } 406 | if (vnode.data.keepAlive) { 407 | if (context._isMounted) { 408 | // vue-router#1212 409 | // During updates, a kept-alive component's child components may 410 | // change, so directly walking the tree here may call activated hooks 411 | // on incorrect children. Instead we push them into a queue which will 412 | // be processed after the whole patch process ended. 413 | queueActivatedComponent(componentInstance); 414 | } else { 415 | activateChildComponent(componentInstance, true /* direct */); 416 | } 417 | } 418 | }, 419 | } 420 | ``` 421 | 当第一次实例化组件时,由于实例的```_isMounted```不存在,所以会调用```mounted```钩子,当我们从```child2```再次切回```child1```时,由于```child1```只是被停用而没有被销毁,所以不会再调用```mounted```钩子,此时会执行```activateChildComponent```函数对组件的状态进行处理。有了分析```deactivateChildComponent```的基础,```activateChildComponent```的逻辑也很好理解,同样的```_inactive```标记为已启用,并且对子组件递归调用```activateChildComponent```做状态处理。 422 | ```js 423 | function activateChildComponent (vm, direct) { 424 | if (direct) { 425 | vm._directInactive = false; 426 | if (isInInactiveTree(vm)) { 427 | return 428 | } 429 | } else if (vm._directInactive) { 430 | return 431 | } 432 | if (vm._inactive || vm._inactive === null) { 433 | vm._inactive = false; 434 | for (var i = 0; i < vm.$children.length; i++) { 435 | activateChildComponent(vm.$children[i]); 436 | } 437 | callHook(vm, 'activated'); 438 | } 439 | } 440 | ``` 441 | 442 | ## 13.8 缓存优化 - LRU 443 | 程序的内存空间是有限的,所以我们无法无节制的对数据进行存储,这时候需要有策略去淘汰不那么重要的数据,保持最大数据存储量的一致。这种类型的策略称为缓存优化策略,根据淘汰的机制不同,常用的有以下三类。 444 | 445 | **1.FIFO: 先进先出策略,我们通过记录数据使用的时间,当缓存大小即将溢出时,优先清除离当前时间最远的数据。** 446 | 447 | **2.LRU: 最近最少使用。LRU策略遵循的原则是,如果数据最近被访问(使用)过,那么将来被访问的几率会更高,如果以一个数组去记录数据,当有一数据被访问时,该数据会被移动到数组的末尾,表明最近被使用过,当缓存溢出时,会删除数组的头部数据,即将最不频繁使用的数据移除。** 448 | 449 | **3.LFU: 计数最少策略。用次数去标记数据使用频率,次数最少的会在缓存溢出时被淘汰。** 450 | 451 | 452 | 这三种缓存算法各有优劣,各自适用不同场景,而我们看```keep-alive```在缓存时的优化处理,很明显利用了```LRU```的缓存策略。我们看关键的代码 453 | ```js 454 | var keepAlive = { 455 | render: function() { 456 | ··· 457 | if (cache[key]) { 458 | vnode.componentInstance = cache[key].componentInstance; 459 | remove(keys, key); 460 | keys.push(key); 461 | } else { 462 | cache[key] = vnode; 463 | keys.push(key); 464 | if (this.max && keys.length > parseInt(this.max)) { 465 | pruneCacheEntry(cache, keys[0], keys, this._vnode); 466 | } 467 | } 468 | } 469 | } 470 | 471 | function remove (arr, item) { 472 | if (arr.length) { 473 | var index = arr.indexOf(item); 474 | if (index > -1) { 475 | return arr.splice(index, 1) 476 | } 477 | } 478 | } 479 | ``` 480 | 结合一个实际的例子分析缓存逻辑的实现。 481 | 1.有三个组件```child1,child2,child3```,```keep-alive```的最大缓存个数设置为2 482 | 2.用```cache```对象去存储组件```vnode```,```key```为组件名字,```value```为组件```vnode```对象,用```keys```数组去记录组件名字,由于是数组,所以```keys```为有序。 483 | 3.`child1,child2`组件依次访问,缓存结果为 484 | ```js 485 | keys = ['child1', 'child2'] 486 | cache = { 487 | child1: child1Vnode, 488 | child2: child2Vnode 489 | } 490 | ``` 491 | 4.再次访问到```child1```组件,由于命中了缓存,会调用```remove```方法把```keys```中的```child1```删除,并通过数组的```push```方法将```child1```推到尾部。缓存结果修改为 492 | ```js 493 | keys = ['child2', 'child1'] 494 | cache = { 495 | child1: child1Vnode, 496 | child2: child2Vnode 497 | } 498 | ``` 499 | 5.访问到```child3```时,由于缓存个数限制,初次缓存会执行```pruneCacheEntry```方法对最少访问到的数据进行删除。```pruneCacheEntry```的定义如下 500 | ```js 501 | function pruneCacheEntry (cache,key,keys,current) { 502 | var cached###1 = cache[key]; 503 | // 销毁实例 504 | if (cached###1 && (!current || cached###1.tag !== current.tag)) { 505 | cached###1.componentInstance.$destroy(); 506 | } 507 | cache[key] = null; 508 | remove(keys, key); 509 | } 510 | 511 | ``` 512 | 删除缓存时会把```keys[0]```代表的组件删除,由于之前的处理,最近被访问到的元素会位于数组的尾部,所以头部的数据往往是最少访问的,因此会优先删除头部的元素。并且会再次调用```remove```方法,将```keys```的首个元素删除。 513 | 514 | 这就是```vue```中对```keep-alive```缓存处理的优化过程。 515 | -------------------------------------------------------------------------------- /src/你真的了解v-model的语法糖了吗.md: -------------------------------------------------------------------------------- 1 | > 双向数据绑定这个概念或者大家并不陌生,视图影响数据,数据同样影响视图,两者间有双向依赖的关系。在响应式系统构建的上,中,下篇我已经对数据影响视图的原理详细阐述清楚了。而如何完成视图影响数据这一关联?这就是本节讨论的重点:指令```v-model```。 2 | 3 | 由于```v-model```和前面介绍的插槽,事件一致,都属于vue提供的指令,所以我们对```v-model```的分析方式和以往大同小异。分析会围绕模板的编译,```render```函数的生成,到最后真实节点的挂载顺序执行。最终我们依然会得到一个结论,**v-model无论什么使用场景,本质上都是一个语法糖**。 4 | 5 | 6 | ## 11.1 表单绑定 7 | ### 11.1.1 基础使用 8 | `v-model`和表单脱离不了关系,之所以视图能影响数据,本质上这个视图需要可交互的,因此表单是实现这一交互的前提。表单的使用以``` 15 | 16 | // 单选框 17 |
18 | one 19 | two 20 |
21 | 22 | // 原生单选框的写法 注:原生单选框的写法需要通过name绑定一组单选,两个radio的name属性相同,才能表现为互斥 23 |
24 | one 25 | two 26 |
27 | 28 | 29 | // 多选框 (原始值: value4: []) 30 |
31 | jack 32 | lili 33 |
34 | 35 | // 下拉选项 36 | 41 | 42 | ``` 43 | 接下来的分析,我们以普通输入框为例 44 | ```js 45 |
46 | 47 |
48 | 49 | new Vue({ 50 | el: '#app', 51 | data() { 52 | return { 53 | value1: '' 54 | } 55 | } 56 | }) 57 | ``` 58 | 进入正文前先回顾一下模板到真实节点的过程。 59 | 1. 模板解析成```AST```树; 60 | 2. ```AST```树生成可执行的```render```函数; 61 | 3. ```render```函数转换为```Vnode```对象; 62 | 4. 根据```Vnode```对象生成真实的```Dom```节点。 63 | 64 | 接下来,我们先看看模板解析为```AST```树的过程。 65 | 66 | ### 11.1.2 AST树的解析 67 | 模板的编译阶段,会调用```var ast = parse(template.trim(), options)```生成```AST```树,```parse```函数的其他细节这里不展开分析,前面的文章或多或少都涉及过,我们还是把关注点放在模板属性上的解析,也就是```processAttrs```函数上。 68 | 69 | 使用过```vue```写模板的都知道,```vue```模板属性由两部分组成,一部分是指令,另一部分是普通```html```标签属性。z这也是属性处理的两大分支。而在指令的细分领域,又将```v-on,v-bind```做特殊的处理,其他的普通分支会执行```addDirective```过程。 70 | ```js 71 | // 处理模板属性 72 | function processAttrs(el) { 73 | var list = el.attrsList; 74 | var i, l, name, rawName, value, modifiers, syncGen, isDynamic; 75 | for (i = 0, l = list.length; i < l; i++) { 76 | name = rawName = list[i].name; // v-on:click 77 | value = list[i].value; // doThis 78 | if (dirRE.test(name)) { // 1.针对指令的属性处理 79 | ··· 80 | if (bindRE.test(name)) { // v-bind分支 81 | ··· 82 | } else if(onRE.test(name)) { // v-on分支 83 | ··· 84 | } else { // 除了v-bind,v-on之外的普通指令 85 | ··· 86 | // 普通指令会在AST树上添加directives属性 87 | addDirective(el, name, rawName, value, arg, isDynamic, modifiers, list[i]); 88 | if (name === 'model') { 89 | checkForAliasModel(el, value); 90 | } 91 | } 92 | } else { 93 | // 2. 普通html标签属性 94 | } 95 | 96 | } 97 | } 98 | ``` 99 | 在揭秘事件机制这一节,我们介绍了```AST```产生阶段对事件指令```v-on```的处理是为```AST```树添加```events```属性。类似的,普通指令会在```AST```树上添加```directives```属性,具体看```addDirective```函数。 100 | 101 | ```js 102 | // 添加directives属性 103 | function addDirective (el,name,rawName,value,arg,isDynamicArg,modifiers,range) { 104 | (el.directives || (el.directives = [])).push(rangeSetItem({ 105 | name: name, 106 | rawName: rawName, 107 | value: value, 108 | arg: arg, 109 | isDynamicArg: isDynamicArg, 110 | modifiers: modifiers 111 | }, range)); 112 | el.plain = false; 113 | } 114 | ``` 115 | 最终```AST```树多了一个属性对象,其中```modifiers```代表模板中添加的修饰符,如:```.lazy, .number, .trim```。 116 | ```js 117 | // AST 118 | { 119 | directives: { 120 | { 121 | rawName: 'v-model', 122 | value: 'value', 123 | name: 'v-model', 124 | modifiers: undefined 125 | } 126 | } 127 | } 128 | ``` 129 | ### 11.1.3 render函数生成 130 | 131 | `render`函数生成阶段,也就是前面分析了数次的```generate```逻辑,其中```genData```会对模板的诸多属性进行处理,最终返回拼接好的字符串模板,而对指令的处理会进入```genDirectives```流程。 132 | ```js 133 | function genData(el, state) { 134 | var data = '{'; 135 | // 指令的处理 136 | var dirs = genDirectives(el, state); 137 | ··· // 其他属性,指令的处理 138 | // 针对组件的v-model处理,放到后面分析 139 | if (el.model) { 140 | data += "model:{value:" + (el.model.value) + ",callback:" + (el.model.callback) + ",expression:" + (el.model.expression) + "},"; 141 | } 142 | return data 143 | } 144 | ``` 145 | `genDirectives`逻辑并不复杂,他会拿到之前```AST```树中保留的```directives```对象,并遍历解析指令对象,最终以```'directives:['```包裹的字符串返回。 146 | ```js 147 | // directives render字符串的生成 148 | function genDirectives (el, state) { 149 | // 拿到指令对象 150 | var dirs = el.directives; 151 | if (!dirs) { return } 152 | // 字符串拼接 153 | var res = 'directives:['; 154 | var hasRuntime = false; 155 | var i, l, dir, needRuntime; 156 | for (i = 0, l = dirs.length; i < l; i++) { 157 | dir = dirs[i]; 158 | needRuntime = true; 159 | // 对指令ast树的重新处理 160 | var gen = state.directives[dir.name]; 161 | if (gen) { 162 | // compile-time directive that manipulates AST. 163 | // returns true if it also needs a runtime counterpart. 164 | needRuntime = !!gen(el, dir, state.warn); 165 | } 166 | if (needRuntime) { 167 | hasRuntime = true; 168 | res += "{name:\"" + (dir.name) + "\",rawName:\"" + (dir.rawName) + "\"" + (dir.value ? (",value:(" + (dir.value) + "),expression:" + (JSON.stringify(dir.value))) : '') + (dir.arg ? (",arg:" + (dir.isDynamicArg ? dir.arg : ("\"" + (dir.arg) + "\""))) : '') + (dir.modifiers ? (",modifiers:" + (JSON.stringify(dir.modifiers))) : '') + "},"; 169 | } 170 | } 171 | if (hasRuntime) { 172 | return res.slice(0, -1) + ']' 173 | } 174 | } 175 | ``` 176 | 这里有一句关键的代码```var gen = state.directives[dir.name]```,为了了解其来龙去脉,我们回到Vue源码中的编译流程,在以往的文章中,我们完整的介绍过```template```模板的编译流程,这一部分的设计是非常复杂且巧妙的,其中大量运用了偏函数的思想,即分离了不同平台不同的编译过程,也为同一个平台每次提供相同的配置选项进行了合并处理,并很好的将配置进行了缓存。其中针对浏览器端有三个重要的指令选项。 177 | ```js 178 | var directive$1 = { 179 | model: model, 180 | text: text, 181 | html, html 182 | } 183 | var baseOptions = { 184 | ··· 185 | // 指令选项 186 | directives: directives$1, 187 | }; 188 | // 编译时传入选项配置 189 | createCompiler(baseOptions) 190 | ``` 191 | 而这个```state.directives['model']```也就是对应的```model```函数,所以我们先把焦点聚焦在```model```函数的逻辑。 192 | ```js 193 | function model (el,dir,_warn) { 194 | warn$1 = _warn; 195 | // 绑定的值 196 | var value = dir.value; 197 | var modifiers = dir.modifiers; 198 | var tag = el.tag; 199 | var type = el.attrsMap.type; 200 | { 201 | // 这里遇到type是file的html,如果还使用双向绑定会报出警告。 202 | // 因为File inputs是只读的 203 | if (tag === 'input' && type === 'file') { 204 | warn$1( 205 | "<" + (el.tag) + " v-model=\"" + value + "\" type=\"file\">:\n" + 206 | "File inputs are read only. Use a v-on:change listener instead.", 207 | el.rawAttrsMap['v-model'] 208 | ); 209 | } 210 | } 211 | //组件上v-model的处理 212 | if (el.component) { 213 | genComponentModel(el, value, modifiers); 214 | // component v-model doesn't need extra runtime 215 | return false 216 | } else if (tag === 'select') { 217 | // select表单 218 | genSelect(el, value, modifiers); 219 | } else if (tag === 'input' && type === 'checkbox') { 220 | // checkbox表单 221 | genCheckboxModel(el, value, modifiers); 222 | } else if (tag === 'input' && type === 'radio') { 223 | // radio表单 224 | genRadioModel(el, value, modifiers); 225 | } else if (tag === 'input' || tag === 'textarea') { 226 | // 普通input,如 text, textarea 227 | genDefaultModel(el, value, modifiers); 228 | } else if (!config.isReservedTag(tag)) { 229 | genComponentModel(el, value, modifiers); 230 | // component v-model doesn't need extra runtime 231 | return false 232 | } else { 233 | // 如果不是表单使用v-model,同样会报出警告,双向绑定只针对表单控件。 234 | warn$1( 235 | "<" + (el.tag) + " v-model=\"" + value + "\">: " + 236 | "v-model is not supported on this element type. " + 237 | 'If you are working with contenteditable, it\'s recommended to ' + 238 | 'wrap a library dedicated for that purpose inside a custom component.', 239 | el.rawAttrsMap['v-model'] 240 | ); 241 | } 242 | // ensure runtime directive metadata 243 | // 244 | return true 245 | } 246 | ``` 247 | 显然,**```model```会对表单控件的```AST```树做进一步的处理**,在上面的基础用法中,我们知道**表单有不同的类型,每种类型对应的事件处理响应机制也不同**。因此我们需要针对不同的表单控件生成不同的```render```函数,因此需要产生不同的```AST```属性。```model```针对不同类型的表单控件有不同的处理分支。我们重点分析普通```input```标签的处理,```genDefaultModel```分支,其他类型的分支,可以仿照下面的分析过程。 248 | 249 | 250 | ```js 251 | function genDefaultModel (el,value,modifiers) { 252 | var type = el.attrsMap.type; 253 | 254 | // v-model和v-bind值相同值,有冲突会报错 255 | { 256 | var value$1 = el.attrsMap['v-bind:value'] || el.attrsMap[':value']; 257 | var typeBinding = el.attrsMap['v-bind:type'] || el.attrsMap[':type']; 258 | if (value$1 && !typeBinding) { 259 | var binding = el.attrsMap['v-bind:value'] ? 'v-bind:value' : ':value'; 260 | warn$1( 261 | binding + "=\"" + value$1 + "\" conflicts with v-model on the same element " + 262 | 'because the latter already expands to a value binding internally', 263 | el.rawAttrsMap[binding] 264 | ); 265 | } 266 | } 267 | // modifiers存贮的是v-model的修饰符。 268 | var ref = modifiers || {}; 269 | // lazy,trim,number是可供v-model使用的修饰符 270 | var lazy = ref.lazy; 271 | var number = ref.number; 272 | var trim = ref.trim; 273 | var needCompositionGuard = !lazy && type !== 'range'; 274 | // lazy修饰符将触发同步的事件从input改为change 275 | var event = lazy ? 'change' : type === 'range' ? RANGE_TOKEN : 'input'; 276 | 277 | var valueExpression = '$event.target.value'; 278 | // 过滤用户输入的首尾空白符 279 | if (trim) { 280 | valueExpression = "$event.target.value.trim()"; 281 | } 282 | // 将用户输入转为数值类型 283 | if (number) { 284 | valueExpression = "_n(" + valueExpression + ")"; 285 | } 286 | // genAssignmentCode函数是为了处理v-model的格式,允许使用以下的形式: v-model="a.b" v-model="a[b]" 287 | var code = genAssignmentCode(value, valueExpression); 288 | if (needCompositionGuard) { 289 | // 保证了不会在输入法组合文字过程中得到更新 290 | code = "if($event.target.composing)return;" + code; 291 | } 292 | // 添加value属性 293 | addProp(el, 'value', ("(" + value + ")")); 294 | // 绑定事件 295 | addHandler(el, event, code, null, true); 296 | if (trim || number) { 297 | addHandler(el, 'blur', '$forceUpdate()'); 298 | } 299 | } 300 | 301 | function genAssignmentCode (value,assignment) { 302 | // 处理v-model的格式,v-model="a.b" v-model="a[b]" 303 | var res = parseModel(value); 304 | if (res.key === null) { 305 | // 普通情形 306 | return (value + "=" + assignment) 307 | } else { 308 | // 对象形式 309 | return ("$set(" + (res.exp) + ", " + (res.key) + ", " + assignment + ")") 310 | } 311 | } 312 | ``` 313 | `genDefaultModel`的逻辑有两部分,**一部分是针对修饰符产生不同的事件处理字符串,二是为```v-model```产生的```AST```树添加属性和事件相关的属性**。其中最重要的两行代码是 314 | ```js 315 | // 添加value属性 316 | addProp(el, 'value', ("(" + value + ")")); 317 | // 绑定事件属性 318 | addHandler(el, event, code, null, true); 319 | ``` 320 | `addHandler`在之前介绍事件时分析过,他会为```AST```树添加事件相关的属性,同样的```addProp```也会为```AST```树添加```props```属性。最终```AST```树新增了两个属性: 321 | 322 | ![](./img/11.1.png) 323 | 324 | 325 | 回到```genData```,通过```genDirectives```处理后,原先的```AST```树新增了两个属性,因此在字符串生成阶段同样需要处理```props```和```events```的分支。 326 | ```js 327 | function genData$2 (el, state) { 328 | var data = '{'; 329 | // 已经分析过的genDirectives 330 | var dirs = genDirectives(el, state); 331 | // 处理props 332 | if (el.props) { 333 | data += "domProps:" + (genProps(el.props)) + ","; 334 | } 335 | // 处理事件 336 | if (el.events) { 337 | data += (genHandlers(el.events, false)) + ","; 338 | } 339 | } 340 | ``` 341 | 最终```render```函数的结果为: 342 | 343 | ```js 344 | "_c('input',{directives:[{name:"model",rawName:"v-model",value:(message),expression:"message"}],attrs:{"type":"text"},domProps:{"value":(message)},on:{"input":function($event){if($event.target.composing)return;message=$event.target.value}}})" 345 | 346 | ``` 347 | 348 | ```js 349 | 350 | ``` 351 | 352 | 如果觉得上面的流程分析啰嗦,可以直接看下面的结论,对比模板和生成的```render```函数,我们可以得到: 353 | 354 | 1. `input`标签所有属性,包括指令相关的内容都是以```data```属性的形式作为参数的整体传入```_c(即:createElement)```函数。 355 | 2. `input type`的类型,在```data```属性中,以```attrs```键值对存在。 356 | 3. `v-model`会有对应的```directives```属性描述指令的相关信息。 357 | 4. **为什么说```v-model```是一个语法糖,从```render```函数的最终结果可以看出,它最终以两部分形式存在于```input```标签中,一个是将```value1```以```props```的形式存在(```domProps```)中,另一个是以事件的形式存储```input```事件,并保留在```on```属性中。** 358 | 5. 重要的一个关键,事件用```$event.target.composing```属性来保证不会在输入法组合文字过程中更新数据,这点我们后面会再次提到。 359 | 360 | 361 | ### 11.1.4 patch真实节点 362 | 363 | 在```patch```之前还有一个生成```vnode```的过程,这个过程没有什么特别之处,所有的包括指令,属性会以```data```属性的形式传递到构造函数```Vnode```中,最终的```Vnode```拥有```directives,domProps,on```属性: 364 | 365 | ![](./img/11.2.png) 366 | 367 | 有了```Vnode```之后紧接着会执行```patchVnode```,```patchVnode```过程是一个真实节点创建的过程,其中的关键是```createElm```方法,这个方法我们在不同的场合也分析过,前面的源码得到指令相关的信息也会保留在```vnode```的```data```属性里,所以对属性的处理也会走```invokeCreateHooks```逻辑。 368 | 369 | ```js 370 | function createElm() { 371 | ··· 372 | // 针对指令的处理 373 | if (isDef(data)) { 374 | invokeCreateHooks(vnode, insertedVnodeQueue); 375 | } 376 | } 377 | ``` 378 | `invokeCreateHooks`会调用定义好的钩子函数,对```vnode```上定义的属性,指令,事件等进行真实DOM的处理,步骤包括以下(不包含全部): 379 | 1. `updateDOMProps`会利用```vnode data```上的```domProps```更新```input```标签的```value```值; 380 | 2. `updateAttrs`会利用```vnode data```上的```attrs```属性更新节点的属性值; 381 | 3. `updateDomListeners`利用```vnode data```上的```on```属性添加事件监听。 382 | 383 | **因此```v-model```语法糖最终反应的结果,是通过监听表单控件自身的```input```事件(其他类型有不同的监听事件类型),去影响自身的```value```值**。如果没有```v-model```的语法糖,我们可以这样写: 384 | `` 385 | 386 | 387 | ### 11.1.5 语法糖的背后 388 | 389 | **然而```v-model```仅仅是起到合并语法,创建一个新的语法糖的意义吗?** 390 | **显然答案是否定的,对于需要使用输入法 (如中文、日文、韩文等) 的语言,你会发现 ```v-model``` 不会在输入法组合文字过程中得到更新。**这就是```v-model```的一个重要的特点。它会在事件处理这一层添加新的事件监听```compositionstart,compositionend```,他们会分别在语言输入的开始和结束时监听到变化,只要借助```$event.target.composing```,就可以设计出只会在输入法组合文字的结束阶段才更新数据,这有利于提高用户的使用体验。这一部分我想借助脱离框架的表单来帮助理解。 391 | 392 | 393 | 脱离框架的一个视图响应数据的实现(效果类似于v-model): 394 | ```js 395 | // html 396 | 397 | 398 | 399 | // js 400 | 401 | 427 | ``` 428 | 429 | 430 | ## 11.2 组件使用v-model 431 | 最后我们简单说说在父组件中使用```v-model```,可以先看结论,**组件上使用```v-model```本质上是子父组件通信的语法糖**。先看一个简单的使用例子。 432 | 433 | ```js 434 | var child = { 435 | template: '
{{value}}
', 436 | methods: { 437 | emitEvent(e) { 438 | this.$emit('input', e.target.value) 439 | } 440 | }, 441 | props: ['value'] 442 | } 443 | new Vue({ 444 | data() { 445 | return { 446 | message: 'test' 447 | } 448 | }, 449 | components: { 450 | child 451 | }, 452 | template: '
', 453 | el: '#app' 454 | }) 455 | ``` 456 | 父组件上使用```v-model```, 子组件默认会利用名为 ```value``` 的 ```prop``` 和名为 ```input``` 的事件,当然像```select```表单会以其他默认事件的形式存在。分析源码的过程也大致类似,这里只列举几个特别的地方。 457 | 458 | `AST`生成阶段和普通表单控件的区别在于,当遇到```child```时,由于不是普通的```html```标签,会执行```getComponentModel```的过程,而```getComponentModel```的结果是在```AST```树上添加```model```的属性。 459 | ```js 460 | function model() { 461 | if (!config.isReservedTag(tag)) { 462 | genComponentModel(el, value, modifiers); 463 | } 464 | } 465 | 466 | function genComponentModel (el,value,modifiers) { 467 | var ref = modifiers || {}; 468 | var number = ref.number; 469 | var trim = ref.trim; 470 | 471 | var baseValueExpression = '$$v'; 472 | var valueExpression = baseValueExpression; 473 | if (trim) { 474 | valueExpression = 475 | "(typeof " + baseValueExpression + " === 'string'" + 476 | "? " + baseValueExpression + ".trim()" + 477 | ": " + baseValueExpression + ")"; 478 | } 479 | if (number) { 480 | valueExpression = "_n(" + valueExpression + ")"; 481 | } 482 | var assignment = genAssignmentCode(value, valueExpression); 483 | // 在ast树上添加model属性,其中有value,expression,callback属性 484 | el.model = { 485 | value: ("(" + value + ")"), 486 | expression: JSON.stringify(value), 487 | callback: ("function (" + baseValueExpression + ") {" + assignment + "}") 488 | }; 489 | } 490 | ``` 491 | 最终```AST```树的结果: 492 | ```js 493 | { 494 | model: { 495 | callback: "function ($$v) {message=$$v}" 496 | expression: ""message"" 497 | value: "(message)" 498 | } 499 | } 500 | ``` 501 | 经过对```AST```树的处理后,回到```genData$2```的流程,由于有了```model```属性,父组件拼接的字符串会做进一步处理。 502 | ```js 503 | function genData$2 (el, state) { 504 | var data = '{'; 505 | var dirs = genDirectives(el, state); 506 | ··· 507 | // v-model组件的render函数处理 508 | if (el.model) { 509 | data += "model:{value:" + (el.model.value) + ",callback:" + (el.model.callback) + ",expression:" + (el.model.expression) + "},"; 510 | } 511 | ··· 512 | return data 513 | } 514 | ``` 515 | 因此,父组件最终的```render```函数表现为: 516 | ```js 517 | "_c('child',{model:{value:(message),callback:function ($$v) {message=$$v},expression:"message"}})" 518 | ``` 519 | 520 | 子组件的创建阶段照例会执行```createComponent ```,其中针对```model```的逻辑需要特别说明。 521 | 522 | ```js 523 | function createComponent() { 524 | // transform component v-model data into props & events 525 | if (isDef(data.model)) { 526 | // 处理父组件的v-model指令对象 527 | transformModel(Ctor.options, data); 528 | } 529 | } 530 | ``` 531 | 532 | ```js 533 | function transformModel (options, data) { 534 | // prop默认取的是value,除非配置上有model的选项 535 | var prop = (options.model && options.model.prop) || 'value'; 536 | 537 | // event默认取的是input,除非配置上有model的选项 538 | var event = (options.model && options.model.event) || 'input' 539 | // vnode上新增props的属性,值为value 540 | ;(data.attrs || (data.attrs = {}))[prop] = data.model.value; 541 | 542 | // vnode上新增on属性,标记事件 543 | var on = data.on || (data.on = {}); 544 | var existing = on[event]; 545 | var callback = data.model.callback; 546 | if (isDef(existing)) { 547 | if ( 548 | Array.isArray(existing) 549 | ? existing.indexOf(callback) === -1 550 | : existing !== callback 551 | ) { 552 | on[event] = [callback].concat(existing); 553 | } 554 | } else { 555 | on[event] = callback; 556 | } 557 | } 558 | ``` 559 | 560 | 从```transformModel```的逻辑可以看出,子组件```vnode```会为```data.props``` 添加 ```data.model.value```,并且给```data.on``` 添加```data.model.callback```。因此父组件```v-model```语法糖本质上可以修改为 561 | ```'' ``` 562 | 563 | 564 | **显然,这种写法就是事件通信的写法,这个过程又回到对事件指令的分析过程了。因此我们可以很明显的意识到,组件使用```v-model```本质上还是一个子父组件通信的语法糖。** 565 | -------------------------------------------------------------------------------- /src/深入响应式系统构建-中.md: -------------------------------------------------------------------------------- 1 | >为了深入介绍响应式系统的内部实现原理,我们花了一整节的篇幅介绍了数据(包括```data, computed,props```)如何初始化成为响应式对象的过程。有了响应式数据对象的知识,上一节的后半部分我们还在保留源码结构的基础上构建了一个以```data```为数据的响应式系统,而这一节,我们继续深入响应式系统内部构建的细节,详细分析```Vue```在响应式系统中对```data,computed```的处理。 2 | 3 | ## 7.8 相关概念 4 | 在构建简易式响应式系统的时候,我们引出了几个重要的概念,他们都是响应式原理设计的核心,我们先简单回顾一下: 5 | - `Observer`类,实例化一个```Observer```类会通过```Object.defineProperty```对数据的```getter,setter```方法进行改写,在```getter```阶段进行**依赖的收集**,在数据发生更新阶段,触发```setter```方法进行**依赖的更新** 6 | - `watcher`类,实例化```watcher```类相当于创建一个依赖,简单的理解是数据在哪里被使用就需要产生了一个依赖。当数据发生改变时,会通知到每个依赖进行更新,前面提到的渲染```wathcer```便是渲染```dom```时使用数据产生的依赖。 7 | - `Dep`类,既然```watcher```理解为每个数据需要监听的依赖,那么对这些依赖的收集和通知则需要另一个类来管理,这个类便是```Dep```,```Dep```需要做的只有两件事,收集依赖和派发更新依赖。 8 | 9 | 10 | 这是响应式系统构建的三个基本核心概念,也是这一节的基础,如果还没有印象,请先回顾上一节对**极简风响应式系统的构建**。 11 | 12 | 13 | ## 7.9 data 14 | ### 7.9.1 问题思考 15 | 在开始分析```data```之前,我们先抛出几个问题让读者思考,而答案都包含在接下来内容分析中。 16 | 17 | - 前面已经知道,```Dep```是作为管理依赖的容器,那么这个容器在什么时候产生?也就是实例化```Dep```发生在什么时候? 18 | 19 | - `Dep`收集了什么类型的依赖?即```watcher```作为依赖的分类有哪些,分别是什么场景,以及区别在哪里? 20 | - `Observer`这个类具体对```getter,setter```方法做了哪些事情? 21 | - 手写的```watcher```和页面数据渲染监听的```watch```如果同时监听到数据的变化,优先级怎么排? 22 | - 有了依赖的收集是不是还有依赖的解除,依赖解除的意义在哪里? 23 | 24 | 带着这几个问题,我们开始对```data```的响应式细节展开分析。 25 | 26 | ### 7.9.2 依赖收集 27 | `data`在初始化阶段会实例化一个```Observer```类,这个类的定义如下(忽略数组类型的```data```): 28 | ```js 29 | // initData 30 | function initData(data) { 31 | ··· 32 | observe(data, true) 33 | } 34 | // observe 35 | function observe(value, asRootData) { 36 | ··· 37 | ob = new Observer(value); 38 | return ob 39 | } 40 | 41 | // 观察者类,对象只要设置成拥有观察属性,则对象下的所有属性都会重写getter和setter方法,而getter,setting方法会进行依赖的收集和派发更新 42 | var Observer = function Observer (value) { 43 | ··· 44 | // 将__ob__属性设置成不可枚举属性。外部无法通过遍历获取。 45 | def(value, '__ob__', this); 46 | // 数组处理 47 | if (Array.isArray(value)) { 48 | ··· 49 | } else { 50 | // 对象处理 51 | this.walk(value); 52 | } 53 | }; 54 | 55 | function def (obj, key, val, enumerable) { 56 | Object.defineProperty(obj, key, { 57 | value: val, 58 | enumerable: !!enumerable, // 是否可枚举 59 | writable: true, 60 | configurable: true 61 | }); 62 | } 63 | ``` 64 | `Observer`会为```data```添加一个```__ob__```属性, ```__ob__```属性是作为响应式对象的标志,同时```def```方法确保了该属性是不可枚举属性,即外界无法通过遍历获取该属性值。除了标志响应式对象外,```Observer```类还调用了原型上的```walk```方法,遍历对象上每个属性进行```getter,setter```的改写。 65 | ```js 66 | Observer.prototype.walk = function walk (obj) { 67 | // 获取对象所有属性,遍历调用defineReactive###1进行改写 68 | var keys = Object.keys(obj); 69 | for (var i = 0; i < keys.length; i++) { 70 | defineReactive###1(obj, keys[i]); 71 | } 72 | }; 73 | ``` 74 | 75 | 76 | 77 | `defineReactive###1`是响应式构建的核心,它会先**实例化一个```Dep```类,即为每个数据都创建一个依赖的管理**,之后利用```Object.defineProperty```重写```getter,setter```方法。这里我们只分析依赖收集的代码。 78 | ```js 79 | function defineReactive###1 (obj,key,val,customSetter,shallow) { 80 | // 每个数据实例化一个Dep类,创建一个依赖的管理 81 | var dep = new Dep(); 82 | 83 | var property = Object.getOwnPropertyDescriptor(obj, key); 84 | // 属性必须满足可配置 85 | if (property && property.configurable === false) { 86 | return 87 | } 88 | // cater for pre-defined getter/setters 89 | var getter = property && property.get; 90 | var setter = property && property.set; 91 | // 这一部分的逻辑是针对深层次的对象,如果对象的属性是一个对象,则会递归调用实例化Observe类,让其属性值也转换为响应式对象 92 | var childOb = !shallow && observe(val); 93 | Object.defineProperty(obj, key, { 94 | enumerable: true, 95 | configurable: true,s 96 | get: function reactiveGetter () { 97 | var value = getter ? getter.call(obj) : val; 98 | if (Dep.target) { 99 | // 为当前watcher添加dep数据 100 | dep.depend(); 101 | if (childOb) { 102 | childOb.dep.depend(); 103 | if (Array.isArray(value)) { 104 | dependArray(value); 105 | } 106 | } 107 | } 108 | return value 109 | }, 110 | set: function reactiveSetter (newVal) {} 111 | }); 112 | } 113 | ``` 114 | 115 | 主要看```getter```的逻辑,我们知道当```data```中属性值被访问时,会被```getter```函数拦截,根据我们旧有的知识体系可以知道,实例挂载前会创建一个渲染```watcher```。 116 | ```js 117 | new Watcher(vm, updateComponent, noop, { 118 | before: function before () { 119 | if (vm._isMounted && !vm._isDestroyed) { 120 | callHook(vm, 'beforeUpdate'); 121 | } 122 | } 123 | }, true /* isRenderWatcher */); 124 | ``` 125 | 与此同时,```updateComponent```的逻辑会执行实例的挂载,在这个过程中,模板会被优先解析为```render```函数,而```render```函数转换成```Vnode```时,会访问到定义的```data```数据,这个时候会触发```gettter```进行依赖收集。而此时数据收集的依赖就是这个渲染```watcher```本身。 126 | 127 | 代码中依赖收集阶段会做下面几件事: 128 | 1. **为当前的```watcher```(该场景下是渲染```watcher```)添加拥有的数据**。 129 | 2. **为当前的数据收集需要监听的依赖** 130 | 131 | 如何理解这两点?我们先看代码中的实现。```getter```阶段会执行```dep.depend()```,这是```Dep```这个类定义在原型上的方法。 132 | ```js 133 | dep.depend(); 134 | 135 | 136 | Dep.prototype.depend = function depend () { 137 | if (Dep.target) { 138 | Dep.target.addDep(this); 139 | } 140 | }; 141 | ``` 142 | `Dep.target`为当前执行的```watcher```,在渲染阶段,```Dep.target```为组件挂载时实例化的渲染```watcher```,因此```depend```方法又会调用当前```watcher```的```addDep```方法为```watcher```添加依赖的数据。 143 | 144 | ```js 145 | Watcher.prototype.addDep = function addDep (dep) { 146 | var id = dep.id; 147 | if (!this.newDepIds.has(id)) { 148 | // newDepIds和newDeps记录watcher拥有的数据 149 | this.newDepIds.add(id); 150 | this.newDeps.push(dep); 151 | // 避免重复添加同一个data收集器 152 | if (!this.depIds.has(id)) { 153 | dep.addSub(this); 154 | } 155 | } 156 | }; 157 | ``` 158 | 159 | 其中```newDepIds```是具有唯一成员是```Set```数据结构,```newDeps```是数组,他们用来记录当前```watcher```所拥有的数据,这一过程会进行逻辑判断,避免同一数据添加多次。 160 | 161 | `addSub`为每个数据依赖收集器添加需要被监听的```watcher```。 162 | 163 | ```js 164 | Dep.prototype.addSub = function addSub (sub) { 165 | //将当前watcher添加到数据依赖收集器中 166 | this.subs.push(sub); 167 | }; 168 | ``` 169 | 170 | 3. **`getter`如果遇到属性值为对象时,会为该对象的每个值收集依赖** 171 | 172 | 这句话也很好理解,如果我们将一个值为基本类型的响应式数据改变成一个对象,此时新增对象里的属性,也需要设置成响应式数据。 173 | 174 | 4. **遇到属性值为数组时,进行特殊处理**,这点放到后面讲。 175 | 176 | **通俗的总结一下依赖收集的过程,每个数据就是一个依赖管理器,而每个使用数据的地方就是一个依赖。当访问到数据时,会将当前访问的场景作为一个依赖收集到依赖管理器中,同时也会为这个场景的依赖收集拥有的数据。** 177 | 178 | 179 | ### 7.9.3 派发更新 180 | 在分析依赖收集的过程中,可能会有不少困惑,为什么要维护这么多的关系?在数据更新时,这些关系会起到什么作用?带着疑惑,我们来看看派发更新的过程。 181 | 在数据发生改变时,会执行定义好的```setter```方法,我们先看源码。 182 | ```js 183 | Object.defineProperty(obj,key, { 184 | ··· 185 | set: function reactiveSetter (newVal) { 186 | var value = getter ? getter.call(obj) : val; 187 | // 新值和旧值相等时,跳出操作 188 | if (newVal === value || (newVal !== newVal && value !== value)) { 189 | return 190 | } 191 | ··· 192 | // 新值为对象时,会为新对象进行依赖收集过程 193 | childOb = !shallow && observe(newVal); 194 | dep.notify(); 195 | } 196 | }) 197 | ``` 198 | 派发更新阶段会做以下几件事: 199 | - **判断数据更改前后是否一致,如果数据相等则不进行任何派发更新操作**。 200 | - **新值为对象时,会对该值的属性进行依赖收集过程**。 201 | - **通知该数据收集的```watcher```依赖,遍历每个```watcher```进行数据更新**,这个阶段是调用该数据依赖收集器的```dep.notify```方法进行更新的派发。 202 | ```js 203 | Dep.prototype.notify = function notify () { 204 | var subs = this.subs.slice(); 205 | if (!config.async) { 206 | // 根据依赖的id进行排序 207 | subs.sort(function (a, b) { return a.id - b.id; }); 208 | } 209 | for (var i = 0, l = subs.length; i < l; i++) { 210 | // 遍历每个依赖,进行更新数据操作。 211 | subs[i].update(); 212 | } 213 | }; 214 | ``` 215 | - **更新时会将每个```watcher```推到队列中,等待下一个```tick```到来时取出每个```watcher```进行```run```操作** 216 | ```js 217 | Watcher.prototype.update = function update () { 218 | ··· 219 | queueWatcher(this); 220 | }; 221 | ``` 222 | `queueWatcher`方法的调用,会将数据所收集的依赖依次推到```queue```数组中,数组会在下一个事件循环```'tick'```中根据缓冲结果进行视图更新。而在执行视图更新过程中,难免会因为数据的改变而在渲染模板上添加新的依赖,这样又会执行```queueWatcher```的过程。所以需要有一个标志位来记录是否处于异步更新过程的队列中。这个标志位为```flushing```,当处于异步更新过程时,新增的```watcher```会插入到```queue```中。 223 | ```js 224 | function queueWatcher (watcher) { 225 | var id = watcher.id; 226 | // 保证同一个watcher只执行一次 227 | if (has[id] == null) { 228 | has[id] = true; 229 | if (!flushing) { 230 | queue.push(watcher); 231 | } else { 232 | var i = queue.length - 1; 233 | while (i > index && queue[i].id > watcher.id) { 234 | i--; 235 | } 236 | queue.splice(i + 1, 0, watcher); 237 | } 238 | ··· 239 | nextTick(flushSchedulerQueue); 240 | } 241 | } 242 | ``` 243 | `nextTick`的原理和实现先不讲,概括来说,```nextTick```会缓冲多个数据处理过程,等到下一个事件循环```tick```中再去执行```DOM```操作,**它的原理,本质是利用事件循环的微任务队列实现异步更新**。 244 | 245 | 246 | 当下一个```tick```到来时,会执行```flushSchedulerQueue```方法,它会拿到收集的```queue```数组(这是一个```watcher```的集合),并对数组依赖进行排序。为什么进行排序呢?源码中解释了三点: 247 | 248 | > - 组件创建是先父后子,所以组件的更新也是先父后子,因此需要保证父的渲染```watcher```优先于子的渲染```watcher```更新。 249 | > - **用户自定义的```watcher```,称为```user watcher```。 ```user watcher```和```render watcher```执行也有先后,由于```user watchers```比```render watcher```要先创建,所以```user watcher```要优先执行**。 250 | > - 如果一个组件在父组件的 ```watcher``` 执行阶段被销毁,那么它对应的 ```watcher``` 执行都可以被跳过。 251 | 252 | 253 | ```js 254 | function flushSchedulerQueue () { 255 | currentFlushTimestamp = getNow(); 256 | flushing = true; 257 | var watcher, id; 258 | // 对queue的watcher进行排序 259 | queue.sort(function (a, b) { return a.id - b.id; }); 260 | // 循环执行queue.length,为了确保由于渲染时添加新的依赖导致queue的长度不断改变。 261 | for (index = 0; index < queue.length; index++) { 262 | watcher = queue[index]; 263 | // 如果watcher定义了before的配置,则优先执行before方法 264 | if (watcher.before) { 265 | watcher.before(); 266 | } 267 | id = watcher.id; 268 | has[id] = null; 269 | watcher.run(); 270 | // in dev build, check and stop circular updates. 271 | if (has[id] != null) { 272 | circular[id] = (circular[id] || 0) + 1; 273 | if (circular[id] > MAX_UPDATE_COUNT) { 274 | warn( 275 | 'You may have an infinite update loop ' + ( 276 | watcher.user 277 | ? ("in watcher with expression \"" + (watcher.expression) + "\"") 278 | : "in a component render function." 279 | ), 280 | watcher.vm 281 | ); 282 | break 283 | } 284 | } 285 | } 286 | 287 | // keep copies of post queues before resetting state 288 | var activatedQueue = activatedChildren.slice(); 289 | var updatedQueue = queue.slice(); 290 | // 重置恢复状态,清空队列 291 | resetSchedulerState(); 292 | 293 | // 视图改变后,调用其他钩子 294 | callActivatedHooks(activatedQueue); 295 | callUpdatedHooks(updatedQueue); 296 | 297 | // devtool hook 298 | /* istanbul ignore if */ 299 | if (devtools && config.devtools) { 300 | devtools.emit('flush'); 301 | } 302 | } 303 | ``` 304 | 305 | 306 | `flushSchedulerQueue`阶段,重要的过程可以总结为四点: 307 | > - 对```queue```中的```watcher```进行排序,原因上面已经总结。 308 | > - 遍历```watcher```,如果当前```watcher```有```before```配置,则执行```before```方法,对应前面的渲染```watcher```:在渲染```watcher```实例化时,我们传递了```before```函数,即在下个```tick```更新视图前,会调用```beforeUpdate```生命周期钩子。 309 | > - 执行```watcher.run```进行修改的操作。 310 | > - 重置恢复状态,这个阶段会将一些流程控制的状态变量恢复为初始值,并清空记录```watcher```的队列。 311 | 312 | ```js 313 | new Watcher(vm, updateComponent, noop, { 314 | before: function before () { 315 | if (vm._isMounted && !vm._isDestroyed) { 316 | callHook(vm, 'beforeUpdate'); 317 | } 318 | } 319 | }, true /* isRenderWatcher */); 320 | ``` 321 | 322 | 323 | 重点看看```watcher.run()```的操作。 324 | ```js 325 | Watcher.prototype.run = function run () { 326 | if (this.active) { 327 | var value = this.get(); 328 | if ( value !== this.value || isObject(value) || this.deep ) { 329 | // 设置新值 330 | var oldValue = this.value; 331 | this.value = value; 332 | // 针对user watcher,暂时不分析 333 | if (this.user) { 334 | try { 335 | this.cb.call(this.vm, value, oldValue); 336 | } catch (e) { 337 | handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\"")); 338 | } 339 | } else { 340 | this.cb.call(this.vm, value, oldValue); 341 | } 342 | } 343 | } 344 | }; 345 | ``` 346 | 首先会执行```watcher.prototype.get```的方法,得到数据变化后的当前值,之后会对新值做判断,如果判断满足条件,则执行```cb```,```cb```为实例化```watcher```时传入的回调。 347 | 348 | 在分析```get```方法前,回头看看```watcher```构造函数的几个属性定义 349 | ```js 350 | var watcher = function Watcher( 351 | vm, // 组件实例 352 | expOrFn, // 执行函数 353 | cb, // 回调 354 | options, // 配置 355 | isRenderWatcher // 是否为渲染watcher 356 | ) { 357 | this.vm = vm; 358 | if (isRenderWatcher) { 359 | vm._watcher = this; 360 | } 361 | vm._watchers.push(this); 362 | // options 363 | if (options) { 364 | this.deep = !!options.deep; 365 | this.user = !!options.user; 366 | this.lazy = !!options.lazy; 367 | this.sync = !!options.sync; 368 | this.before = options.before; 369 | } else { 370 | this.deep = this.user = this.lazy = this.sync = false; 371 | } 372 | this.cb = cb; 373 | this.id = ++uid$2; // uid for batching 374 | this.active = true; 375 | this.dirty = this.lazy; // for lazy watchers 376 | this.deps = []; 377 | this.newDeps = []; 378 | this.depIds = new _Set(); 379 | this.newDepIds = new _Set(); 380 | this.expression = expOrFn.toString(); 381 | // parse expression for getter 382 | if (typeof expOrFn === 'function') { 383 | this.getter = expOrFn; 384 | } else { 385 | this.getter = parsePath(expOrFn); 386 | if (!this.getter) { 387 | this.getter = noop; 388 | warn( 389 | "Failed watching path: \"" + expOrFn + "\" " + 390 | 'Watcher only accepts simple dot-delimited paths. ' + 391 | 'For full control, use a function instead.', 392 | vm 393 | ); 394 | } 395 | } 396 | // lazy为计算属性标志,当watcher为计算watcher时,不会理解执行get方法进行求值 397 | this.value = this.lazy 398 | ? undefined 399 | : this.get(); 400 | 401 | } 402 | ``` 403 | 方法```get```的定义如下: 404 | ```js 405 | Watcher.prototype.get = function get () { 406 | pushTarget(this); 407 | var value; 408 | var vm = this.vm; 409 | try { 410 | value = this.getter.call(vm, vm); 411 | } catch (e) { 412 | ··· 413 | } finally { 414 | ··· 415 | // 把Dep.target恢复到上一个状态,依赖收集过程完成 416 | popTarget(); 417 | this.cleanupDeps(); 418 | } 419 | return value 420 | }; 421 | ``` 422 | `get`方法会执行```this.getter```进行求值,在当前渲染```watcher```的条件下,```getter```会执行视图更新的操作。这一阶段会**重新渲染页面组件** 423 | ```js 424 | new Watcher(vm, updateComponent, noop, { before: () => {} }, true); 425 | 426 | updateComponent = function () { 427 | vm._update(vm._render(), hydrating); 428 | }; 429 | ``` 430 | 431 | 执行完```getter```方法后,最后一步会进行依赖的清除,也就是```cleanupDeps```的过程。 432 | 433 | > 关于依赖清除的作用,我们列举一个场景: 我们经常会使用```v-if```来进行模板的切换,切换过程中会执行不同的模板渲染,如果A模板监听a数据,B模板监听b数据,当渲染模板B时,如果不进行旧依赖的清除,在B模板的场景下,a数据的变化同样会引起依赖的重新渲染更新,这会造成性能的浪费。因此旧依赖的清除在优化阶段是有必要。 434 | 435 | ```js 436 | // 依赖清除的过程 437 | Watcher.prototype.cleanupDeps = function cleanupDeps () { 438 | var i = this.deps.length; 439 | while (i--) { 440 | var dep = this.deps[i]; 441 | if (!this.newDepIds.has(dep.id)) { 442 | dep.removeSub(this); 443 | } 444 | } 445 | var tmp = this.depIds; 446 | this.depIds = this.newDepIds; 447 | this.newDepIds = tmp; 448 | this.newDepIds.clear(); 449 | tmp = this.deps; 450 | this.deps = this.newDeps; 451 | this.newDeps = tmp; 452 | this.newDeps.length = 0; 453 | }; 454 | ``` 455 | 456 | 把上面分析的总结成依赖派发更新的最后两个点 457 | - **执行```run```操作会执行```getter```方法,也就是重新计算新值,针对渲染```watcher```而言,会重新执行```updateComponent```进行视图更新** 458 | - **重新计算```getter```后,会进行依赖的清除** 459 | 460 | 461 | ## 7.10 computed 462 | 计算属性设计的初衷是用于简单运算的,毕竟在模板中放入太多的逻辑会让模板过重且难以维护。在分析```computed```时,我们依旧遵循依赖收集和派发更新两个过程进行分析。 463 | ### 7.10.1 依赖收集 464 | `computed`的初始化过程,**会遍历```computed```的每一个属性值,并为每一个属性实例化一个```computed watcher```**,其中```{ lazy: true}```是```computed watcher```的标志,最终会调用```defineComputed```将数据设置为响应式数据,对应源码如下: 465 | 466 | ```js 467 | function initComputed() { 468 | ··· 469 | for(var key in computed) { 470 | watchers[key] = new Watcher( 471 | vm, 472 | getter || noop, 473 | noop, 474 | computedWatcherOptions 475 | ); 476 | } 477 | if (!(key in vm)) { 478 | defineComputed(vm, key, userDef); 479 | } 480 | } 481 | 482 | // computed watcher的标志,lazy属性为true 483 | var computedWatcherOptions = { lazy: true }; 484 | ``` 485 | `defineComputed`的逻辑和分析```data```的逻辑相似,最终调用```Object.defineProperty```进行数据拦截。具体的定义如下: 486 | ```js 487 | function defineComputed (target,key,userDef) { 488 | // 非服务端渲染会对getter进行缓存 489 | var shouldCache = !isServerRendering(); 490 | if (typeof userDef === 'function') { 491 | // 492 | sharedPropertyDefinition.get = shouldCache 493 | ? createComputedGetter(key) 494 | : createGetterInvoker(userDef); 495 | sharedPropertyDefinition.set = noop; 496 | } else { 497 | sharedPropertyDefinition.get = userDef.get 498 | ? shouldCache && userDef.cache !== false 499 | ? createComputedGetter(key) 500 | : createGetterInvoker(userDef.get) 501 | : noop; 502 | sharedPropertyDefinition.set = userDef.set || noop; 503 | } 504 | if (sharedPropertyDefinition.set === noop) { 505 | sharedPropertyDefinition.set = function () { 506 | warn( 507 | ("Computed property \"" + key + "\" was assigned to but it has no setter."), 508 | this 509 | ); 510 | }; 511 | } 512 | Object.defineProperty(target, key, sharedPropertyDefinition); 513 | } 514 | ``` 515 | 516 | 在非服务端渲染的情形,计算属性的计算结果会被缓存,缓存的意义在于,**只有在相关响应式数据发生变化时,```computed```才会重新求值,其余情况多次访问计算属性的值都会返回之前计算的结果,这就是缓存的优化**,```computed```属性有两种写法,一种是函数,另一种是对象,其中对象的写法需要提供```getter```和```setter```方法。 517 | 518 | 当访问到```computed```属性时,会触发```getter```方法进行依赖收集,看看```createComputedGetter```的实现。 519 | ```js 520 | function createComputedGetter (key) { 521 | return function computedGetter () { 522 | var watcher = this._computedWatchers && this._computedWatchers[key]; 523 | if (watcher) { 524 | if (watcher.dirty) { 525 | watcher.evaluate(); 526 | } 527 | if (Dep.target) { 528 | watcher.depend(); 529 | } 530 | return watcher.value 531 | } 532 | } 533 | } 534 | ``` 535 | `createComputedGetter`返回的函数在执行过程中会先拿到属性的```computed watcher```,```dirty```是标志是否已经执行过计算结果,如果执行过则不会执行```watcher.evaluate```重复计算,这也是缓存的原理。 536 | ```js 537 | Watcher.prototype.evaluate = function evaluate () { 538 | // 对于计算属性而言 evaluate的作用是执行计算回调 539 | this.value = this.get(); 540 | this.dirty = false; 541 | }; 542 | ``` 543 | `get`方法前面介绍过,会调用实例化```watcher```时传递的执行函数,在```computer watcher```的场景下,执行函数是计算属性的计算函数,他可以是一个函数,也可以是对象的```getter```方法。 544 | 545 | > 列举一个场景避免和```data```的处理脱节,```computed```在计算阶段,如果访问到```data```数据的属性值,会触发```data```数据的```getter```方法进行依赖收集,根据前面分析,```data```的```Dep```收集器会将当前```watcher```作为依赖进行收集,而这个```watcher```就是```computed watcher```,并且会为当前的```watcher```添加访问的数据```Dep``` 546 | 547 | 548 | 回到计算执行函数的```this.get()```方法,```getter```执行完成后同样会进行依赖的清除,原理和目的参考```data```阶段的分析。```get```执行完毕后会进入```watcher.depend```进行依赖的收集。收集过程和```data```一致,将当前的```computed watcher```作为依赖收集到数据的依赖收集器```Dep```中。 549 | 550 | 这就是```computed```依赖收集的完整过程,对比```data```的依赖收集,```computed```会对运算的结果进行缓存,避免重复执行运算过程。 551 | 552 | 553 | ### 7.10.2 派发更新 554 | 派发更新的条件是```data```中数据发生改变,所以大部分的逻辑和分析```data```时一致,我们做一个总结。 555 | - 当计算属性依赖的数据发生更新时,由于数据的```Dep```收集过```computed watch```这个依赖,所以会调用```dep```的```notify```方法,对依赖进行状态更新。 556 | - 此时```computed watcher```和之前介绍的```watcher```不同,它不会立刻执行依赖的更新操作,而是通过一个```dirty```进行标记。我们再回头看```依赖更新```的代码。 557 | 558 | ```js 559 | Dep.prototype.notify = function() { 560 | ··· 561 | for (var i = 0, l = subs.length; i < l; i++) { 562 | subs[i].update(); 563 | } 564 | } 565 | 566 | Watcher.prototype.update = function update () { 567 | // 计算属性分支 568 | if (this.lazy) { 569 | this.dirty = true; 570 | } else if (this.sync) { 571 | this.run(); 572 | } else { 573 | queueWatcher(this); 574 | } 575 | }; 576 | ``` 577 | 578 | 由于```lazy```属性的存在,```update```过程不会执行状态更新的操作,只会将```dirty```标记为```true```。 579 | - 由于```data```数据拥有渲染```watcher```这个依赖,所以同时会执行```updateComponent```进行视图重新渲染,而```render```过程中会访问到计算属性,此时由于```this.dirty```值为```true```,又会对计算属性重新求值。 580 | 581 | 582 | ## 7.11 小结 583 | 我们在上一节的理论基础上深入分析了```Vue```如何利用```data,computed```构建响应式系统。响应式系统的核心是利用```Object.defineProperty```对数据的```getter,setter```进行拦截处理,处理的核心是在访问数据时对数据所在场景的依赖进行收集,在数据发生更改时,通知收集过的依赖进行更新。这一节我们详细的介绍了```data,computed```对响应式的处理,两者处理逻辑存在很大的相似性但却各有的特性。源码中会```computed```的计算结果进行缓存,避免了在多个地方使用时频繁重复计算的问题。由于篇幅有限,对于用户自定义的```watcher```我们会放到下一小节分析。文章还留有一个疑惑,依赖收集时如果遇到的数据是数组时应该怎么处理,这些疑惑都会在之后的文章一一解开。 --------------------------------------------------------------------------------