vue 原理总结

71 阅读6分钟

数据驱动视图

MVVM:model(数据) + view(视图) + view model(view 和 model 层的连接,比如:事件)

MVVM就是将其中View的状态和行为抽象化,其中ViewModel将试图(即View)和业务逻辑分开,它可以去除Model的数据的同时帮忙处理View中由于需要展示内容而涉及的业务逻辑。

MVVM采用:双向数据绑定

View中数据变化将自动反映到Model上,反之,Model中数据变化也将会自动展示在页面上。

ViewModel就是View和Model的桥梁。

ViewModel负责把Model的数据同步到View显示出来,还负责把View的修改同步回到Model。

3b2deed9e76142798a44ee3bbc33f39b.png

Vue 响应式: Vue通过defineProperty完成了Data中所有数据的代理,当数据触发get查询时,会将当前的Watcher对象加入到依赖收集池Dep中,当数据Data变化时,会触发set通知所有使用到这个Data的Watcher对象去update视图。

Vue 响应式:

**核心API - Object.definProperty
**

    Object.definProperty 的一些缺点(vue3.0 启用 Proxy)

Object.definProperty 实现响应式:

    监听对象,监听数组

    复杂对象,深度监听

    缺点

Object.definProperty 缺点:

    深度监听,需要递归到底,一次性计算量大

    无法监听新增属性/删除属性(使用 Vue.set Vue.delete)

    无法原生监听数组,需要特殊处理

// 监听数据变化。步骤:**1. 对象监听;2. 深度监听(递归);3. 监听数组;**

// 触发更新视图
function updateView() {
    console.log('视图更新')
}

