为什么重构
本科机械设计制造及其自动化,16年稀里糊涂的进了一家干变厂,17年自学了大半年,18年正式跨行来到前端。工作中主要写业务代码,很少涉及造轮子工作,一直希望能够提高编程能力。恰好,公司业务栈以vue为主,理解它的逻辑,相信对今后肯定会有帮助。于是就有了使用ts重构vue的冲动。更甚者,希望能够参与到开源社区的建设,努力变得更好。
Vue的功能还是很复杂的,源码也涉及到跨平台部分,本次仅学习web方向的源码,期望通过重构引导阅读,加强体会。
重构计划
使用到的技术栈如下:
- TypeScript
- Jest
- es6
- rollup
使用TypeScript编写,使用Jest做单元测试,使用rollup进行构建。此次重构并不是完全的照(拷)搬(贝),将选取常用功能去实现。期望最佳的开发模式是:以问题(feature)引领,去阅读源码,理解后通过自己的方式去实现。以虚拟DOM为例,重构过程可能至少分3步实现,1、创建虚拟DOM,2、虚拟DOM映射为真实DOM,3、给真实DOM设置其他属性(style、event等)。
Vue分为运行版和完整版,完整版本包含compile模块,对如<div id="app">{{message}}</div>
的模版语法进行了编译。为了简化理解逻辑,笔者重构时直接将compile模块忽略了,全部通过渲染函数render
进行编写。
必备基础知识
- 虚拟DOM
谈到三大框架,必定要了解虚拟DOM。通过访问vm._vnode
可以查看vue虚拟DOM的结构,借助children属性实现了DOM的树形结构。
虚拟DOM是什么? 怎么定义虚拟DOM? 虚拟DOM有什么好处?
推荐阅读snabbdom开源库,snabbdom的核心非常精简,总共318行,最关键一点,Vue的虚拟DOM是参考它改进的。
- Proxy和Reflect使用
谈到Vue的特性,必定要了解数据驱动和响应式。通过访问app._data
可以查看处理后的data结构。
Vue源码使用Object.defineProperty,笔者将使用Proxy进行属性拦截,如下述代码,实例化后通过p.name
可以访问到this.data={name: 'xiaoming'}
的属性。
class P {
constructor () {
this.data = {name: 'xiaoming'}
return new Proxy(this, {
get (target, key) {
return Reflect.get(target.data, key)
}
})
}
}
let p = new P()
console.log(p.name)
- 事件循环(nextTick)
观察下面demo,在定时器中修改name
和message
属性,可以发现视图更新了。此时,runCount的值是多少?如果我们同步修改了更多的属性,会影响runCount的值吗?
let runCount = 0
let vm = new Vue({
el: '#app',
data: {
name: 'xiaoming',
message: 'Hello Vue!'
},
render (h) {
runCount += 1
return h('h1', this.name + this.message)
}
})
setTimeout(() => {
vm.name = 'xiaohong'
vm.message = 'Hello world'
}, 1000)
Vue源码对渲染过程进行了优化,其每次更新都是异步的。此外,你是否在业务中使用到$nextTick
,为什么会用到它?了解Js的事件循环,有助于理解Vue的更新原理。
定下小目标
- 基本使用
观察下面demo,用户首先看到Hello Vue!
,1秒钟之后观看到UI变化Hello world
。
vue是如何进行渲染的?当修改message
值,又是如何进行更新的?
let vm = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
},
render (h) {
return h('h1', this.message)
}
})
setTimeout(() => {
vm.message = 'Hello world'
}, 1000)
- 事件绑定
观察下面demo,当用户点击按钮时,点击次数加1,同时控制台输出customClick click
。
Vue是如何绑定事件的?自定义事件和原生事件的处理方式有何不同?
有时候在项目中可能会使用到eventBus
,这又是如何实现的?
Vue.component('button-count', {
data () {
return {
count: 0
}
},
render (h) {
const self = this
return h('button', {
on: {
click () {
self.count += 1
self.$emit('customClick', self.count)
}
}
}, `点击次数:${this.count}`)
}
})
let vm = new Vue({
el: '#app',
render (h) {
return h('button-count', {
nativeOn: {
click () {
console.log('click')
}
},
on: {
customClick () {
console.log('customClick')
}
}
})
}
})
- 组件
我们定义了button-count
组件,当用户点击时,组件自动记录点击次数并更新视图。假设用户点击了2次,此时的runButtonCount
和runCount
的值分别是多少?为什么是这样的?
Vue不仅支持自定义组件,也内置了transition
/keep-alive
等组件,Vue是如何实现组件功能的?父子组件如何进行消息传递?
let runButtonCount = 0
let runCount = 0
Vue.component('button-count', {
data () {
return {
count: 0
}
},
render (h) {
runButtonCount += 1
const self = this
return h('button', {
on: {
click () {
self.count += 1
}
}
}, `点击次数:${this.count}`)
}
})
let vm = new Vue({
el: '#app',
render (h) {
runCount += 1
return h('button-count')
}
})
- 指令
观察下面demo,用户首先看不到任何文字,1秒钟之后观看到111&222&333
。
Vue不仅支持内置指令,也允许用户自定义指令。指令代码是如何控制UI的?
ps:新的项目使用vue进行开发,以iframe形式被嵌入在老页面中。iframe技术还是非常有效,但我们发现新项目中的弹窗无法全屏展示(半透明mask无法全屏),后来借助指令解决了问题。推荐开源库https://github.com/calebroseland/vue-dom-portal。
let vm = new Vue({
el: '#app',
data () {
return {
news: [111, 222]
}
},
render (h) {
const self = this
return h('h1', {
directives: [
{
name: 'show',
value: self.news.length > 2,
expression: 'news.length > 2',
arg: '',
modifiers: { }
}
]
}, self.news.join('&'))
}
})
setTimeout(() => {
vm.news.push(333)
}, 1000)
- 插槽
观察下面demo,页面最终输出什么内容?插槽功能是如何实现的?
Vue.component('app-layout', {
render (h) {
const self = this
return h('div', [
h('header', [self._t('header')]),
h('main', [self._t('default', [h('', '默认内容')])]),
h('footer', [self._t('footer')])
])
}
})
let v = new Vue({
el: '#app',
data () {
return {
title: 'hello world!',
msg: 'msg',
desc: 'desc'
}
},
render (h) {
return h('div', [
h('app-layout', [
h('h1', {attrs: {slot: 'header'}, slot: 'header'}, this.title),
h('p', this.msg),
h('p', {attrs: {slot: 'footer'}, slot: 'footer'}, this.desc)
])
])
}
})
页面渲染后,最终的demo结构如下:
<div>
<header>
<h1>hello world!</h1>
</header>
<main>
<p>msg</p>
</main>
<footer>
<p>desc</p>
</footer>
</div>
- 路由(拓展)
待完善
总结
前面我们定下了很多小目标,接下来就一样样去实现。我们的目标很简单,就是demo能够按照要求运行。
vue源码很复杂,有跨平台代码(web、weex、server),有性能监控代码。看源码时切记不要完美主义,没必要必须理解所有的代码。通过问题主线去阅读,去了解vue实现的原理。
杠精一下
为什么使用ts?
为什么使用rollup?
推荐阅读
笔者重构Vue的储备均来黄老师的两套课程,再次着重推荐。文章有些demo来自黄老师,不知道是否侵权(如有侵权必定删除)。
ts学习推荐:coding.imooc.com/class/chapt…
vue源码学习:ustbhuangyi.github.io/vue-analysi…