手写一个简版的vue

138 阅读3分钟

vue的简单使用:

  1. new Vue(options)options中配置需要的datacomputedmethods,生命周期钩子等
  2. 通过胡子语法绑定变量,通过v-bind(@)绑定methods中事件等
<!-- Vue的简单使用 详情请参考vue官网-->
<div id="app">
  <div class="user">姓名:{{user.name}}</div>
  <input type="text" v-model="value.a">
  <button @click="add">点我加1</button>
</div>
<script>
  var vm = new Vue({
    el: '#app',
    data: {
      user: {
        name: '张三'
      },
      value: { a: 23 }
    },
    methods:{
      add(){
        this.value.a++
      }
    }
  })
</script>

本次手写实现的功能:

1. 模板编译与渲染

  1. 主要采用document.createDocumentFragment()方法创建文档碎片节点,生成虚拟dom,编译模板。
  2. 本次未使用抽象语法树方法编译模板,想了解这部分可以参考:vue源码--手写实现AST抽象语法树
  3. 未封装h函数生成虚拟dom,未封装patch进行diff与渲染dom,想了解可参考:虚拟dom与diff算法
  4. 只通过正则表达式解析胡子语法,想了解mustache模板引擎实现可参考:手写vue的胡子语法

2. 数据响应

  1. 编译模板、watch监听时,涉及到使用data数据的地方都需要收集依赖,监听数据变化
  2. 注意:本次手写vue复用了之前写过的数据响应式代码。详情请参考:vue数据响应式原理

3. 指令编译

  1. v-model为例
  2. 编译时获取节点的所有属性,如果是v-model="data",则让当前节点node.value=data,并监听其input事件,触发input时,改变this.data的值

4. 事件监听

  1. @click为例
  2. 编译时获取节点的属性,如果是@click,则让当前node监听这个方法,并执行其回调

5. 生命周期

  1. created为例
  2. 在初始化数据完成后,调用created的回调函数
  3. 生命周期具体执行时间,可以参考Vue官网

实现代码

Vue.js

  • Vue构造函数,初始化vue实例,监听watch中的属性值,执行生命周期钩子等
  • 代码中用到的observe函数与Watcher构造函数,请参考:vue数据响应式原理 中的完整代码部分
import Compile from "./Compile"  // Compile构造函数
// 引入数据响应式的代码,可参考之前数据响应式原理的文章
import { observe, Watcher } from "./initData"
// vue构造函数
export default class Vue {
  constructor(options) {
    this.$options = options // 存储传入的options
    this._data = options.data  // 存储data
    // 数据响应式
    observe(this._data)
    // 初始数据,把data的属性绑定到this,通过this.name可访问data的name属性
    this._initData(this._data)
    // 将methods的属性绑定到this,通过this.add()可执行add方法
    this._initData(options.methods)
    // 执行creatd的回调,其他生命周期可参考vue生命周期在对应的地方执行
    this.$options.created.call(this)
    // 处理watch中监听的数据 
    this._initWatch(options.watch)
    // 实例Compile,编译模板,传入参数:1.挂载点,2.vue实例
    new Compile(options.el, this)
  }
  _initData(data) {
    let self = this
    // 将data中的属性绑定到vue实例上
    Object.keys(data).forEach(key => {
      Object.defineProperty(self, key, {
        get() {
          return data[key]
        },
        set(val) {
          data[key] = val
        }
      })
    })
  }
  _initWatch(watch) {
    let self = this
    Object.keys(watch).forEach(key => {
      // 实例Watcher,收集依赖,原理参考之前数据响应式原理的文章
      // 参数1.vue实例,2.监听的属性值,3.回调函数
      new Watcher(self, key, watch[key])
    })
  }
}

Compile.js

  • Compile构造函数,用于编译模板,包括解析:v-moel@click{{name}}
  • 代码中用到的parsePath函数与Watcher构造函数,请参考:vue数据响应式原理 中的完整代码部分