// 3.2 数组方法处理,这里稍微复杂一点
const oldArrayPrototype = Array.prototype
const arrProto = Object.create(oldArrayPrototype); // 创建新对象,原型指向 oldArrayPrototype ,再扩展属性不会影响 oldArrayPrototype
['push''pop''shift''unshift''splice'].forEach(method => {
    // 重新定义这些新方法,监听数组变化
    arrProto[method] = function () {
        updateView() // 触发更新视图
        oldArrayPrototype[method].call(this, ...arguments// 调用原有方法
    }
})


// 2.2 重新定义属性,监听起来
function defineReactive(target, key, value) {
    // 2.3 value 如果是对象,需要递归监听,即深度监听 —— 注意这里的递归,而且是在数据监听时,一次性递归完成!!!
    // (如果 value 不是对象,observer 中会做判断)
    observer(value)

    // 核心 API
    Object.defineProperty(target, key, {
        get() {
            return value
        },
        set(newValue) {
            if (newValue !== value) {
                // 2.3 设置的值,也需要监听起来,即深度监听
                observer(newValue)

                // 设置新值
                value = newValue // 注意,value 一直在闭包中,此处设置完之后,再 get 时也是会获取最新的值!!!

                // 触发更新视图
                updateView()
            }
        }
    })
}

// 2.1 监听对象属性
function observer(target) {
    if (typeof target !== 'object' || target === null) {
        // 不是对象或数组
        return target
    }

    if (Array.isArray(target)) {
        // 重写数组的原型
        target.__proto__ = arrProto
    }

    // 重新定义各个属性
    for (let key in target) { // 遍历对象或者数组
        defineReactive(target, key, target[key])
    }
}

// 1. 准备数据
const data = {
    name'zhangsan',
    info: {
        address'北京'
    },
    nums: [102030]
}

// 2. 监听数据
observer(data)

// 测试
data.name = 'lisi'
data.info.address = '上海' // 深度监听
data.x = '100' // 新增属性,监听不到 —— 所以有 Vue.set
delete data.name // 删除属性,监听不到 —— 所有已 Vue.delete
data.nums.push(4// 监听数组

vdom 是实现 vue 和 React 的重要基石

diff 算法是 vdom 中最核心、最关键的部分

解决方案 - vdom:

有了一定复杂度,想减少计算次数比较难

能不能把计算更多的转移为 JS 计算,因为 JS 执行速度很快

vdom - 用 JS 模拟 DOM 结构,计算出最小的变更,操作 DOM 

snabbdom:

    简洁强大的 vdom 库,易学易用

    Vue 参考它实现的 vdom 和 diff

snabbdom 重点总结:h 函数;vnode 数据结构;patch 函数。

vdom 总结:

    用 JS 模拟 DOM 结构(vnode)

    新旧 vnode 对比,得出最小的更新范围,最后更新 DOM

    数据驱动视图的模式下,有效控制 DOM 操作

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <div id="container"></div>
    <button id="btn-change">change</button>

    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-class.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-props.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-style.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-eventlisteners.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/h.js"></script>
    <script type="text/javascript">
        const snabbdom = window.snabbdom
        // 定义关键函数 patch
        const patch = snabbdom.init([
            snabbdom_class,
            snabbdom_props,
            snabbdom_style,
            snabbdom_eventlisteners
        ])

        // 定义关键函数 h
        const h = snabbdom.h

        // 原始数据
        const data = [
            {
                name'张三',
                age'20',
                address'北京'
            },
            {
                name'李四',
                age'21',
                address'上海'
            },
            {
                name'王五',
                age'22',
                address'广州'
            }
        ]
        // 把表头也放在 data 中
        data.unshift({
            name'姓名',
            age'年龄',
            address'地址'
        })

        const container = document.getElementById('container')

        // 渲染函数
        let vnode
        function render(data) {
            const newVnode = h('table', {}, data.map(item => {
                const tds = []
                for (let i in item) {
                    if (item.hasOwnProperty(i)) {
                        tds.push(h('td', {}, item[i] + ''))
                    }
                }
                return h('tr', {}, tds)
            }))

            if (vnode) {
                // re-render
                patch(vnode, newVnode)
            } else {
                // 初次渲染
                patch(container, newVnode)
            }

            // 存储当前的 vnode 结果
            vnode = newVnode
        }

        // 初次渲染
        render(data)


        const btnChange = document.getElementById('btn-change')
        btnChange.addEventListener('click'() => {
            data[1].age = 30
            data[2].address = '深圳'
            // re-render
            render(data)
        })

    </script>
</body>
</html>

diff 算法:diff算法可以看作是一种对比算法,对比的对象是新旧虚拟Dom。顾名思义,diff算法可以找到新旧虚拟Dom之间的差异,但diff算法中其实并不是只有对比虚拟Dom,还有根据对比后的结果更新真实Dom。

diff算法就是一个 patch —> patchVnode —> updateChildren —> patchVnode —> updateChildren —> patchVnode这样的一个循环递归的过程。

 旧children 有,新children无,移出旧children;旧text 有,新text无,移出旧text

key的主要作用其实就是对比两个虚拟节点时,判断其是否为相同节点。加了key以后,我们可以更为明确的判断两个节点是否为同一个虚拟节点,是的话判断子节点是否有变更(有变更更新真实Dom),不是的话继续比。如果不加key的话,如果两个不同节点的标签名恰好相同,那么就会被判定为同一个节点(key都为undefined),结果一对比这两个节点的子节点发现不一样,这样会凭空增加很多对真实Dom的操作,从而导致页面更频繁得进行重绘和回流。

所以我认为合理利用key可以有效减少真实Dom的变动,从而减少页面重绘和回流的频率,进而提高页面更新的效率。

patch的主要作用是对比两个虚拟Dom的根节点,并根据对比结果操作真实Dom。

patchVnode用来比较两个虚拟节点的子节点并更新其子节点对应的真实Dom节点。(addVnodes  removeVnodes)

updateChildren

with 语法:

     改变 {} 内自由变量的查找规则,当做obj 属性来查找

    如果找不到匹配的 obj 属性,就会报错

    with 要慎用,它打破了作用域规则,易读性变差

编译模板:(vue-template-compiler)

    模板不是 html ,有指令、插值、JS 表达式,能实现判断、循环

    html 是标签语言,只有 JS 才能实现判断、循环(图灵完备的)

    因此,模板一定是转换为某种 JS 代码,即编译模板

    模板编译为 render 函数,执行 render 函数返回 vnode

    基于 vnode 再执行 patch 和 diff 

    使用 webpack vue-loader ,会在开发环境下编译模板(重要)

组件 渲染/更新 过程:

初次渲染过程:

        1,解析模板为 render 函数(或在开发环境已完成,vue-loader)

        2,触发响应式,监听 data 属性 getter setter

        3,执行 render 函数,生成 vnode,patch (elem,vnode)

    更新过程:

        1,修改 data ,触发 stter (此前在 getter 中已被监听)

        2,重新执行 render 函数,生成 newVnode

        3,patch (vnode,newVnode)

    异步渲染:

        1,回顾 $nextTick

        2,汇总 data 的修改,一次性更新视图

        3,减少 DOM 操作次数,提高性能

hash 的特点:

    hash 变化会触发网页跳转,即浏览器的前进、后退

    hash 变化不会刷新页面,SPA 必需的特点

    hash 永远不会提交 server 端(前端自生自灭)

H5 history :

    用 URL 规范的路由,但跳转时不刷新页面

    history.pushState

    window.onpopstate

两者选择:

    to B 的系统推荐用 hash ,简单易用,对 url 规范不敏感

    to C 的系统,key考虑选择 H5 history ,但需要服务端支持

    能选择简单的,就别用复杂的,要考虑成本和收益