本文作者:开课吧然叔
图文编辑:开三金
Vue 3.0 beta 了,带着秃头,轰轰烈烈地朝我们走来啦!
作为前端团队的粉丝,怎么能比别人学得晚呢?
今天,然叔再来把Vue 3的源码,给大家重新掰扯掰扯。
跑步进入Vue3.0时代。
二话不说跑通再说!
我们先看看如何在浏览器中断点调试:
克隆Vue3源码
Vue源码位置☟
https://github.com/vuejs/vue-next
git clone git@github.com:vuejs/vue-next.git
安装依赖编译源码
进入代码目录并安装依赖
yarn
yarn dev
测试API
做一个简单的Helloworld测试 我们就先试试原有的vue2 的Api还可不可以使用。
其实vue3中提倡使用composite-api也就是函数定义风格的api。
原有vue偏向于配置配置风格我们把它统称为options风格我们在根目录上创建文件夹:
options.html
<!DOCTYPE html><html lang="en">
<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> <script src="../../packages/vue/dist/vue.global.js"></script></head>
<body> <div id='app'></div> <script> const App = { template: ` <button @click='click'>{{message}}</button> `, data() { return { message: 'Hello Vue 3!!' } }, methods: { click() { console.log('click ....', this.message) this.message = this.message.split('').reverse().join('') } } } let vm = Vue.createApp().mount(App, '#app') // console.log(vm)</script></body>
</html>
其实这个代码就是vue2官网的代码。
拿过来可以正常运行 我们再试验一下vue3的composition api
<!DOCTYPE html><html lang="en">
<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> <script src="../../packages/vue/dist/vue.global.js"></script></head>
<body> <div id="app"></div> <script> const { createApp, reactive, computed, effect } = Vue const MyComponent = { template: ` <button @click="click"> {{ state.message }} </button> `, setup() { const state = reactive({ message:'Hello Vue 3!!' }) effect(() => { console.log('state change ', state.message) }) function click() { state.message = state.message.split('').reverse().join('') } return { state, click } } } createApp().mount(MyComponent, '#app')</script></body>
</html>
直接通过浏览器就可以打开本地文件
可以试一下点击的效果。
接下来如果你要debug一下源码的时候你会发现:
代码是经过打包的无法直接在源码上打断点调试。
添加SourceMap文件
为了能在浏览器中看到源码 并能够断点调试 需要再打包后代码中添加sourcemap
rollup.config.js
// rollup.config.js line:104左右output.sourcemap = true
在tsconfig.json中配置sourcemap输出:
// tsconfig.json{ "compilerOptions": { "baseUrl": ".", "outDir": "dist", "sourceMap": true, // ----------这里这里--------- "target": "esnext", "module": "esnext", "moduleResolution": "node", "allowJs": false, "noUnusedLocals": true, "strictNullChecks": true, "noImplicitAny": true, "noImplicitThis": true, "experimentalDecorators": true, "resolveJsonModule": true, "esModuleInterop": true, "removeComments": false, "jsx": "react", "lib": ["esnext", "dom"], "types": ["jest", "node"], "rootDir": ".", "paths": { "@vue/shared": ["packages/shared/src"], "@vue/runtime-core": ["packages/runtime-core/src"], "@vue/runtime-dom": ["packages/runtime-dom/src"], "@vue/runtime-test": ["packages/runtime-test/src"], "@vue/reactivity": ["packages/reactivity/src"], "@vue/compiler-core": ["packages/compiler-core/src"], "@vue/compiler-dom": ["packages/compiler-dom/src"], "@vue/server-renderer": ["packages/server-renderer/src"], "@vue/template-explorer": ["packages/template-explorer/src"] } }, "include": [ "packages/global.d.ts", "packages/runtime-dom/jsx.d.ts", "packages/*/src", "packages/*/__tests__" ]}
再次运行:
yarn dev
好了 同学们可以快乐的玩耍了。
CompositionApi是个啥
Alpha源码。
这一篇我们来尝鲜一下3.0版中最重要的一个RFC CompositionAPI。
概念:
CompositionAPI被如果只是字面的意思可以被翻译成组合API。
他以前的名字是Vue Function-based API ,我认为现在的名字更为达意。
本质上CompositionAPI就是为了更为方便的实现逻辑的组合而生的。
回顾历史
Vue2如果要在组件中实现逻辑的符合,譬如所有按钮组件都要实现防抖,可选的方式大概有以下三个:
Mixins
高阶组件 (Higher-order Components, aka HOCs)
Renderless Components (基于 scoped slots / 作用域插槽封装逻辑的组件但三者都不是非常的理想。
主要问题存在:
模板数据来源不清晰, 譬如mixin光看模板很难分清一个属性是哪里来的。
命名空间冲突:
性能问题。譬如HOC需要额外的组件逻辑嵌套 会导致无谓的性能开销。
但是我更在意的是对于逻辑的组合这种原始的编程行为我不得不引入其他概念来处理。
当然这个也是为什么很多从java转过来的架构师更喜欢react的原因。
vue算是笑着进去哭着出来的语言。入门好像很容易看看helloworld就可以工作了,但一遇到问题就需要引入一个新概念。
不像React函数即组件那么清爽自然。
动机:
首先先看一下
const App = { template: ` <button @click='click'>{{message}}</button> `, data() { return { message: 'Hello Vue 3!!' } }, methods: { click() { console.log('click ....', this.message) this.message = this.message.split('').reverse().join('') } } } let vm = Vue.createApp().mount(App, '#app')
options api源代码位置:
https://github.com/su37josephxia/vue3-study/blob/master/demo/api/options.html
经典的Vue API可以归纳为options API,可以理解为基于配置的方式声明逻辑的API。
啥意思基本是已定义为基础的。
想一想vue2的helloworld是不是好像只是完成了几个简单的定义就可以实现功能。
我认为这个也是为什么vue那么流行的原因 对于描述一般的逻辑的确非常简单。
当然这也和尤大神是从设计师出身有很大的关系。别让了css和html语言是彻头彻尾的定义性代码。
但是一旦业务逻辑复杂的话这种表达方式就会存在一定问题。
因为逻辑一旦复杂你不能给他写成一坨,必须要考虑如何组织,你要考虑抽取逻辑中的共用部分考虑复用问题,不然程序将变成非常难以维护。
上文中已经提到了哪三种复用方式一方面来讲需要因为全新的概念另外确实编程体验太差还有性能问题。
CompositionAPI的灵感来源于React Hooks的启发(这个是尤大承认的)。
好的东西需要借鉴这个大家不要鄙视链。
使用函数组合API可以将关联API抽取到一个组合函数 该函数封装相关联的逻辑,并将需要暴露给组件的状态以响应式数据源的形式返回。
实战:
好了上代码,第一段逻辑是尤大的逻辑鼠标位置监听逻辑
// 尤大神的经典例子 鼠标位置侦听逻辑 function useMouse() { const state = reactive({ x: 0, y: 0 }) const update = e => { state.x = e.pageX state.y = e.pageY } onMounted(() => { window.addEventListener('mousemove', update) }) onUnmounted(() => { window.removeEventListener('mousemove', update) }) return toRefs(state) }
我们还想组合另外一段逻辑 比如随时刷新的时间逻辑:
function useOtherLogic() { const state = reactive({ time: '' }) onMounted(() => { setInterval(() => { state.time = new Date() }, 1000) }) return toRefs(state) }
在实际的工作中我们可以认为这两个逻辑可能被很多组件复用。
想想你要是用mixin和hoc你将多么无所是从。
但是利用CompositionAPI,我们只需要把他组合并暴露 like this
const MyComponent = { template: `<div>x:{{ x }} y:{{ y }} z:{{ time }} </div>`,
setup() { const { x, y } = useMouse() // 与其它函数配合使用 const { time } = useOtherLogic()
return { x, y, time } } } createApp().mount(MyComponent, '#app')
呃呃 这个好像就是react hooks 没关系啦好用就可以啦。。。
基础API解析
我们先看看Vue3的基础API都有哪些?
const { createApp, reactive, // 创建响应式数据对象 ref, // 创建一个响应式的数据对象 toRefs, // 将响应式数据对象转换为单一响应式对象 isRef, // 判断某值是否是引用类型 computed, // 创建计算属性 watch, // 创建watch监听 // 生命周期钩子 onMounted, onUpdated, onUnmounted, } = Vue
setup使用composition API的入口
setup函数会在 beforeCreate之后 created之前执行
setup(props,context){ console.log('setup....',) console.log('props',props) // 组件参数 console.log('context',context) // 上下文对象}
好的其实大家可以和自己原来的React偶不Vue2代码对号入座。
reactive
reactive() 函数接受一个普通对象 返回一个响应式数据对象
const state = reactive({ count: 0, plusOne: computed(() => state.count 1) })
ref 与 isRef
●ref 将给定的值(确切的说是基本数据类型 ini 或 string)创建一个响应式的数据对象
●isRef 其实就是判断一下是不是ref生成的响应式数据对象
首先这里面有一个重要的概念叫包装对象(value wrapper)。
谈到wrapper从java那边转过来的小朋友肯定记得java里面的wrapclass其实概念差不多啦。
我们知道基本数据类型只有值没有引用。
这样也就造成了一个问题返回一个基础数据类型比如一个字符串是无法跟踪他的状态的。
所以我们就需要讲基础数据类型包装一下,这有点像ReactHooks中的useRef。
但是Vue包装的对象本身就是响应式数据源。好了我们看一下实例理解一下:
// 定义创建响应式数据 const time = ref(new Date()) // 设置定时器为了测试数据响应 setInterval(() => time.value = new Date(), 1000)
// 判断某值是否是响应式类型 console.log('time is ref:', isRef(time)) console.log('time', time) console.log('time.value', time.value) // 我们看看模板里面我们这样展示 template: ` <div> <div>Date is {{ time }}</div> </div> `
toRefs
toRefs 可以将reactive创建出的对象展开为基础类型
// 如果不用toRefs const state = reactive({ count: 0, plusOne: computed(() => state.count 1) }) return { state } // 模板渲染要这样写 template: ` <div> <div>count is {{ state.count }} </div> <div>plusOne is {{ state.plusOne }}</div> </div> ` // 我们再看看用了toRefs const state = reactive({ count: 0, plusOne: computed(() => state.count 1) }) return { ...toRefs(state) } // 模板渲染要这样写 template: ` <div> <div>count is {{ count }} </div> <div>plusOne is {{ plusOne }}</div> </div> `
watch 定义监听器:
这个其实没有什么新东西
watch(() => state.count * 2, val => { console.log(`count * 2 is ${val}`) })
effect 副作用函数:
响应式对象修改会触发这个函数
// 副作用函数 effect(() => { console.log('数值被修改了..',state.count) })
computed 计算属性:
const state = reactive({ count: 0, plusOne: computed(() => state.count 1)})
生命周期钩子Hooks:
想看完整代码
通过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这个测试:
单独运行一个测试:
比如我们看看vue的index这个测试
有两种方法进行单独测试
// 全局安装npm i jest -gjest 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
好了后面我们就可以开始向源码进军了~
vue 3 的代码结构
源码位置是在package文件件内,实际上源码主要分为两部分,编译器和运行时环境。
●编译器
○v-html
○v-text
○v-model
○v-clock
●基本类型解析
○AST
○compiler-core 核心编译逻辑
○compiler-dom 针对浏览器的编译逻辑
●运行时环境
▶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 effectivefunction 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.prototypeconst 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测试。
大家可以参考一下 :
https://github.com/su37josephxia/vue3-study/tree/master/demo/reactivity-demo
// clone代码
yarn
npx jest reactivity-demo