data数据响应式、数据代理
<!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="root">
<div class="c1">
<div title="tt1" id="id">233</div>
<div title="tt2">{{ age }}</div>
<div title="tt3">{{ gender }}</div>
<ul>
<li>111{{name.firstName}}-{{age}}</li>
<li>{{name.lastName}}</li>
<li>3</li>
</ul>
</div>
</div>
<script>
/**
* #0.* 属于重要辅助函数
*/
// 如何得到虚拟dom
// 即:将真实的DOM元素转换成js对象
// 构建虚拟dom对象
class VNode {
constructor(tag, data, value, type) {
this.tag = tag && tag.toLowerCase() // 标签名
this.data = data // 属性
this.value = value // 标签内容
this.type = type // 节点类型
this.children = [] // 子节点
}
// 添加虚拟dom子节点
appendChild(vnode) {
this.children.push(vnode)
}
}
// TODO:#0.1 将真实dom转换成虚拟dom,这个函数当做 compiler 函数
function getVNode(node) {
const type = node.nodeType
let _vnode = null
if (type === 1) {
// 标签节点
const tag = node.nodeName // 标签名
const attrs = node.attributes // attributes 真实dom里存放标签属性 数组对象
const data = {} // 虚拟dom中 属性就只是对象
for (let i = 0; i < attrs.length; i++) {
data[attrs[i].nodeName] = attrs[i].nodeValue // 数组每一项
}
const value = undefined
_vnode = new VNode(tag, data, value, type)
const children = node.childNodes
// 子节点
for (let i = 0; i < children.length; i++) {
_vnode.appendChild(getVNode(children[i]))
}
} else if (type === 3) {
// 文本节点
_vnode = new VNode(undefined, undefined, node.nodeValue, type)
}
// 将每一个虚拟节点返回
return _vnode
}
// TODO:#0.2 将虚拟 DOM 转换成真正的 DOM
function parseVNode(vnode) {
let oDom = null
// 标签节点
if (vnode.type === 1) {
oDom = document.createElement(vnode.tag)
const data = vnode.data
// 属性节点
Object.keys(data).forEach((key) => {
let attrName = key
let attrValue = data[key]
oDom.setAttribute(attrName, attrValue)
})
// 子节点
for (let i = 0; i < vnode.children.length; i++) {
oDom.appendChild(parseVNode(vnode.children[i]))
}
} else if (vnode.type === 3) {
// 创建文本节点
oDom = document.createTextNode(vnode.value)
}
return oDom
}
// 对象解析函数 a.b.c.d这种格式
function getValueByKey(obj, str) {
const arr = str.split('.')
let prop = null
while ((prop = arr.shift())) {
obj = obj[prop]
}
return obj
}
const reg = /\{\{(.+?)\}\}/g
// TODO:#0.3 将带有坑的 Vnode 与数据 data 结合, 得到填充数据的 VNode: 模拟 AST -> VNode
function combine(vnode, data) {
let _type = vnode.type
let _data = vnode.data
let _value = vnode.value
let _tag = vnode.tag
let _children = vnode.children
let _vnode = null
if (_type === 3) {
// 文本节点
// 对文本处理
_value = _value.replace(reg, function (_, g) {
return getValueByKey(data, g.trim())
})
// 重新创建虚拟dom对象
_vnode = new VNode(_tag, _data, _value, _type)
} else if (_type === 1) {
// 元素节点
// 重新创建虚拟dom对象
_vnode = new VNode(_tag, _data, _value, _type)
_children.forEach((_subvnode) => _vnode.appendChild(combine(_subvnode, data)))
}
return _vnode
}
// TODO:#0.4 函数重写
const ARRAY_METHOD = ['push', 'pop', 'shift', 'unshift', 'reverse', 'splice', 'sort']
// 原型继承 array_method.__proto__ 指向 Array.prototype
const array_method = Object.create(Array.prototype)
// TODO:数组部分方法改造成响应式 方法重写(拦截)
ARRAY_METHOD.forEach((key) => {
array_method[key] = function () {
// 方法重写(拦截)
// 将新添加的数据进行响应式化
for (let i = 0; i < arguments.length; i++) {
observe(arguments[i])
}
// 执行数组原型上的方法
return Array.prototype[key].apply(this, arguments)
}
})
// #5.2 定义得到响应式数据的方法
function defineReactiveProperty(target, key, value, enumerable) {
// 拿到MyVue实例对象
let that = this
// TODO:对非数组的引用类型进行拦截,进行响应式注册
if (typeof value === 'object' && value != null) {
observe(value, that)
}
Object.defineProperty(target, key, {
configurable: true, // 可配置的
enumerable: !!enumerable, // 是否可枚举
get() {
console.log('调用:' + key)
return value
},
set(newVal) {
console.log('设置:' + key + '-->' + newVal)
// ! 新添加的值响应式化
if (typeof newVal === 'object' && newVal != null) {
observe(newVal, that)
}
value = newVal
// 数据修改则直接更新
// TODO:这里省略了watcher
that.mountComponent()
}
})
}
// #5.1 递归实现所有数据的响应式注册
function observe(data, vm) {
if (Array.isArray(data)) {
// 数组方法重写
data.__proto__ = array_method
// 如果是数组则遍历注册,这里是对数组内部进行响应式注册
data.forEach((item) => {
observe(item, vm)
})
} else {
Object.keys(data).forEach((key) => {
let value = data[key]
// 除数组外的数据类型均在这里进行响应式注册
defineReactiveProperty.call(vm, data, key, value, true)
})
}
}
// #5.3 代理 -> 映射
function proxy(target, middle, key) {
Object.defineProperty(target, key, {
enumerable: true,
configurable: true,
get() {
return target[middle][key]
},
set(newVal) {
target[middle][key] = newVal
}
})
}
function MyVue(obj) {
// 习惯: 内部的数据使用下划线 开头, 只读数据使用 $ 开头
// 数据
this._data = obj.data
// TODO:#5 响应式化 + 代理 映射
this.initData()
// 根元素
let el = document.querySelector(obj.el) // vue 是字符串, 这里是 DOM
// 根模板
this._template = el
// 为什么要拿父节点
this._parent = this._template.parentNode
// TODO:#1
this.mount()
}
// 数据初始化
MyVue.prototype.initData = function () {
observe(this._data, this)
Object.keys(this._data).forEach((key) => {
proxy(this, '_data', key)
})
}
MyVue.prototype.mount = function () {
// TODO:#2 需要提供一个 render 方法: 生成 虚拟 DOM
// 这里得到了一个未执行的函数 该函数执行后的结果是合并数据后的VNode
this.render = this.createRenderFunction()
// 视图更新
this.mountComponent()
}
MyVue.prototype.mountComponent = function () {
let mount = () => {
// TODO:#4
// this.render() 会得到数据合并后的VNode
// 更新函数执行得到真实DOM并渲染到页面
this.update(this.render())
}
// TODO:#3
mount.call(this) // 本质应该交给 watcher 来调用
}
// 渲染函数
MyVue.prototype.createRenderFunction = function () {
// #0.1 VNode 模拟 AST 获得VNode
const ast = getVNode(this._template)
return function () {
// #0.3 将 带有 坑的 VNode 转换为 待数据的 VNode
return combine(ast, this._data)
}
}
// 更新视图
MyVue.prototype.update = function (vnode) {
// 转换成真实DOM
let node = parseVNode(vnode)
this._parent.replaceChild(node, document.querySelector('#root'))
}
const options = {
el: '#root',
data: {
name: { firstName: 'wang', lastName: 'wu' },
age: 18,
gender: '男'
}
}
// 创建实例
const mv = new MyVue(options)
</script>
</body>
</html>