Vue原理总结

359 阅读5分钟

Vue原理

组件化

背景

首先很久之前就有组件化的概念, 使用 asp jsp php 就可以。

node.js中也有类似的组件化。

Vue和React搬过来,做了一个创新:数据驱动视图。

传统组件它只是静态的渲染,也就是说我们想显示谁的个人信息, 就要在后端拼接完数据去渲染。 渲染完页面就形成了,不会再去改变了。你再需要去改需要自己去操作 DOM。所以当时 jQuery 是很流行的。

vue 可以通过 MVVM 模式执行的数据驱动视图。我们不再去操作 DOM, 我们想改什么地方,直接去改 vue 里的数据就可以了。然后 vue框架本身帮我们根据数据重新渲染视图。这一点是跟传统组件本质的区别。也正是因为这一点,让我们做 vue 开发的时候, 更加关注于数据。所谓数据也就是更加关注业务逻辑。而不是一直操作 DOM。

数据驱动视图(MVVM、setState)

M: Model 层 (vuex,data)

V:View 层 (视图)

VM: ViewModel 层

view ------- viewmodel ------- model

View 层通过 ViewModel 和 Model 做关联,像监听事件,监听指令等等。

在 Model 修改的时候,就能立刻执行 View 的渲染,View 层里面有什么点击事件,各种 DOM 事件监听的时候, 都可以去修改Model 这一层的数据。

所以说这就是数据驱动视图。通过修改 Model 数据去驱动视图 View。这个视图不用我们亲自操作。

响应式

组件data的数据一旦变化,立刻触发视图的更新

核心API - Object.defineProperty 基本用法

const data = {}
const name = 'zhangsan'
Object.defineProperty(data, "name", {
    get: function () {
        console.log('get')
        return name
    },
    set: function(newVal) {
        console.log('set')
        name = newVal
    }
})

监听对象、数组

const data = {
    name: 'XXX',
    info: {
        age: '18'
    }
}

// 自定义数组原型,防止污染全局数组原型
const oldArrayProperty = Array.prototype
const arrProto = Object.create(oldArrayProperty)
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(method => {
    arrProto[method] = function () {
        updateView()
        oldArrayProperty[method].call(this, ...arguments)
    }
})

observer(data)
const updateView = () => {
    //更新view
}

const observer = (target) => {
    if (typeof target !== 'object' || target === null) {
        return target
    }

    Arrsy.isArray(target) && target._proto_ = arrProto
    for (let key in target) {
        defineReactive(target, key, target[key])
    }
}
const defineReactive = (target, key, value) => {
    // 深度监听
    observer(value)
    // vue2的核心api
    Object.defineProperty(target, key, {
        get () {
            return value
        },
        set (newVal) {
            if (newVal !== value) {
                value = newValue
                // 直接赋值对象等需要深度监听的数据
                observer(newValue)
                // 更新视图
                updateView()
            }
        }
    })
}

Object.defineProperty的一些缺点 1、深度监听,需要递归到底。一次向计算量大

2、新增删除属性无法监听(Vue.set Vue.delete)

3、无法原生监听数组,需要特殊处理,无法监听直接修改下标

vdom和diff

vdom

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

Dom操作非常耗费性能

vdom极大可能减少dom操作,优化

树diff的时间复杂度O(n^3) 1、遍历Tree1 2、遍历Tree2 3、排序

优化时间复杂度到O(n) 1、只比较同一层级,不跨级比较 2、tag不相同,则直接删掉重建,不在深度比较 3、tag和key,两者都相同,则认为是相同节点,不再深度比较

diff算法

diff算法是比较两个vnode,计算出最小的变更,以便减少DOM操作次数,提高性能。

原理

只比较同级,不跨域比较

如果tag不相同,直接删除重建,不再深度比较

如果tag和key都相同,默认是一样的节点,也不再深度比较

流程

首先,它会判断是否是首次渲染,因为如果是首次渲染,没有旧的vnode,不需要比较,直接渲染就可以了。

在非首次渲染,首先比较两个节点是否一样。如果不一样,直接删除重建;如果一样,就需要进行vnode比较、就是比较children。

如果新节点没有文本节点,删除旧节点的文本节点;如果有文本节点,替换掉旧的文本节点。

如果只有新节点有子节点,直接插入;如果只有旧节点有子节点,直接删除。 最后就是,新旧节点都有子节点的情况。

这时候会遍历新节点的children,每个新的子节点都需要在旧的children里面进行寻找,找一个一样的节点。

如果没有找到,新的子节点直接插入;如果找到了,这两个节点再进行vnode比较。 也可以简单的理解为,如果没有是重新渲染,如果有的话,直接把旧的子节点挪过来用就可以了。

diff算法源码

h:h函数返回的是vnode对象,可以理解成最后返回的是一个处理好的包含挂载节点的dom对象结构吗,在根据这个结构去生成真实dom

patchVnode

addVnodes

removeVnodes

updateChildren(key的重要性)

模板编译

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

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

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

1、vue template complier将模板编译为render函数

const compiler = required('vue-template-compoler')

const template = `<p>{{message}}</p>`

const res = compiler.compile(template)

console.log(res.render)

2、执行render函数生成vnode

3、基于vnode再执行patch和diff

4、使用webpack vue-loader会在开发环境下编译模板

流程

1、响应式:监听data属性 get、set

2、模板编译: 模板到render函数,再到vdom

2、vdom:patch(element, vnode)、patch(vnode, oldvnode)

渲染过程

初次渲染过程

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

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

3、执行render函数,生成vdom,patch(element, vnode)

render函数执行时收集依赖

更新过程

1、修改data,触发setter

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

3、patch(vnode, oldvnod)

异步渲染

$nextTick、汇总data的修改,一次性更新视图、减少DOM操作次数,提高性能