├── README.md ├── img ├── 1.png ├── 2.png ├── 3.png └── 4.png ├── step0 └── defineProperty_test.html ├── step1 ├── XVue.js └── index.html ├── step2 ├── XVue.js └── index.html ├── step3.1 ├── XVue.js ├── compile.js └── index.html └── step3.2 ├── XVue.js ├── compile.js └── index.html /README.md: -------------------------------------------------------------------------------- 1 | ## Vue.js 代码实现 2 | 3 | 检验学习效果的最好方法就是自己造轮子。最近在学习Vue源码,写了一个迷你版vue,实现数据响应式。从step1到step3.2,是开发步骤和实现思路,每一步都可以独立运行。 4 | 5 | 代码地址:https://github.com/dora-zc/miniature-vue 6 | 7 | 以上每个step文件夹对应下面的每一步骤,代表了代码实现的顺序,每个文件夹下的代码都可以独立运行。 8 | 9 | ### 1. 步骤一 10 | 11 | 创建XVue.js。 12 | 13 | 创建Vue类,通过Observer劫持监听所有属性。 14 | 15 | observe函数的作用:递归遍历data选项,它当中的defineReactive函数为data中每一个key定义getter和setter,达到数据劫持的目的。 16 | 17 | > 步骤一对应代码目录:step1 18 | 19 | ### 2. 步骤二 20 | 21 | 处理页面上的`
{{msg}}
`,也就是收集依赖,当msg的值发生变化时,视图需要做出相应的变化。因此需要创建依赖管理器,把所有依赖保存起来,当数据发生变化的时候再去更新对应的依赖。 22 | 23 | #### 2.1 创建Dep类 24 | 25 | Dep负责将视图中的所有依赖收集管理,包括依赖添加和派发通知 26 | 27 | 1- 在Dep类中创建数组deps=[],用来存放Watcher的实例 28 | 29 | 2-创建addDep方法,添加Watcher 30 | 31 | 3-创建notify方法,通知所有的Wather执行更新。遍历deps数组,调用每个Wather的更新方法 32 | 33 | #### 2.2 创建监听器Watcher类 34 | 35 | Watcher是具体的更新执行者。 36 | 37 | 1-将当前Watcher实例添加到Dep.target上。 38 | 39 | ```js 40 | Dep.target = this 41 | ``` 42 | 43 | 之后在get时,就能通过Dep.target拿到当前Watcher的实例。 44 | 45 | 2-创建update方法 46 | 47 | 3-set方法中,调用dep.notify,让依赖管理器通知更新,则所有的Watcher会执行update方法 48 | 49 | 那么问题来了:Watcher在什么时候收集最合适? 50 | 51 | 在defineReactive函数的get方法中,get方法触发时,把Watcher放进Dep.target中。 52 | 53 | 那么问题又来了:为什么是在get方法中呢? 54 | 55 | 因为在扫描视图中的依赖时,如果扫描到`
{{msg}}
`,此时一定会去访问msg的值,就会触发get。一旦get被触发,就能将Watcher放进dep中,实现依赖收集的目的。所以get是一个合适的时间点。 56 | 57 | 代码测试:在get中输出dep.deps,如果Watcher已经放进去了,并且控制台打印出Watcher中的update方法中的log,说明这一步操作成功了。 58 | 59 | 至此,已经完成的工作如下: 60 | 61 | ![Image text](https://github.com/dora-zc/miniature-vue/blob/master/img/1.png?raw=true) 62 | 63 | > 步骤二对应代码目录:step2 64 | 65 | 现在,Watcher发生变化时,视图还没有更新,下面我们将要完成视图更新的操作。 66 | 67 | 首先,需要Compile对界面模板解析指令,进行编译,编译的阶段实际是创建Watcher的阶段。Watcher是由编译器创建的。编译器在做依赖收集的时候,顺便把Watcher创建了。Watcher在创建的时候,立刻就能知道它将来要更新的是谁,它应该被谁管理,它发生变化以后值应该是什么。于是Watcher就知道调谁(Updater去做更新了)。 68 | 69 | ![Image text](https://github.com/dora-zc/miniature-vue/blob/master/img/2.png?raw=true) 70 | 71 | ### 3.步骤三 72 | 73 | 创建compile.js,用于扫描模板中所有依赖(指令、插值、绑定、事件…),创建更新函数和Watcher 74 | 75 | #### 3.1 扫描模板 76 | 77 | 1-创建编译器Compile类,接收两个参数,el(宿主元素或选择器)和vm(当前vue实例) 78 | 79 | 2-创建node2Fragment函数,将dom节点( $el )截成代码块( 转换为Fragment )来处理,而不是直接做dom操作,提高执行效率 80 | 81 | 3-创建compile函数,执行编译( 将模板中的动态值替换为真实的值 ),传入代码块 82 | 83 | 4-将生成的结果追加至宿主元素 84 | 85 | ##### 3.1.1 node2Fragment函数 86 | 87 | 创建一个新的fragment,将原生节点移动至fragment 88 | 89 | 返回fragment,传给编译函数进行编译 90 | 91 | ##### 3.1.2 compile函数 92 | 93 | 获取所有的孩子节点,进行遍历,判断节点类型,并作出相应的判断 94 | 95 | 处理元素节点 96 | 97 | 处理文本节点( 只处理{{msg}} 这种情况,其他的全部不处理) 98 | 99 | ...其他的节点类型暂时不判断了 100 | 101 | 遍历可能存在的子节点,往下递归 102 | 103 | 下面是compile函数中的两个核心方法 104 | 105 | 1-compileElement方法:编译元素节点 106 | 107 | ```html 108 |
{{msg}}
109 | ``` 110 | 111 | 拿到所有属性名称,进行遍历 112 | 113 | 2-compileText方法:编译文本节点 114 | 115 | 代码测试: 116 | 117 | 在XVue constructor中,创建编译器实例,将宿主元素el和当前vue实例作为参数传入。 118 | 119 | 如果compileElement和compileText两个函数能触发,控制台打印出"开始编译元素节点"和"开始编译文本节点",则说明功能正常,可以继续让下走了。 120 | 121 | > 对应代码:step3.1 122 | 123 | 124 | 125 | #### 3.2 编译元素节点和文本节点,并创建更新函数 126 | 127 | ##### 3.2.1 编译元素节点compileElement方法实现 128 | 129 | 获取节点所有属性,进行遍历。判断指令和事件,已经相应的处理方法。 130 | 131 | 指令只试着处理v-text,v-html,v-model三个,其他的暂不处理 132 | 133 | v-model:双向绑定还需要处理视图对模型的更新 134 | 135 | ##### 3.2.2 创建更新器函数 136 | 137 | 更新器函数:接收四个参数,node,vm,exp,dir(指令) 138 | 139 | 针对指令的更新器主要是在做dom操作 140 | 141 | 在更新器函数中创建Watcher实例,当Watcher监听到变化的时候,就能触发视图的更新。 142 | 143 | 至此,全部代码已经完成,双向数据绑定顺利实现! 144 | 145 | ![Image text](https://github.com/dora-zc/miniature-vue/blob/master/img/4.png?raw=true) 146 | 147 | > 对应代码:step3.2 148 | 149 | 150 | 151 | ## Vue.js 工作机制 152 | 153 | ### 初始化 154 | 155 | 在new Vue()之后,Vue会调用初始化函数,会初始化声明周期、事件、props、methods、data、computed和watcher等。其中最重要的是通过Object.defineProperty设置setter和getter,用来实现响应式和依赖收集。 156 | 157 | 初始化之后会调用$.mount挂载组件。 158 | 159 | ### 编译 160 | 161 | 编译模块分为三个阶段: 162 | 163 | #### 1-parse 164 | 165 | 使用正则解析模板中的vue的指令、变量等等,形成抽象语法树AST 166 | 167 | #### 2-optimize 168 | 169 | 标记一些静态节点,用作后面的性能优化,在diff的时候直接略过 170 | 171 | #### 3-generate 172 | 173 | 把第一步生成的AST转化为渲染函数 render function 174 | 175 | ### 响应式 176 | 177 | 这一块是vue最核心的内容。初始化的时候通过defineProperty进行绑定,设置通知的机制,当编译生成的渲染函数被实际渲染的时候,会触发getter进行依赖收集,在数据变化的时候,触发setter进行更新。 178 | 179 | ### 虚拟dom 180 | 181 | 虚拟dom是由react首创,Vue2开始支持,就是用JavaScript对象来描述dom结构,数据修改的时候,我们先修改虚拟dom中的数据,然后数组做diff算法,最后再汇总所有的diff,力求做最少的dom操作,毕竟js里对比很快,而真实的dom操作太慢了。 182 | 183 | ```html 184 |
185 | click me 186 |
187 | ``` 188 | 189 | ```js 190 | // vdom 191 | { 192 | tag:'div', 193 | props:{ 194 | name:'小菠萝', 195 | style: {color:red}, 196 | onClick:xx 197 | }, 198 | children:[ 199 | { 200 | tag:'a', 201 | text:'click me' 202 | } 203 | ] 204 | } 205 | ``` 206 | 207 | ### 更新视图 208 | 209 | 数据修改触发setter,然后监听器会通知进行修改,通过对比两个dom树,得到改变的地方,就是patch,然后只需要把这些差异修改即可。 210 | 211 | ### 编译 212 | 213 | compile的核心逻辑是获取dom,遍历dom,获取{{}}格式的变量,以及每个dom的属性,截取v-和@开头的部分来设置响应式。 214 | 215 | ![Image text](https://github.com/dora-zc/miniature-vue/blob/master/img/3.png?raw=true) 216 | -------------------------------------------------------------------------------- /img/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenz-fe/miniature-vue/27ea40cf4eea037c9fc0c9517eeae9852dec71ba/img/1.png -------------------------------------------------------------------------------- /img/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenz-fe/miniature-vue/27ea40cf4eea037c9fc0c9517eeae9852dec71ba/img/2.png -------------------------------------------------------------------------------- /img/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenz-fe/miniature-vue/27ea40cf4eea037c9fc0c9517eeae9852dec71ba/img/3.png -------------------------------------------------------------------------------- /img/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenz-fe/miniature-vue/27ea40cf4eea037c9fc0c9517eeae9852dec71ba/img/4.png -------------------------------------------------------------------------------- /step0/defineProperty_test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | hello, 11 | 12 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /step1/XVue.js: -------------------------------------------------------------------------------- 1 | class XVue { 2 | constructor(options) { 3 | this.$data = options.data 4 | this.observe(this.$data) 5 | } 6 | 7 | observe(value) { 8 | if (!value || typeof value !== 'object') { 9 | return 10 | } 11 | Object.keys(value).forEach(key => { 12 | this.defineReactive(value,key,value[key]) 13 | }) 14 | } 15 | 16 | defineReactive(obj, key, val) { 17 | // 递归查找嵌套属性 18 | this.observe(val) 19 | Object.defineProperty(obj, key, { 20 | enumerable: true, 21 | configurable: true, 22 | get() { 23 | return val 24 | }, 25 | set(newVal) { 26 | if (newVal===val) { 27 | return 28 | } 29 | val = newVal 30 | console.log('===================================='); 31 | console.log('数据发生变化'); 32 | console.log('===================================='); 33 | } 34 | }) 35 | } 36 | } -------------------------------------------------------------------------------- /step1/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /step2/XVue.js: -------------------------------------------------------------------------------- 1 | class XVue { 2 | constructor(options) { 3 | this.$data = options.data; 4 | this.observe(this.$data); 5 | 6 | // step2测试代码: 7 | new Watcher(); 8 | console.log('模拟compile', this.$data.test); 9 | } 10 | 11 | observe(value) { 12 | if (!value || typeof value !== 'object') { 13 | return; 14 | } 15 | Object.keys(value).forEach(key => { 16 | this.defineReactive(value, key, value[key]); 17 | }); 18 | } 19 | 20 | defineReactive(obj, key, val) { 21 | // 递归查找嵌套属性 22 | this.observe(val); 23 | 24 | // 创建Dep(step2新增) 25 | const dep = new Dep(); 26 | 27 | Object.defineProperty(obj, key, { 28 | enumerable: true, 29 | configurable: true, 30 | get() { 31 | // 收集依赖(step2新增) 32 | Dep.target && dep.addDep(Dep.target); 33 | console.log(dep.deps); 34 | return val; 35 | }, 36 | set(newVal) { 37 | if (newVal === val) { 38 | return; 39 | } 40 | val = newVal; 41 | dep.notify(); 42 | }, 43 | }); 44 | } 45 | } 46 | 47 | // 依赖管理器:负责将视图中所有依赖收集管理,包括依赖添加和通知 48 | class Dep { 49 | constructor() { 50 | // deps里面存放的是Watcher的实例 51 | this.deps = []; 52 | } 53 | addDep(dep) { 54 | this.deps.push(dep); 55 | } 56 | // 通知所有watcher执行更新 57 | notify() { 58 | this.deps.forEach(dep => { 59 | dep.update(); 60 | }); 61 | } 62 | } 63 | 64 | // Watcher: 具体的更新执行者 65 | class Watcher { 66 | constructor() { 67 | Dep.target = this; 68 | } 69 | update() { 70 | console.log('===================================='); 71 | console.log('from Watcher update: 视图更新啦!!!'); 72 | console.log('===================================='); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /step2/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |
{{msg}}
11 | 12 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /step3.1/XVue.js: -------------------------------------------------------------------------------- 1 | class XVue { 2 | constructor(options) { 3 | this.$data = options.data; 4 | this.observe(this.$data); 5 | 6 | // step2测试代码: 7 | // new Watcher(); 8 | // console.log('模拟compile', this.$data.test); 9 | 10 | // step3.1测试代码: 11 | new Compile(options.el, this) 12 | } 13 | 14 | observe(value) { 15 | if (!value || typeof value !== 'object') { 16 | return; 17 | } 18 | Object.keys(value).forEach(key => { 19 | this.defineReactive(value, key, value[key]); 20 | }); 21 | } 22 | 23 | defineReactive(obj, key, val) { 24 | // 递归查找嵌套属性 25 | this.observe(val); 26 | 27 | // 创建Dep(step2新增) 28 | const dep = new Dep(); 29 | 30 | Object.defineProperty(obj, key, { 31 | enumerable: true, 32 | configurable: true, 33 | get() { 34 | // 收集依赖(step2新增) 35 | Dep.target && dep.addDep(Dep.target); 36 | console.log(dep.deps); 37 | return val; 38 | }, 39 | set(newVal) { 40 | if (newVal === val) { 41 | return; 42 | } 43 | val = newVal; 44 | dep.notify(); 45 | }, 46 | }); 47 | } 48 | } 49 | 50 | // 依赖管理器:负责将视图中所有依赖收集管理,包括依赖添加和通知 51 | class Dep { 52 | constructor() { 53 | // deps里面存放的是Watcher的实例 54 | this.deps = []; 55 | } 56 | addDep(dep) { 57 | this.deps.push(dep); 58 | } 59 | // 通知所有watcher执行更新 60 | notify() { 61 | this.deps.forEach(dep => { 62 | dep.update(); 63 | }); 64 | } 65 | } 66 | 67 | // Watcher: 具体的更新执行者 68 | class Watcher { 69 | constructor() { 70 | Dep.target = this; 71 | } 72 | update() { 73 | console.log('===================================='); 74 | console.log('from Watcher update: 视图更新啦!!!'); 75 | console.log('===================================='); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /step3.1/compile.js: -------------------------------------------------------------------------------- 1 | // 扫描模板中所有依赖创建更新函数和watcher 2 | class Compile { 3 | // el是宿主元素或其选择器 4 | // vm当前Vue实例 5 | constructor(el, vm) { 6 | this.$el = document.querySelector(el); 7 | this.$vm = vm; 8 | if (this.$el) { 9 | // 将dom节点转换为Fragment提高执行效率 10 | this.$fragment = this.node2Fragment(this.$el); 11 | this.compile(this.$fragment); 12 | // 将生成的结果追加至宿主元素 13 | this.$el.appendChild(this.$fragment); 14 | } 15 | } 16 | node2Fragment(el) { 17 | // 创建一个新的Fragment 18 | const fragment = document.createDocumentFragment(); 19 | let child; 20 | // 将原生节点移动至fragment 21 | while ((child = el.firstChild)) { 22 | fragment.appendChild(child); 23 | } 24 | return fragment; 25 | } 26 | compile(el) { 27 | let childNodes = el.childNodes; 28 | Array.from(childNodes).forEach(node => { 29 | // 判断node类型,做相应处理 30 | if (this.isElementNode(node)) { 31 | // 元素节点要识别v-xx或@xx 32 | this.compileElement(node); 33 | } else if ( 34 | this.isTextNode(node) && 35 | /\{\{(.*)\}\}/.test(node.textContent) 36 | ) { 37 | // 文本节点,只关心{{msg}}格式 38 | this.compileText(node, RegExp.$1); // RegExp.$1匹配{{}}之中的内容 39 | } 40 | // 遍历可能存在的子节点 41 | if (node.childNodes && node.childNodes.length) { 42 | this.compile(node); 43 | } 44 | }); 45 | } 46 | 47 | compileElement() { 48 | console.log('开始编译元素节点'); 49 | } 50 | 51 | compileText(node, exp) { 52 | console.log('开始编译文本节点'); 53 | } 54 | 55 | isElementNode(node) { 56 | return node.nodeType == 1; //元素节点 57 | } 58 | 59 | isTextNode(node) { 60 | return node.nodeType == 3; //元素节点 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /step3.1/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |
11 | {{test}} 12 |
{{msg}}
13 |
14 | 15 | 16 | 17 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /step3.2/XVue.js: -------------------------------------------------------------------------------- 1 | class XVue { 2 | constructor(options) { 3 | this.$data = options.data; 4 | this.observe(this.$data); 5 | this.$options = options; 6 | // 执行编译 7 | new Compile(options.el, this); 8 | } 9 | 10 | observe(value) { 11 | if (!value || typeof value !== 'object') { 12 | return; 13 | } 14 | Object.keys(value).forEach(key => { 15 | this.defineReactive(value, key, value[key]); 16 | // 为vue的data做属性代理 17 | this.proxyData(key); 18 | }); 19 | } 20 | 21 | defineReactive(obj, key, val) { 22 | // 递归查找嵌套属性 23 | this.observe(val); 24 | 25 | // 创建Dep 26 | const dep = new Dep(); 27 | 28 | Object.defineProperty(obj, key, { 29 | enumerable: true, 30 | configurable: true, 31 | get() { 32 | // 收集依赖 33 | Dep.target && dep.addDep(Dep.target); 34 | // console.log(dep.deps); 35 | return val; 36 | }, 37 | set(newVal) { 38 | if (newVal === val) { 39 | return; 40 | } 41 | val = newVal; 42 | dep.notify(); 43 | }, 44 | }); 45 | } 46 | 47 | proxyData(key) { 48 | Object.defineProperty(this, key, { 49 | get() { 50 | return this.$data[key]; 51 | }, 52 | set(newVal) { 53 | this.$data[key] = newVal; 54 | }, 55 | }); 56 | } 57 | } 58 | 59 | // 依赖管理器:负责将视图中所有依赖收集管理,包括依赖添加和通知 60 | class Dep { 61 | constructor() { 62 | // deps里面存放的是Watcher的实例 63 | this.deps = []; 64 | } 65 | addDep(dep) { 66 | this.deps.push(dep); 67 | } 68 | // 通知所有watcher执行更新 69 | notify() { 70 | this.deps.forEach(dep => { 71 | dep.update(); 72 | }); 73 | } 74 | } 75 | 76 | // Watcher: 具体的更新执行者 77 | class Watcher { 78 | constructor(vm, key, cb) { 79 | this.vm = vm; 80 | this.key = key; 81 | this.cb = cb; 82 | // 将来 new 一个监听器时,将当前 Watcher 实例附加到 Dep.target 83 | // 将来通过 Dep.target 就能拿到当时创建的 Watcher 实例 84 | Dep.target = this; 85 | // 读取操作,主动触发 get,当前 Watcher 实例被添加到依赖管理器中 86 | this.vm[this.key]; 87 | // 清空操作,避免不必要的重复添加(再次触发 get 就不需要再添加 watcher 了) 88 | Dep.target = null; 89 | } 90 | update() { 91 | // console.log('from Watcher update: 视图更新啦!!!'); 92 | // 通知页面做更新 93 | this.cb.call(this.vm, this.vm[this.key]); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /step3.2/compile.js: -------------------------------------------------------------------------------- 1 | // 扫描模板中所有依赖(指令、插值、绑定、事件等)创建更新函数和watcher 2 | class Compile { 3 | // el是宿主元素或其选择器 4 | // vm当前Vue实例 5 | constructor(el, vm) { 6 | this.$el = document.querySelector(el); 7 | this.$vm = vm; 8 | if (this.$el) { 9 | // 将dom节点转换为Fragment提高执行效率 10 | this.$fragment = this.node2Fragment(this.$el); 11 | // 执行编译,编译完成以后所有的依赖已经替换成真正的值 12 | this.compile(this.$fragment); 13 | // 将生成的结果追加至宿主元素 14 | this.$el.appendChild(this.$fragment); 15 | } 16 | } 17 | node2Fragment(el) { 18 | // 创建一个新的Fragment 19 | const fragment = document.createDocumentFragment(); 20 | let child; 21 | // 将原生节点移动至fragment 22 | while ((child = el.firstChild)) { 23 | // appendChild 是移动操作,移动一个节点,child 就会少一个,最终结束循环 24 | fragment.appendChild(child); 25 | } 26 | return fragment; 27 | } 28 | // 编译指定片段 29 | compile(el) { 30 | let childNodes = el.childNodes; 31 | Array.from(childNodes).forEach(node => { 32 | // 判断node类型,做相应处理 33 | if (this.isElementNode(node)) { 34 | // 元素节点要识别v-xx或@xx 35 | this.compileElement(node); 36 | } else if ( 37 | this.isTextNode(node) && 38 | /\{\{(.*)\}\}/.test(node.textContent) 39 | ) { 40 | // 文本节点,只关心{{msg}}格式 41 | this.compileText(node, RegExp.$1); // RegExp.$1匹配{{}}之中的内容 42 | } 43 | // 遍历可能存在的子节点 44 | if (node.childNodes && node.childNodes.length) { 45 | this.compile(node); 46 | } 47 | }); 48 | } 49 | 50 | compileElement(node) { 51 | // console.log('编译元素节点'); 52 | //
53 | const attrs = node.attributes; 54 | Array.from(attrs).forEach(attr => { 55 | const attrName = attr.name; // 获取属性名 v-text 56 | const exp = attr.value; // 获取属性值 test 57 | if (this.isDirective(attrName)) { 58 | // 指令 59 | const dir = attrName.substr(2); // text 60 | this[dir] && this[dir](node, this.$vm, exp); 61 | } else if (this.isEventDirective(attrName)) { 62 | // 事件 63 | const dir = attrName.substr(1); // click 64 | this.eventHandler(node, this.$vm, exp, dir); 65 | } 66 | }); 67 | } 68 | 69 | compileText(node, exp) { 70 | // console.log('编译文本节点'); 71 | this.text(node, this.$vm, exp); 72 | } 73 | 74 | isElementNode(node) { 75 | return node.nodeType == 1; //元素节点 76 | } 77 | 78 | isTextNode(node) { 79 | return node.nodeType == 3; //元素节点 80 | } 81 | 82 | isDirective(attr) { 83 | return attr.indexOf('v-') == 0; 84 | } 85 | 86 | isEventDirective(dir) { 87 | return dir.indexOf('@') == 0; 88 | } 89 | 90 | // 文本更新 91 | text(node, vm, exp) { 92 | this.update(node, vm, exp, 'text'); 93 | } 94 | 95 | // 处理html 96 | html(node, vm, exp) { 97 | this.update(node, vm, exp, 'html'); 98 | } 99 | 100 | // 双向绑定 101 | model(node, vm, exp) { 102 | this.update(node, vm, exp, 'model'); 103 | 104 | let val = vm.exp; 105 | // 双绑还要处理视图对模型的更新 106 | node.addEventListener('input', e => { 107 | vm[exp] = e.target.value; // 这里相当于执行了 set 108 | }); 109 | } 110 | 111 | // 更新 112 | // 能够触发这个 update 方法的时机有两个:1-编译器初始化视图时触发;2-Watcher更新视图时触发 113 | update(node, vm, exp, dir) { 114 | let updaterFn = this[dir + 'Updater']; 115 | updaterFn && updaterFn(node, vm[exp]); // 立即执行更新;这里的 vm[exp] 相当于执行了 get 116 | new Watcher(vm, exp, function (value) { 117 | // 每次创建 Watcher 实例,都会传入一个回调函数,使函数和 Watcher 实例之间形成一对一的挂钩关系 118 | // 将来数据发生变化时, Watcher 就能知道它更新的时候要执行哪个函数 119 | updaterFn && updaterFn(node, value); 120 | }); 121 | } 122 | 123 | textUpdater(node, value) { 124 | node.textContent = value; 125 | } 126 | 127 | htmlUpdater(node, value) { 128 | node.innerHTML = value; 129 | } 130 | 131 | modelUpdater(node, value) { 132 | node.value = value; 133 | } 134 | 135 | eventHandler(node, vm, exp, dir) { 136 | let fn = vm.$options.methods && vm.$options.methods[exp]; 137 | if (dir && fn) { 138 | node.addEventListener(dir, fn.bind(vm), false); 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /step3.2/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |
11 | {{ test }} 12 |
13 |

14 | 15 |

16 |

17 |

18 | 19 |

20 |
21 | 22 | 23 | 24 | 46 | 47 | 48 | --------------------------------------------------------------------------------