// 引入数据响应式的代码,可参考之前数据响应式原理的文章
// parsePath是通过表达式获取对象的值,比如获取obj[a.c.g]的值
import { parsePath, Watcher } from "./initData"
// Compile构造函数
export default class Compile {
  constructor(el, vue) {
    this.$el = document.querySelector(el) // 获取挂载点的真实DOM
    this.$vue = vue      // 存储Vue实例
    if (this.$el) {
      // 将真实DOM转换为虚拟DOM
      let $fragment = this.node2Fragment(this.$el)
      // 编译解析模板,包括解析胡子语法,指令等
      this.compile($fragment)
      // 渲染DOM
      this.$el.appendChild($fragment)
    }
  }
  node2Fragment(el) {
    let fragment = document.createDocumentFragment();  // 创建文本碎片
    let ch
    // 循环遍历真实dom,并添加到文本碎片中
    // 这里每一次appendChild,真实dom中就会少一个节点
    while (ch = el.firstChild) {
      fragment.appendChild(ch)
    }
    return fragment
  }
  compile(el) {
    let txtReg = /{{(.*?)}}/   // 匹配胡子语法的正则
    el.childNodes.forEach(ch => {
      if (ch.nodeType == 1) {
        // 如果是element节点,调用编译element的方法
        this.compileElement(ch)
      } else if (ch.nodeType == 3 && txtReg.test(ch.textContent)) {
        // 如果是文本节点,且文本内容中使用了胡子语法
        let word = ch.textContent.match(txtReg)[1]    // 获取胡子中的值
        // 编译文本节点,参数:1.当前node,2.胡子中的值,3.正则表达式
        this.compileText(ch, word, txtReg)
      }
    })
  }
  compileElement(node) {
    // 编译属性,Array.from将类数组对象转换为数组
    Array.from(node.attributes).forEach(attr => {
      if (attr.name.indexOf('v-') == 0) {
        // 编译指令属性
        let directive = attr.name.slice(2)
        let exp = attr.value
        // 编译v-model
        if (directive == 'model') {
          let data = parsePath(exp)(this.$vue)  // 获取v-model绑定的数据值
          node.value = data   // 给输入框赋值
          // 监听v-model绑定的值的变化,改变时让输入框值也改变
          new Watcher(this.$vue, exp, newVal => {
            node.value = newVal
          })
          // 监听的input事件
          node.addEventListener('input', e => {
            let newVal = e.target.value
            this.setValue(this.$vue, exp, newVal) // 改变vue实例中对应的属性值
          })
        }
      }
      // 事件监听
      if (attr.name.indexOf('@') == 0) {
        let event = attr.name.slice(1)  // 获取事件名
        let exp = attr.value            // 获取methods中的属性值
        // 给当前node添加事件监听,传入vue实例中的方法,绑定this为vue实例
        node.addEventListener(event, this.$vue[exp].bind(this.$vue))
      }
    })
    // 递归,继续编译子节点的子节点
    this.compile(node)
  }
  // 编译文本节点
  compileText(node, word, txtReg) {
    let oldText = node.textContent    // 获取文本节点的完整字符串
    let value = parsePath(word)(this.$vue)   // 获取胡子中变量的对应值
    node.textContent = oldText.replace(txtReg, value)    // 替换文本节点中的变量
    // 监听变量的变化,重新改变文本内容
    new Watcher(this.$vue, word, val => {
      node.textContent = oldText.replace(txtReg, val)
    })
  }
  // 给obj对象的exp表达式的属性设置新值,给obj[a.b.c]设置值
  setValue(obj, exp, newVal) {
    let arr = exp.split('.')
    let res = obj
    arr.forEach((item, i) => {
      if (i == arr.length - 1) {
        res[item] = newVal
      } else {
        res = res[item]
      }
    })
  }
}

index.html 测试代码

<div id="app">
  <div class="user">
    <ul>
      <li>姓名:{{user.name}}</li>
      <li>年龄:{{user.age}}</li>
      <li>性别:{{user.gender}}</li>
    </ul>
  </div>
  <input type="text" v-model="value.a">
  <div>内容:{{value.a}}</div>
  <button @click="add">点我加1</button>
</div>
<!-- index.js中把Vue构造函数挂到了window上 -->
<script src="index.js"></script> 
<script>
  var vm = new Vue({
    el: '#app',
    data: {
      user: {
        name: '张三',
        age: 18,
        gender: '男',
      },
      value: {
        a: 11
      }
    },
    watch: {
      'user.name'(newVal, oldVal) {
        console.log(`watch监听:user的name发生改变了,新值${newVal},旧值${oldVal}`)
      }
    },
    created() {
      console.log('created:this是',this)
    },
    methods:{
      add(){
        this.value.a++
      }
    }
  })
</script>

效果: