vue的使用:
- new Vue(options),options中配置需要的data,computed,methods,生命周期钩子等
通过胡子语法绑定变量,通过v-bind(@)绑定methods中事件等
<!-- 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. 模板编译与渲染
主要采用document.createDocumentFragment()方法创建文档碎片节点,生成虚拟dom,编译模板。
- 本次未使用抽象语法树方法编译模板,想了解这部分可以参考:vue源码—手写实现AST抽象语法树
- 未封装h函数生成虚拟dom,未封装patch进行diff与渲染dom,想了解可参考:虚拟dom与diff算法
只通过正则表达式解析胡子语法,想了解mustache模板引擎实现可参考:手写vue的胡子语法
2. 数据响应
编译模板、watch监听时,涉及到使用data数据的地方都需要收集依赖
注意:本次手写vue复用了之前写过的数据响应式代码。详情请参考:vue数据响应式原理
3. 指令编译
以v-model为例
编译时获取节点的所有属性,如果是v-model=”data”,则让当前节点node.value=data,并监听其input事件,触发input时,改变this.data的值
4. 事件监听
以@click为例
编译时获取节点的属性,如果是@click,则让当前node监听这个方法,并执行其回调
5. 生命周期
以created为例
- 在初始化数据完成后,调用created的回调函数
- 生命周期具体执行时间,可以参考Vue官网
实现代码
Vue.js
- Vue构造函数,初始化vue实例,监听watch中的属性值,执行生命周期钩子等
代码中用到的observe函数与Watcher构造函数,请参考:vue数据响应式原理 中的完整代码部分
import Compile from "./Compile"// 引入数据响应式的代码,可参考之前数据响应式原理的文章import { observe, Watcher } from "./initData"// vue构造函数export default class Vue {constructor(options) {this.$options = options // 存储传入的optionsthis._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 = thisObject.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) // 获取挂载点的真实DOMthis.$vue = vue // 存储Vue实例if (this.$el) {// 将真实DOM转换为虚拟DOMlet $fragment = this.node2Fragment(this.$el)// 编译解析模板,包括解析胡子语法,指令等this.compile($fragment)// 渲染DOMthis.$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-modelif (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.valuethis.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 = objarr.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>
效果:

