实现简单的vue

1.先来看先要实现的需求,解析下面的文件,实现正确渲染和响应式

// vue-demo/index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">
    <h6>插值表达式</h6>
    <div>{{msg}}</div>
    <div>{{count}}</div>
    <h6>v-text</h6>
    <div v-text="msg"></div>
    <div v-text="count"></div>
    <h6>v-model</h6>
    <input v-model="msg"></input>
    <input v-model="count"></input>
  </div>
  <script>
    let vm = new Vue({
      el: '#app',
      data: { 
        msg: 'hello',
        count: 10
      }
    })
  </script>
</body>
</html>

当前访问,如图所示:

2.大概分为5部分内容

3.new Vue的实现:

主要是把data中的成员注入到Vue实例,并劫持data中的成员,设置getter/setter

// vue-demo/js/vue.js
class Vue{
  constructor (options){
    // 1.接收并保存初始化参数
    this.$options = options || {}
    this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
    // 2.把data中的属性注入到Vue实例,转换成setter/getter
    this.$data = options.data || {}
    this._proxyData(this.$data)
    // 3.调用Observer实现数据劫持
    // 4.调用Compiler解析指令和插值表达式
  }

  _proxyData(data) {
    Object.keys(data).forEach(key => {
      Object.defineProperty(this, key, {
        get() {
          return data[key]
        },
        set(newVal) {
          if(newVal === data[key]){
            return
          }
          data[key] = newVal
        }
      })
    })
  }
}

html中导入vue.js后,在访问控制vm,可以看到1和2已经实现:

4.Observer的实现:

对vm.$data数据对象的所有属性进行监听

// vue-demo/js/observer.js
// 将$data的成员转化为getter/setter
class Observer {
  // 初始的时候拿到$data
  constructor(data){
    this.walk(data)
  }

  walk(data) {
    if (!data || typeof data !== 'object') {
      return
    }
    Object.keys(data).forEach(key => {
      this.defineReactive(data, key, data[key])
    })
  }

  defineReactive(data, key, val) {
    let that = this
    Object.defineProperty(data, key, {
      get(){
        return val
      },
      set(newVal) {
        if (newVal === val) {
          return
        }
        // 如果有个属性的值从字符串改为对象,也需要设置为响应式
        // 需要注意this指向的问题,这里需要调用的是Observer的walk方法
        that.walk(newVal)
        data[key] = newVal
      }
    })
  }
}

html文件中引入observer.js,在vue.js的构造函数中新建Observer对象,并传入this.$data,然后在控制台访问vm,结果如下图所示:

5.模板解析

解析插值表达式和指令

// vue-demo/js/compiler.js
class Compiler{
  constructor(vm){
    this.vm = vm
    this.el = vm.$el
    this.compile(this.el)
  }

  compile(el) {
    const nodes = el.childNodes
    Array.from(nodes).forEach(node => {
      // 判断是文本节点还是元素节点
      if(this.isTextNode(node)){
        this.compileText(node)
      } else if (this.isElementNode(node)){
        this.compileElement(node)
      }
      // 如果还有子节点,递归调用compile方法
      if(node.childNodes && node.childNodes.length){
        this.compile(node)
      }
    })
  }

  isTextNode(node) {
    return node.nodeType === 3
  }

  isElementNode(node) {
    return node.nodeType === 1
  }

  // 判断是否是指定v-开头的属性
  isDirective(attrName) {
    return attrName.startsWith('v-')
  }

  // 处理插值表达式
  compileText(node) {
    const reg = /\{\{(.+?)\}\}/
    // 文本节点内容
    const value = node.textContent
    if(reg.test(value)) {
      // 属性名
      const key = RegExp.$1.trim()
      // 替换插值表达式为this.$data对应的值
      node.textContent = value.replace(reg, this.vm[key])
    }
  }

  // 处理指令
  compileElement(node) {
    Array.from(node.attributes).forEach(attr => {
      // 获取元素名称
      let attrName = attr.name
      // 判断当前元素属性名称是否是指令
      if(this.isDirective(attrName)) {
        // 截掉v-
        attrName = attrName.substr(2)
        // 获取指令对应的值v-text="msg",获取的就是msg
        const key = attr.value
        // 处理不同指令
        this.update(node, key, attrName)
      }
    })
  }

  update(node, key, dir) {
    const updateFn = this[dir + 'Updater']
    updateFn && updateFn(node, this.vm[key])
  }

  textUpdater(node, value) {
    node.textContent = value
  }

  modelUpdater(node, value) {
    node.value = value
  }
}

html中引入compiler.js,并在vue.js的构造函数中新建Compiler,访问可以看到,插值表达式和指令已经正确显示:

6.Dep实现

收集依赖,添加观察者watcher,数据变化后通知观察者更新

// vue-demo/js/dep.js
class Dep{
  constructor(){
    this.subs = []
  }

  addDep(dep){
    if(dep && dep.update) this.subs.push(dep)
  }

