├── .gitignore ├── README.md ├── babel.config.js ├── html ├── 01-mixin.html ├── 02-computed.html ├── 03-watch.html ├── 04-array.html ├── 05-diff.html └── 06-component.html ├── index.html ├── package-lock.json ├── package.json ├── rollup.config.js └── src ├── compiler ├── index.js ├── parser.js └── type.js ├── global-static-api.js ├── hooks └── life-hook.js ├── index.js ├── init.js ├── initState.js ├── lifecycle.js ├── observe ├── array.js ├── dep.js ├── index.js └── watcher.js ├── utils ├── index.js ├── merge.js └── strategy.js └── vdom ├── index.js └── patch.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | dist -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-study 2 | 3 | ## vue2的常见源码实现 4 | 5 | ### rollup环境搭建 6 | 7 | #### 安装rollup及其插件 8 | 9 | ```shell 10 | npm i rollup rollup-plugin-babel @babel/core @babel/preset-env rollup-plugin-node-resolve -D 11 | ``` 12 | 13 | #### 编写配置文件 rollup.config.js 14 | 15 | 这个可以直接使用es module 16 | 17 | ```js 18 | // rollup默认可以导出一个对象 作为打包的配置文件 19 | import babel from "rollup-plugin-babel"; 20 | import resolve from 'rollup-plugin-node-resolve' 21 | export default { 22 | // 入口 23 | input: "./src/index.js", 24 | // 出口 25 | output: { 26 | // 生成的文件 27 | file: "./dist/vue.js", 28 | // 全局对象 Vue 在global(浏览器端就是window)上挂载一个属性 Vue 29 | name: "Vue", 30 | // 打包方式 esm commonjs模块 iife自执行函数 umd 统一模块规范 -> 兼容cmd和amd 31 | format: "umd", 32 | // 打包后和源代码做关联 33 | sourcemap: true, 34 | }, 35 | plugins: [ 36 | babel({ 37 | // 排除第三方模块 38 | exclude: "node_modules/**", 39 | }), 40 | // 自动找文件夹下的index文件 41 | resolve() 42 | ], 43 | }; 44 | 45 | 46 | ``` 47 | 48 | ##### babel.config.js 49 | 50 | ```js 51 | // babel config 52 | module.exports = { 53 | // 预设 54 | presets: ["@babel/preset-env"], 55 | }; 56 | 57 | ``` 58 | 59 | #### 编写脚本 60 | 61 | ```json 62 | "scripts": { 63 | "dev": "rollup -cw" 64 | } 65 | ``` 66 | 67 | -c表示使用配置文件,-w表示监控文件变化。 68 | 69 | #### element.outerHTML 70 | 71 | `outerHTML`属性获取描述元素(包括其后代)的序列化HTML片段。它也可以设置为用从给定字符串解析的节点替换元素。 72 | 73 | ```html 74 |
75 |

{{name}}

