-
2021-11-13 21:33:07
一、vue2源码目录
- compiler:将template编译成 render 函数。对于在线编译,render 在运行时执行,执行时会生成 vnode
- core:核心
- platform:web平台、weex多平台、mpvue小程序端
- server:服务端渲染
- sfc:单文件处理,将.vue文件的template、script、style拆分
- shared:工具、常量
二、compiler 运行时、编译时
- 运行时:new Vue时产生一个对象在内存中维护,包含数据、方法、生命周期等,它们通过 数据与模板的绑定关系 维持vue应用的使用,被维持的绑定关系就是运行时。
- 编译时:是创建对应关系的代码,编译的对象是template,分为离线编译和在线编译:
- 离线编译:开发、打包的过程,将.vue业务代码编译成js render函数的过程。
- 在线编译:上线的代码在运行时编译,new Vue代码在浏览器里跑的过程。
💡 vue和react源码区别
- react做运行时优化,所以react源码复杂
- vue没有太多运行时优化但也很流畅,因为vue做编译时优化
- 所以vue模板必须按它的规则去写,而react就很灵活
compiler编译步骤:
- 如果有render函数,那么已经是编译完成的,返回
- 判断template:
- 如果是string,判断 "#app" 还是 "<div>...</div>",分别进行处理
- 如果是DOM,获取innerHTML
- 如果是离线编译,还要判断编译环境,执行 complieToFunctions 函数
- 然后将template编译成函数。
parser:
compiler 的一个步骤,对模板进行 AST 分析(先分词,再做词法分析)。
- html解析
- 因为匹配是正则匹配,所以字符串越短,匹配效率越高。所以模板应该尽量小。
- 匹配顺序:
- parseEndTag'</'
- doctype
- ie条件注释
- 注释
- parseStartTag '<'
- filter解析
- 分析 v-for 的 key。
- 优化:
- 判断静态节点,纯dom、文本、没有vue指令的是纯静态节点,且子节点均为静态节点。
三、core 核心
- component:模板编译代码
- global-api:文件接口
- instance:实例,处理初始化、状态、数据、生命周期、事件
- observer:数据订阅
- utils:方法
- vdom:虚拟dom,使用虚拟dom的原因是,原生dom有很多无用的属性,占用太多内存
一、observer
observer 是 core 的核心。
- defineReactive
- 核心是创建vue data的监听,通过 Object.defineProperty 方法,为 vue 对象定义 get 和 set 方法。
- get:第一次执行 get 方法将该数据对应的 watcher 绑定到对应的 dep上。此后均为返回值。
- set:如果 newValue === value,则返回;否则设置新值,并更新对应的 dep。
- 区分哪一个 vue data 对应哪一个 dep、watcher:data 本身对应 watcher,dep时在 defineReactive 中作为闭包保存的。
- 对于数组,因为Array.prototype.__proto__ === Object.prototype,所以理论上也是可以监听的。但是对于新创建的数组下标无法监听,所以针对数组新增了 dependArray 方法。
- 同样的,为对象添加新的属性,新增的属性也是无法被监听的。
- 数组重写:
- 原因:数组的操作是很昂贵的。当我给一个数组 unshift(value) 时,所有原来数组下标的值都要向后移动一位,相当于遍历一遍原始数组。在 vue 中,由于使用了 get 和 set 方法,就会触发多次执行,非常浪费资源。
- 具体实现:判断浏览器是否支持 __proto__ 属性(比如Android UC不支持)。
- 如果不支持(选),就找它的原型对象设置到 __proto__ 属性上。
- 然后重写数组方法,包括 push、pop、shift、unshift、splice、sort、reverse等。因为这些方法会造成数组元素索引改变。
- 对push、unshift、splice、reverse等会出现新增元素的方法,将新增元素设置为 observer 对象,然后手动更新 dep。
- 核心是创建vue data的监听,通过 Object.defineProperty 方法,为 vue 对象定义 get 和 set 方法。
-
observer
-
将一个数据修改为可观察数据,主要是为了解决数组和对象新增下标或属性时,新元素不是可观察数据的问题。
-
它会创建一个属性名为 __ob__ 的不可配置、不可枚举的对象,用于表示当前对象已经是响应式对象了。
-
对于嵌套对象,递归调用。
-
- watcher
- watcher 是观察者模式的观察者,观察者都应该有回调函数,watcher 的作用就是,对 render 函数和 vnode之间做连接。而watcher 的回调就是执行render。
- 创建 watcher 对象时,会使 Dep.target = this。然后在 watcher 构造函数中,使 this.value = this[name],这会触发 Object.defineProperty 的 get。在 get 中,将 watcher 添加到 dep中。
- vue1.0 与 vue2.0的区别
- vue1.x:
- 一个指令对应一个 watcher。
- 因为它精确定位,所以不需要 dom diff,所有的更改都位置都记录下来了,但这样的结果是维护成本高性能差。
- vue2.x:
- 一个组件对应一个 watcher。
- 在render函数中,with(this) 的 this就是这个组件的observer对象。由于 render函数 返回了一串长长的 html 字符串,所以需要对它做dom diff 。
- 于此同时,维护的 watcher 少很多。因此,组件的代码写的少点,会对运行时计算的压力降低。
- vue1.x:
- Dep
- dep是观察者模式中的发布者,可以有多个指令订阅它。
- 一般来说,一个数据对应一个 dep ,一个 dep 可以有多个 watcher。
- scheduler
- 调度器,负责批处理功能。
- 将本次更新全部放到一个执行队列中;对于任意数据,按照字段区分,设置等待;将这个执行队列 通过调用 nextTick 延迟执行。所以 vue 数据变化不是立刻执行的。
- nextTick
- 延迟到下一帧执行,核心是 timerFunc 函数, 本质是一个延迟函数,尝试先匹配 微任务 。
- 判断是否支持 promise、mutationObserver、setImmidate,setTimeout 兜底;然后执行回调。
- 小结:
- core是vue代码的核心,而 observer 是 core 的核心。它利用 Object.defineProperty 实现对数据的操作拦截,然后将数据绑定到一个 由 观察者模式 为单元(watcher)组成的数据维护中心(Dep)。
- 对于数组,新增或修改元素可能造成整个数组的元素重新排列,所以对数组进行了重写。所有新增的元素都调用 observer 方法,使其变为一个可观察的对象。
- 对于任何数据,都进行递归操作,使其任意属性变得可以追踪。
二、components
keep-alive,保存的是 vnode 节点,而不是数据。
由于 vnode 节点比描述状态的数据大一些,所以 keep-alive 能够保存的数据大小有限,所以它存在取舍问题,一般舍弃最老的组件。
对于任意组件,无论是否被添加到 keep-alive 缓存列表中,重新访问时,都会把它设置为列表的结尾。
三、use
用于为vue设置插件,它维护一个插件队列,判断是否已存在,如果未存在,执行插件,并且添加到插件队列中。
四、vue运行流程总结
-
编译模板,无论是在线编译,还是离线编译,均会为每一个组件生成一个对应的 render 函数。并且,会根据节点的状态,为它们优化成静态节点 render 函数。
-
defineReactive 函数利用 Object.defineProperty方法, 使 vue data 重写成可控对象。
-
在此方法内,创建一个用于双向数据绑定的、观察者模式中的发布者 dep。
-
在 get 方法中,判断是否为第一次访问数据,第一次访问数据时需要通知 dep 发布。
-
在 set 方法中,判断 newValue === oldValue,需要更新则通知 dep 发布。
-
-
dep 作为 vue data 实现双向数据绑定功能中的发布者。每一个 dep 对应一个数据,维护所有与该数据有关的 watcher。
-
这些 render 函数在某个vue钩子的生命周期时被执行,此时会创建 watcher。
-
watcher 作为 vue data 实现双向数据绑定功能中的观察者,每一个 watcher 对应一个组件。
-
watcher 最主要的工作是维护 数据 与 dom 之间的渲染关系。
-
新创建的 watcher 会调用 get 方法,将触发 Object.defineProperty 的 get 方法,将 watcher 添加到对应的 dep中。
-
watcher 的 get 方法还会尝试调用渲染函数,所以当创建 watcher 后,组件中的数据就已经被渲染了。如果是在线编译,第一次渲染会删除原生 dom ,此后均为 vnode。
-
-
首次渲染完成。。。
-
vue data 在后续更新时不是立刻更新,而是将数据更新的过程添加到一个调度器中,实现批量更新而不是每次数据变化时更新 dom 。由 nextTick 执行回调批量更新。
五、性能优化总结
编译时优化:
- 模板不宜过长:的在对模板进行 compiler 编译时,会使用正则表达式。由于正则表达式匹配的回溯性,过长的模板会造成编译性能问题。
运行时优化:
- 模板不宜过长:compiler 将模板编译成了render函数,在执行 render 函数时,每一个 render函数对应一个 watcher,模板过长会导致 dom diff 计算量大。
- 批处理:更新 vue data 时,不是立刻更新,而是将数据通过调度器组织起来,放在 nextTick 后,在下一个微任务/宏任务时作为回调函数执行。
todo。。。key
六、vue源码原理
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>vue源码分析</title> </head> <body> <div id="app"> <div id='div' v-text="msg"></div> <input id="input" type="text" v-model="msg"> </div> <br> <button id="button">测试批量更新</button> </body> <script src="vue/index.js"></script> <script src="vue/compiler.js"></script> <script src="vue/dep.js"></script> <script src="vue/observer.js"></script> <script src="vue/schedule.js"></script> <script src="vue/watcher.js"></script> <script> const vue = new Vue({ el: 'app', data: { msg: 1, }, }); document.getElementById('button').onclick = function (){ for (let i = 0; i < 100; i++) { vue.msg++; } }; </script> </html>
index.js
// 是一个构造函数,返回vue实例 function Vue({ data, el }){ // 数据 this.data = data; // 双向数据 defineReactive(this, this.data); // 编译 compiler(this, document.getElementById(el)); }
compiler.js
// 编译模板 // ⚠️源码中用到了大量的正则,这里仅仅做案例,ast分析也忽略了 function compiler(vm, el){ const div = el.querySelector('#div'); const input = el.querySelector('#input'); // 新创建dom,是可以绑定很多与vue有关的值,这里模拟的是 createElement const _div = document.createElement('div'); const _input = document.createElement('input'); const parentElement = div.parentElement; parentElement.removeChild(div); parentElement.removeChild(input); // 替换成新创建的dom parentElement.appendChild(_div); parentElement.appendChild(_input); // 'msg' const msg = div.getAttribute('v-text'); // div 的 render,正常的render函数是 width(this){xxx},xxx是ast分析后的结构,并且通过不同的渲染函数渲染 const divRender = function (innerHTML){ _div.innerHTML = innerHTML; }; const inputRender = function (value){ _input.value = value; }; // watcher new Watcher(vm, msg, divRender); const inputWatcher = new Watcher(vm, msg, inputRender); _input.oninput = function (){ // input中输入的值 const value = this.value; // 触发 Object.defineProperty 的 set inputWatcher.vm[inputWatcher.key] = value; }; }
dep.js
let depId = 0; // 观察者模式的发布者 function Dep(){ // 区分是哪个发布者 this.depId = depId++; // 维护关心某个数据的所有watcher,这里用set可以去重 this.watcherList = new Set(); } // 当且仅当 创建任意 watcher 时,会指向正在被创建的 watcher Dep.target = null; // 添加 watcher Dep.prototype.add = function (watcher){ if (this.watcherList.has(watcher)) { return; } watcher.depId = this.depId; this.watcherList.add(watcher); }; // 发布 Dep.prototype.notify = function (){ for (let o of this.watcherList) { // 通知到每一个订阅了该数据的 watcher o.update(this.depId); } };
observer.js
function defineReactive(vm, data){ Reflect.ownKeys(data).forEach(key => { let value = data[key]; // 为每一个key创建新的发布者 const dep = new Dep(); Object.defineProperty(vm, key, { get(){ // 第一次执行get,只有在创建 watcher 时 === true if (!!Dep.target) { // 添加一个 watcher 到 发布者 dep dep.add(Dep.target); } return value; }, set(v){ // 相同则什么都不做 if (v === value) { return; } // 先设置值 value = v; // 再更新 dep.notify(); }, }); }); }
schedule.js
// 调度器是为了批量更新和同步调度,这里只写了nextTick和批量更新 const taskSet = new Map(); // 添加任务,然后调用nextTick function addTask(depId, watcher){ if (!taskSet.has(depId)) { taskSet.set(depId, new Set()); } const set = taskSet.get(depId); set.add(watcher); nextTick(depId); } function nextTick(depId){ // nextTick优先尝试使用微任务,然后才是宏任务,这里做了简化 setTimeout(() => { const set = taskSet.get(depId); for (let watcher of set) { // 执行每一个 watcher 的渲染函数 watcher.render(); } // 执行完后清空任务 set.clear(); }); }
watcher.js
function Watcher(vm, key, render){ // 表示当前正在被创建的 watcher,源码中是一个 watcher 的栈 Dep.target = this; this.vm = vm; this.key = key; // 渲染函数,由compiler生成 this.render = function (){ render(this.value); }; this.value = this.get(); Dep.target = null; // 更新这个 watcher 所在的 dep this.update(this.depId); } // ⚠️源码中,get含义不同,get直接更新了视图 Watcher.prototype.get = function (){ // 会触发 defineReactive 中的 get,从而添加这个 watcher 到 dep return this.vm[this.key]; }; Watcher.prototype.update = function (depId){ // 先得到新的值 this.value = this.get(); // 再添加到批量更新任务列表 addTask(depId, this); };
更多相关内容 -
VUE设计模式详解
2021-09-23 10:58:05设计模式(1):只执行一次的函数 概述 最近最近做项目的时候总会思考一些大的应用设计模式相关的问题,我把自己的思考记录下来,供以后开发时参考,相信对其他人也有用。 只执行一次的函数 我们经常会遇到这种...设计模式(1):只执行一次的函数
概述
最近最近做项目的时候总会思考一些大的应用设计模式相关的问题,我把自己的思考记录下来,供以后开发时参考,相信对其他人也有用。
只执行一次的函数
我们经常会遇到这种情况,就是希望某个函数只执行一次,以后就不执行了。一般情况下,我们会这么写:
<script> export default { data() { return { runOnce: true, }; }, methods: { func() { console.log('hello world', this); }, funcRunOnce() { if (this.runOnce) { this.func(); this.runOnce = false; } }, }, }; </script>
但是这样并不优雅,不仅污染了data,还用2个方法进行实现,实在难看。
用闭包改进
于是我们考虑用闭包,把data里面的runOnce这个变量放到闭包里面去,这样就不会污染data了。代码如下:
<script> export default { methods: { func() { console.log('hello world', this); }, funcRunOnce(params) { let runOnce = true; return () => { if (runOnce) { this.func(); runOnce = false; } }(); }, }, }; </script>
但是这么写显然是错了,因为每次调用funcRunOnce都会构造一次闭包,里面的runOnce这个变量根本不会共享。所以继续改写如下:
// 方法1 <script> export default { created() { this.funcRunOnce = this.runOnce(this.func); }, methods: { func() { console.log('hello world', this); }, runOnce(func) { let runOnce = true; return (params) => { if (runOnce) { func(params); runOnce = false; } }; }, }, }; </script> // 方法2 <script> export default { methods: { func() { console.log('hello world', this); }, runOnce(func) { let runOnce = true; return (params) => { if (runOnce) { func(params); runOnce = false; } }; }, funcRunOnce: this.runOnce(this.func), }, }; </script>
使用utils
可以看到,上面的方法仍然很不优雅,要么用一个created和2个方法实现,要么用三个方法实现。而都用了一个公共的方法runOnce。所以我们考虑把runOnce放到utils.js里面去。
// utils.js export function runOnce(func) { let runOnce = true; return (params) => { if (runOnce) { func(params); runOnce = false; } }; } //example.vue import { runOnce } from '@/utils'; <script> export default { methods: { funcRunOnce: runOnce(() => { console.log('hello world', this); }), }, }; </script>
上面的写法看起来非常简洁,但是实际上是不行的,因为this的指向错了。由于runOnce返回的函数并不是vue实例的方法,所以里面的this指向的是undefined。
注意:即使看起来我们好像在funcRunOnce方法中用箭头函数捕获了外面实例的this,但是实际上它捕获的并不是外面的实例的this,而是runOnce返回的函数里面的this。
捕获this
能用箭头函数的地方我们都用了,但是为什么我们还是捕获不了this呢?如此一来是不是完成不了这个任务了?
并不是,方法还是有的,方法是不用箭头函数捕获this。代码如下:
// utils.js export function runOnce(func) { let runOnce = true; return function(params) { if (runOnce) { func.apply(this, params); runOnce = false; } }; } //example.vue import { runOnce } from '@/utils'; <script> export default { methods: { funcRunOnce: runOnce(function h() { console.log('hello world', this); }), }, }; </script>
通过查看代码可以看出,2个地方的箭头函数都被改写成了function,并且还用到了apply函数来强制施加this。
理由很简单,由于runOnce函数里面没有用箭头函数,所以它返回的函数是属于vue实例的,所以它返回的函数的this,是指向vue实例的;又因为funcRunOnce里面没有用箭头函数,所以我们可以用apply把这个this强制附加到func里面去!
同理我们还可以写出第一次不执行,后续才执行的函数:
// utils.js // 第一次不执行,后续再执行 export function notRunOnce(func) { let once = false; return function(params) { if (once) { func.apply(this, params); } once = true; }; }
学到了什么
- 在vue里面可以用赋值的形式初始化方法,或者在created里面初始化方法。
- 箭头函数虽然能捕获this,但不是万能的;有时候我们需要用function和apply结合来捕获this。
设计模式(2): 响应store中数据的变化
概述
最近最近做项目的时候总会思考一些大的应用设计模式相关的问题,我把自己的思考记录下来,供以后开发时参考,相信对其他人也有用。
store里面响应数据变化
通常情况下,我们会把数据存在store里面,并且,有时我们也需要跟踪store里面的数据变化,并作出响应。例子如下:
export default { computed: { categories: state => state.categories.categories, }, watch: { categories() { this.fetchCardData(); }, }, methods: { fetchCardData() { // 请求卡片数据 }, }, }
如上所示,当store里面的categories改变的时候,我们会自动调用api去请求数据。
不响应store里面的数据变化
上面的例子里面,每次当categories改变的时候,fetchCardData方法都会被调用。有些时候,这并不是我们想要的,我们想要的是,当xxxx的时候,categories会改变,fetchCardData方法会跟着被调用;当xxxx的时候,categories会改变,fetchCardData方法又不会跟着被调用,怎么办呢?
方法是创造一个标记,但是如何优雅的创造标记呢?我有一个方法如下所示:
// store.js const state = { categories: [], categoriesChanges: 0, }; const actions = { updateCategories({ commit }, value) { // 如果带有shouldNotChange,则表示不要刷新页面 if (value.shouldNotChange) { commit(types.UPDATE_CATEGORIES, value.data); } else { commit(types.UPDATE_CATEGORIES, value); commit(types.UPDATE_CATEGORIES_CHANGES); } }, }; const mutations = { [types.UPDATE_CATEGORIES](state, value) { state.categories = value; }, [types.UPDATE_CATEGORIES_CHANGES](state) { state.categoriesChanges += 1; }, }; // component.js export default { computed: { categories: state => state.categories.categories, categoriesChanges: state => state.categories.categoriesChanges, }, watch: { categoriesChanges() { this.fetchCardData(); }, }, methods: { fetchCardData() { // 利用this.categories的部分数据来请求卡片数据 }, }, } // business.js this.$store.dispatch('updateCategories', value); // 会自动调用fetchCardData方法 const payload = { shouldNotChange: true, data: [...value], }; this.$store.dispatch('updateCategories', payload); // 不会自动调用fetchCardData方法
这样,我们发出同一个action,却能达到2种不同的效果,非常方便。
设计模式(3): 二次封装与高阶组件
概述
最近最近做项目的时候总会思考一些大的应用设计模式相关的问题,我把自己的思考记录下来,供以后开发时参考,相信对其他人也有用。
二次封装组件
PM的需求无奇不有,所以很多时候,我们使用的组件满足不了PM的需求,怎么办呢?比如,组件需要传入一个数组,但是我们必须传2个变量;或者我们需要在组件focus的时候调用一个方法,但是组件并没有暴露focus事件等等。虽然都是些很简单的需求,但是组件就是没有暴露实现这些需求的方法。咋办?
方法是对组件进行二次封装。
二次封装主要运用了vue的如下属性:
-
vm.attrs:包含了父作用域中不作为prop被识别(且获取)的特性绑定(class和style除外)。当一个组件没有声明任何prop时,这里会包含所有父作用域的绑定(class和style除外),并且可以通过v−bind="attrs:包含了父作用域中不作为prop被识别(且获取)的特性绑定(class和style除外)。当一个组件没有声明任何prop时,这里会包含所有父作用域的绑定(class和style除外),并且可以通过v−bind="attrs" 传入内部组件。
-
vm.$props: 当前组件接收到的 props 对象。
-
vm.listeners:包含了父作用域中的(不含.native修饰器的)v−on事件监听器。它可以通过v−on="listeners:包含了父作用域中的(不含.native修饰器的)v−on事件监听器。它可以通过v−on="listeners" 传入内部组件。
props可以拿到传给当前组件的所有props,props可以拿到传给当前组件的所有props,attrs可以拿到传给组件的所有非props的属性,$listeners可以拿到所有传给组件的事件监听器。
例子
举个例子,比如说el-upload需要传一个数组,但是我们只能传2个变量;并且,我们需要在el-upload上传success的时候做点其它的事。封装的代码如下:
export default { name: 'YmUpload', props: { name: { type: String, default: '', }, url: { type: String, default: '', }, onSuccess: { type: Function, default: () => 1, }, }, data() { return { fileList: [], }; }, watch: { url() { this.init(); }, }, computed: { uploadAttr() { return { ...this.$attrs, fileList: this.fileList, onSuccess: this.handleSuccess, }; }, }, created() { this.init(); }, methods: { init() { // 组件初始化 const payload = { name: this.name || this.url, url: this.url, }; this.fileList = [payload]; }, handleSuccess(res, file, fileList) { // 做点其它的事 }, }, };
设计模式(4): 给组件实现单独的store
概述
最近最近做项目的时候总会思考一些大的应用设计模式相关的问题,我把自己的思考记录下来,供以后开发时参考,相信对其他人也有用。
组件自身的store
我们在开发组件的时候,时常都有这种需求,就是希望给组件一个独立的store,这个store可能被用来储存数据,共享数据,还可以被用来对数据做一些处理,抽离核心代码等。
store的数据不共享
如果组件自身的store是每个实例独自拥有的并且不共享的话,我们可以直接用一个类来实现。
// store.js export default class Store { constructor(data, config) { this.config = config; this.init(data); } init(data) { // 对数据做处理 } // 其它方法 }
然后我们在组件中实例化这个store,然后挂载到data属性里面去:
<script> import Store from './store'; export default { data() { return { store: [], }; }, methods: { initStore() { // 生成 options 和 config this.store = new Store(options, config); }, }, }; </script>
store的数据需要共享
如果store的数据需要共享,我们建议用动态挂载vuex的store的方法,示例如下:
// store.js const state = { data: [], }; const getters = {}; const actions = {}; const mutations = { setData(state, value) { this.state.data = [...value]; }, }; export default { state, getters, actions, mutations, };
然后我们在注册这个组件的时候动态挂载这个store:
import Store from './store'; export default { install(Vue, options) { Vue.store.registerModule('xxx', store); }, };
最后我们就可以在组件中使用这个store的数据啦~~~
设计模式(5): vue 不监听绑定的变量
概述
最近最近做项目的时候总会思考一些大的应用设计模式相关的问题,我把自己的思考记录下来,供以后开发时参考,相信对其他人也有用。
绑定变量
一般情况下,如果我们需要在组件中使用某个变量,会这么使用:
data() { return { myData: [], }; }
如果这个变量是外部变量,例如从外部文件引入的话,就会这么使用:
import { provinces } from '@/util/consts'; export default { data() { return { myData: provices, }; }, }
问题
但是如果这个变量是一个嵌套层级很深,数据量很大的对象的话,如果按照上面那样使用,vue 就会去遍历这个变量的所有属性,来监听这个变量的变化。非常的消耗性能,一个典型的例子是:
export default { data() { return { bannerBg: null, }; }, mounted() { this.loadScript('/js/three.min.js', () => { this.loadScript('/js/vanta.net.min.js', () => { this.bannerBg = window.VANTA.NET({ el: '#bannerBg', color: 0x2197F3, backgroundColor: 0x071E31, }); }); }); }, beforeDestroy() { this.bannerBg.destroy(); }, methods: { loadScript(path, callback) { const script = document.createElement('script'); script.src = path; script.language = 'JavaScript'; script.onload = () => callback(); document.body.appendChild(script); }, }, }
上面的例子中,我们为了避免内存泄漏,在 beforeDestroy 生命周期里面进行回收,而为了获取回收的变量,我们把它绑定给了 this.bannerBg。
但是事实是,我们并不需要监听 this.bannerBg 这个变量,而这么绑定的结果是,这个 vue 组件在 mounted 的时候需要遍历 this.bannerBg 来增加 vue 的监听属性,非常消耗性能。
解决方案
所以,我们建议不把 bannerBg 放到 data() 里面去监听,而是**直接绑定给 this **就行了。优化后的代码如下:
export default { mounted() { this.loadScript('/js/three.min.js', () => { this.loadScript('/js/vanta.net.min.js', () => { this.bannerBg = window.VANTA.NET({ el: '#bannerBg', color: 0x2197F3, backgroundColor: 0x071E31, }); }); }); }, beforeDestroy() { this.bannerBg.destroy(); }, methods: { loadScript(path, callback) { const script = document.createElement('script'); script.src = path; script.language = 'JavaScript'; script.onload = () => callback(); document.body.appendChild(script); }, }, }
如果这个变量不是过程中生成的,而是初始化的时候生成的,我们建议在 data() 方法里面这么做:
import { provinces } from '@/util/consts'; export default { data() { this.myData = provices; return { // 移到上面去了 // myData: provices, }; }, }
设计模式(6): 数据抽象与业务封装
概述
最近最近做项目的时候总会思考一些大的应用设计模式相关的问题,我把自己的思考记录下来,供以后开发时参考,相信对其他人也有用。
情景描述
我们在做项目的时候,经常会碰到各种各样的业务情景,然后为了实现这些需求,就不断地在 vue 单文件组件里面加代码来实现,最终业务越来越多,单文件组件越来越大,非常难以维护。
解决方案
我们都知道,vue 是通过数据来处理视图的,所以很多业务可以抽象成只处理数据,然后这些业务可以再抽象成 class 来进行业务封装。
event-bus
举个例子来说,vuex 或者 redux 这些状态管理的库,就是用的这个思想,把数据层脱离出去,带来的好处是简化了组件之间的数据流动。它们的源码有些复杂,我们以 event-bus 来举例说明。
首先,我们可以自己实现一个 bus 类,这个类能够储存数据,还能够进行事件的分发与监听。
import Vue from 'vue'; import Bus from 'xxxx'; Vue.prototype.$bus = new Bus();
然后,分别在组件 A 和 B 里面,我们可以监听事件和分发事件。
// 组件A -- 监听事件 created() { this.$bus.on('xxxx', this.xxx); }, beforeDestroy() { this.$bus.off('xxxx', this.xxx); }, // 组件B -- 分发事件 methods: { xxxx() { this.$bus.emit('xxxx', this.xxx); } }
这样,即使处于不同层级,组件 A 和 B 也能流畅的进行数据交互。
抽象方法
我们抽象一下实现方法,我们先把业务抽象为数据和对数据的操作,然后在组件之外实现一个 class,最后用这个 class 进行保存数据和业务处理。
上面这个例子把这个 class 放在了 Vue 实例上面,可能没有那么明显,下面举一个把它放在单文件组件里面的例子。
cascader
这一段参考了 element-cascader 的实现。
比如说,我们要自己实现一个 cascader,要怎么做?
我们上面提到过,我们对 cascader 的操作其实就是对数据的操作,所以我们可以把整个数据抽象出来,然后给它加上选中的业务功能:
import { capitalize } from '@/utils/util'; export default class Node { constructor(data, parentNode) { this.parent = parentNode || null; this.initState(data); this.initChildren(data); } initState(data) { // 加上本身的属性 for (let key in data) { if (key !== 'children') { this[key] = data[key]; } } // 自定义属性 this.isChecked = false; this.indeterminate = false; // 用于自动取消 this.isCheckedCached = false; this.indeterminateCached = false; } initChildren(data) { this.children = (data.children || []).map(child => new Node(child, this)); } setCheckState(isChecked) { const totalNum = this.children.length; const checkedNum = this.children.reduce((c, p) => { const num = p.isChecked ? 1 : (p.indeterminate ? 0.5 : 0); return c + num; }, 0); this.isChecked = isChecked; this.indeterminate = checkedNum !== totalNum && checkedNum > 0; } doCheck(isChecked) { this.broadcast('check', isChecked); this.setCheckState(isChecked); this.emit('check', isChecked); } broadcast(event, ...args) { const handlerName = `onParent${capitalize(event)}`; this.children.forEach(child => { if (child) { child.broadcast(event, ...args); child[handlerName] && child[handlerName](...args); } }); } emit(event, ...args) { const { parent } = this; const handlerName = `onChild${capitalize(event)}`; if (parent) { parent[handlerName] && parent[handlerName](...args); parent.emit(event, ...args); } } onParentCheck(isChecked) { if (!this.disabled) { this.setCheckState(isChecked); } } onChildCheck() { const validChildren = this.children.filter(child => !child.disabled); const isChecked = validChildren.length ? validChildren.every(child => child.isChecked) : false; this.setCheckState(isChecked); } }
上面实现的 class 封装了如下业务:
- 通过 initState 加入了各种自定义的状态,这个状态有了业务:选中状态,半选中状态和未选中状态。
- 通过 setCheckState 实现了 点击 的业务。
- 通过 broadcast 和 emit 实现了 父子组件联动 的业务。
当然,实际情形可能比这个更加复杂,我们只需要在上面的代码中加入各种状态和处理方法即可。
更进一步
上面封装的底层的业务,再高一层,我们可能有 搜索、自动选中 等业务,这个时候要怎么办呢?
方法是在 Node 类和单文件组件之间再封装一层,来实现这些业务,示例代码如下:
export default class Store { constructor(data) { this.nodes = data.map(nodeData => new Node(nodeData)); } // 自动选中 autoSelect(query, label) { } // 搜索 search(searchString) { } }
然后我们可以在单文件组件里面直接使用它:
data() { return { store: null; }; }, watch: { data(newVal) { this.store = new Store(newVal); } },
推荐阅读:
-
vue router 源码概览案例分析
2021-01-19 17:40:07源码这个东西对于实际的工作其实没有立竿见影的效果,不会像那些针对性极强的文章一样看了之后就立马可以运用到实际项目中,产生什么样的效果,源码的作用是一个潜移默化的过程,它的理念、设计模式、代码结构等看了... -
vue设计模式之策略模式 filters
2022-02-15 15:56:35vue 策略模式的应用; vue filters的使用策略模式(Strategy )是属于设计模式中 对象行为型模式, 主要是定义一系列的算法 , 把这些算法一个个封装成单独的类 .实际应用:【替换if-else,switch】普通if-else
var performanceS = function( salary ){ //... return salary * 4; }; var performanceA = function( salary ){ //... return salary * 3; }; var performanceB = function( salary ){ //... return salary * 2; }; var calculateBonus = function( performanceLevel, salary ){ if ( performanceLevel === 'S' ){ return performanceS( salary ); } if ( performanceLevel === 'A' ){ return performanceA( salary ); } if ( performanceLevel === 'B' ){ return performanceB( salary ); } }; calculateBonus( 'A' , 10000 ); // 输出:30000
js策略模式写法
var strategies = { "S": function( salary ){ return salary * 4; }, "A": function( salary ){ return salary * 3; }, "B": function( salary ){ return salary * 2; } }; var calculateBonus = function( level, salary ){ return strategies[ level ]( salary ); }; console.log( calculateBonus( 'A', 10000 ) ); // 输出:30000
vue写法:
# template部分 <template> <div>{{ salary | calculateBonus }}<div> <!-- 一个参数 --> <div>{{ level | calculateBonus(salary) }}<div> <!-- 2个参数 --> </template> //官网:https://cn.vuejs.org/v2/guide/filters.html <!-- 1.filters 用在双花括号插值和 v-bind 表达式: <div>{{ salary | calculateBonus }}<div> <div v-bind:id="rawId | formatId"></div> --> <!-- 2. filters 管道符‘|’ 前面的为第一个参数,多个参数的使用函数方式传入--> <!-- 3. 多个filters依次执行 message | filterA | filterB # message 传入filterA; # filterA 结果 传入filterB -->
export default { name: "App", data(){ return { level:'A', salary: 10000 } }, filters:{ calculateBonus(level, salary) { const strategies = { "S": function( salary ){ return salary * 4; }, "A": function( salary ){ return salary * 3; }, "B": function( salary ){ return salary * 2; } }; return strategies[ level ]( salary ); }, }, methods:{} }
参考:JS设计模式 之 策略模式 - 雨中愚 - 博客园
https://www.cnblogs.com/yuzhongyu/p/14203862.html
-
通过源码分析Vue的双向数据绑定详解
2020-12-12 23:24:41Vue源码的整体架构无非是初始化Vue对象,挂载数据data/props等,在不同的时期触发不同的事件钩子,如created() / mounted() / update()等,后面专门整理各个模块的文章。这里先讲双向数据绑定的部分,也是最主要的... -
Vue.js源码解析:Vue.js 源码目录设计
2022-06-17 16:53:28Vue.js 的源码都在 src ⽬录下,其⽬录结构如下。 如图所示: compiler ⽬录包含 Vue.js 所有编译相关的代码。它包括把模板解析成 ast 语法树,ast 语法树优化,代码⽣成等功能。core ⽬录包含了 Vue.js 的核⼼代码...Vue.js源码版本:2.6
Vue.js源码地址:https://github.com/vuejs/vue/tree/2.6Vue.js 的源码都在 src ⽬录下,其⽬录结构如下。
src ├── compiler # 编译相关 ├── core # 核⼼代码 ├── platforms # 不同平台的⽀持 ├── server # 服务端渲染 ├── sfc # .vue ⽂件解析 ├── shared # 共享代码
如图所示:
compiler
compiler ⽬录包含 Vue.js 所有编译相关的代码。它包括把模板解析成 ast 语法树,ast 语法树优化,代码⽣成等功能。
core
core ⽬录包含了 Vue.js 的核⼼代码,包括内置组件、全局 API 封装,Vue 实例化、观察者、虚拟
DOM、⼯具函数等等。这⾥的代码可谓是 Vue.js 的灵魂,也是我们之后需要重点分析的地⽅。
platform
Vue.js 是⼀个跨平台的 MVVM 框架,它可以跑在 web 上,也可以配合 weex 跑在 natvie 客户端上。
platform 是 Vue.js 的⼊⼝,2 个⽬录代表 2 个主要⼊⼝,分别打包成运⾏在 web 上和 weex 上的Vue.js。server
Vue.js 2.0 ⽀持了服务端渲染,所有服务端渲染相关的逻辑都在这个⽬录下。注意:这部分代码是跑在
服务端的 Node.js,不要和跑在浏览器端的 Vue.js 混为⼀谈。服务端渲染主要的⼯作是把组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将
静态标记"混合"为客户端上完全交互的应⽤程序。sfc
通常我们开发 Vue.js 都会借助 webpack 构建, 然后通过 .vue 单⽂件的编写组件。
这个⽬录下的代码逻辑会把 .vue ⽂件内容解析成⼀个 JavaScript 的对象。shared
Vue.js 会定义⼀些⼯具⽅法,这⾥定义的⼯具⽅法都是会被浏览器端的 Vue.js 和服务端的 Vue.js 所共享
的。 -
浅析从vue源码看观察者模式
2020-08-28 03:09:23本篇文章主要介绍了vue源码看观察者模式,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧 -
Vue 技术栈 带你探究 vue-router 源码 手写vue-router
2021-01-08 15:20:301、Vue源码分析 2、手把手教 保姆级 撸代码 3、无惧面试,学以致用,继承创新 4、谈谈前端发展与学习心得 5、手写源码技术栈,附上详细注释 6、从源码中学习设计模式,一举两得 7、编程思想的提升及代码质量的提高 8... -
比较说明 vue 设计模式 MVC MVVM MVP 三种设计模式 的理解
2021-07-14 11:36:30MVC是Model-View-...MVC模式的特点在于实现关注点分离,即应用程序中的数据模型与业务和展示逻辑解耦。在客户端web开发中,就是将模型(M-数据、操作数据)、视图(V-显示数据的HTML元素)之间实现代码分离,松散耦 -
vue设计模式
2020-08-06 15:16:38vue是一个渐进式的框架 那什么是渐进式??? 渐进式,简单翻译一下就是主键做加法的模式 -
vue-source-code:精简vue源码,核心技术实现
2021-03-23 23:52:17手写Vue源码,实现核心技术,麻雀虽小五脏俱全:”响应式原理,模板编译,依赖收集”算法:Diff算法实现”设计模式:发布-订阅模式,装饰者模式,代理模式”数据结构:AST树,vnode,vDom 文件名,方法名称,变量得... -
vue 源码目录设计简述
2022-04-25 18:08:57学习vue.js的源码,首先要学习源码目录,vue.js的源码都在src目录下,目录结构如下: src ├── compiler # 编译相关 ├── core # 核心代码 ├── platforms # 不同平台的支持 ├── server # 服务端渲染 ├... -
Vue源码篇
2022-06-27 01:33:04Vue源码篇 -
深入理解vue核心设计模式
2020-07-15 01:08:34这个设计模式我也看过不少文章,被称为订阅-发布设计模式确实更合理,在vue2的架构中,我们把核心的设计模式分为Observer,Dep,Watcher,Observer被称为观察者,观察着我们的数据,Dep为数据分配中心,收集数据和... -
初识vue的使用和设计模式
2021-12-20 21:46:00目录 vue的基本介绍 什么是vue? 为什么使用vue?...mvc设计模式 MVVM设计模式 vue的基本介绍 什么是vue? 1.渐进式javascript框架 2.作者:尤雨溪 个人开发 3.Vue (读音 /vjuː/,类似于 .. -
Vue源码常见问题
2020-06-20 17:47:231,new Vue()都做了什么 1,new Vue ( )是创建 Vue 实例,它内部执行了根实例的初始化过程。 2, 初始化过程具体包括以下操作: 合并配置 初始化事件中心 初始化生命周期 (new Vue()时只调用了beforeCreated、... -
设计模式在vue中的应用(六)
2019-02-24 15:59:15设计模式在vue中的应用(一) 设计模式在vue中的应用(二) 设计模式在vue中的应用(三) 设计模式在vue中的应用(四) 设计模式在vue中的应用(五) 设计模式在vue中的应用(六) 为什么要写这些文章呢。正如设计... -
Self-Vue:尝试自己实现vue源码(自我实现的vue源代码)
2021-03-23 20:55:44项目描述:自己实现的vue源码(自我实现的vue源代码)目前v-model命令,响应式对象,副本,双向绑定已经完成 基本原理:非数组使用object.defineProperty设置获取和设置监听,在数组原型对象中extend7种变量方法来... -
大前端-Vue源码分析
2020-07-12 22:56:53Vue源码解析-响应式原理 笔记将会对以下三点进行总结 Vue.js 的静态成员和实例成员初始过程 首次渲染的过程 数据响应式原理 一. 准备工作 Vue源码的获取 项目地址 Vue源码获取 Fork 一份到自己的仓库,克隆到本地,... -
java语言课程设计-设计模式大作业用Springboot和Vue实现的考勤系统源代码.zip
2022-06-09 13:14:31java语言课程设计-设计模式大作业用Springboot和Vue实现的考勤系统源代码。系统访问账号 账号 密码 角色 1111 123 员工 5555 123 财务 4444 123 人事 0000 123 总经理 目录 前端 frontpage为前端项目,使用... -
vue 源码详解(一):原型对象和全局 `API`的设计
2021-07-29 17:32:32vue 源码详解(一): 生成 Vue 实例前的准备工作 1. 从 new Vue() 开始 vue/src/core/index.js : import Vue from './instance/index' // 1. 引入 Vue 构造函数 import { initGlobalAPI } from './global-api/index... -
基于SSM+Vue的“依伴汉服”商城设计与开发(ssm源码+vue源码+毕业论文+PPT+数据库)
2022-06-10 00:23:58基于 Java EE 的 web 开发平台,采用Spring+SpringMvc+Mybatis+Vue+阿里云OSS+ElementUi前后端分离的架构模式,设计并实现国民品牌独立商城——《“依伴汉服”商城》 -
java Vue动漫周边商城网站系统ssm毕业设计源码论文答辩ppt.zip
2022-05-29 15:18:27这是一款javaWeb的前后端分离的Springboot和vue源码,包含论文和答辩ppt,前端vue.js,基于B/S模式,idea或者eclipse为开发工具,功能也比较全面,毕业设计使用,感兴趣的朋友可以下载看看哦 管理员:首页、个人中心、... -
源码分析Vue.js的监听实现教程
2020-12-29 18:48:57相信一说到监听,当然就离不了设计模式中鼎鼎大名的观察者模式。举个例子,你家后院着火了,可一定要等到烟雾很大火光很亮你才能发现啊,可是当你安装了一个火灾预警器,当发生火灾就立马能够通知到你了。这就是一个... -
前端毕业设计大作业基于Electron+Vue构建的桌面音乐播放器源码.zip
2022-06-15 06:25:06期末大作业基于Electron+Vue构建的桌面音乐播放器源码。 特性 支持音乐频谱 界面友好,支持皮肤切换 跨平台,可打包Windows、Mac、Linux 良好的架构模式和代码风格 提供支持主流的第三方音乐平台 期末大作业基于... -
VUE源码相关面试题汇总
2020-07-12 10:52:41Q1: vue3今年发布了,请你说一下他们之间在相应式的实现上有什么区别? A: vue2采用的是defineProperty去定义get,set,而vue3改用了proxy。...请你说一下vue的设计架构。 A: vue2采用的是典型的混入式架构,类似于exp -
Vue源码阅读 - 文件结构与运行机制
2019-06-12 09:31:47vue已是目前国内前端web端三分天下之一,同时也作为本人主要技术栈之一,在日常使用中知其然也好奇着所以然,另外最近的社区涌现了一大票vue源码阅读类的文章,在下借这个机会从大家的文章和讨论中汲取了一些营养,... -
基于SSM+Vue的“依伴汉服”商城设计与开发(含数据库设计+源码)
2022-06-10 00:55:57Vue作为前端开发框架,以学习研究为目的,基于 Java EE 的 web 开发平台,采用Spring+SpringMvc+Mybatis+Vue+阿里云OSS+ElementUi前后端分离的架构模式,设计并实现国民品牌独立商城——《“依伴汉服”网》,包括...