通过Jest深度了解源码
现在准备向原理源码进军了。有个小问题先要处理一下。就是研究一下如何把Vue3的单元测试跑起来。毕竟光读代码不运行是没有灵魂的。歪歪一下中国的球迷是不是就是光看不踢。
Vue3代码是基于Jest进行测试,我们先简单看看什么是jest
Jest简介
Jest 是Facebook的一个专门进行Javascript单元测试的工具,适合JS、NodeJS使用,具有零配置、内置代码覆盖率、强大的Mocks等特点。
总之目前来讲JS界Jest是一套比较成体系的测试工具。为什么这么说呢比如拿以前的测试框架Mocha对比 他只是一个测试框架,如果你需要断言还需要专门的断言库比如assert shoud expect等等 如果需要Mock还需要住啊们的库来支持很不方便。不过Jest基本上可以一次性搞定。
目录文件名约定
Jest测试代码和逻辑代码是遵从约定优于配置(convention over configuration)其实这个也是目前编程世界普遍接受的原则。
Jest的测试代码是基于以下约定
- 测试文件名要以spec结果
- 测试文件后缀为js,jsx,ts,tsx
- 测试文件需要放在tests/unit/目录下或者是/tests/目录下
只要满足这三个要求的测试文件,使用运行jest时就会自动执行。
其实这个规定类似于Maven对于测试代码和逻辑代码的约定只是test目录换成了__tests__
下面我们具体看一下Vue3源码的目录结构
其实逻辑代码和测试代码对应放置还是很方便的 我们再看看另外一个reactive这个包
运行全量测试
package.json文件中已经配置好了jest
npm run test
覆盖率
我们增加一个参数把覆盖率跑出来
npx jest --coverage
实际上跑覆盖率的时候是有错的 我们先不去管他 我们先解析一下这个报告怎么看,如果大家学过软件工程会知道一般从理论上讲覆盖率包括
- 语句覆盖
- 节点覆盖
- 路径覆盖
- 条件组合覆盖
但是一般来讲不同框架理解不一样 在Jest这里大概是这样分解的
- %stmts是语句覆盖率(statement coverage):是不是每个语句都执行了?
- %Branch分支覆盖率(branch coverage):是不是每个if代码块都执行了?
- %Funcs函数覆盖率(function coverage):是不是每个函数都调用了?
- %Lines行覆盖率(line coverage):是不是每一行都执行了?
单独运行一个测试
比如我们看看vue的index这个测试
有两种方法进行单独测试
// 全局安装 npm i jest -g jest index // 或者更更简便一点 npx jest index
index.spec.ts
import { createApp } from '../src' it('should support on-the-fly template compilation', () => { const container = document.createElement('div') const App = { template: `{{ count }}`, data() { return { count: 0 } } } createApp().mount(App, container) // 断言 expect(container.innerHTML).toBe(`0`) })
声明中说为了确认模板编译可以生效,这个就是一个简单的数据绑定 最后 断言也是看了一下 count是否为 0 这个例子其实除了断言部分其实直接拷贝到第一次讲的那个html文件里面是可以运行的。
响应式Reactive的单元测试
看一下每个包对应的测试代码都放在__tests__文件件中
npx jest reactive --coverage
好了后面我们就可以开始向源码进军了
代码结构
源码位置是在package文件件内,实际上源码主要分为两部分,编译器和运行时环境。
- 编译器
- compiler-core 核心编译逻辑
- 基本类型解析
- AST
- compiler-dom 针对浏览器的编译逻辑
- v-html
- v-text
- v-model
- v-clock
- 运行时环境
- runtime-core 运行时核心
- inject
- 生命周期
- watch
- directive
- component
- runtime-dom 运行时针对浏览器的逻辑
- class
- style
- runtime-test 测试环境仿真
主要为了解决单元测试问题的逻辑 在浏览器外完成测试比较方便
- reactivity 响应式逻辑
- template-explorer 模板解析器 可以这样运行
yarn dev template-explorer
然后打开index.html
- vue 代码入口
整合编译器和运行时
- server-renderer 服务器端渲染(TODO)
- share 公用方法
Vue2和Vue3响应方式对比
Vue2响应式是什么
首先我们说说什么是响应式。通过某种方法可以达到数据变了可以自由定义对应的响应就叫响应式。
具体到我们MVVM中 ViewModel的需要就是数据变了需要视图作出响应。 如果用Jest用例便表示就是这样
it('测试数据改变时 是否被响应', () => { const data = reactive({ name: 'abc', age: { n: 5 } }) // Mock一个响应函数 const fn = jest.fn() const result = fn() // 设置响应函数 effect(fn) // 改变数据 data.name = 'efg' // 确认fn生效 expect(fn).toBeCalled() })
假定我们需要的是数据data变化时可以触发fn函数也就是作出相应,当然相应一般是触发视图更新当然也可以不是。我们这里面用jest做了一个Mock函数来检测是否作出相应。
最后代码expect(fn).toBeCalled()有效即代表测试通过也就是作出了相应
Vue2的解决方案
下面展示的是vue2的实现方式是通过Object.defineProperty来重新定义getter,setter方法实现的。
let effective function effect(fun) { effective = fun } function reactive(data) { if (typeof data !== 'object' || data === null) { return data } Object.keys(data).forEach(function (key) { let value = data[key] Object.defineProperty(data, key, { emumerable: false, configurable: true, get: () => { return value }, set: newVal => { if (newVal !== value) { effective() value = newVal } } }) }) return data } module.exports = { effect, reactive }
当然还有两个重要的问题需要处理 第一个就是这样做只能做浅层响应 也就是如果是第二层就不行了。
it('测试多层数据中改变时 是否被响应', () => { const data = reactive({ age: { n: 5 } }) // Mock一个响应函数 const fn = jest.fn() // 设置响应函数 effect(fn) // 改变多层数据 data.age.n = 1 // 确认fn生效 expect(fn).toBeCalled() })
比如以下用例 就过不去了 当然解决的办法是有的 递归调用就好了
当然这样也递归也带来了性能上的极大损失 这个大家先记住。
然后是数组问题 数组问题我们可以通过函数劫持的方式解决
const oldArrayPrototype = Array.prototype const proto = Object.create(oldArrayPrototype); ['push','pop','shift','unshift','splice','sort','reverse'].forEach(method => { // 函数劫持 proto[method] = function(){ effective() oldArrayPrototype[method].call(this,...arguments) } }) // 数组通过数据劫持提供响应式 if(Array.isArray(data)){ data.__proto__ = proto }
Vue3
新版的Vue3使用ES6的Proxy方式来解决这个问题。之前遇到的两个问题就简单的多了。首先Proxy是支持数组的也就是数组是不需要做特别的代码的。对于深层监听也不不必要使用递归的方式解决。当get是判断值为对象时将对象做响应式处理返回就可以了。大家想想这个并不不是发生在初始化的时候而是设置值得时候当然性能上得到很大的提升。
function reactive(data) { if (typeof data !== 'object' || data === null) { return data } const observed = new Proxy(data, { get(target, key, receiver) { // Reflect有返回值不报错 let result = Reflect.get(target, key, receiver) // 多层代理 return typeof result !== 'object' ? result : reactive(result) }, set(target, key, value, receiver) { effective() // proxy + reflect const ret = Reflect.set(target, key, value, receiver) return ret }, deleteProperty(target,key){ const ret = Reflect.deleteProperty(target,key) return ret } }) return observed }
当然目前还是优缺点的缺点,比如兼容性问题目前IE11就不支持Proxy。不过相信ES6的全面支持已经是不可逆转的趋势了,这都不是事。
为了对比理解Vue2、3的响应式实现的不同我把两种实现都写了一下,并且配上了jest测试。大家可以参考一下 github.com/su37josephx…
// clone代码 yarn npx jest reactivity-demo
自定义渲染器
这个自定义渲染器和React的Render很类似。 可以根据需要定义各种各样的渲染器
- App端 Vue-Native
- 小程序端 后续应该会有很多这样的库
具体内容近期更新
新工具vite
我们知道ES6语法中 import在浏览器中完全可用。可以用于加载后端资源只不过这个特性一直被我们忽略。可能是由于webpack搞得太好了。我们一直忽略了他的存在。 Vite正是利用这个特性,只不过又更进一步,提供了对于vue文件的支持。不过可能是有点前卫。
- 简易Http服务器
- 无需webpack
- Vue文件直接渲染
- 热更新