76 | {{age}} 77 |
78 | 87 | ``` 88 | 89 | ## 核心流程 90 | 91 | **vue的核心流程:** 92 | 93 | 1. 创造响应式数据 94 | 2. 模板编译 生成 ast 95 | 3. ast 转为render函数 后续每次数据更新 只执行render函数(不需要再次进行ast的转换) 96 | 4. render函数执行 生成 vNode节点(会使用到响应式数据) 97 | 5. 根据vNode 生成 真实dom 渲染页面 98 | 6. 数据更新 重新执行render 99 | 100 | ## 数据劫持 101 | 102 | **Vue2中使用的是Object.definedProperty**,**Vue3中直接使用Proxy了** 103 | 104 | ## 模板编译为ast 105 | 106 | vue2中使用的是正则表达式进行匹配,然后转换为ast树。 107 | 108 | 模板引擎 性能差 需要正则匹配 替换 vue1.0 没有引入虚拟dom的改变,vue2 采用虚拟dom 数据变化后比较虚拟dom的差异 最后更新需要更新的地方, 核心就是我们需要将模板变成我们的js语法 通过js语法生成虚拟dom,语法之间的转换 需要先变成抽象语法树AST 再组装为新的语法,这里就是把template语法转为render函数。 109 | 110 | ### ast转render 111 | 112 | 把生成的ast语法树,通过字符串拼接等方式转为render函数。 113 | render函数内部主要用到: 114 | 115 | 1. _c函数:创建元素虚拟dom节点 116 | 2. _v函数:创建文本虚拟dom节点 117 | 3. _s函数:将函数内的变量字符串化 118 | 119 | ### render函数生成真实dom 120 | 121 | 调用render函数,会生成虚拟dom,然后把虚拟dom转为真实DOM,挂载到页面即可。 122 | 123 | ## 回忆流程 124 | 125 | **核心流程:** 126 | 127 | 1. 数据处理成响应式,在 initState中处理的(针对对象来说主要是definedProperty,数组则是重写七个方法) 128 | 2. 模板编译:先把模板转成ast语法树,再把语法树生成**render函数** 129 | 3. 调用render函数,可能会进行变量的取值操作(_s函数内有变量),产生对应的虚拟dom 130 | 4. 虚拟dom渲染为真实dom,挂载到页面即可 131 | 132 | 133 | 134 | **完成了,虚拟和真实dom的渲染,也完成了响应式数据的处理,接下来需要进行视图和响应式数据的关联,在渲染页面的时候,收集依赖数据。** 135 | 136 | 1. 使用观察者模式实现依赖收集 137 | 2. 异步更新策略 138 | 3. mixin的实现原理 139 | 140 | 141 | 142 | ### 模板的依赖收集 143 | 144 | 要完成依赖的收集,很明显的就是,我们要如何得知,此模板在此次渲染的时候,用到了那些响应式数据。 145 | 146 | 我们可以给模板中的属性,增加一个**收集器(dep)**。这个收集器,是给每个属性单独增加的。页面渲染的时候,我们把渲染逻辑封装到watcher中。(其实就是手动更新视图的那两个方法app._update(app._render()))。让dep记住这个watcher即可,在属性变化了以后,可以找到对应的dep中存放的watcher,然后执行重新渲染页面。 147 | 148 | 这里面我们用到的方式其实就是**观察者模式**。 149 | 150 | ```js 151 | /** 152 | * watcher 进行实际的视图渲染 153 | * 每个组件都有自己的watcher,可以减少每次更新页面的部分 154 | * 给每个属性都增加一个dep,目的就是收集watcher 155 | * 一个视图(组件)可能有很多属性,多个属性对应一个视图 n个dep对应1个watcher 156 | * 一个属性也可能对应多个视图(组件) 157 | * 所以 dep 和 watcher 是多对多关系 158 | * 159 | * 每个属性都有自己的dep,属性就是被观察者 160 | * watcher就是观察者(属性变化了会通知观察者进行视图更新)-> 观察者模式 161 | */ 162 | class Watcher{} 163 | ``` 164 | 165 | 166 | 167 | 先让watcher收集dep,如果dep已经收集过,则不会再次收集。当dep被收集的时候,我们也会让dep反向收集当前的watcher。实现二者的双向收集。 168 | 169 | 然后在响应式数据发送改变的时候,通知dep的观察者(watcher)进行视图更新。 170 | 171 | ![image-20220415105750259](https://gitee.com/maolovecoding/picture/raw/master/images/web/webpack/image-20220415105750259.png) 172 | 173 | 174 | 175 | #### 视图同步渲染 176 | 177 | 此时,已经完成了响应式数据和视图的绑定,在数据发生改变的情况下,视图会同步更新。也就是说,我们更新了两次响应式数据,也会更新两次视图。 178 | 179 | ![image-20220415110028536](https://gitee.com/maolovecoding/picture/raw/master/images/web/webpack/image-20220415110028536.png) 180 | 181 | 正常情况下,更新两次视图是没有问题的,但是此时两次数据的更新发生在一次同步代码中,我们应该让视图的更新是异步的,这样在一次操作更新多个数据的情况下,也只会渲染一次视图,提高渲染速率。 182 | 183 | **那么我们的想法就是合并更新,在所有的更新数据做完以后,在刷新页面。也就是批处理,事件环。** 184 | 185 | 186 | 187 | #### 事件环 188 | 189 | 我们的期望就是,同步代码执行完毕之后,在执行视图的渲染(作为异步任务)。把更新操作延迟。 190 | 191 | 方法就是使用一个队列维护需要更新的watcher,第一次更新属性值的时候,就开启一个定时器,清空所有的watcher。后续的数据改变的操作,都不会再次开启定时器,只是会把需要更新的watcher再次入队列。(当然watcher我们会先去重)。 192 | 193 | 但是这个清空操作是在同步代码执行完毕后才会执行的。 194 | 195 | ```js 196 | // watcher queue 本次需要更新的视图队列 197 | let queue = []; 198 | // watcher 去重 {0:true,1:true} 199 | let has = {}; 200 | // 批处理 也可以说是防抖 201 | let pending = false; 202 | /** 203 | * 不管执行多少次update操作,但是我们最终只执行一轮刷新操作 204 | * @param {*} watcher 205 | */ 206 | function queueWatcher(watcher) { 207 | const id = watcher.id; 208 | // 去重 209 | if (!has[id]) { 210 | queue.push(watcher); 211 | has[id] = true; 212 | console.log(queue); 213 | if (!pending) { 214 | // 刷新队列 多个属性刷新 其实执行的只是第一次 合并刷新了 215 | setTimeout(flushSchedulerQueue, 0); 216 | pending = true; 217 | } 218 | } 219 | } 220 | /** 221 | * 刷新调度队列 且清理当前的标识 has pending 等都重置 222 | * 先执行第一批的watcher,如果刷新过程中有新的watcher产生,再次加入队列即可 223 | */ 224 | function flushSchedulerQueue() { 225 | const flushQueue = [...queue]; 226 | queue = []; 227 | has = {}; 228 | pending = false; 229 | // 刷新视图 如果在刷新过程中 还有新的watcher 会重新放到queueWatcher中 230 | flushQueue.forEach((watcher) => watcher.run()); // run 就是执行render 231 | } 232 | ``` 233 | 234 | 235 | 236 | #### nextTick 237 | 238 | **原理:** 239 | 240 | 因为我们数据的更新和视图的更新不再是同步,导致我们在同步获取视图最新的dom元素时,可能出现获取的元素和视图实际显示的元素不一致的情况。于是出现了 **nextTick方法** 241 | 242 | 实际上:nextTick方法内部也是维护了一个异步回调队列,开启一个定时器,每次调用该方法传入回调,都是把回调函数放入队列,并不是每次调用nextTick方法都开启一个定时器(比较销毁性能)。再放入第一个回调函数的时候,开启定时器,后续的回调函数只放入队列而不会再次开启定时器了,。所以nextTick不是创建了异步任务,而是将这个任务维护到了队列而已。 243 | 244 | **nextTick方法是同步还是异步?** 245 | 246 | 把任务(回调)放到队列是同步,实际执行任务是异步。 247 | 248 | ```js 249 | // 任务队列 250 | let callbacks = []; 251 | // 是否等待任务刷新 252 | let waiting = false; 253 | /** 254 | * 刷新异步回调函数队列 255 | */ 256 | function flushCallbacks() { 257 | const cbs = [...callbacks]; 258 | callbacks = []; 259 | waiting = false; 260 | cbs.forEach((cb) => cb()); 261 | } 262 | /** 263 | * 异步批处理 264 | * 是先执行内部的回调 还是用户的? 用个队列 排序 265 | * @param {Function} cb 回调函数 266 | */ 267 | export function nextTick(cb) { 268 | // 使用队列维护nextTick中的callback方法 269 | callbacks.push(cb); 270 | if (!waiting) { 271 | setTimeout(flushCallbacks, 0); // 刷新 272 | waiting = true; 273 | } 274 | } 275 | ``` 276 | 277 | 278 | 279 | #### vue的nextTick 280 | 281 | 实际上,vue的nextTick方法,内部并没有直接使用原生的某一个异步api(比如promise,setTimeout等)。而是采用优雅降级的方法。 282 | 283 | 1. 内部先采用的是promise(ie不兼容)。 284 | 2. 有一个和Promise等价的 [MutationObserve](https://developer.mozilla.org/zh-CN/docs/Web/API/MutationObserver)。也是异步微任务。(此API是H5的,只能在浏览器中使用) 285 | 3. 考虑ie浏览器专享的 setImmediate API。性能比settimeout好一些 286 | 4. 最后直接上setTimeout 287 | 288 | **采用优雅降级的目的,**还是为了用户可以尽快看见页面的渲染。 289 | 290 | ```js 291 | /** 292 | * 优雅降级 Promise -> MutationObserve -> setImmediate -> setTimeout(需要开线程 开销最大) 293 | */ 294 | let timerFunc = null; 295 | if (Promise) { 296 | timerFunc = () => Promise.resolve().then(flushCallbacks); 297 | } else if (MutationObserver) { 298 | // 创建并返回一个新的 MutationObserver 它会在指定的DOM发生变化时被调用(异步执行callback)。 299 | const observer = new MutationObserver(flushCallbacks); 300 | // TODO 创建文本节点的API 应该封装 为了方便跨平台 301 | const textNode = document.createTextNode(1); 302 | console.log("observer-----------------") 303 | // 监控文本值的变化 304 | observer.observe(textNode, { 305 | characterData: true, 306 | }); 307 | timerFunc = () => (textNode.textContent = 2); 308 | } else if (setImmediate) { 309 | // IE平台 310 | timerFunc = () => setImmediate(flushCallbacks); 311 | } else { 312 | timerFunc = () => setTimeout(flushCallbacks, 0); 313 | } 314 | ``` 315 | 316 | 对于vue3,肯定就不需要这种方式了,在不兼容ie的情况下,可以直接使用promise了。 317 | 318 | ![image-20220415150046818](https://gitee.com/maolovecoding/picture/raw/master/images/web/webpack/image-20220415150046818.png) 319 | 320 | 321 | 322 | 经过一次次处理,现在是可以在视图更新以后再去拿最新的dom了。 323 | 324 | 当然:对于更改值放在取值的下面,那么获取到的肯定还是旧的dom值。vue也是如此的。 325 | 326 | ![image-20220415150347883](https://gitee.com/maolovecoding/picture/raw/master/images/web/webpack/image-20220415150347883.png) 327 | 328 | 329 | 330 | 331 | 332 | ### mixin的实现 333 | 334 | Vue的mixin,可以实现全局混入和局部混入。 335 | 336 | 全局混入对所有组件实例都生效。 337 | 338 | **暂时我实现了生命周期的混入,对于data等其他特殊选项的合并还未处理。** 339 | 340 | 对于混入的生命周期,无论是一个还是多个相同的生命周期,最终我们都转为使用数组包裹,每个数组元素都是混入进来的生命周期。在创建组件实例的时候,把传入的选项和全局的Vue.options选项进行合并到实例上,实现混入效果。 341 | 342 | ![image-20220415220542253](https://gitee.com/maolovecoding/picture/raw/master/images/web/webpack/image-20220415220542253.png) 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | ### computed 351 | 352 | **计算属性:** 353 | 354 | 计算属性:依赖的值发生改变 才会重新执行用户的方法 计算属性需要维护一个dirty属性。而且在默认情况下,计算属性不会立刻执行,而是在用户取值的时候才会执行。 355 | 356 | 计算属性使用的两种方式: 357 | 358 | ```js 359 | computed: { 360 | /** 361 | * 计算属性:依赖的值发生改变 才会重新执行用户的方法 计算属性需要维护一个dirty属性 362 | */ 363 | // 只有get的计算属性 364 | fullName1() { 365 | return this.firstName + " " + this.lastName 366 | }, 367 | // getter and setter 368 | fullName2: { 369 | get() { 370 | return this.firstName + " " + this.lastName 371 | }, 372 | set(newVal) { 373 | [this.firstName, this.lastName] = newVal.split(" ") 374 | } 375 | } 376 | } 377 | ``` 378 | 379 | 380 | 381 | **特点:** 382 | 383 | 1. 计算属性本身就是一个defineProperty,响应式数据 384 | 2. 计算属性也是一个Watcher,默认渲染会创造一个渲染watcher 385 | 3. 如果watcher中有lazy属性,表明这是一个计算属性watcher 386 | 4. 计算属性维护了一个dirty,当我们直接修改计算属性的值,或者修改了计算属性依赖的值,那么计算属性自己的值并不会直接发生改变,而是使dirty的值发生改变。 387 | 5. 当dirty为false的时候,表示依赖的值没有发生改变,不需要再次计算,直接使用上次缓存的值即可。 388 | 6. 计算属性自身不会收集依赖,而是让计算属性依赖的属性去收集依赖(watcher) 389 | 390 | ```js 391 | /** 392 | * 初始化 computed 393 | * @param {Vue} vm 实例 394 | */ 395 | function initComputed(vm) { 396 | const computed = vm.$options.computed; 397 | const watchers = (vm._computedWatchers = {}); 398 | for (const key in computed) { 399 | const userDef = computed[key]; 400 | // function -> get 401 | // object -> {get(){}, set(newVal){}} 402 | let setter; 403 | const getter = isFunction(userDef) 404 | ? userDef 405 | : ((setter = userDef.set), getter); 406 | // 监控计算属性中 get的变化 407 | // 每次data的属性发生改变 重新执行的就是这个get 408 | // 传入额外的配置项 标明当前的函数 不需要立刻执行 只有在使用到计算属性了 才计算值 409 | // 把属性和watcher对应起来 410 | watchers[key] = new Watcher(vm, getter, { lazy: true }); 411 | // 劫持每一个计算属性 412 | defineComputed(vm, key, setter); 413 | } 414 | } 415 | /** 416 | * 定义计算属性 417 | * @param {*} target 418 | * @param {*} key 419 | * @param {*} setter 420 | */ 421 | function defineComputed(target, key, setter) { 422 | Object.defineProperty(target, key, { 423 | // vm.key -> vm.get key this -> vm 424 | get: createComputedGetter(key), 425 | set: setter, 426 | }); 427 | } 428 | /** 429 | * vue2.x 的计算属性 不会收集依赖,只是让计算属性依赖的属性去收集依赖 430 | * 创建一个懒执行(有缓存的)计算属性 判断值是否发生改变 431 | * 检查是否需要执行这个getter 432 | * @param {string} key 433 | */ 434 | function createComputedGetter(key) { 435 | // this -> vm 因为返回值给了计算属性的 get 我们是从 vm上取计算属性的 436 | return function lazyGetter() { 437 | // 对应属性的watcher 438 | const watcher = this._computedWatchers[key]; 439 | if (watcher.dirty) { 440 | // 如果是脏的 就去执行用户传入的getter函数 watcher.get() 441 | // 但是为了可以拿到get的执行结果 我们调用 evaluate函数 442 | watcher.evaluate(); // dirty = false 443 | } 444 | // 计算属性watcher出栈后 还有渲染watcher(在视图中使用了计算属性) 445 | // 或者说是在其他的watcher中使用了计算属性 446 | if (Dep.target) { 447 | // 让计算属性的watcher依赖的变量也去收集上层的watcher 448 | watcher.depend(); 449 | } 450 | return watcher.value; 451 | }; 452 | } 453 | ``` 454 | 455 | ![image-20220416140102371](https://gitee.com/maolovecoding/picture/raw/master/images/web/webpack/image-20220416140102371.png) 456 | 457 | ![image-20220416140057050](https://gitee.com/maolovecoding/picture/raw/master/images/web/webpack/image-20220416140057050.png) 458 | 459 | 460 | 461 | 462 | 463 | ### watch的实现 464 | 465 | watch选项是一个对象,每个watch的属性作为键, 466 | 467 | 1. 如果watch的属性直接是一个函数,那么会在属性值发生改变后,给该函数传入两个参数,新值和旧值。 468 | 469 | ```js 470 | // 就是一个观察者 471 | firstName(newVal, oldVal) { 472 | console.log(newVal, oldVal) 473 | } 474 | ``` 475 | 476 | 2. watch的属性是一个数组,数组元素可以是直接定义的函数,也可以是methods中的字符串函数名 477 | 478 | ```js 479 | // 就是一个观察者 480 | firstName:[ 481 | function (newVal, oldVal) { 482 | console.log(newVal, oldVal) 483 | }, 484 | function (newVal, oldVal) { 485 | console.log(newVal, oldVal) 486 | } 487 | ] 488 | ``` 489 | 490 | 3. watch也可以是一个methods中的字符串函数名 491 | 492 | 4. vm.$watch,上面三种的定义方式,最终都是转为vm.$watch的形式 493 | 494 | ```js 495 | const unwatch = vm.$watch(()=>vm.firstName, (newVal)=>{},options)// 额外选项options 496 | // 取消watch 497 | unwatch() 498 | 499 | vm.$watch(() => vm.firstName + vm.lastName, (newVal) => { 500 | console.log("类似侦听未定义的计算属性了",newVal) 501 | }) 502 | // 是字符串 则不需要再属性前加vm 503 | vm.$watch("firstName", (newVal) => { 504 | console.log(newVal) 505 | }) 506 | ``` 507 | 508 | ![image-20220416160038397](https://gitee.com/maolovecoding/picture/raw/master/images/web/webpack/image-20220416160038397.png) 509 | 510 | ![image-20220416160128191](https://gitee.com/maolovecoding/picture/raw/master/images/web/webpack/image-20220416160128191.png) 511 | 512 | 513 | 514 | ### 数组和对象元素更新实现原理 515 | 516 | 在vue中,我们知道数组有七个变异方法(会修改数组自身元素的方法),vue对这七个方法实现了重写,不然正常情况下我们使用这七个方法是没有办法实现响应式更新视图的。 517 | 518 | 而且对于一个对象,如果我们修改的是对象已经在data中定义好的对象的属性,当然是可以进行响应式更新的,但是,如果我们新增一个属性,视图是没有办法实现响应式更新的。 519 | 520 | 正常情况下,只有我们让数组属性的值变为一个新数组,或者对象属性变为一个新对象,这样才能让对于没有劫持的数组元素或者对象属性给劫持下来。 521 | 522 | ![image-20220416172738629](https://gitee.com/maolovecoding/picture/raw/master/images/web/webpack/image-20220416172738629.png) 523 | 524 | ```js 525 | // 数组数据响应式更新原理 526 | const vm = new Vue({ 527 | data: { 528 | arr: ["海贼王", "火影忍者", "名侦探柯南"], 529 | obj: { name: "张三" } 530 | }, 531 | el: "#app", 532 | // 模板编译为虚拟dom的时候,从arr对象取值了 _v(_s(变量)) JSON.stringify() 533 | // 所以对象会收集依赖 534 | template: ` 535 |
536 | 542 |
543 | ` 544 | }) 545 | setTimeout(() => { 546 | // 这种修改方式无法监控 547 | vm.arr[1] += 1 548 | // 也不会刷新视图 549 | vm.arr.length = 10; 550 | // 7个数组的变异方法可以监控到 因为我们重写了 551 | // 这里并没有改变 arr属性 只是改变了arr这个数组对象 552 | // arr数组对象自身并没有改变(没有变成新数组,地址没改变) 553 | vm.arr.push("12") 554 | vm.obj.age = 22 555 | console.log("1秒后更新。。。",vm.arr,vm.obj) 556 | }, 1000) 557 | ``` 558 | 559 | **所以我们为了能劫持修改数组自身和给对象新增属性等,也可以被Vue劫持,我们需要在数组,对象等引用类型的属性上,也让其自身具有dep,不仅仅是对象的属性,数组的元素等需要被劫持,数组,对象等自身也需要被劫持。** 560 | 561 | 也就是说:不管这个属性是原始类型,还是引用类型,都让其对应一个dep,用来收集依赖。 562 | 563 | ```js 564 | class Observe { 565 | constructor(data) { 566 | // 让引用数据自身也实现依赖收集 这个dep是放在 data.__ob__ = this 上的 567 | // 也就是说 data.__ob__.dep 并不是 data.dep 所以不会发生重复 568 | this.dep = new Dep(); 569 | // 记录this 也是一个标识 如果对象上有了该属性 标识已经被观测 570 | Object.defineProperty(data, "__ob__", { 571 | value: this, // observe的实例 572 | }); 573 | // 如果劫持的数据是数组 574 | if (Array.isArray(data)) { 575 | // 重写数组上的7个方法 这7个变异方法是可以修改数组本身的 576 | Object.setPrototypeOf(data, arrayProto); 577 | // 对于数组元素是 引用类型的,需要深度观测的 578 | this.observeArray(data); 579 | } else { 580 | // Object.defineProperty 只能劫持已经存在的属性(vue提供单独的api $set $delete 为了增加新的响应式属性) 581 | this.walk(data); 582 | } 583 | } 584 | /** 585 | * 循环对象 对属性依次劫持 重新‘定义’属性 586 | * @param {*} data 587 | */ 588 | walk(data) { 589 | Object.keys(data).forEach((key) => defineReactive(data, key, data[key])); 590 | } 591 | /** 592 | * 劫持数组元素 是普通原始值不会劫持 593 | * @param {Array} data 594 | */ 595 | observeArray(data) { 596 | data.forEach((item) => observe(item)); 597 | } 598 | } 599 | ``` 600 | 601 | ![image-20220416175015018](https://gitee.com/maolovecoding/picture/raw/master/images/web/webpack/image-20220416175015018.png) 602 | 603 | ![image-20220416175053115](https://gitee.com/maolovecoding/picture/raw/master/images/web/webpack/image-20220416175053115.png) 604 | 605 | 可以看见,修改数组自身的元素,视图也能正常更新。 606 | 607 | **但是要注意,直接使用arr[index]的方式修改元素,和新增对象还不存在的元素,目前还不能进行视图更新。** 608 | 609 | ![image-20220416175343403](https://gitee.com/maolovecoding/picture/raw/master/images/web/webpack/image-20220416175343403.png) 610 | 611 | 也就是说目前只是修改数组自身的7个变异方法,可以劫持到,并且实现视图更新。对于使用下标修改元素和修改数组的长度等,是不能劫持到的。 612 | 613 | **对于新增属性,需要使用vm.$set()方法新增才能实现劫持。** 614 | 615 | 通过上面的操作,给每个对象的观察者observe都挂上了一个dep,用来收集每个对象自身的依赖。 616 | 617 | 当我们给对象新增属性的时候,可以observe通知dep更新视图。 618 | 619 | ```js 620 | setTimeout(() => { 621 | vm.obj.age = 22 622 | vm.obj.__ob__.dep.notify()//$set原理 623 | console.log("1秒后更新。。。",vm.arr,vm.obj) 624 | }, 1000) 625 | ``` 626 | 627 | **$set本质上就是这种原理实现的。** 628 | 629 | 630 | 631 | #### 深度数据劫持 632 | 633 | 对于数组元素还是数组的这种情况,需要二次侦听。 634 | 635 | ![image-20220416202926167](https://gitee.com/maolovecoding/picture/raw/master/images/web/webpack/image-20220416202926167.png) 636 | 637 | ```js 638 | function dependArray(arr) { 639 | // console.log(arr); 640 | for (let i = 0; i < arr.length; i++) { 641 | const cur = arr[i]; 642 | // console.log(cur, cur.__ob__); 643 | // 数组元素可能不是数组了 644 | if (Array.isArray(cur)) { 645 | // 收集依赖 646 | cur.__ob__.dep.depend(); 647 | dependArray(cur); 648 | } 649 | } 650 | } 651 | ``` 652 | 653 | 把数组元素循环,对于元素还是数组的情况,让该数组自身也收集依赖。 654 | 655 | 656 | 657 | **数据劫持总结:** 658 | 659 | 1. 默认vue在初始化的时候 会对对象每一个属性都进行劫持,增加dep属性, 当取值的时候会做依赖收集 660 | 661 | 2. 默认还会对属性值是(对象和数组的本身进行增加dep属性) 进行依赖收集 662 | 663 | 3. 如果是属性变化 触发属性对应的dep去更新 664 | 665 | 4. 如果是数组更新,触发数组的本身的dep 进行更新 666 | 667 | 5. 如果取值的时候是数组还要让数组中的对象类型也进行依赖收集 (递归依赖收集) 668 | 669 | 6. 如果数组里面放对象,默认对象里的属性是会进行依赖收集的,因为在取值时 会进行JSON.stringify操作 670 | 671 | ![image-20220416203346466](https://gitee.com/maolovecoding/picture/raw/master/images/web/webpack/image-20220416203346466.png) 672 | 673 | 674 | 675 | 676 | 677 | ## diff算法 678 | 679 | diff算法: 680 | 681 | 在之前的更新中,每次数据更新,在更新视图时,都是完全产生新的虚拟节点,通过新的虚拟节点生成真实节点,用新生成的真实节点替换所有的老节点。 682 | 683 | 这种方法在页面元素很少的情况下性能销毁倒是无所谓,但是在页面元素特别多情况下,很明显是消耗很大性能的。哪怕我只是修改了一个dom的文本内容,也都需要重新生成一遍所有节点。(因为现在只有一个组件) 684 | 685 | 第一次渲染的时候,我们会产生虚拟节点,第二次更新我们也会调用render方法产生新的虚拟节点,我们需要对比两次的vnode,找到需要更新的部分进行更新。 686 | 687 | 688 | 689 | #### 没有key 690 | 691 | 对于没有key的情况下:vue会在两个vnode的tag相同的时候,就任务是同一个节点。这种情况下可能会出现错误复用。 692 | 693 | ```html 694 | 699 | 700 | 705 | ``` 706 | 707 | 此时vue只会让第一个节点和第一个节点比较,第二个节点和第二个节点比较。 708 | 709 | 710 | 711 | ### 有key 712 | 713 | vue在进行diff的时候(新旧虚拟dom都有子节点数组),维护了一个双指针,来进行比较。 714 | 715 | ```js 716 | // 我们为了比较两个儿子的时候,提高比较的性能(速度) 717 | /** 718 | * 1. 我们操作列表 经常会有 push pop shift unshift sort reverse 等方法 针对这些情况可以做一些优化 719 | * 2. vue2中采用双指针的方法 比较两个节点 720 | */ 721 | let oldStartIndex = 0, 722 | oldEndIndex = oldChildren.length - 1, 723 | newStartIndex = 0, 724 | newEndIndex = newChildren.length - 1, 725 | oldStartVnode = oldChildren[oldStartIndex], 726 | oldEndVnode = oldChildren[oldEndIndex], 727 | newStartVnode = newChildren[newStartIndex], 728 | newEndVnode = newChildren[newEndIndex]; 729 | ``` 730 | 731 | #### old head -> new head 732 | 733 | 新旧节点都进行头指针指向的头结点比较。如果两个子节点相同,则会进行复用。 734 | 735 | ```html 736 | 741 | 742 | 747 | ``` 748 | 749 | 此时vue会复用前两个节点(比对后发现前两个节点都不需要更改),只需要在原来的dom元素上追加一个子元素而已。 750 | 751 | 752 | 753 | #### old tail -> new tail 754 | 755 | 在头结点进行比较时,发现不是一个节点,则再次比较两个children的尾节点。 756 | 757 | ```html 758 | 763 | 764 | 768 | ``` 769 | 770 | 在头结点不同,尾节点相同的情况下,会一直比较尾节点,发现相同则复用,到下一轮循环发现头节点还是不一致,继续比对尾节点。此时页面渲染也只是会删除一个旧的dom。 771 | 772 | 773 | 774 | #### 交叉比对 775 | 776 | ##### old head -> new tail 777 | 778 | 在头结点和尾节点都不同的情况下,去比对旧vnode的头结点和新vnode的尾节点。 779 | 780 | ```html 781 | 786 | 787 | 792 | ``` 793 | 794 | 比较旧vnode的头节点和新vnode的尾节点发现一样,则进行复用,只需要移动dom元素的位置到其应该在的位置即可。 795 | 796 | 此时会复用这三个节点,只是会把第一个li移动到最后。 797 | 798 | 799 | 800 | ##### old tail -> new head 801 | 802 | 比较旧vnode的尾节点和新vnode的头结点,一样则也会复用节点。 803 | 804 | ```html 805 | 810 | 811 | 816 | ``` 817 | 818 | 此时也只是移动三个节点中key为a和c这两个dom元素的位置。 819 | 820 | 821 | 822 | #### 乱序比较 823 | 824 | 当前面四种情况都不符合,恭喜了,已经没办法优化了,或者说再想办法优化并不是那么划算了。因为这个时候我们已经需要拿新vnode中的每个节点,去和旧vnode中的每个节点依次比对,此时的时间复杂度已经是O(N^2)了。算是很高的复杂度了。 825 | 826 | 先根据旧节点vnode集合生成一个key和节点所在索引的map。 827 | 828 | ```js 829 | /** 830 | * 生成映射表 831 | * @param {*} children 832 | * @returns 833 | */ 834 | function makeIndexByKey(children) { 835 | const map = {}; 836 | children.forEach((child, index) => (map[child.key] = index)); 837 | return map; 838 | } 839 | ``` 840 | 841 | 我们让新vnode的每个节点,都拿出key去这个map中找旧节点的索引,如果找到则可以复用,找不到则需要创建新的dom元素然后插入到指定位置;如果找到了,则移动这个节点到指定位置,并且标识当前节点已经使用。 842 | 843 | ```js 844 | const map = makeIndexByKey(oldChildren); 845 | // ... 846 | // 乱序比对 a b c -> d e a b f 847 | /** 848 | * 根据老的列表做一个映射关系,用新的去找,找到则移动节点,找不到就新增节点,最后移除多余节点 849 | */ 850 | // 如有值:则是需要移动的节点的索引 851 | let moveIndex = map[newStartVnode.key]; 852 | if (moveIndex !== undefined) { 853 | const moveVnode = oldChildren[moveIndex]; 854 | // 移动节点到头指针所在节点的前面 855 | insertBefore(el, moveVnode, oldStartVnode.el); 856 | // 标识这个节点已经移动过 857 | oldChildren[moveIndex] = undefined; 858 | patchVnode(moveVnode, newStartVnode); 859 | } else { 860 | // 找不到 这是新节点 创建 然后插入进去 完事 861 | insertBefore(el, createEle(newStartVnode), oldStartVnode.el); 862 | } 863 | newStartVnode = newChildren[++newStartIndex]; 864 | ``` 865 | 866 | 此时,就完成了所有diff算法的步骤。 867 | 868 | ```html 869 | 875 | 876 | 884 | ``` 885 | 886 | 这种复杂的也能实现dom复用了。 887 | 888 | **此时对于key来说,是不能出现重复的。否则会报错。** 889 | 890 | 891 | 892 | 893 | 894 | 895 | 896 | 897 | 898 | **核心代码:**大概一百行左右吧。 899 | 900 | ```js 901 | function patchVnode(oldVNode, vnode) { 902 | /** 903 | * 1. 两个节点不是同一个节点,直接删除老的换上新的(不在继续对比属性等) 904 | * 2. 两个节点是同一个节点(tag,key都一致),比较两个节点的属性是否有差异 905 | * 复用老节点,将差异的属性更新 906 | */ 907 | const el = oldVNode.el; 908 | // 不是同一个节点 909 | if (!isSameVNode(oldVNode, vnode)) { 910 | // tag && key 911 | // 直接替换 912 | const newEl = createEle(vnode); 913 | replaceChild(el.parentNode, newEl, el); 914 | return newEl; 915 | } 916 | // 文本的情况 文本我们期望比较一下文本的内容 917 | vnode.el = el; 918 | if (!oldVNode.tag) { 919 | if (oldVNode.text !== vnode.text) { 920 | textContent(el, vnode.text); 921 | } 922 | } 923 | // 是标签 我们需要比对标签的属性 924 | patchProps(el, oldVNode.props, vnode.props); 925 | // 有子节点 926 | /** 927 | * 1.旧节点有子节点 新节点没有 928 | * 2. 都有子节点 929 | * 3. 旧节点没有子节点,新节点有 930 | */ 931 | const oldChildren = oldVNode.children || []; 932 | const newChildren = vnode.children || []; 933 | const oldLen = oldChildren.length, 934 | newLen = newChildren.length; 935 | if (oldLen && newLen) { 936 | // 完整的diff 都有子节点 937 | updateChildren(el, oldChildren, newChildren); 938 | } else if (newLen) { 939 | // 只有新节点有子节点 挂载 940 | mountChildren(el, newChildren); 941 | } else if (oldLen) { 942 | // 只有旧节点有子节点 全部卸载 943 | unmountChildren(el, oldChildren); 944 | } 945 | return el; 946 | } 947 | /** 948 | * 对比更新子节点 949 | * @param {*} el 950 | * @param {*} oldChildren 951 | * @param {*} newChildren 952 | */ 953 | // TODO 对于出现重复的key,有bug,还未修复。。。。 954 | function updateChildren(el, oldChildren, newChildren) { 955 | // 我们为了比较两个儿子的时候,提高比较的性能(速度) 956 | /** 957 | * 1. 我们操作列表 经常会有 push pop shift unshift sort reverse 等方法 针对这些情况可以做一些优化 958 | * 2. vue2中采用双指针的方法 比较两个节点 959 | */ 960 | let oldStartIndex = 0, 961 | oldEndIndex = oldChildren.length - 1, 962 | newStartIndex = 0, 963 | newEndIndex = newChildren.length - 1, 964 | oldStartVnode = oldChildren[oldStartIndex], 965 | oldEndVnode = oldChildren[oldEndIndex], 966 | newStartVnode = newChildren[newStartIndex], 967 | newEndVnode = newChildren[newEndIndex]; 968 | // 乱序比较时 使用的映射表 {key:"节点在数组中的索引"} -> {a:0,b:1,...} 969 | const map = makeIndexByKey(oldChildren); 970 | // 循环比较 只要头指针不超过尾指针 就一直比较 971 | while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) { 972 | // 排除 undefined 的情况 973 | if (!oldStartVnode) oldStartVnode = oldChildren[++oldStartIndex]; 974 | if (!oldEndVnode) oldEndVnode = oldChildren[--oldStartIndex]; 975 | /** 976 | * 1. old head -> new head 977 | * 2. old tail -> new tail 978 | * 3. old head -> new tail 979 | * 4. old tail -> new head 980 | */ 981 | // 进行节点比较 982 | else if (isSameVNode(oldStartVnode, newStartVnode)) { 983 | // 头结点相同 984 | // 从头指针开始比较两个节点 985 | // 相同节点 递归比较子节点 986 | patchVnode(oldStartVnode, newStartVnode); 987 | oldStartVnode = oldChildren[++oldStartIndex]; 988 | newStartVnode = newChildren[++newStartIndex]; 989 | } else if (isSameVNode(oldEndVnode, newEndVnode)) { 990 | // 尾节点相同 991 | // 从尾指针开始比较两个节点 992 | patchVnode(oldEndVnode, newEndVnode); 993 | oldEndVnode = oldChildren[--oldEndIndex]; 994 | newEndVnode = newChildren[--newEndIndex]; 995 | } 996 | // 交叉比对 两次头尾比较 997 | // a b c -> c a b 把尾节点移动到头结点之前 998 | else if (isSameVNode(oldEndVnode, newStartVnode)) { 999 | patchVnode(oldEndVnode, newStartVnode); 1000 | console.log(oldEndVnode, newStartVnode); 1001 | // 将老节点的尾节点插入到老节点头结点(头结点会变化)的前面去 1002 | insertBefore(el, oldEndVnode.el, oldStartVnode.el); 1003 | oldEndVnode = oldChildren[--oldEndIndex]; 1004 | newStartVnode = newChildren[++newStartIndex]; 1005 | } 1006 | // a b c d -> d c b a 头结点移动到尾节点后面 1007 | else if (isSameVNode(oldStartVnode, newEndVnode)) { 1008 | patchVnode(oldStartVnode, newEndVnode); 1009 | insertBefore(el, oldStartVnode.el, oldEndVnode.el.nextSibling); 1010 | oldStartVnode = oldChildren[++oldStartIndex]; 1011 | newEndVnode = newChildren[--newEndIndex]; 1012 | } else { 1013 | // 乱序比对 a b c -> d e a b f 1014 | /** 1015 | * 根据老的列表做一个映射关系,用新的去找,找到则移动节点,找不到就新增节点,最后移除多余节点 1016 | */ 1017 | // 如有值:则是需要移动的节点的索引 1018 | let moveIndex = map[newStartVnode.key]; 1019 | if (moveIndex !== undefined) { 1020 | const moveVnode = oldChildren[moveIndex]; 1021 | // 移动节点到头指针所在节点的前面 1022 | insertBefore(el, moveVnode, oldStartVnode.el); 1023 | // 标识这个节点已经移动过 1024 | oldChildren[moveIndex] = undefined; 1025 | patchVnode(moveVnode, newStartVnode); 1026 | } else { 1027 | // 找不到 这是新节点 创建 然后插入进去 完事 1028 | insertBefore(el, createEle(newStartVnode), oldStartVnode.el); 1029 | } 1030 | newStartVnode = newChildren[++newStartIndex]; 1031 | } 1032 | } 1033 | // 新节点的比旧节点多 挂载 1034 | if (newStartIndex <= newEndIndex) { 1035 | for (let i = newStartIndex; i <= newEndIndex; i++) { 1036 | // 这里可能是向后追加 也可能是向前插入 1037 | // 判断当前的虚拟dom后面是否还有节点 有节点则是插入到该节点前面 1038 | const anchor = newChildren[newEndIndex + 1]?.el; 1039 | // 注意:插入方法在 要插入的那个节点不存在的情况下,自动变为追加方法 appendChild 1040 | insertBefore(el, createEle(newChildren[i]), anchor); 1041 | } 1042 | } 1043 | // 旧节点比新节点多 卸载 1044 | if (oldStartIndex <= oldEndIndex) { 1045 | for (let i = oldStartIndex; i <= oldEndIndex; i++) { 1046 | // 乱序比对时 可能已经标记为 undefined了 1047 | oldChildren[i] && removeChild(el, oldChildren[i].el); 1048 | } 1049 | } 1050 | } 1051 | /** 1052 | * 生成映射表 1053 | * @param {*} children 1054 | * @returns 1055 | */ 1056 | function makeIndexByKey(children) { 1057 | const map = {}; 1058 | children.forEach((child, index) => (map[child.key] = index)); 1059 | return map; 1060 | } 1061 | ``` 1062 | 1063 | 1064 | 1065 | 1066 | 1067 | 1068 | 1069 | ### 为什么需要key 1070 | 1071 | 直接将新节点替换老节点,很消耗性能,所以我们不直接替换,而是在比较两个节点之间的区别之后在替换,这就是diff算法。 1072 | 1073 | diff算是 是一个平级比较的过程,父亲和父亲节点比对 儿子和儿子节点比对。 1074 | 1075 | 我们在比较两个虚拟dom是否一致的时候,是根据虚拟dom的标签名和key值来进行比较的。如果没有key,相当于只要标签名一致,我我们就认为这两个虚拟节点是一样的,然后判断其子元素... 1076 | 1077 | 当我们在遍历动态列表,给其增加key的时候,要尽量避免使用索引作为key,因为两次的虚拟dom的key都是从0开始的,可能会发生错误复用。 1078 | 1079 | **注意:在vue和react中,我们说的key要唯一,实际上是在同级的vnode情况下(也就是兄弟节点这些),并不意味着key需要全局唯一。** 1080 | 1081 | 1082 | 1083 | 1084 | 1085 | ## 实现组件 1086 | 1087 | Vue中,一般一个项目只有一个根组件,也就是 new Vue产生的app。 1088 | 1089 | 但是一个页面不可能只由一个组件构成,很明显我们需要实现自定义组件。 1090 | 1091 | vue中提供了两种自定义组件的方式: 1092 | 1093 | 1. 全局组件 1094 | 2. 局部组件 1095 | 1096 | **组件的使用流程:** 1097 | 1098 | 在任意一个组件中,都可以使用其他组件。当我们在一个组件中使用其他组件的时候,会先去组件内部的局部组件中找是否定义过该组件,如果定义了,则直接使用该局部组件;如果没有定义局部组件,则去全局组件中寻找(和js中的原型,原型链很像了)。所以vue内部很可能也是利用类似于继承的这种模型实现组件的定义的。 1099 | 1100 | 其实vue内部在定义组件的时候,表面上我们是传递了一个对象: 1101 | 1102 | ```js 1103 | Vue.component("cmp",{ 1104 | //... 1105 | }) 1106 | ``` 1107 | 1108 | 实际上这个对象内部也会被Vue.extend给包裹,变成`子类`. 1109 | 1110 | ```js 1111 | Vue.component("cmp",Vue.extend({ 1112 | //... 1113 | })) 1114 | ``` 1115 | 1116 | 1117 | 1118 | 1119 | 1120 | ### 组件的三大特性 1121 | 1122 | 1. 自定义标签 1123 | 2. 组件有自己的属性和事件 1124 | 3. 组件的插槽 1125 | 1126 | 1127 | 1128 | 1129 | 1130 | ### Vue.extend的实现 1131 | 1132 | 既然组件的实现内部还是需要调用extend方法,那么就先把extend实现出来。 1133 | 1134 | **用法:**使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。 1135 | 1136 | **实现:** 1137 | 1138 | 这个实现就不难了:不过就是实现一个构造函数,让该函数继承Vue而已。就是组合式继承。 1139 | 1140 | ```js 1141 | /** 1142 | * 使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。 1143 | * 返回值是一个构造函数 通过new可以创建一个vue组件实例 1144 | * @param {{data:Function,el:string}} options 1145 | * @returns 1146 | */ 1147 | Vue.extend = function (options) { 1148 | // 组合式继承 Vue 1149 | function Sub(options = {}) { 1150 | // 最终使用的组件 就是 new 一个实例 1151 | this._init(options); 1152 | } 1153 | Sub.prototype = Object.create(Vue.prototype); 1154 | Object.defineProperty(Sub.prototype, "constructor", { 1155 | value: Sub, 1156 | writable: true, 1157 | configurable: true, 1158 | }); 1159 | Sub.options = options; // 保存用户传递的选项 1160 | return Sub; 1161 | }; 1162 | ``` 1163 | 1164 | ![image-20220417223435741](https://gitee.com/maolovecoding/picture/raw/master/images/web/webpack/image-20220417223435741.png) 1165 | 1166 | 1167 | 1168 | 1169 | 1170 | ### Vue.component实现 1171 | 1172 | **参数:** 1173 | 1174 | - id: string 1175 | - definition?: Function | object 1176 | 1177 | **用法:**注册或获取全局组件。注册还会自动使用给定的 `id` 设置组件的名称 1178 | 1179 | ```js 1180 | // 注册组件,传入一个扩展过的构造器 1181 | Vue.component('my-component', Vue.extend({ /* ... */ })) 1182 | 1183 | // 注册组件,传入一个选项对象 (自动调用 Vue.extend) 1184 | Vue.component('my-component', { /* ... */ }) 1185 | 1186 | // 获取注册的组件 (始终返回构造器) 1187 | var MyComponent = Vue.component('my-component') 1188 | ``` 1189 | 1190 | **实现:** 1191 | 1192 | ```js 1193 | // 维护一个 全局组件对象 1194 | Vue.options.components = {}; 1195 | /** 1196 | * 定义或者获取全局组件 没有获取到组件时 返回 undefined 1197 | * @param {string} id 1198 | * @param {Function | object} definition 1199 | */ 1200 | Vue.component = function component(id, definition) { 1201 | // 获取全局组件 1202 | if (!definition) return Vue.options[id]; 1203 | // 如果 definition 是一个函数,说明用户自己调用了 Vue.extend 1204 | // 不是函数 就用 extend函数包装一下 1205 | !isFunction(definition) && (definition = Vue.extend(definition)); 1206 | Vue.options.components[id] = definition; 1207 | }; 1208 | ``` 1209 | 1210 | 实现全局的组件注册并不难,其核心还是利用了extend方法。 1211 | 1212 | 1213 | 1214 | ### 全局component和局部component 1215 | 1216 | 对于一个组件中,我们如果使用了一个其他组件,且在全局和局部都注册了一个同名的组件,那么我们会优先使用哪个?vue中会优先使用组件内部注册的局部组件。 1217 | 1218 | 我们在处理创建组件时的配置的时候,要维护一下:`components:{"btn":{}}.__proto__ -> Vue.options.components` 1219 | 1220 | ```js 1221 | const Cmp = Vue.extend({ 1222 | template: `
1223 |

你好!{{name}}

1224 | 1225 |
`, 1226 | components:{ 1227 | btn:{ 1228 | template:`` 1229 | } 1230 | } 1231 | }); 1232 | Vue.component("btn",{ 1233 | template:`` 1234 | }) 1235 | const cmp = new Cmp({ 1236 | data: { 1237 | name: "张三" 1238 | } 1239 | }) 1240 | cmp.$mount("#app") 1241 | ``` 1242 | 1243 | 我们需要修改一下当时extend和合并选项的部分代码实现: 1244 | 1245 | ![image-20220417233341024](https://gitee.com/maolovecoding/picture/raw/master/images/web/webpack/image-20220417233341024.png) 1246 | 1247 | ![image-20220417233534097](https://gitee.com/maolovecoding/picture/raw/master/images/web/webpack/image-20220417233534097.png) 1248 | 1249 | 1250 | 1251 | **不过这样还是有一些小bug,我觉得这样实现就更加完美了。**不过在vue中的实现方式还是上面那种。 1252 | 1253 | 把合并策略再次修改一下: 1254 | 1255 | ```js 1256 | strategy.components = function (parentVal, childVal) { 1257 | // 已经和全局组件对象创建关系了,则不需要再次建立关系 直接返回 1258 | if (Object.getPrototypeOf(parentVal) === Vue.options.components) 1259 | return parentVal; 1260 | // 通过父亲 创建一个对象 原型上有父亲的所有属性和方法 1261 | const res = Object.create(parentVal); // {}.__proto__ = parentVal 1262 | if (childVal) { 1263 | for (const key in childVal) { 1264 | // 拿到所有的孩子的属性和方法 1265 | res[key] = childVal[key]; 1266 | } 1267 | } 1268 | return res; 1269 | }; 1270 | ``` 1271 | 1272 | ![image-20220417235410784](https://gitee.com/maolovecoding/picture/raw/master/images/web/webpack/image-20220417235410784.png) 1273 | 1274 | 1275 | 1276 | 1277 | 1278 | **实现了组件的寻找规则,接下来只需要在组件的模板解析时,去寻找组件并渲染子组件。** 1279 | 1280 | 之前我们都是模板生成ast以后,然后生成虚拟dom,下一步就是比对节点生成真实dom了。 1281 | 1282 | 但是当我们引入组件以后,就需要对元素再次分类,分类出组件的虚拟节点和其他的普通节点。 1283 | 1284 | 我们需要在生成vnode的时候,判断出该标签是原始标签还是自定义组件的标签。 1285 | 1286 | 一个朴素无华的操作就是判断此tag是否是所有原始标签的一种。。。 1287 | 1288 | ```js 1289 | const ReservedTags = [ 1290 | "div", 1291 | "h1", 1292 | "h2", 1293 | "h3", 1294 | "h4", 1295 | "h5", 1296 | "h6", 1297 | "span", 1298 | "ul", 1299 | "ol", 1300 | "li", 1301 | "a", 1302 | "table", 1303 | "button", 1304 | "input", 1305 | ]; 1306 | 1307 | const isReservedTag = (tag) => { 1308 | return ReservedTags.includes(tag); 1309 | }; 1310 | ``` 1311 | 1312 | 1313 | 1314 | #### 渲染流程 1315 | 1316 | **Vue.component**的作用就是进行组件的全局定义而已。把id和definition对应。让 Vue.options.componnets[id] = definition。只是如果definition是对象的情况下,会帮我们使用extend进行包裹成构造函数(Vue子类)。 1317 | 1318 | - Vue.extend返回值就是一个Vue子类,一个继承了父类Vue的构造函数。(为什么Vue的组件中的data不能是一个对象呢?) 1319 | 1320 | ```js 1321 | Vue.extend({ 1322 | data:{} 1323 | }) 1324 | ``` 1325 | 1326 | ![image-20220418124139726](https://gitee.com/maolovecoding/picture/raw/master/images/web/webpack/image-20220418124139726.png) 1327 | 1328 | 我们在实例化这个返回的子类的时候,也就是 new Sub,会调用父亲Vue上的_init方法,然后在该方法的内部,又会进行mergeOptions合并选项的操作。也就是每次合并选项,都会把子类上的options都拿一份放到实例自己的$options上。如果data是一个对象,那么每次都会把data的引用放到实例对象自己身上。 1329 | 1330 | 多个子类实例会共享一个Sub上的options.data。但是如果data是一个函数,我们虽然也是直接把data放到实例对象的身上,但是在初始化属性拦截数据的时候,发现data是一个函数的情况下,我们会执行这个函数,拿到真正的data数据。每次执行函数返回的都是一个全新的对象,哪怕每个对象的所有属性都一样,但是他们直接不会相互影响。 1331 | 1332 | 1333 | 1334 | 在创建子类的构造函数的时候,会把全局的组件和自己身上定义的组件进行合并(组件的合并规则,先找自己身上是否有该组件,没有的情况下,然后去全局查找) 1335 | 1336 | 1337 | 1338 | **组件的渲染:** 1339 | 1340 | 开始渲染的组件会编译组件的模板,变成render函数。然后调用render方法。 1341 | 1342 | createElementVNode会根据tag类型来区分否是普通节点和组件节点。 1343 | 1344 | ![image-20220418125436483](https://gitee.com/maolovecoding/picture/raw/master/images/web/webpack/image-20220418125436483.png) 1345 | 1346 | 对于组件节点:我们在创建的时候,会给一个标识,包含组件的构造函数。且在data中增加一个初始化的init钩子。 1347 | 1348 | ![image-20220418125529930](https://gitee.com/maolovecoding/picture/raw/master/images/web/webpack/image-20220418125529930.png) 1349 | 1350 | 稍后在创建组件对应的真实节点的时候,只需要new Ctor即可。 1351 | 1352 | 1353 | 1354 | **创建真实节点:** 1355 | 1356 | 在创建真实节点的时候,也就是在*createEle*方法内部,我们可以调用createComponent方法来创建组件。如果是组件,当然就会调用上面创建组件的虚拟节点的时候,插入的init的hook。然后返回组件生成的$el;不是组件当然也无伤大雅,会不满足组件的条件,正常往普通组件的流程往下走。 1357 | 1358 | ```js 1359 | function createComponent(vnode) { 1360 | // init 初始化组件 1361 | vnode.props?.hook?.init(vnode); 1362 | return vnode.componentInstance; 1363 | } 1364 | ``` 1365 | 1366 | 1367 | 1368 | ![image-20220418135730777](https://gitee.com/maolovecoding/picture/raw/master/images/web/webpack/image-20220418135730777.png) 1369 | 1370 | ![image-20220418141046739](https://gitee.com/maolovecoding/picture/raw/master/images/web/webpack/image-20220418141046739.png) 1371 | 1372 | 所以到此为止,就实现了组件的渲染流程。 1373 | 1374 | ![image-20220418141303290](https://gitee.com/maolovecoding/picture/raw/master/images/web/webpack/image-20220418141303290.png) 1375 | 1376 | 1377 | 1378 | 1379 | 1380 | ## 源码阅读 1381 | 1382 | 1. 安装源码依赖 *npm install* 1383 | 2. *npm run dev*是否可以打包成功 1384 | 1385 | 1386 | 1387 | **代码结构:** 1388 | 1389 | 1390 | 1391 | 1392 | 1393 | 1394 | 1395 | 1396 | 1397 | 1398 | 1399 | 1400 | 1401 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | // babel config 2 | module.exports = { 3 | // 预设 4 | presets: ["@babel/preset-env"], 5 | }; 6 | -------------------------------------------------------------------------------- /html/01-mixin.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Document 9 | 10 | 11 | 12 |
13 | 14 | 15 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /html/02-computed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Document 9 | 10 | 11 | 12 |
13 | 14 | 15 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /html/03-watch.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Document 9 | 10 | 11 | 12 |
13 | 14 | 15 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /html/04-array.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Document 9 | 10 | 11 | 12 |
13 | 14 | 15 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /html/05-diff.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Document 9 | 10 | 11 | 12 |
13 | 14 | 15 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /html/06-component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Document 9 | 10 | 11 | 12 |
13 | 22 | 23 | 24 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Document 9 | 10 | 11 | 12 |
13 | 15 |
16 | 17 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue2-stage", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "rollup -cw" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "@babel/core": "^7.17.9", 14 | "@babel/preset-env": "^7.16.11", 15 | "rollup": "^2.70.1", 16 | "rollup-plugin-babel": "^4.4.0", 17 | "rollup-plugin-node-resolve": "^5.2.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | // rollup默认可以导出一个对象 作为打包的配置文件 2 | import babel from "rollup-plugin-babel"; 3 | import resolve from 'rollup-plugin-node-resolve' 4 | export default { 5 | // 入口 6 | input: "./src/index.js", 7 | // 出口 8 | output: { 9 | // 生成的文件 10 | file: "./dist/vue.js", 11 | // 全局对象 Vue 在global(浏览器端就是window)上挂载一个属性 Vue 12 | name: "Vue", 13 | // 打包方式 esm commonjs模块 iife自执行函数 umd 统一模块规范 -> 兼容cmd和amd 14 | format: "umd", 15 | // 打包后和源代码做关联 16 | sourcemap: true, 17 | }, 18 | plugins: [ 19 | babel({ 20 | // 排除第三方模块 21 | exclude: "node_modules/**", 22 | }), 23 | // 自动找文件夹下的index文件 24 | resolve() 25 | ], 26 | }; 27 | -------------------------------------------------------------------------------- /src/compiler/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 毛毛 3 | * @Date: 2022-04-14 12:35:12 4 | * @Last Modified by: 毛毛 5 | * @Last Modified time: 2022-04-16 09:12:58 6 | * @Description 模板编译 7 | * 模板引擎实现原理: with + new Function 8 | */ 9 | 10 | import { parseHTML, defaultTagRE } from "./parser"; 11 | import { TEXT_TYPE, ELEMENT_TYPE } from "./type"; 12 | /** 13 | * 生成 render函数 14 | * @param {*} template 模板 15 | * @returns {Function} 16 | */ 17 | export function compileToFunction(template) { 18 | // console.log("compileToFunction-------------->" + template + "---------"); 19 | // 1. template 转 ast 20 | const ast = parseHTML(template); 21 | // console.log(ast); 22 | // 2. 生成render方法(该方法的执行结果是返回虚拟dom) 23 | // TODO 三个方法 _v文本节点 _s把变量转为字符串 _c元素节点 24 | // 2.1 生成render函数的返回代码块字符串形式 25 | const renderCodeBlock = codeGenerator(ast); 26 | // 2.2 生成render函数 new Function 27 | // 生成的代码中,取变量的值的时候,并没有去当前组件实例的上下文中取值 28 | // 而是直接 name age 所以这里绑定上下文(组件实例) name -> vm.name -> vm._data.name 29 | // this -> render.call(thisArg) 30 | const render = new Function(`with(this){\n return ${renderCodeBlock}}`); 31 | // console.log(render); 32 | return render; 33 | } 34 | 35 | /** 36 | * 根据ast生成代码 37 | * @param {{tag:string,children:Array,type:number,text:string,attrs:Array}} ast 38 | */ 39 | function codeGenerator(ast) { 40 | const children = generateChildren(ast.children); 41 | let code = `_c('${ast.tag}',${ 42 | ast.attrs.length > 0 ? generateProps(ast.attrs) : "null" 43 | }${ast.children?.length ? `,${children}` : ""})`; 44 | return code; 45 | } 46 | /** 47 | * 生成属性对象 {name:"",id:"app"} 48 | * @param {Array<{name:string|symbol,value:any}>} attrs 49 | */ 50 | function generateProps(attrs) { 51 | let str = ""; 52 | for (let i = 0; i < attrs.length; i++) { 53 | const attr = attrs[i]; 54 | if (attr.name === "style") { 55 | // style:"color:red;background-color:{{backgroundColor}}" 56 | // style:{color:"red","background-color":"{{backgroundColor}}"} 57 | // let style = ""; 58 | const style = {}; 59 | attr.value.split(";").forEach((item) => { 60 | if (!item.trim()) return; 61 | let [key, value] = item.split(":"); 62 | // let match = null; 63 | // defaultTagRE.lastIndex = 0; 64 | // match = defaultTagRE.exec(value); 65 | // if (match) { 66 | // value = `_s(${match[1]})`; 67 | // } else value = `'${value}'`; 68 | style[key] = value; 69 | // style += `'${key}':${value},`; 70 | // console.log(style); 71 | }); 72 | // str += `${attr.name}:{${style.slice(0, -1)}},`; 73 | str += `${attr.name}:${JSON.stringify(style)},`; 74 | } else str += `"${attr.name}":${JSON.stringify(attr.value)},`; 75 | } 76 | return `{${str.slice(0, -1)}}`; 77 | } 78 | /** 79 | * 生成节点的子节点数组对象 80 | * @param {*} children 81 | */ 82 | function generateChildren(children) { 83 | if (children) { 84 | return children.map((child) => generateChild(child)).join(","); 85 | } 86 | } 87 | /** 88 | * 根据节点生成子字符串形式 89 | * @param {*} node 90 | * @returns 91 | */ 92 | function generateChild(node) { 93 | switch (node.type) { 94 | case ELEMENT_TYPE: 95 | // 元素节点 96 | // console.log(codeGenerator(node)) 97 | return codeGenerator(node); 98 | case TEXT_TYPE: 99 | // console.log(node.text) 100 | // 文本节点 101 | const text = node.text; 102 | if (!defaultTagRE.test(text)) { 103 | // 纯文本节点 没有 {{xx}} 104 | return `_v(${JSON.stringify(text)})`; 105 | } 106 | // console.log(text); 107 | const tokens = []; 108 | // 匹配结果 109 | let match = null; 110 | defaultTagRE.lastIndex = 0; 111 | // 最后一次匹配结果的起始索引位置 112 | let lastIndex = 0; 113 | while ((match = defaultTagRE.exec(text))) { 114 | // console.log(match) 115 | // 当前匹配的到的起始位置 116 | let index = match.index; 117 | if (index > lastIndex) 118 | tokens.push(JSON.stringify(text.slice(lastIndex, index))); 119 | tokens.push(`_s(${match[1].trim()})`); 120 | lastIndex = index + match[0].length; 121 | } 122 | // {{age}}--- 最后一次匹配后还有内容 123 | if (lastIndex < text.length) { 124 | tokens.push(JSON.stringify(text.slice(lastIndex))); 125 | } 126 | // console.log(tokens); 127 | return `_v(${tokens.join("+")})`; 128 | default: 129 | return ""; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/compiler/parser.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 毛毛 3 | * @Date: 2022-04-13 12:25:22 4 | * @Last Modified by: 毛毛 5 | * @Last Modified time: 2022-04-14 14:28:15 6 | * @Description 模板编译 7 | * 解析模板 可以使用现成的包 html-parser2 8 | */ 9 | import { ELEMENT_TYPE, TEXT_TYPE } from "./type"; 10 | /** 11 | * 匹配标签名 12 | * 开头不能包含特殊字符和数字 13 | * 第二个字符开始 可以是任意字符了 / \ 空白符 . 都可以 14 | * div _div _ab88 a_9.//a 15 | * 16 | */ 17 | const ncname = "[a-zA-Z_][\\-\\.0-9a-zA-Z]*"; 18 | /** 19 | * 捕获 标签名 20 | * 注意 ?: 只匹配不捕获 21 | * 这里的匹配标签名 后面还有:的这种 是带命名空间的标签 比如 a:b 22 | */ 23 | const qnameCapture = `((?:${ncname}\\:)?${ncname})`; 24 | // ((?:[a-zA-Z_][\\-\\.0-9a-zA-Z]*\\:)?[a-zA-Z_][\\-\\.0-9a-zA-Z]*) 25 | /** 26 | * 匹配到的分组是一个 标签名
因为 /具有特殊含义 32 | */ 33 | // ^<\\/((?:[a-zA-Z_][\\-\\.0-9a-zA-Z]*\\:)?[a-zA-Z_][\\-\\.0-9a-zA-Z]*)[^>]*> 34 | const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); 35 | // `^<\\/${qnameCapture}[^>]*>` 36 | /** 37 | * 匹配属性 a="abc" a='abc' a=abc a 38 | * 分组一的值就是键key 分组3/4/5匹配到的是value 39 | */ 40 | const attribute = 41 | /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/; 42 | /** 43 | * 匹配标签结束 44 | * 标签可能自闭合

/> 45 | */ 46 | const startTagClose = /^\s*(\/?)>/; 47 | /** 48 | * 匹配 双花括号语法 {{}} 匹配到的是就是双花括号的 变量 49 | */ 50 | export const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g; 51 | /** 52 | * 解析 模板 53 | * @param {string} html 模板字符串 54 | * vue2采用正则编译解析 vue3不是采用正则了 55 | */ 56 | function parseHTML(html) { 57 | /** 58 | * 最终需要转换为一颗抽象语法树 ast abstract syntax tree 59 | * 可以借助栈思想 60 | * 栈中的最后一个标签元素 就是当前正在匹配的元素的父元素 61 | * @type {Array<{tag:string,type:number,children:Array}>} 62 | */ 63 | const stack = []; 64 | // 栈帧 指向最后一个元素 65 | let curParent = null; 66 | let root = null; // 根元素 67 | 68 | /** 69 | * 创建 ast 70 | * @param {string} tag 标签名 71 | * @param {Array<{name:string,value:any}>} attrs 属性 72 | * @returns 73 | */ 74 | function createASTElement(tag, attrs) { 75 | return { 76 | tag, 77 | type: ELEMENT_TYPE, 78 | children: [], 79 | attrs, 80 | parent: null, 81 | }; 82 | } 83 | /** 84 | * 处理开始标签 并且开始构造抽象语法树 85 | * @param {string} tag 86 | * @param {Array<{name:string,value:any}>} attrs 87 | * @param {boolean} isSelfClose 是否自闭合 88 | */ 89 | function start(tag, attrs, isSelfClose) { 90 | // console.log(tag, attrs); 91 | // 当前节点 92 | const node = createASTElement(tag, attrs); 93 | // 根节点 94 | root = root ?? node; 95 | // 更新当前节点的父节点 更新父元素的子元素节点 96 | curParent && ((node.parent = curParent), curParent.children.push(node)); 97 | // TODO 是自闭合标签 不需要入栈的 98 | if (isSelfClose) return; 99 | // 新节点入栈 100 | stack.push(node); 101 | // 更新当前指向的最前面的父节点 102 | curParent = node; 103 | // console.log(node, root); 104 | } 105 | /** 106 | * 处理文本内容 107 | * @param {string} text 108 | */ 109 | function chars(text) { 110 | // 去除空字符串 111 | text = text.replace(/^\s+|\s+$/gm, ""); 112 | // console.log(text); 113 | // 文本节点 插入到父元素的孩子中 114 | text && 115 | curParent.children.push({ 116 | type: TEXT_TYPE, 117 | text, 118 | parent: curParent, 119 | }); 120 | } 121 | /** 122 | * 处理结束标签 123 | * @param {string} tag 标签名称 124 | */ 125 | function end(tag) { 126 | // console.log(tag); 127 | // 弹出最后一个栈元素 并更新指向的父节点 128 | const node = stack.pop(); 129 | // TODO 可以根据tag和node.tag 校验标签是否合法等 也需要考虑自闭合标签 130 | if (tag !== node.tag) { 131 | // console.log("标签不合法---------",tag, node); 132 | } 133 | curParent = stack[stack.length - 1]; 134 | } 135 | /** 136 | * 解析模板的开始标签 137 | * @param {string} html 模板字符串 138 | */ 139 | function parseStartTag() { 140 | // 匹配标签起始位置 141 | const start = html.match(startTagOpen); 142 | if (start) { 143 | // 是开始标签 144 | const match = { 145 | // 标签名 146 | tagName: start[1], 147 | // 属性 148 | attrs: [], 149 | // 是否是自闭合标签 150 | isSelfClose: false, 151 | }; 152 | advance(start[0].length); 153 | // 不是标签结束位置 一直匹配 154 | let attr, end; 155 | while ( 156 | !(end = html.match(startTagClose)) && 157 | (attr = html.match(attribute)) 158 | ) { 159 | // 去除属性 160 | advance(attr[0].length); 161 | match.attrs.push({ 162 | // 属性名 163 | name: attr[1], 164 | // 属性值 key="value" key='value' key=value 165 | // key 对于只有key的这种,我们给默认值true 166 | value: attr[3] || attr[4] || attr[5] || true, 167 | }); 168 | } 169 | // 去除标签的右闭合箭头
中的 > 或者自闭合标签
/> 170 | if (end) { 171 | advance(end[0].length); 172 | // 自闭合 173 | if (end[0].endsWith("/>")) match.isSelfClose = true; 174 | } 175 | // console.log(match); 176 | return match; 177 | } 178 | // 不是开始标签 179 | return false; 180 | } 181 | /** 182 | * 字符串截取 183 | * @param {number} start 截取的起始位置 184 | */ 185 | function advance(start) { 186 | html = html.substring(start); 187 | } 188 | // vue2中 html 开头肯定是 <
hello
189 | while (html) { 190 | // 如果indexOf中索引的值是 0 则说明是个开始标签 或者 结束标签 191 | // > 0 是文本的结束位置
192 | let textEnd = html.indexOf("<"); 193 | if (textEnd === 0) { 194 | // 解析开始标签 开始标签及其标签内的属性等 195 | const startTagMatch = parseStartTag(); // 匹配结果 196 | if (startTagMatch) { 197 | // console.log(startTagMatch); 198 | start( 199 | startTagMatch.tagName, 200 | startTagMatch.attrs, 201 | startTagMatch.isSelfClose 202 | ); 203 | continue; 204 | } 205 | // 去除结束标签 来到这里 肯定是 206 | const endTagMatch = html.match(endTag); 207 | if (endTagMatch) { 208 | advance(endTagMatch[0].length); 209 | // console.log(endTagMatch, html); 210 | end(endTagMatch[1]); 211 | continue; 212 | } 213 | } 214 | // 文本内容 adb

215 | if (textEnd > 0) { 216 | // 获取文本内容 217 | const text = html.substring(0, textEnd); 218 | if (text) { 219 | advance(text.length); // 解析到的文本 220 | chars(text); 221 | } 222 | // console.log(html); 223 | } 224 | } 225 | // console.log(root); 226 | // 返回 生成的vNode树 ast 227 | return root; 228 | } 229 | 230 | export { parseHTML }; 231 | -------------------------------------------------------------------------------- /src/compiler/type.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 毛毛 3 | * @Date: 2022-04-14 13:09:23 4 | * @Last Modified by: 毛毛 5 | * @Last Modified time: 2022-04-14 13:10:07 6 | * 节点类型 7 | */ 8 | // 元素类型 9 | const ELEMENT_TYPE = 1; 10 | // 文本类型 11 | const TEXT_TYPE = 3; 12 | 13 | export{ 14 | ELEMENT_TYPE, 15 | TEXT_TYPE 16 | } -------------------------------------------------------------------------------- /src/global-static-api.js: -------------------------------------------------------------------------------- 1 | import { isFunction } from "./utils"; 2 | import { mergeOptions } from "./utils/merge"; 3 | 4 | /* 5 | * @Author: 毛毛 6 | * @Date: 2022-04-15 20:40:36 7 | * @Last Modified by: 毛毛 8 | * @Last Modified time: 2022-04-18 13:30:21 9 | * 全局静态 api 10 | */ 11 | export function initGlobalStaticAPI(Vue) { 12 | Vue.options = {}; // 全局选项 13 | // 缓存 Vue构造函数 14 | Object.defineProperty(Vue.options, "_base", { 15 | value: Vue, 16 | // 为了可以混入到所有实例的选项中 需要可枚举 17 | enumerable: true, 18 | }); 19 | // 混入 20 | Vue.mixin = function mixin(mixin) { 21 | // 我们期望将用户的选项和全局的options进行合并 22 | // {} + mixin {created(){}} => {created:[fn]} 23 | this.options = mergeOptions(Vue.options, mixin); 24 | // console.log(this.options); 25 | return this; 26 | }; 27 | /** 28 | * 使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。 29 | * 返回值是一个构造函数 通过new可以创建一个vue组件实例 30 | * @param {{data:Function,el:string}} options 31 | * @returns 32 | */ 33 | Vue.extend = function extend(options) { 34 | // 组合式继承 Vue 35 | function Sub(options = {}) { 36 | // 最终使用的组件 就是 new 一个实例 37 | this._init(options); 38 | } 39 | Sub.prototype = Object.create(Vue.prototype); 40 | Object.defineProperty(Sub.prototype, "constructor", { 41 | value: Sub, 42 | writable: true, 43 | configurable: true, 44 | }); 45 | // 保存用户传递的选项 且和全局的配置合并 46 | Sub.options = mergeOptions(Vue.options, options); 47 | return Sub; 48 | }; 49 | // 维护一个 全局组件对象 50 | Vue.options.components = {}; 51 | /** 52 | * 定义或者获取全局组件 没有获取到组件时 返回 undefined 53 | * @param {string} id 54 | * @param {Function | object} definition 55 | */ 56 | Vue.component = function component(id, definition) { 57 | // 获取全局组件 58 | if (!definition) return Vue.options[id]; 59 | // 如果 definition 是一个函数,说明用户自己调用了 Vue.extend 60 | // 不是函数 就用 extend函数包装一下 61 | !isFunction(definition) && (definition = Vue.extend(definition)); 62 | Vue.options.components[id] = definition; 63 | }; 64 | } 65 | -------------------------------------------------------------------------------- /src/hooks/life-hook.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 毛毛 3 | * @Date: 2022-04-15 21:16:58 4 | * @Last Modified by: 毛毛 5 | * @Last Modified time: 2022-04-15 21:20:16 6 | * 执行生命周期的hook 7 | */ 8 | 9 | export function callHook(vm, hook) { 10 | const handles = vm.$options[hook]; 11 | // 生命周期的钩子的this 都是当前实例 12 | handles?.forEach((handle) => handle.call(vm)); 13 | } 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 毛毛 3 | * @Date: 2022-04-12 22:45:40 4 | * @Last Modified by: 毛毛 5 | * @Last Modified time: 2022-04-17 20:15:15 6 | */ 7 | 8 | import { initGlobalStaticAPI } from "./global-static-api"; 9 | import { initMixin } from "./init"; 10 | import { initStateMixin } from "./initState"; 11 | import { initLifeCycle } from "./lifecycle"; 12 | 13 | /** 14 | * Vue构造函数 15 | * @param {*} options 用户选项 16 | */ 17 | function Vue(options) { 18 | // 初始化 19 | this._init(options); 20 | } 21 | 22 | initMixin(Vue); // 扩展_init方法 23 | // vm._update vm._render vm._c vm._v vm._s 24 | initLifeCycle(Vue); // 拓展生命周期 进行组件的挂载和渲染的方法 25 | 26 | // 静态方法 27 | initGlobalStaticAPI(Vue); 28 | 29 | // Vue.$nextTick vm.$watch 30 | initStateMixin(Vue); 31 | 32 | 33 | 34 | export default Vue; 35 | -------------------------------------------------------------------------------- /src/init.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 毛毛 3 | * @Date: 2022-04-12 22:48:39 4 | * @Last Modified by: 毛毛 5 | * @Last Modified time: 2022-04-18 13:16:18 6 | */ 7 | import { initState } from "./initState"; 8 | import { compileToFunction } from "./compiler"; 9 | import { mountComponent } from "./lifecycle"; 10 | import { mergeOptions } from "./utils/merge"; 11 | import { callHook } from "./hooks/life-hook"; 12 | export function initMixin(Vue) { 13 | /** 14 | * 初始化操作 15 | * @param {*} options 16 | */ 17 | Vue.prototype._init = function _init(options) { 18 | // console.log("init------------>", options); 19 | // vue app.$options = options 获取用户配置 20 | const vm = this; 21 | // 合并 Vue.options 和 传入的配置项 22 | // TODO 目前还只是可以合并生命周期和普通属性等,对于 data 这种选项还需要特殊的合并处理 23 | // 这种使用this获取其构造函数上的静态属性options,因为构造函数不一定直接是 Vue,也可以是Vue的子类(组件) 24 | vm.$options = mergeOptions(this.constructor.options, options); // vue认为 $xxx 就是表示vue的属性 25 | // console.log(vm.$options); 26 | // 执行初始化之前,执行 beforeCreate 的钩子 27 | callHook(vm, "beforeCreate"); 28 | // 初始化状态 29 | // TODO computed methods watcher .... 30 | initState(vm); 31 | // 状态初始化完毕之后,执行 created 钩子 32 | callHook(vm, "created"); 33 | // TODO 编译模板 等... 34 | // el vm挂载到的dom容器 35 | if (options.el) vm.$mount(options.el); 36 | }; 37 | Vue.prototype.$mount = function $mount(el) { 38 | const vm = this; 39 | const ops = vm.$options; 40 | el = document.querySelector(el); 41 | let template; 42 | // 是否有render函数 43 | // 没有render 44 | if (!ops.render) { 45 | // 没有template选项 但是写了el 直接用el作为模板 46 | if (!ops.template && el) template = el.outerHTML; 47 | else template = ops.template; // 没有el 一般是组件的挂载 48 | } 49 | // 有template 直接用模板 50 | if (template) { 51 | console.log("------------------", /^[\.#a-zA-Z_]/i.test(template)); 52 | if (/^[\.#a-zA-Z_]/i.test(template)) { 53 | // 模板标签 54 | template = document.querySelector(template).innerHTML; 55 | } 56 | // TODO 去除开头和结尾的空白符 m是忽略换行 进行多行匹配 57 | // template = template.trim(); 58 | template = template.replace(/^\s+|\s+$/gm, ""); 59 | 60 | // 编译模板 生成 render函数 61 | const render = compileToFunction(template); 62 | ops.render = render; 63 | } 64 | // console.log("$mount template-------------->", template); 65 | // 调用 render 实现页面渲染 66 | // console.log(ops.render); 67 | // 组件的挂载 68 | mountComponent(vm, el); 69 | /** 70 | * script 标签引用的是vue.global.js 这个编译过程是在浏览器运行的 71 | * runtime是不包含模板编译的,整个编译打包的时候是通过loader来转义.vue文件的 72 | * 用runtime的时候 不能使用模板template(可以使用.vue,loader处理就行了) 73 | */ 74 | }; 75 | } 76 | -------------------------------------------------------------------------------- /src/initState.js: -------------------------------------------------------------------------------- 1 | import { isFunction } from "./utils"; 2 | import { observe } from "./observe"; 3 | import Watcher, { nextTick } from "./observe/watcher"; 4 | import Dep from "./observe/dep"; 5 | function proxy(vm, target, key) { 6 | Object.defineProperty(vm, key, { 7 | enumerable: true, 8 | get() { 9 | return vm[target][key]; 10 | }, 11 | set(newVal) { 12 | vm[target][key] = newVal; 13 | }, 14 | }); 15 | } 16 | /** 17 | * 初始化实例 18 | * @param {*} vm vue实例 19 | */ 20 | function initState(vm) { 21 | const opts = vm.$options; // 获取所有选项 22 | if (opts.data) { 23 | // data 初始化 24 | initData(vm); 25 | } 26 | // computed 27 | if (opts.computed) { 28 | initComputed(vm); 29 | } 30 | // watch 31 | if (opts.watch) { 32 | initWatch(vm); 33 | } 34 | } 35 | /** 36 | * 初始化watch 37 | * @param {Vue} vm 38 | */ 39 | function initWatch(vm) { 40 | const watch = vm.$options.watch; 41 | for (const key in watch) { 42 | // 字符串 数组 函数 43 | const handler = watch[key]; 44 | if (Array.isArray(handler)) { 45 | for (let i = 0; i < handler.length; i++) { 46 | createWatch(vm, key, handler[i]); 47 | } 48 | } else { 49 | createWatch(vm, key, handler); 50 | } 51 | } 52 | } 53 | /** 54 | * 55 | * @param {*} vm 56 | * @param {string|Function} exprOrFn 侦听的值 57 | * @param {string|Function|object} handler 对象的情况没有考虑 58 | */ 59 | // TODO handler 还可以考虑对象的情况 name:{ handler(){} ...} 60 | function createWatch(vm, exprOrFn, handler) { 61 | if (typeof handler === "string") { 62 | // name: "handler" -> methods["handler"] 63 | handler = vm[handler]; 64 | } 65 | return vm.$watch(exprOrFn, handler); 66 | } 67 | 68 | /** 69 | * 初始化 data 70 | * @param {Vue} vm 实例 71 | */ 72 | function initData(vm) { 73 | // data可能是函数 也可能是对象 74 | let data = vm.$options.data; 75 | // data是函数 执行一下 76 | if (isFunction(data)) data = data.call(vm); 77 | Object.defineProperty(vm, "_data", { 78 | configurable: true, 79 | // enumerable: false, 80 | writable: true, 81 | value: data, 82 | }); 83 | console.log("initData------------>", data); 84 | // 数据劫持 85 | observe(data); 86 | // 把 vm._data 用vm来代理 访问 vm.name -> vm._data.name 87 | for (const key in data) { 88 | proxy(vm, "_data", key); 89 | } 90 | } 91 | /** 92 | * 初始化 computed 93 | * @param {Vue} vm 实例 94 | */ 95 | function initComputed(vm) { 96 | const computed = vm.$options.computed; 97 | const watchers = (vm._computedWatchers = {}); 98 | for (const key in computed) { 99 | const userDef = computed[key]; 100 | // function -> get 101 | // object -> {get(){}, set(newVal){}} 102 | let setter; 103 | const getter = isFunction(userDef) 104 | ? userDef 105 | : ((setter = userDef.set), getter); 106 | // 监控计算属性中 get的变化 107 | // 每次data的属性发生改变 重新执行的就是这个get 108 | // 传入额外的配置项 标明当前的函数 不需要立刻执行 只有在使用到计算属性了 才计算值 109 | // 把属性和watcher对应起来 110 | watchers[key] = new Watcher(vm, getter, { lazy: true }); 111 | // 劫持每一个计算属性 112 | defineComputed(vm, key, setter); 113 | } 114 | } 115 | /** 116 | * 定义计算属性 117 | * @param {*} target 118 | * @param {*} key 119 | * @param {*} setter 120 | */ 121 | function defineComputed(target, key, setter) { 122 | Object.defineProperty(target, key, { 123 | // vm.key -> vm.get key this -> vm 124 | get: createComputedGetter(key), 125 | set: setter, 126 | }); 127 | } 128 | /** 129 | * vue2.x 的计算属性 不会收集依赖,只是让计算属性依赖的属性去收集依赖 130 | * 创建一个懒执行(有缓存的)计算属性 判断值是否发生改变 131 | * 检查是否需要执行这个getter 132 | * @param {string} key 133 | */ 134 | function createComputedGetter(key) { 135 | // this -> vm 因为返回值给了计算属性的 get 我们是从 vm上取计算属性的 136 | return function lazyGetter() { 137 | // 对应属性的watcher 138 | const watcher = this._computedWatchers[key]; 139 | if (watcher.dirty) { 140 | // 如果是脏的 就去执行用户传入的getter函数 watcher.get() 141 | // 但是为了可以拿到get的执行结果 我们调用 evaluate函数 142 | watcher.evaluate(); // dirty = false 143 | } 144 | // 计算属性watcher出栈后 还有渲染watcher(在视图中使用了计算属性) 145 | // 或者说是在其他的watcher中使用了计算属性 146 | if (Dep.target) { 147 | // 让计算属性的watcher依赖的变量也去收集上层的watcher 148 | watcher.depend(); 149 | } 150 | return watcher.value; 151 | }; 152 | } 153 | /** 154 | * 155 | * 实现 $nextTick $watch 156 | * @export 157 | * @param {Vue} Vue 158 | */ 159 | export function initStateMixin(Vue) { 160 | /** 161 | * $nextTick实现 162 | */ 163 | Vue.prototype.$nextTick = nextTick; 164 | /** 165 | * 实现 $watch 166 | */ 167 | // watch的底层实现 全是通过$watch 168 | Object.defineProperty(Vue.prototype, "$watch", { 169 | /** 170 | * watch的实现 也是使用观察者模式 171 | * @param {Function|string} exprOrFn 监控的值 172 | * @param {*} callback 回调函数 173 | * @param {*} options 选项 174 | */ 175 | value(exprOrFn, callback, options = {}) { 176 | // console.log(exprOrFn, callback); 177 | // 创建观察者 user属性 表名这是用户自己定义的watch 178 | // 侦听的属性值发生改变 直接执行callback即可 179 | new Watcher(this, exprOrFn, { user: true, ...options }, callback); 180 | }, 181 | }); 182 | } 183 | export { initState }; 184 | -------------------------------------------------------------------------------- /src/lifecycle.js: -------------------------------------------------------------------------------- 1 | import { createElementVNode, createTextVNode } from "./vdom"; 2 | import { patch } from "./vdom/patch"; 3 | import Watcher from "./observe/watcher"; 4 | /* 5 | * @Author: 毛毛 6 | * @Date: 2022-04-14 14:10:39 7 | * @Last Modified by: 毛毛 8 | * @Last Modified time: 2022-04-18 14:02:59 9 | * 组件挂载 生命周期 10 | * vm._render() 生成虚拟节点 vNode 11 | * vm._update() 虚拟节点变成真实节点 dom 12 | */ 13 | export function mountComponent(vm, container) { 14 | // 记录需要挂载的容器 $el 15 | Object.defineProperty(vm, "$el", { 16 | value: container, 17 | writable: true, 18 | }); 19 | // 这里把渲染逻辑封装到watcher中 20 | const updateComponent = () => { 21 | // 1.调用render 产生虚拟节点 vNode 22 | const vNodes = vm._render(); 23 | // 2. 根据虚拟dom 产生真实dom 24 | vm._update(vNodes); 25 | }; 26 | new Watcher(vm, updateComponent, true); 27 | // 3. 挂载到container上 _update中实现 28 | } 29 | /** 30 | * 扩展原型方法 31 | * @param {*} Vue 32 | */ 33 | export function initLifeCycle(Vue) { 34 | Object.defineProperties(Vue.prototype, { 35 | _render: { 36 | // 当渲染的时候,会去实例中取值,我们就可以将属性和视图绑定在一起 37 | value: function _render() { 38 | const vm = this; 39 | // 绑定 this为组件实例 40 | return vm.$options.render.call(vm); 41 | }, 42 | }, 43 | _update: { 44 | /** 45 | * 将虚拟dom转为真实dom vnode -> dom 46 | * @param {*} vnode 虚拟dom节点 47 | */ 48 | value: function _update(vnode) { 49 | const vm = this; 50 | // 挂载的容器 51 | const el = vm.$el; 52 | // 拿到上次的vnode 53 | const prevVnode = vm._vnode; 54 | // 记录每次产生的 vnode 55 | vm._vnode = vnode; 56 | if (prevVnode) { 57 | // 更新 58 | vm.$el = patch(prevVnode, vnode); 59 | } else { 60 | // 初渲染 61 | // patch 更新 + 初始化 + 组件的创建(el为null) 62 | vm.$el = patch(el, vnode); 63 | } 64 | // console.log("_update----------------->", vnode); 65 | }, 66 | }, 67 | // _c("div",{name:'zs'},...children) 元素 虚拟dom 68 | _c: { 69 | value: function _c() { 70 | // this -> vm 71 | return createElementVNode(this, ...arguments); 72 | }, 73 | }, 74 | // _v(text) 文本虚拟dom 75 | _v: { 76 | value: function _v() { 77 | return createTextVNode(this, ...arguments); 78 | }, 79 | }, 80 | // 就是变量字符串化 81 | _s: { 82 | value: function _s(value) { 83 | // 对于不是对象的字符串,没必要再次转字符串了,不然会多出引号 zs -> \"zs\" 84 | return typeof value === "object" ? JSON.stringify(value) : value; 85 | }, 86 | }, 87 | }); 88 | } 89 | -------------------------------------------------------------------------------- /src/observe/array.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 毛毛 3 | * @Date: 2022-04-13 10:02:33 4 | * @Last Modified by: 毛毛 5 | * @Last Modified time: 2022-04-16 17:40:23 6 | * @Description 重写数组中的变异方法 7 | */ 8 | let oldArrayProto = Array.prototype; 9 | 10 | let newArrayProto = Object.create(oldArrayProto); 11 | /** 12 | * 七个变异方法 会改变数组本身的方法 13 | * @type {Array} 14 | */ 15 | const methods = [ 16 | "push", 17 | "pop", 18 | "unshift", 19 | "shift", 20 | "reverse", 21 | "sort", 22 | "splice", 23 | ]; 24 | methods.forEach((method) => { 25 | // 重写数组的方法 内部调用的还是原来的方法 26 | // 函数的劫持 切片编程 27 | newArrayProto[method] = function (...args) { 28 | // 如果新增的数组元素是对象 需要再次劫持 29 | let inserted; 30 | // Observe实例 31 | const ob = this.__ob__; 32 | switch (method) { 33 | case "push": 34 | case "unshift": // 插入元素 35 | // 新增的元素 可能是对象 36 | inserted = args; 37 | break; 38 | case "splice": // 数组最强方法 splice(start, delCount, ...新增元素) 39 | inserted = args.slice(2); // 新增的元素 40 | break; 41 | default: 42 | break; 43 | } 44 | console.log("新增的内容------------------>", inserted); 45 | if (inserted) { 46 | // 观测新增的内容 47 | ob.observeArray(inserted); 48 | } 49 | console.log(`重写的${method}方法被调用------> this = `, this); 50 | const res = oldArrayProto[method].call(this, ...args); 51 | // 通知更新 dep -> watcher -> 视图更新 52 | ob.dep.notify(); 53 | return res; 54 | }; 55 | }); 56 | export default newArrayProto; 57 | -------------------------------------------------------------------------------- /src/observe/dep.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 毛毛 3 | * @Date: 2022-04-15 09:31:54 4 | * @Last Modified by: 毛毛 5 | * @Last Modified time: 2022-04-16 09:50:12 6 | * 依赖收集 dep 7 | */ 8 | let id = 0; 9 | class Dep { 10 | id = id++; 11 | constructor() { 12 | // 属性的dep要收集watcher 13 | this.subs = []; 14 | } 15 | /** 16 | * 收集当前属性 对应的视图 watcher 17 | */ 18 | depend() { 19 | // 这里我们不希望收集重复的watcher,而且现在还只是单向的关系 dep -> watcher 20 | // watcher 也需要记录 dep 21 | // this.subs.push(Dep.target); 22 | // console.log(this.subs); 23 | // 这里是让watcher先记住dep 24 | Dep.target.addDep(this); // this -> dep 25 | } 26 | /** 27 | * dep 在反过来记录watcher 28 | * @param {*} watcher 29 | */ 30 | addSub(watcher) { 31 | this.subs.push(watcher); 32 | // console.log(watcher); 33 | } 34 | /** 35 | * 更新视图 36 | */ 37 | notify() { 38 | this.subs.forEach((watcher) => watcher.update()); 39 | } 40 | // 当前的watcher 41 | static target = null; 42 | } 43 | 44 | // watcher queue 视图渲染栈 45 | const watcherStack = []; 46 | /** 47 | * watcher入栈 48 | * @param {Watcher} watcher 49 | */ 50 | export function pushWatcherTarget(watcher) { 51 | watcherStack.push(watcher); 52 | Dep.target = watcher; 53 | } 54 | /** 55 | * watcher 出栈 且让 Dep.target 指向上一个入栈的 watcher 56 | */ 57 | export function popWatcherTarget() { 58 | watcherStack.pop(); 59 | Dep.target = watcherStack[watcherStack.length - 1]; 60 | } 61 | export default Dep; 62 | -------------------------------------------------------------------------------- /src/observe/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 毛毛 3 | * @Date: 2022-04-13 08:51:06 4 | * @Last Modified by: 毛毛 5 | * @Last Modified time: 2022-04-16 20:33:36 6 | */ 7 | import { isObject } from "../utils"; 8 | import arrayProto from "./array"; 9 | import Dep from "./dep"; 10 | class Observe { 11 | constructor(data) { 12 | // 让引用数据自身也实现依赖收集 这个dep是放在 data.__ob__ = this 上的 13 | // 也就是说 data.__ob__.dep 并不是 data.dep 所以不会发生重复 14 | this.dep = new Dep(); 15 | // 记录this 也是一个标识 如果对象上有了该属性 标识已经被观测 16 | Object.defineProperty(data, "__ob__", { 17 | value: this, // observe的实例 18 | }); 19 | // 如果劫持的数据是数组 20 | if (Array.isArray(data)) { 21 | // 重写数组上的7个方法 这7个变异方法是可以修改数组本身的 22 | Object.setPrototypeOf(data, arrayProto); 23 | // 对于数组元素是 引用类型的,需要深度观测的 24 | this.observeArray(data); 25 | } else { 26 | // Object.defineProperty 只能劫持已经存在的属性(vue提供单独的api $set $delete 为了增加新的响应式属性) 27 | this.walk(data); 28 | } 29 | } 30 | /** 31 | * 循环对象 对属性依次劫持 重新‘定义’属性 32 | * @param {*} data 33 | */ 34 | walk(data) { 35 | Object.keys(data).forEach((key) => defineReactive(data, key, data[key])); 36 | } 37 | /** 38 | * 劫持数组元素 是普通原始值不会劫持 39 | * @param {Array} data 40 | */ 41 | observeArray(data) { 42 | data.forEach((item) => observe(item)); 43 | } 44 | } 45 | // vue2 应用了defineProperty需要一加载的时候 就进行递归操作,所以好性能,如果层次过深也会浪费性能 46 | // 1.性能优化的原则: 47 | // 1) 不要把所有的数据都放在data中,因为所有的数据都会增加get和set 48 | // 2) 不要写数据的时候 层次过深, 尽量扁平化数据 49 | // 3) 不要频繁获取数据 50 | // 4) 如果数据不需要响应式 可以使用Object.freeze 冻结属性 51 | /** 52 | * vue2 慢的原因 主要在这个方法中 53 | * 定义目标对象上的属性为响应式 54 | * @param {Object} obj 55 | * @param {string|symbol} key 56 | * @param {*} value 57 | */ 58 | export function defineReactive(obj, key, value) { 59 | // 如果属性也是对象 再次劫持 childOb有值的情况下是Observe实例,实例上挂载了dep 60 | const childOb = observe(value); 61 | // 每个属性都有一个dep 62 | let dep = new Dep(); 63 | Object.defineProperty(obj, key, { 64 | get() { 65 | // 判断 Dep.target 66 | if (Dep.target) { 67 | // 让数组自身 和 对象自身 都能实现依赖收集 68 | if (childOb) { 69 | // 来到这里,表名 value是引用类型,且如果是数组,循环看数组元素是否是数组 70 | // 如果还是数组 则需要收集依赖 71 | childOb.dep.depend(); 72 | // TODO 深度实现依赖收集 对于数组元素还是数组的情况,需要让此元素自身也进行依赖收集 73 | if (Array.isArray(value)) dependArray(value); 74 | } 75 | // 当前属性 记住这个watcher 也就是视图依赖的收集 76 | dep.depend(); 77 | } 78 | // console.log("----------------dep.get----------------",key) 79 | return value; 80 | }, 81 | set(newVal) { 82 | if (newVal === value) return; 83 | // 新值是对象 则需要重新观测 84 | observe(newVal); 85 | value = newVal; 86 | // 更新数据 通知视图更新 87 | dep.notify(); 88 | }, 89 | }); 90 | } 91 | 92 | /** 93 | * 数据劫持方法 94 | * @param {*} data 需要劫持的数据 95 | */ 96 | export function observe(data) { 97 | // 不是对象 不需要劫持 98 | if (!isObject(data)) return; 99 | // 如果一个对象被劫持过了,那么不需要再次被劫持了 100 | if (data.__ob__ instanceof Observe) return data.__ob__; 101 | // console.log("observe---------------->", data); 102 | return new Observe(data); 103 | } 104 | 105 | /** 106 | * 给对象属性或者数组元素是数组的,进行依赖收集 107 | * 深层次嵌套会递归,递归太多性能差,不存在的属性监控不到,存在的属性要重写方法 vue3->proxy 108 | * @param {*} arr 109 | */ 110 | function dependArray(arr) { 111 | // console.log(arr); 112 | for (let i = 0; i < arr.length; i++) { 113 | const cur = arr[i]; 114 | // console.log(cur, cur.__ob__); 115 | // 数组元素可能不是数组了 116 | if (Array.isArray(cur)) { 117 | // 收集依赖 118 | cur.__ob__.dep.depend(); 119 | dependArray(cur); 120 | } 121 | } 122 | } 123 | // 1.默认vue在初始化的时候 会对对象每一个属性都进行劫持,增加dep属性, 当取值的时候会做依赖收集 124 | // 2.默认还会对属性值是(对象和数组的本身进行增加dep属性) 进行依赖收集 125 | // 3.如果是属性变化 触发属性对应的dep去更新 126 | // 4.如果是数组更新,触发数组的本身的dep 进行更新 127 | // 5.如果取值的时候是数组还要让数组中的对象类型也进行依赖收集 (递归依赖收集) 128 | // 6.如果数组里面放对象,默认对象里的属性是会进行依赖收集的,因为在取值时 会进行JSON.stringify操作 129 | -------------------------------------------------------------------------------- /src/observe/watcher.js: -------------------------------------------------------------------------------- 1 | import Dep, { popWatcherTarget, pushWatcherTarget } from "./dep"; 2 | 3 | /* 4 | * @Author: 毛毛 5 | * @Date: 2022-04-15 09:09:45 6 | * @Last Modified by: 毛毛 7 | * @Last Modified time: 2022-04-16 15:57:02 8 | * 封装视图的渲染逻辑 watcher 9 | */ 10 | let id = 0; 11 | /** 12 | * watcher 进行实际的视图渲染 13 | * 每个组件都有自己的watcher,可以减少每次更新页面的部分 14 | * 给每个属性都增加一个dep,目的就是收集watcher 15 | * 一个视图(组件)可能有很多属性,多个属性对应一个视图 n个dep对应1个watcher 16 | * 一个属性也可能对应多个视图(组件) 17 | * 所以 dep 和 watcher 是多对多关系 18 | * 19 | * 每个属性都有自己的dep,属性就是被观察者 20 | * watcher就是观察者(属性变化了会通知观察者进行视图更新)-> 观察者模式 21 | */ 22 | class Watcher { 23 | // 目前只有一个watcher实例 因为我只有一个实例 根组件 24 | id = id++; 25 | /** 26 | * 27 | * @param {*} vm 组件实例 28 | * @param {Function|string} exprOrFn 渲染页面的回调函数 或者函数 或者字符串(需要把字符串转为函数) name:()=>{}, -> ()=>name,()=>{} 29 | * @param {boolean|object} options 额外选项 true表示初次渲染 对象是额外的配置 30 | * @param {Function} callback watch等的回调函数 31 | */ 32 | constructor(vm, exprOrFn, options, callback) { 33 | // console.log(this,"--------------------------------------------") 34 | if (typeof options === "boolean") this.renderWatcher = true; 35 | // 记录vm实例 36 | this.vm = vm; 37 | this.options = options; 38 | // exprOrFn是字符串 变成函数 name -> ()=>vm.name 39 | if (typeof exprOrFn === "string") { 40 | this.getter = () => vm[exprOrFn]; 41 | // TODO 有this问题在切换 42 | // this.getter = function () { 43 | // return vm[exprOrFn]; 44 | // }; 45 | } else { 46 | // 调用这个函数 意味着可以发生取值操作 47 | this.getter = exprOrFn; 48 | } 49 | // 标识用户自定义watch 50 | this.user = options?.user; 51 | // 收集 watch等的callback 52 | this.callback = callback; 53 | // 收集 dep watcher -> deps 54 | this.deps = []; // 在组件卸载的时候,清理响应式数据使用 还有实现响应式数据等都需要使用到 55 | this.depsId = new Set(); // dep id 56 | // 是否懒执行 57 | this.lazy = options?.lazy; 58 | // dirty 计算属性使用的 59 | this.dirty = this.lazy; 60 | this.value = this.lazy ? void 0 : this.get(); 61 | } 62 | get() { 63 | /** 64 | * 1.当我们创建渲染watcher的时候 会把当前的渲染watcher放到Dep.target上 65 | * 2.调用_render()取值 走到值的get上 66 | */ 67 | // Dep.target = this; 68 | pushWatcherTarget(this); 69 | // 去 vm上取值 这里的this不是vm了,所以取值需要绑定vm 70 | const val = this.getter.call(this.vm); 71 | // 渲染完毕后清空 72 | // Dep.target = null; 73 | popWatcherTarget(); 74 | return val; // 计算属性执行的返回值 75 | } 76 | evaluate() { 77 | // 获取到用户函数的返回值(getter返回值) 并且标识数据不是脏的 78 | this.value = this.get(); 79 | this.dirty = false; 80 | } 81 | /** 82 | * 一个组件对应多个属性 但是重复的属性 也不需要记录 83 | * 比如在组件视图中 用到了多次的name属性,那么需要记录每次用到name的watcher吗 84 | * @param {*} dep 85 | */ 86 | addDep(dep) { 87 | // dep去重 可以用到 dep.id 88 | const id = dep.id; 89 | if (!this.depsId.has(id)) { 90 | // watcher记录dep 91 | this.deps.push(dep); 92 | this.depsId.add(id); 93 | // dep记录watcher 94 | dep.addSub(this); 95 | } 96 | } 97 | /** 98 | * 更新视图 本质重新执行 render函数 99 | */ 100 | update() { 101 | // 是计算属性 102 | if (this.lazy) { 103 | // 依赖的值变化 就标识计算属性的值是脏值了 104 | return (this.dirty = true); 105 | } 106 | // 同步更新视图 改为异步更新视图 107 | // this.get(); 108 | // 把当前的watcher暂存 109 | queueWatcher(this); 110 | console.log("update watcher................."); 111 | } 112 | /** 113 | * 实际刷新视图的操作 执行render用到的都是实例最新的属性值 114 | */ 115 | run() { 116 | // console.log("run------------------"); 117 | // 可以拿到watch最新的值 118 | const newVal = this.get(); 119 | // watch的回调函数 传入最新的值 和上次还未更新的值 120 | this.user && this.callback.call(this.vm, newVal, this.value); 121 | this.value = newVal; 122 | } 123 | depend() { 124 | // 之前是属性dep记录watcher 125 | // 这里是watcher记录属性dep 126 | let i = this.deps.length; 127 | while (i--) { 128 | // 让计算属性watcher收集上层watcher 129 | // curr dep -> prev watcher -> curr dep -> prev watcher 130 | // dep.depend() -> watcher.addDep(dep) -> dep.addSub(watcher) 131 | this.deps[i].depend(); 132 | } 133 | } 134 | } 135 | // watcher queue 本次需要更新的视图队列 136 | let queue = []; 137 | // watcher 去重 {0:true,1:true} 138 | let has = {}; 139 | // 批处理 也可以说是防抖 140 | let pending = false; 141 | /** 142 | * 不管执行多少次update操作,但是我们最终只执行一轮刷新操作 143 | * @param {*} watcher 144 | */ 145 | function queueWatcher(watcher) { 146 | const id = watcher.id; 147 | // 去重 148 | if (!has[id]) { 149 | queue.push(watcher); 150 | has[id] = true; 151 | console.log(queue); 152 | if (!pending) { 153 | // 刷新队列 多个属性刷新 其实执行的只是第一次 合并刷新了 154 | // setTimeout(flushSchedulerQueue, 0); 155 | // 将刷新队列的执行和用户回调的执行都放到一个微任务中 156 | nextTick(flushSchedulerQueue); 157 | pending = true; 158 | } 159 | } 160 | } 161 | /** 162 | * 刷新调度队列 且清理当前的标识 has pending 等都重置 163 | * 先执行第一批的watcher,如果刷新过程中有新的watcher产生,再次加入队列即可 164 | */ 165 | function flushSchedulerQueue() { 166 | const flushQueue = [...queue]; 167 | queue = []; 168 | has = {}; 169 | pending = false; 170 | // 刷新视图 如果在刷新过程中 还有新的watcher 会重新放到queueWatcher中 171 | flushQueue.forEach((watcher) => watcher.run()); 172 | } 173 | // 任务队列 174 | let callbacks = []; 175 | // 是否等待任务刷新 176 | let waiting = false; 177 | /** 178 | * 刷新异步回调函数队列 179 | */ 180 | function flushCallbacks() { 181 | const cbs = [...callbacks]; 182 | callbacks = []; 183 | waiting = false; 184 | cbs.forEach((cb) => cb()); 185 | } 186 | /** 187 | * 优雅降级 Promise -> MutationObserve -> setImmediate -> setTimeout(需要开线程 开销最大) 188 | */ 189 | let timerFunc = null; 190 | if (Promise) { 191 | timerFunc = () => Promise.resolve().then(flushCallbacks); 192 | } else if (MutationObserver) { 193 | // 创建并返回一个新的 MutationObserver 它会在指定的DOM发生变化时被调用(异步执行callback)。 194 | const observer = new MutationObserver(flushCallbacks); 195 | // TODO 创建文本节点的API 应该封装 为了方便跨平台 196 | const textNode = document.createTextNode(1); 197 | console.log("observer-----------------"); 198 | // 监控文本值的变化 199 | observer.observe(textNode, { 200 | characterData: true, 201 | }); 202 | timerFunc = () => (textNode.textContent = 2); 203 | } else if (setImmediate) { 204 | // IE平台 205 | timerFunc = () => setImmediate(flushCallbacks); 206 | } else { 207 | timerFunc = () => setTimeout(flushCallbacks, 0); 208 | } 209 | /** 210 | * 异步批处理 211 | * 是先执行内部的回调 还是用户的? 用个队列 排序 212 | * @param {Function} cb 回调函数 213 | */ 214 | export function nextTick(cb) { 215 | // 使用队列维护nextTick中的callback方法 216 | callbacks.push(cb); 217 | if (!waiting) { 218 | // setTimeout(flushCallbacks, 0); // 刷新 219 | // 使用vue的原理 优雅降级 220 | timerFunc(); 221 | waiting = true; 222 | } 223 | } 224 | 225 | export default Watcher; 226 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 是否是函数 3 | * @param {*} source 对象 4 | * @returns 5 | */ 6 | export function isFunction(source) { 7 | return typeof source === "function"; 8 | } 9 | 10 | export const isObject = (source) => { 11 | return source != null && typeof source === "object"; 12 | }; 13 | -------------------------------------------------------------------------------- /src/utils/merge.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 毛毛 3 | * @Date: 2022-04-15 20:43:57 4 | * @Last Modified by: 毛毛 5 | * @Last Modified time: 2022-04-17 23:46:24 6 | * 合并对象的方法 7 | */ 8 | import { strategy } from "./strategy"; 9 | /** 10 | * 合并选项 11 | * @param {...any} options 12 | * @returns 13 | */ 14 | export function mergeOptions(...options) { 15 | const opts = {}; 16 | const [source1, source2] = options; 17 | for (const key in source1) { 18 | mergeField(key); 19 | } 20 | for (const key in source2) { 21 | if (!source1.hasOwnProperty(key)) { 22 | mergeField(key); 23 | } 24 | } 25 | function mergeField(key) { 26 | // 策略模式 减少 if / else 27 | if (strategy[key]) { 28 | opts[key] = strategy[key](source1[key], source2[key]); 29 | } 30 | // 优先采用用户的选项 再采用全局已存在的 31 | else opts[key] = source2[key] === void 0 ? source1[key] : source2[key]; 32 | } 33 | if (options.length > 2) { 34 | options.splice(0, 2) 35 | return mergeOptions(opts, ...options); 36 | } 37 | return opts; 38 | } 39 | -------------------------------------------------------------------------------- /src/utils/strategy.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 毛毛 3 | * @Date: 2022-04-15 20:52:34 4 | * @Last Modified by: 毛毛 5 | * @Last Modified time: 2022-04-18 00:50:40 6 | * 导出mixin时用到的一些策略模式 7 | */ 8 | // 策略模式 9 | export const strategy = {}; 10 | // 生命周期 11 | const LIFE_CYCLE = [ 12 | "beforeCreate", 13 | "created", 14 | "beforeMount", 15 | "mounted", 16 | "beforeUpdate", 17 | "update", 18 | ]; 19 | LIFE_CYCLE.forEach((hook) => { 20 | strategy[hook] = function (s1, s2) { 21 | if (s2) { 22 | if (s1) { 23 | // 合并选项 24 | // return s1.concat(s2); 25 | return [...s1, s2]; 26 | } else { 27 | // 全局options没有 用户传递的有 变成数组 28 | return [s2]; 29 | } 30 | } else { 31 | return s1; 32 | } 33 | }; 34 | }); 35 | 36 | // 组件的合并策略 37 | strategy.components = function (parentVal, childVal) { 38 | // TODO 这里这种做法不一定很好 该条件是不是应该有还应该考究 有了该条件 全局的组件定义的位置不同 可能最后的结果不同 39 | // 已经和全局组件对象创建关系了,则不需要再次建立关系 直接返回 40 | // if (Object.getPrototypeOf(parentVal) === Vue.options.components) 41 | // return parentVal; 42 | // 通过父亲 创建一个对象 原型上有父亲的所有属性和方法 43 | const res = Object.create(parentVal); // {}.__proto__ = parentVal 44 | if (childVal) { 45 | for (const key in childVal) { 46 | // 拿到所有的孩子的属性和方法 47 | res[key] = childVal[key]; 48 | } 49 | } 50 | console.log(res); 51 | return res; 52 | }; 53 | -------------------------------------------------------------------------------- /src/vdom/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 毛毛 3 | * @Date: 2022-04-14 14:35:48 4 | * @Last Modified by: 毛毛 5 | * @Last Modified time: 2022-04-18 13:51:26 6 | * 虚拟dom 需要的方法 7 | */ 8 | const ReservedTags = [ 9 | "div", 10 | "h1", 11 | "h2", 12 | "h3", 13 | "h4", 14 | "h5", 15 | "h6", 16 | "span", 17 | "ul", 18 | "ol", 19 | "li", 20 | "a", 21 | "table", 22 | "button", 23 | "input", 24 | ]; 25 | 26 | const isReservedTag = (tag) => { 27 | return ReservedTags.includes(tag); 28 | }; 29 | /** 30 | * 生成虚拟dom 31 | * 虚拟dom是和ast不一样的 -> ast是语法层面的转换,他描述的是语法本身(可以描述 js css html等等) 32 | * 我们的虚拟dom 是描述dom元素,可以增加自定义属性 33 | * @param {*} vm 34 | * @param {*} tag 35 | * @param {*} key 36 | * @param {*} props 37 | * @param {*} children 38 | * @param {*} text 39 | * @returns 40 | */ 41 | function vnode(vm, tag, key, props, children, text, componentOptions) { 42 | return { 43 | vm, 44 | tag, 45 | key, 46 | props, // props -> data 47 | children, 48 | text, 49 | // 组件的选项 包含组件的构造函数 50 | componentOptions, 51 | }; 52 | } 53 | // h函数 创建元素节点 54 | function createElementVNode(vm, tag, data = {}, ...children) { 55 | // if(data == null) data = {} 56 | const key = data?.key; // data可能是 null 57 | key && delete data.key; 58 | if (isReservedTag(tag)) return vnode(vm, tag, key, data, children); 59 | // 组件的虚拟节点 需要包含组件的构造函数等 60 | // 组件的构造函数 如果是局部组件 可能是一个对象 btn: {template:""} 61 | const CtorOrObj = vm.$options.components[tag]; 62 | return createComponentVNode(vm, tag, key, data, children, CtorOrObj); 63 | } 64 | /** 65 | * 创建组件节点 66 | * @param {*} vm 67 | * @param {*} tag 68 | * @param {*} key 69 | * @param {*} data 70 | * @param {*} children 71 | * @param {*} CtorOrObj 72 | * @returns 73 | */ 74 | function createComponentVNode(vm, tag, key, data, children, CtorOrObj) { 75 | if (CtorOrObj != null && typeof CtorOrObj === "object") { 76 | // Vue.extend -> 变成构造函数 77 | CtorOrObj = vm.$options._base.extend(CtorOrObj); 78 | } 79 | // TODO 构造 组件的钩子 组件data是不能为null的 80 | data = data ?? {}; 81 | data.hook = { 82 | // 创建真实节点的时候,如果是组件 则调用此init方法 83 | init(vnode) { 84 | // new Sub -> 保存实例到虚拟节点上 85 | const instance = (vnode.componentInstance = 86 | new vnode.componentOptions.Ctor()); 87 | // instance.$el = 组件渲染的真实节点 88 | instance.$mount(); // 没有传递挂载的dom 最后会去 patch方法 89 | console.log(vnode.componentOptions.Ctor, "----------init"); 90 | }, 91 | }; 92 | return vnode(vm, tag, key, data, children, null, { Ctor: CtorOrObj }); 93 | } 94 | 95 | // _v 函数 创建文本节点 96 | function createTextVNode(vm, text) { 97 | return vnode(vm, undefined, undefined, undefined, undefined, text); 98 | } 99 | /** 100 | * 101 | * @param {*} n1 102 | * @param {*} n2 103 | * @returns {boolean} 是否是同一个vnode 104 | */ 105 | function isSameVNode(n1, n2) { 106 | return n1.tag === n2.tag && n1.key === n2.key; 107 | } 108 | 109 | export { 110 | createElementVNode, 111 | createTextVNode as h, 112 | createTextVNode, 113 | createTextVNode as _c, 114 | isSameVNode, 115 | }; 116 | -------------------------------------------------------------------------------- /src/vdom/patch.js: -------------------------------------------------------------------------------- 1 | /** 2 | * vue的核心流程: 3 | * 1. 创造响应式数据 4 | * 2. 模板编译 生成 ast 5 | * 3. ast 转为render函数 后续每次数据更新 只执行render函数(不需要再次进行ast的转换) 6 | * 4. render函数执行 生成 vNode节点(会使用到响应式数据) 7 | * 5. 根据vNode 生成 真实dom 渲染页面 8 | * 6. 数据更新 重新执行render 9 | */ 10 | 11 | import { isSameVNode } from "."; 12 | 13 | /** 14 | * 更新 | 初渲染时 第一个节点的值是真实元素 15 | * @param {*} oldVNode 旧vnode 16 | * @param {*} vnode 最新的vnode 17 | */ 18 | function patch(oldVNode, vnode) { 19 | // 组件的挂载 vm.$el 对应的就是组件的渲染结果了 20 | if (!oldVNode) return createEle(vnode); 21 | 22 | const isRealElement = oldVNode.nodeType; 23 | // 真实元素 24 | if (isRealElement) { 25 | const elm = oldVNode; 26 | // 获取父节点 1. 元素节点 2. 文档节点 3. 文档碎片节点 27 | const parentElm = elm.parentNode; 28 | // console.log(parentElm) 29 | const newEle = createEle(vnode); 30 | // 插入新dom 移除父节点上的老dom节点 31 | insertBefore(parentElm, newEle, elm.nextSibling); 32 | removeChild(parentElm, elm); 33 | // console.log(newEle) 34 | return newEle; 35 | } 36 | // ------------------- 更新节点 -------------------- 37 | return patchVnode(oldVNode, vnode); // 返回更新后的 dom元素 38 | } 39 | /** 40 | * 直接将新节点替换老节点,很消耗性能 41 | * 所以我们不直接替换,而是在比较两个节点之间的区别之后在替换,这就是diff算法 42 | * diff算是 是一个平级比较的过程,父亲和父亲节点比对 儿子和儿子节点比对 43 | */ 44 | function patchVnode(oldVNode, vnode) { 45 | /** 46 | * 1. 两个节点不是同一个节点,直接删除老的换上新的(不在继续对比属性等) 47 | * 2. 两个节点是同一个节点(tag,key都一致),比较两个节点的属性是否有差异 48 | * 复用老节点,将差异的属性更新 49 | */ 50 | const el = oldVNode.el; 51 | // 不是同一个节点 52 | if (!isSameVNode(oldVNode, vnode)) { 53 | // tag && key 54 | // 直接替换 55 | const newEl = createEle(vnode); 56 | replaceChild(el.parentNode, newEl, el); 57 | return newEl; 58 | } 59 | // 文本的情况 文本我们期望比较一下文本的内容 60 | vnode.el = el; 61 | if (!oldVNode.tag) { 62 | if (oldVNode.text !== vnode.text) { 63 | textContent(el, vnode.text); 64 | } 65 | } 66 | // 是标签 我们需要比对标签的属性 67 | patchProps(el, oldVNode.props, vnode.props); 68 | // 有子节点 69 | /** 70 | * 1.旧节点有子节点 新节点没有 71 | * 2. 都有子节点 72 | * 3. 旧节点没有子节点,新节点有 73 | */ 74 | const oldChildren = oldVNode.children || []; 75 | const newChildren = vnode.children || []; 76 | const oldLen = oldChildren.length, 77 | newLen = newChildren.length; 78 | if (oldLen && newLen) { 79 | // 完整的diff 都有子节点 80 | updateChildren(el, oldChildren, newChildren); 81 | } else if (newLen) { 82 | // 只有新节点有子节点 挂载 83 | mountChildren(el, newChildren); 84 | } else if (oldLen) { 85 | // 只有旧节点有子节点 全部卸载 86 | unmountChildren(el, oldChildren); 87 | } 88 | return el; 89 | } 90 | /** 91 | * 对比更新子节点 92 | * @param {*} el 93 | * @param {*} oldChildren 94 | * @param {*} newChildren 95 | */ 96 | // TODO 对于出现重复的key,有bug,还未修复。。。。 97 | function updateChildren(el, oldChildren, newChildren) { 98 | // 我们为了比较两个儿子的时候,提高比较的性能(速度) 99 | /** 100 | * 1. 我们操作列表 经常会有 push pop shift unshift sort reverse 等方法 针对这些情况可以做一些优化 101 | * 2. vue2中采用双指针的方法 比较两个节点 102 | */ 103 | let oldStartIndex = 0, 104 | oldEndIndex = oldChildren.length - 1, 105 | newStartIndex = 0, 106 | newEndIndex = newChildren.length - 1, 107 | oldStartVnode = oldChildren[oldStartIndex], 108 | oldEndVnode = oldChildren[oldEndIndex], 109 | newStartVnode = newChildren[newStartIndex], 110 | newEndVnode = newChildren[newEndIndex]; 111 | // 乱序比较时 使用的映射表 {key:"节点在数组中的索引"} -> {a:0,b:1,...} 112 | const map = makeIndexByKey(oldChildren); 113 | // 循环比较 只要头指针不超过尾指针 就一直比较 114 | while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) { 115 | // 排除 undefined 的情况 116 | if (!oldStartVnode) oldStartVnode = oldChildren[++oldStartIndex]; 117 | if (!oldEndVnode) oldEndVnode = oldChildren[--oldStartIndex]; 118 | /** 119 | * 1. old head -> new head 120 | * 2. old tail -> new tail 121 | * 3. old head -> new tail 122 | * 4. old tail -> new head 123 | * 5. 前面都不符合的情况下,进行乱序比较 看当前节点是否出现在老节点上 124 | */ 125 | // 进行节点比较 126 | else if (isSameVNode(oldStartVnode, newStartVnode)) { 127 | // 头结点相同 128 | // 从头指针开始比较两个节点 129 | // 相同节点 递归比较子节点 130 | patchVnode(oldStartVnode, newStartVnode); 131 | oldStartVnode = oldChildren[++oldStartIndex]; 132 | newStartVnode = newChildren[++newStartIndex]; 133 | } else if (isSameVNode(oldEndVnode, newEndVnode)) { 134 | // 尾节点相同 135 | // 从尾指针开始比较两个节点 136 | patchVnode(oldEndVnode, newEndVnode); 137 | oldEndVnode = oldChildren[--oldEndIndex]; 138 | newEndVnode = newChildren[--newEndIndex]; 139 | } 140 | // 交叉比对 两次头尾比较 141 | // a b c -> c a b 把尾节点移动到头结点之前 142 | else if (isSameVNode(oldEndVnode, newStartVnode)) { 143 | patchVnode(oldEndVnode, newStartVnode); 144 | console.log(oldEndVnode, newStartVnode); 145 | // 将老节点的尾节点插入到老节点头结点(头结点会变化)的前面去 146 | insertBefore(el, oldEndVnode.el, oldStartVnode.el); 147 | oldEndVnode = oldChildren[--oldEndIndex]; 148 | newStartVnode = newChildren[++newStartIndex]; 149 | } 150 | // a b c d -> d c b a 头结点移动到尾节点后面 151 | else if (isSameVNode(oldStartVnode, newEndVnode)) { 152 | patchVnode(oldStartVnode, newEndVnode); 153 | insertBefore(el, oldStartVnode.el, oldEndVnode.el.nextSibling); 154 | oldStartVnode = oldChildren[++oldStartIndex]; 155 | newEndVnode = newChildren[--newEndIndex]; 156 | } else { 157 | // 乱序比对 a b c -> d e a b f 158 | /** 159 | * 根据老的列表做一个映射关系,用新的去找,找到则移动节点,找不到就新增节点,最后移除多余节点 160 | */ 161 | // 如有值:则是需要移动的节点的索引 162 | let moveIndex = map[newStartVnode.key]; 163 | if (moveIndex !== undefined) { 164 | const moveVnode = oldChildren[moveIndex]; 165 | // 移动节点到头指针所在节点的前面 166 | insertBefore(el, moveVnode, oldStartVnode.el); 167 | // 标识这个节点已经移动过 168 | oldChildren[moveIndex] = undefined; 169 | patchVnode(moveVnode, newStartVnode); 170 | } else { 171 | // 找不到 这是新节点 创建 然后插入进去 完事 172 | insertBefore(el, createEle(newStartVnode), oldStartVnode.el); 173 | } 174 | newStartVnode = newChildren[++newStartIndex]; 175 | } 176 | } 177 | // 新节点的比旧节点多 挂载 178 | if (newStartIndex <= newEndIndex) { 179 | for (let i = newStartIndex; i <= newEndIndex; i++) { 180 | // 这里可能是向后追加 也可能是向前插入 181 | // 判断当前的虚拟dom后面是否还有节点 有节点则是插入到该节点前面 182 | const anchor = newChildren[newEndIndex + 1]?.el; 183 | // 注意:插入方法在 要插入的那个节点不存在的情况下,自动变为追加方法 appendChild 184 | insertBefore(el, createEle(newChildren[i]), anchor); 185 | } 186 | } 187 | // 旧节点比新节点多 卸载 188 | if (oldStartIndex <= oldEndIndex) { 189 | for (let i = oldStartIndex; i <= oldEndIndex; i++) { 190 | // 乱序比对时 可能已经标记为 undefined了 191 | oldChildren[i] && removeChild(el, oldChildren[i].el); 192 | } 193 | } 194 | } 195 | /** 196 | * 生成映射表 197 | * @param {*} children 198 | * @returns 199 | */ 200 | function makeIndexByKey(children) { 201 | const map = {}; 202 | children.forEach((child, index) => (map[child.key] = index)); 203 | return map; 204 | } 205 | 206 | /** 207 | * 卸载dom 208 | * @param {*} el 209 | * @param {*} children 210 | */ 211 | // TODO 不要直接使用innerHTML清空 212 | function unmountChildren(el, children) { 213 | children.forEach((child) => removeChild(el, child.el)); 214 | } 215 | 216 | /** 217 | * 把子节点都变成真实dom 挂载到el上 218 | * @param {*} el 219 | * @param {*} children 220 | */ 221 | function mountChildren(el, children) { 222 | for (let i = 0; i < children.length; i++) { 223 | const child = children[i]; 224 | appendChild(el, createEle(child)); 225 | } 226 | } 227 | 228 | function createEle(vnode) { 229 | const { tag, props, children, text } = vnode; 230 | if (typeof tag === "string") { 231 | // 区分真实节点和组件节点 232 | if (createComponent(vnode)) { 233 | return vnode.componentInstance.$el; 234 | } 235 | // 标签 div h2 236 | // 将虚拟节点和真实节点想管理 根据虚拟节点可以找到真实节点 方便修改属性 237 | vnode.el = createElement(tag); 238 | // 更新属性 239 | patchProps(vnode.el, {}, props); 240 | children.forEach((child) => { 241 | // 如果孩子是组件 会实例化组件 并且插入到父组件内部子节点的最后 242 | appendChild(vnode.el, createEle(child)); 243 | }); 244 | } else if (typeof tag === "object") { 245 | // 组件 246 | } else { 247 | // 创建文本节点 248 | vnode.el = createTextNode(text); 249 | } 250 | return vnode.el; 251 | } 252 | /** 253 | * 更新属性到dom节点上 254 | * @param {*} el 255 | * @param {*} oldProps 老节点上的属性 256 | * @param {*} props 257 | */ 258 | function patchProps(el, oldProps, props) { 259 | // 老的属性中有的属性 新节点没有的 需要删除 260 | const oldStyle = oldProps?.style || {}; // oldProps 可能是null 261 | const newStyle = props?.style || {}; // props可能是null 262 | // 样式移除 263 | for (let key in oldStyle) { 264 | if (!newStyle[key]) { 265 | el.style[key] = ""; 266 | } 267 | } 268 | // 属性移除 269 | for (const key in oldProps) { 270 | if (!props[key]) { 271 | removeAttribute(el, key); 272 | } 273 | } 274 | // 属性存在 则覆盖 275 | for (const key in props) { 276 | if (key === "style") { 277 | Object.keys(props[key]).forEach((k) => (el.style[k] = props["style"][k])); 278 | } else { 279 | setAttribute(el, key, props[key]); 280 | } 281 | } 282 | } 283 | 284 | function createComponent(vnode) { 285 | // init 初始化组件 286 | vnode.props?.hook?.init(vnode); 287 | return vnode.componentInstance; 288 | } 289 | 290 | function createElement(tag, type = "browser") { 291 | switch (type.toLowerCase()) { 292 | case "browser": 293 | return document.createElement(tag); 294 | } 295 | } 296 | 297 | function createTextNode(tag, type = "browser") { 298 | switch (type.toLowerCase()) { 299 | case "browser": 300 | return document.createTextNode(tag); 301 | } 302 | } 303 | 304 | function appendChild(parent, child, type = "browser") { 305 | switch (type.toLowerCase()) { 306 | case "browser": 307 | parent.appendChild(child); 308 | break; 309 | } 310 | } 311 | 312 | function setAttribute(el, key, value, type = "browser") { 313 | switch (type.toLowerCase()) { 314 | case "browser": 315 | el.setAttribute(key, value); 316 | break; 317 | } 318 | } 319 | 320 | function removeChild(parent, child, type = "browser") { 321 | switch (type.toLowerCase()) { 322 | case "browser": 323 | parent.removeChild(child); 324 | break; 325 | } 326 | } 327 | 328 | function insertBefore(parent, child, prevChild, type = "browser") { 329 | switch (type.toLowerCase()) { 330 | case "browser": 331 | // document.insertBefore 332 | parent.insertBefore(child, prevChild); 333 | break; 334 | } 335 | } 336 | 337 | function replaceChild(parent, child, oldChild, type = "browser") { 338 | switch (type.toLowerCase()) { 339 | case "browser": 340 | // document.insertBefore 341 | parent.replaceChild(child, oldChild); 342 | break; 343 | } 344 | } 345 | 346 | function removeAttribute(el, key, type = "browser") { 347 | switch (type.toLowerCase()) { 348 | case "browser": 349 | // document.insertBefore 350 | el.removeAttribute(key); 351 | break; 352 | } 353 | } 354 | /** 355 | * 修改元素的文本内容 356 | * @param {*} element 357 | * @param {*} text 358 | * @param {*} type 359 | */ 360 | function textContent(element, text, type = "browser") { 361 | switch (type.toLowerCase()) { 362 | case "browser": 363 | // document.insertBefore 364 | element.textContent = text; 365 | break; 366 | } 367 | } 368 | 369 | export { 370 | patch, 371 | createEle, 372 | patchProps, 373 | createElement, 374 | createTextNode, 375 | appendChild, 376 | setAttribute, 377 | removeChild, 378 | insertBefore, 379 | }; 380 | --------------------------------------------------------------------------------