【Ts重构Vue】00-Ts重构Vue前言

3,758 阅读5分钟

为什么重构

本科机械设计制造及其自动化,16年稀里糊涂的进了一家干变厂,17年自学了大半年,18年正式跨行来到前端。工作中主要写业务代码,很少涉及造轮子工作,一直希望能够提高编程能力。恰好,公司业务栈以vue为主,理解它的逻辑,相信对今后肯定会有帮助。于是就有了使用ts重构vue的冲动。更甚者,希望能够参与到开源社区的建设,努力变得更好。

Vue的功能还是很复杂的,源码也涉及到跨平台部分,本次仅学习web方向的源码,期望通过重构引导阅读,加强体会。

重构计划

使用到的技术栈如下:

  1. TypeScript
  2. Jest
  3. es6
  4. 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进行编写。

必备基础知识

  1. 虚拟DOM

谈到三大框架,必定要了解虚拟DOM。通过访问vm._vnode可以查看vue虚拟DOM的结构,借助children属性实现了DOM的树形结构。

虚拟DOM是什么? 怎么定义虚拟DOM? 虚拟DOM有什么好处?

推荐阅读snabbdom开源库,snabbdom的核心非常精简,总共318行,最关键一点,Vue的虚拟DOM是参考它改进的。

  1. 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)
  1. 事件循环(nextTick)

观察下面demo,在定时器中修改namemessage属性,可以发现视图更新了。此时,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的更新原理。

定下小目标

  1. 基本使用

观察下面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)
  1. 事件绑定

观察下面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')
        }
      }
    })
  }
})
  1. 组件

我们定义了button-count组件,当用户点击时,组件自动记录点击次数并更新视图。假设用户点击了2次,此时的runButtonCountrunCount的值分别是多少?为什么是这样的?

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')
  }
})
  1. 指令

观察下面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)
  1. 插槽

观察下面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>
  1. 路由(拓展)

待完善

总结

前面我们定下了很多小目标,接下来就一样样去实现。我们的目标很简单,就是demo能够按照要求运行。

vue源码很复杂,有跨平台代码(web、weex、server),有性能监控代码。看源码时切记不要完美主义,没必要必须理解所有的代码。通过问题主线去阅读,去了解vue实现的原理。

杠精一下

为什么使用ts?

为什么使用rollup?

推荐阅读

笔者重构Vue的储备均来黄老师的两套课程,再次着重推荐。文章有些demo来自黄老师,不知道是否侵权(如有侵权必定删除)。

ts学习推荐:coding.imooc.com/class/chapt…

vue源码学习:ustbhuangyi.github.io/vue-analysi…

系列文章

【Ts重构Vue】00-Ts重构Vue前言

【Ts重构Vue】01-如何创建虚拟节点

【Ts重构Vue】02-数据如何驱动视图变化

【Ts重构Vue】03-如何给真实DOM设置样式

【Ts重构Vue】04-异步渲染

【Ts重构Vue】05-实现computed和watch功能