手撸一个mvvm

402 阅读3分钟

此demo是学习vue的时候,尝试写的一个框架

源码仓库

  1. 为了能够触发响应式收集,需要将dom的生成改造成js渲染。
  2. 需要将数据类型更改为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);

数据的绑定

  1. 将数据转换为get/set属性
  2. 将一个行为转化为watcher对象
  3. 在watcher构造函数中触发get,将此watcher对象push到改属性的dep中
  4. 修改数据,触发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)
    }