Vue
计算属性和data、methods、watch的异同
计算属性(computed)
重要点: 当且仅当计算属性依赖的data改变时才会自动计算。
当页面中有某些数据依赖其他数据进行变动的时候,可以使用计算属性。
计算属性的结果会被缓存,除非依赖的响应式属性变化才会重新计算。注意,如果某个依赖 (比如非响应式属性) 在该实例范畴之外,则计算属性是不会被更新的。
适用场景:一个数据受多个数据影响。监听属性(watch)
watch (使用watch函数,当数据改变时自动引发事件)
watch是一个对象,键是 需要观察的表达式,值是 对应回调函数,或是方法名,或者包含选项的对象。
Vue 实例将会在实例化时调用 $watch(),遍历 watch 对象的每一个属性,而且在callback中会返回两个对象,分别是oldValue和newValue.顾名思义,这两个对象就是对象发生变化前后的值。
适用场景:一个数据影响多个数据。
同:
methods,watch和computed都是以函数为基础的.
异:
计算属性是基于它们的响应式依赖进行缓存的,每次访问的时候都是返回之前函数执行的结果,而methods方法需要每次调用
总结
1、watch和computed都是以Vue的依赖追踪机制为基础的,它们都试图处理这样一件事情:当某一个数据(称它为依赖数据)发生变化的时候,所有依赖这个数据的“相关”数据“自动”发生变化,也就是自动调用相关的函数去实现数据的变动。
2、对methods:methods里面是用来定义函数的,很显然,它需要手动调用才能执行。而不像watch和computed那样,“自动执行”预先定义的函数
3、computed在数据未发生变化时,优先读取缓存。computed 计算属性只有在相关的数据发生变化时才会改变要计算的属性,当相关数据没有变化是,它会读取缓存。而不必想 motheds方法 和 watch 方法是的每次都去执行函数。
watch和computed各自处理的数据关系场景不同
・watch方法每次只能监听一个data值的变化
・而计算属性可以同时监听多个data值的变化,且用计算属性可以简化watch中重复的代码
vue的SFC(单文件组件)里面的data必须是一个函数返回,而不能就只是一个对象
Vue 将会递归将 data 的属性转换为 getter/setter,从而让 data 的属性能够响应数据变化。
本质:对象的复制是内存地址的复制,一个改了其他的也改了。
如果两个实例引用同一个对象,当其中一个实例的属性发生改变时,另一个实例属性也随之改变,只有当两个实例拥有自己的作用域时,才不会相互干扰。
这是因为JavaScript的特性所导致,在component中,data必须以函数的形式存在,不可以是对象。
组建中的data写成一个函数,数据以函数返回值的形式定义,这样每次复用组件的时候,都会返回一份新的data,相当于每个组件实例都有自己私有的数据空间,它们只负责各自维护的数据,不会造成混乱。而单纯的写成对象形式,就是所有的组件实例共用了一个data,这样改一个全都改了。
SFC和vue-loader
vue-loader 是一个Webpack的 loader,使用 vue-loader 就可以用 Vue Single-File Component (SFC) 即单文件组件的形式编写一个组件,vue-loader 会将.vue文件转换为 JS模块。
模板块
一个SFC中最多一个< template >块;
其内容将被提取为字符串传递给 vue-template-compiler ,然后webpack将其编译为js渲染函数,并最终注入到从
URL 路径解析规则:
绝对路径原样保存;
以“.”开头,则将其解释为相对模块请求,并根据文件系统上的文件夹结构解析;
以“~”开头,则将其后的内容解析为模块请求,可以在节点模块引用这些内容;
以“@”开头,则其后的内容也被解释为模块请求,@在 vue-cli 创建的项目中默认指向/src,可以使用 webpack 配置 @ 别名
style中scpoed和module
- scoped
使用 scope 时,子组件的根节点将受父组件作用域 CSS 影响
使用 scope 作用域时,父组件的样式不会泄漏到子组件中。 但子组件的根节点将受父级作用域 CSS 和子级作用域 CSS 的影响。 这是为了父级可以设置子组件根元素的样式以进行布局。vue-loader 处理的 CSS 输出,都是通过 PostCSS 进行作用域重写1
data-v-f3f3eg9
使父组件可以使用‘ >>> ’或‘ /deep/ ’ 这种深度选择器作用于子组件
- module
一个 CSS Module 其实就是一个 CSS 类型的文件,其编写方式与 CSS 相同,但在编译时会编译为 ICSS 低级交换格式。
其默认所有的类名/动画名都在本地作用域,当从 JS 模块导入 CSS 模块时,它会导出包含从本地名称到全局名称的所有映射的一个对象
都是挂载到this.$style上
Vue中是如何确保先更新父组件,再更新子组件,为什么要这么做?
所有的 Vue 实例(Vue Component 实例),都有一个唯一 id,且该 id 为自增的
这样,父实例的 id < 子组件 id < 后继兄弟节点 id
Vue数据派发更新时,有一个队列,按 id 排序,即可
为什么这么做?若不控制顺序,则子组件有可能会再次因父组件更新而更新,导致多余的页面更新
Vue的依赖收集
Vue 实例化时,会把 data 的所有属性都 通过defineProperty来设置getter,在该函数的闭包中,创建一个 Dep 对象,如果Dep.target(静态属性)存在的情况下会做依赖收集
Vue 的template 最后会被编译为函数,所以渲染过程是调用一个个的函数,在调用上述函数时(如
addDep是定义值Watcher类下面的一个方法,通过一些逻辑判断是否存在,在执行上面Dep的addSub方法,将渲染watcher push 到subs数组里,此时前面闭包中的Dep将自己 push 到当前的 Watcher 依赖列表里。
这样,访问一次某属性,就完成了依赖的收集
1 | const getter = property && property.get |
1 | { this.b + 1 } //模板 |
访问 this.b + 1 时,这个 watcher 就会收集到 b 这个属性的依赖
Vue派发更新是如何实现的?
Vue派发更新:从数据更新到视图的更新,首先是获取原有值和新值,然后新值和旧值作对比,如果相同则不处理,否则将新值赋值给val,如果新值赋值给val,如果新值仍然是个对象,则在重新调用observe方法将其变成响应式,最后调用dep.notify方法
dep.notyify方法,就是遍历所有的订阅者,也就是渲染watcher,使每一个watcher调用update方法
1 | notify () { |
实际就是每个watcher执行queueWatcher()方法
1 | update () { |
queueWatcher方法,先将watcher push到队列中,然后再下一个tick内执行flushSchedulerQueue,nextTick就是对promise的一层封装
1 | export function queueWatcher (watcher: Watcher) { |
flushSchedulerQueue,首先对队列wacher进行排序,主要为了处理父子组件,userWatcher等情况,然后调用watcher.run方法
1 | watcher.run() |
wathcer.run方法会将新值赋给value
过程及优化:
Vue的diff算法
1、当数据发生时,vue是如何更新节点的
如果修改数据时直接渲染到真实DOM上会引起整个DOM树的重绘和重排。
diff算法就是先根据真实的DOM生成一颗virtual DOM,当virtual DOM某个节点的数据改变后会生成一个新的Vnode,然后Vnode和oldVNode作对比,发现有不一样的地方直接修改在真实的DOM上,然后使oldVnode的值为Vnode
diff的过程就是调用patch函数,比较新旧节点,一边比较一边给真实的DOM打补丁。
2、 virtual DOM和真实的DOM的区别?
virtual DOM是将真实的DOM数据抽取出来,以对象的形式模拟树形结构,比如1
2
3
4
5
6
7
8
9
10
11
12
13
14<div>
<p>123</p>
</div>
// 对应的virtual DOM(伪代码)
var Vnode = {
tag: 'div',
children: [
{
tag: 'p',
text: '123'
}
]
}
//VNode和oldVNode都是对象
3、 diff的比较方式?
在采取diff算法比较新旧节点的时候,比较只会在同层级进行, 不会跨层级比较。
4、 流程图
当数据发生改变时,set方法会让调用Dep.notify通知所有订阅者Watcher,订阅者就会调用patch给真实的DOM打补丁,更新相应的视图。
Vue列表组件设置key和不设置key的区别
key的作用:更新组件时判断两个节点是否相同,相同就复用,不相同就删除旧的,创建新的。
key提高diff效率是不准确的,因为在vue/patch.js中,在不带key的情况下,判断sameVnode时由于a.key和b.key都是undefined,对于列表渲染来说已经可以判断为相同节点,然后调用patchVnode了。就不会走进else代码,所以diff效率不存在高效一说。
带唯一key时每次更新都不能找到可复用的节点,不但要销毁和创建vnode,在DOM里添加移除节点对性能的影响更大。所以会才说“不带key可能性能更好”。
因为不带key时节点能够复用,省去了销毁/创建组件的开销,同时只需要修改DOM文本内容而不是移除/添加节点,这就是文档中所说的“刻意依赖默认行为以获取性能上的提升”。
建议带key的原因:
因为这种模式只适用于渲染简单的无状态组件。对于大多数场景来说,列表组件都有自己的状态。
没有绑定key的情况下,并且在遍历模板简单的情况下,会导致虚拟新旧节点对比更快,节点也会复用。而这种复用是就地复用,一种鸭子辩型的复用。没有key的情况下可以对节点就地复用,提高性能。
优势:
key是给每一个vnode的唯一id,可以依靠key,更准确, 更快的拿到oldVnode中对应的vnode节点。
1、 更准确
因为带key就不是就地复用了,在sameNode函数 a.key === b.key对比中可以避免就地复用的情况。所以会更加准确。
2、 更快
利用key的唯一性生成map对象来获取对应节点,比遍历方式更快。(map会比遍历更快)
Vuejs3.0和Vuejs2.0的区别
Vuejs3.0的Proxy作为其观察者机制代替了Vuejs2.0的Object.defineProperty.
Object.defineProperty 的缺陷 与 proxy的优点
- Object.defineProperty无法监控到数组下标的变化,导致通过数组下标添加元素,不能实时响应;
- Object.defineProperty只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历,如果,属性值是对象,还需要深度遍历。Proxy可以劫持整个对象,并返回一个新的对象。
- Proxy不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。
proxy的缺点:
Proxy是es6提供的新特性,兼容性不好,最主要的是这个属性无法用polyfill来兼容(摒弃了IE)
未来一段时间的趋势会是vue2和vue3并行
Vue源码创建对象的方法
Object.create(null) 和 {} 的区别
1 | Object.create(null) // No properties |
- {}原型指向Object.prototype,会继承Object的属性和方法,例如toString()的方法
Object.create(proto, [propertiesObject])
- proto:新创建对象的原型对象。
- propertiesObject:可选。新创建对象的可枚举属性(即其自身定义的属性,而不是其原型链上的枚举属性)。
vue用Object.create()方法创建对象的原因:
1、使用Object.create(null)创建的对象,是一个完全空白的对象,不会继承任何属性和方法,在后续使用中不需要考虑命名冲突的问题。
2、重写对象的原型方法或者给对象添加新方法,能够保证后续调用时代码风格的统一。
Webpack
webpack处理按需引入
方法一: tree shaking
通常用于描述移除js上下文中的未引用代码(dead-code),它依赖于ES2015(ES6)模块语法的静态结构特性,例如import 和 export 。(ES2015 模块打包工具 rollup 普及的: JavaScript的模块捆绑器,可将一小段代码编译成更大或更复杂的内容)
webpack中各个模块的意义
【Loader】:加载某些资源文件,用于对模块源码的转换, webpack 只能理解 JavaScript 和 JSON 文件,loader描述了webpack如何处理非javascript模块,并且在build中引入这些依赖。loader可以将文件从不同的语言(如TypeScript)转换为JavaScript,或者将内联图像转换为data URL。比如说:CSS-Loader,Style-Loader等。
【Plugin】:目的在于解决loader无法实现的其他事,从打包优化和压缩,到重新定义环境变量,功能强大到可以用来处理各种各样的任务。webpack提供了很多开箱即用的插件:CommonChunkPlugin(optimize)主要用于提取第三方库和公共模块,避免首屏加载的bundle文件,或者按需加载的bundle文件体积过大,导致加载时间过长,是一把优化的利器。而在多页面应用中,更是能够为每个页面间的应用程序共享代码创建bundle。
【Mode】可以在config文件里面配置,也可以在CLI参数中配置:webpack –mode=production(一般会选择在CLI,也就是npm scripts里面进行配置)。
【resolve】模块,resolver是个库,帮助webpack找到bundle需要引入的模块代码,打包时,webpack使用enhanced-resolve来解析路径。
【Manifest】管理所有模块之间的交互。runtime将能够查询模块标识符,检索出背后对应的模块。
webpack模块化原理-Code Splitting
用途:提高首屏时间,代码拆分,动态加载
webpack的模块化不仅支持commonjs和es module,还能通过code splitting实现模块的动态加载。根据wepack官方文档,实现动态加载的方式有两种:import和require.ensure。
编译后的代码,都是通过IFFE的方式启动代码,然后使用webpack实现的require和exports实现的模块化。
对于code splitting的支持,区别在于这里使用webpack_require.e实现动态加载模块和实现基于promise的模块导入。1
chunkFilename: '[name].bundle.js' // 在output里面加入分离的模块名称
1 | () => import() // vue-router加载组件的方式 |
_webpack_require_.e函数的定义,这个函数实现了动态加载, 代码大致逻辑如下:
- 缓存查找:从缓存installedChunks中查找是否有缓存模块,如果缓存标识为0,则表示模块已加载过,直接返回promise;如果缓存为数组,表示缓存正在加载中,则返回缓存的promise对象
- 如果没有缓存,则创建一个promise,并将promise和resolve、reject缓存在installedChunks中
- 构建一个script标签,append到head标签中,src指向加载的模块脚本资源,实现动态加载js脚本
- 添加script标签onload、onerror 事件,如果超时或者模块加载失败,则会调用reject返回模块加载失败异常
- 如果模块加载成功,则返回当前模块promise,对应于import()
webpackJsonp类似于jsonp中的callback,作用是作为模块加载和执行完成的回调,从而触发import的resolve。
大致过程:
根据chunkIds收集对应模块的resolve,这里的chunkIds为数组是因为require.ensure是可以实现异步加载多个模块的,所以需要兼容
把动态模块添加到IFFE的modules中,提供其他CMD方案使用模块
直接调用resolve,完成整个异步加载
总结:
webpack通过webpack_require.e函数实现了动态加载,再通过webpackJsonp函数实现异步加载回调,把模块内容以promise的方式暴露给调用方,从而实现了对code splitting的支持。
webpack打包原理
webpack只是一个打包模块的机制,只是把依赖的模块转化成可以代表这些包的静态文件。webpack就是识别你的 入口文件。识别你的模块依赖,来打包你的代码。至于你的代码使用的是commonjs还是amd或者es6的import。webpack都会对其进行分析。来获取代码的依赖。webpack做的就是分析代码。转换代码,编译代码,输出代码。webpack本身是一个node的模块,所以webpack.config.js是以commonjs形式书写的(node中的模块化是commonjs规范的)
webpack中每个模块有一个唯一的id,是从0开始递增的。整个打包后的bundle.js是一个匿名函数自执行。参数则为一个数组。数组的每一项都为个function。function的内容则为每个模块的内容,并按照require的顺序排列。
loader原理
在解析对于文件,会自动去调用响应的loader,loader 本质上是一个函数,输入参数是一个字符串,输出参数也是一个字符串。当然,输出的参数会被当成是 JS 代码,从而被 esprima 解析成 AST,触发进一步的依赖解析。webpack会按照从右到左的顺序执行loader
为什么webpack会按照从右往左的顺序去执行loader
只是Webpack选择了compose方式(在函数式编程中有组合的概念,函数式编程一般的实现方式是从右往左),而不是pipe的方式而已
devDependencies和dependencies
devDependencies用于本地环境开发时候。
dependencies用户发布环境
其实看名字我也知道是这个意思,我觉得没解释情况。
devDependencies是只会在开发环境下依赖的模块,生产环境不会被打入包内。通过NODE_ENV=developement或NODE_ENV=production指定开发还是生产环境。
而dependencies依赖的包不仅开发环境能使用,生产环境也能使用
npm避免版本的更新带来bug的问题:package-lock.json
package-lock.json的出现时避免同一个package.json产生不同的运行结果.
npm会根据package-lock.json里的内容来处理和安装依赖而不是根据package.json. 因为pacakge-lock.json给每个依赖标明了版本, 获取地址和哈希值, 使得每次安装都会出现相同的结果
官方回答:
当我们对node_modules或者package.json进行了更改后, package-lock.json文件会自动生成. 里面会描述上一次更改后的确切的依赖管理树. 里面包含了唯一的版本号和相关的包信息. 之后的npm install会根据package-lock.json文件进行安装, 保证不同环境, 不同时间下的依赖是一样的.
还有一个好处是, 由于package-lock.json文件中记录了下载源地址, 可以加快我们的npm install速度
package-lock.json是一个包含你所有依赖的巨大列表, 它包含明确的版本号(没有通配符), 依赖的获取地址, 一个用于验证完整性和正确性的哈希值, 以及这个依赖本身所需要的依赖.
npm install的安装原理
查询node_modules目录之中是否已经存在指定模块
若存在,不再重新安装
若不存在
npm 向 registry 查询模块压缩包的网址
下载压缩包,存放在根目录下的.npm目录里
解压压缩包到当前项目的node_modules目录
- 执行工程自身 preinstall
当前 npm 工程如果定义了 preinstall 钩子此时会被执行。 - 确定首层依赖模块
首先需要做的是确定工程中的首层依赖,也就是 dependencies 和 devDependencies 属性中直接指定的模块(假设此时没有添加 npm install 参数)。
工程本身是整棵依赖树的根节点,每个首层依赖模块都是根节点下面的一棵子树,npm 会开启多进程从每个首层依赖模块开始逐步寻找更深层级的节点。 - 获取模块
获取模块是一个递归的过程,分为以下几步:
获取模块信息。在下载一个模块之前,首先要确定其版本,这是因为 package.json 中往往是 semantic version(semver,语义化版本)。此时如果版本描述文件(npm-shrinkwrap.json 或 package-lock.json)中有该模块信息直接拿即可,如果没有则从仓库获取。如 packaeg.json 中某个包的版本是 ^1.1.0,npm 就会去仓库中获取符合 1.x.x 形式的最新版本。 - 获取模块实体。上一步会获取到模块的压缩包地址(resolved 字段),npm 会用此地址检查本地缓存,缓存中有就直接拿,如果没有则从仓库下载。
查找该模块依赖,如果有依赖则回到第1步,如果没有则停止。 模块扁平化(dedupe)
上一步获取到的是一棵完整的依赖树,其中可能包含大量重复模块。比如 A 模块依赖于 loadsh,B 模块同样依赖于 lodash。在 npm3 以前会严格按照依赖树的结构进行安装,因此会造成模块冗余。
从 npm3 开始默认加入了一个 dedupe 的过程。它会遍历所有节点,逐个将模块放在根节点下面,也就是 node-modules 的第一层。当发现有重复模块时,则将其丢弃。
这里需要对重复模块进行一个定义,它指的是模块名相同且 semver 兼容。每个 semver 都对应一段版本允许范围,如果两个模块的版本允许范围存在交集,那么就可以得到一个兼容版本,而不必版本号完全一致,这可以使更多冗余模块在 dedupe 过程中被去掉。
比如 node-modules 下 foo 模块依赖 lodash@^1.0.0,bar 模块依赖 lodash@^1.1.0,则 ^1.1.0 为兼容版本。
而当 foo 依赖 lodash@^2.0.0,bar 依赖 lodash@^1.1.0,则依据 semver 的规则,二者不存在兼容版本。会将一个版本放在 node_modules 中,另一个仍保留在依赖树里。
举个例子,假设一个依赖树原本是这样:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16node_modules
-- foo
---- lodash@version1
-- bar
---- lodash@version2
假设 version1 和 version2 是兼容版本,则经过 dedupe 会成为下面的形式:
node_modules
-- foo
-- bar
-- lodash(保留的版本为兼容版本)
假设 version1 和 version2 为非兼容版本,则后面的版本保留在依赖树中:
node_modules
-- foo
-- lodash@version1
-- bar
---- lodash@version2安装模块
这一步将会更新工程中的 node_modules,并执行模块中的生命周期函数(按照 preinstall、install、postinstall 的顺序)。- 执行工程自身生命周期
当前 npm 工程如果定义了钩子此时会被执行(按照 install、postinstall、prepublish、prepare 的顺序)。
最后一步是生成或更新版本描述文件,npm install 过程完成。
webpack 热更新原理,是如何做到在不刷新浏览器的前提下更新页面的
1.当修改了一个或多个文件;
2.文件系统接收更改并通知webpack;
3.webpack重新编译构建一个或多个模块,并通知HMR(Hot Module Replacement)服务器进行更新;
4.HMR Server 使用webSocket通知HMR runtime 需要更新,HMR运行时通过HTTP请求更新jsonp;
5.HMR运行时替换更新中的模块,如果确定这些模块无法更新,则触发整个页面刷新。
hot-module-replacement-plugin 的作用是提供 HMR 的 runtime,并且将 runtime 注入到 bundle.js 代码里面去。一旦磁盘里面的文件修改,那么 HMR server 会将有修改的 js module 信息发送给 HMR runtime,然后 HMR runtime 去局部更新页面的代码。因此这种方式可以不用刷新浏览器。
函数式编程
函数式编程关心数据的映射,命令式编程关心解决问题的步骤
函数式编程关心类型(代数结构)之间的关系,命令式编程关心解决问题的步骤
函数式编程
- 面向对象编程(OOP)通过封装变化使得代码更容易理解
- 函数式编程(FP)通过最小化变化使得代码更容易理解
特点:
- 声明式编程
- 纯函数
- 引用透明
- 不可变性
优势:
- 使用纯函数的代码绝不会更改或破坏全局状态,有助于提高代码的可测试性和可维护性
- 函数式编程采用声明式的风格,易于推理,提高代码的可读性。
- 函数式编程将函数视为积木,通过一等高阶函数来提高代码的模块化和可重用性。
- 可以利用响应式编程组合各个函数来降低事件驱动程序的复杂性(这点后面可能会单独拿一篇来进行讲解)。
引用透明
引用透明是定义一个纯函数较为正确的方法。纯度在这个意义上表面一个函数的参数和返回值之间映射的纯的关系。如果一个函数对于相同的输入始终产生相同的结果,那么我们就说它是引用透明。
1 | // 非引用透明 |
immutable和mutable的区别
immutable对象(不可变对象)是指一旦创建之后状态不可改变的对象。
mutable对象(可变对象)是指创建之后也可以修改的对象。
在js里基本数据类型就是不可变的,引用类型就是可变的
在有些情况下,对象也被认为是不可变的(immutable),一个对象包含的内部使用的属性改变了,但从外部看对象的状态并没有改变。例如,一个使用memoization来缓存复杂计算结果的对象仍然被看作是不可变(immutable)对象.
函数式编程1加到100
1 | const add = (i) => { |
思路编程
10个ajax同时发起请求,全部返回展示结果,并且至多允许三次失败,说出设计思路
Promise.all 这个函数的局限性在于如果失败一次就返回了。
思路1:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19let successCount = 0
let errorCount = 0
let datas = []
ajax(url, (res) => {
if (success) {
success++
if (success + errorCount === 10) {
console.log(datas)
} else {
datas.push(res.data)
}
} else {
errorCount++
if (errorCount > 3) {
// 失败次数大于3次就应该报错了
throw Error('失败三次')
}
}
})
思路二1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// Promise 写法
let errorCount = 0
let p = new Promise((resolve, reject) => {
if (success) {
resolve(res.data)
} else {
errorCount++
if (errorCount > 3) {
// 失败次数大于3次就应该报错了
reject(error)
} else {
resolve(error)
}
}
})
Promise.all([p]).then(v => {
console.log(v);
});