本文主要记录的是自己平时学习时的笔记,vue相关的知识点很多,后续会陆续更新本片文章。
React和Vue的相同点
- 两者都在渲染流程小红采用虚拟DOM以降低页面开销。但是Vue的虚拟DOM实现的层级更高一些,这也意味着Vue比React更轻量,性能更高一些。
- 两者都提供一些功能性组件以减少用户开销。但是Vue的渲染速度更快一些,这是因为React中有大量用于提供警告和错误提示信息的检查机制。
- 两者都将重点放到核心库上,而将其他功能如路由和全局状态管理交给相关的库。
React和Vue的不同点
- 渲染优化
- React:当组件状态发生变化时,以该组件为根,重新渲染整个组件子树。
- Vue:自动追踪组件依赖,精确渲染状态改变的组件。
- HTML和CSS
- React:一切都是JavaScript。所有的组件的渲染功能都依靠JSX。
- Vue:Vue也提供了渲染函数,甚至支持JSX。然而,我们默认推荐的还是模板。
- 组件作用域内的CSS
- React:通过 CSS-in-JS 的方案实现的。
- Vue:也支持 CSS-in-JS 方案。默认方法是单文件组件里类似
style
的标签。
- 规模
- 向上扩展:
- React:
- 路由库和状态管理库,由社区维护支持,生态系统分散且繁荣
- 提供脚手架工具,故意作了限制
- 不允许在项目生成时进行配置
- 只提供一个单页面应用模板
- 不支持预设配置
- Vue:
- 路由库和状态管理库都由官方维护支持且与核心库同步更新
- 提供CLI脚手架,可构建项目,快速开发组件的原型
- 允许在项目生成时配置
- 提供了各种用途的模板
- 支持预设配置
- React:
- 向下拓展
- React:学习曲线陡峭,需要前置知识
- Vue:既支持向上拓展与 React 一样,也支持向下拓展与 jQuery 一样,上手快
虚拟DOM
虚拟DOM主要做两件事:
- 提供与真实DOM节点所对应的虚拟节点vnode
- 将虚拟节点vnode和旧虚拟节点oldVnode进行对比,然后更新视图。
对两个虚拟节点进行对比是虚拟DOM中最核心的算法(即patch),它可以判断出哪些节点发生了变化,从而只对发生了变化的节点进行更新操作。整个patch的过程并不复杂,当oldVnode不存在时,直接使用vnode渲染视图;当oldVnode和vnode都存在但并不是同一个节点时,使用vnode创建的DOM元素替换旧的DOM元素;当oldVnode和vnode是同一个节点时,使用更详细的对比操作对真实的DOM节点进行更新。
虚拟DOM的运行原理是使用状态生成虚拟节点,然后使用虚拟节点渲染视图。
先创建虚拟节点再渲染视图,就可以将虚拟节点缓存下来,然后使用新创建的虚拟节点和上一次渲染时缓存的虚拟节点进行对比,接着根据对比结果去更新需要更新的真实DOM节点,从而避免不必要的DOM操作,节省了一定的性能开销。
变化侦测
Vue.js最独特的特性之一是响应式系统。响应式系统赋予了Vue重新渲染的能力,其重要组成部分就是变化侦测。变化侦测的作用是侦测数据的变化,当数据变化的时候,会通知视图进行相应的更新。
变化侦测分为两种类型:推(push)和拉(pull)。React的变化侦测属于“拉”,就是说当状态发生变化时,它不知道哪个状态变了,只知道状态有可能变了,然后会发送一个信号告诉框架,框架内部收到信号后,会进行比对找到需要重新渲染的DOM节点。而Vue的变化侦测属于“推”,当状态发生变化时,就立即知道了,而且在一定程度上知道哪些状态发生了改变,降低了依赖追踪所消耗的内存。
Object的变化侦测
通过Object.defineProperty将属性转换成 getter/setter 的形式来追踪数据的变化。但由于在ES6之前JavaScript没有提供元编程的能力,所以getter/setter只能追踪一个数据是否被修改,无法追踪新增属性和删除属性。
我们需要在getter中收集有哪些依赖使用了数据。当setter被触发时,去通知getter中收集的依赖数据发生了变化。
收集依赖就需要为依赖找一个存储的地方,为此创建了Dep,用来收集依赖、删除依赖和向依赖发送信息等。
所谓的依赖,就是Watcher。只有Watcher触发的getter才会收集依赖,哪个Watcher触发了getter,就把哪个Watcher收集到Dep中。当数据发生变化时,会循环依赖列表,把所有的Watcher都通知一遍。
此外,还创建了Observer类,它的作用是把一个object中的所有数据(包括子数据)都转换成响应式的,侦测object中所有数据(包括子数据)的变化。
总结:
Data通过Observer转换成了getter/setter的形式来追踪变化。当外界通过Watcher读取数据时,会触发getter从而将Watcher添加到依赖中。当数据发生了变化时,会触发setter,从而向Dep中的依赖(Watcher)发送通知。Watcher接收到通知后,会向外界发送通知,变化通知到外界后可能会触发视图更新,也有可能触发用户的某个回调函数等。
Array的变化侦测
Array追踪变化的方式和Object不一样。因为它是通过方法来改变内容的,所以我们通过创建拦截器去覆盖数组原型的方式来追踪变化,具体做法就是在Observer中只针对那些需要侦测变化的数组使用__proto__覆盖原型方法。
Array收集依赖的方式和Object相同,都是在getter中收集。但是由于使用依赖的位置不同,数组要在拦截器中向依赖发送消息,所以依赖保存在Observer实例上。
在Observer中,对每个侦测了变化的数据都标记上__ob__,并把this保存在__ob__上。这样做是为了标记数据是否被侦测了变化,保证同一个数据只被侦测一次;同时也可以很方便的通过数据读取到__ob__,从而拿到Observer实例上保存的依赖。当拦截到数组发生变化时,会向依赖发送通知。
除了侦测数组自身的变化,也要侦测数组中元素发生的变化。在Observer中判断如果当前被侦测的数据是数组,则调用observeArray方法将数组中的每一个元素都转换成响应式的并侦测变化。
除了侦测已有数据外,当用户使用push等方法向数组中新增数据时,新增的数据也要进行变化侦测。我们使用当前操作数组的方法来进行判断,如果是push、unshift和splice方法,则从参数中将新增数据提取出来,然后使用observeArray对新增数据进行变化侦测。
但是由于在ES6之前,JavaScript并没有提供元编程的能力,所以对于数组类型的数据,一些语法无法追踪到变化,只能拦截原型上的方法,而无法拦截数组特有的语法,例如使用length清空数组的操作就无法拦截。
模板编译
模板编译的主要目标就是生成渲染函数。而渲染函数的作用是每次执行它,它就会使用当前最新的状态生成一份新的vnode,然后使用这个vnode进行渲染。
在大体逻辑上,模板编译分为三部分:
- 利用解析器将模板解析成AST
- 利用优化器遍历AST标记静态节点
- 使用代码生成器将AST转换成渲染函数中的内容,这个内容可以称为代码字符串。