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 // 存储传入的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>
效果: