• vue源码之数据驱动


    上一次我们简单实现了HTML模拟Vue实现数据渲染,发现还是有很多问题的,这次继续进行优化:

    • 代码没有整合
    • 只考虑了但属性,而Vue中大量使用了层级( {{ child.name }} )
    • Vue使用的是 虚拟DOM

    下面将通过这三个内容简单说明Vue的实现

    页面节点:

    <body>
        <div id="root">
            <div>
                <p>{{ name }} - {{ message }}</p>
            </div>
            <div>{{ name }}</div>
            <div>{{ message }}</div>
        </div>
    </body>

    一、封装代码

    首先,我们先将之前代码进行封装

    let rkuuohao = /{{(.+?)}}/g;
    
    function JGVue(options) {
        // 内部数据使用下划线,只读数据使用$开头
        this._data = options.data
        this._el = options.el
    
        // 准备工作( 准备模版 )
        this._templateDOM = document.querySelector(this._el)
        this._parent = this._templateDOM.parentNode
        // 渲染工作
        this.reder()
    
    }
    
    function compiler(tmpNode, data) {
        let childNodes = tmpNode.childNodes
        for (let i = 0; i < childNodes.length; i++) {
            let type = childNodes[i].nodeType // 1 元素,3文本节点
    
            if (type === 3) {
                // 文本节点可以判断里面是否含有 {{}} 插值
                let txt = childNodes[i].nodeValue // 该属性只有文本节点才有效
                txt = txt.replace(rkuuohao, (_, g) => {
                    let key = g.trim()
                    let value = data[key]
                    return value
                })
                childNodes[i].nodeValue = txt
    
            } else if (type === 1) {
                // 元素,考虑有没有子元素
                compiler(childNodes[i], data)
            }
        }
    }
    
    // 将模版和数据 => 得到html,加载到页面中
    JGVue.prototype.reder = function () {
        this.compiler()
    }
    
    // 编译,将模版与数据结合
    JGVue.prototype.compiler = function () {
        let realHTMLDOM = this._templateDOM.cloneNode(true) // 用 模版 拷贝得到一个 准DOM
        compiler(realHTMLDOM, this._data)
        this.update(realHTMLDOM)
    }
    
    // 将DOM对元素,放入页面中
    JGVue.prototype.update = function (real) {
        this._parent.replaceChild(real, document.querySelector("#root"))
    }
    
    let app = new JGVue({
        el: '#root',
        data: {
            name: 'zhou',
            message: 'info'
        }
    })

    二、实现层级访问

    所谓层级,就是通过访问对象的形式( 数据为{info:{infoData:{name},txt:'测试'}},调用通过 {{ info.infoData.name }} {{ info.txt }} )来进行调用。

    // 使用 ‘xxx.yyy.zzz’ 可以访问某一个对象 
    function getValueByPath(obj, path) {
        let paths = path.split('.') // ['xxx','yyy','zzz']
        // 先得到 obj.xxx,再得到结果中的 yyy,再得到结果中的zzz
        // let res = null;
        // res = obj[paths[0]]
        // res = obj[paths[1]]
        // res = obj[paths[2]]
    
        let res = obj;
        let prop;
        while (prop = paths.shift()) {
            res = res[prop]
        }
        return res
    }

    在封装中调用

    function compiler(tmpNode, data) {
        let childNodes = tmpNode.childNodes
        for (let i = 0; i < childNodes.length; i++) {
            let type = childNodes[i].nodeType // 1 元素,3文本节点
    
            if (type === 3) {
                // 文本节点可以判断里面是否含有 {{}} 插值
                let txt = childNodes[i].nodeValue // 该属性只有文本节点才有效
                txt = txt.replace(rkuuohao, (_, g) => {
                    let path = g.trim()
                    let value = getValueByPath(data, path) // 深度获取对象值
                    return value
                })
                childNodes[i].nodeValue = txt
    
            } else if (type === 1) {
                // 元素,考虑有没有子元素
                compiler(childNodes[i], data)
            }
        }
    }

    在 compiler() 中调用 getValueByPath() ,传入数据data和路径path,实现深度取值。

    三、使用虚拟DOM

    使用虚拟DOM,那么虚拟DOM的结构又是什么样的呢?先简单说几种

    <div / > => { tag: 'div' }
    文本节点 => { tag: undefined, value: '文本节点' }
    <div title="1" class="c" /> => { title: '1', class: 'c' }
    <div><div/ ></div> => { tag: 'div', children:[ { tag: 'div' } ] }

    下面针对几种情况实现虚拟DOM

    首先创建节点类

    class VNode {
        constructor(tag, data, value, type) {
            this.tag = tag && tag.toLowerCase()
            this.data = data
            this.value = value
            this.type = type
            this.children = []
        }
    
        appendChild(vnode) {
            this.children.push(vnode)
        }
    }
    使用递归遍历DOM元素,生成虚拟DOM
    // Vue源码使用栈结构,使用栈存储父元素来实现递归生成
    function getVNode(node) {
        let nodeType = node.nodeType
        let _vnode = null
        if (nodeType === 1) {
            // 元素
            let nodeName = node.nodeName
            let attrs = node.attributes
            let _attrObj = {}
            for (let i = 0; i < attrs.length; i++) {
                _attrObj[attrs[i].nodeName] = attrs[i].nodeValue
            }
            _vnode = new VNode(nodeName, _attrObj, undefined, nodeType)
    
            // 考虑 node 的子元素
            let childNodes = node.childNodes
            for (let i = 0; i < childNodes.length; i++) {
                _vnode.appendChild(getVNode(childNodes[i])) // 递归
            }
    
        } else if (nodeType === 3) {
            // 文本
            _vnode = new VNode(undefined, undefined, node.nodeValue)
        }
        return _vnode
    }

    虚拟 DOM 转换为真正的 DOM

    function parseVNode(vnode) {
        // 创建真实DOM
        let type = vnode.type
        if (type === 3) {
            return document.createTextNode(vnode.value) // 创建文本节点
        } else if (type === 1) {
            let _node = document.createElement(vnode.tag)
    
            // 属性
            let data = vnode.data
            Object.keys(data).forEach(key => {
                let attrName = key
                let attrValue = data[key]
                _node.setAttribute(attrName, attrValue)
            })
    
            // 子元素
            let children = vnode.children
            children.forEach(subvnode => {
                _node.appendChild(parseVNode(subvnode)); // 递归转换子元素 ( 虚拟DOM )
    
            })
    
            return _node
        }
    }

    到这里,虚拟DOM已经实现,下面来测试

    let root = document.querySelector("#root")
    let vroot = getVNode(root)
    console.log(vroot, 'vroot');

    let dom = parseVNode(vroot)
    console.log(dom);

    现在,我们说的三个问题已经完成了,下面将代码整合,附上完整代码

    /********  虚拟DOM - start  *********/
    class VNode {
        constructor(tag, data, value, type) {
            this.tag = tag && tag.toLowerCase()
            this.data = data
            this.value = value
            this.type = type
            this.children = []
        }
    
        appendChild(vnode) {
            this.children.push(vnode)
        }
    }
    
    // 使用递归便利DOM元素,生成虚拟DOM
    // Vue源码使用栈结构,使用栈存储父元素来实现递归生成
    function getVNode(node) {
        let nodeType = node.nodeType
        let _vnode = null
        if (nodeType === 1) {
            // 元素
            let nodeName = node.nodeName
            let attrs = node.attributes
            let _attrObj = {}
            for (let i = 0; i < attrs.length; i++) {
                _attrObj[attrs[i].nodeName] = attrs[i].nodeValue
            }
            _vnode = new VNode(nodeName, _attrObj, undefined, nodeType)
    
            // 考虑 node 的子元素
            let childNodes = node.childNodes
            for (let i = 0; i < childNodes.length; i++) {
                _vnode.appendChild(getVNode(childNodes[i])) // 递归
            }
    
        } else if (nodeType === 3) {
            // 文本
            _vnode = new VNode(undefined, undefined, node.nodeValue, nodeType)
        }
        return _vnode
    }
    
    // 将 vnode 转换为真正的 DOM
    function parseVNode(vnode) {
        // 创建真实DOM
        let type = vnode.type
        if (type === 3) {
            return document.createTextNode(vnode.value) // 创建文本节点
        } else if (type === 1) {
            let _node = document.createElement(vnode.tag)
    
            // 属性
            let data = vnode.data
            Object.keys(data).forEach(key => {
                let attrName = key
                let attrValue = data[key]
                _node.setAttribute(attrName, attrValue)
            })
    
            // 子元素
            let children = vnode.children
            children.forEach(subvnode => {
                _node.appendChild(parseVNode(subvnode)); // 递归转换子元素 ( 虚拟DOM )
    
            })
    
            return _node
        }
    }
    /********  虚拟DOM - end  *********/
    
    let rkuohao = /{{(.+?)}}/g;
    
    function JGVue(options) {
        // 内部数据使用下划线,只读数据使用$开头
        this._data = options.data
        this._el = options.el
    
        // 准备工作( 准备模版 )
        this._templateDOM = document.querySelector(this._el)
        this._parent = this._templateDOM.parentNode
        // 渲染工作
        this.reder()
    
    }
    // 根据路径访问对象成员
    function getValueByPath(obj, path) {
        let paths = path.split('.')
    
        let res = obj;
        let prop;
        while (prop = paths.shift()) {
            res = res[prop]
        }
        return res
    }
    
    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(rkuohao, (_, g) => {
                return getValueByPath(data, g.trim())
            })
            _vnode = new VNode(_tag, _data, _value, _type)
        } else if (_type === 1) {
            // 元素
            _vnode = new VNode(_tag, _data, _value, _type)
            _children.forEach(_subvnode => _vnode.appendChild(combine(_subvnode, data)));
        }
    
        return _vnode
    }
    
    // 将模版和数据 => 得到html,加载到页面中
    JGVue.prototype.reder = function () {
        this.compiler()
    
    }
    
    // 编译,将模版与数据结合
    JGVue.prototype.compiler = function () {
        let ast = getVNode(this._templateDOM)
        let _tmp = combine(ast, this._data)
        this.update(_tmp)
    }
    
    // 将DOM对元素,放入页面中
    JGVue.prototype.update = function (real) {
        let realDOM = parseVNode(real)
        this._parent.replaceChild(realDOM, document.querySelector("#root"))
    }
    
    let app = new JGVue({
        el: '#root',
        data: {
            name: 'zhou',
            message: 'info'
        }
    })

     注意:这里对“将虚拟DOM与数据结合”方法进行重新封装,并有 compiler 更名为 combine 。

  • 相关阅读:
    Mysql Dump
    Amazon的Dynamo中用到的几种技术
    一致性哈希
    eclipse下提交job时报错mapred.JobClient: No job jar file set. User classes may not be found.
    eclipse中java heap space问题解决方法
    hadoop的启动步骤
    java根据经纬度计算距离
    eclipse创建Enterprise Application project没有web.xml
    cygwin+hadoop安装步骤和问题
    java程序员修炼
  • 原文地址:https://www.cnblogs.com/cap-rq/p/14200217.html
Copyright © 2020-2023  润新知