# student-vue0409 **Repository Path**: webmx/student-vue0409 ## Basic Information - **Project Name**: student-vue0409 - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2024-04-09 - **Last Updated**: 2024-04-23 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## 从零写 Vue2 - 创造响应式数据的流程 1. 核心原理是调用 Object.defineProperty 对属性进行劫持 2. new Vue 实例 3. 调用原型\_init 方法中的 initState 初始化数据 4. 将数据传入 observe 方法进行属性劫持,对数据进行非空对象校验,将未被劫持过的数据进行劫持 5. new Observer 实例。判断数据类型如果是对象调用 walk 方法重设属性,比较耗费性能,然后调用 defineReactive 对属性进行属性劫持,同时对属性的值进行深度劫持。 6. 如果是数据类型是数组,则在继承原有原型的基础上,需要对 7 个变异方法 pop、push、shift、unsfhit、sort、reverse、splice 进行重写,并对 push、unshift、splice 传入的数据进行属性深度劫持。除此之外需要遍历数组对数据进行劫持 7. 响应式的缺点是只能劫持已存在的属性,Vue 单独写了一些 api 比如 $set/$delete - 解析模版 1. 同 4 - 依赖收集 1. 所谓依赖收集(观察者模式)被观察者指的是数据(dep),观察者(watcher:渲染 watcher,计算 watcher) 2. 多对多的关系,一个 dep 对应多个 watcher,一个 watcher 对应多个 dep 3. 取值时收集依赖,设值时更新视图 - 渲染 watcher 1. 在调用 render 方法创建 virtualDOM 时,创建 Wtacher 实例进行渲染,会调用 get 方法进行渲染,在 Dep.target 挂载 Wtacher 实例,在获取数据时,会在 defineReactive 方法中创建 Dep 实例,调用 dep.depend 方法,去 Dep.trget 上调用 addDep 方法收集 dep,并调用 dep.addsub 让 dep 收集 watcher。在设置时调用 dep.notify 通知对应 watcher 实例调用 update 方法去执行队列,调用队列中的 run 方法执行 get 方法渲染 2. 在执行渲染时,运用了防抖,防止多次渲染,会将多个同一 dep 放入队列,只执行最后一次。 3. 在处理异步操作时,封装了$nextTick 方法,并不是创建异步任务,而是将任务放入到队列中,采用优雅降级的方式,执行异步任务,Promise > MutationObserver > setImmediate > setTimeout - 计算 watcher 1. 在初始化状态时,会通过 initComputed 方法初始化 computed 属性 2. 遍历 computed 属性,创建 watcher 实例判断 lazy 属性是否执行,并挂载到\_computedWtachers 中,处理获取 get 方法,重写属性通过 defineProperty 挂载到 vm 上,在 get 方法中调用计算 watcher 的 evaluate 方法执行 get 方法更改 dirty 属性,将结果返回 watcher.value 3. 需要调用 watcher.depend 方法,将计算 watcher 观察的 dep 也被渲染 watcher 监听,这样数据更新时视图也更新 - 用户 watcher 1. 初始化时,通过 initWtacher 方法初始化 watche 属性 2. 遍历 watche 获取属性的 key 与 handler,通过创建 watcherWatcher 方法调用 vm.$watcher 方法创建 watcher 实例,传入 exprOrFn 获取数据,再执行 get 方法时会给数据 dep 收集用户 watcher,当更改数据时更新 watcher - mixin 方法 1. 调用 mergeOptions 把 Vue.options 与 mixin 进行合并,可以直接调用 Vue.mixin,也在\_init 时会先通过 mergeOptions 将Vue.options合并到vm实例 2. 合并规则先合并父级数据,再合并子级数据,会对生命周期、指令、过滤等内容合并 3. 采用发布订阅模式,将用户写的生命周期钩子维护到一个数组,在后续一次调用 callHook - diff 算法 1. 对比标签与key,再对比内容,属性 1. 双指针 + 乱序,旧虚拟节点的头尾指针,新虚拟节点的头尾指针,从头头、尾尾、尾头、头尾进行对比,如果没有相同节点会进行乱序对比,最后当一方尾指针大于头指针结束比较。以旧虚拟节点为基础进行操作处理。将新虚拟节点处理到旧虚拟节点上 - 组件的渲染流程 ### Vue2 部分核心 - vue 核心流程 1. 创造响应式数据 2. 模版转换成 ast 语法树 3. 将 ast 语法树转换成 render 函数 4. 后续每次数据更新可以只执行 render 函数(无需再次执行 ast 转换的过程) 5. render 函数会产生虚拟节点(使用响应式数据) 6. 根据虚节点创造真实 DOM 7. 观察者模式实现依赖收集 8. 异步更新策略 1. 初始化数据 - 在 Vue 构造函数,原型绑定 init 方法进行初始化数据 - 调用 initState 方法初始化状态 2. 实现对象的响应式原理 - initData() 初始化数据 - observe 方法对数据观测劫持 0. 非对象或已观测数据不再观测 1. new Observe 类对数据进行观测 - 添加**ob**不可枚举标识,避免死循环 - 数据是对象直接调用 walk 方法 - 数据是数组需重写数组变异方法 2. walk 循环对象,对属性依次劫持 - Observe 原型方法 walk,处理对象 3. defineReactive 方法劫持属性 - Object.defineProperty 重新定义数据 - 需要对 value 深度属性进行劫持,递归处理 observe 方法 3. 实现数组的函数劫持 - Vue2 中监测数组的变化并没有采用 defineProperty 因为修改索引的情况不多,还会浪费大量性能,采用重写数组的变异方法来实现 - initData -> observe -> 对传入的数组进行原型链修改,后续调用方法都是重写的方法 -> 对数组中的每个对象也再次进行代理 - 修改数组索引、修改长度是无法进行监控的 (arr[1] = 100 arr.length=200 不会触发视图更新)(arr[0].xxx = 100 会触发视图更新,因为数组中的对象被 observe) - 数组数据需重写数组变异方法 - 获取数组原型 7 个可变异方法,进行重写,在获取新增数据时进行再次劫持、监测 - push\pop\unshift\shift\splice\reverse\sort 4. 解析模版 - 调用$mount 方法,入参 el 是根节点 id - 先判断是否有 render 属性,其次判断是否存在 template 属性,最后获取 el 中的 outerHtml - 调用 compileToFunction 方法,对模版编译处理成 AST 语法树 - 采用正则对模版进行编译,获取开始标签属性、文本内容、结束标签 - 同时利用栈结构,创建一颗语法树,将编译后的标签、文本拼接到根节点上 - 将 AST 语法树转换成 render 方法 - 调用 codegen 方法对 ast 语法树进行拼接成字符串 - 结合 with + new Function 生成函数 // with 不支持严格模式 - 调用 render 函数,会进行取值操作,产生对应的虚拟 DOM 操作 - 调用 ptach 函数,将虚拟 dom 渲染为真实 DOM 5. 为什么要虚拟 DOM 1. 可以针对不同平台使用(web、小程序)可以跨平台,不需要考虑平台问题 2. 不用关心兼容性,可以在上层将对应的渲染方法,传递虚拟 dom 渲染 3. diff 算法针对更新的时候,有了虚拟 DOM 之后,可以通过 diff 算法比较差异进行修改真实 DOM 6. vue 中依赖收集 - 所谓的依赖收集(观察者模式)被观察者指代的是数据(dep),观察者(watcher 中 渲染 watcher、计算属性、用户 watcher) - 一个 watcher 中可能对应多个数据,watcher 中保存 dep(重新渲染时可以让属性重新记录 watcher)计算属性也会用到 - 多对多的关系 一个 dep 对应多个 watcher 一个 watcher 对应多个 dep。默认渲染的时候会进行依赖收集(会触发 get 方法)。数据更新了就找到对应的 watcher 去触发更新 - 取值的时候收集依赖,设值的时候更新视图 - 在调用 render 方法产生虚拟节点时,创建 Watcher 实例进行渲染 - new Watcher(vm, updateComponent, true) - 调用 get() 方法进行渲染,设置 Dep.target - addDep()添加被观察者 - update()重新渲染 - 在 defineReactive()方法创建 Dep 实例 - 使用数据时调用 get 方法,判断 Dep.target,是否调用 dep.depend()进行收集 - 更改数据时调用 set 方法,调用 dep.notify(),让每个 watcher.update()进行更新 - 数组更新,给 Observe 类添加属性 Dep 实例,在 get 方法中,对深层次属性返回值,进行 depend()依赖收集 - 如果有深层次数组,继续进行递归处理依赖收集 - 在重写数组方法中,调用 dep.notiy(),让 watcher 进行更新 7. Vue 生命周期原理 - 就是内部用发布订阅模式,将用户写的钩子维护成一个数组,后续一次调用 callHook,主要是靠 mergeOptions 8. Vue 生命周期方法有哪些?在哪一步发送请求及原因 - beforeCreate 这里没有响应式数据,Vue3 中不需要了没用 - initLifecycle 组件父子关系 $parent $children - initEvents 初始化 $on $off $emit - created✅ 拿到响应式数据,不涉及 dom 渲染,api 可以在服务端渲染中使用 - initInjections inject 方法 - initState 响应式数据处理 - initProvide provide 方法 - beforeMount 没有实际价值,mountComponent 组件挂载之前 - mounted✅ 可以获取$el new Warcher 之后调用 - beforeUpdate 重新渲染 updateComponent 方法在 new Watcher 中 - updated 更新 watcher,执行异步队列时调用 - beforeDestory✅ $destory 手动调用移除会触发,不会删除 DOM,移除所有 watcher - $destroy、 路由切换、v-if 切换组件、:is 动态组件 - destory✅ 销毁后 - activated - deactivated - errorCaptured 捕获错误 - 生命周期是同步的,请求时异步的,一般请求是在 mounted 9. 异步更新策略 - 数据更新,调用 watcher.update()更新,首先会判断是否缓存,没缓存会将 watcher 进行缓存,调用 nextTick 方法执行 10. `nextTick`原理,使用场景 - nextTick 内部采用了异步任务进行包装(多个 nextTick 调用会合并成一次,内部合并回调)最后在异步任务中批处理。 - nextTick 方法没有直接使用 API,而是采用优雅降级方式, - Promise -- MutationObserver -- setImmediate -- setTimeout。Vue3 中使用 Promise - 主要应用场景就是异步更新,用户为了获取最终渲染结果需要在内部任务执行之后执行用户逻辑,需要把逻辑放到 nextTick 中 11. 为什么 data 是一个函数 - Vue.component 方法调用 Vue.extend 创建组件时,采用的工厂模式,返回了继承 Vue 原型的构造函数,构造函数的 data 指向入参 options 的 data,如果是对象则会共用对象地址,如果 data 是函数则每次 new 实例都会生成一个新的对象地址 7. 计算属性原理 - 依赖的值发生变化才会重新执行用户的方法,计算属性中要维护一个 dirty 属性,默认计算属性不会立刻执行 - 计算属性就是一个 defineProperty - 计算属性也是一个 watcher,默认渲染会创造一个渲染 watcher 8. watch 实现原理 - 底层调用的都是$watch 方法 - 遍历 watch 属性,将 key 和对应的函数传入$watch 方法中 - $watch 方法中,创建 Watcher 实例,传入 vm,exprOrFn,回调函数等。对于 exprOrFn 判断是否为函数,不是函数设置 getter 为函数,监听 key,当 key 发生变化时,调用回调函数 9. `computed` 和 `watch` 的区别 - 相同点:底层都会创建一个 watcher(用法区别:computed 定义的属性可以在模版中使用,watch 不能在视图中使用) - computed 默认不会立即执行,只有取值的时候才会执行,内部会维护一个 dirty 属性,来控制依赖的值是否发生变化。默认计算属性需要同步返回结果 - watch 默认用户会提供一个回调函数,数据变化了就调用这个回调,我们可以监控某个数据的变化,数据变化了执行某些操作 10. diff 算法 - diff 算法的特点:平级比较,内部采用了双指针方式进行了优化,优化了常见的操作。采用了递归比较的方式 - 针对一个节点的 diff 算法 - 先拿出根节点进行比较,如果是同一节点则比较属性,如果不是同一节点直接换成最新的节点 - 同一个节点比较属性后,复用老节点 - 比较儿子 - 一方有儿子,一方没儿子(删除、添加) - 双指针对比,头头,尾尾,头尾,尾头 - 旧虚拟 dom 做一个映射表,用新的树去映射表中查找此元素是否存在,存在则移动不存在则插入,最后删除多余的 - 存在多移动问题,Vue3 优化了递增子序列 - O(n)复杂度比较 11. key 的作用和原理 - isSameVnode 中会根据 key 来判断两个元素是否是同一个元素,key 不相同说明不是同一个元素 - (key 在动态列表中不要使用索引 - bug)使用 key 进量保证 key 的唯一性(可以优化 diff 算法) 12. 既然 Vue 通过数据劫持可以精准探测数据变化,为什么还需要虚拟 DOM 进行`diff`检测差异 - 如果给每个属性都去增加 watcher,颗粒度太小不好控制,降低 watcher 的数量(每一个组件都有一个 watcher)可以通过 diff 算法降低渲染过程。通过 diff 算法和响应式原理折中处理 13. `mixin`原理和使用场景 - mixin 核心就是合并属性(内部采用了策略模式进行合并),全局 mixin,局部 mixin,针对不同的属性有不同的合并策略 - 将 mixin 方法的入参与 vm.options 上的数据进行合并到 vm.options,优先使用新方法 - 可以通过 Vue.mixin 来实现逻辑的复用。问题在于数据来源不明确,声明的时候可能会导致命名冲突。 14. Vue 组件化的理解 - 组件化优点 1. 组件复用可以根据数据渲染对应组件 2. 把组件相关的内容放在一起合理规化 3. 更新时可以组件级更新 - 组件特性:属性、事件、 插槽 - Vue 中怎样处理组件 1. Vue.extend,根据用户传入的对象生成一个组件的构造函数 2. 根据组件生成对应虚拟节点 3. 组件初始化,将虚拟节点转化成真实节点 new Sub().$mount() 15. 渲染过程 - vm.$options.components['my'] = {my: template} - 创建 my 对应虚拟节点{tag:'my', data:{hook: {init}}, componentOptions:{Ctor:Vue.extend({my:template})}} - 创建真实节点 createComponent - init - new Cotr.$mount 组件的初始化 - vm.$el 插入到父元素中 16. 组件更新流程(prepatch) - 组件更新后触发组件 prepatch 方法,会复用组件,并且比较组件的属性、事件、插槽 - 父组件给子组件传递的属性是响应式的,在模版中使用会做依赖收集自己的组件 watcher - 组件更新了,会重新给 props 赋值,赋值完成后触发 watcher 重新更新 1. data 数据更新,依赖收集。 2. 属性更新,给组件传入属性,属性变化后触发更新 3. 插槽变化更新 17. 异步组件原理 - 异步组件的写法很多,主要让大的组件异步加载 mrakdown 组件。先渲染一个注释标签,等组件加载完毕,最后重新渲染 forceUpdate(类似懒加载) - 原理:异步组件默认不会调用 Vue.extend 方法,所有 Ctor 上没有 cid 属性,没有 cid 属性就是异步组件。先渲染一个注释标签,但如果有 loading 会先渲染 loading,第一轮就结束了,如果用户调用了 resovle,会将结果赋予 factory.resolved 上面,强制重新渲染时,再次进入到 resolveAsyncComponent 中,会直接拿到 factory.resolved 结果来渲染 18. 函数组件的优势及原理 - 函数式组件就是调用 options.render 拿到返回结果来渲染,创建 vituralDom 时通过 Ctor.options.functional 属性判断是否函数组件,通过 context 获取上下文属性 19. 组件渲染原理 - 组件三大特性:自定义标签、组件里面有自己的属性和事件、组件的插槽 - 声明组件: Vue.extend() - 全局组件: 全局都可以使用 - 局部组件: - Vue.component 作用就是收集全局的定义 id 和对应的 definition - Vue.options.component[组件名 ] = 包装成构造函数(定义) - Vue.extend 返回一个子类,而且会在子类上记录自己的选项 - 为什么 Vue 组件中的 data 不能是一个对象? ```js function extend() { function Sub() { this._init() //子组件初始化 } Sub.options = 选项 return Sub } let Sub = Vue.extend({data: {age:1}}) new Sub() mergeOptions(Sub.options) Sub.options.data() // 如果data是对象,那么就会是共享的数据 new Sub() mergeOptions(Sub.options) // 针对跟实例而言new Vue,组件是通过一个构造函数多次创建实例,如果是同一个对象的话那么数据会被相互影响 // 如果是new Vue()时会产生vm,根组件data可以返回对象或函数,再new一次是新的实例。而Vue.componet组件可以用多次,data返回必须是函数 ``` - 创建子类的构造函数的时候,会将全局的组件和自己身上定义的组件进行合并(组件的合并 会先查找自己在查找全局) - 组件的渲染 开始渲染组件会编译组件的模版变成 render 函数 -> render 方法 - createElementVnode,会根据 tag 类型区分是否是组件,如果是组件会创造组件的虚拟节点(组件增加初始化的钩子,增加 componentOptions 选项(Ctor)稍后创在组件的真实节点,只需要 new Ctor()) - 创建真实节点 20. `Vue.set`方法如何实现 - Vue.set 方法是 vue 中的一个补丁方法(正常我们添加属性不会触发更新、数组无法监控到索引和长度) - vm.$set(data.user, 'age', 100) - 如何实现,给每个对象都增添了一个 dep 属性 - 数组调用 splice()方法 - 对象存在值直接返回 - 不是响应式的添加 - 对象调用 defineReactive(target, key, val), 再调用更新 target.**ob**.dep.notify() 21. `Vue.del`方法如何实现 - 删除属性 - 如何实现 - 数组调用 splice()方法 - 对象不存在直接返回,不更新 - 对象直接删除,不更新 - 响应式删除后,调用 ob.dep.notify() 22. 组件的传递方式及之间区别 - props 父传递给儿子 - 属性的原理就是把解析后的 porps,验证后就会将属性定义在当前实例上,vm.\_props(这个属性都是通过 defineReactive 定义的组件在渲染过程去 vm 上取值\_props 属性会被代理到 vm 上) - 在创建虚拟 DOM 时,通过 Ctor.options.props,去 data 中找对应 props,放到虚拟节点 {componentOptions: {propsData}},在创建节点时,将属性挂载到 vm.$options.propsData 上,声明一个 vm.\_porps,将所有属性定义到 vm.\_props,代理到 vm 上,更新 vm.\_props 就会更新视图 - emit 子组件触发组件更新,在创建虚拟节点的时候将所有事件绑定到 listeners,通过$on方法绑定事件,通过$emit 方法来触发事件(发布订阅模式) - ref 可以获取 dom 元素和组件的实例(虚拟 DOM 没有处理 ref,无法拿到实例、组件), - 创建 DOM 时会将所有的 dom 操作及属性都维护到一个 cbs 属性中,cbs(create update insert destroy...) 依次调用 cbs 中的 create 方法,这里就包含 ref 相关操作,会操作 ref 并且赋值 - inject provide 在父组件中将属性暴露出来 provide 属性,子组件通过 inject 获取对应属性,一层层去向上查找父级,找到后停止,将属性定义在自己身上 - $attrs(所有组件上的属性 不涵盖 props) $listeners(组件上所有的事件) - $children $parent 获取父子组件 - Vue.observal 可以创造一个全局的对象用于通信 23. v-if 和 v-for 哪个优先级高 - v-for 的优先级更高,在编译的时候将 v-for 渲染成\_l 函数 v-if 会变成三元表达式。v-for 和 v-if 不能一起使用 - v-if(控制是否渲染) 在编译的时候变成三元表达式 - v-show(控制的是样式)会变成一个指令 24. v-if、v-model、v-for 的实现原理 - v-for 渲染成\_l 函数 - v-if 会变成三元表达式 - v-model 放在表单元素实现双向绑定 - v-model 放在不同的元素上会编译出不同的结果,针对文本来说会处理文本(会被编译成 value + input + 指令处理)。value 和 input 实现双向绑定阻止中文的触发,指令就是处理中文输入完毕后,手动触发 - v-model 绑定到组件,会编译成一个对象,组件在创建虚拟节点时会利用这个对象 25. Vue 中 slot 如何实现,什么时候使用 26. Vue.use 原理及使用 - 使用 Vuue 插件,都会通过 Vue.use(plugin),用户在使用这个插件的时候,将 Vue 传入,从而解决版本依赖问题,分离插件和 Vue 的强依赖 27. Vue 事件装饰修饰符有哪些,及原理 - 实现主要靠的是模版编译原理。stop prevent self capture passive native - 编译时直接编译到事件内部 -
- 编译时增加标识 !~& -
- 键盘事件 - 都是通过模版编译来实现的 28. Vue 中.sync 修饰符的作用、用法及原理 29. 如何理解自定义指令 - 自定义指令就是用于定义好的钩子,当元素在不同的状态时会调用对应的钩子(所有钩子会被合并到 cbs 对应方法上,到时候依次调用) 30. keep-alive 平时在哪里使用及原理 - keep-alive 的原理是默认缓存加载过的组件对应的实例,内部采用 LRU 算法 - 下次切换组件加载时候,此时会找到对应缓存的节点来进行初始化,但是会采用上次缓存$el 来触发 - 更新和销毁会触发 actived 和 deactived - 抽象组件,不会被记录到$children和$parent 上 - 可配置包含,排除的组件。设置缓存区,缓存组件名称 - 去 componentOptions 中找组件名判断是否缓存,不缓存直接返回虚拟节点,缓存则判断是否缓存过,缓存更新 key 和实例,没缓存获取 vnode 和 key,并添加 keepAlive 标识,返回 vnode - 渲染完成后缓存虚拟节点,采用 LRU 算法,cacheVnode 会缓存实例,下次复用实例上的$el,在渲染完后监听 include、exclude 重新缓存 - vnode 上有 keepAlive 和 componentInstance 是缓存过的,走 pretch - 组件的 insert、destory 方法判断是否 keepAlive 属性,走 actived,deactived 生命周期 31. 组件写 name 选项有哪些好处及作用 - 可以在自己组件中,循环使用自己的组件 - 有了名字可以定位到具体组件 ### VueRouter 及 VueX ### Vue2 源码查到代码 - package.json 找命令文件路径 - config.js 文件中找 export - Runtime 是不能编译 template,compiler 可以编译 template - 打包入口 - 'src/platforms/web/entry-runtime.ts' - 'src/platforms/web/entry-runtime-with-compiler.ts' (区别重写了$mount 方法,将 template 变成 render 方法) - runtime/index (所谓的运行时,会提供一些 dom 操作、api 属性操作、元素操作、提供一些组件和服务) - core/index initGlobalAPI 初始化全局 API - instance/index VUe 原型挂方法