简易 Vue 构建--篇四

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>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值