  notify() {
    this.subs.forEach(dep => {
      dep.update()
    })
  }
}


在observer.js中依赖收集,发送通知:

// vue-demo/js/observer.js
// defineReactive方法中
// 创建dep对象收集依赖
   const dep = new Dep()
// get过程中收集依赖
   Dep.target && dep.addDep(Dep.target)
// set中数据变化之后,发送通知
   dep.notify()

7.Watcher实现

// vue-demo/js/watcher.js
class Watcher {
  constructor(vm, key, cb) {
    this.vm = vm
    // data中的属性名称
    this.key = key
    // 数据变化时,调用cb更新视图
    this.cb = cb
    // 在Dep静态属性上记录当前的watcher对象,当访问数据时,将watcher添加到dep的subs中
    Dep.target = this
    // 这个访问可以触发getter,让dep为当前key记录watcher,且保存一下更新前的值
    this.oldValue = vm[key]
    // 清空target,避免重复添加
    Dep.target = null
  }

  update() {
    const newVal = this.vm[this.key]
    if(this.oldValue === newVal) {
      return
    }
    this.cb(newVal)
  }
}

8.在compiler.js为指令和插值表达式都创建watcher对象,也就是界面显示依赖$data的地方,监视数据变化

class Compiler{
  constructor(vm){
    this.vm = vm
    this.el = vm.$el
    this.compile(this.el)
  }

  compile(el) {
    const nodes = el.childNodes
    Array.from(nodes).forEach(node => {
      // 判断是文本节点还是元素节点
      if(this.isTextNode(node)){
        this.compileText(node)
      } else if (this.isElementNode(node)){
        this.compileElement(node)
      }
      // 如果还有子节点,递归调用compile方法
      if(node.childNodes && node.childNodes.length){
        this.compile(node)
      }
    })
  }

  isTextNode(node) {
    return node.nodeType === 3
  }

  isElementNode(node) {
    return node.nodeType === 1
  }

  // 判断是否是指定v-开头的属性
  isDirective(attrName) {
    return attrName.startsWith('v-')
  }

  // 处理插值表达式
  compileText(node) {
    const reg = /\{\{(.+?)\}\}/
    // 文本节点内容
    const value = node.textContent
    if(reg.test(value)) {
      // 属性名
      const key = RegExp.$1.trim()
      // 替换插值表达式为this.$data对应的值
      node.textContent = value.replace(reg, this.vm[key])
      new Watcher(this.vm, key, value => {
        node.textContent = value
      })
    }
  }

  // 处理指令
  compileElement(node) {
    Array.from(node.attributes).forEach(attr => {
      // 获取元素名称
      let attrName = attr.name
      // 判断当前元素属性名称是否是指令
      if(this.isDirective(attrName)) {
        // 截掉v-
        attrName = attrName.substr(2)
        // 获取指令对应的值v-text="msg",获取的就是msg
        const key = attr.value
        // 处理不同指令
        this.update(node, key, attrName)
      }
    })
  }

  update(node, key, dir) {
    const updateFn = this[dir + 'Updater']
    // 因为要使用this指向Compiler
    updateFn && updateFn.call(this, node, this.vm[key], key)
  }

  textUpdater(node, value, key) {
    node.textContent = value
    // 创建watcher,监视数据变化
    new Watcher(this.vm, key, value => {
      node.textContent = value
    })
  }

  modelUpdater(node, value, key) {
    node.value = value
    // 创建watcher,监视数据变化
    new Watcher(this.vm, key, value => {
      node.value = value
    })
  }
}

index.html中引入dep.js和watcher.js

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">
    <h6>插值表达式</h6>
    <div>{{msg}}</div>
    <div>{{count}}</div>
    <h6>v-text</h6>
    <div v-text="msg"></div>
    <div v-text="count"></div>
    <h6>v-model</h6>
    <input v-model="msg"></input>
    <input v-model="count"></input>
  </div>
  <script src="./js/dep.js"></script>
  <script src="./js/watcher.js"></script>
  <script src="./js/compiler.js"></script>
  <script src="./js/observer.js"></script>
  <script src="./js/vue.js"></script>
  <script>
    let vm = new Vue({
      el: '#app',
      data: { 
        msg: 'hello',
        count: 10
      }
    })
  </script>
</body>
</html>

运行后,在控制台修改vm.msg='xxx',效果如图所示:

差值表达式和指令关联的msg都更新了,说明响应式机制是ok的

9.实现双向绑定

效果就是改变msg, input的值会改变,这个之前已经实现了,现在只需要实现改变input的值,绑定msg的差值表达式和v-text指令都应该得到更新,只需要在compile.js的指令处理方法modelUpdater中,绑定input事件,改变vm[key]即可

效果如图所示:

到此,简单的vue.js就实现了。

源码见:[https://gitee.com/caicai521/vue-study/tree/master/vue-demo](

posted @ 2022-10-14 09:25  菜菜123521  阅读(180)  评论(0编辑  收藏  举报