作者:aRuis | 来源:互联网 | 2023-08-22 15:02
Vue.js是通过数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,
Vue.js 是通过数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()
来劫持各个属性的setter
,getter
,在数据变动时发布消息给订阅者,触发相应的监听回调。
第一步 – 实现一个指令解析器(Compile)
初始化页面模版,包括基本的程序入口和DOM结构
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content=">
<title>Documenttitle>
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 对象进行关联
//
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('@')
}
}
至此,我们已经基本实现了指令解析器的基础功能,包括指令与数据的绑定和事件处理,下一篇会讲解如何通过发布者-订阅者、数据劫持实现双向数据绑定
参考资料
- Vue高级指南-01 Vue源码解析之手写Vue源码
- Vue 源码解读 —— Vue 初始化过程
- 实现最简 vue3 模型