此demo是学习vue的时候,尝试写的一个框架
- 为了能够触发响应式收集,需要将dom的生成改造成js渲染。
- 需要将数据类型更改为get/set模式,进行数据监听
将dom的生成改造成js渲染
详情请看裕波的一篇react文章,dom生成代码源自此
const attributeExceptions = [
`role`,
];
function appendText(el, text) {
console.log('appendText===', text)
const textNode = document.createTextNode(text);
el.appendChild(textNode);
}
function appendArray(el, children) {
children.forEach((child) => {
if (Array.isArray(child)) {
appendArray(el, child);
} else if (child instanceof window.Element) {
el.appendChild(child);
} else if (typeof child === `string` || typeof child === `number`) {
appendText(el, child);
}
});
}
function setStyles(el, styles) {
if (!styles) {
el.removeAttribute(`styles`);
return;
}
Object.keys(styles).forEach((styleName) => {
if (styleName in el.style) {
el.style[styleName] = styles[styleName]; // eslint-disable-line no-param-reassign
} else {
console.warn(`${styleName} is not a valid style for a <${el.tagName.toLowerCase()}>`);
}
});
}
function makeElement(type, textOrPropsOrChild, ...otherChildren) {
console.log('makeElement----', textOrPropsOrChild)
const el = document.createElement(type);
if (Array.isArray(textOrPropsOrChild)) {
appendArray(el, textOrPropsOrChild);
} else if (textOrPropsOrChild instanceof window.Element) {
el.appendChild(textOrPropsOrChild);
} else if (typeof textOrPropsOrChild === `string` || typeof textOrPropsOrChild === `number`) {
console.log('makeElement===', textOrPropsOrChild)
appendText(el, textOrPropsOrChild);
} else if (typeof textOrPropsOrChild === `object`) {
Object.keys(textOrPropsOrChild).forEach((propName) => {
if (propName in el || attributeExceptions.includes(propName)) {
const value = textOrPropsOrChild[propName];
if (propName === `style`) {
setStyles(el, value);
} else if (value) {
el[propName] = value;
}
} else {
console.warn(`${propName} is not a valid property of a <${type}>`);
}
});
}
if (otherChildren) appendArray(el, otherChildren);
return el;
}
const a = (...args) => makeElement(`a`, ...args);
const button = (...args) => makeElement(`button`, ...args);
const div = (...args) => makeElement(`div`, ...args);
const h1 = (...args) => makeElement(`h1`, ...args);
const header = (...args) => makeElement(`header`, ...args);
const p = (...args) => makeElement(`p`, ...args);
const span = (...args) => makeElement(`span`, ...args);
数据的绑定
- 将数据转换为get/set属性
- 将一个行为转化为watcher对象
- 在watcher构造函数中触发get,将此watcher对象push到改属性的dep中
- 修改数据,触发set函数,触发该属性dep中的watcher对象中的回调函数
//
function Dep() { //让每个被监听的属性,有一个自己的watcher容器
this.subs = [] //装的全部是watch对象(watcher:{exp,fn})
this.subsId = new Map() //去重的容器,因为obj必须是字符串,所以用map对象
this.addSub = function () {
console.log('Dep中调用addSub')
if (!this.subsId.has(Dep.target)) { //去重
this.subs.push(Dep.target)
this.subsId.set(Dep.target, 1)
}
}
this.notify = function () {
console.log('Dep中调用notify', this.subs)
for (let i = 0; i < this.subs.length; i++) {
this.subs[i].fn()
}
}
}
Dep.target = null //初始化 Dep.target
function oberserver(obj) { //将属性转换为get/set,并通过闭包创建自己的dep对象
var _obj = {}//用来存值的仓库,如果直接用obj存值会造成内存溢出
Object.keys(obj).forEach(key => {
if (Object.prototype.toString.call(obj[key]) == '[object Object]') { //如果是对象,进行深度递归
oberserver(obj[key])
} else {
_obj[key] = obj[key]
let dep = new Dep()
Object.defineProperty(obj, key, {
get() {
//在这里,对事件进行添加
console.log(`${key}数据get`)
dep.addSub()
return _obj[key]
},
set(newValue, old) {
console.log(`${key}数据set`)
if (Object.prototype.toString.call(newValue) == '[object Object]') { //如果设置的新值是对象,进行深度递归
oberserver(newValue)
} else {//如果是值,进行赋值操作
_obj[key] = newValue
}
//在这里,对事件进行执行
dep.notify()
}
})
}
})
}
function parseProperty(e) { //解析a.c这种类型的传入
if (e.indexOf('.')) {
var obj = data
let arr = e.split('.').forEach(key => {
obj = obj[key]
})
} else {
data[e]
}
}
function Watcher(exp, fn = '', isRender = false) { //exp是需要观察的属性,fn是回调,
this.exp = exp
this.fn = fn
pushTarget(this)
if (typeof exp == 'string') {//如果只是data的某个属性
console.log('watcher中调用string')
parseProperty(exp)
} else if (isRender) {//如果是回调函数,则执行dom替换操作,在createEl.js中
console.log('watcher中调用isRender')
this.fn = exp()
} else { //例如vue的computed api,
console.log('watcher中调用其他')
exp()
}
}
function pushTarget(watch) {
Dep.target = watch
console.log('设置watcher对象为全局', Dep.target)
}
使用
第一次执行渲染函数后,应该有一个闭包,记录改函数生成的elememt元素,因此返回一个replace函数,下次set的时候,执行这个replace函数。
function render() {
var child = h1({ className: `header` }, data.d)
document.body.appendChild(child)
return function () {
console.log('第二次render', data.d)
let c = h1({ className: `header` }, data.d)
document.body.replaceChild(c, child)
child = c
}
}
var data = { a: { b: 1, c: 2 }, d: 'Hello, world.' }
oberserver(data)
new Watcher(render, '', true)
for (let i = 0; i < 10; i++) {
setTimeout(function () { data.d = i }, i * 200)
}