Vue源码分析(虚拟DOM与优化)

概念

使用步骤
1.编写 页面 模板
    1.直接在HTML标签中写
    2.使用template
    3.使用单文件(<template>2.创建Vue实例
    1.在Vue 的构造函数中:data,methods,computer,watcher,props,...
3.将Vue挂载到页面中(mount)

数据驱动模型
Vue执行流程
    1.获得模板:模板中有‘坑’
    2.利用Vue构造函数提供的数据来‘填坑’,就可以得到页面显示的'标签'3.替换原来有坑的标签
Vue 利用 我们提供的数据 和 页面的 模板 生成了一个新的HTML标签(node元素),替换到了 页面中 放置模板的位置


虚拟DOM
    目标:
    1. 怎么将真正的DOM转换为虚拟DOM
    2.怎么将虚拟DOM转换为真正的DOM

    思路与深拷贝类似

概念
    1.柯里化: 一个函数原本有多个参数 只传入一个参数生成一个新函数 ,由新函数接受到新的参数运行得到的结构
    2.偏函数: 参考柯里化, 传入一部分参数
    3.高阶函数: 一个函数参数是一个函数,该函数对参数这个函数进行加工,得到一个函数,这个加工用的函数就是高阶函数

    为什么要使用柯里化
    为了提升性能  使用柯里化可以缓存一部分能力

    使用两个例子说明

    1.判断元素

    Vue 本质上是使用HTML的字符串作为模板,将字符串的 模板 转换为AST 再转换为VNode 
        1.模板-AST  
        2.AST-VNode
        3.VNode-DOM

    最消耗性能的
    是模板-AST
    例子 字符串 1 + 2 * ( 3 + 4 )  解析该表达式,得到结果
    一般将此转换为 ‘波兰式’ 表达式 然后用栈进行运算

    在Vue中每一个标签可以是真正的HTML标签,也可以是自定义组件,怎么区分
    在vue源码中,将所有可用的HTML标签 已经存起来,
    假设这里只考虑 几个标签
    ```js
        let tags = 'div,p,a,img'.split(',')
    ```
    一个函数,判断标签名是否为 内置标签
    ```js
    function isHTML( tagName ){
        tagName = tagName.toLowerCase()
        //tags.indexOf(tagName) >-1 return true
        for(let i=0;i<...){
            if(tagName === tags[i])return true
        }
        return false
    }//也可以用indexOf判断
    ```
    模板是任意编写的,可以写的很简单,也可以写的很复杂,indexOf内部也要循环

    如果有6个内置标签 模板有10个,就得循环60次

    使用柯里化
    ```js
     let tags = 'div,p,a,img'.split(',')
        function makeMap( keys ) {
            let set = {}
            keys.forEach(key => {
                set[key] = true
            });
            return function ( tagName ) {
                //!!改为boolean
                return !!set[ tagName.toLowerCase() ]
            }
        }

        let isHTML = makeMap( tags )
        //不用再做循环
    ```
    


    2.虚拟DOM 的render

    vue 项目 模板 转换为AST 转换几次?
        1.页面加载渲染 一次
        2.每一个属性(响应式)数据发生变化 要渲染
        3.watch computed 等等

    render的作用是将 虚拟DOM 转换为 真正的DOM 加载到页面中

    //虚拟DOM 可以降级为AST
    一个项目运行时 模板不会变  抽象语法树不会变

    我们可以将代码优化  将虚拟DOM 缓存起来 生成函数  函数只需要传入数据  得到真正的DOM

    

源码模拟与分析

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app">
        <div>
            <div>h1</div>
            <div>h2</div>
            <div>h3</div>
        </div>
        <div>
            <ul>
                <li>1</li>
                <li>2</li>
                <li>3</li>
            </ul>
        </div>
    </div>
</body>
<script>
    //为什么使用虚拟DOM ? 性能
    //<div> => {tag: 'div' }
    //文本节点  => {tag : undefined , value: '文本节点' }
    //<div title='1'  class='c'>  => {tag : 'div'  data: {title: 1, class : 'c'}}
    //<div><div></div></div> {tag: 'div', children: [ {{tag: 'div'}]}


    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 = {}
            //遍历属性节点 nodeType=2 
            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
    }


    let app = document.querySelector('#app')

    let vapp = getVNode(app)

    console.log( vapp )

    //将VNode 转换为真正的dom
    function parseVNode( vnode) {
        let type = vnode.type
        let _node = null
        if( type === 3){
            return document.createTextNode( vnode.value )
        }else if( type === 1 ){

            _node = document.createElement( vnode.tag )

            //属性
            //data 键值对
            let data = vnode.data
            Object.keys(data).forEach( (key) => {
                let attrName = key
                let attrValue = data[key]
                _node.setAttribute( attrName, attrValue)
            })

            //子元素
            //递归转换子元素 (虚拟dom)
            let children = vnode.children
            children.forEach( subvnode => {
                _node.appendChild(parseVNode(subvnode))
            })
        }
        return _node
    }

    //在真正的vue也是使用 递归+加栈
    let dom = parseVNode(vapp)

    console.log(dom)



