【JS】如何实现一个极简版Vue (初始化)

8 篇文章 1 订阅

Vue.js 是通过数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。

第一步 - 实现一个指令解析器(Compile)

初始化页面模版,包括基本的程序入口和DOM结构

<!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">
    <h1>{{ msg }}</h1>
  </div>
  <script type="module">
    import Vue from './vue.js'

    new Vue({
      el: '#app',
      data: {
        msg: 'hello world'
      }
    })
  </script>
</body>
</html>

处理组件配置项,将组件配置对象上的一些深层次属性放到 vm.$options 选项中,而在 Vue 源码中,初始化根组件时会进行选项合并操作,将全局配置合并到根组件的局部配置上

// vue.js
export default class Vue {
  constructor (options) {
    this.el = options.el
    this.$data = options.data
    this.$options = options;
		
    new Compile(options.el, this) // 指令解析器
  }
}

Compile 主要做的事情是解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图
基本思路: 根据el创建文档片段,nodeType分别处理元素和文本节点,然后将解析后的文档片段附加到DOM树

// compile.js
import compileUtils from './compileUtils.js'

export class Compile {
  constructor (el, vm) {
    this.el = document.querySelector(el)
    this.vm = vm

    const fragment = this.node2Fragment(this.el)

    this.compile(fragment)
    this.el.appendChild(fragment)
  }

  node2Fragment (el) {
    const fragment = document.createDocumentFragment()
    
    let firstChild
    while (firstChild = el.firstChild) {
      fragment.appendChild(firstChild)
    }

    return fragment
  }

  compile (fragment) {
    const childNodes = [...fragment.childNodes]

    childNodes.forEach((child) => {
      if (this.isElementNode(child)) {
        // this.compileElement(child)
      } else {
        this.compileText(child)
      }
			// 递归遍历子节点对象
      if (child.childNodes && child.childNodes.length) {
        this.compile(child)
      }
    })
  }

  compileText (node) {
    const content = node.textContent
		
    // 匹配 {{}} 模版字符串
    if (/\{\{(.+?)\}\}/.test(content)) {
      compileUtils.text(node, content, this.vm)
    }
  }

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

工具类根据指令执行对应方法,集中处理DOM的CRUD操作

// compileUtils.js
const compileUtils = {
  /*
  * node 当前元素节点
  * expr 当前指令的value
  * vm 当前Vue实例, 
  * eventName 当前指令事件名称
  */
  text (node, expr, vm) {
    let value;

    if (expr.indexOf('{{') !== -1) {
      value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
        return this.getValue(args[1], vm)
      })

      this.upDater.textUpDater(node, value)
    }
  },
  getValue(expr, vm) {
    return expr.split('.').reduce((data, currentVal) => {
      return data[currentVal]
    }, vm.$data)
  },
  upDater: {
    textUpDater(node, value) {
      node.textContent = value
    }
  }
}

export default compileUtils

{{msg}}已经能出识别出,接下来就可以处理元素节点,例如与之等价的v-text="msg"
基本思路: 获取 node 节点的属性集合,识别自定义指令方法,删除指令属性

export class Compile {
  ...
  compileElement (node) {
    const attrs = [...node.attributes]

    attrs.forEach((attr) => {
      const { name, value } = attr

      if (this.isDirective(name)) {
        const directive = name.split('-')[1]
        // 处理 v-bind:属性 v-on:事件再次分割
        const [dirName, eventName] = directive.split(':')
  
        compileUtils[dirName](node, value, this.vm, eventName)
        node.removeAttribute('v-' + directive)
      }
    })
  }
  isDirective(attrName) {
    return attrName.startsWith('v-')
  }
}

补充 v-text 判断逻辑,并初始化赋值,v-html 实现逻辑同理

const compileUtils = {
  text (node, expr, vm) {
    let value;
    if (expr.indexOf('{{') !== -1) {
      ...
    } else {
      value = this.getValue(expr, vm)
    }
      
    this.upDater.textUpDater(node, value)
  }
}

数据响应式,处理自定义方法,并与 $data 对象进行关联

// <button v-on:click="btnClick">Click</button>
new Vue({
  ...
  methods: {
    btnClick() {
      console.log(this.$data.msg)
    }
  }
})

工具类新增 on 方法,负责DOM元素的事件绑定

const compileUtils = {
  ...
  on (node, expr, vm, name) {
    const handle = vm.$options.methods[expr]

    node.addEventListener(name, handle.bind(vm), false)
  },
} 

代理数据对象, this.msg 映射为 this.$data.msg,通过defineProperty数据劫持实现

export default class Vue {
  constructor (options) {
    ...
    this.proxyData(this.$data)
  }
  
  proxyData (data) {
    for (const key in data) {
      Object.defineProperty(this, key, {
        get() {
          return data[key]
        },
        set(val) {
          data[key] = val
        }
      }}
    }
  }
}

处理 v-on:click@click 的语法糖,有兴趣的小伙伴可以自己实现原生属性的识别和绑定~

export class Compile {
  ...
  compileElement (node) {
    if (this.isDirective(name)) {
      ...
    } else if (this.isEventName(name)) {
      const directive = name.split('@')[1]

      compileUtils['on'](node, value, this.vm, directive)
      node.removeAttribute(name)
    }
  }
  isEventName (attrName) {
    return attrName.startsWith('@')
  }
}

至此,我们已经基本实现了指令解析器的基础功能,包括指令与数据的绑定和事件处理,下一篇会讲解如何通过发布者-订阅者、数据劫持实现双向数据绑定

参考资料

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值