Skip to content

Vue源码你读过吗?

Vue的响应式原理是什么?

Vue是怎么收集依赖的?

Vue到底是怎么把数据渲染到页面上去的?

这些问题网上有很多答案,并且源码第几行都有人写出来,但是我发现我还是无法直接看懂源码,感觉自己看懂了,但又似懂非懂。

这种状态直到我开始着手自己手写mini-vue,才慢慢悟了。

废话不多说,直接进入正题。如果你对vue源码还处于一知半解,可以顺着我的思路,让我们一点点的揭开Vue的面纱。

mini-vue 仓库源码

更多优质内容,看我个人空间

每个分支完成一个小功能,根据自身情况从不同的分支看起。

正文开始

V0.0.1 响应式原理入门

先简单实现一个小功能,一个数字,一个+1按钮,点击按钮数字+1

要求:

  • 要使用Object.defineProperty完成DOM更新

很简单吧,几分钟就能搞定,快动手试试。

js
// 初始化app
const app = document.createElement('div')
const h2 = document.createElement('h2')
const button = document.createElement('button')

const data = {
    count: 0
}

const plus = function(){
    this.count++
}
h2.innerText = data.count
button.innerText = '+1'
button.addEventListener('click', plus.bind(data))

let val = data.count
Object.defineProperty(data, 'count', {
    get: function(){
        return val
    },
    set: function(newVal){
        val = newVal
        h2.innerText = val
    }
})

app.append(h2)
app.append(button)
document.body.append(app)

然后再增加点难度,按照Vue的写法来实现,也就是使用new Vue() 创建一个vue对象

为了方便后续编码,我们这里先简单规划下项目的一个结构

  1. 新建一个文件夹mini-vue, 或者你自己随便起名,都无所谓的
  2. 在mini-vue文件夹下,执行
bash
npm init -y
  1. 安装一下http-server, 并修改下package.json 中的执行脚本
bash
npm i http-server
json
// package.json
... 前后我就省略啦,只改这部分就行
"scripts": {
    "dev": "http-server -p 13003"
},
...
  1. 新建一个index.html文件
html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>nkxrb mini-vue</title>
  <style>
    #app {
      text-align: center;
      padding-top: 10%;
    }

    button {
      cursor: pointer;
    }
  </style>
</head>

<body>
  <div id="app"></div>
  <script type="module" src="/example/index.js"></script>
</body>

</html>
  1. 新建一个example文件夹,里面再新增一个index.js文件
js
/**
 * 遵循Vue2.0的写法,new 一个Vue对象,并挂载到#app节点上
 */
import Vue, { App } from '../src/vue.js'
new Vue(App).$mount('#app')
  1. 新建一个src文件夹,里面新增一个vue.js文件
js
const h2 = document.createElement('h2')
const button = document.createElement('button')

export const App = {
  data: function () {
    return {
      count: 0
    }
  },
  methods: {
    plus () {
      this.count++
    }
  },
  render () {
    // 简易的渲染函数,直接操作真实dom
    // 下列代码将生成一个
    /**
     * <div>
     *  <h2>0</h2>
     *  <button>+1</button>
     * </div>
     */
    const app = document.createElement('div')
    // 将count值渲染到h2标签中
    h2.innerText = this.count
    button.innerText = '+1'
    // 给button绑定点击事件,并利用bind将事件的this指向调用render函数的this对象
    button.addEventListener('click', this.plus.bind(this))
    app.append(h2)
    app.append(button)
    return app
  }
}


/**
 * 使用函数式写法,声明一个Vue类
 * @param app 此处传入的app, 为了简单,已经转换成了一个对象,详情将/example/app.js
 * @returns 返回值需要包含一个mount函数,用于将节点挂载到页面上去
 */
function Vue (app) {
  // 因为vue中data是函数,此处需要执行data()来获取app中声明的对象
  const data = app.data()

  // 将data转换为响应式的
  abserver(data)

  // 将data,methods合并,并一起放入vm中,注意此处的vm===data, 这样可以保证响应式生效
  const vm = Object.assign(data, app.methods)

  // 执行app的render函数,得到最终渲染的dom对象
  const root = app.render ? app.render.call(vm) : ''

  const $mount = selector => {
    // 找到#app元素
    const page = document.querySelector(selector) || document.body
    // 先清空里面的内容
    page.innerHTML = ''
    // 将上面app生成的dom对象,放入page中
    page.append(root)
  }
  // new Vue对象,返回一个$mount函数,用来将生成的元素挂载到 #app DOM上
  return {
    $mount
  }
}

/**
 * 为对象的每个属性添加响应
 * @param {*} data 
 */
function abserver (data) {
  for (let key in data) {
    // 在外边定义一个值用来存储值,此处可看作是一个闭包应用
    let val = data[key]
    Object.defineProperty(data, key, {
      enumerable: true,
      configurable: true,
      get: function () {
        // 此处应该是用来依赖收集的,等到v0.0.2再详细设计
        return val
      },
      set: function (newVal) {
        val = newVal
        // 此处应该是用来更新所有依赖的,等到v0.0.2再详细设计
        h2.innerText = newVal
      }
    })
  }
}
export default Vue

如果你已经按照上面做完了,就可以启动服务看效果了

bash
npm run dev

打开页面 http://localhost:13003

主要逻辑都在vue.js中,代码注释已经很详细了,写的都比较简单,如果你都看懂了。甚至自己都能很快写出来一样的了,那就开始往下学: v0.0.2 模块化与响应式的依赖收集

如果觉得不错,欢迎点赞关注收藏,mini-vue系列文章持续更新中...