</script>
</html>

vue源码 虚拟dom对象

在这里插入图片描述

从new vue 到 虚拟dom 到 渲染

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <div id="app">
        <div>
            <div>{{name}}</div>
            <div>{{age}}</div>
            <div>{{sex}}</div>
        </div>
        <div>
            <ul>
                <li>1</li>
                <li>2</li>
                <li>3</li>
            </ul>
        </div>
    </div>

    <script>

        //虚拟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 将该函数 当作complier函数 
        function getVNode(node) {
            let nodeType = node.nodeType
            let _vnode = null
            if (nodeType === 1) {
                //元素
                let nodeName = node.nodeName
                let attrs = node.attributes
                let _attrObj = {}
                //遍历属性节点 nodeType=2 
                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
        }

        //根据路径访问成员
        function getValueByPath(obj, path) {
            let paths = path.split('.')
            //先取得 xxx 再取得结果中的yyy
            let res = obj
            let prop
            while (prop = paths.shift()) {
                res = res[prop]
            }

            return res

        }

        //将带坑的vdom 与数据data结合 得到 带有数据的vdom  模板 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

            let rex = /\{\{(.+?)\}\}/g
            if (_type === 3) {
                _value = _value.replace(rex, function (_, g) {
                    let path = g.trim()//双花括号里面的东西
                    let value = getValueByPath(data, path)
                    //将值替换{{}}
                    return value
                })
                _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
        }


        //将VNode 转换为真正的dom
        function parseVNode(vnode) {
            let type = vnode.type
            let _node = null
            if (type === 3) {
                return document.createTextNode(vnode.value)
            } else if (type === 1) {

                _node = document.createElement(vnode.tag)

                //属性
                //data 键值对
                let data = vnode.data
                Object.keys(data).forEach((key) => {
                    let attrName = key
                    let attrValue = data[key]
                    _node.setAttribute(attrName, attrValue)
                })

                //子元素
                //递归转换子元素 (虚拟dom)
                let children = vnode.children
                children.forEach(subvnode => {
                    _node.appendChild(parseVNode(subvnode))
                })
            }
            return _node
        }

        function MVue(options) {
            this._options = options
            this._data = options.data
            let elm = document.querySelector(options.el) //vue是字符串
            this._template = elm
            this._parent = elm.parentNode

            this.mount()
        }

        MVue.prototype.mount = function () {
            //需要提供render方法:生成虚拟dom
            this.render = this.createRenderFn()
            this.mountComponent()
        }

        //生成render函数 缓存抽象语法树  使用虚拟dom模拟
        MVue.prototype.createRenderFn = function () {
            let ast = getVNode(this._template)
            //将AST + data =》 vnode
            //带坑的 vnode + data =》 含有数据的vnode
            return function render() {
                //将带坑的vdom 转换为带数据的vdom
                let _temp = combine(ast, this._data)
                return _temp
            }
        }

        MVue.prototype.mountComponent = function () {
            //执行mountcomponent()
            let mount = () => { //函数 this 默认是全局  
                this.update(this.render())
            }

            mount.call(this) //本质上交给watcher来调用


            //为什么不直接
            //this.update( this.render()) //使用发布订阅模式,渲染和计算的行为应该交给watcher完成
        }

        //将虚拟dom 渲染到页面中  diff算法
        MVue.prototype.update = function (VNode) {
            //简化 直接生成HTML dom  replaceChild 到页面
            //父元素。replaceChild(新元素,旧元素)
            let realDom = parseVNode(VNode)
            this._parent.replaceChild( realDom, document.querySelector('#app'))
            //每次将页面中的dom 全部替换
        }

        //在真正的vue中使用了二次提交的 设计结构
        //1.在页面中的dom  和虚拟dom 一一对应
        //2.在每次数据变化时, 生成一个新的vdom(render)
        //3.将开始真实dom 对应的vdom 与 新的vdom 比较  就是diff算法
        //4.相同的不变  不同的更新到 真实dom 对应的vdom 上  也就更新真正的dom(update)


        let app = new MVue({
            el: '#app',
            data: {
                name: 'xa',
                age: 12,
                sex: 'man'
            }
        })
    </script>
</body>

</html>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值