Vue源码分析(响应式化)

概念

使用步骤
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

响应式原理
    vue 赋值属性获得属性都是直接使用的Vue实例
    我们在设置属性时,页面的数据要更新

    对于对象可以响应式化  递归
    数组
        push
        pop
        shift
        unshift
        reverse
        sort
        splice
    
    1.在改变数组的数据时,需要发出通知
        1.vue2中的缺陷  数据发生变化 ,设置length 没法通知  (在vue3中使用proxy 解决了问题)
    2.加入的元素应该变成响应式的

    技巧:如果一个函数已经定义了,我们要扩展其功能
        1.使用一个临时的函数名存储函数
        2.重新定义原来的函数
        3.定义扩展的功能
        4.调用临时的函数


    扩展数组方法 push 和 pop
        直接修改prototype  不行  所有数组变化

        修改要进行响应式的数组的原型(__proto__)

        已经将对象改成响应式的了,但是如果直接给对象赋值另外一个对象,那么就不是响应式的



<!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

            reactify(this._data,this)

            this.mount()
        }

        //响应式部分
        let ARRAY_METHOD = [
            'push',
            // 'pop',
            'shift',
            'unshif',
            'revers',
            'sort',
            'splice',
        ]

        let array_methods = Object.create(Array.prototype)

        ARRAY_METHOD.forEach(method => {
            array_methods[method] = function () {
                //将数据进行响应式化
                for (let i = 0; i < arguments.length; i++) {
                    reactify(arguments[i])
                }

                let res = Array.prototype[method].apply(this, arguments)
                return res
            }
        })


        function defineReactive(target, key, value, enumerable) {
            //this指向vue实例
            let that = this
            if (typeof value === 'object' && value != null && !Array.isArray(value)) {
                reactify(value)
            }

            Object.defineProperty(target, key, {
                configurable: true,
                enumerable: !!enumerable,
                get() {
                    return value
                },
                set(newValue) {
                    value = newValue

                    //模板刷新
                    //vue 实例获取  watcher
                    that.mountComponent()

                }
            })
        }

        //将对象响应式花化
        function reactify(o, vm) {
            let keys = Object.keys(o)
            for (let i = 0; i < keys.length; i++) {
                let key = keys[i] //属性名
                let value = o[key]
                if (Array.isArray(value)) {
                    //数组
                    value.__proto__ = array_methods
                    for (let j = 0; j < value.length; j++) {
                        reactify(value[j], vm)
                    }
                } else {
                    //对象或值类型
                    defineReactive.call(vm, o, key, value, true)
                }
            }
        }

        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',
                dates:[
                    {info:'111'},
                    {info:'2222'},
                    {info:'333'}
                ]
            }
        })

        //修改数据模板刷新

    </script>
</body>

</html>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值